「Linux 系统编程」命名管道(FIFO)、文件实现进程通信
1 命名管道(FIFO)
管道(匿名管道)是一种半双工的通信方式,适用于有血缘关系的进程(例如,父子进程、兄弟进程)。但是,如果要实现两个无血缘关系进程间的通信呢?
命名管道(FIFO),它解决了普通管道只能用于血缘关系进程的问题。FIFO 在文件系统中以 一个特殊的文件形式 存在,不同进程可以通过该文件进行通信。
命名管道(FIFO) 的核心特点:
- 是一个 有名称的文件实体
- 半双工通信(数据单向流动)
- 内核缓冲,不存储磁盘数据。FIFO 文件 在磁盘上不占用数据块,它只是一个索引节点(inode),用于 指向内核中的一块缓冲区。所有数据传输都发生在内核内存中,因此速度比读写真实磁盘文件快得多。
- 阻塞式 I/O 行为(默认)
- 读操作:如果管道为空,读进程会被阻塞,直到有数据写入。
- 写操作:如果管道已满(内核缓冲区满),写进程会被阻塞,直到有读进程取走数据。
- 进程生命周期独立。FIFO 一旦被创建,就会 持续存在于文件系统中,直到被显式删除(如使用
unlink()
系统调用或rm
命令)。它的存在不依赖于任何打开它的进程。
2 命名管道创建
2.1 命令方式创建
Linux 和 Unix 系统提供了一个名为 mkfifo 的命令行工具,专门用于创建命名管道。
1 |
|
常用选项:-m
,--mode=MODE
,即设置管道的权限模式(类似于 chmod),例如 -m 0666
示例:
在当前目录下创建一个名为 myfifo1
的管道:
1 |
|
在指定的目录(例如 /home/marisa/fifo_test
)创建一个名为 myfifo2
的管道:
1 |
|
注意:权限 666
确保了其他非创建者的用户进程也有权限读写这个管道。
创建成功后,使用 ls -l
命令查看。命名管道文件的类型标识是 p
(代表 pipe):
2.2 在程序中创建
在 C 语言程序中,我们使用 mkfifo()
系统函数来创建命名管道。这种方式提供了更大的灵活性,可以在程序逻辑中动态地创建和管理管道。
函数原型:
1 |
|
参数说明:
pathname
:创建的命名管道 FIFO 文件的路径mode
:指定命名管道 FIFO 文件的权限位。通常会被系统的umask
值所修改,最终权限是(mode & ~umask)
。例如,如果mode
设为0666
,umask
是0022
,那么最终权限就是0644
(rw-r–r–)。
返回值:
- 成功:返回
0
。 - 失败:返回
-1
,设置errno
。
示例:
1 |
|
程序方式的特点:
- 动态创建:可以根据程序运行时的条件和需要来决定是否创建、以及创建哪个管道。
- 错误处理:可以优雅地处理“文件已存在”等情况,决定是直接使用还是报错。
- 资源管理:程序可以在退出前使用 unlink() 删除管道文件,完成自动清理。
2.3 命名管道进程通信示例
创建两个无血缘关系的进程,一个负责写入数据,另一个负责读取数据。
写者进程 write_process.c
:
1 |
|
读者进程 read_process.c
:
1 |
|
运行方式:
- 创建命名管道文件
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 命名管道实现机制
-
创建联络点:进程调用
mkfifo("./myfifo", 0666)
。内核在文件系统中 创建了一个特殊类型的文件./myfifo
。 -
打开联络点:
-
进程 A(写者)调用
open("./myfifo", O_WRONLY)
。内核找到这个文件节点,知道它是一个 FIFO,然后为它 分配内核缓冲区 并 返回一个文件描述符。 -
进程 B(读者)调用
open("./myfifo", O_RDONLY)
。内核发现同一个 FIFO 文件节点,并 让进程 B 的文件描述符指向同一个内核缓冲区。
-
-
进程间通信:
-
进程 A 使用
write()
向它的文件描述符写入数据。数据被送入内核缓冲区。 -
进程 B 使用
read()
从它的文件描述符读取数据。数据从同一个内核缓冲区中被取出。
-
内核的角色:内核负责管理这个共享的缓冲区,并处理所有复杂的同步和阻塞问题。例如,如果读者还没打开,写者先打开,写者会被阻塞,直到读者也打开管道,反之亦然。
3.3 命名管道的特质
-
全局可见的路径名:文件系统中的路径名(如
/home/myfifo
)是一个所有进程都可以访问的全局唯一标识。它充当了一个约定的通信地址,任何知道这个地址的进程都可以尝试连接。 -
独立于进程的生命周期:这个管道文件一旦被创建,会 一直存在于文件系统中(除非被主动删除),它的存在不依赖于任何一个创建它或使用它的进程。这使得进程可以在不同的时间点、甚至由不同的用户启动,只要它们都能访问这个路径。
-
内核缓冲区的共享:所有打开这个命名管道的进程,最终通过文件系统,连接到内核中同一块缓冲区。数据的流动是在这块共享的内核缓冲区中进行的,文件本身只是提供了一个访问途径。
4 文件实现进程通信
文件实现进程间通信:多个无血缘关系的进程,可以同时访问该普通文件。
- 有血缘关系的进程对于同一个文件,使用的同一个文件描述符;
- 没有血缘关系的进程,对同一个文件使用的文件描述符可能不同。
但是,普通文件 需要占用磁盘空间。两个进程通过一个普通文件进行通信,内核需要先预读取文件到内存中,然后两个进程再进行通信操作。而且,没有默认的阻塞 I/O 行为,可能需要增加锁机制来建立进程同步。
命名管道和普通文件实现非血缘关系进程通信的区别:
特性 | 命名管道 (FIFO) | 普通文件 (Regular File) |
---|---|---|
数据存储位置 | 内核内存缓冲区 | 磁盘块(经页缓存加速) |
通信本质 | 内存间数据传输 | 磁盘 I/O 操作 |
速度 | 快(内存速度) | 慢(受磁盘 I/O 限制) |
同步机制 | 内置阻塞/同步(打开、读、写均可阻塞) | 无内置同步(需外部机制:如文件锁 fcntl ) |
数据持久性 | 临时数据 进程结束、系统重启后数据丢失 |
持久化数据 数据被永久保存到磁盘上 |
容量限制 | 受限于内核缓冲区大小 | 受限于磁盘空间大小 |
访问模式 | 通常是单向顺序访问 | 可以随机访问(lseek ) |
主要优势 | 实时性高、延迟低、进程同步简单 | 数据持久化、进程可随时独立访问 |
典型应用场景 | 实时消息传递、日志收集器、生产者-消费者模型 | 配置共享、数据交换(非实时)、大规模数据处理 |
-
选择命名管道 (FIFO):当你需要实现 高效、实时、低延迟 的进程间通信,并且 数据不需要持久化存储 时。它天然的阻塞特性简化了进程间的同步协调。
-
选择普通文件:当通信的数据需要 持久化,或者进程不需要同时在线(一个进程写入后,另一个进程可以很久之后再读取),或者需要随机访问数据内容时。