# Tracing with Ftrace > 資料來源:NCKU 黃敬群(Jserv)教授編撰之 Demystifying the Linux CPU Scheduler。 「核心除錯」即使對於經驗豐富的核心開發者來說也是一個重大挑戰。 例如,如果系統出現延遲或同步問題(如 undetected Race conditions),很難精確找到問題的根源-哪些子系統涉及其中?在什麼條件下問題會出現? Ftrace 是專門為了解決這類問題而設計的除錯工具,不僅能用來觀察核心發生的事情,還能透過「觀察函式的運作流程」來接近原始碼。 Ftrace 的名稱來自「函式追蹤器(function tracer)」,這是其多個功能中的一個。每種追蹤模式都稱為追蹤器(tracer),每個追蹤器都帶有許多可以調整的選項。它們可以進行函式追蹤、事件追蹤,測量 Context-switch 的時間,或測量中斷被禁用的時間;Ftrace 也非常具有擴展性,因為可以像模組一樣添加新的追蹤器。 正如預期的那樣,排程相關事件的記錄是本書的目標之一。要了解這些事件的作用及其重要性,必須理解 Ftrace,因為它是使用這些事件的工具。掌握排程相關事件的功能和價值,需要先理解 Ftrace 這個工具。 ## How does it work? ### Code Instrumentation 追蹤(Tracing)是指記錄運行時發生的事件,以便分析程式碼行為和性能。 > 更一般來說,這被稱為 *Software Profiling*,它可以通過不同的技術實現。 在我們的案例中,這是透過 *Code Instrumentation* 來實現的,這包括向原始碼或其二進制檔中「添加指令」,以進行剖析。 使用 Ftrace 有兩種主要方式,每種方式使用不同的 *Instrumentation* 技術: - **Function tracing**:使用由 gcc 提供的 code instrumentation 機制,通過使用 `-pg` 編譯標誌來啟用。 - **Event tracing**:使用原始碼中的 *tracepoints*。 ### Function Tracing: Runtime Injection 函式追蹤使用的是一種 dynamic profiling 形式:**Tracing instructions** 可以在程式執行時,於「二進制可執行檔」中切換,而無需重新編譯程式碼。 其工作方式如下:在編譯時,gcc 會在每個函式的開頭加入**額外的 NOP** 組合語言指令。 這些指令的位置會被保存於「二進制檔」中,以便隨時將這些 NOP 指令**替換成其他指令**。 ftrace 即透過在運行時**切換這些指令**來「開啟或關閉追蹤」。 - 開啟追蹤:將這些 NOP 指令轉換為 JMP 指令,以便跳轉至追蹤函式。 - 關閉追蹤:把 JMP 改回 NOP。 這種儀器化技術被稱為 *Runtime Injection*,有兩大優點: - 當追蹤被禁用時(99% 的情況下),幾乎沒有任何額外負擔。 - 可以篩選被追蹤的對象。 > 例如,可以動態地僅啟動單一子系統中的函式追蹤,或者僅追蹤某個特定函式。 ### Event Tracing: Tracepoints 事件追蹤的方式與函式追蹤有所不同,且其效率較低,因其不使用 Runtime Injection 技術。 相反,事件追蹤使用 C 語言中的 **Tracepoints**,因此是靜態的。 Tracepoints 基本上是對追蹤函式的直接呼叫,該函式會透過參數收集一些資訊,並將其與事件名稱一起寫入 Trace output。 由於這種機制是靜態的,因此需要**重新編譯整個核心**來切換 Tracepoints- 透過在編譯核心前,切換 `CONFIG_TRACEPOINTS` 巨集來完成。 > 以兩個排程事件為例:`sched_stat_runtime` 和 `sched_migrate_task`。 > 前者發生在「排程程式碼的特定位置」,並包含關於**某個行程的核心排程資訊**; > 後者發生在「任務遷移」時,並包含**該執行緒被遷移到的 CPU** 等資訊。 可以如下程式碼來呼叫: ```c // curtask and curr are task_structs, delta_exec is the difference in runtime // since the last timer interrupt. trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime); // p is a task_struct, new_cpu is the CPU the thread migrated to trace_sched_migrate_task(p, new_cpu); ``` 當這些事件發生後,會產生如下之追蹤輸出: ![圖片](https://hackmd.io/_uploads/HyEWarIyJe.png) > 從此 Trace output 可得知一些資訊(可直接從 `tast_struct` 獲得):**命令**、**pid**、**運行時間**、**優先級**。 ### Ring Buffer & Filtering 那麼,這些追蹤函式到底做了什麼? 這些函式會將新的事件寫入 buffer,Ftrace 即使用 **Ring Buffer** 來儲存所有在運行時發生的事件。基本上,**生產者**是核心,而**消費者**是使用者,使用者會從使用者空間中讀取資料。 **Entries losing**: - 由於這是一個 Circular Buffer,如果事件沒有及時被讀取,所有舊的 entries 都會被覆寫(這在事件追蹤時經常發生,因為自開機以來,事件就持續寫入 buffer,即使沒有人在讀取)。 - 另一種 losing entries 的情況是,寫入的速度比讀取的速度快:這在我們「不使用任何 filter 追蹤每一個函式或事件」時很常見。 **Filtering**: 通常,可以盡量多做 filter,以避免 losing entries,這在動態追蹤中是可以實現的,但靜態追蹤則無法。確實,我們可以輕鬆篩選追蹤輸出,但在靜態追蹤中,entries 無論如何都會寫入 buffer,而導致覆寫-這就是為什麼**函式追蹤中有真正的篩選功能,而事件追蹤中則沒有**。 ## Enable the kernel tracing infrastructure 為了進行與「核心追蹤」有關的實驗,必須在 Linux 核心中啟用 ftrace 模組。可以透過檢查「包含核心配置資訊的檔案」來確認某個 Linux 發行版是否具備 ftrace 功能。 ```shell # For Ubuntu/Debian GNU/Linux cat /boot/config-$(uname -r) | grep CONFIG_HAVE_FUNCTION_TRACER ``` 在某些發行版中,核心配置檔案可能不會出現在 `/boot` 目錄下。在這種情況下,如果核心配置中啟用了 `CONFIG_IKCONFIG` 和 `CONFIG_IKCONFIG_PROC`,可以使用以下指令檢查是否具備 ftrace 功能: ```shell # View compressed file contents zcat /proc/config.gz # Alternative path cat /lib/modules/$(uname -r)/build/.config ``` 如果要從頭編譯具有 ftrace 功能的核心,可以在核心原始碼目錄中,執行 `make menuconfig`。 進入 **Kernel hacking > Tracers** 子目錄,然後使用 **Enter** 鍵選擇 **Kernel Function Tracer** 以及其他必要的 ftrace 設定。最後,儲存設定並編譯核心。 ![圖片](https://hackmd.io/_uploads/rkY9GLIJJg.png =80%x) > Select Kernel Function Tracer option to enable ftrace in kernel. ## Interfacing with ftrace 追蹤用來監控「頻繁發生的事件」,故需要一種極其輕量的機制。 ftrace 提供了這種可能性,因為其是 self-contained 的,且完全在核心中實現,不需要任何使用者空間的工具。正如先前所述,ftrace 的輸出來自核心,而在使用者空間中被讀取。 那麼,如何在沒有專門程式的情況下讀取它呢? ### `tracefs` `tracefs` 是一種專門的檔案系統,位於另一個檔案系統 `sysfs` 中,包含與 ftrace 互動的檔案和目錄。此檔案系統可透過執行 `mount -t tracefs nodev /sys/kernel/tracing/` 來掛載;由於沒有實際的裝置被掛載,因此使用 *nodev* 作為目標裝置,並以 `/sys/kernel/tracing/` 作為目標掛載點。 ### `debugfs` 在 Linux v4.1 之前,所有 ftrace 的追蹤控制檔案都位於 `debugfs` 檔案系統中,該檔案系統通常位於 `/sys/kernel/debug/tracing`。`tracefs` 會自動掛載到 `/sys/kernel/debug/tracing`,並且為了向後相容,其也會掛載到 `/sys/kernel/debug/`。 ### Interfacing 我們可以僅使用 `echo` 和 `cat` 來與 ftrace 互動,這些工具可以在輕量或嵌入式 Linux 發行版中找到。只需在這些檔案中寫入 `echo your_value > file`,即可在追蹤前或追蹤過程中,切換選項並設定參數。 #### Trace Directory 以下是位於該檔案系統中的追蹤目錄。 ```shell # ls /sys/kernel/tracing available_events max_graph_depth stack_trace_filter available_filter_functions options synthetic_events available_tracers per_cpu timestamp_mode buffer_percent printk_formats trace buffer_size_kb events trace_clock ... ``` 其中一些檔案的用途不是用來設定選項,而是列出可用的選項。 例如,**可用的追蹤器**可以如下列出: ```shell # cat /sys/kernel/tracing/available_tracers hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop ``` 這些基本上是不同的追蹤模式:我們可以通過執行 `echo function > current_tracer` 來啟動 `function` 追蹤器;而要禁用追蹤,只需執行 `echo nop > current_tracer`。 #### Trace Output 有兩個檔案可以用來**讀取追蹤輸出**:`trace` 和 `trace_pipe`。 - `cat trace`:是 nonconsuming read,當我們在暫停追蹤後,讀取 `trace` 時,會反覆獲得相同的結果。 - `cat trace_pipe`:是一種與 ftrace 的單向溝通方式,追蹤資料會不斷被消耗並寫入終端機;而當沒有更多資料時,該 pipe 會阻塞。 其他大多數檔案則用於過濾「正在追蹤的內容」。 #### Interfacing Example 現在讓我們看看與 ftrace 互動的不同方式: ```bash # Interfacing through an application program sudo trace-cmd record -p function -P 622 sudo trace-cmd report # Interfacing through the filesystem cd /sys/kernel/debug/tracing echo 622 > set_ftrace_pid echo function > current_tracer cat trace ``` > 此為展示「從 User space 與 Kernel space 之間」不同溝通方式的好例子。 在這段程式碼中,兩種方法都追蹤了 PID 為 622 的行程,其本質上是以相同的方式進行,因為 `trace-cmd` 只是查詢了 `tracefs`,就像 `ps` 查詢 `procfs` 一樣。而第二種方法(我們會使用的),明確顯示了如何與核心進行互動,但在實踐中,有時使用像 `trace-cmd` 這樣的工具更方便。 另一種互動工具是 `kernelshark`,其有一 GUI 可顯示「透過 `trace-cmd` 完成」的追蹤圖表。 ## Ftrace usage ### Function Tracing #### Script 寫一份簡單的 script 如下,可輸入任何行程並追蹤: ```bash # !/bin/bash # trace_process.sh TRACE_DIR=/sys/kernel/debug/tracing echo $$ > $TRACE_DIR/set_ftrace_pid # echo every function to filter echo handle_mm_fault > $TRACE_DIR/set_ftrace_filter echo function > $TRACE_DIR/current_tracer exec $* ``` > `$$` 是變數,包含腳本本身的 PID; > `$*` 則是腳本的參數,在此舉例中,是「要追蹤的行程」。 運作方式如下: 1. 開始追蹤當前行程的函式呼叫,透過將當前的 PID `$$` 寫入 `set_ftrace_pid`。 2. 透過 `set_ftrace_filter` 過濾函式,設定只追蹤 `handle_mm_fault`。 3. 將追蹤器設置為函式追蹤器。 4. 執行輸入的程式。 5. 由於行程 `$*` 是腳本的子行程,其會自動被添加到 `set_ftrace_pid` 並繼續被追蹤。 #### Filtering the Function 當核心中的中斷處理程式,因「Page faults」被呼叫時,通常會先呼叫函式 `do_page_fault`(在 x86 架構中則是 `exc_page_fault`)。 未經過濾的輸出會顯示輸入行程所呼叫的「每一個核心函式」,這對我們的需求來說通常是無關且缺乏資訊的。為了過濾掉特定的函式,我們可以先在 `available_filter_functions` 中搜尋其名稱,以檢查該函式是否可用。如果可用,我們就可以將其名稱寫入 `set_ftrace_filter` 進行過濾。 追蹤輸出可以在 `/sys/kernel/debug/tracing/trace` 檔案中找到,或者可以實時查看 `/sys/kernel/debug/tracing/trace_pipe` 的輸出。 以下是使用 `./trace_process.sh ls` 來追蹤 `ls` 的輸出範例: ![圖片](https://hackmd.io/_uploads/Hyzt5IDyyx.png =80%x) 如預期的,我們只看到了 Page faults(共 92 次)。這些資訊本身並不太有用,但時間戳記卻相當有用;透過時間戳記,可以輕易檢測核心中的延遲。另外,使用 **kernelshark** 可以畫出追蹤資料圖,讓延遲顯而易見;如此,可以看到「哪些操作造成最多的額外負擔」。 #### `function_graph` 另一種僅使用 ftrace 的方法是使用 **function_graph** 追蹤器:它類似於 function 追蹤器,但會「顯示每個函數的進入和退出點」,從而建立函式呼叫圖。 與顯示時間戳不同,**function_graph** 會顯示每個函式執行的持續時間。符號 `+`, `!`, `#` 用來表示執行時間大於 10, 100 和 1000 µs 的情況。 如我們所知,「排程」和「執行緒遷移」會帶來許多額外負擔,因此我們可以嘗試使用 **function_graph** 來觀察這些情況。 #### Tracing Example & Entry Point Missing ```bash #!/bin/bash # graph_funct.sh TRACE_DIR=/sys/kernel/debug/tracing echo > $TRACE_DIR/trace echo $$ > $TRACE_DIR/set_ftrace_pid echo > $TRACE_DIR/set_ftrace_filter echo function_graph > $TRACE_DIR/current_tracer echo schedule > $TRACE_DIR/set_graph_function echo nofuncgraph-tail > $TRACE_DIR/trace_options exec $* ``` ![圖片](https://hackmd.io/_uploads/r1sVhIDyye.png =90%x) ![圖片](https://hackmd.io/_uploads/rJ4r28wykx.png =90%x) 此為針對「某完整函式」呼叫追蹤的片段,未設置任何函式或行程過濾器,並且已清除 `funcgraph-tail` 選項,以避免與孤立的退出點混淆。 每個葉節點函式及函式退出點(`}`)處會顯示函式持續時間;能發現,`schedule()` 的執行時間比其他函式長,並且**有 `#` 符號,因為其持續時間超過了 1000µs**。 在某些情況下,我們可能會遇到**只看到退出點(`}`),但追蹤中找不到其進入點**的情況。 這種問題通常發生在: - buffer 達到容量上限時; - 當函式在追蹤行程開始後,才開始執行,因此其進入點未被捕獲。 > 這是在「未經過濾的追蹤」中常見的情況,這正是我們在此所進行的操作。 > 如 Ftrace 文件所述:The function name is always displayed after the closing bracket for a function if the start of that function is not in the trace buffer. 在我們的例子中,這表示退出點「`} /* schedule */`」並**不指向初始的 `schedule()` 進入點**!即使我們能看到該函式的開銷,但實際的進入點並不在追蹤中,因為它未能被寫入。 #### Dealing with Entry Missing 為了減少這種情況,我們可以「只在單個 CPU 上進行追蹤」,而不是所有 4 個 CPU。 這種方法有三個優勢: - 輸出結果不會有跨 CPU 的函式呼叫交錯,這樣函式呼叫的流程不會被打斷。 - 由於追蹤的 entries 變少,buffer 不會被填滿,也就不會丟失太多 entries。 - 性能提升:追蹤每一個函式呼叫會產生顯著的開銷。 通常,最好**將過濾範圍盡量縮小**。例如,僅追蹤感興趣的函式,並且只在單個 CPU 上進行追蹤會是個好方法。在第 7 章中,我們將始終以這種方式進行追蹤,以減少雜訊。 #### Tracing `schedule()` 讓我們嘗試只追蹤 `schedule()` 函式,並在所有 CPU 上進行追蹤,看看它可能需要多少時間。 我們可以通過執行以下命令來完成這個操作: ```bash # !/bin/bash # trace_schedule.sh TRACE_DIR=/sys/kernel/debug/tracing echo > $TRACE_DIR/trace echo $$ > $TRACE_DIR/set_ftrace_pid echo schedule > $TRACE_DIR/set_ftrace_filter echo function_graph > $TRACE_DIR/current_tracer echo schedule > $TRACE_DIR/set_graph_function exec $* ``` 輸出如下: ![圖片](https://hackmd.io/_uploads/SyX2lvwkyg.png =40%x) 輸出的結果並不是平均時間,而是未經排序的混合,且以記錄到的最長時間為主。原因在於我們「僅查找了有註解的退出點」,因此這些其實只是那些「在追蹤中沒有對應進入點的退出點」(如 ftrace 文件所述)。 有些情況下,執行時間超過了 1 秒(`$`),這些是極端情況(呼叫被中斷,並且在此期間進行了其他核心工作),因此實際上並非僅「呼叫」就花費了整整 1 秒。 如果我們查找所有的退出點(而不僅僅是「無對應進入點的退出點」),我們可以看到,平均而言,`schedule()` routine 所需的時間會更短,但仍然比其他函式花費更多時間。 這份追蹤還有另一個問題。 ftrace 的創作者 Steven Rostedt 在其一篇文章中提到: > **Only the leaf functions, the ones that do not call other functions, have an accurate duration**, since the duration of parent functions also includes the overhead of the `function_graph` tracer calling the child functions. 這表示,「只計算進入點和退出點之間的時間差」是不夠的,因為 ftrace 的額外開銷並未被考慮進去。 文章中還提到: > **By using the `set_ftrace_filter` file, you can force any function into becoming a leaf function in the `function_graph` tracer**, and this will allow you to see an accurate duration of that function. 如果我們這樣做,會更準確地,發現大部分時間內 `schedule()` 的執行時間在 20µs 到 400µs 之間。 ### Event Tracing #### Event Directory Function tracing 非常有用,有助於理解程式碼,但我們現在將重點放在「事件」上。 你可能已經注意到在 `/sys/kernel/tracing` 的目錄列表中,有一個名為 `events` 的目錄。此目錄包含「每個事件子系統」的目錄,而「排程子系統」目錄則位於 `/sys/kernel/tracing/events/sched`。 ![圖片](https://hackmd.io/_uploads/HyYcmDwJkl.png) > Every event associated with scheduling 上圖顯示了其內容:每個事件都有一個目錄,其中包含相關資訊和一個用來啟用/停用的開關。這基本上是一個我們將要記錄的事件列表,儘管其中一些名稱幾乎是不言自明的。 #### `printk()` v.s. `trace_printk()` 那麼,事件到底是什麼呢? 如前所述,事件追蹤比函式追蹤更加靜態。表示「事件追蹤點是直接嵌入在程式碼中」的,並且以類似函式的方式呼叫。它們無法在執行時動態切換,若要修改或禁用它們,則需要重新編譯整個核心。 > 在這方面,事件更像是常見的 print 語述,儘管在實際操作中,它們比 `printk()` 更加高效。 Steven Rostedt 對此解釋得相當清楚,說明了為什麼會出現這種情況: > `printk()` is the king of all debuggers, but it has a problem. If you are debugging a high volume area such as the timer interrupt, the scheduler, or the network, `printk()` can lead to bogging down the system or can even create a live lock. > > It is also quite common to see a bug “disappear” when adding a few `printk()`s. This is due to the sheer overhead that `printk()` introduces. > > Ftrace introduces a new form of `printk()` called `trace_printk()`. It can be used just like `printk()`, and can also be used in any context (interrupt code, NMI code, and scheduler code). What is nice about `trace_printk()` is that it does not output to the console. Instead it writes to the Ftrace ring buffer and can be read via the trace file. > > Writing into the ring buffer with `trace_printk()` only takes around a tenth of a microsecond or so. But using `printk()`, especially when writing to the serial console, may take several milliseconds per write. `trace_printk()` 只是在追蹤 buffer 中寫入一則訊息,這與事件的運作方式完全相同,只是事件使用了「預先定義的格式」和「多個 print 欄位」。因此,Steven Rostedt 的描述同樣適用於事件。 #### Event Format ![圖片](https://hackmd.io/_uploads/rJ3jXvvk1g.png) > Control files for the `sched_switch` event ![圖片](https://hackmd.io/_uploads/B1hjLDvy1e.png) > Fields and print format of the `sched_switch` event 如上圖,可以看到事件與 print 的相似之處。 每個事件都有一個格式檔案,該檔案指出欄位和 print 格式,語法與 `printk()` 相同。 #### Example of Event Tracing & Filtering 現在來看看如何啟用事件追蹤以及過濾事件。 事件與任何追蹤器都無關,因為追蹤器僅用於動態追蹤。如果只想查看事件,必須使用 **nop 追蹤器**(它不追蹤任何內容),但也可以在追蹤函式的同時,啟用其他追蹤器來追蹤事件。 ```bash TRACE_DIR=/sys/kernel/debug/tracing # enable scheduling events echo nop > $TRACE_DIR/current_tracer echo 1 > $TRACE_DIR/events/sched/enable # enable just the sched_switch event echo nop > $TRACE_DIR/current_tracer echo 1 > $TRACE_DIR/events/sched/sched_switch/enable ``` 每個事件目錄樹的目錄中都有一個 `enable` 檔案。 從目錄結構中可以看到,該層次結構用來切換單一事件、整個事件子系統,或所有現有的事件。 > 需要注意的是,這種過濾器並**不會阻止事件被寫入追蹤 buffer**,只是忽略了它們。 所謂的「必須重新編譯整個核心來禁用特定事件」,實際上可以解釋為「必須重新編譯整個核心才能**防止 ftrace 將特定事件寫入 buffer,即使這些事件在 `tracefs` 中已被禁用**」。 #### Example of Tracing Output & Switching 以下是一小段「每個排程事件」的追蹤資料: ![圖片](https://hackmd.io/_uploads/BJzsKPw1kg.png) 這段追蹤涉及到「**交換行程**(`pid 0`)」和一個名為「Xorg(`pid 1450`)的應用程式」,Xorg 是系統的顯示伺服器(本質上是 GUI)。 從換掉**交換行程**以執行 **Xorg 行程**,到 Xorg 消耗其 CPU 時間,然後**排程行程**再次切換回**交換行程**,整個過程僅需 143µs。 完成以下任務共耗時 143µs: 1. **交換行程**被切換出去 2. **排程行程**選擇 **Xorg 行程**執行 3. **Xorg** 消耗其 CPU 時間 4. **排程行程**切換回**交換行程** #### `sched_switch` & `sched_stat_runtime` `sched_switch` 和 `sched_stat_runtime` 是最常見的排程事件。 它們相應的追蹤點,在程式碼中如下所示: ```c trace_sched_switch(preempt, prev, next); trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime); ``` `sched_switch` 會在每次「行程切換」時報告,並 print - 有關**舊行程**的資訊,這些資訊是從 `prev` 參數中提取的; - 有關**新行程**的訊息,這些訊息則來自 `next`。 而 `sched_stat_runtime` print「正在運行的行程」的核心排程訊息,例如 pid、實際運行時間和虛擬運行時間。這些訊息同樣是從追蹤點的參數中提取的。 ## Creating new events 追蹤點是在核心中建立的。在這個層面上,事件被視為 **「攜帶事件所需訊息」的結構**。追蹤點必須建立這個結構,填充資料,並將其寫入 ftrace Ring buffer。 因此,這個 buffer 基本上是一個**二進制資料的陣列**,會定期被消耗、解碼並 print 為 Trace output 中的字串。 除了 ftrace 的核心基礎設施外,還有一些介面來**簡化建立追蹤點的過程**。 如今,這個過程幾乎可以完全自動化,這得益於 `TRACE_EVENT()` 巨集和其他由 `TRACE_EVENT()` 呼叫的巨集。使用 `TRACE_EVENT()` 可以快速在核心模組;或甚至在 core kernel code 中建立追蹤點,而無需大量樣板程式碼。 所有現有的**排程事件追蹤點**都是使用 `TRACE_EVENT()` 建立的:這些追蹤點在一「單獨且專用的標頭檔」中宣告,然後包含在主程式碼中。 這些標頭檔的預設路徑是 `include/trace/events`,因此**排程事件**的宣告位於 [`include/trace/events/sched.h`](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/trace/events/sched.h) 中。 #### Header File 讓我們看看這個標頭檔中包含了什麼**內容**。 此程式碼片段是 `sched_migrate_task` 的宣告: > 整個檔案都是「類似 `sched_migrate_task` 事件」的宣告列表。 ```c #undef TRACE_SYSTEM #define TRACE_SYSTEM sched #if !defined(_TRACE_SCHED_H) || \ defined(TRACE_HEADER_MULTI_READ) // Special guard #define _TRACE_SCHED_H // ... some other includes ... #include <linux/tracepoint.h> //TRACE_EVENT() defined in here, then redefined at the end of this trace header /* * Tracepoint for a task being migrated: */ TRACE_EVENT(sched_migrate_task, TP_PROTO(struct task_struct *p, int dest_cpu), TP_ARGS(p, dest_cpu), TP_STRUCT__entry(__array(char, comm, TASK_COMM_LEN) __field(pid_t, pid) __field(int, prio) __field(int, orig_cpu) __field(int, dest_cpu)), TP_fast_assign(memcpy(__entry->comm, p->comm, TASK_COMM_LEN); __entry->pid = p->pid; __entry->prio = p->prio; /* XXX SCHED_DEADLINE */ __entry->orig_cpu = task_cpu(p); __entry->dest_cpu = dest_cpu;), TP_printk("comm=%s pid=%d prio=%d orig_cpu=%d dest_cpu=%d", __entry->comm, __entry->pid, __entry->prio, __entry->orig_cpu, __entry->dest_cpu)); // ... many more event declarations ... #endif /* _TRACE_SCHED_H */ /* This part must be outside protection */ #include <trace/define_trace.h> ``` > 註:`#include` 的存在,和「檔案末的特殊保護」與「`TRACE_EVENT()` 的內部運作」有關(後面會提到)。 該巨集有 6 個參數和 5 個不同的子巨集: 1. `trace_NAME`:即首個參數,為**追蹤點名稱**。 2. `TP_PROTO` & `TP_ARGS`:**追蹤點的參數**。 3. `TP_STRUCT__entry`:**事件的結構**,包含每個屬性的類型和名稱。 > 此處有兩個不同的子子巨集來定義 fields 和陣列。 5. `TP_fast_assign`:**如何填充事件結構**,通常使用事件參數來填充。 > 這並不簡單,因為可以使用其他函式、巨集或 `memcpy()`、`memmove()` 等來填充它們。 7. `TP_printk`:具有與 `printk()` 相同的語法,定義了**如何在追蹤輸出中,以人類可讀的形式 print 結構**。 > 在本節開頭的例子中,可見 `sched_migrate_task` 就是如此 print 的。 使用 `TP_printk` 定義的格式可以從「使用者空間在事件目錄的 `format` 子目錄」中查看。`TRACE_SYSTEM` 是事件的追蹤系統,這會在事件目錄中,產生相應的目錄,並用於這些事件。 如下圖,可以看到在標頭中宣告的 `sched` 目錄: ![圖片](https://hackmd.io/_uploads/rJy54ODkJl.png) > All of the systems in which events are subdivided ### Kernel module to test tracepoints 讓我們來建立自己的**核心模組**,這個模組將執行一些「可以從核心外部追蹤」的操作。 #### *doSomething.c* & *my_module_trace.h* ```c= #include <linux/module.h> #include <linux/kthread.h> #define CREATE_TRACE_POINTS #include "my_module_trace.h" static int do_stuff(void *arg) { struct task_struct *p = current; u64 count = 0; printk(KERN_INFO "Current process is %s with pid %d\n", current->comm, current->pid); while (!kthread_should_stop()) { set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(HZ); printk("hi! %llu\n", count); count++; trace_my_event(p, jiffies); // Tracepoint p = next_task(p); } return 0; } static struct task_struct *my_tsk; static int __init init_func(void) { u32 i = 0, j = 0; struct task_struct *p, *t; printk(KERN_INFO "Hello world\n"); for_each_process(p) { i++; for_each_thread(p, t) { j++; } } printk(KERN_INFO "There are %d processes\n", i); printk(KERN_INFO "There are %d threads\n", j); // printk(KERN_INFO "Average threads per process: %s\n", division_hack(j, i)); printk(KERN_INFO "Current process is %s with pid %d\n", current->comm, current->pid); my_tsk = kthread_run(do_stuff, NULL, "my-kthread"); if (IS_ERR(my_tsk)) return -1; return 0; } static void __exit exit_func(void) { kthread_stop(my_tsk); printk(KERN_INFO "Goodbye world\n"); } module_init(init_func); module_exit(exit_func); MODULE_LICENSE("GPL"); ``` 以下是模組中包含的**追蹤標頭** `my_module_trace.h`: ```c= #undef TRACE_SYSTEM #define TRACE_SYSTEM my_system #if !defined(_MY_MODULE_TRACE_H) || defined(TRACE_HEADER_MULTI_READ) // Special guard #define _MY_MODULE_TRACE_H #include <linux/tracepoint.h> // TRACE_EVENT() is defined here TRACE_EVENT(my_event, TP_PROTO(struct task_struct *t, unsigned long ticks), TP_ARGS(t, ticks), TP_STRUCT__entry( __array(char, name, TASK_COMM_LEN) __field(pid_t, pid) __field(u64, vruntime) __field(unsigned long, ticks) ), TP_fast_assign( memcpy(__entry->name, t->comm, TASK_COMM_LEN); __entry->pid = t->pid; __entry->vruntime = t->se.vruntime; __entry->ticks = ticks; ), TP_printk("name=%s pid=%d vruntime=%lli ticks=%li", __entry->name, __entry->pid, __entry->vruntime, __entry->ticks) ); #endif /* _MY_MODULE_TRACE_H */ /* This part must be outside protection */ #undef TRACE_INCLUDE_PATH #define TRACE_INCLUDE_PATH . #define TRACE_INCLUDE_FILE my_module_trace #include <trace/define_trace.h> ``` #### Compile & Insert & Print 我們可以 - **Compile** module:透過 link 系統中「當前運行的核心標頭」來編譯模組。 > 這些標頭,可以在使用者空間中的 `/lib/modules/your-kernel-version/build/include` 找到。 - **Insert** module:`sudo insmod my_module.ko`。 - **Print** kernel log:`sudo dmesg`(在這裡可以看到我們的 `printk()` 輸出)。 ![圖片](https://hackmd.io/_uploads/Hy0dflY11l.png =70%x) #### Explanation of Code 我們基本上使用了核心 API,這些 API 於包含的標頭檔中定義。 `for_each_process()`、`for_each_thread()` 和 `current` 都是巨集,也在排程程式碼中使用。 `init_func()` 是模組插入時執行的初始化函式,因此當我們第一次輸出當前執行的程式時,會讀取到 `insmod`。 接著我們產生一個**核心執行緒**,該執行緒進入休眠狀態,並在 HZ 週期(1 秒)後喚醒;然後其輸出 `hi!` 並觸發一個事件,再次進入休眠。 我們用 `sudo rmmod my_module` 移除模組,這樣 `exit_func()` 就會執行,**核心執行緒**被終止,模組被移除。 #### If Bugs in Kernel 如前所述,核心中的錯誤,可能會導致系統遭鎖定,最糟糕的情況還會損壞資料:這就是為什麼核心模組通常在虛擬機中測試的原因,Core kernel 程式碼也同樣適用。 例如,在我們的模組中,第 11 行和第 12 行非常重要:如果我們註解掉這兩行,系統在模組插入後幾秒鐘內完全凍結,然後崩潰。 發生的情況是,該執行緒佔用了 CPU,導致其他行程無法獲取資源:由於它是**核心執行緒**,因此不會被搶占。雖然**核心執行緒**像普通任務一樣被排程,但如果它們不自願釋放 CPU,那麼它們幾乎可以無限執行,因其優先權極高。 在佔用 CPU 期間,Kernel log 中可能會讀到如下訊息: ![圖片](https://hackmd.io/_uploads/BJFWDeFJJl.png =90%x) #### Event Trace Output 由於使用了 `TRACE_EVENT()`,現有一專屬於我們追蹤系統的目錄。 我們可以通過在 `tracefs` 中,執行 `echo 1 > trace/events/my_system/enable` 來追蹤我們的事件。 以下是 **Trace Output**: ![圖片](https://hackmd.io/_uploads/r194DlKJkx.png =78%x) ![圖片](https://hackmd.io/_uploads/ryL8DlKk1g.png) 其會每秒走訪一次「行程列表」,並**輸出有關行程的資訊**。 > 首先會追蹤**核心執行緒**(較低的 pid),最終找到使用者行程。 ### Overview of `TRACE_EVENT()` infrastructure 此巨集,會展開許多其他子巨集,以便運作。 其作用為「產生 `struct`」,並「產生追蹤點函式的程式碼」,該函式基本上會將事件寫入 trace ring buffer。為了實現這一點,此巨集使用了一種 **C 前處理器** 的技巧,允許在「使用相同輸入資料」的情況下,改變其行為。 #### Example of C Pre-processor 以下是一個來自 Steven Rostedt 文章的完美範例: ```c #define DOGS \ { \ C(JACK_RUSSELL), C(BULL_TERRIER), C(ITALIAN_GREYHOUND) \ } #undef C #define C(a) ENUM_##a enum dog_enums DOGS; #undef C #define C(a) #a char *dog_strings[] = DOGS; char *dog_to_string(enum dog_enums dog) { return dog_strings[dog]; } ``` 透過在程式中**重新定義子巨集 `C(a)`**,以改變 `DOGS` 的行為; 如此一來,就能夠使用相同的資料,產生不同的程式碼。 #### Macro Changing of `TRACE_EVENT()` `TRACE_EVENT()` 也是以相似的方式處理其參數,但這種技巧的使用方式相當獨特。 想像我們有兩個不同的標頭檔:*generate_code.h* 和 *change_behavior.h*: - *generate_code.h*: 1. 使用巨集,展開並以給定的輸入資料來產生程式碼。 2. 執行 `#include change_behavior.h`。 - *change_behavior.h*: 1. **重新定義子巨集**,改變原始巨集的行為。 2. 執行 `#include generate_code.h`,重新 include 剛剛包含過的標頭檔,以產生新的程式碼 3. 多次重複此過程。 雖然不易看出實際發生的事情,但這正是我們對 DOGS 所做的。 透過這種技巧,改變巨集的使用方式變得更加簡單,因為我們有兩個分開的檔案,但這樣的程式碼也變得難以閱讀。 #### Module of `TRACE_EVENT()` 接下來,我們將透過模組,來展示這個機制在「實際的核心程式碼」中的運作。 *my_module.c*: ```c #define CREATE_TRACE_POINTS #include "my_module_trace.h" ``` > `CREATE_TRACE_POINTS` 只有在核心編譯時「啟用了追蹤選項」時,才會被定義。 *my_module_trace.h*: ```c #if !defined(_MY_MODULE_TRACE_H) || \ defined(TRACE_HEADER_MULTI_READ) // Special guard #define _MY_MODULE_TRACE_H #include <linux/tracepoint.h> // TRACE_EVENT() is defined here TRACE_EVENT(my_event, TP_PROTO(struct task_struct *t, unsigned long ticks), TP_ARGS(t, ticks), TP_STRUCT__entry( __array(char, name, TASK_COMM_LEN) __field(pid_t, pid) __field(u64, vruntime) __field(unsigned long, ticks) ), TP_fast_assign( memcpy(__entry->name, t->comm, TASK_COMM_LEN); __entry->pid = t->pid; __entry->vruntime = t->se.vruntime; __entry->ticks = ticks; ), TP_printk("name=%s pid=%d vruntime=%lli ticks=%li", __entry->name, __entry->pid, __entry->vruntime, __entry->ticks) ); #endif /* _MY_MODULE_TRACE_H */ /* This part must be outside protection */ #undef TRACE_INCLUDE_PATH #define TRACE_INCLUDE_PATH . #define TRACE_INCLUDE_FILE my_module_trace #include <trace/define_trace.h> ``` 這是使用 `TRACE_EVENT()` 的檔案,特殊的保護機制使我們能夠多次 reinclude 它。 #### Special Guard 通常,保護機制用於**避免多次包含**。因為如果「函式宣告」被包含了兩次,將無法編譯,因為會有兩個有相同名稱的函式;而因為追蹤標頭中沒有函式,只有巨集,因此多次包含是完全安全的(在這種情況下也是必要的)。 作為參考,這是**保護機制**的常見用法: ```c #ifndef _MY_MODULE_TRACE_H #define _MY_MODULE_TRACE_H // ... Code ... #endif /* _MY_MODULE_TRACE_H */ ``` #### `TRACE_INCLUDE_FILE` & `TRACE_INCLUDE_PATH` 在檔案的最後,我們定義了兩個額外的巨集: - `TRACE_INCLUDE_FILE`:此檔案的名稱,稍後在「重新包含」時需要它。 - `TRACE_INCLUDE_PATH` :改變了追蹤標頭的路徑,此情況下,設置為 `.` 以表當前目錄。 > 這對於模組來說是必要的,因為它們從不使用 core kernel code 的預設路徑 (`include/trace/events/`)。 #### *define_trace.h* 這些資訊在重新包含時也很重要,「重新包含」是在最後包含的標頭 *define_trace.h* 中執行的(對應於前面提到的 *change_behavior.h*)。 *define_trace.h*: ```c= #ifdef CREATE_TRACE_POINTS // Defined to activate the tracepoints, used here as a guard /* Prevent recursion */ #undef CREATE_TRACE_POINTS // ... Redefine the sub-macros to change the behavior ... #ifndef TRACE_INCLUDE_FILE #define TRACE_INCLUDE_FILE TRACE_SYSTEM #define UNDEF_TRACE_INCLUDE_FILE #endif #ifndef TRACE_INCLUDE_PATH #define __TRACE_INCLUDE(system) \ <trace/events/system.h> // Reread system.h (e.g.: sched.h) trace header #define UNDEF_TRACE_INCLUDE_PATH #else #define __TRACE_INCLUDE(system) __stringify(TRACE_INCLUDE_PATH/system.h) #endif #define TRACE_INCLUDE(system) __TRACE_INCLUDE(system) /* Let the trace headers be reread */ #define TRACE_HEADER_MULTI_READ // Reinclusion: includes the file that just included it // e.g.: if the subsystem was sched, this just included <trace/events/sched.h>, in our example it is <./my_module_trace.h> #include TRACE_INCLUDE(TRACE_INCLUDE_FILE) #ifdef TRACEPOINTS_ENABLED #include <trace/trace_events.h> #endif // ... Undefine every single macro and sub-macro ... /* We may be processing more files */ #define CREATE_TRACE_POINTS #endif /* CREATE_TRACE_POINTS */ ``` - 1~4 行:在重新包含之前,重要的是要**取消定義 `CREATE_TRACE_POINTS`**,這樣可以啟用保護機制。這是因為重新包含的檔案(*my_module_trace.h*)可能會再次包含此檔案(*define_trace.h*),導致編譯時出現無限循環。 - 6~19 行:產生了「包含此檔案的檔案」的路徑,並從 `TRACE_SYSTEM`、`TRACE_INCLUDE_FILE` 和 `TRACE_INCLUDE_PATH` 中獲取資訊。最後,「生成路徑中的檔案」被重新包含,這導致 `TRACE_EVENT()` 產生不同的代碼。 - 28 行:最後,*trace_events.h* 被包含進來:這個標頭只是重複這個過程多次。 - 其程式碼具有以下一般結構: ```c // Stage 1: change behavior #include TRACE_INCLUDE(TRACE_INCLUDE_FILE) // Generate // Stage 2: change behavior #include TRACE_INCLUDE(TRACE_INCLUDE_FILE) // Generate // Stage 3: change behavior #include TRACE_INCLUDE(TRACE_INCLUDE_FILE) // Generate // ... ``` - 每個 Stage 僅僅是「重新定義子巨集,以生成特定目的」的程式碼。 - 這些 Stages 可能生成如下程式碼之範例: - Stage 1:生成具有正確 fields 的 event struct - Stage 2:生成 event struct 中「每個 field 的偏移量」struct - Stage 3:在 `tracefs` 的事件目錄中創建目錄 - Stage 4:生成「以追蹤輸出格式 print 原始事件」的函式 - Stage 5:生成「將 struct 寫入 ring buffer」的函式 就是這樣!這個檔案中的程式碼不難理解,只是難以閱讀。 且其程式碼量也很大,這也是為什麼我們不會詳細探討它。 對我們來說,這並不是特別有趣,但最有趣的是 `TRACE_EVENT()` 的結構(以及它的黑科技)。 對於好奇的人,該檔案的路徑是 [include/trace/trace_events.h](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/trace/trace_events.h)。