Память на шине CXL редко приходит в систему привычной планкой: контроллер сначала регистрируется как PCI-устройство, и только после этого ядро собирает из него объекты отдельной шины CXL. Между этими шагами работает цепочка из прошивки, таблиц ACPI, драйверов и декодеров адресов, и каждый ее этап можно не только наблюдать, но и измерять. Администратор, который видит путь от таблицы CEDT до появления устройства mem0 в sysfs, быстрее находит причину пропавшей памяти и отличает медленную прошивку от медленного драйвера. Дальше разобран полный маршрут обнаружения, команды чтения топологии и разметки и приемы замера времени через журналы, udev, ftrace, perf и bpftrace.
Цепочка обнаружения от PCI-перечисления до появления memdev на шине CXL
Устройство памяти CXL Type 3 до конца процедуры выглядит как обычное конечное устройство PCIe, поэтому обнаружение начинается с классического перечисления PCI. Контроллер объявляет код класса 0508, который расшифровывается как CXL Memory Device, и именно по нему драйвер cxl_pci находит свою цель. Проверить класс проще всего так:
lspci -nn -s 0000:0c:00.0
В квадратных скобках вывода будет код 0508, а не 0580 из семейства прочих контроллеров памяти, и это первый признак расширителя CXL. Спецификация CXL описывает базовые возможности в разделе 8.1.12.1 про заголовок PCI устройства памяти, включая команды почтового ящика Identify Memory Device и Get Partition Info. Полный путь устройства в ядре выглядит так:
- PCI-ядро перечисляет шину и создает объект pci_dev для адреса вроде 0000:0c:00.0;
- драйвер cxl_pci связывается с устройством по коду класса 0508;
- драйвер читает блоки DVSEC и по регистровому указателю находит блок регистров компонента, спроецированный на BAR;
- настраивается почтовый ящик в MMIO, и устройству уходят команды Identify Memory Device и Get Partition Info;
- ядро регистрирует memdev, в системе появляются /sys/bus/cxl/devices/mem0 и символьное устройство /dev/cxl/mem0;
- драйвер поднимается по PCIe-иерархии, и ядро строит объекты портов для корня, мостов и коммутаторов на пути;
- на каждом уровне перечисляются декодеры HDM, после чего устройство получает привязку к региону с отображением SPA в DPA.
Основную часть времени обнаружения дают шаги с третьего по пятый, где драйвер читает регистры и ждет ответов почтового ящика, а шаги с шестого по седьмой разрастаются на топологиях с коммутаторами. Подсистема появилась в ядре 5.14 и дозрела к сериям 6.x.
Таблица CEDT от прошивки и разбор структур CHBS и CFMWS
Прошивка платформы готовит таблицу CEDT, CXL Early Discovery Table, еще до загрузки системы: в ней описаны мосты, через которые видно CXL, и фиксированные окна памяти, размеченные BIOS. Таблица состоит из подструктур двух типов: CHBS с типом 00 описывает хост-мосты, CFMWS с типом 01 описывает окна памяти. Начать знакомство удобно с дампа:
ls /sys/firmware/acpi/tables/ | grep -i cedt
acpidump -t CEDT | less
Типовая запись CHBS занимает 32 байта: Associated host bridge UID 00000007, Specification version 00000001, Register base 0000010370400000 и Register length, равный 65536 байтам, то есть 64 КиБ. По базовому адресу драйвер cxl_acpi находит регистры компонента моста, а UID 7 дальше фигурирует в других структурах как ссылка на этот же мост.
Запись CFMWS объемнее, 44 байта, и в типовом дампе выглядит так: Window base address 000000C050000000, Window size 0000003CA0000000, Interleave Members (2^n) 01, Granularity 00000000, Restrictions 0006, QtgId 0001, список целей из UID 7 и UID 6. Значение 01 в поле Interleave Members означает два моста в одном окне. Размер окна 0x3CA0000000 составляет 242.5 ГиБ, и при двух участниках каждый мост покрывает по 121.25 ГиБ, а ноль в поле Granularity кодирует шаг чередования в 256 байт. Поле Restrictions расшифровывается по битам: бит 0 означает память Type 2, бит 1 память Type 3, бит 2 энергозависимую память, бит 3 энергонезависимую, бит 4 фиксированную конфигурацию без повторного использования адресов. Значение 0x0006 складывается из битов 1 и 2, то есть окно отдано энергозависимой памяти Type 3. Чередование между устройствами внутри одного моста в CFMWS не описывается: оно существует только в декодерах моста и устройств. Идентификатор QtgId 0001 связывает окно с классом обслуживания, видимым позже в поле qos_class корневого декодера.
Иерархия портов, мостов и коммутаторов, которую собирает ядро
Вершиной декодирования служит корень, в терминах ядра это cxl root, ниже стоят порты-мосты, затем корневые порты PCIe, коммутаторы и конечные устройства. Каждый коммутатор на пути добавляет точку, где чередование может разветвляться: прошивка направляет окно на один мост, мост распределяет циклы по корневым портам, а коммутатор делит их еще раз между нижестоящими портами. Объекты портов появляются только после привязки cxl_pci к устройству памяти: до этого момента портовых объектов в системе нет, а при удалении устройства его метод remove убирает и портовые данные. У порта есть глубина depth, у подчиненного порта dport есть признак режима RCH против VH и латентность линии в пикосекундах, а таблица CDAT кешируется рядом с портом.
Собранная иерархия видна в sysfs:
ls /sys/bus/cxl/devices/
decoder0.0 decoder1.0 mem0 port1 port2 port3
Минимальная конфигурация без коммутатора дает корневой порт, пару портов и декодеры. Модули проверяются мгновенно:
lsmod | grep cxl
В списке ожидаются cxl_core, cxl_acpi, cxl_pci, cxl_port, cxl_pmem и cxl_mem, причем отсутствие cxl_acpi означает, что таблица CEDT вообще не была разобрана. Для тренировки без железа модуль cxl_test генерирует эмулированную топологию из двух мостов, четырех корневых портов и восьми конечных устройств, а команда cxl list -BEMPu -b cxl_test печатает эту иерархию разом, от шины до memdev.
Утилита cxl list и команда cxl discover-partitions для чтения разметки
Базовый запрос списка устройств выглядит так:
cxl list -Mu -m mem0
[
{
"memdev":"mem0",
"pmem_size":"68719476736 (64.00 GiB)",
"ram_size":"206158430208 (192.00 GiB)",
"serial":"0x5d1a2b3c",
"numa_node":2
}
]
Флаг -M запрашивает memdev, -u переводит байты в человекочитаемые единицы, -m фильтрует вывод по имени. Сумма разделов равна емкости устройства: 192 ГиБ плюс 64 ГиБ дают 256 ГиБ адресного пространства DPA. Декодеры именуются по схеме decoder с номером порта и экземпляра, и фильтр по шине показывает их ресурсы:
cxl list -BDMu -d root -m mem3
{
"bus":"root3",
"decoders:root3":[
{"decoder":"decoder3.1","resource":"0x8030000000",
"size":"512.00 MiB (536.87 MB)","volatile_capable":true,"nr_targets":2},
{"decoder":"decoder3.2","resource":"0x8050000000",
"size":"256.00 MiB (268.44 MB)","pmem_capable":true,"nr_targets":1}
]
}
Имя decoder3.1 читается как порт 3, экземпляр 1, поле resource показывает базу окна в адресах HPA, флаги volatile_capable и pmem_capable говорят о типах памяти, а nr_targets равное 2 означает окно на два пути. Обратный запрос cxl list -MDu -d 3.2 перечисляет устройства, способные участвовать в схеме декодирования 3.2. Активная разметка опрашивается отдельной командой:
cxl discover-partitions mem0
{
"memdev":"mem0",
"pmem_size":"68719476736 (64.00 GiB)",
"volatile_size":"206158430208 (192.00 GiB)"
}
Команда отправляет в почтовый ящик запрос Get Partition Info и возвращает живую разметку DPA: энергозависимая часть считается от нулевого адреса, за ней лежит раздел pmem. Граница выравнивается по полю partition_align_bytes из ответа Identify Memory Device, поэтому шаг переразметки согласуется с прошивкой.
Замер времени обнаружения по журналам ядра и событиям udev
Самый быстрый замер дает журнал ядра:
journalctl -b -k | grep -i cxl
dmesg -T | grep -i cxl
journalctl -b -k -o short-monotonic | grep -i cxl
Разница меток между связыванием cxl_pci с адресом 0000:0c:00.0 и появлением mem0 дает длительность пробы, а формат short-monotonic дает монотонное время от старта ядра, удобное для вычитания. Для времени инициализации подсистемы в параметры ядра добавляется ключ initcall_debug, после чего в журнале появляются строки с длительностью каждого инициализатора вида returned 0 after 4523 usecs, и по cxl_acpi_init и соседям видно, сколько микросекунд стоило чтение таблиц ACPI. Горячее подключение наблюдается в реальном времени:
udevadm monitor --kernel --subsystem-match=cxl
Монитор печатает события add, bind и change для memdev и портов, и интервал между add и bind показывает, сколько времени ядро потратило на пробу после опознания устройства. Типичные значения выглядят так: проба одного конечного устройства без коммутатора укладывается в десятки миллисекунд, топология с двумя мостами и коммутаторами растягивается до сотен миллисекунд, а долгие паузы почти всегда означают ожидание прошивки. Если почтовый ящик не отвечает в пределах лимита, драйвер получает ошибку -ETIMEDOUT, и эта строка прямо указывает на прошивку устройства как на узкое место. Полное отсутствие строк cxl означает, что модули не загружены или таблица CEDT отсутствует; проверять тогда надо lsmod и дамп таблиц.
Профилирование пробы драйвера через ftrace, perf и bpftrace
Журнал дает границы этапов, а для внутренней детализации служит ftrace:
cd /sys/kernel/debug/tracing
echo '*cxl*' > set_ftrace_filter
echo function_graph > current_tracer
echo 1 > tracing_on
После подключения устройства или перезагрузки модуля трассировка наполняется вложенными вызовами функций cxl с длительностью каждой ветки. Ветки дольше 10 микросекунд помечаются плюсом, дольше 100 микросекунд восклицательным знаком, и по этим меткам удобно находить самые тяжелые участки. Закончив наблюдение, фильтр сбрасывают, а в current_tracer возвращают nop.
Точечный замер длительности пробы дает perf:
perf probe -a cxl_pci_probe
perf probe -a 'cxl_pci_probe%return'
perf record -e probe:cxl_pci_probe -e probe:cxl_pci_probe__return -aR sleep 20
perf script
Пары событий входа и выхода дают длительность каждой пробы в системе. Тот же результат одной строкой печатает bpftrace:
bpftrace -e 'kprobe:cxl_pci_probe { @t[tid] = nsecs; } kretprobe:cxl_pci_probe /@t[tid]/ { printf("probe: %d us\n", (nsecs - @t[tid]) / 1000); delete(@t[tid]); }'
Внутри затянувшейся пробы первым кандидатом на проверку оказывается ожидание почтового ящика: ответ на одну команду стоит у прошивки единицы миллисекунд, а десяток команд чтения емкости и меток уже дают ощутимую долю времени обнаружения. Разница между прошивками одного поколения на одном железе измеряется разами, а не процентами, поэтому профилирование пробы часто заканчивается обновлением прошивки, а не перенастройкой ядра.
Числовой пример раскладки окна памяти и типичные значения этапов
Раскладку адресов удобно считать на окне в 16 ГиБ с четырьмя участниками и гранулярностью 256 байт: каждый из четырех модулей получает по 4 ГиБ, а последовательные блоки по 256 байт распределяются по кругу. Для смещения X внутри окна номер цели равен частному от деления X на 256 по модулю 4, а позиция внутри устройства считается как частное от деления X на 1024, умноженное на 256, плюс остаток от деления X на 256. Для X, равного 70000, частное 273 делится на 4 с остатком 1, то есть блок уходит в первую цель, а позиция в DPA составит 68 умножить на 256 плюс 112, что дает 17520 байт. Проверка сходится и с другой стороны: 70000 это 68 на 1024 плюс 368, а 368 это 256 плюс 112, то есть цель снова 1 и смещение 112.
Для быстрой сверки полезен такой ориентир: регистровый блок моста в CHBS занимает 64 КиБ, окно из примера выше покрывает 242.5 ГиБ на два моста по 121.25 ГиБ, устройство на 256 ГиБ делится на 192 ГиБ volatile и 64 ГиБ pmem, а декодер 3.1 с ресурсом 0x8030000000 отдает окно 512 МиБ на два пути. Если реальный вывод сильно расходится с ориентиром, подозревать стоит прошивку: она задает окна, участников и классы обслуживания, а ядро лишь переносит эти решения в свои объекты.
Типичные отказные сценарии распознаются по паре признаков: memdev появился, а портов в списке нет, значит привязка cxl_pci сорвалась на классе устройства или его перехватил другой драйвер; порты есть, а регион не собрался, значит прошивка и конфигурация разошлись в гранулярности или числе участников; строка -ETIMEDOUT в журнале переводит подозрение на прошивку устройства. Связка из acpidump, cxl list и трассировки закрывает весь цикл: первая показывает решения прошивки, вторая собранную топологию, а трассировка измеряет цену каждого шага в миллисекундах.