1. 匿名管道的限制
匿名管道存在以下核心限制:
- 仅限亲缘关系进程:只能用于父子进程等有血缘关系的进程间通信(如通过
fork()
创建的子进程)。 - 单向通信:数据只能单向流动(一端写,另一端读),双向通信需创建两个管道。
- 临时性:存在于内存中,进程结束后自动销毁。
- 缓冲区有限:大小固定(通常为一个内存页,如4KB),易写满阻塞。
引入命名管道的原因:
为解决匿名管道的局限性,命名管道允许任意进程(无论是否有亲缘关系)通过文件系统路径访问,实现跨进程通信。
2. 什么是命名管道
命名管道(Named Pipe/FIFO)是一种特殊的文件类型,特点包括:
- 文件系统可见:通过路径名(如
/tmp/myfifo
)标识,任何进程可访问。 - 遵循FIFO原则:数据按写入顺序读取,严格保持先进先出。
- 突破亲缘限制:不相关进程可通过路径名打开同一管道通信。
- 双向通信支持:部分场景下支持读写双向操作(需显式设计)。
示例:命名管道在文件系统中显示为特殊文件(权限位带
p
,如prw-r--r--
)。
3. 如何创建命名管道
方法一:命令行创建
mkfifo <路径名> # 例如:mkfifo /tmp/my_pipe
生成一个具名管道文件,权限默认受 umask
影响。
示例:
方法二:程序内创建
使用 mkfifo()
函数:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 成功返回0,失败返回-1
- 参数:
pathname
:管道路径(如/tmp/my_pipe
)。mode
:权限标志(如0666
表示所有用户可读写)。
- 后续操作:
- 需用
open()
打开管道(读模式O_RDONLY
或写模式O_WRONLY
)。 - 默认阻塞行为:读端打开时写端阻塞,反之亦然;可通过
O_NONBLOCK
设为非阻塞。
- 需用
删除管道:
- 命令行:
rm <路径名>
或unlink <路径名>
。- 程序内:
unlink(pathname)
。
4. 匿名管道和命名管道的区别
特性 | 匿名管道 | 命名管道 | 证据来源 |
---|---|---|---|
创建方式 | pipe(fd) 一步创建并打开 | mkfifo() 创建 + open() 打开 | |
进程关系要求 | 必须具有亲缘关系(如父子进程) | 任意进程均可访问 | |
持久性 | 随进程结束销毁 | 文件系统持久,需手动删除 | |
通信方向 | 仅单向 | 可支持双向通信 | |
性能 | 略快(无文件系统操作) | 稍慢(涉及磁盘索引节点) | |
使用场景 | 短期亲缘进程通信 | 长期/跨进程通信(如C/S架构) |
关键补充
- 语义一致性:打开后两者操作方式相同(如
read()
/write()
)。 - 网络支持:命名管道可跨机器通信,匿名管道仅限本地。
- 阻塞行为:两者均受缓冲区影响,但命名管道可通过
O_NONBLOCK
灵活控制阻塞。
5. 命名管道的打开规则
一、为读而打开 FIFO(O_RDONLY
)
O_NONBLOCK
未设置(默认阻塞)- 行为:调用
open()
会阻塞当前进程,直到有另一个进程为写而打开同一 FIFO。 - 原理:内核需确保存在数据生产者,否则读操作无意义。
- 行为:调用
"open以只读方式打开FIFO时,要阻塞到某个进程为写而打开此FIFO" 。
"若没有指定O_NONBLOCK,只读 open 要阻塞到某个其他进程为写而打开此 FIFO" 。
O_NONBLOCK
设置(非阻塞)- 行为:
open()
立即成功返回(返回文件描述符),无论是否有写端打开。 - 后续注意:此时若管道无数据,
read()
可能返回 0(EOF)或EAGAIN
错误(见下文读写规则)。
- 行为:
"先以只读方式打开,如果没有进程已经为写而打开一个FIFO,只读 open() 成功,并且 open() 不阻塞" 。
"若指定了O_NONBLOCK,则只读 open 立即返回" 。
二、为写而打开 FIFO(O_WRONLY
)
O_NONBLOCK
未设置(默认阻塞)- 行为:调用
open()
会阻塞当前进程,直到有另一个进程为读而打开同一 FIFO。 - 原理:内核需确保存在数据消费者,否则写操作可能无限等待。
- 行为:调用
"open以只写方式打开FIFO时,要阻塞到某个进程为读而打开此FIFO" 。
"只写 open 要阻塞到某个其他进程为读而打开它" 。
O_NONBLOCK
设置(非阻塞)- 行为:若无读端已打开,
open()
立即失败,返回-1
并设置错误码ENXIO
(表示设备不存在)。 - 行为:若已有读端打开,则
open()
成功。
- 行为:若无读端已打开,
"先以只写方式打开,如果没有进程已经为读而打开一个FIFO,只写 open() 将出错返回 -1" 。
"若指定了O_NONBLOCK,则只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO" 。
三、关键补充与深度解析
O_RDWR
(读写模式)的特殊性- 行为:以
O_RDWR
模式打开时 永不阻塞,因进程自身已同时打开读写端 。 - 风险:可能导致自我死锁(如写满后读阻塞),实践中极少使用。
- 行为:以
读写操作的阻塞行为(与
open
独立)操作 O_NONBLOCK
未设置O_NONBLOCK
设置read()
空管道阻塞直到有数据写入 立即返回 EAGAIN
(或空数据)write()
满管道阻塞直到有空间 部分写入或返回 EAGAIN
管道断裂与信号处理
- 写端关闭:读端
read()
返回 0(EOF),不阻塞 。 - 读端关闭:写端
write()
触发SIGPIPE
信号(默认终止进程),错误码EPIPE
。
- 写端关闭:读端
原子性与
PIPE_BUF
- 规则:写入 ≤
PIPE_BUF
字节的数据保证原子性(不与其他进程交织)。 - 典型值:Linux 中
PIPE_BUF
为 4096 字节(一页大小)。
- 规则:写入 ≤
四、内核实现原理(选读)
- 阻塞的本质
- 进程休眠在 FIFO inode 的等待队列中,由另一端打开或数据变动时唤醒 。
- 示例:
// Linux 内核片段(读打开阻塞逻辑) if (PIPE_READERS(*inode)++ == 0) wait_for_partner(inode, &PIPE_WCOUNTER(*inode)); // 等待写端
- 非阻塞的冲突处理
- 写打开时若无读端,内核直接返回
ENXIO
而非加入等待队列 :
- 写打开时若无读端,内核直接返回
"若命名管道读端尚未打开,而 O_NONBLOCK=1,写端打开失败并释放资源" 。
总结与建议
场景 | 打开模式 | O_NONBLOCK | 结果 |
---|---|---|---|
读打开,无写端存在 | O_RDONLY | 未设置 | 阻塞 |
读打开,无写端存在 | O_RDONLY | 设置 | 立即成功 |
写打开,无读端存在 | O_WRONLY | 未设置 | 阻塞 |
写打开,无读端存在 | O_WRONLY | 设置 | 立即失败(ENXIO) |
读写打开 | O_RDWR | 任意 | 立即成功(不依赖外部进程) |
工程建议:
- 生产-消费模型:推荐读端阻塞打开(确保写端就绪),写端非阻塞打开(快速失败+重试逻辑)。
- 超时控制:若需阻塞但避免无限等待,结合
select()
/poll()
设置超时。 - 错误处理:始终检查
open()
返回值和errno
,尤其非阻塞模式。
6. 代码示例
下面为了更好理解命名管道,我们直接来一段代码,使用命名管道让两个无血缘关系的进程进行通信——一个进程写一个进程读。
这里client.cc和server.cc代表两个没有血缘关系的进程,在前面学习进程时我们知道,.cc文件跑起来就是一个进程,所以这里不多赘述。而我们命名管道的创建,以及打开管道文件进行操作的代码则封装在comm.hpp中。Makefile则是我们配置的自动化工具。
下面我们就来在comm.hpp中将代码封装起来
首先需要将命名管道创建,最后结束通信后还需要将管道回收,因为命名管道不会随进程的生命周期,所以需要我们手动回收
代码如下:
class NamedFifo
{
public:NamedFifo(const std::string &path, const std::string &name): _path(path), _name(name){_filename = _path + "/" + _name;// 创建命名管道int n = mkfifo(_filename.c_str(), 0666);if(n < 0){std::cerr << "mkfifo failed" << std::endl;}else{std::cout << "mkfifo success" << std::endl;}}~NamedFifo(){// 回收命名管道int n = unlink(_filename.c_str());if(n < 0){std::cerr << "remove fifo failed" << std::endl;}else{std::cout << "remove fifo success" << std::endl;}}private:std::string _path;std::string _name;std::string _filename;
};
由于我们要实现一个进程写,一个进程读的单向通信,所以我们先规定,让客户端client.cc进程来写,服务端server.cc进程来读,那么读写操作我们还需要再封装一个类,因为我们只要创建一个管道就行了。
如果都封装在一个类中,那么客户端和服务端都需要实例化出一个对象,才能对管道读写通信,但这样就会创建两个命名管道了,因为只要构造函数就会创建命名管道,而我们不需要两个命名管道,我们只需要创建一个命名管道,然后服务端和客户端分别以读写的方式打开这个管道文件就可以进行通信了,所以我们可以再封装一个类来实现对打开的命名管道进行操作。
代码如下:
class Fileoper
{
public:Fileoper(const std::string &path, const std::string &name): _path(path), _name(name), _fd(-1){_filename = _path + "/" + _name;}void OpenForRead(){_fd = open(_filename.c_str(), O_RDONLY);if(_fd < 0){std::cerr << "open fifo failed" << std::endl;}else{std::cout << "open fifo success" << std::endl;}}void OpenForWrite(){_fd = open(_filename.c_str(), O_WRONLY);if(_fd < 0){std::cerr << "open fifo failed" << std::endl;}else{std::cout << "open fifo success" << std::endl;}}~Fileoper() {}private:std::string _path;std::string _name;std::string _filename;int _fd;
};
由于我们需要打开指定路径的管道文件,所以成员变量仍然需要和NamedFifo类一样,但是我们打开管道文件后,需要通过返回的文件描述符后续管理规管道文件,所以我们还需要一个成员变量_fd,来接收open返回的文件描述符。客户端需要从管道写入,服务端需要从管道读取,所以客户端以只写的方式打开管道文件,而服务端以只读的方式打开管道文件。但是打开之后我们客户端和服务端还需要对管道进行读写操作,所以我们还需要分别实现一个写函数和一个读函数
代码如下:
void Write(){std::string message;while(true){std::cout << "Please Enter#";std::getline(std::cin, message);write(_fd, message.c_str(), message.size());}}void Read(){while(true){char buffer[1024];ssize_t n = read(_fd, buffer, sizeof(buffer)-1);if(n > 0){buffer[n] = 0;std::cout << "Client say#" << buffer << std::endl;}else if(n == 0){std::cout << "Client quit! me too!" << std::endl;break;}else{std::cerr << "read error" << std::endl;break;}}}
当然,通信结束之后我们需要关闭文件描述符
void Close(){close(_fd);}
测试
我们先定义两个宏
#define PATH "."
#define FILENAME "fifo"
我们想要在当前路径下创建一个fifo的管道文件
服务端:
#include "comm.hpp"int main()
{// 创建管道NamedFifo f(PATH, FILENAME);// 文件操作Fileoper reader(PATH, FILENAME);reader.OpenForRead();reader.Read();reader.Close();return 0;
}
客户端:
#include "comm.hpp"int main()
{Fileoper Writer(PATH, FILENAME);Writer.OpenForWrite();Writer.Write();Writer.Close(); return 0;
}
运行测试:
可以看到成功实现了两个没有血缘关系的进程的单向通信
源码:
comm.hpp:
#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>#define PATH "."
#define FILENAME "fifo"class NamedFifo
{
public:NamedFifo(const std::string &path, const std::string &name): _path(path), _name(name){_filename = _path + "/" + _name;// 创建命名管道int n = mkfifo(_filename.c_str(), 0666);if (n < 0){std::cerr << "mkfifo failed" << std::endl;}else{std::cout << "mkfifo success" << std::endl;}}~NamedFifo(){// 回收命名管道int n = unlink(_filename.c_str());if (n < 0){std::cerr << "remove fifo failed" << std::endl;}else{std::cout << "remove fifo success" << std::endl;}}private:std::string _path;std::string _name;std::string _filename;
};class Fileoper
{
public:Fileoper(const std::string &path, const std::string &name): _path(path), _name(name), _fd(-1){_filename = _path + "/" + _name;}void OpenForRead(){_fd = open(_filename.c_str(), O_RDONLY);if(_fd < 0){std::cerr << "open fifo failed" << std::endl;}else{std::cout << "open fifo success" << std::endl;}}void OpenForWrite(){_fd = open(_filename.c_str(), O_WRONLY);if(_fd < 0){std::cerr << "open fifo failed" << std::endl;}else{std::cout << "open fifo success" << std::endl;}}void Write(){std::string message;while(true){std::cout << "Please Enter#";std::getline(std::cin, message);write(_fd, message.c_str(), message.size());}}void Read(){while(true){char buffer[1024];ssize_t n = read(_fd, buffer, sizeof(buffer)-1);if(n > 0){buffer[n] = 0;std::cout << "Client say#" << buffer << std::endl;}else if(n == 0){std::cout << "Client quit! me too!" << std::endl;break;}else{std::cerr << "read error" << std::endl;break;}}}void Close(){close(_fd);}~Fileoper() {}private:std::string _path;std::string _name;std::string _filename;int _fd;
};