【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,失败返回错误代码

这两个文件描述符fd0和fd1,会被填入当前进程的 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

直接调用 gtaskscode() 执行任务

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

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
相关推荐
Sinclair3 分钟前
安企CMS的安装-宝塔面板部署
服务器·后端
希望奇迹很安静29 分钟前
数通基本实验
运维·服务器·网络
minglie135 分钟前
zynq的linux驱动版终端AtShellDrv
linux
汪汪大队u1 小时前
【无标题】
运维·docker·kubernetes·prometheus
学术小白人2 小时前
第二届智能制造、机器人与自动化国际学术会议(IMRA 2026)
运维·人工智能·机器人·自动化·制造
吴声子夜歌3 小时前
Redis 3.x——集群运维
运维·数据库·redis
欧神附体1233 小时前
在虚拟机搭建本地仓库和网络仓库
linux·运维·服务器
是个西兰花3 小时前
Linux:深入解析Linux线程原理与实现
linux·运维·c++·线程·互斥锁
云泽8084 小时前
飞算 JavaAI 实战:如何自动化生成全流程追溯系统
运维·自动化·vibe coding·飞算javaai·飞算java ai炫技赛
运维大师4 小时前
【Linux运维极简教程】07-磁盘管理与LVM
linux·运维·服务器