可擴充套件排程器類

sched_ext 是一個排程器類,其行為可以由一組 BPF 程式定義 - BPF 排程器。

  • sched_ext 匯出一個完整的排程介面,因此任何排程演算法都可以在其上實現。

  • BPF 排程器可以根據自己的需要對 CPU 進行分組並一起排程它們,因為任務在喚醒時不會繫結到特定的 CPU。

  • BPF 排程器可以隨時動態地開啟和關閉。

  • 無論 BPF 排程器做什麼,系統完整性都會得到維護。 每當檢測到錯誤、可執行任務停滯或呼叫 SysRq 鍵序列 SysRq-S 時,都會恢復預設排程行為。

  • 當 BPF 排程器觸發錯誤時,會轉儲除錯資訊以幫助除錯。 除錯轉儲傳遞給排程器二進位制檔案並由其打印出來。 除錯轉儲也可以透過 sched_ext_dump 跟蹤點訪問。 SysRq 鍵序列 SysRq-D 觸發除錯轉儲。 這不會終止 BPF 排程器,並且只能透過跟蹤點讀取。

切換到和切換出 sched_ext

CONFIG_SCHED_CLASS_EXT 是啟用 sched_ext 的配置選項,而 tools/sched_ext 包含示例排程器。 應啟用以下配置選項才能使用 sched_ext

CONFIG_BPF=y
CONFIG_SCHED_CLASS_EXT=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_DEBUG_INFO_BTF=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_BPF_JIT_DEFAULT_ON=y
CONFIG_PAHOLE_HAS_SPLIT_BTF=y
CONFIG_PAHOLE_HAS_BTF_TAG=y

僅當 BPF 排程器載入並執行時才使用 sched_ext。

如果任務顯式將其排程策略設定為 SCHED_EXT,則在載入 BPF 排程器之前,它將被視為 SCHED_NORMAL 並由公平類排程器排程。

當 BPF 排程器載入並且 ops->flags 中未設定 SCX_OPS_SWITCH_PARTIAL 時,所有 SCHED_NORMALSCHED_BATCHSCHED_IDLESCHED_EXT 任務都由 sched_ext 排程。

但是,當 BPF 排程器載入並且 ops->flags 中設定了 SCX_OPS_SWITCH_PARTIAL 時,只有策略為 SCHED_EXT 的任務由 sched_ext 排程,而策略為 SCHED_NORMALSCHED_BATCHSCHED_IDLE 的任務由公平類排程器排程。

終止 sched_ext 排程器程式、觸發 SysRq-S 或檢測到任何內部錯誤(包括停滯的可執行任務)都會中止 BPF 排程器並將所有任務恢復為公平類排程器。

# 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 排程器的當前狀態可以如下確定

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

您可以透過檢查此單調遞增的計數器來檢查自啟動以來是否已載入任何 BPF 排程器(值為零表示尚未載入任何 BPF 排程器)

# cat /sys/kernel/sched_ext/enable_seq
1

tools/sched_ext/scx_show_state.py 是一個 drgn 指令碼,可顯示更詳細的資訊

# 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

給定任務是否在 sched_ext 上可以如下確定

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

基礎知識

使用者空間可以透過載入一組實現 struct sched_ext_ops 的 BPF 程式來實現任意 BPF 排程器。 唯一強制性欄位是 ops.name,它必須是有效的 BPF 物件名稱。 所有操作都是可選的。 以下修改後的摘錄來自 tools/sched_ext/scx_simple.bpf.c,顯示了一個最小的全域性 FIFO 排程器。

/*
 * Decide which CPU a task should be migrated to before being
 * enqueued (either at wakeup, fork time, or exec time). If an
 * idle core is found by the default ops.select_cpu() implementation,
 * then insert the task directly into SCX_DSQ_LOCAL and skip the
 * ops.enqueue() callback.
 *
 * Note that this implementation has exactly the same behavior as the
 * default ops.select_cpu implementation. The behavior of the scheduler
 * would be exactly same if the implementation just didn't define the
 * simple_select_cpu() struct_ops prog.
 */
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;
}

/*
 * Do a direct insertion of a task to the global DSQ. This ops.enqueue()
 * callback will only be invoked if we failed to find a core to insert
 * into in ops.select_cpu() above.
 *
 * Note that this implementation has exactly the same behavior as the
 * default ops.enqueue implementation, which just dispatches the task
 * to SCX_DSQ_GLOBAL. The behavior of the scheduler would be exactly same
 * if the implementation just didn't define the 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",
};

排程佇列

為了匹配排程器核心和 BPF 排程器之間的阻抗,sched_ext 使用 DSQ(排程佇列),它可以作為 FIFO 和優先順序佇列執行。 預設情況下,有一個全域性 FIFO(SCX_DSQ_GLOBAL)和每個 CPU 一個本地 DSQ(SCX_DSQ_LOCAL)。 BPF 排程器可以使用 scx_bpf_create_dsq()scx_bpf_destroy_dsq() 管理任意數量的 DSQ。

CPU 始終執行來自其本地 DSQ 的任務。 任務被“插入”到 DSQ 中。 非本地 DSQ 中的任務被“移動”到目標 CPU 的本地 DSQ 中。

當 CPU 正在尋找要執行的下一個任務時,如果本地 DSQ 不為空,則選擇第一個任務。 否則,CPU 嘗試從全域性 DSQ 移動任務。 如果這也沒有產生可執行的任務,則呼叫 ops.dispatch()

排程週期

以下簡要說明了如何排程和執行喚醒的任務。

  1. 當任務正在喚醒時,首先呼叫 ops.select_cpu() 操作。 這有兩個目的。 首先,CPU 選擇最佳化提示。 其次,喚醒空閒的選定 CPU。

    ops.select_cpu() 選擇的 CPU 是一個最佳化提示,而不是繫結。 實際的決定是在排程的最後一步做出的。 但是,如果 ops.select_cpu() 返回的 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_bpf_dsq_insert() 並使用以下選項之一,立即將任務插入到全域性或本地 DSQ 中:SCX_DSQ_GLOBALSCX_DSQ_LOCALSCX_DSQ_LOCAL_ON | cpu

    • 透過使用小於 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_LOCALSCX_DSQ_LOCAL_ON | cpuSCX_DSQ_GLOBAL 或自定義 DSQ。 雖然目前無法在持有 BPF 鎖的情況下呼叫 scx_bpf_dsq_insert(),但正在努力解決這個問題,並且將支援它。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)。

    • 進入空閒狀態。

請注意,BPF 排程器始終可以選擇立即在 ops.enqueue() 中排程任務,如上面的簡單示例所示。 如果僅使用內建 DSQ,則無需實現 ops.dispatch(),因為任務永遠不會在 BPF 排程器上排隊,並且本地和全域性 DSQ 都會自動執行。

scx_bpf_dsq_insert() 將任務插入到目標 DSQ 的 FIFO 中。 對於優先順序佇列,請使用 scx_bpf_dsq_insert_vtime()。 內部 DSQ(例如 SCX_DSQ_LOCALSCX_DSQ_GLOBAL)不支援優先順序佇列排程,並且必須使用 scx_bpf_dsq_insert() 進行排程。 有關更多資訊,請參閱函式文件和 tools/sched_ext/scx_simple.bpf.c 中的用法。

任務生命週期

以下虛擬碼總結了由 sched_ext 排程器管理的任務的整個生命週期

ops.init_task();            /* A new task is created */
ops.enable();               /* Enable BPF scheduling for the task */

while (task in SCHED_EXT) {
    if (task can migrate)
        ops.select_cpu();   /* Called on wakeup (optimization) */

    ops.runnable();         /* Task becomes ready to run */

    while (task is runnable) {
        if (task is not in a DSQ) {
            ops.enqueue();  /* Task can be added to a DSQ */

            /* A CPU becomes available */

            ops.dispatch(); /* Task is moved to a local DSQ */
        }
        ops.running();      /* Task starts running on its assigned CPU */
        ops.tick();         /* Called every 1/HZ seconds */
        ops.stopping();     /* Task stops running (time slice expires or wait) */
    }

    ops.quiescent();        /* Task releases its assigned CPU (wait) */
}

ops.disable();              /* Disable BPF scheduling for the task */
ops.exit_task();            /* Task is destroyed */

在哪裡查詢

  • include/linux/sched/ext.h 定義了核心資料結構、ops 表和常量。

  • kernel/sched/ext.c 包含 sched_ext 核心實現和助手。 字首為 scx_bpf_ 的函式可以從 BPF 排程器呼叫。

  • tools/sched_ext/ 託管示例 BPF 排程器實現。

    • scx_simple[.bpf].c:使用自定義 DSQ 的最小全域性 FIFO 排程器示例。

    • scx_qmap[.bpf].c:支援使用 BPF_MAP_TYPE_QUEUE 實現的五個優先順序級別的多級 FIFO 排程器。

ABI 不穩定性

sched_ext 為 BPF 排程器程式提供的 API 沒有穩定性保證。 這包括 include/linux/sched/ext.h 中定義的 ops 表回撥和常量,以及 kernel/sched/ext.c 中定義的 scx_bpf_ kfuncs。

雖然我們會嘗試提供相對穩定的 API 表面,但在核心版本之間可能會發生更改,恕不另行通知。