[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");
}
相关推荐
超级大只老咪2 小时前
快速进制转换
笔记·算法
嵩山小老虎3 小时前
Windows 10/11 安装 WSL2 并配置 VSCode 开发环境(C 语言 / Linux API 适用)
linux·windows·vscode
Fleshy数模3 小时前
CentOS7 安装配置 MySQL5.7 完整教程(本地虚拟机学习版)
linux·mysql·centos
a41324473 小时前
ubuntu 25 安装vllm
linux·服务器·ubuntu·vllm
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.5 小时前
Keepalived VIP迁移邮件告警配置指南
运维·服务器·笔记
一只自律的鸡5 小时前
【Linux驱动】bug处理 ens33找不到IP
linux·运维·bug
17(无规则自律)5 小时前
【CSAPP 读书笔记】第二章:信息的表示和处理
linux·嵌入式硬件·考研·高考
!chen5 小时前
linux服务器静默安装Oracle26ai
linux·运维·服务器
ling___xi5 小时前
《计算机网络》计网3小时期末速成课各版本教程都可用谢稀仁湖科大版都可用_哔哩哔哩_bilibili(笔记)
网络·笔记·计算机网络