Comunicación de canalización para comunicación de procesos de Linux.

Todos sabemos que los procesos son independientes entre sí y, para transmitir datos entre procesos, existe una comunicación entre procesos. La comunicación entre procesos se divide en tres tipos: el primer tipo es la comunicación canalizada basada en el sistema de archivos, el segundo tipo es la comunicación local basada en el sistema v estándar y el tercer tipo se basa en el estándar POSIX, que puede realizar comunicación entre hosts. Lo que vamos a discutir hoy es la primera categoría importante de comunicación por canalización. Antes de comprender la comunicación por canalización, primero debemos saber qué es una canalización.

El concepto de tubería.

Tomemos como ejemplo la tubería de agua: el agua en la tubería de agua fluye de un extremo al otro. En el mundo de la informática, hay dos procesos en ambos extremos de la tubería y el flujo de agua es equivalente a los datos transmitidos entre los dos procesos . Entonces, ¿qué es exactamente este oleoducto? Lo representamos con una gráfica:

Las canalizaciones pertenecen al kernel, por lo que el sistema operativo nos proporciona llamadas al sistema para crear canalizaciones, de las que hablaremos más adelante. Ahora hablemos de por qué usamos tuberías.

Algunas personas tienen dudas de que la canalización sea un archivo a nivel de memoria basado en el sistema de archivos ¿Cómo entiendes esta frase?

Antes de responder a esta pregunta, pensemos en ello: si queremos realizar la comunicación entre dos procesos en función del sistema de archivos, es muy simple: crea un archivo en el disco y lo escribe en un proceso. Primero se carga el archivo. en la memoria y luego los datos se escriben en el archivo. Finalmente, se vacían en el disco periódicamente. Un proceso lee, carga el archivo en la memoria y lee datos de la memoria. File IO reduce en gran medida la eficiencia de la comunicación entre procesos . Entonces, la canalización se basa en el sistema de archivos y el propósito es leer y escribir como otros archivos. La canalización también es un archivo a nivel de memoria, porque se crea en la memoria y cada proceso solo necesita procesar la memoria para la transmisión de datos y no tiene nada que ver con el disco, lo que mejora la eficiencia de la comunicación de datos.

tubería anónima

Las canalizaciones se dividen en canalizaciones anónimas y canalizaciones con nombre . Cuando un proceso crea una tubería a través de una llamada al sistema, porque la tubería es un archivo de nivel de memoria basado en el sistema de archivos. Entonces, cuando se abre este archivo, la tabla de descriptores de archivos del proceso almacena el descriptor del archivo. El archivo tiene dos descriptores, uno para el lado de lectura y otro para el lado de escritura:

Pero el proceso y el proceso son independientes entre sí, entonces, ¿cómo dejar que otro proceso vea esta canalización? Podemos crear un proceso hijo a través de fork, de modo que el proceso hijo herede la tabla de descriptores de archivos del proceso padre, y se pueda encontrar la misma canalización a través de la tabla de descriptores de archivos:

 Para permitir que cada uno de los dos procesos realice sus propias tareas de lectura y escritura, podemos cerrar los descriptores de archivos excepto sus tareas:

Por lo tanto, llamamos canalización anónima a la canalización utilizada para la comunicación de procesos entre parientes consanguíneos . Su interfaz de llamada al sistema es pipe:

 La matriz pipefd almacena los descriptores de archivos de lectura y escritura de la canalización, pipefd [0] es el descriptor de fin de lectura y pipefd [1] es el descriptor de fin de escritura. Ahora intentamos implementar la comunicación entre los procesos padre e hijo:


#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

using namespace std;

// 父进程进行读取,子进程进行写入
int main()
{
    // 第一步:创建管道文件,打开读写端
    int fds[2];
    int n = pipe(fds);
    assert(n == 0);

    // 第二步: fork
    pid_t id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        // 子进程进行写入
        close(fds[0]);
        // 子进程的通信代码
        // string msg = "hello , i am child";
        const char *s = "我是子进程,我正在给你发消息";
        int cnt = 0;
        while (true)
        {
            cnt++;
            char buffer[1024]; // 只有子进程能看到!
            snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());
            // 写端写满的时候,在写会阻塞,等对方进行读取!
            write(fds[1], buffer, strlen(buffer));
            cout << "count: " << cnt << endl;
            // sleep(50); //细节,我每隔1s写一次
            // break;
        }

        // 子进程
        close(fds[1]); // 子进程关闭写端fd
        cout << "子进程关闭自己的写端" << endl;
        // sleep(10000);
        exit(0);
    }
    // 父进程进行读取
    close(fds[1]);
    // 父进程的通信代码
    while (true)
    {
        sleep(2);
        char buffer[1024];
        // cout << "AAAAAAAAAAAAAAAAAAAAAA" << endl;
        // 如果管道中没有了数据,读端在读,默认会直接阻塞当前正在读取的进程!
        ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
        // cout << "BBBBBBBBBBBBBBBBBBBBBB" << endl;
        if (s > 0)
        {
            buffer[s] = 0;
            cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;
        }
        else if(s == 0)
        {
            //读到文件结尾
            cout << "read: " << s << endl;
            break;
        }
        break;

        // 细节:父进程可没有进行sleep
        // sleep(5);
    }
    close(fds[0]);
    cout << "父进程关闭读端" << endl;

    int status = 0;
    n = waitpid(id, &status, 0);
    assert(n == id);

    cout <<"pid->"<< n << " : "<< (status & 0x7F) << endl;

    return 0;
}

Reglas de lectura y escritura de canalizaciones:

Cuando no hay datos para leer:
O_NONBLOCK desactivar : La llamada de lectura se bloquea, es decir, el proceso suspende la ejecución y espera hasta que lleguen los datos.
Habilitación O_NONBLOCK : la llamada de lectura devuelve -1 y el valor de error es EAGAIN .
Cuando la tubería esté llena:
O_NONBLOCK desactivar : la llamada de escritura se bloquea hasta que un proceso lee los datos
Habilitación O_NONBLOCK : la llamada devuelve -1 y el valor de errno es EAGAIN
Si todos los descriptores de archivo correspondientes al extremo de escritura de la tubería están cerrados, la lectura devuelve 0
Si los descriptores de archivo correspondientes a todos los extremos de lectura de la tubería están cerrados, la operación de escritura generará una señal SIGPIPE, lo que puede hacer que el proceso de escritura reciba la señal y se elimine.

Implemente un grupo de procesos con canalizaciones anónimas:

#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define MakeSeed() srand((unsigned long)time(nullptr) ^ getpid() ^ 0x171237 ^ rand() % 1234)

#define PROCSS_NUM 10

///子进程要完成的某种任务 -- 模拟一下/
// 函数指针 类型
typedef void (*func_t)();

void downLoadTask()
{
    std::cout << getpid() << ": 下载任务\n"
              << std::endl;
    sleep(1);
}

void ioTask()
{
    std::cout << getpid() << ": IO任务\n"
              << std::endl;
    sleep(1);
}

void flushTask()
{
    std::cout << getpid() << ": 刷新任务\n"
              << std::endl;
    sleep(1);
}

void loadTaskFunc(std::vector<func_t> *out)
{
    assert(out);
    out->push_back(downLoadTask);
    out->push_back(ioTask);
    out->push_back(flushTask);
}

/下面的代码是一个多进程程序//
class subEp // Endpoint
{
public:
    subEp(pid_t subId, int writeFd)
        : subId_(subId), writeFd_(writeFd)
    {
        char nameBuffer[1024];
        snprintf(nameBuffer, sizeof nameBuffer, "process-%d[pid(%d)-fd(%d)]", num++, subId_, writeFd_);
        name_ = nameBuffer;
    }

public:
    static int num;
    std::string name_;
    pid_t subId_;
    int writeFd_;
};

int subEp::num = 0;

int recvTask(int readFd)
{
    int code = 0;
    ssize_t s = read(readFd, &code, sizeof code);
    if(s == 4) return code;
    else if(s <= 0) return -1;
    else return 0;
}

void sendTask(const subEp &process, int taskNum)
{
    std::cout << "send task num: " << taskNum << " send to -> " << process.name_ << std::endl;
    int n = write(process.writeFd_, &taskNum, sizeof(taskNum));
    assert(n == sizeof(int));
    (void)n;
}

void createSubProcess(std::vector<subEp> *subs, std::vector<func_t> &funcMap)
{
    std::vector<int> deleteFd;
    for (int i = 0; i < PROCSS_NUM; i++)
    {
        int fds[2];
        int n = pipe(fds);
        assert(n == 0);
        (void)n;
        // 父进程打开的文件,是会被子进程共享的
        // 你试着多想几轮
        pid_t id = fork();
        if (id == 0)
        {
            for(int i = 0; i < deleteFd.size(); i++) close(deleteFd[i]);
            // 子进程, 进行处理任务
            close(fds[1]);
            while (true)
            {
                // 1. 获取命令码,如果没有发送,我们子进程应该阻塞
                int commandCode = recvTask(fds[0]);
                // 2. 完成任务
                if (commandCode >= 0 && commandCode < funcMap.size())
                    funcMap[commandCode]();
                else if(commandCode == -1) break;
            }
            exit(0);
        }
        close(fds[0]);
        subEp sub(id, fds[1]);
        subs->push_back(sub);
        deleteFd.push_back(fds[1]);
    }
}

void loadBlanceContrl(const std::vector<subEp> &subs, const std::vector<func_t> &funcMap, int count)
{
    int processnum = subs.size();
    int tasknum = funcMap.size();
    bool forever = (count == 0 ? true : false);

    while (true)
    {
        // 1. 选择一个子进程 --> std::vector<subEp> -> index - 随机数
        int subIdx = rand() % processnum;
        // 2. 选择一个任务 --> std::vector<func_t> -> index
        int taskIdx = rand() % tasknum;
        // 3. 任务发送给选择的进程
        sendTask(subs[subIdx], taskIdx);
        sleep(1);
        if(!forever)
        {
            count--;
            if(count == 0) break;   
        }
    }
    // write quit -> read 0
    for(int i = 0; i < processnum; i++) close(subs[i].writeFd_); // waitpid();
}

    
void waitProcess(std::vector<subEp> processes)
{
    int processnum = processes.size();
    for(int i = 0; i < processnum; i++)
    {
        waitpid(processes[i].subId_, nullptr, 0);
        std::cout << "wait sub process success ...: " << processes[i].subId_ << std::endl;
    }
}

int main()
{
    MakeSeed();
    // 1. 建立子进程并建立和子进程通信的信道, 有bug的,但是不影响我们后面编写
    // 1.1 加载方发表
    std::vector<func_t> funcMap;
    loadTaskFunc(&funcMap);
    // 1.2 创建子进程,并且维护好父子通信信道
    std::vector<subEp> subs;
    createSubProcess(&subs, funcMap);

    // 2. 走到这里就是父进程, 控制子进程,负载均衡的向子进程发送命令码
    int taskCnt = 3; // 0: 永远进行
    loadBlanceContrl(subs, funcMap, taskCnt);

    // 3. 回收子进程信息
    waitProcess(subs);

    return 0;
}

El código del grupo de procesos es un poco complicado y debe implementarlo y comprenderlo usted mismo, por lo que no diremos mucho aquí. Ahora les hablaré de las tuberías que no necesitan comunicarse según su parentesco consanguíneo: tuberías con nombre.

Tuberías con nombre:

Se puede aplicar a dos procesos no relacionados para comunicarse (sin parentesco consanguíneo) y llamar a la interfaz quince para crear un archivo de canalización. A esta tubería la llamamos tubería con nombre . Las canalizaciones con nombre son un tipo especial de archivo.

Cree una interfaz para canalizaciones con nombre:

int mkfifo(const char *nombre de archivo,mode_t modo);
El modo es el permiso, por ejemplo, es 0600, por lo que solo los usuarios pueden usar este archivo de canalización.
A través de canalizaciones con nombre, podemos implementar una comunicación entre el cliente y el servidor:
comunicación.hpp:
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define NAMED_PIPE "/tmp/mypipe.106"

bool createFifo(const std::string &path)
{
    umask(0);
    int n = mkfifo(path.c_str(), 0600);
    if (n == 0)
        return true;
    else
    {
        std::cout << "errno: " << errno << " err string: " << strerror(errno) << std::endl;
        return false;
    }
}

void removeFifo(const std::string &path)
{
    int n = unlink(path.c_str());
    assert(n == 0); // debug , release 里面就没有了
    (void)n;
}

cliente.cc:

#include "comm.hpp"

// 你可不可以把刚刚写的改成命名管道呢!
int main()
{
    std::cout << "client begin" << std::endl;
    int wfd = open(NAMED_PIPE, O_WRONLY);
    std::cout << "client end" << std::endl;
    if(wfd < 0) exit(1); 

    //write
    char buffer[1024];
    while(true)
    {
        std::cout << "Please Say# ";
        fgets(buffer, sizeof(buffer), stdin); // abcd\n
        if(strlen(buffer) > 0) buffer[strlen(buffer)-1] = 0;
        ssize_t n = write(wfd, buffer, strlen(buffer));
        assert(n == strlen(buffer));
        (void)n;
    }

    close(wfd);
    return 0;
}

servidor.cc:

#include "comm.hpp"

int main()
{
    bool r = createFifo(NAMED_PIPE);
    assert(r);
    (void)r;

    std::cout << "server begin" << std::endl;
    int rfd = open(NAMED_PIPE, O_RDONLY);
    std::cout << "server end" << std::endl;
    if(rfd < 0) exit(1);

    //read
    char buffer[1024];
    while(true)
    {
        ssize_t s = read(rfd, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "client->server# " << buffer << std::endl;
        }
        else if(s == 0)
        {
            std::cout << "client quit, me too!" << std::endl;
            break;
        }
        else
        {
            std::cout << "err string: " << strerror(errno) << std::endl;
            break;
        }
    }

    close(rfd);

    // sleep(10);
    removeFifo(NAMED_PIPE);
    return 0;
}

La diferencia entre canalizaciones anónimas y canalizaciones con nombre:

1. La función de canalización crea y abre una canalización anónima . Las canalizaciones con nombre se crean mediante la función mkfififo y se abren con open
2. La única diferencia entre FIFO (canalización con nombre) y canalización (canalización anónima) está en la forma en que se crean y abren, pero una vez completadas estas tareas, tienen la misma semántica.

Reglas de apertura para canalizaciones con nombre

Si la operación de apertura actual es abrir el FIFO para lectura
O_NONBLOCK desactivar : bloquear hasta que un proceso correspondiente abra el FIFO para escribir
Habilitación O_NONBLOCK : retorno exitoso inmediatamente
Si la operación de apertura actual es abrir el FIFO para escribir
O_NONBLOCK desactivar : bloquear hasta que el proceso correspondiente abra el FIFO para lectura O_NONBLOCK habilitar: regresar inmediatamente y fallar, el código de error es ENXIO

En este punto, la comunicación entre procesos ha terminado. ¡Espero que todos nos apoyen y avancemos juntos! 

Supongo que te gusta

Origin blog.csdn.net/m0_69005269/article/details/130446585
Recomendado
Clasificación