017 进程控制 —— 终止进程

进程控制 —— 终止进程

一、进程退出场景

从我们的视角来看进程终止的场景一般就是以下三种:

  1. 代码运行完毕,结果正确(一般不关心)。
  2. 代码运行完毕,结果不正确。
  3. 代码异常终止。

但是进程也可能因多种原因终止,比如:

场景 说明
正常完成任务 程序执行完所有代码逻辑后退出
异常错误终止 遇到不可恢复的错误(如段错误、除零错误)
主动终止 调用退出函数(exit()/_exit())或通过 return 退出
被动终止 收到终止信号(如 SIGKILLSIGTERM
被父进程杀死 父进程调用 kill() 函数发送信号,使子进程退出。

看进程终止的角度、进程终止的原因等不同方面来解释进程的终止,虽然说法上不同,但也大同小异,我们只需要记住一点:

所有进程的退出方式都可以归为两大类:正常退出异常退出,而主动或被动,是从行为发起方角度来分的。进程出现异常,本质是我们的进程收到了对应的信号!!


二、进程的退出码

我们都知道 main 函数是代码的入口,但实际上 main 函数只是用户级别代码的入口,main 函数也是被其他函数调用的,也就是说 main 函数是间接性被操作系统所调用的。

既然 main 函数是间接性被操作系统所调用的,那么当 main 函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为 main 函数的返回值返回,我们一般以 0 表示代码成功执行完毕,以非 0 表示代码执行过程中出现错误,这就是为什么我们都在 main 函数的最后返回 0 的原因。

1. 定义

进程退出码:是进程终止时向 操作系统 返回的一个 整数值,用于标识该进程是否 成功完成任务出现了错误

退出码 含义说明
0 表示进程 成功 退出(Success)
1~255 表示进程 异常错误 退出(Failure)
其它值 可以由程序自定义(常用于表示不同类型的错误)

当我们的代码运行起来就变成了进程,当进程结束后 main 函数的返回值实际上就是该进程的进程退出码,我们可以使用 echo $? 命令查看最近一次进程退出的退出码信息。

例如,对于下面这个简单的代码:

1
2
3
4
5
6
#include <stdio.h>
int main()
{
printf("Hello, World!\n");
return 0;
}

代码运行结束后,我们可以使用 echo $? 查看该进程的进程退出码:

image-20250406133322513

这里进程退出码显示 0 便是可以确定程序顺利执行完毕了。

实际上 Linux 中的 lspwd 等命令都是可执行程序,使用这些命令后我们也可以查看其对应的退出码。

image-20250406133818700

注意: 命令执行错误后,其退出码就是非 0 的数字,该数字具体代表某一错误信息。 退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。

2. 为什么以 0 表示代码执行成功,以 非0 表示代码执行错误?

因为代码执行成功只有一种情况,成功了就是成功了,而代码执行错误却有多种原因,例如内存空间不足、非法访问以及栈溢出等等,我们就可以用这些 非0 的数字分别表示代码执行错误的原因。

3. errno 常量和 strerror 函数(牵扯信号,初步了解)

查看信号对应的退出码

信号终止的进程退出码为 128 + 信号编号。可通过命令 kill -l(列出所有信号及其编号)查看信号列表:

image-20250406153812273

上面我们提到我们可以通过不同的退出码来代表不同的错误信息,那么不同的退出码究竟各自代表什么信息呢?我们可以通过 strerror 函数来查看, 比如我们来看一下退出码 010 所代表的信息:

1
2
3
4
5
6
7
8
9
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;
}

运行结果:

image-20250406140838340

进程在退出是会有退出码,我们可以通过 echo 来查看退出码,那我们如何获取呢?

C/C++中其实还定义了一个叫 errno 的常量来记录错误码,所以我们就可以将 errno 常量与 strerror 函数结合使用,用 errno 来记录进程的错误码,然后传给 strerror 函数得到错误信息,比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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;
}

image-20250406141949307

三、进程常见退出方法

1. exit() 函数

  • 头文件#include <stdlib.h>

  • 行为

    • 执行标准清理操作(刷新缓冲区、关闭文件描述符等)。
    • 调用通过 atexit() 注册的函数。
    • 返回状态码给父进程(通过 wait() 获取)。
  • 示例

    1
    2
    3
    4
    5
    #include <stdlib.h>
    int main()
    {
    exit(3); // 设置退出码为 3
    }

2. _exit() 函数

  • 头文件#include <unistd.h>(函数:void _exit(int status);

  • 行为

    • status 定义了进程的终止状态,父进程通过 wait 来获取该值,虽然 statusint,但是仅有低 8 位可以被父进程所用。所以 exit(-1) 时,在终端执行 echo $? 发现返回值是 255
    • 立即终止 进程(系统调用级别的退出),不执行任何清理(缓冲区不刷新、atexit() 函数不调用)。
    • 适用于子进程在 fork() 后需要快速退出的场景。
  • 示例

    1
    2
    3
    4
    5
    6
    int main()
    {
    printf("Hello, World!\n");
    _exit(0); // 立刻退出,状态码为0
    printf("这一行将不会被打印。.\n");
    }

3. return 退出

  • 行为

    • main() 函数中,return 等效于调用 exit()
    • 在其他函数中,return 仅退出当前函数。
  • 示例

    1
    2
    3
    4
    int main()
    {
    return 42; // 等效于 exit(42)
    }

[!WARNING]

警告:下面的程序会源源不断的创建僵尸进程,直至将系统资源耗尽!请谨慎使用!实测:在虚拟机中运行 20 秒不到,系统直接卡死。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>
#include <sys/types.h>

int main()
{
while (1)
{
if (fork() == 0)
{
_exit(0); // 子进程立即退出,成为僵尸进程
}
}
return 0;
}

三、关键区别对比

image-20250406135753463

方法 是否刷新缓冲区 是否调用 atexit() 适用场景
exit() ✅ 是 ✅ 是 正常退出,需清理资源
_exit() ❌ 否 ❌ 否 子进程快速退出或错误紧急终止
return ✅ 是(仅 main ✅ 是(仅 main main() 函数中的简洁退出方式

四、代码示例分析

1. 父子进程退出行为差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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); // 不刷新缓冲区
}
}

输出结果

1
2
3
4
5
# 由于 printf 未刷新缓冲区,子进程继承了未刷新的缓冲区内容,导致重复输出:
Start (PID:123)
Child exiting
Start (PID:123) // 父进程的缓冲区未刷新,被子进程继承后输出
Parent exiting

2. atexit() 注册清理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <stdio.h>

void cleanup()
{
printf("Cleanup done!\n");
}

int main()
{
atexit(cleanup); // 注册清理函数
printf("Main running\n");
exit(0); // 会调用 cleanup()
}

输出

1
2
Main running
Cleanup done!

五、进程终止后的状态

  1. 僵尸进程(Zombie)

    • 进程已终止,但父进程未通过 wait() 回收其资源。
    • 解决方案:
      • 父进程调用 wait()waitpid()
      • 忽略 SIGCHLD 信号:signal(SIGCHLD, SIG_IGN)注意:在某些系统中,忽略 SIGCHLD 会自动回收子进程,但并非所有系统都支持这一行为!
  2. 孤儿进程

    • 父进程先退出,子进程被 init(PID = 1)接管。
    • 无害,init 会自动回收孤儿进程。

小结

  • exit()(优先使用 ):安全退出,适合大多数场景,确保资源正确释放。
  • _exit():紧急退出,跳过清理。子进程慎用,除非明确需要跳过清理。
  • return:仅在 main() 中等效于 exit()
  • 进程管理:正确处理父子进程关系,避免资源泄漏。