`fork()` 創建子進程時,父子進程擁有**獨立的虛擬地址空間**,但物理頁面透過 **Copy-on-Write (COW)** 共享,直到一方嘗試寫入。本文用一個精簡實驗證明:父子進程變量虛擬地址相同、物理地址初始共享、寫入後分離,並澄清「fork 複製一切」的常見誤解。
#include
#include
#include
struct XX { int a; };
int main() {
pid_t pid;
struct XX s = {5};
struct XX *sp = &s;
pid = fork();
if (pid > 0) { // 父進程
printf("Parent: sp=%p, &sp->a=%p, a=%d\n", sp, &sp->a, sp->a);
sp->a = 4; // 父進程修改
sleep(10); // 等待子進程運行
printf("Parent: sp=%p, &sp->a=%p, a=%d\n", sp, &sp->a, sp->a);
printf("Parent exit\n");
} else { // 子進程
printf("Child: sp=%p, &sp->a=%p, a=%d\n", sp, &sp->a, sp->a);
sp->a = -1; // 子進程修改
printf("Child changed a to %d\n", sp->a);
}
return 0;
}
Parent: sp=0x7fffe86b64a0, &sp->a=0x7fffe86b64a0, a=5
Child: sp=0x7fffe86b64a0, &sp->a=0x7fffe86b64a0, a=5
Child changed a to -1
Parent: sp=0x7fffe86b64a0, &sp->a=0x7fffe86b64a0, a=4
Parent exit
| 階段 | 父進程頁表項 (PTE) | 子進程頁表項 (PTE) | 物理頁框 | 權限位 |
|---|---|---|---|---|
| fork() 後 | 指向 PFN_X | 指向 PFN_X | PFN_X (共享) | 雙方 **Read-only** (觸發 COW) |
| 子進程寫入 | 指向 PFN_X | 指向 PFN_Y (新分配) | PFN_X / PFN_Y | 子進程 PTE 變 **Read-Write** |
| 父進程寫入 | 指向 PFN_Z (新分配) | 指向 PFN_Y | PFN_X / PFN_Y / PFN_Z | 父進程 PTE 變 **Read-Write** |
**本質**:`fork` 只複製 **頁表指針** 並標記 **唯讀**,不複製物理內存。寫入時缺頁中斷 → 內核分配新頁框 → 複製內容 → 更新 PTE → 恢復執行。
| 誤區 | 事實 |
|---|---|
| fork 複製整個進程內存,開銷極大 | 僅複製頁表 (~幾 MB),COW 使實際複製按需發生,極其高效 |
| 父子進程共享變量,可通過全局變量通信 | **錯誤**!虛擬地址雖同,物理隔離。需用管道、共享內存、Socket 等 IPC |
| 子進程修改全局變量,父進程可見 | 不可見,各自擁有獨立副本 |
| vfork() 共享內存,輕量級 | `vfork` 子進程在父進程地址空間運行,**父進程阻塞**,極易死鎖/破壞棧,**已棄用**,勿用 |
用 `/proc/
// 簡易讀取 PFN (需 root)
unsigned long getPFN(pid_t pid, void *vaddr) {
char path[64]; sprintf(path, "/proc/%d/pagemap", pid);
FILE *f = fopen(path, "rb");
unsigned long offset = ((unsigned long)vaddr / 4096) * 8;
fseek(f, offset, SEEK_SET);
unsigned long entry; fread(&entry, 8, 1, f); fclose(f);
return entry & ((1ULL<<55)-1); // PFN 位
}
// 父子進程修改前 PFN 相同,修改後不同
徹底理解 `fork` + `COW`,是 Linux 系統程式開發、高性能服務器設計、容器技術 (Docker `fork`/`clone` 基因) 的基石。下次面試被問「fork 開銷大嗎」,你可以自信回答:「只複製頁表,寫時再複製,極其輕量。」