[toc]

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 文件系统的上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 文件系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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");
}