Linux 性能工具 ftrace 框架

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

一,ringBuffer

Ringbuffer 是 trace32 框架的一個基礎,所有的 trace 原始數據都是通過 Ring Buffer 記錄的,其主要有以下幾個作用:

對於系統,真正的難點在於系統在各種複雜的場景下,例如常規的上下文、中斷上下文 (NMI/IRQ/SOFTIRQ) 等都能很好的 trace,如何保證既不影響系統的邏輯,又能處理好相互之間的關係,同時又不影響系統的性能。

1.1 Ring buffer 設計思路

對於 Ring Buffer 面臨的最大問題

如何解決這些問題呢?首先從 Ring Buffer 使用的方式來看,工作模式,對於該模式,是一個很典型的生產者和消費者,其主要分爲:

其次,從架構圖中,我們面對有很多的寫者,對於同一個 per cpu 的 RingBuffer,其寫必須滿足:

對於讀操作必須要滿足:

1.2 代碼流程和框架

對於 Ringbuffer 的初始化,主要是通過 tracer_alloc_buffers 調用到 ring_buffer_alloc 完成的,其主要流程如下:

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

二,ftrace 的內核註冊

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

完成了核心的註冊後,我們來看看 ftrace 是如何完成各個功能的,對於任何一個 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 時,其採用如下的方案;

2.1.2 動態插樁

static ftrace 一旦使能,對 kernel 中所有的函數 (除開 notrace、online、其他特殊函數) 進行插裝,這帶來的性能開銷是驚人的,有可能導致人們棄用 ftrace 功能。

爲了解決這個問題,內核開發者推出了 dynamic ftrace,因爲實際上調用者一般不需要對所有函數進行追蹤,只會對感興趣的一部分函數進行追蹤。dynamic ftrace 把不需要追蹤的函數入口處指令 “bl _mcount"替換成 nop,這樣基本上對性能無影響,對需要追蹤的函數替換入口處"bl _mcount" 爲需要調用的函數。

在編譯的時候調用 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

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 的函數:

tracepoint 的定義如下:

struct tracepoint {
	const char *name;		/* Tracepoint name */
	struct static_key key;
	void (*regfunc)(void);
	void (*unregfunc)(void);
	struct tracepoint_func __rcu *funcs;
};

我們在探測點插入樁函數:(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,只需要聲明這樣一個宏就可以了

  1. kprobe event

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

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_2PbOw53SB1M2ft_oaAi7Q