进程间通信:认识匿名管道和进程池实现

一.介绍进程间通信

1.什么是进程间通信

进程间通信(Inter-Process Communication,简称 IPC) 是指在不同进程之间传递或交换信息的过程。在操作系统中,每个进程都有自己独立的内存空间(虚拟地址空间),这意味着一个进程无法直接访问另一个进程的内存数据,需要借助操作系统提供的 IPC 机制来实现。

2.为什么要进行进程间通信

为了打破这种隔离,让进程能够协同工作、共享数据或相互控制,就需要借助操作系统提供的 IPC 机制。

作用:

1.数据传输:一个进程需要把数据传输给另一个进程。

2.资源共享:多个进程共享同样的资源。

3.通知事件:一个进程需要向另外的进程发送消息,表示发生了某种事件(如子进程退出,通知父进程回收子进程)。

4.进程控制:有些进程需要完全控制其他进程的执行,此时控制进程希望能够 拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

3.进程间通信的本质:

让不同的进程看到同一份资源------内存。此内存是由操作系统提供的,并且由系统调用设置统一的接口。

4.发展历程

**管道:**匿名管道pipe, 命名管道

**System V IPC:**System V 消息队列,System V 共享内存 和 System V 信号量

**POSIX IPC:**消息队列 ,共享内存,信号量 ,互斥量 ,条件变量 和 读写锁

二.匿名管道

1. 基础概念

匿名管道是 Linux 最基础的 IPC 方式,由 pipe() 系统调用创建。 在内核中存在一块缓冲区,没有对应的磁盘文件、没有文件名、没有 inode,因此称为匿名管道,为半双工模式。

单向通信(半双工)

数据只能从写端流向读端;如需双向通信,需要创建两根管道。

全双工: 通信双方可以同时收发数据。 A 发数据给 B 的同时,B 也可以发数据给 A,两条数据流互不干扰。

2.系统调用

复制代码
 #include <unistd.h>

 int pipe(int pipefd[2]);

在内核创建一块缓冲区(匿名管道),返回一对文件描述符。

pipefd2:文件描述符

  • fd 0:管道读端(read)
  • fd 1:管道写端(write)
  • 通常fd0=3,fd1=4,因为已经有三个默认打开的文件,所以只能排3和4.

注意:数据只能:fd 1 → fd 0

返回值

  • 成功:返回 0
  • 失败:返回 -1,同时设置 errno

使用:

3.匿名管道的原理

我们从进程通信的本质出发,观察匿名管道是如何做到两个进程共享一块内存空间。

首先由一个进程创建一个管道文件并通过文件描述符来IO连接。

然后该进程再fork一个子进程,根据子进程的开始浅拷贝的特性,子进程会拥有和父进程相同的文件描述符,那么其二者都指向同一个匿名管道。这样通信的条件就形成了。

然后根据系统调用的条件(半双工模式),选择留下一个接收,一个发送,就完成了信息的传递。

4.父子进程使用匿名管道通信

子进程写,父进程读:

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


void Childwrite(int fd)
{
    while(1)
    {
    char putbuff[1024];
    snprintf(putbuff,sizeof(putbuff),"fd:%d,子进程pid:%d\n",fd,getpid());
    write(fd,putbuff,sizeof(putbuff));
    }
}
void Fatherread(int fd)
{
    while(1)
    {
    char getbuff[1024];
    read(fd,getbuff,sizeof(getbuff));
    std::cout<<getbuff<<std::endl;
    }
}

int main()
{
int fd[2];
int n=pipe(fd);
pid_t id=fork();
if(id==0)
{
close(fd[0]);
Childwrite(fd[1]);
close(fd[1]);
exit(0);
}
close(fd[1]);
Fatherread(fd[0]);
close(fd[0]);
wait(nullptr);
    return 0;
}

5.五种特性和四种情况

1.五种特性

1.匿名管道只能在有亲缘关系的进程间进行通信(父子,兄弟)

文件描述符只能被 fork( )继承,没有亲缘关系无法拿到同一个管道的读写端。无亲缘进程无法获取管道同一对读写 fd,不能通信。

2.只能进行单向通信,半双工工作模式。

3.管道文件,自带同步机制。

当写慢,读快时:读阻塞;

当写快,读慢时:管道文件很快被写满,写阻塞。

4.管道是面向字节流的。

不存在消息边界。多次写入的数据在内核缓冲区连续存放,读取时可能一次性读出多条消息,也可能拆分读出。业务层必须自行约定分包规则(定长、特殊分隔符、消息头携带长度)区分独立消息。

5.管道的生命周期随进程。

管道缓冲区存在内核内存,无磁盘实体;当所有持有管道 fd 的进程全部关闭两端描述符,内核自动释放,管道销毁。

2.四种情况

1.写慢,读快

现象:读进程读取速度 大于写进程写入速度,管道缓冲区很快被读空。

行为:read() 阻塞,读进程休眠,等待写端写入新数据,读的速度被拖慢。

写端:sleep 2秒,再写入。

2.写快,读慢

现象:写进程写入速度 大于读进程读取速度,内核环形缓冲区被填满。

行为:write() 阻塞,写进程休眠,等待读端读取数据腾出缓冲区空间。

3.写关闭,读继续

1.管道内残留的数据依旧可以正常 read 读取;

  1. 缓冲区数据全部读完之后,后续read()不再阻塞,直接返回 0(EOF);

  2. 子进程收到返回值 0,代表通信通道关闭,可以正常退出。

写三遍然后关闭

4.写继续,读关闭

内核向执行 write 的进程发送 SIGPIPE(管道破裂)信号,直接杀死写进程

我们使用waitpid来接收其的退出码来验证。

三.进程池实战

目标结构:父进程通过管道分别与多个子进程进行通信,父进程写入任务码,子进程读取,然后子进程去执行。

父进程通过管道的机制打开多个子进程,并保持就绪(从进程状态来讲,就是进程阻塞),需要使用时就可以快速执行任务,不用从头开始。

代码实现:

代码结构设计:

思想:先描述,再组织

先描述:主体是管道(为什么?因为通过管理管道可以管理子进程(回顾前面四种情况)),所以我们需要描述单个管道的属性以及其的任务(其实就是根据自己的机制对子进程管理)

再组织:对多个管道进行组织,形成结构,方便多个进行协作(这里没有),方便进行查找想要的管道,然后对其进行操作。

最后对外提供统一的接口:掩盖细节,方便使用。

具体代码:

1.描述:

创建channel类:

int _wfd;//父进程的写管道,关闭时,管道只剩下子进程的读端,读完就会关闭子进程,从而实现对其的管理

pid_t _cpid;//子进程的pid

std::string _name;//这个管道的总体描述字符串

复制代码
class channel
{
public:
    channel(int wfd,pid_t cpid)
    :_wfd(wfd)
    ,_cpid(cpid)
    {
        _name="子进程:"+std::to_string(_cpid)+"write_fd"+std::to_string(_wfd);
    }

    ~channel()
    {}
private:
int _wfd;//父进程的写管道
pid_t _cpid;//子进程的pid
std::string _name;//这个管道的总体描述字符串
};

方法:根据最后对外的接口来设计:

主要就是下面这些:配合完成:任务码读写,关闭管道和子进程

复制代码
channel(int wfd,pid_t cpid)
    :_wfd(wfd)
    ,_cpid(cpid)
    {
        _name="子进程:"+std::to_string(_cpid)+"write_fd"+std::to_string(_wfd);
    }
    void Close()
    {
        close(_wfd);
    }
    void Wait()
    {
        wait(NULL);
    }
    
    void Write(int code)//对管道进行写
    {
        write(_wfd, &code, sizeof(code));
    }

2.组织管理管道

属性:多个channel组成的数组

复制代码
std::vector<channel> _channels;

方法:

1.选择管道

需求:任务尽量平均给各个子进程

平均方式:随机选取

复制代码
ChannelManger()
{
    srand(time(nullptr));
}
int select_channel()
{
    if (_channels.empty())
    {
        std::cerr << "错误:不存在可用子进程通道!" << std::endl;
        return -1;
    }
    return rand()%_channels.size();
}

2.插入构造channel

复制代码
void insert(int wfd,pid_t pid)
{
    // 原地构造,避免拷贝
    _channels.emplace_back(wfd, pid);
}

3.写入任务码

根据挑选的管道写入挑选出的任务码

复制代码
void Cwrite(int Ccode,int Tcode)
{
    _channels[Ccode].Write(Tcode);
}

4.关闭管道写端

按照我们的构想,只需要在父进程中关闭其对各个管道的写端就行了。

复制代码
void Channelclose()
{
    for(auto& ch:_channels)
    {
        ch.Close();
        ch.Wait();
    }
}

但是实际情况要实际分析,所以我们在具体创建管道和子进程时再进行讨论。

3.准备对外接口

1.需要任务管理,进行选择任务,执行任务

复制代码
typedef void(*Task)();
class TaskManger
{
public:
    TaskManger()
    {
    }
    void insert(Task task)
    {
        _Ts.push_back(task);
    }
    int select_Task()
    {
        int Next=_Tflag;
        _Tflag=(_Tflag+1)%_Ts.size();
        return Next;
    }
    void Execute(int flag)
    {
        _Ts[flag]();
    }
    ~TaskManger()
    {}
private:
    std::vector<Task> _Ts;
    int _Tflag=0;
};

2.主体Processpool

2.1属性:

复制代码
class Processpool
{
public:
Processpool()
{
    //插入任务
    _TM.insert(Print);
    _TM.insert(Doneload);
    _TM.insert(Upsend);

}
private:
ChannelManger _CM;//管道管理器
TaskManger _TM;//任务管理器
};

2.2创建进程进程池

复制代码
bool Create()
{
    for(int i=0;i<process_count;i++)
    {
        int pipefd[2];
        int n=pipe(pipefd);//创建相应的管道
        if(n==-1)
        {
            std::cout<<"pipe err"<<std::endl;
            return false;
        }
        ssize_t pid=fork();//创建子进程
        if(pid==0)
        {
          //  _CM.Channelclose_1();
            close(pipefd[1]);//关闭写端
            Work(pipefd[0]);//等待任务码,有就执行任务
            close(pipefd[0]);//关闭读端
            exit(0);
        }
        else if(pid>0)//父进程
        {
             close(pipefd[0]);//关闭读端
             _CM.insert(pipefd[1],pid);//插入初始化管道管理器
  
        }
        else//创建失败
        {
            std::cout<<"fork err"<<std::endl;
            return false;
        }
    }
    return true;
}

work函数(子进程等待任务码,并且执行):

复制代码
void Work(int fd)
{
    while(true)
    {
    int taskcode=0;
    ssize_t n=read(fd,&taskcode , sizeof(taskcode));
    if(n>0)
    {
        if(n==4)
        {
            _TM.Execute(taskcode);
        }
        else
        {
            continue;
        }
    }
    else if(n==0)//读完了,然后父进程的写端关闭,就退出了work
    {
        std::cout<<"子进程退出"<<std::endl;
        break;
    }
    else
    {
        std::cout<<"子进程read,err"<<std::endl;
        break;
    }
}
}

以上为对外接口create,和子进程干的事(读端)

2.3父进程写端

选择子进程,写入任务码

复制代码
void Run()
{
    int taskcode=_TM.select_Task();
    int channelcode=_CM.select_channel();
    std::cout<<"选中的子进程:"<<_CM.Get_name(channelcode)<<std::endl;

    _CM.Cwrite(channelcode,taskcode);
    std::cout<<"选中的任务码:"<<taskcode<<std::endl;

}

2.4退出------关闭父进程写端

我们按照理想的结构:父进程的写端关闭,管道内的内容被读完,那么管道就会关闭,子进程就会退出。

所以:

复制代码
void Channelclose()
{
    for(auto& ch:_channels)
    {
        ch.Close();//关闭写端
        ch.Wait();//回收子进程信息
    }
}

但是我们会发现最后面全部子进程阻塞了,没有一个退出

这个是怎么回事:我们都知道子进程创建时会拷贝父进程的信息------fd,那么父进程指向前面管道的写端也会被拷贝,所以最后形成的结构并不是我们想象的那样。

画图理解

加上wait阻塞,所以从前往后关闭写端的方法使得进程全部阻塞。那么我们该如何解决呢?

解决方案:

方案一:

在子进程创建的时候对其拷贝父进程的各个写端进行删除。然后再进行从前往后的删除。

方案二:

从后往前删除

完整代码:

processpoll.hpp

复制代码
#pragma once
#include<iostream>
#include<vector>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<cstdlib>
#include<time.h>
#include"Task.hpp"
class channel
{
public:
    channel(int wfd,pid_t cpid)
    :_wfd(wfd)
    ,_cpid(cpid)
    {
        _name="子进程:"+std::to_string(_cpid)+"write_fd"+std::to_string(_wfd);
    }
    void Close()
    {
        close(_wfd);
    }
    void Wait()
    {
        wait(NULL);
    }
    void Print()
    {
        std::cout<<_name<<std::endl;
    }
    void Write(int code)
    {
        write(_wfd, &code, sizeof(code));
    }
    int Get_wfd()
    {
        return _wfd;
    }
    pid_t Get_cpid()
    {
        return _cpid;
    }
    std::string& Get_NAME()
    {
        return _name;
    }
    ~channel()
    {}
private:
int _wfd;
pid_t _cpid;
std::string _name;
};

class ChannelManger
{
public:
ChannelManger()
{
    srand(time(nullptr));
}
void insert(int wfd,pid_t pid)
{
    // 原地构造,避免拷贝
    _channels.emplace_back(wfd, pid);
}
int select_channel()
{
    if (_channels.empty())
    {
        std::cerr << "错误:不存在可用子进程通道!" << std::endl;
        return -1;
    }
    return rand()%_channels.size();
}

std::string& Get_name(int num)
{
    return _channels[num].Get_NAME();
}
void Prints()
{
    for(int i=0;i<_channels.size();i++)
    {
        _channels[i].Print();
    }
}
void Cwrite(int Ccode,int Tcode)
{
    _channels[Ccode].Write(Tcode);
}

//方案一,子进程创建时让子进程自己关加给其它的写端
void Channelclose_1()
{
    for(auto& ch:_channels)
    {
        ch.Close();
    }
}
void Channelclose()//再父进程全部关
{
    for(auto& ch:_channels)
    {
        ch.Close();
        ch.Wait();
    }
}

void Channelclose_2()
{
    for(int i=_channels.size()-1;i>=0;i--)
    {
        _channels[i].Close();
        _channels[i].Wait();
    }
}


~ChannelManger()
{}
private:
std::vector<channel> _channels;
};



const int process_count=5;


class Processpool
{
public:
Processpool()
{
    _TM.insert(Print);
    _TM.insert(Doneload);
    _TM.insert(Upsend);

}
void Work(int fd)
{
    while(true)
    {
    int taskcode=0;
    ssize_t n=read(fd,&taskcode , sizeof(taskcode));
    if(n>0)
    {
        if(n==4)
        {
            _TM.Execute(taskcode);
        }
        else
        {
            continue;
        }
    }
    else if(n==0)
    {
        std::cout<<"子进程退出"<<std::endl;
        break;
    }
    else
    {
        std::cout<<"子进程read,err"<<std::endl;
        break;
    }
}
}
bool Creat()
{
    for(int i=0;i<process_count;i++)
    {
        int pipefd[2];
        int n=pipe(pipefd);
        if(n==-1)
        {
            std::cout<<"pipe err"<<std::endl;
            return false;
        }
        ssize_t pid=fork();
        if(pid==0)
        {
          // _CM.Channelclose_1();
            close(pipefd[1]);
            Work(pipefd[0]);
            close(pipefd[0]);
            exit(0);
        }
        else if(pid>0)
        {
             close(pipefd[0]);
             _CM.insert(pipefd[1],pid);
  
        }
        else
        {
            std::cout<<"fork err"<<std::endl;
            return false;
        }
    }
    return true;
}
void Debug()
{
    _CM.Prints();
}
void Run()
{
    int taskcode=_TM.select_Task();
    int channelcode=_CM.select_channel();
    std::cout<<"选中的子进程:"<<_CM.Get_name(channelcode)<<std::endl;

    _CM.Cwrite(channelcode,taskcode);
    std::cout<<"选中的任务码:"<<taskcode<<std::endl;

}
void over()
{
    _CM.Channelclose();
   
}
~Processpool()
{}
private:
ChannelManger _CM;
TaskManger _TM;
};

main.cpp

复制代码
#include"processpool.hpp"

int main()
{
    Processpool pp;
    pp.Creat();
    pp.Debug();
    int cnt=3;
    while(cnt--)
    pp.Run();
    pp.over();
    return 0;
}

task.hpp

复制代码
#include<vector>
#include<iostream>

void Print(){std::cout<<"我在打印"<<std::endl;}
void Doneload(){std::cout<<"我在下载"<<std::endl;}
void Upsend(){std::cout<<"我在上传"<<std::endl;}
typedef void(*Task)();
class TaskManger
{
public:
    TaskManger()
    {
    }
    void insert(Task task)
    {
        _Ts.push_back(task);
    }
    int select_Task()
    {
        int Next=_Tflag;
        _Tflag=(_Tflag+1)%_Ts.size();
        return Next;
    }
    void Execute(int flag)
    {
        _Ts[flag]();
    }
    ~TaskManger()
    {}
private:
    std::vector<Task> _Ts;
    int _Tflag=0;
};

结果:

相关推荐
重生的黑客14 小时前
Linux 进程程序替换与自定义 Shell:从 exec 函数族到命令行解释器
linux·运维·服务器·shell
北极糊的狐14 小时前
阿里云服务器-命令2-Linux 系统实时资源监视器 top 命令详解(进程级实时资源监控)
linux·运维·服务器
三言老师15 小时前
clear与history历史命令管理实操
linux·运维·服务器·网络·centos
味悲16 小时前
Linux 环境下 DNS 服务器搭建
linux·运维·服务器
dok1216 小时前
Johannes 《Linux内核模块与设备驱动开发:编写Linux驱动程序》 (7)open,release (8) write read
linux
feasibility.17 小时前
wsl安装Ubuntu方法(含网络不稳定处理)
linux·运维·windows·ubuntu
‎ദ്ദിᵔ.˛.ᵔ₎17 小时前
linux进程
linux
湿滑路面17 小时前
硅基聊天室——如何用supervisor优雅的管理服务进程
linux·前端·python
我命由我1234518 小时前
Windows 操作系统 - 符号链接
linux·运维·windows·系统架构·操作系统·运维开发·系统