210201 개발일지(56일차) - 운영체제(OS) 프로젝트 #1-1 : pintOS project Alarm Clock

고재개발·2021년 2월 1일
0

OS Project

목록 보기
4/28

pintOS 과제는 프로세스와 스레드를 동일하게 여기고 진행하면 된다. (굳이 비유하자면 멀티 프로세스, 단일 스레드 개념)

Alarm Clock ?

기본 제공되는 pintOS는 프로세스를 재우고 깨우는 기능Busy-Waiting 방식으로 구현돼있다.
이게 무슨 소리냐면, OS에서 A라는 프로세스를 잠시 재우고 싶을 때가 있다.(I/O를 기다릴 때 등, 예를 들어 웹 스크래핑을 한다고 할 때 웹 정보를 다 불러올 때 까지 잠시 기다릴 때)
이걸 여기 과제에서는 Alarm Clock을 이용해서 프로세스를 재우고 깨운다고 생각하면 된다.

Busy-waiting

위에서 언급했듯, 기본 제공되는 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) 방식으로 변경해주고자 하는 것이 이 소과제의 목표다.
즉, 프로세스를 재울 때 시스템 자원 낭비를 최소화하는 것이다.

pintOS에 구현돼있는 Busy-wating 살펴보기

아래 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 (Block & Waiting)

개념

위의 방식과 달리 Sleep & Waiting 방식은 우리가 아는 상식적인 잠자기 방식이다. 즉, 3시간 자기로 했으면 푹 자다가 3시간 후에 알람 시계 소리를 듣고 일어나는 방식이다.

이전 방법의 가장 큰 문제점은 편히 자야할 스레드들을 ready 상태로 둬서 스케줄링(running시키는 것) 되게 했다는 것이다.

이제는 자야할 스레드들을 ready로 보내는 것이 아니라 block(asleep) 상태로 만들고, 깰 시간이 되면 그 때서야 ready 상태로 바꿔주는 것이다.

구현하기

  1. 아래 함수 및 변수들을 각 c파일과 h파일에 추가해줘야 한다.
  1. thread 구조체에 언제 일어날 지 정해주는 변수 wakeup_tick을 추가한다.
/* pintos/src/thread/thread.h */
struct thread{
    ...
    // 깨어나야 할 tick을 저장할 변수 추가
    int64_t wakeup_tick;
    ...
}
  1. 잠자는 스레드들을 모아두는 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);
    ...
}
  1. 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;
}
  1. 이제 스레드를 재울 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);
}
  1. 이번에는 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);
        }
    }
}
  1. 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);
}
  1. 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);
    }
}

※ idle_thread(아이들 스레드)와 main_thread(메인 쓰레드)

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에 들어가지 않는다.

profile
고재개발

0개의 댓글