일단 키워드 공부를 가벼운 수준으로 끝내고, 막대한 양의 코드를 해석하기 시작했다. (이전 글 참고)
코드가 조금 숙지되고 나서 구현을 시작하려니 어디서부터 뭘 구현해야 할지 감이 안 잡혔다.
구현 요구사항 파악이 덜 되었기 때문이다..!
그래서 내가 뭘 구현해야 할지부터 차근차근 정리했다.
이번 포스팅은 내가 alarm clock 프로젝트를 어떻게 구현했는지에 대한 내용이다.

📌 구현 요구 사항
timer_sleep()은 현재 스레드를 ticks 만큼의 시간 동안 재우는 함수이다. 원래 busy-wait으로 구현되어있었던 코드를 살펴보자.
/* Suspends execution for approximately TICKS timer ticks. */
void
timer_sleep (int64_t ticks) {
int64_t start = timer_ticks ();[pintos] 3일차 - Alarm Clock 코드 정리
ASSERT (intr_get_level () == INTR_ON);
while (timer_elapsed (start) < ticks)
thread_yield ();
}
인자로 넘겨준 ticks 만큼의 시간이 되지 않았으면 일단 cpu를 양보(yield)하고 본인은 계속 ticks 만큼의 시간이 지났는지 확인하고 있다.
즉 ticks 만큼의 시간 동안 실제로 스레드를 blocked 상태로 전환시켜서 그 동안은 cpu를 사용하는 일이 없게 만들어야 효율적인데, 그렇게 하지 않고 스레드를 ready 상태로 전환시켜서 자꾸 cpu를 할당받고 time slice 동안 cpu를 사용하면서 ticks 만큼의 시간이 지났는지 확인하게 만든다. (running → ready → running → ready를 반복하며 계속 ticks 만큼의 시간이 지났는지 확인)
따라서 지금 running → waiting → ready 로직을 만들어서 스레드가 ticks 시간 동안은 cpu를 아예 사용하지 못 하게 재워야 한다. sleep_list를 하나 만들어서 waiting 상태의 스레드를 재워두고 timer interrupt가 발생할 때마다 sleep_list에서 ticks 만큼의 시간 동안 잠들었던 스레드를 깨워보자.
📌 구현 방법과 헷갈렸던 부분들
- 구현 요구사항을 파악하기 위해 timer_sleep()이 돌아가는 과정을 파악하는 것도 어려웠다.
- while문이 어떤 일을 하고 있는지 파악하기 어려웠음. 설명은 ‘구현 요구사항’ 참고
- timer_sleep() 안에 wake까지 구현해야 한다고 생각하고
- wake 조건을 이렇게 두 가지로 생각했었다.
- wake 조건 1 : TIME_SLICE에 도달한 스레드
- wake 조건 2 : sleep_list에 저장된 스레드 중 남은 실행 시간이 가장 긴 스레드
- 당연히 아니었고! end_time 즉 스레드가 끝나는 시간이 되었는지만 확인하도록 해야 했다.
- 내가 생각해낸 이유는 다음과 같다.
- 깨우는 건 time_slice랑 상관 없다. time_slice만큼 지나면 발생되는 것은 wake가 아니라 yield(즉 문맥 교환)이다. (참고로 time_slice에 도달했는지 확인하고 yield하는 작업은 thread_tick()에서 한다.)
- sleep_list에 저장된 스레드 중 남은 시간이 가장 긴 스레드를 깨우는 게 아니라 정해진 tick시간 동안 sleep한 스레드를 깨워야 한다.
- timer_sleep()함수의 역할을 분명히 파악하는 과정
- 인자로 넘겨준 ticks 만큼의 시간이 되지 않았으면 스레드를 재웠다가(blocked 상태로 전환했다가), ticks 만큼의 시간이 지나면 다시 깨우기까지 하는 함수라고 생각했는데 깨우는 건 timer_sleep()에서 하지 않는다. timer_interrupt()에서 한다. 왜냐하면 timer_interrupt라는 것은 시스템 전체에서 tick이 한 번 지날 때마다 호출되는 인터럽트 핸들러인데 그 때마다 잠들었던 스레드들 중 ticks만큼 다 기다렸는지 확인하고 다 기다린 스레드들을 모두 깨워줘야 하기 때문이다. timer_sleep()은 그냥 호출되면 무조건 현재 스레드를 재우는 함수이다.
- struct thread의 변수 end에 어떤 값이 들어가야 할 지 정하는 과정
- 현재 스레드에 ticks 시간 동안 자야 한다는 정보가 저장되어야 하므로 struct thread 구조체에 end라는 변수를 하나 추가했다. end에는 해당 스레드가 깨어야 할 시간이 저장되어있다. 즉 end값은 ‘timer_sleep()함수가 호출된 시점의 시스템 시간 (start) + 해당 스레드가 잠든 채로 유지되어야 하는 시간 (ticks)’ 으로 정의된다. end값을 이렇게 정의함으로써 timer_iterrupt()가 현재 시스템 tick과 end를 비교하여 나중에 timer_interrupt()에서 스레드를 언제 깨워야 하는지 쉽게 판단 가능하다. curr→end = end로 구조체 값을 할당해줬다.
- 임계 영역으로 지정해줘야 하는 부분을 파악하는 과정
- list_insert_ordered()와 thread_block()을 함께 임계 영역으로 감쌌다.
- 공유 자원인 자료구조를 읽고 쓸 때 임계영역으로 감싸야 함 → list_insert_ordered()는 공유 자원인 sleep_list에 스레드를 하나 추가할 때 race condition이 발생되지 않게 하기 위해 임계 영역 설정이 필요하다.
- 여러 처리 과정이 atomic해야 할 때 그 처리 과정들을 모두 임계영역으로 감싸야 함 → sleep_list에 스레드를 하나 추가한 뒤 해당 스레드의 상태를 blocked로 바꾸는 과정은 atomic해야 한다. sleep_list에 들어간 스레드의 상태가 blocked로 바뀌지도 않았는데 그 사이에 timer_interrupt가 발생하면, sleep_list에 들어갔지만 blocked 상태의 스레드가 아닌 상태가 되므로 list_insert_ordered()함수와 thread_blocked()함수를 같이 임계영역 안에 포함시켜주었다.
- list_insert_ordered()와 thread_block()을 함께 임계 영역으로 감쌌다.
❌ 잘못된 코드 1
timer_sleep()은 무조건 현재 스레드를 재우는 함수이므로 조건문이 필요 없다. 그리고 sleep_list에 스레드를 넣을 때 마다 스레드가 끝나는 시간이 짧은 순서 대로 정렬하는 list_insert_ordered()함수를 사용하면 나중에 다시 깨울 때 앞에서부터 하나씩 peek하면서 조건을 만족하면 pop할 수 있다.
/* 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 ();
//현재 스레드 저장
struct thread *curr = thread_current ();
if(timer_elapsed(start) == ticks){
//sleep list에 스레드 넣기
list_push_back (&sleep_list, &curr->elem);
//현재 실행 중인 스레드를 sleep상태(blocked 상태)로 바꿔주기
thread_block();
}
}
❌ 잘못된 코드 2
thread_wake()에 인자를 전달할 때 더블포인터 문제
thread_wake()는 내가 구현한 함수인데 인자로 포인터를 받았으므로 list_front(), list_pop_front(), list_empty()에 list를 넘겨줄 때는 앰퍼샌드를 안 붙여줘도 된다. 이 문제 때문에 무한 루프가 돌아 timeout이 발생했었다.
새벽 네 시에 이 문제로 다 같이 디버깅해준 분들에게 무한 감사를...🔥
/* tick이 한 번 지나서 타이머 인터럽트가 호출될 때마다
sleep(blocked)상태인 스레드 중 어떤 스레드를 깨워야 할지
조건문을 확인해 골라서 깨우기 */
static void
thread_wake(struct list *list){ //인자로 sleep_list 받기
struct list_elem *e;
// for (e = list_begin (list); e != list_tail (list); e = list_next (e)){
// while (e != list_tail(list))
while(!list_empty(list)){
e = list_front(list);
struct thread *t_now = list_entry(e, struct thread, elem);
int64_t end_time = t_now->end;
if(end_time > ticks) //wake 조건 : 스레드가 끝나는 시간이 현재 시간보다 크거나 같을 때
break;
// list_remove(e); //해당 스레드를 그 스레드가 포함된 리스트에서 제거
list_pop_front(list);
thread_unblock(t_now); //스레드를 ready_list에 넣기
// e = list_next(e);
}
}
⭕️ 정답 코드
위에서 설명했던 것과 같이 구현하였고 스레드가 깨야 하는 시스템 시간인 end값을 비교해서 남은 sleep 시간이 가장 적은 순서대로 sleep_list가 구성되도록 list_less_endtime()이라는 함수를 추가로 구현했다. 이 함수는 list_insert_ordered()의 세 번째 인자로 들어가는 함수인데, pintos-kaist/include/lib/kernel/list.h 에 typedef로 ‘함수 형태’만 정의되어있는 함수이다. 실제로 들어가보면 아래처럼 정의되어 있는데, list_elem타입의 인자 두 개와 void 포인터를 하나 받고 bool 타입을 반환하는 함수를 직접 구현하여 list_insert_ordered()의 인자로 주면 된다. 나는 list_less_endtime()이라는 이름으로 함수를 생성해서 인자로 전달해주었다.
typedef bool list_less_func (const struct list_elem *a,
const struct list_elem *b,
void *aux);
/* Suspends execution for approximately TICKS timer ticks. */
void
timer_sleep (int64_t ticks) {
enum intr_level old_level;
int64_t start = timer_ticks (); //timer_sleep()이 호출된 시점의 시간
int64_t end = start + ticks; //스레드의 sleep이 끝나는 시간
ASSERT (intr_get_level () == INTR_ON); //인터럽트가 발생하면 그 다음 코드를 실행함
struct thread *curr = thread_current (); //현재 스레드 저장
curr->end = end; //현재 스레드의 end값 초기화
old_level = intr_disable(); //인터럽트를 disable시킴 (임계영역 보호)
list_insert_ordered (&sleep_list, &curr->elem, list_less_endtime, NULL); //sleep list에 스레드 넣기
thread_block(); //현재 실행 중인 스레드를 sleep상태(blocked 상태)로 바꿔주기
intr_set_level(old_level); //임계 영역 해제
}
/* Compares the value of two list elements A and B, given
auxiliary data AUX. Returns true if A is less than B, or
false if A is greater than or equal to B. */
/* 인자로 들어온 a, b 스레드 중 끝나는 시간(end)이 a가 더 크면 true, b가 더 크면 false를 리턴 */
bool
list_less_endtime(const struct list_elem *a, const struct list_elem *b, void *aux){
struct thread* t_a = list_entry(a, struct thread, elem);
struct thread* t_b = list_entry(b, struct thread, elem);
//a<b이면 true, a>=b이면 false 리턴
return t_a->end < t_b->end;
}
🥹 오답 노트 🥹
- 타이머 인터럽트가 발생하면 context switch가 발생하는 줄 알고 sleep 중인 스레드를 깨우는 게 아니라 ready queue에서 우선 순위가 가장 높은 스레드로 전환해야 하는 것 아닌가 생각했는데 타이머 인터럽트 개념에 대해 잘못 알고 있었다. 타이머 인터럽트는 시스템 tick이 하나 증가될 때마다 호출되는 인터럽트 핸들러이며 시스템 tick 시간을 하나씩 증가시킨다. sleep-wait를 구현할 때 sleep_list에 자고 있는 스레드들의 endtime과 현재 시스템 시간을 비교하여 깨울 스레드를 모두 깨운다. sleep_list는 이미 endtime 기준 오름차순으로 정렬된 채 삽입되기 때문에 깨울 스레드를 앞에서부터 꺼내서 깨울 시간이 되었는지 검사한다.
/* Timer interrupt handler. */
static void
timer_interrupt (struct intr_frame *args UNUSED) {
ticks++;
thread_tick ();
}
- sleep_list를 선언만 하고 초기화하지 않아서 컴파일 에러가 발생했었다. timer_init()에 list_init(&sleep_list)로 리스트를 초기화해야 한다.
void
timer_init (void) {
/* 8254 input frequency divided by TIMER_FREQ, rounded to
nearest. */
uint16_t count = (1193180 + TIMER_FREQ / 2) / TIMER_FREQ;
outb (0x43, 0x34); /* CW: counter 0, LSB then MSB, mode 2, binary. */
outb (0x40, count & 0xff);
outb (0x40, count >> 8);
intr_register_ext (0x20, timer_interrupt, "8254 Timer");
list_init(&sleep_list);
}
- thread_wake에서 end_time < ticks인데 end_time ≥ ticks로 구현해서 컴파일 에러가 발생했다.
/* tick이 한 번 지나서 타이머 인터럽트가 호출될 때마다 sleep(blocked)상태인 스레드 중
어떤 스레드를 깨워야 할지 조건문을 확인해 골라서 깨우기 */
static void
thread_wake(struct list *list){ //인자로 sleep_list 받기
struct list_elem *e;
while(!list_empty(list)) {
e = list_front(list);
struct thread *t_now = list_entry(e, struct thread, elem);
int64_t end_time = t_now->end;
if(end_time > ticks) //wake 조건 : 스레드가 끝나는 시간이 현재 시간보다 크거나 같을 때
break;
list_pop_front(list); //해당 스레드를 그 스레드가 포함된 리스트에서 제거
thread_unblock(t_now); //스레드를 ready_list에 넣기
}
}
- 그리고 또 자주 발생했었던 컴파일 에러는 새로운 함수를 만들었지만 프로토타입을 맨 위쪽에 선언해주지 않아서 다른 함수에서 호출 시 인식할 수 없었던 문제, .c 파일에 구현한 함수를 .h 파일에 선언해주지 않아서 다른 파일에서 인식할 수 없었던 문제 정도였던 것 같다.
🤯 테스트 에러 🤯
alarm-simultaneous 테스트가 fail이 떴다.

먼저 alarm-simultaneous 테스트 코드가 뭘 테스트 하는지 파악해야 했다.
/* Creates N threads, each of which sleeps a different, fixed
duration, M times. Records the wake-up order and verifies
that it is valid. */
#include <stdio.h>
#include "tests/threads/tests.h"
#include "threads/init.h"
#include "threads/malloc.h"
#include "threads/synch.h"
#include "threads/thread.h"
#include "devices/timer.h"
static void test_sleep (int thread_cnt, int iterations);
void
test_alarm_simultaneous (void)
{
test_sleep (3, 5);
}
/* Information about the test. */
struct sleep_test
{
int64_t start; /* Current time at start of test. */
int iterations; /* Number of iterations per thread. */
int *output_pos; /* Current position in output buffer. */
};
static void sleeper (void *);
/* Runs THREAD_CNT threads thread sleep ITERATIONS times each. */
static void
test_sleep (int thread_cnt, int iterations)
{
struct sleep_test test;
int *output;
int i;
/* This test does not work with the MLFQS. */
ASSERT (!thread_mlfqs);
msg ("Creating %d threads to sleep %d times each.", thread_cnt, iterations);
msg ("Each thread sleeps 10 ticks each time.");
msg ("Within an iteration, all threads should wake up on the same tick.");
/* Allocate memory. */
output = malloc (sizeof *output * iterations * thread_cnt * 2);
if (output == NULL)
PANIC ("couldn't allocate memory for test");
/* Initialize test. */
test.start = timer_ticks () + 100;
test.iterations = iterations;
test.output_pos = output;
/* Start threads. */
ASSERT (output != NULL);
for (i = 0; i < thread_cnt; i++)
{
char name[16];
snprintf (name, sizeof name, "thread %d", i);
thread_create (name, PRI_DEFAULT, sleeper, &test);
}
/* Wait long enough for all the threads to finish. */
timer_sleep (100 + iterations * 10 + 100);
/* Print completion order. */
msg ("iteration 0, thread 0: woke up after %d ticks", output[0]);
for (i = 1; i < test.output_pos - output; i++)
msg ("iteration %d, thread %d: woke up %d ticks later",
i / thread_cnt, i % thread_cnt, output[i] - output[i - 1]);
free (output);
}
/* Sleeper thread. */
static void
sleeper (void *test_)
{
struct sleep_test *test = test_;
int i;
/* Make sure we're at the beginning of a timer tick. */
timer_sleep (1);
for (i = 1; i <= test->iterations; i++)
{
int64_t sleep_until = test->start + i * 10;
timer_sleep (sleep_until - timer_ticks ());
*test->output_pos++ = timer_ticks () - test->start;
thread_yield ();
}
}
timer_sleep()함수가 동시에 sleep한 여러 스레드를 정확히 같은 tick에 깨우는지 5번 반복 확인하는 테스트 코드이다.
세 개의 스레드(thread 0, 1, 2)가 동시에 timer_sleep(10)을 호출했으므로 다음과 같은 결과가 5번 연속으로 출력되어야 한다.
iteration 0, thread 0: woke up after 10 ticks
iteration 0, thread 1: woke up 0 ticks later
iteration 0, thread 2: woke up 0 ticks later
thread 0이 제일 먼저 측정되었고 thread 1, 2는 직후 정확히 깨어났기 때문에 0 tick later로 출력되었으므로 세 스레드가 동시에 깨어난 것을 의미한다.
iteration 0, thread 0: woke up after 10 ticks
iteration 0, thread 1: woke up 1 ticks later
iteration 0, thread 2: woke up 1 ticks later
만약 스레드 1, 2가 원래 일어나야 할 시점보다 1 tick 늦게 깨어난다면 위와 같이 출력될 것이다.
'정글 > Pintos' 카테고리의 다른 글
| [pintos] 1주차 - Priority Scheduling 구현 과정 (1) | 2025.05.20 |
|---|---|
| [pintos] 1주차 - Priority Scheduling 문서 해석 / 코드 해석 (1) | 2025.05.20 |
| [pintos] 1주차 - Alarm Clock 코드 정리 (1) | 2025.05.20 |
| [pintos] 1주차 - 키워드 정리 (2) : Process/Thread, DeadLock, 프로세스 상태/CPU Scheduling (3) | 2025.05.11 |
| [pintos] 1주차 - 키워드 정리 (1) : Semaphore/Mutex, Busy Waiting/Sleep Waiting (0) | 2025.05.10 |