muduo筆記 日志庫(一)


后半部分見muduo筆記 日志庫(二)

muduo日志庫是異步高性能日志庫,其性能開銷大約是前端每寫一條日志消息耗時1.0us~1.6us。

日志庫模型

采用雙緩沖區(double buffering)交互技術。基本思想是准備2部分buffer:A和B,前端(front end)線程往buffer A填入數據(日志消息),后端(back end)線程負責將buffer B寫入日志文件。當A寫滿時,交換A和B。如此往復。

實現時,在后端設置一個已滿緩沖隊列(Buffer1~n,2<=n<=16),用於緩存一個周期內臨時要寫的日志消息。

這樣做到好處在於:
1)線程安全;2)非阻塞。

這樣,2個buffer在前端寫日志時,不必等待磁盤文件操作,也避免每寫一條日志消息都觸發后端線程。

異常處理:
當一個周期內,產生過多Buffer入隊列,當超過隊列元素上限數量值25時,直接丟棄多余部分,並記錄。

組成部分

muduo日志庫由前端和后端組成。

前端

其中前端主要包括:Logger, LogStream,FixedBuffer ,SourceFile。

類圖關系如下:

Logger類

Logger位於Logging.h/Logging.cc,主要為用戶(前端線程)提供使用日志庫的接口,是一個pointer to impl的實現(即GoF 橋接模式),詳細由內部類Impl實現。

Logger 可以根據用戶提供的__FILE__,__LINE__等宏構造對象,記錄日志的代碼自身信息(所在文件、行數);還提供構造不同等級的日志消息對象。每個Logger對象代表一個日志消息。

Logger 內部定義了日志等級(enum LogLevel),提供全局日志等級(g_logLevel)的獲取、設置接口;提供訪問內部LogStream對象的接口。

日志等級類型LogLevel

定義:

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

各日志等級通常含義:

  • TRACE
    指出比DEBUG粒度更細的一些信息事件(開發過程中使用)

  • DEBUG
    指出細粒度信息事件對調試應用程序是非常有幫助的(開發過程中使用)

  • INFO
    表明消息在粗粒度級別上突出強調應用程序的運行過程。

  • WARN
    系統能正常運行,但可能會出現潛在錯誤的情形。

  • ERROR
    指出雖然發生錯誤事件,但仍然不影響系統的繼續運行。

  • FATAL
    指出每個嚴重的錯誤事件將會導致應用程序的退出。

muduo默認級別為INFO,開發過程中可以選擇TRACE或DEBUG。低於指定級別日志不會被輸出。

用戶接口

Logging.h中,還定義了一系列LOG_開頭的宏,便於用戶以C++風格記錄日志:

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

例如,用戶可以用這樣的方式使用日志:

LOG_TRACE << "trace" << 1;

構造函數

不難發現,每個宏定義都構造了一個Logger臨時對象,然后通過stream(),來達到寫日志的功能。
選取參數最完整的Logger構造函數,構造Logger臨時對象:

muduo::Logger(__FILE__, __LINE__, muduo::Logger::TRACE, __func__)

__FILE__ 是一個宏, 表示當前代碼所在文件名(含路徑)
__LINE__ 是一個宏, 表示當前代碼所在文件的行數
muduo::Logger::TRACE 日志等級TRACE
__func__ 是一個宏, 表示當前代碼所在函數名

對應原型:

Logger(SourceFile file, int line, LogLevel level, const char* func);

這里SourceFile也是一個內部類,用來對構造Logger對象的代碼所在文件名進行了包裝,只記錄基本的文件名(不含路徑),以節省日志消息長度。

輸出位置,沖刷日志

一個應用程序,通常只有一個全局Logger。Logger類定義了2個函數指針,用於設置日志的輸出位置(g_output),沖刷日志(g_flush)。
類型:

typedef void (*OutputFunc)(const char* msg, int len);
typedef void (*FlushFunc)();

Logger默認向stdout輸出、沖刷:

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

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

Logger也提供2個static函數來設置g_output和g_flush。

static void setOutput(OutputFunc);
static void setFlush(FlushFunc);

用戶代碼可以這兩個函數修改Logger的輸出位置(需要同步修改)。一種典型的應用,就是將g_output重定位到后端AsyncLogging::append(),這樣后端線程就能在緩沖區滿或定時,從緩沖區取出數據並(與前端線程異步)寫到日志文件。

muduo::AsyncLogging* g_asyncLog = NULL;

void asyncOutput(const char* msg, int len)
{
  g_asyncLog->append(msg, len);
}

void func()
{
    muduo::Logger::setOutput(asyncOutput);
    LOG_INFO << "123456";
}

int main()
{
  char name[256] = { '\0' };
  strncpy(name, argv[0], sizeof name - 1);
  muduo::AsyncLogging log(::basename(name), kRollSize);
  log.start();
  g_asyncLog = &log;

  func();
}

日志等級,時區

還定義了2個全局變量,用於存儲日志等級(g_logLevel),時區(g_logTimeZone)。當前日志消息等級,如果低於g_logLevel,就不會進行任何操作,幾乎0開銷;只有不低於g_logLevel等級的日志消息,才能被記錄。這是通過LOG_xxx宏定義 的if語句實現的。

#define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
...

g_logTimeZone 會影響日志記錄的時間是用什么時區,默認UTC時間(GMT時區)。例如:

20220306 07:37:08.031441Z  3779 WARN  Hello - Logging_test.cpp:75

這里面的“20220306 07:37:08.031441Z”會受到日志時區的影響。

根據g_logTimeZone產生日志記錄的時間,位於formatTime()。由於不是核心功能,這里不詳述,后續有時間再研究,通常采用默認的GMT時區即可。

析構函數

前面說過,Logger是一個橋接模式,具體實現交給Impl。Logger析構代碼如下:

Logger::~Logger()
{
  impl_.finish();                     // 往Small Buffer添加后綴 文件名:行數
  const LogStream::Buffer& buf(stream().buffer());
  g_output(buf.data(), buf.length()); // 回調保存的g_output, 輸出Small Buffer到指定文件流
  if (impl_.level_ == FATAL)          // 發生致命錯誤, 輸出log並終止程序
  {
    g_flush();                        // 回調沖刷
    abort();
  }
}

析構函數中,Logger主要完成工作:為LogStream對象stream_中的log消息加上后綴(文件名:行號 ,LF指換行符'\n'),將stream_緩存的log消息通過g_output回調寫入指定文件流。另外,如果有致命錯誤(FATAL級別log),就終止程序。

緩沖Small Buffer 大小默認4KB,實際保存每條log消息,具體參見LogStream描述。

Impl類

Logger::Impl是Logger的內部類,負責Logger主要實現,提供組裝一條完整log消息的功能。

下面是3條完整log:

20220306 09:15:44.681220Z  4013 WARN  Hello - Logging_test.cpp:75
20220306 09:15:44.681289Z  4013 ERROR Error - Logging_test.cpp:76
20220306 09:15:44.681296Z  4013 INFO  4056 - Logging_test.cpp:77

格式:日期 + 時間 + 微秒 + 線程id + 級別 + 正文 + 原文件名 + 行號

日期      時間     微秒     線程  級別  正文     源文件名:       行號
20220306 09:15:44.681220Z  4013 WARN  Hello - Logging_test.cpp:75
...

Impl的數據結構

    class Impl
    {
    public:
        typedef Logger::LogLevel LogLevel;
        Impl(LogLevel level, int old_errno, const SourceFile& file, int line);
        void formatTime();  // 根據時區格式化當前時間字符串, 也是一條log消息的開頭
        void finish();      // 添加一條log消息的后綴

        Timestamp time_;    // 用於獲取當前時間
        LogStream stream_;  // 用於格式化用戶log數據, 提供operator<<接口, 保存log消息
        LogLevel level_;    // 日志等級
        int line_;          // 源代碼所在行
        SourceFile basename_; // 源代碼所在文件名(不含路徑)信息
    };

包含了需要組裝成一條完整log信息的所有組成部分。當然,正文部分是由用戶線程直接通過Logstream::operator<<,傳遞給stream_的。

Impl構造函數

除了對各成員進行初始構造,還生成線程tid、格式化時間字符串等,並通過stream_加入Samall Buffer。

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(), static_cast<unsigned int>(CurrentThread::tidStringLength()));
    stream_ << T(LogLevelName[level], kLogLevelNameLength); // 6
    if (savedErrno != 0) // 發生系統調用錯誤
    {
        stream_ << strerror_tl(savedErrno) << " (errno=" << savedErrno << ") "; // 自定義函數strerror_tl將錯誤號轉換為字符串, 相當於strerror_r(3)
    }
}

LogStream類實現

LogStream 主要提供operator<<操作,將用戶提供的整型數、浮點數、字符、字符串、字符數組、二進制內存、另一個Small Buffer,格式化為字符串,並加入當前類的Small Buffer。

Small Buffer存放log消息

Small Buffer,是模板類FixedBuffer<>的一個具現,i.e.FixedBuffer ,默認大小4KB,用於存放一條log消息。為前端類LogStream持有。
相對的,還有Large Buffer,也是FixedBuffer的一個具現,FixedBuffer ,默認大小4MB,用於存放多條log消息。為后端類AsyncLogging持有。

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

class LogStream : noncopyable
{
    ...
    typedef detail::FixedBuffer<detail::kSmallBuffer> Buffer; // Small Buffer Type
    ...
    Buffer buffer_;  // 用於存放log消息的Small Buffer
}

class AsyncLogging: noncopyable
{
    ...
    typedef muduo::detail::FixedBuffer<muduo::detail::kLargeBuffer> Buffer; // Large Buffer Type
    ...
}

模板類FixedBuffer ,內部是用數組char data_[SIZE]存儲,用指針char* cur_表示當前待寫數據的位置。對FixedBuffer<>的各種操作,實際上是對data_數組和cur_指針的操作。

template<int SIZE>
class FixedBuffer : noncopyable
{
public:
    ...
private:
    ...
    char data_[SIZE];
    char* cur_;
}

例如,往FixedBuffer加入數據FixedBuffer<>::append():

    void append(const char* buf, size_t len)
    {
        // FIXME: append partially
        if (implicit_cast<size_t>(avail()) > len) // implicit_cast隱式轉換int avail()為size_t
        {
            memcpy(cur_, buf, len);
            cur_ += len;
        }
    }
    int avail() const { return static_cast<int>(end() - cur_); } // 返回Buffer剩余可用空間大小
    const char* end() const { return data_ + sizeof(data_); }    // 返回Buffer內部數組末尾指針

operator<<格式化數據

針對不同類型數據,LogStream重載了一系列operator<<操作符,用於將數據格式化為字符串,並存入LogStream::buffer_。

{
    typedef LogStream self;
public:
        ...
    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<<(double);
    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);
    ...
}

1)對於字符串類型參數,operator<<本質上是調用buffer_對應的FixedBuffer<>::append(),將其存放當到Small Buffer中。

    self& operator<<(const char* str)
    {
        if (str)
        {
            buffer_.append(str, strlen(str));
        }
        else
        {
            buffer_.append("(null)", 6);
        }
        return *this;
    }

2)對於字符類型,跟參數是字符串類型區別是長度只有1,並且無需判斷指針是否為空。

    self& operator<<(char v)
    {
        buffer_.append(&v, 1);
        return *this;
    }

3)對於十進制整型,如int/long,則是通過模板函數formatInteger(),將轉換為字符串並直接填入Small Buffer尾部。

formatInteger() 並沒有用snprintf對整型數據進行格式轉換,而是用到了Matthew Wilson提出的高效的轉換方法convert()。基本思想是:從末尾開始,對待轉換的整型數,由十進制位逐位轉換為char類型,然后填入緩存,直到剩余待轉數值已為0。

注意:將int等整型轉換為string,muduo並沒有使用std::to_string,而是使用了效率更高的自定義函數formatInteger()。

template<typename T>
void LogStream::formatInteger(T v)
{
    if (buffer_.avail() >= kMaxNumericSize) // Small Buffer剩余空間夠用
    {
        size_t len = convert(buffer_.current(), v);
        buffer_.add(len);
    }
}

const char digits[] = "9876543210123456789";
const char* zero = digits + 9; // zero pointer to '0'
static_assert(sizeof(digits) == 20, "wrong number of digits");

/* Efficient Integer to String Conversions, by Matthew Wilson. */
template<typename T>
size_t convert(char buf[], T value)
{
    T i = value;
    char* p = buf;

    do {
        int lsd = static_cast<int>(i % 10);
        i /= 10;
        *p++ = zero[lsd];
    } while (i != 0);

    if (value < 0)
    {
        *p++ = '-';
    }
    *p = '\0';
    std::reverse(buf, p);

    return static_cast<size_t>(p - buf);
}

4)對於double類型,使用庫函數snprintf轉換為const char*,並直接填入Small Buffer尾部。

LogStream::self &LogStream::operator<<(double v)
{
    if (buffer_.avail() >= kMaxNumericSize)
    {
        int len = snprintf(buffer_.current(), kMaxNumericSize, "%.12g", v ); // 將v轉換為字符串, 並填入buffer_當前尾部. %g 自動選擇%f, %e格式, 並且不輸出無意義0. %.12g 最多保留12位小數
        buffer_.add(static_cast<size_t>(len));
    }
    return *this;
}

5)對於二進制數,原理同整型數,不過並不以10進制格式存放到Small Buffer,而是以16進制字符串(非NUL結尾)形式,在每個數會加上前綴"0x"。
將二進制內存轉換為16進制數的核心函數convertHex,使用了類似於convert的高效轉換算法。

LogStream::self &LogStream::operator<<(const void* p)
{
    uintptr_t v = reinterpret_cast<uintptr_t>(p); // uintptr_t 位數與地址位數相同, 便於跨平台使用
    if (buffer_.avail() >= kMaxNumericSize)       // Small Buffer剩余空間夠用
    {
        char* buf = buffer_.current();
        buf[0] = '0';
        buf[1] = 'x';
        size_t len = convertHex(&buf[2], v);
        buffer_.add(len + 2);
    }
    return *this;
}

const char digitsHex[] = "0123465789ABCDEF";
static_assert(sizeof(digitsHex) == 17, "wrong number of digitsHex");

size_t convertHex(char buf[], uintptr_t value)
{
    uintptr_t i = value;
    char* p = buf;

    do
    {
        int lsd = static_cast<int>(i % 16); // last digit for hex number
        i /= 16;
        *p++ = digitsHex[lsd];
    } while (i != 0);

    *p = '\0';
    std::reverse(buf, p);
    return static_cast<size_t>(p - buf);
}

注意:uintptr_t 位數跟平台地址位數相同,在64位系統中,占64位;在32位系統中,占32位。使用uintptr_t是為了提高可移植性。

6)對於其他類型,都是轉換為以上基本類型,然后再轉換為字符串,添加到Small Buffer末尾。

staticCheck()靜態檢查

在operator<<(char void*)和formatInteger(T v)中分別對二進制內存數據、整型數進行格式化時,都有一個判斷:Small Buffer剩余空間是否夠用,這里面有一個靜態常量kMaxNumericSize(默認48)。那么,如何對kMaxNumericSize進行取值呢?48是否合理,如何驗證?

這就可以用到staticCheck()進行驗證了。目的是為了確保kMaxNumericSize取值,能滿足Small Buffer剩余空間一定能存放下要格式化的數據。取數據位較長的double、long double、long、long long,進行static_assert斷言。

void LogStream::staticCheck()
{
    static_assert(kMaxNumericSize - 10 > std::numeric_limits<double>::digits10,
            "kMaxNumericSize is large enough");
    static_assert(kMaxNumericSize - 10 > std::numeric_limits<long double>::digits10,
            "kMaxNumericSize is large enough");
    static_assert(kMaxNumericSize - 10 > std::numeric_limits<long>::digits10,
            "kMaxNumericSize is large enough");
    static_assert(kMaxNumericSize - 10 > std::numeric_limits<long long>::digits10,
            "kMaxNumericSize is large enough");
}

std::numeric_limits ::digits10 返回T類型的十進制數的有效數字的位數,比如float有效位數6位,double是15位,int是9位。kMaxNumericSize-10 是為了確保kMaxNumericSize足夠大,能讓Small Buffer可用空間比要轉換的位數最長的類型還要多出10byte(1byte容納一個位數)。

小結

至此,muduo日志庫前端核心部分已講完。但默認只能將數據以非線程安全方式輸出到stdout,還不能實現異步記錄log消息。
關鍵點:
1)Logger 提供用戶接口,將實現細節隱藏到Impl,Logger定義一組宏定義LOG_XXX方便用戶在前端使用日志庫;
2)Impl實現除正文部分,一條完整log消息的組裝;
3)LogStream提供operator<< 格式化用戶正文內容,將其轉換為字符串,並添加到Small Buffer(4KB)末尾;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM