天天看點

Linux性能工具(三)ftrace架構

作者:深度Linux

對于ftrace架構,主要來了解下核心是如何實作的,其主要包括如下内容:

  • ring buffer的原理和代碼分析
  • tracer(function、function_graph、irq_off)原理和代碼分析
  • trace event
Linux性能工具(三)ftrace架構

一,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,其寫必須滿足:

  • 不能同時有兩個寫入者在進行寫操作
  • 允許高優先級的寫入者中斷低優先級的寫入者

更多Linux核心視訊教程文檔資料免費領取背景私信【核心】自行擷取。

Linux性能工具(三)ftrace架構

對于讀操作必須要滿足:

  • 讀操作可以随時發生,但是同一時刻隻有一個讀者在工作
  • 讀操作和寫操作可以同時發生
  • 讀操作不會中斷寫操作,但是寫操作會中斷讀操作
  • 支援兩種模式的讀操作:簡易讀,也叫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完成的,其主要流程如下:

Linux性能工具(三)ftrace架構

其主要資料結構如下圖所示:

Linux性能工具(三)ftrace架構
  • 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的一系列的通路節點,是通過如下的流程完成的

Linux性能工具(三)ftrace架構

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

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

2.1 Function tracer的實作

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

Linux性能工具(三)ftrace架構

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

Linux性能工具(三)ftrace架構

2.1.1 靜态插樁

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

Linux性能工具(三)ftrace架構

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

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

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
Linux性能工具(三)ftrace架構

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

Linux性能工具(三)ftrace架構

在初始化時,周遊section __mcount_loc的調用點位址,預設為所有“bl _mcount”替換成“nop”,其定義為kernel/trace/ftrace.c

Linux性能工具(三)ftrace架構

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

Linux性能工具(三)ftrace架構

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

Linux性能工具(三)ftrace架構

我們來看看start_critical_timing的實作,其主要為:

Linux性能工具(三)ftrace架構

其主要的設計思想如下

Linux性能工具(三)ftrace架構

2.2 trace event

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

Linux性能工具(三)ftrace架構

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

Linux性能工具(三)ftrace架構

對于核心,我們建立了幾個操作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);
	...
}           

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

Linux性能工具(三)ftrace架構
Linux性能工具(三)ftrace架構

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

Linux性能工具(三)ftrace架構

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,隻需要聲明這樣一個宏就可以了

3. kprobe event

kprobe event就是這樣的産物。krpobe event和trace event的功能一樣,但是因為它采用的是kprobe插樁機制,是以它不需要預留插樁位置,可以動态的在任何位置進行插樁。開銷會大一點,但是非常靈活,是一個非常友善的補充機制。

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

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

繼續閱讀