同步+异步日志系统(C++实现)

对于一个服务器而言,不论是在调试中还是在运行中,都需要通过打日志的方式来记录程序的运行情况。本文设计的日志系统实现了同步与异步两种功能,原理见下图:

 同步日志:日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。

异步日志:将所写的日志内容先存入阻塞队列中,写线程从阻塞队列中取出内容,写入日志。

日志的运行流程:

1、使用单例模式(局部静态变量方法)获取实例Log::getInstance()。

2、通过实例调用Log::getInstance()->init()函数完成初始化,若设置阻塞队列大小大于0则选择异步日志,等于0则选择同步日志,更新isAysnc变量。

3、通过实例调用write_log()函数写日志,首先根据当前时刻创建日志(前缀为时间,后缀为".log",并更新日期today和当前行数lineCount。

4、在write_log()函数内部,通过isAsync变量判断写日志的方法:如果是异步,工作线程将要写的内容放进阻塞队列中,由写线程在阻塞队列中取出数据,然后写入日志;如果是同步,直接写入日志文件中。

日志的分级与分文件:

分级情况:

  • Debug,调试代码时的输出,在系统实际运行时,一般不使用。
  • Warn,这种警告与调试时终端的warning类似,同样是调试代码时使用。
  • Info,报告系统当前的状态,当前执行的流程或接收的信息等。
  • Erro,输出系统的错误信息

分文件情况:

  1. 按天分,日志写入前会判断当前today是否为创建日志的时间,若为创建日志时间,则写入日志,否则按当前时间创建新的log文件,更新创建时间和行数。
  2. 按行分,日志写入前会判断行数是否超过最大行限制,若超过,则在当前日志的末尾加lineCount / MAX_LOG_LINES为后缀创建新的log文件。

Log.h

 #ifndef LOG_H
 #define LOG_H

#include "blockqueue.h"
#include <mutex>
#include <thread>
#include "buffer.h"
#include <string>
#include <stdarg.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <assert.h>

 class Log
 {
 public:
    static Log* getInstance()
    {
        static Log instance;
        return &instance;
    }
    //初始化日志实例(阻塞队列最大容量、日志保存路径、日志文件后缀)
    void init(int maxQueueCapacity = 1024,
            const char* path_="./log",
            const char* suffix_=".log");

    //异步写日志公有方法,调用私有方法asyncWrite
    static void flushLogThread()
    {
        Log::getInstance()->asyncWrite();
    }

    //将输出内容按照标准格式整理
   void writeLog(int level, const char* format, ...);

private:
    Log();
    ~Log();
    //异步写日志方法
    void asyncWrite();

private:
    const int LOG_NAME_LEN=256;   //日志文件最长文件名
    const int MAX_LOG_LINES=50000;//日志文件内的最长日志条数

    const char* path;      //路径名
    const char* suffix;    //后缀名  
    int lineCount;   //日志行数记录
    int today;             //按当天日期区分文件
    FILE* fp;              //打开log的文件指针
    Buffer buff;           //输出的内容
    std::unique_ptr<BlockQueue<std::string>> deque;  //阻塞队列
    std::unique_ptr<std::thread> writeThread;        //写线程
    bool isAsync;          //是否开启异步日志
    std::mutex mtx;        //同步日志必需的互斥量
};


//四个宏定义,主要用于不同类型的日志输出
#define LOG_DEBUG(format, ...) Log::getInstance()->writeLog(0, format, ##__VA_ARGS__)
#define LOG_INFO(format, ...)  Log::getInstance()->writeLog(1, format, ##__VA_ARGS__)
#define LOG_WARN(format, ...)  Log::getInstance()->writeLog(2, format, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) Log::getInstance()->writeLog(3, format, ##__VA_ARGS__)

#endif // !LOG_H

Log.cpp

#include "log.h"

Log::Log():lineCount(0),
            today(0),
            fp(nullptr),
            deque(nullptr),
            writeThread(nullptr),
            isAsync(false){}

Log::~Log()
{
    if(writeThread&&writeThread->joinable())
    {
        while(!deque->empty())//清空阻塞队列中的全部任务
        {
            deque->flush();
        }
        deque->close();
        writeThread->join();//等待当前线程完成手中的任务
    }
    if(fp)//冲洗文件缓冲区,关闭文件描述符
    {
        std::lock_guard<std::mutex> lock(mtx);
        fflush(fp);
        fclose(fp);
    }
}

void Log::init(int maxQueueCapacity,const char* path_,const char* suffix_)
{
    if(maxQueueCapacity>0)//异步方式
    {
        isAsync=true;
        if(!deque)
        {
            std::unique_ptr<BlockQueue<std::string>> newDeque(new BlockQueue<std::string>(maxQueueCapacity));
            deque=std::move(newDeque);
            std::unique_ptr<std::thread> newThread(new std::thread(flushLogThread));
            writeThread=std::move(newThread);
        }
    }
    else//同步方式
    {
        isAsync=false;
    }
    lineCount=0;
    //生成日志文件名
    time_t timer=time(nullptr);
    struct tm* sysTime=localtime(&timer);
    struct tm t=*sysTime;
    path=path_;
    suffix=suffix_;
    char filename[LOG_NAME_LEN]={0};
    snprintf(filename,LOG_NAME_LEN-1,"%s/%04d_%02d_%02d%s",
            path,t.tm_year+1900,t.tm_mon+1,t.tm_mday,suffix);
    today=t.tm_mday;
    {
        std::lock_guard<std::mutex> lock(mtx);
        buff.retrieveAll();
        if(fp)
        {
            fflush(fp);
            fclose(fp);
        }
        fp=fopen(filename,"a");
        if(fp==nullptr)
        {
            mkdir(path,0777);//先生成目录文件(最大权限)
            fp=fopen(filename,"a");
        }
        assert(fp!=nullptr);
    }
}

void Log::writeLog(int level, const char* format, ...)
{
    struct timeval now={0,0};
    gettimeofday(&now,nullptr);
    time_t tSec=now.tv_sec;
    struct tm* sysTime=localtime(&tSec);
    struct tm t=*sysTime;
    va_list vaList;

    if(today!=t.tm_mday||(lineCount&&(lineCount%MAX_LOG_LINES==0)))
    {
        //生成最新的日志文件名
        char newFile[LOG_NAME_LEN];
        char tail[36]={0};
        snprintf(tail,35,"%04d_%02d_%02d",t.tm_year+1900,t.tm_mon+1,t.tm_mday);
        if(today!=t.tm_mday)//时间不匹配,则替换为最新的日志文件名
        {
            snprintf(newFile,LOG_NAME_LEN-1,"%s/%s%s",path,tail,suffix);
            today=t.tm_mday;
            lineCount=0;
        }
        else//长度超过日志最长行数,则生成xxx-1、xxx-2文件
        {
            snprintf(newFile,LOG_NAME_LEN-1,"%s/%s-%d%s",path,tail,(lineCount/MAX_LOG_LINES),suffix);
        }

        if(fp)
        {
            std::lock_guard<std::mutex> lock(mtx);
            fflush(fp);
            fclose(fp);
        }
        fp=fopen(newFile,"a");
        assert(fp!=nullptr);
    }
    
    //在buffer内生成一条对应的日志信息
    {
        std::lock_guard<std::mutex> lock(mtx);
        lineCount++;
        //添加年月日时分秒微秒———"2022-12-29 19:08:23.406500"
        int n=snprintf(buff.beginWrite(),128,"%04d-%02d-%02d %02d:%02d:%02d.%06ld ",
                        t.tm_year+1900,t.tm_mon+1,t.tm_mday,
                        t.tm_hour,t.tm_min,t.tm_sec,now.tv_usec);
        buff.hasWritten(n);
        //添加日志等级———"2022-12-29 19:08:23.406539 [debug]: "
        switch(level) 
        {
        case 0:
            buff.append("[debug]: ", 9);
            break;
        case 1:
            buff.append("[info] : ", 9);
            break;
        case 2:
            buff.append("[warn] : ", 9);
            break;
        case 3:
            buff.append("[error]: ", 9);
            break;
        default:
            buff.append("[info] : ", 9);
            break;
        }
        //添加使用日志时的格式化输入———"2022-12-29 19:08:23.535531 [debug]: Test 222222222 8 ============= "
        va_start(vaList, format);
        int m = vsnprintf(buff.beginWrite(), buff.writableBytes(), format, vaList);
        va_end(vaList);
        buff.hasWritten(m);
        //添加换行符与字符串结尾
        buff.append("\n\0", 2);
    }
    
    if(isAsync&&deque&&!deque->full())//异步方式(加入阻塞队列中,等待写线程读取日志信息)
    {
        std::lock_guard<std::mutex> lock(mtx);
        deque->push_back(buff.retrieveAllAsString());
    }
    else//同步方式(直接向文件中写入日志信息)
    {
        std::lock_guard<std::mutex> lock(mtx);
        fputs(buff.peek(),fp);
    }
    {//清理buffer缓冲区
        std::lock_guard<std::mutex> lock(mtx);
        buff.retrieveAll();
    }
}

void Log::asyncWrite()
{
    std::string str="";
    while (deque->pop(str))
    {
        std::lock_guard<std::mutex> lock(mtx);
        fputs(str.c_str(),fp);
    }
}

测试程序:test.cpp

分别采用同步和异步方式,各写60000(15000*4)条日志信息。

#include "log.h"

void TestLog() 
{
    int cnt = 0;
    Log::getInstance()->init(0,"./testlog1");//同步日志
    for(int j = 0; j < 15000; j++ )
    {
        LOG_DEBUG("%s 111111111 %d ============= ", "Test", cnt++);
        LOG_INFO ("%s 111111111 %d ============= ", "Test", cnt++);
        LOG_WARN ("%s 111111111 %d ============= ", "Test", cnt++);
        LOG_ERROR("%s 111111111 %d ============= ", "Test", cnt++);
    }

    cnt = 0;
    Log::getInstance()->init(1024,"./testlog2");//异步日志
    for(int j = 0; j < 15000; j++ )
    {
        LOG_DEBUG("%s 222222222 %d ============= ", "Test", cnt++);
        LOG_INFO ("%s 222222222 %d ============= ", "Test", cnt++);
        LOG_WARN ("%s 222222222 %d ============= ", "Test", cnt++);
        LOG_ERROR("%s 222222222 %d ============= ", "Test", cnt++);
    }
}

int main() 
{
    TestLog();
}

实验结果:

同步日志:

由于共写60000条日志,一份日志文件设置最大行数为50000,所以分为两个文件

 

 

 异步日志:

 

附:

缓冲区Buffer类的设计(参考Muduo实现)_{(sunburst)}的博客-CSDN博客

基于C++11实现的阻塞队列(BlockQueue)_{(sunburst)}的博客-CSDN博客

参考资料:

最新版Web服务器项目详解 - 09 日志系统(上)

最新版Web服务器项目详解 - 10 日志系统(下)

  • 16
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
SPI(Serial Peripheral Interface)是一种通信协议,用于在嵌入式系统实现设备之间的串行数据传输。在Linux系统中,SPI通信可以通过内核提供的SPI子系统实现。SPI通信可以采用同步异步方式进行,这里我们将重点讨论在Linux系统中如何实现SPI的异步方式。 在Linux系统中,可以借助内核提供的spidev驱动来实现SPI设备的访问。对于SPI的异步实现,可以通过以下步骤来实现: 首先,需要打开SPI设备,并进行初始化配置。可以通过打开SPI设备对应的设备文件(通常是/dev/spidevX.Y),然后通过ioctl系统调用来设置SPI的工作模式、通信速率等参数。在异步方式中,需要特别注意配置好传输的参数和缓冲区。 其次,需要准备好待发送和接收的数据,通常是通过填充一个缓冲区来实现。在异步方式中,需要考虑到数据的传输顺序和处理方式,特别是在数据量较大的情况下需要做好缓冲区管理。 接着,可以通过异步输入输出操作来实现SPI的数据传输。针对SPI设备,可以使用read和write系统调用来进行数据的同步传输,但在异步方式中,可以使用异步输入输出接口(如aio_read和aio_write)或者使用Linux提供的异步IO库(如libaio)来实现异步数据传输,从而提高系统的并发能力。 最后,在SPI数据传输完成后,需要关闭SPI设备并进行相关资源的释放操作,以确保系统的稳定性和资源的回收。 通过以上步骤,就可以在Linux系统实现SPI通信的异步方式。异步方式可以提高系统的并发能力,适用于需要同时处理多个SPI设备或有较高实时性要求的场景。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值