进程间通信(Inter-Process Communication, IPC)是指不同进程之间进行数据交换或信息共享的机制。由于每个进程拥有独立的地址空间和资源(如内存、文件描述符),操作系统通过内核提供的特殊方法实现进程间的数据传递。
IPC的主要目的是解决进程隔离带来的协作障碍,具体需求包括:
IPC的实现方式主要分为四类(具体技术可能重叠):
(1)管道(Pipe)
pipe()系统调用创建内核缓冲区。 (2)System V IPC
(3)POSIX IPC
(4)其他方式
SIGINT终止进程),开销最小但不适合大数据传输。关键原理:所有IPC均依赖内核管理的共享资源(如缓冲区、内存区域),进程通过系统调用访问这些资源。
|符号实现命令间数据流传递。驱动因素:多任务需求、分布式计算兴起及操作系统架构演进。
总结
进程间通信是打破进程隔离、实现协作的关键机制,其发展从基础管道扩展至多样化技术,核心始终围绕内核中介的共享资源访问。不同场景需权衡性能、复杂度与通信需求选择合适方式(如管道适合简单血缘进程通信,共享内存适合高性能数据共享)。
进程间通信是通过操作系统提供的特殊方法实现进程间的数据传递。
可是,在之前的学习中我们知道进程PCB中的有一个 *file的结构体指针指向file_struct结构体,file_struct结构体中存储的核心成员为 fd_array[](文件指针数组),数组元素指向file结构体,每个file结构体中都包含缓冲区,详细请看专栏【Linux系统】中的【基础IO下】
如下图:

既然这样,那为什么不能用file结构体中的内核缓冲区,通过系统调用read/write以不同的读写方式打开来进行进程间通信呢?
一、设计目标冲突:文件缓冲区与通信缓冲区的本质差异
struct file 中的缓冲区)必须将数据刷新到磁盘以实现持久化,而进程间通信(IPC)要求数据 " 仅存在于内存 ",且通信完成后立即失效。若强制普通文件缓冲区用于IPC,会导致 二、性能损耗:内核缓冲区的双重拷贝问题
write() 仅将数据从用户空间拷贝到内核缓冲区,不直接落盘read() 时若数据在内核缓冲区,则直接读取;否则触发磁盘I/O三、并发安全:缺乏进程间同步机制
四、生命周期管理:通信与文件解耦
SIGPIPE 信号终止写进程五、操作系统的安全隔离原则
普通文件缓冲区允许任意进程通过路径访问,违背了IPC的最小权限原则:
结论:管道是专为IPC优化的内存对象
普通文件的 struct file 内核缓冲区与进程通信需求存在根本性冲突,主要体现在数据持久化、打开方式、性能、并发安全及生命周期管理上。管道通过以下设计实现高效安全的IPC:
管道是 Unix/Linux 系统中历史最悠久的进程间通信(IPC)机制,其核心设计为:
示例;
ltx@iv-ye1i2elts0wh2yp1ahah:~$ who | wc -l
5示例解析
who | wc -l:
who 进程的输出(登录用户列表)通过管道定向至 wc -l 进程的输入。wc -l 统计接收到的行数并输出结果(如 5)。pipe() 创建管道;fork() 两次生成 who 和 wc 进程;dup2() 将 who 的输出重定向至管道写端,wc 的输入重定向至管道读端 。
pipe() 系统调用创建两个文件描述符: fd[0]:读端,从缓冲区取数据fd[1]:写端,向缓冲区写数据 read()/write()),但无实体磁盘文件 。1. 系统调用入口
函数原型:
int pipe(int fd[2]); // 成功返回0,失败返回-1 内核路径:fs/pipe.c → do_pipe2() → __do_pipe_flags() 。
2. 关键步骤(Linux 5.10+)
分配 inode:
get_pipe_inode() 在 pipefs 中分配新 inode,初始化环形缓冲区 。创建文件结构:
为读端(f0)和写端(f1)分配 struct file 对象:
f0 = alloc_file_pseudo(inode, pipe_mnt, "", O_RDONLY, &pipefifo_fops);
f1 = alloc_file_pseudo(inode, pipe_mnt, "", O_WRONLY, &pipefifo_fops); 绑定操作函数集 pipefifo_fops(含 read/write 方法) 。
分配文件描述符:
f0 和 f1 的指针 。fd[2](fd[0]=读端, fd[1]=写端) 。初始化缓冲区:
head/tail),状态为 空 。注:
pipe2()支持附加标志(如O_NONBLOCK),扩展默认行为 。

1. 基本概念
fd[1])流向读端(fd[0]) ,双向通信需创建两个独立管道 fork() 继承文件描述符)通信 2. 关键特性
特性 | 说明 |
|---|---|
临时性 | 生命周期与进程绑定,进程退出后内核自动回收资源 。 |
资源轻量 | 无磁盘文件实体,仅占用内核内存(默认缓冲区 4KB) |
流式传输 | 数据为无格式字节流,需应用层定义消息边界(如分隔符) |
阻塞同步 | 读空时阻塞等待数据,写满时阻塞等待空间 |
1. 核心数据结构
struct pipe_buffer {
struct page *page; // 内存页指针
unsigned int offset; // 当前读写偏移
unsigned int len; // 有效数据长度
};
struct pipe_inode_info {
struct pipe_buffer *bufs; // 环形缓冲区数组
unsigned int head; // 写指针
unsigned int tail; // 读指针
wait_queue_head_t rd_wait; // 读等待队列
wait_queue_head_t wr_wait; // 写等待队列
};head 和 tail 指针管理读写位置 pipefs 分配 inode,支持标准文件操作接口(read/write)2. 系统调用 pipe() 流程
pipe() 时,内核分配 pipe_inode_info 结构体,初始化环形队列和等待队列 fd[0](读端)绑定至读操作函数集,fd[1](写端)绑定至写操作函数集 。fork() 后子进程复制父进程的文件描述符表,共享同一管道缓冲区 。3. 同步与阻塞机制
rd_wait 队列休眠,写操作完成后唤醒;wr_wait 队列休眠,读操作释放空间后唤醒 下面我们直接来一段代码示例,让父子进程通过匿名管道通信,以此加深我们对匿名管道的理解
首先创建管道,注意pipe的参数是输出型参数
#include <iostream>
#include <cstdio>
#include <unistd.h>
int main()
{
// 1.创建管道
int fds[2] = {0};
int n = pipe(fds);
if(n < 0)
{
std::cerr << "pipe error" << std::endl;
return 1;
}
std::cout << "fds[0]:" << fds[0] << std::endl;
std::cout << "fds[1]:" << fds[1] << std::endl;
return 0;
}运行测试一下:
ltx@iv-ye1i2elts0wh2yp1ahah:~/Linux_system/lesson10/TestPipe$ make
g++ -o testPipe testPipe.cc -std=c++11
ltx@iv-ye1i2elts0wh2yp1ahah:~/Linux_system/lesson10/TestPipe$ ./testPipe
fds[0]:3
fds[1]:4接下来创建子进程,然后分别关闭父子进程的写端和读端,这里我们让父进程来读,子进程来写
// 2. 创建子进程
pid_t fd = fork();
if(fd == 0)
{
// child
// 3. 关闭不需要的读写端,形成通信信道
// father->read, child->write
close(fds[0]);
ChildWrite(fds[1]);
close(fds[1]);
}
// father
// 3. 关闭不需要的读写端,形成通信信道
// father->read, child->write
close(fds[1]);
FatherRead(fds[0]);
waitpid(fd, nullptr, 0);
close(fds[0]);
子进程写
void ChildWrite(int wfd)
{
char buffer[1024];
int cnt = 0;
while(true)
{
snprintf(buffer, sizeof(buffer), "I am child, pid:%d, cnt:%d", getpid(), cnt++);
write(wfd, buffer, strlen(buffer));
sleep(5);
}
}父进程读
void FatherRead(int rfd)
{
char buffer[1024];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
if(n > 0)
{
buffer[n] = 0;
std::cout << "child say:" << buffer << std::endl;
}
else if(n == 0)
{
std::cout << "子进程退出" << std::endl;
break;
}
else
{
break;
}
}
}这里我们写一条消息就sleep5秒,子进程写得慢,父进程读得快会怎么样呢

read() 会阻塞,直到管道中有数据可读 。 那如果写得快读得慢呢,或者写端关了读端继续读,读端关了写端继续写会怎么样
1. 写快读慢(写入速度快于读取速度)
write() 暂停),直到读端取走部分数据腾出缓冲区空间后才能继续写入 。
write() 被阻塞;父进程每次读取后,子进程才能继续写入。head - tail = buffer_size 时,写操作被阻塞;读操作使 tail 后移,唤醒阻塞的写端 。
2. 写端关闭后读端继续读
close(fds[1])),父进程(读端)的 read() 会返回 0,表示已读取到文件结束符(EOF)
read() 会立即返回0(而非阻塞),通知进程通信终止 。
n=0 后应退出循环并关闭读端,避免资源泄漏 。
3. 读端关闭后写端继续写
close(fds[0])),子进程(写端)的 write() 会触发 SIGPIPE 信号(默认行为是终止进程)。
write() 将收到信号13(SIGPIPE),进程被强制终止。errno=32),再次写入则触发信号 。
补充:
读端不读且写端持续写
这里我们就不一一验证了,感兴趣可以自己下来验证
场景 | 行为 | 解决方案 |
|---|---|---|
写快读慢 | 写端阻塞直到缓冲区有空位 | 优化读端速度或扩大缓冲区 |
读快写慢 | 读端阻塞直到有新数据 | 增加写端频率 |
写端关闭后读端读 | read() 返回0 (EOF) | 检测 n=0 并退出循环 |
读端关闭后写端写 | 触发 SIGPIPE 终止写进程 | 捕获信号或检查 EPIPE 错误 |
读端不读且写端持续写 | 死锁 | 避免循环依赖或设置超时机制 |
以上是站在文件描述符角度来理解管道,如下图所示,注意:代码示例和图中有一个不同,那就是代码示例是父进程读子进程写,图示则相反

我们还可以从内核角度来理解管道本质
一、管道的内核本质:内存级环形缓冲区
pipe_inode_info)// 内核源码 fs/pipe.c
struct pipe_inode_info {
struct pipe_buffer *bufs; // 环形缓冲区数组(默认16个page,64KB)
unsigned int head; // 写指针(生产者位置)
unsigned int tail; // 读指针(消费者位置)
wait_queue_head_t rd_wait; // 读等待队列
wait_queue_head_t wr_wait; // 写等待队列
unsigned int readers; // 读端引用计数
unsigned int writers; // 写端引用计数
};page)为单位组织,通过 head 和 tail 实现循环写入。文件抽象层
通过 pipefs 虚拟文件系统创建匿名文件:
struct file *f = alloc_file_pseudo(inode, pipe_mnt, "", flags, &pipefifo_fops);返回两个文件描述符:
fd[0] 绑定读操作函数集:.read = pipe_readfd[1] 绑定写操作函数集:.write = pipe_write二、管道创建与通信的内核流程
1. 系统调用 pipe() 的完整流程
步骤 | 内核操作 | 用户态表现 |
|---|---|---|
1. 分配资源 | 调用 get_pipe_inode() 创建 pipe_inode_info | int pipefd[2] |
2. 绑定描述符 | 为读写端分别创建 file 结构体 | 返回 fd[0](读端)、fd[1](写端) |
3. 血缘进程继承 | fork() 时复制文件描述符表 | 子进程共享同一管道缓冲区 |

2. 数据读写内核路径
写操作(pipe_write):
while (缓冲区满) {
将当前进程加入 wr_wait 队列;
设置进程状态为 TASK_INTERRUPTIBLE;
调用 schedule() 让出CPU;
}
将数据拷贝到 bufs[head] 对应的内存页;
head = (head + 1) & (bufs_mask);
唤醒 rd_wait 队列中的进程;读操作(pipe_read):
while (缓冲区空) {
加入 rd_wait 队列;
TASK_INTERRUPTIBLE;
schedule();
}
从 bufs[tail] 拷贝数据到用户空间;
tail = (tail + 1) & (bufs_mask);
唤醒 wr_wait 队列中的进程;关键机制:自旋锁保护
head/tail修改的原子性
三、四种通信场景的内核行为
1. 写得慢 & 读得快
sleep(1))rd_wait 队列休眠(阻塞)2. 写得快 & 读得慢
sleep 高速写,父进程每5秒读1次
wr_wait 队列阻塞100%占用读操作后唤醒并继续写入释放部分空间 3. 写端关闭后读端继续读
writers == 04. 读端关闭后写端继续写
readers == 0write() 返回 -1,errno=EPIPEwaitpid 收到信号四、管道特性的内核实现原理
1. 血缘关系限制
fork() 时复制父进程的 files_structfile->f_inode 访问同一缓冲区2. 原子性保证
条件:单次写入 ≤ PIPE_BUF(默认4KB)
内核实现:
mutex_lock(&pipe->mutex); // 加互斥锁
copy_page_from_iter(buf, offset, bytes, from); // 原子拷贝
mutex_unlock(&pipe->mutex);超过
PIPE_BUF时数据可能被拆分
3. 同步机制
rd_wait 阻塞立即返回缓冲区满立即返回加入 wr_wait 阻塞 管道的核心是内存中的环形缓冲区(pipe_inode_info),通过文件抽象层和等待队列同步机制实现进程间通信。
完整代码示例:
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
void ChildWrite(int wfd)
{
char buffer[1024];
int cnt = 0;
while(true)
{
snprintf(buffer, sizeof(buffer), "I am child, pid:%d, cnt:%d", getpid(), cnt++);
write(wfd, buffer, strlen(buffer));
sleep(5);
}
}
void FatherRead(int rfd)
{
char buffer[1024];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
if(n > 0)
{
buffer[n] = 0;
std::cout << "child say:" << buffer << std::endl;
}
else if(n == 0)
{
std::cout << "子进程退出" << std::endl;
break;
}
else
{
break;
}
}
}
int main()
{
// 1.创建管道
int fds[2] = {0};
int n = pipe(fds);
if(n < 0)
{
std::cerr << "pipe error" << std::endl;
return 1;
}
std::cout << "fds[0]:" << fds[0] << std::endl;
std::cout << "fds[1]:" << fds[1] << std::endl;
// 2. 创建子进程
pid_t fd = fork();
if(fd == 0)
{
// child
// 3. 关闭不需要的读写端,形成通信信道
// father->read, child->write
close(fds[0]);
ChildWrite(fds[1]);
close(fds[1]);
}
// father
// 3. 关闭不需要的读写端,形成通信信道
// father->read, child->write
close(fds[1]);
FatherRead(fds[0]);
waitpid(fd, nullptr, 0);
close(fds[0]);
return 0;
}