进程间通信 之 管道

本文详细介绍了管道通信的基本概念、特点,包括匿名管道和命名管道的工作原理,以及如何在父子进程间使用它们进行通信。此外,还探讨了进程池的设计和应用,展示了如何利用管道技术实现高效的并发任务执行。
摘要由CSDN通过智能技术生成

目录

什么是管道通信

管道通信的特点

匿名管道

命名管道


进程间通信的本质:让不同的进程看到同一份资源!

什么是管道通信

管道是Unix中最古老的进程间通信的形式。它是一种基于文件的通信形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。

管道文件是一种纯内存文件,不需要刷新到磁盘。管道只允许单向通信,如果要双向通信的话,需要建立两个管道,“互相读写”!

管道通信的特点

管道通信时可能会遇到以下四种情况:

  • 1、正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据)
  • 2、正常情况,如果管道被写满了,写端就必须等待,直到有空间为止(读端读走数据)
  • 3、写端关闭,读端如果一直读的话,就会导致 read 返回值为0,表示读到了文件结尾为止
  • 4、读端关闭,写端一直写入,OS会直接杀掉写端进程,通过向目标文件发送 SIGPIPE(13) 信号,终止目标进程。

管道在通信时有以下五种状态

  • 1、匿名管道,可以允许有血缘关系的进程之间进行进程间通信,常用于父子,但匿名管道的通信也仅限于此!
  • 2、匿名管道,默认给读写端要提供同步机制
  • 3、是面向字节流的(面向字节流读取, 跟写的时候的格式无关, 读取的时候只跟字节数有关)
  • 4、管道的声明周期是跟随进程的
  • 5、管道是单向通信的,它是半双工通信的一种特殊情况。

匿名管道

匿名管道没有文件名,只有文件描述符。可以调用 pipe 函数来创建匿名管道

其中,传进去一个数组,pipefd[0] 表示读端文件描述符,pipedf[1] 表示写端文件描述符。

返回值:创建成功返回0,创建失败返回-1,并设置错误码!

当用父子进程之间创建管道来通信时:

上述的管道特点、匿名管道的原理等。都可以使用下面的代码来测试(代码有超详细注释)

下面这段代码创建了一个简单的单向通信的管道

#include <iostream>
#include <unistd.h> // 操作系统调用接口, 只能 .h 结尾(规定的)
#include <cassert>  //   以 c 开头 + C语言头文件(不加 h) 就可以重新封装一套 C 语言头文件 #include<cstdio>  #include<cstring>
#include <sys/types.h>
#include <cstdio>
#include <cstring>
#include <sys/wait.h>
using namespace std;
#define MAX 1024

int main()
{
    int pipefd[2] = {0};

    // 第一步:建立管道
    int n = pipe(pipefd); // 管道创建成功    等于 0 创建成功,创建失败返回 -1
    assert(n == 0);       // 只在 debug 下存在   assert 用于意料之外的问题!
    (void)n;              // 假装使用一下, 防止 n 没有被使用而报警
    // cout << pipefd[0] << " " << pipefd[1] << endl; // 使用 pipe 函数后 pipe[0], pipe[1] 分别自动被设置为 3,4 (未被使用的两个最小的文件描述符)
    // pipefd[0] 读端  pipefd[1] 写端

    // 第二步:创建子进程
    pid_t id = fork();
    if (id < 0)
    {
        // 创建失败
        perror("fork");
        return 1;
    }

    // 第三步:父子关闭不需要的文件描述符,形成单向通信的信道
    // 子进程写入,父进程读取
    if (id == 0)
    {
        // 子进程
        close(pipefd[0]);
        int cnt = 10;
        // 只向管道写入
        while(cnt)
        {
            char message[MAX];
            snprintf(message, sizeof(message) - 1, "Hello father, I am child, pid: %d, cut: %d", getpid(), cnt);
            cnt--;
            write(pipefd[1], message, strlen(message));
            sleep(1);
        }
        // TODO
        exit(0);
    }
    // 父进程
    close(pipefd[1]); // 父进程关闭写端 
    char buffer[MAX];
    while(true)
    {
        ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1); // 一会要给最后一个位置加 '\0',方便 C/C++ 使用(万一读满了, 就最多读 MAX - 1 个)
        // 面向字节流读取, 跟写的时候的格式无关, 读取的时候只跟字节数有关
        if(n > 0)
        {
            buffer[n] = 0; // 自己维护字符串(为 C++ 准备的)
            cout << getpid() << " :child say:" << buffer << " to me !" <<endl;
        }
        if(n == 0) break; // 说明写端关闭了(不是写端不写,而是关闭了)读端就会关闭
    }
    // 子进程退出后才能等待到
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id)
    {
        // 等待成功
        cout << "wait success, child exit sig: " << (status &0x7F) << endl; // 退出信号 :status 的低 7 位
    }
    return 0;
}

使用管道技术设计一个简单的进程池

进程池(Process Pool)是一种并发编程的技术,它允许创建一组预先分配的子进程,这些子进程可以被重复地使用来执行任务。

通常情况下,每个进程都会拥有一个独立的地址空间和资源,这会导致进程的创建和销毁需要耗费大量的时间和资源。而进程池技术通过预先创建一组子进程,这些子进程会在一个池中等待任务分配。当有任务需要执行时,只需要将任务交给其中一个空闲的子进程即可,不需要重复地创建和销毁进程。

进程池通常由一个主进程(也称为管理进程)和一组子进程组成。主进程负责管理子进程的创建、销毁和任务分配,而子进程则负责执行实际的任务。

进程池的主要优势:提高效率、简化编程、提高可扩展性进程池主要适用于CPU密集型的任务,即任务主要涉及到计算和数据处理。

#include <iostream>
#include <cassert>
#include <unistd.h>
#include <functional>
#include <ctime>
#include <vector>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
typedef std::function<void()> task_t;
void Download()
{std::cout << "我是一个下载任务" << std::endl;}
void PrintLog()
{std::cout << "我是一个打印日志的任务" << std::endl;}
void PushVideoStream()
{std::cout << "这是一个推送视频流的任务" << std::endl;}
// void ProcessExit()
// {exit(0);}

class Init
{
public:
    // 任务码
    const static int g_download_code = 0;
    const static int g_printlog_code = 1;
    const static int g_pushVideostream_code = 2;
    // 任务集合
    std::vector<task_t> tasks;
public:
    Init()
    {
        tasks.push_back(Download);
        tasks.push_back(PrintLog);
        tasks.push_back(PushVideoStream);
        srand(time(nullptr) ^ getpid());
    }
    bool CheckSafe(int code)
    {
        if(code >= 0 && code < tasks.size()) return true;
        else return false;
    }
    void RunTask(int code)
    {
        return tasks[code]();
    }
    int SelectTask()
    {
        int code = rand() % tasks.size();
        return code;
    }
    std::string ToDesc(int code) //将任务码转化为任务字符传串
    {
        switch (code)
        {
            case g_download_code:
                return "g_download_code";
            case g_printlog_code:
                return "g_printlog_code";
            case g_pushVideostream_code:
                return "g_pushVideostream_code";
            default:
                return "Unknow";
        }
    }
};

Init init; // 定义对象
 
const int num = 5;
static int number = 1;
class channel // 先描述, 再组织
{
public:
    channel(int fd, pid_t id)
        : ctrlfd(fd),
          workerid(id)
    {
        name = "channel-" + std::to_string(number++);
    }

public:
    int ctrlfd;
    pid_t workerid;
    std::string name;
};

void Work()
{
    while (true)
    {
        int code = 0;
        ssize_t n = read(0, &code, sizeof(code)); // n 表示每次读到的字节个数
        if (n == 0) break;                               // 等于0 说明啥都没读到退出子进程并回收
        (void)n;
        if (!init.CheckSafe(code))
            continue; // code 不合法, 不在任务范围之内
        init.RunTask(code);
    }
}
void PrintDebug(const std::vector<channel> &c)
{
    for (const auto &channel : c)
    {
        std::cout << channel.ctrlfd << "," << channel.workerid << "," << channel.name << std::endl;
    }
}

void CreateChannels(std::vector<channel> *c)
{
    std::vector<int> old; // 用来保存父进程的写端文件描述符
    for (int i = 0; i < num; i++)
    {
        // 1. 先创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 2. 创建子进程
        pid_t id = fork();
        assert(id != -1);

        // 3. 构建单向信道
        if (id == 0) // child
        {
            if(!old.empty())
            {
                for(int i = 0; i < old.size(); i++)
                {
                    close(old[i]); // 关闭继承过来的父进程的文件描述符
                }
            }
            close(pipefd[1]);
            dup2(pipefd[0], 0); // 管道直接切换为从标准输入中读(重定向)
            Work();
            exit(0);
        }
        // father
        close(pipefd[0]);
        old.push_back(pipefd[1]);  //保存这次进程的写端, 方便下次的子进程关闭这些从父进程中继承过来的不需要要的端口
        c->push_back(channel(pipefd[1], id));
        // std::cout << id << std::endl;
    }
}
void SendCommand(std::vector<channel> &c, bool flag, int num = 0)
{
    int quit = 0;
    int pos = 0;
    while (true)
    {
        // 1、选择任务
        int command = init.SelectTask();
        // 2、选择信道
        const auto &channel = c[pos++];
        pos %= c.size();
        // debug
        std::cout << "send command" << 1 << "in" << channel.name <<"worker is" << std::endl;
        sleep(10);
        // 3、发送任务
        write(channel.ctrlfd, &command, sizeof(command)); // 发送任务
        // 4、判断是否要退出
        if (!flag)
        {
            num--;
            if (num <= 0)
                break;
        }
        //sleep(1);
    }
    std::cout << "SendCommand done......" << std::endl;
}
void ReleaseChannels(std::vector<channel>& c)
{
    // version 2
    int num = c.size() - 1;

    for(; num >= 0; num --)
    {
        close(c[num].ctrlfd);
        pid_t rid = waitpid(c[num].workerid, nullptr, 0);
    }
    // version 1
    // // 回收资源,想让子进程退出,释放资源,只需要关闭写端
    // for (const auto &channel : c)
    // {
    //     close(channel.ctrlfd);
    //     // pid_t rid = waitpid(channel.workerid, nullptr, 0); 如果这样做就会出现, 其他子进程的文件描述符还指向写端,导致写端无法正常关闭, 进而导致 wait 读不到数据
    // }
    // // 回收子进程
    // for (const auto &channel : c)
    // {
    //     pid_t rid = waitpid(channel.workerid, nullptr, 0);
    //     if (rid == channel.workerid)
    //     {
    //         std::cout << "wait child" << channel.workerid << "success" << std::endl;
    //     }
    // }
}
const bool g_always_loop = true;
int main()
{
    std::vector<channel> channels;
    CreateChannels(&channels);
    // PrintDebug(channels);
    SendCommand(channels, !g_always_loop, 1000);
    ReleaseChannels(channels);
    // sleep(10);
    return 0;
}

命名管道

匿名管道没有名字,可以让有血缘关系的进程,通过继承的属性,进行管道通信,而命名管道则是可以让两个好不相干的进程进行管道通信!

1、两个进程中的其中一个进程调用系统接口 mkfifo 创建命名管道文件。

创建成功返回0,创建失败返回 - 1,并设置错误码 。

2、使用这个管道文件进行通信,一个进程为读端,一个进程为写端。

一个进程使用 write 接口往这个管道文件写,一个进程使用 read 接口从这个管道文件里读,就形成一个管道式的通信。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值