
컨텍스트 스위칭은 CPU가 한 프로세스(또는 스레드)에서 다른 프로세스로 전환하는 과정입니다.
컨텍스트 스위칭은 CPU가 현재 실행 중인 프로세스를 멈추고 다른 프로세스를 실행하는 과정입니다.
쉬운 비유:
상황: 여러 권의 책을 번갈아 읽기
책 A 읽기:
1. 책 A 펼치기
2. 20페이지까지 읽음
3. 책갈피 꽂기 (20페이지)
4. 책 닫기
책 B로 전환:
5. 책 B 펼치기
6. 책갈피 위치 확인 (35페이지)
7. 35페이지부터 읽기
컨텍스트 스위칭:
- 책갈피 = 컨텍스트 (어디까지 읽었는지)
- 책 바꾸기 = 스위칭
- 책갈피 없으면 = 처음부터 다시 읽어야 함!
컨텍스트(Context)는 프로세스의 현재 상태입니다.
컨텍스트 포함 내용:
CPU 레지스터:
- PC (Program Counter): 다음 실행할 명령어 주소
- SP (Stack Pointer): 스택 최상단 위치
- 범용 레지스터: 계산 중인 값들
프로세스 상태:
- 실행(Running), 준비(Ready), 대기(Waiting)
메모리 정보:
- 코드 영역 위치
- 데이터 영역 위치
- 스택, 힙 위치
열린 파일:
- 파일 디스크립터(File Descriptor, FD) 목록
* 프로세스가 파일에 접근할 때 사용하는 음수가 아닌 추상적인 정수값
기타:
- 우선순위
- 실행 시간 등
즉:
프로세스를 "재개"하는데 필요한 모든 정보!
컴퓨터는 동시에 여러 프로그램이 실행되는 것처럼 보입니다. 하지만 CPU 코어는 한 번에 하나의 작업만 수행할 수 있습니다.
CPU 코어 1개:
실제로는 한 번에 하나만 실행
하지만 사용자는:
- 음악 듣기 (음악 플레이어)
- 웹 브라우징 (브라우저)
- 문서 작성 (에디터)
→ 동시에 실행되는 것처럼 보임!
비밀:
매우 빠르게 전환!
→ 컨텍스트 스위칭
시간 분할 (Time Sharing)
타임라인 (매우 단순화):
0ms: [프로세스 A 실행]
20ms: 컨텍스트 스위칭 → B
20ms: [프로세스 B 실행]
40ms: 컨텍스트 스위칭 → C
40ms: [프로세스 C 실행]
60ms: 컨텍스트 스위칭 → A
60ms: [프로세스 A 실행]
...
각 프로세스:
- 짧은 시간 동안 실행 (타임 슬라이스)
- CPU 양보
- 다시 자기 차례 올 때까지 대기
사용자 입장:
너무 빨라서 "동시에" 실행되는 것처럼 보임!
초기 상태:
프로세스 A 실행 중
1단계: 현재 프로세스 상태 저장
┌─────────────────────────┐
│ CPU 레지스터 값 저장 │
│ → PCB_A에 기록 │
│ - PC = 0x1000 │
│ - SP = 0x2000 │
│ - 기타 레지스터... │
└─────────────────────────┘
2단계: 다음 프로세스 선택
┌─────────────────────────┐
│ 스케줄러 호출 │
│ → 준비 큐 확인 │
│ → 프로세스 B 선택 │
└─────────────────────────┘
3단계: 다음 프로세스 상태 복원
┌─────────────────────────┐
│ PCB_B에서 읽기 │
│ → CPU 레지스터 복원 │
│ - PC = 0x3000 │
│ - SP = 0x4000 │
│ - 기타 레지스터... │
└─────────────────────────┘
4단계: 실행 재개
┌─────────────────────────┐
│ 프로세스 B 실행 │
│ (PC가 가리키는 곳부터) │
└─────────────────────────┘
결과:
프로세스 A → 프로세스 B 전환 완료!
def context_switch_simulation():
"""
컨텍스트 스위칭 시뮬레이션 (개념적)
실제 OS 커널 코드는 아니지만 개념을 이해하기 위한 의사 코드
"""
# ===== 1단계: 인터럽트 발생 =====
# 타이머 인터럽트, I/O 완료, 시스템 콜 등
def timer_interrupt():
"""
타이머 인터럽트: 일정 시간마다 발생
예: 10ms마다 → "시간 다 썼어! 다음 프로세스 차례!"
"""
pass
# ===== 2단계: 현재 프로세스 컨텍스트 저장 =====
def save_context(process):
"""
현재 실행 중인 프로세스의 컨텍스트 저장
PCB (Process Control Block)에 저장:
- CPU 레지스터 값들
- 프로그램 카운터 (다음 실행할 명령어)
- 스택 포인터
- 프로세스 상태
"""
pcb = process.pcb
# CPU 레지스터 → PCB로 복사
pcb.program_counter = get_cpu_register("PC")
pcb.stack_pointer = get_cpu_register("SP")
pcb.registers = get_all_cpu_registers()
# 프로세스 상태 변경
pcb.state = "READY" # 실행(RUNNING) → 준비(READY)
print(f"[저장] {process.name} 컨텍스트 저장")
print(f" PC: {pcb.program_counter}")
print(f" SP: {pcb.stack_pointer}")
# ===== 3단계: 다음 프로세스 선택 (스케줄링) =====
def scheduler():
"""
스케줄러: 다음 실행할 프로세스 선택
방법:
- Round Robin: 순서대로
- Priority: 우선순위 높은 것
- SJF: 짧은 작업 먼저 등등... (다음 글에서!)
"""
ready_queue = get_ready_processes()
# 간단히 첫 번째 선택
next_process = ready_queue[0]
print(f"[스케줄러] {next_process.name} 선택")
return next_process
# ===== 4단계: 다음 프로세스 컨텍스트 복원 =====
def restore_context(process):
"""
선택된 프로세스의 컨텍스트 복원
PCB에서 CPU 레지스터로 복사
"""
pcb = process.pcb
# PCB → CPU 레지스터로 복사
set_cpu_register("PC", pcb.program_counter)
set_cpu_register("SP", pcb.stack_pointer)
set_all_cpu_registers(pcb.registers)
# 프로세스 상태 변경
pcb.state = "RUNNING" # 준비(READY) → 실행(RUNNING)
print(f"[복원] {process.name} 컨텍스트 복원")
print(f" PC: {pcb.program_counter}")
print(f" SP: {pcb.stack_pointer}")
# ===== 5단계: 실행 재개 =====
def resume_execution(process):
"""
프로세스 실행 재개
PC가 가리키는 명령어부터 실행
마치 멈춘 적이 없는 것처럼!
"""
print(f"[실행] {process.name} 재개\n")
# CPU가 명령어 실행...
# ===== 전체 흐름 =====
print("=== 컨텍스트 스위칭 시작 ===\n")
current_process = get_current_process() # 프로세스 A
# 1. 타이머 인터럽트
timer_interrupt()
# 2. 현재 프로세스 저장
save_context(current_process)
# 3. 다음 프로세스 선택
next_process = scheduler()
# 4. 다음 프로세스 복원
restore_context(next_process)
# 5. 실행 재개
resume_execution(next_process)
print("=== 컨텍스트 스위칭 완료 ===")
import time
def program_A():
"""
프로그램 A
실행 중간에 컨텍스트 스위칭 발생
"""
print("프로그램 A 시작")
for i in range(5):
print(f"A: 카운트 {i}")
time.sleep(0.1) # 0.1초 대기
# 이 시점에 컨텍스트 스위칭 가능!
# OS가 프로그램 B로 전환할 수 있음
print("프로그램 A 종료")
def program_B():
"""
프로그램 B
"""
print("프로그램 B 시작")
for i in range(5):
print(f"B: 카운트 {i}")
time.sleep(0.1)
print("프로그램 B 종료")
# 실제로는 OS가 이렇게 전환:
"""
시간 | 실행 중인 프로그램
------|------------------
0ms | A: 카운트 0
10ms | 컨텍스트 스위칭 → B
10ms | B: 카운트 0
20ms | 컨텍스트 스위칭 → A
20ms | A: 카운트 1
30ms | 컨텍스트 스위칭 → B
30ms | B: 카운트 1
...
두 프로그램 모두:
- 자기가 멈춘 줄 모름
- 계속 실행되는 것처럼 느낌
- 변수 값도 그대로 유지
"""
1. 타임 슬라이스 만료
→ 할당된 시간 다 씀
→ 다른 프로세스 차례
2. I/O 요청
→ 파일 읽기, 네트워크 대기 등
→ CPU 놀고 있으니 다른 프로세스 실행
3. 시스템 콜
→ sleep(), wait() 등
→ 명시적으로 CPU 양보
4. 인터럽트(Interrupt) * 현재 프로세스 일시 중단, 긴급 요청 우선 처리, 원래 프로세스로 복귀
→ 하드웨어 신호 (키보드, 마우스 등)
5. 프로세스 종료
→ 다음 프로세스 실행
예시: I/O 대기
import time
def io_bound_process():
"""
I/O 바운드 프로세스
파일 읽기 중에 컨텍스트 스위칭 발생
"""
print("프로세스 시작")
# CPU 작업
result = 100 + 200
print(f"계산 결과: {result}")
# I/O 작업: 파일 읽기
print("파일 읽기 시작...")
# 실제로는: open(), read() 시스템 콜
# → 디스크 I/O 대기
# → 이 시점에 컨텍스트 스위칭!
# → 다른 프로세스 실행
with open('data.txt', 'r') as f:
data = f.read()
# I/O 완료 후 재개
print("파일 읽기 완료")
print(f"데이터 크기: {len(data)}")
# OS 입장:
"""
1. CPU 작업 실행 (계산)
2. I/O 요청 감지 (파일 읽기)
3. 컨텍스트 스위칭 → 다른 프로세스
4. I/O 완료 인터럽트
5. 컨텍스트 스위칭 → 원래 프로세스
6. 계속 실행 (I/O 완료 후)
"""
프로세스 컨텍스트 스위칭:
1. PCB 저장/복원
2. 메모리 맵 전환 (가상 메모리)
3. 캐시/TLB 무효화
4. 많은 데이터 복사
→ 느림 (수 마이크로초)
스레드 컨텍스트 스위칭:
1. 스레드 정보만 저장/복원
2. 메모리 맵 그대로 (같은 프로세스)
3. 캐시 유지
4. 적은 데이터 복사
→ 빠름 (수백 나노초)
차이:
프로세스 스위칭이 스레드보다 10~100배 느림!
┌──────────────┬─────────────┬─────────────┐
│ 구 분 │ 프로세스 │ 스레드 │
├──────────────┼─────────────┼─────────────┤
│ 저장 데이터 │ 많음 │ 적음 │
│ 메모리 맵 │ 전환 │ 유지 │
│ 캐시 │ 무효화 │ 유지 │
│ 속도 │ 느림 │ 빠름 │
│ 시간 │ 수 μs │ 수백 ns │
└──────────────┴─────────────┴─────────────┘
μs = 마이크로초 (10⁻⁶초)
ns = 나노초 (10⁻⁹초)
오버헤드(Overhead)는 컨텍스트 스위칭 자체에 소요되는 비용입니다.
순수 작업 시간: 실제 프로그램 실행
오버헤드: 컨텍스트 스위칭 시간
전체 시간 = 순수 작업 + 오버헤드
오버헤드가 크면:
→ 실제 작업 시간 감소
→ 성능 저하
예시:
프로세스 A: 100ms 작업
컨텍스트 스위칭: 1ms
타임 슬라이스 10ms:
10ms 작업 → 1ms 스위칭 → 10ms 작업 → 1ms 스위칭...
100ms 작업에 10번 스위칭
→ 총 110ms (10% 오버헤드)
타임 슬라이스 1ms:
1ms 작업 → 1ms 스위칭 → 1ms 작업 → 1ms 스위칭...
100ms 작업에 100번 스위칭
→ 총 200ms (50% 오버헤드!)
결론:
타임 슬라이스가 너무 짧으면 오버헤드 증가!
def context_switch_optimization():
"""
컨텍스트 스위칭 최적화 전략
"""
# 1. 타임 슬라이스 조정
"""
너무 짧으면: 오버헤드 증가
너무 길면: 반응성 저하
일반적: 10~100ms
실시간 시스템: 1~10ms
"""
# 2. 스레드 사용
"""
프로세스 대신 스레드:
→ 스위칭 비용 감소
→ 10~100배 빠름
"""
# 3. 프로세스 수 제한
"""
너무 많은 프로세스:
→ 스위칭 빈도 증가
→ 오버헤드 증가
적절한 수:
CPU 코어 수 × 1~2
"""
# 4. CPU 친화도 (Affinity)
"""
프로세스를 특정 CPU 코어에 고정:
→ 캐시 효율 증가
→ 스위칭 비용 감소
"""
import os
import time
def measure_context_switches():
"""
컨텍스트 스위칭 횟수 측정
Linux: /proc/[pid]/status
"""
pid = os.getpid()
# 초기 값
with open(f'/proc/{pid}/status', 'r') as f:
for line in f:
if 'voluntary_ctxt_switches' in line:
start_vol = int(line.split()[1])
if 'nonvoluntary_ctxt_switches' in line:
start_nonvol = int(line.split()[1])
# 작업 수행
time.sleep(1) # 프로그램을 1초 동안 멈추라는 의미
# 최종 값
with open(f'/proc/{pid}/status', 'r') as f:
for line in f:
# voluntary_ctxt_switches: 자발적 문맥 교환 횟수를 찾아 end_vol 에 저장
if 'voluntary_ctxt_switches' in line:
end_vol = int(line.split()[1])
# nonvoluntary_ctxt_switches: 비자발적 문맥 교환 횟수를 찾아 end_nonvol 에 저장
if 'nonvoluntary_ctxt_switches' in line:
end_nonvol = int(line.split()[1])
print(f"자발적 스위칭: {end_vol - start_vol}")
print(f"비자발적 스위칭: {end_nonvol - start_nonvol}")
"""
자발적 (Voluntary): 프로세스가 스스로 CPU 양보
- I/O 대기, sleep() 등
비자발적 (Involuntary): OS가 강제로 전환
- 타임 슬라이스 만료, 우선순위가 더 높은 작업이 나타났을 때
"""
# Linux에서만 동작
try:
measure_context_switches()
except:
print("Linux에서만 측정 가능")
def performance_considerations():
"""
컨텍스트 스위칭 성능 고려
"""
# 나쁜 예: 너무 많은 프로세스
import multiprocessing
def bad_example():
"""
프로세스 1000개 생성
→ 과도한 컨텍스트 스위칭
→ 오버헤드 증가
"""
processes = []
for i in range(1000): # 너무 많음!
p = multiprocessing.Process(target=worker)
p.start()
processes.append(p)
# 좋은 예: CPU 코어 수만큼
def good_example():
"""
CPU 코어 수에 맞춰 프로세스 생성
→ 적절한 병렬성
→ 오버헤드 최소화
"""
cpu_count = os.cpu_count()
pool = multiprocessing.Pool(cpu_count)
# Pool이 알아서 작업 분배
# 최선: 스레드 사용 (I/O 바운드)
import threading
def best_for_io():
"""
I/O 바운드 작업은 스레드
→ 컨텍스트 스위칭 비용 적음
→ 메모리 공유로 효율적
"""
threads = []
for i in range(100): # 많아도 OK
t = threading.Thread(target=io_worker)
t.start()
threads.append(t)
컨텍스트 스위칭
정의:
CPU가 현재 프로세스를 멈추고 다른 프로세스를 실행하는 과정
목적:
멀티태스킹 구현 → 여러 프로그램이 동시에 실행되는 것처럼
동작 과정
1. 현재 프로세스 컨텍스트 저장 → PCB에 CPU 레지스터 값 저장
2. 다음 프로세스 선택 → 스케줄러 호출
3. 다음 프로세스 컨텍스트 복원 → PCB에서 CPU 레지스터로 복사
4. 실행 재개 → 멈춘 곳부터 계속
발생 시점
- 타임 슬라이스 만료
- I/O 요청
- 시스템 콜 (sleep, wait)
- 인터럽트
- 프로세스 종료
성능 영향
오버헤드:
프로세스: 수 마이크로초 (느림)
스레드: 수백 나노초 (빠름)
최적화:
- 적절한 타임 슬라이스
- 스레드 사용
- 프로세스 수 제한
- CPU 친화도
프로세스 vs 스레드
프로세스 스위칭:
- 메모리 맵 전환
- 캐시 무효화
- 비용 큼
스레드 스위칭:
- 메모리 공유
- 캐시 유지
- 비용 적음 (10~100배 빠름)
[09-04] 동시성 (Concurrency)
이전 글: [09-02] 스레드
다음 글: [09-04] 동시성
시리즈: P1. Computer Science