muduo源码学习笔记(2)

前言:

​ 对于muduo库,我觉得,光Linux多线程上提到的一些实现,还是不够的,在base/里面,还有/net里面提供了很多不错的实现,值得去学习,暑假算是看看muduo的百分之八十的源码,并对其进行了一次实现,(剩下的在最近总结的时候,也会开始看看,并实现一遍),对于muduo库,简单谈谈自己对其实现的理解。

日志缓存流LogStream

​ 在muduo实现的基础日志类,一共用来三个文件,LogFilem, LogStream,Loggin。本着自底向上原则,我们从底层的实现慢慢讲起。

​ 在LogStream中,定义了三个类,两个底层的类分别是FixedBuffer和Fmt。

​ muduo在实现写日志的时候,并没有直接写向stderr或者IO流,而是会写入自己定义的FixedBuffer(缓冲流中),下面就让我们看看缓冲流的定义和实现吧!

muduo源码学习笔记(2)_第1张图片

​ FixedBuffer定义在namespace detail算是用户不可见,在创建FixedBuffer的时候可以向其输出大小来控制内部的buf的大小,也定义两个常量分别对应大小buf

        const int kSmallBuffer = 4000;
        const int kLargeBuffer = 4000 * 1024;

​ 一共有三个成员,data就是缓存的数据,cur指向缓存数据的末尾的下一个和cookie_指针

​ 我们需要重点关注以下函数

 //追加字符串  
 void append(const char* /*restrict*/ buf, size_t len)
  {
    // FIXME: append partially
    if (implicit_cast(avail()) > len)
    {
      memcpy(cur_, buf, len);
      cur_ += len;
    }
  }
  //设置缓存
  void setCookie(void (*cookie)())
  { cookie_ = cookie; };

​ 值得注意的是下面在实现转化的时候在detail中定义了

const char digits[] = "9876543210123456789";
const char* zero = digits + 9;
BOOST_STATIC_ASSERT(sizeof(digits) == 20);
​
const char digitsHex[] = "0123456789ABCDEF";
BOOST_STATIC_ASSERT(sizeof digitsHex == 17);
​
// 先用求余除法求出 value转化成字符串的倒着顺序的,然后用std::reverse对其进行倒转
// 另一方面用了取数的方法,相比直接申请赋值会更有效率一些
template
size_t convert(char buf[], T value)
{
  T i = value;
  char* p = buf;
​
  do
  {
    int lsd = static_cast(i % 10);
    i /= 10;
    *p++ = zero[lsd];
  } while (i != 0);
​
  if (value < 0)
  {
    *p++ = '-';
  }
  *p = '\0';
  std::reverse(buf, p);      
​
  return p - buf;
}
/// 同样的实现不过是16进制的而已
size_t convertHex(char buf[], uintptr_t value);

下面我们就开始看看LogStream

muduo源码学习笔记(2)_第2张图片

​ 它有两个数据成员,一个是缓冲流buffer,另一个是一个成员是表示数值的最大字节数(用于后面的优化)。

​ 在里面重载的若干函数,它是通过一个模板函数,对其进行统一起来书写

template 
void LogStream::formatInteger(T v)
{
    if (buffer_.avail() >= kMaxNumericSize)
    {
        size_t len = convert(buffer_.current(),  v);
        buffer_.add(len);
    }
}
​
LogStream& LogStream::operator<<(int v)
{
    formatInteger(v);    
    return *this;
}

​ 值得注意的是,里面有一个重载函数的形参是StringPrice。这也是一个亮点的地方,它实现了无论是字符串还是string都可以进行零拷贝数据,只拷贝指针来传递,也是一个不错的方法,具体怎么实现的,看看它的构造函数也就明白了一切。

   class StringPiece {
    public:
        StringPiece() : ptr_(nullptr), length_(0) {}
        StringPiece(const char *str)
                : ptr_(str), length_(static_cast(strlen(ptr_))) {}
        StringPiece(const unsigned char* str)
                : ptr_(reinterpret_cast(str)),
                  length_(static_cast(strlen(ptr_))) {}
        StringPiece(const string &str)
        : ptr_(str.c_str()), length_(static_cast(str.size())) {}
        StringPiece(const char* offset, int len)
                : ptr_(offset), length_(len) { }
          private:
        const char*         ptr_;
        int                 length_;
       }

​ 但是现在的Logstream也并不全面,对于给定一个格式的输入无从下手,于是Fmt就有了

   class Fmt
    {
    public:
        template
        Fmt(const char *fmt, T val);
​
        const char *data() const
        { return buf_; }
        int length() const
        { return length_; }
    private:
        char buf_[32];
        int length_;
    };
    
    
    template 
    Fmt::Fmt(const char *fmt, T val)
    {
        // 判断必须为算数类型
        BOOST_STATIC_ASSERT(boost::is_arithmetic::value == true);
​
        length_ = snprintf(buf_, sizeof buf_, fmt, val);
        assert(static_cast(length_) < sizeof buf_);
    }
​
    inline LogStream& operator<<(LogStream& s, const Fmt& fmt)
    {
        s.append(fmt.data(), fmt.length());
        return s;
    }

写日志Logger的实现

muduo源码学习笔记(2)_第3张图片

​ 先讲讲Logger,Logger的实现的机制就是靠初始化一个impl,再向impl内写入数据,在析构的时候再将impl_中的buffer提取出来,使用用户定义的outputFunc,输出buffer。如果未定义,它会使用默认的输入输出函数,即直接输出到stdout中。

​ impl的意思是implement,它仅仅负责向stream中进行输入,让我们先看看lmpl_类里面到底包含什么吧。

        class Impl
        {
        public:
            typedef Logger::LogLevel LogLevel;
            Impl(LogLevel level, int old_errno, const SourceFile& file, int line);
            void formatTime();     //格式化时间,时间是以毫秒计算的
            void finish();       // 将文件名和line写入
                            
            Timestamp time_;        //保存写日志当前的时间
            LogStream stream_;        //日志流
            LogLevel  level_;      //日志等级
            int       line_;          //出错的文件行号
            SourceFile basename_;   // 保存这文件名和文件名的大小
        };
​
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();  // 获取线程id
    stream_ << T(CurrentThread::tidString(), CurrentThread::tidStringLength());  //写入线程id
    stream_ << T(LogLevelName[level], 6);   //写入loglevel
    if (savedErrno != 0)
    {
        stream_ << strerror_tl(savedErrno) << "(errno = " << savedErrno << ")";
    }
}
​
​

​ 每当有一个Logger对象就会初始化一个impl,并且向里面写入数据

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

​ 在析构的时候调用析构函数,使用outputFunc函数写入数据

Logger::~Logger()
{
    impl_.finish();    //写入文件名
    const LogStream::Buffer& buf(stream().buffer());
    g_output(buf.data(), buf.length());
    if (impl_.level_ == FATAL)
    {
        g_flush();
        abort();
    }
}
​
void defaultOutput(const char* msg, int len)
{
     size_t n = fwrite(msg, 1, len, stdout);  //默认向stdout写入数据
     //FIXME check n
     (void)n;  
 }
 void defaultFlush()
 {
      fflush(stdout);
  }

​ 请注意,在Loggin.cc中定义了两个全局指针,所以我们在使用Loggin类之前只需要将两个函数指针set一下,就可以在整个文件中获得相同的效果了。

    Logger::OutputFunc g_output = defaultOutput;
    Logger::FlushFunc g_flush = defaultFlush;

​ 但是我们每次使用这个类,都要写一个对象来使用,是不是未免它麻烦,我们可以使宏定义和重载<<来实现

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

​ 在这里我们使用了一个匿名类,将日志写入后,还将其LogStream返回出来便于使用LogStream写入其他数据。

​ 等等,这里我们是不是忘记介绍了一个东西,就是日志的级别和默认记录的日志级别

  enum LogLevel
        {
            TRACE,
            DEBUG,
            INFO,
            WARN,
            ERROR,
            FATAL,
            NUM_LOG_LEVELS,
        };

​ muduo的日志级别一般都在INFO,即INFO级别的日志才会打印出来,具体实现也是依赖一个全局的日志级别的LogLevel,通过读取环境变量设置LogLevel,也可以设置

   Logger::LogLevel initLogLevel()
    {
        if (::getenv("MUDUO_LOG_TRACE"))
            return Logger::TRACE;
        else if (::getenv("MUDUO_LOG_DEBUG"))
            return Logger::DEBUG;
        else
            return Logger::INFO;
    }
    Logger::LogLevel g_logLevel = initLogLevel();
    void Logger::setLogLevel(Logger::LogLevel level)
    {
        g_logLevel = level;
    }

​ 到此,整个Loggin类就全部剖析完了,但是还有一个宏,及宏的小小用法我觉得讲一讲也未尝不可。

// 用了宏的字符串的拼接"'"#val"'Must be non NULL"
#define CHECK_NOTNULL(val) \
    ::jmuduo::CheckNotNull(__FILE__, __LINE__, "'"#val"'Must be non NULL", (val))
​
    template 
    T* CheckNotNull(Logger::SourceFile file, int line, const char *names, T* ptr)
    {
        if (ptr == NULL)
        {
            Logger(file, line, Logger::FATAL).stream() << names;
        }
        return ptr;
    }
}

输出到文件LogFIle

​ 在上面我们实现了如何写日志,并定义了日志的输出接口,在LogFile中我们就可以使用这些接口,将日志输出到文件中,但是这里又有新的问题产生了

​ 1.文件名要如何规定才能方便查阅?

​ 2.程序崩溃了日志怎么办?

​ 3.文件要如何滚动?时间?大小?还是两个都要?

在muduo中,一个典型的日志文件的文件名如下:

​ LogFile_test.20180821-072337.hostname.19238.log

  • ​ 第一部分是进程名字,用于区分那个服务的程序日志​ 第二部分是文件创建的事件,这样我们就可以用通配符来查询一段事件的日志 ​ 第三部分是机器名称 ​ 第四部分是进程ID,如果程序反复重启,我们就会得到不同的日志文件

如果程序崩溃了怎么办?

  • ​ 定时将缓冲区的日志flush硬盘上​ 日志消息带有cookie,其为某个函数的地址,这样就可以在core dump文件中查找cookie尚未来得及写入磁盘的消息

日志滚动

  • ​ 写满固定的日志的大小,就滚动​ 即使大小没有写满,固定时间也滚动日志

    在muduo中,LogFIle主要负责日志滚动及其他逻辑,namespace FileUtil中负责日志的创建和写入、flush日志的接口。

    我们首先看看FileUtil中实现了什么功能

       /// read small file < 64kb
        class ReadSmallFile: boost::noncopyable
        {
        public:
            ReadSmallFile(StringArg filename);
            ~ReadSmallFile();
            /***
             * @maxsize 读入最大字节数
             * @content 传出string
             * @filesize 传入传出参数文件大小
             * @modifyTime 传出参数修改时间
             * @createTime 传出参数创建事件
             * @return 返回err,没有就是0
            */
            template 
            int readToString(int        maxSize,
                             String     *content,
                             int64_t    *filesize,
                             int64_t    *modifyTime,
                             int64_t    *createtime);   
            // 将文件内容加载到内存buf_中
            int readToBuffer(int *size);
​
            const char* buffer() const { return buf_; }
​
            static const int kBuffSize = 64 * 1024;
        private:
            int fd_;
            int err_;
            char buf_[kBuffSize];
        };
        //读取filename对应的文件中,并将其通过content返回
        template 
        int readFile(StringArg filename,
                     int        maxSize,
                     String     content,
                     int64_t* fileSize = NULL,
                     int64_t* modifyTime = NULL,
                     int64_t* createTime = NULL)
        {
            ReadSmallFile file(filename);
            return file.readToString(maxSize, content, fileSize, modifyTime, createTime);
        };

​ 这个虽然与本节无关,但是还是总结了,以后就直接提提就可以了,本节重点关注下面另一个类的实现

class AppendFile : boost::noncopyable
{
 public:
  explicit AppendFile(StringArg filename);
​
  ~AppendFile();
​
  void append(const char* logline, const size_t len);
​
  void flush();
​
  off_t writtenBytes() const { return writtenBytes_; }
​
 private:
​
  size_t write(const char* logline, size_t len);
​
  FILE* fp_; 
  char buffer_[64*1024];
  off_t writtenBytes_;
};

​ 共有三个成员,一个是缓冲区buffer,一个是文件指针fp,一个是已经写入的字节数,muduo在实现文件写入操作的时候,为了实现异步写入磁盘(磁盘的写入速度远小于内存的写入的速度),使用了流缓冲技术,向缓冲中写入日志,再统一::flush()进入磁盘中。

FileUtil::AppendFile::AppendFile(StringArg filename)
    :fp_(fopen(filename.c_str(), "we")),         // e for   O_CLOEXEC
     writtenBytes_(0)
{
    assert(fp_);
    ::setbuffer(fp_, buffer_, sizeof buffer_);  //设置文件流缓冲
}

​ 在实现的时候,由于日志会被当做背景线程使用,所以是在单线程的情况下使用,所以使用了::fwrite_unlocked,进一步提示写入速度。

​ 下面我们开始介绍LogFIle,muduo的日志滚动机制是,一个根据文件大小滚动,二是在每天的零点更新一个日志。并且定义了一个FlushInterval,用来规定日志的flush时间。

   class LogFile: boost::noncopyable
    {
    public:
        LogFile(const string& basename,
                off_t rollSize,
                bool threadSafe = true,
                int flushInterval = 3,
                int checkEveryN = 1024);
        ~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);
​
        const string basename_;    /// 文件名
        const off_t rollSize_;      /// 滚动大小
        const int flushInterval_;   /// 写入周期
        const int checkEveryN_;     /// 检测时间的写入次数
​
        int count_;                 /// 写入次数
​
        boost::scoped_ptr mutex_;
        time_t startOfPeriod_;        //此次日志写入时间 以每天零点为准
        time_t lastRoll_;                 //上次滚动时间
        time_t lastFlush_;                  //上次flush时间
        boost::scoped_ptr file_;  //文件对象
​
        const static int kRollPerSecond_ = 60*60*24;  //滚动时间常量
    };

​ 在这里我们重点关注下面这些函数的实现

void LogFile::append_unlocked(const char* logline, int len)
{
    file_->append(logline, len);
​
    if (file_->writtenBytes() > rollSize_)   //根据大小滚动
    {
        rollFile();
    }
    else
    {
        ++count_;
        if (count_ >= checkEveryN_)    //如果超过检测次数,就检查一次时间
        {
​
            count_ = 0;
            time_t now = ::time(NULL);
            time_t thisPeriod_ = now / kRollPerSecond_* kRollPerSecond_;     /// 小数点后面的默认舍去,所以就可以判断是今天的时间还是明天的时间
            if (thisPeriod_ != startOfPeriod_)      //检测滚动时间
            {
                rollFile();
            }
            else if (now - lastFlush_ > flushInterval_)   //检查flush时间
            {
                lastFlush_ = now;
                file_->flush();
            }
        }
    }
    //滚动日志
    bool LogFile::rollFile()
    {
        time_t now = 0;
        string filename = getLogFileName(basename_, &now);
        time_t start = now / kRollPerSecond_ * kRollPerSecond_;    /// 时间取整
​
        if (now > lastRoll_)
        {
            lastRoll_ = now;
            lastFlush_ = now;
            startOfPeriod_ = start;
            file_.reset(new FileUtil::AppendFile(filename));
            return true;
        }
        return false;
    }

 

你可能感兴趣的:(C++,Muduo)