【Linux】进程间通信(二)命名管道(FIFO)实战指南:从指令操作到面向对象封装的进程间通信实现
文章目录
- 命名管道
- 命名管道的操作
- 指令操作
- 代码操作
- Makefile
- 创建命名管道
- 实现通信
- 以面向对象封装命名管道
- 源码
- 总结
命名管道
(基于文件+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"int main()
{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;return 0;
}
实现通信
我们准备实现让client发送数据然后server接受数据。下面先实现server端,我们已经创建好管道文件了,下面就需要调用文件的各种系统调用接口打开文件、读取文件、关闭文件,和我们在文件系统介绍的一摸一样,小编就不过多赘述了。
这里小编要补充几点:
1、有关命名管道的操作特点,在打开管道一端,但另一端未打开的时候,open操作会被阻塞,因为如果不阻塞直接打开就有可能读到0。
2、读到的数据我们用字符串数组暂存,并且读取时要预留一个位置给\0,所以read的第三个参数需要sizeof(buffer) - 1,读取完毕后自己手动在字符串末尾添加\0。
3、当read读到0时就意味着client退出了,这时我们server端也需要退出,所以需要对read的返回值进行特殊处理。
//server.cpp
#include "common.hpp"int main()
{// 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风格字符串,末尾加0std::cout << "client say# " << buffer << std::endl;}else if(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;return 0;
}
然后实现client端,还是平常打开文件的逻辑,唯一需要注意是处理输入的时候不用cin,而用getline,因为getline可以读入空格。
//client.cpp
int main()
{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);return 0;
}
现在我们来总结一下:
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;#define SIZE 128 //缓冲区大小#endif
//NamedPipe.hpp
#pragma once#include "common.hpp"const int defaultfd = -1;class NamedPipe
{
public:NamedPipe(const std::string name) : _name(name), _fd(defaultfd){}// 创建管道bool Create(){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");return false;}return true;}bool OpenForRead(){_fd = open(_name.c_str(), O_RDONLY);if (_fd < 0){perror("open");return false;}std::cout << "open file success" << std::endl;return true;}bool OpenForWrite(){_fd = open(_name.c_str(), O_WRONLY);if (_fd < 0){perror("open");return false;}return true;}// 输出型参数bool Read(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;}else if (num == 0){return false;}else{return false;}return true;}// 输入型参数void Write(const std::string &in){write(_fd, in.c_str(), in.size());}// 关闭管道文件描述符(本代码示例中服务端、客户端都需要关闭)void Close(){if (_fd == defaultfd){return; // 直接return,避免执行无效操作}int n = close(_fd);if (n < 0)perror("close");_fd = -1;}// 归还管道文件void Remove(){int m = unlink(_name.c_str());(void)m;}~NamedPipe(){}private:std::string _name; // 管道文件名int _fd; // 管道文件描述符
};
//server.cpp
#include "NamedPipe.hpp"int main()
{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();return 0;
}
//client.cpp
#include "NamedPipe.hpp"int main()
{NamedPipe np(fifoname);np.OpenForWrite();std::string message;while(true){std::cout << "please enter# ";getline(std::cin, message); // getline可以读取空格np.Write(message);}np.Close();return 0;
}
总结
命名管道主要用于在毫无关系的进程之间进行文件级进程通信。其他特点匿名、命名管道相同。
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

