[Linux]学习笔记系列 -- [fs]pidfs


title: pidfs

categories:

  • linux
  • fs
    tags:
  • linux
  • fs
    abbrlink: ccab2000
    date: 2025-10-03 09:01:49

https://github.com/wdfk-prog/linux-study

文章目录

fs/pidfs.c 进程ID文件系统(Process ID Filesystem) 为进程提供稳定的文件句柄

历史与背景

这项技术是为了解决什么特定问题而诞生的?

pidfs(Process ID Filesystem)及其暴露给用户空间的pidfd(Process ID File Descriptor)是为了从根本上解决一个长期困扰Linux/Unix系统开发者的严重问题:PID复用竞争条件(PID Reuse Race Condition)

这个问题的经典场景如下:

  1. 一个监控进程(例如服务管理器)启动了一个工作进程,并得到了它的PID,比如PID 1234。
  2. 监控进程想在稍后向PID 1234发送一个信号(例如 kill(1234, SIGTERM))来优雅地关闭它。
  3. 然而,在监控进程发送信号之前,工作进程(PID 1234)可能因为崩溃或正常完成而意外退出了
  4. 操作系统非常快地复用了 PID 1234,并将其分配给了一个全新的、完全不相关 的进程(例如,一个用户刚刚启动的 rm -rf / 命令)。
  5. 监控进程此时执行 kill(1234, SIGTERM)。它本意是想杀死原来的工作进程,但实际上却错误地向那个新启动的、无辜的进程发送了信号,这可能导致数据损坏甚至系统级的灾难。

pidfd出现之前,开发者们尝试使用各种变通方法(例如,检查/proc/[pid]/comm或进程启动时间),但这些方法本身也存在竞争条件,无法从根本上解决问题。pidfspidfd的诞生,就是为了提供一个由内核保证的、无竞争的、稳定的进程句柄

它的发展经历了哪些重要的里程碑或版本迭代?
  • 问题的长期存在:PID复用问题自Unix诞生以来就一直存在,是健壮的进程管理软件开发中的一个著名难题。
  • pidfd_open(2)系统调用的引入 :这是决定性的里程碑。在Linux内核5.3版本中,pidfd_open()系统调用被引入。这个调用接收一个PID,并返回一个特殊的文件描述符------pidfd。这个pidfd就是由内核中的pidfs伪文件系统所支持的。
  • pidfd相关API的扩展 :在pidfd_open之后,内核陆续增加了更多基于pidfd的系统调用,如pidfd_send_signal(2)(无竞争地发送信号)和pidfd_getfd(2)(安全地从另一个进程复制文件描述符),使其功能更加完善。poll(2)epoll(2)也增加了对pidfd的支持,允许进程异步地等待另一个进程的退出。
目前该技术的社区活跃度和主流应用情况如何?

pidfd已经成为现代Linux系统中进行健壮进程管理的事实标准

  • 主流应用
    • systemd :作为Linux系统中最核心的服务管理器,systemdpidfd的早期和主要用户。它使用pidfd来绝对可靠地管理其启动的所有服务的生命周期。
    • 容器运行时 :像Podman、containerd等现代容器运行时,需要精确地控制容器内进程的生命周期,pidfd为它们提供了实现这一目标的理想工具。
    • 高级监控和调试工具 :任何需要长时间、可靠地跟踪特定进程实例的工具,都可以从pidfd中受益。

核心原理与设计

它的核心工作原理是什么?

pidfs是一个内核中的"伪文件系统",它不存储在任何物理磁盘上。它的核心作用是为pidfd提供后端的inodefile对象。整个机制的工作流程如下:

  1. 从PID到文件描述符的转换 :当用户空间程序调用pidfd_open(pid, ...)时:
    • 内核查找PID对应的进程描述符struct task_struct
    • 如果找到了,内核会调用pidfs的内部函数来创建一个特殊的、属于pidfs文件系统的inode
    • 这个pidfs inode的私有数据 (inode->i_private) 会直接指向 那个进程的task_struct。这是最关键的链接。
  2. 创建稳定的内核句柄
    • 接着,内核会创建一个struct file对象,该对象与这个pidfs inode关联。这个file对象就是pidfd在内核中的表示。
    • 最重要的一点是,这个struct file的存在会对目标进程的task_struct持有一个引用计数
  3. 返回给用户空间
    • 内核在调用进程的文件描述符表中分配一个未使用的整数(FD),并将它指向这个新创建的struct file对象。
    • 这个FD就是返回给用户的pidfd
  4. 句柄的稳定性
    • 只要这个pidfd没有被close(),内核就会一直保持对目标进程task_struct的引用。
    • 这意味着,即使目标进程已经退出,它的task_struct也不会被立即完全销毁,其PID也不会被内核复用
    • 当程序对这个pidfd调用pidfd_send_signal()时,内核可以安全地从pidfd追溯到task_struct,并检查该进程的状态。如果进程已经退出,发送信号的操作就会安全地失败(返回ESRCH),而绝不会错误地发给一个新进程。
它的主要优势体现在哪些方面?
  • 完全无竞争:从根本上消除了PID复用带来的所有竞争条件问题。
  • 稳定的进程引用pidfd是一个指向特定进程实例的句柄,而不是一个可能被重用的数字。
  • 清晰的生命周期管理pidfd遵循标准的文件描述符语义,其生命周期由open()close()管理,非常清晰。
  • 可扩展的API :以pidfd为基础,可以构建更多安全、无竞争的进程间交互API。
  • 可等待性pidfd可以被poll/epoll监控,这提供了一种优雅的、异步等待指定进程退出的方式,优于传统的waitpid()
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
  • 专用性pidfd是一个不透明的句柄。你不能对它进行read()write()mmap()。它的唯一用途就是作为参数传递给其他专门的系统调用(如pidfd_send_signal)。
  • 需要新的API :为了利用其优势,现有的、基于PID的应用程序代码需要被重构,以使用新的pidfd_*系列API。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

pidfd是任何需要可靠地、长时间地监控或管理其他进程的场景下的首选解决方案。

  • 服务管理器systemd启动一个服务,立即pidfd_open()获取其pidfd。之后所有对该服务的操作(如systemctl stop, systemctl kill)都通过这个pidfd进行,确保操作对象绝对正确。
  • 进程监控器 :一个监控程序需要确保它监控的Web服务器进程还活着。它可以在启动时获取Web服务器的pidfd。如果服务器崩溃,poll()这个pidfd会立即返回,监控程序可以执行重启逻辑,而无需担心在检查和重启之间,服务器的PID被其他程序占用。
  • 安全的父子进程通信 :一个父进程fork了一个子进程,它可以立即为子进程创建一个pidfd。之后,它可以安全地向这个特定的子进程实例发送信号,或者将子进程的FD安全地传递给其他进程。
是否有不推荐使用该技术的场景?为什么?
  • 简单的、短暂的进程交互 :如果只是想给当前进程或一个刚刚fork出的、生命周期非常明确的子进程发送信号,并且没有复杂的异步逻辑,那么传统的kill(getpid(), ...)waitpid()仍然是简单有效的。
  • 不需要进程句柄的场景 :如果只是想获取进程的静态信息(如命令行、内存使用),那么直接读取/proc/[pid]/下的文件通常就足够了,但需要意识到这其中可能存在的微小竞争窗口。

对比分析

请将其 与 其他相似技术 进行详细对比。

pidfd的主要对比对象就是传统的、基于PID整数的进程管理方法。

特性 pidfd (由pidfs支持) 传统方法 (基于PID整数)
句柄类型 文件描述符 (File Descriptor),一个不透明的内核对象句柄。 整数 (Integer),一个可被复用的标识符。
稳定性 稳定 。一个pidfd永远指向它被创建时所对应的那个进程实例 不稳定 。一个PID在进程退出后可以被系统复用给新进程。
竞争条件 无竞争 。所有基于pidfd的操作都由内核保证作用于正确的进程实例。 存在竞争条件。在检查PID和对其操作之间,进程可能已改变。
核心操作 pidfd_open(), pidfd_send_signal(), poll() kill(), waitpid(), 读取/proc文件系统
生命周期 open()close()管理。只要FD打开,内核就保持对进程的引用。 PID的生命周期由进程自身决定。外部无法控制。
错误处理 如果目标进程已退出,pidfd_send_signal会安全地失败并返回错误。 如果目标进程已退出且PID被复用,kill错误地作用于新进程。
适用场景 健壮的、长时间运行的进程管理、监控和守护程序。 简单的、短暂的脚本或不存在竞争窗口的父子进程交互。

fs/pidfs.c

pidfs_init_fs_context 用于初始化 pidfs 文件系统的上下文

c 复制代码
static const struct stashed_operations pidfs_stashed_ops = {
	.init_inode = pidfs_init_inode,
	.put_data = pidfs_put_data,
};

static int pidfs_init_fs_context(struct fs_context *fc)
{
	struct pseudo_fs_context *ctx;

	ctx = init_pseudo(fc, PID_FS_MAGIC);
	if (!ctx)
		return -ENOMEM;

	ctx->ops = &pidfs_sops;
	ctx->eops = &pidfs_export_operations;
	ctx->dops = &pidfs_dentry_operations;
	fc->s_fs_info = (void *)&pidfs_stashed_ops;
	return 0;
}

pidfs_init 用于初始化 pidfs 文件系统

c 复制代码
static void pidfs_inode_init_once(void *data)
{
	struct pidfs_inode *pi = data;

	inode_init_once(&pi->vfs_inode);
}

static struct file_system_type pidfs_type = {
	.name			= "pidfs",
	.init_fs_context	= pidfs_init_fs_context,
	.kill_sb		= kill_anon_super,
};

void __init pidfs_init(void)
{
	pidfs_cachep = kmem_cache_create("pidfs_cache", sizeof(struct pidfs_inode), 0,
					 (SLAB_HWCACHE_ALIGN | SLAB_RECLAIM_ACCOUNT |
					  SLAB_ACCOUNT | SLAB_PANIC),
					 pidfs_inode_init_once);
	pidfs_mnt = kern_mount(&pidfs_type);
	if (IS_ERR(pidfs_mnt))
		panic("Failed to mount pidfs pseudo filesystem");
}
相关推荐
Lsir10110_4 小时前
【Linux】进程信号(下半)
linux·运维·服务器
酉鬼女又兒4 小时前
零基础入门Linux指南:每天一个Linux命令_pwd
linux·运维·服务器
云飞云共享云桌面4 小时前
高性能图形工作站的资源如何共享给10个SolidWorks研发设计用
linux·运维·服务器·前端·网络·数据库·人工智能
zl_dfq4 小时前
Linux 之 【多线程】(pthread_xxx、轻量级进程、原生线程库、线程ID、__thread、线程栈、线程与信号、线程与程序替换)
linux
choke2334 小时前
Python 基础语法精讲:数据类型、运算符与输入输出
java·linux·服务器
菩提小狗4 小时前
小迪安全2023-2024|第5天:基础入门-反弹SHELL&不回显带外&正反向连接&防火墙出入站&文件下载_笔记|web安全|渗透测试|
笔记·安全·web安全
AZ996ZA4 小时前
自学linux的第二十一天【DHCP 服务从入门到实战】
linux·运维·服务器·php
_OP_CHEN5 小时前
【Linux系统编程】(二十八)深入 ELF 文件原理:从目标文件到程序加载的完整揭秘
linux·操作系统·编译·c/c++·目标文件·elf文件
Wentao Sun5 小时前
致敬软件创业者2026
笔记·程序人生
Fleshy数模5 小时前
MySQL 表创建全攻略:Navicat 图形化与 Xshell 命令行双模式实践
linux·mysql