<aside> ❗ 목차

  1. Busy waiting

  2. Sleep - awake 방식

</aside>

  1. Busy waiting

    우선 핀토스에서의 쓰레드 사이클을 살펴보면 다음과 같다.

    Untitled

    이 모식도를 보다보면 기초개념에 있는 프로세스 상태와 유사하다는 것을 알 수 있다. 대문자 들은 스레드의 상태이다. 핀토스는 1 스레드 방식을 사용하기 때문에 스레드와 프로세스를 동일한 개념으로 생각하는것이 좋다. 현재 busy wait방식은 block 시키지 않고 yield하여 ready list의 맨 뒤로 보낸다.

    핀토스는 외부에 타이머가 따로 존재한다. 이 타이머는 일정 시간마다 타이머 인터럽트를 호출하여 스케줄링을 진행한다. 현재 구현되어있는 방식은 자고있는 스레드를 깨운 후 일어날 시간인지 확인한다. 그리고 아직 일어날 시간이 아니라면 다시 재운다. 이것은 매우 비효율적인 방법이다. 자는지 확인 하느라 cpu가 해야할 일을 못하는 것이기 때문이다. 이를 코드를 통해 확인해 보면 다음과 같다.

    /* devices/timer.c */
    void timer_sleep (int64_t ticks) {
      int64_t start = timer_ticks ();
      while (timer_elapsed (start) < ticks) 
        thread_yield ();
    }
    
    /* devices/timer.c */
    int64_t timer_elapsed (int64_t then) {
      return timer_ticks () - then;
    }
    
    /* Returns the number of timer ticks since the OS booted. */
    int64_t
    timer_ticks (void) {
    	enum intr_level old_level = intr_disable ();
    	int64_t t = ticks;
    	intr_set_level (old_level);
    	barrier ();
    	return t;
    }
    

    현재 핀토스에는 1 tick의 시간이 1ms로 설정되어있다. 1ms마다 타이머 인터럽트를 발생시키고 ticks의 값을 1 증가시킨다. timer_ticks()는 현재 ticks 값을 반환하는 함수이며 start에 현재 시간을 저장한다. 이 값을 timer_elapsed()에 대입하여 현재로 부터 경과된 시간을 계산한다. 이에 따라경과된 시간이 ticks보다 커질때 까지 yield를 반복하며 ready_list의 맨뒤로 이동한다.

  2. Sleep - awake

    busy waiting방식을 개선하기 위해 sleep awake 방식을 구현하고자 한다. 간단하게 설명하면 자는 스레드들을 한데 모아두고 인터럽트 때마다 깨어나야할 쓰레드를 찾아서 깨워준다. busy waiting방식 에서는 재우는 것을 yield를 통해 진행했지만 sleep awake 방식에서는 block 상태로 재운다.

    1. 자는 스레드들을 관리하기 위한 sleep_list를 만들고 일어날 시간을 쓰레드 구조체 안에 기록한다.

      /* thread/thread.h */
      struct thread{
          ...
          int64_t wakeup; // 깨어나야 하는 ticks 값
          ...
      }
      
      /* thread/thread.c */
      static struct list sleep_list;
      
      void
      thread_init (void) 
      {
        ...
        list_init (&ready_list);
        list_init (&all_list);
        list_init (&sleep_list);
        ...
      }
      
    2. 쓰레드를 ticks까지 재우는 함수를 따로 만들어 준다.

      /* 스레드를 ticks시각 까지 재우는 함수 */
      void thread_sleep(int64_t ticks){
          
      		 //이전 인터럽트 레벨을 저장하고 인터럽트 방지
          enum intr_level old_level = intr_disable();
          
      		// idle 스레드는 sleep 되지 않아야한다.
          struct thread *cur = thread_current(); 
          ASSERT(cur != idle_thread);
      
          /* 현재 스레드를 슬립 큐에 삽입한 후에 스케줄한다. */
          list_push_back(&sleep_list, &cur->elem);
      
          /* awake함수가 실행되어야 할 tick값을 update */
          cur->wakeup_tick = ticks;
          update_next_tick_to_awake(ticks);
      
          /* 이 스레드를 블락하고 다시 스케줄될 때 까지 블락된 상태로 대기 */
          thread_block();
      
          /* 인터럽트를 다시 받아들이도록 수정 */
          intr_set_level(old_level);
      }
      

      여기서 idle 스레드는 재우지 않는다. idle스레드는 핀토스에 있는 특별한 스레드이다. ready_list가 공백이면 idle 쓰레드가 cpu를 점유하며 다른 쓰레드를 기다리고 있는다. 만약 idle쓰레드가 잠들면 cpu가 꺼져버린다.

    3. timer_sleep() 수정한다. 새로 만든 thread_sleep()을 활용한다.

      void 
      timer_sleep (int64_t ticks) 
      {
        int64_t start = timer_ticks ();
        thread_sleep (start + ticks);
      }
      
    4. sleep_list를 순회하며 일어날 시간이 지난 스레드를 찾는 함수를 만든다. 시간이 지난 스레드들은 unblock을 통해 ready_list로 이동시킨다. 이 함수를 이용하여 timer 인터럽트때마다 일어날 스레드들을 깨운다.

      /* thread/thread.c */
      void
      thread_awake (int64_t ticks)
      {
        struct list_elem *e = list_begin (&sleep_list);
      
        while (e != list_end (&sleep_list)){
          struct thread *t = list_entry (e, struct thread, elem);
          if (t->wakeup <= ticks){	// 스레드가 일어날 시간이 되었는지 확인
            e = list_remove (e);	// sleep list 에서 제거
            thread_unblock (t);	// 스레드 unblock
          }
          else 
            e = list_next (e);
        }
      }
      
      /* devices/timer.c */
      static void
      timer_interrupt (struct intr_frame *args UNUSED)
      {
        ticks++;
        thread_tick ();
        thread_awake (ticks);	// ticks 가 증가할때마다 awake 작업 수행
      }
      

처음에는 알람 기능을 왜 구현해야하는지 약간 의아했다. 하지만 공부하다 보니 타이머는 CPU에게 꼭 필요한 존재라는 것을 알았다. 초창기 컴퓨터는 배치 시스템으로 한 프로세스가 계속 작업을 하여 스케줄링을 하지 않았다. 하지만 동시성과 즉시 응답이 되지않아 cpu를 돌아가면서 사용하게 되었다. 하지만 이럴 경우 운영체제가 cpu를 사용하지 못하는 경우가 발생한다. 따라서 일정 시간마다 타이머가 인터럽트를 걸어서 운영체제에게 권한을 주어 높은 우선순위의 프로세스에게 권한을 넘기는 등 관리를 하게 하였다.

위 내용은 아래 블로그를 참고하였습니다.

[Pintos] Project 1 : Thread(스레드) - Alarm Clock

https://apption.co/embeds/54addc8d