一、什么是进程间通信
我们知道,操作系统中每个进程都拥有独立的内存空间,彼此相互隔离。但实际业务中,多个进程往往需要配合完成任务,必须交换数据、同步运行状态。由于进程不能直接访问对方内存,操作系统便设计了专门的交互方案,这就是进程间通信(Inter-Process Communication,简称 IPC)。
二、进程通信的目的
进程间通信(IPC)核心目的
- 数据传输:进程间互相传递业务数据、文件、指令等信息。
- 资源共享:多个进程共用同一文件、内存等系统资源,避免重复创建。
- 进程同步:协调执行顺序,防止竞争冲突,保证运行逻辑有序。
- 事件通知:一个进程向其他进程发送状态、信号,触发对应动作。
- 进程控制:管控其他进程的启停、挂起、终止等行为。
三、通信行为本质
两大类行为本质
- 数据拷贝中转(主流方式)
操作系统在进程地址空间之外,开辟中间缓冲区。数据从发送进程拷贝到缓冲区,再从缓冲区拷贝到接收进程。
代表:管道、命名管道、消息队列、Socket。
- 内存区域共享(特殊方式)
操作系统划出一块物理内存 ,映射到多个进程的虚拟地址空间。进程直接读写同一片内存,无额外数据拷贝。
代表:共享内存(需搭配信号量做同步)。
四、通信方法
1. 匿名管道
1)核心原理
匿名管道是操作系统在内核空间 开辟的一块固定大小的环形缓冲区 ,通过一对文件描述符(fd0读端、fd1写端)实现父子 进程间的半双工通信。(父子之间最常用,有血缘关系也能通信)
- 单工:固定单向,永远只能一方发、一方收(如广播)
- 半双工 :可切换方向,但同一时刻只能单向(如对讲机、普通管道)
- 全双工:双向同时传输,收发互不干扰(如手机通话)
2)创建管道
c
// 创建管道
if (pipe(int fd[2]) == -1) {
perror("pipe创建失败");
return 1;
}
内核动作:
-
创建两个
struct file对象(都位于内核内存中); -
分配并初始化共享的管道缓冲区;
-
配置第一个
struct file(读端):-
f_op->read→ 指向管道的读。 -
private_data→ 指向上面那个共享的struct inode。 -
不支持写操作,不支持
lseek。
-
-
配置第二个
struct file(写端):f_op->write→ 指向管道的写。private_data→ 指向同一个struct inode。- 不支持读操作,不支持
lseek。
-
在当前进程的文件描述符表中分配两个槽位:
- 第一个空槽位存入读端
struct file的指针 →fd[0]。 - 第二个空槽位存入写端
struct file的指针 →fd[1]。
- 第一个空槽位存入读端

对比打开文件时创建的struct file:
- 相同点 :无论是
open()还是pipe(),内核创建的都是同一类数据结构 ------struct file对象。它们都遵循"一切皆文件"的设计哲学,都有文件操作函数表 (f_op)、私有数据指针等通用字段。 - 不同点 :它们的作用和内部实现 完全不同:
open()创建的struct file背后连接的是磁盘上的文件 (或设备、目录等),主要功能是读写磁盘数据,维护文件偏移量 (f_pos)。pipe()创建的两个struct file(读端和写端)背后连接的是内核中的管道环形缓冲区 (pipe_inode_info),主要功能是进程间通信,没有文件偏移量的概念,读写行为基于缓冲区而非磁盘。
3)管道中的环形缓冲区
为什么匿名管道要用环形缓冲区 ?因为管道是 "先进先出(FIFO)" 的字节流,用环形缓冲区能最高效、无浪费、无移动地实现 "读一点、写一点、循环利用空间"。其实说是环形,本质还是线性,只是逻辑结构变成了环形。实现逻辑环形,就是当读写指针走到开辟空间的末尾时,重新回到空间起始位置。
工作流程
- 有固定大小(管道默认 64KB)
- 写数据 → 写指针往后移
- 读数据 → 读指针往后移,读过的数据就"释放"了
- 指针走到末尾 → 回到 0 位置继续
- 读写指针相遇 = 缓冲区空 / 满
为线性时的缺点:
当读指针往后移之后,已经读过的空间就浪费掉了

4)管道的使用
管道的使用只能在父子进程之间。其使用类似于对文件的读写,之不过管道有两个struct file,一个是读,一个是写;当创建子进程之后,首先要关闭一个 struct file 只保留一个用来读或写。fd[0]/fd[1] 就是他们的文件描述符。
c
char buf[1024]; // 读写缓冲区
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork失败");
return 1;
}
// 子进程:读数据
if (pid == 0) {
// 子进程只需要读,关闭写端
close(fd[1]);
// 从管道读取数据
int len = read(fd[0], buf, strlen(buf) + 1);
printf("子进程读取到数据:%s\n", buf);
// 关闭读端
close(fd[0]);
return 0;
}
// 父进程:写数据
else {
// 父进程只需要写,关闭读端
close(fd[0]);
// 向管道写入数据
char* msg = "Hello, Pipe!";
write(fd[1], msg, sizeof(msg));
printf("父进程写入数据:%s\n", msg);
// 关闭写端
close(fd[1]);
// 等待子进程结束
wait(NULL);
}
5)字节流概念
核心结论
面向字节流 = 没有边界、没有格式、没有消息头,就是一串连续的字节。读多少、写多少、怎么分块,完全由进程自己决定。
1. 最简单的比喻
管道就像一根水管,流的是 "水",不是 "一包一包的东西"。
- 你写 100 字节 → 变成连续水流进去
- 你读 20 字节 → 取出前面 20 字节
- 剩下 80 字节还在流里
- 没有分割、没有边界、没有编号、没有结构
这就叫字节流。
2. 对比一下
面向字节流(管道)
写:ABCDEFGHIJK
读:先读5个 → ABCDE
再读3个 → FGH
剩下 → IJK
没有边界,怎么读都行,连续不断。
面向数据报(消息队列)
写:第一条:ABC 第二条:DEF
读:必须一次读完整一条,不能读一半
有边界、有格式、有消息头。
3. 管道字节流的 3 个特点
① 无边界
一次写 100 字节,对方可以分 10 次读,每次读 10 字节。
怎么拆、怎么拼,管道不管。
② 连续、有序
像水流一样,先进先出,顺序不乱。
③ 无格式
管道只认0101 二进制字节,不管是字符串、整数、结构体。
4. 最关键的一句话
面向字节流 = 数据没有 "包" 的概念,只有 "流" 的概念。写进去是一串,读出来可以任意切分,管道不负责划分消息。
6)若进程结束没关闭管道
在文件的 struct file 中,有着引用计数 f_count 专门用来记有几个进程打开了这个文件,多一个文件就++,少一个文件就--。那么管道的 struct file 自然也有这个引用计数,当进程结束,管道的 f_count-- ,管道的生命周期也就结束了。
7)管道的阻塞
1. 读端阻塞:管道为空(无数据)
- 现象:
read()调用卡住不返回,进程挂起。 - 触发:管道缓冲区里没有任何数据 ,且所有写端文件描述符都未关闭。
- 逻辑:内核认为还有进程会继续写数据,于是读进程休眠等待数据。
2. 读端不阻塞(正常退出)
管道为空,但所有写端 fd 全部 close:
read() 直接返回 0(表示读到文件末尾),读进程结束读取。
3. 写端阻塞:管道缓冲区已满
- 现象:
write()调用卡住,写进程挂起。 - 触发:管道环形缓冲区被写满 ,且所有读端 fd 都未关闭。
- 管道默认缓冲区大小:常见 4096 字节(PAGE_SIZE)。
- 逻辑:缓冲区装不下新数据,内核等待读进程取走数据。
4. 写端异常:所有读端已关闭(管道破裂)
致命场景 :管道所有读端都 close,此时再执行 write()
- 写进程会收到 SIGPIPE 信号(默认行为:进程直接终止)。
- 若捕获 / 忽略 SIGPIPE:
write()返回 -1 ,errno = EPIPE。 - 俗称:管道断裂(broken pipe)。

读全关,写操作的杀死情况:
c
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
int fd[2];
char buffer[1024];
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) { // Child process
close(fd[0]);
sleep(1);
write(fd[1], "child_write", 11);
exit(0);
} else { // Parent process
close(fd[0]);
int wstatus;
waitpid(pid, &wstatus, 0);
if (WIFEXITED(wstatus)) {
printf("子进程正常退出\n");
}
else if (WIFSIGNALED(wstatus)) { // 被信号杀死
printf("子进程被信号杀死!信号编号:%d (SIGPIPE)\n", WTERMSIG(wstatus));
}
}
return 0;
}
