进程间通信(上):深入理解管道与进程池

引言

在操作系统的设计中,进程是资源分配的基本单位,其核心特征之一便是独立性。每个进程都拥有自己独立的地址空间,并被操作系统精心隔离------一个进程的崩溃通常不会波及整个系统,数据也天然受到保护。这种"独善其身"的设计,为系统的稳定性与安全性筑起了坚固的基石。

然而,这种独立性是一把双刃剑。它带来的副作用同样明显:一个进程无法轻易访问另一个进程持有的数据,如同一个个彼此隔绝的信息孤岛。当一项复杂任务需要多个进程分工协作时,这种隔离就成了天然的障碍。比如,一个数据采集进程如何将海量信息传递给分析处理进程?一个界面进程如何响应来自后台计算进程的完成通知?

正是这种"需要协作"与"天然隔离"之间的矛盾,催生了进程间通信这项关键技术。它要解决的核心问题,就是在不破坏安全隔离这一根本优势的前提下,在不同进程间开辟出高效、受控的数据通道。


1. 进程间通信介绍

IPC(Inter-Process Communication),即进程间通信,是指在不同进程之间传递数据、同步协作的一套机制。

由于每个进程都拥有独立的地址空间,无法直接访问彼此的内存,因此通信必须通过内核中转 来实现。这揭示了进程间通信的本质:让不同的进程看到同一份资源(内存),从而具备通信的条件 。这份共享资源由操作系统提供,并需要配套创建、使用、销毁等管理能力。为此,操作系统必须提供一系列**系统调用(System Call)**来实现进程级的通信能力,这就要求设计出一套统一的通信接口。

有了接口,自然需要相应的实现方案,进而催生了各类通信标准。例如经典的 System V 标准,便由全球顶尖的技术专家共同制定,为进程间通信的规范化奠定了基础。

1-1 进程间通信的目的

进程间通信主要服务于以下几种场景:

  • 数据传输:一个进程需要将它的数据发送给另一个进程。

  • 资源共享:多个进程之间共享同一份资源。

  • 事件通知:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件(例如子进程终止时要通知父进程)。

  • 进程控制:某些进程希望完全控制另一个进程的执行(如调试器进程),此时控制进程希望能够拦截目标进程的所有陷入和异常,并及时感知其状态变化。

1-2 进程间通信的发展

进程间通信的演进脉络,大致可以划分为以下几个阶段:

  • 1970s:管道与信号的诞生,迈出了跨进程交互的第一步。

  • 1980s:System V IPC 三剑客(消息队列、共享内存、信号量)与 BSD Socket 相继出现,奠定了经典通信范式。

  • 1990s:POSIX 标准对 IPC 进行统一规范,提升了可移植性。

  • 2000s:Linux 融合各家之长,持续增强 IPC 能力。

  • 2010s 至今:通信机制逐渐框架化,并向分布式场景延伸。

1-3 进程间通信的分类

根据发展阶段与实现标准,进程间通信机制可分为以下几大类:

2. 管道

2.1 什么是管道

概念 :管道是一种特殊的通信机制,它将一个进程的标准输出直接连接到另一个进程的标准输入,形成一个单向的数据流通道。简单来说,一个进程将数据写入管道的写端 ,另一个进程从管道的读端读取数据。

bash 复制代码
who | wc -l

这里的 | 就是管道符号,who 的输出通过管道直接传给 wc -l 作为输入。

核心特性

  • 管道是一种特殊的文件类型 ,但它并不在磁盘上占用实际的物理空间。管道的数据存在于内核的内存缓冲区中,是内存级别的文件,数据交换直接在内存中进行,无需读写磁盘。

  • 基于文件的理念,让不同进程看到同一份资源------这就是管道的通信本质。

  • 管道隶属于文件系统,但存在于内存中,完美体现了"Linux 一切皆文件"的设计哲学。

管道是 Unix 中最古老的进程间通信形式。我们把从一个进程连接到另一个进程的数据流称为一个"管道"。

2.2 匿名管道

匿名管道是 Unix/Linux 系统中最古老、最基础的进程间通信方式之一。它本质上是一段由内核维护的内存缓冲区 ,以字节流的形式在两个进程之间建立单向数据传输通道。

既然是内存级别的文件,没有文件名,也不需要路径------这就是"匿名"的由来。通常用于具有亲缘关系 (父子进程)的进程间通信。

pipe() 函数

cpp 复制代码
#include <unistd.h>

int pipe(int fd[2]);
  • 功能:创建一个匿名管道。

  • 参数fd 是一个长度为 2 的整型数组。

    • fd[0] --- 管道的读端(记忆:0 像嘴巴,用来读)

    • fd[1] --- 管道的写端(记忆:1 像钢笔,用来写)

  • 返回值 :成功返回 0,失败返回 -1 并设置 errno

2.3 站在文件描述符角度------深度理解管道

2.3.1 文件描述符分配验证

cpp 复制代码
#include <unistd.h>
#include <iostream>

int main()
{
    int fds[2] = {0};
    int n = pipe(fds);
    if (n < 0)
    {
        perror("pipe fail");
        return 1;
    }
    std::cout << "fds[0](读端): " << fds[0] << std::endl;
    std::cout << "fds[1](写端): " << fds[1] << std::endl;
    return 0;
}

运行结果

cpp 复制代码
fds[0](读端): 3
fds[1](写端): 4

2.3.2 注意概念区分

数组下标 fd[0]fd[1] 中的 01,与标准输入输出的文件描述符编号 01,是两个不同层面的概念

由于标准输入(0)、标准输出(1)、标准错误(2)已被占用,pipe() 分配的文件描述符通常为 3 和 4

2.4 用 fork 共享管道

2.4.1 共享原理

父进程创建管道后,调用 fork() 创建子进程。由于子进程会继承父进程的文件描述符表,父子进程便能看到同一个管道,从而实现通信。

2.4.2 关键惯例:关闭不需要的一端

当父进程关闭读端 fd[0]、子进程关闭写端 fd[1] 后,管道中的数据流向就被明确为:父进程 → 管道 → 子进程,实现从父到子的单向通信。

这个操作带来两个好处:

  1. 明确通信方向:避免进程误用错误端口。

  2. 正确触发 EOF :当写端关闭后,读端才能收到 read() 返回 0,避免永久阻塞。

2.5 站在内核角度------管道本质

管道本质上就是内核中的一块缓冲区,以文件的形式呈现给用户态。看待管道,就如同看待文件一样------管道的使用和文件一致,完美体现了"Linux 一切皆文件"的设计思想。

2.6 管道通信实例

2.6.1 基础示例:从键盘读取,写入管道,读出并打印

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    int fds[2];
    char buf[100];
    int len;

    if (pipe(fds) == -1)
        perror("make pipe"), exit(1);

    // 从标准输入读取
    while (fgets(buf, 100, stdin))
    {
        len = strlen(buf);

        // 写入管道
        if (write(fds[1], buf, len) != len)
        {
            perror("write to pipe");
            break;
        }

        memset(buf, 0x00, sizeof(buf));

        // 从管道读取
        if ((len = read(fds[0], buf, 100)) == -1)
        {
            perror("read from pipe");
            break;
        }

        // 写入标准输出
        if (write(1, buf, len) != len)
        {
            perror("write to stdout");
            break;
        }
    }
    return 0;
}

2.6.2 父子进程通信示例

cpp 复制代码
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <iostream>
#include <cstring>

// 子进程:向管道写入数据
void ChildWrite(int wfd)
{
//char buff[1024] = {0} 是防御性编程的好习惯------确保缓冲区初始为全零,避免脏数据干扰。
    char buff[1024]={0};
    int cnt =0;
    while(true)
    {
        //注意几乎所有的对字符串操作的c接口都会自动在结尾放一个\0
        snprintf(buff,sizeof(buff),"I am child pid:%d\n cnt=%d",getpid(),cnt++);
        write(wfd,buff,strlen(buff));//默认加了\0
        sleep(1);
    }
}

// 父进程:从管道读取数据
void FatherRead(int rfd)
{
    char buff[1024]={0};
    while(true)
    {                   //读的时候要自己加\0
        ssize_t n=read(rfd,buff,sizeof(buff)-1);
        if(n>0)
        {
            buff[n]=0;
            std::cout<<"receive child messeage:"<<buff<<std::endl;
        }
    }
}

int main()
{
    // 1. 创建管道:fds[0] 为读端,fds[1] 为写端
    int fds[2] = {0};
    if (pipe(fds) < 0)
    {
        std::cerr << "pipe fail" << std::endl;
        return 1;
    }

    // 2. 创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程:关闭读端,只保留写端
        close(fds[0]);
        ChildWrite(fds[1]);
        close(fds[1]);
        return 0;
    }

    // 3. 父进程:关闭写端,形成"子写父读"的单向通道
    close(fds[1]);
    FatherRead(fds[0]);
    close(fds[0]);
    return 0;
}

进程状态监控

bash 复制代码
while :; do ps ajx | head -1 && ps ajx | grep testpipe | grep -v grep; sleep 1; done

运行结果

复制代码

父子进程均处于 S+(可中断睡眠)状态,由内核调度和管道的阻塞机制自然同步,无需额外的同步手段。

这部分涉及同步与多线程的相关知识,我们会在后续章节中详细展开。


2.6.3 创建进程池处理任务

进程池是一种预先创建多个子进程 的技术。父进程将任务通过管道分发给子进程处理,避免了频繁 fork 带来的开销,同时充分利用多核 CPU 的并行能力。

基本原理

进程池的核心思路巧妙利用了管道的阻塞特性:

  • 父进程没有发送任务 时,子进程 read() 阻塞等待(管道为空,写端未关闭)。

  • 父进程写入一条消息 ,对应子进程立即读到并执行一次任务

  • 执行完毕后子进程再次 read(),继续阻塞等待下一条命令。

这本质上就是父进程通过管道控制子进程的暂停与唤醒 ------管道不仅是数据通道,更是一种天然的进程同步机制

解决字节流问题:任务码

管道是面向字节流的,那如何让子进程知道"一次读多少、读到的是什么"?

答案很简单:规定父进程每次只写一个 int(4 字节),子进程每次只读一个 int

cpp 复制代码
// 父进程:固定写入 4 字节
write(pipefd, &cmd, sizeof(int));

// 子进程:固定读取 4 字节
read(rfd, &cmd, sizeof(int));

这个 int 被称为任务码,不同的数值代表不同的任务类型。由于读写双方约定好了固定长度,自然就绕过了粘包、半包等字节流带来的边界问题。

基本架构

工作流程

解决负载均衡问题

如果将所有的任务交给某一个或某几个子进程,就会出现忙的忙死,闲的闲死 的负载不均问题。因此,父进程在分配任务时需要将工作量均匀分摊到所有子进程。

常见的子进程选择策略有三种:

本节将采用轮询模式

实现代码

文件后缀说明

Task.hpp

cpp 复制代码
#pragma once
#include<iostream>
#include<vector>
#include<ctime>
typedef void (*func_t)();

///////////////////////debug//////////////////////////
void PrintLog()
{
    std::cout<<"我是一个打印日志的任务"<<std::endl;
}

void Download()
{
    std::cout<<"我是一个下载的任务"<<std::endl;
}

void Upload()
{
    std::cout<<"我是一个上传的任务"<<std::endl;
}
/////////////////////////////////////////////////////

class TaskManager
{
public:
    TaskManager()
    {
        srand(time(nullptr));
    }
    ~TaskManager(){}

    void Register(func_t t)
    {
        _tasks.push_back(t);
    }
    int Code()
    {
        return rand()%_tasks.size();
    }

    void Execute(int code)
    {
        if(code>=0&&code<_tasks.size())
        {
            _tasks[code]();//函数指针回调
        }
    }

private:
    std::vector<func_t> _tasks;
};

ProcessPool.hpp

cpp 复制代码
#pragma once
// #ifndef XXX_H
// #define XXX_H
// // 头文件内容
// #endif
//这两个写法等价
#include<unistd.h>
#include<cstdlib>
#include<vector>
#include<sys/wait.h>
#include<iostream>
#include<string>
#include"Task.hpp"

//先描述
class channel
{
public:
    channel(int wfd,pid_t id)
    :_wfd(wfd)
    ,_id(id)
    {
        _name= "channel:wfd=" + std::to_string(wfd) + " id=" + std::to_string(id);
    }
    ~channel() {}

    std::string GetChannelName()
    {
        return _name;
    }
    int GetChannel_wfd()
    {
        return _wfd;
    }
    pid_t GetChannel_id()
    {
        return _id;
    }
    void Send(int code)
    {
        int n= write(_wfd,&code,sizeof(code));//写入后子进程被唤醒
        if(n<0)
        {
            perror("write fail");
            return;
        }
        //(void)n;//编译不告警 
    }
private:
    int _wfd;
    pid_t _id;
    std::string _name;
    //int _loadnum;
    //累计所分配的任务数
};

//再组织
class channelManager
{
public:
    channelManager()
    :_next(0) 
    {}
    ~channelManager() {}

    void InsertChannel(int wfd,pid_t id)
    {//构建channel对象,并push到vector里面
     //也可以用emplace_back接口避免创建临时对象
        // channel c(wfd,id);
        //_channels.push_back(c);

        _channels.emplace_back(wfd,id);
    }

    channel& Select()
    {
        if(_channels.empty())
        {
            std::cerr << "错误:无可用子进程" << std::endl;
            exit(1);
        }
        // // 先确保 _next 在有效范围内
        // _next %= _channels.size();
        // auto& c = _channels[_next];
        // _next++;
        // return c;
        int idx = _next++ % _channels.size();
        return _channels[idx];
    }
    void PrintChannels()
    {
        for(auto& chs:_channels)
        {
            std::cout<<chs.GetChannelName()<<std::endl;
        }
        std::cout<<std::endl;
    }

    void CloseAll()
    {
        for(auto& c:_channels)
        {
            close(c.GetChannel_wfd());
        }
    }
    void StopAndWaitProcess()
    {
        //bug
        // for(int i=0; i<_channels.size(); i++)
        // {
        //     close(_channels[i].GetChannel_wfd());
        //     waitpid(_channels[i].GetChannel_id(), nullptr, 0);
        //     std::cout<<"关闭写端并回收子进程:"<<_channels[i].GetChannelName()<<std::endl;
        // }

        // //解决方案一:倒着关
        // for(int i=_channels.size()-1; i>=0; i--)
        // {
        //     close(_channels[i].GetChannel_wfd());
        //     waitpid(_channels[i].GetChannel_id(), nullptr, 0);
        //     std::cout<<"关闭写端并回收子进程:"<<_channels[i].GetChannelName()<<std::endl;
        // }

        // 解决方案二
        for(int i=0; i<_channels.size(); i++)
        {
            close(_channels[i].GetChannel_wfd());
            waitpid(_channels[i].GetChannel_id(), nullptr, 0);
            std::cout<<"关闭写端并回收子进程:"<<_channels[i].GetChannelName()<<std::endl;
        }
    }

private:
    std::vector<channel> _channels;
    int _next;
};

//默认信道数
const int default_num=5;
//进程池 
class processpool
{
public:
    ///processpool()=default;
    processpool(int num) :_process_num(num)
    {
        _tm.Register(PrintLog);
        _tm.Register(Download);
        _tm.Register(Upload);
    }   
    ~processpool() 
    {_cm.StopAndWaitProcess();}

    void work(int rfd)
    {
        while(true)
        {
            // debug
            // std::cout<<"我是子进程,我的rfd是:"<<rfd<<std::endl;
            // sleep(5);

            int code=0;
            ssize_t n = read(rfd,&code,sizeof(code));//读取4byte
            if(n>0)
            {   //不符合规范就跳出 
                if(sizeof(code)!=n)
                {
                    continue;
                }
                std::cout<<"子进程:"<<getpid()<<"收到一个任务码:"<<code<<std::endl;
                _tm.Execute(code);//执行任务
            }
            else if(n==0)//服务端将写端关闭
            {
                std::cout<<"子进程退出"<<std::endl;
                break;
            }
            else//<0 (-1)读失败
            {
                std::cerr<< "读取错误" << std::endl;
                break;
            }
        }
    }

    bool create()
    {
        //子进程会exit(0)不会干扰循环
        for(int i=0;i<_process_num;i++)
        {
            //创建管道以及子进程
            int fds[2]={0};
            int n=pipe(fds);
            if(n<0)
            {
                perror("pipe fail");
                return false;
            }
            pid_t id=fork();
            if(id<0)
            {
                perror("fork fail");
                return false;
            }
            else if(id==0)
            { 
                //解决方案二:让子进程知道自己只需要保留自己的读写端。因此在启动后,
                //立即遍历并关闭从父进程继承的其他所有写端。由于其他写端对应的进程是先创建的,称它们为"哥哥"进程。
                std::cout<<"#####################"<<"第"<<i<<"次"<<std::endl;
                _cm.PrintChannels();
                std::cout<<"====================="<<std::endl;
                _cm.CloseAll();
                //子进程读,关闭写端
                close(fds[1]);
                work(fds[0]);
                close(fds[0]);
                exit(0);
            }
            else
            {
                sleep(1);
                //父进程写,关闭读端
                close(fds[0]);
                //父进程知道自己的写端fds[1],和子进程是谁,pid,构建channel并管理
                _cm.InsertChannel(fds[1],id);
            }
        }
        return true;
    }

    void Debug()
    {
        _cm.PrintChannels();
    }

    void RunTask()
    {
        //先选择任务
        int task_code=_tm.Code();

        //再选择信道(子进程)
        auto& c=_cm.Select();
        std::cout<<"选择了一个子进程"<<c.GetChannelName()<<std::endl;

        //再发送任务
        c.Send(task_code);
        std::cout<<"发送了一个任务码"<<task_code<<std::endl;
    }
    void Stop()
    {
        //关闭所有的父进程写端,并且回收所有子进程
        _cm.StopAndWaitProcess();
    }
private:
    channelManager _cm;
    int _process_num;
    TaskManager _tm;
};

main.cc

cpp 复制代码
 #include"ProcessPool.hpp"

 int main()
 {
    //创建进程池对象
    processpool pp(default_num);
    //启动进程池
    pp.create();

    // pp.Debug();
    // sleep(1000);//主进程不退出

    //自动派发任务
    int cnt=5;
    while(cnt--)//主进程发送任务
    {
        pp.RunTask();
        sleep(1);
    }
    //pp.Stop();//交给RAII自动管理,回收
    return 0;
 }

debug结果:

  • 第一次debug
  • 第二次debug
  • 第三次debug
  • 关于兄弟进程的问题

假如此时总共有5个子进程,那么第一个子进程的写端就有5个指向(父进程和其它的子进程,自己的写端关闭),接下来也同理,第二个子进程写端有四个指向......当我们close时只将父进程的写端关闭,以第一个管道为例,那么管道文件的引用计数就从5->4,文件没有真正被关闭,那么read读的时候就一直阻塞着,这就是为什么卡在那里

下面给出两种方案解决:

**方案一:**由于计数是逐级递减的,最后一个进程的管道写端计数只有父进程,于是解方案一就是从最后一个进程开始关闭,也就是倒着关

方案二(推荐):每个子进程 fork 后立即关闭所有继承的兄弟写端

下面给出具有调试信息的运行结果方便理解:

最终运行结果:

架构总结

三层组件

核心设计要点

总结 :进程池是管道通信的完整工程实践 ------用 channel 封装单向信道,用 channelMannager 管理信道集合并实现轮询分发,用 TaskManager 解耦任务逻辑,用固定长度的任务码协议规避字节流边界问题,最终构建了一个父进程通过管道精准控制子进程暂停与唤醒的 Master-Worker 模式。


2.7 管道读写规则


2.8 验证管道通信的四种情况

前置概念:管道数据的消费特性

管道是流式消费 ------数据一旦被 read() 读出,就从内核缓冲区中永久移除,不需要也不存在手动清空操作。

这意味着管道的读写是一个动态平衡过程:写入端往缓冲区填充数据,读取端从缓冲区取走数据,取走的空间立刻可以被新数据覆盖。

四种场景详解

读正常 && 写满

当写入速度远大于读取速度时,内核缓冲区会被迅速填满。此后每次腾出少量空间,写入端都会立刻将其填满,直到再次阻塞。

PIPE_BUF = 4096 字节为例:

复制代码
初始:缓冲区满(4096/4096)→ write() 阻塞

父进程 read(buff, 1024) → 读出 1024 字节,缓冲区变为 3072/4096
  → write() 解除阻塞
  → 子进程立刻写入 1024 字节,缓冲区再次满(4096/4096)
  → write() 再次阻塞

... 循环往复:腾出一点 → 填入一点 → 再次阻塞 ...

管道就像一个固定大小的水桶------流出多少空间,就立刻能被填入多少数据,缓冲区始终逼近满水位。

写正常 && 读空

当读取速度大于写入速度时,管道数据一进入就被立刻读走,大部分时间处于空状态。这就是 2.6.2 节父子进程通信示例 所演示的场景:子进程每秒写入一次,父进程阻塞等待并立即读出。

补充:一次 read() 能读多少数据?

read() 每次读取的数据量,取决于以下两个值中的较小者

两种典型情况对比


2.9 实验验证:管道缓冲区大小与写满阻塞

验证代码

cpp 复制代码
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <iostream>

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

    // 2. 创建子进程
    pid_t id = fork();
    char ch = 0;
    int cnt = 1;

    if (id == 0)
    {
        // 子进程:只写不读
        close(fds[0]);
        while (true)
        {
            write(fds[1], &ch, 1);          // 每次写入 1 字节
            std::cout << "写入次数: " << cnt++ << std::endl;
        }
        close(fds[1]);
        return 0;
    }

    // 3. 父进程:关闭写端,但从不读取(验证写满阻塞)
    close(fds[1]);
    waitpid(id, nullptr, 0);
    close(fds[0]);
    return 0;
}

运行结果

cpp 复制代码
写入次数: 65534
写入次数: 65535
写入次数: 65536
^C                    ← 此时子进程阻塞,等待父进程读取

子进程在第 65536 次写入后阻塞。65536 字节 ÷ 1024 = 64 KB,恰好等于 Linux 管道的默认缓冲区容量。

系统参数验证

cpp 复制代码
$ ulimit -a | grep pipe
pipe size                (512 bytes, -p) 8     # 512 × 8 = 4096 字节

$ cat /proc/sys/fs/pipe-max-size
1048576                                        # 1MB,缓冲区最大上限

三个关键数值

三者关系

cpp 复制代码
pipe-max-size = 1MB        ← 硬上限(天花板)
    │
    └── 默认缓冲区 = 64KB   ← 实验写满值(可调,上限 1MB)
            │
            └── PIPE_BUF = 4KB  ← 原子写入保证线
                    │
                    └── 单次 write ≤ 4KB → 原子写入,多进程不穿插

什么是原子性?

原子性 = 不可分割。一个原子操作要么全部完成,要么完全不执行,不存在"做了一半"的中间状态。

在管道中的具体含义:

示例:两个进程同时向同一管道写入:

cpp 复制代码
进程A: write(fd, "AAAA", 4);     // ≤ PIPE_BUF,原子
进程B: write(fd, "BBBB", 4);     // ≤ PIPE_BUF,原子

管道中一定是 AAAABBBBBBBBAAAA,两个完整消息互不穿插。如果写入超过 4KB,则可能出现 AAAA...BBBB...AAAA... 的数据被撕裂现象。


2.10 管道特点总结

  1. 亲缘关系限制 :只能用于具有共同祖先的进程(通常由父进程创建管道后 fork,父子进程共用)。

  2. 自带同步机制:内核会对管道操作进行同步与互斥,保证读写的有序性。

  3. 面向字节流:数据以连续的字节序列传输,接收方每次可读取任意数量的字节。这一特性带来两个衍生问题:

    • 写入简单,读取困难:写入端知道要发什么,但读取端不知道数据全貌、消息边界在哪,需要自行处理粘包和半包问题。

    • 无消息边界:写入三次可能被一次读出,写入一次也可能被分三次读出。

  4. 生命周期随进程:一般而言,进程退出时管道自动释放。

  5. 半双工通信:数据只能单向流动。如需双向通信,必须创建两个管道。

半双工与全双工

匿名管道天生是半双工的。双向通信需创建两个管道:

cpp 复制代码
父进程 ←------ 管道1 ------ 子进程(子写父读)
父进程 ------ 管道2 ------→ 子进程(父写子读)

3.命名管道

3.1 为什么需要命名管道?

匿名管道的一个核心限制是:只能在具有共同祖先(亲缘关系)的进程间通信 。因为它没有名字,只能通过 fork 继承文件描述符来共享。

如果想让两个完全不相关的进程 交换数据,就需要用到命名管道

3.2 什么是命名管道?

命名管道,也称为 FIFO 文件,是一种特殊类型的文件。它与匿名管道的本质相同------都是内核中的一块内存缓冲区,数据在内存中直接传输。但关键区别在于:

命名管道有一个文件名,存在于文件系统中,作为不同进程访问同一个管道的"接头暗号"。

3.3 核心特性

  • 命名管道在文件系统上表现为一个特殊类型的文件,有对应的路径名作为标识符。

  • 不同进程只需打开同一个文件路径,就能访问同一个命名管道,实现跨进程通信。

  • 并不实际存储数据 。磁盘上的映像只是一个"路标",大小始终为 0。所有数据仍然在内核内存缓冲区中流动,不会刷新到磁盘。

3.4 创建命名管道:mkfifo()

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • 功能:创建一个命名管道(FIFO 文件)。

  • 参数

    • pathname:管道的路径名(路径 + 文件名),作为不同进程访问的标识。

    • mode:权限位,指定文件权限。实际创建权限会受 umask 影响(最终权限 = mode & ~umask)。

  • 返回值 :成功返回 0,失败返回 -1 并设置 errno

可以看到 命名管道是特殊文件类型 pipe,命名管道(FIFO 文件)

删除命名管道方法:

三种删除方式

相关推荐
2301_809049421 小时前
phase3_note_vivado_2020_ip_packager_revision
linux·fpga开发
老杨聊技术1 小时前
CentOS 7 安装 Docker 完整版教程
linux·docker·centos
l齐天1 小时前
Ubuntu 22.04 环境下 PBC 与 Golang 的安装、配置与测试
linux·ubuntu·golang
提伯斯6461 小时前
Linux minicom 串口工具超详细使用教程
linux·运维·服务器
艾莉丝努力练剑1 小时前
【Linux网络】网络层IP协议(一)
linux·运维·服务器·网络·tcp/ip·计算机网络·udp
Benszen1 小时前
Linux容器简介
linux·运维·服务器
剑神一笑1 小时前
Linux iptables 深度解析:从规则匹配到 NAT 转发实战
linux·运维·服务器
keyipatience1 小时前
23(半)24磁盘和EXT2文件系统
linux·运维
实心儿儿1 小时前
Linux —— 线程互斥和同步
linux