Linux 进程间通信:无名管道与有名管道详解
1. 引言
在多进程程序设计中,每个进程拥有独立的地址空间,彼此之间无法直接访问对方的数据。然而,在实际开发中,进程之间往往需要协作完成一项任务,例如生产者-消费者模型、客户端-服务端模型等,这就涉及进程间通信问题。
管道是 Unix/Linux 系统中最古老、最经典的进程间通信方式之一。它实现简单、使用广泛,是理解 IPC 机制的绝佳起点。本文将围绕无名管道和有名管道展开,从原理、API 到代码示例,帮助你彻底掌握管道通信。
2. 进程间通信概述
2.1 为什么需要进程间通信
进程是操作系统分配资源的基本单位。为了保证安全性,操作系统为每个进程分配了独立的虚拟地址空间,一个进程无法直接读取或修改另一个进程的内存。当多个进程需要协同工作时,就必须通过操作系统提供的 IPC 机制来交换数据。
2.2 常见的 IPC 方式
| IPC 方式 | 特点 |
|---|---|
| 管道 | 半双工,数据单向流动,简单高效 |
| 命名管道 | 管道的有名版本,支持无亲缘关系进程通信 |
| 消息队列 | 以消息为单位传输,支持多对多 |
| 共享内存 | 速度最快,需配合信号量同步 |
| 信号 | 异步通知机制 |
| 套接字 | 支持跨主机通信 |
2.2 常见的 IPC 方式
从实现机制上看,Linux 下的 IPC 可以分为三大类:
| 类别 | 主要方式 | 说明 |
|---|---|---|
| 古老的通信方式 | 无名管道、有名管道、信号 | 早期 Unix 就提供的经典机制,简单直接 |
| IPC 对象通信 | System V 消息队列、共享内存、信号量集 | 以内核对象形式存在,功能更强;BSD 也提供了对应的 IPC 实现 |
| Socket 通信 | UNIX 域套接字、网络套接字 | 既可单机通信,也可跨主机网络通信 |
其中,管道是最基础、最常见的 IPC 方式:
| IPC 方式 | 特点 |
|---|---|
| 管道 | 半双工,数据单向流动,简单高效 |
| 命名管道 | 管道的有名版本,支持无亲缘关系进程通信 |
| 消息队列 | 以消息为单位传输,支持多对多(实际项目中使用相对较少) |
| 共享内存 | 速度最快,需配合信号量同步 |
| 信号 | 异步通知机制 |
| 套接字 | 支持跨主机通信 |
本文聚焦于最基础的两种:无名管道和有名管道。
3. 无名管道
3.1 什么是无名管道
无名管道,也称匿名管道,是 Unix/Linux 中最简单的 IPC 机制。它的本质是内核维护的一块环形缓冲区,通过两个文件描述符来访问:
fd[0]:读端,用于从管道读取数据fd[1]:写端,用于向管道写入数据
数据在管道中按先进先出的顺序流动,写入的数据只能被读取一次。
3.2 无名管道的特性
无名管道具有以下几个重要特性:
无名管道具有以下几个重要特性:
- 半双工:数据只能单向流动,需要两个管道才能实现双向通信。实际编程中通常把一个管道当作单工来使用。
- 无名字:管道没有文件名,不能通过
open()打开,只能由创建它的进程及子进程使用。 - 亲缘限制:由于管道没有名字,其他进程无法获知它的存在,因此只能用于父子进程或兄弟进程之间的通信。
- 字节流:管道传输的是无格式的字节流,不保留消息边界。
- 生命周期:管道随进程结束而销毁。
- 缓冲区有限:管道缓冲区大小有限,通常为 64KB,写满后继续写入会阻塞。
- 特殊文件:所有管道本质上都是特殊文件,不支持定位操作,因此不能使用
lseek()或fseek()。 - 文件 IO 访问:管道是特殊文件,读写时应优先使用文件 IO:
open、read、write、close;也可以选择带缓冲区的标准 IO:fgets、fread、fgetc等。
无名管道的底层实现是内核维护的队列(环形缓冲区),数据按先进先出顺序流动。### 3.3 创建无名管道
使用 pipe() 系统调用创建无名管道:
#include <unistd.h>
int pipe(int pipefd[2]);
- 成功返回
0,失败返回-1并设置errno pipefd[0]为读端文件描述符pipefd[1]为写端文件描述符
3.4 父子进程通信示例
下面是一个经典的父子进程通信示例:子进程向管道写入消息,父进程从管道读取。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define BUF_SIZE 128
int main() {
int fd[2];
pid_t pid;
char buf[BUF_SIZE];
if (pipe(fd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid = fork();
if (pid < 0) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
/* 子进程:关闭读端,向管道写入数据 */
close(fd[0]);
const char *msg = "Hello from child process!";
write(fd[1], msg, strlen(msg) + 1);
close(fd[1]);
_exit(EXIT_SUCCESS);
} else {
/* 父进程:关闭写端,从管道读取数据 */
close(fd[1]);
ssize_t n = read(fd[0], buf, sizeof(buf));
if (n > 0) {
printf("Parent received: %s\n", buf);
}
close(fd[0]);
wait(NULL);
}
return 0;
}
3.5 双向管道通信示例
单向管道只能实现一个方向的数据流动。要实现父子进程之间的双向收发,需要创建两个管道:一个用于父进程向子进程发送数据,另一个用于子进程向父进程发送数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define BUF_SIZE 128
int main() {
int parent_to_child[2];
int child_to_parent[2];
pid_t pid;
char buf[BUF_SIZE];
if (pipe(parent_to_child) == -1 || pipe(child_to_parent) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid = fork();
if (pid < 0) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
/* 子进程:父->子管道保留读端,子->父管道保留写端 */
close(parent_to_child[1]);
close(child_to_parent[0]);
read(parent_to_child[0], buf, sizeof(buf));
printf("Child received: %s\n", buf);
const char *reply = "Hello from child!";
write(child_to_parent[1], reply, strlen(reply) + 1);
printf("Child sent: %s\n", reply);
close(parent_to_child[0]);
close(child_to_parent[1]);
_exit(EXIT_SUCCESS);
} else {
/* 父进程:父->子管道保留写端,子->父管道保留读端 */
close(parent_to_child[0]);
close(child_to_parent[1]);
const char *msg = "Hello from parent!";
write(parent_to_child[1], msg, strlen(msg) + 1);
printf("Parent sent: %s\n", msg);
read(child_to_parent[0], buf, sizeof(buf));
printf("Parent received: %s\n", buf);
close(parent_to_child[1]);
close(child_to_parent[0]);
wait(NULL);
}
return 0;
}
这里一共需要关闭 4 个文件描述符:
- 子进程关闭:
parent_to_child[1](写端)、child_to_parent[0](读端) - 父进程关闭:
parent_to_child[0](读端)、child_to_parent[1](写端)
为什么要关闭这 4 个文件描述符?
- 每个管道都是半双工的,进程只应保留自己真正需要的那一端。
- 关闭本进程不需要的读端,是为了保证当管道写端全部关闭后,对端的
read()能返回0。例如父进程若不关闭parent_to_child[0],即使子进程读完了数据,父进程自己也还持有该管道读端,子进程的read()就永远等不到 EOF,从而阻塞。 - 关闭本进程不需要的写端,既能避免误写数据,也能让对端通过
read()返回0正确判断数据发送完毕,避免双向通信中的死锁。
预期运行输出:
Parent sent: Hello from parent!
Child received: Hello from parent!
Child sent: Hello from child!
Parent received: Hello from child!
3.6 关键注意事项
无名管道的通用编程顺序为:
- 使用
pipe()创建管道 - 调用
fork()创建子进程 - 父子进程按需关闭不使用的端口,读写管道
- 通信结束后关闭管道
- 关闭未使用的端口:子进程写数据前必须关闭读端,父进程读数据前必须关闭写端,否则可能出现阻塞或数据错乱。
- 读阻塞:当管道为空且写端未关闭时,
read()会阻塞等待数据。 - 写阻塞:当读端存在,但写入数据超过 64KB 缓冲区上限时,继续写入会阻塞,直到对端读出数据腾出空间。
- 读到 EOF:当所有写端关闭且管道中没有内容时,
read()返回0,表示管道已无数据可读,进程间通信结束。 - SIGPIPE 信号:当读端全部关闭后,继续向管道写入数据会导致管道破裂,触发
SIGPIPE信号,默认行为是终止写进程。 - 关闭未使用的端口:子进程写数据前必须关闭读端,父进程读数据前必须关闭写端,否则可能出现阻塞或数据错乱。
- 读阻塞:当管道为空且写端未关闭时,
read()会阻塞等待数据。 - 读到 EOF:当所有写端关闭后,
read()返回0,表示管道已无数据可读。 - SIGPIPE 信号:当读端全部关闭后,向管道写入数据会触发
SIGPIPE信号,默认行为是终止进程。
4. 有名管道
4.1 为什么需要有名管道
无名管道的最大局限在于:它只能在有亲缘关系的进程之间使用。当两个互不相干的进程需要通信时,无名管道就无能为力了。有名管道正是为了解决这一问题而引入的。
4.2 什么是有名管道
有名管道,也称 FIFO,是一种存在于文件系统中的特殊文件。它拥有一个路径名,任何知道该路径名的进程都可以打开它进行通信,不再受亲缘关系限制。
有名管道与无名管道的核心区别:
| 对比项 | 无名管道 | 有名管道 |
|---|---|---|
| 文件系统可见性 | 不可见 | 以特殊文件形式存在 |
| 通信范围 | 父子/兄弟进程 | 任意进程 |
| 创建方式 | pipe() | mkfifo() |
| 打开方式 | 继承文件描述符 | open() |
| 持久性 | 随进程销毁 | 手动删除才销毁 |
4.3 创建有名管道
使用 mkfifo() 函数创建有名管道:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname:管道文件的路径名mode:权限,如0666(受 umask 影响)- 成功返回
0,失败返回-1
也可以通过 shell 命令创建:
mkfifo mypipe
4.4 写端示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#define FIFO_PATH "/tmp/myfifo"
int main() {
/* 若管道不存在则创建 */
if (access(FIFO_PATH, F_OK) == -1) {
if (mkfifo(FIFO_PATH, 0666) == -1) {
perror("mkfifo");
exit(EXIT_FAILURE);
}
}
int fd = open(FIFO_PATH, O_WRONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
const char *msg = "Hello from writer process!";
write(fd, msg, strlen(msg) + 1);
printf("Writer sent data.\n");
close(fd);
return 0;
}
4.5 读端示例
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#define FIFO_PATH "/tmp/myfifo"
#define BUF_SIZE 128
int main() {
/* 若管道不存在则创建 */
if (access(FIFO_PATH, F_OK) == -1) {
if (mkfifo(FIFO_PATH, 0666) == -1) {
perror("mkfifo");
exit(EXIT_FAILURE);
}
}
int fd = open(FIFO_PATH, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
char buf[BUF_SIZE];
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("Reader received: %s\n", buf);
}
close(fd);
return 0;
}
4.6 有名管道的阻塞行为
open() 打开有名管道时有以下阻塞规则:
- 只读打开:阻塞直到有进程以写方式打开同一个管道。
- 只写打开:阻塞直到有进程以读方式打开同一个管道。
- 使用
O_NONBLOCK标志可以改变阻塞行为,只读打开立即返回成功;若没有读端,只写打开会失败并返回ENXIO。
5. 典型应用场景
5.1 无名管道典型场景
- 父子进程之间的简单数据传递
- Shell 管道符
|的底层实现,如ls | grep txt - 进程中简单的任务分工与结果回传
5.2 有名管道典型场景
- 客户端-服务端架构中,服务端与多个客户端之间的请求分发
- 日志收集系统,多个进程向同一管道写入日志
- 跨进程的命令控制,如向后台守护进程发送指令
- 不同语言编写的进程之间交换数据
6. 管道通信的局限与改进
管道虽然简单易用,但也存在一些固有局限:
- 数据无格式:管道传输的是字节流,收发双方需要自行约定数据结构。
- 缓冲区大小固定:大量数据写入会阻塞。
- 数据一次性读取:已被读取的数据无法再次读取。
- 无消息边界:连续写入的数据可能被一次读出,需要应用层处理。
针对这些局限,可以根据实际需求选择更合适的 IPC 方式:
- 需要结构化消息和消息边界 → 消息队列
- 需要高速大数据量传输 → 共享内存
- 需要双向持久连接 → Unix 域套接字
7. 总结
本文系统地介绍了进程间通信中最基础的两种管道机制:
- 无名管道通过
pipe()创建,不占用文件系统,简单高效,适用于父子进程之间的单向通信,是理解 IPC 的入门首选。 - 有名管道通过
mkfifo()创建,以特殊文件形式存在,打破了亲缘进程的限制,允许任意进程通过路径名建立通信。
掌握管道的工作原理和使用细节,是深入学习 Linux 多进程编程的重要基石。建议动手运行文中的示例代码,并通过修改阻塞参数、增加双向管道等方式加深理解。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)