Linux进程通信之管道通信

我们都知道,进程之间都是相互独立的,为了实现进程间能够互相传输数据便有了进程间通信。进程间通信分为三类,第一类是基于文件系统的管道通信,第二类是基于system v标准的本地通信,第三类是基于POSIX标准,能够实现跨主机的通信。今天我们所要探讨的是第一大类管道通信。在认识管道通信之前,我们得先知道什么是管道。

管道的概念

我们拿自来水管道来举例,自来水管道里面的水从一头流向另一头。在计算机世界里,管道的两端是两个进程,水流就相当于两个进程之间传输的数据。那这个管道到底是什么呢?我们用一张图来表示它:

管道是属于内核的,所以os为我们提供了创建管道的系统调用,这个我们后面再讲。现在我们来谈一谈为什么要使用管道这个东西。

有人就有疑问,管道是基于文件系统的内存级文件,这句话是怎么理解的?

在解答这个问题之前,我们思考一下,如果想基于文件系统实现两个进程的通信很简单:在磁盘中创建一个文件,一个进程写,文件先被加载到内存中再将数据写到文件中最后定期刷新到磁盘上。一个进程读,将文件加载到内存中,再从内存中读取数据。文件的IO大大降低了进程间的通信效率。所以管道是基于文件系统的,目的是和别的文件一样可以进行读写。管道也是一个内存级文件,原因就在于它是被创建在内存里的,各个进程进行数据的传输只需要和内存打交道,和磁盘无关,提高了数据通信的效率。

匿名管道

管道分为匿名管道命名管道。当我们一个进程通过系统调用创建出一个管道时,因为管道是一个基于文件系统的内存级文件。所以打开这个文件时,进程的文件描述符表就存有该文件的描述符。该文件有两个描述符,一个是读端描述符,一个是写端描述符:

可是进程与进程之间是相互独立的,那么如何让另一个进程看到这个管道呢?我们可以通过fork创建子进程的方式,这样子进程就继承了父进程的文件描述符表,通过文件描述符表就可以找到同一个管道了:

 为了让两个进程各个执行自己的读写任务,我们可以关闭它们任务除外的文件描述符:

所以我们称有血缘关系间的进程通信所使用的管道为匿名管道。它的系统调用接口是pipe:

 pipefd 数组里存放的是管道的读写文件描述符,pipefd[0]是读端描述符,pipefd[1]是写端描述符。现在我们就尝试实现父子进程之间的通信:


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

管道的读写规则:

当没有数据可读时:
O_NONBLOCK disable read 调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable read 调用返回 -1 errno 值为 EAGAIN
当管道满的时候:
O_NONBLOCK disable write 调用阻塞,直到有进程读走数据
O_NONBLOCK enable :调用返回 -1 errno 值为 EAGAIN
如果所有管道写端对应的文件描述符被关闭,则 read 返回 0
如果所有管道读端对应的文件描述符被关闭,则 write 操作会产生信号 SIGPIPE, 进而可能导致 write进程收到信号被杀死

用匿名管道实现进程池:

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

进程池的代码有一点难度,需要自己实现并加以理解,这里我们就不多说了。现在我为大家讲一讲不用基于血缘关系通信的管道:命名管道。

命名管道:

可以应用于两个毫无关系的进程进行通信(没有血缘关系),调用fifo接口创建一个管道文件。我们称该管道为命名管道。命名管道是一个特殊类型的文件。

创建命名管道的接口:

int mkfifo(const char *filename,mode_t mode);
mode是权限,例如我们这是0600,这样只有使用者可以使用这个管道文件了。
通过命名管道,我们可以实现一个,客户端和服务器之间的通信:
comm.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;
}

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

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

匿名管道和命名管道的区别:

1.匿名管道由 pipe 函数创建并打开。 命名管道由mkfififo 函数创建,打开用 open
2.FIFO (命名管道)与 pipe (匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

命名管道的打开规则

如果当前打开操作是为读而打开 FIFO
O_NONBLOCK disable :阻塞直到有相应进程为写而打开该 FIFO
O_NONBLOCK enable :立刻返回成功
如果当前打开操作是为写而打开 FIFO
O_NONBLOCK disable :阻塞直到有相应进程为读而打开该 FIFO O_NONBLOCK enable:立刻返回失败,错误码为 ENXIO

到这里进程间通信之管道通信就全部结束了,希望大家多多支持,共同进步! 

猜你喜欢

转载自blog.csdn.net/m0_69005269/article/details/130446585