Адаптер с паспортным потолком 3,9 ГБ/с в живых тестах нередко отдаёт на четверть меньше, и виноватых ищут в драйверах, памяти и процессоре, а самый тихий подозреваемый остаётся без внимания. Прячется он на уровне транзакций шины, где каждый пакет TLP оплачивает собственный заголовок и служебное обрамление. Значения Max Payload Size и Max Read Request Size, оставленные прошивкой платы, далеки от возможного максимума чаще, чем кажется. Linux при этом даёт полный контроль над регистрами шины без единой строки кода на C, и в этой статье собраны команды lspci, setpci, ethtool и параметры ядра, которые превращают заводские дефолты в осознанную настройку.

Анатомия пакета TLP и цена заголовка в байтах

Пакет TLP, Transaction Layer Packet, это кирпичик, из которого складывается весь обмен через PCIe. Транзакционный уровень формирует пакет, канальный обрамляет его номером последовательности и контрольной суммой LCRC, физический гонит биты по линиям. Заголовок занимает 12 байт при 32-битной адресации и 16 байт при 64-битной, ещё 4 байта при желании добавляет необязательная контрольная сумма ECRC. На поле с данными остаётся не больше Max Payload Size, сокращённо MPS, а сама величина выбирается из шести фиксированных значений: 128, 256, 512, 1024, 2048 и 4096 байт.

Дальше простая арифметика, которую пропускают зря. Полезная доля при MPS 128 составляет 128 из 144 байтов, то есть 88,9 процента. При MPS 256 выходит 94,1 процента, при 512 ровно 97,0, а при 4096 уже 99,6. Разрыв между крайними вариантами почти 11 процентных пунктов чистой пропускной способности. Реальные проценты скромнее из-за обрамления канального уровня, но порядок такой.

Осталось вспомнить про физический уровень. Первые две генерации шины тратят 20 процентов на кодирование 8b/10b, третья и все последующие перешли на схему 128b/130b с потерей лишь 1,54 процента. Одна линия Gen3 при 8 GT/s отдаёт примерно 984,6 МБ/с в каждую сторону, конфигурация x4 упирается в 3,94 ГБ/с, а x16 в 15,75 ГБ/с. Задача тюнинга в том, чтобы доля служебных данных не разрослась до неприличия.

Регистр Device Control и шестнадцатеричные коды MPS и MRRS

Оба параметра живут в одном и том же месте. Внутри блока PCIe capability у каждого устройства есть 16-битный регистр Device Control со смещением 08, и именно он решает судьбу пакетов. Биты 7:5 под маской 0x00e0 хранят MPS, биты 14:12 под маской 0x7000 отвечают за Max Read Request Size, MRRS. Коды в обоих полях одинаковые: 0 означает 128 байт, 1 даёт 256, 2 даёт 512, 3 даёт 1024, 4 даёт 2048 и 5 даёт 4096 байт.

Рядом стоят соседи, которые тоже заслуживают внимания. Бит 0x0010 включает Relaxed Ordering и разрешает обход строгого порядка записи, бит 0x0100 активирует расширенное поле тегов, а 0x0800 включает No Snoop. Аппаратный потолок, выше которого MPS физически не поднять, лежит в соседнем регистре Device Capabilities: если там заявлено максимум 512 байт, запись большего кода эффекта не даст. Список имён всех регистров печатает setpci --dumpregs, среди них есть имя CAP_EXP.

Зачем вообще два разных поля, если оба про размер? По сути они контролируют разные направления трафика. MPS ограничивает нагрузку любого исходящего пакета, включая записи и ответы на чтения, а MRRS ограничивает только запросы чтения и размер порций данных, которые партнёр вернёт в ответ. Записи едут одним куском в пределах MPS, чтения режутся на фрагменты по MRRS, и эта асимметрия позже вылезет отдельным узким местом.

Диагностика текущих значений через lspci и sysfs

Сначала система должна показать, с чем вообще приходится работать. Утилита lspci из пакета pciutils, актуальная версия 3.15.0, расшифровывает регистры в читаемый вид. Ключ -vvv включает максимальную детализацию, ключ -s выбирает адрес в формате домен, шина, устройство и функция:

lspci -vvv -s 01:00.0

В длинном выводе интерес представляют две строки, в которых lspci раскрывает Device Capabilities и Device Control:

DevCap: MaxPayload 512 bytes, PhantFunc 0
DevCtl: MaxPayload 256 bytes, MaxReadReq 512 bytes

Первая строка показывает аппаратный потолок, вторая текущее значение: адаптер умеет 512 байт, а работает с 256, запас ровно в два раза.

То же самое читается из sysfs. Файл config содержит бинарный дамп конфигурационного пространства, скорость и ширину линка показывают отдельные атрибуты:

hexdump -C /sys/bus/pci/devices/0000:01:00.0/config | head
cat /sys/bus/pci/devices/0000:01:00.0/current_link_speed
cat /sys/bus/pci/devices/0000:01:00.0/current_link_width
cat /sys/bus/pci/devices/0000:01:00.0/max_link_speed

Атрибуты current_link_speed и current_link_width показывают достигнутую договорённость линка, например 8 GT/s и x4, а max_link_speed раскрывает потолок слота. После загрузки с параметрами ядра из следующего раздела полезно заглянуть в буфер сообщений:

dmesg | grep "Max Payload Size"

Строки вида "Max Payload Size set to 512/ 512 (was 256), Max Read Rq 512" фиксируют, что и для кого поменяли.

Ручная настройка через setpci и её закрепление после перезагрузки

setpci пишет и читает конфигурационное пространство напрямую. Флаг -D запрещает реальные записи и лишь печатает план действий, поэтому последовательности сначала проверяют на бумаге. Для установки MPS и MRRS по 512 байт в регистре Device Control достаточно одной команды с маской:

setpci -vD -s 01:00.0 CAP_EXP+08.w=2040:70e0
setpci -v -s 01:00.0 CAP_EXP+08.w=2040:70e0
setpci -s 01:00.0 CAP_EXP+08.w

Имя CAP_EXP указывает на блок PCIe capability, смещение +08 попадает в Device Control, суффикс .w задаёт 16-битную ширину доступа. Значение 0x2040 складывается из кода MPS 512, это 0x0040, и кода MRRS 512, это 0x2000. Маска 0x70e0 защищает остальное содержимое регистра, ведь запись идёт по правилу "прочитай, измени, запиши". Третья команда перечитывает регистр.

Для 4096 байт арифметика такая: MPS даёт 0x00a0, MRRS даёт 0x5000, итоговая запись выглядит как CAP_EXP+08.w=50a0:70e0. Ещё надёжнее работает контрольный список, который стоит прогонять на любом устройстве:

  1. Снять и сохранить текущее значение регистра до любых экспериментов;
  2. Сверить аппаратный максимум MPS из DevCap, чтобы не писать заведомо недопустимый код;
  3. Прогнать команду с флагом -D и убедиться, что изменяются только нужные биты;
  4. Применить запись с маской и перечитать регистр;
  5. Дать нагрузку и следить за ошибками тракта в буфере сообщений ядра.

Настройки конфигурационного пространства не переживают перезагрузку, поэтому сценарий с setpci заворачивают в oneshot unit системного менеджера systemd:

[Unit]
Description=Настройка MPS и MRRS для адаптера
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/pcie-tune.sh
[Install]
WantedBy=multi-user.target

Внутри pcie-tune.sh живёт уже знакомая строка с setpci, а включается механизм командой systemctl enable. Альтернативой служит правило udev с действием add для сменных карт.

Параметры ядра pcie_bus_perf и pcie_bus_safe для всей системы

Править регистры руками красиво, но ядро умеет само и надёжнее. По умолчанию оно MPS не трогает и оставляет прошивочные значения, зато при обнаружении несоответствия прямо предупреждает и советует параметр pci=pcie_bus_safe. Полный набор стратегий выглядит так: pcie_bus_tune_off оставляет всё как есть, pcie_bus_safe выставляет тракту наибольший общий поддерживаемый MPS, pcie_bus_perf ставит максимум возможного и подтягивает MRRS к полученному MPS, а pcie_bus_peer2peer форсирует скромные 128 байт везде, чтобы обмен между устройствами напрямую оставался рабочим. Параметр дописывается в /etc/default/grub, после чего выполняется update-grub:

GRUB_CMDLINE_LINUX_DEFAULT="quiet pcie_bus_perf"

Стратегия perf предполагает обмен между устройством и памятью, и для типичного сервера с NVMe и сетевыми адаптерами это тот случай. Вариант safe выбирает значение, поддерживаемое всеми участниками тракта, и применяет его единообразно.

Разница принципиальна. setpci меняет одно устройство, не спрашивая соседей, а повышение MPS выше значений вышестоящих портов порождает ошибки тракта, видимые в буфере сообщений как записи AER.

Незавершённые чтения, теги и скрытый барьер пропускной способности

Записи через шину ходят в один конец без ожидания ответа, а вот чтения устроены сложнее. Каждое чтение занимает тег, ждёт встречных пакетов завершения и только потом освобождает тег. По умолчанию устройство распоряжается 32 тегами, а с расширенным полем, тем самым битом 0x0100 из Device Control, их становится 256. Если перемножить количество тегов на MRRS, получится максимум данных, одновременно находящихся в полёте. При MRRS 128 и 32 тегах это скромные 4 КБ, при 512 уже 16 КБ, а с расширенными тегами и вовсе 128 КБ.

Теперь прикинем, сколько должно быть в полёте, чтобы труба не пустовала. Полоса Gen3 x4 равна 3,94 ГБ/с, типичное время кругового оборота запроса через корневой порт порядка двух микросекунд, значит, в полёте должно висеть около 8 КБ, чтобы канал успевал заполняться. С MRRS 128 и штатными 32 тегами потолок в 4 КБ не дотягивает, и чтения физически не разгоняют шину. Так объясняются загадочные недоборы чтения в fio при больших глубинах очереди.

Лекарство двухуровневое. Сначала MRRS поднимается до 512 или 1024, чего обычно достаточно, потому что больше 16 КБ в полёте требуется редко. Если и этого мало, включается расширенное поле тегов:

setpci -v -s 01:00.0 CAP_EXP+08.w=0100:0100

Маска 0x0100 меняет только нужный бит и не трогает соседей. Часть устройств заявляет поддержку расширенных тегов в DevCap, но капризничает, поэтому обязателен контрольный прогон. Модель упрощает реальность с многоочередными DMA-движками, но порядок величин передаёт верно.

ethtool, NVMe и практические сценарии тюнинга с цифрами

MPS и MRRS задают крупность пакетов, но фактический размер каждого TLP определяет то, как драйвер формирует данные, и тут выходит на сцену ethtool версии 7.1:

ethtool -l enp1s0
ethtool -L enp1s0 combined 8
ethtool -g enp1s0
ethtool -G enp1s0 rx 4096 tx 4096
ethtool -C enp1s0 adaptive-rx on
ethtool -K enp1s0 gro on

Команда -L увеличивает число каналов, каждый со своей очередью DMA, -G раздувает кольца приёма и передачи до 4096 дескрипторов, -C включает адаптивное объединение прерываний, -K оставляет включённой склейку пакетов GRO. Логика везде одна: чем крупнее куски, которые драйвер отдаёт железу, тем крупнее выходящие TLP и тем меньше процент заголовков. Мелкие пакеты копируются в отдельные буферы по порогу rx-copybreak:

ethtool --set-tunable enp1s0 rx-copybreak 256

С накопителями роли распределяются иначе. NVMe гоняет и запросы, и данные, поэтому последовательным нагрузкам полезен крупный MRRS, а базам данных с операциями в 4 КБ чаще выигрывает умеренный MPS. Методика сравнения тривиальна, fio с прямым доступом:

fio --name=read --filename=/dev/nvme0n1 --direct=1 --rw=read --bs=1M --iodepth=64 --runtime=30 --time_based

Сетевой трафик проверяет iperf3, например iperf3 -c 192.168.10.2 -t 30 -P 8, и сразу видно, приближается ли скорость к потолку линка. Для SR-IOV виртуальные функции создаются записью в sysfs, перед включением значение должно быть нулевым, а максимум подскажет файл sriov_totalvfs:

echo 0 > /sys/bus/pci/devices/0000:01:00.0/sriov_numvfs
echo 4 > /sys/bus/pci/devices/0000:01:00.0/sriov_numvfs

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

echo 0 > /sys/bus/pci/devices/0000:01:00.0/link/l1_aspm

Файл l1_aspm есть у каждого устройства с поддержкой управления питанием, а глобально отключает параметр ядра pcie_aspm=off.

В сухом остатке шина PCIe не прячет свою скорость, она отдаёт её тому, кто соберёт настройку в три слоя. Первый слой задаёт стратегия ядра, pcie_bus_perf или pcie_bus_safe, второй слой уточняет Device Control через setpci с маской, третий слой подстраивает профиль драйвера через ethtool и sysfs. Заводские значения редко бывают оптимальными, зато теперь каждое можно назвать по имени, измерить и осознанно заменить, и это отличает администрирование по наитию от инженерии.