Память за шиной 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.
Рабочая сессия профилирования укладывается в четыре шага:
- снять каркас командой cxl list -v и сверить число портов с платформой;
- наложить его на дерево PCI через lspci -tv и привязать порты к мостам вида 0000:34;
- проверить окна и committed-статус декодеров в sysfs, чтобы исключить конфликты адресов;
- запустить 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. Тот, кто выстроил профилирование корневого комплекса сейчас, встретит новинки с готовым набором команд.