linux之进程终止与等待,一文讲透
一、进程终止的本质
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
注意这句话的两个层面:
- 内核数据结构:
task_struct、页表、文件描述符表……回收掉; - 数据和代码:占用的物理内存页、映射的共享库……释放掉。
所以"进程死了"不是"这个程序不跑了"这么简单,而是它占的一切资源被系统收回。
二、进程退出的三种场景
| 场景 | 说明 | 例子 |
|---|---|---|
| 代码跑完,结果正确 | 正常结束,退出码一般是 0 | ls 执行成功 |
| 代码跑完,结果不正确 | 正常结束,但退出码非 0 | 程序逻辑判断"找不到文件",exit(1) |
| 代码异常终止 | 没跑完就被打断 | 段错误、Ctrl+C、kill -9 |
关键区别:前两种是进程自己决定退出,第三种是被外力杀死。这两种情况在父进程 wait 时能区分(看 status 位图)。
三、进程的退出方法
3.1 正常终止(可以用echo $?查看退出码)
- 从
main返回(最常见); - 调用
exit; - 调用
_exit。
3.2 异常退出
Ctrl + C(信号SIGINT);kill -9(信号SIGKILL);- 段错误、除零等硬件异常触发的信号。
四、退出码(exit code)
退出码(退出状态)可以告诉我们最后一次执行的命令的状态。程序返回退出代码 0 表示执行成功;1 或 0 以外的任何代码都被视为不成功。
查看方式:
./a.out echo $? # 上一条第命令的退出码
Linux Shell 中的主要退出码(课件):
| 退出码 | 含义 |
|---|---|
| 0 | 命令执行无误,理想状态 |
| 1 | 可以解释为"不被允许的操作"。例如没有 sudo 权限却执行 yum;再例如除以 0,命令 let a=1/0 |
| 130(128+2) | 被 SIGINT / ^C 终止 |
| 143(128+15) | 被 SIGTERM 终止 |
规律:信号终止的退出码 = 128 + 信号编号。
另外可以用 strerror 把错误码翻译成人类可读的描述:
#include <string.h>
#include <stdio.h>
printf("%s\n", strerror(1)); // 输出 "Operation not permitted" 之类4.1_exit函数
#include <unistd.h> void _exit(int status);
参数 status 定义了进程的终止状态,父进程通过 wait 来获取该值。
重点细节:
虽然 status 是 int,但仅有低 8 位可以被父进程所用。所以 _exit(-1) 时,在终端执行 echo $? 发现返回值是 255(-1 的补码低 8 位 = 0xFF = 255)。
4.2exit函数
#include <unistd.h> void exit(int status);
exit 最后也会调用 _exit,但在调用 _exit 之前,还做了其他工作:
- 执行用户通过
atexit或on_exit定义的清理函数; - 关闭所有打开的流,所有的缓存数据均被写入;
- 调用
_exit。
4.3exitvs_exit:一个能看见输出,一个看不见
这是最经典的区别演示。
int main()
{
printf("hello"); // 注意:没有 \n
exit(0);
}运行结果:
hello[root@localhost linux]#
int main()
{
printf("hello"); // 同样没有 \n
_exit(0);
}运行结果:
[root@localhost linux]#
什么也没打印出来!
原因链:
printf("hello")没有\n,且 stdout 是行缓冲(终端下),所以"hello" 只待在用户态缓冲区里,还没真正write到内核;exit会刷新所有流(把缓冲区写出去),所以你看到了 hello;_exit不刷新缓冲区,直接进入内核销毁进程,缓冲区里的数据随进程一起蒸发了,所以你什么也看不到。
这也回头解释了第 2 篇那个坑:printf 不带 \n 就 fork,缓冲区被复制,父子各刷一次,于是"打印了两遍"。理解缓冲区,很多怪现象就不怪了。
4.4return退出
return 是一种更常见的退出方法。执行 return n 等同于执行 exit(n),因为调用 main 的运行时函数会把 main 的返回值当作 exit 的参数。
所以这三种写法等价:
int main() { return 0; } // 从 main 返回
int main() { exit(0); } // 显式调用
int main() { _exit(0); } // 极端:不刷缓冲区五、进程等待:为什么必须等
5.1 四条必要性(课件原话)
不管不顾会造成僵尸进程,进而内存泄漏。 子进程退出,父进程如果不管,就可能造成僵尸问题。
僵尸"刀枪不入"。 进程一旦变成僵尸状态,kill -9 也无能为力,因为你没有办法杀死一个已经死去的进程。
父进程需要知道子进程干得怎么样。 父进程派给子进程的任务完成得如何?结果对不对?是否正常退出?
回收资源需要主动动作。 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
一句话:wait 是杀死僵尸的唯一正道。
5.2wait
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
返回值:成功返回被等待进程的 pid,失败返回 -1。
参数:status 是输出型参数,获取子进程退出状态;不关心可以传 NULL。
特性:
- 等待任意一个子进程;
- 阻塞式,子进程不退出就一直卡在这里。
5.3waitpid(更强大)
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
- 正常返回:收集到的子进程 ID;
- 如果设置了
WNOHANG,且没有已退出的子进程可收集:返回 0; - 出错:返回 -1,
errno会被设置。
参数 pid:
| 值 | 含义 |
|---|---|
| -1 | 等待任意一个子进程(与 wait 等效) |
| >0 | 等待指定 pid 的子进程 |
参数 status(输出型):
| 宏 | 作用 |
|---|---|
| WIFEXITED(status) | 若为正常终止子进程返回的状态,则为真(判断是否正常退出) |
| WEXITSTATUS(status) | 若 WIFEXITED 非零,提取子进程退出码 |
参数 options:
0:默认,阻塞等待;WNOHANG:若指定的子进程没有结束,waitpid()返回 0,不予等待(非阻塞)。
5.4 三种时机下的行为
| 情况 | wait / waitpid 的行为 |
|---|---|
| 子进程已经退出 | 立即返回,释放资源,拿到退出信息 |
| 子进程存在且正在运行 | 可能阻塞(阻塞模式下) |
| 不存在该子进程 | 立即出错返回 |
第三种很关键:没有子进程时 wait 会立刻返回 -1(errno = ECHILD),不会傻等。
六、status 不是整数,是位图
status 不能简单地当作整形来看待,可以当作位图来看待(只研究低 16 比特位)。
低 16 位的布局:
15 8 7 0
┌────────────┬─────────┐
│ 退出状态码 │ 终止信号 │
│ (exit code) │ (signal)│
└────────────┴─────────┘
↑ ↑
(st >> 8) & 0xFF st & 0x7F- 高 8 位(bits 8-15):正常退出时的退出码;
- 低 7 位(bits 0-6):导致终止的信号编号,为 0 表示正常终止;
- bit 7:core dump 标志位。
所以:
(status & 0x7F) == 0→ 正常退出,退出码 =(status >> 8) & 0xFF;(status & 0x7F) != 0→ 异常终止,信号 =status & 0x7F。
6.1 手动解析 status
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
if ((pid = fork()) == -1)
perror("fork"), exit(1);
if (pid == 0) {
sleep(20);
exit(10); // 子进程 20 秒后以 10 退出
} else {
int st;
int ret = wait(&st);
if (ret > 0 && (st & 0x7F) == 0) { // 正常退出
printf("child exit code: %d\n", (st >> 8) & 0xFF);
} else if (ret > 0) { // 异常退出
printf("sig code : %d\n", st & 0x7F);
}
}
return 0;
}测试结果(课件):
# 等 20 秒自然退出 child exit code: 10 # 在另一个终端 kill -9 掉子进程 sig code : 9
这就是"父进程怎么区分'任务成功'和'被外面杀了'"的标准答案。
七、阻塞等待 vs 非阻塞等待
7.1 阻塞等待(options = 0)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0) {
printf("%s fork error\n", __FUNCTION__);
return 1;
} else if (pid == 0) { // child
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(257);
} else { // parent
int status = 0;
pid_t ret = waitpid(-1, &status, 0); // 阻塞式等待,等待 5 秒
printf("this is test for wait\n");
if (WIFEXITED(status) && ret == pid) {
printf("wait child 5s success, child return code is :%d.\n",
WEXITSTATUS(status));
} else {
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}运行结果:
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is :1.
注意两点:
this is test for wait是5 秒之后才打印的——父进程 真的被卡住了;- 子进程
exit(257),但打印出来是 1。因为退出码只用低 8 位:257 & 0xFF = 1。这又一次印证了第 4.1 节那条规则。
7.2 非阻塞等待(WNOHANG)
父进程不傻等,而是"一边等一边干别的":
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <vector>
typedef void (*handler_t)(); // 函数指针类型
std::vector<handler_t> handlers; // 函数指针数组
void fun_one() { printf("这是一个临时任务1\n"); }
void fun_two() { printf("这是一个临时任务2\n"); }
void Load() {
handlers.push_back(fun_one);
handlers.push_back(fun_two);
}
// 父进程在等待期间可以干的"其他事"
void handler() {
if (handlers.empty()) Load();
for (auto iter : handlers) iter();
}
int main() {
pid_t pid = fork();
if (pid < 0) {
printf("%s fork error\n", __FUNCTION__);
return 1;
} else if (pid == 0) { // child
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(1);
} else { // parent
int status = 0;
pid_t ret = 0;
do {
ret = waitpid(-1, &status, WNOHANG); // 非阻塞式等待
if (ret == 0) {
printf("child is running\n");
}
handler(); // 干点别的
} while (ret == 0);
if (WIFEXITED(status) && ret == pid) {
printf("wait child 5s success, child return code is :%d.\n",
WEXITSTATUS(status));
} else {
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}运行效果:
child is run, pid is : 45111
child is running
这是一个临时任务1
这是一个临时任务2
child is running
...(反复若干次)
wait child 5s success, child return code is :1.
这种"轮询 + 顺便干活"的模式,就是所有单线程事件循环(包括 Nginx、Redis 的雏形)的思维方式。
注意 WNOHANG 模式下 CPU 会被吃满——真实工程里要配合 sleep、select、epoll 来等,而不是死循环轮询。
八、把僵尸问题彻底解决
回到第 3 篇。当一个父进程不断 fork,正确的收尾长这样:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0) {
// 子进程干活
sleep(2);
exit(42);
}
// 父进程必须回收,否则子进程变僵尸
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret == id && WIFEXITED(status)) {
printf("子进程正常结束,退出码 = %d\n", WEXITSTATUS(status));
}
sleep(1); // 给演示留点时间
return 0;
}三种"避免僵尸"的策略:
| 策略 | 做法 | 适用 |
|---|---|---|
| 主动 wait(推荐) | 父进程 wait/waitpid 回收 | 大多数场景 |
| 忽略 SIGCHLD | signal(SIGCHLD, SIG_IGN),子进程退出自动回收 | 不关心退出状态 |
| 让父进程先退出 | 子进程变孤儿,由 init 收尸(double fork) | 守护进程 |
waitpid 只回收指定 pid 或任意一个子进程;有多个子进程时要循环回收,否则剩下的还是会变僵尸。
九、易错点
| 误解 | 正确理解 |
|---|---|
| status 就是退出码 | status 是位图,退出码在高 8 位,信号在低 7 位 |
| exit(257) 父进程能拿到 257 | 只能拿到 1(低 8 位) |
| _exit 和 exit 一样 | _exit 不刷缓冲区、不跑 atexit |
| wait 会等所有子进程 | 只等任意一个,要全收得循环 |
| 没有子进程时 wait 会一直等 | 立即返回 -1(ECHILD),不会阻塞 |
| kill -9 能杀僵尸 | 不能,僵尸已死,只能由父进程 wait 回收 |
总结
- 进程终止 = 释放内核数据结构 + 代码数据;三种场景:正常对、正常错、异常终止。
- 退出方法:
return/exit/_exit/ 信号。exit会刷缓冲区、跑清理函数;_exit直接走。 - 退出码只有 低 8 位有效(
_exit(-1)→$?= 255);信号终止时$?= 128 + 信号号。 - 必须等待:不然子进程变僵尸,
kill -9也救不了;父进程也要拿子进程的结果。 wait等任意一个;waitpid可选指定 pid、可选WNOHANG非阻塞。status是位图:(st & 0x7F) == 0表示正常退出,退出码 =(st >> 8) & 0xFF;否则低 7 位是终止信号。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
相关文章
解决Ubuntu系统中“Temporary Failure in Name Re
在Ubuntu系统中,"TemporaryFailureinNameResolution"错误通常是DNS查询失败导致的,可能由DNS配置错误、网络连接问题或防火墙设置引起,解决方法包括检查DNS配置、确保网络连接、重启网络服务、检查防火墙设置以及使用systemd-resolved管理DNS配置2026-05-05


最新评论