当前位置 博文首页 > wanggao的专栏:muduo学习笔记:base部分之高性能日志库-LogFile

    wanggao的专栏:muduo学习笔记:base部分之高性能日志库-LogFile

    作者:[db:作者] 时间:2021-09-09 09:48

    LogFIle类是muduo日志库后端的实际管理者,主要负责日志的滚动。内部的AppendFile类型成员变量是最终用于操作本地文件的类,负责将日志数据写入本地文件,记录写入的日志总长度。

    代码类结构如下:
    在这里插入图片描述

    1、AppendFile类

    AppendFile类位于moduo::FileUtil命名空间。由于写日志到缓冲中使用非加锁的fwrite函数,因此要求外部单线程处理或加锁,保证缓冲区日志不会发生交织混乱。

    功能单一,直接上代码和注释,如下。

    class AppendFile : noncopyable
    {
     public:
      explicit AppendFile(StringArg filename) 
        : fp_(::fopen(filename.c_str(), "ae")),  // 'e' for O_CLOEXEC
          writtenBytes_(0)
      {
        assert(fp_);
    	::setbuffer(fp_, buffer_, sizeof buffer_);
    	// posix_fadvise POSIX_FADV_DONTNEED ?
      }
    
      ~AppendFile() {  ::fclose(fp_);  }   // 关闭文件,会强制flush缓冲区
    
      void append(const char* logline, size_t len)  // 写数据到缓冲区
      {
        size_t n = write(logline, len);  // 缓冲区长度可能不足以写下当前日志数据
        size_t remain = len - n;  
        while (remain > 0)   // 需要循环写入当前日志剩余未写的数据
        {
          size_t x = write(logline + n, remain);  
          if (x == 0) {
            int err = ferror(fp_);
            if (err) {
              fprintf(stderr, "AppendFile::append() failed %s\n", strerror_tl(err));
            }
            break;
          }
          n += x;
          remain = len - n; // remain -= x
        }
        writtenBytes_ += len;   // 统计写入缓冲区的总数据长度
      }
    
      void flush()   {  ::fflush(fp_);  }   // flush缓冲区数据到文件
    
      off_t writtenBytes() const { return writtenBytes_; }  // 获取已写入日志的总字节数
    
     private:
    
      size_t write(const char* logline, size_t len){
        // #undef fwrite_unlocked
        return ::fwrite_unlocked(logline, 1, len, fp_);  // 非加锁的写
      }
    
      FILE* fp_;
      char buffer_[64*1024];   // 文件输出缓冲区,64kB大小
      off_t writtenBytes_;     // 已写日志数据的总字节数
    };
    

    2、LogFile类

    日志消息记录长度达到设定值、日志记录时间超过当天进行日志滚动。当短时间内日志长度较小时,不能将日志信息长时间放如缓存中,因此日志每记录1024次数就检查一次距前一次flush到文件的时间是否超过3s,若是则flush到文件中。另外,可以选择是否使用互斥锁锁保证线程安全。

    定义如下:

    namespace FileUtil
    {
    class AppendFile;
    }
    
    class LogFile : noncopyable
    {
     public:
      LogFile(const string& basename,    //  日志文件名,默认保存在当前工作目录下
              off_t rollSize,            //  日志文件超过设定值进行roll
              bool threadSafe = true,    //  默认线程安全,使用互斥锁操作将消息写入缓冲区
              int flushInterval = 3,     //  flush刷新时间间隔
              int checkEveryN = 1024);   //  每1024次日志操作,检查一个是否刷新、是否roll
      ~LogFile();
    
      void append(const char* logline, int len); 
      void flush();
      bool rollFile();
    
     private:
      void append_unlocked(const char* logline, int len); 
    
      static string getLogFileName(const string& basename, time_t* now);  // 获取roll时刻的文件名
    
      const string basename_;
      const off_t rollSize_;
      const int flushInterval_;
      const int checkEveryN_;
    
      int count_;
    
      std::unique_ptr<MutexLock> mutex_;			 // 操作AppendFiles是否加锁
      time_t startOfPeriod_;                         // 用于标记同一天的时间戳(GMT的零点)
      time_t lastRoll_;                              // 上一次roll的时间戳
      time_t lastFlush_;                             // 上一次flush的时间戳
      std::unique_ptr<FileUtil::AppendFile> file_;   
    
      const static int kRollPerSeconds_ = 60*60*24;  // 一天的秒数 86400
    };
    

    2.1、日志滚动实现

    日志的滚动的实现主要由rollFile()函数。

    /* 
     * 滚动日志 
     * 相当于重新生成日志文件,再向里面写数据 
     */  
    bool LogFile::rollFile()  
    {  
      time_t now = 0;  
      string filename = getLogFileName(basename_, &now);  //获取生成一个文件名称  
      
      //注意,这里先除KRollPerSeconds然后乘KPollPerSeconds表示对齐值KRollPerSeconds的整数倍,
      // 也就是事件调整到当天零点(/除法会引发取整)  
      time_t start = now / kRollPerSeconds_ * kRollPerSeconds_;     
      
      if (now > lastRoll_)   //如果大于lastRoll,产生一个新的日志文件,并更新lastRoll  
      {  
        lastRoll_ = now;  
        lastFlush_ = now;  
        startOfPeriod_ = start;  
        file_.reset(new FileUtil::AppendFile(filename));  
        return true;  
      }  
      return false;  
    }  
      
    /* 
     * 构造一个日志文件名 
     * 日志名由基本名字+时间戳+主机名+进程id+加上“.log”后缀 
     */  
    string LogFile::getLogFileName(const string& basename, time_t* now)  
    {  
        string filename;  
        //reserve()将字符串的容量设置为至少basename.size() + 64,因为后面要添加时间、主机名、进程id等内容,
        // 预先设置容量大小,为了避免反复重新分配缓冲区内存而导致效率降低,
        // 或者在使用某些STL操作(例如std::copy)之前保证缓冲区够大
        filename.reserve(basename.size() + 64);  
      
        // 基本文件名  
        filename = basename;  
      
        // 获取当前年月日  
        char timebuf[32];  
        struct tm tm;  
        *now = time(NULL);  
        gmtime_r(now, &tm); // FIXME: localtime_r ?    // 可重入线程安全的时间获取函数
        strftime(timebuf, sizeof timebuf, ".%Y%m%d-%H%M%S.", &tm);  //格式化时间
        
        filename += timebuf;   // 加上时间戳  
        
        filename += ProcessInfo::hostname();    // 加上主机名  
     
        char pidbuf[32];  
        snprintf(pidbuf, sizeof pidbuf, ".%d", ProcessInfo::pid());  
        filename += pidbuf;      // 机上进程id 
      
        filename += ".log";  
      
        return filename;  
    }  
    

    主机名和进程ID获取方式在 ProcessInfo命名空间,

    string ProcessInfo::hostname()
    {
      // HOST_NAME_MAX 64
      // _POSIX_HOST_NAME_MAX 255
      char buf[256];
      if (::gethostname(buf, sizeof buf) == 0){
        buf[sizeof(buf)-1] = '\0';
        return buf;
      }
      else{
        return "unknownhost";
      }
    }
     
    pid_t ProcessInfo::pid()
    {
      return ::getpid();
    }
    

    关于日志超过当天就进行roll的判断,记录当天00:00时刻的秒数,那么从当天任何时间计算当天零点的秒数time_t start = now / kRollPerSeconds_ * kRollPerSeconds_;都是相同的。

    例如:2021-07-18 00:00:00 对应的GMT秒数为 1626566400,那么
    取值范围在[1626566400,1626566400+ 86400) 的秒数now, 恒有 now * 86400 / 86400 = 1626566400。

    注意:time(NULL)得到的实际是本地的时间秒数, 也就是UTC +8的时间戳。

    2.2、日志消息的添加

    LogFile::append()函数添加日志内容,实际是调用AppendFile::append()函数。但是,这里还设计是否使用互斥锁、日志滚动的策略判断。

    void LogFile::append(const char* logline, int len)
    {
      if (mutex_){
        MutexLockGuard lock(*mutex_);
        append_unlocked(logline, len);
      }
      else{
        append_unlocked(logline, len);
      }
    }
    
    void LogFile::flush()
    {
      if (mutex_){
        MutexLockGuard lock(*mutex_);
        file_->flush();
      }
      else{
        file_->flush();
      }
    }
    
    void LogFile::append_unlocked(const char* logline, int len)
    {
      file_->append(logline, len);
    
      if (file_->writtenBytes() > rollSize_){ 
        rollFile(