Драйверы умного или виртуального железа
Первая статья про драйверы была уж совсем вводной, и мне подумалось, что её нельзя не дополнить рассказом про то, как устроены драйверы более современных устройств.
Для начала введём определение bus master: устройство, способное быть не только ведомым, но и ведущим на шине компьютера. То есть — не только отвечать на транзакции ввода-вывода, инициированные процессором, но и самостоятельно их инициировать — по собственной инициативе «ходить» в память.
История таких устройств уходит корнями в понятие DMA: ещё во времена прародителя микропроцессоров, микропроцессора 8080 (КР5080ИК80), появилось понимание, что процессор хорошо бы разгрузить от рутинной операции перетаскивания байтиков между устройствами в-в и памятью.
Контроллер DMA (Direct Memory Access) был внешней по отношению к устройству ввода-вывода подсистемой, которую нужно было явно запрограммировать — установить тип операции (пишем в память, читаем из памяти, копируем память-память), адрес (а) памяти, и пр. Собственно, я совершенно несправедливо пишу об этом в прошедшем времени — всё это вполне существует и сейчас, например, в микроконтроллерах.
Уже в режиме DMA работа драйвера выглядит сущственно иначе — от драйвера требуется не выполнять ввод-вывод, а подготовить настройки для устройства, активировать его, дождаться прерывания по окончанию ввода-вывода, и проверить успешность операции. Всё сказанное в предыдущей статье верно и для DMA устройств, но в дополнение к сказанному, драйвер должен понимать схему взаимодействия устройства и DMA контроллера, а иногда и явно аллоцировать и настраивать контроллер: если в старых устройствах привязка порта ввода-вывода к контроллеру DMA делалась фиксированно, то сейчас во многих случаях возможен полный роутинг или выбор канала DMA из 2–4 вариантов.
Отдельно следует заметить, что сама инициация очередной транзакции ввода-вывода может быть автоматической (DMA лупит с максимально возможной скоростью), автоматической с настройкой темпа (чтобы не съесть всю пропускную способность шины) или по событию.
При этом событием в развитых системах может быть прерывание, просто изменение состояния ножки микроконтроллера, или же источником события может быть другое устройство. Например, таймер. Это позволяет сопрячь воедино ЦАП, DMA engine и таймер так, чтобы подача очередного байта в ЦАП происходила с заданной (таймером) частотой. Есть и другие варианты агрегирования устройств, например, включение одного канала DMA по окончании работы другого. Без привлечения внимания процессора.
Уместно также сказать, что DMA контроллеры иногда умеют явно сопрягать пару каналов, чтобы обеспечивать непрерывность потока данных по окончании работы одного канала запускается второй и генерируется прерывание, по которому процессор снова загружает работой первый канал — для того же ЦАП это может быть жизненно важно.
Вернёмся из мира контроллеров во «взрослые» машины. Большинство современных подсистем ввода-вывода уже не базируются на внешнем DMA, а имеют его аналог прямо внутри.
Это устройства с режимом «мастер шины», bus master.
Проще всего их представить именно как устройства, имеющие встроенный в себя, личный и оптимально устроенный контроллер DMA.
Обычно такие устройства управляются через дерево дескрипторов в памяти: у устройства есть специальный регистр, в который процессор помещает адрес структуры в памяти, которая содержит задание для контроллера. Или, чаще, массив или список структур с такими заданиями. Контроллер самостоятельно читает из памяти задания и выполняет их шаг за шагом. Задание, как правило, состоит из идентификатора операции, адреса в памяти, где брать данные и дополнительных параметров, необходимых для выполнения операции. Например: { запись на диск, адрес на диске, адрес буфера в памяти }. Так устроены современные контроллеры всего: диска, USB, сетевого интерфейса.
Кроме структуры дескрипторов для такого устройства требуются ещё инструменты для обмена событиями: процессор должен уметь сообщить, что изменил или дополнил дескрипторы, а устройство — что закончило ввод-вывод частично или полностью. Второе выполняется, естественно, через прерывания, а для первого часто применяется регистр (дверной звонок — doorbell), в который процессор «стучится», чтобы обратить внимание устройства на изменения.
При этом есть опасность изменить именно то, что устройство сейчас обрабатывает, что накладывает дополнительные ограничения на структуру драйвера.
Отдельно в этом ряду стоят virtio устройства. Появились они как следствие победного шествия гипервизоров по миру. Традиционно гипервизор предлагал гостевой ОС некоторый набор виртуальных устройств, которые копировали то или иное популярное физическое устройство. Известную сетевую карту или дисковый контроллер. Но эмулировать «железное» устройство тяжело, неудобно и муторно — зачастую, приходится поддерживать совершенно не нужные в виртуальном мире свойства, при этом для целей виртуализации структура реального железного устройства, обычно, неоптимальна.
Это и навело авторов на то, чтобы спроектировать заведомо виртуальные устройства, которые никогда не будут (never say never © 007) реализованы в железе и нужны исключительно для общения гипервизора и гостевой ОС. Они созданы так, чтобы для большого числа разнотипных устройств можно было реализовать общую единообразную инфраструктуру, как в ядре гостевой ОС, так и в гипервизоре.
(Посмотреть реализацию)
По сути драйвер virtio — это транспорт пакетов с запросами и ответами между гостевой ОС и гипервизором. Содержание пакета специфично для типа драйвера и его режима. Например, для сетевой карты это адрес пакета ethernet, а для диска — scatter-gather дескриптор с указанием типа дисковой операции и адреса на диске.
В драйверах virtio ядро заполняет пакет и просит обобщённый драйвер vitio «передать» его устройству. Теперь пока устройство не «передаст» пакет обратно, трогать его нельзя. И наоборот, если устройство умеет делать ввод по внешней инициативе, ему нужно передать несколько пустых (подготовленных для чтения) пакетов — по мере поступления данных (например, входящих сетевых сообщений) пакеты будут заполняться и передаваться назад, ядру гостевой ОС.
Кроме того, стандарт virtio поддерживает возможность стандартным образом ядру и устройству договариваться о режиме работ и поддерживаемых функциях. Например, сетевой драйвер virtio может уметь или не уметь считать и вставлять в отправляемые IP-пакеты контрольную сумму.
Нетрудно видеть, что стандарт virtio описывает довольно типовой обобщённый драйвер bus master устройства: мы передаём «устройству» запрос с адресом в памяти и параметрами запроса ввода-вывода, остальное происходит асинхронно.
На фоне вышесказанного говорить про DPC уже не так актуально, но раз в комментариях возникла дискуссия — дам краткое описание.
В некоторых ОС (Фантом «срисовал» это с NT, откуда они срисовали — не знаю) существует штатная поддержка запуска кода внутри «лёгких» нитей — Deferred Procedure Call. Это позволяет понизить время нахождения драйвера в прерывании: хендлер прерывания лишь фиксирует событие и, как максимум, считывает из устройства статус — один регистр. Остальное делается в DPC, которая быстро активируется и доделывает начатое.
Откровенно сказать, смысла в этом не так уж много — проще запустить в драйвере его собственную нить высокоприоритетную и делать ввод-вывод из неё. Однако, могут быть варианты. Можно выделить DPC группу приоритетов и гарантировать им приоритет всегда более высокий, чем у нити. Можно обеспечить сверхбыстрый шедулинг и передавать таким нитям процессор прямо сразу на выходе именно из того прерывания, которое этот DPC запросило, снизив латентность.
Отдельно отметим, что из DPC можно многое из того, что в прерывании нельзя. Что именно — зависит от ОС. В Фантоме внутри DPC можно всё, в том числе и заснуть на месяц. Грех, но — можно. NT, ЕМНИП, всё же, как-то ограничивает права DPC (то есть, это не обычные нити), но деталей я не помню.
На этом я чувствую, что исполнил свой долг по отношению к драйверам. :)
С 1 мая вас, хоть и с запозданием. :)