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

Цикл простоя выполняет два шага при каждой итерации. Сначала он обращается к регулятору из подсистемы CPUIdle для выбора состояния простоя. Затем вызывает драйвер CPUIdle, чтобы фактически перевести процессор в выбранное состояние. Это разделение ответственности не случайно: оно позволяет менять алгоритм выбора состояния независимо от платформозависимого кода управления железом.

Иерархия C-состояний и цена входа в каждое из них

Состояния простоя процессора называются C-состояниями в ACPI или idle states в Linux. Они различаются потреблением энергии и задержками при входе и выходе. Чем больше задержка входа и выхода, тем большую экономию энергии даёт пребывание в C-состоянии.

C0 означает, что ядро активно выполняет инструкции. Всё, что ниже, это разные степени сна. C1 останавливает тактирование ядра при сохранении питания всех регистров и кэшей. Выход из C1 занимает единицы микросекунд. C1E добавляет снижение напряжения. C3 останавливает тактирование шины и очищает внутренние кэши. C6 сохраняет состояние ядра в специальный буфер и полностью обесточивает вычислительную часть. Выход из C6 занимает сотни микросекунд.

Каждое состояние простоя характеризуется двумя параметрами: целевой остаточностью (target residency) и задержкой выхода (exit latency). Целевая остаточность представляет собой минимальное время, которое процессор должен провести в данном состоянии, чтобы сэкономить больше энергии, чем при использовании более поверхностного состояния. Задержка выхода является максимальным временем, которое потребуется процессору для начала выполнения первой инструкции после пробуждения.

Посмотреть все доступные состояния на конкретной системе и их параметры:

# Список состояний для первого ядра
ls /sys/devices/system/cpu/cpu0/cpuidle/

# Задержка выхода из состояния C3 в микросекундах
cat /sys/devices/system/cpu/cpu0/cpuidle/state2/latency

# Целевая остаточность
cat /sys/devices/system/cpu/cpu0/cpuidle/state2/residency

# Сколько раз состояние было активировано с момента загрузки
cat /sys/devices/system/cpu/cpu0/cpuidle/state2/usage

# Суммарное время в данном состоянии (в микросекундах)
cat /sys/devices/system/cpu/cpu0/cpuidle/state2/time

Соотношение значений usage и time даёт среднее время пребывания в состоянии за один вход. Если это среднее значительно меньше residency, регулятор выбирает данное состояние слишком оптимистично и процессор тратит энергию на вход и выход, не успевая отбить её экономией.

Архитектура CPUIdle и разделение на ядро, драйвер и регулятор

Подсистема CPUIdle состоит из трёх компонентов: ядра CPUIdle, драйверов CPUIdle и регуляторов CPUIdle. Регулятор находит наиболее подходящее для текущих условий состояние сна. Драйвер отвечает за перечисление C-состояний и за вход в них.

Ядро CPUIdle спроектировано как фреймворк, разрывающий жёсткие связи между задачей простоя, регуляторами и драйверами. Оно предоставляет унифицированный абстрактный интерфейс для задачи простоя, регуляторов и драйверов.

Есть четыре регулятора CPUIdle: menu, TEO, ladder и haltpoll. Какой из них используется по умолчанию, зависит от конфигурации ядра и в частности от того, может ли цикл простоя останавливать таймер планировщика.

Регулятор menu используется в системах с динамическим тиком (NO_HZ). Он строит прогноз времени простоя на основе истории предыдущих пробуждений и времени до ближайшего таймера, выбирая состояние с наибольшей целевой остаточностью, не превышающей прогноз.

Регулятор TEO (Timer Events Oriented) спроектирован для tickless-систем. Идея основана на наблюдении, что на многих системах события таймера на два и более порядка превышают по частоте любые другие прерывания, поэтому они являются наиболее значимым источником пробуждений CPU из состояний простоя.

Регулятор ladder предназначен для систем с периодическим тиком. Он движется по состояниям вверх и вниз по лестнице: если процессор провёл в текущем состоянии достаточно долго, следующий шаг будет глубже; если пробуждение случилось слишком быстро, шаг будет мельче.

Регулятор haltpoll специфичен для гостевых виртуальных машин. Перед входом в состояние сна он некоторое время опрашивает флаг в памяти в ожидании пробуждения от гипервизора. Это снижает задержку пробуждения за счёт небольшого увеличения потребления.

Драйверы intel_idle и acpi_idle и откуда берутся данные о состояниях

То, какой драйвер CPUIdle используется, обычно зависит от платформы. На большинстве Intel-платформ могут работать два драйвера: intel_idle с жёстко закодированной информацией о состояниях и acpi_idle, читающий эту информацию из таблиц ACPI системы.

Драйвер intel_idle предпочтителен: он содержит актуальные таблицы C-состояний для каждого поколения процессоров Intel и учитывает особенности конкретных степпингов. Параметр max_cstate ограничивает глубину доступных состояний:

# Запретить состояния глубже C1 через параметр ядра
# (добавить в /etc/default/grub в строку GRUB_CMDLINE_LINUX)
intel_idle.max_cstate=1

# Или для ACPI-драйвера
processor.max_cstate=1

Проверить активный драйвер и регулятор:

cat /sys/devices/system/cpu/cpuidle/current_driver
# intel_idle

cat /sys/devices/system/cpu/cpuidle/current_governor
# menu

cat /sys/devices/system/cpu/cpuidle/available_governors
# ladder menu teo

Сменить регулятор без перезагрузки:

echo teo > /sys/devices/system/cpu/cpuidle/current_governor

Для ARM-платформ состояния простоя описываются непосредственно в Device Tree. Узел idle-states в Device Tree содержит свойства entry-latency-us, exit-latency-us и min-residency-us, определяющие параметры состояния сна для конкретной платформы. На платформах с TrustZone переход в глубокое состояние выполняется через вызов PSCI (Power State Coordination Interface) в защищённом мире:

idle-states {
    entry-method = "psci";

    CPU_SLEEP_0: cpu-sleep {
        compatible = "arm,idle-state";
        idle-state-name = "cpu-sleep";
        arm,psci-suspend-param = <0x0010000>;
        entry-latency-us = <40>;
        exit-latency-us  = <100>;
        min-residency-us = <300>;
    };
};

PM QoS и управление задержкой пробуждения из пространства пользователя

Бывают задачи, для которых глубокий сон процессора губителен. Аудиосервер, система жёсткого реального времени, высокочастотный трейдинговый движок: все они требуют, чтобы процессор пробуждался в пределах строго ограниченного времени. Для этого существует подсистема PM QoS.

Задать допустимую задержку пробуждения для конкретного ядра можно через запись значения в микросекундах в файл pm_qos_resume_latency_us. Значение "0" означает отсутствие ограничений, значение "n/a" отключает все C-состояния для данного ядра.

# Ограничить задержку пробуждения cpu0 до 100 микросекунд
echo 100 > /sys/devices/system/cpu/cpu0/power/pm_qos_resume_latency_us

# Полностью запретить C-состояния для cpu0
echo n/a > /sys/devices/system/cpu/cpu0/power/pm_qos_resume_latency_us

# Сбросить ограничение (разрешить все состояния)
echo 0 > /sys/devices/system/cpu/cpu0/power/pm_qos_resume_latency_us

Из кода ядра или драйвера ограничение на задержку устанавливается через API PM QoS:

#include <linux/pm_qos.h>

struct pm_qos_request my_qos_req;

/* Запросить задержку пробуждения не более 100 мкс */
pm_qos_add_request(&my_qos_req, PM_QOS_CPU_DMA_LATENCY, 100);

/* Изменить ограничение */
pm_qos_update_request(&my_qos_req, 50);

/* Снять ограничение при завершении работы */
pm_qos_remove_request(&my_qos_req);

Регулятор CPUIdle сравнивает значение допустимой задержки, зарегистрированное через PM QoS, с задержками выхода из каждого C-состояния и выбирает состояния, удовлетворяющие этому требованию. Это означает, что ни один регулятор не выберет состояние, задержка выхода из которого превышает установленное ограничение, независимо от того, насколько привлекательным с точки зрения экономии энергии оно выглядит.

Написание собственного CPUIdle-драйвера для встраиваемой платформы

Когда стандартные драйверы intel_idle и acpi_idle не подходят, а платформа не использует Device Tree в полной мере, написание собственного CPUIdle-драйвера становится необходимостью. Структура драйвера включает массив описаний состояний и объект cpuidle_driver:

#include <linux/cpuidle.h>

static int my_enter_idle(struct cpuidle_device *dev,
                          struct cpuidle_driver *drv, int index)
{
    /* Платформозависимый вход в состояние */
    switch (index) {
    case 0:
        asm volatile("wfi");          /* Wait For Interrupt */
        break;
    case 1:
        my_platform_enter_deep_sleep();
        break;
    }
    return index;
}

static struct cpuidle_state my_idle_states[] = {
    {
        .name        = "WFI",
        .desc        = "Wait for interrupt",
        .exit_latency    = 1,
        .target_residency = 1,
        .enter           = my_enter_idle,
    },
    {
        .name        = "DEEP",
        .desc        = "Deep sleep with cache flush",
        .flags       = CPUIDLE_FLAG_TIMER_STOP,
        .exit_latency    = 150,
        .target_residency = 500,
        .enter           = my_enter_idle,
    },
};

static struct cpuidle_driver my_idle_driver = {
    .name             = "my_cpuidle",
    .owner            = THIS_MODULE,
    .states           = my_idle_states,
    .state_count      = ARRAY_SIZE(my_idle_states),
    .safe_state_index = 0,    /* индекс безопасного состояния */
};

static int __init my_cpuidle_init(void)
{
    return cpuidle_register(&my_idle_driver, NULL);
}
device_initcall(my_cpuidle_init);

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

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

Диагностика и настройка через cpupower и perf

Инструмент cpupower из пакета linux-tools предоставляет удобный интерфейс для просмотра и настройки состояний простоя:

# Вся информация о текущих idle-состояниях
cpupower idle-info

# Пример вывода:
# CPUIdle driver: intel_idle
# CPUIdle governor: menu
# Available idle states:
# POLL  HALT  C1    C1E   C3    C6    C8
#
# State C6:
#   Latency:          133 us
#   Residency:        400 us

# Отключить конкретное состояние для всех ядер
cpupower idle-set -d 3

# Включить обратно
cpupower idle-set -e 3

# Отключить все состояния глубже C1
cpupower idle-set -D 2

Для анализа того, как реально распределяется время между состояниями, perf предоставляет специализированные события:

# Статистика переходов в idle-состояния за 5 секунд
perf stat -e "power:cpu_idle" -a sleep 5

# Детальный трейс с метками состояний
perf record -e "power:cpu_idle" -a sleep 2
perf report

Инструмент turbostat даёт ещё более детальную картину непосредственно с аппаратных счётчиков процессора:

# Статистика C-состояний каждые 5 секунд
turbostat --interval 5 --show CPU,Avg_MHz,Busy%,C1%,C3%,C6%,C8%

Столбец C6% покажет, какой процент времени каждое ядро провело в глубоком C6. Если на нагруженном сервере это значение неожиданно велико, стоит проверить настройки планировщика и баланс нагрузки между ядрами.

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