目录
1.原理
2.创建命名管道
3.使用命名通道实现简单的通信
4.使用创建的命名管道
1.原理
匿名管道没有名称,它们是通过句柄在父进程和子进程之间传递的。这意味着匿名管道只能用于具有父子关系的进程之间。
但如果程序之间没关系,那么这时候就要用到有名管道了,有名管道通过一个名称(通常是一个文件系统中的路径)来标识。这使得任何进程都可以通过该名称来访问管道,而不必是创建管道的进程的子进程。有名管道支持不同进程间的通信,甚至支持跨计算机(网络)的通信。有名管道的生命周期由创建它的进程控制,但即使创建它的进程终止,只要还有进程连接着管道,管道就会继续存在。
命名管道在操作系统中表现为一种特殊类型的文件,它存在于系统的命名空间中,可以像打开文件那样被打开和读写。一旦创建,命名管道就可以在不同的进程中被打开多次,允许单向或双向的数据流传输。
2.创建命名管道
创建命名管道,直接使用mkfifo命令就可以了
eg:
创建一个命名管道
一号机上的
while
循环持续地将字符串"hello boy"
写入到命名管道myfifo
中,每次写入后暂停一秒。二号机上的cat
命令则从myfifo
中读取数据,并将其输出到标准输出。看一看效果:我们发现在一号机写到myfifo中的数据会被同步到二号机中的myfifo,两个不相关的进程(一号机和二号机上的进程)之间建立通信。这两个进程不需要有任何父子关系或其他特殊关系,只需要知道命名管道的文件路径即可。
myfifo的文件大小始终都没有变,因为并没有被刷新到磁盘中。
3.使用命名通道实现简单的通信
提供一个关闭命名管道的函数:unlink
提供一个namepipe的类,它封装了命名管道的创建、打开、读写和删除的逻辑。以下是代码(namedPipe.hpp):
#pragma once #include <iostream> #include <cstdio> #include <cerrno> #include <string> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> const std::string comm_path = "./myfifo"; #define DefaultFd -1 #define Creater 1 #define User 2 #define Read O_RDONLY #define Write O_WRONLY #define BaseSize 4096 class NamePiped { private: bool OpenNamedPipe(int mode) { _fd = open(_fifo_path.c_str(), mode); if (_fd < 0) return false; return true; } public: NamePiped(const std::string &path, int who) : _fifo_path(path), _id(who), _fd(DefaultFd) { if (_id == Creater) { int res = mkfifo(_fifo_path.c_str(), 0666); if (res != 0) { perror("mkfifo"); } std::cout << "creater create named pipe" << std::endl; } } bool OpenForRead() { return OpenNamedPipe(Read); } bool OpenForWrite() { return OpenNamedPipe(Write); } int ReadNamedPipe(std::string *out) { char buffer[BaseSize]; int n = read(_fd, buffer, sizeof(buffer)); if(n > 0) { buffer[n] = 0; *out = buffer; } return n; } int WriteNamedPipe(const std::string &in) { return write(_fd, in.c_str(), in.size()); } ~NamePiped() { if (_id == Creater) { int res = unlink(_fifo_path.c_str()); if (res != 0) { perror("unlink"); } std::cout << "creater free named pipe" << std::endl; } if(_fd != DefaultFd) close(_fd); } private: const std::string _fifo_path; int _id; int _fd; };
- 成员变量:
_fifo_path
:存储命名管道的路径。_id
:标识该对象是命名管道的创建者(Creater
)还是用户(User
)。_fd
:文件描述符,用于与命名管道进行通信。初始化为DefaultFd
(定义为-1)。- 构造函数:
- 接收命名管道的路径和创建者/用户标识。
- 如果
_id
为Creater
,则调用mkfifo
函数在指定路径下创建命名管道。如果创建失败,会打印错误信息。- OpenForRead和OpenForWrite方法:
- 这两个方法分别用于打开命名管道进行读取和写入操作。
- 内部调用
OpenNamedPipe
方法,传入相应的读取或写入模式(O_RDONLY
或O_WRONLY
)。OpenNamedPipe
方法使用open
系统调用来打开命名管道,并保存文件描述符到_fd
成员变量中。- ReadNamedPipe和WriteNamedPipe方法:
ReadNamedPipe
方法从命名管道中读取数据到提供的字符串指针中。WriteNamedPipe
方法将提供的字符串写入命名管道。- 这两个方法都使用
read
和write
系统调用来执行实际的读写操作。- 析构函数:
- 在对象销毁时,析构函数会被调用。
- 如果
_id
为Creater
,则调用unlink
函数来删除命名管道。这确保了命名管道在不再需要时从文件系统中被移除。- 无论
_id
的值如何,都会检查_fd
是否不是DefaultFd
(即文件描述符是否已打开),如果是,则调用close
函数来关闭文件描述符。
接下来创建一个客户端向命名管道写入(client.cc):
#include "namedPipe.hpp" // write int main() { NamePiped fifo(comm_path, User); if (fifo.OpenForWrite()) { std::cout << "client open namd pipe done" << std::endl; while (true) { std::cout << "Please Enter> "; std::string message; std::getline(std::cin, message); fifo.WriteNamedPipe(message); } } return 0; }
循环的写入信息。
创建一个客户端用来读取命名管道的信息(server.cc):
#include "namedPipe.hpp" int main() { NamePiped fifo(comm_path, Creater); // 对于读端而言,如果我们打开文件,但是写还没来,我会阻塞在open调用中,直到对方打开 // 进程同步 if (fifo.OpenForRead()) { std::cout << "server open named pipe done" << std::endl; sleep(3); while (true) { std::string message; int n = fifo.ReadNamedPipe(&message); if (n > 0) { std::cout << "Client Say> " << message << std::endl; } else if(n == 0) { std::cout << "Client quit, Server Too!" << std::endl; break; } else { std::cout << "fifo.ReadNamedPipe Error" << std::endl; break; } } } return 0; }
- 如果
ReadNamedPipe
返回的值n
大于0,表示成功读取了n
个字符到message
中。
- 程序将输出"Client Say> "和读取到的消息内容。
- 如果
n
等于0,通常表示客户端已经关闭了连接或者发送了一个EOF(文件结束符)。
- 程序将输出"Client quit, Server Too!"并退出循环,然后退出程序。
- 如果
n
小于0,表示读取过程中发生了错误。
- 程序将输出"fifo.ReadNamedPipe Error"并退出循环,然后退出程序。
4.使用创建的命名管道
我们先运行了读端程序,但是并没有提示我们的读端创建成功(对于读端而言,如果我们打开文件,但是写还没来,我会阻塞在open调用中,直到对方打开)
打开写端,读端才成功打开。
这就实现进程间的通信了