

文章目录
进程间通信(Inter-Process Communication,IPC) 是操作系统中实现多个进程协同工作的重要机制。匿名管道(Anonymous Pipe)作为Unix/Linux系统中最早的IPC形式之一,因其简单高效而被广泛应用。本文将详细讲解匿名管道的原理、操作规则及其实际应用,帮助读者深入理解其在操作系统中的作用。
进程间通信简介
进程间通信的目的
进程间通信的主要目标包括以下几个方面:
- 数据传输:一个进程需要将数据发送给另一个进程,例如将计算结果传递给处理进程。
- 资源共享:多个进程之间共享同一资源,如内存区域或文件。
- 通知事件:一个进程向另一个或一组进程发送消息,通知特定事件的发生,例如子进程终止时通知父进程。
- 进程控制:某些进程(如调试器)需要完全控制另一个进程的执行,拦截其陷阱和异常,并实时监控状态变化。
进程间通信的发展
IPC的发展经历了以下几个阶段:
- 管道(Pipe):最早的IPC形式,包括匿名管道和命名管道,适用于简单通信场景。
- System V IPC:包括消息队列、共享内存和信号量,提供了更复杂的通信和同步机制。
- POSIX IPC:现代标准,支持消息队列、共享内存、信号量、互斥量、条件变量和读写锁,具有更高的灵活性和可移植性。
进程间通信的分类
IPC机制可以分为以下几类:
- 管道 :
- 匿名管道(Pipe)
- 命名管道(Named Pipe)
- System V IPC :
- System V 消息队列
- System V 共享内存
- System V 信号量
- POSIX IPC :
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
本文将重点探讨匿名管道的原理与操作。
什么是管道
管道是Unix/Linux系统中一种经典的进程间通信方式,类似于一个数据通道,将一个进程的输出直接连接到另一个进程的输入。管道的核心思想是**"一切皆文件"**,即管道可以像文件一样被读写。管道分为:
- 匿名管道(Anonymous Pipe) :用于有亲缘关系的进程间通信。
- 命名管道(Named Pipe) :允许无亲缘关系的进程间通信。

匿名管道
创建匿名管道
匿名管道 通过<font style="color:black;">pipe</font>
系统调用创建,其函数原型如下:
c
#include <unistd.h>
int pipe(int fd[2]);
- 参数 :
<font style="color:black;">fd</font>
:文件描述符数组,其中<font style="color:black;">fd[0]</font>
表示读端,<font style="color:black;">fd[1]</font>
表示写端。
- 返回值 :
- 成功返回0,失败返回-1并设置错误码。
创建管道后,进程可以通过<font style="color:black;">fd[1]</font>
写入数据,通过<font style="color:black;">fd[0]</font>
读取数据。

实例代码
以下示例展示了如何使用匿名管道从键盘读取数据,写入管道,再从管道读取数据并输出到屏幕:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void) {
int fds[2];
char buf[100];
int len;
if (pipe(fds) == -1) {
perror("make pipe");
exit(1);
}
while (fgets(buf, 100, stdin)) {
len = strlen(buf);
if (write(fds[1], buf, len) != len) {
perror("write to pipe");
break;
}
memset(buf, 0x00, sizeof(buf));
if ((len = read(fds[0], buf, 100)) == -1) {
perror("read from pipe");
break;
}
if (write(1, buf, len) != len) {
perror("write to stdout");
break;
}
}
return 0;
}
代码说明:
<font style="color:black;">pipe(fds)</font>
创建匿名管道。<font style="color:black;">fgets</font>
从标准输入读取数据。<font style="color:black;">write(fds[1], buf, len)</font>
将数据写入管道。<font style="color:black;">read(fds[0], buf, 100)</font>
从管道读取数据。<font style="color:black;">write(1, buf, len)</font>
将数据输出到标准输出(屏幕)。
使用<font style="color:black;">fork</font>
共享管道原理
匿名管道通常用于父子进程间的通信。父进程创建管道后,通过fork创建子进程,子进程继承父进程的文件描述符,从而共享同一管道。
工作原理:
- 父进程调用
<font style="color:black;">pipe</font>
创建管道,得到<font style="color:black;">fd[0]</font>
(读端)和<font style="color:black;">fd[1]</font>
(写端)。 - 调用fork创建子进程,子进程复制父进程的文件描述符,拥有相同的
<font style="color:black;">fd[0]</font>
和<font style="color:black;">fd[1]</font>
。 - 父子进程通过关闭不需要的端实现单向通信。例如:
- 父进程关闭
<font style="color:black;">fd[0]</font>
,子进程关闭<font style="color:black;">fd[1]</font>
,实现子进程写、父进程读。 - 反之亦然。
- 父进程关闭

从文件描述符角度理解管道
管道在操作系统中通过文件描述符操作,其本质是一个内核管理的环形缓冲区:
- 写端(fd[1])将数据写入缓冲区。
- 读端(fd[0])从缓冲区读取数据。
- 数据按先进先出(FIFO)顺序传输。

从文件描述符的角度,管道可以看作一种特殊的双端文件,其读写操作依赖于文件描述符的分配和管理。以下是关键点:
(1) 文件描述符的分配
- 当调用
<font style="color:black;">pipe(fd)</font>
时,内核为当前进程分配两个新的文件描述符:<font style="color:black;">fd[0]</font>
(读端)和<font style="color:black;"> </font><font style="color:black;">fd[1]</font>
(写端)。 - 这些描述符的值(例如 3 和 4)是从进程当前可用的最低文件描述符编号中分配的,通常从 0 开始,0、1、2 分别被标准输入(stdin)、标准输出(stdout)和标准错误(stderr)占用。
- 图示中
<font style="color:black;"> </font><font style="color:black;">fd[0]=3</font>
和<font style="color:black;">fd[1]=4</font>
表明管道的读写端被分配到这些位置。
(2) 文件描述符的继承
- 通过
<font style="color:black;">fork()</font>
,子进程复制了父进程的文件描述符表,继承了<font style="color:black;">fd[0]</font>
和<font style="color:black;">fd[1]</font>
。 - 这使得父子进程可以共享同一管道,但需要通过关闭不必要的描述符来定义通信方向(例如父进程关闭写端,子进程关闭读端)。
(3) 文件描述符的关闭
- 关闭文件描述符(
<font style="color:black;">close(fd[0]</font>
) 或<font style="color:black;">close(fd[1]</font>
))会减少对管道端的使用计数。 - 当所有读端描述符关闭时,写端尝试写入会触发
<font style="color:black;">SIGPIPE</font>
信号。 - 当所有写端描述符关闭时,读端读取会返回 0(EOF),表示管道已无数据。
(4) 管道作为文件
- 管道的读写操作与普通文件类似,使用
<font style="color:black;">read(fd[0], ...)</font><font style="color:black;"></font>
和<font style="color:black;">write(fd[1], ...)</font>
。 - 内核维护一个环形缓冲区作为管道的"文件内容",文件描述符只是进程访问该缓冲区的接口。
- 这体现了Linux 的"一切皆文件"哲学,管道的本质是一个内核缓冲区,文件描述符提供了用户态到内核态的桥梁。
从内核角度看管道本质
管道在内核中表现为一个环形缓冲区,进程以"文件"方式访问该缓冲区。管道的生命周期与进程绑定,进程退出时管道自动释放。

(1) 管道的本质
- 管道本质上是一个内核维护的环形缓冲区,由
<font style="color:black;">inode</font>
结构表示。 - 每个进程通过文件描述符访问该缓冲区,
<font style="color:black;">file</font>
结构是进程与内核之间的接口。 - 多个进程共享同一个
<font style="color:black;">inode</font>
,实现了数据在进程间的传递。
(2) 文件描述符与 inode 的关系
- 每个
<font style="color:black;">file</font>
结构通过<font style="color:black;">f_inode</font>
指向同一个管道<font style="color:black;">inode</font>
,这确保了所有相关文件描述符访问的是同一块共享内存。 <font style="color:black;">f_count</font>
字段跟踪引用计数,当所有关联的<font style="color:black;">file</font>
结构被关闭(<font style="color:black;">f_count</font>
降为 0)时,内核释放<font style="color:black;">inode</font>
及其缓冲区。
(3) 操作机制
- 写操作:进程 1 调用
<font style="color:black;">write</font>
,通过<font style="color:black;">file</font>
结构中的<font style="color:black;">f_op</font>
指向的写函数,将数据写入<font style="color:black;"></font><font style="color:black;">inode</font>
的缓冲区。 - 读操作:进程 2 调用
<font style="color:black;">read</font>
,通过<font style="color:black;">file</font>
结构中的<font style="color:black;">f_op</font>
指向的读函数,从<font style="color:black;">inode</font>
缓冲区读取数据。 - 内核通过
<font style="color:black;"></font><font style="color:black;">inode</font>
管理缓冲区的读写指针,确保数据按 FIFO 顺序传输。
(4) 同步与互斥
- 内核通过锁机制(例如信号量)保护
<font style="color:black;">inode</font>
缓冲区的访问,避免竞争条件。 - 当管道满时,写操作阻塞;当管道空时,读操作阻塞(除非设置了
<font style="color:black;">O_NONBLOCK</font>
)。
管道样例
c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int pipefd[2];
if (pipe(pipefd) == -1)
ERR_EXIT("pipe error");
pid_t pid;
pid = fork();
管道的读写规则
匿名管道的读写行为在不同情况下有所不同,具体规则如下:
- 当没有数据可读时 :
- 阻塞模式(默认) :无数据读取,
<font style="color:black;">read</font>
调用阻塞,进程暂停执行,直到有数据可读。 - 非阻塞模式(O_NONBLOCK) :
<font style="color:black;">read</font>
立即返回-1,<font style="color:black;">errno</font>
设为EAGAIN。
- 阻塞模式(默认) :无数据读取,
- 当管道满时 :
- 阻塞模式 :
<font style="color:black;">write</font>
调用阻塞,直到有进程读取数据,释放缓冲区空间。 - 非阻塞模式 :
<font style="color:black;">write</font>
立即返回-1,<font style="color:black;">errno</font>
设为EAGAIN。
- 阻塞模式 :
- 如果所有写端关闭 :
<font style="color:black;">read</font>
读取完缓冲区数据后返回0,表示文件结束(EOF)。
- 如果所有读端关闭 :
<font style="color:black;">write</font>
操作会触发<font style="color:black;">SIGPIPE</font>
信号,可能导致写进程退出。
- 原子性 :
- 当**写入数据量 ≤ PIPE_BUF(通常为4KB)**时,Linux保证写入的原子性。
- 当写入数据量 > ****PIPE_BUF时,不保证原子性,可能被其他进程中断。
管道的特点
匿名管道具有以下特点:
- 亲缘关系:只能用于具有共同祖先的进程(通常是父子进程)间通信。
- 流式服务:数据以字节流形式传输,无消息边界。
- 生命周期:随进程,进程退出时管道释放。
- 同步与互斥:内核自动管理读写操作的同步和互斥。
- 半双工:数据单向流动,双向通信需建立两个管道。

管道样例
6.1 测试管道读写
以下示例展示了父子进程通过匿名管道通信:
c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while(0)
int main(int argc, char *argv[]) {
int pipefd[2];
if (pipe(pipefd) == -1) ERR_EXIT("pipe error");
pid_t pid = fork();
if (pid == -1) ERR_EXIT("fork error");
if (pid == 0) { // 子进程
close(pipefd[0]); // 关闭读端
write(pipefd[1], "hello", 5);
close(pipefd[1]);
exit(EXIT_SUCCESS);
}
// 父进程
close(pipefd[1]); // 关闭写端
char buf[10] = {0};
read(pipefd[0], buf, 10);
printf("buf=%s\n", buf);
return 0;
}
代码说明:
- 父进程创建管道并fork子进程。
- 子进程关闭读端,写入"hello",然后关闭写端。
- 父进程关闭写端,读取数据并打印。
输出:
c
buf=hello
验证管道通信的四种情况
以下是对管道读写行为的验证:
- 读正常 && 写满 :
- 读端有数据可读,read正常返回。
- 管道缓冲区满时,write阻塞(默认)或返回EAGAIN(非阻塞)。
- 写正常 && 读空 :
- 管道有空间,write正常写入。
- 管道无数据,read阻塞(默认)或返回EAGAIN(非阻塞)。
- 写关闭 && 读正常 :
- 所有写端关闭,read读取完数据后返回0。
- 读端继续读取直到EOF。
- 读关闭 && 写正常 :
- 所有读端关闭,write触发SIGPIPE信号,默认终止写进程。
结论
进程间通信的本质:先让不同的进程可以看到同一份资源(内存),然后再通信。
匿名管道作为一种简单高效的IPC机制,广泛应用于有亲缘关系的进程间通信。其基于文件描述符的操作方式和内核缓冲区的实现,体现了Linux"一切皆文件"的设计哲学。尽管存在只能用于亲缘进程、半双工通信等局限性,但在许多简单场景下,匿名管道仍是理想选择。
通过本文的讲解,读者可以全面理解匿名管道的创建、使用、读写规则及其特点,并通过代码示例掌握其实际操作方法。这为进一步学习更复杂的IPC机制奠定了坚实基础。