Real-time классы Linux — SCHED_FIFO, SCHED_RR, SCHED_DEADLINE
«Real-time» — понятие, которое люди понимают как «быстро». Это неверно. В системном программировании real-time означает не быстро, а предсказуемо — с гарантированным верхним пределом задержки. ABS в машине должен срабатывать за 2 мс не потому, что 2 мс быстро, а потому, что гарантировано не позже — иначе колесо заблокируется, машина юзит.
В Linux есть три real-time scheduling класса: SCHED_FIFO, SCHED_RR, SCHED_DEADLINE. Они дают приложениям приоритет над обычными процессами, но требуют осторожности: SCHED_FIFO + бесконечный цикл = повисшая система. В этом уроке — обзор каждого класса, типичные ловушки, и почему для junior data engineer real-time почти никогда не нужен.
Hard real-time vs soft real-time
Сначала важное концептуальное различие.
Без специальных мер Linux может иметь latency-spikes до нескольких миллисекунд (interrupts, kernel locks, page faults). Для hard real-time это много. Соответственно:
- Hard real-time — специализированные ОС (FreeRTOS, VxWorks, RTEMS, QNX) или Linux с PREEMPT_RT patch.
- Soft real-time — стандартный Linux с RT scheduling классами.
Для junior data engineer почти всегда хватает обычного SCHED_OTHER (CFS/EEVDF). RT-классы нужны, если вы пишете audio framework, kernel video driver, или low-latency trading.
SCHED_FIFO — run until block
SCHED_FIFO — классический RT класс. Простая семантика: процесс работает, пока:
- Сам не заблокируется (sleep, read, mutex).
- Не появится готовый процесс с более высоким приоритетом.
- Сам не вызовет sched_yield.
Никаких time slices. Процесс не вытесняется по таймеру. Это и плюс (предсказуемо), и минус (бесконечный цикл = система виснет).
Защита от RT runaway:
# В sysctl есть параметры:
sysctl kernel.sched_rt_period_us
# 1000000 (1 second)
sysctl kernel.sched_rt_runtime_us
# 950000 (0.95 second)
# Это означает: за каждую секунду RT процессы (FIFO+RR) получат не больше 0.95 сек CPU.
# Если съели всё -- 50 мс отдадут обычным процессам. Это спасает от полного зависания.
# Запуск процесса в SCHED_FIFO (требует root):
sudo chrt -f 50 ./my-rt-program
# 50 -- RT priority (1-99)
# ВНИМАНИЕ: не запускайте CPU-bound бесконечный цикл!
# Test первым в виртуалке или с тестовой задачей
SCHED_RR — FIFO with time slice
SCHED_RR — то же самое, что SCHED_FIFO, но с time slice. Среди равноприоритетных RR-процессов кто-то отрабатывает свой slice и переходит в конец очереди равноприоритетных.
Time slice для RR по умолчанию 100 мс, но можно поменять:
sysctl kernel.sched_rr_timeslice_ms
# 100
# Можно изменить:
sudo sysctl kernel.sched_rr_timeslice_ms=50
# Запуск в SCHED_RR:
sudo chrt -r 50 ./my-program
# Посмотреть текущий time slice процесса:
chrt -p 1234
# scheduling priority X
# (для RR можно увидеть slice через /proc/PID/sched -> policy = 2 = RR)
Когда выбирать FIFO vs RR:
- FIFO — если у вас один RT процесс на priority level. Никакого round-robin не нужно. Простая семантика.
- RR — если несколько процессов одного приоритета и хотите fairness между ними.
SCHED_DEADLINE — современный подход к RT
SCHED_DEADLINE появился в Linux 3.14 (2014). Совсем другой подход: вместо приоритетов задаём (runtime, deadline, period):
Главное преимущество DEADLINE:
- Математически гарантированно, что задача успеет в deadline (если admission проверка прошла).
- Admission control — система отклонит задачу, если её добавление сделает другие не выполнимыми. Это уникально для DEADLINE.
- Не требует ручной настройки priority — задаёте через бизнес-параметры.
# SCHED_DEADLINE не поддерживается чрезвычайно простой chrt, нужны более низкоуровневые syscalls
# Документация: man sched_setattr
# Пример из ядра: tools/testing/selftests/sched/...
# В Python нужен ctypes для вызова sched_setattr:
# Полный пример выходит за рамки урока -- DEADLINE обычно используют в специализированных приложениях
Когда RT нужен в data engineering
Честный ответ: почти никогда.
Spark task, Flink job, Kafka consumer, Postgres query — ни один из этих не требует RT. Они выиграют от больших cores и быстрого I/O, не от RT scheduling. RT scheduling даёт низкую максимальную задержку, не высокую throughput.
cgroups: контейнерный способ ограничить CPU-ресурсыАнти-паттерн: «давайте сделаем все наши ETL процессы SCHED_FIFO, чтобы быстрее работали». Это даст:
- Не быстрее (наоборот — меньше преоптимизаций kernel).
- Риск powerlock всей системы.
- Сложности с другими процессами (ssh не достучаться, мониторинг не работает).
Ловушки real-time
Ловушка 1: priority inversion (уже разбирали в модуле 4)
Real-time процесс ждёт mutex, который держит обычный процесс. Решение: PTHREAD_PRIO_INHERIT.
Ловушка 2: page faults
RT процесс делает первый доступ к странице — page fault, kernel загружает страницу с диска — latency spike.
Решение: mlock() — pin страницы в RAM, или mlockall(MCL_CURRENT | MCL_FUTURE) для всей программы.
#include <sys/mman.h>
// В начале программы:
mlockall(MCL_CURRENT | MCL_FUTURE);
// Все текущие и будущие страницы будут pinned в RAM
Ловушка 3: kernel preemption
Даже обычный Linux может задержать RT процесс на несколько мс из-за non-preemptible kernel sections. Решение — PREEMPT_RT patch.
# Проверить preemption mode kernel:
cat /sys/kernel/realtime
# 0 -- обычный kernel
# 1 -- PREEMPT_RT (полностью preemptible)
# Или:
uname -v | grep -i preempt
Ловушка 4: bandwidth throttling
kernel.sched_rt_runtime_us ограничивает RT процессы до 95% CPU. Если RT процесс пытается забрать больше — его принудительно отключат на 50 мс. Это часто неожиданная задержка.
# Отключить throttling (опасно!):
sudo sysctl kernel.sched_rt_runtime_us=-1
# RT процессы могут забрать 100% CPU -- если зациклятся, ssh потеряете
Ловушка 5: CPU isolation
Для серьёзного RT нужно изолировать CPU от обычных задач:
# В /etc/default/grub:
# GRUB_CMDLINE_LINUX="isolcpus=4,5,6,7"
# После reboot CPU 4-7 не используются для обычных процессов
# Можно прибить RT процессы к ним через taskset
Это уже advanced настройка, обычному DE не нужна.
Реальный пример: Postgres backend и RT
Возьмём ситуацию: «наш Postgres медленный, давайте сделаем его SCHED_FIFO». Что будет?
- Postgres backend — это не один процесс, а десятки (по одному на коннект клиента). Сделать все RT — значит, ssh может перестать работать.
- WAL writer — да, мог бы получить выгоду от стабильной latency. Но он I/O-bound, RT не поможет ускорить fsync.
- Autovacuum — нет, это batch-задача, ей RT противопоказан.
- Background writer — то же, batch.
Правильный подход для Postgres — настроить cgroups (CPUWeight для приоритизации), правильный I/O scheduler (bfq или mq-deadline), shared_buffers + работа с памятью. Не RT.
Попробуй сам
# 1. Проверить текущие RT параметры:
sysctl kernel.sched_rt_period_us
sysctl kernel.sched_rt_runtime_us
sysctl kernel.sched_rr_timeslice_ms
# 2. Найти RT процессы в системе:
ps -eo pid,cls,rtprio,comm | awk '$2 ~ /FF|RR|DL/' | head
# CLS=FF -- SCHED_FIFO
# CLS=RR -- SCHED_RR
# CLS=DL -- SCHED_DEADLINE
# 3. Посмотреть latency-spikes -- классический cyclictest:
# (нужно установить rt-tests package)
# sudo cyclictest -t 4 -p 99 -i 1000 -l 10000
# Покажет min/avg/max latency между активациями таймера
# Без RT: max может быть 1000-10000 мкс
# С RT + isolcpus + mlockall: max может быть 50-100 мкс
# Hard RT (PREEMPT_RT): max <30 мкс
# 4. Безопасно поэкспериментировать с FIFO:
# Запускаем процесс, который сам sleep'ит -- не зависнет
sudo chrt -f 50 sleep 10 &
PID=$!
ps -p $PID -o pid,cls,rtprio,pri
# CLS=FF, RTPRIO=50
chrt -p $PID
# scheduling policy: SCHED_FIFO
# scheduling priority: 50
wait $PID
# 5. Посмотреть, что top показывает для RT процессов:
sudo chrt -f 80 sleep 100 &
PID=$!
top -p $PID
# Колонка PR покажет 'rt' или -101 (для prio 99 -> -100, для prio 80 -> -81)
# NI пустое (нет nice для RT)
kill $PID
# 6. CPU isolation проверить:
cat /sys/devices/system/cpu/isolated
# Если пусто -- ничего не изолировано. Если есть CPU -- isolcpus в kernel cmdline