Linux 性能工具 ftrace 框架
對於 ftrace 架構,主要來了解下內核是如何實現的,其主要包括如下內容:
-
ring buffer 的原理和代碼分析
-
tracer(function、function_graph、irq_off) 原理和代碼分析
-
trace event
一,ringBuffer
Ringbuffer 是 trace32 框架的一個基礎,所有的 trace 原始數據都是通過 Ring Buffer 記錄的,其主要有以下幾個作用:
-
存儲在內存中,速度非常快,對系統的性能影響降到最低的水平
-
ring 結構,可以循環寫,安全而不浪費內存空間,能夠 get 到最新的 trace 信息
對於系統,真正的難點在於系統在各種複雜的場景下,例如常規的上下文、中斷上下文 (NMI/IRQ/SOFTIRQ) 等都能很好的 trace,如何保證既不影響系統的邏輯,又能處理好相互之間的關係,同時又不影響系統的性能。
1.1 Ring buffer 設計思路
對於 Ring Buffer 面臨的最大問題
-
當我們使用 trace 工具的時候,可能處在不同的上下文中執行,對 Ring Buffer 的訪問時隨時可能被打斷的,所以需要對 Ring Buffer 的訪問時需要互斥保護的
-
RingBuffer 不能使用常規的 lock 操作,這樣會使不同的上下文之間出現大量的阻塞操作,產生了大量的耦合邏輯,影響程序原理的邏輯和性能
如何解決這些問題呢?首先從 Ring Buffer 使用的方式來看,工作模式,對於該模式,是一個很典型的生產者和消費者,其主要分爲:
-
Producer/Consumer 模式:有不斷的數據寫入到 Ring Buffer,是一個寫入者;同時對於用戶也不斷的從 RingBuffer 中讀取數據,在生產者已經把 Ring Buffer 空間寫滿的情況下,如果沒有消費者來讀取數據,沒有 Free 空間,那麼生產者就會停止寫入丟棄新的數據
-
Overwrite 模式:在生產者已經把 Ring Buffer 空間寫滿的情況下,如果沒有消費者來讀數據 free 空間,生產者會覆蓋寫入,最老的數據會被覆蓋;
其次,從架構圖中,我們面對有很多的寫者,對於同一個 per cpu 的 RingBuffer,其寫必須滿足:
-
不能同時有兩個寫入者在進行寫操作
-
允許高優先級的寫入者中斷低優先級的寫入者
對於讀操作必須要滿足:
-
讀操作可以隨時發生,但是同一時刻只有一個讀者在工作
-
讀操作和寫操作可以同時發生
-
讀操作不會中斷寫操作,但是寫操作會中斷讀操作
-
支持兩種模式的讀操作:簡易讀,也叫 iterator 讀,在讀取時會關閉寫入,且讀完不會破壞數據可以重複讀取,實例見 "/sys/kernel/debug/tracing/trace";並行讀,也叫 custom 讀,常用於監控程序實時地進行並行讀,其利用了一個 reader page 交換出 ring buffer 中的 head page,避免了讀寫的相互阻塞,實例見 "/sys/kernel/debug/tracing/trace_pipe";
1.2 代碼流程和框架
對於 Ringbuffer 的初始化,主要是通過 tracer_alloc_buffers 調用到 ring_buffer_alloc 完成的,其主要流程如下:
其主要數據結構如下圖所示:
-
struct ring_buffer 在每個 cpu 上有獨立的 struct ring_buffer_per_cpu 數據結構
-
struct ring_buffer_per_cpu 根據定義 size 的大小,分配 page 空間,並把 page 連成環形結構
-
struct buffer_page 是一個控制結構;struct buffer_data_page 纔是一個實際的 page,除了開頭的兩個控制字段 time_stamp、commit,其他空間都是用來存儲數據的;數據使用 struct ring_buffer_event 來存儲,其在包頭中還存儲了時間戳、長度 / 類型信息
-
struct ring_buffer_per_cpu 中使用 head_page(讀)、commit_page(寫確認)、tail_page(寫) 三種指針來管理 page ring;同理 buffer_page->read(讀)、buffer_page->write(寫)、buffer_data_page->commit(寫確認) 用來描述 page 內的偏移指針
-
ring_buffer_per_cpu->reader_page 中還包含了一個獨立的 page,用來支持 reader 方式的讀操作
二,ftrace 的內核註冊
對於 ftrace 的 framwork 層,首先需要建立 debugfs 的一系列的訪問節點,是通過如下的流程完成的
完成了核心的註冊後,我們來看看 ftrace 是如何完成各個功能的,對於任何一個 trace 功能,都可以歸納於如下流程:
-
函數插樁:使用各種插樁方式把自己的 trace 函數插入到需要跟蹤的 probe point 上
-
Input trace 數據:在 trace 的 probe 函數中命中時,會存儲數據到 ring buffer 當中,這裏主要包括 filter 和 tigger 功能
-
Output trace 數據:用戶和程序需要讀取 trace 數據,根據需要輸出數據,對數據進行解析等
2.1 Function tracer 的實現
這個功能是利用_mcount() 函數進行插樁的,在 gcc 使用了 "-gp“選項以後,會在每個函數入口插入以下的語句
每個函數入口處插入對_mcount() 函數的調用,就是 gcc 提供的插樁機制,我們可以重新定義_mcount() 函數中的內容,調用想要執行的內容。對於 tracer 自身而言,是不是需要 - pg 選項,因此在 kernel/tracing/Makefile 中將 - pg 選項中由我們自己定義
2.1.1 靜態插樁
我們來看看 ARM64 如何處理的,其代碼路徑爲 arch/arm64/kernel/entry-ftrace.S
當未選中 CONFIG_DYNAMIC_FTRACE 時,其採用如下的方案;
-
每個函數調用都會根據不同的體系結構的實現調用_mcount 函數
-
如果 ftrace 使用了某些跟蹤器,ftrace_trace_function 指針不再指向 ftrace_stub,而是指向具體的跟蹤函數
-
否則就執行到體系結構相關的 ftrace_stub 從函數返回,而該接口爲空函數
2.1.2 動態插樁
static ftrace 一旦使能,對 kernel 中所有的函數 (除開 notrace、online、其他特殊函數) 進行插裝,這帶來的性能開銷是驚人的,有可能導致人們棄用 ftrace 功能。
爲了解決這個問題,內核開發者推出了 dynamic ftrace,因爲實際上調用者一般不需要對所有函數進行追蹤,只會對感興趣的一部分函數進行追蹤。dynamic ftrace 把不需要追蹤的函數入口處指令 “bl _mcount"替換成 nop,這樣基本上對性能無影響,對需要追蹤的函數替換入口處"bl _mcount" 爲需要調用的函數。
-
ftrace 在初始化時,“scripts/recordmcount.pl”腳本記錄的所有函數入口處插樁位置的 “bl _mcount”,將其替換成“nop” 指令,對性能基本無影響
-
在 tracer enable 的時候,把需要跟蹤的函數的插樁位置 nop 替換成 bl ftrace_caller
在編譯的時候調用 recordmcount.pl 搜索所有_mcount 函數調用點,並且所有的調用點地址保存到 section _mcount_loc,其定義在 include/asm-generic/vmlinux.lds.h,詳細的見文件以具體研究 “scripts/recordmcount.pl、scripts/recordmcount.c”。
在初始化時,遍歷 section __mcount_loc 的調用點地址,默認爲所有 “bl _mcount” 替換成“nop”,其定義爲 kernel/trace/ftrace.c
2.1.3 irqs off/preempt off/preempt irqsoff tracer
-
irqsoff tracer:當中斷被禁止時,系統無法響應外部事件,比如鼠標和鍵盤,時鐘也無法產生 tick 中斷,這也意味着系統響應延遲,irqsoff 這個 tracer 能夠跟蹤並記錄內核中哪些函數禁止了中斷,對於其中中斷禁止時間最長的,irqsoff 將在 Log 文件中第一行標記出來,從而使開發者可以迅速定位造成響應延遲的罪魁禍首
-
preemptoff tracer:跟蹤並記錄禁止內核搶佔並關閉中斷佔用期間的函數,並清晰地顯示出禁止搶佔時間最長的內核函數
-
preempt irqsoff tracer: 跟蹤和記錄禁止中斷或禁止搶佔的內核函數,以及禁止時間最長的函數
preemptoff 與 irqsoff 跟蹤器
preempt off 與 irqs off 跟蹤器用的跟蹤函數是相同的,都是 irqsoff_tracer_call()。
preemptoff 與 irqsoff 跟蹤器的不同之處
irqsoff 跟蹤器的 start 點在開啓或關閉中斷的地方,如 local_irq_disable()
preemptoff 跟蹤器的 start 點在開啓或關閉搶佔的地方,如 prempt_disable()
irqsoff tracer 的插樁方法,是直接在 local_irq_enable()、local_irq_disable() 中直接插入鉤子函數 trace_hardirqs_on()、trace_hardirqs_off()。
我們來看看 start_critical_timing 的實現,其主要爲:
其主要的設計思想如下
2.2 trace event
linux trace 中,最基礎的時 function tracer 和 tracer event,上面學習了 function,本節是學習 event,其也離不開如下流程
trace event 的插樁使用的是 tracepoint 機制,該機制是一種靜態的插樁方法,它需要靜態的定義樁函數,並且在插樁位置顯式調用。這種方法的好處是高效可靠,並且可以處於函數中的任何位置、方便的訪問各種變量,壞處是不太靈活。對於 kernel 在重要的節點固定位置,插入了幾百個 trace event 用於跟蹤。
對於內核,我們創建了幾個操作 tracepoint 的函數:
-
樁函數:trace_##name();
-
註冊回調函數:register_trace_##name();
-
註銷回調函數:unregister_trace_##name();
tracepoint 的定義如下:
struct tracepoint {
const char *name; /* Tracepoint name */
struct static_key key;
void (*regfunc)(void);
void (*unregfunc)(void);
struct tracepoint_func __rcu *funcs;
};
-
key tracepoint: 是否使能開關,如果回調函數數組爲空,則 key 爲 disable;如果回調函數數組中有函數指針,則 key 爲 enable
-
regfunc/unregfunc: 註冊 / 註銷回調函數時的鉤子函數
-
funcs : 回調函數數組,tracepoint 的作用就是在樁函數被命中時,逐個調用回調函數數組的函數
我們在探測點插入樁函數:(kernl/sched/core.c)
static void __sched notrace __schedule(bool preempt)
{
...
trace_sched_switch(preempt, prev, next);
...
}
樁函數被命中時的執行流程,可以看到就是逐個的執行回調函數數組中的函數指針:
可以通過 register_trace_##name()/unregister_trace_##name() 函數向回調函數數組中添加 / 刪除函數指針
trace event 對 tracepoint 的利用,以上可以看到,tracepoint 只是一種靜態插樁方法。trace event 可以使用,其他機制也可以使用,只是 kernel 的絕大部分 tracepoint 都是 trace event 在使用。
trace event 也必須向 tracepoint 註冊自己的回調函數,這些回調函數的作用就是在函數被命中時往 ringbuffer 中寫入 trace 信息。ftrace 開發者們意識到了這點,所以提供了 trace event 功能,開發者不需要自己去註冊樁函數了,易用性較好
2.2.1 增加一個新的 trace event
在現有的代碼中添加探測函數,這是讓很多內核開發者非常不爽的一件事,因爲這可能降低性能或者讓代碼看起來非常臃腫。爲了解決這些問題,內核最終進化出了一個 TRACE_EVENT() 來實現 trace event 的定義,這是非常簡潔、智能的一個宏定義。
首先我們先來了解一下怎麼樣使用 TRACE_EVENT() 新增加一個 trace event,新增加 trace event,我們必須遵循規定的格式。
以下以內核中已經存在的 event sched_switch 爲例,說明定義過程。
首先需要在 include/trace/events / 文件夾下添加一個自己 event 的頭文件,需要遵循註釋的標準格式:include/trace/events/sched.h
在探測點位置中調用樁函數,需要遵循註釋的標準格式
由於內核各個子系統大量使用 event tracing 來 trace 不同的事件,每有一個需要 trace 的事件就實現這麼一套函數,這樣內核就會存在大量類似的重複的代碼,爲了避免這樣的情況,內核開發者使用一個宏,讓宏自動展開成具有相似性的代碼。這個宏就是 TRACE_EVENT,要爲某個事件添加一個 trace event,只需要聲明這樣一個宏就可以了
- kprobe event
kprobe event 就是這樣的產物。krpobe event 和 trace event 的功能一樣,但是因爲它採用的是 kprobe 插樁機制,所以它不需要預留插樁位置,可以動態的在任何位置進行插樁。開銷會大一點,但是非常靈活,是一個非常方便的補充機制。
kprobe 的主要原理是使用 “斷點異常” 和“單步異常”兩種異常指令來對任意地址進行插樁,在此基礎之上實現了三種機制:
-
kprobe:可以被插入到內核的任何指令位置,在被插入指令之前調用 kp.pre_handler(),在被插入指令之後調用 kp.post_handler()
-
jprobe:只支持對函數進行插入
-
kretprobe:和 jprobe 類似,機制略有不同,會替換被探測函數的返回地址,讓函數先執行插入的鉤子函數,再恢復。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_2PbOw53SB1M2ft_oaAi7Q