У каждого устройства на шине PCIe есть целая лестница состояний, от полного раскрытия в рабочем режиме до почти нулевого потребления с отключённым линком. Экономия на этих переходах измеряется ваттами на сервер и часами работы от батареи на ноутбуке, но у каждой ступени есть цена в виде времени пробуждения, и на горячем пути она способна убить задержку, которую строили месяцами. Linux даёт полный набор рычагов для управления: файлы sysfs на каждое устройство, параметры ядра, настройки ASPM через setpci и автономные схемы питания самих устройств, вроде NVMe со своими состояниями.
Разберём лестницу состояний устройства и линка, работу runtime PM в ядре, управление ASPM, цену пробуждения, механизмы сброса и практический порядок настройки узла.
Ступени питания устройства и состояния линка
Состояния устройства по спецификации обозначаются D0 через D3. D0 это полное рабочее состояние, D1 и D2 встречаются редко и зависят от устройства, D3 делится на два случая: D3hot, когда питание шины сохранено и устройство доступно для программных обращений, и D3cold, когда питание снято целиком, а пробуждение возможно только через сигнал PME и платформенные ресурсы. Для D3cold нужен ещё и сброс конфигурационного пространства при возврате, поэтому выход из него самый долгий и самый капризный. D1 и D2 в спецификации существуют, но реальные устройства почти всегда ограничиваются парой D0 и D3, и встретить их живьём доводится нечасто.
Линк имеет собственную шкалу, независимую от состояний устройства: активный L0, мелкий сон L0s, глубокий L1 и его подуровни L1.1 и L1.2 с ещё меньшим потреблением, плюс выключенные L2 и L3. Управляет переходами механизм ASPM, Active State Power Management, который по таймерам неактивности уводит линк в сон без участия софта. Отдельная тонкость в том, что операционная система получает право управлять ASPM только с разрешения прошивки через механизм _OSC, и BIOS, который отчитался о владении питанием PCIe, способен оставить ядро наблюдателем. Выход из L0s стоит порядка микросекунды, из L1 десятки микросекунд, поэтому ASPM почти всегда безопасен для пакетной нагрузки, но на пути с жёсткой задержкой его приходится ограничивать. Заметьте, состояния линка и устройства независимы: устройство может сидеть в D0 при спящем линке, и наоборот.
Runtime PM в ядре и файлы sysfs
Управление на уровне устройства завязано на механизм runtime PM. Драйвер решает, когда устройству можно уснуть, а пользователю доступны короткие файлы в sysfs:
cat /sys/bus/pci/devices/0000:01:00.0/power/runtime_status
echo on > /sys/bus/pci/devices/0000:01:00.0/power/control
echo auto > /sys/bus/pci/devices/0000:01:00.0/power/control
echo 5000 > /sys/bus/pci/devices/0000:01:00.0/power/autosuspend_delay_ms
Режим on держит устройство всегда активным, режим auto отдаёт решение драйверу, который отсчитывает период неактивности из autosuspend_delay_ms и уводит устройство в D3hot, а первое же обращение возвращает его в D0. Файл runtime_status показывает текущее положение: active, suspended, suspending, resuming. Обойти все устройства разом удобно циклом по /sys/bus/pci/devices с выпиской runtime_status, за секунду видно, кто бодрствует без причины. Для ручного эксперимента есть узлы power/runtime_suspend и power/runtime_resume, где запись принудительно уводит и возвращает устройство, что удобно для проверки, оживёт ли драйвер после сна.
Для чувствительных к пробуждению устройств существует ограничитель задержки, файл power/pm_qos_resume_latency_us: положительное число микросекунд задаёт максимально допустимое время выхода из сна, и драйвер обязан отбрасывать состояния, которые в этот бюджет не влезают. Запись 0 снимает ограничение. Внутри ядра та же логика живёт в вызовах pm_runtime_get_sync и pm_runtime_put_autosuspend, и качество драйвера здесь решает: аккуратный драйвер гасит устройство между запросами, неряшливый держит его в D0 вечно, и sysfs тут бессилен.
Пробуждение устройства из сна обслуживает механизм PME, Power Management Event: устройство, получив внешнее событие, выставляет сообщение PME, и система возвращает его в D0. Разрешение на пробуждение видно в файле power/wakeup, где значения enabled и disabled включают и запрещают генерацию событий, а счётчик power/wakeup_count помогает сопоставить пробуждения с событиями до и после сна:
cat /sys/bus/pci/devices/0000:01:00.0/power/wakeup
echo enabled > /sys/bus/pci/devices/0000:01:00.0/power/wakeup
Для выхода из D3cold одного PME мало: нужно, чтобы платформа сохранила питание в дежурной линии и определила ресурс питания средствами ACPI, без чего устройство просыпается только вместе с полным включением узла. Внутри драйвера за это отвечает pci_enable_wake, и качество его вызовов определяет, проснётся ли сетевой контроллер от входящего пакета при закрытой крышке ноутбука или уснёт до следующего открытия. В связке с системными состояниями вроде S0ix картина усложняется ещё и тем, что все устройства на пути к глубокому сну платформы обязаны уметь засыпать сами, и один бодрствующий контроллер держит всю платформу в полусне с лишним потреблением.
Политики ASPM, параметры ядра и точечный setpci
Глобально поведение ASPM управляется через файл политики:
cat /sys/module/pcie_aspm/parameters/policy
default performance powersave powersupersave
echo powersave > /sys/module/pcie_aspm/parameters/policy
То же самое задаётся при загрузке параметрами ядра: pcie_aspm.policy=performance прижимает линк к L0, pcie_aspm=off отключает механизм целиком, pcie_port_pm=off запрещает питание портов. Политика performance оправдана на узлах с жёсткой задержкой, powersupersave выжимает максимум экономии ценой пробуждений, а default доверяет прошивке, что не всегда лучший выбор, потому что прошивки ошибаются. На серверах, где BIOS выставил агрессивные настройки, часто надёжнее задать политику явно и проверить фактические состояния, чем рассчитывать на умолчания из прошивочных таблиц.
Точечное вмешательство выполняется через конфигурационное пространство. Управляющие биты ASPM лежат в регистре Link Control со смещением 0x10 внутри расширенной возможности PCIe, и setpci умеет читать и писать их по символическому имени:
setpci -s 01:00.0 CAP_EXP+10.w
setpci -s 01:00.0 CAP_EXP+10.w=0:3
Чтение возвращает текущее слово, младшие два бита которого это L0s и L1, а запись с маской 0:3 очищает оба. Проверять результат удобнее по lspci: вывод lspci -vvv для устройства содержит строку LnkCtl с перечислением Enabled и Disabled, а строка LnkSta показывает, что реально согласовано с партнёром по линку. Отдельно стоит помнить, что ASPM согласуется обоими концами: принудительное включение на устройстве без поддержки со стороны апстримного порта не даст ничего, а рассинхрон настроек порта и устройства источник тихих сбоев на некоторых чипсетах.
Цена пробуждения в микросекундах и миллисекундах
Иерархия цен выглядит так. Переход L0 и обратно в L0s стоит около микросекунды и для большинства нагрузок незаметен. Выход из L1 добавляет десятки микросекунд на переподготовку линка, что уже ощутимо на очереди глубиной в один запрос, где каждая транзакция начинается с холодного линка. Возврат из D3hot требует повторной инициализации устройства драйвером и измеряется миллисекундами. D3cold тяжелей всех: снятие и подача питания, перезагрузка прошивки, перечитывание конфигурации, и тут счёт уже на десятки миллисекунд, иногда больше, в зависимости от платформы.
Наглядный пример цены: латентная очередь глубиной в один запрос, которая ловит уснувший в L1 линк на каждом обращении, платит лишние десятки микросекунд с каждой транзакцией, и это удвоение исходной задержки. Отсюда практическое правило расстановки. Видеокарта для инференса, сетевой контроллер под постоянным трафиком и NVMe под латентную нагрузку живут с runtime PM в режиме on и ASPM по минимуму, потому что экономия в доли ватта не стоит миллисекундной паузы в хвосте задержки. Датчики, порты USB-хабов, камеры и прочая периферия спят в auto и просыпаются по событию. Между этими полюсами лежит территория профилирования: powertop в разделе Tunables перечисляет устройства, которые ядро готово усадить, и показывает потенциальный выигрыш по каждому, а turbostat своими колонками residency по пакетным состояниям процессора демонстрирует, мешает ли чей-то бодрый D0 уходу всей платформы в глубокий сон.
Сбросы и перестройка состояния через reset и SR-IOV
Иногда состояние устройства нужно не усадить, а обнулить. Универсальный механизм это файл reset в каталоге устройства, запись единицы в который инициирует сброс:
echo 1 > /sys/bus/pci/devices/0000:01:00.0/reset
cat /sys/bus/pci/devices/0000:01:00.0/reset_method
Ядро выбирает доступный способ само, и начиная с версии 5.15 выбор можно задавать руками через файл reset_method: FLR, Function Level Reset, аккуратно перезапускает одну функцию через бит в регистре Device Control и не трогает соседей, сброс через управление питанием и bus reset через переконфигурацию линка грубее и затрагивает больше оборудования. Для устройств с виртуализацией добавляется ещё один уровень состояния: запись в sriov_numvfs создаёт или убирает виртуальные функции, каждая из которых живёт собственным жизненным циклом, и привязка VF к гостю через vfio-pci начинается именно с корректного состояния родительской функции.
Сбросы годятся не всегда. Устройства без поддержки FLR при сбросе теряют трафик соседних функций при bus reset, некоторые прошивки после D3cold не восстанавливают MSI-X корректно, и здесь спасает именно выбор метода через reset_method. При отладке зависшего контроллера последовательность обычно такая: сначала попытка runtime_resume, затем FLR, и только потом bus reset с переконфигурацией, потому что последний вариант виден всему дереву под портом. Спецификация отводит функции до 100 миллисекунд на завершение FLR, и драйвер обязан выждать этот интервал, прежде чем считать устройство готовым к работе.
NVMe и автономные переходы питания
Накопители NVMe принесли в этот мир собственную параллельную систему состояний. Контроллер описывает до 32 состояний питания в структуре identify, каждое со своим потреблением и временем входа и выхода, а механизм APST, Autonomous Power State Transition, перемещает контроллер между ними без участия хоста. Вывод nvme id-ctrl печатает таблицу состояний с временами переходов, и по ней сразу видно, чем грозит самый глубокий из разрешённых PSD, прежде чем гнаться за ваттами. Хост управляет этим через признак с номером 12:
nvme get-feature /dev/nvme0 -f 12 -H
nvme set-feature /dev/nvme0 -f 12 -v 0
Чтение с ключом -H расшифровывает таблицу переходов, а запись нулевого значения запрещает автономные переходы целиком, оставляя контроллер в активном состоянии. Для латентно-чувствительных систем это работает в паре с ASPM: накопитель может лежать в глубоком внутреннем состоянии при активном линке, и первое обращение заплатит и за выход контроллера, и за пробуждение линка, если ASPM успел усадить и его. На слабо нагруженных узлах ноутбучного класса наоборот разумно оставить APST включённым и не мешать экономии.
Практический порядок настройки узла
Свести управление состояниями в воспроизводимую настройку помогает порядок из шести шагов:
- снять инвентаризацию через lspci -vvv с выпиской текущих ASPM и состояний по каждому устройству;
- выбрать глобальную политику через /sys/module/pcie_aspm/parameters/policy или параметр ядра под профиль узла;
- для задержко-критичных устройств зафиксировать power/control в on и выставить pm_qos_resume_latency_us;
- периферию перевести в auto с осмысленным autosuspend_delay_ms и проверить поведение через powertop;
- закрепить выбор метода сброса reset_method для устройств, где FLR нужен гарантированно;
- после настройки прогнать нагрузку и сверить задержки и счётчики ошибок, потому что рассинхрон ASPM виден только под нагрузкой.
Итог у управления состояниями PCIe двоякий, как и у любой экономии. Грамотная расстановка переводит систему в режим, где неактивное оборудование почти не тратит энергию, а горячий путь никогда не ждёт пробуждения. Небрежная же настройка оставляет узел либо с жрущим всё подряд D0, либо с загадочными выбросами задержки, когда очередное обращение упирается в устройство, которое водитель усадил спать на той самой микросекунде, когда оно понадобилось.