初识Linux · 命名管道
目录
前言:
代码编写
前言:
有了前文匿名管道的基础,我们介绍匿名管道的时候就轻松许多了,匿名管道和命名管道的区别主要是在于,匿名管道不需要文件路径,并且匿名管道常用于父子进程这种具有血缘关系的场景,使用命名管道的时候,我们常常用于的情况是两个进程毫无联系,使这两个毫无关系的进程可以进行通信。
对于匿名管道来说,我们知道文件对象以及文件对象里面的文件对象里面属性集合,操作集合都不会重新创建,对于命名管道来说也是一样的,所以对于内核级别的文件缓冲区也是这个样子的,OS就没有必要创建两个了,毕竟浪费空间时间的事OS可不想做。
以上其实算是对于命名管道的原理的部分的简单介绍,其实和匿名管道差不多,本文的主要内容其实还是命名管道的代码编写。
代码编写
那么准备工作是先创建三个文件,分别表示客服端,服务端,以及创建管道的文件,创建命名管道之后,让另外两个进程分别打开管道。
那么我们的第一个任务是了解创建命名管道的函数->mkfifo:
直接man mkfifo查询到的是1号手册的mkfifo,那么我们可以使用试试:
创建了对应管道文件之后,我们可以发现几个特征点,它的名字后面带有| 代表管道的意思,并且,它的文件类型部分的字母对应的是p,也就是Pipe部分了,这个其实在文件权限部分介绍过。
还有一个有意思的点是在于……留个关子。
那么这是命令行部分创建命名管道,我们是要直接应用于代码层面,所以介绍3号手册的函数mkpipe:
对应n个头文件,对于返回值来说的话,如果创建管道成功的话,返回的值是0,出错了,返回的值就是-1,并且错误码被设置,所以如果返回出错我们可以打印对应的错误码看看,我们现在不妨试试:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <cstdio>
#include <cerrno>
const std::string path = "./mypipe";
int CreateFifo()
{
int n = mkfifo(path.c_str(),0666);
if(n < 0)
{
perror("mkfifo");
}
return n;
}
这里使用的头文件相对来说也是比较多的,毕竟涉及到了string mkfifo perror,所以C++的头文件有,C++版的C语言头文件也是有的,在namedpipe文件里面实现好了该函数之后,我们转到server.cc文件里面进行调用,其实在client.cc里面调用都可以,毕竟之后不过就是一个进程作为读端,一个进程作为写端,所以任意调用,这里使用server.cc:
#include "namedPipe.hpp"
int main()
{
CreateFifo();
return 0;
}
这里需要插一个题外话了,我们经常是需要Make文件的,但是makefile好像只能编译一个文件?所以我们需要对原来的makefile进行简单的修改:
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf server client
因为makefile是从上到下查找的,所以我们形成一种依赖关系,从而实现两种文件的编译即可。
那么现在我们尝试编译一下server.cc文件:
也是成功创建了,那么我们再运行一下试试?
也就成功的报错了,提示说文件已经存在了。
那么创建了管道文件我们总得删除管道吧?
可以使用函数unlink:
直接给对应的文件路径就可以了。
可是问题来了,我们现在能保证创建多个管道,但是每次创建管道都要使用函数,每次还要手动的调用,难道这不是很麻烦吗?我们使用的语言难道不是面向对象的C++语言吗?所以我们不妨封装一个对象:
class namepipe
{
public:
namepipe(const std::string fifo_path)
:_fifo_path(_fifo_path)
{}
~namepipe()
{
int res = unlink(_fifo_path.c_str());
if(res != 0)
{
perror("unlink");
}
}
private:
const std::string _fifo_path;
int _fd;
int _id;
};
像这样,封装一个对象的好处是,我们不用自己手动的去销毁管道,因为实例化的对象出了main函数的栈帧自己就调用对应的析构函数了。
那么我们可以这个基础之上,进行一些细节的补充,比如是谁调用的?所以我们可以定义一个宏,表示是谁定义的,然后用宏来初识_id:
#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
class namepipe
{
public:
namepipe(const std::string fifo_path, int who)
: _fifo_path(_fifo_path), _id(who)
{
if (_id == Creater)
{
int res = mkfifo(_fifo_path.c_str(), 0666);
if (res != 0)
{
perror("mkfifo");
}
std::cout << "Creater create named pipe" << std::endl;
}
}
~namepipe()
{
if (_id == Creater)
{
int res = unlink(_fifo_path.c_str());
if (res != 0)
{
perror("unlink");
}
std::cout << "Creater free named pipe" << std::endl;
}
}
private:
const std::string _fifo_path;
int _fd;
int _id;
};
我们使用宏分别创建者的好处还有就是,client调用的时候我们就可以直接通过宏判断是谁创建的,这样就可以省略不必要的构造。
可是现在有一个问题就是,我们已经知道谁创建的,知道了对应的路径,可是打开的文件呢?这是非常重要的,所以我们引入一个变量,_fd,那么可以给一个默认的文件描述符,比如-1,初始化的时候总得初始化上去吧?
namepipe(const std::string fifo_path, int who,int fd = DefaultFd)
: _fifo_path(_fifo_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 OpenNamePipe(Read);
}
bool OpenforWrite()
{
return OpenNamePipe(Write);
}
打开管道我们通过宏的不同传参,就可以保证管道的打开是通过读的方式打开的还是写的方式打开的:
private:
bool OpenNamePipe(int mode)
{
_fd = open(_fifo_path.c_str(), mode);
if (_fd < 0)
return false;
return true;
}
但是毕竟涉及到了_fd的修改,所以我们不希望直接可以调用,那么将它私有是最好的选择,这是打开方式的方法,最后的就是写入读取的方法了:
int ReadNamePipe(std::string *out)
{
char buffer[BaseSize];
int n = read(_fd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
int WriteNamePipe(std::string &in)
{
return write(_fd,in.c_str(),in.size());
}
此时两个一整个管道相关的操作就完成了。
对于server:
#include "namedPipe.hpp"
int main()
{
// CreateFifo();
namepipe fifo(path, Creater);
if (fifo.OpenforRead())
{
std::cout << "server open name pipe done" << std::endl;
sleep(3);
while (true)
{
std::string message;
int n = fifo.ReadNamePipe(&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;
}
对于client:
#include "namedPipe.hpp"
int main()
{
namepipe fifo(path, User);
if (fifo.OpenforWrite())
{
std::cout << "client open named pipe done" << std::endl;
while (true)
{
std::cout << "Please enter> ";
std::string message;
std::getline(std::cin, message);
fifo.WriteNamePipe(message);
}
}
return 0;
}
试试结果?
符合预期。
如果我们运行./server的时候,不打开client,会发现./server也不会有后续动作,并且如果我们直接关掉写端,./server端是直接关闭的,这是上文匿名管道的知识点,实际上也是一种进程间同步!!
感谢阅读!