偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

聊聊使用 tracepoint 和 kprobe 進行內(nèi)核源碼跟蹤的技術(shù)原理!

系統(tǒng) Linux
在 Linux 高級應(yīng)用領(lǐng)域,跟蹤的英文原名叫 tracing,是一個非常大的話題。而且這個領(lǐng)域還在快速地迭代更新,近些年還涌現(xiàn)出了 eBPF 相關(guān)的各種工具和技術(shù)。

飛哥在之前的各種文章中介紹技術(shù)原理的時候或多或少都會涉及到一些內(nèi)核的源碼。不少同學(xué)都比較好奇,我是怎么樣知道內(nèi)核運行過程中是要執(zhí)行到哪段代碼的。

簡單來說我主要是兩個方法。

  • 第一個方法和大家一樣,只要弄清楚一些核心函數(shù),剩下的使用 vscode 跳轉(zhuǎn)翻看,或者再搭配 Google 或者豆包。
  • 第二個方法就是使用 Linux tracing 相關(guān)的技術(shù),如 strace、tracepoint、kprobe 等方法。

關(guān)于 strace 的使用和原理我在揭開 strace 命令捕獲系統(tǒng)調(diào)用的神秘面紗 這一篇文章中講過了。今天我再來帶大家看看 tracepoint 、kprobe 如何使用,以及來深入了解下它們在 Linux 內(nèi)核中的的工作原理!

一、Linux tracing 簡介

在 Linux 高級應(yīng)用領(lǐng)域,跟蹤的英文原名叫 tracing,是一個非常大的話題。而且這個領(lǐng)域還在快速地迭代更新,近些年還涌現(xiàn)出了 eBPF 相關(guān)的各種工具和技術(shù)。

總體上來看,跟蹤可以分成事件源和工具兩類技術(shù)。其中事件源是底層事件的觸發(fā)和數(shù)據(jù)的提供者,工具對底層的事件進行處理,對用戶提供方便觀看的結(jié)果,甚至是友好的界面。

圖片圖片

在工具上有很多。其中火焰圖是一個業(yè)界非常常用的工具,可以把底層采樣來的數(shù)據(jù)渲染成直觀的圖片,方便進行性能問題排查。它底層又是依賴 perf、dtrace 等工具給它提供函數(shù)調(diào)用棧的采樣數(shù)據(jù)來工作的。類似的跟蹤工具還有 ftrace、trace-cmd,也包括最近幾年開始流行起來的 eBPF,以及在其上封裝出來的 BCC、bpftrace等工具。

但不管是上面的何種工具,都是依賴內(nèi)核和硬件底層提供的事件源來工作的。所以,我認為首先把這些事件源真正理解清楚更為重要。

在事件源上,分為硬件事件、軟件事件、tracepoint、kprobe、uprobe、USDT 等等。其中硬件事件和軟件事件在之前的文章中提到過。

  • 硬件事件:在人人都應(yīng)該知道的CPU緩存運行效率 這篇文章中介紹過,使用 perf list hw 可以查看當(dāng)前系統(tǒng)上支持的硬件事件指標(biāo)。大概包括 cpu-cycles 消耗的CPU周期、instructions  執(zhí)行的指令計數(shù)、L1-dcache-loads 一級數(shù)據(jù)緩存讀取次數(shù)、dTLB-loads dTLB緩存讀取次數(shù)等。
  • 軟件事件:在盤點內(nèi)核中常見的性能卡點 中提到,使用 perf list sw 可以查看當(dāng)前系統(tǒng)都支持哪些軟件性能事件。大概包括 context-switches 進程上下文切換、cpu-migrations 進程調(diào)度遷移、page-faults 缺頁中斷等。

事件源中剩下的 tracepoint 和 kprobe 是內(nèi)核態(tài)的跟蹤技術(shù),也就是本文中我要重點給大家講解的技術(shù)。另外 uprobe、USDT 屬于用戶態(tài)的跟蹤技術(shù)。

圖片圖片

tracepoint 是內(nèi)核在源碼中插了很多樁,當(dāng)沒有跟蹤需求的時候,這些樁都是關(guān)閉狀態(tài)。當(dāng)某個跟蹤點被打開的時候,樁代碼就會得以運行。通過這些樁代碼可以跟蹤到內(nèi)核的執(zhí)行過程。這些樁都是靜態(tài)的,都是在源碼里提前放置好了的。優(yōu)勢是比較穩(wěn)定。但內(nèi)核不可能在所有的函數(shù)中都插個樁,所以還有另外一類補充性的技術(shù)就是 kprobe。

kprobe 提供了對這對的動態(tài)插樁,這個動態(tài)表現(xiàn)在可以對任意內(nèi)核函數(shù)插樁代碼。它的缺點是不夠穩(wěn)定,而且要求內(nèi)核編譯時得開啟了 CONFIG_KPROBE_EVENT 選項才能工作。

在實際跟蹤內(nèi)核的時候,優(yōu)先選擇已有的 tracepoint 靜態(tài)跟蹤點,如果 tracepoint 不滿足需求的情況下,再結(jié)合 kprobe 來進行動態(tài)跟蹤。二者結(jié)合搭配著來用。

二、內(nèi)核源碼跟蹤

回到我們本文開篇時的話題,我是如何進行內(nèi)核源碼跟蹤的。 事實上,絕大部分的時間里我都靠直接用 IDE 提供的代碼跳轉(zhuǎn),還有命令行下的 grep 查找源碼就能解決絕大部分的問題了。 但確實有的時候需要查看一下內(nèi)核函數(shù)的實際調(diào)用過程。

今天咱們來演示下如何使用 ftrace、perf 等工具進行靜態(tài)跟蹤和動態(tài)跟蹤。

2.1 靜態(tài)跟蹤

先看靜態(tài)跟蹤。首先是查看下我們當(dāng)前系統(tǒng)都支持哪些靜態(tài)的跟蹤點。使用 ftrace 和 perf 工具都可以。

ftrace 是通過內(nèi)核在/sys/kernel/debug目錄下暴露出來的一套文件系統(tǒng)來和用戶交互的。它在 /sys/kernel/debug/tracing/events/ 目錄下列出了各個模塊

# ls /sys/kernel/debug/tracing/events/

圖片圖片

每個模塊下繼續(xù)查看,可以看到該模塊下所有可用的靜態(tài)跟蹤點。

# ls /sys/kernel/debug/tracing/events/sched/

圖片圖片

還可以使用 perf list 子命令查看,輸出的列表非常長,我把絕大部分都省略掉了。

# perf list tracepoint
List of pre-defined events (to be used in -e):
  alarmtimer:alarmtimer_cancel                       [Tracepoint event]
  alarmtimer:alarmtimer_fired                        [Tracepoint event]
  alarmtimer:alarmtimer_start                        [Tracepoint event]
  alarmtimer:alarmtimer_suspend                      [Tracepoint event]
  block:block_bio_backmerge                          [Tracepoint event]
  block:block_bio_bounce                             [Tracepoint event]
  block:block_bio_complete                           [Tracepoint event]
  block:block_bio_frontmerge                         [Tracepoint event]
  block:block_bio_queue                              [Tracepoint event]
  block:block_bio_remap                              [Tracepoint event]
  ......
sched:sched_switch                                 [Tracepoint event]
sched:sched_wait_task                              [Tracepoint event]
sched:sched_wake_idle_without_ipi                  [Tracepoint event]
sched:sched_wakeup                                 [Tracepoint event]
sched:sched_wakeup_new                             [Tracepoint event]
sched:sched_waking                                 [Tracepoint event]
  scsi:scsi_dispatch_cmd_done                        [Tracepoint event]
  .....

我們拿 sched:sched_switch 這個靜態(tài)跟蹤點來舉例。實際上,該跟蹤點在內(nèi)核中是在進程調(diào)度器的核心函數(shù) __schedule 中埋下的,就是我們下面列出的源碼中的 trace_sched_switch 這一行。這樣每次內(nèi)核執(zhí)行到 __schedule 的時候,都會調(diào)用該跟蹤點。

//file:kernel/sched/core.c
static void __sched notrace __schedule(bool preempt)
{
 ...
 trace_sched_switch(preempt, prev, next);
}

那找到可用的跟蹤點之后,下一步就是真正使用它來跟蹤。ftrace 、trace-cmd、perf 都可以跟蹤系統(tǒng)里的這些靜態(tài)跟蹤點。我們分別介紹一下這幾種工具的用法。

第一種是 ftrace。這個工具使用起來步驟雖然有點小麻煩,但是是所有工具中最接近底層的一個,所以我先介紹它。使用這個工具首先是要進入到 sched_switch 所在的偽文件目錄下來。然后給目錄下的 enable 寫入 1 表示打開該靜態(tài)跟蹤點。

# cd /sys/kernel/debug/tracing/events/sched/sched_switch
# echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable

然后訪問 cat 跟蹤 ftrace 下公用的 trace_pipe 就可以看到打印出來的內(nèi)核輸出了。在輸出的結(jié)果中可以看到觸發(fā)該靜態(tài)跟蹤點時的進程名、進程 pid、進程 prio 等數(shù)據(jù)。如下。

# cat /sys/kernel/debug/tracing/trace_pipe
<...>-1997939 [002] d... 19251337.694159: sched_switch: prev_comm=node prev_pid=1997939 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
<...>-1997960 [004] d... 19251337.694159: sched_switch: prev_comm=node prev_pid=1997960 prev_prio=120 prev_state=S ==> next_comm=swapper/4 next_pid=0 next_prio=120
<idle>-0       [002] d... 19251337.694166: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=node next_pid=1997939 next_prio=120
<...>-1997957 [003] d... 19251337.694170: sched_switch: prev_comm=node prev_pid=1997957 prev_prio=120 prev_state=S ==> next_comm=swapper/3 next_pid=0 next_prio=120
<idle>-0       [005] d... 19251337.694308: sched_switch: prev_comm=swapper/5 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=node next_pid=1997958 next_prio=120
......

跟蹤完畢后記得把這個跟蹤點的開關(guān)給關(guān)了。

# echo 0 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable

第二個工具是使用 perf 命令。前面咱們介紹了如何通過 perf list 查看支持的跟蹤點。如下所示,該命令的輸出也顯示當(dāng)前系統(tǒng)支持 sched:sched_switch 這個靜態(tài)跟蹤點。

# perf list 
...
sched:sched_switch

找到跟蹤點后,就可以使用 perf record 進行下一步的跟蹤了。perf record 會根據(jù) sched:sched_switch 跟蹤點時間來進行錄制,然后輸出到 perf.data 文件中。

# perf record -e 'sched:sched_switch' -a sleep 3

該文件需要使用 perf script 來進行解析,將跟蹤當(dāng)時的現(xiàn)場都打印出來。

# perf script
migration/0    11 [000] 337467.469254: sched:sched_switch: prev_comm=migration/0 prev_pid=11 prev_prio=0 prev_state=S ==> next_comm=swapper/0 next_pid=0 next_pri
perf 3979944 [001] 337467.469273: sched:sched_switch: prev_comm=perf prev_pid=3979944 prev_prio=120 prev_state=R+ ==> next_comm=migration/1 next_pid=15 ne
migration/1    15 [001] 337467.469290: sched:sched_switch: prev_comm=migration/1 prev_pid=15 prev_prio=0 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_pri
perf 3979944 [002] 337467.469307: sched:sched_switch: prev_comm=perf prev_pid=3979944 prev_prio=120 prev_state=R+ ==> next_comm=migration/2 next_pid=20 ne
migration/2    20 [002] 337467.469322: sched:sched_switch: prev_comm=migration/2 prev_pid=20 prev_prio=0 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_pri
perf 3979944 [003] 337467.469345: sched:sched_switch: prev_comm=perf prev_pid=3979944 prev_prio=120 prev_state=R+ ==> next_comm=migration/3 next_pid=25 ne
migration/3    25 [003] 337467.469371: sched:sched_switch: prev_comm=migration/3 prev_pid=25 prev_prio=0 prev_state=S ==> next_comm=swapper/3 next_pid=0 next_pri
perf 3979944 [004] 337467.469384: sched:sched_switch: prev_comm=perf prev_pid=3979944 prev_prio=120 prev_state=R+ ==> next_comm=migration/4 next_pid=30 ne
......

在調(diào)試跟蹤的時候,一般更有用的是把事件發(fā)生時的函數(shù)調(diào)用棧給記錄下來。用 perf record 命令時加個 -g 參數(shù)就可以了,就可以錄制時記錄調(diào)用棧信息。

# perf record -e 'sched:sched_switch' -a -g sleep 3
# perf script
redis-server 3990071 [002] 338407.091729: sched:sched_switch: prev_comm=redis-server prev_pid=3990071 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 ne
        ffffffff81788c99 __sched_text_start+0x3a9 (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
        ffffffff81788c99 __sched_text_start+0x3a9 (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
        ffffffff81789030 schedule+0x40 (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
        ffffffff8178d0e7 schedule_hrtimeout_range_clock+0x87 (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
        ffffffff8130602e ep_poll+0x44e (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
        ffffffff81306100 do_epoll_wait+0xb0 (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
        ffffffff8130613a __x64_sys_epoll_wait+0x1a (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
        ffffffff81004269 do_syscall_64+0x59 (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
        ffffffff8180008c entry_SYSCALL_64+0x7c (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
            7f9a753e221f epoll_wait+0x4f (/usr/lib/x86_64-linux-gnu/libc-2.28.so)
               100000000 [unknown] ([unknown])
......

我的這臺開發(fā)機部署了 redis,所以在錄制的時候抓到了 redis-server 進程觸發(fā) sched_switch 靜態(tài)跟蹤點時的調(diào)用棧情況。

靜態(tài)跟蹤點是靜態(tài)定義到內(nèi)核源碼中的。優(yōu)點是對系統(tǒng)運行影響比較小,穩(wěn)定性比較好。但它的缺點是不可能把所有內(nèi)核函數(shù)中都埋一個跟蹤點進去。要新增新的跟蹤點需要修改和重新編譯內(nèi)核,這顯然不是很靈活。

2.2 動態(tài)跟蹤

在內(nèi)核中,kprobes 是一種動態(tài)的跟蹤機制。它允許動態(tài)地插入代碼來監(jiān)視內(nèi)核中的大多數(shù)的函數(shù)。但缺點是由于太過于靈活,對系統(tǒng)的帶來的影響不像 tracepoint 那么可控。另外就是它需要內(nèi)核編譯時打開了 CONFIG_KPROBE_EVENT 選項才能用。

# cat /boot/config-5.4.56.bsk.10-amd64  | grep CONFIG_KPROBE_EVENT

我們來用一個例子,看看動態(tài)跟蹤 kprobes 如何使用。首先還是進入到 ftrace 根目錄 /sys/kernel/debug/tracing。操作 kprobe_events 文件就可以添加一個動態(tài)跟蹤點。格式是 "p:自定義的名字 函數(shù)名",如果要跟蹤內(nèi)核的 schedule 這個核心函數(shù),操作方法如下。

# cd /sys/kernel/debug/tracing
# echo 'p:yanfei schedule' >> kprobe_events

上面的例子中創(chuàng)建了一個名為 yanfei 的跟蹤點,這個時候會生成一個新的目錄,位于 events/kprobes/yanfei 路徑下。

# cd /sys/kernel/debug/tracing/
# ll events/kprobes/yanfei
total 0
-rw-r--r-- 1 root root 0 May 20 08:37 enable
-rw-r--r-- 1 root root 0 May 20 08:37 filter
-r--r--r-- 1 root root 0 May 20 08:37 format
-r--r--r-- 1 root root 0 May 20 08:37 id
-rw-r--r-- 1 root root 0 May 20 08:37 trigger

其中的 enable 是該跟蹤點的開關(guān),我們把它打開后,通過 cat /sys/kernel/debug/tracing/ trace_pipe 文件就可以看到動態(tài)跟蹤的輸出了。不過我覺得更有用的是用 perf 來采樣查看這個動態(tài)跟蹤點的函數(shù)調(diào)用棧。

在添加完這個跟蹤點后可以用 perf 命令看到這個跟蹤點。

# perf probe --list
kprobes:yanfei       (on schedule@kernel/sched/core.c)

接著使用 perf record 子命令進行錄制。然后使用 perf script 可以查看到調(diào)用的棧信息。

# perf record -e kprobes:yanfei -a -g sleep 1
# perf script
redis-server 3990071 [003] 341464.819710: kprobes:yanfei: (ffffffff81788ff0)
ffffffff81788ff1 schedule+0x1 (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
ffffffff8178d0e7 schedule_hrtimeout_range_clock+0x87 (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
ffffffff8130602e ep_poll+0x44e (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
ffffffff81306100 do_epoll_wait+0xb0 (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
ffffffff8130613a __x64_sys_epoll_wait+0x1a (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
ffffffff81004269 do_syscall_64+0x59 (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
ffffffff8180008c entry_SYSCALL_64+0x7c (/usr/lib/debug/boot/vmlinux-5.4.56.bsk.10-amd64)
    7f9a753e221f epoll_wait+0x4f (/usr/lib/x86_64-linux-gnu/libc-2.28.so)
       100000000 [unknown] ([unknown])

三、內(nèi)核跟蹤原理

在上一小節(jié)中我們介紹了靜態(tài)跟蹤和動態(tài)跟蹤分別都是怎么使用的。在這一小節(jié)中我們來看看它們分別是如何實現(xiàn)的,原理到底是什么。

3.1 靜態(tài)跟蹤點 tracepoint

靜態(tài)跟蹤點的入口是在每個要跟蹤的位置埋下的 trace_xxx 的函數(shù)。例如前面我們提到的,在 __schedule 路徑下執(zhí)行了 trace_sched_switch 這個靜態(tài)跟蹤點。

//file:kernel/sched/core.c
static void __sched notrace __schedule(bool preempt)
{
 ...
 trace_sched_switch(preempt, prev, next);
}

另外在源碼中還可以在多處搜到 register_trace_sched_switch 在這個靜態(tài)跟蹤點上注冊了一些鉤子函數(shù)。

kernel/trace/ftrace.c:  register_trace_sched_switch(ftrace_filter_pid_sched_switch_probe, tr);
kernel/trace/trace_sched_switch.c: ret = register_trace_sched_switch(probe_sched_switch, NULL);
kernel/trace/trace_sched_wakeup.c: ret = register_trace_sched_switch(probe_wakeup_sched_switch, NULL);
kernel/trace/fgraph.c:  ret = register_trace_sched_switch(ftrace_graph_probe_sched_switch, NULL);

這樣每當(dāng)內(nèi)核執(zhí)行到 __schedule 函數(shù)中的 trace_sched_switch 時,就會調(diào)用到所注冊的這些 ftrace_filter_pid_sched_switch_probe、probe_sched_switch、probe_wakeup_sched_switch、ftrace_graph_probe_sched_switch 等函數(shù)來完成整個靜態(tài)跟蹤過程。

但你在源碼里實際上根本搜不到 trace_sched_switch 和 register_trace_sched_switch 函數(shù)的實現(xiàn)。這是因為內(nèi)核并不是通過直接定義的方式來聲明和實現(xiàn)的 trace_xxx 跟蹤點函數(shù)。而是采用了炫技般的宏定義來做的。這些宏實現(xiàn)是挺復(fù)雜的,不用太扣細節(jié),我們來簡單了解下這個實現(xiàn)過程就行了。

內(nèi)核實現(xiàn)靜態(tài)跟蹤點的宏主要有三個,分別是:

  • DEFINE_TRACE:這個宏用來定義一個靜態(tài)跟蹤點
  • DECLARE_TRACE:這個宏用來聲明和實現(xiàn)這個靜態(tài)跟蹤點相關(guān)的各種 trace_xxx,register_trace_xxx 相關(guān)的函數(shù)
  • DO_TRACE:這個宏用來執(zhí)行通過 register_trace_xxx 注冊上來的鉤子函數(shù)

我們先來看 DEFINE_TRACE,我們把整個定義過程精簡了一下,如下:

//file:include/linux/tracepoint.h
#define DEFINE_TRACE(name)      \
 DEFINE_TRACE_FN(name, NULL, NULL);

#define DEFINE_TRACE_FN(name, reg, unreg)     \
 struct tracepoint __tracepoint_##name     \
 ......

可見,這個宏主要是定義了一個名為 __tracepoint_##name 的 struct tracepoint 類型的對象。其中 ##name 就是跟蹤點的名字。struct tracepoint 是一個內(nèi)核對象,它的定義如下

//file:include/linux/tracepoint-defs.h
struct tracepoint { 
 const char *name;  /* Tracepoint name */
 struct static_key key;
 int (*regfunc)(void);
 void (*unregfunc)(void);
 struct tracepoint_func __rcu *funcs;
};

其中每個成員含義如下

  • name:tracepoint的名字
  • key:tracepoint狀態(tài),1表示disable,0表示enable
  • regfunc:添加鉤子函數(shù)的函數(shù)
  • unregfunc:卸載鉤子函數(shù)的函數(shù)
  • funcs:tracepoint中所有的鉤子函數(shù)鏈表

再來看 DECLARE_TRACE 宏,它用來聲明和實現(xiàn)這個靜態(tài)跟蹤點相關(guān)的各種 trace_xxx,register_trace_xxx 相關(guān)的函數(shù)。我們簡單看下它的實現(xiàn)。

//file:include/linux/tracepoint.h
#define DECLARE_TRACE(name, proto, args)    \
 __DECLARE_TRACE(name, PARAMS(proto), PARAMS(args),  \
   cpu_online(raw_smp_processor_id()),  \
   PARAMS(void *__data, proto),   \
   PARAMS(__data, args))

好么,宏套宏,再繼續(xù)看 __DECLARE_TRACE。同樣為了方便你看,我精簡了很多

#define __DECLARE_TRACE(name, proto, args, cond, data_proto, data_args) \
 extern struct tracepoint __tracepoint_##name;   \
 static inline void trace_##name(proto)    \
 {        \
  //判斷trace point是否disable
  //如果開啟的話,就調(diào)用__DO_TRACE遍歷執(zhí)行trace point中的樁函數(shù)
  if (static_key_false(&__tracepoint_##name.key))  \
   __DO_TRACE(&__tracepoint_##name,  \
    TP_PROTO(data_proto),   \
    TP_ARGS(data_args),   \
    TP_CONDITION(cond), 0);   \
 }        \
 static inline int      \
 register_trace_##name(void (*probe)(data_proto), void *data) \
 {        \
  return tracepoint_probe_register(&__tracepoint_##name, \
      (void *)probe, data); \
 }        \
 ......

這個宏主要是聲明和實現(xiàn)了 trace_xxx 和 register_trace_xxx 相關(guān)的函數(shù)。這樣,我們前面看到的 trace_sched_switch 和 register_trace_sched_switch 就有了。

這里值得注意的是,如果靜態(tài)跟蹤點沒有開啟,trace_xxx 跟蹤的開銷非常的低。 trace_xxx 本身是個內(nèi)聯(lián)函數(shù),而且如果跟蹤點未開啟的話,直接 if 判斷開關(guān)沒開后就退出了。所以靜態(tài)跟蹤 tracepoint 在關(guān)閉狀態(tài)的時候?qū)?nèi)核的運行基本沒有啥影響。

如果開啟了某個靜態(tài)跟蹤點后,就會進入 __DO_TRACE 進行真正的跟蹤過程。

//file:include/linux/tracepoint.h
//運行實際的trace函數(shù)
#define __DO_TRACE(tp, proto, args, cond, rcuidle)   \
 do {
  ...
     it_func_ptr = rcu_dereference_raw((tp)->funcs);
     if (it_func_ptr) {
         do {
             it_func = (it_func_ptr)->func;
             __data = (it_func_ptr)->data;
             ((void(*)(proto))(it_func))(args);
         } while ((++it_func_ptr)->func);
     }
 }while(0)

__DO_TRACE 就是把 tracepoint 內(nèi)核對象中之前注冊的 funcs 拿出來都執(zhí)行了一遍。這樣每當(dāng)內(nèi)核執(zhí)行到 trace_sched_switch 時,就會調(diào)用到注冊的 ftrace_filter_pid_sched_switch_probe、probe_sched_switch、probe_wakeup_sched_switch、ftrace_graph_probe_sched_switch 這些鉤子函數(shù)了,進而完成整個靜態(tài)跟蹤過程。

這就是 tracepoint 的核心實現(xiàn)過程。

3.2 動態(tài)跟蹤 kprobes

靜態(tài)跟蹤 tracepoint 雖然是一大堆的宏的定義,但原理還是在想跟蹤的地方埋下了一個函數(shù)。而動態(tài)跟蹤 kprobes 的實現(xiàn)原理不是埋,而是直接替換了要跟蹤的函數(shù)的地址。

圖片圖片

kprobes 找到要跟蹤的指令,直接用一個 BREAKPOINT_INSTRUCTION 指令將其替換掉,并將原來的指令保存起來。后面內(nèi)核再次運行完上圖中的指令 1 后就會進入到 BREAKPOINT_INSTRUCTION 對應(yīng)的處理流程,處理完后仍然會調(diào)回到指令 2 繼續(xù)往下執(zhí)行。

kprobes 使用的核心是 register_kprobe 函數(shù)。在 samples/kprobes/kprobe_example.c 文件下有一個完整的使用例子。我把它簡化了一下

static struct kprobe kp;
// 注冊kprobe
static int __init my_module_init(void)
{
    int ret;

    kp.pre_handler = handler_pre;
    kp.post_handler = handler_post;
    kp.fault_handler = handler_fault;
    kp.symbol_name = "write";   // 指定要跟蹤的內(nèi)核符號

    ret = register_kprobe(&kp);
    if (ret < 0) {
        printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
        return ret;
    }

    printk(KERN_INFO "kprobe registered\n");
    return0;
}
module_init(my_module_init);

在這個示例中,就定義了一個 kprobe 動態(tài)跟蹤點,并指定要跟蹤 write 這個內(nèi)核符號,調(diào)用 register_kprobe 將其注冊到內(nèi)核上。這個注冊過程主要就是保存原來的函數(shù)地址,并替換相應(yīng)的指令為 BREAKPOINT_INSTRUCTION。

// file:kernel/kprobes.c
int register_kprobe(struct kprobe *p)
{
// 獲取探測點的地址
 addr = kprobe_addr(p);
 p->addr = addr;

// 保存原有的指令
 prepare_kprobe(p);

// 執(zhí)行指令替換
 arm_kprobe(p)
 ...
}

在 register_kprobe 函數(shù)中,核心的操作是以上三步,其余代碼都被我精簡掉了。

  • kprobe_addr:是根據(jù)符號來查找函數(shù)地址的
  • prepare_kprobe:是將原來的指令給保存起來
  • arm_kprobe:將指令替換掉

我們重點看 arm_kprobe 是如何將指令替換掉的。它會調(diào)用到 arch_arm_kprobe,而每個 CPU 架構(gòu)都有自己專用的 arch_arm_kprobe 函數(shù)的實現(xiàn),對于 x86 架構(gòu)來說,它的實現(xiàn)位于 arch/x86/kernel/kprobes/core.c 文件下。

//file: arch/x86/kernel/kprobes/core.c
void arch_arm_kprobe(struct kprobe *p)
{
 text_poke(p->addr, ((unsigned char []){BREAKPOINT_INSTRUCTION}), 1);
}

看簡單吧,x86 架構(gòu)調(diào)用 text_poke 就完成了替換。

當(dāng)后面內(nèi)核再次運行到到替換的 BREAKPOINT_INSTRUCTION 指令后將觸發(fā) INT3 中斷,進而調(diào)用到架構(gòu)相關(guān)的 kprobe_int3_handler。在這里,將會獲取到 kprobe 跟蹤點,發(fā)現(xiàn)它有 pre_handler,好,那就跟蹤之。

//file:arch/x86/kernel/kprobes/core.c
int kprobe_int3_handler(struct pt_regs *regs)
{
 // 獲取 kprobe
 p = get_kprobe(addr);

 // 執(zhí)行 pre_handler
 if (!p->pre_handler || !p->pre_handler(p, regs))
  setup_singlestep(p, regs, kcb, 0);
 ...
}

最后再把被替換的指令翻出來,讓內(nèi)核繼續(xù)運行??傮w上來說,內(nèi)核的 kprobe 的原理就是利用 BREAKPOINT_INSTRUCTION 半路截個胡,執(zhí)行自己想要的跟蹤函數(shù)后,再將處理流程還原回原來的指令繼續(xù)進行。

圖片圖片


四、總結(jié)

今天的文章中,咱們分了三部分來介紹。

首先第一部分咱們聊了聊 Linux 跟蹤技術(shù)這個話題。perf、ftrace、trace-cmd,eBPF,BCC、bpftrace 等工具都算是跟蹤這個范疇的工具。這些工具可以幫助我們觀測系統(tǒng)運行狀態(tài),幫助分析系統(tǒng)的性能瓶頸。不管是哪種工具,其實底層都是依賴 tracepoint、kprobe 等底層的機制來工作的。

第二部分我們演示一下如何跟蹤內(nèi)核函數(shù)調(diào)用,以及查看調(diào)用棧。這里我用到了 ftrace 和 perf 工具。perf 的 -g 選項不但能查看到函數(shù)的名字,而且還能追蹤其調(diào)用鏈路,非常實用。

第三部分我們從內(nèi)核視角聊了聊 tracepoint 和 kprobes 是如何實現(xiàn)的。這兩個技術(shù)聽起來唬人,但其實原理都非常的簡單。tracepoint 只不過是在內(nèi)核函數(shù)中插入了一些鉤子而已。kprobe 是在內(nèi)核運行過程中動態(tài)地替換要跟蹤的函數(shù)指令為 BREAKPOINT 指令。這個指令觸發(fā)一段運行邏輯執(zhí)行跟蹤工作后,再跳回原來的函數(shù)來執(zhí)行。僅此而已。

責(zé)任編輯:武曉燕 來源: 開發(fā)內(nèi)功修煉
相關(guān)推薦

2021-02-20 20:51:24

工具內(nèi)核kprobe

2021-02-09 08:17:05

內(nèi)核Kprobe函數(shù)

2020-06-03 21:29:53

BLELoRa室內(nèi)定位

2021-12-20 09:53:51

用戶態(tài)內(nèi)核態(tài)應(yīng)用程序

2024-09-12 17:19:43

YOLO目標(biāo)檢測深度學(xué)習(xí)

2025-07-14 00:10:01

2020-09-28 15:00:19

Linux容器虛擬化

2018-01-24 18:51:39

Linuxftrace內(nèi)核函數(shù)

2010-03-26 12:29:27

第二層路由技術(shù)

2024-10-10 17:05:00

2024-08-27 12:40:59

2023-07-13 11:24:14

SQL優(yōu)化賦值

2021-07-10 08:29:13

Docker內(nèi)核Namespace

2025-01-02 11:06:22

2021-03-06 22:41:06

內(nèi)核源碼CAS

2023-12-08 13:20:00

DDDSOA微服務(wù)

2022-02-08 23:59:12

USB接口串行

2017-07-19 11:11:40

CTS漏洞檢測原理淺析

2024-12-23 15:05:29

2022-11-07 07:54:05

微服務(wù)數(shù)據(jù)庫網(wǎng)關(guān)
點贊
收藏

51CTO技術(shù)棧公眾號