017 进程控制 —— 终止进程
🦄 个人主页: 小米里的大麦-CSDN博客
🎏 所属专栏: Linux_小米里的大麦的博客-CSDN博客
🎁 GitHub主页: 小米里的大麦的 GitHub
⚙️ 操作环境: Visual Studio 2022
文章目录
- 进程控制 —— 终止进程
- 一、进程退出场景
- 二、进程的退出码
- 1. 定义
- 2. 为什么以 `0` 表示代码执行成功,以 `非0` 表示代码执行错误?
- 3. `errno` 常量和 `strerror` 函数(牵扯信号,初步了解)
- 三、进程常见退出方法
- 1. `exit()` 函数
- 2. `_exit()` 函数
- 3. `return` 退出
- 三、关键区别对比
- 四、代码示例分析
- 1. 父子进程退出行为差异
- 2. `atexit()` 注册清理函数
- 五、进程终止后的状态
- 小结
进程控制 —— 终止进程
一、进程退出场景
从我们的视角来看进程终止的场景一般就是以下三种:
- 代码运行完毕,结果正确(一般不关心)。
- 代码运行完毕,结果不正确。
- 代码异常终止。
但是进程也可能因多种原因终止,比如:
场景 | 说明 |
---|---|
正常完成任务 | 程序执行完所有代码逻辑后退出 |
异常错误终止 | 遇到不可恢复的错误(如段错误、除零错误) |
主动终止 | 调用退出函数(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 $?
命令查看最近一次进程退出的退出码信息。
例如,对于下面这个简单的代码:
#include <stdio.h>
int main()
{printf("Hello, World!\n");return 0;
}
代码运行结束后,我们可以使用 echo $?
查看该进程的进程退出码:
这里进程退出码显示 0
便是可以确定程序顺利执行完毕了。
实际上 Linux
中的 ls
、pwd
等命令都是可执行程序,使用这些命令后我们也可以查看其对应的退出码。
注意: 命令执行错误后,其退出码就是非 0
的数字,该数字具体代表某一错误信息。 退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。
2. 为什么以 0
表示代码执行成功,以 非0
表示代码执行错误?
因为代码执行成功只有一种情况,成功了就是成功了,而代码执行错误却有多种原因,例如内存空间不足、非法访问以及栈溢出等等,我们就可以用这些 非0
的数字分别表示代码执行错误的原因。
3. errno
常量和 strerror
函数(牵扯信号,初步了解)
查看信号对应的退出码
信号终止的进程退出码为
128 + 信号编号
。可通过命令kill -l
(列出所有信号及其编号)查看信号列表:
上面我们提到我们可以通过不同的退出码来代表不同的错误信息,那么不同的退出码究竟各自代表什么信息呢?我们可以通过 strerror
函数来查看, 比如我们来看一下退出码 0
到 10
所代表的信息:
#include<stdio.h>
#include<string.h>
int main()
{for(int i=0;i<=10;i++){printf("%d: %s\n",i,strerror(i));}return 0;
}
运行结果:
进程在退出是会有退出码,我们可以通过 echo
来查看退出码,那我们如何获取呢?
C/C++中其实还定义了一个叫 errno
的常量来记录错误码,所以我们就可以将 errno
常量与 strerror
函数结合使用,用 errno
来记录进程的错误码,然后传给 strerror
函数得到错误信息,比如下面的例子:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<errno.h> //注意要带好头文件
int main()
{int ret = 0;char* p = (char*)malloc(1000 * 1000 * 1000 * 4); //这个扩容肯定会出错的,因为扩容空间太大了if (p == NULL){printf("mallo error, %d:%s\n", errno, strerror(errno)); //errno会记录错误码,将它传到strerror中就可以得到错误信息ret = errno; //将错误码作为返回值返回,从而让父进程得到返回信息}else{printf("malloc success\n");}return ret;
}
三、进程常见退出方法
1. exit()
函数
-
头文件:
#include <stdlib.h>
-
行为:
- 执行标准清理操作(刷新缓冲区、关闭文件描述符等)。
- 调用通过
atexit()
注册的函数。 - 返回状态码给父进程(通过
wait()
获取)。
-
示例:
#include <stdlib.h> 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()
后需要快速退出的场景。
-
示例:
int main() {printf("Hello, World!\n");_exit(0); // 立刻退出,状态码为0printf("这一行将不会被打印。.\n"); }
3. return
退出
-
行为:
- 在
main()
函数中,return
等效于调用exit()
。 - 在其他函数中,
return
仅退出当前函数。
- 在
-
示例:
int main() {return 42; // 等效于 exit(42) }
[!WARNING]
警告:下面的程序会源源不断的创建僵尸进程,直至将系统资源耗尽!请谨慎使用!实测:在虚拟机中运行 20 秒不到,系统直接卡死。
#include <unistd.h> #include <sys/types.h>int main() {while (1){if (fork() == 0){_exit(0); // 子进程立即退出,成为僵尸进程}}return 0; }
三、关键区别对比
方法 | 是否刷新缓冲区 | 是否调用 atexit() | 适用场景 |
---|---|---|---|
exit() | ✅ 是 | ✅ 是 | 正常退出,需清理资源 |
_exit() | ❌ 否 | ❌ 否 | 子进程快速退出或错误紧急终止 |
return | ✅ 是(仅 main ) | ✅ 是(仅 main ) | main() 函数中的简洁退出方式 |
四、代码示例分析
1. 父子进程退出行为差异
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{printf("Start (PID:%d)\n", getpid()); // 注意:无换行,缓冲区未刷新if (fork() == 0) // 子进程{printf("Child exiting\n");exit(0); // 刷新缓冲区并退出}else // 父进程{sleep(1);printf("Parent exiting\n");_exit(0); // 不刷新缓冲区}
}
输出结果:
# 由于 printf 未刷新缓冲区,子进程继承了未刷新的缓冲区内容,导致重复输出:
Start (PID:123)
Child exiting
Start (PID:123) // 父进程的缓冲区未刷新,被子进程继承后输出
Parent exiting
2. atexit()
注册清理函数
#include <stdlib.h>
#include <stdio.h>void cleanup()
{printf("Cleanup done!\n");
}int main()
{atexit(cleanup); // 注册清理函数printf("Main running\n");exit(0); // 会调用 cleanup()
}
输出:
Main running
Cleanup done!
五、进程终止后的状态
-
僵尸进程(Zombie):
- 进程已终止,但父进程未通过
wait()
回收其资源。 - 解决方案:
- 父进程调用
wait()
或waitpid()
。 - 忽略
SIGCHLD
信号:signal(SIGCHLD, SIG_IGN)
,注意:在某些系统中,忽略SIGCHLD
会自动回收子进程,但并非所有系统都支持这一行为!
- 父进程调用
- 进程已终止,但父进程未通过
-
孤儿进程:
- 父进程先退出,子进程被
init
(PID = 1)接管。 - 无害,
init
会自动回收孤儿进程。
- 父进程先退出,子进程被
小结
exit()
(优先使用 ):安全退出,适合大多数场景,确保资源正确释放。_exit()
:紧急退出,跳过清理。子进程慎用,除非明确需要跳过清理。return
:仅在main()
中等效于exit()
。- 进程管理:正确处理父子进程关系,避免资源泄漏。