%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # 进程间的通信方式 ⭐ > **进程间通信**(Inter-Process Communication,IPC) 进程间通信方式包括[^1] [^2]: - **管道**(匿名管道 pipe、具名管道 FIFO) - **信号**(异步通知) - **系统 IPC 三件套**(消息队列、信号量、共享内存) - **套接字 Socket** #### IPC 对象的持久性 持久性是指 **IPC 对象的生命周期**: - **==进程持久性==**:IPC 对象存在直至 "**最后一个持有该对象的进程关闭**"。 - 包括:管道、FIFO、socket、匿名内存映射 - **==内核持久性==**:IPC 对象**由内核维护**,直至被显式删除或系统关闭时才销毁,与进程是否已打开该对象无关。 - 包括:POSIX IPC 三件套、System IPC 三件套; - **==文件系统持久性==**:**持久存在于文件系统**,直至被从文件系统中删除。 - 唯一具有文件系统持久性的 IPC 对象是 "**基于内存映射文件的共享内存**"。 > [!NOTE] 三种持久性的 IPC 对象 [^2] > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-866DBB0E895576B65DB9413A29D9D19F.png|865]] > > [!summary] 总结:各种 IPC 对象的持久性[^3] > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-FF1E9BEBA2CF6CB673947049A860B952.png|705]] > <br> ## "通信" 与 "同步" 总结 二者侧重点不同: - 通信:关注于 **进程间的数据交换**。 - 同步:关注于 **进程、线程**操作之间的 "互斥",协调**进程/线程间的执行顺序**,防止产生条件竞争、数据不一致问题。 > [!NOTE] UNIX IPC 涉及的 "**通信**" 与 "**同步**" 工具总览[^1] > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-F386BC1A0E0333527577074F69986048.png|569]] > > 关于上图中 "同步" 工具: > > - 条件变量、互斥锁通常用于 "**线程间同步**",也可用于 "**进程间同步**"(要求将条件变量、互斥锁以 "**共享内存**" 的方式供**跨进程访问**) > - 信号量即可用于 "**进程间同步**",也可用于 "**线程间同步**"。 > > 关于系统 IPC 三件套(消息队列、信号量、共享内存): > > - "**POSIX IPC 三件套**" 是对早期 **"SystemV IPC 三件套"** 实现的改进。 > [!summary] 总结:进程、线程间 "通信" 与 "同步" 方式 > > - **进程间通信**(消息传递) > - **管道**(匿名管道 pipe、具名管道 FIFO) > - **信号**(异步通知) > - **消息队列** > - **共享内存** > - **套接字 Socket** > > - **进程间同步** > - **信号量** > - **文件锁** > - **互斥锁 & 条件变量**(需作为 "**共享内存**" 访问) > > - 线程间通信 ❌(不需要,多线程共享进程的内存空间,在同步机制下安全访问进程内的全局变量即可) > > - **线程间同步**: > - 互斥锁 > - 条件变量 > - 信号量 > - 屏障 > - 原子操作 > <br><br><br> # 管道 「**管道**」是本质上是 **==内核中的一段缓冲区==**,从管道一端写入的数据**缓存在内核中**,而由另一端进行读取。 特点: - 典型实现为 "**半双工通信**":数据只能单向流动; - 通过 **文件 I/O** 直接读/写管道(`read`, `write` 等) - **管道中的数据遵循 FIFO 原则**,不支持 lseek 之类的文件定位。 > [!NOTE] POSIX 标准库提供的管道 API: > > - **匿名管道**:`pipe`、`popen`、`pclose` > - **具名管道**(FIFO):`mkfifo`、`mkfifoat` > > <br> ### 管道的类型 两种类型: - **==匿名管道==**:**通过 `pipe()` 创建**,只在进程中有效,故只能在 **==具有公共祖先的两个关联进程之间==** 使用; - **==具名管道==**(FIFO):**通过 `mkfifo()` 创建**,会在文件系统中创建一个 FIFO 文件,因此 **==可用于无关联的任意进程间的通信==**。 > [!NOTE] 管道的生命周期 > > - **匿名管道**:随进程,当 **"最后一个" 引用管道的进程终止时**,管道也被完全地删除。 > - **具名管道 FIFO**: > - 当最后一个引用 FIFO 的进程终止时,**FIFO 中的数据会被删除**。 > - **FIFO 文件**会**持久化**在文件系统中,**直至被显式删除**。 > > [!example] 匿名管道的典型用法——**父子进程间的单向通信** > > 典型用法: > > 1. 父进程创建管道,并 `fork` 生成子进程; > 2. 关闭其中一方的 "**管道读端**",同时关闭另一方的 "**管道写端**" > > 由此,构成了父子进程间的 "**单向通信**",**两个进程分别负责管道的读或写**。 > > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-7C2131A47DBA02A42406DAF2D1E4368D.png|437]] > > [!example] shell 中的 "管道命令 `|`" 背后即是创建 "匿名管道" > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-6804931E1BEDF33F291226A7C0F7D214.png|665]] > <br><br> ## 匿名管道 ### pipe 函数 ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-C15E53E019ED83A8C5D79225A80BE96A.png|647]] `pipe` 用于创建 **==匿名管道==**,其存在于 "**内存中的内核地址空间**"。 创建成功时,传入参数 `fd` 将被赋予两个 "**文件描述符**",提供一个 "**==单向数据流==**": - `fd[0]` 对应**管道的读端**; - `fd[1]` 对应**管道的写端**,其**输出将是 `fd[0]`的输入**。 > [!NOTE] `pipe()` 系统调用打开的两个文件描述符是 "**内核管道**" 的读写端。 > ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-F491CDA8FDC7519D55370A1F8AAC15CB.png|398]] > [!NOTE] 实现 "**双向通信**" 需要打开两个管道 > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-E98D652BCE03F4B9FC9309C171011B27.png|406]] > <br> ### popen 与 pclose 函数 > 位于头文件 `<stdio.h>` 中 > ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-7D2C8BF619B39CBE962E0ACCEE82B63B.png|732]] `popen` 创建一个管道并 **==启动另一个进程==**,返回一个**标准 I/O 文件指针**(根据参数 `type` 类型,指针指向不同): - 若 `type == 'r'`,则指针指向 `command` 进程的 **标准输出**,可读取来自 `command` 的输出; - 若 `type == 'w'`,则指针指向 `command` 进程的 **标准输入**,可写入 `command` 的标准输入; `pclose` 关闭标准 I/O 流,**阻塞等待 `popen` 创建的子进程结束**。 > [!NOTE] `popen` 是对 `pipe` 、`fork` 、`exec`的封装,其返回一个标准 I/O 文件指针,指向所创建子进程的标准输入或标准输出。 <br> ### 匿名管道使用示例 ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-CFB14C38D7D71CA008E6B64129B17AA6.png|537]] 下例中,父子进程之间的通信示意图如上: ```c #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> int main(int argc, char** argv) { int fd[2]; // 创建一个管道, 获取两个文件描述符fd[0]与fd[1], 后者用于写, 前者用于读, 后者的输出作为前者的输入 if (pipe(fd) == -1) { printf("pipe error\n"); exit(1); } // 创建第一个子进程, 将其标准输入写入到fd[1] pid_t pid1 = fork(); if (pid1 < 0) { printf("fork failed\n"); exit(1); } else if (pid1 == 0) { // 子进程1 close(fd[0]); // 关闭读端 dup2(fd[1], STDOUT_FILENO); // 将标准输出重定向到管道的写端. close(fd[1]); // 关闭写端(已完成重定向, 此后fd[1]已无用) printf("hello, world!\n"); // 子进程1标准输出=>重定向到管道写端. exit(0); } // 创建第二个子进程, 读取fd[0]. pid_t pid2 = fork(); if (pid2 < 0) { printf("fork failed\n"); } else if (pid2 == 0) { close(fd[1]); // 关闭写端; dup2(fd[0], STDIN_FILENO); // 将标准输入重定向到管道的读端 close(fd[0]); // 关闭读端(已完成重定向, 此后不需要再使用fd[0]). // 子进程2执行cat命令, 其标准输入已重定向到管道读端. char* args[] = {"cat", NULL}; execvp("cat", args); perror("execvp failed"); exit(0); } // 父进程: 关闭管道的读写端, 等待两个子进程结束. close(fd[0]); close(fd[1]); // 等待两个子进程结束 waitpid(pid1, NULL, 0); waitpid(pid2, NULL, 0); return 0; } ``` <br><br> ## 具名管道 > ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-B4CAE0AE239FF08E03A8465BD6569DEF.png|647]] `mkfifo` 或 `mkfifoat` **创建或打开一个已存在的具名管道**,**进程间通过文件 I/O 函数**来读写 FIFO 文件,**实现通信**。 > [!NOTE] linux 中具有**同名的 `mkfifo` 命令** <br><br><br> # 消息队列 「**消息队列**」:由操作系统**内核**维护的**消息链接表**,由**消息队列标识符**标识,用于**存储消息**,实现**进程间==异步==通信**。 ### 消息队列的特点 特点: - **异步通信**:消息队列不需要发送方和接收方同时工作,消息**保存在队列中,直到接收方准备好读取** - **类型化消息**:每个消息具有 "**消息类型**" 和 "**消息内容**" 两项信息,其中消息类型由 "**类型标识符**" 指示,**进程可筛选和读取特定类型消息** - **持久化消息**:消息队列**生命周期随内核**,**消息在队列中存在==直到被读取或显式删除==**,因此消息不会因进程的退出而丢失。 - **可按优先级排序**:默认采用 FIFO 队列,也可根据 "**消息优先级**" 改为 "**优先队列**"。 缺点: - **通信不及时** - **不适合大数据传输**:内核中**每个消息体都有一个最大长度的限制**,同时**所有队列所包含的全部消息体的总长度也是有上限** - Linux 内核中,宏定义 `MSGMAX` 和 `MSGMNB`,以字节为单位,分别**定义了==一条消息的最大长度==和==一个队列的最大长度==**。 - 向**消息队列写入、读取消息**的过程中存在 "**用户态-内核态**" 之间的切换开销 ### 消息队列工作过程 - **消息发送**:进程通过系统调用将一条消息发送到消息队列中。消息包含消息类型(标识符)和消息数据。 - **消息存储**:消息在队列中按照 **FIFO 顺序**或**优先级顺序**(优先队列)存储,直到接收方读取。 - **消息读取**:接收进程通过系统调用读取队列中的消息。它可以选择读取特定类型的消息,或者按队列的顺序读取消息。 消息队列中每条消息以 "**==消息体==**" (用户自定义数据类型)形式作为**数据单元**,具有**固定大小**,由**消息发送方和接收方约定好**。 ## OS 消息队列接口 消息队列有不同标准下的实现: - **POSIX 消息队列**: - POSIX 标准提供了 `mq_open`、`mq_send`、`mq_receive` 等 API 来管理消息队列,位于头文件 `<mqueue.h>` 中 - **System V 消息队列**: - System V 消息队列(早期的 IPC 机制)通过 `msgget`、`msgsnd` 和 `msgrcv` 函数来操作消息队列,位于头文件 `<sys/msg.h>` 中 ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程间通信方式.assets/IMG-进程间通信方式-A786F62A918B93FA3565E1D1F54ABB89.png|494]] #### POSIX 消息队列 ```c #include <stdio.h> #include <stdlib.h> #include <fcntl.h> /* FOr O_* constants */ #include <mqueue.h> /* For POSIX message queues */ #include <sys/stat.h> /* For mode constants */ #include <string.h> int main() { mqd_t mq; // 消息队列描述符 struct mq_attr attr; // mqueue中提供的消息队列属性 const char* message = "Hello, POSIX Message queue!"; char buffer[1024]; // 设置消息队列属性 attr.mq_flags = 0; // 阻塞模式 attr.mq_maxmsg = 10; // 最大消息数 attr.mq_msgsize = 1024; // 消息最大大小 attr.mq_curmsgs = 0; // 当前消息数量 // 创建消息队列(为什么是创建文件?) mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0644, &attr); if (mq == (mqd_t) - 1) { perror("mq_open"); exit(1); } // 发送消息: mq_send if (mq_send(mq, message, strlen(message) + 1, 0) == -1) { perror("mq_send"); exit(1); } printf("消息已发送: %s\n", message); // 接收消息: mq_receive if (mq_receive(mq, buffer, 1024, NULL) == -1) { perror("mq_receive"); exit(1); } printf("接收到的消息: %s\n", buffer); // 关闭并删除消息队列 mq_close(mq); mq_unlink("/my_queue"); return 0; } ``` #### System V 消息队列 ```c #include <sys/ipc.h> // System V IPC, for ftok. #include <sys/msg.h> // System V message queue, for msgget, msgsnd, msgrcv, msgctl #include <string.h> #include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> #include <assert.h> // 定义消息结构体 struct msg_buffer { long msg_type; // 消息类型,必须为long类型 char msg_text[100]; }; int main() { key_t key; int msgid; // 生成一个唯一的key, 用作消息队列IPC对象的外部名, 通常通过文件路径和整数生成 key = ftok("profile", 65); // 创建消息队列, 返回消息队列ID标识符 msgid = msgget(key, 0666 | IPC_CREAT); struct msg_buffer message; // 消息体 message.msg_type = 1; // 设置消息类型 // 创建父子进程, 一个发送一个读取 pid_t pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } if (pid > 0) { // parent // 准备要发送的消息内容 strcpy(message.msg_text, "Hello, this is a message!"); // 发生消息到消息队列: msgsnd msgsnd(msgid, &message, sizeof(message), 0); printf("发送的消息: %s\n", message.msg_text); } else { // child // 接收消息 msgrcv(msgid, &message, sizeof(message), 1, 0); // 接收消息类型为1 printf("接收到的消息: %s\n", message.msg_text); } int rc = wait(NULL); assert(rc == 0); msgctl(msgid, IPC_RMID, NULL); return 0; } ``` <br><br><br> # 共享内存 参见[[02-开发笔记/05-操作系统/进程管理/共享内存|共享内存]] <br> # 信号量 - 概念参见:[[02-开发笔记/05-操作系统/并发相关/同步原语#信号量|同步原语#信号量]] - 接口参见:[[02-开发笔记/05-操作系统/并发相关/POSIX 线程&同步 API|POSIX 线程&同步 API]] --- <br><br> # 参考资料 # Footnotes [^1]: 《Linux-UNIX 系统编程手册-下册》 P719 [^2]: 《UNIX 网络编程卷 2:进程间通信》 P4 [^3]: 《Linux-UNIX 系统编程手册-下册》 P726