BPF 迭代器:以灵活和高效的方式检索内核数据结构
本文地址:https://www.ebpf.top/post/bpf-iterator-retrieving-kernel-data-with-flexibility-and-efficiency
BPF 迭代器实现了高性能的内核内数据检索和聚合。在本篇博文中,我们谈论了开发 BPF 迭代器工具背后的动机,并展示如何使用其从用户空间来灵活有效地遍历内核数据。
1. 为什么需要 BPF 迭代器
现有少数方法可以将内核数据复制到到用户空间。最流行是通过 /proc 系统,例如可通过 “cat /proc/net/tcp6"或者 “cat /proc/net/netlink” 命令打印系统中所有的 tcp6 或 netlink 套接字信息。然而,这种方式输出格式往往是固定的,如果用户想获得关于这些套接字的更多信息,就必须通过给内核打补丁的方式实现,这将涉及到上游和发布,往往需要很长的时间。对于像 ss 这样的流行工具也是如此,任何额外的信息都需要修改内核提交补丁。
drgn 工具可在不修改内核的情况下打印出内核数据,一定程度解决了该问题。但是,drgn 的主要缺点是性能,而且也不能在内核内进行指针追踪。此外,如果指针在内核内变得无效,drgn 可能会产生错误的结果。
BPF 迭代器可用来解决上述相关问题,其提供了对内核中特定数据结构进行一次性修改的灵活性,并且可在内核内进行指针追踪。灵活性是通过使用 BPF 程序实现的,正确性则是通过在内核内实现指针追踪,并通过适当的引用计数或锁定保护来确保指针跟踪有效。在目前的状态下,迭代器只改变了内核中一小部分的数据结构。
2. 如何使用 BPF 迭代器
内核代码中的 BPF selftests 目录提供了很好的用户空间使用 BPF 迭代器的样例。通常,你需要先实现一个 BPF 程序。
以下是几个 selftest 中 BPF 程序的样例:
这里,我们以 bpf_iter_task_file.c 文件为例,用于遍历系统任务中打开的文件相关信息:
|
|
在上面的例子中,SEC("iter/task_file")
字段表示该程序是一个 BPF 迭代器程序,可用来迭代所有任务的所有文件。该程序的上下文是 bpf_iter__task_file
。你可以在 vmlinux.h
中找到 bpf_iter__task_file
结构体的定义:
|
|
在上面的代码中,字段变量名 meta
代表元数据,对所有 BPF 迭代器程序都是一样的。其余的字段则取决于不同的迭代器。例如,对于task_file
迭代器,内核层提供 task
、fd
和 file
相关字段。task
和 file
是基于应用计数的,所以它们在 BPF 程序运行时不会消失。
编写 BPF 迭代器程序后,我们还需要编写用户空间部分代码,用来触发 BPF 程序运行并收集数据。selftest 目录中的 bpf_iter.c 提供了一个编写对应用户空间部分的例子。以下说明了一个典型的顺序:
- 将 BPF 程序加载到内核
- 用 BPF 程序创建一个 bpf_link
- 基于 bpf_link 创建一个 bpf_iter_fd
- 然后使用 read(bpf_iter_fd) 读取数据,直到没有数据为止
- 然后关闭该 fd close(bpf_iter_fd)
- 如果需要重新读取数据,得到一个新的 bpf_iter_fd 并再次进行读取。
BPF 迭代器使用内核的 seq_file
来传递数据至用户空间。该数据可以是一个格式化的字符串或原始数据。在格式化字符串的情况下,你可以使用 bpftool iter 子命令来创建并通过 bpf_link
将一个 BPF 迭代器固定在 BPF 文件系统(bpffs)的路径上。然后你可以使用 cat <path>
来打印结果,例如 cat /proc/net/netlink
这种方式。
例如,你可以使用下面的命令将 bpf_iter_ipv6_route.o
对象文件中的 BPF 程序输出到文件 /sys/fs/bpf/my_route
。
|
|
然后用以下命令打印出结果:
|
|
3. BPF 迭代器在内核中的实现
为了在内核中实现一个 BPF 迭代器,开发者必须填写以下定义在 bpf.h 文件中的关键数据结构。
|
|
在数据结构字段设置后,然后调用 bpf_iter_reg_target()
将迭代器注册到主 BPF 迭代器子系统。
下面是结构 bpf_iter_reg
中每个字段的解释:
字段 | 描述 |
---|---|
target | 指定 BPF 迭代器的名称。例如:bpf_map ,bpf_map_elem 该名字应该与内核中其他 bpf_iter 目标名称不能相同。 |
attach_target 和 detach_target | 允许特定目标的 link_create 动作,因为有些目标可能需要特殊处理。在用户空间 link_create 阶段调用。 |
show_fdinfo 和 fill_link_info | 当用户试图获得与迭代器相关的链接信息时,会被调用以填充目标的具体信息。 |
get_func_proto | 允许 BPF 迭代器访问特定于该迭代器的 BPF 辅助函数。 |
ctx_arg_info_size 和 ctx_arg_info | 指定与 BPF 迭代器相关的 BPF 程序参数的验证器状态。 |
feature | 指定内核 BPF 迭代器基础设施中的某些动作请求。目前,只有 BPF_ITER_RESCHED 被支持。这意味着内核函数 cond_resched() 被调用,以避免其他内核子系统(如 rcu)的错误行为。 |
seq_info | 指定内核 BPF 迭代器基础设施中的某些动作请求。目前,只有 BPF_ITER_RESCHED 被支持。这意味着内核函数 cond_resched() 被调用,以避免其他内核子系统(如 rcu)的错误行为。 |
点击这里可查看内核中 task_vma
BPF 迭代器的实现。
4. 已使用的 BPF 迭代器用户场景
下面列出了最新的上游内核中可用的 BPF 迭代器,按 BPF 程序部分名称分组:
迭代器的测试程序参见 bpf_iter.c 文件。
表格有调整,增加了说明和代码实现,添加了 bpf_link 和 ksym 迭代器。
所有实现的迭代都会调用 bpf_iter_reg_target 函数注册,可以从源码搜索该函数快速找到实现文件。
截止到内核 5.15 版本实现了 13 种迭代器,其中 bpf_prog/bpf_map 为 BPF 预实现和加载的迭代器(参见文件kernel/bpf/preload/iterators/iterators.bpf.c),后续添加的还有 iter/bpf_link(Linux 5.19), iter/ksym(Linux 6.0)等等。
在 Meta ,我们基于 bpftool 工具使用 BPF task_file
迭代器来显示引用特定 BPF program/map/link 的进程号。
sudo bpftool prog
显示的输出如下所示:
|
|
我们还开发了基于 bpf_sk_storage
和 task_iter
迭代器分别开发了工具 fbflow 和 dyno 。其中基于 task_iter 迭代器实现的 dyno 中的 task_iter,与所有任务的基于 netlink 的 taskstats 的旧方式相比,性能有了明显的改善。
5. 后续进展
有上游讨论为 bpf_links
实现一个 BPF 迭代器【提交已经实现:bpf: Add bpf_link iterator】。我们也看到有人为 mounts 实现了一个BPF 迭代器(还没有上游化)。随着人们发现更多的用例,我们期待更多的用户在内核中实现 BPF 迭代器。
原文地址:https://developers.secure.facebook.com/blog/post/2022/03/31/bpf-iterator-retrieving-kernel-data-with-flexibility-and-efficiency/
作者: Yonghong Song
时间: 2022.3.31
- 原文作者:DavidDi
- 原文链接:https://www.ebpf.top/post/bpf-iterator-retrieving-kernel-data-with-flexibility-and-efficiency/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。