后半部分見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
類圖關系如下:

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