6

Some Advanced BCC topics

 3 years ago
source link: https://kernel.taobao.org/2018/03/some-advanced-bcc-topics/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Mar 22, 2018

Some Advanced BCC topics

LWN原文链接:https://lwn.net/Articles/747640/

BPF虚拟机正在不断融入各个内核模块子系统。此前的文章中介绍了BPF编译套件(BCC)及相关工具。但BCC不只是一系列管理工具集,它还为那些希望创建基于BPF工具的开发者提供了一整套开发环境。阅读本文了解如何使用上述开发环境来创建BPF程序并将其绑定到对应的tracepoint上。

BCC运行时环境提供了一个宏定义“TRACEPOINT_PROBE”,通过该宏定义可以将一个回调函数附加到指定的tracepoint上并在该tracepoint被触发时调用上述函数。下面的代码片段展示了一个没有任何功能的BPF程序,当kmalloc()函数被调用时该函数就会被自动执行。

TRACEPOINT_PROBE(kmem, kmalloc) {
	return 0;
}

上述宏定义的参数是一个tracepoint的分类项以及希望绑定的事件。上述参数会直接映射到debugfs文件系统的对应层次结构中(比如:/sys/kernel/debug/tracing/category/event/)。为了简化BPF的使用,对应tracepoint在BPF程序加载后会被自动打开。

kmalloc()对应的tracepoint会传递一系列参数,这些参数可以通过与之相关联的format文件看到。在BPF程序中可以通过args变量来访问tracepoint参数。在我们上面的例子中就可以通过args->call_site来获得kmalloc()函数被调用时对应的指令地址。我们可以藉此统计不同函数对kmalloc()函数的调用情况。

BCC可以借助BPF_HASH和BPF_TABLE来完整访问内核导出的所有数据结构(这一内容在上一期文章中已经介绍过)。BCC中最基本的数据结构是映射(map),其他高级数据结构都可以构建在该数据结构之上。这其中最基础的数据结构是BPF_TABLE。通过对BPF_TABLE的封装BCC提供了BPF_HASH和BPF_ARRAY数据结构。因此他们也有一些共通的操作函数,比如map.lookup()、map.update()和map.delete()。

现在回到我们最开始的那个程序,我们可以使用BPF_HASH来保存kmalloc()函数的调用地址信息(以及调用次数),并在这之后使用Python对统计结果进行处理。

#!/usr/bin/env python

from bcc import BPF
from time import sleep

program = """"
	BPF_HASH(callers, u64, unsigned long);

	TRACEPOINT_PROBE(kmem, kmalloc) {
		u64 ip = args->call_site;
		unsigned long *count;
		unsigned long c = 1;

		count = callers.lookup((u64 *)&ip);
		if (count != 0)
			c += *count

		callers.update(&ip, &c);

		return 0;
	}
""""
b = BPF(text=program)

while True:
	try:
		sleep(1)
		for k,v in sorted(b["callers"].items()):
			print ("%s %u" % (b.ksym(k.value), v.value))

		print
	except KeyboardInterrupt:
		exit()

BPF_HASH宏定义的详细介绍可以参考BCC参考手册。这个宏定义有很多可选参数,但是对一般用户而言最基本的是指定哈希表的名字(比如上面例子中的callers)、关键字key的数据类型(u64)以及数值value的数据类型(unsigned long)。BPF哈希表项可以通过lookup()函数来访问,如果查找的key没有对应项则函数返回NULL。update()函数可以插入一个新的key-value键值对或者更新一个已有的key-value键值对。从上面的代码可以看到,BPF代码中使用哈希表是一件非常容易的事情,无论是插入还是更新一个键值对。

在上面的例子中,一旦count被统计到哈希表中,我们就可以通过Python来处理这些数据。这可以通过BPF对象(例子中的b)来索引。由此会生成一个Python哈希表对象并通过items()函数来进行访问。这里需要注意的是Python BCC maps提供的函数集合与BPF maps是有区别的。

items()函数返回一个Python c_long类型的键值对并可以通过value成员来获取数值。下面的例子展示了如何迭代callers哈希表中所有的统计结果并打印调用了kmalloc()函数的相关内核函数(使用BCCBPF.ksym()将内核地址转换成对应的符号):

for k,v in sorted(b["callers"].items()):
	print ("%s %u" % (b.ksym(k.value), v.value))

上面程序的输出结果是:

# ./example.py
i915_sw_fence_await_dma_fence 4
intel_crtc_duplicate_state 4
SyS_memfd_create 1
drm_atomic_state_init 4
sg_kmalloc 7
intel_atomic_state_alloc 4
seq_open 504
SyS_bpf 22

上面的程序非常浅显,但真正的大型工具就不太容易理解了。开发者需要更多更复杂的调试工具。幸好BCC提供了很多简便的方法来进行调试。

Controlling BPF program compliation and loading

当Python BPF对象实例化的时候,对应的BPF程序代码会自动编译并加载到内核中。编译过程可以通过向BPF类的构造函数传递编译器参数cflags来进行控制。这些参数会被直接传递给Clang编译器,因此通常的编译选项都可以使用。比如使用“cflags=[‘-Wall’]”将所有编译器报警打开。

一个常见的cflags用法是用来传递宏定义。比如在xdp_drop_count.py脚本中静态分配了一个足够大的数组以便使用Python的多进程库:

clfags=["-DNUM_CPUS=%d" % multiprocessing.cpu_count()]

BPF类的构造函数还可以接受一系列调试参数。这些参数均可以独立打开并提供额外的编译或加载信息。比如DEBUG_BPF参数可以输出BPF字节码以便在“绝望”的情况下做最后的“挣扎”:

0: (79) r1 = *(u64 *)(r1 +8)
1: (7b) *(u64 *)(r10 -8) = r1
2: (b7) r1 = 1
3: (7b) *(u64 *)(r10 -16) = r1
4: (18) r1 = 0xffff8801a6098a00
6: (bf) r2 = r10
7: (07) r2 += -8
8: (85) call bpf_map_lookup_elem#1
9: (15) if r0 == 0x0 goto pc+3
 R0=map_value(id=0,off=0,ks=8,vs=8,imm=0) R10=fp0
10: (79) r1 = *(u64 *)(r0 +0)
 R0=map_value(id=0,off=0,ks=8,vs=8,imm=0) R10=fp0
11: (07) r1 += 1
12: (7b) *(u64 *)(r10 -16) = r1
13: (18) r1 = 0xffff8801a6098a00
15: (bf) r2 = r10
16: (07) r2 += -8
17: (bf) r3 = r10
18: (07) r3 += -16
19: (b7) r4 = 0
20: (85) call bpf_map_update_elem#2
21: (b7) r0 = 0
22: (95) exit

from 9 to 13: safe
processed 22 insns, stack depth 16

上面的输出直接来自于内核中Clang/LLVM执行的每条字节码指令和寄存器状态。如果上述信息还不足以排查问题还可以使用DEBUG_BPF_REGISTER_STATE参数来输出更详细的信息。

对于运行时调试最简便的方式是使用bpf_trace_printk()函数,一个类似printk()的函数,向/sys/kernel/debug/tracing/trace_pipe文件中写入信息。这些信息可以使用BPF.trace_print()处理。

上述方式的问题是所有信息都会汇总到一起,不方便信息的过滤。更好的方法是使用BPF_PERF_OUTPUT并使用open_perf_buffer()kprobe_poll()处理。详细的例子可以参考open_perf_buffer()函数。

Using BPF with applications

本期文章主要聚焦在通过BCC来调试内核tracepoint上,但实际上BCC也可以调试用户态tracepoint。下一期的文章将会详细介绍如何使用用户静态定义跟踪探针(User Statically-Defined Tracing, USDT)来调试程序。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK