【Linux】BCC 工具編寫
本實驗參照該實驗手冊: GIT - BCC
完整代碼: GIT
一、基本結構
示例1: 以
hello_world.py
爲例, 查看一個最基礎的BCC程序結構
int kprobe__sys_clone(void *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
#!/usr/bin/python
from bcc import BPF
BPF(
# 定義了一個BPF程序內聯,使用C語言編寫
text='見上述C代碼'
).trace_print()
參數說明
-
kprobe__sys_clone
: 這是通過kprobes進行內核動態跟蹤的快捷方式. 如果C函數以開頭kprobe__
,則其餘部分被視爲要檢測的內核函數名稱,在這種情況下爲sys_clone()
-
void *ctx
: ctx有參數,但是由於我們不在這裏使用它們,因此我們將其轉換爲void *
-
bpf_trace_printk
: 輸出, 後續將詳細介紹 -
.trace_print()
: BCC事務, 讀取trace_pipe
並且輸出
實驗: 編寫一個跟蹤 sys_sync()
內核函數的程序, 在運行時打印 “sys_sync() called”. (代碼見 LINK )
示例2: 類似於hello_world.py,並通過
sys_clone()
再次跟蹤新進程,但還有一些要學習的內容.
int hello(void *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
from bcc import BPF
# 將BPF程序聲明爲變量
prog = """ 見上述C代碼 """
# 加載 BPF 程序
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")
# header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE"))
# format output
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))
參數說明
hello()
: 我們聲明一個C函數,而不是kprobe__
的快捷方式b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")
: 爲內核調用創建一個kprobe, 它將執行上述定義的hello函數. 您可以多次調用attach_kprobe(),並將C函數附加到多個內核函數b.trace_fields()
: 從trace_pipe返回固定的字段集。與trace_print相似
二、BPF映射對象
// 創建: BPF映射對象, 該對象是一個哈希, 稱爲last. 鍵和值類型默認爲 u64
BPF_HASH(last);
// 創建: BPF映射對象, 並指定其他參數
BPF_HASH(last, u32);
// 查找: 返回一個指向其值的指針, 否則返回NULL。我們將key做爲地址傳遞給指針
last.lookup(&key);
// 刪除: 由於內核中存在bug, 需要在update前執行
last.delete(&key);
// 更新: 將第二個參數中的值與鍵相關聯,覆蓋以前的值。
last.update(&key, &ts)
示例: sync_timing, 該代碼對
do_sync
函數的調用速度進行了計時,如果最近一次調用了do_sync函數,則打印輸出 (場景: 系統管理員執行 reboot 前, 需要執行sync; sync; sync
. )
#include <uapi/linux/ptrace.h>
BPF_HASH(last);
int do_trace(struct pt_regs *ctx) {
// key = 0, 只在此哈希中存儲一個鍵/值對,其中鍵固定爲零
u64 ts, *tsp, delta, key = 0;
// attempt to read stored timestamp
tsp = last.lookup(&key);
if (tsp != 0) {
// 返回時間, 以納秒爲單位
delta = bpf_ktime_get_ns() - *tsp;
if (delta < 1000000000) {
// output if time is less than 1 second
bpf_trace_printk("%d\\n", delta / 1000000);
}
last.delete(&key);
}
// update stored timestamp
ts = bpf_ktime_get_ns();
last.update(&key, &ts);
return 0;
}
...
b.attach_kprobe(event=b.get_syscall_fnname("sync"), fn_name="do_trace")
print("Tracing for quick sync's... Ctrl-C to end")
# format output
start = 0
while 1:
(task, pid, cpu, flags, ts, ms) = b.trace_fields()
if start == 0:
start = ts
ts = ts - start
print("At time %.2f s: multiple syncs detected, last %s ms ago" % (ts, ms))
實驗:編寫 sync_count.py
修改sync_timing程序,以存儲所有內核同步系統調用 (快速和慢速) 的計數,並與輸出一起打印。通過向現有哈希添加新的key索引,可以在BPF程序中記錄此計數, 代碼見LINK
三、輸出結構
上述實驗使用bpf_trace_printk
: 將 printf()
轉換爲通用 trace_pipe(/sys/kernel/debug/tracing/trace_pipe)
的簡單內核工具。對於一些簡單的示例來說,這是可以的,但是有侷限性:
- 3 args max, 1 %s
- trace_pipe是全局共享的, 因此併發程序將產生衝突輸出。更好的接口是通過
BPF_PERF_OUTPUT()
本節介紹BPF_PERF_OUTPUT
的使用方法
示例: hello_perf_output, 我們不再使用
bpf_trace_printk()
, 而是使用BPF_PERF_OUTPUT()
接口. 這意味着無法獲取trace_field()
成員 (PID, timestamp). 而是需要直接獲取它們.
#include <linux/sched.h>
// 這定義了用來將數據從內核傳遞到用戶空間的C結構
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
// 將輸出通道命名爲 events
BPF_PERF_OUTPUT(events);
int hello(struct pt_regs *ctx) {
// 創建一個空的data_t結構
struct data_t data = {};
// 返回低32位的PID(進程ID),以及高32位的TGID(線程組ID)。對於多線程應用程序,TGID將相同,因此,需要使用PID來區分它們
// 通過將其設置爲u32,我們丟棄了高32位
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
// 使用當前進程名稱填充&data.comm
bpf_get_current_comm(&data.comm, sizeof(data.comm));
// 提交event供用戶空間通過perf環形緩衝區讀取
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
...
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")
# header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE"))
# process event
start = 0
# 該函數將處理從events流中讀取事件
def print_event(cpu, data, size):
global start
# 將事件作爲Python對象獲取,並從C聲明自動生成
event = b["events"].event(data)
if start == 0:
start = event.ts
time_s = (float(event.ts - start)) / 1000000000
print("%-18.9f %-16s %-6d %s" % (time_s, event.comm, event.pid,
"Hello, perf_output!"))
# 將print_event函數與events流相關聯
b["events"].open_perf_buffer(print_event)
# 阻止等待事件
while 1:
b.perf_buffer_poll()
實驗:修改 sync_timing
程序, 使用BPF_PERF_OUTPUT 輸出, 代碼見 LINK
四、kprobe
示例: disksnoop: 跟蹤塊設備的I/O, 延遲以及塊大小
#include <uapi/linux/ptrace.h>
#include <linux/blkdev.h>
BPF_HASH(start, struct request *);
void trace_start(struct pt_regs *ctx, struct request *req) {
// stash start timestamp by request ptr
u64 ts = bpf_ktime_get_ns();
start.update(&req, &ts);
}
void trace_completion(struct pt_regs *ctx, struct request *req) {
u64 *tsp, delta;
tsp = start.lookup(&req);
if (tsp != 0) {
delta = bpf_ktime_get_ns() - *tsp;
bpf_trace_printk("%d %x %d\\n", req->__data_len,
req->cmd_flags, delta / 1000);
start.delete(&req);
}
}
[...]
# 內核常量
REQ_WRITE = 1 # from include/linux/blk_types.h
# load BPF program
b = BPF(text=""" 見上述代碼 """)
b.attach_kprobe(event="blk_start_request", fn_name="trace_start")
b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_start")
b.attach_kprobe(event="blk_account_io_completion", fn_name="trace_completion")
[...]
參數說明
struct request *req
: 用於獲取寄存器, BPF上下文, 然後是該函數的實際參數,trace_start()
: 此功能稍後將附加到blk_start_request()
, 其第一個參數是struct request *
start.update(&req, &ts)
: 使用req結構體做爲key值。指向結構的指針非常有用,因爲它們是唯一的:兩個結構不能具有相同的指針地址(需要小心何時釋放和重用它)。因此,使用時間戳標記請求結構,該結構描述磁盤I/O,以便爲它計時。- 用於存儲時間戳的常用鍵:指向結構的指針和線程ID (用於計時函數輸入返回)
req->__data_len
: 獲取req結構的成員 (可以參閱內核源代碼中有關其成員的定義) 。BCC實際上將這些表達式重寫爲一系列bpf_probe_read()
調用。有時BCC無法處理複雜的取消引用,因此需要直接調用bpf_probe_read()
五、直方圖
示例1: bitehist, 該工具記錄磁盤I/O大小的直方圖(從內核傳輸到用戶空間的唯一數據是存儲區計數,從而提高了效率)
// 定義一個BPF映射對象,它是一個直方圖,命名爲 dist
BPF_HISTOGRAM(dist);
// kprobe__: 此前綴意味着內核函數, 將使用kprobe進行插樁
int kprobe__blk_account_io_completion(struct pt_regs *ctx, struct request *req)
{
// dist.increment: 默認情況下,將作爲第一個參數提供的直方圖存儲區索引增加1
// bpf_log2l: 返回提供值的log2, 這成爲直方圖的索引. 因此我們正在構建2的冪的直方圖
dist.increment(bpf_log2l(req->__data_len / 1024));
return 0;
}
...
try:
sleep(99999999)
except KeyboardInterrupt:
print()
# 將dist直方圖以2的冪次打印,列名爲kbytes
b["dist"].print_log2_hist("kbytes")
實驗: disklatency
編寫一個對磁盤I/O計時的程序,並打印其延遲的直方圖。可以在disksnoop.py程序中找到磁盤I/O檢測和時序,在bitehist.py中可以找到直方圖代碼. 代碼見 LINK
示例2: vfsreadlat, 循環打印read的延遲直方圖
BPF_HASH(start, u32);
BPF_HISTOGRAM(dist);
int do_entry(struct pt_regs *ctx){ ... }
int do_return(struct pt_regs *ctx){ ... }
...
# 從源文件中讀取C代碼
b = BPF(src_file = "vfsreadlat.c")
b.attach_kprobe(event="vfs_read", fn_name="do_entry")
# kretprobe: 將 do_return 附加到內核函數vfs_read的返回, 而不是入口
b.attach_kretprobe(event="vfs_read", fn_name="do_return")
# header
print("Tracing... Hit Ctrl-C to end.")
# output
loop = 0
do_exit = 0
while (1):
...
print()
b["dist"].print_log2_hist("usecs")
# 清除直方圖
b["dist"].clear()
if do_exit:
exit()
六、插樁類型
1. tracepoint
示例: urandomread,使用內核跟蹤點,其具有穩定的API,更推薦使用 (相比起kprobes)
// 內核跟蹤點: random:urandom_read
TRACEPOINT_PROBE(random, urandom_read) {
// args 填充爲跟蹤點參數的結構
bpf_trace_printk("%d\\n", args->got_bits);
return 0;
}
可以通過 perf list
查看 跟蹤點列表,Linux >= 4.7時才能將BPF程序附加到跟蹤點
# 查看 urandom_read 結構
$ cat /sys/kernel/debug/tracing/events/random/urandom_read/format
name: urandom_read
ID: 1071
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:int got_bits; offset:8; size:4; signed:1;
field:int pool_left; offset:12; size:4; signed:1;
field:int input_left; offset:16; size:4; signed:1;
print fmt: "got_bits %d nonblocking_pool_entropy_left %d input_entropy_left %d", REC->got_bits, REC->pool_left, REC->input_left
實驗: 將disksnoop.py轉換爲使用block:block_rq_issue
和block:block_rq_complete
跟蹤點. 代碼見: LINK
2. uprobe
示例: 該程序檢測用戶級別的函數,
strlen()
庫函數,並對其字符串參數進行頻率計數
...
int count(struct pt_regs *ctx) {
// 這將獲取的第一個參數strlen(),即字符串
if (!PT_REGS_PARM1(ctx))
return 0;
struct key_t key = {};
u64 zero = 0, *val;
bpf_probe_read(&key.c, sizeof(key.c), (void *)PT_REGS_PARM1(ctx));
// could also use `counts.increment(key)`
val = counts.lookup_or_try_init(&key, &zero);
if (val) {
(*val)++;
}
return 0;
};
...
# uprobe: 附加到庫c (如果這是主程序,請使用其路徑名),檢測用戶級別的函數strlen(),並在執行時調用C函數count()
b.attach_uprobe(name="c", sym="strlen", fn_name="count")
# header
print("Tracing strlen()... Hit Ctrl-C to end.")
# sleep until Ctrl-C
try:
sleep(99999999)
except KeyboardInterrupt:
pass
# print output
print("%10s %s" % ("COUNT", "STRING"))
counts = b.get_table("counts")
for k, v in sorted(counts.items(), key=lambda counts: counts[1].value):
print("%10d \"%s\"" % (v.value, k.c.encode('string-escape')))
3. USDT
示例: nodejs_http_server.py, 該程序對用戶靜態定義的跟蹤(USDT)探針進行檢測,這是內核跟蹤點的用戶級別版本
int do_trace(struct pt_regs *ctx) {
uint64_t addr;
char path[128]={0};
// 從USDT探針讀取參數6的地址addr
bpf_usdt_readarg(6, ctx, &addr);
// 現在字符串addr指向path變量
bpf_probe_read(&path, sizeof(path), (void *)addr);
bpf_trace_printk("path:%s\\n", path);
return 0;
};
...
# 初始化給定PID的USDT跟蹤
u = USDT(pid=int(pid))
# 把上述do_trace()方法附加到http__server__request USDT探針
u.enable_probe(probe="http__server__request", fn_name="do_trace")
if debug:
print(u.get_text())
print(bpf_text)
# 初始化: 需要將USDT對象(u)傳遞給BPF
b = BPF(text=bpf_text, usdt_contexts=[u])