天天看點

【調試】SystemTap調試網卡狀态一例

作者:江冉

調試其實不僅僅是針對核心或者程序崩潰的情況,很多時候我們需要跟蹤的問題并不是通過分析一個core dump能夠解決的,比如類似一些狀态資訊輸出不對,或者核心或程式行為不符合預期。此時我們經常需要依賴于日志,尤其是核心層面的問題。但是日志往往并不不如我們期望的那樣包羅萬象,常常要面臨的窘境是日志中空空如也。原因也很容易了解,列印日志需要代碼中實作的,而發生問題這部分代碼邏輯中沒有相關實作,自然也就沒有任何日志了。此時我們也可以考慮gdb,但是在雲上做gdb kernel調試代價極大,基本我們不會考慮。

那麼今天我們就來了解一下SystemTap這樣一個輕量的調試工具,該工具堪稱Linux上核心調試的神器,筆者之前有多年的Windows調試經驗,在開始使用SystemTap之後也不得不感歎其強大。他的優點在于自由度高,并且可以在live的系統上運作,是以相當友善和高效。

首先我們簡單了解一下SystemTap的原理:

SystemTap的基本工作原理是将腳本編譯成核心子產品,核心子產品加載以後用于檢查運作的核心的兩種方法是Kprobes和Kretprobe,兩種服務都內建在Linux核心中,Kprobes的原理相對簡單,他在需要探測的執行指令處加上特定指令,這部分原理其實和調試器是類似的,是以一旦執行被探測的函數就會轉入SystemTap的腳本邏輯中。Kretprobe相對複雜,需要了解堆棧機制的工作原理,簡單來講它通過修改堆棧上函數傳回位址來達到嵌入指令的目的。

【調試】SystemTap調試網卡狀态一例

為了更快了解SystemTap的使用方法,我們還是利用一個執行個體來逐漸講解。

問題現象:

這也是一個比較有趣的問題,使用者在雲上執行個體中使用ip link指令後發現他的eth0狀态顯示為Unknown,如下圖:

【調試】SystemTap調試網卡狀态一例

但是如果我們建立一個相同規格并且在同一個可用區的執行個體是無法複現的。更新核心後問題依然存在。

研究步驟:

研究疑難問題的時候思路往往大同小異,依然是不斷對自己提問的過程,很顯然第一個問題自然就是這個state是從哪裡擷取的,或者說資料源是什麼。

1. 資料源在哪裡?

我自己是從ip link的代碼出發來尋找資料源:

【調試】SystemTap調試網卡狀态一例

顯然是來自核心網絡裝置對象中的operstate:

【調試】SystemTap調試網卡狀态一例

事實上源資料是可以從如下檔案獲得:

/sys/devices/pci0000:00/0000:00:03.0/virtio0/net/eth0/operstate

2. 調試什麼?

有了資料源,接下去一個問題是,我們雖然知道錯誤的狀态是從哪裡來的,可是這隻是一個靜态的資料,對我們似乎沒有意義。可以繼續我們研究的關鍵在于 - 這個資料是什麼時候被設上的。知道了這一點我們至少可以知道我們去調試哪個過程。這個部分和調試技術本身關聯就不大了,我們完全可以充分發散思路。我自己最後是挑選了這樣一個過程作為我的調試對象:

rmmod virtio_net

modprobe virtio_net

重新加載虛拟網卡驅動,驅動被重新加載了,自然所有的網絡裝置的狀态也會重新設,那麼我們就可以重點研究這個過程中為什麼把operstate設成了unknown。

3. 閱讀代碼:

閱讀代碼永遠是調試的核心步驟,我們現在尋找一下核心中哪裡會設定operstate:

【調試】SystemTap調試網卡狀态一例

我們看到在上面這部分代碼是總是會設定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 beginn");

}

probe kernel.function("default_operstate")

printf("calling default_operstaten")

運作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 kernel.function("linkwatch_do_dev")

printf("calling linkwatch_do_devn")

probe kernel.function("linkwatch_init_dev")

printf("calling linkwatch_init_devn")

在正常和非正常的機器運作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驅動中調用的。

【調試】SystemTap調試網卡狀态一例

這裡有一個邏輯判斷後端有無設上VIRTIO_NET_F_STATUS,如果是那麼我們會調用netif_carrier_off,如果不是那麼直接調用netif_carrier_on,導緻問題。如果想進一步确認在這個邏輯裡的問題,很簡單,修改stap探測響應的代碼行就可以了,仔細研究還會發現stap很多功能,比如列印參數,探測代碼行,列印堆棧,都可以根據具體情況靈活應用。

問題結論:

VIRTIO_NET_F_STATUS是在後端qemu中設定的,于是我們據此就可以區分問題是否是前端還是後端産生的。

繼續閱讀