管道知识总结
管道是 Unix/Linux 中最古老的进程间通信(IPC)方式之一,本质是内核中的一块缓冲区,通过文件描述符暴露给进程,使用方式与文件一致,体现了“Linux 一切皆文件”的思想。管道分为匿名管道和命名管道。
1. 管道的基本概念
- 定义:从一个进程连接到另一个进程的数据流。
- 典型命令:
who | wc -l,who进程的标准输出写入管道,wc进程从管道读取。 - 分类:
- 匿名管道:
pipe - 命名管道:FIFO,
mkfifo
- 匿名管道:
2. 匿名管道
2.1 创建与使用
#include <unistd.h>
int pipe(int fd[2]);
fd[0]:读端fd[1]:写端- 成功返回 0,失败返回错误码。
2.2 原理
- 父进程调用
pipe创建管道,得到两个文件描述符。 fork后子进程继承这两个描述符,父子进程可通过管道通信。- 通常父进程关闭读端、子进程关闭写端,形成单向数据流。
- 站在文件描述符角度:管道对应两个
struct file,一个读、一个写。 - 站在内核角度:管道有数据页、读操作、写操作,使用方式与文件一致。
2.3 代码示例
从键盘读数据,写入管道,再从管道读出写到屏幕:
int fds[2];
char buf[100];
pipe(fds);
while (fgets(buf, 100, stdin)) {
write(fds[1], buf, strlen(buf));
memset(buf, 0, sizeof(buf));
int len = read(fds[0], buf, 100);
write(1, buf, len);
}
2.4 用 fork 共享管道
- 父进程创建管道
pipe(fds)。 fork出子进程。- 父进程关闭
fd[0],子进程关闭fd[1],即可实现父写子读或父读子写。
2.5 进程池应用
利用匿名管道可以编写进程池:
- 主进程创建多个子进程,每个子进程对应一个管道。
- 子进程通过
dup2(pipefd[0], 0)将管道读端重定向到标准输入,然后循环read(0, &cmd, sizeof(cmd))读取任务。 - 父进程保存每个管道的写端,通过轮询方式派发任务。
- 清理时父进程关闭所有写端,
waitpid回收子进程。 - 相关文件:
Channel.hpp、ProcessPool.hpp、Task.hpp、Main.cc、Makefile。
3. 命名管道(FIFO)
3.1 为什么需要命名管道
匿名管道只能用于具有共同祖先的进程。命名管道可以在不相关的进程之间交换数据。
3.2 创建
命令行:
mkfifo filename
程序内:
int mkfifo(const char *filename, mode_t mode);
3.3 匿名管道与命名管道的区别
| 对比项 | 匿名管道 | 命名管道 |
|---|---|---|
| 创建方式 | pipe 创建并打开 | mkfifo 创建,open 打开 |
| 使用范围 | 亲缘进程 | 任意进程 |
| 语义 | 相同 | 相同 |
3.4 打开规则
- 以读方式打开 FIFO:
O_NONBLOCK禁用:阻塞直到有进程以写方式打开。O_NONBLOCK启用:立刻返回成功。
- 以写方式打开 FIFO:
O_NONBLOCK禁用:阻塞直到有进程以读方式打开。O_NONBLOCK启用:立刻返回失败,错误码ENXIO。
3.5 实例
- 文件拷贝:一个进程读文件写入命名管道,另一个进程读管道写入目标文件。
- server & client 通信:
- server:
mkfifo("mypipe", 0644),open("mypipe", O_RDONLY),循环读并打印。 - client:
open("mypipe", O_WRONLY),读键盘输入写入管道。 - 运行结果:server 等待,client 输入后 server 打印,client 退出后 server 读到 0 退出。
- server:
4. 管道读写规则
| 场景 | O_NONBLOCK 禁用 | O_NONBLOCK 启用 |
|---|---|---|
| 没有数据可读 | read 阻塞,直到有数据 | read 返回 -1,errno = EAGAIN |
| 管道满 | write 阻塞,直到有进程读走数据 | write 返回 -1,errno = EAGAIN |
| 所有写端关闭 | read 返回 0 | 同左 |
| 所有读端关闭 | write 产生 SIGPIPE,可能导致进程退出 | 同左 |
- 写入数据量 ≤
PIPE_BUF:Linux 保证写入原子性。 - 写入数据量 >
PIPE_BUF:不保证原子性。
5. 管道特点
- 只能用于具有共同祖先的进程之间通信。
- 提供流式服务。
- 生命周期随进程,进程退出管道释放。
- 内核会对管道操作进行同步与互斥。
- 管道是半双工的,数据只能单向流动;需要双向通信时,需建立两个管道。
- 验证管道通信的四种情况:
- 读正常 && 写满
- 写正常 && 读空
- 写关闭 && 读正常
- 读关闭 && 写正常
6. 管道与共享内存配合
共享内存是最快的 IPC 形式,但没有同步与互斥,会带来并发问题。可以借助管道实现访问控制:
- 创建 FIFO 作为同步机制。
- server 端等待
Wait(fd),client 写入共享内存后Signal(fd)。 - server 收到信号后读取共享内存。
- 这样让进程执行产生一定顺序性,但注意这只是一个演示,实际需更完善的同步机制。
7. 内核如何管理 IPC 资源
Linux 内核用 struct ipc_ids、struct ipc_id_ary、struct kern_ipc_perm 等结构统一管理 System V IPC 资源:
ipc_ids:管理 IPC 标识符。ipc_id_ary:保存kern_ipc_perm指针数组。kern_ipc_perm:权限、key、uid、gid、mode 等。- 具体资源:
- 共享内存:
struct shmid_kernel - 消息队列:
struct msg_queue - 信号量:
struct sem_array
- 共享内存:
这些 IPC 资源生命周期随内核,必须显式删除,否则不会自动清除。
8. 小结
- 管道是 Linux 中基于文件描述符的进程间通信机制。
- 匿名管道用于亲缘进程,命名管道用于任意进程。
- 管道读写有阻塞/非阻塞规则,写端全关
read返回 0,读端全关write触发SIGPIPE。 - 管道可用于进程池、minishell 管道实现、共享内存访问控制等场景。
- 理解管道要抓住两点:文件描述符视角和内核缓冲区视角。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/Diopeng_out/article/details/166590984




