11 KiB
11 KiB
实验04 Linux 进程间通信
实验目的
- 掌握管道(pipe)的使用方法
- 掌握 System V 消息队列(message queue)的创建和使用
- 掌握共享内存(shared memory)配合信号量的同步通信
- 了解命名管道(FIFO)在多进程通信中的应用
- 理解不同 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 向管道写入消息
- 子进程 2 从管道读取消息并显示
- 父进程等待所有子进程结束
关键代码提示
#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]写端- 不使用的端口必须关闭,否则可能导致读端永远阻塞
- 管道是半双工的,数据单向流动
- 管道缓冲区大小通常为 64KB(Linux)
常见问题
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 读端永远阻塞 | 写端未关闭 | 父进程中关闭两端,子进程中关闭不用的端 |
| 数据不完整 | read 可能短读 |
循环读取直到满足期望字节数 |
| 管道破裂 SIGPIPE | 读端已关闭但写端仍在写 | 检查 write 返回值,捕获 SIGPIPE |
任务二:task72s.c / task72c.c —— 消息队列客户端/服务器
任务要求
- 服务器(task72s.c):创建消息队列,等待接收客户端消息,处理后回复
- 客户端(task72c.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:
#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:
#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,接收方依次读取并显示。
关键代码提示
#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:
#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:
#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 是字节流,无消息边界 | 使用定长结构体或长度前缀协议 |
实验总结
通过本实验,应掌握以下能力:
- 使用匿名管道实现父子进程间通信
- 使用消息队列实现任意进程间的消息传递
- 使用共享内存实现高速数据传输,配合信号量同步
- 使用命名管道实现无亲缘关系进程间的通信
- 理解不同 IPC 机制的优缺点和适用场景