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


目录
相关文章
|
1月前
|
安全 算法 Linux
探索Linux命令gpgv2:安全通信与数据验证的利器
`gpgv2`是GPG的签名验证工具,用于确保文件完整性和来源真实性。基于公钥密码学,支持多种加密算法和OpenPGP标准。通过`--verify`等参数验证签名,例如`gpgv2 --verify signature_file file`。重要注意事项包括保护私钥、定期更新密钥、验证签名来源及使用强密码。在数据安全场景中,`gpgv2`是保障信息安全的利器。
|
2月前
|
存储 负载均衡 Linux
【Linux 系统】进程间通信(匿名管道 & 命名管道)-- 详解(下)
【Linux 系统】进程间通信(匿名管道 & 命名管道)-- 详解(下)
|
3天前
|
Linux
linux中 grep过滤查找 及 管道 ”|” 的使用
linux中 grep过滤查找 及 管道 ”|” 的使用
|
3天前
|
安全 Linux 数据格式
【Linux】进程通信----管道通信(下)
【Linux】进程通信----管道通信(下)
14 0
|
3天前
|
Unix Linux
【Linux】进程通信----管道通信(上)
【Linux】进程通信----管道通信(上)
20 0
|
19天前
|
Linux Shell C++
【linux】命名管道
【linux】命名管道
6 0
|
19天前
|
消息中间件 负载均衡 Linux
【linux】匿名管道|进程池
【linux】匿名管道|进程池
7 0
|
1月前
|
Linux Shell
【Linux】管道
【Linux】管道
|
19天前
|
Linux 芯片
一篇文章讲明白Linux内核态和用户态共享内存方式通信
一篇文章讲明白Linux内核态和用户态共享内存方式通信
10 0
|
1月前
|
Linux Shell
【Linux】命名管道
【Linux】命名管道