SystemTap除錯網絡卡狀態一例
除錯其實不僅僅是針對核心或者程序崩潰的情況,很多時候我們需要跟蹤的問題並不是通過分析一個 core dump 能夠解決的,比如類似一些狀態資訊輸出不對,或者核心或程式行為不符合預期。此時我們經常需要依賴於日誌,尤其是核心層面的問題。但是日誌往往並不不如我們期望的那樣包羅永珍,常常要面臨的窘境是日誌中空空如也。原因也很容易理解,列印日誌需要程式碼中實現的,而發生問題這部分程式碼邏輯中沒有相關實現,自然也就沒有任何日誌了。此時我們也可以考慮 gdb ,但是在雲上做 gdb kernel 除錯代價極大,基本我們不會考慮。
那麼今天我們就來了解一下 SystemTap 這樣一個輕量的除錯工具,該工具堪稱 Linux 上核心除錯的神器,筆者之前有多年的 Windows 除錯經驗,在開始使用 SystemTap 之後也不得不感嘆其強大。他的優點在於自由度高,並且可以在 live 的系統上執行,因此相當方便和高效。
首先我們簡單瞭解一下 SystemTap 的原理:
SystemTap 的基本工作原理是將指令碼編譯成核心模組,核心模組載入以後 用於檢查執行的核心的兩種方法是 Kprobes 和Kretprobe,兩種服務都整合在 Linux 核心中, Kprobes的原理相對簡單,他在需要探測的執行指令處加上特定指令,這部分原理其實和偵錯程式是類似的,因此一旦執行被探測的函式就會轉入 SystemTap 的指令碼邏輯中。 Kretprobe相對複雜,需要理解堆疊機制的工作原理,簡單來講它通過修改堆疊上函式返回地址來達到嵌入指令的目的。
為了更快了解 SystemTap 的使用方法,我們還是利用一個例項來逐步講解。
問題現象:
這也是一個比較有趣的問題,使用者在雲上例項中使用 ip link 命令後發現他的 eth0 狀態顯示為 Unknown ,如下圖:
但是如果我們建立一個相同規格並且在同一個可用區的例項是無法復現的。升級核心後問題依然存在。
研究步驟:
研究疑難問題的時候思路往往大同小異,依然是不斷對自己提問的過程,很顯然第一個問題自然就是這個 state 是從哪裡獲取的,或者說資料來源是什麼。
1. 資料來源在哪裡?
我自己是從 ip link 的程式碼出發來尋找資料來源:
顯然是來自核心網路裝置物件中的 operstate :
事實上源資料是可以從如下檔案獲得:
/sys /devices/pci0000:00/0000:00:03.0/virtio0/net/eth0/ operstate
2. 除錯什麼?
有了資料來源,接下去一個問題是,我們雖然知道錯誤的狀態是從哪裡來的,可是這只是一個靜態的資料,對我們似乎沒有意義。可以繼續我們研究的關鍵在於 - 這個資料是什麼時候被設上的。知道了這一點我們至少可以知道我們去除錯哪個過程。這個部分和除錯技術本身關聯就不大了,我們完全可以充分發散思路。我自己最後是挑選了這樣一個過程作為我的除錯物件:
rmmod virtio_net
modprobe virtio_net
重新載入虛擬網絡卡驅動,驅動被重新載入了,自然所有的網路裝置的狀態也會重新設,那麼我們就可以重點研究這個過程中為什麼把 operstate 設成了 unknown 。
3. 閱讀程式碼:
閱讀程式碼永遠是除錯的核心步驟,我們現在尋找一下核心中哪裡會設定 operstate :
我們看到在上面這部分程式碼是總是會設定 operstate ,無論是IF_OPER_LOWERLAYERDOWN,IF_OPER_DOWN或者IF_OPER_UP,至少不會是IF_OPER_UNKNOWN。也就是很有可能在非正常情況並沒有呼叫到 default_operstate() 。那麼如果確認呢?那就該輪到 SystemTap 登場了。
SystemTap 登場:
SystemTap 安裝比較簡單:
yum install kernel-devel
yum install systemtap
接下去是安裝符號檔案, centos 的話可以從 debuginfo.centos.org 下載到對應的符號檔案, rpm 安裝即可。
建立一個 stp 指令碼如:
probe begin
{
prinf("stap begin\n");
}
probe kernel.function("default_operstate")
{
printf("calling default_operstate\n")
}
執行 stap -g setlink.stp 即可。探測開始會列印 "stap begin" ,然後我們就可以開始執行 rmmod virtio_net;modprobe virtio_net ,觀察是否有輸出 default_operstate ,當然最好的方法是準備一臺正常的機器進行對比。對比結果當然是正如預期,正常的情況下能夠輸出 "calling default_operstate" ,而非正常情況卻沒有輸出。
4. 體力活:
真正的體力活開始了,接下去的思路非常簡單:
- 閱讀程式碼,看每一層的呼叫情況。
- 一旦有不確認的情況,使用 systemtap 確認呼叫路徑。
目的只有一個找到程式碼源頭上的區別。舉一個例子,確認呼叫路徑如下:
rfc2863_policy->default_operstate
但是有兩處程式碼會呼叫rfc2863_policy,linkwatch_do_dev和 linkwatch_init_dev,於是我們不想動腦的分析的話,直接修改 stp 指令碼如:
probe begin
{
prinf("stap begin\n");
}
probe kernel.function(" linkwatch_do_dev ")
{
printf("calling linkwatch_do_dev \n")
}
probe kernel.function(" linkwatch_init_dev ")
{
printf("calling linkwatch_init_dev \n")
}
在正常和非正常的機器執行 stap 對比輸出即可知道我們下一步的方向了。那麼中間的步驟我們就不贅述了,直奔主題:
正常機器:首先呼叫 netif_carrier_off然後再call netif_carrier_on->linkwatch_fire_event->linkwatch_do_dev->rfc2863_policy->default_operstate
非正常機器:直接 call netif_carrier_on,因此以下邏輯導致無法觸發event:
if (test_and_clear_bit(__LINK_STATE_NOCARRIER, &dev->state)) {
if (dev->reg_state == NETREG_UNINITIALIZED)
return;
atomic_inc(&dev->carrier_changes);
linkwatch_fire_event(dev);
上面的邏輯簡單理解為,如果先呼叫 netif_carrier_off ,那麼裝置會被標記為__LINK_STATE_NOCARRIER,之後核心網路棧監測到網路鏈路是通的,就會呼叫 netif_carrier_on ,此時會判斷__LINK_STATE_NOCARRIER是否已經標記上了,如果是說明之前的鏈路是不通的,那麼需要改變狀態就會發送 event 觸發 operstate 的改變。但是如果直接呼叫 netif_carrier_on ,裝置並沒有被標記上__LINK_STATE_NOCARRIER,也就是鏈路直接就是通,不沒有必要傳送 event 觸發後面關於 operstate 的邏輯了,自然 operstate 就停留在 unknown 的狀態了。
netif_carrier_off是在 virtio_net驅動中呼叫的。
這裡有一個邏輯判斷後端有無設上VIRTIO_NET_F_STATUS,如果是那麼我們會呼叫 netif_carrier_off ,如果不是那麼直接呼叫 netif_carrier_on ,導致問題。如果想進一步確認在這個邏輯裡的問題,很簡單,修改 stap 探測響應的程式碼行就可以了,仔細研究還會發現 stap 很多功能,比如列印引數,探測程式碼行,列印堆疊,都可以根據具體情況靈活應用。
問題結論:
VIRTIO_NET_F_STATUS是在後端 qemu 中設定的,於是我們據此就可以區分問題是否是前端還是後端產生的。