上一篇談到 Context Switch 時,我們知道 OS 可以在不同 Process 之間切換。
那這些 Process 到底是怎麼被建立出來的?
在 Unix-like 系統裡,一個非常經典的方式就是:fork();
我們知道 fork() 會複製目前的 Process,建立一個 Child Process。
但「複製 Process」這句話其實很容易讓人誤會。
假設 Parent Process 已經用了 2 GB 記憶體:
Parent Process
Memory = 2 GB
呼叫 fork() 的瞬間,OS 難道真的要立刻:複製 2 GB,再建立 2 GB 嗎?
現代作業系統通常不會這麼做。
所以今天就來拆:
fork() 到底複製了什麼?
#include <stdio.h>
#include <unistd.h>
int main() {
fork();
printf("Hello\n");
return 0;
}
如果 fork() 成功,執行結果通常會看到兩次:
Hello
Hello
為什麼?
因為原本只有:
Parent Process
呼叫 fork() 之後,系統建立了一個新的 Child Process:
fork()
│
┌─────┴─────┐
▼ ▼
Parent Child
而 Parent 和 Child 都會繼續執行 fork() 之後的程式。
所以:
fork();
printf("Hello\n");
printf() 會被兩個 Process 各自執行一次。
一個 Process 要能執行,除了程式碼之外,還需要:
Program Counter
Stack Pointer
Registers
Virtual Address Space
Process State
Open Files
...
fork() 建立 Child 時,Child 會從 Parent 的執行環境衍生出自己的初始狀態,因此 fork() 完成之後:
main()
│
│
fork()
│
├──────────────┐
▼ ▼
Parent Child
│ │
▼ ▼
下一行 下一行
它們不是共用同一組 CPU Registers 繼續跑。
Child 是新的 Process,也有自己的執行狀態。
只是建立的那一刻,它的初始 execution context 是從 Parent 衍生而來,所以兩邊才會從相對應的位置繼續。
要看 fork() 的 return value,例如:
pid_t pid = fork();
if (pid == 0) {
printf("I am child\n");
} else {
printf("I am parent\n");
}
同一次 fork(),Parent 和 Child 看到的 return value 不一樣。
Parent
fork() 回傳 Child PID
Child
fork() 回傳 0
如果建立失敗:fork() 回傳 -1
Parent 和 Child 從幾乎相同的程式位置繼續,但 Kernel 刻意讓它們看到不同的 return value,因此兩邊就可以走不同的程式邏輯。
不是。
Child 是一個新的 Process,因此它有自己的 PID,例如:
Parent PID = 5000
Child PID = 5001
Parent 和 Child 之間會存在親子關係,但:parent ≠ Child
它們是兩個不同的 Process。
Process
│
├── Virtual Address Space
├── CPU Execution State
├── OS Management Information
├── Open Files / Resources
└── Execution Environment
fork() 建立 Child 時,Child 會繼承 Parent 的大量執行環境。
Parent
│
├── Code
├── Data
├── Heap
├── Stack
├── Open File Descriptors
├── Working Directory
├── Environment
└── Execution State
fork()
↓
Child
│
├── 對應的 Code / Data / Heap / Stack
├── 繼承的 File Descriptor 狀態
├── 相同的 Working Directory
├── 相同的 Environment
└── 從 Parent 衍生的 Execution State
「Child 看起來得到 Parent 的記憶體內容」不代表 Kernel 一定立刻把所有 Physical Memory 完整複製一份,這就是接下來最重要的 Copy-on-Write。
Copy-on-Write,簡稱 COW:
先不要複製,真的有人要修改時再複製。
假設 fork() 前:
Parent Virtual Memory
│
▼
Physical Page A
fork() 之後,不一定馬上變成:Parent → Physical Page A ; Child → Physical Page B
一開始可以先讓 Parent 和 Child 的某些 Virtual Pages 指向相同的 Physical Pages:
Parent Virtual Page ───┐
▼
Physical Page A
▲
Child Virtual Page ────┘
也就是:先共享。
只要兩邊都只是讀取:Parent → read ; Child → read
內容沒有被改變,就沒有必要真的複製。
CPU 發現目前的 page mapping 不允許這次 write,因此觸發 Page Fault。在Kernel 接手之後,會判斷:
這不是單純亂存取記憶體,而是合法的 Copy-on-Write 情況。
接著 Kernel 可以:
原本:
Parent ──┐
├── Physical Page A
Child ──┘
1.建立新的 Physical Page後,複製需要的內容:
Physical Page A
│
│ copy
▼
Physical Page B
2.然後調整 Child 的 mapping:
Parent Virtual Page
│
▼
Physical Page A
Child Virtual Page
│
▼
Physical Page B
3.最後 Child 再對自己的 Page B 進行修改。結果:
Parent → 原本內容
Child → 修改後內容
兩邊就真正分開了。
所以 Copy-on-Write 的「Copy」不是:
fork() 發生 → 全部 Copy
而是:
fork() → 先共享
真的有人要 Write
→ Page Fault
→ Kernel 建立 private copy
→ 修改 mapping
→ 再繼續 write
這就是 Copy-on-Write 名字的由來。
不會。舉個例
#include <stdio.h>
#include <unistd.h>
int main() {
int x = 10;
pid_t pid = fork();
if (pid == 0) {
x = 20;
printf("Child: %d\n", x);
} else {
x = 30;
printf("Parent: %d\n", x);
}
return 0;
}
Child: 20
Parent: 30
雖然 fork() 剛完成時,兩邊看到的:x = 10 內容相同。
即使底層一開始因 COW 暫時共享相同的 Physical Page,一旦其中一方需要修改,Kernel 就會讓它們分離。
因此:
COW 是底層的最佳化,不會改變 fork() 提供給程式的 private address-space
可以。假設:
int x = 10;
Parent 看到:&x = 0x7fff1234
Child 也可能看到:&x = 0x7fff1234
看起來 Address 完全相同,但這是 Virtual Address。
最明顯的就是:PID
Parent PID ≠ Child PID
Child 必須有自己的 Process Identity。
下一篇: