进程间通信0.0-pipe()匿名管道,详细分析进程池调度队列执行逻辑,进程池模拟实现。

@bit::Shadow
✧(≖ ◡ ≖✿

目录

通信的目的

通信的本质

管道

匿名管道

pipe()创建

图解

内核角度:

浅度:

深度:

父子进程关于pipe端口设计

父子通信实例

管道与系统的关系:

管道文件的五种特性

半双工与全双工

*系统退出管道会怎么样?

进程通信4种(异常)通信情况:

匿名管道模拟进程池

两种通信模式

进程池图解

核心数据结构(类)

任务分发轮询调度方法:

完整退出示例:

完整流程:


通信的目的

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

通信的本质

先让不同进程看到(维护)同一份资源(内存),然后才有通信的条件。

这份资源一般都为OS提供,OS的系统调用。

管道

管道作为一种进程间通信的较古老方式,依旧在后端中占有较重要地位,实现进程间通信的目的。

管道文件:只会被打开无需刷新的磁盘就可以被读取与写入,针对的是进程间的实时通信。

匿名管道

父子/pid相邻的进程间通信的中间件。

pipe()创建

cpp 复制代码
int pipe(int fds[2]);

参数:

  • 传递整型数组作为创建的管道的两端(本质是"文件"的两侧),读侧与写侧。
  • 传递进的参数fds0被设置为读侧,fds1被设置为写侧。

返回值:

0:正常创建

-1:创建失败,errno被设置。

原理:通过创建当前进程的PCB指向的file_struct内未占用相邻文件流的fd使其分别指向"管道"的读端和写端。

图解

验证fds0 fds1

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
void test1() { 
    int fds[2] = {0};
    int n = pipe(fds);
    if(n == -1)
    {
        perror("pipe failed");
        exit(1);
    }
    std::cout << fds[0] << " " << fds[1] << std::endl;
}

内核角度:

浅度:
深度:

真正的管道是内存级的,与磁盘无关联。并不会出现先前文件内容"数据刷新到磁盘"的现象。

所以真正的图片是:

父子进程关于pipe端口设计

在进程匿名管道的通信中,管道链接父子进程。

要求父端写入子端读取图解

父子通信实例

父端发送信息+模拟间隔。子端先阻塞等待,后接受后退出。

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

int main()
{
    int fds[2];

    // 创建匿名管道
    if (pipe(fds) == -1)
    {
        perror("pipe failed");
        return 1;
    }

    //父子
    pid_t pid = fork();
    if (pid == -1)
    {
        perror("fork failed");
        return 1;
    }

    // ==================== 子进程:只读 ====================
    if (pid == 0)
    {
        close(fds[1]); // 关闭写端

        char buffer[1024];
        ssize_t n;

        //read阻塞等待
        while ((n = read(fds[0], buffer, sizeof(buffer) - 1)) > 0)
        {
            buffer[n] = '\0';
            std::cout << "[子进程] 收到: " << buffer << std::endl;
        }

        if (n == 0)
            std::cout << "[子进程] 写端已关闭,退出\n";
        else if (n == -1)
            perror("read error");

        close(fds[0]);
        //子进程退出
        exit(0);
    }

    // ==================== 父进程:只写 ====================
    close(fds[0]); // 关闭读端

    const char* messages[] = {
        "Hello from parent",
        "第二条消息",
        "第三条消息",
        nullptr
    };

    for (int i = 0; messages[i] != nullptr; ++i)
    {
        ssize_t written = write(fds[1], messages[i], strlen(messages[i]));
        if (written == -1)
        {
            perror("write error");
            break;
        }
        std::cout << "[父进程] 发送: " << messages[i] << std::endl;
        sleep(1); // 模拟间隔发送
    }

    close(fds[1]); // 关闭写端 → 子进程 read() 返回 0(EOF)

    // 等待子进程退出,回收资源
    int status;
    waitpid(pid, &status, 0);

    if (WIFEXITED(status))
        std::cout << "[父进程] 子进程退出码: " << WEXITSTATUS(status) << std::endl;

    return 0;
}

管道与系统的关系:

1.由PCB->fieles_struct指向的文件流内缓冲区(Page Cache)是系统级缓冲,由内核统一管理,对用户透明,存在"落盘风险"(丢失数据)。

2.管道是纯内存匿名缓冲,根本没有对应的磁盘inode,所以"刷盘"对它而言毫无意义。

管道文件的五种特性

  1. 匿名管道常用来进行具有血缘关系的(fd临近)的进程间通信(常用于父子通信)。
  2. 管道文件自带同步机制。像上面的子进程每隔1秒写一次那么父进程的无延迟循环必须等待子进程的成功写入,否则阻塞(read)。
  3. 管道传输是面向字节流的。
  4. 匿名管道是单向通信的? 不,这取决于你的实现方式。

写满时发生:

模式: 操作 返回情况

阻塞(默认情况) write()强制挂起 等读端情况读取后继续执行

非阻塞 立即返回失败 处理返回失败的错误码情况

半双工与全双工

半双工:任意一个时刻,一个发出一个接受 或 另一个发出另一个接受。(半双工具有两种能力但两者不能同时存在)

全双工:任何时刻可以同时接收发送信息。(吵架)

匿名管道是半双工的一种特殊情况。

*系统退出管道会怎么样?

答:系统退出,OS检测发出异常,执行资源回收逻辑。

进程通信4种(异常)通信情况:

  • 写慢读快,读端阻塞等待。
  • 写快读慢,读端满则立即读取输出,读端不满缓冲等待。
  • 写端关读端继续,读完(read() == 0)退出。
  • 读端关写端继续,写端直接关闭。

管道的容量为64KB。

匿名管道模拟进程池

两种通信模式

普通模式:任务来了 → fork() → 处理 → 子进程退出

↑ 每次都有 fork/exit 开销

进程池模式:程序启动时预先 fork N 个子进程

任务来了 → 从池中取一个空闲进程 → 处理 → 归还池中

↑ 无 fork 开销,子进程常驻

进程池图解

图解1

图解2:

核心数据结构(类)

cpp 复制代码
// 单个管道+子进程的封装
struct Channel
{
    int        _wfd;        // 父进程持有的写端
    pid_t      _cpid;       // 子进程 pid
    std::string _name;      // 调试用名称

    Channel(int fd, pid_t pid, const std::string& name)
        : _wfd(fd), _cpid(pid), _name(name) {}
};

// 进程池管理器
class ProcessPool
{
private:
    std::vector<Channel> _channels;
    int _num;
    int _next;              // 轮询下标

public:
    ProcessPool(int num)
        : _num(num), _next(0)
    {
        _channels.reserve(num);
        for (int i = 0; i < num; ++i)
            createWorker(i);
    }
};

任务分发轮询调度方法:

cpp 复制代码
void dispatch(int taskId)
{
    // 轮询选择子进程(Round-Robin)
    Channel& ch = _channels[_next % _num];
    _next++;

    std::cout << "[主进程] 分发任务" << taskId
              << " → " << ch._name
              << " (pid=" << ch._cpid << ")\n";

    write(ch._wfd, &taskId, sizeof(taskId));
}

完整退出示例:

cpp 复制代码
void ShutDown()
{
      //1.关闭所有写端,子进程read()==0子进程自然退出
      for(aotu& e:_channels)
      {
           close(e._wfd);
      }
      //2子进程退出后的僵尸进程处理
      for(auto& e:_channels)
      {
           //waitpid(_channels._cpid,&status,0);
           waitpid(_channels._cpid,nullptr,0);
      }
}

完整流程:

close(写端)

子进程 read() 返回 0(EOF)

子进程退出循环,exit(0)

父进程 waitpid() 回收,无僵尸

宏PIPE_BUF是管道原子写入的限制大小,它第一轮内核保证一次write()操作不会与其他的进程的写入交错的最大字节数4096.

感谢支持,持续更新

欢迎关注

相关推荐
lcj25111 小时前
【list】【手撕 STL】List 容器全解析!迭代器 / 增删改查 / 去重排序,面试必背的核心考点!
c++·面试·list
指尖的爷1 小时前
C++头文件的作用
开发语言·c++
keykey6.1 小时前
反向传播与梯度下降:神经网络如何学习
开发语言·人工智能·深度学习·机器学习
CQU_JIAKE1 小时前
6.6aaaaaa
linux·运维·服务器
Apibro1 小时前
【Linux】Qt Creator 中文输入法
linux·qt
Jun6261 小时前
QT(5)-第三方日志系统
开发语言·数据库·qt
冰暮流星1 小时前
javascript建立对象之构造函数
开发语言·javascript·ecmascript
smallswan1 小时前
第十四 算数运算
linux·服务器·前端
VX_181 小时前
Docker镜像直接部署JumpServer
运维·docker·容器