首页 > 其他分享 >管道和FIFO

管道和FIFO

时间:2024-10-14 21:01:56浏览次数:8  
标签:读取 写入 描述符 FIFO 管道 进程

管道

概述

管道为一个常见需求提供了一个优雅的解决方案:给定两个运行不同程序(命令)的进程,在shell中如何让一个进程的输出作为另一个进程的输入呢?管道可以用来在相关进程之间传递数据。

1

管道其实就和真实的管道类似是,它可以进行数据的传递,比如说水管,它就可以把水流从一端送到另一端。管道也是一样的,它可以把数据一字节流的形式从一端送到另一端。这种方式可以用作进程间通信

简而言之,管道允许数据从一个进程流向另一个进程

特征

字节流

管道是一个字节流,这意味着在管道中处理数据时,不存在消息或消息边界的概念。简单来说,管道仅仅是一个字节的序列,没有其他特殊的结构

  • 可以从管道中读取任意大小的数据块
  • 管道传递的数据是顺序的,即读取的顺序和写入的顺序保持一致

单向

在管道中数据的传递方向是单向的。管道的一段用于写入,另一端则用于读取。

写入不超过PIPE_BUF字节的操作是原子的

对于超过 PIPE_BUF 字节 的写入数据来说,内核可能会将其分割成较小的片段依次写入,如果只有一个写入进程那没有关系,如果是多个写入进程,可能会发生 数据交叉 的情况,除此之外,write调用会阻塞直到所用数据被写入管道

write 调用阻塞 是指当一个进程试图向管道、文件或其他 I/O 设备写入数据时,如果该操作无法立即完成,进程会被暂停(阻塞),直到能够继续写入

有限容量

管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的。一旦管道被填满之后,后续向该管道的写入操作就会被阻塞直到读者从管道中移除了一些数据为止。

管道的创建和使用

// 由pipe系统调用创建管道
int pipe(int filedes[2]);

// 创建示例
int pipe_fd[2];

if (pipe(pipe_fd) == -1) { // 创建管道
  /* 错误信息输出 */
}

成功的pipe()调用会在数组filedes中返回两个打开的文件描述符:一个表示管道的读取端(filedes[0]),另一个表示管道的写入端(filedes[1])。

与所有文件描述符一样,可以使用read()和write()系统调用来在管道上执行I/O。一旦向管道的写入端写入数据之后立即就能从管道的读取端读取数据。管道上的 read()调用会读取的数据量为所请求的字节数与管道中当前存在的字节数两者之间较小的那个(但当管道为空时阻塞)。

管道一般用于多个进程,单个进程用到的不多。如下所示,左边是父进程创建完管道,然后调用fork,子进程会 复制 管道的文件描述符。右边是一种规范吧,及时 关掉不使用的管道文件描述符,这样就能够实现父进程的数据流向子进程了

2

为什么要 关掉不使用的管道文件描述符 呢?

  • 读取进程 如果没有关闭 管道的写入端,那么读取将永远不会结束,即 read()会一直阻塞等待数据,这是因为,一直有一个管道的写入描述符存在,即读取进程没有关闭的那个

    从管道中读取数据的进程会关闭其持有的管道的写入描述符,这样当其他进程完成输出并关闭其写入描述符之后,读者就能够看到文件结束(在读完管道中的数据之后)。

  • 写入进程 如果没有关闭 管道的读取端,那么写入将永远不会结束,管道写满了,write()就会阻塞,这是应为,一直有一个管道的读取描述符存在,即读取进程没有关闭的那个

关闭未使用文件描述符的最后一个原因是只有当所有进程中所有引用一个管道的文件描述符被关闭之后才会销毁该管道以及释放该管道占用的资源以供其他进程复用。此时,管道中所有未读取的数据都会丢失

管道用作进程同步

  • 父进程在创建子进程之前构建了一个管道
  • 每个子进程会继承管道的写入端的文件描述符并在完成动作之后关闭这些描述符
  • 当所有子进程都关闭了管道的写入端的文件描述符之后,父进程在管道上的read()就会结束并返回文件结束(0),父进程就能做其他事情了

同步的关键在于,及时关掉不使用的管道文件描述符,即一个进程不同时持有管道的写入端和读取端

管道连接过滤器

管道连接过滤器 就是指通过管道将多个数据处理程序(过滤器)连接在一起,使得数据流从一个过滤器传递到下一个过滤器,经过多个处理步骤,最终得到所需的输出

简单来说就是文件描述符的绑定,一般来说就是 标准输出和输入(STDOUT_FILENO 和 STDIN_FILENO) ,我的读入端是你的写入端,这样就能形成数据的流通了

// 使用示例
int pfd[2];

pipe(pfd); // 创建管道

if (pfd[0] != STDIN_FILENO) {   // 如果相同那就没有多余描述符,也就不用关闭了
  dup2(pfd[0], STDIN_FILENO);   // 将标准输入绑定为管道的读取端
  close(pfd[0]);   // 关闭多余的文件描述符
}

if (pfd[1] != STDOUT_FILENO) {
  dup2(pfd[1], STDOUT_FILENO);   // 将标准输出绑定为管道的读取端
  close(pfd[1]);   // 关闭多余的文件描述符
}

通过管道与shell命令进行通信

popen() 和 pclose() 函数用于通过管道与 shell 命令进行通信,简化了执行 shell 命令并读取其输出或向其发送输入的任务。

popen()

  • 功能
    • 创建一个管道,并启动一个子进程来执行指定的 shell 命令
    • 提供一种简单的方式来读取命令的输出或向命令发送输入

popen()函数创建了一个管道,然后创建了一个子进程来执行shell,而 shell又创建了一个子进程来执行 command 字符串。

4

  • 语法
  FILE *popen(const char *command, const char *mode);
  • 参数

    • command:要执行的 shell 命令字符串
    • mode:打开模式:
      • "r":读取模式,从命令的标准输出读取数据
      • "w":写入模式,向命令的标准输入发送数据
  • 返回值

    • 成功时返回管道的读取或写入文件流,与mode相关
    • 失败时返回 NULL,并设置 errno 以指示错误原因

pclose()

  • 功能

    • 关闭通过 popen() 打开的管道,并等待子进程的终止
  • 语法

  int pclose(FILE *stream);
  • 参数

    • stream:由 popen() 返回的指向 FILE 对象的指针
  • 返回值

    • 返回子进程的终止状态。如果成功,返回子进程的退出状态;如果失败,返回 -1
// 示例
#include <stdio.h>

int main() {
    FILE *fp;
    char buffer[128];

    // 使用 popen 执行 'ls' 命令,读取输出
    fp = popen("ls", "r");
    if (fp == NULL) {
        perror("popen failed");
        return 1;
    }

    // 读取并打印命令的输出
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }

    // 关闭管道
    pclose(fp);
    return 0;
}

注意事项

  • 管道是单向的:

    • 在 mode 为 "r" 时,命令的标准输出连接到管道
    • 在 mode 为 "w" 时,命令的标准输入连接到管道
  • 信号处理:

    popen() 创建的子进程与调用进程在同一进程组中,因此来自终端的信号会同时发送到两个进程

  • 资源管理:

    使用 popen() 后,必须用 pclose() 关闭管道,避免资源泄露,不应使用 fclose()

  • 错误处理:

    在调用 popen() 和 pclose() 时应检查返回值,以确保操作成功

管道和stdio缓冲区

由于 popen() 返回的文件流指针没有引用终端,stdio 库对该文件流使用块缓冲。

popen()函数的文件流指针指向的是管道,而不是终端,所以称为 没有引用终端

当使用 mode 为 w 调用 popen() 时,输出默认只在 缓冲区满或调用 pclose() 后才发送到子进程。如果需要子进程 立即接收数据,则应定期调用 fflush() 或使用 setbuf(fp, NULL) 禁用缓冲

以 mode 为 r 调用 popen() 时,子进程的输出只有在 填满缓冲区或调用 pclose() 后才对调用进程可用。若无法修改子进程代码以调用 setbuf() 或 fflush(),可以使用伪终端替代管道,伪终端让 stdio 库逐行输出数据

FIFO (命名管道)

从语义上来讲,FIFO 与管道类似,它们两者之间最大的差别在于FIFO在文件系统中拥有一个名称,并且其打开方式与打开一个普通文件是一样的。这样就能够将FIFO用于非相关进程之间的通信(如客户端和服务器)。

FIFO的创建

// 函数原型
int mkfifo(const char *pathname, mode_t mode);

// 使用示例
umask(0);   // 将掩码设置为0,这样我们赋予fifo的权限就不会被影响
if (mkfifo(FIFO_NAME, 0320) == -1) {
  /* 错误信息 */
}

int fifo_r_fd = open(FIFO_NAME, O_RDONLY);   // 读进程
...
int fifo_w_fd = open(FIFO_NAME, O_WRONLY);   // 写进程

一般来讲,使用FIFO时唯一明智的做法是在两端分别设置一个读取进程和一个写入进程。,打开一个FIFO会同步读取进程和写入进程。

简单来说,当你用open打开fifo时,它会阻塞直到另一个进程用open打开fifo,并且两者打开的方式是 只读或只写

使用FIFO实现一个客户端/服务器应用程序

5

服务器无法使用单个FIFO响应多个客户端的请求,这样多个客户端读取数据时会发生数据竞争,所以每个客户端单独创建一个FIFO用来接收响应(假设服务器为所有客户所知)

这里服务器使用单个FIFO接收所有客户端请求,因为管道是字节流,没有消息边界,所以得 约定某种规则来分隔消息,以下是3种方法

5

  • 分隔字符 就是用某个特殊字符来分隔消息,这要求 读取消息的进程在从FIFO中扫描数据时必须要逐个字节地分析直到找到分隔符为止

  • 具有长度字段的头 指明了消息剩余部分的长度,能够高效地读取任意大小的消息,但一旦不合规则(如错误的length字段)的消息被写入到管道中之后问题就出来了

  • 固定长度的消息 让服务器总是读取这个大小固定的消息,但 有可能会浪费通道容量,或者说如果其中一个客户端意外地或故意发送了一条长度不对的消息,那么所有后续的消息都会出现步调不一致的情况

打开FIFO进程的死锁

6

非阻塞IO

FIFO和管道的open语义

这是打开FIFO文件的一些情况,如果读进程和写进程都没有打开,对应的open会阻塞
7

可以使用标志 O_NONBLOCK 以非阻塞的方式打开,这样open会立即返回

// 可以直接在open中指明
int fifo_r_fd = open(FIFO_NAME, O_RDONLY | O_NONBLOCK);

// 或者使用fcntl
int flags;

flags = fcntl(fd, F_GETFL);
flags |= O_NONBLOCK;   // 打开非阻塞标志
flags ~= O_NONBLOCK;   // 禁用非阻塞标志
fcntl(fd, F_SETFL, flags);

FIFO和管道的read语义

8

FIFO和管道的write语义

9

标签:读取,写入,描述符,FIFO,管道,进程
From: https://www.cnblogs.com/dylaris/p/18464746

相关文章

  • 基于FIFO使用UART实现FPGA板与PC通信
    基于FIFO使用UART实现FPGA板与PC通信1.UART简介UART(通用异步收发传输器)是一种常用的串行通信协议,广泛用于FPGA与外部设备(如PC、传感器等)之间的通信。UART通信的核心是将并行数据转换为串行数据传输,然后在接收端再将串行数据恢复为并行数据。UART协议特点:异步通信:无需时钟......
  • AI代理与AI管道:构建LLM应用的实用指南
    这里我们用CrewAI来创建应用,展示一下如何为你的LLM(大语言模型)应用选择合适的架构。你可以把AI代理想象成一个能够使用外部工具的LLM。它会在一个循环中运行,每次迭代时决定要做什么、用什么工具来解决问题。通过这种方式,代理能处理比传统LLM应用更复杂的问题。(我在下面的文章......
  • 液体泄露检测系统 监控识别管道液体泄漏系统
    液体泄露检测系统通过在关键区域安装监控摄像头,液体泄露检测系统对管道的液体泄露情况进行全天候不间断实时监测。液体泄露检测系统利用Ai视觉智能分析技术,实时感知监控画面中管道液体泄露事件。液体泄露检测系统检测到画面中管道设备液体泄露现象时,将自动发出警报提示相关人员及......
  • 异步FIFO的空和满是准确的吗
    关注公众号FPGA开源工坊获取更多FPGA相关内容交流群:838607138异步FIFO的空和满是准确的吗在我们关于两级同步器电路那篇推文里面提到了异步FIFO中格雷码进行同步的时候是允许漏采的,那么这个漏采会出问题吗,这篇推文我们就来讨论一下这个问题。首先来说明一下异步FIFO的空满信号......
  • .net core elsa工作流程框架源码学习之Pipeline管道的理解
    elsa这个框架运用管道来实现切面编程,切面编程的意义我的理解是在于:把业务逻辑和其他与业务不相关的逻辑进行解耦,或者把通用的逻辑:异常处理,日志处理等在不侵入业务逻辑的情况下,服务与这些业务。接下来,详细看看elsa框架的管道是怎么实现的。主要依靠,下面这个委托方法,这个方法返回一......
  • 光学式管道液体传感器
    原理及优势/PrincipleandAdvantages1.光学式管道液体传感器的原理/PrincipleofOpticalPipelineLiquidSensor传感器利用光电效应和光的棱镜的折射效应进行检测管道内是否有液体的存在当管道内没有液体,传感器的红外发光源发出的光照射到棱镜上,会进行反射光电二极管......
  • redis 管道 批量处理 transmit multiple commands to the Redis server in one tran
    Redispipelining|Docshttps://redis.io/docs/latest/develop/use/pipelining/RedispipeliningHowtooptimizeround-triptimesbybatchingRediscommandsRedispipeliningisatechniqueforimprovingperformancebyissuingmultiplecommandsatoncewithou......
  • 持续测试:确保 DevOps 管道的质量
    持续测试是现代软件开发中的关键实践,尤其是在DevOps框架中。它涉及在整个软件交付管道中自动执行测试,以确保对代码库所做的每项更改都得到彻底验证。通过将测试集成到开发过程的每个阶段,持续测试旨在尽早发现并解决缺陷,从而显着降低生产中失败的风险。什么是持续测试?持续测试是......
  • Windows管道(Pipes)(一)
      Windows操作系统提供了多种机制用于实现应用程序间的通信及数据的共享,比如剪贴板(Clipboard)、组件对象模型(COM)、数据复制(DataCopy)、动态数据交换(DDE)、文件映射(FileMapping)、邮件槽(Mailslots)、管道(Pipes)、远程过程调用(RPC)、Windows套接字(WindowsSockets)等......
  • 进程-管道
    管道定义    什么是管道                管道是Unix中最古老的进程间通信的形式。        我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”                我们通常把是把一个进程的输出连接或“......