10시 전까지 목표는 Priority scheduling의 모든 테스트 통과하기 + WIL 작성 및 발표자료 완성하기다.

조건 변수
어떠한 동작을 특정 조건이 만족 되기까지 스레드들을 대기시켰다가, 그 조건이 만족된 이후에 실행되도록 할 수 있는 것으로 이해했다. (참고)
- cond->waiters (세마포어) : 여기서 세마포어 별 우선순위를 바탕으로 release할 세마포어를 고름
- 굳이 조건문 안쓰고 조건변수 쓰는 이유? → 조건문을 사용하면 busy waiting
- 세마포어 쓰면 스레드가 아예 날라가니까 조건변수를 써서 잠깐 재움 (wait시킴)
- 전역 변수보다 (원자성 측면에서) 더 안정적임
cond_wait()와 cond_signal()을 다음과 같이 수정했다.
void cond_wait (struct condition *cond, struct lock *lock) {
struct semaphore_elem waiter;
ASSERT (cond != NULL);
ASSERT (lock != NULL);
ASSERT (!intr_context ());
ASSERT (lock_held_by_current_thread (lock));
sema_init (&waiter.semaphore, 0);
// list_push_back (&cond->waiters, &waiter.elem);
list_insert_ordered(&cond->waiters, &waiter.elem, cmp_priority_sema, NULL);
lock_release (lock);
sema_down (&waiter.semaphore);
lock_acquire (lock);
}
void cond_signal (struct condition *cond, struct lock *lock UNUSED) {
ASSERT (cond != NULL);
ASSERT (lock != NULL);
ASSERT (!intr_context ());
ASSERT (lock_held_by_current_thread (lock));
if (!list_empty (&cond->waiters)) {
list_sort(&cond->waiters, cmp_priority_sema, NULL);
sema_up (&list_entry (list_pop_front (&cond->waiters),
struct semaphore_elem, elem)->semaphore);
}
}
bool cmp_priority_sema(struct list_elem *a, struct list_elem *b, void *aux UNUSED) {
struct semaphore_elem *s1 = list_entry(a, struct semaphore_elem, elem);
struct semaphore_elem *s2 = list_entry(b, struct semaphore_elem, elem);
return list_entry(list_begin(&s1->semaphore.waiters), struct thread, elem)->priority >
list_entry(list_begin(&s2->semaphore.waiters), struct thread, elem)->priority;
}
cond_wait에서는 waiters 리스트에 세마포어의 우선순위 순서대로 삽입하는 방식으로 수정했고, cond_signal에서는 세마포어의 우선순위 기준으로 waiters 리스트를 정렬하는 코드를 추가했다. 테스트 결과는 다음과 같다.

여기까지는 괜찮았는데, priority donation을 구현하는 과정에서 이전에 pass를 받았던 priority-condvar 테스트를 FAIL하는 문제가 발생했다.


Priority donation을 구현하기 위해 수정한 함수들은 다음과 같다.
void lock_acquire (struct lock *lock) {
ASSERT (lock != NULL);
ASSERT (!intr_context ());
ASSERT (!lock_held_by_current_thread (lock));
/* If the lock is not available, store address of the lock.
* Store the current priority and maintain donated threads on list (multiple donation).
* Donate priority. */
struct thread *curr = thread_current();
if (lock->holder != NULL) {
curr->wait_on_lock = lock;
list_insert_ordered(&lock->holder->donations, &curr->d_elem, cmp_priority_donate, NULL);
struct thread *holder = curr->wait_on_lock->holder;
/* If necessary, you may impose a reasonable limit on
depth of nested priority donation, such as 8 levels. */
int cnt = 0;
while (cnt <= 8 && holder) {
if (!holder->wait_on_lock)
break;
holder->priority = thread_get_priority();
holder = holder->wait_on_lock->holder;
cnt++;
}
}
/* Acquires lock for the current thread, first waiting
for any current owner to release it if necessary. */
sema_down (&lock->semaphore);
lock->holder = thread_current ();
}
void lock_release (struct lock *lock) {
ASSERT (lock != NULL);
ASSERT (lock_held_by_current_thread (lock));
lock->holder = NULL;
/* When the lock is released, remove the thread
that holds the lock on donation list and set priority properly. */
// Remove the thread waiting for the lock from donation list
struct thread *curr = thread_current();
struct list_elem *e = list_begin(&curr->donations);
while (e != list_end(&curr->donations)) {
struct thread *t = list_entry(e, struct thread, d_elem);
if (t->wait_on_lock == lock) e = list_remove(e);
else e = list_next(e);
}
// Set priority of the current thread
if (list_empty(&curr->donations))
curr->priority = thread_get_priority_ori();
else {
int highest = get_highest_priority();
int curr_ori = thread_get_priority_ori();
curr->priority = (highest > curr_ori) ? highest : curr_ori;
}
sema_up (&lock->semaphore);
}
디버깅 결과 다음과 같은 결과가 나타났다.
unblock: idle, priority=0
unblock: main, priority=31
TIMEOUT
→ priority-condvar 테스트에서 기대했던 스레드들 (Thread priority 30 starting. 등)은 unblock조차 되지 않았다.
무엇이 잘못 작성되었는지 처음부터 반복해서 함수의 구조를 분석했다. 5시간이 지나고 드디어 코드에서 틀린 점을 찾아냈다! 정말 생각지도 못한 함수에서 이슈가 있었다 🤯

결론부터 말하자면, thread_set_priority()에서 덮어쓰는 current thread의 priority는 현재 priority가 아니라 initial priority였다.
/* 수정 전 */
thread_current()->priority = new_priority;
/* 수정 후 */
thread_current()->priority_ori = new_priority;
- 수정 전의 코드는 현재 priority를 바로 덮어써버리기 때문에 만약 donation을 받고 있는 중이었다면 기부받은 priority가 사라지고 덮어쓰기 된다. 그 결과,
- donation이 무효화되었다.
- 스케줄러는 우선순위를 잘못 판단했다.
- 스레드가 실행되지 않았다.
- "Thread priority XX starting." 출력이 나오지 않았다.
- 수정 후의 코드는 priority_ori를 바꾼다 → donation 중이면 무시되고, 나중에 복원할 때 반영된다.
priority donation을 구현하느라 lock_acquire()과 lock_release()를 많이 수정해서, 이 함수들 중에서 이슈가 생겼을 것이라고 생각해서 이 오류를 찾는데 시간이 더 걸렸던 것 같다. 게다가 GPT도 틀린 코드를 찾는데 별 도움이 안 되었기 때문에, 정말 코드 한줄마다 디버깅을 하면서 테스트를 돌리느라 시간이 벌써 6시가 다 되간다😂

코드 수정 후 priority-condvar, priority-donate-one, priority-donate-multiple 테스트 모두 통과할 수 있었다.
이번 트러블 슈팅을 통해 다음을 확인할 수 있었다.
- priority는 "상태"이지, "설정값"이 아니다: priority는 항상 초기 priority 값과 donations의 결과로 계산돼야 하며, 직접 건드리면 시스템의 상태를 오염시킬 수 있다.
- donation 상태에서는 외부 입력보다 기부받은 우선순위가 더 우선된다: donation 중에는 초기 priority 값만 바꾸고 적용은 refresh_priority()에 맡겨야 한다.
- 우선순위 기반 스케줄링 로직을 일관성 있게 흐름을 유지해야 한다: donate_priority() → refresh_priority() → yield() 이 흐름이 끊기면, 예상치 못한 기아나 오작동이 발생할 수 있다.
이번 문제는 단순히 변수 이름 하나의 실수였지만, 결과적으로 전체 스케줄링이 멈추고 테스트 로그조차 안 나오는 심각한 현상으로 이어졌다. "작은 실수가 시스템 전체를 정지시킬 수 있다"는 운영체제 개발의 핵심 교훈을 몸소 체험한 계기가 되었다. 그래도 최근 면접 때문에 이번 핀토스 과제에 제대로 집중을 하지 못했는데, 트러블슈팅을 하면서 donation, refresh, preemption 등 여러 개념을 더 명확히 정리할 수 있었다.
결과적으로는 작은 실수였지만, 그걸 찾기 위해서 전체 흐름을 다시 그려보고, 테스트 하나하나를 반복하면서 접근하는 과정이 굉장히 의미 있었다.

'Krafton Jungle > 5. PintOS' 카테고리의 다른 글
| [PintOS 2주차] Day 1 (0) | 2025.05.15 |
|---|---|
| [PintOS 1주차] Bonus: Advanced Scheduler (0) | 2025.05.15 |
| [PintOS 1주차] Day 7 (0) | 2025.05.14 |
| [Pintos 1주차] Day 4-6 (0) | 2025.05.13 |
| [PintOS 1주차] Day 3 (0) | 2025.05.10 |