內(nèi)核調(diào)試?yán)鳎黤trace 使用教程
1. 位置無關(guān)碼
加載地址:存儲(chǔ)代碼的物理地址。如ARM64處理器上電復(fù)位后是從0x0地址開始第一條指令的,所以通常這個(gè)地方存放代碼最開始的部分,如異常向量表的處理地址
運(yùn)行地址:指程序運(yùn)行時(shí)的地址
鏈接地址:在編譯鏈接時(shí)指定的地址,編程人員設(shè)想將來程序要運(yùn)行的地址。程序所有標(biāo)號的地址在鏈接后便確定了,不管程序在哪里運(yùn)行都不會(huì)改變。aarch64-linux-gnu-obidump (objdump)工具進(jìn)行反匯編查看的就接地鏈接地址
鏈接地址和運(yùn)行地址可以相同,也可以不同。那什么時(shí)候運(yùn)行地址和鏈接地址不相同,什么時(shí)候相同呢?我們以一塊ARM64開發(fā)板為例,芯片內(nèi)部有SRAM,起始地址為0x0,DDR內(nèi)存的起始地址為0x4000 0000。
通常代碼存儲(chǔ)在 Nor Flash存儲(chǔ)器或者 Nand Flash存儲(chǔ)器中,芯片內(nèi)部的 BOOT ROM會(huì)把開始的小部分代碼裝載到SRAM中運(yùn)行。芯片上電復(fù)位之后,從SRAM中取指令。由于 Uboot的鏡像太大了,SRAM放不下,因此必須要放在DDR內(nèi)存中。通常Uboot編譯時(shí)鏈接地址都沒置到DDR內(nèi)存中,也就是0x4000 0000地址處。那這時(shí)運(yùn)行地址和鏈接地址就不一樣了。運(yùn)行地址為0x0,鏈接地址變成了0x4000 0000那么程序?yàn)槭裁催€能運(yùn)行呢個(gè)重要問題,就是位置無關(guān)代碼和位置有關(guān)代碼。
位置無關(guān)代碼:從字面意思看,該指令的執(zhí)行是與內(nèi)存地址無關(guān)的;無論運(yùn)行地址和鏈接地址相等或者不相等,該指令都能正常運(yùn)行。在匯編語言中,像BL、B、MOV指令屬于位置無關(guān)指令,不管程序裝載在哪個(gè)位置,它們都能正確地運(yùn)行,它們的地址域是基于PC值的相對偏移尋址,相當(dāng)于[pc+offset]
位置有關(guān)代碼:從字面意思看,該指令的執(zhí)行是與內(nèi)存地址有關(guān)的,和當(dāng)前PC值無關(guān)。ARM匯編里面通過絕對跳轉(zhuǎn)修改PC值為當(dāng)前鏈接地址的值
1 | ldr pc, = on_sdram ;跳到 SDRAM中繼續(xù)執(zhí)行 |
因此,當(dāng)通過LDR指令跳轉(zhuǎn)到鏈接地址處執(zhí)行時(shí),運(yùn)行地址就等于鏈接地址了。這個(gè)過程叫作“重定位”。在重定位之前,程序只能執(zhí)行和位置無關(guān)的一些匯編代碼。為什么要刻意設(shè)置加載地址、運(yùn)行地址以及鏈接地址不一樣呢?如果所有代碼都在ROM(或 Nor Flash存儲(chǔ)器)中執(zhí)行,那么鏈接地址可以與加載地址相同;而在實(shí)際項(xiàng)目應(yīng)用中,往往想要把程序加載到DDR內(nèi)存中,DDR內(nèi)存的訪問速度比ROM要快很多,而且容量也大。但是礙于加載地址的影響,不可能直接達(dá)到這一步,所以思路就是讓程序的加載地址等于ROM起始地址,而鏈接地址等于DDR內(nèi)存中某一處的起始地址(暫且稱為 ram start)。程序先從ROM中啟動(dòng),最先啟動(dòng)的部分要實(shí)現(xiàn)代碼復(fù)制功能(把整個(gè)ROM代碼復(fù)制到DDR內(nèi)存中),并通過LDR指令來跳轉(zhuǎn)到DDR內(nèi)存中,也就是在鏈接地址里運(yùn)行B指令沒法實(shí)現(xiàn)這個(gè)跳轉(zhuǎn))。上述重定位過程在U-Boot中實(shí)現(xiàn),如圖所示。

當(dāng)跳轉(zhuǎn)到 Linux內(nèi)核中時(shí),U-Boot需要把 Linux內(nèi)核映像內(nèi)容復(fù)制到DDR內(nèi)存中,然后跳轉(zhuǎn)到內(nèi)核入口地址處( stext函數(shù))。當(dāng)跳轉(zhuǎn)到內(nèi)核入口地址( stext函數(shù))時(shí),程序運(yùn)行在運(yùn)行地址,即DDR內(nèi)存的地址。但是我們從 vmlinux看到的 stext 函數(shù)的鏈接地址是虛擬地址(內(nèi)核啟動(dòng)匯編代碼也需要一個(gè)重定位過程。這個(gè)重定位過程在__primary_switch()匯編函數(shù)中完成。啟動(dòng)MMU之后,通過ldr指令把 __primary_switched()函數(shù)的鏈接地址加載到x8寄存器,然后通過br指令跳轉(zhuǎn)到 __primary_switched()函數(shù)的鏈接地址處,從而實(shí)現(xiàn)了重定位,如圖所示
1 | <arch/arm64/kernel/head.S> |
2. ftrace
frace最早出現(xiàn)在 Linux2.6.27內(nèi)核中,其設(shè)計(jì)目標(biāo)簡單,基于靜態(tài)代碼插樁(stub)技術(shù),不需要用戶通過額外的編程來定義 trace行為。靜態(tài)代碼插樁技術(shù)比較可靠,不會(huì)因?yàn)橛脩羰褂貌划?dāng)而導(dǎo)致內(nèi)核崩潰。ftrace 的名字源于 function trace利用GCC的 profile特性在所有函數(shù)入口處添加一段插樁代碼, ftrace重載這段代碼來實(shí)現(xiàn) trace 功能。GCC的-pg選項(xiàng)會(huì)在每個(gè)函數(shù)入口處加入 mcount的調(diào)用代碼,原本 mcount有l(wèi)ibc實(shí)現(xiàn),而內(nèi)核不會(huì)鏈接libc庫,因此frace編寫了自己的mcount stub函數(shù)。在使用ftrace之前,需要確保內(nèi)核編譯配置選項(xiàng)。
1 | CONFIG_FTRACE=y |
ftrace的相關(guān)配置選項(xiàng)比較多,針對不同的跟蹤器有各自對應(yīng)的配置選項(xiàng)。ftrace通過debugfs文件系統(tǒng)向用戶空間提供訪間接口,因此需要在系統(tǒng)啟動(dòng)時(shí)掛載 debugfs,可以修改系統(tǒng)的 /etc/fstab文件或手動(dòng)掛載。
1 | mount -t debugfs debugfs/sys/kernel/debug |
在 sys/kernel/debug/trace目錄下提供了各種跟蹤器( tracer)和事件( event),一些常用的選項(xiàng)如下。
available_tracers:列出當(dāng)前系統(tǒng)支持的跟蹤器
available_events:列出當(dāng)前系統(tǒng)支持的事件
current_tracer:設(shè)置和顯示當(dāng)前正在使用的跟蹤器。使用echo命令把跟蹤器的名字寫入該文件,即可切換不同的跟蹤器。默認(rèn)為nop,即不做任何跟蹤操作
trace: 讀取跟蹤信息。通過cat命令查看 ftrace記錄下來的眼蹤信息
tracing_on:用于開始或暫停跟蹤
trace_options:設(shè)置 ftrace的一些相關(guān)選項(xiàng)
ftrace當(dāng)前包含多個(gè)跟蹤器,方便用戶跟蹤不同類型的信息,如進(jìn)程睡眠、喚醒、搶占、延遲的信息。查看 available_tracers可以知道當(dāng)前系統(tǒng)支持哪些跟蹤器,如果系統(tǒng)支持的跟蹤器上沒有用戶想要的。那就必須在配置內(nèi)核時(shí)打開,然后重新編譯內(nèi)核。常用的ftrace跟蹤器如下所示:
nop:不跟蹤任何信息。將nop寫入current_tracer文件可以清空之前收集到的跟蹤信息
function:跟蹤內(nèi)核函數(shù)執(zhí)行情況
function_graph:可以顯示類似于C語言的函數(shù)調(diào)用關(guān)系圖,比較直觀
hwlat:用來跟蹤與硬件相關(guān)的延時(shí)
blk:跟蹤塊設(shè)備的函數(shù)
mmiotrace:用于跟蹤內(nèi)存映射I/O操作
wakeup:跟蹤普通優(yōu)先級的進(jìn)程從獲得調(diào)度到被喚醒的最長延遲時(shí)間
weakup_rt:跟蹤RT類型的任務(wù)從獲得調(diào)度到被喚醒的最長延遲時(shí)間
irqoff:跟蹤關(guān)閉中斷的信息,并記錄關(guān)閉的最大時(shí)長
preemptoff:跟蹤關(guān)閉禁止搶占的信息,并記錄關(guān)閉的最大時(shí)長
3. irqs跟蹤器
當(dāng)中斷關(guān)閉(俗稱關(guān)中斷)后,CPU就不能響應(yīng)其他的事件。如果這時(shí)有一個(gè)鼠標(biāo)中斷,要在下一次開中斷時(shí)才能響應(yīng)這個(gè)中斷,這段延時(shí)稱為中斷延遲。向current_tracer文件寫入 irqsoff字符串即可打開 irqsoff來跟蹤中斷延遲。
1 | cd /sys/kernel/debug/tracing/ |
4. Function tracing - no modification necessary
Ftrace 最強(qiáng)大的追蹤器之一是函數(shù)追蹤器。它使用gcc的-pg選項(xiàng)讓內(nèi)核中的每個(gè)函數(shù)調(diào)用一個(gè)特殊的函數(shù)“ mcount() ”。該函數(shù)必須在匯編中實(shí)現(xiàn),因?yàn)檎{(diào)用不遵循正常的 C ABI。
當(dāng)配置 CONFIG_DYNAMIC_FTRACE 時(shí),調(diào)用會(huì)在啟動(dòng)時(shí)轉(zhuǎn)換為 NOP,以保持系統(tǒng)以 100% 的性能運(yùn)行。在編譯過程中,記錄了 mcount() 調(diào)用站點(diǎn)。該列表在啟動(dòng)時(shí)用于將這些站點(diǎn)轉(zhuǎn)換為 NOP。由于 NOP 對跟蹤毫無用處,因此當(dāng)啟用函數(shù)(或函數(shù)圖)跟蹤器時(shí),保存該列表以將調(diào)用站點(diǎn)轉(zhuǎn)換回跟蹤調(diào)用。
由于此性能增強(qiáng),強(qiáng)烈建議啟用 CONFIG_DYNAMIC_FTRACE。此外,CONFIG_DYNAMIC_FTRACE 提供了篩選應(yīng)跟蹤哪個(gè)函數(shù)的能力。請注意,即使 NOP 在基準(zhǔn)測試中沒有顯示任何影響,但已知添加-pg選項(xiàng)附帶的幀指針會(huì)導(dǎo)致輕微的開銷。
要找出哪些跟蹤器可用,只需在跟蹤目錄中查找available_tracers文件即可 :
1 | [tracing]# cat available_tracers |
要啟用函數(shù)跟蹤器,只需將“function” echo 到 current_tracer文件中。
1 | [tracing]# echo function > current_tracer |
標(biāo)題很好地解釋了輸出的格式。前兩項(xiàng)是跟蹤的任務(wù)名稱和 PID。執(zhí)行跟蹤的 CPU 位于括號內(nèi)。時(shí)間戳是自啟動(dòng)以來的時(shí)間,后跟函數(shù)名稱。在這種情況下,函數(shù)是被跟蹤的函數(shù),其父函數(shù)跟在“ <- ”符號之后。
這些信息非常強(qiáng)大,并且很好地顯示了函數(shù)的流程。但這可能有點(diǎn)難以遵循。由 Frederic Weisbecker 創(chuàng)建的函數(shù)圖跟蹤器跟蹤函數(shù)的進(jìn)入和退出,這使跟蹤器能夠了解被調(diào)用函數(shù)的深度。函數(shù)圖跟蹤器可以使人眼更容易跟蹤內(nèi)核中的執(zhí)行流程:
1 | [tracing]# echo function_graph > current_tracer |
這給出了一個(gè)函數(shù)的開始和結(jié)束,用類似 C 的注釋“ { ”來啟動(dòng)一個(gè)函數(shù),“ } ”在末尾。葉函數(shù)不調(diào)用其他函數(shù),只是以“ ; ”結(jié)尾。DURATION 列顯示在相應(yīng)函數(shù)中花費(fèi)的時(shí)間。函數(shù)圖跟蹤器記錄函數(shù)進(jìn)入和退出的時(shí)間,并將差異報(bào)告為持續(xù)時(shí)間。這些數(shù)字只出現(xiàn)在葉函數(shù)和“ }" 符號。注意,這次還包括嵌套函數(shù)內(nèi)所有函數(shù)的開銷以及函數(shù)圖跟蹤器本身的開銷。函數(shù)圖跟蹤器劫持了函數(shù)的返回地址,以便為函數(shù)插入跟蹤回調(diào)函數(shù)退出。這會(huì)破壞 CPU 的分支預(yù)測并導(dǎo)致比函數(shù)跟蹤器更多的開銷。最接近的真實(shí)時(shí)序僅發(fā)生在葉函數(shù)中。
孤獨(dú)的“ + ”是有一個(gè)注釋標(biāo)記。當(dāng)持續(xù)時(shí)間大于 10 微秒時(shí),顯示“ + ”。如果持續(xù)時(shí)間大于 100 微秒,將顯示“ !”。
5. Using trace_printk()
printk()是所有調(diào)試器之王,但它有一個(gè)問題。如果您正在調(diào)試諸如定時(shí)器中斷、調(diào)度程序或網(wǎng)絡(luò)之類的大容量區(qū)域,printk()可能會(huì)導(dǎo)致系統(tǒng)陷入困境,甚至可能會(huì)創(chuàng)建實(shí)時(shí)鎖。添加一些printk()時(shí),看到錯(cuò)誤“消失”也很常見。這是由于printk()引入的絕對開銷。
Ftrace 引入了一種新形式的printk()稱為 trace_printk()。它可以像printk()一樣使用,也可以在任何上下文中使用(中斷代碼、NMI 代碼和調(diào)度程序代碼)。是什么樣的好的trace_printk()是,它不會(huì)輸出到控制臺(tái)。相反,它寫入 Ftrace 環(huán)形緩沖區(qū),并且可以通過跟蹤文件讀取。
使用trace_printk()寫入環(huán)形緩沖區(qū)只需要大約十分之一微秒左右。但是使用printk(),尤其是在寫入串行控制臺(tái)時(shí),每次寫入可能需要幾毫秒。trace_printk()的性能優(yōu)勢 使您可以記錄內(nèi)核中最敏感的區(qū)域,而幾乎沒有影響。
例如,您可以將這樣的內(nèi)容添加到內(nèi)核或模塊中:
1 | trace_printk("read foo %d out of bar %p\n", bar->foo, bar); |
然后通過查看跟蹤文件,您可以看到您的輸出。
1 | [tracing]# cat trace |
上面的示例是通過添加一個(gè)實(shí)際上具有foo和bar構(gòu)造的模塊來完成的 。
trace_printk()輸出將出現(xiàn)在任何跟蹤器中,甚至是函數(shù)和函數(shù)圖跟蹤器。
1 | [tracing]# echo function_graph > current_tracer |
是的,trace_printk() 輸出看起來像函數(shù)圖跟蹤器中的注釋。
6. Starting and stopping the trace
顯然,有時(shí)您只想跟蹤特定的代碼路徑。也許您只想跟蹤運(yùn)行特定測試時(shí)發(fā)生的情況。文件tracing_on用于禁止環(huán)形緩沖區(qū)記錄數(shù)據(jù):
1 | [tracing]# echo 0 > tracking_on |
這將禁用 Ftrace 環(huán)形緩沖區(qū)的記錄。其他所有事情仍然發(fā)生在跟蹤器上,它們?nèi)匀粫?huì)產(chǎn)生大部分開銷。他們確實(shí)注意到環(huán)形緩沖區(qū)沒有記錄,也不會(huì)嘗試寫入任何數(shù)據(jù),但仍會(huì)執(zhí)行跟蹤器發(fā)出的調(diào)用。
要重新啟用環(huán)形緩沖區(qū),只需將“1”寫入該文件:
1 | [tracing]# echo 1 >tracing_on |
請注意,在數(shù)字和大于號“ > ”之間有一個(gè)空格非常重要。否則,您可能正在將標(biāo)準(zhǔn)輸入或輸出寫入該文件。
1 | [tracing]# echo 0>tracing_on /* 這行不通!*/ |
一個(gè)常見的運(yùn)行可能是:
1 | [tracing]# echo 0 > tracing_on |
第一行禁止環(huán)形緩沖區(qū)記錄任何數(shù)據(jù)。接下來啟用函數(shù)圖跟蹤器。函數(shù)圖跟蹤器的開銷仍然存在,但不會(huì)將任何內(nèi)容記錄到跟蹤緩沖區(qū)中。最后一行啟用環(huán)形緩沖區(qū),運(yùn)行測試程序,然后禁用環(huán)形緩沖區(qū)。這縮小了函數(shù)圖跟蹤器存儲(chǔ)的數(shù)據(jù)范圍,以僅包括run_test程序積累的數(shù)據(jù) 。
7. Trace Markers
查看內(nèi)核內(nèi)部發(fā)生的事情可以讓用戶更好地了解他們的系統(tǒng)是如何工作的。但有時(shí)需要在用戶空間發(fā)生的事情和內(nèi)核內(nèi)部發(fā)生的事情之間進(jìn)行協(xié)調(diào)。跟蹤中顯示的時(shí)間戳都與跟蹤中發(fā)生的事情有關(guān),但它們與墻時(shí)間不太對應(yīng)。
為了幫助同步用戶空間和內(nèi)核空間中的操作,創(chuàng)建了trace_marker文件。它提供了一種從用戶空間寫入 Ftrace 環(huán)形緩沖區(qū)的方法。該標(biāo)記隨后將出現(xiàn)在軌跡中,以給出軌跡中特定事件發(fā)生的位置。
1 | [tracing]# echo hello world > trace_marker |
在<...>表示該寫的標(biāo)記任務(wù)的名字沒有記錄。未來的版本可能會(huì)解決這個(gè)問題。
8. Starting, Stopping and Recording in a Program
該tracing_on和trace_marker 文件的工作很好地跟蹤應(yīng)用程序的活動(dòng),如果應(yīng)用程序的源可用。如果應(yīng)用程序中存在問題并且您需要找出應(yīng)用程序特定位置的內(nèi)核內(nèi)部發(fā)生了什么,這兩個(gè)文件就派上用場了。
在應(yīng)用程序啟動(dòng)時(shí),您可以打開這些文件以準(zhǔn)備好文件描述符:
1 | int trace_fd = -1; |
然后,在代碼中的某個(gè)關(guān)鍵位置,可以放置標(biāo)記以顯示應(yīng)用程序當(dāng)前所在的位置:
1 | if (marker_fd >= 0) |
在查看示例時(shí),您首先會(huì)看到一個(gè)名為“find_debugfs()”的函數(shù)。掛載調(diào)試文件系統(tǒng)的正確位置是/sys/kernel/debug但強(qiáng)大的工具不應(yīng)依賴于掛載在那里的調(diào)試文件系統(tǒng)。find_debugfs()的示例 位于此處。文件描述符被初始化為 -1 以允許此代碼在啟用和不啟用跟蹤的內(nèi)核的情況下工作。
當(dāng)檢測到問題時(shí),將 ASCII 字符“0”寫入trace_fd文件描述符將停止跟蹤。正如在第 1 部分中討論的那樣,這只會(huì)禁用記錄到 Ftrace 環(huán)形緩沖區(qū)中,但跟蹤器仍然會(huì)產(chǎn)生開銷。
使用上面的初始化代碼時(shí),跟蹤將在應(yīng)用程序開始時(shí)啟用,因?yàn)楦櫰饕愿采w模式運(yùn)行。也就是說,當(dāng)跟蹤緩沖區(qū)填滿時(shí),它將刪除舊數(shù)據(jù)并用新數(shù)據(jù)替換它。由于在出現(xiàn)問題時(shí)只有最近的跟蹤信息是相關(guān)的,因此在應(yīng)用程序正常運(yùn)行期間無需停止和啟動(dòng)跟蹤。只有在檢測到問題時(shí)才需要禁用跟蹤器,以便跟蹤記錄導(dǎo)致錯(cuò)誤的歷史記錄。如果應(yīng)用程序中需要間隔跟蹤,它可以將 ASCII“1”寫入 trace_fd 以啟用跟蹤。
下面是一個(gè)名為simple_trace.c的簡單程序示例, 它使用上述初始化過程:
1 | req.tv_sec = 0; |
(由于這是一個(gè)僅用于示例目的的簡單程序,因此未添加錯(cuò)誤檢查。) 這是跟蹤這個(gè)簡單程序的過程:
1 | [tracing]# echo 0 > tracing_on |
第一行禁用跟蹤,因?yàn)槌绦驅(qū)⒃趩?dòng)時(shí)啟用它。接下來選擇函數(shù)圖跟蹤器。程序被執(zhí)行,結(jié)果如下。請注意,輸出可能有點(diǎn)冗長,其中大部分內(nèi)容已被刪除并替換為 [...]:
1 | [...] |
請注意,對 trace_marker 的寫入在函數(shù)圖跟蹤器中顯示為注釋。這里的第一列代表 CPU。當(dāng)我們像這樣交錯(cuò) CPU 跟蹤時(shí),可能很難讀取跟蹤。工具 grep 可以很容易地過濾它,或者可以使用 per_cpu 跟蹤文件。per_cpu 跟蹤文件位于 per_cpu 下的 debugfs 跟蹤目錄中。
1 | [tracing]# ls per_cpu |
在這些 CPU 目錄中的每一個(gè)目錄中都存在一個(gè)跟蹤文件,僅顯示該 CPU 的跟蹤。要在不受其他 CPU 干擾的情況下更好地了解函數(shù)圖跟蹤器,只需查看 per_cpu/cpu0/trace。
1 | [tracing]# cat per_cpu/cpu0/trace |
9. Disabling the Tracer Within the Kernel
在內(nèi)核驅(qū)動(dòng)程序的開發(fā)過程中,可能會(huì)存在測試過程中出現(xiàn)的奇怪錯(cuò)誤。也許驅(qū)動(dòng)陷入睡眠狀態(tài),永遠(yuǎn)不會(huì)醒來。當(dāng)內(nèi)核事件發(fā)生時(shí),試圖從用戶空間禁用跟蹤器是很困難的,通常會(huì)導(dǎo)致緩沖區(qū)溢出和相關(guān)信息丟失,然后用戶才能停止跟蹤。
有兩個(gè)在內(nèi)核中運(yùn)行良好的函數(shù):tracing_on()和tracking_off()。這兩個(gè)行為就像分別將“1”或“0” echo 到tracing_on文件中一樣。如果內(nèi)核中存在可以檢查的某些條件,則可以通過添加如下內(nèi)容來停止跟蹤器:
1 | if (test_for_error()) |
接下來,添加幾個(gè)trace_printk() s(參見第 1 部分),重新編譯并引導(dǎo)內(nèi)核。然后,您可以啟用函數(shù)或函數(shù)圖跟蹤器,然后等待錯(cuò)誤條件發(fā)生。檢查tracing_on 文件將讓您知道錯(cuò)誤條件何時(shí)發(fā)生。當(dāng)內(nèi)核調(diào)用tracking_off()時(shí),它將從“1”切換到“0” 。
檢查跟蹤后,或?qū)⑵浔4嬖诹硪粋€(gè)文件中:
1 | cat trace > ~/trace.sav |
您可以繼續(xù)跟蹤以檢查另一個(gè)命中。為此,只需將“1” echo 到tracing_on 中,跟蹤將繼續(xù)。如果可以合法觸發(fā)觸發(fā)tracing_off()調(diào)用的條件,這也很有用 。如果條件是由正常操作觸發(fā)的,只需通過在tracing_on 中echo “1”來重新啟動(dòng)跟蹤,希望下次遇到條件時(shí)將是因?yàn)楫惓!?/p>
10. ftrace_dump_on_oops
有時(shí)內(nèi)核會(huì)崩潰,檢查內(nèi)存和崩潰狀態(tài)更像是一門 CSI 科學(xué),而不是程序調(diào)試科學(xué)。將kdump / kexec與crash 實(shí)用程序一起使用是檢查崩潰點(diǎn)系統(tǒng)狀態(tài)的一種有價(jià)值的方法,但它不會(huì)讓您看到在導(dǎo)致崩潰的事件之前發(fā)生了什么。
在內(nèi)核引導(dǎo)參數(shù)中配置 Ftrace 并啟用ftrace_dump_on_oops,或者通過在/proc/sys/kernel/ftrace_dump_on_oops 中echo “1” ,將使 Ftrace 能夠在 oops 或 panic 時(shí)以 ASCII 格式將整個(gè)跟蹤緩沖區(qū)轉(zhuǎn)儲(chǔ)到控制臺(tái)。將控制臺(tái)輸出到串行日志使調(diào)試崩潰更容易。您現(xiàn)在可以追溯導(dǎo)致崩潰的事件。
轉(zhuǎn)儲(chǔ)到控制臺(tái)可能需要很長時(shí)間,因?yàn)槟J(rèn)的 Ftrace 環(huán)形緩沖區(qū)每個(gè) CPU 超過 1 兆字節(jié)。要縮小環(huán)形緩沖區(qū)的大小,請將希望環(huán)形緩沖區(qū)的千字節(jié)數(shù)寫入 buffer_size_kb。請注意,該值是每個(gè) CPU,而不是環(huán)形緩沖區(qū)的總大小。
1 | [tracing]# echo 50 > buffer_size_kb |
以上將把 Ftrace 環(huán)形緩沖區(qū)縮小到每個(gè) CPU 50 KB。您還可以使用sysrq-z將 Ftrace 緩沖區(qū)的轉(zhuǎn)儲(chǔ)觸發(fā)到控制臺(tái) 。
要為內(nèi)核轉(zhuǎn)儲(chǔ)選擇特定位置,內(nèi)核可以直接調(diào)用 ftrace_dump()。請注意,這可能會(huì)永久禁用 Ftrace,可能需要重新啟動(dòng)才能再次啟用它。這是因?yàn)?ftrace_dump()讀取緩沖區(qū)。緩沖區(qū)被寫入所有上下文(中斷、NMI、調(diào)度),但緩沖區(qū)的讀取需要鎖定。為了能夠執(zhí)行ftrace_dump()鎖定被禁用并且緩沖區(qū)可能最終在輸出后被破壞。
1 | /* |
11. Stack Tracing
最后要討論的主題是檢查內(nèi)核堆棧大小以及每個(gè)函數(shù)使用多少堆??臻g的能力。啟用堆棧跟蹤器 ( CONFIG_STACK_TRACER ) 將顯示堆棧的最大使用發(fā)生在哪里。
堆棧跟蹤器是從函數(shù)跟蹤器基礎(chǔ)結(jié)構(gòu)構(gòu)建的。它不使用 Ftrace 環(huán)形緩沖區(qū),但確實(shí)使用函數(shù)跟蹤器來掛鉤每個(gè)函數(shù)調(diào)用。因?yàn)樗褂煤瘮?shù)跟蹤器基礎(chǔ)結(jié)構(gòu),所以在未啟用時(shí)不會(huì)增加開銷。要啟用堆棧跟蹤器,請將 1 echo 到 /proc/sys/kernel/stack_tracer_enabled 中。要查看啟動(dòng)期間的最大堆棧大小,請將“ stacktrace ”添加到內(nèi)核啟動(dòng)參數(shù)。
堆棧跟蹤器在每次函數(shù)調(diào)用時(shí)檢查堆棧的大小。如果它大于最后記錄的最大值,它會(huì)記錄堆棧跟蹤并使用新大小更新最大值。要查看當(dāng)前最大值,請查看 stack_max_size文件。
1 | [tracing]# echo 1 > /proc/sys/kernel/stack_tracer_enabled |
這不僅為您提供了找到的最大堆棧的大小,還顯示了每個(gè)函數(shù)使用的堆棧大小的細(xì)分。請注意, write_cache_pages的堆棧最大,使用了 304 個(gè)字節(jié),其次是generic_make_request,使用了 224 個(gè)字節(jié)的堆棧。
要重置最大值,請將“0”回顯到stack_max_size 文件中。
1 | [tracing]# echo 0 > stack_max_size |
保持運(yùn)行一段時(shí)間將顯示內(nèi)核使用過多堆棧的位置。但請記住,堆棧跟蹤器只有在未啟用時(shí)才沒有開銷。當(dāng)它運(yùn)行時(shí),您可能會(huì)注意到性能有所下降。
請注意,當(dāng)內(nèi)核使用單獨(dú)的堆棧時(shí),堆棧跟蹤器不會(huì)跟蹤最大堆棧大小。因?yàn)橹袛嘤凶约旱亩褩?,它不?huì)跟蹤那里的堆棧使用情況。原因是當(dāng)堆棧不是當(dāng)前任務(wù)的堆棧時(shí),目前沒有簡單的方法可以快速查看堆棧的頂部是什么。使用拆分堆棧時(shí),進(jìn)程堆??赡苁莾身?,而中斷堆??赡苤挥幸豁摗_@可能會(huì)在未來修復(fù),但在使用堆棧跟蹤器時(shí)請記住這一點(diǎn)。
12. Function filtering
運(yùn)行函數(shù)跟蹤器可能會(huì)讓人不知所措。數(shù)據(jù)量可能很大,人腦很難掌握。Ftrace 提供了一種方法來限制您看到的功能。存在兩個(gè)文件,可讓您限制跟蹤的功能:
1 | set_ftrace_filter |
這些過濾功能取決于CONFIG_DYNAMIC_FTRACE 選項(xiàng)。如前幾篇文章所述,當(dāng)啟用此配置時(shí),所有mcount調(diào)用者位置都將被存儲(chǔ),并在啟動(dòng)時(shí)轉(zhuǎn)換為 NOP。這些位置被保存并用于在功能跟蹤器被激活時(shí)啟用跟蹤。但這也有一個(gè)很好的副作用:并非所有功能都必須啟用。上述文件將確定哪些功能被啟用,哪些不啟用。
當(dāng)set_ftrace_filter 中列出任何函數(shù)時(shí),只會(huì)跟蹤那些函數(shù)。當(dāng)跟蹤處于活動(dòng)狀態(tài)時(shí),這將有助于系統(tǒng)的性能。跟蹤每個(gè)函數(shù)會(huì)產(chǎn)生很大的開銷,但是在使用set_ftrace_filter 時(shí),只有該文件中列出的那些函數(shù)才會(huì)更改 NOP 以調(diào)用跟蹤器。根據(jù)正在跟蹤的功能,僅啟用幾百個(gè)功能幾乎不會(huì)引起注意。
該set_ftrace_notrace文件是相反set_ftrace_filter。不是將跟蹤限制為一組函數(shù),而是不會(huì)跟蹤set_ftrace_notrace 中列出的函數(shù)。某些函數(shù)經(jīng)常出現(xiàn),跟蹤這些函數(shù)不僅會(huì)減慢系統(tǒng)速度,還會(huì)填滿跟蹤緩沖區(qū),并使分析您關(guān)心的函數(shù)變得更加困難。rcu_read_lock()和spin_lock()等函數(shù) 屬于這一類。
向這些文件添加函數(shù)的過程通常使用 bash 重定向。使用符號“>”將刪除文件中的所有現(xiàn)有函數(shù)并將正在回顯的內(nèi)容添加到文件中。使用“>>”附加到文件將保留現(xiàn)有功能并添加新功能。
1 | [tracing]# echo sys_read > set_ftrace_filter |
要?jiǎng)h除所有功能,只需在過濾器文件中回顯一個(gè)空行即可。
1 | [tracing]# echo sys_read sys_open sys_write > set_ftrace_notrace |
這些文件中列出的函數(shù)也可以在內(nèi)核命令行上設(shè)置。選項(xiàng) ftrace_notrace 和 ftrace_filter 將通過列出逗號分隔的函數(shù)集來預(yù)設(shè)這些文件。
ftrace_notrace=rcu_read_lock,rcu_read_unlock,spin_lock,spin_unlock
ftrace_filter=kfree,kmalloc,schedule,vmalloc_fault,spurious_fault內(nèi)核命令行添加的函數(shù)設(shè)置了相應(yīng)過濾器文件中的內(nèi)容。這些選項(xiàng)僅預(yù)加載文件,仍然可以使用如上所述的 bash 重定向來刪除或添加功能。set_ftrace_notrace 中列出的函數(shù)優(yōu)先。也就是說,如果一個(gè)函數(shù)同時(shí)列在 set_ftrace_notrace 和 set_ftrace_filter 中,則不會(huì)跟蹤該函數(shù)。
13. Wildcard filters
可以添加到過濾器文件的函數(shù)列表顯示在 available_filter_functions 文件中。這個(gè)函數(shù)列表源自前面提到的存儲(chǔ)的 mcount 調(diào)用者列表。
1 | [tracing]# cat available_filter_functions | head -8 |
您可以 grep 此文件并將結(jié)果重定向到過濾器文件之一:
1 | [tracing]# grep sched available_filter_functions > set_ftrace_filter |
不幸的是,向過濾文件添加大量函數(shù)很慢,您會(huì)注意到上面的 grep 需要幾秒鐘才能執(zhí)行。這是因?yàn)閷懭脒^濾器文件的每個(gè)函數(shù)名稱將被單獨(dú)處理。上面的 grep 產(chǎn)生了 300 多個(gè)函數(shù)名。這 300 個(gè)名稱中的每一個(gè)都將與內(nèi)核中的每個(gè)函數(shù)名稱進(jìn)行比較(使用 strcmp()),這相當(dāng)多。
1 | [tracing]# wc -l available_filter_functions |
所以上面的 grep 導(dǎo)致set_ftrace_filter生成超過 300 * 24331 (7,299,300) 次比較!
幸運(yùn)的是,這些文件也使用通配符;以下 glob 表達(dá)式是有效的:
value* - 選擇所有以value開頭的函數(shù)。*value* - 選擇所有包含文本value 的函數(shù)。*value - 選擇所有以value結(jié)尾的函數(shù)。
內(nèi)核包含一個(gè)相當(dāng)簡單的解析器,不會(huì)以預(yù)期的方式處理 value*value。它將忽略第二個(gè) 值并選擇所有以value開頭的函數(shù),而不管它以什么結(jié)尾。傳遞給過濾器文件的通配符直接針對每個(gè)可用函數(shù)進(jìn)行處理,這比在列表中傳遞單個(gè)函數(shù)要快得多。
因?yàn)?bash 也使用星號 (*),所以最好用引號將輸入括起來:
1 | [tracing]# echo set* > set_ftrace_filter |
過濾器還可以通過在過濾器文件的輸入中使用“mod”命令來僅選擇屬于特定模塊的那些函數(shù):
1 | [tracing]# echo ':mod:tg3' > set_ftrace_filter |
如果您正在調(diào)試單個(gè)模塊,并且只想在跟蹤中查看屬于該模塊的函數(shù),這將非常有用。
在之前的文章中,啟用和禁用記錄到環(huán)形緩沖區(qū)是使用tracing_on文件以及tracing_on()和 tracing_off()內(nèi)核函數(shù)完成的。但是,如果您不想重新編譯內(nèi)核,并且想在特定函數(shù)處停止跟蹤,則 set_ftrace_filter有一個(gè)方法可以這樣做。使功能跟蹤啟用或禁用環(huán)形緩沖區(qū)的命令格式如下:
function:command[:count]這將在函數(shù)開始 時(shí)執(zhí)行命令。該命令是 traceon或traceoff,并且可以添加一個(gè)可選的計(jì)數(shù)以使命令只執(zhí)行給定的次數(shù)。如果計(jì)數(shù)被保留(包括前導(dǎo)冒號),則每次調(diào)用該函數(shù)時(shí)都會(huì)執(zhí)行該命令。
不久前,我正在調(diào)試對內(nèi)核所做的更改,該更改導(dǎo)致某些程序出現(xiàn)分段錯(cuò)誤。我很難捕捉到跟蹤,因?yàn)楫?dāng)我看到分段錯(cuò)誤后能夠停止跟蹤時(shí),數(shù)據(jù)已經(jīng)被覆蓋了。但是控制臺(tái)上的回溯顯示正在調(diào)用函數(shù)__bad_area_nosemaphore。然后我可以使用以下命令停止跟蹤器:
1 | [tracing]# echo '__bad_area_nosemaphore:traceoff' > set_ftrace_filter |
請注意,帶有命令的函數(shù)不會(huì)影響一般過濾器。即使已將命令添加到 __bad_area_nosemaphore,過濾器仍允許跟蹤所有函數(shù)。命令和過濾器功能是分開的,互不影響。將上述命令附加到函數(shù) __bad_area_nosemaphore 后,下次發(fā)生分段錯(cuò)誤時(shí),跟蹤停止并包含調(diào)試情況所需的數(shù)據(jù)。
14. Removing functions from the filters
如前所述,用“>”回顯將清除過濾器文件。但是如果您只想從過濾器中刪除一些功能怎么辦?
1 | [tracing]# cat set_ftrace_filter > /tmp/filter |
上述工作,但如前所述,如果 set_ftrace_filter 中已有多個(gè)函數(shù),則可能需要一段時(shí)間才能完成。以下執(zhí)行相同的操作,但速度要快得多:
1 | [tracing]# echo '!*lock*' >> set_ftrace_filter |
這 '!'符號將刪除過濾器文件中列出的函數(shù)。如上所示,“!”與通配符一起使用,但也可以與單個(gè)函數(shù)一起使用。自從 '!'在 bash 中具有特殊含義,它必須用單引號括起來,否則 bash 將嘗試執(zhí)行其后的內(nèi)容。另請注意使用了“>>”。如果您錯(cuò)誤地使用了“>”,則過濾器文件中將沒有任何功能。因?yàn)槊詈瓦^濾器不會(huì)相互干擾,清除 set_ftrace_filter 不會(huì)清除命令。命令必須用“!”清除象征。
1 | [tracing]# echo 'sched*' > set_ftrace_filter |
這可能看起來很別扭,但是使用“>”和“>>”只影響要跟蹤的函數(shù)而不影響函數(shù)命令,實(shí)際上簡化了過濾函數(shù)和添加和刪除命令之間的控制。
15. Tracing a specific process
也許您只需要跟蹤一個(gè)特定的進(jìn)程或一組進(jìn)程。文件 set_ftrace_pid 允許您指定要跟蹤的特定進(jìn)程。要僅跟蹤當(dāng)前線程,您可以執(zhí)行以下操作:
1 | [tracing]# echo $$ > set_ftrace_pid |
上面將設(shè)置函數(shù) tracer 只跟蹤執(zhí)行 echo 命令的 bash shell。如果要跟蹤特定進(jìn)程,可以創(chuàng)建一個(gè) shell 腳本包裝程序。
1 | [tracing]# cat ~/bin/ftrace-me |
請注意,如果要在執(zhí)行上述操作后返回通用函數(shù)跟蹤,則必須清除 set_ftrace_pid 文件。
1 | [tracing]# echo -1 > set_ftrace_pid |
16. What calls a specific function?
有時(shí)了解什么在調(diào)用特定函數(shù)很有用。直接前任很有幫助,但整個(gè)回溯甚至更好。函數(shù)跟蹤器包含一個(gè)選項(xiàng),該選項(xiàng)將為跟蹤器調(diào)用的每個(gè)函數(shù)在環(huán)形緩沖區(qū)中創(chuàng)建一個(gè)回溯。由于為每個(gè)函數(shù)創(chuàng)建回溯具有很大的開銷,這可能會(huì)實(shí)時(shí)鎖定系統(tǒng),因此在使用此功能時(shí)必須小心。想象一下運(yùn)行在 1000 HZ 的較慢系統(tǒng)上的定時(shí)器中斷。很可能讓定時(shí)器中斷調(diào)用產(chǎn)生回溯的每個(gè)函數(shù)需要 1 毫秒才能完成。到定時(shí)器中斷返回時(shí),將在任何其他工作完成之前觸發(fā)一個(gè)新的中斷,從而導(dǎo)致活鎖。
要使用函數(shù)跟蹤器回溯功能,被調(diào)用的函數(shù)必須受到函數(shù)過濾器的限制。啟用函數(shù)回溯的選項(xiàng)是函數(shù)跟蹤器獨(dú)有的,只有在啟用函數(shù)跟蹤器時(shí)才能激活它。這意味著您必須先啟用函數(shù)跟蹤器,然后才能訪問該選項(xiàng):
1 | [tracing]# echo kfree > set_ftrace_filter |
請注意,在啟用 func_stack_trace 選項(xiàng)以確保啟用過濾器之前,我小心地對 set_ftrace_filter 進(jìn)行分類。最后,我在禁用過濾器之前禁用了 options/func_stack_trace。還要注意該選項(xiàng)是非易失性的,也就是說,即使您在 current_tracer 中啟用了另一個(gè)跟蹤器插件,如果您重新啟用跟蹤器功能,該選項(xiàng)仍然會(huì)啟用。
17. The function_graph tracer
函數(shù)跟蹤器非常強(qiáng)大,但可能很難理解它產(chǎn)生的線性格式。Frederic Weisbecker 已將函數(shù)跟蹤器擴(kuò)展到 function_graph 跟蹤器。function_graph 跟蹤器搭載了大部分由函數(shù)跟蹤器創(chuàng)建的代碼,但在mcount調(diào)用中添加了自己的鉤子。因?yàn)樗匀皇褂胢count調(diào)用方法,所以上面解釋的大部分函數(shù)過濾也適用于 function_graph 跟蹤器,但traceon / traceoff命令和set_ftrace_pid 除外(盡管后者將來可能會(huì)改變)。function_graph tracer在之前的文章中也有說明,但是set_graph_function文件沒有說明。上一節(jié)中使用的func_stack_trace可以看到什么可能調(diào)用一個(gè)函數(shù),但是set_graph_function可以用來查看一個(gè)函數(shù)調(diào)用了什么:
1 | [tracing]# echo kfree > set_graph_function |
這將顯示僅由kfree()執(zhí)行的調(diào)用圖?!?==========> ”表示通話過程中發(fā)生了中斷。跟蹤記錄kfree() 塊中的所有函數(shù),甚至是那些在kfree()范圍內(nèi)觸發(fā)的中斷調(diào)用的函數(shù)。
function_graph 跟蹤器顯示函數(shù)在持續(xù)時(shí)間字段中花費(fèi)的時(shí)間。在之前的文章中提到,只有葉子函數(shù),即不調(diào)用其他函數(shù)的葉函數(shù),才有準(zhǔn)確的持續(xù)時(shí)間,因?yàn)楦负瘮?shù)的持續(xù)時(shí)間還包括 function_graph 跟蹤器調(diào)用子函數(shù)的開銷。通過使用set_ftrace_filter文件,您可以強(qiáng)制任何函數(shù)成為 function_graph 跟蹤器中的葉函數(shù),這將允許您查看該函數(shù)的準(zhǔn)確持續(xù)時(shí)間。
1 | [tracing]# echo smp_apic_timer_interrupt > set_ftrace_filter |
上面顯示定時(shí)器中斷需要 16 到 26 微秒才能完成。
18. Function profiling
oprofile和perf是非常強(qiáng)大的分析工具,它們定期對系統(tǒng)進(jìn)行采樣,并可以顯示大部分時(shí)間都花在了什么地方。使用函數(shù)分析器,可以很好地查看實(shí)際的函數(shù)執(zhí)行情況,而不僅僅是示例。如果內(nèi)核中配置了CONFIG_FUNCTION_GRAPH_TRACER,則函數(shù)分析器將使用函數(shù)圖基礎(chǔ)結(jié)構(gòu)來記錄函數(shù)執(zhí)行了多長時(shí)間。如果只配置了CONFIG_FUNCTION_TRACER,函數(shù)分析器將只計(jì)算被調(diào)用的函數(shù)。
1 | [tracing]# echo nop > current_tracer |
以上還包括函數(shù)被搶占或 schedule() 被調(diào)用以及任務(wù)被換出的次數(shù)。這可能看起來沒用,但它確實(shí)可以讓我們了解哪些函數(shù)經(jīng)常被搶占。Ftrace 還包括允許您讓函數(shù)圖跟蹤器忽略任務(wù)計(jì)劃時(shí)間的選項(xiàng)。
1 | [tracing]# echo 0 > options/sleep-time |
請注意,sleep-time選項(xiàng)包含“-”,而不是 sleep_time。
禁用功能分析器然后重新啟用它會(huì)導(dǎo)致數(shù)字重置。該列表按平均時(shí)間排序,但使用腳本您可以輕松地按任何數(shù)字排序。所述trace_stat / function0僅表示存在一個(gè)CPU 0 trace_stat /功能#為系統(tǒng)上的每個(gè)CPU。所有被追蹤和命中的函數(shù)都在這個(gè)文件中。
1 | [tracing]# cat trace_stat/function0 | wc -l |
未命中的函數(shù)未列出。以上顯示自我開始分析以來,已命中 2978 個(gè)函數(shù)。
影響分析的另一個(gè)選項(xiàng)是圖形時(shí)間(再次使用“-”)。默認(rèn)情況下它是啟用的。啟用后,函數(shù)的時(shí)間包括函數(shù)內(nèi)調(diào)用的所有函數(shù)的時(shí)間。從上面示例的輸出中可以看出,列出了幾個(gè)系統(tǒng)調(diào)用的平均值最高。禁用時(shí),次數(shù)只包括函數(shù)本身的執(zhí)行次數(shù),不包括從函數(shù)調(diào)用函數(shù)的次數(shù):
1 | [tracing]# echo 0 > options/graph-time |
請注意,睡眠時(shí)間和圖形時(shí)間也會(huì)影響 function_graph 跟蹤器顯示的持續(xù)時(shí)間。
19. 總結(jié)
函數(shù)跟蹤器非常強(qiáng)大,有很多不同的選項(xiàng)。它已經(jīng)在主線 Linux 中可用,并且希望在大多數(shù)發(fā)行版中默認(rèn)啟用。它允許您深入了解內(nèi)核及其功能庫,讓您很好地了解事情發(fā)生的原因。開始使用函數(shù)跟蹤器打開我們稱之為內(nèi)核的黑匣子。玩得開心!
原文:
https://carlyleliu.github.io/2021/Linux%E5%86%85%E6%A0%B8%E8%B0%83%E8%AF%95%EF%BC%88%E4%B8%80%EF%BC%89ftrace/
