컨텍스트 스위칭과 운영체제의 프로세스 전환 비용

컨텍스트 스위칭에서 PCB에 저장하는 실행 상태, 프로세스·스레드 전환 비용, TLB·캐시 오버헤드와 CPU Affinity 최적화를 정리한다.

2026-08-14 · 최초 발행 2026-01-06

실행 상태를 넘겨받는 커널 작업

컨텍스트 스위칭은 CPU가 실행 중인 프로세스를 멈추고 다른 프로세스를 실행할 때, 현재 상태를 PCB에 보관한 뒤 다음 프로세스의 상태를 되살리는 과정이다. 멀티태스킹 환경에서는 피할 수 없는 동작이며, CPU 활용률·응답성·자원 배분 방식에 직접 영향을 준다.

여기서 context는 단순히 범용 레지스터 값만 뜻하지 않는다. CPU 레지스터, 메모리 관리 정보, I/O 상태처럼 실행을 이어가기 위해 필요한 정보 전체가 대상이며 PCB(Process Control Block)에 저장된다. 스케줄러는 다음 실행 대상을 고르고, dispatcher가 실제 전환을 수행한다.

전환은 선점형 스케줄링에서 타임 슬라이스가 끝나는 타이머 인터럽트, 디스크 읽기·쓰기처럼 Blocking I/O를 일으키는 요청, 더 높은 우선순위 프로세스가 Ready 상태가 되는 상황에서 발생한다. fork(), exec() 같은 시스템 콜로 커널 모드에 진입한 뒤에도 일부 경우 전환이 뒤따른다.

타이머I/O우선순위Process A실행전환트리거인터럽트대기선점상태 저장스케줄러상태 복원Process B실행

PCB에 남겨야 하는 상태

CPU 레지스터에는 연산 결과와 임시 데이터를 담는 범용 레지스터, 스택과 실행 위치를 가리키는 특수 레지스터, 세그먼트와 플래그 정보가 포함된다.

  • x86-32: EAX, EBX, ECX, EDX, ESI, EDI
  • x86-64: RAX, RBX, RCX, RDX, RSI, RDI
  • ARM: R0 ~ R15
  • Stack Pointer: ESP/RSP
  • Base Pointer: EBP/RBP
  • Instruction Pointer: EIP/RIP
  • x86 세그먼트 레지스터: CS, DS, SS, ES, FS, GS
  • 상태 플래그: EFLAGS/RFLAGS, Zero·Carry·Overflow·Sign 플래그, CPSR

프로그램 카운터는 다음에 실행할 명령어의 주소를 보관한다. x86에서는 32비트 EIP와 64비트 RIP, ARM에서는 PC(R15), MIPS에서는 PC라는 이름을 쓴다. 이 값이 보존되지 않으면 중단한 지점에서 실행을 다시 시작할 수 없다.

주소 공간 전환에 필요한 정보도 context에 포함된다. x86의 CR3(Control Register 3), ARM의 TTBR(Translation Table Base Register)은 가상 주소를 물리 주소로 변환하는 페이지 테이블을 가리킨다. x86 Legacy 환경에서는 GDT(Global Descriptor Table), LDT(Local Descriptor Table), 접근 권한과 세그먼트 한계도 함께 고려한다.

PCB는 Running·Ready·Blocked·Zombie·Terminated 같은 프로세스 상태, 정적·동적 우선순위, 사용한 CPU 시간과 대기 시간도 관리한다.

저장한 뒤 복원되는 순서

인터럽트 또는 시스템 콜이 발생하면 CPU는 커널 모드로 들어간다. 커널은 현재 프로세스 상태를 PCB에 저장하고, 스케줄러가 다음 프로세스를 고른다. 선택된 PCB에서 상태를 복원한 뒤 사용자 모드로 돌아가 새 프로세스의 실행을 재개한다. 이 흐름은 다음 7단계로 표현할 수 있다.

  1. 인터럽트 또는 시스템 콜 발생
  2. 커널 모드로 전환
  3. 현재 프로세스 상태를 PCB에 저장
  4. 스케줄러가 다음 프로세스 선택
  5. 선택된 프로세스의 PCB에서 상태 복원
  6. 사용자 모드로 전환
  7. 복원된 프로세스 실행 재개

현재 task의 레지스터는 다음과 같은 방식으로 보관할 수 있다.

// 의사 코드
void save_context(struct task_struct *prev) {
    // 범용 레지스터 저장
    prev->thread.ax = read_register(AX);
    prev->thread.bx = read_register(BX);
    prev->thread.cx = read_register(CX);

    // 특수 레지스터 저장
    prev->thread.sp = read_register(SP);
    prev->thread.bp = read_register(BP);
    prev->thread.ip = read_register(IP);

    // 플래그 레지스터 저장
    prev->thread.flags = read_register(FLAGS);
}

페이지 테이블 베이스도 저장 대상이다.

// 페이지 테이블 베이스 저장
prev->mm->pgd = read_cr3();

FPU와 SIMD 상태는 Lazy 방식에서 실제 사용 중인 경우에만 저장한다.

// FPU 사용 중인 경우만
if (prev->flags & PF_USED_FPU) {
    save_fpu_state(&prev->thread.fpu);
}

다음 task로 넘어갈 때는 같은 순서의 역방향으로 상태를 복원한다.

void restore_context(struct task_struct *next) {
    // 범용 레지스터 복원
    write_register(AX, next->thread.ax);
    write_register(BX, next->thread.bx);
    write_register(CX, next->thread.cx);

    // 특수 레지스터 복원
    write_register(SP, next->thread.sp);
    write_register(BP, next->thread.bp);
    write_register(IP, next->thread.ip);

    // 플래그 레지스터 복원
    write_register(FLAGS, next->thread.flags);
}

주소 공간을 바꾸는 경우에는 페이지 테이블과 TLB도 처리해야 한다.

// 페이지 테이블 전환
write_cr3(next->mm->pgd);

// TLB 플러시 (아키텍처에 따라)
if (!has_pcid_support()) {
    flush_tlb();
}

IP를 복원한 뒤 사용자 공간으로 복귀하면 해당 위치에서 실행이 이어진다.

// IP 레지스터가 복원되어 자동으로 해당 주소에서 실행
// 커널 모드 → 사용자 모드 전환
return_to_userspace();
프로세스 A실행인터럽트커널 모드레지스터저장메모리 정보저장상태저장스케줄러호출상태복원메모리 정보복원레지스터복원사용자 모드프로세스 B실행

레지스터보다 오래 남는 전환 비용

직접 비용은 레지스터를 저장하고 복원하는 데서 시작한다. 범용 레지스터는 1020개, 특수 레지스터는 510개를 다루며 메모리 쓰기·읽기에는 수백 나노초가 든다. CR3 업데이트와 PCID 미지원 환경의 TLB 플러시도 수백 나노초, 사용자 모드에서 커널 모드를 거쳐 다시 사용자 모드로 돌아오는 권한 레벨 전환은 수십 나노초가 걸린다. 일반적인 직접 비용은 1~5 마이크로초이며 아키텍처와 구현에 따라 달라진다.

더 큰 부담은 전환 뒤에 나타나는 간접 비용이다. 이전 프로세스의 데이터가 L1/L2 캐시에서 밀려나면 새 프로세스는 Cold Cache 상태에서 데이터를 다시 적재해야 하고, 비용은 수십수백 마이크로초가 될 수 있다. TLB를 비운 뒤 새 항목을 채우기 위한 페이지 테이블 워킹도 수십 마이크로초의 비용을 낸다. 명령어 파이프라인 플러시와 분기 예측 실패에는 수수십 사이클이 소요된다.

간접 비용의 전체 범위는 수십수백 마이크로초다. 직접 비용의 10100배가 될 수 있으며 워크로드에 따른 차이도 크다.

Linux에서는 vmstatcs 컬럼으로 초당 Context Switch 횟수를 확인할 수 있다.

# vmstat으로 초당 Context Switch 횟수 확인
vmstat 1

# cs 컬럼이 Context Switch 횟수
# r: 실행 대기 중인 프로세스 수
# b: 블록된 프로세스 수

sched_yield()를 이용한 간단한 측정 코드도 작성할 수 있다.

#include <time.h>

struct timespec start, end;

// 측정 시작
clock_gettime(CLOCK_MONOTONIC, &start);

// Context Switch 유도
sched_yield();

// 측정 종료
clock_gettime(CLOCK_MONOTONIC, &end);

long ns = (end.tv_sec - start.tv_sec) * 1000000000L +
          (end.tv_nsec - start.tv_nsec);
printf("Context Switch: %ld ns\n", ns);

주소 공간이 바뀌는가에 따른 차이

프로세스 전환에서는 현재 프로세스의 레지스터를 저장한 뒤 페이지 테이블 베이스(CR3)를 전환하고, PCID를 지원하지 않으면 TLB를 플러시한 다음 다음 프로세스의 레지스터를 복원한다. 레지스터 처리, 페이지 테이블 전환, TLB 플러시, 캐시 무효화가 모두 비용에 포함되며 총 비용은 5~50 마이크로초다.

같은 프로세스 안에서 스레드를 전환할 때는 현재 스레드의 레지스터를 저장하고 다음 스레드의 레지스터를 복원한다. 메모리 공간을 공유하므로 페이지 테이블을 유지할 수 있고, TLB와 캐시 일부도 남는다. 전환 비용은 0.5~5 마이크로초이며 프로세스 전환의 약 10%다.

항목 프로세스 스레드
레지스터 저장/복원 필요 필요
페이지 테이블 전환 필요 (CR3 변경) 불필요 (공유)
TLB 플러시 필요 (PCID 미지원 시) 불필요
캐시 무효화 전체 부분
메모리 공간 독립 공유
전환 시간 5~50 μs 0.5~5 μs
속도 비율 1x 10x
ContextSwitching프로세스전환스레드전환레지스터저장/복원페이지 테이블전환TLB플러시캐시무효화높은 비용(5-50μs)레지스터저장/복원메모리 공간유지TLB유지캐시부분 유지낮은 비용(0.5-5μs)

TLB를 유지하기 위한 하드웨어 지원

일부 CPU는 여러 레지스터 세트를 하드웨어에 구현해 두고, Context Switching 때 레지스터 세트 포인터만 바꾼다. 이 방식에서는 메모리에 상태를 저장하고 복원할 필요가 없다. Intel Hyper-Threading의 논리 코어당 독립 레지스터, ARM FIQ의 전용 R8~R14, SPARC의 레지스터 윈도우가 예시다. 나노초 단위의 빠른 전환과 오버헤드 감소가 장점이지만, 하드웨어 비용이 늘고 레지스터 세트 수가 제한된다.

TLB를 매 전환마다 무효화하면 새 프로세스가 실행될 때 TLB 미스가 급증한다. 각 TLB 엔트리에 프로세스 ID 태그를 두면 플러시 없이 여러 프로세스의 TLB 항목을 공존시켜 히트율을 높일 수 있다.

ARM과 MIPS에서는 ASID(Address Space Identifier)가 TLB 엔트리에 816비트 프로세스 ID를 둔다. x86-64에서는 Nehalem 이후 PCID(Process-Context Identifier)를 지원하며, CR3 레지스터에 12비트 PCID를 넣어 TLB 플러시를 막는다. 이 방식은 TLB 미스를 5080% 줄이고 Context Switching 비용을 30~50% 줄인다.

// PCID 사용 예시 (Linux)
if (cpu_has_pcid) {
    // PCID 포함하여 CR3 설정
    write_cr3(pgd | (pcid << 12));
} else {
    // 기존 방식 (TLB 플러시)
    write_cr3(pgd);
}

전환 빈도와 상태 저장 범위를 다루는 방법

Lazy Context Switching은 FPU·SIMD 레지스터를 사용할 때만 저장하고 복원한다. 모든 프로세스가 FPU를 사용하는 것은 아니므로, FPU 사용 플래그를 지운 뒤 FPU 명령어가 예외를 발생시키면 예외 핸들러가 이전 프로세스의 FPU 상태를 저장하고 현재 프로세스의 상태를 복원한다. FPU를 사용하지 않는 프로세스 전환에서는 20~30% 빨라지며 전체 시스템 성능 향상으로 이어진다.

CPU Affinity는 프로세스를 특정 CPU 코어에 바인딩해 캐시 지역성을 높이고 Context Switching의 간접 비용을 낮춘다.

#define _GNU_SOURCE
#include <sched.h>

cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);  // CPU 0에 고정

// 현재 프로세스를 CPU 0에 바인딩
sched_setaffinity(0, sizeof(cpuset), &cpuset);

캐시 히트율은 10~30% 향상될 수 있고 메모리 접근 지연은 감소하며 성능 예측 가능성도 높아진다. 반면 코어 고정은 부하 불균형과 CPU 활용률 저하 위험을 함께 가진다.

긴 타임 슬라이스는 전환 빈도를 줄이지만 응답성과 트레이드오프가 있다. 스레드 풀은 생성·삭제 비용을 줄이기 위해 스레드를 재사용한다. 비동기 I/O는 Blocking I/O가 일으키는 전환을 피하기 위해 epoll, kqueue, IOCP를 활용하며, Cooperative Multitasking에서는 명시적으로 yield해 불필요한 선점을 줄인다.

운영체제가 전환을 다루는 방식

Linux의 CFS(Completely Fair Scheduler)는 Ready Queue를 Red-Black Tree로 관리하며 O(log N) 복잡도를 가진다. 스케줄링 경로에서는 현재 task와 다음 task가 다를 때 context_switch()를 호출한다.

// kernel/sched/core.c
asmlinkage __visible void __sched schedule(void)
{
    struct task_struct *prev, *next;

    prev = current;
    next = pick_next_task(rq, prev);

    if (prev != next) {
        context_switch(rq, prev, next);
    }
}

메모리 관리 전환은 다음처럼 다음 주소 공간의 페이지 테이블을 적재한다.

void switch_mm(struct mm_struct *prev, struct mm_struct *next)
{
    load_cr3(next->pgd);
}

/proc/interruptsperf는 Linux에서 전환을 관찰하는 수단이다.

# /proc/interrupts에서 Context Switch 확인
cat /proc/interrupts | grep LOC

# perf로 상세 분석
perf stat -e context-switches ./app

Windows는 32단계 우선순위 기반으로 라운드 로빈과 우선순위를 사용한다. KiSwapContext가 전환을 수행하고, Performance Monitor의 Context Switches/sec, Thread Context Switches/sec 카운터로 측정할 수 있다.

FreeRTOS는 우선순위 기반 선점형 스케줄링과 마이크로초 단위의 결정적 Context Switching 시간을 제공한다. VxWorks는 256단계 우선순위와 타임슬라이스 설정을 통해 예측 가능한 전환 시간을 다룬다. QNX는 마이크로커널 구조에서 빠른 IPC와 Context Switching, 실시간 보장을 제공한다.

컨텍스트 스위칭운영체제프로세스스레드스케줄링