第 25 章 进程终止 (Process Termination)
核心结论
-
正常终止:
_exit(status)(系统调用)vsexit(status)(库函数);后者会运行 atexit handler、刷新 stdio buffer;fork 后子进程必须用_exit。 -
退出状态:只有低 8 位传给父进程;惯例 0 = 成功,非 0 = 失败;> 128 与 shell
$? = 128 + 信号号混淆。 -
退出处理器(atexit/on_exit):atexit 注册无参无返回值函数;exit 时按注册反序调用;on_exit (glibc 扩展) 可传 status + arg;信号终止时不调用——必须用 handler 拦截。
-
fork + stdio buffer 陷阱:stdout 是 block-buffered(重定向到文件)时,fork 时 buffer 中的内容被复制——父子 exit 都 flush 导致重复输出;解法:fork 前 fflush / setvbuf / 子用 _exit。
-
write 不被复制:write 直写内核 buffer,fork 不复制——「write 早于 printf 输出」是常见迷惑的根源。
|
本章主旨
本章是「进程生命周期」四章的第二章——进程如何终止。读者应掌握: |
一、核心概念
本章围绕 5 个核心概念展开:从 _exit/exit 的本质区别入手,到退出状态语义、atexit/on_exit 退出处理器、fork+stdio buffer 陷阱,最后到 main return 与 exit 的等价。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
_exit vs exit |
_exit 是系统调用,直接终止;exit 是库函数,运行 atexit handler、刷新 stdio buffer,再调 _exit;fork 后子必须用 _exit。 |
§25.1;fork 后子用 exit 会运行父进程的 atexit handler + 刷新父的 stdio buffer 副本——破坏父状态。 |
退出状态字节 |
exit/_exit 的 status 只有低 8 位(0-255)传给父;父用 wait + WEXITSTATUS 提取;惯例 0 成功,非 0 失败。 |
§25.1;status > 128 与 shell |
atexit/on_exit 退出处理器 |
atexit 注册 void func(void);exit 时按反序调用;on_exit (glibc) 可传 status + arg;fork 继承注册,exec 清空。 |
§25.3;SUSv3 要求至少 32 个 atexit handler(glibc 用链表,无限);atexit handler 不能用 exit(SUSv3 未定义);atexit 在信号终止时不调用——必须在 handler 中调用 exit 才能跑清理。 |
fork + stdio buffer 陷阱 |
stdout 默认是 line-buffered(终端)或 block-buffered(重定向文件);fork 时 buffer 中的内容被复制——父子 exit 都 flush → 重复输出;write 不被复制(直写内核)。 |
§25.4;解法:fork 前 fflush(stdout) 或 setvbuf 禁用 buffer 或子用 _exit;常见 bug 源。 |
main return 与 exit 等价 |
C89 下从 main 无 return 退出 → 退出状态未定义;C99 等价于 exit(0);return n 等价于 exit(n);但若 main 局部变量被 setvbuf 引用,return 会破坏环境。 |
§25.1;现代编译 |
二、详细笔记
25.1 进程终止:_exit vs exit
What:进程正常终止有两种方式——_exit(status) 系统调用直接终止;exit(status) 库函数先运行 atexit handler、刷新 stdio buffer、再调 _exit。
Why:理解两者的区别是写「fork + exec」模式的关键——子必须用 _exit 才能避免破坏父进程状态。
How:核心语义对比(§25.1):
| 维度 | _exit | exit |
|---|---|---|
类型 |
系统调用(unistd.h) |
库函数(stdlib.h) |
行为 |
立即终止 |
atexit 反序调用 → 刷新 stdio → _exit |
atexit handlers |
不运行 |
运行(反序) |
stdio buffer flush |
不刷新 |
刷新 |
父 wait 可见状态 |
可 |
可 |
适用场景 |
fork 后的子进程 |
主进程正常退出 |
退出状态(§25.1):
-
status 是 int,只有低 8 位有效。
-
0 = 成功;非 0 = 失败。
-
SUSv3 定义
EXIT_SUCCESS(0) 和EXIT_FAILURE(1)。-
status > 128 与 shell
$? = 128 + signo冲突——shell 无法区分。
-
When:
-
主进程 / 库代码——用
exit(EXIT_SUCCESS)。 -
fork 后子进程——用
_exit(EXIT_SUCCESS)。 -
信号 handler 中——用
_exit(安全);不能用exit(非 async-signal-safe)。
Example:fork 后子的正确退出:
switch (fork()) {
case -1: errExit("fork");
case 0:
/* 子进程 */
exec_or_other_work();
_exit(EXIT_SUCCESS); /* 必须用 _exit */
default:
/* 父进程 */
work_and_wait();
exit(EXIT_SUCCESS); /* 父用 exit */
}
main return 与 exit 等价(§25.1 补充):
-
C89:main 无 return 退出 → 退出状态未定义(Linux 上从栈/寄存器取随机值)。
-
C99+:main 无 return → 等价于 exit(0)。
-
return n等价于exit(n)。 -
例外:main 局部变量在 setvbuf 等调用中被引用——return 触发未定义行为。
25.2 进程终止的内核清理
What:进程正常或异常终止时,内核执行一系列清理(§25.2)。
Why:理解清理的内容能写出「正确释放资源」的代码。
How:终止时内核做的事:
| 动作 | 描述 |
|---|---|
关闭 fd |
所有 open fd / 目录流 / 消息目录描述符 / 转换描述符 |
释放文件锁 |
close 触发文件锁释放(第 55 章) |
分离 SysV shm |
shm_nattch 减 1 |
SysV semadj |
终止时按 semadj 加到 semaphore 值 |
SIGHUP 通知 |
若为 controlling terminal 进程,给前台进程组发 SIGHUP |
关闭 POSIX sem/mq |
sem_close / mq_close 等效 |
SIGHUP+SIGCONT |
进程组 orphan 时停止进程收此信号 |
mlock 解除 |
mlock/mlockall 锁定的内存 |
mmap 解除 |
所有 mmap 映射 |
When:写进程退出清理——大部分资源靠「exit 自动清理」,无需手动;但跨进程共享资源(SysV shm、文件锁)需要约定。
=== 25.3 atexit 与 on_exit 退出处理器
What:atexit 注册的函数在 exit 时自动调用(反序);on_exit (glibc) 可传 status + arg。
Why:库需要在进程退出时自动清理——但库不能要求主程序显式调用清理函数——退出处理器是「自动清理」的标准机制。
How:atexit 用法(§25.3):
#include <stdlib.h>
int atexit(void (*func)(void));
static void cleanup(void) {
/* 清理资源 */
close(fd);
unlink(tempfile);
}
atexit(cleanup);
exit(EXIT_SUCCESS); /* cleanup() 会在 _exit 之前被调用 */
关键事实:
-
atexit 反序调用——先注册后调用。
-
fork 继承 atexit 注册。
-
exec 清空 atexit 注册。
-
信号终止不调用 atexit——必须在 signal handler 中调用 exit 才能跑清理。
-
atexit handler 中调用 exit 未定义(部分系统无限递归)。
-
glibc 用链表,无限数量;SUSv3 要求 ≥ 32。
-
on_exit (glibc 扩展):
#define _BSD_SOURCE
#include <stdlib.h>
int on_exit(void (*func)(int status, void *arg), void *arg);
-
func 接收 status + arg——比 atexit 灵活。
-
与 atexit 同一链表——混用也按反序。
-
非标准——可移植代码避免。
When:
-
库代码——需要自动清理时用 atexit。
-
调试工具——用 on_exit 传状态码。
-
主程序——通常不必用 exit handler,正常顺序写清理代码即可。
Example:第 25 章 Listing 25-1 exit_handlers.c——atexit + on_exit 混用:
on_exit(onexitFunc, (void *) 10); /* 注册 #1 */
atexit(atexitFunc1); /* 注册 #2 */
atexit(atexitFunc2); /* 注册 #3 */
on_exit(onexitFunc, (void *) 20); /* 注册 #4 */
exit(2);
/* 调用顺序(反序):#4, #3, #2, #1 */
/* 输出:
on_exit function called: status=2, arg=20
atexit function 2 called
atexit function 1 called
on_exit function called: status=2, arg=10
*/
=== 25.4 fork + stdio buffer 交互陷阱
What:fork 时父进程 stdio buffer 中的数据会被复制到子进程——父子都调用 exit 时都 flush buffer,导致重复输出。
Why:stdio buffer 在用户态(libc 数据结构),fork 复制整个地址空间当然复制它;只有写到内核缓冲区(如 write)的数据不被复制。
How:经典陷阱(§25.4 Listing 25-2):
// 摘自《The Linux Programming Interface》第 25 章(Listing 25-2)
int main(void) {
printf("Hello world\n"); /* stdio,可能有 buffer */
write(STDOUT_FILENO, "Ciao\n", 5); /* 直写内核 */
fork(); /* fork 时 printf 的 buffer 还在 */
exit(EXIT_SUCCESS); /* 父子都 flush → 重复 */
}
运行结果:
-
输出到终端(line-buffered)——
Hello world\nCiao\n正常。-
输出到文件(block-buffered)——
Ciao\nHello world\nHello world\n——Hello world 出现两次,Ciao 在前。
-
解法(§25.4):
| 方法 | 代码 |
|---|---|
fork 前 fflush |
|
禁用 stdio buffer |
|
子用 _exit |
子进程 |
When:
-
fork + exec 模式——子用
_exit(最常用)。 -
fork 后父子都写 stdout——必须 fflush 或禁用 buffer。
-
调试输出——「为什么我重复打印了」通常是这个问题。
25.5 异常终止(信号)
What:进程被信号杀死时无 atexit 调用,无 stdio flush。
Why:理解这一点才能写出「信号也能清理」的程序。
How:
-
信号 handler 中调用 exit——能跑 atexit + flush stdio。
-
handler 中调用
_exit——立即终止;常用于 SIGTERM handler 配合清理后立即退。 -
SIGKILL 无法拦截——内核直接终止;atexit 不跑;stdio 不 flush。
When:daemon 接 SIGTERM——handler 中清理临时文件 + exit(EXIT_SUCCESS),让父 wait 看到正常状态。
三、关键图表
(本章无独立编号图表)
|
_exit vs exit 行为对比
|
|
exit status 字节布局
|
四、思维导图
mindmap
root((第 25 章 进程终止))
_exit vs exit
系统调用 库函数
atexit flush
fork 后子用 exit
status 低 8 位
退出状态
0 成功
非 0 失败
128 shell 冲突
EXIT SUCCESS FAILURE
atexit
注册无参函数
反序调用
fork 继承
exec 清空
信号不调
on_exit
glibc 扩展
传 status arg
非标准
可移植避免
fork stdio 陷阱
buffer 复制
父子 flush 重复
write 不复制
fflush 或 _exit
main return
return n 等于 exit n
C99 无 return 等于 exit 0
C89 未定义
异常终止
信号默认杀
无 atexit
handler 中 exit 可清理
SIGKILL 不可拦截
内核清理
关闭 fd
释放文件锁
SysV shm 减引用
孤儿进程组 SIGHUP
五、重点与易错点
-
_exit与exit的本质区别——前者是系统调用直接终止;后者是库函数会运行 atexit + 刷新 stdio;fork 后子必须用_exit。 -
status 只有低 8 位有效——传给父 wait 的只有 status & 0xFF;status > 128 与 shell
$? = 128 + signo冲突。 -
atexit 反序调用——先注册后调用;典型:先注册底层资源,后注册高层资源(这样高层先清理,底层还能用)。
-
atexit 在信号终止时不调用——必须在 handler 中调用
exit()才能跑清理;但exit不是 async-signal-safe——实际中 handler 应仅做标志 + 主程序检查后 exit。 -
atexit handler 中不能调用 exit——SUSv3 未定义;某些系统无限递归;调用
_exit则后续 handler 全部跳过。 -
fork 继承 atexit 注册——子会运行父的 atexit handler;这就是为什么 fork 后子必须用
_exit(避免子运行父注册的清理)。 -
exec 清空 atexit 注册——exec 后旧程序的清理函数不适用。
-
fork + stdio buffer 是常见 bug 源——重定向到文件时
printf内容重复;解法:fork 前 fflush / setvbuf 禁用 / 子用 _exit。 -
write 不被 fork 复制——直写内核 buffer;「write 早于 printf 输出」是 fork+stdio 陷阱的常见迷惑。
-
main 无 return 的退出:C89 未定义(Linux 取栈/寄存器随机值);C99 等价
exit(0);建议显式return 0。 -
return n 等价于 exit(n)——除非 main 局部变量被 setvbuf 等引用;return 触发未定义行为。
-
glibc atexit 用链表——无数量上限;SUSv3 要求 ≥ 32。
-
SIGKILL/SIGSTOP 默认动作不可改——atexit 不会跑;stdio 不 flush;不可拦截。
-
退出状态检测的 4 个 W 宏——WIFEXITED + WEXITSTATUS;WIFSIGNALED + WTERMSIG + WCOREDUMP;WIFSTOPPED + WSTOPSIG;WIFCONTINUED(2.6.10+)。
-
跨章衔接:第 24 章 fork 后子用 _exit;第 26 章父 wait 取 exit status;第 21 章信号 handler 中不能用 exit(非 async-signal-safe);第 13 章 stdio buffer 模式。