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
Параметр 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-устройств от базового адреса.
Драйвер общается с устройством, когда пишет и читает регистры устройства.
Добавим область памяти с регистрами сетевого адаптера в таблицу страниц ядра - иначе драйвер получит ошибку доступа к памяти, когда обратится к регистру адаптера. Глава 3 расскажет о таблицах страниц подробнее.
// virtio mmio net interface
kvmmap(kpgtbl, VIRTIO1, VIRTIO1, PGSIZE, PTE_R | PTE_W);
Функция devintr обрабатывает прерывания от устройств. Адаптер генерирует прерывание с номером 2 - объявим константу VIRTIO1_IRQ и вызовем обработчик прерываний virtio_net_intr из devintr:
} else if (irq == VIRTIO1_IRQ) {
virtio_net_intr();
}
Настроим сетевой адаптер
Настроим адаптер, когда ОС стартует:
Сбросим адаптер.
Сообщим, что ОС видит адаптер - установим бит
ACKNOWLEDGE
регистра состояния.Сообщим, что ОС умеет управлять адаптером - установим бит
DRIVER
регистра состояния.Согласуем набор возможностей адаптера - примитивный сетевой драйвер использует лишь
VIRTIO_NET_F_MAC
.Подтвердим выбор - установим бит
FEATURES_OK
регистра состояния.Снова прочтем бит
FEATURES_OK
и убедимся, что бит установлен, иначе устройство отвергло такой набор возможностей.Прочитаем MAC-адрес адаптера, подготовим очереди входящих и исходящих пакетов.
Установим бит
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
Отправим DHCP-запрос
Пишем тесты:
Пишем функции после тестов:
DHCP-клиент выполняет такие шаги:
Отправляет сообщение DHCPDISCOVER, чтобы найти DHCP-серверы в сети.
Получает предложения DHCPOFFER серверов и выбирает одно.
Отправляет DHCPREQUEST серверу с просьбой выдать IP-адрес.
Получает подтверждение 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. Код работает с непрерывным массивом быстрее, чем с односвязным списком.
Обработка прерываний расходует процессорное время впустую, когда драйвер реагирует на прерывание и обрабатывает один запрос, но за это время приходят и другие запросы. Драйвер лучше справится с лавиной запросов, если откликнется на прерывание, отключит прерывания и обработает доступные запросы, а после снова включит прерывания.