进程间通信-对匿名管道的学习

文章目录

介绍

目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源
  • 通知事件:一个进程需要向另一个或另一组进程发送消息,通知发生了某种事件
  • 进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

进程间通信发展

  • 管道
  • System V 进程间通信
  • POSIX 进程间通信

进程间通信分类

  • 管道

    • 匿名管道
    • 命名管道
  • System V IPC

    • System V 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

管道

  • 管道是Unix中的一种进程间通信的形式
  • 将从一个进程连接到另一个进程的一个数据流称为一个"管道"

匿名管道

文件描述符角度理解父、子进程间管道原理

  1. 创建父进程并且以读打开文件
  1. 创建子进程,那么子进程就会继承父进程数据

  2. 子进程关闭以读打开文件,并且重新以写打开文件

  3. 由于子进程以写打开文件,父进程以读打开文件,那么子进程在文件中写的信息,在父进程中就能直接读取

  4. 管道是以类似以上方法实现的,即管道的使用和文件一一致

测试管道读写

cpp 复制代码
#include <unistd.h>
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cerrno>
#include <cstring>


#define ERR_EXIT(m) \
do \
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)

int main(int argc,char *argv[])
{
    int pipefd[2];
    if(pipe(pipefd) == -1)
    {
        ERR_EXIT("pipe error");
    }

    pid_t pid;
    pid = fork();
    if(pid == -1)
    {
        ERR_EXIT("fork_error");
    }
    if(pid == 0)//子进程
    {
        close(pipefd[0]);//关闭读端
        write(pipefd[1],"hello",5);//向管道写入数据
        close(pipefd[1]);//关闭写端
        exit(EXIT_SUCCESS);
    }
    //父进程
    close(pipefd[1]);//关闭写端
    char buf[10] = {0};//缓冲区
    read(pipefd[0],buf,10);//从管道读取数据
    std::cout << "buf = " << buf << std::endl;
    return 0;
}

创建进程池处理任务

Channel.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>

class Channel
{
public:
    Channel(int wfd, pid_t who)
        : _wfd(wfd),
          _who(who)
    {
        _name = "Channel-" + std::to_string(_wfd) + "-" + std::to_string(_wfd);
    }

    std::string Name()
    {
        return _name;
    }

    // 写
    void Send(int cmd)
    {
        write(_wfd, &cmd, sizeof(cmd));
    }

    // 关闭文件描述符
    void Close()
    {
        close(_wfd);
    }

    pid_t Id()
    {
        return _who;
    }

    int wFd()
    {
        return _wfd;
    }

    ~Channel() {}

private:
    int _wfd;          // 文件描述符
    std::string _name; // 格式化进程名
    pid_t _who;        // 进程pid
};

ProcessPool.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <functional>
#include <vector>
#include "Channel.hpp"
#include "Task.hpp"
#include <sys/wait.h>

using work_t = std::function<void()>;

// 进程状态枚举
enum
{
    OK = 0,
    UsageError,
    PipeError,
    ForkError,
};

class ProcessPool
{
public:
    ProcessPool(int processnum, work_t work)
        : _processnum(processnum),
          _work(work)
    {
    }

    void DebugPrint()
    {
        for (auto &channel : _channels)
        {
            std::cout << channel.Name() << std::endl;
        }
    }

    // 初始化线进程池
    int InitProcessPool()
    {
        // 创建指定个数进程
        for (int i = 0; i < _processnum; i++)
        {
            // 1.先有管道
            int pipefd[2] = {0};
            int n = pipe(pipefd); // 创建管道
            if (n < 0)
            {
                return PipeError;
            }

            // 2.创建子进程
            pid_t id = fork();
            if (id < 0)
            {
                return ForkError;
            }

            // 3.建立通信信道
            if (id == 0) // 子进程
            {
                // 关闭历史wfd
                std::cout << getpid() << ",child close history fd:";
                for (auto &channel : _channels)
                {
                    std::cout << channel.wFd() << " ";
                    channel.Close();
                }

                std::cout << "over" << std::endl;

                close(pipefd[1]); // 关闭写端
                // 更新文件描符的指向
                std::cout << "debug: " << pipefd[0] << std::endl; //
                dup2(pipefd[0], 0);
                // 任务执行
                _work();
                exit(0);
            }
            // 父进程执行
            close(pipefd[0]);                      // 关闭读端
            _channels.emplace_back(pipefd[1], id); // 插入一个子进程
        }

        return OK;
    }

    void DispatchTask()
    {
        int who = 0;
        // 派发任务
        int num = 20;
        while (num--)
        {
            /* code */
            // 选择一个任务
            int task = tm.SelectTask();
            // 选择一个子进程
            Channel &curr = _channels[who++];
            who %= _channels.size(); // 防止越界
            std::cout << "######################" << std::endl;
            std::cout << "send " << task << " to " << curr.Name() << ", 任务还剩 : " << num << std::endl;
            std::cout << "######################" << std::endl;
            // 派发任务
            curr.Send(task);
            sleep(1);
        }
    }

    void CleanProcessPool()
    {
        for (auto &channel : _channels)
        {
            channel.Close();
            // 进程回收
            pid_t rid = waitpid(channel.Id(), nullptr, 0);
            if (rid > 0)
            {
                std::cout << "child" << rid << "wait...success" << std::endl;
            }
        }
    }

private:
    std::vector<Channel> _channels;
    int _processnum; // 指定个数进程
    work_t _work;    // 任务
};

Task.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <vector>
#include <unistd.h>
#include <sys/types.h>

using task_t = std::function<void()>;

class TaskManger
{
public:
    TaskManger()
    {
        srand(time(nullptr));
        _tasks.push_back([]()
                         { std::cout << "sub process[" << getpid() << " ] 执⾏访问数据库的任务\n " << std::endl; });
        _tasks.push_back([]()
                         { std::cout << "sub process[" << getpid() << " ] 执⾏url解析\n"
                                     << std::endl; });
        _tasks.push_back([]()
                         { std::cout << "sub process[" << getpid() << " ] 执⾏加密任务\n"
                                     << std::endl; });
        _tasks.push_back([]()
                         { std::cout << "sub process[" << getpid() << " ] 执⾏数据持久化任务\n " << std::endl; });
    }

    // 随机任务选取
    int SelectTask()
    {
        return rand() % _tasks.size();
    }

    // 任务执行
    void Excute(unsigned long number)
    {
        if (number > _tasks.size() || number < 0)
            return;
        _tasks[number]();
    }

    ~TaskManger()
    {
    }

private:
    std::vector<task_t> _tasks;
};

TaskManger tm;

void Worker()
{
    while (true)
    {
        int cmd = 0;
        // 读管道
        int n = read(0, &cmd, sizeof(cmd));
        if (n == sizeof(cmd)) // 执行任务
        {
            tm.Excute(cmd);
        }
        else if (n == 0)
        {
            std::cout << "pid: " << getpid() << " quit..." << std::endl;
            break;
        }
        else
        {
        }
    }
}

main.cc

cpp 复制代码
#include "ProcessPool.hpp"
#include "Task.hpp"
void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " process-num" << std::endl;
}
// 我们⾃⼰就是master
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return UsageError;
    }
    int num = std::stoi(argv[1]);
    ProcessPool *pp = new ProcessPool(num, Worker);
    // 1. 初始化进程池
    pp->InitProcessPool();
    // 2. 派发任务
    pp->DispatchTask();
    // 3. 退出进程池
    pp->CleanProcessPool();
    delete pp;
    return 0;
}

进程池单进程创建及执行任务:

进程池多进程创建及执行任务

管道读写规则

  • 当没有数据可读时
    • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    • O_NONBLOCK enable: read调用返回-1, errno值为EAGAIN。
  • 当管道满时
    • O_NONBLOCKdisable: write调用阻塞,直到有进程读走数据
    • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

进程读走数据

  • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
相关推荐
人邮异步社区1 小时前
完全没接触过AI/NLP,如何系统学习大模型?
人工智能·学习·自然语言处理·大模型
蒋士峰DBA修行之路1 小时前
红帽练习环境介绍
linux·开发语言·bash
2301_807583231 小时前
Linux-虚拟化技术概述及KVM虚拟机环境部署
linux·运维·服务器
HalvmånEver1 小时前
Linux:命令行参数与环境变量(进程五)
linux·运维·服务器
python百炼成钢1 小时前
43.Linux LCD驱动
java·linux·运维·驱动开发
QiZhang | UESTC1 小时前
学习日记day37
学习
YJlio1 小时前
SDelete 学习笔记(9.9):安全擦除原理、SSD 场景与企业合规实战
笔记·学习·安全
axihaihai1 小时前
maven的构建问题
java·linux·maven
稚辉君.MCA_P8_Java1 小时前
DeepSeek Java 多线程打印的12种实现方法
java·linux·后端·架构·maven