Linux文件通信-管道

目录

一、管道的“骨架” 

二、管道通信的原理

匿名管道 pipe:多子进程场景下文件描述符正确关闭核心原理

回顾pipe 最核心的规则

多子进程场景的灾难:不关闭会发生什么?

解决方案 1:子进程创建后 批量关闭 不用的描述符

解决方案 2:逆序关闭(继承覆盖问题)

总结

三、进程管理与信号:管道的“边界控制”

1. 进程状态与ps命令

2. 信号:管道的 “异常通知”

实战:自定义SIGPIPE处理

四、管道的局限性与优化方向

1. 匿名管道的核心局限性

2. 优化与替代方案

(1)命名管道(FIFO):突破 “亲缘关系” 限制

命名管道的核心特性

命名管道的核心 API 详解

命名管道的内核原理

命名管道 vs 匿名管道:核心差异对比

注意事项

结余

(2)更复杂的 IPC 机制:应对多样化需求

五、拓展实战:基于管道的进程池实现

1. 任务定义头文件(Task.hpp)

2. 管道型进程池核心代码

总结:管道是Linux IPC的“入门钥匙”


在 Linux 系统编程中,进程间通信(IPC) 是实现多进程协作的关键,而管道(Pipe) 作为最基础的 IPC 机制之一,背后蕴含着内核数据结构、系统调用和进程管理的深层逻辑。本文将从数据结构、管道原理、进程管理、实战应用四个维度解析管道通信,并拓展实现管道型进程池,展示管道在批量任务调度中的进阶用法。

一、管道的“骨架” 

要理解管道,先得看清它依赖的核心数据结构——这些结构体是Linux内核管理“文件”和“进程”的基石。
 
1.  struct file 与 struct file_operations 
 

-  struct file :描述**“打开的文件”**,包含文件状态(如是否可读写)、文件偏移量、指向文件操作的指针等。管道本质上是一种“特殊文件”,因此也由 struct file 管理。
-  struct file_operations :是一个函数指针集合,定义了对文件的所有操作(如 read 、 write 、 open 、 close 等)。管道的读写逻辑,就通过重载这些函数指针实现“字节流通信”。
 
2.  task_struct :进程的“身份证”
 
 task_struct 是Linux内核中描述进程的结构体,包含进程ID(PID)、进程状态、文件描述符表、父子进程关系等关键信息。
 
- 每个进程都有一个 task_struct 实例,而文件描述符表是其中的核心组件——它记录了进程打开的所有文件(包括管道),让进程能通过“文件描述符”(如 pipefd[0] / pipefd[1] )操作管道。
 
3. 数据结构的关联:进程与管道的“纽带”
 
进程通过文件描述符表关联到 struct file ,而 struct file 又通过 struct file_operations 定义管道的读写行为。这种关联,让“进程操作管道”的逻辑得以落地:

graph LR
    A[进程 task_struct] --> B[文件描述符表];
    B --> C[struct file(管道)];
    C --> D[struct file_operations(管道读写逻辑)];

二、管道通信的原理


管道是“单向字节流”通信机制,分为匿名管道和命名管道(FIFO)。我们先聚焦“匿名管道”的原理与实现。
 
1. 接口定义: pipe() 系统调用
 
创建匿名管道的入口是 pipe() 系统调用,原型如下:

#include <unistd.h>
int pipe(int pipefd[2]);

-  pipefd[0] :读端,用于从管道中读取数据;
-  pipefd[1] :写端,用于向管道中写入数据;
- 返回值:成功返回 0 ,失败返回 -1 。
 
2. 内核实现细节
 
匿名管道的内核实现,藏着三个关键逻辑:
 
- 基于文件系统的“匿名性”:
匿名管道没有文件名,仅在创建它的进程及其子进程中可见(通过 fork 继承文件描述符)。内核通过“文件系统”机制管理管道的缓冲区,但不将其暴露到磁盘文件系统中。
- 容量限制: PIPE_BUF :
Linux中管道的默认缓冲区大小是 4096字节(PIPE_BUF) 。如果写入数据超过 PIPE_BUF ,写入操作可能不再“原子性”(多个写操作的数据可能交织)。
- 通信流程:“写→存→读”:
写进程向 pipefd[1] 写入数据,内核将数据暂存到“管道缓冲区”;读进程从 pipefd[0] 读取数据,内核从缓冲区中消费数据——以此实现进程间的“字节流”通信。
 
3. 实战示例:父子进程管道通信
 
下面是一个经典的“父进程写、子进程读”的管道通信示例:

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    int pipefd[2];
    char buf[100];

    // 1. 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }

    // 2. fork创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {  // 子进程(读端)
        close(pipefd[1]);  // 关闭写端
        int n = read(pipefd[0], buf, sizeof(buf));
        printf("子进程读取到:%.*s\n", n, buf);
        close(pipefd[0]);
    } else {  // 父进程(写端)
        close(pipefd[0]);  // 关闭读端
        const char* msg = "Hello, Pipe!";
        write(pipefd[1], msg, strlen(msg));
        close(pipefd[1]);
        wait(NULL);  // 等待子进程结束
    }

    return 0;
}

运行结果会输出: 子进程读取到:Hello, Pipe! ,完美演示了管道的“父子通信”能力。

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib> //stdlib.h
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define N 2
#define NUM 1024

using namespace std;

// child
void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t self = getpid();
    int number = 0;

    char buffer[NUM];
    while (true)
    {
        sleep(1);

        // 构建发送字符串
        //buffer[0] = 0; // 字符串清空, 只是为了提醒阅读代码的人,我把这个数组当做字符串了
        //snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
        // cout << buffer << endl;
        // 发送/写入给父进程, system call
        write(wfd, buffer, strlen(buffer)); // strlen(buffer) + 1???
      
        //if(number >= 5) break;
    }
}

// father
void Reader(int rfd)
{
    char buffer[NUM];

    while(true)
    {
        buffer[0] = 0; 
        // system call
        ssize_t n = read(rfd, buffer, sizeof(buffer)); //sizeof != strlen
        if(n > 0)
        {
            buffer[n] = 0; // 0 == '\0'
            cout << "father get a message[" << getpid() << "]# " << buffer << endl;
        }
        else if(n == 0) 
        {
            printf("father read file done!\n");
            break;
        }
        else break;
        // cout << "n: " << n << endl;
    }
}

int main()
{
    int pipefd[N] = {0};
    int n = pipe(pipefd);
    if (n < 0)
        return 1;

    // cout << "pipefd[0]: " << pipefd[0] << " , pipefd[1]: " << pipefd[1] << endl;

    // child -> w, father->r
    pid_t id = fork();
    if (id < 0)
        return 2;
    if (id == 0)
    {
        // child
        close(pipefd[0]);

        // IPC code
        Writer(pipefd[1]);

        close(pipefd[1]);
        exit(0);
    }
    // father
    close(pipefd[1]);

    // IPC code
    Reader(pipefd[0]);

    pid_t rid = waitpid(id, nullptr, 0);
    if(rid < 0) return 3;

    close(pipefd[0]);


    sleep(5);
    return 0;
}

匿名管道 pipe:多子进程场景下文件描述符正确关闭核心原理

父进程创建多个子进程 + 管道通信的场景中,文件描述符(读端 / 写端)的关闭规则是最容易出错、也是必须掌握的核心。

你问的核心问题:为什么多子进程用 pipe 时,必须 “逆序关闭” 或 “子进程创建后批量关闭” 不用的文件描述符?如果不关闭,会导致:子进程写端 “继承并指向” 上一个子进程,读端永远读不到 EOF,父进程无法正常回收子进程!

我用最通俗、最底层的方式给你讲清楚。


回顾pipe 最核心的规则

  1. 管道只有在所有写端都被关闭时,读端才会读到 0(EOF)
  2. 只要还有任意一个写端开着,读端就会一直阻塞
  3. fork () 会完整复制父进程的文件描述符表→ 子进程会继承父进程的管道读端 + 管道写端

这三条规则,就是多子进程必须关闭描述符的根本原因。


多子进程场景的灾难:不关闭会发生什么?

假设:

  • 父进程创建 pipe(int fd[2])fd[0] 读,fd[1]
  • 父进程依次创建 3 个子进程
  • 每个子进程都要向管道写数据
  • 父进程从管道读数据,并等待子进程退出

错误行为(不关闭描述符)

  1. 父进程创建 pipe → 拥有 fd[0], fd[1]
  2. fork 子进程 1 → 子进程 1 也拥有 fd[0], fd[1]
  3. fork 子进程 2 → 子进程 2 也拥有 fd[0], fd[1]
  4. fork 子进程 3 → 子进程 3 也拥有 fd[0], fd[1]

最终结果:整个系统中,一共有 4 个写端(父 1 个 + 3 个子进程)!

即使:

  • 3 个子进程都关闭了自己的写端
  • 子进程全部退出

只要父进程的写端没关,读端就永远等不到 EOF,一直阻塞!父进程卡死,无法回收子进程。


解决方案 1:子进程创建后 批量关闭 不用的描述符

规则(黄金法则)

  • 谁用读,谁就关写
  • 谁用写,谁就关读
  • 子进程只用写 → 子进程必须立刻关闭读端 fd[0]
  • 父进程只用读 → 父进程必须关闭写端 fd[1]

代码模板

int fd[2];
pipe(fd); // 创建管道

// 父进程:创建多个子进程
for (int i = 0; i < 3; i++) {
    pid_t id = fork();

    if (id == 0) {
        // -----------------------
        // 子进程:只写,所以关闭读端
        // -----------------------
        close(fd[0]);

        // 子进程写数据...
        write(fd[1], ...);

        close(fd[1]); // 写完关闭写端
        exit(0);
    }
}

// -----------------------
// 父进程:只读,所以关闭写端
// -----------------------
close(fd[1]);

// 父进程读数据(所有子进程写端关闭后,会读到0,退出循环)
while (read(fd[0], ...) > 0);

close(fd[0]);

为什么必须 “批量关闭”?

因为 fork 会复制描述符,每创建一个子进程,就多一份读端 + 写端。子进程创建后立刻关闭读端,父进程最终统一关闭写端

这样才能保证:最终只有子进程持有写端,父进程不持有任何写端。所有子进程退出 → 所有写端关闭 → 父进程读到 EOF → 正常回收。


解决方案 2:逆序关闭(继承覆盖问题)

你提到的 **“逆至关闭”**,准确名称是:

父进程在创建多个子进程时,必须避免 “旧子进程的描述符被新子进程继承”

多子进程的隐藏坑:描述符继承传递

父进程创建子进程 1 → 子进程 1 继承了写端父进程再创建子进程 2 → 子进程 2 也继承了父进程的写端这就导致:子进程 2 的写端,相当于 “指向并继承” 了子进程 1 的写端→ 写端数量变多→ 读端永远读不到 EOF

逆序关闭 / 即时关闭 方案

每创建完一个子进程,父进程就把这个子进程用不到的描述符立刻关闭!不让下一个子进程继承多余描述符。

代码示例:

int fd[2];
pipe(fd);

// 子进程1
if (fork() == 0) {
    close(fd[0]);
    write(...);
    close(fd[1]);
    exit(0);
}
// 父进程:不关闭任何描述符,继续创建子进程2

// 子进程2
if (fork() == 0) {
    close(fd[0]);
    write(...);
    close(fd[1]);
    exit(0);
}

// 父进程最后统一关闭写端
close(fd[1]);

真正安全的 “逆序 / 批量关闭” 写法(推荐)

for (...) {
    int fd[2];
    pipe(fd);

    if (fork() == 0) {
        close(fd[0]);
        write(...);
        close(fd[1]);
        exit(0);
    }

    // 父进程:立刻关闭当前管道的写端!
    close(fd[1]);
}

核心作用:

不让后续子进程继承前面的管道描述符从根源避免:

  • 多余写端产生
  • 子进程写端指向 “上一个子进程”
  • 读端阻塞

最关键的一句话总结:

不关闭描述符 = 写端永远存在 = 读端永远阻塞 = 子进程无法回收

关闭规则只有两条:

  1. 子进程只用写 → 子进程必须关读端

  2. 父进程只用读 → 父进程必须关写端

多子进程必须:

  • 创建后立即关闭不用的端(批量关闭)

  • 不让多余描述符被下一个子进程继承(逆序 / 即时关闭)


记忆口诀(非常好用)

谁读谁关写,谁写谁关读。多进程不关闭,描述符满天飞。写端不关闭,读端死等你。


总结

  1. pipe 通信的关键:必须保证所有写端都关闭,读端才能结束
  2. fork 会复制文件描述符 → 多子进程会产生大量多余写端
  3. 子进程创建后必须批量关闭读端
  4. 父进程必须最终关闭写端
  5. 逆序关闭 / 即时关闭 → 避免子进程继承多余写端

只要遵守这套规则,你的多子进程 + 管道通信永远不会卡死,子进程可以正常回收。

三、进程管理与信号:管道的“边界控制”

管道通信并非孤立存在,它依赖进程生命周期管理信号机制处理异常场景(如管道断连、进程崩溃),确保通信稳定性。

1. 进程状态与ps命令

通过ps命令可查看管道中进程的状态,理解其阻塞 / 运行逻辑:

ps -ef | grep 进程名  # 查看进程基本信息
ps -aux | grep 进程名 # 查看进程状态(STAT列)
  • 状态S:可中断睡眠,如读进程等待管道数据时的状态;
  • 状态Z:僵尸进程,若父进程未调用wait/waitpid回收子进程,会导致资源泄漏,管道通信中需特别注意。

2. 信号:管道的 “异常通知”

Linux 通过信号(Signal) 处理管道通信中的异常,常见关键信号如下:

信号触发场景默认行为
SIGINT按下Ctrl+C,手动中断进程终止进程
SIGPIPE管道读端关闭后,写端继续写入终止进程
SIGCHLD子进程退出,父进程未回收忽略信号
实战:自定义SIGPIPE处理

若子进程意外崩溃导致管道读端关闭,父进程继续写管道会触发SIGPIPE并终止。通过自定义信号处理函数,可避免父进程意外退出:

#include <signal.h>
#include <stdio.h>

void sigpipe_handler(int sig) {
    printf("捕获到SIGPIPE(信号%d):管道读端已关闭,停止写入!\n", sig);
}

int main() {
    signal(SIGPIPE, sigpipe_handler);  // 注册信号处理函数
    // 后续管道操作...
    return 0;
}

四、管道的局限性与优化方向

匿名管道虽基础,但存在明显短板,需根据场景选择优化方案:

1. 匿名管道的核心局限性

  • 通信范围有限:仅支持有亲缘关系的进程(父子、兄弟),无法实现无亲缘关系进程(如两个独立的 Shell 进程)通信;
  • 通信方向单一:仅支持 “单向通信”,若需双向通信,需创建两个管道(一个用于 A→B,一个用于 B→A);
  • 无持久化:管道随进程退出而销毁,无法跨会话(如重启进程后)保留通信状态。

2. 优化与替代方案

(1)命名管道(FIFO):突破 “亲缘关系” 限制

在 Linux 系统中,进程间通信(IPC)是多进程协作的核心能力。传统的匿名管道(pipe)只能用于父子进程等有亲缘关系的进程之间通信,而命名管道(FIFO,First In First Out) 彻底突破了这一限制,它通过在文件系统中创建一个可见的管道文件,让任意两个无亲缘关系的进程都能通过这个文件完成数据交互,是 Linux 中最经典的 IPC 方案之一。


命名管道的核心特性

1. 「文件可见,不占磁盘」的特殊文件

命名管道会在文件系统中生成一个实体文件(可以用ls命令看到),但它本质是内存级文件,不会真正占用磁盘存储空间,所有数据都在内存的管道缓冲区中流转,读写操作不会触发磁盘 IO,性能极高。

2. 单向通信的半双工特性

和匿名管道一致,命名管道是半双工通信,同一时间只能支持单向数据流动:一个进程作为写端写入数据,另一个进程作为读端读取数据。如果需要双向通信,通常会创建两个命名管道,分别用于两个方向的数据传输。

3. 阻塞式读写的同步特性

命名管道的openreadwrite操作默认都是阻塞的:

  • 读端以O_RDONLY打开管道时,会阻塞等待,直到有写端以O_WRONLY打开管道,open才会返回(对应代码第 17 行注释:// 等待写入方打开之后,自己才会打开文件,向后执行
  • 写端以O_WRONLY打开管道时,会阻塞等待,直到有读端以O_RDONLY打开管道
  • 当管道为空时,read操作会阻塞,直到有数据写入;当管道满时,write操作会阻塞,直到有数据被读取
  • 当所有写端关闭后,read会返回 0(EOF),读端可以感知到写端退出(对应代码第 38-40 行的x == 0分支)

4. 多进程共享的通信媒介

命名管道的核心优势是让无亲缘关系的进程看到同一份资源:两个完全独立的进程,只需要打开同一个管道文件,就能通过文件描述符操作同一个内核管道缓冲区,实现数据交互,这是匿名管道无法做到的。


命名管道的核心 API 详解

1. 创建命名管道:mkfifo

#include <sys/stat.h>
#include <sys/types.h>

int mkfifo(const char *pathname, mode_t mode);
  • 参数说明
    • pathname:管道文件的路径(如"./myfifo"),如果文件已存在,mkfifo会返回 - 1 并设置errnoEEXIST
    • mode:文件权限,和open的权限参数一致,最终权限为mode & ~umask,通常设置为0644
  • 返回值:成功返回 0,失败返回 - 1,错误码存于errno
  • 代码对应:图 1 第 9 行int n = mkfifo(FIFO_FILE, MODE);,并通过perror处理创建失败的情况

2. 打开命名管道:open

命名管道本质是文件,因此用标准open函数打开,根据读写需求选择打开方式:

// 读端打开(阻塞等待写端)
int fd = open("myfifo", O_RDONLY);
// 写端打开(阻塞等待读端)
int fd = open("myfifo", O_WRONLY);
// 非阻塞打开(需额外指定O_NONBLOCK)
int fd = open("myfifo", O_RDONLY | O_NONBLOCK);
  • 代码对应:图 1 第 17 行int fd = open(FIFO_FILE, O_RDONLY);,读端打开后会阻塞,直到写端打开管道,才会执行后续代码(打印server open file done

3. 读写管道:read/write

打开管道后,用标准文件 IO 函数read/write完成数据读写,和普通文件读写完全一致:

// 读数据
char buffer[1024] = {0};
ssize_t n = read(fd, buffer, sizeof(buffer));
// 写数据
const char *msg = "hello from client";
write(fd, msg, strlen(msg));
  • 代码对应:图 1 第 31 行int x = read(fd, buffer, sizeof(buffer));,循环读取客户端发送的数据并打印;当x == 0时,说明写端关闭,读端退出循环

4. 关闭与删除管道:close/unlink

  • close(fd):关闭管道文件描述符,释放内核资源
  • unlink(pathname):删除文件系统中的管道文件,避免残留(对应图 2 第 4-9 行代码)

代码实现(服务端 + 客户端)

1. 服务端代码(读端)

#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cstdlib>

// 自定义错误码
#define FIFO_CREATE_ERR 1
#define FIFO_OPEN_ERR 2
#define FIFO_DELETE_ERR 3

// 管道文件路径与权限
const char *FIFO_FILE = "./myfifo";
const mode_t MODE = 0644;

using namespace std;

int main()
{
    // 1. 创建命名管道
    int n = mkfifo(FIFO_FILE, MODE);
    if (n == -1)
    {
        perror("mkfifo");
        exit(FIFO_CREATE_ERR);
    }

    // 2. 以只读方式打开管道(阻塞等待写端)
    int fd = open(FIFO_FILE, O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }
    cout << "server open file done" << endl;

    // 3. 循环读取客户端数据
    while (true)
    {
        char buffer[1024] = {0};
        ssize_t x = read(fd, buffer, sizeof(buffer));
        if (x > 0)
        {
            buffer[x] = '\0'; // 确保字符串以'\0'结尾
            cout << "client say# " << buffer << endl;
        }
        else if (x == 0)
        {
            // 写端关闭,读端退出
            cout << "client quit, me too!\n" << endl;
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }

    // 4. 关闭管道、删除管道文件
    close(fd);
    int m = unlink(FIFO_FILE);
    if (m == -1)
    {
        perror("unlink");
        exit(FIFO_DELETE_ERR);
    }

    return 0;
}

2. 客户端代码(写端)

#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cstdlib>

const char *FIFO_FILE = "./myfifo";

using namespace std;

int main()
{
    // 1. 以只写方式打开管道(阻塞等待读端)
    int fd = open(FIFO_FILE, O_WRONLY);
    if (fd < 0)
    {
        perror("open");
        exit(1);
    }

    // 2. 循环向服务端发送数据
    char buffer[1024] = {0};
    while (true)
    {
        cout << "client say# ";
        cin.getline(buffer, sizeof(buffer));
        if (strcmp(buffer, "quit") == 0)
            break; // 输入quit退出
        write(fd, buffer, strlen(buffer));
    }

    // 3. 关闭管道
    close(fd);
    return 0;
}

命名管道的内核原理

结合图 3 的原理示意图,命名管道的通信本质可以拆解为 3 个核心步骤:

  1. 资源共享:两个独立进程(task_struct)通过打开同一个管道文件,在各自的files_struct文件表中,分别指向内核中同一个管道的读端(struct file r)和写端(struct file w
  2. 内存缓冲:内核为管道分配一个文件缓冲区(内存空间),写端进程通过write将数据写入缓冲区,读端进程通过read从缓冲区读取数据,实现数据流转
  3. 单向同步:管道缓冲区是单向的,只能从写端流向读端,同时内核通过阻塞机制保证读写同步:读端等待写端写入,写端等待读端读取,避免数据丢失

命名管道 vs 匿名管道:核心差异对比
特性匿名管道(pipe)命名管道(FIFO)
通信对象仅支持有亲缘关系的进程(父子、兄弟)支持任意无亲缘关系的进程
文件可见性无实体文件,仅存在于内核有实体文件,文件系统可见
打开方式通过pipe创建,直接获取读写 fd通过mkfifo创建,open打开获取 fd
生命周期随进程退出而销毁需手动unlink删除,否则一直存在
通信方向半双工(单向)半双工(单向)
阻塞特性默认阻塞默认阻塞,支持非阻塞打开

注意事项
  1. 管道文件残留:程序退出后如果未unlink,管道文件会一直存在,下次mkfifo会报错File exists,因此服务端退出时必须执行unlink
  2. 阻塞与非阻塞:默认open是阻塞的,若需非阻塞,需添加O_NONBLOCK标志,但非阻塞模式下open会立即返回,读端打开后若没有写端,read会立即返回 - 1
  3. 多写端场景:一个管道可以被多个写端同时写入,读端会按顺序读取所有写端的数据,但需注意数据粘包问题(需自定义协议分包)
  4. 权限问题mkfifomode参数受umask影响,若需特定权限,可在创建后用chmod修改
  5. 双向通信实现:命名管道是单向的,若需双向通信,需创建两个管道:fifo1(客户端→服务端)、fifo2(服务端→客户端),CS任意一方断开,及通信停止。

结余

命名管道(FIFO)是 Linux 中最经典的 IPC 方案之一,它通过文件系统的管道文件,彻底突破了匿名管道的亲缘限制,让任意进程都能高效通信。其核心优势在于:

  • 接口简单:复用标准文件 IO 函数,学习成本低
  • 性能高效:内存级通信,无磁盘 IO 开销
  • 兼容性强:支持所有 POSIX 系统,跨进程、跨终端通信

无论是服务端 - 客户端架构,还是多进程协作场景,命名管道都是轻量、可靠的通信选择,也是理解 Linux 进程间通信的核心基础。

(2)更复杂的 IPC 机制:应对多样化需求

若需更灵活的通信(如结构化数据、大内存传输),可选择以下 IPC 机制(主要为机内通信机制,同样可用于网络通信):

IPC 机制核心优势适用场景
消息队列支持结构化数据(消息类型 + 数据),非阻塞通信多进程间按类型传递数据
共享内存直接操作物理内存,速度比管道快 1 个数量级大内存数据传输(如视频流)
信号量实现进程同步与互斥,避免数据竞争多进程共享资源(如缓冲区)

五、拓展实战:基于管道的进程池实现

管道的典型进阶应用是管道型进程池—— 通过 “父进程管理管道写端、子进程监听管道读端” 的模式,实现批量任务的分发与执行。以下是完整实现代码与解析。

1. 任务定义头文件(Task.hpp

先定义进程池需执行的任务(如日志刷新、野区更新等),通过函数指针统一任务类型:

#pragma once

#include <iostream>
#include <vector>

// 定义任务类型:无参数、无返回值的函数指针
typedef void (*task_t)();

// 任务1:刷新日志
void task1() {
    std::cout << "[任务1] 刷新日志完成,当前时间:" << __TIME__ << std::endl;
}

// 任务2:刷新野区
void task2() {
    std::cout << "[任务2] 野区刷新完成,生成3只小野怪" << std::endl;
}

// 任务3:检测软件更新
void task3() {
    std::cout << "[任务3] 软件版本检测完成,当前为最新版v1.0.0" << std::endl;
}

// 任务4:更新血量和蓝量
void task4() {
    std::cout << "[任务4] 角色状态更新完成,血量+100,蓝量+50" << std::endl;
}

// 加载所有任务到任务列表
void LoadTask(std::vector<task_t> *tasks) {
    tasks->push_back(task1);
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);
}

2. 管道型进程池核心代码

通过管道实现 “父进程分发任务、子进程执行任务”,核心逻辑包括:进程池初始化、任务分发、资源回收。

管道资源的描述

#include "Task.hpp"
#include <string>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>

const int processnum = 10;
std::vector<task_t> tasks;

// 先描述
class channel
{
public:
    channel(int cmdfd, int slaverid, const std::string &processname)
    :_cmdfd(cmdfd), _slaverid(slaverid), _processname(processname)
    {}
public:
    int _cmdfd;               // 发送任务的文件描述符
    pid_t _slaverid;          // 子进程的PID
    std::string _processname; // 子进程的名字 -- 方便我们打印日志
    // int _cmdcnt;
};

void Menu()
{
    std::cout << "################################################" << std::endl;
    std::cout << "# 1. 刷新日志             2. 刷新出来野怪        #" << std::endl;
    std::cout << "# 3. 检测软件是否更新      4. 更新用的血量和蓝量  #" << std::endl;
    std::cout << "#                         0. 退出               #" << std::endl;
    std::cout << "#################################################" << std::endl;
}

int main()
{
    LoadTask(&tasks);

    srand(time(nullptr)^getpid()^1023); // 种一个随机数种子
    // 在组织
    std::vector<channel> channels;
    // 1. 初始化 --- bug?? -- 找一下这个问题在哪里?然后提出一些解决方案!
    InitProcessPool(&channels);
    // Debug(channels);

    // 2. 开始控制子进程
    ctrlSlaver(channels);

    // 3. 清理收尾
    QuitProcess(channels);
    return 0;
}

初始化进程池:创建子进程+管道,建立通信通道

核心控制部分:父进程写子进程读

进程池资源释放时依次遍历关闭管道并等待子进程

void QuitProcess(const std::vector<channel> &channels)
{
    for(const auto &c : channels){
        close(c._cmdfd);
        waitpid(c._slaverid, nullptr, 0);
    }
}

完整代码

#include "Task.hpp"
#include <string>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>

const int processnum = 10;
std::vector<task_t> tasks;

// 先描述
class channel
{
public:
    channel(int cmdfd, int slaverid, const std::string &processname)
    :_cmdfd(cmdfd), _slaverid(slaverid), _processname(processname)
    {}
public:
    int _cmdfd;               // 发送任务的文件描述符
    pid_t _slaverid;          // 子进程的PID
    std::string _processname; // 子进程的名字 -- 方便我们打印日志
    // int _cmdcnt;
};

void slaver()
{
    // read(0)
    while(true)
    {
        int cmdcode = 0;
        int n = read(0, &cmdcode, sizeof(int)); // 如果父进程不给子进程发送数据呢??阻塞等待!
        if(n == sizeof(int))
        {
            //执行cmdcode对应的任务列表
            std::cout <<"slaver say@ get a command: "<< getpid() << " : cmdcode: " <<  cmdcode << std::endl;
            if(cmdcode >= 0 && cmdcode < tasks.size()) tasks[cmdcode]();
        }
        if(n == 0) break;
    }
}
// 输入:const &
// 输出:*
// 输入输出:&
void InitProcessPool(std::vector<channel> *channels)
{
    // version 2: 确保每一个子进程都只有一个写端
    std::vector<int> oldfds;
    for(int i = 0; i < processnum; i++)
    {
        int pipefd[2]; // 临时空间
        int n = pipe(pipefd);
        assert(!n); // 演示就可以
        (void)n;

        pid_t id = fork();
        if(id == 0) // child
        {
            std::cout << "child: " << getpid() << " close history fd: ";
            for(auto fd : oldfds) {
                std::cout << fd << " ";
                close(fd);
            }
            std::cout << "\n";

            close(pipefd[1]);
            dup2(pipefd[0], 0);
            close(pipefd[0]);
            slaver();
            std::cout << "process : " << getpid() << " quit" << std::endl;
            // slaver(pipefd[0]);
            exit(0);
        }
        // father
        close(pipefd[0]);

        // 添加channel字段了
        std::string name = "process-" + std::to_string(i);
        channels->push_back(channel(pipefd[1], id, name));
        oldfds.push_back(pipefd[1]);

        sleep(1);
    }
}

void Debug(const std::vector<channel> &channels)
{
    // test
    for(const auto &c :channels)
    {
        std::cout << c._cmdfd << " " << c._slaverid << " " << c._processname << std::endl;
    }
}

void Menu()
{
    std::cout << "################################################" << std::endl;
    std::cout << "# 1. 刷新日志             2. 刷新出来野怪        #" << std::endl;
    std::cout << "# 3. 检测软件是否更新      4. 更新用的血量和蓝量  #" << std::endl;
    std::cout << "#                         0. 退出               #" << std::endl;
    std::cout << "#################################################" << std::endl;
}

void ctrlSlaver(const std::vector<channel> &channels)
{
    int which = 0;
    // int cnt = 5;
    while(true)
    {
        int select = 0;
        Menu();
        std::cout << "Please Enter@ ";
        std::cin >> select;

        if(select <= 0 || select >= 5) break;
        // select > 0&& select < 5
        // 1. 选择任务
        // int cmdcode = rand()%tasks.size();
        int cmdcode = select - 1;

        // 2. 选择进程
        // int processpos = rand()%channels.size();

        std::cout << "father say: " << " cmdcode: " <<
            cmdcode << " already sendto " << channels[which]._slaverid << " process name: " 
                << channels[which]._processname << std::endl;
        // 3. 发送任务
        write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));

        which++;
        which %= channels.size();

        // cnt--;
        // sleep(1);
    }
}
    
void QuitProcess(const std::vector<channel> &channels)
{
    for(const auto &c : channels){
        close(c._cmdfd);
        waitpid(c._slaverid, nullptr, 0);
    }
}
int main()
{
    LoadTask(&tasks);

    srand(time(nullptr)^getpid()^1023); // 种一个随机数种子
    // 在组织
    std::vector<channel> channels;
    // 1. 初始化 --- bug?? -- 找一下这个问题在哪里?然后提出一些解决方案!
    InitProcessPool(&channels);
    // Debug(channels);

    // 2. 开始控制子进程
    ctrlSlaver(channels);

    // 3. 清理收尾
    QuitProcess(channels);
    return 0;
}

总结:管道是Linux IPC的“入门钥匙”


从 struct file 到 task_struct 的底层关联,到 pipe() 系统调用的上层接口,再到进程管理和信号的边界控制——管道通信串联起了Linux内核数据结构、系统调用和进程协作的核心逻辑。
 
它或许不是最强大的IPC机制,但绝对是理解“Linux进程间如何协作”的最佳入门工具。掌握了管道,再去学习命名管道、消息队列、共享内存等IPC机制,会更加水到渠成。
 
希望本文能帮大家彻底吃透Linux管道通信,下次面对多进程协作场景时,能精准选择最适合的方案!

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值