进程间通信 IPC (Inter Process Communication)
管道
管道(Pipe)是 Linux 内核提供的一种进程间通信(IPC)机制,它基于一个名为 pipefs 的内核伪文件系统实现。
pipefs 简单介绍
- 内核启动早期就会
kern_mount(&pipe_fs_type),把 pipefs 挂在内部 VFS 命名空间里,用户态永远ls不到它,但所有匿名管道对象都“住”在这个文件系统里 。 - 它只存在于内存,没有对应的磁盘块设备;super-block 也只有一个,全局共享 。
- 在
/proc/filesystems中能看到一行nodev pipefs,表示“无设备节点”的伪文件系统。
匿名管道在 pipefs 里的“一生”
① 进程调用 `pipe()/pipe2()`
↓
② 内核调用 `do_pipe_flags()`
– 在 pipefs 中分配一个 inode(`struct inode *inode = pipefs_create_inode(...)`)[](https://my.oschina.net/emacs_8773548/blog/17230515)
– 同一 inode 生成两个 `struct file`:一个只读、一个只写,对应返回给用户的 fd[0]、fd[1]
↓
③ 父子进程通过 `fork` 继承这两个 fd,就能以“写 fd→pipefs 缓冲区→读 fd”完成半双工字节流通信[](https://www.cnblogs.com/cps666/p/17339265.html)
↓
④ 所有 fd 关闭后,inode 引用计数归零,pipefs 自动回收这块内存缓冲区。
命名管道(FIFO)与 pipefs 的关系
(1) FIFO 也要复用 pipefs 的 inode 和缓冲区代码,但它会先在一个普通文件系统(ext4、xfs…)里创建一个“入口”文件,权限、路径对用户可见;打开时内核再把该文件绑定到 pipefs 新生成的 inode 上,因此无血缘关系的进程也能通过同一路径打开它进行通信 。
虚拟文件系统中的pipe数据结构
pipe_inode_info - 管道的核心结构
struct pipe_inode_info {
struct mutex mutex; // 互斥锁
wait_queue_head_t rd_wait; // 读等待队列
wait_queue_head_t wr_wait; // 写等待队列
unsigned int head; // 环形缓冲区头指针
unsigned int tail; // 环形缓冲区尾指针
unsigned int max_usage; // 最大使用量
unsigned int ring_size; // 环形缓冲区大小
struct pipe_buffer *bufs; // 缓冲区数组(粗放管道数据的 环形数组)
// ...
};
pipe_buffer - 管道缓冲区
struct pipe_buffer {
struct page *page; // 存储数据的物理页
unsigned int offset; // 页内偏移
unsigned int len; // 数据长度
const struct pipe_buf_operations *ops; // 操作函数
};
匿名管道
匿名管道(Anonymous Pipe)是 Unix/Linux 系统里最简单、最轻量的一种进程间通信(IPC)机制。它本质上是一块内核维护的内存缓冲区,由两个文件描述符(fd)分别充当“读端”和“写端”——父进程在调用 pipe() 系统调用时一次性拿到一对 fd:fd[0] 只读、fd[1] 只写。
因为管道没有名字,所以只能在亲缘进程(父子、兄弟)之间使用:父进程先 pipe(),再 fork(),把不需要的一端关掉,于是子进程和父进程就通过这一对 fd 单向传数据——数据从写端进去,从读端出来,字节流、先进先出、无结构边界。
Shell 里常见的 ps -ef | grep 关键字 | awk '{print $2}' 中的竖线“|”就是匿名管道的壳层语法:
- 每遇到一个“|”,Shell 都会提前 pipe() 创建一条匿名管道;
- 然后把左边命令的 stdout(fd 1)重定向到管道的写端,把右边命令的 stdin(fd 0)重定向到读端;
- 最后并发启动两条命令,数据在内核缓冲区里流动,对用户完全透明。
特点速记
- 单向通信:数据只能从一个进程流向另一个进程,不能反向
- 无名字:只能用于具有共同祖先的进程。
- 字节流:读写端不保留消息边界,需要应用自己定义协议。
- 缓冲区有限:Linux 默认 64 KB,写端阻塞或返回 EAGAIN。
- 随进程存亡:所有指向它的 fd 关闭后,内核自动回收。
- 只能父子传递:不能打开两次,也不能在毫无关系的进程间使用。
一句话:匿名管道就是“内核给你一根临时水管”,让父子进程可以按字节顺序、单向、无痛地传数据,用完即走,不占文件系统名字空间。
命名管道
prw-r--r-- 1 root root hello 其中‘ p ’ 就是管道pipe的缩写, hello就是管道文件
命名管道(Named Pipe,FIFO)就是“有名字的管道”。它继承了匿名管道“字节流、先进先出”的语义,却突破了两个限制:
- 无需亲缘关系——任何进程只要知道路径就能打开;
- 生命周期随文件系统——不依赖进程是否健在。
应用开发使用的管道API
1.1 匿名管道创建
/**
* pipe - 创建匿名管道
* @pipefd: 包含两个整数的数组,用于接收管道文件描述符
*
* 成功时:pipefd[0] = 读端文件描述符
* pipefd[1] = 写端文件描述符
* 返回值:0成功,-1失败并设置errno
*
* 注意:创建的管道是阻塞模式,缓冲区大小由操作系统决定
*/
#include <unistd.h>
int pipe(int pipefd[2]);
/**
* pipe2 - 创建匿名管道并设置标志
* @pipefd: 包含两个整数的数组
* @flags: 管道标志位(O_NONBLOCK, O_CLOEXEC)
*
* 相比pipe()的优势:
* 1. 原子性:创建管道和设置标志是原子操作
* 2. 支持close-on-exec标志
*
* 返回值:0成功,-1失败
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
int pipe2(int pipefd[2], int flags);
/* 可用标志位 flags:
* O_NONBLOCK - 非阻塞模式
* O_CLOEXEC - exec时自动关闭(Linux 2.6.23+)
*/
1.2 命名管道创建
/**
* mkfifo - 创建命名管道(FIFO)
* @pathname: FIFO文件路径
* @mode: 文件权限模式(受umask影响)
*
* 创建的FIFO可以用open()打开,支持多进程访问
* 读端打开时会阻塞直到写端打开,除非指定O_NONBLOCK
*
* 返回值:0成功,-1失败
*/
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
/**
* mkfifoat - 在指定目录创建FIFO
* @dirfd: 目录文件描述符
* @pathname: FIFO文件路径(相对或绝对)
* @mode: 文件权限模式
*
* 作用类似于mkfifo(),但可以指定目录
* 如果pathname是绝对路径,则忽略dirfd
*
* 返回值:0成功,-1失败
*/
#include <fcntl.h>
#include <sys/stat.h>
int mkfifoat(int dirfd, const char *pathname, mode_t mode);
/**
* mknod - 创建特殊文件(包括FIFO)
* @pathname: 文件路径
* @mode: 文件类型和权限
* @dev: 设备号(创建FIFO时设为0)
*
* 可以用来创建FIFO:mode = S_IFIFO | permissions
* 通常更推荐使用mkfifo(),语义更明确
*
* 返回值:0成功,-1失败
*/
#include <sys/stat.h>
#include <sys/types.h>
int mknod(const char *pathname, mode_t mode, dev_t dev);
/**
* mknodat - 在指定目录创建特殊文件
* @dirfd: 目录文件描述符
* @pathname: 文件路径
* @mode: 文件类型和权限
* @dev: 设备号
*
* 返回值:0成功,-1失败
*/
int mknodat(int dirfd, const char *pathname, mode_t mode, dev_t dev);
2. 标准I/O封装API
/**
* popen - 创建管道并启动进程
* @command: 要执行的shell命令
* @type: 打开模式:
* "r" - 父进程读取子进程的标准输出
* "w" - 父进程写入到子进程的标准输入
*
* 功能:
* 1. 创建管道
* 2. fork()创建子进程
* 3. 在子进程中执行shell命令
* 4. 返回标准I/O FILE指针
*
* 注意:popen()返回的FILE指针需要由pclose()关闭,而不是fclose()
*/
#include <stdio.h>
FILE *popen(const char *command, const char *type);
/**
* pclose - 关闭popen()创建的管道流
* @stream: popen()返回的FILE指针
*
* 功能:
* 1. 关闭标准I/O流
* 2. 等待子进程结束
* 3. 返回子进程的退出状态
*
* 返回值:子进程退出状态,-1表示错误
*/
int pclose(FILE *stream);
3. 零拷贝高效传输API
/**
* splice - 在内核空间移动数据,避免用户空间拷贝
* @fd_in: 源文件描述符
* @off_in: 源偏移指针(NULL表示当前位置)
* @fd_out: 目标文件描述符
* @off_out: 目标偏移指针(NULL表示当前位置)
* @len: 要移动的字节数
* @flags: 控制标志位
*
* 功能:将数据从fd_in移动到fd_out,不经过用户空间
* 支持:管道->管道、管道->文件、文件->管道
*
* 返回值:成功移动的字节数,-1失败
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
/* splice()标志位:
* SPLICE_F_MOVE - 尝试移动页而不是复制(当前未实现)
* SPLICE_F_NONBLOCK - 非阻塞操作
* SPLICE_F_MORE - 提示后续还有更多数据
* SPLICE_F_GIFT - 未使用
*/
/**
* vmsplice - 将用户空间内存"赠送"给管道
* @fd: 管道写端文件描述符
* @iov: iovec结构体数组
* @nr_segs: iov数组元素个数
* @flags: 控制标志位
*
* 功能:将用户空间数据写入管道,可避免拷贝
* 注意:数据可能被内核重用,写入后不应再访问
*
* 返回值:成功写入的字节数,-1失败
*/
ssize_t vmsplice(int fd, const struct iovec *iov, unsigned long nr_segs, unsigned int flags);
/* vmsplice()标志位 flags:
* SPLICE_F_GIFT - 用户放弃缓冲区所有权(必须为匿名内存)
* SPLICE_F_NONBLOCK - 非阻塞操作
*/
/**
* tee - 在两个管道间复制数据,不消耗源数据
* @fd_in: 源管道读端
* @fd_out: 目标管道写端
* @len: 要复制的字节数
* @flags: 控制标志位
*
* 功能:类似splice(),但不从源管道移除数据
* 只能用于管道到管道的复制
*
* 返回值:成功复制的字节数,-1失败
*/
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
/* tee()标志位 flags:
* SPLICE_F_NONBLOCK - 非阻塞操作
* SPLICE_F_MOVE - 未实现
* SPLICE_F_GIFT - 未使用
*/
4. 控制与查询API
/**
* fcntl - 控制文件描述符属性
* @fd: 文件描述符
* @cmd: 命令
* @...: 可变参数(取决于cmd)
*
* 用于管道的常见命令:
* F_GETPIPE_SZ - 获取管道容量
* F_SETPIPE_SZ - 设置管道容量(Linux 2.6.35+)
* F_GETFL - 获取文件状态标志
* F_SETFL - 设置文件状态标志(如O_NONBLOCK)
*/
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
// 使用示例:
int get_pipe_size(int fd) {
return fcntl(fd, F_GETPIPE_SZ); // 返回管道缓冲区大小
}
int set_pipe_size(int fd, int size) {
return fcntl(fd, F_SETPIPE_SZ, size); // 设置新大小
}
/**
* fpathconf - 获取管道相关配置值
* @fd: 文件描述符
* @name: 配置名称
*
* 常用name值:
* _PC_PIPE_BUF - 原子写入的最大字节数(PIPE_BUF)
* _PC_NAME_MAX - 路径名最大长度
*
* 返回值:配置值,-1失败
*/
#include <unistd.h>
long fpathconf(int fd, int name);
// 使用示例:
long pipe_buf = fpathconf(pipefd[0], _PC_PIPE_BUF);
5. 高级控制API
/**
* pipe_buf - 原子写入保证常量
*
* POSIX保证写入小于PIPE_BUF字节的数据是原子的
* 多个进程/线程同时写入不会交错
* 典型值:512, 4096, 或由_PC_PIPE_BUF查询
*/
#define PIPE_BUF 4096 /* 常见值,实际值应使用fpathconf获取 */
/**
* ioctl - 设备I/O控制(管道特定操作)
* @fd: 文件描述符
* @request: 请求码
* @...: 可变参数
*
* 管道相关请求(Linux特有):
* FIONREAD - 获取可读字节数
*
* 注意:ioctl不是POSIX标准,可移植性差
*/
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
// 使用示例:
int bytes_available;
ioctl(pipefd[0], FIONREAD, &bytes_available);
拓展: 我开发中遇到的疑惑
由于我开发过程中总把pipe管道与epoll搞混,pipe管道最新api不是epoll吗?
epoll 并不是“管道的新 API”,而是 Linux 的 I/O 多路复用机制;它跟 pipe/pipe2 属于两个层面的东西,只是经常一起使用。
一句话区分:
- pipe/pipe2 —— 负责“创建”管道,返回一对 fd;
- epoll —— 负责“监听”任何 fd(包括管道、socket、eventfd…)上的读写事件。
为什么容易混淆
- 在 Android Looper、libuv、Go runtime 等事件循环里,最常见的配方就是:
pipe2()创建通知管道 →epoll_ctl()把读端 fd 加进 epoll 监听表 → 其他线程/进程往写端丢一个字节,epoll 立刻返回,实现“自唤醒”。 - 因为这套组合太经典,很多人就把“管道+epoll”当成“管道的新 API”,其实 epoll 本身对管道没有任何特殊路径,只是把它当成普通的 pollable file。
最新的“管道 API”依旧只有两级
| 系统调用 | 出现版本 | 作用 | 备注 |
|---|---|---|---|
pipe(int fd[2]) | 1973 UNIX V3 | 创建匿名管道 | POSIX 标准 |
pipe2(int fd[2], int flags) | Linux 2.6.27 | 同上,但可一次性指定 O_NONBLOCK、O_CLOEXEC 等标志 | Linux 专属 |
2008 年以后,内核侧只对管道实现做过 容量可调(F_SETPIPE_SZ)、环形页缓存 等性能优化,没有再新增系统调用;用户态 API 保持稳定。
结论
- 如果你问“创建/读写管道”的最新接口,答案仍然是
pipe2(); - 如果你问“怎样让管道不阻塞又及时响应”,答案是把它扔进
epoll(或select/poll)里监听——但那是 I/O 多路复用层的事,不是管道层的新 API。