进程间通信之管道

文章目录

  • 一、管道
    • [1. 匿名管道](#1. 匿名管道)
    • [2. 命名管道](#2. 命名管道)

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

一、管道

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

由于 file 结构体的缓冲区只有一个,因此只能让一个进程以写的方式,另一个进程以读的方式打开同一个文件,这样便实现了单向的进程间通信

操作系统提供了仅在内存中创建 file 结构体 (不在磁盘上创建对应的文件),这种特殊的文件称为管道文件,其中没有名字的称为 匿名管道,有名字的称为 命名管道

1. 匿名管道

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

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

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

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

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

父子进程通信的步骤:

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

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

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

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),让写端慢一点,匿名管道中没有数据时,读端会等待写端写入
  • 放开父进程中的 sleep(3),让读端慢一点,匿名管道中写满数据时,写端会等待读端读取
  • 放开子进程中模拟写端关闭的代码,并且让写端慢一点,写端关闭后,读端读取完匿名管道中的数据后,读端会读取到文件结尾
  • 放开子进程中模拟读端关闭的代码,并且让写端慢一点,读端关闭时,此时匿名管道无意义,操作系统会向写端发送 SIGPIPE 13 号信号

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

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

cpp 复制代码
// processpool.hpp
#include <iostream>
#include <vector>
#include <functional>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

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 <iostream>
#include <string>

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 影响

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

cpp 复制代码
// namepipe.h
#pragma once

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

// client.cc
#include "namepipe.h"
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

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 <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

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

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

mkfifo 文件名

功能:创建命名管道

相关推荐
友友马9 分钟前
『 Linux 』网络层 - IP协议(一)
linux·网络·tcp/ip
A.A呐2 小时前
【Linux第一章】Linux介绍与指令
linux
Gui林2 小时前
【GL004】Linux
linux
ö Constancy2 小时前
Linux 使用gdb调试core文件
linux·c语言·vim
tang_vincent2 小时前
linux下的spi开发与框架源码分析
linux
xiaozhiwise2 小时前
Linux ASLR
linux
wellnw2 小时前
[linux] linux c实现共享内存读写操作
linux·c语言
a_安徒生2 小时前
linux安装TDengine
linux·数据库·tdengine
追风赶月、2 小时前
【Linux】线程概念与线程控制
linux·运维·服务器
小字节,大梦想2 小时前
【Linux】重定向,dup
linux