| | } | +----------------------------------------------------------------+ Рисунок 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.3.4 Опрос терминала Иногда удобно производить опрос устройства, то есть считывать с него данные, если они есть, или продолжать выполнять обычную работу - в противном случае. Программа на Рисунке 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", указывает непосредственно на операторский терминал и его драйвер. 10.3.7 Вход в систему Как показано в главе 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. Алгоритм регистрации 10.4 ПОТОКИ Схема реализации драйверов устройств, хотя и отвечает заложенным требо- ваниям, страдает некоторыми недостатками, которые с годами стали заметнее. Разные драйверы имеют тенденцию дублировать свои функции, в частности драй- веры, которые реализуют сетевые протоколы и которые обычно включают в себя секцию управления устройством и секцию протокола. Несмотря на то, что секция протокола должна быть общей для всех сетевых устройств, на практике это не так, поскольку ядро не имеет адекватных механизмов для общего использования. Например, символьные списки могли бы быть полезными благодаря своим возмож- ностям в буферизации, но они требуют больших затрат ресурсов на посимвольную обработку. Попытки обойти этот механизм, чтобы повысить производительность системы, привели к нарушению модульности подсистемы управления вводом-выво- дом. Отсутствие общности на уровне драйверов распространяется вплоть до 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, и эти сооб- щения передаются на физическое устройство. 10.4.2 Анализ потоков Ричи упоминает о том, что им была предпринята попытка создания потоков только с процедурами "вывода" или только с процедурами обслуживания. Однако, процедура обслуживания необходима для управления потоками данных, так как модули должны иногда ставить данные в очередь, если соседние модули на время закрыты для приема данных. Процедура "вывода" так же необходима, поскольку данные должны иногда доставляться в соседние модули незамедлительно. Напри- мер, строковому интерфейсу терминала нужно вести эхо-сопровождение ввода данных на терминале в темпе с процессом. Системная функция 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