\section{Драйверы внешних устройств}

\termin{Драйвер}{driver} представляет собой специализированный
программный модуль, управляющий внешним устройством. Драйверы обеспечивают
единый интерфейс к различным устройствам, тем самым ``отвязывая''
пользовательские программы и ядро ОС от особенностей аппаратуры.

Слово \term{driver} происходит от глагола \termin{to drive}{вести}
и переводится с английского языка как извозчик или шофер: тот, кто
ведет транспортное средство.

Нужно отметить, что большинство ``настоящих'' ОС запрещают пользовательским
программам непосредственный доступ к аппаратуре. Это делается для повышения
надежности и обеспечения безопасности в многопользовательских системах.
В таких системах драйверы являются для прикладных программ единственным
способом доступа к внешнему миру.

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

Например, в большинстве аппаратных реализаций последовательного порта
\standname{RS232} передача байта состоит из четырех шагов: записи значения в 
регистр данных, записи команды ``передавать'' в регистр команды, ожидания
прерывания по концу передачи и проверки успешности передачи путем
считывания статусного регистра устройства. Нарушение последовательности шагов
может приводить к непредсказуемым последствиям -- например, перезапись
регистра данных после подачи команды может приводить к передаче искаженных
данных и т.д.

Нельзя также забывать о неприятностях более высокого уровня -- например,
смешивании вывода разных процессов на печати или устройстве внешней
памяти. Поэтому оказывается необходимо связать с каждым внешним устройством
какой-то разграничитель доступа во времени. В современных ОС эта функция
возлагается именно на драйвер. Обычно один из модулей драйвера представляет
собой процесс-монитор (\term{fork-процесс} в \sysname{VAX/VMS}, 
\term{стратегическую функцию} в \sysname{Unix}, \sysname{OS/2} 
и \sysname{Windows NT}), выполняющий асинхронно поступающие запросы
на доступ к устройству. Подробнее этот механизм обсуждается в разделах
\ref{async_drv} и \ref{QIO}.

Чаще всего драйверы являются частью ядра системы, исполняются в высшем 
кольце защиты и имеют доступ на запись к сегментам данных пользовательских 
программ, а часто и к данным самого ядра. Чтобы избежать этого, пришлось
бы выделять каждый драйвер в отдельное адресное пространство и обеспечивать
обмен данными между драйвером, ядром и пользовательской программой 
посредством статических разделяемых буферов или динамического 
отображения блоков данных между разными адресными пространствами. Оба
решения приводят к значительным накладным расходам, а второе
еще и предъявляет чрезвычайно жесткие требования к архитектуре диспетчера 
памяти (подробнее эта проблема обсуждается в разделе \ref{iAPX432}).

Поэтому подавляющее большинство современных ОС предоставляет драйверам 
практически неконтролируемый доступ к данным пользовательских задач
и ядра. Это означает, что ошибка в драйвере может привести
к разрушению пользовательских программ и самой ОС.

При определении интерфейса драйвера разработчики ОС должны
найти правильный баланс между противоречивыми требованиями:
\begin{itemize}
\item стремлением как можно сильнее упростить драйвер, чтобы облегчить
его разработку и (косвенно) уменьшить вероятность опасных ошибок;
\item желанием предоставить гибкий и интеллектуальный интерфейс
к разнообразным устройствам.
\end{itemize}

\subsection{Функции драйверов}

Понятно, что обеспечить единый интерфейс к разнообразным категориям
устройств, перечисленным в начале раздела \ref{ExternalDevices} по меньшей
мере сложно. Наиболее радикально подошли к этой проблеме разработчики
системы \sysname{UNIX}, разделившие все устройства на два класса:
\term{блочные} (высокоскоростные устройства памяти с произвольным доступом,
в первую очередь дисковые устройства) и \term{последовательные}
или \term{символьные} устройства (вс\"е остальное).

\note{При желании можно  обойтись только последовательными устройствами,
но скорость работы дисковой подсистемы очень важна для производительности
всей машины. \sysname{UNIX} использует различные методы для работы с
разными типами устройств. Механизм доступа к блочным устройствам
подробнее обсуждается в \ref{block_devices}}

Для последовательных устройств определен следующий набор операций:
\begin{itemize}
\item \verb"int open(char * fname, int flags, mode_t mode)" --
процедура инициализации устройства. Например, для лентопротяжек
эта процедура может включать в себя перемотку ленты к началу.
Функция возвращает целочисленный идентификатор-\termin{``ручку''}{handle},
часто называемый также \term{дескриптором файла},
который используется программой при всех последующих обращениях к
устройству;

\item \verb"int read(int handle, char * where, size_t how_much)" --
чтение данных с устройства. Если устройство приспособлено
только для вывода (например, принтер), эта функция может быть не определена;
\item \verb"int write(int handle, char * what, size_t how_much)" --
запись данных на устройство. Если устройство приспособлено
только для ввода, (например, перфоленточный ввод или мышь), эта функция
также может быть не определена;
\item \verb"void close(int handle)" --
процедура закрытия (освобождения) устройства;
\item \verb"int ioctl(int handle, int cmd, ...)" --
процедура задания специальной команды, которая не может быть
сведена к операциям чтения и записи. Набор таких команд зависит от устройства.
Например, для растровых графических устройств
могут быть определены операции установки видеорежима; для последовательных
портов \standname{RS232} это могут быть команды установки скорости,
количества бит, обработки бита четности и т.д., для дисководов -- команды
форматирования носителя;
\item \verb"off_t lseek(int handle, off_t offset, int whence)"
Long seek -- команда перемещения головки чтения/записи к
заданной позиции. Некоторые устройства, например модемы или принтеры,
могут не иметь этой функции.

{\small Слово \term{long} в названии функции появилось по
историческим причинам: в версиях \sysname{Unix} для 16-разрядных машин
индекс позиции не мог обозначаться словом, потому что это ограничивало бы
логическую длину устройства недопустимо малым значением 65384 байт.
Поэтому необходимо было использовать двойное слово, что соответствовало типу
\verb"long" языка \progname{C}.
}

\item \verb"caddr_t mmap(caddr_t addr, size_t len, int prot,"\\
      \verb"             int flags, int handle, off_t offset)"
Memory Map -- отображение устройства в адресное пространство процесса.
Параметр \verb"prot" задает права доступа к отображенному участку:
на чтение, на запись и на исполнение. Отображение может происходить
на заданный виртуальный адрес, или же система может выбирать адрес для
отображения сама.

Эта функция отсутствовала в старых версиях системы, но большинство
современных систем семейства (\sysname{BSD 4.4}, ряд наследников
\sysname{BSD 4.3}, \sysname{SVR4} и \sysname{Linux}) поддерживают ее.

{\small Речь идет об отображении в память данных, хранящихся на устройстве.
Для последовательных устройств ввода/вывода, например, для модемов,
эту функцию невозможно реализовать разумным образом. Напротив, для
лент и других последовательных устройств памяти, поддерживающих функцию
\verb"lseek", отображение может быть реализовано с использованием
аппаратных средств виртуализации памяти и операций \verb"read" и
\verb"write". Смысл в специальной функции отображения появляется
для драйверов устройств, использующих большие объемы памяти, отображенной
в физическое адресное пространство процессора, например, для растровых
видеоадаптеров, некоторых звуковых устройств или страниц
\termint{общей памяти}{backpane memory}{двухпортовой памяти, используемой
как высокоскоростной канал обмена данными в многопроцессорных системах}.
}
\end{itemize}

{\small
Речь в данном случае идет не о функциях самого драйвера, а о системных
вызовах, доступных прикладной программе. В классическом \sysname{UNIX}
и некоторых более поздних системах, например в \sysname{Linux},
эти функции непосредственно отображаются на функции драйвера. Однако
вместо целочисленной ``ручки" драйвер в этих системах получает два
указателя на структуры данных ядра. Например, прототип
функции драйвера \verb"write" (цитируется по \cite{khg})
выглядит в системе \sysname{Linux} как:
\begin{example}
\small
\begin{verbatim}

static int foo_write(struct inode * inode, struct file * file,
                     char * buf, int count)
\end{verbatim}
\end{example}

При открытии файла ядро создает экземпляр \verb"struct file"
и \verb"struct inode" в таблицах, размещенных в так называемой
\termint{пользовательской области}{user area}. Пользовательская область
является атрибутом задачи, но размещена в адресном пространстве ядра
и задаче непосредственно не доступна. Передаваемый программе
целочисленный идентификатор-``ручка" является индексом структур
\verb"file" и \verb"inode" в соответствующих таблицах.

Структура \verb"inode" используется файловой системой \sysname{UNIX}
для определения устройства. Эта структура хранит права доступа к устройству,
ряд сведений, большая часть из которых имеет смысл только для дисковых
файлов, и два числа, идентифицирующих устройство: \term{major} и \term{minor}.
Права доступа проверяются ядром еще до вызова функций дравера.
Major задает номер драйвера в таблицах ядра, а minor -- номер устройства,
управляемого данным драйвером. Например, последовательные порты
\verb"/dev/tty0" и \verb"/dev/tty1" будут управляться одним драйвером
последовательных портов и иметь значения minor, соответственно, 0 и 1.
Ядро использует значение major для того, чтобы выбрать нужный драйвер.

В большинстве ситуаций драйверу интересно только значение поля minor.
Однако существуют хитрые драйверы, использующие также поле major.
Например, в большинстве систем семейства \sysname{Unix} лентопротяжные
устройства имеют два драйвера. Один из драйверов при открытии перематывает
ленту к началу, другой не перематывает. Ничем другим эти драйверы не
отличаются. В действительности оба драйвера используют один и тот же
код, который определяет текущий режим работы в зависимости от
значения поля major.

Структура \verb"file" используется для хранения \term{дескриптора открытого
файла}. Если несколько задач обращается к одному устройству или одна задача
открывает устройство несколько раз, создается несколько копий структуры
\verb"file".  Наиболее важным, с точки зрения драйвера, полем структуры
\verb"file" является положение текущего указателя чтения-записи устройства.
Для таких устройств, как последовательные порты, которые не имеют функции
\verb"lseek", это поле смысла не имеет.

Нужно отметить, что многие последовательные драйверы не позволяют открывать
устройство несколько раз. При последующих вызовах verb"open" для таких
устройств будет возвращаться ошибка \verb"EBUSY" -- ``Устройство занято''.
Например, для лентопротяжек такое поведение представляется вполне разумным. У
драйвера, разрешающего создание нескольких дескрипторов файла для одного
устройства, все функции должны быть реентерабельными. В классическом
\sysname{Unix} и в \sysname{Linux} обеспечение такой реентерабельности
возлагается на разработчика драйвера.

Подробнее интерфейс драйвера в \sysname{Linux} обсуждается в документе
\cite{khg}.
}

Как уже говорилось, для последовательных драйверов в системах семейства
\sysname{Unix} системные вызовы отображаются в функции драйвера,
хотя во многих случаях вместо прямого вызова драйвера используются более
сложные механизмы, обсуждаемые в разделах \ref{async_drv} и \ref{QIO}.
Чаще всего это делается для того, чтобы обеспечить иллюзию реентерабельности
функций драйвера и, таким образом, обеспечить возможность одновременного
использования драйвера несколькими процессами.

Понятно, что систему команд лазерного принтера, плоттера или даже просто
терминала с позиционированием курсора, вроде \machname{vt100},
нельзя свести к операциям \verb"read" и \verb"write", использование
функции \verb"lseek" выглядит несколько искусственно, а соответствующих
команд \verb"ioctl" получилось бы неприемлемо много.
В системах семейства \sysname{UNIX} эта проблема решается очень просто:
система берет на себя только передачу данных между компьютером и устройством,
а за формирование вывода (возможно, это будут не только данные, но
и команды) и, напротив, интерпретацию введенных данных
 отвечает пользовательская программа.

{\small
Отчасти этот подход оправдан тем, что к одному и тому же последовательному
порту \standname{RS232} можно подключить огромное количество разнообразных
устройств:
\begin{itemize}
\item классическое устройство ввода/вывода (терминал, принтер),
\item сетевой адаптер (модем или нуль-модемный кабель),
\item координатное устройство (мышь или дигитайзер),
\item устройство звукового ввода/вывода, (голосовой модем),
\item устройство графического ввода/вывода
(Чем факс-модем не графическое устройство?),
\item контрольно-измерительную аппаратуру
\end{itemize}
и вообще, практически любое достаточно медленное устройство.
}

Понятно, что поступив таким образом, мы переносим проблему поддержки
различных устройств из системы в пользовательскую программу, что не очень
хорошо. Это было осознано еще в 70-е гг. при разработке экранного
редактора \progname{vi}, который должен был работать с большим
количеством различных видеотерминалов, использовавших различные несовместимые
системы команд.

Тогда проблема была решена путем создания системной
базы данных по системам команд разных терминалов. Эта база данных
представляет собой текстовый файл с именем \verb"/etc/termcap".
Причины, по которым этот файл называется именно так, точно неизвестны.
Легенда гласит, что имя происходит от сокращения слов
\termin{terminal capabilities}{возможности терминала}.
Файл состоит из абзацев. Заголовок абзаца представляет собой имя терминала,
а текст состоит из фраз вида \verb"Name=value", разделенных символом
\verb"':'". При этом \verb"Name" представляет собой символическое имя
того или иного свойства, а \verb"value" -- его значение.
Это может быть последовательность символов, формирующих соответствующую
команду или генерируемых терминалом при нажатии соответствующей клавиши.
Кроме того, это может быть именно значение, например, ширина экрана
терминала, измеренная в символах.

Для работы с терминальной базой данных было создано несколько библиотек
подпрограмм -- низкоуровневая библиотека \progname{termcap} и
высокоуровневый пакет \progname{curses}.

Для терминалов описанный подход оказался если и не идеальным, то,
во всяком случае, приемлемым. Но, например, для графических устройств
он не подошел -- системы команд различных устройств оказались слишком
непохожими и не сводимыми к единой системе ``свойств''. Первым
приближением к решению этой проблемы стало создание специализированных
программ-\term{фильтров}. При использовании фильтров пользовательская
программа генерирует графический вывод в виде последовательности команд
некоторого языка. Фильтр принимает эти команды, синтезирует на их основе
изображение, и выводит его на графическое устройство. Вся поддержка
различных устройств вывода возлагается на фильтр, пользовательская
программа должна лишь знать его входной язык.

Самым удачным вариантом языка графического вывода в наше время считается
\progname{PostScript} -- язык управления интеллектуальными принтерами,
разработанный фирмой \entname{Adobe}. \progname{PostScript} предоставляет
богатый набор графических примитивов, но основное его преимущество состоит
в том, что это полнофункциональный язык программирования с условными
операторами, циклами и подпрограммами.
Первоначально этот язык использовался только для управления дорогими моделями
лазерных принтеров, но потом появились интерпретаторы этого языка, способные
выводить \progname{PostScript} на более простые устройства.
Наиболее известной программой этого типа является \progname{GhostScript} --
программа, реализованная в рамках проекта \entname{GNU} и доступная как
freeware. \progname{GhostScript} поставляется в виде исходных текстов на
языке \progname{C}, способен работать во многих операционных системах:
практически во всех ОС семейства \sysname{Unix}, \sysname{OS/2},
\sysname{MS/DR~DOS} и т.д. и поддерживает практически все популярные модели
графических устройств, принтеров, плоттеров и прочего оборудования.

Аналогичный подход используется в оконной системе \progname{X~Window}.
В этой системе весь вывод на терминал осуществляется специальной
программой-сервером. На сервер возлагается задача поддержки различных
графических устройств. В большинстве реализаций сервер исполняется как
обычная пользовательская программа, осуществляя доступ к устройству при
помощи функций \verb"ioctl" ``установить видеорежим'' и
``отобразить видеопамять в адресное пространство процесса''.

В обоих случаях ``драйвер'' оказывается разбит на две части:
собственно драйвер, исполняющийся в режиме ядра, который занимается только
обменом данными с устройством, и программу, интерпретирующую полученные
данные и/или формирующую команды для устройства. Эта программа может быть
довольно сложной, но ошибка в ней не будет фатальной для системы,
так как она исполняется в пользовательском кольце доступа.

Напротив, в операционных системах \sysname{OS/2} и \sysname{Windows~NT}
существует несколько типов драйверов с различными наборами функций.
Так, в \sysname{OS/2} используются драйверы физических устройств 
следующих типов:
\begin{itemize}
\item простые драйверы последовательных устройств ввода/вывода,
аналогичные драйверам символьных устройств в \sysname{Unix};
\item драйверы \term{запоминающих устройств прямого доступа},
аналогичные драйверам блочных устройств в \sysname{Unix};
\item драйверы видеоадаптеров, используемые графической оконной системой
\progname{Presentation Manager} (\progname{PM});
\item драйверы позиционных устройств ввода (мышей и др.),
также используемые \progname{PM};
\item драйверы принтеров и других устройств вывода твердой копии;
\item драйверы звуковых устройств, используемые подсистемой
``мультимедиа'' \progname{MMOS/2};
\item драйверы сетевых адаптеров стандарта \standname{NDIS},
используемые сетевым программным обеспечением фирм \entname{IBM}
и \entname{MicroSoft};
\item драйверы сетевых адаптеров стандарта \standname{ODI},
используемые программным обеспечением фирмы \entname{Novell};
\item \termint{DMD}{Device Manager Driver} {драйвер-менеджер класса
устройств}.
\item различного рода ``фильтры'', например, \progname{ODINSUP.SYS} --
преобразователь \progname{ODI}-интерфейса в \progname{NDIS}.
\end{itemize}

{\small
На функциях DMD следует остановиться подробнее. Рассмотрим достаточно
типичную конфигурацию, содержащую HBA магистрально-модульного 
стандарта \standname{SCSI}, к которому подключены пять устройств: 
жесткий диск, привод CD-ROM, магнитооптический диск, лентопротяжка 
и сканер. При этом каждое из устройств обладает достаточно серьезной 
спецификой, так что их сложно свести к общему набору  функций.

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

CD-ROM, в свою очередь, нельзя рассматривать как удаляемый диск, 
доступный только для записи: ведь практически все CD-ROM приводы, кроме 
функции считывания данных, еще имеют функцию проигрывания музыкальных 
компакт-дисков.

Лентопротяжка и сканер попросту не являются устройствами памяти прямого
доступа, а сканер вообще нельзя рассматривать как устройство памяти.

Когда \sysname{OS/2} управляет описанной аппаратной конфигурацией,
оказываются задействованы пять DMD:

\begin{description}
\item[\tt OS2DASD.DMD] управляет классом запоминающих устройств прямого
доступа \termint{}{DASD}{Direct Access Storage Device}, и предоставляет
стандартные функции для доступа к дискам.
\item[\tt OPTICAL.DMD] обеспечивает управление устройствами
прямого доступа с удаляемыми носителями. Основная его задача -- обработка
аппаратного сигнала смены носителя и оповещение других модулей системы
(дискового кэша, файловой системы) об этой смене.
\item[\tt OS2CDROM.DMD] обеспечивает специфические для приводов CD-ROM
функции, например проигрывание аудизаписей.
\end{description}

Каждый из этих DMD не работает непосредственно с аппаратурой, а транслирует
запросы пользовательских программ и других модулей ядра (в первую очередь,
менеджеров файловых систем) в запросы к драйверу нижнего уровня.
Такой многослойный подход позволяет вынести общую для класса устройств
логику в DMD и не заниматься повторной реализацией этой логики в каждом
новом драйвере.

В случае \standname{SCSI} ситуация дополнительно усложняется тем, что
все запросы к устройствам должны быть оформлены в виде команд 
\standname{SCSI} и пройти через HBA, доступ к которому нужно синхронизовать,
не допуская попыток одновременно передать в HBA две команды. Именно эти 
функции, то есть:
\begin{itemize}
\item трансляцию абстрактных запросов к абстрактным дискам, приводам CD-ROM
и т.д. в последовательности команд \standname{SCSI}, 
\item обеспечение последовательной передачи этих команд через HBA
\item диспетчеризацию пришедших в ответ на команды данных или кодов ошибки
\end{itemize} 

исполняет четвертый DMD: \verb'OS2SCSI.DMD'. Этот DMD передает
запросы на отправку команд \standname{SCSI} \termint{ADD}{Adapter
Device Driver}{драйверу устройства-адаптера}, то есть собственно 
драйверу HBA. От ADD требуется только умение передавать
команды в шину \standname{SCSI} и производить первичный анализ пришедших
на команды ответов: какой из ранее переданных команд соответствует ответ, 
чем завершилась операция -- успехом или ошибкой, пришли ли в ответ данные
и если пришли, то сколько именно и куда их положить и т.д.

Пятый DMD -- \verb'OS2ASPI.DMD' -- обеспечивает сервис \termint{ASPI}{Advanced
SCSI Programming Interface}{продвинутый интерфейс для программирования SCSI}.
Этот сервис дает возможность прикладным программам и другим драйверам 
формировать произвольные команды \standname{SCSI} и таким образом 
осуществлять доступ к устройствам, которые не являются дисками. 
Сервисом \standname{ASPI} пользуются драйверы лентопротяжки и сканера.

Видно, что разработчики \sysname{Unix} и \sysname{OS/2} совершенно
по-разному подошли к решению проблемы, поставленной в начале данного
раздела. Впрочем, нужно отметить, что большинство современных систем
семейства \sysname{Unix} сделали не один шаг на пути к аналогичной 
многослойной структуре драйвера.

Например, в \sysname{Linux} и системах линии \sysname{BSD Unix} за 
управление устройствами \standname{SCSI} отвечает специализированный
модуль ядра, по функциям вполне аналогичный вышеперечисленным пяти DMD.
Он изображает из себя группу драйверов блочных и последовательных
устройств (дисков, приводов CD-ROM, лентопротяжек и т.д.) и транслирует
запросы ко всем этим устройствам в вызовы достаточно простого драйвера
HBA, который аналогичен драйверу адаптера в \sysname{OS/2} и 
непосредственно недоступен для прикладных программ. 

В этих же системах все драйверы терминальных устройств должны уметь 
обрабатывать достаточно обширный набор \verb'ioctl' и выполнять ряд 
важных функций по управлению заданиями. Для реализации соответствующей 
логики ядро предоставляет таким драйверам сервисную библиотеку, которую 
тоже можно рассматривать как аналог DMD. 

В \sysname{Unix System V Release 3} драйверы последовательных устройств
реализуются с использованием модуля \progname{STREAMS}. С каждым
драйвером последовательного устройства связан абстрактный поток данных,
на который можно повесить практически неограниченное количество 
промежуточных фильтров пред- и постобработки данных и модулей 
обработки команд \verb'ioctl'.

Однако такой многослойно-модульный подход не охватывает всей подсистемы
ввода/вывода, что порой приводит к неприятным последствиям.
Например, системы семейства \sysname{Unix} традиционно имеют проблемы 
с удаляемыми носителями в устройствах памяти произвольного доступа.
Проблема состоит в том, что ядро \sysname{Unix} вообще не имеет
представления о том, что устройство может быть внезапно заменено.
При смене устройства оператор должен явным образом сообщить
об этом системе, используя команды \verb'umount' и \verb'mount'.
Если, к примеру, заменить смонтированную дискету, не сообщив об этом
системе, то можно ожидать совершенно неожиданных результатов, начиная
от чтения содержимого удаленной дискеты и кончая порчей данных на новой
дискете и даже разрушением системы.

Впрочем, обработка смены носителя является гораздо более сложной
проблемой, чем просто написание эквивалента \verb'OPTICAL.DMD',
который детектирует сигнал смены носителя. Ведь этот сигнал должен 
быть передан модулям управления дисковым кэшем и файловой системой,
которые, в свою очередь, должны разумно обработать его: как минимум,
дисковый кэш должен объявить все связанные с диском буферы неактуальными,
а менеджер файловой системы должен сбросить все свои внутренние
структуры данных, связанные с удаленным диском, и объяснить всем 
пользовательским программам, работавшим с этим диском, что их данные
пропали. Другие аспекты работы с удаляемыми носителями обсуждаются в
разделе \ref{FaultTolerance}.
}













