1. 程式人生 > 其它 >muduo筆記 日誌庫(二)

muduo筆記 日誌庫(二)

目錄

前半部分muduo筆記 日誌庫(一),中提到muduo非同步日誌庫分為兩部分:前端和後端。這部分描述後端。

後端

前端主要實現非同步日誌中的日誌功能,為使用者提供將日誌內容轉換為字串,封裝為一條完整的log訊息存放到RAM中;
而實現非同步,核心是通過專門的後端執行緒,與前端執行緒併發執行,將RAM中的大量日誌訊息寫到磁碟上。

後端主要包括:AsyncLogging, LogFile, AppendFile,MutexLock。

AsyncLogging 提供後端執行緒,定時將log緩衝寫到磁碟,維護緩衝及緩衝佇列。

LogFile 提供日誌檔案滾動功能,寫檔案功能。

AppendFile 封裝了OS提供的基礎的寫檔案功能。

類圖關係如下:

AsyncLogging類

AsyncLogging 主要職責:提供大緩衝Large Buffer(預設4MB)存放多條日誌訊息,緩衝佇列BufferVector用於存放多個Large Buffer,為前端執行緒提供執行緒安全的寫Large Buffer操作;提供專門的後端執行緒,用於定時或緩衝佇列非空時,將緩衝佇列中的Large Buffer通過LogFile提供的日誌檔案操作介面,逐個寫到磁碟上。

資料成員

/**
* Provide async logging function. backend.
* Background thread (just only one) call this module to write log to file.
*/
class AsyncLogging : noncopyable
{
        ...
private:

    typedef muduo::detail::FixedBuffer<muduo::detail::kLargeBuffer> Buffer; // Large Buffer Type
    typedef std::vector<std::unique_ptr<Buffer>> BufferVector;              // 已滿緩衝佇列型別
    typedef BufferVector::value_type BufferPtr;                             

    const int flushInterval_;                    // 沖刷緩衝資料到檔案的超時時間, 預設3秒
    std::atomic<bool> running_;                  // 後端執行緒loop是否執行標誌
    const string basename_;                      // 日誌檔案基本名稱
    const off_t rollSize_;                       // 日誌檔案滾動大小
    muduo::Thread thread_;                       // 後端執行緒
    muduo::CountDownLatch latch_;                // 門閥, 同步呼叫執行緒與新建的後端執行緒
    muduo::MutexLock mutex_;                     // 互斥鎖, 功能相當於std::mutex
    muduo::Condition cond_ GUARDED_BY(mutex_);   // 條件變數, 與mutex_配合使用, 等待特定條件滿足
    BufferPtr currentBuffer_ GUARDED_BY(mutex_); // 當前緩衝
    BufferPtr nextBuffer_ GUARDED_BY(mutex_);    // 空閒緩衝
    BufferVector buffers_ GUARDED_BY(mutex_);    // 已滿緩衝佇列
}

AsyncLogging資料按功能主要分為3部分:1)維護存放log訊息的大緩衝Large Buffer;2)後端執行緒;3)傳遞給其他類物件的引數,如basename_,rollSize_;

LargeBuffer 存放大量log訊息

Large Buffer(FixedBuffermuduo::detail::kLargeBuffer)預設大小4MB,用於儲存多條log訊息;相對的,還有Small Buffer(FixedBuffermuduo::detail::kSmallBuffer)預設大小4KB,用於儲存一條log訊息。

當前端執行緒通過呼叫LOG_XXX << "..."時,如何將log訊息傳遞給後端呢?
可以通過呼叫AsyncLogging::append()

void AsyncLogging::append(const char *logline, int len)
{
    muduo::MutexLockGuard lock(mutex_);
    if (currentBuffer_->avail() > len)
    { // current buffer's free space is enough to fill C string logline[0..len-1]
        currentBuffer_->append(logline, static_cast<size_t>(len));
    }
    else
    { // current buffer's free space is not enough
        buffers_.push_back(std::move(currentBuffer_));

        if (nextBuffer_)
        {
            currentBuffer_ = std::move(nextBuffer_);
        }
        else
        {
            currentBuffer_.reset(new Buffer); // rarely happens
        }

        currentBuffer_->append(logline, static_cast<size_t>(len));
        cond_.notify();
    }
}

append()可能會被多個前端執行緒呼叫,因此必須考慮執行緒安全,可以用mutex_加鎖。
append()基本思路:當前緩衝(currentBuffer_)剩餘空間(avail())足夠存放新log訊息大小(len)時,就直接存放到當前緩衝;當前緩衝剩餘空間不夠時,說明當前緩衝已滿(或者接近已滿),就將當前緩衝move到已滿緩衝佇列(buffers_),將空閒緩衝move到當前緩衝,再把新log訊息存放到當前緩衝中(此時當前緩衝為空,剩餘空間肯定夠用),最後喚醒等待中的後端執行緒。

注意:Large Buffer是通過std::unique_ptr指向的,move操作後,原來的 std::unique_ptr就會值為空。

問題:
1)為什麼最後要通過cond_喚醒後端執行緒?
因為沒有log訊息要記錄時,後端執行緒很可能阻塞等待log訊息,當有緩衝滿時,及時喚醒後端將已滿緩衝資料寫到磁碟上,能有效改善新能;否則,短時間內產生大量log訊息,可能造成資料堆積,甚至丟失,而後端執行緒一直休眠(直到3秒超時喚醒)。

2)為什麼呼叫notify()而不是notifyAll(),只喚醒一個執行緒,而不是喚醒所有執行緒?
因為一個應用程式通常只有一個日誌庫後端,而一個後端通常只有一個後端執行緒,也只會有一個後端執行緒在該條件變數上等待,因此喚醒一個執行緒足以。

後端執行緒 非同步寫資料到log檔案

後端執行緒的建立就是啟動,是在start()中,通過呼叫Thread::start()完成。門閥latch_目的在於讓呼叫start()執行緒等待執行緒函式啟動完成,而執行緒函式中呼叫latch_.countDown()表示啟動完成,當然,前提是latch_計數器初值為1。

    // Control background thread

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

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

而AsyncLogging::stop()用於關閉後端執行緒,通常是在解構函式中,呼叫AsyncLogging::stop() 停止後端執行緒。

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

後端執行緒函式threadFunc,會構建1個LogFile物件,用於控制log檔案建立、寫日誌資料,建立2個空閒緩衝區buffer1、buffer2,和一個待寫緩衝佇列buffersToWrite,分別用於替換當前緩衝currentBuffer_、空閒緩衝nextBuffer_、已滿緩衝佇列buffers_,避免在寫檔案過程中,鎖住緩衝和佇列,導致前端無法寫資料到後端緩衝。

threadFunc中,提供了一個loop,基本流程是這樣的:
1)每次當已滿緩衝佇列中有資料時,或者即使沒有資料但3秒超時,就將當前緩衝加入到已滿緩衝佇列(即使當前緩衝沒滿),將buffer1移動給當前緩衝,buffer2移動給空閒緩衝(如果空閒緩衝已移動的話)。
2)然後,再交換已滿緩衝佇列和待寫緩衝佇列,這樣已滿緩衝佇列就為空,待寫緩衝佇列就有資料了。
3)接著,將待寫緩衝佇列的所有緩衝通過LogFile物件,寫入log檔案。
4)此時,待寫緩衝佇列中的緩衝,已經全部寫到LogFile指定的檔案中(也可能在核心緩衝中),擦除多餘緩衝,只用保留兩個,歸還給buffer1和buffer2。
5)此時,待寫緩衝佇列中的緩衝沒有任何用處,直接clear即可。
6)將核心快取記憶體中的資料flush到磁碟,防止意外情況造成資料丟失。

後端執行緒函式threadFunc,會構建1個LogFile物件,用於控制log檔案建立、寫日誌資料,建立2個空閒緩衝區buffer1、buffer2,和一個待寫緩衝佇列buffersToWrite,分別用於替換當前緩衝currentBuffer_、空閒緩衝nextBuffer_、已滿緩衝佇列buffers_,避免在寫檔案過程中,鎖住緩衝和佇列,導致前端無法寫資料到後端緩衝。

threadFunc中,提供了一個loop,基本流程是這樣的:
1)每次當已滿緩衝佇列中有資料時,或者即使沒有資料但3秒超時,就將當前緩衝加入到已滿緩衝佇列(即使當前緩衝沒滿),將buffer1移動給當前緩衝,buffer2移動給空閒緩衝(如果空閒緩衝已移動的話)。
2)然後,再交換已滿緩衝佇列和待寫緩衝佇列,這樣已滿緩衝佇列就為空,待寫緩衝佇列就有資料了。
3)接著,將待寫緩衝佇列的所有緩衝通過LogFile物件,寫入log檔案。
4)此時,待寫緩衝佇列中的緩衝,已經全部寫到LogFile指定的檔案中(也可能在核心緩衝中),擦除多餘緩衝,只用保留兩個,歸還給buffer1和buffer2。
5)此時,待寫緩衝佇列中的緩衝沒有任何用處,直接clear即可。
6)將核心快取記憶體中的資料flush到磁碟,防止意外情況造成資料丟失。

void AsyncLogging::threadFunc()
{
    assert(running_ == true);
    latch_.countDown();
    LogFile output(basename_, rollSize_, false); // only called by this thread, so no need to use thread safe
    BufferPtr newBuffer1(new Buffer);
    BufferPtr newBuffer2(new Buffer);
    newBuffer1->bzero();
    newBuffer2->bzero();
    BufferVector buffersToWrite;
    static const int kBuffersToWriteMaxSize = 25;

    buffersToWrite.reserve(16); // FIXME: why 16?
    while (running_)
    {
        // ensure empty buffer
        assert(newBuffer1 && newBuffer1->length() == 0);
        assert(newBuffer2 && newBuffer2->length() == 0);
        // ensure buffersToWrite is empty
        assert(buffersToWrite.empty());

        { // push buffer to vector buffersToWrite
            muduo::MutexLockGuard lock(mutex_);
            if (buffers_.empty())
            { // unusual usage!
                cond_.waitForSeconds(flushInterval_); // wait condition or timeout
            }
            // not empty or timeout

            buffers_.push_back(std::move(currentBuffer_));
            currentBuffer_ = std::move(newBuffer1);
            buffersToWrite.swap(buffers_);
            if (!nextBuffer_)
            {
                nextBuffer_ = std::move(newBuffer2);
            }
        }

        // ensure buffersToWrite is not empty
        assert(!buffersToWrite.empty());

        if (buffersToWrite.size() > kBuffersToWriteMaxSize) // FIXME: why 25? 25x4MB = 100MB, 也就是說, 從上次loop到本次loop已經堆積超過100MB, 就丟棄多餘緩衝
        {
            char buf[256];
            snprintf(buf, sizeof(buf), "Dropped log message 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()); // keep 2 buffer
        }

        // append buffer content to logfile
        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);
        }

        // move vector buffersToWrite's last buffer to newBuffer1
        if (!newBuffer1)
        {
            assert(!buffersToWrite.empty());
            newBuffer1 = std::move(buffersToWrite.back());
            buffersToWrite.pop_back();
            newBuffer1->reset(); // reset buffer
        }

        // move vector buffersToWrite's last buffer to newBuffer2
        if (!newBuffer2)
        {
            assert(!buffersToWrite.empty());
            newBuffer2 = std::move(buffersToWrite.back());
            buffersToWrite.pop_back();
            newBuffer2->reset(); // reset buffer
        }

        buffersToWrite.clear();
        output.flush();
    }
    output.flush();
}

異常處理:
當已滿緩衝佇列中的資料堆積(預設緩衝數超過25),就會丟棄多餘緩衝,只保留最開始2個。
為什麼保留2個?個人覺得2個~16個都是可以的,不過,為了有效減輕log導致的負擔,丟棄多餘的也未嘗不可。

25的含義:
25個緩衝,每個4MB,共100MB。也就是說,上次處理週期到本次,已經堆積了超過100MB資料待處理。
假設磁碟的寫速度100MB/S,要堆積100MB有2種極端情況:
1)1S內產生200MB資料;
2)25秒內,平均每秒產生104MB資料;

不論哪種情況,都是要超過磁碟的處理速度。而實際應用中,只有產生資料速度不到磁碟寫速度的1/10,應用程式效能才不會受到明顯影響。

[======]

LogFile類

LogFile 主要職責:提供對日誌檔案的操作,包括滾動日誌檔案、將log資料寫到當前log檔案、flush log資料到當前log檔案。

建構函式

LogFile::LogFile(const std::string &basename,
                 off_t rollSize,
                 bool threadSafe,    // 執行緒安全控制項, 預設為true. 當只有一個後端AsnycLogging和後端執行緒時, 該項可置為false
                 int flushInterval,
                 int checkEveryN)
        : basename_(basename),  // 基礎檔名, 用於新log檔案命名
        rollSize_(rollSize),    // 滾動檔案大小
        flushInterval_(flushInterval), // 沖刷時間限值, 預設3 (秒)
        checkEveryN_(checkEveryN),     // 寫資料次數限值, 預設1024
        count_(0),                     // 寫資料次數計數, 超過限值checkEveryN_時清除, 然後重新計數
        mutex_(threadSafe ? new MutexLock : NULL), // 互斥鎖指標, 根據是否需要執行緒安全來初始化
        startOfPeriod_(0),             // 本次寫log週期的起始時間(秒)
        lastRoll_(0),                  // 上次roll日誌檔案時間(秒)
        lastFlush_(0)                  // 上次flush日誌檔案時間(秒)
{
    assert(basename.find('/') == string::npos); // basename不應該包含'/', 這是路徑分隔符
    rollFile();
}

重新啟動時,可能並沒有log檔案,因此在構建LogFile物件時,直接呼叫rollFile()以建立一個全新的日誌檔案。

滾動日誌檔案

當日志文件接近指定的滾動限值(rollSize)時,需要換一個新檔案寫資料,便於後續歸檔、檢視。呼叫LogFile::rollFile()可以實現檔案滾動。

bool LogFile::rollFile()
{
    time_t now = 0;
    string filename = getLogFileName(basename_, &now);
    time_t start = now / kRollPerSeconds_ * kRollPerSeconds_;

    if (now > lastRoll_)
    { // to avoid identical roll by roll time
        lastRoll_ = now;
        lastFlush_ = now;
        startOfPeriod_ = start;
        // create new log file with new filename
        file_.reset(new FileUtil::AppendFile(filename));
        return true;
    }
    return false;
}

滾動日誌檔案操作的關鍵是:1)取得新log檔名,檔名全域性唯一;2)建立並開啟一個新log檔案,用指向LogFile物件的unique_ptr指標file_表示。

異常處理:
滾動操作會新建一個檔案,而為避免頻繁建立新檔案,rollFile會確保上次滾動時間到現在如果不到1秒,就不會滾動。

注意:是否滾動日誌檔案的條件判斷,並不在rollFile,而是在寫資料到log檔案的LogFile::append_unlocked()中,因為寫新資料的時候,是判斷當前log檔案大小是否足夠大的最合適時機。而rollFile只用專門負責如何滾動log檔案即可。

日誌檔名

getLogFileName根據呼叫者提供的基礎名,以及當前時間,得到一個全新的、唯一的log檔名。或許叫nextLogFileName更合適。

string LogFile::getLogFileName(const string &basename, time_t *now) // static
{
    string filename;
    filename.reserve(basename.size() + 64); // extra 64 bytes for timestamp etc.
    filename = basename;

    char timebuf[32];
    struct tm tmbuf;
    *now = time(NULL);
    gmtime_r(now, &tmbuf); // FIXME: localtime_r ?
    strftime(timebuf, sizeof(timebuf), ".%Y%m%d-%H%M%S.", &tmbuf);
    filename += timebuf;

    filename += ProcessInfo::hostname();

    char pidbuf[32];
    snprintf(pidbuf, sizeof(pidbuf), ".%d", ProcessInfo::pid());
    filename += pidbuf;
    filename += ".log";

    return filename;
}

gmtime_r獲取的是gmt時區時間,localtime_r獲取的是本地時間。

新log檔名格式:

basename + now + hostname + pid + ".log"

basename 基礎名, 由使用者指定, 通常可設為應用程式名
now 當前時間, 格式: "%Y%m%d-%H%M%S"
hostname 主機名
pid 程序號, 通常由OS提供, 通過getpid獲取
".log" 固定字尾名, 表明這是一個log檔案
各部分之間, 用"."連線

如下面是一個根據basename為"test_log_mt"生成的log檔名:

test_log_mt.20220218-134000.ubuntu.12426.log

寫日誌檔案操作

LogFile提供了2個介面,用於向當前日誌檔案file_寫入資料。append本質上是通過append_unlocked完成對日誌檔案寫操作,但多了執行緒安全。使用者只需呼叫第一個介面即可,append會根據執行緒安全需求,自行判斷是否需要加上;第二個是private介面。

void append(const char *logline, int len);
void append_unlocked(const char *logline, int len);

append_unlocked 會先將log訊息寫入file_檔案,之後再判斷是否需要滾動日誌檔案;如果不滾動,就根據append_unlocked的呼叫次數和時間,確保1)一個log檔案超時(預設1天),就建立一個新的;2)flush檔案操作,不會頻繁執行(預設間隔3秒)。

void LogFile::append_unlocked(const char *logline, int len)
{
    file_->append(logline, len);

    if (file_->writtenBytes() > rollSize_)
    { // written bytes to file_ > roll threshold (rollSize_)
        rollFile();
    }
    else
    {
        ++count_;
        if (count_ >= checkEveryN_)
        {
            count_ = 0;
            time_t now = ::time(NULL);
            time_t thisPeriod_ = now / kRollPerSeconds_ * kRollPerSeconds_;

            if (thisPeriod_ != startOfPeriod_)
            { // new period, roll file for log
                rollFile();
            }
            else if (now - lastFlush_ > flushInterval_)
            { // timeout ( flushInterval_ = 3 seconds)
                lastFlush_ = now;
                file_->flush();
            }
        }
    }
}

append如何根據需要選擇是否執行緒安全地呼叫append_unlocked?
可以根據mutex_是否為空。因為構造時,根據使用者傳入的threadSafe實參,決定了mutex_是否為空。

void LogFile::append(const char *logline, int len)
{
    if (mutex_)
    {
        MutexLockGuard lock(*mutex_);
        append_unlocked(logline, len);
    }
    else
    {
        append_unlocked(logline, len);
    }
}

flush日誌檔案

flush操作往往與write檔案操作配套。LogFile::flush實際上是通過AppendFile::flush(),完成對日誌檔案的沖刷。與LogFile::append()類似,flush也能通過mutex_指標是否為空,自動選擇執行緒安全版本,還是非執行緒安全版本。

void LogFile::flush()
{
    if (mutex_)
    {
        MutexLockGuard lock(*mutex_);
        file_->flush();
    }
    else
    {
        file_->flush();
    }
}

[======]

AppendFile類

AppendFile位於FileUtil.h/.cc,封裝了OS提供的,底層的建立/開啟檔案、寫檔案、關閉檔案等操作介面,並沒有專門考慮執行緒安全問題。執行緒安全由上一層級呼叫者,如LogFile來保證。

資料結構

AppendFile的資料結構較簡單,

// not thread safe
class AppendFile : public noncopyable
{
public:
    explicit AppendFile(StringArg filename);
    ~AppendFile();
    void append(const char* logline, size_t len);  // 新增log訊息到檔案末尾
    void flush();                                  // 沖刷檔案到磁碟
    off_t writtenBytes() const { return writtenBytes_; } // 返回已寫位元組數
private:
    size_t write(const char* logline, size_t len);       // 寫資料到檔案

    FILE* fp_;                                // 檔案指標
    char buffer_[ReadSmallFile::kBufferSize]; // 檔案操作的緩衝區
    off_t writtenBytes_;                      // 已寫位元組數
};

RAII方式開啟、關閉檔案

AppendFile採用RAII方式管理檔案資源,構建物件即開啟檔案,銷燬物件即關閉檔案。

AppendFile::AppendFile(StringArg filename)
: fp_(::fopen(filename.c_str(), "ae")), // 'e' for O_CLOEXEC
writtenBytes_(0)
{
    assert(fp_);
    ::setbuffer(fp_, buffer_, sizeof(buffer_)); // change stream fp_'s buffer to buffer_
#if 0
    // optimization for predeclaring an access pattern for file data
    struct stat statbuf;
    fstat(fd_, &statbuf);
    ::posix_fadvise(fp_, 0, statbuf.st_size, POSIX_FADV_DONTNEED);
#endif
}

AppendFile::~AppendFile()
{
    ::fclose(fp_);
}

為posix_fadvise(2)指定POSIX_FADV_DONTNEED選項,告訴核心在近期不會訪問檔案的指定資料,以便核心對其進行優化。

寫資料到檔案

AppendFile有個兩個介面:append和write。其中,append()是供使用者呼叫的public介面,確保將指定資料附加到檔案末尾,實際的寫檔案操作是通過write()來完成的;write通過非執行緒安全的glibc庫函式fwrite_unlocked()來完成寫檔案操作,而沒有選擇執行緒安全的fwrite(),主要是出於效能考慮。

一個後端通常只有一個後端執行緒,一個LogFile物件,一個AppendFile物件,這樣,也就只會有一個執行緒寫同一個log檔案。

void AppendFile::append(const char *logline, size_t len)
size_t AppendFile::write(const char *logline, size_t len);

append和write實現:

void AppendFile::append(const char *logline, size_t len)
{
    size_t written = 0;

    /* write len byte to fp_ unless complete writing or error occurs */
    while (written != len)
    {
        size_t remain = len - written;
        size_t n = write(logline + written, remain);
        if (n != remain)
        {
            int err = ferror(fp_);
            if (err)
            {
                fprintf(stderr, "AppendFile::append() failed %s\n", strerror_tl(err));
                clearerr(fp_); // clear error indicators for fp_
                break;
            }
        }
        written += n;
    }
    writtenBytes_ += written;
}

size_t AppendFile::write(const char *logline, size_t len)
{
    // not thread-safe
    return ::fwrite_unlocked(logline, 1, len, fp_);
}

可以看出,append是通過一個迴圈來確保所有資料都寫到磁碟檔案上,除非發生錯誤。

[======]

使用非同步日誌

自此,一個完整的非同步日誌前端、後端都已完成。但問題在於,應用程式如何使用?
為此,寫一個測試程式,對比為前端Logger設定輸出回撥函式前後不同。

#include "muduo/base/AsyncLogging.h"
#include "muduo/base/Logging.h"
#include "muduo/base/Timestamp.h"

#include <stdio.h>
#include <unistd.h>

using namespace muduo;

static const off_t kRollSize = 1*1024*1024;
AsyncLogging* g_asyncLog = NULL;

inline AsyncLogging* getAsyncLog()
{
    return g_asyncLog;
}

void test_Logging()
{
    LOG_TRACE << "trace";
    LOG_DEBUG << "debug";
    LOG_INFO << "info";
    LOG_WARN << "warn";
    LOG_ERROR << "error";
    LOG_SYSERR << "sys error";
    // 注意不能輕易使用 LOG_FATAL, LOG_SYSFATAL, 會導致程式abort

    const int n = 10;
    for (int i = 0; i < n; ++i) {
        LOG_INFO << "Hello, " << i << " abc...xyz";
    }
}

void test_AsyncLogging()
{
    const int n = 3*1024*1024;
    for (int i = 0; i < n; ++i) {
        LOG_INFO << "Hello, " << i << " abc...xyz";
    }
}

void asyncLog(const char* msg, int len)
{
    AsyncLogging* logging = getAsyncLog();
    if (logging)
    {
        logging->append(msg, len);
    }
}

int main(int argc, char* argv[])
{
    printf("pid = %d\n", getpid());

    AsyncLogging log(::basename(argv[0]), kRollSize);
    test_Logging();

    sleep(1);

    g_asyncLog = &log;
    Logger::setOutput(asyncLog); // 為Logger設定輸出回撥, 重新配接輸出位置
    log.start();

    test_Logging();
    test_AsyncLogging();

    sleep(1);
    log.stop();
    return 0;
}

可以發現,在呼叫Logger::setOutput設定輸出回撥前,預設輸出位置是stdout(見defaultOutput),而設定了輸出位置為自定義asyncLog後,每當Logger要輸出log訊息時,就會通過asyncLog呼叫預先設定好的g_asyncLog的append()函式,將log訊息輸出到AsycnLogging的Large Buffer中。

從這裡也可以發現,muduo日誌庫AsycnLogging類有個bug:AsycnLogging並沒有同步設定一個flush函式,這樣Logger::flush呼叫的其實還是預設的flush到stdout,並不能跟AsycnLogging::append()同步。當然,這不是什麼難事,直接為其新增一個自定義flush()即可。

[======]

小結

1)AsyncLogging 提供多個Large Buffer快取多條log訊息,前端需要在重新配接輸出位置後,將每條log訊息輸出到Large Buffer中。後端執行緒也是由AsyncLogging 負責維護。

2)LogFile 提供日誌檔案操作,包括滾動日誌檔案、寫日誌檔案。

3)AppendFile 封裝了最底層的的寫檔案操作,供LogFile使用。

[======]

參考

https://blog.csdn.net/luotuo44/article/details/19252535
https://docs.microsoft.com/en-us/previous-versions/af6x78h6(v=vs.140)?redirectedfrom=MSDN