进程、fork/exec、wait 与信号 / Processes, fork/exec, wait, and Signals
1. 为什么进程不是 main 函数
main() 只是进程生命周期的一段。进程从可执行文件装载开始,带着 argv、env、地址空间、fd 表、信号处理方式、当前工作目录、权限和资源限制运行,最后以 exit status 或 signal 结束。
executable file
-> loader creates process image
-> C/C++ runtime init
-> main(argc, argv)
-> return / exit / signal
-> parent observes wait status2. 基础词小注释
PID:process id,内核给进程的编号。PID 会复用,日志里最好同时记录启动时间或父子关系。
PPID:parent process id。父进程通常负责 wait 子进程,避免僵尸进程。
address space:进程看到的虚拟地址空间。fork 后父子最初看起来相同,但通过 copy-on-write 分离。
argv/env:启动参数和环境变量。它们是进程输入的一部分,不能把环境变量当成稳定配置数据库。
exit status:进程正常退出时留给父进程的状态码。shell 常把 0 当成功,非 0 当失败。
signal:异步通知机制,例如 SIGINT、SIGTERM、SIGCHLD、SIGPIPE。信号处理有严格限制。
zombie:子进程已退出但父进程尚未 wait,内核保留退出状态形成僵尸。
exec:用新程序映像替换当前进程映像。成功后不会返回到原代码。
3. fork 的直觉模型
fork() 创建子进程。返回后父子进程从同一行代码继续执行,但返回值不同:父进程得到子 PID,子进程得到 0。
before fork:
one process
after fork:
parent: fork returns child_pid
child : fork returns 0
both continue after fork callfork 不是简单复制所有物理内存。现代系统通常用 copy-on-write:父子虚拟地址最初映射到相同物理页,谁写入,谁触发复制。
4. exec 的直觉模型
execve 系列调用把当前进程的代码、数据、堆、栈替换成新程序。PID 可以不变,但进程映像变了。
child after fork
has parent-like address space
adjusts fd redirection
exec /usr/bin/grep
becomes grep process image成功的 exec 不返回。只有失败时才返回 -1 并设置 errno。写代码时必须在 child 分支处理 exec 失败,并用 _exit 退出,避免跑回父进程逻辑。
5. wait 和退出状态
父进程通过 waitpid 观察子进程结束。状态不是单纯整数,要用宏解析:WIFEXITED、WEXITSTATUS、WIFSIGNALED、WTERMSIG。
pid_t pid = fork();
if (pid == 0) {
execlp("grep", "grep", "needle", "file.txt", nullptr);
_exit(127);
}
int status = 0;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
int code = WEXITSTATUS(status);
}6. fd 继承和 close-on-exec
fork 后子进程继承父进程打开的 fd。exec 默认也保留没有设置 close-on-exec 的 fd。这既是 shell 管道重定向的基础,也是服务程序泄漏 fd 到子进程的常见来源。
parent opens log fd
-> fork child
-> child exec helper
-> helper unexpectedly keeps log fd open
-> log rotation or shutdown blocked创建 fd 时优先使用 O_CLOEXEC、SOCK_CLOEXEC、pipe2(O_CLOEXEC)、accept4(SOCK_CLOEXEC)。
7. 信号基础
信号是异步通知,不是普通函数调用。处理函数可能在任意指令附近打断当前线程,因此 signal handler 中只能调用 async-signal-safe 函数。不要在 handler 里写复杂 C++、分配内存、加锁或打印 iostream。
常见做法是 handler 只设置 std::atomic 标志,或写入 self-pipe/eventfd,让主循环在安全位置处理关闭。
8. SIGCHLD 和僵尸进程
子进程退出后,如果父进程不 wait,内核要保留退出状态,进程表里会出现 zombie。长期运行服务启动子进程时,必须设计回收策略。
9. SIGPIPE
向已经关闭读端的 pipe/socket 写入,进程可能收到 SIGPIPE,默认动作是终止。网络服务通常忽略 SIGPIPE 或在 send 中使用 MSG_NOSIGNAL,并处理 EPIPE。
10. C++ RAII 封装进程句柄
进程不是简单对象,但可以封装“子进程 PID + 是否需要 wait/kill + stdout/stderr fd”。析构时是否 kill 要谨慎,通常需要显式 stop/wait,而不是析构里偷偷强杀。
11. 常见错误
- fork 后子进程继续执行父进程控制流。
- exec 失败后使用
exit而不是_exit,导致重复 flush 父进程缓冲。 - 忘记 wait 子进程,产生 zombie。
- 子进程继承不该继承的 fd。
- signal handler 中调用非安全函数。
- 把 exit code 和 signal termination 混在一起。
12. 调试命令
ps -o pid,ppid,state,cmd
pstree -p
strace -f -e fork,clone,execve,wait4 ./program
lsof -p <pid>
cat /proc/<pid>/status13. 练习
- 写一个 mini runner:fork 后 exec 指定命令,父进程 wait 并打印退出原因。
- 故意 exec 一个不存在命令,确认 child 用 127 退出。
- 打开一个文件后 fork+exec,用 lsof 观察 fd 是否泄漏。
- 写 SIGINT 处理,只设置退出标志,由主循环优雅关闭。
- 创建子进程不 wait,观察 zombie,再修复。
14. 总结
进程篇要抓住四件事:fork 复制执行流,exec 替换映像,wait 回收状态,signal 打断正常控制流。C++ 封装的价值,是把这些边界变成明确生命周期,而不是把系统行为藏起来。
深化切片 1:最小实验
围绕 fork 和 exec 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 2:错误注入
围绕 exec 和 waitpid 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 3:外部观察
围绕 waitpid 和 exit status 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 4:RAII 封装
围绕 exit status 和 signal 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 5:并发边界
围绕 signal 和 fd inheritance 做一次工程练习。系统调用可能阻塞,信号可能打断等待,多个线程可能同时操作同一个资源。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 6:生产复盘
围绕 fd inheritance 和 zombie 做一次工程练习。记录命令、errno、pid、fd、路径、peer、字节数和状态,保证另一个人能复现。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 7:最小实验
围绕 zombie 和 SIGPIPE 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 8:错误注入
围绕 SIGPIPE 和 fork 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 9:外部观察
围绕 fork 和 exec 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 10:RAII 封装
围绕 exec 和 waitpid 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 11:并发边界
围绕 waitpid 和 exit status 做一次工程练习。系统调用可能阻塞,信号可能打断等待,多个线程可能同时操作同一个资源。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 12:生产复盘
围绕 exit status 和 signal 做一次工程练习。记录命令、errno、pid、fd、路径、peer、字节数和状态,保证另一个人能复现。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 13:最小实验
围绕 signal 和 fd inheritance 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 14:错误注入
围绕 fd inheritance 和 zombie 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 15:外部观察
围绕 zombie 和 SIGPIPE 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 16:RAII 封装
围绕 SIGPIPE 和 fork 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 17:并发边界
围绕 fork 和 exec 做一次工程练习。系统调用可能阻塞,信号可能打断等待,多个线程可能同时操作同一个资源。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 18:生产复盘
围绕 exec 和 waitpid 做一次工程练习。记录命令、errno、pid、fd、路径、peer、字节数和状态,保证另一个人能复现。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 19:最小实验
围绕 waitpid 和 exit status 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 20:错误注入
围绕 exit status 和 signal 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 21:外部观察
围绕 signal 和 fd inheritance 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 22:RAII 封装
围绕 fd inheritance 和 zombie 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 23:并发边界
围绕 zombie 和 SIGPIPE 做一次工程练习。系统调用可能阻塞,信号可能打断等待,多个线程可能同时操作同一个资源。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 24:生产复盘
围绕 SIGPIPE 和 fork 做一次工程练习。记录命令、errno、pid、fd、路径、peer、字节数和状态,保证另一个人能复现。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 25:最小实验
围绕 fork 和 exec 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 26:错误注入
围绕 exec 和 waitpid 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 27:外部观察
围绕 waitpid 和 exit status 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 28:RAII 封装
围绕 exit status 和 signal 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 29:并发边界
围绕 signal 和 fd inheritance 做一次工程练习。系统调用可能阻塞,信号可能打断等待,多个线程可能同时操作同一个资源。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 30:生产复盘
围绕 fd inheritance 和 zombie 做一次工程练习。记录命令、errno、pid、fd、路径、peer、字节数和状态,保证另一个人能复现。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 31:最小实验
围绕 zombie 和 SIGPIPE 做一次工程练习。用一个最小程序观察一个系统行为,不要把 shell、库封装和业务代码混在一起。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 32:错误注入
围绕 SIGPIPE 和 fork 做一次工程练习。故意制造一个失败条件,例如文件不存在、权限不足、子进程退出码非零、管道对端关闭或映射越界。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 33:外部观察
围绕 fork 和 exec 做一次工程练习。用 ps、strace、lsof、pmap、ss、tcpdump 或调试器观察内核状态,而不是只看 C++ 变量。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。
深化切片 34:RAII 封装
围绕 exec 和 waitpid 做一次工程练习。把资源获取、移动、释放和失效状态写进类型,避免正常路径能释放、异常路径泄漏。
hypothesis
-> minimal program
-> external observation
-> failure injection
-> engineering rule复盘时要说明:这个现象属于 C++ 对象生命周期、POSIX/系统调用语义、内核调度、文件系统、虚拟内存,还是网络协议。分层越清楚,进程、fork/exec、wait 与信号 就越不容易学成 API 背诵。