[크래프톤정글/WIL] 7주차 회고: Pintos Project 1 스레드와 동기화, 번아웃과 뇌정지, 페이스 메이커의 교훈

코딩이 어려운 사람·2026년 10월 22일

WIL

목록 보기
7/8

소주제

  1. 7주차 학습 개요와 기술 스택: Pintos Project 1 스레드 스케줄링과 커널 동기화 아키텍처
  2. 내가 직면했던 현실과 한계: 팀 루틴 동기화의 실패, 며칠의 질주와 이틀간의 번아웃
  3. 핵심 기술 개념과 코드 분석: 4KB 단일 페이지 내 struct thread와 커널 스택의 공존 구조
  4. Alarm Clock 비차단 수면 전환(sleep_list)과 우선순위 역전(Priority Donation) 해결
  5. 협업과 소통의 성찰: 루틴의 물리적 일치가 아닌 결과물의 동기화와 나만의 지속 가능한 페이스
  6. 실행 가능한 액션 플랜: 기상/취침 루틴 고정과 비동기 싱크업 회의 설계
  7. 7주차 핵심 엔지니어링 Q&A 3선

1. 7주차 학습 개요와 기술 스택: Pintos Project 1 스레드 스케줄링과 커널 동기화 아키텍처

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. 내가 직면했던 현실과 한계: 팀 루틴 동기화의 실패, 며칠의 질주와 이틀간의 번아웃

"팀원들과 생활루틴을 맞추려다보니 몇일을 달리고 그 후 2일을 망가지고 다시 시작한 한주, 뇌가 멈춘 느낌이 들었습니다."

Pintos라는 수만 줄의 거대한 운영체제 소스코드 앞에 서자 압도적인 부담감이 엄습했습니다. 팀원들과 보폭을 맞추어야 한다는 강박에, 새벽까지 코딩하는 팀원들의 야행성 생활 루틴에 제 바이오리듬을 억지로 뜯어맞추려 했습니다.

며칠 동안 3~4시간만 자며 극한으로 코드를 붙잡고 질주했으나, 그 대가는 참혹했습니다. 신체 면역과 신경계가 버티지 못하고 무너지면서, 그 후 장장 이틀 동안 침대에서 일어나지 못하고 완전히 몸과 마음이 망가져 버렸습니다. 화면 앞에 앉아도 모니터의 활자가 전혀 읽히지 않고 뇌가 완전히 멈춰버린 듯한 '극심한 번아웃(Brain Freeze)'을 겪었습니다. 무리한 과욕과 페이스 조절의 실패가 학습의 연속성을 얼마나 치명적으로 파괴하는지 뼈저리게 통감한 한 주였습니다.


3. 핵심 기술 개념과 코드 분석: 4KB 단일 페이지 내 struct thread와 커널 스택의 공존 구조

핀토스에서 스레드가 생성되면 커널 페이지 할당자로부터 정확히 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 포인터를 O(1)O(1)에 찾아냅니다.

4. Alarm Clock 비차단 수면 전환(sleep_list)과 우선순위 역전(Priority Donation) 해결

4.1 Alarm Clock: Busy Waiting 제거

원본 핀토스의 timer_sleep()은 thread_yield()를 반복 호출하며 CPU를 낭비합니다. 이를 개선하기 위해 스레드를 블록 상태로 전환하고 깨어날 틱(Tick) 순서대로 정렬된 sleep_list에 삽입한 뒤, 타이머 인터럽트 핸들러가 기상 시각이 도래한 스레드들만 깨워 ready_list로 복귀시키도록 수정했습니다.

4.2 Priority Donation: 우선순위 역전(Priority Inversion) 방어

  • 문제: 우선순위 높은 스레드 H가 락을 기다리고 있으나, 락을 쥔 낮은 우선순위 스레드 L이 중간 우선순위 스레드 M에 밀려 CPU를 잡지 못해 전체 시스템이 지연되는 현상.
  • 해결: 스레드 H가 락을 대기할 때, 락을 쥐고 있는 스레드 L에게 자신의 높은 우선순위를 임시로 기부(Donation)하고, 락이 해제되면 원래 우선순위로 복원합니다. 여러 락이 얽힌 다중 기부(Multiple Donation)와 체인 형태의 중첩 기부(Nested Donation)까지 재귀적으로 추적하여 해결했습니다.

5. 협업과 소통의 성찰: 루틴의 물리적 일치가 아닌 결과물의 동기화와 나만의 지속 가능한 페이스

이 주차의 번아웃을 통해 팀워크에 대한 제 오랜 편견이 깨졌습니다. "협업을 잘한다는 것은 팀원과 똑같은 시간에 자고 똑같은 시간에 일어나는 것이 아니다."

진정한 프로페셔널 팀워크는 각자가 최상의 맑은 정신으로 집중할 수 있는 나만의 신체 루틴을 철저히 지키면서, 사전에 합의된 '핵심 인터페이스 시간(오전 데일리 스크럼, 코드 리뷰, 통합 빌드)'에 고밀도로 동기화하는 것입니다. 남의 페이스에 휩쓸려 무리하다가 이틀 동안 뇌가 멈춰버리는 것은 나 자신뿐 아니라 팀 전체의 개발 속도를 갉아먹는 무책임한 행동이었습니다. 5개월이라는 장기 레이스를 완주하기 위해서는 '지속 가능한 나만의 페이스(Sustainable Pace)'를 타협 없이 지켜내야 합니다.


6. 실행 가능한 액션 플랜: 기상/취침 루틴 고정과 비동기 싱크업 회의 설계

  1. 절대 수면 시간 엄수: 매일 밤 1시 취침, 아침 7시 기상의 6시간 수면 패턴을 어떤 상황에서도 절대 타협하지 않고 사수하기.
  2. 비동기 싱크업 도입: 팀원들의 작업 시간대가 다를 경우 GitHub Projects와 Slack에 오늘의 작업 내용과 PR 리뷰 요청을 명확한 텍스트로 남겨 비동기적으로 협업하기.
  3. 뇌정지 신호 감지 시 즉시 휴식: 코드가 눈에 들어오지 않고 집중력이 30% 이하로 떨어지면 억지로 모니터 앞에 앉아 있지 말고 즉시 20분의 산책이나 파워냅(Power Nap)을 취하기.

7. 7주차 핵심 엔지니어링 Q&A 3선

Q1. 커널 함수 내부에서 커다란 지역 배열(예: char buf[4096])을 선언하면 안 되는 하드웨어적 이유는?

답변: 핀토스에서 스레드 관리 구조체(struct thread)와 커널 스택은 단 하나의 4KB 물리 메모리 페이지를 공유합니다. 커널 함수 내에서 대형 지역 변수를 선언하면 스택 프레임의 크기가 급증하여 커널 스택 포인터가 낮은 주소로 급격히 내려오게 되며, 결국 4KB 경계선 하단에 위치한 struct thread의 메타데이터와 magic 필드를 직접 덮어써서 커널 메모리가 파괴되는 커널 스택 오버플로우가 발생하기 때문입니다.

Q2. 타이머 인터럽트 핸들러(timer_interrupt) 내부에서 thread_sleep()이나 lock_acquire()를 호출하면 안 되는 이유는?

답변: 인터럽트 핸들러는 프로세스나 스레드의 문맥이 아닌 하드웨어 CPU의 인터럽트 컨텍스트(Interrupt Context)에서 실행됩니다. 인터럽트 컨텍스트는 양보(Yield)하거나 블록(Sleep/Wait)될 수 없으며, 만약 인터럽트 핸들러가 잠들게 되면 CPU 제어권을 다른 스레드로 안전하게 넘겨줄 스케줄링 주체가 존재하지 않아 시스템 전체가 정지하는 커널 패닉에 봉착하기 때문입니다.

Q3. 다중 우선순위 기부(Multiple Donation) 상황에서 스레드가 락을 해제할 때 우선순위를 즉시 원래 값으로 복원하면 안 되는 이유는?

답변: 스레드 L이 락 A와 락 B를 동시에 쥐고 있고, 락 A에는 우선순위 50인 스레드가, 락 B에는 우선순위 60인 스레드가 대기 중인 다중 기부 상황을 가정할 수 있습니다. 이때 스레드 L이 락 B를 해제했다고 해서 우선순위를 자신의 본래 우선순위(예: 31)로 낮추어 버리면, 여전히 락 A를 기다리고 있는 우선순위 50인 스레드가 우선순위 역전 문제에 노출됩니다. 따라서 락을 해제할 때는 여전히 소유 중인 다른 락들의 대기자들 중 가장 높은 우선순위(50)를 재계산하여 유지해야 합니다.


크래프톤 정글 7주차 Pintos 4KB 스레드 페이지 구조 및 Sleep List 타이머 인터럽트 아키텍처 다이어그램

profile
그사세가 아닌 내세상으로 만들기

0개의 댓글