文章目录
命名管道
(基于文件+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中。
cpp
#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指令在命令行删除管道文件。

cpp
#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的返回值进行特殊处理。
cpp
//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风格字符串,末尾加0
std::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可以读入空格。
cpp
//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+&
输出参数:*
输入输出参数:&

源码
cpp
//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
cpp
//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; // 管道文件描述符
};
cpp
//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;
}
cpp
//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;
}
总结
命名管道主要用于在毫无关系的进程之间进行文件级进程通信。其他特点匿名、命名管道相同。
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~
