目录
- 一、命名管道
-
- [1.1 创建命名管道](#1.1 创建命名管道)
- [1.2 测试代码](#1.2 测试代码)
-
- [1.2.1 创建管道](#1.2.1 创建管道)
- [1.2.2 删除管道](#1.2.2 删除管道)
- [1.2.3 判断命名管道是否存在](#1.2.3 判断命名管道是否存在)
- [1.2.4 打开管道](#1.2.4 打开管道)
- [1.2.5 发送 和 读取 数据](#1.2.5 发送 和 读取 数据)
- [1.3 源代码](#1.3 源代码)

个人主页:矢望
个人专栏:C++、Linux、C语言、数据结构、Coze-AI、MySQL
一、命名管道
上期博客讲解的匿名管道只适用于具有血缘关系的进程之间进行通信,如果两个进程之间毫无关系,应该如何通信呢?
这个时候就可以通过命名管道进行通信。
如果现在有两个没有血缘关系的进程A、B。它们要打开磁盘上的同一个文件,例如log.txt,那么此时OS会给这两个进程同时创建两份相关文件的核心数据结构和缓冲区吗,答案是否定的。OS只会创建一份,否则就造成了资源的浪费。
所以A、B两个进程就会各自拥有自己的文件描述符表,然后映射到同一份数据结构和缓冲区,这个时候就满足了让不同的进程看到同一份资源。
因此匿名管道和命名管道的原理是相同的。命名管道是一种特殊类型的文件。
那么如何保证两个进程访问的是统一份文件呢? 文件是通过路径+文件名进行唯一标识的,所以只需要保证访问同一个路径下的同一份文件就可以保证访问到的是同一个文件。所以这个文件一定有文件名。
我们两个进程之间进行通信的消息不需要向磁盘做刷新,而进程在向普通文件写的时候,缓冲区中的内容是要被刷新到磁盘的,所以这个文件肯定和普通文件不同,它是Linux上的管道文件。
1.1 创建命名管道
mkfifo是 Linux 系统中用于创建命名管道named pipe的命令和系统调用。

创建命名管道:

如上图,文件类型是管道文件,并且有唯一标识符inode编号。
通信演示 :

1.2 测试代码
现在想要编写一个代码,代码演示的是两个进程,一个是Client,一个是Server,Client给命名管道写,Server读数据。
同时生成两个可执行程序的Makefile。


如上图,同时生成两个可执行程序。
由于这两个进程都要进行管道操作,所以我们可以再创建一个文件,这个文件中可以使用类实现统一的管道操作。

这两个进程一个要写,一个要读,所以在类中就需要有方法函数去创建管道和打开管道。
1.2.1 创建管道
相关函数mkfifo。

返回值,创建成功返回0,失败返回-1。如下图。

sql
class Fifo
{
public:
// ...
// 1. 创建管道
void Build()
{
int n = mkfifo(_commfile.c_str(), _mode);
if(n < 0)
{
std::cerr << "mkfifo error: " << strerror(errno) << std::endl;
exit(1);
}
std::cout << "创建命名管道成功..." << std::endl;
}
private:
std::string _commfile; // 命名管道名称
mode_t _mode = 0666; // 打开文件的权限
};
sql
#include "PiPe.hpp"
int main()
{
Fifo pipefile;
pipefile.Build(); // 创建管道
return 0;
}
代码测试 :

如上图,管道创建出来了。
而当我们再次运行Client时就会报错。

如上图,因为文件已经存在了。
所以我们还需要一个删除管道的成员函数。
1.2.2 删除管道
可以使用函数unlink删除文件。

sql
// 3. 删除管道
void Delete()
{
int n = unlink(_commfile.c_str());
(void)n; // 防止编译器告警
}
sql
#include "PiPe.hpp"
int main()
{
Fifo pipefile;
pipefile.Build(); // 创建管道
sleep(5);
pipefile.Delete(); // 删除管道
return 0;
}
运行测试 :

现在我们能够创建和删除文件了,但还有一个细节没有处理,就是我们的管道如果是存在的,我们就不需要再创建了,也就是我们还需要一个判断命名管道是否存在的接口。
1.2.3 判断命名管道是否存在
这里可以使用stat函数。stat() 函数用于获取指定路径的文件状态信息,如果成功获取状态就证明了文件是存在的 。这个函数成功时返回0,失败返回-1。

使用这个函数之前需要定义结构体变量,用于存储文件状态信息。
struct stat 结构体常用成员:
c
struct stat {
mode_t st_mode; // 文件类型和权限(如 S_IFIFO 表示管道)
ino_t st_ino; // inode 编号
off_t st_size; // 文件大小(字节)
uid_t st_uid; // 所有者用户 ID
gid_t st_gid; // 所有者组 ID
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后状态改变时间
// ... 其他成员
};
判断函数:
cpp
class Fifo
{
public:
// 1. 创建管道
void Build()
{
// 管道存在
if(isExists()) return;
int n = mkfifo(_commfile.c_str(), _mode);
if(n < 0)
{
std::cerr << "mkfifo error: " << strerror(errno) << std::endl;
exit(1);
}
std::cout << "创建命名管道成功..." << std::endl;
}
// 3. 删除管道
void Delete()
{
// 管道不存在
if(!isExists()) return;
int n = unlink(_commfile.c_str());
(void)n; // 防止编译器告警
std::cout << "删除命名管道成功..." << std::endl;
}
private:
bool isExists()
{
struct stat st; // 定义结构体变量,用于存储文件状态信息
int n = stat(_commfile.c_str(), &st);
// stat() 函数用于获取指定路径的文件状态信息
// 参数1: 文件路径
// 参数2: 指向 struct stat 的指针,用于接收状态信息
if(n == 0) return true; // 成功获取文件状态
else return false;
}
std::string _commfile; // 命名管道名称
mode_t _mode = 0666; // 打开文件的权限
};
cpp
#include "PiPe.hpp"
int main()
{
Fifo pipefile;
pipefile.Build(); // 创建管道
sleep(5);
pipefile.Delete(); // 删除管道
return 0;
}
运行测试 :

如上,由于运行之前管道文件存在,所以并没有创建,而是直接删除了。
再次运行查看 :

1.2.4 打开管道
打开管道各自有各自的方式,所以需要一个成员变量_fd。
open函数:

cpp
#define ForRead 1
#define ForWrite 2
class Fifo
{
public:
// 2. 打开管道
void Open(int mode)
{
if(mode & ForRead)
_fd = open(_commfile.c_str(), O_RDONLY);
else if(mode & ForWrite)
_fd = open(_commfile.c_str(), O_WRONLY);
else
{}
if(_fd < 0)
{
std::cerr << "open error: " << strerror(errno) << std::endl;
exit(2);
}
else std::cout << "open file success..." << std::endl;
}
private:
std::string _commfile; // 命名管道名称
mode_t _mode = 0666; // 打开文件的权限
int _fd = -1; // 当前进程打开管道时的文件描述符
};
Server:
cpp
#include "PiPe.hpp"
int main()
{
// 创建 && 以读方式打开
Fifo pipefile;
pipefile.Build(); // 创建管道
pipefile.Open(ForRead);
return 0;
}
Client:
cpp
#include "PiPe.hpp"
int main()
{
// 以写方式打开
Fifo file_client;
file_client.Open(ForWrite);
return 0;
}
现在两个进程都以各自的方式打开了同一个命名管道接下来就是写入和读出数据。
1.2.5 发送 和 读取 数据
cpp
// 发送数据
void Send(std::string& msgin)
{
ssize_t n = write(_fd, msgin.c_str(), msgin.size());
(void)n; // 防止编译器告警
}
// 读取数据
int Recv(std::string& msgout)
{
char buffer[256];
ssize_t n = read(_fd, buffer, sizeof buffer - 1); // -1 保证给 '\0' 留出位置
if(n > 0)
{
buffer[n] = '\0';
msgout = buffer;
return n;
}
else if(n == 0) return n; // 读到文件末尾
else return n; // 读取发生错误
}
Client:
cpp
// 以写方式打开
Fifo file_client;
file_client.Open(ForWrite);
std::string msg;
while(true)
{
std::cout << "输入信息: ";
std::getline(std::cin, msg);
file_client.Send(msg); // 发送消息
}
Server:
cpp
// 创建 && 以读方式打开
Fifo pipefile;
pipefile.Build(); // 创建管道
pipefile.Open(ForRead);
std::string msg;
while(true)
{
int n = pipefile.Recv(msg); // 读取消息
if(n > 0)
{
std::cout << "Client Say # " << msg << std::endl;
}
else
{
break;
}
}
// 删除管道
pipefile.Delete();
运行测试:
这里发现了一个现象:


如上图,我们的打印在open的后面。
为什么读端打开时候不直接向后走呢?
如果直接向后走,那么就到读数据了,如果写端没有打开,那就直接读到0了,就出错了,堵塞是为了区分对方读关闭或者写关闭的情况。因此,open 的阻塞机制将双方同步在通信的起点,保证只有当连接真正建立时,后续的读写操作才有意义。这避免了程序处理半连接状态的复杂性。
区分写端关闭 :当读端正在运行时,如果写端进程退出(无论正常还是异常),读端正在进行的 read 调用会立即返回 0。程序据此可以判断对方已关闭,并可以相应地退出或进行清理。
区分读端关闭 :当写端正在运行时,如果读端进程退出,写端正在进行的 write 调用会触发 SIGPIPE 信号。进程如果不处理此信号,默认会退出。程序也可以通过处理信号或检查 write 的返回值来得知读端已关闭。
open阻塞规则 :O_RDONLY 打开:阻塞直到有进程以 O_WRONLY 打开;O_WRONLY 打开:阻塞直到有进程以 O_RDONLY 打开。
再次运行 :

如上图,通信成功。当写端对应的进程退出也就是写端退出时,读端就读到0了,读端进程也退出。
1.3 源代码
PiPe.hpp:
cpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define ForRead 1
#define ForWrite 2
const std::string comm_file = "./fifo";
class Fifo
{
public:
Fifo(const std::string& commfile = comm_file)
:_commfile(commfile)
{}
// 1. 创建管道
void Build()
{
// 管道存在
if(isExists()) return;
int n = mkfifo(_commfile.c_str(), _mode);
if(n < 0)
{
std::cerr << "mkfifo error: " << strerror(errno) << std::endl;
exit(1);
}
std::cout << "创建命名管道成功..." << std::endl;
}
// 2. 打开管道
void Open(int mode)
{
if(mode & ForRead)
_fd = open(_commfile.c_str(), O_RDONLY);
else if(mode & ForWrite)
_fd = open(_commfile.c_str(), O_WRONLY);
else
{}
if(_fd < 0)
{
std::cerr << "open error: " << strerror(errno) << std::endl;
exit(2);
}
else std::cout << "open file success..." << std::endl;
}
// 发送数据
void Send(std::string& msgin)
{
ssize_t n = write(_fd, msgin.c_str(), msgin.size());
(void)n; // 防止编译器告警
}
// 读取数据
int Recv(std::string& msgout)
{
char buffer[256];
ssize_t n = read(_fd, buffer, sizeof buffer - 1); // -1 保证给 '\0' 留出位置
if(n > 0)
{
buffer[n] = '\0';
msgout = buffer;
return n;
}
else if(n == 0) return n; // 读到文件末尾
else return n; // 读取发生错误
}
// 3. 删除管道
void Delete()
{
// 管道不存在
if(!isExists()) return;
int n = unlink(_commfile.c_str());
(void)n; // 防止编译器告警
std::cout << "删除命名管道成功..." << std::endl;
}
~Fifo()
{}
private:
bool isExists()
{
struct stat st; // 定义结构体变量,用于存储文件状态信息
int n = stat(_commfile.c_str(), &st);
// stat() 函数用于获取指定路径的文件状态信息
// 参数1: 文件路径
// 参数2: 指向 struct stat 的指针,用于接收状态信息
if(n == 0) return true; // 成功获取文件状态
else return false;
}
std::string _commfile; // 命名管道名称
mode_t _mode = 0666; // 打开文件的权限
int _fd = -1; // 当前进程打开管道时的文件描述符
};
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~