「Linux 系统编程」命名管道(FIFO)、文件实现进程通信

1 命名管道(FIFO)

管道(匿名管道)是一种半双工的通信方式,适用于有血缘关系的进程(例如,父子进程、兄弟进程)。但是,如果要实现两个无血缘关系进程间的通信呢?

命名管道(FIFO),它解决了普通管道只能用于血缘关系进程的问题。FIFO 在文件系统中以 一个特殊的文件形式 存在,不同进程可以通过该文件进行通信。

命名管道(FIFO) 的核心特点:

  • 是一个 有名称的文件实体
  • 半双工通信(数据单向流动)
  • 内核缓冲,不存储磁盘数据。FIFO 文件 在磁盘上不占用数据块,它只是一个索引节点(inode),用于 指向内核中的一块缓冲区。所有数据传输都发生在内核内存中,因此速度比读写真实磁盘文件快得多。
  • 阻塞式 I/O 行为(默认)
    • 读操作:如果管道为空,读进程会被阻塞,直到有数据写入。
    • 写操作:如果管道已满(内核缓冲区满),写进程会被阻塞,直到有读进程取走数据。
  • 进程生命周期独立。FIFO 一旦被创建,就会 持续存在于文件系统中直到被显式删除(如使用 unlink() 系统调用或 rm 命令)。它的存在不依赖于任何打开它的进程。


2 命名管道创建

2.1 命令方式创建

Linux 和 Unix 系统提供了一个名为 mkfifo 的命令行工具,专门用于创建命名管道。

1
mkfifo [选项] <命名管道名称>

常用选项-m--mode=MODE,即设置管道的权限模式(类似于 chmod),例如 -m 0666

示例

在当前目录下创建一个名为 myfifo1 的管道:

1
mkfifo myfifo1

在指定的目录(例如 /home/marisa/fifo_test)创建一个名为 myfifo2 的管道:

1
mkfifo -m 0666 /home/marisa/fifo_test/myfifo2

注意:权限 666 确保了其他非创建者的用户进程也有权限读写这个管道。

创建成功后,使用 ls -l 命令查看。命名管道文件的类型标识是 p(代表 pipe):


2.2 在程序中创建

在 C 语言程序中,我们使用 mkfifo() 系统函数来创建命名管道。这种方式提供了更大的灵活性,可以在程序逻辑中动态地创建和管理管道。

函数原型

1
2
3
4
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

参数说明

  • pathname:创建的命名管道 FIFO 文件的路径
  • mode:指定命名管道 FIFO 文件的权限位。通常会被系统的 umask 值所修改,最终权限是 (mode & ~umask)。例如,如果 mode 设为 0666umask0022,那么最终权限就是 0644 (rw-r–r–)。

返回值

  • 成功:返回 0
  • 失败:返回 -1,设置 errno

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>

int main() {
char* fifo_name = "./myfifo3";

// 尝试创建FIFO
if (mkfifo(fifo_name, 0666) == -1) {
// 如果错误原因是“文件已存在”,我们可以直接使用它,不算错误。
if (errno != EEXIST) {
perror("mkfifo failed");
exit(EXIT_FAILURE);
} else {
printf("FIFO already exists, using it.\n");
}
} else {
printf("FIFO created successfully.\n");
}

// 可以添加后续的 open, read/write 等操作
// 例如: int fd = open(fifo_name, O_WRONLY);
// write(fd, "Hello", 6);

// 程序结束时可以选择是否删除FIFO
// unlink(fifo_name);

return 0;
}

程序方式的特点

  • 动态创建:可以根据程序运行时的条件和需要来决定是否创建、以及创建哪个管道。
  • 错误处理:可以优雅地处理“文件已存在”等情况,决定是直接使用还是报错。
  • 资源管理:程序可以在退出前使用 unlink() 删除管道文件,完成自动清理。

2.3 命名管道进程通信示例

创建两个无血缘关系的进程,一个负责写入数据,另一个负责读取数据。

写者进程 write_process.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
int fd = open("./myfifo1", O_WRONLY); // 打开对应的命名管道文件

char* msg = "This is writer Alice Margatroid.\n";
write(fd, msg, strlen(msg));

close(fd);

return 0;
}

读者进程 read_process.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
char buf[1024];
int fd = open("./myfifo1", O_RDONLY); // 注意打开的命名管道一致

int bytes = read(fd, buf, sizeof(buf) - 1);
buf[bytes] = '\0'; // 添加终止符
printf("reader Kirisame Marisa get message: %s\n", buf);

close(fd);

return 0;
}

运行方式

  • 创建命名管道文件 myfifo1
  • 先运行读者进程(此时命名管道还未写入数据,读者进程会阻塞等待)
  • 再运行写者进程

也可以多个写进程可以向同一个 FIFO 写入数据,读进程会按顺序读取所有写入的内容。或者,一个写进程写数据,多个读进程从同一个 FIFO 读取数据。注意:数据一旦被读取就会从管道中移除。一个写进程写、多个读进程读,这些读进程读取的数据都是不同的。



3 命名管道进程通信的原理

命名管道(FIFO)之所以能实现无血缘关系进程间的通信,是因为它在文件系统中拥有一个 全局可见的、有名字 的 “入口点”(即特殊的管道文件)。这个文件名成为了所有想通信的进程之间一个预先约定好的 “联络点”。

3.1 命名管道和匿名管道的根本区别

  • 匿名管道

    通过 pipe() 系统调用创建。它 只存在于内核内存 中,不会在磁盘的文件系统里创建一个可见的文件。它只返回两个文件描述符(一个读端,一个写端)。由于没有名字,其他进程无法“找到”它。它的 文件描述符只能通过 fork() 由父进程传递给子进程,因此通信范围被严格限制在了有血缘关系的进程之间。

    比喻:就像父子俩在家里私下约定了一个暗号。外人不知道这个暗号,甚至不知道这个暗号的存在,所以无法加入对话。

  • 命名管道

    通过 mkfifo() 系统调用创建。它不仅 在内核中创建了数据缓冲区,更重要的是它在文件系统的目录树中 创建了一个确实存在、有名字的文件节点(例如 ./myfifo)。这个文件不存储实际数据,它只是一个标识,一个访问点

    比喻:就像在市中心立了一个所有人都能看到的、写着“公共留言板”的牌子。任何知道这个牌子位置的人(进程),无论他们之间是什么关系,都可以去那里留言(写)或者看留言(读)。

命名管道大小和容量限制 机制上与 匿名管道 完全相同。都使用相同的内核缓冲区机制。

在 Linux 系统中,管道缓冲区的大小默认是 64 KiB、在某些历史版本中,这个值可能是 4 KiB 或 8 KiB。允许管道容量系统上限 pipe-max-size

区别总结

特性 匿名管道 命名管道
存在形式 仅存在于内存中,没有磁盘索引节点(inode) 在文件系统中有一个名称和索引节点(inode),但 不占用磁盘数据块
持久性 随进程的创建而创建,随最后一个使用它的进程的结束而销毁 一旦创建,除非被手动删除(unlink),否则会一直存在于文件系统中
访问控制 只能由 有亲缘关系 的进程(如父子进程、兄弟进程)访问 任何知道其名称的进程都可以访问,无需亲缘关系

3.2 命名管道实现机制

  1. 创建联络点:进程调用 mkfifo("./myfifo", 0666)。内核在文件系统中 创建了一个特殊类型的文件 ./myfifo

  2. 打开联络点

    • 进程 A(写者)调用 open("./myfifo", O_WRONLY)。内核找到这个文件节点,知道它是一个 FIFO,然后为它 分配内核缓冲区返回一个文件描述符

    • 进程 B(读者)调用 open("./myfifo", O_RDONLY)。内核发现同一个 FIFO 文件节点,并 让进程 B 的文件描述符指向同一个内核缓冲区

  3. 进程间通信

    • 进程 A 使用 write() 向它的文件描述符写入数据。数据被送入内核缓冲区

    • 进程 B 使用 read() 从它的文件描述符读取数据。数据从同一个内核缓冲区中被取出

内核的角色:内核负责管理这个共享的缓冲区,并处理所有复杂的同步和阻塞问题。例如,如果读者还没打开,写者先打开,写者会被阻塞,直到读者也打开管道,反之亦然。


3.3 命名管道的特质

  • 全局可见的路径名:文件系统中的路径名(如 /home/myfifo)是一个所有进程都可以访问的全局唯一标识。它充当了一个约定的通信地址,任何知道这个地址的进程都可以尝试连接。

  • 独立于进程的生命周期:这个管道文件一旦被创建,会 一直存在于文件系统中(除非被主动删除),它的存在不依赖于任何一个创建它或使用它的进程。这使得进程可以在不同的时间点、甚至由不同的用户启动,只要它们都能访问这个路径。

  • 内核缓冲区的共享:所有打开这个命名管道的进程,最终通过文件系统,连接到内核中同一块缓冲区。数据的流动是在这块共享的内核缓冲区中进行的,文件本身只是提供了一个访问途径。



4 文件实现进程通信

文件实现进程间通信:多个无血缘关系的进程,可以同时访问该普通文件。

  • 有血缘关系的进程对于同一个文件,使用的同一个文件描述符
  • 没有血缘关系的进程,对同一个文件使用的文件描述符可能不同

但是,普通文件 需要占用磁盘空间。两个进程通过一个普通文件进行通信,内核需要先预读取文件到内存中,然后两个进程再进行通信操作。而且,没有默认的阻塞 I/O 行为,可能需要增加锁机制来建立进程同步。


命名管道和普通文件实现非血缘关系进程通信的区别

特性 命名管道 (FIFO) 普通文件 (Regular File)
数据存储位置 内核内存缓冲区 磁盘块(经页缓存加速)
通信本质 内存间数据传输 磁盘 I/O 操作
速度 (内存速度) (受磁盘 I/O 限制)
同步机制 内置阻塞/同步(打开、读、写均可阻塞) 无内置同步(需外部机制:如文件锁 fcntl
数据持久性 临时数据
进程结束、系统重启后数据丢失
持久化数据
数据被永久保存到磁盘上
容量限制 受限于内核缓冲区大小 受限于磁盘空间大小
访问模式 通常是单向顺序访问 可以随机访问lseek
主要优势 实时性高、延迟低、进程同步简单 数据持久化、进程可随时独立访问
典型应用场景 实时消息传递、日志收集器、生产者-消费者模型 配置共享、数据交换(非实时)、大规模数据处理
  • 选择命名管道 (FIFO):当你需要实现 高效、实时、低延迟 的进程间通信,并且 数据不需要持久化存储 时。它天然的阻塞特性简化了进程间的同步协调。

  • 选择普通文件:当通信的数据需要 持久化,或者进程不需要同时在线(一个进程写入后,另一个进程可以很久之后再读取),或者需要随机访问数据内容时。


「Linux 系统编程」命名管道(FIFO)、文件实现进程通信
https://marisamagic.github.io/2025/09/06/20250906/
作者
MarisaMagic
发布于
2025年9月6日
许可协议