Эта статья представляет собой короткое руководство по созданию минимального планировщика с использованием sched_ext на языке C. Этот планировщик использует глобальную очередь планирования, из которой каждый CPU берёт задачи на выполнение на один квант времени. Порядок планирования — «первым пришёл — первым обслужен» (First-In-First-Out, FIFO). По сути, это реализация циклического планирования (round-robin):

Этот короткий туториал покрывает основы; чтобы узнать больше, загляните в материалы из вики scx.
Требования
Чтобы собрать собственный планировщик, нам нужно ядро 6.12 или ядро 6.11 с патчами. Можно взять ядро с патчами расширений планировщика для Ubuntu 24.10 отсюда, либо использовать CachyOS и установить пропатченное ядро оттуда.
Кроме того, вам понадобятся:
свежий
clangдля компиляцииbpftoolдля подключения планировщика
На Ubuntu, например, можно выполнить: apt install clang linux-tools-common linux-tools-$(uname -r).
Больше ничего не нужно для запуска, а код этого туториала вы найдёте в репозитории minimal-scheduler. Я бы рекомендовал просто клонировать его:
git clone https://github.com/parttimenerd/minimal-scheduler cd minimal-scheduler
Сам планировщик находится в файле sched_ext.bpf.c, но прежде чем мы на него посмотрим, я покажу, как этим планировщиком пользоваться.
Использование
Вкратце, нужны всего два шага:
#собрать бинарник планировщика ./build.sh #запустить планировщик sudo ./start.sh #сделать что-нибудь … #остановить планировщик sudo ./stop.sh
Позже я покажу, что находится внутри этих скриптов, но сначала перейдём к коду планировщика:
Планировщик
Предполагается, что у вас уже есть некоторый опыт написания eBPF-программ. Если нет, хорошей отправной точкой будет книга Лиз Райс «Learning eBPF».
Код планировщика зависит только от BPF-заголовков ядра Linux и sched_ext. Ниже приведён код из sched_ext.bpf.c:
// Этот заголовок генерируется автоматически, как будет объяснено далее #include <vmlinux.h> // Следующие два заголовка берутся из набора заголовков Linux #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> // Определяем идентификатор обще�� очереди диспетчеризации (DSQ) // Мы используем её как глобальную очередь планирования #define SHARED_DSQ_ID 0 // Два макроса, которые делают дальнейший код более читаемым // и размещают функции в правильных секциях // бинарного файла #define BPF_STRUCT_OPS(name, args...) \ SEC("struct_ops/"#name) BPF_PROG(name, ##args) #define BPF_STRUCT_OPS_SLEEPABLE(name, args...) \ SEC("struct_ops.s/"#name) \ BPF_PROG(name, ##args) // Инициализируем планировщик, создав общую очередь диспетчеризации (DSQ) s32 BPF_STRUCT_OPS_SLEEPABLE(sched_init) { // Все функции scx_ приходят из vmlinux.h return scx_bpf_create_dsq(SHARED_DSQ_ID, -1); } // Ставим в очередь задачу, которая хочет выполняться, // помещая её в общую DSQ с заданным квантом времени int BPF_STRUCT_OPS(sched_enqueue, struct task_struct *p, u64 enq_flags) { // Вычисляем квант времени для задачи на основе числа задач в очереди // Это делает систему немного более отзывчивой, чем базовый round-robin, // где каждой задаче постоянно выдаётся один и тот же квант времени // Базовый квант — 5_000_000 нс, то есть 5 мс u64 slice = 5000000u / scx_bpf_dsq_nr_queued(SHARED_DSQ_ID); scx_bpf_dispatch(p, SHARED_DSQ_ID, slice, enq_flags); return 0; } // Забираем (consume) задачу из общей DSQ для CPU, // когда CPU нужно что-то выполнять — обычно после того, как он закончил // выполнять предыдущую задачу в течение выделенного кванта времени int BPF_STRUCT_OPS(sched_dispatch, s32 cpu, struct task_struct *prev) { scx_bpf_consume(SHARED_DSQ_ID); return 0; } // Определяем основную структуру операций планировщика (sched_ops) SEC(".struct_ops.link") struct sched_ext_ops sched_ops = { .enqueue = (void *)sched_enqueue, .dispatch = (void *)sched_dispatch, .init = (void *)sched_init, // Доступно больше функций, но мы сосредоточимся // на важных для минимального планировщика .flags = SCX_OPS_ENQ_LAST | SCX_OPS_KEEP_BUILTIN_IDLE, // Имя, которое появится в // /sys/kernel/sched_ext/root/ops // после подключения планировщика // Имя должно быть корректным идентификатором C .name = "minimal_scheduler" }; // Все планировщики должны распространяться по лицензии GPLv2 char _license[] SEC("license") = "GPL";
Визуализировать взаимодействие всех функций в планировщике можно с помощью следующей диаграммы:

Теперь, когда вы увидели код, запустите скрипт build.sh, чтобы сгенерировать заголовочный файл vmlinux.h (из BTF), а затем скомпилируйте код планировщика в объектный файл eBPF (BPF bytecode в .o):
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h clang -target bpf -g -O2 -c sched_ext.bpf.c -o sched_ext.bpf.o -I.
Затем запустите скрипт start.sh с root-правами, чтобы подключить планировщик с помощью bpftool:
bpftool struct_ops register sched_ext.bpf.o /sys/fs/bpf/sched_ext
Теперь пользовательский планировщик стал планировщиком этой системы. Проверить это можно, открыв файл /sys/kernel/sched_ext/root/ops:
> cat /sys/kernel/sched_ext/root/ops minimal_scheduler
А также посмотрев вывод dmesg | tail:
> sudo dmesg | tail # ... [32490.366637] sched_ext: BPF scheduler "minimal_scheduler" enabled
Поиграйтесь со своей системой и посмотрите, как она себя ведёт.
Если вы закончили, можно отключить планировщик, запустив скрипт stop.sh с root-правами. Это удалит файл /sys/fs/bpf/sched_ext/sched_ops.
Задания для читателя
Теперь, когда вы знаете, как выглядит базовый планировщик, можно начать его модифицировать. Вот несколько идей:
— Изменяйте квант времени
Как ведёт себя система, если увеличить или уменьшить квант времени?
Например, попробуйте квант 1 с. Видите ли вы разницу в том, как движется курсор? Или попробуйте маленький квант в 100 мкс и запустите программу, которая выполняет какие-то вычисления — заметите ли вы разницу в её производительности?
— Используйте фиксированный квант времени
Как поведёт себя система, если изменить планировщик так, чтобы он всегда использовал один и тот же квант времени, игнорируя число задач, поставленных в очередь?
Попробуйте, например, создать нагрузку на систему и посмотреть, как она себя ведёт.
— Ограничьте используемые CPU
Как ведёт себя система, если планировщик планирует задачи только на конкретные CPU?
Попробуйте, например, сделать систему фактически «одноядерной», потребляя задачи только на CPU 0 в sched_dispatch (подсказка: параметр cpu — это идентификатор CPU).
— Создайте несколько очередей планирования
Как ведёт себя система с несколькими очередями планирования для разных CPU и процессов?
Попробуйте, например, создать две очереди планирования, где одна очередь будет только для процесса с определённым id (подсказка: task_struct#tgid — это id процесса) и будет планироваться на половине ваших CPU.
Загляните в заголовочный файл linux/sched.h, чтобы узнать больше о task_struct.
— Используйте больше возможностей BPF
Если вы уже умеете писать базовые eBPF-программы, используйте bpf_trace_printk и хуки running и stopping.
Хук running вызывается каждый раз, когда задача начинает выполняться на CPU; получить текущий id CPU можно через smp_processor_id():
int BPF_STRUCT_OPS(sched_running, struct task_struct *p) { // ... return 0; // в eBPF нет функций с типом void }
Хук stopping вызывается каждый раз, когда задача прекращает выполняться:
int BPF_STRUCT_OPS(sched_stopping, struct task_struct *p, bool runnable) { // ... return 0; }
Это можно использовать, чтобы делать визуализации порядка планирования.
Дальше — больше
Чтобы пойти дальше, можно посмотреть собранные материалы в вики scx, особенно хорошо документированный код sched-ext в ядре.
Если вам интересно, как использовать это в Rust, взгляните на scx и scx_rust_scheduler, а для Java — на hello-ebpf.
Заключение
Надеюсь, вам было интересно увидеть, насколько просто сделать крошечный планировщик, не полагаясь на большие фреймворки в качестве зависимостей.

Работа с sched_ext и eBPF быстро упирается в понимание того, как код на C взаимодействует с ядром, памятью и ресурсами ОС. Курс «Программист C» помогает закрыть этот слой: системное программирование на практике, архитектура процессора и памяти, реальные кейсы низкоуровневых приложений и интеграции с ОС. Пройдите вступительный тест и узнаете, подойдет ли вам программа курса.
А чтобы узнать больше о формате обучения и задать экспертам вопросы, приходите на бесплатные демо-уроки:
15 января, 20:00 — Работа с памятью на языке C. Записаться
22 января, 19:00 — eBPF: рентгеновское зрение для production. Видим сеть, безопасность и узкие места на уровне ядра Linux. Записаться
