【Linux系统】进程间通信:匿名管道

发布于:2025-08-04 ⋅ 阅读:(15) ⋅ 点赞:(0)

1. 进程间通信介绍

1.1 什么是进程间通信

进程间通信(Inter-Process Communication, IPC)是指不同进程之间进行数据交换或信息共享的机制。由于每个进程拥有独立的地址空间和资源(如内存、文件描述符),操作系统通过内核提供的特殊方法实现进程间的数据传递。

  • 核心原因:进程的独立性导致其无法直接访问彼此的数据(例如,一个进程的全局变量对另一个进程不可见)。
  • 实现原理:内核作为中介,开辟一块缓冲区(如管道、共享内存),进程将数据从用户空间拷贝到内核缓冲区,再由目标进程读取。
  • 本质:让不同进程通过操作系统访问同一份资源(特定形式的内存空间)。

1.2 为什么要进程间通信

IPC的主要目的是解决进程隔离带来的协作障碍,具体需求包括:

  1. 数据传输:一个进程需将数据发送给另一个进程(如子进程向父进程返回计算结果)。
  2. 资源共享:多个进程需共享资源(如内存、文件、设备),避免重复占用。
  3. 事件通知:进程需通知其他进程特定事件(如进程终止时告知父进程)。
  4. 进程协同:实现进程间的同步、互斥或协同操作(如调试进程控制目标进程)。
  5. 提高效率:将任务分解为多个并发进程,提升系统整体性能。

1.3 怎么进程间通信

IPC的实现方式主要分为四类(具体技术可能重叠):

(1)管道(Pipe)

  • 匿名管道:仅限血缘关系进程(如父子进程),单向通信,通过pipe()系统调用创建内核缓冲区。
    • 特点:半双工、容量固定(64KB)、生命周期随进程。
  • 命名管道(FIFO) :通过文件系统路径标识,支持无血缘关系进程通信。

(2)System V IPC

  • 共享内存:多个进程直接访问同一块物理内存,速度最快但需同步机制(如信号量)避免冲突。
  • 消息队列:内核维护的消息链表,支持异步通信并保留消息边界。
  • 信号量:用于进程同步(如资源互斥访问)。

(3)POSIX IPC

  • 标准化IPC机制(如POSIX消息队列、信号量),设计更简洁,支持跨主机通信。

(4)其他方式

  • 信号(Signal) :内核向进程发送事件通知(如SIGINT终止进程),开销最小但不适合大数据传输。
  • 套接字(Socket) :支持网络通信(TCP/UDP)和本地通信(Unix域套接字)。
  • 文件/内存映射:通过文件或内存映射区域间接交换数据。

关键原理:所有IPC均依赖内核管理的共享资源(如缓冲区、内存区域),进程通过系统调用访问这些资源。


1.4 进程间通信的历史发展

  1. 早期阶段(1960s–1970s)

    • 管道作为最古老的IPC出现在Unix中,通过|符号实现命令间数据流传递。
    • 信号用于简单事件通知(如进程终止)。
  2. System V IPC(1980s)

    • AT&T在Unix System V中引入共享内存、消息队列和信号量,成为IPC标准。
  3. 标准化(1990s–2000s)

    • POSIX IPC提供跨平台兼容性,支持更灵活的通信模型。
    • 套接字因互联网普及成为跨主机通信主流。
  4. 现代发展(2010s至今)

    • 高级框架:如D-Bus(桌面通信)、gRPC(远程调用)、MQTT(物联网)简化复杂通信。
    • 微内核优化:IPC成为微内核(如QNX)的核心机制,通过消息传递替代系统调用。

驱动因素:多任务需求、分布式计算兴起及操作系统架构演进。


总结

进程间通信是打破进程隔离、实现协作的关键机制,其发展从基础管道扩展至多样化技术,核心始终围绕内核中介的共享资源访问。不同场景需权衡性能、复杂度与通信需求选择合适方式(如管道适合简单血缘进程通信,共享内存适合高性能数据共享)。


1.5 问题拓展

进程间通信是通过操作系统提供的特殊方法实现进程间的数据传递。

可是,在之前的学习中我们知道进程PCB中的有一个 *file的结构体指针指向file_struct结构体,file_struct结构体中存储的核心成员为 fd_array[](文件指针数组),数组元素指向file结构体,每个file结构体中都包含缓冲区,详细请看专栏【Linux系统】中的【基础IO下】

如下图:

既然这样,那为什么不能用file结构体中的内核缓冲区,通过系统调用read/write以不同的读写方式打开来进行进程间通信呢?

一、设计目标冲突:文件缓冲区与通信缓冲区的本质差异

  1. 数据持久性要求不同

    • 普通文件的内核缓冲区(如 struct file 中的缓冲区)必须将数据刷新到磁盘以实现持久化,而进程间通信(IPC)要求数据 " 仅存在于内存 ",且通信完成后立即失效。若强制普通文件缓冲区用于IPC,会导致
      • 不必要的数据落盘操作,违反通信的临时性需求;
      • 磁盘I/O延迟破坏通信实时性
    • 管道的缓冲区被设计为“纯内存文件”,数据永不写入磁盘,从根源上规避此问题
  2. 打开方式限制

    • 普通文件若分别以只读只写方式打开,会因缺少配对操作端而阻塞:
      • 只读打开会阻塞直到有进程以写方式打开同一文件
      • 只写打开同理
    • 管道在创建时即同时以读写方式打开同一文件,确保两端进程可即时通信

二、性能损耗:内核缓冲区的双重拷贝问题

  1. 普通文件的读写流程

    • write() 仅将数据从用户空间拷贝到内核缓冲区,不直接落盘
    • 后续由内核异步将缓冲区数据刷新到磁盘
    • read() 时若数据在内核缓冲区,则直接读取;否则触发磁盘I/O
    • 问题:通信需两次拷贝(用户→内核→用户),且磁盘I/O引入毫秒级延迟
  2. 管道的优化设计

    • 数据仅在内核缓冲区中流动,无磁盘交互
    • 通过共享同一内存区域,发送方写缓冲区和接收方读缓冲区为同一物理内存,实现零拷贝

三、并发安全:缺乏进程间同步机制

  1. 普通文件缓冲区的竞争风险

    • 多个进程并发读写同一文件时,内核不自动提供互斥锁或同步机制
    • 可能导致数据覆盖或读取不完整(如进程A写入中途,进程B读到部分数据)
  2. 管道的进程同步保障

    • 通过阻塞式读写实现同步:
      • 管道满时写进程阻塞,直到读进程取走数据
      • 管道空时读进程阻塞,直到写进程写入数据
    • 内核自动管理缓冲区状态,避免并发冲突

四、生命周期管理:通信与文件解耦

  1. 文件缓冲区的残留风险

    • 普通文件关闭后,内核缓冲区数据可能残留并被后续进程读取,导致通信数据泄露;
    • 若文件未关闭但通信进程终止,缓冲区成为“僵尸资源”
  2. 管道的动态销毁机制

    • 匿名管道:随进程结束自动销毁缓冲区
    • 命名管道:所有进程关闭后内核立即回收资源
    • 读端关闭后继续写入会触发 SIGPIPE 信号终止写进程

五、操作系统的安全隔离原则

普通文件缓冲区允许任意进程通过路径访问,违背了IPC的最小权限原则

  • 管道通过继承文件描述符(匿名管道)或受限访问权限(命名管道)确保仅目标进程可访问缓冲区
  • 若直接使用普通文件,恶意进程可能通过路径劫持通信数据

结论:管道是专为IPC优化的内存对象

普通文件的 struct file 内核缓冲区与进程通信需求存在根本性冲突,主要体现在数据持久化、打开方式、性能、并发安全及生命周期管理上。管道通过以下设计实现高效安全的IPC:

  1. 纯内存存储:规避磁盘I/O
  2. 双向打开:即时激活读写端
  3. 阻塞同步:内核自动管理缓冲区状态
  4. 动态销毁:随进程结束回收资源

2. 管道

2.1 管道本质定义

管道是 Unix/Linux 系统中历史最悠久的进程间通信(IPC)机制,其核心设计为:

  • 内存级单向数据流:管道不涉及磁盘存储,数据存在于内核缓冲区(内存区域),仅通过读写操作在进程间传递 。
  • 单向通信信道:数据流向固定(如进程 A → 管道 → 进程 B),双向通信需创建两个独立管道 。
  • 进程连接桥梁:管道将一个进程的输出直接定向为另一进程的输入,形成"生产者-消费者"模型 。

 示例;

ltx@iv-ye1i2elts0wh2yp1ahah:~$ who | wc -l
5

示例解析 who | wc -l

  • who 进程的输出(登录用户列表)通过管道定向至 wc -l 进程的输入。
  • wc -l 统计接收到的行数并输出结果(如 5)。
  • 底层实现
    1. Shell调用 pipe() 创建管道;
    2. fork() 两次生成 who 和 wc 进程;
    3. 通过 dup2() 将 who 的输出重定向至管道写端,wc 的输入重定向至管道读端 。


2.2 管道原理

  1. 内核缓冲区结构

    • 环形队列(Ring Buffer) :管道本质是内核维护的固定容量循环队列(默认 4KB),采用先进先出(FIFO)策略管理数据流 。
    • 无磁盘交互:数据仅存在于内存缓冲区,进程终止后自动销毁,避免持久化开销 。
  2. 文件描述符抽象

    • 通过 pipe() 系统调用创建两个文件描述符:
      • fd[0]读端,从缓冲区取数据
      • fd[1]写端,向缓冲区写数据 
    • 管道被视为 伪文件(Pseudo-file) :支持标准文件 I/O 操作(read()/write()),但无实体磁盘文件 。
  3. 单向数据流

    • 数据严格从写端流向读端(半双工),双向通信需创建两个独立管道 。、

2.3 管道创建

1. 系统调用入口

  • 函数原型

    int pipe(int fd[2]);  // 成功返回0,失败返回-1  
    
  • 内核路径fs/pipe.c → do_pipe2() → __do_pipe_flags() 。

2. 关键步骤(Linux 5.10+)

  1. 分配 inode
    • 调用 get_pipe_inode() 在 pipefs 中分配新 inode,初始化环形缓冲区 。
  2. 创建文件结构
    • 为读端(f0)和写端(f1)分配 struct file 对象:

      f0 = alloc_file_pseudo(inode, pipe_mnt, "", O_RDONLY, &pipefifo_fops);  
      f1 = alloc_file_pseudo(inode, pipe_mnt, "", O_WRONLY, &pipefifo_fops);  
      
    • 绑定操作函数集 pipefifo_fops(含 read/write 方法) 。

  3. 分配文件描述符
    • 在当前进程的文件描述符表中,寻找两个空闲位置,存储 f0 和 f1 的指针 。
    • 将描述符值写入用户空间数组 fd[2]fd[0]=读端, fd[1]=写端) 。
  4. 初始化缓冲区
    • 设置环形队列头尾指针(head/tail),状态为  。

pipe2() 支持附加标志(如 O_NONBLOCK),扩展默认行为 。


2.4 匿名管道

一、本质定义与核心特性

1. 基本概念

  • 内存级通信机制:匿名管道是由内核管理的临时缓冲区(环形队列),仅存在于内存中,不涉及磁盘存储,进程终止后自动销毁 
  • 单向数据流:数据严格从 写端(fd[1])流向读端(fd[0] ,双向通信需创建两个独立管道 
  • 血缘进程限制:仅限父子进程或兄弟进程(通过 fork() 继承文件描述符)通信 

2. 关键特性

特性 说明
临时性 生命周期与进程绑定,进程退出后内核自动回收资源 。
资源轻量 无磁盘文件实体,仅占用内核内存(默认缓冲区 4KB) 
流式传输 数据为无格式字节流,需应用层定义消息边界(如分隔符)
阻塞同步 读空时阻塞等待数据,写满时阻塞等待空间 

二、实现原理与内核机制

1. 核心数据结构

struct pipe_buffer {  
    struct page *page;     // 内存页指针  
    unsigned int offset;   // 当前读写偏移  
    unsigned int len;      // 有效数据长度  
};  

struct pipe_inode_info {  
    struct pipe_buffer *bufs; // 环形缓冲区数组  
    unsigned int head;        // 写指针  
    unsigned int tail;        // 读指针  
    wait_queue_head_t rd_wait; // 读等待队列  
    wait_queue_head_t wr_wait; // 写等待队列  
};
  • 环形队列:内核维护固定容量的循环缓冲区(默认 4KB),通过 head 和 tail 指针管理读写位置 
  • 文件抽象:通过虚拟文件系统 pipefs 分配 inode,支持标准文件操作接口(read/write

2. 系统调用 pipe() 流程

  1. 缓冲区创建
    • 调用 pipe() 时,内核分配 pipe_inode_info 结构体,初始化环形队列和等待队列 
  2. 文件描述符绑定
    • 返回两个文件描述符:fd[0](读端)绑定至读操作函数集,fd[1](写端)绑定至写操作函数集 。
  3. 进程继承
    • fork() 后子进程复制父进程的文件描述符表,共享同一管道缓冲区 。

3. 同步与阻塞机制

  • 自旋锁保护:读写操作前获取自旋锁,防止并发冲突 。
  • 等待队列
    • 读空时,进程加入 rd_wait 队列休眠,写操作完成后唤醒;
    • 写满时,进程加入 wr_wait 队列休眠,读操作释放空间后唤醒 

三、代码示例

下面我们直接来一段代码示例,让父子进程通过匿名管道通信,以此加深我们对匿名管道的理解

首先创建管道,注意pipe的参数是输出型参数

#include <iostream>
#include <cstdio>
#include <unistd.h>

int main()
{
    // 1.创建管道
    int fds[2] = {0};
    int n = pipe(fds);
    if(n < 0)
    {
        std::cerr << "pipe error" << std::endl;
        return 1;
    }
    std::cout << "fds[0]:" << fds[0] << std::endl;
    std::cout << "fds[1]:" << fds[1] << std::endl;


    return 0;
}

运行测试一下:

ltx@iv-ye1i2elts0wh2yp1ahah:~/Linux_system/lesson10/TestPipe$ make
g++ -o testPipe testPipe.cc -std=c++11
ltx@iv-ye1i2elts0wh2yp1ahah:~/Linux_system/lesson10/TestPipe$ ./testPipe
fds[0]:3
fds[1]:4

接下来创建子进程,然后分别关闭父子进程的写端和读端,这里我们让父进程来读,子进程来写

    // 2. 创建子进程
    pid_t fd = fork();
    if(fd == 0)
    {
        // child
        // 3. 关闭不需要的读写端,形成通信信道
        // father->read, child->write
        close(fds[0]);
        ChildWrite(fds[1]);
        close(fds[1]);
    }

    // father
    // 3. 关闭不需要的读写端,形成通信信道
    // father->read, child->write
    close(fds[1]);
    FatherRead(fds[0]);
    waitpid(fd, nullptr, 0);
    close(fds[0]);

子进程写

void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while(true)
    {
        snprintf(buffer, sizeof(buffer), "I am child, pid:%d, cnt:%d", getpid(), cnt++);
        write(wfd, buffer, strlen(buffer));
        sleep(5);
    }
}

父进程读

void FatherRead(int rfd)
{
    char buffer[1024];
    while(true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "child say:" << buffer << std::endl;
        }
        else if(n == 0)
        {
            std::cout << "子进程退出" << std::endl;
            break;
        }
        else
        {
            break;
        }
    }
}

这里我们写一条消息就sleep5秒,子进程写得慢,父进程读得快会怎么样呢

  • 现象:父进程的 read() 会阻塞,直到管道中有数据可读 。
    • 当管道为空时,读端会阻塞等待数据。
  • 结果:父进程每次读取需等待子进程写入,输出频率与子进程写入频率一致(每5秒一次)。

那如果写得快读得慢呢,或者写端关了读端继续读,读端关了写端继续写会怎么样

1. 写快读慢(写入速度快于读取速度)

  • 行为表现
    当子进程(写端)持续快速写入,而父进程(读端)读取速度较慢时,管道缓冲区(默认64KB)会被写满。此时写端进入阻塞状态(write() 暂停),直到读端取走部分数据腾出缓冲区空间后才能继续写入 。

    • 示例
      若子进程每秒写入10KB数据,父进程每秒仅读取1KB,约6.4秒后管道写满,子进程的 write() 被阻塞;父进程每次读取后,子进程才能继续写入。
  • 底层机制
    管道本质是内核维护的环形队列。当 head - tail = buffer_size 时,写操作被阻塞;读操作使 tail 后移,唤醒阻塞的写端 。

  • 风险提示
    长期阻塞可能导致系统资源浪费或响应延迟,但不会丢失数据(因阻塞机制保证原子性)。


2. 写端关闭后读端继续读

  • 行为表现
    若子进程(写端)关闭管道(close(fds[1])),父进程(读端)的 read() 会返回 0,表示已读取到文件结束符(EOF)

  • 底层机制
    写端关闭后,管道的引用计数归零。读端读取完缓冲区剩余数据后,再次调用 read() 会立即返回0(而非阻塞),通知进程通信终止 。

  • 正确操作
    父进程检测到 n=0 后应退出循环并关闭读端,避免资源泄漏 。


3. 读端关闭后写端继续写

  • 行为表现
    若父进程(读端)关闭管道(close(fds[0])),子进程(写端)的 write() 会触发 SIGPIPE 信号(默认行为是终止进程)。

    • 示例
      父进程意外退出时,子进程下一次 write() 将收到信号13(SIGPIPE),进程被强制终止。
  • 底层机制
    操作系统为避免资源浪费,当检测到读端关闭时,会通过信号强制终止写端进程。若写端尝试写入已关闭的管道,首次可能收到 EPIPE 错误errno=32),再次写入则触发信号 。

补充:

读端不读且写端持续写

  • 行为:管道写满后写端永久阻塞,形成死锁。需外部干预(如杀死进程)解除 。

这里我们就不一一验证了,感兴趣可以自己下来验证

总结:管道的核心特性

场景 行为 解决方案
写快读慢 写端阻塞直到缓冲区有空位 优化读端速度或扩大缓冲区
读快写慢 读端阻塞直到有新数据 增加写端频率
写端关闭后读端读 read() 返回0 (EOF) 检测 n=0 并退出循环
读端关闭后写端写 触发 SIGPIPE 终止写进程 捕获信号或检查 EPIPE 错误
读端不读且写端持续写 死锁 避免循环依赖或设置超时机制

以上是站在文件描述符角度来理解管道,如下图所示,注意:代码示例和图中有一个不同,那就是代码示例是父进程读子进程写,图示则相反

我们还可以从内核角度来理解管道本质

2.5 内核角度理解

一、管道的内核本质:内存级环形缓冲区

  1. 核心数据结构(基于 pipe_inode_info
// 内核源码 fs/pipe.c
struct pipe_inode_info {
    struct pipe_buffer *bufs;    // 环形缓冲区数组(默认16个page,64KB)
    unsigned int head;            // 写指针(生产者位置)
    unsigned int tail;            // 读指针(消费者位置)
    wait_queue_head_t rd_wait;    // 读等待队列
    wait_queue_head_t wr_wait;    // 写等待队列
    unsigned int readers;        // 读端引用计数
    unsigned int writers;         // 写端引用计数
};
  • 环形队列:缓冲区以内存页(page)为单位组织,通过 head 和 tail 实现循环写入。
  • 无磁盘交互:数据仅存于内存,不刷新到磁盘(区别于普通文件)。
  1. 文件抽象层
    • 通过 pipefs 虚拟文件系统创建匿名文件:

      struct file *f = alloc_file_pseudo(inode, pipe_mnt, "", flags, &pipefifo_fops);
      
    • 返回两个文件描述符:

      • fd[0] 绑定读操作函数集:.read = pipe_read
      • fd[1] 绑定写操作函数集:.write = pipe_write

二、管道创建与通信的内核流程

1. 系统调用 pipe() 的完整流程

步骤 内核操作 用户态表现
1. 分配资源 调用 get_pipe_inode() 创建 pipe_inode_info int pipefd[2]
2. 绑定描述符 为读写端分别创建 file 结构体 返回 fd[0](读端)、fd[1](写端)
3. 血缘进程继承 fork() 时复制文件描述符表 子进程共享同一管道缓冲区

2. 数据读写内核路径

  • 写操作(pipe_write

    while (缓冲区满) {
        将当前进程加入 wr_wait 队列;
        设置进程状态为 TASK_INTERRUPTIBLE;
        调用 schedule() 让出CPU;
    }
    将数据拷贝到 bufs[head] 对应的内存页;
    head = (head + 1) & (bufs_mask);
    唤醒 rd_wait 队列中的进程;
    
  • 读操作(pipe_read

    while (缓冲区空) {
        加入 rd_wait 队列;
        TASK_INTERRUPTIBLE;
        schedule();
    }
    从 bufs[tail] 拷贝数据到用户空间;
    tail = (tail + 1) & (bufs_mask);
    唤醒 wr_wait 队列中的进程;
    

关键机制:自旋锁保护 head/tail 修改的原子性


三、四种通信场景的内核行为

1. 写得慢 & 读得快

  • 场景:父进程读循环无延迟,子进程每秒写1次(sleep(1)
  • 内核行为
    • 读进程在 rd_wait 队列休眠(阻塞)
    • 写操作唤醒读进程后立即返回
  • 性能影响:CPU空转少,但读进程频繁阻塞/唤醒增加上下文切换开销

2. 写得快 & 读得慢

  • 场景:子进程移除 sleep 高速写,父进程每5秒读1次

  • 内核行为

    阶段 写进程状态 缓冲区状态
    初始 运行态
    缓冲区满 加入 wr_wait 队列阻塞 100%占用
    读操作后 唤醒并继续写入 释放部分空间
  • 风险:频繁阻塞唤醒导致吞吐量下降,极端时触发进程挂起

3. 写端关闭后读端继续读

  • 内核行为
    1. 读操作发现 writers == 0
    2. 若缓冲区有数据:正常返回数据
    3. 若缓冲区空:返回 0(EOF)

4. 读端关闭后写端继续写

  • 内核行为
    1. 写操作检查 readers == 0
    2. 向写进程发送 SIGPIPE 信号(默认终止进程)
    3. write() 返回 -1,errno=EPIPE
  • 风险:子进程被强制终止,父进程 waitpid 收到信号

四、管道特性的内核实现原理

1. 血缘关系限制

  • 本质原因:管道依赖文件描述符继承
  • 内核机制
    • fork() 时复制父进程的 files_struct
    • 子进程通过相同的 file->f_inode 访问同一缓冲区

2. 原子性保证

  • 条件:单次写入 ≤ PIPE_BUF(默认4KB)

  • 内核实现

    mutex_lock(&pipe->mutex);  // 加互斥锁
    copy_page_from_iter(buf, offset, bytes, from); // 原子拷贝
    mutex_unlock(&pipe->mutex);
    

    超过 PIPE_BUF 时数据可能被拆分

3. 同步机制

  • 阻塞控制

    条件 读进程行为 写进程行为
    缓冲区空 加入 rd_wait 阻塞 立即返回
    缓冲区满 立即返回 加入 wr_wait 阻塞
  • 唤醒机制:通过内核调度器实现读写协同

管道的核心是内存中的环形缓冲区pipe_inode_info),通过文件抽象层等待队列同步机制实现进程间通信。


完整代码示例:

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>

void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while(true)
    {
        snprintf(buffer, sizeof(buffer), "I am child, pid:%d, cnt:%d", getpid(), cnt++);
        write(wfd, buffer, strlen(buffer));
        sleep(5);
    }
}

void FatherRead(int rfd)
{
    char buffer[1024];
    while(true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "child say:" << buffer << std::endl;
        }
        else if(n == 0)
        {
            std::cout << "子进程退出" << std::endl;
            break;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    // 1.创建管道
    int fds[2] = {0};
    int n = pipe(fds);
    if(n < 0)
    {
        std::cerr << "pipe error" << std::endl;
        return 1;
    }
    std::cout << "fds[0]:" << fds[0] << std::endl;
    std::cout << "fds[1]:" << fds[1] << std::endl;

    // 2. 创建子进程
    pid_t fd = fork();
    if(fd == 0)
    {
        // child
        // 3. 关闭不需要的读写端,形成通信信道
        // father->read, child->write
        close(fds[0]);
        ChildWrite(fds[1]);
        close(fds[1]);
    }

    // father
    // 3. 关闭不需要的读写端,形成通信信道
    // father->read, child->write
    close(fds[1]);
    FatherRead(fds[0]);
    waitpid(fd, nullptr, 0);
    close(fds[0]);

    return 0;
}


网站公告

今日签到

点亮在社区的每一天
去签到