Files
2026-06-25 00:09:09 +08:00

7.0 KiB
Raw Permalink Blame History

实验十 文件通信——命名管道

一、实验内容

理解命名管道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 端实现

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 端实现

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 关键设计点

双管道实现双向通信:

  • faPeter 写Jack 读Peter → Jack
  • fbJack 写Peter 读Jack → Peter

读端返回0的处理

  • 当写端进程关闭管道时,读端 read() 返回0
  • 此时对端已退出,发送信号杀死本方父进程

\r 覆盖输出:

  • \r 将光标移到行首,覆盖之前的输出
  • 实现两人对话在同一行显示

3.4 编译与运行

# 编译
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 测试命令

# 编译
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

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三方聊天程序设计

// 需要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架构、守护进程通信等场景