# 第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 #include #include #include #include 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 形式运行\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 形式运行\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 \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 \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)