深入理解Lustre檔案系統-第1篇 跟蹤除錯系統
一直以來,Linus Torvalds對核心偵錯程式都秉持著抵觸態度,並且擺出了我是bastard我怕誰的姿態。他保持了一貫風格,言辭尖銳卻直指本質。相信這是經驗之談。在除錯核心時,最關鍵的問題是如何獲取出錯相關的資訊,準確定位出錯位置。獲取資訊有很多方法,其中核心偵錯程式只能提供有限的幫助,而分析日誌則是最基本也是最主要的方法。為核心層軟體提供一種方便的日誌工具,將大大簡化其除錯工作。在Linux社群中,對升級日誌列印系統的討論正進行得如火如荼。而Lustre檔案系統則早已擁有了一個強大、高效的核心層跟蹤除錯系統。本章將分析這個系統。
Lustre的跟蹤除錯系統可以分成兩個部分,一個是跟蹤系統,負責核心日誌的儲存和管理,另一個是除錯系統,它向上提供跟蹤系統呼叫介面,並負責跟蹤系統狀態的管理。
1.1 日誌的獲得
Lustre的日誌首先被快取在核心一塊有限的空間中。為了獲得這些日誌,需要使用Lustre給出的特定工具,即lctl debug_kernel。
除此之外,Lustre還將一部分重要資訊輸出到系統列印中,因此可以通過dmesg命令或/var/log/messages檢視。然而,這些資訊是經過刪減的。與通過lctldebug_kernel命令獲得的輸出相比,不僅列印頻率受到了限制,而且每條列印給出的資訊項也經過了刪節。
1.2 列印輸出的釋義
典型地,通過lctl debug_kernel命令獲得的列印輸出如下所示:
00000080:00000001:0.0:1339902577.938752:0:28824:0:(file.c:507:ll_file_open())Process entered 00000080:00000001:0.0:1339902577.938755:0:28824:0:(file.c:533:ll_file_open())Process leaving (rc=0 : 0 : 0)
其格式為:
子系統號:掩碼號:CPU號.型別:秒.微秒:棧地址:PID:擴充套件PID:(檔名:行數:函式名()) 輸出資訊
- 子系統號。子系統號標識了日誌輸出時所處的功能模組,例如S_MDS對應元資料伺服器,S_MDC對應元資料客戶端。
- 掩碼號。Lustre的日誌區分了多種級別和用途。例如D_TRACE代表上面所述的函式跟蹤資訊,D_WARNING代表警告資訊,而D_ERROR則代表出錯資訊。可以根據需要,通過sysctl介面更改日誌系統的掩碼號,從而使能或無效某些日誌輸出。例如,要開啟D_TRACE型別的日誌輸出,則只需執行:echo “trace” > /proc/sys/lnet/debug
- CPU號。代表了當前程序執行所在的CPU ID。為了減少衝突,提高效能,Lustre在為每個CPU單獨維護一套跟蹤資訊。
- 型別。針對Linux環境,Lustre區分了三種類型,分別對應處在硬體中斷狀態中、處在軟中斷狀態中和正常狀態。這三種類型也分別對應不同的跟蹤資訊。這些資訊儲存在struct cfs_trace_cpu_data結構體中。每個CPU和每種日誌型別都對應著不同的structcfs_trace_cpu_data結構體例項,因此例項的數目為3*NR_CPUS,其中NR_CPUS是核心配置時就已經確定的CPU數目。
- 秒和微秒。這是日誌在列印到核心空間時,通過do_gettimeofday()函式獲得的時間。
- 棧地址。GCC給出了內嵌函式,可以用以獲得函式所處的棧地址。不過這些內嵌函式是平臺相關的,例如IA64平臺是__builtin_dwarf_cfa()函式,而其他平臺是__builtin_frame_address()函式。
- PID。當前程序的PID,即current->pid。
- 擴充套件PID。在Linux作業系統中,這個部分沒有用到,而在Darwin中,則對應了current_thread()。
- 檔名、行數、函式名。為了獲得這些定位資訊,Lustre以巨集定義的形式包裝了日誌列印函式,並在巨集定義中儲存__FILE__、__LINE__和__FUNCTION__,然後以引數形式傳遞給日誌列印函式。
- 輸出資訊。應注意的是,每次呼叫日誌列印函式,都應確保列印資訊的最後字元為回車,否則Lustre將在系統日誌中輸出一行報錯。
1.3 跟蹤除錯系統的用途
對函式的跟蹤分析是跟蹤系統最基本的用處。在Lustre每個重要函式的入口處都添加了巨集定義ENTRY。在函式退出前則添加了巨集定義EXIT,或使用巨集定義RETURN替代return。如果被使能,這些巨集定義將在函式進入後和退出前分別在日誌中輸出一條列印。這些列印資訊可以描繪出函式呼叫的流程,從而方便除錯時確定出錯位置和出錯原因。
記憶體洩露檢測是跟蹤除錯系統的另外一個重要用途。記憶體洩露是核心開發人員時常遇到的棘手問題之一。Lustre的跟蹤系統為記憶體洩露檢測提供了一種方便實用的方法。Lustre使用巨集定義封裝核心的記憶體申請和釋放函式,在記憶體的每次申請和釋放時,都在日誌中輸出一條掩碼號為D_MALLOC的列印。這樣,日誌中將形成成對的記憶體申請和釋放列印。為了檢測記憶體洩露,可以在解除安裝Lustre客戶端例項後,通過lctldebug_kernel命令,獲得並分析日誌檔案,檢測是否存在記憶體洩露。Lustre以Perl指令碼(leak_finder.pl)的形式提供了分析工具。
跟蹤除錯系統還被在Lustre發現嚴重內部錯誤時的處理。在Lustre發現內部BUG時,會呼叫LBUG()巨集。在這個巨集中,Lustre會進行如下處理:
- 如果錯誤發生中斷上下文中,即in_interrupt()函式返回1,這種情況是不應存在、不被允許的,Lustre直接呼叫panic()函式,不進行任何後續處理。
- 列印函式呼叫堆疊資訊。
- 如果Lustre不會呼叫panic()函式,即libcfs_panic_on_lbug變數值為0,Lustre將啟動一個核心執行緒,把Lustre之前記錄的日誌打印出來
- 嘗試呼叫路徑位於/usr/lib/lustre/lnet_upcall的程式,以進行可能存在的額外處理。
- 如果libcfs_panic_on_lbug變數值為1,則呼叫panic()。
- 將程序狀態設定為不可中斷的等待狀態(TASK_UNINTERRUPTIBLE),並進入schedule()的死迴圈,反覆進行程序切換。
在Lustre發現斷言失效時,也會呼叫LBUG,進行上述操作。
1.4 跟蹤除錯系統的初始化
跟蹤除錯系統是Lustre中Libcfs的一部分。Libcfs是一個適用於多種作業系統的庫函式集合,Lustre的其他子系統,包括使用者層工具,均用到了Libcfs中定義的函式。Libcfs的模組初始化函式init_libcfs_module()呼叫libcfs_debug_init()函式進行跟蹤除錯系統的初始化。
前面已經提到,每個CPU和每種日誌型別都對應著不同的structcfs_trace_cpu_data結構體例項。這些例項都放置在cfs_trace_data全域性二維陣列中。libcfs_debug_init()函式所需要做的就是初始化這個陣列。
為此,它首先要確定為日誌快取分配的記憶體上限。這個值首先來自於libcfs_debug_mb變數,該變數可以通過/proc/sys/lnet/debug_mb設定。如果這個值被設定得太大(大於總物理頁數的80%,或大於512MB)或太小(使得每個CPU分配的數目小於1),那麼這個值將被修定為5MB*CPU數目。
日誌快取的記憶體上限被均攤到每個CPU上。對於每個CPU,它的每種日誌型別所能分配的日誌快取大小上限按全域性陣列pages_factor來劃分。對於Linux作業系統,每個CPU的日誌快取大小按%10、%10、%80的比例,分別劃分給硬體中斷狀態、軟中斷狀態和正常狀態。確定好的這個值被設定在struct cfs_trace_cpu_data結構體例項的tcd_max_pages欄位中。跟蹤除錯系統在初始化時並不申請日誌快取,而是在系統執行過程中動態申請快取,但是申請的快取總數將不超過tcd_max_pages欄位的限制。
注意在初始化的完成之前,呼叫ENTRY、LBUG、LASSERT、CERROR等使用了跟蹤除錯系統的函式是不被允許的。
1.5 跟蹤除錯系統的使用
將列印儲存到除錯跟蹤系統的最常用方法是呼叫CDEBUG_LIMIT巨集,它的定義如下:
#define CDEBUG_LIMIT(mask, format, ...) \
do { \
static cfs_debug_limit_state_t cdls; \
\
__CDEBUG(&cdls, mask, format, ##__VA_ARGS__);\
} while (0)
mask引數是列印的掩碼號,隨後的引數與常見的printk()函式的引數一致。
__CDEBUG巨集首先將記錄呼叫所在的子系統號、檔名、函式名和行號,然後根據子系統號和掩碼號確定是否呼叫libcfs_debug_msg()函式,儲存列印資訊。掩碼號為D_ERROR、D_EMERG、D_WARNING、D_CONSOLE的列印總是會被儲存,而其他型別的列印如果被列印,需要滿足兩個條件:一是全域性變數libcfs_debug使能了該型別的列印,而是全域性變數libcfs_subsystem_debug使能了該子系統的列印。
libcfs_debug_msg()函式呼叫libcfs_debug_vmsg2()函式完成大多數的工作。這個函式的處理流程為:
1. 根據上下文和所處CPU號確定cfs_trace_cpu_data結構例項。
2. 確定列印輸出中的頭資訊,儲存在ptldebug_header結構中。
3. 如果cfs_trace_cpu_data結構例項tcd_shutting_down欄位為有效,則表明跟蹤除錯系統處在正在退出狀態,那麼跳到第9步。
4. 估計列印資訊的長度。注意此時無法確定列印資訊的準確長度,因為CDEBUG可變引數的值會影響列印資訊長度值,Lustre將這部分資訊的平均長度預估為85。而檔名長度、函式名長度、呼叫深度(Linux未使用)和頭資訊長度(如果全域性變數libcfs_debug_binary未被置零)則是可以此時確定的資訊長度,這裡稱為已知長度。已知長度和預估長度之而後就是估計出的列印資訊的總長度。
5. 呼叫cfs_trace_get_tage()函式,該函式將以cfs_trace_cpu_data結構和列印資訊長度為引數,返回一個cfs_trace_page結構指標,作為儲存列印資訊的空間。
6. 獲取cfs_trace_page結構的page欄位的地址,並預留已知長度,然後呼叫vsnprintf,嘗試將可變引數部分列印到該位置中。注意,由於可變引數的最終輸出可能大於預估長度,因此可能無法完全將資訊列印到快取中。如果列印成功,則進行下非同步。否則,根據vsnprintf輸出的輸出長度,計算確定的列印資訊長度,返回到第5步。由此可見,對平均長度預估的值非常重要,如果預估長度過大,則會造成空間浪費,如果預估長度過小,則會使得列印重複兩次的概率過大,造成時間損失。在呼叫CDEBUG_LIMIT時,也應確保單次列印資訊不會過長。
7. 判斷列印資訊的最後一個字元是否為回車,否則列印一行報錯。
8. 將確定長度的頭資訊複製到快取的相應位置。
9. 如果掩碼號和全域性變數libcfs_printk按位與的值為非真,則不需要向系統日誌輸出,該函式返回。否則繼續。
10. 如果__CDEBUG的第一個cfs_debug_limit_state_t型別的引數不為NULL,且全域性變數libcfs_console_ratelimit為真,則說明系統日誌的輸出有列印頻率限制。方法是判斷cfs_debug_limit_state_t型別的cdls_next欄位的值,如果早於當前時間,那麼就不向系統日誌列印輸出,而返回。否則繼續。
11. 如果cfs_debug_limit_state_t型別的引數不為NULL,則更新cfs_debug_limit_state_t型別的cdls_next欄位的值。這個值是動態變化的,如果當前時間比上次設定cdls_next欄位值晚太多,則降低cdls_delay欄位的值(除以全域性變數libcfs_console_backoff的4倍),否則增加cdls_delay欄位(乘以libcfs_console_backoff)。通過cdls_delay欄位的值加上當前時間,可以獲取cdls_next欄位的值。
12. 將列印輸出到系統日誌中。
cfs_trace_get_tage()函式由於牽涉到快取的申請和重複利用,所以非常重要,它的流程為:
- 呼叫cfs_trace_get_tage_try()函式。在這個函式中,如果在cfs_trace_cpu_data結構中,當前快取頁的剩餘空間足夠儲存列印資訊,那麼該函式返回該快取頁,否則需要申請一個全新的快取頁。如果當前所使用的快取數目已經達到上限,那麼就不能再申請記憶體了,這個函式將返回NULL。如果這個函式成功申請了全新的快取頁,且當前頁數大於8,thread_running也表明有跟蹤服務執行緒在執行,那麼它將向全域性變數trace_tctl的tctl_waitq欄位傳送一個訊號,以喚醒服務執行緒。
- 如果cfs_trace_get_tage_try()函式成功獲得一個快取頁,函式返回。否則繼續。
- 如果有日誌守護程序正在執行,即全域性變數thread_running被置為有效,則呼叫cfs_tcd_shrink()函式。這個函式將把最早的10%的快取頁從cfs_trace_cpu_data結構的tcd_pages連結串列中移到tcd_daemon_pages連結串列中。如果它的tcd_cur_daemon_pages欄位表明,tcd_daemon_pages連結串列的長度超過了tcd_max_pages欄位大小,那麼這些快取頁將被直接釋放掉,這部分日誌也就丟失了。
- 如果tcd_cur_pages大於0,則表明cfs_trace_cpu_data結構中還有快取頁,那麼將最早的快取頁放到tcd_pages連結串列的最尾端,並設定這個快取頁已使用的長度為0。
1.6 服務執行緒
Lustre的跟蹤除錯系統提供了一個核心態服務執行緒,可以自動地收集核心快取中的日誌,將它儲存到給定檔案中。這個服務例程可以通過/proc/sys/lnet/daemon_file進行控制。向這個檔案寫入檔名,將啟動這個核心態服務;寫入“size=”可以設定日誌檔案的的最大大小;寫入“stop”,將終止這個服務。
這個服務執行緒將執行tracefiled()函式,其每個迴圈的流程為:
1. 通過collect_pages函式,將所有CPU、所有型別的cfs_trace_cpu_data結構中的快取頁,放置在page_collection結構的pc_pages連結串列中。
2. 開啟全域性變數cfs_tracefile所指定的檔案。如果開啟失敗,則呼叫put_pages_on_daemon_list()函式,並跳到第6步。put_pages_on_daemon_list()函式將pc_pages連結串列中的快取頁移動到相應CPU和型別的cfs_trace_cpu_data結構的tcd_daemon_pages連結串列中。
3. 將pc_pages連結串列中的所有快取頁寫入檔案中,如果寫入失敗,則pc_pages連結串列中的所有頁歸還至各cfs_trace_cpu_data結構。
4. 關閉檔案。
5. 呼叫put_pages_on_daemon_list()函式,進行從pc_pages連結串列到tcd_daemon_pages連結串列的快取頁移動。
6. 如果全域性變數trace_tctl的tctl_shutdown被設定,且last_loop變數為1,則退出該迴圈。如果last_loop不為1,則將其置為1,並返回第1步。重複一次的目的是確保在tctl_shutdown設定之後,把新列印的日誌刷入到檔案中。
7. 使得本執行緒睡眠在全域性變數trace_tctl的tctl_waitq等待佇列上,等待cfs_trace_get_tage_try()函式將自己喚醒。
1.7 跟蹤除錯系統的退出
Libcfs在解除安裝模組時將呼叫cfs_tracefile_exit()函式。這個函式首先呼叫cfs_trace_stop_thread()函式,把全域性變數trace_tctl的tctl_shutdown欄位設定為有效,並將全域性變數thread_running置零。cfs_tracefile_exit()函式隨後呼叫cfs_trace_cleanup()函式。這個函式將釋放所有申請的快取頁和其他記憶體空間。
1.8 使用者層的日誌解析
向/proc/sys/lnet/dump_kernel寫入相應檔案路徑名,可以將核心中的日誌快取儲到對應檔案中。然而這個檔案中儲存的資訊是一個二進位制資訊,其中的日誌頭是直接以二進位制的形式儲存的,因此需要通過解析這個檔案獲得日誌的字串輸出。
1.9 總結
Lustre的跟蹤除錯系統是Lustre開發的重要輔助工具。這個工具對於定位和分析錯誤有著很大的幫助作用。跟蹤除錯系統不是一個離線除錯的工具,它在Lustre的實際執行過程中保持著線上執行,在達到除錯效果的同時,跟蹤除錯系統要減少時間開銷,降低記憶體使用,因而十分注重實現的高效性。本章對這個系統進行了分析。相信讀者可以通過本章瞭解該系統的原理、實現和使用方式,也會對如何設計和實現一個強大而高效的跟蹤除錯獲得自己的認識和理解。
本文章歡迎轉載,請保留原始部落格連結http://blog.csdn.net/fsdev/article