Context Switching: CPU가 프로세스 상태를 저장하고 복원하는 과정
Context Switching(문맥 교환)은 CPU가 하나의 프로세스나 스레드 실행을 중단하고 다른 프로세스나 스레드를 실행하기 위해 현재 상태를 저장한 뒤 새로운 상태를 복원하는 과정이다. 이 메커니즘 덕분에 하나의 CPU 코어로도 여러 프로세스가 동시에 실행되는 것처럼 보이는 시분할(time-sharing)이 가능해진다. 다만 저장과 복원에는 실제 비
1. 개념
Context Switching은 CPU가 실행 중인 프로세스(또는 스레드)를 중단하고, 그 실행 상태를 저장한 후, 다른 프로세스의 저장된 상태를 불러와 실행을 재개하는 과정이다.
여기서 "상태(context)"는 다음을 포함한다.
- 프로그램 카운터(PC, Program Counter): 다음에 실행할 명령어의 주소
- 범용 레지스터 값들
- 스택 포인터(SP, Stack Pointer)
- 프로세서 상태 레지스터(플래그 등)
- 메모리 관리 정보(페이지 테이블 포인터 등)
이 정보는 운영체제가 관리하는 PCB(Process Control Block)라는 커널 자료구조에 저장된다. 프로세스마다 하나의 PCB가 존재하며, 컨텍스트 스위칭은 결국 "현재 프로세스의 레지스터 값을 PCB에 저장 → 다음 프로세스의 PCB에서 값을 읽어 레지스터에 적재"하는 작업이다.
2. 왜 사용하는가
CPU 코어 수는 유한하지만 실행하고 싶은 프로세스는 그보다 훨씬 많다. 컨텍스트 스위칭이 없다면 하나의 프로세스가 CPU를 독점하고 끝날 때까지 다른 작업은 대기해야 한다.
컨텍스트 스위칭을 통해 운영체제는 다음을 달성한다.
- 시분할(time-sharing): 짧은 시간 단위(타임 슬라이스)로 여러 프로세스를 번갈아 실행해 동시 실행처럼 보이게 함
- 응답성 확보: I/O 대기 중인 프로세스가 CPU를 점유하지 않고 다른 프로세스에게 양보
- 우선순위 기반 스케줄링: 높은 우선순위 작업이 필요할 때 즉시 CPU를 넘겨받을 수 있음
3. 동작 원리
컨텍스트 스위칭은 보통 다음 트리거 중 하나로 발생한다.
- 타이머 인터럽트: 타임 슬라이스가 만료되어 스케줄러가 개입
- 시스템 콜: 프로세스가 I/O 요청 등으로 커널에 제어를 넘김
- 인터럽트: 하드웨어 이벤트(디스크 완료, 네트워크 패킷 도착 등)
- 프로세스 종료 또는 블로킹: 대기 상태로 전환
전체 흐름은 다음과 같다.
[프로세스 A 실행 중]
↓ 인터럽트/타이머 발생
[유저 모드 → 커널 모드 전환]
↓
[A의 레지스터/PC/SP 값을 A의 PCB에 저장]
↓
[스케줄러가 다음 실행할 프로세스 B 선택]
↓
[B의 PCB에서 레지스터/PC/SP 값을 CPU에 복원]
↓
[커널 모드 → 유저 모드 전환]
↓
[프로세스 B 실행 시작]프로세스 간 컨텍스트 스위칭과 같은 프로세스 내 스레드 간 컨텍스트 스위칭은 비용이 다르다. 프로세스는 각자 독립된 가상 주소 공간을 가지므로 전환 시 페이지 테이블 교체와 TLB(Translation Lookaside Buffer) 플러시가 필요하다. 반면 스레드는 같은 주소 공간을 공유하므로 이 비용이 없어 상대적으로 가볍다.
4. 코드 예제
컨텍스트 스위칭의 개념을 파이썬으로 단순화해서 시뮬레이션하면 다음과 같다. 실제 CPU 레지스터 조작은 아니지만 "상태 저장 → 다음 프로세스 로드 → 상태 복원" 구조를 그대로 보여준다.
class PCB:
"""프로세스 제어 블록: 프로세스의 실행 문맥을 저장"""
def __init__(self, pid, program_counter=0, registers=None):
self.pid = pid
self.program_counter = program_counter
self.registers = registers or {"AX": 0, "BX": 0}
self.state = "ready" # ready, running, waiting
class Scheduler:
def __init__(self):
self.processes = []
self.current = None
def add_process(self, pcb: PCB):
self.processes.append(pcb)
def context_switch(self, next_pcb: PCB):
if self.current:
# 1. 현재 프로세스 상태를 PCB에 저장
self.current.state = "ready"
print(f"[저장] PID={self.current.pid} PC={self.current.program_counter} "
f"REG={self.current.registers}")
# 2. 다음 프로세스 상태를 CPU 문맥으로 복원
next_pcb.state = "running"
self.current = next_pcb
print(f"[복원] PID={next_pcb.pid} PC={next_pcb.program_counter} "
f"REG={next_pcb.registers}")
# 사용 예시
scheduler = Scheduler()
p1 = PCB(pid=1, program_counter=100, registers={"AX": 5, "BX": 10})
p2 = PCB(pid=2, program_counter=200, registers={"AX": 1, "BX": 2})
scheduler.add_process(p1)
scheduler.add_process(p2)
scheduler.context_switch(p1) # p1 실행 시작
p1.program_counter = 105 # p1이 몇 명령어 실행했다고 가정
scheduler.context_switch(p2) # 타이머 인터럽트로 p2로 전환실행 결과:
[복원] PID=1 PC=100 REG={'AX': 5, 'BX': 10}
[저장] PID=1 PC=105 REG={'AX': 5, 'BX': 10}
[복원] PID=2 PC=200 REG={'AX': 1, 'BX': 2}실제 리눅스 시스템에서 컨텍스트 스위칭 횟수를 관찰하려면 다음 명령어를 사용한다.
# 시스템 전체 컨텍스트 스위칭 횟수 (cs 컬럼)를 1초 간격으로 5회 출력
vmstat 1 5
# 특정 프로세스의 자발적/비자발적 컨텍스트 스위칭 횟수 확인
grep ctxt_switches /proc/$(pgrep -n python)/statusvoluntary_ctxt_switches는 프로세스가 I/O 대기 등으로 스스로 CPU를 양보한 횟수, nonvoluntary_ctxt_switches는 타임 슬라이스 만료 등으로 강제로 전환된 횟수를 의미한다.
5. 시간 복잡도 또는 성능 특성
컨텍스트 스위칭 자체는 O(1) 연산이다. 레지스터 저장/복원은 고정된 개수의 필드를 다루므로 프로세스 개수나 데이터 크기와 무관하게 일정한 시간이 걸린다.
다만 절대적인 비용은 하드웨어와 커널 구현에 따라 달라지며, 다음 요인이 실질적인 지연을 만든다.
- 직접 비용: 레지스터 저장/복원, 커널 모드 전환에 드는 명령어 실행 시간
- 간접 비용: 캐시(L1/L2) 미스, TLB 플러시로 인해 새 프로세스가 실행을 재개한 직후 메모리 접근이 느려지는 현상(캐시 워밍업 비용)
정확한 마이크로초 단위 수치는 CPU 아키텍처, 커널 버전, 활성화된 보안 완화 기법(예: 스펙터/멜트다운 패치) 등에 따라 달라지므로 이 문서에서 절대값을 단정하지 않는다. 실제 환경에서 측정하려면 vmstat의 cs 컬럼이나 perf sched 도구를 사용해 직접 확인하는 것이 정확하다.
6. 실무 사용 사례
- 스레드 풀 크기 설계: CPU 바운드 작업에서 스레드 수를 코어 수보다 지나치게 많이 만들면 컨텍스트 스위칭 오버헤드가 늘어나 처리량이 오히려 감소한다.
- 이벤트 루프 기반 런타임(Node.js 등): 단일 스레드에서 논블로킹 I/O와 콜백 큐를 사용해 컨텍스트 스위칭 없이 동시성을 흉내낸다. I/O 바운드 작업에서 스레드 기반 모델보다 오버헤드가 적은 이유 중 하나다.
- 코루틴/그린 스레드(Python asyncio, Go goroutine): 커널 개입 없이 유저 공간에서 실행 흐름을 전환하므로 커널 수준 컨텍스트 스위칭보다 훨씬 저렴하다.
- CPU affinity 설정: 특정 프로세스를 고정된 코어에 묶어 캐시 지역성을 유지하고 불필요한 스위칭 비용을 줄이는 기법으로, 고성능 서버나 실시간 시스템에서 사용한다.
7. 주의할 점
- 스레드를 과도하게 생성하면 컨텍스트 스위칭 빈도가 늘어나 실제 작업 처리 시간보다 전환 오버헤드가 커지는 스레싱(thrashing) 현상이 발생할 수 있다.
- I/O 바운드 작업에 스레드를 늘리는 것과 CPU 바운드 작업에 스레드를 늘리는 것은 효과가 다르다. CPU 바운드 작업은 코어 수를 초과하는 스레드 생성이 오히려 성능을 떨어뜨리는 경우가 많다.
- 비동기/이벤트 루프 모델이 항상 우월한 것은 아니다. CPU 바운드 연산에는 컨텍스트 스위칭 회피보다 병렬 처리(멀티 프로세스)가 더 적합할 수 있다.
- 컨텍스트 스위칭 비용을 줄이기 위한 CPU affinity나 NUMA 최적화는 특정 워크로드에서만 효과가 있으며, 무분별하게 적용하면 오히려 로드 밸런싱을 해칠 수 있다.
8. 핵심 정리
Context Switching은 운영체제가 CPU라는 제한된 자원을 여러 프로세스/스레드에 나누어 쓰기 위해 실행 상태를 PCB에 저장하고 복원하는 메커니즘이다. 연산 자체는 O(1)이지만 캐시 미스, TLB 플러시 같은 간접 비용이 실질적인 성능에 영향을 준다. 이 비용을 이해하면 스레드 풀 크기 설정, 비동기 런타임 선택, CPU affinity 튜닝 같은 실무 의사결정을 더 근거 있게 내릴 수 있다.