【Linux】进程间通信(二)命名管道(FIFO)实战指南:从指令操作到面向对象封装的进程间通信实现
文章目录
命名管道
(基于文件+inode的进程间通信方案)
首先我们要清楚,多个进程可以同时打开多个普通文件,OS会为每一个进程都创建一个struct file,但是多个进程共享同一份inode、文件缓冲区和操作方法集,所以只会加载一次文件的属性和内容到struct file的inode和文件缓冲区中,这和父进程打开一个匿名管道并fork一个子进程后父子进程的行为类似。
命名管道就是从上面的普通文件改造而来,最直观的区别就是进程对命名管道的文件缓冲区写数据时,数据不会刷新到磁盘中。
命名管道的操作
指令操作
下面是创建命名管道的指令:

为什么命管道叫做fifo呢?其实管道本质就是一个队列,因为它有先进先出的特性。

我们可以看到,mkfifo创建出来的文件的类型是p,也就是管道文件。
下面我们来尝试用管道来传输数据:

代码操作
Makefile
我们创建两个独立的文件:client.cpp,server.cpp分别表示客户端和服务端。接下来写Makefile:

这里Makefile其实不能实现我们想要的——client.cpp,server.cpp分别编译并生成两个可执行程序,最后只会生成一个可执行程序。
这是因为Makefile本身一次只会形成一个可执行程序,运行时会从上往下扫描,把遇到的第一个目标文件形成可执行程序。
所以我们需要先创建一个只有依赖关系没有依赖方法的伪目标all,它依赖两个可执行程序:client,server,这样Makefile从上往下扫描时遇到的第一个目标文件就是all,然后就会执行all依赖关系中的client,server,这样就能一次创建两个可执行程序了。

创建命名管道
下面是代码层面场景命名管道的库函数调用接口:

因为进程间通信需要不同的进程看到同一份资源,所以我们再创建一个common.hpp文件,把客户端和服务端共享的内容都放到common.hpp中。
#ifndef__COMMON_HPP__#define__COMMON_HPP__#include<iostream>#include<string>#include<sys/types.h>#include<sys/stat.h>#include<unistd.h> std::string fifoname ="fifo"; mode_t mode =0666;#endif下面就需要创建管道了,我们让服务端创建。但是需要注意,创建管道文件是可能失败的,比如管道文件已存在时。
程序结束时还需要删除管道文件,删除文件需要用到unlink,它不仅是系统调用,也是一个指令,可以用unlink指令在命令行删除管道文件。
#include"common.hpp"intmain(){int n =mkfifo(fifoname.c_str(), mode);if(n ==0){ std::cout <<"mkfifo suceessful"<< std::endl;}else{ std::cout <<"mkfifo failed"<< std::endl;}sleep(5);int m =unlink(fifoname.c_str());(void)m;return0;}实现通信
我们准备实现让client发送数据然后server接受数据。下面先实现server端,我们已经创建好管道文件了,下面就需要调用文件的各种系统调用接口打开文件、读取文件、关闭文件,和我们在文件系统介绍的一摸一样,小编就不过多赘述了。
这里小编要补充几点:
1、有关命名管道的操作特点,在打开管道一端,但另一端未打开的时候,open操作会被阻塞,因为如果不阻塞直接打开就有可能读到0。
2、读到的数据我们用字符串数组暂存,并且读取时要预留一个位置给\0,所以read的第三个参数需要sizeof(buffer) - 1,读取完毕后自己手动在字符串末尾添加\0。
3、当read读到0时就意味着client退出了,这时我们server端也需要退出,所以需要对read的返回值进行特殊处理。
//server.cpp#include"common.hpp"intmain(){// 1、创建管道文件int n =mkfifo(fifoname.c_str(), mode);if(n ==0){ std::cout <<"mkfifo suceessful"<< std::endl;}else{// std::cout << "mkfifo failed" << std::endl;perror("mkfifo");exit(1);}// 2、打开管道文件// 命名管道特点,在打开一端,但另一端未打开的时候,open操作会阻塞int fd =open(fifoname.c_str(), O_RDONLY);if(fd <0){perror("open");exit(2);} std::cout <<"open file success"<< std::endl;// 3、读取管道数据char buffer[SIZE]={0};while(true){ buffer[0]=0;// 清空字符串 ssize_t num =read(fd, buffer,sizeof(buffer)-1);if(num >0)// read失败返回-1{ buffer[num]=0;// 保持C风格字符串,末尾加0 std::cout <<"client say# "<< buffer << std::endl;}elseif(num ==0){ std::cout <<"clent quit, me too!"<< std::endl;break;}else{// read错误break;} std::cout <<"num: "<< num << std::endl;}// 4、归还资源close(fd);int m =unlink(fifoname.c_str());(void)m;return0;}然后实现client端,还是平常打开文件的逻辑,唯一需要注意是处理输入的时候不用cin,而用getline,因为getline可以读入空格。
//client.cppintmain(){int fd =open(fifoname.c_str(), O_WRONLY);if(fd <0){perror("open");exit(1);} std::string message;while(true){ std::cout <<"please enter# ";getline(std::cin, message);// getline可以读取空格write(fd, message.c_str(), message.size());}close(fd);return0;}现在我们来总结一下:
1、client和server是如何看到同一份资源的?因为命名管道不同于匿名管道,它有文件系统路径标识,所以当server和client通过路径+文件名打开的文件时就能通过路径解析找到唯一的文件inode,进而保证不同的进程打开的是同一个文件。
2、为什么fifo叫命名管道?因为命名管道本身就有名字,并且也有inode,open打开文件时如果打开的是命名管道就会对其做特殊处理,我们作为程序员不用操心。
以面向对象封装命名管道
1、构造函数中不写创建管道逻辑,析构函数中不写关闭管道逻辑,而是将创建管道和关闭管道和关闭文件描述符单独写成三个方法,因为客户端和服务端都会使用命名管道,服务端既要读取数据又要打开管道、打开文件、关闭管道、关闭文件描述符,而客户端只打开文件、关闭文件,这样解耦合方便服务端、客户端各自调用自己需要的接口。
2、封装Close时添加一个文件描述符默认值判断defaultfd,defaultfd默认为-1,打开管道成功了将defaultfd改为管道的fd,关闭管道后将defaultfd重新置为-1,当Close的参数为-1时表示程序没有打开管道文件或者已经将管道删除了,这时直接return,避免对无效 fd 执行 close 导致的系统错误、资源污染。 3、实现面向对象代码时对于参数传递的最佳实践如下:
输入参数:const+&
输出参数:*
输入输出参数:&

源码
//common.hpp#ifndef__COMMON_HPP__#define__COMMON_HPP__#include<stdio.h>#include<iostream>#include<string>#include<sys/types.h>#include<sys/stat.h>#include<unistd.h>#include<fcntl.h> std::string fifoname ="fifo"; mode_t mode =0666;#defineSIZE128//缓冲区大小#endif//NamedPipe.hpp#pragmaonce#include"common.hpp"constint defaultfd =-1;classNamedPipe{public:NamedPipe(const std::string name):_name(name),_fd(defaultfd){}// 创建管道boolCreate(){int n =mkfifo(_name.c_str(), mode);if(n ==0){ std::cout <<"mkfifo suceessful"<< std::endl;}else{// std::cout << "mkfifo failed" << std::endl;perror("mkfifo");returnfalse;}returntrue;}boolOpenForRead(){ _fd =open(_name.c_str(), O_RDONLY);if(_fd <0){perror("open");returnfalse;} std::cout <<"open file success"<< std::endl;returntrue;}boolOpenForWrite(){ _fd =open(_name.c_str(), O_WRONLY);if(_fd <0){perror("open");returnfalse;}returntrue;}// 输出型参数boolRead(std::string *out){char buffer[SIZE]={0}; ssize_t num =read(_fd, buffer,sizeof(buffer)-1);if(num >0)// read失败返回-1{ buffer[num]=0;// 保持C风格字符串,末尾加0*out = buffer;}elseif(num ==0){returnfalse;}else{returnfalse;}returntrue;}// 输入型参数voidWrite(const std::string &in){write(_fd, in.c_str(), in.size());}// 关闭管道文件描述符(本代码示例中服务端、客户端都需要关闭)voidClose(){if(_fd == defaultfd){return;// 直接return,避免执行无效操作}int n =close(_fd);if(n <0)perror("close"); _fd =-1;}// 归还管道文件voidRemove(){int m =unlink(_name.c_str());(void)m;}~NamedPipe(){}private: std::string _name;// 管道文件名int _fd;// 管道文件描述符};//server.cpp#include"NamedPipe.hpp"intmain(){ std::string fifoname ="fifo"; NamedPipe np(fifoname);// 1、创建管道文件 np.Create();// 2、打开管道文件 np.OpenForRead();// 3、读取管道数据 std::string message;while(true){bool res = np.Read(&message);if(res){ std::cout <<"client say# "<< message << std::endl;}else{break;}}// 4、归还资源 np.Close(); np.Remove();return0;}//client.cpp#include"NamedPipe.hpp"intmain(){ NamedPipe np(fifoname); np.OpenForWrite(); std::string message;while(true){ std::cout <<"please enter# ";getline(std::cin, message);// getline可以读取空格 np.Write(message);} np.Close();return0;}总结
命名管道主要用于在毫无关系的进程之间进行文件级进程通信。其他特点匿名、命名管道相同。
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~
