【Linux系统编程】(三十二)命名管道 FIFO 精讲:突破亲缘限制,实现任意进程间的 IPC 通信


目录

前言

一、命名管道的诞生:解决匿名管道的核心痛点

[1.1 匿名管道的致命短板](#1.1 匿名管道的致命短板)

[1.2 命名管道的核心设计思路](#1.2 命名管道的核心设计思路)

[1.3 命名管道与匿名管道的核心关联](#1.3 命名管道与匿名管道的核心关联)

[二、命名管道的核心认知:什么是 FIFO 文件?](#二、命名管道的核心认知:什么是 FIFO 文件?)

[2.1 FIFO 文件的本质](#2.1 FIFO 文件的本质)

[2.2 FIFO 文件的标识](#2.2 FIFO 文件的标识)

[2.3 命名管道的通信模型](#2.3 命名管道的通信模型)

[三、命名管道的创建:mkfifo () 函数与命令行](#三、命名管道的创建:mkfifo () 函数与命令行)

[3.1 命令行创建命名管道](#3.1 命令行创建命名管道)

[3.2 程序中创建命名管道:mkfifo () 函数](#3.2 程序中创建命名管道:mkfifo () 函数)

[3.2.1 mkfifo () 函数接口](#3.2.1 mkfifo () 函数接口)

[3.2.2 核心注意点](#3.2.2 核心注意点)

[3.2.3 简单创建示例](#3.2.3 简单创建示例)

四、命名管道的打开规则:最关键的核心知识点

[4.1 open () 函数的核心参数](#4.1 open () 函数的核心参数)

[4.2 以读方式打开命名管道(O_RDONLY)](#4.2 以读方式打开命名管道(O_RDONLY))

[4.2.1 阻塞模式(默认,仅 O_RDONLY)](#4.2.1 阻塞模式(默认,仅 O_RDONLY))

[4.2.2 非阻塞模式(O_RDONLY | O_NONBLOCK)](#4.2.2 非阻塞模式(O_RDONLY | O_NONBLOCK))

[4.3 以写方式打开命名管道(O_WRONLY)](#4.3 以写方式打开命名管道(O_WRONLY))

[4.3.1 阻塞模式(默认,仅 O_WRONLY)](#4.3.1 阻塞模式(默认,仅 O_WRONLY))

[4.3.2 非阻塞模式(O_WRONLY | O_NONBLOCK)](#4.3.2 非阻塞模式(O_WRONLY | O_NONBLOCK))

[4.4 命名管道打开规则总结](#4.4 命名管道打开规则总结)

[4.5 打开规则的本质](#4.5 打开规则的本质)

五、命名管道的读写规则:完全复用匿名管道

[5.1 无数据可读时的读操作规则](#5.1 无数据可读时的读操作规则)

[5.2 管道缓冲区写满时的写操作规则](#5.2 管道缓冲区写满时的写操作规则)

[5.3 所有写端关闭后的读操作规则](#5.3 所有写端关闭后的读操作规则)

[5.4 所有读端关闭后的写操作规则](#5.4 所有读端关闭后的写操作规则)

[5.5 管道写入的原子性规则](#5.5 管道写入的原子性规则)

[5.6 命名管道读写规则的核心注意点](#5.6 命名管道读写规则的核心注意点)

[六、命名管道的实战开发:从基础案例到 Server-Client 通信](#六、命名管道的实战开发:从基础案例到 Server-Client 通信)

[6.1 实战案例 1:命名管道基础读写](#6.1 实战案例 1:命名管道基础读写)

[6.1.1 写端程序:fifo_write.c](#6.1.1 写端程序:fifo_write.c)

[6.1.2 读端程序:fifo_read.c](#6.1.2 读端程序:fifo_read.c)

[6.1.3 编译与运行](#6.1.3 编译与运行)

[6.1.4 运行结果分析](#6.1.4 运行结果分析)

[6.2 实战案例 2:基于命名管道实现文件拷贝](#6.2 实战案例 2:基于命名管道实现文件拷贝)

[6.2.1 发送端程序:fifo_copy_send.c](#6.2.1 发送端程序:fifo_copy_send.c)

[6.2.2 接收端程序:fifo_copy_recv.c](#6.2.2 接收端程序:fifo_copy_recv.c)

[6.2.3 测试步骤](#6.2.3 测试步骤)

[6.2.4 案例核心亮点](#6.2.4 案例核心亮点)

[6.3 实战案例 3:基于命名管道实现 Server-Client 通信](#6.3 实战案例 3:基于命名管道实现 Server-Client 通信)

[6.3.1 服务端程序:serverPipe.c](#6.3.1 服务端程序:serverPipe.c)

[6.3.2 客户端程序:clientPipe.c](#6.3.2 客户端程序:clientPipe.c)

[6.3.3 编译与运行](#6.3.3 编译与运行)

[6.3.4 案例核心亮点](#6.3.4 案例核心亮点)

七、命名管道的核心特点与使用场景

[7.1 命名管道的核心特点](#7.1 命名管道的核心特点)

[7.2 命名管道的典型使用场景](#7.2 命名管道的典型使用场景)

[7.3 命名管道的局限性](#7.3 命名管道的局限性)

[7.4 命名管道与其他 IPC 方式的对比](#7.4 命名管道与其他 IPC 方式的对比)

八、命名管道开发的避坑指南

[8.1 坑 1:未处理 SIGPIPE 信号,导致进程意外退出](#8.1 坑 1:未处理 SIGPIPE 信号,导致进程意外退出)

[8.2 坑 2:非阻塞模式下,错误判断不严谨](#8.2 坑 2:非阻塞模式下,错误判断不严谨)

[8.3 坑 3:多进程写管道时,数据混乱](#8.3 坑 3:多进程写管道时,数据混乱)

[8.4 坑 4:双向通信时,只创建一个管道,导致死锁](#8.4 坑 4:双向通信时,只创建一个管道,导致死锁)

[8.5 坑 5:FIFO 文件未删除,导致程序重启时 mkfifo 报错](#8.5 坑 5:FIFO 文件未删除,导致程序重启时 mkfifo 报错)

[8.6 坑 6:阻塞模式下,进程卡死,无法退出](#8.6 坑 6:阻塞模式下,进程卡死,无法退出)

总结


前言

在 Linux 进程间通信的学习中,匿名管道作为入门级的 IPC 方式,让我们理解了 "内核缓冲区 + 一切皆文件" 的设计思想,但它有一个致命的限制 ------只能用于具有亲缘关系的进程间通信 。而命名管道(FIFO)作为匿名管道的 "升级版",完美突破了这一限制,让任意无亲缘关系的进程也能实现高效的管道通信。

命名管道也叫FIFO(First In First Out),是一种特殊的管道文件,它拥有磁盘级的文件名,通过文件系统实现进程间的关联,操作接口与匿名管道完全兼容,却能覆盖更广泛的通信场景。本文将从命名管道的核心概念出发,一步步拆解其原理、创建方式、打开规则、实战应用,结合硬核代码和通俗讲解,让你彻底吃透命名管道,掌握跨进程通信的关键技能。下面就让我们正式开始吧!


一、命名管道的诞生:解决匿名管道的核心痛点

在学习命名管道之前,我们先回顾一下匿名管道的核心局限性,这也是命名管道诞生的根本原因。

1.1 匿名管道的致命短板

匿名管道基于文件描述符继承 实现通信,只有通过fork()创建的子进程(或兄弟进程)才能继承父进程的管道描述符,因此只能用于有共同祖先的亲缘进程(父 / 子、兄 / 弟)。

但在实际开发中,我们更多的需求是让完全独立的无亲缘进程通信:比如一个服务进程和一个客户端进程、两个独立启动的应用程序、不同用户的进程之间,此时匿名管道就完全无法满足需求。

1.2 命名管道的核心设计思路

为了解决匿名管道的亲缘限制问题,Linux 设计者在匿名管道的基础上,增加了文件系统的标识 ,也就是命名管道文件

命名管道的核心设计思路可以总结为:给内核中的管道缓冲区,在文件系统中分配一个唯一的文件名,任何进程只要能访问这个文件,就能通过它连接到内核中的管道缓冲区,实现进程间通信

简单来说,匿名管道是**"无名的内核缓冲区"**,只有亲缘进程能找到;命名管道是 "有名的内核缓冲区",任何进程只要知道文件名,就能找到并使用。

1.3 命名管道与匿名管道的核心关联

命名管道是匿名管道的超集,二者的内核实现和操作语义完全一致:

  • 底层都是内核维护的单向字节流缓冲区
  • 都遵循相同的读写规则(阻塞 / 非阻塞、原子写入、SIGPIPE 信号等);
  • 都使用**read()/write()/close()**等标准文件操作接口;
  • 都是半双工通信,双向通信需要创建两个管道。

二者的唯一区别仅在于创建和打开的方式

  • 匿名管道通过**pipe()**函数创建,创建即打开,返回两个文件描述符;
  • 命名管道通过**mkfifo()函数创建(生成管道文件),通过open()**函数打开,获取文件描述符。

可以说,只要掌握了匿名管道,学习命名管道只需要重点掌握创建和打开这两个新操作,其余内容完全可以复用。

二、命名管道的核心认知:什么是 FIFO 文件?

命名管道的核心是FIFO 文件,这是一种特殊的文件类型,与普通文件、目录、设备文件并列,存在于 Linux 的文件系统中,但又有其独特的属性。

2.1 FIFO 文件的本质

FIFO 文件仅作为进程间通信的 "标识" ,它本身不存储任何数据,数据依旧存储在内核的管道缓冲区中。

当进程对 FIFO 文件执行**read()/write()**操作时,实际上是对内核中的管道缓冲区进行操作,FIFO 文件只是一个 "入口"。进程退出后,FIFO 文件会保留在文件系统中(除非手动删除),但内核中的管道缓冲区会被释放,数据也会丢失。

2.2 FIFO 文件的标识

在 Linux 中,通过ls -l命令查看文件时,FIFO 文件的文件类型标识为 p(pipe 的首字母),这是区分 FIFO 文件和其他文件的关键。

例如,创建一个名为mypipe的命名管道后,执行ls -l会看到如下结果:

复制代码
prw-r--r-- 1 root root 0 2月  11 15:00 mypipe

其中:

  • p:表示文件类型为命名管道(FIFO);
  • 大小为0:因为 FIFO 文件不存储数据,仅作为标识;
  • 权限位与普通文件一致(如rw-r--r--),用于控制进程对管道的访问权限。

2.3 命名管道的通信模型

命名管道的通信模型非常简单,核心分为三步:

  1. 创建 FIFO 文件 :通过**mkfifo()**或命令行创建一个 FIFO 文件,作为管道的标识;
  2. 进程打开 FIFO 文件 :通信的双方进程分别通过**open()**函数打开该 FIFO 文件,获取读 / 写文件描述符;
  3. 进程间通信:一个进程向 FIFO 文件写入数据(实际写入内核缓冲区),另一个进程从 FIFO 文件读取数据(实际从内核缓冲区读取),实现数据传输。

整个过程中,FIFO 文件就像一个 "桥梁",连接了两个无亲缘进程和内核中的管道缓冲区,突破了匿名管道的亲缘限制。

三、命名管道的创建:mkfifo () 函数与命令行

命名管道有两种创建方式:命令行创建程序中通过 mkfifo () 函数创建,前者适用于测试和手动操作,后者适用于程序开发,二者的效果完全一致。

3.1 命令行创建命名管道

在 Linux 终端中,直接使用mkfifo命令即可创建命名管道,语法如下:

bash 复制代码
# mkfifo [选项] 管道文件名
mkfifo mypipe

常用选项:

  • -m :指定管道文件的权限,如mkfifo -m 0644 mypipe
  • 不指定权限时,默认权限由umask决定(通常为 0666 & ~umask)。

创建后,通过ls -l即可看到标识为p的 FIFO 文件,通过rm 管道文件名即可删除该文件。

3.2 程序中创建命名管道:mkfifo () 函数

在 C/C++ 程序中,通过**mkfifo()函数创建命名管道,这是开发中的主流方式,函数原型定义在<sys/stat.h>**头文件中。

3.2.1 mkfifo () 函数接口

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>

// 功能:创建一个命名管道(FIFO文件)
// 参数:
//   pathname - 管道文件的路径+名称,如"./mypipe"
//   mode     - 管道文件的权限,如0644、0666,与open()函数的mode参数一致
// 返回值:成功返回0,失败返回-1,并设置errno
int mkfifo(const char *pathname, mode_t mode);

3.2.2 核心注意点

  1. 权限计算mkfifo()mode参数会与当前进程的umask进行按位与取反运算,最终的文件权限为mode & ~umask。如果需要让管道文件的权限严格等于mode,可以先通过umask(0)将掩码置 0;
  2. 文件已存在 :如果**pathname指定的 FIFO 文件已存在,再次调用mkfifo()会失败,errno设置为EEXIST**;
  3. 路径合法性pathname指定的目录必须存在,否则会失败,errno设置为ENOENT
  4. FIFO 文件与普通文件 :**mkfifo()**创建的是 FIFO 文件,不能创建普通文件,也不能覆盖已存在的普通文件。

3.2.3 简单创建示例

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>

int main()
{
    // 将umask置0,让管道权限严格为0644
    umask(0);
    // 创建命名管道文件./mypipe,权限0644
    if (mkfifo("./mypipe", 0644) < 0)
    {
        perror("mkfifo error");
        return -1;
    }
    printf("命名管道创建成功\n");
    return 0;
}

编译运行该程序后,在当前目录下执行ls -l,就能看到prw-r--r--的 FIFO 文件mypipe

四、命名管道的打开规则:最关键的核心知识点

命名管道的打开规则是学习的重中之重,也是与普通文件打开的最大区别,直接决定了进程的运行行为(阻塞 / 非阻塞)。

命名管道是单向通信 的,必须有一个进程以读方式 打开,一个进程以写方式 打开,内核才会完成管道的初始化,否则打开操作会根据是否设置非阻塞标志产生不同的结果。

命名管道的打开规则围绕open()函数的flags 参数 展开,核心分为读打开写打开 两种情况,每种情况又分为阻塞模式(默认)非阻塞模式(O_NONBLOCK)

4.1 open () 函数的核心参数

命名管道通过标准的**open()**函数打开,函数原型如下:

cpp 复制代码
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int open(const char *pathname, int flags, mode_t mode);

对于命名管道,核心关注flags参数,常用取值:

  • O_RDONLY:以只读方式打开(读端);
  • O_WRONLY:以只写方式打开(写端);
  • O_NONBLOCK :非阻塞模式,与 O_RDONLY/O_WRONLY 配合使用(如 O_RDONLY | O_NONBLOCK);
  • O_CREAT:如果文件不存在则创建(命名管道一般先通过 mkfifo () 创建,此参数很少用)。

4.2 以读方式打开命名管道(O_RDONLY)

4.2.1 阻塞模式(默认,仅 O_RDONLY)

如果进程以纯读方式 打开命名管道,且此时没有任何进程以写方式打开该管道 ,则open()调用会阻塞,直到有进程以写方式打开该管道,才会返回文件描述符。

简单来说:读端先打开,会阻塞等待写端打开

4.2.2 非阻塞模式(O_RDONLY | O_NONBLOCK)

如果进程以非阻塞读方式 打开命名管道,无论此时是否有进程以写方式打开该管道,open()调用都会立刻返回成功,获取读端文件描述符。

4.3 以写方式打开命名管道(O_WRONLY)

4.3.1 阻塞模式(默认,仅 O_WRONLY)

如果进程以纯写方式 打开命名管道,且此时没有任何进程以读方式打开该管道 ,则open()调用会阻塞,直到有进程以读方式打开该管道,才会返回文件描述符。

简单来说:写端先打开,会阻塞等待读端打开

4.3.2 非阻塞模式(O_WRONLY | O_NONBLOCK)

如果进程以非阻塞写方式 打开命名管道,且此时没有任何进程以读方式打开该管道 ,则open()调用会立刻失败 ,返回 - 1,errno设置为ENXIO

4.4 命名管道打开规则总结

为了方便记忆,我们将命名管道的打开规则整理成表格,核心记住阻塞模式下,读 / 写端相互等待;非阻塞模式下,读端一定成功,写端可能失败

打开方式 无对应进程打开另一端 有对应进程打开另一端
O_RDONLY(阻塞读) 阻塞等待 立刻成功
O_RDONLY O_NONBLOCK(非阻塞读) 立刻成功 立刻成功
O_WRONLY(阻塞写) 阻塞等待 立刻成功
O_WRONLY O_NONBLOCK(非阻塞写) 失败(ENXIO) 立刻成功

核心口诀堵读等写,堵写等读;非堵读必成,非堵写看端

4.5 打开规则的本质

命名管道的打开规则,本质是为了保证管道的通信双方都存在 ,避免出现 "一个进程向管道写入数据,但没有进程读取" 或 "一个进程从管道读取数据,但没有进程写入" 的无效操作,是内核对管道通信的基础保障

而非阻塞模式则为程序开发提供了灵活性,让进程可以不等待另一端,直接执行后续逻辑,适用于需要轮询或处理多任务的场景。

五、命名管道的读写规则:完全复用匿名管道

命名管道的读写规则与匿名管道完全一致,因为二者的底层都是内核维护的字节流缓冲区,操作语义完全相同。这里我们对核心读写规则进行重点回顾,结合命名管道的场景做简单说明,让你无缝复用知识。

命名管道的读写规则同样与是否设置非阻塞模式(O_NONBLOCK)密切相关,同时涉及原子写入SIGPIPE 信号等关键特性,是开发中必须遵守的规则,也是面试高频考点。

5.1 无数据可读时的读操作规则

  • 阻塞模式(默认)read()调用会阻塞,直到有进程向管道写入数据,才会被唤醒并读取数据;
  • 非阻塞模式(O_NONBLOCK)read()调用立刻返回 - 1errno设置为EAGAIN(表示资源暂时不可用,可重试)。

5.2 管道缓冲区写满时的写操作规则

Linux 内核中管道缓冲区的默认大小为4096 字节(1 页) ,可以通过ulimit -p命令查看。当管道缓冲区被写满时:

  • 阻塞模式(默认)write()调用会阻塞,直到有进程从管道读取数据,释放缓冲区空间,才会被唤醒并继续写入;
  • 非阻塞模式(O_NONBLOCK)write()调用立刻返回 - 1errno设置为EAGAIN

5.3 所有写端关闭后的读操作规则

如果所有持有管道写端文件描述符的进程都关闭了写端 ,此时管道中剩余的数据可以正常读取;当数据读取完毕后,再次调用read()返回 0,与读取普通文件到末尾的行为一致,表示 "管道已无数据,且不会再有新数据写入"。

这是命名管道通信中,读端判断写端退出的核心方式,也是实现通信结束的关键。

5.4 所有读端关闭后的写操作规则

如果所有持有管道读端文件描述符的进程都关闭了读端 ,此时进程向管道写端写入数据时,内核会向该进程发送SIGPIPE 信号 ,该信号的默认处理方式是终止进程

这是一个非常重要的 "坑",在命名管道开发中,如果未处理 SIGPIPE 信号,可能导致进程意外退出。因此,实际开发中需要通过**signal()sigaction()**函数捕获并处理 SIGPIPE信号(如忽略该信号)。

5.5 管道写入的原子性规则

Linux 内核保证了命名管道写入的原子性,规则与匿名管道一致:

  • 当要写入的数据量不大于 PIPE_BUF(内核定义的宏,默认 4096 字节)时,内核保证写入操作的原子性 ------ 数据会被完整写入,不会被其他进程的写入操作打断;
  • 当要写入的数据量大于 PIPE_BUF 时,内核不保证写入操作的原子性 ------ 数据可能被拆分,与其他进程的写入数据交错存在于管道中。

PIPE_BUF 的取值可以通过**<limits.h>头文件查看,也可以通过命令getconf PIPE_BUF**获取,不同系统的取值可能不同,但至少为 512 字节。

5.6 命名管道读写规则的核心注意点

命名管道的读写操作完全遵循文件的操作语义,但又有管道的特殊属性,核心注意点:

  1. 命名管道是字节流,无数据边界,读进程无法区分写进程的写入次数;
  2. 读写操作的返回值需要严格判断:**read()**返回 0 表示写端关闭,返回 - 1 表示出错(需结合 errno 判断是阻塞还是真错误);
  3. 多进程写命名管道时,为了保证数据不混乱,写入的数据量必须不大于 PIPE_BUF,利用原子写入特性保证数据完整性;
  4. 双向通信需要创建两个命名管道,分别负责两个方向的数据流。

六、命名管道的实战开发:从基础案例到 Server-Client 通信

理论学习后,最关键的是实战。本节我们通过三个经典的实战案例,从简单到复杂,一步步实现命名管道的开发,分别是:基础读写案例文件拷贝案例Server-Client 通信案例,覆盖命名管道的核心使用场景。

6.1 实战案例 1:命名管道基础读写

需求 :创建两个独立的程序,fifo_write.c(写端)和fifo_read.c(读端),实现写端向命名管道写入字符串,读端从命名管道读取并打印,验证命名管道的基本通信功能。

6.1.1 写端程序:fifo_write.c

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

// 错误处理宏
#define ERR_EXIT(m) \
do{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)

// 命名管道文件名
#define FIFO_NAME "./myfifo"

int main()
{
    // 1. 创建命名管道(如果已存在,会报错,这里先判断,不存在则创建)
    umask(0);
    if (mkfifo(FIFO_NAME, 0644) < 0)
    {
        // 错误码EEXIST表示文件已存在,无需处理
        if (errno != EEXIST)
            ERR_EXIT("mkfifo error");
    }

    // 2. 以阻塞写方式打开命名管道(默认O_WRONLY,会阻塞等待读端打开)
    int wfd = open(FIFO_NAME, O_WRONLY);
    if (wfd < 0)
        ERR_EXIT("open write error");
    printf("写端成功打开命名管道,文件描述符:%d\n", wfd);

    // 3. 向管道写入数据
    char buf[1024] = {0};
    while (1)
    {
        // 从键盘读取输入
        printf("请输入要发送的内容:");
        fflush(stdout);
        ssize_t s = read(0, buf, sizeof(buf)-1);
        if (s > 0)
        {
            buf[s-1] = 0; // 去掉换行符
            // 向管道写入数据
            write(wfd, buf, strlen(buf));
            // 输入quit,退出程序
            if (strcmp(buf, "quit") == 0)
                break;
        }
    }

    // 4. 关闭文件描述符
    close(wfd);
    printf("写端关闭命名管道\n");
    return 0;
}

6.1.2 读端程序:fifo_read.c

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

// 错误处理宏
#define ERR_EXIT(m) \
do{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)

// 命名管道文件名,与写端保持一致
#define FIFO_NAME "./myfifo"

int main()
{
    // 1. 以阻塞读方式打开命名管道(默认O_RDONLY,会阻塞等待写端打开)
    int rfd = open(FIFO_NAME, O_RDONLY);
    if (rfd < 0)
        ERR_EXIT("open read error");
    printf("读端成功打开命名管道,文件描述符:%d\n", rfd);

    // 2. 从管道读取数据
    char buf[1024] = {0};
    while (1)
    {
        ssize_t s = read(rfd, buf, sizeof(buf)-1);
        if (s > 0)
        {
            buf[s] = 0;
            printf("读端读取到数据:%s\n", buf);
            // 读取到quit,退出程序
            if (strcmp(buf, "quit") == 0)
                break;
        }
        else if (s == 0)
        {
            // s==0表示所有写端关闭
            printf("所有写端已关闭,读端退出\n");
            break;
        }
        else
        {
            ERR_EXIT("read error");
        }
    }

    // 3. 关闭文件描述符
    close(rfd);
    // 删除命名管道文件(可选,也可以手动删除)
    unlink(FIFO_NAME);
    printf("读端关闭并删除命名管道\n");
    return 0;
}

6.1.3 编译与运行

  1. 编译 :在终端中分别编译两个程序:

    bash 复制代码
    gcc fifo_write.c -o fifo_write
    gcc fifo_read.c -o fifo_read
  2. 运行 :打开两个终端,分别运行读端和写端(顺序任意):

    • 终端 1:./fifo_read
    • 终端 2:./fifo_write
  3. 测试 :在写端终端输入任意字符串,读端终端会实时打印;输入quit,双方程序退出。

6.1.4 运行结果分析

  • 无论先运行读端还是写端,阻塞模式下都会相互等待,直到另一端打开,才会打印 "成功打开命名管道";
  • 写端输入的内容会通过命名管道传输到读端,实现无亲缘进程间的通信;
  • 写端输入quit后,会向管道写入quit,读端读取到后退出,同时删除 FIFO 文件;
  • 如果直接关闭写端终端,读端会检测到s==0(写端关闭),并退出程序。

6.2 实战案例 2:基于命名管道实现文件拷贝

需求 :创建两个程序,fifo_copy_send.c(发送端)和fifo_copy_recv.c(接收端),实现发送端读取本地文件,通过命名管道发送给接收端,接收端将数据写入新文件,完成文件拷贝,验证命名管道的大数据传输能力

6.2.1 发送端程序:fifo_copy_send.c

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>

#define ERR_EXIT(m) \
do{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)

#define FIFO_NAME "./copy_fifo"
#define SRC_FILE "./src.txt" // 要拷贝的源文件

int main()
{
    // 1. 创建命名管道
    umask(0);
    if (mkfifo(FIFO_NAME, 0644) < 0)
    {
        if (errno != EEXIST)
            ERR_EXIT("mkfifo error");
    }

    // 2. 打开源文件和命名管道
    int src_fd = open(SRC_FILE, O_RDONLY);
    if (src_fd < 0)
        ERR_EXIT("open src file error");
    int wfd = open(FIFO_NAME, O_WRONLY);
    if (wfd < 0)
        ERR_EXIT("open fifo write error");
    printf("发送端准备就绪,开始拷贝文件...\n");

    // 3. 读取源文件,写入命名管道
    char buf[1024] = {0};
    ssize_t n = 0;
    while ((n = read(src_fd, buf, sizeof(buf))) > 0)
    {
        write(wfd, buf, n);
    }

    // 4. 关闭文件描述符
    close(src_fd);
    close(wfd);
    unlink(FIFO_NAME); // 可选删除
    printf("文件拷贝完成,发送端退出\n");
    return 0;
}

6.2.2 接收端程序:fifo_copy_recv.c

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define ERR_EXIT(m) \
do{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)

#define FIFO_NAME "./copy_fifo"
#define DST_FILE "./dst.txt" // 拷贝后的目标文件

int main()
{
    // 1. 打开命名管道和目标文件
    int rfd = open(FIFO_NAME, O_RDONLY);
    if (rfd < 0)
        ERR_EXIT("open fifo read error");
    // 创建目标文件,权限0644,存在则截断
    int dst_fd = open(DST_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (dst_fd < 0)
        ERR_EXIT("open dst file error");
    printf("接收端准备就绪,开始接收数据...\n");

    // 2. 从命名管道读取数据,写入目标文件
    char buf[1024] = {0};
    ssize_t n = 0;
    while ((n = read(rfd, buf, sizeof(buf))) > 0)
    {
        write(dst_fd, buf, n);
    }

    // 3. 关闭文件描述符
    close(rfd);
    close(dst_fd);
    printf("文件接收完成,目标文件:%s\n", DST_FILE);
    return 0;
}

6.2.3 测试步骤

  1. 创建源文件src.txt,写入任意内容:echo "hello world, this is fifo copy test" > src.txt
  2. 编译两个程序:gcc fifo_copy_send.c -o send && gcc fifo_copy_recv.c -o recv
  3. 打开两个终端,分别运行./recv./send
  4. 运行完成后,查看目标文件dst.txtcat dst.txt,内容与src.txt完全一致,说明文件拷贝成功。

6.2.4 案例核心亮点

  • 利用命名管道的字节流特性,实现了任意大小文件的拷贝(只要内存足够);
  • 读写缓冲区设置为 1024 字节,小于 PIPE_BUF(4096),保证了写入的原子性;
  • 发送端读取文件的返回值直接作为写入管道的长度,保证了数据的完整性;
  • 完美体现了 "一切皆文件" 的思想:文件和管道的操作接口完全一致,仅需替换文件描述符。

6.3 实战案例 3:基于命名管道实现 Server-Client 通信

需求 :实现一个简单的**服务端(Server)- 客户端(Client)**通信模型,服务端持续监听命名管道,客户端向服务端发送消息,服务端实时打印客户端发送的内容,支持客户端退出后服务端继续等待新的客户端连接,这是命名管道在实际开发中的典型应用场景。

6.3.1 服务端程序:serverPipe.c

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define ERR_EXIT(m) \
do{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)

#define FIFO_NAME "./server_fifo"

int main()
{
    // 1. 创建命名管道,服务端作为管道的创建者
    umask(0);
    if (mkfifo(FIFO_NAME, 0644) < 0)
    {
        if (errno != EEXIST)
            ERR_EXIT("mkfifo error");
    }
    printf("服务端启动,等待客户端连接...\n");

    while (1)
    {
        // 2. 以阻塞读方式打开管道,客户端退出后,重新打开等待新客户端
        int rfd = open(FIFO_NAME, O_RDONLY);
        if (rfd < 0)
            ERR_EXIT("open read error");

        char buf[1024] = {0};
        while (1)
        {
            ssize_t s = read(rfd, buf, sizeof(buf)-1);
            if (s > 0)
            {
                buf[s-1] = 0; // 去掉换行符
                printf("客户端消息:%s\n", buf);
            }
            else if (s == 0)
            {
                // 客户端关闭写端,服务端关闭当前读端,重新等待
                printf("客户端断开连接,等待新客户端...\n");
                close(rfd);
                break;
            }
            else
            {
                ERR_EXIT("read error");
                close(rfd);
                break;
            }
        }
    }

    // 实际不会执行到这里,服务端持续运行
    close(rfd);
    unlink(FIFO_NAME);
    return 0;
}

6.3.2 客户端程序:clientPipe.c

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define ERR_EXIT(m) \
do{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)

#define FIFO_NAME "./server_fifo"

int main()
{
    // 1. 以阻塞写方式打开命名管道,连接服务端
    int wfd = open(FIFO_NAME, O_WRONLY);
    if (wfd < 0)
        ERR_EXIT("open write error");
    printf("客户端成功连接服务端,请输入消息(输入quit退出):\n");

    // 2. 向服务端发送消息
    char buf[1024] = {0};
    while (1)
    {
        printf("> ");
        fflush(stdout);
        ssize_t s = read(0, buf, sizeof(buf)-1);
        if (s > 0)
        {
            write(wfd, buf, s);
            if (strncmp(buf, "quit", 4) == 0)
            {
                printf("客户端退出\n");
                break;
            }
            memset(buf, 0, sizeof(buf));
        }
    }

    // 3. 关闭写端
    close(wfd);
    return 0;
}

6.3.3 编译与运行

  1. 编写 Makefile,方便编译:

    bash 复制代码
    .PHONY:all
    all:serverPipe clientPipe
    
    serverPipe:serverPipe.c
        gcc -o $@ $^
    clientPipe:clientPipe.c
        gcc -o $@ $^
    
    .PHONY:clean
    clean:
        rm -f serverPipe clientPipe
  2. 编译: make

  3. 运行服务端: ./serverPipe,服务端启动并等待客户端连接;

  4. 运行客户端: 打开新终端,./clientPipe,客户端成功连接服务端;

  5. 测试: 客户端输入任意消息,服务端实时打印;客户端输入quit,断开连接,服务端继续等待新的客户端;

  6. 多客户端测试: 打开多个终端,运行多个./clientPipe,依次向服务端发送消息,服务端均可正常接收。

6.3.4 案例核心亮点

  • 服务端通过外层死循环 ,实现了持续监听,客户端退出后,重新打开命名管道,等待新的客户端连接;
  • 利用写端关闭后读端 read 返回 0的特性,服务端能精准检测到客户端的断开连接;
  • 实现了一对多的通信雏形,一个服务端可以为多个客户端提供通信服务;
  • 代码简洁,核心逻辑清晰,可直接扩展为更复杂的通信模型(如添加消息解析、指令处理等)。

七、命名管道的核心特点与使用场景

结合前面的原理和实战,我们总结命名管道的核心特点,并梳理其典型的使用场景,帮助你在实际开发中快速判断是否适合使用命名管道。

7.1 命名管道的核心特点

  1. 无亲缘限制 :最大的优势,支持任意进程间的通信,只要进程能访问 FIFO 文件(权限足够);
  2. 基于文件系统:FIFO 文件存在于文件系统中,进程退出后文件保留,可重复使用(需手动删除);
  3. 操作接口统一 :完全使用open()/read()/write()/close()等标准文件操作接口,契合 "一切皆文件" 思想,学习成本低;
  4. 读写规则与匿名管道一致:阻塞 / 非阻塞、原子写入、SIGPIPE 信号等规则完全复用,无需重新学习;
  5. 半双工通信 :数据只能单向流动,双向通信需要创建两个命名管道
  6. 字节流服务:无数据边界,读进程无法区分写进程的写入次数,需手动定义数据格式(如添加分隔符、消息头);
  7. 生命周期与进程 + 文件:内核缓冲区的生命周期随进程(所有进程关闭后释放),FIFO 文件的生命周期随文件系统(需手动删除);
  8. 内核自动同步互斥:同一时刻只允许一个进程对管道进行读 / 写操作,避免数据混乱。

7.2 命名管道的典型使用场景

命名管道适用于简单、低并发、单向 / 双向字节流通信 的场景,尤其是需要在无亲缘进程间传递数据的场景,典型应用包括:

  1. 独立进程间的简单数据传输:如两个独立的应用程序之间传递配置、状态、日志等简单数据;
  2. 服务端 - 客户端的基础通信:如简单的本地服务端程序,为客户端提供基础的消息响应服务;
  3. 本地程序的进程间协作:如一个主程序启动多个子进程,子进程通过命名管道向主程序上报运行状态;
  4. 文件 / 数据的跨进程拷贝:如将一个进程的输出数据,通过命名管道直接传输到另一个进程的输入,实现数据的无缝流转;
  5. 脚本与程序间的通信:如 Shell 脚本通过命名管道向 C/C++ 程序传递指令或数据,实现脚本与编译型程序的协作。

7.3 命名管道的局限性

命名管道并非万能的,也存在一些局限性,在高并发、高要求的通信场景中,需要选择其他 IPC 方式:

  1. 半双工通信:双向通信需要创建两个管道,增加了开发复杂度;
  2. 无消息边界:字节流模式,需要手动处理数据的拆分和解析,容易出现粘包问题;
  3. 不支持跨主机通信 :仅适用于同一台 Linux 主机上的进程间通信,无法实现跨主机的网络通信;
  4. 并发性能一般:内核的同步互斥机制限制了并发读写的性能,高并发场景下效率较低;
  5. 无自带的消息格式:需要开发者手动定义消息格式,如消息长度、消息类型等,不如消息队列灵活。

7.4 命名管道与其他 IPC 方式的对比

为了方便在实际开发中选择合适的 IPC 方式,我们将命名管道与常见的 IPC 方式(匿名管道、共享内存、消息队列、Socket)进行核心对比:

IPC 方式 亲缘限制 通信方向 数据格式 跨主机 并发性能 核心优势
匿名管道 半双工 字节流 一般 简单、轻量、亲缘进程专用
命名管道 半双工 字节流 一般 无亲缘限制、接口统一
共享内存 全双工 自定义 极高 最快的 IPC 方式
消息队列 全双工 消息 较好 有消息边界、自带格式
Socket(本地) 全双工 字节流 / 数据包 较好 跨主机、功能强大

选择建议

  • 亲缘进程间简单通信:匿名管道
  • 无亲缘进程间本地简单通信:命名管道
  • 本地高速度大数据传输:共享内存(需手动实现同步互斥);
  • 本地需要消息边界的通信:消息队列
  • 跨主机通信或复杂的本地通信:Socket

八、命名管道开发的避坑指南

在命名管道的实际开发中,新手很容易踩坑,导致程序出现阻塞、崩溃、数据丢失等问题。本节梳理开发中最常见的坑,并给出对应的解决方案,帮助你避开陷阱,写出健壮的代码。

8.1 坑 1:未处理 SIGPIPE 信号,导致进程意外退出

问题:所有读端关闭后,写端继续写入数据,内核会发送 SIGPIPE 信号,默认处理方式是终止进程,导致程序意外退出。

解决方案:在写端程序中,捕获并忽略 SIGPIPE 信号:

cpp 复制代码
#include <signal.h>
// 忽略SIGPIPE信号
signal(SIGPIPE, SIG_IGN);

8.2 坑 2:非阻塞模式下,错误判断不严谨

问题 :非阻塞模式下,read()/write()返回 - 1 时,直接认为是程序错误,实际上可能是EAGAIN(资源暂时不可用),属于正常情况。

解决方案 :判断返回值为 - 1 时,结合errno进行判断,忽略EAGAIN

cpp 复制代码
ssize_t s = read(rfd, buf, sizeof(buf));
if (s < 0)
{
    if (errno == EAGAIN)
    {
        // 非阻塞模式下,无数据,可重试
        continue;
    }
    else
    {
        // 真正的错误,处理
        perror("read error");
        break;
    }
}

8.3 坑 3:多进程写管道时,数据混乱

问题:多个进程同时向命名管道写入数据,且写入的数据量大于 PIPE_BUF,导致数据交错,出现混乱。

解决方案 :保证每个进程的写入数据量不大于 PIPE_BUF,利用原子写入特性,避免数据交错;如果需要写入大数据,可将数据拆分为多个 PIPE_BUF 大小的块,依次写入。

8.4 坑 4:双向通信时,只创建一个管道,导致死锁

问题:实现双向通信时,只创建一个命名管道,两个进程同时以读 + 写方式打开,导致双方相互阻塞,出现死锁。

解决方案双向通信必须创建两个命名管道,分别负责 A→B 和 B→A 的数据流,每个进程对一个管道读,对另一个管道写。

8.5 坑 5:FIFO 文件未删除,导致程序重启时 mkfifo 报错

问题 :程序退出后,未删除 FIFO 文件,再次启动程序时,mkfifo()会因文件已存在而报错(errno=EEXIST)。

解决方案

  1. 在**mkfifo()**后,判断 errno 是否为 EEXIST,若是则忽略错误;
  2. 读端 / 服务端退出时,通过**unlink()**函数删除 FIFO 文件;
  3. 程序启动时,先检查 FIFO 文件是否存在,若存在则先删除。

8.6 坑 6:阻塞模式下,进程卡死,无法退出

问题 :阻塞模式下,进程打开管道后,另一端一直未打开,导致进程卡死在open()调用,无法退出。

解决方案

  1. 使用信号:为进程注册信号处理函数(如 SIGINT),捕获 Ctrl+C 信号,在信号处理函数中关闭文件描述符并退出;
  2. 使用非阻塞模式:结合轮询,实现非阻塞的等待逻辑,避免进程卡死;
  3. 使用超时机制 :通过**alarm()**设置定时器,超时后触发 SIGALRM 信号,退出阻塞。

总结

命名管道的学习,不仅让我们掌握了一种实用的 IPC 方式,更让我们加深了对 Linux "一切皆文件" 设计思想的理解。在实际开发中,命名管道虽然不如 Socket 功能强大,不如共享内存速度快,但它以轻量、简单、易用的特点,在本地跨进程通信的场景中占据着重要的位置。

当然,命名管道也只是 Linux IPC 家族的一员,后续还可以继续学习共享内存 (最快的 IPC)、消息队列 (有消息边界的通信)、Socket(跨主机通信)等方式,构建完整的 Linux 进程间通信知识体系。但无论学习哪种 IPC 方式,命名管道的基础知识和设计思想,都是重要的铺垫。

希望本文能帮助你彻底吃透命名管道,在实际开发中灵活运用,实现高效的进程间通信!

相关推荐
小白同学_C8 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖8 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
不做无法实现的梦~10 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
默|笙12 小时前
【Linux】fd_重定向本质
linux·运维·服务器
陈苏同学12 小时前
[已解决] Solving environment: failed with repodata from current_repodata.json (python其实已经被AutoDL装好了!)
linux·python·conda
“αβ”12 小时前
网络层协议 -- ICMP协议
linux·服务器·网络·网络协议·icmp·traceroute·ping
不爱学习的老登13 小时前
Windows客户端与Linux服务器配置ssh无密码登录
linux·服务器·windows
何中应14 小时前
CentOS安装Nginx提示没找到
后端·centos·操作系统
小王C语言14 小时前
进程状态和进程优先级
linux·运维·服务器
xlp666hub14 小时前
【字符设备驱动】:从基础到实战(下)
linux·面试