Alarm Clock
1. sleep_list 정렬
기존 thread_sleep() 함수에서는 sleep_list에서 깨울 스레드를 찾기 위해 모든 리스트 요소를 다 뒤져야 했다.
따라서 불필요한 리스트 순회가 존재했다.
sleep_list에 스레드를 넣을 때 localTick 기준으로 정렬하여 삽입하면, 리스트를 전부 순회할 필요 없이 HEAD만 보면 된다.
가장 빨리 깨어나야 할 스레드가 HEAD쪽에 위치하기 때문에 단 한번이라도 깨어날 때가 되지 않은 스레드를 만난다면 그 뒤의 스레드들은 모두 깨어날 때가 아닌 것이고, 따라서 순회를 종료하면 된다.
2. thread_sleep() 함수에서의 thread_block() 위치의 중요성
위와 같은 thread_sleep() 함수이다.
처음엔 thread_block() 함수가 if문 바로 아래, 맨 위에 있었다. 코드의 순서가 크게 중요하지 않을 거라고 여겨 코드를 짰고, Alarm Clock만 구현했을 땐 잘 돌아갔다. 그런데 Priority Scheduling으로 들어가니 갑자기 Alarm Clock 테스트 케이스가 전부 터져 버렸다. 그래서 thread_sleep 함수를 한참동안 살펴보고 수정했는데, 바꿀 수 있는 걸 전부 바꿔봐도 안 돼서 마지막으로 코드 순서를 바꿔봤는데 통과가 됐다.
thread_block() 함수엔 thread의 state를 BLOCKED로 바꾸는 것 외에도 schedule() 함수가 있다.
이 함수가 실행되면 현재 스레드는 sleep 상태로 들어가고 새로운 스레드가 running된다.
그렇기 때문에 thread_block()이 위에 있으면 아래에 있는 curThread->localTick = ticks와 list_insert_ordered가 실행되지 않았던 것이다.
Block과 관련된, 그리고 스케줄링과 관련된 함수들은 순서를 꼼꼼하게 체크해야 한다고 느꼈다.
3. thread_awake() 함수에서의 thread_unblock() 위치의 중요성
위 문제점과 비슷한 상황인데, 일단 이 코드는 성공 코드다.
기존엔 unblock이 remove 위에 있었다. 그때는 테스트 통과가 안됐었는데 unblock을 밑으로 내리니까 통과가 되었다.
하지만, sleep함수에서는 block이었기 때문에 스케줄링이 있었고, 실제로 CPU를 넘겨주기 때문에 순서가 중요했다는 게 이해됐었지만 여기서는 아니었다.
unblock은 그저 thread의 state를 READY로 바꾸고 ready_list에 정렬해서 넣어주는 함수일 뿐이다. 그런데 왜 오류가 났을까? 어차피 함수는 마지막 줄 까지 잘 실행될 텐데?
이유는 간단했다. unblock을 먼저 할 경우 스레드가 sleep_list와 ready_list에 동시에 존재하는 순간이 생기기 때문이다.
sleep에서 지운 후 ready에 넣어야 충돌할 일이 없는데, 먼저 unblock을 해버리면 sleep과 ready에 동시에 들어가 있는 상황이 생긴다. 이것 때문에 오류가 났다고 판단했다.
로우레벨로 갈수록 생각지 못한 사소한 것들 때문에 오류가 나는 경우가 많은 것 같다. 최대한 변수가 없도록 안전하게 코딩하는 습관을 들여야 한다고 느꼈다.
Priority Scheduling
1. timer interrupt마다 preemption이 일어나면 안 된다.
위 사진은 thread_awake 함수에 thread_preemption 함수를 넣은 결과이다. 테스트가 매우 처참하게 터졌다.
thread_awake 함수는 매 timer interrupt마다 실행되고, thread_preemption 함수는 선점하는 함수이기 때문에 CPU를 다른 스레드에게 넘겨주는 thread_yield가 일어난다.
thread_preemption 함수이다. 조건에 부합할 경우 우선순위가 높은 스레드가 CPU를 선점하게 된다.
preemption은 ready_list에 변화가 생기는 모든 부분에 적용되어야 한다. 하지만 thread_awake 함수에서만은 예외였다.
이유는 매 timer interrupt마다(1tick마다) 실행되는 thread_awake에서 선점을 적용시키면 오버헤드가 매우 커질 수 있기 때문이다.
따라서 timer interrupt에 영향을 받는 thread_awake를 제외한 thread_set_priority 함수와 thread_create 함수에만 preemption을 적용시켜야 한다.
2. sema_down 함수에서 if대신 while을 사용하는 이유
sema_down의 메인 로직을 보면 while문으로 되어있다. 그러나 if문으로 바꿔도 기능상 아무런 영향이 없다. 완벽하게 동일하다. 왜냐하면 thread_block()을 통해 스레드는 sleep 상태에 들어가게 되고, while이든 if든 어차피 자고 있기 때문이다.
둘이 차이점이라고 하면 unblock되어 깨어났을 때 while문은 조건을 한번 더 검사하고, if문은 바로 아래로 내려간다는 점 뿐이다. 하지만 어차피 sema_up이 되었을 때 깨어나기 때문에 조건 검사를 안 해도 결과는 같다.
그렇다면 왜 헷갈리게 while문을 쓰는 것일까? 바로 "의도되지 않은 일부 예외상황"을 방지하기 위해서이다.
예외상황 1 : Spurious Wakeup(가짜 깨어남) 상황
예외상황 2 : Race Condition 상황
Spurious Wakeup(가짜 깨어남) 상황
- 대기 중인 스레드가 “아무런 이유 없이” 깨어나는 현상
- 멀티스레드 환경에서 발생할 수 있으며, 이 때문에 "잘못된 작업을 수행할 가능성"이 생긴다.
- while문을 사용할 경우, 스레드가 unblock된 후 “sema->value == 0” 조건을 다시 한번 검사한다.
- 실제로 작업이 수행되기 전 조건을 한번 더 검사하기 때문에, if문에 비해서 안전하게 작업이 수행될 수 있다.
Race Condition 상황
- 두 개 이상의 스레드(프로세스)가 자원에 동시에 접근하려고 할 때 발생한다.
- 접근 순서에 따라 실행 결과가 달라질 수 있기 때문에, "예상치 못한 오류"가 발생할 수 있다.
- 따라서 while문을 사용해서 조건을 한번 더 검사해야, Race Condition으로 인해 조건이 더 이상 참이 아니게 된 경우에도 올바른 처리가 가능하다.
'크래프톤 정글 > TIL' 카테고리의 다른 글
PintOS 프로젝트 2주차 커널 모드, Context Switching, fsync() (0) | 2024.05.28 |
---|---|
PintOS 프로젝트 2주차 [User Programs / Argument Passing] (2) | 2024.05.23 |
PintOS 프로젝트 1주차 [Threads / Priority Donation] (0) | 2024.05.20 |
PintOS 프로젝트 1주차 [Threads / Priority Scheduling] (0) | 2024.05.20 |
PintOS 프로젝트 1주차 [Threads / Alarm Clock] (0) | 2024.05.20 |