Страница:
| } |
| } |
| } |
| |
| выполнить (пока в каноническом списке еще есть символы |
| и не исчерпано количество символов, указанное в вызове |
| функции 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'а.
В этом разделе рассмотрена работа терминалов ввода-вывода, которые пере-
дают данные на машину по одному символу за одну операцию, в точности как
пользователь их вводит с клавиатуры. Интеллектуальные терминалы подготавли-
вают свой вводной поток на внешнем устройстве, освобождая центральный про-
цессор для другой работы. Структура драйверов для таких терминалов походит
на структуру драйверов для терминалов ввода-вывода, несмотря на то, что фун-
кции строкового интерфейса различаются в зависимости от возможностей внешних
устройств.
Пользователи устанавливают параметры терминала, такие как символы стира-
ния и удаления, и извлекают значения текущих установок с помощью системной
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 указывает на существование исключительных
условий, связанных с конкретными дескрипторами, что бывает полезно при рабо-
те в сети.
Операторский терминал - это терминал, с которого пользователь регистри-
руется в системе, он управляет процессами, запущенными пользователем с тер-
минала. Когда процесс открывает терминал, драйвер терминала открывает стро-
ковый интерфейс. Если процесс возглавляет группу процессов как результат вы-
полнения системной функции setpgrp и если процесс не связан с одним из опе-
раторских терминалов, строковый интерфейс делает открываемый терминал опера-
торским. Он сохраняет старший и младший номера устройства для файла термина-
ла в адресном пространстве, выделенном процессу, а номер группы процессов,
связанной с открываемым процессом, в структуре данных терминального драйве-
ра. Открываемый процесс становится управляющим процессом, обычно входным
(начальным) командным процессором, что мы увидим далее.
Операторский терминал играет важную роль в обработке сигналов. Когда
пользователь нажимает клавиши "delete" (удаления), "break" (прерывания),
стирания или выхода, программа обработки прерываний загружает строковый ин-
терфейс, который посылает соответствующий сигнал всем процессам в группе.
318
Подобно этому, когда пользователь "зависает", программа обработки прерываний
от терминала получает информацию о "зависании" от аппаратуры, и строковый
интерфейс посылает соответствующий сигнал всем процессам в группе. Таким об-
разом, все процессы, запущенные с конкретного терминала, получают сигнал о
"зависании"; реакцией по умолчанию для большинства процессов будет выход из
программы по получении сигнала; это похоже на то, как при завершении работы
пользователя с терминалом из системы удаляются побочные процессы. После по-
сылки сигнала о "зависании" программа обработки прерываний от терминала раз-
ъединяет терминал с группой процессов, чтобы процессы из этой группы не мог-
ли больше получать сигналы, возникающие на терминале.
Зачастую процессам необходимо прочитать ил записать данные непосредст-
венно на операторский терминал, хотя стандартный ввод и вывод могут быть пе-
реназначены в другие файлы. Например, 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 слегка отличается. Поток представляет собой пол-
нодуплексную связь между процессом и драйвером устройства. Он состоит из со-
вокупности линейно связанных между собой пар очередей, каждая из которых
(пара) включает одну очередь для ввода и другую - для вывода. Когда процесс
записывает данные в поток, ядро посылает данные в очереди для вывода; когда
драйвер устройства получает входные данные, он пересылает их в очереди для
ввода к процессу, производящему чтение. Очереди обмениваются сообщениями с
соседними очередями, используя четко определенный интерфейс. Каждая пара
очередей связана с одним из модулей ядра, таким как драйвер, строковый ин-
терфейс или протокол, и модули ядра работают с данными, прошедшими через со-
>
| } |
| } |
| |
| выполнить (пока в каноническом списке еще есть символы |
| и не исчерпано количество символов, указанное в вызове |
| функции 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'а завершается,
когда пользователь нажимает
дится выход из shell'а.
В этом разделе рассмотрена работа терминалов ввода-вывода, которые пере-
дают данные на машину по одному символу за одну операцию, в точности как
пользователь их вводит с клавиатуры. Интеллектуальные терминалы подготавли-
вают свой вводной поток на внешнем устройстве, освобождая центральный про-
цессор для другой работы. Структура драйверов для таких терминалов походит
на структуру драйверов для терминалов ввода-вывода, несмотря на то, что фун-
кции строкового интерфейса различаются в зависимости от возможностей внешних
устройств.
Пользователи устанавливают параметры терминала, такие как символы стира-
ния и удаления, и извлекают значения текущих установок с помощью системной
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 указывает на существование исключительных
условий, связанных с конкретными дескрипторами, что бывает полезно при рабо-
те в сети.
Операторский терминал - это терминал, с которого пользователь регистри-
руется в системе, он управляет процессами, запущенными пользователем с тер-
минала. Когда процесс открывает терминал, драйвер терминала открывает стро-
ковый интерфейс. Если процесс возглавляет группу процессов как результат вы-
полнения системной функции setpgrp и если процесс не связан с одним из опе-
раторских терминалов, строковый интерфейс делает открываемый терминал опера-
торским. Он сохраняет старший и младший номера устройства для файла термина-
ла в адресном пространстве, выделенном процессу, а номер группы процессов,
связанной с открываемым процессом, в структуре данных терминального драйве-
ра. Открываемый процесс становится управляющим процессом, обычно входным
(начальным) командным процессором, что мы увидим далее.
Операторский терминал играет важную роль в обработке сигналов. Когда
пользователь нажимает клавиши "delete" (удаления), "break" (прерывания),
стирания или выхода, программа обработки прерываний загружает строковый ин-
терфейс, который посылает соответствующий сигнал всем процессам в группе.
318
Подобно этому, когда пользователь "зависает", программа обработки прерываний
от терминала получает информацию о "зависании" от аппаратуры, и строковый
интерфейс посылает соответствующий сигнал всем процессам в группе. Таким об-
разом, все процессы, запущенные с конкретного терминала, получают сигнал о
"зависании"; реакцией по умолчанию для большинства процессов будет выход из
программы по получении сигнала; это похоже на то, как при завершении работы
пользователя с терминалом из системы удаляются побочные процессы. После по-
сылки сигнала о "зависании" программа обработки прерываний от терминала раз-
ъединяет терминал с группой процессов, чтобы процессы из этой группы не мог-
ли больше получать сигналы, возникающие на терминале.
Зачастую процессам необходимо прочитать ил записать данные непосредст-
венно на операторский терминал, хотя стандартный ввод и вывод могут быть пе-
реназначены в другие файлы. Например, 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 слегка отличается. Поток представляет собой пол-
нодуплексную связь между процессом и драйвером устройства. Он состоит из со-
вокупности линейно связанных между собой пар очередей, каждая из которых
(пара) включает одну очередь для ввода и другую - для вывода. Когда процесс
записывает данные в поток, ядро посылает данные в очереди для вывода; когда
драйвер устройства получает входные данные, он пересылает их в очереди для
ввода к процессу, производящему чтение. Очереди обмениваются сообщениями с
соседними очередями, используя четко определенный интерфейс. Каждая пара
очередей связана с одним из модулей ядра, таким как драйвер, строковый ин-
терфейс или протокол, и модули ядра работают с данными, прошедшими через со-
>