Шина PCIe умеет засыпать, и делает это десятки раз в секунду. Пока устройство простаивает, линк проваливается из рабочего состояния L0 в экономные уровни L1 и L1.2, где глушатся такты и падает потребление. Обратная сторона экономии это время пробуждения: от долей микросекунды в L0s до сотен микросекунд в L1.2, и каждая пауза ложится на первую же команду после простоя. Для ноутбука выгода видна в батарее, а для накопителя и сетевой карты оборачивается хвостами задержек и редкими провалами отклика. В статье разобраны состояния линка, поля утилиты lspci, политики ядра и точечная настройка через setpci, плюс измерения на NVMe и сети, чтобы режим выбирался по числам, а не по ощущениям.

Состояния покоя линка PCIe от L0 до L2 и цена каждого пробуждения

Стандарт PCIe описывает лестницу экономных состояний. L0 это полный рабочий режим, через который идут все транзакции. L0s экономит немного: глушится лишь одно из направлений передачи, а пробуждение занимает меньше микросекунды, поэтому его разрешают почти всегда. L1 глубже: останавливаются оба направления, вместе с ними такты и часть физического уровня, зато и экономия заметнее. Подуровни L1.1 и L1.2 появились в поколении PCIe 3.0: в L1.1 останавливается опорный генератор, а синфазное напряжение удерживается фиксацией на уровне покоя, в L1.2 с физического уровня снимается основное питание и остаются только цепи пробуждения, а возврат к жизни начинается по сигналу CLKREQ#. Самый глубокий уровень L2 почти полностью отключает устройство, выход из него измеряется миллисекундами, и до него ASPM не доходит: эта территория системного сна и гибернации.

Цена пробуждения задается прошивкой и самим устройством и объявляется в регистрах. Типичные значения выглядят так: выход из L0s укладывается в 1 мкс, из L1 в диапазон 8-64 мкс, из L1.2 занимает десятки и до пары сотен микросекунд, потому что системе нужно заново раскачать такты и восстановить синхронизацию. Экономия соразмерна: линк, который в L0 расходует доли ватта на каждое направление, в L1.2 довольствуется единицами милливатт. На ноутбуке с тремя-четырьмя простаивающими устройствами суммарная выгода доходит до ватта-двух, что хорошо видно на фоне общего потребления платформы в простое.

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

Чтение возможностей и текущего режима ASPM в полях LnkCap, LnkCtl и LnkSta утилиты lspci

Диагностика начинается с вывода параметров линка. Сначала находится устройство и его корневой порт:

lspci -nn | grep -Ei "nvme|ethernet|bridge"

Вывод показывает адреса вида 0000:01:00.0 и 0000:00:01.0, где первый адрес принадлежит накопителю, а второй порту, к которому он подключен. Дальше снимается расширенный вывод по каждому адресу:

lspci -vv -s 0000:01:00.0 | grep -E "Lnk(Cap|Ctl|Sta)"

Строки расскажут всю историю ASPM для этого конца линка:

LnkCap: Port #0, Speed 8GT/s, Width x4, ASPM L0s L1, Exit Latency L0s <1us, L1 <8us
LnkCtl: ASPM L1 Enabled; RCB 64 bytes CommClk+ ExtSynch- DLActive- BWMgmt- ABWMgmt-
LnkSta: Speed 8GT/s (ok), Width x4 (ok), ASPM L1 Enabled

LnkCap перечисляет, что устройство вообще умеет: здесь объявлены оба уровня L0s и L1, задержка выхода из L0s меньше микросекунды, из L1 меньше восьми микросекунд. LnkCtl показывает, что система включила сейчас, а LnkSta фиксирует фактическое состояние линка. Расхождение между ними информативно: если в LnkCtl стоит Enabled, а в LnkSta видно Disabled, линк не доходит до экономного режима, обычно из-за несогласованности тактов или отказа прошивки. Проверять нужно оба конца: устройство и его корневой порт должны объявлять и разрешать одни и те же уровни.

Отдельная возможность объявляется в собственном блоке подуровней:

lspci -vv -s 0000:01:00.0 | grep -A3 "L1 PM Substates"
Capabilities: [1c0 v1] L1 PM Substates
    L1SubCap: PCI-PM_L1.2+ PCI-PM_L1.1+ ASPM_L1.2+ ASPM_L1.1+ L1_PM_Substates+
        PortCommonModeRestoreTime=32us PortTPowerOnTime=44us
    L1SubCtl1: PCI-PM_L1.2- PCI-PM_L1.1- ASPM_L1.2- ASPM_L1.1-

Знаки плюс в строке L1SubCap означают поддержку уровней L1.1 и L1.2 со стороны ASPM. Числа в конце дают нижнюю границу выхода из L1.2: сложите 32 мкс на восстановление синфазного напряжения, 44 мкс на возвращение питания и несколько микросекунд на обучение линка, получится порог около 80 мкс. Минусы в L1SubCtl1 говорят, что подуровни пока не включены, хотя поддержка есть.

Глобальное управление через политики ядра и параметр pcie_aspm в загрузчике GRUB

Ядро собирает решения по всем линкам в одну политику. Активный вариант читается из файла:

cat /sys/module/pcie_aspm/parameters/policy
[default] powersave performance powersupersave

Квадратные скобки отмечают действующий режим. Вариант default повторяет решения прошивки: что BIOS разрешил на старте, то и работает. Вариант performance запрещает ASPM всюду и годится для рабочих станций и серверов, где стабильные задержки дороже ваттов. Вариант powersave включает все, что устройство объявило в LnkCap, включая подуровни, а powersupersave давит еще сильнее, разрешая глубокий сон даже там, где стабильность под вопросом. Смена применяется на лету, без перезагрузки:

echo performance | sudo tee /sys/module/pcie_aspm/parameters/policy

Чтобы режим переживал перезагрузки, параметр добавляется в командную строку ядра. В файле /etc/default/grub строка принимает вид:

GRUB_CMDLINE_LINUX_DEFAULT="quiet pcie_aspm=off"

После правки конфигурация обновляется командой sudo update-grub в Debian и Ubuntu или sudo grub2-mkconfig -o /boot/grub2/grub.cfg в семействе RHEL. Значение off полностью запрещает механизм, и файл policy в sysfs перестает влиять. Отдельный случай это pcie_aspm=force: параметр включает ASPM даже там, где прошивка отказала. На железе с кривыми таблицами ACPI система способна зависнуть при первом же обращении к устройству, поэтому force уместен только на собственном ноутбуке и только с возможностью откатить настройку из меню загрузчика. Решения ядра после перезагрузки видны в журнале:

sudo dmesg | grep -i aspm

В выводе будут порты с разрешенными уровнями и причины отказов, если прошивка их выдала: на части ноутбуков она отдает ASPM лишь избранным портам, и default повторит ровно эту картину.

Точечное управление устройством через setpci и регистры подуровней

Политика меняется сразу для всей системы, а иногда нужно отключить экономию только у одного капризного устройства, скажем у NVMe накопителя, оставив остальным их ватты. Для таких случаев есть прямая запись в регистр LnkCtl утилитой setpci. Сначала находится смещение блока capability PCIe:

lspci -vv -s 0000:01:00.0 | grep "Express ("

Строка вида "Capabilities: [80 v2] Express (v2) Endpoint, MSI 00" дает смещение 0x80, а регистр LnkCtl лежит на 0x10 внутри блока, то есть на 0x90. Утилита setpci умеет считать смещение сама по ключевому слову CAP_EXP:

sudo setpci -s 0000:01:00.0 CAP_EXP+10.w

В ответ придет текущее значение, например 0207. За ASPM отвечают биты 5 и 4: ноль запрещает экономию, 01b включает только L0s, 10b только L1, 11b оба уровня. Запись с маской меняет лишь эти два бита, не трогая остальное:

sudo setpci -s 0000:01:00.0 CAP_EXP+10.w=20:30

Здесь значение 0x20 ложится в биты 5:4 и включает чистый L1. Для запрета экономии вместо этого пишется 00:30, для обоих уровней 30:30. То же значение выставляется и на корневом порту 0000:00:01.0, иначе концы линка не договорятся и ничего не заработает. Проверка выполняется повторным чтением поля LnkSta через lspci.

С подуровнями L1.1 и L1.2 прямой путь сложнее: их состояние сидит в регистрах L1SubCtl отдельной capability, и вместе с ними требуется разрешение Clock Power Management и физически подведенный сигнал CLKREQ#. Свежие ядра сами программируют подуровни, когда политика разрешает, поэтому ручная запись оправдана только для базового L1 и только как временная мера при отладке.

Измерение задержек NVMe накопителя через fio до и после смены политики

Голые цифры задержек снимаются утилитой fio. Чтение с сырого устройства безопасно и не трогает данные:

fio --name=qd1 --filename=/dev/nvme0n1 --rw=randread --bs=4k --iodepth=1 --direct=1 --time_based --runtime=30 --group_reporting

Ключевые параметры здесь такие: bs=4k задает блок, iodepth=1 превращает тест в худший сценарий для ASPM, потому что между командами возникают паузы, в которые накопитель успевает уснуть, а direct=1 отключает кеш. Типичный фрагмент вывода:

iops=12500, bw=48.8MiB/s, lat (usec): min=48, max=196, avg=79.7, stdev=5.4

Связь задержки и скорости при глубине очереди 1 прямая: IOPS равен единице, деленной на задержку. При 79.7 мкс это 1 / 0.0000797, то есть около 12550 операций в секунду. Если бы каждая команда платила выходом из L1.2 на 120 мкс, средняя задержка выросла бы до 200 мкс, а IOPS упал бы до 5000, минус 60 процентов. В реальности накопитель уходит в глубокий сон не между каждой командой, поэтому на равномерной нагрузке потери скромнее, обычно единицы процентов, зато после паузы в простое первый отклик приносит всплеск на сотню-другую микросекунд. Для хвостов полезен вывод перцентилей: fio печатает таблицу clat percentiles, где 99.9-й перцентиль показывает именно редкие всплески от пробуждений. Методика сравнения проста: замер при default, переключение, замер при performance и сверка средних и максимумов при одинаковых условиях.

Сетевые карты и Wi-Fi адаптеры с расчетом цены пробуждения линка

Для сети цена пробуждения переводится в байты. Линия 10GbE тянет 1250 байт за микросекунду, и выход из L1 за заявленные 64 мкс означает, что за время просыпания мимо прошло бы 64 x 1250 = 80 000 байт трафика. Для крупных последовательных передач это почти незаметно, ядро и контроллер разрулят паузу буферами. Для мелких пакетов телефонии, DNS и кластерных протоколов картина другая: каждая пауза съедает часть бюджета задержки, а частые пробуждения не дают линку даже заснуть, отчего экономия исчезает, а издержки остаются.

Wi-Fi адаптеры в ноутбуках чувствительнее: с включенными подуровнями L1.2 отклик после простоя поползет вверх, и пинг может подскакивать с привычных двух-трех миллисекунд до десятков миллисекунд в момент пробуждения. По этой причине часть прошивок и драйверов сами запрещают глубокие уровни для таких устройств, и тогда в LnkSta видно Disabled при Enabled у соседнего конца. Правило для сервера и рабочей станции с интенсивным трафиком простое: политика performance, экономия остается ноутбукам и домашним системам. На настольных платформах с дешевыми сетевыми контроллерами отключение ASPM заодно лечит плавающие обрывы линка, когда скорость падает с 8GT/s до 2.5GT/s без видимой причины.

Выбор режима под задачу и проверка экономии энергии на батарее

Экономия измеряется напрямую. На ноутбуке мгновенное потребление батареи читается из sysfs:

cat /sys/class/power_supply/BAT0/power_now

Значение приходит в микроваттах: 8431000 это 8.43 Вт. Серия показаний в простое при политике default и такая же серия при performance дадут честную разницу, обычно от полуватта до двух ватт на платформе с несколькими устройствами. Утилита powertop подсказывает в списке Tunables строку про включение ASPM L1, а команда sudo powertop --auto-tune применяет рекомендации разом, включая экономию линков.

Итоговое решение сводится к короткой последовательности:

  1. снять вывод LnkCap, LnkCtl и LnkSta для устройства и его корневого порта, убедиться в поддержке уровней и зафиксировать фактическое состояние;
  2. прогнать fio на накопителе или сетевой тест при политике default и записать средние и максимальные задержки;
  3. переключить политику в sysfs или добавить параметр pcie_aspm в GRUB и повторить замеры при тех же условиях;
  4. на ноутбуке сверить показания power_now до и после, на сервере и рабочей станции выбрать performance без долгих раздумий;
  5. закрепить выбранный вариант параметром ядра в GRUB или правилом udev с setpci, чтобы режим переживал обновления ядра и переустановку системы.

Механизм ASPM хорош тем, что не требует выбирать между комфортом и автономностью навсегда: настройка меняется на лету и откатывается одной строкой. Пара замеров с fio и показаниями power_now показывает, где у конкретного железа проходит граница между экономией и задержками.