
🔥铅笔小新z:个人主页
🎬博客专栏:Linux学习
💫滴水不绝,可穿石;步履不休,能至渊。

一、进程终止
进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
1.1 进程退出的场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
main()函数的返回值,通常代表程序的执行情况:
- 结果正确返回
0 - 不正确返回
非0,即1、2、3、4、......,不同值代表不同的错误信息

在这段代码里,我们可以从打印信息中得知我们的proc程序正常运行了。
1.1.1 echo $?
但是如果将打印信息去掉,我们该如何得知程序是否正常运行呢?

那么我们就要学习一个新指令:echo $?(打印最近一个程序或进程退出时的退出码)
这个退出码也要写到当前进程的 task_struct 结构体中。
1.1.2 所有的错误信息表示

在上面的代码中,我们可以打印出所有的退出码对应的错误信息。
举例子:

可以明确当前目录中没有 hello.txt 这个文本文件,所以打印出 No such file or direcotry,对应着数字 2这个错误信息。
一个程序的执行结果由进程的退出码决定。
1.1.3 退出码无意义
c
#include<stdio.h>
int main()
{
int a = 10;
a /= 0;
return 89;
}
当我们运行上面的代码,再打印退出码会出现什么?

退出码是 136 ,而我们上面打印返回值信息的时候最多就只有134个,为什么会出现136呢?
因为异常的程序退出码无意义!
1.2 程序常见退出方法
- 从
main()函数返回 - 调用
exit() - _exit
1.2.1 调用exit()
任何地方调用exit()表示程序直接结束,不返回。并返回给父进程子进程的退出码。
c
#include<stdio.h>
#include<stdlib.h>
void func()
{
printf("func begin!\n");
exit(43);
printf("func end!\n");
}
int main()
{
func();
printf("main end!\n");
return 0;
}
程序运行结果:

退出码打印:

从运行结果和退出码两个信息中我们可以知道,程序执行到func()函数中的exit()语句就退出了,没有返回值。
1.2.2 调用_exit()
_exit()的作用是谁调用它就终止谁。
c
#include<stdio.h>
#include<unistd.h>
void func()
{
printf("func begin!\n");
_exit(4);
printf("func end!\n");
}
int main()
{
func();
printf("main end!\n");
return 0;
}
运行结果及退出码:

1.2.3 exit()和_exit()的区别
在讲区别之前要了解一个关于缓冲区的知识:
当我们用 printf("hello linux\n")时,hello linux是在缓冲区中的,\n就是将缓冲区中的内容刷新到显示器上,如果不刷新缓冲区,我们就无法看到hello linux这个语句。
我们先来看四组例子:
c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello linux!\n");
sleep(2);
exit(10);
return 0;
}
这段代码的运行结果是:

\n刷新缓冲区,所以我们是先看到打印信息然后等待两秒后进程结束。
c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello linux!");
sleep(2);
exit(10);
return 0;
}
这段代码的运行结果是:

因为没有\n刷新缓冲区,所以是等待两秒后,打印信息才出现。
c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello linux!\n");
sleep(2);
_exit(10);
return 0;
}
这段代码运行结果是:

具体情况同第一组。
c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello linux!");
sleep(2);
_exit(10);
return 0;
}
这段代码运行结果是:

为什么跟第二组的结果不一样呢,为什么没有\n刷新缓冲区还没有出现打印信息呢?
这就是exit()和_exit()的区别所在:
exit()是库函数,进程调用exit()时,进程退出时会进行缓冲区的回收。_exit()是系统调用,进程调用_exit()时,进程退出时不会进行缓冲区的回收。
库函数和系统调用之间的关系是上下级的关系,库函数会调用相关的系统调用来完成对应任务。只有操作系统才有资格终止进程,库函数是没有资格的,所以exit()在底层也要调用_exit()来终止进程。
我们之前谈的缓冲区一定不是操作系统内部的缓冲区,是库缓冲区(C语言提供的缓冲区)。
二、进程等待
2.1 进程等待的必要性
- 之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成'僵⼫进程'的问题,进⽽造成内存
泄漏。 - 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,"杀⼈不眨眼"的
kill -9也⽆能为⼒,因为谁也
没有办法杀死⼀个已经死去的进程。 - 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是
不对,或者是否正常退出。 - ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息。
2.2 进程等待的方法
2.2.1 wait()

简单的说,wait() 函数不仅是为了"收尸"(防止僵尸进程),更是为了让父进程拿到子进程的"遗言"(退出状态)。
wait()返回的是目标僵尸进程的pid。
参数传什么?
- 如果你不关心子进程是怎么死的: 直接传
NULL。pid_t rid = wait(NULL);
- 如果你想知道子进程的退出细节: 传一个
int变量的地址。int status;pid_t rid = wait(&status);
2.2.1.1 创建一个僵尸进程
c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
printf("我是一个子进程:pid : %d, ppid : %d\n", getpid(), getppid());
cnt--;
sleep(1);
}
exit(0);
}
sleep(100);
return 0;
}
-
子进程在 5 秒后执行
exit(0)变成了尸体。 -
此时父进程还卡在
sleep(100)里,根本没有调用wait()帮子进程收尸。

所以在上面图片中第五秒时,子进程的状态变成了Z(僵尸进程)。
2.2.1.1 利用wait()解决僵尸进程
c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
printf("我是一个子进程:pid : %d, ppid : %d\n", getpid(), getppid());
cnt--;
sleep(1);
}
exit(0);
}
pid_t rid = wait(NULL);
if (rid > 0)
{
printf("wait success, rid : %d\n",rid);
}
sleep(10);
return 0;
}

我们来详细解释代码逻辑:
-
0 - 5秒:
-
**子进程:**在跑
while循环,每秒打印一次。 -
父进程: 卡在
wait()这一行。它像是在车站等人的接机者,子进程不出来(不退出),父进程就哪里也不去,一直等在那里。
-
-
第 5 秒左右:
-
子进程执行完循环,调用
exit(0)。 -
子进程瞬间变成僵尸状态(Z)。
-
操作系统内核监测到子进程退出了,立刻"捅"了一下正在阻塞等待的父进程,并将子进程的退出信息交给父进程。
-
-
回收瞬间:
-
父进程的
wait()拿到了子进程的 PID,并从阻塞状态醒来。 -
一旦
wait()返回,子进程的残留信息(PCB)被彻底释放,僵尸状态消失。
-
-
5秒之后:
-
父进程打印
wait success。 -
父进程开始执行它最后的
sleep(10)。
-
2.2.2 waitpid()
c
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
相比于 wait 只有一个 status 参数,waitpid 多了首尾两个参数。我们一个个看:
2.2.2.1 参数
参数一: pid (你要等谁?)
-
pid > 0:(最常用) 只等待进程 ID 等于这个pid的子进程。精准打击,别人死活我不管。 -
pid == -1:(很常用) 等待任意一个子进程。此时它的行为和wait一模一样!实际上,Linux 底层里wait(&status)其实就是封装了waitpid(-1, &status, 0)。 -
补充(稍冷门):
pid == 0等待同一个进程组的任意子进程;pid < -1等待指定进程组的任意子进程。
参数二: status (拿到遗言)
完全和前面讲的 wait 一样,传入一个整型变量的地址,配合宏(如 WIFEXITED)来提取退出码或信号。
参数三: options (你要怎么等?)
-
0:阻塞等待(死等)。子进程不退出,父进程就卡在这里不走。 -
WNOHANG:非阻塞等待(轮询) 。HANG 的意思是挂起(卡住),NO HANG 就是不卡住。如果子进程还没死,waitpid会立刻返回 0,父进程可以继续往下执行自己的代码,干点别的事,过一会儿再回来问一句:"你死了没?"。
2.2.2.2 返回值 pid_t:
结合 WNOHANG,waitpid 的返回值有三种极其重要的状态:
-
返回值
> 0: 表示收集成功,返回的是死掉的子进程的 PID。 -
返回值
== 0: 只有在WNOHANG模式下才会出现! 表示子进程还在运行,暂时还没死,告诉父进程你可以先去忙别的。 -
返回值
< 0: 调用出错(比如你传的pid根本不是你的子进程,或者根本没有子进程了)。
2.2.2.3 深度解析status
c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 #include<stdlib.h>
6 #include<errno.h>
7 #include<string.h>
8 int main()
9 {
10 pid_t id = fork();
11 if (id == 0)
12 {
13 int cnt = 3;
14 while (cnt)
15 {
16 printf("我是一个子进程:pid : %d, ppid : %d\n", getpid(), get ppid());
17 cnt--;
18 sleep(1);
19 }
20 exit(1);
21 }
22 int status = 0;
23 pid_t rid = waitpid(id, &status, 0);
24 if (rid > 0)
25 {
26 printf("wait success, rid: %d, exit code: %d\n",rid, status);
27 }
28 else
29 {
30 printf("wait failed: %d: %s\n", errno, strerror(errno));
31 }
32 return 0;
33 }

这段代码最后的退出码(exit code)是256,我们设置子进程退出码不是1吗,这是为什么?
我们来讲讲状态码 status 的底层结构。
虽然 status 是一个 32 位的 int,但通常只使用它的低 16 位(0-15位)。根据子进程是"正常退出"还是"被信号终止",这 16 位的排布意义会有所不同:
-
正常退出:
如果子进程是通过return或exit()正常退出的:-
高 8 位 (Bits 8-15): 存储子进程的退出码 (Exit Code)(范围 0-255)。
-
低 8 位 (Bits 0-7): 全部为
0。
-
-
被信号终止:
如果子进程是因为收到某个信号(如SIGKILL,SIGSEGV)而被强行终止的:-
高 8 位 (Bits 8-15): 未使用。
-
第 7 位 (Bit 7): Core Dump 标志 。如果生成了核心转储文件,该位为
1。 -
低 7 位 (Bits 0-6): 存储导致进程终止的信号编号 (Signal Number)(范围 1-127)。
-
如果想要得到具体的退出码的话,就要右移8位然后(&0xFF)。

此时我们设置的是exit(1)。

此时我们设置的是exit(52)。
那么低8位是什么呢,我们来看看Linux中的信号:

当进程异常退出时,低7个比特位保存的是异常时对应的信号编号。(低8位是core dump标志)
- 没有异常时,低7个比特位都是0。
- 一旦低7个比特位不是0,则是异常退出,退出码无意义。
获取进程退出时的退出信号:
status & 0x7F
通过宏提取信息:
在实际编程中,我们不应该手动进行位运算(如 status >> 8),因为不同 Unix 系统的位排布可能有细微差别。POSIX 标准定义了专门的宏(包含在 <sys/wait.h> 中)来处理这些位运算:
提取整行退出码:
WIFEXITED(status):- 作用: 判断子进程是否正常退出。
- 底层操作: 检查低 7 位是否为 0。如果是,说明没有信号干预,返回 true(非 0)。
WEXITSTATUS(status):- 作用: 在确认为正常退出后,提取退出码。
- 底层操作: 将
status右移 8 位,并与0xFF取交集((status >> 8) & 0xFF),提取出第 8-15 位的值。
提取信号退出码:
WIFSIGNALED(status):- 作用: 判断子进程是否因为未捕获的信号而终止。
- 底层操作: 检查低 7 位是否大于 0 且低 8 位不等于 0x7F(0x7F 是进程暂停的标志)。如果是,返回 true。
WTERMSIG(status):- 作用: 在确认为信号终止后,提取信号编号。
- 底层操作: 将
status与0x7F取交集(status & 0x7F),屏蔽掉高位和第 7 位(core dump 位),只保留低 7 位。
2.2.2.4 阻塞与非阻塞等待
在 Linux 系统编程中,父进程创建子进程后,常常需要知道子进程什么时候结束、结果如何。waitpid 就是用来做这件事的。
它的第三个参数 options 决定了父进程在等待时的行为。最核心的区别就是:父进程是"死等"还是"抽空看一眼"。
2.2.2.4.1 阻塞等待 ------"不见不散"
阻塞等待是 waitpid 的默认行为。当你把第三个参数设置为 0 时,父进程就会进入阻塞状态。
- 工作原理: 父进程调用
waitpid后,如果指定的子进程还在运行,操作系统会将父进程挂起(从 CPU 的运行队列移出,放入等待队列),父进程进入睡眠状态。直到子进程退出并变成"僵尸进程(Zombie)",操作系统才会唤醒父进程去回收它。 - 返回值:
> 0:成功,返回退出的子进程的 PID。0:表示子进程还在运行,尚未退出。-1:出错(如查无此进程)。
- 优点: 父进程不会被卡住,可以一边执行自身任务,一边兼顾子进程的状态,实现并发。
- 缺点: 为了确保最终能回收子进程(防止产生孤儿 / 僵尸进程),通常需要结合 轮询 或者信号机制 来使用,代码复杂度较高。
典型代码范例(轮询方式):
c
// 父进程不断地用 WNOHANG 去"问"子进程有没有死
while (1)
{
pid_t wait_ret = waitpid(child_pid, &status, WNOHANG);
if (wait_ret == 0)
{
printf("子进程还在跑,父进程先去干点别的事...\n");
sleep(1); // 模拟父进程执行其他任务,避免 CPU 100% 空转
} else if (wait_ret == child_pid)
{
printf("子进程 %d 终于退出了,回收完毕!\n", wait_ret);
break; // 退出轮询
} else
{
perror("waitpid error");
break;
}
}
总结
进程控制(上)到这里就暂时结束了。
