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:
git clone https://github.com/llvm/llvm-project.git- Build LLVM and Clang:
cd llvm-projectmkdir build(in-tree build is not supported)cd buildcmake -DLLVM_ENABLE_PROJECTS=clang -G "Unix Makefiles" ../llvmmake注意:在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功能,防止內存不足導致系統出問題。此外磁盤建議不少於100G。
配置用戶空間
生成必要的內容
構建基於libbpf的BPF應用需要使用BPF CO-RE包含的幾個步驟:
- 生成帶所有內核類型的頭文件
vmlinux.h; - 使用Clang(版本10或更新版本)將BPF程序的源代碼編譯為
.o對象文件; - 從編譯好的BPF對象文件中生成BPF skeleton 頭文件 (BPF skeleton 頭文件內容來自上一步生成的
.o文件,可以參考libbpf-tools的Makefile文件,可以看到 skeleton 頭文件其實是通過bpftool gen命令生成的以.skel結尾的文件); - 在用戶空間代碼中包含生成的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 應用(例如的runqslower的runqslower_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_btf和fentry/fexit BPF程序類型(這三個都是tracing類型的),使用的也是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 externs 和 struct “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的rodata、bss、或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
...
