Загрузка драйверов
Чаще всего драйверы размещаются в адресном пространстве ядра системы, исполняются в высшем кольце защиты и имеют доступ для записи к сегментам данных пользовательских программ, и, как правило, к данным самого ядра. Это означает, что ошибка в драйвере может привести к разрушению пользовательских программ и самой ОС.
Чтобы избежать этого, пришлось бы выделять каждому драйверу свое адресов пространство и обеспечивать обмен данными между драйвером, ядром и пользовательской программой посредством статических разделяемых буферов или динамического отображения блоков данных между разными адресными пространствами. Оба решения приводят к значительным накладным расходам, а второе еще и предъявляет своеобразные требования к архитектуре диспетчера памяти (подробнее эта проблема обсуждалась в разд. "Взаимно-недоверяющие подсистемы").
Терминальный интерфейс в Unix
На практике иногда – особенно при использовании многоуровневых драйверов – оказывается возможным перенести отдельные функции работы с устройствами в контекст пользовательского процесса. Одна из относительно удачных попыток такого переноса была осуществлена в 70-е годы при разработке экранного редактора vi, для ОС Unix (которая тогда еще была единственным представителем того, что потом превратилось в весьма обширное семейство ОС).
По замыслу разработчиков этот редактор должен был работать с большим количеством разных видеотерминалов, использовавших различные несовместимые системы команд для перемещения курсора и редактирования текста в буфере терминала и столь же несовместимые схемы кодирования специальных клавиш (стрелочек, функциональных клавиш и пр.). Как и в примере с мышами, разные терминалы могли подключаться к компьютеру через различные типы последовательных портов: "токовую петлю", позднее – RS232 и др.
Как в обсуждавшемся в разд. "Многоуровневые драйверы" примере с мышами, в этом случае тоже было естественно разделить драйвер терминала на драйвер порта и модуль, занимающийся генерацией команд и анализом приходящих от терминала кодов расширенных клавиш. Вместо разработки отдельного модуля, решающего вторую часть задачи, была создана системная база данных для систем команд разных терминалов. Эта база данных представляет собой текстовый файл с именем /etc/termcap (terminal capabilities – возможности терминала).
Файл /etc/termcap состоит из абзацев. Заголовок абзаца представляет собой имя терминала, а текст состоит из фраз вида Name=value, разделенных символом ':'. Name представляет собой символическое имя того или иного свойства, a value – его значение. Чаще всего это последовательность символов, формирующих соответствующую команду, или генерируемых терминалом при нажатии соответствующей клавиши. Кроме того, это может быть, например, ширина экрана терминала, измеренная в символах.
Для работы с терминальной базой данных позднее было создано несколько библиотек подпрограмм – низкоуровневая библиотека termcap и высокоуровневый пакет curses.
Для терминалов описанный подход оказался если и не идеальным, то, во всяком случае, приемлемым. Но, например, для графических устройств он не подошел – системы команд различных устройств оказались слишком непохожими и не сводимыми к единой системе "свойств".
Первым приближением к решению этой проблемы стало создание специализированных программ-фильтров. При использовании фильтров пользовательская программа генерирует графический вывод в виде последовательности команд некоторого языка. Фильтр принимает эти команды, синтезирует на их основе изображение и выводит его на графическое устройство. Вся поддержка различных устройств вывода возлагается на фильтр, пользовательская программа должна лишь знать его входной язык.
Самым удачным вариантом языка графического вывода в наше время считается PostScript – язык управления интеллектуальными принтерами, разработанный фирмой Adobe. PostScript предоставляет богатый набор графических примитивов, но основное его преимущество состоит в том, что это полнофункциональный язык программирования с условными операторами циклами и подпрограммами.
Первоначально этот язык использовался только для управления дорогими моделями лазерных принтеров, но потом появились интерпретаторы этого языка способные выводить PostScript на более простые устройства. Наиболее известной программой этого типа является GhostScript– программа, реализованная в рамках проекта GNU и доступная как freeware. GhostScript поставляется в виде исходных текстов на языке С, способна работать во многих операционных системах; практически во всех ОС семейства Unix, OS/2, MS/OR DOS и т. д. и поддерживает практически все популярные модели графических устройств, принтеров, плоттеров и прочего оборудования.
Аналогичный подход используется в оконной системе X Window. В этой системе весь вывод на терминал осуществляется специальной программой-сервером. На сервер возлагается задача поддержки различных графических устройств. В большинстве реализаций сервер исполняется как обычная пользовательская программа, осуществляя доступ к устройству при помощи функций ioctl "установить видеорежим" и "отобразить видеопамять в адресное пространство процесса".
В обоих случаях "драйвер" оказывается разбит на две части: собственно драйвер, исполняющийся в режиме ядра, который занимается только обменом данными с устройством, и программу, интерпретирующую полученные данные и/или формирующую команды для устройства. Эта программа может быть довольно сложной, но ошибка в ней не будет фатальной для системы, так как она исполняется в пользовательском кольце доступа. Проше всего происходит загрузка драйверов в системах, в которых ядро собирается в статический загрузочный модуль. В них драйвер просто присоединяется редактором связей к образу ядра и без каких-либо дополнительных усилии оказывается в памяти в процессе загрузки системы.
В системах с динамической подгрузкой модулей ядра, драйвер представляет собой перемещаемый загрузочный или объектный модуль, иногда того же формата, что и стандартные объектные или загрузочные модули в системе, а иногда и специализированного. В этом случае ядро должно содержать редактор связей, возможно являющийся функциональным подмножеством полноценного системного линкера.
Связывание кода драйвера с используемыми им функциями ядра обычно производится редактором связей в момент подключения модуля к образу ядра при статической сборке и в момент подгрузки модуля при сборке лирической. Однако связывание кода ядра с функциями драйвера, как правило, производится иным образом. Дело в том, что к ядру может подключаться переменное и, в большинстве систем, практически неограниченное количество драйверов. В этом случае обычно оказывается более удобным поддерживать таблицу точек входа зарегистрированных в системе драйверов.
Два основных подхода к формированию такой таблицы – это использование таблицы точек входа в качестве обязательного элемента формата драйвера, либо регистрация точек входа в системной таблице функциями инициализации драйвера.