Знакомая картина: в одном терминале идёт сборка с make -j8, в соседнем открылся браузер, и интерфейс начинает дёргаться. Совет "поднимите приоритет браузеру через nice" срабатывает через раз, потому что ядро давно делит процессорное время не между процессами, а между целыми группами. Дальше по порядку: автогруппы, политики SCHED_BATCH и SCHED_IDLE, контроллер cpu в cgroups v2, настройки systemd и команды taskset, nice, chrt, systemctl.
Деление процессорного времени между группами задач в ядре CFS
Основной планировщик Linux со времён ядра 2.6.23 называется CFS, Completely Fair Scheduler. Время в нём распределяется между группами задач: механизм group scheduling включается опцией CONFIG_FAIR_GROUP_SCHED, собранной практически во всех дистрибутивах. Сначала время делится между группами, потом внутри каждой между её процессами, поэтому nice имеет смысл только внутри одной группы.
Значения nice лежат от -20 до 19, чем меньше число, тем выше приоритет, и каждая ступень разницы даёт коэффициент 1.25: процесс с nice 10 получит в 1.25 раза больше времени, чем сосед с nice 11. Политики реального времени SCHED_FIFO и SCHED_RR работают со статическими приоритетами от 1 до 99 и всегда вытесняют обычные задачи, а появившийся в ядре 3.14 режим SCHED_DEADLINE добавляет дедлайны. Для фоновых нагрузок есть SCHED_BATCH с ядра 2.6.16, помечающий задачу как процессороёмкую, и SCHED_IDLE с ядра 2.6.23, которая выполняется ниже любого nice 19.
Числовой пример. Две группы делят загруженное ядро, вес первой 100, вес второй 900: пока обе просят время, первой достаётся десятая часть, второй девять десятых; когда первая спит, вторая получает всё до её пробуждения. У защиты от зависших RT-задач тоже есть числа: /proc/sys/kernel/sched_rt_period_us хранит период 1000000 микросекунд, а /proc/sys/kernel/sched_rt_runtime_us лимит 950000, то есть 5% времени всегда резервируется под обычные задачи.
Автогруппы ядра Linux и параметр sched_autogroup_enabled
Автогруппирование появилось в ядре 2.6.38 ради сборки ядра с make -j10, превращавшей рабочий стол в слайдшоу. Каждый новый сеанс, созданный вызовом setsid, то есть новое окно терминала или SSH-подключение, становится отдельной группой, а между сеансами время делится поровну, сколько бы претендентов ни сидело в каждом.
Числовая иллюстрация: в одной автогруппе десять процессороёмких процессов сборки, в другой один видеоплеер. С автогруппами плеер получает около 50% времени ядра, без них только около 9%, ведь формально у сборки в десять раз больше кандидатов. Отсюда и казус: перечислять процессы сборки через nice не имеет смысла, они уже сгруппированы сами.
Переключатель один: /proc/sys/kernel/sched_autogroup_enabled, единица стоит по умолчанию, ноль выключает механизм для новых групп:
cat /proc/sys/kernel/sched_autogroup_enabled
sudo sysctl -w kernel.sched_autogroup_enabled=0
Чтобы выбор пережил перезагрузку, строку kernel.sched_autogroup_enabled = 0 кладут в файл /etc/sysctl.d/99-autogroup.conf; при загрузке тот же эффект даёт параметр ядра noautogroup.
Коварное следствие автогрупп: nice работает только внутри одного сеанса, поэтому renice -n -10 -p 4123 в одном окне терминала не даст процессу ничего против процесса в соседнем. Лечится записью echo 10 > /proc/self/autogroup, опускающей приоритет всех задач текущего сеанса. Файл читается как /autogroup-1 nice 0, диапазон записи от -20 до 19, выход за границы даёт ошибку EINVAL. Автогруппы не трогают реальное время, а для процесса в cgroup с контроллером cpu перестают действовать вовсе.
Приоритеты отдельных процессов командами taskset, nice и chrt
Привязка к ядрам делается через taskset: taskset -c 2-5 make -j4 запустит сборку на ядрах со второго по пятый, taskset -a -pc 0,1 4123 переназначит маску работающему процессу, причём ключ -a распространит её на все потоки. Шестнадцатеричную маску утилита тоже понимает: taskset -p 0x3 4123.
Приоритет внутри группы задаёт связка nice и renice. Запуск с минимальным приоритетом выглядит как nice -n 19 rsync -a /data /mnt/backup/, правка работающего процесса как renice -n 5 -p 4123. Отрицательные nice, скажем renice -n -5 -p 4123, требуют прав суперпользователя, за обратное направление отвечает лимит RLIMIT_NICE.
Политики переключает chrt. chrt -b 0 ./indexer переводит задачу в SCHED_BATCH, chrt -i 0 ./backup в SCHED_IDLE, chrt -f 50 ./sensor в SCHED_FIFO с приоритетом 50, chrt -r 20 то же для SCHED_RR. chrt -m печатает диапазоны приоритетов, chrt -p 4123 показывает текущие, а chrt -i -p 0 4123 меняет политику у работающего процесса. Для SCHED_FIFO и SCHED_RR приоритет обязателен и лежит от 1 до 99, для SCHED_BATCH и SCHED_IDLE он всегда ноль. С ядра 2.6.12 непривилегированный пользователь может переходить на RT-политики в пределах лимита RLIMIT_RTPRIO, а с 2.6.39 свободно выходить из SCHED_IDLE.
Контроллер cpu в cgroups v2 и файлы в /sys/fs/cgroup
Когда групп много и конфигурация должна переживать перезапуски, наступает время cgroups. Современные дистрибутивы монтируют единую иерархию cgroup v2 в /sys/fs/cgroup, проверка через stat -fc %T /sys/fs/cgroup/: ответ cgroup2fs означает вторую версию, tmpfs гибридную схему. Включение контроллера и создание рабочей группы выглядит так:
- убедиться, что контроллер виден, командой cat /sys/fs/cgroup/cgroup.controllers;
- включить его для дочерних групп командой echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control;
- создать каталог группы командой mkdir /sys/fs/cgroup/backup;
- задать вес командой echo 200 > /sys/fs/cgroup/backup/cpu.weight;
- ограничить потолок командой echo "150000 100000" > /sys/fs/cgroup/backup/cpu.max;
- переместить процесс командой echo 4123 > /sys/fs/cgroup/backup/cgroup.procs и убедиться через cat /proc/4123/cgroup, что появилась строка 0::/backup.
Квота 150000 при периоде 100000 микросекунд означает полтора ядра. Формат cpu.max парный, "MAX PERIOD", слово max снимает потолок, запись одного числа меняет только квоту, период по умолчанию 100000 микросекунд при допустимом диапазоне от 1000 до 1000000.
Ключевые файлы контроллера. cpu.weight принимает вес от 1 до 10000, по умолчанию 100, и работает пропорционально. cpu.weight.nice это вход через привычные nice от -20 до 19, читается приближённо. cpu.max.burst, доступный с ядра 5.14, позволяет группе догонять накопленное простое: при квоте 200000 на период 100000 запись 300000 в cpu.max.burst даёт в пике до 500000 микросекунд за период. cpu.idle=1 превращает группу в аналог SCHED_IDLE: приоритеты потоков сохраняются, но против соседей группа стоит на последнем месте, и её cpu.weight покажет ноль.
Статистика живёт в cpu.stat: usage_usec, user_usec и system_usec доступны всегда, при включённом контроллере добавляются nr_periods, nr_throttled, throttled_usec, nr_bursts и burst_usec. Рост nr_throttled при заметном throttled_usec сигнализирует о заниженной квоте. Латентность показывает cpu.pressure в формате avg10, avg60, avg300, общесистемный аналог лежит в /proc/pressure/cpu с ядра 4.20.
Две ловушки. Правило no internal process constraint запрещает включать контроллер в непустой группе: процессы сначала переносят в дочерние cgroups, потом включают +cpu у родителя. Вторая касается реального времени: контроллер cpu не ограничивает RT-задачи, а на ядрах с CONFIG_RT_GROUP_SCHED его нельзя включить, пока RT-процессы вне корневой группы. Наследование идёт строго сверху вниз, перенос процесса одним PID за запись, и все потоки переезжают вместе.
Группы ресурсов в systemd с CPUWeight, CPUQuota и slice-юнитами
systemd оборачивает контроллер cpu в настройки юнитов. CPUWeight с версии 232 соответствует cpu.weight, диапазон 1-10000, по умолчанию 100, значение idle включает аналог SCHED_IDLE для группы. CPUQuota с версии 213 пишет в cpu.max и считается от одного ядра: CPUQuota=20% это пятая часть ядра, 150% полтора ядра. CPUQuotaPeriodSec из версии 242 задаёт период, по умолчанию 100ms, ядро допускает от 1ms до 1000ms. AllowedCPUs из версии 244 работает через контроллер cpuset на единой иерархии, пример AllowedCPUs=4-7.
Правки на ходу делает systemctl set-property: команда systemctl set-property backup.slice CPUWeight=2000 создаёт drop-in в /etc/systemd/system.control/backup.slice.d/, а ключ --runtime отправляет его в /run/systemd/system.control/ до ближайшей перезагрузки. Разовый вариант без файлов: systemd-run --scope -p CPUQuota=30% -p CPUWeight=300 ./migrate.sh запустит скрипт в собственной группе.
Постоянную структуру описывают slice-юниты. Файл /etc/systemd/system/build.slice:
[Slice]
CPUWeight=400
CPUQuota=150%
AllowedCPUs=4-7
После systemctl daemon-reload любая служба со строкой Slice=build.slice в секции [Service] попадает в эту группу целиком. По умолчанию службы сходятся в system.slice, пользовательские задачи в user.slice и вложенные user-1000.slice, app.slice и session.slice. Пропорции легко посчитать: служба с CPUWeight=20 против соседнего slice со значением по умолчанию 100 получают доли 1:6 и 5:6. В [Service] доступны ещё Nice= и CPUSchedulingPolicy=batch или idle, поэтому точечная настройка каждого потока через chrt нужна не всегда.
Наблюдать в динамике умеют systemd-cgls и systemd-cgtop.
Практические сценарии для сборки, резервных копий и многопользовательских серверов
Рабочий стол под нагрузкой. Сборка с make -j12 против браузера и плеера. Минимальное вмешательство: запустить сборку в отдельном окне терминала, получив отдельную автогруппу. Гарантия выглядит иначе: systemd-run --user --scope -p CPUQuota=300% make -j12 оставит сборке до трёх ядер из двенадцати.
Резервное копирование, не мешающее работе. nice -n 19 rsync -a --delete /data /mnt/backup/ или chrt -i 0 rsync -a --delete /data /mnt/backup/ почти не ощущается интерактивом, а на простаивающей машине копирование идёт на полной скорости. На уровне групп ту же роль играют cpu.idle=1 или CPUWeight=idle.
Сервер на несколько клиентов. Каждому свой slice с весом: 1000, 500 и 100 делят спорные ядра в пропорции 10:5:1. Сверх того каждой задаётся квота CPUQuota=200% как страховка от зависшей задачи клиента, а AllowedCPUs прибивает чувствительные к кешу нагрузки к нужным NUMA-узлам. Период квоты резать до единиц миллисекунд не стоит: мелкая нарезка добавляет накладные расходы и ухудшает латентность.
Изолированный рантайм для тестов. Сборка в taskset -c 0-3, тесты в taskset -c 4-7, квоты по 200% с периодом 50ms: ядра не перетаптывают кеши друг друга, а надзорная служба всегда доступна.
Диагностика групп планирования и ошибки, которые дорогого стоят
Классическая ошибка: администратор меняет nice, не видит эффекта против соседнего окна терминала и решает, что nice сломан. На деле действие упёрлось в границу автогруппы, решение в правке /proc/self/autogroup или переносе нагрузок в cgroups. Вторая, попытка включить +cpu в занятой группе из середины дерева, ядро откажет, пока у группы есть собственные процессы. Третья, путаница с процентами: 100% квоты это всегда одно ядро, на машине с 32 ядрами такая квота оставляет группе около 3% суммарной мощности. Четвёртая, раздача RT-приоритетов выше 90 без проверки лимитов: резерв sched_rt_runtime_us спасает систему в целом, но один жадный поток SCHED_FIFO испортит латентность соседям.
Диагностический набор короткий. cat /proc/self/autogroup показывает, в какой автогруппе живёт оболочка. cat /sys/fs/cgroup/backup/cpu.stat отвечает, как часто группа упирается в потолок, по полям nr_periods и nr_throttled. cat /proc/pressure/cpu говорит о нехватке времени по значениям avg10 и avg60. systemd-cgtop находит группу-обжору за секунды, ps -eo pid,cls,ni,comm,psr проверяет класс, nice и ядро, а lscpu показывает число ядер и структуру NUMA для масок taskset и значений AllowedCPUs.
Слоистость групп планирования поначалу путает, но именно она делает систему предсказуемой. Разумная последовательность такая: автогруппы и nice для повседневного интерактива, SCHED_BATCH и SCHED_IDLE для фоновых задач, контроллер cpu в cgroups v2 там, где нужны гарантии, systemd с CPUWeight и CPUQuota там, где конфигурация обязана переживать перезагрузку. Когда все слои известны, планировщик перестаёт быть чёрным ящиком: любое дёргание интерфейса объясняется конкретной группой, весом и квотой, и меняется одной командой.