407 lines
8.3 KiB
Markdown
407 lines
8.3 KiB
Markdown
|
|
# 第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)
|