정신없는 한 주를 보내고, 오늘은 PintOS의 2번째 프로젝트에 입문하기 위한 준비 운동을 시작했다. 이번 Project 에서는, 정리할 것이 생길 때마다 TIL을 최대한 정리해보려고 한다. 정리 자체는 노션이나 공책에 잘 해왔는데, 이 velog에는 꾸준히 업로드를 못해서 스스로 내심 아쉬웠는데 이번 기회에 만회해보고자 한다.
오늘 저녁에는 협력사 설명회를 다녀왔고, 그 이전엔 Advanced Scheduler에 재도전하느라 아쉽게도 많은 시간을 할애하진 못했다.
그래도 위와 같이 mlfqs 끝까지 테스트 통과에 성공했고, 협력사 설명회에서도 몇 가지 동기부여를 얻었기에 이번 Project 2 도 분명히 잘 해낼 수 있을 것이라는 생각이 든다.
오늘은 중간중간 기운이 남아있을 때 틈틈이 이번 Project 2 에서 다루게 될 파일들을 살펴보고, 대략적인 견적을 내보았다. 이번 주차에도 내 페이스대로 정해진 시간에 최대한 몰입해서 공부하고, 팀원들과 시너지를 발휘하면 무난하게 해낼 수 있을것만 같다.
깃북에서 새로운 코드를 추가할 때 #ifdef 와 #ifndef 를 유의하라는 내용이 있어서, process.c 파일에선 어느 경우에 쓰이는지를 확인해보았다.
#ifdef는 어떤 매크로가 정의되어있는지를 따져보는 기법이다.
#ifdef {매크로명}
해당 매크로가 정의되어 있는 경우 실행될 문장들
...
#else
해당 매크로가 정의되어 있지 않는 경우 실행될 문장들
...
#endif
위와 같은 방식으로 사용되며, 만약 {매크로명} 이란 매크로가 정의되어 있다면, #ifdef 하의 문장들을 컴파일하고, 그렇지 않다면 #else 하의 문장들을 컴파일하게 된다.
PintOS에서는 Project 3에 Virtual Memory(가상메모리)를 구현하게 되는데, Project 2에서 그대로 가져가 사용되어야 하는 코드가 있고, 아닌 코드가 있는 모양이다.
그래서 새로 구현하는 코드를 #ifdef VM 으로 감싸진 블록 내에 잘못 넣었다가는, 현재 Project 2에서는 VM이란 매크로가 정의되어있지 않아서 실행되지 않을 것이고, 이후 Project 3으로 가서나 쓰일 수 있게 된다. #else 에 넣더라도, 혹여나 Project 2와 Project 3에서 동시에 사용되어야 하는 경우 문제가 생긴다.
#ifndef는 위의 반대로, 매크로가 정의되어 있지 않은 경우를 기준으로 한다.
#ifndef _INTTYPES_H
#define _INTTYPES_H 1
이런 식으로, 중복해서 define 함으로써 오류가 발생하는 경우를 방지해주는 용도로도 요긴하게 사용되고 있다. PintOS를 C 언어로 구현해주고 있기에, 이런 문법적인 부분도 중간중간 파고들어 체크해주는 것도 나의 이해도를 높여주고 있다.
또한, Project 1에서 작업한 thread.c 에도 #ifdef USERPROG 라는 조건 하에 처리된 코드들이 있었던 것을 같이 확인해보았다. Project 1과 Project 2의 쓰레드 구현 방식이 달라짐에 따라, thread 관련 함수들에 #ifdef 를 활용해 각 프로젝트마다 다른 코드가 실행되도록 한 것을 파악할 수 있었다.
만약 가이드를 보지 않은 상태에서 #ifndef 와 #ifndef 를 고려하지 않고 아무데나 새로운 코드를 구현했다가는, 어쩌면 무한 디버깅의 늪에 빠졌을 것이다. 역시나 가이드를 충실히 읽고 시작해야겠다는 확신이 든다.
tss.c 의 경우, 깃북에서는 이 파일을 읽어볼 필요는 없다곤 했지만, 그래도 살펴보는 김에 주석을 한번 살펴보았다.
x86-64 structure 인 TSS(Task.-State Segment)는, 프로세서에 내장된 멀티태스킹 지원의 한 형태인 "task"를 정의하는데 사용되는 개념이라고 한다. 하지만 대부분의 x86-64 운영체제는 TSS를 거의 완전히 무시한다고 하는데, portability, speed, flexibility 등을 포함해 이유는 다양하다고 한다.
PintOS 역시 예외는 아니다. 다만, TSS 로만 할 수 있는 작업이 딱 하나 있는데, 바로 user mode 에서 발생하는 인터럽트에 대한 stack switching 이다. 인터럽트가 발생하면, 프로세서는 현재 TSS의 rsp0 (ring 0 stack pointer) 에 들은 것들을 참조하여 어떤 스택을 인터럽트 처리에 사용할지 결정하게 된다.
바로 이 작업을 위해서, 필요한 TSS를 생성하고 최소한의 필드를 초기화해주는 것이 tss.c 파일의 역할인 것이다.
인터럽트가 발생했을 때, x86-64 프로세서의 동작 방식은 interrupt된, 즉 방해받은 코드가 어디에 속하는지에 따라 약간 달라진다.
만약, 그 코드가 interrupt handler와 같은 ring (모드를 얘기하는듯)에 속한다면, stack switch 는 발생하지 않는다. (커널에서 실행 중에 인터럽트가 발생하는 경우에 해당)
반면, 그 코드가 interrupt handler와 다른 ring 에 속한다면, 프로세서는 TSS에 들어있는 그 링에 대한 stack으로 switch 하게 된다.(이 때, 충돌을 방지하기 위해서, 이미 사용 중인 스택으로 switch 하지 않도록 유의해야 한다). 우리가 프로그램을 user space 에서 실행하고 있기에, 현재 실행 중인 프로세스의 kernel stack은 미사용 중이고, 우리는 언제나 이 kernel stack 을 사용하는 것이 가능하다.
따라서 프로세서 스케줄러가 현재 쓰레드에서 새로운 쓰레드로 switch할 때, TSS의 스택 포인터 역시 새로운 쓰레드의 kernel stack을 가리키도록 변경해주게 된다는 것이 핵심인듯 하다.
오늘은 여기까지 !