eBPF - 加载、附加和链接之间的区别是什么?

5

我对在系统调用和libbpf函数中使用的eBPF术语感到非常困惑。有人能够解释一下我的bpf加载/附加进程的理解有什么错误吗?


我的理解:

我一直在研究cgroups的代码,所以我将以此为例。我知道cgroups的bpf程序存储在cgroup_bpf->effective[bpf_attach_type]中(参见这里这里)。我知道当调用处理程序(例如,在这种情况下是函数proc_sys_call_handler())时,像__cgroup_bpf_run_filter_sysctl()(在cgroup.c中)这样的函数会访问并运行这些程序。但是这些程序如何加载/存储/附加/链接?每个术语之间有什么区别?

Loading - 显然,这需要在before attaching之前发生。我猜这是一个cgroup程序存储在通用位置(即不是cgroup_bpf->effective[bpf_attach_type]中),因此尚未附加/链接以在内核中执行。因此,加载完全与bpf_attach_type分开。如果我正确的话,那么cgroup程序类型的通用加载位置在哪里?

Attaching - 也许这是将程序与bpf_attach_type相关联的地方。例如,现在我们取出通用存储的程序,然后可以使用任何想要的bpf_attach_type将其放入cgroup_bpf->effective[bpf_attach_type]数组中。然后,在到达内核中相应的附加点时,运行该程序。

链接 - 这只是一种特殊类型的附件吗?与普通附加有何不同?我发现当使用cgroup程序类型时,link_create()cgroup_bpf_prog_attach()同时最终都会调用cgroup_bpf_attach()
1个回答

13

让我们看一下...

加载

加载程序的过程是通过bpf(BPF_PROG_LOAD, ...)系统调用(对于大多数程序类型)将其指令注入到内核中。程序经过验证器,该验证器运行一系列检查,并可能重写一些指令(特别是对于映射访问)。然后,如果启用JIT编译,则可以对程序进行JIT编译。在内核内存中定义的程序是包含(或指向)有关程序的信息,包括其eBPF字节码和(如果相关)JIT编译指令的struct bpf_prog对象

这个过程结束后,程序位于内核内存中。它没有附加到特定的对象上。它有一个引用计数器,并且内核保留它直到计数器达到零。文件描述符可以持有对程序的引用:例如,bpf()系统调用返回一个文件描述符给加载应用程序。其他引用可以通过附加、链接、固定程序或(如果我记得正确的话)在prog_array映射中引用它来创建。如果没有引用剩余(例如,加载应用程序在加载程序后退出,因此关闭指向程序的文件描述符),则它将从内核中删除。

“附加类型”的概念取决于程序类型。一些程序类型没有这种概念:XDP程序只附加到接口的XDP挂钩上。附加到cgroup的程序确实有一个“附加类型”,该类型告诉程序确切地附加在哪里。

加载程序大多与这些附加类型分开。但是,某些程序类型 - 并非所有程序类型 - 确实需要用户在加载时传递预期的附加类型,通过传递给bpf()系统调用的union bpf_attr对象的expected_attach_type字段。验证器和系统调用处理程序使用此预期的附加类型执行各种验证。

附加

您对附加步骤的理解听起来不错。根据其附加和/或程序类型,程序被“附加”到应运行的挂钩上。相关的内核结构,在您的情况下是cgroup_bpf->effective,将指向程序(不会存储它 - 程序不会移动,cgroup_bpf->effective只是指向struct bpf_prog *列表),并在此挂钩上发生的事件将触发程序。

需要翻译的内容:

请注意,对于某些程序类型,例如网络或附加到cgroup的程序,附加程序会增加其引用计数,以便加载应用程序可以退出而不从内核中删除程序。对于其他一些程序类型,例如kprobes,这不足以保持程序处于打开状态,因为附加是基于由perf_event_open()返回的文件描述符来保持程序附加,并且需要一个进程保持运行以保持此文件描述符打开。

链接

当加载应用程序关闭时,我们如何使eBPF探针继续运行?这就是eBPF链接发挥作用的地方。eBPF程序可以附加到链接而不是它们的传统钩子上。链接本身附加到内核钩子上。这提供了更好的操作程序的接口。其中一个优点是可以将此类链接“固定”,以在其加载的应用程序退出时继续运行eBPF探针。另一个优点是更容易跟踪程序所持有的引用,并确保如果加载应用程序意外退出,则不会仍然加载任何eBPF程序。

链接是“特殊类型的附件”吗?我不确定。查看代码后,似乎新版本内核中的跟踪钩子现在总是使用链接。对于其他程序类型,eBPF链接提供的接口是稍后添加的,并似乎与传统钩子并存。例如,对于cgroups,您可以通过旧方式(通过cgroup_bpf_prog_attach())附加程序,或者将它们加载,创建eBPF链接并将程序附加到该链接(通过link_create())- 如您所观察到的,在这两种情况下,都将运行cgroup_bpf_attach()

我不认为目前有关于eBPF链接的良好文档,因此我们最好的资料可能是补丁集的封面信和提交日志:

eBPF链接不要与用于存储字节码并在加载到内核之前的ELF对象文件的链接混淆。例如,libbpf能够链接多个包含各种eBPF函数,子程序或其他对象的目标文件,并生成一个包含所有这些对象的单个输出ELF文件。这与“bpf_link”接口无关。

固定

固定是一种持有对eBPF对象(程序、映射或链接)的引用的方法。可以使用bpf(BPF_OBJ_PIN,...)系统调用来执行此操作,它会在eBPF虚拟文件系统中创建一个路径,并且稍后可以通过open()打开该路径以获取对象的文件描述符。只要对象被固定,它就会留在内核中。固定程序或映射不是运行它们所必需的。只要存在其他引用(文件描述符或程序已连接到某些钩子; 或对于映射,它们被现有程序引用...),程序就会保留在内核内存中并且如果连接,则可以运行。

特别是,固定eBPF链接确保连接到该链接的程序在其加载应用程序终止并关闭其文件描述符后仍保留在内存中。

总结

  • 加载:将程序注入内核,验证器启动,它可能会重写一些指令并链接到相关的内部eBPF对象(BTF、映射等),并且可能进行JIT编译。字段expected_attach_type 可能是必需的。
  • 连接:根据程序类型或需要,将程序连接到eBPF链接而不是直接连接到其常规附加点。链接连接到常规钩子,并提供了一个更灵活的界面来管理程序。
  • 附加:使用提供的附加类型将程序附加到与其程序类型相关的钩子。
  • 固定:可以将程序或链接(或映射)固定到bpffs中以使它们持久化(但不跨重启)。

在这个过程的末尾,程序位于内核内存中。它不附加到特定的对象上。这两个句子解决了我很多困惑。非常感谢! - wxz
2
不客气!我认为那是一个非常好的问题,你绝不是唯一一个对此感到困惑的人,“加载”/“附加”这两个步骤通常在演示中很快地提到,但我认为没有太多的文档详细介绍每个阶段 - 更不用说eBPF链接了。所以这是一个很好的机会,希望能为将来的其他人带来一些澄清和帮助 :)。 - Qeole

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接