天天看点

BPF架构(二)辅助函数

作者:煮酒论架构

辅助函数

eBPF程序不能随意调用内核函数。这样做会将eBPF程序绑定到特定的内核版本,并使程序的兼容性复杂化。但是,eBPF程序可以对helper函数进行函数调用,这是内核提供的一个众所周知的稳定API。
BPF架构(二)辅助函数

辅助函数(Helper functions)使得 BPF 能够通过一组内核定义的函数调用(function call)来从内核中查询数据,或者将数据推送到内核。不同类型的 BPF 程序能够使用的 辅助函数可能是不同的,例如,与 attach 到 tc 层的 BPF 程序相比,attach 到 socket 的 BPF程序只能够调用前者可以调用的辅助函数的一个子集。另外一个例子是, 轻量级隧道(lightweight tunneling )使用的封装和解封装(Encapsulation and decapsulation)辅助函数,只能被更低的 tc 层(lower tc layers)使用;而推送通知到 用户态所使用的事件输出辅助函数,既可以被 tc 程序使用也可以被 XDP 程序使用。

所有的辅助函数都共享同一个通用的、和系统调用类似的函数签名。签名定义如下:

u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)            

前一节介绍的调用约定适用于所有的 BPF 辅助函数。

内核将辅助函数抽象成 BPF_CALL_0() 到 BPF_CALL_5() 几个宏,形式和相应类型的系 统调用类似。下面的例子是从某个辅助函数中抽取出来的,可以看到它通过调用相应 map 的回调函数完成更新 map 元素的操作:

BPF_CALL_4(bpf_map_update_elem, struct bpf_map *, map, void *, key,
           void *, value, u64, flags)
{
    WARN_ON_ONCE(!rcu_read_lock_held());
    return map->ops->map_update_elem(map, key, value, flags);
}

const struct bpf_func_proto bpf_map_update_elem_proto = {
    .func           = bpf_map_update_elem,
    .gpl_only       = false,
    .ret_type       = RET_INTEGER,
    .arg1_type      = ARG_CONST_MAP_PTR,
    .arg2_type      = ARG_PTR_TO_MAP_KEY,
    .arg3_type      = ARG_PTR_TO_MAP_VALUE,
    .arg4_type      = ARG_ANYTHING,
};
           

这种方式有很多优点:虽然 cBPF 允许其加载指令(load instructions)进行 超出范围的访问(overload),以便从一个看似不可能的包偏移量(packet offset,负的)位置 获取数据以唤醒多功能辅助函数,但每个 cBPF JIT 仍然需要为这个 cBPF extension 实现对应的支持。

更多关于 Linux BPF extension 的内容,可参考 (译) Linux Socket Filtering (LSF, aka BPF)(Kernel,2021), 译注中附录了一些相关的内核实现。译注。

而在 eBPF 中,JIT 编译器会以一种透明和高效的方式编译新加入的辅助函数,这意味着 JIT 编 译器只需要发射(emit)一条调用指令(call instruction),因为寄存器映射的方式使得 BPF 排列参数的方式(assignments)已经和底层架构的调用约定相匹配了。这使得基于辅 助函数扩展核心内核(core kernel)非常方便。所有的 BPF 辅助函数都是核心内核的一 部分,无法通过内核模块(kernel module)来扩展或添加。

前面提到的函数签名还允许校验器执行类型检测(type check)。上面的 struct bpf_func_proto 用于存放校验器必需知道的所有关于该辅助函数的信息,这 样校验器可以确保辅助函数期望的类型和 BPF 程序寄存器中的当前内容是匹配的。

参数类型范围很广,从任意类型的值,到限制只能为特定类型,例如 BPF 栈缓冲区(stack buffer)的 pointer/size 参数对,辅助函数可以从这个位置读取数据或向其写入数据。 对于这种情况,校验器还可以执行额外的检查,例如,缓冲区是否已经初始化过了。

当前可用的 BPF 辅助函数已经很多,并且数量还在不断增加,例如,写作本文时,tc BPF 程序可以使用38 种不同的 BPF 辅助函数。对于一个给定的 BPF 程序类型,内核的 struct bpf_verifier_ops 包含了 get_func_proto 回调函数,这个函数提供了从某个 特定的enum bpf_func_id 到一个可用的辅助函数的映射。

继续阅读