7주차는 교육용 운영체제 핀토스(Pintos)의 첫 관문인 Project 1: Threads에 돌입한 주차였습니다. 부팅 직후 단일 스레드에서 멀티스레드 선점형(Preemptive) 커널로 확장되는 부팅 시퀀스(init.c), 스레드의 4가지 상태 전이(RUNNING, READY, BLOCKED, DYING), 타이머 인터럽트 기반 Alarm Clock, 세마포어와 락의 소유권, 그리고 다중/중첩 Priority Donation 메커니즘을 설계하고 C 언어로 구현했습니다.
[핀토스 스레드 상태 전이 및 스케줄러 흐름도]
thread_create()
│
▼
[THREAD_BLOCKED] ──► thread_unblock() ──► [THREAD_READY] ◄───────────────┐
▲ │ │
│ ▼ schedule() │
thread_block() (락/세마포어/알람 대기) [THREAD_RUNNING] │ thread_yield()
│ (단 1개만 CPU 점유) │ (타임슬라이스 만료)
└────────────────────────────────────────┴────────────────────────┘
│ thread_exit()
▼
[THREAD_DYING] (메모리 회수)
"팀원들과 생활루틴을 맞추려다보니 몇일을 달리고 그 후 2일을 망가지고 다시 시작한 한주, 뇌가 멈춘 느낌이 들었습니다."
Pintos라는 수만 줄의 거대한 운영체제 소스코드 앞에 서자 압도적인 부담감이 엄습했습니다. 팀원들과 보폭을 맞추어야 한다는 강박에, 새벽까지 코딩하는 팀원들의 야행성 생활 루틴에 제 바이오리듬을 억지로 뜯어맞추려 했습니다.
며칠 동안 3~4시간만 자며 극한으로 코드를 붙잡고 질주했으나, 그 대가는 참혹했습니다. 신체 면역과 신경계가 버티지 못하고 무너지면서, 그 후 장장 이틀 동안 침대에서 일어나지 못하고 완전히 몸과 마음이 망가져 버렸습니다. 화면 앞에 앉아도 모니터의 활자가 전혀 읽히지 않고 뇌가 완전히 멈춰버린 듯한 '극심한 번아웃(Brain Freeze)'을 겪었습니다. 무리한 과욕과 페이스 조절의 실패가 학습의 연속성을 얼마나 치명적으로 파괴하는지 뼈저리게 통감한 한 주였습니다.
핀토스에서 스레드가 생성되면 커널 페이지 할당자로부터 정확히 4KB(4096 바이트)의 메모리 페이지 1장을 할당받습니다.
높은 주소 (0x1000 = 4096 바이트 경계)
+------------------------------------+ ◄── 초기 커널 스택 포인터 (t->tf.rsp)
| 커널 스택 |
| │ | 함수 호출 시 스택 프레임이 아래 방향으로 성장
| ▼ | (Grows Downward)
| |
| 스택 가용 영역 |
| |
+------------------------------------+ ◄── sizeof(struct thread) 경계선
| magic | [중요] 경계 최상단: THREAD_MAGIC (0xcd6ab344)
| intr_frame tf | 문맥 교환용 하드웨어 레지스터 세트
| priority | 현재 우선순위 (0 ~ 63)
| elem | ready_list / sleep_list 연결 노드
| status | 스레드 상태 (RUNNING, READY, BLOCKED, DYING)
+------------------------------------+
낮은 주소 (0x0: struct thread *t의 시작점, 4KB 정렬)
THREAD_MAGIC: 커널 스택이 아래로 자라나 struct thread 경계선을 침범하면 magic 필드가 가장 먼저 깨집니다. 핀토스는 스케줄러 전환 시마다 ASSERT(is_thread(t))를 통해 매직 넘버가 훼손되었는지를 검사하여 스택 오버플로우를 감지합니다.thread_current()의 비트 마스킹 트릭: 4KB 페이지의 시작 주소는 항상 하위 12비트가 0x000으로 정렬되어 있으므로, 현재 CPU의 스택 포인터(%rsp)에서 하위 12비트를 내림(pg_round_down(rsp))하는 단 한 번의 비트 연산으로 현재 실행 중인 struct thread 포인터를 에 찾아냅니다.원본 핀토스의 timer_sleep()은 thread_yield()를 반복 호출하며 CPU를 낭비합니다. 이를 개선하기 위해 스레드를 블록 상태로 전환하고 깨어날 틱(Tick) 순서대로 정렬된 sleep_list에 삽입한 뒤, 타이머 인터럽트 핸들러가 기상 시각이 도래한 스레드들만 깨워 ready_list로 복귀시키도록 수정했습니다.
이 주차의 번아웃을 통해 팀워크에 대한 제 오랜 편견이 깨졌습니다. "협업을 잘한다는 것은 팀원과 똑같은 시간에 자고 똑같은 시간에 일어나는 것이 아니다."
진정한 프로페셔널 팀워크는 각자가 최상의 맑은 정신으로 집중할 수 있는 나만의 신체 루틴을 철저히 지키면서, 사전에 합의된 '핵심 인터페이스 시간(오전 데일리 스크럼, 코드 리뷰, 통합 빌드)'에 고밀도로 동기화하는 것입니다. 남의 페이스에 휩쓸려 무리하다가 이틀 동안 뇌가 멈춰버리는 것은 나 자신뿐 아니라 팀 전체의 개발 속도를 갉아먹는 무책임한 행동이었습니다. 5개월이라는 장기 레이스를 완주하기 위해서는 '지속 가능한 나만의 페이스(Sustainable Pace)'를 타협 없이 지켜내야 합니다.
답변: 핀토스에서 스레드 관리 구조체(struct thread)와 커널 스택은 단 하나의 4KB 물리 메모리 페이지를 공유합니다. 커널 함수 내에서 대형 지역 변수를 선언하면 스택 프레임의 크기가 급증하여 커널 스택 포인터가 낮은 주소로 급격히 내려오게 되며, 결국 4KB 경계선 하단에 위치한 struct thread의 메타데이터와 magic 필드를 직접 덮어써서 커널 메모리가 파괴되는 커널 스택 오버플로우가 발생하기 때문입니다.
답변: 인터럽트 핸들러는 프로세스나 스레드의 문맥이 아닌 하드웨어 CPU의 인터럽트 컨텍스트(Interrupt Context)에서 실행됩니다. 인터럽트 컨텍스트는 양보(Yield)하거나 블록(Sleep/Wait)될 수 없으며, 만약 인터럽트 핸들러가 잠들게 되면 CPU 제어권을 다른 스레드로 안전하게 넘겨줄 스케줄링 주체가 존재하지 않아 시스템 전체가 정지하는 커널 패닉에 봉착하기 때문입니다.
답변: 스레드 L이 락 A와 락 B를 동시에 쥐고 있고, 락 A에는 우선순위 50인 스레드가, 락 B에는 우선순위 60인 스레드가 대기 중인 다중 기부 상황을 가정할 수 있습니다. 이때 스레드 L이 락 B를 해제했다고 해서 우선순위를 자신의 본래 우선순위(예: 31)로 낮추어 버리면, 여전히 락 A를 기다리고 있는 우선순위 50인 스레드가 우선순위 역전 문제에 노출됩니다. 따라서 락을 해제할 때는 여전히 소유 중인 다른 락들의 대기자들 중 가장 높은 우선순위(50)를 재계산하여 유지해야 합니다.
