← 운영체제 2 / 6
운영체제 · 4장 부록

XNU 커널과 GCD·Swift Concurrency Who actually makes the kernel thread?

4장 §8은 커널이 스레드를 어떤 구조체로 표현하는지를 Linux(task_struct)Windows(ETHREAD/KTHREAD/TEB)로 다뤘다. 이 부록은 그 세 번째 축, macOS·iOS의 XNU를 같은 깊이로 채운다. 그리고 “DispatchQueue.global().async {} 한 줄이 실제로 커널까지 내려가 스레드를 만드는(또는 재사용하는) 경로”를 코드 레벨로 추적한다.
확장 부록 · 4장 §8(OS 내부)의 연장 · Mach/BSD 이중 구조 + libdispatch + Clutch + Turnstile
🏷️ 이 부록의 위치

이 페이지는 전부 ⊕ 교재 외 확장입니다. 4장 본문 §1(스레드 해부)·§3(사용자/커널 스레드와 M:N 매핑 모델)·§5.4(GCD의 큐·QoS 4클래스)에서 이미 다룬 개념 비교·매핑 모델·GCD API 표면은 여기서 반복하지 않고, 그 아래 커널 레벨만 다룹니다. Swift의 actor·Sendable·구조적 동시성 문법은 Swift Concurrency 코스로 넘깁니다.

⊕ 교재 외 확장 현대 애플 런타임·커널 스케줄러·저수준 SPI

🚧 먼저 — 흔한 오해 하나 정정

“GCD가 앱 시작 시 코어 수만큼 커널 스레드(KT) 풀을 미리 다 만들어 둔다”는 설명은 부정확하다. 실제로는 사용자 공간의 libdispatch는 스레드를 직접 만들지 않는다 — 커널의 workqueue 서브시스템에 “일꾼 스레드가 필요하다”고 요청만 하고, 실제 생성·캐싱·재사용 결정은 커널이 시스템 부하를 보고 내린다. 아래 §8.4가 이 경로를 정확히 추적한다.

한 문장으로: 당신은 “작업(Task/block)”을 제출하고, libdispatch는 “스레드 달라”고 커널에 요청하며, 실제 커널 스레드를 만들고 재사용하는 결정권자는 XNU의 workqueue 매니저다. 개발자→libdispatch→커널의 세 층이 각각 무엇을 책임지는지가 이 부록의 뼈대다.

진행률
0 / 0 완료

8.3XNU의 이중 구조 — BSD proc + Mach task/thread ⊕ 확장

macOS·iOS의 커널 XNU(“X is Not Unix”)는 두 혈통의 합성이다: 최하층의 Mach 마이크로커널(태스크·스레드·가상 메모리·IPC 담당)과 그 위의 BSD 층(POSIX API·프로세스 개념·파일 시스템·시그널 담당). 그래서 하나의 “프로세스”·“스레드”가 두 개의 구조체로 이중 표현된다.

📐 용어 — 마이크로커널 · 하이브리드 커널 · Mach 포트(IPC) ⊕ 확장

마이크로커널(microkernel): 스케줄링·메모리·IPC 같은 최소한의 핵심만 커널(특권 모드)에 두고, 파일시스템·드라이버·네트워크 스택은 사용자 공간 서버로 빼는 설계. 반대인 모놀리식(monolithic)은 그 전부를 커널 안에 둔다(Linux). 마이크로커널은 모듈성·안정성(한 서버가 죽어도 커널은 산다)이 높지만, 기능마다 서버 간 IPC 왕복이 생겨 느리다.

왜 XNU는 “하이브리드”인가: 순수 Mach 마이크로커널은 BSD 기능을 별도 서버로 두면 IPC 오버헤드가 크다. 그래서 XNU는 성능을 위해 BSD(파일·시그널·POSIX)를 사용자 공간 서버가 아니라 커널 주소 공간 안에 함께 넣었다 — 마이크로커널의 구조 + 모놀리식의 성능을 절충한 하이브리드 커널. 위 §8.3의 proc(BSD)+task(Mach) 이중 표현이 바로 이 절충의 흔적이다.

Mach 포트(port)와 IPC: Mach의 모든 통신은 메시지 전달이고, 그 통로가 포트다 — 커널이 관리하는 단방향 메시지 큐 + 능력(capability) 기반 권한(그 포트로 보낼/받을 권리를 가진 task만 접근). 표의 “task의 포트 권한”이 이것이며, 프로세스 제어·예외(exception) 처리·드라이버 통신이 전부 포트 위에서 돈다. 4장 §9의 액터 모델(사유 상태 + 우편함 메시지)과 같은 정신이 커널 최하층에 박혀 있는 셈이다.

개념Mach 층 (스케줄링·메모리)BSD 층 (POSIX·시스템 콜)
프로세스task — 주소 공간(vm_map)·포트 권한·스레드 목록의 컨테이너. 스스로 실행되지 않는다(자원 보유자).proc — PID·자격증명·파일 디스크립터·시그널 상태
스레드thread(thread_t) — 실제 스케줄링 단위. 레지스터 상태·커널 스택·우선순위·run stateuthread — 시스템 콜 처리 중의 BSD측 스레드 상태(errno, 시그널 마스크 등)

핵심은 Mach task는 자원 컨테이너일 뿐, CPU에 올라가는 것은 언제나 Mach thread라는 점이다. 이는 4장 §1의 “프로세스는 자원, 스레드는 실행 흐름” 구분이 커널 구조에 그대로 박제된 형태다.

하나의 프로세스 = BSD proc + Mach task · 하나의 스레드 = Mach thread(+uthread) BSD proc PID · 자격증명(uid/gid) 파일 디스크립터 테이블 시그널 처리 상태 Mach task vm_map (주소 공간) IPC 포트 권한 스레드 목록 (실행 안 함) Mach thread — 진짜 스케줄링 단위(CPU에 올라감) thread_t (T1) 레지스터·PC·SP 커널 스택 sched/base priority thread_t (T2) 레지스터·PC·SP 커널 스택 sched/base priority uthread (T1) — errno·sigmask uthread (T2) task가 thread들을 소유
XNU는 프로세스를 BSD proc+Mach task로, 스레드를 Mach thread+BSD uthread로 이중 표현한다. Linux의 task_struct(§8.1)·Windows의 ETHREAD/KTHREAD/TEB(§8.2)와 나란히 놓이는 세 번째 모델이며, 모두 1:1(사용자 스레드 하나 = 커널 스레드 하나)이다.
💡 세 OS를 한 줄로

Linux: 프로세스도 스레드도 task_struct 하나, 공유 정도만 다름. Windows: 커널 KTHREAD + 사용자 TEB. XNU: 커널 Mach thread + 사용자 pthread. 표현은 달라도 결론은 같다 — 오늘날 데스크톱·모바일 OS는 전부 커널이 직접 스케줄하는 1:1 커널 스레드 위에 서 있다(§3.1의 “1:1이 이겼다”).

8.4pthread_workqueue — GCD는 커널에 스레드를 “요청”만 한다 ⊕ 확장

4장 §5.4는 GCD를 큐·QoS·main queue 같은 API 표면에서 다뤘다. 그 아래에서 실제 스레드는 누가 만드는가? 답의 핵심은: libdispatch(사용자 공간)는 pthread_create를 직접 부르지 않는다. 대신 커널의 workqueue 서브시스템에게 일꾼 스레드를 요청하고, 생성 여부는 커널이 결정한다.

※ 아래 등장하는 함수들은 공개 API가 아니라 SPI(System Programming Interface — 애플 내부/시스템 라이브러리 전용 비공개 인터페이스로, 앱에서 직접 호출하면 심사에서 거부된다)다. 여기서는 “libdispatch가 커널과 어떻게 대화하는가”를 보여주는 개념 예시로만 쓴다.

libdispatch가 커널에 하는 것 — “요청”뿐 (개념적 SPI, C)
/* (1) 부팅 시 딱 한 번: "워커 스레드의 진입점은 이 함수다" 라고 커널에 등록 */
pthread_workqueue_setdispatch_np(_dispatch_worker_thread);
/*     ↑ 실제 생성이 아니라 '콜백 등록'일 뿐 */

/* (2) 큐에 일이 쌓였고 처리할 스레드가 부족할 때: "N개 더 줘" 라고 요청 */
pthread_workqueue_addthreads_np(priority, /*numthreads=*/1);
/*     ↑ 이 호출은 syscall로 커널에 내려가고,
         실제 pthread(=Mach thread) 생성은 커널의 workqueue 매니저가 수행 */

과거 pthread는 개별 작업 항목을 커널에 넘기는 _np API(additem류)로 스케줄링까지 관여할 수 있었지만, 현대 XNU에서는 그 권한이 커널로 이동했다. libdispatch는 “스레드 몇 개 필요”만 말하고, 어떤 작업을 어느 스레드에 태울지·몇 개를 실제로 만들지는 커널이 시스템 전체 관점에서 정한다.

심화 왜 커널이 결정해야 하나

스레드 풀 크기를 사용자 공간에서 정하면 libdispatch는 자기 프로세스만 본다 — 다른 앱의 부하, 전체 CPU 압박, 메모리 압박, 배터리 상태를 모른 채 “몇 개 만들지”를 추측할 수밖에 없다. 커널이 정하면 실시간 시스템 부하를 보고 “지금은 스레드를 더 안 만든다 / 만든다”를 전역 최적으로 결정할 수 있다. GCD의 설계 철학 — “스레드 풀 관리를 개발자 손에서 떼어 OS에 최대한 가깝게 둔다” — 이 커널 workqueue로 귀결된 이유다.

8.4.1 DispatchQueue.global().async 한 줄의 전체 경로

user space kernel space ① 당신의 코드 DispatchQueue.global().async { … } ② libdispatch block을 큐에 enqueue (원자 연산 ~15 instr) 유휴 워커 없으면 → addthreads 요청 syscall ③ XNU workqueue 매니저 CPU/메모리 부하·동시성 한도 확인 유휴 KT 재사용 or 새 KT 생성 결정 ④ Mach thread (KT) _dispatch_worker_thread 진입점부터 실행 큐를 drain → block 실행 → 유휴로 복귀 워커가 큐에서 block을 꺼내 사용자 코드 실행
① 작업 제출 → ② libdispatch가 큐잉하고 부족하면 스레드를 요청 → ③ 커널 workqueue 매니저가 부하를 보고 재사용/생성 결정 → ④ Mach thread가 워커 진입점에서 큐를 비운다. 생성 권한은 오직 ③에 있다.

8.4.2 Overcommit — 코어 수를 넘겨 만들 때

일반(non-overcommit) 글로벌 큐에서 커널은 대략 “동시에 실행 가능한 워커 수 ≒ 활성 코어 수”를 상한으로 삼는다 — CPU를 실제로 쓰는 작업이라면 코어보다 많은 스레드는 문맥 교환 낭비이기 때문이다. 그런데 워커가 블로킹 시스템 콜(디스크 I/O, 락 대기)에 걸리면 그 스레드는 CPU를 안 쓰면서 자리를 차지한다. 이때 다른 작업이 굶지 않도록 커널은 overcommit 정책으로 코어 수를 넘겨 새 워커를 만든다.

🚧 이것이 “스레드 폭발(thread explosion)”의 메커니즘

수백 개의 블록을 전부 블로킹하는 작업으로 글로벌 큐에 쏟아부으면, 커널은 “막힌 워커」를 보충하려 overcommit으로 계속 새 KT를 만든다 → 수십~수백 개 KT가 뜨고 각자 스택(수백 KB~수 MB)을 잡아 메모리·스케줄링이 무너진다. 해법은 스레드가 아니라 동시성의 폭을 제한하는 것: 직렬 큐로 직렬화하거나, DispatchSemaphore/maxConcurrentOperationCount로 동시 실행 수를 묶거나, 애초에 블로킹을 Swift Concurrency의 await(스레드를 붙잡지 않고 suspend)로 바꾼다(§8.7).

8.4.3 왜 이렇게까지 — 큐잉 15명령 vs 스레드 생성 수백 명령

GCD로 작업 하나를 큐에 넣는 데는 원자적 큐 삽입 약 15개 명령이면 충분한 반면, 전통적 pthread_create는 커널 진입 + 스택 매핑 + 자료구조 할당으로 수백 개 명령이 든다. 그래서 GCD의 규칙은 “스레드를 만들지 말고 작업을 제출하라”이며, 만들어진 KT는 커널이 캐싱해 재사용한다 — 매번 새로 만드는 Thread{}(§8 본문의 NSThread 안티패턴)와 정반대다.

심화 DISPATCH_USE_KEVENT_WORKQUEUE

현대 macOS/iOS의 libdispatch는 kqueue/kevent(커널이 다수 소켓·타이머·파일 이벤트의 준비 상태를 한 번에 통지하는 메커니즘, §9.1의 netpoller와 같은 계열)와 workqueue를 하나로 통합했다. 이벤트가 준비되면 커널이 곧바로 적절한 워커를 깨우거나 만들어 그 이벤트 핸들러를 태운다 — 폴링 없이, 유휴 스레드를 붙잡지 않고. 즉 “스레드 풀”과 “이벤트 루프”가 커널 안에서 합쳐진 work-stealing·이벤트 구동 구조다.

8.5Clutch 스케줄러 — QoS를 실제 실행 순서로 바꾸는 곳 ⊕ 확장

커널이 워커 스레드를 만들었다면, 어느 스레드를 먼저, 그리고 어떤 코어에 올릴지는 스케줄러가 정한다. 현대 XNU는 Clutch 스케줄러를 쓰는데, 이는 애플의 비대칭 멀티코어(AMP)를 잘 다루려고 다듬어진 설계다(§8.5.1). 4장 §5.4에서 개발자가 지정한 QoS(.userInteractive.background)가 여기서 실제 우선순위로 번역된다.

Clutch의 발상: 개별 스레드가 아니라 스레드 그룹(같은 목적으로 협력하는 스레드들, 예컨대 한 앱의 한 QoS 대역)을 스케줄 단위로 본다. 그리고 각 그룹에 interactivity score를 매긴다.

Interactivity score의 직관 (의사 정의)
interactivity_score ∝  자발적 블로킹 시간(voluntary blocking)
                       ───────────────────────────────────────
                              CPU 사용 시간(CPU usage)

· 자주 자발적으로 블록(입력·네트워크 대기) → 점수 높음 → "상호작용형"
· CPU만 오래 태움(계산·디코딩)           → 점수 낮음 → "배치형"
스레드 그룹CPU 사용자발적 블로킹interactivity스케줄러 취급
UI/터치 반응10ms90ms높음우선 배치
이미지 디코딩·배치100ms~0ms낮음상대적으로 밀림

효과: 별도 설정 없이도 “터치에 반응하는 스레드”가 “백그라운드 계산 스레드”보다 자연스럽게 우선한다. 예컨대 이미지 라이브러리가 다운로드·디코딩(CPU 집약, 낮은 interactivity)을 도는 동안에도 메인 스레드의 UI 갱신(높은 interactivity)이 부드럽게 끼어드는 이유가 이 설계다 — QoS는 “의도”만 표현하고, 실제 반응성은 커널이 실측 행동으로 보정한다.

8.5.1 AMP — 왜 비대칭 코어가 Clutch를 부르나 ⊕ 확장

4장 §2.3의 멀티코어는 대칭(SMP)이었다 — 코어가 전부 같아, 스케줄러는 “어느 코어든” 올리면 됐다. 그러나 애플 실리콘·모바일 SoC는 비대칭 멀티코어(AMP, asymmetric multiprocessing)다: 성능은 높지만 전력을 많이 먹는 P코어(Performance)와, 느리지만 전력 효율이 좋은 E코어(Efficiency)가 섞여 있다(ARM big.LITTLE의 계보). 그래서 스케줄러에 새로운 질문이 생긴다 — “먼저 실행할까”뿐 아니라 “어느 종류의 코어에 올릴까?”

UI/터치 그룹 interactivity 높음 디코딩/배치 그룹 interactivity 낮음 P코어빠름·고전력 P코어빠름·고전력 E E E E E코어 — 느림·저전력 P코어로 (빠른 반응) E코어로 (전력 절약)
AMP에서 스케줄러의 새 임무: interactivity 높은 그룹은 P코어에 올려 반응성을, 낮은 배치 그룹은 E코어에 몰아 전력·발열을 아낀다. Clutch가 개별 스레드가 아니라 그룹 단위로 스코어를 매기는 이유가 이 배치 결정 때문이다.

이제 8.5의 interactivity score가 왜 중요한지가 분명해진다: 대칭 코어라면 “순서”만 정하면 되지만, 비대칭 코어에서는 “어느 코어”까지 정해야 하고, 그 판단 기준이 그룹의 상호작용성이다. 상호작용형(UI·입력)은 P코어로 올려 지연을 줄이고, 배치형(디코딩·동기화)은 E코어로 밀어 배터리를 아낀다. 반응성과 전력을 동시에 잡는 이 균형이 모바일에서 특히 중요하다.

📝 Clutch와 Edge

엄밀히는, 코어 선택까지 포함해 AMP를 본격적으로 다루는 부분을 애플은 Edge 스케줄러라 부르며 Clutch의 확장·후속으로 도입했다. 실무 관점에서는 “Clutch 계열의 그룹 기반·interactivity 기반 스케줄링이 AMP 코어 배치까지 책임진다”로 이해하면 충분하다.

8.6Turnstile — 우선순위 역전을 막는 다단계 전파 ⊕ 확장

QoS가 있으면 새 문제가 생긴다: 우선순위 역전(priority inversion). 높은 QoS 스레드가, 낮은 QoS 스레드가 쥐고 있는 락을 기다리는데, 그 낮은 스레드가 CPU를 못 받아 락을 못 놓으면 → 높은 스레드도 무한정 대기한다.

📐 시나리오

T_high(userInteractive)가 락 대기 → 락 보유자는 T_low(background) → 시스템에 T_mid가 많으면 T_low가 스케줄을 못 받음 → 락이 영원히 안 풀림 → 사실상 T_high가 T_mid보다 낮은 우선순위로 전락(역전).

XNU의 turnstile이 이를 우선순위 상속(priority inheritance)으로 푼다: 대기 중인 스레드의 우선순위를 락 보유자에게 임시로 전파(propagate)한다. T_low가 잠깐 T_high의 우선순위로 승격 → 빨리 실행해 락을 놓음 → T_high가 즉시 진행. 이를 위해 커널 스레드는 우선순위 큐를 유지한다.

XNU thread의 이중 우선순위 (개념, thread 구조체)
struct thread {
    int  base_pri;   /* Base priority  — 당신이 지정한 QoS가 번역된 값 */
    int  sched_pri;  /* Scheduled priority — turnstile 부스트 등이 반영된 '실제' 실행 우선순위 */
    ...
    priority_queue  base_inheritor_queue;   /* 유저측 상속(예: 우선순위 지정 SPI) */
    priority_queue  sched_inheritor_queue;  /* 커널측 상속(turnstile 부스트) */
};
/* 두 값이 다를 수 있다:  base_pri = utility(낮음),  sched_pri = userInteractive(임시 부스트) */

8.6.1 Multi-hop — GCD가 특히 필요로 하는 것

GCD에서는 대기 사슬이 한 단계로 끝나지 않는다. 예를 들어 DispatchQueue.main.sync { … }로 메인 스레드가 어떤 큐를 기다리는데, 그 큐의 블록이 실행되려면 새 워커 스레드 생성이 끝나야 하고, 그 생성은 또 다른 스레드가 담당한다. Turnstile은 이 사슬을 따라 우선순위를 여러 단계로 연쇄 전파한다.

T_high userInteractive (대기 중) sync 블록 실행 대기 (워커 필요) Queue 매니저 스레드 생성 요청 처리 Creator KT 새 워커를 만드는 중 우선순위 부스트가 사슬을 따라 전파 (multi-hop) → "새 스레드를 만드는 작업"조차 T_high의 우선순위를 상속받아 빨리 끝난다 → 사슬 어느 지점도 T_mid에 밀리지 않음 = 역전 방지
Turnstile은 대기 사슬(T_high → sync 블록 → 큐 매니저 → 생성 담당 스레드)을 따라 우선순위를 다단계로 상속시킨다. 그래서 “워커를 만드는 일”까지 부스트를 받아, 복잡한 GCD 대기에서도 역전이 생기지 않는다.
💡 실전 함의 — 왜 block.wait()보다 락 공유가 나은가

dispatch_block_wait()는 QoS override 위에 구현돼 multi-hop이 아니다(1단계 전파만). 앱에서는 메인 스레드가 최고 우선순위라 대개 1-hop이면 충분하지만, 더 깊은 사슬에서 확실한 우선순위 전파를 원하면 락(뮤텍스) 기반 대기가 낫다 — 커널 turnstile이 락 소유권 사슬을 따라 multi-hop 부스트를 제대로 걸어주기 때문이다. “가능하면 sync+락, 저수준 wait 남용은 피하라”는 조언의 커널 근거가 이것이다.

8.7Swift Concurrency의 Task ↔ 커널 스레드 — 근본적 비대칭 ⊕ 확장

지금까지가 커널 스레드(무거운 것)의 이야기였다. Swift Concurrency의 Task는 그 위에 얹힌 가벼운 것이며, 둘의 관계는 1:1이 아니다. 이것이 4장 §9(goroutine·virtual thread·async/await)에서 본 M:N 부활의 애플 판본이다.

Mach thread (KT)Swift Task
크기스택 수백 KB~수 MB힙에 async frame 수백 바이트~
생성 주체커널 workqueue(§8.4)Swift 런타임(할당만)
블로킹 시그 KT가 통째로 멈춤await에서 suspend → KT를 반납
개수 한도수천 개면 이미 위험(§8.4.2)수십만 개도 가능

핵심은 await의 의미다. Task가 await를 만나 대기 상태가 되면, 그 Task는 실행 중이던 커널 스레드를 붙잡은 채 잠들지 않는다 — 상태를 힙의 async frame에 저장하고 스레드를 양보한다. 그 KT는 즉시 다른 Task를 실행한다. 대기가 풀리면 Task는 아무 유휴 KT에서나 재개된다(재개된 스레드가 이전과 다를 수 있다).

협력적 스레드 풀 — 폭(width) ≒ 코어 수. Task는 이 좁은 KT 집합을 공유·양보한다. KT #1 (core 0) KT #2 (core 1) Task A Task B Task C … ×10만 await frame을 힙에 저장 KT 반납(suspend) resume 유휴 KT에서 재개 (다른 KT일 수 있음)
Swift Concurrency의 협력적 스레드 풀은 폭이 코어 수로 제한된다(GCD의 overcommit과 대비 — 스레드 폭발을 구조적으로 막는다). Task는 await마다 KT를 양보하므로, 좁은 KT 집합으로 수십만 Task를 태울 수 있다.
🚧 그래서 협력적 스레드 풀에서 블로킹은 금물

Swift Concurrency의 KT 풀은 폭이 코어 수로 고정돼 있다(GCD처럼 overcommit으로 늘리지 않는다). 그러므로 Task 안에서 Thread.sleep·동기 파일 I/O·세마포어 대기 같은 실제 블로킹을 하면 await가 아니라 KT 자체를 붙잡아 풀을 고갈시키고, 최악의 경우 전체가 멈춘다(deadlock 유발). 블로킹 작업은 await 가능한 async API로 바꾸거나, 정 필요하면 별도 전용 스레드/큐로 격리해야 한다.

이 부록은 스레드와 Task의 관계까지만 다룬다. actor가 어떻게 데이터 레이스(4장 §6)를 컴파일 타임에 막는지, Sendable·구조적 동시성·MainActor의 실전 적용은 별도 코스에서 이어진다.

8.83-way 비교 — Linux · Windows · macOS/iOS로 §8을 닫는다 ⊕ 확장

LinuxWindowsmacOS · iOS (XNU)
커널 스레드 구조체task_struct(프로세스=스레드 통합)KTHREAD + ETHREADMach thread (+ BSD uthread)
프로세스 컨테이너task_struct + 공유 mm/filesEPROCESSBSD proc + Mach task
사용자↔커널 매핑1:1 (NPTL)1:11:1 (pthread ↔ Mach thread)
생성 시스템 콜clone()NtCreateThreadbsdthread_create / workqueue
암시적 스레딩 런타임—(libⁿ 다양)Thread Pool APIGCD(libdispatch) + 커널 workqueue
대표 스케줄러CFS → EEVDF우선순위 기반 + 부스트Clutch(interactivity score)
우선순위 상속PI-futex / rt_mutex우선순위 부스트Turnstile(multi-hop)
경량 동시성(M:N)—(goroutine은 Go 런타임)Swift Task(협력적 스레드 풀)
📝 §8 전체를 한 문장으로

세 OS 모두 커널이 직접 스케줄하는 1:1 커널 스레드를 바닥에 깔고(§3.1의 결론), 그 위에 “작업 기반 병렬성”(§5)을 얹는다 — Windows는 Thread Pool, 애플은 GCD, 그리고 그보다 위에 goroutine/virtual thread/Swift Task 같은 M:N 경량 동시성(§9)이 “값싼 10만 동시성”을 준다. XNU가 다른 점은 이 세 층(1:1 커널 스레드 · GCD workqueue · Swift Task)이 커널 workqueue·Clutch·Turnstile로 촘촘히 통합돼 있다는 것이다.

심화 더 파고들 지점

이 부록에서 멈춘 곳들: Edge 스케줄러의 P-코어/E-코어 간 스레드 이동(migration) 세부 · kevent 워크큐의 자료구조 · os_signpost/Instruments의 System Trace로 turnstile 부스트와 스레드 생성 관찰하기 · libdispatch 오픈소스(src/queue.c·src/event/)의 실제 큐잉 경로. 참고: Apple libdispatch/XNU 오픈소스, WWDC “Modernizing Grand Central Dispatch Usage”·“Swift Concurrency: Behind the Scenes”, Levin *OS Internals.

8.9 용어 사전 ⊕ 확장

이 부록의 XNU·애플 특화 용어를 한자리에 모았다(스레드 일반 용어는 본문 §10.5 용어 사전 참조). 오른쪽 링크로 설명으로 이동한다.

① 커널 구조 (Mach · BSD)

용어
XNUmacOS·iOS 커널. Mach 마이크로커널 + BSD 층의 하이브리드.8.3
마이크로커널 / 모놀리식 / 하이브리드핵심만 커널에(나머지 사용자 서버) / 전부 커널에 / 둘의 절충. XNU는 하이브리드.8.3
Mach task / thread자원 컨테이너(주소 공간·포트) / 실제 스케줄링 단위(thread_t). CPU엔 thread만 올라감.8.3
BSD proc / uthreadPID·자격증명·fd 테이블 / 시스템 콜 처리 중의 BSD측 스레드 상태(errno 등).8.3
Mach 포트(port) / IPC커널이 관리하는 단방향 메시지 큐 + 능력(capability) 기반 권한. Mach 통신의 통로.8.3
SPISystem Programming Interface — 애플 내부/시스템 전용 비공개 인터페이스(공개 API의 반대).8.4

② 스레드 생성 (GCD ↔ 커널)

용어
libdispatchGCD의 사용자 공간 구현. 스레드를 직접 안 만들고 커널 workqueue에 요청만 한다.8.4
(커널) workqueue실제 워커 스레드의 생성·캐싱·재사용을 시스템 부하 기반으로 결정하는 XNU 서브시스템.8.4
overcommit워커가 블로킹으로 자리만 차지할 때, 코어 수를 넘겨 새 워커를 만드는 정책 → 스레드 폭발의 원인.8.4.2
kqueue / kevent다수 소켓·타이머·파일 이벤트의 준비 상태를 한 번에 통지하는 커널 메커니즘. workqueue와 통합됨.8.4.3

③ 스케줄링 (Clutch · Turnstile)

용어
Clutch 스케줄러개별 스레드가 아니라 스레드 그룹을 단위로, interactivity score로 우선순위를 정하는 XNU 스케줄러.8.5
interactivity score자발적 블로킹 시간 / CPU 사용 시간. 높으면 상호작용형(우선), 낮으면 배치형.8.5
AMP / P코어·E코어비대칭 멀티코어. 고성능·고전력 P코어와 저성능·저전력 E코어 혼합(big.LITTLE 계보).8.5.1
Edge 스케줄러AMP의 코어 선택·이동까지 다루는 Clutch의 확장/후속.8.5.1
우선순위 역전 / 상속(Turnstile)저우선순위가 락을 쥐어 고우선순위를 막음 / 대기자의 우선순위를 보유자에게 전파해 해소.8.6
base_pri / sched_priQoS가 번역된 기본 우선순위 / turnstile 부스트가 반영된 ‘실제’ 실행 우선순위.8.6
multi-hop 전파대기 사슬(대기자→sync 블록→큐 매니저→생성 스레드)을 따라 우선순위를 여러 단계로 상속.8.6.1

④ Swift Concurrency

용어
Swift Task힙의 async frame(수백 바이트~)에 사는 경량 실행 단위. 커널 스레드와 1:1이 아님.8.7
await / suspendTask가 대기 시 상태를 힙에 저장하고 커널 스레드를 반납 — 붙잡은 채 잠들지 않음.8.7
협력적 스레드 풀폭이 코어 수로 고정된 KT 풀(overcommit 안 함) → 블로킹하면 풀 고갈, 그래서 블로킹 금물.8.7