[Linux] Pipes for inter-process communication-named pipes & anonymous pipe communication & process pool design

1. Introduction to inter-process communication

1.What is communication

Data transfer: One process needs to send its data to another process

Resource sharing: sharing the same resources between multiple processes.

Notification event: A process needs to send a message to another process or a group of processes to notify it (them) that a certain 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 the Debug process). At this time, the control process hopes to intercept all traps and exceptions of another process and be able to know its status changes in time.

2. Why there is communication and how to communicate

We know that sometimes we need multiple processes to collaborate to complete certain services, such as cat file | grep "hello". At this time, different processes need to communicate with each other. But processes are independent. Today we need to communicate, so the cost of communication must not be low. First we need to let different processes see the same resource before they can communicate.

How should we understand the essential issue of communication: OS needs to directly or indirectly provide "memory space" to the processes of both communicating parties. The process that wants to communicate must see a common resource. For different types of communication, The essence is that the resources mentioned earlier are provided by which module in the OS.

There are three main types of inter-process communication methods:

1. Pipeline

anonymous pipe

named pipe

2.System V IPC

System V message queue

System V shared memory

System V semaphore

3.POSIX IPC

message queue

Shared memory

Signal amount

mutex

condition variable

read-write lock

2. Pipeline

1. What is a pipeline?

Pipes are the oldest form of inter-process communication in Unix.

We call a data flow from one process to another process a "pipeline"

Insert image description here

2. Anonymous pipe

2.1 What is an anonymous pipe?

We know that the file descriptor table opened by the parent process will be copied to the child process, and the address in the file descriptor table points to the same struct_file. At this time, the parent process and the child process see the same file. A struct_file has file operation methods and its own kernel buffer-struct_page{}. In this way, by forking the parent process to create a child process, the two processes can see the same memory resource. This resource is called an anonymous pipe.

Insert image description here

The parent process opens the same file for reading and writing respectively, and then the parent and child processes close the reading end and write segment respectively. In this way, the parent process can write data to the pipe, the child process reads, and vice versa. Data is written and read from the pipe in the parent process. Generally speaking, pipes can only be used for one-way data communication. Anonymous pipes can currently be used for inter-process communication between parent and child processes.

Insert image description here

2.2 Interface understanding –pipe

int pipe(int pipefd[2]);
函数功能:创建一个管道
头文件:#include <unistd.h>
参数:pipefd[2],输出型参数,用于返回两个文件描述符,pipefd[0]为读端,pipefd[1]为写端
返回值:成功返回0,失败返回-1,错误码被设置

2.3 Anonymous pipeline implementation of inter-process communication

#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.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]);
        // 子进程的通信代码
        const char *message = "我是子进程,我正在给你发消息";
        int cnt = 0;
        while (true)
        {
    
    
            cnt++;
            char buffer[1024];
            snprintf(buffer, sizeof(buffer), "chile->parent say:%s[%d][%d]", message, cnt, getpid());
            // 写端写满的时候,在写会阻塞,等对方进行读取!
            write(fds[1], buffer, strlen(buffer));
            sleep(1);
        }
        
        close(fds[1]);
        cout << "子进程关闭自己的写端" << endl;
        exit(0);
    }

    // 父进程进行读取
    close(fds[1]);
    // 父进程的通信代码
    while (true)
    {
    
    
        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;
        }
    }
    close(fds[0]);

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

    assert(n == id);

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

2.4 Reading and writing characteristics

1. Reading is slow, writing is fast

We will sleep 1 second every time the parent process reads

Insert image description here

Data written multiple times is read each time

2. Fast to read, slow to write

We let the child process sleep for 1 second every time it writes.

Insert image description here

Since writing is only done once every second, after the parent process reads once, it will wait for the child process to write again before reading again. During this period, it will be blocked at read.

3. Write to close, read 0

After we let the child process write once, sleep for 5 seconds and then close the writing end.

Insert image description here

After 5 seconds, the write end is closed and 0-end of file is read.

4. When reading is closed, the OS will signal the process to terminate the write end.

After we let the parent process read once, we close the reading end

Insert image description here

At this time, the child process received signal No. 13

Insert image description here

Summarize:

When there is no data to read

O_NONBLOCK disable: The read call blocks, that is, the process suspends execution until data arrives.

O_NONBLOCK enable: The read call returns -1, and the errno value is EAGAIN.

when the pipe is full

O_NONBLOCK disable: The write call blocks until a process reads the data.

O_NONBLOCK enable: The call returns -1, and the errno value is EAGAIN

If the file descriptors corresponding to the write ends of all pipes are closed, read returns 0

If the file descriptors corresponding to all pipe readers are closed, the write operation will generate the signal SIGPIPE, which may cause the write process to exit.

When the amount of data to be written is no larger than PIPE_BUF, Linux will guarantee the atomicity of the write.

When the amount of data to be written is greater than PIPE_BUF, Linux will no longer guarantee the atomicity of the write.

2.5 Characteristics of pipelines

It can only be used for communication between processes with a common ancestor (processes with related relationships); usually, a pipe is created by a process, and then the process calls fork, and then the pipe can be used between the parent and child processes. Pipeline provides streaming services

Generally speaking, when the process exits, the pipe is released, so the life cycle of the pipe changes with the process

Generally speaking, the kernel will synchronize and mutually exclude pipeline operations.

The pipeline is half-duplex, and data can only flow in one direction; when communication between two parties is required, two pipelines need to be established.

Insert image description here

2.6 Process pool design

Here we implement a process to create multiple child processes, and then send tasks to the child processes through the parent process, so that the child processes can perform the corresponding tasks.

Code:

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

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

const int processnum = 5;
typedef std::function<void()> func_t;

void downloadTask()
{
    
    
    std::cout << getpid() << " 正在执行下载任务" << std::endl;
    sleep(1);
}

void IOTask()
{
    
    
    std::cout << getpid() << " 正在执行IO任务" << std::endl;
    sleep(1);
}

void fflushTask()
{
    
    
    std::cout << getpid() << " 正在执行刷新任务" << std::endl;
    sleep(1);
}

void loadTaskFunc(std::vector<func_t> &funcmap)
{
    
    
    funcmap.push_back(downloadTask);
    funcmap.push_back(IOTask);
    funcmap.push_back(fflushTask);
}

class subEp
{
    
    
public:
    subEp(const pid_t &subId, const 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 fd)
{
    
    
    int code = 0;
    ssize_t s = read(fd, &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 == 4);
    (void)n;
}

void createSubProcess(std::vector<subEp> &subs, std::vector<func_t> &funcmap)
{
    
    
    std::vector<int> deleteFd;
    for (int i = 0; i < processnum; i++)
    {
    
    
        int fds[2];
        int n = pipe(fds);
        assert(n == 0);
        (void)n;
        // 父进程打开的文件,是会被子进程共享的

        pid_t id = fork();
        if (id == 0)
        {
    
    
            close(fds[1]);
            while (true)
            {
    
    
                for (int i = 0; i < deleteFd.size(); i++)
                    close(deleteFd[i]);
                // 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]);
        subs.push_back(subEp(id, fds[1]));
        deleteFd.push_back(fds[1]);
    }
}

void loadBlanceContrl(std::vector<subEp> &subs, 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 > 0)
        {
    
    
            count--;
            if (count == 0)
                break;
        }
    }

    // write quit -> read 0
    for (int i = 0; i < processnum; i++)
        close(subs[i]._writeFd);
}

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

int main()
{
    
    
    MakeSeed();
    // 1. 建立子进程并建立和子进程通信的信道
    // 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;
}

Precautions:

After we create the second and subsequent child processes here, they will inherit the write end opened by the parent process for the first child process, so our subsequent processes will inherit the multiple write ends of the parent process, so we are closing When using file descriptors, you cannot close one and wait for the next one, because this will block the file descriptor of the previous sub-process. The file descriptor inherited by the child process is not closed, so in the end the file descriptor is not closed, so the previous child process will not exit, so it will be blocked here in waitpid.

We have two solutions here. One is to uniformly close the file descriptors of all child processes, and then unify waitpid.

The second is that after each child process is created, the parent process puts the write file descriptor into an array. When the child process is executed, it closes the write file descriptor opened by the parent process inherited by the previous child process. The reason for this is that, When the child process closes the write file descriptor, copy-on-write will occur, so only the write file descriptor of the parent process of the previous child process will be closed.

3. Named pipes

One limitation of the application of anonymous pipes is that they can only communicate between processes with a common ancestor (related to one another).

If we want to exchange data between unrelated processes, we can use FIFO files to do the job, which are often called named pipes. A named pipe is a special type of file

We can let different processes open the same file with a specified name (path + file name), so that different processes can see the same resource. The path + file name ensures the uniqueness of the file

3.1 Create a named pipe

Named pipes can be created from the command line using the following command:

mkfifo filename

Insert image description here

We loop and write "hello world" into the pipe

Insert image description here

Finally, it was discovered that the size of name_pipe did not change because the data in the pipe file was not written to the disk.

Insert image description here

When we use another process to view the data in name_pipe, we can see the written data, thus realizing communication between two different processes.

Named pipes can also be created from programs. The related functions are:

int mkfifo(const char *filename,mode_t mode);
函数功能:创建命名管道文件
头文件:
#include <sys/types.h>
#include <sys/stat.h>
参数:
filename:文件名
mode:文件的权限
返回值:成功返回0,失败返回-1,错误码被设置

int unlink(const char *path);
函数功能:移除管道文件
头文件:
#include <unistd.h>
参数:文件的路径
返回值:成功返回0,失败返回-1,错误码被设置,返回-1时,命名管道不会被改变

3.2 Use named pipes to implement server&client communication

The client sends information to the server, and the server prints out the information after receiving it.

3.2.1server.cc
#include "comm.hpp"

int main()
{
    
    
    int r = ctreateFifo(NAME_PIPE);
    assert(r);
    (void)r;

    std::cout << "server begin" << std::endl;
    int rfd = open(NAME_PIPE, O_RDONLY);
    std::cout << "server end" << std::endl;
    assert(rfd != -1);

    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);
    removeFifo(NAME_PIPE);
    return 0;
}
3.2.2client.cc
#include "comm.hpp"

int main()
{
    
    
    std::cout << "client begin" << std::endl;
    int wfd = open(NAME_PIPE, O_WRONLY);
    std::cout << "client end" << std::endl;
    assert(wfd != -1);
    (void)wfd;

    char buffer[1024];
    while (true)
    {
    
    
        std::cout << "Please say#";
        fgets(buffer, sizeof(buffer), stdin);
        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;
}
3.3.3comm.hpp
#pragma once

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

#define NAME_PIPE "./named_pipe"

bool ctreateFifo(const std::string &path)
{
    
    
    umask(0);
    int n = mkfifo(path.c_str(), 0666);
    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);
    (void)n;
}

3.3 Opening rules for named pipes

If the current open operation is to open the FIFO for reading

O_NONBLOCK disable: Block until a corresponding process opens the FIFO for writing

O_NONBLOCK enable: Return success immediately

If the current open operation is to open the FIFO for writing

O_NONBLOCK disable: Block until a corresponding process opens the FIFO for reading

O_NONBLOCK enable: Return failure immediately, the error code is ENXIO

4. The difference between anonymous pipes and named pipes

Anonymous pipes are created and opened by the pipe function.

Named pipes are created by the mkfifo function and opened with open

The only difference between FIFO (named pipe) and pipe (anonymous pipe) is the way they are created and opened, but once the work is completed, they have the same semantics.

Guess you like

Origin blog.csdn.net/qq_67582098/article/details/134885009