
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[一. 进程间通信基础认知](#一. 进程间通信基础认知)
[1.1 进程间通信的核心目的](#1.1 进程间通信的核心目的)
[1.2 进程间通信的发展与分类](#1.2 进程间通信的发展与分类)
[1.2.1 阶段一:管道](#1.2.1 阶段一:管道)
[1.2.2 阶段二:System V IPC](#1.2.2 阶段二:System V IPC)
[1.2.3 阶段三:POSIX IPC](#1.2.3 阶段三:POSIX IPC)
[1.2.4 IPC分类体系](#1.2.4 IPC分类体系)
[二. 管道的基础概念](#二. 管道的基础概念)
[2.1 管道的定义](#2.1 管道的定义)
[2.2 管道的核心特性](#2.2 管道的核心特性)
[三. 匿名管道](#三. 匿名管道)
[3.1 匿名管道的创建函数](#3.1 匿名管道的创建函数)
[3.2 匿名管道的简单使用示例:从键盘到屏幕](#3.2 匿名管道的简单使用示例:从键盘到屏幕)
[四. 基于 fork 的匿名管道跨进程通信](#四. 基于 fork 的匿名管道跨进程通信)
[4.1 fork 共享管道的核心原理](#4.1 fork 共享管道的核心原理)
[4.2 从文件描述符视角理解管道通信](#4.2 从文件描述符视角理解管道通信)
[4.3 子写父读的完整实战示例以及四个场景分析(重点)](#4.3 子写父读的完整实战示例以及四个场景分析(重点))
[五. 从内核视角看管道的本质](#五. 从内核视角看管道的本质)
[5.1 管道的内核数据结构](#5.1 管道的内核数据结构)
[5.2 管道的内核实现逻辑](#5.2 管道的内核实现逻辑)
[5.3 管道读写规则](#5.3 管道读写规则)
[5.3.1 阻塞模式(默认)](#5.3.1 阻塞模式(默认))
[5.3.2 非阻塞模式(O_NONBLOCK)](#5.3.2 非阻塞模式(O_NONBLOCK))
[5.3.3 原子性保证](#5.3.3 原子性保证)
[5.4 管道与普通文件的异同](#5.4 管道与普通文件的异同)
[6.1 核心设计思路](#6.1 核心设计思路)
[6.2 通道封装(Channel.hpp)](#6.2 通道封装(Channel.hpp))
[6.3 任务管理(Task.hpp)](#6.3 任务管理(Task.hpp))
[6.4 进程池实现(ProcessPool.hpp)](#6.4 进程池实现(ProcessPool.hpp))
[6.5 主程序(Main.cc)](#6.5 主程序(Main.cc))
[6.6 编译运行](#6.6 编译运行)
前言
在 Linux 系统中,进程拥有独立的地址空间,彼此无法直接交换数据,进程间通信(IPC)正是打破这种隔离、实现数据交互的关键技术。管道作为最古老、最基础的 IPC 手段,贴合"一切皆文件"的设计思想,尤其适合亲缘进程间的通信。本文将从文件描述符和内核视角深入解析匿名管道的底层原理,并在此基础上进一步探讨基于管道通信的进程池模型,拆解其如何通过预先创建子进程避免频繁创建销毁的开销,以及任务分发与协同机制。
一. 进程间通信基础认知
在学习管道之前,我们需要先明确进程间通信的核心目的和分类,建立对 IPC 技术的整体认知,这能帮助我们更好地理解管道的设计初衷和应用场景。
1.1 进程间通信的核心目的
在现代操作系统中,进程 是程序执行的基本单位 ,每个进程拥有独立的地址空间。这种设计带来了隔离性和安全性 ,但也带来了一个问题:进程之间如何交换信息和协同工作?
进程间通信的本质是实现进程间 的数据交互、资源共享和事件协同,具体可分为四个方面:
- 数据传输:一个进程将自身数据发送给另一个进程,是最基础的 IPC 需求;
- 资源共享:多个进程共享同一份系统资源(如文件、内存),提高资源利用率;
- 通知事件:进程向其他进程发送事件通知,如子进程退出时通知父进程、进程完成任务后通知调度进程;
- 进程控制:一个进程对另一个进程进行执行控制,如调试进程拦截目标进程的异常和陷入,实时获取其状态。

1.2 进程间通信的发展与分类
Linux 的 IPC 技术从 Unix 继承并不断发展,整体可分为三大类,管道是其中最基础的一类:
1.2.1 阶段一:管道
- 管道 :包括 匿名管道(pipe) 和 命名管道(FIFO),是最基础的 IPC 方式,基于文件系统实现,简单但功能强大,适用于亲缘关系进程;
1.2.2 阶段二:System V IPC
- System V IPC :包括共享内存 、消息队列 、信号量,由 System V 系统引入,基于内核的 IPC 资源管理实现,生命周期随内核;
1.2.3 阶段三:POSIX IPC
- POSIX IPC :遵循 POSIX 标准的 IPC 方式,是对 System V IPC 的改进,包括 POSIX 共享内存、消息队列、信号量等,具有跨平台兼容性,并且扩展了互斥量、条件变量、读写锁等
1.2.4 IPC分类体系
bash
Linux IPC
├── 管道
│ ├── 匿名管道 (pipe)
│ └── 命名管道 (FIFO)
├── System V IPC
│ ├── 消息队列
│ ├── 共享内存
│ └── 信号量
└── POSIX IPC
├── 消息队列
├── 共享内存
├── 信号量
├── 互斥量
├── 条件变量
└── 读写锁
管道作为最原始的 IPC 方式,虽然功能简单,但却是理解 Linux 进程间通信和文件系统的关键,也是实现其他复杂 IPC 的基础。
二. 管道的基础概念
2.1 管道的定义
管道是一种半双工 的数据流通信方式,本质是内核 中的一块缓冲区 ,它将一个进程的标准输出与另一个进程的标准输入相连,形成一条单向的数据流通道 。我们可以把管道理解为进程间的 "一根水管",数据从一端写入 ,从另一端读出,实现单向的通信。
在 Linux 命令行中,我们经常使用的管道符|就是管道的典型应用,例如who | wc -l:
- who进程的标准输出被重定向到管道的写端;
- wc -l进程的标准输入被重定向到管道的读端;
- 内核中的管道缓冲区作为中间介质,完成两个进程间的数据传递。

- 从上图中我们可以看出最后他们三个指令的父进程都是bash的,他们之间是具有血缘关系的进程
2.2 管道的核心特性
管道的设计贴合 Linux 一切皆文件的思想,其核心特性可总结为:
- 半双工通信 :数据只能沿一个方向流动,若需双向通信,需创建两个管道;
- 基于缓冲区 :管道的实质是内核缓冲区,数据写入后暂存于内核,直到被另一个进程读取;
- 文件式操作 :管道通过文件描述符操作,读写接口与文件一致(read/write),符合 Linux 文件操作规范;
- 亲缘进程专属:匿名管道仅支持具有共同祖先的亲缘进程(父进程与子进程、兄弟进程)间通信。
匿名管道的两个初步理解图:



三. 匿名管道
3.1 匿名管道的创建函数
cpp
#include <unistd.h>
int pipe(int pipefd[2]);
函数参数
- pipefd: 整型数组,是输出型参数,用于保存管道的读、写文件描述符:
- pipefd0: 管道的读端,仅用于读取管道中的数据;
- pipefd1: 管道的写端,仅用于向管道中写入数据。
返回值
- 成功:返回 0;
- 失败:返回 - 1,并设置 errno 表示错误原因。
注意 :调用pipe函数的进程会同时持有管道的读端和写端 ,若要实现两个进程间的单向通信,需要在进程创建后关闭各自无用的文件描述符,避免数据读写异常
3.2 匿名管道的简单使用示例:从键盘到屏幕
下面的示例实现了一个基础的匿名管道通信:从键盘读取数据写入管道,再从管道读取数据输出到屏幕,直观展示管道的读写操作。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
int fds[2];
char buf[100];
int len;
// 创建管道
if (pipe(fds) == -1)
{
perror("make pipe");
exit(1);
}
// 从标准输入读取数据
while (fgets(buf, 100, stdin))
{
len = strlen(buf);
// 写入管道
if (write(fds[1], buf, len) != len)
{
perror("write to pipe");
break;
}
memset(buf, 0x00, sizeof(buf));
// 从管道读取
if ((len = read(fds[0], buf, 100)) == -1)
{
perror("read from pipe");
break;
}
// 写入标准输出
if (write(1, buf, len) != len)
{
perror("write to stdout");
break;
}
}
return 0;
}
该示例中,进程自身同时完成管道的写和读操作,虽然未实现跨进程通信,但清晰展示了管道的基本读写流程:通过fd1写 ,通过fd0读,操作接口与普通文件完全一致。
四. 基于 fork 的匿名管道跨进程通信
匿名管道本身由单个进程创建 ,要实现跨进程通信 ,需要借助 fork 函数创建子进程 ------ 子进程会继承父进程 的文件描述符表 ,从而与父进程共享同一个管道的读、写端,这是匿名管道实现亲缘进程通信的核心原理。

4.1 fork 共享管道的核心原理
fork 函数创建的子进程会复制父进程的文件描述符表,包括父进程创建的管道读、写端文件描述符,因此父子进程会共享同一个内核管道缓冲区,实现数据互通。其核心步骤分为三步:
- 父进程创建管道:父进程调用 pipe 创建管道,持有 fd0(读)和 fd1(写)两个文件描述符;
- 父进程 fork 创建子进程:子进程继承父进程的文件描述符表,同样持有管道的 fd0 和 fd1;
- 关闭无用的文件描述符:根据通信方向,父、子进程分别关闭无用的读 / 写端,实现单向通信。
例如要实现父进程读、子进程写,则:
- 父进程关闭写端fd1,仅保留读端fd0;
- 子进程关闭读端fd0,仅保留写端fd1。

4.2 从文件描述符视角理解管道通信
从文件描述符的角度,我们可以更清晰地看到父子进程共享管道的过程,以父读子写为例:
步骤 1:父进程创建管道
父进程的文件描述符表中,0、1、2 分别为标准输入、标准输出、标准错误,pipe创建的管道分配到3(读端fd0) 和 4(写端fd1)。
bash
父进程:0(tty) 1(tty) 2(tty) 3(pipe读) 4(pipe写)
子进程:0(tty) 1(tty) 2(tty) 3(pipe读) 4(pipe写)
步骤 2:父进程 fork 创建子进程
子进程复制父进程的文件描述符表,此时父子进程的文件描述符 3、4 均指向同一个内核管道缓冲区。
bash
父进程:0(tty) 1(tty) 2(tty) 3(pipe读) 4(pipe写)
子进程:0(tty) 1(tty) 2(tty) 3(pipe读) 4(pipe写)
步骤 3:关闭无用文件描述符
父进程关闭写端 4,子进程关闭读端 3,此时管道形成单向的 "子写父读" 通道 ,数据只能从子进程写入,父进程读出。
bash
父进程:0(tty) 1(tty) 2(tty) 3(pipe读) -
子进程:0(tty) 1(tty) 2(tty) - 4(pipe写)
核心关键点 :父子进程的文件描述符指向同一个内核管道缓冲区 ,这是进程间能通过管道通信 的根本原因;关闭无用描述符则是为了保证通信的单向性,避免出现数据读写的混乱。
4.3 子写父读的完整实战示例以及四个场景分析(重点)
下面的示例实现了子进程向管道写入字符串,父进程从管道读取并打印的功能,是 "子写父读" 的标准实现:
场景1:写端慢,读端快------以慢的节奏来:当管道没有数据时,读端就要阻塞(等写)
cpp
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>
#define MAX 1024
void Child_write(int wfd)
{
char buffer[MAX];
int cnt = 0;
while (true)
{
//场景1:写端慢,读端快------以慢的节奏来:当管道没有数据时,读端就要阻塞(等写)
snprintf(buffer, sizeof(buffer), "I am child, pid:%d, cnt:%d", getpid(), cnt++);
write(wfd, buffer, strlen(buffer));
sleep(1);
}
}
void Father_read(int rfd)
{
char buffer[MAX];
int cnt = 0;
while (true)
{
//场景1:写端慢,读端快------以慢的节奏来:当管道没有数据时,读端就要阻塞(等写)
size_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};
int n = pipe(fds);
if (n < 0)
{
std::cerr << "pipe error" << std::endl;
}
// 2、创建子进程
pid_t id = fork();
if (id == 0)
{
// child
// 3、关闭不需要的读写端,形成单向通信信道
// father -> r , child -> w
close(fds[0]);
Child_write(fds[1]);
// 子进程结束前端口全部关闭
close(fds[1]);
exit(0);
}
// 3、关闭不需要的读写端,形成单向通信信道
// father
close(fds[1]);
Father_read(fds[0]);
//waitpid(id, nullptr, 0);
// 父进程结束前端口全部关闭
close(fds[0]);
return 0;
}

场景2:写端快,读端慢(读端会把写端写入的数据一次全部读上来:取决于缓冲区大小)------当管道满了的时候,写端就要阻塞等待(等读)
cpp
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>
#define MAX 1024
//子进程:w
void Child_write(int wfd)
{
char buffer[MAX];
int cnt = 0;
while (true)
{
//场景2:写端快,读端慢(读端会把写端写入的数据一次全部读上来:取决于缓冲区大小)------当管道满了的时候,写端就要阻塞等待(等读)
snprintf(buffer, sizeof(buffer), "I am child, pid:%d, cnt:%d", getpid(), cnt++);
write(wfd, buffer, strlen(buffer));
printf("child:%d\n", cnt);//展示写了多少次数据到管道里面
}
}
//父进程:r
void Father_read(int rfd)
{
char buffer[MAX];
int cnt = 0;
while (true)
{
//场景2:写端快,读端慢(读端会把写端写入的数据一次全部读上来:取决于缓冲区大小)------当管道满了的时候,写端就要阻塞等待(等读)
sleep(5);
size_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};
int n = pipe(fds);
if (n < 0)
{
std::cerr << "pipe error" << std::endl;
}
// 2、创建子进程
pid_t id = fork();
if (id == 0)
{
// child
// 3、关闭不需要的读写端,形成单向通信信道
// father -> r , child -> w
close(fds[0]);
Child_write(fds[1]);
// 子进程结束前端口全部关闭
close(fds[1]);
exit(0);
}
// 3、关闭不需要的读写端,形成单向通信信道
// father
close(fds[1]);
Father_read(fds[0]);
//waitpid(id, nullptr, 0);
// 父进程结束前端口全部关闭
close(fds[0]);
return 0;
}

场景3:写端不写(close),读端继续------读完全部数据后,read就会读到返回值为0,表示文件结尾(不会阻塞等待)
cpp
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>
#define MAX 1024
//子进程:w
void Child_write(int wfd)
{
char buffer[MAX];
int cnt = 0;
while (true)
{
// 场景3:写端不写(close),读端继续------读完全部数据后,read就会读到返回值为0,表示文件结尾(不会阻塞等待)
snprintf(buffer, sizeof(buffer), "I am child, pid:%d, cnt:%d", getpid(), cnt++);
write(wfd, buffer, strlen(buffer));
sleep(2);
break; //break后,子进程就会把写端也进行关闭
}
}
//父进程:r
void Father_read(int rfd)
{
char buffer[MAX];
int cnt = 0;
while (true)
{
// 场景3:写端不写(close),读端继续------读完全部数据后,read就会读到返回值为0,表示文件结尾(不会阻塞等待)
size_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;
std::cout << "child 已退出,我也退出" << std::endl;
break;
}
}
}
int main()
{
// 1、创建管道
int fds[2] = {0};
int n = pipe(fds);
if (n < 0)
{
std::cerr << "pipe error" << std::endl;
}
// 2、创建子进程
pid_t id = fork();
if (id == 0)
{
// child
// 3、关闭不需要的读写端,形成单向通信信道
// father -> r , child -> w
close(fds[0]);
Child_write(fds[1]);
// 子进程结束前端口全部关闭
close(fds[1]);
exit(0);
}
// 3、关闭不需要的读写端,形成单向通信信道
// father
close(fds[1]);
Father_read(fds[0]);
//waitpid(id, nullptr, 0);
// 父进程结束前端口全部关闭
close(fds[0]);
return 0;
}

场景4:读端不读(close),写端继续,OS直接杀死子进程,发送异常信号
cpp
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>
#define MAX 1024
//子进程:w
void Child_write(int wfd)
{
char buffer[MAX];
int cnt = 0;
while (true)
{
// 场景4:读端不读(close),写端继续------写端再写入没有任何意义(OS不会做浪费时间和空间的事情),OS直接杀死子进程,发送异常信号
snprintf(buffer, sizeof(buffer), "I am child, pid:%d, cnt:%d", getpid(), cnt++);
write(wfd, buffer, strlen(buffer));
printf("child:%d\n", cnt);
sleep(2);
}
}
//父进程:r
void Father_read(int rfd)
{
char buffer[MAX];
int cnt = 0;
while (true)
{
// 场景4:读端不读(close),写端继续------写端再写入没有任何意义(OS不会做浪费时间和空间的事情),OS直接杀死子进程,发送异常信号
size_t n = read(rfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << "child say: " << buffer << std::endl;
}
break; //break后,父进程就会把读端也进行关闭
}
}
int main()
{
// 1、创建管道
int fds[2] = {0};
int n = pipe(fds);
if (n < 0)
{
std::cerr << "pipe error" << std::endl;
}
// 2、创建子进程
pid_t id = fork();
if (id == 0)
{
// child
// 3、关闭不需要的读写端,形成单向通信信道
// father -> r , child -> w
close(fds[0]);
Child_write(fds[1]);
// 子进程结束前端口全部关闭
close(fds[1]);
exit(0);
}
// 3、关闭不需要的读写端,形成单向通信信道
// father
close(fds[1]);
Father_read(fds[0]);
//waitpid(id, nullptr, 0);
// 父进程结束前端口全部关闭
close(fds[0]);
// 场景4:读端不读(close),写端继续,OS直接杀死子进程,发送异常信号
int status = 0; //位图
int ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("exit code: %d, exit signal: %d\n", (status>>8)&0xFF, status&0x7F);
//异常退出:退出码无意义(高8位比特位为0),低8位表示终止信号
}
return 0;
}

五. 从内核视角看管道的本质
从文件描述符视角,我们理解了管道的使用流程,而从内核视角 ,我们能看透管道的底层实现 ------ 管道的本质是内核中的一块缓冲区,由两个file结构体指向同一个inode ,贴合 Linux **"一切皆文件"**的设计思想。
5.1 管道的内核数据结构
在 Linux 内核中,管道的底层实现涉及三个核心数据结构:
- **file结构体:**进程的文件描述符表中的每个项都指向一个file结构体,记录文件的操作方式、当前偏移量等信息;
- **inode结构体:**用于描述文件的物理属性,管道的inode中保存了管道缓冲区的地址、大小、读写位置等核心信息;
- 管道缓冲区:内核中的一块连续内存,是管道实际存储数据的地方。
对于匿名管道,父子进程的fd0 和fd1 会分别指向不同的file结构体 ,但这两个file结构体 最终会指向同一个inode结构体 ,而该inode指向内核中的管道缓冲区。

5.2 管道的内核实现逻辑
当进程对管道执行read/write操作时,内核的处理逻辑如下:
- **写操作write(fd1, data, len):**内核将数据从进程地址空间复制到管道缓冲区,并更新inode中的写位置;
- **读操作read(fd0, buf, len):**内核将管道缓冲区中的数据复制到进程地址空间,并更新inode中的读位置;
- 缓冲区同步:内核会保证管道缓冲区的读写同步,若缓冲区为空,读操作会阻塞;若缓冲区满,写操作会阻塞。这个上面也有分析到一点。
简单来说,管道的读写操作本质是进程地址空间与内核缓冲区之间的数据拷贝 ,而两个进程共享同一个内核缓冲区 ,就实现了数据的跨进程传递。
5.3 管道读写规则
5.3.1 阻塞模式(默认)
| 情况 | 读操作 | 写操作 |
|---|---|---|
| 没有数据可读 | 阻塞等待 | - |
| 管道已满 | - | 阻塞等待 |
| 所有写端关闭 | 返回 0(EOF) | - |
| 所有读端关闭 | - | 产生 SIGPIPE 信号 |
5.3.2 非阻塞模式(O_NONBLOCK)
| 情况 | 读操作 | 写操作 |
|---|---|---|
| 没有数据可读 | 返回 -1,errno=EAGAIN | - |
| 管道已满 | - | 返回 -1,errno=EAGAIN |
5.3.3 原子性保证
写入数据量 ≤ PIPE_BUF:
- Linux保证写入的原子性
写入数据量 > PIPE_BUF:
- 不保证原子性
- 可能与其他进程的数据交错

5.4 管道与普通文件的异同
管道的操作接口与普通文件一致,但二者在底层实现和使用上有明显区别,核心对比如下:
| 特性 | 管道 | 普通文件 |
|---|---|---|
| 存储介质 | 内核缓冲区 | 磁盘 / 块设备 |
| 生命周期 | 随进程(进程退出释放) | 随文件系统(需手动删除) |
| 数据读写 | 流式读写,不可随机访问 | 支持随机访问(lseek) |
| 共享方式 | 仅亲缘进程通过文件描述符共享 | 所有进程可通过路径 / 文件描述符共享 |
| 数据持久化 | 不持久化(读出即删除) | 持久化(数据保存在磁盘) |
但二者的核心共性是都遵循 Linux 的文件操作模型,通过文件描述符 、file结构体 、inode结构体实现操作,这也是管道能复用文件读写接口的根本原因。
六、实践:进程池实现
6.1 核心设计思路
本进程池实现的核心逻辑:
- 父进程创建指定数量的子进程,通过匿名管道与每个子进程建立单向通信(父写子读);
- 父进程采用轮询策略 将任务分发给不同子进程,实现简单的负载均衡;
- 子进程循环读取管道中的任务码,执行对应任务;
- 父进程通过关闭管道写端通知子进程退出,并回收所有子进程资源。

6.2 通道封装(Channel.hpp)
定义了Channel 和ChannelManager 两个类,用于管理基于管道的进程池:
Channel 封装单个子进程的写端 fd 和 pid,提供发送任务、关闭管道和等待回收的接口;ChannelManager 通过容器统一管理多个 Channel,并使用轮询算法实现任务的负载均衡分发。
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <sys/wait.h>
// 先描述
// 通道类:管理单个子进程的通信管道和进程ID
class Channel
{
public:
// 构造函数:初始化管道写端、子进程ID,生成通道名称
Channel(int wfd, pid_t subid)
: _wfd(wfd), _subid(subid)
{
_name = "Channel - " + std::to_string(_wfd) + " - " + std::to_string(_subid);
}
// 获取管道写端
int Wfd() { return _wfd; }
// 获取子进程ID
pid_t Subid() { return _subid; }
// 获取通道名称(调试用)
std::string Name() { return _name; }
// 发送任务给对应管道,使后续子进程接收
void Send(int taskcode)
{
// 把随机分配的任务码写入管道文件,使其激活对应的子进程进行读取read操作
ssize_t n = write(_wfd, &taskcode, sizeof(taskcode));
(void)n; // 屏蔽未使用变量警告(实际场景应检查写操作是否成功)
}
// 关闭管道写端
void Close()
{
if (_wfd > 0)
{
close(_wfd);
}
}
// 等待子进程进行回收
void Wait()
{
pid_t rid = (_subid, nullptr, 0);
(void)rid; // 屏蔽未使用变量警告
}
~Channel() {}
private:
int _wfd;
pid_t _subid;
std::string _name;
};
// 再组织
// 对所有创建的管道进行管理
class ChannelManager
{
public:
ChannelManager()
: _next(0)
{
}
void Insert(int wfd, pid_t subid)
{
// Channel cn(wfd, subid);
// _channels.push_back(cn);
_channels.emplace_back(wfd, subid);
}
Channel &Select()
{
Channel &ch = _channels[_next]; // 获取对应的管道
_next++; // 轮询选择子进程
_next %= _channels.size(); // 防止越界
return ch;
}
// 关闭所有管道的写端,使所有子进程read返回0退出
void CloseSubProcess()
{
for (auto &channel : _channels)
{
channel.Close();
std::cout << channel.Name() << " close success!" << std::endl;
}
}
// 等待所有子进程进行回收处理
void WaitSubProcess()
{
for (auto &channel : _channels)
{
channel.Wait();
std::cout << channel.Name() << " wait success!" << std::endl;
}
}
// 调试打印:输出所有通道信息
void Print()
{
for (auto &channel : _channels)
{
std::cout << channel.Name() << " - " << channel.Wfd() << " - " << channel.Subid() << std::endl;
}
}
private:
std::vector<Channel> _channels;
int _next;
};
6.3 任务管理(Task.hpp)
任务管理层,通过 TaskManager 类将具体的任务函数(如打印日志、下载、上传、访问数据库)封装成统一的 std::function<void()> 类型并存储在 _task 容器中,提供 LoadTast() 加载所有任务、Code() 随机返回任务码(下标)以及 Execute(taskcode) 根据任务码调用对应函数的功能,使得进程池中的子进程只需拿到任务码就能执行相应的业务逻辑。
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <functional>
#include <vector>
#include <ctime>
// 包装器function:统一可调用对象的类型
using task_t = std::function<void()>;
// 具体任务1:打印日志(带子进程ID标识)
void PrintLog()
{
std::cout << "我是一个打印日志的任务" << std::endl;
}
// 具体任务2:模拟下载
void DownLoad()
{
std::cout << "我是一个下载任务" << std::endl;
}
// 具体任务3:模拟上传
void UpLoad()
{
std::cout << "我是一个上传任务" << std::endl;
}
// 具体任务4:模拟访问MySQL
void ReadMysql()
{
std::cout << "我是一个访问数据库的任务" << std::endl;
}
class TaskManager
{
public:
TaskManager()
{
srand(time(nullptr));
}
// 加载所有任务
void LoadTast()
{
_task.push_back(PrintLog); //将任务函数存放vector中,后续子进程通过_tm下标即可执行对应任务
_task.push_back(DownLoad);
_task.push_back(UpLoad);
_task.push_back(ReadMysql);
}
//随机获取任务码
int Code()
{
// 随机分配任务(任务码)(0~3)
return rand() % _task.size();
}
//// 执行任务码对应的任务
void Execute(int taskcode)
{
if(taskcode >= 0 && taskcode < _task.size())
{
_task[taskcode]();
}
}
~TaskManager()
{}
private:
std::vector<task_t> _task; // 存储所有可执行的任务
// 实现将任务函数放入vector容器中,通过下标即可获取对应的函数进行调用
};
6.4 进程池实现(ProcessPool.hpp)
完整的基于管道通信的进程池,主要包含三个部分:
ProcessPool 类负责整体流程控制,通过 Start() 创建指定数量的子进程和匿名管道(父进程关闭读端保留写端,子进程关闭写端进入 Work() 循环阻塞读取);DispatchTask() 和 PushTask() 配合 ChannelManager 的轮询机制将任务码均匀分发到各子进程;Stop() 统一关闭所有管道写端,使子进程 read 返回 0 退出并被回收,从而实现进程池的生命周期管理。
cpp
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__
#include "Channel.hpp"
#include "Task.hpp"
#include <cstdlib>
class ProcessPool
{
public:
ProcessPool(int num)
: _process_num(num)
{
_tm.LoadTast(); // 初始化进程池时加载所有的任务
}
// 子进程工作函数:循环读取管道中的任务码并执行
// rfd:管道读端文件描述符
void Work(int rwd)
{
//子进程一直等待获取任务码执行相应任务,只有当管道关闭,则子进程read返回0进行退出
while (true)
{
int taskcode = 0;
ssize_t n = read(rwd, &taskcode, sizeof(taskcode)); //当管道没有数据时则子进程堵塞等待
if (n < 0)
{
// 读取失败
std::cout << "读取错误" << std::endl;
break; // break后退出Work函数,则子进程退出
}
else if (n == 0)
{
// n为0说明读取文件结尾,则对应管道的写端close了,则子进程退出
//std::cout << "子进程退出" << std::endl;
break;
}
else
{
if (n != sizeof(taskcode))
{
// 说明数据没有读取完整,重新读取
continue;
}
else
{
// 读取成功且长度正确:执行任务
std::cout << "子进程[" << getpid() << "]接收一个任务码: " << taskcode << std::endl;
_tm.Execute(taskcode);
}
}
}
// 测试Work函数功能
// while(true)
// {
// std::cout << "我是子进程, 我的rfd是: " << rwd << std::endl;
// sleep(5);
// }
}
// 启动进程池(父进程执行):创建指定数量的子进程和管道
void Start()
{
for (int i = 0; i < _process_num; i++)
{
// 1. 创建匿名管道
int pipefd[2] = {0};
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)
{
// 子进程
close(pipefd[1]); // 子进程关闭写端(只读)
Work(pipefd[0]); // 执行工作函数
close(pipefd[0]);
exit(0);
}
else
{
// 父进程
close(pipefd[0]); // 父进程关闭读端(只写)
// 创建通道对象并加入管理列表
_cm.Insert(pipefd[1], id); // wfd subid
}
}
}
// 分配所有的任务给所有子进程执行
void DispatchTask()
{
int cnt = 10;
while (cnt--)
{
PushTask(cnt);
usleep(1000000); // 模拟任务分发间隔(500ms)
}
}
//分配随机任务给子进程
void PushTask(int cnt)
{
// 1、随机选分配一个任务(任务码:下标):
int taskcode = _tm.Code();
// 2、选择一个信道(子进程),需要负载均衡的选择一个子进程->轮询,执行任务
Channel &ch = _cm.Select(); // 获取对应管道
std::cout << "选择了一个子进程" << ch.Name() << std::endl;
// 3、通过任务码发送任务给选择的子进程执行
ch.Send(taskcode);
std::cout << "send " << taskcode << " to " << ch.Name() << ", 任务还剩: " << cnt << std::endl;
}
//退出进程池:关闭所有管道,回收子进程
void Stop()
{
//1、关闭所有管道的写端
_cm.CloseSubProcess();
//2、等待所有子进程进行回收处理
_cm.WaitSubProcess();
}
// 调试打印:输出所有通道信息
void DebugPrint()
{
_cm.Print();
}
private:
ChannelManager _cm; //进程池管理ChannelManager
int _process_num; // 进程池大小(子进程数量)
TaskManager _tm; //进程池管理TaskManager
};
#endif
6.5 主程序(Main.cc)
cpp
#include "processpool.hpp"
#include <memory>
#include <string>
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "Usage: " << argv[0] << " process-num" << std::endl;
return 1;
}
int num = std::stoi(argv[1]);
// 1. 创建进程池对象(智能指针自动管理内存)
std::unique_ptr<ProcessPool> pp(new ProcessPool(num));
// 2. 启动进程池(创建子进程和管道)
pp->Start();
sleep(2);
//测试管道
// pp->DebugPrint();
// sleep(100);
// 3. 派发所有随机任务
pp->DispatchTask();
//4、停止进程池(回收资源)
pp->Stop();
return 0;
}
6.6 编译运行
Makefile:
bash
BIN=processpool
CC=g++
FLAGS=-c -Wall -std=c++11
LDFLAGS=-o
SRC=$(wildcard *.cc)
OBJ=$(SRC:.cc=.o)
$(BIN):$(OBJ)
$(CC) $(LDFLAGS) $@ $^
%.o:%.cc
$(CC) $(FLAGS) $<
.PHONY:clean
clean:
rm -f $(BIN) $(OBJ)
运行示例:


七、核心要点总结
本文从进程间通信的基础出发,详细解析了 Linux 匿名管道的核心概念、创建 API、基于 fork 的跨进程通信实现,从文件描述符 和内核两个视角深入剖析了匿名管道的底层逻辑,最后实现简单的进程池,核心要点可总结为:
- 管道是 Linux 最基础的 IPC 方式,本质是内核中的一块缓冲区,贴合**"一切皆文件"** 的设计思想,通过read/write接口操作;
- 匿名管道通过pipe 函数创建,返回读、写两个文件描述符,需借助fork实现亲缘进程间通信,核心是子进程继承父进程的文件描述符表,共享同一个内核管道缓冲区;
- 匿名管道通信的标准流程是:创管道→fork 子进程→关闭无用文件描述符→读写通信→关闭描述符,关闭无用描述符是保证单向通信的关键;
- 从文件描述符视角,父子进程的文件描述符指向同一个内核管道缓冲区;从内核视角,管道是由两个file结构体指向同一个inode的内核缓冲区,读写操作是进程与内核缓冲区之间的数据拷贝;
- 匿名管道是半双工的,仅支持亲缘进程间的单向通信,若需双向通信需创建两个管道,且其生命周期随进程,数据不持久化。

结束语
本文从 IPC 基础知识起步,循序渐进讲解管道原理,结合代码实例剖析匿名管道通信细节,再深入内核剖析底层架构与读写规则,最终落地完成管道版简单进程池的实现。从理论到实操完整梳理了管道相关知识点,既理清进程隔离与数据互通的底层逻辑,也借助进程池项目验证管道在并发场景的实用价值。至此全文内容完结,依托管道实现进程通信与任务调度的相关内容介绍完毕。
