Каждый разработчик систем реального времени рано или поздно упирается в один и тот же вопрос: почему задача, которая обязана отработать за 500 микросекунд, иногда берёт 50 миллисекунд? Ответ почти всегда один и тот же: неверно выбранная политика планировщика. Linux предлагает три специализированных класса для задач с жёсткими временными требованиями, и каждый из них думает о процессорном времени по-своему. Понять логику каждого значит получить настоящий контроль над поведением системы под нагрузкой.
Почему стандартный CFS не подходит для задач реального времени
Completely Fair Scheduler превосходно справляется с обычной нагрузкой: браузер, компилятор, фоновые службы. Его принцип красив в своей простоте: дать каждому потоку справедливую долю процессорного времени. Но именно эта "справедливость" делает CFS непригодным там, где опоздание на миллисекунду уже является сбоем.
CFS не даёт гарантий. Он лишь стремится к равенству. Реальновременные политики работают иначе: любой поток под SCHED_FIFO или SCHED_RR с приоритетом от 1 до 99 безоговорочно вытесняет все потоки CFS. Выше их обоих стоит SCHED_DEADLINE: как только в системе появляется хоть один готовый к выполнению deadline-поток, планировщик немедленно передаёт ему управление.
Посмотреть текущие политики всех процессов можно так:
ps -eo pid,policy,rtprio,comm | head -20
Вывод покажет FF для SCHED_FIFO, RR для SCHED_RR, #6 для SCHED_DEADLINE и TS для обычных CFS-задач. Уже один взгляд на этот список даёт понимание приоритетной иерархии в живой системе.
SCHED_FIFO и безоговорочное владение процессором
SCHED_FIFO строится на железном правиле: поток выполняется до тех пор, пока добровольно не освободит CPU, не заблокируется на вводе-выводе, или пока его не вытеснит поток с более высоким приоритетом. Никакого принудительного квантования по таймеру, никаких невидимых переключений контекста. Поток либо владеет процессором, либо ждёт.
Это делает SCHED_FIFO самым предсказуемым и одновременно самым требовательным инструментом. Разработчик берёт на себя ответственность за поведение задачи: процесс с бесконечным вычислительным циклом и без единого системного вызова заморозит ядро CPU полностью. Именно поэтому начиная с Linux 2.6.25 действует механизм rt_bandwidth: реальновременные задачи могут суммарно занимать не более 95% процессорного времени в секунду, оставляя оставшееся системным потокам ядра.
Назначить политику SCHED_FIFO существующему процессу через утилиту chrt:
# Установить SCHED_FIFO с приоритетом 50 для процесса с PID 1823
chrt -f -p 50 1823
# Запустить команду сразу под SCHED_FIFO с приоритетом 30
chrt -f 30 ./my_realtime_app
# Проверить результат
chrt -p 1823
Тот же результат программно через системный вызов:
#include <sched.h>
#include <string.h>
struct sched_param param;
memset(¶m, 0, sizeof(param));
param.sched_priority = 50;
if (sched_setscheduler(0, SCHED_FIFO, ¶m) != 0) {
perror("sched_setscheduler");
}
Ноль в первом аргументе означает "текущий процесс". Вызов требует либо прав root, либо capability CAP_SYS_NICE. При равных приоритетах поведение строго определено: вытесненный поток после завершения более приоритетного возвращается в начало своей очереди, а не в конец. Это не случайность, а намеренное поведение, закреплённое в спецификации POSIX.
SCHED_RR и честное распределение внутри одного приоритетного уровня
SCHED_RR отличается от SCHED_FIFO ровно одним механизмом: квантованием времени. Когда выделенный квант истекает, поток перемещается в конец очереди своего приоритетного уровня, уступая место следующему претенденту. Если других претендентов того же уровня нет, поток немедленно получает CPU снова и ведёт себя неотличимо от SCHED_FIFO.
Разница проявляется только при конкуренции нескольких потоков одинакового приоритета. Именно для этого случая SCHED_RR и существует: несколько каналов обработки данных, несколько параллельных задач телеметрии, несколько потоков аудиопайплайна с одинаковой срочностью.
# Установить SCHED_RR с приоритетом 99 для процесса с PID 1823
chrt -r -p 99 1823
# Узнать текущий квант времени для SCHED_RR в наносекундах
chrt -r -p 1823
# Прочитать значение кванта из /proc
cat /proc/sys/kernel/sched_rr_timeslice_ms
Минимальный квант для SCHED_RR составляет 1 миллисекунду, и это важная деталь: гранулярность переключения фиксирована, а не произвольно мелкая. Если задача критична к задержкам порядка сотен микросекунд, SCHED_RR с его миллисекундным квантом может оказаться грубоватым инструментом, и тогда стоит рассмотреть SCHED_FIFO с явным sched_yield() в нужных точках кода.
Оба класса, SCHED_FIFO и SCHED_RR, используют одну фундаментальную структуру: 100 очередей выполнения, по одной на каждый приоритетный уровень от 1 до 99. Планировщик всегда выбирает первую непустую очередь с наивысшим номером. Никакой динамики, никакого учёта истории, только жёсткая иерархия.
SCHED_DEADLINE и концепция временного контракта с планировщиком
Если SCHED_FIFO и SCHED_RR отвечают на вопрос "кто важнее?", то SCHED_DEADLINE задаёт принципиально иной вопрос: "когда именно задача должна завершить порцию работы?". Это сдвиг от приоритетной модели к модели временных обязательств. Политика появилась в ядре Linux 3.14 в марте 2014 года и занимает высшую позицию среди всех пользовательских классов планирования.
Каждая задача описывается тремя параметрами:
- runtime (бюджет в наносекундах): сколько процессорного времени задача потребляет за каждый период
- period (период в наносекундах): с какой периодичностью задача активируется снова
- deadline (дедлайн в наносекундах): в течение какого времени от начала периода бюджет должен быть предоставлен
Ядро требует соблюдения: runtime <= deadline <= period. Это не просто правило форматирования аргументов, а условие темпоральной изоляции: задача, исчерпавшая бюджет до истечения дедлайна, принудительно приостанавливается до следующего периода.
Установить SCHED_DEADLINE через chrt:
# Задача получает 10 мс CPU каждые 100 мс, дедлайн 50 мс
# Все параметры в наносекундах
chrt --deadline \
--sched-runtime 10000000 \
--sched-deadline 50000000 \
--sched-period 100000000 \
--pid 0 472
Программная установка через sched_setattr(), который является единственным системным вызовом, поддерживающим SCHED_DEADLINE:
#include <linux/sched/types.h>
#include <sys/syscall.h>
#include <string.h>
#include <unistd.h>
struct sched_attr {
uint32_t size;
uint32_t sched_policy;
uint64_t sched_flags;
int32_t sched_nice;
uint32_t sched_priority;
uint64_t sched_runtime;
uint64_t sched_deadline;
uint64_t sched_period;
};
struct sched_attr attr;
memset(&attr, 0, sizeof(attr));
attr.size = sizeof(attr);
attr.sched_policy = SCHED_DEADLINE;
attr.sched_runtime = 10 * 1000 * 1000; /* 10 мс */
attr.sched_deadline = 50 * 1000 * 1000; /* 50 мс */
attr.sched_period = 100 * 1000 * 1000; /* 100 мс */
if (syscall(SYS_sched_setattr, 0, &attr, 0) != 0) {
perror("sched_setattr");
}
В основе SCHED_DEADLINE лежат два алгоритма. EDF (Earliest Deadline First) решает задачу выбора: из всех готовых к выполнению задач CPU получает та, чей дедлайн наступает раньше. CBS (Constant Bandwidth Server) решает задачу изоляции: если задача долго не активировалась и проснулась с просроченным дедлайном, CBS сбрасывает её дедлайн на текущее время + period, предотвращая несправедливое преимущество перед остальными задачами. Это и есть темпоральная изоляция на практике.
Подводные камни, которые проявляются в самый неподходящий момент
Три политики звучат стройно в документации, но на практике каждая припасла нетривиальный сюрприз. Самый коварный касается инверсии приоритетов в SCHED_FIFO: высокоприоритетный поток, ожидающий mutex, удерживаемый низкоприоритетным потоком, фактически простаивает, пока средние по приоритету задачи спокойно выполняются. Планировщик тут ни при чём, проблема в синхронизации. На ядрах с PREEMPT_RT она решается через priority inheritance в мьютексах, на стандартном ядре её нужно учитывать и проектировать вокруг неё.
Второй камень специфичен для SCHED_DEADLINE: admission control. Ядро считает суммарную утилизацию всех принятых deadline-задач и отклоняет новую заявку, если сумма runtime/period по всем задачам на данном CPU превышает лимит rt_bandwidth. Параметры этого лимита живут здесь:
# Посмотреть текущие параметры rt_bandwidth
cat /proc/sys/kernel/sched_rt_period_us
cat /proc/sys/kernel/sched_rt_runtime_us
# Разрешить реальновременным задачам использовать 100% CPU (осторожно!)
echo -1 > /proc/sys/kernel/sched_rt_runtime_us
Значение -1 во втором параметре снимает ограничение полностью, что допустимо только в системах с PREEMPT_RT, где системные потоки сами работают под реальновременными политиками и не вытесняются пользовательскими задачами. На обычном ядре это прямая дорога к зависанию системы.
Третья особенность касается многоядерных систем. SCHED_DEADLINE реализует глобальный EDF, позволяя задачам мигрировать между ядрами CPU. Для жёстких гарантий задачу лучше привязать к конкретному ядру:
# Привязать процесс к ядру CPU 2 и установить SCHED_FIFO
taskset -c 2 chrt -f 70 ./critical_task
На одноядерной конфигурации или при явной привязке через taskset гарантии SCHED_DEADLINE становятся жёсткими, а не статистическими.
Как выбрать подходящую политику под конкретную задачу
Многие разработчики по умолчанию тянутся к SCHED_FIFO как к "самому реальновременному" варианту. Это не всегда верно. Каждая политика закрывает свой класс задач, и правильный выбор определяет поведение системы не в нормальном режиме, а именно при пиковой нагрузке.
SCHED_FIFO подходит для единственной критической задачи, которая должна гарантированно отработать без каких-либо переключений. Обработчик прерываний реального времени, задача управления с жёстким требованием к латентности, критический участок пайплайна, где переключение контекста недопустимо.
SCHED_RR выбирают тогда, когда несколько параллельных задач одинаковой важности должны честно делить CPU. Многоканальная обработка аудио, параллельные потоки сбора телеметрии, ситуации, где монополизация одним потоком недопустима даже внутри реальновременного класса.
SCHED_DEADLINE оптимален для периодических задач с заранее известным бюджетом: видеокодек с фиксированным фреймрейтом, сетевой стек с гарантированной полосой, промышленный контроллер с циклическим временем обработки. Его главное преимущество над SCHED_FIFO в смешанной нагрузке состоит в темпоральной изоляции: перерасход бюджета одной задачи физически не может украсть время у другой.
Выбор политики это не технический нюанс, это архитектурное решение. Система, спроектированная с правильными классами планирования, держит заявленные задержки не "обычно" и не "в большинстве случаев", а при любой нагрузке. Именно эта разница отделяет систему реального времени от системы, которая "обычно работает быстро".