Files
obsidian/操作系统/实验/实验04_进程间通信.md

438 lines
11 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.
# 实验04 Linux 进程间通信
## 实验目的
1. 掌握管道pipe的使用方法
2. 掌握 System V 消息队列message queue的创建和使用
3. 掌握共享内存shared memory配合信号量的同步通信
4. 了解命名管道FIFO在多进程通信中的应用
5. 理解不同 IPC 机制的适用场景和性能特点
## 涉及知识点
- 匿名管道:`pipe`、父子进程间通信
- System V 消息队列:`msgget``msgsnd``msgrcv``msgctl`
- System V 共享内存:`shmget``shmat``shmdt``shmctl`
- System V 信号量:`semget``semop``semctl`
- 命名管道FIFO`mkfifo`
- IPC 键值:`ftok`
---
## 任务一task71.c —— 父子进程管道通信
### 任务要求
父进程创建 2 个子进程,通过管道实现父子进程间的通信:
1. 子进程 1 向管道写入消息
2. 子进程 2 从管道读取消息并显示
3. 父进程等待所有子进程结束
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int pipefd[2]; // pipefd[0]=读端, pipefd[1]=写端
pid_t pid1, pid2;
if (pipe(pipefd) < 0) {
perror("pipe");
exit(1);
}
// 子进程 1写端
pid1 = fork();
if (pid1 == 0) {
close(pipefd[0]); // 关闭读端
char *msg = "Hello from child 1!";
write(pipefd[1], msg, strlen(msg) + 1);
printf("子进程1(PID=%d): 发送消息 -> %s\n", getpid(), msg);
close(pipefd[1]);
exit(0);
}
// 子进程 2读端
pid2 = fork();
if (pid2 == 0) {
close(pipefd[1]); // 关闭写端
char buf[256];
int n = read(pipefd[0], buf, sizeof(buf));
printf("子进程2(PID=%d): 收到消息 -> %s (共%d字节)\n",
getpid(), buf, n);
close(pipefd[0]);
exit(0);
}
// 父进程:关闭两端,等待子进程
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
printf("父进程: 所有子进程已结束\n");
return 0;
}
```
### 关键要点
- `pipe()` 创建一对文件描述符:`pipefd[0]` 读端、`pipefd[1]` 写端
- 不使用的端口必须关闭,否则可能导致读端永远阻塞
- 管道是半双工的,数据单向流动
- 管道缓冲区大小通常为 64KBLinux
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 读端永远阻塞 | 写端未关闭 | 父进程中关闭两端,子进程中关闭不用的端 |
| 数据不完整 | `read` 可能短读 | 循环读取直到满足期望字节数 |
| 管道破裂 SIGPIPE | 读端已关闭但写端仍在写 | 检查 `write` 返回值,捕获 `SIGPIPE` |
---
## 任务二task72s.c / task72c.c —— 消息队列客户端/服务器
### 任务要求
- 服务器task72s.c创建消息队列等待接收客户端消息处理后回复
- 客户端task72c.c向消息队列发送请求消息等待服务器回复
### 关键代码提示
**公共头文件定义:**
```c
// msg_def.h
#define MSG_KEY 1234
#define MSG_TYPE_REQUEST 1
#define MSG_TYPE_REPLY 2
typedef struct {
long mtype; // 消息类型(必须 > 0
char mtext[256]; // 消息内容
} MsgBuf;
```
**服务器 task72s.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "msg_def.h"
int main() {
// 创建消息队列
int msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
if (msqid < 0) { perror("msgget"); exit(1); }
printf("服务器: 消息队列已创建 (ID=%d)\n", msqid);
MsgBuf msg;
while (1) {
// 掭收类型为 MSG_TYPE_REQUEST 的消息
if (msgrcv(msqid, &msg, sizeof(msg.mtext),
MSG_TYPE_REQUEST, 0) < 0) {
perror("msgrcv");
break;
}
printf("服务器: 收到请求 -> %s\n", msg.mtext);
// 处理请求(简单回显)
msg.mtype = MSG_TYPE_REPLY;
snprintf(msg.mtext, sizeof(msg.mtext),
"服务器回复: 已收到你的消息");
msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0);
}
// 删除消息队列
msgctl(msqid, IPC_RMID, NULL);
return 0;
}
```
**客户端 task72c.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "msg_def.h"
int main() {
int msqid = msgget(MSG_KEY, 0666);
if (msqid < 0) { perror("msgget"); exit(1); }
MsgBuf msg;
msg.mtype = MSG_TYPE_REQUEST;
printf("请输入消息: ");
fgets(msg.mtext, sizeof(msg.mtext), stdin);
// 发送请求
msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0);
// 接收回复
msgrcv(msqid, &msg, sizeof(msg.mtext), MSG_TYPE_REPLY, 0);
printf("客户端: %s\n", msg.mtext);
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| `msgget` 失败 | 消息队列不存在或权限不足 | 确保服务器先启动,检查权限 |
| `msgrcv` 阻塞 | 没有对应类型的消息 | 检查 `mtype` 是否匹配 |
| 消息队列残留 | 程序异常退出未删除 | 用 `ipcrm -q <msqid>` 手动删除 |
---
## 任务三task73.c —— 共享内存 + IPC 信号量同步
### 任务要求
使用共享内存实现两个进程间的数据传输,配合 System V 信号量实现同步。发送方写入 1~10接收方依次读取并显示。
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>
#define SHM_KEY 5678
#define SEM_KEY 9012
// 共享内存结构
typedef struct {
int data;
int done; // 发送方是否完成
} SharedData;
// semop 辅助函数
void sem_op(int semid, int semnum, int op) {
struct sembuf sb = {semnum, op, 0};
semop(semid, &sb, 1);
}
int main() {
// 创建共享内存
int shmid = shmget(SHM_KEY, sizeof(SharedData),
IPC_CREAT | 0666);
SharedData *shm = (SharedData *)shmat(shmid, NULL, 0);
// 创建信号量0=可用槽位, 1=已有数据
int semid = semget(SEM_KEY, 2, IPC_CREAT | 0666);
semctl(semid, 0, SETVAL, 1); // sem[0]=1: 可写
semctl(semid, 1, SETVAL, 0); // sem[1]=0: 无数据
pid_t pid = fork();
if (pid == 0) {
// 接收方
for (int i = 0; i < 10; i++) {
sem_op(semid, 1, -1); // 等待有数据
printf("接收: %d\n", shm->data);
sem_op(semid, 0, +1); // 释放可写
}
shmdt(shm);
exit(0);
} else {
// 发送方
for (int i = 1; i <= 10; i++) {
sem_op(semid, 0, -1); // 等待可写
shm->data = i;
printf("发送: %d\n", i);
sem_op(semid, 1, +1); // 通知有数据
}
wait(NULL);
// 清理
shmdt(shm);
shmctl(shmid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);
}
return 0;
}
```
### IPC 资源管理
| 操作 | 命令 |
|------|------|
| 查看共享内存 | `ipcs -m` |
| 查看信号量 | `ipcs -s` |
| 查看消息队列 | `ipcs -q` |
| 删除共享内存 | `ipcrm -m <shmid>` |
| 删除信号量 | `ipcrm -s <semid>` |
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 共享内存未清理 | 程序异常退出 | 用 `ipcs` 查看,`ipcrm` 手动删除 |
| 数据竞争 | 未使用信号量同步 | 确保发送方写完后才通知接收方读取 |
| `shmat` 返回 -1 | 权限不足或 key 冲突 | 检查权限,换用不同 key |
---
## 任务四task74s.c / task74c.c —— 多进程并发服务器(选做)
### 任务要求
使用命名管道FIFO实现一个多进程并发服务器
- 服务器创建一个公共 FIFO 接收客户端请求
- 每个客户端创建自己的私有 FIFO 用于接收回复
- 服务器为每个请求 fork 一个子进程处理
### 关键代码提示
**服务器 task74s.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <signal.h>
#define SERVER_FIFO "/tmp/server_fifo"
#define CLIENT_FIFO_TEMPLATE "/tmp/client_%d_fifo"
typedef struct {
pid_t client_pid;
char message[256];
} Request;
void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main() {
// 创建服务器 FIFO
unlink(SERVER_FIFO);
mkfifo(SERVER_FIFO, 0666);
signal(SIGCHLD, sigchld_handler);
printf("服务器启动,等待连接...\n");
int server_fd = open(SERVER_FIFO, O_RDONLY);
Request req;
while (1) {
int n = read(server_fd, &req, sizeof(Request));
if (n <= 0) continue;
pid_t pid = fork();
if (pid == 0) {
// 子进程处理请求
char client_fifo[256];
snprintf(client_fifo, sizeof(client_fifo),
CLIENT_FIFO_TEMPLATE, req.client_pid);
int client_fd = open(client_fifo, O_WRONLY);
char reply[256];
snprintf(reply, sizeof(reply),
"服务器已处理: %s", req.message);
write(client_fd, reply, strlen(reply) + 1);
close(client_fd);
exit(0);
}
}
close(server_fd);
unlink(SERVER_FIFO);
return 0;
}
```
**客户端 task74c.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#define SERVER_FIFO "/tmp/server_fifo"
#define CLIENT_FIFO_TEMPLATE "/tmp/client_%d_fifo"
typedef struct {
pid_t client_pid;
char message[256];
} Request;
int main() {
// 创建私有 FIFO
char client_fifo[256];
snprintf(client_fifo, sizeof(client_fifo),
CLIENT_FIFO_TEMPLATE, getpid());
unlink(client_fifo);
mkfifo(client_fifo, 0666);
// 发送请求
Request req;
req.client_pid = getpid();
printf("请输入消息: ");
fgets(req.message, sizeof(req.message), stdin);
int server_fd = open(SERVER_FIFO, O_WRONLY);
write(server_fd, &req, sizeof(Request));
close(server_fd);
// 接收回复
int client_fd = open(client_fifo, O_RDONLY);
char reply[256];
read(client_fd, reply, sizeof(reply));
printf("收到回复: %s\n", reply);
close(client_fd);
unlink(client_fifo);
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| FIFO 文件残留 | 程序异常退出 | 启动时 `unlink` 清理旧 FIFO |
| `open` 阻塞 | FIFO 另一端未打开 | 确保服务器先启动 |
| 多客户端并发时数据混乱 | FIFO 是字节流,无消息边界 | 使用定长结构体或长度前缀协议 |
---
## 实验总结
通过本实验,应掌握以下能力:
1. 使用匿名管道实现父子进程间通信
2. 使用消息队列实现任意进程间的消息传递
3. 使用共享内存实现高速数据传输,配合信号量同步
4. 使用命名管道实现无亲缘关系进程间的通信
5. 理解不同 IPC 机制的优缺点和适用场景