调度器73—sched_ext-1-内核文档翻译

一、sched-ext.rst

翻译自:linux-6.13.6/Documentation/scheduler/sched-ext.rst

==========================
1. 可扩展调度程序类
==========================

sched_ext 是一个调度器类,其行为可以由一组BPF程序定义,即BPF调度器。

* sched_ext 导出一个完整的调度接口,以便在上面实现任何调度算法。

* BPF调度器可以按照它认为合适的方式对CPU进行分组,并将它们安排在一起,因为任务在唤醒时并不与特定的CPU绑定。

* BPF调度器可以随时动态打开和关闭。

* 无论BPF调度器做什么,系统完整性都会得到维护。每当检测到错误、可运行任务停滞或调用 SysRq 键序列时,默认调度行为都会恢复:kbd:“SysRq-S”。

* 当BPF调度器触发错误时,会转储调试信息以帮助调试。调试转储被传递给调度程序二进制文件并由其打印出来。调试转储也可以通过`sched_ext_dump`跟踪点访问。SysRq键序列:kbd:“SysRq-D”触发调试转储。这不会终止BPF调度程序,只能通过跟踪点读取。

 

2. 切换到 sched_ext 和从 sched_ext 切换
===============================

``CONFIG_SCHED_CLASS_EXT 是启用 SCHED_EXT 的配置选项,而 tools/SCHED_EXT 包含示例调度器。应启用以下配置选项以使用 sched_ext:

..代码块::none

CONFIG_BPF=y
CONFIG_SCHED_CLASS_EXT=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_DEBUG_INF_BTF=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_BPF_JIT_DEFAULT_ON=y
CONFIG_PAHOLE_HAS_SPIT_BTF=y
CONFIG_PAHOLE_HAS_BTF_TAG=y

sched_ext 仅在BPF调度程序加载并运行时使用。

如果一个任务明确地将其调度策略设置为“SCHED_EXT”,它将被视为“SCHED_NORMAL”,并由CFS调度,直到加载BPF调度程序

(1) 当加载BPF调度程序并且“ops->flags”中未设置“SCX_OPS_SWITCH_PARTIAL”时,所有“SCHED_NORMAL”、“SCHED_BATCH”、“CHED_IDLE”,以及
“SCHED_EXT”任务由 SCHED_EXT 调度。

(2) 但是,当加载BPF调度程序并在“ops->flags”中设置“SCX_OPS_SWITCH_PARTIAL”时,只有具有“SCHED_EXT”策略的任务由 SCHED_EXT 调度,而具有“SCHED_NORMAL”、“SCHED_BATCH”和“SCHED_IDLE”策略的任务由CFS调度。

终止 sched_ext 调度程序,触发:kbd:“SysRq-S”,或检测到任何内部错误,包括停滞的可运行任务,会中止BPF调度程序并将所有任务恢复到CFS。

..代码块::none

    # make -j16 -C tools/sched_ext
    # tools/sched_ext/build/bin/scx_simple
    local=0 global=3
    local=5 global=24
    local=9 global=44
    local=13 global=56
    local=17 global=72
    ^CEXIT: BPF scheduler unregistered

BPF调度器的当前状态可以确定如下:

..代码块::none

    # cat /sys/kernel/sched_ext/state
    enabled
    # cat /sys/kernel/sched_ext/root/ops
    simple

您可以通过检查这个单调递增的计数器来检查自启动以来是否加载了任何BPF调度程序(零值表示没有加载BPF调度程序):

..代码块::none

    # cat /sys/kernel/sched_ext/enable_seq
    1

``tools/sched_ext/scx_show_state.py 是一个显示更详细信息的drgn脚本:

..代码块::none

    # tools/sched_ext/scx_show_state.py
    ops           : simple
    enabled       : 1
    switching_all : 1
    switched_all  : 1
    enable_state  : enabled (2)
    bypass_depth  : 0
    nr_rejected   : 0
    enable_seq    : 1

如果设置了“CONFIG_SCHED_DEBUG”,则可以按如下方式确定给定任务是否在 sched_ext 上:

..代码块::none

    # grep ext /proc/self/sched
    ext.enabled    

 

3. 基础知识
==========

Userspace 可以通过加载一组实现 “struct sched_ext_ops” 的BPF程序来实现任意BPF调度器。#### 唯一的必填字段是“ops.name”,它必须是有效的BPF对象名。所有操作都是可选的。以下修改后的摘录来自“tools/sched_ext/scx_simple.bpf.c”,显示了一个最小的全局FIFO调度器。

..代码块::c

    /*
     * 决定任务在进入队列之前(在wakeup、fork或exec时)应迁移到哪个CPU。如果默认的 ops.select_cpu()
     * 实现发现了空闲内核,则将任务直接插入 SCX_DSQ_LOCAL 并跳过 ops.englue() 回调。
     *
     * 请注意,此实现与默认的 ops.select_cpu 实现具有完全相同的行为。如果实现没有定义 simple_select_cpu()
     * struct_ops 程序,调度器的行为将完全相同。
     */
    s32 BPF_STRUCT_OPS(simple_select_cpu, struct task_struct *p, s32 prev_cpu, u64 wake_flags)
    {
        s32 cpu;
        /* Need to initialize or the BPF verifier will reject the program */
        bool direct = false;

        cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &direct);

        if (direct)
            scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);

        return cpu;
    }

    /*
     * 将任务直接插入全局DSQ。只有当我们在上面的 ops.select_cpu() 中找不到要插入的核心时,才会调用此 ops.englue()
     * 回调。
     * 请注意,此实现的行为与默认的 ops.enqueue 实现完全相同,后者只是将任务分派给 SCX_DSQ_GLOBAL。如果该实现没有
     * 定义 simple_enqueue struct_ops prog,则调度器的行为将完全相同。
     */
    void BPF_STRUCT_OPS(simple_enqueue, struct task_struct *p, u64 enq_flags)
    {
        scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
    }

    s32 BPF_STRUCT_OPS_SLEEPABLE(simple_init)
    {
        /*
         * By default, all SCHED_EXT, SCHED_OTHER, SCHED_IDLE, and
         * SCHED_BATCH tasks should use sched_ext.
         */
        return 0;
    }

    void BPF_STRUCT_OPS(simple_exit, struct scx_exit_info *ei)
    {
        exit_type = ei->type;
    }

    SEC(".struct_ops")
    struct sched_ext_ops simple_ops = {
        .select_cpu             = (void *)simple_select_cpu,
        .enqueue                = (void *)simple_enqueue,
        .init                   = (void *)simple_init,
        .exit                   = (void *)simple_exit,
        .name                   = "simple",
    };


4. 分发队列
---------------

为了匹配调度器核心和BPF调度器之间的阻抗,sched_ext 使用DSQ(分发队列),它既可以作为FIFO,也可以作为优先级队列。默认情况下,每个CPU有一个全局FIFO(“SCX_DSQ_GLOBAL”)和一个本地DSQ(“SCX_DSQ_LOCAL”)。BPF调度器可以使用“scx_bpf_create_dsq()”和“scx_bpf_destroy_dsq()”管理任意数量的dsq。

CPU总是从其本地DSQ执行任务。任务被“inserted”到DSQ中。非本地DSQ中的任务是“move”到目标CPU的本地DSQ。

当CPU正在寻找下一个要运行的任务时,如果本地DSQ不为空,则选择第一个任务。否则,CPU会尝试从全局DSQ中移动任务。如果这也没有产生可运行的任务,则调用`ops.dispatch()`。


5. 调度周期
----------------

下面简要显示了一个唤醒任务是如何被调度和执行的。

1.当任务被唤醒时,`ops.select_cpu()`是调用的第一个操作。这有两个目的。首先,CPU选择优化提示。第二,如果空闲,则唤醒所选CPU。

由`ops.select_cpu()`选择的CPU是一个优化提示,而不是绑定。实际决策是在调度的最后一步做出的。但是,如果CPU
``ops.select_cpu()`返回与任务最终运行的cpu匹配的结果。

选择CPU的一个副作用是将其从空闲状态唤醒。虽然BPF调度器可以使用`scx_bpf_kick_cpu()`唤醒任何cpu,但明智地使用`ops.select_cpu)`可以更简单、更高效。

通过调用`scx_bpf_dsq_insert()`,可以从`ops.select_cpu()``立即将任务插入DSQ。如果任务从“ops.select_cpu()”插入到“SCX_DSQ_LOCAL”中,则它将被插入到从“ops.select_cpu()”返回的任何cpu的本地DSQ中。此外,直接从`ops.select_cpu()`插入将导致跳过`ops.enqueue()`回调。

请注意,调度器核心将忽略无效的CPU选择,例如,如果它在任务的允许 cpumask 之外。

2.一旦选择了目标CPU,就会调用“ops.enqueue()”(除非任务是直接从“ops.select_cpu()”插入的)``ops.enqueue()`可以做出以下决定之一:

* 通过分别用“SCX_DSQ_GLOBAL”或“SCX_DSQ_LOCAL”调用“scx_bpf_dsq_insert()”,立即将任务插入全局或本地DSQ。

* 通过调用具有小于2^63的DSQ ID的`scx_bpf_dsq_insert()`,立即将任务插入到自定义DSQ中。

* 在BPF侧入队任务。

3.当CPU准备调度时,它首先查看其本地DSQ。如果为空,则查看全局DSQ。如果仍然没有任务要运行,则调用`ops.dispatch()`,它可以使用以下两个函数来填充本地DSQ。

* `scx_bpf_dsq_insert()`将任务插入到DSQ中。可以使用任何目标DSQ——“SCX_DSQ_LOCAL”、“SCX_DSM_LOCAL_ON|cpu”、“SCS_DSQ_GLOBAL”或自定义DSQ。虽然“scx_bpf_dsq_insert()”目前无法在持有bpf锁的情况下调用,但正在进行处理并将得到支持``scx_bpf_dsq_insert()`调度插入,而不是立即执行插入。最多可以有“ops.dispatch_max_batch”个待处理任务。

*`scx_bpf_move_to_local()`将任务从指定的非本地DSQ移动到分发DSQ。在持有任何BPF锁的情况下无法调用此函数``scx_bpf_move_to_local()`在尝试从指定的DSQ移动之前,先刷新挂起的插入任务。

4.在ops.dispatch()`返回后,如果本地DSQ中有任务,CPU将运行第一个任务。如果为空,则采取以下步骤:

* 尝试从全局DSQ转移。如果成功,则运行该任务。

* 如果`ops.dispatch()`已经分派了任何任务,请重试#3。

* 如果前一个任务是SCX任务并且仍然是可以运行状态,请继续执行它(请参阅“SCX_OPS_ENQ_LAST”)。

* 进入idle。

请注意,BPF调度器始终可以选择在“ops.enqueue()”中立即分派任务,如上述简单示例所示。如果只使用内置的DSQ,则不需要实现“ops.dispatch()”,因为任务永远不会在BPF调度器上排队,并且本地和全局DSQ都会自动执行。

``scx_bpf_dsq_insert()`将任务插入到目标DSQ的FIFO中。对优先级队列使用“scx_bpf_dsq_insert_vtime()”。内部DSQ(如“SCX_DSQ_LOCAL”和“SCX_DSQ_GLOBAL”)不支持优先级队列调度,并且必须使用“scx_bpf_dsq_insert()”进行分发。有关更多信息,请参阅“tools/sched_ext/scx_simple.bpf.c”中的函数文档和用法。


5. 在哪里看
=============

* include/linux/sched/ext.h 定义了核心数据结构、操作表和常量。

*`kernel/sched/ext.c`包含 sched_ext 核心实现和帮助函数。前缀为“scx_bpf_”的函数可以从bpf调度器调用。

*`tools/sched_ext/` 承载BPF调度器示例实现。

*`scx_simple[.bpf].c `:使用自定义DSQ的最小全局FIFO调度器示例。

*`scx_qmap[.bpf].c `:一个支持五个FIFO的多级FIFO调度器,使用“BPF_MAP_TYPE_QUEUE”实现的优先级。


6. ABI不稳定
===============

sched_ext 为BPF调度器程序提供的API没有稳定性保证。这包括在“include/linux/sched/ext.h”中定义的ops表回调和常量,以及在“kernel/sched/ext.c”中定义的“scx_bpf_” kfuncs。

虽然我们将尽可能提供一个相对稳定的API表面,但它们可能会在内核版本之间发生变化,而不会发出警告。

 

posted on 2025-03-10 01:23  Hello-World3  阅读(419)  评论(0)    收藏  举报

导航