Thread Alarm Clock 단계별 구현 및 테스트하기
이번 포스트에서는 PintOS 프로젝트 1의 첫 번째 과제인 Alarm Clock을 구현하는 과정을 단계별로 정리해보려고 합니다. Alarm Clock 기능은 busy waiting 없이 스레드를 sleep/wakeup 방식으로 재우고 깨우는 기능을 구현하는 것이 핵심이며, 이를 통해 스레드 동기화, 인터럽트 기반 이벤트 처리 등을 체험할 수 있습니다.
주요 목표
timer_sleep(int64_t ticks)
호출 시, 해당 스레드를 지정된 tick 동안 잠들게 하고 정확한 시점에 깨우기- 기존 busy-waiting 방식을 제거하고 sleep list 기반의 효율적인 대기 방식 구현
- 커널 내에서의 동기화 원리, 스레드 상태 전이, 인터럽트 처리 흐름을 직접 다뤄보기
학습 목적
- 스레드 상태 전이 구조 학습
THREAD_RUNNING → THREAD_BLOCKED → THREAD_READY → THREAD_RUNNING
의 순환을 직접 구현하면서, 커널 스케줄링 흐름을 체득하기
- 동기화 원리의 기초 체득
- 인터럽트를 통한 비동기 이벤트 처리 구조와 함께, 임계 구역 보호를 위해
intr_disable()
과intr_set_level()
를 사용하는 이유를 명확히 이해하기
- 인터럽트를 통한 비동기 이벤트 처리 구조와 함께, 임계 구역 보호를 위해
- sleep/wakeup 기반의 리소스 절약형 대기 구현
- busy waiting 대신
sleep_list
를 활용해, CPU 점유 없이 필요한 시점에만 스레드를 깨우는 구조를 설계하기
- busy waiting 대신
(참고) 수정해야 할 파일
- threads/thread.*
- devices/timer.*
Alarm Clock 단계별로 구현하기
1단계: 문제 정의하기 – Busy Waiting 제거
- 기존 구현은
timer_sleep()
호출 시, while 루프로 tick 값을 계속 확인하는 방식으로, CPU 자원을 낭비하고 있음 - 목표는 tick만큼 잠든 후 정확한 시점에 깨우되, 그동안 CPU를 점유하지 않도록 하는 것
- 따라서, 스레드를 BLOCKED 상태로 만들어 스케줄러에서 제외시키고, 깨어날 시간이 되면 다시 READY 상태로 이동
2단계: 흐름 및 구조 설계하기 – Sleep List 기반 구조
핵심 아이디어
sleep_list
라는 전역 리스트를 선언하고, 각 스레드의 깨어날 tick 값을 기준으로 정렬하여 삽입- 타이머 인터럽트가 발생할 때마다 현재 tick과 비교해, 깨울 시점이 된 스레드를 unblock함
스레드 흐름
Running → BLOCKED → READY → Running
핵심 동작 정리
timer_sleep(ticks)
호출 시- 현재 tick + ticks → wakeup_tick 계산
thread_sleep(wakeup_tick)
호출
thread_sleep()
- 현재 스레드의
wakeup_tick
저장 sleep_list
에 정렬 삽입thread_block()
호출 → BLOCKED 상태 진입
- 현재 스레드의
- 매 tick마다
timer_interrupt()
ticks++
후,thread_awake(ticks)
호출sleep_list
맨 앞부터 순회하며wakeup_tick <= 현재 tick
인 스레드들을 깨움
3단계: 함수 단위 설계 및 구현하기
각 함수가 어떤 책임을 가지고 어떤 동작을 해야하며, 어떤 자료구조를 다루는지 정리하는 단계입니다.
(0) 구현 전 준비 사항
- struct thread에 필드 추가
int64_t wakeup_tick
struct list_elem wait_elem
- 전역 sleep_list 선언 및 thread_init()에서 초기화
- cmp_tick() 정렬용 비교 함수 정의 (sleep_list 삽입 시 사용)
// thread의 wakeup_tick 값을 비교하는 비교 함수
// 이 함수는 리스트에 thread들을 'wakeup_tick' 순으로 정렬하기 위해 사용된다.
bool cmp_tick(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED) {
// 리스트 요소 a와 b를 각각 thread 구조체로 변환
struct thread *t1 = list_entry(a, struct thread, wait_elem); // a가 포함된 thread 추출
struct thread *t2 = list_entry(b, struct thread, wait_elem); // b가 포함된 thread 추출
// 두 스레드의 wakeup_tick 값을 비교하여 t1이 먼저 깨워져야 한다면 true를 반환
// 즉, t1이 t2보다 먼저 깨어나야 한다면 t1이 리스트 앞에 오게 된다.
return t1->wakeup_tick < t2->wakeup_tick;
}
(1) thread_sleep() 함수 추가
스레드를 잠들게 하는 핵심적인 처리를 담당합니다.
- 현재 스레드의
wakeup_tick
을 설정하고 sleep_list
에 정렬 삽입- 상태를
THREAD_BLOCKED
로 변경
void thread_sleep(int64_t wakeup_tick) {
// 현재 실행 중인 스레드의 포인터를 가져오기
struct thread *cur = thread_current();
// idle_thread는 시스템 유휴 상태에서 돌아가는 스레드이므로 잠재우면 안 됨
if (cur == idle_thread) return;
// 인터럽트를 비활성화하여 sleep_list에 접근할 때 다른 인터럽트가 끼어들지 못하게 함
enum intr_level old_level = intr_disable();
// 현재 스레드가 언제 깨어나야 할지를 설정 (timer_tick()과 비교할 대상)
cur->wakeup_tick = wakeup_tick;
// sleep_list에 현재 스레드를 삽입하되,
// 깨어날 시점(wakeup_tick)을 기준으로 정렬해서 삽입
list_insert_ordered(&sleep_list, &cur->wait_elem, cmp_tick, NULL);
// 현재 스레드를 비활성화(blocked 상태)로 전환하여 CPU를 양보
thread_block();
// 인터럽트 상태를 이전 상태로 복구 (다시 인터럽트 허용)
intr_set_level(old_level);
}
(2) timer_sleep() 함수 추가
유저 스레드가 호출하는 인터페이스로, 내부에서 thread_sleep()
을 호출합니다.
void timer_sleep(int64_t ticks) {
// 요청한 시간이 0 이하라면 굳이 재울 필요가 없으므로 함수 종료
if (ticks <= 0) return;
// 현재 시점에 ticks만큼 더한 시점을 계산하여,
// 그 시점이 되면 스레드를 깨우도록 한다
int64_t wakeup_tick = timer_ticks() + ticks;
// 계산한 깨울 시점을 넘겨서 현재 스레드 재우기
thread_sleep(wakeup_tick);
}
(3) thread_awake() 함수 추가
타이머 인터럽트가 발생할 때 호출되어, 깨어날 시간이 지난 스레드를 깨웁니다.
void thread_awake(int64_t now_tick) {
// 인터럽트를 비활성화하여 sleep_list 접근 중 동시 수정 방지
enum intr_level old_level = intr_disable();
// sleep_list의 처음 요소부터 순회 시작
struct list_elem *e = list_begin(&sleep_list);
// sleep_list는 wakeup_tick 기준으로 오름차순 정렬되어 있으므로,
// 깨울 스레드가 있는 앞쪽부터 순차적으로 검사하면 됨
while (e != list_end(&sleep_list)) {
// 리스트 요소 e를 thread 구조체 포인터로 변환
struct thread *t = list_entry(e, struct thread, wait_elem);
// 현재 시간이 해당 스레드의 깨울 시간 이상이면
// → 이제 깨울 수 있으므로 리스트에서 제거하고 unblock
if (t->wakeup_tick <= now_tick) {
e = list_remove(e); // 현재 요소를 제거한 다음 다음 요소로 이동
thread_unblock(t); // 깨워서 ready 상태로 전환
} else {
// 남은 스레드들은 아직 깰 시간이 안 됐으므로 검사 종료
break;
}
}
// 인터럽트 상태를 원래대로 복원
intr_set_level(old_level);
}
(4) timer_interrupt() 함수 수정
기존에 tick을 증가시키는 기능에 더해, thread_awake()
호출을 추가합니다.
static void timer_interrupt(struct intr_frame *args UNUSED) {
ticks++;
thread_tick();
thread_awake(ticks); // 이제 매 tick마다 깨울 스레드가 있는지 확인
}
Alarm Clock 테스트하기
Alarm clock의 구현이 끝났다면 이제 테스트를 해볼 차례인데요. 먼저 threads 디렉터리에서 make clean
후 make
로 수정된 C코드를 실행파일로 만들어줍니다. 이후 build 디렉터리로 이동한 다음 make check
를 통해 테스트를 해볼 수 있는데요. 모든 테스트가 전부 실행되기 때문에, alarm으로 시작하는 테스트가 다 끝난 뒤 중간에 종료하시는 걸 추천드립니다.
테스트 이후, build 디렉터리 안에 tests/threads 디렉터리가 새로 생기고, 이 안에 테스트 결과 파일들이 새롭게 생기게 되는데요. .errors
에는 테스트 실행 중 표준 에러(stderr)에 찍힌 메시지, .output
에는 테스트 실행 중 표준 출력(stdout)에 찍힌 내용, .result
에는 테스트 통과 여부(PASS/FAIL) 및 간략한 로그가 포함되어 있습니다.
'크래프톤 정글 > Code 정글(C언어)' 카테고리의 다른 글
[Pintos] Threads: Priority Scheduling - 동기화 Primitive 단계별로 수정하기 (0) | 2025.05.11 |
---|---|
[Pintos] Pintos 학습 프로세스 ver1.0 (0) | 2025.05.10 |
[Pintos] Pintos를 시작하면서 - 협업에 대한 고민, AI의 활용 범위 (0) | 2025.05.09 |
[WebProxy] 캐시 기능이 추가된 동시성 프록시 서버 단계별 구현 및 테스트하기 (0) | 2025.05.07 |
[VSCode] 내가 쓰는 VSCode 유용한 단축키 모음 (0) | 2025.05.06 |