命名管道(FIFO / 有名管道)
命名管道(Named Pipe),也称为FIFO(First In First Out),是 Linux 系统中一种特殊的进程间通信(IPC)机制。它的本质是一个存在于文件系统中的特殊文件,通过文件路径名标识,允许任意进程(无论是否有亲缘关系)通过打开该文件进行数据传输,数据遵循 "先进先出" 的原则。
1.1 与匿名管道的核心区别
特性 命名管道(FIFO) 匿名管道(Pipe) 存在形式 存在于文件系统中,有具体路径名 仅存在于内核中,无文件系统实体 适用进程 任意进程(无亲缘关系也可通信) 仅适用于有亲缘关系的进程(父子 / 兄弟) 创建方式 mkfifo()系统调用或mkfifo命令pipe()系统调用生命周期 手动删除( rm)或文件系统卸载随进程退出自动销毁 打开方式 通过 open()函数按路径打开直接使用 pipe()返回的文件描述符可复用性 可被多个进程反复打开和使用 仅能被创建它的进程及其子进程使用 1.2 核心特性
- 先进先出:数据的读取顺序与写入顺序完全一致,不支持随机访问。
- 半双工通信:同一时刻只能单向传输数据,双向通信需要两个命名管道。
- 文件系统可见:通过
ls -l命令查看时,文件类型标识为p(pipe)。- 内核缓冲:数据存储在内核缓冲区中,不落地到磁盘,性能接近匿名管道。
- 原子操作保障:当写入数据量小于等于
PIPE_BUF时,系统保证写入的原子性(不会被其他进程的写入打断)。
2.1 内核数据结构
命名管道的底层实现依赖于内核中的两个核心结构:
struct inode:文件系统中的索引节点,标识命名管道的存在,其i_pipe指针指向管道的核心数据结构。struct pipe_inode_info:管道的核心控制结构,包含:
- 环形缓冲区:存储传输的数据(默认大小通常为 64KB,可通过
fcntl调整)。- 读写等待队列:阻塞的读 / 写进程队列,用于实现同步机制。
- 引用计数:记录当前打开该管道的进程数。
- 互斥锁与自旋锁:保障并发访问的线程安全。
2.2 数据传输流程
命名管道的数据传输本质是用户态 - 内核态 - 用户态的拷贝过程:
- 写进程:通过
write()将用户空间数据拷贝到内核的管道缓冲区。- 内核调度:当缓冲区有数据时,唤醒阻塞的读进程。
- 读进程:通过
read()将内核缓冲区的数据拷贝到用户空间。注意:命名管道的数据不会写入磁盘,仅存在于内核内存中,因此性能远高于普通文件传输。
3.1 创建命名管道
3.1.1 命令行创建
使用
mkfifo命令直接在文件系统中创建命名管道:# 创建名为myfifo的命名管道,权限为0666(读写权限) mkfifo -m 0666 myfifo # 查看管道文件 ls -l myfifo # 输出:prw-rw-rw- 1 user user 0 6月 10 10:00 myfifo3.1.2 系统调用创建
mkfifo()函数是创建命名管道的核心系统调用,原型如下:#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);
- 参数说明:
pathname:管道文件的路径名(绝对路径或相对路径)。mode:管道文件的权限(如0666),实际权限会受umask影响。- 返回值:成功返回 0,失败返回 - 1 并设置
errno。- 常见错误码:
EEXIST:指定路径的文件已存在。ENOENT:路径中的目录不存在。EACCES:没有权限创建文件。3.2 打开命名管道
命名管道通过
open()函数打开,但其行为与普通文件有显著区别:#include <fcntl.h> int open(const char *pathname, int flags);3.2.1 阻塞模式(默认)
- 只读打开(O_RDONLY):如果没有进程以只写模式打开该管道,调用会阻塞,直到有写进程打开。
- 只写打开(O_WRONLY):如果没有进程以只读模式打开该管道,调用会阻塞,直到有读进程打开。
- 读写打开(O_RDWR):不会阻塞,但这种用法不符合管道的设计初衷,一般不推荐。
3.2.2 非阻塞模式(O_NONBLOCK)
当
flags包含O_NONBLOCK时,open()不会阻塞:
- 只读打开:如果没有写进程,立即返回成功(后续读操作可能返回 0)。
- 只写打开:如果没有读进程,立即返回失败,
errno设为ENXIO。3.3 读写操作
命名管道的读写操作与普通文件一致,使用
read()和write()函数,但有特殊的行为规则:3.3.1 读操作(read ())
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count);
参数名 类型 核心作用 fdint文件描述符,即通过 open()打开 FIFO 后返回的整数(必须是O_RDONLY/O_RDWR模式)bufvoid *指向用户空间缓冲区的指针,用于存储从 FIFO 读取的数据(需提前分配内存) countsize_t期望读取的最大字节数(不能超过 buf的内存大小,否则会导致缓冲区溢出)返回值
- 正数:成功读取的字节数(可能小于
count,比如 FIFO 中剩余数据不足);- 0:所有写端已关闭(FIFO 到达 EOF,这是命名管道的关键特征);
- -1:读取失败,需检查
errno(结合 FIFO 场景的关键错误码见下文)。FIFO 场景专属注意事项(对应之前的 3.3.1)
- 阻塞模式(默认):若 FIFO 无数据但写端未关闭,
read()会阻塞,直到有数据写入或写端全部关闭;- 非阻塞模式(
O_NONBLOCK):若 FIFO 无数据,read()立即返回 - 1,且errno = EAGAIN(需重试,非真正错误);- 常见错误码:
EAGAIN:非阻塞模式下无数据;EBADF:fd不是有效的读文件描述符;EINTR:读取过程中被信号中断(可重试)。3.3.2 写操作(write ())
#include <unistd.h> ssize_t write(int fd, const void *buf, size_t count);
参数名 类型 核心作用 fdint文件描述符,即通过 open()打开 FIFO 后返回的整数(必须是O_WRONLY/O_RDWR模式)bufconst void *指向用户空间缓冲区的指针,存储要写入 FIFO 的数据(数据需提前准备) countsize_t期望写入的字节数(注意 PIPE_BUF阈值,保证原子性)返回值
- 正数:成功写入的字节数(可能小于
count,比如 FIFO 缓冲区空间不足);- -1:写入失败,需检查
errno(结合 FIFO 场景的关键错误码见下文);- 注意:
write()不会返回 0(这是与read()的核心区别)。FIFO 场景专属注意事项(对应之前的 3.3.2)
- 阻塞模式(默认):若 FIFO 缓冲区空间不足,
write()会阻塞,直到有读进程取走数据、缓冲区有空间;- 非阻塞模式(
O_NONBLOCK):若缓冲区不足,write()立即返回 - 1,且errno = EAGAIN(需重试);- 关键错误 / 信号:
- 若所有读端已关闭,
write()会触发SIGPIPE信号(默认终止进程),若捕获该信号,write()返回 - 1 且errno = EPIPE;- 常见错误码:
EAGAIN(非阻塞无空间)、EPIPE(读端全关)、EBADF(无效写描述符)、EINTR(被信号中断);- 原子性:当
count ≤ PIPE_BUF(通常 4096 字节)时,系统保证写入原子性,多进程并发写入不会错乱;超过则无原子性保证。3.3.3 原子操作与 PIPE_BUF
PIPE_BUF是 Linux 内核定义的宏(在<limits.h>中),表示管道的原子写入阈值,通常为 4096 字节(4KB)。
- 当写入数据量 ≤
PIPE_BUF时,系统保证写入的原子性,不会被其他进程的写入打断。- 当写入数据量 >
PIPE_BUF时,写入可能被拆分,不保证原子性,多进程并发写入可能导致数据错乱。可以通过
sysconf()函数获取系统的PIPE_BUF值:#include <unistd.h> long pipe_buf = sysconf(_SC_PIPE_BUF);3.4 关闭与销毁
- 关闭:通过
close()函数关闭管道的文件描述符,当最后一个进程关闭管道时,内核会释放管道的内核缓冲区资源。- 销毁:命名管道作为文件系统中的实体,需要手动通过
unlink()系统调用或rm命令删除,否则会一直存在于文件系统中。
4.1 基础读写通信示例
写进程(fifo_writer.c)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #define FIFO_PATH "./myfifo" #define BUFFER_SIZE 1024 int main() { // 1. 创建命名管道,权限0666(读写权限) int ret = mkfifo(FIFO_PATH, 0666); if (ret == -1) { perror("mkfifo failed"); // 若管道已存在,忽略错误(允许复用) if (errno != EEXIST) { exit(EXIT_FAILURE); } } // 2. 以只写模式打开管道(阻塞模式) int fd = open(FIFO_PATH, O_WRONLY); if (fd == -1) { perror("open fifo failed"); exit(EXIT_FAILURE); } printf("Writer: 成功打开管道,等待读进程连接...\n"); // 3. 写入数据 const char *messages[] = { "Hello from writer!", "This is a named pipe example.", "End of message." }; int msg_count = sizeof(messages) / sizeof(messages[0]); for (int i = 0; i < msg_count; i++) { ssize_t bytes_written = write(fd, messages[i], strlen(messages[i]) + 1); if (bytes_written == -1) { perror("write failed"); close(fd); exit(EXIT_FAILURE); } printf("Writer: 已写入数据: %s\n", messages[i]); sleep(1); // 模拟间隔写入 } // 4. 关闭管道 close(fd); printf("Writer: 数据写入完成,关闭管道\n"); return 0; }读进程(fifo_reader.c)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #define FIFO_PATH "./myfifo" #define BUFFER_SIZE 1024 int main() { // 1. 以只读模式打开管道(阻塞模式) int fd = open(FIFO_PATH, O_RDONLY); if (fd == -1) { perror("open fifo failed"); exit(EXIT_FAILURE); } printf("Reader: 成功打开管道,开始读取数据...\n"); // 2. 读取数据 char buffer[BUFFER_SIZE]; ssize_t bytes_read; while ((bytes_read = read(fd, buffer, BUFFER_SIZE)) > 0) { printf("Reader: 读取到数据: %s\n", buffer); memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区 } // 3. 处理读取结果 if (bytes_read == 0) { printf("Reader: 写端已关闭,读取完成\n"); } else if (bytes_read == -1) { perror("read failed"); close(fd); exit(EXIT_FAILURE); } // 4. 关闭管道并删除文件 close(fd); unlink(FIFO_PATH); // 读取完成后删除管道 printf("Reader: 关闭管道并删除文件\n"); return 0; }编译与运行
# 编译 gcc fifo_writer.c -o writer gcc fifo_reader.c -o reader # 运行(两个终端分别执行) # 终端1:运行读进程 ./reader # 终端2:运行写进程 ./writer4.2 非阻塞模式通信示例
修改读进程的打开方式,使用非阻塞模式:
// 以只读+非阻塞模式打开管道 int fd = open(FIFO_PATH, O_RDONLY | O_NONBLOCK); if (fd == -1) { perror("open fifo failed"); exit(EXIT_FAILURE); } // 非阻塞读取逻辑 while (1) { ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE); if (bytes_read > 0) { printf("Reader: 读取到数据: %s\n", buffer); memset(buffer, 0, BUFFER_SIZE); } else if (bytes_read == 0) { printf("Reader: 写端已关闭\n"); break; } else { if (errno == EAGAIN) { // 无数据,等待1秒后重试 printf("Reader: 暂无数据,等待...\n"); sleep(1); } else { perror("read failed"); break; } } }
5.1 阻塞与非阻塞机制的底层逻辑
命名管道的阻塞机制依赖于内核的等待队列:
- 当读进程打开管道但无数据时,读进程会被加入到管道的读等待队列,进程状态变为
TASK_INTERRUPTIBLE,让出 CPU。- 当写进程写入数据后,内核会唤醒读等待队列中的进程,使其恢复运行并读取数据。
- 非阻塞模式下,进程不会被加入等待队列,而是立即返回错误或 0,由用户态程序自行处理重试逻辑。
5.2 SIGPIPE 信号处理
当写进程向已关闭读端的管道写入数据时,内核会向写进程发送
SIGPIPE信号,默认行为是终止进程。为了避免进程意外退出,可以通过signal()或sigaction()函数捕获该信号:#include <signal.h> void sigpipe_handler(int signum) { printf("Received SIGPIPE signal, read end closed\n"); } int main() { // 注册SIGPIPE信号处理函数 signal(SIGPIPE, sigpipe_handler); // ... 后续代码 ... }
6.1 无亲缘关系进程通信
这是命名管道最核心的应用场景,例如:
- 后台服务进程与前台命令行工具的通信。
- 不同用户的进程之间的数据传输(需保证管道文件的权限正确)。
6.2 日志收集与处理
通过命名管道实现日志的解耦:
- 业务进程将日志写入命名管道。
- 专门的日志处理进程(如日志过滤、归档、上传)从管道读取日志并处理。
- 优势:业务进程无需关心日志的存储和上传,提高性能和可维护性。
6.3 命令行工具协作
通过命名管道连接多个命令行工具,实现数据流式处理:
# 将ls的输出通过命名管道传递给grep过滤 mkfifo pipe ls -l > pipe & grep ".c" < pipe rm pipe6.4 服务端 - 客户端模型
服务端创建命名管道,客户端通过该管道向服务端发送请求,服务端处理后返回结果(需另一个管道用于响应):
- 优点:实现简单,无需复杂的网络编程。
- 缺点:仅适用于本地进程通信,不支持跨机器。
IPC 机制 优点 缺点 适用场景 命名管道 实现简单,文件系统可见,跨进程 半双工,无数据边界,速度一般 本地任意进程间的简单数据传输 匿名管道 轻量,性能高 仅适用于亲缘进程,无文件实体 父子进程间的临时通信 消息队列 有数据边界,支持多消息类型 数据量有限,内核资源消耗较大 结构化数据的进程间通信 共享内存 速度最快(无数据拷贝) 需同步机制(信号量),实现复杂 大数据量、高性能需求的通信 套接字(UDS) 支持全双工,可跨网络 实现复杂,性能开销较大 本地 / 跨网络的进程通信
1899

被折叠的 条评论
为什么被折叠?



