pintOS 과제는 프로세스와 스레드를 동일하게 여기고 진행하면 된다. (굳이 비유하자면 멀티 프로세스, 단일 스레드 개념)
기본 제공되는 pintOS는 프로세스를 재우고 깨우는 기능이 Busy-Waiting 방식으로 구현돼있다.
이게 무슨 소리냐면, OS에서 A라는 프로세스를 잠시 재우고 싶을 때가 있다.(I/O를 기다릴 때 등, 예를 들어 웹 스크래핑을 한다고 할 때 웹 정보를 다 불러올 때 까지 잠시 기다릴 때)
이걸 여기 과제에서는 Alarm Clock을 이용해서 프로세스를 재우고 깨운다고 생각하면 된다.
위에서 언급했듯, 기본 제공되는 pintOS의 Alarm Clock 기능은 Busy-waiting이라는 것이다. 이 방법을 3시간 낮잠 자기에 비유한 블로그를 봤는데, 이해에 도움이 됐다.
낮잠 자기 시작 -> 1분후 깸 -> 1분 지났네..? -> 다시 자야지 -> 1분후 깸 -> 2분 지났네..? -> 다시 자야지 -> … -> 1분 후 깸 -> 2시간 59분 지났네..? -> 다시 자야지 -> 1분 후 깸 -> 3시간 지났네? 이제 낮잠이 끝났구나…
출처: https://bowbowbow.tistory.com/20#과제-목표
이렇게 잠을 자면, 재우는게 미안한 느낌이다. 그래서 이런 방식을 쓰지 않고 Sleep & Wake-up(혹은 Block & Wake-up) 방식으로 변경해주고자 하는 것이 이 소과제의 목표다.
즉, 프로세스를 재울 때 시스템 자원 낭비를 최소화하는 것이다.
아래 timer_sleep()함수 코드를 보면, thread가 자꾸 cpu로 올라왔다가 내려갔다를 반복하게 된다.(while문에 해당하는 부분)
/* pintos/src/device/timer.c */
void timer_sleep(int64_t ticks)
{
int64_t start = timer_ticks();
while(timer_elapsed(start) < ticks) //timer_elapsed(start)는 timer_sleep이 호출된 시점에서 몇 tick이 지났는지를 반환하는 함수
thread_yield(); //현재 CPU점유 다른 스레드에게 양보하고 ready_list 제일 뒤로 이동
}
위의 방식과 달리 Sleep & Waiting 방식은 우리가 아는 상식적인 잠자기 방식이다. 즉, 3시간 자기로 했으면 푹 자다가 3시간 후에 알람 시계 소리를 듣고 일어나는 방식이다.
이전 방법의 가장 큰 문제점은 편히 자야할 스레드들을 ready 상태로 둬서 스케줄링(running시키는 것) 되게 했다는 것이다.
이제는 자야할 스레드들을 ready로 보내는 것이 아니라 block(asleep) 상태로 만들고, 깰 시간이 되면 그 때서야 ready 상태로 바꿔주는 것이다.
- 아래 함수 및 변수들을 각 c파일과 h파일에 추가해줘야 한다.
- thread 구조체에 언제 일어날 지 정해주는 변수 wakeup_tick을 추가한다.
/* pintos/src/thread/thread.h */ struct thread{ ... // 깨어나야 할 tick을 저장할 변수 추가 int64_t wakeup_tick; ... }
- 잠자는 스레드들을 모아두는 list인 sleep_list를 만들고, 잠자는 스레드들 중 가장 먼저 일어날 수 있도록 해주는 변수 next_tick_to_awake를 만든다.
/* pintos/src/thread/thread.c */ static struct list sleep_list; static int64_t next_tick_to_awake; ... void thread_init(void){ ... list_init (&sleep_list); ... }
- next_tick_to_awake를 관리해주는 함수 update_next_tick_to_awake()와 get_next_tick_to_awake()를 만든다.
/* pintos/src/thread/thread.c */ // 가장 먼저 일어나야할 스레드가 일어날 시각을 반환함 void update_next_tick_to_awake(int64_t ticks){ // next_tick_to_awake 가 깨워야 할 스레드의 깨어날 tick값 중 가장 작은 tick을 갖도록 업데이트 함 next_tick_to_awake = (next_tick_to_awake > ticks) ? ticks : next_tick_to_awake; } // 가장 먼저 일어나야할 스레드가 일어날 시각을 반환함 int64_t get_next_tick_to_awake(void){ return next_tick_to_awake; }
- 이제 스레드를 재울 thread_sleep() 함수를 구현한다. 재울 애들은 sleep_list에 추가하고 block상태로 만들어주면 된다.
/* pintos/src/thread/thread.c */ //스레드를 ticks시각 까지 재우는 함수 void thread_sleep(int64_t ticks){ struct thread *cur; // 인터럽트를 금지하고 이전 인터럽트 레벨을 저장함 enum intr_level old_level; old_level = intr_disable(); cur = thread_current(); // idle 스레드는 sleep 되지 않아야 함 ASSERT(cur != idle_thread); // awake함수가 실행되어야 할 tick값을 update update_next_tick_to_awake(cur-> wakeup_tick = ticks); // 현재 스레드를 슬립 큐에 삽입한 후에 스케줄한다. list_push_back(&sleep_list, &cur->elem); //이 스레드를 블락하고 다시 스케줄될 때 까지 블락된 상태로 대기 thread_block(); // 인터럽트를 다시 받아들이도록 수정 intr_set_level(old_level); }
- 이번에는 sleep_list에서 잠을 꺠워주는 함수 thread_awake() 를 만들어야 한다. sleep_list의 모든 스레드들을 순회하면서 현재 tick이 깨워야할 tick보다 작으면 깨우면 된다. 현재 tick이 크다면 update_next_tick_to_awkae()함수를 실행한다.
/* pintos/src/thread/thread.c */ //푹 자고 있는 스레드 중에 깨어날 시각이 ticks시각이 지난 애들을 모조리 깨우는 함수 void thread_awake(int64_t wakeup_tick){ next_tick_to_awake = INT64_MAX; struct list_elem *e; e = list_begin(&sleep_list); while(e != list_end(&sleep_list)){ struct thread * t = list_entry(e, struct thread, elem); if(wakeup_tick >= t->wakeup_tick){ e = list_remove(&t->elem); thread_unblock(t); }else{ e = list_next(e); update_next_tick_to_awake(t->wakeup_tick); } } }
- thread_sleep() 함수가 불려지는 timer.c에 timer_sleep() 함수를 수정해줘야 한다.
/* pintos/src/device/timer.c */ void timer_sleep (int64_t ticks){ int64_t start = timer_ticks (); ASSERT (intr_get_level () == INTR_ON); // 기존의 busy waiting을 유발하는 코드를 삭제하고, 새로 구현한 thread를 sleep list에 삽입하는 함수 호출함 thread_sleep(start + ticks); }
- thread_awake() 함수를 활용할 timer.c에 timer_interrupt() 함수를 수정해줘야 한다.
/* pintos/src/device/timer.c */ static void timer_interrupt (struct intr_frame *args){ ... // 매 tick마다 sleep queue에서 깨어날 thread가 있는지 확인하여, 깨우는 함수를 호출하도록 함. if(get_next_tick_to_awake() <= ticks){ thread_awake(ticks); } }
main_thread는 thread_init()함수 안에 init_thread()함수로 인해 생성된다. 이는 처음에 생성되자마자 running되는 스레드라고 생각하면 된다.
반면, idle_thread는 thread_start()함수 안에 thread_create()함수에 의해 생성된다. 처음에는 ready_list에 들어가 있다가, 차례가 되면 실행되어 idle_thread의 본모습(?)으로 변하게 된다. idle_thread는 전역 변수로 선언되어 있으며, ready_list에 다른 thread들이 없을 때 run 한다. 그 후 다른 thread가 실행 되면 block 상태로 만들고, sleep_list에 들어가거나 ready_list에 들어가지 않는다.