25 进程的终止 exit()、_exit()

_exit() 系统调用

虽然参数是 int 类型,但是只有低 8 位可用。而且由于终端中用信号值 + 128 表示进程因信号退出的状态码($?),所以最好也不要使用超过 127 的退出值。

exit() 函数

会做这几件事:

  1. 执行退出处理程序,这些程序是用 atexit()on_exit() 注册的。
  2. 刷新 stdio 流缓冲区。
  3. 调用 _exit()

由于在执行退出处理程序时,main() 函数已经退出,所以退出处理函数不能使用 main() 函数中定义的变量。

如果用户程序没有调用 _exit()exit(),并正常从 main() 函数中返回,则使用 main() 函数的返回值作为对 exit() 调用时传入的参数,这个过程是由运行时函数处理的。如果 main() 函数没有显式提供返回值,则在 C89 中属于未定义行为,在 C99/C++ 中会隐式返回 0。

退出处理程序

退出处理程序注册函数有两种,一个是 atexit(),一个是 on_exit()。后者是 glibc 提供的、非标准的,但是参数的信息更多。

退出处理程序的特点:

  1. 如果用户显式调用了系统调用 _exit(),那么就没有调用退出处理程序的过程。
  2. 进程能够注册多个退出处理程序,其调用顺序和注册顺序相反。SUSv3 规定一个进程应该至少能注册 32 个退出处理程序,允许的最大值也能通过 sysconf() 查到,在 glibc 中是近乎无限制的。
  3. 如果退出处理程序调用 exit(),则结果未定义。

通过 fork() 创建出的子进程会继承退出处理程序。但是 exec() 会将退出处理程序连同其他的代码段(即所有的代码段)替换掉,因此退出处理程序是不能跨越 exec() 的。

Tip

一个能够跨越 exec() 的例子是文件描述符。

fork() 和 stdio 的交互

没有交互。

由于 fork() 是系统调用,而 stdio 是用户代码,所以 fork() 肯定不会主动刷新用户缓冲区的。在 fork() 的时候,stdio 的用户缓冲区(比如 stdout)会被拷贝一份,这样可能会导致重复输出。解决办法有 2 种:

  1. fork() 之前将所有的 stdio 的流都用 fflush 刷新好。(我觉得这种做法更好一点。)
  2. 或者让一个进程(一般是父进程)用 exit() 退出,而其他进程用 _exit() 退出,从而不对缓冲区刷新。(但是如果缓冲区在程序运行中途还是刷新了,那就无法解决重复输出的问题。)