chinese直男口爆体育生外卖, 99久久er热在这里只有精品99, 又色又爽又黄18禁美女裸身无遮挡, gogogo高清免费观看日本电视,私密按摩师高清版在线,人妻视频毛茸茸,91论坛 兴趣闲谈,欧美 亚洲 精品 8区,国产精品久久久久精品免费

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線(xiàn)課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

萬(wàn)字長(zhǎng)文解讀Linux內(nèi)核追蹤機(jī)制

dyquk4xk2p3d ? 來(lái)源:infoQ ? 2023-06-11 11:05 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

Linux 存在眾多 tracing tools,比如 ftrace、perf,他們可用于內(nèi)核的調(diào)試、提高內(nèi)核的可觀測(cè)性。眾多的工具也意味著繁雜的概念,諸如 tracepoint、trace events、kprobe、eBPF 等,甚至讓人搞不清楚他們到底是干什么的。本文嘗試?yán)砬暹@些概念。

注入 Probe 的機(jī)制 Probe Handler

如果我們想要追蹤內(nèi)核的一個(gè)函數(shù)或者某一行代碼,查看執(zhí)行的上下文和執(zhí)行情況,通用的做法是在代碼或函數(shù)的執(zhí)行前后 printk 打印日志,然后通過(guò)日志來(lái)查看追蹤信息。但是這種方式需要重新編譯內(nèi)核并重啟,非常麻煩。如果是在生產(chǎn)環(huán)境排查問(wèn)題,這種方式也是無(wú)法接受的。

一種比較合理的方式是在內(nèi)核正常運(yùn)行時(shí),自定義一個(gè)函數(shù),注入到我們想要追蹤的內(nèi)核函數(shù)執(zhí)行前后,當(dāng)內(nèi)核函數(shù)執(zhí)行時(shí)觸發(fā)我們定義的函數(shù),我們?cè)诤瘮?shù)中實(shí)現(xiàn)獲取我們想要的上下文信息并保存下來(lái)。同時(shí)因?yàn)樵黾恿藘?nèi)核函數(shù)的執(zhí)行流程,我們定義的函數(shù)最好是需要的時(shí)候開(kāi)啟,不需要的時(shí)候關(guān)閉,避免對(duì)內(nèi)核函數(shù)造成影響。

這個(gè)自定義的函數(shù)就是 probe handler,注入 probe handler 的地方被稱(chēng)為探測(cè)點(diǎn)或者 Hook 點(diǎn),在探測(cè)點(diǎn)前執(zhí)行的 probe handler 叫 pre handler, 執(zhí)行后的叫 post handler,注入 probe handler 的方式被稱(chēng)為“插樁”,內(nèi)核提供了多種 probe handler 注入機(jī)制。接下來(lái)我們聊一聊他們是如何實(shí)現(xiàn)在內(nèi)核運(yùn)行時(shí)注入 probe handler。

Kprobes 機(jī)制

Kprobes 是一個(gè)動(dòng)態(tài) tracing 機(jī)制,能夠動(dòng)態(tài)的注入到內(nèi)核的任意函數(shù)中的任意地方,采集調(diào)試信息和性能信息,并且不影響內(nèi)核的運(yùn)行。Kprobes 有兩種類(lèi)型:kprobes、kretprobes。kprobes 用于在內(nèi)核函數(shù)的任意位置注入 probe handler,kretprobes 用于在函數(shù)返回位置注入 probe handler。出于安全性考慮,在內(nèi)核代碼中,并非所有的函數(shù)都能“插樁”,kprobe 維護(hù)了一個(gè)黑名單記錄了不允許插樁的的函數(shù),比如 kprobe 自身,防止遞歸調(diào)用。

kprobes 機(jī)制如何實(shí)現(xiàn)注入 probe handler

內(nèi)核提供了一個(gè) krpobe 注冊(cè)接口,當(dāng)我們調(diào)用接口注冊(cè)一個(gè) kprobe 在指定探測(cè)點(diǎn)注入 probe handler 時(shí),內(nèi)核會(huì)把探測(cè)點(diǎn)對(duì)應(yīng)的指令復(fù)制一份,記錄下來(lái),并且把探測(cè)點(diǎn)的指令的首字節(jié)替換為「斷點(diǎn)」指令,在 x86 平臺(tái)上也就是 int3 指令。

cpu 執(zhí)行斷點(diǎn)指令時(shí),會(huì)觸發(fā)內(nèi)核的斷點(diǎn)處理函數(shù)「do_int3」,它判斷是否為 kprobe 引起的斷點(diǎn),如果是 kprobe 機(jī)制觸發(fā)的斷點(diǎn),會(huì)保存這個(gè)程序的狀態(tài),比如寄存器、堆棧等信息,并通過(guò) Linux 的「notifier_call_chain」機(jī)制,將 cpu 的使用權(quán)交給之前 kprobe 的 probe handler,同時(shí)會(huì)把內(nèi)核所保存的寄存器、堆棧信息傳遞給 probe handler。

前面已經(jīng)提到了,probe handler 分兩種類(lèi)型,一種是 pre handler、一種是 post handler。pre handler 將首先被調(diào)用(如果有的話(huà)),pre handler 執(zhí)行完成后,內(nèi)核會(huì)將 cpu 的 flag 寄存器的值設(shè)置為 1,開(kāi)始單步執(zhí)行原指令,單步執(zhí)行是 cpu 的一個(gè) debug 特性,當(dāng) cpu 執(zhí)行完一個(gè)指令后便會(huì)產(chǎn)生一個(gè) int1 異常,觸發(fā)中斷處理函數(shù)「do_debug」執(zhí)行,do_debug 函數(shù)會(huì)檢查本次中斷是否為 kprobe 引起,如果是的話(huà),執(zhí)行 post handler,執(zhí)行完畢后關(guān)閉單步,恢復(fù)原始執(zhí)行流。

1a34dbae-07fd-11ee-962d-dac502259ad0.png

kretprobe 探針很有意思,Kprobe 會(huì)在函數(shù)的入口處注冊(cè)一個(gè) kprobe,當(dāng)函數(shù)執(zhí)行時(shí),這個(gè) krpobe 會(huì)把函數(shù)的返回地址暫存下來(lái),并把它替換為 trampoline 地址。

Kprobe 也會(huì)在 trampoline 注冊(cè)一個(gè) kprobe,函數(shù)執(zhí)行返回時(shí),cpu 控制權(quán)轉(zhuǎn)移到 trampoline,此時(shí)又會(huì)觸發(fā) trampoline 上的 kprobe 探針,繼續(xù)陷入中斷,并執(zhí)行 probe handler。

為什么有了 kprobe 還需要 kretprobe?

Kprobe 在可以函數(shù)的任意位置插入 probe,理論上他也能實(shí)現(xiàn) kretprobe 的功能,但是實(shí)際上會(huì)面臨幾個(gè)挑戰(zhàn)。

比如當(dāng)我們?cè)诤瘮?shù)的最后一行代碼上注入探針,試圖使用 kprobe 實(shí)現(xiàn) kretprobe 的效果,但是實(shí)際上這種方式并不好,函數(shù)可能會(huì)存在多個(gè)返回情況,比如不滿(mǎn)足 if 條件,發(fā)生異常等情況,此時(shí)代碼完全有可能不會(huì)執(zhí)行最后一行代碼,而是在某個(gè)地方就返回了,也就意味著不會(huì)觸發(fā)探針執(zhí)行。

kretprobe 的優(yōu)勢(shì)就在于它可以穩(wěn)定的在函數(shù)返回時(shí)觸發(fā) probe handler 執(zhí)行,無(wú)論函數(shù)是基于什么情況下返回。

另外一方面 kprobe 雖然可以在函數(shù)的任意位置插入探針,但是實(shí)際情況下都是在函數(shù)入口處插入探針,因?yàn)楹瘮?shù)入口是有一條標(biāo)準(zhǔn)的指令序列 prologue 可以進(jìn)行斷點(diǎn)替換,而函數(shù)內(nèi)部的其他位置,可能會(huì)存在跳轉(zhuǎn)指令、循環(huán)指令等情況,指令序列不太規(guī)則,不方便做斷點(diǎn)替換。

Uprobes

Uprobes 也分為 uprobes 和 uretprobes,和 Kprobes 從原理上來(lái)說(shuō)基本上是類(lèi)似的,通過(guò)斷點(diǎn)指令替換原指令實(shí)現(xiàn)注入 probe handler 的能力,并且他沒(méi)有 Kprobes 的黑名單限制。Uprobes 需要我們提供「探測(cè)點(diǎn)的偏移量」,探測(cè)點(diǎn)的偏移量是指從程序的起始虛擬內(nèi)存地址到探測(cè)點(diǎn)指令的偏移量。我們可以通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)理解:

root@zfane-maxpower:~/traceing# cat hello.c
#include 
void test(){
    printf("hello world");
}
int main() {
    test();
    return 0;
}
root@zfane-maxpower:~/traceing# gcc hello.c -o hello

通過(guò) readelf 讀取程序的 ELF 信息,拿到程序的符號(hào)表、節(jié)表。符號(hào)表包含程序中所有的符號(hào),例如全局變量、局部變量、函數(shù)、動(dòng)態(tài)鏈接庫(kù)符號(hào),以及符號(hào)對(duì)應(yīng)的虛擬內(nèi)存地址。

匯編語(yǔ)言是按照節(jié)來(lái)編寫(xiě)程序的,例如.text 節(jié)、.data 節(jié)。每個(gè)節(jié)都包含程序中的特定數(shù)據(jù)或代碼,節(jié)表就是程序中各個(gè)節(jié)的信息表。

通過(guò)符號(hào)表可以拿到 hello 函數(shù)的虛擬內(nèi)存地址,通過(guò)節(jié)表拿到.text 節(jié)的虛擬內(nèi)存地址,以及.text 節(jié)相較于 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 函數(shù)的指令在 hello 二進(jìn)制文件的偏移量就可以計(jì)算出來(lái)了。

offset=test 函數(shù)的虛擬地址 -  .text 段的虛擬地址 + .text 端偏移量
offset= 0000000000001149 - 0000000000001060 + 00001060
offset= 0000000000001149

現(xiàn)在我們可以通過(guò)編寫(xiě)內(nèi)核模塊向二進(jìn)制程序注入 probe handler 獲取數(shù)據(jù)了。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#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");
Tracepoint

Tracepoint 是一個(gè)靜態(tài)的 tracing 機(jī)制,開(kāi)發(fā)者在內(nèi)核的代碼里的固定位置聲明了一些 Hook 點(diǎn),通過(guò)這些 hook 點(diǎn)實(shí)現(xiàn)相應(yīng)的追蹤代碼插入,一個(gè) Hook 點(diǎn)被稱(chēng)為一個(gè) tracepoint。

tracepoint 有開(kāi)啟和關(guān)閉兩種狀態(tài),默認(rèn)處于關(guān)閉狀態(tài),對(duì)內(nèi)核產(chǎn)生的影響非常小,只是增加了極少的時(shí)間開(kāi)銷(xiāo)(一個(gè)分支條件判斷),極小的空間開(kāi)銷(xiāo)(一條函數(shù)調(diào)用語(yǔ)句和幾個(gè)數(shù)據(jù)結(jié)構(gòu))。

在 x86 環(huán)境下,內(nèi)核代碼編譯后,關(guān)閉狀態(tài)的 tracepoint 代碼對(duì)應(yīng)的 cpu 指令是:nop 指令,

啟用 tracepoint 時(shí),通過(guò) Linux 內(nèi)核提供的 static jump patch 靜態(tài)跳轉(zhuǎn)補(bǔ)丁機(jī)制,nop 指令會(huì)被替換為 jmp 指令,jmp 指令將 cpu 的使用權(quán)轉(zhuǎn)移給 static_call 靜態(tài)跳轉(zhuǎn)函數(shù),這個(gè)函數(shù)會(huì)遍歷 tracepoint probe handler 數(shù)組獲取當(dāng)前 tracepoint 注冊(cè)的 probe handler,并進(jìn)一步跳轉(zhuǎn)到 probe handler 執(zhí)行,probe handler 執(zhí)行完成后,再通過(guò) jmp 指令跳轉(zhuǎn)回原函數(shù)繼續(xù)執(zhí)行。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


static void probe_sched_switch(void *ignore, bool preempt,
                               struct task_struct *prev, struct task_struct *next,
                               unsigned int prev_state) {
    pr_info("probe_sched_switch: pid [%d] -> [%d] \n",prev->tgid, next->tgid);
}


struct tracepoints_table {
    const char *name;
    void *fct;
    struct tracepoint *value;
    char init;
};


struct tracepoints_table interests[] = {{.name = "sched_switch", .fct = probe_sched_switch}};


#define FOR_EACH_INTEREST(i) \
    for (i = 0; i < sizeof(interests) / sizeof(struct tracepoints_table); i++)


static void lookup_tracepoints(struct tracepoint *tp, void *ignore) {
    int i;
    FOR_EACH_INTEREST(i) {
        if (strcmp(interests[i].name, tp->name) == 0) interests[i].value = tp;
    }
}


static void cleanup(void) {
    int i;


    // Cleanup the tracepoints
    FOR_EACH_INTEREST(i) {
        if (interests[i].init) {
            tracepoint_probe_unregister(interests[i].value, interests[i].fct,NULL);
        }
    }
}


static void __exit tracepoint_exit(void) { cleanup(); }


static int __init tracepoint_init(void)
{
    int i;
    // Install the tracepoints
    for_each_kernel_tracepoint(lookup_tracepoints, NULL);
    FOR_EACH_INTEREST(i) {
        if (interests[i].value == NULL) {
            printk("Error, %s not found\n", interests[i].name);
            cleanup();
            return 1;
        }


        tracepoint_probe_register(interests[i].value, interests[i].fct, NULL);
        interests[i].init = 1;
    }


    return 0;
}


module_init(tracepoint_init)
module_exit(tracepoint_exit)
MODULE_LICENSE("GPL");
通過(guò)追蹤工具來(lái)注入 Probe Event Tracing

在前面的代碼示例中,我們需要通過(guò)編寫(xiě) kernel module 的方式注冊(cè) probe handler,看上去非常簡(jiǎn)單,但在實(shí)際開(kāi)發(fā)的過(guò)程當(dāng)中,編寫(xiě)內(nèi)核模塊是一個(gè)很大的挑戰(zhàn),如果內(nèi)核模塊的代碼寫(xiě)的有問(wèn)題,會(huì)直接導(dǎo)致內(nèi)核 crash,在生產(chǎn)環(huán)境上使用內(nèi)核模塊需要謹(jǐn)慎考慮。

Linux 內(nèi)核為此提供了一個(gè)不需要編寫(xiě)內(nèi)核模塊就能使用 tracepoint 的機(jī)制:event tracing。他抽象出了如下概念:

TraceEvent:事件是在程序執(zhí)行過(guò)程中發(fā)生的特定事情,例如函數(shù)調(diào)用、系統(tǒng)調(diào)用或硬件中斷。事件被描述為一個(gè)有限的結(jié)構(gòu),包含有關(guān)事件的元數(shù)據(jù)和數(shù)據(jù)。每個(gè)事件都有一個(gè)唯一的標(biāo)識(shí)符和名稱(chēng)。

Event Provider:事件提供程序是一個(gè)模塊或應(yīng)用程序,用于在事件跟蹤系統(tǒng)中注冊(cè)和定義事件。事件提供程序負(fù)責(zé)確定事件的格式和語(yǔ)義,并將事件發(fā)送到跟蹤緩沖區(qū)。

Event Consumer:事件消費(fèi)者是從事件跟蹤緩沖區(qū)中讀取事件的進(jìn)程或應(yīng)用程序。事件消費(fèi)者可以將事件輸出到文件、控制臺(tái)或通過(guò)網(wǎng)絡(luò)發(fā)送到遠(yuǎn)程主機(jī)。

Event Tracing Session:事件跟蹤會(huì)話(huà)是一個(gè)包含多個(gè)事件提供程序和事件消費(fèi)者的 ETI 實(shí)例。在一個(gè)事件跟蹤會(huì)話(huà)中,可以收集多個(gè)事件源的事件數(shù)據(jù),并將其聚合到單個(gè)跟蹤緩沖區(qū)中。

Trace Buffer:跟蹤緩沖區(qū)是一個(gè)在內(nèi)核中分配的內(nèi)存區(qū)域,用于存儲(chǔ)事件數(shù)據(jù)。事件提供程序?qū)⑹录?xiě)入跟蹤緩沖區(qū),事件消費(fèi)者從跟蹤緩沖區(qū)讀取事件數(shù)據(jù)。

Trace Event Format (TEF):跟蹤事件格式是一個(gè)描述事件數(shù)據(jù)布局和語(yǔ)義的模板。它指定事件的名稱(chēng)、參數(shù)和字段,以及每個(gè)字段的大小和類(lèi)型。在 ETI 中,跟蹤事件格式可以由事件提供程序靜態(tài)定義或動(dòng)態(tài)生成。

Trace Event Id (TEID):跟蹤事件 ID 是唯一標(biāo)識(shí)一個(gè)跟蹤事件的整數(shù)值。每個(gè)事件提供程序都有自己的 TEID 命名空間,它們使用不同的整數(shù)值來(lái)標(biāo)識(shí)它們的事件。在內(nèi)核代碼中,包含 tracepoint 代碼的函數(shù)就可以理解為是一個(gè) event provider,event provider 通過(guò)在 tracepoint 上注冊(cè)一個(gè) probe handler。當(dāng)這個(gè)函數(shù)執(zhí)行到 tracepoint 時(shí),觸發(fā) probe handler 執(zhí)行,它會(huì)構(gòu)建一個(gè) TraceEvent。內(nèi)核代碼中已經(jīng)有了專(zhuān)門(mén)用于構(gòu)建 trace event 的 probe handler,無(wú)需我們自己注入了。

TraceEvent 會(huì)包含當(dāng)前函數(shù)的上下文和參數(shù),probe handler 會(huì)將 event 保存至在 Trace Buffer 中,接下來(lái)對(duì)于事件的分析、處理操作可以放在用戶(hù)態(tài)執(zhí)行,通過(guò)系統(tǒng)調(diào)用從 Trace Buffer 中讀取 event,或者直接通過(guò) mmap 直接將 Trace Buffer 映射到用戶(hù)態(tài)的內(nèi)存空間讀取 event。

我們現(xiàn)在可以這樣使用 tracepoint:

查看當(dāng)前內(nèi)核支持的 event。

cat /sys/kernel/debug/tracing/available_events

啟用 syscalls:sys_enter_connect 這個(gè)事件。

echo 1 > /sys/kernel/debug/tracing/events/syscalls/sys_enter_connect/enable

查看事件數(shù)據(jù)。

root@zfane-powerpc:~# cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 195/195   #P:16
#
#                                _-----=> irqs-off/BH-disabled
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| / _-=> migrate-disable
#                              |||| /     delay
#           TASK-PID     CPU#  |||||  TIMESTAMP  FUNCTION
#              | |         |   |||||     |         |
      sd-resolve-809     [001] .....  1401.623886: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10)
      sd-resolve-809     [001] .....  1411.634396: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10)
 systemd-resolve-793     [001] .....  1411.634827: sys_connect(fd: 14, uservaddr: 7ffe2e97d050, addrlen: 10)
 systemd-resolve-793     [001] .....  1411.634967: sys_connect(fd: 13, uservaddr: 7ffe2e97d000, addrlen: 10)
      sd-resolve-809     [001] .....  1421.645348: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10)
        rsyslogd-848     [002] .....  1426.678287: sys_connect(fd: 6, uservaddr: 7f3be1fb3bc0, addrlen: 6e)
      sd-resolve-809     [001] .....  1431.655820: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10)
 systemd-resolve-793     [001] .....  1436.661514: sys_connect(fd: 13, uservaddr: 7ffe2e97d050, addrlen: 10)
 systemd-resolve-793     [001] .....  1436.661679: sys_connect(fd: 14, uservaddr: 7ffe2e97d000, addrlen: 10)
        rsyslogd-848     [009] .....  1436.677930: sys_connect(fd: 6, uservaddr: 7f3be1fb3bc0, addrlen: 6e)
        rsyslogd-848     [009] .....  1436.686721: sys_connect(fd: 6, uservaddr: 7f3be1fb3bc0, addrlen: 6e)
      sd-resolve-809     [001] .....  1441.666368: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10)
 systemd-resolve-793     [001] .....  1451.675741: sys_connect(fd: 13, uservaddr: 7ffe2e97d050, addrlen: 10)
      sd-resolve-809     [000] .....  1451.675874: sys_connect(fd: c, uservaddr: 7f618b836d8c, addrlen: 10)

在這個(gè)示例中,我們只是查看了 sys_enter_connect 這個(gè) trace event,沒(méi)有做進(jìn)一步的分析和處理操作,在后面我們可以借助一些工具消費(fèi) trace event。

基于 tracepoint 的 Trace Event 雖然解決了 tracepoint 的 probe handler 注冊(cè)需要編寫(xiě)內(nèi)核模塊才能使用的問(wèn)題,但任然有 2 個(gè)問(wèn)題沒(méi)有解決:

并非所有的內(nèi)核函數(shù)都有 Tracepoint,即使有某個(gè)內(nèi)核函數(shù)有 Tracepoint,如果內(nèi)核開(kāi)發(fā)者沒(méi)有為這個(gè) Tracepoint 實(shí)現(xiàn)構(gòu)建 Event 和保存 Event 到 Trace Buffer 的邏輯,同樣也沒(méi)有辦法獲取 Trace 信息。

內(nèi)核開(kāi)發(fā)者需要編寫(xiě)代碼將 trace 信息保存到 Trace Buffer,作為內(nèi)核的用戶(hù),我們只能看到內(nèi)核開(kāi)發(fā)者想讓我們看到的數(shù)據(jù)根據(jù)前面提到的 trace event 的實(shí)現(xiàn)原理,event 就是 probe handler 構(gòu)建的,那么如果我們?cè)?kprobe 的 probe handler 中實(shí)現(xiàn)構(gòu)建一個(gè) event 并保存的邏輯,不就能實(shí)現(xiàn)一個(gè)基于 kprobe 的 Trace Event 嗎?Event Trace 已經(jīng)支持了這樣的騷操作,下面是 Linux 內(nèi)核給出的示例:

添加基于 kprobe、kretprobe 的 event。

echo 'p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack)' > /sys/kernel/tracing/kprobe_events

他的語(yǔ)法格式按照如下約定:

p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS]  : Set a probe
r[MAXACTIVE][:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS]  : Set a return probe
p:[GRP/]EVENT] [MOD:]SYM[+0]%return [FETCHARGS]       : Set a return probe
-:[GRP/]EVENT                                         : Clear a probe

[GRP/][EVENT] 定義一個(gè) event,[MOD:]SYM[+offs]|MEMADDR, 定義一個(gè) kprobe。[FETCHARGS] 是設(shè)置參數(shù)的類(lèi)型。在上面的示例中,為什么往這個(gè)文件里寫(xiě)入一些文本,就可以實(shí)現(xiàn) kprobe 的 probe handler 的能力?這主要依賴(lài)于 TraceFS 文件系統(tǒng)。

Tracefs 是什么?

TraceFS 是 Linux 內(nèi)核提供的一個(gè)虛擬文件系統(tǒng),他提供了一組文件和目錄,用戶(hù)可以通過(guò)讀寫(xiě)這些文件和目錄來(lái)與內(nèi)核中的跟蹤工具交互。

以 kprobe_event 為例,krpobe_event 在 tracefs 文件系統(tǒng)中注冊(cè)了一個(gè)回調(diào)函數(shù) init_kprobe_trace,在掛載 tracefs 文件系統(tǒng)時(shí)執(zhí)行,他會(huì)創(chuàng)建 kprobe_events 文件,并注冊(cè)對(duì)這個(gè)文件的讀寫(xiě)操作監(jiān)聽(tīng)。

static const struct file_operations kprobe_events_ops = {
  .owner          = THIS_MODULE,
  .open           = probes_open,
  .read           = seq_read,
  .llseek         = seq_lseek,
  .release        = seq_release,
  .write    = probes_write,
};


/* Make a tracefs interface for controlling probe points */
static __init int init_kprobe_trace(void)
{
  struct dentry *d_tracer;
  struct dentry *entry;


  if (register_module_notifier(&trace_kprobe_module_nb))
    return -EINVAL;


  d_tracer = tracing_init_dentry();
  if (IS_ERR(d_tracer))
    return 0;


  entry = tracefs_create_file("kprobe_events", 0644, d_tracer,
            NULL, &kprobe_events_ops);


  /* Event list interface */
  if (!entry)
    pr_warning("Could not create tracefs "
         "'kprobe_events' entry\n");


  /* Profile interface */
  entry = tracefs_create_file("kprobe_profile", 0444, d_tracer,
            NULL, &kprobe_profile_ops);


  if (!entry)
    pr_warning("Could not create tracefs "
         "'kprobe_profile' entry\n");
  return 0;
}
fs_initcall(init_kprobe_trace);

當(dāng) kprobe_event 文件有寫(xiě)操作時(shí),便會(huì)觸發(fā)create_trace_kprobe函數(shù)執(zhí)行,按照特定的語(yǔ)法解析 kprobe_event 文件內(nèi)容,創(chuàng)建一個(gè) kprobe。

static ssize_t probes_write(struct file *file, const char __user *buffer,
          size_t count, loff_t *ppos)
{
  return traceprobe_probes_write(file, buffer, count, ppos,
      create_trace_kprobe);
}

在內(nèi)核追蹤技術(shù)的發(fā)展初期,追蹤相關(guān)的文件都放在 debugfs 虛擬文件系統(tǒng)中,debugfs 主要設(shè)計(jì)目的是為了提供一個(gè)通用的內(nèi)核調(diào)試接口,內(nèi)核的任意子系統(tǒng)都有可能使用 debugfs 做調(diào)試,所以很多人出于安全考慮 debugfs 是不啟用的,這就導(dǎo)致無(wú)法使用內(nèi)核的追蹤能力,tracefs 隨之誕生了,他會(huì)創(chuàng)建一個(gè)/sys/kernel/tracing目錄,但為了保證兼容性,tracefs 仍然掛載在/sys/kernel/debug/tracing 下。如果沒(méi)有啟用 debugfs,tracefs 可以?huà)燧d在/sys/kernel/tracing。

隨著 Linux 追蹤技術(shù)的發(fā)展,TraceFS 文件系統(tǒng)也成為了追蹤系統(tǒng)的基礎(chǔ)設(shè)施,很多跟蹤工具都使用 TraceFS 作為管理接口,比如 Perf、LTTng 等。

Function Trace

前面提到的 event trace 機(jī)制與基于 tracefs 文件系統(tǒng)管理 event 的機(jī)制最初就是 Ftrace 的一部分能力,現(xiàn)在已經(jīng)成為 Linux 內(nèi)核追蹤系統(tǒng)的通用模塊,很多追蹤工具也都依賴(lài)它。那么 Ftrace 是什么呢?

Ftrace 有兩層含義:

為函數(shù)注入 probe handler 的函數(shù)跟蹤的機(jī)制;

基于 trace fs 和 event trace 機(jī)制的 trace 框架。我們前面已經(jīng)了解了 kprobes、tracepoint 兩種注入 probe handler 的機(jī)制,而 Ftrace 又帶了一種新的實(shí)現(xiàn)方式:編譯時(shí)注入。

gcc 有一個(gè)編譯選項(xiàng):-pg,當(dāng)使用這個(gè)編譯選項(xiàng)編譯代碼時(shí),他會(huì)在每一個(gè)函數(shù)的入口添加對(duì) mcount 函數(shù)的調(diào)用,mcount 函數(shù)由 libc 提供,它的實(shí)現(xiàn)會(huì)根據(jù)具體的機(jī)器架構(gòu)生成相應(yīng)的代碼。一般情況下 mcount 函數(shù)會(huì)記錄當(dāng)前函數(shù)的地址、耗時(shí)等信息,在程序執(zhí)行結(jié)束后,生成一個(gè).out 文件用于給 gprof 來(lái)做性能分析的。我們可以編譯一個(gè) hello.c 文件查看匯編代碼中包含了 mcount 調(diào)用。

root@zfane-maxpower:~/traceing# cat hello.c
#include 
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) // 在這個(gè)地方添加了 mcount 調(diào)用
  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)    // 在這個(gè)地方添加了 mcount 調(diào)用
  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:

內(nèi)核代碼的編譯是不依賴(lài) libc 庫(kù),而 ftrace 提供了一個(gè) mcount 函數(shù),在這個(gè)函數(shù)中實(shí)現(xiàn) probe handler 的能力,如果所有的內(nèi)核函數(shù)都在函數(shù)入口添加 mcount 調(diào)用,運(yùn)行時(shí)會(huì)對(duì)性能造成極大的影響,我們之前介紹的 kprobes、tracepoint 都具備動(dòng)態(tài)開(kāi)啟和關(guān)閉的能力盡可能的減少對(duì)內(nèi)核的影響,F(xiàn)trace 也不例外,他具備動(dòng)態(tài)開(kāi)啟某個(gè)函數(shù)的 probe handler 的能力,其實(shí)現(xiàn)思路有一點(diǎn)特別。

內(nèi)核編譯時(shí)(設(shè)置 -pg 的編譯選項(xiàng)),在匯編階段生成.o 的目標(biāo)文件,再調(diào)用 ftrace 在內(nèi)核代碼包中放置的一個(gè) Perl 腳本 Recordmcount.pl,他會(huì)掃描每一個(gè)目標(biāo)文件,查找 mcount 函數(shù)調(diào)用的地址,并記錄到一個(gè)臨時(shí)的.s 文件中(一個(gè)目標(biāo)文件對(duì)應(yīng)一個(gè).s 文件),查找完成后,將臨時(shí)的.s 文件編譯成.o 目標(biāo)文件和原來(lái)的.o 文件鏈接到一起。

在編譯過(guò)程的鏈接階段,vmlinux.lds.h 把所有的 mcount_loc 端的內(nèi)容放在 vmlinux 的.init.data 端,并聲明了兩個(gè)全局符號(hào)start_mcount_loc 和 __stop_mcount_loc 來(lái)開(kāi)啟和關(guān)閉 mcount 函數(shù)調(diào)用。

在內(nèi)核啟動(dòng)階段,會(huì)調(diào)用 ftrace_init 函數(shù),在這個(gè)函數(shù)中,根據(jù)記錄的 mcount 函數(shù)偏移地址,把所有的 mcount 函數(shù)調(diào)用對(duì)應(yīng)的指令修改為 NOP 指令。ftrace_init 函數(shù)在 start_kernel 中調(diào)用,比 kerne__init 還要先執(zhí)行,此時(shí)不會(huì)有任何內(nèi)核代碼執(zhí)行,修改指令不會(huì)有任何影響。

在對(duì)某個(gè)函數(shù)啟用 ftrace probe handler,會(huì)將 NOP 指令修改為對(duì) ftrace probe handler 的調(diào)用即可,和 kprobe trap 一樣的原理,找到需要被 trace 的函數(shù),函數(shù)的 mcount 調(diào)用是 NOP 指令,把 NOP 指令的第一個(gè)字節(jié)改為 int 3,也就是斷點(diǎn)指令,再把 NOP 指令調(diào)整為 probe handler 的地址。

在內(nèi)核 4.19 版本,提升了最低版本的 gcc 限制,最低可允許 gcc 4.6 版本編譯,gcc 4.6 版本支持 -mfentry 編譯參數(shù),使用 fentry 的特殊函數(shù)調(diào)用作為所有函數(shù)的第一條指令,他可以替代 mcount 函數(shù)調(diào)用,并且性能更好。

Ftrace 這種通過(guò)編譯參數(shù)注入的 probe handler 非常好用,編譯完成后,相當(dāng)于各個(gè)內(nèi)核函數(shù)都聲明了 tracepoint,在內(nèi)核運(yùn)行時(shí)可以動(dòng)態(tài)打開(kāi)和關(guān)閉。那我們能否可以只使用 Ftrace 的 probe handler 注入能力呢?也是可以的,他有一個(gè)新的名字叫 fprobe,在 2022 年合入內(nèi)核代碼,他是 ftrace 的包裝器,可以?xún)H使用 ftrace 的函數(shù)追蹤的功能。

#define pr_fmt(fmt) "%s: " fmt, __func__


#include 
#include 
#include 
#include 
#include 


#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");

除了編寫(xiě)內(nèi)核模塊的方式,能否通過(guò) event trace 機(jī)制來(lái)使用呢?答案是可以的,需要使用最新版的內(nèi)核才行,fprobe 支持 event trace 是在 23 年 4 月份剛合并到內(nèi)核里。

Perf

Perf 是一個(gè) Linux 下的性能分析工具的集合,最初由英特爾公司的 Andi Kleen 開(kāi)發(fā),于 2008 年首次發(fā)布。Perf 設(shè)計(jì)之初是為了解決英特爾處理器性能分析工具集(Intel Performance Tuning Utilities)在 Linux 上的移植問(wèn)題而開(kāi)發(fā)的,它可以利用英特爾的硬件性能監(jiān)視器(Hardware Performance Monitoring)來(lái)對(duì) CPU 性能進(jìn)行采樣和分析。隨著時(shí)間的推移,Perf 逐漸成為了一個(gè)通用的性能分析工具,也支持內(nèi)核追蹤。

有了前面提到的 Ftrace,為什么 Perf 也要支持內(nèi)核跟蹤機(jī)制呢,主要原因在于 perf 有著特殊的分析方式:采樣分析。采樣的對(duì)象是 event,以基于時(shí)間的采樣方式為例,他的大致流程是這樣的,每隔一段時(shí)間,就在所有 CPU 上產(chǎn)生一個(gè)中斷,查看當(dāng)前是哪個(gè) pid,哪個(gè)函數(shù)在執(zhí)行,并將 pid/func 構(gòu)建成一個(gè) event 做統(tǒng)計(jì),在采樣結(jié)束后,我們就能知道 CPU 大部分時(shí)間耗在哪個(gè) pid/func 上。

1a65f23e-07fd-11ee-962d-dac502259ad0.png

除了上面提到的基于時(shí)間的采樣,perf 還支持如下采樣方式:

計(jì)數(shù). 統(tǒng)計(jì)某個(gè)事件的發(fā)生次數(shù)。

基于事件的采樣. 每當(dāng)發(fā)生的事件數(shù)達(dá)到特定的閾值時(shí),就會(huì)記錄一個(gè)樣本。

基于指令的采樣. 處理器跟蹤按給定時(shí)間間隔出現(xiàn)的指令,并對(duì)這些指令生成的事件采樣。這樣便可以跟蹤各個(gè)指令,并查看哪些是對(duì)性能至關(guān)重要的指令。最開(kāi)始 perf 是僅支持由硬件產(chǎn)生的 Hardware event,這種方式可以推廣到各種事件,比如 trace event 事件,當(dāng)這個(gè)事件發(fā)生的時(shí)候上來(lái)冒個(gè)頭,看看擊中了誰(shuí),然后算出分布,我們就知道誰(shuí)會(huì)引發(fā)特別多的那個(gè)事件了。

接下來(lái)我們看一下 perf 是如何使用 trace event。

我們可以通過(guò) perf 命令設(shè)置一個(gè) probe。

$ sudo perf probe -x /usr/lib/debug/boot/vmlinux-$(uname -r) -k do_sys_open

接下來(lái)通過(guò) record 子命令 啟用 Trace Event,并將 trace 信息保存到 perf.data。

$ sudo perf record -e probe:do_sys_open -aR sleep 1

現(xiàn)在我們可以通過(guò) report 子命令,分析 trace 信息。

$ sudo perf report -i perf.data

perf 采樣拿到的 event 最終會(huì)被放到一個(gè)叫做 perf event 的數(shù)據(jù)結(jié)構(gòu)里面,因?yàn)?event 都是在內(nèi)核態(tài)產(chǎn)生的,采樣時(shí)需要一個(gè)數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)采集到的 event,并在采樣結(jié)束后,將采集到的 event 從內(nèi)核態(tài)發(fā)送到用戶(hù)態(tài)來(lái)使用,perf event 就是用來(lái)做這個(gè)事情的,我們通常說(shuō)的 perf 是指用戶(hù)態(tài)的工具,perf event 是內(nèi)核態(tài)的數(shù)據(jù)結(jié)構(gòu)。perf 工具通過(guò)系統(tǒng)調(diào)用 perf_event_open 來(lái)創(chuàng)建 perf event。

在內(nèi)核中,perf_event 結(jié)構(gòu)體,存儲(chǔ)該事件的配置和運(yùn)行狀態(tài)。創(chuàng)建 perf event 時(shí)還會(huì)創(chuàng)建 perf event 對(duì)應(yīng)的 ring buffer 用來(lái)存儲(chǔ) trac event 數(shù)據(jù)。perf 工具通過(guò) perf_event_open 系統(tǒng)調(diào)用拿到 perf event 的 fd 后,就可以通過(guò) mmap 內(nèi)存映射機(jī)制 將內(nèi)核態(tài)的 ringbuffer 映射到用戶(hù)態(tài)來(lái)訪問(wèn),最終 perf 將數(shù)據(jù)寫(xiě)到 perf.data 中以供后續(xù)分析。

Perf 使用 Trace Event

Perf 工具是基于 Perf Event 這個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn)分析能力的,當(dāng)使用 Perf 添加 Trace Event 時(shí),內(nèi)核會(huì)將追蹤數(shù)據(jù)寫(xiě)到 perf event 對(duì)應(yīng)的 ringbuffer。

還是以上面的 perf 使用案例為例。我們通過(guò) perf probe 子命令添加一個(gè) uprobe event,在 TraceFS 中也可以看到 uprobe_event 的定義,但處于禁用狀態(tài)。

root@zfane-maxpower:~/traceing# perf probe -x /root/traceing/hello show_test=test
Added new event:
  probe_hello:show_test (on test in /root/traceing/hello)


You can now use it in all perf tools, such as:


  perf record -e probe_hello:show_test -aR sleep 1


root@zfane-maxpower:~/traceing# cat /sys/kernel/tracing/uprobe_events
p:probe_hello/show_test /root/traceing/hello:0x0000000000001169
root@zfane-maxpower:~/traceing# cat /sys/kernel/tracing/events/probe_hello/enable
0
root@zfane-maxpower:~/traceing#

同樣是往 uprobe_events 文件中寫(xiě) trace event definition,為什么手動(dòng)寫(xiě)就是往 Trace Buffer 里發(fā)送數(shù)據(jù),用 perf 寫(xiě)就是往 perf event ring buffer 發(fā)送數(shù)據(jù)呢?

在使用 perf record 子命令采集數(shù)據(jù)時(shí),會(huì)通過(guò) perf_event_open 創(chuàng)建 perf event,perf event 在初始化階段掃描所有的 trace event, 檢查是否存在與 perf event 關(guān)聯(lián)的 uprobe_event,找到對(duì)應(yīng)的 uprobe event 事件后,就可以啟用 urpobe event 了。

uprobe event 啟用時(shí)才會(huì)觸發(fā) uprobe 注冊(cè)操作,但是 perf event 不是通過(guò) TraceFS 的 enable 文件來(lái)注冊(cè) uprobe event 的,而是直接調(diào)用 uprobe event 注冊(cè)接口,uprobe event 注冊(cè)接口有兩種注冊(cè)類(lèi)型:TRACE_REG_PERF_REGISTER、TRACE_REG_REGISTER。TRACE_REG_PERF_REGISTER 表示由 perf event 注冊(cè),uprobe event 有一個(gè) flag 屬性 用于存儲(chǔ)注冊(cè)類(lèi)型,TRACE_REG_PERF_REGISTER 對(duì)應(yīng)的 flag 值為 TP_FLAG_PROFILE,其他的則是 TP_FLAG_TRACE。

uprobe event 的 probe handler 固定是 uprobe_dispatcher 函數(shù),uprobe_dispatcher 函數(shù)會(huì)根據(jù) uprobe event 的 flag 屬性來(lái)判斷往哪個(gè) ring buffer 里寫(xiě)追蹤數(shù)據(jù),kprobe 也是同理。tracepoint 和它倆不一樣,用于聲明 tracepoint 的 TRACE_EVENT 宏定義中包含了專(zhuān)門(mén)給 perf event 使用的 probe handler,他會(huì)直接往 perf event 的 ringbuffer 中寫(xiě)數(shù)據(jù)。

為什么要有兩套 Ring buffer?

Event Tracing 框架下,內(nèi)核中的追蹤數(shù)據(jù)往 Ring Buffer 中寫(xiě)入,我們可以通過(guò) Tracefs 文件系統(tǒng)來(lái)訪問(wèn) Ring Buffer,為什么 perf 工具不直接使用這個(gè) Ring Buffer 來(lái)獲取追蹤信息?而是在內(nèi)核中讓 Trace Event 的追蹤數(shù)據(jù)直接寫(xiě)入到 Perf Event 的 ring buffer 中。

其實(shí)主要原因就是 Ftrace 實(shí)現(xiàn)的 Ring Buffer 無(wú)法滿(mǎn)足 Perf 的需要,Perf 需要在 NMI 場(chǎng)景下也能往 Ring Buffer 中寫(xiě)入數(shù)據(jù)。

Non-Maskable Interrupt (NMI) 是一種中斷信號(hào),它可以打破處理器的正常執(zhí)行流程,而且無(wú)法被忽略或屏蔽。一般來(lái)說(shuō),NMI 通常用于緊急情況下的故障處理或者硬件監(jiān)控等場(chǎng)景。NMI 信號(hào)通常是由硬件觸發(fā)的,例如內(nèi)存錯(cuò)誤、總線(xiàn)錯(cuò)誤、電源故障等,這些故障可能會(huì)導(dǎo)致系統(tǒng)崩潰或者停機(jī)。為了避免在故障發(fā)生后丟失重要的性能事件數(shù)據(jù),Perf 需要將這些數(shù)據(jù)盡可能快地寫(xiě)入 ring buffer 中,以確保數(shù)據(jù)不會(huì)丟失,這就要求 Ring Buffer 的實(shí)現(xiàn)上不可以有寫(xiě)競(jìng)爭(zhēng),或可能導(dǎo)致死鎖的情況。

很不湊巧的是,F(xiàn)trace 的 Ring Buffer 在設(shè)計(jì)上,使用了自旋鎖來(lái)防止并發(fā)訪問(wèn),自旋鎖會(huì)一直占用 CPU 資源直到鎖可用,在 NMI 的場(chǎng)景下,如果 Ftrace 正在持有自旋鎖,NMI 中斷處理程序就無(wú)法獲取自旋鎖,可能會(huì)導(dǎo)致系統(tǒng)死鎖或者卡死。

另外一點(diǎn)就是 NMI 場(chǎng)景下 RingBuffer 的訪問(wèn)一定要快,處理器必須盡可能快地響應(yīng) NMI 中斷信號(hào),任何慢速的操作都可能會(huì)導(dǎo)致系統(tǒng)的穩(wěn)定性和性能受到影響。Ftrace Ring Buffer 也沒(méi)有足夠的快,最終 Perf 的開(kāi)發(fā)人員自行實(shí)現(xiàn)了一套新的 無(wú)鎖 Ring Buffer。

通過(guò)編寫(xiě) eBPF 代碼來(lái)注入 probe 如何使用 eBPF 追蹤內(nèi)核?

由于內(nèi)核態(tài)和用戶(hù)態(tài)的內(nèi)存空間是隔離的,他們的虛擬內(nèi)存實(shí)現(xiàn)原理不同,想要從內(nèi)核態(tài)向用戶(hù)態(tài)傳遞數(shù)據(jù)需要經(jīng)過(guò)地址轉(zhuǎn)換和數(shù)據(jù)拷貝,比較耗時(shí)。而在分析網(wǎng)絡(luò)數(shù)據(jù)包時(shí),如果所有的網(wǎng)絡(luò)數(shù)據(jù)包都從內(nèi)核態(tài)發(fā)到用戶(hù)態(tài),帶來(lái)的成本也更大,很多時(shí)候我們都是只需一部分?jǐn)?shù)據(jù)包就可以了,所以最理想的方式是內(nèi)核態(tài)有一個(gè) Packet Filter 機(jī)制,能夠過(guò)濾我們不需要的數(shù)據(jù)包,這樣就大大減少了內(nèi)核需要拷貝的數(shù)據(jù)。

早期 unix 系統(tǒng)也提供了 packet filter 機(jī)制,提供了一個(gè)基于內(nèi)存棧的虛擬機(jī),來(lái)對(duì)內(nèi)核態(tài)的數(shù)據(jù)包做過(guò)濾計(jì)算,比如 CMU/Stanford Packet Filter(CSPF)、NIT(Network Interface Tap) 等,它們的性能不夠好。tcpdump 的作者 Steve McCanne 和 Van Jacobson 在 BSD 操作系統(tǒng)上實(shí)現(xiàn)了一個(gè)全新架構(gòu)的 Packet Filter 機(jī)制:Berkeley Packet Filter (BPF),拋棄了之前基于內(nèi)存棧虛擬機(jī)的設(shè)計(jì),改為基于寄存器的虛擬機(jī),號(hào)稱(chēng)性能比之前的 packet filter 機(jī)制快很多。同時(shí)可以在內(nèi)核態(tài)接到 device interface 傳過(guò)來(lái)的包時(shí)就進(jìn)行 filter,不需要的包直接丟棄,不會(huì)多出任何無(wú)效 copy。憑借優(yōu)秀的架構(gòu)設(shè)計(jì)和性能表現(xiàn),BPF 被移植到了很多操作系統(tǒng)。

BPF 的作者發(fā)表了一篇論文 The BSD Packet Filter: A New Architecture for User-level Packet Capture 來(lái)詳細(xì)描述了 BPF 的設(shè)計(jì)理念與實(shí)現(xiàn)思路,感興趣的可以看一下。

BSD 系統(tǒng)的 BPF 在被移植到 Linux 上后被稱(chēng)為 Linux Socket Filter(LSF),但是大家依然稱(chēng)呼它為 BPF,BPF 在 Linux 內(nèi)核最初也是提供 Packet filter 的能力,用戶(hù)態(tài)使用 BPF 字節(jié)碼來(lái)定義過(guò)濾表達(dá)式,然后傳遞給內(nèi)核,由內(nèi)核虛擬機(jī)解釋執(zhí)行。

隨著時(shí)間的推移,Linux 內(nèi)核開(kāi)發(fā)者為 BPF 添加了更多的能力,比如 Linux 3.0 版本增加 BPF JIT 編譯器,在 2014 年 Alexei Starovoitov 為 BPF 帶來(lái)了一次革命性的更新,將 BPF 擴(kuò)展為一個(gè)通用的虛擬機(jī),也就是 eBPF。eBPF 不僅擴(kuò)展了寄存器的數(shù)量,引入了全新的 BPF 映射存儲(chǔ),還在 4.x 內(nèi)核中將原本單一的數(shù)據(jù)包過(guò)濾事件逐步擴(kuò)展到了內(nèi)核態(tài)函數(shù)、用戶(hù)態(tài)函數(shù)、跟蹤點(diǎn)、性能事件(perf_events)以及安全控制等。

話(huà)說(shuō)回 Linux 追蹤技術(shù)。eBPF 的影響也來(lái)到了內(nèi)核追蹤領(lǐng)域,2015 年 eBPF 支持 kprobe、2016 年開(kāi)始支持 tracepoint、perf event,現(xiàn)在我們可以通過(guò)在 eBPF 虛擬機(jī)運(yùn)行自定義的 probe handler 獲取跟蹤數(shù)據(jù),并通過(guò) eBPF Map 共享到用戶(hù)態(tài)來(lái)對(duì)跟蹤數(shù)據(jù)做分析。相比于編寫(xiě)內(nèi)核代碼或是 ftrace、perf 靈活性大大增強(qiáng)。

eBPF 的本質(zhì)是一個(gè)在內(nèi)核態(tài)的虛擬機(jī),可以在虛擬機(jī)中執(zhí)行簡(jiǎn)單代碼,一個(gè)完整的 eBPF 程序通常包含用戶(hù)態(tài)和內(nèi)核態(tài)兩部分:用戶(hù)態(tài)程序通過(guò) BPF 系統(tǒng)調(diào)用,完成 eBPF 程序的加載、事件掛載以及映射創(chuàng)建和更新,而內(nèi)核態(tài)中的 eBPF 程序可以理解為我們的 probe handler,用來(lái)獲取追蹤數(shù)據(jù)。

eBPF 程序根據(jù)其用途劃分為多種類(lèi)型,在追蹤方面有如下類(lèi)型:

BPF_PROG_TYPE_KPROBE

BPF_PROG_TYPE_TRACEPOINT

BPF_PROG_TYPE_PERF_EVENT

BPF_PROG_TYPE_RAW_TRACEPOINT

BPF_PROG_TYPE_RAW_TRACEPOINT_WRITEABLE

BPF_PROG_TYPE_TRACING 從類(lèi)型名稱(chēng)也能看出來(lái)對(duì)應(yīng)類(lèi)型的 eBPF 程序是如何實(shí)現(xiàn)追蹤能力的,比如 kprobes 類(lèi)型的 eBPF 程序,就是通過(guò) kprobes 機(jī)制注入 probe handler,probe handler 就是我們?cè)趦?nèi)核態(tài)虛擬機(jī)中運(yùn)行的 eBPF 代碼。同時(shí) eBPF 程序類(lèi)型里面沒(méi)有 UPROBE,主要原因是因?yàn)?uprobes 和 kprobes 原理相同,KPROBE 類(lèi)型的 eBPF 程序也可以使用 uprobes。

那么 eBPF 是如何使用 kprobe、tracepoint 等機(jī)制將自己作為 probe handler 注入到內(nèi)核函數(shù)中的?

在前面的介紹里,我們?nèi)绻褂?kprobe 機(jī)制探測(cè)內(nèi)核函數(shù),可以使用 register_kprobe 函數(shù)、event trace、perf event 方式來(lái)注冊(cè) probe handler。**eBPF 采用 perf event 將內(nèi)核態(tài)程序做為 probe handler,** 在 eBPF 用戶(hù)態(tài)程序中,可以通過(guò) attach_kprobe 函數(shù)將內(nèi)核態(tài) eBPF 程序通過(guò) kprobes 機(jī)制附加到某個(gè)內(nèi)核函數(shù)中。attach_kprobe 函數(shù)會(huì)創(chuàng)建一個(gè) perf event,再將 eBPF 內(nèi)核態(tài)程序附加到 perf event。每個(gè) perf event 的 kprobe probe handler 都是 kprobe_dispatch 函數(shù),他會(huì)去 perf event 中獲取注冊(cè)在當(dāng)前 perf event 的回調(diào)函數(shù)列表并依次執(zhí)行,同時(shí)將指向 perf ringbuffer 的指針的傳遞給 eBPF 程序,eBPF 程序可以通過(guò) libbpf 封裝好的 PT_REGS_PARAMx 宏定義來(lái)獲取緩沖區(qū)中的數(shù)據(jù)。

static int kprobe_dispatcher(struct kprobe *kp, struct pt_regs *regs)
{
  struct trace_kprobe *tk = container_of(kp, struct trace_kprobe, rp.kp);
  int ret = 0;


  raw_cpu_inc(*tk->nhit);


  if (trace_probe_test_flag(&tk->tp, TP_FLAG_TRACE))
    kprobe_trace_func(tk, regs);
#ifdef CONFIG_PERF_EVENTS
  if (trace_probe_test_flag(&tk->tp, TP_FLAG_PROFILE))
    ret = kprobe_perf_func(tk, regs); // 調(diào)用 perf event 的 probe handler
#endif
  return ret;
}


/* Kprobe profile handler */
static int
kprobe_perf_func(struct trace_kprobe *tk, struct pt_regs *regs)
{
  struct trace_event_call *call = trace_probe_event_call(&tk->tp);
  struct kprobe_trace_entry_head *entry;
  struct hlist_head *head;
  int size, __size, dsize;
  int rctx;


  if (bpf_prog_array_valid(call)) {
    unsigned long orig_ip = instruction_pointer(regs);
    int ret;


    ret = trace_call_bpf(call, regs); // 在這里調(diào)用 bpf 程序


    /*
     * We need to check and see if we modified the pc of the
     * pt_regs, and if so return 1 so that we don't do the
     * single stepping.
     */
    if (orig_ip != instruction_pointer(regs))
      return 1;
    if (!ret)
      return 0;
  }


  head = this_cpu_ptr(call->perf_events);
  if (hlist_empty(head))
    return 0;


  dsize = __get_data_size(&tk->tp, regs);
  __size = sizeof(*entry) + tk->tp.size + dsize;
  size = ALIGN(__size + sizeof(u32), sizeof(u64));
  size -= sizeof(u32);


  entry = perf_trace_buf_alloc(size, NULL, &rctx);
  if (!entry)
    return 0;


  entry->ip = (unsigned long)tk->rp.kp.addr;
  memset(&entry[1], 0, dsize);
  store_trace_args(&entry[1], &tk->tp, regs, sizeof(*entry), dsize);
  perf_trace_buf_submit(entry, size, rctx, call->event.type, 1, regs,
            head, NULL);
  return 0;
}

不論是 kprobes、tracepoint 類(lèi)型的 eBPF 程序,都是復(fù)用 perf event 來(lái)實(shí)現(xiàn) probe handler 注入,在某個(gè)內(nèi)核版本,eBPF 的負(fù)責(zé)人 Alex 提出了一個(gè)新的方式 Raw Tracepoint,不需要依賴(lài) perf event,eBPF 程序直接作為 probe handler 注冊(cè)到 tracepoint 上。

從使用上來(lái)說(shuō),tracepoint 類(lèi)型的 eBPF 程序需要定義好 tracepoint 關(guān)聯(lián)的函數(shù)的參數(shù)的數(shù)據(jù)結(jié)構(gòu),這個(gè)可以在 TraceFS 中查看,比如 sched_process_exec 這個(gè) tracepoint。

root@zfane-maxpower:~# cat /sys/kernel/tracing/events/sched/sched_process_exec/format
name: sched_process_exec
ID: 311
format:
  field:unsigned short common_type;  offset:0;  size:2;  signed:0;
  field:unsigned char common_flags;  offset:2;  size:1;  signed:0;
  field:unsigned char common_preempt_count;  offset:3;  size:1;  signed:0;
  field:int common_pid;  offset:4;  size:4;  signed:1;


  field:__data_loc char[] filename;  offset:8;  size:4;  signed:1;
  field:pid_t pid;  offset:12;  size:4;  signed:1;
  field:pid_t old_pid;  offset:16;  size:4;  signed:1;


print fmt: "filename=%s pid=%d old_pid=%d", __get_str(filename), REC->pid, REC->old_pid

tracepoint 定義好數(shù)據(jù)結(jié)構(gòu),配合 bpf 輔助函數(shù)提取 tracepoint 傳遞過(guò)來(lái)的數(shù)據(jù)。

struct sched_process_exec_args{ // 聲明數(shù)據(jù)結(jié)構(gòu)
    unsigned short common_type;
    unsigned char common_flags;
    unsigned char common_preempt_count;
    int common_pid;
    int __data_loc;
    pid_t pid;
    pid_t old_pid;
};


SEC("tracepoint/sched/sched_process_exec")
int tracepoint_demo(struct sched_process_exec_args *ctx) { 
    struct event *e;
    e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) {
        return 0;
    }
    unsigned short filename_offset=ctx->__data_loc & 0xFFFF;
    char *filename=(char *)ctx +filename_offset;


    bpf_core_read(&e->filename,sizeof(e->filename),filename); // 通過(guò)輔助函數(shù)讀取值
    e->pid=bpf_get_current_pid_tgid() >>32;
    bpf_get_current_comm(&e->command,sizeof(e->command));
    bpf_ringbuf_submit(e, 0);


    return 0;
}


char _license[] SEC("license") = "GPL";

eBPF 程序接受到的數(shù)據(jù)是由 perf probe 傳遞過(guò)來(lái)的。tracepoint 關(guān)聯(lián)的函數(shù)的參數(shù)會(huì)寫(xiě)到 perf ringbuffer 緩沖區(qū),perf probe 會(huì)將指向緩沖區(qū)的指針傳遞給 eBPF 程序。tracepoint 關(guān)聯(lián)的函數(shù)參數(shù)在緩沖區(qū)的布局如下:

+---------+
| 8 bytes | hidden 'struct pt_regs *' (inaccessible to bpf program)
+---------+
| N bytes | static tracepoint fields defined in tracepoint/format (bpf readonly)
+---------+
| dynamic | __dynamic_array bytes of tracepoint (inaccessible to bpf yet)
+---------+

perf probe 傳遞了指向緩沖區(qū)的指針,eBPF 也無(wú)法直接使用指針訪問(wèn)內(nèi)存上的數(shù)據(jù),各個(gè)內(nèi)核函數(shù)的參數(shù)不一樣,在不知道數(shù)據(jù)的類(lèi)型、長(zhǎng)度,無(wú)法保證安全訪問(wèn),所以需要借助 bpf 輔助函數(shù)讀取數(shù)據(jù)。

再說(shuō)回 raw tracepoint 類(lèi)型的 eBPF 程序,從使用上來(lái)說(shuō),它的函數(shù)參數(shù)結(jié)構(gòu)體變成了 struct bpf_raw_tracepoint_args,不在需要我們定義 tracepoint 關(guān)聯(lián)的結(jié)構(gòu)體了。SEC 聲明也改成 raw_traceoint,其他的在使用上和 tracepoint 類(lèi)型的 eBPF 程序保持一致。

// include/trace/events/sched.h
SEC("raw_tracepoint/sched_process_exec")
int raw_tracepoint_demo(struct bpf_raw_tracepoint_args *ctx) {


    struct event *e;
    e=bpf_ringbuf_reserve(&events,sizeof(*e),0);
    if (!e) {
        return 0;
    }
    bpf_core_read(&e->filename,sizeof(e->filename),ctx->args[0]);
    e->pid=bpf_get_current_pid_tgid() >>32;
    bpf_get_current_comm(&e->command,sizeof(e->command));
    bpf_ringbuf_submit(e, 0);
    return 0;
}

raw_tracepoint 類(lèi)型的 eBPF 程序相比于普通的 tracepoint 類(lèi)型的 eBPF 程序核心的改變是,直接附加在 tracepoint 上,可以提供參數(shù)的“原始訪問(wèn)“。直接附加在 tracepoint 的意思是,tracepoint 對(duì)應(yīng)的函數(shù)執(zhí)行時(shí),內(nèi)核將直接調(diào)用 bpf 程序執(zhí)行,為此內(nèi)核提供了 tracepoint 注冊(cè) bpf 程序的注冊(cè)接口 bpf_raw_tracepoint_open。而參數(shù)的原始訪問(wèn)不好描述,但可以對(duì)比 raw_tracepoint 和 tracepoint 參數(shù)傳遞方式來(lái)理解。

對(duì)于 tracepoint 類(lèi)型 eBPF 程序,是 perf event 在 ringbuffer 中分配一塊內(nèi)存空間,然后內(nèi)核會(huì)將函數(shù)的參數(shù)寫(xiě)到這個(gè)內(nèi)存空間中,perf probe 再把這個(gè)內(nèi)存空間的地址傳遞給 eBPF 程序,而原始訪問(wèn)則是,直接把函數(shù)參數(shù)全部轉(zhuǎn)換為 u64 類(lèi)型,得到一個(gè)數(shù)組,并把數(shù)組傳遞給 eBPF 程序。更短的調(diào)用鏈和跳過(guò)參數(shù)處理,相比于 tracepoint ,raw tracepoint 有更好的性能。

samples/bpf/test_overhead performance on 1 cpu:


tracepoint    base  kprobe+bpf tracepoint+bpf raw_tracepoint+bpf
task_rename   1.1M   769K        947K            1.0M
urandom_read  789K   697K        750K            755K
BTF-enabled raw_tracepoint

在內(nèi)核 4.18 版本,引入了 BTF (BPF Type Format),它用來(lái)描述 BPF prog 和 map 相關(guān)調(diào)試信息的 元數(shù)據(jù)格式,后面 BTF 又進(jìn)一步拓展成可描述 function info 和 line info。BTF 為 Struct 和 Union 類(lèi)型提供了對(duì)應(yīng)成員的 offset 信息,并結(jié)合 Clang 的擴(kuò)展(主要是[__builtin_preserve_access_index())和 BPF 加載器,BPF Prog 就可以準(zhǔn)確訪問(wèn)某個(gè) Struct 或者 Union 類(lèi)型的成員,而不用擔(dān)心重定位問(wèn)題。

在內(nèi)核 5.5 版本專(zhuān)門(mén)定義了一個(gè) BPF_PROG_TYPE_TRACING 類(lèi)型,支持訪問(wèn) BTF 信息,率先支持的就是 raw_tracepoint,不再需要輔助函數(shù)訪問(wèn)內(nèi)存。

SEC("tp_btf/sched_process_exec")
int BPF_PROG(sched_process_exec,struct task_struct *p, pid_t old_pid,
             struct linux_binprm *bprm) {
    struct event *e;
    e =bpf_ringbuf_reserve(&events,sizeof (*e),0);
    if (!e){
        return 0;
    }
    bpf_printk("filename : %s",bprm->filename); // 直接訪問(wèn)
    bpf_core_read(&e->filename,sizeof(e->filename),bprm->filename);
    e->pid=bpf_get_current_pid_tgid() >>32;
    bpf_get_current_comm(&e->command,sizeof(e->command));
    bpf_ringbuf_submit(e, 0);
    return 0;
}
內(nèi)核函數(shù)與 BPF 程序的橋梁:BPF Trampoline

BPF_PROG_TYPE_TRACING 類(lèi)型的 eBPF 程序通過(guò)不同的 Attach 類(lèi)型,可以實(shí)現(xiàn)不同的能力,除了支持 raw_tracepoint attach 類(lèi)型外,還支持 FENTRY/FEXIT。FENTRY、FEXIT 已經(jīng)是老朋友了,在前面介紹 Ftrace 時(shí)就有提到過(guò),這倆是用于函數(shù)追蹤的,F(xiàn)ENTRY 類(lèi)似于 kprobe、FEXIT 類(lèi)似于 kretprobe(除了函數(shù)返回值,F(xiàn)EXIT 還可以獲取到函數(shù)的參數(shù))。

它們依賴(lài) gcc 的 -pg -mentry 編譯參數(shù)在每個(gè)函數(shù)入口添加 fentry 調(diào)用,在不開(kāi)啟 fentry 時(shí),fentry 調(diào)用指令會(huì)被替換為 NOP 指令,避免影響性能,開(kāi)啟時(shí) fentry 指令會(huì)被替換為 BPF Trampoline 函數(shù)調(diào)用指令,在 BPF Trampoline 函數(shù)中會(huì)調(diào)用 eBPF 程序執(zhí)行。

BPF Trampoline 是一個(gè)內(nèi)核函數(shù)和 bpf 程序之間的一個(gè)橋梁,它允許內(nèi)核函數(shù)調(diào)用 BPF 程序,當(dāng)我們通過(guò) Fentry 機(jī)制 attach 到某個(gè)內(nèi)核函數(shù)時(shí),內(nèi)核會(huì)為這個(gè) eBPF 程序生成一個(gè) BPF Trampoline 函數(shù),被追蹤的內(nèi)核函數(shù)的參數(shù)會(huì)被轉(zhuǎn)換成 u64 數(shù)組,存儲(chǔ)到 Trampoline 函數(shù)棧中,指向這個(gè)棧的指針又存儲(chǔ)到 eBPF 程序可以訪問(wèn)的 R1 寄存器中,再根據(jù) BTF 信息,BPF 程序可以直接訪問(wèn)內(nèi)存了,同樣也不需要輔助函數(shù)來(lái)讀取數(shù)據(jù)。

Fentry、FEXIT 這種基于 Trampoline 方式的 probe handler 注入方式,沒(méi)有額外的 kprobe、perf event 數(shù)據(jù)結(jié)構(gòu)引入,其開(kāi)銷(xiāo)成本非常小,如果內(nèi)核支持 FENTRY 機(jī)制,函數(shù)追蹤場(chǎng)景使用 FENTRY 代替 kprobes 有更好的性能。

eBPF 如何從內(nèi)核態(tài)向用戶(hù)態(tài)傳遞數(shù)據(jù)?

BPF Map 是 eBPF 在用戶(hù)態(tài)和內(nèi)核態(tài)共享數(shù)據(jù)的方式,在上面的示例中我特意使用了 BPF ringbuffer Map 從內(nèi)核態(tài)向用戶(hù)態(tài)傳遞數(shù)據(jù),它需要內(nèi)核 5.8 及其以上的版本才可以使用。在此之前,perf event Map 是事實(shí)上的標(biāo)準(zhǔn),通過(guò) perf ring buffer 可以高效的在內(nèi)核態(tài)與用戶(hù)態(tài)之間傳遞數(shù)據(jù)。

但在實(shí)踐中發(fā)現(xiàn),perf ring buffer 存在兩個(gè)缺點(diǎn):內(nèi)存浪費(fèi)和數(shù)據(jù)亂序。

perf ring buffer 需要在每一個(gè) cpu 上創(chuàng)建,每一個(gè) cpu 都有可能執(zhí)行 BPF 代碼,產(chǎn)生的數(shù)據(jù)會(huì)存儲(chǔ)到當(dāng)前 CPU 的 perf ring buffer 上,如果某個(gè)時(shí)刻執(zhí)行的 BPF 程序可能會(huì)產(chǎn)生大量的數(shù)據(jù),perf ring buffer 空間滿(mǎn)了的情況下,就覆蓋掉老數(shù)據(jù),造成一部分?jǐn)?shù)據(jù)丟失,但是大部分情況下不會(huì)產(chǎn)生很多的數(shù)據(jù),針對(duì)這種情況,要么容忍數(shù)據(jù)丟失,要么就每個(gè) cpu 創(chuàng)建大容量的 perf ringbuffer,防止突發(fā)的數(shù)據(jù)暴增,但大部分時(shí)間空著。

同時(shí)每個(gè) cpu 具有獨(dú)立的 perf ring buffer,可能會(huì)導(dǎo)致連續(xù)的追蹤數(shù)據(jù)分布在不同的 perf ringbuffer 上,比如追蹤進(jìn)程的生命周期 fork、exec、exit,eBPF 程序在 3 個(gè)不同的 cpu 上執(zhí)行,用戶(hù)態(tài)是通過(guò)輪詢(xún) cpu 上 perf ringbuffer 來(lái)接收數(shù)據(jù)的,可能就會(huì)出現(xiàn) exit 事件比 exec 事件先接收。

perf ringbuffer 這兩個(gè)問(wèn)題并非無(wú)解,比如可以在構(gòu)建一個(gè)跨 cpu 的全局計(jì)數(shù)器,每一次往 perf ringbuffer 寫(xiě)入數(shù)據(jù)時(shí)帶上序列號(hào)。在用戶(hù)態(tài)聚合所有的 perf ringbuffer 上的數(shù)據(jù)時(shí),創(chuàng)建一個(gè)隊(duì)列,并根據(jù)序列號(hào)按序入隊(duì),這樣就可以保證事件的順序,這種方案總歸是增加了用戶(hù)態(tài)程序的復(fù)雜度和帶來(lái)額外的成本。

為此社區(qū)內(nèi)提出了一個(gè)新的 ring buffer 設(shè)計(jì),BPF ringbuffer,它是一個(gè)跨 CPU 共享、MPSC 模型的 ringbuffer,可以直接通過(guò) mmap 機(jī)制映射到用戶(hù)態(tài)訪問(wèn) ringbuffer。對(duì)于低效率內(nèi)存使用的問(wèn)題,由于是跨 cpu 共享的 ring buffer, 所以這個(gè)問(wèn)題就不存在了;對(duì)于數(shù)據(jù)亂序的問(wèn)題,每個(gè)事件被寫(xiě)入 bpf ringbuffer 時(shí)都會(huì)被分配一個(gè)唯一的 sequence number,并且 sequence number 會(huì)遞增。這樣,在讀取 buffer 數(shù)據(jù)時(shí),可以根據(jù) sequence number 來(lái)判斷哪些事件先發(fā)生,哪些事件后發(fā)生,從而保證讀取的數(shù)據(jù)是有序的。

應(yīng)該選擇哪個(gè)內(nèi)核追蹤技術(shù)?

Brendan Gregg 博客中有一片文章討論了選擇哪個(gè) trace 追蹤工具(發(fā)布于 2015 年),我認(rèn)為直到現(xiàn)在依然有幫助(Choosing a Linux Tracer (2015)),于我個(gè)人而言,排查問(wèn)題和檢測(cè)性能時(shí),我會(huì)優(yōu)先考慮 perf 系列的工具,它可以幫助我獲取追蹤數(shù)據(jù),并快速的得到一個(gè)分析結(jié)果。如果構(gòu)建一個(gè)常駐的內(nèi)核追蹤程序,eBPF 是我的好幫手,它具備可編程性,可以讓我在多個(gè)節(jié)點(diǎn)上按照期望的方式拿到追蹤數(shù)據(jù)并匯總計(jì)算。

總 結(jié)

(kprobes、uprobes)、tracepoint、fprobe(fentry/fexit) 是注入 probe handler 調(diào)用的機(jī)制。kprobes、uprobes 通過(guò)動(dòng)態(tài)指令替換實(shí)現(xiàn)在指令執(zhí)行時(shí)調(diào)用 probe handler。

tracepoint 是代碼里靜態(tài)聲明了 probe handler 的調(diào)用,提供 probe handler 的注冊(cè)接口,內(nèi)核開(kāi)發(fā)者定義發(fā)給 probe handler 的追蹤數(shù)據(jù),執(zhí)行 tracepoint 時(shí)將追蹤數(shù)據(jù)傳遞給 probe handler,可以動(dòng)態(tài)開(kāi)啟和關(guān)閉,tracepoint 由內(nèi)核開(kāi)發(fā)者維護(hù),穩(wěn)定性很好。

fprobe(fentry/fexit) 是通過(guò)在內(nèi)核編譯期間對(duì)函數(shù)添加第三方調(diào)用,可以動(dòng)態(tài)開(kāi)啟和關(guān)閉,達(dá)到了類(lèi)似于 tracepoint 的效果,除了 frpobe ,eBPF 同樣也可以實(shí)現(xiàn) fentry/fexit 的機(jī)制,他們都是通過(guò) Trampoline 來(lái)跳轉(zhuǎn)到 probe handler 執(zhí)行。

probe handler 在內(nèi)核態(tài)執(zhí)行,抓取到的追蹤數(shù)據(jù)往往需要傳遞到用戶(hù)態(tài)做分析使用,perf_event、trace_event_ring_buffer、eBPF Map 是從內(nèi)核態(tài)向用戶(hù)態(tài)傳遞數(shù)據(jù)的方式。

perf_event 存儲(chǔ)的追蹤數(shù)據(jù)可以通過(guò) MMAP 映射到用戶(hù)態(tài)來(lái)訪問(wèn)。trace_event_ring_buffer 是通過(guò)虛擬文件系統(tǒng) TraceFS 的方式暴露追蹤數(shù)據(jù)。eBPF Map 有多種實(shí)現(xiàn)方式,有基于 perf event 的、有基于系統(tǒng)調(diào)用的,有基于 BPF ringbuffer 的。

1a7f6f3e-07fd-11ee-962d-dac502259ad0.png






審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 寄存器
    +關(guān)注

    關(guān)注

    31

    文章

    5434

    瀏覽量

    124523
  • 中斷處理
    +關(guān)注

    關(guān)注

    0

    文章

    94

    瀏覽量

    11260
  • LINUX內(nèi)核
    +關(guān)注

    關(guān)注

    1

    文章

    317

    瀏覽量

    22407
  • gcc編譯器
    +關(guān)注

    關(guān)注

    0

    文章

    78

    瀏覽量

    3749

原文標(biāo)題:萬(wàn)字長(zhǎng)文解讀 Linux 內(nèi)核追蹤機(jī)制

文章出處:【微信號(hào):良許Linux,微信公眾號(hào):良許Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評(píng)論

    相關(guān)推薦
    熱點(diǎn)推薦

    富士康在美拿到285億補(bǔ)貼 美媒寫(xiě)2萬(wàn)字長(zhǎng)文討伐

    預(yù)計(jì)兩年時(shí)間建成,6月剛由特朗普親自奠基的富士康美國(guó)工廠正遭受當(dāng)?shù)剌浾撚懛?。?dāng)?shù)貢r(shí)間29日,美國(guó)媒體TheVerge發(fā)表一篇超2萬(wàn)字的文章,痛斥威斯康星州政府41億美元(約合人民幣285億元)巨資
    發(fā)表于 11-02 09:12 ?2441次閱讀

    Linux內(nèi)核解讀入門(mén)

    Linux內(nèi)核解讀入門(mén)關(guān)鍵詞:Linux, 內(nèi)核,源代碼一.核心源程序的文件組織: 1. Linux
    發(fā)表于 01-16 14:40 ?103次下載

    淺談Linux內(nèi)核解讀入門(mén)

    針對(duì)好多Linux 愛(ài)好者對(duì)內(nèi)核很有興趣卻無(wú)從下口,本文旨在介紹一種解讀linux內(nèi)核源碼的入門(mén)方法,而不是解說(shuō)
    發(fā)表于 11-08 10:06 ?2次下載

    探究物聯(lián)網(wǎng)行業(yè)的一線(xiàn)聲音

    在這篇萬(wàn)字長(zhǎng)文中,共有45家企業(yè)說(shuō)出了關(guān)于物聯(lián)網(wǎng)行業(yè)的,企業(yè)的年度感想。
    的頭像 發(fā)表于 02-05 17:16 ?4626次閱讀

    李書(shū)福萬(wàn)字長(zhǎng)文確認(rèn)“藍(lán)色吉利行動(dòng)”失敗,將組建全新純電車(chē)公司

    2月20日,吉利集團(tuán)舉行內(nèi)部會(huì)議,董事長(zhǎng)李書(shū)福發(fā)表了萬(wàn)字講話(huà),對(duì)汽車(chē)行業(yè)的未來(lái)發(fā)展和吉利的應(yīng)對(duì)布局等給出了一系列判斷與思考。 李書(shū)福認(rèn)為,“汽車(chē)產(chǎn)業(yè)革命已經(jīng)開(kāi)始‘暴動(dòng)’,從理論到實(shí)踐、從傳聞到現(xiàn)實(shí)
    的頭像 發(fā)表于 02-25 10:25 ?2037次閱讀

    Linux內(nèi)核文件Cache機(jī)制

    Linux內(nèi)核文件Cache機(jī)制(開(kāi)關(guān)電源技術(shù)與設(shè)計(jì) 第二版)-Linux內(nèi)核文件Cache機(jī)制
    發(fā)表于 08-31 16:34 ?4次下載
    <b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>文件Cache<b class='flag-5'>機(jī)制</b>

    人工智能300年!LSTM之父萬(wàn)字長(zhǎng)文:詳解現(xiàn)代AI和深度學(xué)習(xí)發(fā)展史

    來(lái)源:新智元 編輯:昕朋 好困 導(dǎo)讀 最近,LSTM之父Jürgen Schmidhuber梳理了17世紀(jì)以來(lái)人工智能的歷史。在這篇萬(wàn)字長(zhǎng)文中,Schmidhuber為讀者提供了一個(gè)大事年表,其中
    的頭像 發(fā)表于 01-10 12:25 ?849次閱讀

    萬(wàn)字長(zhǎng)文聊聊“車(chē)規(guī)級(jí)”芯片

    AEC-Q100 是一種基于封裝集成電路應(yīng)力測(cè)試的失效機(jī)制。汽車(chē)電子委員會(huì)(AEC)總部設(shè)在美國(guó),最初由三大汽車(chē)制造商(克萊斯勒、福特和通用汽車(chē))建立,目的是建立共同的零部件資格和質(zhì)量體系標(biāo)準(zhǔn)。
    的頭像 發(fā)表于 03-10 09:28 ?2231次閱讀

    萬(wàn)字長(zhǎng)文聊聊“車(chē)規(guī)級(jí)”芯片

    AEC-Q100 是一種基于封裝集成電路應(yīng)力測(cè)試的失效機(jī)制。汽車(chē)電子委員會(huì)(AEC)總部設(shè)在美國(guó),最初由三大汽車(chē)制造商(克萊斯勒、福特和通用汽車(chē))建立,目的是建立共同的零部件資格和質(zhì)量體系標(biāo)準(zhǔn)。
    的頭像 發(fā)表于 03-29 10:46 ?1682次閱讀

    人工智能300年!LSTM之父萬(wàn)字長(zhǎng)文:詳解現(xiàn)代AI和深度學(xué)習(xí)發(fā)展史

    來(lái)源:新智元編輯:昕朋好困導(dǎo)讀最近,LSTM之父JürgenSchmidhuber梳理了17世紀(jì)以來(lái)人工智能的歷史。在這篇萬(wàn)字長(zhǎng)文中,Schmidhuber為讀者提供了一個(gè)大事年表,其中包括神經(jīng)網(wǎng)絡(luò)
    的頭像 發(fā)表于 01-13 11:02 ?1347次閱讀
    人工智能300年!LSTM之父<b class='flag-5'>萬(wàn)字長(zhǎng)文</b>:詳解現(xiàn)代AI和深度學(xué)習(xí)發(fā)展史

    萬(wàn)字長(zhǎng)文盤(pán)點(diǎn)!2022十大AR工業(yè)典型案例,不可不看!

    萬(wàn)字長(zhǎng)文盤(pán)點(diǎn)!2022十大AR工業(yè)典型案例,不可不看!
    的頭像 發(fā)表于 01-17 14:43 ?2777次閱讀
    近<b class='flag-5'>萬(wàn)字長(zhǎng)文</b>盤(pán)點(diǎn)!2022十大AR工業(yè)典型案例,不可不看!

    如何用AI聊天機(jī)器人寫(xiě)出萬(wàn)字長(zhǎng)文

    如何用AI聊天機(jī)器人寫(xiě)出萬(wàn)字長(zhǎng)文
    的頭像 發(fā)表于 12-26 16:25 ?1381次閱讀

    阿里通義千問(wèn)重磅升級(jí),免費(fèi)開(kāi)放1000萬(wàn)字長(zhǎng)文檔處理功能

    近日,阿里巴巴旗下的人工智能應(yīng)用通義千問(wèn)迎來(lái)重磅升級(jí),宣布向所有人免費(fèi)開(kāi)放1000萬(wàn)字長(zhǎng)文檔處理功能,這一創(chuàng)新舉措使得通義千問(wèn)成為全球文檔處理容量第一的AI應(yīng)用。
    的頭像 發(fā)表于 03-26 11:09 ?1189次閱讀

    詳解linux內(nèi)核的uevent機(jī)制

    linux內(nèi)核中,uevent機(jī)制是一種內(nèi)核和用戶(hù)空間通信的機(jī)制,用于通知用戶(hù)空間應(yīng)用程序各種硬件更改或其他事件,比如插入或移除硬件設(shè)備(
    的頭像 發(fā)表于 09-29 17:01 ?1910次閱讀

    萬(wàn)字長(zhǎng)文,看懂激光基礎(chǔ)知識(shí)!

    深入介紹激光基礎(chǔ)知識(shí),幫助您輕松理解激光領(lǐng)域的關(guān)鍵概念和原理。
    的頭像 發(fā)表于 12-20 09:49 ?1087次閱讀
    <b class='flag-5'>萬(wàn)字長(zhǎng)文</b>,看懂激光基礎(chǔ)知識(shí)!