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

eBPF - difference between loading, attaching, and linking?

我对 bpf 系统调用和 libbpf 函数中使用的 eBPF 术语感到非常困惑。有人可以打破我对 loading/attaching 使用 bpf 的过程的理解错误吗?


我的理解:

我一直在查看 cgroups 的代码,所以我将使用它作为示例。我知道 cgroups 的 bpf 程序存储在 cgroup_bpf->effective[bpf_attach_type] 中(参见 here and here). I know that that these programs are accessed and run when functions like __cgroup_bpf_run_filter_sysctl() (in cgroup.c),无论处理程序调用它们(例如,在这种情况下,函数 proc_sys_call_handler())都会调用它们。但是这些程序如何loaded/stored/attached/linked,每个术语之间有什么区别?

正在加载 - 显然,这需要发生 before attaching。我猜这是 cgroup 程序存储在通用位置的地方(即 not in cgroup_bpf->effective[bpf_attach_type]),因此尚未 attached/linked 在内核中执行然而。因此,加载与bpf_attach_type完全分开。如果我是对的,这个 cgroup 程序类型的通用加载位置在哪里?

附加 - 也许这是程序与 bpf_attach_type 相关联的地方。就像,现在我们采用通用存储的程序,现在我们可以使用我们想要的任何 bpf_attach_type 将它放在 cgroup_bpf->effective[bpf_attach_type] 数组中。然后,当到达内核中相应的附加点时,程序是运行.

Linking - 这只是一种特殊类型的附件吗?这与正常的附加有何不同?当使用 cgroup 程序类型时,我看到 link_create() and cgroup_bpf_prog_attach() both end up calling cgroup_bpf_attach()

让我们看看...

正在加载

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

在这个过程结束时,程序位于内核内存中。它不依附于特定的对象。它有一个引用计数器,内核会一直保持它直到计数器归零。引用可以由程序的文件描述符保存:例如,一个由 bpf() 系统调用返回给加载应用程序。可以通过附加、linking、固定程序或(如果我没记错的话)在 prog_array 映射中引用它来创建其他引用。如果没有引用保留(例如,加载应用程序在加载程序后立即退出,从而关闭其指向该程序的文件描述符),则将其从内核中删除。

“附加类型”的概念取决于程序类型。有些程序类型没有这个概念:XDP 程序只是附加到接口的 XDP 挂钩上。附加到 cgroups 的程序确实有一个“附加类型”,可以准确地告诉程序附加到哪里。

加载程序主要与这些附加类型分开。然而,一些程序类型——不是全部——do require the user to pass the expected attach type at load time,通过 union bpf_attr 对象的 expected_attach_type 字段传递给 bpf() 系统调用。验证器和系统调用处理程序使用这种预期的附加类型来执行各种验证。

正在附加

您对附件步骤的理解听起来不错。根据其附加 and/or 程序类型,该程序被“附加”到它应该 运行 的挂钩上。相关的内核结构,cgroup_bpf->effective 在你的例子中,将指向程序(不是 store 它 - 程序不会移动,cgroup_bpf->effective 只是指向一个列表stuct bpf_prog *), 在这个钩子上发生的事件将触发程序。

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

正在链接

我们如何在加载应用程序关闭时保持 eBPF 探测 运行ning?这就是 eBPF links 发挥作用的地方。 eBPF 程序可以附加到 links 而不是它们的传统钩子。 links 本身附加到内核挂钩。这为操作程序提供了更好的界面。一个优点是可以 pin 这样的 links,当它们的加载应用程序退出时保持 eBPF 探测 运行ning。另一个优点是更容易跟踪程序上的引用,并确保在加载应用程序意外退出时没有 eBPF 程序保持加载状态。

link是“特殊类型的附件”吗?我不确定。查看代码,似乎跟踪挂钩现在总是在较新的内核上使用 links。对于其他程序类型,后来添加了 eBPF links 提供的接口,并且似乎 传统的钩子旁边。例如,对于 cgroups,您可以以旧方式附加程序(通过 cgroup_bpf_prog_attach() 您可以加载它们,创建一个 eBPF link 并将您的程序附加到link(通过 link_create())-正如您观察到的,在这两种情况下,您最终都会 运行ning cgroup_bpf_attach().

我认为目前没有关于 eBPF links 的好的文档,所以我们拥有的最好的可能是来自补丁集的求职信和提交日志:

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

固定

固定是一种保存对 eBPF 对象(程序、映射或 links)的引用的方法。它是通过 bpf(BPF_OBJ_PIN, ...) 系统调用完成的,它在 the eBPF virtual file system 中创建一个路径,稍后可以通过 open()-ing 该路径检索对象的文件描述符。只要对象被固定,它就会保留在内核中。 运行 不必固定程序或地图。只要存在其他引用(文件描述符,或者程序附加到某些挂钩;或者对于映射,它们被现有程序引用......),程序保持加载在内核内存中,如果附加,它可以 运行.

特别是,固定 eBPF link 可确保附加到该 link 的程序在其加载应用程序终止并关闭其文件描述符后保持加载状态。

总结

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