РАЗБОР Статья 

Сетевой адаптер Virtio-Net[1] - от устройства к драйверу

Marylin
Marylin Mod.Assembler · 394 сообщений
Подписаться
122
Режим чтения
В статье рассмотрим программный интерфейс паравиртуализации VirtIO от компании RedHat, который поддерживает широкий круг физ.оборудования и используется в таких гипервизорах как: Qemu, Xen, Cloud HV, Proxmox VE, oVirt, OpenStack, и VirtualBox. Основной акцент сделаем на создание драйвера сетевого адаптера Virtio-Net, что позволит взлянуть на сеть с абсолютно нового ракурса. Материал получился большим, поэтому я разделил его на 2 части - в первой само вирт.устройство, а во-второй программное взаимодействие с ним.

Оглавление - часть(1) :
1. Общие сведения​
2. Установка Qemu на Windows​
3. Программный поиск Virtio-Net на шине PCI​
4. Доступ к пространству конфигурации​
5. Механизм ECAM для PCI-Ex​
6. Заключение​



1. Общие сведения

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

Проблему решает паравиртуализация. Здесь вместо эмуляции реального железа, гость получает производительный интерфейс для обмена данными с хостом. Для этого в гостевой ОС устанавливается специальный драйвер Front-End (который мы позже напишем), а на стороне гипервизора qemu работает обслуживающий Back-End. Транспорт между ними построен на разделяемой памяти и кольцевых буферах, что позволяет передавать данные практически без копирования.

Если в обычной схеме драйвер адаптера NIC напрямую управляет физ.устройством, то в паравиртуализации с одним вирт.девайсом работают две согласованные части: Front-End отправляет и принимает запросы через интерфейс без намёков на эмуляцию, а Back-end в qemu обрабатывает эти запросы и при необходимости взаимодействует с реальным оборудованием хоста. Выигрыш достигается за счёт разгрузки кода гипервизора, поскольку обращения к физ.девайсам происходят только, когда это действительно необходимо, например при маршрутизации сетевых пакетов через TAP во внешнюю сеть.

Virtio заточен под Qemu/KVM и способен эмулировать зоопарк из более 40 различных устройств, хотя даже последняя спека v1.4 описывает лишь некоторые из них. Будучи установленной как гость, Linux из коробки имеет полную поддержку в виде гостевых драйверов, а вот для остальных ОС придётся скачивать их отдельно. Чтобы далее не было путаницы, сразу определим понятие "Device" (программный эмуль на хосте) и "Driver" на стороне клиента, от имени которого мы и будем выступать:

Host_Guest.webp

Особенностью VirtIO является двусторонний обмен драйвера с устройством через разделяемую память. По сути это хорошо известный механизм проецирования "Memory Mapping", когда 2 разных процесса имеют совместный доступ к одному фрейму физической ОЗУ. Обратите внимание, что для каждой из гостевых VM, хост создаёт отдельные экземпляры устройств Virtio-Net, у которых будет свой МАС и своя разделяемая память. При желании гости могут общаться между собой по внутренней сети Ethernet, однако доступ к чужому кольцевому буферу VirtQueue у них закрыт.

По умолчанию Qemu создаёт сетевой стек TCP/IP с массой ограничений (режим SLiRP). Гость находится как-будто за фаерволом, который полностью блокирует входящий трафик из вне. Если нужен полноценный обмен или выход в Инет, можно сменить конфиг например на мост Bridge. Cервер DHCP (Dynamic Host Configuration Protocol) играет роль шлюза Gateway и имеет постоянную прописку по адресу 10.0.2.2. Своим гостям он назначает IP начиная с 10.0.2.15 ..16, и т.д. Таким образом, чтобы проверить работоспособность сети, мы можем отправить ARP-запрос серверу DHCP, и он должен вернуть нам МАС маршрутизатора, частью которого и является.

Код:
guest (10.0.2.15) <--+--> DHCP server (10.0.2.2) /Firewall <---> Internet
                     |
                     +--> DNS  server (10.0.2.3)
                     |
                     +--> SMB  server (10.0.2.4)


2. Установка QEMU на Windows

Если хотите практики, нужно установить на свою основную систему эмулятор Qemu. У меня Win7-x64, а макс.поддерживаемая версия для неё Qemu_v6.2. Для Win скачать бинарные установщики *.exe можно от сюда, а обладатели Linux как правило тянут софт напрямую из официальных репозиториев через встроенный пакетный менеджер. Хэлп для юзера лежит здесь.

После установки, qemu в Win запускается через bat-файл, в котором нужно задать ключи конфигурации, и перечислить типы требуемых устройств (чипсет, диск, сеть и другое). Настроек здесь как звёзд на небе, поэтому начинающие могут утонуть в них. Однако радует, что разработчики зашили в qemu дефолтные установки, и если не указать конкретные опции в *.bat/sh, эмуль запускается в штатном режиме по умолчанию.

Проблема, с которой столкнулся лично я в своей v6.2 - это отсутствие прокрутки экрана во встроенном мониторе аккордом Ctrl+Alt+Up/Down, в результате чего на запрос help мне доступны только несколько последних строк. Поэтому я подключился к монитору через внешний telnet-клиент на хосте, и теперь могу не только стандартно прокручивать экран мышью, но и копировать с него информацию для вставки сюда. Вот пример моего батника, который (в порядке следования строк) проделывает следующее..

1. Запускает клиента telnet на хосте, сразу указывая адрес и порт сервера qemu​
2. Определяет эмуляцию именно cpu х86_64, а не х32 или arm​
3. Выбирает современный чипсет Q35 с шиной PCIe вместо древнего i440fx, 2 ядра для ЦП, и 256 МБ озу для клиентской ОС​
4. Синхронизирует время с хостом, т.к. у qemu в дефолте стоит UTC​
5. Включает логи обращений со стороны хоста к нашему драйверу, и его очередям queue​
6. Указывает, что в качестве сетевого адаптера использовать Virtio-Net на шине PCI​
7. Активирует доступ к монитору qemu через telnet на хосте​

Bash:
start cmd.exe /k telnet 127.0.0.1 4444
qemu-system-x86_64 ^
-machine q35 -smp 2 -m 256M ^
-rtc base=localtime,clock=host ^
-trace "virtio_*" ^
-trace "virtqueue_*" ^
-nic user,model=virtio-net-pci,hostfwd=tcp::5555-:80 ^
-monitor telnet:127.0.0.1:4444,server,nowait

Если запустить этот батник, получим окно с логами трассировки, отдельный терминал telnet с привязкой к монитору qemu, ну и собственно GUI-окно клиентской ОС, которой пока у нас нет, но позже напишем загрузчик и небольшое ядро для тестов драйвера сети. Именно поэтому мы не подключаем сейчас в батнике образ харда опцией -drive xx, и qemu в последней строке предупреждает нас об этом "No bootable device":

qumu_start.webp

Зато эмуль возвратил нам дефолтный конфиг сети где видно, что DHCP-сервер и вправду присвоил нам указанный выше клиентский IP 10.0.2.15, а себе как шлюзу Gateway взял 10.0.2.2. Чтобы собрать более подробную инфу о самом устройстве Virtio-Net, можно ввести в терминале telnet команду info pci:

Код:
(qemu) info pci
........
  Bus 0, device 2, function 0:
    Ethernet controller:
      PCI device    1af4:1000
      PCI subsystem 1af4:0001
      IRQ 11, pin A
      BAR0: I/O port at 0xc040 [size=0x20]  <------------ 32 байта
      BAR4: prefetch mem at 0xfe000000 [size=0x4000] <--- 16 Kбайт
      id ""
(qemu)

Справку по всем параметрам команды "info" возвращает help info, причём помощь можно запрашивать прямо в аргументах батника. Например если указать model=help, получим список всех поддерживаемых qemu сетевых адаптеров:

-nic user,model=help,hostfwd=tcp::5555-:80 ^

Код:
Supported NIC models:
e1000
e1000-82544gc
e1000-82545em
e1000e
i82550
i82551
i82557a,b,c
i82558a,b
i82559a,b,c,er
i82562
i82801
ne2k_pci
pcnet
rtl8139
tulip
virtio-net-pci   <---------------- наш пациент
virtio-net-pci-non-transitional
virtio-net-pci-transitional
vmxnet3


3. Программный поиск VirtIO на шине PCI

Qemu полностью эмулирует аппаратную часть системы, а потому гостевая VM в нашем лице получит всё, включая биос и конфиг-пространство PCI для поиска и настройки вирт.оборудования. Каждому из найденных устройств, в пространстве pci выделяется блок размером 256 байт, куда биос сохраняет паспорт девайса и его требования к ОС.

В младенчестве Net работало как Legacy-устройство (спека v0.9.5), но позже появился причёсанный режим Modern v1.1+. Таким образом, сейчас весь зоопарк Virtio-устройств может функционировать в смешанном режиме "Transitional", когда хост qemu сам понимает, что от него хочет драйвер и авто-переключается в нужный режим. Основное их отличие - метод доступа к регистрам устройств: Legacy использует исключительно порты ввода-вывода PIO, а в Modern регистры отображаются в регион системной памяти MMIO (memory mapped input-otput).

Pci-Cfg любого устройства начинается с идентификаторов Vendor/Device. Вендором для Virtio является компания RedHat, которой выделен ID=0x1AF4. А вот устройств у него может быть вагон и целая тележка, поэтому код DevID плавает в диапазоне от 0x1000 и далее, причём в режиме Modern база меняется на 0x1040. С полным списком DeviceID можно ознакомиться здесь, а табличку ниже я собрал из последней спеки v1.4 от 2026 года (всего 49 устройств):

DevId.webp

Не последнюю роль играют и коды DevID в поле Subsystem по смещению 0x2E от начала PciCfg - они позволяют однозначно определить тип устройства. Коды в режиме Modern формируются по схеме: Base(0x1040)+SubSys. Обратите внимание, что в Legacy часть оборудования не поддерживается вообще, хотя герой данной статьи Virtio-Net занял в этом списке почётное первое место.

С приходом Modern разрабы не отправили Legacy торжественно на свалку - режим до сих пор поддерживается qemu, т.к. прост и надёжен как автомат Калашникова. Да, новый имеет много дополнительных плюшек, но для обычной работы сетевого адаптера они попросту не нужны - главное, чтобы пакеты курсировали по линку туда-обратно, с чем вполне справляется и Legacy. Поэтому на практике мы реализуем именно его, а Modern рассмотрим лишь в теории (по сути те-же регистры, только не в порту, а в памяти).


4. Доступ к пространству PCI

Функция AH=0xB1 прерывания биос INT-1Ah отвечает за работу с PciConfigSpace.
В регистре AL указывается номер под-функции в сл.порядке:

Код:
AL = 0x02 : Поиск устройства по Ven/Dev = 1AF4:1000 (возвращает адрес BDF в регистре BX)
AL = 0x08 : Прочитать байт из PciCfg по адресу BDF  (возвращает в регистр CL)
AL = 0x09 : Прочитать слово 2 байта                 (возвращает в регистр CX)
AL = 0x0А : Прочитать двойное слово 4 байта         (возвращает в регистр ЕCX)

Таким образом, код на ассемблере fasm ниже находит устройство Virtio-Net в пространстве PCI, и возвращает нам номер порта, через который позже будем взаимодействовать с регистрами сетевого адаптера. При чтении данных этим прерыванием, смещение внутри 256-байтного блока указывается в регистре DI.

Код:
; VirtioNet BDF
    mov   dx,0x1AF4   ; Vendor
    mov   cx,0x1000   ; Device
    xor   si,si       ; первое устройство (для много-функциональных)
    mov   ax,0xB102   ; поиск!
    int   1Ah         ; в BX получили адрес на шине Bus:Device:Func
                      ; BDF нужен для сл.функции чтения

; Взять порт I/O из BAR0
    mov   ax,0xB10A   ; читать дворд
    mov   di,0x10     ; смещение = BAR[0]
    int   1Ah         ; в регистре ECX получили номер порта Virtio-Net

    dec   cx          ; мл.бит это флаг - сбросить его!
    mov   [BAR0],cx   ; сохранить номер порта в переменной

Ниже представлена общая схема работы с устройством Virtio-Net.
Всё, что расположено после BAR[0] относится к Modern, и если вообще не обращаться к эти полям, хост сам переключит адаптер в Legacy. Порт PIO открывает доступ к регистрам, которыми можно оперировать инструкциями in/out (чтение-запись). Например такой незадачливый код вернёт состояние линка - подключён вирт.сетевой кабель к маршрутизатору на хосте, или нет:

Код:
    mov   dx,[BAR0]   ; DX = номер порта
    add   dx,0x1A     ; выбираем в нём регистр "Net_Status"
    in    al,dx       ; прочитать байт в AL
    cmp   al,1        ; тест на 1 (статус)
    jz    @ok         ; если равно - кабель подключён,
;....                 ; иначе что-то не настроено в конфигах.

VirtIO.webp

Когда адаптер в режиме Modern, тактика игры меняется. Здесь порт уходит на скамейку запасных, а доступ к отображённым в память регистрам открывает BAR[4] и 1-байтное значение из поля CapPointer. Оно хранит смещение к первой структуре VIRTIO_PCI_CAP, а всего имеется как минимум 5 таких структур (см ID выше). Каждая размером 16-байт и описывает один (конкретно взятый) блок регистров в памяти MMIO. Адрес внешнего блока собирается из 2-х составляющих: база для всех лежит в BAR[4], а смещение - в поле Offset структуры. Размер блока в поле Length как-правило равен 0x1000 или 4КБ (все блоки регистров выровнены в памяти на эту границу).

Всё тем-же прерыванием int-1Ah можно вывести дамп конфиг-пространства Virtio-Net, чтобы продемонстрировать, как это выглядит в реале. Здесь значение CapPointer=84h и сместившись к нему видим первую структуру PCI_CAP в массиве - вот её поля:

Код:
Vndr = 09h (нужный нам маркер, остальные пропускаем)
Next = 70h (смещение к сл.структуре в массиве)
Len  = 14h (размер этой структуры, обычно 10 или 14)
Type = 05h (тип внешнего блока: здесь 5=PCI_CFG, а нам нужен основной 1=COMMON_CFG)
Bar  = 00h (это порт PIO, значит PCI_CFG настраивается через Legacy - нам не нужно)
Offs = 00000000h, Length = 00000000h

CfgDump.webp

Основные регистры адаптера Net в режиме Modern хранит блок COMMON_CFG, поэтому мы должны найти его по полю Type=1 в каждой из структур PCI_CAP. В данном случае видно, что сл.структура лежит по Next=70h и Type у неё равен 2, значит идём далее-далее и наконец обнаруживаем Type=01 по смещению 40h, при чём она является последней, т.к. поле Next=00. Здесь видим BAR[4] и Offset=0, т.е. это самый первый блок в памяти MMIO по адресу 0xFE000000.

Получив таким образом физ.адрес, мы попадаем в структуру COMMON_CFG, которую спека описывает так. Обратите внимание, что аналогичные поля присутсвуют и в регистрах порта PIO режима Legacy, просто в память MMIO были добавляены ещё сдесяток регистров, для описания свойств кольцевых буферов Queue:

Код:
struct VIRTIO_PCI_COMMON_CFG  ;<------ cfg_type(1)
   device_feature_select    dd  0   ; (00) RW:  0 = только биты[0:31] функций активны, 1 = все биты[0:63]
   device_feature           dd  0   ; (04) R :  (!)--> предлагаемые хостом функции
   driver_feature_select    dd  0   ; (08) RW:  0 = только биты[0:31] функций активны, 1 = все биты[0:63]
   driver_feature           dd  0   ; (0C) RW:  (!)--> драйвер выбирает функции хоста

   msix_config              dw  0   ; (10) RW:  конфиги MSI-X, 0 = отключён, тогда активен ISR в порту PIO
   num_queues               dw  0   ; (12) R :  общее кол-во очередей для данного девайса Virtio (Net,Block,etc)
   device_status            db  0   ; (14) RW:  (!)--> драйвер записывает статус операции, например "RESET=0" или "OK=4"
   config_generation        db  0   ; (15) R :  счётчик конфигов (хост делает +1, когда заметно меняется конфигурация)

;----> О конкретной "virtqueue"
   queue_select             dw  0   ; (16) RW:  (!)--> драйвер выбирает очередь для конфигурации (отсчёт с нуля),
   queue_size               dw  0   ; (18) RW:  (!)--> ..а хост возвращает её размер в дескрипторах.
   queue_msix_vector        dw  0   ; (1A) RW:  драйвер указывает вектор очереди, если msi-x активны
   queue_enable             dw  0   ; (1C) RW:  драйвер записывает 1, чтобы активировать выбранную очередь (можно временно вкл/выкл)
   queue_notify_off         dw  0   ; (1E) R :  драйвер считывает это значение, чтобы вычислить смещение от начала "структуры уведомлений"
   queue_desc               dq  0   ; (20) RW:  (!)драйвер записывает сюда физ.адрес области дескрипторов
   queue_driver             dq  0   ; (28) RW:  Avail - драйвер записывает сюда физ.адрес кольца доступных буферов
   queue_device             dq  0   ; (30) RW:  Used  - драйвер записывает сюда физ.адрес кольца обработанных
ends

В своей демо-ОС я вывел более подробные сведения о возможностях Capability, включая фрагменты структур из MMIO. Напомню, что современный режим рассматривается здесь лишь потому, что адаптер находится сейчас в подвешенном состоянии "Transitional", т.е. мы можем вогнать его хоть в Legacy, хоть в Modern. Просто бородатый Legacy намного проще для понимания, поэтому я выбрал именно его. На скрине ниже конфиги ещё не настроены, а потому отдельные регистры по адресу 0xFE000000 не валидны:

CapDump.webp


5. Механизм CAM/ECAM

Долгое время роль основного транспорта на мат.платах исполняла пар-шина PCI, но сейчас её вытеснила последовательная PCI-Ex с более высокой пропускной способностью. Устройства на этих шинах принципиально отличаются, и чтобы описать характеристики девайса PCI-Ex, стандартных 256 байт в конфиг-пространстве уже не хватает - они требуют до 4КБ памяти, хотя реально не используют и половины.

Но проблема в том, что классический доступ к PCI через порты 0xCF8/0xCFC включая int-1Ah:AH=B1h, способен адресовать лишь первые 256 байт, т.к. для выбора регистра (аля смещение) в этом механизме под названием САМ (config access mechanism) выделяется всего 8 бит. Поэтому для устройств PCI-Ex был введён расширенный ECAM, в котором используются уже не порты, а прямой доступ к памяти MMCFG. Теперь читать и записывать конфиги можно обычной инструкцией mov, но для этого требуется база сегмента pcie-mmcfg.

В целях совместимости, устройства PCIe поддерживают оба механизма, но в случае САМ доступны только штатные 256 байт из общих 4КБ. В доках на qemu упоминается, что дефолтной базой для САМ является адрес 0xE0000000, хотя он может съехать в зависимости от объёма установленной ОЗУ. А вот с ECAM дела обстоят иначе - здесь базовый адрес прописывается биосом в лист MCFG системной таблицы ACPI (advanced config & power interface), а потому заранее не известен.

Адресация устройств на обоих шинах трёхмерная Bus:Device:Func, при этом согласно спецификации всего может быть шин(256), девайсов(32) на одной шине, и функций(8) у девайса. Таким образом, если нам известна база mmcfg и BDF устройства в системе (см int-1Ah:AX=B102h), мы сможем найти его пространство используя простую формулу ниже. Обратите внимание на кол-во выделенных бит под регистры - это 8 и 12. Они не явно задают размер блока конфигурации для одного устройства. То-есть мы должны организовать поиск с шагом или 256, или 4096-байт, в зависимости от используемого подхода:

ECAM.webp

Если вам понадобится найти базу механизма ECAM (задача довольно распространённая для осдевов), вот реализация, которую я использовал в своём ядре. Расширенную карту памяти можно получить и командой info mtree в мониторе qemu (в данном случае терминале telnet):

Acpi_Ecam.webp


6. Заключение

Виртуальный адаптер сети Virtio-Net довольно мощная штука, с огромным кол-вом настроек на все случаи жизни. По сути громкое имя RedHat уже говорит само за себя. Это профессионалы своего дела, которые грамотно и с нуля разработали уникальный в своём роде программный интерфейс - лично я ничего подобного больше нигде не встречал.

В следующей части статьи мы рассмотрим логику его работы, и напишем небольшую ОС реального режима для тестов. Для тех, кому мешает языковой барьер, вот спецификация в формате html, чтобы прямо в браузере могли перевести её на русский. Всем удачи, до скорого!
Полезно

Комментарии

0