当前位置:首页 > 文章列表 > 文章 > linux > Linux pidfd怎样安全等待进程结束

Linux pidfd怎样安全等待进程结束

来源:17golang原创 2026-11-15 19:59:55 0浏览 收藏

在守护进程、任务执行器或容器运行时里,我们经常需要等待“某一个进程”结束。只保存一个 PID 看起来够用,但 PID 会被系统回收后重新分配;如果轮询 /proc/ 或延迟调用 kill(pid, 0),就可能把已经结束的旧进程误认成同一个新进程。

Linux 的 pidfd 把进程绑定成一个文件描述符,之后可以把它交给 poll、epoll 监听生命周期;如果它指向调用者的子进程,还可以用 waitid(P_PIDFD, ...) 读取退出状态。这样,“等待它结束”和“取得它的退出原因”都有稳定的对象可追踪。

最实用的组合是:pidfd_open 获取句柄,poll/epoll 判断进程是否结束,waitid(P_PIDFD) 只在目标是自己的子进程时负责读取并回收退出状态。不要对 pidfd 调用 read,它不是可读的普通文件。

官方参考地址:https://www.man7.org/linux/man-pages/man2/pidfd_open.2.html

先把进程身份从 PID 换成 pidfd

pidfd_open(pid, 0) 的输入仍然是 PID,但返回值是指向该任务的文件描述符。这个描述符在目标进程结束后仍然代表原来的任务,不会因为 PID 号码后来被复用而自动指向另一个进程。对于已经存在的进程,官方手册把 pidfd_open 作为优先获取方式。

这和打开 /proc/pid 目录不是一回事:后者不能用于 poll,也不能交给 waitid。因此,如果程序需要把多个子进程统一放进一个 epoll 集合,pidfd 是更自然的对象。

Linux pidfd 从目标子进程和 PID 连接到 pidfd 文件描述符,再分到 poll epoll 与 waitid 退出状态的结构说明图
图1:pidfd 生命周期契约结构图,展示进程身份、稳定句柄和用户态消费边界;这是静态说明图,不是运行截图。

支持范围决定你能把它用到哪一步

写代码前先确定内核与目标对象的边界:

能力内核起点它解决的问题注意事项
pidfd_openLinux 5.3为已有 PID 获取任务文件描述符进程不存在时返回错误;权限与 PID 命名空间仍然有效
poll/epoll 监视 pidfd随 pidfd 能力提供在任务变成 zombie 时收到可读事件pidfd 本身不能 read;被回收后会出现挂起事件
waitid(P_PIDFD)Linux 5.4按 pidfd 读取子进程的退出状态目标必须是调用者的子进程
PIDFD_NONBLOCKLinux 5.10让针对 pidfd 的等待不阻塞目标尚未结束时要处理 EAGAIN
PIDFD_THREADLinux 6.9让句柄指向线程而非线程组组长线程与整个线程组的可读时机不同

其中最容易误解的是 waitid(P_PIDFD):它不是“所有 pidfd 都能读取退出码”。手册限定了目标必须是调用者的子进程;对于只是观察同一机器上已有的非子进程,通常只能把 pidfd 当作可轮询的生命周期信号。

最小 C 示例:poll 等结束,waitid 取状态

下面的示例创建一个短生命周期子进程。父进程先用 pidfd_open 得到稳定句柄,再用 poll 等待句柄变为可读,最后用 waitid(P_PIDFD) 读取 siginfo_t。代码展示接口关系,不把终端输出当作文章证据。

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

// 某些较旧的头文件没有暴露 P_PIDFD,数值来自 Linux waitid 接口定义。
#ifndef P_PIDFD
#define P_PIDFD 3
#endif

static int open_pidfd(pid_t pid) {
    // glibc 环境可能没有 pidfd_open 包装器,因此直接调用系统调用。
    return (int)syscall(SYS_pidfd_open, pid, 0U);
}

int main(void) {
    pid_t child = fork();
    if (child == -1) {
        perror("fork");
        return EXIT_FAILURE;
    }

    if (child == 0) {
        // 子进程只用于提供一个可等待的生命周期,不读取任何外部输入。
        sleep(1);
        _exit(7);
    }

    int pidfd = open_pidfd(child);
    if (pidfd == -1) {
        perror("pidfd_open");
        return EXIT_FAILURE;
    }

    struct pollfd watched = {
        .fd = pidfd,
        .events = POLLIN, // 进程变成 zombie 后,pidfd 会报告可读事件。
    };
    while (poll(&watched, 1, -1) == -1) {
        // 被信号打断时继续等待;其它错误必须关闭句柄并退出。
        if (errno != EINTR) {
            perror("poll");
            close(pidfd);
            return EXIT_FAILURE;
        }
    }

    siginfo_t info = {0};
    // 目标是自己的子进程,所以可以按 pidfd 读取并回收退出状态。
    if (waitid(P_PIDFD, (id_t)pidfd, &info, WEXITED) == -1) {
        perror("waitid");
        close(pidfd);
        return EXIT_FAILURE;
    }

    if (info.si_code == CLD_EXITED) {
        printf("child exited with status %d\\n", info.si_status);
    } else {
        // 被信号终止时,si_status 表示导致终止的信号编号。
        printf("child changed state, code=%d status=%d\\n",
               info.si_code, info.si_status);
    }

    // waitid 已完成回收,pidfd 自身也要释放。
    close(pidfd);
    return EXIT_SUCCESS;
}

这段代码里有两个不能互换的动作。poll 只负责把“还没结束”变成“可以处理”;它不会把退出码写进 pidfd。waitid 才负责取得 si_status,并且在没有使用 WNOWAIT 时完成子进程回收。

poll 和 waitid 各自回答什么问题

把两个接口混成一个“读取进程结果”的动作,是 pidfd 代码最常见的误区。可以按下面的职责拆开:

接口或事件能回答不能回答
POLLIN/EPOLLIN被监视的任务已经终止并成为 zombie,可以进入状态处理不能直接得到退出码;也不能对 pidfd 使用 read
EPOLLHUP任务已被回收,句柄进入挂起状态不能说明是正常退出还是信号终止
waitid(P_PIDFD, WEXITED)为子进程填写 siginfo_t 并完成回收不能等待不属于调用者的任意进程
waitid(..., WNOWAIT)读取状态但暂时保留子进程的可等待状态不能代替最终的回收动作
pidfd 文件描述符连接 POLLIN EPOLLIN EPOLLHUP 与 waitid WEXITED WNOWAIT 和子进程回收边界的结构说明图
图2:poll、waitid 与回收边界结构图,帮助区分可观测事件和退出状态读取;这是静态说明图,不是运行截图。

如果程序用 epoll 管理大量任务,收到 EPOLLIN 后可以把 pidfd 从“等待集合”转入“状态处理集合”,随后调用 waitid。处理完成就关闭描述符;不要反复把同一个已经回收的 pidfd 当成普通输入源。

已有进程、子进程和线程 pidfd 要分开判断

只观察已有的非子进程

对于已经运行的、但不属于当前进程的任务,pidfd_open 可以建立稳定引用,也可以用 poll 或 epoll 观察它结束。但此时不能把 waitid(P_PIDFD) 当成通用的退出码查询接口;权限、父子关系和目标是否已经被其它地方回收都可能使等待失败。

自己创建的子进程

自己通过 fork 创建的子进程,适合使用本文的组合。若程序一开始就需要在创建动作中拿到 pidfd,可以考虑带 CLONE_PIDFD 的 clone 方案,避免“先拿 PID、稍后再打开 pidfd”之间的额外窗口。

线程级句柄

不带 PIDFD_THREAD 时,pidfd 默认针对进程;线程组内还有其它线程时,整个线程组的结束边界才是主要关注点。使用 PIDFD_THREAD 后,句柄可以指向特定线程,线程退出成为 zombie 时的可读时机也不同。只有确实需要线程级生命周期时才启用它,不要把线程 pidfd 和普通进程 pidfd 的事件语义混用。

旧内核和降级路径怎么处理

部署环境低于 Linux 5.3 时,不能假设 pidfd_open 存在;低于 Linux 5.4 时,也不能假设 P_PIDFD 可用于 waitid。建议把能力探测放在启动或任务管理器初始化阶段,并把“等待对象”抽象出来:

  • 目标是自己的子进程且内核支持 pidfd:优先使用 pidfd + poll/epoll + waitid。
  • 目标是自己的子进程但内核较旧:使用 waitpid,不要在外层再用 PID 轮询模拟稳定句柄。
  • 目标不是自己的子进程且内核支持 pidfd:使用 pidfd 做结束监视,但把退出码查询视为能力之外。
  • 只有在明确接受限制时才使用 /proc/PID:它不是 pollable 文件,也不能消除 PID 复用风险。

降级路径的关键不是“找一个语法相同的替代函数”,而是向上层说明能力差异:是否能可靠识别原对象、是否能收到结束事件、是否能读取退出状态、是否由当前进程负责回收。

生产代码里的资源和安全注意

  • 始终关闭 pidfd:每个监视对象都会消耗文件描述符;任务结束、超时取消或错误返回都要走统一清理路径。
  • 处理 EINTR 和 EAGAIN:poll 可能被信号打断;启用 PIDFD_NONBLOCK 后,目标尚未结束时 waitid 可能直接返回 EAGAIN。
  • 不要把事件当退出码:POLLIN 只代表生命周期进入可处理状态,最终结果仍由 siginfo_t 的 si_code 和 si_status 解读。
  • 尊重命名空间与权限:pidfd 稳定的是任务引用,不会绕过 PID namespace、权限检查或目标已不存在的事实。
  • 避免重复回收:多个线程不要同时对同一个子进程调用 wait 系列接口;如果需要先观察后回收,明确使用 WNOWAIT 并安排唯一的最终回收者。

一句话记住这套组合

Linux pidfd 解决的是“我监视的到底是不是原来那个进程”这一层身份问题。pidfd_open 建立稳定句柄,poll/epoll 提供可复用的结束事件,waitid(P_PIDFD) 只在子进程场景下负责退出状态和回收。把这三个职责分清,再根据内核版本、父子关系和线程粒度选择降级路径,就不会把 PID 复用、不可读句柄和重复回收混在一起。

pidfd 可以直接 read 吗?

不可以。当前实现中对 pidfd 调用 read 会失败,应该通过 poll、epoll 或 waitid 获取生命周期与状态信息。

收到 EPOLLIN 后还要 waitid 吗?

如果需要子进程退出码或完成回收,仍然需要。EPOLLIN 只是告诉你进入了可以处理的状态。

pidfd 能替代所有 kill(pid, 0) 检查吗?

它能避免用裸 PID 反复确认同一对象的复用竞态,但不能替代所有权限判断、信号发送和进程管理策略。发送信号时应使用与 pidfd 对应的专用接口,并继续处理权限和目标状态错误。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
MySQL INNODB_CACHED_INDEXES 查看每个索引的缓存页MySQL INNODB_CACHED_INDEXES 查看每个索引的缓存页
上一篇
MySQL INNODB_CACHED_INDEXES 查看每个索引的缓存页
农产品经营者如何准备质量追溯台账
下一篇
农产品经营者如何准备质量追溯台账
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    1043次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1040次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1187次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1143次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    987次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码