Linux 內核追蹤機制:性能監控與故障排查
在 Linux 系統的廣袤世界裏,內核猶如其靈魂所在,掌控着整個系統的運行。而 Linux 內核追蹤機制,作爲深入瞭解內核運行奧祕的關鍵工具,正發揮着日益重要的作用。想象一下,當你面對一個複雜的 Linux 系統問題時,如同置身於一座龐大而錯綜複雜的迷宮之中。此時,內核追蹤機制就像是手中的一張地圖,能幫助你清晰地看到系統內部的運行軌跡,讓你準確找到問題的根源。無論是排查性能瓶頸,還是追蹤系統故障,亦或是監測安全隱患,它都能大顯身手。
在當今這個數字化時代,隨着互聯網應用的蓬勃發展,Linux 系統在服務器、嵌入式設備、雲計算等衆多領域的應用愈發廣泛,其重要性不言而喻。而隨着應用的複雜度不斷攀升,對系統性能、穩定性和安全性的要求也越來越高。Linux 內核追蹤機制作爲深入瞭解系統運行的關鍵工具,能夠幫助我們更好地應對這些挑戰,爲系統的優化和維護提供有力支持。這便是我們深入探究這一機制的初衷。在接下來的文章中,我們將一步步揭開 Linux 內核追蹤機制的神祕面紗,深入剖析其原理、關鍵技術、實際應用以及未來的發展趨勢,幫助你全面掌握這一強大的工具,爲你的 Linux 技術之旅增添有力的助力。
一、追蹤機制簡介
1.1Linux 內核追蹤機制基礎
在 Linux 內核追蹤機制的領域中,幾個核心概念構成了其運作的基石。
探針(Probe)可謂是追蹤機制的 “偵察兵”。它就像是一個精心設置的 “陷阱”,被巧妙地安置在程序的特定位置,用於捕獲程序運行時的相關信息。探針主要分爲靜態探針和動態探針兩類。靜態探針如同提前部署好的崗哨,在代碼編譯階段就已被固定在特定位置;而動態探針則像是靈活的巡邏兵,能夠在程序運行過程中根據需要隨時 “上崗” 。例如,內核中的 kprobes 就是一種動態探針,它允許我們在運行時爲內核函數的任意位置添加探測點,從而獲取該函數執行時的上下文信息,如函數參數、寄存器狀態等。
跟蹤點(Tracepoint)則是內核中預先定義好的探測點,它們就像是在代碼中設置的一個個 “標記點”。這些標記點通常位於關鍵的代碼路徑上,比如系統調用的入口和出口、進程調度的關鍵環節等。當內核執行到這些跟蹤點時,與之關聯的處理函數就會被觸發,從而記錄下相關的事件信息。跟蹤點的優勢在於其對系統性能的影響極小,就像在路邊默默記錄車輛通行信息的監控攝像頭,不會對交通流造成明顯干擾。以網絡數據包的接收處理爲例,在相關的內核代碼中設置了跟蹤點,當數據包到達並被內核處理時,跟蹤點就能精確記錄下數據包的相關信息,爲網絡性能分析和故障排查提供重要依據。
事件(Event)是追蹤機制中的關鍵元素,它是對系統中發生的特定行爲或狀態變化的抽象描述。這些事件涵蓋了系統運行的方方面面,從硬件中斷的發生到軟件函數的調用,從內存的分配釋放到進程的創建銷燬,都可以被定義爲事件。每一個事件都攜帶了豐富的信息,例如事件發生的時間、相關的進程 ID、涉及的函數參數等。通過對這些事件的收集和分析,我們就能夠像閱讀一本詳細的系統運行日誌一樣,深入瞭解系統的運行狀態。例如,當系統發生內存不足的情況時,會觸發一系列與內存管理相關的事件,通過追蹤這些事件,我們可以清晰地看到是哪些進程佔用了大量內存,從而針對性地進行優化。
1.2 追蹤的意義
Linux 內核追蹤機制在多個關鍵領域發揮着不可替代的重要作用。
在性能優化方面,它就像是一位精準的 “性能分析師”。通過對系統中各種事件的細緻追蹤,我們能夠精準地定位到系統的性能瓶頸所在。例如,通過追蹤 CPU 的使用情況,我們可以發現哪些進程或函數佔用了大量的 CPU 時間,進而對這些熱點代碼進行優化。在內存管理方面,追蹤機制可以幫助我們找出內存泄漏的源頭,以及哪些內存分配操作頻繁發生,從而合理調整內存分配策略,提高內存的使用效率。想象一下,一個複雜的服務器應用程序在運行過程中出現了性能下降的情況,通過內核追蹤機制,我們可以像使用高精度的掃描儀一樣,對系統的各個角落進行掃描,快速找到導致性能問題的代碼片段,爲優化工作提供明確的方向。
在故障排查領域,它是一把萬能的 “問題診斷鑰匙”。當系統出現故障時,追蹤機制能夠提供詳細的系統運行記錄,幫助我們快速回溯故障發生的過程。例如,當系統突然崩潰時,通過查看之前追蹤到的事件信息,我們可以瞭解到在崩潰前系統執行了哪些操作,哪些模塊出現了異常。這就好比在偵破一起案件時,追蹤機制提供的信息就像是案件現場的各種線索,讓我們能夠逐步還原事件的真相,找出故障的根源。無論是硬件故障還是軟件錯誤,都能在追蹤機制的幫助下無所遁形。
從安全監控的角度來看,它又像是一位忠誠的 “安全衛士”。通過追蹤系統中的敏感操作和異常行爲,我們可以及時發現潛在的安全威脅。例如,追蹤機制可以監測到未經授權的系統調用、惡意軟件的活動跡象等。一旦發現異常,我們可以立即採取措施進行防範,如阻斷惡意進程的運行、加強系統的訪問控制等。在網絡安全日益重要的今天,Linux 內核追蹤機制爲保障系統的安全穩定運行提供了有力的支持。
二、主要追蹤技術與原理
2.1Kprobes 機制
Kprobes 作爲 Linux 內核追蹤領域的重要技術,猶如一位技藝精湛的 “調試大師”,能夠在不修改內核源碼的情況下,對內核函數進行動態探測,爲我們深入瞭解內核運行狀態提供了有力支持。
kprobes 主要包含兩種類型:kprobe 和 kretprobes,它們如同兩個緊密協作的 “偵察兵”,在不同位置發揮着探測作用。
kprobe 能夠在函數的任意位置注入 probe handler。當內核執行到被探測函數的指定位置時,就如同觸發了一個精心設置的 “機關”,與之關聯的 probe handler 會被立即調用。這使得我們可以在函數執行過程中的關鍵節點,獲取到函數的參數、當前寄存器狀態等重要信息。例如,在網絡數據包處理函數中,通過在數據校驗部分注入 kprobe,我們可以實時查看數據包的校驗值以及相關處理參數,爲網絡性能優化和故障排查提供關鍵線索。
kretprobes 則專注於在函數返回時發揮作用。它巧妙地在函數返回地址處設置探測點,當函數執行完畢準備返回時,就會觸發 kretprobes 註冊的 probe handler。這對於瞭解函數的返回值以及函數執行後的系統狀態變化非常有幫助。比如,在內存分配函數返回時,通過 kretprobes 我們可以得知分配的內存地址是否符合預期,以及內存分配操作對系統內存狀態的影響。
從實現原理的角度來看,內核爲 kprobes 的運作提供了一套嚴謹而精妙的機制。當我們想要註冊一個 kprobe 時,就如同向內核 “提交一份探測申請”。內核接收到申請後,會執行一系列操作。首先,它會將探測點對應的指令複製一份,小心翼翼地保存起來,這就像是爲原指令準備了一個 “備份”。然後,內核會對原指令進行 “改造”,將其首字節替換爲特定的 “斷點” 指令。在常見的 x86 平臺上,這個斷點指令通常是 int3 。
當 CPU 執行到這個被替換的斷點指令時,就如同觸發了一箇中斷信號,會進入到內核的斷點處理流程。在這個過程中,內核會迅速保存當前程序的狀態,包括寄存器、堆棧等重要信息,這些信息就像是程序運行的 “快照”,記錄了當時的關鍵狀態。隨後,內核通過 Linux 的 “notifier_call_chain” 機制,將 CPU 的控制權轉交給之前註冊的 kprobe 的 probe handler,同時把保存的寄存器、堆棧信息傳遞給它。probe handler 得以利用這些信息,進行特定的處理,比如記錄相關數據、進行條件判斷等。
在 probe handler 執行完畢後,內核會將 CPU 的 flag 寄存器的值設置爲 1,開啓單步執行模式。這就像是讓 CPU 以 “慢動作” 的方式執行原指令,每執行完一條指令,就會觸發一個 int1 異常。內核的中斷處理函數 “do_debug” 會檢查這個異常是否由 kprobe 引起。如果是,就會執行 post handler,完成後續的處理工作。最後,關閉單步執行模式,恢復程序的原始執行流,讓程序繼續順暢運行。整個過程就像是一場精心編排的 “舞蹈”,各個環節緊密配合,確保了 kprobes 能夠準確、高效地實現對內核函數的探測。
kprobes 機制如何實現注入 probe handler?
內核提供了一個 krpobe 註冊接口,當我們調用接口註冊一個 kprobe 在指定探測點注入 probe handler 時,內核會把探測點對應的指令複製一份,記錄下來,並且把探測點的指令的首字節替換爲「斷點」指令,在 x86 平臺上也就是 int3 指令。
cpu 執行斷點指令時,會觸發內核的斷點處理函數「do_int3」,它判斷是否爲 kprobe 引起的斷點,如果是 kprobe 機制觸發的斷點,會保存這個程序的狀態,比如寄存器、堆棧等信息,並通過 Linux 的「notifier_call_chain」機制,將 cpu 的使用權交給之前 kprobe 的 probe handler,同時會把內核所保存的寄存器、堆棧信息傳遞給 probe handler。
前面已經提到了,probe handler 分兩種類型,一種是 pre handler、一種是 post handler。pre handler 將首先被調用(如果有的話),pre handler 執行完成後,內核會將 cpu 的 flag 寄存器的值設置爲 1,開始單步執行原指令,單步執行是 cpu 的一個 debug 特性,當 cpu 執行完一個指令後便會產生一個 int1 異常,觸發中斷處理函數「do_debug」執行,do_debug 函數會檢查本次中斷是否爲 kprobe 引起,如果是的話,執行 post handler,執行完畢後關閉單步,恢復原始執行流。
kretprobe 探針很有意思,Kprobe 會在函數的入口處註冊一個 kprobe,當函數執行時,這個 krpobe 會把函數的返回地址暫存下來,並把它替換爲 trampoline 地址。
Kprobe 也會在 trampoline 註冊一個 kprobe,函數執行返回時,cpu 控制權轉移到 trampoline,此時又會觸發 trampoline 上的 kprobe 探針,繼續陷入中斷,並執行 probe handler。
爲什麼有了 kprobe 還需要 kretprobe?
Kprobe 在可以函數的任意位置插入 probe,理論上他也能實現 kretprobe 的功能,但是實際上會面臨幾個挑戰。
比如當我們在函數的最後一行代碼上注入探針,試圖使用 kprobe 實現 kretprobe 的效果,但是實際上這種方式並不好,函數可能會存在多個返回情況,比如不滿足 if 條件,發生異常等情況,此時代碼完全有可能不會執行最後一行代碼,而是在某個地方就返回了,也就意味着不會觸發探針執行。
kretprobe 的優勢就在於它可以穩定的在函數返回時觸發 probe handler 執行,無論函數是基於什麼情況下返回。
另外一方面 kprobe 雖然可以在函數的任意位置插入探針,但是實際情況下都是在函數入口處插入探針,因爲函數入口是有一條標準的指令序列 prologue 可以進行斷點替換,而函數內部的其他位置,可能會存在跳轉指令、循環指令等情況,指令序列不太規則,不方便做斷點替換。
2.2Uprobes 機制
Uprobes 機制是 Linux 內核追蹤技術中的重要一員,它與 Kprobes 機制有着相似之處,但也具備自身獨特的特點和應用場景。
Uprobes 也分爲 uprobes 和 uretprobes,和 Kprobes 從原理上來說基本上是類似的,通過斷點指令替換原指令實現注入 probe handler 的能力,並且他沒有 Kprobes 的黑名單限制。Uprobes 需要我們提供「探測點的偏移量」,探測點的偏移量是指從程序的起始虛擬內存地址到探測點指令的偏移量。我們可以通過一個簡單的例子來理解:
root@zfane-maxpower:~/traceing# cat hello.c
#include <stdio.h>
void test(){
printf("hello world");
}
int main() {
test();
return 0;
}
root@zfane-maxpower:~/traceing# gcc hello.c -o hello
通過 readelf 讀取程序的 ELF 信息,拿到程序的符號表、節表。符號表包含程序中所有的符號,例如全局變量、局部變量、函數、動態鏈接庫符號,以及符號對應的虛擬內存地址。
彙編語言是按照節來編寫程序的,例如. text 節、.data 節。每個節都包含程序中的特定數據或代碼,節表就是程序中各個節的信息表。
通過符號表可以拿到 hello 函數的虛擬內存地址,通過節表拿到. text 節的虛擬內存地址,以及. text 節相較於 ELF 起始地址的偏移量。
root@zfane-maxpower:~/traceing# readelf -s hello|grep test
36: 0000000000001149 31 FUNC GLOBAL DEFAULT 16 test
root@zfane-maxpower:~/traceing# readelf -S hello|grep .text
[16] .text PROGBITS 0000000000001060 00001060
那麼 test 函數的指令在 hello 二進制文件的偏移量就可以計算出來了。
offset=test 函數的虛擬地址 - .text 段的虛擬地址 + .text 端偏移量
offset= 0000000000001149 - 0000000000001060 + 00001060
offset= 0000000000001149
現在我們可以通過編寫內核模塊向二進制程序注入 probe handler 獲取數據了。
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uprobes.h>
#include <linux/namei.h>
#include <linux/string.h>
#include <linux/uaccess.h>
#define DEBUGGEE_FILE "/home/zfane/hello/hello"
#define DEBUGGEE_FILE_OFFSET (0x1149)
static struct inode *debuggee_inode;
static int uprobe_sample_handler(struct uprobe_consumer *con,
struct pt_regs *regs)
{
printk("handler is executed, arg0: %s\\n",regs->di);
return 0;
}
static int uprobe_sample_ret_handler(struct uprobe_consumer *con,
unsigned long func,
struct pt_regs *regs)
{
printk("ret_handler is executed\\n");
return 0;
}
static struct uprobe_consumer uc = {
.handler = uprobe_sample_handler,
.ret_handler = uprobe_sample_ret_handler
};
static int __init init_uprobe_sample(void)
{
int ret;
struct path path;
ret = kern_path(DEBUGGEE_FILE, LOOKUP_FOLLOW, &path);
if (ret) {
return -1;
}
debuggee_inode = igrab(path.dentry->d_inode);
path_put(&path);
ret = uprobe_register(debuggee_inode,
DEBUGGEE_FILE_OFFSET, &uc);
if (ret < 0) {
return -1;
}
printk(KERN_INFO "insmod uprobe_sample\\n");
return 0;
}
static void __exit exit_uprobe_sample(void)
{
uprobe_unregister(debuggee_inode,
DEBUGGEE_FILE_OFFSET, &uc);
printk(KERN_INFO "rmmod uprobe_sample\\n");
}
module_init(init_uprobe_sample);
module_exit(exit_uprobe_sample);
MODULE_LICENSE("GPL");
與 Kprobes 不同的是,Uprobes 沒有 Kprobes 所具有的黑名單限制,這使得它在應用上更加靈活。它主要用於對用戶空間的程序進行追蹤,爲我們深入瞭解用戶程序的運行狀態提供了有力手段。例如,在一個複雜的用戶應用程序中,我們可以利用 Uprobes 在特定函數的入口或出口設置探測點,監測函數的調用情況、參數傳遞以及返回值等信息。這對於排查用戶程序中的性能問題、邏輯錯誤等非常有幫助。
在實際應用場景中,當我們需要對某個用戶態的服務程序進行性能優化時,Uprobes 就可以大顯身手。通過在服務程序的關鍵函數上設置 uprobes,我們可以詳細瞭解函數的執行時間、調用頻率等信息,從而找出性能瓶頸所在。又如,在安全監測方面,我們可以利用 Uprobes 監測用戶程序中是否存在惡意的系統調用行爲,及時發現潛在的安全威脅。
2.3ftrace 機制
Ftrace 機制在 Linux 內核追蹤領域中佔據着舉足輕重的地位,它猶如一把多用途的 “瑞士軍刀”,爲內核開發者和系統管理員提供了豐富而強大的追蹤功能。
Ftrace 有兩層含義:
-
爲函數注入 probe handler 的函數跟蹤的機制;
-
基於 trace fs 和 event trace 機制的 trace 框架。我們前面已經瞭解了 kprobes、tracepoint 兩種注入 probe handler 的機制,而 Ftrace 又帶了一種新的實現方式:編譯時注入。
gcc 有一個編譯選項:-pg,當使用這個編譯選項編譯代碼時,他會在每一個函數的入口添加對 mcount 函數的調用,mcount 函數由 libc 提供,它的實現會根據具體的機器架構生成相應的代碼。一般情況下 mcount 函數會記錄當前函數的地址、耗時等信息,在程序執行結束後,生成一個. out 文件用於給 gprof 來做性能分析的。我們可以編譯一個 hello.c 文件查看彙編代碼中包含了 mcount 調用。
root@zfane-maxpower:~/traceing# cat hello.c
#include <stdio.h>
void test(){
printf("hello world");
}
int main() {
test();
return 0;
}
root@zfane-maxpower:~/traceing# gcc -pg -S hello.c
root@zfane-maxpower:~/traceing# cat hello.s
.file "hello.c"
.text
.section .rodata
.LC0:
.string "hello world"
.text
.globl test
.type test, @function
test:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
1: call *mcount@GOTPCREL(%rip) // 在這個地方添加了 mcount 調用
leaq .LC0(%rip), %rax
movq %rax, %rdi
movl $0, %eax
call printf@PLT
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size test, .-test
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
1: call *mcount@GOTPCREL(%rip) // 在這個地方添加了 mcount 調用
movl $0, %eax
call test
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
內核代碼的編譯是不依賴 libc 庫,而 ftrace 提供了一個 mcount 函數,在這個函數中實現 probe handler 的能力,如果所有的內核函數都在函數入口添加 mcount 調用,運行時會對性能造成極大的影響,我們之前介紹的 kprobes、tracepoint 都具備動態開啓和關閉的能力盡可能的減少對內核的影響,Ftrace 也不例外,他具備動態開啓某個函數的 probe handler 的能力,其實現思路有一點特別。
內核編譯時(設置 -pg 的編譯選項),在彙編階段生成. o 的目標文件,再調用 ftrace 在內核代碼包中放置的一個 Perl 腳本 Recordmcount.pl,他會掃描每一個目標文件,查找 mcount 函數調用的地址,並記錄到一個臨時的. s 文件中(一個目標文件對應一個. s 文件),查找完成後,將臨時的. s 文件編譯成. o 目標文件和原來的. o 文件鏈接到一起。
在編譯過程的鏈接階段,vmlinux.lds.h 把所有的 mcount_loc 端的內容放在 vmlinux 的. init.data 端,並聲明瞭兩個全局符號 start_mcount_loc 和 __stop_mcount_loc 來開啓和關閉 mcount 函數調用。
在內核啓動階段,會調用 ftrace_init 函數,在這個函數中,根據記錄的 mcount 函數偏移地址,把所有的 mcount 函數調用對應的指令修改爲 NOP 指令。ftrace_init 函數在 start_kernel 中調用,比 kerne__init 還要先執行,此時不會有任何內核代碼執行,修改指令不會有任何影響。
在對某個函數啓用 ftrace probe handler,會將 NOP 指令修改爲對 ftrace probe handler 的調用即可,和 kprobe trap 一樣的原理,找到需要被 trace 的函數,函數的 mcount 調用是 NOP 指令,把 NOP 指令的第一個字節改爲 int 3,也就是斷點指令,再把 NOP 指令調整爲 probe handler 的地址。
在內核 4.19 版本,提升了最低版本的 gcc 限制,最低可允許 gcc 4.6 版本編譯,gcc 4.6 版本支持 -mfentry 編譯參數,使用 fentry 的特殊函數調用作爲所有函數的第一條指令,他可以替代 mcount 函數調用,並且性能更好。
Ftrace 這種通過編譯參數注入的 probe handler 非常好用,編譯完成後,相當於各個內核函數都聲明瞭 tracepoint,在內核運行時可以動態打開和關閉。那我們能否可以只使用 Ftrace 的 probe handler 注入能力呢?也是可以的,他有一個新的名字叫 fprobe,在 2022 年合入內核代碼,他是 ftrace 的包裝器,可以僅使用 ftrace 的函數追蹤的功能。
#define pr_fmt(fmt) "%s: " fmt, __func__
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fprobe.h>
#include <linux/sched/debug.h>
#include <linux/slab.h>
#define BACKTRACE_DEPTH 16
#define MAX_SYMBOL_LEN 4096
static struct fprobe sample_probe;
static unsigned long nhit;
static char symbol[MAX_SYMBOL_LEN] = "kernel_clone";
module_param_string(symbol, symbol, sizeof(symbol), 0644);
MODULE_PARM_DESC(symbol, "Probed symbol(s), given by comma separated symbols or a wildcard pattern.");
static char nosymbol[MAX_SYMBOL_LEN] = "";
module_param_string(nosymbol, nosymbol, sizeof(nosymbol), 0644);
MODULE_PARM_DESC(nosymbol, "Not-probed symbols, given by a wildcard pattern.");
static bool stackdump = true;
module_param(stackdump, bool, 0644);
MODULE_PARM_DESC(stackdump, "Enable stackdump.");
static bool use_trace = false;
module_param(use_trace, bool, 0644);
MODULE_PARM_DESC(use_trace, "Use trace_printk instead of printk. This is only for debugging.");
static void show_backtrace(void)
{
unsigned long stacks[BACKTRACE_DEPTH];
unsigned int len;
len = stack_trace_save(stacks, BACKTRACE_DEPTH, 2);
stack_trace_print(stacks, len, 24);
}
static void sample_entry_handler(struct fprobe *fp, unsigned long ip, struct pt_regs *regs)
{
if (use_trace)
/*
* This is just an example, no kernel code should call
* trace_printk() except when actively debugging.
*/
trace_printk("Enter <%pS> ip = 0x%p\\n", (void *)ip, (void *)ip);
else
pr_info("Enter <%pS> ip = 0x%p\\n", (void *)ip, (void *)ip);
nhit++;
if (stackdump)
show_backtrace();
}
static void sample_exit_handler(struct fprobe *fp, unsigned long ip, struct pt_regs *regs)
{
unsigned long rip = instruction_pointer(regs);
if (use_trace)
/*
* This is just an example, no kernel code should call
* trace_printk() except when actively debugging.
*/
trace_printk("Return from <%pS> ip = 0x%p to rip = 0x%p (%pS)\\n",
(void *)ip, (void *)ip, (void *)rip, (void *)rip);
else
pr_info("Return from <%pS> ip = 0x%p to rip = 0x%p (%pS)\\n",
(void *)ip, (void *)ip, (void *)rip, (void *)rip);
nhit++;
if (stackdump)
show_backtrace();
}
static int __init fprobe_init(void)
{
char *p, *symbuf = NULL;
const char **syms;
int ret, count, i;
sample_probe.entry_handler = sample_entry_handler;
sample_probe.exit_handler = sample_exit_handler;
if (strchr(symbol, '*')) {
/* filter based fprobe */
ret = register_fprobe(&sample_probe, symbol,
nosymbol[0] == '\\0' ? NULL : nosymbol);
goto out;
} else if (!strchr(symbol, ',')) {
symbuf = symbol;
ret = register_fprobe_syms(&sample_probe, (const char **)&symbuf, 1);
goto out;
}
/* Comma separated symbols */
symbuf = kstrdup(symbol, GFP_KERNEL);
if (!symbuf)
return -ENOMEM;
p = symbuf;
count = 1;
while ((p = strchr(++p, ',')) != NULL)
count++;
pr_info("%d symbols found\\n", count);
syms = kcalloc(count, sizeof(char *), GFP_KERNEL);
if (!syms) {
kfree(symbuf);
return -ENOMEM;
}
p = symbuf;
for (i = 0; i < count; i++)
syms[i] = strsep(&p, ",");
ret = register_fprobe_syms(&sample_probe, syms, count);
kfree(syms);
kfree(symbuf);
out:
if (ret < 0)
pr_err("register_fprobe failed, returned %d\\n", ret);
else
pr_info("Planted fprobe at %s\\n", symbol);
return ret;
}
static void __exit fprobe_exit(void)
{
unregister_fprobe(&sample_probe);
pr_info("fprobe at %s unregistered. %ld times hit, %ld times missed\\n",
symbol, nhit, sample_probe.nmissed);
}
module_init(fprobe_init)
module_exit(fprobe_exit)
MODULE_LICENSE("GPL");
Ftrace 具備極其廣泛的功能用途,能夠跟蹤多種類型的事件,爲我們全方位瞭解內核運行狀態提供了可能。它可以對函數調用進行細緻入微的跟蹤,無論是內核函數之間的層層調用,還是系統調用的觸發過程,都能被 ftrace 清晰地記錄下來。通過分析這些函數調用信息,我們就像繪製一幅詳細的地圖一樣,能夠清晰地瞭解代碼的執行路徑,明確各個函數在何時被調用,以及它們之間的調用關係。這對於優化代碼結構、提高程序性能具有重要意義。
同時,ftrace 還能對系統調用、中斷事件、定時器事件等進行跟蹤。在系統調用方面,它可以記錄系統調用的參數、返回值以及調用的時間點,幫助我們分析系統調用的性能和行爲。對於中斷事件,ftrace 能夠捕捉到中斷髮生的時刻、中斷源以及中斷處理函數的執行情況,這對於排查硬件相關的問題非常關鍵。在定時器事件方面,它可以監測定時器的觸發時間、定時任務的執行情況,有助於優化系統的時間管理機制。
在性能分析領域,ftrace 更是發揮着不可替代的作用。通過對各種事件的跟蹤數據進行深入分析,我們可以精準地識別出系統中的瓶頸所在。例如,通過分析函數調用的耗時,我們可以找出那些執行時間較長的函數,進而對這些函數進行優化,提高系統的整體性能。
Ftrace 的實現原理基於一種巧妙的代碼插樁技術。在編譯內核時,就如同在程序中預埋了許多 “傳感器”,ftrace 會在每個函數入口插入函數探針。這些探針就像是一個個 “小哨兵”,在函數實際運行之前,它們會被觸發,用於記錄本次函數調用的相關信息。爲了最大程度減少對系統運行效率的影響,ftrace 在不使用時會採取一種智能的策略,將函數探針指令替換爲 NOP(無操作指令),這就像是讓 “小哨兵” 暫時休息,從而降低系統的性能開銷。而當需要使用 ftrace 進行追蹤時,又能動態地將 NOP 替換爲函數探針,迅速啓動追蹤功能。
具體來說,在初始化階段,ftrace 會對_mcount_loc 段進行掃描,並將其中的函數探針指令全部修改爲 NOP。當我們需要開啓特定的追蹤功能時,ftrace 會根據配置,將需要跟蹤的函數對應的 NOP 指令再次替換爲能夠跳轉到特定執行探測操作代碼的指令。這種通過二級指針概念實現的快速切換機制,使得 ftrace 能夠在不影響系統性能的前提下,靈活地實現不同類型探針的啓用和切換。此外,ftrace 還支持動態配置,用戶可以根據實際需求,輕鬆地實現不同探針的切換和配置,以滿足各種複雜的追蹤場景。
三、追蹤工具詳解
3.1perf 工具
perf 是一款功能強大且靈活的性能分析工具,猶如一位全能的 “性能診斷專家”,在 Linux 系統中佔據着重要地位。它被廣泛應用於 CPU 分析、事件檢測等諸多關鍵領域,爲我們深入瞭解系統性能提供了有力支持。
在 CPU 分析方面,perf 能夠精準地剖析 CPU 的使用情況。例如,當我們遇到 CPU 利用率過高的問題時,perf 可以通過其豐富的功能,幫助我們找出是哪些進程或函數在 “霸佔” CPU 資源。通過 perf top 命令,我們可以實時查看各個函數或進程對 CPU 的佔用比例,就像擁有了一個實時的 CPU 使用 “監視器”。在一個多進程運行的服務器系統中,使用 perf top 發現某個特定的服務進程佔用了大量 CPU 時間,進一步分析發現是該進程中的某個算法函數存在性能問題,經過優化後,CPU 的利用率顯著降低。
perf 還可以對 CPU 緩存命中率進行分析。通過監測緩存命中和未命中的次數,我們能夠了解到程序對緩存的使用效率。如果緩存命中率過低,就意味着程序在頻繁地訪問內存,這會大大降低系統的運行速度。藉助 perf 的分析結果,我們可以優化程序的內存訪問模式,提高緩存命中率,從而提升系統性能。
在事件檢測方面,perf 支持對各種硬件和軟件事件的檢測。它可以監測硬件事件,如 CPU 的週期數、指令執行數等;也能監測軟件事件,像系統調用的次數、進程上下文切換的頻率等。通過對這些事件的細緻分析,我們可以全面瞭解系統的運行狀態。例如,通過監測系統調用的次數和類型,我們可以判斷系統中哪些應用程序在頻繁地進行系統調用,進而分析這些調用是否必要,是否存在優化的空間。
3.2ftrace 工具集
ftrace 工具集是 Linux 內核追蹤領域的瑰寶,它包含了多種功能強大的追蹤器,爲我們深入探究內核運行機制提供了豐富的手段。
function 追蹤器就像是一位敏銳的 “函數調用觀察者”,它專注於跟蹤函數的調用情況。通過它,我們可以清晰地瞭解到在系統運行過程中,哪些函數被調用了,以及它們的調用順序。在分析一個複雜的內核模塊時,使用 function 追蹤器可以幫助我們梳理出函數之間的調用關係,明確各個函數在整個模塊執行過程中的作用。例如,在網絡數據包處理的內核代碼中,function 追蹤器能夠準確記錄下從數據包接收函數到最終處理函數的一系列調用過程,讓我們對數據包的處理流程一目瞭然。
function_graph 追蹤器則更進一步,它以圖形化的方式展示函數調用關係,如同繪製了一幅詳細的 “函數調用地圖”。它不僅能顯示函數之間的調用關係,還能呈現出函數的執行時間,這對於我們分析代碼的執行效率非常有幫助。在一個大型的應用程序中,通過 function_graph 追蹤器,我們可以直觀地看到哪些函數調用鏈耗時較長,從而快速定位到性能瓶頸所在。比如,在一個數據庫管理系統中,使用 function_graph 追蹤器發現某個特定的查詢處理函數調用鏈花費了大量時間,進一步分析發現是其中一個子函數的算法效率較低,經過優化後,查詢性能得到了顯著提升。
使用 ftrace 工具集時,我們首先需要掛載 debugfs 文件系統,這就像是爲我們打開了一扇通往 ftrace 功能的大門。然後,通過修改 /sys/kernel/debug/tracing/ 目錄下的相關文件,我們可以輕鬆地配置和使用不同的追蹤器。例如,要使用 function 追蹤器,我們只需將 “function” 寫入 current_tracer 文件;若要使用 function_graph 追蹤器,則將 “function_graph” 寫入該文件即可。同時,我們還可以通過設置 set_ftrace_filter 文件來指定要追蹤的函數,進一步細化追蹤的範圍。
3.3ply 工具
ply 工具是基於 BPF(Berkeley Packet Filter)技術構建的,它猶如一把輕巧而鋒利的 “追蹤利刃”,能夠在不影響系統性能的前提下,實現對內核行爲的深度追蹤。
ply 的追蹤原理基於其獨特的設計理念。它將用戶編寫的類似 C 語言風格的腳本,直接編譯成作用於內核的 BPF 程序,這一過程就像是將我們的追蹤指令精準地 “嵌入” 到內核中。通過巧妙地結合 kprobes 和 tracepoints,ply 可以在內核的任意點上靈活地附加探針,從而實現對內核行爲的全方位監測。
在性能診斷場景中,ply 能夠快速識別出系統性能瓶頸的根源。例如,當文件系統讀取效率低下時,ply 可以通過追蹤相關的系統調用和內核函數,分析出是哪些操作導致了讀取速度緩慢。通過執行 “ply 'kretprobe:vfs_read { @["size"] = quantize (retval); }'” 這樣的命令,我們可以獲取到 read (2) 系統調用返回大小的分佈情況,從而判斷文件讀取的性能狀況。
在錯誤分析方面,ply 也發揮着重要作用。當 VFS 讀操作出現失敗時,ply 可以通過追蹤相關事件,幫助我們迅速定位到導致錯誤的進程。例如,使用 “ply 'kretprobe:vfs_read if (retval < 0) { @[pid, comm, retval] = count (); }'” 命令,能夠找出在讀取 VFS 時遇到錯誤的進程,爲解決問題提供關鍵線索。
在網絡行爲監測方面,ply 同樣表現出色。它可以跟蹤數據包的發送源,以及處理 TCP 重置請求等網絡行爲。通過執行 “ply 'kprobe:dev_queue_xmit { @[stack] = count (); }'” 命令,我們能夠了解數據包發送時的相關信息;而 “ply 'tracepoint:tcp/tcp_receive_reset {printf ("saddr:% v port:% v->% v\n",data->saddr, data->sport, data->dport);}'” 命令,則可以幫助我們解析接收 TCP 重置包的主機和端口信息,從而加強網絡安全監控 。
四、應用案例分析
4.1 性能優化案例
在某大型電商平臺的服務器集羣中,隨着業務量的迅猛增長,系統響應速度逐漸變慢,嚴重影響了用戶體驗。技術團隊迅速展開排查,藉助 Linux 內核追蹤機制,精準定位到問題的關鍵所在。
他們首先利用 perf 工具對系統進行全面監測。通過 perf top 命令,實時查看各個函數和進程對 CPU 的佔用情況,發現一個負責訂單處理的核心進程佔用了大量 CPU 資源。進一步深入分析,藉助 perf record 和 perf report 命令,詳細剖析該進程的函數調用棧,發現其中一個複雜的算法函數在處理大量訂單數據時,執行效率低下,成爲了性能瓶頸。
爲了更深入瞭解該函數的執行細節,技術團隊運用 ftrace 工具集的 function_graph 追蹤器。這一追蹤器以直觀的圖形化方式展示了函數之間的調用關係以及執行時間,讓技術人員清晰地看到該算法函數在整個訂單處理流程中被頻繁調用,且單次執行時間較長。
基於這些追蹤結果,技術團隊對該算法函數進行了針對性優化。他們優化了算法邏輯,減少了不必要的計算步驟,同時合理調整了數據結構,提高了數據訪問效率。經過優化後,再次使用 perf 和 ftrace 工具進行測試,發現該算法函數的執行時間大幅縮短,CPU 的利用率顯著降低,系統的響應速度得到了極大提升,成功解決了性能瓶頸問題,爲電商平臺的穩定運行和業務拓展提供了有力保障。
4.2 故障排查案例
在一個關鍵的生產環境中,Linux 服務器突然出現頻繁死機的嚴重故障,導致業務中斷,造成了巨大的損失。運維團隊迅速啓動緊急預案,利用 Linux 內核追蹤機制展開全面排查。
首先,他們查看了系統日誌,發現大量與內存相關的錯誤信息,但這些信息僅能提供一些表面線索,無法明確問題的根源。爲了深入瞭解系統死機前的運行狀態,運維團隊使用了 kprobes 機制。通過在內存管理相關的關鍵函數上設置 kprobe,他們能夠實時捕獲這些函數在執行過程中的參數和返回值等重要信息。
經過一段時間的監測,發現每當系統進行大規模內存分配操作時,就會出現異常情況。進一步使用 ftrace 工具集的 function 追蹤器,詳細跟蹤內存分配函數的調用流程,發現一個內核模塊在內存分配過程中,存在內存泄漏的問題。該模塊在頻繁申請內存後,未能及時釋放不再使用的內存,導致系統內存逐漸耗盡,最終引發死機。運維團隊立即對該內核模塊進行修復,確保內存的正確分配和釋放。修復完成後,經過長時間的穩定性測試,服務器再也沒有出現死機的情況,系統恢復了正常運行,保障了業務的連續性。
Linux 內核追蹤機制作爲深入瞭解 Linux 系統運行奧祕的關鍵工具,其重要性不言而喻。通過對探針、跟蹤點、事件等核心概念的深入理解,我們掌握了其運作的基礎。Kprobes、Uprobes 和 ftrace 等主要追蹤技術,爲我們提供了從不同角度、不同層面探測內核運行狀態的有力手段,它們各自獨特的工作方式和實現原理,構成了內核追蹤機制的豐富內涵。
perf、ftrace 工具集、ply 工具等衆多實用的追蹤工具,極大地便利了我們對內核的追蹤和分析工作。它們在性能優化、故障排查、安全監控等實際應用中發揮着巨大作用,通過具體的應用案例,我們清晰地看到了這些工具如何幫助我們解決複雜的系統問題,提升系統的性能和穩定性。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/TMQjHFLctuAWwOJzS2Rgow