Linux 进程通信——匿名管道

从本章开始,我们开始进入Linux进程通信的话题。首先我们要讲的是Linux中最古老的通信方式之一------匿名管道通信,本节将从理解通信和具体介绍匿名管道两方面讲解。

一.理解进程间通信

1.进程通信的目的

数据的传输,不仅限于同一主机的不同进程,将来学习Linux网络部分我们还会学到跨网络进程的通信。进程通信的目的简单来说就以下四点:

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

2.进程通信技术

随着计算机和互联网的发展,衍生出了不同的进程通信技术(IPC技术)。进程通信的本质可以看作:先让不同 的进程,看到同一份资源(绝不能由任意一个进程提供,而是OS),才有通信的条件。

通信技术的发展大概可以分为三个阶段:

1、管道通信

2、System V进程间通信

3、POSIX进程间通信

我们本节讲解的匿名管道通信,主要针对于父子进程之间的通信,具体的原理稍后会讲到。

二.匿名管道通信

1、什么是匿名管道

要理解匿名管道通信的原理,我们首先要知道它被设计出来的初衷。本着一切皆文件 的设计哲学,工程师们在有进程间通信需求的时候,第一时间想的是能否利用现有的代码框架去设计通信的功能(就类似于我们之前讲到的C语言中的多态性),自然就想到了我们的文件系统------进程有自己的地址空间,缓冲区,我们可不可以把文件缓冲区的内容刷新给其他进程?

在Linux系统中,我们之前就使用过"管道文件"了。例如:对某个可执行程序的监控

ps ajx | grep main.exe

管道文件符|实际上的原理类似于下图:

2、匿名管道通信的原理

上面说到,匿名管道通信用于父子进程间的通信,接下来我们讲解匿名管道的原理,就能理解为什么它只能用于父子进程间的通信了。现在模拟父进程打开一个文件main.exe

此时创建子进程:现在问题是,父进程打开的文件流还需要给子进程整体拷贝一份吗?不需要,因为左边是进程,右边是文件管理,并且我们还有一个叫做写时拷贝的机制。此时父子进程共享资源------类似于浅拷贝的机制。现在的状态如下:

此时,就算父进程退出,被打开的文件也不会关闭,因为文件有着引用计数 ------有多少个进程指向文件自己,由此实现了文件共享。父子进程就可以用同一个fd文件描述符访问到同一个文件,同一段缓冲区。

而真正的匿名管道,不需要刷新到磁盘上(临时数据),甚至和磁盘都没关系。

操作系统提供了一种内存级 的管道文件,并以读写方式打开,所以会返回两个文件描述符。

创建好子进程之后,父子进行单向通信,因此父进程关闭读通道,子进程关掉写通道。这个管道是被OS单独设计的,但普通文件的接口他也可以使用,并配自己单独的系统调用------pipe。

为什么创建pipe时不需要路径?因为他是内存级的,没有文件名------因此叫做匿名管道。

那么如何保证,两个进程看到的是通过一个管道文件呢?------子进程继承父进程的文件描述符表。

3、测试匿名管道

我们先来看看创建匿名管道的接口pipe:传入一个存储读写端的数组pipefd,其中默认pipefd[0]为读端,pipefd[1]为写端,并以读写方式打开管道文件,根据需求关闭合适的读写端即可。

bash 复制代码
 int pipe(int pipefd[2]);

       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <fcntl.h>              /* Obtain O_* constant definitions */
       #include <unistd.h>


DESCRIPTION
       pipe()  creates a pipe, a unidirectional data channel that can be used for interprocess communication.  The array pipefd is used to return two file de‐
       scriptors referring to the ends of the pipe.  pipefd[0] refers to the read end of the pipe.  pipefd[1] refers to the write end of the pipe.  Data writ‐
       ten to the write end of the pipe is buffered by the kernel until it is read from the read end of the pipe.  For further details, see pipe(7).

接着我们写一段代码来测试管道通信的流程。在这段代码中,子进程负责写,所以关闭读端;父进程负责读,所以关闭写端。

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

void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while (true)
    {
        //格式化
        snprintf(buffer, sizeof(buffer), "I am child, pid: %d, cnt: %d", getpid(), cnt++);
        write(wfd, buffer, strlen(buffer));
        sleep(1);
    }
}

void FatherRead(int rfd)
{
    char buffer[1024];
    while (true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "child say: " << buffer << std::endl;
        }
    }
}

int main()
{
    // 1. 创建管道
    int fds[2] = {0}; // fds[0]:读端   fds[1]: 写端
    int n = pipe(fds);
    if (n < 0)
    {
        std::cerr << "pipe error" << std::endl;
        return 1;
    }
    std::cout << "fds[0]: " << fds[0] << std::endl;
    std::cout << "fds[1]: " << fds[1] << std::endl;

    // 2. 创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // child
        // code
        // 3. 关闭不需要的读写端,形成通信信道
        // f -> r, c -> w
        close(fds[0]);
        ChildWrite(fds[1]);
        close(fds[1]);
        exit(0);
    }
    // 3. 关闭不需要的读写端,形成通信信道
    // f -> r, c -> w
    close(fds[1]);
    FatherRead(fds[0]);
    waitpid(id, nullptr, 0);
    close(fds[0]);

    return 0;
}

关于这段代码有一些细节需要说清楚:

父进程的读操作要放在waitpid之前。有如下原因:

1.避免死锁:如果先调用waitpid,父进程会一直等待子进程结束,再执行读操作,但尤其遇到上面代码中子进程是一个死循环的逻辑,永远不会退出,父进程就会一直阻塞等待子进程退出,子进程写满管道文件后等待父进程读才会继续写。

我们对父子进程进行监控。可以看到子进程成功被创建,并且成功实现了父子进程之间的通信。

注意一个细节:

一个处理字符串的接口,只要是c语言提供的,基本都会自动添加\0 .并且我们也知道,在文件中写的时候不需要带反斜杠,所以用strlen即可,但是读数据使用系统调用的时候需要自己手动添加\0

4.匿名管道的五种特性与四种通信情况

五种特性:

1.匿名管道,只能用来进行具有血缘关系的进程进行进程间通信(尤其是父子进程)。因为从根本来讲,匿名管道就是依赖于这种父子进程间的内核数据结构继承而存在的。

2.如果两个进程同时写入显示器,一个休眠一秒写入,一个不停写入,两者实际上是互不影响的。现在我们的例子是子进程休眠一秒写,父进程不停读,如果父进程没读到就阻塞等待。所以可以看到,管道文件自带同步机制

3.如果此时子进程不停在写,父进程每三秒才读一次 :管道文件大小有上限,子进程会一瞬间把数据写满,此时会阻塞等待父进程读完。

我们对测试代码做以下修改:

cpp 复制代码
//子进程不再休眠,父进程每3秒读一次
void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while (true)
    {
        snprintf(buffer, sizeof(buffer), "I am child, pid: %d, cnt: %d", getpid(), cnt++);
        write(wfd, buffer, strlen(buffer));
        printf("chiled:%d\n",cnt);
    }
}

void FatherRead(int rfd)
{
    char buffer[1024];
    while (true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "child say: " << buffer << std::endl;
        }
        sleep(3);
    }
}

现象如下:子进程先执行自己的写方法,然后每三秒,父进程会读一堆数据出来。

bash 复制代码
...
chiled:1899
chiled:1900
chiled:1901
child say: I am child, pid: 3154275, cnt: 1I am child, pid: 3154275, cnt: 2I am child, pid: 3154275, cnt: 3I am child, pid: 3154275, cnt: 4I am child, pid: 3154275, cnt: 5I am child, pid: 3154275, cnt: 6I am child, pid: 3154275, cnt: 7I am child, pid: 3154275, cnt: 8I am child, pid: 3154275, cnt: 9I am child, pid: 3154275, cnt: 10I am child, pid: 3154275, cnt: 11I am child, pid: 3154275, cnt: 12I am child, pid: 3154275, cnt: 13I am child, pid: 3154275, cnt: 14I am child, pid: 3154275, cnt: 15I am child, pid: 3154275, cnt: 16I am child, pid: 3154275, cnt: 17I am child, pid: 3154275, cnt: 18I am child, pid: 3154275, cnt: 19I am child, pid: 3154275, cnt: 20I am child, pid: 3154275, cnt: 21I am child, pid: 3154275, cnt: 22I am child, pid: 3154275, cnt: 23I am child, pid: 3154275, cnt: 24I am child, pid: 3154275, cnt: 25I am child, pid: 3154275, cnt: 26I am child, pid: 3154275, cnt: 27I am child, pid: 3154275, cnt: 28I am child, pid: 3154275, cnt: 29I am child, pid: 3154275, cnt: 30I am child, pid: 3154275, cnt: 31I am chil
child say: d, pid: 3154275, cnt: 32I am child, pid: 3154275, cnt: 33I am child, pid: 3154275, cnt: 34I am child, pid: 3154275, cnt: 35I am child, pid: 3154275, cnt: 36I am child, pid: 3154275, cnt: 37I am child, pid: 3154275, cnt: 38I am child, pid: 3154275, cnt: 39I am child, pid: 3154275, cnt: 40I am child, pid: 3154275, cnt: 41I am child, pid: 3154275, cnt: 42I am child, pid: 3154275, cnt: 43I am child, pid: 3154275, cnt: 44I am child, pid: 3154275, cnt: 45I am child, pid: 3154275, cnt: 46I am child, pid: 3154275, cnt: 47I am child, pid: 3154275, cnt: 48I am child, pid: 3154275, cnt: 49I am child, pid: 3154275, cnt: 50I am child, pid: 3154275, cnt: 51I am child, pid: 3154275, cnt: 52I am child, pid: 3154275, cnt: 53I am child, pid: 3154275, cnt: 54I am child, pid: 3154275, cnt: 55I am child, pid: 3154275, cnt: 56I am child, pid: 3154275, cnt: 57I am child, pid: 3154275, cnt: 58I am child, pid: 3154275, cnt: 59I am child, pid: 3154275, cnt: 60I am child, pid: 3154275, cnt: 61I am child, pid: 3154275, cnt: 62I am chil
child say: d, pid: 3154275, cnt: 63I am child, pid: 3154275, cnt: 64I am child, pid: 3154275, cnt: 65I am child, pid: 3154275, cnt: 66I am child, pid: 3154275, cnt: 67I am child, pid: 3154275, cnt: 68I am child, pid: 3154275, cnt: 69I am child, pid: 3154275, cnt: 70I am child, pid: 3154275, cnt: 71I am child, pid: 3154275, cnt: 72I am child, pid: 3154275, cnt: 73I am child, pid: 3154275, cnt: 74I am child, pid: 3154275, cnt: 75I am child, pid: 3154275, cnt: 76I am child, pid: 3154275, cnt: 77I am child, pid: 3154275, cnt: 78I am child, pid: 3154275, cnt: 79I am child, pid: 3154275, cnt: 80I am child, pid: 3154275, cnt: 81I am child, pid: 3154275, cnt: 82I am child, pid: 3154275, cnt: 83I am child, pid: 3154275, cnt: 84I am child, pid: 3154275, cnt: 85I am child, pid: 3154275, cnt: 86I am child, pid: 3154275, cnt: 87I am child, pid: 3154275, cnt: 88I am child, pid: 3154275, cnt: 89I am child, pid: 3154275, cnt: 90I am child, pid: 3154275, cnt: 91I am child, pid: 3154275, cnt: 92I am child, pid: 3154275, cnt: 93I am chil

管道是面向字节流的。怎么读和怎么写,没有必然关系。

4.任何一个时刻,只有一个发,一个收------半双工通信。若任何一个时刻可以同时收发------全双工通信。管道就属于半双工通信。

5.匿名管道 文件的生命周期,是随进程的。上面也提到过,管道文件以读写方式打开,并且被父子进程所共享。当父子进程全部退出时,管道文件的引用计数归零,就算不手动关闭也会被操作系统自动回收。

四种通信情况:

1.写慢读快------读端在写端sleep时阻塞,等待写端。

2.写快读慢------写满管道文件,写端就阻塞等待读端。

3.写关读继续------read就会读到返回值为0.表示读到了结尾

此时将代码做如下修改:

cpp 复制代码
void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while (true)
    {
        snprintf(buffer, sizeof(buffer), "I am child, pid: %d, cnt: %d", getpid(), cnt++);
        write(wfd, buffer, strlen(buffer));
        printf("chiled:%d\n",cnt);
        sleep(2);
        //写端执行一次就关闭
        break;
    }
}

void FatherRead(int rfd)
{
    char buffer[1024];
    while (true)
    {
        sleep(5);
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "child say: " << buffer << std::endl;
        }
        else{
            std::cout<< "n:"<<n<<std::endl;
        }
    }
}

结果如下:我们可以看到这里read的返回值n一直输出0,这代表读到了文件的结尾。

bash 复制代码
wujiahao@VM-12-14-ubuntu:~/PipeTest$ ./testPipe
fds[0]: 3
fds[1]: 4
chiled:1
child say: I am child, pid: 3156663, cnt: 0
n:0
n:0
n:0

4.读关闭写继续------写端再写入已经没有意义了,因为此时已经无法实现通信了,还会浪费内存和cpu资源。

操作系统不做没意义的行为,会直接杀掉进程------发送异常信号kill -13信号,杀掉写进程。

对代码做以下修改:

cpp 复制代码
void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while (true)
    {
        snprintf(buffer, sizeof(buffer), "I am child, pid: %d, cnt: %d", getpid(), cnt++);
        write(wfd, buffer, strlen(buffer));
        printf("chiled:%d\n",cnt);
        //sleep(2);
        //写端执行一次就关闭
        //break;
    }
}

void FatherRead(int rfd)
{
    char buffer[1024];
    while (true)
    {
        //sleep(5);
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "child say: " << buffer << std::endl;
        }
        else if(n==0){
            std::cout<< "n:"<<n<<std::endl;
            std::cout<<"child 退出,我也退出"<<;
            break;
        }
        else{
            break;
        }
        break;
    }
}

int main()
{
    // 1. 创建管道
    int fds[2] = {0}; // fds[0]:读端   fds[1]: 写端
    int n = pipe(fds);
    if (n < 0)
    {
        std::cerr << "pipe error" << std::endl;
        return 1;
    }
    std::cout << "fds[0]: " << fds[0] << std::endl;
    std::cout << "fds[1]: " << fds[1] << std::endl;

    // 2. 创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // child
        // code
        // 3. 关闭不需要的读写端,形成通信信道
        // f -> r, c -> w
        close(fds[0]);
        ChildWrite(fds[1]);
        close(fds[1]);
        exit(0);
    }
    // 3. 关闭不需要的读写端,形成通信信道
    // f -> r, c -> w
    close(fds[1]);
    FatherRead(fds[0]);
    close(fds[0]);
    sleep(5);

    int status=0;
    int ret=waitpid(id, nullptr, 0);
    if(ret>0){
        printf("exit code:%d,exit signal:%d\n",(status>>8)&0xFF,status&0x7F);
        sleep(5);
    }
    

    return 0;
}

现象如下:当读端关闭,子进程(写端)会被杀掉,然后父进程waitpid得到子进程的推出信息。

bash 复制代码
wujiahao@VM-12-14-ubuntu:~/PipeTest$ ./testPipe
fds[0]: 3
fds[1]: 4
chiled:1
chiled:2
child say: I am child, pid: 3165035, cnt: 0
chiled:3
exit code:0,exit signal:0

我们可以监控父子进程。在关闭读端后子进程变成僵尸进程,随后自己成被杀掉。

bash 复制代码
3138898 3165678 3165678 3138898 pts/0    3165678 S+    1002   0:00 ./testPipe
3165678 3165679 3165678 3138898 pts/0    3165678 Z+    1002   0:00 [testPipe] <defunct>
3151209 3165732 3165731 3151209 pts/1    3165731 S+    1002   0:00 grep --color=auto testPipe
==========
3138898 3165678 3165678 3138898 pts/0    3165678 S+    1002   0:00 ./testPipe
3151209 3165736 3165735 3151209 pts/1    3165735 S+    1002   0:00 grep --color=auto testPipe

5.验证管道文件容量以及管道写入原子性

管道文件容量

子进程按一字节写入,父进程一直不读(为了让子进程写满管道文件后阻塞)。

cpp 复制代码
void ChildWrite(int wfd)
{
    //char buffer[1024];
    char c =0;
    int cnt = 0;
    while (true)
    {
        //snprintf(buffer, sizeof(buffer), "I am child, pid: %d, cnt: %d", getpid(), cnt++);
        write(wfd, &c, 1);
        printf("chiled:%d\n",cnt++);
        //sleep(2);
        //写端执行一次就关闭
        //break;
    }
}

执行结果:可以看到,一共写入了65536个字符,也就是64kb(不同系统下可能有所不同,这里时Ubuntu 22.04版本)。

bash 复制代码
child:65534
child:65535

验证管道写入原子性

我们可以用指令查看pipe:

man 7 pipe

PIPE_BUF:如果写入的字节数小于PIPE_BUF,就应该是原子写入。

三.基于匿名管道的进程池

基于我们现有的知识,我们可以写一个基于匿名管道的进程池:一个父进程写端,多个子进程读端,每个子进程根据管道文件中读到的内容执行不同的任务。

根据不同任务码让子进程执行不同任务,而任务码通过管道进行发送。如果父进程一直不发送,子进程就会阻塞等待,于是就可以通过这种方式实现子进程的暂停和唤醒。

池化技术可以通过减少某种资源的创建成本,提高任务的效率。

既然存在多个管道文件,我们就要本着先描述再组织的原则,对多个管道文件和子进程进行管理。

1.ProcessPool.hpp

为了更加模块化,我们分为多个文件编写。这个文件的主要作用是描述单个管道文件的类Channel以及管理所有管道文件的类ChannelManager,还有进程池的实现ProcessPool。

1、Channel类

作用:封装父子进程间的通信管道和子进程信息

关键成员

  • int _wfd:管道写端文件描述符(父进程使用)

  • pid_t _subid:子进程PID

  • std::string _name:通道名称,格式"channel-wfd-subid"

cpp 复制代码
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__

#include <iostream>
#include <cstdlib> // stdlib.h stdio.h -> cstdlib cstdio
#include <vector>
#include <unistd.h>
#include <sys/wait.h>
#include "Task.hpp"

// 先描述
class Channel
{
public:
    Channel(int fd, pid_t id) : _wfd(fd), _subid(id)
    {
        _name = "channel-" + std::to_string(_wfd) + "-" + std::to_string(_subid);
    }
    ~Channel()
    {
    }
    //向子进程发送任务码
    void Send(int code)
    {
        int n = write(_wfd, &code, sizeof(code));
        (void)n; // ?
    }
    //关闭管道写端
    void Close()
    {
        close(_wfd);
    }
    //等待子进程退出
    void Wait()
    {
        pid_t rid = waitpid(_subid, nullptr, 0);
        (void)rid;
    }
    //返回文件描述符和子进程ID等信息
    int Fd() { return _wfd; }
    pid_t SubId() { return _subid; }
    std::string Name() { return _name; }

private:
    int _wfd;
    pid_t _subid;
    std::string _name;
    // int _loadnum;
};

2、ChannelManager类

作用:管理所有Channel,实现负载均衡

关键成员

  • std::vector<Channel> _channels:存储所有Channel对象

  • int _next:轮询索引,用于负载均衡,并初始化为0

cpp 复制代码
class ChannelManager
{
public:
    ChannelManager() : _next(0)
    {
    }
    //创建并插入新的管道channel
    void Insert(int wfd, pid_t subid)
    {
        _channels.emplace_back(wfd, subid);
        // Channel c(wfd, subid);
        // _channels.push_back(std::move(c));
    }

    //负载均衡------轮询的选择一个管道执行任务
    Channel &Select()
    {
        auto &c = _channels[_next];
        _next++;
        //防止越界
        _next %= _channels.size();
        return c;
    }
    //打印出当前进程池中所有管道文件
    void PrintChannel()
    {
        for (auto &channel : _channels)
        {
            std::cout << channel.Name() << std::endl;
        }
    }

    //关闭当前进程池中管道文件的写端
    void StopSubProcess()
    {
        for (auto &channel : _channels)
        {
            channel.Close();
            std::cout << "关闭: " << channel.Name() << std::endl;
        }
    }

    //等待所有子进程的退出
    void WaitSubProcess()
    {
        for (auto &channel : _channels)
        {
            channel.Wait();
            std::cout << "回收: " << channel.Name() << std::endl;
        }
    }
    ~ChannelManager() {}

private:
    std::vector<Channel> _channels;
    int _next;
};

3、ProcessPool类

作用:管理整个进程池的生命周期和任务分发

关键成员

  • ChannelManager _cm:通道管理器

  • int _process_num:子进程数量

  • TaskManager _tm:任务管理器

cpp 复制代码
const int gdefaultnum = 5;

class ProcessPool
{
public:
    ProcessPool(int num) : _process_num(num)
    {      
        //用任务管理类注册一些方法用于分发给子进程
        _tm.Register(PrintLog);
        _tm.Register(Download);
        _tm.Register(Upload);
    }
    void Work(int rfd)
    {
        while (true)
        {
            int code = 0;
            ssize_t n = read(rfd, &code, sizeof(code));
            if (n > 0)
            {
                if (n != sizeof(code))
                {
                    continue;
                }
                std::cout << "子进程[" << getpid() << "]收到一个任务码: " << code << std::endl;
                //成功读到任务码,执行任务
                _tm.Execute(code);
            }
            //n==0时,代表读到管道文件结尾,
            else if (n == 0)
            {
                std::cout << "子进程退出" << std::endl;
                break;
            }
            else
            {
                std::cout << "读取错误" << std::endl;
                break;
            }
        }
    }
    bool Start()
    {
        for (int i = 0; i < _process_num; i++)
        {
            // 1. 创建管道
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if (n < 0)
                return false;

            // 2. 创建子进程
            pid_t subid = fork();
            if (subid < 0)
                return false;
            else if (subid == 0)
            {
                // 子进程
                // 3. 关闭不需要的文件描述符。子进程读,所以关闭写端
                close(pipefd[1]);
                //子进程执行读取到的任务码
                Work(pipefd[0]); //??
                //执行完后关闭流,退出
                close(pipefd[0]);
                exit(0);
            }
            else
            {
                // 父进程
                //  3. 关闭不需要的文件描述符,父进程写端,所以关闭读端
                close(pipefd[0]); // 写端:pipefd[1];
                _cm.Insert(pipefd[1], subid);
                // wfd, subid
            }
        }
        return true;
    }
    void Debug()
    {
        //打印所有子进程
        _cm.PrintChannel();
    }
    void Run()
    {
        // 1. 选择一个任务
        int taskcode = _tm.Code();

        // 2. 选择一个信道[子进程],负载均衡的选择一个子进程,完成任务
        auto &c = _cm.Select();
        std::cout << "选择了一个子进程: " << c.Name() << std::endl;
        // 2. 发送任务
        c.Send(taskcode);
        std::cout << "发送了一个任务码: " << taskcode << std::endl;
    }
    void Stop()
    {
        // 关闭父进程所有的wfd即可
        _cm.StopSubProcess();
        // 回收所有子进程
        _cm.WaitSubProcess();
    }
    ~ProcessPool()
    {
    }

private:
    ChannelManager _cm;
    int _process_num;
    TaskManager _tm;
};

#endif

2.Task.hpp

作用:管理所有可执行的任务函数

关键成员

  • std::vector<task_t> _tasks:存储任务函数指针的容器
cpp 复制代码
#pragma once

#include <iostream>
#include <vector>
#include <ctime>

typedef void (*task_t)();

////////////////debug/////////////////////
void PrintLog()
{
    std::cout << "我是一个打印日志的任务" << std::endl;
}

void Download()
{
    std::cout << "我是一个下载的任务" << std::endl;
}

void Upload()
{
    std::cout << "我是一个上传的任务" << std::endl;
}
//////////////////////////////////////

class TaskManager
{
public:
    TaskManager()
    {
        srand(time(nullptr));
    }    
    //注册任务函数到tasks表中
    void Register(task_t t)
    {
        _tasks.push_back(t);
    }
    //生成随机任务码
    int Code()
    {
        return rand() % _tasks.size();
    }
    //执行任务
    void Execute(int code)
    {
        if(code >= 0 && code < _tasks.size())
        {
            _tasks[code]();
        }
    }
    ~TaskManager()
    {}
private:
    std::vector<task_t> _tasks;
};

3.Main.cc

cpp 复制代码
int main()
{
    // 1. 创建进程池对象(5个子进程)
    ProcessPool pp(gdefaultnum);  // gdefaultnum = 5
    
    // 2. 启动进程池
    pp.Start(); 
    // 创建5个管道,fork 5个子进程
    // 父进程保留所有管道写端,子进程进入Work循环等待任务
    
    // 3. 分发10个任务
    int cnt = 10;
    while(cnt--) {
        pp.Run();  // 每次随机选择任务和子进程
        sleep(1);
    }
    
    // 4. 停止进程池
    pp.Stop();
    // 关闭所有管道写端 -> 子进程read返回0 -> 子进程退出
    // 等待所有子进程退出
    return 0;
}

整个Main函数的执行流程如下:

执行效果:通过查看执行结果中的进程名,我们就知道我们执行了轮询方式实现负载均衡。

相关推荐
zz-zjx2 小时前
Nginx 生产级知识架构树(按流量路径 + 运维维度组织)含生产常见错误
运维·nginx·架构
diqiudq3 小时前
用AMD显卡节省nVidia显卡显存占用
linux·深度学习·ubuntu·显存释放
励志不掉头发的内向程序员4 小时前
【Linux系列】并发世界的基石:透彻理解 Linux 进程 — 进程状态
linux·运维·服务器·开发语言·学习
种时光的人4 小时前
无状态HTTP的“记忆”方案:Spring Boot中Cookie&Session全栈实战
服务器·spring boot·后端·http
小龙报5 小时前
《KelpBar海带Linux智慧屏项目》
linux·c语言·vscode·单片机·物联网·ubuntu·学习方法
mljy.5 小时前
Linux《线程同步和互斥(下)》
linux
养生技术人5 小时前
Oracle OCP认证考试题目详解082系列第50题
运维·数据库·sql·oracle·database·开闭原则
朱包林5 小时前
Prometheus监控K8S集群-ExternalName-endpoints-ElasticStack采集K8S集群日志实战
运维·云原生·容器·kubernetes·prometheus
谢语花6 小时前
【VS2022】LNK assimp64.lib找不到文件_openframework
android·运维·服务器