Есть ошибки, которые кажутся безобидными в журнале, пока не понимаешь, что за ними стоит. Строчка AMD-Vi: Event logged [IO_PAGE_FAULT device=07:00.0 domain=0x000a address=0xbfda0000] говорит ровно об одном: устройство попыталось обратиться к адресу, трансляция которого не была разрешена блоком управления памятью ввода-вывода. Это не просто лог-сообщение. Это точка, где аппаратная защита встречается с программной логикой ядра, и то, как именно ядро на неё реагирует, определяет судьбу как устройства, так и процесса, который за ним стоит.

IOMMU, Input-Output Memory Management Unit, выполняет для устройств ровно ту же работу, что MMU для процессов. Он транслирует адреса, которые использует устройство при DMA, в физические адреса памяти. Пока трансляция проходит по заранее настроенным таблицам страниц, всё тихо. Как только устройство обращается к адресу, для которого отображение отсутствует или явно запрещено, IOMMU фиксирует нарушение и сигнализирует ядру. Что происходит дальше, зависит от типа ошибки, аппаратных возможностей и того, как настроен обработчик.

Два принципиально разных класса ошибок IOMMU

Прежде чем разбирать код, важно понять разницу между двумя типами ошибок, потому что обрабатываются они совершенно по-разному.

Первый тип, неустранимые ошибки (unrecoverable faults). Они возникают, когда устройство нарушает допустимый диапазон адресов необратимо: обратилось к физической памяти, которой у него нет прав касаться, или запросило запись в область, открытую только для чтения. Единственное разумное действие в этом случае, остановить транзакцию и зафиксировать нарушение. Ядро выведет сообщение в dmesg и, в зависимости от политики, может изолировать устройство или инициировать сброс.

Второй тип, устранимые страничные ошибки (recoverable page faults, IOPF). Они принципиально иные. Устройство запросило адрес, трансляция которого ещё не настроена, но может быть настроена. Это типичная ситуация при Shared Virtual Addressing (SVA): устройство работает с виртуальными адресами процесса, и нужная страница просто не была подгружена в момент обращения. Ядро может обработать такую ошибку через стандартный механизм подкачки страниц и разрешить устройству повторить транзакцию.

Некоторые системы позволяют устройствам обрабатывать страничные ошибки IOMMU через механизм mm ядра. Примерами служат системы с расширением PCI PRI или модель stall в Arm SMMU. Именно вокруг этого разделения и построена вся архитектура обработки ошибок.

Структура ошибки и путь от железа до обработчика

Когда IOMMU фиксирует нарушение, он сигнализирует через прерывание. Драйвер IOMMU принимает это прерывание в своём IRQ-обработчике, читает аппаратные регистры состояния и формирует структуру iommu_fault, описывающую произошедшее:

struct iommu_fault {
    u32    type;      /* IOMMU_FAULT_DMA_UNRECOV или IOMMU_FAULT_PAGE_REQ */
    u32    reason;    /* код причины из аппаратного регистра */
    struct iommu_fault_unrecoverable event;   /* для неустранимых */
    struct iommu_fault_page_request  prm;     /* для страничных */
};

Для страничных ошибок поле prm содержит адрес, вызвавший ошибку, идентификатор адресного пространства процесса (PASID), флаги доступа и групповой идентификатор PRG, необходимый для формирования ответа устройству.

Когда IOMMU-драйвер получает событие ошибки, как правило в IRQ-обработчике, он сообщает о ней через iommu_report_device_fault(). Обработчик страничных ошибок затем вызывает обработчик mm и сообщает об успехе или неудаче через iommu_page_response().

Центральный путь выглядит так:

/* В IRQ-обработчике IOMMU-драйвера */
static irqreturn_t my_iommu_fault_irq(int irq, void *dev)
{
    struct iopf_fault event = {};

    /* Читаем аппаратный регистр состояния */
    u64 fault_status = readq(iommu_base + FAULT_STATUS_REG);

    event.fault.type = (fault_status & RECOVERABLE_BIT)
                       ? IOMMU_FAULT_PAGE_REQ
                       : IOMMU_FAULT_DMA_UNRECOV;

    event.fault.prm.addr  = fault_status & FAULT_ADDR_MASK;
    event.fault.prm.pasid = (fault_status >> PASID_SHIFT) & PASID_MASK;
    event.fault.prm.flags = IOMMU_FAULT_PAGE_REQUEST_PASID_VALID;

    iommu_report_device_fault(my_dev, &event);
    return IRQ_HANDLED;
}

После вызова iommu_report_device_fault() ядро берёт управление: проверяет, зарегистрирован ли обработчик на уровне домена (например, VFIO для виртуальных машин), и если нет, передаёт ошибку в очередь IOPF для обработки в рабочем потоке.

Очередь IOPF и обработка в рабочем потоке

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

IOMMU-драйвер может создать несколько рабочих очередей ошибок и связать их с устройствами, поддерживающими IOPF. Драйвер может выбирать между единой глобальной очередью, одной на IOMMU-устройство, одной на домен и другими вариантами.

Создание очереди и добавление устройства выполняется при инициализации IOMMU-драйвера:

static int my_iommu_probe(struct platform_device *pdev)
{
    struct my_iommu *iommu = /* ... */;

    /* Создаём очередь ошибок для этого IOMMU */
    iommu->iopf_queue = iopf_queue_alloc("my-iommu-faults");
    if (!iommu->iopf_queue)
        return -ENOMEM;

    return 0;
}

/* При добавлении устройства к IOMMU */
int my_iommu_attach_device(struct iommu_domain *domain, struct device *dev)
{
    struct my_iommu *iommu = /* получить из dev */;

    /* Связать устройство с очередью ошибок */
    return iopf_queue_add_device(iommu->iopf_queue, dev);
}

Рабочий поток очереди забирает накопленные события, группирует страничные запросы по PRG-идентификатору (потому что устройство может отправить несколько запросов как одну логическую группу) и передаёт группу обработчику домена. Обработчик домена для SVA вызывает стандартный handle_mm_fault(), тот же механизм, что используется при страничных ошибках процессора.

После того как страница подгружена, ядро отправляет ответ обратно устройству через iommu_page_response(). Устройство получает разрешение повторить транзакцию:

/* Внутри обработчика IOPF-группы */
static int my_iopf_handler(struct iopf_group *group)
{
    struct iopf_fault *iopf;
    struct iommu_page_response resp = {};
    int ret = IOMMU_PAGE_RESP_SUCCESS;

    /* Обрабатываем каждый запрос в группе */
    list_for_each_entry(iopf, &group->faults, list) {
        struct vm_area_struct *vma;
        unsigned long addr = iopf->fault.prm.addr;

        /* Стандартная подкачка страницы через mm ядра */
        int fault_ret = handle_mm_fault(vma, addr,
                                        FAULT_FLAG_REMOTE, NULL);
        if (fault_ret & VM_FAULT_ERROR) {
            ret = IOMMU_PAGE_RESP_INVALID;
            break;
        }
    }

    resp.argsz   = sizeof(resp);
    resp.version = IOMMU_PAGE_RESP_VERSION_1;
    resp.grpid   = group->last_fault.fault.prm.grpid;
    resp.code    = ret;

    /* Отправляем ответ устройству */
    iopf_group_response(group, &resp);
    return 0;
}

Обработчик может ответить тремя кодами: IOMMU_PAGE_RESP_SUCCESS для повторной трансляции, IOMMU_PAGE_RESP_INVALID для завершения ошибки, IOMMU_PAGE_RESP_FAILURE для завершения ошибки и прекращения дальнейших страничных запросов.

Аппаратные механизмы PRI и Arm SMMU Stall

Обработка страничных ошибок на аппаратном уровне реализована двумя основными способами, и понимание их различий важно при разработке драйверов.

PCIe Page Request Interface (PRI) позволяет устройствам явно запрашивать подгрузку страниц перед тем, как начать DMA-операцию. Устройство отправляет Page Request Message с адресом и ожидает Page Response от системного программного обеспечения. Только получив подтверждение, оно приступает к передаче данных. Это кооперативная модель: устройство осознанно сигнализирует о своей потребности.

Модель Arm SMMU Stall принципиально иная. Здесь устройство не запрашивает страницу заранее: оно просто приостанавливается (stall) при ошибке трансляции и ждёт, пока программное обеспечение не заполнит таблицы страниц и не даст команду продолжить. С точки зрения аппаратуры, транзакция не отменяется, она замораживается в точке ошибки.

Конфигурация поддержки PRI в Device Tree узла устройства выглядит так:

pcie0: pcie@fc000000 {
    compatible = "vendor,pcie-host";
    /* ... */
    iommu-map = <0x0 &smmu 0x0 0x10000>;
    pasid-num-bits = <20>;
    /* Включаем PRI для этого контроллера */
    ats-supported;
    pri-supported;
};

Проверить, поддерживает ли конкретное устройство PRI, можно через lspci с просмотром расширенных возможностей:

lspci -v -s 03:00.0 | grep -A5 "Page Request"
# Page Request Interface (PRI): PRIEnabled- PRIReset-
# Outstanding PR Capacity: 256

Флаг PRIEnabled- означает, что PRI поддерживается, но пока не активирован. Ядро включает его при настройке домена SVA для устройства.

Обработка неустранимых ошибок Intel VT-d и AMD-Vi

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

При возникновении ошибок IOMMU сигнализирует через прерывание. Причина ошибки и устройство, её вызвавшее, выводятся на консоль.

Типичный вывод Intel VT-d при неустранимой ошибке:

DMAR:[DMA Write] Request device [00:02.0] fault addr 6df084000
DMAR:[fault reason 05] PTE Write access is not set

Здесь fault reason 05 это код из спецификации Intel VT-d: бит записи не установлен в записи таблицы страниц. Расшифровку кодов можно найти в drivers/iommu/intel/dmar.c. Аналогичный вывод для AMD-Vi:

AMD-Vi: Event logged [IO_PAGE_FAULT device=07:00.0 domain=0x000a address=0xbfda0000 flags=0x0000]

Когда таких строчек в журнале появляются тысячи подряд, ядро автоматически подавляет повторяющиеся сообщения через механизм rate limiting:

amd_iommu_report_page_fault: 5235 callbacks suppressed

Это не означает, что ошибки прекратились. Это означает, что ядро перестало их печатать, чтобы не перегружать буфер сообщений.

Диагностику состояния IOMMU на работающей системе выполняют через несколько точек:

# Проверить режим работы IOMMU
cat /sys/kernel/iommu_groups/*/type

# Просмотреть группы IOMMU и принадлежащие им устройства
ls /sys/kernel/iommu_groups/

# Посмотреть свежие ошибки IOMMU в реальном времени
dmesg -w | grep -i iommu

# Проверить, включён ли IOMMU в принципе
dmesg | grep "IOMMU enabled\|DMAR\|AMD-Vi" | head -20

Параметры ядра для управления политикой ошибок

Поведение ядра при ошибках IOMMU настраивается параметрами командной строки. Это особенно актуально при отладке: иногда нужно временно ослабить защиту, чтобы понять, откуда берётся ошибка.

Основные параметры для Intel VT-d:

# Отключить IOMMU полностью (только для отладки)
intel_iommu=off

# Режим passthrough: не транслировать адреса, но сохранить защиту прерываний
intel_iommu=pt

# Отключить IOMMU только для встроенной графики если возникают проблемы
intel_iommu=igfx_off

Для AMD-Vi:

# Отключить
amd_iommu=off

# Принудительная изоляция всех устройств, без послаблений от драйверов
amd_iommu=force_isolation

# Включить сброс TLB при каждом unmap (медленнее, но безопаснее при отладке)
amd_iommu=fullflush

Параметр iommu=pt (passthrough) часто используют как временное решение при диагностике: если ошибки IOMMU исчезают в этом режиме, причина однозначно в некорректных DMA-маппингах в драйвере устройства, а не в аппаратной конфигурации.

Регистрация пользовательского обработчика и интеграция с VFIO

Для сценариев виртуализации, где устройство передано виртуальной машине через VFIO, страничные ошибки IOMMU нужно перенаправить в гостевую ОС, а не обрабатывать в хостовом ядре. Для этого на уровне домена регистрируется пользовательский обработчик через поле iopf_handler структуры iommu_domain_ops:

static int vfio_iommu_iopf_handler(struct iopf_group *group)
{
    struct vfio_iommu *iommu = group->domain->iommu_data;

    /*
     * Передаём событие в гостевую ОС через механизм
     * виртуальных прерываний или shared memory
     */
    return vfio_deliver_fault_to_guest(iommu, group);
}

static const struct iommu_domain_ops vfio_iommu_domain_ops = {
    .iopf_handler = vfio_iommu_iopf_handler,
    /* ... */
};

Когда такой обработчик зарегистрирован, iommu_report_device_fault() вызывает его напрямую, минуя системную очередь IOPF. Гостевая ОС получает событие, обрабатывает его в своём контексте и возвращает ответ через VFIO-интерфейс, который затем формирует Page Response и отправляет его устройству.

Именно этот слой, от аппаратного прерывания через iommu_report_device_fault() до iommu_page_response() или вывода в dmesg, определяет, насколько надёжно система защищает память от ошибочных или злонамеренных DMA-операций устройств. Неустранимая ошибка фиксирует нарушение и останавливает транзакцию. Устранимая даёт системе шанс исправить состояние таблиц страниц и продолжить работу. Разница между этими двумя путями не в тяжести ошибки, а в том, предусмотрел ли разработчик железа и программного стека возможность восстановления.