LWN:利用BPF来检查kernel数据结构内容!

关注了就能看到更多这么棒的文章哦~

Dumping kernel data structures with BPF

By Jonathan Corbet
April 27, 2020

原文来自:https://lwn.net/Articles/818714/

主译:DeepL

自从操作系统内核软件出现以来,就一直有需求要从内核中的数据结构中提取信息。多年来,人们采取了各种各样的方法来实现。如今,人们很自然地将BPF作为解决各种问题的首选工具,从内核数据结构中获取信息也不例外。目前有两个正在review的patch set,它们在使用BPF从内核数据结构提取信息到用户空间方面,采取了两种不同的方法。

当编者第一次遇到古老的Unix系统时,当时,类似ps这样的工具会通过打开/dev/kmem直接在内核的内存空间中来获取信息。这种方法的优点是不需要内核提供API来做支持,但也有一些缺点,包括:安全问题、在收集复杂数据时没有原子操作保护、偶尔会返回随机的错误数据。早期,这种行为还算可以接受,但当代用户对这种行为的容忍度却变得出奇的低。在内核内存中到处乱翻数据的行为现在已经非常不受欢迎了。

在现在的Linux系统中,这个问题可以通过系统调用或者/proc、sysfs、debugfs虚拟文件组合在一起来解决。这种方法是可行的,但也有一些问题。每当要输出的信息本身发生变化时,内核必须进行修改;debugfs中的 "调试 "信息最终会变成正常运行的系统操作的依赖项(其实debugfs根本不应该被打开),而且在修改之后很可能会导致现有应用程序无法正常运行。因此,人们自然期待着更灵活、适应性更强的方案。

Structure dumpers

有一种方法是由Yonghong Song发布的,它直接针对虚拟文件方案。简而言之,它允许提供BPF程序来实现类似/proc这样的文件,从而提供各种数据结构的内容给用户空间。

具体地说,它创建了一个新的虚拟文件系统,希望mount到/sys/kernel/bpfdump。这是一个singleton文件系统,也就是说无论mount多少次(或mount到多少个不同的命名空间里去),它提供的内容都是相同的。然后,内核子系统可以在该文件系统中创建子目录来提供特定的数据结构。例如,在这组patch中,创建了task子目录,用来把内核中当前在用的task_struct结构暴露出来,还有bpf_map用来遍历BPF maps列表,netlink则提供了当前活跃的netlink连接的信息。

然后这个补丁系列增加了一个新的BPF program类型,叫做BPF_TRACE_DUMP。这种program被调用时会被提供一个指向结构的指针,主要依赖这个函数来生成后续要提供给user space的数据,最终通过seq_file结构来提供给user space。为此,新增加了两个helper function,即bpf_seq_printf()和bpf_seq_write()。这些program会以常规方式通过bpf()系统调用加载到内核中。

最后,扩展了BPF_OBJ_PIN命令的含义,该命令最初是为了支持BPF program和maps能在指向它们的文件描述符被关闭后仍然可用。利用这个命令,可以把BPF_TRACE_DUMP program给固定(pin)到/sys/kernel/bpfdump目录下创建的一个文件中。因此,例如,如果某人人想创建一个新的进程查看程序,名为 "myps",那他可以加载一个BPF program,从task structure中生成所需的输出信息,然后将其 "pin"到/sys/kernel/bpfdump/task下的一个名为myps的文件中。

这个patch set包含了一些示例程序来作为演示以及自测试。比如,可以在/sys/kernel/bpfdump/netlink下固定(pin)一个与/proc/net/netlink产生的输出完全相同的文件。当然,重复提供现有接口已经提供的功能,并没有多少好处,但这确实展示了如何创建新接口。有了这个功能之后,用户可以相对高效地创建接口来提供他们所需要的信息。如果需要新的信息,也不需要改变内核。

不过,还是需要先进行一些配置的。每一个以这种方式提供的structure类型都需要一些支持代码来遍历这些active structure,并将其传递给相关的BPF program。但这对每种structure来说都是一次性的工作。在这之后,理论上,内核开发者再也不用费心考虑如何从该结构类型导出信息到用户空间了。只要没有人担心这些提供出去的数据需要保密就行。

printk()

另一种方法是由Alan Maguire发布的,它着重满足debug场景的需求。当这种特定场景下,人们很自然地就想用printk()来提供信息到user space。

在调试某个问题时,通常需要查看内核数据结构中的各个字段。在正确的地方加上printk()调用,然后重新编译内核,通常就可以达到目的。可是,很常见的一种情况是打印的字段不够,就需要重新开始。人们就挺想要一个调试功能:能够简单地打印任意一个结构的全部内容。这在Python这样的解释语言中很容易做到,但在C语言中一般没有。

打印某些特定structure类型,在内核中已经支持一段时间了。例如,rtc_time结构就可以使用%ptR格式描述符来直接打印。但是只有很少的结构可以支持,每一个新增结构都会导致在printk()中添加更多的代码,而且每当structure被修改时,printk中的相关打印代码也必须更新。所以这个功能距离能打印任意structure,还差得很远。

Maguire意识到,在内核中加入了BPF type format(BTF)数据之后,就可以有更好的方案了。BTF最初是为了解决BPF program在系统间二进制兼容的问题而加入的。任何特定的数据结构的内部排布都会随着内核配置的改变而变化,这使得人们创建好的BPF program换一个kernel config之后就很可能无法运行了。BTF用来在内核编译之后描述内核中最终使用的数据结构。用户空间的工具可以在将BPF program加载到内核中之前,利用这些structure中的offset信息来把structure的引用能 "重新定位 "到正确位置。

一旦我们在内核中获得了structure layout的描述信息,就可以用它来打印出该structure的数据。所以Maguire添加了一个新的format directive格式描述来实现这个功能。这个格式是"%pT",其中type是传递的结构指针的类型。而"%pTN"则会再加上字段名(field name)。patch set中提供的一个例子打印了sk_buff结构(在网络层中用于保存数据包),只用了下面这样一行语句:

    pr_info("%pTN", skb);

就打印出了这么完整的信息:

    {{{.next=00000000c7916e9c,.prev=00000000c7916e9c,
      {.dev=00000000c7916e9c|.dev_scratch=0}}|
      .rbnode={.__rb_parent_color=0,.rb_right=00000000c7916e9c,.rb_left=00000000c7916e9c}|
      .list={.next=00000000c7916e9c,.prev=00000000c7916e9c}},
      {.sk=00000000c7916e9c|.ip_defrag_offset=0},{.tstamp=0|.skb_mstamp_ns=0},
      .cb=['\0'],{{._skb_refdst=0,.destructor=00000000c7916e9c}|
      .tcp_tsorted_anchor={.next=00000000c7916e9c,.prev=00000000c7916e9c}},
      ._nfct=0,.len=0,.data_len=0,.mac_len=0,.hdr_len=0,.queue_mapping=0,
      .__cloned_offset=[],.cloned=0x0,.nohdr=0x0,.fclone=0x0,.peeked=0x0,
      .head_frag=0x0,.pfmemalloc=0x0,.active_extensions=0,.headers_start=[],
      .__pkt_type_offset=[],.pkt_type=0x0,.ignore_df=0x0,.nf_trace=0x0,
      .ip_summed=0x0,.ooo_okay=0x0,.l4_hash=0x0,.sw_hash=0x0,.wifi_acked_valid=0x0,
      .wifi_acked=0x0,.no_fcs=0x0,.encapsulation=0x0,.encap_hdr_csum=0x0,
      .csum_valid=0x0,.__pkt_vlan_present_offset=[],.vlan_present=0x0,
      .csum_complete_sw=0x0,.csum_level=0x0,.csum_not_inet=0x0,.dst_pending_co

这里出于可读性考虑,原来的 "全在一行"的输出方式被做了一些分割。输出信息被限制为1024个字符,这就解释了上面的结尾为什么看起来比较突兀。如果这个限制给用户造成了麻烦的话,可以省略掉"N",这样就能输出更多的字段,但没有字段名了。Arnaldo Carvalho de Melo 建议增加一个额外的 "z"选项,值为零的字段就不用打印了,这样输出更加紧凑。这个建议可能会在下一个版本的patch set中实现。

虽然printk()是这个功能的主要使用者,但Maguire认为它也可以用在其他地方。例如,ftrace 可以用它来打印出 tracepoints 的结构内容,或者kernel可以用它来增强 oops之后的输出内容。

这些patch set展示了两种不同的方法,利用内核的 BPF 功能来格式化内核数据结构中的信息,以便在内核之外使用。它们解决的是不同的使用场景,所以并不是说这两种方法只能选一种合入mainline。两者都以其各自的方式使内核内部更容易查看,期待它们的合入。

全文完

LWN文章遵循CC BY-SA 4.0许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注LWN深度文章以及开源社区的各种新近言论~

你可能感兴趣的:(LWN:利用BPF来检查kernel数据结构内容!)