2025. 5. 25. 13:04ㆍ개발
Project 1: Threads - Pintos 스레드 구조부터 우선순위 스케줄링까지
Pintos Project 1에서는 스레드 관리와 스케줄링 정책을 구현하게 됩니다.
Alarm Clock을 통한 busy waiting 제거, 우선순위 기반 스케줄링 및 Nested Priority Donation 구현이 주요 목표입니다.
1-1. Pintos 스레드 구조 이해
Pintos는 각 스레드를 struct thread로 표현합니다. 스레드의 메타 정보와 커널 스택이 1페이지에 함께 존재하며, 아래와 같은 필드들이 핵심입니다:
struct thread {
tid_t tid; // 스레드 ID
enum thread_status status; // 상태 (RUNNING, READY 등)
uint8_t *kernel_stack; // 스택 포인터
int priority; // 스레드 우선순위
...
};
스레드 메모리 구조는 다음과 같습니다:
4KB ----------------------------------
| kernel stack |
|-----------------------------|
| struct thread |
0KB ----------------------------------
커널 스택은 위에서 아래로 성장하므로, stack overflow가 발생하면 struct thread를 덮어쓸 수 있습니다. 이를 방지하기 위해 ASSERT(is_thread(t)) 같은 검사가 들어갑니다.
1-2. Alarm Clock 구현
timer_sleep()은 busy waiting을 제거하고, 스레드를 슬립 리스트에 넣어 대기하게 만드는 기능입니다.
- wakeup_tick: 깨어나야 할 tick 값을 각 스레드에 저장
- sleep_list: sleep 중인 스레드들을 저장하는 리스트
- timer_interrupt(): 매 tick 마다 리스트를 확인하고 깨어나야 할 스레드 깨움
void timer_sleep(int64_t ticks) {
int64_t start = timer_ticks();
thread_sleep_until(start + ticks);
}
핵심은 sleep list를 정렬하여 매 tick마다 전체 순회하지 않도록 최적화하는 것입니다.
1-3. Priority Scheduling
Pintos 기본 스케줄러는 Round-Robin 방식이기 때문에, 이를 priority 기반 스케줄링으로 바꾸는 작업이 필요합니다. 이를 위해 다음과 같은 핵심 수정이 이루어집니다:
- 스레드가
priority값을 가지도록 하고 ready_list를 priority 기준으로 정렬- 더 높은 priority를 가진 스레드가 생기면 즉시 선점 (preemption)
1. ready_list 정렬 기준 함수
bool priority_more(const struct list_elem *a, const struct list_elem *b, void *aux) {
return thread_get_priority(list_entry(a, struct thread, elem)) >
thread_get_priority(list_entry(b, struct thread, elem));
}
이 비교 함수는 ready_list, lock->waiters 등 스레드를 우선순위 기준으로 정렬할 때 사용됩니다.
2. thread_yield()에서 ready_list 정렬 삽입
void thread_yield(void) {
struct thread *curr = thread_current();
enum intr_level old_level;
ASSERT(!intr_context());
old_level = intr_disable();
if (curr != idle_thread)
list_insert_ordered(&ready_list, &curr->elem, priority_more, NULL);
do_schedule(THREAD_READY);
intr_set_level(old_level);
}
스레드가 CPU를 양보할 때, 단순히 list_push_back()으로 넣는 대신 우선순위 기준으로 삽입해야 합니다. 이 로직이 없다면 스레드의 priority를 구현해도 실행 순서가 보장되지 않아 테스트가 실패합니다.
3. thread_create()에서 우선순위 선점 (Preemption)
새로운 스레드를 생성한 직후, 해당 스레드의 우선순위가 현재 실행 중인 스레드보다 높다면 즉시 CPU를 양보해야 합니다. 이를 구현한 코드가 아래와 같습니다:
if (t->priority > curr->priority)
{
thread_yield();
}
thread_create()의 마지막 부분에 추가되며, 현재 스레드가 CPU를 점유하고 있지만, 더 높은 우선순위의 스레드가 ready_list에 생겼다면 스스로 양보하도록 합니다.
이 선점 로직이 없으면 priority-preempt 테스트에서 실패하게 됩니다. 예를 들어 낮은 priority의 스레드가 thread_create()로 높은 priority 스레드를 만들고도 계속 실행되는 현상이 발생합니다.
관련 테스트
priority-preempt: 더 높은 우선순위 스레드가 생기면 즉시 CPU를 양보하는가?priority-change: 실행 중에 priority가 변경될 경우에도 정렬이 유지되는가?donate-multiple: 여러 스레드가 동시에 donation할 때, 올바른 priority 순서를 유지하는가?
위 테스트들은 모두 ready_list 정렬, thread_yield() 수정, thread_create() 선점 로직이 올바르게 작동해야 통과됩니다.
요약
Priority Scheduling은 단순히 숫자 필드를 추가하는 것이 아니라, 스레드를 리스트에 넣고 꺼낼 때 모두 우선순위를 기준으로 정렬해야 작동합니다. 또한, 새로운 스레드가 만들어졌을 때 선점할 수 있도록 thread_create()에 선점 조건을 추가해야 진짜 우선순위 스케줄러가 됩니다.
1-4. Priority Donation (Nested)

그림 설명
- 높은 우선순위 스레드(H)가 락을 요청할 때, 락을 보유한 낮은 우선순위 스레드(L)에게 자신의 우선순위를 일시적으로 "기부"함
- L의 우선순위가 일시적으로 H의 수준으로 상승
- 중간 우선순위 스레드(M)가 더 이상 L을 선점할 수 없게 됨
- L이 빠르게 작업을 마치고 락을 반환하면, 원래의 우선순위로 복귀
- H가 락을 획득하고 실행을 계속함
Priority Inversion 문제를 해결하기 위해, 낮은 우선순위 스레드가 lock을 보유했을 때, 높은 우선순위 스레드의 priority를 기부(donation)해야 합니다.
lock->holder의 priority를donate_priority()로 상향lock->waiters정렬 유지- Nested Donation: 최대 깊이 제한 (예: 8단계)
- lock release 시 priority 회수
void donate_priority(struct thread *t) {
for (int depth = 0; depth < MAX_DEPTH; depth++) {
if (t->wait_on_lock == NULL) break;
...
t->wait_on_lock->holder->priority = max(t->priority, holder->priority);
t = t->wait_on_lock->holder;
}
}
기부를 받은 스레드는 lock을 release할 때 priority를 복구해야 하며, 이를 위해 original_priority 필드가 필요합니다.
1-5. 테스트 전략 및 디버깅 예시
다음 테스트들은 이 기능들이 올바르게 동작하는지를 확인합니다:
priority-donate-one: 단일 기부가 제대로 작동하는가?priority-donate-nest: nested donation 2단계 이상 처리 가능한가?priority-donate-multiple: 여러 스레드가 하나의 스레드에 동시에 기부할 때
🔍 디버깅 팁
printf("donating from %d to %d\n", a->priority, b->priority);로 donation 로그 출력ASSERT(is_thread(t))실패 시 스택 오염 의심bt명령어로 GDB에서 backtrace 확인
마무리
Project 1은 스레드의 생성, 정렬, 상태 전이, lock을 통한 동기화까지 운영체제 스케줄링의 기본을 모두 담고 있습니다. 각 단계마다 디버깅을 철저히 하며, 스레드의 상태 변화, ready_list의 정렬 상태, lock 보유 여부를 주의 깊게 관찰해야 합니다.
다음 편에서는 Project 2: System Call과 사용자 프로세스를 정리할 예정입니다.
'개발' 카테고리의 다른 글
| [Pintos] userprog- exec 개발하면서 겪었던 문제 (0) | 2025.05.27 |
|---|---|
| 궁금증 pintos에서 Condition Variable(condvar)의 동작방식과 존재이유 (0) | 2025.05.25 |
| pintos 프로젝트 시작하기 (0) | 2025.05.25 |
| 프록시 서버에서 캐시 최신화 방법들 (0) | 2025.05.15 |
| 프록시 서버 구현 (0) | 2025.05.14 |