muduo源码学习(1):异步日志——日志消息的存储及输出

目录

前言

日志存储的实现

日志输出的实现

总结


前言

        muduo中的日志,是诊断日志。用于将代码运行时的重要信息进行保存,方便故障诊断和追踪。

        日志一般有两种,一种是同步日志,一种是异步日志,同步日志就是当需要写出一条日志信息的时候,只有等到这条日志消息完全写出之后才能执行后续的程序,可见,这种方式的日志的问题就在于程序可能会阻塞在磁盘写入操作上;

        而另一种异步日志则不会,异步日志的思路是需要写日志消息的时候只是将日志消息进行存储,当积累到一定量或者到达一定时间间隔时,由后台线程自动将存储的所有日志进行输出,可见,对于异步日志来说,每次有日志消息产生的时候,只需要一个存储的行为即可,存储结束就可以执行后面的业务代码了,而真正写入到磁盘的操作,是由后台线程进行的,这样做的好处就是:前台线程不会阻塞在写日志上,后台线程真正写出日志时,日志消息往往已经积累了很多,此时只需要调用一次IO函数,如fwrite,而不需要每条消息都调用一个IO函数,如此减少了IO函数调用次数,提高了效率。

        而muduo中实现的,就是异步日志。

        muduo中的日志消息格式为:时间 线程id 日志级别 日志正文 源文件名及行号。

        日志设置了6种级别:TRACE、DEBUG、INFO、WARN、ERROR和FATAL。

        日志的输出形式为流形式:日志级别<

        如上所述,异步日志的实现关键,是先将日志消息进行存储,然后由后台线程进行写出。muduo将“日志存储”和“日志输出”分为了两大块,现在先来看看日志的存储部分。

日志存储的实现

        前面说过,日志的输出形式为流形式,但muduo并没有使用C++自带的iostream等流库,而是使用自己实现的LogStream,用简短的代码量实现需要的功能,抛去其它不需要的冗杂的功能,使得代码效率更高。

        实现”日志流“的基础,还需要一个内置的缓冲区,muduo通过FixedBuffer类来实现,如下所示:

/*LogStream.h*/
template//构造时需要指定缓冲区大小
class FixedBuffer : noncopyable
{
 public:
  FixedBuffer()
    : cur_(data_)
  {
    setCookie(cookieStart);
  }

  ~FixedBuffer()
  {
    setCookie(cookieEnd);
  }

  void append(const char* /*restrict*/ buf, size_t len)//在缓冲区空闲处memcpy
  {
    // FIXME: append partially
    if (implicit_cast(avail()) > len)//如果剩余的缓冲区空间足以放下len长度的buf
    {
      memcpy(cur_, buf, len);
      cur_ += len;
    }
  }

  const char* data() const { return data_; }
  int length() const { return static_cast(cur_ - data_); }

  char* current() { return cur_; }//返回当前缓冲区空闲位置
  int avail() const { return static_cast(end() - cur_); } //返回剩余有用的空间大小
  void add(size_t len) { cur_ += len; }//当前位置指针往后偏移

  void reset() { cur_ = data_; }//相当于缓冲区重置为空闲
  void bzero() { memZero(data_, sizeof data_); }//清零

  ...
  // for used by unit test
  string toString() const { return string(data_, length()); }//把当前缓冲区的内容转换为string
  StringPiece toStringPiece() const { return StringPiece(data_, length()); }//

 private:
  const char* end() const { return data_ + sizeof data_; }  //返回最后一个字符的地址
  ...
  char data_[SIZE];  //缓冲区
  char* cur_;   //当前空闲缓冲区头部地址
};

     FixedBuffer是一个模板类,模板参数用来指定缓冲区data_的大小,并提供一系列缓冲区操作函数。        

     此时再来看LogStream类,LogStream重载了多个版本的"<<"运算符,如下所示:

/*LogStream.h*/
class LogStream : noncopyable   
{
  typedef LogStream self;
 public:
  typedef detail::FixedBuffer Buffer;  //4K大小的缓冲区类型
 
/*多个<<重载版本,内部实际上都调用了append函数*/
  self& operator<<(bool v);
  self& operator<<(short);
  self& operator<<(unsigned short);
  self& operator<<(int);
  self& operator<<(unsigned int);
  self& operator<<(long);
  self& operator<<(unsigned long);
  self& operator<<(long long);
  self& operator<<(unsigned long long);
  self& operator<<(const void*);
  self& operator<<(float v);
  self& operator<<(char v);
  self& operator<<(const char* str);
  self& operator<<(const unsigned char* str);
  self& operator<<(const string& v);
  self& operator<<(const StringPiece& v);
  self& operator<<(const Buffer& v);

  void append(const char* data, int len) { buffer_.append(data, len); }
  const Buffer& buffer() const { return buffer_; }
  void resetBuffer() { buffer_.reset(); }

 private:
  void staticCheck();

  template
  void formatInteger(T);  //用来将T类型的数字转换为字符串,放到缓冲区中

  Buffer buffer_;   //缓冲区

  static const int kMaxNumericSize = 32;
};

       可以看到,LogStream类通过重载”<<“来实现”日志流“的形式,不管是哪一种重载版本,最终都是会调用append函数,而append函数中又会去调用LogStream类中buffer_的append函数,可以看到,这里的buffer_实际上就是一个FixedBuffer的对象,也就相当于是一个缓冲区,其大小由kSmallBuffer指定,为4K字节。

 

       通过上面的分析,可以知道”日志流“是通过LogStream来实现的,而”日志流“中的缓冲区则是由FixedBuffer来实现。

       至此也就明白了每条日志是如何通过"<<"来进行存储的,但是光是存储是不行的,存储只是一个收集日志消息的作用,不可能把大量的日志都放在内存中,最终还是得将收集到的所有日志输出到某一个指定的地方去,因此接着来分析日志的输出。

 

日志输出的实现

       显然,作为一个日志输出的类,其中应包含LogStream的一个实例用来存储日志消息,这一点体现在Impl类中,如下所示:

class Impl
{
 public:
  typedef Logger::LogLevel LogLevel;
  Impl(LogLevel level, int old_errno, const SourceFile& file, int line);//需要用日志级别、错误信息、文件名和行号构造
  void formatTime();  //获取当前的时间到time_中
  void finish(); 

  Timestamp time_;     //时间
  LogStream stream_;   //自带一个4K大小缓冲区
  LogLevel level_;     //日志级别
  int line_;           //行数
  SourceFile basename_;    //文件名
};

        Impl的5个成员,分别对应了日志消息生成的时间、存储的日志消息、日志消息级别、日志消息所在的文件名(注意,这个文件名指的是产生日志的线程所在的文件名)及其行数,这几乎构成了在本文最初所说的muduo日志消息格式中的大部分要素。而对于Impl的构造函数,则需要指定这些信息,来看下Impl类唯一的构造函数以及finish函数:

Impl::Impl(LogLevel level, int savedErrno, const SourceFile& file, int line)
  : time_(Timestamp::now()),
    stream_(),
    level_(level),
    line_(line),
    basename_(file)
{
  formatTime();//根据time_获取当前时间,写入缓冲区
  CurrentThread::tid();
  stream_ << T(CurrentThread::tidString(), CurrentThread::tidStringLength());//向缓冲区中写入线程id
  stream_ << T(LogLevelName[level], 6);//向缓冲区中写入日志级别
  if (savedErrno != 0)
  {
    stream_ << strerror_tl(savedErrno) << " (errno=" << savedErrno << ") ";
  }
}
void Impl::finish()//向缓冲区中写入文件名和行号
{
  stream_ << " - " << basename_ << ':' << line_ << '\n';
}

        可以看到,在构造函数中,就会将当前时间、当前线程id以及日志级别写入到缓冲区stream_中,如果构造函数的第二个参数不为0,那么还会把系统错误errno对应的错误描述也添加到缓冲区stream_中。而在finish函数中,则会将文件名和行号写入缓冲区。至此可以看到,通过Impl类可以将日志格式中除日志正文外的所有要素都写入缓冲区stream_中,那么,日志正文又是何时写入呢?

        完整的日志输出,是通过Logger类来实现的,其定义如下所示:

class Logger
{
 public:
  enum LogLevel   //几种日志级别
  {
    TRACE,  
    DEBUG,  
    INFO,   
    WARN,   
    ERROR,  
    FATAL,  
    NUM_LOG_LEVELS,
  };

  // compile time calculation of basename of source file
  ...
  //用构造函数的参数去初始化impl类型成员,即初始化缓冲区
  Logger(SourceFile file, int line);
  Logger(SourceFile file, int line, LogLevel level);
  Logger(SourceFile file, int line, LogLevel level, const char* func);
  Logger(SourceFile file, int line, bool toAbort);
  ~Logger();

  LogStream& stream() { return impl_.stream_; }

  static LogLevel logLevel();
  static void setLogLevel(LogLevel level);

  typedef void (*OutputFunc)(const char* msg, int len);
  typedef void (*FlushFunc)();
  static void setOutput(OutputFunc);
  static void setFlush(FlushFunc);
  static void setTimeZone(const TimeZone& tz);
 private:
  Impl impl_;//构造Logger类时就会构造impl_,此时缓冲区中就会自动写入当前线程id和日志级别
};
Logger::~Logger()
{
  impl_.finish();//析构时向缓冲区写入文件名和行号
  const LogStream::Buffer& buf(stream().buffer());//获取当前缓冲区的所有数据
  g_output(buf.data(), buf.length());//调用输出函数
  ...
}

        可以看到,在Logger类中,存在一个Impl类的实例,Logger类的多种构造函数实际上也是用构造参数去构造了impl,因此,日志消息本身实际上是存在于impl_中的,如前所述,它在构造时会向impl_的缓冲区stream_中写入时间、线程id和日志级别,而在Logger析构时,又会调用finish函数,向impl_的缓冲区stream_中写入文件名和行号。因此,如果想要写入日志正文,实际上也是对impl_的缓冲区stream_进行写入,如通过Logger实例执行stream()<<"log message";那么在该Logger实例析构时,就会将时间+线程id+日志级别+"log message"+文件名+行号写到Logger中impl_成员的缓冲区中。

        当然,Logger调用析构函数时,日志才算是按照格式完整写好,此时就需要立刻将这条日志”写出到某个指定的地方“来完成日志的输出功能。实际的日志输出,是通过一个全局函数指针g_output实现的,当日志写完后,Logger的析构函数就会调用g_output所指向的函数进行日志输出,g_output可由setOutput函数进行设置,也提供默认的输出函数,如下所示:

void defaultOutput(const char* msg, int len)
{
  size_t n = fwrite(msg, 1, len, stdout);
  //FIXME check n
  (void)n;
}

       可见,默认的输出函数就是将日志消息写到到stdout即标准输出中,在未重定向stdout的情况下,会将这条日志消息打印到终端屏幕上。 

       至此,也就完成了日志的输出。

 

       根据Logger类也可以知道,每一条日志消息应当都对应一个Logger类的实例,实例构造和析构时会自动写入一些必备的消息元素,并在析构时自动将该日志消息通过g_output进行输出。

 

       muduo中每一种日志级别都对应了一条宏定义,如下所示:

#define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
  muduo::Logger(__FILE__, __LINE__, muduo::Logger::TRACE, __func__).stream()
#define LOG_DEBUG if (muduo::Logger::logLevel() <= muduo::Logger::DEBUG) \
  muduo::Logger(__FILE__, __LINE__, muduo::Logger::DEBUG, __func__).stream()
#define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) \
  muduo::Logger(__FILE__, __LINE__).stream()
#define LOG_WARN muduo::Logger(__FILE__, __LINE__, muduo::Logger::WARN).stream()
#define LOG_ERROR muduo::Logger(__FILE__, __LINE__, muduo::Logger::ERROR).stream()
#define LOG_FATAL muduo::Logger(__FILE__, __LINE__, muduo::Logger::FATAL).stream()
#define LOG_SYSERR muduo::Logger(__FILE__, __LINE__, false).stream()
#define LOG_SYSFATAL muduo::Logger(__FILE__, __LINE__, true).stream()

        这些宏定义,最终都是对应impl_成员的缓冲区,只不过用来构造impl_的日志级别参数有所不同。这也就相当于构造了不同的临时缓冲区对象,如调用LOG_Info<<"log info";就会构造一个临时的Logger对象,在该条语句执行结束后就会析构,这样就可以通过一条语句实现日志消息的写入和输出,最终将 时间+线程id+日志级别+"log message"+文件名+行号 打印到终端屏幕上。

        每一条日志消息都会对应一个Logger的临时对象,这样的好处是可以及时的将日志消息进行输出,占用的内存也可以立即释放。

 

总结

       至此,分析了muduo中日志消息的存储以及输出,不过还未涉及到异步的实现,将在下文中分析。

       日志的存储主要涉及到两个类:

        FixedBuffer用于实现一个指定大小的缓冲区,是每条日志消息的实际存储位置;

        LogSteam中含有FixedBuffer的一个实例,其大小默认为4K,因此LogStream也通过FixedBuffer实现了日志消息的存储,除此之外,LogStream类还重载了一系列的"<<",由此来实现”日志流“,从而可以流的形式来写入日志消息;

       日志的输出也主要涉及到两个类:

        Impl类包含了一个LogStream实例,用于存储日志消息,Impl类在构造和调用finish函数时会向LogStream中写入时间等信息;

       Logger类中包含了一个Impl类实例,可以通过stream()函数来获取该实例下的LogStream,从而向其中写入日志正文。在Logger析构时调用g_output所指向的函数,将缓冲区中的日志消息全部输出。

       每条日志消息对应了一个Logger的临时对象,每条日志消息对应的缓冲区大小默认为4K。

你可能感兴趣的:(muduo)