한 줄 정의
CPU가 현재 실행 중인 프로세스(또는 스레드)의 상태를 저장하고, 다음에 실행할 프로세스의 상태를 복원하는 과정. 멀티태스킹의 핵심 메커니즘이며, 공짜가 아닌 비용이 수반되는 연산이다.
쉽게 말하면
책상에서 프로젝트 A 보고서를 작성하다가 급하게 프로젝트 B 작업을 해야 할 때를 생각해보세요. A의 자료를 정리해서 서랍에 넣고(상태 저장), B의 자료를 꺼내서 펼치고(상태 복원), “어디까지 했더라?” 하고 맥락을 떠올리는(캐시 워밍업) 시간이 필요합니다. 컨텍스트 스위칭은 CPU가 바로 이 작업을 하는 것이며, 서랍에 넣고 꺼내는 시간(직접 비용)뿐 아니라 맥락을 다시 떠올리는 시간(간접 비용 — 캐시/TLB miss)까지 포함됩니다.
왜 이걸 알아야 하는가?
- 성능 튜닝의 숨은 변수: 톰캣 스레드 풀 사이즈를 500으로 늘렸더니 오히려 처리량이 떨어지는 현상이 발생할 수 있습니다. 이는 실제 CPU 코어 수 대비 과도한 스레드가 컨텍스트 스위칭 오버헤드를 유발하기 때문이며, 이 원리를 이해해야 적절한 스레드 풀 사이징이 가능합니다.
- Virtual Thread / Coroutine의 존재 이유: “경량 스레드가 왜 빠른가?”에 대한 답의 핵심이 “커널 컨텍스트 스위칭을 회피”하는 것입니다. 컨텍스트 스위칭 비용을 모르면 Virtual Thread가 왜 I/O-bound에서 유리한지 설명할 수 없습니다.
- 장애 분석에서의 단서:
vmstat에서cs(context switches/sec) 수치가 비정상적으로 높으면 스레드 경합이나 lock contention의 간접 증거입니다. 모니터링 지표의 의미를 파악하려면 컨텍스트 스위칭의 동작 원리를 알아야 합니다.
왜 이렇게 설계했는가?
- 해결하는 문제: CPU 코어는 한 번에 하나의 실행 흐름만 처리할 수 있는데, 사용자는 여러 프로그램을 “동시에” 실행하고 싶습니다. 컨텍스트 스위칭은 하나의 CPU에서 여러 프로세스를 빠르게 번갈아 실행하여 동시성(concurrency)의 착각을 만드는 메커니즘입니다.
- 이게 없다면: 한 프로그램이 끝나야 다음 프로그램을 실행할 수 있는 배치(batch) 시스템이 됩니다. I/O 대기 중에도 CPU가 놀게 되어 자원 활용률이 극도로 낮아집니다.
- 핵심 트레이드오프: 컨텍스트 스위칭 자체는 “유용한 일”을 하지 않는 순수 오버헤드입니다. 하지만 이 비용을 지불해야 CPU 활용률과 응답성을 높일 수 있으므로, 오버헤드를 최소화하면서 충분한 빈도로 전환하는 것이 스케줄러 설계의 핵심입니다.
어떻게 동작하는가?
컨텍스트 스위칭이 발생하는 시점
- 타이머 인터럽트 (선점): 스케줄러가 설정한 타임 슬라이스(Linux CFS 기준 보통 1~10ms)가 만료되면 하드웨어 타이머가 인터럽트를 발생시키고, 커널이 현재 프로세스를 선점합니다.
- I/O 요청 (자발적):
프로세스가
read(),write()같은 블로킹 시스템 콜을 호출하면 스스로 CPU를 양보하고 WAITING 상태로 전환됩니다. - 동기화 대기 (자발적):
mutex_lock(),sem_wait()등에서 lock을 획득하지 못하면 BLOCKED 상태로 전환됩니다. - 더 높은 우선순위 프로세스 도착 (선점): 실시간 프로세스가 깨어나면 현재 실행 중인 일반 프로세스를 즉시 선점합니다.
컨텍스트 스위칭 단계 (프로세스 간)
- 인터럽트/시스템 콜 발생 → 유저 모드에서 커널 모드로 전환
- 현재 프로세스 상태 저장:
- CPU 레지스터 (범용 레지스터, PC, SP, 상태 레지스터 등)를 현재 프로세스의 PCB(
task_struct)에 저장 - FPU/SIMD 레지스터 (lazy saving 방식으로 필요 시에만)
- CPU 레지스터 (범용 레지스터, PC, SP, 상태 레지스터 등)를 현재 프로세스의 PCB(
- 스케줄러 호출:
schedule()함수가 실행 큐에서 다음 실행할 프로세스를 선택- Linux CFS의 경우
vruntime이 가장 작은 태스크를 Red-Black Tree에서 선택
- 새 프로세스 상태 복원:
- 새 프로세스의 PCB에서 레지스터 값을 CPU에 로드
- 페이지 테이블 전환: CR3 레지스터(x86)에 새 프로세스의 페이지 테이블 베이스 주소를 로드
- TLB flush: 주소 공간이 바뀌므로 TLB 항목을 무효화 (PCID 지원 시 부분 무효화 가능)
- 커널 모드에서 유저 모드로 복귀 → 새 프로세스의 PC부터 실행 재개
직접 비용 vs 간접 비용
| 구분 | 내용 | 규모 |
|---|---|---|
| 직접 비용 | 레지스터 저장/복원, 스케줄러 실행, 모드 전환 | ~1-10μs |
| 간접 비용 — TLB miss | 프로세스 전환 시 TLB flush로 인해 가상→물리 주소 변환을 다시 수행 | 수십~수백 μs |
| 간접 비용 — CPU 캐시 cold | 새 프로세스의 데이터/명령어가 L1/L2/L3 캐시에 없어 메모리 접근 필요 | 수백 μs~수 ms |
| 간접 비용 — 파이프라인 flush | CPU의 명령어 파이프라인과 분기 예측 버퍼가 무효화됨 | 수십 ns |
핵심 포인트: 직접 비용보다 간접 비용이 훨씬 크다. 레지스터 교체는 마이크로초 단위이지만, 캐시와 TLB가 “차가워진” 후 다시 “데워지는” 시간이 전체 비용의 대부분을 차지합니다.
프로세스 간 전환 vs 스레드 간 전환
같은 프로세스 내의 스레드 간 전환은 프로세스 간 전환보다 가볍습니다:
| 단계 | 프로세스 전환 | 스레드 전환 (같은 프로세스) |
|---|---|---|
| 레지스터 저장/복원 | O | O |
| 페이지 테이블 전환 (CR3) | O | X (같은 주소 공간) |
| TLB flush | O (PCID 없으면 전체) | X |
| 캐시 cold start | 심함 (다른 메모리 영역) | 경미 (공유 영역은 캐시 유지) |
| 커널 모드 전환 | O | O |
스레드 간 전환이 가벼운 이유는 주소 공간을 공유하기 때문에 페이지 테이블 교체와 TLB flush가 불필요하다는 점입니다.
PCID (Process Context Identifier)
최신 x86 프로세서(Haswell 이후)는 PCID를 지원하여 TLB flush 비용을 줄입니다:
- TLB 항목에 프로세스별 ID를 태깅하여, 프로세스가 전환되어도 이전 프로세스의 TLB 항목이 즉시 무효화되지 않습니다.
- 동일 프로세스로 다시 돌아왔을 때 TLB hit 가능성이 높아집니다.
- Linux 4.14부터 기본 활성화되어 있으며, Meltdown 패치(KPTI) 이후 PCID의 중요성이 더 커졌습니다.
자발적 vs 비자발적 컨텍스트 스위칭
- 자발적 (voluntary): 프로세스가 I/O, lock 대기 등으로 스스로 CPU를 양보. 정상적인 동작.
- 비자발적 (involuntary): 타이머 만료나 높은 우선순위 프로세스 때문에 강제 전환. 과도하면 CPU 경합의 신호.
/proc/[pid]/status에서 확인 가능:
voluntary_ctxt_switches: 150
nonvoluntary_ctxt_switches: 42
vmstat의 cs 컬럼으로 시스템 전체 컨텍스트 스위칭 횟수를 모니터링할 수 있습니다.
시각화
컨텍스트 스위칭 과정
sequenceDiagram participant P1 as 프로세스 A (Running) participant K as 커널 participant P2 as 프로세스 B (Ready) Note over P1: 타이머 인터럽트 발생 P1->>K: 유저 → 커널 모드 전환 K->>K: 프로세스 A 레지스터를 PCB_A에 저장 K->>K: schedule() — 다음 프로세스 선택 K->>K: CR3 ← PCB_B의 페이지 테이블 K->>K: TLB flush (PCID 없으면 전체) K->>K: PCB_B에서 레지스터 복원 K->>P2: 커널 → 유저 모드 전환 Note over P2: 프로세스 B 실행 재개 Note over P1: Ready 큐에서 대기
비용 구조
컨텍스트 스위칭 총 비용
├── 직접 비용 (~1-10μs)
│ ├── 모드 전환 (유저 ↔ 커널)
│ ├── 레지스터 저장/복원 (범용 + FPU/SIMD)
│ └── 스케줄러 실행 (CFS: O(log N))
│
└── 간접 비용 (~수백 μs, 상황에 따라 수 ms) ← 진짜 비싼 부분
├── TLB miss (프로세스 전환 시)
├── CPU 캐시 cold start (L1/L2/L3)
└── 파이프라인 flush + 분기 예측 초기화
정리
| 기준 | 프로세스 간 전환 | 스레드 간 전환 (OS) | Virtual Thread 전환 | Coroutine 전환 |
|---|---|---|---|---|
| 전환 주체 | 커널 | 커널 | JVM 런타임 | Dispatcher (유저 공간) |
| 레지스터 교체 | 전체 (범용+FPU) | 전체 (범용+FPU) | Continuation 프레임만 | 최소한의 상태만 |
| 페이지 테이블 전환 | O | X | X | X |
| TLB flush | O | X | X | X |
| 캐시 영향 | 심함 | 경미 | 경미 | 거의 없음 |
| 커널 모드 전환 | O | O | X (유저 공간) | X (유저 공간) |
| 비용 | ~1-10μs + 간접 비용 | ~1μs + 경미한 간접 비용 | ~수백 ns | ~수십 ns |
| 발생 조건 | OS 스케줄러 결정 | OS 스케줄러 결정 | I/O 블로킹 시 자발적 | suspend 시 자발적 |
실무에서 만난 사례
- 스레드 풀 사이징:
CPU-bound 작업의 경우 스레드 수를
코어 수 + 1정도로 설정하는 것이 일반적입니다. 코어 수보다 과도하게 많은 스레드는 컨텍스트 스위칭만 증가시키고 실제 처리량은 감소시킵니다. I/O-bound의 경우 블로킹 동안 다른 스레드가 CPU를 활용할 수 있으므로 더 많은 스레드가 유효합니다. vmstat모니터링:vmstat 1에서cs값이 수만~수십만까지 치솟으면 lock contention이나 과도한 스레드 경합을 의심할 수 있습니다.pidstat -w로 프로세스별 컨텍스트 스위칭 횟수를 확인하여 원인 프로세스를 특정합니다.- KPTI (Meltdown 패치)의 영향: Meltdown 취약점 패치로 시스템 콜마다 커널/유저 페이지 테이블을 분리하게 되면서 컨텍스트 스위칭 비용이 증가했습니다. PCID 지원 CPU에서는 이 오버헤드가 크게 완화됩니다.
관련 개념
출처
- Abraham Silberschatz, “Operating System Concepts” Chapter 3.2 - Process Scheduling
- Robert Love, “Linux Kernel Development” Chapter 6 - Process Scheduling
- Brendan Gregg, “Systems Performance” Chapter 6 - CPUs