前提引入
通过对匿名管道的学习,可以理解两个具有亲缘关系的进程之间的通信方式,通常是父子进程之间的通信。
那如果想让两个不相关的进程之间通信应该怎么做呢?
我们通常使用FIFO文件来完成通信工作,这个文件也就是所谓的命名管道。Linux在一切皆文件,匿名管道是文件,命名管道也是文件。任何知道其文件名的进程都可以访问它,即使这些进程没有亲缘关系。
匿名管道的原理就是父进程和子进程都可以看到同一个文件,命名管道的原理也是如此。
当进程A打开一个路径/a/b/c.txt
,进程B也打开一个路径/a/b/c.txt
,那么操作系统会将一个文件在内存中加载两次吗?
当然不会,完全没有必要。
管道文件有路径有名字,并且路径具有唯一性,这就是为什么叫命名管道的原因。
所以要如何让两个毫无关系的进程可以看到同一个文件呢?如图所示,这时候就需要管道文件了。可以将管道文件看做普通文件,但是它有着自己的特性,所以又和普通文件不同。
当两个进程想要打开这个管道文件时,前面的操作和普通文件相同,会在自己的文件描述符表占用一个位置然后得到一个struct file
。但是操作系统并不会将inode
和文件内容
加载到内存两次来提供给struct file
使用,而是两个struct file
指向同一份inode
和数据
。
通过这样就做到了没有关系的两个进程可以看到同一个文件,并且可以对该文件进行操作。
这个文件就是管道文件。
管道文件并不是真正的普通文件,
主要特点:
- 文件系统可见:命名管道在文件系统中以文件形式存在,可通过文件名访问。
- 持久性:创建后,除非显式删除(如使用 unlink 或 rm),它会一直存在。
- 单向通信:数据流是单向的,通常一个进程写入,另一个进程读取。如果需要双向通信,则需创建两个 FIFO。
- 默认阻塞:读写操作会阻塞,直到读端和写端都打开,除非设置为非阻塞模式。
- 管道文件在创建后,进程只需要打开它进行读写操作,而不需要像某些文件那样频繁刷新缓冲区。管道的读写操作是直接基于内存的,效率较高。
创建命名管道
命名管道可以通过命令行或程序代码创建。
命令行创建
在终端中使用 mkfifo
命令:
$ mkfifo myfifo
这会在当前目录下创建一个名为 myfifo 的命名管道。使用 ls -l 查看时,文件类型字段会显示 p,例如:
prw-r--r-- 1 user user 0 Oct 10 12:00 myfifo
程序代码创建
在 C/C++ 中,使用 <font style="color:black;">mkfifo</font>
函数:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
<font style="color:black;">filename</font>
:命名管道的路径名。<font style="color:black;">mode</font>
:文件权限,例如<font style="color:black;">0644</font>
(所有者可读写,其他人可读)。
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
if (mkfifo("myfifo", 0644) == -1) {
perror("mkfifo failed");
exit(EXIT_FAILURE);
}
printf("命名管道 'myfifo' 创建成功。\n");
return 0;
}
运行后,文件系统会多一个名为 <font style="color:black;">myfifo</font>
的命名管道。
命名管道的打开规则
命名管道通过 open
系统调用打开,其行为取决于打开模式(O_RDONLY
或 O_WRONLY
)和是否设置 O_NONBLOCK
标志:
- 以读模式打开(O_RDONLY)
- 默认(阻塞):open 会阻塞,直到有进程以写模式打开同一 FIFO。
- 非阻塞(O_NONBLOCK):立即返回成功,即使没有写端。
- 以写模式打开(O_WRONLY)
- 默认(阻塞):open 会阻塞,直到有进程以读模式打开同一 FIFO。
- 非阻塞(O_NONBLOCK):如果没有读端,open 失败,返回错误码 ENXIO。
这些规则确保读写两端同步,避免数据丢失或读取错误。
实际应用示例
示例 1:文件复制
以下示例展示如何使用命名管道将文件 <font style="color:black;">source.txt</font>
的内容复制到 <font style="color:black;">dest.txt</font>
。
写进程:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
int main() {
mkfifo("fifo", 0644); // 创建命名管道
int infd = open("source.txt", O_RDONLY); // 打开源文件
if (infd == -1) { perror("open source"); exit(1); }
int outfd = open("fifo", O_WRONLY); // 打开 FIFO 以写入
if (outfd == -1) { perror("open fifo"); exit(1); }
char buf[1024];
int n;
while ((n = read(infd, buf, 1024)) > 0) {
write(outfd, buf, n); // 写入 FIFO
}
close(infd);
close(outfd);
return 0;
}
读进程:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
int main() {
int outfd = open("dest.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); // 打开目标文件
if (outfd == -1) { perror("open dest"); exit(1); }
int infd = open("fifo", O_RDONLY); // 打开 FIFO 以读取
if (infd == -1) { perror("open fifo"); exit(1); }
char buf[1024];
int n;
while ((n = read(infd, buf, 1024)) > 0) {
write(outfd, buf, n); // 写入目标文件
}
close(infd);
close(outfd);
unlink("fifo"); // 删除 FIFO
return 0;
}
运行方式:
- 编译两个程序:
<font style="color:black;">gcc writer.c -o writer</font>
和<font style="color:black;">gcc reader.c -o reader</font>
。 - 在两个终端分别运行
<font style="color:black;">./writer</font>
和<font style="color:black;"></font><font style="color:black;">./reader</font>
。 <font style="color:black;">source.txt</font>
的内容会被复制到<font style="color:black;">dest.txt</font>
。
示例 2:服务器-客户端通信
以下示例展示命名管道如何实现简单的服务器-客户端通信。
服务器端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
mkfifo("chatfifo", 0644); // 创建命名管道
int rfd = open("chatfifo", O_RDONLY); // 以读模式打开
if (rfd < 0) { perror("open"); exit(1); }
char buf[1024];
while (1) {
printf("等待客户端消息...\n");
ssize_t s = read(rfd, buf, sizeof(buf) - 1);
if (s > 0) {
buf[s] = '\0';
printf("客户端说: %s\n", buf);
} else if (s == 0) {
printf("客户端退出,服务器关闭。\n");
break;
}
}
close(rfd);
unlink("chatfifo");
return 0;
}
客户端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int wfd = open("chatfifo", O_WRONLY); // 以写模式打开
if (wfd < 0) { perror("open"); exit(1); }
char buf[1024];
while (1) {
printf("请输入消息# ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf) - 1); // 从标准输入读取
if (s > 0) {
buf[s] = '\0';
write(wfd, buf, strlen(buf)); // 写入 FIFO
}
}
close(wfd);
return 0;
}
运行方式:
- 编译:
<font style="color:black;">gcc server.c -o server</font>
和<font style="color:black;">gcc client.c -o client</font>
。 - 先运行
<font style="color:black;">./server</font>
,再运行<font style="color:black;">./client</font>
。 - 在客户端输入消息,服务器会显示接收到的内容。
总结
命名管道(FIFO)是 Linux 中一种简单而高效的进程间通信工具。通过命令行或代码创建后,它可以在文件系统中持久存在,适用于任意进程之间的单向数据交换。