정글의 커리큘럼은 매 주가 놀라움의 연속이다. 매 주차 발제가 끝나면 "생각보단 할만해 보이는데?" 란 기분이 든다. 그러나 그것도 잠시, "아 고수준이 아니라 저수준이었지.." 와 같은 깨달음을 상기시키며, 본격적으로 구현에 들어가게 된다. 확실히 c언어와 포인터 개념이 들어간 저수준 프로그래밍에서는 머릿속에서 그릴 수 있는 depth를 항상 넘겨버리기 일쑤이고, 노트와 펜 없이 그런 복잡한 플로우를 따라가기는 벅찬 경우가 많다. 더욱이 그러한 코드들로 이루어진 방대한 코드베이스들을 들여다 보고 있자면 막막함이 찾아온다. 그렇게 고통스러운 나날들을 며칠 보내고 나면 이제 전체 윤곽이 어느정도 그려지기 시작하면서, 숨통이 트인다. 그리고 한 주가 끝나갈 무렵, 결국 모든 퍼즐이 맞춰지면서 비로소 큰 그림을 이해할 수 있게 된다. 그러나 이러한 기쁨과 자신감은 언제 그랬냐는듯 새로운 주차에 들어가면 온데간데 사라지고, 발제 첫 날의 나로 되돌아간다. 마치 쓰레드의 life cycle 처럼, 매주 더닝-크루거 효과를 체험하는 듯하다.
pintos 는 그 자체로 지금까지의 정글 커리큘럼을 총망라한 집합체라 할 수 있겠다. 특히 매 주차마다 제공되는 노션 메뉴얼만 봐도 그렇다. 필요한 사전지식 및 참고하면 좋은 것들에 대한 내용이 상세히 포함되어 있을뿐만 아니라, qemu 에뮬레이터를 포함한 초기세팅도 스텝 바이 스텝으로 작성되어 있다. 또한 그동안 학습했던 내용들이 각 챕터별로 필요한 내용에 매핑되어있기도 하다.
나의 경우엔, 메뉴얼을 있는 그대로 따르지는 않는 편이다. 특히 기존에 어느정도 알고 있는 내용일 경우, 나 스스로 컨트롤 할 수 있다고 믿기 때문이다. 여기서 첫 이슈가 생겼다. 메뉴얼에서는 AWS EC2 Ubuntu 22.04 (x86_64) 환경이라고 명시되어 있었지만, 나는 개인 리눅스 서버에서 작업을 진행했다. 왜냐하면 리눅스 배포판 자체에 따른 차이는 크게 없을 것이라 생각했기 때문이다. 그러나 make 명령어를 실행하니, 타입 관련 컴파일 에러 메시지가 출력이 된다. 나는 gcc 버전 차이에 따른 사소한 이슈일 것이라 생각하고, 타입 에러들을 하나하나 고쳐나갔다. 그러나 고칠 때마다 다른 곳에서 또 타입 에러가 발생하곤 했는데, 그럼에도 계속 고쳐 나갔으나 결과적으로는 단시간에 해결할 수 없는 이슈를 만나 중단하고, 결국, Ubuntu 22.04 환경을 사용하기로 했다. 야크셰이빙을 할 충분한 시간이 없었다는 것이 좀 아쉬웠지만, 배포판 이슈가 아닌 gcc 버전 이슈 였다는 것을 밝혔다는 데에 우선 만족을 했다. 이때, 내 리눅스 시스템의 gcc는 14 버전이었고, Ubuntu 22.04 의 gcc 버전은 11 이었다.
P.S.
rb tree, malloc lab, proxy lab 에서도 환경 설정 시, 메뉴얼을 있는 그대로 따르지 않았지만 큰 문제는 없었다. 다만, malloc lab 에서는 ./mdriver 명령어 수행 시, 출력되는 기본 score가 조금 다른 이슈가 있긴 했으나, 중요한 것은 아니었고, proxy lab 에서는 sprintf 함수의 %s 이슈가 있었으나, 간단한 코드 수정으로 해결할 수 있었다.
메뉴얼에서 제공되는 스텝을 모두 수행한 뒤, 나는 테스트 케이스를 채점하는 과정을 분석해보았다. 왜냐하면 메뉴얼에서 제시하는 make check 명령어가 시간이 매우 오래 걸렸기 때문이었다. 처음에는 컴파일 및 빌드 과정이 오래 걸리는 것으로 생각했지만, 실제로는 특정 몇몇 테스트 케이스가 대부분의 시간을 차지했다. 이는 매우 비효율적이다. 테스트 케이스는 각 기능에 따라 별개의 그룹으로 묶을 수 있는데, 아직 구현하지 않은 기능은 전혀 테스트할 필요가 없었기 때문이다. 그래서 이를 각각 개별로 테스트할 수 있는 셸 스크립트를 만들었다.
메뉴얼에 대해 한 가지 아쉬웠던 점이 있다. 메뉴얼에 포함되어 있던 Georgia Tech OS 강의를 듣는데에 이틀을 소요했다는 것이다. 코드를 분석하기 전에 필요한 개념들을 먼저 습득하는 것이 코드 분석을 가속화시킨다는 점은 분명하지만, pintos의 경우에는 pintos 만의 특수한 환경에 대한 이해가 더 중요했다. 일반적인 OS 지식과 정확히 일치하지 않는 부분들이 꽤 많았기 때문이다. mlfqs(multi-level feedback queue scheduler) 를 좀 더 깊이있게 살펴볼 기회가 없었던 것이 매우 아쉽다.
OS 강의를 들을 때가 아닌, 본격적으로 코드 분석을 시작한 시점부터 점점 프로젝트 전체에 대한 윤곽이 잡히기 시작했다. 개념으로만 알고 있던 것들이 실제로 어떻게 작동하는건지 코드 레벨에서 디테일을 채울 수 있기 때문이다. 또한, 추상화의 레벨도 코드 레벨에서 함께 이해할 수 있다. 나의 경우에는 이러한 부분에서 기존에 알고 있던 것들에서 몇몇 착각을 하고 있었다는 것을 깨닫게 되었다. 새삼 코치님이 강조하신 말이 떠오른다. 코드 레벨에서 이해를 하는 것이 가장 정확하다.
코드 레벨에서 살펴본 것 중 가장 인상깊었던 것은 인터럽트와 컨텍스트 스위칭이다. timer_interrupt 함수를 분석하면서 자연스럽게 인터럽트 발생 전후 과정 대해서 추적하게 되었다. 이때 하나의 함수를 차례로 따라가다보니 결국 thread_launch, do_iret 함수까지 도달하게 되어 자세히 살펴보게 되었다. 사실 첫 의문은 thread 의 context switching 시, 인터럽트 enable, disable 정보를 어떻게 저장하고 불러오는지에 대한 것으로부터 시작됐는데, 이를 코드 레벨에서 정확히 어느 시점에 일어나는지 이해하고 싶었기 때문이다. 하지만 c언어 레벨에서는 그러한 것을 직접 다루는 코드를 찾지 못했고, thread_launch, do_iret 내의 어셈블리어에서 다룬다는 것을 알아냈다. 여기서 나는 왜 갑자기 어셈블리어를 사용하는지 궁금했고, 꼭 사용을 해야하는지도 궁금했다. 결과적으로는, 컨텍스트 스위칭과 인터럽트를 다루기 위해서는 cpu의 레지스터들을 직접 사용할 수 있어야 했고, 이 작업은 c언어 레벨에서는 힘들기 때문이다. 또한 인터럽트 enable, disable 정보는 interrupt stack frame 인 intr_frame 구조체에 저장되며 이는 thread 구조체 내부에 포함되어 있다. 정확히는 uint64_t 타입의 eflags 변수(eflags 레지스터를 의미)에 사전 정의된 값인 #define FLAG_IF (1<<9) 를 통해 연산을 하고, 이를 mov %%rbx, 16(%%rax) 명령어를 통해 현재 인터럽트 플래그(IF) 를 저장, 그리고 현재 메모리에 저장된 전환될 쓰레드의 intr_frame를 mov %%rcx, %%rdi, call do_iret 를 통해 복원하는 과정으로 진행된다.
이제 정글의 중반에 접어든 이 시점에서, pintos는 그 동안의 지난 정글의 커리큘럼을 되돌아보게 만든다. 몇 번이나 강조해도 지나치지 않는 정글 커리큘럼의 치밀함이 여기서 또 등장해야겠다. rb tree, malloc lab, proxy lab 에서의 학습들이 하나도 빠짐없이 모두 관련있는 내용인 것이다. 내가 정글에 들어온 목적, CS 지식 쌓기를 충실하게 수행 중인 셈이다.
정글 커리큘럼의 꽃인 pintos project의 첫 주차가 이제 막 끝이 났다. 항상 그랬듯이, 다음 스텝에서는 또 어떤 험난한 여정들이 기다리고 있을지 기대가 된다.
P.S.
노션 메뉴얼에 지난 기수의 질문 모음집이 있다. 처음 메뉴얼을 훑어볼 당시에는 잘 느껴지지 않았던 질문의 의도들이 직접 코드를 어느정도 분석해 보니 공감이 되었다. 이 사실을 명심하는 것 또한 다음 스텝을 진행하는 데에 큰 힌트가 될 것이라 생각한다.
'카이스트 정글' 카테고리의 다른 글
pintos project3: virtual memory (0) | 2024.10.23 |
---|---|
pintos project2: user programs (0) | 2024.10.09 |
Proxy Lab 그리고 ECF (0) | 2024.09.25 |
야크셰이빙 중독 (0) | 2024.09.21 |
Malloc Lab 그리고 CS:APP (0) | 2024.09.13 |