【Linux】进程退出

标签:Linux 进程控制 exit _exit 退出码 task_struct 进程等待

重点回答三个问题:父进程为什么要关心子进程怎么退出的?进程有哪几种退出方式,区别是什么?异常终止时退出信息存在哪里?


一、为什么要关心进程的退出情况

子进程也是进程,它由父进程创建,替父进程执行任务。任务执行得怎么样,父进程自然需要知道。进程退出可以分为三种场景:

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

前两种是代码跑完了 ,结果好坏由退出码 决定;第三种是代码没跑完 ,要看终止信号。

二、类比:函数的返回值与进程的退出码

函数调用中,被调函数通过返回值(x86 下放在 eax 寄存器)告诉调用者执行情况:

比如a()调用b() :

复制代码
 b() 中
mov eax, 1
ret

 a() 中
call b
mov 0x123[], eax    ; 从 eax 取到返回值

进程之间是同样的模式:

函数世界 进程世界
call fork + exec
return exit / return
返回值 退出码
调用者读取返回值 父进程 wait 读取退出码

三、进程的三种正常退出方式

正常终止 可以通过 echo $? 查看退出码:

  1. 从 main 返回
  2. 调用 exit
  3. 调用 _exit

异常退出:Ctrl+C 等信号终止。

3.1 从 main 返回

复制代码
int main()
{
    return 0;   // 0 表示成功
}

main 的返回值通常用来表明程序的执行情况:

  • return 0:代码跑完,结果正确
  • return 非0(1, 2, 3 ...):代码跑完,结果不正确,不同的值代表不同的出错原因,具体含义由程序员约定

return n 等价于 exit(n):因为调用 main 的运行时启动代码,会把 main 的返回值当作 exit 的参数。所以第 1 种本质上就是第 2 种。

3.2 exit(n)

复制代码
#include <stdlib.h>
void exit(int status);

exit 是 C 库函数,依次做三件事:

  1. 执行用户通过 atexit 或 on_exit 注册的清理函数 (此函数是用户自己写的)
  2. 关闭所有打开的流,刷新缓冲区,缓冲数据全部写出
  3. 调用 _exit

3.3 _exit(n)

复制代码
#include <unistd.h>
void _exit(int status);

_exit 是 系统调用 ,直接进入内核终止进程,不执行清理函数,不刷新 C 库的缓冲区 。status 定义了进程的终止状态,父进程通过 wait 获取。

说明:status 虽然是 int,但只有低 8 位 能被父进程使用。所以 _exit(-1) 时,终端 echo $? 看到的是 255;exit(257) 得到 1(257 % 256)。

3.4 exit 与 _exit 的关系

exit = 用户态收尾工作 + _exit

四、核心实验:为什么 exit 有输出而 _exit 没有

复制代码
// 实验一
int main()
{
    printf("hello");   // 注意:没有 \n
    exit(0);
}
// 运行结果:hello[root@localhost linux]#

// 实验二
int main()
{
    printf("hello");   // 注意:没有 \n
    _exit(0);
}
// 运行结果:[root@localhost linux]#      (什么都没有)

原因:数据有两个"仓库"

printf 并不是直接把字符写到屏幕,而是先写进 C 库维护的用户态缓冲区,之后才刷新到内核、再到屏幕:

复制代码
printf ──> [C库缓冲区(在进程自己的内存里)] 
刷新  ──> 内核 ──> 屏幕

终端的刷新策略是行缓冲 :遇到 \n、缓冲区满、主动 fflush、正常退出时才会刷新。

  • exit:退出前先刷新缓冲区,字符送出去了,所以能看到 hello
  • _exit:直接结束进程,缓冲区属于进程的内存,进程一没,里面的字符就丢了

这也说明:这个缓冲区是 C 库的,不是操作系统内核的 。如果缓冲区在内核里,_exit 也不会丢数据。

原因:

printf("hello"); _exit(0); 的结果是:hello 丢了。

什么情况下数据会随着进程结束而丢失?

  • 数据放在进程自己的内存里:进程一结束,这块内存被回收,数据就没了。
  • 数据放在内核的内存里:内核不会因为你的进程结束而消失,它会继续把数据送到屏幕(或写到文件),数据不会丢。

所以缓冲区在进程自己的内存 里**。**

一个实际坑:fork 之后的子进程

fork 会把父进程用户态缓冲区里未刷新的内容也复制给子进程 。(因为子进程没缺页的时候,mm_struct也会指向父进程的内存)。如果父子进程退出时都调用 exit,各自都会刷新一次,导致同一份内容被打印两遍。因此子进程在 fork 后需要提前退出(如 exec 失败后)时,通常用 _exit 而不是 exit。

五、谁在读取退出码:echo $?

复制代码
$ ./proc
$ echo $?
1
  • ./proc 这个进程的父进程是 bash
  • bash 通过等待子进程拿到退出码并保存下来
  • echo $? 打印的就是最近一个程序(进程)退出时的退出码

注意:$? 每次都是"上一条命令"的结果,所以连续执行两次 echo $?,第二次得到的是第一次 echo 本身的退出码 0。

六、退出码存在哪里:task_struct

进程退出时,退出信息被记录在进程的 PCB 中(Linux 下是 task_struct )。进程退出后会先变成僵尸状态 ,PCB 保留,等待父进程通过 wait/waitpid 来读取。这也是进程等待必要性的来源之一。

七、异常终止时 task_struct 里还有东西吗

既然异常终止"退出码无意义",是不是 task_struct 里什么都没有?

不是。 进程终止时,内核会把终止信息统一填入 task_struct 的 exit_code 字段,只是两种终止方式填的内容不同:

复制代码
正常终止:  | 15 ...... 8 | 7 | 6 ...... 0 |
           |   退出码     | 0 |     0      |

被信号杀死: | 15 ...... 8 | 7        | 6 ...... 0 |
            |    未用     | core dump | 终止信号   |
  • 正常退出(return / exit / _exit):退出码放在第 8~15 位,低 7 位为 0
  • 被信号杀死:终止信号编号放在低 7 位,第 7 位是 core dump 标志,高 8 位无参考价值

所以"异常时退出码无意义"的准确含义是:进程没机会执行 return/exit/_exit,没有程序员设定的退出码,此时应该看的是终止信号。

补充:task_struct 里还有一个 exit_signal 字段,它表示子进程退出时发给父进程的信号(通常是 SIGCHLD),和"子进程被什么信号杀死"不是一回事,不要混淆。

父进程如何区分两种终止方式?

wait/waitpid 的 status 参数取出的就是上面这个位图,不能当普通整数看:

复制代码
int status = 0;
waitpid(pid, &status, 0);

if ((status & 0x7F) == 0) {
    // 低 7 位为 0:正常退出
    printf("exit code: %d\n", (status >> 8) & 0xFF);
} else {
    // 低 7 位非 0:被信号终止
    printf("signal: %d\n", status & 0x7F);
}

标准库提供了等价的宏,实际开发中更推荐使用:

复制代码
if (WIFEXITED(status)) {
    printf("exit code: %d\n", WEXITSTATUS(status));
} else {
    printf("killed by signal: %d\n", WTERMSIG(status));
}

八、总结

属于 执行清理函数 刷新缓冲区 最终调用
return(main) 语言层面 是 是 exit
exit C 库函数 是 是 _exit
_exit 系统调用 否 否 内核
  1. 子进程是父进程派出去干活的,父进程需要知道结果
  2. 三种正常退出方式:main 返回 → 本质是 exit → 最终都落到 _exit
  3. exit 比 _exit 多做用户态收尾(清理函数、刷新缓冲区),缓冲区在 C 库而非内核
  4. 退出码 0 表示成功,非 0 表示失败,只有低 8 位有效,数值含义由程序约定
  5. bash 是命令的父进程,echo $? 读取的是上一个进程的退出信息
  6. 退出信息保存在 task_struct 中,进程退出后以僵尸状态等待父进程读取
  7. 正常退出看退出码 (第 8~15 位),异常退出看终止信号(低 7 位)
  8. 退出码($?)不是 errno;信号终止在 shell 中显示为 128 + 信号编号
相关推荐
水饺编程2 小时前
第2章:Linux 基础,mkdir 命令的简单入门
linux·c语言·汇编
自变量yjs2 小时前
Linux --- 线程控制
linux·运维·服务器
陌上花开缓缓归以2 小时前
linux 内核4.4 内存分配使用分析
linux·运维·服务器
欢乐的小猪3 小时前
Linux命令 chmod
linux·运维·服务器
Happy_Sharing3 小时前
Windows虚拟机安装ubuntu后,如何在虚拟机Ubuntu系统中访问Windows文件夹
linux·运维·ubuntu
码上有光4 小时前
Linux:信号的认识、信号的产生、信号的保存、信号的处理
android·linux·运维·信号
anew___4 小时前
《从零手写操作系统 (46):设备模型与devtmpfs——硬件资源的文件系统化》
linux·运维·服务器
Escalating_xu4 小时前
【Linux 网络编程】Windows 客户端访问 Linux:用 Winsock 跑通 UDP 回显
linux·网络·windows
无足鸟ICT4 小时前
【RHCA+】patch命令(更新补丁)
linux·运维·服务器