Process 的家族關係#

Linux 上沒有「憑空建立 process」的 API。每一個 process 都來自於另一個 process,靠 fork(2) 複製,再用 exec(3) 替換成想跑的程式。整個系統開機後從 PID 1(init process)開始,所有後代都是它的子孫。

  • 父子關係是 strict tree:每個 process 只有一個 parent,但可以有多個 children
  • 親屬關係由 kernel 在 task_struct 中以 linked list 維護
  • 觀察整棵樹最快的工具是 pstree

fork(2):複製出一個自己#

fork(2) 是 Linux 建立新 process 的核心系統呼叫。呼叫成功後會回傳兩次:一次在 parent、一次在 child。

  • Parent 拿到 child 的 PID(> 0)
  • Child 拿到 0
  • 失敗回傳 -1
#include <unistd.h>
#include <stdio.h>

int main(void) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        printf("child pid=%d ppid=%d\n", getpid(), getppid());
    } else {
        printf("parent pid=%d child=%d\n", getpid(), pid);
    }
    return 0;
}

現代 Linux fork(2) 採用 copy-on-write,page table 共用直到任一方寫入才真正複製,所以 fork 本身很便宜。

exec(3):替換成另一個程式#

exec(3) 系列函式(execl、execv、execle、execve、…)會把當前 process 的程式碼、資料段整個替換成新的可執行檔,但 PID 不變。

  • 成功時不會回傳,因為原本的程式已經被覆蓋
  • 失敗才會 return -1,errno 標示原因
  • File descriptor 預設保留(除非設了 FD_CLOEXEC)

典型 fork + exec 模式:

pid_t pid = fork();
if (pid == 0) {
    execlp("ls", "ls", "-l", (char*)NULL);
    perror("exec");      // 只有失敗才會跑到這
    _exit(127);
}

wait(2) 與 waitpid(2):回收 child#

當 child 結束時,kernel 會保留它的 exit status,直到 parent 透過 wait(2) 或 waitpid(2) 取走。這個動作稱為 reap。

  • wait(2):阻塞直到任一 child 結束
  • waitpid(2):可指定 PID,可加 WNOHANG 改成 non-blocking
  • 若 parent 不 reap,child 會卡在 zombie 狀態
int status;
pid_t child = fork();
if (child == 0) {
    execlp("sleep", "sleep", "1", (char*)NULL);
    _exit(127);
}
waitpid(child, &status, 0);
if (WIFEXITED(status)) {
    printf("exit code = %d\n", WEXITSTATUS(status));
}

用 ps 觀察父子關係#

ps -ef                          # 完整列表,PPID 欄位是父行程 PID
ps -o pid,ppid,pgid,sid,cmd     # 自訂欄位
ps --forest                     # 用縮排畫出樹狀關係

想找出某個 process 的所有後代,可以遞迴比對 PPID,或直接用 pstree。

用 pstree 觀察 process tree#

pstree 會把整個系統的 process 以樹狀結構畫出,PID 1 在根部。

pstree              # 從 PID 1 開始
pstree -p           # 同時顯示 PID
pstree -p 1234      # 從特定 PID 出發
pstree -s 1234      # 顯示某個 PID 到 root 的祖先鏈

在 container 內跑 pstree -p 1 會看到該 container 的整棵 process 樹,而且根部就是該應用,這正是 PID namespace 隔離的效果。

Shell 是怎麼執行命令的#

Shell(bash、dash、ash)跑外部命令的標準流程:

  • fork(2) 出一個 child
  • Child 中 exec(3) 成目標程式
  • Parent shell 用 waitpid(2) 等 child 結束,拿到 exit code 寫進 $?

如果是內建命令(cd、export),shell 不會 fork,直接在自己 process 內執行。

延伸閱讀#

  • man 2 fork
  • man 3 exec
  • man 2 wait
  • man 1 pstree