文章目录
-
- 进程间通信(二):命名管道与进程池架构实战
- 一、命名管道(FIFO)原理
-
- [1.1 匿名管道的局限性](#1.1 匿名管道的局限性)
- [1.2 命名管道的特点](#1.2 命名管道的特点)
- [1.3 创建命名管道](#1.3 创建命名管道)
- [1.4 命名管道 vs 匿名管道](#1.4 命名管道 vs 匿名管道)
- 二、命名管道的打开规则
-
- [2.1 阻塞式打开(默认)](#2.1 阻塞式打开(默认))
- [2.2 非阻塞式打开](#2.2 非阻塞式打开)
- [2.3 打开规则总结表](#2.3 打开规则总结表)
- 三、命名管道实战案例
-
- [3.1 实战1: 文件拷贝](#3.1 实战1: 文件拷贝)
- [3.2 实战2: Server-Client通信](#3.2 实战2: Server-Client通信)
- 四、进程池架构设计
-
- [4.1 什么是进程池](#4.1 什么是进程池)
- [4.2 进程池架构图](#4.2 进程池架构图)
- [4.3 Channel类设计](#4.3 Channel类设计)
- [4.4 TaskManager类设计](#4.4 TaskManager类设计)
- 五、进程池核心实现
-
- [5.1 ProcessPool类设计](#5.1 ProcessPool类设计)
- [5.2 初始化进程池](#5.2 初始化进程池)
- [5.3 派发任务](#5.3 派发任务)
- [5.4 清理进程池](#5.4 清理进程池)
- 整的ProcessPool类
- 六、主函数与运行结果
- 七、总结
进程间通信(二):命名管道与进程池架构实战
💬 欢迎讨论:在上一篇中,我们学习了匿名管道的原理和使用,但匿名管道只能用于有亲缘关系的进程。如果两个毫不相关的进程想要通信怎么办?比如一个Server进程和一个Client进程,它们没有父子关系,如何交换数据?本篇将带你深入理解命名管道(FIFO),并通过一个完整的进程池项目,掌握管道在实际开发中的应用。
👍 点赞、收藏与分享:这篇文章包含了命名管道的完整原理、Server-Client通信实战、进程池的架构设计和代码实现,内容深入且实用,如果对你有帮助,请点赞、收藏并分享!
🚀 循序渐进:建议先掌握上一篇的匿名管道知识,这样理解命名管道和进程池会更轻松。
一、命名管道(FIFO)原理
1.1 匿名管道的局限性
回顾一下匿名管道的使用场景:
c
// 父进程创建管道
pipe(pipefd);
// fork后子进程继承文件描述符
pid = fork();
// 父子进程通过管道通信
核心问题:必须通过fork继承文件描述符!
这就导致了一个限制:
bash
✓ 可以通信的进程:
父进程 ←→ 子进程
兄弟进程 ←→ 兄弟进程
✗ 无法通信的进程:
进程A ←/→ 进程B (两个独立启动的进程)
实际场景:
bash
# 两个独立的程序
./server & # 启动服务端
./client # 启动客户端
# 它们之间如何通信?
# 匿名管道做不到!
1.2 命名管道的特点
命名管道(FIFO)解决了这个问题!
核心思想:把管道变成一个文件系统中的实体。
bash
匿名管道 命名管道(FIFO)
┌─────────────┐ ┌─────────────┐
│ 内核缓冲区 │ │ 内核缓冲区 │
│ │ │ │
│ 没有名字 │ │ 有文件名! │
│ │ │ │
│ 不可见 │ │ 可以ls看到 │
│ │ │ │
│ 只能亲缘进程 │ │ 任意进程 │
└─────────────┘ └─────────────┘
命名管道的特点:
-
在文件系统中可见
bashls -l myfifo prwxr-xr-x 1 user user 0 Jan 22 10:00 myfifo ↑ p表示这是一个管道文件 -
任意进程都可以打开
c// 进程A int fd = open("myfifo", O_WRONLY); // 进程B int fd = open("myfifo", O_RDONLY); // 它们通过文件名找到同一个管道! -
本质还是管道
bash# FIFO 不占用用于存储数据内容的磁盘块,但会占用少量元数据空间(inode/目录项)。 # 数据在内核缓冲区中 # 读取后数据就消失
1.3 创建命名管道
方法1: 命令行创建
bash
mkfifo myfifo
ls -l myfifo
prwxr-xr-x 1 user user 0 Jan 22 10:00 myfifo
# 测试通信
# 终端1
echo "hello" > myfifo # 阻塞,等待读端
# 终端2
cat myfifo # 读取数据
hello # 输出,终端1解除阻塞
方法2: 程序中创建
c
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
// 参数:
// pathname: FIFO文件名
// mode: 权限(如0644)
//
// 返回值:
// 成功: 0
// 失败: -1
示例代码:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
umask(0); // 清除umask,使mode生效
if (mkfifo("myfifo", 0644) < 0) {
ERR_EXIT("mkfifo");
}
printf("命名管道创建成功\n");
return 0;
}
运行结果:
bash
gcc create_fifo.c -o create_fifo
./create_fifo
命名管道创建成功
ls -l myfifo
prw-r--r-- 1 user user 0 Jan 22 10:10 myfifo
1.4 命名管道 vs 匿名管道
| 特性 | 匿名管道 | 命名管道 |
|---|---|---|
| 创建方式 | pipe()系统调用 | mkfifo()或命令行 |
| 访问方式 | 文件描述符 | open()打开文件 |
| 可见性 | 不可见 | 文件系统中可见 |
| 使用范围 | 只能亲缘进程 | 任意进程 |
| 生命周期 | 随进程 | 随文件(需手动删除) |
| 打开操作 | 无需打开 | 需要open() |
核心区别:
bash
匿名管道:
pipe(fds) → 直接返回文件描述符
命名管道:
mkfifo("myfifo") → 创建文件
open("myfifo", O_RDONLY) → 打开得到文件描述符
之后的read/write操作完全一样!
二、命名管道的打开规则
2.1 阻塞式打开(默认)
读端打开:
c
int fd = open("myfifo", O_RDONLY);
// 阻塞,直到有进程以写方式打开
写端打开:
c
int fd = open("myfifo", O_WRONLY);
// 阻塞,直到有进程以读方式打开
流程图:
bash
进程A 进程B
│ │
├─ open("fifo", O_RDONLY) │
│ (阻塞中...) │
│ │
│ ├─ open("fifo", O_WRONLY)
│ (解除阻塞) │ (解除阻塞)
│ │
↓ ↓
打开成功 打开成功
验证代码:
c
// 进程A: 只读打开
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
printf("准备打开FIFO(只读)...\n");
int fd = open("myfifo", O_RDONLY);
printf("打开成功! fd=%d\n", fd);
sleep(10);
close(fd);
return 0;
}
c
// 进程B: 只写打开
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
printf("准备打开FIFO(只写)...\n");
int fd = open("myfifo", O_WRONLY);
printf("打开成功! fd=%d\n", fd);
sleep(10);
close(fd);
return 0;
}
测试:
bash
# 终端1
./reader
准备打开FIFO(只读)...
(阻塞中...)
# 终端2
./writer
准备打开FIFO(只写)...
# 此时终端1和2同时输出:
打开成功! fd=3
2.2 非阻塞式打开
使用O_NONBLOCK标志:
c
// 读端非阻塞打开
int fd = open("myfifo", O_RDONLY | O_NONBLOCK);
// 立即返回,不等待写端
// 写端非阻塞打开
int fd = open("myfifo", O_WRONLY | O_NONBLOCK);
// 如果没有读端,立即返回-1, errno=ENXIO
测试代码:
c
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
int main() {
int fd = open("myfifo", O_WRONLY | O_NONBLOCK);
if (fd < 0) {
printf("打开失败: %s\n", strerror(errno));
// 输出: No such device or address (ENXIO)
} else {
printf("打开成功\n");
}
return 0;
}
2.3 打开规则总结表
| 打开方式 | 阻塞模式 | 非阻塞模式 |
|---|---|---|
| O_RDONLY | 等待写端打开 | 立即返回成功 |
| O_WRONLY | 等待读端打开 | 无读端则返回-1(ENXIO) |
| O_RDWR | 立即返回 | 立即返回 |
注意: O_RDWR方式打开FIFO是未定义行为,不推荐使用!
三、命名管道实战案例
3.1 实战1: 文件拷贝
需求:两个进程协作拷贝文件
bash
进程A: 读取源文件 → 写入FIFO
进程B: 从FIFO读取 → 写入目标文件
进程A代码(读文件,写管道):
c
// writer.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
// 1. 创建FIFO
umask(0);
if (mkfifo("myfifo", 0644) < 0) {
// 已存在也没关系
}
// 2. 打开源文件
int infd = open("source.txt", O_RDONLY);
if (infd < 0)
ERR_EXIT("open source");
// 3. 打开FIFO(写端)
int outfd = open("myfifo", O_WRONLY);
if (outfd < 0)
ERR_EXIT("open fifo");
// 4. 读文件,写FIFO
char buf[1024];
ssize_t n;
while ((n = read(infd, buf, sizeof(buf))) > 0) {
write(outfd, buf, n);
}
printf("写入完成\n");
close(infd);
close(outfd);
return 0;
}
进程B代码(读管道,写文件):
c
// reader.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
// 1. 打开FIFO(读端)
int infd = open("myfifo", O_RDONLY);
if (infd < 0)
ERR_EXIT("open fifo");
// 2. 打开目标文件
int outfd = open("dest.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (outfd < 0)
ERR_EXIT("open dest");
// 3. 读FIFO,写文件
char buf[1024];
ssize_t n;
while ((n = read(infd, buf, sizeof(buf))) > 0) {
write(outfd, buf, n);
}
printf("读取完成\n");
close(infd);
close(outfd);
// 4. 删除FIFO
unlink("myfifo");
return 0;
}
测试:
bash
# 准备源文件
echo "Hello FIFO World" > source.txt
# 编译
gcc writer.c -o writer
gcc reader.c -o reader
# 运行(需要两个终端)
# 终端1
./writer
(阻塞,等待reader打开...)
# 终端2
./reader
写入完成 ← 终端1输出
读取完成 ← 终端2输出
# 验证
cat dest.txt
Hello FIFO World
流程图:
bash
writer进程 FIFO reader进程
│ │ │
├─ open("source") │ │
├─ open("myfifo",W) │ │
│ (阻塞...) │ │
│ │ ├─ open("myfifo",R)
│ (解除阻塞) │ (解除阻塞) │
│ │ ├─ open("dest")
│ │ │
├─ read(source) │ │
├─ write(fifo) ─────→│ │
│ ├──────────────────→ ├─ read(fifo)
│ │ ├─ write(dest)
│ │ │
├─ close(fifo) │ │
│ │ ├─ close(fifo)
│ │ ├─ unlink("myfifo")
3.2 实战2: Server-Client通信
需求:实现一个简单的消息收发程序
bash
Server: 接收Client发送的消息并显示
Client: 从键盘读取,发送给Server
Server代码:
c
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#define FIFO_NAME "server_fifo"
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
// 1. 创建FIFO
umask(0);
if (mkfifo(FIFO_NAME, 0666) < 0) {
// 已存在
}
printf("Server启动,等待Client连接...\n");
// 2. 打开FIFO(读端)
int fd = open(FIFO_NAME, O_RDONLY);
if (fd < 0)
ERR_EXIT("open");
printf("Client已连接!\n");
// 3. 循环读取消息
char buf[1024];
while (1) {
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("Client说: %s", buf);
} else if (n == 0) {
printf("Client断开连接\n");
break;
} else {
ERR_EXIT("read");
}
}
close(fd);
unlink(FIFO_NAME);
return 0;
}
Client代码:
c
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FIFO_NAME "server_fifo"
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
// 1. 打开FIFO(写端)
int fd = open(FIFO_NAME, O_WRONLY);
if (fd < 0)
ERR_EXIT("open");
printf("已连接到Server,开始输入消息:\n");
// 2. 循环发送消息
char buf[1024];
while (fgets(buf, sizeof(buf), stdin)) {
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}
测试:
bash
# 编译
gcc server.c -o server
gcc client.c -o client
# 终端1: 启动Server
$ ./server
Server启动,等待Client连接...
(阻塞中...)
# 终端2: 启动Client
./client
已连接到Server,开始输入消息:
# 此时终端1显示:
Client已连接!
# 终端2输入:
hello
world
# 终端1显示:
Client说: hello
Client说: world
# 终端2按Ctrl+D退出:
# 终端1显示:
Client断开连接
四、进程池架构设计
4.1 什么是进程池
问题场景:
假设你在写一个Web服务器,每来一个请求就fork一个进程处理:
c
while (1) {
int client = accept(server_fd);
pid_t pid = fork();
if (pid == 0) {
handle_request(client);
exit(0);
}
}
问题:
bash
1. fork开销大 (创建进程慢)
2. 进程频繁创建销毁 (浪费资源)
3. 进程数量不可控 (可能创建成千上万个)
解决方案:进程池
bash
进程池思想:
┌──────────────────────────────────┐
│ 提前创建N个工作进程 │
│ 放入"池子"中待命 │
│ 有任务时,从池子中取一个进程处理 │
│ 处理完后,进程回到池子等待下一个任务│
└──────────────────────────────────┘
优势:
✓ 避免频繁创建销毁进程
✓ 控制并发数量
✓ 提高响应速度
4.2 进程池架构图
bash
主进程(Master)
│
├─── pipe0 ───→ 子进程0 (Worker)
├─── pipe1 ───→ 子进程1 (Worker)
├─── pipe2 ───→ 子进程2 (Worker)
├─── pipe3 ───→ 子进程3 (Worker)
└─── pipe4 ───→ 子进程4 (Worker)
工作流程:
1. Master创建5个Worker进程
2. 每个Worker通过管道与Master通信
3. Master轮流给Worker分配任务
4. Worker执行任务,然后等待下一个
核心组件:
bash
1. Channel类: 封装通信通道(管道)
2. TaskManager类: 管理任务
3. ProcessPool类: 进程池核心逻辑
4.3 Channel类设计
作用:封装Master到Worker的单向通道
cpp
// Channel.hpp
class Channel {
private:
int _wfd; // 写端文件描述符
pid_t _who; // 对应的子进程PID
std::string _name; // 通道名称
public:
// 构造函数
Channel(int wfd, pid_t who);
// 发送任务编号
void Send(int cmd);
// 关闭管道
void Close();
// 获取子进程PID
pid_t Id();
// 获取写端fd
int wFd();
};
完整实现:
cpp
#ifndef __CHANNEL_HPP__
#define __CHANNEL_HPP__
#include <iostream>
#include <string>
#include <unistd.h>
class Channel {
public:
Channel(int wfd, pid_t who) : _wfd(wfd), _who(who) {
// 生成通道名: Channel-3-1234
_name = "Channel-" + std::to_string(wfd) + "-" + std::to_string(who);
}
std::string Name() {
return _name;
}
void Send(int cmd) {
// 发送4字节的任务编号
::write(_wfd, &cmd, sizeof(cmd));
}
void Close() {
::close(_wfd);
}
pid_t Id() {
return _who;
}
int wFd() {
return _wfd;
}
~Channel() {}
private:
int _wfd; // 写端fd
std::string _name; // 通道名
pid_t _who; // 子进程PID
};
#endif
4.4 TaskManager类设计
作用:管理所有任务
cpp
// Task.hpp
class TaskManager {
private:
std::vector<task_t> tasks; // 任务列表
public:
TaskManager();
// 随机选择一个任务
int SelectTask();
// 执行指定编号的任务
void Execute(unsigned long number);
};
完整实现:
cpp
#pragma once
#include <iostream>
#include <vector>
#include <functional>
#include <ctime>
#include <unistd.h>
using task_t = std::function<void()>;
class TaskManager {
public:
TaskManager() {
srand(time(nullptr));
// 注册4个任务
tasks.push_back([]() {
std::cout << "子进程[" << getpid() << "] 执行数据库查询任务\n";
});
tasks.push_back([]() {
std::cout << "子进程[" << getpid() << "] 执行URL解析任务\n";
});
tasks.push_back([]() {
std::cout << "子进程[" << getpid() << "] 执行加密任务\n";
});
tasks.push_back([]() {
std::cout << "子进程[" << getpid() << "] 执行数据持久化任务\n";
});
}
int SelectTask() {
return rand() % tasks.size();
}
void Execute(unsigned long number) {
if (number >= tasks.size()) return;
tasks[number]();
}
~TaskManager() {}
private:
std::vector<task_t> tasks;
};
// 全局任务管理器
TaskManager tm;
// Worker进程的工作函数
void Worker() {
while (true) {
int cmd = 0;
// 从标准输入读取任务编号
int n = ::read(0, &cmd, sizeof(cmd));
if (n == sizeof(cmd)) {
// 执行任务
tm.Execute(cmd);
} else if (n == 0) {
// 管道关闭,退出
std::cout << "pid: " << getpid() << " 退出...\n";
break;
}
}
}
注意Worker函数的关键点:
cpp
// 为什么从标准输入(fd=0)读取?
// 因为在创建进程池时,会用dup2重定向!
// ProcessPool创建子进程时:
dup2(pipefd[0], 0); // 标准输入重定向到管道读端
work(); // 调用Worker函数
// 这样Worker就可以用read(0)读取任务了!
五、进程池核心实现
5.1 ProcessPool类设计
cpp
class ProcessPool {
private:
std::vector<Channel> channels; // 通道列表
int processnum; // 进程数量
work_t work; // 工作函数
public:
ProcessPool(int n, work_t w);
// 初始化进程池
int InitProcessPool();
// 派发任务
void DispatchTask();
// 清理进程池
void CleanProcessPool();
};
5.2 初始化进程池
核心逻辑:
bash
for i in range(进程数量):
1. 创建管道pipe(pipefd)
2. fork创建子进程
3. 子进程:
- 关闭历史通道的写端
- 关闭当前管道的写端
- dup2重定向标准输入到管道读端
- 调用work()进入工作循环
4. 父进程:
- 关闭管道读端
- 保存Channel对象
完整代码:
cpp
int InitProcessPool() {
for (int i = 0; i < processnum; i++) {
// 1. 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0) return PipeError;
// 2. 创建子进程
pid_t id = fork();
if (id < 0) return ForkError;
if (id == 0) { // 子进程
// 3. 关闭历史通道(重要!)
std::cout << getpid() << " 子进程关闭历史fd: ";
for (auto &c : channels) {
std::cout << c.wFd() << " ";
c.Close();
}
std::cout << "完成\n";
// 4. 关闭当前管道写端
::close(pipefd[1]);
// 5. 重定向标准输入
dup2(pipefd[0], 0);
// 6. 进入工作循环
work();
::exit(0);
}
// 父进程
::close(pipefd[0]); // 关闭读端
channels.emplace_back(pipefd[1], id); // 保存Channel
}
return OK;
}
为什么要关闭历史通道?
这是一个非常关键的细节!
bash
第1次循环:
父)
父进程: fork() → 子进程1
子进程1: 继承了pipefd1
父进程: 保存pipefd1[1]到channels
第2次循环:
父进程: pipe(pipefd2)
父进程: fork() → 子进程2
子进程2: 继承了pipefd1和pipefd2! ← 问题!
如果不关闭pipefd1:
子进程2持有pipefd1[1]
→ pipefd1的写端引用计数=2(父进程+子进程2)
→ 即使父进程关闭pipefd1[1],子进程1的read也不会返回0
→ 子进程1永远无法正常退出!
图解:
bash
创建3个子进程的文件描述符情况:
父进程 子进程1 子进程2 子进程3
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│pipe1[1]│ │pipe1[0]│ │pipe1[0]│✗ │pipe1[0]│✗
│pipe2[1]│ │ │ │pipe1[1]│✗ │pipe1[1]│✗
│pipe3[1]│ │ │ │pipe2[0]│ │pipe2[0]│✗
└────────┘ └────────┘ │pipe2[1]│✗ │pipe2[1]│✗
└────────┘ │pipe3[0]│
│pipe3[1]│✗
└────────┘
✗ 表示需要关闭的fd
5.3 派发任务
核心逻辑:轮询算法
cpp
void DispatchTask() {
int who = 0; // 当前使用的通道索引
int num = 20; // 总共派发20个任务
while (num--) {
// 1. 选择一个任务
int task = tm.SelectTask();
// 2. 选择一个子进程
Channel &curr = channels[who++];
who %= channels.size(); // 轮询
// 3. 派发任务
std::cout << "######################\n";
std::cout << "派发任务" << task << "给" << curr.Name()
<< ", 剩余: " << num << "\n";
std::cout << "######################\n";
curr.Send(task);
sleep(1); // 模拟任务间隔
}
}
流程图:
bash
Master进程
│
├─ 选择任务编号2
├─ 选择Worker0
├─ 发送: write(pipe0[1], &2, 4)
│
├─ 选择任务编号1
├─ 选择Worker1
├─ 发送: write(pipe1[1], &1, 4)
│
├─ 选择任务编号3
├─ 选择Worker2
├─ 发送: write(pipe2[1], &3, 4)
│
└─ ...轮询
Worker0 Worker1 Worker2
│ │ │
├─ read(0) 阻塞 ├─ read(0) 阻塞 ├─ read(0) 阻塞
├─ 收到2 │ │
├─ 执行任务2 │ │
├─ read(0) 阻塞 ├─ 收到1 │
│ ├─ 执行任务1 │
│ ├─ read(0) 阻塞 ├─ 收到3
│ │ ├─ 执行任务3
5.4 清理进程池
核心逻辑:
cpp
void CleanProcessPool() {
for (auto &c : channels) {
// 1. 关闭管道写端
c.Close(); // 子进程的read(0)会返回0
// 2. 等待子进程退出
pid_t rid = ::waitpid(c.Id(), nullptr, 0);
if (rid > 0) {
std::cout << "回收子进程 " << rid << " 成功\n";
}
}
}
为什么关闭管道就能让子进程退出?
bash
Master关闭所有写端:
close(pipe0[1])
close(pipe1[1])
close(pipe2[1])
...
Worker0中:
read(0, &cmd, 4) → 返回0 (EOF)
→ 退出循环
→ exit(0)
Master中:
waitpid(worker0_pid) → 回收资源
整的ProcessPool类
cpp
// ProcessPool.hpp
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"
#include "Channel.hpp"
using work_t = std::function<void()>;
enum {
OK = 0,
UsageError,
PipeError,
ForkError
};
class ProcessPool {
public:
ProcessPool(int n, work_t w) : processnum(n), work(w) {}
int InitProcessPool() {
for (int i = 0; i < processnum; i++) {
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0) return PipeError;
pid_t id = fork();
if (id < 0) return ForkError;
if (id == 0) { // 子进程
// 关闭历史通道
std::cout << getpid() << " 关闭历史fd: ";
for (auto &c : channels) {
std::cout << c.wFd() << " ";
c.Close();
}
std::cout << "\n";
::close(pipefd[1]);
dup2(pipefd[0], 0);
work();
::exit(0);
}
// 父进程
::close(pipefd[0]);
channels.emplace_back(pipefd[1], id);
}
return OK;
}
void DispatchTask() {
int who = 0;
int num = 20;
while (num--) {
int task = tm.SelectTask();
Channel &curr = channels[who++];
who %= channels.size();
std::cout << "######################\n";
std::cout << "派发任务" << task << "给" << curr.Name()
<< ", 剩余: " << num << "\n";
std::cout << "######################\n";
curr.Send(task);
sleep(1);
}
}
void CleanProcessPool() {
for (auto &c : channels) {
c.Close();
pid_t rid = ::waitpid(c.Id(), nullptr, 0);
if (rid > 0) {
std::cout << "回收子进程 " << rid << " 成功\n";
}
}
}
void DebugPrint() {
for (auto &c : channels) {
std::cout << c.Name() << "\n";
}
}
private:
std::vector<Channel> channels;
int processnum;
work_t work;
};
#endif
六、主函数与运行结果
6.1 Main.cc
cpp
#include "ProcessPool.hpp"
#include "Task.hpp"
#include <string>
void Usage(std::string proc) {
std::cout << "Usage: " << proc << " process-num\n";
}
int main(int argc, char *argv[]) {
if (argc != 2) {
Usage(argv[0]);
return UsageError;
}
int num = std::stoi(argv[1]);
// 创建进程池
ProcessPool *pp = new ProcessPool(num, Worker);
// 1. 初始化进程池
pp->InitProcessPool();
// 2. 派发任务
pp->DispatchTask();
// 3. 清理进程池
pp->CleanProcessPool();
delete pp;
return 0;
}
6.2 Makefile
makefile
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)
6.3 编译运行
bash
# 编译
make
g++ -c -Wall -std=c++11 Main.cc
g++ -o processpool Main.o
# 运行(创建5个工作进程)
./processpool 5
12345 关闭历史fd:
12346 关闭历史fd: 4
12347 关闭历史fd: 4 6
12348 关闭历史fd: 4 6 8
12349 关闭历史fd: 4 6 8 10
######################
派发任务2给Channel-4-12345, 剩余: 19
######################
子进程[12345] 执行加密任务
######################
派发任务1给Channel-6-12346, 剩余: 18
######################
子进程[12346] 执行URL解析任务
######################
派发任务0给Channel-8-12347, 剩余: 17
######################
子进程[12347] 执行数据库查询任务
######################
派发任务3给Channel-10-12348, 剩余: 16
######################
子进程[12348] 执行数据持久化任务
######################
派发任务2给Channel-12-12349, 剩余: 15
######################
子进程[12349] 执行加密任务
... (后续任务轮询派发)
pid: 12345 退出...
pid: 12346 退出...
pid: 12347 退出...
pid: 12348 退出...
pid: 12349 退出...
回收子进程 12345 成功
回收子进程 12346 成功
回收子进程 12347 成功
回收子进程 12348 成功
回收子进程 12349 成功
七、总结
本篇我们学习了命名管道和进程池的完整实现。
核心知识点:
-
命名管道(FIFO)
- 在文件系统中可见
- 任意进程都可以打开
- 通过文件名建立连接
-
命名管道的打开规则
- 阻塞模式:等待对端打开
- 非阻塞模式:立即返回
- 读端/写端的不同行为
-
进程池架构
- 避免频繁创建销毁进程
- 控制并发数量
- 提高系统性能
-
进程池三大组件
- Channel:封装通信通道
- TaskManager:管理任务
- ProcessPool:核心逻辑
-
进程池关键技术
- 关闭历史通道(避免fd泄漏)
- dup2重定向标准输入
- 轮询算法派发任务
- 关闭管道触发子进程退出
进程池完整流程图:
bash
主进程
│
├─ 创建5个管道
├─ fork 5个子进程
│ ├─ 子进程1: dup2(pipe1[0], 0) → Worker()
│ ├─ 子进程2: dup2(pipe2[0], 0) → Worker()
│ ├─ 子进程3: dup2(pipe3[0], 0) → Worker()
│ ├─ 子进程4: dup2(pipe4[0], 0) → Worker()
│ └─ 子进程5: dup2(pipe5[0], 0) → Worker()
│
├─ 轮询派发20个任务
│ write(pipe1[1], &task)
│ write(pipe2[1], &task)
│ ...
│
├─ 关闭所有管道写端
│ close(pipe1[1])
│ close(pipe2[1])
│ ...
│
└─ waitpid回收所有子进程
💡 思考题
- 如果不关闭历史通道,会有什么后果?
- 如何实现任务结果的返回?(提示:需要双向管道)
- 如何实现动态调整进程池大小?
下一篇,我们将学习共享内存、消息队列和信号量!