Копирование памяти выглядит безобидной операцией, пока речь не заходит о терабайтах в сутки: каждое memcpy съедает такты ядра, вымывает кеш и конкурирует с полезной работой за полосу памяти. DMA-движки снимают эту работу с процессора: они переносят блоки сами, а ядро только программирует передачу и ждёт завершения. В Linux подсистема dmaengine унифицирует работу с движками от старого ioatdma до современного Intel DSA, а утилита accel-config и модуль dmatest дают полный контроль и честные замеры.

DMA-движки в системе, от ioatdma до Intel DSA

Исторически первым массовым движком был ioatdma, I/O Acceleration Technology, встроенный в чипсеты Xeon E5 и позже. Драйвер ioatdma живёт в ядре давно, каналы видны в /sys/class/dma, и он до сих пор обслуживает копирование в подсистеме async_tx на многих серверах.

Современный этап начался с Intel DSA, Data Streaming Accelerator, встроенного ускорителя копирования, CRC и сравнения данных в Sapphire Rapids. DSA подключён не через PCIe, а внутренней фабрикой процессора, но с точки зрения ядра это обычный движок dmaengine с драйвером idxd, который появился в ядре 5.12 и развивается дальше. По PCIe живут DMA-движки карт: FPGA-ускорители, SmartNIC, NVMe-накопители со своими PRP и SGL, и для них dmaengine тоже предоставляет единый интерфейс, когда производитель реализует драйвер.

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

Проверка движков в системе и включение каналов

Первый шаг, увидеть железо. Список каналов даёт каталог:

ls /sys/class/dma/

Каналы ioatdma называются dma0chan0, dma0chan1 и дальше, DSA-каналы появляются как отдельные устройства после включения очередей. Что за движок за каналом, подсказывает dmesg:

dmesg | grep -i -E "ioat|idxd|dsa"

В выводе видны инициализация драйвера, число каналов и версии. Загруженность каналов без нагрузки легко проверить чтением счётчиков канала, где копятся число передач и перенесённые байты.

Отдельно стоит помнить, что ядро само использует ioatdma, когда подсистема async_tx находит свободный канал: MD RAID при пересборке массивов и некоторые криптографические пути перекладывают копирование на движок автоматически. Для чистых замеров это фоновое использование иногда отключают, выгружая модуль или связывая приложение с конкретным каналом, иначе полоса канала делится между бенчмарком и RAID.

Для DSA важен второй слой, очереди WQ, Work Queues. Устройство dsa0 содержит очереди wq0.0, wq0.1 и дальше, каждая очередь относится к режиму dedicated или shared. Увидеть состояние:

ls /sys/bus/dsa/devices/
cat /sys/bus/dsa/devices/dsa0/wq0.0/state

Состояние disabled означает, что очередь ещё не включена, и работать с ней нельзя, пока не пройдёт настройка через accel-config.

Модуль dmatest, честный замер каналов копирования

Ядро несёт модуль dmatest, который прогоняет каналы через серию копирований со случайными данными и проверкой содержимого. Базовый запуск:

modprobe dmatest
echo dma0chan0 > /sys/module/dmatest/parameters/channel
echo 1000 > /sys/module/dmatest/parameters/iterations
echo 1 > /sys/module/dmatest/parameters/run

Модуль выполняет итерации и печатает в dmesg результаты с временем и полосой. Полезные параметры: bufsize задаёт размер блока, timeout предохранитель на зависшую передачу, max_channels ограничивает параллелизм, а iterations управляет длительностью. Сравнение каналов между собой и с CPU memcpy занимает минуты:

dmesg | tail -20

Типичная картина на сервере: каналы ioatdma выдают полосу в единицы гигабайт в секунду на передачу, что ниже AVX-256 memcpy на горячих кешах, но выигрыш появляется на больших блоках и при освобождении ядра. Главное правило замера: сравнивать на одинаковых размерах и с учётом NUMA-привязки, иначе полоса смешается с латентностью удалённой памяти.

Полезные параметры модуля сверх базовых: verify включает контрольную сверку скопированного, pattern задаёт шаблон заполнения, verbose поднимает детальность вывода, а nowait позволяет начать тест без ожидания готовности каналов. Для статистически устойчивых цифр берут сотни итераций и несколько размеров блока, от килобайта до мегабайт, и строят кривую полосы от размера: она сразу показывает, где движок догоняет memcpy, а где проигрывает.

Intel DSA и утилита accel-config

DSA требует настройки очередей до использования. Утилита accel-config из одноимённого пакета делает это одной командой:

accel-config list
accel-config enable-wq dsa0/wq0.0

После enable очередь переходит в состояние enabled, и в /dev появляется символьное устройство /dev/dsa/wq0.0, через которое пользователи или библиотеки дают команды. Полная настройка сохраняется в JSON и восстанавливается:

accel-config load-config -c /etc/accel-config/dsa0.json

В конфигурации задают размер очереди, приоритет, привязку к группе движков и режим работы. Режим dedicated даёт очереди монопольный движок, режим shared позволяет нескольким процессам делить очередь через ENQCMD, инструкцию из SVT, Shared Virtual Addressing, где процессы дают команды напрямую со своими таблицами страниц. Shared-режим удобен для многопроцессных сервисов, dedicated для изолированных тяжёлых потоков.

Библиотека libaccel-config и обёртки позволяют отправлять операции memcpy, compare, fill, CRC32C и diff-check из приложения. Операции покрывают типовые задачи хранилищ: сравнение страниц при дедупликации, заполнение нулями, контрольные суммы без процессора.

Дескрипторы DSA поддерживают пакетирование: один дескриптор-пакет ссылается на серию операций, и движок выполняет их подряд без участия процессора. Для хранилищ, где сравнение и копирование идут пачками, пакетирование сокращает число постановок в разы. Завершение настраивается гибко: прерывание на последнюю операцию, опрос Completion Record в памяти или командныйComplete режим, где библиотека сама дожидается результатов. Для латентностных путей берут опрос, для фоновых копий прерывание, чтобы не жечь ядро циклом ожидания.

Каналы, прерывания и NUMA-привязка

Каналы DMA генерируют прерывания завершения, и их раскладка по ядрам влияет на задержку. Список прерываний движка:

grep -i -E "ioat|idxd" /proc/interrupts

Привязка выполняется тем же способом, что и для сетевых векторов, записью в smp_affinity_list каталога /proc/irq/<номер>. Ядро, обслуживающее завершение, стоит держать на NUMA-узле памяти: перенос дескрипторов между узлами удлиняет и постановку, и завершение.

Приложение тоже привязывают: numactl --cpunodebind=0 --membind=0 для потока, который ставит копирования, гарантирует, что исходные буферы лежат рядом с движком. Для DSA это критично, ведь движок принадлежит конкретному сокету, и работа с памятью чужого узла съедает выигрыш от выгрузки.

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

Глубину очереди канала подбирают под профиль: для фоновых массовых копий хватает десятков outstanding дескрипторов, для сервисов с частыми короткими операциями нужны сотни, чтобы постановка не упиралась в заполненное кольцо. Очередь DSA настраивается полем size при конфигурации WQ, и суммарная глубина очередей устройства ограничена возможностями железа, что видно в accel-config list. Распределение глубин между очередями делается по важности: критичный сервис получает dedicated очередь с нужной глубиной, фоновые делят shared.

Ещё один практический приём, объединение прерываний: если движок поддерживает режим завершения пакетами, прерывание ставят на последнюю операцию пачки, и одно прерывание закрывает десятки передач. Для ioatdma тот же эффект достигается группировкой постановок в одном цикле обработки, где приложение отдаёт список передач, а завершение опрашивает по хвосту кольца.

Сравнение с CPU memcpy и границы выгоды

DMA выигрывает не всегда, и граница известна из практики. На маленьких блоках до килобайта накладные расходы постановки дескриптора превышают само копирование, и CPU быстрее. На средних блоках в мегабайтах полоса сравнивается с memcpy, но DMA освобождает ядро и кеш, и суммарная пропускная способность сервиса растёт. На гигабайтных блоках и в фоновых сценариях вроде миграции памяти или синхронизации реплик DMA доминирует, потому что процессор вообще не трогает данные.

Классические применения: копирование страниц в виртуализации, репликация WAL в базах, синхронизация кешей, дедупликация с операцией compare, генерация нулевых страниц. В хранилищах DSA берёт на себя CRC32C и сравнение при запись-в-зеркало. Замер всегда ведут на своём железе и своих размерах блоков, полоса зависит от поколения процессора и конфигурации очередей.

Отдельная тонкость связана с кешем: копирование CPU прогревает кеш полезными данными, а DMA может обойти кеш. Если данные сразу нужны тому же ядру, memcpy иногда быстрее суммарно. Если данные уйдут на диск или в сеть, bypass кеша наоборот экономит полосу L3.

Контекст использования важен и в хранилищах. NVMe-накопители переносят данные собственными DMA через PRP или SGL, и хостовый движок в основном пути ввода-вывода им не помощник: его место в подготовке, репликации и дедупликации. Сети добавляют ещё одну грань: RDMA-адаптеры тоже копируют сами, и выгружать туда же CPU-копирование бессмысленно. Поэтому DMA-движки живут в слое между: подготовка данных для сетей и дисков, фоновые копии, проверки целостности.

Включённый IOMMU добавляет к DMA трансляцию адресов, и на горячих путях это заметно. Параметр intel_iommu=pt переводит трансляцию в сквозной режим для идентичных отображений, снимая цену переводов там, где изоляция не нужна. Для карт, поддерживающих ATS, переводчики кешируют адреса на стороне устройства, и повторные передачи идут без штрафа. Состояние IOMMU видно в dmesg и в /sys/kernel/iommu_groups.

Типичные ошибки и диагностика DMA-нагрузок

  1. постановка копирований на канале чужого NUMA-узла, полоса падает, проверка привязки через numactl --show и привязка буферов на узел движка;
  2. попытка работать с очередью DSA в состоянии disabled, ошибка доступа, включение через accel-config enable-wq;
  3. забытый modprobe dmatest при замерах, каналы не тестируются, проверка lsmod;
  4. слишком маленькие блоки, накладные расходы постановки съедают выигрыш, замеры показывают преимущество memcpy, размер блоков поднимают выше мегабайта;
  5. прерывания завершения на перегруженном ядре, задержки растут, привязка irq в /proc/irq/<номер>/smp_affinity_list.

Диагностика опирается на dmesg, счётчики каналов в /sys/class/dma, вывод accel-config list и dmatest. Для DSA счётчики очередей показывают число выполненных операций и ошибки, а библиотечные вызовы возвращают коды, по которым видно переполнение очереди.

Ошибки самого движка ловятся в dmesg по строкам SWERR и COMPLETION: программная ошибка дескриптора указывает на неверный адрес или ключ, аппаратная на перегрев или сбой питания домена. Лечится сбросом очереди командой accel-config reset-wq dsa0/wq0.0 или полный reset устройства, после чего конфигурацию перезагружают из JSON. Зависший канал ioatdma сбрасывается через выгрузку и загрузку модуля ioatdma, но это затрагивает все каналы, поэтому в продакшене предпочитают аккуратный пул каналов и мониторинг зависаний по счётчикам передачи.

Правильная настройка DMA превращает фоновые копии в невидимую работу: ядра свободны, кеш не вымыт, полоса памяти разгружена. Порядок внедрения короткий: увидеть каналы, включить очереди, замерить dmatest на своих блоках, привязать прерывания и потоки по NUMA, и вынести в accel-config конфигурацию для перезагрузки. Один вечер настройки обычно освобождает пару ядер на сервере с тяжёлым фоновым копированием.