요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
callback과 core scheduler의 경계
sched-ext.rst:119-195BPF callback이 정책을 결정하지만 실제 task 상태, context switch와 안전한 fallback은 core scheduler가 관리합니다.
select_cpu는 affinity와 idle CPU 정보를 바탕으로 초기 후보를 정하고 enqueue는 runnable task를 정책 자료구조 또는 DSQ에 넣습니다. dispatch는 CPU가 다음 task를 필요로 할 때 호출되어 local DSQ로 task를 공급합니다.
Dispatch Queue
sched-ext.rst:196-297DSQ는 core scheduler가 이해하는 task 대기열입니다. 모든 CPU에는 local DSQ가 있고 시스템 공용 global DSQ를 사용할 수 있으며 BPF scheduler가 custom DSQ를 생성할 수도 있습니다. FIFO와 virtual time 기반 priority queue 방식의 삽입 API는 함께 사용할 수 있는 범위가 다릅니다.
BPF 쪽 정책 자료구조와 실제 실행 직전의 local DSQ를 분리하면 정책 유연성과 core scheduler의 상태 검증을 함께 유지할 수 있습니다.
Task lifecycle과 오류 복귀
sched-ext.rst:298-370init, enable, runnable, running, stopping, quiescent와 exit callback은 task가 sched_ext에 들어오고 실행되고 빠져나가는 수명주기를 나눕니다. callback마다 허용되는 BPF helper와 lock context가 다르므로 sleep 가능 여부와 task reference lifetime을 확인해야 합니다.
watchdog는 runnable task가 과도하게 오래 dispatch되지 않는 상황을 검출합니다. BPF program이 오류를 보고하거나 watchdog이 발동하면 상태 dump를 남기고 sched_ext를 비활성화하여 시스템을 복구합니다. 정책 성능뿐 아니라 이 복구 경로도 시험해야 합니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. _sched-ext:
==========================
Extensible Scheduler Class
==========================
sched_ext is a scheduler class whose behavior can be defined by a set of BPF
programs - the BPF scheduler.
* sched_ext exports a full scheduling interface so that any scheduling
algorithm can be implemented on top.
* The BPF scheduler can group CPUs however it sees fit and schedule them
together, as tasks aren't tied to specific CPUs at the time of wakeup.
* The BPF scheduler can be turned on and off dynamically anytime.
* The system integrity is maintained no matter what the BPF scheduler does.
The default scheduling behavior is restored anytime an error is detected,
a runnable task stalls, or on invoking the SysRq key sequence
`SysRq-S`.
* When the BPF scheduler triggers an error, debug information is dumped to
aid debugging. The debug dump is passed to and printed out by the
scheduler binary. The debug dump can also be accessed through the
`sched_ext_dump` tracepoint. The SysRq key sequence `SysRq-D`
triggers a debug dump. This doesn't terminate the BPF scheduler and can
only be read through the tracepoint.
Switching to and from sched_ext
===============================
``CONFIG_SCHED_CLASS_EXT`` is the config option to enable sched_ext and
``tools/sched_ext`` contains the example schedulers. The following config
options should be enabled to use sched_ext:
.. code-block:: none
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
sched_ext is used only when the BPF scheduler is loaded and running.
If a task explicitly sets its scheduling policy to ``SCHED_EXT``, it will be
treated as ``SCHED_NORMAL`` and scheduled by the fair-class scheduler until the
BPF scheduler is loaded.
When the BPF scheduler is loaded and ``SCX_OPS_SWITCH_PARTIAL`` is not set
in ``ops->flags``, all ``SCHED_NORMAL``, ``SCHED_BATCH``, ``SCHED_IDLE``, and
``SCHED_EXT`` tasks are scheduled by sched_ext.
However, when the BPF scheduler is loaded and ``SCX_OPS_SWITCH_PARTIAL`` is
set in ``ops->flags``, only tasks with the ``SCHED_EXT`` policy are scheduled
by sched_ext, while tasks with ``SCHED_NORMAL``, ``SCHED_BATCH`` and
``SCHED_IDLE`` policies are scheduled by the fair-class scheduler.
Terminating the sched_ext scheduler program, triggering `SysRq-S`, or
detection of any internal error including stalled runnable tasks aborts the
BPF scheduler and reverts all tasks back to the fair-class scheduler.
.. code-block:: 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
The current status of the BPF scheduler can be determined as follows:
.. code-block:: none
# cat /sys/kernel/sched_ext/state
enabled
# cat /sys/kernel/sched_ext/root/ops
simple
You can check if any BPF scheduler has ever been loaded since boot by examining
this monotonically incrementing counter (a value of zero indicates that no BPF
scheduler has been loaded):
.. code-block:: none
# cat /sys/kernel/sched_ext/enable_seq
1
``tools/sched_ext/scx_show_state.py`` is a drgn script which shows more
detailed information:
.. code-block:: 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
Whether a given task is on sched_ext can be determined as follows:
.. code-block:: none
# grep ext /proc/self/sched
ext.enabled : 1
The Basics
==========
Userspace can implement an arbitrary BPF scheduler by loading a set of BPF
programs that implement ``struct sched_ext_ops``. The only mandatory field
is ``ops.name`` which must be a valid BPF object name. All operations are
optional. The following modified excerpt is from
``tools/sched_ext/scx_simple.bpf.c`` showing a minimal global FIFO scheduler.
.. code-block:: c
/*
* 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",
};
Dispatch Queues
---------------
To match the impedance between the scheduler core and the BPF scheduler,
sched_ext uses DSQs (dispatch queues) which can operate as both a FIFO and a
priority queue. By default, there is one global FIFO (``SCX_DSQ_GLOBAL``),
and one local DSQ per CPU (``SCX_DSQ_LOCAL``). The BPF scheduler can manage
an arbitrary number of DSQs using ``scx_bpf_create_dsq()`` and
``scx_bpf_destroy_dsq()``.
A CPU always executes a task from its local DSQ. A task is "inserted" into a
DSQ. A task in a non-local DSQ is "move"d into the target CPU's local DSQ.
When a CPU is looking for the next task to run, if the local DSQ is not
empty, the first task is picked. Otherwise, the CPU tries to move a task
from the global DSQ. If that doesn't yield a runnable task either,
``ops.dispatch()`` is invoked.
Scheduling Cycle
----------------
The following briefly shows how a waking task is scheduled and executed.
1. When a task is waking up, ``ops.select_cpu()`` is the first operation
invoked. This serves two purposes. First, CPU selection optimization
hint. Second, waking up the selected CPU if idle.
The CPU selected by ``ops.select_cpu()`` is an optimization hint and not
binding. The actual decision is made at the last step of scheduling.
However, there is a small performance gain if the CPU
``ops.select_cpu()`` returns matches the CPU the task eventually runs on.
A side-effect of selecting a CPU is waking it up from idle. While a BPF
scheduler can wake up any cpu using the ``scx_bpf_kick_cpu()`` helper,
using ``ops.select_cpu()`` judiciously can be simpler and more efficient.
A task can be immediately inserted into a DSQ from ``ops.select_cpu()``
by calling ``scx_bpf_dsq_insert()``. If the task is inserted into
``SCX_DSQ_LOCAL`` from ``ops.select_cpu()``, it will be inserted into the
local DSQ of whichever CPU is returned from ``ops.select_cpu()``.
Additionally, inserting directly from ``ops.select_cpu()`` will cause the
``ops.enqueue()`` callback to be skipped.
Note that the scheduler core will ignore an invalid CPU selection, for
example, if it's outside the allowed cpumask of the task.
2. Once the target CPU is selected, ``ops.enqueue()`` is invoked (unless the
task was inserted directly from ``ops.select_cpu()``). ``ops.enqueue()``
can make one of the following decisions:
* Immediately insert the task into either the global or a local DSQ by
calling ``scx_bpf_dsq_insert()`` with one of the following options:
``SCX_DSQ_GLOBAL``, ``SCX_DSQ_LOCAL``, or ``SCX_DSQ_LOCAL_ON | cpu``.
* Immediately insert the task into a custom DSQ by calling
``scx_bpf_dsq_insert()`` with a DSQ ID which is smaller than 2^63.
* Queue the task on the BPF side.
3. When a CPU is ready to schedule, it first looks at its local DSQ. If
empty, it then looks at the global DSQ. If there still isn't a task to
run, ``ops.dispatch()`` is invoked which can use the following two
functions to populate the local DSQ.
* ``scx_bpf_dsq_insert()`` inserts a task to a DSQ. Any target DSQ can be
used - ``SCX_DSQ_LOCAL``, ``SCX_DSQ_LOCAL_ON | cpu``,
``SCX_DSQ_GLOBAL`` or a custom DSQ. While ``scx_bpf_dsq_insert()``
currently can't be called with BPF locks held, this is being worked on
and will be supported. ``scx_bpf_dsq_insert()`` schedules insertion
rather than performing them immediately. There can be up to
``ops.dispatch_max_batch`` pending tasks.
* ``scx_bpf_move_to_local()`` moves a task from the specified non-local
DSQ to the dispatching DSQ. This function cannot be called with any BPF
locks held. ``scx_bpf_move_to_local()`` flushes the pending insertions
tasks before trying to move from the specified DSQ.
4. After ``ops.dispatch()`` returns, if there are tasks in the local DSQ,
the CPU runs the first one. If empty, the following steps are taken:
* Try to move from the global DSQ. If successful, run the task.
* If ``ops.dispatch()`` has dispatched any tasks, retry #3.
* If the previous task is an SCX task and still runnable, keep executing
it (see ``SCX_OPS_ENQ_LAST``).
* Go idle.
Note that the BPF scheduler can always choose to dispatch tasks immediately
in ``ops.enqueue()`` as illustrated in the above simple example. If only the
built-in DSQs are used, there is no need to implement ``ops.dispatch()`` as
a task is never queued on the BPF scheduler and both the local and global
DSQs are executed automatically.
``scx_bpf_dsq_insert()`` inserts the task on the FIFO of the target DSQ. Use
``scx_bpf_dsq_insert_vtime()`` for the priority queue. Internal DSQs such as
``SCX_DSQ_LOCAL`` and ``SCX_DSQ_GLOBAL`` do not support priority-queue
dispatching, and must be dispatched to with ``scx_bpf_dsq_insert()``. See
the function documentation and usage in ``tools/sched_ext/scx_simple.bpf.c``
for more information.
Task Lifecycle
--------------
The following pseudo-code summarizes the entire lifecycle of a task managed
by a sched_ext scheduler:
.. code-block:: c
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 && task->scx.slice == 0) {
ops.enqueue(); /* Task can be added to a DSQ */
/* Any usable CPU becomes available */
ops.dispatch(); /* Task is moved to a local DSQ */
}
ops.running(); /* Task starts running on its assigned CPU */
while task_is_runnable(p) {
while (task->scx.slice > 0 && task_is_runnable(p))
ops.tick(); /* Called every 1/HZ seconds */
ops.dispatch(); /* task->scx.slice can be refilled */
}
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 */
Where to Look
=============
* ``include/linux/sched/ext.h`` defines the core data structures, ops table
and constants.
* ``kernel/sched/ext.c`` contains sched_ext core implementation and helpers.
The functions prefixed with ``scx_bpf_`` can be called from the BPF
scheduler.
* ``tools/sched_ext/`` hosts example BPF scheduler implementations.
* ``scx_simple[.bpf].c``: Minimal global FIFO scheduler example using a
custom DSQ.
* ``scx_qmap[.bpf].c``: A multi-level FIFO scheduler supporting five
levels of priority implemented with ``BPF_MAP_TYPE_QUEUE``.
ABI Instability
===============
The APIs provided by sched_ext to BPF schedulers programs have no stability
guarantees. This includes the ops table callbacks and constants defined in
``include/linux/sched/ext.h``, as well as the ``scx_bpf_`` kfuncs defined in
``kernel/sched/ext.c``.
While we will attempt to provide a relatively stable API surface when
possible, they are subject to change without warning between kernel
versions.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
sched_ext의 목적과 복구 특성
1-29sched_ext는 BPF program 집합으로 scheduling 동작을 정의할 수 있는 scheduler class다. BPF scheduler가 사용할 전체 scheduling interface를 노출하므로 임의의 알고리즘을 구현할 수 있다.
task는 wakeup 순간 특정 CPU에 영구 결박되지 않으므로 BPF scheduler가 CPU를 원하는 방식으로 묶고 함께 schedule할 수 있다. scheduler는 runtime에 동적으로 load하거나 unload할 수 있다.
잘못된 BPF scheduler가 system integrity를 무너뜨리지 않도록 내부 오류, runnable task stall 또는 SysRq-S가 감지되면 sched_ext를 중단하고 기본 fair-class scheduling을 복원한다.
BPF scheduler error가 발생하면 scheduler userspace binary가 debug dump를 받아 출력한다. 같은 dump는 sched_ext_dump tracepoint에서도 읽을 수 있다. SysRq-D는 scheduler를 종료하지 않고 dump만 trigger하며, 이 경우 tracepoint를 통해서만 읽는다.
BPF scheduler는 동적으로 동작하지만 오류나 강제 복구 요청이 있으면 모든 task를 fair class로 되돌린다.
sched_ext 활성화와 상태 확인
30-118CONFIG_SCHED_CLASS_EXT가 sched_ext를 활성화하며 example scheduler는 tools/sched_ext에 있다. BPF syscall, JIT, BTF와 pahole 기능을 함께 켠다.
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 scheduler가 load되어 실행 중일 때만 sched_ext를 사용한다. 그 전에 task가 policy를 SCHED_EXT로 명시해도 SCHED_NORMAL처럼 fair class가 schedule한다.
BPF scheduler가 load되고 ops->flags에 SCX_OPS_SWITCH_PARTIAL이 없으면 SCHED_NORMAL, SCHED_BATCH, SCHED_IDLE, SCHED_EXT task 모두 sched_ext로 전환된다. flag가 있으면 policy가 명시적으로 SCHED_EXT인 task만 sched_ext가 처리하고 나머지는 fair class에 남는다.
scheduler process 종료, SysRq-S, 내부 오류 또는 runnable task stall은 BPF scheduler를 abort하고 모든 task를 fair class로 되돌린다. example scheduler는 다음처럼 build하고 실행한다.
# 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
/sys/kernel/sched_ext/state는 현재 enable 상태를, root/ops는 active ops 이름을 보여 준다. enable_seq는 boot 이후 BPF scheduler가 load될 때마다 증가하므로 0이면 한 번도 load되지 않은 것이다.
# cat /sys/kernel/sched_ext/state
enabled
# cat /sys/kernel/sched_ext/root/ops
simple
# cat /sys/kernel/sched_ext/enable_seq
1
tools/sched_ext/scx_show_state.py는 drgn으로 ops, enabled, switching_all, switched_all, enable_state, bypass_depth, rejected count와 sequence를 표시한다. 특정 task의 /proc/<pid>/sched에서 ext.enabled가 1인지 확인하면 sched_ext 적용 여부를 알 수 있다.
# 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
# grep ext /proc/self/sched
ext.enabled : 1
최소 BPF scheduler와 struct sched_ext_ops
119-195userspace loader는 struct sched_ext_ops를 구현하는 BPF program 집합을 load한다. mandatory field는 유효한 BPF object 이름인 ops.name뿐이고 모든 operation callback은 optional이다.
아래 코드는 tools/sched_ext/scx_simple.bpf.c에서 가져온 최소 global FIFO scheduler다. simple_select_cpu()는 기본 select_cpu와 동일하게 idle CPU candidate를 찾는다. direct가 true면 선택한 CPU의 SCX_DSQ_LOCAL에 task를 바로 넣고 enqueue callback을 건너뛴다. verifier가 모든 path에서 초기화된 값을 요구하므로 direct=false로 초기화한다.
s32 BPF_STRUCT_OPS(simple_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
s32 cpu;
/* 초기화하지 않으면 BPF verifier가 거부 */
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;
}
idle core direct insertion에 실패한 task만 simple_enqueue()로 온다. 이 callback은 task를 SCX_DSQ_GLOBAL FIFO에 넣는다. 기본 enqueue도 같은 동작을 하므로 callback을 생략해도 결과는 같다.
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)
{
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",
};
simple_init()이 0을 반환하고 partial switch flag를 지정하지 않으므로 SCHED_EXT뿐 아니라 SCHED_OTHER, SCHED_IDLE, SCHED_BATCH task도 sched_ext를 사용한다. simple_exit()은 scx_exit_info의 종료 종류를 userspace reporting용 변수에 남긴다.
Dispatch Queue
196-213sched_ext는 scheduler core와 BPF scheduler 사이의 실행 task 전달을 DSQ(dispatch queue)로 연결한다. DSQ는 FIFO와 priority queue로 동작할 수 있다. 기본으로 system 전체 SCX_DSQ_GLOBAL FIFO 하나와 CPU마다 SCX_DSQ_LOCAL이 하나씩 있다.
BPF scheduler는 scx_bpf_create_dsq()/scx_bpf_destroy_dsq()로 custom DSQ를 원하는 수만큼 관리할 수 있다. CPU는 항상 자신의 local DSQ에서 task를 실행한다. task를 queue에 넣는 operation은 insert이고, non-local DSQ의 task를 특정 CPU local DSQ로 옮기는 것은 move다.
CPU가 다음 task를 찾을 때 local DSQ가 비어 있지 않으면 첫 task를 고른다. 비어 있으면 global DSQ에서 local로 move를 시도하고, 거기에도 runnable task가 없을 때 ops.dispatch()를 호출한다.
모든 task는 실행 직전에 target CPU의 local DSQ에 있어야 한다.
wakeup부터 실행까지 scheduling cycle
214-297task wakeup에서 첫 callback은 ops.select_cpu()다. CPU 선택 optimization hint와 idle CPU wakeup이라는 두 역할을 한다. 반환 CPU는 강제 binding이 아니며 최종 실행 CPU는 scheduling 마지막 단계에서 결정된다. 다만 hint와 최종 CPU가 같으면 migration 비용을 줄일 수 있다.
BPF scheduler는 scx_bpf_kick_cpu()로 임의 CPU를 깨울 수 있지만 select_cpu의 side effect를 적절히 쓰는 편이 단순하고 효율적이다. select_cpu 안에서 scx_bpf_dsq_insert()로 task를 즉시 넣을 수 있다. SCX_DSQ_LOCAL이면 select_cpu가 반환한 CPU의 local DSQ에 들어가며 이후 ops.enqueue()는 호출되지 않는다. task allowed cpumask 밖의 invalid CPU 반환은 core가 무시한다.
direct insert가 없으면 target CPU 선택 뒤 ops.enqueue()가 호출된다. callback은 SCX_DSQ_GLOBAL, SCX_DSQ_LOCAL, SCX_DSQ_LOCAL_ON|cpu, 2^63보다 작은 ID의 custom DSQ 중 하나에 즉시 insert하거나 BPF-side data structure에 task를 보관할 수 있다.
CPU가 schedule할 때 local, global 순서로 확인하고 둘 다 비면 ops.dispatch()를 부른다. dispatch는 scx_bpf_dsq_insert()로 어느 DSQ든 insertion을 예약할 수 있으며 pending 수는 ops.dispatch_max_batch까지다. 현재 이 helper는 BPF lock을 보유한 채 호출할 수 없지만 지원이 개발 중이다.
scx_bpf_move_to_local()은 지정 non-local DSQ에서 현재 dispatching CPU의 local DSQ로 task를 옮긴다. BPF lock을 보유한 채 호출할 수 없고, move 전에 pending insert를 flush한다.
dispatch가 반환한 뒤 local DSQ에 task가 있으면 첫 task를 실행한다. 없으면 global DSQ move를 다시 시도하고, dispatch가 task를 하나라도 예약했다면 dispatch 단계부터 재시도한다. 이전 task가 SCX task이고 여전히 runnable이면 SCX_OPS_ENQ_LAST 규칙에 따라 계속 실행할 수 있으며, 모두 실패하면 CPU가 idle로 간다.
enqueue에서 항상 built-in DSQ로 즉시 dispatch하면 BPF-side에 대기 task가 없어 ops.dispatch()를 구현할 필요가 없다. scx_bpf_dsq_insert()는 FIFO에 넣고 scx_bpf_dsq_insert_vtime()은 priority queue ordering을 사용한다. SCX_DSQ_LOCAL과 GLOBAL 같은 internal DSQ는 priority queue insertion을 지원하지 않는다.
direct insertion 경로와 BPF-side queue를 사용하는 dispatch 경로를 함께 보여 준다.
sched_ext task lifecycle
298-340새 task가 만들어지면 ops.init_task()로 BPF scheduler별 상태를 초기화하고 ops.enable()로 task에 BPF scheduling을 켠다. SCHED_EXT에 있는 동안 wakeup과 runnable/run/stop/quiescent cycle을 반복한다.
ops.init_task(); /* 새 task 생성 */
ops.enable(); /* task에 BPF scheduling 활성화 */
while (task in SCHED_EXT) {
if (task can migrate)
ops.select_cpu(); /* wakeup optimization */
ops.runnable(); /* runnable 전환 */
while (task is runnable) {
if (task is not in a DSQ && task->scx.slice == 0) {
ops.enqueue();
/* usable CPU가 생김 */
ops.dispatch();
}
ops.running();
while (task_is_runnable(p)) {
while (task->scx.slice > 0 && task_is_runnable(p))
ops.tick();
ops.dispatch(); /* slice refill 가능 */
}
ops.stopping();
}
ops.quiescent(); /* wait로 CPU release */
}
ops.disable();
ops.exit_task();
migration 가능한 wakeup에서는 select_cpu를 호출하고 runnable callback 뒤 DSQ와 slice 상태에 따라 enqueue/dispatch한다. CPU 실행 직전에 running, 1/HZ tick마다 tick, time slice 만료나 wait로 CPU를 떠날 때 stopping을 부른다. task가 wait 상태로 assigned CPU를 완전히 놓으면 quiescent가 호출된다.
task가 SCHED_EXT를 벗어나면 disable이 호출되고, task destruction 때 exit_task가 scheduler별 memory와 map state를 정리한다.
task lifetime callback과 반복되는 runnable lifecycle을 구분한다.
구현 위치와 ABI 안정성
341-369| 경로 | 내용 |
|---|---|
| include/linux/sched/ext.h | core data structure, ops table, constant |
| kernel/sched/ext.c | sched_ext core와 BPF에서 호출 가능한 scx_bpf_ helper |
| tools/sched_ext/ | example BPF scheduler와 userspace loader |
| tools/sched_ext/scx_simple[.bpf].c | custom DSQ를 사용하는 최소 global FIFO 예 |
| tools/sched_ext/scx_qmap[.bpf].c | BPF_MAP_TYPE_QUEUE로 구현한 5-level priority multi-level FIFO |
sched_ext가 BPF scheduler에 제공하는 API에는 안정성 보장이 없다. include/linux/sched/ext.h의 callback·constant와 kernel/sched/ext.c의 scx_bpf_ kfunc는 kernel version 사이에서 사전 경고 없이 바뀔 수 있다.
가능한 범위에서 비교적 안정적인 surface를 유지하려고 하지만, out-of-tree scheduler는 대상 kernel source와 함께 build하고 API 변경을 추적해야 한다.
sched_ext 등록과 해제
sched-ext.rst:1-118sched_ext는 BPF struct_ops로 scheduler 동작 일부를 구현하는 확장형 scheduling class입니다. BPF scheduler가 정상 등록되면 대상 task가 sched_ext class로 전환되고, 프로그램 오류나 watchdog timeout이 발생하면 커널이 built-in scheduler로 복귀시킬 수 있습니다.
sched_ext ABI는 안정 ABI가 아닙니다. 커널 버전, BTF type과 struct_ops callback 변경을 함께 추적해야 하며 BPF CO-RE만으로 모든 의미 변화가 자동 해결되지는 않습니다.