libvirt log系統分析

1.編譯和安裝

配置參數需要加上–enable-debug=yes,相關定義在src/util/virlog.h文件中定義
在這裏插入圖片描述

圖1-1 ENABLE_DEBUG宏

如果沒有加這個編譯參數,調用VIR_DEBUG_INT宏的函數或者其他宏,就沒有任何效果,這是一切的一切的基礎。其他編譯安裝省略,可見其他相關文檔



2.查看libvirtd配置文件

#從libvirtd入手,打開libvirtd的配置文件

[root@localhost ~]# vi /etc/libvirt/libvirtd.conf
#################################################################
#
# Logging controls
#
#log_level = 3
#log_filters="3:remote 4:event"
#log_outputs="3:syslog:libvirtd"
#    1: DEBUG
#    2: INFO
#    3: WARNING
#    4: ERROR

        省略了一些介紹性的文字,只列出# Logging controls部分最重要的三個配置選項,以及幾個日誌等級,雖然每個配置都有自己的等級的介紹,但是這幾個其實是一樣的,在源碼中只有一份定義,如圖所示
在這裏插入圖片描述

圖2-1 日誌等級定義
  後面還有一個#log_buffer_size = 6,根據註釋說明,該配置項已經不再使用,應該是已經過時的項,後面就是# Auditing部分,雖然從技術上講和log子系統沒什麼區別,但是側重點不同,是屬於安全審計一類的信息記錄,和本文檔討論的內容無關,本文不會進行分析。



3.libvirtd日誌輸出方式log_outputs

先看log_outpus, 以下是配置文件中完整的註釋信息

# Logging outputs:
# An output is one of the places to save logging information
# The format for an output can be:
#    x:stderr
#      output goes to stderr
#    x:syslog:name
#      use syslog for the output and use the given name as the ident
#    x:file:file_path
#      output to a file, with the given filepath
#    x:journald
#      output to journald logging system
# In all case the x prefix is the minimal level, acting as a filter
#    1: DEBUG
#    2: INFO
#    3: WARNING
#    4: ERROR
#
# Multiple outputs can be defined, they just need to be separated by spaces.
# e.g. to log all warnings and errors to syslog under the libvirtd ident:
#log_outputs="3:syslog:libvirtd"

可以看到,前面的數字表示了打印的等級,第二位和第三位是根據輸出類型不同,意義也不同,例如默認的 log_outputs=“3:syslog:libvirtd” 中,代表輸出的內容的等級爲3: WARNING,並且使用linux系統自身的syslog來進行記錄日誌,日誌條目的名字爲libvirtd。目前libvirtd默認用的什麼配置沒有研究過,並且我們不想借用系統自帶的syslog系統,因爲那樣可能會和系統其他程序或者系統自身的log信息混在一起,不方便調試,因此選擇打印到獨立的文件中
log_level = 4//決定了待打印的log信息的等級
log_outputs=“1:file:/var/log/libvirt/libvirtd.log”//這兩個配置表示日誌信息會被當作ERROR的級別,並且在輸出的控制是DEBUG,即只要log級別大於等於DEBUG都可以被輸出到/var/log/libvirt/libvirtd.log文件。
查看libvirtd.c的main函數1353行:

VIR_DEBUG("Decided on pid file path '%s'", NULLSTR(pid_file));

#重新啓動libvirtd進程

[root@localhost ~]# systemctl restart libvirtd.service

#查看日誌文件

[root@localhost ~]# cat /var/log/libvirt/libvirtd.log|grep "Decided on pid file path"

並沒有看到相關日誌信息,這是什麼原因呢?下一節再分析,此處先做一個workaround,不深究原因,更改log_level的數值

log_level = 1

之後再重啓libvirtd服務,可以看到,log信息確實寫入了log_outputs指定的文件裏。

[root@localhost ~]# cat /var/log/libvirt/libvirtd.log|grep "Decided on pid file path"
2018-08-17 02:12:40.573+0000: 21095: debug : main:1353 : Decided on pid file path '/var/run/libvirtd.pid'



4.libvirtd日誌輸入級別log_level

從上一節可以看到,log_level確實可以影響日誌是否能正常被打印到log_outputs所指定的文件中。具體如何影響的,分析代碼。先將log_level改成4,分析爲什麼無法打印。
#關閉後臺進程,直接用gdb跟蹤

[root@localhost ~]# systemctl stop libvirtd.service

(gdb) b 1353
Breakpoint 1 at 0x17888: file libvirtd.c, line 1353.
(gdb) r
Starting program: /usr/sbin/libvirtd 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
2018-08-17 02:25:46.299+0000: 21314: info : libvirt version: 2.0.0
2018-08-17 02:25:46.299+0000: 21314: info : hostname: localhost.localdomain
2018-08-17 02:25:46.299+0000: 21314: debug : virLogParseFilters:1289 : filters=1:remote 4:event
2018-08-17 02:25:46.299+0000: 21314: debug : virLogParseFilter:1227 : filter=1:remote
2018-08-17 02:25:46.299+0000: 21314: debug : virLogParseFilter:1227 : filter=4:event
2018-08-17 02:25:46.299+0000: 21314: debug : virLogParseOutputs:1191 : outputs=1:file:/var/log/libvirt/libvirtd.log
2018-08-17 02:25:46.299+0000: 21314: debug : virLogParseOutput:1097 : output=1:file:/var/log/libvirt/libvirtd.log

Breakpoint 1, main (argc=<optimized out>, argv=0x7fffffffe638) at libvirtd.c:1353
1353	    VIR_DEBUG("Decided on pid file path '%s'", NULLSTR(pid_file));
(gdb) b virLogVMessage
Breakpoint 2 at 0x7ffff738d470: file util/virlog.c, line 549.

(gdb) c
Continuing.

進入virLogVMessage函數後,列出各種變量信息
在這裏插入圖片描述

圖4-1列出變量信息
可以看到source的priority爲3,在log_level等三個配置中沒有任何地方配置的是3,這裏會誤導人,因爲後面還會有變化,從代碼中可以看到

在這裏插入圖片描述

圖4-2

在這裏插入圖片描述

圖4-3

source->priority的值是會在virLogSourceUpdate函數中刷新的,只要virLogSourceUpdate被調用,繼續跟蹤斷點可以看到,猜測是正確的,如下圖所示:
在這裏插入圖片描述

圖4-4 virLogSourceUpdate內部

可以看到,virLogSourceUpdate函數給source->priority進行了重新賦值,使之成爲了配置文件中log_level設定的數值4,至於virLogSourceUpdate具體做了什麼,這要結合最後一個filter參數來參數,比較複雜,不影響此處的理解,之後再來分析,這裏可以不用深究。接下來:
在這裏插入圖片描述

圖4-5 判斷是否打印
第575行可以看到這裏的prioriy爲VIR_LOG_DEBUG=1,而source->priority=4,會執行goto clean語句,因此無法打印。而此處的priority來源於調用此函數的宏本身的設置

在這裏插入圖片描述

圖4-6-1

在這裏插入圖片描述

圖4-6-2
所以,這種設計思路就是允許通過log_level來設定log內容的級別,如果log_level小於log_outputs設定的輸出的級別,就可以打印到log文件中,但是一定要使用級別正確(夠資格)的宏,這裏用VIR_ERROR,就可以了,經過驗證,分析無誤。 爲什麼要log_level不能大於log_outputs設定的輸出的級別,按理說大優先級的內容應該可以更加優先的輸出,這種理解在信息安全領域是不正確的,根據分級安全的理論,讀操作的時候,低級別的reader無法讀取高級別的內容,但是可以做write操作,直觀的看就是低安全級別的人員可以把自身的信息暴露給上級,而不用擔心泄密;反過來,高級別的writer就不能像低級別的地方做write操作,這樣就會泄露信息,這個log的模型也是一個write操作,因此也是用的這種思路,但是不涉及read操作。
[root@localhost ~]# cat /var/log/libvirt/libvirtd.log|grep "Decided on pid file path"
2018-08-17 08:07:37.838+0000: 27663: error : main:1354 : Decided on pid file path '/var/run/libvirtd.pid'

可以看到,VIR_ERROR宏具有最高權限,隨後是VIR_WARN,VIR_INFO,VIR_DEBUG,高級別的宏可以打印低級別的log_level的log日誌,但是反過來不行。注意,這個地方是控制打印的宏是否有資格做出打印這個動作,但是無法決定是否有權力寫到某個文件中,如果打印的動作都無法做出,就會跳轉到clean之後直接返回了,不會執行以下代碼。log信息決定是否有權力寫到某個文件中是由log_outputs參數決定的,如下圖所示,這樣兩種行爲的區別就分清楚了。
在這裏插入圖片描述

圖4-7判斷是否允許打印到輸出對象(文件或者標準輸出等)
# util/virlog.c:619 virLogOutputs[i].f(source, priority,
                               filename, linenr, funcname,
                               timestamp, metadata, filterflags,
                               str, msg, virLogOutputs[i].data);=>virLogOutputToFd

通過循環語句,把所有log_outputs所描述的輸出途徑都遍歷一遍並且進行輸出,真正寫入是調用這個函數。整個日誌大致框架就是這樣。


5.libvirtd過濾器log_filter

5.1.virLogSourceUpdate函數分析

上一章圖4-2和圖4-3可以看到,當滿足source->serial < virLogFiltersSerial的時候,就會調用virLogSourceUpdate(source),在這個函數內部,會對source的各種變量進行重新設置,依據就是前一章提到的log_level,以及本章要提到的filter。首先分析這個函數:
在這裏插入圖片描述

圖5-1 virLogSourceUpdate
474行有一個判斷條件if (source->serial < virLogFiltersSerial) ,這個地方先不管,這是另外一個機制,比較難以理解,不影響filter本身的功能。 475行的unsigned int priority = virLogDefaultPriority;//用一個全局變量給當前的priority賦值,這個全局變量來源於libvirtd.conf配置文件中的log_level。還有一個全局變量virLogNbFilters,代表filter的數量,默認是0,而virLogFilters數組就代表了filter的內容,如果沒有設置filter,此處也爲空。該函數就會執行到486行,相當於filter沒有生效,但是這不是我們要看到的效果,因此在配置文件中配置一下filter log_filters="3:daemon4:event" 調試進入1virLogSourceUpdate函數,打印信息如下:
(gdb) p virLogFiltersSerial
$2 = 4
(gdb) p virLogNbFilters
$3 = 2
(gdb) p virLogFilters
$4 = (virLogFilterPtr) 0x5555557e11b0
(gdb) p virLogFilters[0]
$5 = {match = 0x5555557ec040 "daemon", priority = VIR_LOG_WARN, flags = 0}
(gdb) p source[0]
$6 = {name = 0x5555555ad318 "daemon.libvirtd", priority = 3, serial = 1, flags = 0}
(gdb) p virLogFilters[1]
$7 = {match = 0x5555557ebfa0 "event", priority = VIR_LOG_ERROR, flags = 0}

可以看到virLogNbFilters就是對應的兩個filter的數量,virLogFilters就是兩個filter的內容,"daemon"的優先級爲 VIR_LOG_WARN,"event"的優先級爲VIR_LOG_ERROR,也就是log_filters中設置的3和4。
479行的循環會遍歷virLogFilters數組,用if (strstr(source->name, virLogFilters[i].match)) 來判斷,filter數組virLogFilters裏面的內容是不是source->name的子串,是的話,則匹配成功。

此處,virLogFilters[0]的daemon就可以和source->name匹配,這樣source的優先級也會被filter中定義的優先級給刷新。

可以看到,log_outputs, log_level, log_filter三者組合起來使用就可以很靈活的控制LOG打印,三者之間的關係是log_level會指定默認的輸入數據(source)的級別(級別不夠的宏無法被打印,因此打印出來的一定是級別夠的),而過濾器log_filter會改變這種級別,使之以過濾器處理過的級別來輸出(可以理解成是一種中間人攻擊,中途篡改了優先級),log_filter定義的輸出通道決定了輸出的級別,如果source數據的級別小於輸出通道的級別,就無法從這個通道輸出。

5.2.使用VIR_LOG_INIT宏

上一節可以看到,log_filter可以對信息進行過濾,既然能過濾,說明log信息一定存在某種標識符,這個就是VIR_LOG_INIT決定的,例如src/qemu/qemu_process.c文件裏面開頭的VIR_LOG_INIT(“qemu.qemu_process”);
這樣,如果配置log_filters=“1:qemu.qemu_process”,該文件下的所有信息都可以被轉化爲優先級1,並且這一步是在與log_level比較之前生效的,從而小於等於log_level,因此都可以被打印出來,這裏filter的功能不涉及優先級大小的條件,只要是滿足VIR_LOG_INIT宏所定義的字符串的的前驅值,都能任意替換掉優先級,無論放大還是放小。

5.3.分析VIR_LOG_INIT宏

在這裏插入圖片描述

圖5-2 聲明本文件的log標誌

在這裏插入圖片描述

圖5-3 宏的具體信息
這裏的內存純粹初始化,沒有任何實際意義,除了name以外的東西都會變化 (gdb) p virLogSelf $11 = {name = 0x7f8f7d3e8ef0 "util.log", priority = 3, serial = 2, flags = 0} 這個virLogSelf是靜態變量,之所以每次看到的都是這個,是因爲當前斷點打到該文件裏了,因此次次都是同樣的值,static標記的變量對單個文件全局有效,而5.1節中virLogSourceUpdate的參數source就是調用VIR_DEBUG或者其他宏的代碼所在的文件的virLogSelf靜態變量,再和virLogFilters全局變量進行比較,用來確定該文件中的log是否改變優先級並且打印到log中。



6.libvirtd日誌序列化(serialization)

6.1.virLogFiltersSerial的意義

前面章節中忽略掉的最後一樣東西,就是本章要提到的序列化(這樣稱呼不確定是否合理,暫且這麼稱呼),當調用virLogSourceUpdate函數之前,要用以下語句作爲判斷

 if (source->serial < virLogFiltersSerial)
        virLogSourceUpdate(source);

在這裏插入圖片描述

圖6-1解析filter
跟蹤給全局變量virLogFiltersSerial賦值的流程可以看到,virLogParseFilters函數會解析log_filters中參數的個數,並且使得virLogFiltersSerial每次循環都加1,而virLogFiltersSerial在libvirtd初始化的時候會變成2,因此virLogFiltersSerial = 2+virLogNbFilters,經過驗證,此處代碼分析無誤。

在每次調用virLogSourceUpdate結束前,都會把source->serial 更新爲 virLogFiltersSerial,這樣代表源數據的信息已經經過filter更新過了,不需要再執行這個函數,從這個角度看,用一個bool型的變量也可以完成此功能,比如說flag屬性,但是當前的設計可以應對filter的數量動態增加的情況,這樣,virLogSourceUpdate就會被再次調用了,只是目前沒在使用和分析中發現filter數量動態增加的情況,背後的設計思路還未知,這裏不用深究。 ## 6.2.LogLock的意義 上一小節裏面提到的virLogSourceUpdate所有內容,都是在一對virLogLock()和virLogUnlock();之間的,這樣做的意義何在,按照代碼註釋描述:
/*
     * 3 intentionally non-thread safe variable reads.
     * Since writes to the variable are serialized on
     * virLogLock, worst case result is a log message
     * is accidentally dropped or emitted, if another
     * thread is updating log filter list concurrently
     * with a log message emission.
     */

大致意思就是多線程同時寫log的時候,爲了防止衝突,增加了一個鎖,在virLogSourceUpdate函數被調用之前增加一個打印,在bash下啓動libvirtd
在這裏插入圖片描述

圖6-2增加打印信息
可以看到諸如下圖所示的信息,直觀的顯示了這種設計在宏觀上遇到的場景。

在這裏插入圖片描述

圖6-3 log打印搶佔
紅色字體部分即爲發生搶佔了,兩個name爲util.file的source想調用virLogSourceUpdate函數並利用virLogFiltersSerial全局變量修改自身的序列號,由於鎖的存在,發生的搶佔,只有一個成功調用了virLogSourceUpdate函數並且修改了變量,另外一個進程就不會在同一時刻進入此函數的臨界區了。

疑問

1.log_filters="3:remote 4:event"裏面的remote和event是在哪定義的?具有什麼意義?

A:不需要定義,只是字符串而已,詳情見第5章

長期更新維護…

[1]: Linux下編譯安裝qemu和libvirt
[2]: Networking

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章