Linux中进程间通信 ---管道篇

1.进程为什么需要通信

进程之间需要通信,是因为每个进程都有独立的虚拟地址空间,彼此完全隔离,不能直接访问对方的数据 。为了让多个进程协同完成任务、交换数据、同步执行顺序,就必须通过内核提供的机制进行通信。比如父子进程分工处理数据、服务进程与客户端交互、多个程序共享资源、控制执行先后等,都需要靠进程间通信来实现信息传递与配合。

2.进程间如何通信

inux 进程间通信是让不同进程交换数据的机制,主要通过内核提供的公共介质实现。常用方式:

匿名管道(亲缘进程单向通信)、命名管道(无亲缘也可用)、消息队列、共享内存(效率最高,直接映射同一块物理内存)、信号量(同步互斥)、Socket(跨主机)、信号(简单通知)。多数方式借助内核缓冲区或公共资源完成数据传递,实现进程间协同与数据交互。

3.进程间通信的本质

先让进程看到同一份资源,提供公共资源的人只能是操作系统,公共资源如何提供呢,而这个提供方案只能是系统调用

4. 管道

4.1本质

管道通信的本质,是操作系统在内核中开辟一段内存缓冲区,以文件描述符形式暴露给进程,让多个进程通过读写操作完成数据传输,一个进程往文件里写,另一个进程从文件里面读。它是一种半双工、面向字节流的通信方式,数据只能单向流动,遵循先进先出规则,本质是借助内核实现的进程间数据传递机制。

子进程会复制父进程的 PCB(进程控制块)以及 mm_struct(内存描述符)等核心数据结构,但并不会复制物理内存中的文件内容,也不会重新打开文件 ------ 父子进程会共享相同的文件表项(包含文件偏移量、文件状态标志等),因此能看到同一份打开的文件资源;且父子进程持有的文件描述符指向内核中同一个文件结构体,对文件的读写操作会共享偏移量,需注意同步问题以避免数据错乱。

4.2匿名管道

匿名管道是 Linux 内核提供的、基于内存缓冲区实现的进程间通信机制 ,只适用于具有亲缘关系的进程(如父子、兄弟进程)。它在创建时会生成一对文件描述符,分别用于读和写,数据在内核中以字节流形式传输,遵循先进先出规则。匿名管道不产生磁盘文件,进程退出后资源自动释放,通信为半双工模式,数据只能单向流动。由于子进程会继承父进程打开的文件描述符,因此亲缘进程可通过它直接实现简单高效的数据交互。

①半双工和全双工

半双工通信指通信双方可以互相发送数据,但同一时刻只能有一方发送、另一方接收 ,不能双向同时传输,类似对讲机,一方说话时另一方只能收听。全双工通信则允许双方在同一时刻同时发送和接收数据,双向传输互不干扰,如同打电话,双方可以同时讲话与收听。在进程通信中,匿名管道、信号等属于半双工;而普通双向管道、socket 通信通常支持全双工,能更灵活地实现并发数据交互。

② 详细学习匿名管道

bash 复制代码
who | wc -l

统计当前的linux有多少个人在用;| 就会被OS认为是管道;who和wc -l 分别是两个进程,关系是兄弟;

struct_file是被子进程独立创建的,拷贝自父进程;父子进程都可以使用自己的struct_file 上的fd访问对应的内核级缓冲区;父子进程间通信不需要刷新到磁盘上,所以我们如何设计一个纯内存级别的和磁盘没有关系的、配有对应的系统调用接口,让它可以纯内存级的打开一个内存级文件,让它专门来做两个进程之间的通信,是的,这就是OS为我们设计的管道

问题:如果我们不关闭呢?

文件描述符泄露,或者误操作会影响我们的进程;

问题:为什么创建管道,再创建子进程

因为子进程要拷贝父进程的管道;

问题:为什么不只给父进程创建读端?

因为这样的话子进程继承的也是读,就没有人写了;

问题:为什么叫做管道?为什么要只有单向通信

之所以叫管道 ,是因为它像一根真实的管子,数据从一端流入、另一端流出,形象描述内核缓冲区的单向传输。设计为单向通信,是为了实现简单、高效、同步安全,避免双向读写冲突,内核只需维护单方向的字节流,符合进程间 "生产者 - 消费者" 的典型模型,也便于父子进程继承文件描述符后直接使用。

④验证接口

创建管道的接口

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

//father process read 
//child process write
int main()
{
    //1.创建管道
    int fds[2]={0};
    int n=pipe(fds);
    if(n != 0)
    {
        std::cerr<<"Create Pipe Error" <<std ::endl;
        return 1;
    }

    //2.创建进程
    pid_t id=fork();
    if(id == 0)
    {
        int cnt=0;
       while (true)
       {
          std::string message="hello ";
          message +=std::to_string(getpid());
          message +=",";
          message +=std::to_string(cnt);
          ::write(fds[1],message.c_str(),sizeof(message));
          sleep(1);
          cnt ++;
       }
       ::close(fds[0]);
       exit(0);

    }
    else if(id < 0)
    {
        std::cerr << "Fork Error" << std::endl;
        return 2;
    }
    else
    {
        
        while (true)
        {
            char buffer [1024];
            ssize_t n =::read(fds[0],buffer,1024);
            if( n>0)
            {
                buffer[n] =0;
                std ::cout << "child ->father" << buffer <<std::endl;
            }

        }
      
        ::close(fds[1]);
        pid_t rid =waitpid(id,nullptr,0);
        std::cout <<"father wait child success" <<rid <<std::endl;
    }
    return 0;
}

int pipefd[2],是一个输出型参数,未来我们就用这个参数进行我们管道的读写了;fd[0]读端,fd[1]写,0下标想象成嘴巴,用来读;1下标想象成笔,笔是用来写的;

代码结果为:

这样我们就创建了一个子进程进行写,父进程进行读的管道;我们发现如果子进程在写的时候5秒写一次,这个时间父进程在等待,也就是有数据读,没数据不读,这种也是一种保护;有没有可能我写一半的时候别人就来读呢?是有这样的可能的,取决于buffer的大小;也有可能写了很多内容才读一点;所以我们要对我们的数据缓冲区保护;

我们发现如果我们让父进程不读,只让子进程写,最后写了64KB就写不进去了,所以管道其实是有大小的,这样也是为了让我们的管道安全;并不是说子进程写多少父进程读多少,读写双方都不关心双发读写多少,所以称为这种情况为面向字节流;

如果子进程写入之后直接退了,也就是写端关闭,这个时候我们的父进程会读取完管道内剩余的内容,最后读到0,表示管道内的内容为空了;

写端一直写,但是读端已经关闭,这个时候在OS看来是一个浪费时间、浪费空间的事情,所以OS会直接关闭这个进程,以13号信号的方式;低7位为退出信号,次地7位位退出状态;所以想要得到信号让我们的status & 0x7F(01111111),按位与的结果是有0就是0,这样就能得到我们第七位的结果也就是我们的信号了;同理想要得到我们的退出状态,让status & 0xFF(1111 1111)

⑤匿名管道的特点

5.基于匿名管道的应用-进程池

5.1同步和互斥

同步 :指多个进程按约定的先后顺序执行,保证操作有序、结果正确,是 "谁先谁后" 的协调。

互斥 :指多个进程不能同时访问临界资源(如共享内存、文件),同一时间只允许一个进程使用,避免竞争冲突。同步保证秩序,互斥保证安全,二者共同让多进程协同稳定运行。

同步应用场景

  • 生产者 - 消费者模型:生产者生产数据后,消费者才能读取,保证执行顺序。
  • 父子进程协作:父进程先完成任务,子进程再基于结果运行,依赖先后次序。
  • 多步骤任务:如数据处理后再输出,按流程有序执行。

互斥应用场景

  • 共享资源访问:多个进程同时操作共享内存、文件,避免数据混乱。
  • 打印机使用:多进程打印时,同一时间仅一个进程占用设备。
  • 全局变量修改:多进程修改同一变量,防止竞争导致结果错误。

5.2进程池模型

5.3进程池的应用场景

应用场景 说明
高并发网络服务器 Web 服务器、网关、游戏服务器、TCP/UDP 服务,用进程池处理大量客户端连接
后台任务执行 日志处理、数据上报、异步计算、消息消费等不阻塞主线程的后台工作
批量数据处理 日志清洗、文件解析、数据格式转换、音视频转码、图片处理等批量任务
数据库 / IO 密集服务 大量数据库查询、文件读写、网络请求,用进程池并行提升吞吐量
计算密集型模块 算法计算、模型推理、数值计算、仿真计算,利用多核 CPU 并行加速
安全隔离任务 执行第三方代码、解析未知文件、调用外部插件,进程崩溃不影响主程序
定时任务调度 定时备份、定时统计、定时清理、定时同步等周期性任务
微服务 / 分布式节点 微服务中的任务 worker 节点,接收任务并稳定并发执行
软件类型 典型例子 是否用进程池
Web 服务器 Nginx、Apache、网关 ✅ 大量用
游戏服务器 登录服、战斗服、网关 ✅ 大量用
数据库 MySQL、PostgreSQL ✅ 大量用
中间件 消息队列、分布式存储 ✅ 大量用
后端服务 微服务、接口服务 ✅ 大量用
日志 / 数据系统 日志收集、清洗、分析 ✅ 大量用
普通手机 / 桌面 App 微信、抖音、浏览器、编辑器 不用

5.4Makefile 拓展

这样的makefile写好,无论新建多少.cc文件,都不再需要对应的makefile ;

5.5 进程池创建

processpool.cc

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <vector>
#include <cstdlib>
#include <functional>
#include "Task.hpp"

using work_t =std::function<void()>;

enum
{    
    OK = 0 ,
    UsageError,
    PipeError,
    ForkError
};
class channel
{
  public:
   channel(int wfd,pid_t who)
   :_wfd(wfd)
   ,_who(who)
   {
      _name="Channel" + std::to_string(wfd) +std::to_string(who);
   }
   void send(int cmd)
   {
      ::write(_wfd,&cmd,sizeof(cmd));
   }
   std::string Name( )
   {
      return _name; 
   }
   void close()
   {
      ::close(_wfd);
   }
   int ID()
   {
      return _who;
   }
    ~channel()
   {}
   private:
   int _wfd;
   std::string _name;
   pid_t _who;
};


void worker()
{
   while(true)
   {
     int cmd =0;
     int n= ::read(0,&cmd,sizeof(cmd));
     if(n==sizeof(cmd))
    {
        tm.Excute(cmd);
    }
      else if(n==0)
      {
          std::cout <<"pid"<<getpid() <<"quit ...." <<std::endl;
          break;
      }
      else if(n <0)
      {

      }

   } 
}


//channels 输出型参数
int InitProcessPool(const int & processnum, std::vector<channel> & channels ,work_t work)
{
      for(int i=0;i<processnum;i++)
    {
        //1.先有管道
        int fds[2]={0};
        int n = pipe(fds);
        if(n < 0) 
         return PipeError;
        //2,创建进程
        pid_t id = fork();
        if(id < 0) 
        return ForkError;
        if(id ==0)
        {
            //子进程
            ::close (fds[1]);
            //想让子进程从标准输入0读
            dup2(fds[0],0); //用于更改文件描述符,将fds[0]改为0;
            work();
            ::exit(0);    //让子进程把自己的工作做完直接退出,不要再走下面的代码
        }
            //父进程
        //pid_t rid=waitpid(id,nullptr,0);
        ::close (fds[0]);
        channels.emplace_back(fds[1],id);  //直接传构造     
    }
    return OK;
}

void Debug( std::vector<channel> & channels)
{
   for( auto &c :channels)
   {
      std::cout <<c.Name() << std::endl;
   }
}
 
void DispathchTask(std::vector<channel> channels)
{
    int nums=20;
    while(nums--)
    {
        //a.选择一个任务,整数
        int task= tm.SelectTask();
        int who =0;
        //b.选择一个管道channel
        channel & curr = channels[who++];
        who%= channels.size();
        //c.派发任务
        curr.send(task);
        std::cout << "任务还剩:"<< nums << std::endl;
        sleep(2);

    }
}

void ExitProcessPool(std::vector<channel> channels)
{
    for(auto & c:channels)
    {
        c.close();
        pid_t rid =waitpid(c.ID(),nullptr,0);
        if(rid >0)
        {
            std::cout <<"child" <<rid << "wait.....sucess"<<std::endl;
        }
    }
}
void Usage(std::string proc)
{
    std::cout << "Usage "<< proc << "process-num"<< std::endl;
}

int main(int argc ,char*argv[])
{ 
    if(argc != 2)
    {
        Usage(argv[0]);
        return UsageError;
    }
     int nums = std ::stoi(argv[1]);  //把一个char*转为整数  
    std ::vector<channel> channels;  //如果不保存起来一次循环后进程结束,管道会被释放
    //1.初始化进程池
    InitProcessPool(nums,channels,worker);
    // Debug(channels);
    //2.派发任务
    DispathchTask(channels);
    //3.退出进程池
    ExitProcessPool(channels); 
    return OK;
    
}

Task.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <unordered_map>
#include <functional>
#include <ctime>

using task_t = std::function<void()>;
static int number = 0;


void download()
{
   std::cout<< "Im download task" << std::endl;
}
void sql()
{
   std::cout<< "Im databases task" << std::endl;
}
void log()
{
   std::cout<< "Im log task" << std::endl;
}

class TaskManger
{
    public:
    TaskManger()
    {
       srand(time(nullptr));
       InsertTask(download) ;
       InsertTask(log) ;
       InsertTask(sql) ;
    }   
    void InsertTask(task_t t)
    {
        tasks[number++]=t;  
    }
    int SelectTask()
    {
        return rand() % number;
    }
    void Excute(int number)
    {
        if(tasks.find(number) ==tasks.end()) return;
        tasks[number]();
    }
    ~TaskManger()
    {

    }
  
private:
     std::unordered_map<int,task_t> tasks;    
};

TaskManger tm;

重点bug

6.命名管道

创建命名管道

生成了一个p开头的 fifo 命名管道的文件;

6.1什么是命名管道

命名管道(FIFO,First In First Out)是一种半双工的进程间通信机制 ,它在文件系统中以特殊文件形式存在,所以有文件名和inode ,,允许无亲缘关系的进程间进行数据交换。与匿名管道(pipe)不同,命名管道可被多个进程通过路径名访问,生命周期独立于创建进程,只要文件未删除就可长期使用。它遵循先进先出的读写规则,数据按写入顺序被读取,且默认以阻塞模式工作:读端会等待数据写入,写端会等待读端打开。命名管道常用于客户端 - 服务器架构,例如日志收集、命令行工具间的数据传递,既保留了管道的简单易用性,又突破了匿名管道仅能用于父子进程的限制,是跨进程通信的轻量方案。

进程间通信的本质是让不同的进程看到同一份文件,所以不同的进程通过命名管道的文件路径一个以写方式,一个以读方式就能看到这份资源;这份资源不需要每个进程都拷贝,只用加载到文件内核缓冲区就可以了;但是我们的管道文件和普通文件的区别是管道文件不刷新到磁盘,只在文件内核级缓冲区;

6.2命名管道和匿名管道的区别

对比维度 命名管道(FIFO) 匿名管道(pipe)
存在形式 特殊文件 形式存在于文件系统中 仅存在于进程内存中,无实体文件
访问方式 通过路径名访问,支持无亲缘关系进程 仅通过文件描述符传递,仅限亲缘进程(父子 / 兄弟)
生命周期 独立于创建进程,需手动删除才会消失 随创建进程结束而自动销毁
通信方向 半双工(单向),可双向需创建两个管道 半双工(单向),双向需创建两个管道
阻塞特性 默认阻塞,读等待写、写等待读 默认阻塞,读端无数据 / 写端无读端时阻塞
典型用途 客户端 - 服务器架构、跨进程日志 / 数据传递 父子进程间简单通信、shell 管道命令
创建 / 使用 mkfifo() 创建,open()/read()/write() 访问 pipe() 直接创建,配合 fork() 传递描述符

7.命名管道的实现

从上面学习了的命名管道的特性,我们知道了创建命名管道的系统调用函数mkfifo,并且因为命名管道本质上是文件,所以我们对命名管道的操作可以转为对文件的操作,可以使用系统调用函数write,read,close等等,而我们的命名管道需要在两个完全没有关系的进程间实现通信,所以我们的可以创建两个可执行程序,分别是Server.ccClient.cc,服务端和客户端,我们让客户端充当向管道中写的操作,为服务端充当读,最后开启两个xshell观察本地通信情况;值得注意的是,我们的管道具有特性就是一端关闭,另一端会一直读到0才结束,所以我们在一端关闭的时候,要让另一端退出循环;

我们在创建文件的时候,会设置文件的mode ,也就是未来进程使用这个文件的权限,而因为文件会记录用户的UID,进程task_struct中也会记录当前进程的UID,通过对比两者判断是否能对文件做操作;

Server.hpp

cpp 复制代码
#include <iostream>
#include <string>
#include <unistd.h>
#include "Comm.hpp"

class Init
{
public:
    Init()
    {
        umask(0);
        int n = ::mkfifo(gpipeFile.c_str(), gmode);
        if (n < 0)
        {
            // std::cerr << "管道创建失败" << std::endl;
            return;
        }
        std::cout << "管道创建成功" << std::endl;
    }

    ~Init()
    {
        int n = ::unlink(gpipeFile.c_str());
        if (n < 0)
        {
            // std::cerr << "管道删除失败" << std::endl;
            return;
        }
        std::cout << "管道删除成功" << std::endl;
    }

private:
};
Init init;

class Server
{
public:
    Server()
        : _fd(gdefaultfd)
    {
    }
    bool OpenPipeForRead()
    {
        _fd = OpenPipe(FORREAD);
        if (_fd < 0)
        {
            std::cerr << "Open failed" << std::endl;
            return false;
        }
        return true;
    }

    int RecvPipe(std::string *out)
    {
        char buffer[gsize];
        ssize_t n = ::read(_fd, buffer, sizeof(buffer) - 1);
        // sizeof(buffer)-1是期望读到的元素个数,n是实际读到的元素个数
        if (n > 0)
        {
            buffer[n] = 0; // n是读取的元素个数,n为0就
            // 是将读取的最后一个元素置为0,防止溢出
            *out = buffer;
        }
        return n;
    }

     void ClosePipe()
    {
        ClosePipeHelp(_fd);
    }

    ~Server()
    {
    }

private:
    int _fd = 0;
};

Server.cc

cpp 复制代码
#include "Server.hpp"
#include <iostream>

int main()
{
    Server s;
    s.OpenPipeForRead();
    std::string message;
    while (true)
    {
        if(s.RecvPipe(&message) > 0)
        {
            std::cout << "Clinet say #" << message << std::endl;  
        }
        else
        {
            break;
           
        }     
    }
     std::cout<<"Client quit , me too" << std::endl;
    s.ClosePipe();
    return 0;
}

Client.hpp

cpp 复制代码
#include <iostream>
#include <string>
#include "Comm.hpp"

class Client
{
public :
    Client()
    :_fd(gdefaultfd)
    {}

    bool OpenPipeForWRITE()
    {
        _fd = OpenPipe(FORWRITE);
        if (_fd < 0)
        {
            std::cerr << "Open failed" << std::endl;
            return false;
        }
        return true;
    }

    int  SendPipe(const std::string &in)
    {
      
        return ::write(_fd, in.c_str(),in.size());
    
    }

    void ClosePipe()
    {
        ClosePipeHelp(_fd);
    }

    ~Client()
    {}
private:
    int _fd;
};

Client.cc

cpp 复制代码
#include "Client.hpp"
#include <iostream>

int main()
{
    Client client;
    client.OpenPipeForWRITE();
    std::string message;
    while (true)
    {
        std::cout << "可以开始写了" ; 
        std::getline(std::cin, message);
        client.SendPipe(message);
    }

    client.ClosePipe();
    std::cout << "Client" << std::endl;
    return 0;
}

Comm.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

std::string gpipeFile = "./fifo";
mode_t gmode = 0600;
int gsize = 1024;
int gdefaultfd = -1;
const int FORWRITE = O_WRONLY;
const int FORREAD = O_RDONLY;

int OpenPipe(int flag)
{
    int fd = ::open(gpipeFile.c_str(), O_RDONLY);
    if (fd < 0)
    {
        std::cerr << "Open failed" << std::endl;
        return fd;
    }
    return fd;
}

void ClosePipeHelp(int fd)
{

    if (fd >= 0)
        ::close(fd);
}
相关推荐
zzzsde1 小时前
【Linux】进程控制(2):进程等待&&进程替换
linux·服务器·网络
实在智能RPA1 小时前
实在 Agent 支持哪些企业业务场景的自动化?全行业智能自动化场景深度拆解
java·运维·自动化
longxibo2 小时前
【Ubuntu datasophon1.2.1 二开之八:验证实时数据入湖】
大数据·linux·clickhouse·ubuntu·linq
BY组态2 小时前
【对比分析】Ricon组态系统 vs 传统组态软件
运维·物联网·web组态·组态
CDN3602 小时前
各种网站高防服务器选型:360CDN 高防够用吗?
服务器·网络·安全
嵌入式-老费2 小时前
vivado hls的应用(带ddr读取的ip)
服务器·网络·tcp/ip
不知名。。。。。。。。2 小时前
仿muduo库实现高并发服务器----HttpServer
运维·服务器·算法
恋红尘2 小时前
K8S 服务发现-叩丁狼
linux·docker·kubernetes
IMPYLH2 小时前
Linux 的 dd 命令
linux·运维·服务器