C++ / a working model

109 / 163   ·   C11   ·   约 8 分钟

插曲:进程 API

先记住这句话

本插曲介绍 UNIX 进程创建与控制的核心接口。fork 复制当前进程并让父子从同一返回点继续但返回值不同,wait 让父进程阻塞直至子进程结束,exec 族则用全新可执行映像替换当前进程。三者组合既简洁又极具表达力。

本篇内容
  1. fork() 如何复制正在运行的进程
  2. 用 wait() 让父进程与子进程同步
  3. exec() 用新程序彻底覆盖当前映像
  4. 运行示例
  5. 动手练习

官方章节 PDF

fork() 如何复制正在运行的进程

fork 立刻克隆调用者:子进程获得独立的地址空间、寄存器和指令指针,却从 fork 的返回处接着执行,仿佛自己刚刚调用了它。父进程得到子进程的 PID,子进程得到 0,据此可以立刻分道扬镳。两者此后完全独立,由调度器决定谁先继续,因此未同步时输出顺序不可预测。

用 wait() 让父进程与子进程同步

父进程调用 wait 或 waitpid 后会暂停,直到某个子进程退出并回收其终止状态。这既消除了僵尸进程,也把后续语句的执行顺序变成确定的:无论谁先被调度,父进程都会等到子进程完成打印并结束之后才继续。

exec() 用新程序彻底覆盖当前映像

子进程在 fork 之后常立刻调用 exec 系列函数之一。成功时整个地址空间(代码、数据、堆栈)被指定可执行文件替换,exec 之后的语句永远不会执行。shell 正是依靠 fork 后紧跟 exec 来启动用户输入的命令。

常见误区

  • 不检查 fork 返回值就继续,父子可能同时走入错误分支
  • 漏掉 wait 会让已退出的子进程变成僵尸并占用进程表槽位
  • 在没有 wait 的情况下假定子进程一定先打印

运行一个例子

最低标准 C11 · 完整程序 · 下载 .c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void) {
    pid_t rc = fork();
    if (rc < 0) {
        fprintf(stderr, "fork failed\n");
        return 1;
    } else if (rc == 0) {
        printf("hello from child\n");
    } else {
        wait(NULL);
        printf("hello from parent after wait\n");
    }
    return 0;
}

在本地编译

gcc -std=c11 -Wall -Wextra -Wpedantic -Werror ostep-05-process-api.c -o example && ./example

预期结果

hello from child
hello from parent after wait

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

父进程加上 wait(NULL) 之后,为什么子进程的输出总是出现在父进程输出之前?

查看参考答案

无论调度器先选谁,父进程都会在 wait 处阻塞直到子进程退出。子进程打印完毕后立即结束,因此它的那一行必然先出现。

继续查证

标准草案与官方章节会更新;版本标记只说明示例最低要求。

回到目录