PintOS는 알람을 구현할 때 기본적으로 busy waiting 방식을 사용한다.
따라서 sleep/wakeup 방식으로 개선하는 것이 목표이다.
기존의 timer_sleep 함수
timer_sleep 함수는, running중인 스레드를 지정된 ticks 동안 blocked 시키는 함수이다.
기존의 timer_sleep은 위 사진처럼 busy waiting이 일어나도록 설계되어 있다.
지정된 ticks가 지날 때 까지 while문 조건을 끝없이 검사하며 의미없이 CPU를 잡아먹는다.
즉, 지정된 시간을 기다리기만 할 뿐인 스레드가 무의미하게 while문을 돌며 CPU자원을 소모한다. 이것이 busy waiting이며, 개선해야 하는 부분이다.
어떻게 개선해야 하는가?
- thread 구조체에, 깨어나야 할 시간을 의미하는 Local Tick 필드를 추가하여 현재 시스템 Tick과 비교해 지정된 시간이 되면 깨우도록 하기.
- 매 timer interrupt 마다 thread_awake 함수가 작동하며, 지정된 Local Tick 시간이 되면 sleep_list에서 깨워 ready_list로 넣어주도록 하기.
thread.c
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. */
int64_t localTick ; // 로컬 틱 필드 추가
/* Shared between thread.c and synch.c. */
struct list_elem elem ; /* List element. */
#ifdef USERPROG
/* Owned by userprog/process.c. */
uint64_t * pml4; /* Page map level 4 */
#endif
#ifdef VM
/* Table for whole virtual memory owned by thread. */
struct supplemental_page_table spt;
#endif
/* Owned by thread.c. */
struct intr_frame tf ; /* Information for switching */
unsigned magic ; /* Detects stack overflow. */
};
우선 thread 구조체에 Local Tick을 의미하는 localTick 필드를 추가한다.
해당 필드 값을 보고 thread_awake 함수에서 깨울 지 말지를 검사한다.
thread.c
// ready_list 선언
static struct list ready_list ;
// sleep_list 선언
static struct list sleep_list ;
thread.c 파일에 ready_list는 이미 선언되어 있을 것이다. ready_list와 똑같은 형태로 sleep_list를 선언해 준다.
sleep_list에는 Sleep(Blocked)상태인 스레드들이 저장될 것이다.
thread.c
// 스레드 시스템 초기화 + PintOS의 초기 스레드 생성
// 여기서 run queue(ready_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 ); // ready_list 초기화
list_init ( & sleep_list ); // sleep_list 초기화
list_init ( & destruction_req );
/* Set up a thread structure for the running thread. */
initial_thread = running_thread ();
init_thread ( initial_thread , "main" , PRI_DEFAULT );
initial_thread -> status = THREAD_RUNNING ;
initial_thread -> tid = allocate_tid ();
}
sleep_list를 선언했으니 초기화는 필수이다.
thread_init 함수는 PintOS의 초기 스레드를 초기화하는 함수이다. 이 함수에서는 기본적으로 ready_list가 초기화되는데, ready_list와 같은 방식으로 sleep_list도 함께 초기화해 준다.
timer.c
void
timer_sleep ( int64_t ticks ) {
// 시간 재기 ( OS 부팅 후 시간을 가져온다 )
int64_t start = timer_ticks ();
ASSERT ( intr_get_level () == INTR_ON );
// elapsed는 start이후로 지난 시간을 반환한다.
// ticks가 0이 아니라면 재우게 된다.
if ( timer_elapsed ( start ) < ticks ) {
// start + ticks 시간까지 잠재우게 된다
thread_sleep ( start + ticks );
}
}
개선된 timer_sleep
개선된 timer_sleep 함수는 while문을 돌지 않고 thread_sleep() 함수를 호출하고 있다. 로직을 보려면 thread_sleep 함수를 봐야 한다.
thread.c
// 로컬틱 넣고, state 변경하고, sleep_list에 넣고, 스케줄러 호출하기
void thread_sleep ( int64_t ticks ) {
// 현재 스레드 담을 포인터
struct thread * curThread ;
// 인터럽트 레벨 담을 변수
enum intr_level old_level ;
// 인터럽트 비활성화 + 인터럽트 상태 old_level에 저장
old_level = intr_disable ();
// 현재 스레드 담기
curThread = thread_current ();
// 현재 스레드가 idle이 아닐 경우
if ( curThread != idle_thread ) {
// 로컬 틱 설정
curThread -> localTick = ticks ;
// localTick 기준으로 정렬되어 삽입
list_insert_ordered ( & sleep_list , & curThread -> elem , compare_thread_ticks , NULL );
// state를 blocked로 만들고 스케줄러 호출, 새 스레드에게 CPU 주기
// 이거 위에 있었을 때 안됐다
thread_block ();
}
// 인터럽트 활성화 + 복구
intr_set_level ( old_level );
}
스레드를 지정된 ticks 동안 재우는 함수이다.
재운다는 건, 스레드 state를 BLOCKED로 만들고 sleep_list에 넣는 걸 의미한다.
우선 작업을 하기 전 인터럽트를 비활성화 해야 한다. 이것은 sleep_list의 조작을 방해받지 않고 안전하게 하기 위해서, 또는 race condition 같은 상황을 방지하기 위해서 필수적이다.
현재 스레드가 idle thread가 아닐 경우 로직을 실행한다. idle thread는 CPU가 실행할 작업이 아무것도 없을 때 실행되는 스레드이며, 수행할 작업이 없다면 항상 실행되고 있어야 한다. idle thread는 재우면 안 되므로 조건문으로 걸러준다.
이후 현재 스레드의 localtick 을 입력받은 ticks 로 설정한다. system tick이 localtick과 동일해지면 해당 스레드는 깨어난다.
그리고 sleep_list에 스레드를 localtick 기준으로 정렬해서 넣는데, 이렇게 하면 가장 먼저 깨어나야 할 스레드가 sleep_list의 HEAD쪽에 위치하게 되므로 리스트를 전부 탐색할 필요가 없어진다.
마지막으로 thread_block()을 호출하여 현재 스레드의 state를 blocked로 만들고 스케줄러를 호출하여 다음 스레드에게 CPU를 넘겨준다.
thread.c
// list_insert_ordered를 위한 구현, 로컬틱 기준 정렬
bool compare_thread_ticks ( const struct list_elem * a , const struct list_elem * b , void * aux UNUSED ){
struct thread * thread_a = list_entry ( a , struct thread, elem);
struct thread * thread_b = list_entry ( b , struct thread, elem);
if ( thread_a -> localTick < thread_b -> localTick ) {
return true ;
}
else return false ;
}
list_insert_ordered 함수의 3번째 인자로 들어가는 함수이다. 함수의 인자로 함수가 들어간다.
a, b의 localTick을 비교하여 b가 클 경우 true, 아닐 경우 false를 리턴한다.
이 함수를 인자로 넣으면, localTick이 작은 스레드가(먼저 깨어나야 할 스레드가) 리스트의 HEAD 쪽에 오게 정렬된다.
따라서 이후에 thread_awake를 통해 sleep_list에서 깨울 스레드를 찾을 때 리스트를 전부 검사할 필요가 없게 된다.
timer.c
// sleep_list의 HEAD를 확인하여 System tick과 일치하면 wakeup 시킨다. ( sleep_list 정렬되어있어야함 )
static void
timer_interrupt ( struct intr_frame * args UNUSED ) {
// 타이머 인터럽트가 실행될 때마다 system ticks가 증가한다
ticks ++ ;
// 실행 중인 프로세스의 CPU 사용량을 업데이트 하는 함수
thread_tick ();
// 모든 tick마다 실행되어, wakeup할 스레드를 고르는 코드
thread_awake ( ticks );
}
PintOS는 매 타이머 인터럽트마다(1ms 간격) system tick을 1 증가시키고, sleep_list에서 깨울 스레드들을 찾는다.
아니, 정확히는 찾게 만들어야 한다. 따라서 thread_awake 함수를 timer_interrupt에 추가해야 한다.
thread.c
// 현재 system tick에 깨어나야 할 스레드를 찾고 깨우는 함수
void thread_awake ( int64_t ticks ) {
// sleep_list가 비어 있으면 바로 return
if ( list_empty ( & sleep_list )) {
return ;
}
// sleep_list HEAD 가지고 오기
struct list_elem * e = list_begin ( & sleep_list );
// 정렬해놨으니까 HEAD만 보면서, system tick보다 큰 값 나오면 break
while ( e != list_end ( & sleep_list ) && ! list_empty ( & sleep_list ) && e != list_head ( & sleep_list )) {
// 스레드 가져오기
struct thread * t = list_entry ( e , struct thread, elem);
// localTick이 systemTick 과 같거나 이미 지났을 경우 깨우기
if ( t -> localTick <= ticks ) {
// sleep_list에서 지워주기, 다음 요소 획득
e = list_remove ( e );
// sleep_list에서 unblock 하기
// state를 READY로 바꾸고 ready_list에 삽입한다
thread_unblock ( t );
}
// HEAD의 localTick이 systemTick보다 한 번이라도 클 경우 break
else {
break ;
}
}
}
thread_awake 함수이다. timer_interrupt 함수에 포함되어 있으므로 매 타이머 인터럽트마다(1ms마다) 이 함수가 실행되는 것이다.
인자로 ticks를 받아서 스레드가 가진 Local Tick과 비교해 일치하거나 일어날 시간이 지났다면 해당 스레드를 sleep_list에서 지우고 unblock한 후 실행될 수 있도록 ready_list에 넣는다.
참고로 PintOS의 list 형태는 {TAIL, 요소, HEAD} 형태이며 HEAD와 TAIL은 의미없는 값을 가지는 더미데이터이다. 즉 리스트의 끝을 판단하기 위한 역할일 뿐이다. 따라서 우리가 원하는 가장 빨리 깨어나야 하는 스레드를 찾으려면 list_begin 함수로 HEAD 다음 요소를 가져와야 한다.
또한 list는 정렬되어 있기 때문에 단 한번이라도 깨어날 시간이 되지 않은 스레드를 만난다면, 그대로 리스트 탐색을 종료해도 된다. 그 뒤의 스레드들은 더 늦게 깨어날 스레드들일 테니까.
=========================
이렇게 Alarm Clock은 끝이 났다. 무의미하게 CPU를 낭비하는 busy waiting 방식을 개선하고 sleep/wakeup으로 바꾸어 CPU가 낭비되지 않게 했다.
실행 결과
550 idle ticks
Thread: 550 idle ticks 부분을 보면 CPU 유휴 시간이 늘어났다는 걸 알 수 있다.
기존엔 0 idle ticks였는데 busy waiting을 개선하고 나니 550 idle ticks로 늘어났다.
참고로 550 idle ticks는 idle thread가 550 ticks동안 실행되었다는 것이다.
즉 CPU가 550 ticks 동안 놀았다는 뜻이고, CPU 자원 낭비가 줄어들었다는 걸 의미한다.