109 / 163 · C11 · 约 8 分钟
插曲:进程 API
先记住这句话
本插曲介绍 UNIX 进程创建与控制的核心接口。fork 复制当前进程并让父子从同一返回点继续但返回值不同,wait 让父进程阻塞直至子进程结束,exec 族则用全新可执行映像替换当前进程。三者组合既简洁又极具表达力。
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 处阻塞直到子进程退出。子进程打印完毕后立即结束,因此它的那一行必然先出现。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。