QEMU позволяет эмулировать работу не только одной отдельной машины, но и связывать несколько независимых машин между собой. Для организации такой связи их обычно объединяют в одну сеть, например с использованием virio-net-pci. Но виртуальный ethernet — не единственный способ, связь может быть и более близкой и плотной: общая память и mailbox, линии gpio и даже NTB.
Быстрая работа связанных QEMU машин приятна при разработке/отладке и очень важна при массовом прогоне автотестов в CI: нужна как высокая пропускная способность, так и низкая задержка передачи сообщения. Для одной из задач с коллегами из отдела системного программирования YADRO я оптимизировал часть такой связки, а именно — обмен прерываниями. В статье расскажу о дизайне нескольких подходов организации IQI, разберу внутреннее устройство QEMU и поделюсь, как оправдались наши ожидания.
Моя команда занимается задачами программного связывания CPU с CPU, CPU с устройствами, а также устройств с устройствами. Используются как медленные, но надежные шины, так и быстрые, но капризные. Больше всего нам нравятся задачи в стиле «Здесь водятся драконы», когда информацию приходится собирать буквально по крупицам, а иногда и быть первопроходцами.
Содержание
Введение
Есть группа задач, которые хорошо решаются с применением QEMU:
Разработка ОС, в том числе драйверов, загрузчиков и специфичного архитектурного кода (например, текущая разработка, связанная с RISC-V, где многие функциональные модели QEMU появились даже раньше, чем их прототипы на ПЛИС).
Тестирование, изучение и анализ.
Разработка и отладка любого уровня коммуникаций (кроме, пожалуй, физического).
Конечно, на этом список не заканчивается, но мы поговорим о значительной части последнего пункта. В качестве простого примера коммуникаций хорошо подойдет фреймворк OpenAMP. Для нормального функционирования ему достаточно небольшого участка общей между двумя процессорами памяти и двух прерываний, идущих со стороны каждого процессора друг к другу. Обычно эта схема применяется для общения двух разных процессоров в рамках одного SoC (например, общение между OS Zephyr на Cortex-M4 с OS Linux на Cortex-A7 в чипе NXP imx7d). Часто, по ряду причин, мы не можем запустить единую машину QEMU сразу со всем многообразием процессоров в SoC — например из-за разной битности.
Другим примером может служить ivshmem — виртуальное устройство, предоставляемое QEMU, с готовыми драйверами в OS Zephyr, OS Linux и др. Изначально оно создавалось для коммуникаций между машинами QEMU, состоит из разделяемой между машинами памяти и некоторого количества прерываний между ними.
Цель статьи — описать методы обмена примитивными событиями (гость может заказать возникновение прерывания в другом госте) из первой машины, которую мы далее называем QEMU0, во вторую машину, соответственно называемую QEMU1, и наоборот.
Организовать такой обмен (IQI) можно тремя способами. Каждый быстрее предыдущего, но сложнее в реализации:
Запрос события машиной QEMU0 после записи ее гостем в соответствующий регистр, прием машиной QEMU1 и выставление прерывания в ее госте.
Плюсы: универсальность — архитектуры гостей и хоста могут быть разными, нет требований к аппаратной поддержке виртуализации, относительная простота реализации.
Минусы: самый медленный из способов.
Посылка события машиной QEMU0 после записи ее гостем в соответствующий регистр, выставление прерывания напрямую в гостевом ядре, без участия машины QEMU1 (средствами KVM).
Плюсы: оптимизированная половина тракта позволяет сократить задержку между отправкой события и началом его обработки гостем-приемником.
Минусы: требуется поддержка KVM, и, как следствие, архитектуры гостей и хоста должны совпадать.
Посылка события гостевым ядром напрямую (без участия машины QEMU0), прием прерывания в гостевом ядре (без участия машины QEMU1).
Плюсы: самый быстрый из методов.
Минусы: те же, что в предыдущем способе, к тому же мы теряем контроль над маскированием и статусом прерываний.
В самом первом случае мы полностью передаем механизм обмена моделям QEMU. Соответственно, в данном контексте скорее правильнее говорить об обмене событиями, а не прерываниями, так как прерывания здесь вторичны и являются реакцией на события.
Последовательность отправки прерывания:
Гостевое ядро QEMU1 производит запись в регистр.
Осуществляется выход ядра из вызова
ioctl(KVM_RUN)
в QEMU1 с причинойKVM_EXIT_MMIO
.Производится отправка события в QEMU2.
Вызывается прерывание для гостевого ядра QEMU2 функцией
msix_notify()
.
Упрощенно это можно представить в следующем виде:
Во втором случае мы исключаем из цепочки QEMU2, т.к. отправка запроса прерывания идет непосредственно в гостевое ядро средствами KVM, а в третьем исключаем еще и QEMU1 — запрос прерывания идет непосредственно из гостевого ядра. Таким образом, в третьем случае обмен прерываниям происходит исключительно методами KVM.
При этом в качестве гостевой ОС мы тоже используем Linux, но, вообще говоря, это не обязательно. Обязательное условие — Linux с поддержкой KVM в качестве ОС хоста с определенными KVM Capabilities. Для простоты будем считать, что методы 2 и 3 демонстрационного проекта работают только под x86_64.
Результирующий код, как модели QEMU, так и модуля ядра Linux, способен использовать все три метода, переключаемые через аргументы запуска машины. Тем не менее мы разделили изменения на стадии для более простого восприятия материала.
Короткая справка про IPC
Запущенная машина QEMU видна в системе как процесс, а значит, для обмена между машинами, а также между машинами и KVM, нужно воспользоваться какими-то IPC.
В QEMU уже можно использовать некоторые фундаментальные особенности POSIX IPC, в том числе уникальные для Linux, без которых взаимодействие между машиной и ядром было бы сильно затруднено.
UNIX Socket
Главная особенность сокета, способность передавать файловые дескрипторы другим процессам в системе, в основном применяется для передачи eventfd и memfd.
В QEMU API есть ряд ограничений:
за раз не может быть передано больше, чем
TCP_MAX_FDS (16)
,нельзя отправить только файловые дескрипторы, необходима ненулевая запись в socket.
Если такие ограничения не смущают, то можно использовать функции, предоставленные QEMU для работы с UNIX-сокетами:
qemu_chr_fe_set_msgfds()
,qemu_chr_fe_get_msgfds()
.
eventfd
Важная для нас особенность — возможность передавать дескрипторы ядру Linux в KVM-режиме, что обеспечивает доставку прерываний напрямую в гостевое ядро, минуя прослойку в виде QEMU. В остальном, если операционная система не поддерживает eventfd, QEMU использует pipe для эмуляции поведения eventfd. Поэтому для обмена прерываниями лучше всего использовать предоставленный API:
event_notifier_init()
,event_notifier_init_fd()
,event_notifier_set_handler()
,event_notifier_set()
,event_notifier_test_and_clear()
.
В общем, если использовать функции QEMU, то передача будет более универсальной под многие случаи.
Реализация модели и драйвера
Я постарался максимально разделить стадии для большей наглядности, чтобы по истории было видно — когда, что и в каком коммите было добавлено.
Модель для QEMU:
stage 0 — базовая модель для проверки загрузки модуля ядра,
stage 1 — добавление единственного прерывания MSI-X и вектора состояний для входов,
stage 2 — реализация обмена состояниями входов/выходов поверх eventfd,
stage 3 — отправка прерываний непосредственно в гостевое ядро, минуя QEMU,
stage 4 — отправка прерываний непосредственно из гостевого ядра, минуя QEMU.
В итоговой модели у нас есть возможность выбрать режим работы аргументами командной строки при запуске.
Модуль ядра Linux:
stage 0 — модуль MAILBOX с функционалом без прерываний,
stage 1 — добавление поддержки единственного прерывания MSI-X и вектора прерываний,
stage 2 — добавление поддержки множественных векторов прерываний MSI-X, где каждому событию появления данных или приема данных противоположной стороной соответствует отдельное прерывание.
Во второй стадии у нас происходит серьезная модификация модуля ядра драйвера — она обусловлено тем, что прекращается обмен состояниями между экземплярами QEMU, а модули общаются «напрямую», минуя прослойку. Это означает, что у нас нет информации о состоянии ISTATUS, TXDONE и мы не можем контролировать прерывания с помощью IEN.
Подготовительная работа
Описание проекта-обертки
Компоненты:
qemu (v9.0.0 — c патчами pcie_mbox),
linux (v6.8-rc5 — с патчами qemu-mailbox, mailbox-pingpong),
buildroot (2024.02-31).
Ссылка на проект-обертку: qemu-playground.
Проект предусматривает генерацию initrd образа для использования с QEMU. Тем не менее основные работы велись через 9p filesystem, где в качестве директории для монтирования использовалась директория, собранная с помощью overlayfs отдельно для каждой машины.
Описание модели в QEMU
Модель представляет из себя PCI-устройство, подключенное к основной шине PCI.
В BAR0 находятся все регистры управления для модели MAILBOX. BAR1 служит исключительно для таблиц MSI-X.
В модели специально были разделены регистры для каналов вместо объединения их в векторы по причинам, о которых вы узнаете позднее.
Общие регистры:
Имя | Адрес | Доступ | Описание |
---|---|---|---|
CFG | 0x00 | RO | Не используется |
ISTATUS | 0x04 | RO | Вектор с информацией о состоянии прерываний DATA |
IEN | 0x08 | RW | Включение/выключение прерывания для линии |
TXDONE | 0x10 | RO | Вектор с информацией о состоянии прерываний TXDONE |
Регистры для каждого канала:
Имя | Адрес | Доступ | Описание |
---|---|---|---|
DATA | 0x00 | RW | Запись или чтение сообщения |
ACK | 0x04 | WO | Подтверждение приема сообщения |
Немного о драйвере ядра для mailbox и модуле pingpong
Подсистема mailbox на данный момент используется в основном в следующих компонентах ядра:
remoteproc — фактически это физический уровень для virtio поверх shared memory,
firmware — простая отправка команд для сопроцессора, куда перенесен функционал, который считается «небезопасным» для исполнения на основном процессоре: включение/выключение, мультиплексирование ног, настройка тактирования и прочее.
В подсистеме mailbox нас интересуют два основных события:
Сообщение готово для чтения (в нашем случае это просто прием прерывания, так как само сообщение мы не передаем), далее оно будет упоминаться как DATA.
Сообщение было получено (в модели может быть инициировано двумя способами — чтение DATA или запись в ACK), далее оно будет упоминаться как TX_DONE.
Сам по себе драйвер mailbox нам ничего не дает, так как не позволяет принимать/отправлять сообщения, поэтому к нему необходим драйвер-клиент, в нашем случае это pingpong.
Задача драйвера pingpong очень простая:
запросить канал для связи с соседней машиной,
инициировать передачу сообщения,
посчитать задержку по достижению какого-либо критерия, в нашем случае по количеству итераций.
Код модулей содержится в отдельном репозитории (разумеется, репозиторий включен в суперпроект):
Подсистема mailbox на данный момент одна из наименее сложных в ядре. Тем не менее она рассчитана на использование практически исключительно совместно с DT (Device Tree) — то есть канал нельзя запросить через ACPI или функцией (как это сделано для DMA), а только этим способом. Поэтому в qemu-mailbox добавлена функция qemu_mbox_request_channel()
для запроса каналов.
Запуск машин QEMU1 и QEMU2
Приведем пример для запуска машин, в качестве которого использовался третий метод как наиболее полный, причем для запуска первой машины использовалась следующая команда:
build-qemu/qemu-system-x86_64 -machine q35 --enable-kvm -smp 2 -m 512 \
-cpu host,kvm-poll-control=true,kvm-hint-dedicated=true,kvmclock-stable-bit=true \
-chardev socket,path=/tmp/mbox_socket0,id=mbox0,server=on \
-device pcie-mbox,chardev=mbox0,topo=simple \
-initrd build-image/initramfs.cpio.xz \
-kernel build-linux/arch/x86_64/boot/bzImage \
-append "console=ttyS0 earlycon nokaslr initrd=/init" \
-display none -serial mon:stdio "$@"
Для второй, соответственно:
build-qemu/qemu-system-x86_64 -machine q35 --enable-kvm -smp 2 -m 512 \
-cpu host,kvm-poll-control=true,kvm-hint-dedicated=true,kvmclock-stable-bit=true \
-chardev socket,path=/tmp/mbox_socket0,id=mbox0 \
-device pcie-mbox,chardev=mbox0,topo=simple \
-initrd build-image/initramfs.cpio.xz \
-kernel build-linux/arch/x86_64/boot/bzImage \
-append "console=ttyS0 earlycon nokaslr initrd=/init" \
-display none -serial mon:stdio "$@"
Как мы видим, запуск машин практически одинаков, за исключением запуска -chardev socket
. Первая машина создает сокет и, прежде чем начать исполнение, ждет, когда произойдет подключение.
Все сценарии запуска можно посмотреть здесь.
Сами методы выбираются с помощью ключей к -device pcie-mbox
:
Метод | -device pcie-mbox |
---|---|
1 | topo=simple |
2 | topo=recieve |
3 | topo=both |
При загрузке гостевого ядра следует обратить внимание на наличие строчки:
[ 0.408755] cpuidle: using governor haltpoll
Это означает, что мы используем специальный драйвер для контроля перехода гостевого ядра в состояние HLT. С использованием этого драйвера наблюдается уменьшение задержек, в некоторых случаях почти в два раза (подробнее — в документации по guest halt polling). Даже большего эффекта можно достичь за счет отключения CONFIG_CPU_MITIGATIONS в ядре хоста.
Методика проведения измерений
Производятся замеры времени между событиями TX_DONE на одной из машин QEMU. Сам обмен производится в цикле — как только мы получили событие DATA, мы посылаем подтверждение TX_DONE.
Цикл посылки и получения находится в модуле mailbox-pingpong, загруженном на машинах. Инициирует цикл сторона, которая получила команду run
, а вторая лишь подтверждает получение прерывания путем отправки прерывания со своей стороны.
По итогам измерений мы получаем информацию о задержках, количестве итераций и общем времени, затраченном на тест:
# dmesg
...
[ 6.443918] mbox_pp: made 5000 iterations, lasted 217322 usecs
[ 6.444867] mbox_pp: latency 43 us (43464 ns)
Также к каждому измерению был сделан вывод временной метки события TX_DONE в наносекундах с помощью trace_printk()
:
qemu-irqs # cat /sys/kernel/debug/tracing/trace
...
mbox_pp_thread-169 [000] ..... 6.243447: mbox_pp_func: TX_DONE=6078138811
mbox_pp_thread-169 [000] ..... 6.243534: mbox_pp_func: TX_DONE=6078226751
mbox_pp_thread-169 [000] ..... 6.243616: mbox_pp_func: TX_DONE=6078308151
mbox_pp_thread-169 [000] ..... 6.243681: mbox_pp_func: TX_DONE=6078373971
mbox_pp_thread-169 [000] ..... 6.243723: mbox_pp_func: TX_DONE=6078415721
...
Каждый экземпляр QEMU живет в собственном cpuset — по четыре CPU для каждого эксклюзивно, к тому же сами CPU ограничены по нижней планке возможной частоты.
Для всех измерений далее использован этот метод.
Базовая модель без прерываний
Простая модель, можем вызвать событие DATA посредством записи через QOM:
stage 0 — hw/misc: Add PCI Mailbox model
Мы начинаем с того, что создаем регион памяти с заданными функциями для чтения и записи pcie_mbox_io_read/write()
:
static const MemoryRegionOps pcie_mbox_mmio_ops = {
.read = pcie_mbox_io_read,
.write = pcie_mbox_io_write,
.endianness = DEVICE_NATIVE_ENDIAN,
.valid = {
.min_access_size = 4,
.max_access_size = 4,
}
};
При каждом обращении к региону для чтения и записи будут вызываться именно наши определенные функции, также мы жестко задаем размер доступа шириной в четыре байта. Это именно жесткие ограничения — при попытке чтения/записи с размером, отличным от указанного, мы получим ошибку шины.
В функции чтения/записи нам передается относительный адрес внутри самого региона памяти, ширина доступа и само значение в случае операции записи:
switch (addr) {
case A_CFG:
...
break;
case A_ISTATUS:
...
break;
case A_IEN:
...
break;
case A_TXDONE:
...
break;
default:
/* report error here */
break;
}
Помимо MemoryRegionOps .read()/.write()
, также доступны версии .read/write_with_attrs()
, которые позволяют генерировать ошибку на шине в каких-то случаях, например регистр с таким адресом отсутствует или доступен только для чтения/записи.
Далее мы регистрируем наш регион как BAR0:
pci_register_bar(dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &s->mmio);
Для того чтобы писать и читать состояние линий вне QEMU или гостя, добавим дополнительные свойства для QOM:
for (i = 0; i < PCIE_MBOX_CHAN_CNT; i++) {
s->chans[i].idx = i;
object_property_add(obj, "chan[*]", "uint32",
pcie_mbox_chan_qom_get,
pcie_mbox_chan_qom_set, NULL, &s->chans[i]);
}
Проверим появление устройства:
# lspci -vvv -s 00:03.0
00:03.0 Communication controller: Red Hat, Inc. Device 1111
Subsystem: Red Hat, Inc. Device 1100
Control: I/O+ Mem+ BusMaster- SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR+ FastB2B- DisINTx-
Status: Cap- 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
Region 0: Memory at fe004000 (64-bit, prefetchable) [size=1K]
Проверим загрузку модуля (mailbox_pingpong — единственный клиент для qemu-mailbox, сам по себе модуль qemu-mailbox не обладает достаточным функционалом для приема сообщений):
# modprobe qemu-mailbox
# modprobe mailbox_pingpong
Добавление прерывания MSI-X
Добавление единственного прерывания MSI-X и вектора статуса прерываний:
stage 1 — hw/misc: pcie_mbox: Add single MSIX intr
Добавляем выделенный только для MSI-X BAR1 c единственным прерыванием:
|
Он отобразится в виде отдельного BAR, также можно увидеть количество линий прерывания:
00:03.0 Communication controller: Red Hat, Inc. Device 1111
Subsystem: Red Hat, Inc. Device 1100
Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR+ FastB2B- DisINTx+
Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
Latency: 0
Region 0: Memory at febf1000 (32-bit, non-prefetchable) [size=1K]
Region 1: Memory at febf2000 (32-bit, non-prefetchable) [size=4K]
Capabilities: [40] MSI-X: Enable+ Count=1 Masked-
Vector table: BAR=1 offset=00000000
PBA: BAR=1 offset=00000800
Kernel driver in use: qemu-mailbox
Тогда при вызове изменения состояния через QOM просто вызываем прерывание и выставляем бит линии в векторе прерываний ISTATUS, если соблюдены необходимые условия:
static void pcie_mbox_push_chan(PcieMboxState *s, uint8_t idx, uint32_t value)
{
PcieMboxChanState *chan = &s->chans[idx];
PCIDevice *pdev = PCI_DEVICE(s);
trace_pcie_mbox_push(idx, value);
set_bit(chan->idx, &s->intsts);
if (s->ien & BIT_ULL(chan->idx)) {
msix_notify(pdev, 0);
}
}
Соответствующая часть драйвера mailbox заключается в простом назначении обработчика прерываний с чтением состояния из ISTATUS:
static irqreturn_t qemu_mbox_isr(int virq, void *data)
{
struct qemu_mbox *mbox = data;
struct mbox_chan *chan;
unsigned int stat = 0;
int offset;
u32 msg;
/* чтение вектора статусов прерываний */
regmap_read(mbox->map, QEMU_MBOX_ISTATUS, &stat);
dev_dbg(mbox->dev, "stat: 0x%x\n", stat);
for_each_set_bit(offset, (unsigned long *)&stat, QEMU_MBOX_MAX_CHAN_CNT) {
regmap_read(mbox->map, QEMU_MBOX_CHAN_ADDR(offset), &msg);
chan = &mbox->mbox.chans[offset];
if (chan->cl)
mbox_chan_received_data(chan, &msg);
}
return IRQ_HANDLED;
}
С помощью QMP отправим сообщение для первого канала и проверим изменения состояния из гостевой машины:
# tools/qemu1.sh -qmp unix:./qmp.sock,server,wait=off
$ socat UNIX:qmp.sock - < tools/qmp_push_mbox
{"QMP": {"version": {"qemu": {"micro": 90, "minor": 2, "major": 8}, "package": "v9.0.0-rc0-74-gc819c30a8e-dirty"}, "capabilities": ["oob"]}}
{"return": {}}
{"return": {}}
Путь к устройству можно узнать через info qom-tree в консоли монитора QEMU.
qemu-irqs # cat /proc/interrupts
...
28: 1 0 0 0 PCI-MSIX-0000:00:03.0 0-edge qemu_mbox_isr
...
Добавление обмена векторами прерываний
Добавим обмен файловыми дескрипторами для уведомления об изменении состояния:
stage 2 — hw/misc: pcie_mbox: Add inter-vm events
На каждый канал назначим по два eventfd: один — для события DATA, другой — для TX_DONE.
Посылка события осуществляется по записи или чтению DATA регистра:
static void pcie_mbox_io_write(void *opaque, hwaddr addr,
uint64_t val, unsigned size)
{
...
case PCIE_MBOX_CHAN_OFFSET ... PCIE_MBOX_CHAN_END:
pcie_mbox_chan_write(s, addr, val, size);
break;
...
}
static uint64_t pcie_mbox_io_read(void *opaque, hwaddr addr,
unsigned size)
{
...
case PCIE_MBOX_CHAN_OFFSET ... PCIE_MBOX_CHAN_END:
val = pcie_mbox_chan_read(s, addr, size);
break;
...
}
Обработчик назначен для каждого eventfd и по сути просто вызывает прерывания для канала так же, как это делает QOM:
static void pcie_mbox_vector_notify(void *opaque)
{
MailboxVector *vec = opaque;
if (!event_notifier_test_and_clear(&vec->push)) {
return;
}
trace_pcie_mbox_vector_notify(vec->idx);
pcie_mbox_push_chan(vec->parent, vec->idx, 1);
}
Добавление KVM-обработчиков для приема прерываний MSI-X
stage 3 — hw/misc: pcie_mbox: Add KVM rcv interrupts
До этого момента все вышеперечисленное будет работать как в softmmu-режиме, так и KVM-режиме. Но использование KVM_IRQFD уже ограниченно исключительно KVM-режимом. Вызов ioctl(KVM_IRQFD)
ставит в соответствие переданный номер прерывания, переданному (в этом же вызове) файловому дескриптору. Иными словами, запись в файловый дескриптор будет вызывать соответствующее прерывание в гостевом ядре.
Как только драйвер устройства включает прерывания MSI-X, мы устанавливаем обработчики событий маскирования и демаскирования:
static void pcie_mbox_enable_irqfd(PcieMboxState *s)
{
...
/* Удалим все старые обработчики out/in[].push/tx_done */
for (i = 0; i < s->nr_chans; i++) {
eventfd = event_notifier_get_fd(&s->out[i].push);
qemu_set_fd_handler(eventfd, NULL, NULL, NULL);
eventfd = event_notifier_get_fd(&s->in[i].tx_done);
qemu_set_fd_handler(eventfd, NULL, NULL, NULL);
}
/* Установим обработчики событий для MSI-X прерываний */
ret = msix_set_vector_notifiers(pdev,
pcie_mbox_vector_use,
pcie_mbox_vector_release,
NULL);
...
}
Тогда при демаскировании прерывания MSI-X мы запрашиваем номер линии прерывания для передачи в KVM_IRQFD c помощью функции kvm_irqchip_add_msi_route()
и назначаем ему файловый дескриптор eventfd, полученный нами ранее. С этого момента прерывание идет фактически «напрямую» в гостевое ядро из соседнего экземпляра QEMU, минуя собственный.
static int pcie_mbox_vector_use(PCIDevice *pdev, unsigned nr,
MSIMessage msg)
{
EventNotifier *n;
...
virq = kvm_irqchip_add_msi_route(&c, nr, pdev);
...
ret = kvm_irqchip_add_irqfd_notifier_gsi(kvm_state, n, NULL, virq);
...
}
На этой стадии мы уже не можем полагаться на чтение регистров:
ISTATUS,
TXDONE.
Мы, в принципе, уже не можем полагаться на состояние модели QEMU, так как eventfd-события идут в обход нашей модели, напрямую в гостевое ядро.
Также добавим отдельные обработчики прерываний, рассчитанные на новую схему обмена:
Теперь мы не проверяем ISTATUS и TXDONE в обработчиках прерываний и записываем подтверждение о получении в регистр ACK в qemu_mbox_vec_isr()
:
static irqreturn_t qemu_mbox_vec_isr(int virq, void *data)
{
...
regmap_write(mbox->map, QEMU_MBOX_CHAN_ADDR(vec->idx) + QEMU_MBOX_ACK, 1);
mbox_chan_received_data(chan, &msg);
return IRQ_HANDLED;
}
static irqreturn_t qemu_mbox_txdone_vec_isr(int virq, void *data)
{
...
mbox_chan_txdone(chan, 0);
return IRQ_HANDLED;
}
Добавление KVM-обработчиков для отправки прерываний MSI-X
Также следует учитывать, что если принято решение использовать DMA для записи в регистры, то путь прерывания пойдет по первому методу, так как именно модель DMA внутри QEMU будет писать в регистры, а не модуль ядра в режиме KVM.
Теперь в отправляющей события стороне необходимо проделать обратную операцию — писать в соответствующий eventfd при записи в регистры DATA, ACK. Именно с этой целью на каждый канал был сделан отдельный регистр ACK, поскольку KVM_IOEVENTFD поддерживает только запись:
static int pcie_mbox_setup_kvm_ioevents(PcieMboxState *s, bool enable)
{
hwaddr addr = s->mmio.addr + PCIE_MBOX_CHAN_STRIDE;
int i, ret;
for (i = 0; i < s->nr_chans; i++) {
hwaddr reg = addr + i * PCIE_MBOX_CHAN_STRIDE + A_DATA;
ret = pcie_mbox_setup_kvm_ioevent(&s->out[i].push, reg, 0x01, enable);
if (ret) {
error_report("pcie_mbox: Failed to setup kvm_vm_ioctl(KVM_IOEVENTFD)");
return ret;
}
}
for (i = 0; i < s->nr_chans; i++) {
hwaddr reg = addr + i * PCIE_MBOX_CHAN_STRIDE + A_ACK;
ret = pcie_mbox_setup_kvm_ioevent(&s->in[i].tx_done, reg, 0x01, enable);
if (ret) {
error_report("pcie_mbox: Failed to setup kvm_vm_ioctl(KVM_IOEVENTFD)");
return ret;
}
}
return 0;
}
Стандартной инфраструктуры для QEMU в данном случае не предусмотрено, поэтому мы будем оперировать вызовом функции kvm_vm_ioctl()
:
static int pcie_mbox_setup_kvm_ioevent(EventNotifier *notifier,
hwaddr addr, uint8_t data, bool assign)
{
struct kvm_ioeventfd kick = {
.flags = KVM_IOEVENTFD_FLAG_DATAMATCH,
.fd = event_notifier_get_fd(notifier),
/* Данные для сравнения, для нашего случая всегда 0x01 */
.datamatch = data,
/* Физический адрес регистра */
.addr = addr,
.len = 0x4,
};
if (!kvm_check_extension(kvm_state, KVM_CAP_IOEVENTFD)) {
return -ENOSYS;
}
if (!assign) {
kick.flags |= KVM_IOEVENTFD_FLAG_DEASSIGN;
}
trace_pcie_mbox_setup_kvm_ioevent(addr, data, assign);
return kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &kick);
}
Трассировка назначения файлового дескриптора в соответствие адресу и значению:
pcie_mbox_setup_kvm_ioevent addr: 0xfebf1020 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf1040 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf1060 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf1080 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf10a0 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf10c0 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf10e0 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf1100 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf1024 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf1044 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf1064 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf1084 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf10a4 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf10c4 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf10e4 data: 1 assign: 1
pcie_mbox_setup_kvm_ioevent addr: 0xfebf1104 data: 1 assign: 1
В итоге картина будет выглядеть следующим образом:
Вывод
Итоги замеров
Всего мы сняли по 5000 точек с каждого метода, машина работала в штатном режиме, без применения каких-либо специальных сценариев нагрузки.
Прикладываю характеристики машины, конфигурацию ядра хоста и конфигурацию ядра гостя.
Linux munin 6.9.8-gentoo-x86_64 #1 SMP PREEMPT_DYNAMIC Wed Jul 10 12:20:51 MSK 2024 x86_64 AMD Ryzen 9 7950X 16-Core Processor AuthenticAMD GNU/Linux
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 48 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 32
On-line CPU(s) list: 0-31
Vendor ID: AuthenticAMD
Model name: AMD Ryzen 9 7950X 16-Core Processor
CPU family: 25
Model: 97
Thread(s) per core: 2
Core(s) per socket: 16
Socket(s): 1
Stepping: 2
Frequency boost: enabled
CPU(s) scaling MHz: 51%
CPU max MHz: 5879.8818
CPU min MHz: 3000.0000
BogoMIPS: 9004.46
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good amd_lbr_v2 nopl nonstop_tsc cpuid extd_apic
id aperfmperf rapl pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ibs skinit wdt tce
topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpb cat_l3 cdp_l3 hw_pstate ssbd mba perfmon_v2 ibrs ibpb stibp ibrs_enhanced vmmcall fsgsbase bmi1 avx2 smep bmi2 erms invpcid cqm rdt_a avx512f avx512dq
rdseed adx smap avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local avx512_bf16 clzero irperf xsaveerptr rdpru wbnoinvd
cppc arat npt lbrv svm_lock nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold avic v_vmsave_vmload vgif x2avic v_spec_ctrl vnmi avx512vbmi umip pku ospke avx512_vbmi2 gfni vaes vpc
lmulqdq avx512_vnni avx512_bitalg avx512_vpopcntdq rdpid overflow_recov succor smca fsrm flush_l1d
Virtualization features:
Virtualization: AMD-V
Caches (sum of all):
L1d: 512 KiB (16 instances)
L1i: 512 KiB (16 instances)
L2: 16 MiB (16 instances)
L3: 64 MiB (2 instances)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-31
Vulnerabilities:
Gather data sampling: Not affected
Itlb multihit: Not affected
L1tf: Not affected
Mds: Not affected
Meltdown: Not affected
Mmio stale data: Not affected
Retbleed: Not affected
Spec rstack overflow: Mitigation; safe RET
Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl
Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Spectre v2: Mitigation; Enhanced / Automatic IBRS, IBPB conditional, STIBP always-on, RSB filling, PBRSB-eIBRS Not affected
Srbds: Not affected
Tsx async abort: Not affected
RANGE SIZE STATE REMOVABLE BLOCK
0x0000000000000000-0x000000007fffffff 2G online yes 0
0x0000000100000000-0x000000107fffffff 62G online yes 2-32
Memory block size: 2G
Total online memory: 64G
Total offline memory: 0B
В таблице приведены усредненные значение в микросекундах по 10 измерениям на каждый метод передачи (без использования trace_printk()
):
Метод | host haltpoll | guest haltpoll | guest haltpoll + host mitigations=off |
---|---|---|---|
1 | 28 µs | 24 µs | 13 µs |
2 | 10 µs | 7 µs | 4 µs |
3 | 7 µs | 5 µs | 3 µs |
Ссылки на сырые данные измерений (guest haltpoll c использованием trace_printk()
):
Метод | Файлы измерений | Сводные таблицы измерений |
---|---|---|
1 | ||
2 | ||
3 |
Интерпретация результатов
Мы наблюдаем существенное сокращение времени задержки в сравнении первого и второго методов передачи прерываний и относительно небольшое в сравнении второго и третьего. Существенная часть уменьшения задержек заключается в экономии вызовов и KVM_EXIT_MMIO. Возможно, в каких-то специальных сценариях применения и для другого железа картина будет существенно отличаться, но не в данном случае (я специально воздержался от применения каких-либо сценариев нагрузки).
Но не стоит забывать, что переключение на использование KVM irqs несет за собой следующие неудобства:
мы отказываемся от сопутствующих регистров в модели (статус и маскирование прерываний),
усложняем драйвер Linux, внося в него изменения,
отказываемся от «кросс»-модели (мы не можем применять KVM в режиме softmmu),
усложняем отладку и трассировку (так как трассирования методами QEMU уже недостаточно).
Сами по себе модули ядра могут быть использованы для добавления в подсистему mailbox поддержки ACPI, а совместно с какой-либо разделяемой памятью — и для реализации remoteproc/rpmsg для x86_64.
Также можно доработать pcie_mbox_setup_kvm_ioevent()
и предложить изменения в upstream QEMU.
Библиография
От автора
Благодарю коллег за ценные комментарии и дополнения:
Сергея Мирошниченко (@zergium), руководителя отдела системного программирования в YADRO и автора статьи «Lane margining: как оценить качество PCIe-соединения без дополнительной аппаратуры»,
Евгения Шатохина, ведущего инженера-программиста в отделе перспективных разработок YADRO.