Linux learning record - 십구 inter-process communication (1) pipeline


1. Introduction to inter-process communication

What I learned before is a single process, how do multiple processes work?

1. Purpose

Data transfer: One process needs to send its data to another process
Resource sharing: The same resource is shared between multiple processes.
Notification event: A process needs to send a message to another process or a group of processes, informing it (they) that some event has occurred (such as
notifying the parent process when the process terminates).
Process control: Some processes want to completely control the execution of another process (such as Debug process). At this time, the control process hopes to be able to intercept
all traps and exceptions of another process, and to be able to know its state changes in time.

Processes are independent, which increases the cost of communication. The prerequisite for two different processes to communicate is to allow the two processes to see the same resource. This is provided directly or indirectly by the operating system.

For any means of communication, different processes must first see the same resource, and then one party writes and the other reads to complete the communication.

2. Development

Pipes
System V interprocess communication
POSIX interprocess communication

2. Pipeline

1. Principle

Pipes are the oldest form of interprocess communication in Unix.

We call a flow of data from one process to another a "pipe"

Multiple processes can be created at the same time, connected through pipelines, and their parent processes are also the same, bash.

A pipe is also a file. A file opens the pipe for writing and redirects its standard output to the pipe, and a file at the other end opens the pipe for reading and redirects its standard input to the pipe.

When the file process starts, there is a table of file descriptors in the structure, pointing to each file. In addition, an anonymous pipe will be opened. This is a pure memory file provided by the operating system and does not need to refresh the content to the disk. Through the process call, this anonymous pipe will open the same file through reading and writing; the current process will fork, copy its own file descriptor table to the child process, the specific content will be changed accordingly, and the opened files need to be copied ? Do not copy. But it doesn't matter, because of the file description table, the child process still points to the file created by the parent process, including the anonymous pipe. This is actually equivalent to a shallow copy. The files pointed to by the parent and child processes are the same. Therefore, both father and son point to the anonymous pipeline. If one party changes, the other party can get new data; but at this time, the pipeline only supports one-way communication. The next thing to do is to determine the data flow direction and close unnecessary fds. The operating system will turn off both the writing method of the child process and the reading method of the parent process, so that the parent writes and the child reads, forming a pipeline.

Pipelines can only communicate in one direction. If you want two-way communication, define two pipelines.

2. Simple simulation implementation

Simple code, the child process sends a message to the parent process, which is a kind of pipeline. The whole step is to create a pipeline, create a subprocess, close unnecessary fds, and start communication.

Create a pipeline with the pipe function, int pipe(int fd[2]), using the unistd.h header file. pipefd is an output parameter, the system will create a pipe, and then pass the file descriptors of the read and write ends to the pipefd array, and there are two integers in the array. The pipe function returns -1 on failure, the error code is set, and returns 0 on success.

#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. Summary

The system can distinguish between pipeline files and ordinary files.

features

1. One-way communication, a kind of half-duplex, which means that both parties work alternately; full-duplex means that both parties can work at the same time 2. The
essence of a pipeline is a file, because the life cycle of the file descriptor and the file follows the process, so the life cycle of the pipeline The life cycle also follows the process. When the parent-child process exits, the previously closed file descriptor will return to its previous position.
3. Parent-child processes can communicate and there is inheritance in it. Pipeline communication is usually used for processes with a "blood" relationship, often used for parent-child processes. Communication between sibling processes is also possible. When pipe opens a pipe, you don’t need to care about which file is opened, as long as you get two file descriptors, because pipe opens an anonymous pipe. 4.
In pipe communication, the number of write times and the number of read times are not strictly matched. Wrote 7 but only read 1 to read them all. Reading and writing are not strongly correlated. Reading is byte stream-oriented, and reading only depends on the number of bytes that should be read.
5. The pipeline has a certain coordination ability, so that reading and writing can communicate according to certain steps----self-contained synchronization mechanism

Scenes

1. If we read all the pipeline data and the other party does not write, the reader can only wait for
2. After the write end is full (usually 65535/65536, about 64kb), it will not continue to write. Wait until the reader reads. You can not write sleep in the child process, but the parent process sleep(10), that is, the child process writes crazily, while the parent process reads slowly, depending on the actual phenomenon 3. If the write end is closed, after reading the pipeline data, read it
again It will return 0, indicating that the end of the file has been read.
4. The writing end keeps writing, and the reading end is closed, so writing becomes meaningless; the operating system will not maintain meaningless, inefficient, and resource-wasting processes, so the process will be killed directly. drop this process. The system will terminate the process through the signal, SIGPIPE -13 closes the process

The amount of data read and written in a single read and write of the pipeline is a macro PIPE_BUF, which can be viewed with man 7. The amount of data written by the pipeline at a time should be smaller than this PIPE_BUF, and its size is 4096 bytes. When it is smaller, the write operation is atomic. This will explain what is atomic later.

3. Control process - anonymous pipe

A parent process can have multiple child processes, and each child process communicates with the parent process through a pipeline. If the parent process does not write data into the pipeline, the child process will block, and if the data is written, the child process will re-enter the running state. The parent process can wake up the child process by writing specific messages to the child process, and even let the child process perform certain tasks in a directed manner. The parent process will first describe and then organize the pipelines and processes created by itself.

The child process will inherit the file descriptor of the parent process when it is created, so the first parent process will have many child processes connected to it, which is confusing. The actual structure to be realized is one-to-one, a pair of parent-child processes has its own independent pipeline without being affected by other processes.

Step by step to design this structure

write a beginning

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

Every cycle, pipefd needs to be recreated, and it is all returned to 0, so for the parent process, it is like doing a completely repeated action, and it cannot distinguish between the child process and the corresponding pipeline. So we need to let the parent process know which pipe belongs to which child process no matter where it is. Here we write a class, and insert it into the vector constructed by this class after each loop, so that the results of each communication are preserved.

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

Now put this part into a function createProcess, the function parameter is the vector. And write the function WaitCommand that controls the child process to wait for input.

current code

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

Open a new file with the suffix .hpp, and write the tasks performed by the child process in it.

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

Go back to the CtrlProcess.cc file

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

Define a global Task object t, and stipulate that each input command is in units of 4 bytes. After all these are written, the parent-child process has established the pipeline and managed all the pipelines. Then after the CreateProcess function is completed, the parent process continues to execute, and at this time it can send a message to the child process. You can print the pid of the corresponding child process in the sentence printed by each task in Task, and the header file is 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;
}

Continue to improve the code. Let the sub-processes select tasks in order and modify them in main; each sub-process has a name; control the input command number, re-enter if it is wrong, and enter 3 to exit as a whole; finally, all exit problems must be handled. If the parent process Write a part of the data and then exit, then the child process will also exit after reading. In the waitcommand function, the child process will break when it reads 0, and then it will go to the createProcess function. The child process will end its own reading end, so we Just close the write end of the parent process, and then recycle the child process.

overall code

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

Now the waitProcess function is problematic. The function turns two loops into one loop, and the child process will be recycled directly after exiting, so you will find that the program cannot exit. The problem occurs in the createProcess function.

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

Although it seems to be a cycle, each time a child process is created and the corresponding pipeline is established, the fork child process will inherit the file opened by the parent process, that is, the last pipeline will be inherited, and the child process will point to this pipeline, in turn By analogy, the entire structure is not independent at all. To solve this problem, the first way is to exit recycling backwards. When the child process exits, it will close its own read and write terminals, and it will not point to the above pipeline.

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

But if we still want to build an ideal structure, we must start from create. The write end of the parent process is not closed every time. At the end of this cycle, we save the write end. When the next child process, it will inherit the write end of the parent process above, and then in the child process Before doing things, close all the previously stored write ports.

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

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

anonymous pipe

4. Named pipes

Anonymous pipes have limitations and can only be used for communication between blood-related processes. To allow communication between two unfamiliar processes, named pipes are required.

To create a named pipe, you need to use the mkfifo command. The parameters in the brackets are the named pipe parameters, and fifo means first in first out.

insert image description here

Its file type starts with p, indicating that it is a pipeline file.

Now write a command echo "string" > fifo to write content in it, but the cursor will stop at the beginning of the next line and keep blinking. This is because the fifo file is just a symbol, and the things written in it will not actually exist on the disk. , only write to the pipe file, we can use cat to read, it reads from the display by default, cat < fifo will read from the pipe file, and then print to the display. Even if you write a code that keeps outputting content, you can open another window and use cat to get the content.

1. Principle

I wrote about reference counting in the hard link part of the previous basic IO blog, and reference counting is also used in named pipes. If a file in the disk is not opened, it will stay in the disk; after opening, it will have its own file structure, which contains various parameters, including reference counts; after the operator creates a process, there will be a file description Symbol table and other things, the process can open the file in the memory, and the reference count becomes 1 at this time; if another process is opened and the same file is opened, the system will not open a structure of this file, but this Two processes point to the same file structure, the count becomes 2, the processes disappear one by one, and the count changes from 2 to 0. But how are two processes guaranteed to open the same file? To make the file unique, the file path + file name are all the same.

2. Simulation implementation

Create a pipeline file, let the read and write end processes open the file according to their own needs, and then start communication.

We define two files, which need to be written in order to form two executable programs.

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

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

Open two windows to observe Client and Server. After starting to run, the Server will be stuck at opening the pipeline, because it only opens the read end, and the client needs to open the write end to continue running, that is, only create file at the beginning, after we ./client, Client.cc starts running, Then the server will print open file...; the program can communicate normally, but if there is a space every time a statement is entered, the server will also receive the space, which will cause a blank line under each line, so we are in Client there to change it.

        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;

If you enter an empty string, the server will exit. This is because the last assert in the above code will fail the assertion, then the server will exit after n is 0 after receiving it, so it is written as assert(n >= 0).

The link below has been modified and changed to automatically print without typing spaces.

named pipe

Finish.

Guess you like

Origin blog.csdn.net/kongqizyd146/article/details/129912113