Linux 进程间通信--匿名管道
管道通信定义
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个管道
在Linux中,父进程通过 fork() 创建子进程时,子进程会继承父进程的文件描述符表(files_struct),这是一种浅拷贝——父子进程的文件描述符(如 fd[0])指向同一个内核文件对象(struct file),包括共享文件偏移量、访问模式等状态。因此,父子进程可以通过相同的文件描述符并发读写同一个文件。
若需要进程间共享数据,除了普通磁盘文件,还可以使用基于内存的通信媒介,主要包括以下两类机制:
内存文件系统(如
tmpfs/ramfs)- 数据完全驻留内存,拥有完整的文件系统抽象(
inode、file_operations),但永不触盘。 - 适合作为临时文件的存储媒介,但并非最优的进程通信(IPC)方式,因文件操作需经过VFS层,存在额外开销。
- 数据完全驻留内存,拥有完整的文件系统抽象(
专用IPC机制
- 匿名管道:通过
pipe()创建,仅在内核缓冲区中流转数据,无文件系统关联(无inode),适用于父子进程通信。 - 共享内存:直接映射同一物理内存区域,完全绕过文件系统,性能最高。
- 其他:消息队列、信号量等。
- 匿名管道:通过
那么,管道通信为什么需要通过内存级文件实现资源共享呢?
- 性能:内存的访问速度远高于磁盘。使用内核缓冲区(如匿名管道或命名管道)可以避免磁盘I/O,极大地提升数据传输的速度,减少延迟。
- 临时性:管道通常用于进程间的短暂通信,数据是临时的,不需要持久化存储。使用内存级文件可以在进程完成通信后自动释放资源,而使用磁盘文件则需要显式清理。
- 同步和并发性(最重要):管道是一种同步通信机制,进程可以通过管道实现数据流的同步传递。使用内存级文件可以更好地管理并发和同步,而磁盘文件的使用可能导致额外的锁机制,增加复杂性和延迟。
- 资源效率:管道的设计是为了实现轻量级的进程间通信,仅需维护一个固定大小的内核缓冲区,而磁盘文件通常需要更多的系统资源(如磁盘空间和I/O操作)
父进程在创建管道时,为了避免读写冲突,会创建**两个文件描述符(fd)**指向同一个管道对象(内核缓冲区),分别用于读和写。这两个fd会对应两个独立的struct file对象(一个设置读模式,一个设置写模式)。若不分离读写,共用一个struct file会导致文件偏移量(f_pos)竞争,引发读写混乱。
当父进程fork()出子进程时,子进程会继承这两个fd及对应的struct file对象(引用计数+1),因此父子进程最初均持有读写两端。为实现单向通信:
- 若子进程负责写,则关闭读端的fd(减少读端
struct file的引用计数); - 若父进程负责读,则关闭写端的fd(减少写端
struct file的引用计数)。
最终,读端和写端的struct file引用计数均降为1,确保读写完全隔离,互不干扰。


因其单向数据流特性,这一机制(两个struct file对象+内核缓冲区)被命名为管道。由于它仅用于进程间通信,无需磁盘存储和文件路径,故称为匿名管道(无inode,不关联文件系统)。
管道通信不仅仅只局限于父子进程,只要是有血缘关系的进程都可以进行管道通信,例如,父进程创建两个子进程,随后父进程将读写端都关闭了,这时两个兄弟进程的读写段都和父进程一样,故他们直接也可以构成管道通信,父进程创建的子进程,子进程再创建一个子进程,那么由于父进程和它的孙子进程共享同一个file_struct,故也可以构成管道通信。
管道通信使用
pipe 是 Linux/Unix 系统中用于创建匿名管道(Anonymous Pipe)的系统调用,主要用于父子进程或兄弟进程之间的单向通信。它创建一个内核缓冲区,并返回两个文件描述符(fd),一个用于读,一个用于写。
函数原型
#include <unistd.h>
int pipe(int pipefd[2]);
- 参数:
pipefd[2]:一个长度为 2 的整型数组,用于存储返回的文件描述符。pipefd[0]:读端(从管道读取数据)。pipefd[1]:写端(向管道写入数据)。
- 返回值:
- 成功时返回
0,失败返回-1并设置errno。
- 成功时返回
基本使用流程
(1)创建管道
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
- 此时:
pipefd[0]可用于read()(读数据)。pipefd[1]可用于write()(写数据)。
(2)fork() 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
- 父进程和子进程都会继承
pipefd,因此两者都可以读写管道。
(3)关闭不需要的文件描述符
为了实现单向通信,通常:
- 父进程关闭写端,只读数据:
close(pipefd[1]); // 父进程关闭写端
- 子进程关闭读端,只写数据:
close(pipefd[0]); // 子进程关闭读端
这样:
- 父进程只能
read()(等待子进程写入)。 - 子进程只能
write()(发送数据给父进程)。
关键特性
| 特性 | 说明 |
|---|---|
| 单向通信 | 通常一个进程读,另一个进程写。 |
| 阻塞机制 | - 读空管道会阻塞,直到有数据写入。 - 写满管道会阻塞,直到有空间。 |
| 内核缓冲区 | 默认大小通常为 64KB(可通过 fcntl 修改)。 |
| 文件描述符继承 | fork() 后子进程继承 pipefd,需手动关闭不需要的端。 |
| 匿名管道 | 无文件名,仅用于相关进程(父子/兄弟)通信。 |
使用用例
下面是一个综合测试用例,我们让父进程持续读取子进程的数据,而子进程每隔一秒写入一段数据到管道中
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/wait.h>
#define SIZE 1024
enum global_ret{
pipe_error = 1,
write_error,
read_error,
};
using namespace std;
void Write(int fd)
{
int cnt = 5;
char inbuffer[SIZE];
while(cnt--)
{
int pid = getpid();
// 告知此处将 inbuffer 作为字符串缓冲区使用
inbuffer[0] = 0;
snprintf(inbuffer, SIZE - 1, "%s-%d: %s %d", "pid", pid, "hello, linux", cnt);
int n = write(fd, inbuffer, strlen(inbuffer));
if(n == -1)
{
perror("write error");
exit(global_ret::write_error);
}
sleep(1);
}
exit(0);
}
void Read(int fd)
{
char outbuffer[SIZE];
while(true)
{
int n = read(fd, outbuffer, sizeof(outbuffer));
if(n == 0)
{
// 读取到了文件末尾,不再读取
return;
}else if(n == -1)
{
perror("read error");
exit(global_ret::read_error);
}
outbuffer[n] = 0;
cout << "parent process get message: " << outbuffer << endl;
}
}
int main()
{
int fd[2];
if(pipe(fd) == -1)
{
perror("pipe error");
return global_ret::pipe_error;
}
pid_t id = fork();
if(id == 0)
{
// 子进程
// 关闭读端口
close(fd[0]);
// 执行写操作
Write(fd[1]);
// 退出时关闭写端口(关闭所有端口)
close(fd[1]);
}
// 父进程
// 关闭写端口
close(fd[1]);
// 执行读操作
Read(fd[0]);
// 退出时关闭读端口(关闭所有端口)
close(fd[0]);
int status = 0;
int ret = waitpid(id, &status, 0);
// 正常退出
if (WIFEXITED(status)) {
cout << "Child process " << id << " exited with code: " << WEXITSTATUS(status) << endl;
} else {
// 异常退出
cout << "Child process " << id << " killed by signal: " << WTERMSIG(status) << endl;
}
return 0;
}
运行结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/pipe
╰─➤ ./main.o
parent process get message: pid-3873378: hello, linux 4
parent process get message: pid-3873378: hello, linux 3
parent process get message: pid-3873378: hello, linux 2
parent process get message: pid-3873378: hello, linux 1
parent process get message: pid-3873378: hello, linux 0
Child process 3873378 exited with code: 0
父进程的“每隔一秒读取一次”是 管道阻塞特性 和 进程调度 的自然结果:
(1) 管道的阻塞行为
- 当管道为空时,父进程的
read()会阻塞(暂停执行),直到子进程写入数据。 - 当管道有数据时,父进程的
read()会立即返回数据。 - 当所有写端关闭时,
read()返回0(EOF)。
(2) 子进程的写入节奏
- 子进程每次写入后
sleep(1),导致数据以 每秒一条 的速率进入管道。 - 父进程的
read()会被动等待子进程的数据,因此看起来像是“同步”读取。
(3) 没有“乱码”风险
- 管道是内核管理的缓冲区,读写操作是原子的,不会出现“读一半”或“写一半”的乱码问题。
- 如果父进程尝试读取时子进程正在写入,内核会保证
read()要么读到完整数据,要么阻塞等待。
- 如果父进程尝试读取时子进程正在写入,内核会保证
异常情况
(1)子进程疯狂写入,父进程延迟读取
- 现象:
子进程持续写入数据,直到管道缓冲区满(默认大小通常为 64KB),此时子进程的write()会阻塞,直到父进程读取数据腾出空间。 - 原因:
管道是固定大小的内核缓冲区,当缓冲区满时,write()会阻塞(除非设置为非阻塞模式)。
阻塞是内核的流量控制机制,防止生产者(子进程)过快导致数据丢失。
(2)父进程读取空管道
- 现象:
父进程调用read()时,若管道无数据且写端未关闭,父进程会阻塞,直到子进程写入数据。 - 原因:
管道设计为同步通信,读操作默认阻塞等待数据。
若需要非阻塞读取,可通过 fcntl(fd, F_SETFL, O_NONBLOCK) 设置,此时 read() 会返回 -1 并设置 errno=EAGAIN。
(3)子进程退出,父进程读取
- 现象:
子进程退出后关闭写端,父进程的read()返回 0(EOF),表示无更多数据,不会阻塞。 - 原因:
当管道所有写端关闭时,read()会立即返回 0,通知读端通信结束。
返回 0 是合法的“文件结束”标志,不是错误。
(4)父进程退出,子进程写入
- 现象:
父进程关闭读端后退出,子进程继续写入会触发SIGPIPE信号(默认终止进程),错误码为EPIPE。 - 原因:
当管道读端关闭时,写操作无意义,内核通过SIGPIPE终止子进程。
若我们忽略 SIGPIPE(signal(SIGPIPE, SIG_IGN)),此时 write() 返回 -1,errno=EPIPE。
总结对比
| 场景 | 子进程行为 | 父进程行为 | 结果 |
|---|---|---|---|
| 写端疯狂写入,缓冲区满 | write() 阻塞 |
正常读取 | 子进程暂停,直到父进程读取 |
| 读端读取空管道 | 未写入数据 | read() 阻塞 |
父进程暂停,直到子进程写入 |
| 写端关闭,读端尝试读取 | 已退出 | read() 返回 0 |
父进程检测到 EOF |
| 读端关闭,写端尝试写入 | write() 触发 SIGPIPE |
已退出 | 子进程被终止(信号 13) |
- 管道缓冲区大小:
可通过fcntl(fd, F_SETPIPE_SZ, size)修改(需权限)。 - 非阻塞模式:
设置O_NONBLOCK后,read()和write()在无法立即完成时会返回-1(errno=EAGAIN)。 - 原子性保证:
管道保证 ≤PIPE_BUF(通常 4KB)的写入是原子的,避免多进程写入交织。
下面我们来验证一下父进程退出,子进程写入的现象:
父进程读取2秒后就退出,而子进程会持续写入5秒
void Read(int fd)
{
char outbuffer[SIZE];
int cnt = 2;
while(cnt--)
{
int n = read(fd, outbuffer, sizeof(outbuffer));
if(n == 0)
{
// 读取到了文件末尾,不再读取
return;
}else if(n == -1)
{
perror("read error");
exit(global_ret::read_error);
}
outbuffer[n] = 0;
cout << "parent process get message: " << outbuffer << endl;
}
}
运行结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/pipe
╰─➤ ./main.o
parent process get message: pid-3876701: hello, linux 4
parent process get message: pid-3876701: hello, linux 3
Child process 3876701 killed by signal: 13
可以看到子进程被 13 号信号杀掉了