🌸🌸🌸블로그 글은 복습 겸 gitbook의 순서에 따라 번역 및 관련 내용에 JK피셜을 달아 정리할 예정이다. 말그대로 JK피셜인 칸큼 100%맞는 말은 아닐 수 있다는 거 참고해주길 바란다.
alarm clock은 devices 폴더와 threads폴더에 있는 파일을수정하는 것부터 시작한다.
자료는 32bit pintOS를 설명하는 한양대 ppt와 kaist OS lab ppt를 참고했다. 하지만 우리의 pintOS는 64bit로 바뀌었으니 main을 gitbook으로 삼아 공부하는 것을 추천한다.(처음부터 그렇게 안해서 시간낭비 후회하는 1인😂)
우리팀은 처음 이틀은 각자 배경지식을 쌓고 오고, 한양대, kaist ppt를 기반으로 페어프로그래밍으로 구현을 한 다음 돌려보고 디버깅을 하는 쪽으로 방향을 잡았다. 디버깅은 GPT와 블로그 등을 참고해서 수정했다. 처음부터 맞았을 때 그 쾌감이란! 물론 틀렸어도 내가 구현해본 경험이 있으니 이해가 더 쉬웠다. 함수 구현 중 핀트가 엇나간 함수 들은 블로그에 정리하면서 복기해볼 예정이다.
devices/timer.c 에 있는 timer_sleep()을 다시 구현해봅시다.
현재 제공된 작동 구현은 busy waiting를 수행합니다. 즉, 현재 시간을 확인하고 thread_yield()를 호출하여 충분한 시간이 경과할 때까지 반복적으로 루프를 실행합니다. busy waiting를 피하기 위해 다시 구현해보세요.
void timer_sleep (int64_t ticks);
호출하는 스레드의 실행을 중지하고 시간이 적어도 x 타이머 틱 만큼 경과할 때까지 대기합니다. 시스템이 다른 작업을 처리하고 있지 않다면, 스레드는 정확히 x 틱 후에 깨어나지 않아도 됩니다. 그저 올바른 시간만큼 대기한 후에 준비 큐에 넣으면 됩니다.
timer_sleep()은 실시간으로 작동하는 스레드에 유용합니다. 예를 들어, 커서를 1초에 한 번씩 깜박이는 작업에 사용될 수 있습니다. timer_sleep()의 인자는 밀리초나 다른 단위가 아닌 타이머의 ticks으로 표현됩니다. TIMER_FREQ 라는 초당 타이머 틱이 있습니다. 이 TIMER_FREQ는 devices.timer.h에 정의된 매크로 입니다. 기본값은 100이며, 값을 변경할 경우 많은 테스트 실패들이 실패할 수 있으므로 변경하지 않는 것이 좋습니다.
특정 시간(밀리초, 마이크로초, 나노초) 동안 슬립하는 데 사용되는 별도의 함수인 timer_msleep(), timer_usleep(), timer_nsleep()도 있습니다. 필요할 때 자동으로 timer_sleep()을 호출합니다. 이 함수들을 수정할 필요는 없습니다. 알람 시계 구현은 이후 프로젝트에는 필요하지 않지만, 프로젝트 4에 유용할 수 있습니다.
기존 코드는 아래와 같다.
//devices/timer.c
/* Suspends execution for approximately TICKS timer ticks. */
void
timer_sleep (int64_t ticks) {
int64_t start = timer_ticks ();
ASSERT (intr_get_level () == INTR_ON);
while (timer_elapsed (start) < ticks)
thread_yield ();
}
원래 코드에서는 while 루프를 사용하여 timer_elapsed(start)가 ticks보다 작은 동안 계속해서 thread_yield() 함수를 호출하여 스레드를 양보하는 방식으로 슬립을 구현되어있다. 이는 busy-waiting 방식으로 동작하여 프로세서 자원을 지속적으로 사용하게 되어 효율적이지 않다.
//devices/timer.c
/* Suspends execution for approximately TICKS timer ticks. */
void timer_sleep(int64_t local_ticks) /* local_ticks: 재우고 싶은 시간*/
{
int64_t start = timer_ticks();
ASSERT(intr_get_level() == INTR_ON); /* 인터럽트 방지 */
if (timer_elapsed(start) < local_ticks) /* 깨울 시간이 안 됐을 경우 */
{
thread_sleep(start + local_ticks);
}
}
위와 같이 변경함으로써 스레드를 지정된 시간(ticks 틱) 동안 슬립 상태로 만들고, 해당 시간이 경과하면 스레드를 다시 실행 대기 상태로 전환시키도록 하자.
이렇게 코드를 변경함으로써 스레드를 대기 상태로 전환하여 프로세서 자원을 효율적으로 활용할 수 있게 되어 전체 시스템의 성능을 향상시킨다.
함수가 시작될 때, 현재 타이머 틱(timer_ticks())의 값을 start 변수에 저장한다. 그리고 인터럽트가 활성화되어 있는지 확인하기 위해 intr_get_level() 함수를 호출하여 인터럽트 상태를 검사한다.
다음으로, timer_elapsed() 함수를 사용하여 현재까지 경과한 시간을 계산한다.
//devices/timer.c
int64_t
timer_elapsed(int64_t then)
{
return timer_ticks() - then;
}
timer_elapsed() 함수는 이전에 저장된 시간(then)으로부터 현재까지 경과한 시간을 반환한다. timer_ticks() 함수를 사용하여 현재의 타이머 틱 값을 가져온 후, 이전에 저장된 시간(then)을 뺌으로써 경과한 시간을 계산한다.
다시 timer_sleep() 설명으로 돌아가서, 만약 경과한 시간이 local_ticks보다 작다면 (즉, 깨울 시간이 아직 되지 않았다면) thread_sleep() 함수를 호출하여 스레드를 일시 정지시킨다. thread_sleep() 함수는 스레드를 재워야 하는 시간을 인자로 받아 일시 정지시키는 역할을 한다.
thread_sleep() 등의 함수를 통해 구현하는 방법은 32bit OS기반 한양대 및 kaist ppt을 따르는 것이다. thread, userprog까지는 32bit나 64bit의 내용이 크게 다르지 않으니 참고해도 좋고, 아예 다른 방법으로 해도 좋다.
하지만 우리팀은 처음 pintOS를 접한 뉴비로써 guide를 따라가기로 했다. 나와 같이 한양대, kaist 32bit버전으로 구현해볼 사람들은 다음으로 thread_sleep()를 구현해보도록 하자.
thread_sleep()함수는 인자로 받은 ticks동안 스레드를 슬립 상태로 전환했다가 ticks가 지난 후, 대기 상태로 전환시키는 함수다.
thread_sleep(ticks)을 구현하기 위해 먼저 추가해줘야 할 것이 몇개있다.
/* thread/thread.c */
static struct list sleep_list;
void
thread_init (void) {
ASSERT (intr_get_level () == INTR_OFF);
/* Reload the temporal gdt for the kernel
* This gdt does not include the user context.
* The kernel will rebuild the gdt with user context, in gdt_init (). */
struct desc_ptr gdt_ds = {
.size = sizeof (gdt) - 1,
.address = (uint64_t) gdt
};
lgdt (&gdt_ds);
/* Init the globla thread context */
lock_init (&tid_lock);
list_init (&ready_list);
list_init (&destruction_req);
/*-------------------------[project 1]-------------------------*/
list_init(&sleep_list);
/*-------------------------[project 1]-------------------------*/
lock_init(&filesys_lock);
lock_init(&exit_info_lock);
.
.
.
}
struct thread {
/* Owned by thread.c. */
tid_t tid; /* Thread identifier. */
enum thread_status status; /* Thread state. */
char name[16]; /* Name (for debugging purposes). */
int priority; /* Priority. */
/*-------------------------[project 1]-------------------------*/
int64_t wake_up_tick; /*local tick * /
/*-------------------------[project 1]-------------------------*/
};
.
.
.
이제 진짜로 thread_sleep(ticks)을 작성해보자.
//include/threads/thread.h
void thread_sleep(int64_t local_ticks);
//threads/thread.c
void thread_sleep(int64_t local_ticks){
struct thread *curr = thread_current();
if(curr != idle_thread){
intr_disable();
curr -> status = THREAD_BLOCKED;
curr -> wake_up_tick = local_ticks;
list_push_back(&sleep_list, &curr->elem);
schedule();
}
}
➡️interrupt 설정 실패, thread_block()함수 사용하지 않았다..;;
인터럽트는 실행 중인 작업을 중단하고, 우선순위가 높은 작업 또는 중요한 이벤트에 대한 처리를 할 수 있도록 하는 기능이다.thread_sleep() 함수에서는 슬립 상태로 전환될 때 해당 스레드가 CPU를 해제하고 다른 스레드에게 실행을 양보해야한다.
우리는 그러한 작업을 schedule()함수가 해주는 것이 아닐까라고 생각했지만(함수명만 보고 그런줄..🥲), schedule() 함수는 ready_list의 head의 status를 바꾸고, 선택된 스레드를 실행 상태로 변경하는 함수였다.
//include/threads/thread.h
void thread_sleep(int64_t local_ticks);
//threads/thread.c
void thread_sleep(int64_t local_ticks) /* local_ticks: 깨울 시간 */
{
struct thread *curr = thread_current();
enum intr_level old_level;
ASSERT(!intr_context());
ASSERT(curr != idle_thread)
old_level = intr_disable(); /* 인터럽트 방지 */
curr->wake_up_tick = local_ticks;
update_next_to_wake(local_ticks); /* sleep_list의 min_tick 업데이트 */
list_push_back(&sleep_list, &curr->elem);
thread_block();
intr_set_level(old_level); /* 인터럽트 재개 */
}
수정한 함수에서는 local_ticks 값을 현재 스레드의 wake_up_tick 변수에 저장해서 스레드가 깨어날 예정 시간을 저장해 놓는다. update_next_to_wake() 함수를 호출하여 sleep_list에서 가장 빠른 깨어날 시간을 업데이트
한다. 이후, list_push_back() 함수를 사용하여 현재 스레드를 sleep_list의 끝에 추가한다. thread_block() 함수를 호출하여 현재 스레드를 블록 상태로 전환하고, 이로써 스레드는 스케줄러의 관리 대상에서 제외되어 실행 대기 상태가 된다. 이후에는 intr_set_level() 함수를 호출하여 이전에 저장한 인터럽트 상태를 복원한다. 이는 인터럽트를 다시 활성화하여 스레드 스케줄링이 이루어질 수 있도록 하는 것이다.
//include/threads/thread.h
void update_next_to_wake(int64_t local_ticks);
static int64_t min_ticks; //min_ticks선언
//threads/thread.c
void thread_init(void)
{
ASSERT(intr_get_level() == INTR_OFF);
.
.
.
min_ticks = INT64_MAX; //min_ticks초기화
}
//threads/thread.c
/* local_ticks와 min_ticks 비교 => 최솟값 업데이트 */
void update_next_to_wake(int64_t local_ticks)
{
min_ticks = (local_ticks < min_ticks) ? local_ticks : min_ticks;
}
timer_interrupt() 함수는 타이머 인터럽트가 발생했을 때 호출되는 핸들러 함수다. 앞서 언급한 timer_interrupt()를 수정해보자.
🏴 여기서 잠깐!
타이머 인터럽트는 컴퓨터 시스템에서 정기적인 간격으로 발생하는 인터럽트입니다. 이는 하드웨어 타이머를 사용하여 일정한 주기로 발생하며, 운영 체제가 시스템의 시간을 추적하고 스케줄링을 수행하는 데에 사용됩니다.타이머 인터럽트는 일반적으로 하드웨어 타이머가 일정한 간격으로 시그널을 생성하여 CPU에게 인터럽트를 발생시킵니다. 이 인터럽트는 CPU의 현재 작업을 중단하고 운영 체제의 타이머 인터럽트 핸들러 함수를 실행하도록 합니다.
타이머 인터럽트는 여러 가지 용도로 사용됩니다. 주요 용도는 다음과 같습니다:시스템 시간 추적: 타이머 인터럽트를 사용하여 시스템이 부팅된 이후 경과한 시간을 추적합니다. 이를 통해 운영 체제는 정확한 시간 정보를 제공하고, 타이밍 기반 작업을 수행할 수 있습니다.
스케줄링: 타이머 인터럽트를 사용하여 프로세스 또는 스레드 간의 시간 할당을 조절합니다. 인터럽트가 발생할 때마다 스케줄러는 실행 중인 작업을 중단하고 다른 작업에 CPU를 할당하는 스케줄링 결정을 내릴 수 있습니다.
디바이스 제어: 타이머 인터럽트를 사용하여 주변 장치의 작업을 제어할 수 있습니다. 예를 들어, 일정한 간격으로 발생하는 타이머 인터럽트를 사용하여 주기적인 데이터 수집이나 디바이스 상태 감시 등을 수행할 수 있습니다.
//devices/timer.c
static void
timer_interrupt(struct intr_frame *args UNUSED)
{
ticks++;
thread_tick();
/*-------------------------[project 1]-------------------------*/
/* 깨울 스레드가 있으면 깨우기 */
if (get_next_to_wakeup() <= ticks) /* get_next_to_wakeup(): 가장 작은 wakeup_ticks를 가진 스레드를 반환 */
{
thread_wakeup(ticks);
}
/*-------------------------[project 1]-------------------------*/
}
우리는 update_next_to_wake()함수를 구현하면서, min_ticks를 설정해놓았다. min_ticks는 일어날 시간 중에 제일 작은 시간을 말하는 global 값이다. 따라서 get_next_to_wakeup()는 min_ticks를 반환하도록 설정해 주면 된다.
//include/threads/thred.h
int64_t get_next_to_wakeup(void);
//threads/thread.c
int64_t get_next_to_wakeup(void)
{
return min_ticks;
}
다음은 thread_wakeup()함수다. thread_wakeup 함수는 일정 시간이 지난 스레드들을 깨우는 역할을 한다. 이 함수는 타이머 인터럽트 핸들러에서 호출되며,
앞서 thread를 재우면서 해당 thread를 넣어둔 sleep_list에서 현재 시간(ticks)과 비교하여 깨어야 할 스레드를 찾아서 block상태에서 해제한다.
//include/threads/thred.h
void thread_wakeup(int64_t);
//threads/thread.c
get_next_to_wakeup();
intr_disable();
min_thread->status = THREAD_READY;
list_remove(&min_thread->elem);
list_push_back(&ready_list, &min_thread->elem);
}
지금 우리가 고안한 코드를 보면 get_next_to_wakeup()코드에 너무 꽂혔던 것 같다. 또한 디버깅하면서 느낀 것은 thread_wakeup()함수에서 구현 순서가 매우 중요한 것을 체감했다.(지금 생각하면 당연한 거지만..)
thread_wakeup()의 안에서 사용된 list_remove()와 thread_unblock()함수의 순서가 매우 중요했는데, unblock()을 먼저하면 t의 연결리스트가 끊기므로, list_remove()를 할수 없었다.
이경험을 통해서 flow를 완벽하게 이해하는게 중요하다는 것을 다시 한번 깨달았다.
//include/threads/thred.h
void thread_wakeup(int64_t);
//threads/thread.c
void thread_wakeup(int64_t ticks) /* ticks: global ticks */
{
struct list_elem *curr = list_begin(&sleep_list);
/* ⚠️ list_front 사용 시 sleep_list가 비어 있을 경우, ASSERT 발생 => list_begin 사용 */
while (curr != list_end(&sleep_list)) /* sleep_list 끝까지 탐색 */
{
struct thread *t = list_entry(curr, struct thread, elem);
int64_t tmp_ticks = t->wake_up_tick;
if (tmp_ticks <= ticks) /* 현재 탐색 중인 스레드가 깰 시간이 되었을 때 */
{
curr = list_remove(&t->elem); /* sleep_list에서 제거 */
thread_unblock(t);
/*
⚠️ thread_unblock을 list_remove보다 먼저 사용 시 ready_list로 이동 => list_remove 시 ready_list에서 제거
* 원래 의도: sleep_list에서 제거
*/
}
else /* 깨울 스레드가 아니면 */
{
curr = list_next(curr);
update_next_to_wake(t->wake_up_tick);
}
}
}
sleep_list의 list_begin을 사용하여 첫 번째 요소인 curr을 가져오고, 현재 탐색 중인 스레드 t의 깨울 시간을 tmp_ticks에 저장한다.
만약 tmp_ticks가 현재 시간 ticks보다 작거나 같다면, 스레드를 깨워야한다.
curr을 sleep_list에서 제거하고, thread_unblock을 호출하여 스레드를 블록 상태에서 해제한다. 동시에 update_next_to_wake를 호출하여 sleep_list에서 가장 작은 깨울 시간을 가진 스레드를 찾으며, 반복을 마치고 함수가 종료된다. thread_wakeup 함수는 sleep_list를 순회하면서 깨어야 할 스레드를 찾고 해제하는 역할을 수행한다. 이를 통해 timer_sleep 함수에서 지정한 시간이 경과하면 해당 스레드들이 실행 가능 상태로 전환된다.