ГЛАВА 10. ПОДСИСТЕМА УПРАВЛЕНИЯ ВВОДОМ-ВЫВОДОМ
Подсистема управления вводом-выводом позволяет процессам поддерживать
связь с периферийными устройствами, такими как накопители на магнитных дис-
ках и лентах, терминалы, принтеры и сети, с одной стороны, и с модулями яд-
ра, которые управляют устройствами и именуются драйверами устройств, с дру-
гой. Между драйверами устройств и типами устройств обычно существует одноз-
начное соответствие: в системе может быть один дисковый драйвер для управле-
ния всеми дисководами, один терминальный драйвер для управления всеми терми-
налами и один ленточный драйвер для управления всеми ленточными накопителя-
ми. Если в системе имеются однотипные устройства, полученные от разных изго-
товителей - например, две марки ленточных накопителей, - в этом случае можно
трактовать однотипные устройства как устройства двух различных типов и иметь
для них два отдельных драйвера, поскольку таким устройствам для выполнения
одних и тех же операций могут потребоваться разные последовательности ко-
манд. Один драйвер управляет множеством физических устройств данного типа.
Например, один терминальный драйвер может управлять всеми терминалами, подк-
люченными к системе. Драйвер различает устройства, которыми управляет: вы-
ходные данные, предназначенные для одного терминала, не должны быть посланы
на другой.
Система поддерживает "программные устройства", с каждым из которых не
связано ни одно конкретное физическое устройство. Например, как устройство
трактуется физическая память, чтобы позволить процессу обращаться к ней изв-
не, пусть даже память не является периферийным устройством. Команда ps обра-
щается к информационным структурам ядра в физической памяти, чтобы сообщить
статистику процессов. Еще один пример: драйверы могут вести трассировку за-
писей в удобном для отладки виде, а драйвер трассировки дает возможность
пользователям читать эти записи. Наконец, профиль ядра, рассмотренный в гла-
ве 8, выполнен как драйвер: процесс записывает адреса программ ядра, обнару-
женных в таблице идентификаторов ядра, и читает результаты профилирования.
В этой главе рассматривается взаимодействие между процессами и подсисте-
мой управления вводом-выводом, а также между машиной и драйверами устройств.
Исследуется общая структура и функционирование драйверов и в качестве приме-
ров общего взаимодействия рассматриваются дисковые и терминальные драйверы.
Завершает главу описание нового метода реализации драйверов потоковых уст-
ройств.
10.1 ВЗАИМОДЕЙСТВИЕ ДРАЙВЕРОВ С ПРОГРАММНОЙ И АППАРАТНОЙ СРЕДОЙ
В системе UNIX имеется два типа устройств - устройства вводавывода бло-
ками и устройства неструктурированного или посимвольного ввода-вывода. Как
уже говорилось в главе 2, устройства ввода-вывода блоками, такие как диски и
ленты, для остальной части системы выглядят как запоминающие устройства с
произвольной выборкой; к устройствам посимвольного ввода-вывода относятся
все другие устройства, в том числе терминалы и сетевое оборудование. Устрой-
ства ввода-вывода блоками могут иметь интерфейс и с устройствами посимволь-
ного ввода-вывода.
Пользователь взаимодействует с устройствами через посредничество файло-
вой системы (см. Рисунок 2.1). Каждое устройство имеет имя, похожее на имя
файла, и пользователь обращается к нему как к файлу. Специальный файл уст-
ройства имеет индекс и занимает место в иерархии каталогов файловой системы.
Файл устройства отличается от других файлов типом файла, хранящимся в его
индексе, либо "блочный", либо "символьный специальный", в зависимости от ус-
290
тройства, которое этот файл представляет. Если устройство имеет как блочный,
так и символьный интерфейс, его представляют два файла: специальный файл ус-
тройства ввода-вывода блоками и специальный файл устройства посимвольного
ввода-вывода. Системные функции для обычных файлов, такие как open, close,
read и write, имеют то же значение и для устройств, в чем мы убедимся позже.
Системная функция ioctl предоставляет процессам возможность управлять уст-
ройствами посимвольного ввода-вывода, но не применима в отношении к файлам
обычного типа (*). Тем не менее, драйверам устройств нет необходимости под-
держивать полный набор системных функций. Например, вышеупомянутый драйвер
трассировки дает процессам возможность читать записи, созданные другими
драйверами, но не позволяет создавать их.
10.1.1 Конфигурация системы
Задание конфигурации системы это процедура указания администраторами
значений параметров, с помощью которых производится настройка системы. Неко-
торые из параметров указывают размеры таблиц ядра, таких как таблица процес-
сов, таблица индексов и таблица файлов, а также сколько буферов помещается в
буферном пуле. С помощью других параметров указывается конфигурация устрой-
ств, то есть производятся конкретные указания ядру, какие устройства включа-
ются в данную системную реализацию и их "адрес". Например, в конфигурации
может быть указано, что терминальная плата вставлена в соответствующий разъ-
ем на аппаратной панели.
Существует три стадии, на которых может быть указана конфигурация уст-
ройств. Во-первых, администраторы могут кодировать информацию о конфигурации
в файлах, которые транслируются и компонуются во время построения ядра. Ин-
формация о конфигурации обычно указывается в простом формате, и программа
конфигурации преобразует ее в файл, готовый для трансляции. Во-вторых, адми-
нистраторы могут указывать информацию о конфигурации после того, как система
уже запущена; ядро динамически корректирует внутренние таблицы конфигурации.
Наконец, самоидентифицирующиеся устройства дают ядру возможность узнать, ка-
кие из устройств включены. Ядро считывает аппаратные ключи для самонастрой-
ки. Подробности задания системной конфигурации выходят за пределы этой кни-
ги, однако во всех случаях результатом процедуры задания конфигурации явля-
ется генерация или заполнение таблиц, составляющих основу программ ядра.
Интерфейс "ядро - драйвер" описывается в таблице ключей устройств вво-
да-вывода блоками и в таблице ключей устройств посимвольного ввода-вывода
(Рисунок 10.1). Каждый тип устройства имеет в таблице точки входа, которые
при выполнении системных функций адресуют ядро к соответствующему драйверу.
Функции open и close, вызываемые файлом устройства, "пропускаются" через
таблицы ключей устройств в соответствии с типом файла. Функции mount и
umount так же вызывают выполнение процедур открытия и закрытия устройств, но
для устройств ввода-вывода блоками. Функции read и write, вызываемые устрой-
ствами ввода-вывода блоками и файлами в смонтированных файловых системах,
запускают алгоритмы работы с буферным кешем, инициирующие реализацию страте-
гической процедуры работы с устройствами. Некоторые из драйверов запускают
эту процедуру изнутри из процедур чтения и записи. Более подробно взаимодей-
ствие с каждым драйвером рассматривается в следующем разделе.
Интерфейс "аппаратура - драйвер" состоит из машинно-зависимых управляю-
щих регистров или команд ввода-вывода для управления устройствами и вектора-
ми прерываний: когда происходит прерывание от устройства, система идентифи-
цирует устройство, вызвавшее прерывание, и запускает программу обработки со-
----------------------------------------
(*) И наоборот, системная функция fcntl обеспечивает контроль над действия-
ми, производимыми на уровне дескриптора файла, но не на уровне устройст-
ва. В других реализациях функция ioctl применима для файлов всех типов.
291
ответствующего прерывания. Очевидно, что "программные устройства", такие как
драйвер системы построения профиля ядра (глава 8) не имеют аппаратного ин-
терфейса, однако программы обработки других прерываний могут обращаться к
"обработчику программного прерывания" непосредственно. Например, программа
обработки прерывания по таймеру обращается к программе обработки прерывания
системы построения профиля ядра.
Администраторы устанавливают специальные файлы устройств командой mknod,
в которой указывается тип файла (блочный или символьный), старший и младший
номера устройства. Команда mknod запускает выполнение системной функции с
тем же именем, создающей файл устройства. Например, в командной строке
mknod /dev/tty13 c 2 13
"/dev/tty13" - имя файла устройства, "c" указывает, что тип файла - "сим-
вольный специальный" ("b", соответственно, блочный), "2" - старший номер ус-
тройства, "13" - младший номер устройства. Старший номер устройства показы-
вает его тип, которому соответствует точка входа в таблице ключей устройств,
младший номер устройства - это порядковый номер единицы устройства данного
типа. Если процесс открывает специальный блочный файл с именем "/dev/dsk1" и
кодом 0, ядро запускает программу gdopen в точке 0 таблицы ключей устройств
блочного ввода-вывода (Рисунок 10.2); если процесс читает специальный сим-
вольный файл с именем "/dev/mem" и кодом 3,
Подсистема управления файлами
+------------------------------------------------------------+
| open close |
| open close read write ioctl read write |
| mount umount |
+--+-----+----+-----+-----+------------+-----+-----+-----+---+
| | | | | | | +-+-----+---+
| | | | | | | | функции |
| | | | | | | | работы с |
| | | | | | | | буферным |
| | | | | | | | кешем |
| | | | | | | +-----+-----+
+--+-----+----+-----+-----+-+ ++-----+---------+-----+
| Таблица ключей устройств | | Таблица ключей уст- |
| посимвольного ввода-вывода| | ройств ввода-вывода |
| | | блоками |
+--+-----+----+-----+-----+-+ +--+------+--------+---+
+--+-----+----+-----+-----+-+ +--+------+--------+---+
|open close read write ioctl| Точки | open close strategy|
| | входа | |
| Драйвер | для | Драйвер |
| | драй- | |
|программа обработки преры- | веров | программа обработки |
| ваний от устройства | |прерываний от устройст|
+------------+--------------+ +-----------+----------+
+--------+----------+ +---------+---------+
| Вектор прерывания | | Вектор прерывания |
+--------+----------+ +---------+---------+
+-------------------+----------------+
|
Прерывания от устройств
Рисунок 10.1. Точки входа для драйверов
292
ядро запускает программу mmread в точке 3 таблицы ключей устройств посим-
вольного ввода-вывода. Программа nulldev - это "пустая" программа, использу-
емая в тех случаях, когда отсутствует необходимость в конкретной функции
драйвера. С одним старшим номером устройства может быть связано множество
периферийных устройств; младший номер устройства позволяет отличить их одно
от другого. Не нужно создавать специальные файлы устройств при каждой заг-
рузке системы; их только нужно корректировать, если изменилась конфигурация
системы, например, если к установленной конфигурации были добавлены устройс-
тва.
10.1.2 Системные функции и взаимодействие с драйверами
В этом разделе рассматривается взаимодействие ядра с драйверами устрой-
ств. При выполнении тех системных функций, которые используют дескрипторы
файлов, ядро, следуя за указателями, хранящимися в пользовательском дескрип-
торе файла, обращается к таблице
+-----------------------------------------------+
| таблица ключей устройств ввода-вывода блоками |
+-------+--------+---------+--------------------+
| вход | open | close | strategy |
+-------+--------+---------+--------------------+
| 0 | gdopen | gdclose | gdstrategy |
+-------+--------+---------+--------------------+
| 1 | gtopen | gtclose | gtstrategy |
+-------+--------+---------+--------------------+
+----------------------------------------------------------------+
| таблица ключей устройств посимвольного ввода-вывода |
+------+-----------+-----------+---------+-----------+-----------+
| вход | open | close | read | write | ioctl |
+------+-----------+-----------+---------+-----------+-----------+
| 0 | conopen | conclose | conread | conwrite | conioctl |
+------+-----------+-----------+---------+-----------+-----------+
| 1 | dzbopen | dzbclose | dzbread | dzbwrite | dzbioctl |
+------+-----------+-----------+---------+-----------+-----------+
| 2 | syopen | nulldev | syread | sywrite | syioctl |
+------+-----------+-----------+---------+-----------+-----------+
| 3 | nulldev | nulldev | mmread | mmwrite | nodev |
+------+-----------+-----------+---------+-----------+-----------+
| 4 | gdopen | gdclose | gdread | gdwrite | nodev |
+------+-----------+-----------+---------+-----------+-----------+
| 5 | gtopen | gtclose | gtread | gtwrite | nodev |
+------+-----------+-----------+---------+-----------+-----------+
Рисунок 10.2. Пример заполнения таблиц ключей устройств ввода-
вывода блоками и символами
файлов ядра и к индексу, где оно проверяет тип файла, и переходит к таблице
ключей устройств ввода-вывода блоками или символами. Ядро извлекает из ин-
декса старший и младший номера устройства, использует старший номер в качес-
тве указателя на точку входа в соответствующей таблице и вызывает выполнение
функции драйвера в соответствии с выполняемой системной функцией, передавая
младший номер в качестве параметра. Важным различием в реализации системных
функций для файлов устройств и для файлов обычного типа является то, что ин-
декс специального файла не блокируется в то время, когда ядро выполняет
программу драйвера. Драйверы часто приостанавливают свою работу, ожидая свя-
зи с аппаратными средствами или поступления данных, поэтому ядро не в состо-
293
янии определить, на какое время процесс будет приостановлен. Если индекс
заблокирован, другие процессы, обратившиеся к индексу (например, посредством
системной функции stat), приостановятся на неопределенное время, поскольку
один процесс приостановил драйвер.
Драйвер устройства интерпретирует параметры вызова системной функции в
отношении устройства. Драйвер поддерживает структуры данных, описывающие
состояние каждой контролируемой единицы данного типа устройства; функции
драйвера и программы обработки прерываний реализуются в соответствии с сос-
тоянием драйвера и с тем, какое действие выполняется в этот момент (напри-
мер, данные вводятся или выводятся). Теперь рассмотрим каждый интерфейс бо-
лее подробно.
+------------------------------------------------------------+
| алгоритм open /* для драйверов устройств */ |
| входная информация: имя пути поиска |
| режим открытия |
| выходная информация: дескриптор файла |
| { |
| преобразовать имя пути поиска в индекс, увеличить значе-|
| ние счетчика ссылок в индексе; |
| выделить в таблице файлов место для пользовательского |
| дескриптора файла, как при открытии обычного файла; |
| |
| выбрать из индекса старший и младший номера устройства; |
| |
| сохранить контекст (алгоритм setjmp) в случае передачи |
| управления от драйвера; |
| |
| если (устройство блочного типа) |
| { |
| использовать старший номер устройства в качестве ука-|
| зателя в таблице ключей устройств ввода-вывода бло- |
| ками; |
| вызвать процедуру открытия драйвера по данному индек-|
| су: передать младший номер устройства, режимы откры-|
| тия; |
| } |
| в противном случае |
| { |
| использовать старший номер устройства в качестве ука-|
| зателя в таблице ключей устройств посимвольного вво-|
| да-вывода; |
| вызвать процедуру открытия драйвера по данному индек-|
| су: передать младший номер устройства, режимы откры-|
| тия; |
| } |
| |
| если (открытие в драйвере не выполнилось) |
| привести таблицу файлов к первоначальному виду, |
| уменьшить значение счетчика в индексе; |
| } |
+------------------------------------------------------------+
Рисунок 10.3. Алгоритм открытия устройства
При открытии устройства ядро следует той же процедуре, что и при откры-
294
тии файлов обычного типа (см. раздел 5.1), выделяя в памяти индексы, увели-
чивая значение счетчика ссылок и присваивая значение точки входа в таблицу
файлов и пользовательского дескриптора файла. Наконец, ядро возвращает зна-
чение пользовательского дескриптора файла вызывающему процессу, так что отк-
рытие устройства выглядит так же, как и открытие файла обычного типа. Одна-
ко, перед тем, как вернуться в режим задачи, ядро запускает
зависящую от устройства процедуру open (Рисунок 10.3). Для устройства вво-
да-вывода блоками запускается процедура open, закодированная в таблице клю-
чей устройств ввода-вывода блоками, для устройств посимвольного ввода-вывода
- процедура open, закодированная в соответствующей таблице. Если устройство
имеет как блочный, так и символьный тип, ядро запускает процедуру open, со-
ответствующую типу файла устройства, открытого пользователем: обе процедуры
могут даже быть идентичны, в зависимости от конкретного драйвера.
Зависящая от типа устройства процедура open устанавливает связь между
вызывающим процессом и открываемым устройством и инициализирует информацион-
ные структуры драйвера. Например, процедура open для терминала может приос-
тановить процесс до тех пор, пока в машину не поступит сигнал (аппаратный) о
том, что пользователь предпринял попытку зарегистрироваться. После этого
инициализируются информационные структуры драйвера в соответствии с приняты-
ми установками терминала (например, скоростью передачи информации в бодах).
Для "программных устройств", таких как память системы, процедура open может
не включать в себя инициализацию.
Если во время открытия устройства процессу пришлось приостановиться по
какой-либо из внешних причин, может так случиться, что событие, которое дол-
жно было бы вызвать возобновление выполнения процесса, так никогда и не про-
изойдет. Например, если на данном терминале еще не зарегистрировался ни один
из пользователей, процесс getty, "открывший" терминал (раздел 7.9), приоста-
навливается до тех пор, пока пользователем не будет предпринята попытка ре-
гистрации, при этом может пройти достаточно большой промежуток времени. Ядро
должно иметь возможность возобновить выполнение процесса и отменить вызов
функции open по получении сигнала: ему следует сбросить индекс, отменить
точку входа в таблице файлов и пользовательский дескриптор файла, которые
были выделены перед входом в драйвер, поскольку открытие не произошло. Ядро
сохраняет контекст процесса, используя алгоритм setjmp (раздел 6.4.4), преж-
де чем запустить процедуру open; если процесс возобновляется по сигналу, яд-
ро восстанавливает контекст процесса в том состоянии, которое он имел перед
обращением к драйверу, используя алгоритм longjmp (раздел 6.4.4), и возвра-
щает системе все выделенные процедуре open структуры данных. Точно так же и
драйвер может уловить сигнал и очистить доступные ему структуры данных, если
это необходимо. Ядро также переустанавливает структуры данных файловой сис-
темы, когда драйвер сталкивается с исключительными ситуациями, такими, как
попытка пользователя обратиться к устройству, отсутствующему в данной конфи-
гурации. В подобных случаях функция open не выполняется.
Процессы могут указывать значения различных параметров, характеризующие
особенности выполнения процедуры открытия. Из них наиболее часто использует-
ся "no delay" (без задержки), означающее, что процесс не будет приостановлен
во время выполнения процедуры open, если устройство не готово. Системная
функция open возвращает управление немедленно и пользовательский процесс не
узнает, произошло ли аппаратное соединение или нет. Открытие устройства с
параметром "no delay", кроме всего прочего, затронет семантику вызова функ-
ции read, что мы увидим далее (раздел 10.3.4).
Если устройство открывается многократно, ядро обрабатывает пользователь-
ские дескрипторы файлов, индекс и записи в таблице файлов так, как это опи-
сано в главе 5, запуская определяемую типом устройства процедуру open при
каждом вызове системной функции open. Таким образом, драйвер устройства мо-
жет подсчитать, сколько раз устройство было "открыто", и прервать выполнение
функции open, если количество открытий приняло недопустимое значение. Напри-
295
мер, имеет смысл разрешить процессам многократно "открывать" терминал на за-
пись для того, чтобы пользователи могли обмениваться сообщениями. Но при
этом не следует допускать многократного "открытия" печатающего устройства
для одновременной записи, так как процессы могут затереть друг другу инфор-
мацию. Эти различия имеют смысл скорее на практике, нежели на стадии разра-
ботки: разрешение одновременной записи на терминалы способствует установле-
нию взаимодействия между пользователями; запрещение одновременной записи на
принтеры служит повышению читабельности машинограмм (**).
+------------------------------------------------------------+
| алгоритм close /* для устройств */ |
| входная информация: дескриптор файла |
| выходная информация: отсутствует |
| { |
| выполнить алгоритм стандартного закрытия (глава 5ххх); |
| если (значение счетчика ссылок в таблице файлов не 0) |
| перейти на finish; |
| если (существует еще один открытый файл, старший и млад-|
| ший номера которого совпадают с номерами закрываемого |
| устройства) |
| перейти на finish; /* не последнее закрытие */ |
| если (устройство символьного типа) |
| { |
| использовать старший номер в качестве указателя в |
| таблице ключей устройства посимвольного ввода-выво- |
| да; |
| вызвать процедуру закрытия, определяемую типом драй- |
| вера и передать ей в качестве параметра младший но- |
| мер устройства; |
| } |
| если (устройство блочного типа) |
| { |
| если (устройство монтировано) |
| перейти на finish; |
| переписать блоки устройства из буферного кеша на уст-|
| ройство; |
| использовать старший номер в качестве указателя в |
| таблице ключей устройства ввода-вывода блоками; |
| вызвать процедуру закрытия, определяемую типом драй- |
| вера и передать ей в качестве параметра младший но- |
| мер устройства; |
| сделать недействительными блоки устройства, оставшие-|
| ся в буферном кеше; |
| } |
| finish: |
| освободить индекс; |
| } |
+------------------------------------------------------------+
Рисунок 10.4. Алгоритм закрытия устройства
----------------------------------------
(**) На практике вывод на печать обычно управляется специальными процессами
буферизации, и права доступа устанавливаются таким образом, чтобы толь-
ко система буферизации могла обращаться к принтеру.
296
Процесс разрывает связь с открытым устройством, закрывая его. Однако,
ядро запускает определяемую типом устройства процедуру close только в пос-
леднем вызове функции close для этого устройства, и то только если не оста-
лось процессов, которым устройство необходимо открытым, поскольку процедура
закрытия устройства завершается разрывом аппаратного соединения; отсюда яс-
но, что ядру следует подождать, пока не останется ни одного процесса, обра-
щающегося к устройству. Поскольку ядро запускает процедуру открытия устройс-
тва при каждом вызове системной функции open, а процедуру закрытия только
один раз, драйверу устройства неведомо, сколько процессов используют устрой-
ство в данный момент. Драйверы могут легко выйти из строя, если при их напи-
сании не соблюдалась осторожность: когда при выполнении процедуры close они
приостанавливают свою работу и какой-нибудь процесс открывает устройство до
того, как завершится процедура закрытия, устройство может стать недоступным
для работы, если в результате комбинации вызовов open и close сложилась не-
распознаваемая ситуация.
Алгоритм закрытия устройства похож на алгоритм закрытия файла обычного
типа (Рисунок 10.4). Однако, до того, как ядро освобождает индекс, в нем вы-
полняются действия, специфичные для файлов устройств.
1. Просматривается таблица файлов для того, чтобы убедиться в том, что ни
одному из процессов не требуется, чтобы устройство было открыто. Чтобы
установить, что вызов функции close для устройства является последним,
недостаточно положиться на значение счетчика ссылок в таблице файлов,
поскольку несколько процессов могут обращаться к одному и тому же уст-
ройству, используя различные точки входа в таблице файлов. Так же недос-
таточно положиться на значение счетчика в таблице индексов, поскольку
одному и тому же устройству могут соответствовать несколько файлов уст-
ройства. Например, команда ls -l покажет, что одному и тому же устройст-
ву символьного типа ("c" в начале строки) соответствуют два файла уст-
ройства, старший и младший номера у которых (9 и 1) совпадают. Значение
счетчика связей для каждого файла, равное 1, говорит о том, что имеется
два индекса.
crw--w--w- 1 root vis 9, 1 Aug 6 1984 /dev/tty01
crw--w--w- 1 root unix 9, 1 May 3 15:02 /dev/tty01
Если процессы открывают оба файла независимо один от другого, они обра-
тятся к разным индексам одного и того же устройства.
2. Если устройство символьного типа, ядро запускает процедуру закрытия уст-
ройства и возвращает управление в режим задачи. Если устройство блочного
типа, ядро просматривает таблицу результатов монтирования и проверяет,
не располагается ли на устройстве смонтированная файловая система. Если
такая система есть, ядро не сможет запустить процедуру закрытия устройс-
тва, поскольку не был сделан последний вызов функции close для устройст-
ва. Даже если на устройстве нет смонтированной файловой системы, в бу-
ферном кеше еще могут находиться блоки с данными, оставшиеся от смонти-
рованной ранее файловой системы и не переписанные на устройство, пос-
кольку имели пометку "отложенная запись". Поэтому ядро просматривает бу-
ферный кеш в поисках таких блоков и переписывает их на устройство перед
запуском процедуры закрытия устройства. После закрытия устройства ядро
вновь просматривает буферный кеш и делает недействительными все буферы,
которые содержат блоки для только что закрытого устройства, в то же вре-
мя позволяя буферам с актуальной информацией остаться в кеше.
3. Ядро освобождает индекс файла устройства.
Короче говоря, процедура закрытия устройства разрывает связь с устройст-
вом и инициализирует заново информационные структуры драйвера и аппаратную
часть устройства с тем, чтобы ядро могло бы позднее открыть устройство
297
вновь.
Алгоритмы чтения и записи ядром на устройстве похожи на аналогичные ал-
горитмы для файлов обычного типа. Если процесс производит чтение или запись
на устройстве посимвольного ввода-вывода, ядро запускает процедуры read или
write, определяемые типом драйвера. Несмотря на часто встречающиеся ситуа-
ции, когда ядро осуществляет передачу данных непосредственно между адресным
пространством задачи и устройством, драйверы устройств могут буферизовать
информацию внутри себя. Например, терминальные драйверы для буферизации дан-
ных используют символьные списки (раздел 10.3.1). В таких случаях драйвер
устройства выделяет "буфер", копирует данные из пространства задачи при вы-
полнении процедуры write и выводит их из "буфера" на устройство. Процедура
записи, управляемая драйвером, регулирует объем выводимой информации (т.н.
управление потоком данных): если процессы генерируют информацию быстрее, чем
устройство выводит ее, процедура записи приостанавливает выполнение процес-
сов до тех пор, пока устройство не будет готово принять следующую порцию
данных. При чтении драйвер устройства помещает данные, полученные от устрой-
ства, в буфер и
Память
| |
+-----+
160110| CSR | +------+ +---------tty00
| RDB +---------+ dz00 +-+---------tty01
| TDB | +------+ | ...
+-----+ +---------tty07
160120| CSR | +------+ +---------tty08
160122| RDB +---------+ dz01 +-+---------tty09
160126| TDB | +------+ | ...
+-----+ +---------tty15
| |
Рисунок 10.5. Отображение в памяти ввода-вывода с использова-
нием контроллера VAX DZ11
копирует их из буфера в пользовательские адреса, указанные в вызове систем-
ной функции.
Конкретный метод взаимодействия драйвера с устройством определяется осо-
бенностями аппаратуры. Некоторые из машин обеспечивают отображение ввода-вы-
вода в памяти, подразумевающее, что конкретные адреса в адресном пространст-
ве ядра являются не номерами ячеек в физической памяти, а специальными ре-
гистрами, контролирующими соответствующие устройства. Записывая в указанные
регистры управляющие параметры в соответствии со спецификациями аппаратных
средств, драйвер осуществляет управление устройством. Например, контроллер
ввода-вывода для машины VAX-11 содержит специальные регистры для записи ин-
формации о состоянии устройства (регистры контроля и состояния) и для пере-
дачи данных (буферные регистры), которые формируются по специальным адресам
в физической памяти. В частности, терминальный контроллер VAX DZ11 управляет
8 асинхронными линиями терминальной связи (см. [Levy 80], где более подробно
объясняется архитектура машин VAX). Пусть регистр контроля и состояния (CSR)
для конкретного терминала DZ11 имеет адрес 160120, передающий буферный ре-
гистр (TDB) - адрес 120126, а принимающий буферный регистр (RDB) - адрес
160122 (Рисунок 10.5). Для того, чтобы передать символ на терминал
"/dev/tty09", драйвер терминала записывает единицу (1 = 9 по модулю 8) в
298
указанный двоичный разряд регистра контроля и состояния и затем записывает
символ в передающий буферный регистр. Запись в передающий буферный регистр
является передачей данных. Контроллер DZ11 выставляет бит "выполнено" в ре-
гистре контроля и состояния, когда готов принять следующую порцию данных.
Дополнительно драйвер может выставить бит "возможно прерывание передачи" в
регистре контроля и состояния, что заставляет контроллер DZ11 прерывать ра-
боту системы, когда он готов принять следующую порцию данных. Чтение данных
из DZ11 производится аналогично.
На других машинах имеется программируемый ввод-вывод, подразумевающий,
что в машине имеются инструкции по управлению устройствами. Драйверы управ-
ляют устройствами, выполняя соответствующие инструкции. Например, в машине
IBM 370 имеется инструкция "Start I/O" (Начать ввод-вывод), которая иниции-
рует операцию ввода-вывода, связанную с устройством. Способ связи драйвера с
периферийными устройствами незаметен для пользователя.
Поскольку интерфейс между драйверами устройств и соответствующими аппа-
ратными средствами является машинно-зависимым, на этом уровне не существует
стандартных интерфейсов. Как в случае вводавывода с отображением в памяти,
так и в случае программируемого ввода-вывода драйвер может посылать на уст-
ройство управляющие последовательности с целью установления режима прямого
доступа в память (ПДП) для устройства. Система позволяет осуществлять массо-
вую передачу данных между устройством и памятью в режиме ПДП параллельно с
работой центрального процессора, при этом устройство прерывает работу систе-
мы по завершении передачи данных. Драйвер организует управление виртуальной
памятью таким образом, чтобы ячейки памяти с их действительными номерами ис-
пользовались для ПДП.
Быстродействующие устройства могут иногда передавать данные непосредст-
венно в адресное пространство задачи, без вмешательства буфера ядра. В ре-
зультате повышается скорость передачи данных, поскольку при этом производит-
ся на одну операцию копирования меньше, и, кроме того, объем данных, переда-
ваемых за одну операцию, не ограничивается размером буферов ядра. Драйверы,
осуществляющие такую передачу данных без "обработки", обычно используют
блочный интерфейс для процедур посимвольного чтения и записи, если у них
имеется двойник блочного типа.
10.1.2.4 Стратегический интерфейс
Ядро использует стратегический интерфейс для передачи данных между бу-
ферным кешем и устройством, хотя, как уже говорилось ранее, процедуры чтения
и записи для устройств посимвольного вводавывода иногда пользуются процеду-
рой strategy (их двойника блочного типа) для непосредственной передачи дан-
ных между устройством и адресным пространством задачи. Процедура strategy
может управлять очередностью выполнения заданий на ввод-вывод, связанный с
устройством, или выполнять более сложные действия по планированию выполнения
подобных заданий. Драйверы в состоянии привязывать передачу данных к одному
физическому адресу или ко многим. Ядро передает адрес заголовка буфера стра-
тегической процедуре драйвера; в заголовке содержится список адресов (стра-
ниц памяти) и размеры данных, передаваемых на или с устройства. Аналогичное
действие имеет место при работе механизма свопинга, описанного в главе 9.
При работе с буферным кешем ядро передает данные с одного адреса; во время
свопинга ядро передает данные, расположенные по нескольким адресам (страницы
памяти). Если данные копируются из или в адресное пространство задачи, драй-
вер должен блокировать процесс (или по крайней мере, соответствующие страни-
цы) в памяти до завершения передачи данных.
Например, после монтирования файловой системы ядро идентифицирует каждый
файл в файловой системе по номеру устройства и номеру индекса. В номере уст-
ройства закодированы его старший и младший номера. Когда ядро обращается к
блоку, который принадлежит файлу, оно копирует номер устройства и номер бло-
ка в заголовок буфера, как уже говорилось ранее в главе 3. Обращения к дис-
299
ку, использующие алгоритмы работы с буферным кешем (например, bread или
bwrite), инициируют выполнение стратегической процедуры, определяемой стар-
шим номером устройства. Стратегическая процедура использует значения полей
младшего номера и номера блока из заголовка буфера для идентификации места
расположения данных на устройстве, а адрес буфера - для идентификации места
назначения передаваемых данных. Точно так же, когда процесс обращается к ус-
тройству ввода-вывода блоками непосредственно (например, открывая устройство
и читая или записывая на него), он использует алгоритмы работы с буферным
кешем, и интерфейс при этом функционирует вышеописанным образом.
Системная функция ioctl является обобщением специфичных для терминала
функций stty (задать установки терминала) и gtty (получить установки терми-
нала), имевшихся в ранних версиях системы UNIX. Она выступает в качестве об-
щей точки входа для всех связанных с типом устройства команд и позволяет
процессам задавать аппаратные параметры, ассоциированные с устройством, и
программные параметры, ассоциированные с драйвером. Специальные действия,
выполняемые функцией ioctl для разных устройств различны и определяются ти-
пом драйвера. Программы, использующие вызов ioctl, должны должны знать, с
файлом какого типа они работают, так как они являются аппаратно-зависимыми.
Исключение из общего правила сделано для системы, которая не видит различий
между файлами разных типов. Более подробно использование функции ioctl для
терминалов рассмотрено в разделе 10.3.3.
Синтаксис командной строки, содержащей вызов системной функции:
ioctl(fd,command,arg);
где fd - дескриптор файла, возвращаемый предварительно вызванной функцией
open, command - действие (команда), которое необходимо выполнить драйверу,
arg - параметр команды (может быть указателем на структуру). Команды специ-
фичны для различных драйверов; следовательно, каждый драйвер интерпретирует
команды в соответствии со своими внутренними спецификациями, от команды, в
свою очередь, зависит формат структуры данных, описываемой передаваемым па-
раметром. Драйверы могут считывать структуру данных arg из пространства за-
дачи в соответствии с предопределенным форматом или записывать установки ус-
тройства в пространство задачи по адресу указанной структуры. Например, на-
личие интерфейса, предоставляемого функцией ioctl, дает возможность пользо-
вателям устанавливать для терминала скорость передачи информации в бодах,
перематывать магнитную ленту, и, наконец, выполнять сетевые операции, зада-
вая номера виртуальных каналов и сетевые адреса.
10.1.2.6 Другие функции, имеющие отношение к файловой системе
Такие функции работы с файловой системой, как stat и chmod, выполняются
одинаково, как для обычных файлов, так и для устройств; они манипулируют с
индексом, не обращаясь к драйверу. Даже системная функция lseek работает для
устройств. Например, если процесс подводит головку на лентопротяжном устрой-
стве к указанному адресу смещения в байтах с помощью функции lseek, ядро
корректирует смещение в таблице файлов но не выполняет никаких действий,
специфичных для данного типа драйвера. Когда позднее процесс выполняет чте-
ние (read) или запись (write), ядро пересылает адрес смещения из таблицы
файлов в адресное пространство задачи, подобно тому, как это имеет место при
работе с файлами обычного типа, и устройство физически перемещает головку к
соответствующему смещению, указанному в пространстве задачи. Этот случай ил-
люстрируется на примере в разделе 10.3.
300
Периферийные Соединительная Вектор
устройства панель прерывания
+------------------+
| - |
tty00 -------------+ | - |
tty01 .... | | - |
... | +--+ +------------------+
tty07 -------------+-+ |------------| ttyintr 0 |
tty08 -------------+ +--+ +------------------+
tty09 .... +-+ |------------| ttyintr 1 |
... +--------+ +--+ +------------------+
tty15 ----+ +--------+ |------------| consintr |
консоль ------+ +--+ +------------------+
принтер00 -------------+-+ |------------| printintr 0 |
.... | +--+ +------------------+
принтер03 -------------+ | | | - |
| | | - |
+--+ +------------------+
Рисунок 10.6. Прерывания от устройств
10.1.3 Программы обработки прерываний
Как уже говорилось выше (раздел 6.4.1), возникновение прерывания побуж-
дает ядро запускать программу обработки прерываний, в основе алгоритма кото-
рой лежит соотношение между устройством, вызвавшим прерывание, и смещением в
таблице векторов прерываний. Ядро запускает программу обработки прерываний
для данного типа устройства, передавая ей номер устройства или другие пара-
метры для того, чтобы идентифицировать единицу устройства, вызвавшую преры-
вание. Например, в таблице векторов прерываний на Рисунке 10.6 показаны две
точки входа для обработки прерываний от терминалов ("ttyintr"), каждая из
которых используется для обработки прерываний, поступивших от 8 терминалов.
Если устройство tty09 прервало работу системы, система вызывает программу
обработки прерывания, ассоциированную с местом аппаратного подключения уст-
ройства. Поскольку с одной записью в таблице векторов прерываний может быть
связано множество физических устройств, драйвер должен уметь распознавать
устройство, вызвавшее прерывание. На рисунке записи в таблице векторов пре-
рываний, соответствующие прерываниям от терминалов, имеют метки 0 и 1, чтобы
система различала их между собой при вызове программы обработки прерываний,
используя к примеру этот номер в качестве передаваемого программе параметра.
Программа обработки прерываний использует этот номер и другую информацию,
переданную механизмом прерывания, для того, чтобы удостовериться, что именно
устройство tty09, а не tty12, прервало работу системы. Этот пример в упро-
щенном виде показывает то, что имеет место в реальных системах, где на самом
деле существует несколько уровней контроллеров и соответствующих программ
обработки прерываний, но он иллюстрирует общие принципы.
Если подвести итог, можно сказать, что номер устройства, используемый
программой обработки прерываний, идентифицирует единицу аппаратуры, а млад-
ший номер в файле устройства идентифицирует устройство для ядра. Драйвер ус-
тройства устанавливает соответствие между младшим номером устройства и номе-
ром единицы аппаратуры.
Так сложилось исторически, что дисковые устройства в системах UNIX раз-
бивались на разделы, содержащие различные файловые системы, что означало
301
"деление [дискового] пакета на несколько управляемых по-своему частей" (см.
[System V 84b]). Например, если на диске располагаются четыре файловые сис-
темы, администратор может оставить одну из них несмонтированной, одну смон-
тировать только для чтения, а две других только для записи. Несмотря на то,
что все файловые системы сосуществуют на одном физическом устройстве, поль-
зователи не могут ни обращаться к файлам немонтированной файловой системы,
используя методы доступа, описанные в главах 4 и 5, ни записывать файлы в
файловые системы, смонтированные только для чтения. Более того, так как каж-
дый раздел (и, следовательно, файловая система) занимает на диске смежные
дорожки и цилиндры, скопировать всю файловую систему легче, чем в том слу-
чае, если бы раздел занимал участки, разбросанные по всему дисковому тому.
Дисковый драйвер транслирует адрес файловой системы, состоящий из логи-
ческого номера устройства и номера блока, в точный номер дискового сектора.
Драйвер получает адрес одним из следующих путей: либо стратегическая проце-
дура использует буфер из буферного пула, заголовок которого содержит номера
устройства и блока, либо процедуры чтения и записи передают логический
(младший) номер устройства в качестве параметра; они преобразуют адрес сме-
щения в байтах, хранящийся в пространстве задачи, в адрес соответствующего
блока. Дисковый драйвер использует номер устройства для идентификации физи-
ческого устройства и указания используемого раздела, обращаясь при этом к
внутренним таблицам для поиска сектора, отмечающего начало раздела на диске.
Наконец, он добавляет номер блока в файловой системе к номеру блока, с кото-
рого начинается каждый сектор, чтобы идентифицировать сектор, используемый
для ввода-вывода.
+---------------------------------------------+
| Раздел Начальный блок Длина в блоках |
| |
| Размер блока = 512 байт |
| |
| 0 0 64000 |
| 1 64000 944000 |
| 2 168000 840000 |
| 3 336000 672000 |
| 4 504000 504000 |
| 5 672000 336000 |
| 6 840000 168000 |
| 7 0 1008000 |
+---------------------------------------------+
Рисунок 10.7. Разделы на диске RP07
Исторически сложилось так, что размеры дисковых разделов устанавливаются
в зависимости от типа диска. Например, диск DEC RP07 разбит на разделы, ха-
рактеристика которых приведена на Рисунке 10.7. Предположим, что файлы
"/dev/dsk0", "/dev/dsk1", "/dev/dsk2" и "/dev/dsk3" соответствуют разделам
диска RP07, имеющим номера от 0 до 3, и имеют аналогичные младшие номера.
Пусть размер логического блока в файловой системе совпадает с размером дис-
кового блока. Если ядро пытается обратиться к блоку с номером 940 в файловой
системе, хранящейся в "/dev/dsk3", дисковый драйвер переадресует запрос к
блоку с номером 336940 (раздел 3 начинается с блока, имеющего номер 336000;
336000 + 940 = 336940) на диске.
Размеры разделов на диске варьируются и администраторы располагают фай-
ловые системы в разделах соответствующего размера: большие файловые системы
попадают в разделы большего размера и т. д. Разделы на диске могут перекры-
ваться. Например, разделы 0 и 1 на диске RP07 не пересекаются, но вместе они
занимают блоки с номерами от 0 до 1008000, то есть весь диск. Раздел 7 так
же занимает весь диск. Перекрытие разделов не имеет значения, поскольку фай-
302
ловые системы, хранящиеся в разделах, размещаются таким образом, что между
ними нет пересечений. Иметь один раздел, включающий в себя все дисковое
пространство, выгодно, поскольку весь том можно быстро скопировать.
Использование разделов фиксированного состава и размера ограничивает
гибкость дисковой конфигурации. Информацию о разделах в закодированном виде
не следует включать в дисковый драйвер, но нужно поместить в таблицу содер-
жимого дискового тома. Однако, найти общее место на всех дисках для размеще-
ния таблицы содержимого дискового тома и сохранить тем самым совместимость с
предыдущими версиями системы довольно трудно. В существующих реализациях
версии V предполагается, что блок начальной загрузки первой из файловых сис-
тем на диске занимает первый сектор тома, хотя по логике это, казалось бы,
самое подходящее место для таблицы содержимого тома. И все же дисковый драй-
вер должен иметь закодированную информацию о месте расположения таблицы со-
держимого тома для каждого диска, не препятствуя существованию дисковых раз-
делов переменного размера.
В связи с тем, что для системы UNIX является типичным высокий уровень
дискового трафика, драйвер диска должен максимизировать передачу данных с
тем, чтобы обеспечить наилучшую производительность всей системы. Новейшие
дисковые контроллеры осуществляют планирование выполнения заданий, требующих
обращения к диску, позиционируют головку диска и обеспечивают передачу дан-
ных между диском и центральным процессором; иначе это приходится делать дис-
ковому драйверу.
Сервисные программы могут непосредственно обращаться к диску в обход
стандартного метода доступа к файловой системе, рассмотренного в главах 4 и
5, как пользуясь блочным интерфейсом, так и не прибегая к структурированию
данных. Непосредственно работают с диском две важные программы - mkfs и
fsck. Программа mkfs форматирует раздел диска для файловой системы UNIX,
создавая при этом суперблок, список индексов, список свободных дисковых бло-
ков с указателями и корневой каталог новой файловой системы. Программа fsck
проверяет целостность существующей файловой системы и исправляет ошибки, как
показано в главе 5.
Рассмотрим программу, приведенную на Рисунке 10.8, в применении к файлам
"/dev/dsk15" и "/dev/rdsk15", и предположим, что команда ls выдала следующую
информацию:
ls -1 /dev/dsk15 /dev/rdsk15
br-------- 2 root root 0,21 Feb 12 15:40 /dev/dsk15
crw-rw---- 2 root root 7,21 Mar 7 09:29 /dev/rdsk15
Отсюда видно, что файл "/dev/dsk15" соответствует устройству блочного
типа, владельцем которого является пользователь под именем "root", и только
пользователь "root" может читать с него непосредственно. Его старший номер -
0, младший - 21. Файл "/dev/rdsk15" соответствует устройству посимвольного
ввода-вывода, владельцем которого является пользователь "root", однако права
доступа к которому на запись и чтение есть как у владельца, так и у группы.
Его старший номер - 7, младший - 21. Процесс, открывающий файлы, получает
доступ к устройству через таблицу клю-
чей устройств ввода-вывода блоками и таблицу ключей устройств посимвольного
ввода-вывода, соответственно, а младший номер устройства 21 информирует
драйвер о том, к какому разделу диска производится обращение, например, дис-
ковод 2, раздел 1. Поскольку младшие номера у файлов совпадают, они ссылают-
ся на один и тот же раздел диска, если предположить, что это одно устройство
(***). Таким образом, процесс, выполняющий программу, открывает один и тот
---------------------------------------
(***) Не существует иного способа установить, что символьный и блочный драй-
веры ссылаются на одно и то же устройство, кроме просмотра таблиц сис-
темной конфигурации и текста программ драйвера.
303
же драйвер дважды (используя различные интерфейсы), позиционирует головку к
смещению с адресом 8192 и считывает данные с этого места. Результаты выпол-
нения операций чтения должны быть идентичными при условии, что работает
только одна файловая система.
+------------------------------------------------------------+
| #include "fcntl.h" |
| main() |
| { |
| char buf1[4096], buf2[4096] |
| int fd1, fd2, i; |
| |
| if (((fd1 = open("/dev/dsk5/", O_RDONLY)) == -1) || |
| ((fd2 = open("/dev/rdsk5", O_RDONLY)) == -1))|
| { |
| printf("ошибка при открытии\n"); |
| exit(); |
| } |
| |
| lseek(fd1, 8192L, 0); |
| lseek(fd2, 8192L, 0); |
| |
| if ((read(fd1, buf1, sizeof(buf1)) == -1) || |
| (read(fd2, buf2, sizeof(buf2)) == -1)) |
| { |
| printf("ошибка при чтении\n"); |
| exit(); |
| } |
| |
| for (i = 0; i < sizeof(buf1); i++) |
| if (buf1[i] != buf2[i]) |
| { |
| printf("различие в смещении %d\n", i); |
| exit(); |
| } |
| printf("данные совпадают\n"); |
| } |
+------------------------------------------------------------+
Рисунок 10.8. Чтение данных с диска с использованием блочного
интерфейса и без структурирования данных
Программы, осуществляющие чтение и запись на диск непосредственно, пред-
ставляют опасность, поскольку манипулируют с чувствительной информацией,
рискуя нарушить системную защиту. Администраторам следует защищать интерфей-
сы ввода-вывода путем установки прав доступа к файлам дисковых устройств.
Например, дисковые файлы "/dev/dsk15" и "/dev/rdsk15" должны принадлежать
пользователю с именем "root", и права доступа к ним должны быть определены
таким образом, чтобы пользователю "root" было разрешено чтение, а всем ос-
тальным пользователям и чтение, и запись должны быть запрещены.
Программы, осуществляющие чтение и запись на диск непосредственно, могут
также нарушить целостность данных в файловой системе. Алгоритмы файловой
системы, рассмотренные в главах 3, 4 и 5, координируют выполнение операций
ввода-вывода, связанных с диском, тем самым поддерживая целостность информа-
ционных структур на диске, в том числе списка свободных дисковых блоков и
указателей из индексов на информационные блоки прямой и косвенной адресации.
Процессы, обращающиеся к диску непосредственно, обходят эти алгоритмы. Пусть
даже их программы написаны с большой осторожностью, проблема целостности все
304
равно не исчезнет, если они выполняются параллельно с работой другой файло-
вой системы. По этой причине программа fsck не должна выполняться при нали-
чии активной файловой системы.
Два типа дискового интерфейса различаются между собой по использованию
буферного кеша. При работе с блочным интерфейсом ядро пользуется тем же ал-
горитмом, что и для файлов обычного типа, исключение составляет тот момент,
когда после преобразования адреса смещения логического байта в адрес смеще-
ния логического блока (см. алгоритм bmap в главе 4) оно трактует адрес сме-
щения логического блока как физический номер блока в файловой системе. За-
тем, используя буферный кеш, ядро обращается к данным, и, в конечном итоге,
к стратегическому интерфейсу драйвера. Однако, при обращении к диску через
символьный интерфейс (без структурирования данных), ядро не превращает адрес
смещения в адрес файла, а передает его немедленно драйверу, используя для
передачи рабочее пространство задачи. Процедуры чтения и записи, входящие в
состав драйвера, преобразуют смещение в байтах в смещение в блоках и копиру-
ют данные непосредственно в адресное пространство задачи, минуя буферы ядра.
Таким образом, если один процесс записывает на устройство блочного типа,
а второй процесс затем считывает с устройства символьного типа по тому же
адресу, второй процесс может не считать информацию, записанную первым про-
цессом, так как информация может еще находиться в буферном кеше, а не на
диске. Тем не менее, если второй процесс обратится к устройству блочного ти-
па, он автоматически попадет на новые данные, находящиеся в буферном кеше.
При использовании символьного интерфейса можно столкнуться со странной
ситуацией. Если процесс читает или пишет на устройство посимвольного вво-
да-вывода порциями меньшего размера, чем, к примеру, блок, результаты будут
зависеть от драйвера. Например, если производить запись на ленту по 1 байту,
каждый байт может попасть в любой из ленточных блоков.
Преимущество использования символьного интерфейса состоит в скорости,
если не возникает необходимость в кешировании данных для дальнейшей работы.
Процессы, обращающиеся к устройствам ввода -вывода блоками, передают инфор-
мацию блоками, размер каждого из которых ограничивается размером логического
блока в данной файловой системе. Например, если размер логического блока в
файловой системе 1 Кбайт, за одну операцию ввода-вывода может быть передано
не больше 1 Кбайта информации. При этом процессы, обращающиеся к диску с по-
мощью символьного интерфейса, могут передавать за одну дисковую операцию
множество дисковых блоков, в зависимости от возможностей дискового контрол-
лера. С функциональной точки зрения, процесс получает тот же самый резуль-
тат, но символьный интерфейс может работать гораздо быстрее. Если воспользо-
ваться примером, приведенным на Рисунке 10.8, можно увидеть, что когда про-
цесс считывает 4096 байт, используя блочный интерфейс для файловой системы с
размером блока 1 Кбайт, ядро производит четыре внутренние итерации, на каж-
дом шаге обращаясь к диску, прежде чем вызванная системная функция возвраща-
ет управление, но когда процесс использует символьный интерфейс, драйвер мо-
жет закончить чтение за одну дисковую операцию. Более того, использование
блочного интерфейса вызывает дополнительное копирование данных между адрес-
ным пространством задачи и буферами ядра, что отсутствует в символьном ин-
терфейсе.
10.3 ТЕРМИНАЛЬНЫЕ ДРАЙВЕРЫ
Терминальные драйверы выполняют ту же функцию, что и остальные драйверы:
управление передачей данных от и на терминалы. Однако, терминалы имеют одну
особенность, связанную с тем, что они обеспечивают интерфейс пользователя с
системой. Обеспечивая интерактивное использование системы UNIX, терминальные
драйверы имеют свой внутренний интерфейс с модулями, интерпретирующими ввод
и вывод строк. В каноническом режиме интерпретаторы строк преобразуют нест-
руктурированные последовательности данных, введенные с клавиатуры, в канони-
ческую форму (то есть в форму, соответствующую тому, что пользователь имел в
305
виду на самом деле) прежде, чем послать эти данные принимающему процессу;
строковый интерфейс также преобразует неструктурированные последовательности
выходных данных, созданных процессом, в формат, необходимый пользователю. В
режиме без обработки строковый интерфейс передает данные между процессами и
терминалом без каких-либо преобразований.
Программисты, например, работают на клавиатуре терминала довольно быст-
ро, но с ошибками. На этот случай терминалы имеют клавишу стирания ("erase";
клавиша может быть обозначена таким образом), чтобы пользователь имел воз-
можность стирать часть введенной строки и вводить коррективы. Терминалы пе-
ресылают машине всю введенную последовательность, включая и символы стирания
(*** *). В каноническом режиме строковый интерфейс буферизует информацию в
строки (набор символов, заканчивающийся символом возврата каретки (*****)) и
процессы стирают символы у себя, прежде чем переслать исправленную последо-
вательность считывающему процессу.
В функции строкового интерфейса входят:
* построчный разбор введенных последовательностей;
* обработка символов стирания;
* обработка символов "удаления", отменяющих все остальные символы, введен-
ные до того в текущей строке;
* отображение символов, полученных терминалом;
* расширение выходных данных, например, преобразование символов табуляции
в последовательности пробелов;
* сигнализирование процессам о зависании терминалов и прерывании строк или
в ответ на нажатие пользователем клавиши удаления;
* предоставление возможности не обрабатывать специальные символы, такие
как символы стирания, удаления и возврата каретки.
Функционирование без обработки подразумевает использование асинхронного
терминала, поскольку процессы могут считывать символы в том виде, в каком
они были введены, вместо того, чтобы ждать, когда пользователь нажмет клави-
шу ввода или возврата каретки.
Ричи отметил, что первые строковые интерфейсы, используемые еще при раз-
работке системы в начале 70-х годов, работали в составе программ командного
процессора и редактора, но не в ядре (см. [Ritchie 84], стр.1580). Однако,
поскольку в их функциях нуждается множество программ, их место в составе яд-
ра. Несмотря на то, что строковый интерфейс выполняет такие функции, из ко-
торых логически вытекает его место между терминальным драйвером и остальной
частью ядра, ядро не запускает строковый интерфейс иначе, чем через терми-
нальный драйвер. На Рисунке 10.9 показаны поток данных, проходящий через
терминальный драйвер и строковый интерфейс, и соответствующие ему управляю-
щие воздействия, проходящие через терминальный драйвер. Пользователи могут
указать, какой строковый интерфейс используется посредством вызова системной
функции ioctl, но реализовать схему, по которой одно устройство использовало
бы несколько строковых интерфейсов одновременно, при чем каждый интерфейсный
модуль, в свою очередь, успешно вызывал бы следующий модуль для обработки
данных, довольно трудно.
Строковый интерфейс обрабатывает данные в символьных списках. Символьный
список (clist) представляет собой переменной длины список символьных блоков
с использованием указателей и с подсчетом количества символов в списке. Сим-
---------------------------------------
(****) В этом разделе рассматривается использование терминалов ввода-вывода,
которые передают все символы, введенные пользователем, без обработки.
(*****) В данной главе используется общий термин "возврат каретки" для обоз-
начения символов возврата каретки и перевода строки.
306
Поток данных Поток управляющих
воздействий
+-----------------------+ +-----------------------+
| Процесс чтения/записи | | Процесс чтения/записи |
+-----------------------+ +-----------------------+
- | ^ - | ^
- v | - v |
- +---------------------+ - +-----------------------+
вывод | Строковый интерфейс| ввод | Терминальный драйвер |
- +---------------------+ - +-----------------------+
- | ^ - | ^
- v | - v |
+-----------------------+ +---------------------+
| Терминальный драйвер | | Строковый интерфейс |
+-----------------------+ +---------------------+
| ^
v |
+-----------------------+
| Драйвер ввода-вывода |
+-----------------------+
| ^
v |
+-------------------------+
| Устройство ввода-вывода |
+-------------------------+
Рисунок 10.9. Последовательность обращений и поток данных че-
рез строковый интерфейс
Указатель Смещение Смещение
на до до
следующий начала конца Массив символов
блок 0 1 2 3 4 5 6 7 8 9 14
----------+---------+---------++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+---
| 7 | 14 ||g|a|r|b|a|g|e||| |e|q|n| ||| |...
----+-----+---------+---------++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+---
|
v
Рисунок 10.10. Символьный блок
вольный блок (cblock) содержит указатель на следующий блок в списке, неболь-
шой массив хранимой в символьном виде информации и адреса смещений, показы-
вающие место расположения внутри блока корректной информации (Рисунок
10.10). Смещение до начала показывает первую позицию расположения корректной
информации в массиве, смещение до конца показывает первую позицию расположе-
ния некорректной информации.
Ядро обеспечивает ведение списка свободных символьных блоков и выполняет
над символьными списками и символьными блоками шесть операций.
1. Ядро назначает драйверу символьный блок из списка свободных символьных
блоков.
2. Оно также возвращает символьный блок в список свободных символьных бло-
ков.
3. Ядро может выбирать первый символ из символьного списка: оно удаляет
первый символ из первого символьного блока в списке и устанавливает зна-
чения счетчика символов в списке и указателей в блоке таким образом,
чтобы последующие операции не выбирали один и тот же символ. Если в ре-
307
зультате операции выбран последний символ блока, ядро помещает в список
свободных символьных блоков пустой блок и переустанавливает указатели в
символьном списке. Если в символьном списке отсутствуют символы, ядро
возвращает пустой символ.
4. Ядро может поместить символ в конец символьного списка путем поиска пос-
леднего символьного блока в списке, включения символа в него и переуста-
новки адресов смещений. Если символьный блок заполнен, ядро выделяет но-
вый символьный блок, включает его в конец символьного списка и помещает
символ в новый блок.
5. Ядро может удалять от начала списка группу символов по одному блоку за
одну операцию, что эквивалентно удалению всех символов в блоке за один
раз.
6. Ядро может поместить блок с символами в конец символьного списка.
Символьные списки позволяют создать несложный механизм буферизации, по-
лезный при небольшом объеме передаваемых данных, типичном для медленных уст-
ройств, таких как терминалы. Они дают возможность манипулировать с данными с
каждым символом в отдельности и с группой символьных блоков. Например, Рису-
нок 10.11 иллюстрирует удаление символов из символьного списка; ядро удаляет
по одному символу из первого блока в списке (Рисунок 10.11а-в) до тех пор,
пока в блоке не останется ни одного символа (Рисунок
символьный символьные
список блоки
+-------+ +---+---+---+---+---+---+---+---+
+----->| 0 8 | | p | i | c | | f | i | l | e |
| ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----------+-+ | | 0 8 | | * | | | | | t | b | l | |
| 27 | +--+ ++------+ +---+---+---+---+---+---+---+---+
| символов | +--+ v
+----------+-+ | +-------+ +---+---+---+---+---+---+---+---+
| | 0 8 | | | | | t | r | o | f | f | |
| ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----->| 0 3 | | - | m | m | | | | | |
+-------+ +---+---+---+---+---+---+---+---+
(а)
+-------+ +---+---+---+---+---+---+---+---+
+----->| 1 8 | | | i | c | | f | i | l | e |
| ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----------+-+ | | 0 8 | | * | | | | | t | b | l | |
| 26 | +--+ ++------+ +---+---+---+---+---+---+---+---+
| символов | +--+ v
+----------+-+ | +-------+ +---+---+---+---+---+---+---+---+
| | 0 8 | | | | | t | r | o | f | f | |
| ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----->| 0 3 | | - | m | m | | | | | |
+-------+ +---+---+---+---+---+---+---+---+
(б)
308
+-------+ +---+---+---+---+---+---+---+---+
+----->| 2 8 | | | | c | | f | i | l | e |
| ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----------+-+ | | 0 8 | | * | | | | | t | b | l | |
| 25 | +--+ ++------+ +---+---+---+---+---+---+---+---+
| символов | +--+ v
+----------+-+ | +-------+ +---+---+---+---+---+---+---+---+
| | 0 8 | | | | | t | r | o | f | f | |
| ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----->| 0 3 | | - | m | m | | | | | |
+-------+ +---+---+---+---+---+---+---+---+
(в)
+-------+ +---+---+---+---+---+---+---+---+
+----->| 0 8 | | * | | | | | t | b | l | |
| ++------+ +---+---+---+---+---+---+---+---+
+----------+-+ | v
| 19 | +--+ +-------+ +---+---+---+---+---+---+---+---+
| символов | +--+ | 0 8 | | | | | t | r | o | f | f | |
+----------+-+ | ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----->| 0 3 | | - | m | m | | | | | |
+-------+ +---+---+---+---+---+---+---+---+
(г)
Рисунок 10.11. Удаление символов из символьного списка
символьный символьные
список блоки
+-------+ +---+---+---+---+---+---+---+---+
+----->| 0 8 | | p | i | c | | f | i | l | e |
| ++------+ +---+---+---+---+---+---+---+---+
+----------+-+ | v
| 22 | +--+ +-------+ +---+---+---+---+---+---+---+---+
| символа | +--+ | 0 8 | | * | | | | | t | b | l | |
+----------+-+ | ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----->| 0 6 | | | | | t | r | o | f | | |
+-------+ +---+---+---+---+---+---+---+---+
(а)
+-------+ +---+---+---+---+---+---+---+---+
+----->| 0 8 | | p | i | c | | f | i | l | e |
| ++------+ +---+---+---+---+---+---+---+---+
+----------+-+ | v
| 23 | +--+ +-------+ +---+---+---+---+---+---+---+---+
| символа | +--+ | 0 8 | | * | | | | | t | b | l | |
+----------+-+ | ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----->| 0 7 | | | | | t | r | o | f | f | |
+-------+ +---+---+---+---+---+---+---+---+
309
(б)
+-------+ +---+---+---+---+---+---+---+---+
+----->| 0 8 | | p | i | c | | f | i | l | e |
| ++------+ +---+---+---+---+---+---+---+---+
+----------+-+ | v
| 24 | +--+ +-------+ +---+---+---+---+---+---+---+---+
| символа | +--+ | 0 8 | | * | | | | | t | b | l | |
+----------+-+ | ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----->| 0 8 | | | | | t | r | o | f | f | |
+-------+ +---+---+---+---+---+---+---+---+
(в)
+-------+ +---+---+---+---+---+---+---+---+
+----->| 0 8 | | p | i | c | | f | i | l | e |
| ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----------+-+ | | 0 8 | | * | | | | | t | b | l | |
| 25 | +--+ ++------+ +---+---+---+---+---+---+---+---+
| символов | +--+ v
+----------+-+ | +-------+ +---+---+---+---+---+---+---+---+
| | 0 8 | | | | | t | r | o | f | f | |
| ++------+ +---+---+---+---+---+---+---+---+
| v
| +-------+ +---+---+---+---+---+---+---+---+
+----->| 0 1 | | - | | | | | | | |
+-------+ +---+---+---+---+---+---+---+---+
(г)
Рисунок 10.12. Включение символов в символьный список
10.11г); затем оно устанавливает указатель списка на следующий блок, который
становится первым блоком в списке. Подобно этому на Рисунке 10.12 показано,
как ядро включает символы в символьный список; при этом предполагается, что
в одном блоке помещается до 8 символов и что ядро размещает новый блок в
конце списка (Рисунок 10.12г).
10.3.2 Терминальный драйвер в каноническом режиме
Структуры данных, с которыми работают терминальные драйверы, связаны с
тремя символьными списками: списком для хранения данных, выводимых на терми-
нал, списком для хранения неструктурированных вводных данных, поступивших в
результате выполнения программы обработки прерывания от терминала, вызванно-
го попыткой пользователя ввести данные с клавиатуры, и списком для хранения
обработанных входных данных, поступивших в результате преобразования строко-
вым интерфейсом специальных символов (таких как символы стирания и удаления)
в неструктурированном списке.
Когда процесс ведет запись на терминал (Рисунок 10.13), терминальный
драйвер запускает строковый интерфейс. Строковый интерфейс в цикле считывает
символы из адресного пространства процесса и помещает их в символьный список
для хранения выводных данных до тех пор, пока поток данных не будет исчер-
пан. Строковый интерфейс обрабатывает выводимые символы, например, заменяя
символы табуляции на последовательности пробелов. Если количество символов в
списке для хранения выводных данных превысит верхнюю отметку, строковый ин-
терфейс вызывает процедуры драйвера, пересылающие данные из символьного
списка на терминал и после этого приостанавливающие выполнение процесса, ве-
310
+------------------------------------------------------------+
| алгоритм terminal_write |
| { |
| выполнить (пока из пространства задачи еще поступают |
| данные) |
| { |
| если (на терминал поступает информация) |
| { |
| приступить к выполнению операции записи данных |
| из списка, хранящего выводные данные; |
| приостановиться (до того момента, когда терми- |
| нал будет готов принять следующую порцию дан- |
| ных); |
| продолжить; /* возврат к началу цикла */ |
| } |
| скопировать данные в объеме символьного блока из |
| пространства задачи в список, хранящий выводные |
| данные: строковый интерфейс преобразует символы |
| табуляции и т.д.; |
| } |
| |
| приступить к выполнению операции записи данных из спис-|
| ка, хранящего выводные данные; |
| } |
+------------------------------------------------------------+
Рисунок 10.13. Алгоритм переписи данных на терминал
дущего запись. Когда объем информации в списке для хранения выводных данных
падает за нижнюю отметку, программа обработки прерываний возобновляет выпол-
нение всех процессов, приостановленных до того момента, когда терминал смо-
жет принять следующую порцию данных. Строковый интерфейс завершает цикл об-
работки, скопировав всю выводимую информацию из адресного пространства зада-
чи в соответствующий символьный список, и вызывает выполнение процедур драй-
вера, пересылающих данные на терминал, о которых уже было сказано выше.
Если на терминал ведут запись несколько процессов, они независимо друг
от друга следуют указанной процедуре. Выводимая информация может быть иска-
жена; то есть на терминале данные, записываемые процессами, могут пересе-
каться. Это может произойти из-за того, что процессы ведут запись на терми-
нал, используя несколько вызовов системной функции write. Ядро может перек-
лючать контекст, пока процесс выполняется в режиме задачи, между последова-
тельными вызовами функции write, и вновь запущенные процессы могут вести за-
пись на терминал, пока первый из процессов приостановлен. Выводимые данные
могут быть также искажены и на терминале, поскольку процесс может приостано-
виться на середине выполнения системной функции write, ожидая завершения вы-
вода на терминал из системы предыдущей порции данных. Ядро может запустить
другие процессы, которые вели запись на терминал до того, как первый процесс
был повторно запущен. По этой причине, ядро не гарантирует, что содержимое
буфера данных, выводимое в результате вызова системной функции write, поя-
вится на экране терминала в непрерывном виде.
Рассмотрим программу, приведенную на Рисунке 10.14. Родительский процесс
создает до 18 порожденных процессов; каждый из порожденных процессов записы-
вает строку (с помощью библиотечной функции sprintf) в массив output, кото-
рый включает сообщение и значение счетчика i в момент выполнения функции
fork, и затем входит в цикл пошаговой переписи строки в файл стандартного
вывода. Если стандартным выводом является терминал, терминальный драйвер ре-
гулирует поток поступающих данных. Выводимая строка имеет более 64 символов
311
+----------------------------------------------------------------+
| char form[]="это пример вывода строки из порожденного процесса"| |
| main() |
| { |
| char output[128]; |
| int i; |
| |
| for (i = 0; i < 18; i++) |
| { |
| switch (fork()) |
| { |
| case -1: /* ошибка --- превышено максимальное чис-|
| ло процессов */ |
| exit(); |
| |
| default: /* родительский процесс */ |
| break; |
| |
| case 0: /* порожденный процесс */ |
| /* формат вывода строки в переменной output */ |
| sprintf(output,"%%d\n%s%d\n",form,i,form,i); |
| for (;;) |
| write(1,output,sizeof(output)); |
| } |
| } |
| } |
+----------------------------------------------------------------+
Рисунок 10.14. Передача данных через стандартный вывод
в длину, то есть слишком велика для того, чтобы поместиться в символьном
блоке (длиной 64 байта) в версии V системы. Следовательно, терминальному
драйверу требуется более одного символьного блока для каждого вызова функции
write, иначе выводной поток может стать искаженным. Например, следующие
строки были частью выводного потока, полученного в результате выполнения
программы на машине AT&T 3B20:
this is a sample output string from child 1
this is a sample outthis is a sample output string from child 0
Чтение данных с терминала в каноническом режиме более сложная операция.
В вызове системной функции read указывается количество байт, которые процесс
хочет считать, но строковый интерфейс выполняет чтение по получении символа
перевода каретки, даже если количество символов не указано. Это удобно с
практической точки зрения, так как процесс не в состоянии предугадать,
сколько символов пользователь введет с клавиатуры, и, с другой стороны, не
имеет смысла ждать, когда пользователь введет большое число символов. Напри-
мер, пользователи вводят командные строки для командного процессора shell и
ожидают ответа shell'а на команду по получении символа возврата каретки. При
этом нет никакой разницы, являются ли введенные строки простыми командами,
такими как "date" или "who", или же это более сложные последовательности ко-
манд, подобные следующей:
pic file* | tbl | eqn | troff -mm -Taps | apsend
Терминальный драйвер и строковый интерфейс ничего не знают о синтаксисе
командного процессора shell, и это правильно, поскольку другие программы,
которые считывают информацию с терминалов (например, редакторы), имеют раз-
312
личный синтаксис команд. Поэтому строковый интерфейс выполняет чтение по по-
лучении символа возврата каретки.
На Рисунке 10.15 показан алгоритм чтения с терминала. Предположим, что
терминал работает в каноническом режиме; в разделе 10.3.3 будет рассмотрена
работа в режиме без обработки. Если в настоящий момент в любом из символьных
списков для хранения вводной информации отсутствуют данные, процесс, выпол-
+------------------------------------------------------------+
| алгоритм terminal_read |
| { |
| если (в каноническом символьном списке отсутствуют дан- |
| ные) |
| { |
| выполнить (пока в списке для неструктурированных |
| вводных данных отсутствует информация) |
| { |
| если (терминал открыт с параметром "no delay" |
| (без задержки)) |
| возвратить управление; |
| если (терминал в режиме без обработки с использо-|
| ванием таймера и таймер не активен) |
| предпринять действия к активизации таймера |
| (таблица ответных сигналов); |
| приостановиться (до поступления данных с термина-|
| ла); |
| } |
| |
| /* в списке для неструктурированных вводных данных |
| есть информация */ |
| если (терминал в режиме без обработки) |
| скопировать все данные из списка для неструктури-|
| рованных вводных данных в канонический список; |
| в противном случае /* терминал в каноническом ре- |
| жиме */ |
| { |
| выполнить (пока в списке для неструктурированных |
| вводных данных есть символы) |
| { |
| копировать по одному символу из списка для |
| неструктурированных вводных данных в кано- |
| нический список: |
| выполнить обработку символов стирания и уда-|
| ления; |
| если (символ - "возврат каретки" или "конец |
| файла") |
| прерваться; /* выход из цикла */ |
| } |
| } |
| } |
| |
| выполнить (пока в каноническом списке еще есть символы |
| и не исчерпано количество символов, указанное в вызове |
| функции read) |
| копировать символы из символьных блоков канонического|
| списка в адресное пространство задачи; |
| } |
+------------------------------------------------------------+
Рисунок 10.15. Алгоритм чтения с терминала
313
няющий чтение, приостанавливается до поступления первой строки данных. Когда
данные поступают, программа обработки прерывания от терминала запускает
"программу обработки прерывания" строкового интерфейса, которая помещает
данные в список для хранения неструктурированных вводных данных для передачи
процессам, осуществляющим чтение, и в список для хранения выводных данных,
передаваемых в качестве эхосопровождения на терминал. Если введенная строка
содержит символ возврата каретки, программа обработки прерывания возобновля-
ет выполнение всех приостановленных процессов чтения. Когда процесс, осущес-
твляющий чтение, выполняется, драйвер выбирает символы из списка для хране-
ния неструктурированных вводных данных, обрабатывает символы стирания и уда-
ления и помещает символы в канонический символьный список. Затем он копирует
строку символов в адресное пространство задачи до символа возврата каретки
или до исчерпания числа символов, указанного в вызове системной функции
read, что встретится раньше. Однако, процесс может обнаружить, что данных,
ради которых он возобновил свое выполнение, больше не существует: другие
процессы считали данные с терминала и удалили их из списка для неструктури-
рованных вводных данных до того, как первый процесс был запущен вновь. Такая
ситуация похожа на ту, которая имеет место, когда из канала считывают данные
несколько процессов.
Обработка символов в направлении ввода и в направлении вывода асиммет-
рична, что видно из наличия двух символьных списков для ввода и одного - для
вывода. Строковый интерфейс выводит данные из пространства задачи, обрабаты-
вает их и помещает их в список для хранения выводных данных. Для симметрии
следовало бы иметь
только один список для вводных данных. Однако, в таком случае потребовалось
бы использование программы обработки прерываний для интерпретации символов
+--------------------------------------------------------------+
| char input[256]; |
| |
| main() |
| { |
| register int i; |
| |
| for (i = 0; i < 18; i++) |
| { |
| switch (fork()) |
| { |
| case -1: /* ошибка */ |
| printf("операция fork не выполнена из-за ошибки\n");|
| exit(); |
| |
| default: /* родительский процесс */ |
| break; |
| |
| case 0: /* порожденный процесс */ |
| for (;;) |
| { |
| read(0,input,256); /* чтение строки */ |
| printf("%d чтение %s\n",i,input); |
| } |
| } |
| } |
| } |
+--------------------------------------------------------------+
Рисунок 10.16. Конкуренция за данные, вводимые с терминала
314
стирания и удаления, что сделало бы процедуру более сложной и длительной и
запретило бы возникновение других прерываний на все критическое время. Ис-
пользование двух символьных списков для ввода подразумевает, что программа
обработки прерываний может просто сбросить символы в список для неструктури-
рованных вводных данных и возобновить выполнение процесса, осуществляющего
чтение, который собственно и возьмет на себя работу по интерпретации вводных
данных. При этом программа обработки прерываний немедленно помещает введен-
ные символы в список для хранения выводных данных, так что пользователь ис-
пытывает лишь минимальную задержку при просмотре введенных символов на тер-
минале.
На Рисунке 10.16 приведена программа, в которой родительский процесс по-
рождает несколько процессов, осуществляющих чтение из файла стандартного
ввода, конкурируя за получение данных, вводимых с терминала. Ввод с термина-
ла обычно осуществляется слишком медленно для того, чтобы удовлетворить все
процессы, ведущие чтение, поэтому процессы большую часть времени находятся в
приостановленном состоянии в соответствии с алгоритмом terminal_read, ожидая
ввода данных. Когда пользователь вводит строку данных, программа обработки
прерываний от терминала возобновляет выполнение всех процессов, ведущих чте-
ние; поскольку они были приостановлены с одним и тем же уровнем приоритета,
они выбираются для запуска с одинаковым уровнем приоритета. Пользователь не
в состоянии предугадать, какой из процессов выполняется и считывает строку
данных; успешно созданный процесс печатает значение переменной i в момент
его создания. Все другие процессы в конце концов будут запущены, но вполне
возможно, что они не обнаружат введенной информации в списках для хранения
вводных данных и их выполнение снова будет приостановлено. Вся процедура
повторяется для каждой введенной строки; нельзя дать гарантию, что ни один
из процессов не захватит все введенные данные.
Одновременному чтению с терминала несколькими процессами присуща неод-
нозначность, но ядро справляется с ситуацией наилучшим образом. С другой
стороны, ядро обязано позволять процессам одновременно считывать данные с
терминала, иначе порожденные командным процессором shell процессы, читающие
из стандартного ввода, никогда не будут работать, поскольку shell тоже обра-
щается к стандартному вводу. Короче говоря, процессы должны синхронизировать
свои обращения к терминалу на пользовательском уровне.
Когда пользователь вводит символ "конец файла" (Ctrl-d в ASCII), строко-
вый интерфейс передает функции read введенную строку до символа конца файла,
но не включая его. Он не передает данные (код возврата 0) функции read, если
в символьном списке встретился только символ "конец файла"; вызывающий про-
цесс сам распознает, что обнаружен конец файла и больше не следует считывать
данные с терминала. Если еще раз обратиться к примерам программ по shell'у,
приведенным в главе 7, можно отметить, что цикл работы shell'а завершается,
когда пользователь нажимает : функция read возвращает 0 и произво-
дится выход из shell'а.
В этом разделе рассмотрена работа терминалов ввода-вывода, которые пере-
дают данные на машину по одному символу за одну операцию, в точности как
пользователь их вводит с клавиатуры. Интеллектуальные терминалы подготавли-
вают свой вводной поток на внешнем устройстве, освобождая центральный про-
цессор для другой работы. Структура драйверов для таких терминалов походит
на структуру драйверов для терминалов ввода-вывода, несмотря на то, что фун-
кции строкового интерфейса различаются в зависимости от возможностей внешних
устройств.
10.3.3 Терминальный драйвер в режиме без обработки символов
Пользователи устанавливают параметры терминала, такие как символы стира-
ния и удаления, и извлекают значения текущих установок с помощью системной
315
функции ioctl. Сходным образом они устанавливают необходимость эхо-сопровож-
дения ввода данных с терминала, задают скорость передачи информации в бодах,
заполняют очереди символов ввода и вывода или вручную запускают и останавли-
вают выводной поток символов. В информационной структуре терминального драй-
вера хранятся различные управляющие установки (см. [SVID 85], стр.281), и
строковый интерфейс получает параметры функции ioctl и устанавливает или
считывает значения соответствующих полей структуры данных. Когда процесс ус-
танавливает значения параметров терминала, он делает это для всех процессов,
использующих терминал. Установки терминала не сбрасываются автоматически при
выходе из процесса, сделавшего изменения в установках.
Процессы могут также перевести терминал в режим без обработки символов,
в котором строковый интерфейс передает символы в точном соответствии с тем,
как пользователь ввел их: обработка вводного потока полностью отсутствует.
Однако, ядро должно знать, когда выполнить вызванную пользователем системную
функцию read, поскольку символ возврата каретки трактуется как обычный вве-
денный символ. Оно выполняет функцию read после того, как с терминала будет
введено минимальное число символов или по прохождении фиксированного проме-
жутка времени от момента получения с терминала любого набора символов. В
последнем случае ядро хронометрирует ввод символов с терминала, помещая за-
писи в таблицу ответных сигналов (глава 8). Оба критерия (минимальное число
символов и фиксированный промежуток времени) задаются в вызове функции
ioctl. Когда соответствующие критерии удовлетворены, программа обработки
прерываний строкового интерфейса возобновляет выполнение всех приостановлен-
ных процессов. Драйвер пересылает все символы из списка для хранения нест-
руктурированных вводных данных в канонический список и выполняет запрос про-
цесса на чтение, следуя тому же самому алгоритму, что и в случае работы в
каноническом режиме. Режим без обработки символов особенно важен в экран-
но-ориентированных приложениях, таких как экранный редактор vi, многие из
команд которого не заканчиваются символом возврата каретки. Например, коман-
да dw удаляет слово в текущей позиции курсора.
На Рисунке 10.17 приведена программа, использующая функцию ioctl для
сохранения текущих установок терминала для файла с дескриптором 0, что соот-
ветствует значению дескриптора файла стандартного ввода. Функция ioctl с ко-
мандой TCGETA приказывает
драйверу извлечь установки и сохранить их в структуре с именем savetty в ад-
ресном пространстве задачи. Эта команда часто используется для того, чтобы
определить, является ли файл терминалом или нет, поскольку она ничего не из-
меняет в системе: если она завершается неудачно, процессы предполагают, что
файл не является терминалом. Здесь же, процесс вторично вызывает функцию
ioctl для того, чтобы перевести терминал в режим без обработки: он отключает
эхо-сопровождение ввода символов и готовится к выполнению операций чтения с
+----------------------------------------------------------------+
| #include |
| #include |
| struct termio savetty; |
| main() |
| { |
| extern sigcatch(); |
| struct termio newtty; |
| int nrd; |
| char buf[32]; |
| signal(SIGINT,sigcatch); |
| if (ioctl(0,TCGETA,&savetty) == -1) |
| { |
| printf("ioctl завершилась неудачно: нет терминала\n"); |
| exit(); |
| } |
| newtty = savetty; |
316
| newtty.c_lflag &= ~ICANON;/* выход из канонического режима */|
| newtty.c_lflag &= ~ECHO; /* отключение эхо-сопровождения*/ |
| newtty.c_cc[VMIN] = 5; /* минимум 5 символов */ |
| newtty.c_cc[VTIME] = 100; /* интервал 10 секунд */ |
| if (ioctl(0,TCSETAF,&newtty) == -1) |
| { |
| printf("не могу перевести тер-л в режим без обработки\n");|
| exit(); |
| } |
| for(;;) |
| { |
| nrd = read(0,buf,sizeof(buf)); |
| buf[nrd] = 0; |
| printf("чтение %d символов '%s'\n",nrd,buf); |
| } |
| } |
| sigcatch() |
| { |
| ioctl(0,TCSETAF,&savetty); |
| exit(); |
| } |
+----------------------------------------------------------------+
Рисунок 10.17. Режим без обработки - чтение 5-символьных блоков
+----------------------------------------------------------------+
| #include |
| |
| main() |
| { |
| register int i,n; |
| int fd; |
| char buf[256]; |
| |
| /* открытие терминала только для чтения с опцией "no delay" */ |
| if((fd = open("/dev/tty",O_RDONLY|O_NDELAY)) == -1) |
| exit(); |
| |
| n = 1; |
| for(;;) /* всегда */ |
| { |
| for(i = 0; i < n; i++) |
| ; |
| |
| if(read(fd,buf,sizeof(buf)) > 0) |
| { |
| printf("чтение с номера %d\n",n); |
| n--; |
| } |
| else |
| /* ничего не прочитано; возврат вследствие "no delay" */ |
| n++; |
| } |
| } |
+----------------------------------------------------------------+
Рисунок 10.18. Опрос терминала
317
терминала по получении с терминала 5 символов, как минимум, или по прохожде-
нии 10 секунд с момента ввода первой порции символов. Когда процесс получает
сигнал о прерывании, он сбрасывает первоначальные параметры терминала и за-
вершается.
Иногда удобно производить опрос устройства, то есть считывать с него
данные, если они есть, или продолжать выполнять обычную работу - в противном
случае. Программа на Рисунке 10.18 иллюстрирует этот случай: после открытия
терминала с параметром "no delay" (без задержки) процессы, ведущие чтение с
него, не приостановят свое выполнение в случае отсутствия данных, а вернут
управление немедленно (см. алгоритм terminal_read, Рисунок 10.15). Этот ме-
тод работает также, если процесс следит за множеством устройств: он может
открыть каждое устройство с параметром "no delay" и опросить всех из них,
ожидая поступления информации с каждого. Однако, этот метод растрачивает вы-
числительные мощности системы.
В системе BSD есть системная функция select, позволяющая производить оп-
рос устройства. Синтаксис вызова этой функции:
select(nfds,rfds,wfds,efds,timeout)
где nfds - количество выбираемых дескрипторов файлов, а rfds, wfds и efds
указывают на двоичные маски, которыми "выбирают" дескрипторы открытых фай-
лов. То есть, бит 1 << fd (сдвиг на 1 разряд влево значения дескриптора фай-
ла) соответствует установке на тот случай, если пользователю нужно выбрать
этот дескриптор файла. Параметр timeout (тайм-аут) указывает, на какое время
следует приостановить выполнение функции select, ожидая поступления данных,
например; если данные поступают для любых дескрипторов и тайм-аут не закон-
чился, select возвращает управление, указывая в двоичных масках, какие деск-
рипторы были выбраны. Например, если пользователь пожелал приостановиться до
момента получения данных по дескрипторам 0, 1 или 2, параметр rfds укажет на
двоичную маску 7; когда select возвратит управление, двоичная маска будет
заменена маской, указывающей, по каким из дескрипторов имеются готовые дан-
ные. Двоичная маска wfds выполняет похожую функцию в отношении записи деск-
рипторов, а двоичная маска efds указывает на существование исключительных
условий, связанных с конкретными дескрипторами, что бывает полезно при рабо-
те в сети.
10.3.5 Назначение операторского терминала
Операторский терминал - это терминал, с которого пользователь регистри-
руется в системе, он управляет процессами, запущенными пользователем с тер-
минала. Когда процесс открывает терминал, драйвер терминала открывает стро-
ковый интерфейс. Если процесс возглавляет группу процессов как результат вы-
полнения системной функции setpgrp и если процесс не связан с одним из опе-
раторских терминалов, строковый интерфейс делает открываемый терминал опера-
торским. Он сохраняет старший и младший номера устройства для файла термина-
ла в адресном пространстве, выделенном процессу, а номер группы процессов,
связанной с открываемым процессом, в структуре данных терминального драйве-
ра. Открываемый процесс становится управляющим процессом, обычно входным
(начальным) командным процессором, что мы увидим далее.
Операторский терминал играет важную роль в обработке сигналов. Когда
пользователь нажимает клавиши "delete" (удаления), "break" (прерывания),
стирания или выхода, программа обработки прерываний загружает строковый ин-
терфейс, который посылает соответствующий сигнал всем процессам в группе.
318
Подобно этому, когда пользователь "зависает", программа обработки прерываний
от терминала получает информацию о "зависании" от аппаратуры, и строковый
интерфейс посылает соответствующий сигнал всем процессам в группе. Таким об-
разом, все процессы, запущенные с конкретного терминала, получают сигнал о
"зависании"; реакцией по умолчанию для большинства процессов будет выход из
программы по получении сигнала; это похоже на то, как при завершении работы
пользователя с терминалом из системы удаляются побочные процессы. После по-
сылки сигнала о "зависании" программа обработки прерываний от терминала раз-
ъединяет терминал с группой процессов, чтобы процессы из этой группы не мог-
ли больше получать сигналы, возникающие на терминале.
10.3.6 Драйвер косвенного терминала
Зачастую процессам необходимо прочитать ил записать данные непосредст-
венно на операторский терминал, хотя стандартный ввод и вывод могут быть пе-
реназначены в другие файлы. Например, shell может посылать срочные сообщения
непосредственно на терминал, несмотря на то, что его стандартный файл вывода
и стандартный файл ошибок, возможно, переназначены в другое место. В версиях
системы UNIX поддерживается "косвенный" доступ к терминалу через файл уст-
ройства "/dev/tty", в котором для каждого процесса определен управляющий
(операторский) терминал. Пользователи, прошедшие регистрацию на отдельных
терминалах, могут обращаться к файлу "/dev/tty", но они получат доступ к
разным терминалам.
Существует два основных способа поиска ядром операторского терминала по
имени файла "/dev/tty". Во-первых, ядро может специально указать номер уст-
ройства для файла косвенного терминала с отдельной точкой входа в таблицу
ключей устройств посимвольного ввода-вывода. При запуске косвенного термина-
ла драйвер этого терминала получает старший и младший номера операторского
терминала из адресного пространства, выделенного процессу, и запускает драй-
вер реального терминала, используя данные таблицы ключей устройств посим-
вольного ввода-вывода. Второй способ, обычно используемый для поиска опера-
торского терминала по имени "/dev/tty", связан с проверкой соответствия
старшего номера устройства номеру косвенного терминала перед вызовом проце-
дуры open, определяемой типом данного драйвера. В случае совпадения номеров
освобождается индекс файла "/dev/tty", выделяется индекс операторскому тер-
миналу, точка входа в таблицу файлов переустанавливается так, чтобы указы-
вать на индекс операторского терминала, и вызывается процедура open, принад-
лежащая терминальному драйверу. Дескриптор файла, возвращенный после откры-
тия файла "/dev/tty", указывает непосредственно на операторский терминал и
его драйвер.
Как показано в главе 7, процесс начальной загрузки, имеющий номер 1, вы-
полняет бесконечный цикл чтения из файла "/etc/inittab" инструкций о том,
что нужно делать, если загружаемая система определена как "однопользователь-
ская" или "многопользовательская". В многопользовательском режиме самой пер-
вой обязанностью процесса начальной загрузки является предоставление пользо-
вателям возможности регистрироваться в системе с терминалов (Рисунок 10.19).
Он порождает процессы, именуемые getty-процессами (от "get tty" - получить
терминал), и следит за тем, какой из процессов открывает какой терминал;
каждый getty-процесс устанавливает свою группу процессов, используя вызов
системной функции setpgrp, открывает отдельную терминальную линию и обычно
приостанавливается во время выполнения функции open до тех пор, пока машина
не получит аппаратную связь с терминалом. Когда функция open возвращает уп-
равление, getty-процесс исполняет программу login (регистрации в системе),
которая требует от пользователей, чтобы они идентифицировали себя указанием
319
регистрационного имени и пароля. Если пользователь зарегистрировался успеш-
но, программа login наконец запускает командный процессор shell и пользова-
тель приступает к работе. Этот вызов shell'а именуется "login shell" (регис-
трационный shell, регистрационный интерпретатор команд). Процесс, связанный
с shell'ом, имеет тот же идентификатор, что и начальный getty-процесс, поэ-
тому login shell является процессом, возглавляющим группу процессов. Если
пользователь не смог успешно зарегистрироваться, программа регистрации за-
вершается через определенный промежуток времени, закрывая открытую терми-
нальную линию, а процесс начальной загрузки порождает для этой линии следую-
щий getty-процесс. Процесс начальной загрузки делает паузу до получения сиг-
нала об окончании порожденного ранее процесса. После возобновления работы он
выясняет, был ли прекративший существование процесс регистрационным shell'ом
и если это так, порождает еще один getty-процесс, открывающий терминал,
вместо прекратившего существование.
+------------------------------------------------------------+
| алгоритм login /* процедура регистрации */ |
| { |
| исполняется getty-процесс: |
| установить группу процессов (вызов функции setpgrp); |
| открыть терминальную линию; /* приостанов до завершения|
| открытия */ |
| если (открытие завершилось успешно) |
| { |
| исполнить программу регистрации: |
| запросить имя пользователя; |
| отключить эхо-сопровождение, запросить пароль; |
| если (регистрация прошла успешно) |
| /* найден соответствующий пароль в /etc/passwd */ |
| { |
| перевести терминал в канонический режим (ioctl);|
| исполнить shell; |
| } |
| в противном случае |
| считать количество попыток регистрации, пытаться|
| зарегистрироваться снова до достижения опреде- |
| ленной точки; |
| } |
| } |
+------------------------------------------------------------+
Рисунок 10.19. Алгоритм регистрации
Схема реализации драйверов устройств, хотя и отвечает заложенным требо-
ваниям, страдает некоторыми недостатками, которые с годами стали заметнее.
Разные драйверы имеют тенденцию дублировать свои функции, в частности драй-
веры, которые реализуют сетевые протоколы и которые обычно включают в себя
секцию управления устройством и секцию протокола. Несмотря на то, что секция
протокола должна быть общей для всех сетевых устройств, на практике это не
так, поскольку ядро не имеет адекватных механизмов для общего использования.
Например, символьные списки могли бы быть полезными благодаря своим возмож-
ностям в буферизации, но они требуют больших затрат ресурсов на посимвольную
обработку. Попытки обойти этот механизм, чтобы повысить производительность
системы, привели к нарушению модульности подсистемы управления вводом-выво-
дом. Отсутствие общности на уровне драйверов распространяется вплоть до
320
уровня команд пользователя, на котором несколько команд могут выполнять об-
щие логические функции, но различными средствами. Еще один недостаток пост-
роения драйверов заключается в том, что сетевые протоколы требуют использо-
вания средства, подобного строковому интерфейсу, в котором каждая дисциплина
реализует одну из частей протокола и составные части соединяются гибким об-
разом. Однако, соединить традиционные строковые интерфейсы довольно трудно.
Ричи недавно разработал схему, получившую название "потоки" (streams),
для повышения модульности и гибкости подсистемы управления вводом-выводом.
Нижеследующее описание основывается на его работе [Ritchie 84b], хотя реали-
зация этой схемы в версии V слегка отличается. Поток представляет собой пол-
нодуплексную связь между процессом и драйвером устройства. Он состоит из со-
вокупности линейно связанных между собой пар очередей, каждая из которых
(пара) включает одну очередь для ввода и другую - для вывода. Когда процесс
записывает данные в поток, ядро посылает данные в очереди для вывода; когда
драйвер устройства получает входные данные, он пересылает их в очереди для
ввода к процессу, производящему чтение. Очереди обмениваются сообщениями с
соседними очередями, используя четко определенный интерфейс. Каждая пара
очередей связана с одним из модулей ядра, таким как драйвер, строковый ин-
терфейс или протокол, и модули ядра работают с данными, прошедшими через со-
ответствующие очереди.
Каждая очередь представляет собой структуру данных, состоящую из следую-
щих элементов:
* процедуры открытия, вызываемой во время выполнения системной функции
open
* процедуры закрытия, вызываемой во время выполнения системной функции
close
* процедуры "вывода", вызываемой для передачи сообщения в очередь
* процедуры "обслуживания", вызываемой, когда очередь запланирована к ис-
полнению
* указателя на следующую очередь в потоке
* указателя на список сообщений, ожидающих обслуживания
* указателя на внутреннюю структуру данных, с помощью которой поддержива-
ется рабочее состояние очереди
* флагов, а также верхней и нижней отметок, используемых для управления
потоками данных, диспетчеризации и поддержания рабочего состояния очере-
ди.
Ядро выделяет пары очередей, соседствующие в памяти; следовательно, оче-
редь легко может отыскать своего партнера по паре.
+----------+
| Индекс |
+-----------------------+ файла |
| |устройства|
v +----------+
+------------+-----------+
Заголовок | Очередь | Очередь |
потока | для вывода | для ввода |
+------+-----+-----------+
| ^
v |
+------------+-----+-----+
Драйвер | Очередь | Очередь |------- пара очередей
| для вывода | для ввода |
+------------+-----------+
Рисунок 10.20. Поток после открытия
Устройство с потоковым драйвером является устройством посимвольного вво-
321
да-вывода; оно имеет в таблице ключей устройств соответствующего типа специ-
альное поле, которое указывает на структуру инициализации потока, содержащую
адреса процедур, а также верхнюю и нижнюю отметки, упомянутые выше. Когда
ядро выполняет системную функцию open и обнаруживает, что файл устройства
имеет тип "специальный символьный", оно проверяет наличие нового поля в таб-
лице ключей устройств посимвольного ввода-вывода. Если в таблице отсутствует
соответствующая точка входа, то драйвер не является потоковым, и ядро выпол-
няет процедуру, обычную для устройств посимвольного ввода-вывода. Однако,
при первом же открытии потокового драйвера ядро выделяет две пары очередей -
одну для заголовка потока и другую для драйвера. У всех открытых потоков мо-
дуль заголовка имеет идентичную структуру: он содержит общую процедуру "вы-
вода" и общую процедуру "обслуживания" и имеет интерфейс с модулями ядра бо-
лее высокого уровня, выполняющими функции read, write и ioctl. Ядро инициа-
лизирует структуру очередей драйвера, назначая значения указателям каждой
очереди и копируя адреса процедур драйвера из структуры инициализации драй-
вера, и запускает процедуру открытия. Процедура открытия драйвера выполняет
обычную инициализацию, но при этом сохраняет информацию, необходимую для
повторного обращения к ассоциированной с этой процедурой очереди. Наконец,
ядро отводит специальный указатель в копии индекса в памяти для ссылки на
заголовок потока (Рисунок 10.20). Когда еще один процесс открывает устройст-
во, ядро обнаруживает назначенный ранее поток с помощью этого указателя и
запускает процедуру открытия для всех модулей потока.
Модули поддерживают связь со своими соседями по потоку путем передачи
сообщений. Сообщение состоит из списка заголовков блоков, содержащих инфор-
мацию сообщения; каждый заголовок блока содержит ссылку на место расположе-
ния начала и конца информации блока. Существует два типа сообщений - управ-
ляющее и информационное, которые определяются указателями типа в заголовке
сообщения. Управляющие сообщения могут быть результатом выполнения системной
функции ioctl или результатом особых условий, таких как зависание терминала,
а информационные сообщения могут возникать в результате выполнения системной
функции write или в результате поступления данных от устройства.
Сообщение 1 Сообщение 2 Сообщение 3
+---------+ +---------+ +---------+
| Блок +--------->| +-------->| |
+----+----+ +---------+ +----+----+
v v
+---------+ +---------+
| | | |
+----+----+ +---------+
v
+---------+
| |
+---------+
Рисунок 10.21. Сообщения в потоках
Когда процесс производит запись в поток, ядро копирует данные из адрес-
ного пространства задачи в блоки сообщения, которые выделяются модулем заго-
ловка потока. Модуль заголовка потока запускает процедуру "вывода" для моду-
ля следующей очереди, которая обрабатывает сообщение, незамедлительно пере-
дает его в следующую очередь или ставит в эту же очередь для последующей об-
работки. В последнем случае модуль связывает заголовки блоков сообщения в
список с указателями, формируя двунаправленный список (Рисунок 10.21). Затем
он устанавливает в структуре данных очереди флаг, показывая тем самым, что
имеются данные для обработки, и планирует собственное обслуживание. Модуль
включает очередь в список очередей, требующих обслуживания и запускает меха-
низм диспетчеризации; планировщик (диспетчер) вызывает процедуры обслужива-
322
ния для каждой очереди в списке. Ядро может планировать обслуживание модулей
по программному прерыванию, подобно тому, как оно вызывает функции в таблице
ответных сигналов (см. главу 8); обработчик программных прерываний вызывает
индивидуальные процедуры обслуживания.
+----------+
| Индекс |
+-----------------------+ файла |
| |устройства|
v +----------+
+------------+-----------+
Заголовок | Очередь | Очередь |
потока | для вывода | для ввода |
+------+-----+-----------+
| ^
| |
v |
+------------+-----------+
Строковый | Очередь | Очередь |
интерфейс | для вывода | для ввода |
+------+-----+-----------+
| ^
| |
v |
+------------+-----+-----+
Терминальный | Очередь | Очередь |
драйвер | для вывода | для ввода |
+------------+-----------+
Рисунок 10.22. Продвижение модуля к потоку
Процессы могут "продвигать" модули к открытому потоку, используя вызов
системной функции ioctl. Ядро помещает выдвинутый модуль сразу под заголов-
ком потока и связывает указатели очереди таким образом, чтобы сохранить дву-
направленную структуру списка. Модули, расположенные в потоке ниже, не бес-
покоятся о том, связаны ли они с заголовком потока или же с выдвинутым моду-
лем: интерфейсом выступает процедура "вывода" следующей очереди в потоке; а
следующая очередь принадлежит только что выдвинутому модулю. Например, про-
цесс может выдвинуть модуль строкового интерфейса в поток терминального
драйвера с целью обработки символов стирания и удаления (Рисунок 10.22); мо-
дуль строкового интерфейса не имеет тех же составляющих, что и строковые ин-
терфейсы, рассмотренные в разделе 10.3, но выполняет те же функции. Без мо-
дуля строкового интерфейса терминальный драйвер не обработает вводные симво-
лы и они поступят в заголовок потока в неизмененном виде. Сегмент программы,
открывающий терминал и выдвигающий строковый интерфейс, может выглядеть сле-
дующим образом:
fd = open("/dev/ttyxy",O_RDWR);
ioctl(fd,PUSH,TTYLD);
где PUSH - имя команды, а TTYLD - число, идентифицирующее модуль строкового
интерфейса. Не существует ограничения на количество модулей, могущих быть
выдвинутыми в поток. Процесс может выталкивать модули из потока в порядке
поступления, "первым пришел - первым вышел", используя еще один вызов сис-
темной функции ioctl
ioctl(fd,POP,0);
При том, что модуль строкового интерфейса выполняет обычные функции по
323
управлению терминалом, соответствующее ему устройство может быть средством
сетевой связи вместо того, чтобы обеспечивать связь с одним-единственным
терминалом. Модуль строкового интерфейса работает одинаково, независимо от
того, какого типа модуль расположен ниже него. Этот пример наглядно демонст-
рирует повышение гибкости вследствие соединения модулей ядра.
10.4.1 Более детальное рассмотрение потоков
Пайк описывает реализацию мультиплексных виртуальных терминалов, исполь-
зующую потоки (см. [Pike 84]). Пользователь видит несколько виртуальных тер-
миналов, каждый из которых занимает отдельное окно на экране физического
терминала. Хотя в статье Пайка рассматривается схема для интеллектуальных
графических терминалов, она работала бы и для терминалов ввода-вывода тоже;
каждое окно занимало бы целый экран и пользователь для переключения вирту-
альных окон набирал бы последовательность управляющих клавиш.
+---------+ +---------+ +-----------------+
Уровень | shell 1 | | shell 2 | | mpx |
пользователя +---------+ +---------+ +-----------------+
---------------+---------------+-----------+---+-------+----
Уровень ядра | ^ | ^ +--+ ^ | ^ | ^
| | | | | +--+ | | | |
| | | | | | | | | |
v | v | со- v | v | со- | |
терми- +-+++ терми- +-+++ об-+-+++ +-+++об- | |
нальная | | | нальная | | | ще-| | | | | |ще- | |
линия +++-+ линия +++-+ ния+++-+ +++-+ния | |
| ^ +-----------+-^------+ ^ | ^ | |
| | | +---------+-+--------+ | | | |
| | | | | | +-----------+ | | |
v | v | v | v +-----------+ v |
+-+++-+++ +-+++-+++ +-+++
псевдо- | | | | | | | | | | псевдо- | | |
терми- +++-+++-+ +++-+++-+ терми- +-+-+
нальная | ^ | ^ | ^ | ^ нальная терми-
пара 1 | +-+ | | +-+ | пара 2 нальный
+-----+ +-----+ драйвер
Рисунок 10.23. Отображение виртуальных окон на экране физи-
ческого терминала
На Рисунке 10.23 показана схема расположения процессов и модулей ядра.
Пользователь вызывает процесс mpx, контролирующий работу физического терми-
нала. Mpx читает данные из линии физического терминала и ждет объявления об
управляющих событиях, таких как создание нового окна, переключение управле-
ния на другое окно, удаление окна и т.п.
Когда mpx получает уведомление о том, что пользователю нужно создать но-
вое окно, он создает процесс, управляющий новым окном, и поддерживает связь
с ним через псевдотерминал. Псевдотерминал - это программное устройство, ра-
ботающее по принципу пары: выходные данные, направляемые к одной составляю-
щей пары, посылаются на вход другой составляющей; входные данные посылаются
тому модулю потока, который расположен выше по течению. Для того, чтобы отк-
рыть окно (Рисунок 10.24), mpx назначает псевдотерминальную пару и открывает
одну из составляющих пары, направляя поток к ней (открытие драйвера служит
гарантией того, что псевдотерминальная пара не была выбрана раньше). Mpx
ветвится и новый процесс открывает другую составляющую псевдотерминальной
324
+----------------------------------------------------------------+
| /* предположим, что дескрипторы файлов 0 и 1 уже относятся к |
| физическому терминалу */ |
| для(;;) /* цикл */ |
| { |
| выбрать(ввод); /* ждать ввода из какой-либо линии */ |
| прочитать данные, введенные из линии; |
| переключить(линию с вводимыми данными) |
| { |
| если выбран физический терминал: /* данные вводятся по ли- |
| нии физического терми- |
| нала */ |
| если(считана управляющая команда) /* например, создание |
| нового окна */ |
| { |
| открыть свободный псевдотерминал; |
| пойти по ветви нового процесса: |
| если(процесс родительский) |
| { |
| выдвинуть интерфейс сообщений в сторону mpx; |
| продолжить; /* возврат в цикл "для" */ |
| } |
| /* процесс-потомок */ |
| закрыть ненужные дескрипторы файлов; |
| открыть другой псевдотерминал из пары, выбрать stdin, |
| stdout, stderr; |
| выдвинуть строковый интерфейс терминала; |
| запустить shell; /* подобно виртуальному терминалу */|
| } |
| /* "обычные" данные, появившиеся через виртуальный терминал */ |
| демультиплексировать считывание данных с физического тер-|
| минала, снять заголовки и вести запись на соответствую- |
| щий псевдотерминал; |
| продолжить; /* возврат в цикл "для" */ |
| |
| если выбран логический терминал: /* виртуальный терминал |
| связан с окном */ |
| закодировать заголовок, указывающий назначение информации|
| окна; |
| переписать заголовок и информацию на физический терминал;|
| продолжить; /* возврат в цикл "для" */ |
| } |
| } |
+----------------------------------------------------------------+
Рисунок 10.24. Псевдопрограмма мультиплексирования окон
пары. Mpx выдвигает модуль управления сообщениями в псевдотерминальный по-
ток, чтобы преобразовывать управляющие сообщения в информационные (об этом в
следующем параграфе), а порожденный процесс помещает в псевдотерминальный
поток модуль строкового интерфейса перед запуском shell'а. Этот shell теперь
выполняется на виртуальном терминале; для пользователя виртуальный терминал
неотличим от физического.
Процесс mpx является мультиплексором, направляющим вывод данных с вирту-
альных терминалов на физический терминал и демультиплексирующим ввод данных
с физического терминала на подходящий виртуальный. Mpx ждет поступления дан-
ных по любой из линий, используя системную функцию select. Когда данные пос-
тупают от физического терминала, mpx решает вопрос, являются ли поступившие
325
данные управляющим сообщением, извещающим о необходимости создания нового
окна или удаления старого, или же это информационное сообщение, которое не-
обходимо разослать процессам, считывающим информацию с виртуального термина-
ла. В последнем случае данные имеют заголовок, идентифицирующий тот вирту-
альный терминал, к которому они относятся; mpx стирает заголовок с сообщения
и переписывает данные в соответствующий псевдотерминальный поток. Драйвер
псевдотерминала отправляет данные через строковый интерфейс терминала про-
цессам, осуществляющим чтение. Обратная процедура имеет место, когда процесс
ведет запись на виртуальный терминал; mpx присоединяет заголовок к данным,
информируя физический терминал, для вывода в какое из окон предназначены эти
данные.
Если процесс вызывает функцию ioctl с виртуального терминала, строковый
интерфейс терминала задает необходимые установки терминала для его виртуаль-
ной линии; для каждого из виртуальных терминалов установки могут быть раз-
личными. Однако, на физический терминал должна быть послана и кое-какая ин-
формация, зависящая от типа устройства. Модуль управления сообщениями преоб-
разует управляющие сообщения, генерируемые функцией ioctl, в информационные
сообщения, предназначенные для чтения и записи их процессом mpx, и эти сооб-
щения передаются на физическое устройство.
Ричи упоминает о том, что им была предпринята попытка создания потоков
только с процедурами "вывода" или только с процедурами обслуживания. Однако,
процедура обслуживания необходима для управления потоками данных, так как
модули должны иногда ставить данные в очередь, если соседние модули на время
закрыты для приема данных. Процедура "вывода" так же необходима, поскольку
данные должны иногда доставляться в соседние модули незамедлительно. Напри-
мер, строковому интерфейсу терминала нужно вести эхо-сопровождение ввода
данных на терминале в темпе с процессом. Системная функция write могла бы
запускать процедуру "вывода" для следующей очереди непосредственно, та, в
свою очередь, вызывала бы процедуру "вывода" для следующей очереди и так да-
лее, не нуждаясь в механизме диспетчеризации. Процесс приостановился бы в
случае переполнения очередей для вывода. Однако, со стороны ввода модули не
могут приостанавливаться, поскольку их выполнение вызывается программой об-
работки прерываний, иначе был бы приостановлен совершенно безобидный про-
цесс. Связь между модулями не должна быть симметричной в направлениях ввода
и вывода, хотя это и делает схему менее изящной.
Также было бы желательно реализовать каждый модуль в виде отдельного
процесса, но использование большого количества модулей привело бы к перепол-
нению таблицы процессов. Модули наделяются специальным механизмом диспетче-
ризации - программным прерыванием, независимым от обычного планировщика про-
цессов. По этой причине модули не могут приостанавливать свое выполнение,
так как они приостанавливали бы тем самым произвольный процесс (тот, который
прерван). Модули должны хранить внутри себя информацию о своем состоянии,
что делает лежащие в их основе программы более громоздкими, чем если бы при-
остановка выполнения была разрешена.
В реализации потоков можно выделить несколько отклонений или несоответс-
твий:
* Учет ресурсов процесса в потоках затрудняется, поскольку модулям необя-
зательно выполняться в контексте процесса, использующего поток. Ошибочно
предполагать, что все процессы одинаково используют модули потоков, пос-
кольку одним процессам может потребоваться использование сложных сетевых
протоколов, тогда как другие могут использовать простые строковые интер-
фейсы.
* Пользователи имеют возможность переводить терминальный драйвер в режим
без обработки, в котором функция read возвращает управление через корот-
кий промежуток времени в случае отсутствия данных (например, если
326
newtty.c_cc[VMIN] = 0 на Рисунке 10.17). Эту особенность сложно реализо-
вать в потоковой среде без подключения специальной программы на уровне
заголовка потока.
* Потоки выступают средствами линейной связи и не могут позволить произво-
дить с легкостью мультиплексирование на уровне ядра. В примере использо-
вания окон, рассмотренном в предыдущем разделе, выполнялось мультиплек-
сирование на уровне пользовательского процесса.
Несмотря на эти несоответствия, с потоками связываются большие надежды в
совершенствовании разработки модулей драйвера.
.te1 10.5 ВЫВОДЫ
Данная глава представляет собой обзор драйверов устройств в системе
UNIX. Устройства могут быть либо блочного, либо символьного типа; интерфейс
между устройствами и остальной частью ядра определяется типом устройств. Ин-
терфейсом для устройств блочного типа выступает таблица ключей устройств
ввода-вывода блоками, состоящая из точек входа, соответствующих процедурам
открытия и закрытия устройств и стратегической процедуре. Стратегическая
процедура управляет передачей данных от и к устройству блочного типа. Интер-
фейсом для устройств символьного типа выступает таблица ключей устройств по-
символьного ввода-вывода, которая состоит из точек входа, соответствующих
процедурам открытия и закрытия устройства, чтения, записи и процедуре ioctl.
Системная функция ioctl использует при обращении к устройствам символьного
типа свой собственный интерфейс, который позволяет осуществлять передачу уп-
равляющей информации между процессами и устройствами. По получении прерыва-
ния от устройства ядро вызывает программу обработки соответствующего преры-
вания, опираясь на информацию, хранящуюся в таблице векторов прерываний, и
на параметры, сообщенные устройством, от которого поступило прерывание.
Дисковые драйверы превращают номера логических блоков, используемые фай-
ловой системой, в физические адреса на диске. Блочный интерфейс дает возмож-
ность ядру буферизовать данные. Взаимодействие без обработки ускоряет
ввод-вывод на диск, но игнорирует буферный кеш, увеличивая тем самым шансы
разрушить файловую систему.
Терминальные драйверы осуществляют непосредственное взаимодействие с
пользователями. Ядро связывает с каждым терминалом три символьных списка,
один для неструктурированного ввода с клавиатуры, один для ввода с обработ-
кой символов стирания, удаления и возврата каретки и один для вывода. Сис-
темная функция ioctl дает процессам возможность следить за тем, как ядро об-
рабатывает вводимые данные, переводя терминал в канонический режим или уста-
навливая значения различных параметров для режима без обработки символов.
Getty-процесс открывает терминальные линии и ждет связи: он формирует группу
процессов во главе с регистрационным shell'ом, инициализирует с помощью фун-
кции ioctl параметры терминала и обращается к пользователю с предложением
зарегистрироваться. Установленный таким образом операторский терминал посы-
лает процессам в группе сигналы в ответ на возникновение таких событий, как
"зависание" пользователя или нажатие им клавиши прерывания.
Потоки выступают средством повышения модульности построения драйверов
устройств и протоколов. Поток - это полнодуплексная связь между процессами и
драйверами устройств, которая может включать в себя строковые интерфейсы и
протоколы для промежуточной обработки данных. Модули потоков характеризуются
четко определенным взаимодействием и гибкостью, позволяющей использовать их
в сочетании с другими модулями. Эта гибкость имеет особое значение для сете-
вых протоколов и драйверов.
.te1 10.6 УПРАЖНЕНИЯ
*1. Предположим, что в системе имеются два файла устройств с одними и теми
же старшим и младшим номерами, при том, что оба устройства - символьно-
327
го типа. Если два процесса желают одновременно открыть физическое уст-
ройство, не будет никакой разницы, открывают ли они один и тот же файл
устройства или же разные файлы. Что произойдет, когда они станут закры-
вать устройство ?
*2. Вспомним из главы 5, что системной функции mknod требуется разрешение
суперпользователя на создание нового специального файла устройства. Ес-
ли доступ к устройству управляется правами доступа к файлу, почему фун-
кции mknod нужно разрешение суперпользователя ?
3. Напишите программу, которая проверяет, что файловые системы на диске не
перекрываются. Этой программе потребовались бы два аргумента: файл уст-
ройства, представляющий дисковый том, и дескриптор файла, откуда берут-
ся номера секторов и их размер для диска данного типа. Для проверки от-
сутствия перекрытий этой программе понадобилась бы информация из супер-
блоков. Будет ли такая программа всегда правильной ?
4. Программа mkfs инициализирует файловую систему на диске путем создания
суперблока, выделения места для списка индексов, включения всех инфор-
мационных блоков в связанный список и создания корневого каталога. Как
бы вы написали программу mkfs ? Как изменится эта программа при наличии
таблицы содержимого тома ? Каким образом следует инициализировать таб-
лицу содержимого тома ?
5. Программы mkfs и fsck (глава 5) являются программами пользовательского
уровня, а не частью ядра. Прокомментируйте это.
6. Предположим, что программисту нужно разработать базу данных, работающую
в среде ОС UNIX. Программы базы данных выполняются на пользовательском
уровне, а не в составе ядра. Как система управления базой данных будет
взаимодействовать с диском ? Подумайте над следующими вопросами:
* Использование стандартного интерфейса файловой системы вместо непос-
редственной работы с неструктурированными данными на диске,
* Потребность в быстродействии,
* Необходимость знать, когда фактически данные располагаются на диске,
* Размер базы данных: должна ли она помещаться в одной файловой систе-
ме, занимать собой весь дисковый том или же располагаться на несколь-
ких дисковых томах ?
7. Ядро системы UNIX по умолчанию предполагает, что файловая система рас-
полагается на идеальных дисках. Однако, диски могут содержать ошибки,
которые делают непригодными и выводят из строя определенные сектора,
несмотря на то, что остальная часть диска осталась "пригодной". Как
дисковому драйверу (или интеллектуальному контроллеру диска) следует
учитывать небольшое количество плохих секторов. Как это отразилось бы
на производительности системы ?
8. При монтировании файловой системы ядро запускает процедуру открытия для
данного драйвера, но позже освобождает индекс специального файла уст-
ройства по завершении выполнения вызова системной функции mount. При
демонтировании файловой системы ядро обращается к индексу специального
файла устройства, запускает процедуру закрытия для данного драйвера и
вновь освобождает индекс. Сравните эту последовательность операций над
индексом, а также обращений к процедурам открытия и закрытия драйвера,
с последовательностью действий, совершаемых при открывании и закрывании
устройства блочного типа. Прокомментируйте результаты сравнения.
9. Выполните программу, приведенную на Рисунке 10.14, но направьте вывод
данных в файл. Сравните содержимое файла с содержимым выводного потока,
когда вывод идет на терминал. Вам придется прервать процессы, чтобы ос-
тановить их; только прежде пусть они получат достаточно большое коли-
чество данных. Что произойдет, если вызов функции write в программе за-
менить на printf(output);
10. Что произойдет, если пользователь попытается выполнить редактирование
текста на фоне программы:
ed file &
Обоснуйте ответ.
328
11. К файлам терминалов обычно устанавливаются следующие права доступа
crw--w--w- 2 mjb lus 33,11 Oct 25 20:27 tty61
при входе пользователя в систему. То есть, чтение и запись разрешаются
пользователю с именем "mjb", а остальным пользователям разрешена только
запись. Почему ?
12. Предположим, что вам известно имя файла терминала вашего товарища. На-
пишите программу записи сообщений с вашего терминала на терминал вашего
товарища. Какая еще информация вам нужна, чтобы закодировать приемлемое
воспроизведение обычной команды write ?
13. Выполните команду stty: если параметры не указаны, она выбирает значе-
ния установок терминала и сообщает их пользователю. В противном случае
пользователь может в интерактивном режиме сделать различные установки
сам.
14. Напишите элементарный строковый интерфейс, записывающий идентификатор
машины в начале каждой строки выводного потока.
15. В каноническом режиме пользователь может на время приостановить вывод
данных на терминал, нажав последовательность клавиш , и продол-
жить вывод, нажав . Как в стандартном строковом интерфейсе реа-
лизуется эта особенность ?
*16. Процесс начальной загрузки порождает getty-процесс для каждой терми-
нальной линии в системе. Что произошло бы, если бы для одного и того же
терминала существовали бы одновременно два getty-процесса, ожидающие
регистрации пользователя ? Может ли ядро помешать этому ?
17. Пусть командный процессор shell реализован таким образом, что он "игно-
рирует" конец файла и продолжает считывать данные из стандартного вво-
да. Что произошло бы, если бы пользователь (в регистрационном shell'е)
угадал конец файла и продолжил ввод с клавиатуры ?
*18. Предположим, что процесс считывает данные с операторского терминала, но
игнорирует или улавливает сигналы о "зависании". Что произойдет, когда
процесс продолжит считывать данные с операторского терминала после за-
висания ?
19. Программа getty-процесса несет ответственность за открытие терминальной
линии, а программа login - за проверку регистрационных имен и паролей.
Какие преимущества в том, что эти функции выполняются отдельными прог-
раммами ?
20. Рассмотрим два метода реализации драйвера косвенного терминала ("/dev
/tty"), описанные в разделе 10.3.6. Какие различия между ними чувствует
пользователь ? (Совет: подумайте о системных функциях stat и fstat).
21. Разработайте метод планирования выполнения модулей потока, в соответст-
вии с которым ядро имеет в своем составе специальный процесс, выполняю-
щий процедуры обслуживания модулей тогда, когда выполнение этих проце-
дур запланировано.
*22. Разработайте схему построения виртуальных терминалов (окон) с использо-
ванием традиционных (не потоковых) драйверов.
*23. Разработайте метод реализации виртуальных терминалов с использованием
потоков, в котором мультиплексированием ввода-вывода между виртуальным
и физическим терминалами занимался бы один из модулей ядра, а не поль-
зовательский процесс. Опишите механизм соединения потоков со сверткой и
разверткой. Что лучше: включить модуль, осуществляющий мультиплексиро-
вание, в состав ядра или построить его как пользовательский процесс ?
24. Команда ps сообщает интересную информацию об активности процессов в ра-
ботающей системе. В традиционных реализациях ps считывает информацию из
таблицы процессов, прямо из памяти ядра. Такой метод не совсем удобен в
среде разработки, когда размер записей таблицы процессов меняется и ко-
манде ps становится нелегко обнаружить в таблице соответствующие поля.
Разработайте драйвер, нечувствительный к изменениям среды.
329
Last-modified: Thu, 12 Feb 1998 07:20:28 GMT