【Linux】BCC 工具編寫

【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_issueblock: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])
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章