作者:小树苗渴望变成参天大树
作者宣言:认真写好每一篇博客
作者gitee:gitee✨
作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
今天博主开始给大家讲解一下命名管道文件,就是相比较于匿名管道,是有名字的,命名管道文件的特点进行没有任何关系的进程间进行通信,他的原理几乎和匿名管道差不多,但是还是有差距,博主在匿名管道哪里没讲开,因为需要两者对比的区分开才更有效果,他进行通信的时候和文件操作差不多,话不多说,开始进入正文讲解
讲解逻辑:
我们在命令行当中通过指令来实现两个进程之间的通信,因为指令也是程序,每个指令互相都是没有关系的,我们使用mkfifo
这个指令来创建命名管道文件
mkfifo myfifo
通过上面的演示,我们echo往管道文件写入,但是他是阻塞的,等到有人来读取他,他才没有阻塞,所以我们得出来一个结论,我们管道文件只有在读写端都接入的时候,才会被打开。这也是管道文件的一个特性,符合同步互斥机制。
管道文件的演示很简单,接下来我们直接来谈谈他的理解
大家应该很奇怪,为什么叫理解,不讲原理吗,原因是我们的命名管道和匿名管道原理差不多,但是周边有点小不同,需要大家去理解一下。
可以简单理解命名管道文件在普通文件和匿名管道之间,但是效率还是匿名管道强一点,因为少了一层拷贝,我们可以通过下面的脚本来观察,管道文件确实是不刷盘的。
博主使用一直往管道问价那里面然后读取,我们使用ls -l | grep myfifo
去查看,发现文件大小一直为0,所以内容不在硬盘上。
通过上面的理解,大家应该理解了命名管道文件,原理和匿名很相似,所以接下来博主直接带大家来编写大家,让大家更好的理解
既然是命名管道文件,肯定不能像创建普通文件一样使用mkdir去创建,按照我们之前的经验,准确来说是进程间通信的本质,我们认为这个命名管道文件应该是由操作系统去创建,我们来看看怎么在代码中创建管道:
第一个参数是创建管道的路径和文件名,第二个参数文件
传刚才文件的路径和文件名就可以了
serverPipe.cc
#include "fifo.hpp"
//此进程是服务端,给客户端发送内容的
int main()
{
//创建管道文件
int n=mkfifo(PATHNAME,MODE);
if(n<0)
{
perror("mkfifo:");
exit(EXIT_MKFIFO);
}
//打开管道,等待读入方打开管道,这个才会打开
int fd=open(PATHNAME,O_WRONLY);
if(fd<0)
{
perror("open:");
exit(EXIT_OPEN);
}
//进行通信
string s;
while(true)
{
cout << "Please Enter@ ";
getline(cin, s);
write(fd,s.c_str(),s.size());
}
//关闭管道
close(fd);
//删除管道文件
int m=unlink(PATHNAME);
if(m<0)
{
perror("unlink:");
exit(EXIT_UNLINK);
}
return 0;
}
clientPipe.cc
#include "fifo.hpp"
//此进程是客户端,接收服务端发过来的消息
int main()
{
//创建管道文件另一个进程已经帮助我们做好了,我们拿来使用就好了
int fd=open(PATHNAME,O_RDONLY);
if(fd<0)
{
perror("open:");
exit(EXIT_OPEN);
}
char buffer[N];
while(true)
{
buffer[0]=0;
size_t n=read(fd,buffer,sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
cout << "server say# " << buffer << endl;
}
else if (n == 0)
{
printf("server quit, me too!, error string: %s, error code: %d\n", strerror(errno), errno);
break;
}
else
break;
}
close(fd);
return 0;
}
fifo.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define PATHNAME "./myfifo" //创建管道文件的路径和文件名
#define MODE 0664 //权限
#define N 1024
#define EXIT_MKFIFO -1
#define EXIT_OPEN -2
#define EXIT_UNLINK -3
这个理解起来不难,大家也看到效果了。读端会等待写端,所以也是具有同步互斥机制的
但是代码不够优雅,博主选择将创建管道和删除管道,放到一个类里面,都放在头文件里面:
fifo.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define PATHNAME "./myfifo" //创建管道文件的路径和文件名
#define MODE 0664 //权限
#define N 1024
#define EXIT_MKFIFO -1
#define EXIT_OPEN -2
#define EXIT_UNLINK -3
class Init
{
public:
Init()
{
int n=mkfifo(PATHNAME,MODE);
if(n<0)
{
perror("mkfifo:");
exit(EXIT_MKFIFO);
}
}
~Init()
{
int m=unlink(PATHNAME);
if(m<0)
{
perror("unlink:");
exit(EXIT_UNLINK);
}
}
};
我们发现每次写代码打印错误信息的时候,都是直接写的,而现在博主就交给你们一个办法,以后再学习中都可以使用到,而且这个以后去公司的时候,也不需要你自己去写的,我们了解这个原理就可以了,这里会介绍一些不常用的函数接口,接下来博主就来介绍一下什么是日志。
我们的日志是有等级之分的,先简单的来区分一下:
#define INFO 0 //常规消息
#define WARNING 1 //警告消息
#define ERROR 2 //错误消息
#define FATAL 3 //非常严重消息
#define DEBUG 4 //调试消息
日志一般都会有时间的,所以要重点介绍一下获取时间的函数,其次日志等级是认为规定,所以传参要传等级,我们打印的格式以及要打印多少个信息也是我们认为控制的,所以最好传可变参数,这样就可以很好的控制了。类似于这样:
log(INFO, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
博主先把代码粘贴出来,再来解释博主认为你们不理解的地方
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
//日志等级
#define INFO 0 //常规消息
#define WARNING 1 //警告消息
#define ERROR 2 //错误消息
#define FATAL 3 //非常严重消息
#define DEBUG 4 //调试消息
//打印方式
#define SCREEN 0
#define ONEFILE 1
#define CLASSFILE 2
#define SIZE 1024
#define LOGTXT "log.txt" //默认的存放所有日志信息的文件
class Log
{
public:
Log()
{
printmethod=SCREEN;//默认的打印方式
path="./log/";//默认的存放日志的路径
}
void enable(int method)
{
printmethod=method;//自定义打印方式
}
string leveltostring(int level)//将日志等级信息转换成字符串,打印的时候要使用到
{
switch(level)
{
case INFO:return "INFO:";
case WARNING:return "WARNING:";
case ERROR:return "ERROR:";
case FATAL:return "FATAL:";
case DEBUG:return "DEBUG:";
default:return "NONE";
}
}
void operator()(int level,const char* famat,...)
{
time_t t=time(nullptr);//获得时间戳
struct tm*ctime=localtime(&t);//这是获取当地的一个时间,这个结构体李米娜有许多属性。
char leftbuffer[SIZE];//存放日志时间等信息
snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%d-%d-%d-%d:%d:%d]",leveltostring(level).c_str(),ctime->tm_year+1990,ctime->tm_mon+1,ctime->tm_mday,ctime->tm_hour,ctime->tm_min,ctime->tm_sec);
va_list s;
va_start(s,famat);
char rightbuffer[SIZE];//存放日志等级信息,有用户去决定的
vsnprintf(rightbuffer,sizeof(rightbuffer),famat,s);
va_end(s);
char logtxt[SIZE*2];//拼在一起
snprintf(logtxt,sizeof(logtxt),"%s %s",leftbuffer,rightbuffer);
printfmethod(level,logtxt);//打印方式
}
void printfmethod(int level,const string&logtxt)
{
switch(printmethod)
{
case SCREEN:
cout<<logtxt<<endl;
break;
case ONEFILE:
printfonefile(LOGTXT,logtxt);//本来一个参数就可以,但是为了适配下面函数的调用,加了一个参数
break;
case CLASSFILE:
printfclassfile(level,logtxt);
break;
}
}
void printfonefile(const string& logname,const string&logtxt)
{
string _logtxt=path+LOGTXT;
int fd=open(_logtxt.c_str(),O_CREAT|O_WRONLY|O_APPEND);
if(fd<0)
{
perror("open:");
return;
}
write(fd,logtxt.c_str(),logtxt.size());
close(fd);
}
void printfclassfile(int level,const string&logtxt)//多文件打印
{
string _logtxt=LOGTXT;
_logtxt+=".";
_logtxt+=leveltostring(level);//为了区分不同的日志等职,分别放到不同的文件里面。
printfonefile(_logtxt,logtxt);
}
~Log()
{
}
private:
int printmethod;//打印方式
string path;//使日志文件和可执行程序不在一个目录下,单独建立一个目录
};
第一个:va_list是什么,这是类型,而va_start这其实是一个读取可变模板参数的起始位置的函数,我给大家据一个例子:任意数相加
int sum(int n,...)
{
va_list s;
va_start(s,n);
int sum=0;
while(n)
{
sum+=va_arg(s,int);
n--;
}
va_end(s);
return sum;
}
所以我们对于这种可变参数在一开始都至少有一个形参。这样大家应该知道作用了吧,我们在写rightbuffer这个字符串的时候还需要一个函数
vsnprintf
这样就很明显看出来这个函数的作用了。
第二个
我们来看获取时间的函数,最重要的localtime·
这样一看就很清楚了,只是陌生函数多了,但是理解起来和用法都很简单。
通过打印出来的日志我们也可以发现,我们的管道只有双方都打开才会打开。这样打出来的消息就很规范
通过上面的演示,我们发现我们以后想要打印一些错误信息,就可以来复用这个模板了,让程序看上去非常的优雅。大家下去在好好的理解一下这个模板。
今天讲解了命名管道,有了匿名管道的铺垫,我们在理解命名管道的时候就简单许多,所以操作来说也是很简单的,大家下去夺取理解就好了,我们今天讲的重点还有一个就是日志,这个让大家以后都可以重复使用的一个模板非常的方便,大家如果不想去实现,就使用博主写的吧,希望大家下去实现以下,这样更容易理解,而且更有印象。我们下篇讲解的是共享内存的知识,希望大家过啊里支持一下