> [!abstract] Introduction
> 컨텍스트 스위치*context switch*는 운영체제가 멀티태스킹을 실현하는 핵심 원리입니다. CPU의 제어권을 현재 실행 중인 태스크에서 다른 태스크로 넘기는 이 과정은, 소프트웨어(커널)와 하드웨어(CPU 아키텍처)가 긴밀하게 협력해야 하는 대표적인 영역입니다. 이 글에서는 컨텍스트 스위치가 무엇이며 언제, 왜 발생하는지부터 리눅스 커널에서의 실제 구현, 그리고 x86-64와 AArch64 아키텍처에서의 하드웨어 동작까지를 종합적으로 살펴봅니다.
## 컨텍스트란 무엇인가
컨텍스트*context*란 CPU가 특정 태스크의 실행을 재개하기 위해 필요한 모든 상태 정보의 총합이다. 이 정보는 [Process Control Block](Process%20Control%20Block.md)(이하 PCB)에 보관되며, 다음과 같은 요소들로 구성된다.
| 범주 | 내용 |
|------|------|
| **범용 레지스터** | x86-64: `RAX`~`R15` (16개), AArch64: `X0`~`X30` (31개) |
| **프로그램 상태** | 플래그 레지스터(`RFLAGS`), 조건 코드 |
| **명령어 포인터** | `RIP` (x86-64), `PC` / `ELR_EL1` (AArch64) |
| **부동소수점/SIMD**[^simd] | FPU, `SSE`/`AVX`/`AVX-512` (x86-64), `NEON`/`SVE`[^sve]/`SME` (AArch64) |
| **메모리 관리** | 페이지 테이블 베이스 — `CR3` (x86-64), `TTBR0_EL1` (AArch64) |
| **커널 스택** | 태스크별 커널 스택 포인터 |
| **스케줄링 메타데이터** | 상태(`RUNNING`, `READY`, `BLOCKED`), 우선순위, 소비한 CPU 시간 |
컨텍스트의 크기는 아키텍처와 활용하는 확장 기능에 따라 달라진다. x86-64에서 `AVX-512`를 사용하면 `XSAVE`[^xsave] 영역만 약 2.7KB에 달하고, AArch64에서 `SVE`를 최대 벡터 길이로 사용하면 레지스터 파일만 약 8KB까지 커진다.
## 컨텍스트 스위치는 언제 발생하는가
컨텍스트 스위치를 유발하는 이벤트는 크게 비자발적*involuntary*과 자발적*voluntary*으로 나뉜다.
**비자발적 전환**은 태스크의 의사와 무관하게 커널이 강제로 CPU를 회수하는 경우다. 타이머 인터럽트에 의한 선점*preemption*[^preemption]이 대표적이다. [스케줄러](Scheduler.md)가 설정한 타임 퀀텀[^time-quantum]이 만료되면, 타이머 인터럽트 핸들러가 `TIF_NEED_RESCHED` 플래그를 설정하고, 커널이 사용자 공간으로 복귀하기 직전 또는 선점 가능 지점에서 스케줄링이 발생한다. 더 높은 우선순위의 태스크가 깨어나는 경우에도 현재 태스크가 밀려난다.
**자발적 전환**은 태스크가 스스로 CPU를 양보하는 경우다. `read()`, `write()` 같은 블로킹 시스템 콜을 호출하면 태스크는 I/O 완료를 기다리며 대기 상태로 전환된다. 뮤텍스나 세마포어 같은 동기화 프리미티브에서 블로킹되는 경우, `sched_yield()`를 명시적으로 호출하는 경우도 여기에 해당한다.
```mermaid
flowchart LR
A[실행 중인 태스크] -->|"타이머 인터럽트<br/>높은 우선순위 태스크 도착"| B[비자발적 전환]
A -->|"블로킹 I/O<br/>동기화 대기<br/>sched_yield"| C[자발적 전환]
B --> D["schedule() 호출"]
C --> D
D --> E[컨텍스트 스위치 실행]
```
## 컨텍스트 스위치의 6단계
컨텍스트 스위치의 전체 과정은 커널 진입부터 사용자 공간 복귀까지 6개 단계로 구분할 수 있다.
### 1단계: 커널 진입
이벤트(인터럽트, 시스템 콜, 예외)가 발생하면 CPU는 하드웨어 수준에서 커널 모드로 전환한다. 이 과정에서 아키텍처마다 하드웨어가 자동으로 저장하는 상태의 범위가 다르다.
x86-64의 경우, 인터럽트 경로에서는 `TSS`(Task State Segment)로부터 커널 스택 포인터(`RSP0`)를 로드한 뒤, `SS`, `RSP`, `RFLAGS`, `CS`, `RIP` 다섯 개 값을 커널 스택에 푸시한다. `SYSCALL` 명령어를 통한 빠른 경로에서는 `RIP`를 `RCX`에, `RFLAGS`를 `R11`에 저장하고, `LSTAR` MSR[^msr]에서 커널 진입점을 로드한다. 이 경로에서는 스택 전환이 자동으로 이루어지지 않으므로, 커널 코드가 per-CPU[^per-cpu] 자료구조에서 직접 커널 스택을 가져와야 한다.
AArch64는 하드웨어가 저장하는 것이 `SPSR_EL1`(프로그램 상태)과 `ELR_EL1`(복귀 주소) 단 두 개뿐이다. 나머지 모든 레지스터는 커널의 진입 스텁이 `STP`(Store Pair) 명령어로 직접 저장해야 한다.
### 2단계: 현재 태스크 상태 저장
커널 진입 코드가 나머지 범용 레지스터를 커널 스택의 `pt_regs`[^pt-regs] 구조체에 저장한다. 부동소수점/SIMD 상태는 `switch_fpu()` 함수를 통해 태스크의 스레드 정보 구조체에 저장된다. 리눅스는 여기서 `TIF_NEED_FPU_LOAD` 플래그를 활용한 **지연 복원*lazy restore*** 전략을 사용한다 — FPU 상태는 사용자 공간으로 돌아갈 때 `switch_fpu_return()`에서 실제로 필요한 시점에만 복원한다.
이어서 나가는 태스크의 PCB가 갱신된다. 상태가 `RUNNING`에서 `READY`(또는 `BLOCKED`)로 변경되고, 스케줄링 통계가 기록되며, 커널 스택 포인터가 PCB에 저장된다.
### 3단계: 다음 태스크 선택
[스케줄러](Scheduler.md)가 호출된다. 리눅스 v6.6 이후에는 [Earliest Eligible Virtual Deadline First](Earliest%20Eligible%20Virtual%20Deadline%20First.md)(Earliest Eligible Virtual Deadline First)가 기본 스케줄링 알고리즘으로, 가상 데드라인이 가장 이른 적격 태스크를 선택한다. 선택된 태스크가 현재 태스크와 동일하면 전환 없이 바로 복귀한다.
### 4단계: 주소 공간 전환
들어오는 태스크가 다른 프로세스에 속하면 — 즉 `mm_struct`[^mm-struct]가 다르면 — 주소 공간을 전환해야 한다. 이 단계가 프로세스 간 컨텍스트 스위치에서 가장 비용이 큰 부분이다.
x86-64에서는 새 프로세스의 `PML4`/`PML5`[^pml] 물리 주소를 `CR3` 레지스터에 기록한다. `PCID`(Process Context Identifier) 없이는 이 쓰기 자체가 TLB[^tlb] 전체를 플러시한다. `PCID`를 사용하면 `CR3`의 하위 12비트에 태그를 포함시키고 bit 63을 설정하여 "no-flush" 로드를 수행할 수 있어, 다른 프로세스의 TLB 항목이 보존된다.
AArch64는 설계적으로 유리하다. 사용자 공간 페이지 테이블(`TTBR0_EL1`)과 커널 페이지 테이블(`TTBR1_EL1`)이 분리되어 있어, 프로세스 전환 시 `TTBR0`만 변경하면 된다. `ASID`(Address Space Identifier)가 TLB 항목에 태깅되므로, `ASID`가 다른 프로세스 간 전환에서는 TLB 플러시가 필요 없다. 16비트 `ASID`(ARMv8.2+)는 최대 65,536개의 주소 공간을 동시에 태깅할 수 있어 롤오버가 거의 발생하지 않는다.
같은 프로세스 내의 [스레드](Thread.md) 간 전환이라면 `mm_struct`가 동일하므로 이 단계가 완전히 생략된다. 이것이 스레드 컨텍스트 스위치가 프로세스 컨텍스트 스위치보다 가벼운 핵심 이유다.
### 5단계: 레지스터 및 스택 복원
들어오는 태스크의 커널 스택 포인터가 PCB에서 로드되고, 범용 레지스터가 커널 스택의 `pt_regs`에서 복원된다. 이 시점에서 CPU는 완전히 새 태스크의 컨텍스트에서 실행된다.
추가로 복원해야 할 per-process 상태로는 스레드 로컬 저장소 베이스(x86-64의 `FS`/`GS` 세그먼트 베이스), 디버그 레지스터(추적 중인 프로세스), 성능 모니터링 카운터 등이 있다. AArch64에서는 PAC(Pointer Authentication) 키 5개(각 128비트)와 MTE(Memory Tagging Extension) 관련 레지스터도 복원 대상이다.
### 6단계: 사용자 공간 복귀
x86-64에서는 `IRET`(약 40~80 사이클) 또는 `SYSRET`(약 20~30 사이클)를 통해 사용자 모드(Ring 3[^ring])로 복귀한다. `SYSRET`가 더 빠르지만, `RCX`에 비정규*non-canonical*[^non-canonical] 주소가 들어 있으면 커널 스택을 가진 채 Ring 3에서 폴트가 발생하는 보안 함정이 있어, 리눅스는 특정 엣지 케이스에서 `IRET`로 폴백한다.
AArch64는 `ERET` 하나로 통일된 복귀 메커니즘을 제공한다. `ELR_EL1`에서 `PC`를, `SPSR_EL1`에서 `PSTATE`를 복원하여 `EL0`으로 돌아간다.
새 태스크는 이전에 중단된 정확한 명령어 지점에서 실행을 재개하며, 전환이 일어났다는 사실을 인지하지 못한다.
## 리눅스 커널 구현: `context_switch()`
리눅스 v6.19의 `context_switch()` 함수(`kernel/sched/core.c:5236`)는 위 6단계 중 4~5단계를 직접 오케스트레이션한다. 이 함수는 `__schedule()`에서 스케줄러가 다음 태스크를 선택한 후 호출된다.
```c
/*
* context_switch - switch to the new MM and the new thread's register state.
*/
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
prepare_task_switch(rq, prev, next);
arch_start_context_switch(prev);
if (!next->mm) { // → 커널 스레드
enter_lazy_tlb(prev->active_mm, next);
next->active_mm = prev->active_mm;
if (prev->mm) // ← 유저 프로세스에서 온 경우
mmgrab_lazy_tlb(prev->active_mm);
else
prev->active_mm = NULL;
} else { // → 유저 프로세스
membarrier_switch_mm(rq, prev->active_mm, next->mm);
switch_mm_irqs_off(prev->active_mm, next->mm, next);
lru_gen_use_mm(next->mm);
if (!prev->mm) { // ← 커널 스레드에서 온 경우
rq->prev_mm = prev->active_mm;
prev->active_mm = NULL;
}
}
switch_mm_cid(rq, prev, next);
prepare_lock_switch(rq, next, rf);
switch_to(prev, next, prev);
barrier();
return finish_task_switch(prev);
}
```
이 코드의 핵심 분기는 `next->mm`의 존재 여부다.
**커널 스레드로의 전환(`!next->mm`)**: 커널 스레드는 고유한 사용자 공간 메모리 맵을 갖지 않는다. 이전 태스크의 `active_mm`[^active-mm]을 빌려 쓰는 **Lazy TLB** 최적화를 적용하여 `CR3`/`TTBR0` 변경과 TLB 플러시를 완전히 회피한다. `mmgrab_lazy_tlb()`로 참조 카운트를 올려 메모리 맵이 조기 해제되는 것을 방지한다.
**유저 프로세스로의 전환**: `switch_mm_irqs_off()`가 실질적인 주소 공간 전환을 수행한다. 내부적으로 `CR3` 또는 `TTBR0`를 변경하며, `PCID`/`ASID`를 활용해 불필요한 TLB 플러시를 최소화한다.
양쪽 경로 모두 `switch_to()` 매크로를 통해 레지스터와 스택을 전환한다. 이 매크로는 아키텍처별 어셈블리로 구현되어 있으며, x86-64에서는 `__switch_to_asm()`이 callee-saved[^callee-saved] 레지스터(`RBX`, `RBP`, `R12`~`R15`)를 푸시하고 `RSP`를 교체한 뒤 `__switch_to()`를 호출한다. `switch_to()`가 반환된 시점에서 CPU는 이미 `next` 태스크의 컨텍스트에서 실행되고 있다.
![[contextSwitchCallFlow.svg]]
`finish_task_switch()`는 전환 완료 후 마무리 작업을 담당한다. 런큐[^runqueue] 잠금 해제, 죽은 태스크의 자원 회수, `perf` 이벤트 처리 등을 수행한다. 이 함수는 BPF[^bpf] 및 `perf`에서 off-CPU[^off-cpu] 분석의 핵심 추적 지점이기도 하다.
## 프로세스 전환 vs. 스레드 전환
![[processVsThreadSwitch.svg]]
| 항목 | 프로세스 전환 | 스레드 전환 |
|------|-------------|-----------|
| 레지스터 저장/복원 | O | O |
| 페이지 테이블 전환 | O | X (공유 주소 공간) |
| TLB 플러시 | O (`PCID`/`ASID` 없으면) | X |
| FPU 상태 전환 | O | O |
| 캐시 영향 | 높음 (cold cache) | 낮음 (공유 캐시) |
| 대략적 비용 | 약 1~10 μs | 약 0.5~5 μs |
[스레드](Thread.md) 간 전환이 가벼운 이유는 명확하다. 같은 프로세스에 속한 스레드들은 동일한 `mm_struct`를 공유하므로, 주소 공간 전환과 그에 따른 TLB 무효화가 생략된다.
## 성능 비용 분석
컨텍스트 스위치의 비용은 직접 비용과 간접 비용으로 나뉘며, 실무에서는 간접 비용이 지배적인 경우가 많다.
### 직접 비용
직접 비용은 전환 과정 자체에서 소비되는 시간이다. 범용 레지스터 저장/복원에 약 50ns, `CR3` 쓰기와 TLB 플러시에 약 200~500ns, FPU의 `XSAVE`/`XRSTOR`에 약 200ns, 스케줄러의 다음 태스크 선택에 약 100~500ns가 소요된다. 합산하면 대략 1~5μs 수준이다.
### 간접 비용
간접 비용은 전환 이후에 발생하는 성능 저하다. TLB가 플러시되면 새 태스크의 메모리 접근마다 페이지 테이블 워크가 발생하고, 이전 태스크의 워킹 셋이 차지하던 캐시 라인이 새 태스크의 데이터로 교체되면서 L1/L2/L3 캐시 미스가 급증한다. 분기 예측기의 히스토리도 무효화되어 분기 미스프레딕션이 늘어난다. 이러한 간접 비용은 워킹 셋 크기에 따라 직접 비용의 수 배에서 수십 배에 달할 수 있다.
### KPTI의 추가 비용
x86-64에서 Meltdown 취약점 완화를 위해 도입된 `KPTI`(Kernel Page Table Isolation)는 프로세스마다 사용자/커널 두 세트의 페이지 테이블을 유지한다. 시스템 콜마다 `CR3`를 두 번 전환해야 하므로, syscall 집약적 워크로드에서 5~30%의 추가 오버헤드가 발생한다. `PCID`가 이 비용을 상당 부분 상쇄하지만, 완전히 제거하지는 못한다. AArch64는 `TTBR0`/`TTBR1` 분리 설계 덕분에 대부분의 코어에서 `KPTI`가 불필요하다(Cortex-A75 예외).
## x86-64 vs. AArch64 비교
| 항목 | x86-64 | AArch64 |
|------|--------|---------|
| 권한 모델 | Ring 0~3 (0과 3만 사용) | `EL0`~`EL3` |
| 하드웨어 자동 저장 | 5개 (`SS`, `RSP`, `RFLAGS`, `CS`, `RIP`) | 2개 (`SPSR_EL1`, `ELR_EL1`) |
| 시스템 콜 진입 | `SYSCALL`/`SYSRET` (전용 빠른 경로) | `SVC` (동기 예외와 동일 경로) |
| 범용 레지스터 수 | 16 (`RAX`~`R15`) | 31 (`X0`~`X30`) + `SP_EL0` |
| 페이지 테이블 레지스터 | `CR3` 하나 (전체 주소 공간) | `TTBR0` (사용자) + `TTBR1` (커널) 분리 |
| TLB 태깅 | `PCID` (12비트, 최대 4,096) | `ASID` (8/16비트, 최대 65,536) |
| 커널 매핑 전환 | 필요 (`CR3`가 전체를 커버) | 불필요 (`TTBR1` 고정) |
| SIMD 상태 크기 | 최대 약 2.7KB (`AVX-512`) | 최대 약 8KB (`SVE` 최대 벡터 길이) |
| Spectre 완화 | `IBPB` 명시적 발행 필요 | 대부분 코어에서 하드웨어 격리 |
| 사용자 복귀 메커니즘 | `IRET` (느림) / `SYSRET` (빠름, 주의 필요) | `ERET` (단일 통합 메커니즘) |
| 대략적 전환 비용 | 약 1~5 μs | 약 0.5~3 μs |
AArch64가 컨텍스트 스위치에서 구조적으로 유리한 이유는 `TTBR` 분리 설계와 넓은 `ASID` 공간에 있다. 커널 매핑을 전환할 필요가 없고, TLB 플러시가 발생할 확률이 현저히 낮다.
## 완화 기법
컨텍스트 스위치의 비용을 줄이기 위해 하드웨어와 소프트웨어 양쪽에서 다양한 최적화가 적용된다.
**`PCID`/`ASID`**는 TLB 항목에 프로세스 식별자를 태깅하여 전체 플러시를 회피한다. 스위치 집약적 워크로드에서 TLB 미스 페널티를 10~40% 줄인다.
**Lazy FPU 복원**은 FPU 상태를 즉시 복원하지 않고, 태스크가 실제로 부동소수점 명령어를 사용할 때까지 지연시킨다. 리눅스에서는 `TIF_NEED_FPU_LOAD` 플래그로 구현된다.
**CPU 어피니티 고정**은 특정 태스크를 같은 코어에 바인딩하여 웜 캐시와 TLB를 유지한다.
**Tickless 커널(`NO_HZ`)**은 유휴 또는 단일 태스크 CPU에서 타이머 인터럽트를 억제하여 불필요한 전환을 방지한다.
**사용자 공간 스케줄링**(고루틴, 그린 스레드 등)은 커널 진입 없이 사용자 공간에서 전환을 수행하여 비용을 수십 나노초 수준까지 낮춘다. 다만 커널의 선점과 I/O 스케줄링을 활용할 수 없다는 트레이드오프가 있다.
---
[^simd]: SIMD(Single Instruction, Multiple Data)는 하나의 명령어로 여러 데이터를 동시에 처리하는 병렬 연산 방식이다. x86의 `SSE`/`AVX`, ARM의 `NEON`/`SVE`가 대표적이다. — Intel SDM Volume 1, Chapter 10; ARM Architecture Reference Manual, Chapter C1.
[^sve]: `SVE`(Scalable Vector Extension)는 ARM의 가변 길이 벡터 확장이다. 벡터 길이가 128~2048비트 사이에서 하드웨어 구현에 따라 달라지므로, 컨텍스트 저장 크기도 가변적이다. `SME`(Scalable Matrix Extension)는 행렬 연산용 확장이다. — ARM Architecture Reference Manual for A-profile, Chapter B1 (The AArch64 Application Level Programmers' Model).
[^xsave]: `XSAVE`/`XRSTOR`는 x86의 확장 상태 저장/복원 명령어로, FPU, `SSE`, `AVX`, `AVX-512` 등 프로세서 확장 기능의 상태를 메모리에 일괄 저장하거나 복원한다. 비트마스크(`XCR0`, `XSTATE_BV`)로 필요한 컴포넌트만 선택적으로 처리할 수 있다. — Intel SDM Volume 1, Chapter 13 "Managing State Using the XSAVE Feature Set".
[^preemption]: 선점*preemption*이란 현재 실행 중인 태스크의 동의 없이 운영체제가 CPU를 강제로 회수하는 동작이다. 선점형 스케줄링에서는 타이머 인터럽트나 높은 우선순위 태스크 도착 시 자동으로 발생한다. — Silberschatz et al., *Operating System Concepts* 10th ed., §5.1.
[^time-quantum]: 타임 퀀텀*time quantum*(또는 타임 슬라이스*time slice*)은 스케줄러가 하나의 태스크에 연속으로 허용하는 최대 CPU 사용 시간이다. 이 시간이 만료되면 타이머 인터럽트가 발생하여 스케줄링 판단이 이루어진다. — Silberschatz et al., *Operating System Concepts* 10th ed., §5.3.4.
[^msr]: MSR(Model-Specific Register)은 프로세서 모델에 따라 다른 특수 레지스터로, `RDMSR`/`WRMSR` 명령어로 접근한다. `LSTAR`(MSR 주소 `0xC0000082`)는 `SYSCALL` 명령어 실행 시 분기할 커널 진입점 주소를 저장한다. — Intel SDM Volume 3, Chapter 5 "SYSCALL/SYSRET"; AMD64 APM Volume 2, §6.1.1.
[^per-cpu]: per-CPU 자료구조는 각 CPU 코어마다 독립된 복사본을 두는 데이터로, 잠금 없이 접근할 수 있어 성능이 좋다. 커널 스택 포인터, 현재 태스크 포인터(`current`), 런큐 등이 per-CPU로 관리된다. — `include/linux/percpu.h`; Love, *Linux Kernel Development* 3rd ed., Chapter 12.
[^pt-regs]: `pt_regs`는 리눅스 커널에서 인터럽트나 시스템 콜 진입 시 CPU 레지스터 상태를 저장하는 아키텍처별 구조체다. x86-64에서는 `arch/x86/include/asm/ptrace.h`, AArch64에서는 `arch/arm64/include/asm/ptrace.h`에 정의되어 있다.
[^mm-struct]: `mm_struct`는 리눅스에서 프로세스의 가상 주소 공간 전체를 기술하는 구조체다. 페이지 테이블 루트, VMA(Virtual Memory Area) 목록, 메모리 통계 등을 포함하며, 같은 프로세스의 스레드들은 하나의 `mm_struct`를 공유한다. — `include/linux/mm_types.h`; Bovet and Cesati, *Understanding the Linux Kernel* 3rd ed., Chapter 9.
[^pml]: `PML4`(Page Map Level 4)와 `PML5`(Page Map Level 5)는 x86-64의 다단계 페이지 테이블 계층에서 최상위 테이블이다. `PML4`는 4단계 변환으로 48비트 가상 주소(256TB)를, `PML5`는 5단계 변환으로 57비트 가상 주소(128PB)를 지원한다. — Intel SDM Volume 3, Chapter 4 "Paging".
[^tlb]: TLB(Translation Lookaside Buffer)는 가상 주소에서 물리 주소로의 변환 결과를 캐싱하는 하드웨어 버퍼다. TLB 히트 시 페이지 테이블 워크 없이 즉시 주소를 변환할 수 있어, 메모리 접근 성능에 큰 영향을 미친다. — Hennessy and Patterson, *Computer Architecture: A Quantitative Approach* 6th ed., Appendix B.
[^ring]: Ring은 x86 아키텍처의 하드웨어 권한 수준 모델이다. Ring 0(커널 모드)은 모든 명령어와 하드웨어에 접근할 수 있고, Ring 3(사용자 모드)은 특권 명령어 실행이 제한된다. 현재 권한 수준(`CPL`)은 `CS` 세그먼트 셀렉터의 하위 2비트에 인코딩된다. — Intel SDM Volume 3, §5.5 "Privilege Levels".
[^non-canonical]: x86-64의 48비트 가상 주소 모드에서, 유효 주소는 `0x0000000000000000`~`0x00007FFFFFFFFFFF`(사용자)과 `0xFFFF800000000000`~`0xFFFFFFFFFFFFFFFF`(커널) 범위에만 존재한다. 이 두 범위 사이의 주소를 비정규*non-canonical* 주소라 하며, 접근 시 General Protection Fault(`#GP`)가 발생한다. — Intel SDM Volume 1, §3.3.7.1; AMD64 APM Volume 2, §1.1.
[^active-mm]: `active_mm`은 현재 CPU에서 실제로 활성화된 메모리 맵을 가리키는 `task_struct`의 필드다. 유저 프로세스에서는 `mm`과 동일하지만, 커널 스레드(`mm == NULL`)는 직전 태스크에서 빌려온 `active_mm`을 사용하여 불필요한 주소 공간 전환을 피한다. — `include/linux/sched.h`; Bovet and Cesati, *Understanding the Linux Kernel* 3rd ed., §3.2.
[^callee-saved]: callee-saved 레지스터(또는 비휘발성*non-volatile* 레지스터)는 함수 호출 규약(ABI)에서 피호출 함수*callee*가 원래 값을 보존해야 하는 레지스터다. x86-64 System V ABI에서는 `RBX`, `RBP`, `R12`~`R15`가 이에 해당한다. — *System V Application Binary Interface — AMD64 Architecture Processor Supplement*, §3.2.1.
[^runqueue]: 런큐*runqueue*는 특정 CPU에서 실행 대기 중인 태스크들을 관리하는 per-CPU 자료구조다. 리눅스에서는 `struct rq`로 구현되며, 스케줄링 클래스별 서브큐(`cfs_rq`, `rt_rq` 등)를 포함한다. — `kernel/sched/sched.h`; Love, *Linux Kernel Development* 3rd ed., Chapter 4.
[^bpf]: BPF(Berkeley Packet Filter)는 원래 네트워크 패킷 필터링용 가상 머신이었으나, eBPF(extended BPF)로 확장되어 리눅스 커널의 범용 추적·모니터링 프레임워크가 되었다. 커널 함수에 프로브를 걸어 안전하게 실시간 성능 분석을 수행할 수 있다. — `Documentation/bpf/`; Gregg, B. (2019) *BPF Performance Tools*, Addison-Wesley.
[^off-cpu]: off-CPU 분석은 태스크가 CPU 위에서 실행되지 않는 시간(I/O 대기, 잠금 대기, 슬립 등)을 측정하는 성능 분석 기법이다. `perf`나 BPF 도구로 `finish_task_switch()`를 추적하여 대기 원인과 소요 시간을 파악한다. — Gregg, B. (2019) *BPF Performance Tools*, Addison-Wesley, Chapter 14 "Kernel".
## 출처
- Silberschatz, A., Galvin, P. B., and Gagne, G. (2018) *Operating System Concepts*. 10th ed. Hoboken, NJ: Wiley.
- Arpaci-Dusseau, R. H. and Arpaci-Dusseau, A. C. (2023) *Operating Systems: Three Easy Pieces*. 1.10 ed. Arpaci-Dusseau Books.
- Tanenbaum, A. S. and Bos, H. (2023) *Modern Operating Systems*. 5th ed. London: Pearson.
- Stallings, W. (2018) *Operating Systems: Internals and Design Principles*. 9th ed. London: Pearson.
- Anderson, T. and Dahlin, M. (2012) *Operating Systems: Principles and Practice*. Recursive Books.
- Bovet, D. P. and Cesati, M. (2008) *Understanding the Linux Kernel*. 3rd ed. Sebastopol, CA: O'Reilly.
- Love, R. (2010) *Linux Kernel Development*. 3rd ed. Upper Saddle River, NJ: Addison-Wesley.
- Gregg, B. (2019) *BPF Performance Tools*. Upper Saddle River, NJ: Addison-Wesley.
- Hennessy, J. L. and Patterson, D. A. (2019) *Computer Architecture: A Quantitative Approach*. 6th ed. Cambridge, MA: Morgan Kaufmann.
- Patterson, D. A. and Hennessy, J. L. (2021) *Computer Organization and Design MIPS Edition: The Hardware/Software Interface*. 6th ed. Cambridge, MA: Morgan Kaufmann.
- Intel Corporation. *Intel® 64 and IA-32 Architectures Software Developer's Manual*.
- AMD. *AMD64 Architecture Programmer's Manual — System Programming*.
- Markstedter, M. (2023) *Blue Fox: Arm Assembly Internals and Reverse Engineering*. Indianapolis, IN: Wiley.
- *System V Application Binary Interface — AMD64 Architecture Processor Supplement*.
- Linux kernel source v6.19 — `kernel/sched/core.c`, `arch/x86/kernel/process_64.c`, `arch/arm64/kernel/process.c`.