LinuxBCC 工具编写
Posted 宣之于口
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LinuxBCC 工具编写相关的知识,希望对你有一定的参考价值。
【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])
以上是关于LinuxBCC 工具编写的主要内容,如果未能解决你的问题,请参考以下文章