Dossier d'apprentissage Linux - Pipeline de communication inter-processus (1)


1. Introduction à la communication inter-processus

Ce que j’ai appris auparavant est un processus unique, comment fonctionnent plusieurs processus ?

1. Objectif

Transfert de données : un processus doit envoyer ses données à un autre processus.
Partage de ressources : la même ressource est partagée entre plusieurs processus.
Événement de notification : un processus doit envoyer un message à un autre processus ou à un groupe de processus, l'informant (eux) qu'un événement s'est produit (par exemple,
notifier le processus parent lorsque le processus se termine).
Contrôle de processus : certains processus souhaitent contrôler complètement l'exécution d'un autre processus (comme le processus de débogage). À ce stade, le processus de contrôle espère pouvoir intercepter
tous les pièges et exceptions d'un autre processus et pouvoir connaître son état. changements dans le temps.

Les processus sont indépendants, ce qui augmente le coût de la communication. La condition préalable à la communication entre deux processus différents est de permettre aux deux processus de voir la même ressource. Ceci est fourni directement ou indirectement par le système d’exploitation.

Quel que soit le moyen de communication, différents processus doivent d'abord voir la même ressource, puis une partie écrit et l'autre lit pour terminer la communication.

2. Développement

Communication interprocessus Pipes
System V Communication interprocessus
POSIX

2. Pipeline

1. Principe

Les tuyaux sont la forme la plus ancienne de communication interprocessus sous Unix.

Nous appelons un flux de données d'un processus à un autre un « pipe ».

Plusieurs processus peuvent être créés en même temps, connectés via des pipelines, et leurs processus parents sont également les mêmes, bash.

Un tube est également un fichier. Un fichier ouvre le tube en écriture et redirige sa sortie standard vers le tube, et un fichier à l'autre extrémité ouvre le tube en lecture et redirige son entrée standard vers le tube.

Lorsque le processus de fichier démarre, il y a un tableau de descripteurs de fichiers dans la structure, pointant vers chaque fichier. De plus, un canal anonyme sera ouvert. Il s'agit d'un fichier de mémoire pure fourni par le système d'exploitation et n'a pas besoin d'actualiser le fichier. contenu sur le disque. Grâce à l'appel de processus, ce canal anonyme ouvrira le même fichier en lecture et en écriture ; le processus actuel se divisera, copiera sa propre table de descripteur de fichier dans le processus enfant, le contenu spécifique sera modifié en conséquence et les fichiers ouverts devront être copié ? Ne pas copier. Mais cela n'a pas d'importance, à cause de la table de description de fichier, le processus enfant pointe toujours vers le fichier créé par le processus parent, y compris le canal anonyme. Cela équivaut en fait à une copie superficielle. Les fichiers pointés par les processus parent et enfant sont les mêmes. Par conséquent, le père et le fils pointent vers le pipeline anonyme. Si une partie change, l'autre partie peut obtenir de nouvelles données, mais pour le moment, le pipeline ne prend en charge que la communication unidirectionnelle. La prochaine chose à faire est de déterminer le flux de données. direction et fermez les fds inutiles. Le système d'exploitation désactivera à la fois la méthode d'écriture du processus enfant et la méthode de lecture du processus parent, de sorte que le parent écrive et l'enfant lise, formant ainsi un pipeline.

Les pipelines ne peuvent communiquer que dans une seule direction. Si vous souhaitez une communication bidirectionnelle, définissez deux pipelines.

2. Implémentation simple de la simulation

Code simple, le processus enfant envoie un message au processus parent, qui est une sorte de pipeline. Toute l'étape consiste à créer un pipeline, à créer un sous-processus, à fermer les fds inutiles et à démarrer la communication.

Créez un pipeline avec la fonction pipe, int pipe(int fd[2]), à l'aide du fichier d'en-tête unistd.h. pipefd est un paramètre de sortie, le système créera un tube, puis transmettra les descripteurs de fichier des extrémités de lecture et d'écriture au tableau pipefd, et il y a deux entiers dans le tableau. La fonction pipe renvoie -1 en cas d'échec, le code d'erreur est défini et renvoie 0 en cas de succès.

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

int main()
{
    
    
    //一定要保证不同进程看到同一份资源
    int pipefd[2] = {
    
    0};
    //1、创建管道
    int n = pipe(pipefd);
    if(n < 0)
    {
    
    
        std::cout << "pipe error, " << errno << ":" << strerror(errno) << std::ednl;
        return 1;
    }
    std::cout << "pipefd[0]: " << pipefd[0] << std::endl;//读端
    std::cout << "pipefd[0]: " << pipefd[1] << std::endl;//写端
    //2、创建子进程
    pid_t id = fork();
    assert(id != -1)//断言或者判断都行
    //子进程
    if(id == 0)
    {
    
    
        //3、关闭不需要的fd,让父进程进行读取,让子进程进行写入
        close(pipdfd[0]);
        //4、开始通信
        const std::string namestr = "hello, 我是子进程";//要传变化的数据,来证明是通信过来的数据
        int cnt = 1;
        char buffer[1024];
        while(true)
        {
    
    
            snprintf(buffer, sizeof(buffer), "%s, 计数器: %d, 我的PID: %d\n", namestr.c_str(), cnt++, getpid());
            write(pipefd[1], buffer, strlen(buffer));
            sleep(1);
        }
        close(pipefd[1]);
        exit(0);
    }
    //父进程
    //3、关闭不需要的fd,让父进程进行读取,让子进程进行写入
    close(pipefd[1]);
    //4、开始通信
    char buffer[1024];
    while(true)
    {
    
    
        int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
    
    
            buffer[n] = '\0';
            std::cout << "我是父进程, child give me message: " << buffer << std::endl;
        }
    }
    close(pipefd[0]);
    return 0;
}

pipeline

3. Résumé

Le système peut faire la distinction entre les fichiers pipeline et les fichiers ordinaires.

caractéristiques

1. Communication unidirectionnelle, une sorte de semi-duplex, ce qui signifie que les deux parties travaillent alternativement ; full-duplex signifie que les deux parties peuvent travailler en même temps 2. L'essence
d'un pipeline est un fichier, car le cycle de vie du descripteur de fichier et le fichier suit le processus, donc le cycle de vie du pipeline Le cycle de vie suit également le processus. Lorsque le processus parent-enfant se termine, le descripteur de fichier précédemment fermé reviendra à sa position précédente.
3. Les processus parent-enfant peuvent communiquer et il y a un héritage. La communication pipeline est généralement utilisée pour les processus ayant une relation « de sang », souvent utilisée pour les processus parent-enfant. La communication entre les processus frères est également possible. Lorsque pipe ouvre un tube, vous n'avez pas besoin de vous soucier du fichier ouvert, tant que vous obtenez deux descripteurs de fichier, car pipe ouvre un tube anonyme. 4.
Dans la communication pipe, le nombre d'écritures et le nombre de les temps de lecture ne correspondent pas strictement. J'en ai écrit 7 mais je n'en ai lu que 1 pour les lire tous. La lecture et l’écriture ne sont pas fortement corrélées. La lecture est orientée flux d'octets et la lecture dépend uniquement du nombre d'octets à lire.5.
Le pipeline a une certaine capacité de coordination, de sorte que la lecture et l'écriture peuvent communiquer selon certaines étapes ---- synchronisation autonome mécanisme

Scènes

1. Si nous lisons toutes les données du pipeline et que l'autre partie n'écrit pas, le lecteur ne peut qu'attendre
2. Une fois la fin de l'écriture terminée (généralement 65535/65536, environ 64 Ko), il ne continuera pas à écrire. le lecteur lit. Vous ne pouvez pas écrire sleep dans le processus enfant, mais le processus parent sleep(10), c'est-à-dire que le processus enfant écrit de manière folle, tandis que le processus parent lit lentement, en fonction du phénomène réel 3. Si la fin d'écriture est fermée, après En lisant les données du pipeline, relisez
-les. Il renverra 0, indiquant que la fin du fichier a été lue.
4. La fin de l'écriture continue d'écrire et la fin de la lecture est fermée, donc l'écriture n'a plus de sens, le système d'exploitation ne maintiendra pas processus dénués de sens, inefficaces et gaspilleurs de ressources, le processus sera donc tué directement. abandonnez ce processus. Le système terminera le processus via le signal, SIGPIPE -13 ferme le processus

La quantité de données lues et écrites en une seule lecture et écriture du pipeline est une macro PIPE_BUF, qui peut être visualisée avec man 7. La quantité de données écrites par le pipeline à la fois doit être inférieure à ce PIPE_BUF et sa taille est de 4096 octets. Lorsqu'elle est plus petite, l'opération d'écriture est atomique. Cela expliquera ce qui est atomique plus tard.

3. Processus de contrôle - canal anonyme

Un processus parent peut avoir plusieurs processus enfants, et chaque processus enfant communique avec le processus parent via un pipeline. Si le processus parent n'écrit pas de données dans le pipeline, le processus enfant se bloquera, et si les données sont écrites, le processus enfant reviendra à l'état de fonctionnement. Le processus parent peut réveiller le processus enfant en écrivant des messages spécifiques au processus enfant, et même laisser le processus enfant effectuer certaines tâches de manière dirigée. Le processus parent décrira d'abord puis organisera les pipelines et les processus créés par lui-même.

Le processus enfant héritera du descripteur de fichier du processus parent lors de sa création, de sorte que le premier processus parent aura de nombreux processus enfants connectés, ce qui prête à confusion. La structure réelle à réaliser est un à un, une paire de processus parent-enfant possède son propre pipeline indépendant sans être affectée par d'autres processus.

Étape par étape pour concevoir cette structure

écrire un début

#include <iostream>
#include <string>
#include <unistd.h>
#include <cassert>
using namespace std;

const int gnum = 5;//子进程数量

int main()
{
    
    
    //1、先进行构建控制结构,1父多子,每个子都有自己的管道
    //父写子读
    for(int i = 0; i < gnum; i++)
    {
    
    
        //1.1 创建管道
        int pipefd[2] = {
    
    0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;
        //1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        if(id == 0)//子进程,读
        {
    
    
            //1.3 关闭不要的fd
            close(pipefd[1]);
            //...
            close(pipefd[0]);
            exit(0);
        }
        //父进程,写
        //1.3 关闭不要的fd
        close(pipefd[0]);
    }
    return 0;
}

Chaque cycle, pipefd doit être recréé, et tout est remis à 0, donc pour le processus parent, c'est comme faire une action complètement répétée, et il ne peut pas faire la distinction entre le processus enfant et le pipeline correspondant. Nous devons donc faire savoir au processus parent quel canal appartient à quel processus enfant, peu importe où il se trouve. Ici, nous écrivons une classe, et l'insérons dans le vecteur construit par cette classe après chaque boucle, afin que les résultats de chaque communication soient préservés.

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
using namespace std;

const int gnum = 5;//子进程数量

class EndPoint
{
    
    
public:
    pid_t child_id;//哪一个子进程
    int _write_fd;//哪一个管道
public:
    EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
    {
    
    }
    ~EndPoint()
    {
    
    }
};

int main()
{
    
    
    vector<EndPoint> end_pints;
    //1、先进行构建控制结构,1父多子,每个子都有自己的管道
    //父写子读
    for(int i = 0; i < gnum; i++)
    {
    
    
        //1.1 创建管道
        int pipefd[2] = {
    
    0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;
        //1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        if(id == 0)//子进程,读
        {
    
    
            //1.3 关闭不要的fd
            close(pipefd[1]);
            //让所有的子进程都从标准输入中读
            //1.3.1 重定向
            dup2(pipefd[0], 0);
            //1.3.2 子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }
        //父进程,写
        //1.3 关闭不要的fd
        close(pipefd[0]);
        //1.4 将新的子进程和他的管道写端,构建对象
        end_points.push_back(EndPoint(id, pipefd[1]));
    }
    //2. 循环结束,我们已经拿到了所有子进程和他的管道
    return 0;
}

Maintenant, mettez cette partie dans une fonction createProcess, le paramètre de la fonction est le vecteur. Et écrivez la fonction WaitCommand qui contrôle le processus enfant pour qu'il attende l'entrée.

code actuel

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
using namespace std;

const int gnum = 5;//子进程数量

class EndPoint
{
    
    
public:
    pid_t child_id;//哪一个子进程
    int _write_fd;//哪一个管道
public:
    EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
    {
    
    }
    ~EndPoint()
    {
    
    }
};

void WaitCommand()
{
    
    
    ;
}

void createProcess(vector<EndPoint>* end_points)
{
    
    
    //1、先进行构建控制结构,1父多子,每个子都有自己的管道
    //父写子读
    for(int i = 0; i < gnum; i++)
    {
    
    
        //1.1 创建管道
        int pipefd[2] = {
    
    0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;
        //1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        if(id == 0)//子进程,读
        {
    
    
            //1.3 关闭不要的fd
            close(pipefd[1]);
            //让所有的子进程都从标准输入中读
            //1.3.1 重定向
            dup2(pipefd[0], 0);
            //1.3.2 子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }
        //父进程,写
        //1.3 关闭不要的fd
        close(pipefd[0]);
        //1.4 将新的子进程和他的管道写端,构建对象
        end_points->push_back(EndPoint(id, pipefd[1]));
    }
}

int main()
{
    
    
    vector<EndPoint> end_pints;
    createProcess(&end_points);
    //2. 循环结束,我们已经拿到了所有子进程和他的管道
    return 0;
}

Ouvrez un nouveau fichier avec le suffixe .hpp et écrivez-y les tâches effectuées par le processus enfant.

Tâche.hpp

#pragma once

#include <iostream>
#include <vector>

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

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

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

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

//规定每个command都必须是4字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQUEST 2

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

    void Execute(int command)
    {
    
    
        if(command >= 0 && command < funcs.size()) funcs[command]();
    }

    ~Task()
    {
    
    }

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

Revenez au fichier CtrlProcess.cc

void WaitCommand()
{
    
    
    while(true)
    {
    
    
        int command;
        int n = read(0, &command, sizeof(char));
        if(n == sizeof(int))
        {
    
    
            t.Execute(command);
        }
        else if(n == 0)//子进程要退出
        {
    
    
            break;
        }
        else//有异常,就需要 
        {
    
    
            break;
        }
    }
}

Définissez un objet Task global t et stipulez que chaque commande d'entrée est en unités de 4 octets. Une fois tout cela écrit, le processus parent-enfant a établi le pipeline et géré tous les pipelines. Puis, une fois la fonction CreateProcess terminée, le processus parent continue de s'exécuter et peut à ce moment envoyer un message au processus enfant. Vous pouvez imprimer le pid du processus enfant correspondant dans la phrase imprimée par chaque tâche dans Task, et le fichier d'en-tête est unistd.h.

int main()
{
    
    
    vector<EndPoint> end_pints;
    createProcess(&end_points);
    //2. 循环结束,我们已经拿到了所有子进程和他的管道
    int num = 0;
    while(true)
    {
    
    
        //1. 选择任务
        int command = COMMAND_LOG;
        //2. 选择进程
        int index = rand() % end_points.size();
        //3. 下发任务
        write(end_points[index]._write_fd, &command, sizeof(command));
        sleep(1);
    }
    return 0;
}

Continuez à améliorer le code. Laissez les sous-processus sélectionner les tâches dans l'ordre et les modifier dans le principal ; chaque sous-processus a un nom ; contrôler le numéro de commande d'entrée, ressaisir s'il est erroné et saisir 3 pour quitter dans son ensemble ; enfin, tous sortent les problèmes doivent être traités. Si le processus parent écrit une partie des données puis se ferme, alors le processus enfant se terminera également après la lecture. Dans la fonction waitcommand, le processus enfant s'arrêtera lorsqu'il lira 0, puis il ira à la fonction createProcess.Le processus enfant terminera sa propre fin de lecture, nous fermons donc simplement la fin d'écriture du processus parent, puis recyclons le processus enfant.

code global

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"
using namespace std;

const int gnum = 3;//子进程数量
Task t;

class EndPoint
{
    
    
private:
    static int number;
public:
    pid_t child_id;//哪一个子进程
    int _write_fd;//哪一个管道
    string processname;
public:
    EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
    {
    
    
        char namebuffer[64];
        snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);//格式化形式输入到buffer中
        processname = buffer;    
    }

    string name() const
    {
    
    
        return processname;
    }

    ~EndPoint()
    {
    
    }
};

int EndPoint::number = 0;

void WaitCommand()
{
    
    
    while(true)
    {
    
    
        int command;
        int n = read(0, &command, sizeof(char));
        if(n == sizeof(int))
        {
    
    
            t.Execute(command);
        }
        else if(n == 0)
        {
    
    
            cout << "父进程让我退出,我就退出: " << getpid() << endl;
            break;
        }
        else
        {
    
    
            break;
        }
    }
}

void createProcess(vector<EndPoint>* end_points)
{
    
    
    //1、先进行构建控制结构,1父多子,每个子都有自己的管道
    //父写子读
    for(int i = 0; i < gnum; i++)
    {
    
    
        //1.1 创建管道
        int pipefd[2] = {
    
    0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;
        //1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        if(id == 0)//子进程,读
        {
    
    
            //1.3 关闭不要的fd
            close(pipefd[1]);
            //让所有的子进程都从标准输入中读
            //1.3.1 重定向
            dup2(pipefd[0], 0);
            //1.3.2 子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }
        //父进程,写
        //1.3 关闭不要的fd
        close(pipefd[0]);
        //1.4 将新的子进程和他的管道写端,构建对象
        end_points->push_back(EndPoint(id, pipefd[1]));
    }
}

int ShowBoard()
{
    
    
    cout << "*******************************************" << endl;
    cout << "|   0. 执行日志任务    1. 执行数据库任务    |" << endl;
    cout << "|   2. 执行请求任务    3. 退出             |" << endl;
    cout << "*******************************************" << endl;
    cout << "请选择: ";
    int command = 0;
    cin >> command;
    return command;
}

void ctrlProcess(const vector<EndPoint> &end_pints)
{
    
    
    //2.1 我们可以写成自动化的,也可以写成交互式的
    int num = 0;
    int cnt = 0;
    while(true)
    {
    
    
        //1. 选择任务
        int command = ShowBoard();
        if(command == 3) break;//输入3就退出,再回到main处
        if(command < 0 || command > 2)
        {
    
    
            cout << "没有对应任务" << endl;
            continue;
        }
        //2. 选择进程
        int index = cnt++;//按照顺序来选择任务
        cnt %= end_points.size();
        string name = end_points[index].name();
        cout << "选择了进程: " << name << " | 处理任务" << command << endl; 
        //3. 下发任务
        write(end_points[index]._write_fd, &command, sizeof(command));
    }
}

void waitProcess(const vector<EndPoint> &end_points)
{
    
    
    //1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
    for(const auto &ep : end_points) close(ep._write_fd);
    cout << "父进程让所有的子进程全部退出" << endl;
    sleep(10);
    //2. 父进程要回收子进程的僵尸状态
    for(const auto &ep : end_points) waitpid(ep._child_id, nullptr, 0);
    cout << "父进程回收了所有的子进程" << endl;
    sleep(10);
}

int main()
{
    
    
    vector<EndPoint> end_pints;
    createProcess(&end_points);
    //2. 循环结束,我们已经拿到了所有子进程和他的管道
    ctrlProcess(&end_points);
    //3. 处理所有的退出问题
    waitProcess(end_points);
    return 0;
}

Maintenant, la fonction waitProcess pose problème. La fonction transforme deux boucles en une seule boucle et le processus enfant sera recyclé directement après la sortie, vous constaterez donc que le programme ne peut pas se terminer. Le problème se produit dans la fonction createProcess.

void createProcess(vector<EndPoint>* end_points)
{
    
    
    //1、先进行构建控制结构,1父多子,每个子都有自己的管道
    //父写子读
    for(int i = 0; i < gnum; i++)
    {
    
    
        //1.1 创建管道
        int pipefd[2] = {
    
    0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;
        //1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        if(id == 0)//子进程,读
        {
    
    
            //1.3 关闭不要的fd
            close(pipefd[1]);
            //让所有的子进程都从标准输入中读
            //1.3.1 重定向
            dup2(pipefd[0], 0);
            //1.3.2 子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }
        //父进程,写
        //1.3 关闭不要的fd
        close(pipefd[0]);
        //1.4 将新的子进程和他的管道写端,构建对象
        end_points->push_back(EndPoint(id, pipefd[1]));
    }
}

Bien que cela semble être un cycle, chaque fois qu'un processus enfant est créé et que le pipeline correspondant est établi, le processus enfant fork héritera du fichier ouvert par le processus parent, c'est-à-dire que le dernier pipeline sera hérité et le processus enfant pointera vers ce pipeline, à son tour. Par analogie, l'ensemble de la structure n'est pas du tout indépendant. Pour résoudre ce problème, la première méthode consiste à quitter le recyclage à l'envers. Lorsque le processus enfant se termine, il fermera ses propres terminaux de lecture et d'écriture et ne pointera pas vers le pipeline ci-dessus.

void waitProcess(const vector<EndPoint> &end_points)
{
    
    
    //1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
    for(int end = end_points.size() - 1; end >= 0; end--)
    {
    
    
        cout << "父进程让所有的子进程全部退出: " << end_points[end]._child_id << endl;
        close(end_points[end]._write_fd);
        //2. 父进程要回收子进程的僵尸状态
        waitpid(end_points[end]._child_id, nullptr, 0);
        cout << "父进程回收了所有的子进程: " << end_points[end]._child_id << endl;
    }
    sleep(10);
}

Mais si nous voulons quand même construire une structure idéale, nous devons commencer par créer. La fin d'écriture du processus parent n'est pas fermée à chaque fois. À la fin de ce cycle, nous sauvegardons la fin d'écriture. Lors du prochain processus enfant, il héritera de la fin d'écriture du processus parent ci-dessus, puis dans le processus enfant Avant de faire quoi que ce soit, fermez tous les ports d'écriture précédemment stockés.

void createProcess(vector<EndPoint>* end_points)
{
    
    
    vector<int> fds;
    //1、先进行构建控制结构,1父多子,每个子都有自己的管道
    //父写子读
    for(int i = 0; i < gnum; i++)
    {
    
    
        //1.1 创建管道
        int pipefd[2] = {
    
    0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;
        //1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        if(id == 0)//子进程,读
        {
    
    
            //这里可以只写一个close,其他的打印语句是为了更好地观察
            cout << getpid() << " 子进程关闭父进程对应的写端: ";
            for(auto &fd : fds) 
            {
    
    
                cout << fd << " ";
                close(fd);
            }
            cout << endl;
            //1.3 关闭不要的fd
            close(pipefd[1]);
            //让所有的子进程都从标准输入中读
            //1.3.1 重定向
            dup2(pipefd[0], 0);
            //1.3.2 子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }
        //父进程,写
        //1.3 关闭不要的fd
        close(pipefd[0]);
        //1.4 将新的子进程和他的管道写端,构建对象
        end_points->push_back(EndPoint(id, pipefd[1]));
        fds.push_back(pipefd[1]);
    }
}

void waitProcess(const vector<EndPoint> &end_points)
{
    
    
    //1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
    for(int end = end_points.size() - 1; end >= 0; end--)
    {
    
    
        cout << "父进程让所有的子进程全部退出: " << end_points[end]._child_id << endl;
        close(end_points[end]._write_fd);
        //2. 父进程要回收子进程的僵尸状态
        waitpid(end_points[end]._child_id, nullptr, 0);
        cout << "父进程回收了所有的子进程: " << end_points[end]._child_id << endl;
    }
    sleep(10);
}

tous les codes

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"
using namespace std;

const int gnum = 3;//子进程数量
Task t;

class EndPoint
{
    
    
private:
    static int number;
public:
    pid_t child_id;//哪一个子进程
    int _write_fd;//哪一个管道
    string processname;
public:
    EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
    {
    
    
        char namebuffer[64];
        snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);//格式化形式输入到buffer中
        processname = buffer;    
    }

    string name() const
    {
    
    
        return processname;
    }

    ~EndPoint()
    {
    
    }
};

int EndPoint::number = 0;

void WaitCommand()
{
    
    
    while(true)
    {
    
    
        int command;
        int n = read(0, &command, sizeof(char));
        if(n == sizeof(int))
        {
    
    
            t.Execute(command);
        }
        else if(n == 0)
        {
    
    
            cout << "父进程让我退出,我就退出: " << getpid() << endl;
            break;
        }
        else
        {
    
    
            break;
        }
    }
}

void createProcess(vector<EndPoint>* end_points)
{
    
    
    vector<int> fds;
    //1、先进行构建控制结构,1父多子,每个子都有自己的管道
    //父写子读
    for(int i = 0; i < gnum; i++)
    {
    
    
        //1.1 创建管道
        int pipefd[2] = {
    
    0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;
        //1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        if(id == 0)//子进程,读
        {
    
    

            cout << getpid() << " 子进程关闭父进程对应的写端: ";
            for(auto &fd : fds) 
            {
    
    
                cout << fd << " ";
                close(fd);
            }
            cout << endl;
            //1.3 关闭不要的fd
            close(pipefd[1]);
            //让所有的子进程都从标准输入中读
            //1.3.1 重定向
            dup2(pipefd[0], 0);
            //1.3.2 子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }
        //父进程,写
        //1.3 关闭不要的fd
        close(pipefd[0]);
        //1.4 将新的子进程和他的管道写端,构建对象
        end_points->push_back(EndPoint(id, pipefd[1]));
        fds.push_back(pipefd[1]);
    }
}

int ShowBoard()
{
    
    
    cout << "*******************************************" << endl;
    cout << "|   0. 执行日志任务    1. 执行数据库任务    |" << endl;
    cout << "|   2. 执行请求任务    3. 退出             |" << endl;
    cout << "*******************************************" << endl;
    cout << "请选择: ";
    int command = 0;
    cin >> command;
    return command;
}

void ctrlProcess(const vector<EndPoint> &end_pints)
{
    
    
    //2.1 我们可以写成自动化的,也可以写成交互式的
    int num = 0;
    int cnt = 0;
    while(true)
    {
    
    
        //1. 选择任务
        int command = ShowBoard();
        if(command == 3) break;//输入3就退出,再回到main处
        if(command < 0 || command > 2)
        {
    
    
            cout << "没有对应任务" << endl;
            continue;
        }
        //2. 选择进程
        int index = cnt++;//按照顺序来选择任务
        cnt %= end_points.size();
        string name = end_points[index].name();
        cout << "选择了进程: " << name << " | 处理任务" << command << endl; 
        //3. 下发任务
        write(end_points[index]._write_fd, &command, sizeof(command));
    }
}

void waitProcess(const vector<EndPoint> &end_points)
{
    
    
    //1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
    for(int end = end_points.size() - 1; end >= 0; end--)
    {
    
    
        cout << "父进程让所有的子进程全部退出: " << end_points[end]._child_id << endl;
        close(end_points[end]._write_fd);
        //2. 父进程要回收子进程的僵尸状态
        waitpid(end_points[end]._child_id, nullptr, 0);
        cout << "父进程回收了所有的子进程: " << end_points[end]._child_id << endl;
    }
    sleep(10);
}

int main()
{
    
    
    vector<EndPoint> end_pints;
    createProcess(&end_points);
    //2. 循环结束,我们已经拿到了所有子进程和他的管道
    ctrlProcess(&end_points);
    //3. 处理所有的退出问题
    waitProcess(end_points);
    return 0;
}

tuyau anonyme

4. Tubes nommés

Les canaux anonymes ont des limites et ne peuvent être utilisés que pour la communication entre des processus liés au sang. Pour permettre la communication entre deux processus inconnus, des canaux nommés sont nécessaires.

Pour créer un canal nommé, vous devez utiliser la commande mkfifo. Les paramètres entre parenthèses sont les paramètres du canal nommé, et fifo signifie premier entré, premier sorti.

insérer la description de l'image ici

Son type de fichier commence par p, indiquant qu'il s'agit d'un fichier pipeline.

Maintenant, écrivez une commande echo "string" > fifo pour y écrire du contenu, mais le curseur s'arrêtera au début de la ligne suivante et continuera de clignoter. En effet, le fichier fifo n'est qu'un symbole et les éléments qui y sont écrits le seront. n'existe pas réellement sur le disque. , écrivez uniquement dans le fichier pipe, nous pouvons utiliser cat pour lire, il lit à partir de l'écran par défaut, cat < fifo lira à partir du fichier pipe, puis imprimera sur l'écran. Même si vous écrivez un code qui continue à afficher du contenu, vous pouvez ouvrir une autre fenêtre et utiliser cat pour obtenir le contenu.

1. Principe

J'ai écrit sur le comptage de références dans la partie lien physique du précédent blog IO de base, et le comptage de références est également utilisé dans les canaux nommés. Si un fichier sur le disque n'est pas ouvert, il restera sur le disque ; après ouverture, il aura sa propre structure de fichier, qui contient divers paramètres, y compris les décomptes de références ; une fois que l'opérateur aura créé un processus, il y aura une description du fichier. Table des symboles et autres choses, le processus peut ouvrir le fichier dans la mémoire, et le nombre de références devient 1 à ce moment ; si un autre processus est ouvert et que le même fichier est ouvert, le système n'ouvrira pas une structure de ce fichier, mais ces deux processus pointent vers la même structure de fichiers, le décompte devient 2, les processus disparaissent un par un et le décompte passe de 2 à 0. Mais comment garantir que deux processus ouvriront le même fichier ? Pour rendre le fichier unique, le chemin du fichier + le nom du fichier sont tous identiques.

2. Implémentation des simulations

Créez un fichier pipeline, laissez les processus de fin de lecture et d'écriture ouvrir le fichier en fonction de leurs propres besoins, puis démarrez la communication.

Nous définissons deux fichiers qui doivent être écrits afin de former deux programmes exécutables.

comm.hpp

#pragma once
#include <iostream>
#include <string>

#define NUM 1024

const std::string fifoname = "./fifo";
uint32_t mode = 0666;

Makefile

.PHONY:all
all:Server Client

Server:Server.cc
    g++ -0 Server Server.cc
Client:Client.cc
    g++ -0 Client Client.cc

.PHONY:clean
clean:
    rm -f Server Client

Serveur.cc

#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;


int main()
{
    
    
    //1、创建管道文件,只需要一次创建
    umask(0);//不影响系统默认设置,只会影响当前进程
    int n = mkfifo(fifoname.c_str(), mode);
    if(n != 0)
    {
    
    
        cout << errno << " : " << strerror(errno) << endl;
        return 1;
    }
    //2、让服务端开启管道文件
    cout << "create fifo success, begin ipc" << endl;
    int rfd = open(fifoname.c_str(), O_RDONLY);
    if(rfd < 0)
    {
    
    
        cout << errno << " : " << strerror(errno) << endl;
        return 2;
    }
    cout << "open fifo success, begin ipc" << endl;
    //3、通信
    char buffer[NUM];
    while(true)
    {
    
    
        buffer[0] = 0;//C风格的字符串只要第一个位置是0,那就是空字符串
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);//不一定会发字符串,也有可能发4字节数据流,只是这里就看作字符串
        if(n > 0)
        {
    
    
            buffer[n] = 0;
            cout << "client# " << buffer << endl;           
        }
        else if (n == 0)//写端关闭,读端就会读到0
        {
    
    
            cout << "client quit, me too" << endl;
            break;
        }
        else
        {
    
    
            cout << errno << " : " << strerror(errno) << endl;
            break;
        }
    }
    //关闭不要的fd
    close(rfd);
    unlink(fifoname.c_str());//它会自己去除所有引用计数,退出管道文件
    return 0;
}

Client.cc

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cassert>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;

int main()
{
    
    
    //1、需不需要创建管道文件?不需要,只需要打开对应的文件即可
    int wfd = open(fifoname.c_str(), O_WRONLY);
    if(wfd < 0)
    {
    
    
        cerr << errno << " : " << strerror(errno) << endl;
        return 1;
    }
    //通信
    char buffer[NUM];
    while(true)
    {
    
    
        cout << "请输入你的消息: ";
        char* msg = fgets(buffer, sizeof(buffer), stdin);//sizeof(buffer)不用减1,因为C语言接口会自动处理成字符串,如果是系统接口就需要减1
        //但有时候会混淆,所以也可以无脑减1,代码风格统一。
        assert(msg);
        (void)msg;
        ssize_t n = write(wfd, buffer, sizeof(buffer));//虽然这是系统接口,不加1就得不到最后的\0,我们只需要获取字符串内容即可
        assert(n > 0);
        (void)n;
    }
    close(wfd);
    return 0;
}

Ouvrez deux fenêtres pour observer le client et le serveur. Après avoir commencé à s'exécuter, le serveur sera bloqué à l'ouverture du pipeline, car il n'ouvre que la fin de lecture et le client doit ouvrir la fin d'écriture pour continuer à s'exécuter, c'est-à-dire créer uniquement le fichier au début, après avoir ./ client, Client.cc démarre, puis le serveur imprimera le fichier ouvert... ; le programme peut communiquer normalement, mais s'il y a un espace à chaque fois qu'une instruction est saisie, le serveur recevra également l'espace, ce qui provoquera un ligne vide sous chaque ligne, nous sommes donc dans Client là pour le changer.

        cout << "请输入你的消息: ";
        char* msg = fgets(buffer, sizeof(buffer), stdin);//sizeof(buffer)不用减1,因为C语言接口会自动处理成字符串,如果是系统接口就需要减1
        //但有时候会混淆,所以也可以无脑减1,代码风格统一。
        assert(msg);
        (void)msg;        
        buffer[strlen(buffer) - 1] = 0;
        ssize_t n = write(wfd, buffer, sizeof(buffer));//虽然这是系统接口,不加1就得不到最后的\0,我们只需要获取字符串内容即可
        assert(n > 0);
        (void)n;

Si vous entrez une chaîne vide, le serveur se fermera. En effet, la dernière assertion du code ci-dessus échouera à l'assertion, puis le serveur se fermera après que n soit 0 après l'avoir reçu, il est donc écrit comme assert(n >= 0).

Le lien ci-dessous a été modifié et changé pour s'imprimer automatiquement sans taper d'espaces.

tuyau nommé

Finition.

Je suppose que tu aimes

Origine blog.csdn.net/kongqizyd146/article/details/129912113
conseillé
Classement