1. 什么是命名管道
我们之前说完了匿名管道,知道了具有血缘关系的两个进程可以通过匿名管道来完成进程间通信。但是,如果两个完全不相关的文件想要通过管道完成进程间通信,可以办到吗!答案是一定的,我们通过命名管道即可实现!
我们知道,要完成进程间通信,首先需要让两个进程先看到同一份资源。如果两个进程打开同一个文件,是否是看到了同一份资源呢?这里需要思考一个问题,系统是否会把这一个文件的内容和属性在内存中加载两份?按照系统的尿性,为了节约内存资源,肯定是不会的!所以,两个进程打开同一个文件,看到了同一份资源!而这个文件有路径,有文件名,并且这种文件只会打开,不会把内容刷新到磁盘。我们将这种文件称为命名管道文件!
2. 创建命名管道mkfifo
在Linux中mkfifo命令可以创建一个命名管道!
下面就是我们利用命名管道做的一个简单的应用:
上面实在指令级创建的一个管道文件,实际上,系统也提供了相应的系统调用来帮助我们实现代码层面的管道文件创建。
创建命名管道的接口:
删除命名管道的接口:
1.server.cc
#include "comm.hpp"
int main()
{
umask(0);
// 创建命名管道
int n = mkfifo(FIFO_FILE, 0666);
if (n < 0)
{
// 命名管道创建失败
perror("mkfifo file\n");
return -1;
}
std::cout << "管道创建成功!" << std::endl;
// 创建成功->服务端打开管道->读取内容
int fd = open(FIFO_FILE, O_RDONLY);
if (fd < 0)
{
// 命名管道打开失败
perror("open file\n");
return -1;
}
std::cout << "管道打开成功!" << std::endl;
char buffer[1024];
while (true)
{
int num = read(fd, buffer, sizeof(buffer) - 1);
if (num > 0)
{
buffer[num] = 0;
std::cout << "clenit say#" << buffer << std::endl;
}
else if (num == 0)
{
std::cout << "客户端关闭!" << num << std::endl;
break;
}
else
{
std::cerr << "read file!" << std::endl;
}
}
close(fd);
// 删除管道文件
n = unlink(FIFO_FILE);
if (n < 0)
{
perror("unlink file!\n");
return -1;
}
std::cout << "管道删除成功!" << std::endl;
return 0;
}
1.client.cc
#include "comm.hpp"
int main()
{
// 打开管道文件
int fd = open(FIFO_FILE, O_WRONLY);
if (fd < 0)
{
// 命名管道打开失败
perror("open file\n");
return -1;
}
std::cout << "管道打开成功!" << std::endl;
// 向管道文件中写入内容
std::string message;
while (true)
{
std::cout << "请输入消息:";
std::getline(std::cin,message);
write(fd,message.c_str(),sizeof(message));
}
close(fd);
return 0;
}
1.comm.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <string>
#define FIFO_FILE "fifo"
Makefile
.PHPNY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server fifo
以下是进程间通信的测试接口:
这里有一个细节:我们先启动服务端的时候,只有命名管道会被创建成功,但是并不会直接以读的方式打开命名管道【也就是说server进程在open处阻塞】,而是在客户端在以写的方式打开管道是,管道才会被打开。这也很好理解,毕竟管道文件只被读的方式打开是没有意义的,毕竟做进程间通信需要一方传入数据给另一方。
以上代码是面向过程的,下面我们将代码调整封装一下:
2.comm.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <string>
#define FIFO_FILE "fifo"
// 一种解决错误的方案
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
// 命名管道类
class name_fifo
{
public:
// 构造函数
name_fifo(const std::string &path, const std::string &name)
: _path(path),
_name(name)
{
umask(0);
_fifo_name = _path + "/" + _name;
// 创建命名管道
int n = mkfifo(_fifo_name.c_str(), 0666);
if (n < 0)
{
// 命名管道创建失败
// perror("命名管道创建失败\n");
ERR_EXIT("mkfifo");
}
else
{
std::cout << "管道创建成功!" << std::endl;
}
}
// 析构函数
~name_fifo()
{
// 删除管道文件
int n = unlink(_fifo_name.c_str());
if (n < 0)
{
// perror("删除管道文件失败!\n");
ERR_EXIT("unlink");
}
else
{
std::cout << "管道删除成功!" << std::endl;
}
}
private:
std::string _path;
std::string _name;
std::string _fifo_name;
};
// 命名管道操作类
class fifo_opre
{
public:
fifo_opre(const std::string &path, const std::string &name)
: _path(path),
_name(name),
_fd(-1)
{
_fifo_name = _path + "/" + _name;
}
void open_for_read()
{
// 创建成功->服务端打开管道->读取内容
_fd = open(_fifo_name.c_str(), O_RDONLY);
if (_fd < 0)
{
// 命名管道打开失败
// perror("命名管道打开失败\n");
ERR_EXIT("open");
}
else
{
std::cout << "管道打开成功!" << std::endl;
}
}
void open_for_write()
{
// 打开管道文件
_fd = open(_fifo_name.c_str(), O_WRONLY);
if (_fd < 0)
{
// 命名管道打开失败
// perror("命名管道打开失败\n");
ERR_EXIT("open");
}
else
{
std::cout << "管道打开成功!" << std::endl;
}
}
void Read()
{
char buffer[1024];
while (true)
{
int num = read(_fd, buffer, sizeof(buffer) - 1);
if (num > 0)
{
buffer[num] = 0;
std::cout << "clenit say#" << buffer << std::endl;
}
else if (num == 0)
{
std::cout << "客户端关闭!" << num << std::endl;
break;
}
else
{
// std::cerr << "读取失败!" << std::endl;
ERR_EXIT("read");
}
}
}
void Write()
{
// 向管道文件中写入内容
std::string message;
while (true)
{
std::cout << "请输入消息:";
std::getline(std::cin, message);
write(_fd, message.c_str(), sizeof(message));
}
}
void Close()
{
if (_fd > 0)
close(_fd);
}
~fifo_opre()
{
}
private:
std::string _path;
std::string _name;
std::string _fifo_name;
int _fd;
};
2.client.cc
#include "comm.hpp"
int main()
{
fifo_opre writer(".", "fifo");
writer.open_for_write();
writer.Write();
writer.Close();
return 0;
}
2.server.cc
#include "comm.hpp"
int main()
{
name_fifo nf(".", "fifo");
fifo_opre reader(".", "fifo");
reader.open_for_read();
reader.Read();
reader.Close();
}
3. 一种解决报错退出的方案
// 一种解决错误的方案
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
所有的报错退出可以用上面的宏进行替换,外面嵌套的循环是为了方便进行块级别的替换。
下面使用普通用户在根目录创建一个管道文件,演示一下报错效果: