【Linux】进程间通信(1)管道&&进程池实现

目录

[一 系统及语言变化](#一 系统及语言变化)

[二 进程间通信简介](#二 进程间通信简介)

[1 是什么----本质前提](#1 是什么----本质前提)

[2 为什么要通信](#2 为什么要通信)

[3 怎么进行通信](#3 怎么进行通信)

[三 管道](#三 管道)

[1 什么是管道](#1 什么是管道)

[2 匿名管道原理](#2 匿名管道原理)

[3 demo代码](#3 demo代码)

(1)创建管道

(2)创建子进程

(3)形成单向通信的管道

[4 管道通信场景](#4 管道通信场景)

(1)场景1

(2)场景2

(3)场景3

(4)场景4

[四 实践:进程池实现](#四 实践:进程池实现)

[1 任务模块](#1 任务模块)

[2 子进程工作模块](#2 子进程工作模块)

[3 Channel 类(管道 + 子进程封装)](#3 Channel 类(管道 + 子进程封装))

[4 进程池核心(ProcessPool)](#4 进程池核心(ProcessPool))

[(1) Start () ------ 创建 N 个子进程 + N 条管道](#(1) Start () —— 创建 N 个子进程 + N 条管道)

[(2)PushTask () ------ 派发任务(负载均衡)](#(2)PushTask () —— 派发任务(负载均衡))

(3)Stop () ------ 关闭进程池Stop () —— 关闭进程池)

[5 main 函数(流程控制)](#5 main 函数(流程控制))

[6 进程池完整代码](#6 进程池完整代码)


一 系统及语言变化

从这节博客开始,我们就把语言切换为C++,系统由centos7 切换为ubentu 24.04/20.04 ,vscode远程开发

vim被vscode所替代;vim虽然写代码的效率高,但是不一定开发效率高;vscode是一款文件编辑器,它是一个轻量化,插件式的软件

vscode打开文件夹,是默认打开你的电脑里的特定目录

安装Remote - SSH 插件:

Remote - SSH 是 VS Code 的官方插件,核心作用是:通过 SSH 协议连接远程服务器 / 虚拟机,在本地 VS Code 里直接编辑、运行、调试远程代码

vscode和xhsell协调步骤:

**(1)**搜索插件Remote-SSH插件,安装(vscode中)

(2)出现远端资源管理器,点击后出现加号,点击输入 ssh @IP地址,之后选择一个配置文件,右下角就会出下降:已添加主机

(3)点击刷新,就会出现一台主机(左上角),点击这一排右边的任意一个按键,后输入主机密码,就链接成功

(4)在xshell中,mkdir xxx创建一个新目录

之后在VS中,打开文件夹,选择对应路径,当前打开就在家目录下;之后会再让输入一次密码,写入代码,用crtl+s保存

这样协调操作就完成了

vscode中,光标在哪一行,crtl+c,crtl+v 这一行就会直接复制,粘贴

crtl+~(波浪号):在vscode中出现命令行终端,在这一行下面,就可以输入Linux目录,就相当于一台小型的机器远端xshell

不推荐用vscode做本地和远端的调试,推荐使用cgbd

好了,现在我们可以进入进程间通信的学习了


二 进程间通信简介

1 是什么----本质前提

进程间通信指的就是两个或多个进程,进行互相传递信息的过程

进程具有独立性-->进程=内核数据结构+代码和数据,要规避进程之间的耦合关系

所以,至少在目前,一个进程把自己的数据,发送给另一个进程也是一件比较困难的事

但是父进程的全局变量不是能交给子进程吗? 但是这不是通信,是父给子的,且只能给全局变量,所以只能叫做继承数据,无法做到持续性通信

2 为什么要通信

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

3 怎么进行通信

假如在操作系统有两个进程:A B,B不能看到A申请的空间,A也不能看B的,那么怎么进行通信呢?

创建一个进程交换的公共场所(本质是内存),由操作系统提供

进程间通信的本质前提:让不同的进程,看到同一份资源(资源由操作系统提供),我们后续进行进程间通信时,大部分时间都是想办法让进程看到同一份资源(利用系统调用)

操作系统必然提供系统调用

为通信接口制定标准:我们用到的是System V

复用文件部分代码,最小的通信代价:管道

常见的通信方式:管道(匿名管道,命名管道)


三 管道

1 什么是管道

管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"

结论1:父子进程为什么会向同一个显示器打印?因为指向了同一个文件

父进程写好一份文件之后,再创建子进程,子进程就能共享这份文件,一个向里面写入,一个向外读取

结论2:以文件形式继承给子进程,这种通信方案,叫做管道(说明管道本质是文件)

进程不能用open打开磁盘文件,需要系统调用

管道在设计之初,只允许进行单向数据通信,父进程关闭不需要文件描述符

管道特点1:基于文件的单向数据通信--->为了简单化设计,不需要考虑数据朝向问题

为什么父进程需要董事打开读写:为了让子进程继承读写打开的方式

2 匿名管道原理

cpp 复制代码
#include <unistd.h>
功能:创建⼀匿名管道
原型
int pipe(int fd[2]);
参数
fd:⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端
返回值:成功返回0,失败返回错误代码

这两个文件描述符fd[0]和fd[1],会被填入当前进程的 files_struct 文件描述符表中(占下标3,4的位置),指向内核中同一个管道文件对象。

每个文件对象都有独立的文件偏移量 pos:读端的 pos 记录读取位置,写端的 pos 记录写入位置;

它们共享同一个内核缓冲区和 inode,数据会先写入缓冲区,再由读端从缓冲区读取

匿名管道的本质,是操作系统内核在内存中创建的一个临时文件,它具备以下特点:
没有磁盘路径、没有文件名,因此也被称为匿名管道;
不占用磁盘空间,数据仅存放在内核的缓冲区中;
拥有独立的 inode 节点,但该节点只存在于内存中,不持久化到磁盘

3 demo代码

我们来写一段代码,需求是:子进程进行写入,父进程读取,父子进程传递可变字符串

注意:C++文件后缀:.cc .cpp .cxx

(1)创建管道

管道头文件:

cpp 复制代码
#include<unsitd.h>

输出型数组:

cpp 复制代码
// pipefd[0] = 读端
// pipefd[1] = 写端
    int pipefd[2] = {0};
    pipe(pipefd);

(2)创建子进程

管道特点2:管道只能用来让具有血缘关系的进程进行进程间通信,常用于父子进程之间,进行进程间通信

cpp 复制代码
pid_t id = fork();

(3)形成单向通信的管道

子进程想写:保留1,关闭0

cpp 复制代码
closed(pipefd[0]);

父进程想读:保留0,关闭1

cpp 复制代码
closed(pipefd[1]);

管道特点3:管道的本质是文件,一般文件,如果打开它的进程推出了,那么文件也会被系统自动的关闭!打开文件的生命周期随进程

父进程关闭,子进程不管-->管道还在维持,有相关文件没有结束

子进程写数据函数:WriteData,父进程读数据函数:ReadData

cpp 复制代码
if(id == 0)
    {
        // 子进程:只写,关闭读端
        close(pipefd[0]);
        WriteData(pipefd[1]);
        close(pipefd[1]);
        exit(0);
    }
    else
    {
        // 父进程:只读,关闭写端
        close(pipefd[1]);
        ReadData(pipefd[0]);
        close(pipefd[0]);
    }
cpp 复制代码
void WriteData(int wfd)
{
    int cnt = 1;
    pid_t id = getpid();

    while(true)
    {
        sleep(1);  // 每秒写一次

        // 构造消息
        std::string message = "hello father...";
        message += ...;

        // 向管道写
        write(wfd, message.c_str(), message.size());
    }
}
cpp 复制代码
void ReadData(int rfd)
{
    char inbuffer[1024];
    while(true)
    {
        sleep(5);  // 每5秒读一次

        // 从管道读
        ssize_t n = read(rfd, inbuffer, sizeof(inbuffer)-1);

        if(n > 0)
        {
            inbuffer[n] = '\0';
            std::cout << "读到:" << inbuffer << std::endl;
        }
    }
}

管道特点4:管道自己内部实现了:进程间同步

我们整理一下上面的特点:

1 :管道在设计之初,只允许进行单项数据通信

管道特点之一:基于文件的,单向数据通信

2 :管道只能用来让具有血缘关系的进程,进行进程间通信

常用于父子进程之间,进行进程间通信!

3: 管道的本质是文件,一般文件,如果打开它的进程退出了,那么文件也会被系统自动关闭!

打开的文件的生命周期随进程!

4:管道自己内部是实现了:进程间的同步!

5 :管道是面向字节流的!

4 管道通信场景

(1)场景1

写端很慢,读端很快,以慢的节奏来----父进程等待数据进入,即等待子进程进入

管道是面向字节流的:最朴素的认识-->读写次数不匹配,读端可以按照自己的需求读

读写次数匹配场景:发,取快递:别人给你发了几个快递,你就要取几个快递

读写不匹配场景:一次买了100吨水,但是每次的使用量不同,可能是10吨,也可能是几百毫升

(2)场景2

写端很快,读端很慢,读端会把写端写入的数据能一次读上了,就全部读上来

极端化场景:写端特别快,读端不读-->管道会被写慢,写满后写端怎么办?

写进程会被阻塞

我们这个代码的验证发现写到65536就不写了--->65536/1024=64

所以管道会被写满,最多写64KB,读端不读,写端一直写道把管道阻塞

(3)场景3

写端不写,关闭写端(close(fwd)),读端会怎么样?

读端把管道内容全部读完,会读到0,表示end of file,读到文件结尾

read:系统调用,read返回0,读到文件结尾

场景3是不通信,正常退出的场景

(4)场景4

写端一直写,读端不读了且关闭读端close(rwd)

操作系统不会做任何浪费时间和空间的事,写端一直写毫无意义,操作系统会通过信号杀掉写进程(13号信号,SIGPIPE)

什么时候用到管道


四 实践:进程池实现

用进程池比再去修改shell更有价值

池化技术:减少系统调用的次数,提高效率(调用系统调用成本高)

池:提高效率 进程池:预先创建子进程,有任务先处理任务,不需要有任务时,再创建子进程

进程池要先创建一批子进程,父进程想向哪个子进程写入,就向哪个子进程写入,控制写入

**任务码:**父进程发给子进程的 "指令编号",用来告诉子进程要执行什么任务。不是固定的系统概念,是自己定义的一个数字

父进程可以通过给任意一个子进程发送任务码的情况,写入代码

负载均衡:如果一直让一个子进程完成任务,其他子进程不安排任务,就会造成忙闲不均;把任务码均匀的撒到每一个子进程,让子进程以相同的压力工作,提高系统的稳定性--->做法可以有:随机数,轮询,计数器....

代码整体结构:

cpp 复制代码
1. 任务模块:定义4种任务 + 任务码
2. 子进程工作逻辑:从管道读任务码,执行对应任务
3. Channel类:管理【子进程PID + 父进程写端fd】
4. 进程池类:创建N个子进程 + 管道,实现负载均衡
5. main函数:加载任务 → 启动进程池 → 推送任务 → 关闭进程池

1 任务模块

cpp 复制代码
// 任务类型:用数字代表要做什么事 ------ 这就是【任务码】
#define LOG_TASK 0       // 打印日志
#define DOWNLOAD_TASK 1  // 下载
#define MYSQL_TASK 2     // 访问数据库
#define REDIS_TASK 3     // 访问redis

// 所有任务放到一个数组里
std::vector<task_t> gtasks;

// 加载任务:把函数放进任务列表
void LoadTask()
{
    gtasks.push_back(printlog);   // 下标0
    gtasks.push_back(download);   // 下标1
    gtasks.push_back(readmysql);  // 下标2
    gtasks.push_back(writeredis); // 下标3
}

任务码 = 数字编号

0、1、2、3 分别代表一种任务

子进程收到数字,就执行对应函数

2 子进程工作模块

cpp 复制代码
void Work(int rfd)
{
    while (true)
    {
        int code = 0;
        // 子进程阻塞读:等待父进程发任务码
        ssize_t n = read(rfd, &code, sizeof(code));

        if (n == sizeof(int))
        {
            // 执行任务码对应的任务
            gtasks[code]();
        }
        else if (n == 0)
        {
            break; // 父进程关闭写端 → 子进程退出
        }
    }
}

子进程循环等待父进程发任务

读到任务码 code

直接调用 gtasks[code]() 执行任务

父进程关闭管道 → 子进程自动退出

3 Channel 类(管道 + 子进程封装)

cpp 复制代码
class Channel
{
public:
    Channel(int wfd, pid_t who)
        : _wfd(wfd),     // 父进程写端fd
          _sub_process_id(who)  // 子进程pid
    {}

    // 父进程通过这里发送【任务码】
    void SendTask(int taskcode)
    {
        write(_wfd, &taskcode, sizeof(taskcode));
    }
};

4 进程池核心(ProcessPool)

(1) Start () ------ 创建 N 个子进程 + N 条管道

cpp 复制代码
void Start()
{
    for (int i = 0; i < _number; i++)
    {
        // 1. 创建管道
        int pipefd[2];
        pipe(pipefd);

        // 2. 创建子进程
        pid_t id = fork();

        if(id == 0) // 子进程
        {
            close(pipefd[1]); // 子进程关写端
            Work(pipefd[0]);  // 子进程进入工作循环
            close(pipefd[0]);
            exit(0);
        }
        else // 父进程
        {
            close(pipefd[0]); // 父进程关读端
            _channels.emplace_back(pipefd[1], id);
        }
    }
}

创建 N 个子进程

每个子进程绑定一条独立管道

父进程只保留写端

子进程只保留读端

(2)PushTask () ------ 派发任务(负载均衡)

cpp 复制代码
void PushTask(int taskcode)
{
    int who = Next(); // 轮询选一个子进程
    _channels[who].SendTask(taskcode); // 发任务码
}

轮询调度:依次给每个子进程发任务

发送的内容就是任务码(一个 int 数字)

(3)Stop () ------ 关闭进程池

cpp 复制代码
void Stop()
{
    for(auto &channel: _channels)
    {
        channel.Close();  // 关闭写端 → 子进程读到0退出
        channel.Wait();   // 回收子进程
    }
}

父进程关闭所有管道写端

子进程 read 返回 0 → 自动退出循环

父进程 wait 回收,防止僵尸进程

5 main 函数(流程控制)

cpp 复制代码
int main()
{
    // 1. 加载任务列表
    LoadTask();

    // 2. 创建进程池(例如5个子进程)
    std::unique_ptr<ProcessPool> pp = make_unique<ProcessPool>(5);

    // 3. 启动进程池:创建进程+管道
    pp->Start();

    // 4. 推送任务(任务码)
    pp->PushTask(0);
    pp->PushTask(1);
    pp->PushTask(2);
    pp->PushTask(3);

    // 5. 关闭进程池
    pp->Stop();
}

6 进程池完整代码

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <functional>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>

#define __MAIN__

///////////////////////任务测试代码/////////////////////////
using task_t = std::function<void()>;

void printlog()
{
    // sleep(1);
    std::cout << "我是一个打印日志的任务, pid: " << getpid() << std::endl;
}

void download()
{
    // sleep(1);
    std::cout << "我是一个下载任务, pid: " << getpid() << std::endl;
}

void readmysql()
{
    // sleep(1);
    std::cout << "我是一个访问数据库的任务, pid: " << getpid() << std::endl;
}

void writeredis()
{
    // sleep(1);
    std::cout << "我是一个访问redis的任务, pid: " << getpid() << std::endl;
}

std::vector<task_t> gtasks;

void LoadTask()
{
    gtasks.push_back(printlog);
    gtasks.push_back(download);
    gtasks.push_back(readmysql);
    gtasks.push_back(writeredis);
}
// *: 输出型参数
// const &: 输入型参数
// &: 输入输出型
void RandomTask(std::vector<int> *out)
{
    for (int i = 0; i < 50; i++)
    {
        int code = rand() % gtasks.size();
        usleep(23223);
        out->push_back(code);
    }
}

#define LOG_TASK 0
#define DOWNLOAD_TASK 1
#define MYSQL_TASK 2
#define REDIS_TASK 3

std::string Task2String(int code)
{
    switch (code)
    {
    case LOG_TASK:
        return "printlog";
    case DOWNLOAD_TASK:
        return "download";
    case MYSQL_TASK:
        return "readmysql";
    case REDIS_TASK:
        return "writeredis";
    default:
        return "unknown";
    }
}

///////////////////////进程池代码/////////////////////////

void Work(int rfd)
{
    while (true)
    {
        int code = 0;
        ssize_t n = read(rfd, &code, sizeof(code));
        if (n == sizeof(int))
        {
            if (code >= 0 && code < gtasks.size())
            {
                gtasks[code]();
            }
        }
        else if (n == 0)
        {
            break; // 子进程只要读到返回值为0, 表明父进程让我退出
        }
        else
        {
            break;
        }
    }
}

class Channel
{
public:
    Channel(int wfd, pid_t who) : _wfd(wfd), _sub_process_id(who)
    {
        _name = "Channel-" + std::to_string(_sub_process_id) + "-" + std::to_string(_wfd);
    }
    int Fd()
    {
        return _wfd;
    }
    pid_t SubId()
    {
        return _sub_process_id;
    }
    std::string Name()
    {
        return _name;
    }
    void Close()
    {
        if (_wfd >= 0)
            close(_wfd);
    }
    void Wait()
    {
        pid_t rid = waitpid(_sub_process_id, nullptr, 0);
        (void)rid;
    }
    void SendTask(int taskcode)
    {
        ssize_t n = write(_wfd, &taskcode, sizeof(taskcode));
        (void)n;
    }
    ~Channel()
    {
    }

private:
    int _wfd;
    pid_t _sub_process_id;
    std::string _name;
};

class ProcessPool
{
private:
    int Next()
    {
        int choice = _next_choice;
        _next_choice++;
        _next_choice %= _channels.size();
        return choice;
    }

public:
    ProcessPool(int number) : _number(number), _next_choice(0)
    {
        std::cout << "number: " << _number << std::endl;
    }
    // 父进程
    void Start()
    {
        for (int i = 0; i < _number; i++)
        {
            // 1. 创建管道
            int pipefd[2];
            int n = pipe(pipefd);
            if (n < 0)
            {
                perror("pipe");
                exit(2);
            }
            // 2. 创建子进程
            pid_t id = fork();
            if (id < 0)
            {
                perror("fork");
                exit(3);
            }
            else if (id == 0) // 子进程
            {
                // 关闭父进程历史的wfd!
                for(auto &channel : _channels)
                    channel.Close();

                close(pipefd[1]);
                Work(pipefd[0]);
                close(pipefd[0]);
                exit(0);
            }
            else // 父进程
            {
                close(pipefd[0]);
                // pipefd[1];
                _channels.emplace_back(pipefd[1], id);
            }
        }
    }
    // 1. 什么任务?任务码决定
    // 2. 任务给谁?属于进程池内部操作,负载均衡
    void PushTask(int taskcode)
    {
        // 选择一个子进程
        int who = Next();
        _channels[who].SendTask(taskcode);

        std::cout << "发送任务: " << Task2String(taskcode) << "[" << taskcode << "]" << " 给: " << _channels[who].Name() << std::endl;
    }
    void Stop()
    {
        // version 2 ???
        for(auto &channel: _channels)
        {
            channel.Close();
            channel.Wait();
            std::cout << channel.Name() << " close and wait success!" << std::endl;
        }

        // version3
        // int end = _channels.size() - 1;
        // while(end >= 0)
        // {
        //     _channels[end].Close();
        //     _channels[end].Wait();
        //     std::cout <<  _channels[end].Name() << " closea and wait success!" << std::endl;
        //     end--;
        // }

        // 内部bug!
        // 1. 关闭wfd -- version1
        // for (auto &channel : _channels)
        // {
        //     channel.Close();
        //     std::cout << channel.Name() << " close success!" << std::endl;
        // }

        // // sleep(3);
        // // // 2. 回收子进程
        // for (auto &channel : _channels)
        // {
        //     channel.Wait();
        //     std::cout << channel.Name() << " wait success!" << std::endl;
        // }
    }
    void DebugPrint()
    {
        std::cout << "-------------------------------------" << std::endl;
        for (auto &channel : _channels)
        {
            std::cout << channel.Fd() << std::endl;
            std::cout << channel.SubId() << std::endl;
            std::cout << channel.Name() << std::endl;
        }
        std::cout << "-------------------------------------" << std::endl;
    }
    ~ProcessPool() {}

private:
    std::vector<Channel> _channels;
    int _number;
    int _next_choice;
};

// 父进程

#ifdef __MAIN__

static void Usage(const std::string &proc)
{
    std::cout << "Usage:\n\t" << proc << " process_number" << std::endl;
}

// ./process_pool 5
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    int number = std::stoi(argv[1]);
    // 0. 加载任务
    srand(time(nullptr) ^ getpid());
    LoadTask();
    std::vector<int> task_codes;
    RandomTask(&task_codes);

    // 1. 创建进程池对象
    std::unique_ptr<ProcessPool> pp = std::make_unique<ProcessPool>(number);
    // 2. 启动进程池
    pp->Start();
    sleep(2);
    // for (auto task : task_codes)
    // {
    //     pp->PushTask(task);
    //     usleep(500000);
    // }

    // while(true)
    // {
    //     // int code = 0;
    //     // std::cout << "Please Enter Your Task# ";
    //     // std::cin >> code;
    //     // if(code < 0 || code > gtasks.size())
    //     // {
    //     //     std::cout << "任务码错误, 请重新输入"<< std::endl;
    //     //     continue;
    //     // }

    //     pp->PushTask(code);
    // }

    pp->Stop();
    return 0;
}

#endif
相关推荐
Miki Makimura2 小时前
C++ 聊天室项目:Linux 环境搭建与问题总结
linux·开发语言·c++
Yiyi_Coding2 小时前
bat 脚本(真实项目可用):ftp取远程文件
运维·脚本·ftp
开开心心_Every2 小时前
实用PDF擦除隐藏信息工具,空白处理需留意
运维·服务器·网络·pdf·电脑·excel·依赖倒置原则
Hello World . .2 小时前
Linux:Linux命令行音视频播放器
linux·音视频
qZ6bgMe432 小时前
一个高性能的 .NET MQTT 客户端与服务器库
运维·服务器·.net
kvo7f2JTy2 小时前
.NET 11 预览版1:CoreCLR 在 WebAssembly 上的全面集成与性能突破
服务器·.net·wasm
做cv的小昊2 小时前
【conda】打包已有conda环境并在其他服务器上搭建
运维·服务器·python·conda·运维开发·pip·开发
Vfw3VsDKo2 小时前
Android设备搭建本地RTSP服务器(基于live555)
android·运维·服务器
YYYing.2 小时前
【Linux/C++网络篇(二) 】TCP并发服务器演进史:从多进程到Epoll的进化指南
linux·服务器·网络·c++·tcp/ip