【除錯】SystemTap除錯網路卡狀態一例

江冉發表於2018-09-28

除錯其實不僅僅是針對核心或者程式崩潰的情況,很多時候我們需要跟蹤的問題並不是通過分析一個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
“);

}

 

probe kernel.function(“default_operstate”)

{

printf(“calling default_operstate
“)

}

 

執行stap -g setlink.stp即可。探測開始會列印“stap begin”,然後我們就可以開始執行rmmod virtio_net;modprobe virtio_net,觀察是否有輸出default_operstate,當然最好的方法是準備一臺正常的機器進行對比。對比結果當然是正如預期,正常的情況下能夠輸出“calling default_operstate”,而非正常情況卻沒有輸出。

 

4. 體力活:

真正的體力活開始了,接下去的思路非常簡單:

 

  1. 閱讀程式碼,看每一層的呼叫情況。
  2. 一旦有不確認的情況,使用systemtap確認呼叫路徑。

 

目的只有一個找到程式碼源頭上的區別。舉一個例子,確認呼叫路徑如下:

 

rfc2863_policy->default_operstate

 

但是有兩處程式碼會呼叫rfc2863_policy,linkwatch_do_dev和linkwatch_init_dev,於是我們不想動腦的分析的話,直接修改stp指令碼如:

 

probe begin

{

prinf(“stap begin
“);

}

 

probe kernel.function(“linkwatch_do_dev“)

{

printf(“calling linkwatch_do_dev
“)

}

 

probe kernel.function(“linkwatch_init_dev“)

{

printf(“calling linkwatch_init_dev
“)

}

 

在正常和非正常的機器執行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中設定的,於是我們據此就可以區分問題是否是前端還是後端產生的。


相關文章