컨텍스트 스위칭
CPU 코어 하나는 한 순간에 하나의 실행 흐름(프로세스나 스레드)만 실행합니다. 그런데 컴퓨터에는 훨씬 많은 작업이 동시에 돌고 있는 것처럼 보입니다. 운영체제가 CPU를 작업들 사이에서 번갈아 넘겨 주기 때문입니다. 이렇게 CPU가 실행하던 작업에서 다른 작업으로 바뀌는 일을 컨텍스트 스위칭(context switching, 문맥 교환)이라고 부릅니다. 작업이 나중에 멈춘 곳에서 이어서 실행되려면 그 작업의 실행 상태, 곧 프로그램 카운터와 레지스터 값 등을 어딘가에 저장해 두어야 합니다. 운영체제는 이 상태를 프로세스의 PCB(스레드는 보통 TCB라 부릅니다)에 저장하고, 다음에 실행할 작업의 상태를 복원합니다.
컨텍스트 스위칭은 멀티태스킹을 가능하게 하는 대신 비용이 듭니다. 전환하는 동안에는 사용자 작업이 실행되지 못하고, 전환 뒤에는 이전 작업 위주로 채워져 있던 캐시와 TLB가 새 작업에 맞지 않아 한동안 느려질 수 있습니다. 이 토픽에서는 상태를 저장하고 복원하는 과정, 시간 조각 길이에 따라 전환 비용의 비율이 달라지는 모습, 프로세스 사이 전환과 스레드 사이 전환의 차이를 봅니다.
핵심 개념
컨텍스트
작업을 나중에 이어서 실행하는 데 필요한 정보입니다. 프로그램 카운터, 스택 포인터를 포함한 CPU 레지스터 값과 프로세스 상태, 메모리 관리 정보 등이 PCB에 담깁니다.
핵심 개념
저장과 복원
전환할 때 운영체제는 지금 실행하던 작업의 레지스터 값을 그 작업의 PCB에 저장하고, 다음에 실행할 작업의 PCB에서 값을 꺼내 CPU에 채웁니다. 그러면 CPU는 그 작업이 멈췄던 곳부터 이어서 실행합니다.
핵심 개념
전환이 일어나는 때
할당된 시간이 끝났을 때, 입출력 같은 이벤트를 기다리며 스스로 CPU를 내놓을 때, 더 높은 우선순위의 작업이 준비되었을 때 일어납니다. 입출력 대기로 스스로 내놓는 경우는 비선점 스케줄링에서도 일어나고, 시간 만료와 우선순위 선점은 선점 스케줄링에서만 일어납니다.
핵심 개념
직접 비용과 간접 비용
직접 비용은 상태를 저장하고 복원하는 시간과 다음 작업을 고르는 스케줄러 시간입니다. 간접 비용은 전환 뒤 캐시와 TLB가 새 작업에 맞지 않아 생기는 성능 저하입니다.
핵심 개념
주소 공간 전환
다른 프로세스로 바꾸면 그 프로세스의 가상 주소 공간으로 바꿔야 해서 페이지 테이블 전환이 필요합니다. 같은 프로세스의 스레드는 주소 공간을 공유하므로 이 일이 없습니다.
핵심 개념
자발적 전환과 비자발적 전환
작업이 입출력 등 자원을 기다리며 CPU를 내놓은 전환이 자발적, 시간 조각이 끝나거나 더 높은 우선순위 작업에 밀려난 전환이 비자발적입니다. 리눅스는 이 둘의 횟수를 따로 세어 보여 줍니다.
컨텍스트 스위칭의 배경
여러 프로그램을 동시에 올려 두고 CPU를 번갈아 쓰게 하는 다중 프로그래밍과 시분할 시스템은 한 CPU로 여러 작업을 진행하는 것처럼 보이게 합니다. 이를 가능하게 하는 기본 동작이 컨텍스트 스위칭입니다. 전환이 얼마나 빠른가는 하드웨어가 레지스터를 저장하고 복원하는 방식과 운영체제의 구현에 달려 있습니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
컨텍스트 스위칭이 무엇인가요?
CPU가 실행하던 작업에서 다른 작업으로 바뀌는 일입니다. 실행하던 작업의 상태(PC, 레지스터 등)를 PCB에 저장하고 다음 작업의 상태를 복원해 이어서 실행합니다.
면접 질문
왜 오버헤드가 생기나요?
전환 중에는 사용자 작업이 실행되지 못하고, 전환 뒤에는 캐시와 TLB의 내용이 새 작업에 맞지 않아 한동안 성능이 떨어지기 때문입니다.
면접 질문
프로세스 전환과 스레드 전환의 차이는?
같은 프로세스의 스레드는 주소 공간을 공유해 페이지 테이블 전환과 그에 따른 TLB 처리가 필요 없어서 보통 더 가볍습니다.
면접 질문
컨텍스트 스위칭을 줄이려면?
코어 수에 맞춰 스레드 수를 제한하고(스레드 풀), 불필요한 락 경합과 잦은 블로킹을 줄이며, 시간 조각을 지나치게 짧게 잡지 않는 방법이 있습니다.
실제 시스템에서는
함께 보면 좋은 토픽
참고 자료
개념 코드 — 상태 저장과 복원 (단순화)
// 실제 커널은 이 부분을 어셈블리로 구현한다. 개념만 보여 주는 단순화한 코드. struct pcb { unsigned long pc; // 프로그램 카운터 unsigned long sp; // 스택 포인터 unsigned long regs[16]; // 범용 레지스터 int state; // READY, RUNNING, WAITING ... }; void context_switch(struct pcb *prev, struct pcb *next) { save_cpu_state(prev); // CPU 레지스터 → prev의 PCB prev->state = READY; // 선점된 경우. 입출력 대기로 내놓으면 WAITING switch_address_space(next); // 다른 프로세스일 때만 페이지 테이블 교체 next->state = RUNNING; load_cpu_state(next); // next의 PCB → CPU 레지스터, 이후 next가 이어서 실행 }
전환의 핵심은 두 동작입니다. 지금 CPU에 있는 레지스터 값을 prev의 PCB에 저장하고, next의 PCB에 있던 값을 CPU에 채워 넣으면 next가 멈췄던 곳부터 이어서 실행됩니다. 주소 공간 전환은 다른 프로세스로 바꿀 때만 일어나고, 같은 프로세스의 스레드끼리 바꿀 때는 건너뜁니다. 이 차이가 스레드 전환이 보통 더 가벼운 이유입니다. 위 코드는 이해를 위한 개념도이며, 실제 운영체제는 아키텍처마다 저장해야 할 레지스터 집합과 전환 순서가 다르고 이 구간을 어셈블리로 작성합니다.
리눅스 — 컨텍스트 스위치 횟수 확인
# 시스템 전체: 1초마다 출력, cs 열이 초당 컨텍스트 스위치 수 vmstat 1 # 스레드 단위 누적 횟수 (PID는 메인 스레드) grep ctxt /proc/1234/status # voluntary_ctxt_switches: 120 (스스로 CPU를 내놓은 전환) # nonvoluntary_ctxt_switches: 35 (시간이 끝나거나 우선순위 높은 작업에 선점된 전환)
vmstat의 cs 열은 시스템 전체에서 초당 일어난 컨텍스트 스위치 수입니다. 값이 평소보다 크게 늘었다면 실행 가능한 스레드가 코어보다 많거나 락 경합으로 블로킹이 잦은지 의심해 볼 수 있습니다. /proc/PID/status의 두 값은 PID가 가리키는 메인 스레드의 누적 횟수이고 스레드별 값은 /proc/PID/task/TID/status에서 보며, 자발적 전환이 많으면 입출력이나 락 대기가 많다는 뜻이며 비자발적 전환이 많으면 CPU를 두고 다른 작업과 경쟁한다는 신호입니다. 위 숫자는 설명을 위한 예이고 PID와 값은 시스템마다 다릅니다.
C · Python — 내 프로세스의 전환 횟수
/* C: getrusage로 자발적/비자발적 전환 횟수 */ #include <sys/resource.h> struct rusage ru; getrusage(RUSAGE_SELF, &ru); printf("voluntary=%ld involuntary=%ld\n", ru.ru_nvcsw, ru.ru_nivcsw); # Python: 같은 값을 resource 모듈로 import resource ru = resource.getrusage(resource.RUSAGE_SELF) print(ru.ru_nvcsw, ru.ru_nivcsw)
getrusage는 프로세스가 지금까지 겪은 컨텍스트 스위치 횟수를 돌려줍니다. ru_nvcsw는 자발적 전환, ru_nivcsw는 비자발적 전환의 횟수입니다. 작업 전후에 값을 한 번씩 읽어 차이를 보면 그 작업 중 몇 번 전환되었는지 알 수 있어, 스레드 수를 바꿔 가며 비교하는 실험에 쓸 수 있습니다. 이 두 필드는 리눅스 같은 유닉스 계열에서 제공되며, 모든 플랫폼에서 의미 있는 값이 채워지는 것은 아닙니다.
Q1
컨텍스트 스위칭 때 운영체제가 저장하고 복원하는 것은?
Q2
컨텍스트 스위칭이 성능 오버헤드가 되는 이유는?
Q3
같은 프로세스 안의 스레드 간 전환이 서로 다른 프로세스 간 전환보다 보통 가벼운 이유는?