Linux利用管道通信实现一个写端对应多个读端(一)

简介: Linux利用管道通信实现一个写端对应多个读端(一)

匿名管道

实现目标

实现一个父进程创建出多个子进程,并且父进程与每个子进程都有独立的管道

父进程可以通过管道写入指定的任务,并随机指派某个子进程通过管道读取任务并执行

思路

  1. 首先需要创建出多个子进程(静态实现指定数量),因为每个子进程与父进程之间的管道是独立的,到后面需要进行读写时对应的文件描述符都是不一样的,所以可以选用数组将每个子进程对应的管道读写fd记录下来。因为涉及到了多个属性,因此可以将这些属性归并成一个类。数组的类型就采用这个类。其中可以包括管道的名字,子进程id,父进程写端的fd。
  2. 指派的任务可以有很多个,所以可以利用函数指针的类型数组将所有的任务记录下来,访问数组就可以执行对应的函数
  3. 这里需要注意一个问题:父进程创建子进程是一个一个的创建的。在父进程创建的第2个子进程开始,由于已经创建过管道所以父进程已经打开过写端,而子进程被创建出来会继承父进程的文件描述符表,因此这个已经被打开的写端也会基础下去。如此反复则从第2个开始被创建出来的子进程都会拥有上一个子进程的写端,这样会导致即使关闭了父进程对应的写端时,子进程的读端仍然会处于阻塞状态等待读取。为了避免这种情况可以在每一次创建子进程后都将子进程继承下来的上一个子进程的写端记录下来,然后在进程创建后如果当前有记录就将记录下来的写端全部关闭。因为进程具有独立性所以每一次创建出来子进程都要进行一次这个操作才能保证每个子进程都没有写端。
  4. 因为子进程的id和任务都有被保存在数组中,只需要下标就可以找到。因此可以使用随机数来随机指派子进程和任务。将随机的任务下标写到随机指派的子进程对应的管道中,再由该子进程去读取任务下标并执行。因为所有的子进程都是在阻塞中等待管道有数据,因此不管指派的是哪个子进程都是可以读到数据的
  5. 当子进程拿到下标后就执行对应的函数。因为写入的是整数,所以如果读取到的不是整数说明读取失败,失败时那么子进程的读端也就可以退出了。
  6. 可以使用计数来指定父进程写入多少次,如果父进程的写端退出了,那么要把所有子进程对应的写端都要关闭掉。
  7. 当父进程写端全部退出后子进程的读端也可以退出了,全部子进程的读端都退出后要对子进程依次进行回收。

代码实现

#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<cstring>
#include<cassert>
#include<sys/wait.h>
#include<sys/types.h>
#include<vector>
#include<ctime>
using namespace std;
//静态指定池里的子进程数
#define childNum 15
//利用函数指针记录需要传给子进程完成的任务
typedef void(*func)();
void Delete(){
    cout << getpid() << ": " << "删除任务" << endl;
    sleep(1);
}
void Install(){
    cout << getpid() << ": " << "安装任务" << endl;
    sleep(1);
}
void Renew(){
    cout << getpid() << ": " << "更新任务" << endl;
    sleep(1);
}
void Pack(){
    cout << getpid() << ": " << "打包任务" << endl;
    sleep(1);
}
void Send(){
    cout << getpid() << ": " << "发送任务" << endl;
    sleep(1);
}
//保存任务的函数
void LoadFunc(vector<func>& Funcgroup){
    Funcgroup.push_back(Delete);
    Funcgroup.push_back(Install);
    Funcgroup.push_back(Renew);
    Funcgroup.push_back(Pack);
    Funcgroup.push_back(Send);
}
//创建保存子进程id、写端的fd、每个池的名字的类
class Ed{
public:
    Ed(const string& name, const pid_t& id, const int& writefd)
        :_name(name)
        ,_id(id)
        ,_writefd(writefd)
    {}
    const string getname() const{
        return _name;
    }
    const pid_t getid() const{
        return _id;
    }
    const int getwritefd() const{
        return _writefd;
    }
private:
    string _name;
    pid_t _id;
    int _writefd;
};
//读取管道里的任务码
int ReadTask(int readFd){
    int code;
    ssize_t s = read(readFd, &code, sizeof(code));
    if(s == 4) return code;
    return -1;
}
//实现保存所有创建出来的子进程信息
//并实现子进程接收任务且执行任务
void CreateChild(vector<Ed>& childgroup, vector<func>& Taskgroup){
    //因为子进程会继承父进程的文件描述符表,因此从第二个子进程创建开始
    //每个子进程都会打开了一个对应着上个子进程的写端
    //因为父进程里对应的写端是打开的 所以再次创建子进程就会被继承下去
    //因此要记录下来每一次创建子进程时打开的写端文件描述符,在下一次创建子进程后
    //将子进程继承下来的文件描述符表关掉,这样所有创建出来的子进程才不会有联系
    //利用数组记录子进程继承下来的写端
    vector<int> DeleteFd;
    //创建子进程并创建管道
    for(int i = 0; i < childNum; ++i){
        //创建管道
        int fds[2];
        int n = pipe(fds);
        assert(n == 0);
        pid_t id = fork();
        //子进程
        if(id == 0){
            //在子进程被创建出来后,先将继承下来的上个子进程的写端关闭
            for(int i = 0; i < DeleteFd.size(); ++i)
                close(DeleteFd[i]);
            //关闭子进程的写
            close(fds[1]);
            while(1){
                //读取
                int TaskNum = ReadTask(fds[0]);            
                //执行任务
                if(TaskNum >= 0 && TaskNum < Taskgroup.size())
                    Taskgroup[TaskNum]();
                else if(TaskNum == -1)
                    break;
                else
                    cout << "没有此任务" << endl;
            }
            exit(0);
        }
        //关闭父进程的读
        close(fds[0]);
        //记录子进程
        childgroup.push_back({"proce " + to_string(i), id, fds[1]});
        //记录子进程被创建后继承下来的上一个进程的写端
        DeleteFd.push_back(fds[1]);
    }
}
//发送任务给子进程 
void SendChild(const Ed& s, int taskNum){
    cout << "send " << taskNum << " " << "to " << s.getname() << endl;
    //写入任务到子进程所对应的管道文件
    int n = write(s.getwritefd(), &taskNum, sizeof(taskNum));
    assert(n == sizeof(int));
}
//父进程随机选择子进程和任务并发送
//参数cnt是用来计数的,如果传入时为负数则一直循环,否则计数
void SelSend(const vector<func>& Funcgroup, const vector<Ed>& childgroup, int cnt){
    while(1){
        if(cnt >= 0)
            if(cnt-- == 0)
                break;
        //随机选择一个进程
        int procI = rand() % childNum;
        //随机选择一个任务
        int taskI = rand() % Funcgroup.size();
        //发送任务
        SendChild(childgroup[procI], taskI);
        sleep(2);
    }
    //如果写端退出,则关闭所有子进程对应的写端
    for(int i = 0; i < childNum; ++i)
        close(childgroup[i].getwritefd());
}
//回收子进程
void waitProcess(vector<Ed>& childgroup){
    for(int i = 0; i < childgroup.size(); ++i){
        waitpid(childgroup[i].getid(), nullptr, 0);
        cout << "等待成功: " << childgroup[i].getid() <<  endl;
    }
}
int main(){
    //随机数种子
    srand(time(nullptr));
    //记录所有的子进程的任务
    vector<func> Funcgroup;
    LoadFunc(Funcgroup);
    //记录创建出来的子进程的pid
    vector<Ed> childgroup;
    CreateChild(childgroup, Funcgroup);
    //父进程任意选择控制子进程
    SelSend(Funcgroup, childgroup, 3);
    //回收子进程
    waitProcess(childgroup);
    return 0;
}

动图结果演示

以下动图结果演示为计数器是3的情况,因此只会有3次读写

ab50014071014e3c990bfd68b8d40c64.gif


相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
目录
相关文章
|
3月前
|
网络协议 Linux 网络性能优化
Linux C/C++之TCP / UDP通信
这篇文章详细介绍了Linux下C/C++语言实现TCP和UDP通信的方法,包括网络基础、通信模型、编程示例以及TCP和UDP的优缺点比较。
79 0
Linux C/C++之TCP / UDP通信
|
3月前
|
网络协议 Linux
linux学习之套接字通信
Linux中的套接字通信是网络编程的核心,允许多个进程通过网络交换数据。套接字提供跨网络通信能力,涵盖本地进程间通信及远程通信。主要基于TCP和UDP两种模型:TCP面向连接且可靠,适用于文件传输等高可靠性需求;UDP无连接且速度快,适合实时音视频通信等低延迟场景。通过创建、绑定、监听及读写操作,可以在Linux环境下轻松实现这两种通信模型。
55 1
|
5月前
|
消息中间件 Linux 开发者
Linux进程间通信秘籍:管道、消息队列、信号量,一文让你彻底解锁!
【8月更文挑战第25天】本文概述了Linux系统中常用的五种进程间通信(IPC)模式:管道、消息队列、信号量、共享内存与套接字。通过示例代码展示了每种模式的应用场景。了解这些IPC机制及其特点有助于开发者根据具体需求选择合适的通信方式,促进多进程间的高效协作。
217 3
|
5月前
|
存储 Unix Linux
Linux I/O 重定向与管道
【8月更文挑战第17天】重定向在Linux中改变命令I/O流向,默认有&quot;&gt;&quot;覆盖输出至文件及&quot;&gt;&gt;&quot;追加输出至文件末尾,便于保存结果;使用&quot;&lt;&quot;从文件读取输入而非键盘,高效处理数据。文件描述符如0(stdin)、1(stdout)、2(stderr)标识I/O资源,支持读写操作。管道以&quot;|&quot;连接命令,使前一命令输出成为后一命令输入,如排序用户或找出CPU占用最高的进程,构建复杂数据处理流程。
58 9
|
5月前
|
Linux
Linux源码阅读笔记13-进程通信组件中
Linux源码阅读笔记13-进程通信组件中
|
5月前
|
消息中间件 安全 Java
Linux源码阅读笔记13-进程通信组件上
Linux源码阅读笔记13-进程通信组件上
|
5月前
|
存储 Linux 数据处理
在Linux中,什么是管道操作,以及如何使用它?
在Linux中,什么是管道操作,以及如何使用它?
|
5月前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
93 0
|
5月前
|
消息中间件 Linux
Linux0.11 管道(十一)
Linux0.11 管道(十一)
37 0
|
5月前
|
数据挖掘 Linux 应用服务中间件
在Linux中,如何在Linux中使用管道?
在Linux中,如何在Linux中使用管道?

热门文章

最新文章