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


title: pipe

categories:

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

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

文章目录

fs/pipe.c 管道(Pipe)与FIFO的VFS层核心实现

历史与背景

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

管道(Pipe)是Unix哲学中最具标志性的发明之一,它的诞生是为了解决一个基础而强大的需求:将一个进程的标准输出直接连接到另一个进程的标准输入,从而实现进程间的单向数据流通信(Inter-Process Communication, IPC)

在管道出现之前,如果想实现 command1 | command2 的效果,可能需要:

  1. command1将其输出写入一个临时文件。
  2. command2等待command1执行完毕。
  3. command2再从那个临时文件中读取数据进行处理。
  4. 最后还需要清理这个临时文件。

这个过程效率低下(有磁盘I/O)、笨拙且容易出错。管道的出现,就是为了提供一个在内核内存中直接进行的、高效的、同步的数据流传输机制,它优雅地解决了以下问题:

  1. 消除临时文件:所有数据都在内核的缓冲区中传递,不涉及任何磁盘I/O。
  2. 实现流水线作业(Pipeline) :它使得command1command2可以并发执行command2不需要等待command1全部完成,一旦command1写入一些数据到管道中,command2就可以立即开始读取和处理,这极大地提高了效率和系统的响应性。
  3. 提供简单的接口 :通过标准的read(2)write(2)系统调用进行操作,完美地融入了Unix"一切皆文件"的哲学。

FIFO (First-In, First-Out),又称命名管道(Named Pipe),是管道的一个扩展。它解决了匿名管道只能用于有亲缘关系(父子)进程间通信的局限性。通过在文件系统中创建一个特殊的文件名,任何两个不相关的进程都可以通过打开这个文件名来建立一个管道连接。

它的发展经历了哪些重要的里程碑或版本迭代?

管道的概念自Unix早期就已存在,其核心思想非常稳定。Linux的实现(fs/pipe.c)的发展主要体现在性能和效率的优化上。

  • 环形缓冲区(Circular Buffer)fs/pipe.c的核心是实现了一个高效的内核环形缓冲区来暂存数据。
  • splice(2)tee(2)系统调用的引入 :这是一个重要的性能里程碑。传统的read/write模式需要将数据从内核空间拷贝到用户空间,再从用户空间拷贝回内核空间。splice(2)允许在两个文件描述符之间(例如,一个管道和一个套接字)**零拷贝(Zero-copy)**地移动数据,数据完全在内核空间内进行传递。tee(2)则允许零拷贝地将一个管道的数据复制到另一个管道,非常适合需要对数据流进行分叉处理的场景。
  • PIPE_BUF的原子性保证 :POSIX标准规定,对管道进行不大于PIPE_BUF字节的写入操作必须是原子的。fs/pipe.c的实现严格遵守了这个规范,这对于防止多个写入者的数据交错至关重要。
目前该技术的社区活跃度和主流应用情况如何?

管道是所有Linux/Unix-like系统中最基本、最常用的IPC机制之一。

  • 主流应用
    • 所有命令行Shellls -l | grep ".c" | wc -l 这样的命令流水线是管道最经典、最广泛的应用。
    • 脚本编程:在Shell脚本中,管道是组合各种命令行工具来完成复杂任务的核心粘合剂。
    • 应用程序内部:许多应用程序会使用管道来在其多进程模块之间传递数据。

核心原理与设计

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

fs/pipe.c的核心是实现了一个内核内的生产者-消费者模型,后端是一个环形缓冲区 。它巧妙地利用了VFS的inodefile结构,将一个非持久化的内存对象表现得像一个文件。

  1. 创建管道 (pipe(2))
    • 当进程调用pipe(pipefd)时,内核会执行do_pipe_flags()
    • 这个函数会创建一个新的**inode**对象。这个inode非常特殊,它不对应任何磁盘上的文件,而是代表这个新创建的管道。它的i_pipe字段会指向一个pipe_inode_info结构,该结构中包含了环形缓冲区本身以及相关的锁和等待队列。
    • 然后,内核会创建两个 struct file对象。
    • 一个file对象被设置为只读 (读端),另一个被设置为只写(写端)。
    • 这两个file对象都指向同一个管道inode
    • 最后,内核在调用进程的文件描述符表中找到两个空位,将这两个file对象的指针放进去,并返回两个FD(pipefd[0]为读端,pipefd[1]为写端)。
  2. 写入数据 (write(pipefd[1], ...)
    • 进程向写端FD写入数据。
    • 内核最终会调用pipe_write()
    • pipe_write()会获取管道inode上的锁,然后将用户空间的数据拷贝到环形缓冲区中。
    • 阻塞逻辑 :如果环形缓冲区已满,写入进程将在一个等待队列上睡眠,直到读端进程消费了一些数据,腾出空间为止。
    • 写入完成后,它会唤醒可能正在等待数据读取的进程。
  3. 读取数据 (read(pipefd[0], ...)
    • 进程从读端FD读取数据。
    • 内核最终会调用pipe_read()
    • pipe_read()会获取锁,然后从环形缓冲区中拷贝数据到用户空间。
    • 阻塞逻辑:如果环形缓冲区为空,读取进程将在等待队列上睡眠,直到写端进程写入了一些数据为止。
    • 读取完成后,它会唤醒可能正在等待缓冲区空间的写入进程。
  4. 关闭与EOF
    • 所有 持有写端FD的进程都关闭了它们的FD后,读端进程再进行read()就会立即返回0,表示文件结束(End-Of-File, EOF)。这是流水线能够正常终止的关键。
它的主要优势体现在哪些方面?
  • 简单性:提供了非常简洁的、基于文件描述符的API。
  • 高效性:数据在内存中传递,避免了磁盘I/O。
  • 并发性:天然支持生产者和消费者的并发执行。
  • 同步性:内核自动处理了缓冲区满或空时的阻塞和唤醒,为开发者提供了隐式的同步。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
  • 单向通信:匿名管道是半双工的,数据只能在一个方向上流动。如果需要双向通信,需要创建两个管道。
  • 仅限亲缘进程(匿名管道) :匿名管道的FD只能通过fork()在父子进程间继承,无法用于不相关的进程。
  • 无格式:管道传输的是无结构的字节流。数据的边界和格式需要通信双方自行约定。
  • 有限的缓冲区:管道的内核缓冲区大小是有限的(通常是64KB)。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?
  • 命令行流水线 :这是管道的"杀手级应用"。例如,tar -czf - my_dir/ | ssh remote_host "tar -xzf -"tar的输出直接通过管道流向ssh,实现了在网络间高效地传输文件归档,而无需在本地创建临时文件。
  • 将程序输出捕获到变量中 :在Shell脚本中,output=$(ls -l),这个命令替换(Command Substitution)的背后就是通过管道实现的。
  • 简单的多进程协调 :一个父进程fork出多个工作子进程,可以通过管道向它们分发任务,或者从它们那里收集结果。
是否有不推荐使用该技术的场景?为什么?
  • 双向或复杂通信 :不推荐。对于需要双向、请求-响应模式的通信,或者需要在多个进程间进行广播或多播的场景,**Unix域套接字(Unix Domain Sockets)网络套接字(Network Sockets)**是更强大、更灵活的选择。
  • 需要持久化或非亲缘IPC :如果数据需要在进程退出后依然存在,或者需要在任意两个进程间通信,应使用命名管道(FIFO)消息队列(Message Queues)
  • 传递结构化数据:如果需要在进程间传递复杂的数据结构,而不是简单的字节流,使用**共享内存(Shared Memory)**配合信号量(Semaphore)进行同步,会是最高效的方式,因为它避免了内核与用户空间之间的数据拷贝。

对比分析

请将其 与 其他相似技术 进行详细对比。
特性 匿名管道 (Pipe) 命名管道 (FIFO) Unix域套接字 (UDS)
标识方式 无名 。通过pipe()返回的FD访问。 有名。在文件系统中有一个路径名。 有名。在文件系统中有一个路径名。
通信范围 有亲缘关系的进程 (父子、兄弟)。 同一主机上的任意进程 同一主机上的任意进程
生命周期 随持有其FD的进程存在而存在。 随其文件系统中的节点存在而存在。 随其文件系统中的节点存在而存在。
通信模型 单向字节流 (半双工)。 单向字节流 (半双工)。 双向字节流 (类似TCP) 或 数据报 (类似UDP)。
创建方式 pipe() mkfifo() socket() with AF_UNIX
典型用途 命令行流水线,简单的父子进程通信。 作为不同服务间进行数据流转的"粘合点"。 客户端-服务器模型,本地的双向RPC。

管道文件系统初始化:创建 pipefs 和 /proc/sys/fs/pipe-* 调优接口

本代码片段的功能是初始化Linux内核中的"管道文件系统"(pipefs)。这是一个双重任务:首先,它注册并挂载了一个名为pipefs的特殊内存文件系统,这是内核实现匿名管道(|)和命名管道(FIFO)的基础;其次,它利用sysctl机制创建了一组位于/proc/sys/fs/目录下的接口,允许系统管理员在运行时查看和调整管道(pipe)的性能和资源限制参数。

实现原理分析

此代码的实现整合了VFS(虚拟文件系统)层和sysctl配置框架,是内核IPC(进程间通信)基础设施的关键部分。

  1. Pipe Filesystem (pipefs):

    • 注册 (register_filesystem) : init_pipe_fs函数首先调用register_filesystem,向VFS注册一个pipe_fs_typepipefs是一个"伪文件系统",它不与任何物理块设备关联,其所有的文件和目录对象(在这里主要是inode)都只存在于系统内存中。
    • 内部挂载 (kern_mount) : 注册成功后,内核立即通过kern_mount在内部"挂载"这个文件系统。这个挂载操作对用户空间是不可见的,它不会出现在mount命令的输出中。其目的是为了创建一个vfsmount实例和一个超级块(superblock),这是VFS层分配新inode(每个管道或FIFO都由一个inode表示)的必要前提。这个全局的pipe_mnt挂载点为内核中所有管道的创建提供了一个家。
  2. Sysctl配置接口:

    • 代码定义了一个fs_pipe_sysctls表,用于创建三个用户可调的参数:
      • pipe-max-size: 控制单个管道所能容纳的最大字节数。这是一个重要的性能和资源控制参数。
      • pipe-user-pages-hard / pipe-user-pages-soft: 控制一个非特权用户总共可以为管道分配多少内存页。这是一个防止普通用户通过创建大量管道而耗尽系统内存的资源限制机制。
    • 自定义处理函数 : pipe-max-size使用了一个特殊的proc handler proc_dopipe_max_size。这个handler通过一个自定义的转换函数do_proc_dopipe_max_size_conv来工作。
      • 输入值修正 (round_pipe_size) : 当用户写入一个值时,do_proc_dopipe_max_size_conv函数并不直接接受该值,而是调用round_pipe_size()对其进行处理。在内核中,管道的缓冲区大小通常被要求是页面大小(PAGE_SIZE)的整数倍,并且可能是2的幂次方以优化内存管理。round_pipe_size()函数就是用来将用户提供的任意值,修正(通常是向上取整)到下一个内核所支持的有效缓冲区大小。这个"输入消毒"的步骤极大地增强了系统的健壮性。

代码分析

c 复制代码
// do_proc_dopipe_max_size_conv: pipe-max-size的专用sysctl转换函数。
static int do_proc_dopipe_max_size_conv(unsigned long *lvalp,
					unsigned int *valp,
					int write, void *data)
{
	if (write) { // 写入路径
		unsigned int val;

		// 关键:调用round_pipe_size()将用户输入值修正为内核支持的有效大小。
		val = round_pipe_size(*lvalp);
		if (val == 0) // 修正后的值为0是无效的。
			return -EINVAL;

		*valp = val;
	} else { // 读取路径
		unsigned int val = *valp;
		*lvalp = (unsigned long) val;
	}

	return 0;
}

// proc_dopipe_max_size: pipe-max-size的proc handler。
static int proc_dopipe_max_size(const struct ctl_table *table, int write,
				void *buffer, size_t *lenp, loff_t *ppos)
{
	// 调用通用的无符号整数处理函数,但传入我们自定义的转换函数。
	return do_proc_douintvec(table, write, buffer, lenp, ppos,
				 do_proc_dopipe_max_size_conv, NULL);
}

// 定义在 /proc/sys/fs/ 目录下的pipe相关sysctl条目。
static const struct ctl_table fs_pipe_sysctls[] = {
	{
		.procname	= "pipe-max-size", // 文件名
		.data		= &pipe_max_size,  // 关联到内核的pipe_max_size变量
		.maxlen		= sizeof(pipe_max_size),
		.mode		= 0644,
		.proc_handler	= proc_dopipe_max_size, // 使用带修正逻辑的专用handler
	},
	{
		.procname	= "pipe-user-pages-hard", // 文件名,硬限制
		.data		= &pipe_user_pages_hard,
		.maxlen		= sizeof(pipe_user_pages_hard),
		.mode		= 0644,
		.proc_handler	= proc_doulongvec_minmax, // 使用标准的unsigned long handler
	},
	{
		.procname	= "pipe-user-pages-soft", // 文件名,软限制
		.data		= &pipe_user_pages_soft,
		.maxlen		= sizeof(pipe_user_pages_soft),
		.mode		= 0644,
		.proc_handler	= proc_doulongvec_minmax,
	},
};

// init_pipe_fs: pipefs的初始化函数。
static int __init init_pipe_fs(void)
{
	int err = register_filesystem(&pipe_fs_type);

	if (!err) {
		// 在内核内部挂载pipefs,为创建管道的inode提供一个根。
		pipe_mnt = kern_mount(&pipe_fs_type);
		if (IS_ERR(pipe_mnt)) {
			err = PTR_ERR(pipe_mnt);
			unregister_filesystem(&pipe_fs_type);
		}
	}
#ifdef CONFIG_SYSCTL
	// 注册上面定义的sysctl表。
	register_sysctl_init("fs", fs_pipe_sysctls);
#endif
	return err;
}

// 将初始化函数注册为fs_initcall。
fs_initcall(init_pipe_fs);

管道资源管理:默认值和配额

本代码片段的功能是为Linux内核中的管道(pipe)机制定义一组核心的、默认的资源限制参数。它不执行任何动作(如注册或初始化),而是为管道的创建和大小调整操作提供静态的、全局性的默认值和配额。这些变量是管道子系统在平衡性能、保证正确性和防止资源耗尽之间做出决策的基础。

实现原理分析

这些变量是内核管道实现中硬编码的策略和限制,后续会由管道的fcntl操作(如F_SETPIPE_SZ)和内存分配逻辑直接使用。

  1. 保证正确性 (PIPE_MIN_DEF_BUFFERS):

    • 这个宏定义了当用户处于其配额限制下时,内核仍允许为新管道分配的最小缓冲区数量。
    • 其存在的核心原因是为了防止特定场景下的死锁。正如注释中详细解释的,当管道被用作信号量或令牌传递机制时(例如GNU make的jobserver),一个进程可能需要在读取一个新"令牌"之前,先向管道写回一个旧"令牌"。如果管道大小仅为一个缓冲区,且该缓冲区已被这个进程自己尚未读取的数据占满,那么这个写操作就会阻塞,而进程又因为阻塞而无法执行读操作来清空缓冲区,从而形成死锁。
    • 将最小值设置为2确保了总有一个额外的空闲缓冲区可供写入,从而打破了这种潜在的死锁循环。
  2. 单管道大小限制 (pipe_max_size):

    • 这个变量定义了一个非特权用户 可以通过fcntl(fd, F_SETPIPE_SZ, size)系统调用将单个管道的缓冲区设置为的最大尺寸。
    • 这是一个单管道的资源上限,默认值为1MB。其目的是防止单个应用程序通过创建一个巨大的管道来不成比例地消耗大量内核内存。
    • 系统管理员(root用户)可以通过修改/proc/sys/fs/pipe-max-size来调整这个全局限制。特权进程在调用F_SETPIPE_SZ时也不受此变量的限制。
  3. 用户总配额 (pipe_user_pages_hard / pipe_user_pages_soft):

    • 这两个变量实现了一个每个用户的管道内存总配额机制。
    • pipe_user_pages_soft(软限制): 定义了一个"警戒线"。当一个非特权用户所拥有的所有管道的总内存占用(以页为单位)超过这个值时,内核在为该用户创建新管道或扩大现有管道时,会将其缓冲区大小限制在一个较小的值(由PIPE_MIN_DEF_BUFFERS决定)。其默认值是根据系统默认的单个管道缓冲区数(PIPE_DEF_BUFFERS)和默认的每个进程可打开文件数(INR_OPEN_CUR)估算出的一个"合理"值。
    • pipe_user_pages_hard(硬限制): 定义了一个绝对的上限。当一个非特权用户的总管道内存占用达到这个值时,任何进一步的管道内存分配请求都将失败。默认情况下,它被初始化为0,在内核中通常意味着"无限制"。
    • 这套配额机制是防止单个用户(或其失控的程序)通过创建大量管道来耗尽整个系统内核内存的关键防御措施。

代码分析

c 复制代码
/*
 * 当用户超出其管道缓冲区配额时,新的管道缓冲区将被限制在此大小。
 * 一般的管道用例至少需要两个缓冲区:一个用于尚未读取的数据,另一个用于新数据。
 * 如果这个值小于2,那么即使管道未满,对非空管道的写入也可能阻塞。
 * 这可能发生在GNU make的jobserver或类似的使用管道作为信号量的场景中:
 * 多个进程可能在读取令牌之前等待将令牌写回管道。
 *
 * 用户可以通过F_SETPIPE_SZ将管道缓冲区减小到此值以下,但风险自负,
 * 即:对未满管道的写入可能会阻塞,直到管道被清空。
 */
#define PIPE_MIN_DEF_BUFFERS 2

/*
 * 一个非root用户被允许将管道扩大的最大尺寸。
 * root用户可以在/proc/sys/fs/pipe-max-size中设置。
 */
static unsigned int pipe_max_size = 1048576; // 默认值为1MB。

/* 
 * 每个用户可分配的最大页数。硬限制默认不设置,软限制匹配默认值。
 */
// 硬限制:一个用户可以为管道分配的总内存页数的绝对上限。初始化为0表示默认无限制。
static unsigned long pipe_user_pages_hard;
// 软限制:当用户的管道总内存页数超过此值时,新管道的分配会受到限制。
// 其默认值是根据系统默认的管道缓冲区数和默认的打开文件数计算出的一个合理值。
static unsigned long pipe_user_pages_soft = PIPE_DEF_BUFFERS * INR_OPEN_CUR;

Pipe Filesystem VFS Implementation: Defining Pipe Behavior within the Virtual File System

本代码片段的功能是为内核中的"管道文件系统"(pipefs)定义其在虚拟文件系统(VFS)层面的核心行为。它不处理管道的数据读写,而是为管道的inode(索引节点)和dentry(目录条目)对象提供必要的操作函数集。这使得管道------一个纯粹的内存IPC(进程间通信)机制------能够被VFS识别和管理,就像它是一个普通文件一样,从而可以被ls, stat等工具(在特定上下文中,如/proc/[pid]/fd/)所操作。

实现原理分析

此代码是VFS"粘合层"的典型实现,它将一个非传统的文件系统实体(pipefs)集成到标准的内核VFS框架中。

  1. 超级块操作 (pipefs_ops):

    • struct super_operations定义了应用于整个文件系统实例(由一个超级块superblock表示)的操作。
    • .destroy_inode = free_inode_nonrcu: 这个回调函数指定了当一个pipefs的inode的引用计数降为零时,内核应该如何释放它。free_inode_nonrcu是一个标准的、非RCU(Read-Copy-Update)安全的inode释放函数。对于pipefs这种inode不会被频繁查找和遍历的伪文件系统,使用非RCU版本是合适的,因为它更简单,开销也更小。
    • .statfs = simple_statfs: 这个回调函数实现了statfs(2)系统调用。simple_statfs是一个通用的辅助函数,用于那些没有物理后备存储的内存文件系统。当用户对一个pipefs实例(虽然用户通常不会直接这样做)调用statfs时,它会返回一组合理的、表示"无容量限制"的默认值。
  2. 目录条目操作 (pipefs_dentry_operations):

    • struct dentry_operations定义了应用于单个目录条目(dentry)的操作。dentry是文件名和inode之间的链接。
    • .d_dname = pipefs_dname: 这是本片段中最核心的功能。它指定了如何为一个pipefs的dentry生成一个"可打印"的名称。
  3. 动态命名 (pipefs_dname):

    • 这个函数在内核需要将一个pipe的dentry转换成路径字符串时被调用(例如,在读取/proc/[pid]/fd/目录时)。
    • 它不返回一个固定的文件名,而是调用dynamic_dname动态地格式化一个唯一的名称,格式为pipe:[inode号]。例如,一个inode号为12345的管道将被命名为pipe:[12345]
    • 这种命名方案确保了:a) 管道的名称是唯一的;b) 它清晰地向用户表明这是一个管道,而不是一个普通文件;c) 它不依赖于任何磁盘上的目录结构。

代码分析

c 复制代码
/*
 * pipefs_dname() 在 d_path() 中被调用。
 */
// pipefs_dname: 为一个pipefs的目录条目(dentry)生成一个动态名称。
// @dentry: 目标目录条目。
// @buffer: 用于存放生成名称的缓冲区。
// @buflen: 缓冲区的长度。
static char *pipefs_dname(struct dentry *dentry, char *buffer, int buflen)
{
	// 调用 dynamic_dname 辅助函数,安全地格式化一个字符串。
	// 格式为 "pipe:[inode号]",例如 "pipe:[12345]"。
	// d_inode(dentry)->i_ino 用于从dentry获取其关联的inode的编号。
	return dynamic_dname(buffer, buflen, "pipe:[%lu]",
				d_inode(dentry)->i_ino);
}

// 定义pipefs的目录条目操作集。
static const struct dentry_operations pipefs_dentry_operations = {
	// .d_dname: 将d_dname操作指向上面定义的 pipefs_dname 函数。
	.d_dname	= pipefs_dname,
};

// 定义pipefs的超级块操作集。
static const struct super_operations pipefs_ops = {
	// .destroy_inode: 指定当inode引用计数为0时,用于释放inode内存的函数。
	// free_inode_nonrcu是一个标准的、非RCU安全的释放函数。
	.destroy_inode = free_inode_nonrcu,
	// .statfs: 指定用于获取文件系统统计信息(通过statfs系统调用)的函数。
	// simple_statfs为内存文件系统提供一个通用的、默认的实现。
	.statfs = simple_statfs,
};
相关推荐
oMcLin2 小时前
Ubuntu 22.04 系统中不明原因的磁盘 I/O 高负载:如何利用 iotop 和 systemd 排查优化
linux·运维·ubuntu
fengyehongWorld2 小时前
Linux systemd 与 systemctl 命令
linux·运维·服务器
d111111111d2 小时前
STM32 DMA传输配置详解:数据宽度与传输方向设置指南
笔记·stm32·单片机·嵌入式硬件·学习
Howrun7772 小时前
不可重入函数Non-Reentrant & 可重入函数Reentrant
linux·服务器
Thera7772 小时前
Linux 核心绑定(CPU Affinity)详解:原理、方法与优缺点分析
linux·运维·服务器
じ☆冷颜〃2 小时前
二分查找的推广及其在排序与链表结构中的关联
网络·windows·经验分享·笔记·算法·链表
幽络源小助理2 小时前
SpringBoot+Vue智能学习平台系统源码 | 教育类JavaWeb项目免费下载 – 幽络源
vue.js·spring boot·学习
小鹏linux2 小时前
【linux】进程与服务管理命令 - setup
linux·运维·服务器
倔强的石头1062 小时前
【Linux指南】进程控制系列(二)进程终止 —— 退出场景、方法与退出码详解
linux·运维·服务器