linux进程间通信——学习与应用命名管道, 日志程序的使用与实现
前言:本节主要讲解linux进程间通信里面的命名管道, 命名管道和我们学过的匿名管道是类似的。 博主将会带着友友们先看一下原理, 然后就会着手使用以下命名管道是怎么使用的。 最后我们还会试着引入日志系统, 我们从本节开始就会引入日志这个东西了!以后我们可能会将本节的日志拿过来用, 所以友友们要着重学习了!!
ps:本节内容建议学习了匿名管道的友友们进行观看!
命名管道原理
具有血缘关系的进程进行进程间通信, 如果毫不相关的进程进行进程间通信呢? 这个时候就用到了mkfifo, mkfifo可以创建出一种管道文件, 这种文件是一种命名管道。可以用来让不相关的进程之间进行通信。
这里我们创建一个以myfifo为文件名的命名管道:
然后我们就可以看到这个以p开头的文件, 叫做命名管道。 并且这个管道是在磁盘上面的。
但是, 我们如果向这个管道之中写入数据, 就会阻塞住:
所以呢,我们如果再打开一个终端, 在我们的管道中去读取, 就会看到读取到了内容, 并且不会再被阻塞住了!!! mkfifo管道文件不会保存数据, 他只相当于一个中转层。
理解:如果两个不同的进程, 打开同一个文件的时候, 在内核中, 操作系统会打开几个文件? 那么我们想一下, 对于操作系统来说, 如果两个进程打开同一个文件,首先文件的struct file因为是一样的, 所以就不需要写两份, 只需要写一份。 而我们的文件的inode, operators, 文件缓冲区这些操作系统创建几份呢? 操作系统会不会担心错乱呢? ——答案是只会创建一份,并且不会错乱的, 因为操作系统认为两个进程都打开一个文件了, 用户都不怕发生冲突, 操作系统更不怕发生冲突!!!
所以, 我们的两个不同进程打开同一个文件本质上也是这一张图:
如何理解mkfifo
进程间通信的前提:先让不同进程看到同一份资源——也就是struct file。 那么问题来了, 为什么我们不去直接让文件去磁盘里面刷盘呢?要知道, 我们只需要文件缓冲区就可以实现数据的修改与存储。 那么他就不需要再去文件去刷盘了!!!所以说, mkfifo的文件, 是一个内存级别的文件。
那么, 我们怎么保证, 我们打开的是同一个文件呢? 以及, 我们为什么要打开同一个文件呢? ——看到同一个路径下面的同一个文件名 = 路径 + 文件名(路径 + 文件名具有唯一性!!!) ——也就是说, 命名管道, 是利用使用路径 + 文件名的方案, 让不同的进程, 看到同一份文件资源, 进而实现不同文件之间的通信的!!!
那么问题来了, 命名管道能不能也设计成我们之前写的进程池的样子呢?——可以的!只是不需要创建子进程,直接使用mkfifo创建就好了!!!
命名管道和普通管道是一样的——都会面向字节流, 进程同步与互斥, 生命周期随进程, 当使用时需要打开一个写端, 一个读端, 单向通信等等。 不同点就是命名管道可以作用于没有血缘关系的进程之间进行通信。
日志
对于日志, 博主也不太熟悉, 也没有查过相关资料。 这里只是谈一些本篇内容需要用到的东西:
日志包括: 日志的内容包括日志的时间, 日志的等级, 日志的内容, 文件的名称和行号等。
其中, 日志时间, 日志等级, 日志内容一般的日志都会有。 并且日志的等级一般有以下几个等级:
- Info:常规消息。
- Waring:报警信息, 不影响整个代码的向后执行, 但是需要让用户知道, 否则可能会引发一些问题。
- Error:比较严重的问题, 可能需要立即处理, 可能不影响继续向后进程, 比如飞机起飞, 机长就广播消息, 这些消息, 是Info消息; 当飞机起飞时, 这个时候飞机可能会抖动, 可能会遇到强气流等等——这就是Waring;后来飞机的发动机出问题了, 这个问题非常严重了, 虽然飞机仍旧可以飞行, 但是可能下一秒就会出事故。——这就是Error
- Fatal:致命的 当程序遇到致命问题时, 如果不解决, 程序无法向后执行。
- Debug:调试问题。
创建文件
首先我们要创建下面这五个文件,其中Client.cpp是用于客户端输入, Server.cpp是用于服务端读入。 然后Log.hpp是用于日志文件。
makefile
make用来生成客户端和服务端的可执行程序
.PHONY:all all: server.exe client.exeserver.exe:Server.cppg++ -o $@ $^ -g -std=c++11client.exe:Client.cppg++ -o $@ $^ -g -std=c++11.PHONY:clean clean:rm -f server.exe client.exe
Com.hpp头文件准备
我们在Com.hpp可以先枚举一下管道操作的错误类型。
enum {FIFO_CREAT_ERR = 1, //创建出错的错误码FIFO_DELE_ERR = 2, //删除出错的错误码FIFO_OPEN_ERR = 3 //打开出错的错误码 };
我们打开创建命名管道的时候需要指定命名管道的文件名, 所以我们可以定义一个FIFO_FILE作为文件名, 然后定义一个MODE当作创建文件名的时候的权限码
#define FIFO_FILE "./myfifo" #define MODE 0660
然后我们就可以定义一个class类, 类的构造函数用来创建管道, 类的析构函数用来删除管道。 ——这是一种设计, 当我们以后创建管道的时候, 只需要定义一个对象,这个时候管道就创建好了, 并且当这个对象析构的时候, 管道文件就删除了!!!
class Init { public:Init(){int n = mkfifo(FIFO_FILE, MODE);if (n == -1) {perror("mkfifo");exit(FIFO_CREAT_ERR);}}~Init(){int m = unlink(FIFO_FILE);if (m == -1){perror("unlink");exit(FIFO_DELE_ERR);}}};
ps:头文件包含哪一个博主没有说, 友友们自己查一下man手册即可!
以下是整个Com.hpp的文件代码
#pragma once
#include <iostream>
using namespace std;
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include<unistd.h>#define FIFO_FILE "./myfifo"
#define MODE 0660enum
{FIFO_CREAT_ERR = 1,FIFO_DELE_ERR = 2,FIFO_OPEN_ERR = 3
};class Init
{
public:Init(){int n = mkfifo(FIFO_FILE, MODE);if (n == -1) {perror("mkfifo");exit(FIFO_CREAT_ERR);}}~Init(){int m = unlink(FIFO_FILE);if (m == -1){perror("unlink");exit(FIFO_DELE_ERR);}}};
日志文件的准备
日志文件是本节很重要的一块。 我们需要注意:
#define部分
先定义好日志等级
#define Info 0
#define Debug 1
#define Waring 2
#define Error 3
#define Fatal 4
定义打印方式——分为在显示器上打印, 在一个文件里面打印,在多个文件里面打印:
#define Info 0
#define Debug 1
#define Waring 2
#define Error 3
#define Fatal 4
定义打印到目标文件的文件名
#define LogFile "log.txt"
我们打印日志文件到管道里面, 所以一定要有日志文件的缓冲区, 所以这里要定义一个缓冲区的最大大小:
#define SIZE 1024
定义类
//然后我们定义一个日志的class类:
class Log { public:Log(){printStyle = Screen; //默认打印到屏幕上path = "./log/"; //默认的路径是./log/}public:int printStyle; //打印风格string path; //打印到那个路径下面 };
我们重载()来当作打印日志内容:
void operator()(int level, const char* format, ...){//内容先不写}
LevelToString
重载()里面的内容我们先不写, 因为这里面会用到许多别的接口。 我们先实现一下别的接口, 首先就是我们知道打印日志内容需要日志等级, 而日志等级我们在上面define成了数字码0,1, 2, 3等。 所以我们接受日志等级使用的是int, 现在我们打印日志等级不能只打印数字, 所以我们还要将这个数字转变成string类型。 所以, 我们可以实现一个LevelToString函数, 用来将数字转变为string串:
string LevelToString(int level){switch(level){case Info:return "Info";case Debug:return "Debug";case Waring:return "Waring";case Error:return "Error";default:return "None";}}
有了这个接口之后我们就可以着手实现重载()了。
operator()
重载()有两个步骤, 第一个步骤是将要打印的数据先保存到缓冲区中。 第二个步骤是将缓冲区的数据打印到日志文件里面。
我们先实现第一个步骤:
void operator()(int level, const char* format, ...){//第一个步骤//获取时间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,ctime->tm_mon, ctime->tm_yday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec); //向缓冲区中打印数据//再向要打印的内容放到第二个缓冲区之中va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format,s); //va_end(s);//合并两个缓冲区char logtxt[SIZE * 2]; //文本缓冲区, 用来将两个文本文件合起来;snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);//第二个步骤printLog(logtxt);}
printLog
我们将第二个步骤用一个printLog函数封装。为什么printLog函数要传送日志等级呢? 我们前面不是已经打印了日志等级了吗? 难道还要再打印一遍吗?——不是的, 因为我们的打印日志的时候, 定义的是三种打印方式, 一种Screen在屏幕上面打印, 一种是Onefile在一个文件里面打印, 一种是Classfile在多个文件里面打印,当我们在多个文件里面打印的时候我们就可以使用level来区分到底去哪个文件里面打印了。下面我们实现printfLog:
void printLog(int level, const string logtxt){switch(printStyle){case Screen:cout << logtxt << endl;break;case Onefile:printOneFile(LogFile, logtxt); //向文件里面打, 这个文件名定义的是LogFile break;case Classfile:printClassFile(level, logtxt);//想多个文件里面打印, 根据level区分去哪个文件里面打印break;default:break;}}
上面的printOneFile用来封装向一个文件里面打印的代码; printClassFile用来封装向多个文件打印到代码。
printOneFile
向一个文件里面写就是很标准的文件写入, 先打开文件获取fd, 然后判断文件是否打开成功。 如果成功就像里面写入数据。最后关闭文件。 ——值得一提的是, 这里的logname是打印到的目标文件的文件名。 我们在前面加上path(这个path是./log/)是为了创建一个文件夹, 以后打印数据都向当前目录的log目录下创建文件进行打印数据。
void printOneFile(const string& logname, const string& logtxt){string _logname = path + logname; //打印到的文件路径int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0660);if (fd < 0) return;write(fd, logtxt.c_str(), logtxt.size()); //文件描述符,文件源, 文件大小close(fd);}
printClassFile
打印到多个文件夹我们就可以根据level转换成字符串的不同, 创建出不同的文件名。 再将数据打印到这些文件名的文件夹中:
void printClassFile(int level, const string& logtxt){//创建文件夹的文件名string filename = LogFile;filename += ".";filename += LevelToString(level);printOneFile(filename, logtxt); //然后去这个文件里打印数据}
Enable
最后我们再加一个用来修改打印方式的函数, 日志系统就完成了
void Enable(int method){printStyle = method;}
下面是真个日志系统全部代码
全部代码:
#pragma once
#include<iostream>
using namespace std;
#include<stdarg.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>#define SIZE 1024#define Info 0
#define Debug 1
#define Waring 2
#define Error 3
#define Fatal 4#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printStyle = Screen; //默认打印到屏幕上path = "./log/"; //默认的路径是./log/}void Enable(int method){printStyle = method;}void printOneFile(const string& logname, const string& logtxt){string _logname = path + logname; //打印到的文件路径int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0660);if (fd < 0) return;write(fd, logtxt.c_str(), logtxt.size()); //文件描述符,文件源, 文件大小close(fd);}void printClassFile(int level, const string& logtxt){//创建文件夹的文件名string filename = LogFile;filename += ".";filename += LevelToString(level);printOneFile(filename, logtxt); //然后去这个文件里打印数据}void printLog(int level, const string logtxt){switch(printStyle){case Screen:cout << logtxt << endl;break;case Onefile:printOneFile(LogFile, logtxt); //向文件里面打, 这个文件名定义的是LogFile break;case Classfile:printClassFile(level, logtxt);break;default:break;}}void operator()(int level, const char* format, ...){//获取时间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,ctime->tm_mon, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec); //向缓冲区中打印数据va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format,s); //va_end(s);char logtxt[SIZE * 2]; //文本缓冲区, 用来将两个文本文件合起来;snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);//将要打印的数据放到缓冲区里面后就可以打印数据了printLog(level, logtxt);}string LevelToString(int level){switch(level){case Info:return "Info";case Debug:return "Debug";case Waring:return "Waring";case Error:return "Error";default:return "None";}}public:int printStyle; //打印风格string path; //打印到那个路径下面
};
Sever端
接下来准备我们的Sever端, 这里我们写日志, 我们是向不同的文件中打印(因为测试向不同的文件中打印成功的话, 向显示器, 向单个文件中打印一定可以成功)
#include"Com.hpp"
#include"Log.hpp"int main()
{Init init; //创建管道和删除管道Log log; //日志系统log.Enable(Classfile); //向不同的文件中打印return 0;
}
然后我们打开信道——这个打开信道和创建管道是不同的, 创建管道只是将我们的命名管道文件给创建出来。 而我们的打开信道就是打开这个管道文件, 并且我们现在写的是服务端, 所以打开管道的方式是只读, 也就是O_RDONLY;然后报错信息等等都通过日志系统的()重载打印到日志系统之中。
#include"Com.hpp"
#include"Log.hpp"int main()
{Init init; //创建管道和删除管道Log log; //日志系统log.Enable(Classfile); //向不同的文件中打印//打开信道int fd = open(FIFO_FILE, O_RDONLY); //Sever端是读取if (fd < 0){log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);exit(FIFO_OPEN_ERR);}//开始通信log(Info, "server open file done, error string: %s, error code: %d", strerror(errno), errno);log(Waring, "server open file done, error string: %s, error code: %d", strerror(errno), errno);log(Error, "server open file done, error string: %s, error code: %d", strerror(errno), errno);while (true){char buffer[1024];int x = read(fd, buffer, sizeof(buffer));if (x > 0){buffer[x] = 0;cout << "client say#: " << buffer << endl;} else if (x == 0) {log(Fatal, "client quit, me too!!! error string: %s, error code: %d", strerror(errno), errno);break;}else break;}close(fd);return 0;
}
Client端
我们的客户端同样是打开管道文件, 同时以写的方式打开。 然后我们创建缓冲区,将数据写到缓冲区中, 再从缓冲区通过write函数写到文件里面
#include"Com.hpp"
#include"Log.hpp"int main()
{int fd = open(FIFO_FILE, O_WRONLY);if (fd < 0) {perror("open");exit(FIFO_OPEN_ERR);}//cout << "client open file done" << endl;//成功打开客户端string line; //创建缓冲区while (true){cout << "Please Entering#: ";getline(cin, line); //向缓冲去中写入数据write(fd, line.c_str(), line.size());}close(fd);return 0;
}
运行测试
写完所有的代码之后我们就可以运行测试一下了。 首先我们打开两个终端:
先在目录下面创建一个log文件, 用来给日志系统打印数据。
然后运行先运行我们的服务端, 再运行客户端——一定先运行服务端。 因为如果先运行客户端, 我们的客户端打开的管道文件和我们的服务端打开的管道文件就不是同一个管道文件——打开后如下:
然后我们每向客户端输入一条数据, 服务端就会打印一条数据
并且我们在客户端ctrl + c后,服务端读取到0, 那么久else if判断, 程序就退出了!
然后我们打开我们的日志文件, 就写入了许多数据:
——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!