1. 程式人生 > >Android系統分析工具(二) ftrace

Android系統分析工具(二) ftrace

可以通過kernel原始碼中的Documentation/trace 目錄下的文件以及 kernel/trace 下的原始檔以瞭解其餘檔案的用途。

在配置linux核心時選擇

     Kernel hacking ---> Tracers

這樣在編譯核心的時候會加上-pg編譯選項。執行的時候是,系統會在 debugfs 下建立一個 tracing 目錄 /sys/kernel/debug/tracing

ftrace 的作用是幫助開發人員瞭解 Linux 核心的執行時行為,以便進行故障除錯或效能分析。

最早 ftrace 是一個 function tracer,僅能夠記錄核心的函式呼叫流程。如今 ftrace 已經成為一個 framework,採用 plugin 的方式支援開發人員新增更多種類的 trace 功能。

Ftrace 由 RedHat 的 Steve Rostedt 負責維護。到 2.6.30 為止,已經支援的 tracer 包括:

Function tracerFunction graph tracer: 跟蹤函式呼叫。

Schedule switch tracer: 跟蹤程序排程情況。

Wakeup tracer:跟蹤程序的排程延遲,即高優先順序程序從進入 ready 狀態到獲得 CPU 的延遲時間。該 tracer 只針對實時程序。

Irqsoff tracer:當中斷被禁止時,系統無法相應外部事件,比如鍵盤和滑鼠,時鐘也無法產生 tick 中斷。這意味著系統響應延遲,irqsoff 這個 tracer 能夠跟蹤並記錄核心中哪些函式禁止了中斷,對於其中中斷禁止時間最長的,irqsoff 將在 log 檔案的第一行標示出來,從而使開發人員可以迅速定位造成響應延遲的罪魁禍首。

Preemptoff tracer:和前一個 tracer 類似,preemptoff tracer 跟蹤並記錄禁止核心搶佔的函式,並清晰地顯示出禁止搶佔時間最長的核心函式。

Preemptirqsoff tracer: 同上,跟蹤和記錄禁止中斷或者禁止搶佔的核心函式,以及禁止時間最長的函式。

Branch tracer: 跟蹤核心程式中的 likely/unlikely 分支預測命中率情況。 Branch tracer 能夠記錄這些分支語句有多少次預測成功。從而為優化程式提供線索。

Hardware branch tracer:利用處理器的分支跟蹤能力,實現硬體級別的指令跳轉記錄。在 x86 上,主要利用了 BTS 這個特性。

Initcall tracer:記錄系統在 boot 階段所呼叫的 init call 。

Mmiotrace tracer:記錄 memory map IO 的相關資訊。

Power tracer:記錄系統電源管理相關的資訊。

Sysprof tracer:預設情況下,sysprof tracer 每隔 1 msec 對核心進行一次取樣,記錄函式呼叫和堆疊資訊。

Kernel memory tracer: 記憶體 tracer 主要用來跟蹤 slab allocator 的分配情況。包括 kfree,kmem_cache_alloc 等 API 的呼叫情況,使用者程式可以根據 tracer 收集到的資訊分析內部碎片情況,找出記憶體分配最頻繁的程式碼片斷,等等。

Workqueue statistical tracer:這是一個 statistic tracer,統計系統中所有的 workqueue 的工作情況,比如有多少個 work 被插入 workqueue,多少個已經被執行等。開發人員可以以此來決定具體的 workqueue 實現,比如是使用 single threaded workqueue 還是 per cpu workqueue.

Event tracer: 跟蹤系統事件,比如 timer,系統呼叫,中斷等。

這裡還沒有列出所有的 tracer,ftrace 是目前非常活躍的開發領域,新的 tracer 將不斷被加入核心。

Ftrace 最初是在 2.6.27 中出現的,那個時候,systemTap 已經開始嶄露頭角,其他的 trace 工具包括 LTTng 等也已經發展多年。那為什麼人們還要再開發一個 trace 工具呢?

SystemTap 專案是 Linux 社群對 SUN Dtrace 的反應,目標是達到甚至超越 Dtrace 。因此 SystemTap 設計比較複雜,Dtrace 作為 SUN 公司的一個專案開發了多年才最終穩定釋出,況且得到了 Solaris 核心中每個子系統開發人員的大力支援。 SystemTap 想要趕超 Dtrace,困難不僅是一樣,而且更大,因此她始終處在不斷完善自身的狀態下,在真正的產品環境,人們依然無法放心的使用她。不當的使用和 SystemTap 自身的不完善都有可能導致系統崩潰。

Ftrace 的設計目標簡單,本質上是一種靜態程式碼插裝技術,不需要支援某種程式設計介面讓使用者自定義 trace 行為。靜態程式碼插裝技術更加可靠,不會因為使用者的不當使用而導致核心崩潰。 ftrace 程式碼量很小,穩定可靠。實際上,即使是 Dtrace,大多數使用者也只使用其靜態 trace 功能。因此 ftrace 的設計非常務實。

從 2.6.30 開始,ftrace 支援 event tracer,其實現和功能與 LTTng 非常類似,或許將來 ftrace 會同 LTTng 進一步融合,各自取長補短。 ftrace 有定義良好的 ASCII 介面,可以直接閱讀,這對於核心開發人員非常具有吸引力,因為只需核心程式碼加上 cat 命令就可以工作了,相當方便; LTTng 則採用 binary 介面,更利於專門工具分析使用。此外他們內部 ring buffer 的實現不相同,ftrace 對所有 tracer 都採用同一個 ring buffer,而 LTTng 則使用各自不同的 ring buffer 。

目前,或許將來 LTTng 都只能是核心主分支之外的工具。她主要受到嵌入式工程師的歡迎,而核心開發人員則更喜歡 ftrace 。

Ftrace 的實現依賴於其他很多核心特性,比如 tracepoint[3],debugfs[2],kprobe[4],IRQ-Flags[5] 等。限於篇幅,關於這些技術的介紹請讀者自行查閱相關的參考資料。

ftrace 在核心態工作,使用者通過 debugfs 介面來控制和使用 ftrace 。從 2.6.30 開始,ftrace 支援兩大類 tracer:傳統 tracer 和 Non-Tracer Tracer 。下面將分別介紹他們的使用。

使用傳統的 ftrace 需要如下幾個步驟:

  • 選擇一種 tracer
  • 使能 ftrace
  • 執行需要 trace 的應用程式,比如需要跟蹤 ls,就執行 ls
  • 關閉 ftrace
  • 檢視 trace 檔案

使用者通過讀寫 debugfs 檔案系統中的控制檔案完成上述步驟。使用 debugfs,首先要掛載她。命令如下:

# mkdir /debug 
 # mount -t debugfs nodev /debug

此時您將在 /debug 目錄下看到 tracing 目錄。 Ftrace 的控制介面就是該目錄下的檔案。

選擇 tracer 的控制檔案叫作 current_tracer 。選擇 tracer 就是將 tracer 的名字寫入這個檔案,比如,使用者打算使用 function tracer,可輸入如下命令:

#echo ftrace > /debug/tracing/current_tracer

檔案 tracing_enabled 控制 ftrace 的開始和結束。

#echo 1 >/debug/tracing/tracing_enable

上面的命令使能 ftrace 。同樣,將 0 寫入 tracing_enable 檔案便可以停止 ftrace 。

ftrace 的輸出資訊主要儲存在 3 個檔案中。

  • Trace,該檔案儲存 ftrace 的輸出資訊,其內容可以直接閱讀。
  • latency_trace,儲存與 trace 相同的資訊,不過組織方式略有不同。主要為了使用者能方便地分析系統中有關延遲的資訊。
  • trace_pipe 是一個管道檔案,主要為了方便應用程式讀取 trace 內容。算是擴充套件介面吧。

下面詳細解析各種 tracer 的輸出資訊。

Function tracer 的輸出

Function tracer 跟蹤函式呼叫流程,其 trace 檔案格式如下:

# tracer: function 
 # 
 #  TASK-PID   CPU#    TIMESTAMP        FUNCTION 
 #   |  |       |          |                | 
  bash-4251  [01]  10152.583854:    path_put <-path_walk 
  bash-4251  [01] 10152.583855: dput <-path_put 
  bash-4251  [01] 10152.583855: _atomic_dec_and_lock <-dput

可以看到,tracer 檔案類似一張報表,前 4 行是表頭。第一行顯示當前 tracer 的型別。第三行是 header 。

對於 function tracer,該表將顯示 4 列資訊。首先是程序資訊,包括程序名和 PID ;第二列是 CPU,在 SMP 體系下,該列顯示核心函式具體在哪一個 CPU 上執行;第三列是時間戳;第四列是函式資訊,預設情況下,這裡將顯示核心函式名以及它的上一層呼叫函式。

通過對這張報表的解讀,使用者便可以獲得完整的核心執行時流程。這對於理解核心程式碼也有很大的幫助。有志於精讀核心程式碼的讀者,或許可以考慮考慮 ftrace 。

如上例所示,path_walk() 呼叫了 path_put 。此後 path_put 又呼叫了 dput,進而 dput 再呼叫 _atomic_dec_and_lock 。

Schedule switch tracer 的輸出

Schedule switch tracer 記錄系統中的程序切換資訊。在其輸出檔案 trace 中 , 輸出行的格式有兩種:

第一種表示程序切換資訊:

Context switches: 
       Previous task              Next Task 
  <pid>:<prio>:<state>  ==>  <pid>:<prio>:<state>

第二種表示程序 wakeup 的資訊:

	Wake ups: 
       Current task               Task waking up 
  <pid>:<prio>:<state>    +  <pid>:<prio>:<state>

這裡舉一個例項:

# tracer: sched_switch 
 # 
 #  TASK_PID   CPU#     TIMESTAMP             FUNCTION 
 #     |         |            |                  | 
   fon-6263  [000] 4154504638.932214:  6263:120:R +   2717:120:S 
   fon-6263  [000] 4154504638.932214:  6263:120:? ==> 2717:120:R 
   bash-2717 [000] 4154504638.932214:  2717:120:S +   2714:120:S

第一行表示程序 fon 程序 wakeup 了 bash 程序。其中 fon 程序的 pid 為 6263,優先順序為 120,程序狀態為 Ready 。她將程序 ID 為 2717 的 bash 程序喚醒。

第二行表示程序切換髮生,從 fon 切換到 bash 。

irqsoff tracer 輸出

有四個 tracer 記錄核心在某種狀態下最長的時延,irqsoff 記錄系統在哪裡關中斷的時間最長; preemptoff/preemptirqsoff 以及 wakeup 分別記錄禁止搶佔時間最長的函式,或者系統在哪裡排程延遲最長 (wakeup) 。這些 tracer 資訊對於實時應用具有很高的參考價值。

為了更好的表示延遲,ftrace 提供了和 trace 類似的 latency_trace 檔案。以 irqsoff 為例演示如何解讀該檔案的內容。

# tracer: irqsoff 
 irqsoff latency trace v1.1.5 on 2.6.26 
 -------------------------------------------------------------------- 
 latency: 12 us, #3/3, CPU#1 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:2) 
    ----------------- 
    | task: bash-3730 (uid:0 nice:0 policy:0 rt_prio:0) 
    ----------------- 
 => started at: sys_setpgid 
 => ended at:   sys_setpgid 
 #                _------=> CPU# 
 #               / _-----=> irqs-off 
 #              | / _----=> need-resched 
 #              || / _---=> hardirq/softirq 
 #              ||| / _--=> preempt-depth 
 #              |||| / 
 #              |||||     delay 
 #  cmd     pid ||||| time  |   caller 
 #     \   /    |||||   \   |   / 
    bash-3730  1d...    0us : _write_lock_irq (sys_setpgid) 
    bash-3730  1d..1    1us+: _write_unlock_irq (sys_setpgid) 
    bash-3730  1d..2   14us : trace_hardirqs_on (sys_setpgid)

在檔案的頭部,irqsoff tracer 記錄了中斷禁止時間最長的函式。在本例中,函式 trace_hardirqs_on 將中斷禁止了 12us 。

檔案中的每一行代表一次函式呼叫。 Cmd 代表程序名,pid 是程序 ID 。中間有 5 個字元,分別代表了 CPU#,irqs-off 等資訊,具體含義如下:

CPU# 表示 CPU ID ;

irqs-off 這個字元的含義如下:’ d ’表示中斷被 disabled 。’ . ’表示中斷沒有關閉;

need-resched 字元的含義:’ N ’表示 need_resched 被設定,’ . ’表示 need-reched 沒有被設定,中斷返回不會進行程序切換;

hardirq/softirq 字元的含義 : 'H' 在 softirq 中發生了硬體中斷, 'h' – 硬體中斷,’ s ’表示 softirq,’ . ’不在中斷上下文中,普通狀態。

preempt-depth: 當搶佔中斷使能後,該域代表 preempt_disabled 的級別。

在每一行的中間,還有兩個域:time 和 delay 。 time: 表示從 trace 開始到當前的相對時間。 Delay 突出顯示那些有高延遲的地方以便引起使用者注意。當其顯示為 ! 時,表示需要引起注意。

function graph tracer 輸出

Function graph tracer 和 function tracer 類似,但輸出為函式呼叫圖,更加容易閱讀:

# tracer: function_graph 
 # 
 # CPU  OVERHEAD/DURATION      FUNCTION CALLS 
 # |     |   |                 |   |   |   | 
 0)               |  sys_open() { 
 0)               |    do_sys_open() { 
 0)               |      getname() { 
 0)               |        kmem_cache_alloc() { 
 0)   1.382 us    |          __might_sleep(); 
 0)   2.478 us    |        } 
 0)               |        strncpy_from_user() { 
 0)               |          might_fault() { 
 0)   1.389 us    |            __might_sleep(); 
 0)   2.553 us    |          } 
 0)   3.807 us    |        } 
 0)   7.876 us    |      } 
 0)                |      alloc_fd() { 
 0)   0.668 us    |        _spin_lock(); 
 0)   0.570 us    |        expand_files(); 
 0)   0.586 us    |        _spin_unlock();

OVERHEAD 為 ! 時提醒使用者注意,該函式的效能比較差。上面的例子中可以看到 sys_open 呼叫了 do_sys_open,依次又呼叫了 getname(),依此類推。

Sysprof tracer 的輸出

Sysprof tracer 定時對核心進行取樣,她的輸出檔案中記錄了每次取樣時核心正在執行哪些核心函式,以及當時的核心堆疊情況。

每一行前半部分的格式和 3.1.1 中介紹的 function tracer 一樣,只是,最後一部分 FUNCTION 有所不同。

Sysprof tracer 中,FUNCTION 列格式如下:

Identifier  address frame_pointer/pid

當 identifier 為 0 時,代表一次取樣的開始,此時第三個數字代表當前程序的 PID ;

Identifier 為 1 代表核心態的堆疊資訊;當 identifier 為 2 時,代表使用者態堆疊資訊;顯示堆疊資訊時,第三列顯示的是 frame_pointer,使用者可能需要開啟 system map 檔案查詢具體的符號,這是 ftrace 有待改進的一個地方吧。

當 identifier 為 3 時,代表一次取樣結束。

從 2.6.30 開始,ftrace 還支援幾種 Non-tracer tracer,所謂 Non-tracer tracer 主要包括以下幾種:

  • Max Stack Tracer
  • Profiling (branches / unlikely / likely / Functions)
  • Event tracing

和傳統的 tracer 不同,Non-Tracer Tracer 並不對每個核心函式進行跟蹤,而是一種類似邏輯分析儀的模式,即對系統進行取樣,但似乎也不完全如此。無論怎樣,這些 tracer 的使用方法和前面所介紹的 tracer 的使用稍有不同。下面我將試圖描述這些 tracer 的使用方法。

Max Stack Tracer 的使用

這個 tracer 記錄核心函式的堆疊使用情況,使用者可以使用如下命令開啟該 tracer:

# echo 1 > /proc/sys/kernel/stack_tracer_enabled

從此,ftrace 便留心記錄核心函式的堆疊使用。 Max Stack Tracer 的輸出在 stack_trace 檔案中:

# cat /debug/tracing/stack_trace 
 Depth Size Location (44 entries) 
 ----- ---- -------- 
 0) 3088 64 update_curr+0x64/0x136 
 1) 3024 64 enqueue_task_fair+0x59/0x2a1 
 2) 2960 32 enqueue_task+0x60/0x6b 
 3) 2928 32 activate_task+0x27/0x30 
 4) 2896 80 try_to_wake_up+0x186/0x27f 
…
 42)  80 80 sysenter_do_call+0x12/0x32

從上例中可以看到核心堆疊最滿的情況如下,有 43 層函式呼叫,堆疊使用大小為 3088 位元組。此外還可以在 Location 這列中看到整個的 calling stack 情況。這在某些情況下,可以提供額外的 debug 資訊,幫助開發人員定位問題。

Branch tracer

Branch tracer 比較特殊,她有兩種模式,即是傳統 tracer,又實現了 profiling tracer 模式。

作為傳統 tracer 。其輸出檔案為 trace,格式如下:

# tracer: branch 
 # 
 #  TASK-PID   CPU#    TIMESTAMP        FUNCTION 
 #    |   |        |          |                | 
  Xorg-2491   [000] 688.561593: [ ok ] fput_light:file.h:29 
  Xorg-2491   [000] 688.561594: [ ok ] fput_light:file_table.c:330

在 FUNCTION 列中,顯示了 4 類資訊:

函式名,檔案和行號,用中括號引起來的部分,顯示了分支的資訊,假如該字串為 ok,表明 likely/unlikely 返回為真,否則字串為 MISS 。舉例來說,在檔案 file.h 的第 29 行,函式 fput_light 中,有一個 likely 分支在執行時解析為真。我們看看 file.h 的第 29 行:

static inline void fput_light(struct file *file, int fput_needed) 
 {LINE29:    if (unlikely(fput_needed)) 
                  fput(file); 
 }

Trace 結果告訴我們,在 688 秒的時候,第 29 行程式碼被執行,且預測結果為 ok,即 unlikely 成功。

Branch tracer 作為 profiling tracer 時,其輸出檔案為 profile_annotated_branch,其中記錄了 likely/unlikely 語句完整的統計結果。

#cat trace_stat/branch_ annotated 
 correct incorrect    %      function            file        line 
 ------- ----------  ---- ------------------ -------------- ----- 
 0      46             100   pre_schedule_rt    sched_rt.c     1449

下面是檔案 sched_rt.c 的第 1449 行的程式碼:

	if (unlikely(rt_task(prev)) && rq->rt.highest_prio.curr > prev->prio) 
    pull_rt_task(rq);

記錄表明,unlikely 在這裡有 46 次為假,命中率為 100% 。假如為真的次數更多,則說明這裡應該改成 likely 。

Workqueue profiling

假如您在核心編譯時選中該 tracer,ftrace 便會統計 workqueue 使用情況。您只需使用下面的命令檢視結果即可:

#cat /debug/tracing/trace_stat/workqueue

典型輸出如下:

# CPU INSERTED  EXECUTED  NAME 
 #  |     |         |           | 
   0   38044    38044    events/0 
   0     426      426    khelper 
   0    9853     9853    kblockd/0 
   0       0        0    kacpid 
…

可以看到 workqueue events 在 CPU 0 上有 38044 個 worker 被插入並執行。

Event tracer

Event tracer 不間斷地記錄核心中的重要事件。使用者可以用下面的命令檢視 ftrace 支援的事件。

#cat /debug/tracing/available_event

下面以跟蹤程序切換為例講述 event tracer 的使用。首先開啟 event tracer,並記錄程序切換:

# echo sched:sched_switch >> /debug/tracing/set_event 
 # echo sched_switch >> /debug/tracing/set_event 
 # echo 1 > /debug/tracing/events/sched/sched_switch/enable

上面三個命令的作用是一樣的,您可以任選一種。

此時可以檢視 ftrace 的輸出檔案 trace:

>head trace 
 # tracer: nop 
 # 
 #   TASK-PID CPU#  TIMESTAMP FUNCTION 
 #    | |      |     |             | 
 <idle>-0 [000] 12093.091053: sched_switch: task swapper:0 [140] ==> 
  /user/bin/sealer:2612 [120]

我想您會發現該檔案很容易解讀。如上例,表示一個程序切換 event,從 idle 程序切換到 sealer 程序。

研究 tracer 的實現是非常有樂趣的。理解 ftrace 的實現能夠啟發我們在自己的系統中設計更好的 trace 功能。

Ftrace 的整體構架:

Ftrace 有兩大組成部分,一是 framework,另外就是一系列的 tracer 。每個 tracer 完成不同的功能,它們統一由 framework 管理。 ftrace 的 trace 資訊儲存在 ring buffer 中,由 framework 負責管理。 Framework 利用 debugfs 系統在 /debugfs 下建立 tracing 目錄,並提供了一系列的控制檔案。

本文並不打算系統介紹 tracer 和 ftrace framework 之間的介面,只是打算從純粹理論的角度,簡單剖析幾種具體 tracer 的實現原理。假如讀者需要開發新的 tracer,可以參考某個 tracer 的原始碼。

Ftrace 採用 GCC 的 profile 特性在所有核心函式的開始部分加入一段 stub 程式碼,ftrace 過載這段程式碼來實現 trace 功能。

gcc 的 -pg 選項將在每個函式入口處加入對 mcount 的呼叫程式碼。比如下面的 C 程式碼。

//test.c 
 void foo(void) 
 { 
   printf( “ foo ” ); 
 }

用 gcc 編譯:

gcc – S test.c

反彙編如下:

	_foo: 
        pushl   %ebp 
        movl    %esp, %ebp 
        subl    $8, %esp 
        movl    $LC0, (%esp) 
        call    _printf 
        leave 
        ret

再加入 -gp 選項編譯:

gcc – pg – S test.c

得到的彙編如下:

_foo: 
        pushl   %ebp 
        movl    %esp, %ebp 
        subl    $8, %esp 
 LP3: 
        movl    $LP3,%edx 
        call    _mcount 
        movl    $LC0, (%esp) 
        call    _printf 
        leave 
        ret

增加 pg 選項後,gcc 在函式 foo 的入口處加入了對 mcount 的呼叫:call _mcount 。原本 mcount 由 libc 實現,但您知道核心不會連線 libc 庫,因此 ftrace 編寫了自己的 mcount stub 函式,並藉此實現 trace 功能。

在每個核心函式入口加入 trace 程式碼,必然會影響核心的效能,為了減小對核心效能的影響,ftrace 支援動態 trace 功能。

當 CONFIG_DYNAMIC_FTRACE 被選中後,核心編譯時會呼叫一個 perl 指令碼:recordmcount.pl 將每個函式的地址寫入一個特殊的段:__mcount_loc

在核心初始化的初期,ftrace 查詢 __mcount_loc 段,得到每個函式的入口地址,並將 mcount 替換為 nop 指令。這樣在預設情況下,ftrace 不會對核心效能產生影響。

當用戶開啟 ftrace 功能時,ftrace 將這些 nop 指令動態替換為 ftrace_caller,該函式將呼叫使用者註冊的 trace 函式。其具體的實現在相應 arch 的彙編程式碼中,以 x86 為例,在 entry_32.s 中:

	ENTRY(ftrace_caller) 
       cmpl $0, function_trace_stop 
       jne  ftrace_stub 
       pushl %eax 
       pushl %ecx 
       pushl %edx 
       movl 0xc(%esp), %eax 
       movl 0x4(%ebp), %edx 
       subl $MCOUNT_INSN_SIZE, %eax 
 .globl ftrace_call 
 ftrace_call: 
       call ftrace_stubline 10popl %edx 
       popl %ecx 
       popl %eax 

 .globl ftrace_stub 
 ftrace_stub: 
       ret 
 END(ftrace_caller)

Function tracer 將 line10 這行程式碼替換為 function_trace_call() 。這樣每個核心函式都將呼叫 function_trace_call() 。

在 function_trace_call() 函式內,ftrace 記錄函式呼叫堆疊資訊,並將結果寫入 ring buffer,稍後,使用者可以通過 debugfs 的 trace 檔案讀取該 ring buffer 中的內容。

Irqsoff tracer 的實現依賴於 IRQ-Flags 。 IRQ-Flags 是 Ingo Molnar 維護的一個核心特性。使得使用者能夠在中斷關閉和開啟時得到通知,ftrace 過載了其通知函式,從而能夠記錄中斷禁止時間。即,中斷被關閉時,記錄下當時的時間戳。此後,中斷被開啟時,再計算時間差,由此便可得到中斷禁止時間。

IRQ-Flags 封裝開關中斷的巨集定義:

#define local_irq_enable() \ 
    do { trace_hardirqs_on (); raw_local_irq_enable(); } while (0)

ftrace 在檔案 ftrace_irqsoff.c 中過載了 trace_hardirqs_on 。具體程式碼不再羅列,主要是使用了 sched_clock()函式來獲得時間戳。

Hw-branch 只在 IA 處理器上實現,依賴於 x86 的 BTS 功能。 BTS 將 CPU 實際執行到的分支指令的相關資訊儲存下來,即每個分支指令的源地址和目標地址。

軟體可以指定一塊 buffer,處理器將每個分支指令的執行情況寫入這塊 buffer,之後,軟體便可以分析這塊 buffer 中的功能。

Linux 核心的 DS 模組封裝了 x86 的 BTS 功能。 Debug Support 模組封裝了和底層硬體的介面,主要支援兩種功能:Branch trace store(BTS) 和 precise-event based sampling (PEBS) 。 ftrace 主要使用了 BTS 功能。

核心程式碼中常使用 likely 和 unlikely 提高編譯器生成的程式碼質量。 Gcc 可以通過合理安排彙編程式碼最大限度的利用處理器的流水線。合理的預測是 likely 能夠提高效能的關鍵,ftrace 為此定義了 branch tracer,跟蹤程式中 likely 預測的正確率。

為了實現 branch tracer,重新定義了 likely 和 unlikely 。具體的程式碼在 compiler.h 中。

# ifndef likely 
 #  define likely(x) (__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 1)) 
 # endif 
 # ifndef unlikely 
 #  define unlikely(x) (__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 0)) 
 # endif

其中 __branch_check 的實現如下:

#define __branch_check__(x, expect) ({\ 
    int ______r;    \ 
    static struct ftrace_branch_data \ 
    __attribute__((__aligned__(4)))  \ 
    __attribute__((section("_ftrace_annotated_branch"))) \ 
                         ______f = { \ 
                           .func = __func__, \ 
                           .file = __FILE__, \ 
                           .line = __LINE__, \ 
                    }; \ 
              ______r = likely_notrace(x);\ 
              ftrace_likely_update(&______f, ______r, expect); \ 
              ______r; \ 
  })

ftrace_likely_update() 將記錄 likely 判斷的正確性,並將結果儲存在 ring buffer 中,之後使用者可以通過 ftrace 的 debugfs 介面讀取分支預測的相關資訊。從而調整程式程式碼,優化效能。

總結

本文講解了 ftrace 的基本使用。在實踐中,ftrace 是一個非常有效的效能調優和 debug 分析工具,每個人使用她的方法和角度都不相同,一定有很多 best practice,這非本文所能涉及。但希望通過本文的講解,能讓讀者對 ftrace 形成一個基本的瞭解,進而在具體工作中使用她。

參考資料