
能从冰封的土地里
培育出十万朵怒放的蔷薇
Linux 系统篇(三十二) —— 命名管道详解
文章目录
一、为什么有了匿名管道,还需要命名管道?
在学习 Linux 进程间通信时,我们已经接触过匿名管道。通过 pipe() 创建一个管道,再配合 fork() 创建子进程,就可以让父子进程之间交换数据。
例如,父进程读取一份数据,然后通过管道发送给子进程,子进程接收到数据后再进行处理。
这种方式很好用,但随着问题逐渐复杂,我们会发现一个限制:
匿名管道通常用于具有亲缘关系的进程之间通信。如果两个进程互不相关,它们应该怎样交换数据?
先来看一个具体场景。
假设我们现在有两个独立运行的程序:
server:负责接收消息。client:负责发送消息。
我们希望先启动服务器,再启动客户端。客户端输入一段文字,服务器就能接收到并显示出来。
问题来了:这两个程序不是通过 fork() 创建出来的父子进程,它们没有共同的父进程,也没有提前继承对方的文件描述符。
那么,之前学习的匿名管道还能直接使用吗?
通常不行。匿名管道没有一个供两个独立程序共同查找的文件系统路径,双方无法仅仅通过一个共同的文件名找到并打开它。
既然如此,我们能不能给管道一个名字,让两个互不相关的进程都能找到它?
答案是可以的。
这就是我们今天要学习的主角——命名管道(Named Pipe),也叫 FIFO。
二、什么是命名管道?
2.1 给管道一个名字
匿名管道和命名管道的核心思想其实相同:都可以让一个进程写入数据,再让另一个进程读取数据。
不同的是,命名管道在文件系统中有一个对应的名字,两个独立运行的进程可以通过这个名字找到同一个管道。
例如,我们可以创建一个名为 fifo 的命名管道:
mkfifo fifo
创建完成后,执行:
ls -l
可能看到类似这样的结果:

注意最前面的字符:
p
它表示这是一个 FIFO 类型的文件,也就是命名管道。
普通文件通常以 - 开头,目录通常以 d 开头,而命名管道以 p 开头。
这里有一个非常重要的知识点:
命名管道虽然在文件系统中有一个名字,但它并不是用来保存普通文件内容的。
普通文件通常将数据持久地保存到文件中,而命名管道主要用于进程之间传递数据。数据被读取后,就会从管道的数据流中消耗掉,并不是一直保存在这个路径对应的文件里。
可以把它理解成一个有名字的传送通道:
client找到管道,向里面写入消息。server找到同一个管道,从里面读取消息。- 数据经过管道传递,而不是作为普通文件内容永久保存。
因此,命名管道本质上是一种特殊类型的文件,其实际通信能力由 Linux 内核提供。
2.2 命名管道能解决什么问题?
继续看 client 和 server。
我们只需要让两个进程约定好同一个命名管道路径,就能分别打开它:
client ── write() ──> 命名管道 ── read() ──> server
双方不需要是父子进程,也不需要通过 fork() 相互继承文件描述符。
三、如何创建命名管道?
理解了命名管道的作用,接下来就要解决实际问题:怎样创建它?
Linux 中主要有两种方式:使用命令行创建,以及在程序中调用 mkfifo()。
3.1 通过命令行创建
最简单的方法就是使用 mkfifo 命令:
mkfifo fifo
这条命令会在当前目录下创建一个名为 fifo 的命名管道。
我们可以通过下面的命令查看:
ls -l fifo
如果想删除它,可以执行:
rm fifo
这里要注意,rm fifo 删除的是文件系统中的管道名称,而不是把已经传输过的数据清空。对于仍然打开着该 FIFO 的进程,已经打开的引用在最后一个引用关闭之前仍然可以存在。
3.2 通过程序创建
实际开发中,我们通常需要让程序自动创建命名管道,因此要使用系统调用 mkfifo()。
它的函数原型是:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
两个参数分别表示:
filename:命名管道的路径。mode:创建时设置的权限,例如0644、0666。
返回值:
- 成功:返回
0。- 失败:返回
-1,并设置errno,可以使用perror()查看错误原因。
下面来看一个简单的例子:
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <cerrno>
int main()
{
// 在当前目录下创建一个名为 p2 的命名管道
if (mkfifo("p2", 0644) == -1)
{
// 如果管道已经存在,也会创建失败,errno 为 EEXIST
perror("mkfifo");
return 1;
}
printf("mkfifo success\n");
return 0;
}
如果创建成功,就能在目录中看到 p2。
这里还有两个容易忽略的细节。
第一,mkfifo() 不负责打开管道。
它只是在文件系统中创建命名管道。后续要使用它传输数据,还需要调用 open() 获取文件描述符。
第二,重复创建会失败。
如果 p2 已经存在,mkfifo() 通常会返回 -1,并将 errno 设置为 EEXIST。实际程序中可以检查该错误,并判断已有路径是否确实是一个 FIFO,而不是直接认定创建失败就代表管道已经存在且可用。
四、匿名管道和命名管道有什么区别?
现在我们已经知道,匿名管道和命名管道都可以传输数据,那么它们究竟有什么区别?
| 对比项 | 匿名管道 | 命名管道 |
|---|---|---|
| 创建方式 | pipe() | mkfifo() |
| 打开方式 | pipe() 创建时直接获得文件描述符 | 使用 open() 打开 |
| 是否有文件系统名称 | 通常没有可供双方共同打开的路径名 | 有 |
| 常见通信对象 | 父子进程等有亲缘关系的进程 | 可以是互不相关的进程 |
| 数据传输方式 | 内核提供的管道数据流 | 内核提供的 FIFO 数据流 |
| 是否直接保存普通文件内容 | 否 | 否 |
| 是否具有先进先出的特性 | 是 | 是 |
匿名管道由 pipe() 创建并直接返回读写文件描述符;命名管道则先通过 mkfifo() 创建,再通过 open() 获取文件描述符。
当两种管道都完成创建和打开后,它们的基本读写语义是相同的。
例如,两者都可以使用 read() 读取数据,使用 write() 写入数据,也都可能因为管道为空、缓冲区已满或对端关闭而发生阻塞、结束或报错。
五、命名管道最容易出问题的地方:open() 为什么会阻塞?
创建好命名管道之后,我们就可以使用 open() 打开它。
但是,在真正编写程序之前,还有一个必须理解的问题:
为什么有时候程序执行到 open() 就不动了,看起来像卡死了一样?
这通常不是程序出错,而是命名管道本身的打开规则导致的。
5.1 以只读方式打开 FIFO
假设服务器准备读取客户端发送的数据:
int fd = open("fifo", O_RDONLY);
这里的 O_RDONLY 表示以只读方式打开。
如果没有使用 O_NONBLOCK,也就是默认的阻塞模式,那么当 FIFO 还没有任何进程以写方式打开时,这个 open() 会一直等待。
为什么?
因为服务器的目的是读取数据,但目前还没有发送方建立写入连接。Linux 会让服务器等待,直到有进程以写方式打开同一个 FIFO。
可以把这个过程理解成:
服务器:我要打开管道,准备接收数据
|
v
等待写入端出现
|
v
客户端:打开同一个管道,准备发送数据
|
v
open() 完成
注意,这里的阻塞发生在 open() 阶段,而不一定是 read() 阶段。
5.2 以只写方式打开 FIFO
再来看客户端:
int fd = open("fifo", O_WRONLY);
这里的 O_WRONLY 表示以只写方式打开。
如果没有使用 O_NONBLOCK,而当前没有任何进程以读方式打开 FIFO,那么客户端的 open() 同样会阻塞。
原因也很简单:如果没有读取端,发送方就还没有建立起正常的管道通信关系。
因此,默认阻塞模式下有两条规则:
- 读端打开 FIFO:等待写端打开。
- 写端打开 FIFO:等待读端打开。
这意味着,如果我们先启动服务器,再启动客户端,通常会看到这样的现象:
- 服务器执行
open("fifo", O_RDONLY),暂时阻塞。- 客户端执行
open("fifo", O_WRONLY)。- 双方匹配成功,
open()返回。- 服务器可以开始读取,客户端可以开始写入。
这也解释了为什么命名管道的两个程序可以分开启动,却仍然能够配合工作。
5.3 如果使用 O_NONBLOCK,会发生什么?
有时,我们不希望程序一直等待,而是希望 open() 立即返回。这时可以使用 O_NONBLOCK,即非阻塞模式。
例如:
int fd = open("fifo", O_RDONLY | O_NONBLOCK);
对于 FIFO,非阻塞打开规则如下。
| 打开方式 | 是否有对端 | 结果 |
|---|---|---|
O_RDONLY | 没有写端 | 阻塞等待 |
O_RDONLY | O_NONBLOCK | 没有写端 | 立即成功 |
O_WRONLY | 没有读端 | 阻塞等待 |
O_WRONLY | O_NONBLOCK | 没有读端 | 立即失败,errno 为 ENXIO |
为什么读端非阻塞打开时,即使没有写端也能成功?
因为只读打开本身不需要等待写端建立连接。此时如果立即调用 read(),并且当前没有数据、也没有写端,通常会读到 0。这与普通的阻塞读取不同:如果仍然存在写端,只是暂时没有数据,那么非阻塞读取通常会返回 -1,并将 errno 设置为 EAGAIN 或 EWOULDBLOCK。
这两个场景要区分清楚:
- 没有数据,但仍有写端: 暂时没有内容可读。
- 没有数据,也没有写端: 当前没有发送方,
read()可以返回0。
六、实战一:使用命名管道实现文件拷贝
理论讲完了,我们来解决一个实际问题。
假设当前目录下有一个文件 abc,里面存放着一段文本。我们想将它复制为 abc.bak。
平常复制文件,可以使用 cp 命令。那么,如果不直接让一个程序从源文件读取后写入目标文件,而是让两个独立程序通过命名管道完成数据传输,该怎么做?
我们可以把任务拆分成两部分:
- 程序 A:读取
abc,将读取到的数据写入 FIFO。- 程序 B:从 FIFO 读取数据,将数据写入
abc.bak。
整体流程如下:

6.1 第一个程序:读取源文件,写入命名管道
先实现程序 A。
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
#include <cstdio>
#include <cerrno>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
int main()
{
// 创建命名管道;如果已经存在,后面还可以继续使用
if (mkfifo("tp", 0644) == -1 && errno != EEXIST)
ERR_EXIT("mkfifo");
// 打开源文件,只读
int infd = open("abc", O_RDONLY);
if (infd == -1)
ERR_EXIT("open abc");
// 打开 FIFO,只写;没有读端时会阻塞等待
int outfd = open("tp", O_WRONLY);
if (outfd == -1)
ERR_EXIT("open tp");
char buf[1024];
ssize_t n;
// 每次读取一部分数据,再写入 FIFO
while ((n = read(infd, buf, sizeof(buf))) > 0)
{
ssize_t sent = 0;
// write 可能只写入部分数据,因此循环写完当前数据块
while (sent < n)
{
ssize_t ret = write(outfd, buf + sent, n - sent);
if (ret == -1)
{
if (errno == EINTR)
continue;
ERR_EXIT("write tp");
}
sent += ret;
}
}
// read 返回 -1 表示读取出错
if (n == -1)
ERR_EXIT("read abc");
close(infd);
close(outfd);
return 0;
}
首先,open("abc", O_RDONLY) 打开源文件。接着,open("tp", O_WRONLY) 打开命名管道的写端。如果此时还没有程序打开读端,当前程序就会阻塞等待。
进入循环后,read() 每次最多读取 1024 字节。读取多少,就向管道写入多少。文件很大也没有关系,不需要一次性将整个文件读入内存。
这里特意处理了一个容易忽略的问题:write() 不一定一次就能写完要求的数据,因此要检查实际写入的字节数,并继续写入剩余部分。
6.2 第二个程序:读取命名管道,写入目标文件
程序 B 负责接收数据,并将数据写入 abc.bak。
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
#include <cstdio>
#include <cerrno>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
int main()
{
// 创建或复用已经存在的 FIFO
if (mkfifo("tp", 0644) == -1 && errno != EEXIST)
ERR_EXIT("mkfifo");
// 创建目标文件;若已存在,则清空原内容
int outfd = open("abc.bak",
O_WRONLY | O_CREAT | O_TRUNC,
0644);
if (outfd == -1)
ERR_EXIT("open abc.bak");
// 打开 FIFO,只读;没有写端时会阻塞等待
int infd = open("tp", O_RDONLY);
if (infd == -1)
ERR_EXIT("open tp");
char buf[1024];
ssize_t n;
// 读取 FIFO,直到所有写端关闭且数据读完
while ((n = read(infd, buf, sizeof(buf))) > 0)
{
ssize_t sent = 0;
// 确保当前读到的数据全部写入目标文件
while (sent < n)
{
ssize_t ret = write(outfd, buf + sent, n - sent);
if (ret == -1)
{
if (errno == EINTR)
continue;
ERR_EXIT("write abc.bak");
}
sent += ret;
}
}
if (n == -1)
ERR_EXIT("read tp");
close(infd);
close(outfd);
// 删除 FIFO 的文件系统名称
if (unlink("tp") == -1)
perror("unlink tp");
return 0;
}
open() 的几个选项:
O_WRONLY:只写。O_CREAT:目标文件不存在时创建它。O_TRUNC:目标文件已存在时,将其原有内容截断为零长度。
为什么程序 B 要等到 read() 返回 0 才结束?
因为 FIFO 是数据流。只要还有写端打开,读取端就不能仅仅因为当前暂时没有数据,就认定通信结束。
当所有写端都关闭,并且 FIFO 中剩余的数据已经读取完毕时,read() 才会返回 0,告诉程序已经到达当前数据流的末尾。
七、实战二:使用命名管道实现 Server 和 Client 通信
前面的文件拷贝让我们理解了命名管道的数据传递过程。但在实际开发中,更常见的学习场景是让两个程序持续交换信息。
现在我们实现一个简单的通信程序:
server:服务器,负责接收并显示客户端消息。client:客户端,负责从键盘读取输入,再发送给服务器。fifo:双方共同使用的命名管道。
最终效果类似这样:

这个例子仍然是单向通信:客户端发送,服务器接收。如果想让服务器也能主动回复客户端,可以再设计一个用于反向传输的 FIFO。
7.1 先设计公共代码 comm.hpp
我们先把创建管道、打开管道、读写数据等操作封装成类,避免客户端和服务器各自重复编写相同的代码。
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// 负责创建和删除命名管道
class NamedFifo
{
public:
NamedFifo(const std::string& path, const std::string& name)
: _fifoname(path + "/" + name), _created(false)
{
// 如果管道已存在,不重复创建
if (mkfifo(_fifoname.c_str(), 0666) == 0)
{
_created = true;
std::cout << "mkfifo success: "
<< _fifoname << std::endl;
}
else if (errno == EEXIST)
{
// 已存在的路径必须确实是 FIFO
struct stat st;
if (lstat(_fifoname.c_str(), &st) == -1 ||
!S_ISFIFO(st.st_mode))
{
std::cerr << "path exists but is not a FIFO"
<< std::endl;
std::exit(EXIT_FAILURE);
}
std::cout << "FIFO already exists: "
<< _fifoname << std::endl;
}
else
{
perror("mkfifo");
std::exit(EXIT_FAILURE);
}
}
~NamedFifo()
{
// 只删除本对象创建的 FIFO,避免误删别人已有的路径
if (_created && unlink(_fifoname.c_str()) == -1)
perror("unlink FIFO");
}
private:
std::string _fifoname;
bool _created;
};
// 负责打开、读取和写入命名管道
class FileOper
{
public:
FileOper(const std::string& path, const std::string& name)
: _fifoname(path + "/" + name), _fd(-1)
{
}
void OpenForRead()
{
// 阻塞模式下,等待写端打开 FIFO
_fd = open(_fifoname.c_str(), O_RDONLY);
if (_fd == -1)
perror("open FIFO for read");
else
std::cout << "open FIFO for read success"
<< std::endl;
}
void OpenForWrite()
{
// 阻塞模式下,等待读端打开 FIFO
_fd = open(_fifoname.c_str(), O_WRONLY);
if (_fd == -1)
perror("open FIFO for write");
else
std::cout << "open FIFO for write success"
<< std::endl;
}
void Read()
{
if (_fd == -1)
{
std::cerr << "FIFO is not open" << std::endl;
return;
}
char buffer[1024];
while (true)
{
// 留一个字节存放字符串结束符
ssize_t n = read(_fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << "client says: " << buffer;
std::cout.flush();
}
else if (n == 0)
{
// 所有写端关闭,且管道数据已经读完
std::cout << "client closed the pipe"
<< std::endl;
break;
}
else if (errno == EINTR)
{
// 被信号打断,继续读取
continue;
}
else
{
perror("read FIFO");
break;
}
}
}
void Write()
{
if (_fd == -1)
{
std::cerr << "FIFO is not open" << std::endl;
return;
}
std::string message;
while (true)
{
std::cout << "Please enter#: ";
if (!std::getline(std::cin, message))
break;
if (message == "quit")
break;
// 加上换行符,方便服务器显示消息
message += '\n';
// 保证当前消息全部写入 FIFO
size_t sent = 0;
while (sent < message.size())
{
ssize_t n = write(
_fd,
message.data() + sent,
message.size() - sent
);
if (n == -1 && errno == EINTR)
continue;
if (n <= 0)
{
perror("write FIFO");
return;
}
sent += static_cast<size_t>(n);
}
}
}
void Close()
{
if (_fd != -1)
{
close(_fd);
_fd = -1;
}
}
~FileOper()
{
// 析构时兜底关闭文件描述符
Close();
}
private:
std::string _fifoname;
int _fd;
};
这里使用了两个类,各自负责不同的工作。
NamedFifo 负责管道的创建和清理。FileOper 负责打开管道以及通过文件描述符进行读写。
将它们分开,是为了让职责更加清晰:管道的生命周期管理与数据传输操作不必混在一起。
需要特别留意 NamedFifo 的析构函数:只有当前对象成功创建了 FIFO,才会尝试删除它。如果管道原本就存在,这个对象不会擅自删除它。
不过,在多进程程序中,如果某个进程创建了 FIFO,另一个进程正在使用它,创建者退出时删除路径仍然可能影响后续进程重新打开它。因此,实际项目应当明确规定由谁负责创建和删除 FIFO,并协调好清理时机。
7.2 客户端 client.cc
客户端不需要创建管道。它只需要打开服务器已经准备好的 FIFO,然后不断读取用户输入并发送消息。
#include "comm.hpp"
int main()
{
// 创建文件操作对象,指定当前目录下的 fifo
FileOper writerfifo(".", "fifo");
// 打开 FIFO 写端;服务器需要先打开读端
writerfifo.OpenForWrite();
// 如果打开失败,就不要继续发送
writerfifo.Write();
// 关闭文件描述符
writerfifo.Close();
return 0;
}
客户端代码很短,因为实际读写逻辑已经封装在 FileOper 中。
其中,"." 表示当前目录,"fifo" 表示管道名称,两者组合后得到 ./fifo。
当客户端输入普通文本时,消息会被写入管道;输入 quit 时,客户端退出输入循环,关闭写端。
这里的 quit 只是客户端自己约定的退出命令,并不会作为消息发送给服务器。
7.3 服务器 server.cc
服务器负责创建命名管道,并打开读端等待客户端消息。
#include "comm.hpp"
int main()
{
// 创建 FIFO;如果已经存在,则验证并复用
NamedFifo fifo(".", "fifo");
// 创建文件操作对象
FileOper readerfifo(".", "fifo");
// 打开读端,等待客户端打开写端
readerfifo.OpenForRead();
// 循环接收并显示客户端发送的消息
readerfifo.Read();
// 关闭文件描述符
readerfifo.Close();
return 0;
}
服务器启动后,先创建 fifo,再调用 OpenForRead()。
如果客户端还没有启动,服务器会在打开读端时等待。客户端启动后,打开同一个 FIFO 的写端,双方就能建立通信关系。
服务器接收到数据后,通过 Read() 输出消息。当所有写端关闭且管道中的数据已经读完时,read() 返回 0,服务器退出读取循环,随后关闭文件描述符。
这里有一个需要明确的细节:服务器的 NamedFifo 对象在 main() 结束时析构。如果它创建了 FIFO,就会尝试删除 FIFO 的名称。因此,这份示例适合展示单个客户端完成一轮通信的基本过程,不是完整的多客户端服务器实现。
7.4 Makefile
我们可以通过 Makefile 一次编译客户端和服务器。
.PHONY: all clean
all: client server
client: client.cc comm.hpp
g++ -o $@ client.cc -std=c++11
server: server.cc comm.hpp
g++ -o $@ server.cc -std=c++11
clean:
rm -f client server
这里有几个细节:
all是默认目标,执行make时会编译client和server。$@表示当前目标名称,例如client。- 将
comm.hpp加入依赖列表后,修改头文件也会触发对应程序重新编译。clean用于删除编译生成的可执行文件,不会删除 FIFO。
八、把整个过程串起来:命名管道到底是怎么工作的?
前面分别学习了创建命名管道、打开规则、文件拷贝以及客户端和服务器通信。现在我们把这些知识点串联起来,形成一个完整的理解。
假设服务器和客户端分别运行在两个终端中。
8.1 命名管道通信的完整流程

九、使用命名管道时,还有哪些问题需要注意?
9.1 命名管道是文件,为什么不能像普通文件一样读写?
因为它属于特殊文件。
普通文件的主要用途是保存数据,而 FIFO 的主要用途是提供进程间的数据传输通道。虽然它们都可以使用 open()、read()、write() 和 close() 等接口,但底层行为并不完全相同。
例如,FIFO 通常不能像普通文件那样使用 lseek() 随意移动读写位置。
9.2 命名管道中的数据会一直保存吗?
不会。
FIFO 中的数据由内核管理,读取端读走数据后,这些数据就从管道的数据流中消耗掉了。
如果希望数据能够长期保存,就应该使用普通文件或其他持久化存储方式,而不是把 FIFO 当作文件仓库。
9.3 为什么服务器有时收不到完整的一条消息?
因为 FIFO 是字节流,而不是自动划分消息的队列。
例如,客户端执行两次写入:
write(fd, "Hello", 5);
write(fd, "World", 5);
服务器可能一次读取 10 个字节,也可能先读到 "Hello",再读到 "World",还可能在不同的读取操作中取得部分数据。
因此,不能简单地认为:
一次 write() = 一次 read()
如果要传输具有明确结构的消息,就需要设计协议,而不能依赖每次读取都恰好对应一次写入。
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/Huang_jin007/article/details/168976427




