# 实验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 #include #include #include #include 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):向消息队列发送请求消息,等待服务器回复 ### 关键代码提示 **公共头文件定义:** ```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 #include #include #include #include #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 #include #include #include #include #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 ` 手动删除 | --- ## 任务三:task73.c —— 共享内存 + IPC 信号量同步 ### 任务要求 使用共享内存实现两个进程间的数据传输,配合 System V 信号量实现同步。发送方写入 1~10,接收方依次读取并显示。 ### 关键代码提示 ```c #include #include #include #include #include #include #include #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 ` | | 删除信号量 | `ipcrm -s ` | ### 常见问题 | 问题 | 原因 | 解决方法 | |------|------|----------| | 共享内存未清理 | 程序异常退出 | 用 `ipcs` 查看,`ipcrm` 手动删除 | | 数据竞争 | 未使用信号量同步 | 确保发送方写完后才通知接收方读取 | | `shmat` 返回 -1 | 权限不足或 key 冲突 | 检查权限,换用不同 key | --- ## 任务四:task74s.c / task74c.c —— 多进程并发服务器(选做) ### 任务要求 使用命名管道(FIFO)实现一个多进程并发服务器: - 服务器创建一个公共 FIFO 接收客户端请求 - 每个客户端创建自己的私有 FIFO 用于接收回复 - 服务器为每个请求 fork 一个子进程处理 ### 关键代码提示 **服务器 task74s.c:** ```c #include #include #include #include #include #include #include #include #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 #include #include #include #include #include #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 机制的优缺点和适用场景