Операционная система постоянно прерывает работу процессора для переключения задач, обновления статистики и обслуживания внутренних механизмов. Традиционный подход генерировал таймерные прерывания с фиксированной частотой 100-1000 раз в секунду на каждом ядре, независимо от того, занят процессор или простаивает. Linux kernel NOHZ кардинально меняет эту модель, отключая периодический tick там, где он не нужен. Три режима определяют поведение системы: CONFIG_NO_HZ_IDLE для энергосбережения на простаивающих ядрах, CONFIG_NO_HZ_FULL для изоляции вычислительных нагрузок и набор параметров изоляции CPU для real-time приложений.

Эволюция от периодических прерываний к динамическим

До версии 2.6.21 ядро Linux генерировало таймерные прерывания на каждом процессоре с частотой, определяемой CONFIG_HZ. Значение 1000 Hz означало 1000 прерываний в секунду на каждое ядро. Система с 96 ядрами обрабатывала 96000 прерываний каждую секунду, даже если большинство процессоров простаивали.

Проверка текущей частоты таймера:

grep "CONFIG_HZ=" /boot/config-$(uname -r)

Типичные значения CONFIG_HZ=250 для серверов, CONFIG_HZ=1000 для desktop систем. Каждое прерывание пробуждает процессор из энергосберегающего состояния, потребляет CPU cycles на обработку scheduler logic и препятствует входу в глубокие C-states.

Батарейное устройство с CONFIG_HZ_PERIODIC=y разряжается в 2-3 раза быстрее по сравнению с CONFIG_NO_HZ_IDLE=y. Мейнфрейм с 1500 виртуальными машинами может тратить половину процессорного времени на обслуживание ненужных прерываний таймера.

Режим dyntick-idle появился как решение. Вместо фиксированной частоты ядро программирует следующее прерывание таймера на момент, когда действительно требуется обработка. Если у задачи осталось 18 миллисекунд timeslice, таймер устанавливается через 18 миллисекунд. Если процессор полностью простаивает, таймер не устанавливается вообще.

Конфигурация ядра для dyntick-idle:

grep "CONFIG_NO_HZ_IDLE" /boot/config-$(uname -r)

Вывод CONFIG_NO_HZ_IDLE=y означает, что режим включен. Проверка активации через boot параметры:

cat /proc/cmdline

По умолчанию CONFIG_NO_HZ_IDLE ядра загружаются с nohz=on. Принудительное отключение:

# В /etc/default/grub добавить
GRUB_CMDLINE_LINUX="nohz=off"

# Применить изменения
sudo update-grub
sudo reboot

Режим не бесплатен. Увеличивается количество инструкций на пути к idle loop и обратно. На многих архитектурах возрастает число дорогостоящих операций перепрограммирования часов. Системы с агрессивными требованиями к real-time latency иногда используют CONFIG_HZ_PERIODIC=y, чтобы избежать деградации from-idle transition latencies.

Full dynticks для изоляции вычислительных нагрузок

CONFIG_NO_HZ_FULL расширяет концепцию tickless за пределы idle состояния. Если на процессоре выполняется ровно одна runnable задача, scheduler-clock прерывания подавляются. Процессор становится adaptive-ticks CPU, работающим без регулярных прерываний.

Проверка поддержки в ядре:

grep "CONFIG_NO_HZ_FULL" /boot/config-$(uname -r)

Активация через boot параметр nohz_full:

# В /etc/default/grub
GRUB_CMDLINE_LINUX="nohz_full=1,6-8"

Параметр указывает, что CPU 1, 6, 7 и 8 будут adaptive-ticks процессорами. Синтаксис поддерживает диапазоны и списки через запятую. Специальное значение "all" выбирает все процессоры кроме boot CPU:

GRUB_CMDLINE_LINUX="nohz_full=all"

Критическое ограничение: нельзя пометить все процессоры как adaptive-tick. Минимум один non-adaptive-tick CPU должен оставаться online для timekeeping задач. Это гарантирует, что системные вызовы типа gettimeofday() возвращают корректные значения на adaptive-tick процессорах.

Boot CPU автоматически исключается из nohz_full mask. Если указать nohz_full=0,1-7 на 8-core системе, CPU 0 будет удален из маски при загрузке, и сообщение об ошибке появится в dmesg. Система должна иметь минимум два процессора для работы CONFIG_NO_HZ_FULL.

Проверка активных настроек после загрузки:

cat /sys/devices/system/cpu/nohz_full

Вывод показывает список изолированных процессоров. Если параметр не был установлен, файл будет пустым.

Мониторинг количества runnable задач на процессор:

cat /proc/sched_debug | grep "nr_running"

Full dynticks работает только когда nr_running=1 для процессора. Две и более задачи на одном CPU отключают режим, возвращая периодические прерывания.

Adaptive-ticks режим критичен для real-time приложений. Максимальная latency улучшается на величину, равную продолжительности scheduling-clock прерывания. Для HPC workloads с короткими итерациями задержка одного CPU множится на количество процессоров минус один, так как остальные ждут отстающего.

Условие single runnable task означает ограничения. Процессор может запускать kernel threads, которые учитываются в nr_running. Migration threads, ksoftirqd, kworker могут нарушить условие. Изоляция требует дополнительной конфигурации kernel threads.

RCU callbacks и их влияние на tickless режим

Read-Copy-Update механизм использует callback для отложенного освобождения памяти. Если процессор имеет pending RCU callbacks, он не может войти в dyntick-idle или adaptive-tick режим. Offloading RCU callbacks решает проблему.

Конфигурация ядра:

grep "CONFIG_RCU_NOCB_CPU" /boot/config-$(uname -r)

CONFIG_RCU_NOCB_CPU=y позволяет выносить RCU callbacks с указанных процессоров. Boot параметр rcu_nocbs указывает список:

GRUB_CMDLINE_LINUX="rcu_nocbs=1,3-5"

Процессоры 1, 3, 4, 5 никогда не будут ставить RCU callbacks в очередь. Callbacks обрабатываются rcuo kernel threads, выполняющимися на non-isolated процессорах.

Начиная с nohz_full, параметр rcu_nocbs устанавливается автоматически для тех же процессоров. Явное указание не требуется:

GRUB_CMDLINE_LINUX="nohz_full=4-7"
# rcu_nocbs=4-7 добавляется автоматически

Проверка активных rcuo threads:

ps aux | grep rcuo

Вывод показывает rcuo/N threads для каждого offloaded процессора. Pinning этих threads на housekeeping CPUs управляется вручную через taskset:

taskset -p -c 0 $(pgrep rcuo/4)

Команда привязывает rcuo thread для CPU 4 к процессору 0. Housekeeping процессоры могут терпеть jitter от RCU processing без влияния на isolated нагрузку.

Параметр rcu_nocb_poll активирует polling вместо wakeup через прерывания:

GRUB_CMDLINE_LINUX="nohz_full=4-7 rcu_nocb_poll"

Rcuo threads постоянно проверяют наличие callbacks вместо ожидания пробуждения от isolated CPU. Это уменьшает jitter на изолированных процессорах за счет увеличения нагрузки на housekeeping CPUs.

Мониторинг RCU через:

cat /sys/kernel/debug/rcu/rcu_data

Вывод содержит статистику по каждому CPU, включая количество callbacks, quiescent states и grace periods.

Изоляция процессоров от scheduler

Параметр isolcpus удаляет указанные процессоры из scheduler load balancing. Задачи по умолчанию не планируются на isolated CPUs, если явно не привязаны через CPU affinity.

Базовая изоляция:

GRUB_CMDLINE_LINUX="isolcpus=1-7"

Процессоры 1-7 скрыты от scheduler. Системные процессы не мигрируют туда автоматически. Проверка после загрузки:

cat /proc/cmdline | grep isolcpus

Современные версии ядра (6.6+) требуют указания флагов изоляции:

GRUB_CMDLINE_LINUX="isolcpus=managed_irq,domain,1-7"

Флаг managed_irq изолирует от managed interrupts. Флаг domain изолирует от SMP balancing и scheduling algorithms. Без явного указания флагов изоляция может работать не полностью.

Запуск задачи на isolated CPU через taskset:

taskset -c 4 ./compute_intensive_app

Приложение выполняется на CPU 4, не конкурируя с другими процессами. Все threads приложения по умолчанию наследуют affinity, но можно управлять индивидуально:

taskset -p -c 5,6 <pid>

Альтернативный подход через cpusets позволяет runtime изменения:

# Создание cpuset для изоляции
mkdir /sys/fs/cgroup/cpuset/isolation
echo 4-7 > /sys/fs/cgroup/cpuset/isolation/cpus
echo 0 > /sys/fs/cgroup/cpuset/isolation/mems
echo 0 > /sys/fs/cgroup/cpuset/isolation/sched_load_balance

# Создание cpuset для housekeeping
mkdir /sys/fs/cgroup/cpuset/housekeeping
echo 0-3 > /sys/fs/cgroup/cpuset/housekeeping/cpus
echo 0 > /sys/fs/cgroup/cpuset/housekeeping/mems

# Перемещение задачи в isolation
echo <pid> > /sys/fs/cgroup/cpuset/isolation/tasks

Cpusets предпочтительнее isolcpus для production систем, так как конфигурация изменяется без перезагрузки. Isolcpus считается deprecated в некоторых сценариях, но остается полезным для embedded систем без cgroups support.

Управление прерываниями и kernel threads

Hardware interrupts могут нарушать изоляцию. Перенаправление IRQ на housekeeping CPUs через irqaffinity:

GRUB_CMDLINE_LINUX="irqaffinity=0-3"

Все прерывания по умолчанию обрабатываются на CPU 0-3. Isolated процессоры 4-7 освобождаются от IRQ handling. Проверка текущей IRQ affinity:

cat /proc/irq/default_smp_affinity

Вывод в hexadecimal формате представляет binary CPU mask. Значение 0x0f соответствует CPU 0-3.

Индивидуальное управление конкретным IRQ:

echo 1 > /proc/irq/60/smp_affinity_list

IRQ 60 привязывается к CPU 1. Список всех IRQ и их affinity:

for i in /proc/irq/*/smp_affinity_list; do
    echo "$i: $(cat $i)"
done

Kernel threads могут выполняться на isolated CPUs. Параметр kthread_cpus ограничивает их размещение:

GRUB_CMDLINE_LINUX="kthread_cpus=0-3"

Kernel threads создаются только на CPU 0-3. Проверка существующих kernel threads:

ps -eLo psr,comm | grep -E "^\s+[0-9]+ \[.*\]"

Вывод показывает, на каком процессоре выполняется каждый kernel thread. После применения kthread_cpus=0-3 все должны находиться на первых четырех процессорах.

Workqueues требуют отдельной обработки:

for i in /sys/devices/virtual/workqueue/*/cpumask; do
    echo 1 > $i
done

Команда привязывает все workqueues к CPU 0. Cpumask 0x1 в hex представляет binary 0001, то есть CPU 0.

Инструмент irqbalance автоматизирует распределение прерываний:

systemctl start irqbalance

По умолчанию irqbalance уважает isolcpus параметр и не размещает IRQ на изолированных процессорах. Дополнительный контроль через environment variable:

IRQBALANCE_BANNED_CPUS=f0
systemctl restart irqbalance

Hex значение f0 соответствует binary 11110000, банируя CPU 4-7.

Дополнительные параметры для минимизации jitter

Timer tick полностью не исчезает даже при nohz_full. Процессы вроде load balancing, CFS vruntime calculation требуют периодических проверок. Tick появляется примерно раз в секунду на isolated CPUs.

Мониторинг частоты прерываний:

watch -n1 'cat /proc/interrupts | grep -E "LOC|RES"'

LOC показывает local timer interrupts, RES показывает rescheduling interrupts. Isolated CPU должен показывать минимальные значения по сравнению с housekeeping CPUs.

Параметр skew_tick смещает RCU таймеры, чтобы они не срабатывали одновременно:

GRUB_CMDLINE_LINUX="skew_tick=1"

Предотвращение watchdogs от генерации NMI:

GRUB_CMDLINE_LINUX="nmi_watchdog=0 nowatchdog nosoftlockup"

Watchdog механизмы проверяют зависание CPU через периодические прерывания. Отключение снижает jitter, но удаляет защиту от lockup ситуаций.

Управление C-states для минимизации wake-up latency:

GRUB_CMDLINE_LINUX="intel_idle.max_cstate=1"

Процессор не входит в C-states глубже C1. Это увеличивает энергопотребление, но гарантирует быстрое пробуждение. Альтернатива через idle=poll:

GRUB_CMDLINE_LINUX="idle=poll"

Процессор постоянно busy-loops в idle вместо halt инструкции. Потребление энергии максимально, wake-up latency минимальна. Перегрев может вызвать thermal throttling, деградирующий производительность хуже, чем dyntick-idle.

Отключение CPU frequency scaling:

GRUB_CMDLINE_LINUX="intel_pstate=disable cpufreq.off=1"

Процессор работает на фиксированной частоте без динамических изменений. Предсказуемая производительность для latency-sensitive приложений.

Отключение hyperthreading для устранения контекции на ресурсах процессора:

GRUB_CMDLINE_LINUX="nosmt"

Каждый physical core получает эксклюзивные ресурсы. IPC увеличивается, но общий throughput снижается.

Практическая конфигурация для real-time систем

Сценарий 1: 8-core система для latency-critical приложения. CPU 0-3 для housekeeping, CPU 4-7 изолированы:

# /etc/default/grub
GRUB_CMDLINE_LINUX="isolcpus=managed_irq,domain,4-7 nohz_full=4-7 rcu_nocbs=4-7 rcu_nocb_poll kthread_cpus=0-3 irqaffinity=0-3 nmi_watchdog=0 nosoftlockup intel_idle.max_cstate=1"

# Применить
sudo update-grub
sudo reboot

Проверка после загрузки:

cat /proc/cmdline
cat /sys/devices/system/cpu/nohz_full
cat /proc/interrupts | grep -E "CPU4|CPU5|CPU6|CPU7"

Isolated CPUs должны показывать минимальное количество прерываний. Запуск приложения:

taskset -c 4 chrt -f 99 ./realtime_app

Команда chrt устанавливает SCHED_FIFO политику с приоритетом 99. Highest priority гарантирует, что задача вытесняет любые другие.

Сценарий 2: 96-core сервер для HPC workload. CPU 0-47 для системы, CPU 48-95 для вычислений:

GRUB_CMDLINE_LINUX="isolcpus=managed_irq,domain,48-95 nohz=on nohz_full=48-95 rcu_nocbs=48-95 kthread_cpus=0-47 irqaffinity=0-47 skew_tick=1"

Cpusets для управления группами задач:

# Создание partitions
mkdir /sys/fs/cgroup/cpuset/compute
echo 48-95 > /sys/fs/cgroup/cpuset/compute/cpus
echo 1 > /sys/fs/cgroup/cpuset/compute/mems
echo 0 > /sys/fs/cgroup/cpuset/compute/sched_load_balance

mkdir /sys/fs/cgroup/cpuset/system
echo 0-47 > /sys/fs/cgroup/cpuset/system/cpus
echo 0 > /sys/fs/cgroup/cpuset/system/mems

# Systemd slice для автоматического размещения
cat > /etc/systemd/system/compute.slice << EOF
[Unit]
Description=Compute Workload Slice

[Slice]
CPUAccounting=yes
EOF

systemctl daemon-reload

Запуск через systemd:

systemd-run --scope -p Slice=compute.slice -p CPUAffinity=48-95 ./hpc_application

Сценарий 3: Laptop с hybrid архитектурой. Efficient cores 0-7, performance cores 8-11:

GRUB_CMDLINE_LINUX="isolcpus=domain,8-11 nohz_full=8-11 kthread_cpus=0-7 irqaffinity=0-7"

Performance cores резервируются для demanding tasks. Background процессы остаются на efficient cores, экономя батарею.

Script для упрощенного запуска на isolated CPUs:

#!/bin/bash
# /usr/local/bin/run-isolated

ISOLATED_CPUS="8-11"
PRIORITY=80

taskset -c $ISOLATED_CPUS chrt -f $PRIORITY "$@"

Использование:

chmod +x /usr/local/bin/run-isolated
run-isolated ./benchmark

Измерение эффективности изоляции

Инструмент для измерения jitter на изолированных процессорах:

# Компиляция из исходников Linux kernel
cd tools/testing/selftests/timers
make
./nanosleep

# Альтернатива - cyclictest из rt-tests
sudo apt-get install rt-tests
cyclictest -m -Sp90 -i200 -h400 -q -c4

Параметр -c4 запускает на CPU 4. Вывод показывает минимальную, среднюю и максимальную latency. Значения ниже 100 микросекунд считаются хорошими для real-time Linux систем.

Продвинутый мониторинг через hwlat_detector:

echo 1 > /sys/kernel/debug/tracing/tracing_on
echo hwlat > /sys/kernel/debug/tracing/current_tracer
echo 1000000 > /sys/kernel/debug/tracing/tracing_thresh
cat /sys/kernel/debug/tracing/trace

Tracer обнаруживает hardware-induced latency выше 1 миллисекунды. SMI прерывания и BIOS вмешательства становятся видимыми.

Проверка scheduler stats:

cat /proc/sched_debug

Параметр nr_switches показывает количество context switches на процессор. Isolated CPU с single task должен показывать минимальные значения.

Количество voluntary и involuntary context switches для процесса:

grep ctxt /proc/<pid>/status

Low values подтверждают минимальное вмешательство scheduler.

Комплексный подход к NOHZ конфигурации, CPU isolation и RCU offloading превращает Linux в платформу для latency-sensitive и энергоэффективных приложений. Понимание взаимодействия kernel параметров, boot options и runtime tuning позволяет достичь микросекундных latencies на commodity hardware.