01-4. Context Switch 詳解

⏱️ 閱讀時間: 12 分鐘 🎯 難度: ⭐⭐⭐ (中等)


🎯 本篇重點

理解 Context Switch(上下文切換)的原理、成本以及為什麼它很昂貴。


🤔 什麼是 Context Switch?

一句話解釋:

Context Switch 是作業系統將 CPU 從一個 Process 切換到另一個 Process 的過程,需要保存當前 Process 的狀態並載入新 Process 的狀態。


🎭 用演員換場來比喻

舞台 = CPU

舞台上正在演出:
  演員 A(Process A)正在表演
    ↓
  導演(作業系統): "換人!"
    ↓
  1. 記錄演員 A 的進度(保存 Context)
  2. 演員 A 下台(Process A 離開 CPU)
  3. 演員 B 上台(Process B 獲得 CPU)
  4. 恢復演員 B 的進度(載入 Context)
    ↓
  演員 B 繼續表演(Process B 執行)

這整個過程就是 Context Switch!


📦 什麼是 Context(上下文)?

Process 的 Context 包含:

Process Context(進程上下文)
├─ CPU 暫存器狀態
│  ├─ Program Counter (PC):下一條要執行的指令位址
│  ├─ Stack Pointer (SP):堆疊頂端位址
│  ├─ General Registers:通用暫存器的值
│  └─ Status Register:狀態旗標
│
├─ 記憶體管理資訊
│  ├─ Page Table:虛擬記憶體映射表
│  └─ Memory Limits:記憶體邊界
│
├─ I/O 狀態
│  ├─ 開啟的檔案清單
│  ├─ 網路連接
│  └─ 裝置狀態
│
└─ Process 狀態
   ├─ Process ID (PID)
   ├─ Priority(優先級)
   └─ Scheduling Information(調度資訊)

這些資訊存放在:Process Control Block (PCB)


🔄 Context Switch 完整流程

步驟分解

時間 t0: Process A 執行中
  │
  ├─ 步驟 1:觸發 Context Switch
  │  • Time Slice 用完
  │  • Process A 發起 I/O 請求
  │  • 高優先級 Process B 就緒
  │
  ├─ 步驟 2:保存 Process A 的 Context
  │  • 保存所有 CPU 暫存器
  │  • 保存 PC、SP
  │  • 保存狀態旗標
  │  • 更新 PCB_A
  │
  ├─ 步驟 3:選擇下一個 Process
  │  • 調度器選擇 Process B
  │  • Process B 狀態:Ready → Running
  │
  ├─ 步驟 4:載入 Process B 的 Context
  │  • 從 PCB_B 讀取狀態
  │  • 恢復所有暫存器
  │  • 切換記憶體映射(Page Table)
  │  • 恢復 PC、SP
  │
  └─ 步驟 5:Process B 開始執行
     • CPU 跳轉到 Process B 的程式碼
     • Process B 繼續執行

時間 t1: Process B 執行中

🔍 實際案例

案例 1:Time Slice 用完

import os
import time
from multiprocessing import Process

def task_a():
    pid = os.getpid()
    for i in range(5):
        print(f"[Process A - {pid}] 執行中...{i}")
        time.sleep(0.1)  # 模擬工作

def task_b():
    pid = os.getpid()
    for i in range(5):
        print(f"[Process B - {pid}] 執行中...{i}")
        time.sleep(0.1)

if __name__ == '__main__':
    p1 = Process(target=task_a)
    p2 = Process(target=task_b)

    p1.start()
    p2.start()

    p1.join()
    p2.join()

輸出(交錯執行):

[Process A - 1234] 執行中...0
[Process B - 1235] 執行中...0
[Process A - 1234] 執行中...1  ← Context Switch
[Process B - 1235] 執行中...1  ← Context Switch
[Process A - 1234] 執行中...2
[Process B - 1235] 執行中...2
...

每次切換都發生了 Context Switch!


案例 2:I/O 阻塞

import os
import time

def cpu_bound_task():
    """CPU 密集任務"""
    pid = os.getpid()
    print(f"[CPU Task - {pid}] 開始計算...")
    for i in range(10000000):
        _ = i * i
    print(f"[CPU Task - {pid}] 計算完成")

def io_bound_task():
    """I/O 密集任務"""
    pid = os.getpid()
    print(f"[I/O Task - {pid}] 開始讀取檔案...")
    time.sleep(2)  # 模擬檔案 I/O
    print(f"[I/O Task - {pid}] 讀取完成")

# I/O Task 在等待時,CPU Task 可以執行
# 這需要 Context Switch

時間軸:

0ms    - 10ms  : I/O Task Running  → 發起 I/O 請求
10ms   - 12ms  : Context Switch (I/O Task → CPU Task)
12ms   - 2000ms: CPU Task Running  (I/O Task 在 Waiting)
2000ms - 2002ms: I/O 完成,Context Switch (CPU Task → I/O Task)
2002ms - 2010ms: I/O Task Running  → 繼續執行

💰 Context Switch 的成本

為什麼 Context Switch 很昂貴?

1. 直接成本:保存/載入 Context

操作                          時間成本
─────────────────────────────────────
保存 CPU 暫存器               ~10 個時鐘週期
保存 PC、SP、Flags            ~5 個時鐘週期
更新 PCB                      ~20 個時鐘週期
載入新 PCB                    ~20 個時鐘週期
切換 Page Table               ~100 個時鐘週期
載入新 Context                ~15 個時鐘週期
─────────────────────────────────────
總計                          ~170 個時鐘週期

在 2.5 GHz CPU 上:

170 個時鐘週期 = 170 / 2,500,000,000 = 68 奈秒 (ns)

看起來很快?但是…


2. 間接成本:Cache 失效

這才是最大的成本!

Process A 執行時:
  CPU Cache 載入了 Process A 的資料
    ↓
Context Switch 到 Process B
    ↓
  CPU Cache 全部失效(Cache Miss)
    ↓
  需要從記憶體重新載入 Process B 的資料
    ↓
  記憶體存取:~100 奈秒 × 數千次 = 數十微秒 (μs)

Cache Miss 的影響:

操作時間
CPU 暫存器存取~1 ns
L1 Cache~1 ns
L2 Cache~3 ns
L3 Cache~10 ns
主記憶體 (RAM)~100 ns
SSD~50,000 ns

Cache Miss 造成效能下降 100 倍!


3. TLB 失效

TLB (Translation Lookaside Buffer): 虛擬記憶體位址轉換的快取

Process A:
  虛擬位址 0x1000 → 實體位址 0xABCD  (存在 TLB)

Context Switch 到 Process B
  ↓
TLB 失效
  ↓
Process B:
  虛擬位址 0x1000 → 需要查 Page Table → 很慢!

完整成本估算

直接成本(保存/載入)           ~68 ns
Cache 失效重新載入             ~50,000 ns
TLB 失效                      ~10,000 ns
調度器開銷                     ~1,000 ns
─────────────────────────────────────
總計                          ~61,068 ns ≈ 61 微秒 (μs)

這意味著:

  • 每次 Context Switch ≈ 61 微秒
  • 每秒 1000 次 Context Switch = 61 毫秒的純開銷
  • 佔用約 6% CPU 時間(在 1 秒鐘內)

⚠️ Context Switch 頻繁的影響

場景:高並發系統

# 啟動 1000 個 Process
from multiprocessing import Process

def task():
    for i in range(100):
        _ = i * i

processes = [Process(target=task) for _ in range(1000)]

for p in processes:
    p.start()

for p in processes:
    p.join()

問題:

  • 1000 個 Process 爭搶 CPU(假設 4 核)
  • 每個 Process 只能短暫執行(Time Slice ≈ 10ms)
  • 頻繁的 Context Switch

結果:

Context Switch 次數:~100,000 次/秒
純 Context Switch 開銷:100,000 × 61μs = 6.1 秒
實際工作時間:1 秒
總時間:7.1 秒

效率:1 / 7.1 = 14%  ← 大部分時間在切換!

🎯 如何減少 Context Switch?

方法 1:減少 Process/Thread 數量

# ❌ 過多的 Process
processes = [Process(target=task) for _ in range(1000)]

# ✅ 使用 Process Pool
from multiprocessing import Pool

with Pool(processes=4) as pool:  # 只創建 4 個 Process
    pool.map(task, range(1000))

方法 2:使用協程(Coroutine)

# ❌ 多個 Process(有 Context Switch)
from multiprocessing import Process

def io_task():
    response = requests.get('https://api.example.com')

processes = [Process(target=io_task) for _ in range(100)]

# ✅ 使用 asyncio(沒有 Context Switch)
import asyncio
import aiohttp

async def io_task():
    async with aiohttp.ClientSession() as session:
        async with session.get('https://api.example.com') as response:
            return await response.text()

async def main():
    tasks = [io_task() for _ in range(100)]
    await asyncio.gather(*tasks)

# 單個 Process,無 Context Switch!

方法 3:批次處理

# ❌ 每個請求一個 Process
for item in items:
    p = Process(target=process_item, args=(item,))
    p.start()

# ✅ 批次處理
def process_batch(batch):
    for item in batch:
        process_item(item)

# 每 100 個 item 一個 Process
batch_size = 100
for i in range(0, len(items), batch_size):
    batch = items[i:i+batch_size]
    p = Process(target=process_batch, args=(batch,))
    p.start()

📊 Context Switch 監控

Linux 查看 Context Switch 次數

# 查看系統整體 Context Switch
vmstat 1

# 輸出:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0 1234567    123   4567    0    0    12    34  123 5678 10  5 85  0  0
                                                      cs = Context Switch (每秒)

Python 監控

import psutil
import time

def monitor_context_switches():
    # 獲取初始值
    ctx_start = psutil.cpu_stats().ctx_switches

    time.sleep(1)

    # 獲取 1 秒後的值
    ctx_end = psutil.cpu_stats().ctx_switches

    cs_per_second = ctx_end - ctx_start
    print(f"Context Switch: {cs_per_second} 次/秒")

monitor_context_switches()

✅ 重點回顧

Context Switch 的本質:

  • 保存當前 Process 的 Context
  • 載入新 Process 的 Context
  • 切換 CPU 執行

Context Switch 的成本:

  1. 直接成本:保存/載入暫存器 (~68 ns)
  2. 間接成本:Cache/TLB 失效 (~61 μs) ← 主要成本
  3. 調度開銷:選擇下一個 Process (~1 μs)

為什麼昂貴:

  • ✅ Cache 失效是最大成本
  • ✅ TLB 失效導致記憶體存取變慢
  • ✅ 頻繁切換浪費大量 CPU 時間

如何優化:

  • ✅ 減少 Process/Thread 數量
  • ✅ 使用協程(asyncio)替代多進程
  • ✅ 批次處理減少切換次數
  • ✅ 調整調度策略

上一篇: 01-3. Process 的生命週期 下一篇: 01-5. Process vs Thread 完整對比


最後更新:2025-01-04

0%