한 줄 정의

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 활용률과 응답성을 높일 수 있으므로, 오버헤드를 최소화하면서 충분한 빈도로 전환하는 것이 스케줄러 설계의 핵심입니다.

어떻게 동작하는가?

컨텍스트 스위칭이 발생하는 시점

  1. 타이머 인터럽트 (선점): 스케줄러가 설정한 타임 슬라이스(Linux CFS 기준 보통 1~10ms)가 만료되면 하드웨어 타이머가 인터럽트를 발생시키고, 커널이 현재 프로세스를 선점합니다.
  2. I/O 요청 (자발적): 프로세스가 read(), write() 같은 블로킹 시스템 콜을 호출하면 스스로 CPU를 양보하고 WAITING 상태로 전환됩니다.
  3. 동기화 대기 (자발적): mutex_lock(), sem_wait() 등에서 lock을 획득하지 못하면 BLOCKED 상태로 전환됩니다.
  4. 더 높은 우선순위 프로세스 도착 (선점): 실시간 프로세스가 깨어나면 현재 실행 중인 일반 프로세스를 즉시 선점합니다.

컨텍스트 스위칭 단계 (프로세스 간)

  1. 인터럽트/시스템 콜 발생 → 유저 모드에서 커널 모드로 전환
  2. 현재 프로세스 상태 저장:
    • CPU 레지스터 (범용 레지스터, PC, SP, 상태 레지스터 등)를 현재 프로세스의 PCB(task_struct)에 저장
    • FPU/SIMD 레지스터 (lazy saving 방식으로 필요 시에만)
  3. 스케줄러 호출:
    • schedule() 함수가 실행 큐에서 다음 실행할 프로세스를 선택
    • Linux CFS의 경우 vruntime이 가장 작은 태스크를 Red-Black Tree에서 선택
  4. 새 프로세스 상태 복원:
    • 새 프로세스의 PCB에서 레지스터 값을 CPU에 로드
    • 페이지 테이블 전환: CR3 레지스터(x86)에 새 프로세스의 페이지 테이블 베이스 주소를 로드
    • TLB flush: 주소 공간이 바뀌므로 TLB 항목을 무효화 (PCID 지원 시 부분 무효화 가능)
  5. 커널 모드에서 유저 모드로 복귀 → 새 프로세스의 PC부터 실행 재개

직접 비용 vs 간접 비용

구분내용규모
직접 비용레지스터 저장/복원, 스케줄러 실행, 모드 전환~1-10μs
간접 비용 — TLB miss프로세스 전환 시 TLB flush로 인해 가상→물리 주소 변환을 다시 수행수십~수백 μs
간접 비용 — CPU 캐시 cold새 프로세스의 데이터/명령어가 L1/L2/L3 캐시에 없어 메모리 접근 필요수백 μs~수 ms
간접 비용 — 파이프라인 flushCPU의 명령어 파이프라인과 분기 예측 버퍼가 무효화됨수십 ns

핵심 포인트: 직접 비용보다 간접 비용이 훨씬 크다. 레지스터 교체는 마이크로초 단위이지만, 캐시와 TLB가 “차가워진” 후 다시 “데워지는” 시간이 전체 비용의 대부분을 차지합니다.

프로세스 간 전환 vs 스레드 간 전환

같은 프로세스 내의 스레드 간 전환은 프로세스 간 전환보다 가볍습니다:

단계프로세스 전환스레드 전환 (같은 프로세스)
레지스터 저장/복원OO
페이지 테이블 전환 (CR3)OX (같은 주소 공간)
TLB flushO (PCID 없으면 전체)X
캐시 cold start심함 (다른 메모리 영역)경미 (공유 영역은 캐시 유지)
커널 모드 전환OO

스레드 간 전환이 가벼운 이유는 주소 공간을 공유하기 때문에 페이지 테이블 교체와 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

vmstatcs 컬럼으로 시스템 전체 컨텍스트 스위칭 횟수를 모니터링할 수 있습니다.

시각화

컨텍스트 스위칭 과정

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 프레임만최소한의 상태만
페이지 테이블 전환OXXX
TLB flushOXXX
캐시 영향심함경미경미거의 없음
커널 모드 전환OOX (유저 공간)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