muduo 日志库学习(二) 对大佬的博客进行一点点补充 代码部分使用最新的代码讲解

转发原博:
大佬博客

上一篇文章讲到muduo日志库的基础部分,现在来讲muduo日志库的异步日志工作流程。除了异步,muduo日志库还具有自动把数据从FILE结构体缓冲区flush到硬盘功能和定期roll(回滚)日志文件的功能。

异步:

异步日志由LogFile{.h, .cc}、AsyncLogging{.h, .cc}中定义的类来配合工作的。主要是有两个类 LogFile 和AsyncLogging。在使用异步日志前,要更改Logger的默认输出函数,使得Logger的默认输出函数向AsyncLogging输入日志内容(具体的修改可参考上一篇文章)。
LogFile类和AsyncLogging类各有自己的buffer(在下文中,分别记为file_buffer和async_buffer)。
当用户使用LOG_*写入日志内容时,将会把日志内容写入到async_buffer中,当async_buffer写满了,就会把async_buffer中的内容写入到file_buffer中。在LogFile类中,会自动将file_buffer的内容flush到硬盘。
这是数据流的大致流向,读者先有一个大致的方向。具体是怎么异步的,下面会慢慢细说。
AsyncLogging类有一个线程成员变量,AsyncLogging类的代码由两个线程执行。一个是使用LOG_*进行写日志的线程(更确切地说是使用了LOG_*的所有线程)另外一个就是AsyncLogging类的线程成员变量了,分别把这两个线程称为前台和后台线程,他们也分别是同步问题中的生产者和消费者。一般情况下,前台线程和后台线程各有两个buffer。前面说的async_buffer是前台和后台buffer的统称。
先贴上前台线程代码。代码中用到了currentBuffer_和nextBuffer_两个前台buffer。还有一个buffers_智能指针数组(其类型为boost::ptr_vector),其存放已经满了的前台buffer。

void AsyncLogging::append(const char* logline, int len)
{
    muduo::MutexLockGuard lock(mutex_);
    if (currentBuffer_->avail() > len)
    {
      currentBuffer_->append(logline, len);
    }
    else // 生产者线程生产太快,用nextBuffer填充currentBuffer,则nextBuffer为nullptr
    {
      buffers_.push_back(std::move(currentBuffer_));

      if (nextBuffer_)
      {
        currentBuffer_ = std::move(nextBuffer_);
      }
      else
      {
        currentBuffer_.reset(new Buffer); // Rarely happens
      }
      currentBuffer_->append(logline, len);
      cond_.notify(); // 通知消费者线程
    }
  }

前台线程是生产者,前台线程有2快buffer,一块是currentbuffer另外一个是nextbuffer,具体情况看代码注释。

可以看到前台线程所做的工作比较简单,如果currentBuffer_够用,就把日志内容写入到currentBuffer_中,如果不够用(就认为其满了),就把currentBuffer_放到已满buffer数组中,等待消费者线程(即后台线程)来取。并且把currentBuffer_指向nextBuffer_的buffer。

可以看到前台线程把两个前台buffer用掉了,下面可以看看后台线程是怎么归还这个两个buffer的。

在AsyncLogging类中还有下面的声明:

typedef boost::ptr_vector<Buffer> BufferVector;
typedef BufferVector::auto_type BufferPtr;

底下是消费者线程的代码:

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;
      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())  // unusual usage!
          {
            cond_.waitForSeconds(flushInterval_);
          }
          buffers_.push_back(std::move(currentBuffer_));
          currentBuffer_ = std::move(newBuffer1);
          buffersToWrite.swap(buffers_);
          if (!nextBuffer_) // 其实就是前段线程写日志太快,所以用完了第一块buffer,那么将第二块buffer移为currentBuffer
          {
            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)
        {
          // FIXME: use unbuffered stdio FILE ? or use ::writev ?
          output.append(buffer->data(), buffer->length());
        }

        if (buffersToWrite.size() > 2)
        {
          // drop non-bzero-ed buffers, avoid trashing
          buffersToWrite.resize(2);
        }

        if (!newBuffer1) //将其归还
        {
          assert(!buffersToWrite.empty());
          newBuffer1 = std::move(buffersToWrite.back());
          buffersToWrite.pop_back();
          newBuffer1->reset();
        }

        if (!newBuffer2) // 将buffer归还
        {
          assert(!buffersToWrite.empty());
          newBuffer2 = std::move(buffersToWrite.back());
          buffersToWrite.pop_back();
          newBuffer2->reset();
        }
        buffersToWrite.clear();
        output.flush(); //fflush()会强迫将缓冲区内的数据写回参数stream 指定的文件中
      }
      output.flush();
}

首先我们得明白

cond_.waitForSeconds(flushInterval_);
pthread_cond_timedwait(&pcond_, mutex_.getPthreadMutex(), &abstime)

我们嘚知道第二个函数是怎么执行的,其实它实现了两个功能

第一:如果在3s内,生产者生产日志过快,调用了notify,则次函数不用等待3s,而是直接返回。

第二:如果3s内,没有被唤醒,则会唤醒,返回。

无论是哪种情况,都还会有一个currentBuffer_前台buffer正在使用。将这个currentBuffer_放到已满buffers_数组中。这样buffers_就有了待进行IO的buffer了。
将bufferToWrite和buffers_进行swap。这就完成了将写了日志记录的buffer从前台线程到后台线程的转变。后台线程慢慢进行IO即可。这个指针的交换很重要,完成了异步的过程,因为buffer给前端,bufferToWrite给后端去写日志
这个过程就完成了日志的异步输出。下面说一下后台线程怎么进行具体的IO的。这个过程将涉及到另外一个类 LogFile。

class LogFile::File :boost::noncopyable
{
 public:
  explicit File(const string& filename)
    : fp_(::fopen(filename.data(),"ae")),
      writtenBytes_(0)
  {
      //修改FILE结构体默认提供的缓冲区。主要原因可能是因为默认提供的buffer比较小。
      ::setbuffer(fp_, buffer_, sizeofbuffer_);
   }
 
  ~File()
  {
    ::fclose(fp_);
  }
 
  void append(const char* logline, const size_tlen)
  {
      //这里的代码是 循环调用write成员函数,直到把logline的所有数据都写入去。
   }
    writtenBytes_ += len;
  }
 
  void flush()
  {
    ::fflush(fp_);
  }
 
  size_t writtenBytes() const { returnwrittenBytes_; }
 
 private:
 
  size_t write(const char* logline, size_t len)
  {
#undef fwrite_unlocked
//为了快速,使用unlocked(无锁)的fwrite函数。平时我们使用的C语言IO函数,都是线程安全的,
//为了做到线程安全,会在函数的内部加锁。这会拖慢速度。而对于这个类。可以保证,从
//始到终只有一个线程能访问,所以无需进行加锁操作。
    return ::fwrite_unlocked(logline, 1, len,fp_);
  }
 
  FILE* fp_;
  char buffer_[64*1024];
  size_t writtenBytes_;
};

首先的知道一个系统调用函数,flush() 是flush日志内容到磁盘。

muduo里面的自动flush还是比较容易看懂的。在前面贴出的后台线程中的最后两条语句都是output.flush(); 其中,倒数第二句就是自动flush了。后台线程每次休眠的时间都是flushInterval_,即刷新间隔。无论后台线程是被前台线程唤醒还是超时醒来,其的醒来都是在flushInterval_时间内醒来。醒来后一路执行下去,总会执行到output.flush(); flush日志内容到磁盘。

自动Flush:

muduo里面的自动flush还是比较容易看懂的。在前面贴出的后台线程中的最后两条语句都是output.flush(); 其中,倒数第二句就是自动flush了。后台线程每次休眠的时间都是flushInterval_,即刷新间隔。无论后台线程是被前台线程唤醒还是超时醒来,其的醒来都是在flushInterval_时间内醒来。醒来后一路执行下去,总会执行到output.flush(); flush日志内容到磁盘。

或许有人会疑问:后台线程醒来后,一路执行下去会进入File类的append函数,进行磁盘IO,这是阻塞型的IO,这就有可能在flushInterval_时间内不能执行到output.flush()了。其实,进入append函数进行磁盘IO,不正是我们所说的,想要的flush吗?

在muduo的配套书籍《Linux多线程服务端编程 使用muduo C++网络库》中(p.110)说到,rolling的条件通常有两个:文件大小(例如每写满1GB就换下一个文件)和时间(例如每天零点新建一个日志文件,不论前一个文件有没有写满)。

在muduo中,确实会根据文件大小和时间来主动滚动文件。不过,时间并不是每天的零点。具体是什么时候,是不确定的,其是根据日志的疏密情况来判断的。但还是会在凌晨的第一个小时里滚动文件。

还是贴代码吧。要注意阅读注释部分。

voidLogFile::append_unlocked(const char* logline, int len)
{
  file_->append(logline, len); //调用File类的append函数,进行IO
 
  //写入的数据已经超过日志滚动大小了。这里是根据文件大小进行滚动
  if(file_->writtenBytes() > rollSize_)
  {
    rollFile();
  }
  else //这里根据时间来判断是否到了滚动的时候
  {
    //在下面的代码中,可以看到并不是根据时间,而是根据count,即次数。
    //当count_大于   kCheckTimeRoll_(其值是一个常量1024),时,就认为
    //其到了滚动时间了。
    if(count_ > kCheckTimeRoll_)
    {
      count_= 0; //注意,这里已经将count_清零了。
      time_tnow = ::time(NULL);
 
      //kRollPerSeconds_ 为常量60*60*24即一天的秒数。
      //thisPeriod_的计算结果为now当天的零时,即now向下取一天的整数
      time_tthisPeriod_ = (now / kRollPerSeconds_) * kRollPerSeconds_;
 
      //startOfPeriod_是上次滚动那天的零时。
      //这一天不是上一次滚动文件的那一天  才会滚动文件。
      if(thisPeriod_ != startOfPeriod_)
      {
        rollFile();
      }
      else if(now - lastFlush_ > flushInterval_)
      {
        lastFlush_ = now;
        file_->flush(); //刷新文件
      }
    }
    else
    {
      //计数。每当LogFile的append函数被调用一次,count_才有可能加一。
      ++count_;
    }
  }
}

这里可以看到,当LogFile的append函数被调用一次,count_才有可能加一。而append会在什么时候被调用呢?

回到AsyncLogging的threadFunc函数,可以看到,当后台线程从条件变量那里醒来一次就会调用一次LogFile的append函数。

后台线程有两种情况会醒来,一是至少一个前台buffer满了;二是超时。

如果是日志写得比较多,快。那么很容易导致前台buffer满,进而后台线程醒来调用LogFile的append函数。不过在这种情况下,日志文件容易因为超过滚动大小而被滚动。此时,count_也是会被计数的。不过,并不可能导致滚动日志。因为日志写得多而快,很容易导致一天中因文件大小而滚动了很多次日志文件。这会使得条件thisPeriod_ != startOfPeriod_不能被满足,进而不能因时间滚动日志文件。

还有一种情况是后台线程因为超时醒来。这种情况下(日志写得比较少,慢),每次醒来调用LogFile的append函数都会导致count_被加一。默认情况下3秒钟醒来一次,那么要到达kCheckTimeRoll_(即1024)次,需要1024*3秒,大约是51分钟。由于条件 thisPeriod_ != startOfPeriod 的存在,同一天里的计数不算,并且计数清零(一开始的count_=0)。最终的结果是使得一天里最后一个小时的计数才会有效,所以在每天凌晨的第一个小时的某个时刻会进行滚动日志文件。经陈硕提醒,代码中的::time(NULL)函数返回的不是本地时间,而是UTC时间。所以东八区的中国,会在早上8点滚动日志文件。

最后别人问了一个问题?

你好, 我看了muduo的log库,有一点不明白: 如果程序中间crash, 最近flushInterval_(三秒)内的log丢失。应该咋找回来? 书上说每条log带有cookie, 在哪里体现了啊? void FixedBuffer<SIZE>::cookieStart() 这个是一个空函数啊… 如何理解啊?

答:这个函数的地址记录在coredump文件了。函数地址本身就是一个标志,用于日后查找的。

猜你喜欢

转载自blog.csdn.net/u014303647/article/details/88636584