进程间通信之管道

文章目录

  • 一、管道
    • 1. 匿名管道
    • 2. 命名管道

进程具有独立性,因此进程间通信的前提是两个进程能看到同一份资源

一、管道

对于进程打开的内存文件,操作系统是以引用计数的方式创建的 file 结构体,如果让两个进程与同一个 file 结构体关联,便可以让两个进程看到同一份资源

由于 file 结构体的缓冲区只有一个,因此只能让一个进程以写的方式,另一个进程以读的方式打开同一个文件,这样便实现了单向的进程间通信
进程间通信之管道_第1张图片
操作系统提供了仅在内存中创建 file 结构体(不在磁盘上创建对应的文件),这种特殊的文件称为管道文件,其中没有名字的称为 匿名管道,有名字的称为 命名管道

1. 匿名管道

进程创建匿名管道成功后,会以读和写两种方式打开管道文件,因此匿名管道通常用于父子进程的进程间通信

系统调用 pipe,头文件 unistd.h

  • int pipe(int pipefd[2]),创建匿名管道

返回值:匿名管道创建成功返回 0,出错返回 -1,并且 errno 被设置为相应的出错信息

参数:pipefd 为输出型参数,pipefd[0] 存储读端文件描述符,pipefd[1] 存储写端文件描述符

父子进程通信的步骤:

  • 父进程创建管道
    父进程以读和写两种方式打开管道文件
  • 创建子进程
    子进程会拷贝父进程的文件描述符表,因此子进程也会以读和写两种方式打开同一个管道文件
  • 父进程和子进程分别关闭自己不需要的读端或写端
    一个进程向管道中写入,另一个进程从管道中读取

子进程向匿名管道写入,父进程从匿名管道读取

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int main()
{
    // 创建匿名管道
    // pipefd[0] 表示读
    // pipefd[1] 表示写
    int pipefd[2] = { 0 };
    if (pipe(pipefd) < 0)
    {
        // 创建匿名管道失败
        cout << "匿名管道创建失败: " << errno << " " << strerror(errno) << endl;
        exit(1);
    }

    // 创建子进程
    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        // 子进程向匿名管道写入,需要关闭读
        close(pipefd[0]);

        // 开始通信
        int cnt = 0;
        char buffer[64];
        while (true)
        {
            snprintf(buffer, sizeof(buffer), "我是子进程,这是我给你发的第 %d 个信息", ++cnt);
            
            // 向匿名管道中写入
            write(pipefd[1], buffer, strlen(buffer));
            cout << cnt << endl;
            // sleep(1); // 让写端慢一点
            // if (cnt == 3) break; // 模拟写端关闭
        }
        close(pipefd[1]);

        exit(0);
    }

    // 父进程从匿名管道读取,需要关闭写
    close(pipefd[1]);

    // 开始通信
    char buffer[64];
    while (true)
    {
        // 从匿名管道中读取
        // sleep(3); // 让读端慢一点
        // sleep(3); break; // 模拟读端关闭
        int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (n > 0)
        {        
            buffer[n] = '\0';
            cout << buffer << endl;
        }
        else if (n == 0)
        {
            cout << "写端已经关闭,我读到文件结尾了" << endl;
            break;
        }
        else
        {
            cout << "读取错误" << endl;
            break;
        }
    }
    close(pipefd[0]);

    // 读端关闭,写端会收到 13 号信号 SIGPIPE
    int status;
    waitpid(id, &status, 0);
    cout << "子进程退出信号: " << (status & 0x7F) << endl;

    return 0;
}
  • 放开子进程代码中的 sleep(1),让写端慢一点,匿名管道中没有数据时,读端会等待写端写入
    进程间通信之管道_第2张图片
  • 放开父进程中的 sleep(3),让读端慢一点,匿名管道中写满数据时,写端会等待读端读取
    进程间通信之管道_第3张图片
  • 放开子进程中模拟写端关闭的代码,并且让写端慢一点,写端关闭后,读端读取完匿名管道中的数据后,读端会读取到文件结尾
    进程间通信之管道_第4张图片
  • 放开子进程中模拟读端关闭的代码,并且让写端慢一点,读端关闭时,此时匿名管道无意义,操作系统会向写端发送 SIGPIPE 13 号信号
    进程间通信之管道_第5张图片

命令行中的 | 即为匿名管道,| 会将前一个进程的标准输出重定向到匿名管道,后一个进程的标准输入重定向到匿名管道

在这里插入图片描述

通过匿名管道创建进程池:

// processpool.hpp
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

namespace starrycat
{
    class EndPoint
    {
    public:
        EndPoint()
        {}
        EndPoint(pid_t cid, int wfd) : _cid(cid), _wfd(wfd)
        {}
        ~EndPoint()
        {}

    public:
        pid_t _cid;
        int _wfd;
    };

    const int NUM = 5;

    class ProcessPool
    {
        using fun_t = std::function<void(int)>;

    public:
        ProcessPool(fun_t Command, int num = NUM)
        {
            std::vector<int> tmpfd; // 保存子进程不需要的父进程的文件描述符
            for (int i = 0; i < num; ++i)
            {
                // 创建管道
                int pipefd[2] = {0};
                if (pipe(pipefd) < 0)
                {
                    std::cout << "创建管道失败: " << errno << " " << strerror(errno) << std::endl;
                    exit(1);
                }

                // 创建子进程
                int id = fork();
                assert(id != -1);
                if (id == 0)
                {
                    // 子进程从管道中读取,需要关闭写
                    close(pipefd[1]);
                    for (auto e : tmpfd) close(e); // 关闭不需要的文件描述符

                    // 开始通信
                    while (true)
                    {
                        // 读取四字节整数的命令
                        int cmd = 0;
                        int n = read(pipefd[0], &cmd, sizeof(int));
                        if (n == sizeof(int))
                        {
                            // 测试
                            std::cout << getpid() << " ";

                            // 执行命令
                            Command(cmd);
                        }
                        else if (n == 0)
                        {
                            // 测试
                            std::cout << getpid() << " 读取到文件结尾了" << std::endl;
                            break;
                        }
                        else
                        {
                            std::cout << "读取异常" << std::endl;
                            break;
                        }
                    }

                    close(pipefd[0]);
                    exit(0);
                }

                // 父进程向管道中写入,需要关闭读
                close(pipefd[0]);
                _endPoints.push_back(EndPoint(id, pipefd[1]));
                tmpfd.push_back(pipefd[1]);
            }
        }

        ProcessPool(const ProcessPool &p) = delete;
        ProcessPool &operator=(const ProcessPool &p) = delete;

        ~ProcessPool()
        {
            for (auto& e : _endPoints) 
            {
                close(e._wfd);
                waitpid(e._cid, nullptr, 0);

                // 测试
                std::cout << "等待子进程:" << e._cid  << "成功" << std::endl;
                sleep(1);
            }
        }

        // 规定命令为四字节整数
        void push(int command)
        {
            // 以轮训的方式调用子进程
            static int index = 0;
            write(_endPoints[index]._wfd, &command, sizeof(int));

            index++;
            index %= _endPoints.size();
        }

    private:
        std::vector<EndPoint> _endPoints;
    };
}

// myctrlprocess.cc
#include "processpool.hpp"
#include 
#include 

using namespace std;

// 子进程个数
const int num = 5;

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

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

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

void CommandError()
{
    cout << "任务不存在" << endl;
}

void Command(int cmd)
{
    switch (cmd)
    {
    case 0:
        PrintLog();
        break;
    case 1:
        InsertMySQL();
        break;
    case 2:
        NetQuest();
        break;
    default:
        CommandError();
        break;
    }
}

int main()
{
    starrycat::ProcessPool ppool(Command);

    int command = 0;
    while (true)
    {
        cout << "请输入命令: ";
        cin >> command;

        // 测试
        if (command == -1) break;

        ppool.push(command);
        sleep(1);
    }

    return 0;
}

2. 命名管道

命名管道支持两个毫不相关的进程通信,其使用和文件一样

系统调用 mkfifo,头文件 sys/types.h、sys/stat.h

  • int mkfifo(const char *pathname, mode_t mode),创建命名管道

返回值:命名管道创建成功返回 0,出错返回 -1,并且 errno 被设置为相应的出错信息

参数:

  • pathname 表示创建命名管道的路径名(如果只有文件名,则表示在进程所在的路径下创建)
  • mode 表示创建命名管道的文件权限,受 umask 影响

客户端向命名管道写入,服务端从命名管道读取

// namepipe.h
#pragma once

// 命名管道文件名
const char* const fifoname = "fifo";

// client.cc
#include "namepipe.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int main()
{
    // 以写方式打开命名管道
    int wfd = open(fifoname, O_WRONLY);
    if (wfd < 0)
    {
        cout << "打开文件失败" << errno << " " << strerror(errno) << endl;
        exit(1);
    }

    // 开始通信
    char buffer[1024];
    while (true)
    {
        cout << "请输入: ";
        char* str = fgets(buffer, sizeof(buffer), stdin);
        if (str == NULL)
        {
            cout << "客户端退出" << endl;
            break;
        }
        buffer[strlen(buffer) - 1] = '\0'; // 去掉输入的回车符

        // 输入 quit 表示客户端退出
        if(strcmp(buffer, "quit") == 0) 
        {
            cout << "客户端退出" << endl;
            break;
        }

        // 向命名管道写入
        write(wfd,  buffer, strlen(buffer));
    }

    close(wfd);

    return 0;
}

// server.cc
#include "namepipe.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int main()
{
    // 创建命名管道
    umask(0);  // 设置进程的权限掩码
    if (mkfifo(fifoname, 0666) < 0)
    {
        cout << "创建管道失败: " << errno << " " << strerror(errno) << endl;
        exit(1);
    }

    // 以读方式打开命名管道
    int rfd = open(fifoname, O_RDONLY);
    if (rfd < 0)
    {
        cout << "打开文件失败" << errno << " " << strerror(errno) << endl;
    }

    // 开始通信
    char buffer[1024];
    while (true)
    {
        // 从命名管道读取
        int n = read(rfd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0';
            cout << "客户端: " << buffer << endl;
        }
        else if (n == 0)
        {
            cout << "客户端退出了,服务端也退出" << endl;
            break;
        }
        else
        {
            cout << "读取异常" << endl;
            break;
        }
    }

    close(rfd); 
    unlink(fifoname); // 删除命名管道
    
    return 0;
}

先启动 myserver,在启动 myclient

进程间通信之管道_第6张图片

命名管道和匿名管道的通信特性是一样的

mkfifo 文件名
功能:创建命名管道

在这里插入图片描述

你可能感兴趣的:(Linux,linux)