12【进程间通信——管道】
1 IPC是什么 & 为什么要通信
1.1 什么是进程间通信
进程间通信(Inter-Process Communication,IPC)是指两个或多个进程之间实现数据层面的交互。由于进程的天然独立性,通信的成本相对较高。
1.2 既然成本高为什么还要通信
因为很多场景下,进程之间必须协同工作,例如:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有缺陷和异常,并能够及时知道它的状态改变
1.3 通信的本质:共享一块资源
进程间通信的核心问题是:如何让两个进程看到同一块“资源”
这块资源为什么不能由其中一个进程来提供?——> 因为这样会破坏进程独立性,这块资源必须由操作系统提供!!!
本质上进程在访问 IPC,就是在访问操作系统,因为操作系统不信任用户进程,所以通信的整个生命周期 —— 创建、读写、释放 ——都必须通过系统调用接口来完成!
1.4 通信方式的分类
类型 | 方式 | 说明 |
---|---|---|
基于文件的通信 | 匿名管道、命名管道 | 最基础、最常见的通信方式 |
System V IPC | 消息队列、共享内存、信号量 | 较老但仍在广泛使用的传统机制 |
POSIX IPC | 消息队列、共享内存、信号量、互斥量、条件变量、读写锁 | 接口更标准、更现代化、更易用 |
2 管道文件
2.1 管道的原理
我们知道,一个文件可以被多个进程同时打开,这就具备了“公共资源”的特性。理论上,一个进程写、另一个进程读,就可以实现进程间通信。
但是,如果我们用普通文件作为通信媒介,数据必须写入外设(如磁盘),这将带来严重的效率问题!
于是,出现了“内存级文件”的概念——它像文件一样出现在文件系统中,但实际上只存在于内存,不会被刷写到磁盘。这类文件就是我们所说的:管道(pipe)。
管道本质上是一种内核缓冲区(内存空间),通过“文件接口”封装出来,用户进程可以读写它,但数据只存在于内存中,从而避免频繁访问外设,提高通信效率。
2.2 匿名管道
问题1:为什么要用“父子进程”?
在 Linux 中,子进程通过 fork() 从父进程复制而来,它会自动继承父进程的文件描述符表(fd表)。这就为“共享一块通信资源”提供了最简单的实现路径。
问题2:为什么不能用 open()
打开一个普通文件作为通信通道
open()
打开的是真实存在于磁盘上的文件,会有刷盘动作- 普通文件不能只存在于内存中
- 操作系统无法区分你是“为了存储”还是“为了通信”
所以 Linux 提供了专门的接口 pipe(),用于创建一个 匿名管道(内存级文件)
匿名管道的特点:
- 用
pipe(int fd[2])
创建,会自动生成两个文件描述符:fd[0]
: 读端(只读权限)fd[1]
: 写端(只写权限)
- 是单向通信通道,默认不支持读写共用
- 管道没有路径,不存在于磁盘中,只存在于内核内存
- 通常在
fork()
之后,将一个端口交给子进程,一个端口保留在父进程,实现通信
直接看代码
#include <iostream>
#include <unistd.h>#define N 2using namespace std;int main()
{int pipefd[N] = {0};int n = pipe(pipefd);if(n < 0) return 1;cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << endl;// VScode里面快速注释是ctrl + /// pipefd[0]: 3, pipefd[1]: 4return 0;
}
makefile
文件
testPipe:testPipe.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f testPipe
main.c
文件:
#include <iostream>
#include <unistd.h> // pipe 、close、fork、write的头文件
#include <stdlib.h> // exit 的头文件
#include <string.h> // 字符串的头文件
#include <stdio.h> // snprintf的头文件
#include <sys/types.h> // wait的头文件
#include <sys/wait.h>#define N 2
#define NUM 1024using namespace std;// child
void Writer(int wfd)
{string s = "hello, I am child";pid_t self = getpid();int number = 0;char buffer[NUM]; // 定义的缓冲区while(true){// 构建发送字符串buffer[0] = 0; // 字符串清空,提醒阅读代码的人,我把这个数组当字符串了// int snprintf(char *str, size_t size, const char *format, ...);// 向哪个字符串写,写多大,什么形式写进去// s.c_str():将 std::string 转为 C 风格字符串(const char*)snprintf(buffer, sizeof(buffer),"%s-%d-%d", s.c_str(), self, number++);// cout << buffer << endl;// 发送/写入给父进程 man 2 write 查2号手册write// ssize_t write(int fd, const void *buf, size_t count);write(wfd, buffer, strlen(buffer)); // 这里不加一,\0不需要往里面写入sleep(1);}
}// father
void Reader(int rfd)
{char buffer[NUM];while(true){buffer[0] = 0;// ssize_t read(int fd, void *buf, size_t count); 查2号手册 man 2 readssize_t n = read(rfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0; // 我是当字符串用,必须\0结尾,这里0等于 '\0'cout << "father get a message[" << getpid() << "]# " << buffer << endl;}}
}int main()
{int pipefd[N] = {0};int n = pipe(pipefd);if(n < 0) return 1;// cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << endl;pid_t id = fork();if(id < 0) return 2;// chile -> w father -> rif(id == 0){// 子进程close(pipefd[0]);// IPC codeWriter(pipefd[1]);// 写完后,写窗口也要关掉close(pipefd[1]);exit(0); // 子进程写完后退出}// fatherclose(pipefd[1]);// IPC codeReader(pipefd[0]);pid_t rid = waitpid(id, nullptr, 0); // 回收子进程if(rid < 0) return 3;// 读完成后,读窗口也关闭close(pipefd[0]);return 0;
}
2.3 匿名管道的特征
- 血缘关系限定:只能用于父子(或兄弟)进程通信
- 单向通信:一个管道只能实现单向流动;要双向通信需建立两个管道
- 进程协同机制:Linux 自动实现同步和互斥,保证管道数据安全
- 字节流特性:管道是面向字节流的,系统不关心你写了几次,它只看缓冲区里的字节
- 原子性写入保证:系统会确保在写入量不超过 PIPE_BUF 时,父进程不会来读
- 生命周期与进程绑定:匿名管道与进程生命周期一致,进程退出后管道自动销毁。
2.4 管道中的四种关键状态
情况 | 描述 |
---|---|
① 读写正常,管道空 | 读端阻塞,防止读到无效数据 |
② 读写正常,管道满 | 写端阻塞,防止数据覆盖 |
③ 写端关闭,读端读 | 读到返回值为 0,代表 EOF,不阻塞 |
④ 读端关闭,写端写 | 写操作被操作系统发送 SIGPIPE 信号终止 |
2.5 命名管道
匿名管道只能用于有血缘关系的进程,而两个毫无关系的进程通信就需要命名管道。Linux 提供的命名管道(Named Pipe / FIFO),创建方式如下:
mkfifo /tmp/myfifo
命名管道的特点:
- 具有路径名,出现在文件系统中
- 使用
open()
打开,进程通过同一路径访问同一个管道文件 - 同样是内存级通信,不会刷盘
- 适合非父子进程之间的通信
- 内核内部只开辟一块缓冲区,不论多少进程打开,只维护一份数据
问题1:两个无血缘进程如何确保访问的是同一个命名管道?
答:只要访问的路径+文件名相同,就会访问同一个命名管道。
问题 2:多个进程同时打开同一个命名管道,操作系统会创建多个副本吗?
答:不会。操作系统内部只维护一个文件对象 + 一个缓冲区