「Linux 系统编程」孤儿进程、僵尸进程、wait 和 waitpid 子进程回收

1 孤儿进程和僵尸进程

1.1 孤儿进程

孤儿进程:父进程已经终止或退出,但仍然在运行中的子进程。

通常情况下,Linux 系统通过 init 进程(进程号 PID 为 1)来自动接管这些孤儿进程,成为它们的新父进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(){
pid_t pid = fork();

if(pid == 0){
// 子进程 一直执行,父进程退出之后变为孤儿进程
while(1){
printf("child process: PID = %d, parent PID = %d\n", getpid(), getppid());
sleep(1);
}
}else if(pid > 0){
// 父进程
printf("parent process: PID = %d, son PID = %d\n", getpid(), pid);
// 父进程休息 5 秒后退出,之后子进程变为孤儿进程
sleep(5);
printf("------parent process going to die------\n");
}else{
perror("fork error");
exit(1);
}

return 0;
}

一开始,创建的子进程 PID 为 24919,其父进程 PID 为 24918。之后父进程退出,子进程还在执行,子进程 24919 变为孤儿进程,被新的父进程 2145 收养。

最后可以通过 kill 命令杀死一直进程的子进程。

在我的电脑中,2145 对应的进程信息为:

1
2
PPID  PID   PGID  SID  TTY TPGID STAT  UID   TIME  COMMAND
1 2145 2145 2145 ? -1 Ss 1000 0:00 /lib/systemd/systemd --user

示例中,孤儿进程被 /lib/systemd/systemd --user 进程收养。用户级 systemd 进程(示例中的 PID=2145)本身是由系统级 init/systemd(PID=1)启动的,这形成了一个层次化的进程管理结构。


1.2 僵尸进程

僵尸进程:已经执行完毕但仍在进程表中占用位置的进程

子进程终止,父进程尚未对子进程进行回收,在此期间,子进程为“僵尸进程”。(每个进程结束后都必然会经历僵尸态,只是时间长短上有差别)

子进程终止时,子进程残留资源 PCB 存放于内核中,PCB 记录了进程结束原因,进程回收就是回收 PCB。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(){
pid_t pid = fork();

if(pid == 0){
// 子进程
printf("child process: PID = %d, parent PID = %d\n", getpid(), getppid());
// 子进程休息 5 秒后退出
sleep(5);
printf("------child process going to die------\n");
}else if(pid > 0){
// 父进程 一直执行,子进程退出后子进程变为僵尸进程
while(1){
printf("parent process: PID = %d, son PID = %d\n", getpid(), pid);
sleep(1);
}
}else{
perror("fork error");
exit(1);
}

return 0;
}

可以看到执行到后面子进程的状态变为 [zombie] <defunct>,表示子进程已经处于终止的状态。

最后可以通过 kill 命令杀死父进程。



2 wait() 函数

在 Linux 中,当一个进程创建子进程(通常使用 fork())后,子进程会独立运行。父进程需要通过 wait()回收子进程资源获取子进程状态

  • 回收子进程资源:当子进程终止时,不会立即从系统中完全消失,而是会变成一个僵尸进程。

    父进程通过 wait() 函数来获取这些信息并彻底释放子进程占用的系统资源(如进程号、进程表项等)。如果父进程不进行回收,僵尸进程会一直存在。

  • 获取子进程状态:父进程可能需要暂停执行,等待一个或所有子进程结束,然后再继续。

    或者,父进程需要知道子进程是正常退出(及其退出码)还是被信号终止。


2.1 wait() 函数原型及参数

wait():等待 任意一个 子进程结束。

函数原型

1
2
3
4
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

参数

  • status:一个指向整型的指针,用于 存储子进程的退出状态信息。如果不关心子进程的退出状态,可以传入 NULL

返回值

  • 成功:返回一个被等待回收的子进程的 PID
  • 失败:返回 -1,设置 errno。(例如,当前进程已经没有需要等待的子进程)

工作方式

  • wait() 函数会 阻塞(block)父进程的执行,直到它的任意一个子进程终止

  • 一旦有子进程终止,wait() 会立即回收该子进程,并将其退出状态信息填入 status 指向的变量中,然后返回该子进程的 PID。


2.2 子进程 退出状态 和 异常终止信号

一个进程终止时会关闭所有文件描述符,释放在用户空间分配的内存,但其 PCB 还保留着,内核在其中保存了一些信息:

  • 如果是正常终止则保存着 退出状态
  • 如果是异常终止则保存着 导致该进程终止的信号

可以使用一系列宏来解析 wait() 返回的状态信息:

  • WIFEXITED(status): 如果子进程正常退出,返回真

  • WEXITSTATUS(status): 如果 WIFEXITED 为真,提取子进程的退出码

  • WIFSIGNALED(status): 如果子进程因信号而终止,返回真

  • WTERMSIG(status): 如果 WIFSIGNALED 为真,提取导致终止的信号编号

  • WIFSTOPPED(status): 如果子进程当前已停止,返回真

  • WSTOPSIG(status): 如果 WIFSTOPPED 为真,提取导致停止的信号编号


2.2 wait() 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(){
pid_t pid = fork(), wpid;
int status; // 子进程状态

if(pid == 0){
// 子进程
printf("child process, PID = %d, sleep for 10s\n", getpid());
sleep(10);
printf("------child process going to die------\n");
_exit(233); // 正常退出,返回状态码 233
}else if(pid > 0){
wpid = wait(&status); // 等待回收的子进程 PID
if(wpid == -1){
perror("wait error"); // 没有子进程可以等待回收
exit(1);
}
if(WIFEXITED(status)){
// 说明子进程正常终止
printf("child exited normally with status: %d\n", WEXITSTATUS(status));
}else if(WIFSIGNALED(status)){
// 说明子进程被某个信号终止
printf("child killed by signal: %d\n", WTERMSIG(status));
}
}

return 0;
}


3 waitpid() 函数

3.1 waitpid() 函数原型及参数

waitpid() 提供了更加精确的 wait 控制,是现在开发更加常用的子进程回收方式。

函数原型

1
2
3
4
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

参数

  • pid:指定要等待回收的子进程的 PID。

    • 小于 -1:等待进程组 ID 等于 pid 绝对值的任何一个子进程。
    • -1:等待 任意一个 子进程,行为与 wait() 相同。
    • 0:等待与父进程 同一个进程组 的任何一个子进程。
    • 大于 0:等待进程 ID 等于 pid 的特定子进程
  • status:同 wait(),用于 存储状态信息

  • options:修改函数的行为,可以是一个或多个选项的按位或(|)。

    • 0默认行为,与 wait() 一样阻塞
    • WNOHANG非阻塞模式。如果没有子进程退出,立即返回 0,而不是阻塞父进程。
    • WUNTRACED:除了返回已终止的子进程信息外,还返回因信号而停止(stopped)的子进程信息。
    • WCONTINUED:返回因收到 SIGCONT 信号而恢复执行(continued)的子进程信息。

返回值

  • 成功:返回状态发生变化的子进程的 PID。

  • 失败:返回 -1(并设置 errno)。

  • 如果使用了 WNOHANG 且没有子进程退出,则返回 0

waitpid() 的好处

  • 可以 等待特定的子进程

  • 可以非阻塞(WNOHANG):父进程可以周期性地检查子进程是否结束,而不必挂起。这在事件循环(如网络服务器)中非常有用。

  • 可以获取更多状态(停止、恢复)。


3.2 waitpid() 回收指定的子进程

示例中,循环创建 5 个子进程,指定回收第 3 个子进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(){
pid_t pid, wpid, tmppid;
int i;

for(i = 0; i < 5; i ++ ){
pid = fork();
if(pid == 0){ // 在子进程,返回 0 表示子进程创建成功
// 父进程循环创建多个子进程,子进程不进行 fork()
break;
}
if(i == 2){
// 需要指定回收第 3 个子进程
// 此时的 pid 是父进程第 3 次 fork() 的返回值(写时复制)
// 也就是新闯进啊的第 3 个子进程的进程ID
tmppid = pid; // 用一个临时变量存储这个指定的进程ID
printf("------get the child process pid = %d------\n", tmppid);
}
}

if(pid == 0){
// 子进程,从 break 跳出
sleep(i);
printf("this is %dth child process, PID = %d\n", i + 1, getpid());
}else{
sleep(5);
printf("this is parent process, PID = %d\n", getpid());
// 如果时间小于指定回收子进程终止的时间,且采用 非阻塞模式,最终 wpid = 0
// 如果采用 阻塞模式,无论如何父进程都会等待直到指定回收子进程终止

wpid = waitpid(tmppid, NULL, WNOHANG); // 指定一个进程回收, 非阻塞
// wpid = waitpid(tmppid, NULL, 0); // 指定一个进程回收, 阻塞

if(wpid == -1){
perror("waitpid error");
exit(1);
}
printf("this is parent process, wait child process finish: %d\n", wpid);
}

return 0;
}


3.3 waitpid() 回收多个子进程

示例中,循环创建 5 个子进程,需要回收所有的子进程。

使用 非阻塞方式(WNOHANG)回收所有子进程,循环检查是否有子进程退出。当检查到有子进程退出时,回收该子进程;当暂时没有子进程退出时(waitpid 返回 0),父进程可以暂时去处理其他事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(){
pid_t pid, wpid;
int i;

for(i = 0; i < 5; i ++ ){
pid = fork();
if(pid == 0){ // 在子进程,返回 0 表示子进程创建成功
// 父进程循环创建多个子进程,子进程不进行 fork()
break;
}
}

if(pid == 0){
// 子进程,从 break 跳出
sleep(i);
printf("this is %dth child process, PID = %d\n", i + 1, getpid());
}else{
// 父进程
// 使用 非阻塞方式 回收子进程,循环检查
while((wpid = waitpid(-1, NULL, WNOHANG)) != -1){
if(wpid > 0){
// 回收子进程 wpid
printf("wait child process, PID = %d\n", wpid);
}else if(wpid == 0){
// 指定 WNOHANG,但是暂时没有子进程退出
printf("parent process can do other things\n");
sleep(1); // 父进程可以做其他事
}
}
}

return 0;
}


「Linux 系统编程」孤儿进程、僵尸进程、wait 和 waitpid 子进程回收
https://marisamagic.github.io/2025/09/03/20250903/
作者
MarisaMagic
发布于
2025年9月3日
许可协议