Драйвер Ethernet для xv6

от автора

Драйвер Ethernet для xv6

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

Научим xv6 работать в сети, познакомимся со стандартом виртуальных устройств VirtIO, деревом устройств DeviceTree, технологией Ethernet, сетевыми протоколами, возведем сетевой мост между виртуальными машинами.

Драйвер помогает ОС общаться с сетевым адаптером. ОС просит драйвер отправить пакеты в сеть. Драйвер сообщает ОС, когда пакеты приходят из сети.

Драйвер и сетевой адаптер ведут очереди пакетов — исходящих и входящих. Драйвер помещает исходящий пакет в очередь, чтобы адаптер отправил пакет в сеть. Адаптер принимает пакет из сети и помещает в очередь входящих, чтобы драйвер прочел пакет.

Сетевой адаптер отправляет в сеть Ethernet-пакеты, а драйвер упаковывает информацию в Ethernet-пакеты перед отправкой. Сетевой адаптер Ethernet направляет пакет другому адаптеру — указывает в пакете MAC-адрес адаптера-получателя.

Ethernet-пакет содержит пакеты других протоколов — IP, TCP, UDP, ARP и другие. Ethernet-адаптер общается с другими Ethernet-адаптерами, но программы общаются с программами на других компьютерах и не знают об Ethernet.

Подключим сетевой адаптер

Xv6 работает на виртуальной машине QEMU. Укажем опции device и netdev, чтобы подключить сетевой адаптер virtio-net к машине и связать с адаптером tap на машине-хосте. Адаптер virtio-net передаст пакеты адаптеру tap, а tap — адаптеру virtio-net. Запустим Wireshark на хосте и увидим пакеты на tap.

QEMUOPTS += -device virtio-net-device,netdev=mynet,bus=virtio-mmio-bus.1,mac=52:54:00:78:76:36# 'xv6' in ASCII QEMUOPTS += -netdev tap,id=mynet 

Makefile

Параметр bus=virtio-mmio-bus.1 означает, что QEMU отобразит регистры адаптера в память. Xv6 аналогично подключает и жесткий диск — указывает параметр bus=virtio-mmio-bus.0.

QEMUOPTS += -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 

Код определяет адреса регистров устройства по смещению базового адреса. Пример: базовый адрес virtio-дискаVIRTIO0 = 0x10001000. Найдем базовый адрес сетевого адаптера. QEMU печатает дерево устройств, когда получит опцию dumpdtb. Программа dtc преобразует дерево из двоичного формата в текст.

qemu-system-riscv64 -M virt,dumpdtb=virt.dtb -nographic dtc -I dtb -O dts virt.dtb >> virt.dts 

Узлы дерева virtio_mmio@XXXXXXXX — разъемы на шине virtio-mmio. Адрес XXXXXXXX — базовый адрес памяти, с которого устройство отобразит регистры. QEMU нумерует разъемы по возрастанию адресов, поэтому virtio-mmio-bus.1 получит адрес 0x10002000. Файл kernel/virtio.h определяет смещения регистров virtio-устройств от базового адреса.

Фрагмент devicetree

Фрагмент devicetree

Драйвер общается с устройством, когда пишет и читает регистры устройства.

Добавим область памяти с регистрами сетевого адаптера в таблицу страниц ядра — иначе драйвер получит ошибку доступа к памяти, когда обратится к регистру адаптера. Глава 3 расскажет о таблицах страниц подробнее.

kernel/vm.c:34

  // virtio mmio net interface   kvmmap(kpgtbl, VIRTIO1, VIRTIO1, PGSIZE, PTE_R | PTE_W); 

Настроим сетевой адаптер

Настроим адаптер, когда ОС стартует:

  1. Сбросим адаптер.

  2. Сообщим, что ОС видит адаптер — установим бит ACKNOWLEDGE регистра состояния.

  3. Сообщим, что ОС умеет управлять адаптером — установим бит DRIVER регистра состояния.

  4. Согласуем набор возможностей адаптера — примитивный сетевой драйвер использует лишь VIRTIO_NET_F_MAC.

  5. Подтвердим выбор — установим бит FEATURES_OK регистра состояния.

  6. Снова прочтем бит FEATURES_OK и убедимся, что бит установлен, иначе устройство отвергло такой набор возможностей.

  7. Прочитаем MAC-адрес адаптера, подготовим очереди входящих и исходящих пакетов.

  8. Установим бит DRIVER_OK регистра состояния.

Функция virtio_net_init настраивает адаптер.

Очереди VirtIO

Драйвер и устройство передают друг другу буферы памяти с помощью очередей. Поместить буфер в VirtIO-очередь сложнее, чем добавить элемент к односвязному списку. Пристегнитесь.

Очередь VirtIO содержит три массива:

  • Массив дескрипторов. Каждый дескриптор описывает буфер памяти.

  • Массив available содержит номера дескрипторов, которые драйвер передал устройству.

  • Массив used содержит номера дескрипторов, которые устройство передало драйверу.

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

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

Отправляем пакеты

Напишем программу ping и добавим к ядру xv6 системный вызов ping, который просит драйвер отправить пакет в сеть.

Пишем тесты. Тесты берегут нервы и время.

Пишем функции после тестов:

Объявим функцию ядра sys_ping и добавим в массив syscalls под номером SYS_ping. Объявим системный вызов ping в user/user.h и создадим точку входа в ядро с помощью скрипта user/usys.pl. Глава 4 расскажет о системных вызовах подробнее.

Системный вызов ping принимает IP-адрес хоста, на который отправит ICMP ECHO.

Функция virtio_net_intr обрабатывает прерывания, когда адаптер отправил или получил пакет. Глава 5 расскажет о прерываниях устройств.

Запустим программу ping и увидим, что пакет ушел в сеть.

ping 192.168.56.100 

Принимаем пакеты

Адаптер прерывает процессор, когда получит пакет из сети. Функция virtio_net_intr обрабатывает и входящие пакеты. Драйвер обрабатывает входящий пакет и возвращает буфер в очередь входящих пакетов.

Пишем тесты:

Пишем функции после тестов:

Вспомогательные функции разбирают и печатают заголовки пакета:

Запустим xv6 и увидим, как случайные пакеты из сети попадают на адаптер, а драйвер печатает заголовки пакетов.

[Ethernet] EtherType: 0x86dd SourceAddress: 26:e3:ce:e6:aa:83 DestinationAddress: 33:33:0:0:0:fb [IPv6] NextHeader: 17 PayloadSize: 149 SourceAddress: fe80:00:00:00:24e3:ceff:fee6:aa83 DestinationAddress: ff2:00:00:00:00:00:00:0fb [UDP] SourcePort: 5353 DestinationPort: 5353 Length: 149 Checksum: 0xad89 

Отправим пакет с другой машины

Запустим еще одну ВМ под QEMU и наведем мост между сетевыми адаптерами tap0 и tap1 хоста. Отправим ICMP ECHO и увидим, что xv6 получает ARP-запросы. Машина посылает в сеть ARP-запрос, чтобы узнать MAC-адрес владельца IP-адреса.

# /etc/network/interfaces.d/br0 on Debian host machine auto br0         iface br0 inet dhcp         bridge_ports enp0s8         bridge_fd 0  
# Debian host brctl addif br0 tap0 tap1 
# Gentoo VM ping 192.168.56.100 
QEMU подключает сетевые интерфейсы tap0 и tap1 к хосту после запуска виртуальных машин

QEMU подключает сетевые интерфейсы tap0 и tap1 к хосту после запуска виртуальных машин
Мост br0 объединяет интерфейсы tap0, tap1 и enp0s8 в одну сеть

Мост br0 объединяет интерфейсы tap0, tap1 и enp0s8 в одну сеть

Отправим DHCP-запрос

Пишем тесты:

Пишем функции после тестов:

DHCP-клиент выполняет такие шаги:

  1. Отправляет сообщение DHCPDISCOVER, чтобы найти DHCP-серверы в сети.

  2. Получает предложения DHCPOFFER серверов и выбирает одно.

  3. Отправляет DHCPREQUEST серверу с просьбой выдать IP-адрес.

  4. Получает подтверждение DHCPACK или отказ DHCPNAK сервера.

Добавим функцию ядра sys_dhcp_request и системный вызов dhcp_request, чтобы получить IP-адрес. Протокол DHCP работает поверх UDP, поэтому dhcp_request отправляет пакет, ждет ответа сервера и отправляет пакет повторно, если ответ не получил.

Добавим функцию ядра sleep_for — аналог sys_sleep. Функция засыпает на указанное число тиков таймера.

Не понял, почему на запросы DHCPDISCOVER драйвера DHCP-сервер не отвечает. Сервер отвечает на запросы другой ВМ с Gentoo в этой же сети. Собирал DHCPDISCOVER по аналогии с тем, что отправляет dhcpcd на Gentoo. Напишите в комментариях, что я делаю не так.

А кроме того…

Сетевой адаптер ограничивает размер пакета, который способен передать. Сетевой адаптер способен делить пакеты на фрагменты, которые соблюдают ограничение MTU — Maximum Transfer Unit.

Сетевой адаптер способен считать и проверять контрольные суммы — это разгрузит центральный процессор.

Спецификация VirtIO расскажет, как включить эти опции адаптера.

Сокеты Беркли проедлагают набор функций для общения программ по сети. Функция socket создает объект ядра и возвращает файловый дескриптор, с которым программа работает вызовами recv и send — аналогами read и write. Глава 1 расскажет о файловых дескрипторах.

Мы работали с очередями split virtqueue. Теперь VirtIO предлагает новый формат — packed virtqueue. Код для packed virtqueue работает быстрее split virtqueue.

Очередь packed virtqueue разрешает и драйверу и устройству писать в массив дескрипторов. Драйвер и устройство больше не используют массивы available и used, когда передают друг другу буферы. Буфер packed virtqueue — массив элементов, а не элемент списка, как в split virtqueue. Код работает с непрерывным массивом быстрее, чем с односвязным списком.

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

Ссылки


ссылка на оригинал статьи https://habr.com/ru/articles/826500/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *