第 25 章 进程终止 (Process Termination)

      +

      核心结论

      • 正常终止_exit(status)(系统调用)vs exit(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 输出」是常见迷惑的根源。

      本章主旨

      本章是「进程生命周期」四章的第二章——进程如何终止。读者应掌握:_exit vs exit 的本质区别(系统调用 vs 库函数,是否运行 atexit/flush stdio);退出状态字节的语义;atexit/on_exit 的注册与调用顺序;fork + stdio buffer 的常见陷阱;以及信号终止时不调用 atexit 的应对。理解这些才能写出「资源清理正确、无重复输出、退出状态可读」的健壮程序。

      一、核心概念

      本章围绕 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 $? = 128 + signo 冲突——shell 无法区分「子进程 exit(130)」与「子进程被 SIGINT 杀死」。

      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;现代编译 -std=c99-std=gnu99 让无 return 退出 = exit(0)。

      二、详细笔记

      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

      printf("…​"); fflush(stdout); fork();

      禁用 stdio buffer

      setvbuf(stdout, NULL, _IONBF, 0);

      子用 _exit

      子进程 _exit(EXIT_SUCCESS);

      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 exit

      类型

      系统调用

      库函数

      atexit handlers

      不运行

      反序运行

      stdio flush

      不刷新

      刷新

      父 wait 可见

      fork 后子适用

      信号 handler 中适用

      main return 等价

      -

      exit status 字节布局
      退出原因 status 高字节 status 低字节

      正常 exit

      0

      exit status (0-255)

      被信号杀

      信号号

      0(若 core 则 0x80)

      被 SIGSTOP

      0x7F

      stop 信号号

      四、思维导图

      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

      五、重点与易错点

      1. _exitexit 的本质区别——前者是系统调用直接终止;后者是库函数会运行 atexit + 刷新 stdio;fork 后子必须用 _exit

      2. status 只有低 8 位有效——传给父 wait 的只有 status & 0xFF;status > 128 与 shell $? = 128 + signo 冲突。

      3. atexit 反序调用——先注册后调用;典型:先注册底层资源,后注册高层资源(这样高层先清理,底层还能用)。

      4. atexit 在信号终止时不调用——必须在 handler 中调用 exit() 才能跑清理;但 exit 不是 async-signal-safe——实际中 handler 应仅做标志 + 主程序检查后 exit。

      5. atexit handler 中不能调用 exit——SUSv3 未定义;某些系统无限递归;调用 _exit 则后续 handler 全部跳过。

      6. fork 继承 atexit 注册——子会运行父的 atexit handler;这就是为什么 fork 后子必须用 _exit(避免子运行父注册的清理)。

      7. exec 清空 atexit 注册——exec 后旧程序的清理函数不适用。

      8. fork + stdio buffer 是常见 bug 源——重定向到文件时 printf 内容重复;解法:fork 前 fflush / setvbuf 禁用 / 子用 _exit。

      9. write 不被 fork 复制——直写内核 buffer;「write 早于 printf 输出」是 fork+stdio 陷阱的常见迷惑。

      10. main 无 return 的退出:C89 未定义(Linux 取栈/寄存器随机值);C99 等价 exit(0);建议显式 return 0

      11. return n 等价于 exit(n)——除非 main 局部变量被 setvbuf 等引用;return 触发未定义行为。

      12. glibc atexit 用链表——无数量上限;SUSv3 要求 ≥ 32。

      13. SIGKILL/SIGSTOP 默认动作不可改——atexit 不会跑;stdio 不 flush;不可拦截。

      14. 退出状态检测的 4 个 W 宏——WIFEXITED + WEXITSTATUS;WIFSIGNALED + WTERMSIG + WCOREDUMP;WIFSTOPPED + WSTOPSIG;WIFCONTINUED(2.6.10+)。

      15. 跨章衔接:第 24 章 fork 后子用 _exit;第 26 章父 wait 取 exit status;第 21 章信号 handler 中不能用 exit(非 async-signal-safe);第 13 章 stdio buffer 模式。