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 日志等級定義
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的值是會在virLogSourceUpdate函數中刷新的,只要virLogSourceUpdate被調用,繼續跟蹤斷點可以看到,猜測是正確的,如下圖所示:
圖4-4 virLogSourceUpdate內部
可以看到,virLogSourceUpdate函數給source->priority進行了重新賦值,使之成為了配置文件中log_level設定的數值4,至於virLogSourceUpdate具體做了什么,這要結合最后一個filter參數來參數,比較復雜,不影響此處的理解,之后再來分析,這里可以不用深究。接下來:
圖4-5 判斷是否打印


[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
(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 宏的具體信息
6.libvirtd日志序列化(serialization)
6.1.virLogFiltersSerial的意義
前面章節中忽略掉的最后一樣東西,就是本章要提到的序列化(這樣稱呼不確定是否合理,暫且這么稱呼),當調用virLogSourceUpdate函數之前,要用以下語句作為判斷
if (source->serial < virLogFiltersSerial) virLogSourceUpdate(source);
圖6-1解析filter
在每次調用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增加打印信息

紅色字體部分即為發生搶占了,兩個name為util.file的source想調用virLogSourceUpdate函數並利用virLogFiltersSerial全局變量修改自身的序列號,由於鎖的存在,發生的搶占,只有一個成功調用了virLogSourceUpdate函數並且修改了變量,另外一個進程就不會在同一時刻進入此函數的臨界區了。
疑問
1.log_filters="3:remote 4:event"里面的remote和event是在哪定義的?具有什么意義?
A:不需要定義,只是字符串而已,詳情見第5章
長期更新維護…
[1]: Linux下編譯安裝qemu和libvirt
[2]: Networking