Context Switching: CPU 실행 상태를 넘기는 과정과 오버헤드
Context Switching의 상태 저장·복원 과정, 프로세스와 쓰레드 전환 비용, TLB·캐시 영향과 최적화 방법을 정리한다.
2026-08-14 · 최초 발행 2026-01-03
CPU가 다른 작업으로 넘어갈 때 보존해야 하는 상태
Context Switching은 CPU가 실행 중인 프로세스의 상태를 PCB에 보관하고, 다른 프로세스가 멈췄던 지점의 상태를 복원하는 작업이다. 여기에는 레지스터, 프로그램 카운터(PC), 메모리 관리 정보 등이 포함된다.
선점형 스케줄링에서 타임 슬라이스가 끝나거나, 프로세스가 I/O를 요청해 Sleep 상태로 들어가거나, 인터럽트와 우선순위 변경이 발생하면 전환이 일어날 수 있다. System Call로 Kernel 모드에 진입한 뒤에도 일부 경우에는 다른 프로세스로 넘어간다. 스케줄러가 다음 대상을 고르고 Dispatcher가 실제 상태 전환을 수행한다.
PCB에 담기는 실행 문맥
CPU의 범용 레지스터에는 연산 결과와 임시 데이터가 남아 있다. x86에서는 EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP가, ARM에서는 R0 ~ R15가 여기에 해당한다. x86의 CS, DS, SS, ES, FS, GS 같은 세그먼트 레지스터와 EFLAGS, ARM의 CPSR 플래그도 실행 상태의 일부다.
PC는 다음에 실행할 명령어 주소를 가리킨다. x86에서는 32비트의 EIP 또는 64비트의 RIP, ARM에서는 R15 (PC)가 이 역할을 한다. 이 값을 정확히 복원해야 중단된 위치에서 실행을 이어 갈 수 있다.
주소 공간도 프로세스별 상태다. x86의 CR3(Page Directory Base Register), ARM의 TTBR(Translation Table Base Register)는 가상 주소를 물리 주소로 변환하는 페이지 테이블 기반을 가리킨다. 페이지 테이블 캐시인 TLB는 전환 때 일부 경우 플러시가 필요하다.
열린 파일의 파일 디스크립터 테이블, 읽기·쓰기 버퍼 상태, Pending I/O 요청도 I/O 상태로 관리된다.
저장한 뒤 다음 실행 상태를 불러오는 순서
전환은 인터럽트나 System Call로 시작해 Kernel 모드로 들어간다. 현재 프로세스의 레지스터와 메모리 관리 정보를 PCB A에 저장하고, 스케줄러가 다음 대상을 선택한다. 이어 PCB B의 상태를 CPU에 복원한 뒤 User 모드에서 실행을 재개한다.
현재 상태를 PCB에 옮기는 형태는 다음과 같다.
// Pseudo code
void save_context(PCB *pcb) {
pcb->registers.eax = CPU.eax;
pcb->registers.ebx = CPU.ebx;
// ... 모든 레지스터 저장
pcb->registers.eip = CPU.eip;
pcb->registers.esp = CPU.esp;
pcb->registers.eflags = CPU.eflags;
}
pcb->page_table_base = CPU.CR3; // x86
pcb->stack_pointer = CPU.esp; // 현재 스택 위치
다음 프로세스를 다시 CPU에 올릴 때는 반대 방향으로 상태를 복원한다.
void restore_context(PCB *pcb) {
CPU.eax = pcb->registers.eax;
CPU.ebx = pcb->registers.ebx;
// ... 모든 레지스터 복원
CPU.eip = pcb->registers.eip;
CPU.esp = pcb->registers.esp;
CPU.eflags = pcb->registers.eflags;
}
CPU.CR3 = pcb->page_table_base; // 페이지 테이블 전환
// CPU.eip가 다음 명령어 주소를 가리킴
// 자동으로 해당 위치에서 실행 재개
전환 비용은 저장·복원보다 더 넓다
직접 비용에는 수십 개 레지스터의 저장·복원, CR3 업데이트와 일부 아키텍처에서의 TLB 플러시, User → Kernel → User 권한 레벨 전환이 들어간다. 각각 수백 나노초가 걸릴 수 있으며, 일반적인 총 직접 비용은 1 ~ 5 마이크로초다.
문제는 간접 비용이다. 프로세스 A의 데이터가 캐시에서 밀려나고 프로세스 B의 데이터를 다시 읽으면서 캐시 미스가 늘 수 있다. 최악의 경우 수백 마이크로초가 걸린다. TLB 플러시 뒤 새 항목을 적재하는 비용은 수십 마이크로초, 명령어 파이프라인 플러시와 분기 예측 실패에는 수 사이클이 든다. 전체 간접 비용은 수 마이크로초 ~ 수백 마이크로초로, 직접 비용보다 훨씬 클 수 있다.
Linux에서는 다음처럼 구간을 측정할 수 있다.
// Linux
clock_gettime(CLOCK_MONOTONIC, &start);
// Context Switch 발생
clock_gettime(CLOCK_MONOTONIC, &end);
전환 빈도를 줄이고, CPU Affinity를 설정하며, 프로세스 대신 쓰레드를 사용하거나 Lazy Context Switching을 적용하는 방식이 비용을 낮추는 선택지가 된다.
주소 공간을 바꾸는 전환과 공유하는 전환
프로세스 전환은 현재 레지스터를 저장한 뒤 페이지 테이블을 전환하고, 일부 아키텍처에서는 TLB를 플러시한 다음 다음 프로세스의 레지스터를 복원한다. 페이지 테이블 전환과 TLB 미스가 더해져 수 마이크로초 ~ 수십 마이크로초의 비용이 든다.
동일 프로세스 안의 쓰레드 전환은 레지스터를 저장·복원하지만 메모리 관리 정보는 유지한다. 페이지 테이블을 바꾸지 않고 TLB도 유지하므로 수백 나노초 ~ 수 마이크로초 수준이다.
| 항목 | 프로세스 | 쓰레드 |
|---|---|---|
| 레지스터 저장/복원 | 필요 | 필요 |
| 페이지 테이블 전환 | 필요 (CR3 변경) | 불필요 (공유) |
| TLB 플러시 | 필요 (일부 아키텍처) | 불필요 |
| 캐시 무효화 | 큼 | 작음 |
| 총 비용 | 5 ~ 50 μs | 0.5 ~ 5 μs |
쓰레드 전환은 페이지 테이블 전환이 없으므로 프로세스보다 5~10배 빠르다.
하드웨어와 운영체제가 비용을 다루는 방식
여러 레지스터 세트를 하드웨어에 유지하면 저장·복원 대신 레지스터 세트 전환으로 Context Switching을 처리할 수 있다. Intel Hyper-Threading은 논리 코어당 레지스터 세트를 제공하고, ARM FIQ 모드는 R8~R14 전용 레지스터를 사용한다.
TLB에서는 ASID(Address Space Identifier)와 PCID(Process-Context Identifier)가 전환 비용에 영향을 준다. ASID는 TLB 항목에 프로세스 ID 태그를 붙여 전환 때 플러시를 피하며 ARM과 MIPS에서 지원한다. x86-64의 PCID는 ASID와 유사하고 Intel Nehalem 이후 지원된다.
캐시 측면에서는 일부 항목을 유지하는 방식과 LRU(Least Recently Used) 정책, 프로세스별 캐시 영역을 나누는 캐시 파티셔닝으로 간섭을 줄일 수 있다.
Linux의 CFS(Completely Fair Scheduler)는 우선순위 기반으로 동작하며 Red-Black Tree로 Ready Queue를 관리한다. 커널의 전환 흐름은 메모리 관리 정보와 CPU 상태를 차례로 바꾼다.
// kernel/sched/core.c
void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)
{
// 메모리 관리 정보 전환
switch_mm(prev->mm, next->mm);
// CPU 상태 전환 (레지스터)
switch_to(prev, next);
}
전환 횟수는 다음 명령으로 확인할 수 있다.
vmstat 1 # 초당 Context Switch 횟수
# cs 컬럼 확인
Windows는 32단계 우선순위 기반 스케줄러를 사용하며 KiSwitchThread 함수와 Dispatcher가 전환을 수행한다. Performance Monitor의 Context Switches/sec 카운터로 측정할 수 있다. 실시간 OS는 결정적인 Context Switching 시간과 엄격한 우선순위 준수를 특징으로 하며, FreeRTOS는 수십 마이크로초 이내, VxWorks는 예측 가능한 시간을 제공한다.
전환 빈도와 캐시 지역성을 조절하는 선택지
Lazy Context Switching은 모든 프로세스가 FPU를 쓰지 않는다는 점을 이용한다. FPU, SIMD 레지스터를 필요할 때만 저장·복원하고, FPU 사용 시 발생한 예외를 핸들러가 처리하면서 FPU 레지스터를 복원한다. FPU를 사용하지 않는 프로세스에서 전환 시간을 줄일 수 있다.
CPU Affinity는 프로세스를 특정 CPU 코어에 고정해 캐시 지역성을 높인다.
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset); // CPU 0에 고정
sched_setaffinity(0, sizeof(cpuset), &cpuset);
캐시 미스가 줄어들면 Context Switching의 간접 비용도 낮아진다.
Hyper-Threading은 물리 코어 하나에 논리 코어 두 개를 두고 레지스터 세트를 다중화한다. 빠른 Context Switching과 유휴 실행 유닛 활용에 도움이 될 수 있지만, 실제 병렬 실행은 아니며 자원 경쟁이 발생할 수 있다.