Windows XP Windows 7 Windows 2003 Windows Vista Windows 綜合教學 Linux 系統教學
Windows 10 Windows 8 Windows 2008 Windows NT Windows Server 電腦軟體教學
您现在的位置: Windows教程網 >> Windows教程 >  >> Linux 系統教學 >> Linux 教學

Linux 父子進程深度解析:fork、wait、信號與僵屍/孤兒進程

編輯:Linux 教學

前言

fork() 建立子進程是 Unix 程式設計的核心。本文系統整理父子進程關係、信號處理、僵屍/孤兒進程成因與正確回收模式,附帶程式碼驗證。

1. fork() 語義與複製機制

  • 子進程獲得父進程幾乎完整的副本:記憶體 (Copy-on-Write)、檔案描述符表、信號處理函數、環境變數、當前工作目錄等。
  • 差異:返回值 (父得子 PID,子得 0)、PID/PPID、資源使用統計、鎖、未決信號集清空、定時器清零。
  • Copy-on-Write (COW):父子共享物理頁框,僅在任一方寫入時才複製頁面,極大提升 fork() 效能。

2. 正確回收子進程:避免僵屍進程

子進程終止後,核心保留其 task_struct (PID、退出狀態等) 直到父進程呼叫 wait()/waitpid() 讀取狀態。此期間子進程處於 Z (Zombie) 狀態。

錯誤做法

// 僅等待特定子進程,若有多個子進程會遺漏
waitpid(child_pid, &status, 0);

正確做法:信號驅動非阻塞回收

#include <signal.h> #include <sys/wait.h> #include <stdio.h>

void sigchld_handler(int sig) {
    int status; pid_t pid;
    // WNOHANG 非阻塞,循環回收所有已終止子進程
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status))
            printf("Child %d exited normally, status=%d\n", pid, WEXITSTATUS(status));
        else if (WIFSIGNALED(status))
            printf("Child %d killed by signal %d\n", pid, WTERMSIG(status));
    }
}

int main() {
    struct sigaction sa = { .sa_handler = sigchld_handler, .sa_flags = SA_RESTART | SA_NOCLDSTOP };
    sigaction(SIGCHLD, &sa, NULL);

    for (int i = 0; i < 3; i++) {
        if (fork() == 0) {
            printf("Child %d (PID=%d) sleeping...\n", i, getpid());
            sleep(2 + i);
            _exit(i + 10); // 子進程用 _exit 避免刷新標準 I/O 緩衝區
        }
    }
    // 父進程繼續工作...
    sleep(10);
    return 0;
}

關鍵點:

  • SA_NOCLDSTOP:不因子進程停止/繼續 (SIGSTOP/SIGCONT) 產生 SIGCHLD,僅在終止時產生。
  • SA_RESTART:被信號中斷的系統調用 (如 read) 自動重啟。
  • waitpid(-1, ..., WNOHANG):回收任一已終止子進程,非阻塞。
  • 使用 _exit() 而非 exit() 終止子進程,避免雙重 atexit、標準 I/O 緩衝區衝突。

3. 孤兒進程與 init 收養

父進程先於子進程終止 → 子進程成為孤兒 → 核心將其 PPID 設為 1 (init / systemd) → init 定期 wait() 回收。

驗證程式:

if (fork() == 0) {
    printf("Child PID=%d, PPID=%d\n", getpid(), getppid());
    sleep(5);  // 父進程已退出
    printf("Child PID=%d, new PPID=%d (adopted by init)\n", getpid(), getppid());
    _exit(0);
} else {
    printf("Parent PID=%d exiting...\n", getpid());
    _exit(0); // 父進程立即退出
}

4. 信號在父子進程間的傳遞

  • 終端產生的信號 (SIGINT Ctrl+C, SIGTSTP Ctrl+Z):送達前台進程組所有成員 (父子皆收到)。
  • 父進程自定義處理函數:子進程繼承該處理函數 (因 fork 複製信號處置)。
  • 子進程成為孤兒後:不再屬於原進程組,終端信號不再送達;若被 init 收養,預設處理動作生效。

驗證:父進程自定義 SIGINT,子進程孤兒後行為

void sigint_handler(int sig) { printf("PID=%d caught SIGINT\n", getpid()); }

int main() {
    signal(SIGINT, sigint_handler);
    for (int i = 0; i < 2; i++) {
        if (fork() == 0) {
            printf("Child %d (PID=%d) sleeping...\n", i, getpid());
            sleep(3); // 此時父進程仍在,收到 SIGINT 會執行 handler
            printf("Child %d after 1st sleep\n", i);
            sleep(5); // 父進程已退出,子進程成孤兒,再收到 SIGINT 接預設動作(終止)
            printf("Child %d after 2nd sleep\n", i);
            _exit(0);
        }
    }
    sleep(2); // 父進程較早退出
    return 0;
}

5. 進程間通訊 (IPC) 基礎

因 fork 後記憶體獨立 (COW),父子無法直接共享變數。常用 IPC:

機制適用場景
管道親緣進程單向/雙向資料流
Socketpair親緣進程雙向通訊 (全雙工)
共享記憶體 (shm)高效大量資料共享,需同步 (信號量/mutex)
訊號簡單事件通知 (不可攜帶資料,除非 real-time signal)
Unix Domain Socket本機任意進程通訊,支援傳遞檔案描述符

結語

  • fork() 複製資源,waitpid() 回收狀態,SIGCHLD 非阻塞回收是標準模式。
  • 僵屍進程佔用 PID 與核心資源,長期運行服務必須正確處理。
  • 孤兒進程由 init 收養自動回收,但失去原父進程協調能力。
  • 信號處理函數在 fork 後共享,孤兒後行為改變,設計時需考慮生命週期。
Copyright © Windows教程網 All Rights Reserved