目录
(图像由AI生成)
0.前言
在上一个博客中,我们介绍了如何通过 fork()
函数创建子进程。子进程创建后,通常会执行一些任务,然后终止。而父进程在子进程终止后,需要适当的方式来处理和等待子进程的退出。本文将详细讨论 Linux 中进程的终止与进程等待的相关内容。
1. 进程终止
进程终止是操作系统管理进程生命周期的重要阶段,当进程完成其预定的任务或遇到意外时,它会终止并向系统报告其退出状态。理解进程如何正常或异常终止,对于开发人员和系统管理员进行进程管理至关重要。
1.1 进程退出的场景
进程的退出场景可以大致归纳为以下三种:
- 代码运行完毕,结果正确:进程执行完所有任务并成功返回预期结果。
- 代码运行完毕,结果不正确:进程虽然执行结束,但由于逻辑错误或其他原因导致输出结果与预期不符。
- 代码异常终止:进程在执行过程中发生了未预期的错误,导致进程崩溃或被系统强制终止。
虽然进程退出的原因和场景各异,但所有进程最终都会通过一定的机制向系统报告其结束状态。
1.2 进程常见退出方法
进程退出的方式大致分为两类:正常退出和异常退出。我们可以通过不同的系统调用或外部信号来结束进程。
1.2.1 正常退出
在进程正常退出的情况下,它的生命周期如预期完成,并返回特定的状态码,表示程序执行成功或失败。常见的正常退出方法如下:
-
从
main()
函数返回: 在 C/C++ 语言中,进程的入口点是main()
函数。当程序执行完毕并到达main()
的结束处,进程会通过return
语句返回一个状态码,向系统报告其退出状态。典型地,return 0
表示正常退出,而非零值(如return 1
或return -1
)表示出现了某些错误。int main() { // ... 业务逻辑 return 0; // 正常退出 }
-
调用
exit()
:exit()
是标准库函数,允许程序随时结束执行,并返回状态码。调用exit()
后,程序会执行清理操作,例如关闭打开的文件、释放资源,并调用通过atexit()
注册的回调函数。exit()
通常用于程序需要在特定条件下主动退出时。#include <stdlib.h> int main() { // ... 业务逻辑 if (某种错误发生) { exit(1); // 异常退出 } exit(0); // 正常退出 }
-
调用
_exit()
:_exit()
是系统调用,通常在子进程中使用。与exit()
不同,_exit()
不会执行缓冲区的刷新或已注册的清理函数,它直接向内核报告进程结束并释放其资源。通常在子进程完成其工作后,调用_exit()
立即退出。#include <unistd.h> int main() { if (fork() == 0) { // 子进程执行 _exit(0); // 立即退出 } // 父进程继续执行 return 0; }
1.2.2 异常退出
异常退出是指进程在非预期情况下由于错误或外部干预而终止。常见的异常退出方式包括:
-
Ctrl+C
(信号终止): 当用户在命令行按下Ctrl+C
,系统会发送SIGINT
信号给进程,指示其立即终止。这是一种外部干预的方式,常用于终止长时间运行的任务。./your_program # 用户按下 Ctrl+C,程序收到 SIGINT 信号并终止
-
异常信号终止: 进程可能由于内部错误(如访问无效内存地址)而收到操作系统发送的异常信号,导致进程非正常退出。常见的异常信号包括
SIGSEGV
(段错误)、SIGFPE
(算术错误,如除零)等。例如,非法内存访问会导致
SIGSEGV
信号:int main() { int *ptr = NULL; *ptr = 42; // 导致段错误,异常退出 return 0; }
当进程因信号终止时,系统会向父进程报告该终止信号,而不是正常的退出状态码。开发者可以通过适当的信号处理机制捕捉并处理这些信号,避免进程非预期崩溃。
2. 进程等待
当子进程终止时,父进程需要进行适当的处理,避免出现僵尸进程。僵尸进程不仅会占用系统的进程表条目,还会导致内存资源无法及时回收。因此,父进程通过进程等待机制来回收子进程资源,并获取子进程的退出状态。
2.1 进程等待的重要性
当子进程结束后,如果父进程不主动等待并回收子进程资源,就可能导致子进程进入僵尸状态。僵尸进程的特性是已经终止,但仍然在系统的进程表中保留一些信息,包括退出状态。由于这些进程已经结束,系统资源无法通过常规的手段释放。
- 僵尸进程占用系统资源,并且无法被终止。即使使用
kill -9
这样的强制终止信号,也无法“杀死”一个已经处于僵尸状态的进程,因为它已然是“死去的进程”。 - 另外,父进程往往需要知道子进程任务完成的情况,例如子进程是否正常退出,返回结果是否正确。这些信息对父进程判断后续操作具有重要意义。
因此,父进程通过等待机制能够:
- 回收子进程的资源,避免僵尸进程;
- 获取子进程的退出状态,了解子进程的执行结果。
2.2 进程等待的方法
Linux 提供了几种等待子进程的方法,最常见的有 wait()
和 waitpid()
函数。
2.2.1 wait()
方法
wait()
是一个简单的进程等待函数,父进程通过调用 wait()
可以阻塞自身,直到有一个子进程终止。它的基本使用方式如下:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
- 返回值:成功时,返回已终止的子进程的 PID;如果发生错误,返回
-1
。 - 参数:
status
:指向一个整数的指针,用于存储子进程的退出状态。如果不关心子进程的退出状态,可以将该参数设置为NULL
。
wait()
函数适用于父进程只需等待任意一个子进程退出的场景。当父进程有多个子进程时,wait()
将会等待其中的任何一个结束,并返回它的进程 ID。
2.2.2 waitpid()
方法
waitpid()
是 wait()
的增强版本,提供了更多的功能和灵活性。例如,父进程可以通过 waitpid()
等待特定的子进程,或者选择非阻塞等待。其函数定义如下:
pid_t waitpid(pid_t pid, int *status, int options);
-
返回值:
- 如果成功,
waitpid()
返回终止的子进程的 PID。 - 如果设置了
WNOHANG
选项且没有任何子进程终止,返回0
。 - 如果发生错误,返回
-1
,并设置errno
以指示错误原因。
- 如果成功,
-
参数:
- pid:
pid = -1
:等待任意子进程终止,功能与wait()
相同。pid > 0
:等待指定 PID 的子进程终止。
- status:与
wait()
中类似,保存子进程的退出状态。可以通过WIFEXITED(status)
判断子进程是否正常终止,使用WEXITSTATUS(status)
提取退出码。 - options:
WNOHANG
:如果没有子进程终止,则waitpid()
立即返回,而不会阻塞父进程。这对于父进程需要同时处理其他任务时非常有用。
- pid:
2.3 获取子进程 status
在使用 wait()
或 waitpid()
等待子进程时,除了能够回收子进程的资源,还可以通过 status
参数获取子进程的退出信息。这个参数是一个输出型参数,由操作系统填充,用来向父进程反馈子进程的退出状态。
status
参数的使用
-
status
的意义: 当我们调用wait()
或waitpid()
时,status
参数是一个用于存储子进程退出状态的变量。如果我们不关心子进程的退出状态,可以将这个参数设置为NULL
。然而,如果我们希望获得子进程的退出信息,需要提供一个指针,操作系统会将退出信息写入该地址。 -
位图解读:
status
参数并不是一个简单的整形值,而是一个位图,通常我们只需要关心它的低 16 位。其中,最常用的信息包括:- 子进程是否正常退出;
- 子进程的退出码;
- 如果是异常终止,是什么信号导致的异常终止。
当子进程正常退出时,status
中的高位(第 8 到第 15 位)存储了子进程的退出码,而低 7 位(第 0 到第 6 位)用于表示子进程的信号终止信息。
-
正常退出:如果子进程是通过
exit()
或return
正常退出,status
的低 7 位应该是0
,表示没有通过信号终止。此时,高位存储的是子进程的退出码,可以通过st >> 8
提取。 -
异常终止:如果子进程因为信号而被终止,低 7 位会存储导致终止的信号编号。父进程可以通过
st & 0X7F
获取到信号编号,进一步判断子进程因何信号终止。
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main() {
pid_t pid;
if ( (pid=fork()) == -1 ) {
perror("fork"), exit(1);
}
if (pid == 0) {
// 子进程休眠20秒后正常退出,退出码为10
sleep(20);
exit(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);
}
}
}
测试结果
-
当子进程正常退出时,输出如下:
子进程正常退出,父进程通过
status
获取子进程的退出码为10
。 -
当子进程在其他终端被
kill
掉时,输出如下:这是因为子进程被
SIGKILL
信号(编号为 9)终止,父进程通过status
获取到了导致子进程终止的信号编号。
2.4 阻塞等待和非阻塞等待
在进程等待时,父进程可以选择采用阻塞等待或者非阻塞等待的方式来处理子进程的退出。阻塞等待会使父进程在子进程退出前一直处于等待状态,而非阻塞等待则允许父进程在子进程未退出时继续执行其他任务。
2.4.1 阻塞等待
阻塞等待是最常见的等待方式。当父进程调用 wait()
或 waitpid()
并不设置任何非阻塞选项时,父进程会一直等待直到有子进程退出。此时,父进程会被阻塞,无法进行其他操作。
代码示例
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork(); // 创建子进程
if (pid == -1) {
perror("fork failed");
exit(1);
}
if (pid == 0) { // 子进程
printf("Child process running...\n");
sleep(5); // 子进程休眠5秒模拟任务执行
printf("Child process finished.\n");
exit(0);
} else { // 父进程
int status;
printf("Parent waiting for child to exit (blocking)...\n");
wait(&status); // 阻塞等待子进程结束
printf("Child exited with status: %d\n", WEXITSTATUS(status));
}
return 0;
}
输出结果:
- 父进程在子进程运行期间被阻塞,直到子进程结束后才继续执行。
2.4.2 非阻塞等待
非阻塞等待允许父进程在等待子进程时继续执行其他任务,而不是阻塞等待子进程结束。通过在 waitpid()
中传入 WNOHANG
选项,父进程可以立即返回,即使子进程还没有结束。
代码示例
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork(); // 创建子进程
if (pid == -1) {
perror("fork failed");
exit(1);
}
if (pid == 0) { // 子进程
printf("Child process running...\n");
sleep(5); // 子进程休眠5秒模拟任务执行
printf("Child process finished.\n");
exit(0);
} else { // 父进程
int status;
printf("Parent checking child status (non-blocking)...\n");
while (1) {
pid_t result = waitpid(pid, &status, WNOHANG); // 非阻塞等待
if (result == 0) {
// 子进程还没有结束
printf("Child process is still running...\n");
sleep(1); // 父进程继续执行其他任务
} else if (result == -1) {
perror("waitpid failed");
exit(1);
} else {
// 子进程结束
printf("Child exited with status: %d\n", WEXITSTATUS(status));
break;
}
}
}
return 0;
}
输出结果:
- 父进程每隔 1 秒检查一次子进程状态,而不会阻塞自己等待子进程结束。当子进程结束时,父进程获取子进程的退出状态并结束循环。
3.结语
Linux 系统中的进程终止和进程等待是进程管理中的核心内容。通过合理地终止进程并及时进行等待操作,父进程可以有效地处理子进程的退出,避免产生僵尸进程,同时保证系统资源的高效利用。希望通过本文的讲解,读者能够对进程终止和进程等待有更深入的理解。