【Linux】进程间通信(二):命名管道与进程池架构实战

文章目录

进程间通信(二):命名管道与进程池架构实战

💬 欢迎讨论:在上一篇中,我们学习了匿名管道的原理和使用,但匿名管道只能用于有亲缘关系的进程。如果两个毫不相关的进程想要通信怎么办?比如一个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看到  │
│             │        │             │
│ 只能亲缘进程 │        │ 任意进程    │
└─────────────┘        └─────────────┘

命名管道的特点:

  1. 在文件系统中可见

    bash 复制代码
    ls -l myfifo
    prwxr-xr-x 1 user user 0 Jan 22 10:00 myfifo
    ↑
    p表示这是一个管道文件
  2. 任意进程都可以打开

    c 复制代码
    // 进程A
    int fd = open("myfifo", O_WRONLY);
    
    // 进程B
    int fd = open("myfifo", O_RDONLY);
    
    // 它们通过文件名找到同一个管道!
  3. 本质还是管道

    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 成功

七、总结

本篇我们学习了命名管道和进程池的完整实现。

核心知识点:

  1. 命名管道(FIFO)

    • 在文件系统中可见
    • 任意进程都可以打开
    • 通过文件名建立连接
  2. 命名管道的打开规则

    • 阻塞模式:等待对端打开
    • 非阻塞模式:立即返回
    • 读端/写端的不同行为
  3. 进程池架构

    • 避免频繁创建销毁进程
    • 控制并发数量
    • 提高系统性能
  4. 进程池三大组件

    • Channel:封装通信通道
    • TaskManager:管理任务
    • ProcessPool:核心逻辑
  5. 进程池关键技术

    • 关闭历史通道(避免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回收所有子进程

💡 思考题

  1. 如果不关闭历史通道,会有什么后果?
  2. 如何实现任务结果的返回?(提示:需要双向管道)
  3. 如何实现动态调整进程池大小?

下一篇,我们将学习共享内存、消息队列和信号量!


相关推荐
EndingCoder2 小时前
属性和参数装饰器
java·linux·前端·ubuntu·typescript
Data-Miner2 小时前
122页满分可编辑PPT | 企业4A数字化架构演进与治理方案
架构
凉、介2 小时前
ARM 架构中的内存屏障
arm开发·笔记·学习·架构·操作系统·嵌入式
馨谙2 小时前
shell编程三剑客------sed流编辑器基础应用大全以及运行示例
linux·运维·编辑器
HellowAmy2 小时前
我的C++规范 - 随机时间点
开发语言·c++·代码规范
驱动探索者2 小时前
Linux list 设计
linux·运维·list
郝学胜-神的一滴2 小时前
深入解析C/S架构与B/S架构:技术选型与应用实践
c语言·开发语言·前端·javascript·程序人生·架构
遇见火星2 小时前
在Linux中使用parted对大容量磁盘进行分区详细过程
linux·运维·网络·分区·parted
啊阿狸不会拉杆3 小时前
《计算机操作系统》第七章 - 文件管理
开发语言·c++·算法·计算机组成原理·os·计算机操作系统