【Linux】プロセス間通信 -- 無名パイプの応用

序文

前回のブログで、匿名パイプの周辺知識と使い方を学びました. この記事では、この知識に基づいてプロセス間通信を実装します. 早速、今日の
コンテンツを開始しましょう.

ここに画像の説明を挿入

1. 一般的な枠組み

ここに画像の説明を挿入
プロセスを作成し、このプロセスを親プロセスとして、5 つの子プロセスと対応するパイプラインを作成します。父进程进行写操作,子进程进行读操作,根据父进程写入的数据,进行相应的动作。

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

2. タスクを割り当てる

この部分は私たちができる编写在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;
};

私たちがそれを使用する方法函数指针Task クラスを定義します。内部に 1 つあり存储函数指针的vector、そのコンストラクターに上記の 3 つのタスクを追加します。数字command次に、 one を渡し、ベクター内の番号に対応するタスクを実行することにより、インターフェイスがあります。

3. 制御モジュールを作成する

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

匿名パイプは“亲戚关系”、一般的に使用される既存のプロセスに使用され父子进程、必要な让子进程有父进程创建的管道ので、先创建管道,然后再创建子进程コピーオンライトにより、子プロセスはファイルを含む親プロセスのプロセス情報の一部を継承します。もちろん記述子。
次に、子プロセスを正常に作成した後、親プロセスは最初に現在のパイプの読み取り側を閉じて、次の作成に影響を与えないようにする必要があります。
同時に、複数の子プロセスを作成する必要があり、毎回作成されるパイプラインは同じ配列を使用して読み取りおよび書き込みファイル記述子を格納するため、1 つの内部ストレージを使用します。これは、親プロセスがデータを書き込む必要があるためです子进程的名称,子进程的pid,子进程对应的管道的写端。書き込み終了。これも の先描述,再组织考えと一致する。

この部分を関数にカプセル化して、操作の各ステップをカプセル化します。これにより、コードがより論理的で読みやすくなります。

子プロセスのwaitCommand関数については、次の部分で説明します.waitCommandはパイプラインデータを読み取り、通信部分に属します.

// 用于存储子进程的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]));
    }
}

ただし、ここでもう 1 つ注意する必要があります。つまり、子プロセスが正常に作成された後、まだループが存在します。

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

この手順については、最終的な終了手順で詳しく説明します。

4.通信開始

これで、親子プロセスが作成され、子プロセスの pid と、対応するパイプの書き込み側のファイル記述子も保存されました。
次に、通信を開始できます。

父进程往管道写入

//展示面板
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 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. プログラムを閉じる

プログラムを閉じるとき、サブプロセスを終了したい場合は、対応する書き込みターミナルを閉じるだけでよく、サブプロセスは自動的にループを終了し、ファイルの最後まで読み取るとプロセスを終了します。
次に、親プロセスも子プロセスのゾンビ状態をリサイクルする必要があります。
しかし、ここでは2番目のステップについて説明します。

なぜ次のループが必要なのですか

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

次に子进程会继承父进程所有的文件描述符、2 番目の子プロセスを作成すると、親プロセスは最初の子プロセス パイプの書き込み終了を持ちます。したがって第二个子进程同样会继承这个文件描述符、これは、作成するサブプロセスが増えるほど、前のサブプロセスのパイプラインが発生するため链接数越多サブプロセスの書き込み側を閉じることができず、サブプロセスが最後まで読み取られないという事実につながります。親プロセスは子プロセスをリサイクルできません。したがって、この問題を解決するには3つの方法があります引用计数不为1顺序一个一个关闭时阻塞状态

解决方法一:
一度にすべてのサブプロセスの書き込み終了を閉じてから、サブプロセスをリサイクルできます

	//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);
    }

解决方法三:
新しい子プロセスが作成されたら、親プロセスから継承された他の子プロセスのパイプラインのファイル記述子を閉じます。これがループです。その後、書き込み終了を 1 つずつ閉じて、順次リサイクルします

//将从父进程那继承来的其他进程的读端关闭
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. 完全なコード

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;
}

以下は実行結果の一部です

ここに画像の説明を挿入

結論

以上で、この記事の内容は終わりです。お読みいただきありがとうございます。

この記事が役立つと思われる場合は、ブロガーをサポートすることをお勧めします。これは私にとって非常に重要です.
ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/m0_72563041/article/details/130140753