linux性能工具--ftrace框架

對於ftrace架構,主要來了解下內核是如何實現的,其主要包括如下內容:

  • ring buffer的原理和代碼分析
  • tracer(function、function_graph、irq_off)原理和代碼分析
  • trace event

image

1. ring Buffer

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完成的,其主要流程如下:

image

其主要數據結構圖如下圖所示:

image

  • 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方式的讀操作

2 ftrace的內核註冊

對於ftrace的framwork層,首先需要建立debugfs的一系列的訪問節點,是通過如下的流程完成的

image

完成了核心的註冊後,我們來看看ftrace是如何完成各個功能的,對於任何一個trace功能,都可以歸納於如下流程

  • 函數插樁: 使用各種插樁方式把自己的trace函數插入到需要跟蹤的probe point上
  • Input trace數據: 在trace的probe函數中命中時,會存儲數據到ring buffer當中,這裏主要包括filter和tigger功能
  • Output trace數據: 用戶和程序需要讀取trace數據,根據需要輸出數據,對數據進行解析等

2.1 Function tracer的實現

這個功能是利用_mcount()函數進行插樁的,在gcc使用了"-gp“選項以後,會在每個函數入口插入以下的語句

image

每個函數入口處插入對_mcount()函數的調用,就是gcc提供的插樁機制,我們可以重新定義_mcount()函數中的內容,調用想要執行的內容。對於tracer自身而言,是不是需要-pg選項,因此在kernel/tracing/Makefile中將-pg選項中由我們自己定義

image

2.1.1 靜態插樁

我們來看看ARM64如何處理的,其代碼路徑爲arch/arm64/kernel/entry-ftrace.S

image

當未選中CONFIG_DYNAMIC_FTRACE時,其採用如下的方案

  • 每個函數調用都會根據不同的體系結構的實現調用_mcount函數
  • 如果ftrace使能了某些跟蹤器,ftrace_trace_function指針不再指向ftrace_stub,而是指向具體的跟蹤函數
  • 否則就執行到體系結構相關的ftrace_stub從函數返回,而該接口爲空函數

image

也就是說開啓ftrace調用函數時,都會先調用_mcount,總是至少會執行兩條指令,即使ftrace_trace_function沒有被指向某個跟蹤函數。

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

image

在編譯的時候調用recordmcount.pl搜索所有_mcount函數調用點,並且所有的調用點地址保存到section _mcount_loc,其定義在include/asm-generic/vmlinux.lds.h,詳細的見文件以具體研究“scripts/recordmcount.pl、scripts/recordmcount.c”。

image

在初始化時,遍歷section __mcount_loc的調用點地址,默認給所有“bl _mcount”替換成“nop”,其定義爲kernel/trace/ftrace.c

image

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()

image

irqsoff tracer的插樁方法,是直接在local_irq_enable()、local_irq_disable()中直接插入鉤子函數trace_hardirqs_on()、trace_hardirqs_off()。

image

我們來看看start_critical_timing的實現,其主要爲:

image

其主要的設計思想如下

image

2.2 trace event

linux trace中,最基礎的時function tracer和tracer event,上面學習了function,本節是學習event,其也離不開如下流程

image

trace event的插樁使用的是tracepoint機制,該機制是一種靜態的插樁方法,它需要靜態的定義樁函數,並且在插樁位置顯式調用。這種方法的好處是高效可靠,並且可以處於函數中的任何位置、方便的訪問各種變量,壞處是不太靈活。對於kernel在重要的節點固定位置,插入了幾百個trace event用於跟蹤。

image

  • 樁函數: 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);
	...
}

樁函數被命中時的執行流程,可以看到就是逐個的執行回調函數數組中的函數指針

image

image

可以通過 register_trace_##name()/unregister_trace_##name() 函數向回調函數數組中添加/刪除函數指針

image

trace event 對 tracepoint 的利用,以上可以看到,tracepoint 只是一種靜態插樁方法。trace event 可以使用,其他機制也可以使用,只是 kernel 的絕大部分 tracepoint 都是 trace event 在使用。

單純的定義和使用一個 trace point,可以參考: Documentation/trace/tracepoints.txt

trace event 也必須向 tracepoint 註冊自己的回調函數,這些回調函數的作用就是在函數被命中時往 ringbuffer 中寫入 trace 信息。ftrace開發者們意識到了這點,所以提供了trace event功能,開發者不需要自己去註冊樁函數了,易用性較好

2.2.1 增加一個新的 trace event

在現有的代碼中添加探測函數,這是讓很多內核開發者非常不爽的一件事,因爲這可能降低性能或者讓代碼看起來非常臃腫。爲了解決這些問題,內核最終進化出了一個 TRACE_EVENT() 來實現 trace event 的定義,這是非常簡潔、智能的一個宏定義。

首先我們先來了解一下怎麼樣使用 TRACE_EVENT() 新增加一個 trace event,新增加 trace event,我們必須遵循規定的格式。格式可以參考:
Using the TRACE_EVENT() macro (Part 1)samples/trace_events

以下以內核中已經存在的 event sched_switch 爲例,說明定義過程。

  • 首先需要在 include/trace/events/文件夾下添加一個自己 event 的頭文件,需要遵循註釋的標準格式:include/trace/events/sched.h
  • 在探測點位置中調用樁函數,需要遵循註釋的標準格式

由於內核各個子系統大量使用 event tracing 來 trace 不同的事件,每有一個需要 trace 的事件就實現這麼一套函數,這樣內核就會存在大量類似的重複的代碼,爲了避免這樣的情況,內核開發者使用一個宏,讓宏自動展開成具有相似性的代碼。這個宏就是 TRACE_EVENT,要爲某個事件添加一個 trace event,只需要聲明這樣一個宏就可以了

3. kprobe event

kprobe event就是這樣的產物。krpobe event和trace event的功能一樣,但是因爲它採用的是kprobe插樁機制,所以它不需要預留插樁位置,可以動態的在任何位置進行插樁。開銷會大一點,但是非常靈活,是一個非常方便的補充機制。

kprobe的主要原理是使用“斷點異常”和“單步異常”兩種異常指令來對任意地址進行插樁,在此基礎之上實現了三種機制:

  • kprobe: 可以被插入到內核的任何指令位置,在被插入指令之前調用kp.pre_handler(),在被插入指令之後調用kp.post_handler()
  • jprobe: 只支持對函數進行插入
  • kretprobe: 和jprobe類似,機制略有不同,會替換被探測函數的返回地址,讓函數先執行插入的鉤子函數,再恢復。

具體的kprobe原理可以參考:Linux kprobe(內核探針 x86)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章