Первая статья про драйверы была уж совсем вводной, и мне подумалось, что её нельзя не дополнить рассказом про то, как устроены драйверы более современных устройств.

Для начала введём определение 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 мая вас, хоть и с запозданием. :)

Комментарии (6)


  1. Jef239
    05.05.2016 13:59

    DPC были ещё в RSX-11M, оттуда они перешли в VAX/VMS и потом в WinNT.
    Смысл DPC — не в легковесной поддержке тредов. Наоборот, это обработчик прерываний, вызваемый не из самого прерывания, а чуть погодя.

    Идея состоит в том, что на PDP-11 нужно было как можно быстрее выйти из прерывания, ибо в режиме прерывания другие прерывания заблокированы (стек маленький, его берегли). Поэтому в прерывании делали абсолютный минимум — считывали то, что разрушалось при выходе из прерывания и ставили в очередь DPC. Дальше адрес выхода из прерывания подменялся на вход в обработтчик очереди DPC.

    То есть в большинстве случаев (кроме прерывания, произошедших во время нахождения в некоторых процедурах ядра) DPC обрабатывался синхронно, просто с открытыми прерываниями.

    Был ещё промежуточный уровень — когда открыты более приоритетные прерывания, но не может ещё раз произойти то прерывание, по которому мы уже прерывались. Не помню, как этот режим звался.

    Все это нужно было для того, чтобы пересылать байты на бешенной по тем временам скорости 38400 бод, то есть с прерываниями раз в 300 микросекунд. FIFO тогда не было, и малейшая задержка с закрытыми прерываниями приводила к потере байта


    1. dzavalishin
      05.05.2016 18:26

      Непонятно, зачем для этого новое понятие. Открыть прерывания в драйвере можно и так. Или выставить новый level — многие юниксовые драйвера так и делали, открывая прерывания для своего и более высоких уровней приоритета.


      1. Jef239
        05.05.2016 19:13

        Почему же? Все понятно.
        1) С DPC мы получали существенное ускорение за счет одновременной обработки множества прерываний от одного или нескольких однотипных устройств.
        2) DPC вызывается, когда таблицы ядра консистентны. То есть прерывание может произойти во время изменения таблиц ядра, а DPC — нет.

        Ещё раз. Как происходит обработка при бешенном потоке в 38400 бод (3840 прерываний в секунду) при скорости процессора 300 тысяч операций в секунду. И это — без FIFO.

        1) Прерывание
        1.1) Считали в регистр принятый байт
        1.2) Разрешили вышестоящие прерывания
        1.3) Записали принятый байт в DCB (device control block). памяти там немного, но 16 байт для программного FIFO найдется
        1.4) Добавили DPC в очередь (если его ещё там не было)

        2) DPC вызывает при выходе из прерывания (если были в режиме пользователя). Или при выходе из ядра (если были в режиме ядра)

        3) DPC пробегает DCB всех устройств данного типа и организовывает передачу байт в буфера ядра или прямо в программу пользователя. При этом делаются медленные операции маппинга памяти и кучи разбора системных таблиц.


  1. Ruslan_r
    05.05.2016 13:59

    Может быть так и нужно проектировать современную операционную систему как слой, который изолирован от железа, т.е. концептуальные вещи ничего не знающие о железе? И тогда эта ось сможет работать на любом желе ну или почти на любом.


    1. dzavalishin
      05.05.2016 18:27

      Так и делают. Переносимые ОС делятся на три слоя: (условно) аппаратно независимый, архитектурно зависимый, и специфичный для определённой конфигурации железа.


  1. MacIn
    10.05.2016 16:22

    существует штатная поддержка запуска кода внутри «лёгких» нитей — Deferred Procedure Call

    Это неверно, «нити» тут ни при чем.