sched_ext的初始化过程
了解初始化的必要性
了解初始化流程对sched_ext总体的数据结构会有了解,使得这些分析更方便:
- 哪些全局数据结构是需要变为每一个调度器单独一份
- 分析扩展式调度器的框架代码
前置知识
在看这部分代码前,需要至少了解以下内核中的基本数据结构和机制:
- sysfs相关:
kobject
、kset
、uevent
- cgroup相关:
cgroup
、cgroup_subsys_state
、css_set
- cpu cgroup相关:
task_group
- 同步原语相关:
rw_semaphore
- 初始化相关:
__initcall
/device_initcall
- BPF相关:
btf kfunc
机制、struct_ops
机制
初始化流程
txt
scx_init ->
register_btf_kfunc_id_set
---
注册各类btf kfunc helper函数以及默认行为函数
register_bpf_struct_ops(&bpf_sched_ext_ops, sched_ext_ops)
---
注册sched_ext_ops以及一系列callback
(sched_ext_ops就是用户注册的BPF调度器的各种ops操作)
kset_create_and_add("sched_ext", &scx_uevent_ops, kernel_kobj)
---
定义kset,设置其uevent处理callback,这之后sysfs中就能看到sched_ext及其内文件了
sysfs_create_group(&scx_kset->kobj, &scx_global_attr_group)
---
在sysfs中创建sched_ext目录
并确定sched_ext目录下各个文件的输入输出时会使用的callback
INFO
scx_init
是通过initcall机制被调用的
上述代码中最关键是bpf_sched_ext_ops
的注册,其提供了一系列可以调用的回调函数:
c
static struct bpf_struct_ops bpf_sched_ext_ops = {
.verifier_ops = &bpf_scx_verifier_ops,
.reg = bpf_scx_reg,
.unreg = bpf_scx_unreg,
.check_member = bpf_scx_check_member,
.init_member = bpf_scx_init_member,
.init = bpf_scx_init,
.update = bpf_scx_update,
.validate = bpf_scx_validate,
.name = "sched_ext_ops",
.owner = THIS_MODULE,
.cfi_stubs = &__bpf_ops_sched_ext_ops
};
其中的bpf_scx_reg
是注册初始化的过程:
c
static int bpf_scx_reg(void *kdata, struct bpf_link *link)
{
return scx_ops_enable(kdata, link);
}
scx_ops_enable
的流程:
c
static int scx_ops_enable(struct sched_ext_ops *ops, struct bpf_link *link) {
// ...
mutex_lock(&scx_ops_enable_mutex)
for_each_node_state(node, N_POSSIBLE) {
struct scx_dispatch_q *dsq;
dsq = kzalloc_node(sizeof(*dsq), GFP_KERNEL, node);
init_dsq(dsq, SCX_DSQ_GLOBAL);
dsqs[node] = dsq;
}
// 为每个CPU创建一个dsq
scx_root_kobj = kzalloc(sizeof(*scx_root_kobj), GFP_KERNEL);
scx_root_kobj->kset = scx_kset; // 注:scx_kset在scx_init中已经初始化了
kobject_init_and_add(scx_root_kobj, &scx_ktype, NULL, "root");
// 将kobject插入sysfs中
scx_exit_info = alloc_exit_info(ops->exit_dump_len);
// 退出信息,会在sched_ext退出时返回相关的信息(比如报错行数)
scx_ops = *ops;
// 将传入的ops注册到scx_ops
cpus_read_lock();
ret = SCX_CALL_OP_RET(SCX_KF_UNLOCKED, init);
// 调用用户定义的init
for (i = SCX_OPI_CPU_HOTPLUG_BEGIN; i < SCX_OPI_CPU_HOTPLUG_END; i++)
if (((void (**)(void))ops)[i])
static_branch_enable_cpuslocked(&scx_has_op[i]);
// 确认调度器中有哪些可用操作,将其scx_has_op[i]设为1
cpus_read_unlock();
ret = validate_ops(ops);
if (ret)
goto err_disable;
// 验证传入的ops是否可用
scx_dsp_ctx = __alloc_percpu(struct_size_t(struct scx_dsp_ctx, buf,
scx_dsp_max_batch),
__alignof__(struct scx_dsp_ctx));
if (ops->timeout_ms)
timeout = msecs_to_jiffies(ops->timeout_ms);
else
timeout = SCX_WATCHDOG_MAX_TIMEOUT;
WRITE_ONCE(scx_watchdog_timeout, timeout);
WRITE_ONCE(scx_watchdog_timestamp, jiffies);
queue_delayed_work(system_unbound_wq, &scx_watchdog_work,
scx_watchdog_timeout / 2);
// 一些传入的全局变量初始化
scx_ops_bypass(true);
/*
* 让sched_ext进入bypass模式
* bypass模式是一套默认的FIFO调度逻辑
* bypass模式可用防止某些调度器的ops需要初始化后才能正确调用而设计的
*/
for (i = SCX_OPI_NORMAL_BEGIN; i < SCX_OPI_NORMAL_END; i++)
if (((void (**)(void))ops)[i])
static_branch_enable(&scx_has_op[i]);
// 将scx_has_op[i]设为1(目前不确定static_branch_enable和static_branch_enable_cpuslocked有什么区别)
//...
percpu_down_write(&scx_fork_rwsem);
/*
* 相当于加了个写锁(实际上是percpu_rw_semaphore)
* 这个锁类似用户态的互斥锁可用让调用者无法持有锁被阻塞时睡眠
*/
scx_ops_init_task_enabled = true;
scx_cgroup_lock();
scx_cgroup_init();
// 在cgroup树中先序访问每一个cgroup并触发用户定义的ops.cgroup_init
scx_task_iter_start(&sti);
while ((p = scx_task_iter_next_locked(&sti))) {
if (!tryget_task_struct(p))
continue;
scx_task_iter_unlock(&sti);
ret = scx_ops_init_task(p, task_group(p), false);
if (ret) {
put_task_struct(p);
scx_task_iter_relock(&sti);
scx_task_iter_stop(&sti);
scx_ops_error("ops.init_task() failed (%d) for %s[%d]",
ret, p->comm, p->pid);
goto err_disable_unlock_all;
}
scx_set_task_state(p, SCX_TASK_READY);
put_task_struct(p);
scx_task_iter_relock(&sti);
}
scx_task_iter_stop(&sti);
// 迭代每一个任务并触发用户定义的ops.init_task
scx_cgroup_unlock();
percpu_up_write(&scx_fork_rwsem);
WRITE_ONCE(scx_switching_all, !(ops->flags & SCX_OPS_SWITCH_PARTIAL));
static_branch_enable(&__scx_ops_enabled);
percpu_down_write(&scx_fork_rwsem);
scx_task_iter_start(&sti);
while ((p = scx_task_iter_next_locked(&sti))) {
const struct sched_class *old_class = p->sched_class;
struct sched_enq_and_set_ctx ctx;
sched_deq_and_put_task(p, DEQUEUE_SAVE | DEQUEUE_MOVE, &ctx);
p->scx.slice = SCX_SLICE_DFL;
p->sched_class = __setscheduler_class(p, p->prio);
check_class_changing(task_rq(p), p, old_class);
sched_enq_and_set_task(&ctx);
check_class_changed(task_rq(p), p, old_class, p->prio);
}
scx_task_iter_stop(&sti);
percpu_up_write(&scx_fork_rwsem);
/*
* 迭代每一个任务,将其调度器类设置为sched_ext
* 且对任务在sched_ext中需要用的基础时间片长度初始化
*/
scx_ops_bypass(false);
//...
kobject_uevent(scx_root_kobj, KOBJ_ADD);
// 对用户态发送了一个kobject uevent:KOBJ_ADD
mutex_unlock(&scx_ops_enable_mutex);
//...
}
接下来就需要对这个初始化过程中用到的数据结构变为多份,目前考虑先变成2份
然后再将多个调度器之间设置层级关系,任务通过cpumask匹配情况确定调度器(也可能直接根据cgroup判断属于哪个调度器)