【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 

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 

// 这定义了用来将数据从内核传递到用户空间的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 
#include 

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])

你可能感兴趣的:(BPF,Linux)