首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

Linux C 語言 fork 與進程地址空間:複製時寫 (COW) 機制實證與常見誤區

     編輯:關於C語言

`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

關鍵觀察

  1. 虛擬地址完全相同:`sp` 與 `&sp->a` 在父子進程中數值一致 — **虛擬地址空間獨立但布局相同**。
  2. 初始值共享:子進程讀到 `a=5` — **物理頁面共享 (COW)**。
  3. 修改相互隔離:子進程改 `-1`,父進程後讀仍為 `4` (非 `-1`) — **寫入觸發頁面複製,物理頁面分離**。
  4. 無競爭條件:父進程 `sleep(10)` 確保子進程先運行,但即使並發,COW 硬體機制保證隔離性。

三、底層機制:頁表與 COW

階段父進程頁表項 (PTE)子進程頁表項 (PTE)物理頁框權限位
fork() 後指向 PFN_X指向 PFN_XPFN_X (共享)雙方 **Read-only** (觸發 COW)
子進程寫入指向 PFN_X指向 PFN_Y (新分配)PFN_X / PFN_Y子進程 PTE 變 **Read-Write**
父進程寫入指向 PFN_Z (新分配)指向 PFN_YPFN_X / PFN_Y / PFN_Z父進程 PTE 變 **Read-Write**

**本質**:`fork` 只複製 **頁表指針** 並標記 **唯讀**,不複製物理內存。寫入時缺頁中斷 → 內核分配新頁框 → 複製內容 → 更新 PTE → 恢復執行。

四、常見誤區澄清

誤區事實
fork 複製整個進程內存,開銷極大僅複製頁表 (~幾 MB),COW 使實際複製按需發生,極其高效
父子進程共享變量,可通過全局變量通信**錯誤**!虛擬地址雖同,物理隔離。需用管道、共享內存、Socket 等 IPC
子進程修改全局變量,父進程可見不可見,各自擁有獨立副本
vfork() 共享內存,輕量級`vfork` 子進程在父進程地址空間運行,**父進程阻塞**,極易死鎖/破壞棧,**已棄用**,勿用

五、工程實踐指引

  1. fork 後立即 exec:`fork()` + `execve()` 模式 (Shell 啟動程式) 最高效 — 子進程立即替換地址空間,COW 頁面無需複製即被丟棄。
  2. 多進程服務器模型:Master `fork` Worker (Nginx, Redis, Apache prefork) — 共享只讀配置/代碼/常數,各 Worker 獨立請求狀態。
  3. 避免 fork 後大量寫入:大量寫入觸發大規模頁面複製,記憶體暴增。若需共享可寫數據,用 `mmap(MAP_SHARED)` 或 `shmget`。
  4. 多線程中 fork 陷阱:僅調用 `async-signal-safe` 函數 (如 `write`, `_exit`),**嚴禁** `malloc`/`lock`/`printf` 等,極易死鎖 (鏡像了鎖狀態但無持有者)。

六、驗證物理地址差異 (進階)

用 `/proc//pagemap` 讀取頁框號 (PFN) 可直接觀測物理頁面分離:


// 簡易讀取 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 開銷大嗎」,你可以自信回答:「只複製頁表,寫時再複製,極其輕量。」

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved