在前面的文章中,sched_ext实现之struct_ops 介绍了sched_ext如何基于struct_ops特性实现了eBPF代码的灵活替换。另外,在sched_ext实现中提到“提供与核心调度器交互的统一接口,同时避免对核心调度逻辑的直接修改”,“避免核心调度器逻辑的直接修改”指sched_ext只在调度框架sched_class函数方法的调用路径上实现eBPF函数替换埋点,并不能改变调度框架的逻辑。sched_ext的struct_ops实现了上面说的统一接口交互,而真正的功能扩展则需要利用本文提到的kfunc机制。
1.Kfunc机制
提到kfunc熟悉bpftrace的朋友肯定会想到bpftrace中的kfunc trace类型,但实际上bpftrace的kfunc功能借助的是内核bpf fentry的trace类型,而内核中的kfunc功能是提供一个help 函数用于访问内核函数,主要用于替换之前的help func机制。所以如果想新增一个help func api,当前内核社区也只会采纳kfunc的方式,原先help func的方式已经不再增加新的api。
Kfunc的实现逻辑大致如下:
不同于help func那种固定的func id访问形式,所有的kfunc都是通过btf_id作为唯一标识,libbpf中会将找到目标内核函数的btf_id,在校验检查阶段会通过该btf_id找到对应的内核函数,而要真正访问该内核函数则是通过BPF_CALL 内核函数地址 -__bpf_base_call 获得的offset实现跳转。
1.指令修改
在libbpf中会对BPF 程序中调用内核函数的指令修改将如下:
insn->code == (BPF_JMP | BPF_CALL)
insn->src_reg == BPF_PSEUDO_KFUNC_CALL /* 新增的标志 */
insn->imm == func_btf_id /* 内核函数的btf_id */
所有的kfunc都是单独保存在“.BTF_ids” section中,通过resolve_btfids工具在链接脚本中将所有的kfunc增加btf_id作为唯一标识,最终在vmlinx btf中才能找到对应的kfunc。
在验证器的初始阶段,内核函数调用信息会被收集到 struct bpf_kfunc_desc 中,并保存在 prog->aux->kfunc_tab 中,供 JIT 使用。
2.校验检查
新增的 check_kfunc_call() 函数负责验证内核函数调用指令。 确保该内核函数可以被特定的 BPF 程序类型使用。 使用 btf_check_kfunc_args_match() 确保寄存器可以用作内核函数的参数。
3.地址修正
在 do_misc_fixups() 阶段,fixup_kfunc_call() 会将 insn->imm 替换为内核函数的地址,并且 JIT 可以通过新函数 bpf_jit_find_kfunc_model() 来查找函数模型。
2.sched_ext中的kfunc
sched_ext中所有的kfunc都定义在sched/ext.c文件中,声明在BTF_ID_FLAGS宏内的函数就是可调用的kfunc函数,大致看了下目前实现的kfunc有24个:
BTF_KFUNCS_START(scx_kfunc_ids_enqueue_dispatch)
BTF_ID_FLAGS(func, scx_bpf_dispatch, KF_RCU)
BTF_ID_FLAGS(func, scx_bpf_dispatch_vtime, KF_RCU)
下面介绍下sched_ext中几个常用的kfunc函数:
1.scx_bpf_dispatch
负责将task放到到指定的队列上,通常在ops.enqueue的回调函数中调用执行,例如simple_enqueue的实现。
scx_bpf_dispatch_vtime同scx_bpf_dispatch,只不过在dispatch的时候可以设置dsq_vtime,通常用于非fifo的dsq中。例如:
2.scx_bpf_consume
有点类似pick_next_task函数,负责将一个task从队列上取出来放到cpu上执行。每次都是将local dsq上的任务放到cpu上的执行,如果local dsq上没有任务则从gobal dsq上拉取。这里的local dsq类似 cfs的rq,以percpu形式的存在;而global dsq则为了负载均衡,可以将某个local dsq上的任务通过global dsq传到另一个local dsq。
3.scx_bpf_create_dsq
新建一个队列,sched_ext默认会创建global dsq和local dsq,两者都是fifo形式,如果想创建其他形式如根据vtime实现的cfs方式等,则需要调用该接口创建自定义的队列。与之对应的scx_bpf_destroy_dsq通过dsq_id删除指定的dsq。
4.scx_bpf_select_cpu_dfl
采用默认的cpu挑选策略,选择一个cpu供task运行。
5.scx_bpf_kick_cpu
该接口可以向另一个 CPU 发送 IPI(处理器间中断)信号,以唤醒空闲的 CPU(SCX_KICK_IDLE
标志)并要求抢占当前正在运行的任务(SCX_KICK_PREEMPT
标志)。
6.scx_bpf_dsq_nr_queued
用于统计dsq上运行的task数量。
3.小结
综上来看,sched_ext的实现也是站在了struct_ops和kfunc两大基础机制的肩膀上,技术的重大突破向来需要几代人的共同努力,linux社区如是,AI大模型如是。长江后浪推前浪,借助sched_ext的浪潮,又助推出了scx_lavd、scx_rusty、scx_bpfland等高性能调度器,感兴趣的小伙伴欢迎进群交流。
4.参考
https://lore.kernel.org/bpf/20210325015124.1543397-1-kafai@fb.com/
https://lore.kernel.org/bpf/20240501151312.635565-1-tj@kernel.org/
https://blogs.igalia.com/changwoo/sched-ext-scheduler-architecture-and-interfaces-part-2/