[Linux] Inter-process communication--Application of anonymous pipes

foreword

In the previous blog, I learned the peripheral knowledge and use of anonymous pipes. This article will implement inter-process communication based on this knowledge. Without further ado
, let’s start today’s content immediately.

insert image description here

1. General framework

insert image description here
We create a process, take this process as the parent process, and create 5 child processes and corresponding pipelines.父进程进行写操作,子进程进行读操作,根据父进程写入的数据,进行相应的动作。

部分细节,变量在单独部分没有展示,可以查看完整代码部分

2. Assign tasks

This part we can编写在Task.hpp

#pragma once

#include<iostream>
#include<vector>
#include<unistd.h>

//函数指针
typedef void(*fun_t)();

void PrintLog()
{
    
    
    std::cout<<"pid:"<<getpid()<<" 打印日志任务,正在被执行"<<std::endl;
}

void InsertMySQL()
{
    
    
    std::cout<<"pid:"<<getpid()<<" 执行数据库任务,正在被执行"<<std::endl;
}

void NetRequest()
{
    
    
    std::cout<<"pid:"<<getpid()<<" 执行网络请求任务,正在被执行"<<std::endl;
}

//数字对应的指令
#define COMMAND_LOG 1
#define COMMAND_MYSQL 2
#define COMMAND_REQUEST 3

class Task
{
    
    
public:
    Task()
    {
    
    
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }

    void Execute(int command)
    {
    
    
        //commend是执行第几个命令
        if(command>=0&&command<funcs.size())
        {
    
    
            funcs[command]();
        }
    }

    ~Task()
    {
    
    

    }
public:
    std::vector<fun_t>funcs;
};

The way we use it 函数指针. Define a Task class, there is one inside 存储函数指针的vector, and we add the above three tasks in its constructor. Then there is an interface, by passing in one 数字command, and then executing the task corresponding to the number in the vector.

3. Create a control module

从这部分开始,我们编写在ctrlProcess.cpp中

Anonymous pipes are used for “亲戚关系”existing processes, which are commonly used 父子进程, and we need them 让子进程有父进程创建的管道, so we need them 先创建管道,然后再创建子进程, so that because of copy-on-write, the child process will inherit part of the process information of the parent process, including file descriptors of course.
Then after successfully creating a child process, the parent process must first close the reading end of the current pipe, so that it will not affect the next creation.
At the same time, because we want to create multiple child processes, and the pipeline created each time uses the same array to store its read and write file descriptors, so we use one, internal storage 子进程的名称,子进程的pid,子进程对应的管道的写端, because the parent process needs to write data to the write end . This is also in line with 先描述,再组织the thought of .

We encapsulate this part into a function to encapsulate each step of operation, which can also make the code more logical and readable.

The waitCommand function of the child process will be explained in the next part, because waitCommand reads pipeline data and belongs to the communication part.

// 用于存储子进程的pid和相对应管道的文件描述符
class EndPoint
{
    
    
    //计数器
    static int number;
public:
    EndPoint(pid_t child_id, int write_id)
        : _child_id(child_id), _write_id(write_id)
    {
    
    
        //进程名的格式:process-0[pid,fd]
        char namebuffer[64];
        snprintf(namebuffer,sizeof(namebuffer),"process-%d[%d:%d]",number++,_child_id,_write_id);
        processname=namebuffer;
    }

    std::string name() const
    {
    
    
        return processname;
    }

    ~EndPoint()
    {
    
    
    }

public:
    pid_t _child_id; // 子进程的pid
    int _write_id;   // 相对应管道的文件描述符
    std::string processname; //进程的名字 
};
int EndPoint::number=0;


// 构建控制结构,父进程写入,子进程读取
void createProcesses(vector<EndPoint> &end_points)
{
    
    
    // 1.先进行构建控制结构:父进程进行写入,子进程读取
    for (int i = 0; i < gnum; i++)
    {
    
    
        // 1.1 创建管道
        int pipefd[2] = {
    
    0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void )n; // 防止release版本将为使用的变量删除

        // 1.2 创建子进程
        pid_t id = fork();
        assert(id != -1);
        (void )id; // 防止release版本将为使用的变量删除

        if (id == 0)
        {
    
    
            // 子进程

            //将从父进程那继承来的其他进程的读端关闭
            cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端:";
            for(const auto&ep:end_points)
            {
    
    
                cout<<ep._write_id<<" ";
                close(ep._write_id);
            }
            cout<<endl;

            // 关闭自己的写端
            close(pipefd[1]);

            // 1.3 通信

            // 子进程读取“指令”,都从标准输出中获取
            // 将管道的读重定向到标准输出中
            dup2(pipefd[0], 0);

            // 1.4 子进程开始等待命令。
            WaitCommand();

            // 关闭读端然后退出子进程
            close(pipefd[0]);
            exit(0);
        }

        // 到这的一定是父进程

        // 关闭读端
        close(pipefd[0]);

        // 将新创建的子进程的fd和管道的写的文件描述符存储起来
        end_points.push_back(EndPoint(id, pipefd[1]));
    }
}

But here, we need to pay attention to one more thing, that is, after the child process is successfully created, there is still a loop

//将从父进程那继承来的其他进程的读端关闭
cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端"<<endl;
for(const auto&ep:end_points)
{
    
    
	cout<<ep._write_id<<" ";
    close(ep._write_id);
}
cout<<endl;

We will explain this step in detail in the final exit procedure.

4. Start communication

We have now created the parent-child process, and also stored the pid of the child process and the file descriptor of the write end of the corresponding pipe.
Next, we can start communicating.

父进程往管道写入

//展示面板
int ShowBoard()
{
    
    
    cout<<endl;
    cout<<"#######################################"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"# 0. 执行日志任务   1. 执行数据库任务 #"<<endl;
    cout<<"# 2. 执行请求任务   3. 退出           #"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"请选择# ";

    int command=0;
    std::cin>>command;

    return command;
}

//父进程写入
void ctrlProcess(const vector<EndPoint>&end_points)
{
    
    
    // 父进程开始发布命令
    int cnt=0;
    while(true)
    {
    
    
        //1. 选择任务
        int command=ShowBoard();
        //为3就退出
        if(command==3)
        {
    
    
            break;
        }

        if(command<0&&command>2)
        {
    
    
            cout<<"输入有误,请重新输入"<<endl;
            continue;
        }

        //2. 按顺序给子进程派发任务
        int indix=cnt++;
        cnt%=end_points.size();

        cout<<"你选择了进程:"<<end_points[indix].name()<<" | 处理"<<command<<"号任务"<<endl;

        //4. 下发任务
        write(end_points[indix]._write_id,&command,sizeof(command));
        
        sleep(1);
    }
}

The child process reads the pipe, gets the data, and executes the corresponding task

// 子进程读数据
void WaitCommand()
{
    
    
    while(true)
    {
    
    
        int command;
        //一次读取4个字节
        int n = read(0, &command, sizeof(int));
        //成功读取4字节,就执行对应的命令
        if (n == sizeof(int))
        {
    
    
            t.Execute(command);
            cout<<endl;
        }
        else if (n == 0)
        {
    
    
            //相对应的写端关闭了
            cout<<"父进程让我退出,我就退出了"<<getpid()<<endl;
            break;
        }
    }
}

5. Closing the program

When closing the program, if we want to end the sub-process, we only need to close the corresponding write terminal, and the sub-process will automatically exit the loop and end the process when it reads to the end of the file.
Then the parent process also needs to recycle the zombie state of the child process.
But here we will explain the second step. A question when assigning tasks

Why do you need the following loop

//将从父进程那继承来的其他进程的读端关闭
cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端"<<endl;
for(const auto&ep:end_points)
{
    
    
	cout<<ep._write_id<<" ";
    close(ep._write_id);
}
cout<<endl;

We know, 子进程会继承父进程所有的文件描述符, then when we create the second child process, the parent process has the write end of the first child process pipe. Therefore 第二个子进程同样会继承这个文件描述符, this leads to the fact that the more sub-processes we create, the pipeline of the previous sub-process 链接数越多, 引用计数不为1so that 顺序一个一个关闭时the writing end of the sub-process cannot be closed, and the sub-process will not read to the end of the file, but 阻塞状态will not exit process, the parent process cannot recycle the child process.
So we have three ways to solve this problem

解决方法一:
We can close the writing ends of all sub-processes at one time, and then recycle the sub-processes

	//1.关闭子进程的写端
    for(const auto&ep:end_points)
    {
    
    
        close(ep._write_id);
    }

    sleep(5);
    cout<<"父进程让所有的子进程都退出"<<endl;
    
    //2. 父进程回收子进程的僵尸状态
    for(const auto&ep:end_points)
    {
    
    
        waitpid(ep._child_id,nullptr,0);
    }

    cout<<"父进程回收了所有的子进程"<<endl;
    sleep(5);

解决方法二:
We can close the writing end of the child process backwards, and then recycle the child process

	//倒着关闭子进程的写端,再回收子进程
    for(int i=end_points.size()-1;i>=0;i--)
    {
    
    
        close(end_points[i]._write_id);
        cout<<"父进程让"<<end_points[i]._child_id<<"子进程退出"<<endl;

        waitpid(end_points[i]._child_id,nullptr,0);
        cout<<"父进程回收了"<<end_points[i]._child_id<<"子进程"<<endl;
        cout<<endl;

        sleep(1);
    }

解决方法三:
After the new child process is created, close the file descriptors of the pipelines of other child processes inherited from the parent process, which is the loop, and then
we can close the write ends one by one and recycle them sequentially.

//将从父进程那继承来的其他进程的读端关闭
cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端"<<endl;
for(const auto&ep:end_points)
{
    
    
	cout<<ep._write_id<<" ";
    close(ep._write_id);
}
cout<<endl;

//一个一个退出
for(const auto&ep:end_points)
{
    
    
	close(ep._write_id);
    cout<<"父进程让"<<ep._child_id<<"子进程退出"<<endl;

    waitpid(ep._child_id,nullptr,0);
    cout<<"父进程回收了"<<ep._child_id<<"子进程"<<endl;
    cout<<endl;

    sleep(1);
}

6. Complete code

Task.hpp

#pragma once

#include<iostream>
#include<vector>
#include<unistd.h>

//函数指针
typedef void(*fun_t)();

void PrintLog()
{
    
    
    std::cout<<"pid:"<<getpid()<<" 打印日志任务,正在被执行"<<std::endl;
}

void InsertMySQL()
{
    
    
    std::cout<<"pid:"<<getpid()<<" 执行数据库任务,正在被执行"<<std::endl;
}

void NetRequest()
{
    
    
    std::cout<<"pid:"<<getpid()<<" 执行网络请求任务,正在被执行"<<std::endl;
}

//数字对应的指令
#define COMMAND_LOG 1
#define COMMAND_MYSQL 2
#define COMMAND_REQUEST 3

class Task
{
    
    
public:
    Task()
    {
    
    
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }

    void Execute(int command)
    {
    
    
        //commend是执行第几个命令
        if(command>=0&&command<funcs.size())
        {
    
    
            funcs[command]();
        }
    }

    ~Task()
    {
    
    

    }
public:
    std::vector<fun_t>funcs;
};

ctrlProcess.cpp

#include <iostream>
#include <string>
#include <cassert>
#include <unistd.h>
#include <vector>
#include <sys/types.h>
#include <sys/wait.h>
#include "task.hpp"

using namespace std;

const int gnum = 3;
Task t; // 定义为全局的

// 用于存储子进程的pid和相对应管道的文件描述符
class EndPoint
{
    
    
    //计数器
    static int number;
public:
    EndPoint(pid_t child_id, int write_id)
        : _child_id(child_id), _write_id(write_id)
    {
    
    
        //进程名的格式:process-0[pid,fd]
        char namebuffer[64];
        snprintf(namebuffer,sizeof(namebuffer),"process-%d[%d:%d]",number++,_child_id,_write_id);
        processname=namebuffer;
    }

    std::string name() const
    {
    
    
        return processname;
    }

    ~EndPoint()
    {
    
    
    }

public:
    pid_t _child_id; // 子进程的pid
    int _write_id;   // 相对应管道的文件描述符
    std::string processname; //进程的名字 
};
int EndPoint::number=0;

// 子进程读数据
void WaitCommand()
{
    
    
    while(true)
    {
    
    
        int command;
        //一次读取4个字节
        int n = read(0, &command, sizeof(int));
        //成功读取4字节,就执行对应的命令
        if (n == sizeof(int))
        {
    
    
            t.Execute(command);
            cout<<endl;
        }
        else if (n == 0)
        {
    
    
            //相对应的写端关闭了
            cout<<"父进程让我退出,我就退出了"<<getpid()<<endl;
            break;
        }
    }
}

// 构建控制结构,父进程写入,子进程读取
void createProcesses(vector<EndPoint> &end_points)
{
    
    
    // 1.先进行构建控制结构:父进程进行写入,子进程读取
    for (int i = 0; i < gnum; i++)
    {
    
    
        // 1.1 创建管道
        int pipefd[2] = {
    
    0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void )n; // 防止release版本将为使用的变量删除

        // 1.2 创建子进程
        pid_t id = fork();
        assert(id != -1);
        (void )id; // 防止release版本将为使用的变量删除

        if (id == 0)
        {
    
    
            // 子进程

            //将从父进程那继承来的其他进程的读端关闭
            cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端:";
            for(const auto&ep:end_points)
            {
    
    
                cout<<ep._write_id<<" ";
                close(ep._write_id);
            }
            cout<<endl;

            // 关闭自己的写端
            close(pipefd[1]);

            // 1.3 通信

            // 子进程读取“指令”,都从标准输出中获取
            // 将管道的读重定向到标准输出中
            dup2(pipefd[0], 0);

            // 1.4 子进程开始等待命令。
            WaitCommand();

            // 关闭读端然后退出子进程
            close(pipefd[0]);
            exit(0);
        }

        // 到这的一定是父进程

        // 关闭读端
        close(pipefd[0]);

        // 将新创建的子进程的fd和管道的写的文件描述符存储起来
        end_points.push_back(EndPoint(id, pipefd[1]));
    }
}

int ShowBoard()
{
    
    
    cout<<endl;
    cout<<"#######################################"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"# 0. 执行日志任务   1. 执行数据库任务 #"<<endl;
    cout<<"# 2. 执行请求任务   3. 退出           #"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"#######################################"<<endl;
    cout<<"请选择# ";

    int command=0;
    std::cin>>command;

    return command;
}

void ctrlProcess(const vector<EndPoint>&end_points)
{
    
    
    // 父进程开始发布命令
    int cnt=0;
    while(true)
    {
    
    
        //1. 选择任务
        int command=ShowBoard();
        //为3就退出
        if(command==3)
        {
    
    
            break;
        }

        if(command<0&&command>2)
        {
    
    
            cout<<"输入有误,请重新输入"<<endl;
            continue;
        }

        //2. 按顺序给子进程派发任务
        int indix=cnt++;
        cnt%=end_points.size();

        cout<<"你选择了进程:"<<end_points[indix].name()<<" | 处理"<<command<<"号任务"<<endl;

        //4. 下发任务
        write(end_points[indix]._write_id,&command,sizeof(command));
        
        sleep(1);
    }
}

//回收子进程
void waitProcess(vector<EndPoint>&end_points)
{
    
    
    //如果我们创建管道后,直接再创建子进程,那么子进程将继承父进程的所有文件描述符,
    //后创建的子进程会保留指向先创建的管道的读写文件描述符
    //所以顺序同时关闭子进程的写端和回收僵尸进程,其实并没有关闭子进程的写端,因为此时其引用计数仍>0

    // //这种写法会在waitpid时堵塞,因为子进程的写端还没有关闭
    // //所以子进程的读端处于堵塞状态,不会退出
    // for(const auto&ep:end_points)
    // {
    
    
    //     close(ep._write_id);
    //     cout<<"父进程关闭了"<<ep._child_id<<"的写端"<<endl;
    //     waitpid(ep._child_id,nullptr,0);
    //     cout<<"父进程回收了"<<ep._child_id<<endl;
    // }

    //解决方法一:倒着关闭写端,回收僵尸进程
    //解决方法二:在创建新的子进程后,子进程关闭从父进程那边继承的其他子进程的读写端


    //我们只需要让父进程关闭子进程的写端,子进程的读端会读到文件尾,然后自己就退了。

    // //1.关闭子进程的写端
    // for(const auto&ep:end_points)
    // {
    
    
    //     close(ep._write_id);
    // }

    // sleep(5);
    // cout<<"父进程让所有的子进程都退出"<<endl;
    
    // //2. 父进程回收子进程的僵尸状态
    // for(const auto&ep:end_points)
    // {
    
    
    //     waitpid(ep._child_id,nullptr,0);
    // }

    // cout<<"父进程回收了所有的子进程"<<endl;
    // sleep(5);

    //倒着关闭子进程的写端,再回收子进程
    for(int i=end_points.size()-1;i>=0;i--)
    {
    
    
        close(end_points[i]._write_id);
        cout<<"父进程让"<<end_points[i]._child_id<<"子进程退出"<<endl;

        waitpid(end_points[i]._child_id,nullptr,0);
        cout<<"父进程回收了"<<end_points[i]._child_id<<"子进程"<<endl;
        cout<<endl;

        sleep(1);
    }



    //一个一个退出
    for(const auto&ep:end_points)
    {
    
    
        close(ep._write_id);
        cout<<"父进程让"<<ep._child_id<<"子进程退出"<<endl;

        waitpid(ep._child_id,nullptr,0);
        cout<<"父进程回收了"<<ep._child_id<<"子进程"<<endl;
        cout<<endl;

        sleep(1);
    }

}

int main()
{
    
    
    // 用于存储子进程的pid和相对应管道的文件描述符
    vector<EndPoint> end_points;

    // 创建控制模块
    createProcesses(end_points);
    sleep(1);
    //开始通信
    ctrlProcess(end_points);

    //回收子进程
    waitProcess(end_points);

    cout<<"程序成功退出,欢迎下次使用"<<endl;

    return 0;
}

The following are some running results

insert image description here

conclusion

This is the end of the content of this article, thank you for reading

If you think this article is helpful to you, you might as well like it to support the blogger, please, this is really important to me.
insert image description here

Guess you like

Origin blog.csdn.net/m0_72563041/article/details/130140753