PintOS 프로젝트: 타이머 인터럽트로 알람 시계(Alarm Clock) 구현하기
본 포스트는 PintOS Project 1의 기능 중 하나인 알람 시계(Alarm Clock) 구현에 대한 상세한 내용을 다룹니다. 이 기능은 운영체제의 핵심 개념인 인터럽트와 스레드 스케줄링을 깊이 있게 이해하는 데 큰 도움이 됩니다. 관련 용어에 대한 기본 지식이 있다고 가정하고, 기능 구현에 초점을 맞춰 딥다이브하는 내용이므로 다소 복잡하게 느껴질 수 있습니다.
핵심 개념: 타이머 인터럽트와 ISR
알람 시계를 구현하기 위해선 먼저 컴퓨터의 '시간' 개념을 관리하는 두 가지 핵심 요소를 이해해야 합니다.
1. 타이머 인터럽트 (Timer Interrupt)
타이머 인터럽트는 시스템 타이머 칩에 의해 일정 주기(Pintos에서는 보통 100Hz)로 발생하는 하드웨어 인터럽트입니다. 이는 운영체제가 시간 기반의 작업을 스케줄링하고 관리할 수 있도록 주기적으로 CPU에 신호를 보내는 생명줄과도 같습니다.
타이머 인터럽트가 없다면, 특정 시간 후에 작업을 실행하는 것 자체가 불가능하며 모든 작업이 순차적으로만 실행될 것입니다.
주요 역할
- CPU 시간 분배 및 스케줄링: 주기적인 인터럽트는 현재 실행 중인 프로세스를 잠시 멈추고, 다른 프로세스에게 CPU를 할당할 기회를 줍니다. 이것이 바로 멀티태스킹의 기본 원리입니다.
- 대기 시간 관리:
timer_sleep()
과 같이 특정 시간 동안 대기(sleep)하는 스레드가 정확히 그 시간이 지나면 다시 깨어날 수 있도록 하는 기준이 됩니다. 이 시간의 기본 단위를 틱(tick)이라고 합니다. - 시스템 타이밍: 시스템 부팅 후 경과 시간, 프로세스별 실행 시간 등 모든 시간 관련 측정은 타이머 인터럽트를 통해 가능해집니다.
2. ISR (Interrupt Service Routine)
타이머 인터럽트가 발생하면 CPU는 현재 하던 일을 즉시 멈추고, 이 인터럽트를 처리하기 위해 미리 지정된 함수를 실행합니다. 이 함수가 바로 인터럽트 서비스 루틴(ISR) 또는 인터럽트 핸들러입니다.
ISR의 동작 방식
- 인터럽트 발생: 타이머 칩이 CPU에 인터럽트 신호를 보냅니다.
- ISR 실행: CPU는 현재 작업을 중단하고, 해당 인터럽트에 약속된 ISR 함수(Pintos에서는
timer_interrupt()
)를 실행합니다. 이 루틴은 전역ticks
변수 증가, 잠든 스레드 상태 갱신 등의 작업을 수행합니다. - 복귀: ISR의 모든 작업이 끝나면, CPU는 멈췄던 원래 코드로 돌아가 작업을 계속 수행합니다.
스레드 상태(Thread Status)의 이해
알람 시계는 스레드를 '재웠다가' '깨우는' 기능입니다. 이를 위해 스레드의 상태를 명확히 정의하고 관리해야 합니다.
enum thread_status {
THREAD_RUNNING, /* 현재 CPU에서 실행 중인 스레드 */
THREAD_READY, /* 실행 준비가 완료되어 CPU 할당을 기다리는 스레드 */
THREAD_BLOCKED, /* 특정 이벤트를 기다리며 실행이 중단된 상태 (대기 중) */
THREAD_DYING /* 실행이 끝나 곧 제거될 스레드 */
};
THREAD_RUNNING
: 현재 CPU 코어를 점유하고 명령어를 실행 중인 상태. 단일 CPU 시스템에서는 오직 하나의 스레드만 이 상태를 가집니다.THREAD_READY
: CPU만 할당받으면 언제든 실행될 수 있는 상태.ready_list
에서 자신의 차례를 기다립니다.THREAD_BLOCKED
:timer_sleep()
을 호출한 스레드가 이 상태로 전환됩니다. 깨어날 시간이 되기 전까지는 스케줄링 대상에서 제외됩니다.THREAD_DYING
: 모든 작업을 마치고 자원 해제를 기다리는 상태입니다.
알람 시계 구현: 함수 설계 및 코드
사전 준비: 초기화 및 구조체 필드 추가
먼저 잠든 스레드들을 관리할 sleep_list
를 선언 및 초기화하고, 각 스레드가 자신이 깨어날 시간을 기억하도록 thread
구조체에 필드를 추가해야 합니다.
1. thread.c
- sleep_list 선언 및 초기화
/* thread.c */
// 잠든 스레드들을 관리할 리스트 선언
static struct list sleep_list;
...
void thread_init (void) {
...
// 시스템 초기화 시 sleep_list도 초기화
list_init (&sleep_list);
...
}
2. thread.h
- thread 구조체에 필드 추가
스레드가 깨어날 시간을 저장할 wakeup_ticks
필드를 추가합니다.
/* thread.h */
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 wakeup_ticks; // 스레드가 깨어날 시간을 저장할 필드
...
};
작성/수정해야 할 함수들
1. timer_sleep(int64_t ticks)
사용자에게 노출되는 인터페이스 함수입니다. 현재 스레드를 주어진 ticks
만큼 재웁니다.
- 역할:
ticks
가 유효한지 검사하고, 현재 시간(timer_ticks()
)에ticks
를 더해 최종적으로 깨어날 시간(wake_up_time
)을 계산한 뒤,thread_sleep()
을 호출합니다.
2. thread_sleep(int64_t wake_up_time)
timer_sleep
에서 호출되는 내부 함수로, 스레드를 실제로 재우는 역할을 합니다.
- 역할: 현재 스레드의
wakeup_ticks
필드에 인자로 받은wake_up_time
을 저장하고,sleep_list
에 스레드를 추가한 뒤,thread_block()
을 호출하여 스레드를BLOCKED
상태로 만듭니다.
/* thread.c */
// wake_up_time을 기준으로 스레드를 sleep_list에 삽입하고 재운다.
void thread_sleep(int64_t ticks) // PintOS 원본에서는 인자가 wake_up_time이 아닌 ticks일 수 있음
{
struct thread *curr;
enum intr_level old_level;
old_level = intr_disable(); // 경쟁 상태(Race Condition) 방지를 위해 인터럽트 비활성화
curr = thread_current();
ASSERT(curr != idle_thread); // idle 스레드는 재우면 안됨
curr->wakeup_ticks = ticks; // 깨어날 시간 저장
list_insert_ordered(&sleep_list, &curr->elem, thread_wake_time_compare, NULL); // sleep_list에 정렬하여 삽입
thread_block(); // 스레드를 BLOCKED 상태로 전환
intr_set_level(old_level); // 인터럽트 상태 복원
}
3. thread_wake_time_compare(...)
sleep_list
를 wake_up_time
기준으로 정렬하기 위한 비교 함수입니다.
- 역할: 두 스레드의
wakeup_ticks
를 비교하여 더 작은 값을 가진 스레드가 리스트의 앞쪽에 오도록 합니다. 이를 통해thread_awake
함수가 효율적으로 작동할 수 있습니다.
/* thread.c */
// 두 스레드의 wakeup_ticks를 비교, a의 시간이 더 빠르면 true 반환
bool thread_wake_time_compare(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED)
{
struct thread *st_a = list_entry(a, struct thread, elem);
struct thread *st_b = list_entry(b, struct thread, elem);
return st_a->wakeup_ticks < st_b->wakeup_ticks;
}
4. thread_awake(int64_t curr_ticks)
sleep_list
를 순회하며 깨어날 시간이 된 스레드를 깨우는 함수입니다.
- 역할:
sleep_list
의 맨 앞부터 확인하여,wakeup_ticks
가 현재 시간(curr_ticks
)보다 작거나 같은 스레드를 모두 깨웁니다. '깨운다'는 것은 스레드를sleep_list
에서 제거하고ready_list
에 삽입하여 상태를THREAD_READY
로 변경하는 것을 의미합니다. (list_remove
와thread_unblock
호출)
5. (수정) timer_interrupt(struct intr_frame *args)
모든 것을 하나로 묶는 핵심 핸들러입니다.
- 역할: 매 타이머 인터럽트마다 호출됩니다.
- 수정 내용: 기존 코드에
thread_awake(ticks)
를 호출하는 로직을 추가하여, 매 틱마다 잠든 스레드를 깨울 시간이 되었는지 확인하도록 합니다.
/* timer.c */
static void timer_interrupt(struct intr_frame *args UNUSED)
{
ticks++; // 전역 틱 증가
thread_tick();
thread_awake(ticks); // 매 틱마다 깨울 스레드가 있는지 확인
}
알람 시계 전체 실행 흐름 요약
timer_sleep()
호출: 특정 스레드가timer_sleep(100)
을 호출하여 100틱 동안 대기를 요청합니다.- 깨어날 시간 계산: 현재
ticks
가 500이라면, 스레드의wakeup_ticks
는600
으로 설정됩니다. - 대기 목록 추가 및 차단: 해당 스레드는
sleep_list
에wakeup_ticks
600을 기준으로 정렬되어 추가되고, 상태는THREAD_BLOCKED
로 변경됩니다. 스케줄러는 다른READY
상태의 스레드를 실행합니다. - 타이머 인터럽트 발생: 시스템 타이머가 주기적으로 인터럽트를 발생시키고,
timer_interrupt
핸들러가 호출됩니다.ticks
는 1씩 계속 증가합니다. (501, 502, ...) - 대기 목록 확인:
timer_interrupt
내의thread_awake(ticks)
는ticks
가 600이 되는 순간,sleep_list
에서wakeup_ticks
가 600인 스레드를 발견합니다. - 스레드 상태 변경:
thread_awake
는 이 스레드를sleep_list
에서 제거하고,thread_unblock
을 호출하여ready_list
에 추가합니다. 스레드의 상태는THREAD_READY
로 변경됩니다. - 실행 재개:
ready
큐에 들어간 스레드는 스케줄러에 의해 다시 CPU를 할당받아timer_sleep()
호출 이후의 코드를 계속 실행하게 됩니다.
'크래프톤 정글 > 핀토스' 카테고리의 다른 글
[크래프톤 정글 Week 08 ] Pintos-project1-Priority Scheduling 키워드 정리 (1) | 2024.11.14 |
---|---|
[크래프톤 정글 week 08] Pintos-Project1 키워드 정리 (4) | 2024.11.12 |