# 实验十 文件通信——命名管道 ## 一、实验内容 理解命名管道(FIFO)与匿名管道差异,掌握 mkfifo/open/read/write,实现无亲缘进程通信。 设计双管道聊天程序,Peter 与 Jack 双向对话。 ## 二、实验设计 ### 2.1 命名管道 vs 匿名管道 | 特性 | 命名管道(FIFO) | 匿名管道 | |------|---------------|----------| | 创建方式 | mkfifo() | pipe() | | 存在方式 | 文件系统路径名,持久存在 | 仅限亲缘进程间 | | 通信双方 | 无需亲缘关系 | 必须有亲缘关系 | | 双向通信 | 需两个管道 | 一个管道双向(有限) | | 打开方式 | open() | pipe()返回的fd | ### 2.2 双向通信架构 ``` mkfifo("fa") mkfifo("fb") ↓ ↓ +-------fa-------+ +------fb------+ | | | | Peter (写fa) (读fb) Jack (写fb) (读fa) | | | | +-------fa------+ +------fb------+ ↑ ↑ read() read() ``` ### 2.3 函数设计 | 函数 | 功能 | |------|------| | `mkfifo()` | 创建命名管道 | | `open()` | 打开管道(阻塞) | | `read()` | 读取数据 | | `write()` | 写入数据 | | `fork()` | 创建读写进程 | ### 2.4 调用关系 **Peter (A端):** ``` main() ├── unlink("fa"), unlink("fb") 清理旧管道 ├── mkfifo("fa"), mkfifo("fb") 创建管道 ├── fork() │ ├── 子进程: open("fb", O_RDONLY) → 读Jack消息 │ └── 父进程: open("fa", O_WRONLY) → 写给Jack └── 循环读写 ``` **Jack (B端):** ``` main() ├── fork() │ ├── 子进程: open("fa", O_RDONLY) → 读Peter消息 │ └── 父进程: open("fb", O_WRONLY) → 写给Peter └── 循环读写 ``` ## 三、编码实现 ### 3.1 Peter 端实现 ```cpp int main() { unlink("fa"); unlink("fb"); mkfifo("fa", 0666); // 创建两个管道 mkfifo("fb", 0666); printf("I am Peter...\n"); if (fork() == 0) { // 子进程:读 Jack 的消息 int fd2 = open("fb", O_RDONLY); char buf[100]; while (1) { memset(buf, 0, 100); if (read(fd2, buf, 100) == 0) { kill(getppid(), 9); // 对方退出,杀死父进程 return 0; } printf("\rJack says:%s", buf); // 显示消息 fflush(stdout); } } else { // 父进程:写给 Jack int fd1 = open("fa", O_WRONLY); char buf[100]; while (1) { fgets(buf, sizeof(buf), stdin); write(fd1, buf, sizeof(buf)); } } return 0; } ``` ### 3.2 Jack 端实现 ```cpp int main() { printf("I am Jack...\n"); if (fork() == 0) { // 子进程:读 Peter 的消息 int fd1 = open("fa", O_RDONLY); char buf[100]; while (1) { memset(buf, 0, 100); if (read(fd1, buf, 100) == 0) { kill(getppid(), 9); // 对方退出,杀死父进程 return 0; } printf("\rPeter says:%s", buf); // 显示消息 fflush(stdout); } } else { // 父进程:写给 Peter int fd2 = open("fb", O_WRONLY); char buf[100]; while (1) { fgets(buf, sizeof(buf), stdin); write(fd2, buf, sizeof(buf)); } } return 0; } ``` ### 3.3 关键设计点 **双管道实现双向通信:** - fa:Peter 写,Jack 读(Peter → Jack) - fb:Jack 写,Peter 读(Jack → Peter) **读端返回0的处理:** - 当写端进程关闭管道时,读端 read() 返回0 - 此时对端已退出,发送信号杀死本方父进程 **\r 覆盖输出:** - `\r` 将光标移到行首,覆盖之前的输出 - 实现两人对话在同一行显示 ### 3.4 编译与运行 ```bash # 编译 g++ exp10_peter.cpp -o peter g++ exp10_jack.cpp -o jack # 终端1:运行 Peter(先运行,因为是创建管道的一方) ./peter I am Peter... # 终端2:运行 Jack(后运行) ./jack I am Jack... ``` **运行示例:** ``` 终端1 (Peter): Hello Jack! Jack says: Hi Peter 终端2 (Jack): I am Jack... Hi Peter Peter says: Hello Jack! ``` ## 四、实验结果 ### 4.0 测试命令 ```bash # 编译 g++ exp10_peter.cpp -o peter g++ exp10_jack.cpp -o jack # 终端1:先运行 Peter(创建管道) ./peter # 终端2:后运行 Jack ./jack # 双方开始聊天,输入内容后回车发送 # 按 Ctrl+C 停止 # 清理管道文件 rm -f fa fb ``` ### 4.1 正常运行 ``` 终端1 (Peter): $ ./peter I am Peter... Hello Jack says: Hi Peter 终端2 (Jack): $ ./jack I am Jack... Hi Peter Peter says: Hello ``` **说明:** - Peter 先运行,创建管道 fa 和 fb - Jack 后运行,打开已存在的管道 - 双向聊天正常进行 ### 4.2 管道生命周期 ``` 1. Peter 创建 fa, fb 2. Peter 子进程打开 fb (读) 3. Peter 父进程打开 fa (写) 4. Jack 子进程打开 fa (读) 5. Jack 父进程打开 fb (写) 6. 双方交换消息 7. 一方退出时,另一方收到0字节,退出 ``` ### 4.3 阻塞特性 - `open(pipe, O_RDONLY)` 在写端未打开时阻塞 - `open(pipe, O_WRONLY)` 在读端未打开时阻塞 - 因此 Peter 先创建管道,Jack 再连接 ## 五、实验结果思考与体会 ### 5.1 命名管道应用场景 | 场景 | 说明 | |------|------| | 客户端/服务器 | 客户端连接命名管道与服务器通信 | | 进程池 | 工作进程通过命名管道接收任务 | | 日志系统 | 多个进程写入同一日志管道 | ### 5.2 思考问题解答 **问题1:单进程实现双向通信** 单进程双向通信需要非阻塞I/O或select/poll: ```cpp int fd1 = open("fa", O_RDONLY | O_NONBLOCK); int fd2 = open("fb", O_WRONLY | O_NONBLOCK); while (1) { FD_SET(fd1, &rfds); select(maxfd+1, &rfds, NULL, NULL, NULL); if (FD_ISSET(fd1, &rfds)) read(fd1, buf, 100); } ``` **问题2:匿名管道能否实现** 匿名管道只能用于有亲缘关系的进程,无法实现无亲缘进程的双向通信。 **问题3:三方聊天程序设计** ```cpp // 需要3个管道 mkfifo("p1_to_p2", 0666); mkfifo("p2_to_p3", 0666); mkfifo("p3_to_p1", 0666); // 每个进程读自己的管道,写下一个进程的管道 // 环形拓扑 ``` ### 5.3 实验体会 1. **命名管道本质**:是文件系统中的一种特殊文件,持久存在,可被任意进程打开 2. **与匿名管道的区别**: - 匿名管道通过 pipe() 创建,用于亲缘进程 - 命名管道通过 mkfifo() 创建,可用路径名访问 3. **双向通信设计**:需要两个管道实现双向,一个管道只能单向传输 4. **阻塞特性理解**: - open(O_RDONLY) 在无写者时阻塞 - open(O_WRONLY) 在无读者时阻塞 - 这是为什么必须先运行 Peter 再运行 Jack 5. **读端返回0的意义**:表示写端已关闭,通信结束 6. **实际应用**:命名管道常用于C/S架构、守护进程通信等场景