Overview Link to heading

关于 BPF 的一些疑惑与解答,第三篇。本篇内容更偏概念。

Q:BPF 是否有稳定的 ABI(Application Binary Interface,应用二进制接口)? Link to heading

A:有。BPF 指令、BPF 程序的参数、一组辅助函数及其参数、以及已识别的返回码都属于 ABI。不过,有一个特定的例外:使用 bpf_probe_read() 等辅助函数来遍历内核内部数据结构,并且与内核内部头文件一起编译的跟踪程序。这两种内核内部实现都可能发生变化,并且在新内核中可能会失效,因此程序需要相应地进行调整。

新 BPF 功能通常是通过使用 kfuncs 而非新增 helper 来实现的。kfuncs 不被视为稳定 API 的一部分,其生命周期预期如 BPF_kfunc_lifecycle_expectations 中所述。

Q:跟踪点(tracepoints)是否属于稳定的 ABI? Link to heading

A:否。跟踪点(Tracepoints)与内部实现细节相关联,因此它们可能会发生变化,并且在新内核中可能会失效。当这种情况发生时,BPF 程序需要相应地进行修改。

Q:kprobes 能否附加到稳定 ABI 的一部分? Link to heading

A:不能,kprobes 可以附加的位置属于内部实现细节,这意味着它们可能会发生变化,并且在新内核中可能会失效。当这种情况发生时,BPF 程序需要相应地进行修改。

Q:一个 BPF 程序使用多少栈空间? Link to heading

A:目前所有程序类型都限制在 512 字节的栈空间内,但验证器会计算实际使用的栈空间量,而解释器和大多数即时编译代码都会消耗所需的空间。

Q:BPF 能否卸载到硬件? Link to heading

A:可以。NFP 驱动程序支持 BPF 硬件卸载。

Q:经典 BPF 解释器是否还存在? Link to heading

A:否。经典 BPF 程序会被转换为扩展 BPF 指令(eBPF)。

Q:BPF 能调用任意的内核函数吗? Link to heading

A:不能。 BPF 程序只能调用以 BPF 辅助函数或 kfuncs 形式暴露的特定函数。可用的函数集为每种程序类型定义。

Q:BPF 能否覆盖任意内核内存? Link to heading

A:不能。跟踪 eBPF 程序可以通过 bpf_probe_read()bpf_probe_read_str() 这两个辅助函数读取任意内存。网络程序无法读取任意内存,因为它们没有访问这些辅助函数的权限。程序本身无法直接读取或写入任意内存。

Q:BPF 能否覆盖任意用户内存? Link to heading

A:某种程度上可以。跟踪BPF程序时,可以使用 bpf_probe_write_user() 函数覆盖当前任务的用户内存。每次加载此类程序时,内核都会打印警告信息,因此这个辅助函数仅适用于实验和原型开发。跟踪 BPF 程序仅限 root 用户操作。

Q:能否在内核模块代码中添加 BPF 功能,例如新的程序或映射类型、新的辅助函数等? Link to heading

A:可以通过 kfuncs 和 kptrs 实现。核心 BPF 功能(如程序类型、映射和辅助函数)无法通过模块进行扩展。不过,模块可以通过导出 kfuncs(这些函数可能返回指向模块内部数据结构的 kptrs )来为 BPF 程序提供功能。

Q:有些内核函数(例如 tcp_slow_start)可以被 BPF 程序调用。这些内核函数会成为 ABI 吗? Link to heading

A:不可以。内核函数原型将发生变化,验证器将拒绝 bpf 程序。此外,例如,一些 bpf 可调用的内核函数已经被其他内核 tcp 拥塞控制(cc)实现使用。如果这些内核函数中的任何一个发生变化,树内和树外的内核 tcp cc 实现都必须进行修改。bpf 程序也是如此,需要相应地进行调整。详情请参阅:BPF_kfunc_lifecycle_expectations

Q:BPF 程序可以附加到许多内核函数上。这些内核函数会成为 ABI 的一部分吗? Link to heading

A:不会。内核函数原型将有所变动,因此依附于它们的 BPF 程序也需要相应调整。为了便于将 BPF 程序适配不同版本的内核,应当采用编译一次、处处运行(CO-RE)的 BPF 技术。

Q:给函数加上 BTF_ID 标记,是将其变成 ABI 吗? Link to heading

A:不是。BTF_ID 宏并不会让一个函数成为 ABI 的一部分,EXPORT_SYMBOL_GPL 宏也不会。

Q:用户在使用 BTF 支持的 BPF 映射时,允许将 bpf_spin_lock 和 bpf_timer 字段嵌入到 BPF 映射值的字段中。这允许在映射值中的这些字段上使用相应的辅助函数。用户还允许嵌入指向某些内核类型的指针(带有 __kptr_untrusted__kptr BTF 标签)。内核会为这些特性保留向后兼容性吗? Link to heading

A:这要看情况。对于 bpf_spin_lock 和 bpf_timer,是的;对于 kptr 以及其他所有情况,不行,但请看下文。

对于已经添加的结构类型,比如 bpf_spin_lock 和 bpf_timer,内核将保持向后兼容性,因为它们是 UAPI 的一部分。

对于 kptr 来说,它们也属于 UAPI 的一部分,但仅限于 kptr 机制。你在结构体中使用带有 __kptr_untrusted__kptr 标签的指针时,所支持的类型并不属于 UAPI 的契约。这些支持的类型会随着内核版本的更新而变化。不过,对于支持的类型,访问 kptr 字段和 bpf_kptr_xchg() 辅助函数等操作,将会在各个内核版本中持续得到支持。

对于任何其他受支持的 struct 类型,除非本文件中明确说明并添加到 bpf.h UAPI 头文件中,否则这些类型在内核版本之间可以任意改变其大小、类型、对齐方式,或任何其他对用户可见的 API 或 ABI 细节。用户必须调整其 BPF 程序以适应这些变化,并更新它们以确保程序继续正常运行。

注意:BPF 子系统特别为类型名称保留 ‘bpf_’ 前缀,以便未来引入更多特殊字段。因此,用户程序必须避免定义带有 ‘bpf_’ 前缀的类型,以免未来版本中程序失效。换句话说,使用带有 ‘bpf_’ 前缀的 BTF 类型将无法保证向后兼容性。

Q:与上述情况相同,但针对已分配的对象(即使用 bpf_obj_new 为用户自定义类型分配的对象)。内核是否会保留这些特性的向后兼容性? Link to heading

A:与映射值类型不同,用于处理已分配对象以及它们内部特殊字段支持的 API 是通过 kfuncs 暴露的,因此其生命周期预期与 kfuncs 本身相同。详情请参阅:BPF_kfunc_lifecycle_expectations