关注了就能看到更多这么棒的文章哦~
Kernel operations structures in BPF
By Jonathan Corbet
February 7, 2020
原文来自:https://lwn.net/Articles/811631/
在5.6 kernel将要包含的功能中,有一个会引起许多人的兴趣:支持加载BPF program来配置TCP拥塞控制算法(TCP congestion-control algorithms)。用网络部分的开发者Toke Høiland-Jørgensen的说法来说:“这是把kernel变成一个基于BPF runtime的微内核的又一进展”。拥塞控制是最新交给BPF的一个新任务,这已经远远超出了当初设计BPF的时候的目标。不过,如果细细看进去的话,肯定会感到更加惊讶,因为这个功能的实现方式打破了许多领域的惯例!
这个功能的使用场景再明显不过了。目前已经有许多拥塞控制算法在用了,每种都有各自特别适用的特定网络条件。因此产生了动态选择拥塞控制算法而不用重新编译kernel以及重启系统。网络开发者也可以更加轻松地来实验拥塞控制算法。有人可能会提出异议,认为拥塞控制本质上与其他那些flow disp或者IR protocol decoding这些任务没有什么区别,它们都可以用BPF来实现了,拥塞控制应该也可以。不过,事实上,拥塞控制的调整要复杂的多。
仔细看看Martin KaFai Lau的patch set就知道,合入5.6的这个功能不仅仅是在TCP拥塞控制算法这里加了个BPF hook而已,而是一个更加通用的方案。具体来说,新加的这个功能可以用来在kernel里让BPF program替换任意的“operation structure”(就是一个全是各种函数指针的结构体)。目前来说,还只能替换tcp_congestion_ops这个结构体里的内容,从而改变拥塞控制算法。不过按我们的经验,不就之后就会有许多其他用法被人们创造出来。
The user-space API
从user-space角度来说,如果要用这个功能新加载一个operation structure需要若干步骤。首先就是利用bpf()系统调用,把每个函数都加载为一个独立的BPF program。新增的BPF_PROG_TYPE_STRUCT_OPS类型就是为了这一步定义的。给每个BPF program传入的参数中,user space都需要提供BPF type format (BTF) ID,对应着要替换的structure。BTF也是最近加入kernel的,用来描述和区分当前运行的kernel中的具体的函数与数据结构,目前主要用在对tracing function的类型检查上。
user space还需要提供一个偏移量来指明此BPF program是用来替换哪个函数的。举例来说,在struct tcp_congestion_ops里面,ssthresh()函数指针是第六个成员,因此传入的偏移量参数是5(因为从0开始计数)。不过这个API如何确保不受structure layout randomization功能的影响,目前还不是很清楚。
等到structure中每个成员对应的program加载好了,kernel就会针对每个program都返回一个对应的文件描述符。接下来,user space就可以生成类似这样的一个结构:
struct bpf_tcp_congestion_ops {
refcount_t refcnt;
enum bpf_struct_ops_state state;
struct tcp_congestion_ops data;
};
这里的data就是要替换的结构的相同类型,这里就是struct tcp_congestion_ops。其中并不是放置了许多函数指针,而是放置那些program加载进去之后取得的相应文件描述符。结构中那些不是函数指针的成员就不用设置了,不过kernel还是会用下面的方式来替换一下。
最后一步,把这个structure加载到kernel里。大家可以想到许多方式来完成这个任务,实际上的实现方式,大多数人可能想不到。user space需要创建一个特殊的基于BPF_MAP_TYPE_STRUCT_OPS类型的BPF map。跟这个map关联在一起的是kernel中一个特殊结构对应的BTF type ID。真正替换这个结构的动作是通过把上面填好的bpf_tcp_congestion_ops放到BPF map里的第一个元素(元素0)来实现的。也可以对这个map进行query操作(查看rfcnt和state等信息)或者干脆把第一个元素删掉从而移除这个structure。
BPF map近一两年拥有了越来越多的功能。尽管如此,这次似乎也是第一次看到BPF map被这样用起来。这样的接口是否是最合适优雅的实现方式,是有争议的。不过大多数user-space开发者看不到这个接口,因为同其他BPF API一样,都会被libbpf库来封装起来。
The kernel side
要替换一个operation structure肯定需要kernel里面的一些代码来支持。user space是不能任意地替换structure内容的。为了能替换一个特定类型的structure,kernel代码需要创建下面这样的一个结构:
#define BPF_STRUCT_OPS_MAX_NR_MEMBERS 64
struct bpf_struct_ops {
const struct bpf_verifier_ops *verifier_ops;
int (*init)(struct btf *btf);
int (*check_member)(const struct btf_type *t,
const struct btf_member *member);
int (*init_member)(const struct btf_type *t,
const struct btf_member *member,
void *kdata, const void *udata);
int (*reg)(void *kdata);
void (*unreg)(void *kdata);
const struct btf_type *type;
const struct btf_type *value_type;
const char *name;
struct btf_func_model func_models[BPF_STRUCT_OPS_MAX_NR_MEMBERS];
u32 type_id;
u32 value_id;
};
这里还有许多细节,无法在本文中介绍清楚,此结构中的一些成员是自动由宏定义来填充的。verifier_ops结构则包含了一些函数,用来验证每个替换进来的函数都是安全可靠的。patch set还对这个structure新增了一个成员:struct_access(),这是用来管控指定此结构中哪些操作可以用BPF函数来替换。
init()函数会在最开始先调用一次,来完成一些全局性的配置工作。check_member()则用来判断目标结构中的某个特定成员是否被允许用BPF来实现,而init_member()则会在这个结构中检查任何一个成员的具体值。特别指出,init_member()可以验证那些不是函数指针的成员(比如flag成员等)。reg()函数会在完成那些检查工作之后把这个替换structure注册进来,具体在拥塞控制这个场景,它会把tcp_contestion_ops结构(内容已经被换成了那些BPF program)安装到网络协议栈里面会调用到它们的地方。unreg()则反之。
这种类型的结构在创建时都会有一类的名字:structure的类型会被替换为bpf_开头。所以用来替换tcp_congestion_ops结构的就是bpf_tcp_congestion_ops。这个结构就是在user space在加载新的operation structure时要利用BTF ID来引用的structure了。最后,会在kernel/bpf/bpf_struct_ops_types.h里面增加如下一行:
BPF_STRUCT_OPS_TYPE(tcp_congestion_ops)
最后是故意没有分号的。利用这些魔术一般的宏定义功能,在bpf_struct_ops.c里面include这个文件4次之后,一切就都准备好了,不需要创建一个注册这个structure type的特殊函数。
In closing
大家如果对tcp_congestion_ops替换功能在kernel里的实现细节感到好奇,可以参看net/ipv4/bpf_tcp_ca.c。代码库里也已经实际实现了两种算法(TCTCP和CUBIC)。
在kernel里面能任意替换operation structure的话,会有许多潜在的用法。kernel代码里面有非常多的地方都是通过这些operation structure来调用到的。举例来说,如果我们能替换security_hook_heads structure的一部分,就可以任意修改安全策略了,这就类似KRSI这个功能。如果能替换file_operations structure,就可以重新实现kernel的I/O subsystem。还有许许多多的例子可说。
目前还没有人提出来要做这些改动,不过肯定有人会对此感兴趣的。有一天,也许任何的kernel功能都可以被user space提供的BPF program来替代。这样一来,用户对自己在运行的系统就会拥有更大的自由度,不过我们一直所说的"Linux kernel"则会变得多样化,user space加载的不同代码会让它大变样。这个结果应该会非常有趣啊!
全文完
LWN文章遵循CC BY-SA 4.0许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注LWN深度文章以及开源社区的各种新近言论~