我与Linux的爱恋:进程间通信 匿名管道以及命名管道的实现与应用


🔥个人主页guoguoqiang. 🔥专栏Linux的学习

文章目录

匿名管道的应用---- 进程池/C++实现

当系统中需要处理很多任务时,可以将这些任务分配给众多子进程来分担任务,然而,频繁的创建和注销进程会导致较高的时间成本。为减少这种开销,可以采取预先创建一组子进程的策略(避免在任务分配时进行即时创建),并在子进程空闲时让它们处于阻塞状态(以防止子进程完成任务后被立即终止,从而在下一次任务分配时无需重新创建进程)。

我们可以使用匿名管道这一技术。通过子进程与父进程之间创建匿名管道,我们可以实现父进程向特定的子进程发送任务指令。

父进程不应该同时派发大量任务给同一个子进程,让其他进程处于空闲状态,这样会使整体效率下降,我们可以通过轮询 或者随机派发 的方式向子进程派发任务。

那么我们应该如何实现子进程对不同任务的管理呢?? 先描述再组织

随机派发任务,首先要有任务

cpp 复制代码
#pragma once
 
#include <iostream>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>
 
#define TaskNum 3
 
typedef void (*task_t)(); // task_t 函数指针类型
 
void Print()
{
    std::cout << "I am print task" << std::endl;
}
void DownLoad()
{
    std::cout << "I am a download task" << std::endl;
}
void Flush()
{
    std::cout << "I am a flush task" << std::endl;
}
 
task_t tasks[TaskNum];
 
void LoadTask()
{
    srand(time(nullptr) ^ getpid() ^ 17777);
    tasks[0] = Print;
    tasks[1] = DownLoad;
    tasks[2] = Flush;
}
 
void ExcuteTask(int number)
{
    if (number < 0 || number > 2)
        return;
    tasks[number]();
}
 
int SelectTask()
{
    return rand() % TaskNum;
}
void work()
{
    while (true)
    {
        int command = 0;
        int n = read(0, &command, sizeof(command));
        if (n == sizeof(int))
        {
            std::cout << "pid is : " << getpid() << " handler task" << std::endl;
            ExcuteTask(command);
        }
        else if (n == 0)
        {
            std::cout << "sub process : " << getpid() << " quit" << std::endl;
            break;
        }
    }
}

先描述

cpp 复制代码
 
class Channel
{
public:
    Channel(int wfd, pid_t id, const std::string &name)
        : _wfd(wfd), _subprocessid(id), _name(name)
    {
    }
    int GetWfd() { return _wfd; }
    pid_t GetProcessId() { return _subprocessid; }
    std::string GetName() { return _name; }
    void CloseChannel()
    {
        close(_wfd);
    }
    void Wait()
    {
        pid_t rid = waitpid(_subprocessid, nullptr, 0);
        if (rid > 0)
        {
            std::cout << "wait " << rid << " success" << std::endl;
        }
    }
    ~Channel()
    {
    }
 
private:
    int _wfd;
    pid_t _subprocessid;
    std::string _name;
};

创建管道和子进程

cpp 复制代码
// 形参类型和命名规范
// const &: 输出
// & : 输入输出型参数
// * : 输出型参数
//  task_t task: 回调函数
void CreateChannelAndSub(int num, std::vector<Channel> *channels, task_t task)
{
    for (int i = 0; i < num; i++)
    {
        // 1.创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        if (n < 0)
            exit(1);
 
        // 2.创建子进程
        pid_t id = fork();
        if (id == 0)
        {
            // child - read
            close(pipefd[1]);
            dup2(pipefd[0], 0); // 将管道的读端,重定向到标准输入
            task();             // 执行任务
            close(pipefd[0]);
            exit(0);
        }
 
        // 3.构建一个channel的名称
        std::string channel_name = "Channel-" + std::to_string(i);
        // 父进程 -- write
        close(pipefd[0]);
        // a. 子进程的pid b. 父进程关心的管道的w端
        channels->push_back(Channel(pipefd[1], id, channel_name));
    }
}

选择管道以及发送任务

cpp 复制代码
// 0 1 2 3 4 channelnum
int NextChannel(int channelnum)
{
    static int next = 0;
    int channel = next;
    next++;
    next %= channelnum;
    return channel;
}

void SendTaskCommand(Channel &channel, int taskcommand)
{
    write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}

通过channel控制子进程

cpp 复制代码
void ctrlProcessOnce(std::vector<Channel> &channels)
{
    sleep(1);
    // a.选择一个任务
    int taskcommand = SelectTask();
    // b.选择一个信道和进程
    int channel_index = NextChannel(channels.size());
    // c.发送任务
    SendTaskCommand(channels[channel_index], taskcommand);
    std::cout<< std::endl;
    std::cout << "taskcommand: " << taskcommand << " channel: "
              << channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << std::endl;
}
 
void ctrlProcess(std::vector<Channel> &channels, int times = -1)
{
    if (times > 0)
    {
        while (times--)
        {
            ctrlProcessOnce(channels);
        }
    }
    else
    {
        while (true)
        {
            ctrlProcessOnce(channels);
        }
    }
}

回收管道和子进程--->关闭所有的写端、回收子进程

cpp 复制代码
void CleanUpChannel(std::vector<Channel> &channels)
{
    for (auto &channel : channels)
    {
        channel.CloseChannel();
 
    }
    // 注意
    for (auto &channel : channels)
    {
        channel.Wait();
    }
}

设计主函数

cpp 复制代码
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " processnum " << std::endl;
    }
 
    int num = std::stoi(argv[1]);
    // 加载任务
    LoadTask();
 
    // 再组织
    std::vector<Channel> channels;
 
    // 1. 创建信道和子进程
    CreateChannelAndSub(num, &channels, work1);
 
    // 2. 通过channel控制子进程
    ctrlProcess(channels, num);
 
    // 3. 回收管道和子进程. a. 关闭所有的写端 b. 回收子进程
    CleanUpChannel(channels);
    return 0;
}

测试结果

我们发现在回收管道和子进程的过程中我们是先把所有的管道关闭结束后,再进行等待;那我们能不能关闭一个等待一个呢?

cpp 复制代码
void CleanUpChannel(std::vector<Channel> &channels)
{
    for (auto &channel : channels)
    {
        channel.CloseChannel();
        channel.Wait();
    }
}

发现进程一直卡在这里,也不结束。


随着子进程越来越多,那么前面管道的写端就会越来越多;

所以,如果我们关闭一个文件描述符,仅仅只是关闭了父进程的一个,但子进程继承的写端都没有关闭;所以此时这种情况,不能关一个,退一个;在管道内有一个引用计数属性,只要引用计数不为0,不会真正关闭管道,这样子进程也不会真正退出,进程就阻塞了;

那为什么分开关闭,先关闭完,再等待就可以了?

因为我们关闭是从上往下的,最后一个管道先释放,最后一个管道释放了,那么它曾经对应指向上一个写端也就自动关闭了,类似于递归,从上往下关,然后从下往上不断读到0的

所以我们可以尝试倒着关

cpp 复制代码
void CleanUpChannel(std::vector<Channel> &channels)
{
    int num = channels.size() -1;
    while(num >= 0)
    {
        channels[num].CloseChannel();
        channels[num--].Wait();
    }
}

如果我就想一个一个关闭呢?

因为我们是在创建子进程的时候出现了问题,所以我们要修改一下创建子进程的逻辑

因为第二次创建管道开始,第一个管道就会多出写端,因此我们只需要在第二次创建时作出修改即可;

因为第一次创建后已经被push_back了,所以在第二次创建的时候,可以把第一次的关闭了;同样后面创建依次如此

cpp 复制代码
void CreateChannelAndSub(int num, std::vector<Channel> *channels, task_t task)
{
    // BUG? --> fix bug
    for (int i = 0; i < num; i++)
    {
        // 1. 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        if (n < 0)
            exit(1);
 
        // 2. 创建子进程
        pid_t id = fork();
        if (id == 0)
        {
            if (!channels->empty())
            {
                // 第二次之后,开始创建的管道
                for(auto &channel : *channels) channel.CloseChannel();
            }
            // child - read
            close(pipefd[1]);
            dup2(pipefd[0], 0); // 将管道的读端,重定向到标准输入
            task();
            close(pipefd[0]);
            exit(0);
        }
 
        // 3.构建一个channel名称
        std::string channel_name = "Channel-" + std::to_string(i);
        // 父进程
        close(pipefd[0]);
        // a. 子进程的pid b. 父进程关心的管道的w端
        channels->push_back(Channel(pipefd[1], id, channel_name));
    }
}

命名管道的应用------使用Client&Server通信

namedPipe.hpp

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

const std::string comm_path ="./myfifo";
#define Defaultd -1
#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096

class NamedPipe{
private:
    bool OpenNamedPipe(int mode){
        _fd=open(_fifo_path.c_str(),mode);
        if(_fd<0)
            return false;
        return true;
    }
public:
    NamedPipe(const std::string &path,int who)
        :_fifo_path(path),_id(who),_fd(Defaultd)
    {
        if(_id==Creater){
            int res=mkfifo(_fifo_path.c_str(),0666);
            if(res!=0){
                perror("mkfifo");
            }
            std::cout<<"creater create named pipe"<<std::endl;
        }
    }
    bool OpenForRead(){
        return OpenNamedPipe(Read);
    }
    bool OpenForWrite(){
        return OpenNamedPipe(Write);
    }
    // const &: const std::string &XXX
    // *      : std::string *
    // &      : std::string & 
    int ReadNamedPipe(std::string *out){
        char buffer[BaseSize];
        int n=read(_fd,buffer,sizeof(buffer));
        if(n>0){
            buffer[n]=0;
            *out=buffer;
        }
        return n;
    }
    int WriteNamedPipe(const std::string &in){
        return write(_fd,in.c_str(),in.size());
    }
    ~NamedPipe(){
        if(_id==Creater){
            int res=unlink(_fifo_path.c_str());
            if(res!=0){
                perror("unlink");
            }
            std::cout<<"creater free named pipe"<<std::endl;
        }
        if(_fd!=Defaultd) close(_fd);
    }
private:
    const std::string  _fifo_path;
    int _id;
    int _fd;
};

client.cc

cpp 复制代码
#include "namedPipe.hpp"
 
// write
int main()
{
    NamePiped fifo(comm_path, User);
    if (fifo.OpenForWrite())
    {
        std::cout << "client open namd pipe done" << std::endl;
        while (true)
        {
            std::cout << "Please Enter> ";
            std::string message;
            std::getline(std::cin, message);
            fifo.WriteNamedPipe(message);
        }
    }
 
    return 0;
}

server.cc

cpp 复制代码
#include "namedPipe.hpp"
 
// server read: 管理命名管道的整个生命周期
int main()
{
    NamePiped fifo(comm_path, Creater);
    // 对于读端而言,如果我们打开文件,但是写还没来,我会阻塞在open调用中,直到对方打开
    // 进程同步
    if (fifo.OpenForRead())
    {
        std::cout << "server open named pipe done" << std::endl;
 
        sleep(3);
        while (true)
        {
            std::string message;
            int n = fifo.ReadNamedPipe(&message);
            if (n > 0)
            {
                std::cout << "Client Say> " << message << std::endl;
            }
            else if(n == 0)
            {
                std::cout << "Client quit, Server Too!" << std::endl;
                break;
            }
            else
            {
                std::cout << "fifo.ReadNamedPipe Error" << std::endl;
                break;
            }
        }
    }
 
    return 0;
}
  1. 总体架构

    • 在客户端 - 服务器(Client - Server)通信模型中,命名管道可以作为一种简单而有效的通信方式。服务器端创建命名管道,等待客户端连接并接收请求,客户端则通过打开命名管道向服务器发送请求,服务器处理请求后将结果通过命名管道返回给客户端。
  2. 服务器端实现步骤

创建命名管道
  • 首先,服务器需要创建一个命名管道。在Linux系统中,可以使用mkfifo命令或者mkfifo()系统调用创建命名管道。例如,以下是使用mkfifo()系统调用创建命名管道的C语言代码片段:
c 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define PIPE_NAME "my_named_pipe"
int main() {
    // 创建命名管道
    if (mkfifo(PIPE_NAME, 0666) == -1) {
        perror("mkfifo");
        return 1;
    }
    // 后续步骤:打开管道进行读取,接收客户端请求并处理等
    return 0;
}
  • 这里创建了一个名为my_named_pipe的命名管道,权限设置为0666(所有者、所属组和其他用户都有读写权限)。
打开命名管道进行读取
  • 服务器创建命名管道后,需要打开它进行读取操作,以接收客户端发送的请求。使用open函数打开命名管道,例如:
c 复制代码
int pipe_fd = open(PIPE_NAME, O_RDONLY);
if (pipe_fd == -1) {
    perror("open pipe for read");
    return 1;
}
  • 这样就打开了命名管道用于读取,pipe_fd是管道的读端文件描述符。
接收和处理客户端请求
  • 服务器通过read函数从命名管道读取客户端发送的请求。假设请求是一个简单的字符串,代码可能如下:
c 复制代码
char buffer[100];
int n = read(pipe_fd, buffer, sizeof(buffer));
if (n > 0) {
    buffer[n] = '\0';
    // 处理请求,例如解析请求内容并执行相应操作
    printf("收到客户端请求: %s\n", buffer);
} else if (n == 0) {
    // 管道关闭,可能客户端已经退出
    printf("客户端已关闭连接\n");
} else {
    perror("read from pipe");
    return 1;
}
  • 处理完请求后,服务器可能需要将处理结果返回给客户端。这需要打开命名管道用于写入(如果之前没有打开),并通过write函数发送结果。
关闭命名管道
  • 服务器在完成对命名管道的操作后,需要关闭管道,以释放资源。使用close函数关闭管道,例如:
c 复制代码
close(pipe_fd);
  1. 客户端实现步骤
打开命名管道进行写入
  • 客户端需要知道服务器创建的命名管道的名称,然后打开它进行写入操作,以发送请求。同样使用open函数,例如:
c 复制代码
int pipe_fd = open(PIPE_NAME, O_WRONLY);
if (pipe_fd == -1) {
    perror("open pipe for write");
    return 1;
}
  • 这里打开了命名管道用于写入,pipe_fd是管道的写端文件描述符。
发送请求
  • 客户端通过write函数向命名管道发送请求。例如,发送一个简单的字符串请求:
c 复制代码
char request[] = "这是客户端请求";
int n = write(pipe_fd, request, sizeof(request));
if (n == -1) {
    perror("write to pipe");
    return 1;
}
接收服务器响应(可选)
  • 如果客户端需要接收服务器返回的处理结果,可能需要再次打开命名管道用于读取(如果之前只用于写入),并通过read函数接收响应。
关闭命名管道
  • 客户端在完成对命名管道的操作后,也需要关闭管道,例如:
c 复制代码
close(pipe_fd);
  1. 注意事项
阻塞与非阻塞模式
  • 无论是服务器端还是客户端,在打开命名管道时可以选择阻塞或非阻塞模式。默认情况下是阻塞模式。在阻塞模式下,如果打开管道用于读取,当管道中没有数据时,read操作会一直阻塞等待数据;如果打开管道用于写入,当管道缓冲区满或者没有进程读取时,write操作会阻塞。在非阻塞模式下,readwrite操作会立即返回,并根据情况设置errno来指示操作是否可以立即执行。例如,在打开命名管道时使用O_NONBLOCK标志可以设置为非阻塞模式。
错误处理
  • 整个通信过程中可能会出现各种错误,如命名管道创建失败、打开失败、读写失败等。需要仔细处理这些错误,通过检查函数返回值和errno变量来确定错误原因,并采取相应的措施,如打印错误信息、重试操作或者退出程序。
多客户端处理(可扩展)
  • 上述示例是简单的单客户端 - 服务器通信。如果要处理多个客户端,可以使用多线程、多路复用(如selectpollepoll)等技术。例如,服务器可以使用一个线程池来处理多个客户端的请求,每个线程负责处理一个客户端的请求,通过合理的调度和资源分配来提高服务器的并发处理能力。

本篇内容到此结束,感谢大家观看。

相关推荐
宁zz15 小时前
乌班图安装jenkins
运维·jenkins
无名之逆16 小时前
Rust 开发提效神器:lombok-macros 宏库
服务器·开发语言·前端·数据库·后端·python·rust
大丈夫立于天地间16 小时前
ISIS协议中的数据库同步
运维·网络·信息与通信
cg501716 小时前
Spring Boot 的配置文件
java·linux·spring boot
暮云星影16 小时前
三、FFmpeg学习笔记
linux·ffmpeg
rainFFrain16 小时前
单例模式与线程安全
linux·运维·服务器·vscode·单例模式
GalaxyPokemon16 小时前
Muduo网络库实现 [九] - EventLoopThread模块
linux·服务器·c++
mingqian_chu17 小时前
ubuntu中使用安卓模拟器
android·linux·ubuntu
xujiangyan_17 小时前
nginx的反向代理和负载均衡
服务器·网络·nginx