云计算百科
云计算领域专业知识百科平台

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


目录

前言

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

1.1 匿名管道的致命短板

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

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

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

2.1 FIFO 文件的本质

2.2 FIFO 文件的标识

2.3 命名管道的通信模型

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

3.1 命令行创建命名管道

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

3.2.1 mkfifo () 函数接口

3.2.2 核心注意点

3.2.3 简单创建示例

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

4.1 open () 函数的核心参数

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

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

4.2.2 非阻塞模式(O_RDONLY | O_NONBLOCK)

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

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

4.3.2 非阻塞模式(O_WRONLY | O_NONBLOCK)

4.4 命名管道打开规则总结

4.5 打开规则的本质

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

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

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

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

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

5.5 管道写入的原子性规则

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

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

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

6.1.1 写端程序:fifo_write.c

6.1.2 读端程序:fifo_read.c

6.1.3 编译与运行

6.1.4 运行结果分析

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

6.2.1 发送端程序:fifo_copy_send.c

6.2.2 接收端程序:fifo_copy_recv.c

6.2.3 测试步骤

6.2.4 案例核心亮点

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

6.3.1 服务端程序:serverPipe.c

6.3.2 客户端程序:clientPipe.c

6.3.3 编译与运行

6.3.4 案例核心亮点

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

7.1 命名管道的核心特点

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

7.3 命名管道的局限性

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

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

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

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

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

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

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

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 命名管道的通信模型

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

  • 创建 FIFO 文件:通过mkfifo()或命令行创建一个 FIFO 文件,作为管道的标识;
  • 进程打开 FIFO 文件:通信的双方进程分别通过open()函数打开该 FIFO 文件,获取读 / 写文件描述符;
  • 进程间通信:一个进程向 FIFO 文件写入数据(实际写入内核缓冲区),另一个进程从 FIFO 文件读取数据(实际从内核缓冲区读取),实现数据传输。
  •         整个过程中,FIFO 文件就像一个 “桥梁”,连接了两个无亲缘进程和内核中的管道缓冲区,突破了匿名管道的亲缘限制。

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

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

    3.1 命令行创建命名管道

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

    # 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 () 函数接口

    #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 核心注意点

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

    #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()函数打开,函数原型如下:

    #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()调用立刻返回 – 1,errno设置为EAGAIN(表示资源暂时不可用,可重试)。

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

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

    • 阻塞模式(默认):write()调用会阻塞,直到有进程从管道读取数据,释放缓冲区空间,才会被唤醒并继续写入;
    • 非阻塞模式(O_NONBLOCK):write()调用立刻返回 – 1,errno设置为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 命名管道读写规则的核心注意点

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

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

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

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

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

    6.1.1 写端程序:fifo_write.c

    #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

    #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 编译与运行

  • 编译:在终端中分别编译两个程序: gcc fifo_write.c -o fifo_write
    gcc fifo_read.c -o fifo_read
  • 运行:打开两个终端,分别运行读端和写端(顺序任意):
    • 终端 1:./fifo_read
    • 终端 2:./fifo_write
  • 测试:在写端终端输入任意字符串,读端终端会实时打印;输入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

    #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

    #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 测试步骤

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

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

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

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

    6.3.1 服务端程序:serverPipe.c

    #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

    #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 编译与运行

  • 编写 Makefile,方便编译: .PHONY:all
    all:serverPipe clientPipe

    serverPipe:serverPipe.c
    gcc -o $@ $^
    clientPipe:clientPipe.c
    gcc -o $@ $^

    .PHONY:clean
    clean:
    rm -f serverPipe clientPipe

  • 编译:make;
  • 运行服务端:./serverPipe,服务端启动并等待客户端连接;
  • 运行客户端:打开新终端,./clientPipe,客户端成功连接服务端;
  • 测试:客户端输入任意消息,服务端实时打印;客户端输入quit,断开连接,服务端继续等待新的客户端;
  • 多客户端测试:打开多个终端,运行多个./clientPipe,依次向服务端发送消息,服务端均可正常接收。
  • 6.3.4 案例核心亮点

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

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

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

    7.1 命名管道的核心特点

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

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

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

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

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

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

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

    选择建议:

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

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

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

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

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

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

    #include <signal.h>
    // 忽略SIGPIPE信号
    signal(SIGPIPE, SIG_IGN);

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

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

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

    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)。

            解决方案:

  • mkfifo()后,判断 errno 是否为 EEXIST,若是则忽略错误;
  • 读端 / 服务端退出时,通过unlink()函数删除 FIFO 文件;
  • 程序启动时,先检查 FIFO 文件是否存在,若存在则先删除。
  • 8.6 坑 6:阻塞模式下,进程卡死,无法退出

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

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

  • 总结

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

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

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

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Linux系统编程】(三十二)命名管道 FIFO 精讲:突破亲缘限制,实现任意进程间的 IPC 通信
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!