25 进程的终止 exit()、_exit()
_exit()
系统调用
虽然参数是 int 类型,但是只有低 8 位可用。而且由于终端中用信号值 + 128 表示进程因信号退出的状态码($?
),所以最好也不要使用超过 127 的退出值。
exit()
函数
会做这几件事:
- 执行退出处理程序,这些程序是用
atexit()
和on_exit()
注册的。 - 刷新 stdio 流缓冲区。
- 调用
_exit()
。
由于在执行退出处理程序时,main()
函数已经退出,所以退出处理函数不能使用 main()
函数中定义的变量。
如果用户程序没有调用 _exit()
和 exit()
,并正常从 main()
函数中返回,则使用 main()
函数的返回值作为对 exit()
调用时传入的参数,这个过程是由运行时函数处理的。如果 main()
函数没有显式提供返回值,则在 C89 中属于未定义行为,在 C99/C++ 中会隐式返回 0。
退出处理程序
退出处理程序注册函数有两种,一个是 atexit()
,一个是 on_exit()
。后者是 glibc 提供的、非标准的,但是参数的信息更多。
退出处理程序的特点:
- 如果用户显式调用了系统调用
_exit()
,那么就没有调用退出处理程序的过程。 - 进程能够注册多个退出处理程序,其调用顺序和注册顺序相反。SUSv3 规定一个进程应该至少能注册 32 个退出处理程序,允许的最大值也能通过
sysconf()
查到,在 glibc 中是近乎无限制的。 - 如果退出处理程序调用
exit()
,则结果未定义。
通过 fork()
创建出的子进程会继承退出处理程序。但是 exec()
会将退出处理程序连同其他的代码段(即所有的代码段)替换掉,因此退出处理程序是不能跨越 exec()
的。
Tip
一个能够跨越 exec()
的例子是文件描述符。
fork()
和 stdio 的交互
没有交互。
由于 fork()
是系统调用,而 stdio 是用户代码,所以 fork()
肯定不会主动刷新用户缓冲区的。在 fork()
的时候,stdio 的用户缓冲区(比如 stdout)会被拷贝一份,这样可能会导致重复输出。解决办法有 2 种:
- 在
fork()
之前将所有的 stdio 的流都用fflush
刷新好。(我觉得这种做法更好一点。) - 或者让一个进程(一般是父进程)用
exit()
退出,而其他进程用_exit()
退出,从而不对缓冲区刷新。(但是如果缓冲区在程序运行中途还是刷新了,那就无法解决重复输出的问题。)