Files
obsidian/操作系统/08_进程间通信/08_进程间通信.md

407 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第08讲进程间通信
> 🎯 **本节目标**:掌握管道、消息队列、共享内存等进程间通信方式
## 📋 前置知识
- [[06_进程控制]] — 进程的基本概念
- [[06_进程控制_深入]] — fork 和 exec 的工作原理
---
## 🤔 为什么需要这个?
进程之间是相互隔离的,每个进程有自己的地址空间。但有时候进程需要协作:
- Shell 需要将 `ls` 的输出传给 `grep`
- 浏览器需要与下载管理器通信
- 数据库需要与应用程序交互
**进程间通信IPC** 就是解决这个问题的。
**生活比喻**
- **管道** = 对讲机(单向通信)
- **消息队列** = 邮箱(异步通信)
- **共享内存** = 共享白板(最快的通信方式)
---
## 📖 核心概念
### 1. IPC 方式概览
```mermaid
graph TD
A[进程间通信 IPC] --> B[管道 Pipe]
A --> C[消息队列 Message Queue]
A --> D[共享内存 Shared Memory]
A --> E[信号 Signal]
A --> F[信号量 Semaphore]
A --> G[套接字 Socket]
B --> B1[单向通信]
C --> C1[异步通信]
D --> D1[最快]
E --> E1[异步通知]
F --> F1[同步控制]
G --> G1[网络通信]
style A fill:#e1f5fe
style D fill:#e8f5e9
```
**对比**
| 方式 | 优点 | 缺点 | 适用场景 |
|------|------|------|----------|
| 管道 | 简单、易用 | 单向、只能父子进程 | Shell 命令组合 |
| 消息队列 | 异步、可按类型读取 | 有大小限制 | 任务分发 |
| 共享内存 | 最快 | 需要同步机制 | 大量数据交换 |
| 信号 | 异步通知 | 只能传递信号编号 | 事件通知 |
| 信号量 | 同步控制 | 不能传递数据 | 互斥、同步 |
| 套接字 | 跨网络 | 开销大 | 网络通信 |
### 2. 管道Pipe
管道是最古老的 IPC 方式,用于有亲缘关系的进程之间:
```mermaid
graph LR
A[写端 fd[1]] -->|数据流| B[读端 fd[0]]
style A fill:#e8f5e9
style B fill:#e1f5fe
```
**特点**
- **单向**:只能从一端写,另一端读
- **有亲缘关系**:通常用于父子进程
- **自带同步**:读端空时阻塞,写端满时阻塞
### 3. 命名管道FIFO
命名管道让没有亲缘关系的进程也能通信:
```mermaid
graph LR
A[进程1] -->|写入| B[/tmp/my_fifo]
B -->|读取| C[进程2]
style B fill:#fff3e0
```
**特点**
- 有文件名,存在于文件系统中
- 任意进程都可以打开
- 使用方法与普通文件相同
### 4. 消息队列
消息队列是一种异步通信方式:
```mermaid
graph LR
A[发送方 msgsnd] -->|消息| B[消息队列]
B -->|消息| C[接收方 msgrcv]
style B fill:#fff3e0
```
**消息结构**
```c
struct msgbuf {
long mtype; // 消息类型
char mtext[512]; // 消息内容
};
```
**优势**
- 可以按类型读取消息
- 异步通信,不需要同步
- 可以设置优先级
### 5. 共享内存
共享内存是最快的 IPC 方式:
```mermaid
graph TD
A[进程1] -->|读写| B[共享内存区域]
C[进程2] -->|读写| B
style B fill:#e8f5e9
```
**工作流程**
1. 创建共享内存段
2. 将共享内存映射到进程地址空间
3. 直接读写共享内存
4. 使用完毕后分离
**注意**:共享内存本身不提供同步机制,需要配合信号量使用。
---
## 💻 动手实践
### 示例1使用管道通信
```c
// pipe1.c - 管道通信示例
#include "wrapper.h"
int main() {
int count;
int fds[2]; // fds[0]=读端, fds[1]=写端
const char some_data[] = "1234567890";
char buffer[BUFSIZ + 1];
memset(buffer, '\0', sizeof(buffer));
// 创建管道
pipe(fds);
// 向管道写入数据
count = Write(fds[1], (void *)some_data, strlen(some_data));
printf("Wrote %d bytes\n", count);
// 从管道读取数据
count = Read(fds[0], (void *)buffer, BUFSIZ);
printf("Read %d bytes: %s\n", count, buffer);
exit(EXIT_SUCCESS);
}
```
**编译运行**
```bash
gcc -o pipe1 pipe1.c -L. -lwrapper
./pipe1
```
**预期输出**
```
Wrote 10 bytes
Read 10 bytes: 1234567890
```
### 示例2创建命名管道
```c
// fifo1.c - 创建命名管道
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
int res = mkfifo("/tmp/my_fifo", 0777);
if (res == 0)
printf("FIFO created\n");
exit(EXIT_SUCCESS);
}
```
**编译运行**
```bash
gcc -o fifo1 fifo1.c
./fifo1
ls -l /tmp/my_fifo
```
**使用命名管道**
```bash
# 终端1写入数据
echo "Hello FIFO" > /tmp/my_fifo
# 终端2读取数据
cat /tmp/my_fifo
```
### 示例3共享内存通信
```c
// shmwrite.c - 写入共享内存
#include "wrapper.h"
int main(int argc, char *argv[]) {
int shmid;
key_t key;
void *shmptr;
if (argc <= 1) {
fprintf(stderr, "请以 ./shmwrite <key> <message> 形式运行\n");
exit(2);
}
// 将参数转换成十六进制数作为 key
sscanf(argv[1], "%x", &key);
// 创建共享内存
shmid = Shmget(key, 4096, IPC_CREAT | 0644);
// 将共享内存映射到进程地址空间
shmptr = Shmat(shmid, 0, 0);
// 写入数据
memcpy(shmptr, argv[2], strlen(argv[2]) + 1);
// 分离共享内存
Shmdt(shmptr);
exit(0);
}
```
```c
// shmread.c - 读取共享内存
#include "wrapper.h"
int main(int argc, char *argv[]) {
int shmid;
key_t key;
void *shmptr;
if (argc <= 1) {
fprintf(stderr, "请以 ./shmread <key> 形式运行\n");
exit(2);
}
sscanf(argv[1], "%x", &key);
// 获取已存在的共享内存
shmid = Shmget(key, 4096, IPC_CREAT | 0644);
// 映射共享内存
shmptr = Shmat(shmid, 0, 0);
// 读取数据
printf("%s\n", (char *)shmptr);
// 分离共享内存
Shmdt(shmptr);
exit(0);
}
```
**编译运行**
```bash
gcc -o shmwrite shmwrite.c -L. -lwrapper
gcc -o shmread shmread.c -L. -lwrapper
# 写入数据
./shmwrite 0x12345678 "Hello Shared Memory!"
# 读取数据
./shmread 0x12345678
```
**预期输出**
```
Hello Shared Memory!
```
### 示例4消息队列通信
```c
// msgsnd1.c - 发送消息
#include "wrapper.h"
typedef struct MESSAGE {
int mtype;
char mtext[512];
} mymsg, *pmymsg;
int main(int argc, char *argv[]) {
int msqid;
key_t key;
mymsg msginfo;
if (argc != 3) {
fprintf(stderr, "使用方法: msgsnd1 <key> <message>\n");
exit(2);
}
sscanf(argv[1], "%x", &key);
// 获取消息队列
msqid = Msgget(key, 0644);
// 设置消息类型和内容
msginfo.mtype = 1;
memcpy(&msginfo.mtext, argv[2], strlen(argv[2]) + 1);
// 发送消息
Msgsnd(msqid, (pmymsg)&msginfo, strlen(msginfo.mtext) + 1, 0);
printf("you send a message \"%s\" to msq %d\n", argv[1], msqid);
return 0;
}
```
```c
// msgrcv1.c - 接收消息
#include "wrapper.h"
typedef struct MESSAGE {
int mtype;
char mtext[512];
} mymsg, *pmymsg;
int main(int argc, char *argv[]) {
int msqid;
key_t key;
mymsg msginfo;
if (argc != 2) {
fprintf(stderr, "使用方法: msgrcv1 <key>\n");
exit(2);
}
sscanf(argv[1], "%x", &key);
// 获取消息队列
msqid = Msgget(key, 0644);
// 接收消息类型为1
msgrcv(msqid, (pmymsg)&msginfo, 512, 1, 0);
printf("%s\n", msginfo.mtext);
return 0;
}
```
**编译运行**
```bash
gcc -o msgsnd1 msgsnd1.c -L. -lwrapper
gcc -o msgrcv1 msgrcv1.c -L. -lwrapper
# 发送消息
./msgsnd1 0x12345678 "Hello Message Queue!"
# 接收消息
./msgrcv1 0x12345678
```
**预期输出**
```
Hello Message Queue!
```
---
## 🔗 知识关联
- 管道在 Shell 中广泛使用,详见 [[06_进程控制_深入]]
- 共享内存的同步需要信号量,详见 [[07_多线程编程]]
- 套接字是网络通信的基础,详见 [[09_网络编程]]
---
## 📝 思考题
1. **管道的局限性**:为什么管道只能用于有亲缘关系的进程?
2. **共享内存的速度优势**:为什么共享内存比管道快?
3. **消息队列 vs 管道**:在什么场景下消息队列比管道更合适?
---
## 📚 扩展阅读
- 《UNIX环境高级编程》第15章进程间通信
- 《深入理解计算机系统》第10章系统级I/O
- [Linux IPC 编程](https://www.tldp.org/LDP/tlk/ipc/ipc.html)