ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Что делает планировщик (scheduler) ядра: потоки, CPU и приоритеты

Автор: · Обновлено

Планировщик ядра выбирает, какой готовый к выполнению поток получит процессорное время. От его решений зависят отзывчивость приложений, распределение CPU между задачами и…

Планировщик ядра выбирает, какой готовый к выполнению поток получит процессорное время. От его решений зависят отзывчивость приложений, распределение CPU между задачами и задержка от пробуждения потока до начала работы.

В этом уроке разберём общую идею на примере Linux, прочитаем небольшое расписание и посмотрим на реальные задачи через ps и /proc. Достаточно знать, что такое PID и состояние процесса из [урока о процессах](/lessons/without-university/linux-developer-foundations/linux-developer-10). Команды рассчитаны на Linux с Bash, procps и GNU coreutils; sudo не нужен. Документация проверена 24 сентября 2026 года.

Какую задачу решает scheduler

Представьте, что одновременно работают редактор, компилятор и сервер. Часть их потоков вычисляет, часть ждёт диск, сеть, таймер или блокировку. Для каждого логического CPU ядру нужно выбрать поток, который действительно может продолжить выполнение. Если подходящих задач нет, CPU может выполнять idle-задачу и переходить в энергосберегающие состояния.

В Linux единица планирования — task, в обычном многопоточном приложении это отдельный поток. У процесса есть общие ресурсы, например адресное пространство, но разные его потоки могут отдельно ожидать и получать CPU. Один поток не исполняется одновременно на двух логических CPU; несколько потоков одного процесса могут работать параллельно. Логический CPU при этом не обязательно равен физическому ядру: процессор может поддерживать SMT.

Scheduler отвечает за выбор исполнителя и распределение процессорного времени. Cron и systemd timers отвечают на другой вопрос: когда запустить работу по времени. После запуска её потоки всё равно подчиняются планировщику ядра. Аналогично, амперсанд в shell отправляет команду в фон, но не резервирует для неё CPU.

Выполняется, готов или ждёт

Полезно разделять три состояния, даже если диагностический инструмент объединяет некоторые из них:

СостояниеЧто происходитМожет ли scheduler выбрать поток для работы?
Running — выполняетсяПоток уже исполняется на CPUОн может продолжить работу или быть вытеснен
Runnable — готовЕсть работа, но CPU пока занят другим потокомДа, когда это допускают политика и ограничения
Blocked / sleeping — ждётПродолжение зависит от события: данных, таймера, освобождения ресурсаПока нет; сначала нужно пробуждение

В ps буква R означает и running, и runnable. S — прерываемый сон, D — непрерываемый сон, часто связанный с I/O. T обозначает остановку, Z — уже завершившийся процесс, статус которого ещё не забрал родитель. По одной строке R нельзя узнать, исполнялся ли поток в этот момент или стоял в очереди. Состояния и поля ps.

Когда ожидаемое событие наступает, поток становится готовым к выполнению. Пробуждение не гарантирует немедленного запуска: могут быть другие претенденты, ограничения CPU или более приоритетная работа. Поэтому истечение sleep задаёт момент, раньше которого продолжать нельзя, но не точное время следующей инструкции.

Вытеснение и переключение контекста

Текущий поток может сам заблокироваться, например при ожидании данных. Тогда CPU освобождается для другой готовой задачи. Возможен и другой сценарий: поток ещё способен работать, но ядро решает отдать CPU конкуренту. Это вытеснение, или preemption. Поводом пересмотреть выбор может быть пробуждение задачи или событие таймера; конкретные правила зависят от политики планирования.

Если исполнителем становится другой поток, происходит переключение контекста: ядро сохраняет необходимое состояние исполнения прежнего потока и восстанавливает состояние следующего. Это включает регистры и состояние стека; при смене процесса может потребоваться смена адресного пространства. Вся память процесса при каждом переключении не копируется.

Сам переход из пользовательского режима в ядро ещё не означает переключение на другой поток. Системный вызов может закончиться, и та же задача продолжит работу. Прерывание тоже не обязано менять исполнителя. Переключения имеют цену: помимо работы ядра, возможны потери от менее удачного использования кэшей. Но их полное отсутствие не является целью — иначе длительное вычисление могло бы задержать интерактивную работу.

Прочитаем учебное расписание

Рассмотрим упрощённую модель round-robin на одном логическом CPU с квантом 2 мс. Это упражнение на состояния, а не обещание такого расписания в Linux. Переключениями и накладными расходами модели пренебрегаем. В момент 0 обе задачи готовы, первой выбирается A. Задаче A нужно 5 мс CPU. Задаче B нужен 1 мс CPU, затем 3 мс ожидания I/O, затем ещё 1 мс CPU.

ВремяКто занимает CPUЧто происходит с другой задачей
0–2 мсAB готова, ждёт своей очереди
2–3 мсB, затем блокируется до момента 6A снова готова
3–5 мсAB ждёт I/O и не участвует в выборе
5–6 мсA заканчивает оставшуюся работуB ещё ждёт; другой готовой задачи нет
6–7 мсB просыпается, получает CPU и заканчиваетA уже завершилась

За 7 мс CPU выполнил 5 мс работы A и 2 мс работы B. Ожидание I/O задачи B перекрылось с вычислением A. На границе 5 мс квант закончился, но переключаться не на кого: продолжила A. Этот пример объясняет, почему scheduler не просто «раздаёт каждому фиксированное время по кругу» независимо от состояния.

Задачу, занятую преимущественно вычислениями, называют CPU-bound. Если большую часть времени она ждёт ввод-вывод — I/O-bound. Для первой важна доступная доля CPU, для второй — в том числе задержка после пробуждения. Планировщик сочетает справедливость распределения, отзывчивость и эффективное использование процессора; одна метрика не описывает все цели.

Приоритет, nice и классы планирования

Для обычных задач часто используется политика SCHED_OTHER, также называемая SCHED_NORMAL в ядре. Значение nice находится в диапазоне от −20 до 19: меньшее число даёт больший относительный вес при конкуренции за CPU. Это не процент загрузки и не обещание завершиться раньше. При отсутствии конкуренции даже задача с nice 19 может использовать доступный CPU; группы задач и ограничения ресурсов также влияют на распределение.

Не смешивайте nice с приоритетами политик реального времени. SCHED_FIFO не использует циклический квант, SCHED_RR добавляет такой квант между готовыми потоками одинакового real-time приоритета, а SCHED_DEADLINE описывает требования через runtime, deadline и period и проверяет возможность принять задачу. Эти политики требуют отдельного проектирования; применять их к приложению только потому, что оно «важное», недостаточно. Политики Linux и nice.

CFS и EEVDF: почему важна версия ядра

В описаниях Linux часто встречается CFS — Completely Fair Scheduler. Он учитывает виртуальное время выполнения, чтобы делить CPU с учётом весов задач. Это не модель с одним неизменным квантом для всех процессов. Устройство CFS.

Начиная с Linux 6.6 механизм выбора задач обычного fair-класса развивается на основе EEVDF — Earliest Eligible Virtual Deadline First. Сначала определяется, какие задачи имеют право претендовать на CPU с учётом уже полученной доли, затем среди них выбирается ранний виртуальный deadline. Это помогает учитывать одновременно справедливость и задержки. Виртуальный deadline внутри EEVDF не означает гарантированный срок завершения пользовательского запроса и не равен политике SCHED_DEADLINE. Устройство EEVDF.

Не переносите устройство одной версии ядра на все Linux-системы. Для диагностики сначала установите версию и настройки системы; алгоритмы fair-класса также не описывают все остальные классы планирования.

Несколько CPU и ограничения выбора

На многопроцессорной системе работа распределяется по очередям логических CPU. Балансировка может перемещать задачи, но учитывает топологию и цену миграции: после переноса нужные данные могут отсутствовать в кэше нового CPU. Поэтому стратегия «постоянно переносить поток на любое свободное ядро» не обязательно выгодна. Домены планирования.

CPU affinity ограничивает набор CPU, на которых разрешено выполнять поток. Свободный процессор вне этого набора не помогает конкретной задаче. Отдельно могут действовать ограничения cgroups, например лимит процессорного времени контейнера. Affinity и квота — разные ограничения: список разрешённых CPU сам по себе не задаёт процент времени. CPU affinity.

Наблюдение без изменения настроек системы

Выполните в Bash:

uname -r
ps -L -p "$$" -o pid,tid,psr,stat,ni,cls,comm
grep -E '^(State|Threads|Cpus_allowed_list|voluntary_ctxt_switches|nonvoluntary_ctxt_switches):' "/proc/$$/status"

PID обозначает процесс, TID — конкретный поток. PSR показывает последний логический CPU, на котором исполнялась задача, а не её закрепление за процессором. NI — nice; CLS обозначает класс, например TS для SCHED_OTHER. Команда ps делает снимок, поэтому значения могут измениться сразу после чтения.

В /proc виден Cpus_allowed_list — разрешённый набор CPU. Счётчики voluntary_ctxt_switches и nonvoluntary_ctxt_switches отражают накопленные добровольные и недобровольные переключения, например при блокировке и вытеснении соответственно. Это не количество системных вызовов и не мгновенная нагрузка. Файл /proc/PID/status показывает счётчики главного потока; для остальных смотрите /proc/PID/task/TID/status. Поля status, Каталог task.

Теперь создадим только свой короткоживущий процесс. Вложенный Bash ограничивает действие trap этим примером:

bash <<'BASH'
nice -n 5 sleep 3 &
scheduler_pid=$!
trap 'kill "$scheduler_pid" 2>/dev/null || true; wait "$scheduler_pid" 2>/dev/null || true' EXIT
sleep 0.1
ps -p "$scheduler_pid" -o pid,ni,stat,etime,time,comm
grep -E '^(State|voluntary_ctxt_switches|nonvoluntary_ctxt_switches):' "/proc/$scheduler_pid/status"
wait "$scheduler_pid"
trap - EXIT
BASH

Обычно вы увидите sleep в состоянии S, небольшое прошедшее время ETIME и почти нулевое накопленное CPU-время TIME. Через несколько секунд команда закончится. Номер PID, точные счётчики и момент снимка заранее не фиксированы. В сильно загруженной среде процесс может успеть завершиться до чтения /proc: это гонка наблюдения, а не неисправность scheduler.

Опция nice -n 5 увеличивает унаследованное значение nice на 5 в допустимых пределах. Если исходное значение 0, в NI будет 5; при другом исходном значении результат может отличаться. Nice не меняет заданную длительность sleep: пока задача спит, CPU ей не нужен. После истечения таймера конкуренция может задержать получение CPU и фактическое завершение команды. Команда nice.

Ошибка и диагностика

Наблюдение или предположениеЧто проверить до вывода
«У процесса R, значит он прямо сейчас работает»R также включает очередь готовых задач; одного снимка недостаточно
«Понизил nice — процесс должен занимать строго 10% CPU»Nice задаёт относительный вес; нужны сведения о конкурентах, группах и квотах
«CPU свободен, но задача медленная»Может идти ожидание I/O, блокировки, пробуждения или ограничение affinity/cgroup
«Много context switches — scheduler сломан»Сравните изменения счётчиков за интервал с числом задач и характером ожиданий
«Любой syscall обязательно переключает поток»Переход режима исполнения и смена выполняемого потока — разные события

Начинайте с конкретного симптома: высокое время ответа, очередь готовых задач или большая доля времени ожидания. По одному числу в top нельзя обосновать настройку приоритета. Если приложение блокируется на внешнем сервисе, увеличение его веса CPU может вообще не затронуть причину задержки.

Практика

  1. Выполните оба примера и запишите NI текущего shell и дочернего sleep; для sleep также запишите состояние, ETIME и TIME. Объясните, почему ожидание может увеличивать прошедшее время почти без роста CPU-времени. Не требуйте конкретного числа переключений: оно зависит от выполнения.
  2. Вернитесь к расписанию и измените I/O-ожидание B с 3 до 5 мс. Укажите интервал простоя CPU и новый момент завершения всех задач.
  3. Рассмотрите сервер с четырьмя разрешёнными CPU и одним вычисляющим потоком. Объясните, почему scheduler не может автоматически превратить его в четыре независимых вычисления.

Разбор второго задания: B блокируется в момент 3 и становится готова в момент 8. A по-прежнему заканчивает в 6; с 6 до 8 обе задачи не готовы к выполнению — A завершена, B ждёт. В модели CPU простаивает, затем B работает с 8 до 9. В третьем задании параллелизм должен появиться в самой программе: поток исполнения нельзя просто одновременно продолжить с одной и той же инструкции на четырёх CPU.

Проверка понимания: данные уже пришли по сети, но обработчик начал исполняться позже. Достаточно ли этого, чтобы объявить сетевой запрос медленным? Нет: нужно различать время прихода события, пробуждение задачи и ожидание CPU после него.

Что важно запомнить

Частые вопросы

Планировщик выбирает процессы или потоки?

В Linux планируется отдельная task, для многопоточного приложения — поток. Поэтому один поток процесса может ждать сеть, а другой — исполняться. Общие ресурсы процесса не заставляют все его потоки одновременно получать или освобождать CPU.

Почему высокий приоритет не ускоряет любую программу?

Приоритет влияет на конкуренцию за процессорное время, но не устраняет медленный диск, ожидание сети или блокировку. Если CPU и так доступен, изменение nice может не дать выигрыша; сначала нужно определить, на что тратится время.

Всегда ли таймер заставляет переключиться на другой поток?

Нет. Он может стать поводом учесть время и пересмотреть выбор, но решение зависит от политики и наличия других готовых задач. При отсутствии подходящего конкурента текущий поток может продолжить выполнение.

Источники