muduo网络库——日志篇(一站看懂日志运转流程)

代码史诗,恢弘巨制。今天为大家带来的是muduo网络库之日志篇。
请大家跟随我的脚步,一起学习这段异步,双缓冲,多线程的高速日志模块。
听着就很吊。

muduo的日志模块分为前后端两个部分,主要由两个类负责:
前端:Logger 类
后端:AsyncLogging 类

我们先看前端部分:

Logger.h文件:

(Logger类中又定义了一些其他类,但是不要怕,我会给出注释)

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

  class SourceFile //用于从文件路径中提取出文件名
  {
   public:
    template<int N>
    SourceFile(const char (&arr)[N])
      : data_(arr),
        size_(N-1)
    {
      const char* slash = strrchr(data_, '/'); // 查找最后一个'/'的位置
      if (slash)
      {
        data_ = slash + 1;
        size_ -= static_cast<int>(data_ - arr);
      }
    }

    explicit SourceFile(const char* filename)
      : data_(filename)
    {
      const char* slash = strrchr(filename, '/');
      if (slash)
      {
        data_ = slash + 1;
      }
      size_ = static_cast<int>(strlen(data_));
    }

    const char* data_;
    int size_;
  };
  //不同情况的构造函数
  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();

  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:

class Impl //用于封装一条完整的日志,
{
 public:
  typedef Logger::LogLevel LogLevel;
  Impl(LogLevel level, int old_errno, const SourceFile& file, int line);
  void formatTime();
  void finish();

  Timestamp time_; //时间相关
  LogStream stream_; //封装了对左移运算符<<的重载和储存单条日志的缓冲区
  LogLevel level_;
  int line_;
  SourceFile basename_;
};

  Impl impl_;

 LogStream& stream() { return impl_.stream_; } //用法:Logger::stream()<<"日志信息"

};

然后搞了一些宏定义,给我们打日志用:

#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()

经过如上代码,我们可以看出,我们在写日志的时候,是创建了一个临时的 logger 类在初始化的时候填入了文件名,行数和日志级别等信息,然后把我们要输出的日志内容经过左移运算符传入到 stream 的缓冲区中。

然后我们看一下 Logger 类的构造函数:

Logger::Logger(SourceFile file, int line)
  : impl_(INFO, 0, file, line)
{
}

Logger::Logger(SourceFile file, int line, LogLevel level, const char* func)
  : impl_(level, 0, file, line)
{
  impl_.stream_ << func << ' ';
}

Logger::Logger(SourceFile file, int line, LogLevel level)
  : impl_(level, 0, file, line)
{
}

Logger::Logger(SourceFile file, int line, bool toAbort)
  : impl_(toAbort?FATAL:ERROR, errno, file, line)
{
}

其实就是用这些参数初始化了 impl_ 。

所以我们看一下 Impl 类的构造函数:

Logger::Impl::Impl(LogLevel level, int savedErrno, const SourceFile& file, int line)
  : time_(Timestamp::now()),
    stream_(),
    level_(level),
    line_(line),
    basename_(file)
{
  formatTime();
  CurrentThread::tid();
  stream_ << T(CurrentThread::tidString(), CurrentThread::tidStringLength());
  stream_ << T(LogLevelName[level], 6);
  if (savedErrno != 0)
  {
    stream_ << strerror_tl(savedErrno) << " (errno=" << savedErrno << ") ";
  }
}

可以看出 impl_ 用传进来的参数也通过左移运算符传入到了 stream_ 的缓冲区中。其中牵扯到一些其他的实现细节这里不再赘述,否则就要扯远了,本文旨在让大家理解日志模块的大致运行思路。

以上就是单条日志的大致组装过程,现在一条日志已经存在于 stream_ 的缓冲区中了。

现在这个临时的 Logger 类对象最后的使命就是把这条日志传给后端。这个功能是在对象的析构函数中实现的。(就显得很浪漫)

Logger 类的析构函数:

Logger::~Logger()
{
  impl_.finish();
  const LogStream::Buffer& buf(stream().buffer());
  g_output(buf.data(), buf.length());
  if (impl_.level_ == FATAL)
  {
    g_flush();
    abort();
  }
}

void Logger::setOutput(OutputFunc out)
{
  g_output = out;
}

内容并不复杂,故事的最后我们成功通过 g_output 函数把日志传给了后端,一个没有名字的 Logger 类对象默默从程序中消失了。

一刻也来不及为前端日志对象哀悼了,接下来登场的是

后端部分:

AsyncLogging.h 文件:

class AsyncLogging : noncopyable
{
 public:

  AsyncLogging(const string& basename,
               off_t rollSize,
               int flushInterval = 3);

  ~AsyncLogging()
  {
    if (running_)
    {
      stop();
    }
  }

  void append(const char* logline, int len);//上文中前端就是通过这个函数把数据传给后端的

  void start()
  {
    running_ = true;
    thread_.start();
    latch_.wait();
  }

  void stop() NO_THREAD_SAFETY_ANALYSIS
  {
    running_ = false;
    cond_.notify();
    thread_.join();
  }

 private:

  void threadFunc();//主要事件处理线程

  typedef muduo::detail::FixedBuffer<muduo::detail::kLargeBuffer> Buffer;
  typedef std::vector<std::unique_ptr<Buffer>> BufferVector;
  typedef BufferVector::value_type BufferPtr;

  const int flushInterval_;
  std::atomic<bool> running_;
  const string basename_;
  const off_t rollSize_;
  muduo::Thread thread_;
  muduo::CountDownLatch latch_;
  muduo::MutexLock mutex_;
  muduo::Condition cond_ GUARDED_BY(mutex_);
  BufferPtr currentBuffer_ GUARDED_BY(mutex_);//当前缓冲区,接收前端传过来的日志
  BufferPtr nextBuffer_ GUARDED_BY(mutex_);//预备缓冲区,currentBuffer_写满的时候第一时间顶上去
  BufferVector buffers_ GUARDED_BY(mutex_);//已满缓冲区数组,写满的Buffer都会进到这里
};

}

主要功能主要在 append 函数和 threadFunc 函数中。

append 函数:

void AsyncLogging::append(const char* logline, int len)
{
  muduo::MutexLockGuard lock(mutex_); //多线程写到这里,加锁保证安全
  if (currentBuffer_->avail() > len) //如果当前缓冲区够写
  {
    currentBuffer_->append(logline, len); //就直接写
  }
  else //当前缓冲区写满了
  {
    buffers_.push_back(std::move(currentBuffer_)); //把当前缓冲区入栈

    if (nextBuffer_) //存在预备缓冲区
    {
      currentBuffer_ = std::move(nextBuffer_); //替补出场,预备缓冲区成为新的当前缓冲区
    }
    else
    {
      currentBuffer_.reset(new Buffer);
    }
    currentBuffer_->append(logline, len); //这时候的当前缓冲区一定够空间存入新日志了
    cond_.notify(); //有新日志了生产者发出信号提醒消费者。
  }
}

下面是把日志写入文件的核心代码

!24 threadFunc 函数:!

void AsyncLogging::threadFunc()
{
  assert(running_ == true);
  latch_.countDown();
  LogFile output(basename_, rollSize_, false); //初始化文件类,这个文件类会自己更新文件,不会一直写入一个文件中
  BufferPtr newBuffer1(new Buffer); //替补缓冲区一号
  BufferPtr newBuffer2(new Buffer); //替补缓冲区二号
  newBuffer1->bzero();
  newBuffer2->bzero();
  BufferVector buffersToWrite; //用来给 buffers_ 替补的,
  buffersToWrite.reserve(16);
  while (running_)
  {//元气满满的一轮循环开始
    assert(newBuffer1 && newBuffer1->length() == 0);
    assert(newBuffer2 && newBuffer2->length() == 0);
    assert(buffersToWrite.empty());

    {
      muduo::MutexLockGuard lock(mutex_); //拿一下锁
      if (buffers_.empty())  //没有数据
      {
        cond_.waitForSeconds(flushInterval_); //等待生产者发出有数据的信号,一直没有就等3秒超时
      }
      buffers_.push_back(std::move(currentBuffer_)); //放里面等会全都写了
      currentBuffer_ = std::move(newBuffer1); //替补登场,每次循环后面会补充替补
      buffersToWrite.swap(buffers_); //交换两个数组,把空的数组给前端,把有内容的数组拿到后端来写。
      if (!nextBuffer_)
      {
        nextBuffer_ = std::move(newBuffer2); //替补登场,不能让前端的缓冲区不够用了
      }
    }

    assert(!buffersToWrite.empty());

    if (buffersToWrite.size() > 25) //日志太多了,执行罢工操作
    {
      char buf[256];
      snprintf(buf, sizeof buf, "Dropped log messages at %s, %zd larger buffers\n",
               Timestamp::now().toFormattedString().c_str(),
               buffersToWrite.size()-2);
      fputs(buf, stderr);
      output.append(buf, static_cast<int>(strlen(buf))); //只写这一条,说一下日志太多了
      buffersToWrite.erase(buffersToWrite.begin()+2, buffersToWrite.end()); //把日志全丢了,主打一个潇洒
    } //打工人上班前的幻想,大多数时候是工作很多,但是拼尽全力又能做完
    //日志没有压垮打工人,那就本本分分的干活
    for (const auto& buffer : buffersToWrite) //遍历数组,开始干活
    {
      output.append(buffer->data(), buffer->length()); //向文件类里面写入日志
    }

    if (buffersToWrite.size() > 2)
    {
      buffersToWrite.resize(2); //删掉多余缓冲区,留两个是为了下面补充替补的时候就不需要重复申请了
    }

    if (!newBuffer1) //补充替补缓冲区一号,如果需要的话
    {
      assert(!buffersToWrite.empty());
      newBuffer1 = std::move(buffersToWrite.back());
      buffersToWrite.pop_back();
      newBuffer1->reset();
    }

    if (!newBuffer2) //补充替补缓冲区二号,如果需要的话
    {
      assert(!buffersToWrite.empty());
      newBuffer2 = std::move(buffersToWrite.back());
      buffersToWrite.pop_back();
      newBuffer2->reset();
    }

    buffersToWrite.clear();
    output.flush(); //累了,文件类的细节不想讲了,反正就是写好了
    //别睡了打工人,讲到这里,新的循环要开始了
  }
  output.flush();
}

总结:

至此我们了解完了muduo网络库的日志模块的主要内容,从多线程的前端生产者生产日志,到后端单线程一个消费者利用双缓冲处理日志。相信各位也大体上了解了日志模块是怎么工作的了,如果看了一遍没看懂,那可能是我废话太多影响各位理解了,再看一遍就好了。

如果看了两遍,还是没看懂。
该休息了,我的朋友。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值