# [09-03] 컨텍스트 스위칭 (Context Switching)

이용성·2026년 3월 31일
post-thumbnail

컨텍스트 스위칭은 CPU가 한 프로세스(또는 스레드)에서 다른 프로세스로 전환하는 과정입니다.


🎯 컨텍스트 스위칭이란 무엇인가

컨텍스트 스위칭 (Context Switching)의 정의

컨텍스트 스위칭은 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 완료 후)
"""

🔀 프로세스 vs 스레드 스위칭

비용 차이

프로세스 컨텍스트 스위칭:
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)

  • 동시성의 정의: 여러 작업을 번갈아 수행
  • 병렬성과의 차이: 동시 vs 동시에 보이는 것
  • 동시성의 장점: 반응성, 자원 활용
  • 동시성 프로그래밍: 코루틴, 비동기 프로그래밍

이전 글: [09-02] 스레드
다음 글: [09-04] 동시성
시리즈: P1. Computer Science

profile
AI 전문가를 꿈꾸는 도전자

0개의 댓글