사실 발제 설명 듣기 전까지는 핀토스가 뭔지도 몰랐는데 그냥 OS를 만드는거였다. 머?!?!?!? 컴퓨터 시스템을 내가 만든다고????
막막했는데 생각보다 과제설명서가 아주 구체적이고 자세해서 금방 이해는 됐다. 구현은 다른 얘기지만...ㅎ
근데 문제는 이번주를 좀 여유롭게 보냈다. 이번 주차 문제 두개 중에 하나를 우리 팀이 엄청난 선두주자로 여유롭고 깔끔하게 끝내버려서 자만심이 좀 생겼다. 두번째도 금방 하겠지 뭐~ 했는데 그 뒤로 팀원 한 명이 계속 바빠서 팀원 오면 해야지~~ 라고 했는데 그 팀원은 슈퍼천재라 내가 삼일동안 못 구현한걸 두시간만에 해버려서 나만 낙동강 오리알 신세가 돼버렸다... 뭐 누굴 탓하겠어 내 잘못이지..
그런고로 RB트리 때와 비슷하게 <시간 내에 절대 할 수 없을 것 같은 남은 양>에 더하여 <그게 온전히 내가 열심히 안했기 때문>이라는 생각에 자괴감에 쩔은 주차였다. 핀토스는 5주동안 연속된 내용을 하는거라 첫 주차 못 따라가면 뒤에도 안될 확률이 높은데 첫 단추를 이렇게 꿰면 어떡하나.....
지금 다른 때와 다르게 이렇게 구구절절 감상을 쓰는 이유는 이런 이유로 WIL 쓸 것이 거의 없기 때문이다. 보통 트러블슈팅이나 새롭게 알게 된 사실, 실수했던 것들을 쓰는데 트러블슈팅한 기억이 거의 없다.. 안되는 경우는 많았는데 디버거를 돌릴 수는 없고 gdb는 왜인지 나만 안되고 printf를 찍어서 어디서 멈추나 확인했는데 어셈블리 명령어면... 더 이상 할 수 있는게 없어ㅋ.... 진짜 이렇게 디버깅조차 불가능 한 적이 없었는데 깊은 수렁 속으로 빠지는 느낌이다.
alarm-clock 구현 시작할 때, 다같이 로직 회의를 하고 구현은 각자 하자고 했다.
내가 생각한 로직은 스케줄러가 cpu에 올릴 새로운 스레드를 찾을 때마다(실행중이던 스레드가 끝나거나 time slice에 밀려 ready list로 밀려날 때) timer_interrupt에서 thread_sleep으로 시그널을 보내고 sleep 안에서 시그널을 받을 때만 시간을 확인하고 깨어나야 할 지 결정해야 된다는 거였다. 세마포어를 공부하자마 회의를 해서 세마포어 개념이 약간 섞여 나왔다. 어차피 CPU도 공유 자원이니 그게 그거 아닌가? 하는 생각이 들었던 것 같다.
그런데 다른 팀원은 벌써 세마포어가 나올 정도로 복잡할 리 없다, thread.c 내에서 끝낼 수 있다는 주장이었다. waiting 큐를 하나 더 만들어서 sleep하는 스레드들을 넣어놨다가 시간이 되면 깨운다고 했다.
여기서 내가 가진 두 가지 의문점은 그러면 어차피 시간을 계속 체크해야 깨울 시간인지 알 수 있을텐데
while문을 계속 도는게 괜찮은가 busy wait을 피하는게 아니라 그냥 다른 함수에 전가하는 것이 아닌가였다.
이걸 생각하려면 "busy wait"의 정의부터 통일해야 했다.
팀원은 "ready 큐에 있는걸 run 상태로 만들었는데 그게 아직 sleep할 시간이 남은 스레드라 다시 ready 큐로 내리고 또 ready 큐에서 하나 뽑아서 run 상태에 올리고를 반복하게 되는 것"이 busy wait라고 했다.
나는 "원래 wait하는 동안 잠들어서 아무것도 안 해야 하는 스레드들이 while문을 계속 돌면서 시간을 체크하는게 busy해서" busy wait라고 생각했다.
의견 합의가 안된 상태에서 계속 고민하는데, 그러면 waiting 큐를 만들긴 하는데 시간을 계속 체크하지 않을 방법은 무엇인가...하고 멍을 때리고 있었다. 아무리 생각해도 timer interrupt 함수를 활용해야 할 것 같았다.
아 그러면 팀원꺼랑 내꺼랑 합쳐야 하나? timer interrupt에 waiting 큐를 추가하면 사실상 내가 아까 말한거 아닌가? 하는 생각의 흐름이 지나다가 유레카!! 어차피 timer interrupt는 매 tick마다 실행되니까 그 때마다 waiting 큐를 확인해서 깨울 애들을 찾으면 되겠구나! 그럼 thread_sleep은 무슨 역할을 해야하지? 팀원이 "아직 일어나면 안될 애들이 ready 큐에 들어가는 것 자체가 문제. 걔를 아직 sleep 중인지 확인하는게 CPU를 busy하게 만든다"라고 했는데, thread_yield를 까보면 현재 스레드를 ready 큐 맨 뒤에 추가하는게 있기 때문에 thread_sleep에서 이 함수는 빠져야 한다. 그렇다면 thread_sleep은 자야 하는 스레드를 waiting 큐에 넣는것만 해주면 되겠다! 그리고 waiting 큐는 남은 시간이 적은 순으로 정렬하면 탐색을 빠르게 할 수 있네! 하고 로직 완성~!
꽉 막혀있던 생각의 흐름이 딱 한 줄기만큼 뚫리고 나니까 줄줄 이어져서 깜짝 놀랐다. 논리 흐름대로 생각하고 나니 어떤 함수를 어떻게 바꾸어야 할지도 바로 다 잡혀서 수도코드도 1분만에 나왔고 구현은 그대로 진행하기만 하면 되었다.
그래서 결론은, 처음 세마포어 이야기 했을 때는 이게 맞는 것 같은데 왜 아니라는 거지... 과제설명서에 세마포어 개념까지 쓰여 있는데.. 라고 생각했는데 결국은 팀원의 말이 맞았어서, 내 로직이 말이 된다고 해도 더 좋은 길은 없는지 고민하고 다른 사람의 의견을 수용하는 방식을 배워야 겠다.
void thread_awake(int64_t cur_time){
for (struct list_elem* e = list_begin (&waiting_list); e != list_end (&waiting_list);) {
struct thread *cur_thread = list_entry (e, struct thread, elem);
if (cur_thread->wakeup_time >cur_time) return;
e = list_remove(e);
thread_unblock(cur_thread);
}
};
자고 있는 스레드를 깨워주는 thread_awake를 작성할 때 내 코드가 팀원의 코드와 딱 한 줄만 다른데 동작을 안해서 트러블슈팅 한 내용이다.
원래 팀원은 while문을 돌면서 waiting list를 탐색하는 방식이었는데 list.h에 for문으로 순회하는 방식이 써있길래 그 방식으로 바꿨더니 동작을 안 했다.
이전이랑 순회 방식 빼고는 다 똑같이 했는데 딱 하나 다른게 깨운 원소를 리스트에서 빼주는 remove 동작이었다. 원래의 코드는 list_remove(&t->ele)였는데 list_remove(e)로 바꾸었길래, 그러면 e랑 &t->ele가 달라서 안되는건가?하고 프린트 해봤지만 정확히 같은 주소를 가리키고 있었다...
아무리 생각해도 remove 문제인 것 같아서 유일한 차이점인 remove의 위치를 여기저기 바꿔 가면서 해봤더니 unblock보다 remove를 먼저 해주면 동작을 했다!
우리가 생각한 이유는 다음과 같다. unblock 라인을 실행한 뒤 remove 라인을 실행하기 전에 external interrupt가 일어나면, 해당 스레드는 unblock되어 ready 큐에 들어가 있는데 waiting list에도 들어있는 이상한 상황이 되는 것이었다.
그렇다면 두 라인 사이에 interrupt가 발생하는걸 막아주면 되나?라고 생각하고 intr_disable()을 해봤는데 intr_context failed가 나오는 걸 보니 이미 interrupt가 발생한 상황인 듯 했다. awake를 timer_interrupt 안에서 부르기 때문에 이미 interrupt 중인데, 지금 하고 있는 애를 막아달라고 해서 문제인 것 같았다.