포스트

컨텍스트 스위치 — 전환은 공짜가 아니다, 직접비용과 캐시·TLB 오염

컨텍스트 스위치의 진짜 비용 — 레지스터 저장·복구의 직접비용과 TLB flush·캐시 오염의 간접비용을 측정과 애니메이션으로 분석합니다.

컨텍스트 스위치 — 전환은 공짜가 아니다, 직접비용과 캐시·TLB 오염

“전환은 한 번에 몇 마이크로초나 걸릴까”

스케줄러가 “이제 B를 돌려라”라고 결정하면, CPU는 돌던 A를 멈추고 B로 갈아탑니다. 이 갈아타기가 컨텍스트 스위치(context switch)입니다. 보통 “레지스터 몇 개 저장하고 복구하는 거니까 금방”이라고 넘깁니다. 그런데 측정해 보면 직접 비용은 1~3μs인데, 실제 체감 비용은 그 수십 배까지 튀어 오릅니다.

그 차이가 어디서 오는지가 이 글의 핵심입니다. 컨텍스트 스위치의 진짜 비용은 레지스터 저장이 아니라, 전환 직후 텅 비어버린 캐시와 TLB입니다. 이걸 모르면 “스레드를 많이 띄웠는데 왜 더 느려지지?”, “CPU는 노는데 왜 처리량이 안 오르지?” 같은 질문 앞에서 영원히 막힙니다.

컨텍스트 스위치가 저장·복구하는 것

“컨텍스트”란 한 실행 흐름을 그 순간 그대로 얼려서 나중에 되살리는 데 필요한 전부입니다. 커널은 이걸 프로세스의 PCB(Process Control Block), 리눅스에선 task_struct에 저장합니다.

무엇정체왜 필요
PC (program counter, RIP)다음에 실행할 명령 주소멈춘 바로 그 줄부터 재개하려고
범용 레지스터rax~r15 등 연산 중간값계산 상태를 그대로 복원
SP (stack pointer, RSP)스택 꼭대기함수 호출 깊이·지역변수 복원
상태 레지스터 (RFLAGS)비교 결과·플래그분기 조건 복원
CR3 (페이지 테이블 베이스)이 프로세스의 주소공간 뿌리다른 프로세스로 갈 때만 교체

마지막 줄 CR3가 이 글 전체의 주인공입니다. CR3를 바꾼다는 건 “주소공간 자체를 통째로 갈아끼운다”는 뜻이고, 거기서 비싼 부작용이 줄줄이 따라 나옵니다.

무엇이 전환을 일으키나

컨텍스트 스위치는 내가 부르는 게 아니라 항상 커널 모드에서 일어납니다. 커널로 진입하는 계기는 크게 넷입니다.

flowchart LR
  A["실행 중인 프로세스"] -->|타이머 인터럽트<br/>타임슬라이스 소진| K["커널 진입<br/>스케줄러 호출"]
  A -->|블로킹 시스템콜<br/>read·recv·lock 대기| K
  A -->|더 높은 우선순위<br/>깨어남 → 선점| K
  A -->|자발적 양보<br/>yield·sleep·exit| K
  K -->|다음 주자 선택| B["다른 프로세스 실행"]
  • 타이머 인터럽트: 타임슬라이스가 끝나면 하드웨어가 강제로 커널에 진입시켜 시분할을 성립시킵니다(= 비자발적 전환).
  • 블로킹 시스템콜: read()가 디스크를 기다려야 하면, 그동안 CPU를 놀릴 이유가 없으니 커널이 다른 프로세스로 전환합니다(= 자발적 전환).
  • 선점(preemption): 더 급한 프로세스가 깨어나면 현재 프로세스를 밀어냅니다.
  • 자발적 양보: sched_yield(), sleep(), 종료.

자발 vs 비자발 — 진단의 갈림길. 리눅스는 이 둘을 따로 셉니다(/proc/<pid>/status의 voluntary_ctxt_switches / nonvoluntary_ctxt_switches). 자발적 전환이 폭증하면 I/O·락 대기가 많다는 신호이고, 비자발적 전환이 폭증하면 CPU 경합(러너블 스레드 과다)이라는 신호입니다. 원인이 정반대라 처방도 정반대입니다.

애니메이션 ①: 레지스터 파일이 통째로 교체된다

전환의 “직접 비용” 부분입니다. CPU 안의 레지스터 파일 내용을 A의 PCB로 저장(save)한 뒤, B의 PCB에서 복구(restore)해 CPU에 채워 넣습니다. 아래에서 파란 값(프로세스 A)이 왼쪽 PCB로 빠져나간 뒤, 주황 값(프로세스 B)이 오른쪽 PCB에서 레지스터 파일로 올라타는 것을 보세요.

PCB A (저장 대상) task_struct A CPU 레지스터 파일 현재 컨텍스트 PCB B (복구 출처) task_struct B ① save: A의 레지스터 → PCB A ② restore: PCB B → 레지스터 (+ CR3 교체)

여기까지가 직접 비용입니다. 레지스터 십수 개를 메모리에 쓰고 읽는 일이라, 현대 CPU에선 보통 수백 ns ~ 수 μs에 끝납니다. 문제는 이 다음입니다.

진짜 비용은 보이지 않는다: 캐시·TLB 오염

CR3를 바꿔 주소공간을 갈아끼우는 순간, 두 개의 핵심 캐시가 무력화됩니다.

  • TLB(Translation Lookaside Buffer): 가상→물리 주소 변환을 캐싱하는 작은 표. 주소공간이 바뀌면 옛 매핑은 전부 무효이므로 TLB를 비웁니다(flush). 전환 직후 모든 메모리 접근은 페이지 테이블을 다시 걸어내려가는 page table walk를 겪습니다 — 수십 사이클씩.
  • CPU 캐시(L1/L2): 물리적으로 비워지진 않지만, B가 쓰는 데이터·코드는 캐시에 없습니다(cold). 한동안 접근마다 메모리까지 내려갔다 오는 캐시 미스가 쏟아지다가, 자주 쓰는 데이터가 다시 올라오면서 점차 warm해집니다.

이 “워밍업 세금”이 직접 비용을 압도합니다. 그래서 컨텍스트 스위치의 실측 비용은 워크로드의 메모리 발자국(working set)이 클수록 커집니다 — 레지스터 개수와는 무관하게요.

애니메이션 ②: cold 캐시가 warm 해지는 과정

전환 직후의 메모리 접근들을 시간순으로 늘어놓은 것입니다. 막대 높이는 접근 지연(latency). 처음엔 캐시·TLB가 비어 미스(빨강, 느림)가 연달아 터지다가, working set이 캐시에 올라오면서 점점 히트(초록, 빠름)로 내려앉습니다.

접근 지연 ↑ 전환 직후: TLB flush + cold 캐시 → 미스 폭증 working set 적재 완료 → warm 시간 →

핵심은 이겁니다: 전환이 잦을수록 매번 이 워밍업을 처음부터 다시 합니다. 막대가 초록으로 내려앉기도 전에 또 전환되면, 프로세스는 영원히 빨간 구간(미스)만 살다 갑니다. 이게 잦은 전환이 처리량을 죽이는 메커니즘입니다.

프로세스 전환 vs 스레드 전환 — TLB가 가른다

같은 “전환”이라도 비용이 크게 다릅니다. 갈림길은 주소공간(CR3)이 바뀌느냐입니다.

 프로세스 전환같은 프로세스 내 스레드 전환
주소공간(CR3)교체됨그대로 유지
TLBflush (또는 PCID로 무효화)유지 → 변환 캐시 살아있음
캐시cold 가능성 큼공유 데이터는 warm일 수 있음
상대 비용비쌈상대적으로 쌈

스레드가 “가볍다”고 말하는 핵심 근거가 바로 이것입니다 — 스레드끼리 전환할 땐 주소공간이 그대로라 TLB를 비우지 않아도 됩니다(스레드 글). 단, L1/L2 캐시 오염은 스레드 전환에서도 데이터가 다르면 여전히 발생합니다.

PCID/ASID: “전부 비우기”를 피하는 법

매 전환마다 TLB를 통째로 flush하는 건 낭비입니다. A로 곧 돌아올 텐데 A의 변환까지 버리니까요. 그래서 현대 CPU는 TLB 엔트리마다 주소공간 태그를 답니다 — x86은 PCID(Process-Context ID), ARM은 ASID.

  • 각 TLB 엔트리에 “이건 어느 주소공간 것”이라는 ID를 붙입니다.
  • 전환 시 flush하지 않고 현재 PCID와 일치하는 엔트리만 유효로 봅니다.
  • A→B→A로 돌아오면 A의 옛 TLB 엔트리가 아직 살아 있어 재워밍업 없이 재개합니다.

리눅스는 이 기능을 활용해 컨텍스트 스위치의 TLB 비용을 크게 줄입니다. (다만 Meltdown 완화책인 KPTI는 커널/유저 페이지테이블을 분리해 일부 비용을 되돌려 놓았고, PCID는 그 타격을 줄이는 데도 쓰입니다.)

헷갈리지 말 것: 모드 전환 ≠ 컨텍스트 스위치

가장 흔한 오해입니다. 시스템콜은 모드 전환(mode switch)이지 컨텍스트 스위치가 아닙니다.

  • 모드 전환: 유저 모드 ↔ 커널 모드. 같은 프로세스 안에서 권한만 바뀜. CR3 그대로, TLB 그대로. 상대적으로 쌈.
  • 컨텍스트 스위치: 다른 프로세스/스레드로 실행 주체가 바뀜. CR3 교체·TLB 무효화 동반. 비쌈.

read() 한 번은 모드 전환만 일으킬 수도, (블로킹되면) 이어서 컨텍스트 스위치까지 일으킬 수도 있습니다. 둘을 합쳐서 “시스템콜은 비싸다”고 뭉뚱그리면 최적화 방향을 못 잡습니다.

직접 측정하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1) 시스템 전체 전환율 — cs 컬럼이 초당 컨텍스트 스위치 수
vmstat 1
#  r  b   ...   cs   us sy id wa
#          ...  9123  12 38 50  0   ← cs 폭증 + sy 높음이면 전환 과다 의심

# 2) 특정 프로세스의 자발/비자발 전환 — 원인 진단의 핵심
grep ctxt /proc/$(pgrep -n nginx)/status
#  voluntary_ctxt_switches:     20431   ← I/O·락 대기 많음
#  nonvoluntary_ctxt_switches:  88123   ← CPU 경합(러너블 과다) 많음

# 3) 프로세스별 전환율 실시간
pidstat -w 1        # cswch/s(자발) nvcswch/s(비자발)

# 4) 전환의 '간접 비용'을 직접 본다 — 미스 카운터
perf stat -e context-switches,cache-misses,dTLB-load-misses,iTLB-load-misses ./app
#  context-switches 가 오를 때 cache-misses·dTLB-misses 도 같이 뛰면
#  → 워밍업 세금을 내고 있다는 증거

현실 체크 — “스레드를 늘렸는데 느려졌다.” 코어 수보다 훨씬 많은 바쁜 스레드를 띄우면, CPU는 일하는 시간보다 갈아타는 시간이 늘어납니다(nvcswch/s 폭증). 게다가 매 전환이 캐시를 식혀 미스가 늘죠. 그래서 CPU 바운드 작업의 워커 수는 보통 코어 수 근처가 최적이고, 그 이상은 전환 오버헤드로 처리량이 오히려 떨어집니다. “동시성을 늘리면 빨라진다”가 항상 참이 아닌 이유입니다.

면접/리뷰 단골 질문

  • Q. 컨텍스트 스위치 때 무엇을 저장하나? → PC·범용 레지스터·SP·상태 레지스터, 그리고 다른 프로세스로 갈 때만 CR3(페이지테이블 베이스). CR3 교체가 TLB 무효화를 부른다.
  • Q. 진짜 비용은 어디서 오나? → 레지스터 저장(직접비용)이 아니라 TLB flush + cold 캐시로 인한 미스 폭증(간접비용). working set이 클수록 비싸다.
  • Q. 프로세스 전환과 스레드 전환의 차이는? → 스레드 전환은 주소공간(CR3)이 그대로라 TLB를 안 비워도 돼 더 싸다. 단 데이터 캐시 오염은 둘 다 가능.
  • Q. PCID/ASID는 무엇을 해결하나? → TLB 엔트리에 주소공간 태그를 붙여 전환 시 전체 flush를 피한다. 곧 돌아올 프로세스의 변환을 살려둬 재워밍업을 줄인다.
  • Q. 시스템콜은 컨텍스트 스위치인가? → 아니다. 그건 같은 프로세스 내 모드 전환. 블로킹되어 다른 프로세스로 넘어갈 때 비로소 컨텍스트 스위치가 더해진다.

정리

  • 컨텍스트 스위치는 한 실행 흐름을 PCB에 얼리고 다른 흐름을 되살리는 일 — PC·레지스터·SP·(프로세스 간이면) CR3를 교체한다.
  • 직접 비용(레지스터 저장)은 작고, 간접 비용(TLB flush + cold 캐시 → 미스 폭증)이 진짜다. 그래서 working set이 클수록 비싸다.
  • 트리거는 타이머 인터럽트·블로킹 시스템콜·선점·자발적 양보. 자발/비자발 비율이 원인(I/O냐 CPU 경합이냐)을 가른다.
  • 스레드 전환이 프로세스 전환보다 싼 이유는 주소공간 유지 → TLB 보존. PCID/ASID는 프로세스 전환의 TLB 비용까지 깎는다.
  • 모드 전환(시스템콜) ≠ 컨텍스트 스위치. 잦은 전환은 매번 캐시를 식혀 처리량을 죽인다 → 워커 수는 코어 수 근처가 보통 최적.

다음 글: 전환과 격리를 이해했으니, 이제 분리된 프로세스들이 어떻게 서로 대화하는가 — 파이프·시그널·공유메모리·소켓을 다루는 IPC로 이어집니다. 전환을 결정하는 쪽은 앞 글 CPU 스케줄링입니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

시리즈 · 운영체제 A-Z (20편)

  1. 운영체제란 무엇인가 — 커널·유저모드·시스템콜, 왜 프로그램은 하드웨어를 직접 못 만지나
  2. 부팅 — 전원 버튼에서 커널까지, 그 1초 사이에 일어나는 모든 일
  3. 프로세스란 무엇인가 — 주소 공간·PCB·fork와 exec, 그리고 COW의 마법
  4. 스레드와 동시성 — 한 주소 공간 속 여러 실행 흐름, 그리고 비결정성의 시작
  5. CPU 스케줄링 — 누구에게 CPU를 줄까, FCFS부터 리눅스 CFS까지
  6. 컨텍스트 스위치 — 전환은 공짜가 아니다, 직접비용과 캐시·TLB 오염 지금 보는 글
  7. 프로세스 간 통신(IPC) — 격리된 프로세스가 협력하는 법, 파이프부터 공유 메모리까지
  8. 동기화 (1) — 경쟁 상태와 임계 구역, 뮤텍스 vs 스핀락
  9. 동기화 (2) — 세마포어·조건 변수, 그리고 데드락의 네 가지 조건
  10. 가상 메모리 — 모든 프로세스가 같은 주소에서 시작하는데 왜 안 겹치나
  11. 페이징·페이지 테이블·TLB — 4단계 주소 변환과 그 캐시
  12. 페이지 폴트·디맨드 페이징·스왑 — malloc 1GB가 즉시 성공하는 이유
  13. 페이지 교체 알고리즘 — 메모리가 꽉 찼을 때 누구를 쫓아낼까
  14. 메모리 할당자 — malloc은 어떻게 힙을 쪼개나, buddy와 slab까지
  15. 파일 시스템 (1) — 파일명은 파일이 아니다, inode와 VFS의 세계
  16. 파일 시스템 (2) — 쓰는 중 전원이 꺼지면? 저널링과 크래시 일관성
  17. I/O·인터럽트·DMA — CPU보다 백만 배 느린 장치와 효율적으로 대화하기
  18. 블록 I/O와 페이지 캐시 — 같은 파일을 두 번째 읽으면 왜 빠른가
  19. 가상화와 컨테이너 — VM과 컨테이너, 격리 스펙트럼의 양 끝
  20. 성능 관찰과 병목 분석 — "느려요"를 추측이 아니라 측정으로 (시리즈 마무리)