BCC和libbpf的轉換

BCC和libbpf的轉換

本文講述如何將基於BCC的BPF應用轉換為libbpf + BPF CO-RE。BPF CO-RE可以參見上一篇博文

為什麼是libbpf和BPF CO-RE?

歷史上,當需要開發一個BPF應用時可以選擇BCC 框架,在實現各種用於Tracepoints的BPF程式時需要將BPF程式載入到內核中。BCC提供了內置的Clang編譯器,可以在運行時編譯BPF程式碼,並將其訂製為符合特定主機內核的程式。這是在不斷變化的內核內部下開發可維護的BPF應用程式的唯一方法。在BPF的可移植性和CO-RE一文中詳細介紹了為什麼會這樣,以及為什麼BCC是之前唯一的可行方式,此外還解釋了為什麼 libbpf是目前比較好的選擇。去年,Libbpf的功能和複雜性得到了重大提升,消除了與BCC之間的很多差異(特別是對Tracepoints應用來說),並增加了很多BCC不支援的新的且強大的特性(如全局變數和BPF skeletons)。

誠然,BCC會竭盡全力簡化BPF開發人員的工作,但有時在獲取便利性的同時也增加了問題定位和修復的困難度。用戶必須記住其命名規範以及自動生成的用於Tracepoints的結構體,且必須依賴這些程式碼的重寫來讀取內核數據和獲取kprobe參數。當使用BPF map時,需要編寫一個半面向對象的C程式碼,這與內核中發生的情況並不完全匹配。除此之外,BCC使得用戶在用戶空間編寫了大量樣板程式碼,且需要手動配置最瑣碎的部分。

如上所述,BCC依賴運行時編譯,且本身嵌入了龐大的LLVM/Clang庫,由於這些原因,BCC與理想的使用有一定差距:

  • 編譯時的高資源利用率(記憶體和CPU),在繁忙的伺服器上時有可能干擾主流程。
  • 依賴內核頭文件包,不得不在每台目標主機上進行安裝。即使這樣,如果需要某些沒有通過公共頭文件暴露的內核內容時,需要將類型定義拷貝黏貼到BPF程式碼中,通過這種方式達成目的。
  • 即使是很小的編譯時錯誤也只能在運行時被檢測到,之後不得不重新編譯並重啟用戶層的應用;這大大影響了開發的迭代時間(並增加了挫敗感…)

Libbpf + BPF CO-RE (Compile Once – Run Everywhere) 選擇了一個不同的方式,其思想在於將BPF程式視為一個普通的用戶空間的程式:僅需要將其編譯成一些小的二進位,然後不用經過修改就可以部署到目的主機上。libbpf扮演了BPF程式的載入器,負責配置工作(重定位,載入和校驗BPF程式,創建BPF maps,附加到BPF鉤子上等),開發者僅需要關注BPF程式的正確性和性能即可。這種方式使得開銷降到了最低,消除了大量依賴,提升了整體開發者的開發體驗。

在API和程式碼約定方面,libbpf堅持”最少意外”的哲學,即大部分內容都需要明確地闡述:不會隱含任何頭文件,也不會重寫程式碼。僅使用簡單的C程式碼和適當的輔助宏即可消除大部分單調的環節。 此外,用戶編寫的是需要執行的內容,BPF應用程式的結構是一對一的,最終由內核驗證並執行。

本指南用於簡單快速地將BCC轉換為libbpf+BPF CO-RE。本文解釋了多種預配置步驟,並概述了常見的模式,以及可能會碰到的不同點,困難和陷阱。

一開始將BCC轉換為普通的BPF CO-RE時,可能會感到不適和困惑,但很快就會掌握它,並在下次遇到編譯或驗證問題時欣賞libbpf的明確性和直接性。

此外,注意BPF CO-RE用到的很多Clang特性都比較新,需要用到Clang 10或更新的版本

可以參照官方文檔升級Clang:

  1. git clone //github.com/llvm/llvm-project.git
  2. Build LLVM and Clang:
    1. cd llvm-project
    2. mkdir build (in-tree build is not supported)
    3. cd build
    4. cmake -DLLVM_ENABLE_PROJECTS=clang -G "Unix Makefiles" ../llvm
    5. make

注意:在2.3步執行cmake時,可能會因為Host GCC version must be at least 5.1,這樣的錯誤,需要升級GCC,升級之後刪除build再重新編譯即可。但有時即便GCC升級成功,且清除build中的快取,再次編譯時還是會出現上述錯誤,可以手動指定GCC路徑來解決該問題:

 CC=$HOME/toolchains/bin/gcc cmake -DLLVM_ENABLE_PROJECTS=clang -G "Unix Makefiles" ../llvm

另外就是在執行make命令時會執行lib庫的編譯和鏈接,在鏈接過程中會佔用大量記憶體,建議在執行該命令時打開(或擴大)系統的swap功能,防止記憶體不足導致系統出問題。

配置用戶空間

生成必要的內容

構建基於libbpf的BPF應用需要使用BPF CO-RE包含的幾個步驟:

  • 生成帶所有內核類型的頭文件vmlinux.h
  • 使用Clang(版本10或更新版本)將BPF程式的源程式碼編譯為.o對象文件
  • 從編譯好的BPF對象文件中生成BPF skeleton 頭文件 (BPF skeleton 頭文件內容來自上一步生成的.o文件,可以參考libbpf-tools的Makefile文件,可以看到 skeleton 頭文件其實是通過bpftool gen命令生成的)
  • 在用戶空間程式碼中包含生成的BPF skeleton 頭文件
  • 最後,編譯用戶空間程式碼,這樣會嵌入BPF對象程式碼,後續就不用發布單獨的文件

具體步驟依賴用戶指定的配置和構建系統,此處不一一列出。一種方式是參考BCC』s libbpf-tools,它給出了一個通用的Makefile文件,可以通過該文件來檢查環境配置。

當編譯BPF程式碼並生成BPF skeleton後,需要在用戶空間程式碼中包含libbpf和skeleton頭文件:

#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include "path/to/your/skeleton.skel.h"

Locked記憶體的限制

BPF的BPF maps以及其他內容使用了locked類型的記憶體。默認的限制非常低,因此,除非增加該值,否則有可能連一個很小的BPF程式都無法載入。BCC會無條件地將限制設置為無限大,但libbpf不會自動進行設置。

生產環境中可能會有更好的方式來設置locked記憶體的限制。但為了快速實驗或在沒有更好的辦法時,可以通過setrlimit(2)系統調用進行設置(在程式開始前調用)。

    #include <sys/resource.h>

    rlimit rlim = {
        .rlim_cur = 512UL << 20, /* 512 MBs */
        .rlim_max = 512UL << 20, /* 512 MBs */
    };

    err = setrlimit(RLIMIT_MEMLOCK, &rlim);
    if (err)
        /* handle error */

Libbpf 日誌

如果程式運行不正常,最好的方式是檢查libbpf的日誌輸出。libbpf會以多種級別輸出大量有用的日誌。默認會輸出error級別的日誌。建議安裝一個自定義的日誌回調,這樣就可以配置日誌的輸出級別:

int print_libbpf_log(enum libbpf_print_level lvl, const char *fmt, va_list args) {
    if (!FLAGS_bpf_libbpf_debug && lvl >= LIBBPF_DEBUG)
        return 0;
    return vfprintf(stderr, fmt, args);
}

/* ... */

libbpf_set_print(print_libbpf_log); /* set custom log handler */

BPF skeleton 和 BPF app 生命周期

對BPF skeleton(以及libbpf API)的詳細介紹和使用超出了本文檔的範疇,內核selftests以及BCC提供的libbpf-tools 例子可以幫助熟悉這部分內容。查看runqslower 示例,它是一個使用skeleton的簡單卻真實的工具。

儘管如此,了解主要的libbpf概念和每個BPF應用經過的階段是很有用的。BPF應用包含一組BPF程式(合作或完全獨立),以及在所有的BPF程式間共享的BPF maps和全局變數(允許操作共同的數據)。BPF 也可以在用戶空間(我們將用戶空間中的程式稱為”控制app”)中訪問maps和全局變數,允許控制app獲取或設置必要的額外數據。BPF應用通常會經過如下階段:

  • 打開階段:BPF對象文件的解析:發現但尚未創建的BPF maps,BPF程式和全局變數。在BPF app打開後,可以在所有的表項創建並載入前進行任何額外的調整(設置BPF類型;預設值全局變數的初始值等);
  • 載入階段:創建BPF maps並解決了符號重定位之後,BPF程式會被載入到內核進行校驗。此時,BPF程式所有的部分都是有效且存在於內核中的,但此時的BPF並沒有被執行。在載入階段之後,可以配置BPF map狀態的初始值,此時不會導致BPF程式程式碼競爭性地執行;
  • 附加階段:此階段中,BPF程式會附加到各種BPF鉤子上(如Tracepoints,kprobes,cgroup鉤子,網路報文處理流水線等)。此時,BPF會開始執行有用的工作,並讀取/更新BPF maps和全局變數;
  • 清理階段:分離並從內核卸載BPFBPF程式。銷毀BPF maps,並釋放所有的BPF使用的資源。

生成的BPF skeleton 使用如下函數觸發相應的階段:

  • <name>__open() – 創建並打開 BPF 應用(例如的runqslowerrunqslower_bpf__open()函數);
  • <name>__load() – 初始化,載入和校驗BPF 應用部分;
  • <name>__attach() – 附加所有可以自動附加的BPF程式 (可選,可以直接使用libbpf API作更多控制);
  • <name>__destroy() – 分離所有的 BPF 程式並使用其使用的所有資源.

BPF 程式碼轉換

本章節會檢查常用的轉換流,並概述BCC和libbpf/BPF CO-RE之間存在的典型的不匹配。通過本章節,希望可以使你的BPF程式碼能夠同時兼容BCC和BPF CO-RE。

檢測BCC與libbpf模式

在需要同時支援BCC和libbpf模式的場景下,需要檢測BPF程式程式碼能夠編譯為哪種模式。最簡單的方式是依賴BCC中的宏BCC_SEC

#ifdef BCC_SEC
#define __BCC__
#endif

之後,在整個BPF程式碼中,可以執行以下操作:

#ifdef __BCC__
/* BCC-specific code */
#else
/* libbpf-specific code */
#endif

這樣就可以擁有通用的BPF源程式碼,並且只有必要的邏輯程式碼段才是BCC或libbpf特定的。

頭文件包含

使用 libbpf/BPF CO-RE時,不需要包含內核頭文件(如#include <linux/whatever.h>),僅需要包含一個vmlinux.h和少量libbpf輔助功能的頭文件:

#ifdef __BCC__
/* linux headers needed for BCC only */
#else /* __BCC__ */
#include "vmlinux.h"   /* all kernel types */
#include <bpf/bpf_helpers.h>  /* most used helpers: SEC, __always_inline, etc */
#include <bpf/bpf_core_read.h>  /* for BPF CO-RE helpers */
#include <bpf/bpf_tracing.h>    /* for getting kprobe arguments */
#endif /* __BCC__ */

vmlinux.h可能不包含某些有用的內核#define定義的常量,此時需要重新聲明這些變數。但bpf_helpers.h中提供了大部分常用的變數。

欄位訪問

BCC會默默地重寫你的BPF程式碼,並將諸如tsk-> parent-> pid之類的欄位訪問轉換為一系列的bpf_probe_read()調用。Libbpf/BPF CO-RE沒有此項功能,但bpf_core_read.h提供了一系列普通C程式碼編寫的輔助函數來完成類似的工作。上述的tsk->parent->pid會變成BPF_CORE_READ(tsk, parent, pid)。從Linux 5.5開始使用tp_btffentry/fexit BPF程式類型,使用的也是C語法。但對於老版本的內核以及其他BPF程式類型(如Tracepoints和kprobe),最好將其轉換為BPF_CORE_READ

BPF_CORE_READ宏也可以工作在BCC模式下,因此為了避免在#ifdef __BCC__/#else/#endif中重複使用,可以將所有欄位的讀取轉換為BPF_CORE_READ,這樣就可以同時給BCC和libbpf模式使用。使用BCC時,需要確保包含 bpf_core_read.h頭文件。

BPF maps

BCC 和libbpf對BPF maps的聲明是不同的,但轉換方式很直接,下面是一些例子:

/* Array */
#ifdef __BCC__
BPF_ARRAY(my_array_map, struct my_value, 128);
#else
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 128);
    __type(key, u32);
    __type(value, struct my_value);
} my_array_map SEC(".maps");
#endif

/* Hashmap */
#ifdef __BCC__
BPF_HASH(my_hash_map, u32, struct my_value);
#else
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, u32);
    __type(value, struct my_value);
} my_hash_map SEC(".maps")
#endif

/* Per-CPU array */
#ifdef __BCC__
BPF_PERCPU_ARRAY(heap, struct my_value, 1);
#else
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 1);
    __type(key, u32);
    __type(value, struct my_value);
} heap SEC(".maps");
#endif

請注意BCC中maps的默認大小,通常為10240。使用libbpf時必須明確指定大小。

PERF_EVENT_ARRAY, STACK_TRACE和其他特殊的maps(DEVMAP, CPUMAP, etc) 尚不支援鍵/值類型的BTF類型,因此需要直接指定key_size/value_size:

/* Perf event array (for use with perf_buffer API) */
#ifdef __BCC__
BPF_PERF_OUTPUT(events);
#else
struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u32));
} events SEC(".maps");
#endif

訪問BPF程式碼中的BPF maps

BCC使用偽C++語言處理maps,在幕後將其重寫為實際的BPF輔助調用,通常使用如下模式:

some_map.operation(some, args)

將其重寫為如下格式:

bpf_map_operation_elem(&some_map, some, args);

下面是一些例子:

#ifdef __BCC__
    struct event *data = heap.lookup(&zero);
#else
    struct event *data = bpf_map_lookup_elem(&heap, &zero);
#endif

#ifdef __BCC__
    my_hash_map.update(&id, my_val);
#else
    bpf_map_update_elem(&my_hash_map, &id, &my_val, 0 /* flags */);
#endif

#ifdef __BCC__
    events.perf_submit(args, data, data_len);
#else
    bpf_perf_event_output(args, &events, BPF_F_CURRENT_CPU, data, data_len);
#endif

BPF程式

所有BPF程式提供的功能都需要通過SEC()(來自bpf_helpers.h)宏來自定義section名稱,如:

#if !defined(__BCC__)
SEC("tracepoint/sched/sched_process_exec")
#endif
int tracepoint__sched__sched_process_exec(
#ifdef __BCC__
    struct tracepoint__sched__sched_process_exec *args
#else
    struct trace_event_raw_sched_process_exec *args
#endif
) {
/* ... */
}

這只是一個約定,但如果遵循libbpf的section名稱,會有更好的開發體驗。期望的名稱可以參見此處(原文中給出的程式碼行可能不準,參見section_defs的定義即可),通常的用法為:

  • tp/<category>/<name> 用於Tracepoints;
  • kprobe/<func_name> 用於kprobe ,kretprobe/<func_name> 用於kretprobe;
  • raw_tp/<name> 用於原始Tracepoint;
  • cgroup_skb/ingress, cgroup_skb/egress,以及整個cgroup/<subtype> 程式家族。

Tracepoints

從上面的例子中可以看到,Tracepoint上下文的類型名稱略有不同。BCC允許Tracepoint使用tracepoint__<category>__<name>命名模式。BCC會在編譯時自動生成相應的類型。libbpf沒有此功能,但幸運的是,內核已經提供了所有Tracepoint數據的類似類型,一般命名為trace_event_raw_<name>,但有時內核中的少量Tracepoints會重用常用的類型,因此如果上述模式不起作用,則需要在內核源碼(或 vmlinux.h)中查找具體的類型名稱。如必須使用struct trace_event_raw_sched_process_template來代替struct trace_event_raw_sched_process_exit

在大多數情況下,用於訪問tracepoint 上下文數據的程式碼完全相同,但特殊的可變長度字元串欄位除外。對於此類情況,其轉換也很直接:data_loc_<some_field>變為__data_loc_<some_field>(注意雙下劃線)即可。

Kprobes

BCC有很多種方式聲明kprobe。實踐中,這類BPF程式會接收一個指向struct pt_regs的指針作為上下文參數,但BCC允許像使用內核函數參數一樣給BPF程式傳參。使用libbpf的BPF_KPROBE宏可以獲得類似的效果,目前其存在於內核selftest的bpf_trace_helpers.h頭文件中,但後續應該會作為libbpf的一部分(已經是了):

#ifdef __BCC__
int kprobe__acct_collect(struct pt_regs *ctx, long exit_code, int group_dead)
#else
SEC("kprobe/acct_collect")
int BPF_KPROBE(kprobe__acct_collect, long exit_code, int group_dead)
#endif
{
    /* BPF code accessing exit_code and group_dead here */
}

對於有返回值的kprobe,也有對應的宏BPF_KRETPROBE

注意:在4.17 內核中,Syscall 函數發生了重命名。從4.17 版本開始,用於Syscall krpobe調用的sys_kill對應當前的__x64_sys_kill(在x64系統上,不同的架構具有不同的前綴)。在附加一個kprobe/kretprobe時應該注意這一點。但如果可能的話,儘可能遵循tracepoints。

如果要開發一個新的,帶tracepoint/kprobe/kretprobe的BPF程式,查看新的raw_tp/fentry/fexit 探針,它們提供了更好的性能和易用性(內核5.5開始提供此功能)。

在BCC中處理編譯時的#if

在BCC模式中大量使用了預處理#ifdef 和 #if 條件。大部分是因為支援不同的內核版本或啟用/禁用可選擇的邏輯(依賴應用配置)。此外,BCC允許在用戶空間側提供自定義的#define,在BPF程式碼編譯期間的運行時階段進行替換。通常用於自定義各種參數。

不能使用libbpf + BPF CO-RE做類似的事情(通過編譯時(compile-time)邏輯),原因是BPF程式遵循一次編譯就可以在所有可能的內核以及應用配置上運行。

為了處理不同的內核版本,BPF CO-RE支援兩種補充機制:Kconfig externsstruct 「flavors」(在上一篇部落格中有涉及)。通過聲明外部變數,BPF程式碼可以知道處理的內核版本:

#define KERNEL_VERSION(a, b, c) (((a) << 16) + ((b) << 8) + (c))

extern int LINUX_KERNEL_VERSION __kconfig;

if (LINUX_KERNEL_VERSION < KERNEL_VERSION(5, 2, 0)) {
  /* deal with older kernels */
} else {
  /* 5.2 or newer */
}

類似地,可以通過從Kconfig(位於內核的.config文件中)中抽取類似CONFIG_xxx的變數來獲取內核版本:

extern int CONFIG_HZ __kconfig;

/* now you can use CONFIG_HZ in calculations */

通常,如果重命名了一個欄位,或將其移入一個子結構體中時,可以通過檢查目標內核是否存在該欄位來判斷是否發生了這種情況。可以通過bpf_core_field_exists(<field>)實現,如果返回1,則表示目標欄位位於目標內核中;返回0則表示不存在內核中。配合struct flavors,可以處理內核結構布局的發生重大變動的情況。下面是一個簡短的例子,展示了如何適應 struct kernfs_iattrs在不同內核版本中的變化:

/* struct kernfs_iattrs will come from vmlinux.h */

struct kernfs_iattrs___old {
    struct iattr ia_iattr;
};

if (bpf_core_field_exists(root_kernfs->iattr->ia_mtime)) {
    data->cgroup_root_mtime = BPF_CORE_READ(root_kernfs, iattr, ia_mtime.tv_nsec);
} else {
    struct kernfs_iattrs___old *root_iattr = (void *)BPF_CORE_READ(root_kernfs, iattr);
    data->cgroup_root_mtime = BPF_CORE_READ(root_iattr, ia_iattr.ia_mtime.tv_nsec);
}

應用配置

BPF CO-RE的辦法是使用全局變數自定義程式的行為。全局變數允許用戶空間app在BPF程式載入和校驗前預配置必要的參數和標誌。全局變數可以是可變的或恆定的。常量(只讀)最常用於指定一個BPF程式的一次性配置(在程式載入和校驗前)。可變的量在BPF程式載入並運行後,可用於BPF程式與其用戶空間副本之間的雙向數據交換。

在BPF程式碼側,可以使用一個const volatile全局變數(當用於可變的量時,只需丟棄const volatile修飾符)聲明只讀的全局變數。

const volatile struct {
    bool feature_enabled;
    int pid_to_filter;
} my_cfg = {};

有如下幾點需要重點關註:

  • 必須指定const volatile來防止不合時宜的編譯器優化(編譯器可能並且會錯誤地採用零值並將其內聯到程式碼中);
  • 如果定義了一個可變的(非const)量時,確保不會被標記為static:非靜態全局變數最好與編譯器配合。這種情況下通常不需要volatile
  • 變數需要被初始化,否則libbpf會拒絕載入BPF應用。初始值可以為0或其他任意值。這類值作為變數的默認值,除非在控制應用程式中覆蓋。

使用BPF程式碼中的全局變數很簡單:

if (my_cfg.feature_enabled) {
    /* … */
}

if (my_cfg.pid_to_filter && pid == my_cfg.pid_to_filter) {
    /* … */
}

全局變數提供了更好的用戶體驗,並避免了BPF map查詢造成的開銷。此外,對於不變的量,它們的值是對BPF驗證器來說是透明的(眾所周知的),並在程式驗證期間將其視為常量。這種方式可以允許BPF校驗器精確且高效地消除無用程式碼分支。

控制app可以使用BPF skeleton方便地提供這類變數:

struct <name> *skel = <name>__open();
if (!skel)
    /* handle errors */

skel->rodata->my_cfg.feature_enabled = true;
skel->rodata->my_cfg.pid_to_filter = 123;

if (<name>__load(skel))
    /* handle errors */

只讀變數可以在BPF skeleton載入前在用戶空間進行設置和修改。一旦載入了BPF程式,則無法在用戶空間進行設置和修改。這保證BPF校驗器在校驗期間將這類變數視為常數,以便更好地移除無效程式碼。而非常量則可以在BPF skeleton載入之後的整個生命周期中(從BPF和用戶空間)進行修改,這些變數可以用於交換可變的配置,狀態等等。

常見的問題

在運行BPF程式時可能會遇到各種問題。有時只是一個誤解,有時是因為BCC和libbpf實現上的差異導致的。下面給出了一些典型的場景,可以幫助更好地進行BCC到BPF CO-RE的轉換。

全局變數

BPF全局變數看起來就像一個用戶空間的變數:它們可以在表達式中使用,也可以更新(非const表達式),甚至可以使用它們的地址並傳遞到輔助函數中。但這是在BPF程式碼側有效。在用戶空間側,只能通過BPF skeletob進行讀取和更新。

  • skel->rodata 用於只讀變數;
  • skel->bss 用於初始值為0的可變數;
  • skel->data 用於初始值非0的可變數。

可以在用戶空間進行讀取/更新,這些更新會立即反映到BPF側。但在用戶空間側,這些變數並不是全局的,它們只是BPF skeleton的rodatabss、或data的成員,在skeleton 載入期間進行了初始化。因此意味著在BPF程式碼和用戶空間程式碼中聲明完全相同的全局變數將視為完全獨立的變數,在任何情況下都不會出現交集。

循環展開

除非目標內核為5.3以上的版本,否則BPF程式碼中的所有循環都必須使用#pragma unroll標識,強制Clang進行循環展開,並消除所有可能的循環控制流:

#pragma unroll
for (i = 0; i < 10; i++) { ... }

如果沒有循環展開,或循環沒有在固定迭代之後結束,那麼會返回一個”back-edge from insn X to Y”的校驗器錯誤,即BPF校驗器檢測到了一個無限循環(或無法在有限次數的迭代之後結束的循環)。

輔助子程式

如果使用靜態輔助函數,則必須將其標記為static __always_inline(由於當前libbpf的處理限制):

static __always_inline unsigned long
probe_read_lim(void *dst, void *src, unsigned long len, unsigned long max)
{
    ...
}

從5.5內核開始支援非內聯的全局函數,但它們具有與靜態函數不同的語義和校驗限制,這種情況下,最好也使用內核標記!

bpf_printk 調試

BPF程式沒有常規調試器可以用於設置斷點,檢查變數和BPF maps,以及程式碼的單步調試等。使用這類工具通常無法確定BPF程式碼的問題所在。

這種情況下,使用日誌輸出是最好的選擇。使用bpf_printk(fmt, args...)列印輸出額外的資訊來理解發生的事情。該函數接受printf類的格式,最大支援3個參數。它的使用非常簡單,但開銷也比較大,不適合用於生產環境,因此僅適用於臨時調試:

char comm[16];
u64 ts = bpf_ktime_get_ns();
u32 pid = bpf_get_current_pid_tgid();

bpf_get_current_comm(&comm, sizeof(comm));
bpf_printk("ts: %lu, comm: %s, pid: %d\n", ts, comm, pid);

日誌資訊可以從一個特殊的/sys/kernel/debug/tracing/trace_pipe文件中讀取:

$ sudo cat /sys/kernel/debug/tracing/trace_pipe
...
      [...] ts: 342697952554659, comm: runqslower, pid: 378
      [...] ts: 342697952587289, comm: kworker/3:0, pid: 320
...
Tags: