Skip to content

sched_ext的初始化过程

了解初始化的必要性

了解初始化流程对sched_ext总体的数据结构会有了解,使得这些分析更方便:

  1. 哪些全局数据结构是需要变为每一个调度器单独一份
  2. 分析扩展式调度器的框架代码

前置知识

在看这部分代码前,需要至少了解以下内核中的基本数据结构和机制:

  • sysfs相关:kobjectksetuevent
  • cgroup相关:cgroupcgroup_subsys_statecss_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判断属于哪个调度器)