017 进程控制 —— 终止进程

017 进程控制 —— 终止进程
小米里的大麦进程控制 —— 终止进程
一、进程退出场景
从我们的视角来看进程终止的场景一般就是以下三种:
- 代码运行完毕,结果正确(一般不关心)。
- 代码运行完毕,结果不正确。
- 代码异常终止。
但是进程也可能因多种原因终止,比如:
场景 | 说明 |
---|---|
正常完成任务 | 程序执行完所有代码逻辑后退出 |
异常错误终止 | 遇到不可恢复的错误(如段错误、除零错误) |
主动终止 | 调用退出函数(exit() /_exit() )或通过 return 退出 |
被动终止 | 收到终止信号(如 SIGKILL 、SIGTERM ) |
被父进程杀死 | 父进程调用 kill() 函数发送信号,使子进程退出。 |
看进程终止的角度、进程终止的原因等不同方面来解释进程的终止,虽然说法上不同,但也大同小异,我们只需要记住一点:
所有进程的退出方式都可以归为两大类:正常退出 和 异常退出,而主动或被动,是从行为发起方角度来分的。进程出现异常,本质是我们的进程收到了对应的信号!!
二、进程的退出码
我们都知道
main
函数是代码的入口,但实际上main
函数只是用户级别代码的入口,main
函数也是被其他函数调用的,也就是说main
函数是间接性被操作系统所调用的。既然
main
函数是间接性被操作系统所调用的,那么当main
函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main
函数的返回值返回,我们一般以0
表示代码成功执行完毕,以非0
表示代码执行过程中出现错误,这就是为什么我们都在main
函数的最后返回0
的原因。
1. 定义
进程退出码:是进程终止时向 操作系统 返回的一个 整数值,用于标识该进程是否 成功完成任务 或 出现了错误。
退出码 | 含义说明 |
---|---|
0 |
表示进程 成功 退出(Success) |
1~255 |
表示进程 异常 或 错误 退出(Failure) |
其它值 | 可以由程序自定义(常用于表示不同类型的错误) |
当我们的代码运行起来就变成了进程,当进程结束后 main
函数的返回值实际上就是该进程的进程退出码,我们可以使用 echo $?
命令查看最近一次进程退出的退出码信息。
例如,对于下面这个简单的代码:
1 |
|
代码运行结束后,我们可以使用 echo $?
查看该进程的进程退出码:
这里进程退出码显示 0
便是可以确定程序顺利执行完毕了。
实际上 Linux
中的 ls
、pwd
等命令都是可执行程序,使用这些命令后我们也可以查看其对应的退出码。
注意: 命令执行错误后,其退出码就是非 0
的数字,该数字具体代表某一错误信息。 退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。
2. 为什么以 0
表示代码执行成功,以 非0
表示代码执行错误?
因为代码执行成功只有一种情况,成功了就是成功了,而代码执行错误却有多种原因,例如内存空间不足、非法访问以及栈溢出等等,我们就可以用这些 非0
的数字分别表示代码执行错误的原因。
3. errno
常量和 strerror
函数(牵扯信号,初步了解)
查看信号对应的退出码
信号终止的进程退出码为
128 + 信号编号
。可通过命令kill -l
(列出所有信号及其编号)查看信号列表:
上面我们提到我们可以通过不同的退出码来代表不同的错误信息,那么不同的退出码究竟各自代表什么信息呢?我们可以通过 strerror
函数来查看, 比如我们来看一下退出码 0
到 10
所代表的信息:
1 |
|
运行结果:
进程在退出是会有退出码,我们可以通过 echo
来查看退出码,那我们如何获取呢?
C/C++中其实还定义了一个叫 errno
的常量来记录错误码,所以我们就可以将 errno
常量与 strerror
函数结合使用,用 errno
来记录进程的错误码,然后传给 strerror
函数得到错误信息,比如下面的例子:
1 |
|
三、进程常见退出方法
1. exit()
函数
头文件:
#include <stdlib.h>
行为:
- 执行标准清理操作(刷新缓冲区、关闭文件描述符等)。
- 调用通过
atexit()
注册的函数。 - 返回状态码给父进程(通过
wait()
获取)。
示例:
1
2
3
4
5
int main()
{
exit(3); // 设置退出码为 3
}
2. _exit()
函数
头文件:
#include <unistd.h>
(函数:void _exit(int status);
)行为:
status
定义了进程的终止状态,父进程通过wait
来获取该值,虽然status
是int
,但是仅有低8
位可以被父进程所用。所以exit(-1)
时,在终端执行echo $?
发现返回值是255
。- 立即终止 进程(系统调用级别的退出),不执行任何清理(缓冲区不刷新、
atexit()
函数不调用)。 - 适用于子进程在
fork()
后需要快速退出的场景。
示例:
1
2
3
4
5
6int main()
{
printf("Hello, World!\n");
_exit(0); // 立刻退出,状态码为0
printf("这一行将不会被打印。.\n");
}
3. return
退出
行为:
- 在
main()
函数中,return
等效于调用exit()
。 - 在其他函数中,
return
仅退出当前函数。
- 在
示例:
1
2
3
4int main()
{
return 42; // 等效于 exit(42)
}
[!WARNING]
警告:下面的程序会源源不断的创建僵尸进程,直至将系统资源耗尽!请谨慎使用!实测:在虚拟机中运行 20 秒不到,系统直接卡死。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
while (1)
{
if (fork() == 0)
{
_exit(0); // 子进程立即退出,成为僵尸进程
}
}
return 0;
}
三、关键区别对比
方法 | 是否刷新缓冲区 | 是否调用 atexit() |
适用场景 |
---|---|---|---|
exit() |
✅ 是 | ✅ 是 | 正常退出,需清理资源 |
_exit() |
❌ 否 | ❌ 否 | 子进程快速退出或错误紧急终止 |
return |
✅ 是(仅 main ) |
✅ 是(仅 main ) |
main() 函数中的简洁退出方式 |
四、代码示例分析
1. 父子进程退出行为差异
1 |
|
输出结果:
1 | # 由于 printf 未刷新缓冲区,子进程继承了未刷新的缓冲区内容,导致重复输出: |
2. atexit()
注册清理函数
1 |
|
输出:
1 | Main running |
五、进程终止后的状态
僵尸进程(Zombie):
- 进程已终止,但父进程未通过
wait()
回收其资源。 - 解决方案:
- 父进程调用
wait()
或waitpid()
。 - 忽略
SIGCHLD
信号:signal(SIGCHLD, SIG_IGN)
,注意:在某些系统中,忽略SIGCHLD
会自动回收子进程,但并非所有系统都支持这一行为!
- 父进程调用
- 进程已终止,但父进程未通过
孤儿进程:
- 父进程先退出,子进程被
init
(PID = 1)接管。 - 无害,
init
会自动回收孤儿进程。
- 父进程先退出,子进程被
小结
exit()
(优先使用 ):安全退出,适合大多数场景,确保资源正确释放。_exit()
:紧急退出,跳过清理。子进程慎用,除非明确需要跳过清理。return
:仅在main()
中等效于exit()
。- 进程管理:正确处理父子进程关系,避免资源泄漏。