You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确终止fork()创建的进程且无需阻塞等待其结果

问题背景

根据任务要求,程序需调用fork()创建自身副本处理任务,且子进程运行期间父进程不得被阻塞。
测试过程中出现两个异常现象:

  • pgrep查询结果显示子进程直到父程序执行完毕才会终止
  • 每次调用fork()后,pgrep统计的进程数持续增长,已运行结束的子进程并未被正常回收

测试用基础程序总运行时长约35秒,运行期间共生成3次子进程,预期每个子进程运行5秒后自动退出,代码如下:

#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
    for (int i = 1; i < 36; i++) {
        if (i % 10 == 0) {
            pid_t forkrank = fork();            
            if (forkrank == 0) {
                printf("forked process starts\n");
                sleep(5);
                return 1;
            }
        }
        sleep(1);
    }
    return 0;
}

该程序运行期间pgrep显示子进程直到父进程结束才会被清理,运行结果截图:
pgrep查询结果

如果添加阻塞代码等待子进程执行结果,子进程就可以被正常终止回收,对应代码如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
    for (int i = 1; i < 36; i++) {
        if (i % 10 == 0) {
            pid_t forkrank = fork();
            if (forkrank == 0) {
                printf("forked process starts\n");
                sleep(5);
                return 1;
            }
            int wstatus;
            waitpid(forkrank, &wstatus, 0);
            int retValue = WEXITSTATUS(wstatus);
        }
        sleep(1);
    }
    return 0;
}

对应pgrep正常查询结果截图:
pgrep正常查询结果

核心疑问:

  1. 该现象的产生原因是什么?
  2. 如何在不阻塞主进程的前提下正常终止并回收fork()创建的子进程?

原因说明

这是Unix/Linux进程模型里的僵尸进程(zombie process)机制,不是子进程没正常退出:

  • 子进程执行完退出逻辑后,内核不会立刻完全释放它的进程表项,会保留PID、退出码、资源使用统计等最小信息,等待父进程调用wait()/waitpid()取走这些信息。这个状态下的子进程就是僵尸进程,仍然会被pgrep这类进程查询工具统计到。
  • 如果父进程始终不调用wait类函数回收,子进程就会一直保持僵尸状态,直到父进程终止。父进程退出后,所有残留的僵尸子进程会被PID为1的init进程接管,由init统一完成回收,这就是观察到“子进程等父进程结束才消失”的原因。
  • 加了阻塞式waitpid之后能正常回收,是因为这个调用会暂停父进程执行,直到对应子进程退出、内核释放子进程资源后才返回,自然不会留僵尸,但也阻塞了主流程,不符合需求。

非阻塞回收子进程的实现

两种成熟方案,可按需选择:

方案1:非阻塞轮询waitpid

给waitpid传入WNOHANG参数,调用时如果没有已经退出的子进程,函数会立刻返回0,不会阻塞主流程。只需要在主循环的每次迭代里插入一小段非阻塞回收逻辑,就能在不影响主任务执行的前提下清理退出的子进程。
核心实现参考:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char* argv[])
{
    for (int i = 1; i < 36; i++) {
        if (i % 10 == 0) {
            pid_t forkrank = fork();
            if (forkrank == 0) {
                printf("forked process starts\n");
                sleep(5);
                return 1;
            }
        }
        // 非阻塞回收所有已退出的子进程
        int wstatus;
        pid_t done_pid;
        while ((done_pid = waitpid(-1, &wstatus, WNOHANG)) > 0) {
            int exit_code = WEXITSTATUS(wstatus);
            // 这里可以按需处理子进程退出逻辑,比如打日志
            printf("child %d exited, code=%d\n", done_pid, exit_code);
        }
        sleep(1);
    }
    return 0;
}

这里waitpid第一个参数传-1表示匹配任意子进程,套while循环是为了一次轮询就把所有已经退出的子进程全部回收,避免多个子进程同时退出时漏处理。

方案2:忽略SIGCHLD信号让内核自动回收

如果不需要获取子进程的退出状态,最简单的做法是在程序初始化时直接设置忽略SIGCHLD信号,内核会在子进程退出后自动回收其资源,完全不会产生僵尸进程,不需要写任何wait相关逻辑。
只需要在main函数开头加两行代码:

#include <signal.h> // 记得补充头文件
signal(SIGCHLD, SIG_IGN);

说明:这个行为是POSIX标准明确规定的,所有主流Linux发行版、macOS等BSD系统均支持。如果业务需要感知子进程退出、拿到退出码,就不能用这个方案,要么用上面的WNOHANG轮询,要么注册SIGCHLD信号处理函数,在信号回调里调用waitpid回收。


内容的提问来源于stack exchange,提问作者Ivan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 01:21:34