Learning Objectives
- 區分程式與行程,說出核心要為一個行程記住哪些資料
- 說明五種行程狀態與它們之間的轉換,以及 ready 和 blocked 的差別
- 舉出行程會被阻塞的原因,並說明阻塞的行程不佔用 CPU
- 說明內容切換保存了什麼,代價在哪裡
同一支程式開兩次,就是兩個行程
開兩個終端機視窗,或把同一個瀏覽器開兩次,磁碟上的程式檔案只有一個,電腦裡卻有兩個互不相干的東西在跑:一個當掉,另一個還在;它們記憶體裡的資料也不同。
| 程式(program) | 行程(process) | |
|---|---|---|
| 是什麼 | 磁碟上的檔案,內容是指令與初始資料 | 一支程式跑起來之後的實體 |
| 狀態 | 不會動 | 有目前執行到哪一行、記憶體裡的值、開著的檔案 |
| 數量 | 一個檔案 | 同一個檔案可以同時有很多個 |
核心給每個行程一個編號(pid,process ID),並且讓每個行程都以為自己獨佔一套東西:
- 位址空間:行程看到的記憶體。不同行程看到同一個位址,內容可以完全不同。
- CPU:其實是輪流用,但行程感覺不到自己被暫停過。
下面這支程式在真正的 Linux 上列出系統裡所有行程。Linux 的 /proc 是核心提供的虛擬目錄,每個行程一個子目錄,ps 這類工具就是讀這裡:
PID STATE COMMAND
1 S dumb-init
2 R output.s
在真正的 Linux 上實際執行的結果(Compiler Explorer 的 Linux x86-64 環境,gcc 13.3,2026-11-04)。
這個環境是個小容器,只有兩個行程:負責啟動的 dumb-init 和這支程式自己(編譯出來的執行檔叫 output.s)。在你自己的 Linux 或 WSL 上,用 ps -eo pid,ppid,stat,comm 會看到幾十到幾百個。
PCB:核心替每個行程記的資料
行程被暫停時,核心要記得所有東西,之後才接得回來。這份資料叫行程控制區塊(process control block,PCB)。Linux 裡對應的資料結構叫 task_struct。
| PCB 裡的資料 | 用途 | 在 os-lab 哪裡看 |
|---|---|---|
| pid、父行程 pid | 辨識行程、維持親子關係 | 卡片標題、「行程樹」面板 |
| 狀態 | 就緒、執行中、等待中 | 卡片上的標籤 |
| 暫存器、程式計數器 | 暫停時 CPU 的內容,恢復時放回去 | 卡片上的「暫存器」與第幾行 |
| 記憶體資訊(頁表) | 這個行程的位址空間 | 「記憶體」面板 |
| 開啟的檔案(fd 表) | 第 4 課 | 「檔案與 fd」面板 |
| 訊號的處理方式 | 第 4 課 | |
| 排程資訊 | 優先權、用了多少 CPU,第 5、6 課 | 統計列 |
Linux 把一部分 PCB 的內容透過 /proc/<pid>/status 給你看。下面在真正的 Linux 上讀自己的狀態,然後睡 5 次,再讀一次:
Name: output.s
State: R (running)
Pid: 2
PPid: 1
VmRSS: 1416 kB
Threads: 1
voluntary_ctxt_switches: 0
nonvoluntary_ctxt_switches: 0
--- 睡 5 次之後 ---
Name: output.s
State: R (running)
Pid: 2
PPid: 1
VmRSS: 1548 kB
Threads: 1
voluntary_ctxt_switches: 5
nonvoluntary_ctxt_switches: 0
在真正的 Linux 上實際執行的結果(Compiler Explorer 的 Linux x86-64 環境,gcc 13.3,2026-11-04)。
最後兩行是這個行程到目前為止的內容切換次數。voluntary 是自己讓出 CPU(例如等待 I/O、睡眠),nonvoluntary 是被核心換下來(例如時間片用完)。睡 5 次,voluntary 剛好多 5。State 是 R,因為它讀這個檔案的時候正在執行。
五種狀態
教科書常用五種狀態描述行程的一生:
| 狀態 | 意思 |
|---|---|
| 新建(new) | 正在建立,還不能跑 |
| 就緒(ready) | 可以跑,只是還沒輪到 CPU |
| 執行(running) | 正在 CPU 上跑。一顆核心同時只有一個 |
| 等待(waiting,也叫 blocked) | 在等某件事,給它 CPU 也沒用 |
| 結束(terminated) | 跑完了,等著被清理 |
這張圖最重要的地方,是等待的行程在事情發生之後會回到就緒排隊,不會直接回到執行,因為 CPU 可能正有別人在用。
ready 和 blocked 是最容易混淆的一對。 就緒的行程想要 CPU,只差排隊;等待中的行程不要 CPU,它缺的是別的東西。排程器只會從就緒的行程裡挑。
行程在等什麼
常見的阻塞原因:
| 行程做了什麼 | 在等什麼 | 誰叫醒它 |
|---|---|---|
sleep(2) |
時間到 | 計時器中斷 |
read 檔案或網路 |
資料送到 | 磁碟或網卡的中斷 |
read 管線 |
另一個行程寫入資料 | 寫入資料的那個行程 |
wait |
子行程結束 | 子行程結束時由核心通知 |
pthread_mutex_lock(第 8、9 課) |
別人放開鎖 | 放開鎖的執行緒 |
共同點是等的東西不在 CPU 上,所以行程把 CPU 讓出來。一支程式花在等待的時間可能遠多於計算的時間,例如文字編輯器大部分時間都在等你按鍵。多工因此才划算:一個行程等硬碟時,別的行程可以用 CPU。
在 Linux 的 ps 裡,狀態用一個字母表示(這些字母和上面五種狀態不是一對一):
| 字母 | 意思 |
|---|---|
R |
正在跑或可以跑:包含教科書的「執行」與「就緒」 |
S |
可以被訊號打斷的睡眠:大部分在等事件的行程 |
D |
不可打斷的睡眠,通常在等 I/O |
T |
被工作控制的訊號停住(例如按 Ctrl+Z) |
Z |
殭屍:已經結束,父行程還沒來收(下一課) |
所以 ps 看到很多個 R,不代表有那麼多行程同時在 CPU 上。
內容切換存了什麼、代價在哪裡
從行程 A 換到行程 B 叫內容切換(context switch)。觸發的時機有兩種:A 自己進入等待(voluntary),或是計時器中斷讓核心決定換人(nonvoluntary)。
核心要做的事:
- 把 A 的暫存器、程式計數器、堆疊指標存進 A 的 PCB。
- 把 A 的狀態改成就緒或等待,選出下一個行程 B。
- 換到 B 的位址空間(把 CPU 指向 B 的頁表)。
- 從 B 的 PCB 放回暫存器與程式計數器,B 從上次停下的地方繼續。
B 完全不知道自己被暫停過,它的暫存器和停下時一模一樣。
代價分兩層:
- 直接的代價:上面這幾步要執行指令,這段時間沒有人在做有用的計算。
- 間接的代價:換了位址空間,TLB(位址轉換的快取,第 13 課)的內容用不上;B 的資料也不在 CPU 快取裡。B 剛開始跑的那段時間會比較慢。
所以時間片不能切得太短,否則 CPU 都花在切換上,這個取捨第 5 課會細談。
確認一下
同一支程式執行兩次,產生兩個行程。下面哪一個不是兩者各有一份?
行程 A 呼叫 read 讀硬碟,資料讀完時,A 會變成什麼狀態?
一台單核心電腦,ps 顯示有 4 個行程的狀態是 R。下面哪一個說法正確?
行程 A 跑到一半被換下來,之後怎麼知道要從哪裡繼續?
等 5 秒有兩種寫法:sleep(5),或是寫一個迴圈一直檢查時間。兩者差在哪裡?
重點整理
- 程式是磁碟上的檔案,行程是它跑起來之後的實體;同一支程式可以有很多個行程,各有自己的位址空間與暫存器。
- 核心用 PCB 記住行程的一切。暫停時把 CPU 的內容存進去,輪到時放回來。
- 五種狀態裡最關鍵的區別:就緒是在等 CPU,等待是在等別的事;等待結束之後要回到就緒排隊。
- 內容切換有直接代價,也有快取、TLB 失效的間接代價。
下一課看行程是怎麼來的:fork、exec 和 wait,也就是 Shell 啟動程式的方式。