| | | | 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.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). Затем он устанавливает в структуре данных очереди флаг, показывая тем самым, что и