Память за шиной CXL появляется в системе как очередной NUMA-узел, и всё выглядит предсказуемо. Между ядром процессора и чипами памяти стоит цепочка звеньев: корневой комплекс CXL, мосты, порты, декодеры адресов, регистры состояния. Любое из них способно тихо съедать наносекунды или вовсе отсекать половину ёмкости. Профилирование отвечает на три вопроса: как собрана топология, куда декодеры реально ведут адреса и какие ошибки копятся на пути данных. Ответы разбросаны по дереву PCI, по sysfs, по выводу утилит cxl и ndctl, по потоку событий perf. Ниже рабочий набор команд с реальным синтаксисом и цифрами.

Иерархия устройств CXL от узла ACPI0017 до конечного порта

Ядро Linux описывает корневой комплекс CXL объектом root на вершине дерева декодирования памяти. На ACPI-платформах его представляет устройство ACPI0017, порождающее шинный объект root0 с провайдером ACPI.CXL, и cxl list -B возвращает пару полей bus root0 и provider ACPI.CXL. Под root0 висят хост-мосты ACPI0016, и каждый из них становится dport для корневого порта. Дальше раскрываются PCI-мосты, апстрим- и даунстрим-порты коммутаторов, а на концах работают устройства памяти, которые ядро называет memdev и нумерует как mem0, mem1. Атрибуты memdev пришли в ядре 5.12, а объекты портов и декодеров в 5.14, поэтому на старых ядрах устройство видно, а его место в иерархии нет.

У каждой ступени есть уровень глубины, от нуля у корня. Порты существуют, только пока драйвер cxl_mem привязан к устройству класса 0502: он поднимается от конечного устройства вверх и вписывает в схему все промежуточные коммутаторы. Пропал memdev, и вместе с ним исчезла вся ветка портов над ним.

Отдельный сюжет это два режима корневого комплекса. В режиме VH, virtual hierarchy, даунстрим-порт выглядит как обычный PCI-мост. В режиме RCH, restricted CXL host, он прячется внутрь корневого комплекса, а компонентные регистры доступны только через блок RCRB, Root Complex Register Block. Ядро помечает такие dport признаком rch, и часть регистров статуса линка в этой конфигурации недоступна, поэтому часть счётчиков у портов в таких платформах отсутствует в принципе.

Обход дерева PCI командой lspci и поиск портов по вендору

Второй источник это дерево PCI, и здесь работает lspci. Фильтр по вендору отрезает лишнее моментально:

# lspci -nn -d 19e5:

Идентификатор 19e5 принадлежит Huawei: на серверах с процессорами Kunpeng 920 этот производитель публикует корневые порты CXL. В ответе всплывают строки с классом PCI bridge [0604], каждый из которых кандидат в элемент топологии. Полная схема с иерархией собирается флагом -tv:

# lspci -tv
-[0000:00]-+-00.0
           +-04.0-[04]----00.0

Здесь корневой порт 00:04.0 открывает шину 04, за которой живёт единственное устройство 04:00.0. Дальше класс решает всё: у конечного устройства памяти CXL класс 0502, Memory Device из раздела 8.1.12.1 спецификации CXL 2.0, и по нему драйвер cxl_mem находит свою цель. Детали порта раскрывает lspci -vv -s 04:00.0, где в блоке LnkSta видны скорость и ширина линка, например Speed 32GT/s и Width x16.

Поколение Gen3 несёт 8 GT/s на линию, Gen4 до 16 GT/s, Gen5 до 32 GT/s. Умножение 32 GT/s на долю полезных бит 128 из 130 и деление на 8 бит и 16 линий дают около 63 ГБ/с на порт в каждую сторону. Восемь таких портов дают порядка полутерабайта в секунду, и отставание от планки в тестах подсказывает, где копать.

Устройства CXL узнаются ещё и по блокам DVSEC с идентификатором вендора 1E98, зарезервированным для консорциума CXL: в них спрятаны описания компонентных регистров, по которым ядро отличает CXL-мост от очередного моста.

Файловая система sysfs как точная карта портов и декодеров

Третий источник, sysfs, отдаёт самую свежую иерархию без запуска внешних программ:

# ls /sys/bus/cxl/devices/
decoder1.0  decoder2.0  endpoint3  mem0  nvdimm-bridge0  port1  port2  region0

Схема нумерации строгая: порты называются portX, устройства памяти memX, декодеры decoderX.Y, где X это номер порта-владельца, а Y номер экземпляра. Ссылки связывают всё воедино: uport ведёт к устройству, опубликовавшему порт, ACPI0017, ACPI0016 или апстрим-порту коммутатора, а parent_dport указывает на даунстрим-порт родителя. Линки dport0 и дальше перечисляют даунстрим-цели, и число в имени совпадает с идентификатором в аппаратном списке целей декодера.

Чтение значений сводится к cat:

# cat /sys/bus/cxl/devices/decoder1.0/start
0x8050000000
# cat /sys/bus/cxl/devices/decoder1.0/target_list
0

Атрибуты start и size задают окно адресов, target_list перечисляет цели в порядке чередования, cap_pmem и cap_ram описывают принимаемый тип памяти, а locked показывает, что конфигурация заперта до сброса. Файл decoders_committed из ядра 6.7 отвечает, сколько декодеров реально активны, и число бывает больше нуля без видимых регионов, если окна настроила прошивка или предыдущее ядро после kexec.

У mem0 своя кладовая: serial отдаёт 64-битный номер из PCIe Device Serial Number, numa_node привязывает устройство к узлу NUMA, каталоги ram и pmem показывают размер волатильной и постоянной частей. Сверка серийников с наклейкой на плате спасает от перепутанных слотов.

Снимок всей топологии утилитой cxl list с фильтрами

Четвёртый источник, утилита cxl из пакета ndctl, сводит всё в один JSON; свежие релизы v80 и выше подтягивают новые поля вслед за ядром. Комбинация cxl list -B -M -P печатает сразу корневые объекты, устройства памяти и порты. Максимальная детализация выглядит так:

# cxl list -v

Один флаг -v включает memdevs, regions, buses, ports, decoders и targets, два добавляют простаивающие устройства, три подключают health, partition и media-errors. С фильтрами картина сужается за секунды:

# cxl list -BTu -b ACPI.CXL
{
  "bus":"root0",
  "provider":"ACPI.CXL",
  "nr_dports":1,
  "dports":[
    {
      "dport":"ACPI0016:00",
      "alias":"pci0000:34",
      "id":"0"
    }
  ]
}

Ключ alias переводит объект ядра в привычный PCI-адрес. Вывод cxl list -Mu выглядит скромнее: memdev mem0, pmem_size 256.00 MiB, serial 0 и host 0000:35:00.0. Флаг -u превращает байты в человекочитаемые мегабайты. Фильтры комбинируются логикой И между типами и ИЛИ внутри одного: cxl list -P -p switch -m mem0 покажет порты коммутаторов, через которые достижим mem0. Флаг -vvv добавляет здоровье: cxl list -m mem0 -H возвращает поля вида life_used_percent 15, temperature 25, dirty_shutdowns 10 и предупреждение media_data_lost.

Рабочая сессия профилирования укладывается в четыре шага:

  1. снять каркас командой cxl list -v и сверить число портов с платформой;
  2. наложить его на дерево PCI через lspci -tv и привязать порты к мостам вида 0000:34;
  3. проверить окна и committed-статус декодеров в sysfs, чтобы исключить конфликты адресов;
  4. запустить cxl monitor фоном и включить подписку perf на события CXL на время прогона.

Порядок не прихоть: пока топология не сверена, любая цифра повисает в воздухе, ведь непонятно, к какому порту она относится.

Декодеры HDM и регистры компонентов под контролем ядра

Сердце корневого комплекса это массив декодеров HDM, Host Managed Device Memory, со структурой из раздела 8.2.5.12 спецификации CXL 2.0; местоположение регистров объявляется через DVSEC Register Locator. Ядро маппит компонентные регистры при привязке драйвера и строит объекты decoderX.Y. У корня декодеры статичны: окна и число целей заданы прошивкой через записи CFMWS в таблице CEDT, менять их нельзя, можно лишь строить регионы внутри окон.

Снимок корневого декодера:

"decoder":"decoder3.2",
"resource":"0x8050000000",
"size":"256.00 MiB (268.44 MB)",
"pmem_capable":true,
"nr_targets":1

Адрес 0x8050000000 и окно в 256 МиБ означают, что прошивка отдала декодеру полоску физического пространства под постоянную память. Поле interleave_ways сообщает, скольким целям достанется окно, а interleave_granularity, обычно 256 или 4096 байт, задаёт шаг чередования. На конечных устройствах та же арифметика продолжается на уровне DPA, адреса внутри устройства, и атрибуты dpa_size и dpa_resource у endpoint-декодеров появились в ядре 6.0.

Активация декодера двухэтапная: сначала программная настройка, затем запись в атрибут commit, причём следующий декодер включается только после предыдущего по номеру экземпляра. Регион в состоянии commit означает, что аппаратура уже маршрутизирует адреса, и именно тогда идёт настоящий трафик CXL.mem, который и хочется профилировать. Команды cxl create-region -d decoder0.0 -m mem0, cxl enable-region и cxl destroy-region ведут цикл без ручной возни с регистрами.

События памяти в perf и трассировка ошибок через tracepoints

Топология отвечает на вопрос где, а события на вопрос что происходит. Точка входа это perf:

# perf list | grep -i cxl
  cxl:cxl_aer_correctable_error                      [Tracepoint event]
  cxl:cxl_aer_uncorrectable_error                    [Tracepoint event]
  cxl:cxl_port_aer_correctable_error                 [Tracepoint event]
  cxl:cxl_port_aer_uncorrectable_error               [Tracepoint event]
  cxl:cxl_general_media                              [Tracepoint event]
  cxl:cxl_dram                                       [Tracepoint event]
  cxl:cxl_poison                                     [Tracepoint event]

Пара строк с приставкой port интересна для профилирования именно корневого комплекса: она ловит корректируемые и некорректируемые ошибки на портах и мостах, а не только на устройствах. Запись сессии классическая:

# perf record -e 'cxl:cxl_dram,cxl:cxl_general_media,cxl:cxl_poison' -a -- sleep 60
# perf script

Каждое событие несёт адрес DPA внутри устройства, переведённый адрес HPA, номер канала, ранка и счётчик исправленных ошибок. Расшифровка вменяемая: Memory Data ECC Error в корректируемом списке, Received Poison From Peer в некорректируемом. Лог заголовков вкладывается в запись, и его подхватывает rasdaemon.

Список отравленных страниц достаётся отдельным приёмом: запись true в атрибут trigger_poison_list из ядра 6.4 заставляет драйвер вычитать список и породить события cxl_poison:

# echo true > /sys/bus/cxl/devices/mem0/trigger_poison_list

Источник элемента помечен значениями External, Internal, Injected, Vendor Specific или Unknown, и Injected сразу выдаёт рукотворную проверку. Долгий мониторинг без привязки к сессии perf обеспечивает cxl monitor:

# systemctl start cxl-monitor.service
# cxl monitor --daemon --log=/var/log/cxl-monitor.log

Служба конвертирует события ядра в JSON и пишет в журнал, а --daemon уводит процесс в фон. Аппаратных счётчиков у самих портов в ядре пока нет: драйвер CXL PMU из версии 6.4 обслуживает блоки CPMU устройств Type-3, а счётчики портов в таблице зрелости числятся не начатыми. perf list в паре с grep честно покажет, что появилось в конкретном ядре.

Числа латентности и пропускной способности региона в sysfs

Финальный слой это цифры, и они уже лежат в sysfs. С версии 6.9 у каждого региона есть каталоги access0 и access1 с атрибутами read_bandwidth, write_bandwidth, read_latency и write_latency:

# cat /sys/bus/cxl/devices/region0/access0/read_latency
# cat /sys/bus/cxl/devices/region0/access1/write_bandwidth

Latency даётся в наносекундах, bandwidth в мегабайтах в секунду; значение 400 означает путь до памяти за 400 наносекунд. Каталог access0 описывает дорогу к ближайшему инициатору, access1 к ближайшему ядру CPU, и расхождение подсвечивает разницу потребителей. Источник чисел это таблицы CDAT с записями DSLBIS и SSLBIS плюс HMAT от прошивки; ядро комбинирует координаты по всей иерархии портов, а заметное расхождение с тестами само по себе сигнал.

Снимок региона целиком удобно брать командой cxl list -R:

{
  "region":"region0",
  "resource":277025390592,
  "size":549755813888,
  "type":"ram",
  "interleave_ways":2,
  "interleave_granularity":256,
  "decode_state":"commit",
  "qos_class_mismatch":true
}

Число 549755813888 байт это ровно 512 ГиБ из двух устройств с чередованием по 256 байт. Флаг qos_class_mismatch в значении true выглядит как готовая зацепка: классы качества декодера и устройства не совпали, и платформа предупреждает о деградации. Классы читаются из атрибутов qos_class у decoderX.Y с ядра 6.5 и у memX с 6.8. Регионы с постоянной памятью параллельно видны в мире nvdimm: команда ndctl list -R печатает их с полями size, type pmem и target_node, помогая сверить две подсистемы.

Остаётся помнить, чем история продолжится в ближайших ядрах. Управление ёмкостью устройств DCD, разделяемая память между хостами и счётчики портов числятся в планах, и каждый новый релиз добавит по строке в perf list и по каталогу в sysfs. Тот, кто выстроил профилирование корневого комплекса сейчас, встретит новинки с готовым набором команд.