进程间通信(一)管道通信

管道是进程间通信(IPC)常见的方式,管道一般分为:匿名管道有名管道

匿名管道

匿名管道 的原理主要是通过内核的环形缓冲区 实现单向通信,仅用于父子进程或兄弟进程等有亲缘关系的进程间

Linux 中用 pipe 创建一个匿名管道

环形缓冲区

什么是环形缓冲区?简单来说用一个固定大小的连续内存空间,通过一个读指针(read_pos)和一个写指针(write_pos)来实现内容的读写

lua 复制代码
                写数据
                   ↓
        +-------------------------+
        |   已读 |   数据    | 空   |
        +-------------------------+
        ↑                        ↑
     read_pos               write_pos
                   ↑
                读数据

当 read_pos 和 write_pos 相遇:

  • 如果是读端达到 write_pos => 没有数据,阻塞读
  • 如果是写端达到 read_pos 前一位 => 没有空间,阻塞写

根据环形缓冲区的特点,因此匿名管道的信息采用先进先出(FIFO)方式传递数据,所谓的匿名是没有文件系统中的名称标识

通过 pipe 创建一个管道

arduino 复制代码
#include <unistd.h>
int pipe(int pipefd[2]);

pipe 会创建两个管道标识符

  • pipefd[0]:从管道进行读取数据。
  • pipefd[1]:向管道进行写入数据。

终端中的

我们经常在终端中使用 |,这就是管道的符号,通过匿名管道的方式来实现父子进程的通信,例如下面的命令

通过 配合 grep 来实现搜索数据的过滤,下面是一段简单的代码来实现上面的功能

scss 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
​
int main() {
    int pipefd[2];  // pipefd[0] = 读取端, pipefd[1] = 写入端
    pid_t pid1, pid2;
​
    // 1. 创建匿名管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(1);
    }
​
    // 2. 创建第一个子进程(执行 `ls`,写入管道)
    pid1 = fork();
    if (pid1 == -1) {
        perror("fork");
        exit(1);
    }
​
    if (pid1 == 0) {  // 子进程1(`ls`)
        close(pipefd[0]);  // 关闭读取端(不需要读)
        dup2(pipefd[1], STDOUT_FILENO);  // 将标准输出重定向到管道写入端
        close(pipefd[1]);  // 关闭原始写入端(已被 dup2 替换)
​
        execlp("ls", "ls", NULL);  // 执行 `ls`
        perror("execlp ls");  // 如果执行失败
        exit(1);
    }
​
    // 3. 创建第二个子进程(执行 `grep Hello`,从管道读取)
    pid2 = fork();
    if (pid2 == -1) {
        perror("fork");
        exit(1);
    }
​
    if (pid2 == 0) {  // 子进程2(`grep Hello`)
        close(pipefd[1]);  // 关闭写入端(不需要写)
        dup2(pipefd[0], STDIN_FILENO);  // 将标准输入重定向到管道读取端
        close(pipefd[0]);  // 关闭原始读取端(已被 dup2 替换)
​
        execlp("grep", "grep", "Hello", NULL);  // 执行 `grep Hello`
        perror("execlp grep");  // 如果执行失败
        exit(1);
    }
​
    // 4. 父进程关闭管道(子进程已经接管)
    close(pipefd[0]);
    close(pipefd[1]);
​
    // 5. 等待两个子进程结束
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);
​
    return 0;
}

shell 在遇到 | 时,会通过 fork 创建出一个子进程,来进行进一步的操作,这里其中 STDOUT_FILENO 是标准输入的意思,意味着 execlp("ls", "ls", NULL) 的结果都会写入管道中

三个标准文件描述符

  • STDIN_FILENO (0): 标准输入(键盘输入)
  • STDOUT_FILENO (1): 标准输出(屏幕输出)
  • STDERR_FILENO (2): 标准错误输出(屏幕错误输出)

在 pid2 也就是 grep 子进程会通过读取管道中的信息,来过滤相关信息,然后输出到 shell

有名管道

有名管道允许无亲缘关系的进程通信,通过在文件系统中创建一个特殊类型的文件来实现

Linux 中用 mkfifo 命令进行系统调用

bash 复制代码
mkfifo myfifo

其中开头的 p 表示这是一个管道文件,也称 FIFO 文件,对于文件来说,我们都可以使用openwriteread等函数操作

例如这里用有名管道实现两个不相关进程简单的聊天

c 复制代码
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
​
#define FIFO_FILE "/tmp/learning/icp/unname/chat_fifo"
​
int main() {
    int fd;
    char buf[100];
​
    // 创建有名管道(如果不存在)
    mkfifo(FIFO_FILE, 0666);
    
    // 以只写方式打开管道(会阻塞直到读取端打开)
    fd = open(FIFO_FILE, O_WRONLY);
    printf("Writer ready. Enter messages (Ctrl+C to exit):\n");
​
    while (1) {
        printf("> ");
        fflush(stdout);  // 确保提示符显示
        if (fgets(buf, sizeof(buf), stdin) == NULL) {
            break;  // 读取失败(如Ctrl+D)
        }
        write(fd, buf, strlen(buf) + 1);  // +1 包含字符串结束符
    }
​
    close(fd);
    return 0;
}
arduino 复制代码
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
​
#define FIFO_FILE "/tmp/learning/icp/unname/chat_fifo"
#define BUF_SIZE 100
​
int main() {
    int fd;
    char buf[BUF_SIZE];
​
    // 以只读方式打开管道(会阻塞直到写入端打开)
    fd = open(FIFO_FILE, O_RDONLY);
    printf("Reader ready. Waiting for messages...\n");
​
    while (1) {
        ssize_t bytes_read = read(fd, buf, BUF_SIZE - 1);  // 保留1字节给结束符
        if (bytes_read <= 0) {
            break;  // 写入端关闭或出错
        }
        buf[bytes_read] = '\0';  // 确保字符串正确终止
        printf("Received: %s", buf);
    }
​
    close(fd);
    return 0;
}

这里我们写了两个简单的程序,两个完全不同的进程进行简单的聊天,通过 mkfifo(FIFO_FILE, 0666); 会创建一个管道文件

这里去运行这两个程序

可以看到 reader 进程成功接收到了 writer 传递过来的消息

也在当前目录下创建了一个 chat_fifo 的管道文件,这里再通过观察 chat_fifo 文件

有名管道特性

  • 阻塞行为

    • 读空管道会阻塞直到有数据
    • 写满管道会阻塞直到有空间
  • 数据也是以先进先出(FIFO) 传递

  • 持久性:有名管道在文件系统中存在,直到被显式删除(unlink)

这里还有一点:无论是无名管道还是命名管道,数据读取后都会消失

管道的实际引用

上面 shell 通过 | 创建匿名管道进行通信就是最简单的实践,管道还广泛的引用在其他应用场景中

容器日志重定向

docker 使用管道技术重定向输出到日志分析系统

perl 复制代码
# 容器日志收集
docker logs -f container_id | grep "ERROR" > errors.log
  • 可以分离容器和日志分析器的生命周期
  • 支持更复杂的处理流程

Nginx中管道技术的高效请求/响应流转发

bash 复制代码
location / {
  proxy_pass http://backend;
  proxy_set_header X-Real-IP $remote_addr;
}

上面的配置是将所有请求 / 转发到 [http://backend](http://backend) 服务器组

复制代码
客户端请求 → 接收缓冲区 → 解析 → 各阶段处理器 → 上游服务器 → 响应缓冲 → 客户端
  • 管道将 HTTP请求响应数据 拆分为多个数据块(chunks),通过内存管道在不同处理模块间流动
  • 避免了完整数据缓冲,支持边接收边转发
  • 降低了内存占用,提升了性能

大数据处理

Apache Spark :RDD 转换操作底层使用管道式数据处理

流式处理框架

Fluentd日志收集 :管道式日志处理流程,其核心在于通过插件化的组件将日志的输入、过滤、缓冲、输出串联为流水线式处理链路

管道技术的核心优势在于它的 流式处理能力低资源消耗 ,这使得它在需要高效数据流转的场景中仍然是不可替代的基础技术。现代实现通常会结合缓冲、非阻塞IO等优化手段来提升性能

相关推荐
罗念笙17 小时前
说下你常用的Linux命令?
linux·操作系统
想拿高薪的韭菜1 天前
操作系统高频(六)linux内核
linux·操作系统
LUCIAZZZ1 天前
计算机网络-TCP的重传机制
java·网络·网络协议·tcp/ip·计算机网络·操作系统·springboot
tinker2 天前
Ubuntu 初始安装记录
操作系统
Zevalin爱灰灰2 天前
面试可能会遇到的问题&回答(嵌入式软件开发部分)
stm32·单片机·面试·操作系统·嵌入式·ucos
热爱前端的小张3 天前
第三章 内存管理(下)
操作系统
易保山5 天前
MIT6.S081 - Lab6 Copy-on-Write(写时复制)
linux·操作系统·c
司六米希6 天前
【操作系统】查内存泄漏方法
操作系统
GoGeekBaird7 天前
69天探索操作系统-第58天:操作系统中的高级负载均衡
后端·操作系统