【Linux进程间通信】用管道实现简单的进程池、命名管道

本文介绍了在Linux系统中,为何需要进程池以及如何通过管道(包括匿名管道和命名管道)来实现。作者详细讲解了进程池的原理,以及如何使用`mkfifo`创建和控制命名管道,还讨论了两者之间的区别和打开规则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【Linux进程间通信】用管道实现简单的进程池、命名管道

作者:爱写代码的刚子

时间:2024.2.10

前言:本篇博客将会介绍并实现简单的线程池

为什么要实现进程池?

  • 系统调用是有成本的,池化技术是为了我们的访问速度和效率
  • 在需要频繁的创建删除较多进程的情况下,导致计算机资源消耗过多
  • 进程池则是创建指定进程数量等待执行事件,避免了不必要的创建和销毁过程

代码实现

  • ProcessPool_Task.hpp
#pragma once

#include <iostream>
#include <functional>
#include <vector>

using task_t=std::function<void()>;
//typedef void(*task_t)();
void task1()
{
    std::cout<<"task1"<<std::endl;
}

void task2()
{
    std::cout<<"task2"<<std::endl;
}

void task3()
{
    std::cout<<"task3"<<std::endl;
}

void task4()
{
    std::cout<<"task4"<<std::endl;
}

void LoadTask(std::vector<task_t> *tasks)
{
    tasks->push_back(task1);
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);

}
  • ProcessPool.cc
#include "ProcessPool_Task.hpp"
#include <string>
#include <vector>
#include <ctime>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <iostream>

#include <sys/wait.h>
#include <sys/stat.h>

const int processnum = 5;
//描述

std::vector<task_t> tasks;
class channel
{ 
public:
    channel(int cmdfd,int slaverid,const std::string &processname)
    :_cmdfd(cmdfd),_slaverid(slaverid),_processname(processname)
    {}

public:
    int _cmdfd;
    pid_t _slaverid;
    std::string _processname;
};


void slaver()
{
    while(true)
    {
        int cmdcode = 0;
        int n = read(0,&cmdcode, sizeof(int));//如果父进程不给子进程发送数据,则会阻塞等待
        if(n == sizeof(int))
        {
            //执行cmdcode对应的任务列表
            std::cout <<"slaver say@ get a command: "<<getpid() << ": cmdcode: "<< cmdcode <<std::endl;
            if(cmdcode >=0 && cmdcode<tasks.size()) tasks[cmdcode]();
        }
        if(n == 0)break;
    }
}
//参数规范
//输入:const &
//输出:*
//输入输出:&


void InitProcessPool(std::vector<channel> *channels)
{
    //确保每一个子进程都只有一个写端
    std::vector<int> oldfds;
    for(int i=0;i<processnum;++i)
    {
        int pipefd[2];//临时空间
        int n = pipe(pipefd);
        assert(!n);
        (void)n;


        
        pid_t id = fork();
        if(id==0)//子进程,子进程拿到的pipefd都是3
        {
            std::cout<< "child" << getpid() << "close history fd: ";
            for(auto fd : oldfds) 
            {
                std::cout<<fd<<" ";
                close(fd);
            }
            std::cout<<"\n";
            close(pipefd[1]);
            dup2(pipefd[0],0);//将pipefd[0]重定向到0,将来直接往键盘文件(fd为0)文件里面读即可。
            close(pipefd[0]);
            
            slaver();
            std::cout<< "process : "<< getpid() << "quit" <<std::endl;
            //方法一:
            //slaver(pipefd[0]);
            exit(0);
        }
        //父进程,父进程拿到的pipefd是4,5,6...
        close(pipefd[0]);

        //添加channel字段
        std::string name = "process-"+ std::to_string(i);
        channels->push_back(channel(pipefd[1],id,name ));//pipefd[1]表示父进程要往pipefd[1]里面写
        oldfds.push_back(pipefd[1]);
    }
}

void Debug(const std::vector<channel> &channels)
{
    for(const auto &c : channels)
    {
        std::cout<<c._cmdfd<<" "<<c._slaverid<<" "<<c._processname << std::endl;
    }
}

void ctrlSlaver(const std::vector<channel> &channels)
{
    int which = 0;//轮转的方式
    while(true)
    {
        //1.选择任务
        int cmdcode = rand()%tasks.size();

        //2.选择进程
        //[负载均衡(1.随机数 2.轮转)]
        int processpos = rand()%channels.size();

        //3.发送任务
        write(channels[which]._cmdfd,&cmdcode,sizeof(cmdcode));
        
        ++which;
        which %= channels.size();
        
        sleep(1);
    }
}

void QuitProcess(const std::vector<channel> &channels)
{
    //for(const auto &c : channels) close(c._cmdfd);
    //for(const auto &c : channels) waitpid(c._slaverid,nullptr,0);
    //这里存在子进程有多个写端的问题,解决办法:
    //方法一.从后往前关闭子进程
    int last = channels.size()-1;
    for(int i= last;i>= 0;i--)
    {
        close(channels[i]._cmdfd);
        waitpid(channels[i]._slaverid,nullptr,0);
    }
    //方法二.确保每一个子进程都只有一个写端

}
int main()
{
    
    LoadTask(&tasks);
    //随机数
    srand(time(nullptr)^getpid()^1023);
    //组织
    std::vector<channel> channels;//将特定的结构转化为数据的增删查改
    //初始化
    InitProcessPool(&channels);
    Debug(channels);
    //控制子进程
    ctrlSlaver(channels);
    //清理收尾
    QuitProcess(channels);

    return 0;
}

图解:

在这里插入图片描述

命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。

  • 命名管道是一种特殊类型的文件

创建一个命名管道
  • $ mkfifo filename在命令行上创建命名管道

在这里插入图片描述

p开头表示这是命名管道(但是并不在磁盘上),同时管道文件的大小为0

在这里插入图片描述

  • *int mkfifo(const char filename, mode_t mode); 程序中创建命名管道的函数

  • common.hpp

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <fcntl.h>
#define FIFO_FILE "./myfifo"
#define MODE 0664

enum {
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FILE_OPEN_ERR

};

  • makefile
.PHONY:all
all:server client
server:server.cc
	g++ -o $@ $^ -g -std=c++11
client:client.cc
	g++ -o $@ $^ -g -std=c++11

.PHONY:clean
clean:
	rm -f server client

  • server.cc
#include "common.hpp"

using namespace std;

//管理管道文件
int main()
{
    int n = mkfifo(FIFO_FILE,MODE);
    if(n==-1)
    {
        
        perror("mkfifo");
        exit(FIFO_CREATE_ERR);
    }
    //sleep(5);
    int fd = open(FIFO_FILE,O_RDONLY);//注意这里会等待写端打开之后才会打开文件,向后执行,open阻塞了
    if(fd<0)
    {
        perror("open");
        exit(FILE_OPEN_ERR);
    }

    while(true)
    {
        char buffer[1024]={0};
        int x =read(fd,buffer,sizeof(buffer));
        if(x>0)
        {
            buffer[x]=0;
            cout<<"client say#"<<buffer<<endl;
        }
        else if(x==0)
        {
        	cout<<"cilent quit,me too\n"<<endl;//写端关闭读端也关闭
        	break;
        }
        else
        {
        	break;
        }
        
    }
    close(fd);
    
    int m =unlink(FIFO_FILE);
    if(m==-1)
    {
        perror("unlink");
        exit(FIFO_DELETE_ERR);
    }
    return 0;
}

  • client.cc
#include "common.hpp"
using namespace std;
int main()
{
    int fd = open(FIFO_FILE,O_WRONLY);
    if(fd<0)
    {
        perror("open");
        exit(FILE_OPEN_ERR);
    }
    string line;
    while(true)
    {
        cout<<"Please Enter@ ";
        getline(cin,line);

        write(fd,line.c_str(),line.size());
    }
    close(fd);
    return 0;
}

在这里插入图片描述

理解命名管道

不同的两个进程打开同一个文件的时候,在内核中操作系统文件描述符只会指向同一个文件,进程间通信的前提:先让不同的进程看到同一份资源,管道文件则不需要进行刷盘(内存级文件),所以大小为0字节。

【问题】:如何保证打开的是同一个文件?看到同一个路径下的同一个文件名。(inode),即= 路径 + 文件名(唯一性)

匿名管道与命名管道的区别

匿名管道由pipe函数创建并打开。

命名管道由mkfifo函数创建,打开用open

FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

命名管道的打开规则
  • 如果当前打开操作是为读而打开FIFO时

    • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
    • O_NONBLOCK enable:立刻返回成功
  • 如果当前打开操作是为写而打开FIFO时

    • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
    • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱写代码的刚子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值