АРХИТЕКТУРА ОПЕРАЦИОННОЙ СИСТЕМЫ UNIX THE DESIGN OF THE UNIX OPERATING SYSTEM by Maurice J. Bach (Перевод с английского к.т.н. Крюкова А.В.) СОДЕРЖАНИЕ ПРЕДИСЛОВИЕ ............................................... xi ГЛАВА 1. ОБЩИЙ ОБЗОР ОСОБЕННОСТЕЙ СИСТЕМЫ ................. 1 1.1 ИСТОРИЯ ........................................... 1 1.2 СТРУКТУРА СИСТЕМЫ ................................. 4 1.3 ОБЗОР С ТОЧКИ ЗРЕНИЯ ПОЛЬЗОВАТЕЛЯ ................. 6 1.4 ФУНКЦИИ ОПЕРАЦИОННОЙ СИСТЕМЫ ...................... 14 1.5 ПРЕДПОЛАГАЕМАЯ АППАРАТНАЯ СРЕДА ................... 15 1.6 ВЫВОДЫ ............................................ 18 ГЛАВА 2. ВВЕДЕНИЕ В АРХИТЕКТУРУ ЯДРА ОПЕРАЦИОННОЙ СИСТЕМЫ . 19 2.1 АРХИТЕКТУРА ОПЕРАЦИОННОЙ СИСТЕМЫ UNIX ............. 19 2.2 ВВЕДЕНИЕ В ОСНОВНЫЕ ПОНЯТИЯ СИСТЕМЫ ............... 22 2.3 СТРУКТУРЫ ДАННЫХ ЯДРА ............................. 34 2.4 УПРАВЛЕНИЕ СИСТЕМОЙ ............................... 34 2.5 ВЫВОДЫ И ОБЗОР ПОСЛЕДУЮЩИХ ГЛАВ ................... 36 2.6 УПРАЖНЕНИЯ ........................................ 37 ГЛАВА 3. БУФЕР СВЕРХОПЕРАТИВНОЙ ПАМЯТИ (КЕШ) .............. 38 3.1 ЗАГОЛОВКИ БУФЕРА .................................. 39 3.2 СТРУКТУРА ОБЛАСТИ БУФЕРОВ (БУФЕРНОГО ПУЛА) ........ 40 3.3 МЕХАНИЗМ ПОИСКА БУФЕРА ............................ 42 3.4 ЧТЕНИЕ И ЗАПИСЬ ДИСКОВЫХ БЛОКОВ ................... 53 3.5 ПРЕИМУЩЕСТВА И НЕУДОБСТВА БУФЕРНОГО КЕША .......... 56 3.6 ВЫВОДЫ ............................................ 57 3.7 УПРАЖНЕНИЯ ........................................ 58 ГЛАВА 4. ВНУТРЕННЕЕ ПРЕДСТАВЛЕНИЕ ФАЙЛОВ .................. 60 4.1 ИНДЕКСЫ ........................................... 61 4.2 СТРУКТУРА ФАЙЛА ОБЫЧНОГО ТИПА ..................... 67 4.3 КАТАЛОГИ .......................................... 73 4.4 ПРЕВРАЩЕНИЕ СОСТАВНОГО ИМЕНИ ФАЙЛА (ПУТИ ПОИСКА) В ИДЕНТИФИКАТОР ИНДЕКСА ........................... 74 4.5 СУПЕРБЛОК ......................................... 76 4.6 НАЗНАЧЕНИЕ ИНДЕКСА НОВОМУ ФАЙЛУ ................... 77 4.7 ВЫДЕЛЕНИЕ ДИСКОВЫХ БЛОКОВ ......................... 84 4.8 ДРУГИЕ ТИПЫ ФАЙЛОВ ................................ 88 4.9 ВЫВОДЫ ............................................ 88 4.10 УПРАЖНЕНИЯ ....................................... 89 ГЛАВА 5. СИСТЕМНЫЕ ОПЕРАЦИИ ДЛЯ РАБОТЫ С ФАЙЛОВОЙ СИСТЕМОЙ 91 5.1 OPEN .............................................. 92 5.2 READ .............................................. 96 5.3 WRITE .............................................101 5.4 ЗАХВАТ ФАЙЛА И ЗАПИСИ .............................103 5.5 УКАЗАНИЕ МЕСТА В ФАЙЛЕ, ГДЕ БУДЕТ ВЫПОЛНЯТЬСЯ ВВОД-ВЫВОД - LSEEK ................................103 5.6 CLOSE .............................................103 5.7 СОЗДАНИЕ ФАЙЛА ....................................105 5.8 СОЗДАНИЕ СПЕЦИАЛЬНЫХ ФАЙЛОВ .......................107 5.9 СМЕНА ТЕКУЩЕГО И КОРНЕВОГО КАТАЛОГА ...............109 5.10 СМЕНА ВЛАДЕЛЬЦА И РЕЖИМА ДОСТУПА К ФАЙЛУ .........110 5.11 STAT И FSTAT .....................................110 5.12 КАНАЛЫ ...........................................111 5.13 DUP ..............................................117 5.14 МОНТИРОВАНИЕ И ДЕМОНТИРОВАНИЕ ФАЙЛОВЫХ СИСТЕМ ....119 5.15 LINK .............................................128 5.16 UNLINK ...........................................132 5.17 АБСТРАКТНЫЕ ОБРАЩЕНИЯ К ФАЙЛОВЫМ СИСТЕМАМ ........138 5.18 СОПРОВОЖДЕНИЕ ФАЙЛОВОЙ СИСТЕМЫ ...................139 5.19 ВЫВОДЫ ...........................................140 5.20 УПРАЖНЕНИЯ .......................................140 ГЛАВА 6. СТРУКТУРА ПРОЦЕССОВ ..............................146 6.1 СОСТОЯНИЯ ПРОЦЕССА И ПЕРЕХОДЫ МЕЖДУ НИМИ ..........147 6.2 ФОРМАТ ПАМЯТИ СИСТЕМЫ .............................151 6.3 КОНТЕКСТ ПРОЦЕССА .................................159 6.4 СОХРАНЕНИЕ КОНТЕКСТА ПРОЦЕССА .....................162 6.5 УПРАВЛЕНИЕ АДРЕСНЫМ ПРОСТРАНСТВОМ ПРОЦЕССА ........171 6.6 ПРИОСТАНОВКА ВЫПОЛНЕНИЯ ...........................182 6.7 ВЫВОДЫ ............................................188 6.8 УПРАЖНЕНИЯ ........................................189 ГЛАВА 7. УПРАВЛЕНИЕ ПРОЦЕССОМ .............................191 7.1 СОЗДАНИЕ ПРОЦЕССА .................................192 7.2 СИГНАЛЫ ...........................................200 7.3 ЗАВЕРШЕНИЕ ВЫПОЛНЕНИЯ ПРОЦЕССА ....................212 7.4 ОЖИДАНИЕ ЗАВЕРШЕНИЯ ВЫПОЛНЕНИЯ ПРОЦЕССА ...........213 7.5 ВЫЗОВ ДРУГИХ ПРОГРАММ .............................217 7.6 КОД ИДЕНТИФИКАЦИИ ПОЛЬЗОВАТЕЛЯ ПРОЦЕССА ...........227 7.7 ИЗМЕНЕНИЕ РАЗМЕРА ПРОЦЕССА ........................229 7.8 КОМАНДНЫЙ ПРОЦЕССОР SHELL .........................232 7.9 ЗАГРУЗКА СИСТЕМЫ И НАЧАЛЬНЫЙ ПРОЦЕСС ..............235 7.10 ВЫВОДЫ ...........................................238 7.11 УПРАЖНЕНИЯ .......................................239 ГЛАВА 8. ДИСПЕТЧЕРИЗАЦИЯ ПРОЦЕССОВ И ЕЕ ВРЕМЕННЫЕ ХАРАКТЕРИСТИКИ ...................................247 8.1 ПЛАНИРОВАНИЕ ВЫПОЛНЕНИЯ ПРОЦЕССОВ .................248 8.2 СИСТЕМНЫЕ ОПЕРАЦИИ, СВЯЗАННЫЕ СО ВРЕМЕНЕМ .........258 8.3 ТАЙМЕР ............................................260 8.4 ВЫВОДЫ ............................................268 8.5 УПРАЖНЕНИЯ ........................................268 ГЛАВА 9. АЛГОРИТМЫ УПРАВЛЕНИЯ ПАМЯТЬЮ .....................271 9.1 СВОПИНГ ...........................................272 9.2 ПОДКАЧКА ПО ЗАПРОСУ ...............................285 9.3 СИСТЕМА СМЕШАННОГО ТИПА СО СВОПИНГОМ И ПОДКАЧКОЙ ПО ЗАПРОСУ ........................................307 9.4 ВЫВОДЫ ............................................307 9.5 УПРАЖНЕНИЯ ........................................308 ГЛАВА 10. ПОДСИСТЕМА УПРАВЛЕНИЯ ВВОДОМ-ВЫВОДОМ ............312 10.1 ВЗАИМОДЕЙСТВИЕ ДРАЙВЕРОВ С ПРОГРАММНОЙ И АППАРАТНОЙ СРЕДОЙ ................................313 10.2 ДИСКОВЫЕ ДРАЙВЕРЫ ................................325 10.3 ТЕРМИНАЛЬНЫЕ ДРАЙВЕРЫ ............................329 10.4 ПОТОКИ ...........................................344 10.5 ВЫВОДЫ ...........................................351 10.6 УПРАЖНЕНИЯ .......................................352 ГЛАВА 11. ВЗАИМОДЕЙСТВИЕ ПРОЦЕССОВ ........................355 11.1 ТРАССИРОВКА ПРОЦЕССОВ ............................356 11.2 ВЗАИМОДЕЙСТВИЕ ПРОЦЕССОВ В ВЕРСИИ V СИСТЕМЫ ......359 11.3 ВЗАИМОДЕЙСТВИЕ В СЕТИ ............................382 11.4 ГНЕЗДА ...........................................383 11.5 ВЫВОДЫ ...........................................388 11.6 УПРАЖНЕНИЯ .......................................389 ГЛАВА 12. МНОГОПРОЦЕССОРНЫЕ СИСТЕМЫ .......................391 12.1 ПРОБЛЕМЫ, СВЯЗАННЫЕ С МНОГОПРОЦЕССОРНЫМИ СИСТЕМАМИ ........................................392 12.2 ГЛАВНЫЙ И ПОДЧИНЕННЫЙ ПРОЦЕССОРЫ .................393 12.3 СЕМАФОРЫ .........................................395 12.4 СИСТЕМА TUNIS ....................................410 12.5 УЗКИЕ МЕСТА В ФУНКЦИОНИРОВАНИИ МНОГОПРОЦЕССОРНЫХ СИСТЕМ ...........................................410 12.6 УПРАЖНЕНИЯ .......................................410 ГЛАВА 13. РАСПРЕДЕЛЕННЫЕ СИСТЕМЫ ..........................412 13.1 ПЕРИФЕРИЙНЫЕ ПРОЦЕССОРЫ ..........................414 13.2 СВЯЗЬ ТИПА NEWCASTLE .............................422 13.3 "ПРОЗРАЧНЫЕ" РАСПРЕДЕЛЕННЫЕ ФАЙЛОВЫЕ СИСТЕМЫ .....426 13.4 РАСПРЕДЕЛЕННАЯ МОДЕЛЬ БЕЗ ПЕРЕДАТОЧНЫХ ПРОЦЕССОВ .429 13.5 ВЫВОДЫ ...........................................430 13.6 УПРАЖНЕНИЯ .......................................431 ПРИЛОЖЕНИЕ - СИСТЕМНЫЕ ОПЕРАЦИИ ...........................434 БИБЛИОГРАФИЯ ..............................................454 АЛФАВИТНЫЙ УКАЗАТЕЛЬ ......................................458 ПРЕДИСЛОВИЕ Впервые система UNIX была описана в 1974 году в статье Кена Томпсона и Дэнниса Ричи в журнале "Communications of the ACM" [Thompson 74]. С этого времени она получила широкое распростране- ние и завоевала широкую популярность среди производителей ЭВМ, которые все чаще стали оснащать ею свои машины. Особой популяр- ностью она пользуется в университетах, где довольно часто участ- вует в исследовательском и учебном процессе. Множество книг и статей посвящено описанию отдельных частей системы; среди них два специальных выпуска "Bell System Technical Journal" за 1978 год [BSTJ 78] и за 1984 год [BSTJ 84]. Во многих книгах описывается пользовательский интерфейс, в частности ис- пользование электронной почты, подготовка документации, работа с командным процессором Shell; в некоторых книгах, таких как "The UNIX Programming Environment" [Kernighan 84] и "Advanced UNIX Programming" [Rochkind 85], описывается программный интерфейс. Настоящая книга посвящена описанию внутренних алгоритмов и струк- тур, составляющих основу операционной системы (т.н. "ядро"), и объяснению их взаимосвязи с программным интерфейсом. Таким обра- зом, она будет полезна для работающих в различных операционных средах. Во-первых, она может использоваться в качестве учебного пособия по курсу "Операционные системы" как для студентов послед- него курса, так и для аспирантов первого года обучения. При рабо- те с книгой было бы гораздо полезнее обращаться непосредственно к исходному тексту системных программ, но книгу можно читать и не- зависимо от него. Во-вторых, эта книга может служить в качестве справочного руководства для системных программистов, из которого последние могли бы лучше уяснить себе механизм работы ядра опера- ционной системы и сравнить между собой алгоритмы, используемые в UNIX, и алгоритмы, используемые в других операционных системах. Наконец, программисты, работающие в среде UNIX, могут углубить свое понимание механизма взаимодействия программ с операционной системой и посредством этого прийти к написанию более эффективных и совершенных программ. Содержание и порядок построения материала в книге соответс- твуют курсу лекций, подготовленному и прочитанному мной для сот- рудников фирмы Bell Laboratories, входящей в состав корпорации AT &T, между 1983 и 1984 гг. Несмотря на то, что главное внимание в курсе лекций обращалось на исходный текст системных программ, я обнаружил, что понимание исходного текста облегчается, если поль- зователь имеет представление о системных алгоритмах. В книге я пытался изложить описание алгоритмов как можно проще, чтобы и в малом отразить простоту и изящество рассматриваемой операционной системы. Таким образом, книга представляет собой не только под- робное истолкование особенностей системы на английском языке; это изображение общего механизма работы различных алгоритмов, и что гораздо важнее, это отражение процесса их взаимодействия между собой. Алгоритмы представлены на псевдокоде, похожем на язык Си, поскольку читателю легче воспринимать описание на естественном языке; наименования алгоритмов соответствуют именам процедур, составляющих ядро операционной системы. Рисунки описывают взаимо- действие различных информационных структур под управлением опера- ционной системы. В последних главах многие системные понятия ил- люстрируются с помощью небольших программ на языке Си. В целях экономии места и обеспечения ясности изложения из этих примеров исключен контроль возникновения ошибок, который обычно предусмат- ривается при написании программ. Эти примеры прогонялись мною под управлением версии V; за исключением программ, иллюстрирующих особенности, присущие версии V, их можно выполнять под управлени- ем других версий операционной системы. Большое число упражнений, подготовленных первоначально для курса лекций, приведено в конце каждой главы, они составляют клю- чевую часть книги. Отдельные упражнения, иллюстрирующие основные понятия, размещены непосредственно в тексте книги. Другая часть упражнений отличается большей сложностью, поскольку их предназна- чение состоит в том, чтобы помочь читателю углубить свое понима- ние особенностей системы. И, наконец, часть упражнений является по природе исследовательской, предназначенной для изучения от- дельных проблем. Упражнения повышенной сложности помечены звез- дочками. Системное описание базируется на особенностях операционной системы UNIX версия V редакция 2, распространением которой зани- мается корпорация AT&T, с учетом отдельных особенностей редакции 3. Это та система, с которой я наиболее знаком, однако я поста- рался отразить и интересные детали других разновидностей операци- онных систем, в частности систем, распространяемых через "Berkeley Software Distribution" (BSD). Я не касался вопросов, связанных с характеристиками отдельных аппаратных средств, стара- ясь только в общих чертах охватить процесс взаимодействия ядра операционной системы с аппаратными средствами и игнорируя харак- терные особенности физической конфигурации. Тем не менее, там, где вопросы, связанные с машинными особенностями, представились мне важными с точки зрения понимания механизма функционирования ядра, оказалось уместным и углубление в детали. По крайней мере, беглый просмотр затронутых в книге вопросов ясно указывает те составные части операционной системы, которые являются наиболее машинно-зависимыми. Общение с книгой предполагает наличие у читателя опыта прог- раммирования на одном из языков высокого уровня и желательно на языке ассемблера. Читателю рекомендуется приобрести опыт работы с операционной системой UNIX и познакомиться с языком программиро- вания Си [Kernighan 78]. Тем не менее, я старался изложить мате- риал в книге таким образом, чтобы читатель смог овладеть им даже при отсутствии требуемых навыков. В приложении к книге приведено краткое описание обращений к операционной системе, которого будет достаточно для того, чтобы получить представление о содержании книги, но которое не может служить в качестве полного справочного руководства. Материал в книге построен следующим образом. Глава 1 служит введением, содержащим краткое, общее описание системных особен- ностей с точки зрения пользователя и объясняющим структуру систе- мы. В главе 2 дается общее представление об архитектуре ядра и поясняются некоторые основные понятия. В остальной части книги освещаются вопросы, связанные с общей архитектурой системы и опи- санием ее различных компонент как блоков единой конструкции. В ней можно выделить три раздела: файловая система, управление про- цессами и вопросы, связанные с развитием. Файловая система предс- тавлена первой, поскольку ее понимание легче по сравнению с уп- равлением процессами. Так, глава 3 посвящена описанию механизма функционирования системного буфера сверхоперативной памяти (ке- ша), составляющего основу файловой системы. Глава 4 описывает ин- формационные структуры и алгоритмы, используемые файловой систе- мой. В этих алгоритмах используются методы, объясняемые в главе 3, для ведения внутренней "бухгалтерии", необходимой для управле- ния пользовательскими файлами. Глава 5 посвящена описанию обраще- ний к операционной системе, обслуживающих интерфейс пользователя с файловой системой; для обеспечения доступа к пользовательским файлам используются алгоритмы главы 4. Основное внимание в главе 6 уделяется управлению процессами. В ней определяется понятие контекста процесса и исследуются внут- ренние составляющие ядра операционной системы, управляющие кон- текстом процесса. В частности, рассматривается обращение к опера- ционной системе, обработка прерываний и переключение контекста. В главе 7 анализируются те системные операции, которые управляют контекстом процесса. Глава 8 касается планирования процессов, глава 9 - распределения памяти, включая системы подкачки и заме- щения страниц. В главе 10 дается обзор общих особенностей взаимодействия, которое обеспечивают драйверы устройств, особое внимание уделяет- ся дисковым и терминальным драйверам. Несмотря на то, что уст- ройства логически входят в состав файловой системы, их рассмотре- ние до этого момента откладывалось в связи с возникновением вопросов, связанных с управлением процессами, при обсуждении тер- минальных драйверов. Эта глава также служит мостиком к вопросам, связанным с развитием системы, которые рассматриваются в конце книги. Глава 11 касается взаимодействия процессов и организации сетей, в том числе сообщений, используемых в версии V, разделения памяти, семафоров и пакетов BSD. Глава 12 содержит компактное из- ложение особенностей двухпроцессорной системы UNIX, в главе 13 исследуются двухмашинные распределенные вычислительные системы. Материал, представленный в первых девяти главах, может быть прочитан в процессе изучения курса "Операционные системы" в тече- ние одного семестра, материал остальных глав следует изучать на опережающих семинарах с параллельным выполнением практических за- даний. Теперь мне бы хотелось предупредить читателя о следующем. Я не пытался оценить производительность системы в абсолютном выра- жении, не касался и параметров конфигурации, необходимых для инс- талляции системы. Эти данные меняются в зависимости от типа маши- ны, конфигурации комплекса технических средств, версии и реализации системы, состава задач. Кроме того, я сознательно из- бегал любых предсказаний по поводу дальнейшего развития операци- онной системы UNIX. Изложение вопросов, связанных с развитием, не подкреплено обязательством корпорации AT&T обеспечить соответс- твующие характеристики, даже не гарантируется то, что соответс- твующие области являются объектом исследования. Мне приятно выразить благодарность многим друзьям и коллегам за помощь при написании этой книги и за конструктивные критичес- кие замечания, высказанные при ознакомлении с рукописью. Я должен выразить глубочайшую признательность Яну Джонстону, который посо- ветовал мне написать эту книгу, оказал мне поддержку на начальном этапе и просмотрел набросок первых глав. Ян открыл мне многие секреты ремесла и я всегда буду в долгу перед ним. Дорис Райан также поддерживала меня с самого начала, и я всегда буду ценить ее доброту и внимательность. Дэннис Ричи добровольно ответил на многочисленные вопросы, касающиеся исторического и технического аспектов системы. Множество людей пожертвовали своим временем и силами на ознакомление с вариантами рукописи, появление этой кни- ги во многом обязано высказанным ими подробным замечаниям. Среди них Дебби Бэч, Дуг Байер, Лэнни Брэндвейн, Стив Барофф, Том Бат- лер, Рон Гомес, Месат Гандак, Лаура Изрейел, Дин Джегелс, Кейт Келлеман, Брайан Керниган, Боб Мартин, Боб Митц, Дейв Новиц, Майкл Попперс, Мэрилин Сэфран, Курт Шиммель, Зуи Спитц, Том Вэ- ден, Билл Вебер, Лэрри Вэр и Боб Зэрроу. Мэри Фрустак помогала подготовить рукопись к набору. Я хотел бы также поблагодарить мое руководство за постоянную поддержку, которую я ощущал на всем протяжении работы, и коллег за атмосферу, способствовавшую мне в работе, и за замечательные условия, предоставленные фирмой AT&T Bell Laboratories. Джон Вейт и персонал издательства Prentice-Hall оказали самую разнообразную помощь в придании книге ее окончательного вида. Последней по списку, но не по величине явилась помощь моей жены, Дебби, оказавшей мне эмоциональную под- держку, без которой я бы не достиг успеха. ОБЩИЙ ОБЗОР ОСОБЕННОСТЕЙ СИСТЕМЫ За время, прошедшее с момента ее появления в 1969 году, сис- тема UNIX стала довольно популярной и получила распространение на машинах с различной мощностью обработки, от микропроцессоров до больших ЭВМ, обеспечивая на них общие условия выполнения прог- рамм. Система делится на две части. Одну часть составляют прог- раммы и сервисные функции, то, что делает операционную среду UNIX такой популярной; эта часть легко доступна пользователям, она включает такие программы, как командный процессор, обмен сообще- ниями, пакеты обработки текстов и системы обработки исходных текстов программ. Другая часть включает в себя собственно опера- ционную систему, поддерживающую эти программы и функции. В этой книге дается детальное описание собственно операционной системы. Основное внимание концентрируется на описании системы UNIX версии V, распространением которой занимается корпорация AT&T, при этом рассматриваются интересные особенности и других версий. Приводят- ся основные информационные структуры и алгоритмы, используемые в операционной системе и в конечном итоге создающие условия для функционирования стандартного пользовательского интерфейса. Данная глава служит введением в систему UNIX. В ней делается обзор истории ее создания и намечаются контуры общей структуры системы. В следующей главе содержится более детальная вводная ин- формация по операционной системе. 1.1 ИСТОРИЯ В 1965 году фирма Bell Telephone Laboratories, объединив свои усилия с компанией General Electric и проектом MAC Массачусетско- го технологического института, приступили к разработке новой опе- рационной системы, получившей название Multics [Organick 72]. Пе- ред системой Multics были поставлены задачи - обеспечить одновременный доступ к ресурсам ЭВМ большого количества пользова- телей, обеспечить достаточную скорость вычислений и хранение дан- ных и дать возможность пользователям в случае необходимости сов- местно использовать данные. Многие разработчики, впоследствии принявшие участие в создании ранних редакций системы UNIX, учас- твовали в работе над системой Multics в фирме Bell Laboratories. Хотя первая версия системы Multics и была запущена в 1969 году на ЭВМ GE 645, она не обеспечивала выполнение главных вычислительных задач, для решения которых она предназначалась, и не было даже ясно, когда цели разработки будут достигнуты. Поэтому фирма Bell Laboratories прекратила свое участие в проекте. По окончании работы над проектом Multics сотрудники Исследо- вательского центра по информатике фирмы Bell Laboratories оста- лись без "достаточно интерактивного вычислительного средства" [Ritchie 84a]. Пытаясь усовершенствовать среду программирования, Кен Томпсон, Дэннис Ричи и другие набросали на бумаге проект фай- ловой системы, получивший позднее дальнейшее развитие в ранней версии файловой системы UNIX. Томпсоном были написаны программы, имитирующие поведение предложенной файловой системы в режиме под- качки данных по запросу, им было даже создано простейшее ядро операционной системы для ЭВМ GE 645. В то же время он написал на Фортране игровую программу "Space Travel" ("Космическое путешест- вие") для системы GECOS (Honeywell 635), но программа не смогла удовлетворить пользователей, поскольку управлять "космическим кораблем" оказалось сложно, кроме того, при загрузке программа занимала много места. Позже Томпсон обнаружил малоиспользуемый компьютер PDP-7, оснащенный хорошим графическим дисплеем и имею- щий дешевое машинное время. Создавая программу "Космическое путе- шествие" для PDP-7, Томпсон получил возможность изучить машину, однако условия разработки программ потребовали использования кросс-ассемблера для трансляции программы на машине с системой GECOS и использования перфоленты для ввода в PDP-7. Для того, чтобы улучшить условия разработки, Томпсон и Ричи выполнили на PDP-7 свой проект системы, включивший первую версию файловой сис- темы UNIX, подсистему управления процессами и небольшой набор утилит. В конце концов, новая система больше не нуждалась в под- держке со стороны системы GECOS в качестве операционной среды разработки и могла поддерживать себя сама. Новая система получила название UNIX, по сходству с Multics его придумал еще один сот- рудник Исследовательского центра по информатике Брайан Керниган. Несмотря на то, что эта ранняя версия системы UNIX уже была многообещающей, она не могла реализовать свой потенциал до тех пор, пока не получила применение в реальном проекте. Так, для то- го, чтобы обеспечить функционирование системы обработки текстов для патентного отдела фирмы Bell Laboratories, в 1971 году систе- ма UNIX была перенесена на ЭВМ PDP-11. Система отличалась неболь- шим объемом: 16 Кбайт для системы, 8 Кбайт для программ пользова- телей, обслуживала диск объемом 512 Кбайт и отводила под каждый файл не более 64 Кбайт. После своего первого успеха Томпсон соб- рался было написать для новой системы транслятор с Фортрана, но вместо этого занялся языком Би (B), предшественником которого явился язык BCPL [Richards 69]. Би был интерпретируемым языком со всеми недостатками, присущими подобным языкам, поэтому Ричи пере- делал его в новую разновидность, получившую название Си (C) и разрешающую генерировать машинный код, объявлять типы данных и определять структуру данных. В 1973 году система была написана заново на Си, это был шаг, неслыханный для того времени, но имев- ший огромный резонанс среди сторонних пользователей. Количество машин фирмы Bell Laboratories, на которых была инсталлирована система, возросло до 25, в результате чего была создана группа по системному сопровождению UNIX внутри фирмы. В то время корпорация AT&T не могла заниматься продажей компьютерных продуктов в связи с соответствующим соглашением, подписанным ею с федеральным правительством в 1956 году, и расп- ространяла систему UNIX среди университетов, которым она была нужна в учебных целях. Следуя букве соглашения, корпорация AT&T не рекламировала, не продавала и не сопровождала систему. Несмот- ря на это, популярность системы устойчиво росла. В 1974 году Томпсон и Ричи опубликовали статью, описывающую систему UNIX, в журнале Communications of the ACM [Thompson 74], что дало еще один импульс к распространению системы. К 1977 году количество машин, на которых функционировала система UNIX, увеличилось до 500, при чем 125 из них работали в университетах. Система UNIX завоевала популярность среди телефонных компаний, поскольку обес- печивала хорошие условия для разработки программ, обслуживала ра- боту в сети в режиме диалога и работу в реальном масштабе времени (с помощью системы MERT [Lycklama 78a]). Помимо университетов, лицензии на систему UNIX были переданы коммерческим организациям. В 1977 году корпорация Interactive Systems стала первой организа- цией, получившей права на перепродажу системы UNIX с надбавкой к цене за дополнительные услуги (*), (*) Организации, получившие права на перепродажу с надбавкой к цене за дополнительные услуги, оснащают вычислительную сис- тему прикладными программами, касающимися конкретных облас- тей применения, стремясь удовлетворить требования рынка. Та- кие организации чаще продают прикладные программы, нежели операционные системы, под управлением которых эти программы работают. которые заключались в адаптации системы к функционированию в автоматизированных системах управления учрежденческой деятельностью. 1977 год также был отмечен "переносом" системы UNIX на машину, отличную от PDP (благодаря чему стал возможен запуск системы на другой машине без изменений или с небольшими изменениями), а именно на Interdata 8/32. С ростом популярности микропроцессоров другие компании стали переносить систему UNIX на новые машины, однако ее простота и яс- ность побудили многих разработчиков к самостоятельному развитию системы, в результате чего было создано несколько вариантов ба- зисной системы. За период между 1977 и 1982 годом фирма Bell Laboratories объединила несколько вариантов, разработанных в кор- порации AT&T, в один, получивший коммерческое название UNIX вер- сия III. В дальнейшем фирма Bell Laboratories добавила в версию III несколько новых особенностей, назвав новый продукт UNIX вер- сия V (**), (**) А что же версия IV ? Модификация внутреннего варианта систе- мы получила название "версия V". и эта версия стала официально распространяться корпорацией AT&T с января 1983 года. В то же время сотрудники Калифорнийского университета в Бэркли разработали вариант системы UNIX, получивший название BSD 4.3 для машин серии VAX и отличающийся некоторыми новыми, интересными особенностями. Основное внимание в этой книге концентрируется на описании системы UNIX версии V, однако время от времени мы будем касаться и особенностей системы BSD. К началу 1984 года система UNIX была уже инсталлирована приб- лизительно на 100000 машин по всему миру, при чем на машинах с широким диапазоном вычислительных возможностей - от микропроцес- соров до больших ЭВМ - и разных изготовителей. Ни о какой другой операционной системе нельзя было бы сказать того же. Популярность и успех системы UNIX объяснялись несколькими причинами: * Система написана на языке высокого уровня, благодаря чему ее легко читать, понимать, изменять и переносить на другие машины. По оценкам, сделанным Ричи, первый вариант системы на Си имел на 20-40 % больший объем и работал медленнее по сравнению с ва- риантом на ассемблере, однако преимущества использования языка высокого уровня намного перевешивают недостатки (см. [Ritchie 78b], стр. 1965). * Наличие довольно простого пользовательского интерфейса, в кото- ром имеется возможность предоставлять все необходимые пользова- телю услуги. * Наличие элементарных средств, позволяющих создавать сложные программы из более простых. * Наличие иерархической файловой системы, легкой в сопровождении и эффективной в работе. * Обеспечение согласования форматов в файлах, работа с последова- тельным потоком байтов, благодаря чему облегчается чтение прикладных программ. * Наличие простого, последовательного интерфейса с периферийными устройствами. * Система является многопользовательской, многозадачной; каждый пользователь может одновременно выполнять несколько процессов. * Архитектура машины скрыта от пользователя, благодаря этому об- легчен процесс написания программ, работающих на различных кон- фигурациях аппаратных средств. Простота и последовательность вообще отличают систему UNIX и объясняют большинство из вышеприведенных доводов в ее пользу. Хотя операционная система и большинство команд написаны на Си, система UNIX поддерживает ряд других языков, таких как Форт- ран, Бейсик, Паскаль, Ада, Кобол, Лисп и Пролог. Система UNIX мо- жет поддерживать любой язык программирования, для которого имеет- ся компилятор или интерпретатор, и обеспечивать системный интерфейс, устанавливающий соответствие между пользовательскими запросами к операционной системе и набором запросов, принятых в UNIX. 1.2 СТРУКТУРА СИСТЕМЫ На Рисунке 1.1 изображена архитектура верхнего уровня системы UNIX. Технические средства, показанные в центре диаграммы, выпол- няют функции, обеспечивающие функционирование операционной систе- мы и перечисленные в разделе 1.5. Операционная система взаимо- действует с аппаратурой непосредственно (***), (***) В некоторых реализациях системы UNIX операционная система взаимодействует с собственной операционной системой, кото- рая, в свою очередь, взаимодействует с аппаратурой и выпол- няет необходимые функции по обслуживанию системы. В таких реализациях допускается инсталляция других операционных систем с загрузкой под их управлением прикладных программ параллельно с системой UNIX. Классическим примером подобной реализации явилась система MERT [Lycklama 78a]. Более новым примером могут служить реализации для компьютеров серии IBM 370 [Felton 84] и UNIVAC 1100 [Bodenstab 84]. обеспечивая обслуживание программ и их независимость от деталей аппаратной конфигурации. Если представить систему состоящей из пластов, в ней можно выделить системное ядро, изолированное от пользовательских здддддддддбдддддддддддддддддддддддддддддддддд© Ё Ё Другие прикладные программы Ё Ё зддддедддддддбддддддддбдддддддбдддд© Ё Ё Ё cppЁ nroff Ё sh Ё who Ё Ё Ё Ё Ё цдддддддаддддддддаддддддд╢ a. Ё Ё Ё цдддд╢ Ядро Ёout Ё Ё Ё Ё Ё здддддддддддддд© Ё Ё Ё Ё ЁcompЁ Ё Ё цдддд╢ Ё Ё Ё Ё Ё Технические Ё Ё Ё Ё Ё cc цдддд╢ Ё Ё ЁdateЁ Ё Ё Ё Ё Ё средства Ё Ё Ё Ё Ё Ё as Ё Ё Ё цдддд╢ Ё Ё Ё Ё юдддддддддддддды Ё Ё Ё Ё цдддд╢ Ё Ё Ё Ё Ё цдддддддбддддддддбддддддд╢ wc Ё Ё Ё Ё ld Ё vi Ё ed Ё grep Ё Ё Ё Ё юддддедддддддаддддддддадддддддадддды Ё Ё Ё Другие прикладные программы Ё юдддддддддадддддддддддддддддддддддддддддддддды Рисунок 1.1. Архитектура системы UNIX программ. Поскольку программы не зависят от аппаратуры, их легко переносить из одной системы UNIX в другую, функционирующую на другом комплексе технических средств, если только в этих програм- мах не подразумевается работа с конкретным оборудованием. Напри- мер, программы, расчитанные на определенный размер машинного сло- ва, гораздо труднее переводить на другие машины по сравнению с программами, не требующими подобных установлений. Программы, подобные командному процессору shell и редакторам (ed и vi) и показанные на внешнем по отношению к ядру слое, взаи- модействуют с ядром при помощи хорошо определенного набора обра- щений к операционной системе. Обращения к операционной системе понуждают ядро к выполнению различных операций, которых требует вызывающая программа, и обеспечивают обмен данными между ядром и программой. Некоторые из программ, приведенных на рисунке, в стандартных конфигурациях системы известны как команды, однако на одном уровне с ними могут располагаться и доступные пользователю программы, такие как программа a.out, стандартное имя для испол- няемого файла, созданного компилятором с языка Си. Другие прик- ладные программы располагаются выше указанных программ, на верх- нем уровне, как это показано на рисунке. Например, стандартный компилятор с языка Си, cc, располагается на самом внешнем слое: он вызывает препроцессор для Си, ассемблер и загрузчик (компонов- щик), т.е. отдельные программы предыдущего уровня. Хотя на рисун- ке приведена двухуровневая иерархия прикладных программ, пользо- ватель может расширить иерархическую структуру на столько уровней, сколько необходимо. В самом деле, стиль программирова- ния, принятый в системе UNIX, допускает разработку комбинации программ, выполняющих одну и ту же, общую задачу. Многие прикладные подсистемы и программы, составляющие верх- ний уровень системы, такие как командный процессор shell, редак- торы, SCCS (система обработки исходных текстов программ) и пакеты программ подготовки документации, постепенно становятся синонимом понятия "система UNIX". Однако все они пользуются услугами прог- рамм нижних уровней и в конечном счете ядра с помощью набора об- ращений к операционной системе. В версии V принято 64 типа обра- щений к операционной системе, из которых немногим меньше половины используются часто. Они имеют несложные параметры, что облегчает их использование, предоставляя при этом большие возможности поль- зователю. Набор обращений к операционной системе вместе с реали- зующими их внутренними алгоритмами составляют "тело" ядра, в свя- зи с чем рассмотрение операционной системы UNIX в этой книге сво- дится к подробному изучению и анализу обращений к системе и их взаимодействия между собой. Короче говоря, ядро реализует функ- ции, на которых основывается выполнение всех прикладных программ в системе UNIX, и им же определяются эти функции. В книге часто употребляются термины "система UNIX", "ядро" или "система", одна- ко при этом имеется ввиду ядро операционной системы UNIX, что и должно вытекать из контекста. 1.3 ОБЗОР С ТОЧКИ ЗРЕНИЯ ПОЛЬЗОВАТЕЛЯ В этом разделе кратко рассматриваются главные детали системы UNIX, в частности файловая система, среда выполнения процессов и элементы структурных блоков (например, каналы). Подробное иссле- дование взаимодействия этих деталей с ядром содержится в последу- ющих главах. 1.3.1 Файловая система Файловая система UNIX характеризуется: * иерархической структурой, * согласованной обработкой массивов данных, * возможностью создания и удаления файлов, * динамическим расширением файлов, * защитой информации в файлах, * трактовкой периферийных устройств (таких как терминалы и ленточные устройства) как файлов. Файловая система организована в виде дерева с одной исходной вершиной, которая называется корнем (записывается: "/"); каждая вершина в древовидной структуре файловой системы, кроме листьев, является каталогом файлов, а файлы, соответствующие дочерним вер- шинам, являются либо каталогами, либо обычными файлами, либо фай- лами устройств. Имени файла предшествует указание пути поиска, который описывает место расположения файла в иерархической струк- туре файловой системы. Имя пути поиска состоит из компонент, раз- деленных между собой наклонной чертой (/); каждая компонента / здддддддддддддбддддддддддддедддддддддбдддддддддбддддддддд© Ё Ё Ё Ё Ё Ё fsl bin etc usr unix dev здад© здддеддд© Ё здад© здад© Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё mjb maury sh date who passwd src bin tty00 tty01 Ё Ё cmd здддаддд© Ё Ё date.c who.c Рисунок 1.2. Пример древовидной структуры файловой системы представляет собой набор символов, составляющих имя вершины (фай- ла), которое является уникальным для каталога (предыдущей компо- ненты), в котором оно содержится. Полное имя пути поиска начина- ется с указания наклонной черты и идентифицирует файл (вершину), поиск которого ведется от корневой вершины дерева файловой систе- мы с обходом тех ветвей дерева файлов, которые соответствуют име- нам отдельных компонент. Так, пути "/etc/passwd", "/bin/who" и "/usr/src/cmd/who.c" указывают на файлы, являющиеся вершинами де- рева, изображенного на Рисунке 1.2, а пути "/bin/passwd" и "/usr/ src/date.c" содержат неверный маршрут. Имя пути поиска необяза- тельно должно начинаться с корня, в нем следует указывать маршрут относительно текущего для выполняемого процесса каталога, при этом предыдущие символы "наклонная черта" в имени пути опускают- ся. Так, например, если мы находимся в каталоге "/dev", то путь "tty01" указывает файл, полное имя пути поиска для которого "/dev /tty01". Программы, выполняемые под управлением системы UNIX, не со- держат никакой информации относительно внутреннего формата, в ко- тором ядро хранит файлы данных, данные в программах представляются как бесформатный поток байтов. Программы могут ин- терпретировать поток байтов по своему желанию, при этом любая ин- терпретация никак не будет связана с фактическим способом хране- ния данных в операционной системе. Так, синтаксические правила, определяющие задание метода доступа к данным в файле, устанавли- ваются системой и являются едиными для всех программ, однако се- мантика данных определяется конкретной программой. Например, программа форматирования текста troff ищет в конце каждой строки текста символы перехода на новую строку, а программа учета сис- темных ресурсов acctcom работает с записями фиксированной длины. Обе программы пользуются одними и теми же системными средствами для осуществления доступа к данным в файле как к потоку байтов, и внутри себя преобразуют этот поток по соответствующему формату. Если любая из программ обнаружит, что формат данных неверен, она принимает соответствующие меры. Каталоги похожи на обычные файлы в одном отношении; система представляет информацию в каталоге набором байтов, но эта инфор- мация включает в себя имена файлов в каталоге в объявленном фор- мате для того, чтобы операционная система и программы, такие как ls (выводит список имен и атрибутов файлов), могли их обнаружить. Права доступа к файлу регулируются установкой специальных би- тов разрешения доступа, связанных с файлом. Устанавливая биты разрешения доступа, можно независимо управлять выдачей разрешений на чтение, запись и выполнение для трех категорий пользователей: владельца файла, группового пользователя и прочих. Пользователи могут создавать файлы, если разрешен доступ к каталогу. Вновь созданные файлы становятся листьями в древовидной структуре фай- ловой системы. Для пользователя система UNIX трактует устройства так, как если бы они были файлами. Устройства, для которых назначены спе- циальные файлы устройств, становятся вершинами в структуре файло- вой системы. Обращение программ к устройствам имеет тот же самый синтаксис, что и обращение к обычным файлам; семантика операций чтения и записи по отношению к устройствам в большой степени сов- падает с семантикой операций чтения и записи обычных файлов. Спо- соб защиты устройств совпадает со способом защиты обычных файлов: путем соответствующей установки битов разрешения доступа к ним (файлам). Поскольку имена устройств выглядят так же, как и имена обычных файлов, и поскольку над устройствами и над обычными фай- лами выполняются одни и те же операции, большинству программ нет необходимости различать внутри себя типы обрабатываемых файлов. Например, рассмотрим программу на языке Си (Рисунок 1.3), в которой создается новая копия существующего файла. Предположим, что исполняемая версия программы имеет наименование copy. Для за- пуска программы пользователь вводит с терминала: copy oldfile newfile где oldfile - имя существующего файла, а newfile - имя создавае- мого файла. Система выполняет процедуру main, присваивая аргумен- ту argc значение количества параметров в списке argv, а каждому элементу массива argv значение параметра, сообщенного пользовате- лем. В приведенном примере argc имеет значение 3, элемент argv[0] содержит строку символов "copy" (имя программы условно является нулевым параметром), argv[1] - строку символов "oldfile", а argv[2] - строку символов "newfile". Затем программа проверяет, правильное ли количество параметров было указано при ее запуске. Если это так, запускается операция open (открыть) для файла oldfile с параметром "read-only" (только для чтения), в случае успешного выполнения которой запускается операция creat (открыть) для файла newfile. Режим доступа к вновь созданному файлу описы- вается числом 0666 (в восьмиричном коде), что означает разрешение доступа к файлу для чтения и записи для всех пользователей. Все обращения к операционной системе в случае неудачи возвращают код -1; если же неудачно завершаются операции open и creat, программа выдает сообщение и запускает операцию exit (выйти) с возвращением кода состояния, равного 1, завершая свою работу и указывая на возникновение ошибки. Операции open и creat возвращают целое значение, являющееся дескриптором файла и используемое программой в последующих ссыл- ках на файлы. После этого программа вызывает подпрограмму copy, выполняющую в цикле операцию read (читать), по которой произво- дится чтение в буфер порции символов из существующего файла, и операцию write (писать) для записи информации в новый файл. Опе- рация read каждый раз возвращает количество прочитанных байтов (0 - если достигнут конец файла). Цикл завершается, если достигнут конец файла или если произошла ошибка при выполнении операции read (отсутствует контроль возникновения ошибок при выполнении операции write). Затем управление из подпрограммы copy возвраща- ется в основную программу и запускается операция exit с кодом состояния 0 в качестве параметра, что указывает на успешное за- вершение выполнения программы. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё char buffer[2048]; Ё Ё int version = 1; /* будет объяснено в главе 2 */ Ё Ё Ё Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё int fdold,fdnew; Ё Ё Ё Ё if (argc != 3) Ё Ё { Ё Ё printf("need 2 arguments for copy program\n); Ё Ё exit(1); Ё Ё } Ё Ё fdold = open(argv[1],O_RDONLY); /* открыть исходный Ё Ё файл только для Ё Ё чтения */ Ё Ё if (fdold == -1) Ё Ё { Ё Ё printf("cannot open file %s\n",argv[1]); Ё Ё exit(1); Ё Ё } Ё Ё fdnew = creat(argv[2],0666); /* создать новый файл с Ё Ё разрешением чтения и Ё Ё записи для всех поль-Ё Ё зователей */ Ё Ё if (fdnew == -1) Ё Ё { Ё Ё printf("cannot create file %s\n",argv[2]); Ё Ё exit(1); Ё Ё } Ё Ё copy(fdold,fdnew); Ё Ё exit(0); Ё Ё } Ё Ё Ё Ё copy(old,new) Ё Ё int old,new; Ё Ё { Ё Ё int count; Ё Ё Ё Ё while ((count = read(old,buffer,sizeof(buffer))) > 0) Ё Ё write(new,buffer,count); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 1.3. Программа копирования файла Программа копирует любые файлы, указанные при ее вызове в ка- честве аргументов, при условии, что разрешено открытие существую- щего файла и создание нового файла. Файл может включать в себя как текст, который может быть выведен на печатающее устройство, например, исходный текст программы, так и символы, не выводимые на печать, даже саму программу. Таким образом, оба вызова: copy copy.c newcopy.c copy copy newcopy являются допустимыми. Существующий файл также может быть катало- гом. Например, по вызову: copy . dircontents копируется содержимое текущего каталога, обозначенного символом ".", в обычный файл "dircontents"; информация в новом файле сов- падает, вплоть до каждого байта, с содержимым каталога, только этот файл обычного типа (для создания нового каталога предназна- чена операция mknod). Наконец, любой из файлов может быть файлом устройства. Например, программа, вызванная следующим образом: copy /dev/tty terminalread читает символы, вводимые с терминала (файл /dev/tty соответствует терминалу пользователя), и копирует их в файл terminalread, за- вершая работу только в том случае, если пользователь нажмет . Похожая форма запуска программы: copy /dev/tty /dev/tty вызывает чтение символов с терминала и их копирование обратно на терминал. 1.3.2 Среда выполнения процессов Программой называется исполняемый файл, а процессом называет- ся последовательность операций программы или часть программы при ее выполнении. В системе UNIX может одновременно выполняться мно- жество процессов (эту особенность иногда называют мультипрограм- мированием или многозадачным режимом), при чем их число логически не ограничивается, и множество частей программы (такой как copy) может одновременно находиться в системе. Различные системные опе- рации позволяют процессам порождать новые процессы, завершают процессы, синхронизируют выполнение этапов процесса и управляют реакцией на наступление различных событий. Благодаря различным обращениям к операционной системе, процессы выполняются независи- мо друг от друга. Например, процесс, выполняющийся в программе, приведенной на Рисунке 1.4, запускает операцию fork, чтобы породить новый про- цесс. Новый процесс, именуемый порожденным процессом, получает значение кода завершения операции fork, равное 0, и активизирует операцию execl, которая выполняет программу copy (Рисунок 1.3). Операция execl загружает файл "copy", который предположительно находится в текущем каталоге, в адресное пространство порожденно- го процесса и запускает программу с параметрами, полученными от пользователя. В случае успешного выполнения операции execl управ- ление в вызвавший ее процесс не возвращается, поскольку процесс выполняется в новом адресном пространстве (подробнее об этом в главе 7). Тем временем, процесс, запустивший операцию fork (роди- тельский процесс), получает ненулевое значение кода завершения операции, вызывает операцию wait, которая приостанавливает его выполнение до тех пор, пока не закончится выполнение программы copy, и завершается (каждая программа имеет выход в конце главной процедуры, после которой располагаются программы стандартных биб- лиотек Си, подключаемые в процессе компиляции). Например, если исполняемая программа называется run, пользователь запускает ее следующим образом: run oldfile newfile здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё /* предусмотрено 2 аргумента: исходный файл и новый файл */Ё Ё if (fork() == 0) Ё Ё execl("copy","copy",argv[1],argv[2],0); Ё Ё wait((int *)0) Ё Ё printf("copy done\n"); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 1.4. Программа порождения нового процесса, выполняю- щего копирование файлов Процесс выполняет копирование файла с именем "oldfile" в файл с именем "newfile" и выводит сообщение. Хотя данная программа мало что добавила к программе "copy", в ней появились четыре основных обращения к операционной системе, управляющие выполнением процес- сов: fork, exec, wait и exit. Вообще использование обращений к операционной системе дает возможность пользователю создавать программы, выполняющие сложные действия, и как следствие, ядро операционной системы UNIX не включает в себя многие функции, являющиеся частью "ядра" в других системах. Такие функции, и среди них компиляторы и редакторы, в системе UNIX являются программами пользовательского уровня. Наи- более характерным примером подобной программы может служить ко- мандный процессор shell, с которым обычно взаимодействуют пользо- ватели после входа в систему. Shell интерпретирует первое слово командной строки как имя команды: во многих командах, в том числе и в командах fork (породить новый процесс) и exec (выполнить по- рожденный процесс), сама команда ассоциируется с ее именем, все остальные слова в командной строке трактуются как параметры ко- манды. Shell обрабатывает команды трех типов. Во-первых, в качестве имени команды может быть указано имя исполняемого файла в объект- ном коде, полученного в результате компиляции исходного текста программы (например, программы на языке Си). Во-вторых, именем команды может быть имя командного файла, содержащего набор ко- мандных строк, обрабатываемых shell'ом. Наконец, команда может быть внутренней командой языка shell (в отличие от исполняемого файла). Наличие внутренних команд делает shell языком программи- рования в дополнение к функциям командного процессора; командный язык shell включает команды организации циклов (for-in-do-done и while-do-done), команды выполнения по условиям (if-then-else-fi), оператор выбора, команду изменения текущего для процесса каталога (cd) и некоторые другие. Синтаксис shell'а допускает сравнение с образцом и обработку параметров. Пользователям, запускающим ко- манды, нет необходимости знать, какого типа эти команды. Командный процессор shell ищет имена команд в указанном набо- ре каталогов, который можно изменить по желанию пользователя, вызвав shell. Shell обычно исполняет команду синхронно, с ожида- нием завершения выполнения команды прежде, чем считать следующую командную строку. Тем не менее, допускается и асинхронное испол- нение, когда очередная командная строка считывается и исполняет- ся, не дожидаясь завершения выполнения предыдущей команды. О ко- мандах, выполняемых асинхронно, говорят, что они выполняются на фоне других команд. Например, ввод команды who вызывает выполнение системой программы, хранящейся в файле /bin/who (****) и осуществляющей вывод списка пользователей, ко- торые в настоящий момент работают с системой. Пока команда who выполняется, командный процессор shell ожидает завершения ее вы- полнения и только затем запрашивает у пользователя следующую ко- манду. Если же ввести команду who & система выполнит программу who на фоне и shell готов немедленно принять следующую команду. В среду выполнения каждого процесса в системе UNIX включается текущий каталог. Текущий для процесса каталог является начальным каталогом, имя которого присоединяется ко всем именам путей поис- ка, которые не начинаются с наклонной черты. Пользователь может запустить внутреннюю команду shell'а cd (изменить каталог) для перемещения по дереву файловой системы и для смены текущего ката- лога. Командная строка cd /usr/src/uts делает текущим каталог "/usr/src/uts". Командная строка cd ../.. делает текущим каталог, который на две вершины "ближе" к корню (корневому каталогу): параметр ".." относится к каталогу, являю- щемуся родительским для текущего. Поскольку shell является пользовательской программой и не входит в состав ядра операционной системы, его легко модифициро- вать и помещать в конкретные условия эксплуатации. Например, вместо командного процессора Баурна (называемого так по имени его создателя, Стива Баурна), являющегося частью версии V стандартной системы, можно использовать процессор команд Си, обеспечивающий работу механизма ведения истории изменений и позволяющий избегать повторного ввода только что использованных команд. В некоторых случаях при желании можно воспользоваться командным процессором shell с ограниченными возможностями, являющимся предыдущей верси- ей обычного shell'а. Система может работать с несколькими команд- ными процессорами одновременно. Пользователи имеют возможность запускать одновременно множество процессов, процессы же в свою очередь могут динамически порождать новые процессы и синхронизи- ровать их выполнение. Все эти возможности обеспечиваются благода- ря наличию мощных программных и аппаратных средств, составляющих среду выполнения процессов. Хотя привлекательность shell'а в наи- большей степени определяется его возможностями как языка програм- мирования и его возможностями в обработке аргументов, в данном разделе основное внимание концентрируется на среде выполнения процессов, управление которой в системе возложено на командный процессор shell. Другие важные особенности shell'а выходят за ддддддддддддддддддддддддддд (****) Каталог "/bin" содержит большинство необходимых команд и обычно входит в число каталогов, в которых ведет поиск ко- мандный процессор shell. рамки настоящей книги (подробное описание shell'а см. в [Bourne 78]). 1.3.3 Элементы конструкционных блоков Как уже говорилось ранее, концепция разработки системы UNIX заключалась в построении операционной системы из элементов, кото- рые позволили бы пользователю создавать небольшие программные мо- дули, выступающие в качестве конструкционных блоков при создании более сложных программ. Одним из таких элементов, с которым часто сталкиваются пользователи при работе с командным процессором shell, является возможность переназначения ввода-вывода. Говоря условно, процессы имеют доступ к трем файлам: они читают из файла стандартного ввода, записывают в файл стандартного вывода и выво- дят сообщения об ошибках в стандартный файл ошибок. Процессы, за- пускаемые с терминала, обычно используют терминал вместо всех этих трех файлов, однако каждый файл независимо от других может быть "переназначен". Например, команда ls выводит список всех файлов текущего каталога на устройство (в файл) стандартного вывода, а команда ls > output переназначает выводной поток со стандартного вывода в файл "output" в текущем каталоге, используя вышеупомянутый системный вызов creat. Подобным же образом, команда mail mjb < letter открывает (с помощью системного вызова open) файл "letter" в ка- честве файла стандартного ввода и пересылает его содержимое поль- зователю с именем "mjb". Процессы могут переназначать одновре- менно и ввод, и вывод, как, например, в командной строке: nroff -mm < doc1 > doc1.out 2> errors где программа форматирования nroff читает вводной файл doc1, в качестве файла стандартного вывода задает файл doc1.out и выво- дит сообщения об ошибках в файл errors ("2>" означает переназна- чение вывода, предназначавшегося для файла с дескриптором 2, ко- торый соответствует стандартному файлу ошибок). Программы ls, mail и nroff не знают, какие файлы выбраны в качестве файлов стандартного ввода, стандартного вывода и записи сообщений об ошибках; командный процессор shell сам распознает символы "<", ">" и "2>" и назначает в соответствии с их указанием файлы для стандартного ввода, стандартного вывода и записи сообщений об ошибках непосредственно перед запуском процессов. Вторым конструкционным элементом является канал, механизм, обеспечивающий информационный обмен между процессами, выполнение которых связано с операциями чтения и записи. Процессы могут пе- реназначать выводной поток со стандартного вывода на канал для чтения с него другими процессами, переназначившими на канал свой стандартный ввод. Данные, посылаемые в канал первыми процессами, являются входными для вторых процессов. Вторые процессы так же могут переназначить свой выводной поток и так далее, в зависимос- ти от пожеланий программиста. И снова, так же как и в вышеуказан- ном случае, процессам нет необходимости знать, какого типа файл используется в качестве файла стандартного вывода; их выполнение не зависит от того, будет ли файлом стандартного вывода обычный файл, канал или устройство. В процессе построения больших и слож- ных программ из конструкционных элементов меньшего размера прог- раммисты часто используют каналы и переназначение ввода-вывода при сборке и соединении отдельных частей. И действительно, такой стиль программирования находит поддержку в системе, благодаря че- му новые программы могут работать вместе с существующими програм- мами. Например, программа grep производит поиск контекста в наборе файлов (являющихся параметрами программы) по следующему образцу: grep main a.c b.c c.c где "main" - подстрока, поиск которой производится в файлах a.c, b.c и c.c с выдачей в файл стандартного вывода тех строк, в кото- рых она содержится. Содержимое выводного файла может быть следую- щим: a.c: main(argc,argv) c.c: /* here is the main loop in the program */ c.c: main() Программа wc с необязательным параметром -l подсчитывает число строк в файле стандартного ввода. Командная строка grep main a.c b.c c.c Ё wc -l вызовет подсчет числа строк в указанных файлах, где будет обнару- жена подстрока "main"; выводной поток команды grep поступит не- посредственно на вход команды wc. Для предыдущего примера результат будет такой: 3 Использование каналов зачастую делает ненужным создание временных файлов. 1.4 ФУНКЦИИ ОПЕРАЦИОННОЙ СИСТЕМЫ На Рисунке 1.1 уровень ядра операционной системы изображен непосредственно под уровнем прикладных программ пользователя. Вы- полняя различные элементарные операции по запросам пользователь- ских процессов, ядро обеспечивает функционирование пользователь- ского интерфейса, описанного выше. Среди функций ядра можно отметить: * Управление выполнением процессов посредством их создания, за- вершения или приостановки и организации взаимодействия между ними. * Планирование очередности предоставления выполняющимся процессам времени центрального процессора (диспетчеризация). Процессы ра- ботают с центральным процессором в режиме разделения времени: центральный процессор (*****) выполняет процесс, по завершении отсчитываемого ядром кванта времени процесс приостанавливается и ядро активизирует выполнение другого процесса. Позднее ядро запускает приостановленный процесс. * Выделение выполняемому процессу оперативной памяти. Ядро опера- ционной системы дает процессам возможность совместно использо- вать участки адресного пространства на определенных условиях, защищая при этом адресное пространство, выделенное процессу, от вмешательства извне. Если системе требуется свободная память, ядро освобождает память, временно выгружая процесс на внешние дддддддддддддддддддддддддд (*****) В главе 12 рассматриваются многопроцессорные системы; до того речь будет идти об однопроцессорной модели. запоминающие устройства, которые называют устройствами выгруз- ки. Если ядро выгружает процессы на устройства выгрузки цели- ком, такая реализация системы UNIX называется системой со сво- пингом (подкачкой); если же на устройство выгрузки выводятся страницы памяти, такая система называется системой с замещением страниц. * Выделение внешней памяти с целью обеспечения эффективного хра- нения информации и выборка данных пользователя. Именно в про- цессе реализации этой функции создается файловая система. Ядро выделяет внешнюю память под пользовательские файлы, мобилизует неиспользуемую память, структурирует файловую систему в форме, доступной для понимания, и защищает пользовательские файлы от несанкционированного доступа. * Управление доступом процессов к периферийным устройствам, таким как терминалы, ленточные устройства, дисководы и сетевое обору- дование. Выполнение ядром своих функций довольно очевидно. Например, оно узнает, что данный файл является обычным файлом или устройс- твом, но скрывает это различие от пользовательских процессов. Так же оно, форматируя информацию файла для внутреннего хранения, за- щищает внутренний формат от пользовательских процессов, возвращая им неотформатированный поток байтов. Наконец, ядро реализует ряд необходимых функций по обеспечению выполнения процессов пользова- тельского уровня, за исключением функций, которые могут быть реализованы на самом пользовательском уровне. Например, ядро вы- полняет действия, необходимые shell'у как интерпретатору команд: оно позволяет процессору shell читать вводимые с терминала дан- ные, динамически порождать процессы, синхронизировать выполнение процессов, открывать каналы и переадресовывать ввод-вывод. Поль- зователи могут разрабатывать свои версии командного процессора shell с тем, чтобы привести рабочую среду в соответствие со свои- ми требованиями, не затрагивая других пользователей. Такие прог- раммы пользуются теми же услугами ядра, что и стандартный процессор shell. 1.5 ПРЕДПОЛАГАЕМАЯ АППАРАТНАЯ СРЕДА Выполнение пользовательских процессов в системе UNIX осущест- вляется на двух уровнях: уровне пользователя и уровне ядра. Когда процесс производит обращение к операционной системе, режим выпол- нения процесса переключается с режима задачи (пользовательского) на режим ядра: операционная система пытается обслужить запрос пользователя, возвращая код ошибки в случае неудачного завершения операции. Даже если пользователь не нуждается в каких-либо опре- деленных услугах операционной системы и не обращается к ней с запросами, система еще выполняет учетные операции, связанные с пользовательским процессом, обрабатывает прерывания, планирует процессы, управляет распределением памяти и т.д. Большинство вы- числительных систем разнообразной архитектуры (и соответствующие им операционные системы) поддерживают большее число уровней, чем указано здесь, однако уже двух режимов, режима задачи и режима ядра, вполне достаточно для системы UNIX. Основные различия между этими двумя режимами: * В режиме задачи процессы имеют доступ только к своим собствен- ным инструкциям и данным, но не к инструкциям и данным ядра (либо других процессов). Однако в режиме ядра процессам уже доступны адресные пространства ядра и пользователей. Например, виртуальное адресное пространство процесса может быть поделено на адреса, доступные только в режиме ядра, и на адреса, доступ- ные в любом режиме. * Некоторые машинные команды являются привилегированными и вызы- вают возникновение ошибок при попытке их использования в режиме задачи. Например, в машинном языке может быть команда, управля- ющая регистром состояния процессора; процессам, выполняющимся в Процессы A B C D здддддддбдддддддбдддддддбддддддд© Режим ядра Ё Я Ё Ё Ё Я Ё цдддддддедддддддедддддддеддддддд╢ Режим задачи Ё Ё З Ё З Ё Ё юдддддддадддддддадддддддаддддддды Рисунок 1.5. Процессы и режимы их выполнения режиме задачи, она недоступна. Проще говоря, любое взаимодействие с аппаратурой описывается в терминах режима ядра и режима задачи и протекает одинаково для всех пользовательских программ, выполняющихся в этих режимах. Операционная система хранит внутренние записи о каждом процессе, выполняющемся в системе. На Рисунке 1.5 показано это разделение: ядро делит процессы A, B, C и D, расположенные вдоль горизонталь- ной оси, аппаратные средства вводят различия между режимами вы- полнения, расположенными по вертикали. Несмотря на то, что система функционирует в одном из двух ре- жимов, ядро действует от имени пользовательского процесса. Ядро не является какой-то особой совокупностью процессов, выполняющих- ся параллельно с пользовательскими, оно само выступает составной частью любого пользовательского процесса. Сделанный вывод будет скорее относиться к "ядру", распределяющему ресурсы, или к "яд- ру", производящему различные операции, и это будет означать, что процесс, выполняемый в режиме ядра, распределяет ресурсы и произ- водит соответствующие операции. Например, командный процессор shell считывает вводной поток с терминала с помощью запроса к операционной системе. Ядро операционной системы, выступая от име- ни процессора shell, управляет функционированием терминала и пе- редает вводимые символы процессору shell. Shell переходит в режим задачи, анализирует поток символов, введенных пользователем и вы- полняет заданную последовательность действий, которые могут пот- ребовать выполнения и других системных операций. 1.5.1 Прерывания и особые ситуации Система UNIX позволяет таким устройства, как внешние устройства ввода-вывода и системные часы, асинхронно прерывать работу центрального процессора. По получении сигнала прерывания ядро операционной системы сохраняет свой текущий контекст (зас- тывший образ выполняемого процесса), устанавливает причину преры- вания и обрабатывает прерывание. После того, как прерывание будет обработано ядром, прерванный контекст восстановится и работа про- должится так, как будто ничего не случилось. Устройствам обычно приписываются приоритеты в соответствии с очередностью обработки прерываний. В процессе обработки прерываний ядро учитывает их приоритеты и блокирует обслуживание прерывания с низким приорите- том на время обработки прерывания с более высоким приоритетом. Особые ситуации связаны с возникновением незапланированных событий, вызванных процессом, таких как недопустимая адресация, задание привилегированных команд, деление на ноль и т.д. Они от- личаются от прерываний, которые вызываются событиями, внешними по отношению к процессу. Особые ситуации возникают прямо "посреди- не" выполнения команды, и система, обработав особую ситуацию, пы- тается перезапустить команду; считается, что прерывания возникают между выполнением двух команд, при этом система после обработки прерывания продолжает выполнение процесса уже начиная со следую- щей команды. Для обработки прерываний и особых ситуаций в системе UNIX используется один и тот же механизм. 1.5.2 Уровни прерывания процессора Ядро иногда обязано предупреждать возникновение прерываний во время критических действий, могущих в случае прерывания запортить информацию. Например, во время обработки списка с указателями возникновение прерывания от диска для ядра нежелательно, т.к. при обработке прерывания можно запортить указатели, что можно увидеть на примере в следующей главе. Обычно имеется ряд привилегирован- ных команд, устанавливающих уровень прерывания процессора в слове состояния процессора. Установка уровня прерывания на определенное значение отсекает прерывания этого и более низких уровней, разре- шая обработку только прерываний с более высоким приоритетом. На Рисунке 1.6 показана последовательность уровней прерывания. Если ядро игнорирует прерывания от диска, в этом случае игнорируются и все остальные прерывания, кроме прерываний от часов и машинных сбоев. здддддддддддддддддддддддддддддд© ^ Ё Машинные сбои Ё Ё цдддддддддддддддддддддддддддддд╢ Ё Ё Системные часы Ё Высокий приоритет цдддддддддддддддддддддддддддддд╢ Ё Ё Диск Ё Ё цдддддддддддддддддддддддддддддд╢ Ё Ё Сетевое оборудование Ё Ё цдддддддддддддддддддддддддддддд╢ Ё Ё Терминалы Ё Низкий приоритет цдддддддддддддддддддддддддддддд╢ Ё Ё Программные прерывания Ё Ё юдддддддддддддддддддддддддддддды v Рисунок 1.6. Стандартные уровни прерываний 1.5.3 Распределение памяти Ядро постоянно располагается в оперативной памяти, наряду с выполняющимся в данный момент процессом (или частью его, по мень- шей мере). В процессе компиляции программа-компилятор генерирует последовательность адресов, являющихся адресами переменных и ин- формационных структур, а также адресами инструкций и функций. Компилятор генерирует адреса для виртуальной машины так, словно на физической машине не будет выполняться параллельно с трансли- руемой ни одна другая программа. Когда программа запускается на выполнение, ядро выделяет для нее место в оперативной памяти, при этом совпадение виртуальных адресов, сгенерированных компилятором, с физическими адресами совсем необязательно. Ядро, взаимодействуя с аппаратными средс- твами, транслирует виртуальные адреса в физические, т.е. отобра- жает адреса, сгенерированные компилятором, в физические, машинные адреса. Такое отображение опирается на возможности аппаратных средств, поэтому компоненты системы UNIX, занимающиеся им, явля- ются машинно-зависимыми. Например, отдельные вычислительные маши- ны имеют специальное оборудование для подкачки выгруженных стра- ниц памяти. Главы 6 и 9 посвящены более подробному рассмотрению вопросов, связанных с распределением памяти, и исследованию их соотношения с аппаратными средствами. 1.6 ВЫВОДЫ В этой главе описаны полная структура системы UNIX, взаимоот- ношения между процессами, выполняющимися в режиме задачи и в режиме ядра, а также аппаратная среда функционирования ядра опе- рационной системы. Процессы выполняются в режиме задачи или в ре- жиме ядра, в котором они пользуются услугами системы благодаря наличию набора обращений к операционной системе. Архитектура сис- темы поддерживает такой стиль программирования, при котором из небольших программ, выполняющих только отдельные функции, но хо- рошо, составляются более сложные программы, использующие механизм каналов и переназначение ввода-вывода. Обращения к операционной системе позволяют процессам произво- дить операции, которые иначе не выполняются. В дополнение к обра- ботке подобных обращений ядро операционной системы осуществляет общие учетные операции, управляет планированием процессов, расп- ределением памяти и защитой процессов в оперативной памяти, обс- луживает прерывания, управляет файлами и устройствами и обрабаты- вает особые ситуации, возникающие в системе. В функции ядра системы UNIX намеренно не включены многие функции, являющиеся частью других операционных систем, поскольку набор обращений к системе позволяет процессам выполнять все необходимые операции на пользовательском уровне. В следующей главе содержится более де- тальная информация о ядре, описывающая его архитектуру и вводящая некоторые основные понятия, которые используются при описании его функционирования. ВВЕДЕНИЕ В АРХИТЕКТУРУ ЯДРА ОПЕРАЦИОННОЙ СИСТЕМЫ В предыдущей главе был сделан только поверхностный обзор осо- бенностей операционной среды UNIX. В этой главе основное внимание уделяется ядру операционной системы, делается обзор его архитек- туры и излагаются в общих чертах основные понятия и структуры, существенные для понимания всего последующего материала книги. 2.1 АРХИТЕКТУРА ОПЕРАЦИОННОЙ СИСТЕМЫ UNIX Как уже ранее было замечено (см. [Christian 83], стр.239), в системе UNIX создается иллюзия того, что файловая система имеет "места" и что у процессов есть "жизнь". Обе сущности, файлы и процессы, являются центральными понятиями модели операционной системы UNIX. На Рисунке 2.1 представлена блок-схема ядра систе- мы, отражающая состав модулей, из которых состоит ядро, и их вза- имосвязи друг с другом. В частности, на ней слева изображена фай- ловая подсистема, а справа подсистема управления процессами, две главные компоненты ядра. Эта схема дает логическое представление о ядре, хотя в действительности в структуре ядра имеются отклоне- ния от модели, поскольку отдельные модули испытывают внутреннее воздействие со стороны других модулей. Схема на Рисунке 2.1 имеет три уровня: уровень пользователя, уровень ядра и уровень аппаратуры. Обращения к операционной сис- теме и библиотеки составляют границу между пользовательскими программами и ядром, проведенную на Рисунке 1.1. Обращения к опе- рационной системе выглядят так же, как обычные вызовы функций в программах на языке Си, и библиотеки устанавливают соответствие между этими вызовами функций и элементарными системными операция- ми, о чем более подробно см. в главе 6. При этом программы на ас- семблере могут обращаться к операционной системе непосредственно, без использования библиотеки системных вызовов. Программы часто обращаются к другим библиотекам, таким как библиотека стандартных подпрограмм ввода-вывода, достигая тем самым более полного ис- пользования системных услуг. Для этого во время компиляции библи- отеки связываются с программами и частично включаются в программу пользователя. Далее мы проиллюстрируем эти моменты на примере. программы пользователя ^ Ё здддддддддддддддддддддд© точка пере- Ё Ё библиотеки Ё сечения ЫЫЫЫЫЫЫЫЫЁЫЫЫЫЫЫЫ юдддддддддддддддддддддды Ы Ё Ы ^ Уровень пользователя Ы Ё Ы Ё --------------------------Ё---------------------Ё----------------- Уровень ядра v v зддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё ^ обращения к операционной системе ^ Ё юддддддеддддддддддддддддддддддддддддддддддддеддддддды Ё Ё здддддддддддддддддеддддддддддддддд© зддддддддддддддддеддддддддд© Ё v Ё Ё v Ё Ё Ё Ё Ё Ё подсистема управле- Ё Ё ............Ё Ё ния файлами Ё Ё . взаимо- .Ё Ё <дддед© Ё . действие .Ё Ё Ё Ё Ё . процессов.Ё Ё ^ ^ Ё Ё Ё подсистема ............Ё Ё Ё Ё Ё Ё Ё ............Ё юдддддддеддддддддддддддедддддддддды Ё Ё . планиров-.Ё Ё v юде> управления . щик .Ё Ё здддддддддддддд© Ё ............Ё Ё Ё буфер сверх- Ё Ё ............Ё Ё Ё оперативной Ё Ё процессами . распреде-.Ё Ё Ё памяти (кеш) Ё Ё . ление .Ё Ё юдддддддддддддды Ё . памяти .Ё Ё ^ Ё ^ ............Ё Ё Ё Ё Ё Ё Ё v юдддддддедддддддддддддддддды здддддддедддддддддддддддддддддд© Ё Ё v . Ё Ё Ё символ . блок Ё Ё Ё . Ё Ё цдддддддддддддддддддддддддддддд╢ Ё Ё Ё Ё Ё драйверы устройств Ё Ё Ё ^ Ё Ё юддддддддддддддеддддддддддддддды Ё Ё Ё зддддддддддддддеддддддддддддддддддддддддддддддедддддддддддддддддд© Ё v аппаратный контроль v Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Уровень ядра ------------------------------------------------------------------ Уровень аппаратуры здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё технические средства (аппаратура) Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 2.1. Блок-схема ядра операционной системы На рисунке совокупность обращений к операционной системе раз- делена на те обращения, которые взаимодействуют с подсистемой уп- равления файлами, и те, которые взаимодействуют с подсистемой уп- равления процессами. Файловая подсистема управляет файлами, размещает записи файлов, управляет свободным пространством, дос- тупом к файлам и поиском данных для пользователей. Процессы взаи- модействуют с подсистемой управления файлами, используя при этом совокупность специальных обращений к операционной системе, таких как open ( для того, чтобы открыть файл на чтение или запись), close, read, write, stat (запросить атрибуты файла), chown (изме- нить запись с информацией о владельце файла) и chmod (изменить права доступа к файлу). Эти и другие операции рассматриваются в главе 5. Подсистема управления файлами обращается к данным, которые хранятся в файле, используя буферный механизм, управляющий пото- ком данных между ядром и устройствами внешней памяти. Буферный механизм, взаимодействуя с драйверами устройств ввода-вывода бло- ками, инициирует передачу данных к ядру и обратно. Драйверы уст- ройств являются такими модулями в составе ядра, которые управляют работой периферийных устройств. Устройства ввода-вывода блоками относятся к типу запоминающих устройств с произвольной выборкой; их драйверы построены таким образом, что все остальные компоненты системы воспринимают эти устройства как запоминающие устройства с произвольной выборкой. Например, драйвер запоминающего устройства на магнитной ленте позволяет ядру системы воспринимать это уст- ройство как запоминающее устройство с произвольной выборкой. Под- система управления файлами также непосредственно взаимодействует с драйверами устройств "неструктурированного" ввода-вывода, без вмешательства буферного механизма. К устройствам неструктуриро- ванного ввода-вывода, иногда именуемым устройствами посимвольного ввода-вывода (текстовыми), относятся устройства, отличные от уст- ройств ввода-вывода блоками. Подсистема управления процессами отвечает за синхронизацию процессов, взаимодействие процессов, распределение памяти и пла- нирование выполнения процессов. Подсистема управления файлами и подсистема управления процессами взаимодействуют между собой, когда файл загружается в память на выполнение (см. главу 7): под- система управления процессами читает в память исполняемые файлы перед тем, как их выполнить. Примерами обращений к операционной системе, используемых при управлении процессами, могут служить fork (создание нового про- цесса), exec (наложение образа программы на выполняемый процесс), exit (завершение выполнения процесса), wait (синхронизация про- должения выполнения основного процесса с моментом выхода из по- рожденного процесса), brk (управление размером памяти, выделенной процессу) и signal (управление реакцией процесса на возникновение экстраординарных событий). Глава 7 посвящена рассмотрению этих и других системных вызовов. Модуль распределения памяти контролирует выделение памяти процессам. Если в какой-то момент система испытывает недостаток в физической памяти для запуска всех процессов, ядро пересылает процессы между основной и внешней памятью с тем, чтобы все про- цессы имели возможность выполняться. В главе 9 описываются два способа управления распределением памяти: выгрузка (подкачка) и замещение страниц. Программу подкачки иногда называют планировщи- ком, т.к. она "планирует" выделение памяти процессам и оказывает влияние на работу планировщика центрального процессора. Однако в дальнейшем мы будем стараться ссылаться на нее как на "программу подкачки", чтобы избежать путаницы с планировщиком центрального процессора. Модуль "планировщик" распределяет между процессами время центрального процессора. Он планирует очередность выполнения про- цессов до тех пор, пока они добровольно не освободят центральный процессор, дождавшись выделения к.-л. ресурса, или до тех пор, пока ядро системы не выгрузит их после того, как их время выпол- нения превысит заранее определенный квант времени. Планировщик выбирает на выполнение готовый к запуску процесс с наивысшим при- оритетом; выполнение предыдущего процесса (приостановленного) бу- дет продолжено тогда, когда его приоритет будет наивысшим среди приоритетов всех готовых к запуску процессов. Существует несколь- ко форм взаимодействия процессов между собой, от асинхронного об- мена сигналами о событиях до синхронного обмена сообщениями. Наконец, аппаратный контроль отвечает за обработку прерываний и за связь с машиной. Такие устройства, как диски и терминалы, могут прерывать работу центрального процессора во время выполне- ния процесса. При этом ядро системы после обработки прерывания может возобновить выполнение прерванного процесса. Прерывания об- рабатываются не самими процессами, а специальными функциями ядра системы, перечисленными в контексте выполняемого процесса. 2.2 ВВЕДЕНИЕ В ОСНОВНЫЕ ПОНЯТИЯ СИСТЕМЫ В это разделе дается обзор некоторых основных информационных структур, используемых ядром системы, и более подробно описывает- ся функционирование модулей ядра, показанных на Рисунке 2.1. 2.2.1 Обзор особенностей подсистемы управления файлами Внутреннее представление файла описывается в индексе, который содержит описание размещения информации файла на диске и другую информацию, такую как владелец файла, права доступа к файлу и время доступа. Термин "индекс" (inode) широко используется в ли- тературе по системе UNIX. Каждый файл имеет один индекс, но может быть связан с несколькими именами, которые все отражаются в ин- дексе. Каждое имя является указателем. Когда процесс обращается к файлу по имени, ядро системы анализирует по очереди каждую компо- ненту имени файла, проверяя права процесса на просмотр входящих в путь поиска каталогов, и в конце концов возвращает индекс файла. Например, если процесс обращается к системе: open("/fs2/mjb/rje/sourcefile", 1); ядро системы возвращает индекс для файла "/fs2/mjb/rje/sourcefile". Если процесс создает новый файл, ядро присваивает этому файлу неиспользуемый индекс. Индексы хранятся в файловой системе (и это мы еще увидим), однако при обработке фай- лов ядро заносит их в таблицу индексов в оперативной памяти. Ядро поддерживает еще две информационные структуры, таблицу файлов и пользовательскую таблицу дескрипторов файла. Таблица файлов выступает глобальной структурой ядра, а пользовательская таблица дескрипторов файла выделяется под процесс. Если процесс открывает или создает файл, ядро выделяет в каждой таблице эле- мент, корреспондирующий с индексом файла. Элементы в этих трех структурах - в пользовательской таблице дескрипторов файла, в таблице файлов и в таблице индексов - хранят информацию о состоя- нии файла и о доступе пользователей к нему. В таблице файлов хра- нится смещение в байтах от начала файла до того места, откуда начнет выполняться следующая команда пользователя read или write, а также информация о правах доступа к открываемому процессу. Таб- лица дескрипторов файла идентифицирует все открытые для процесса файлы. На Рисунке 2.2 показаны эти таблицы и связи между ними. В системных операциях open (открыть) и creat (создать) ядро возв- ращает дескриптор файла, которому соответствует указатель в таб- лице дескрипторов файла. При выполнении операций read (читать) и write (писать) ядро использует дескриптор файла для входа в таб- лицу дескрипторов и, следуя указателям на таблицу файлов и на таблицу индексов, находит информацию в файле. Более подробно эти информационные структуры рассматриваются в главах 4 и 5. Сейчас достаточно сказать, что использование этих таблиц обеспечивает различную степень разделения доступа к файлу. Пользовательская таблица дескрип- Таблица Таблица торов файла файлов индексов зддддддддд© зддддд© зддддд© Ё - е - © Ё Ё Ё Ё цддддддддд╢ Ё Ё Ё Ё Ё - е © Ё Ё Ё Ё Ё цддддддддд╢ Ё Ё цддддд╢ Ё - е©Ё ю - - - ->цддддд╢ з - - - ->Ё Ё цддддддддд╢ ю - - - - ->Ё - е - ы цддддд╢ Ё ЁЁ цддддд╢ Ё Ё Ё Ёю - © Ё Ё Ё Ё Ё Ё Ё Ё цддддд╢ Ё Ё Ё цддддд╢ з - - - ->Ё Ё Ё Ё юд - - ->Ё - е - ы цддддд╢ Ё Ё цддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё юддддддддды юддддды юддддды Рисунок 2.2. Таблицы файлов, дескрипторов файла и индексов Обычные файлы и каталоги хранятся в системе UNIX на устройс- твах ввода-вывода блоками, таких как магнитные ленты или диски. Поскольку существует некоторое различие во времени доступа к этим устройствам, при установке системы UNIX на лентах размещают фай- ловые системы. С годами бездисковые автоматизированные рабочие места станут общим случаем, и файлы будут располагаться в удален- ной системе, доступ к которой будет осуществляться через сеть (см. главу 13). Для простоты, тем не менее, в последующем тексте подразумевается использование дисков. В системе может быть нес- колько физических дисков, на каждом из которых может размещаться одна и более файловых систем. Разбивка диска на несколько файло- вых систем облегчает администратору управление хранимыми данными. На логическом уровне ядро имеет дело с файловыми системами, а не с дисками, при этом каждая система трактуется как логическое уст- ройство, идентифицируемое номером. Преобразование адресов логи- ческого устройства (файловой системы) в адреса физического уст- ройства (диска) и обратно выполняется дисковым драйвером. Термин "устройство" в этой книге используется для обозначения логическо- го устройства, кроме специально оговоренных случаев. Файловая система состоит из последовательности логических блоков длиной 512, 1024, 2048 или другого числа байт, кратного 512, в зависимости от реализации системы. Размер логического бло- ка внутри одной файловой системы постоянен, но может варьировать- ся в разных файловых системах в данной конфигурации. Использова- ние логических блоков большого размера увеличивает скорость пере- дачи данных между диском и памятью, поскольку ядро сможет передать больше информации за одну дисковую операцию, и сокращает количество продолжительных операций. Например, чтение 1 Кбайта с диска за одну операцию осуществляется быстрее, чем чтение 512 байт за две. Однако, если размер логического блока слишком велик, полезный объем памяти может уменьшиться, это будет показано в главе 5. Для простоты термин "блок" в этой книге будет использо- ваться для обозначения логического блока, при этом подразумевает- ся логический блок размером 1 Кбайт, кроме специально оговоренных случаев. здддддддддбдддддддддбдддддддЫЫЫЫддддддддбдддддддЫЫЫЫдддддд© Ё Ё Ё Ё Ё юдддддддддадддддддддадддддддЫЫЫЫддддддддадддддддЫЫЫЫдддддды блок супер- список индексов информационные загрузки блок блоки Рисунок 2.3. Формат файловой системы Файловая система имеет следующую структуру (Рисунок 2.3). * Блок загрузки располагается в начале пространства, отведенного под файловую систему, обычно в первом секторе, и содержит прог- рамму начальной загрузки, которая считывается в машину при заг- рузке или инициализации операционной системы. Хотя для запуска системы требуется только один блок загрузки, каждая файловая система имеет свой (пусть даже пустой) блок загрузки. * Суперблок описывает состояние файловой системы - какого она размера, сколько файлов может в ней храниться, где располагает- ся свободное пространство, доступное для файловой системы, и другая информация. * Список индексов в файловой системе располагается вслед за су- перблоком. Администраторы указывают размер списка индексов при генерации файловой системы. Ядро операционной системы обращает- ся к индексам, используя указатели в списке индексов. Один из индексов является корневым индексом файловой системы: это ин- декс, по которому осуществляется доступ к структуре каталогов файловой системы после выполнения системной операции mount (монтировать) (раздел 5.14). * Информационные блоки располагаются сразу после списка индексов и содержат данные файлов и управляющие данные. Отдельно взятый информационный блок может принадлежать одному и только одному файлу в файловой системе. 2.2.2 Процессы В этом разделе мы рассмотрим более подробно подсистему управ- ления процессами. Даются разъяснения по поводу структуры процесса и некоторых информационных структур, используемых при распределе- нии памяти под процессы. Затем дается предварительный обзор диаг- раммы состояния процессов и затрагиваются различные вопросы, свя- занные с переходами из одного состояния в другое. Процессом называется последовательность операций при выполнении программы, которые представляют собой наборы байтов, интерпретируемые центральным процессором как машинные инструкции (т.н. "текст"), данные и стековые структуры. Создается впечатле- ние, что одновременно выполняется множество процессов, поскольку их выполнение планируется ядром, и, кроме того, несколько процес- сов могут быть экземплярами одной программы. Выполнение процесса заключается в точном следовании набору инструкций, который явля- ется замкнутым и не передает управление набору инструкций другого процесса; он считывает и записывает информацию в раздел данных и в стек, но ему недоступны данные и стеки других процессов. Одни процессы взаимодействуют с другими процессами и с остальным миром посредством обращений к операционной системе. С практической точки зрения процесс в системе UNIX является объектом, создаваемым в результате выполнения системной операции fork. Каждый процесс, за исключением нулевого, порождается в ре- зультате запуска другим процессом операции fork. Процесс, запус- тивший операцию fork, называется родительским, а вновь созданный процесс - порожденным. Каждый процесс имеет одного родителя, но может породить много процессов. Ядро системы идентифицирует каж- дый процесс по его номеру, который называется идентификатором процесса (PID). Нулевой процесс является особенным процессом, ко- торый создается "вручную" в результате загрузки системы; после порождения нового процесса (процесс 1) нулевой процесс становится процессом подкачки. Процесс 1, известный под именем init, являет- ся предком любого другого процесса в системе и связан с каждым процессом особым образом, описываемым в главе 7. Пользователь, транслируя исходный текст программы, создает исполняемый файл, который состоит из нескольких частей: * набора "заголовков", описывающих атрибуты файла, * текста программы, * представления на машинном языке данных, имеющих начальные значения при запуске программы на выполнение, и указания на то, сколько пространства памяти ядро системы выделит под неинациали- зированные данные, так называемые bss (*) (ядро устанавливает их в 0 в момент запуска), * других секций, таких как информация символических таблиц. Для программы, приведенной на Рисунке 1.3, текст исполняемого файла представляет собой сгенерированный код для функций main и copy, к определенным данным относится переменная version (встав- ленная в программу для того, чтобы в последней имелись некоторые определенные данные), а к неопределенным - массив buffer. Компи- лятор с языка Си для системы версии V создает отдельно текстовую секцию по умолчанию, но не исключается возможность включения инс- трукций программы и в секцию данных, как в предыдущих версиях системы. Ядро загружает исполняемый файл в память при выполнении сис- темной операции exec, при этом загруженный процесс состоит по меньшей мере из трех частей, так называемых областей: текста, данных и стека. Области текста и данных корреспондируют с секция- ми текста и bss-данных исполняемого файла, а область стека созда- ется автоматически и ее размер динамически устанавливается ядром системы во время выполнения. Стек состоит из логических записей активации, помещаемых в стек при вызове функции и выталкиваемых из стека при возврате управления в вызвавшую процедуру; специаль- ный регистр, именуемый указателем вершины стека, показывает теку- щую глубину стека. Запись активации включает параметры передавае- дддддддддддддддддддддддддддддддддддддддддддддддд (*) Сокращение bss имеет происхождение от ассемблерного псевдоо- ператора для машины IBM 7090 и расшифровывается как "block started by symbol" ("блок, начинающийся с символа"). мые функции, ее локальные переменные, а также данные, необходимые для восстановления предыдущей записи активации, в том числе зна- чения счетчика команд и указателя вершины стека в момент вызова функции. Текст программы включает последовательности команд, уп- равляющие увеличением стека, а ядро системы выделяет, если нужно, место под стек. В программе на Рисунке 1.3 параметры argc и argv, а также переменные fdold и fdnew, содержащиеся в вызове функции main, помещаются в стек, как только встретилось обращение к функ- ции main (один раз в каждой программе, по условию), так же и па- раметры old и new и переменная count, содержащиеся в вызове функ- ции copy, помещаются в стек в момент обращения к указанной функ- ции. Поскольку процесс в системе UNIX может выполняться в двух ре- жимах, режиме ядра или режиме задачи, он пользуется в каждом из этих режимов отдельным стеком. Стек задачи содержит аргументы, локальные переменные и другую информацию относительно функций, выполняемых в режиме задачи. Слева на Рисунке 2.4 показан стек задачи для процесса, связанного с выполнением системной операции write в программе copy. Процедура запуска процесса (включенная в библиотеку) обратилась к функции main с передачей ей двух пара- метров, поместив в стек задачи запись 1; в записи 1 есть место для двух локальных переменных функции main. Функция main затем вызывает функцию copy с передачей ей двух параметров, old и new, и помещает в стек задачи запись 2; в записи 2 есть место для ло- кальной переменной count. Наконец, процесс активизирует системную операцию write, вызвав библиотечную функцию с тем же именем. Каж- дой системной операции соответствует точка входа в библиотеке системных операций; библиотека системных операций написана на языке ассемблера и включает специальные команды прерывания, кото- рые, выполняясь, порождают "прерывание", вызывающее переключение аппаратуры в режим ядра. Процесс ищет в библиотеке точку входа, соответствующую отдельной системной операции, подобно тому, как он вызывает любую из функций, создавая при этом для библиотечной функции запись активации. Когда процесс выполняет специальную ин- струкцию, он переключается в режим ядра, выполняет операции ядра и использует стек ядра. Стек ядра содержит записи активации для функций, выполняющих- ся в режиме ядра. Элементы функций и данных в стеке ядра соот- ветствуют функциям и данным, относящимся к ядру, но не к програм- ме пользователя, тем не менее, конструкция стека ядра подобна конструкции стека задачи. Стек ядра для процесса пуст, если про- цесс выполняется в режиме задачи. Справа на Рисунке 2.4 представ- лен стек ядра для процесса выполнения системной операции write в программе copy. Подробно алгоритмы выполнения системной операции write будут описаны в последующих разделах. Стек задачи Направление Стек ядра здддддддддддддд© увеличения стека здддддддддддддддддд© Ё Локальные Ё ^ Ё Ё Ё переменные Ё Ё Ё ^ Ё Ё (не показаны)Ё Ё Ё . Ё Ё--------------Ё Ё Ё . Ё ЁАдрес записи 2Ё Ё Ё . Ё Ё--------------Ё Ё Ё . Ё ЁАдрес возвратаЁ Ё Ё . Ё Ё после вызова Ё Ё Ё . Ё Ё write Ё Ё Ё . Ё Ё--------------Ё Ё Ё . Ё Ёпараметры, пе-Ё Ё Ё . Ё Ё редаваемые Ё Ё Ё . Ё Ё write Ё Ё Ё . Ё Ё(new, buffer, Ё Ё Ё v Ё Ё count) Ё Запись 3 Ё Ё цдддддддддддддд╢ call write() Запись 3 цдддддддддддддддддд╢ Ё Локальные Ё Ё Локальные Ё Ё переменные Ё Ё переменные Ё Ё (count) Ё Ё Ё Ё--------------Ё Ё------------------Ё ЁАдрес записи 1Ё Ё Адрес записи 1 Ё Ё--------------Ё Ё------------------Ё ЁАдрес возвратаЁ Ё Адрес возврата Ё Ё после вызова Ё Ё после вызова Ё Ё copy Ё Ё func2 Ё Ё--------------Ё Ё------------------Ё Ёпараметры, пе-Ё Ё параметры, пере- Ё Ё редаваемые Ё Ё даваемые функции Ё Ё copy Ё Ё ядра func2 Ё Ё (old, new) Ё Запись 2 Запись 2 Ё Ё цдддддддддддддд╢ call copy() call func2() цдддддддддддддддддд╢ Ё Локальные Ё Ё Локальные Ё Ё переменные Ё Ё переменные Ё Ё(fdold, fdnew)Ё Ё Ё Ё--------------Ё Ё------------------Ё ЁАдрес записи 0Ё Ё Адрес записи 0 Ё Ё--------------Ё Ё------------------Ё ЁАдрес возвратаЁ Ё Адрес возврата Ё Ё после вызова Ё Ё после вызова Ё Ё main Ё Ё func1 Ё Ё--------------Ё Ё------------------Ё Ёпараметры, пе-Ё Ё параметры, пере- Ё Ё редаваемые Ё Ё даваемые функции Ё Ё main Ё Ё ядра func1 Ё Ё (argc, argv) Ё Запись 1 Запись 1 Ё Ё юдддддддддддддды call main() call func1() юдддддддддддддддддды Запись 0 Запись 0 Старт Интерфейс обращений к операционной системе Рисунок 2.4. Стеки задачи и ядра для программы копирования. промежуточная таблица облас- таблица тей процессов областей зддддддддддддддддддддд© зддддддддддддд© здддддддддддд© Ё часть адресного про-Ё Ё Ё Ё Ё Ё странства задачи, Ё Ё Ё Ё Ё Ё выделенная процессу Ё Ё Ё Ё Ё юддддддддддддддддддддды цддддддддддддд╢ цдддддддддддд╢ ^ зддед> дддедддддед> Ё Ё Ё цддддддддддддд╢ цдддддедддддд╢ зддддддддддедддддддддд© цддед> дддедд© Ё Ё Ё Ё Ё Ё Ё цддддддддддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё цдддддедддддд╢ цддддддддддедддддддддд╢ Ё Ё Ё юддед> Ё Ё Ё v дддддедды Ё Ё цдддддедддедд╢ цддддддддддддддддддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё юддддддддддддды юдддддедддедды Ё Ё Ё Ё юддддддддддддддддддддды Ё Ё таблица процессов здддддддддддддддддддддддддедддедд© Ё оперативная память v v Ё юдддддддддддддддддддддддддддддддды Рисунок 2.5. Информационные структуры для процессов Каждому процессу соответствует точка входа в таблице процес- сов ядра, кроме того, каждому процессу выделяется часть оператив- ной памяти, отведенная под задачу пользователя. Таблица процессов включает в себя указатели на промежуточную таблицу областей про- цессов, точки входа в которую служат в качестве указателей на собственно таблицу областей. Областью называется непрерывная зона адресного пространства, выделяемая процессу для размещения текс- та, данных и стека. Точки входа в таблицу областей описывают ат- рибуты области, как например, хранятся ли в области текст прог- раммы или данные, закрытая ли эта область или же совместно ис- пользуемая, и где конкретно в памяти размещается содержимое об- ласти. Внешний уровень косвенной адресации (через промежуточную таблицу областей, используемых процессами, к собственно таблице областей) позволяет независимым процессам совместно использовать области. Когда процесс запускает системную операцию exec, ядро системы выделяет области под ее текст, данные и стек, освобождая старые области, которые использовались процессом. Если процесс запускает операцию fork, ядро удваивает размер адресного прост- ранства старого процесса, позволяя процессам совместно использо- вать области, когда это возможно, и, с другой стороны, производя физическое копирование. Если процесс запускает операцию exit, яд- ро освобождает области, которые использовались процессом. На Ри- сунке 2.5 изображены информационные структуры, связанные с запус- ком процесса. Таблица процессов ссылается на промежуточную таблицу областей, используемых процессом, в которой содержатся указатели на записи в собственно таблице областей, соответствую- щие областям для текста, данных и стека процесса. Запись в таблице процессов и часть адресного пространства за- дачи, выделенная процессу, содержат управляющую информацию и дан- ные о состоянии процесса. Это адресное пространство является расширением соответствующей записи в таблице процессов, различия между данными объектами будут рассмотрены в главе 6. В качестве полей в таблице процессов, которые рассматриваются в последующих разделах, выступают: * поле состояния, * идентификаторы, которые характеризуют пользователя, являю- щегося владельцем процесса (код пользователя или UID), * значение дескриптора события, когда процесс приостановлен (находится в состоянии "сна"). Адресное пространство задачи, выделенное процессу, содержит описывающую процесс информацию, доступ к которой должен обеспечи- ваться только во время выполнения процесса. Важными полями явля- ются: * указатель на позицию в таблице процессов, соответствующую текущему процессу, * параметры текущей системной операции, возвращаемые значения и коды ошибок, * дескрипторы файла для всех открытых файлов, * внутренние параметры ввода-вывода, * текущий каталог и текущий корень (см. главу 5), * границы файлов и процесса. Ядро системы имеет непосредственный доступ к полям адресного пространства задачи, выделенного выполняемому процессу, но не имеет доступ к соответствующим полям других процессов. С точки зрения внутреннего алгоритма, при обращении к адресному прост- ранству задачи, выделенному выполняемому процессу, ядро ссылается на структурную переменную u, и, когда запускается на выполнение другой процесс, ядро перенастраивает виртуальные адреса таким об- разом, чтобы структурная переменная u указывала бы на адресное пространство задачи для нового процесса. В системной реализации предусмотрено облегчение идентификации текущего процесса благода- ря наличию указателя на соответствующую запись в таблице процес- сов из адресного пространства задачи. 2.2.2.1 Контекст процесса Контекстом процесса является его состояние, определяемое текстом, значениями глобальных переменных пользователя и информа- ционными структурами, значениями используемых машинных регистров, значениями, хранимыми в позиции таблицы процессов и в адресном пространстве задачи, а также содержимым стеков задачи и ядра, от- носящихся к данному процессу. Текст операций системы и ее гло- бальные информационные структуры совместно используются всеми процессами, но не являются составной частью контекста процесса. Говорят, что при запуске процесса система исполняется в кон- тексте процесса. Когда ядро системы решает запустить другой про- цесс, оно выполняет переключение контекста с тем, чтобы система исполнялась в контексте другого процесса. Ядро осуществляет пе- реключение контекста только при определенных условиях, что мы увидим в дальнейшем. Выполняя переключение контекста, ядро сохра- няет информацию, достаточную для того, чтобы позднее переключить- ся вновь на первый процесс и возобновить его выполнение. Анало- гичным образом, при переходе из режима задачи в режим ядра, ядро системы сохраняет информацию, достаточную для того, чтобы позднее вернуться в режим задачи и продолжить выполнение с прерванного места. Однако, переход из режима задачи в режим ядра является сменой режима, но не переключением контекста. Если обратиться еще раз к Рисунку 1.5, можно сказать, что ядро выполняет переключение контекста, когда меняет контекст процесса A на контекст процесса B; оно меняет режим выполнения с режима задачи на режим ядра и наоборот, оставаясь в контексте одного процесса, например, про- цесса A. Ядро обрабатывает прерывания в контексте прерванного процес- са, пусть даже оно и не вызывало никакого прерывания. Прерванный процесс мог при этом выполняться как в режиме задачи, так и в ре- жиме ядра. Ядро сохраняет информацию, достаточную для того, чтобы можно было позже возобновить выполнение прерванного процесса, и обрабатывает прерывание в режиме ядра. Ядро не порождает и не планирует порождение какого-то особого процесса по обработке пре- рываний. 2.2.2.2 Состояния процесса Время жизни процесса можно разделить на несколько состояний, каждое из которых имеет определенные характеристики, описывающие процесс. Все состояния процесса рассматриваются в главе 6, однако представляется существенным для понимания перечислить некоторые из состояний уже сейчас: 1. Процесс выполняется в режиме задачи. 2. Процесс выполняется в режиме ядра. 3. Процесс не выполняется, но готов к выполнению и ждет, ког- да планировщик выберет его. В этом состоянии может находиться много процессов, и алгоритм планирования устанавливает, какой из процессов будет выполняться следующим. 4. Процесс приостановлен ("спит"). Процесс "впадает в сон", когда он не может больше продолжать выполнение, например, когда ждет завершения ввода-вывода. Поскольку процессор в каждый момент времени выполняет только один процесс, в состояниях 1 и 2 может находиться самое большее один процесс. Эти два состояния соответствуют двум режимам выпол- нения, режиму задачи и режиму ядра. 2.2.2.3 Переходы из состояния в состояние Состояния процесса, перечисленные выше, дают статическое представление о процессе, однако процессы непрерывно переходят из состояния в состояние в соответствии с определенными правилами. Диаграмма переходов представляет собой ориентированный граф, вер- шины которого представляют собой состояния, в которые может пе- рейти процесс, а дуги - события, являющиеся причинами перехода процесса из одного состояния в другое. Переход между двумя состо- яниями разрешен, если существует дуга из первого состояния во второе. Несколько дуг может выходить из одного состояния, однако процесс переходит только по одной из них в зависимости от того, какое событие произошло в системе. На Рисунке 2.6 представлена диаграмма переходов для состояний, перечисленных выше. Как уже говорилось выше, в режиме разделения времени может выполняться одновременно несколько процессов, и все они могут од- новременно работать в режиме ядра. Если им разрешить свободно вы- полняться в режиме ядра, то они могут испортить глобальные инфор- мационные структуры, принадлежащие ядру. Запрещая произвольное переключение контекстов и управляя возникновением событий, ядро защищает свою целостность. Ядро разрешает переключение контекста только тогда, когда процесс переходит из состояния "запуск в режиме ядра" в состояние "сна в памяти". Процессы, запущенные в режиме ядра, не могут быть выгружены другими процессами; поэтому иногда говорят, что ядро невыгружаемо, при этом процессы, находящиеся в режиме задачи, мо- гут выгружаться системой. Ядро поддерживает целостность своих ин- формационных структур, поскольку оно невыгружаемо, таким образом решая проблему "взаимного исключения" - обеспечения того, что критические секции программы выполняются в каждый момент времени в рамках самое большее одного процесса. запуск зддддддд© в режи- Ё Ё ме за- Ё 1 Ё дачи Ё Ё юдбддддды Ё ^ обращение Ё Ё возврат к системе Ё Ё или пре- Ё Ё рывание Ё Ё Ё Ё v Ё запуск здддддад© в режи- Ё Ё<ддддддддд© прерывание, ме яд- Ё 2 Ё Ё возврат из ра Ё Ё<ддддддддды прерывания юдбддддды Ё ^ приоста-Ё Ё процесс пла- зддддддд© нов Ё Ё нирования зддддддд© готов- ожида- Ё Ё<дддддддддды юдддддддддддд╢ Ё ность ние Ё 4 Ё Ё 3 Ё к выпол- ("сон")Ё цддддддддддддддддддддддддддд>Ё Ё нению юддддддды пробуждение юддддддды переключение контекста до- пустимо Рисунок 2.6. Состояния процесса и переходы между ними В качестве примера рассмотрим программу (Рисунок 2.7) включе- ния информационной структуры, чей адрес содержится в указателе bp1, в список с использованием указателей после структуры, чей адрес содержится в bp. Если система разрешила переключение кон- текста при выполнении ядром фрагмента программы, возможно возник- новение следующей ситуации. Предположим, ядро выполняет программу до комментариев и затем осуществляет переключение контекста. Спи- сок с использованием сдвоенных указателей имеет противоречивый вид: структура bp1 только наполовину входит в этот список. Если процесс следует за передними указателями, он обнаружит bp1 в дан- ном списке, но если он последует за фоновыми указателями, то во- обще не найдет структуру bp1 (Рисунок 2.8). Если другие процессы будут обрабатывать указатели в списке до момента повторного за- пуска первого процесса, структура списка может постоянно разру- шаться. Система UNIX предупреждает возникновение подобных ситуаций, запрещая переключение контекстов на время выполнения процесса в режиме ядра. Если процесс переходит в состояние "сна", делая допустимым переключение контекста, алгоритмы ядра обеспечи- вают защиту целостности информационных структур системы. зддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё struct queue { Ё Ё Ё Ё Ё Ё Ё Ё } *bp, *bp1; Ё Ё bp1 - > forp = bp - > forp; Ё Ё bp1 - > backp = bp; Ё Ё bp - > forp = bp1; Ё Ё /* здесь рассмотрите возможность переключения контекста */Ё Ё bp1 - > forp - > backp = bp1; Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 2.7. Пример программы, создающей список с двунаправлен- ными указателями зддддддд© Ё Ё Ё bp1 Ё Ё Ё юддддддды зддддддд© зддддддд© ддддд>Ё цдддддддддддддддддддддддддддддд>Ё цдддд> Ё bp Ё Ё Ё <ддддд╢ Ё<дддддддддддддддддддддддддддддд╢ Ё<дддд юддддддды юддддддды Включение bp1 в список с двунаправленными указателями зддддддд© зддддддд© зддддддд© ддддд>Ё цдддддддддд>Ё цдддддддддд>Ё цдддд> Ё bp Ё Ё bp1 Ё Ё Ё <ддддд╢ Ё<дддддддддд╢ Ё зддддд╢ Ё<дддд юддддддды<дддд© юддддддды Ё юддддддды юддддддддддддддддддды Рисунок 2.8. Список с указателями, некорректный из-за переклю- чения контекста Проблемой, которая может привести к нарушению целостности ин- формации ядра, является обработка прерываний, могущая вносить из- менения в информацию о состоянии ядра. Например, если ядро выполняло программу, приведенную на Рисунке 2.7, и получило пре- рывание по достижении комментариев, программа обработки прерыва- ний может разрушить ссылки, если будет манипулировать указателя- ми, как было показано ранее. Чтобы решить эту проблему, система могла бы запретить все прерывания на время работы в режиме ядра, но при этом затянулась бы обработка прерывания, что в конечном счете нанесло бы ущерб производительности системы. Вместо этого ядро повышает приоритет прерывания процессора, запрещая прерыва- ния на время выполнения критических секций программы. Секция программы является критической, если в процессе ее выполнения за- пуск программ обработки произвольного прерывания может привести к возникновению проблем, имеющих отношение к нарушению целостности. Например, если программа обработки прерывания от диска работает с буферными очередями, то часть прерываемой программы, при выполне- нии которой ядро обрабатывает буферные очереди, является крити- ческой секцией по отношению к программе обработки прерывания от диска. Критические секции невелики по размеру и встречаются не- часто, поэтому их существование не оказывает практически никакого воздействия на производительность системы. В других операционных системах данный вопрос решается путем запрещения любых прерываний при работе в системных режимах или путем разработки схем блоки- ровки, обеспечивающих целостность. В главе 12 мы еще вернемся к этому вопросу, когда будем говорить о многопроцессорных системах, где применения указанных мер для решения проблемы недостаточно. Чтобы подвести черту, еще раз скажем, что ядро защищает свою целостность, разрешая переключение контекста только тогда, когда процесс переходит в состояние "сна", а также препятствуя воздейс- твию одного процесса на другой с целью изменения состояния пос- леднего. Оно также повышает приоритет прерывания процессора на время выполнения критических секций программ, запрещая таким об- разом прерывания, которые в противном случае могут вызвать нару- шение целостности. Планировщик процессов периодически выгружает процессы, выполняющиеся в режиме задачи, для того, чтобы процессы не могли монопольно использовать центральный процессор. 2.2.2.4 "Сон" и пробуждение Процесс, выполняющийся в режиме ядра, имеет значительную сте- пень автономии в решении того, как ему следует реагировать на возникновение системных событий. Процессы могут общаться между собой и "предлагать" различные альтернативы, но при этом оконча- тельное решение они принимают самостоятельно. Мы еще увидим, что существует набор правил, которым подчиняется поведение процессов в различных обстоятельствах, но каждый процесс в конечном итоге следует этим правилам по своей собственной инициативе. Например, если процесс должен временно приостановить выполнение ("перейти ко сну"), он это делает по своей доброй воле. Следовательно, программа обработки прерываний не может приостановить свое выпол- нение, ибо если это случится, прерванный процесс должен был бы "перейти ко сну" по умолчанию. Процессы приостанавливают свое выполнение, потому что они ожидают возникновения некоторого события, например, завершения ввода-вывода на периферийном устройстве, выхода, выделения сис- темных ресурсов и т.д. Когда говорят, что процесс приостановился по событию, то имеется ввиду, что процесс находится в состоянии "сна" до наступления события, после чего он пробудится и перейдет в состояние "готовности к выполнению". Одновременно могут приос- тановиться по событию много процессов; когда событие наступает, все процессы, приостановленные по событию, пробуждаются, посколь- ку значение условия, связанного с событием, больше не является "истинным". Когда процесс пробуждается, он переходит из состояния "сна" в состояние "готовности к выполнению", находясь в котором он уже может быть выбран планировщиком; следует обратить внимание на то, что он не выполняется немедленно. Приостановленные процес- сы не занимают центральный процессор. Ядру системы нет надобности постоянно проверять то, что процесс все еще приостановлен, т.к. ожидает наступления события, и затем будить его. Например, процесс, выполняемый в режиме ядра, должен иногда блокировать структуру данных на случай приостановки в будущем; процессы, пытающиеся обратиться к заблокированной структуре, обя- заны проверить наличие блокировки и приостановить свое выполне- ние, если структура заблокирована другим процессом. Ядро выполня- ет блокировки такого рода следующим образом: while (условие "истинно") sleep (событие: условие принимает значение "ложь"); set condition true; то есть: пока (условие "истинно") приостановиться (до наступления события, при котором условие принимает значение "ложь"); присвоить условию значение "истина"; Ядро снимает блокировку и "будит" все процессы, приостанов- ленные из-за этой блокировки, следующим образом: set condition false; wakeup (событие: условие "ложно"); то есть: присвоить условию значение "ложь"; перезапуститься (при наступлении события, при котором условие принимает значение "ложь"); На Рисунке 2.9 приведен пример, в котором три процесса, A, B и C оспаривают заблокированный буфер. Переход в состояние "сна" вызывается заблокированностью буфера. Процессы, однажды запустив- шись, обнаруживают, что буфер заблокирован, и приостанавливают свое выполнение до наступления события, по которому буфер будет разблокирован. В конце концов блокировка с буфера снимается и все процессы "пробуждаются", переходя в состояние "готовности к вы- полнению". Ядро наконец выбирает один из процессов, скажем, B, для выполнения. Процесс B, выполняя цикл "while", обнаруживает, что буфер разблокирован, блокирует его и продолжает свое выполне- ние. Если процесс B в дальнейшем снова приостановится без снятия блокировки с буфера (например, ожидая завершения операции вво- да-вывода), ядро сможет приступить к планированию выполнения дру- гих процессов. Если будет при этом выбран процесс A, этот про- цесс, выполняя цикл "while", обнаружит, что буфер заблокирован, и снова перейдет в состояние "сна"; возможно то же самое произойдет и с процессом C. В конце концов выполнение процесса B возобновит- ся и блокировка с буфера будет снята, в результате чего процессы A и C получат доступ к нему. Таким образом, цикл "while-sleep" обеспечивает такое положение, при котором самое большее один про- цесс может иметь доступ к ресурсу. Алгоритмы перехода в состояние "сна" и пробуждения более под- робно будут рассмотрены в главе 6. Тем временем они будут счи- таться "неделимыми". Процесс переходит в состояние "сна" мгновенно и находится в нем до тех пор, пока не будет "разбужен". После того, как он приостанавливается, ядро системы начинает пла- нировать выполнение следующего процесса и переключает контекст на него. 2.3 СТРУКТУРЫ ДАННЫХ ЯДРА Большинство информационных структур ядра размещается в табли- цах фиксированного размера, а не в динамически выделенной памяти. Преимущество такого подхода состоит в том, что программа ядра проста, но в ней ограничивается число элементов информационной структуры до значения, предварительно заданного при генерации системы. Если во время функционирования системы число элементов информационной структуры ядра выйдет за указанное значение, ядро не сможет динамически выделить место для новых элементов и должно сообщить об ошибке пользователю, сделавшему запрос. Если, с дру- гой стороны, ядро сгенерировано таким образом, что выход за гра- ницы табличного пространства будет маловероятен, дополнительное табличное пространство может не понадобиться, поскольку оно не может быть использовано для других целей. Как бы то ни было, простота алгоритмов ядра представляется более важной, чем сжатие последних байтов оперативной памяти. Обычно в алгоритмах для по- иска свободных мест в таблицах используются несложные циклы и этот метод более понятен и иногда более эффективен по сравнению с более сложными схемами выделения памяти. 2.4 УПРАВЛЕНИЕ СИСТЕМОЙ К управляющим процессам, грубо говоря, относятся те процессы, которые выполняют различные функции по обеспечению благополучной работы пользователей системы. К таким функциям относятся формати- рование дисков, создание новых файловых систем, восстановление разрушенных файловых систем, отладка ядра и др. С концептуальной точки зрения, между управляющими и пользовательскими процессами нет разницы. Они используют один и тот же набор обращений к опе- рационной системе, доступный для всех. Управляющие процессы отли- чаются от обычных пользовательских процессов только правами и привилегиями, которыми они обладают. Например, режимы разрешения доступа к файлу могут предусматривать предоставление возможности работы с файлами для управляющих процессов и отсутствие такой возможности для обычных пользователей. Внутри системы ядро выде- ляет особого пользователя, именуемого суперпользователем, и наде- ляет его особыми привилегиями, о чем мы еще поговорим ниже. Поль- зователь может стать суперпользователем, если соответствующим об- разом зарегистрируется в системе или запустит специальную программу. Привилегии суперпользователя будут рассмотрены в сле- дующих главах. Если сказать коротко, ядро системы не выделяет уп- равляющие процессы в отдельный класс. Время Процесс A Процесс B Процесс C зддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё Буфер заблокирован Ё Приостановлен Ё . Ё . Ё . Буфер заблокирован Ё . Приостановлен Ё . . Ё . . Ё . . Буфер заблокирован Ё . . Приостановлен Ё здддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё ЁБуфер разблокирован Пробуждение всех "спящих" процессовЁ Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Ё Готов к Готов к Готов к Ё выполнению выполнению выполнению Ё . . Ё . Запущен . Ё . Буфер разблокирован . Ё . Блокировка буфера . Ё . . . Ё . . . Ё . . . Ё . . . Ё . . . Ё . Приостановка по . Ё . произвольной причине . Ё . . . Ё Запущен . . Ё Буфер заблокирован . . Ё Приостановка . . Ё . . . Ё . . Запущен Ё . . Буфер заблокирован Ё . . Приостановка Ё . . . Ё . Пробуждение . Ё . Снятие блокировки . Ё . буфера . Ё Готов к Пробуждение всех Готов к Ё выполнению "спящих" процессов выполнению Ё v Переключение контекста Запущен Рисунок 2.9. Многократная приостановка выполнения процессов, вызванная блокировкой 2.5 ВЫВОДЫ И ОБЗОР ПОСЛЕДУЮЩИХ ГЛАВ В этой главе описана архитектура ядра операционной системы; его основными компонентами выступают подсистема управления файла- ми и подсистема управления процессами. Подсистема управления фай- лами управляет хранением и выборкой данных в пользовательских файлах. Файлы организованы в виде файловых систем, которые трак- туются как логические устройства; физическое устройство, такое как диск, может содержать несколько логических устройств (файло- вых систем). Каждая файловая система имеет суперблок, в котором описывается структура и содержимое файловой системы, каждый файл в файловой системе описывается индексом, хранящим атрибуты файла. Системные операции работают с файлами, используя индексы. Процессы находятся в различных состояниях и переходят из сос- тояния в состояние, следуя определенным правилам перехода. В частности, процессы, выполняющиеся в режиме ядра, могут приоста- новить свое выполнение и перейти в состояние "сна", но ни один процесс не может перевести в это состояние другой процесс. Ядро является невыгружаемым и это означает, что процесс, выполняющийся в режиме ядра, будет продолжать свое выполнение до тех пор, пока не перейдет в состояние "сна" или пока не вернется в режим зада- чи. Ядро обеспечивает целостность своих информационных структур благодаря своей невыгружаемости, а также путем блокирования пре- рываний на время выполнения критических секций программы. В остальных частях главы детально описываются подсистемы, изображенные на Рисунке 2.1, а также взаимодействие между ними, начиная с подсистемы управления файлами и включая подсистему уп- равления процессами. В следующей главе рассматривается буфер сверхоперативной памяти (кеш) и описываются алгоритмы управления буфером, используемые в главах 4, 5 и 7. В главе 4 рассматривают- ся внутренние алгоритмы файловой системы, включая обработку ин- дексов, структуру файлов, преобразование имени пути в индекс. В главе 5 рассматриваются системные операции, которые, используя приведенные в главе 4 алгоритмы, обращаются к файловой системе, т.е. такие, как open, close, read и write. Глава 6 имеет дело с понятием контекста процесса и его адресным пространством, а глава 7 рассматривает системные операции, связанные с управлением про- цессами и использующие алгоритмы главы 6. Глава 8 касается плани- рования выполнения процессов, в главе 9 обсуждаются алгоритмы распределения памяти. Глава 10 посвящена драйверам устройств, рассмотрение которых до того откладывалось, чтобы прежде объяс- нить связь драйвера терминала с управлением процессами. В главе 11 представлено несколько форм взаимодействия процессов. Наконец, в последних двух главах рассматриваются вопросы, связанные с уг- лубленным изучением особенностей системы, в частности, особеннос- ти многопроцессорных систем и распределенных систем. 2.6 УПРАЖНЕНИЯ 1. Рассмотрим следующий набор команд: grep main a.c b.c c.c > grepout & wc -1 < grepout & rm grepout & Амперсанд (символ "&") в конце каждой командной строки говорит командному процессору shell о том, что команду следует выпол- нить на фоне, при этом shell может выполнять все командные строки параллельно. Почему это не равноценно следующей команд- ной строке ? grep main a.c b.c c.c Ё wc -1 2. Рассмотрим пример программы, приведенный на Рисунке 2.7. Пред- положим, что в тот момент, когда при ее выполнении встретился комментарий, произошло переключение контекста и другой процесс убрал содержимое буфера из списка указателей с помощью следую- щих команд: remove(gp) struct queue *gp; { gp - > forp - > backp = gp - > backp; gp - > backp - > forp = gp - > forp; gp - > forp = gp - > backp = NULL; } Рассмотрим три случая: - Процесс убирает из списка с указателями структуру bp1. - Процесс убирает из списка с указателями структуру, следующую после структуры bp1. - Процесс убирает из списка структуру, которая первоначально следовала за bp1 до того, как структура bp была наполовину включена в указанный список. В каком состоянии будет список после того, как первый процесс завершит выполнение части программы, расположенной после ком- ментариев ? 3. Что произошло бы в том случае, если ядро попыталось бы возоб- новить выполнение всех процессов, приостановленных по событию, но в системе не было бы к этому моменту ни одного такого про- цесса ? БУФЕР СВЕРХОПЕРАТИВНОЙ ПАМЯТИ (КЕШ) Как уже говорилось в предыдущей главе, ядро операционной сис- темы поддерживает файлы на внешних запоминающих устройствах боль- щой емкости, таких как диски, и позволяет процессам сохранять но- вую информацию или вызывать ранее сохраненную информацию. Если процессу необходимо обратиться к информации файла, ядро выбирает информацию в оперативную память, где процесс сможет просматривать эту информацию, изменять ее и обращаться с просьбой о ее повтор- ном сохранении в файловой системе. Вспомним для примера программу copy, приведенную на Рисунке 1.3: ядро читает данные из первого файла в память и затем записывает эти данные во второй файл. По- добно тому, как ядро должно заносить данные из файла в память, оно так же должно считывать в память и вспомогательные данные для работы с ними. Например, суперблок файловой системы содержит по- мимо всего прочего информацию о свободном пространстве, доступном файловой системе. Ядро считывает суперблок в память для того, чтобы иметь доступ к его информации, и возвращает его опять фай- ловой системе, когда желает сохранить его содержимое. Похожая вещь происходит с индексом, который описывает размещение файла. Ядро системы считывает индекс в память, когда желает получить доступ к информации файла, и возвращает индекс вновь файловой системе, когда желает скорректировать размещение файла. Ядро об- рабатывает такую вспомогательную информацию, не будучи прежде знакома с ней и не требуя для ее обработки запуска каких-либо процессов. Ядро могло бы производить чтение и запись непосредственно с диска и на диск при всех обращениях к файловой системе, однако время реакции системы и производительность при этом были бы низ- кими из-за низкой скорости передачи данных с диска. По этой при- чине ядро старается свести к минимуму частоту обращений к диску, заведя специальную область внутренних информационных буферов, именуемую буферным кешем (*) и хранящую содержимое блоков диска, к которым перед этим производились обращения. Буферный кеш представляет собой программную структуру, которую не следует путать с аппаратными кешами, ускоряющими косвенную адресацию памяти. На Рисунке 2.1 показано, что модуль буферного кеша занимает в архитектуре ядра место между подсистемой управления файлами и драйверами устройств (ввода-вывода блоками). Перед чтением инфор- мации с диска ядро пытается считать что-нибудь из буфера кеша. Если в этом буфере отсутствует информация, ядро читает данные с диска и заносит их в буфер, используя алгоритм, который имеет целью поместить в буфере как можно больше необходимых данных. Аналогично, информация, записываемая на диск, заносится в буфер для того, чтобы находиться там, если ядро позднее попытается счи- тать ее. Ядро также старается свести к минимуму частоту выполне- ния операций записи на диск, выясняя, должна ли информация дейс- твительно запоминаться на диске или это промежуточные данные, которые будут вскоре затерты. Алгоритмы более высокого уровня позволяют производить предварительное занесение данных в буфер кеша или задерживать запись данных с тем, чтобы усилить эффект использования буфера. В этой главе рассматриваются алгоритмы, ис- пользуемые ядром при работе с буферами в сверхоперативной памяти. 3.1 ЗАГОЛОВКИ БУФЕРА Во время инициализации системы ядро выделяет место под совокупность буферов, потребность в которых определяется в зави- симости от размера памяти и производительности системы. Каждый буфер состоит из двух частей: области памяти, в которой хранится информация, считываемая с диска, и заголовка буфера, который идентифицирует буфер. Поскольку существует однозначное соответс- твие между заголовками буферов и массивами данных, в нижеследую- щем тексте используется термин "буфер" в ссылках как на ту, так и на другую его составляющую, и о какой из частей буфера идет речь будет понятно из контекста. Информация в буфере соответствует информации в одном логичес- ком блоке диска в файловой системе, и ядро распознает содержимое буфера, просматривая идентифицирующие поля в его заголовке. Буфер представляет собой копию дискового блока в памяти; содержимое дискового блока отображается в буфер, но это отображение времен- ное, поскольку оно имеет место до того момента, когда ядро примет решение отобразить в буфер другой дисковый блок. Один дисковый блок не может быть одновременно отображен в несколько буферов. Если бы два буфера содержали информацию для одного и того же дис- кового блока, ядро не смогло бы определить, в каком из буферов содержится текущая информация, и, возможно, возвратило бы на диск некорректную информацию. Предположим, например, что дисковый блок отображается в два буфера, A и B. Если ядро запишет данные снача- ла в буфер A, а затем в буфер B, дисковый блок будет содержать данные из буфера B, если в результате операций записи буфер за- полнится до конца. Однако, если ядро изменит порядок, в котором оно копирует содержимое буферов на диск, на противоположный, дис- ковый блок будет содержать некорректные данные. здддддддддддддддддд© Ё номер устройства Ё цдддддддддддддддддд╢ указатель на Ё номер блока Ё область данных цдддддддддддддддддд╢ зддддддддддддд> указатель на Ё поле состояния Ё Ё предыдущий буфер цдддддддддддддддддд╢ Ё в хеш-очереди Ё ддддддеддды <ддддддддддддд© цдддддддддддддддддд╢ Ё Ё ддддддеддддддддддддддддд> Ё цдддддддддддддддддд╢ указатель на юдддедддддд Ё следующий буфер цдддддддддддддддддд╢ в хеш-очереди Ё ддддддеддд© цдддддддддддддддддд╢ Ё <дддддддддддддддддедддддд Ё Ё указатель на юдддддддддддддддддды юддддддддддддд> предыдущий буфер указатель на в списке свободных следующий буфер в списке свободных Рисунок 3.1. Заголовок буфера Заголовок буфера (Рисунок 3.1) содержит поле "номер устройс- тва" и поле "номер блока", которые определяют файловую систему и номер блока с информацией на диске и однозначно идентифицируют буфер. Номер устройства - это логический номер файловой системы (см. раздел 2.2.1), а не физический номер устройства (диска). За- головок буфера также содержит указатель на область памяти для бу- фера, размер которой должен быть не меньше размера дискового бло- ка, и поле состояния, в котором суммируется информация о текущем состоянии буфера. Состояние буфера представляет собой комбинацию из следующих условий: * буфер заблокирован (термины "заблокирован (недоступен)" и "за- нят" равнозначны, так же, как и понятия "свободен" и "досту- пен"), * буфер содержит правильную информацию, * ядро должно переписать содержимое буфера на диск перед тем, как переназначить буфер; это условие известно, как "задержка, выз- ванная записью", * ядро читает или записывает содержимое буфера на диск, * процесс ждет освобождения буфера. В заголовке буфера также содержатся два набора указателей, используемые алгоритмами выделения буфера, которые поддерживают общую структуру области буферов (буферного пула), о чем подробнее будет говориться в следующем разделе. 3.2 СТРУКТУРА ОБЛАСТИ БУФЕРОВ (БУФЕРНОГО ПУЛА) здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё указатели вперед Ё Ё зддддддддддддддддддд© зддддддд© зддддддд© зддддддд© Ё юд>Ё заголовок списка Ёддд>Ё буфер Ёддд>Ё буфер ЁЫЫЫ>Ё буфер Ёдды зддЁ свободных буферов Ё<дддЁ 1 Ё<дддЁ 2 Ё<ЫЫЫЁ n Ё<д© Ё юддддддддддддддддддды юддддддды юддддддды юддддддды Ё Ё указатели назад Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды до после здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё указатели вперед Ё Ё зддддддддддддддддддд© зддддддд© зддддддд© Ё юд>Ё заголовок списка Ёдддддддддддддддд>Ё буфер ЁЫЫЫ>Ё буфер Ёдды зддЁ свободных буферов Ё<ддддддддддддддддЁ 2 Ё<ЫЫЫЁ n Ё<д© Ё юддддддддддддддддддды юддддддды юддддддды Ё Ё указатели назад Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 3.2. Список свободных буферов Ядро помещает информацию в область буферов, используя алго- ритм поиска буферов, к которым наиболее долго не было обращений: после выделения буфера дисковому блоку нельзя использовать этот буфер для другого блока до тех пор, пока не будут задействованы все остальные буферы. Ядро управляет списком свободных буферов, который необходим для работы указанного алгоритма. Этот список представляет собой циклический перечень буферов с двунаправленны- ми указателями и с формальными заголовками в начале и в конце пе- речня (Рисунок 3.2). Все буферы попадают в список при загрузке системы. Если нужен любой свободный буфер, ядро выбирает буфер из "головы" списка, но если в области буферов ищется определенный блок, может быть выбран буфер и из середины списка. И в том, и в другом случае буфер удаляется из списка свободных буферов. Если ядро возвращает буфер буферному пулу, этот буфер добавляется в хвост списка, либо в "голову" списка (в случае ошибки), но никог- да не в середину. По мере удаления буферов из списка буфер с нуж- ной информацией продвигается все ближе и ближе к "голове" списка (Рисунок 3.2). Следовательно, те буферы, которые находятся ближе к "голове" списка, в последний раз использовались раньше, чем бу- феры, находящиеся дальше от "головы" списка. Когда ядро обращается к дисковому блоку, оно сначала ищет бу- фер с соответствующей комбинацией номеров устройства и блока. Вместо того, чтобы просматривать всю область буферов, ядро орга- низует из буферов особые очереди, хешированные по номеру устройс- тва и номеру блока. В хеш-очереди ядро устанавливает для буферов циклическую связь в виде списка с двунаправленными указателями, структура которого похожа на структуру списка свободных буферов. Количество буферов в хеш-очереди варьируется в течение всего вре- мени функционирования системы, в чем мы еще убедимся дальше. Ядро вынуждено прибегать к функции хеширования, чтобы единообразно распределять буферы между хеш-очередями, однако функция хеширова- ния должна быть несложной, чтобы не пострадала производительность системы. Администраторы системы задают количество хеш-очередей при генерации операционной системы. заголовки хеш-очередей зддддддддддддддддд© Ё Ё здддд© здддд© здддд© <ЫЫЫЫЁ блок 0 модуль 4 ЁЫЫЫЫЫЫЁ 28 ЁЫЫЫЫЫЁ 4 ЁЫЫЫЫЫЁ 64 ЁЫЫЫЫ> Ё Ё юдддды юдддды юдддды цддддддддддддддддд╢ Ё Ё здддд© здддд© здддд© <ЫЫЫЫЁ блок 1 модуль 4 ЁЫЫЫЫЫЫЁ 17 ЁЫЫЫЫЫЁ 5 ЁЫЫЫЫЫЁ 97 ЁЫЫЫЫ> Ё Ё юдддды юдддды юдддды цддддддддддддддддд╢ Ё Ё здддд© здддд© здддд© <ЫЫЫЫЁ блок 2 модуль 4 ЁЫЫЫЫЫЫЁ 98 ЁЫЫЫЫЫЁ 50 ЁЫЫЫЫЫЁ 10 ЁЫЫЫЫ> Ё Ё юдддды юдддды юдддды цддддддддддддддддд╢ Ё Ё здддд© здддд© здддд© <ЫЫЫЫЁ блок 3 модуль 4 ЁЫЫЫЫЫЫЁ 3 ЁЫЫЫЫЫЁ 35 ЁЫЫЫЫЫЁ 99 ЁЫЫЫЫ> Ё Ё юдддды юдддды юдддды юддддддддддддддддды Рисунок 3.3. Буферы в хеш-очередях На Рисунке 3.3 изображены буферы в хеш-очередях: заголовки хеш-очередей показаны в левой части рисунка, а квадратиками в каждой строке показаны буферы в соответствующей хеш-очереди. Так, квадратики с числами 28, 4 и 64 представляют буферы в хеш-очереди для "блока 0 модуля 4". Пунктирным линиям между буферами соот- ветствуют указатели вперед и назад вдоль хеш-очереди; для просто- ты на следующих рисунках этой главы данные указатели не показыва- ются, но их присутствие подразумевается. Кроме того, на рисунке блоки идентифицируются только своими номерами и функция хеширова- ния построена на использовании только номеров блоков; однако на практике также используется номер устройства. Любой буфер всегда находится в хеш-очереди, но его положение в очереди не имеет значения. Как уже говорилось, никакая пара бу- феров не может одновременно содержать данные одного и того же дискового блока; поэтому каждый дисковый блок в буферном пуле су- ществует в одной и только одной хеш-очереди и представлен в ней только один раз. Тем не менее, буфер может находиться в списке свободных буферов, если его статус "свободен". Поскольку буфер может быть одновременно в хеш-очереди и в списке свободных буфе- ров, у ядра есть два способа его обнаружения. Ядро просматривает хеш-очередь, если ему нужно найти определенный буфер, и выбирает буфер из списка свободных буферов, если ему нужен любой свободный буфер. В следующем разделе будет показано, каким образом ядро осуществляет поиск определенных дисковых блоков в буферном кеше, а также как оно работает с буферами в хеш-очередях и в списке свободных буферов. Еще раз напомним: буфер всегда находится в хеш -очереди, а в списке свободных буферов может быть, но может и от- сутствовать. 3.3 МЕХАНИЗМ ПОИСКА БУФЕРА Как показано на Рисунке 2.1, алгоритмы верхнего уровня, ис- пользуемые ядром для подсистемы управления файлами, инициируют выполнение алгоритмов управления буферным кешем. При выборке бло- ка алгоритмы верхнего уровня устанавливают логический номер уст- ройства и номер блока, к которым они хотели бы получить доступ. Например, если процесс хочет считать данные из файла, ядро уста- навливает, в какой файловой системе находится файл и в каком бло- ке файловой системы содержатся данные, о чем подробнее мы узнаем из главы 4. Собираясь считать данные из определенного дискового блока, ядро проверяет, находится ли блок в буферном пуле, и если нет, назначает для него свободный буфер. Собираясь записать дан- ные в определенный дисковый блок, ядро проверяет, находится ли блок в буферном пуле, и если нет, назначает для этого блока сво- бодный буфер. Для выделения буферов из пула в алгоритмах чтения и записи дисковых блоков используется операция getblk (Рисунок 3.4). Рассмотрим в этом разделе пять возможных механизмов использо- вания getblk для выделения буфера под дисковый блок. 1. Ядро обнаруживает блок в хеш-очереди, соответствующий ему буфер свободен. 2. Ядро не может обнаружить блок в хеш-очереди, поэтому оно выделяет буфер из списка свободных буферов. 3. Ядро не может обнаружить блок в хеш-очереди и, пытаясь вы- делить буфер из списка свободных буферов (как в случае 2), обнаруживает в списке буфер, который помечен как "занят на время записи". Ядро должно переписать этот буфер на диск и выделить другой буфер. 4. Ядро не может обнаружить блок в хеш-очереди, а список сво- бодных буферов пуст. 5. Ядро обнаруживает блок в хеш-очереди, но его буфер в нас- тоящий момент занят. Обсудим каждый случай более подробно. Осуществляя поиск блока в буферном пуле по комбинации номеров устройства и блока, ядро ищет хеш-очередь, которая бы содержала этот блок. Просматривая хеш-очередь, ядро придерживается списка с указателями, пока (как в первом случае) не найдет буфер с искомы- ми номерами устройства и блока. Ядро проверяет занятость блока и в том случае, если он свободен, помечает буфер "занятым" для то- го, чтобы другие процессы (**) не смогли к нему обратиться. (**) Из предыдущей главы напомним, что все операции ядра произво- дятся в контексте процесса, выполняемого в режиме ядра. Та- ким образом, слова "другие процессы" относятся к процессам, тоже выполняющимся в режиме ядра. Эти слова мы будем исполь- зовать и тогда, когда будем говорить о взаимодействии нес- кольких процессов, работающих в режиме ядра; и будем гово- рить "ядро", когда взаимодействие между процессами будет отсутствовать. Затем ядро удаляет буфер из списка свободных буферов, поскольку буфер не может одновременно быть занятым и находиться в указанном списке. Если другие процессы попытаются обратиться к блоку в то время, когда его буфер занят, они приостановятся до тех пор, пока буфер не освободится. На Рисунке 3.5 показан первый случай, когда ядро ищет блок 4 в хеш-очереди, помеченной как "блок 0 модуль 4". Обнаружив буфер, ядро удаляет его из списка свободных буферов, делая блоки 5 и 28 соседями в списке. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм getblk Ё Ё входная информация: номер файловой системы Ё Ё номер блока Ё Ё выходная информация: буфер, который можно использовать дляЁ Ё блока Ё Ё { Ё Ё выполнить если (буфер не найден) Ё Ё { Ё Ё если (блок в хеш-очереди) Ё Ё { Ё Ё если (буфер занят) /* случай 5 */ Ё Ё { Ё Ё приостановиться (до освобождения буфера); Ё Ё продолжить; /* цикл с условием продолжения */Ё Ё } Ё Ё пометить буфер занятым; /* случай 1 */ Ё Ё удалить буфер из списка свободных буферов; Ё Ё вернуть буфер; Ё Ё } Ё Ё в противном случае /* блока нет в хеш-очереди */ Ё Ё { Ё Ё если (в списке нет свободных буферов) /*случай 4*/Ё Ё { Ё Ё приостановиться (до освобождения любого буфера);Ё Ё продолжить; /* цикл с условием продолжения */Ё Ё } Ё Ё удалить буфер из списка свободных буферов; Ё Ё если (буфер помечен для отложенной переписи) Ё Ё /* случай 3 */ Ё Ё { Ё Ё асинхронная перепись содержимого буфера на диск;Ё Ё продолжить; /* цикл с условием продолжения */Ё Ё } Ё Ё /* случай 2 -- поиск свободного буфера */ Ё Ё удалить буфер из старой хеш-очереди; Ё Ё включить буфер в новую хеш-очередь; Ё Ё вернуть буфер; Ё Ё } Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 3.4. Алгоритм выделения буфера заголовки хеш-очередей зддддддддддддддддд© зддддддддддддддддд© Ё Ё Ёздддд© здддд©Ё здддд© Ё блок 0 модуль 4 ЁЫЫЫЫ ю╢ 28 ц© з╢ 4 цы Ё 64 Ё Ё Ё юддддыЁ Ёюдддды юдддды цддддддддддддддддд╢ Ё юдддддд© Ё Ё здддд©Ё здддд©Ё здддд© Ё блок 1 модуль 4 ЁЫЫЫЫ Ё 17 ЁЁ з╢ 5 цы з╢ 97 ц© Ё Ё юддддыЁ Ёюдддды здыюддддыЁ цддддддддддддддддд╢ юдддЁдддддддды здддддды Ё Ё здддд© Ёздддд© Ёздддд© Ё блок 2 модуль 4 ЁЫЫЫЫ Ё 98 ЁздддыЁ 50 Ё ю╢ 10 ц© Ё Ё юддддыЁ юдддды юддддыЁ цддддддддддддддддд╢ Ё Ё Ё Ё здддд©Ё здддд© здддд©Ё Ё блок 3 модуль 4 ЁЫЫЫЫз>Ё 3 цы Ё 35 Ё Ё 99 ЁЁ Ё Ё Ё юдддды юдддды юддддыЁ юддддддддддддддддды Ё Ё зддддддддддддддддд© Ё Ё Ёзаголовок списка цдддды Ё Ё Ё Ё Ёсвободных буферовЁ<ддддддддддддддддддддддддддддддддды юддддддддддддддддды (а) Поиск блока 4 в первой хеш-очереди заголовки хеш-очередей зддддддддддддддддд© здддддддддддддд© Ё Ё здддд©Ё здддд© Ёздддд© Ё блок 0 модуль 4 ЁЫЫЫЫ з╢ 28 цы Ё 4 Ё ЁЁ 64 Ё Ё Ё Ёюдддды юдддды Ёюдддды цддддддддддддддддд╢ юддддддддддддддддд© Ё Ё Ё здддд© здддд©Ё Ёздддд© Ё блок 1 модуль 4 ЁЫЫЫЫ Ё 17 Ё з╢ 5 цы ю╢ 97 ц© Ё Ё юдддды Ёюдддды юддддыЁ цддддддддддддддддд╢ Ё здддддды Ё Ё здддд© Ёздддд© Ёздддд© Ё блок 2 модуль 4 ЁЫЫЫЫ Ё 98 ЁздддыЁ 50 Ё ю╢ 10 ц© Ё Ё юддддыЁ юдддды юддддыЁ цддддддддддддддддд╢ Ё Ё Ё Ё здддд©Ё здддд© здддд©Ё Ё блок 3 модуль 4 ЁЫЫЫЫз>Ё 3 цы Ё 35 Ё Ё 99 ЁЁ Ё Ё Ё юдддды юдддды юддддыЁ юддддддддддддддддды Ё Ё зддддддддддддддддд© Ё Ё Ёзаголовок списка цдддды Ё Ё Ё Ё Ёсвободных буферовЁ<ддддддддддддддддддддддддддддддддды юддддддддддддддддды (б) Удаление блока 4 из списка свободных буферов Рисунок 3.5. Поиск буфера - случай 1: буфер в хеш-очереди здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм brelse Ё Ё входная информация: заблокированный буфер Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё возобновить выполнение всех процессов при наступлении Ё Ё события, связанного с освобождением любого буфера; Ё Ё возобновить выполнение всех процессов при наступлении Ё Ё события, связанного с освобождением данного буфера; Ё Ё поднять приоритет прерывания процессора так, чтобы Ё Ё блокировать любые прерывания; Ё Ё если (содержимое буфера верно и буфер не старый) Ё Ё поставить буфер в конец списка свободных буферов Ё Ё в противном случае Ё Ё поставить буфер в начало списка свободных буферов Ё Ё понизить приоритет прерывания процессора с тем, чтобы Ё Ё вновь разрешить прерывания; Ё Ё разблокировать (буфер); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 3.6. Алгоритм высвобождения буфера Перед тем, как перейти к остальным случаям, рассмотрим, что произойдет с буфером после того, как он будет выделен блоку. Ядро системы сможет читать данные с диска в буфер и обрабатывать их или же переписывать данные в буфер и при желании на диск. Ядро оставляет у буфера пометку "занят"; другие процессы не могут об- ратиться к нему и изменить его содержимое, пока он занят, таким образом поддерживается целостность информации в буфере. Когда яд- ро заканчивает работу с буфером, оно освобождает буфер в соот- ветствии с алгоритмом brelse (Рисунок 3.6). Возобновляется выпол- нение тех процессов, которые были приостановлены из-за того, что буфер был занят, а также те процессы, которые были приостановлены из-за того, что список свободных буферов был пуст. Как в том, так и в другом случае, высвобождение буфера означает, что буфер ста- новится доступным для приостановленных процессов несмотря на то, что первый процесс, получивший буфер, заблокировал его и запретил тем самым получение буфера другими процессами (см. раздел 2.2.2.4). Ядро помещает буфер в конец списка свободных буферов, если только перед этим не произошла ошибка ввода-вывода или если буфер не помечен как "старый" - момент, который будет пояснен да- лее; в остальных случаях буфер помещается в начало списка. Теперь буфер свободен для использования любым процессом. Ядро выполняет алгоритм brelse в случае, когда буфер процессу больше не нужен, а также при обработке прерывания от диска для высвобождения буферов, используемых при асинхронном вводе-выводе с диска и на диск (см. раздел 3.4). Ядро повышает приоритет пре- рывания работы процессора так, чтобы запретить возникновение лю- бых прерываний от диска на время работы со списком свободных бу- феров, предупреждая искажение указателей буфера в результате вложенного выполнения алгоритма brelse. Похожие последствия могут произойти, если программа обработки прерываний запустит алгоритм brelse во время выполнения процессом алгоритма getblk, поэтому ядро повышает приоритет прерывания работы процессора и в страте- гических моментах выполнения алгоритма getblk. Более подробно эти случаи мы разберем с помощью упражнений. зддддддддддддддддд© зддддддддддддддддд© Ё Ё Ёздддд© здддд©Ё здддд© Ё блок 0 модуль 4 ЁЫЫЫЫ ю╢ 28 ц© з╢ 4 цы Ё 64 Ё Ё Ё юддддыЁ Ёюдддды юдддды цддддддддддддддддд╢ Ё юдддддд© Ё Ё здддд©Ё здддд©Ё здддд© Ё блок 1 модуль 4 ЁЫЫЫЫ Ё 17 ЁЁ з╢ 5 цы з╢ 97 ц© Ё Ё юддддыЁ Ёюдддды здыюддддыЁ цддддддддддддддддд╢ юдддЁдддддддды здддддды Ё Ё здддд© Ёздддд© Ёздддд© Ё блок 2 модуль 4 ЁЫЫЫЫ Ё 98 ЁздддыЁ 50 Ё ю╢ 10 ц© Ё Ё юддддыЁ юдддды юддддыЁ цддддддддддддддддд╢ Ё Ё Ё Ё здддд©Ё здддд© здддд©Ё Ё блок 3 модуль 4 ЁЫЫЫЫз>Ё 3 цы Ё 35 Ё Ё 99 ЁЁ Ё Ё Ё юдддды юдддды юддддыЁ юддддддддддддддддды Ё Ё зддддддддддддддддд© Ё Ё Ёзаголовок списка цдддды Ё Ё Ё Ё Ёсвободных буферовЁ<ддддддддддддддддддддддддддддддддды юддддддддддддддддды (а) Поиск блока 18 - отсутствует в кеше заголовки хеш-очередей зддддддддддддддддд© зддддддддддддддддд© Ё Ё Ёздддд© здддд©Ё здддд© Ё блок 0 модуль 4 ЁЫЫЫЫ ю╢ 28 ц© з╢ 4 цы Ё 64 Ё Ё Ё юддддыЁ Ёюдддды юдддды цддддддддддддддддд╢ Ё юдддддд© Ё Ё здддд©Ё здддд©Ё здддд© Ё блок 1 модуль 4 ЁЫЫЫЫ Ё 17 ЁЁ зд>Ё 5 цы з╢ 97 ц© Ё Ё юддддыЁ Ё юдддды здыюддддыЁ цддддддддддддддддд╢ юдЁдддддддддды здддддды Ё Ё здддд© Ё здддд© Ёздддд© здддд© Ё блок 2 модуль 4 ЁЫЫЫЫ Ё 98 Ё Ё Ё 50 Ё ю╢ 10 ц© Ё 18 Ё Ё Ё юдддды Ё юдддды юддддыЁ юдддды цддддддддддддддддд╢ Ё Ё Ё Ё Ё здддд© здддд©Ё Ё блок 3 модуль 4 ЁЫЫЫЫ Ё Ё 35 Ё Ё 99 ЁЁ Ё Ё Ё юдддды юддддыЁ юддддддддддддддддды Ё Ё зддддддддддддддддд© Ё Ё Ёзаголовок списка цдддддддддддддды Ё Ё Ё Ё Ёсвободных буферовЁ<ддддддддддддддддддддддддддддддддды юддддддддддддддддды (б) Удаление первого блока из списка свободных буферов, наз- начение блока 18 Рисунок 3.7. Второй случай выделения буфера При выполнении алгоритма getblk имеет место случай 2, когда ядро просматривает хеш-очередь, в которой должен был бы находиться блок, но не находит его там. Так как блок не может быть ни в какой другой хеш-очереди, поскольку он не должен "хешироваться" в заголовки хеш-очередей другом месте, следовательно, его нет в буферном кеше. Поэтому ядро удаляет первый буфер из списка свободных буферов; этот буфер был уже выделен другому дисковому блоку и также находится в хеш-очереди. Если буфер не помечен для отложенной переписи, ядро помечает буфер занятым, удаляет его из хеш-очереди, где он находится, назначает в заголовке буфера номера устройства и блока, соответствующие данному дисковому блоку, и помещает буфер в хеш-очередь. Ядро использует буфер, не переписав информацию, которую буфер прежде хранил для другого дискового блока. Тот процесс, который будет искать прежний дисковый блок, не обнаружит его в пуле и получит для него точно таким же образом новый буфер из списка свободных буферов. Когда ядро заканчивает работу с буфером, оно освобождает буфер вышеописанным способом. На Рисунке 3.7, например, ядро ищет блок 18, но не находит его в хеш-очереди, помеченной как "блок 2 модуль 4". Поэтому ядро удаляет первый буфер из списка свободных буферов (блок 3), назначает его блоку 18 и помещает его в соответствующую хеш-очередь. Если при выполнении алгоритма getblk имеет место случай 3, ядро так же должно выделить буфер из списка свободных буферов. Однако, оно обнаруживает, что удаляемый из списка буфер был поме- чен для отложенной переписи, поэтому прежде чем использовать бу- фер ядро должно переписать его содержимое на диск. Ядро приступа- ет к асинхронной записи на диск и пытается выделить другой буфер из списка. Когда асинхронная запись заканчивается, ядро освобож- дает буфер и помещает его в начало списка свободных буферов. Бу- фер сам продвинулся от конца списка свободных буферов к началу списка. Если после асинхронной переписи ядру бы понадобилось по- местить буфер в конец списка, буфер получил бы "зеленую улицу" по всему списку свободных буферов, результат такого перемещения про- тивоположен действию алгоритма поиска буферов, к которым наиболее долго не было обращений. Например, если обратиться к Рисунку 3.8, ядро не смогло обнаружить блок 18, но когда попыталось выделить первые два буфера (по очереди) в списке свободных буферов, то оказалось, что они оба помечены для отложенной переписи. Ядро удалило их из списка, запустило операции переписи на диск в соот- ветствующие блоки, и выделило третий буфер из списка, блок 4. Да- лее ядро присвоило новые значения полям буфера "номер устройс- тва" и "номер блока" и включило буфер, получивший имя "блок 18", в новую хеш-очередь. В четвертом случае (Рисунок 3.9) ядро, работая с процессом A, не смогло найти дисковый блок в соответствующей хеш-очереди и предприняло попытку выделить из списка свободных буферов новый буфер, как в случае 2. Однако, в списке не оказалось ни одного буфера, поэтому процесс A приостановился до тех пор, пока другим процессом не будет выполнен алгоритм brelse, высвобождающий бу- фер. Планируя выполнение процесса A, ядро вынуждено снова прос- матривать хеш-очередь в поисках блока. Оно не в состоянии немед- ленно выделить буфер из списка свободных буферов, так как возможна ситуация, когда свободный буфер ожидают сразу несколько процессов и одному из них будет выделен вновь освободившийся бу- фер, на который уже нацелился процесс A. Таким образом, алгоритм поиска блока снова гарантирует, что только один буфер включает содержимое дискового блока. На Рисунке 3.10 показана конкуренция между двумя процессами за освободившийся буфер. заголовки хеш-очередей зддддддддддддддддд© зддддддддддддддддд© Ё Ё Ёздддд© здддд©Ё здддд© Ё блок 0 модуль 4 ЁЫЫЫЫ ю╢ 28 ц© з╢ 4 цы Ё 64 Ё Ё Ё юддддыЁ Ёюдддды юдддды цддддддддддддддддд╢ Ё юдддддд© Ё Ё здддд©Ё здддд©Ё здддд© Ё блок 1 модуль 4 ЁЫЫЫЫ Ё 17 ЁЁ зд╢ 5 цы з╢ 97 ц© Ё Ё юддддыЁ Ё юдддды здыюддддыЁ цддддддддддддддддд╢ юддЁотсрочкады здддддды Ё Ё здддд© Ё здддд© Ёздддд© Ё блок 2 модуль 4 ЁЫЫЫЫ Ё 98 Ёздды Ё 50 Ё ю╢ 10 ц© Ё Ё юддддыЁ юдддды юддддыЁ цддддддддддддддддд╢ Ё Ё Ё Ё здддд©Ё здддд© здддд©Ё Ё блок 3 модуль 4 ЁЫЫЫЫз>Ё 3 цы Ё 35 Ё Ё 99 ЁЁ Ё Ё Ё юдддды юдддды юддддыЁ юддддддддддддддддды Ёотсрочка Ё зддддддддддддддддд© Ё Ё Ёзаголовок списка цдддды Ё Ё Ё Ё Ёсвободных буферовЁ<ддддддддддддддддддддддддддддддддды юддддддддддддддддды (а) При поиске блока 18 в списке свободных буферов обнаружены блоки с отсроченной записью заголовки хеш-очередей зддддддддддддддддд© Ё Ё здддд© здддд© Ё блок 0 модуль 4 ЁЫЫЫЫз>Ё 28 цдддддддддддд© Ё 64 Ё Ё Ё Ё юдддды Ё юдддды цддддддддддддддддд╢ Ё Ё Ё Ё Ё здддд© здддд© Ё здддд© Ё блок 1 модуль 4 ЁЫЫЫЫЁ Ё 17 Ё Ё 5 Ё юдд>Ё 97 ц© Ё Ё Ё юдддды юдддды юддддыЁ цддддддддддддддддд╢ Ё запись здддддды Ё Ё Ё здддд© здддд© Ёздддд© здддд© Ё блок 2 модуль 4 ЁЫЫЫЫЁ Ё 98 Ё Ё 50 Ё ю╢ 10 ц© Ё 18 Ё Ё Ё Ё юдддды юдддды юддддыЁ юдддды цддддддддддддддддд╢ Ё Ё Ё Ё Ё здддд© здддд© здддд©Ё Ё блок 3 модуль 4 ЁЫЫЫЫЁ Ё 3 Ё Ё 35 Ё Ё 99 ЁЁ Ё Ё Ё юдддды юдддды юддддыЁ юддддддддддддддддды Ё запись Ё зддддддддддддддддд© Ё Ё Ёзаголовок списка цдддды Ё Ё Ё Ё Ёсвободных буферовЁ<ддддддддддддддддддддддддддддддддды юддддддддддддддддды (б) Перепись блоков 3 и 5, переназначение блока 4 на блок 18 Рисунок 3.8. Третий случай выделения буфера заголовки хеш-очередей зддддддддддддддддд© Ё Ё здддд© здддд© здддд© Ё блок 0 модуль 4 ЁЫЫЫЫ Ё 28 Ё Ё 4 Ё Ё 64 Ё Ё Ё юдддды юдддды юдддды цддддддддддддддддд╢ Ё Ё здддд© здддд© здддд© Ё блок 1 модуль 4 ЁЫЫЫЫ Ё 17 Ё Ё 5 Ё Ё 97 Ё Ё Ё юдддды юдддды юдддды цддддддддддддддддд╢ Ё Ё здддд© здддд© здддд© Ё блок 2 модуль 4 ЁЫЫЫЫ Ё 98 Ё Ё 50 Ё Ё 10 Ё Ё Ё юдддды юдддды юдддды цддддддддддддддддд╢ Ё Ё здддд© здддд© здддд© Ё блок 3 модуль 4 ЁЫЫЫЫ Ё 3 Ё Ё 35 Ё Ё 99 Ё Ё Ё юдддды юдддды юдддды юддддддддддддддддды зддддддддддддддддд© Ёзаголовок списка цддддддддд© Ё Ё Ё Ёсвободных буферовЁ<дддддддды юддддддддддддддддды Поиск блока 18, список свободных буферов пуст Рисунок 3.9. Четвертый случай выделения буфера Последний случай (Рисунок 3.11) наиболее сложный, поскольку он связан с комплексом взаимоотношений между несколькими процес- сами. Предположим, что ядро, работая с процессом A, ведет поиск дискового блока и выделяет буфер, но приостанавливает выполнение процесса перед освобождением буфера. Например, если процесс A по- пытается считать дисковый блок и выделить буфер, как в случае 2, то он приостановится до момента завершения передачи данных с дис- ка. Предположим, что пока процесс A приостановлен, ядро активизи- рует второй процесс, B, который пытается обратиться к дисковому блоку, чей буфер был только что заблокирован процессом A. Процесс B (случай 5) обнаружит этот захваченный блок в хеш-очереди. Так как использовать захваченный буфер не разрешается и, кроме того, нельзя выделить для одного и того же дискового блока второй бу- фер, процесс B помечает буфер как "запрошенный" и затем приоста- навливается до того момента, когда процесс A освободит данный бу- фер. В конце концов процесс A освобождает буфер и замечает, что он запрошен. Тогда процесс A "будит" все процессы, приостановленные по событию "буфер становится свободным", включая и процесс B. Когда же ядро вновь запустит на выполнение процесс B, процесс B должен будет убедиться в том, что буфер свободен. Возможно, что третий процесс, C, ждал освобождения этого же буфера, и ядро зап- ланировало активизацию процесса C раньше B; при этом процесс C мог приостановиться и оставить буфер заблокированным. Следова- тельно, процесс B должен проверить то, что блок действительно свободен. Процесс B также должен убедиться в том, что в буфере содержится первоначально затребованный дисковый блок, поскольку процесс C мог выделить данный буфер другому блоку, как в случае 2. При выполнении процесса B может обнаружиться, что он ждал ос- вобождения буфера не с тем содержимым, поэтому процессу B придет- ся вновь заниматься поисками блока. Если же его настроить на ав- томатическое выделение буфера из списка свободных буферов, он может упустить из виду возможность того, что какой-либо другой процесс уже выделил буфер для данного блока. Процесс A Процесс B зддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё Не может найти блок b Ы Ё в хеш-очереди Ы Ё Ы Ё Список свободных буфе- Ы Ё ров пуст Ы Ё Ы Ё Процесс приостановлен Ы Ё Ы Не может найти блок b Ё Ы в хеш-очереди Ё Ы Ё Ы Список свободных буфе- Ё Ы ров пуст Ё Ы Ё Ы Процесс приостановлен Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё здддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё Ё Некоторый процесс освобождает буфер: операция brelse Ё Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддды Ё Ы Выбирает буфер из Ё Ы списка свободных буферов Ё Ы Ё Ы Назначает этот буфер Ё Ы блоку b Ё Ы v Время Рисунок 3.10. Состязание за свободный буфер В конце концов, процесс B найдет этот блок, при необходимости выбрав новый буфер из списка свободных буферов, как в случае 2. Пусть некоторый процесс, осуществляя поиск блока 99 (Рисунок 3.11), обнаружил этот блок в хеш-очереди, однако он оказался за- нятым. Процесс приостанавливается до момента освобождения блока, после чего он запускает весь алгоритм с самого начала. На Рисунке 3.12 показано содержимое занятого буфера. заголовки хеш-очередей зддддддддддддддддд© зддддддддддддддддд© Ё Ё Ёздддд© здддд©Ё здддд© Ё блок 0 модуль 4 ЁЫЫЫЫ ю╢ 28 ц© з╢ 4 цы Ё 64 Ё Ё Ё юддддыЁ Ёюдддды юдддды цддддддддддддддддд╢ Ё юдддддд© Ё Ё здддд©Ё здддд©Ё здддд© Ё блок 1 модуль 4 ЁЫЫЫЫ Ё 17 ЁЁ з╢ 5 цы з╢ 97 ц© Ё Ё юддддыЁ Ёюдддды здыюддддыЁ цддддддддддддддддд╢ юдддЁдддддддды здддддды Ё Ё здддд© Ёздддд© Ёздддд© Ё блок 2 модуль 4 ЁЫЫЫЫ Ё 98 ЁздддыЁ 50 Ё ю╢ 10 ц© Ё Ё юддддыЁ юдддды юддддыЁ цддддддддддддддддд╢ Ё Ё Ё Ё здддд©Ё здддд© здддд©Ё Ё блок 3 модуль 4 ЁЫЫЫЫз>Ё 3 цы Ё 35 Ё Ё 99 ЁЁ Ё Ё Ё юдддды юдддды юддддыЁ юддддддддддддддддды Ё занятЁ зддддддддддддддддд© Ё Ё Ёзаголовок списка цдддды Ё Ё Ё Ё Ёсвободных буферовЁ<ддддддддддддддддддддддддддддддддды юддддддддддддддддды Поиск блока 99, блок занят Рисунок 3.11. Пятый случай выделения буфера Алгоритм выделения буфера должен быть надежным; процессы не должны "засыпать" навсегда и рано или поздно им нужно выделить буфер. Ядро гарантирует такое положение, при котором все процес- сы, ожидающие выделения буфера, продолжат свое выполнение, благо- даря тому, что ядро распределяет буферы во время обработки обра- щений к операционной системе и освобождает их перед возвратом управления процессам (***). (***) Исключением является системная операция mount, которая зах- ватывает буфер до тех пор, пока не будет исполнена операция umount. Это исключение не является существенным, поскольку общее количество буферов намного превышает число активных монтированных файловых систем. В режиме задачи процессы непосредственно не контролируют выделение буферов ядром системы, поэтому они не могут намеренно "захватывать" буферы. Ядро теряет контроль над буфером только тогда, когда ждет завершения операции ввода-вывода между буфером и диском. Было задумано так, что если дисковод испорчен, он не может прерывать работу центрального процессора, и тогда ядро никогда не освободит буфер. Дисковод должен следить за работой аппаратных средств в таких случаях и возвращать ядру код ошибки, сообщая о плохой работе диска. Короче говоря, ядро может гарантировать, что процессы, приостановленные в ожидании буфера, в конце концов возобновят свое выполнение. Можно также представить себе ситуацию, когда процесс "зависа- ет" в ожидании получения доступа к буферу. В четвертом случае, например, если несколько процессов приостанавливаются, ожидая ос- вобождения буфера, ядро не гарантирует, что они получат доступ к буферу в той очередности, в которой они запросили доступ. Процесс может приостановить и возобновить свое выполнение, когда буфер станет свободным, только для того, чтобы приостановиться вновь из -за того, что другой процесс получил управление над буфером пер- вым. Теоретически, так может продолжаться вечно, но практически такой проблемы не возникает в связи с тем, что в системе обычно заложено большое количество буферов. Процесс A Процесс B Процесс C зддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё Буфер выделен блоку b Ы Ы Ё Ы Ы Ё Буфер заблокирован Ы Ы Ё Ы Ы Ё Начат ввод-вывод Ы Ы Ё Ы Ы Ё Приостановлен до Ы Ы Ё завершения ввода-вывода Ы Ы Ё Ы Ы Ы Ё Ы Поиск блока b Ы Ё Ы в хеш-очереди Ы Ё Ы Ы Ё Ы Буфер заблокирован, Ы Ё Ы приостановка Ы Ё Ы Ы Ы Ё Ы Ы Приостановлен Ё Ы Ы в ожидании освобождения Ё Ы Ы любого буфера Ё Ы Ы (случай 4) Ё зддддддддддддддддддддддддддд© Ы Ы Ё Ё Ввод-вывод закончен, Ё Ы Ы Ё Ё выполнение возобновляется Ё Ы Ы Ё юддддддддддддддддддддддддддды Ы Ы Ё brelse(): возобновляются Ы Ы Ё другие процессы Ы Ы Ё Ы Ы Получает буфер, Ё Ы Ы первоначально Ё Ы Ы назначенный Ё Ы Ы блоку b Ё Ы Ы Ё Ы Ы Переназначение Ё Ы Ы буфера блоку b' Ё Ы Буфер не содержит Ы Ё Ы блок b Ы Ё Ы Ы Ё Ы Поиск начинается Ы Ё Ы снова Ы Ё Время v Рисунок 3.12. Состязание за свободный буфер 3.4 ЧТЕНИЕ И ЗАПИСЬ ДИСКОВЫХ БЛОКОВ Теперь, когда алгоритм выделения буферов нами уже рассмотрен, будет легче понять процедуру чтения и записи дисковых блоков. Чтобы считать дисковый блок (Рисунок 3.13), процесс использует алгоритм getblk для поиска блока в буферном кеше. Если он там, ядро может возвратить его немедленно без физического считывания блока с диска. Если блок в кеше отсутствует, ядро приказывает дисководу "запланировать" запрос на чтение и приостанавливает ра- боту, ожидая завершения ввода-вывода. Дисковод извещает контрол- лер диска о том, что он собирается считать информацию, и контрол- лер тогда передает информацию в буфер. Наконец, дисковый контроллер прерывает работу процессора, сообщая о завершении опе- рации вода-вывода, и программа обработки прерываний от диска во- зобновляет выполнение приостановленного процесса; теперь содержи- мое дискового блока находится в буфере. Модули, запросившие информацию данного блока, получают ее; когда буфер им уже не пот- ребуется, они освободят его для того, чтобы другие процессы полу- чили к нему доступ. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм bread /* чтение блока */ Ё Ё входная информация: номер блока в файловой системе Ё Ё выходная информация: буфер, содержащий данные Ё Ё { Ё Ё получить буфер для блока (алгоритм getblk); Ё Ё если (данные в буфере правильные) Ё Ё возвратить буфер; Ё Ё приступить к чтению с диска; Ё Ё приостановиться (до завершения операции чтения); Ё Ё возвратить (буфер); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 3.13. Алгоритм чтения дискового блока В главе 5 будет показано, как модули более высокого уровня (такие как подсистема управления файлами) могут предчувствовать потребность во втором дисковом блоке, когда процесс читает инфор- мацию из файла последовательно. Эти модули формируют запрос на асинхронное выполнение второй операции ввода-вывода, надеясь на то, что информация уже будет в памяти, когда вдруг возникнет не- обходимость в ней, и тем самым повышая быстродействие системы. Для этого ядро выполняет алгоритм чтения блока с продвижением breada (Рисунок 3.14). Ядро проверяет, находится ли в кеше первый блок, и если его там нет, приказывает дисководу считать этот блок. Если в буферном кеше отсутствует и второй блок, ядро дает команду дисководу считать асинхронно и его. Затем процесс приос- танавливается, ожидая завершения операции ввода-вывода над первым блоком. Когда выполнение процесса возобновляется, он возвращает буфер первому блоку и не обращает внимание на то, когда завершит- ся операция ввода-вывода для второго блока. После завершения этой операции контроллер диска прерывает работу системы; программа об- работки прерываний узнает о том, что ввод-вывод выполнялся асинх- ронно, и освобождает буфер (алгоритм brelse). Если бы она не ос- вободила буфер, буфер остался бы заблокированным и по этой причине недоступным для всех процессов. Невозможно заранее разб- локировать буфер, так как операция ввода-вывода, связанная с бу- фером, активна и, следовательно, содержимое буфера еще не адек- ватно. Позже, если процесс пожелает считать второй блок, он обнаружит его в буферном кеше, поскольку к тому времени операция ввода-вывода закончится. Если же, в начале выполнения алгоритма breada, первый блок обнаружился в буферном кеше, ядро тут же про- веряет, находится там же и второй блок, и продолжает работу по только что описанной схеме. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм breada /* чтение блока с продвижением */ Ё Ё входная информация: (1) в файловой системе номер блока для Ё Ё немедленного считывания Ё Ё (2) в файловой системе номер блока для Ё Ё асинхронного считывания Ё Ё выходная информация: буфер с данными, считанными немедленноЁ Ё { Ё Ё если (первый блок отсутствует в кеше) Ё Ё { Ё Ё получить буфер для первого блока (алгоритм getblk);Ё Ё если (данные в буфере неверные) Ё Ё приступить к чтению с диска; Ё Ё } Ё Ё если (второй блок отсутствует в кеше) Ё Ё { Ё Ё получить буфер для второго блока (алгоритм getblk);Ё Ё если (данные в буфере верные) Ё Ё освободить буфер (алгоритм brelse); Ё Ё в противном случае Ё Ё приступить к чтению с диска; Ё Ё } Ё Ё если (первый блок первоначально находился в кеше) Ё Ё { Ё Ё считать первый блок (алгоритм bread); Ё Ё возвратить буфер; Ё Ё } Ё Ё приостановиться (до того момента, когда первый буфер Ё Ё будет содержать верные данные); Ё Ё возвратить буфер; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 3.14. Алгоритм чтения блока с продвижением Алгоритм записи содержимого буфера в дисковый блок (Рисунок 3.15) похож на алгоритм чтения. Ядро информирует дисковод о том, что есть буфер, содержимое которого должно быть выведено, и дис- ковод планирует операцию ввода-вывода блока. Если запись произво- дится синхронно, вызывающий процесс приостанавливается, ожидая ее завершения и освобождая буфер в момент возобновления своего вы- полнения. Если запись производится асинхронно, ядро запускает операцию записи на диск, но не ждет ее завершения. Ядро освободит буфер, когда завершится ввод-вывод. Могут возникнуть ситуации, и это будет показано в следующих двух главах, когда ядро не записывает данные немедленно на диск. Если запись "откладывается", ядро соответствующим образом помеча- ет буфер, освобождая его по алгоритму brelse, и продолжает работу без планирования ввода-вывода. Ядро записывает блок на диск перед тем, как другой процесс сможет переназначить буфер другому блоку, как показано в алгоритме getblk (случай 3). Между тем, ядро наде- ется на то, что процесс получает доступ до того, как буфер будет переписан на диск; если этот процесс впоследствии изменит содер- жимое буфера, ядро произведет дополнительную операцию по сохране- нию изменений на диске. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм bwrite /* запись блока */ Ё Ё входная информация: буфер Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё приступить к записи на диск; Ё Ё если (ввод-вывод синхронный) Ё Ё { Ё Ё приостановиться (до завершения ввода-вывода); Ё Ё освободить буфер (алгоритм brelse); Ё Ё } Ё Ё в противном случае если (буфер помечен для отложенной Ё Ё записи) Ё Ё пометить буфер для последующего размещения в Ё Ё "голове" списка свободных буферов; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 3.15. Алгоритм записи дискового блока Отложенная запись отличается от асинхронной записи. Выполняя асинхронную запись, ядро запускает дисковую операцию немедленно, но не дожидается ее завершения. Что касается отложенной записи, ядро отдаляет момент физической переписи на диск насколько воз- можно; затем по алгоритму getblk (случай 3) оно помечает буфер как "старый" и записывает блок на диск асинхронно. После этого контроллер диска прерывает работу системы и освобождает буфер, используя алгоритм brelse; буфер помещается в "голову" списка свободных буферов, поскольку он имеет пометку "старый". Благодаря наличию двух выполняющихся асинхронно операций ввода-вывода - чтения блока с продвижением и отложенной записи - ядро может за- пускать программу brelse из программы обработки прерываний. Сле- довательно, ядро вынуждено препятствовать возникновению прерыва- ний при выполнении любой процедуры, работающей со списком свободных буферов, поскольку brelse помещает буферы в этот спи- сок. 3.5 ПРЕИМУЩЕСТВА И НЕУДОБСТВА БУФЕРНОГО КЕША Использование буферного кеша имеет, с одной стороны, несколь- ко преимуществ и, с другой стороны, некоторые неудобства. * Использование буферов позволяет внести единообразие в проце- дуру обращения к диску, поскольку ядру нет необходимости знать причину ввода-вывода. Вместо этого, ядро копирует дан- ные в буфер и из буфера, невзирая на то, являются ли данные частью файла, индекса или суперблока. Буферизация ввода-выво- да с диска повышает модульность разработки программ, посколь- ку те составные части ядра, которые занимаются вводом-выводом на диск, имеют один интерфейс на все случаи. Короче говоря, упрощается проектирование системы. * Система не накладывает никаких ограничений на выравнивание информации пользовательскими процессами, выполняющими ввод-вывод, поскольку ядро производит внутреннее выравнивание информации. В различных аппаратных реализациях часто требует- ся выравнивать информацию для ввода-вывода с диска определен- ным образом, т.е. производить к примеру двухбайтное или четырехбайтное выравнивание данных в памяти. Без механизма буферизации программистам пришлось бы заботиться самим о пра- вильном выравнивании данных. По этой причине на машинах с ог- раниченными возможностями в выравнивании адресов возникает большое количество ошибок программирования и, кроме того, становится проблемой перенос программ в операционную среду UNIX. Копируя информацию из пользовательских буферов в сис- темные буферы (и обратно), ядро системы устраняет необходи- мость в специальном выравнивании пользовательских буферов, делая пользовательские программы более простыми и мобильными. * Благодаря использованию буферного кеша, сокращается объем дискового трафика и время реакции и повышается общая произво- дительность системы. Процессы, считывающие данные из файловой системы, могут обнаружить информационные блоки в кеше и им не придется прибегать ко вводу-выводу с диска. Ядро часто приме- няет "отложенную запись", чтобы избежать лишних обращений к диску, оставляя блок в буферном кеше и надеясь на попадание блока в кеш. Очевидно, что шансы на такое попадание выше в системах с большим количеством буферов. Тем не менее, число буферов, которые можно заложить в системе, ограничивается объемом памяти, доступной выполняющимся процессам: если под буферы задействовать слишком много памяти, то система будет работать медленнее в связи с тем, что ей придется заниматься подкачкой и замещением выполняющихся процессов. * Алгоритмы буферизации помогают поддерживать целостность фай- ловой системы, так как они сохраняют общий, первоначальный и единственный образ дисковых блоков, содержащихся в кеше. Если два процесса одновременно попытаются обратиться к одному и тому же дисковому блоку, алгоритмы буферизации (например, getblk) параллельный доступ преобразуют в последовательный, предотвращая разрушение данных. * Сокращение дискового трафика является важным преимуществом с точки зрения обеспечения хорошей производительности или быст- рой реакции системы, однако стратегия кеширования также имеет некоторые неудобства. Так как ядро в случае отложенной записи не переписывает данные на диск немедленно, такая система уяз- вима для сбоев, которые оставляют дисковые данные в некор- ректном виде. Хотя в последних версиях системы и сокращен ущерб, наносимый катастрофическими сбоями, основная проблема остается: пользователь, запрашивающий выполнение операции за- писи, никогда не знает, в какой момент данные завершат свой путь на диск (****). * Использование буферного кеша требует дополнительного копиро- вания информации при ее считывании и записи пользовательскими процессами. Процесс, записывающий данные, передает их ядру и ядро копирует данные на диск; процесс, считывающий данные, получает их от ядра, которое читает данные с диска. При пере- даче большого количества данных дополнительное копирование отрицательным образом отражается на производительности систе- мы, однако при передаче небольших объемов данных производи- тельность повышается, поскольку ядро буферизует данные (ис- пользуя алгоритм getblk и отложенную запись) до тех пор, пока это представляется эффективным с точки зрения экономии време- ни работы с диском. 3.6 ВЫВОДЫ В данной главе была рассмотрена структура буферного кеша и различные способы, которыми ядро размещает блоки в кеше. В алго- ритмах буферизации сочетаются несколько простых идей, которые в сумме обеспечивают работу механизма кеширования. При работе с блоками в буферном кеше ядро использует алгоритм замены буферов, к которым наиболее долго не было обращений, предполагая, что к ддддддддддддддддддддддддддддддддддддддд (****) Стандартный набор операций ввода-вывода в программах на языке Си включает операцию fflush. Эта функция занимается подкачиванием данных из буферов в пользовательском адрес- ном пространстве в рабочую область ядра. Тем не менее пользователю не известно, когда ядро запишет данные на диск. блокам, к которым недавно было обращение, вероятно, вскоре обра- тятся снова. Очередность, в которой буферы появляются в списке свободных буферов, соответствует очередности их предыдущего ис- пользования. Остальные алгоритмы обслуживания буферов, типа "пер- вым пришел - первым вышел" и замещения редко используемых, либо являются более сложными в реализации, либо снижают процент попа- дания в кеш. Использование функции хеширования и хеш-очередей да- ет ядру возможность ускорить поиск заданных блоков, а использова- ние двунаправленных указателей в списках облегчает исключение бу- феров. Ядро идентифицирует нужный ему блок по номеру логического ус- тройства и номеру блока. Алгоритм getblk просматривает буферный кеш в поисках блока и, если буфер присутствует и свободен, блоки- рует буфер и возвращает его. Если буфер заблокирован, обративший- ся к нему процесс приостанавливается до тех пор, пока буфер не освободится. Механизм блокирования гарантирует, что только один процесс в каждый момент времени работает с буфером. Если в кеше блок отсутствует, ядро назначает блоку свободный буфер, блокирует и возвращает его. Алгоритм bread выделяет блоку буфер и при необ- ходимости читает туда информацию. Алгоритм bwrite копирует инфор- мацию в предварительно выделенный буфер. Если при выполнении ука- занных алгоритмов ядро не увидит необходимости в немедленном копировании данных на диск, оно пометит буфер для "отложенной за- писи", чтобы избежать излишнего ввода-вывода. К сожалению, проце- дура откладывания записи сопровождается тем, что процесс никогда не уверен, в какой момент данные физически попадают на диск. Если ядро записывает данные на диск синхронно, оно поручает драйверу диска передать блок файловой системе и ждет прерывания, сообщаю- щего об окончании ввода-вывода. Существует множество способов использования ядром буферного кеша. Посредством буферного кеша ядро обеспечивает обмен данными между прикладными программами и файловой системой, передачу до- полнительной системной информации, например, индексов, между ал- горитмами ядра и файловой системой. Ядро также использует буфер- ный кеш, когда читает программы в память для выполнения. В следующих главах будет рассмотрено множество алгоритмов, исполь- зующих процедуры, описанные в данной главе. Другие алгоритмы, ко- торые кешируют индексы и страницы памяти, также используют прие- мы, похожие на те, что описаны для буферного кеша. 3.7 УПРАЖНЕНИЯ 1. Рассмотрим функцию хеширования применительно к Рисунку 3.3. Наилучшей функцией хеширования является та, которая единым об- разом распределяет блоки между хеш-очередями. Что Вы могли бы предложить в качестве оптимальной функции хеширования ? Должна ли эта функция в своих расчетах использовать логический номер устройства ? 2. В алгоритме getblk, если ядро удаляет буфер из списка свобод- ных буферов, оно должно повысить приоритет прерывания работы процессора так, чтобы блокировать прерывания до проверки спис- ка. Почему ? *3. В алгоритме getblk ядро должно повысить приоритет прерывания работы процессора так, чтобы блокировать прерывания до провер- ки занятости блока. (Это не показано в тексте.) Почему ? 4. В алгоритме brelse ядро помещает буфер в "голову" списка сво- бодных буферов, если содержимое буфера неверно. Если содержи- мое буфера неверно, должен ли буфер появиться в хеш-очереди ? 5. Предположим, что ядро выполняет отложенную запись блока. Что произойдет, когда другой процесс выберет этот блок из его хеш- очереди ? Из списка свободных буферов ? *6. Если несколько процессов оспаривают буфер, ядро гарантирует, что ни один из них не приостановится навсегда, но не гаранти- рует, что процесс не "зависнет" и дождется получения буфера. Переделайте алгоритм getblk так, чтобы процессу было в конеч- ном итоге гарантировано получение буфера. 7. Переделайте алгоритмы getblk и brelse так, чтобы ядро следова- ло не схеме замещения буферов, к которым наиболее долго не было обращений, а схеме "первым пришел - первым вышел". Повто- рите то же самое со схемой замещения редко используемых буфе- ров. 8. Опишите ситуацию в алгоритме bread, когда информация в буфере уже верна. *9. Опишите различные ситуации, встречающиеся в алгоритме breada. Что произойдет в случае следующего выполнения алгоритма bread или breada, когда текущий блок прочитан с продвижением ? В ал- горитме breada, если первый или второй блок отсутствует в ке- ше, в дальнейшем при проверке правильности содержимого буфера предполагается, что блок мог быть в буферном пуле. Как это мо- жет быть ? 10. Опишите алгоритм, запрашивающий и получающий любой свободный буфер из буферного пула. Сравните этот алгоритм с getblk. 11. В различных системных операциях, таких как umount и sync (глава 5), требуется, чтобы ядро перекачивало на диск содер- жимое всех буферов, которые помечены для "отложенной записи" в данной файловой системе. Опишите алгоритм, реализующий пе- рекачку буферов. Что произойдет с очередностью расположения буферов в списке свободных буферов в результате этой опера- ции ? Как ядро может гарантировать, что ни один другой про- цесс не подберется к буферу с пометкой "отложенная запись" и не сможет переписать его содержимое в файловую систему, пока процесс перекачки приостановлен в ожидании завершения опера- ции ввода-вывода ? 12. Определим время реакции системы как среднее время выполнения системного вызова. Определим пропускную способность системы как количество процессов, которые система может выполнять в данный период времени. Объясните, как буферный кеш может способствовать повышению реакции системы. Способствует ли он с неизбежностью увеличению пропускной способности системы ? ВНУТРЕННЕЕ ПРЕДСТАВЛЕНИЕ ФАЙЛОВ Как уже было замечено в главе 2, каждый файл в системе UNIX имеет уникальный индекс. Индекс содержит информацию, необходимую любому процессу для того, чтобы обратиться к файлу, например, права собственности на файл, права доступа к файлу, размер файла и расположение данных файла в файловой системе. Процессы обраща- ются к файлам, используя четко определенный набор системных вызо- вов и идентифицируя файл строкой символов, выступающих в качестве составного имени файла. Каждое составное имя однозначно определя- ет файл, благодаря чему ядро системы преобразует это имя в индекс файла. Эта глава посвящена описанию внутренней структуры файлов в операционной системе UNIX, в следующей же главе рассматриваются обращения к операционной системе, связанные с обработкой файлов. Раздел 4.1 касается индекса и работы с ним ядра, раздел 4.2 - внутренней структуры обычных файлов и некоторых моментов, связан- ных с чтением и записью ядром информации файлов. В разделе 4.3 исследуется строение каталогов - структур данных, позволяющих яд- ру организовывать файловую систему в виде иерархии файлов, раздел 4.4 содержит алгоритм преобразования имен пользовательских файлов в индексы. В разделе 4.5 дается структура суперблока, а в разде- лах 4.6 и 4.7 представлены алгоритмы назначения файлам дисковых индексов и дисковых блоков. Наконец, в разделе 4.8 идет речь о других типах файлов в системе, а именно о каналах и файлах уст- ройств. Алгоритмы, описанные в этой главе, уровнем выше по сравнению с алгоритмами управления буферным кешем, рассмотренными в преды- дущей главе (Рисунок 4.1). Алгоритм iget возвращает последний из идентифицированных индексов с возможностью считывания его с дис- ка, используя буферный кеш, а алгоритм iput освобождает индекс. Алгоритм bmap устанавливает параметры ядра, связанные с обращени- ем к файлу. Алгоритм namei преобразует составное имя пользова- тельского файла в имя индекса, используя алгоритмы iget, iput и Алгоритмы работы с файловой системой на нижнем уровне зддддддддддддддддддддбддддддддддддддддддбддддддддддддддддд© Ё namei Ё Ё Ё цдддддддддддддддддддд╢ alloc free Ё ialloc ifree Ё Ё iget iput bmap Ё Ё Ё цддддддддддддддддддддаддддддддддддддддддаддддддддддддддддд╢ цддддддддддддддддддддддддддддддддддддддддддддддддддддддддд╢ Ё алгоритмы работы с буферами Ё цддддддддддддддддддддддддддддддддддддддддддддддддддддддддд╢ Ё getblk brelse bread breada bwrite Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 4.1. Алгоритмы файловой системы bmap. Алгоритмы alloc и free выделяют и освобождают дисковые бло- ки для файлов, алгоритмы ialloc и ifree назначают и освобождают для файлов индексы. 4.1 ИНДЕКСЫ 4.1.1 Определение Индексы существуют на диске в статической форме и ядро считы- вает их в память прежде, чем начать с ними работать. Дисковые ин- дексы включают в себя следующие поля: * Идентификатор владельца файла. Права собственности разделены между индивидуальным владельцем и "групповым" и тем самым по- могают определить круг пользователей, имеющих права доступа к файлу. Суперпользователь имеет право доступа ко всем файлам в системе. * Тип файла. Файл может быть файлом обычного типа, каталогом, специальным файлом, соответствующим устройствам ввода-вывода символами или блоками, а также абстрактным файлом канала (ор- ганизующим обслуживание запросов в порядке поступления, "пер- вым пришел - первым вышел"). * Права доступа к файлу. Система разграничивает права доступа к файлу для трех классов пользователей: индивидуального вла- дельца файла, группового владельца и прочих пользователей; каждому классу выделены определенные права на чтение, запись и исполнение файла, которые устанавливаются индивидуально. Поскольку каталоги как файлы не могут быть исполнены, разре- шение на исполнение в данном случае интерпретируется как пра- во производить поиск в каталоге по имени файла. * Календарные сведения, характеризующие работу с файлом: время внесения последних изменений в файл, время последнего обраще- ния к файлу, время внесения последних изменений в индекс. * Число указателей на файл, означающее количество имен, исполь- зуемых при поиске файла в иерархии каталогов. Указатели на файл подробно рассматриваются в главе 5. * Таблица адресов на диске, в которых располагается информация файла. Хотя пользователи трактуют информацию в файле как ло- гический поток байтов, ядро располагает эти данные в несопри- касающихся дисковых блоках. Дисковые блоки, содержащие инфор- мацию файла, указываются в индексе. * Размер файла. Данные в файле адресуются с помощью смещения в байтах относительно начала файла, начиная со смещения, равно- го 0, поэтому размер файла в байтах на 1 больше максимального смещения. Например, если пользователь создает файл и записы- вает только 1 байт информации по адресу со смещением 1000 от начала файла, размер файла составит 1001 байт. В индексе отсутствует составное имя файла, необходимое для осуществления доступа к файлу. зддддддддддддддддддддддддддддддддддддддд© Ё владелец mjb Ё Ё группа os Ё Ё тип - обычный файл Ё Ё права доступа rwxr-xr-x Ё Ё последнее обращение 23 Окт 1984 13:45 Ё Ё последнее изменение 22 Окт 1984 10:30 Ё Ё коррекция индекса 23 Окт 1984 13:30 Ё Ё размер 6030 байт Ё Ё дисковые адреса Ё юддддддддддддддддддддддддддддддддддддддды Рисунок 4.2. Пример дискового индекса На Рисунке 4.2 показан дисковый индекс некоторого файла. Этот индекс принадлежит обычному файлу, владелец которого - "mjb" и размер которого - 6030 байт. Система разрешает пользователю "mjb" производить чтение, запись и исполнение файла; членам груп- пы "os" и всем остальным пользователям разрешается только читать или исполнять файл, но не записывать в него данные. Последний раз файл был прочитан 23 октября 1984 года в 13:45, запись последний раз производилась 22 октября 1984 года в 10:30. Индекс изменялся последний раз 23 октября 1984 года в 13:30, хотя никакая информа- ция в это время в файл не записывалась. Ядро кодирует все вышепе- речисленные данные в индексе. Обратите внимание на различие в за- писи на диск содержимого индекса и содержимого файла. Содержимое файла меняется только тогда, когда в файл производится запись. Содержимое индекса меняется как при изменении содержимого файла, так и при изменении владельца файла, прав доступа и набора указа- телей. Изменение содержимого файла автоматически вызывает коррек- цию индекса, однако коррекция индекса еще не означает изменения содержимого файла. Копия индекса в памяти, кроме полей дискового индекса, вклю- чает в себя и следующие поля: * Состояние индекса в памяти, отражающее - заблокирован ли индекс, - ждет ли снятия блокировки с индекса какой-либо процесс, - отличается ли представление индекса в памяти от своей дис- ковой копии в результате изменения содержимого индекса, - отличается ли представление индекса в памяти от своей дис- ковой копии в результате изменения содержимого файла, - находится ли файл в верхней точке (см. раздел 5.15). * Логический номер устройства файловой системы, содержащей файл. * Номер индекса. Так как индексы на диске хранятся в линейном массиве (см. раздел 2.2.1), ядро идентифицирует номер диско- вого индекса по его местоположению в массиве. В дисковом ин- дексе это поле не нужно. * Указатели на другие индексы в памяти. Ядро связывает индексы в хеш-очереди и включает их в список свободных индексов по- добно тому, как связывает буферы в буферные хеш-очереди и включает их в список свободных буферов. Хеш-очередь идентифи- цируется в соответствии с логическим номером устройства и но- мером индекса. Ядро может располагать в памяти не более одной копии данного дискового индекса, но индексы могут находиться одновременно как в хеш-очереди, так и в списке свободных ин- дексов. * Счетчик ссылок, означающий количество активных экземпляров файла (таких, которые открыты). Многие поля в копии индекса, с которой ядро работает в памя- ти, аналогичны полям в заголовке буфера, и управление индексами похоже на управление буферами. Индекс так же блокируется, в ре- зультате чего другим процессам запрещается работа с ним; эти про- цессы устанавливают в индексе специальный флаг, возвещающий о том, что выполнение обратившихся к индексу процессов следует во- зобновить, как только блокировка будет снята. Установкой других флагов ядро отмечает противоречия между дисковым индексом и его копией в памяти. Когда ядру нужно будет записать изменения в файл или индекс, ядро перепишет копию индекса из памяти на диск только после проверки этих флагов. Наиболее разительным различием между копией индекса в памяти и заголовком буфера является наличие счетчика ссылок, подсчитыва- ющего количество активных экземпляров файла. Индекс активен, ког- да процесс выделяет его, например, при открытии файла. Индекс на- ходится в списке свободных индексов, только если значение его счетчика ссылок равно 0, и это значит, что ядро может переназна- чить свободный индекс в памяти другому дисковому индексу. Таким образом, список свободных индексов выступает в роли кеша для не- активных индексов. Если процесс пытается обратиться к файлу, чей индекс в этот момент отсутствует в индексном пуле, ядро переназ- начает свободный индекс из списка для использования этим процес- сом. С другой стороны, у буфера нет счетчика ссылок; он находится в списке свободных буферов тогда и только тогда, когда он разбло- кирован. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм iget Ё Ё входная информация: номер индекса в файловой системе Ё Ё выходная информация: заблокированный индекс Ё Ё { Ё Ё выполнить Ё Ё { Ё Ё если (индекс в индексном кеше) Ё Ё { Ё Ё если (индекс заблокирован) Ё Ё { Ё Ё приостановиться (до освобождения индекса); Ё Ё продолжить; /* цикл с условием продолжения */ Ё Ё } Ё Ё /* специальная обработка для точек монтирования Ё Ё (глава 5) */ Ё Ё если (индекс в списке свободных индексов) Ё Ё убрать из списка свободных индексов; Ё Ё увеличить счетчик ссылок для индекса; Ё Ё возвратить (индекс); Ё Ё } Ё Ё /* индекс отсутствует в индексном кеше */ Ё Ё если (список свободных индексов пуст) Ё Ё возвратить (ошибку); Ё Ё убрать новый индекс из списка свободных индексов; Ё Ё сбросить номер индекса и файловой системы; Ё Ё убрать индекс из старой хеш-очереди, поместить в новую;Ё Ё считать индекс с диска (алгоритм bread); Ё Ё инициализировать индекс (например, установив счетчик Ё Ё ссылок в 1); Ё Ё возвратить (индекс); Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 4.3. Алгоритм выделения индексов в памяти 4.1.2 Обращение к индексам Ядро идентифицирует индексы по имени файловой системы и номе- ру индекса и выделяет индексы в памяти по запросам соответствую- щих алгоритмов. Алгоритм iget назначает индексу место для копии в памяти (Рисунок 4.3); он почти идентичен алгоритму getblk для по- иска дискового блока в буферном кеше. Ядро преобразует номера ус- тройства и индекса в имя хеш-очереди и просматривает эту хеш-оче- редь в поисках индекса. Если индекс не обнаружен, ядро выделяет его из списка свободных индексов и блокирует его. Затем ядро го- товится к чтению с диска в память индекса, к которому оно обраща- ется. Ядро уже знает номера индекса и логического устройства и вычисляет номер логического блока на диске, содержащего индекс, с учетом того, сколько дисковых индексов помещается в одном диско- вом блоке. Вычисления производятся по формуле номер блока = ((номер индекса - 1) / число индексов в блоке) + + начальный блок в списке индексов где операция деления возвращает целую часть частного. Например, предположим, что блок 2 является начальным в списке индексов и что в каждом блоке помещаются 8 индексов, тогда индекс с номером 8 находится в блоке 2, а индекс с номером 9 - в блоке 3. Если же в дисковом блоке помещаются 16 индексов, тогда индексы с номерами 8 и 9 располагаются в дисковом блоке с номером 2, а индекс с но- мером 17 является первым индексом в дисковом блоке 3. Если ядро знает номера устройства и дискового блока, оно чи- тает блок, используя алгоритм bread (глава 2), затем вычисляет смещение индекса в байтах внутри блока по формуле: ((номер индекса - 1) модуль (число индексов в блоке)) * * размер дискового индекса Например, если каждый дисковый индекс занимает 64 байта и в блоке помещаются 8 индексов, тогда индекс с номером 8 имеет адрес со смещением 448 байт от начала дискового блока. Ядро убирает индекс в памяти из списка свободных индексов, помещает его в соответс- твующую хеш-очередь и устанавливает значение счетчика ссылок рав- ным 1. Ядро переписывает поля типа файла и владельца файла, уста- новки прав доступа, число указателей на файл, размер файла и таблицу адресов из дискового индекса в память и возвращает забло- кированный в памяти индекс. Ядро манипулирует с блокировкой индекса и счетчиком ссылок независимо один от другого. Блокировка - это установка, которая действует на время выполнения системного вызова и имеет целью запретить другим процессам обращаться к индексу пока тот в работе (и возможно хранит противоречивые данные). Ядро снимает блокиров- ку по окончании обработки системного вызова: блокировка индекса никогда не выходит за границы системного вызова. Ядро увеличивает значение счетчика ссылок с появлением каждой активной ссылки на файл. Например, в разделе 5.1 будет показано, как ядро увеличива- ет значение счетчика ссылок тогда, когда процесс открывает файл. Оно уменьшает значение счетчика ссылок только тогда, когда ссылка становится неактивной, например, когда процесс закрывает файл. Таким образом, установка счетчика ссылок сохраняется для множест- ва системных вызовов. Блокировка снимается между двумя обращения- ми к операционной системе, чтобы позволить процессам одновременно производить разделенный доступ к файлу; установка счетчика ссылок действует между обращениями к операционной системе, чтобы предуп- редить переназначение ядром активного в памяти индекса. Таким об- разом, ядро может заблокировать и разблокировать выделенный ин- декс независимо от значения счетчика ссылок. Выделением и осво- бождением индексов занимаются и отличные от open системные опера- ции, в чем мы и убедимся в главе 5. Возвращаясь к алгоритму iget, заметим, что если ядро пытается взять индекс из списка свободных индексов и обнаруживает список пустым, оно сообщает об ошибке. В этом отличие от идеологии, ко- торой следует ядро при работе с дисковыми буферами, где процесс приостанавливает свое выполнение до тех пор, пока буфер не осво- бодится. Процессы контролируют выделение индексов на пользова- тельском уровне посредством запуска системных операций open и close и поэтому ядро не может гарантировать момент, когда индекс станет доступным. Следовательно, процесс, приостанавливающий свое выполнение в ожидании освобождения индекса, может никогда не во- зобновиться. Ядро скорее прервет выполнение системного вызова, чем оставит такой процесс в "зависшем" состоянии. Однако, процес- сы не имеют такого контроля над буферами. Поскольку процесс не может удержать буфер заблокированным в течение выполнения нес- кольких системных операций, ядро здесь может гарантировать скорое освобождение буфера, и процесс поэтому приостанавливается до того момента, когда он станет доступным. В предшествующих параграфах рассматривался случай, когда ядро выделяет индекс, отсутствующий в индексном кеше. Если индекс на- ходится в кеше, процесс (A) обнаружит его в хеш-очереди и прове- рит, не заблокирован ли индекс другим процессом (B). Если индекс заблокирован, процесс A приостанавливается и выставляет флаг у индекса в памяти, показывая, что он ждет освобождения индекса. Когда позднее процесс B разблокирует индекс, он "разбудит" все процессы (включая процесс A), ожидающие освобождения индекса. Когда же наконец процесс A сможет использовать индекс, он забло- кирует его, чтобы другие процессы не могли к нему обратиться. Ес- ли первоначально счетчик ссылок имел значение, равное 0, индекс также появится в списке свободных индексов, поэтому ядро уберет его оттуда: индекс больше не является свободным. Ядро увеличивает значение счетчика ссылок и возвращает заблокированный индекс. Если суммировать все вышесказанное, можно отметить, что алго- ритм iget имеет отношение к начальной стадии системных вызовов, когда процесс впервые обращается к файлу. Этот алгоритм возвраща- ет заблокированную индексную структуру со значением счетчика ссы- лок, на 1 большим, чем оно было раньше. Индекс в памяти содержит текущую информацию о состоянии файла. Ядро снимает блокировку с индекса перед выходом из системной операции, поэтому другие сис- темные вызовы могут обратиться к индексу, если пожелают. В главе 5 рассматриваются эти случаи более подробно. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм iput /* разрешение доступа к индексу в памяти */Ё Ё входная информация: указатель на индекс в памяти Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё заблокировать индекс если он еще не заблокирован; Ё Ё уменьшить на 1 счетчик ссылок для индекса; Ё Ё если (значение счетчика ссылок == 0) Ё Ё { Ё Ё если (значение счетчика связей == 0) Ё Ё { Ё Ё освободить дисковые блоки для файла (алгоритм Ё Ё free, раздел 4.7); Ё Ё установить тип файла равным 0; Ё Ё освободить индекс (алгоритм ifree, раздел 4.6); Ё Ё } Ё Ё если (к файлу обращались или изменился индекс или Ё Ё изменилось содержимое файла) Ё Ё скорректировать дисковый индекс; Ё Ё поместить индекс в список свободных индексов; Ё Ё } Ё Ё снять блокировку с индекса; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 4.4. Освобождение индекса 4.1.3 Освобождение индексов В том случае, когда ядро освобождает индекс (алгоритм iput, Рисунок 4.4), оно уменьшает значение счетчика ссылок для него. Если это значение становится равным 0, ядро переписывает индекс на диск в том случае, когда копия индекса в памяти отличается от дискового индекса. Они различаются, если изменилось содержимое файла, если к файлу производилось обращение или если изменились владелец файла либо права доступа к файлу. Ядро помещает индекс в список свободных индексов, наиболее эффективно располагая индекс в кеше на случай, если он вскоре понадобится вновь. Ядро может также освободить все связанные с файлом информационные блоки и индекс, если число ссылок на файл равно 0. 4.2 СТРУКТУРА ФАЙЛА ОБЫЧНОГО ТИПА Как уже говорилось, индекс включает в себя таблицу адресов расположения информации файла на диске. Так как каждый блок на диске адресуется по своему номеру, в этой таблице хранится сово- купность номеров дисковых блоков. Если бы данные файла занимали непрерывный участок на диске (то есть файл занимал бы линейную последовательность дисковых блоков), то для обращения к данным в файле было бы достаточно хранить в индексе адрес начального блока и размер файла. Однако, такая стратегия размещения данных не поз- воляет осуществлять простое расширение и сжатие файлов в файловой системе без риска фрагментации свободного пространства памяти на диске. Более того, ядру пришлось бы выделять и резервировать неп- рерывное пространство в файловой системе перед выполнением опера- ций, могущих привести к увеличению размера файла. дддддддддддддбддддддддддбддддддддддбддддддддддбддддддддддддд ЫЫЫЫЫЫЫЫЫЫ Ё Файл A Ё Файл B Ё Файл C Ё ЫЫЫЫЫЫЫЫЫЫЫ дддддддддддддаддддддддддаддддддддддаддддддддддаддддддддддддд 40 50 60 70 Адреса блоков дддддддддддддбддддддддддбддддддддддбддддддддддбдддддддддбддд ЫЫЫЫЫЫЫЫЫЫ Ё Файл A Ё Свободны Ё Файл C Ё Файл B Ё ЫЫ дддддддддддддаддддддддддаддддддддддаддддддддддадддддддддаддд 40 50 60 70 81 Адреса блоков Рисунок 4.5. Размещение непрерывных файлов и фрагментация свободного пространства Предположим, например, что пользователь создает три файла, A, B и C, каждый из которых занимает 10 дисковых блоков, а также что система выделила для размещения этих трех файлов непрерывное мес- то. Если потом пользователь захочет добавить 5 блоков с информа- цией к среднему файлу, B, ядру придется скопировать файл B в то место в файловой системе, где найдется окно размером 15 блоков. В дополнение к затратам ресурсов на проведение этой операции дис- ковые блоки, занимаемые информацией файла B, станут неиспользуе- мыми, если только они не понадобятся файлам размером не более 10 блоков (Рисунок 4.5). Ядро могло бы минимизировать фрагментацию пространства памяти, периодически запуская процедуры чистки памя- ти, уплотняющие имеющуюся память, но это потребовало бы дополни- тельного расхода временных и системных ресурсов. В целях повышения гибкости ядро присоединяет к файлу по одно- му блоку, позволяя информации файла быть разбросанной по всей файловой системе. Но такая схема размещения усложняет задачу по- иска данных. Таблица адресов содержит список номеров блоков, со- держащих принадлежащую файлу информацию, однако простые вычисле- ния показывают, что линейным списком блоков файла в индексе трудно управлять. Если логический блок занимает 1 Кбайт, то фай- лу, состоящему из 10 Кбайт, потребовался бы индекс на 10 номеров блоков, а файлу, состоящему из 100 Кбайт, понадобился бы индекс на 100 номеров блоков. Либо пусть размер индекса будет варьиро- ваться в зависимости от размера файла, либо пришлось бы устано- вить относительно жесткое ограничение на размер файла. Для того, чтобы небольшая структура индекса позволяла рабо- тать с большими файлами, таблица адресов дисковых блоков приво- дится в соответствие со структурой, представленной на Рисунке 4.6. Версия V системы UNIX работает с 13 точками входа в таблицу адресов индекса, но принципиальные моменты не зависят от коли- чества точек входа. Блок, имеющий пометку "прямая адресация" на рисунке, содержит номера дисковых блоков, в которых хранятся ре- альные данные. Блок, имеющий пометку "одинарная косвенная адреса- ция", указывает на блок, содержащий список номеров блоков прямой адресации. Чтобы обратиться к данным с помощью блока косвенной адресации, ядро должно считать этот блок, найти соответствующий вход в блок прямой адресации и, считав блок прямой адресации, об- наружить данные. Блок, имеющий пометку "двойная косвенная адреса- ция", содержит список номеров блоков одинарной косвенной адреса- ции, а блок, имеющий пометку "тройная косвенная адресация", содержит список номеров блоков двойной косвенной адресации. В принципе, этот метод можно было бы распространить и на под- держку блоков четверной косвенной адресации, блоков пятерной кос- венной адресации и так далее, но на практике оказывается доста- точно имеющейся структуры. Предположим, что размер логического блока в файловой системе 1 Кбайт и что номер блока занимает 32 бита (4 байта). Тогда в блоке может храниться до 256 номеров бло- ков. Расчеты показывают (Рисунок 4.7), что максимальный размер файла превышает 16 Гбайт, если использовать в индексе 10 блоков прямой адресации и 1 одинарной косвенной адресации, 1 двойной косвенной адресации и 1 тройной косвенной адресации. Если же учесть, что длина поля "размер файла" в индексе - 32 бита, то размер файла в действительности ограничен 4 Гбайтами (2 в степени 32). Процессы обращаются к информации в файле, задавая смещение в байтах. Они рассматривают файл как поток байтов и ведут подсчет байтов, начиная с нулевого адреса и заканчивая адресом, равным размеру файла. Ядро переходит от байтов к блокам: файл начинается с нулевого логического блока и заканчивается блоком, номер кото- рого определяется исходя из размера файла. Ядро обращается к ин- дексу и превращает логический блок, принадлежащий файлу, в соот- ветствующий дисковый блок. На Рисунке 4.8 представлен алгоритм bmap пересчета смещения в байтах от начала файла в номер физичес- кого блока на диске. Рассмотрим формат файла в блоках (Рисунок 4.9) и предположим, что дисковый блок занимает 1024 байта. Если процессу нужно обра- титься к байту, имеющему смещение от начала файла, равное 9000, в результате вычислений ядро приходит к выводу, что этот байт рас- полагается в блоке прямой адресации с номером 8 (начиная с 0). Затем ядро обращается к блоку с номером 367; 808-й байт в этом Информацион- Индекс ные блоки зддддддддддддд© зддддд© Ё прямой адр. цддддддддддддддддддддддддддддддддддд>Ё Ё Ё 0Ё Ё Ё цддддддддддддд╢ юддддды Ё прямой адр. цддддддддддддддддд© зддддд© Ё 1Ё юддддддддддддддддд>Ё Ё цддддддддддддд╢ Ё Ё Ё прямой адр. цддддддддддддддддд© юддддды Ё 2Ё Ё зддддд© цддддддддддддд╢ юддддддддддддддддд>Ё Ё Ё прямой адр. цддддддддддддддддд© Ё Ё Ё 3Ё Ё юддддды цддддддддддддд╢ Ё зддддд© Ё прямой адр. Ё юддддддддддддддддд>Ё Ё Ё 4Ё Ё Ё цддддддддддддд╢ юддддды Ё прямой адр. Ё Ы Ё 5Ё Ы цддддддддддддд╢ Ы Ё прямой адр. Ё Ы Ё 6Ё Ы цддддддддддддд╢ зддддд© Ё прямой адр. Ё зддддддддддддддддд>Ё Ё Ё 7Ё Ё Ё Ё цддддддддддддд╢ здддддддддддддды юддддды Ё прямой адр. Ё Ё зддддд© Ё 8Ё Ё зддддддддддддддддд>Ё Ё цддддддддддддд╢ Ё Ё Ё Ё Ё прямой адр. цдды здддддд© Ё юддддды Ё 9Ё цддддддедддды зддддд© цддддддддддддд╢ зд>цдддддд╢ здддддд>Ё Ё Ё одинарной цдды цдддддд╢ Ё Ё Ё Ёкосвенной адрЁ юдддддды Ё юддддды цддддддддддддд╢ зд>здддддд© зд>здддддд© Ё зддддд© Ё двойной цдды цдддддд╢ Ё цдддддд╢ Ё зд>Ё Ё Ёкосвенной адрЁ цдддддд╢ Ё цдддддд╢ Ё Ё Ё Ё цддддддддддддд╢ цддддддеды цддддддеддды Ё юддддды Ё тройной цдд© юдддддды юдддддды юддд© Ёкосвенной адрЁ юд>здддддд© зд>здддддд© з>зддддддбды юддддддддддддды цдддддд╢ Ё цдддддд╢ Ё цдддддд╢ цддддддеды цдддддд╢ Ё цдддддд╢ цдддддд╢ цддддддеды цдддддд╢ юдддддды юдддддды юдддддды Рисунок 4.6. Блоки прямой и косвенной адресации в индексе здддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё 10 блоков прямой адресации по 1 Кбайту каждый = 10 Кбайт Ё Ё 1 блок косвенной адресации с 256 блоками прямой Ё Ё адресации = 256 Кбайт Ё Ё 1 блок двойной косвенной адресации с 256 блоками Ё Ё косвенной адресации = 64 МбайтаЁ Ё 1 блок тройной косвенной адресации с 256 блоками Ё Ё двойной косвенной адресации = 16 Гбайт Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 4.7. Объем файла в байтах при размере блока 1 Кбайт здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм bmap /* отображение адреса смещения в байтах от Ё Ё начала логического файла на адрес блока Ё Ё в файловой системе */ Ё Ё входная информация: (1) индекс Ё Ё (2) смещение в байтах Ё Ё выходная информация: (1) номер блока в файловой системе Ё Ё (2) смещение в байтах внутри блока Ё Ё (3) число байт ввода-вывода в блок Ё Ё (4) номер блока с продвижением Ё Ё { Ё Ё вычислить номер логического блока в файле исходя из Ё Ё заданного смещения в байтах; Ё Ё вычислить номер начального байта в блоке для ввода- Ё Ё вывода; /* выходная информация 2 */ Ё Ё вычислить количество байт для копирования пользова- Ё Ё телю; /* выходная информация 3 */ Ё Ё проверить возможность чтения с продвижением, пометить Ё Ё индекс; /* выходная информация 4 */ Ё Ё определить уровень косвенности; Ё Ё выполнить (пока уровень косвенности другой) Ё Ё { Ё Ё определить указатель в индексе или блок косвенной Ё Ё адресации исходя из номера логического блока в Ё Ё файле; Ё Ё получить номер дискового блока из индекса или из Ё Ё блока косвенной адресации; Ё Ё освободить буфер от данных, полученных в резуль- Ё Ё тате выполнения предыдущей операции чтения с Ё Ё диска (алгоритм brelse); Ё Ё если (число уровней косвенности исчерпано) Ё Ё возвратить (номер блока); Ё Ё считать дисковый блок косвенной адресации (алго- Ё Ё ритм bread); Ё Ё установить номер логического блока в файле исходя Ё Ё из уровня косвенности; Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 4.8. Преобразование адреса смещения в номер блока в файловой системе блоке (если вести отсчет с 0) и является 9000-м байтом в файле. Если процессу нужно обратиться по адресу, указанному смещением 350000 байт от начала файла, он должен считать блок двойной кос- венной адресации, который на рисунке имеет номер 9156. Так как блок косвенной адресации имеет место для 256 номеров блоков, пер- вым байтом, к которому будет получен доступ в результате обраще- зддддддддддддд© Ё 4096 Ё цддддддддддддд╢ Ё 228 Ё цддддддддддддд╢ Ё 45423 Ё цддддддддддддд╢ Ё 0 Ё цддддддддддддд╢ Ё 0 Ё цддддддддддддд╢ зддддддддддд>здддддд© Ё 11111 Ё Ё Ё Ё цддддддддддддд╢ Ё Ё Ё Ё 0 Ё Ё Ё Ё цддддддддддддд╢ Ё юдддддды Ё 101 Ё Ё 367 цддддддддддддд╢ Ё информаци- Ё 367 цдддддддддддддддддддддды онный цддддддддддддд╢ блок Ё 0 Ё зд>здддддд© цддддддддддддд╢ здддд>здддддд© Ё Ё Ё здд>здддддд© Ё 428 Ё Ё Ё 331 цдды Ё Ё Ё Ё Ё цддддддддддддд╢ Ё 0цдддддд╢ 75цдддддд╢ Ё Ё Ё Ё 9156 цдды Ё Ё Ё 3333 цдды Ё Ё цддддддддддддд╢ юдддддды цдддддд╢ юдддддды Ё 824 Ё 9156 Ё Ё 3333 юддддддддддддды двойная юдддддды информаци- адресация 331 онный одинарная блок адресация Рисунок 4.9. Размещение блоков в файле и его индексе ния к блоку двойной косвенной адресации, будет байт с номером 272384 (256К + 10К); таким образом, байт с номером 350000 будет иметь в блоке двойной косвенной адресации номер 77616. Поскольку каждый блок одинарной косвенной адресации позволяет обращаться к 256 Кбайтам, байт с номером 350000 должен располагаться в нулевом блоке одинарной косвенной адресации для блока двойной косвенной адресации, а именно в блоке 331. Так как в каждом блоке прямой адресации для блока одинарной косвенной адресации хранится 1 Кбайт, байт с номером 77616 находится в 75-м блоке прямой адреса- ции для блока одинарной косвенной адресации, а именно в блоке 3333. Наконец, байт с номером в файле 350000 имеет в блоке 3333 номер 816. При ближайшем рассмотрении Рисунка 4.9 обнаруживается, что несколько входов для блока в индексе имеют значение 0 и это зна- чит, что в данных записях информация о логических блоках отсутс- твует. Такое имеет место, если в соответствующие блоки файла ни- когда не записывалась информация и по этой причине у номеров бло- ков остались их первоначальные нулевые значения. Для таких блоков пространство на диске не выделяется. Подобное расположение блоков в файле вызывается процессами, запускающими системные операции lseek и write (см. следующую главу). В следующей главе также объ- ясняется, каким образом ядро обрабатывает системные вызовы опера- ции read, с помощью которой производится обращение к блокам. Преобразование адресов с большими смещениями, в частности с использованием блоков тройной косвенной адресации, является слож- ной процедурой, требующей от ядра обращения уже к трем дисковым блокам в дополнение к индексу и информационному блоку. Даже если ядро обнаружит блоки в буферном кеше, операция останется дорогос- тоящей, так как ядру придется многократно обращаться к буферному кешу и приостанавливать свою работу в ожидании снятия блокировки с буферов. Насколько эффективен этот алгоритм на практике ? Это зависит от того, как используется система, а также от того, кто является пользователем и каков состав задач, вызывающий потреб- ность в более частом обращении к большим или, наоборот, маленьким файлам. Однако, как уже было замечено [Mullender 84], большинство файлов в системе UNIX имеет размер, не превышающий 10 Кбайт и да- же 1 Кбайта ! (*) Поскольку 10 Кбайт файла располагаются в блоках прямой адресации, к большей части данных, хранящихся в файлах, доступ может производиться за одно обращение к диску. Поэтому в отличие от обращения к большим файлам, работа с файлами стандарт- ного размера протекает быстро. В двух модификациях только что описанной структуры индекса предпринимается попытка использовать размерные характеристики файла. Основной принцип в реализации файловой системы BSD 4.2 [McKusick 84] состоит в том, что чем больше объем данных, к кото- рым ядро может получить доступ за одно обращение к диску, тем быстрее протекает работа с файлом. Это свидетельствует в пользу увеличения размера логического блока на диске, поэтому в системе BSD разрешается иметь логические блоки размером 4 или 8 Кбайт. Однако, увеличение размера блоков на диске приводит к увеличению фрагментации блоков, при которой значительные участки дискового пространства остаются неиспользуемыми. Например, если размер ло- гического блока 8 Кбайт, тогда файл размером 12 Кбайт занимает 1 полный блок и половину второго блока. Другая половина второго блока (4 Кбайта) фактически теряется; другие файлы не могут ис- пользовать ее для хранения данных. Если размеры файлов таковы, что число байт, попавших в последний блок, является равномерно распределенной величиной, то средние потери дискового пространс- тва составляют полблока на каждый файл; объем теряемого дискового пространства достигает в файловой системе с логическими блоками размером 4 Кбайта 45% [McKusick 84]. Выход из этой ситуации в системе BSD состоит в выделении только части блока (фрагмента) для размещения оставшейся информации файла. Один дисковый блок может включать в себя фрагменты, принадлежащие нескольким файлам. Некоторые подробности этой реализации исследуются на примере уп- ражнения в главе 5. Второй модификацией рассмотренной классической структуры ин- декса является идея хранения в индексе информации файла (см. [Mullender 84]). Если увеличить размер индекса так, чтобы индекс занимал весь дисковый блок, небольшая часть блока может быть ис- пользована для собственно индексных структур, а оставшаяся часть - для хранения конца файла и даже во многих случаях для хранения файла целиком. Основное преимущество такого подхода заключается в том, что необходимо только одно обращение к диску для считывания индекса и всей информации, если файл помещается в индексном бло- ке. ддддддддддддддддддддддддддддддддддддддд (*) На примере 19978 файлов Маллендер и Танненбаум говорят, что приблизительно 85% файлов имеют размер менее 8 Кбайт и 48% - менее 1 Кбайта. Несмотря на то, что эти данные варьируются от одной реализации системы к другой, для многих реализаций сис- темы UNIX они показательны. 4.3 КАТАЛОГИ Из главы 1 напомним, что каталоги являются файлами, из кото- рых строится иерархическая структура файловой системы; они играют важную роль в превращении имени файла в номер индекса. Каталог - это файл, содержимым которого является набор записей, состоящих из номера индекса и имени файла, включенного в каталог. Составное имя - это строка символов, завершающаяся пустым символом и разде- ляемая наклонной чертой ("/") на несколько компонент. Каждая ком- понента, кроме последней, должна быть именем каталога, но послед- няя компонента может быть именем файла, не являющегося каталогом. В версии V системы UNIX длина каждой компоненты ограничивается 14 символами; таким образом, вместе с 2 байтами, отводимыми на номер индекса, размер записи каталога составляет 16 байт. зддддддддддддддддддддддддддддддддддддддддддддддд© Ё Смещение в байтах Номер индекса Имя Ё Ё внутри каталога (2 байта) файла Ё цддддддддддддддддддддбдддддддддддддддбдддддддддд╢ Ё 0 Ё 83 Ё . Ё Ё 16 Ё 2 Ё .. Ё Ё 32 Ё 1798 Ё init Ё Ё 48 Ё 1276 Ё fsck Ё Ё 64 Ё 85 Ё clri Ё Ё 80 Ё 1268 Ё motd Ё Ё 96 Ё 1799 Ё mount Ё Ё 112 Ё 88 Ё mknod Ё Ё 128 Ё 2114 Ё passwd Ё Ё 144 Ё 1717 Ё umount Ё Ё 160 Ё 1851 Ё checklistЁ Ё 176 Ё 92 Ё fsdbld Ё Ё 192 Ё 84 Ё config Ё Ё 208 Ё 1432 Ё getty Ё Ё 224 Ё 0 Ё crash Ё Ё 240 Ё 95 Ё mkfs Ё Ё 256 Ё 188 Ё inittab Ё юддддддддддддддддддддадддддддддддддддадддддддддды Рисунок 4.10. Формат каталога /etc На Рисунке 4.10 показан формат каталога "etc". В каждом ката- логе имеются файлы, в качестве имен которых указаны точка и две точки ("." и "..") и номера индексов у которых совпадают с номе- рами индексов данного каталога и родительского каталога, соот- ветственно. Номер индекса для файла "." в каталоге "/etc" имеет адрес со смещением 0 и значение 83. Номер индекса для файла ".." имеет адрес со смещением 16 от начала каталога и значение 2. За- писи в каталоге могут быть пустыми, при этом номер индекса равен 0. Например, запись с адресом 224 в каталоге "/etc" пустая, нес- мотря на то, что она когда-то содержала точку входа для файла с именем "crash". Программа mkfs инициализирует файловую систему таким образом, что номера индексов для файлов "." и ".." в корне- вом каталоге совпадают с номером корневого индекса файловой сис- темы. Ядро хранит данные в каталоге так же, как оно это делает в файле обычного типа, используя индексную структуру и блоки с уровнями прямой и косвенной адресации. Процессы могут читать дан- ные из каталогов таким же образом, как они читают обычные файлы, однако исключительное право записи в каталог резервируется ядром, благодаря чему обеспечивается правильность структуры каталога. Права доступа к каталогу имеют следующий смысл: право чтения дает процессам возможность читать данные из каталога; право записи позволяет процессу создавать новые записи в каталоге или удалять старые (с помощью системных операций creat, mknod, link и unlink), в результате чего изменяется содержимое каталога; право исполнения позволяет процессу производить поиск в каталоге по имени файла (поскольку "исполнять" каталог бессмысленно). На при- мере Упражнения 4.6 показана разница между чтением и поиском в каталоге. 4.4 ПРЕВРАЩЕНИЕ СОСТАВНОГО ИМЕНИ ФАЙЛА (ПУТИ ПОИСКА) В ИДЕН- ТИФИКАТОР ИНДЕКСА Начальное обращение к файлу производится по его составному имени (имени пути поиска), как в командах open, chdir (изменить каталог) или link. Поскольку внутри системы ядро работает с ин- дексами, а не с именами путей поиска, оно преобразует имена путей поиска в идентификаторы индексов, чтобы производить доступ к фай- лам. Алгоритм namei производит поэлементный анализ составного имени, ставя в соответствие каждой компоненте имени индекс и ка- талог и в конце концов возвращая идентификатор индекса для вве- денного имени пути поиска (Рисунок 4.11). Из главы 2 напомним, что каждый процесс связан с текущим ка- талогом (и протекает в его рамках); рабочая область, отведенная под задачу пользователя, содержит указатель на индекс текущего каталога. Текущим каталогом первого из процессов в системе, нуле- вого процесса, является корневой каталог. Путь к текущему катало- гу каждого нового процесса берет начало от текущего каталога про- цесса, являющегося родительским по отношению к данному (см. раздел 5.10). Процессы изменяют текущий каталог, запрашивая вы- полнение системной операции chdir (изменить каталог). Все поиски файлов по имени начинаются с текущего каталога процесса, если только имя пути поиска не предваряется наклонной чертой, указы- вая, что поиск нужно начинать с корневого каталога. В любом слу- чае ядро может легко обнаружить индекс каталога, с которого начи- нается поиск. Текущий каталог хранится в рабочей области процесса, а корневой индекс системы хранится в глобальной пере- менной (**). Алгоритм namei использует при анализе составного имени пути поиска промежуточные индексы; назовем их рабочими индексами. Ин- декс каталога, откуда поиск берет начало, является первым рабочим индексом. На каждой итерации цикла алгоритма ядро проверяет сов- падение рабочего индекса с индексом каталога. В противном случае, нарушилось бы утверждение, что только файлы, не являющиеся ката- логами, могут быть листьями дерева файловой системы. Процесс так- же должен иметь право производить поиск в каталоге (разрешения на чтение недостаточно). Код идентификации пользователя для процесса должен соответствовать коду индивидуального или группового вла- ддддддддддддддддддддддддддддддддддддддд (**) Чтобы изменить для себя корневой каталог файловой системы, процесс может запустить системную операцию chroot. Новое значение корня сохраняется в рабочей области процесса. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм namei /* превращение имени пути поиска в индекс */Ё Ё входная информация: имя пути поиска Ё Ё выходная информация: заблокированный индекс Ё Ё { Ё Ё если (путь поиска берет начало с корня) Ё Ё рабочий индекс = индексу корня (алгоритм iget); Ё Ё в противном случае Ё Ё рабочий индекс = индексу текущего каталога Ё Ё (алгоритм iget); Ё Ё Ё Ё выполнить (пока путь поиска не кончился) Ё Ё { Ё Ё считать следующую компоненту имени пути поиска; Ё Ё проверить соответствие рабочего индекса каталогу Ё Ё и права доступа; Ё Ё если (рабочий индекс соответствует корню и компо- Ё Ё нента имени "..") Ё Ё продолжить; /* цикл с условием продолжения */Ё Ё считать каталог (рабочий индекс), повторяя алго- Ё Ё ритмы bmap, bread и brelse; Ё Ё если (компонента соответствует записи в каталоге Ё Ё (рабочем индексе)) Ё Ё { Ё Ё получить номер индекса для совпавшей компонен-Ё Ё ты; Ё Ё освободить рабочий индекс (алгоритм iput); Ё Ё рабочий индекс = индексу совпавшей компоненты Ё Ё (алгоритм iget); Ё Ё } Ё Ё в противном случае /* компонента отсутствует в Ё Ё каталоге */ Ё Ё возвратить (нет индекса); Ё Ё } Ё Ё возвратить (рабочий индекс); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 4.11. Алгоритм превращения имени пути поиска в индекс дельца файла и должно быть предоставлено право исполнения, либо поиск нужно разрешить всем пользователям. В противном случае, по- иск не получится. Ядро выполняет линейный поиск файла в каталоге, ассоциирован- ном с рабочим индексом, пытаясь найти для компоненты имени пути поиска подходящую запись в каталоге. Исходя из адреса смещения в байтах внутри каталога (начиная с 0), оно определяет местоположе- ние дискового блока в соответствии с алгоритмом bmap и считывает этот блок, используя алгоритм bread. По имени компоненты ядро производит в блоке поиск, представляя содержимое блока как после- довательность записей каталога. При обнаружении совпадения ядро переписывает номер индекса из данной точки входа, освобождает блок (алгоритм brelse) и старый рабочий индекс (алгоритм iput), и переназначает индекс найденной компоненты (алгоритм iget). Новый индекс становится рабочим индексом. Если ядро не находит в блоке подходящего имени, оно освобождает блок, прибавляет к адресу сме- щения число байтов в блоке, превращает новый адрес смещения в но- мер дискового блока (алгоритм bmap) и читает следующий блок. Ядро повторяет эту процедуру до тех пор, пока имя компоненты пути по- иска не совпадет с именем точки входа в каталоге, либо до тех пор, пока не будет достигнут конец каталога. Предположим, например, что процессу нужно открыть файл "/etc/ passwd". Когда ядро начинает анализировать имя файла, оно натал- кивается на наклонную черту ("/") и получает индекс корня систе- мы. Сделав корень текущим рабочим индексом, ядро наталкивается на строку "etc". Проверив соответствие текущего индекса каталогу ("/") и наличие у процесса права производить поиск в каталоге, ядро ищет в корневом каталоге файл с именем "etc". Оно просматри- вает корневой каталог блок за блоком и исследует каждую запись в блоке, пока не обнаружит точку входа для файла "etc". Найдя эту точку входа, ядро освобождает индекс, отведенный для корня (алго- ритм iput), и выделяет индекс файлу "etc" (алгоритм iget) в соот- ветствии с номером индекса в обнаруженной записи. Удостоверившись в том, что "etc" является каталогом, а также в том, что имеются необходимые права производить поиск, ядро просматривает каталог "etc" блок за блоком в поисках записи, соответствующей файлу "passwd". Если посмотреть на Рисунок 4.10, можно увидеть, что за- пись о файле "passwd" является девятой записью в каталоге. Обна- ружив ее, ядро освобождает индекс, выделенный файлу "etc", и вы- деляет индекс файлу "passwd", после чего - поскольку имя пути поиска исчерпано - возвращает этот индекс процессу. Естественно задать вопрос об эффективности линейного поиска в каталоге записи, соответствующей компоненте имени пути поиска. Ричи показывает (см. [Ritchie 78b], стр.1968), что линейный поиск эффективен, поскольку он ограничен размером каталога. Более того, ранние версии системы UNIX не работали еще на машинах с большим объемом памяти, поэтому значительный упор был сделан на простые алгоритмы, такие как алгоритмы линейного поиска. Более сложные схемы поиска потребовали бы отличной, более сложной, структуры каталога, и возможно работали бы медленнее даже в небольших ката- логах по сравнению со схемой линейного поиска. 4.5 СУПЕРБЛОК До сих пор в этой главе описывалась структура файла, при этом предполагалось, что индекс предварительно связывался с файлом и что уже были определены дисковые блоки, содержащие информацию. В следующих разделах описывается, каким образом ядро назначает ин- дексы и дисковые блоки. Чтобы понять эти алгоритмы, рассмотрим структуру суперблока. Суперблок состоит из следующих полей: * размер файловой системы, * количество свободных блоков в файловой системе, * список свободных блоков, имеющихся в файловой системе, * индекс следующего свободного блока в списке свободных бло- ков, * размер списка индексов, * количество свободных индексов в файловой системе, * список свободных индексов в файловой системе, * следующий свободный индекс в списке свободных индексов, * заблокированные поля для списка свободных блоков и свобод- ных индексов, * флаг, показывающий, что в суперблок были внесены изменения. В оставшейся части главы будет объяснено, как пользоваться массивами, указателями и замками блокировки. Ядро периодически переписывает суперблок на диск, если в суперблок были внесены из- менения, для того, чтобы обеспечивалась согласованность с данны- ми, хранящимися в файловой системе. 4.6 НАЗНАЧЕНИЕ ИНДЕКСА НОВОМУ ФАЙЛУ Для выделения известного индекса, то есть индекса, для кото- рого предварительно определен собственный номер (и номер файловой системы), ядро использует алгоритм iget. В алгоритме namei, нап- ример, ядро определяет номер индекса, устанавливая соответствие между компонентой имени пути поиска и именем в каталоге. Другой алгоритм, ialloc, выполняет назначение дискового индекса вновь создаваемому файлу. Как уже говорилось в главе 2, в файловой системе имеется ли- нейный список индексов. Индекс считается свободным, если поле его типа хранит нулевое значение. Если процессу понадобился новый ин- декс, ядро теоретически могло бы произвести поиск свободного ин- декса в списке индексов. Однако, такой поиск обошелся бы дорого, поскольку потребовал бы по меньшей мере одну операцию чтения (до- пустим, с диска) на каждый индекс. Для повышения производитель- ности в суперблоке файловой системы хранится массив номеров сво- бодных индексов в файловой системе. На Рисунке 4.12 приведен алгоритм ialloc назначения новых ин- дексов. По причинам, о которых пойдет речь ниже, ядро сначала проверяет, не заблокировал ли какой-либо процесс своим обращением список свободных индексов в суперблоке. Если список номеров ин- дексов в суперблоке не пуст, ядро назначает номер следующего ин- декса, выделяет для вновь назначенного дискового индекса свобод- ный индекс в памяти, используя алгоритм iget (читая индекс с диска, если необходимо), копирует дисковый индекс в память, ини- циализирует поля в индексе и возвращает индекс заблокированным. Затем ядро корректирует дисковый индекс, указывая, что к индексу произошло обращение. Ненулевое значение поля типа файла говорит о том, что дисковый индекс назначен. В простейшем случае с индексом все в порядке, но в условиях конкуренции делается необходимым проведение дополнительных проверок, на чем мы еще кратко остано- вимся. Грубо говоря, конкуренция возникает, когда несколько про- цессов вносят изменения в общие информационные структуры, так что результат зависит от очередности выполнения процессов, пусть даже все процессы будут подчиняться протоколу блокировки. Здесь пред- полагается, например, что процесс мог бы получить уже используе- мый индекс. Конкуренция связана с проблемой взаимного исключения, описанной в главе 2, с одним замечанием: различные схемы блоки- ровки решают проблему взаимного исключения, но не могут сами по себе решить все проблемы конкуренции. Если список свободных индексов в суперблоке пуст, ядро прос- матривает диск и помещает в суперблок как можно больше номеров свободных индексов. При этом ядро блок за блоком считывает индек- сы с диска и наполняет список номеров индексов в суперблоке до отказа, запоминая индекс с номером, наибольшим среди найденных. Назовем этот индекс "запомненным"; это последний индекс, записан- ный в суперблок. В следующий раз, когда ядро будет искать на дис- ке свободные индексы, оно использует запомненный индекс в качест- ве стартовой точки, благодаря чему гарантируется, что ядру не придется зря тратить время на считывание дисковых блоков, в кото- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм ialloc /* выделение индекса */ Ё Ё входная информация: файловая система Ё Ё выходная информация: заблокированный индекс Ё Ё { Ё Ё выполнить Ё Ё { Ё Ё если (суперблок заблокирован) Ё Ё { Ё Ё приостановиться (пока суперблок не освободится); Ё Ё продолжить; /* цикл с условием продолжения */ Ё Ё } Ё Ё если (список индексов в суперблоке пуст) Ё Ё { Ё Ё заблокировать суперблок; Ё Ё выбрать запомненный индекс для поиска свободных Ё Ё индексов; Ё Ё искать на диске свободные индексы до тех пор, покаЁ Ё суперблок не заполнится, или пока не будут най- Ё Ё дены все свободные индексы (алгоритмы bread и Ё Ё brelse); Ё Ё снять блокировку с суперблока; Ё Ё возобновить выполнение процесса (как только супер-Ё Ё блок освободится); Ё Ё если (на диске отсутствуют свободные индексы) Ё Ё возвратить (нет индексов); Ё Ё запомнить индекс с наибольшим номером среди най- Ё Ё денных для последующих поисков свободных индек- Ё Ё сов; Ё Ё } Ё Ё /* список индексов в суперблоке не пуст */ Ё Ё выбрать номер индекса из списка индексов в супербло- Ё Ё ке; Ё Ё получить индекс (алгоритм iget); Ё Ё если (индекс после всего этого не свободен) /* !!! */Ё Ё { Ё Ё переписать индекс на диск; Ё Ё освободить индекс (алгоритм iput); Ё Ё продолжить; /* цикл с условием продолжения */ Ё Ё } Ё Ё /* индекс свободен */ Ё Ё инициализировать индекс; Ё Ё переписать индекс на диск; Ё Ё уменьшить счетчик свободных индексов в файловой сис- Ё Ё теме; Ё Ё возвратить (индекс); Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 4.12. Алгоритм назначения новых индексов рых свободные индексы наверняка отсутствуют. После формирования нового набора номеров свободных индексов ядро запускает алгоритм назначения индекса с самого начала. Всякий раз, когда ядро назна- чает дисковый индекс, оно уменьшает значение счетчика свободных индексов, записанное в суперблоке. Список свободных индексов в суперблоке здддддддддддддддддддддбддддддбддддддбддддддддддддддддддд© Ё свободные индексы Ё Ё Ё пустота Ё Ё<>Ё 83 Ё 48 Ё<>Ё юдддддддддддддддддддддаддддддаддддддаддддддддддддддддддды 18 19 20 массив 1 ^ Ё указатель Список свободных индексов в суперблоке здддддддддддддддддддддбддддддбддддддбддддддддддддддддддд© Ё свободные индексы Ё Ё Ё пустота Ё Ё<>Ё 83 Ё <Ё>Ё юдддддддддддддддддддддаддддддаддддддаддддддддддддддддддды 18 19 20 массив 1 ^ Ё указатель (а) Назначение свободного индекса из середины списка Список свободных индексов в суперблоке зддддддбдддддддддддддддддддддддддддддддддддддддддддддддд© Ё 470 Ё пустота Ё Ё<Ё>Ё юддддддадддддддддддддддддддддддддддддддддддддддддддддддды 0  массив 1 ^  Ёуказатель (запомненный индекс)   Список свободных индексов в суперблоке зддддддбддддддддддддддддддддддддддддддбдддддбдддддбддддд© Ё 535 Ё свободные индексы Ё 476 Ё 475 Ё 471 Ё Ё<ЁЁЁЁ>Ё юддддддаддддддддддддддддддддддддддддддадддддадддддаддддды 0 48 49 50 ^ указатель Ё (б) Назначение свободного индекса, когда список в супер- блоке пуст Рисунок 4.13. Два массива номеров свободных индексов Рассмотрим две пары массивов номеров свободных индексов (Ри- сунок 4.13). Если список свободных индексов в суперблоке имеет вид первого массива на Рисунке 4.13(а) при назначении индекса яд- ром, то значение указателя на следующий номер индекса уменьшается до 18 и выбирается индекс с номером 48. Если же список выглядит как первый массив на Рисунке 4.13(б), ядро заметит, что массив пуст и обратится в поисках свободных индексов к диску, при этом поиск будет производиться, начиная с индекса с номером 470, кото- рый был ранее запомнен. Когда ядро заполнит список свободных ин- дексов в суперблоке до отказа, оно запомнит последний индекс в качестве начальной точки для последующих просмотров диска. Ядро производит назначение файлу только что выбранного с диска индекса (под номером 471 на рисунке) и продолжает прерванную обработку. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм ifree /* освобождение индекса */ Ё Ё входная информация: номер индекса в файловой системе Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё увеличить на 1 счетчик свободных индексов в файловой Ё Ё системе; Ё Ё если (суперблок заблокирован) Ё Ё возвратить управление; Ё Ё если (список индексов заполнен) Ё Ё { Ё Ё если (номер индекса меньше номера индекса, запом- Ё Ё ненного для последующего просмотра) Ё Ё запомнить для последующего просмотра номер Ё Ё введенного индекса; Ё Ё } Ё Ё в противном случае Ё Ё сохранить номер индекса в списке индексов; Ё Ё возвратить управление; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 4.14. Алгоритм освобождения индекса Алгоритм освобождения индекса построен значительно проще. Увеличив на единицу общее количество доступных в файловой системе индексов, ядро проверяет наличие блокировки у суперблока. Если он заблокирован, ядро, чтобы предотвратить конкуренцию, немедленно сообщает: номер индекса отсутствует в суперблоке, но индекс может быть найден на диске и доступен для переназначения. Если список не заблокирован, ядро проверяет, имеется ли место для новых номе- ров индексов и если да, помещает номер индекса в список и выходит из алгоритма. Если список полон, ядро не сможет в нем сохранить вновь освобожденный индекс. Оно сравнивает номер освобожденного индекса с номером запомненного индекса. Если номер освобожденного индекса меньше номера запомненного, ядро запоминает номер вновь освобожденного индекса, выбрасывая из суперблока номер старого запомненного индекса. Индекс не теряется, поскольку ядро может найти его, просматривая список индексов на диске. Ядро поддержи- вает структуру списка в суперблоке таким образом, что последний номер, выбираемый им из списка, и есть номер запомненного индек- са. В идеале не должно быть свободных индексов с номерами, мень- зддддддбддддддддддддддддддддддддддддддбдддддбдддддбддддд© Ё 535 Ё свободные индексы Ё 476 Ё 475 Ё 471 Ё Ё<ЁЁЁЁ>Ё юддддддаддддддддддддддддддддддддддддддадддддадддддаддддды 0 ^ 48 49 50 Ё ^ запомненный индекс указатель Ё (а) Первоначальный вид списка свободных индексов в супер- блоке зддддддбддддддддддддддддддддддддддддддбдддддбдддддбддддд© Ё 499 Ё свободные индексы Ё 476 Ё 475 Ё 471 Ё Ё<ЁЁЁЁ>Ё юддддддаддддддддддддддддддддддддддддддадддддадддддаддддды 0 ^ 48 49 50 Ё ^ запомненный индекс указатель Ё (б) Освободился индекс номер 499 зддддддбддддддддддддддддддддддддддддддбдддддбдддддбддддд© Ё 499 Ё свободные индексы Ё 476 Ё 475 Ё 471 Ё Ё<ЁЁЁЁ>Ё юддддддаддддддддддддддддддддддддддддддадддддадддддаддддды 0 ^ 48 49 50 Ё ^ запомненный индекс указатель Ё (в) Освободился индекс номер 601 Рисунок 4.15. Размещение номеров свободных индексов в суперб- локе шими, чем номер запомненного индекса, но возможны и исключения. Рассмотрим два примера освобождения индексов. Если в списке свободных индексов в суперблоке еще есть место для новых номеров свободных индексов (как на Рисунке 4.13(а)), ядро помещает в спи- сок новый номер, переставляет указатель на следующий свободный индекс и продолжает выполнение процесса. Но если список свободных индексов заполнен (Рисунок 4.15), ядро сравнивает номер освобож- денного индекса с номером запомненного индекса, с которого нач- нется просмотр диска в следующий раз. Если вначале список свобод- ных индексов имел вид, как на Рисунке 4.15(а), то когда ядро освобождает индекс с номером 499, оно запоминает его и выталкива- ет номер 535 из списка. Если затем ядро освобождает индекс с но- мером 601, содержимое списка свободных индексов не изменится. Когда позднее ядро использует все индексы из списка свободных ин- дексов в суперблоке, оно обратится в поисках свободных индексов к диску, при этом, начав просмотр с индекса с номером 499, оно сно- ва обнаружит индексы 535 и 601. Процесс A Процесс B Процесс C здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё Назначает индекс I Ы Ы Ё из суперблока Ы Ы Ё Ы Ы Ё Приостанавливается Ы Ы Ё на время считывания Ы Ы Ё индекса (а) Ы Ы Ё Ы Ы Ы Ё Ы Пытается назначить Ы Ё Ы индекс из суперблока Ы Ё Ы Ы Ё Ы Суперблок пуст (б) Ы Ё Ы Ы Ё Ы Просматривает диск в Ы Ё Ы поисках свободных ин- Ы Ё Ы дексов, помещение ин- Ы Ё Ы декса I в суперблок Ы Ё Ы (в) Ы Ё Ы Ы Ы Ё Индекс I в памяти Ы Ы Ё Выполняются обычные Ы Ы Ё действия Ы Ы Ё Ы Ы Ы Ё Ы Заканчивает просмотр, Ы Ё Ы назначает другой индекс Ы Ё Ы (г) Ы Ё Ы Ы Ы Ё Ы Ы Назначает индекс I Ё Ы Ы из суперблока Ё Ы Ы Ё Ы Ы Индекс I уже исполь- Ё Ы Ы зуется ! Ё Ы Ы Ё Ы Ы Назначает другой Ё Ы Ы индекс (д) Ё Ы Ы v Время Рисунок 4.16. Конкуренция в назначении индексов В предыдущем параграфе описывались простые случаи работы ал- горитмов. Теперь рассмотрим случай, когда ядро назначает новый индекс и затем копирует его в память. В алгоритме предполагается, что ядро может и обнаружить, что индекс уже назначен. Несмотря на редкость такой ситуации, обсудим этот случай (с помощью Рисунков 4.16 и 4.17). Пусть у нас есть три процесса, A, B и C, и пусть ядро, действуя от имени процесса A (***), назначает индекс I, но приостанавливает выполнение процесса перед тем, как скопировать дисковый индекс в память. Алгоритмы iget (вызванный алгоритмом ддддддддддддддддддддддддддддддддддддддд (***) Как и в предыдущей главе, здесь под "процессом" имеется ввиду "ядро, действующее от имени процесса". ЁВремя Ё здддбдддбдддбдддддддддддддддддддддддддддддддд© Ё (а) Ё Ё Ё Ё Ё Ё Ё Ё Ё I Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё Ё Ё Ё Ё Ё Ё Ё юдддадддадддадддддддддддддддддддддддддддддддды Ё Ё здддддддддддддддддддддддддддддддддддддддддддд© Ё (б) Ё пусто Ё Ё Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё Ё Ё Ё Ё юдддддддддддддддддддддддддддддддддддддддддддды Ё Ё здддбдддбдддбддддддддддддддддддддбдддбдддбддд© Ё (в) Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё свободные индексы Ё J Ё I Ё K Ё Ё Ё ЫЫЁЫЫЫЁЫЫЫЁЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЁЫЫЫЁЫЫЫЁЫЫ Ё Ё юдддадддадддаддддддддддддддддддддадддадддаддды Ё Ё здддбдддбдддбддддддддддддддддддддбдддбдддбддд© Ё (г) Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё свободные индексы Ё J Ё I Ё Ё Ё Ё ЫЫЁЫЫЫЁЫЫЫЁЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЁЫЫЫЁЫЫЫЁ Ё Ё юдддадддадддаддддддддддддддддддддадддадддаддды Ё Ё здддбдддбдддбддддддддддддддддбдддбдддбдддбддд© Ё (д) Ё Ё Ё Ё свободные Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё индексы Ё L Ё Ё Ё Ё Ё Ё ЫЫЁЫЫЫЁЫЫЫЁЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЁЫЫЫЁ Ё Ё Ё Ё юдддадддадддаддддддддддддддддадддадддадддаддды v Рисунок 4.17. Конкуренция в назначении индексов (продолжение) ialloc) и bread (вызванный алгоритмом iget) дают процессу A дос- таточно возможностей для приостановления своей работы. Предполо- жим, что пока процесс A приостановлен, процесс B пытается назна- чить новый индекс, но обнаруживает, что список свободных индексов в суперблоке пуст. Процесс B просматривает диск в поисках свобод- ных индексов, и начинает это делать с индекса, имеющего меньший номер по сравнению с индексом, назначенным процессом A. Возможно, что процесс B обнаружит индекс I на диске свободным, так как про- цесс A все еще приостановлен, а ядро еще не знает, что этот ин- декс собираются назначить. Процесс B, не осознавая опасности, за- канчивает просмотр диска, заполняет суперблок свободными (предположительно) индексами, назначает индекс и уходит со сцены. Однако, индекс I остается в списке номеров свободных индексов в суперблоке. Когда процесс A возобновляет выполнение, он заканчи- вает назначение индекса I. Теперь допустим, что процесс C затем затребовал индекс и случайно выбрал индекс I из списка в суперб- локе. Когда он обратится к копии индекса в памяти, он обнаружит из установки типа файла, что индекс уже назначен. Ядро проверяет это условие и, обнаружив, что этот индекс назначен, пытается наз- начить другой. Немедленная перепись скорректированного индекса на диск после его назначения в соответствии с алгоритмом ialloc сни- жает опасность конкуренции, поскольку поле типа файла будет со- держать пометку о том, что индекс использован. Блокировка списка индексов в суперблоке при чтении с диска устраняет другие возможности для конкуренции. Если суперблок не заблокирован, процесс может обнаружить, что он пуст, и попытаться заполнить его с диска, время от времени приостанавливая свое вы- полнение до завершения операции ввода-вывода. Предположим, что второй процесс так же пытается назначить новый индекс и обнаружи- вает, что список пуст. Он тоже попытается заполнить список с дис- ка. В лучшем случае, оба процесса продублируют друг друга и пот- ратят энергию центрального процессора. В худшем, участится конкуренция, подобная той, которая описана в предыдущем парагра- фе. Сходным образом, если процесс, освобождая индекс, не прове- рил наличие блокировки списка, он может затереть номера индексов уже в списке свободных индексов, пока другой процесс будет запол- нять этот список информацией с диска. И опять участится конкурен- ция вышеописанного типа. Несмотря на то, что ядро более или менее удачно управляется с ней, производительность системы снижается. Установка блокировки для списка свободных индексов в суперблоке устраняет такую конкуренцию. 4.7 ВЫДЕЛЕНИЕ ДИСКОВЫХ БЛОКОВ Когда процесс записывает данные в файл, ядро должно выделять из файловой системы дисковые блоки под информационные блоки пря- мой адресации и иногда под блоки косвенной адресации. Суперблок файловой системы содержит массив, используемый для хранения номе- ров свободных дисковых блоков в файловой системе. Сервисная прог- рамма mkfs ("make file system" - создать файловую систему) орга- низует информационные блоки в файловой системе в виде списка с указателями так, что каждый элемент списка указывает на дисковый блок, в котором хранится массив номеров свободных дисковых бло- ков, а один из элементов массива хранит номер следующего блока данного списка. Когда ядру нужно выделить блок из файловой системы (алгоритм alloc, Рисунок 4.19), оно выделяет следующий из блоков, имеющихся в списке в суперблоке. Выделенный однажды, блок не может быть пе- реназначен до тех пор, пока не освободится. Если выделенный блок является последним блоком, имеющимся в кеше суперблока, ядро трактует его как указатель на блок, в котором хранится список свободных блоков. Ядро читает блок, заполняет массив в суперблоке новым списком номеров блоков и после этого продолжает работу с первоначальным номером блока. Оно выделяет буфер для блока и очи- щает содержимое буфера (обнуляет его). Дисковый блок теперь счи- тается назначенным и у ядра есть буфер для работы с ним. Если в файловой системе нет свободных блоков, вызывающий процесс получа- ет сообщение об ошибке. Если процесс записывает в файл большой объем информации, он неоднократно запрашивает у системы блоки для хранения информации, но ядро назначает каждый раз только по одному блоку. Программа mkfs пытается организовать первоначальный связанный список номе- ров свободных блоков так, чтобы номера блоков, передаваемых фай- лу, были рядом друг с другом. Благодаря этому повышается произво- дительность, поскольку сокращается время поиска на диске и время ожидания при последовательном чтении файла процессом. На Рисунке 4.18 номера блоков даны в настоящем формате, определяемом ско- ростью вращения диска. К сожалению, очередность номеров блоков в списке свободных блоков перепутана в связи с частыми обращениями к списку со стороны процессов, ведущих запись в файлы и удаляющих их, в результате чего номера блоков поступают в список и покидают его в случайном порядке. Ядро не предпринимает попыток сортиро- вать номера блоков в списке. список в суперблоке здддддбдддддбдддддбдддддбддддддддддддддддддддд© Ё 109 Ё 106 Ё 103 Ё 100 Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё юддеддадддддадддддадддддаддддддддддддддддддддды зддддды Ё Ё 109 Ё здддддбдддддбдддддбдддддбдддддддддддддддбддддд© юд>Ё 211 Ё 208 Ё 205 Ё 202 Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё 112 Ё юддеддадддддадддддадддддадддддддддддддддаддддды зддддды Ё Ё 211 Ё здддддбдддддбдддддбдддддбдддддддддддддддбддддд© юд>Ё 310 Ё 307 Ё 304 Ё 301 Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё 214 Ё юддеддадддддадддддадддддадддддддддддддддаддддды зддддды Ё Ё 310 Ё здддддбдддддбдддддбдддддбдддддддддддддддбддддд© юд>Ё 409 Ё 406 Ё 403 Ё 400 Ё Ё 313 Ё юддеддадддддадддддадддддадддддддддддддддаддддды Ё v Рисунок 4.18. Список номеров свободных дисковых блоков с указателями Алгоритм освобождения блока free - обратный алгоритму выделе- ния блока. Если список в суперблоке не полон, номер вновь осво- божденного блока включается в этот список. Если, однако, список полон, вновь освобожденный блок становится связным блоком; ядро переписывает в него список из суперблока и копирует блок на диск. Затем номер вновь освобожденного блока включается в список сво- бодных блоков в суперблоке. Этот номер становится единственным номером в списке. На Рисунке 4.20 показана последовательность операций alloc и free для случая, когда в исходный момент список свободных блоков содержал один элемент. Ядро освобождает блок 949 и включает номер блока в список. Затем оно выделяет этот блок и удаляет его номер из списка. Наконец, оно выделяет блок 109 и удаляет его номер из списка. Поскольку список свободных блоков в суперблоке теперь пуст, ядро снова наполняет список, копируя в него содержимое бло- ка 109, являющегося следующей связью в списке с указателями. На Рисунке 4.20(г) показан заполненный список в суперблоке и следую- щий связной блок с номером 211. Алгоритмы назначения и освобождения индексов и дисковых бло- ков сходятся в том, что ядро использует суперблок в качестве кеша, хранящего указатели на свободные ресурсы - номера блоков и номера индексов. Оно поддерживает список номеров блоков с указа- телями, такой, что каждый номер свободного блока в файловой сис- теме появляется в некотором элементе списка, но ядро не поддержи- вает такого списка для свободных индексов. Тому есть три причины. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм alloc /* выделение блока файловой системы */ Ё Ё входная информация: номер файловой системы Ё Ё выходная информация: буфер для нового блока Ё Ё { Ё Ё выполнить (пока суперблок заблокирован) Ё Ё приостановиться (до того момента, когда с суперблокаЁ Ё будет снята блокировка); Ё Ё удалить блок из списка свободных блоков в суперблоке; Ё Ё если (из списка удален последний блок) Ё Ё { Ё Ё заблокировать суперблок; Ё Ё прочитать блок, только что взятый из списка свобод- Ё Ё ных (алгоритм bread); Ё Ё скопировать номера блоков, хранящиеся в данном бло- Ё Ё ке, в суперблок; Ё Ё освободить блочный буфер (алгоритм brelse); Ё Ё снять блокировку с суперблока; Ё Ё возобновить выполнение процессов (после снятия бло- Ё Ё кировки с суперблока); Ё Ё } Ё Ё получить буфер для блока, удаленного из списка (алго- Ё Ё ритм getblk); Ё Ё обнулить содержимое буфера; Ё Ё уменьшить общее число свободных блоков; Ё Ё пометить суперблок как "измененный"; Ё Ё возвратить буфер; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 4.19. Алгоритм выделения дискового блока 1. Ядро устанавливает, свободен ли индекс или нет, проверяя: если поле типа файла очищено, индекс свободен. Ядро не нуждается в другом механизме описания свободных индексов. Тем не менее, оно не может определить, свободен ли блок или нет, только взглянув на него. Ядро не может уловить различия между маской, показывающей, что блок свободен, и информацией, случайно имею- щей сходную маску. Следовательно, ядро нуждается во внешнем механизме идентификации свободных блоков, в качестве него в традиционных реализациях системы используется список с указа- телями. 2. Сама конструкция дисковых блоков наводит на мысль об использо- вании списков с указателями: в дисковом блоке легко разместить большие списки номеров свободных блоков. Но индексы не имеют подходящего места для массового хранения списков номеров сво- бодных индексов. 3. Пользователи имеют склонность чаще расходовать дисковые блоки, нежели индексы, поэтому кажущееся запаздывание в работе при просмотре диска в поисках свободных индексов не является таким критическим, как если бы оно имело место при поисках свободных дисковых блоков. список в суперблоке здддддбдддддбдддддбдддддбддддддддддддддддддддд© Ё 109 Ё 106 Ё 103 Ё 100 Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё юддеддадддддадддддадддддаддддддддддддддддддддды зддддды Ё Ё 109 Ё здддддбдддддбдддддбдддддбдддддддддддддддбддддд© юд>Ё 211 Ё 208 Ё 205 Ё 202 Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё 112 Ё юдддддадддддадддддадддддадддддддддддддддаддддды (а) Первоначальная конфигурация список в суперблоке здддддбдддддбддддддддддддддддддддддддддддддддд© Ё 109 Ё 949 Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё юддеддадддддаддддддддддддддддддддддддддддддддды зддддды Ё Ё 109 Ё здддддбдддддбдддддбдддддбдддддддддддддддбддддд© юд>Ё 211 Ё 208 Ё 205 Ё 202 Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё 112 Ё юдддддадддддадддддадддддадддддддддддддддаддддды (б) После освобождения блока с номером 949 список в суперблоке здддддбдддддбдддддбдддддбддддддддддддддддддддд© Ё 109 Ё 106 Ё 103 Ё 100 Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё юддеддадддддадддддадддддаддддддддддддддддддддды зддддды Ё Ё 109 Ё здддддбдддддбдддддбдддддбдддддддддддддддбддддд© юд>Ё 211 Ё 208 Ё 205 Ё 202 Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё 112 Ё юдддддадддддадддддадддддадддддддддддддддаддддды (в) После назначения блока с номером 949 список в суперблоке здддддбдддддбдддддбдддддбдддддддддддддддбддддд© Ё 211 Ё 208 Ё 205 Ё 202 Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё 112 Ё юддеддадддддадддддадддддадддддддддддддддаддддды зддддды Ё Ё 211 Ё здддддбдддддбдддддбдддддбдддддддддддддддбддддд© юд>Ё 344 Ё 341 Ё 338 Ё 335 Ё ЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё 243 Ё юдддддадддддадддддадддддадддддддддддддддаддддды (г) Новое заполнение списка в суперблоке после назначения блока с номером 109 Рисунок 4.20. Запрашивание и освобождение дисковых блоков 4.8 ДРУГИЕ ТИПЫ ФАЙЛОВ В системе UNIX поддерживаются и два других типа файлов: кана- лы и специальные файлы. Канал, иногда называемый fifo (сокращенно от "first-in-first-out" - "первым пришел - первым вышел" - пос- кольку обслуживает запросы в порядке поступления), отличается от обычного файла тем, что содержит временные данные: информация, однажды считанная из канала, не может быть прочитана вновь. Кроме того, информация читается в том порядке, в котором она была запи- сана в канале, и система не допускает никаких отклонений от дан- ного порядка. Способ хранения ядром информации в канале не отли- чается от способа ее хранения в обычном файле, за исключением того, что здесь используются только блоки прямой, а не косвенной, адресации. Конкретное представление о каналах можно будет полу- чить в следующей главе. Последним типом файлов в системе UNIX являются специальные файлы, к которым относятся специальные файлы устройств ввода-вы- вода блоками и специальные файлы устройств посимвольного вво- да-вывода. Оба подтипа обозначают устройства, и поэтому индексы таких файлов не связаны ни с какой информацией. Вместо этого ин- декс содержит два номера - старший и младший номера устройства. Старший номер устройства указывает его тип, например, терминал или диск, а младший номер устройства - числовой код, идентифици- рующий устройство в группе однородных устройств. Более подробно специальные файлы устройств рассматриваются в главе 10. 4.9 ВЫВОДЫ Индекс представляет собой структуру данных, в которой описы- ваются атрибуты файла, в том числе расположение информации файла на диске. Существует две разновидности индекса: копия на диске, в которой хранится информация индекса, пока файл находится в рабо- те, и копия в памяти, где хранится информация об активных файлах. Алгоритмы ialloc и ifree управляют назначением файлу дискового индекса во время выполнения системных операций creat, mknod, pipe и unlink (см. следующую главу), а алгоритмы iget и iput управляют выделением индексов в памяти в момент обращения процесса к файлу. Алгоритм bmap определяет местонахождение дисковых блоков, принад- лежащих файлу, используя предварительно заданное смещение в бай- тах от начала файла. Каталоги представляют собой файлы, которые устанавливают соответствие между компонентами имен файлов и номе- рами индексов. Алгоритм namei преобразует имена файлов, с которы- ми работают процессы, в идентификаторы индексов, с которыми рабо- тает ядро. Наконец, ядро управляет назначением файлу новых диско- вых блоков, используя алгоритмы alloc и free. Структуры данных, рассмотренные в настоящей главе, состоят из связанных списков, хеш-очередей и линейных массивов, и поэтому алгоритмы, работающие с рассмотренными структурами данных, доста- точно просты. Сложности появляются тогда, когда возникает конку- ренция, вызываемая взаимодействием алгоритмов между собой, и не- которые из этих проблем синхронизации рассмотрены в тексте. Тем не менее, алгоритмы не настолько детально разработаны и могут служить иллюстрацией простоты конструкции системы. Вышеописанные структуры и алгоритмы работают внутри ядра и невидимы для пользователя. С точки зрения общей архитектуры сис- темы (Рисунок 2.1), алгоритмы, рассмотренные в данной главе, име- ют отношение к нижней половине подсистемы управления файлами. Следующая глава посвящена разбору обращений к операционной систе- ме, обеспечивающих функционирование пользовательского интерфейса, и описанию верхней половины подсистемы управления файлами, из ко- торой вызывается выполнение рассмотренных здесь алгоритмов. 8. В версии V системы UNIX разрешается использовать не более 14 символов на каждую компоненту имени пути поиска. Алгоритм namei отсекает лишние символы в компоненте. Что нужно сделать в файловой системе и в соответствующих алгоритмах, чтобы стали допустимыми имена компонент произвольной длины ? 9. Предположим, что пользователь имеет закрытую версию системы UNIX, причем он внес в нее такие изменения, что имя компоненты теперь может состоять из 30 символов; закрытая версия системы обеспечивает тот же способ хранения записей каталогов, как и стандартная операционная система, за исключением того, что за- писи каталогов имеют длину 32 байта вместо 16. Если пользова- тель смонтирует закрытую файловую систему в стандартной опера- ционной среде, что произойдет во время работы алгоритма namei, когда процесс обратится к файлу ? *10. Рассмотрим работу алгоритма namei по преобразованию имени пути поиска в идентификатор индекса. В течение всего просмотра ядро проверяет соответствие текущего рабочего индекса индексу каталога. Может ли другой процесс удалить (unlink) каталог ? Каким образом ядро предупреждает такие действия ? В следующей главе мы вернемся к этой проблеме. *11. Разработайте структуру каталога, повышающую эффективность поиска имен файлов без использования линейного просмотра. Рассмотрите два способа: хеширование и n-арные деревья. *12. Разработайте алгоритм сокращения количества просмотров ката- лога в поисках имени файла, используя запоминание часто упот- ребляемых имен. *13. В идеальном случае в файловой системе не должно быть свободных индексов с номерами, меньшими, чем номер "запомнен- ного" индекса, используемый алгоритмом ialloc. Как случается, что это утверждение бывает ложным ? 14. Суперблок является дисковым блоком и содержит кроме списка свободных блоков и другую информацию, как показано в данной главе. Поэтому список свободных блоков в суперблоке не может содержать больше номеров свободных блоков, чем может помес- титься в одном дисковом блоке в связанном списке свободных дисковых блоков. Какое число номеров свободных блоков было бы оптимальным для хранения в одном блоке из связанного списка ? *15. Рассмотрим вариант системной реализации, в котором свободные дисковые блоки отслеживаются с помощью битовой маски вместо использования списка блоков с указателями. Каковы преимущества и неудобства такой схемы ? СИСТЕМНЫЕ ОПЕРАЦИИ ДЛЯ РАБОТЫ С ФАЙЛОВОЙ СИСТЕМОЙ В последней главе рассматривались внутренние структуры данных для файловой системы и алгоритмы работы с ними. В этой главе речь пойдет о системных функциях для работы с файловой системой с ис- пользованием понятий, введенных в предыдущей главе. Рассматрива- ются системные функции, обеспечивающие обращение к существующим файлам, такие как open, read, write, lseek и close, затем функции создания новых файлов, а именно, creat и mknod, и, наконец, функ- ции для работы с индексом или для передвижения по файловой систе- ме: chdir, chroot, chown, stat и fstat. Исследуются более сложные системные функции: pipe и dup имеют важное значение для реализа- ции каналов в shell'е; mount и umount расширяют видимое для поль- зователя дерево файловых систем; link и unlink изменяют иерархи- ческую структуру файловой системы. Затем дается представление об абстракциях, связанных с файловой системой, в отношении поддержки различных файловых систем, подчиняющихся стандартным интерфейсам. В последнем разделе главы речь пойдет о сопровождении файловой системы. Глава знакомит с тремя структурами данных ядра: таблицей файлов, в которой каждая запись связана с одним из открытых в системе файлов, таблицей пользовательских дескрипторов файлов, в которой каждая запись связана с файловым дескриптором, известным процессу, и таблицей монтирования, в которой содержится информа- ция по каждой активной файловой системе. На Рисунке 5.1 показана взаимосвязь между системными функция- ми и алгоритмами, описанными ранее. Системные функции классифици- руются на несколько категорий, хотя некоторые из функций присутствуют более, чем в одной категории: Функции для работы с файловой системой здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© цддддддбддддддддддддддбддддддддбдддддддбдддддддбдддддддддбддддддд╢ Ё Воз- Ё Используют Ё Назна- Ё Рабо- Ё Ввод- Ё Работа- Ё Управ-Ё Ё вра- Ё алгоритм Ё чают Ё тают Ё вывод Ё ют со Ё ление Ё Ё щают Ё namei Ё индек- Ё с ат- Ё из Ё структу-Ё де- Ё Ё деск-Ё Ё сы Ё рибу- Ё файла Ё рой фай-Ё ревь- Ё Ё рип- Ё Ё Ё тами Ё Ё ловых Ё ями Ё Ё торы Ё Ё Ё файла Ё Ё систем Ё Ё Ё файлаЁ Ё Ё Ё Ё Ё Ё цддддддеддддддддддддддеддддддддедддддддедддддддедддддддддеддддддд╢ Ё open Ё open stat Ё Ё Ё Ё Ё Ё Ё creatЁ creat link Ё creat Ё chown Ё read Ё Ё Ё Ё dup Ё chdir unlinkЁ mknod Ё chmod Ё write Ё mount Ё chdir Ё Ё pipe Ё chroot mknod Ё link Ё stat Ё lseek Ё umount Ё chown Ё Ё closeЁ chown mount Ё unlink Ё Ё Ё Ё Ё Ё Ё chmod umountЁ Ё Ё Ё Ё Ё цддддддеддддддддддддддеддддддддедддддддедддддддедддддддддеддддддд╢ юдддбддаддддддддддддддаддддддддадддддддадддддддадддддддддаддддбдды Ё Алгоритмы работы с файловой системой на нижнем уровне Ё цдддддддддддддбддддддддддддддддддбдддддддддддддддддддддддд╢ Ё namei Ё Ё Ё цддддддддддддд╢ ialloc ifree Ё alloc free bmap Ё Ё iget iput Ё Ё Ё цдддддддддддддаддддддддддддддддддадддддддддддддддддддддддд╢ цддддддддддддддддддддддддддддддддддддддддддддддддддддддддд╢ Ё алгоритмы работы с буферами Ё цддддддддддддддддддддддддддддддддддддддддддддддддддддддддд╢ Ё getblk brelse bread breada bwrite Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.1. Функции для работы с файловой системой и их связь с другими алгоритмами * Системные функции, возвращающие дескрипторы файлов для ис- пользования другими системными функциями; * Системные функции, использующие алгоритм namei для анализа имени пути поиска; * Системные функции, назначающие и освобождающие индекс с ис- пользованием алгоритмов ialloc и ifree; * Системные функции, устанавливающие или изменяющие атрибуты файла; * Системные функции, позволяющие процессу производить ввод-вы- вод данных с использованием алгоритмов alloc, free и алгоритмов выделения буфера; * Системные функции, изменяющие структуру файловой системы; * Системные функции, позволяющие процессу изменять собственное представление о структуре дерева файловой системы. 5.1 OPEN Вызов системной функции open (открыть файл) - это первый шаг, который должен сделать процесс, чтобы обратиться к данным в фай- ле. Синтаксис вызова функции open: fd = open(pathname,flags,modes); где pathname - имя файла, flags указывает режим открытия (напри- мер, для чтения или записи), а modes содержит права доступа к файлу в случае, если файл создается. Системная функция open возв- ращает целое число (*), именуемое пользовательским дескриптором файла. Другие операции над файлами, такие как чтение, запись, по- ддддддддддддддддддддддддддддддддддддддд (*) Все системные функции возвращают в случае неудачного заверше- ния код -1. Код возврата, равный -1, больше не будет упоми- наться при рассмотрении синтаксиса вызова системных функций. зиционирование головок чтения-записи, воспроизведение дескриптора файла, установка параметров ввода-вывода, определение статуса файла и закрытие файла, используют значение дескриптора файла, возвращаемое системной функцией open. Ядро просматривает файловую систему в поисках файла по его имени, используя алгоритм namei (см. Рисунок 5.2). Оно проверяет права на открытие файла после того, как обнаружит копию индекса файла в памяти, и выделяет открываемому файлу запись в таблице файлов. Запись таблицы файлов содержит указатель на индекс откры- того файла и поле, в котором хранится смещение в байтах от начала файла до места, откуда предполагается начинать выполнение после- дующих операций чтения или записи. Ядро сбрасывает это смещение в 0 во время открытия файла, имея в виду, что исходная операция чтения или записи по умолчанию будет производиться с начала фай- ла. С другой стороны, процесс может открыть файл в режиме записи в конец, в этом случае ядро устанавливает значение смещения, рав- ное размеру файла. Ядро выделяет запись в личной (закрытой) таб- лице в адресном пространстве задачи, выделенном процессу (таблица эта называется таблицей пользовательских дескрипторов файлов), и запоминает указатель на эту запись. Указателем выступает дескрип- тор файла, возвращаемый пользователю. Запись в таблице пользова- тельских файлов указывает на запись в глобальной таблице файлов. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм open Ё Ё входная информация: имя файла Ё Ё режим открытия Ё Ё права доступа (при создании файла) Ё Ё выходная информация: дескриптор файла Ё Ё { Ё Ё превратить имя файла в идентификатор индекса (алгоритм Ё Ё namei); Ё Ё если (файл не существует или к нему не разрешен доступ) Ё Ё возвратить (код ошибки); Ё Ё выделить для индекса запись в таблице файлов, инициали- Ё Ё зировать счетчик, смещение; Ё Ё выделить запись в таблице пользовательских дескрипторов Ё Ё файла, установить указатель на запись в таблице файлов;Ё Ё если (режим открытия подразумевает усечение файла) Ё Ё освободить все блоки файла (алгоритм free); Ё Ё снять блокировку (с индекса); /* индекс заблокирован Ё Ё выше, в алгоритме Ё Ё namei */ Ё Ё возвратить (пользовательский дескриптор файла); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.2. Алгоритм открытия файла Предположим, что процесс, открывая файл "/etc/passwd" дважды, один раз только для чтения и один раз только для записи, и однаж- ды файл "local" для чтения и для записи (**), выполняет следующий набор операторов: ддддддддддддддддддддддддддддддддддддддд (**) В описании вызова системной функции open содержатся три па- раметра (третий используется при открытии в режиме созда- ния), но программисты обычно используют только первые два из них. Компилятор с языка Си не проверяет правильность коли- чества параметров. В системе первые два параметра и третий (с любым "мусором", что бы ни произошло в стеке) передаются обычно ядру. Ядро не проверяет наличие третьего параметра, если только необходимость в нем не вытекает из значения вто- рого параметра, что позволяет программистам указать только два параметра. таблица пользова- тельских дескрип- торов файла таблица файлов таблица индексов зддддддддд© здддддддддддд© здддддддддддддд© 0Ё Ё Ё Ё Ё Ы Ё цддддддддд╢ Ё Ё Ё Ы Ё 1Ё Ё Ё Ё Ё Ы Ё цддддддддд╢ цдддддддддддд╢ Ё Ы Ё 2Ё Ё Ё Ы Ё Ё Ы Ё цддддддддд╢ Ё Ы Ё Ё Ы Ё 3Ё ддддедддд© Ё Ы Ё Ё Ы Ё цддддддддд╢ Ё Ё Ы Ё цдддддддддддддд╢ 4Ё ддддеддд©Ё Ё Ы Ё здддд>Ё счет- Ё цддддддддд╢ ЁЁ Ё Ы Ё Ёзддд>Ё чик (/etc/ Ё 5Ё ддддедд©ЁЁ цдддддддддддд╢ ЁЁ Ё 2 passwd)Ё цддддддддд╢ ЁЁЁ Ё счет- Ё ЁЁ цдддддддддддддд╢ 6Ё Ё ЁЁюдд>Ё чик ЧтениееддыЁ Ё Ы Ё цддддддддд╢ ЁЁ Ё 1 Ё Ё Ё Ы Ё 7Ё Ё ЁЁ цдддддддддддд╢ Ё Ё Ы Ё цддддддддд╢ ЁЁ Ё Ы Ё Ё Ё Ы Ё Ё Ы Ё ЁЁ Ё Ы Ё Ё Ё Ы Ё Ё Ы Ё ЁЁ Ё Ы Ё Ё Ё Ы Ё Ё Ы Ё ЁЁ Ё Ы Ё Ё Ё Ы Ё юддддддддды ЁЁ цдддддддддддд╢ Ё Ё Ы Ё ЁЁ Ё счет- Чте-Ё Ё Ё Ы Ё Ёюддд>Ё чик ние-едддЁд© Ё Ы Ё Ё Ё 1 ЗаписьЁ Ё Ё Ё Ы Ё Ё цдддддддддддд╢ Ё Ё Ё Ы Ё Ё Ё Ы Ё Ё Ё цдддддддддддддд╢ Ё Ё Ы Ё Ё Ё Ё счет- Ё Ё Ё Ы Ё Ё юд>Ё чик (local)Ё Ё Ё Ы Ё Ё Ё 1 Ё Ё Ё Ы Ё Ё цдддддддддддддд╢ Ё Ё Ы Ё Ё Ё Ы Ё Ё цдддддддддддд╢ Ё Ё Ы Ё Ё Ё счет- Ё Ё Ё Ы Ё юдддд>Ё чик Записьеддды Ё Ы Ё Ё 1 Ё Ё Ы Ё цдддддддддддд╢ Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юдддддддддддды юдддддддддддддды Рисунок 5.3. Структуры данных после открытия fd1 = open("/etc/passwd",O_RDONLY); fd2 = open("local",O_RDWR); fd3 = open("/etc/passwd",O_WRONLY); На Рисунке 5.3 показана взаимосвязь между таблицей индексов, таблицей файлов и таблицей пользовательских дескрипторов файла. Каждый вызов функции open возвращает процессу дескриптор файла, а соответствующая запись в таблице пользовательских дескрипторов файла указывает на уникальную запись в таблице файлов ядра, пусть даже один и тот же файл ("/etc/passwd") открывается дважды. Запи- таблицы пользова- тельских дескрип- торов файла (процесс A) таблица файлов таблица индексов зддддддддд© здддддддддддд© здддддддддддддд© 0Ё Ё Ё Ё Ё Ы Ё цддддддддд╢ Ё Ё Ё Ы Ё 1Ё Ё Ё Ё Ё Ы Ё цддддддддд╢ цдддддддддддд╢ Ё Ы Ё 2Ё Ё Ё Ы Ё Ё Ы Ё цддддддддд╢ Ё Ы Ё Ё Ы Ё 3Ё ддддедддд© Ё Ы Ё Ё Ы Ё цддддддддд╢ Ё Ё Ы Ё цдддддддддддддд╢ 4Ё ддддеддд©Ё Ё Ы Ё здддд>Ё счет- Ё цддддддддд╢ ЁЁ Ё Ы Ё Ёзддд>Ё чик (/etc/ Ё 5Ё ддддедд©ЁЁ цдддддддддддд╢ ЁЁздд>Ё 3 passwd)Ё цддддддддд╢ ЁЁЁ Ё счет- Ё ЁЁЁ цдддддддддддддд╢ Ё Ы Ё ЁЁюдд>Ё чик ЧтениееддыЁЁ Ё Ы Ё Ё Ы Ё ЁЁ Ё 1 Ё ЁЁ Ё Ы Ё Ё Ы Ё ЁЁ цдддддддддддд╢ ЁЁ Ё Ы Ё юддддддддды ЁЁ Ё Ы Ё ЁЁ Ё Ы Ё ЁЁ Ё Ы Ё ЁЁ Ё Ы Ё (процесс B) ЁЁ Ё Ы Ё ЁЁ Ё Ы Ё зддддддддд© ЁЁ Ё Ы Ё ЁЁ Ё Ы Ё 0Ё Ё ЁЁ цдддддддддддд╢ ЁЁ Ё Ы Ё цддддддддд╢ ЁЁ Ё счет- Чте-Ё ЁЁ Ё Ы Ё 1Ё Ё Ёюддд>Ё чик ние-едддЁЁ© Ё Ы Ё цддддддддд╢ Ё Ё 1 ЗаписьЁ ЁЁЁ Ё Ы Ё 2Ё Ё Ё цдддддддддддд╢ ЁЁЁ Ё Ы Ё цддддддддд╢ Ё Ё Ы Ё ЁЁЁ цдддддддддддддд╢ 3Ё ддддеддЁдд© Ё Ы Ё ЁЁЁ Ё счет- Ё цддддддддд╢ Ё Ё Ё Ы Ё ЁЁюд>Ё чик (local)Ё 4Ё ддддед©Ё Ё Ё Ы Ё ЁЁ Ё 1 Ё цддддддддд╢ ЁЁ Ё Ё Ы Ё ЁЁ цдддддддддддддд╢ 5Ё Ё ЁЁ Ё Ё Ы Ё ЁЁ Ё Ы Ё цддддддддд╢ ЁЁ Ё цдддддддддддд╢ ЁЁ Ё Ы Ё Ё Ы Ё ЁЁ Ё Ё счет- Ё ЁЁ Ё Ы Ё Ё Ы Ё ЁЁ юд>Ё чик ЧтениеедддыЁ Ё Ы Ё Ё Ы Ё ЁЁ Ё 1 Ё Ё Ё Ы Ё юддддддддды ЁЁ цдддддддддддд╢ Ё Ё Ы Ё ЁЁ Ё Ы Ё Ё Ё Ы Ё ЁЁ Ё Ы Ё Ё Ё Ы Ё ЁЁ Ё Ы Ё Ё Ё Ы Ё ЁЁ Ё Ы Ё Ё цдддддддддддддд╢ ЁЁ Ё Ы Ё Ё Ё счет- Ё ЁЁ цдддддддддддд╢ Ёзд>Ё чик (private)Ё ЁЁ Ё счет- Ё ЁЁ Ё 1 Ё Ёюдддд>Ё чик ЗаписьеддддыЁ цдддддддддддддд╢ Ё Ё 1 Ё Ё Ё Ы Ё Ё цдддддддддддд╢ Ё Ё Ы Ё Ё Ё Ы Ё Ё Ё Ы Ё Ё Ё Ы Ё Ё юдддддддддддддды Ё Ё Ы Ё Ё Ё цдддддддддддд╢ Ё Ё Ё счет- Ё Ё юддддд>Ё чик Чтениееддддды Ё 1 Ё юдддддддддддды Рисунок 5.4. Структуры данных после того, как два процесса произвели открытие файлов си в таблице файлов для всех экземпляров одного и того же откры- того файла указывают на одну запись в таблице индексов, хранящих- ся в памяти. Процесс может обращаться к файлу "/etc/passwd" с чтением или записью, но только через дескрипторы файла, имеющие значения 3 и 5 (см. рисунок). Ядро запоминает разрешение на чте- ние или запись в файл в строке таблицы файлов, выделенной во вре- мя выполнения функции open. Предположим, что второй процесс вы- полняет следующий набор операторов: fd1 = open("/etc/passwd",O_RDONLY); fd2 = open("private",O_RDONLY); На Рисунке 5.4 показана взаимосвязь между соответствующими структурами данных, когда оба процесса (и больше никто) имеют от- крытые файлы. Снова результатом каждого вызова функции open явля- ется выделение уникальной точки входа в таблице пользовательских дескрипторов файла и в таблице файлов ядра, и ядро хранит не бо- лее одной записи на каждый файл в таблице индексов, размещенных в памяти. Запись в таблице пользовательских дескрипторов файла по умол- чанию хранит смещение в файле до адреса следующей операции ввода- вывода и указывает непосредственно на точку входа в таблице ин- дексов для файла, устраняя необходимость в отдельной таблице фай- лов ядра. Вышеприведенные примеры показывают взаимосвязь между записями таблицы пользовательских дескрипторов файла и записями в таблице файлов ядра типа "один к одному". Томпсон, однако, отме- чает, что им была реализована таблица файлов как отдельная струк- тура, позволяющая совместно использовать один и тот же указатель смещения нескольким пользовательским дескрипторам файла (см. [Thompson 78], стр.1943). В системных функциях dup и fork, расс- матриваемых в разделах 5.13 и 7.1, при работе со структурами дан- ных допускается такое совместное использование. Первые три пользовательских дескриптора (0, 1 и 2) именуются дескрипторами файлов: стандартного ввода, стандартного вывода и стандартного файла ошибок. Процессы в системе UNIX по договорен- ности используют дескриптор файла стандартного ввода при чтении вводимой информации, дескриптор файла стандартного вывода при за- писи выводимой информации и дескриптор стандартного файла ошибок для записи сообщений об ошибках. В операционной системе нет ника- кого указания на то, что эти дескрипторы файлов являются специ- альными. Группа пользователей может условиться о том, что файло- вые дескрипторы, имеющие значения 4, 6 и 11, являются специальными, но более естественно начинать отсчет с 0 (как в языке Си). Принятие соглашения сразу всеми пользовательскими программами облегчит связь между ними при использовании каналов, в чем мы убедимся в дальнейшем, изучая главу 7. Обычно операторс- кий терминал (см. главу 10) служит и в качестве стандартного вво- да, и в качестве стандартного вывода и в качестве стандартного устройства вывода сообщений об ошибках. 5.2 READ Синтаксис вызова системной функции read (читать): number = read(fd,buffer,count) где fd - дескриптор файла, возвращаемый функцией open, buffer - адрес структуры данных в пользовательском процессе, где будут размещаться считанные данные в случае успешного завершения выпол- нения функции read, count - количество байт, которые пользователю нужно прочитать, number - количество фактически прочитанных байт. На Рисунке 5.5 приведен алгоритм read, выполняющий чтение обычно- го файла. Ядро обращается в таблице файлов к записи, которая со- ответствует значению пользовательского дескриптора файла, следуя здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм read Ё Ё входная информация: пользовательский дескриптор файла Ё Ё адрес буфера в пользовательском про- Ё Ё цессе Ё Ё количество байт, которые нужно прочи- Ё Ё тать Ё Ё выходная информация: количество байт, скопированных в поль-Ё Ё зовательское пространство Ё Ё { Ё Ё обратиться к записи в таблице файлов по значению пользо-Ё Ё вательского дескриптора файла; Ё Ё проверить доступность файла; Ё Ё установить параметры в адресном пространстве процесса, Ё Ё указав адрес пользователя, счетчик байтов, параметры Ё Ё ввода-вывода для пользователя; Ё Ё получить индекс по записи в таблице файлов; Ё Ё заблокировать индекс; Ё Ё установить значение смещения в байтах для адресного Ё Ё пространства процесса по значению смещения в таблице Ё Ё файлов; Ё Ё выполнить (пока значение счетчика байтов не станет удов-Ё Ё летворительным) Ё Ё { Ё Ё превратить смещение в файле в номер дискового блока Ё Ё (алгоритм bmap); Ё Ё вычислить смещение внутри блока и количество байт, Ё Ё которые будут прочитаны; Ё Ё если (количество байт для чтения равно 0) Ё Ё /* попытка чтения конца файла */ Ё Ё прерваться; /* выход из цикла */ Ё Ё прочитать блок (алгоритм breada, если производится Ё Ё чтение с продвижением, и алгоритм bread - в против- Ё Ё ном случае); Ё Ё скопировать данные из системного буфера по адресу Ё Ё пользователя; Ё Ё скорректировать значения полей в адресном простран- Ё Ё стве процесса, указывающие смещение в байтах внутри Ё Ё файла, количество прочитанных байт и адрес для пе- Ё Ё редачи в пространство пользователя; Ё Ё освободить буфер; /* заблокированный в алгоритме Ё Ё bread */ Ё Ё } Ё Ё разблокировать индекс; Ё Ё скорректировать значение смещения в таблице файлов для Ё Ё следующей операции чтения; Ё Ё возвратить (общее число прочитанных байт); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.5. Алгоритм чтения из файла за указателем (см. Рисунок 5.3). Затем оно устанавливает значения нескольких параметров ввода-вывода в адресном пространстве про- цесса (Рисунок 5.6), тем самым устраняя необходимость в их пере- даче в качестве параметров функции. В частности, ядро указывает в качестве режима ввода-вывода "чтение", устанавливает флаг, свидетельствующий о том, что ввод-вывод направляется в ад- ресное пространство пользователя, значение поля счетчика байтов приравнивает количеству байт, которые будут прочитаны, устанавли- вает адрес пользовательского буфера данных и, наконец, значение смещения (из таблицы файлов), равное смещению в байтах внутри файла до места, откуда начинается ввод-вывод. После того, как яд- ро установит значения параметров ввода-вывода в адресном прост- ранстве процесса, оно обращается к индексу, используя указатель из таблицы файлов, и блокирует его прежде, чем начать чтение из файла. Затем в алгоритме начинается цикл, выполняющийся до тех пор, пока операция чтения не будет произведена до конца. Ядро преобра- зует смещение в байтах внутри файла в номер блока, используя ал- здддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё mode чтение или запись Ё Ё count количество байт для чтения или записи Ё Ё offset смещение в байтах внутри файла Ё Ё address адрес места, куда будут копироваться данные,Ё Ё в памяти пользователя или ядра Ё Ё flag отношение адреса к памяти пользователя или Ё Ё к памяти ядра Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.6. Параметры ввода-вывода, хранящиеся в пространс- тве процесса горитм bmap, и вычисляет смещение внутри блока до места, откуда следует начать ввод-вывод, а также количество байт, которые будут прочитаны из блока. После считывания блока в буфер, возможно, с продвижением (алгоритмы bread и breada) ядро копирует данные из блока по назначенному адресу в пользовательском процессе. Оно корректирует параметры ввода-вывода в адресном пространстве про- цесса в соответствии с количеством прочитанных байт, увеличивая значение смещения в байтах внутри файла и адрес места в пользова- тельском процессе, куда будет доставлена следующая порция данных, и уменьшая число байт, которые необходимо прочитать, чтобы выпол- нить запрос пользователя. Если запрос пользователя не удовлетво- рен, ядро повторяет весь цикл, преобразуя смещение в байтах внут- ри файла в номер блока, считывая блок с диска в системный буфер, копируя данные из буфера в пользовательский процесс, освобождая буфер и корректируя значения параметров ввода-вывода в адресном пространстве процесса. Цикл завершается, либо когда ядро выполнит запрос пользователя полностью, либо когда в файле больше не будет данных, либо если ядро обнаружит ошибку при чтении данных с диска или при копировании данных в пространство пользователя. Ядро кор- ректирует значение смещения в таблице файлов в соответствии с ко- личеством фактически прочитанных байт; поэтому успешное выполне- ние операций чтения выглядит как последовательное считывание данных из файла. Системная операция lseek (раздел 5.6) устанавли- вает значение смещения в таблице файлов и изменяет порядок, в ко- тором процесс читает или записывает данные в файле. здддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main() Ё Ё { Ё Ё int fd; Ё Ё char lilbuf[20],bigbuf[1024]; Ё Ё Ё Ё fd = open("/etc/passwd",O_RDONLY); Ё Ё read(fd,lilbuf,20); Ё Ё read(fd,bigbuf,1024); Ё Ё read(fd,lilbuf,20); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.7. Пример программы чтения из файла Рассмотрим программу, приведенную на Рисунке 5.7. Функция open возвращает дескриптор файла, который пользователь засылает в переменную fd и использует в последующих вызовах функции read. Выполняя функцию read, ядро проверяет, правильно ли задан пара- метр "дескриптор файла", а также был ли файл предварительно отк- рыт процессом для чтения. Оно сохраняет значение адреса пользова- тельского буфера, количество считываемых байт и начальное смеще- ние в байтах внутри файла (соответственно: lilbuf, 20 и 0), в пространстве процесса. В результате вычислений оказывается, что нулевое значение смещения соответствует нулевому блоку файла, и ядро возвращает точку входа в индекс, соответствующую нулевому блоку. Предполагая, что такой блок существует, ядро считывает полный блок размером 1024 байта в буфер, но по адресу lilbuf ко- пирует только 20 байт. Оно увеличивает смещение внутри пространс- тва процесса на 20 байт и сбрасывает счетчик данных в 0. Посколь- ку операция read выполнилась, ядро переустанавливает значение смещения в таблице файлов на 20, так что последующие операции чтения из файла с данным дескриптором начнутся с места, располо- женного со смещением 20 байт от начала файла, а системная функция возвращает число байт, фактически прочитанных, т.е. 20. При повторном вызове функции read ядро вновь проверяет кор- ректность указания дескриптора и наличие соответствующего файла, открытого процессом для чтения, поскольку оно никак не может уз- нать, что запрос пользователя на чтение касается того же самого файла, существование которого было установлено во время последне- го вызова функции. Ядро сохраняет в пространстве процесса пользо- вательский адрес bigbuf, количество байт, которые нужно прочитать процессу (1024), и начальное смещение в файле (20), взятое из таблицы файлов. Ядро преобразует смещение внутри файла в номер дискового блока, как раньше, и считывает блок. Если между вызова- ми функции read прошло непродолжительное время, есть шансы, что блок находится в буферном кеше. Однако, ядро не может полностью удовлетворить запрос пользователя на чтение за счет содержимого буфера, поскольку только 1004 байта из 1024 для данного запроса находятся в буфере. Поэтому оно копирует оставшиеся 1004 байта из буфера в пользовательскую структуру данных bigbuf и корректирует параметры в пространстве процесса таким образом, чтобы следующий шаг цикла чтения начинался в файле с байта 1024, при этом данные следует копировать по адресу байта 1004 в bigbuf в объеме 20 байт, чтобы удовлетворить запрос на чтение. Теперь ядро переходит к началу цикла, содержащегося в алго- ритме read. Оно преобразует смещение в байтах (1024) в номер ло- гического блока (1), обращается ко второму блоку прямой адреса- ции, номер которого хранится в индексе, и отыскивает точный дисковый блок, из которого будет производиться чтение. Ядро счи- тывает блок из буферного кеша или с диска, если в кеше данный блок отсутствует. Наконец, оно копирует 20 байт из буфера по уточненному адресу в пользовательский процесс. Прежде чем выйти из системной функции, ядро устанавливает значение поля смещения в таблице файлов равным 1044, то есть равным значению смещения в байтах до места, куда будет производиться следующее обращение. В последнем вызове функции read из примера ядро ведет себя, как и в первом обращении к функции, за исключением того, что чтение из файла в данном случае начинается с байта 1044, так как именно это значение будет обнаружено в поле смещения той записи таблицы фай- лов, которая соответствует указанному дескриптору. Пример показывает, насколько выгодно для запросов ввода-выво- да работать с данными, начинающимися на границах блоков файловой системы и имеющими размер, кратный размеру блока. Это позволяет ядру избегать дополнительных итераций при выполнении цикла в ал- горитме read и всех вытекающих последствий, связанных с дополни- тельными обращениями к индексу в поисках номера блока, который содержит данные, и с конкуренцией за использование буферного пу- ла. Библиотека стандартных модулей ввода-вывода создана таким об- разом, чтобы скрыть от пользователей размеры буферов ядра; ее ис- пользование позволяет избежать потерь производительности, присущих процессам, работающим с небольшими порциями данных, из-за чего их функционирование на уровне файловой системы неэф- фективно (см. упражнение 5.4). Выполняя цикл чтения, ядро определяет, является ли файл объ- ектом чтения с продвижением: если процесс считывает последова- тельно два блока, ядро предполагает, что все очередные операции будут производить последовательное чтение, до тех пор, пока не будет утверждено обратное. На каждом шаге цикла ядро запоминает номер следующего логического блока в копии индекса, хранящейся в памяти, и на следующем шаге сравнивает номер текущего логического блока со значением, запомненным ранее. Если эти номера равны, яд- ро вычисляет номер физического блока для чтения с продвижением и сохраняет это значение в пространстве процесса для использования в алгоритме breada. Конечно же, пока процесс не считал конец бло- ка, ядро не запустит алгоритм чтения с продвижением для следующе- го блока. Обратившись к Рисунку 4.9, вспомним, что номера некоторых блоков в индексе или в блоках косвенной адресации могут иметь ну- левое значение, пусть даже номера последующих блоков и ненулевые. Если процесс попытается прочитать данные из такого блока, ядро выполнит запрос, выделяя произвольный буфер в цикле read, очищая его содержимое и копируя данные из него по адресу пользователя. Этот случай не имеет ничего общего с тем случаем, когда процесс обнаруживает конец файла, говорящий о том, что после этого места запись информации никогда не производилась. Обнаружив конец фай- ла, ядро не возвращает процессу никакой информации (см. упражне- ние 5.1). Когда процесс вызывает системную функцию read, ядро блокирует индекс на время выполнения вызова. Впоследствии, этот процесс мо- жет приостановиться во время чтения из буфера, ассоциированного с данными или с блоками косвенной адресации в индексе. Если еще одному процессу дать возможность вносить изменения в файл в то время, когда первый процесс приостановлен, функция read может возвратить несогласованные данные. Например, процесс может счи- тать из файла несколько блоков; если он приостановился во время чтения первого блока, а второй процесс собирался вести запись в другие блоки, возвращаемые данные будут содержать старые данные вперемешку с новыми. Таким образом, индекс остается заблокирован- ным на все время выполнения вызова функции read для того, чтобы процессы могли иметь целостное видение файла, то есть видение то- го образа, который был у файла перед вызовом функции. Ядро может выгружать процесс, ведущий чтение, в режим задачи на время между двумя вызовами функций и планировать запуск других процессов. Так как по окончании выполнения системной функции с индекса снимается блокировка, ничто не мешает другим процессам обращаться к файлу и изменять его содержимое. Со стороны системы было бы несправедливо держать индекс заблокированным все время от момента, когда процесс открыл файл, и до того момента, когда файл будет закрыт этим процессом, поскольку тогда один процесс будет держать все время файл открытым, тем самым не давая другим про- цессам возможности обратиться к файлу. Если файл имеет имя "/etc/ passwd", то есть является файлом, используемым в процессе регист- рации для проверки пользовательского пароля, один пользователь может умышленно (или, возможно, неумышленно) воспрепятствовать регистрации в системе всех остальных пользователей. Чтобы пре- дотвратить возникновение подобных проблем, ядро снимает с индекса блокировку по окончании выполнения каждого вызова системной функ- ции, использующей индекс. Если второй процесс внесет изменения в файл между двумя вызовами функции read, производимыми первым про- цессом, первый процесс может прочитать непредвиденные данные, од- нако структуры данных ядра сохранят свою согласованность. Предположим, к примеру, что ядро выполняет два процесса, кон- курирующие между собой (Рисунок 5.8). Если допустить, что оба процесса выполняют операцию open до того, как любой из них вызы- вает системную функцию read или write, ядро может выполнять функ- ции чтения и записи в любой из шести последовательностей: чте- ние1, чтение2, запись1, запись2, или чтение1, запись1, чтение2, запись2, или чтение1, запись1, запись2, чтение2 и т.д. Состав ин- формации, считываемой процессом A, зависит от последовательности, в которой система выполняет функции, вызываемые двумя процессами; система не гарантирует, что данные в файле останутся такими же, какими они были после открытия файла. Использование возможности захвата файла и записей (раздел 5.4) позволяет процессу гаранти- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё /* процесс A */ Ё Ё main() Ё Ё { Ё Ё int fd; Ё Ё char buf[512]; Ё Ё fd = open("/etc/passwd",O_RDONLY); Ё Ё read(fd,buf,sizeof(buf)); /* чтение1 */ Ё Ё read(fd,buf,sizeof(buf)); /* чтение2 */ Ё Ё } Ё Ё Ё Ё /* процесс B */ Ё Ё main() Ё Ё { Ё Ё int fd,i; Ё Ё char buf[512]; Ё Ё for (i = 0; i < sizeof(buf); i++) Ё Ё buf[i] = 'a'; Ё Ё fd = open("/etc/passwd",O_WRONLY); Ё Ё write(fd,buf,sizeof(buf)); /* запись1 */ Ё Ё write(fd,buf,sizeof(buf)); /* запись2 */ Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.8. Процессы, ведущие чтение и запись ровать сохранение целостности файла после его открытия. Наконец, программа на Рисунке 5.9 показывает, как процесс мо- жет открывать файл более одного раза и читать из него, используя разные файловые дескрипторы. Ядро работает со значениями смещений в таблице файлов, ассоциированными с двумя файловыми дескриптора- ми, независимо, и поэтому массивы buf1 и buf2 будут по завершении выполнения процесса идентичны друг другу при условии, что ни один процесс в это время не производил запись в файл "/etc/passwd". 5.3 WRITE Синтаксис вызова системной функции write (писать): number = write(fd,buffer,count); где переменные fd, buffer, count и number имеют тот же смысл, что и для вызова системной функции read. Алгоритм записи в обычный файл похож на алгоритм чтения из обычного файла. Однако, если в файле отсутствует блок, соответствующий смещению в байтах до места, куда должна производиться запись, ядро выделяет блок, ис- пользуя алгоритм alloc, и присваивает ему номер в соответствии с точным указанием места в таблице содержимого индекса. Если смеще- ние в байтах совпадает со смещением для блока косвенной адреса- ции, ядру, возможно, придется выделить несколько блоков для ис- пользования их в качестве блоков косвенной адресации и информаци- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main() Ё Ё { Ё Ё int fd1,fd2; Ё Ё char buf1[512],buf2[512]; Ё Ё Ё Ё fd1 = open("/etc/passwd",O_RDONLY); Ё Ё fd2 = open("/etc/passwd",O_RDONLY); Ё Ё read(fd1,buf1,sizeof(buf1)); Ё Ё read(fd2,buf2,sizeof(buf2)); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.9. Чтение из файла с использованием двух дескрипторов онных блоков. Индекс блокируется на все время выполнения функции write, так как ядро может изменить индекс, выделяя новые блоки; разрешение другим процессам обращаться к файлу может разрушить индекс, если несколько процессов выделяют блоки одновременно, ис- пользуя одни и те же значения смещений. Когда запись завершается, ядро корректирует размер файла в индексе, если файл увеличился в размере. Предположим, к примеру, что процесс записывает в файл байт с номером 10240, наибольшим номером среди уже записанных в файле. Обратившись к байту в файле по алгоритму bmap, ядро обнаружит, что в файле отсутствует не только соответствующий этому байту блок, но также и нужный блок косвенной адресации. Ядро назначает дисковый блок в качестве блока косвенной адресации и записывает номер блока в копии индекса, хранящейся в памяти. Затем оно выде- ляет дисковый блок под данные и записывает его номер в первую по- зицию вновь созданного блока косвенной адресации. Так же, как в алгоритме read, ядро входит в цикл, записывая на диск по одному блоку на каждой итерации. При этом на каждой итерации ядро определяет, будет ли производиться запись целого блока или только его части. Если записывается только часть блока, ядро в первую очередь считывает блок с диска для того, чтобы не затереть те части, которые остались без изменений, а если записы- вается целый блок, ядру не нужно читать весь блок, так как в лю- бом случае оно затрет предыдущее содержимое блока. Запись осу- ществляется поблочно, однако ядро использует отложенную запись (раздел 3.4) данных на диск, запоминая их в кеше на случай, если они понадобятся вскоре другому процессу для чтения или записи, а также для того, чтобы избежать лишних обращений к диску. Отложен- ная запись, вероятно, наиболее эффективна для каналов, так как другой процесс читает канал и удаляет из него данные (раздел 5.12). Но даже для обычных файлов отложенная запись эффективна, если файл создается временно и вскоре будет прочитан. Например, многие программы, такие как редакторы и электронная почта, созда- ют временные файлы в каталоге "/tmp" и быстро удаляют их. Исполь- зование отложенной записи может сократить количество обращений к диску для записи во временные файлы. ЗАХВАТ ФАЙЛА И ЗАПИСИ В первой версии системы UNIX, разработанной Томпсоном и Ричи, отсутствовал внутренний механизм, с помощью которого процессу мог бы быть обеспечен исключительный доступ к файлу. Механизм захвата был признан излишним, поскольку, как отмечает Ричи, "мы не имеем дела с большими базами данных, состоящими из одного файла, кото- рые поддерживаются независимыми процессами" (см. [Ritchie 81]). Для того, чтобы повысить привлекательность системы UNIX для ком- мерческих пользователей, работающих с базами данных, в версию V системы ныне включены механизмы захвата файла и записи. Захват файла - это средство, позволяющее запретить другим процессам про- изводить чтение или запись любой части файла, а захват записи - это средство, позволяющее запретить другим процессам производить ввод-вывод указанных записей (частей файла между указанными сме- щениями). В упражнении 5.9 рассматривается реализация механизма захвата файла и записи. 5.5 УКАЗАНИЕ МЕСТА В ФАЙЛЕ, ГДЕ БУДЕТ ВЫПОЛНЯТЬСЯ ВВОД-ВЫВОД - LSEEK Обычное использование системных функций read и write обеспе- чивает последовательный доступ к файлу, однако процессы могут ис- пользовать вызов системной функции lseek для указания места в файле, где будет производиться ввод-вывод, и осуществления произ- вольного доступа к файлу. Синтаксис вызова системной функции: position = lseek(fd,offset,reference); где fd - дескриптор файла, идентифицирующий файл, offset - смеще- ние в байтах, а reference указывает, является ли значение offset смещением от начала файла, смещением от текущей позиции ввода-вы- вода или смещением от конца файла. Возвращаемое значение, position, является смещением в байтах до места, где будет начи- наться следующая операция чтения или записи. Например, в програм- ме, приведенной на Рисунке 5.10, процесс открывает файл, считыва- ет байт, а затем вызывает функцию lseek, чтобы заменить значение поля смещения в таблице файлов величиной, равной 1023 (с перемен- ной reference, имеющей значение 1), и выполняет цикл. Таким обра- зом, программа считывает каждый 1024-й байт файла. Если reference имеет значение 0, ядро осуществляет поиск от начала файла, а если 2, ядро ведет поиск от конца файла. Функция lseek ничего не долж- на делать, кроме операции поиска, которая позиционирует головку чтения-записи на указанный дисковый сектор. Для того, чтобы вы- полнить функцию lseek, ядро просто выбирает значение смещения из таблицы файлов; в последующих вызовах функций read и write смеще- ние из таблицы файлов используется в качестве начального смеще- ния. 5.6 CLOSE Процесс закрывает открытый файл, когда процессу больше не нужно обращаться к нему. Синтаксис вызова системной функции close (закрыть): здддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё int fd,skval; Ё Ё char c; Ё Ё Ё Ё if(argc != 2) Ё Ё exit(); Ё Ё fd = open(argv[1],O_RDONLY); Ё Ё if (fd == -1) Ё Ё exit(); Ё Ё while ((skval = read(fd,&c,1)) == 1) Ё Ё { Ё Ё printf("char %c\n",c); Ё Ё skval = lseek(fd,1023L,1); Ё Ё printf("new seek val %d\n",skval); Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.10. Программа, содержащая вызов системной функции lseek close(fd); где fd - дескриптор открытого файла. Ядро выполняет операцию зак- рытия, используя дескриптор файла и информацию из соответствующих записей в таблице файлов и таблице индексов. Если счетчик ссылок в записи таблицы файлов имеет значение, большее, чем 1, в связи с тем, что были обращения к функциям dup или fork, то это означает, что на запись в таблице файлов делают ссылку другие пользователь- ские дескрипторы, что мы увидим далее; ядро уменьшает значение счетчика и операция закрытия завершается. Если счетчик ссылок в таблице файлов имеет значение, равное 1, ядро освобождает запись в таблице и индекс в памяти, ранее выделенный системной функцией open (алгоритм iput). Если другие процессы все еще ссылаются на индекс, ядро уменьшает значение счетчика ссылок на индекс, но ос- тавляет индекс процессам; в противном случае индекс освобождается для переназначения, так как его счетчик ссылок содержит 0. Когда выполнение системной функции close завершается, запись в таблице пользовательских дескрипторов файла становится пустой. Попытки процесса использовать данный дескриптор заканчиваются ошибкой до тех пор, пока дескриптор не будет переназначен другому файлу в результате выполнения другой системной функции. Когда процесс за- вершается, ядро проверяет наличие активных пользовательских деск- рипторов файла, принадлежавших процессу, и закрывает каждый из них. Таким образом, ни один процесс не может оставить файл откры- тым после своего завершения. На Рисунке 5.11, например, показаны записи из таблиц, приведенных на Рисунке 5.4, после того, как второй процесс закры- вает соответствующие им файлы. Записи, соответствующие дескрипто- рам 3 и 4 в таблице пользовательских дескрипторов файлов, пусты. Счетчики в записях таблицы файлов теперь имеют значение 0, а сами записи пусты. Счетчики ссылок на файлы "/etc/passwd" и "private" в индексах также уменьшились. Индекс для файла "private" находит- ся в списке свободных индексов, поскольку счетчик ссылок на него равен 0, но запись о нем не пуста. Если еще какой-нибудь процесс пользовательские дескрип- торы файла таблица файлов таблица индексов зддддддддд© здддддддддддд© здддддддддддддд© 0Ё Ё Ё Ё Ё Ы Ё цддддддддд╢ Ё Ё Ё Ы Ё 1Ё Ё Ё Ё Ё Ы Ё цддддддддд╢ цдддддддддддд╢ Ё Ы Ё 2Ё Ё Ё Ы Ё Ё Ы Ё цддддддддд╢ Ё Ы Ё Ё Ы Ё 3Ё ддддедддд© Ё Ы Ё Ё Ы Ё цддддддддд╢ Ё Ё Ы Ё цдддддддддддддд╢ 4Ё ддддеддд©Ё Ё Ы Ё здддд>Ё счет- Ё цддддддддд╢ ЁЁ Ё Ы Ё Ё Ё чик (/etc/ Ё 5Ё ддддедд©ЁЁ цдддддддддддд╢ Ё здд>Ё 2 passwd)Ё цддддддддд╢ ЁЁЁ Ё счет- Ё Ё Ё цдддддддддддддд╢ Ё Ы Ё ЁЁюдд>Ё чик едды Ё Ё Ы Ё Ё Ы Ё ЁЁ Ё 1 Ё Ё Ё Ы Ё Ё Ы Ё ЁЁ цдддддддддддд╢ Ё Ё Ы Ё юддддддддды ЁЁ Ё Ы Ё Ё Ё Ы Ё ЁЁ Ё Ы Ё Ё Ё Ы Ё зддддддддд© ЁЁ Ё Ы Ё Ё Ё Ы Ё 0Ё Ё ЁЁ цдддддддддддд╢ Ё Ё Ы Ё цддддддддд╢ ЁЁ Ё счет- Ё Ё Ё Ы Ё 1Ё Ё Ёюддд>Ё чик еддддЁ© Ё Ы Ё цддддддддд╢ Ё Ё 1 Ё ЁЁ Ё Ы Ё 2Ё Ё Ё цдддддддддддд╢ ЁЁ Ё Ы Ё цддддддддд╢ Ё Ё Ы Ё ЁЁ цдддддддддддддд╢ 3Ё NULL Ё Ё Ё Ы Ё ЁЁ Ё счет- Ё цддддддддд╢ Ё Ё Ы Ё Ёюд>Ё чик (local)Ё 4Ё NULL Ё Ё Ё Ы Ё Ё Ё 1 Ё цддддддддд╢ Ё Ё Ы Ё Ё цдддддддддддддд╢ 5Ё Ё Ё Ё Ы Ё Ё Ё Ы Ё цддддддддд╢ Ё цдддддддддддд╢ Ё Ё Ы Ё Ё Ы Ё Ё Ё счет- Ё Ё Ё Ы Ё Ё Ы Ё Ё Ё чик Ё Ё Ё Ы Ё Ё Ы Ё Ё Ё 0 Ё Ё Ё Ы Ё юддддддддды Ё цдддддддддддд╢ Ё Ё Ы Ё Ё Ё Ы Ё Ё Ё Ы Ё Ё Ё Ы Ё Ё цдддддддддддддд╢ Ё Ё Ы Ё Ё Ё счет- Ё Ё цдддддддддддд╢ Ё Ё чик (private)Ё Ё Ё счет- Ё Ё Ё 0 Ё юдддд>Ё чик едддды цдддддддддддддд╢ Ё 1 Ё Ё Ы Ё цдддддддддддд╢ Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юдддддддддддддды Ё Ы Ё цдддддддддддд╢ Ё счет- Ё Ё чик Ё Ё 0 Ё юдддддддддддды Рисунок 5.11. Таблицы после закрытия файла обратится к файлу "private", пока индекс еще находится в списке свободных индексов, ядро востребует индекс обратно, как показано в разделе 4.1.2. 5.7 СОЗДАНИЕ ФАЙЛА Системная функция open дает процессу доступ к существующему файлу, а системная функция creat создает в системе новый файл. Синтаксис вызова системной функции creat: fd = creat(pathname,modes); где переменные pathname, modes и fd имеют тот же смысл, что и в системной функции open. Если прежде такого файла не существовало, ядро создает новый файл с указанным именем и указанными правами доступа к нему; если же такой файл уже существовал, ядро усекает файл (освобождает все существующие блоки данных и устанавливает размер файла равным 0) при наличии соответствующих прав доступа к нему (***). На Рисунке 5.12 приведен алгоритм создания файла. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм creat Ё Ё входная информация: имя файла Ё Ё установки прав доступа к файлу Ё Ё выходная информация: дескриптор файла Ё Ё { Ё Ё получить индекс для данного имени файла (алгоритм namei);Ё Ё если (файл уже существует) Ё Ё { Ё Ё если (доступ не разрешен) Ё Ё { Ё Ё освободить индекс (алгоритм iput); Ё Ё возвратить (ошибку); Ё Ё } Ё Ё } Ё Ё в противном случае /* файл еще не существует */ Ё Ё { Ё Ё назначить свободный индекс из файловой системы (алго- Ё Ё ритм ialloc); Ё Ё создать новую точку входа в родительском каталоге: Ё Ё включить имя нового файла и номер вновь назначенного Ё Ё индекса; Ё Ё } Ё Ё выделить для индекса запись в таблице файлов, инициализи-Ё Ё ровать счетчик; Ё Ё если (файл существовал к моменту создания) Ё Ё освободить все блоки файла (алгоритм free); Ё Ё снять блокировку (с индекса); Ё Ё возвратить (пользовательский дескриптор файла); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.12. Алгоритм создания файла Ядро проводит синтаксический анализ имени пути поиска, ис- пользуя алгоритм namei и следуя этому алгоритму буквально, когда речь идет о разборе имен каталогов. Однако, когда дело касается последней компоненты имени пути поиска, а именно идентификатора создаваемого файла, namei отмечает смещение в байтах до первой ддддддддддддддддддддддддддддддддддддддд (***) Системная функция open имеет два флага, O_CREAT (создание) и O_TRUNC (усечение). Если процесс устанавливает в вызове функции флаг O_CREAT и файл не существует, ядро создаст файл. Если файл уже существует, он не будет усечен, если только не установлен флаг O_TRUNC. пустой позиции в каталоге и запоминает это смещение в пространс- тве процесса. Если ядро не обнаружило в каталоге компоненту имени пути поиска, оно в конечном счете впишет имя компоненты в только что найденную пустую позицию. Если в каталоге нет пустых позиций, ядро запоминает смещение до конца каталога и создает новую пози- цию там. Оно также запоминает в пространстве процесса индекс просматриваемого каталога и держит индекс заблокированным; ката- лог становится по отношению к новому файлу родительским катало- гом. Ядро не записывает пока имя нового файла в каталог, так что в случае возникновения ошибок ядру приходится меньше переделы- вать. Оно проверяет наличие у процесса разрешения на запись в ка- талог. Поскольку процесс будет производить запись в каталог в ре- зультате выполнения функции creat, наличие разрешения на запись в каталог означает, что процессам дозволяется создавать файлы в ка- талоге. Предположив, что под данным именем ранее не существовало фай- ла, ядро назначает новому файлу индекс, используя алгоритм ialloc (раздел 4.6). Затем оно записывает имя нового файла и номер вновь выделенного индекса в родительский каталог, а смещение в байтах сохраняет в пространстве процесса. Впоследствии ядро освобождает индекс родительского каталога, удерживаемый с того времени, когда в каталоге производился поиск имени файла. Родительский каталог теперь содержит имя нового файла и его индекс. Ядро записывает вновь выделенный индекс на диск (алгоритм bwrite), прежде чем за- писать на диск каталог с новым именем. Если между операциями за- писи индекса и каталога произойдет сбой системы, в итоге окажет- ся, что выделен индекс, на который не ссылается ни одно из имен путей поиска в системе, однако система будет функционировать нор- мально. Если, с другой стороны, каталог был записан раньше вновь выделенного индекса и сбой системы произошел между ними, файловая система будет содержать имя пути поиска, ссылающееся на неверный индекс (более подробно об этом см. в разделе 5.16.1). Если данный файл уже существовал до вызова функции creat, яд- ро обнаруживает его индекс во время поиска имени файла. Старый файл должен позволять процессу производить запись в него, чтобы можно было создать "новый" файл с тем же самым именем, так как ядро изменяет содержимое файла при выполнении функции creat: оно усекает файл, освобождая все информационные блоки по алгоритму free, так что файл будет выглядеть как вновь созданный. Тем не менее, владелец и права доступа к файлу остаются прежними: ядро не передает право собственности на файл владельцу процесса и иг- норирует права доступа, указанные процессом в вызове функции. На- конец, ядро не проверяет наличие разрешения на запись в каталог, являющийся родительским для существующего файла, поскольку оно не меняет содержимого каталога. Функция creat продолжает работу, выполняя тот же алгоритм, что и функция open. Ядро выделяет созданному файлу запись в таб- лице файлов, чтобы процесс мог читать из файла, а также запись в таблице пользовательских дескрипторов файла, и в конце концов возвращает указатель на последнюю запись в виде пользовательского дескриптора файла. 5.8 СОЗДАНИЕ СПЕЦИАЛЬНЫХ ФАЙЛОВ Системная функция mknod создает в системе специальные файлы, в число которых включаются поименованные каналы, файлы устройств и каталоги. Она похожа на функцию creat в том, что ядро выделяет для файла индекс. Синтаксис вызова системной функции mknod: mknod(pathname,type and permissions,dev) где pathname - имя создаваемой вершины в иерархической структуре файловой системы, type and permissions - тип вершины (например, каталог) и права доступа к создаваемому файлу, а dev указывает старший и младший номера устройства для блочных и символьных спе- циальных файлов (глава 10). На Рисунке 5.13 приведен алгоритм, реализуемый функцией mknod при создании новой вершины. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм создания новой вершины Ё Ё входная информация: вершина (имя файла) Ё Ё тип файла Ё Ё права доступа Ё Ё старший, младший номера устройства Ё Ё (для блочных и символьных специальных Ё Ё файлов) Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё если (новая вершина не является поименованным каналом Ё Ё и пользователь не является суперпользователем) Ё Ё возвратить (ошибку); Ё Ё получить индекс вершины, являющейся родительской для Ё Ё новой вершины (алгоритм namei); Ё Ё если (новая вершина уже существует) Ё Ё { Ё Ё освободить родительский индекс (алгоритм iput); Ё Ё возвратить (ошибку); Ё Ё } Ё Ё назначить для новой вершины свободный индекс из файловойЁ Ё системы (алгоритм ialloc); Ё Ё создать новую запись в родительском каталоге: включить Ё Ё имя новой вершины и номер вновь назначенного индекса; Ё Ё освободить индекс родительского каталога (алгоритм Ё Ё iput); Ё Ё если (новая вершина является блочным или символьным спе-Ё Ё циальным файлом) Ё Ё записать старший и младший номера в структуру индек-Ё Ё са; Ё Ё освободить индекс новой вершины (алгоритм iput); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.13. Алгоритм создания новой вершины Ядро просматривает файловую систему в поисках имени файла, который оно собирается создать. Если файл еще пока не существует, ядро назначает ему новый индекс на диске и записывает имя нового файла и номер индекса в родительский каталог. Оно устанавливает значение поля типа файла в индексе, указывая, что файл является каналом, каталогом или специальным файлом. Наконец, если файл яв- ляется специальным файлом устройства блочного или символьного ти- па, ядро записывает в индекс старший и младший номера устройства. Если функция mknod создает каталог, он будет существовать по за- вершении выполнения функции, но его содержимое будет иметь невер- ный формат (в каталоге будут отсутствовать записи с именами "." и ".."). В упражнении 5.33 рассматриваются шаги, необходимые для преобразования содержимого каталога в правильный формат. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм смены каталога Ё Ё входная информация: имя нового каталога Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё получить индекс для каталога с новым именем (алгоритм Ё Ё namei); Ё Ё если (индекс не является индексом каталога или же про- Ё Ё цессу не разрешен доступ к файлу) Ё Ё { Ё Ё освободить индекс (алгоритм iput); Ё Ё возвратить (ошибку); Ё Ё } Ё Ё снять блокировку с индекса; Ё Ё освободить индекс прежнего текущего каталога (алгоритм Ё Ё iput); Ё Ё поместить новый индекс в позицию для текущего каталога Ё Ё в пространстве процесса; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.14. Алгоритм смены текущего каталога 5.9 СМЕНА ТЕКУЩЕГО И КОРНЕВОГО КАТАЛОГА Когда система загружается впервые, нулевой процесс делает корневой каталог файловой системы текущим на время инициализации. Для индекса корневого каталога нулевой процесс выполняет алгоритм iget, сохраняет этот индекс в пространстве процесса в качестве индекса текущего каталога и снимает с индекса блокировку. Когда с помощью функции fork создается новый процесс, он наследует те- кущий каталог старого процесса в своем адресном пространстве, а ядро, соответственно, увеличивает значение счетчика ссылок в ин- дексе. Алгоритм chdir (Рисунок 5.14) изменяет имя текущего каталога для процесса. Синтаксис вызова системной функции chdir: chdir(pathname); где pathname - каталог, который становится текущим для процесса. Ядро анализирует имя каталога, используя алгоритм namei, и прове- ряет, является ли данный файл каталогом и имеет ли владелец про- цесса право доступа к каталога. Ядро снимает с нового индекса блокировку, но удерживает индекс в качестве выделенного и остав- ляет счетчик ссылок без изменений, освобождает индекс прежнего текущего каталога (алгоритм iput), хранящийся в пространстве процесса, и запоминает в этом пространстве новый индекс. После смены процессом текущего каталога алгоритм namei использует ин- декс в качестве начального каталога при анализе всех имен путей, которые не берут начало от корня. По окончании выполнения систем- ной функции chdir счетчик ссылок на индекс нового каталога имеет значение, как минимум, 1, а счетчик ссылок на индекс прежнего те- кущего каталога может стать равным 0. В этом отношении функция chdir похожа на функцию open, поскольку обе функции обращаются к файлу и оставляют его индекс в качестве выделенного. Индекс, вы- деленный во время выполнения функции chdir, освобождается только тогда, когда процесс меняет текущий каталог еще раз или когда процесс завершается. Процессы обычно используют глобальный корневой каталог файло- вой системы для всех имен путей поиска, начинающихся с "/". Ядро хранит глобальную переменную, которая указывает на индекс гло- бального корня, выделяемый по алгоритму iget при загрузке систе- мы. Процессы могут менять свое представление о корневом каталоге файловой системы с помощью системной функции chroot. Это бывает полезно, если пользователю нужно создать модель обычной иерархи- ческой структуры файловой системы и запустить процессы там. Син- таксис вызова функции: chroot(pathname); где pathname - каталог, который впоследствии будет рассматривать- ся ядром в качестве корневого каталога для процесса. Выполняя функцию chroot, ядро следует тому же алгоритму, что и при смене текущего каталога. Оно запоминает индекс нового корня в прост- ранстве процесса, снимая с индекса блокировку по завершении вы- полнения функции. Тем не менее, так как умолчание на корень для ядра хранится в глобальной переменной, ядро освобождает индекс прежнего корня не автоматически, а только после того, как оно са- мо или процесс-предок исполнят вызов функции chroot. Новый индекс становится логическим корнем файловой системы для процесса (и для всех порожденных им процессов) и это означает, что все пути поиска в алгоритме namei, начинающиеся с корня ("/"), возьмут на- чало с данного индекса и что все попытки войти в каталог ".." над корнем приведут к тому, что рабочим каталогом процесса останется новый корень. Процесс передает всем вновь порождаемым процессам этот каталог в качестве корневого подобно тому, как передает свой текущий каталог. 5.10 СМЕНА ВЛАДЕЛЬЦА И РЕЖИМА ДОСТУПА К ФАЙЛУ Смена владельца или режима (прав) доступа к файлу является операцией, производимой над индексом, а не над файлом. Синтаксис вызова соответствующих системных функций: chown(pathname,owner,group) chmod(pathname,mode) Для того, чтобы поменять владельца файла, ядро преобразует имя файла в идентификатор индекса, используя алгоритм namei. Вла- делец процесса должен быть суперпользователем или владельцем фай- ла (процесс не может распоряжаться тем, что не принадлежит ему). Затем ядро назначает файлу нового владельца и нового группового пользователя, сбрасывает флаги прежних установок (см. раздел 7.5) и освобождает индекс по алгоритму iput. После этого прежний вла- делец теряет право "собственности" на файл. Для того, чтобы поме- нять режим доступа к файлу, ядро выполняет процедуру, подобную описанной, вместо кода владельца меняя флаги, устанавливающие ре- жим доступа. 5.11 STAT И FSTAT Системные функции stat и fstat позволяют процессам запраши- вать информацию о статусе файла: типе файла, владельце файла, правах доступа, размере файла, числе связей, номере индекса и времени доступа к файлу. Синтаксис вызова функций: stat(pathname,statbuffer); fstat(fd,statbuffer); где pathname - имя файла, fd - дескриптор файла, возвращаемый функцией open, statbuffer - адрес структуры данных пользователь- ского процесса, где будет храниться информация о статусе файла после завершения выполнения вызова. Системные функции просто пе- реписывают поля из индекса в структуру statbuffer. Программа на Рисунке 5.33 иллюстрирует использование функций stat и fstat. Вызывает канал Не могут совместно использовать Ы канал Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Процесс A Ы Ы Ё Ы Ы здддддддддддддадддддддддддддд© Ы Ы Ё Ё Ы Процесс B Процесс C Ё здддддддаддддддд© Ё Ё Процесс D Ы Процесс E Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Совместно используют канал Рисунок 5.15. Дерево процессов и совместное использование ка- налов 5.12 КАНАЛЫ Каналы позволяют передавать данные между процессами в порядке поступления ("первым пришел - первым вышел"), а также синхронизи- ровать выполнение процессов. Их использование дает процессам воз- можность взаимодействовать между собой, пусть даже не известно, какие процессы находятся на другом конце канала. Традиционная ре- ализация каналов использует файловую систему для хранения данных. Различают два вида каналов: поименованные каналы и, за отсутстви- ем лучшего термина, непоименованные каналы, которые идентичны между собой во всем, кроме способа первоначального обращения к ним процессов. Для поименованных каналов процессы используют сис- темную функцию open, а системную функцию pipe - для создания не- поименованного канала. Впоследствии, при работе с каналами про- цессы пользуются обычными системными функциями для файлов, такими как read, write и close. Только связанные между собой процессы, являющиеся потомками того процесса, который вызвал функцию pipe, могут разделять доступ к непоименованным каналам. Например (см. Рисунок 5.15), если процесс B создает канал и порождает процессы D и E, эти три процесса разделяют между собой доступ к каналу, в отличие от процессов A и C. Однако, все процессы могут обращаться к поименованному каналу независимо от взаимоотношений между ними, при условии наличия обычных прав доступа к файлу. Поскольку непо- именованные каналы встречаются чаще, они будут рассмотрены первы- ми. 5.12.1 Системная функция pipe Синтаксис вызова функции создания канала: pipe(fdptr); где fdptr - указатель на массив из двух целых переменных, в кото- ром будут храниться два дескриптора файла для чтения из канала и для записи в канал. Поскольку ядро реализует каналы внутри файло- вой системы и поскольку канал не существует до того, как его бу- дут использовать, ядро должно при создании канала назначить ему индекс. Оно также назначает для канала пару пользовательских дескрипторов и соответствующие им записи в таблице файлов: один из дескрипторов для чтения из канала, а другой для записи в ка- нал. Поскольку ядро пользуется таблицей файлов, интерфейс для вы- зова функций read, write и др. согласуется с интерфейсом для обычных файлов. В результате процессам нет надобности знать, ве- дут ли они чтение или запись в обычный файл или в канал. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм pipe Ё Ё входная информация: отсутствует Ё Ё выходная информация: дескриптор файла для чтения Ё Ё дескриптор файла для записи Ё Ё { Ё Ё назначить новый индекс из устройства канала (алгоритм Ё Ё ialloc); Ё Ё выделить одну запись в таблице файлов для чтения, одну -Ё Ё для переписи; Ё Ё инициализировать записи в таблице файлов таким образом, Ё Ё чтобы они указывали на новый индекс; Ё Ё выделить один пользовательский дескриптор файла для чте-Ё Ё ния, один - для записи, проинициализировать их таким Ё Ё образом, чтобы они указывали на соответствующие точки Ё Ё входа в таблице файлов; Ё Ё установить значение счетчика ссылок в индексе равным 2; Ё Ё установить значение счетчика числа процессов, производя-Ё Ё щих чтение, и процессов, производящих запись, равным 1;Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.16. Алгоритм создания каналов (непоименованных) На Рисунке 5.16 показан алгоритм создания непоименованных ка- налов. Ядро назначает индекс для канала из файловой системы, обозначенной как "устройство канала", используя алгоритм ialloc. Устройство канала - это именно та файловая система, из которой ядро может назначать каналам индексы и выделять блоки для данных. Администраторы системы указывают устройство канала при конфигури- ровании системы и эти устройства могут совпадать у разных файло- вых систем. Пока канал активен, ядро не может переназначить ин- декс канала и информационные блоки канала другому файлу. Затем ядро выделяет в таблице файлов две записи, соответству- ющие дескрипторам для чтения и записи в канал, и корректирует "бухгалтерскую" информацию в копии индекса в памяти. В каждой из выделенных записей в таблице файлов хранится информация о том, сколько экземпляров канала открыто для чтения или записи (перво- начально 1), а счетчик ссылок в индексе указывает, сколько раз канал был "открыт" (первоначально 2 - по одному для каждой записи таблицы файлов). Наконец, в индексе записываются смещения в бай- тах внутри канала до места, где будет начинаться следующая опера- ция записи или чтения. Благодаря сохранению этих смещений в ин- дексе имеется возможность производить доступ к данным в канале в порядке их поступления в канал ("первым пришел - первым вышел"); этот момент является особенностью каналов, поскольку для обычных файлов смещения хранятся в таблице файлов. Процессы не могут ме- нять эти смещения с помощью системной функции lseek и поэтому произвольный доступ к данным канала невозможен. 5.12.2 Открытие поименованного канала Поименованный канал - это файл, имеющий почти такую же семан- тику, как и непоименованный канал, за исключением того, что этому файлу соответствует запись в каталоге и обращение к нему произво- дится по имени. Процессы открывают поименованные каналы так же, как и обычные файлы, и, следовательно, с помощью поименованных каналов могут взаимодействовать между собой даже процессы, не имеющие друг к другу близкого отношения. Поименованные каналы постоянно присутствуют в иерархии файловой системы (из которой они удаляются с помощью системной функции unlink), а непоимено- ванные каналы являются временными: когда все процессы заканчивают работу с каналом, ядро отбирает назад его индекс. Алгоритм открытия поименованного канала идентичен алгоритму открытия обычного файла. Однако, перед выходом из функции ядро увеличивает значения тех счетчиков в индексе, которые показывают количество процессов, открывших поименованный канал для чтения или записи. Процесс, открывающий поименованный канал для чтения, приостановит свое выполнение до тех пор, пока другой процесс не откроет поименованный канал для записи, и наоборот. Не имеет смысла открывать канал для чтения, если процесс не надеется полу- чить данные; то же самое касается записи. В зависимости от того, открывает ли процесс поименованный канал для записи или для чте- ния, ядро возобновляет выполнение тех процессов, которые были приостановлены в ожидании процесса, записывающего в поименованный канал или считывающего данные из канала (соответственно). Если процесс открывает поименованный канал для чтения, причем процесс, записывающий в канал, существует, открытие завершается. Или если процесс открывает поименованный файл с параметром "no delay", функция open возвращает управление немедленно, даже когда нет ни одного записывающего процесса. Во всех остальных случаях процесс приостанавливается до тех пор, пока записывающий процесс не откроет канал. Аналогичные правила действуют для процесса, от- крывающего канал для записи. 5.12.3 Чтение из каналов и запись в каналы Канал следует рассматривать под таким углом зрения, что про- цессы ведут запись на одном конце канала, а считывают данные на другом конце. Как уже говорилось выше, процессы обращаются к дан- ным в канале в порядке их поступления в канал; это означает, что очередность, в которой данные записываются в канал, совпадает с очередностью их выборки из канала. Совпадение количества процес- сов, считывающих данные из канала, с количеством процессов, веду- щих запись в канал, совсем не обязательно; если одно число отли- чается от другого более, чем на 1, процессы должны координировать свои действия по использованию канала с помощью других механиз- мов. Ядро обращается к данным в канале точно так же, как и к дан- ным в обычном файле: оно сохраняет данные на устройстве канала и назначает каналу столько блоков, сколько нужно, во время выполне- ния функции write. Различие в выделении памяти для канала и для зддддддддддддддддддддбдддддддддддддддддддд© Ё Указатель чтения Ё Указатель записи Ё юддддддддддедддддддддаддддддддддеддддддддды Ё Ё Ё здддддддддддддддды юддддддддддддддддддд© Ё Ё v v здддбдддбдддбдддбдддбдддбдддбдддбдддбддд© Ё 0 Ё 1 Ё 2 Ё 3 Ё 4 Ё 5 Ё 6 Ё 7 Ё 8 Ё 9 Ё юдддадддадддадддадддадддадддадддадддаддды Блоки прямой адресации в индексе Рисунок 5.17. Логическая схема чтения и записи в канал обычного файла состоит в том, что канал использует в индексе только блоки прямой адресации в целях повышения эффективности ра- боты, хотя это и накладывает определенные ограничения на объем данных, одновременно помещающихся в канале. Ядро работает с бло- ками прямой адресации индекса как с циклической очередью, поддер- живая в своей структуре указатели чтения и записи для обеспечения очередности обслуживания "первым пришел - первым вышел" (Рисунок 5.17). Рассмотрим четыре примера ввода-вывода в канал: запись в ка- нал, в котором есть место для записи данных; чтение из канала, в котором достаточно данных для удовлетворения запроса на чтение; чтение из канала, в котором данных недостаточно; и запись в ка- нал, где нет места для записи. Рассмотрим первый случай, в котором процесс ведет запись в канал, имеющий место для ввода данных: сумма количества записыва- емых байт с числом байт, уже находящихся в канале, меньше или равна емкости канала. Ядро следует алгоритму записи данных в обычный файл, за исключением того, что оно увеличивает размер ка- нала автоматически после каждого выполнения функции write, пос- кольку по определению объем данных в канале растет с каждой опе- рацией записи. Иначе происходит увеличение размера обычного файла: процесс увеличивает размер файла только тогда, когда он при записи данных переступает границу конца файла. Если следующее смещение в канале требует использования блока косвенной адреса- ции, ядро устанавливает значение смещения в пространстве процесса таким образом, чтобы оно указывало на начало канала (смещение в байтах, равное 0). Ядро никогда не затирает данные в канале; оно может сбросить значение смещения в 0, поскольку оно уже установи- ло, что данные не будут переполнять емкость канала. Когда процесс запишет в канал все свои данные, ядро откорректирует значение указателя записи (в индексе) канала таким образом, что следующий процесс продолжит запись в канал с того места, где остановилась предыдущая операция write. Затем ядро возобновит выполнение всех других процессов, приостановленных в ожидании считывания данных из канала. Когда процесс запускает функцию чтения из канала, он проверя- ет, пустой ли канал или нет. Если в канале есть данные, ядро счи- тывает их из канала так, как если бы канал был обычным файлом, выполняя соответствующий алгоритм. Однако, начальным смещением будет значение указателя чтения, хранящегося в индексе и показы- вающего протяженность прочитанных ранее данных. После считывания каждого блока ядро уменьшает размер канала в соответствии с коли- чеством считанных данных и устанавливает значение смещения в пространстве процесса так, чтобы при достижении конца канала оно указывало на его начало. Когда выполнение системной функции read завершается, ядро возобновляет выполнение всех приостановленных процессов записи и запоминает текущее значение указателя чтения в индексе (а не в записи таблицы файлов). Если процесс пытается считать больше информации, чем факти- чески есть в канале, функция read завершится успешно, возвратив все данные, находящиеся в данный момент в канале, пусть даже не полностью выполнив запрос пользователя. Если канал пуст, процесс обычно приостанавливается до тех пор, пока какой-нибудь другой процесс не запишет данные в канал, после чего все приостановлен- ные процессы, ожидающие ввода данных, возобновят свое выполнение и начнут конкурировать за чтение из канала. Если, однако, процесс открывает поименованный канал с параметром "no delay" (без за- держки), функция read возвратит управление немедленно, если в ка- нале отсутствуют данные. Операции чтения и записи в канал имеют ту же семантику, что и аналогичные операции для терминальных уст- ройств (глава 10), она позволяет процессам игнорировать тип тех файлов, с которыми эти программы имеют дело. Если процесс ведет запись в канал и в канале нет места для всех данных, ядро помечает индекс и приостанавливает выполнение процесса до тех пор, пока канал не начнет очищаться от данных. Когда впоследствии другой процесс будет считывать данные из кана- ла, ядро заметит существование процессов, приостановленных в ожи- дании очистки канала, и возобновит их выполнение подобно тому, как это было объяснено выше. Исключением из этого утверждения яв- ляется ситуация, когда процесс записывает в канал данные, объем которых превышает емкость канала (то есть, объем данных, которые могут храниться в блоках прямой адресации); в этом случае ядро записывает в канал столько данных, сколько он может вместить в себя, и приостанавливает процесс до тех пор, пока не освободится дополнительное место. Таким образом, возможно положение, при ко- тором записываемые данные не будут занимать непрерывное место в канале, если другие процессы ведут запись в канал в то время, на которое первый процесс прервал свою работу. Анализируя реализацию каналов, можно заметить, что интерфейс процессов согласуется с интерфейсом обычных файлов, но его вопло- щение отличается, так как ядро запоминает смещения для чтения и записи в индексе вместо того, чтобы делать это в таблице файлов. Ядро вынуждено хранить значения смещений для поименованных кана- лов в индексе для того, чтобы процессы могли совместно использо- вать эти значения: они не могли бы совместно использовать значе- ния, хранящиеся в таблице файлов, так как процесс получает новую запись в таблице файлов по каждому вызову функции open. Тем не менее, совместное использование смещений чтения и записи в индек- се наблюдалось и до реализации поименованных каналов. Процессы, обращающиеся к непоименованным каналам, разделяют доступ к каналу через общие точки входа в таблицу файлов, поэтому они могли бы по умолчанию хранить смещения записи и чтения в таблице файлов, как это принято для обычных файлов. Это не было сделано, так как про- цедуры низкого уровня, работающие в ядре, больше не имеют доступа к записям в таблице файлов: программа упростилась за счет того, что процессы совместно используют значения смещений, хранящиеся в индексе. 5.12.4 Закрытие каналов При закрытии канала процесс выполняет ту же самую процедуру, что и при закрытии обычного файла, за исключением того, что ядро, прежде чем освободить индекс канала, выполняет специальную обра- ботку. Оно уменьшает количество процессов чтения из канала или записи в канал в зависимости от типа файлового дескриптора. Если значение счетчика числа записывающих в канал процессов становится равным 0 и имеются процессы, приостановленные в ожидании чтения данных из канала, ядро возобновляет выполнение последних и они завершают свои операции чтения без возврата каких-либо данных. Если становится равным 0 значение счетчика числа считывающих из канала процессов и имеются процессы, приостановленные в ожидании возможности записи данных в канал, ядро возобновляет выполнение последних и посылает им сигнал (глава 7) об ошибке. В обоих слу- чаях не имеет смысла продолжать держать процессы приостановленны- ми, если нет надежды на то, что состояние канала когда-нибудь из- менится. Например, если процесс ожидает возможности производить чтение из непоименованного канала и в системе больше нет процес- сов, записывающих в этот канал, значит, записывающий процесс ни- когда не появится. Несмотря на то, что если канал поименованный, в принципе возможно появление нового считывающего или записываю- щего процесса, ядро трактует эту ситуацию точно так же, как и для непоименованных каналов. Если к каналу не обращается ни один за- писывающий или считывающий процесс, ядро освобождает все информа- ционные блоки канала и переустанавливает индекс таким образом, чтобы он указывал на то, что канал пуст. Когда ядро освобождает индекс обычного канала, оно освобождает для переназначения и дис- ковую копию этого индекса. зддддддддддддддддддддддддддддддддд© Ё char string[] = "hello"; Ё Ё main() Ё Ё { Ё Ё char buf[1024]; Ё Ё char *cp1,*cp2; Ё Ё int fds[2]; Ё Ё Ё Ё cp1 = string; Ё Ё cp2 = buf; Ё Ё while(*cp1) Ё Ё *cp2++ = *cp1++; Ё Ё pipe(fds); Ё Ё for (;;) Ё Ё { Ё Ё write(fds[1],buf,6); Ё Ё read(fds[0],buf,6); Ё Ё } Ё Ё } Ё юддддддддддддддддддддддддддддддддды Рисунок 5.18. Чтение из канала и запись в канал 5.12.5 Примеры Программа на Рисунке 5.18 иллюстрирует искусственное исполь- зование каналов. Процесс создает канал и входит в бесконечный цикл, записывая в канал строку символов "hello" и считывая ее из канала. Ядру не нужно ни знать о том, что процесс, ведущий запись в канал, является и процессом, считывающим из канала, ни прояв- лять по этому поводу какое-либо беспокойство. Процесс, выполняющий программу, которая приведена на Рисунке 5.19, создает поименованный канал с именем "fifo". Если этот про- цесс запущен с указанием второго (формального) аргумента, он пос- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё char string[] = "hello"; Ё Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё int fd; Ё Ё char buf[256]; Ё Ё Ё Ё /* создание поименованного канала с разрешением чтения и Ё Ё записи для всех пользователей */ Ё Ё mknod("fifo",010777,0); Ё Ё if(argc == 2) Ё Ё fd = open("fifo",O_WRONLY); Ё Ё else Ё Ё fd = open("fifo",O_RDONLY); Ё Ё for (;;) Ё Ё if(argc == 2) Ё Ё write(fd,string,6); Ё Ё else Ё Ё read(fd,buf,6); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.19. Чтение и запись в поименованный канал тоянно записывает в канал строку символов "hello"; будучи запущен без второго аргумента, он ведет чтение из поименованного канала. Два процесса запускаются по одной и той же программе, тайно дого- ворившись взаимодействовать между собой через поименованный канал "fifo", но им нет необходимости быть родственными процессами. Другие пользователи могут выполнять программу и участвовать в ди- алоге (или мешать ему). 5.13 DUP Системная функция dup копирует дескриптор файла в первое сво- бодное место в таблице пользовательских дескрипторов файла, возв- ращая новый дескриптор пользователю. Она действует для всех типов файла. Синтаксис вызова функции: newfd = dup(fd); где fd - дескриптор файла, копируемый функцией, а newfd - новый дескриптор, ссылающийся на файл. Поскольку функция dup дублирует дескриптор файла, она увеличивает значение счетчика в соответс- твующей записи таблицы файлов - записи, на которую указывают свя- занные с ней точки входа в таблице файловых дескрипторов, которых теперь стало на одну больше. Например, обзор структур данных, изображенных на Рисунке 5.20, показывает, что процесс вызывает следующую последовательность функций: он открывает (open) файл с именем "/etc/passwd" (файловый дескриптор 3), затем открывает файл с именем "local" (файловый дескриптор 4), снова файл с име- нем "/etc/passwd" (файловый дескриптор 5) и, наконец, дублирует (dup) файловый дескриптор 3, возвращая дескриптор 6. таблица пользова- тельских дескрип- торов файла таблица файлов таблица индексов зддддддддд© здддддддддддд© здддддддддддддд© 0Ё ддддедддд© Ё Ё Ё Ы Ё цддддддддд╢ Ё Ё Ё Ё Ы Ё 1Ё ддддеддд©юдд>Ё Ё Ё Ы Ё цддддддддд╢ Ё цдддддддддддд╢ Ё Ы Ё 2Ё ддддедд©юддд>Ё Ы Ё Ё Ы Ё цддддддддд╢ юдддд>Ё Ы Ё Ё Ы Ё 3Ё ддддедддд© Ё Ы Ё Ё Ы Ё цддддддддд╢ Ё Ё Ы Ё цдддддддддддддд╢ 4Ё ддддеддд©Ё Ё Ы Ё здддд>Ё счет- Ё цддддддддд╢ ЁЁ Ё Ы Ё Ё Ё чик (/etc/ Ё 5Ё ддддедд©ЁЁ цдддддддддддд╢ Ё здд>Ё 2 passwd)Ё цддддддддд╢ ЁЁЁ Ё счет- Ё Ё Ё цдддддддддддддд╢ 6Ё дддде© ЁЁюдд>Ё чик едды Ё Ё Ы Ё цддддддддд╢юдЁЁддд>Ё 2 Ё Ё Ё Ы Ё 7Ё Ё ЁЁ цдддддддддддд╢ Ё Ё Ы Ё цддддддддд╢ ЁЁ Ё Ы Ё Ё Ё Ы Ё Ё Ы Ё ЁЁ Ё Ы Ё Ё Ё Ы Ё Ё Ы Ё ЁЁ Ё Ы Ё Ё Ё Ы Ё Ё Ы Ё ЁЁ цдддддддддддд╢ Ё Ё Ы Ё юддддддддды ЁЁ Ё счет- Ё Ё Ё Ы Ё Ёюддд>Ё чик еддддЁ© Ё Ы Ё Ё Ё 1 Ё ЁЁ Ё Ы Ё Ё цдддддддддддд╢ ЁЁ Ё Ы Ё Ё Ё Ы Ё ЁЁ цдддддддддддддд╢ Ё Ё Ы Ё ЁЁ Ё счет- Ё Ё Ё Ы Ё Ёюд>Ё чик (local)Ё Ё Ё Ы Ё Ё Ё 1 Ё Ё Ё Ы Ё Ё цдддддддддддддд╢ Ё Ё Ы Ё Ё Ё Ы Ё Ё цдддддддддддд╢ Ё Ё Ы Ё Ё Ё счет- Ё Ё Ё Ы Ё юдддд>Ё чик едддды Ё Ы Ё Ё 1 Ё Ё Ы Ё цдддддддддддд╢ Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юдддддддддддддды Ё Ы Ё юдддддддддддды Рисунок 5.20. Структуры данных после выполнения функции dup Возможно, dup - функция, не отличающаяся изяществом, посколь- ку она предполагает, что пользователь знает о том, что система возвратит свободную точку входа в таблице пользовательских деск- рипторов, имеющую наименьший номер. Однако, она служит важной за- даче конструирования сложных программ из более простых конструк- ционных блоков, что, в частности, имеет место при создании конвейеров, составленных из командных процессоров. Рассмотрим программу, приведенную на Рисунке 5.21. В перемен- ной i хранится дескриптор файла, возвращаемый в результате откры- тия файла "/etc/passwd", а в переменной j - дескриптор файла, возвращаемый системой в результате дублирования дескриптора i с помощью функции dup. В адресном пространстве процесса оба пользо- вательских дескриптора, представленные переменными i и j, ссыла- ются на одну и ту же запись в таблице файлов и поэтому используют одно и то же значение смещения внутри файла. Таким образом, пер- вые два вызова процессом функции read реализуют последовательное считывание данных, и в буферах buf1 и buf2 будут располагаться разные данные. Совсем другой результат получается, когда процесс здддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main() Ё Ё { Ё Ё int i,j; Ё Ё char buf1[512],buf2[512]; Ё Ё Ё Ё i = open("/etc/passwd",O_RDONLY); Ё Ё j = dup(i); Ё Ё read(i,buf1,sizeof(buf1)); Ё Ё read(j,buf2,sizeof(buf2)); Ё Ё close(i); Ё Ё read(j,buf2,sizeof(buf2)); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.21. Программа на языке Си, иллюстрирующая использо- вание функции dup открывает один и тот же файл дважды и читает дважды одни и те же данные (раздел 5.2). Процесс может освободить с помощью функции close любой из файловых дескрипторов по своему желанию, и ввод-вывод получит нормальное продолжение по другому дескриптору, как показано на примере. В частности, процесс может "закрыть" дескриптор файла стандартного вывода (файловый дескриптор 1), снять с него копию, имеющую то же значение, и затем рассматривать новый файл в качестве файла стандартного вывода. В главе 7 будет представлен более реалистический пример использования функций pipe и dup при описании особенностей реализации командного про- цессора. 5.14 МОНТИРОВАНИЕ И ДЕМОНТИРОВАНИЕ ФАЙЛОВЫХ СИСТЕМ Физический диск состоит из нескольких логических разделов, на которые он разбит дисковым драйвером, причем каждому разделу со- ответствует файл устройства, имеющий определенное имя. Процессы обращаются к данным раздела, открывая соответствующий файл уст- ройства и затем ведя запись и чтение из этого "файла", представ- ляя его себе в виде последовательности дисковых блоков. Это взаи- модействие во всех деталях рассматривается в главе 10. Раздел диска может содержать логическую файловую систему, состоящую из блока начальной загрузки, суперблока, списка индексов и информа- ционных блоков (см. главу 2). Системная функция mount (монтиро- вать) связывает файловую систему из указанного раздела на диске с существующей иерархией файловых систем, а функция umount (демон- тировать) выключает файловую систему из иерархии. Функция mount, таким образом, дает пользователям возможность обращаться к данным в дисковом разделе как к файловой системе, а не как к последова- тельности дисковых блоков. Синтаксис вызова функции mount: mount(special pathname,directory pathname,options); где special pathname - имя специального файла устройства, соот- ветствующего дисковому разделу с монтируемой файловой системой, directory pathname - каталог в существующей иерархии, где будет монтироваться файловая система (другими словами, точка или место монтирования), а options указывает, следует ли монтировать файло- вую систему "только для чтения" (при этом не будут выполняться з д д д д д д д д д д д д д д д д д д д д д д д д © / Ё Ё Ё зддддддддддддддддбддаддддддддддддд© Корневая Ё Ё Ё Ё Ё файловая bin etc usr система Ё Ё Ё Ы Ё здддддеддддд© зддддадддд© Ы Ё Ё Ё Ё Ё Ё Ы Ё cc date sh getty passwd Ы ю д д д д д д д д д д д д д д д д д д д д д д д д ы Ы з д д д д д д д д д д д д д д д д д д д д д д д д © / Ё Ё Ё Файловая зддддддддддддддддбддаддддддддддддд© система из Ё Ё Ё Ё Ё раздела с bin include src именем Ё Ё Ё Ё Ё /dev/dsk1 здддддеддддд© Ё Ё Ё Ё Ё Ё Ё Ё Ё awk banner yacc stdio.h uts ю д д д д д д д д д д д д д д д д д д д д д д д д ы Рисунок 5.22. Дерево файловых систем до и после выполнения функции mount такие функции, как write и creat, которые производят запись в файловую систему). Например, если процесс вызывает функцию mount следующим образом: mount("/dev/dsk1","/usr",0); ядро присоединяет файловую систему, находящуюся в дисковом разде- ле с именем "/dev/dsk1", к каталогу "/usr" в существующем дереве файловых систем (см. Рисунок 5.22). Файл "/dev/dsk1" является блочным специальным файлом, т.е. он носит имя устройства блочного типа, обычно имя раздела на диске. Ядро предполагает, что раздел на диске с указанным именем содержит файловую систему с супербло- ком, списком индексов и корневым индексом. После выполнения функ- ции mount к корню смонтированной файловой системы можно обращать- ся по имени "/usr". Процессы могут обращаться к файлам в монтированной файловой системе и игнорировать тот факт, что сис- тема может отсоединяться. Только системная функция link контроли- рует файловую систему, так как в версии V не разрешаются связи между файлами, принадлежащими разным файловым системам (см. раз- дел 5.15). Ядро поддерживает таблицу монтирования с записями о каждой монтированной файловой системе. В каждой записи таблицы монтиро- вания содержатся: * номер устройства, идентифицирующий монтированную файловую систему (упомянутый выше логический номер файловой системы); * указатель на буфер, где находится суперблок файловой системы; * указатель на корневой индекс монтированной файловой системы ("/" для файловой системы с именем "/dev/dsk1" на Рисунке 5.22); * указатель на индекс каталога, ставшего точкой монтирования (на Рисунке 5.22 это каталог "usr", принадлежащий корневой файловой системе). Связь индекса точки монтирования с корневым индексом монтиро- ванной файловой системы, возникшая в результате выполнения сис- темной функции mount, дает ядру возможность легко двигаться по иерархии файловых систем без получения от пользователей дополни- тельных сведений. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм mount Ё Ё входная информация: имя блочного специального файла Ё Ё имя каталога точки монтирования Ё Ё опции ("только для чтения") Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё если (пользователь не является суперпользователем) Ё Ё возвратить (ошибку); Ё Ё получить индекс для блочного специального файла (алго- Ё Ё ритм namei); Ё Ё проверить допустимость значений параметров; Ё Ё получить индекс для имени каталога, где производится Ё Ё монтирование (алгоритм namei); Ё Ё если (индекс не является индексом каталога или счетчик Ё Ё ссылок имеет значение > 1) Ё Ё { Ё Ё освободить индексы (алгоритм iput); Ё Ё возвратить (ошибку); Ё Ё } Ё Ё найти свободное место в таблице монтирования; Ё Ё запустить процедуру открытия блочного устройства для Ё Ё данного драйвера; Ё Ё получить свободный буфер из буферного кеша; Ё Ё считать суперблок в свободный буфер; Ё Ё проинициализировать поля суперблока; Ё Ё получить корневой индекс монтируемой системы (алгоритм Ё Ё iget), сохранить его в таблице монтирования; Ё Ё сделать пометку в индексе каталога о том, что каталог Ё Ё является точкой монтирования; Ё Ё освободить индекс специального файла (алгоритм iput); Ё Ё снять блокировку с индекса каталога точки монтирования;Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.23. Алгоритм монтирования файловой системы На Рисунке 5.23 показан алгоритм монтирования файловой систе- мы. Ядро позволяет монтировать и демонтировать файловые системы только тем процессам, владельцем которых является суперпользова- тель. Предоставление возможности выполнять функции mount и umount всем пользователям привело бы к внесению с их стороны хаоса в ра- боту файловой системы, как умышленному, так и явившемуся резуль- татом неосторожности. Суперпользователи могут разрушить систему только случайно. Ядро находит индекс специального файла, представляющего фай- ловую систему, подлежащую монтированию, извлекает старший и млад- ший номера, которые идентифицируют соответствующий дисковый раз- дел, и выбирает индекс каталога, в котором файловая система будет смонтирована. Счетчик ссылок в индексе каталога должен иметь зна- чение, не превышающее 1 (и меньше 1 он не должен быть - почему ?), в связи с наличием потенциально опасных побочных эффектов (см. упражнение 5.27). Затем ядро назначает свободное место в таблице монтирования, помечает его для использования и присваива- ет значение полю номера устройства в таблице. Вышеуказанные наз- начения производятся немедленно, поскольку вызывающий процесс мо- жет приостановиться, следуя процедуре открытия устройства или считывая суперблок файловой системы, а другой процесс тем време- нем попытался бы смонтировать файловую систему. Пометив для ис- пользования запись в таблице монтирования, ядро не допускает ис- пользования в двух вызовах функции mount одной и той же записи таблицы. Запоминая номер устройства с монтируемой системой, ядро может воспрепятствовать повторному монтированию одной и той же системы другими процессами, которое, будь оно допущено, могло бы привести к непредсказуемым последствиям (см. упражнение 5.26). Ядро вызывает процедуру открытия для блочного устройства, со- держащего файловую систему, точно так же, как оно делает это при непосредственном открытии блочного устройства (глава 10). Проце- дура открытия устройства обычно проверяет существование такого устройства, иногда производя инициализацию структур данных драй- вера и посылая команды инициализации аппаратуре. Затем ядро выде- ляет из буферного пула свободный буфер (вариант алгоритма getblk) для хранения суперблока монтируемой файловой системы и считывает суперблок, используя один из вариантов алгоритма read. Ядро сох- раняет указатель на индекс каталога, в котором монтируется систе- ма, давая возможность маршрутам поиска файловых имен, содержащих имя "..", пересекать точку монтирования, как мы увидим дальше. Оно находит корневой индекс монтируемой файловой системы и запо- минает указатель на индекс в таблице монтирования. С точки зрения пользователя, место (точка) монтирования и корень файловой систе- мы логически эквивалентны, и ядро упрочивает эту эквивалентность благодаря их сосуществованию в одной записи таблицы монтирования. Процессы больше не могут обращаться к индексу каталога - точки монтирования. Ядро инициализирует поля в суперблоке файловой системы, очи- щая поля для списка свободных блоков и списка свободных индексов и устанавливая число свободных индексов в суперблоке равным 0. Целью инициализации (задания начальных значений полей) является сведение к минимуму опасности разрушить файловую систему, если монтирование осуществляется после аварийного завершения работы системы. Если ядро заставить думать, что в суперблоке отсутствуют свободные индексы, то это приведет к запуску алгоритма ialloc, ведущего поиск на диске свободных индексов. К сожалению, если список свободных дисковых блоков испорчен, ядро не исправляет этот список изнутри (см. раздел 5.17 о сопровождении файловой системы). Если пользователь монтирует файловую систему только для чтения, запрещая проведение всех операций записи в системе, ядро устанавливает в суперблоке соответствующий флаг. Наконец, ядро помечает индекс каталога как "точку монтирования", чтобы другие процессы позднее могли ссылаться на нее. На Рисунке 5.24 предс- тавлен вид различных структур данных по завершении выполнения функции mount. 5.14.1 Пересечение точек монтирования в маршрутах поиска имен файлов Давайте повторно рассмотрим поведение алгоритмов namei и iget в случаях, когда маршрут поиска файлов проходит через точку мон- тирования. Точку монтирования можно пересечь двумя способами: из файловой системы, где производится монтирование, в файловую сис- тему, которая монтируется (в направлении от глобального корня к листу), и в обратном направлении. Эти способы иллюстрирует следу- ющая последовательность команд shell'а. Таблица индексов Таблица монтирования здддддддддддддддддд© здддддддддддддддддддд© Ё Ё Ё Ё цдддддддддддддддддд╢ Ё Ё Ё Индекс каталога, е д д © Ё Ё Ё где производится Ё Ё Ё Ё монтирование Ё Ё Ё Ё зддддддд© Ё Помечен как "точ-Ё<ддд© Ё Ёзд>Ё Буфер Ё Ё ка монтирования" Ё ЁЁ Ё ЁЁ юддддддды Ё Счетчик ссылок =1Ё Ё Ё ЁЁ цдддддддддддддддддд╢ Ёю >цдддддддддддддддддддд╢Ё Ё Ё Ё Ё Суперблок дддеы цдддддддддддддддддд╢ юддде Индекс точки монти-Ё Ё Индекс устройстваЁ Ё рования Ё Ё Не используется Ё здддед Корневой индекс Ё Ё Счетчик ссылок =0Ё Ё цдддддддддддддддддддд╢ цдддддддддддддддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё цдддддддддддддддддд╢<ддды Ё Ё Ё Индекс корня мон-Ё Ё Ё Ё тируемой файловойЁ Ё Ё Ё системы Ё Ё Ё Ё Счетчик ссылок =1Ё юдддддддддддддддддддды цдддддддддддддддддд╢ Ё Ё юдддддддддддддддддды Рисунок 5.24. Структуры данных после монтирования mount /dev/dsk1 /usr cd /usr/src/uts cd ../../.. По команде mount после выполнения некоторых логических прове- рок запускается системная функция mount, которая монтирует файло- вую систему в дисковом разделе с именем "/dev/dsk1" под управле- нием каталога "/usr". Первая из команд cd (сменить каталог) побуждает командный процессор shell вызвать системную функцию chdir, выполняя которую, ядро анализирует имя пути поиска, пере- секающего точку монтирования в "/usr". Вторая из команд cd приво- дит к тому, что ядро анализирует имя пути поиска и пересекает точку монтирования в третьей компоненте ".." имени. Для случая пересечения точки монтирования в направлении из файловой системы, где производится монтирование, в файловую сис- тему, которая монтируется, рассмотрим модификацию алгоритма iget (Рисунок 5.25), которая идентична версии алгоритма, приведенной на Рисунке 4.3, почти во всем, за исключением того, что в данной модификации производится проверка, является ли индекс индексом точки монтирования. Если индекс имеет соответствующую пометку, ядро соглашается, что это индекс точки монтирования. Оно обнару- живает в таблице монтирования запись с указанным индексом точки монтирования и запоминает номер устройства монтируемой файловой системы. Затем, используя номер устройства и номер индекса корня, общего для всех файловых систем, ядро обращается к индексу корня здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм iget Ё Ё входная информация: номер индекса в файловой системе Ё Ё выходная информация: заблокированный индекс Ё Ё { Ё Ё выполнить Ё Ё { Ё Ё если (индекс в индексном кеше) Ё Ё { Ё Ё если (индекс заблокирован) Ё Ё { Ё Ё приостановиться (до освобождения индекса); Ё Ё продолжить; /* цикл с условием продолжения */ Ё Ё } Ё Ё /* специальная обработка для точек монтирования */ Ё Ё если (индекс является индексом точки монтирования) Ё Ё { Ё Ё найти запись в таблице монтирования для точки мон- Ё Ё тирования; Ё Ё получить новый номер файловой системы из таблицы Ё Ё монтирования; Ё Ё использовать номер индекса корня для просмотра; Ё Ё продолжить; /* продолжение цикла */ Ё Ё } Ё Ё если (индекс в списке свободных индексов) Ё Ё убрать из списка свободных индексов; Ё Ё увеличить счетчик ссылок для индекса; Ё Ё возвратить (индекс); Ё Ё } Ё Ё Ё Ё /* индекс отсутствует в индексном кеше */ Ё Ё убрать новый индекс из списка свободных индексов; Ё Ё сбросить номер индекса и файловой системы; Ё Ё убрать индекс из старой хеш-очереди, поместить в новую;Ё Ё считать индекс с диска (алгоритм bread); Ё Ё инициализировать индекс (например, установив счетчик Ё Ё ссылок в 1); Ё Ё возвратить (индекс); Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.25. Модификация алгоритма получения доступа к ин- дексу монтируемого устройства и возвращает при выходе из функции этот индекс. В первом примере смены каталога ядро обращается к индексу каталога "/usr" из файловой системы, в которой производится мон- тирование, обнаруживает, что этот индекс имеет пометку "точка монтирования", находит в таблице монтирования индекс корня монти- руемой файловой системы и обращается к этому индексу. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм namei /* превращение имени пути поиска в индекс */Ё Ё входная информация: имя пути поиска Ё Ё выходная информация: заблокированный индекс Ё Ё { Ё Ё если (путь поиска берет начало с корня) Ё Ё рабочий индекс = индексу корня (алгоритм iget); Ё Ё в противном случае Ё Ё рабочий индекс = индексу текущего каталога Ё Ё (алгоритм iget); Ё Ё Ё Ё выполнить (пока путь поиска не кончился) Ё Ё { Ё Ё считать следующую компоненту имени пути поиска; Ё Ё проверить соответствие рабочего индекса каталогу Ё Ё и права доступа; Ё Ё если (рабочий индекс соответствует корню и компо- Ё Ё нента имени "..") Ё Ё продолжить; /* цикл с условием продолжения */Ё Ё поиск компоненты: Ё Ё считать каталог (рабочий индекс), повторяя алго- Ё Ё ритмы bmap, bread и brelse; Ё Ё если (компонента соответствует записи в каталоге Ё Ё (рабочем индексе)) Ё Ё { Ё Ё получить номер индекса для совпавшей компонен-Ё Ё ты; Ё Ё если (найденный индекс является индексом кор- Ё Ё ня и рабочий индекс является индексом корня Ё Ё и имя компоненты "..") Ё Ё { Ё Ё /* пересечение точки монтирования */ Ё Ё получить запись в таблице монтирования для Ё Ё рабочего индекса; Ё Ё освободить рабочий индекс (алгоритм iput); Ё Ё рабочий индекс = индексу точки монтирования;Ё Ё заблокировать индекс точки монтирования; Ё Ё увеличить значение счетчика ссылок на рабо- Ё Ё чий индекс; Ё Ё перейти к поиску компоненты (для ".."); Ё Ё } Ё Ё освободить рабочий индекс (алгоритм iput); Ё Ё рабочий индекс = индексу с новым номером Ё Ё (алгоритм iget); Ё Ё } Ё Ё в противном случае /* компонента отсутствует в Ё Ё каталоге */ Ё Ё возвратить (нет индекса); Ё Ё } Ё Ё возвратить (рабочий индекс); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.26. Модификация алгоритма синтаксического анализа имени файла Для второго случая пересечения точки монтирования в направле- нии из файловой системы, которая монтируется, в файловую систему, где выполняется монтирование, рассмотрим модификацию алгоритма namei (Рисунок 5.26). Она похожа на версию алгоритма, приведенную на Рисунке 4.11. Однако, после обнаружения в каталоге номера ин- декса для данной компоненты пути поиска ядро проверяет, не указы- вает ли номер индекса на то, что это корневой индекс файловой системы. Если это так и если текущий рабочий индекс так же явля- ется корневым, а компонента пути поиска, в свою очередь, имеет имя "..", ядро идентифицирует индекс как точку монтирования. Оно находит в таблице монтирования запись, номер устройства в которой совпадает с номером устройства для последнего из найденных индек- сов, получает индекс для каталога, в котором производится монти- рование, и продолжает поиск компоненты с именем "..", используя только что полученный индекс в качестве рабочего. В корне файло- вой системы, тем не менее, корневым каталогом является "..". В вышеприведенном примере (cd "../../..") предполагается, что в начале процесс имеет текущий каталог с именем "/usr/src/uts". Когда имя пути поиска подвергается анализу в алгоритме namei, на- чальным рабочим индексом является индекс текущего каталога. Ядро меняет текущий рабочий индекс на индекс каталога с именем "/usr/src" в результате расшифровки первой компоненты ".." в име- ни пути поиска. Затем ядро анализирует вторую компоненту ".." в имени пути поиска, находит корневой индекс смонтированной (перед этим) файловой системы - индекс каталога "usr" - и делает его ра- бочим индексом при анализе имени с помощью алгоритма namei. Нако- нец, оно расшифровывает третью компоненту ".." в имени пути поис- ка. Ядро обнаруживает, что номер индекса для ".." совпадает с но- мером корневого индекса, рабочим индексом является корневой ин- декс, а ".." является текущей компонентой имени пути поиска. Ядро находит запись в таблице монтирования, соответствующую точке мон- тирования "usr", освобождает текущий рабочий индекс (корень фай- ловой системы, смонтированной в каталоге "usr") и назначает ин- декс точки монтирования (каталога "usr" в корневой файловой системе) в качестве нового рабочего индекса. Затем оно просматри- вает записи в каталоге точки монтирования "/usr" в поисках имени ".." и находит номер индекса для корня файловой системы ("/"). После этого системная функция chdir завершается как обычно, вызы- вающий процесс не обращает внимания на тот факт, что он пересек точку монтирования. 5.14.2 Демонтирование файловой системы Синтаксис вызова системной функции umount: umount(special filename); где special filename указывает демонтируемую файловую систему. При демонтировании файловой системы (Рисунок 5.27) ядро обращает- ся к индексу демонтируемого устройства, восстанавливает номер ус- тройства для специального файла, освобождает индекс (алгоритм iput) и находит в таблице монтирования запись с номером устройс- тва, равным номеру устройства для специального файла. Прежде чем ядро действительно демонтирует файловую систему, оно должно удос- товериться в том, что в системе не осталось используемых файлов, для этого ядро просматривает таблицу индексов в поисках всех фай- лов, чей номер устройства совпадает с номером демонтируемой сис- темы. Активным файлам соответствует положительное значение счет- чика ссылок и в их число входят текущий каталог процесса, файлы с разделяемым текстом, которые исполняются в текущий момент (глава 7), и открытые когда-то файлы, которые потом не были закрыты. Ес- ли какие-нибудь файлы из файловой системы активны, функция umount завершается неудачно: если бы она прошла успешно, активные файлы сделались бы недоступными. Буферный пул все еще содержит блоки с "отложенной записью", не переписанные на диск, поэтому ядро "вымывает" их из буферного пула. Ядро удаляет записи с разделяемым текстом, которые находят- ся в таблице областей, но не являются действующими (подробности в главе 7), записывает на диск все недавно скорректированные су- перблоки и корректирует дисковые копии всех индексов, которые требуют этого. Казалось, было бы достаточно откорректировать дис- ковые блоки, суперблок и индексы только для демонтируемой файло- вой системы, однако в целях сохранения преемственности изменений здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм umount Ё Ё входная информация: имя специального файла, соответствую- Ё Ё щего демонтируемой файловой системе Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё если (пользователь не является суперпользователем) Ё Ё возвратить (ошибку); Ё Ё получить индекс специального файла (алгоритм namei); Ё Ё извлечь старший и младший номера демонтируемого устрой-Ё Ё ства; Ё Ё получить в таблице монтирования запись для демонтируе- Ё Ё мой системы, исходя из старшего и младшего номеров; Ё Ё освободить индекс специального файла (алгоритм iput); Ё Ё удалить из таблицы областей записи с разделяемым текс- Ё Ё том для файлов, принадлежащих файловой Ё Ё системе; /* глава 7ххх */ Ё Ё скорректировать суперблок, индексы, выгрузить буферы Ё Ё на диск; Ё Ё если (какие-то файлы из файловой системы все еще ис- Ё Ё пользуются) Ё Ё возвратить (ошибку); Ё Ё получить из таблицы монтирования корневой индекс монти-Ё Ё рованной файловой системы; Ё Ё заблокировать индекс; Ё Ё освободить индекс (алгоритм iput); /* iget был при Ё Ё монтировании */ Ё Ё запустить процедуру закрытия для специального устрой- Ё Ё ства; Ё Ё сделать недействительными (отменить) в пуле буферы из Ё Ё демонтируемой файловой системы; Ё Ё получить из таблицы монтирования индекс точки монтиро- Ё Ё вания; Ё Ё заблокировать индекс; Ё Ё очистить флаг, помечающий индекс как "точку монтирова- Ё Ё ния"; Ё Ё освободить индекс (алгоритм iput); /* iget был при Ё Ё монтировании */ Ё Ё освободить буфер, используемый под суперблок; Ё Ё освободить в таблице монтирования место, занятое ранее;Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.27. Алгоритм демонтирования файловой системы ядро выполняет аналогичные действия для всей системы в целом. За- тем ядро освобождает корневой индекс монтированной файловой сис- темы, удерживаемый с момента первого обращения к нему во время выполнения функции mount, и запускает из драйвера процедуру зак- рытия устройства, содержащего файловую систему. Впоследствии ядро просматривает буферы в буферном кеше и делает недействительными те из них, в которых находятся блоки демонтируемой файловой сис- темы; в хранении информации из этих блоков в кеше больше нет не- обходимости. Делая буферы недействительными, ядро вставляет их в начало списка свободных буферов, в то время как блоки с актуаль- ной информацией остаются в буферном кеше. Ядро сбрасывает в ин- дексе системы, где производилось монтирование, флаг "точки монти- рования", установленный функцией mount, и освобождает индекс. Пометив запись в таблице монтирования свободной для общего ис- пользования, функция umount завершает работу. / Ё Ё usr Ё зддддддддддддаддддддддддддд© Ё Ё src include Ё Ё Ё зддддадддд© uts sys realfile.h Ё Ы Ы Ё Ы Ы sys ЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ Ы Ё Ы здддддддаддддддд© Ы inode.h testfile.h ЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ Рисунок 5.28. Файлы в дереве файловой системы, связанные с помощью функции link 5.15 LINK Системная функция link связывает файл с новым именем в струк- туре каталогов файловой системы, создавая для существующего ин- декса новую запись в каталоге. Синтаксис вызова функции link: link(source file name, target file name); где source file name - существующее имя файла, а target file name - новое (дополнительное) имя, присваиваемое файлу после вы- полнения функции link. Файловая система хранит имя пути поиска для каждой связи, имеющейся у файла, и процессы могут обращаться к файлу по любому из этих имен. Ядро не знает, какое из имен фай- ла является его подлинным именем, поэтому имя файла специально не обрабатывается. Например, после выполнения набора функций: link("/usr/src/uts/sys","/usr/include/sys"); link("/usr/include/realfile.h","/usr/src/uts/sys/testfile.h"); на один и тот же файл будут указывать три имени пути поиска: "/usr/src/uts/sys/testfile.h", "/usr/include/sys/testfile.h" и "/usr/include/realfile" (см. Рисунок 5.28). Ядро позволяет суперпользователю (и только ему) связывать ка- талоги, упрощая написание программ, требующих пересечения дерева файловой системы. Если бы это было разрешено произвольному поль- зователю, программам, пересекающим иерархическую структуру фай- лов, пришлось бы заботиться о том, чтобы не попасть в бесконечный цикл в том случае, если пользователь связал каталог с вершиной, стоящей ниже в иерархии. Предполагается, что суперпользователи более осторожны в указании таких связей. Возможность связывать между собой каталоги должна была поддерживаться в ранних версиях системы, так как эта возможность требуется для реализации команды mkdir, которая создает новый каталог. Включение функции mkdir ус- траняет необходимость в связывании каталогов. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм link Ё Ё входная информация: существующее имя файла Ё Ё новое имя файла Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё получить индекс для существующего имени файла (алгоритм Ё Ё namei); Ё Ё если (у файла слишком много связей или производится Ё Ё связывание каталога без разрешения суперпользователя) Ё Ё { Ё Ё освободить индекс (алгоритм iput); Ё Ё возвратить (ошибку); Ё Ё } Ё Ё увеличить значение счетчика связей в индексе; Ё Ё откорректировать дисковую копию индекса; Ё Ё снять блокировку с индекса; Ё Ё получить индекс родительского каталога для включения но-Ё Ё вого имени файла (алгоритм namei); Ё Ё если (файл с новым именем уже существует или существую- Ё Ё щий файл и новый файл находятся в разных файловых сис- Ё Ё темах) Ё Ё { Ё Ё отменить корректировку, сделанную выше; Ё Ё возвратить (ошибку); Ё Ё } Ё Ё создать запись в родительском каталоге для файла с но- Ё Ё вым именем: Ё Ё включить в нее новое имя и номер индекса существую- Ё Ё щего файла; Ё Ё освободить индекс родительского каталога (алгоритм Ё Ё iput); Ё Ё освободить индекс существующего файла (алгоритм iput); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.29. Алгоритм связывания файлов На Рисунке 5.29 показан алгоритм функции link. Сначала ядро, используя алгоритм namei, определяет местонахождение индекса ис- ходного файла, увеличивает значение счетчика связей в индексе, корректирует дисковую копию индекса (для обеспечения согласован- ности) и снимает с индекса блокировку. Затем ядро ищет файл с но- вым именем; если он существует, функция link завершается неудачно и ядро восстанавливает прежнее значение счетчика связей, изменен- ное ранее. В противном случае ядро находит в родительском катало- ге свободную запись для файла с новым именем, записывает в нее новое имя и номер индекса исходного файла и освобождает индекс родительского каталога, используя алгоритм iput. Поскольку файл с новым именем ранее не существовал, освобождать еще какой-нибудь индекс не нужно. Ядро, освобождая индекс исходного файла, делает заключение: счетчик связей в индексе имеет значение, на 1 боль- шее, чем то значение, которое счетчик имел перед вызовом функции, и обращение к файлу теперь может производиться по еще одному име- ни в файловой системе. Счетчик связей хранит количество записей в каталогах, которые (записи) указывают на файл, и тем самым от- личается от счетчика ссылок в индексе. Если по завершении выпол- нения функции link к файлу нет обращений со стороны других про- цессов, счетчик ссылок в индексе принимает значение, равное 0, а счетчик связей - значение, большее или равное 2. Например, выполняя функцию, вызванную как: link("source","/dir/target"); ядро обнаруживает индекс для файла "source", увеличивает в нем значение счетчика связей, запоминает номер индекса, скажем 74, и снимает с индекса блокировку. Ядро также находит индекс каталога "dir", являющегося родительским каталогом для файла "target", ищет свободное место в каталоге "dir" и записывает в него имя файла "target" и номер индекса 74. По окончании этих действий оно освобождает индекс файла "source" по алгоритму iput. Если значе- ние счетчика связей файла "source" раньше было равно 1, то теперь оно равно 2. Стоит упомянуть о двух тупиковых ситуациях, явившихся причи- ной того, что процесс снимает с индекса исходного файла блокиров- ку после увеличения значения счетчика связей. Если бы ядро не снимало с индекса блокировку, два процесса, выполняющие одновре- менно следующие функции: процесс A: link("a/b/c/d","e/f/g"); процесс B: link("e/f","a/b/c/d/ee"); зашли бы в тупик (взаимная блокировка). Предположим, что процесс A обнаружил индекс файла "a/b/c/d" в тот самый момент, когда про- цесс B обнаружил индекс файла "e/f". Фраза "в тот же самый мо- мент" означает, что системой достигнуто состояние, при котором каждый процесс получил искомый индекс. (Рисунок 5.30 иллюстрирует стадии выполнения процессов.) Когда же теперь процесс A попытает- ся получить индекс файла "e/f", он приостановит свое выполнение до тех пор, пока индекс файла "f" не освободится. В то же время процесс B пытается получить индекс каталога "a/b/c/d" и приоста- навливается в ожидании освобождения индекса файла "d". Процесс A будет удерживать заблокированным индекс, нужный процессу B, а процесс B, в свою очередь, будет удерживать заблокированным ин- декс, нужный процессу A. На практике этот классический пример взаимной блокировки невозможен благодаря тому, что ядро освобож- дает индекс исходного файла после увеличения значения счетчика связей. Поскольку первый из ресурсов (индекс) свободен при обра- щении к следующему ресурсу, взаимная блокировка не происходит. Следующий пример показывает, как два процесса могут зайти в тупик, если с индекса не была снята блокировка. Одиночный процесс может также заблокировать самого себя. Если он вызывает функцию: link("a/b/c","a/b/c/d"); то в начале алгоритма он получает индекс для файла "c"; если бы ядро не снимало бы с индекса блокировку, процесс зашел бы в ту- пик, запросив индекс "c" при поиске файла "d". Если бы два про- цесса, или даже один процесс, не могли продолжать свое выполнение из-за взаимной блокировки (или самоблокировки), что в результате произошло бы в системе ? Поскольку индексы являются теми ресурса- ми, которые предоставляются системой за конечное время, получение сигнала не может быть причиной возобновления процессом своей ра- боты (глава 7). Следовательно, система не может выйти из тупика без перезагрузки. Если к файлам, заблокированным процессами, нет обращений со стороны других процессов, взаимная блокировка не затрагивает остальные процессы в системе. Однако, любые процессы, Процесс A Процесс B зддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё Ы Пытается получить индекс Ё Ы для файла "e" Ё Ы ПРИОСТАНОВ - индекс файла Ё Ы "e" заблокирован Ё Ы Ы Ё Получает индекс для "a" Ы Ё Освобождает индекс "a" Ы Ё Получает индекс для "b" Ы Ё Освобождает индекс "b" Ы Ё Получает индекс для "c" Ы Ё Освобождает индекс "c" Ы Ё Получает индекс для "d" Ы Ё Ы Ё Пытается получить индекс Ы Ё для "e" Ы Ё ПРИОСТАНОВ - индекс файла Ы Ё "e" заблокирован Ы Ё Ы Ы Ё Ы Ы Ё зддддддддддддддддддддддддддддддддддддддддддддддд© Ё Ё Возобновление выполнения - индекс файла "e" Ё Ё Ё разблокирован Ё Ё юддддддддддддддддддддддддддддддддддддддддддддддды Ё Ы Получает индекс для "e" Ё Ы Освобождает индекс "e" Ё Ы Получает индекс для "f" Ё Ы Получает индекс для "a" Ё Ы Освобождает индекс "a" Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё Ы Пытается получить индекс Ё Ы для файла "d" Ё Ы ПРИОСТАНОВ - индекс файла Ё Ы "d" заблокирован Ё Ы процессом A Ё Ы Ё Ы Ё Получает индекс для "e" Ё Освобождает индекс "e" Ё Пытается получить индекс Ё для "f" Ё ПРИОСТАНОВ - индекс файла Ё "f" заблокирован Ё процессом B Ё зддддддддддддддддддддддддддддддд© Ё Ё Тупик (взаимная блокировка) Ё v юддддддддддддддддддддддддддддддды Время Рисунок 5.30. Взаимная блокировка процессов при выполнении функции link обратившиеся к этим файлам (или обратившиеся к другим файлам че- рез заблокированный каталог), непременно зайдут в тупик. Таким образом, если заблокированы файлы "/bin" или "/usr/bin" (обычные хранилища команд) или файл "/bin/sh" (командный процессор shell), последствия для системы будут гибельными. 5.16 UNLINK Системная функция unlink удаляет из каталога точку входа для файла. Синтаксис вызова функции unlink: unlink(pathname); где pathname указывает имя файла, удаляемое из иерархии катало- гов. Если процесс разрывает данную связь файла с каталогом при помощи функции unlink, по указанному в вызове функции имени файл не будет доступен, пока в каталоге не создана еще одна запись с этим именем. Например, при выполнении следующего фрагмента прог- раммы: unlink("myfile"); fd = open("myfile",O_RDONLY); функция open завершится неудачно, поскольку к моменту ее выполне- ния в текущем каталоге больше не будет файла с именем myfile. Ес- ли удаляемое имя является последней связью файла с каталогом, ядро в итоге освобождает все информационные блоки файла. Однако, если у файла было несколько связей, он остается все еще доступным под другими именами. На Рисунке 5.31 представлен алгоритм функции unlink. Сначала для поиска файла с удаляемой связью ядро использует модификацию алгоритма namei, которая вместо индекса файла возвращает индекс родительского каталога. Ядро обращается к индексу файла в памяти, используя алгоритм iget. (Особый случай, связанный с удалением имени файла ".", будет рассмотрен в упражнении). После проверки отсутствия ошибок и (для исполняемых файлов) удаления из таблицы областей записей с неактивным разделяемым текстом (глава 7) ядро стирает имя файла из родительского каталога: сделать значение но- мера индекса равным 0 достаточно для очистки места, занимаемого именем файла в каталоге. Затем ядро производит синхронную запись каталога на диск, гарантируя тем самым, что под своим прежним именем файл уже не будет доступен, уменьшает значение счетчика связей и с помощью алгоритма iput освобождает в памяти индексы родительского каталога и файла с удаляемой связью. При освобождении в памяти по алгоритму iput индекса файла с удаляемой связью, если значения счетчика ссылок и счетчика связей становятся равными 0, ядро забирает у файла обратно дисковые бло- ки, которые он занимал. На этот индекс больше не указывает ни од- но из файловых имен и индекс неактивен. Для того, чтобы забрать дисковые блоки, ядро в цикле просматривает таблицу содержимого индекса, освобождая все блоки прямой адресации немедленно (в со- ответствии с алгоритмом free). Что касается блоков косвенной ад- ресации, ядро освобождает все блоки, появляющиеся на различных уровнях косвенности, рекурсивно, причем в первую очередь освобож- даются блоки с меньшим уровнем. Оно обнуляет номера блоков в таб- лице содержимого индекса и устанавливает размер файла в индексе равным 0. Затем ядро очищает в индексе поле типа файла, указывая тем самым, что индекс свободен, и освобождает индекс по алгоритму ifree. Ядро делает необходимую коррекцию на диске, так как диско- вая копия индекса все еще указывает на то, что индекс использует- ся; теперь индекс свободен для назначения другим файлам. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм unlink Ё Ё входная информация: имя файла Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё получить родительский индекс для файла с удаляемой Ё Ё связью (алгоритм namei); Ё Ё /* если в качестве файла выступает текущий каталог... */Ё Ё если (последней компонентой имени файла является ".") Ё Ё увеличить значение счетчика ссылок в индексе; Ё Ё в противном случае Ё Ё получить индекс для файла с удаляемой связью (алго-Ё Ё ритм iget); Ё Ё если (файл является каталогом, но пользователь не явля- Ё Ё ется суперпользователем) Ё Ё { Ё Ё освободить индексы (алгоритм iput); Ё Ё возвратить (ошибку); Ё Ё } Ё Ё если (файл имеет разделяемый текст и текущее значение Ё Ё счетчика связей равно 1) Ё Ё удалить записи из таблицы областей; Ё Ё в родительском каталоге: обнулить номер индекса для уда-Ё Ё ляемой связи; Ё Ё освободить индекс родительского каталога (алгоритм Ё Ё iput); Ё Ё уменьшить число связей файла; Ё Ё освободить индекс файла (алгоритм iput); Ё Ё /* iput проверяет, равно ли число связей 0, если Ё Ё * да, Ё Ё * освобождает блоки файла (алгоритм free) и Ё Ё * освобождает индекс (алгоритм ifree); Ё Ё */ Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.31. Алгоритм удаления связи файла с каталогом 5.16.1 Целостность файловой системы Ядро посылает свои записи на диск для того, чтобы свести к минимуму опасность искажения файловой системы в случае системного сбоя. Например, когда ядро удаляет имя файла из родительского ка- талога, оно синхронно переписывает каталог на диск - перед тем, как уничтожить содержимое файла и освободить его индекс. Если система дала сбой до того, как произошло удаление содержимого файла, ущерб файловой системе будет нанесен минимальный: один из индексов будет иметь число связей, на 1 превышающее число записей в каталоге, которые ссылаются на этот индекс, но все остальные имена путей поиска файла останутся допустимыми. Если запись на диск не была сделана синхронно, точка входа в каталог на диске после системного сбоя может указывать на свободный (или переназ- наченный) индекс. Таким образом, число записей в каталоге на дис- ке, которые ссылаются на индекс, превысило бы значение счетчика ссылок в индексе. В частности, если имя файла было именем послед- ней связи файла, это имя указывало бы на неназначенный индекс. Не вызывает сомнения, что в первом случае ущерб, наносимый системе, менее серьезен и легко устраним (см. раздел 5.18). Предположим, например, что у файла есть две связи с именами "a" и "b", одна из которых - "a" - разрывается процессом с по- мощью функции unlink. Если ядро записывает на диске результаты всех своих действий, то оно, очищая точку входа в каталог для файла "a", делает то же самое на диске. Если система дала сбой после завершения записи результатов на диск, число связей у файла "b" будет равно 2, но файл "a" уже не будет существовать, пос- кольку прежняя запись о нем была очищена перед сбоем системы. Файл "b", таким образом, будет иметь лишнюю связь, но после пере- загрузки число связей переустановится и система будет работать надлежащим образом. Теперь предположим, что ядро записывало на диск результаты своих действий в обратном порядке и система дала сбой: то есть, ядро уменьшило значение счетчика связей для файла "b", сделав его равным 1, записало индекс на диск и дало сбой перед тем, как очистить в каталоге точку входа для файла "a". После перезагрузки системы записи о файлах "a" и "b" в соответствующих каталогах бу- дут существовать, но счетчик связей у того файла, на который они указывают, будет иметь значение 1. Если затем процесс запустит функцию unlink для файла "a", значение счетчика связей станет равным 0, несмотря на то, что файл "b" ссылается на тот же ин- декс. Если позднее ядро переназначит индекс в результате выполне- ния функции creat, счетчик связей для нового файла будет иметь значение, равное 1, но на файл будут ссылаться два имени пути по- иска. Система не может выправить ситуацию, не прибегая к помощи программ сопровождения (fsck, описанной в разделе 5.18), обращаю- щихся к файловой системе через блочный или строковый интерфейс. Для того, чтобы свести к минимуму опасность искажения файло- вой системы в случае системного сбоя, ядро освобождает индексы и дисковые блоки также в особом порядке. При удалении содержимого файла и очистке его индекса можно сначала освободить блоки, со- держащие данные файла, а можно освободить индекс и заново перепи- сать его. Результат в обоих случаях, как правило, одинаковый, од- нако, если где-то в середине произойдет системный сбой, они будут различаться. Предположим, что ядро сначала освободило дисковые блоки, принадлежавшие файлу, и дало сбой. После перезагрузки сис- темы индекс все еще содержит ссылки на дисковые блоки, занимаемые файлом прежде и ныне не хранящие относящуюся к файлу информацию. Ядру файл показался бы вполне удовлетворительным, но пользователь при обращении к файлу заметит искажение данных. Эти дисковые бло- ки к тому же могут быть переназначены другим файлам. Чтобы очис- тить файловую систему программой fsck, потребовались бы большие усилия. Однако, если система сначала переписала индекс на диск, а потом дала сбой, пользователь не заметит каких-либо искажений в файловой системе после перезагрузки. Информационные блоки, ранее принадлежавшие файлу, станут недоступны для системы, но каких-ни- будь явных изменений при этом пользователи не увидят. Программе fsck так же было бы проще забрать назад освободившиеся после уда- ления связи дисковые блоки, нежели производить очистку, необходи- мую в первом из рассматриваемых случаев. 5.16.2 Поводы для конкуренции Поводов для конкуренции при выполнении системной функции unlink очень много, особенно при удалении имен каталогов. Команда rmdir удаляет каталог, убедившись предварительно в том, что в ка- талоге отсутствуют файлы (она считывает каталог и проверяет зна- чения индексов во всех записях каталога на равенство нулю). Но так как команда rmdir запускается на пользовательском уровне, действия по проверке содержимого каталога и удаления каталога вы- полняются не так уж просто; система должна переключать контекст между выполнением функций read и unlink. Однако, после того, как команда rmdir обнаружила, что каталог пуст, другой процесс может предпринять попытку создать файл в каталоге функцией creat. Избе- жать этого пользователи могут только путем использования механиз- ма захвата файла и записи. Тем не менее, раз процесс приступил к выполнению функции unlink, никакой другой процесс не может обра- титься к файлу с удаляемой связью, поскольку индексы родительско- го каталога и файла заблокированы. Обратимся еще раз к алгоритму функции link и посмотрим, каким образом система снимает с индекса блокировку до завершения выпол- нения функции. Если бы другой процесс удалил связь файла пока его индекс свободен, он бы тем самым только уменьшил значение счетчи- ка связей; так как значение счетчика связей было увеличено перед удалением связи, это значение останется положительным. Следова- тельно, файл не может быть удален и система работает надежно. Эта ситуация аналогична той, когда функция unlink вызывается сразу после завершения выполнения функции link. Другой повод для конкуренции имеет место в том случае, когда один процесс преобразует имя пути поиска файла в индекс файла по алгоритму namei, а другой процесс удаляет каталог, имя которого входит в путь поиска. Допустим, процесс A делает разбор имени "a/ b/c/d" и приостанавливается во время получения индекса для файла "c". Он может приостановиться при попытке заблокировать индекс или при попытке обратиться к дисковому блоку, где этот индекс хранится (см. алгоритмы iget и bread). Если процессу B нужно уд- лить связь для каталога с именем "c", он может приостановиться по той же самой причине, что и процесс A. Пусть ядро впоследствии решит возобновить процесс B раньше процесса A. Прежде чем процесс A продолжит свое выполнение, процесс B завершится, удалив связь каталога "c" и его содержимое по этой связи. Позднее, процесс A попытается обратиться к несуществующему индексу, который уже был удален. Алгоритм namei, проверяющий в первую очередь неравенство значения счетчика связей нулю, сообщит об ошибке. Такой проверки, однако, не всегда достаточно, поскольку можно предположить, что какой-нибудь другой процесс создаст в любом месте файловой системы новый каталог и получит тот индекс, кото- рый ранее использовался для "c". Процесс A будет заблуждаться, думая, что он обратился к нужному индексу (см. Рисунок 5.32). Как бы то ни было, система сохраняет свою целостность; самое худшее, что может произойти, это обращение не к тому файлу - с возможным нарушением защиты - но соперничества такого рода на практике до- вольно редки. Процесс может удалить связь файла в то время, как другому процессу нужно, чтобы файл оставался открытым. (Даже процесс, удаляющий связь, может быть процессом, выполнившим это открытие). Поскольку ядро снимает с индекса блокировку по окончании выполне- ния функции open, функция unlink завершится успешно. Ядро будет выполнять алгоритм unlink точно так же, как если бы файл не был открыт, и удалит из каталога запись о файле. Теперь по имени уда- ленной связи к файлу не сможет обратиться никакой другой процесс. Однако, так как системная функция open увеличила значение счетчи- ка ссылок в индексе, ядро не очищает содержимое файла при выпол- нении алгоритма iput перед завершением функции unlink. Поэтому процесс, открывший файл, может производить над файлом все обычные действия по его дескриптору, включая чтение из файла и запись в файл. Но когда процесс закрывает файл, значение счетчика ссылок в алгоритме iput становится равным 0, и ядро очищает содержимое файла. Короче говоря, процесс, открывший файл, продолжает работу так, как если бы функция unlink не выполнялась, а unlink, в свою очередь, работает так, как если бы файл не был открыт. Другие системные функции также могут продолжать выполняться в процессе, открывшем файл. В приведенном на Рисунке 5.33 примере процесс открывает файл, указанный в качестве параметра, и затем удаляет связь только что открытого файла. Функция stat завершится неудачно, поскольку пер- воначальное имя после unlink больше не указывает на файл (предпо- Процесс A Процесс B Процесс C здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё Ы Удаляется связь фай- Ы Ё Ы ла с именем "с" Ы Ё Ы Ы Ё Ы Обнаруживает, что Ы Ё Ы индекс файла "c" Ы Ё Ы заблокирован Ы Ё Ы Приостанавливает Ы Ё Ы выполнение Ы Ё Ы Ы Ы Ё Просматривает ка- Ы Ы Ё талог "b" в поис- Ы Ы Ё ках имени "c" Ы Ы Ё Получает номер ин- Ы Ы Ё декса для "c" Ы Ы Ё Обнаруживает, что Ы Ы Ё индекс файла "c" Ы Ы Ё заблокирован Ы Ы Ё Приостанавливает Ы Ы Ё выполнение Ы Ы Ё Ы Ы Ы Ё Ы Возобновляет выпол- Ы Ё Ы нение, индекс "c" Ы Ё Ы свободен Ы Ё Ы Удаляет связь с име- Ы Ё Ы нем "c", прежний ин- Ы Ё Ы декс освобождается, Ы Ё Ы если число связей =0 Ы Ё Ы Ы Ы Ё Ы Ы Назначает индекс Ё Ы Ы новому файлу "n" Ё Ы Ы Случайно назнача- Ё Ы Ы ет ему индекс, ра- Ё Ы Ы нее принадлежавший Ё Ы Ы "c" Ё Ы Ы Ё Ы Ы В конечном итоге Ё Ы Ы снимает блокировку Ё Ы Ы с индекса "n" Ё Ы Ы Ё Возобновляет выпол- Ы Ё нение, прежний ин- Ы Ё декс "c" (теперь Ы Ё "n") свободен Ы Ё Получает индекс "n" Ы Ё Просматривает ка- Ы Ё талог "n" в поис- Ы Ё ках имени "d" Ы v Время Рисунок 5.32. Соперничество процессов за индекс при выполне- нии функции unlink здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё #include Ё Ё #include Ё Ё Ё Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё int fd; Ё Ё char buf[1024]; Ё Ё struct stat statbuf; Ё Ё Ё Ё if (argc != 2) /* нужен параметр */ Ё Ё exit(); Ё Ё fd = open(argv[1],O_RDONLY); Ё Ё if (fd == -1) /* open завершилась Ё Ё неудачно */ Ё Ё exit(); Ё Ё if (unlink(argv[1]) == -1) /* удалить связь с только Ё Ё что открытым файлом */ Ё Ё exit(); Ё Ё if (stat(argv[1],&statbuf) == -1) /* узнать состоя- Ё Ё ние файла по имени */ Ё Ё printf("stat %s завершилась неудачно\n",argv[1]);Ё Ё /* как и следовало бы */ Ё Ё else Ё Ё printf("stat %s завершилась успешно!\n",argv[1]);Ё Ё if (fstat(fd,&statbuf) == -1) /* узнать состояние Ё Ё файла по идентификатору */ Ё Ё printf("fstat %s сработала неудачно!\n",argv[1]);Ё Ё else Ё Ё printf("fstat %s завершилась успешно\n",argv[1]);Ё Ё /* как и следовало бы */ Ё Ё while (read(fd,buf,sizeof(buf)) > 0) /* чтение откры- Ё Ё того файла с удаленной связью */ Ё Ё printf("%1024s",buf); /* вывод на печать поля Ё Ё размером 1 Кбайт */ Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.33. Удаление связи с открытым файлом лагается, что тем временем никакой другой процесс не создал файл с тем же именем), но функция fstat завершится успешно, так как она выбирает индекс по дескриптору файла. Процесс выполняет цикл, считывая на каждом шаге по 1024 байта и пересылая файл в стан- дартный вывод. Когда при чтении будет обнаружен конец файла, про- цесс завершает работу: после завершения процесса файл перестает существовать. Процессы часто создают временные файлы и сразу же удаляют связь с ними; они могут продолжать ввод-вывод в эти фай- лы, но имена файлов больше не появляются в иерархии каталогов. Если процесс по какой-либо причине завершается аварийно, он не оставляет от временных файлов никакого следа. 5.17 АБСТРАКТНЫЕ ОБРАЩЕНИЯ К ФАЙЛОВЫМ СИСТЕМАМ Уайнбергером было введено понятие "тип файловой системы" для объяснения механизма работы принадлежавшей ему сетевой файловой системы (см. краткое описание этого механизма в [Killian 84]) и в позднейшей версии системы V поддерживаются основополагающие прин- ципы его схемы. Наличие типа файловой системы дает ядру возмож- ность поддерживать одновременно множество файловых систем, таких как сетевые файловые системы (глава 13) или даже файловые системы из других операционных систем. Процессы пользуются для обращения к файлам обычными функциями системы UNIX, а ядро устанавливает соответствие между общим набором файловых операций и операциями, специфичными для каждого типа файловой системы. Операции файловой Общие индексы Индекс файловой системы системы версии V зддддддддддддддд© здддддд© зддддддд© Версия V Ё open Ё здддддед дедддддддд>Ё Ё Ё close Ё Ё цдддддд╢ цддддддд╢ Ё read Ё Ё здддед деддд© Ё Ё Ё write Ё<ддды Ё цдддддд╢ Ё цддддддд╢ Ё Ы Ё<дддддЁдддед дедддЁдддд>Ё Ё Ё Ы Ё Ё цдддддд╢ Ё цддддддд╢ Ё Ы Ё Ё Ё Ё Ё Ё Ы Ё Ё Ы Ё Ё цдддддд╢ Ё Ё Ы Ё цддддддддддддддд╢ Ё Ё Ё Ё Ё Ы Ё Удаленная Ё ropen Ё Ё цдддддд╢ Ё юддддддды система Ё rclose Ё Ё Ё Ы Ё Ё Ё rread Ё Ё Ё Ы Ё Ё Индекс удален- Ё rwrite Ё<ддддды Ё Ы Ё Ё ной системы Ё Ы Ё Ё Ы Ё Ё зддддддд© Ё Ы Ё Ё Ы Ё Ё Ё Ё Ё Ы Ё Ё Ы Ё Ё цддддддд╢ Ё Ы Ё Ё Ы Ё юдддд>Ё Ё цддддддддддддддд╢ Ё Ы Ё цддддддд╢ Ё Ы Ё Ё Ы Ё Ё Ё Ё Ы Ё Ё Ы Ё цддддддд╢ Ё Ы Ё Ё Ы Ё Ё Ё Ё Ы Ё Ё Ы Ё цддддддд╢ Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юддддддддддддддды юдддддды юддддддды Рисунок 5.34. Индексы для файловых систем различных типов Индекс выступает интерфейсом между абстрактной файловой сис- темой и отдельной файловой системой. Общая копия индекса в памяти содержит информацию, не зависящую от отдельной файловой системы, а также указатель на частный индекс файловой системы, который уже содержит информацию, специфичную для нее. Частный индекс файловой системы содержит такую информацию, как права доступа и расположе- ние блоков, а общий индекс содержит номер устройства, номер ин- декса на диске, тип файла, размер, информацию о владельце и счет- чик ссылок. Другая частная информация, описывающая отдельную файловую систему, содержится в суперблоке и структуре каталогов. На Рисунке 5.34 изображены таблица общих индексов в памяти и две таблицы частных индексов отдельных файловых систем, одна для структур файловой системы версии V, а другая для индекса удален- ной (сетевой) системы. Предполагается, что последний индекс со- держит достаточно информации для того, чтобы идентифицировать файл, находящийся в удаленной системе. У файловой системы может отсутствовать структура, подобная индексу; но исходный текст программ отдельной файловой системы позволяет создать объектный код, удовлетворяющий семантическим требованиям файловой системы UNIX и назначающий свой "индекс", который соответствует общему индексу, назначаемому ядром. Файловая система каждого типа имеет некую структуру, в кото- рой хранятся адреса функций, реализующих абстрактные действия. Когда ядру нужно обратиться к файлу, оно вызывает косвенную функ- цию в зависимости от типа файловой системы и абстрактного дейс- твия (см. Рисунок 5.34). Примерами абстрактных действий являются: открытие и закрытие файла, чтение и запись данных, возвращение индекса для компоненты имени файла (подобно namei и iget), осво- бождение индекса (подобно iput), коррекция индекса, проверка прав доступа, установка атрибутов файла (прав доступа к нему), а также монтирование и демонтирование файловых систем. В главе 13 будет проиллюстрировано использование системных абстракций при рассмот- рении распределенной файловой системы. 5.18 СОПРОВОЖДЕНИЕ ФАЙЛОВОЙ СИСТЕМЫ Ядро поддерживает целостность системы в своей обычной работе. Тем не менее, такие чрезвычайные обстоятельства, как отказ пита- ния, могут привести к фатальному сбою системы, в результате кото- рого содержимое системы утрачивает свою согласованность: боль- шинство данных в файловой системе доступно для использования, но некоторая несогласованность между ними имеет место. Команда fsck проверяет согласованность данных и в случае необходимости вносит в файловую систему исправления. Она обращается к файловой системе через блочный или строковый интерфейс (глава 10) в обход традици- онных методов доступа к файлам. В этом разделе рассматриваются некоторые примеры противоречивости данных, которая обнаруживается командой fsck. Дисковый блок может принадлежать более чем одному индексу или списку свободных блоков. Когда файловая система открывается в первый раз, все дисковые блоки находятся в списке свободных бло- ков. Когда дисковый блок выбирается для использования, ядро уда- ляет его номер из списка свободных блоков и назначает блок индек- су. Ядро не может переназначить дисковый блок другому индексу до тех пор, пока блок не будет возвращен в список свободных блоков. Таким образом, дисковый блок может либо находиться в списке сво- бодных блоков, либо быть назначенным одному из индексов. Рассмот- рим различные ситуации, могущие иметь место при освобождении яд- ром дискового блока, принадлежавшего файлу, с возвращением номера блока в суперблок, находящийся в памяти, и при выделении дисково- го блока новому файлу. Если ядро записывало на диск индекс и бло- ки нового файла, но перед внесением изменений в индекс прежнего файла на диске произошел сбой, оба индекса будут адресовать к од- ному и тому же номеру дискового блока. Подобным же образом, если ядро переписывало на диск суперблок и его списки свободных ресур- сов и перед переписью старого индекса случился сбой, дисковый блок появится одновременно и в списке свободных блоков, и в ста- ром индексе. Если блок отсутствует как в списке свободных блоков, так и в файле, файловая система является несогласованной, ибо, как уже говорилось выше, все блоки обязаны где-нибудь присутствовать. Та- кая ситуация могла бы произойти, если бы блок был удален из файла и помещен в список свободных блоков в суперблоке. Если производи- лась запись прежнего файла на диск и система дала сбой перед за- писью суперблока, блок будет отсутствовать во всех списках, хра- нящихся на диске. Индекс может иметь счетчик связей с ненулевым значением при том, что его номер отсутствует во всех каталогах файловой систе- мы. Все файлы, за исключением каналов (непоименованных), должны присутствовать в древовидной структуре файловой системы. Если система дала сбой после создания канала или обычного файла, но перед созданием соответствующей этому каналу или файлу точки вхо- да в каталог, индекс будет иметь в поле счетчика связей установ- ленное значение, пусть даже он явно не присутствует в файловой системе. Еще одна проблема может возникнуть, если с помощью функ- ции unlink была удалена связь каталога без проверки удаления из каталога всех содержащихся в нем связей с отдельными файлами. Если формат индекса неверен (например, если значение поля ти- па файла не определено), значит где-то имеется ошибка. Это может произойти, если администратор смонтировал файловую систему, кото- рая отформатирована неправильно. Ядро обращается к тем дисковым блокам, которые, как кажется ядру, содержат индексы, но в дейс- твительности оказывается, что они содержат данные. Если номер индекса присутствует в записи каталога, но сам индекс свободен, файловая система является несогласованной, пос- кольку номер индекса в записи каталога должен быть номером назна- ченного индекса. Это могло бы произойти, если бы ядро, создавая новый файл и записывая на диск новую точку входа в каталог, не успела бы скопировать на диск индекс файла из-за сбоя. Также это может случиться, если процесс, удаляя связь файла с каталогом, запишет освободившийся индекс на диск, но не успеет откорректиро- вать каталог из-за сбоя. Возникновение подобных ситуаций можно предотвратить, копируя на диск результаты работы в надлежащем по- рядке. Если число свободных блоков или свободных индексов, записан- ное в суперблоке, не совпадает с их количеством на диске, файло- вая система так же является несогласованной. Итоговая информация в суперблоке всегда должна соответствовать информации о текущем состоянии файловой системы. 5.19 ВЫВОДЫ Этой главой завершается первая часть книги, посвященная расс- мотрению особенностей файловой системы. Глава познакомила пользо- вателя с тремя таблицами, принадлежащими ядру: таблицей пользова- тельских дескрипторов файла, системной таблицей файлов и таблицей монтирования. В ней рассмотрены алгоритмы выполнения системных функций, имеющих отношение к файловой системе, и взаимодействие между этими функциями. Исследованы некоторые абстрактные свойства файловой системы, позволяющие системе UNIX поддерживать файловые системы различных типов. Наконец, описан механизм выполнения ко- манды fsck, контролирующей целостность и согласованность данных в файловой системе. 5.20 УПРАЖНЕНИЯ 1. Рассмотрим программу, приведенную на Рисунке 5.35. Какое значение возвращает каждая операция read и что при этом со- держится в буфере ? Опишите, что происходит в ядре во время выполнения каждого вызова read. 2. Вновь вернемся к программе на Рисунке 5.35 и предположим, что оператор lseek(fd,9000L,0); стоит перед первым обращением к функции read. Что ищет про- цесс и что при этом происходит в ядре ? 3. Процесс может открыть файл для работы в режиме добавления записей в конец файла, при этом имеется в виду, что каждая операция записи располагает данные по адресу смещения, ука- зывающего текущий конец файла. Таким образом, два процесса могут открыть файл для работы в режиме добавления записей в конец файла и вводить данные, не опасаясь затереть записи друг другу. Что произойдет, если процесс откроет файл в ре- жиме добавления в конец, а записывающую головку установит на начало файла ? 4. Библиотека стандартных подпрограмм ввода-вывода повышает эффективность выполнения пользователем операций чтения и записи благодаря буферизации данных в библиотеке и сохране- нию большого количества модулей обращения к операционной системе, необходимых пользователю. Как бы вы реализовали библиотечные функции fread и fwrite ? Что должны делать библиотечные функции fopen и fclose ? здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main() Ё Ё { Ё Ё int fd; Ё Ё char buf[1024]; Ё Ё fd = creat("junk",0666); Ё Ё lseek(fd,2000L,2); /* ищется байт с номером 2000 */ Ё Ё write(fd,"hello",5); Ё Ё close(fd); Ё Ё Ё Ё fd = open("junk",O_RDONLY); Ё Ё read(fd,buf,1024); /* читает нули */ Ё Ё read(fd,buf,1024); /* считывает нечто, отличное от 0 */Ё Ё read(fd,buf,1024); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.35. Считывание нулей и конца файла 5. Если процесс читает данные из файла последовательно, ядро запоминает значение блока, прочитанного с продвижением, в индексе, хранящемся в памяти. Что произойдет, если несколь- ко процессов будут одновременно вести последовательное счи- тывание данных из одного и того же файла ? зддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main() Ё Ё { Ё Ё int fd; Ё Ё char buf[256]; Ё Ё Ё Ё fd = open("/etc/passwd",O_RDONLY); Ё Ё if (read(fd,buf,1024) < 0) Ё Ё printf("чтение завершается неудачно\n"); Ё Ё } Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.36. Чтение большой порции данных в маленький буфер 6. Рассмотрим программу, приведенную на Рисунке 5.36. Что про- изойдет в результате выполнения программы ? Обоснуйте от- вет. Что произошло бы, если бы объявление массива buf было вставлено между объявлениями двух других массивов размером 1024 элемента каждый ? Каким образом ядро устанавливает, что прочитанная порция данных слишком велика для буфера ? *7. В файловой системе BSD разрешается фрагментировать послед- ний блок файла в соответствии со следующими правилами: * Свободные фрагменты отслеживаются в структурах, подобных суперблоку; * Ядро не поддерживает пул ранее выделенных свободных фраг- ментов, а разбивает на фрагменты в случае необходимости свободный блок; * Ядро может назначать фрагменты блока только для последне- го блока в файле; * Если блок разбит на несколько фрагментов, ядро может наз- начить их различным файлам; * Количество фрагментов в блоке не должно превышать величи- ну, фиксированную для данной файловой системы; * Ядро назначает фрагменты во время выполнения системной функции write. Разработайте алгоритм, присоединяющий к файлу фрагменты блока. Какие изменения должны быть сделаны в индексе, чтобы позволить использование фрагментов ? Какие преимущества с системной точки зрения предоставляет использование фрагмен- тов для тех файлов, которые используют блоки косвенной ад- ресации ? Не выгоднее ли было бы назначать фрагменты во время выполнения функции close вместо того, чтобы назначать их при выполнении функции write ? *8. Вернемся к обсуждению, начатому в главе 4 и касающемуся расположения данных в индексе файла. Для того случая, когда индекс имеет размер дискового блока, разработайте алгоритм, по которому остаток данных файла переписывается в индексный блок, если помещается туда. Сравните этот метод с методом, предложенным для решения предыдущей проблемы. *9. В версии V системы функция fcntl используется для реализа- ции механизма захвата файла и записи и имеет следующий фор- мат: fcntl(fd,cmd,arg); где fd - дескриптор файла, cmd - тип блокирующей операции, а в arg указываются различные параметры, такие как тип бло- кировки (записи или чтения) и смещения в байтах (см. прило- жение). К блокирующим операциям относятся * Проверка наличия блокировок, принадлежащих другим процес- сам, с немедленным возвратом управления в случае обнару- жения таких блокировок, * Установка блокировки и приостанов до успешного заверше- ния, * Установка блокировки с немедленным возвратом управления в случае неудачи. Ядро автоматически снимает блокировки, установленные про- цессом, при закрытии файла. Опишите работу алгоритма, реа- лизующего захват файла и записи. Если блокировки являются обязательными, другим процессам следует запретить доступ к файлу. Какие изменения следует сделать в операциях чтения и записи ? *10. Если процесс приостановил свою работу в ожидании снятия с файла блокировки, возникает опасность взаимной блокировки: процесс A может заблокировать файл "one" и попытаться заб- локировать файл "two", а процесс B может заблокировать файл "two" и попытаться заблокировать файл "one". Оба процесса перейдут в состояние, при котором они не смогут продолжить свою работу. Расширьте алгоритм решения предыдущей проблемы таким образом, чтобы ядро могло обнаруживать ситуации вза- имной блокировки и прерывать выполнение системных функций. Следует ли поручать обнаружение взаимных блокировок ядру ? 11. До существования специальной системной функции захвата фай- ла пользователям приходилось прибегать к услугам параллель- но действующих процессов для реализации механизма захвата путем вызова системных функций, выполняющих элементарные действия. Какие из системных функций, описанных в этой гла- ве, могли бы использоваться ? Какие опасности подстерегают при использовании этих методов ? 12. Ричи заявлял (см. [Ritchie 81]), что захвата файла недоста- точно для того, чтобы предотвратить путаницу, вызываемую такими программами, как редакторы, которые создают копию файла при редактировании и переписывают первоначальный файл по окончании работы. Объясните, что он имел в виду, и про- комментируйте. 13. Рассмотрим еще один способ блокировки файлов, предотвращаю- щий разрушительные последствия корректировки. Предположим, что в индексе содержится новая установка прав доступа, поз- воляющая только одному процессу в текущий момент открывать файл для записи и нескольким процессам открывать файл для чтения. Опишите реализацию этого способа. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё if (argc != 2) Ё Ё { Ё Ё printf("введите: команда имя каталога\n"); Ё Ё exit(); Ё Ё } Ё Ё Ё Ё /* права доступа к каталогу: запись, чтение и ис- Ё Ё полнение разрешены для всех */ Ё Ё /* только суперпользователь может делать следую- Ё Ё щее */ Ё Ё if (mknod(argv[1],040777,0) == -1) Ё Ё printf("mknod завершилась неудачно\n"); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.37. Каталог, создание которого не завершено *14. Рассмотрим программу (Рисунок 5.37), которая создает ката- лог с неверным форматом (в каталоге отсутствуют записи с именами "." и ".."). Попробуйте, находясь в этом каталоге, выполнить несколько команд, таких как ls -l, ls -ld, или cd. Что произойдет при этом ? 15. Напишите программу, которая выводит для файлов, имена кото- рых указаны в качестве параметров, информацию о владельце, типе файла, правах доступа и времени доступа. Если файл (параметр) является каталогом, программа должна читать за- писи из каталога и выводить вышеуказанную информацию для всех файлов в каталоге. 16. Предположим, что у пользователя есть разрешение на чтение из каталога, но нет разрешения на исполнение. Что произой- дет, если каталог использовать в качестве параметра команды ls, заданной с опцией "-i" ? Что будет, если указана опция "-l" ? Поясните свои ответы. Ответьте на вопрос, сформули- рованный для случая, когда есть разрешение на исполнение, но нет разрешения на чтение из каталога. 17. Сравните права доступа, которые должны быть у процесса для выполнения следующих действий, и прокомментируйте: * Для создания нового файла требуется разрешение на запись в каталог. * Для "создания" существующего файла требуется разрешение на запись в файл. * Для удаления связи файла с каталогом требуется разрешение на запись в каталог, а не в файл. *18. Напишите программу, которая навещает все каталоги, начиная с текущего. Как она должна управлять циклами в иерархии ка- талогов ? 19. Выполните программу, приведенную на Рисунке 5.38, и объяс- ните, что при этом происходит в ядре. (Намек: выполните ко- манду pwd, когда программа закончится). 20. Напишите программу, которая заменяет корневой каталог указанным каталогом, и исследуйте дерево каталогов, доступ- ное для этой программы. 21. Почему процесс не может отменить предыдущий вызов функции chroot ? Измените конкретную реализацию процесса таким об- разом, чтобы он мог менять текущее значение корня на преды- дущее. Какие у этой возможности преимущества и неудобства ? 22. Рассмотрим простой пример канала (Рисунок 5.19), когда процесс записывает в канал строку "hello" и затем считывает здддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё if (argc != 2) Ё Ё { Ё Ё printf("нужен 1 аргумент - имя каталога\n"); Ё Ё exit(); Ё Ё } Ё Ё Ё Ё if (chdir(argv[1]) == -1) Ё Ё printf("%s файл не является каталогом\n",argv[1]);Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 5.38. Пример программы с использованием функции chdir ее. Что произошло бы, если бы массив для записи данных в канал имел размер 1024 байта вместо 6 (а объем считываемых за одну операцию данных оставался равным 6) ? Что произой- дет, если порядок вызова функций read и write в программе изменить, поменяв функции местами ? 23. Что произойдет при выполнении программы, иллюстрирующей ис- пользование поименованных каналов (Рисунок 5.19), если функция mknod обнаружит, что канал с таким именем уже су- ществует ? Как этот момент реализуется ядром ? Что произош- ло бы, если бы вместо подразумеваемых в тексте программы одного считывающего и одного записывающего процессов связь между собой через канал попытались установить несколько считывающих и записывающих процессов ? Как в этом случае гарантировалась бы связь одного считывающего процесса с од- ним записывающим процессом ? 24. Открывая поименованный канал для чтения, процесс приоста- навливается до тех пор, пока еще один процесс не откроет канал для записи. Почему ? Не мог бы процесс успешно пройти функцию open, продолжить работу до того момента, когда им будет предпринята попытка чтения данных из канала, и приос- тановиться при выполнении функции read ? 25. Как бы вы реализовали алгоритм выполнения системной функции dup2 (из версии 7), вызываемой следующим образом: dup2(oldfd,newfd); где oldfd - файловый дескриптор, который дублируется деск- риптором newfd ? Что произошло бы, если бы дескриптор newfd уже принадлежал открытому файлу ? *26. Какие последствия имело бы решение ядра позволить двум про- цессам одновременно смонтировать одну и ту же файловую сис- тему в двух точках монтирования ? 27. Предположим, что один процесс меняет свой текущий каталог на каталог "/mnt/a/b/c", после чего другой процесс в ката- логе "/mnt" монтирует файловую систему. Завершится ли функ- ция mount успешно ? Что произойдет, если первый процесс вы- полнит команду pwd ? Ядро не позволит функции mount успешно завершиться, если значение счетчика ссылок в индексе ката- лога "/mnt" превышает 1. Прокомментируйте этот момент. 28. При исполнении алгоритма пересечения точки монтирования по имени ".." в маршруте поиска файла ядро проверяет выполне- ние трех условий, связанных с точкой монтирования: что но- мер обнаруженного индекса совпадает с номером корневого ин- декса, что рабочий индекс является корнем файловой системы и что имя компоненты маршрута поиска - "..". Почему необхо- димо проверять выполнение всех трех условий ? Докажите, что проверки любых двух условий недостаточно для того, чтобы разрешить процессу пересечь точку монтирования. 29. Если пользователь монтирует файловую систему только для чтения, ядро устанавливает соответствующий флаг в супербло- ке. Как ядро может воспрепятствовать выполнению операций записи в функциях write, creat, link, unlink, chown и chmod ? Какого рода информацию записывают в файловую систе- му все перечисленные функции ? *30. Предположим, что один процесс пытается демонтировать файло- вую систему, в то время как другой процесс пытается создать в файловой системе новый файл. Только одна из функций umount и creat выполнится успешно. Подробно рассмотрите возникшую конкуренцию. *31. Когда функция umount проверяет отсутствие в файловой систе- ме активных файлов, возникает одна проблема, связанная с тем, что корневой индекс файловой системы, назначаемый при выполнении функции mount с помощью алгоритма iget, имеет счетчик ссылок с положительным значением. Как функция umount сможет убедиться в отсутствии активных файлов и отчитаться перед корнем файловой системы ? Рассмотрите два случая: * функция umount освобождает корневой индекс по алгоритму iput перед проверкой активных индексов. (Как функции вер- нуть этот индекс обратно, если будут обнаружены активные файлы ?) * функция umount проверяет отсутствие активных файлов до того, как освободить корневой индекс, и разрешая корнево- му индексу оставаться активным. (Насколько активным может быть корневой индекс ?) 32. Обратите внимание на то, что при выполнении команды ls -ld количество связей с каталогом никогда не равно 1. Почему ? 33. Как работает команда mkdir (создать новый каталог) ? (На- водящий вопрос: какие номера по завершении выполнения ко- манды имеют индексы для файлов "." и ".." ?) *34. Понятие "символические связи" имеет отношение к возможности указания с помощью функции link связей между файлами, при- надлежащими к различным файловым системам. С файлом симво- лической связи ассоциирован указатель нового типа; содержи- мым файла является имя пути поиска того файла, с которым он связан. Опишите реализацию символических связей. *35. Что произойдет, если процесс вызовет функцию unlink("."); Каким будет текущий каталог процесса ? Предполагается, что процесс обладает правами суперпользователя. 36. Разработайте системную функцию, которая усекает существую- щий файл до произвольных размеров, указанных в качестве ар- гумента, и опишите ее работу. Реализуйте системную функцию, которая позволяла бы пользователю удалять сегмент файла, расположенный между двумя адресами, заданными в виде смеще- ний, и сжимать файл. Напишите программу, которая не вызыва- ла бы эти функции, но обладала бы теми же функциональными возможностями. 37. Опишите все условия, при которых счетчик ссылок в индексе может превышать значение 1. 38. Затрагивая тему абстрактных обращений к файловым системам, ответьте на вопрос: следует ли файловой системе каждого ти- программы, или же достаточно общей операции блокирования ? СТРУКТУРА ПРОЦЕССОВ В главе 2 были сформулированы характеристики процессов. В настоящей главе на более формальном уровне определяется понятие "контекст процесса" и показывается, каким образом ядро идентифи- цирует процесс и определяет его местонахождение. В разделе 6.1 описаны модель состояний процессов для системы UNIX и последова- тельность возможных переходов из состояния в состояние. В ядре находится таблица процессов, каждая запись которой описывает сос- тояние одного из активных процессов в системе. В пространстве процесса хранится дополнительная информация, используемая в уп- равлении протеканием процесса. Запись в таблице процессов и пространство процесса составляют в совокупности контекст процес- са. Аспектом контекста процесса, наиболее явно отличающим данный контекст от контекста другого процесса, без сомнения является со- держимое адресного пространства процесса. В разделе 6.2 описыва- ются принципы управления распределением памяти для процессов и ядра, а также взаимодействие операционной системы с аппаратными средствами при трансляции виртуальных адресов в физические. Раз- дел 6.3 посвящен рассмотрению составных элементов контекста про- цесса, а также описанию алгоритмов управления контекстом процес- са. Раздел 6.4 демонстрирует, каким образом осуществляется сохранение контекста процесса ядром в случае прерывания, вызова системной функции или переключения контекста, а также каким обра- зом возобновляется выполнение приостановленного процесса. В раз- деле 6.5 приводятся различные алгоритмы, используемые в тех сис- темных функциях, которые работают с адресным пространством процесса и которые будут рассмотрены в следующей главе. И, нако- нец, в разделе 6.6 рассматриваются алгоритмы приостанова и возоб- новления выполнения процессов. 6.1 СОСТОЯНИЯ ПРОЦЕССА И ПЕРЕХОДЫ МЕЖДУ НИМИ Как уже отмечалось в главе 2, время жизни процесса можно тео- ретически разбить на несколько состояний, описывающих процесс. Полный набор состояний процесса содержится в следующем перечне: 1. Процесс выполняется в режиме задачи. 2. Процесс выполняется в режиме ядра. 3. Процесс не выполняется, но готов к запуску под управлением ядра. 4. Процесс приостановлен и находится в оперативной памяти. 5. Процесс готов к запуску, но программа подкачки (нулевой про- цесс) должна еще загрузить процесс в оперативную память, прежде чем он будет запущен под управлением ядра. Это состоя- ние будет предметом обсуждения в главе 9 при рассмотрении системы подкачки. 6. Процесс приостановлен и программа подкачки выгрузила его во внешнюю память, чтобы в оперативной памяти освободить место для других процессов. 7. Процесс возвращен из привилегированного режима (режима ядра) в непривилегированный (режим задачи), ядро резервирует его и переключает контекст на другой процесс. Об отличии этого сос- тояния от состояния 3 (готовность к запуску) пойдет речь ни- же. 8. Процесс вновь создан и находится в переходном состоянии; про- цесс существует, но не готов к выполнению, хотя и не приостановлен. Это состояние является начальным состоянием всех процессов, кроме нулевого. 9. Процесс вызывает системную функцию exit и прекращает сущест- вование. Однако, после него осталась запись, содержащая код выхода, и некоторая хронометрическая статистика, собираемая родительским процессом. Это состояние является последним сос- тоянием процесса. Рисунок 6.1 представляет собой полную диаграмму переходов процесса из состояния в состояние. Рассмотрим с помощью модели переходов типичное поведение процесса. Ситуации, которые будут обсуждаться, несколько искусственны и процессы не всегда имеют дело с ними, но эти ситуации вполне применимы для иллюстрации различных переходов. Начальным состоянием модели является созда- ние процесса родительским процессом с помощью системной функции fork; из этого состояния процесс неминуемо переходит в состояние готовности к запуску (3 или 5). Для простоты предположим, что процесс перешел в состояние "готовности к запуску в памяти" (3). Планировщик процессов в конечном счете выберет процесс для выпол- нения и процесс перейдет в состояние "выполнения в режиме ядра", где доиграет до конца роль, отведенную ему функцией fork. После всего этого процесс может перейти в состояние "выполне- ния в режиме задачи". По прохождении определенного периода време- ни может произойти прерывание работы процессора по таймеру и про- цесс снова перейдет в состояние "выполнения в режиме ядра". Как только программа обработки прерывания закончит работу, ядру может понадобиться подготовить к запуску другой процесс, поэтому первый процесс перейдет в состояние "резервирования", уступив дорогу второму процессу. Состояние "резервирования" в действительности не отличается от состояния "готовности к запуску в памяти" (пунк- тирная линия на рисунке, соединяющая между собой оба состояния, подчеркивает их эквивалентность), но они выделяются в отдельные состояния, чтобы подчеркнуть, что процесс, выполняющийся в режиме ядра, может быть зарезервирован только в том случае, если он со- Выполняется в режиме задачи зддддддд© Ё Ё Ё 1 Ё Вызов функ- Ё Ё ции, преры- юбдддддды вание Ё ^ ^ Преры- зддддд© зддддддды Ё Ё вание, Ё Ё Ё зддддддды юддд© Возврат в возвратЁ Ё Ё Ё Возврат Ё режим задачи из пре-Ё Ё Ё Ё Ё рыва-Ё v v Ё Выполняет- Ё зддддддд© ния Ё здддддда©ся в режи- задддддд© Ё Ё юдд>Ё Ёме ядра Ё Ё Ё 9 Ё<ддддддддддд╢ 2 цдддддддддддд>Ё 7 Ё Ё Ё Выход Ё Ё Резервирует-Ё Ё юддддддды юбдддддды ся юддддддды Прекращение Ё ^ Ы Зарезер- существования Ё Ё Ы вирован Ё Ё Ы зддддддддддддддды юдддддд© ЫЫЫЫЫЫЫЫ Ё Приостанов Запуск Ё Ы v Ё Ы При-зддддддд© здаддддд© Готов к ос- Ё Ё Возобновление Ё Ё запуску та- Ё 4 цддддддддддддддддддддддд>Ё 3 Ё в памяти нов-Ё Ё Ё Ё лен юдддбддды юбдддддды в па- Ё Ё ^ ^ мяти Ё Ё Ё Ё Достаточно Ё Ё Ё Ё памяти Ё Ё Ё юддд© Ё Вы- Вы- Ё Ё Ё Ё грузка грузка Ё Ё Ё Создан Ё Ё ЁЗа- задддддд© Ё Ё Ёгруз-Ё Ё fork Ё Ё Ёка Ё 8 Ё<ддддд Ё Ё Ё Ё Ё Ё Ё Ё юбдддддды Ё Ё Ё Ё Ё Ё Ё Ё Недоста- Ё Ё Ё зддды точно Ё Ё Ё Ё памяти Ё Ё Ё Ё (только система Ё Ё Ё Ё подкачки) v v Ё v зддддддд© здддаддд© Ё Ё Возобновление Ё Ё Ё 6 цддддддддддддддддддддддд>Ё 5 Ё Ё Ё Ё Ё юддддддды юддддддды Приостановлен, Готов к запуску, выгружен выгружен Рисунок 6.1. Диаграмма переходов процесса из состояния в сос- тояние бирается вернуться в режим задачи. Следовательно, ядро может при необходимости подкачивать процесс из состояния "резервирования". При известных условиях планировщик выберет процесс для исполнения и тот снова вернется в состояние "выполнения в режиме задачи". Когда процесс выполняет вызов системной функции, он из состо- яния "выполнения в режиме задачи" переходит в состояние "выполне- ния в режиме ядра". Предположим, что системной функции требуется ввод-вывод с диска и поэтому процесс вынужден дожидаться заверше- ния ввода-вывода. Он переходит в состояние "приостанова в памя- ти", в котором будет находиться до тех пор, пока не получит изве- щения об окончании ввода-вывода. Когда ввод-вывод завершится, произойдет аппаратное прерывание работы центрального процессора и программа обработки прерывания возобновит выполнение процесса, в результате чего он перейдет в состояние "готовности к запуску в памяти". Предположим, что система выполняет множество процессов, кото- рые одновременно никак не могут поместиться в оперативной памяти, и программа подкачки (нулевой процесс) выгружает один процесс, чтобы освободить место для другого процесса, находящегося в сос- тоянии "готов к запуску, но выгружен". Первый процесс, выгружен- ный из оперативной памяти, переходит в то же состояние. Когда программа подкачки выбирает наиболее подходящий процесс для заг- рузки в оперативную память, этот процесс переходит в состояние "готовности к запуску в памяти". Планировщик выбирает процесс для исполнения и он переходит в состояние "выполнения в режиме ядра". Когда процесс завершается, он исполняет системную функцию exit, последовательно переходя в состояния "выполнения в режиме ядра" и, наконец, в состояние "прекращения существования". Процесс может управлять некоторыми из переходов на уровне за- дачи. Во-первых, один процесс может создать другой процесс. Тем не менее, в какое из состояний процесс перейдет после создания (т.е. в состояние "готов к выполнению, находясь в памяти" или в состояние "готов к выполнению, но выгружен") зависит уже от ядра. Процессу эти состояния не подконтрольны. Во-вторых, процесс может обратиться к различным системным функциям, чтобы перейти из сос- тояния "выполнения в режиме задачи" в состояние "выполнения в ре- жиме ядра", а также перейти в режим ядра по своей собственной во- ле. Тем не менее, момент возвращения из режима ядра от процесса уже не зависит; в результате каких-то событий он может никогда не вернуться из этого режима и из него перейдет в состояние "прекра- щения существования" (см. раздел 7.2, где говорится о сигналах). Наконец, процесс может завершиться с помощью функции exit по сво- ей собственной воле, но как указывалось ранее, внешние события могут потребовать завершения процесса без явного обращения к функции exit. Все остальные переходы относятся к жестко закреп- ленной части модели, закодированной в ядре, и являются результа- том определенных событий, реагируя на них в соответствии с прави- лами, сформулированными в этой и последующих главах. Некоторые из правил уже упоминались: например, то, что процесс может выгрузить другой процесс, выполняющийся в ядре. Две принадлежащие ядру структуры данных описывают процесс: запись в таблице процессов и пространство процесса. Таблица про- цессов содержит поля, которые должны быть всегда доступны ядру, а пространство процесса - поля, необходимость в которых возникает только у выполняющегося процесса. Поэтому ядро выделяет место для пространства процесса только при создании процесса: в нем нет не- обходимости, если записи в таблице процессов не соответствует конкретный процесс. Запись в таблице процессов состоит из следующих полей: * Поле состояния, которое идентифицирует состояние процесса. * Поля, используемые ядром при размещении процесса и его прост- ранства в основной или внешней памяти. Ядро использует инфор- мацию этих полей для переключения контекста на процесс, когда процесс переходит из состояния "готов к выполнению, находясь в памяти" в состояние "выполнения в режиме ядра" или из сос- тояния "резервирования" в состояние "выполнения в режиме за- дачи". Кроме того, ядро использует эту информацию при пере- качки процессов из и в оперативную память (между двумя состо- яниями "в памяти" и двумя состояниями "выгружен"). Запись в таблице процессов содержит также поле, описывающее размер процесса и позволяющее ядру планировать выделение пространс- тва для процесса. * Несколько пользовательских идентификаторов (UID), устанавли- вающих различные привилегии процесса. Поля UID, например, описывают совокупность процессов, могущих обмениваться сигна- лами (см. следующую главу). * Идентификаторы процесса (PID), указывающие взаимосвязь между процессами. Значения полей PID задаются при переходе процесса в состояние "создан" во время выполнения функции fork. * Дескриптор события (устанавливается тогда, когда процесс при- остановлен). В данной главе будет рассмотрено использование дескриптора события в алгоритмах функций sleep и wakeup. * Параметры планирования, позволяющие ядру устанавливать поря- док перехода процессов из состояния "выполнения в режиме яд- ра" в состояние "выполнения в режиме задачи". * Поле сигналов, в котором перечисляются сигналы, посланные процессу, но еще не обработанные (раздел 7.2). * Различные таймеры, описывающие время выполнения процесса и использование ресурсов ядра и позволяющие осуществлять слеже- ние за выполнением и вычислять приоритет планирования процес- са. Одно из полей является таймером, который устанавливает пользователь и который необходим для посылки процессу сигнала тревоги (раздел 8.3). Пространство процесса содержит поля, дополнительно характери- зующие состояния процесса. В предыдущих главах были рассмотрены последние семь из приводимых ниже полей пространства процесса, которые мы для полноты вновь кратко перечислим: * Указатель на таблицу процессов, который идентифицирует за- пись, соответствующую процессу. * Пользовательские идентификаторы, устанавливающие различные привилегии процесса, в частности, права доступа к файлу (см. раздел 7.6). * Поля таймеров, хранящие время выполнения процесса (и его по- томков) в режиме задачи и в режиме ядра. * Вектор, описывающий реакцию процесса на сигналы. * Поле операторского терминала, идентифицирующее "регистрацион- ный терминал", который связан с процессом. * Поле ошибок, в которое записываются ошибки, имевшие место при выполнении системной функции. * Поле возвращенного значения, хранящее результат выполнения системной функции. * Параметры ввода-вывода: объем передаваемых данных, адрес ис- точника (или приемника) данных в пространстве задачи, смеще- ния в файле (которыми пользуются операции ввода-вывода) и т.д. * Имена текущего каталога и текущего корня, описывающие файло- вую систему, в которой выполняется процесс. * Таблица пользовательских дескрипторов файла, которая описыва- ет файлы, открытые процессом. * Поля границ, накладывающие ограничения на размерные характе- ристики процесса и на размер файла, в который процесс может вести запись. * Поле прав доступа, хранящее двоичную маску установок прав доступа к файлам, которые создаются процессом. Пространство состояний процесса и переходов между ними расс- матривалось в данном разделе на логическом уровне. Каждое состоя- ние имеет также физические характеристики, управляемые ядром, в частности, виртуальное адресное пространство процесса. Следующий раздел посвящен описанию модели распределения памяти; в остальных разделах состояния процесса и переходы между ними рассматриваются на физическом уровне, особое внимание при этом уделяется состоя- ниям "выполнения в режиме задачи", "выполнения в режиме ядра", "резервирования" и "приостанова (в памяти)". В следующей главе затрагиваются состояния "создания" и "прекращения существования", а в главе 8 - состояние "готовности к запуску в памяти". В главе 9 обсуждаются два состояния выгруженного процесса и организация подкачки по обращению. 6.2 ФОРМАТ ПАМЯТИ СИСТЕМЫ Предположим, что физическая память машины имеет адреса, начи- ная с 0 и кончая адресом, равным объему памяти в байтах. Как уже отмечалось в главе 2, процесс в системе UNIX состоит из трех ло- гических секций: команд, данных и стека. (Общую память, которая рассматривается в главе 11, можно считать в данном контексте частью секции данных). В секции команд хранится набор машинных инструкций, исполняемых под управлением процесса; адресами в сек- ции команд выступают адреса команд (для команд перехода и обраще- ний к подпрограммам), адреса данных (для обращения к глобальным переменным) и адреса стека (для обращения к структурам данных, которые локализованы в подпрограммах). Если адреса в сгенериро- ванном коде трактовать как адреса в физической памяти, два про- цесса не смогут параллельно выполняться, если их адреса перекры- ваются. Компилятор мог бы генерировать адреса, непересекающиеся у разных программ, но на универсальных ЭВМ такой порядок не практи- куется, поскольку объем памяти машины ограничен, а количество транслируемых программы неограничено. Даже если для того, чтобы избежать излишнего пересечения адресов в процессе их генерации, машина будет использовать некоторый набор эвристических процедур, подобная реализация не будет достаточно гибкой и не сможет удов- летворять предъявляемым к ней требованиям. Поэтому компилятор генерирует адреса для виртуального адрес- ного пространства заданного диапазона, а устройство управления памятью, называемое диспетчером памяти, транслирует виртуальные адреса, сгенерированные компилятором, в адреса ячеек, расположен- ных в физической памяти. Компилятору нет необходимости знать, в какое место в памяти ядро потом загрузит выполняемую программу. На самом деле, в памяти одновременно могут существовать несколько копий программы: все они могут выполняться, используя одни и те же виртуальные адреса, фактически же ссылаясь на разные физичес- кие ячейки. Те подсистемы ядра и аппаратные средства, которые сотрудничают в трансляции виртуальных адресов в физические, обра- зуют подсистему управления памятью. 6.2.1 Области Ядро в версии V делит виртуальное адресное пространство про- цесса на совокупность логических областей. Область - это непре- рывная зона виртуального адресного пространства процесса, рассматриваемая в качестве отдельного объекта для совместного ис- пользования и защиты. Таким образом, команды, данные и стек обыч- но образуют автономные области, принадлежащие процессу. Несколько процессов могут использовать одну и ту же область. Например, если несколько процессов выполняют одну и ту же программу, вполне ес- тественно, что они используют одну и ту же область команд. Точно так же, несколько процессов могут объединиться и использовать об- щую область разделяемой памяти. Ядро поддерживает таблицу областей и выделяет запись в табли- це для каждой активной области в системе. В разделе 6.5 описыва- ются поля таблицы областей и операции над областями более подроб- но, но на данный момент предположим, что таблица областей содержит информацию, позволяющую определить местоположение облас- ти в физической памяти. Каждый процесс имеет частную таблицу об- ластей процесса. Записи этой таблицы могут располагаться, в зави- симости от конкретной реализации, в таблице процессов, в адресном пространстве процесса или в отдельной области памяти; для просто- ты предположим, что они являются частью таблицы процессов. Каждая запись частной таблицы областей содержит указатель на соответс- твующую запись общей таблицы областей и первый виртуальный адрес процесса в данной области. Разделяемые области могут иметь разные виртуальные адреса в каждом процессе. Запись частной таблицы об- ластей также содержит поле прав доступа, в котором указывается тип доступа, разрешенный процессу: только чтение, только запись или только исполнение. Частная таблица областей и структура об- ласти аналогичны таблице файлов и структуре индекса в файловой системе: несколько процессов могут совместно использовать адрес- ное пространство через область, подобно тому, как они разделяют доступ к файлу с помощью индекса; каждый процесс имеет доступ к области благодаря использованию записи в частной таблице облас- тей, точно так же он обращается к индексу, используя соответству- ющие записи в таблице пользовательских дескрипторов файла и в таблице файлов, принадлежащей ядру. На Рисунке 6.2 изображены два процесса, A и B, показаны их области, частные таблицы областей и виртуальные адреса, в которых эти области соединяются. Процессы разделяют область команд 'a' с виртуальными адресами 8К и 4К соответственно. Если процесс A чи- тает ячейку памяти с адресом 8К, а процесс B читает ячейку с ад- ресом 4К, то они читают одну и ту же ячейку в области 'a'. Область данных и область стека у каждого процесса свои. Область является понятием, не зависящим от способа реализации управления памятью в операционной системе. Управление памятью представляет собой совокупность действий, выполняемых ядром с целью повышения эффективности совместного использования оператив- ной памяти процессами. Примерами способов управления памятью мо- гут служить рассматриваемые в главе 9 замещение страниц памяти и подкачка по обращению. Понятие области также не зависит и от соб- ственно распределения памяти: например, от того, делится ли па- мять на страницы или на сегменты. С тем, чтобы заложить фундамент для перехода к описанию алгоритмов подкачки по обращению (глава 9), все приводимые здесь рассуждения относятся, в первую очередь, к организации памяти, базирующейся на страницах, однако это не предполагает, что система управления памятью основывается на ука- занных алгоритмах. Частные таблицы областей Области процесса (Виртуальные адреса) здддддддд© Коман-здддддддддддддд© Ё Ё ды Ё 8К цддддд© здд>Ё b Ё Процесс Дан-цдддддддддддддд╢ Ё Ё Ё Ё A ные Ё 16К цдддддЁддды юдддддддды здддддддд© цдддддддддддддд╢ Ё Ё Ё Стек Ё 32К цдддддЁдддддддддддддддддддд>Ё c Ё юдддддддддддддды Ё здддддддд© Ё Ё юдддддд>Ё Ё юдддддддды Ё a Ё Коман-здддддддддддддд© здддддд>Ё Ё ды Ё 4К цддддды юдддддддды здддддддд© Процесс Дан-цдддддддддддддд╢ Ё Ё B ные Ё 8К цдддддддддддддддддддддддддд>Ё e Ё цдддддддддддддд╢ здддддддд© Ё Ё Стек Ё 32К цддддд© Ё Ё юдддддддды юдддддддддддддды юдддддд>Ё d Ё Ё Ё юдддддддды Рисунок 6.2. Процессы и области 6.2.2 Страницы и таблицы страниц В этом разделе описывается модель организации памяти, которой мы будем пользоваться на протяжении всей книги, но которая не яв- ляется особенностью системы UNIX. В организации памяти, базирую- щейся на страницах, физическая память разделяется на блоки одина- кового размера, называемые страницами. Обычный размер страниц составляет от 512 байт до 4 Кбайт и определяется конфигурацией технических средств. Каждая адресуемая ячейка памяти содержится в некоторой странице и, следовательно, каждая ячейка памяти может адресоваться парой (номер страницы, смещение внутри страницы в байтах). Например, если объем машинной памяти составляет 2 в 32-й степени байт, а размер страницы 1 Кбайт, общее число страниц - 2 в 22-й степени; можно считать, что каждый 32-разрядный адрес состоит из 22-разрядного номера страницы и 10-разрядного смещения внутри страницы (Рисунок 6.3). Когда ядро назначает области физические страницы памяти, не- обходимости в назначении смежных страниц и вообще в соблюдении какой-либо очередности при назначении не возникает. Целью стра- ничной организации памяти является повышение гибкости назначения физической памяти, которое строится по аналогии с назначением дисковых блоков файлам в файловой системе. Как и при назначении блоков файлу, так и при назначении области страниц памяти, прес- ледуется задача повышения гибкости и сокращения неиспользуемого (вследствие фрагментации) пространства памяти. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё Шестнадцатиричный адрес 58432 Ё Ё Ё Ё Двоичный 0101 1000 0100 0011 0010 Ё Ё Ё Ё Номер страницы, смещение Ё Ё внутри страницы 01 0110 0001 00 0011 0010 Ё Ё Ё Ё В шестнадцатиричной системе 161 32 Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.3. Адресация физической памяти по страницам здддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё Логический номер страницы Физический номер страницы Ё Ё Ё Ё 0 177 Ё Ё 1 54 Ё Ё 2 209 Ё Ё 3 17 Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.4. Отображение логических номеров страниц на физи- ческие Ядро устанавливает соотношение между виртуальными адресами области и машинными физическими адресами посредством отображения логических номеров страниц в области на физические номера страниц в машине, как это показано на Рисунке 6.4. Поскольку область это непрерывное пространство виртуальных адресов программы, логичес- кий номер страницы служит указателем на элемент массива физичес- ких номеров страниц. Запись таблицы областей содержит указатель на таблицу физических номеров страниц, именуемую таблицей стра- ниц. Записи таблицы страниц содержат машинно-зависимую информа- цию, такую как права доступа на чтение или запись страницы. Ядро поддерживает таблицы страниц в памяти и обращается к ним так же, как и ко всем остальным структурам данных ядра. На Рисунке 6.5 приведен пример отображения процесса в физи- ческие адреса памяти. Пусть размер страницы составляет 1 Кбайт и пусть процессу нужно обратиться к объекту в памяти, имеющему вир- туальный адрес 68432. Из таблицы областей видно, что виртуальный адрес начала области стека - 65536 (64К), если предположить, что стек растет в направлении увеличения адресов. После вычитания этого адреса из адреса 68432 получаем смещение в байтах внутри области, равное 2896. Так как каждая страница имеет размер 1 Кбайт, адрес указывает со смещением 848 на 2-ю (начиная с 0) страницу области, расположенной по физическому адресу 986К. В разделе 6.5.5 (где идет речь о загрузке области) рассматривается случай, когда запись таблицы страниц помечается "пустой". В современных машинах используются разнообразные аппаратные регистры и кеши, которые повышают скорость выполнения вышеописан- ной процедуры трансляции адресов и без которых пересылки в памяти и адресные вычисления чересчур бы замедлились. Возобновляя выпол- нение процесса, ядро посредством загрузки соответствующих регист- ров сообщает техническим средствам управления памятью о том, в Частная таблица областей Таблицы страниц процесса (Физические адреса) здддддддддддддд© Команды Ё 8К цдддддддддддддд© цдддддддддддддд╢ юдддддддд>здддддддддддддд© Данные Ё 32К цддддддд© Ё пусто Ё цдддддддддддддд╢ Ё цдддддддддддддд╢ Стек Ё 64К цддд© Ё Ё 137К Ё юдддддддддддддды Ё v цдддддддддддддд╢ Виртуальные адреса Ё здддддддддддддд© Ё 852К Ё Ё Ё 87К Ё цдддддддддддддд╢ здддддддддддды цдддддддддддддд╢ Ё 764К Ё v Ё 552К Ё цдддддддддддддд╢ здддддддддддддд© цдддддддддддддд╢ Ё 433К Ё Ё 541К Ё Ё 727К Ё цдддддддддддддд╢ цдддддддддддддд╢ цдддддддддддддд╢ Ё 333К Ё Ё 783К Ё Ё 941К Ё цдддддддддддддд╢ цдддддддддддддд╢ цдддддддддддддд╢ Ё Ы Ё Ё 986К Ё Ё 1096К Ё Ё Ы Ё цдддддддддддддд╢ цдддддддддддддд╢ Ё Ы Ё Ё 897К Ё Ё 2001К Ё Ё Ы Ё цдддддддддддддд╢ цдддддддддддддд╢ юдддддддддддддды Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юдддддддддддддды юдддддддддддддды Рисунок 6.5. Преобразование виртуальных адресов в физические каких физических адресах выполняется процесс и где располагаются таблицы страниц. Поскольку такие операции являются машинно-зави- симыми и в разных версиях реализуются по-разному, здесь мы их рассматривать не будем. Часть вопросов, связанных с архитектурой вычислительных систем, затрагивается в упражнениях. Организацию управления памятью попробуем пояснить на следую- щем простом примере. Пусть память разбита на страницы размером 1 Кбайт каждая, обращение к которым осуществляется через описанные ранее таблицы страниц. Регистры управления памятью в системе группируются по три; первый регистр в тройке содержит адрес таб- лицы страниц в физической памяти, второй регистр содержит первый виртуальный адрес, отображаемый с помощью тройки регистров, тре- тий регистр содержит управляющую информацию, такую как номера страниц в таблице страниц и права доступа к страницам (только чтение, чтение и запись). Такая модель соответствует вышеописан- ной модели области. Когда ядро готовит процесс к выполнению, оно загружает тройки регистров соответствующей информацией из записей частной таблицы областей процесса. Если процесс обращается к ячейкам памяти, расположенным за пределами принадлежащего ему виртуального пространства, создается исключительная ситуация. Например, если область команд имеет раз- мер 16 Кбайт (Рисунок 6.5), а процесс обращается к виртуальному адресу 26К, создается исключительная ситуация, обрабатываемая операционной системой. То же самое происходит, если процесс пыта- ется обратиться к памяти, не имея соответствующих прав доступа, например, пытается записать адрес в защищенную от записи область команд. И в том, и в другом примере процесс обычно завершается (более подробно об этом в следующей главе). 6.2.3 Размещение ядра Несмотря на то, что ядро работает в контексте процесса, отображение виртуальных адресов, связанных с ядром, осуществляет- ся независимо от всех процессов. Программы и структуры данных яд- ра резидентны в системе и совместно используются всеми процесса- ми. При запуске системы происходит загрузка программ ядра в память с установкой соответствующих таблиц и регистров для отоб- ражения виртуальных адресов ядра в физические. Таблицы страниц для ядра имеют структуру, аналогичную структуре таблицы страниц, связанной с процессом, а механизмы отображения виртуальных адре- сов ядра похожи на механизмы, используемые для отображения поль- зовательских адресов. На многих машинах виртуальное адресное пространство процесса разбивается на несколько классов, в том числе системный и пользовательский, и каждый класс имеет свои собственные таблицы страниц. При работе в режиме ядра система разрешает доступ к адресам ядра, при работе же в режиме задачи такого рода доступ запрещен. Поэтому, когда в результате прерыва- ния или выполнения системной функции происходит переход из режима задачи в режим ядра, операционная система по договоренности с техническими средствами разрешает ссылки на адреса ядра, а при возврате в режим ядра эти ссылки уже запрещены. В других машинах можно менять преобразование виртуальных адресов, загружая специ- альные регистры во время работы в режиме ядра. На Рисунке 6.6 приведен пример, в котором виртуальные адреса от 0 до 4М-1 принадлежат ядру, а начиная с 4М - процессу. Имеются две группы регистров управления памятью, одна для адресов ядра и одна для адресов процесса, причем каждой группе соответствует таблица страниц, хранящая номера физических страниц со ссылкой на адреса виртуальных страниц. Адресные ссылки с использованием группы регистров ядра допускаются системой только в режиме ядра; следовательно, для перехода между режимом ядра и режимом задачи требуется только, чтобы система разрешила или запретила адресные ссылки с использованием группы регистров ядра. В некоторых системах ядро загружается в память таким образом, что большая часть виртуальных адресов ядра совпадает с физически- ми адресами и функция преобразования виртуальных адресов в физи- ческие превращается в функцию тождественности. Работа с прост- ранством процесса, тем не менее, требует, чтобы преобразование виртуальных адресов в физические производилось ядром. Адрес таблицы Вирту- Номера стра- страниц альный ниц в табли- адрес це здддддддддддбддддддддддддбддддддддддд© Регистр ядра 1 Ё дддддддд© 0 Ё Ё цдддддддддддедЁддддддддддеддддддддддд╢ Регистр ядра 2 Ё ддд© Ё Ё 1М Ё Ё цддддддддЁддедЁддддддддддеддддддддддд╢ Регистр ядра 3 Ё дд©Ё Ё Ё 2М Ё Ё цдддддддЁЁддедЁддддддддддеддддддддддд╢ Регистр процесса 1 Ё зддд ЁЁ Ё Ё 4М Ё Ё цдЁдддддЁЁддедЁддддддддддеддддддддддд╢ Регистр процесса 2 Ё Ёздд ЁЁ Ё Ё Ё Ё цдЁЁддддЁЁддедЁддддддддддеддддддддддд╢ Регистр процесса 3 Ё ЁЁзд ЁЁ Ё Ё Ё Ё юдЁЁЁдддЁЁддадЁддддддддддаддддддддддды зддддддддддддддддддддддддыЁЁ ЁЁ Ё Ё зддддддддддддддыЁ ЁюддддЁддддддд© Ё Ё здддды юдддддЁдддддддЁдддддддддд© Ё Ё Ё здды Ё Ё v v v v v v здддддд© здддддд© здддддд© здддддд© здддддд© здддддд© Ё 856K Ё Ё 747K Ё Ё 556K Ё Ё 0K Ё Ё 128K Ё Ё 256K Ё цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ Ё 917K Ё Ё 950K Ё Ё 997K Ё Ё 4K Ё Ё 97K Ё Ё 292K Ё цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ Ё 564K Ё Ё 333K Ё Ё 458K Ё Ё 3K Ё Ё 135K Ё Ё 304K Ё цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ Ё 444K Ё Ё Ы Ё Ё 632K Ё Ё 17K Ё Ё 139K Ё Ё 279K Ё цдддддд╢ Ё Ы Ё цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юдддддды юдддддды юдддддды юдддддды юдддддды юдддддды Таблицы страниц процесса Таблицы страниц ядра (области) Рисунок 6.6. Переключение режима работы с непривилегированно- го (режима задачи) на привилегированный (режим ядра) 6.2.4 Пространство процесса Каждый процесс имеет свое собственное пространство, однако ядро обращается к пространству выполняющегося процесса так, как если бы в системе оно было единственным. Ядро подбирает для теку- щего процесса карту трансляции виртуальных адресов, необходимую для работы с пространством процесса. При компиляции загрузчик назначает переменной 'u' (имени пространства процесса) фиксиро- ванный виртуальный адрес. Этот адрес известен остальным компонен- там ядра, в частности модулю, выполняющему переключение контекста (раздел 6.4.3). Ядру также известно, какие таблицы управления па- мятью используются при трансляции виртуальных адресов, принадле- жащих пространству процесса, и благодаря этому ядро может быстро перетранслировать виртуальный адрес пространства процесса в дру- гой физический адрес. По одному и тому же виртуальному адресу яд- ро может получить доступ к двум разным физическим адресам, описы- вающим пространства двух процессов. Процесс имеет доступ к своему пространству, когда выполняется в режиме ядра, но не тогда, когда выполняется в режиме задачи. Поскольку ядро в каждый момент времени работает только с одним пространством процесса, используя для доступа виртуальный адрес, пространство процесса частично описывает контекст процесса, вы- полняющегося в системе. Когда ядро выбирает процесс для исполне- ния, оно ищет в физической памяти соответствующее процессу прост- ранство и делает его доступным по виртуальному адресу. Адрес таблицы Вирту- Номера стра- страниц альный ниц в табли- адрес це здддддддддддбддддддддддддбддддддддддд© Регистр ядра 1 Ё Ё Ё Ё цдддддддддддеддддддддддддеддддддддддд╢ Регистр ядра 2 Ё Ё Ё Ё цдддддддддддеддддддддддддеддддддддддд╢ (Прост- Регистр ядра 3 Ё ддд© Ё 2M Ё 4 Ё ранство юддддддддЁддаддддддддддддаддддддддддды процесса) Ё юддддддддддддддддддддд© Ё Таблицы страниц для пространства процессов Ё v здддддд© здддддд© здддддд© здддддд© Ё 114K Ё Ё 843K Ё Ё1879K Ё Ё 184K Ё цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ Ё 708K Ё Ё 794K Ё Ё 290K Ё Ё 176K Ё цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ Ё 143K Ё Ё 361K Ё Ё 450K Ё Ё 209K Ё цдддддд╢ цдддддд╢ цдддддд╢ цдддддд╢ Ё 565K Ё Ё 847K Ё Ё 770K Ё Ё 477K Ё юдддддды юдддддды юдддддды юдддддды Процесс A Процесс B Процесс C Процесс D Рисунок 6.7. Карта памяти пространства процесса в ядре Предположим, например, что пространство процесса имеет размер 4 Кбайта и помещается по виртуальному адресу 2М. На Рисунке 6.7 показана карта памяти, где первые два регистра из группы относят- ся к программам и данным ядра (адреса и указатели не показаны), а третий регистр адресует к пространству процесса D. Если ядру нуж- но обратиться к пространству процесса A, оно копирует связанную с этим пространством информацию из соответствующей таблицы страниц в третий регистр. В любой момент третий регистр ядра описывает пространство текущего процесса, но ядро может сослаться на прост- ранство другого процесса, переписав записи в таблице страниц с новым адресом. Информация в регистрах 1 и 2 для ядра неизменна, поскольку все процессы совместно используют программы и данные ядра. 6.3 КОНТЕКСТ ПРОЦЕССА Контекст процесса включает в себя содержимое адресного прост- ранства задачи, выделенного процессу, а также содержимое относя- щихся к процессу аппаратных регистров и структур данных ядра. С формальной точки зрения, контекст процесса объединяет в себе пользовательский контекст, регистровый контекст и системный кон- текст (*). Пользовательский контекст состоит из команд и данных процесса, стека задачи и содержимого совместно используемого пространства памяти в виртуальных адресах процесса. Те части вир- туального адресного пространства процесса, которые периодически отсутствуют в оперативной памяти вследствие выгрузки или замеще- ния страниц, также включаются в пользовательский контекст. Регистровый контекст состоит из следующих компонент: * Счетчика команд, указывающего адрес следующей команды, кото- рую будет выполнять центральный процессор; этот адрес являет- ся виртуальным адресом внутри пространства ядра или прост- ранства задачи. * Регистра состояния процессора (PS), который указывает аппа- ратный статус машины по отношению к процессу. Регистр PS, например, обычно содержит подполя, которые указывают, являет- ся ли результат последних вычислений нулевым, положительным или отрицательным, переполнен ли регистр с установкой бита переноса и т.д. Операции, влияющие на установку регистра PS, выполняются для отдельного процесса, потому-то в регистре PS и содержится аппаратный статус машины по отношению к процес- су. В других имеющих важное значение подполях регистра PS указывается текущий уровень прерывания процессора, а также текущий и предыдущий режимы выполнения процесса (режим яд- ра/задачи). По значению подполя текущего режима выполнения процесса устанавливается, может ли процесс выполнять привиле- гированные команды и обращаться к адресному пространству яд- ра. * Указателя вершины стека, в котором содержится адрес следующе- го элемента стека ядра или стека задачи, в соответствии с ре- жимом выполнения процесса. В зависимости от архитектуры маши- ны указатель вершины стека показывает на следующий свободный элемент стека или на последний используемый элемент. От архи- тектуры машины также зависит направление увеличения стека (к старшим или младшим адресам), но для нас сейчас эти вопросы несущественны. * Регистров общего назначения, в которых содержится информация, сгенерированная процессом во время его выполнения. Чтобы об- легчить последующие объяснения, выделим среди них два регист- ра - регистр 0 и регистр 1 - для дополнительного использова- ния при передаче информации между процессами и ядром. Системный контекст процесса имеет "статическую часть" (первые три элемента в нижеследующем списке) и "динамическую часть" (пос- ледние два элемента). На протяжении всего времени выполнения про- цесс постоянно располагает одной статической частью системного контекста, но может иметь переменное число динамических частей. Динамическую часть системного контекста можно представить в виде ддддддддддддддддддддддддддддддддддддддд (*) Используемые в данном разделе термины "пользовательский кон- текст" (user-level context), "регистровый контекст" (register context), "системный контекст" (system-level context) и "кон- текстные уровни" (context layers) введены автором. стека, элементами которого являются контекстные уровни, которые помещаются в стек ядром или выталкиваются из стека при наступле- нии различных событий. Системный контекст включает в себя следую- щие компоненты: * Запись в таблице процессов, описывающая состояние процесса (раздел 6.1) и содержащая различную управляющую информацию, к которой ядро всегда может обратиться. * Часть адресного пространства задачи, выделенная процессу, где хранится управляющая информация о процессе, доступная только в контексте процесса. Общие управляющие параметры, такие как приоритет процесса, хранятся в таблице процессов, поскольку обращение к ним должно производиться за пределами контекста процесса. * Записи частной таблицы областей процесса, общие таблицы об- ластей и таблицы страниц, необходимые для преобразования вир- туальных адресов в физические, в связи с чем в них описывают- ся области команд, данных, стека и другие области, принадле- жащие процессу. Если несколько процессов совместно используют общие области, эти области входят составной частью в контекст каждого процесса, поскольку каждый процесс работает с этими областями независимо от других процессов. В задачи управления памятью входит идентификация участков виртуального адресного пространства процесса, не являющихся резидентными в памяти. * Стек ядра, в котором хранятся записи процедур ядра, если про- цесс выполняется в режиме ядра. Несмотря на то, что все про- цессы пользуются одними и теми же программами ядра, каждый из них имеет свою собственную копию стека ядра для хранения ин- дивидуальных обращений к функциям ядра. Пусть, например, один процесс вызывает функцию creat и приостанавливается в ожида- нии назначения нового индекса, а другой процесс вызывает функцию read и приостанавливается в ожидании завершения пере- дачи данных с диска в память. Оба процесса обращаются к функ- циям ядра и у каждого из них имеется в наличии отдельный стек, в котором хранится последовательность выполненных обра- щений. Ядро должно иметь возможность восстанавливать содержи- мое стека ядра и положение указателя вершины стека для того, чтобы возобновлять выполнение процесса в режиме ядра. В раз- личных системах стек ядра часто располагается в пространстве процесса, однако этот стек является логически-независимым и, таким образом, может помещаться в самостоятельной области памяти. Когда процесс выполняется в режиме задачи, соответс- твующий ему стек ядра пуст. * Динамическая часть системного контекста процесса, состоящая из нескольких уровней и имеющая вид стека, который освобожда- ется от элементов в порядке, обратном порядку их поступления. На каждом уровне системного контекста содержится информация, необходимая для восстановления предыдущего уровня и включаю- щая в себя регистровый контекст предыдущего уровня. Ядро помещает контекстный уровень в стек при возникновении прерывания, при обращении к системной функции или при переключе- нии контекста процесса. Контекстный уровень выталкивается из сте- ка после завершения обработки прерывания, при возврате процесса в режим задачи после выполнения системной функции, или при переклю- чении контекста. Таким образом, переключение контекста влечет за собой как помещение контекстного уровня в стек, так и извлечение уровня из стека: ядро помещает в стек контекстный уровень старого процесса, а извлекает из стека контекстный уровень нового процес- са. Информация, необходимая для восстановления текущего контекс- тного уровня, хранится в записи таблицы процессов. На Рисунке 6.8 изображены компоненты контекста процесса. Сле- ва на рисунке изображена статическая часть контекста. В нее входят: пользовательский контекст, состоящий из программ процесса Статическая часть контекста Динамическая часть контекста зддддддддддддддддддддддддд© логичес- Ё Ы Ё ЁПользовательский контекстЁ кий ука- Ё Ы Ё Ё зддддддддддддддддддддд© Ё затель наЁ Ы Ё Ё Ё Программы процесса Ё Ё текущий Ё Ы Ё Ё Ё Данные Ё Ёздддддддд>цдддддддддддддддд╢ Ё Ё Стек Ё ЁЁ контек- Ё Стек ядра для Ё Ё Ё Разделяемые данные Ё ЁЁ стный Ё уровня 3 Ё Ё юддддддддддддддддддддды ЁЁ уровень Ё Ё Ё ЁЁ Ё Сохраненный ре-Ё Ё Статическая часть ЁЁУровень 3Ё гистровый кон- Ё Ё системного контекста ЁЁ Ё текст уровня 2 Ё Ё зддддддддддддддддддддд© ЁЁ цдддддддддддддддд╢ Ё Ё Запись таблицы про- Ё ЁЁ Ё Стек ядра для Ё Ё Ё цессов едеы Ё уровня 2 Ё Ё ЁПространство процессаЁ Ё Ё Ё Ё Ё Частная таблица об- Ё Ё Ё Сохраненный ре-Ё Ё Ё ластей процесса Ё Ё Уровень 2Ё гистровый кон- Ё Ё юддддддддддддддддддддды Ё Ё текст уровня 1 Ё юддддддддддддддддддддддддды цдддддддддддддддд╢ Ё Стек ядра для Ё Ё уровня 1 Ё Ё Ё Ё Сохраненный ре-Ё Уровень 1Ё гистровый кон- Ё Ё текст уровня 0 Ё цдддддддддддддддд╢ КонтекстныйЁ Ё уровеньЁ (Пользователь- Ё ядра 0Ё ский уровень) Ё юдддддддддддддддды Рисунок 6.8. Компоненты контекста процесса (машинных инструкций), данных, стека и разделяемой памяти (если она имеется), а также статическая часть системного контекста, состоящая из записи таблицы процессов, пространства процесса и записей частной таблицы областей (информации, необходимой для трансляции виртуальных адресов пользовательского контекста). Справа на рисунке изображена динамическая часть контекста. Она имеет вид стека и включает в себя несколько элементов, хранящих регистровый контекст предыдущего уровня и стек ядра для текущего уровня. Нулевой контекстный уровень представляет собой пустой уровень, относящийся к пользовательскому контексту; увеличение стека здесь идет в адресном пространстве задачи, стек ядра не- действителен. Стрелка, соединяющая между собой статическую часть системного контекста и верхний уровень динамической части кон- текста, означает то, что в таблице процессов хранится информация, позволяющая ядру восстанавливать текущий контекстный уровень про- цесса. Процесс выполняется в рамках своего контекста или, если гово- рить более точно, в рамках своего текущего контекстного уровня. Количество контекстных уровней ограничивается числом поддерживае- мых в машине уровней прерывания. Например, если в машине поддер- живаются разные уровни прерываний для программ, терминалов, дис- ков, всех остальных периферийных устройств и таймера, то есть 5 уровней прерывания, то, следовательно, у процесса может быть не более 7 контекстных уровней: по одному на каждый уровень прерыва- ния, 1 для системных функций и 1 для пользовательского контекста. 7 уровней будет достаточно, даже если прерывания будут поступать в "наихудшем" из возможных порядков, поскольку прерывание данного уровня блокируется (то есть его обработка откладывается централь- ным процессором) до тех пор, пока ядро не обработает все прерыва- ния этого и более высоких уровней. Несмотря на то, что ядро всегда исполняет контекст какого-ни- будь процесса, логическая функция, которую ядро реализует в каждый момент, не всегда имеет отношение к данному процессу. Нап- ример, если возвращая данные, дисковое запоминающее устройство посылает прерывание, то прерывается выполнение текущего процесса и ядро обрабатывает прерывание на новом контекстном уровне этого процесса, даже если данные относятся к другому процессу. Програм- мы обработки прерываний обычно не обращаются к статическим сос- тавляющим контекста процесса и не видоизменяют их, так как эти части не связаны с прерываниями. 6.4 СОХРАНЕНИЕ КОНТЕКСТА ПРОЦЕССА Как уже говорилось ранее, ядро сохраняет контекст процесса, помещая в стек новый контекстный уровень. В частности, это имеет место, когда система получает прерывание, когда процесс вызывает системную функцию или когда ядро выполняет переключение контекс- та. Каждый из этих случаев подробно рассматривается в этом разде- ле. 6.4.1 Прерывания и особые ситуации Система отвечает за обработку всех прерываний, поступили ли они от аппаратуры (например, от таймера или от периферийных уст- ройств), от программ (в связи с выполнением инструкций, вызываю- щих возникновение "программных прерываний") или явились результа- том особых ситуаций (таких как обращение к отсутствующей странице). Если центральный процессор ведет обработку на более низком уровне по сравнению с уровнем поступившего прерывания, то перед выполнением следующей инструкции его работа прерывается, а уровень прерывания процессора повышается, чтобы другие прерывания с тем же (или более низким) уровнем не могли иметь места до тех пор, пока ядро не обработает текущее прерывание, благодаря чему обеспечивается сохранение целостности структур данных ядра. В процессе обработки прерывания ядро выполняет следующую последова- тельность действий: 1. Сохраняет текущий регистровый контекст выполняющегося процес- са и создает в стеке (помещает в стек) новый контекстный уро- вень. 2. Устанавливает "источник" прерывания, идентифицируя тип преры- вания (например, прерывание по таймеру или от диска) и номер устройства, вызвавшего прерывание (например, если прерывание вызвано дисковым запоминающим устройством). При возникновении прерывания система получает от машины число, которое исполь- зует в качестве смещения в таблице векторов прерывания. Со- держимое векторов прерывания в разных машинах различно, но, как правило, в них хранится адрес программы обработки преры- вания, соответствующей источнику прерывания, и указывается путь поиска параметра для программы. В качестве примера расс- мотрим таблицу векторов прерывания, приведенную на Рисунке 6.9. Если источником прерывания явился терминал, ядро получа- ет от аппаратуры номер прерывания, равный 2, и вызывает прог- зддддддддддддддддддддддддддддддддддддддддд© Ё Номер прерывания Программа обработки Ё Ё прерывания Ё Ё Ё Ё 0 clockintr Ё Ё 1 diskintr Ё Ё 2 ttyintr Ё Ё 3 devintr Ё Ё 4 softintr Ё Ё 5 otherintr Ё юддддддддддддддддддддддддддддддддддддддддды Рисунок 6.9. Пример векторов прерывания рамму обработки прерываний от терминала, именуемую ttyintr. 3. Вызов программы обработки прерывания. Стек ядра для нового контекстного уровня, если рассуждать логически, должен отли- чаться от стека ядра предыдущего контекстного уровня. В неко- торых разработках стек ядра текущего процесса используется для хранения элементов, соответствующих программам обработки прерываний, в других разработках эти элементы хранятся в гло- бальном стеке прерываний, благодаря чему обеспечивается возв- рат из программы без переключения контекста. 4. Программа завершает свою работу и возвращает управление ядру. Ядро исполняет набор машинных команд по сохранению регистро- вого контекста и стека ядра предыдущего контекстного уровня в том виде, который они имели в момент прерывания, после чего возобновляет выполнение восстановленного контекстного уровня. Программа обработки прерываний может повлиять на поведение процесса, поскольку она может внести изменения в глобальные структуры данных ядра и возобновить выполнение приостановлен- ных процессов. Однако, обычно процесс продолжает выполняться так, как если бы прерывание никогда не происходило. зддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм inthand /* обработка прерываний */ Ё Ё входная информация: отсутствует Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё сохранить (поместить в стек) текущий контекстный Ё Ё уровень; Ё Ё установить источник прерывания; Ё Ё найти вектор прерывания; Ё Ё вызвать программу обработки прерывания; Ё Ё восстановить (извлечь из стека) предыдущий кон- Ё Ё текстный уровень; Ё Ё } Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.10. Алгоритм обработки прерываний На Рисунке 6.10 кратко изложено, каким образом ядро обрабаты- вает прерывания. С помощью использования в отдельных случаях пос- ледовательности машинных операций или микрокоманд на некоторых машинах достигается больший эффект по сравнению с тем, когда все операции выполняются программным обеспечением, однако имеются уз- кие места, связанные с числом сохраняемых контекстных уровней и скоростью выполнения машинных команд, реализующих сохранение кон- текста. По этой причине определенные операции, выполнения которых требует реализация системы UNIX, являются машинно-зависимыми. Последовательность прерываний зддддддддддддддддддддддддддддддд© Ё Контекстный уровень ядра 3 Ё Ё Исполнить программу обра- Ё Ё ботки прерывания по таймеру Ё Ё Ё Ё Сохранить регистровый кон- Ё Ё текст программы обработки Ё Ё прерывания от диска Ё Прерывание по таймеру ЫЫЫЫЫЫЫЫцддддддддддддддддддддддддддддддд╢ ^ Ё Контекстный уровень ядра 2 Ё Ё Ё Исполнить программу обра- Ё Ё Ё ботки прерывания от диска Ё Ё Ё Ё Ё Ё Сохранить регистровый кон- Ё Ё Ё текст обращения к системной Ё Ё Ё функции Ё Прерывание от диска ЫЫЫЫЫЫЫЫЫЫцддддддддддддддддддддддддддддддд╢ ^ Ё Контекстный уровень ядра 1 Ё Ё Ё Исполнить обращение к сис- Ё Ё Ё темной функции Ё Ё Ё Ё Ё Ё Сохранить регистровый кон- Ё Ё Ё текст пользовательского Ё Ё Ё уровня Ё Вызов системной функции ЫЫЫЫЫЫюддддддддддддддддддддддддддддддды ^ Ё Ё Ё Ё Ё Ё Исполнение в режиме задачи Рисунок 6.11. Примеры прерываний На Рисунке 6.11 показан пример, в котором процесс запрашивает выполнение системной функции (см. следующий раздел) и получает прерывание от диска при ее выполнении. Запустив программу обра- ботки прерывания от диска, система получает прерывание по таймеру и вызывает уже программу обработки прерывания по таймеру. Каждый раз, когда система получает прерывание (или вызывает системную функцию), она создает в стеке новый контекстный уровень и сохра- няет регистровый контекст предыдущего уровня. 6.4.2 Взаимодействие с операционной системой через вызовы системных функций Такого рода взаимодействие с ядром было предметом рассмотре- ния в предыдущих главах, где шла речь об обычном вызове функций. Очевидно, что обычная последовательность команд обращения к функ- ции не в состоянии переключить выполнения процесса с режима зада- чи на режим ядра. Компилятор с языка Си использует библиотеку функций, имена которых совпадают с именами системных функций, иначе ссылки на системные функции в пользовательских программах были бы ссылками на неопределенные имена. В библиотечных функциях обычно исполняется команда, переводящая выполнение процесса в ре- жим ядра и побуждающая ядро к запуску исполняемого кода системной функции. В дальнейшем эта команда именуется "внутренним прерыва- нием операционной системы". Библиотечные процедуры исполняются в режиме задачи, а взаимодействие с операционной системой через вы- зов системной функции можно определить в нескольких словах как особый случай программы обработки прерывания. Библиотечные функ- ции передают ядру уникальный номер системной функции одним из ма- шинно-зависимых способов - либо как параметр внутреннего прерыва- ния операционной системы, либо через отдельный регистр, либо через стек - а ядро таким образом определяет тип вызываемой функ- ции. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм syscall /* алгоритм запуска системной функции */Ё Ё входная информация: номер системной функции Ё Ё выходная информация: результат системной функции Ё Ё { Ё Ё найти запись в таблице системных функций, соответствую-Ё Ё щую указанному номеру функции; Ё Ё определить количество параметров, передаваемых функции;Ё Ё скопировать параметры из адресного пространства задачи Ё Ё в пространство процесса; Ё Ё сохранить текущий контекст для аварийного завершения Ё Ё (см. раздел 6.44); Ё Ё запустить в ядре исполняемый код системной функции; Ё Ё если (во время выполнения функции произошла ошибка) Ё Ё { Ё Ё установить номер ошибки в нулевом регистре сохра- Ё Ё ненного регистрового контекста задачи; Ё Ё включить бит переноса в регистре PS сохраненного Ё Ё регистрового контекста задачи; Ё Ё } Ё Ё в противном случае Ё Ё занести возвращаемые функцией значения в регистры 0 Ё Ё и 1 в сохраненном регистровом контексте задачи; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.12. Алгоритм обращения к системным функциям Обрабатывая внутреннее прерывание операционной системы, ядро по номеру системной функции ведет в таблице поиск адреса соот- ветствующей процедуры ядра, то есть точки входа системной функ- ции, и количества передаваемых функции параметров (Рисунок 6.12). Ядро вычисляет адрес (пользовательский) первого параметра функ- ции, прибавляя (или вычитая, в зависимости от направления увели- чения стека) смещение к указателю вершины стека задачи (аналогич- но для всех параметров функции). Наконец, ядро копирует параметры задачи в пространство процесса и вызывает соответствующую проце- дуру, которая выполняет системную функцию. После исполнения про- цедуры ядро выясняет, не было ли ошибки. Если ошибка была, ядро делает соответствующие установки в сохраненном регистровом кон- тексте задачи, при этом в регистре PS обычно устанавливается бит переноса, а в нулевой регистр заносится номер ошибки. Если при выполнении системной функции не было ошибок, ядро очищает в ре- гистре PS бит переноса и заносит возвращаемые функцией значения в регистры 0 и 1 в сохраненном регистровом контексте задачи. Когда ядро возвращается после обработки внутреннего прерывания операци- онной системы в режим задачи, оно попадает в следующую библиотеч- ную инструкцию после прерывания. Библиотечная функция интерпрети- рует возвращенные ядром значения и передает их программе пользо- вателя. В качестве примера рассмотрим программу, которая создает файл с разрешением чтения и записи в него для всех пользователей (ре- жим доступа 0666) и которая приведена в верхней части Рисунка 6.13. Далее на рисунке изображен отредактированный фрагмент сге- нерированного кода программы после компиляции и дисассемблирова- ния (создания по объектному коду эквивалентной программы на языке ассемблера) в системе Motorola 68000. На Рисунке 6.14 изображена конфигурация стека для системной функции создания. Компилятор ге- нерирует программу помещения в стек задачи двух параметров, один из которых содержит установку прав доступа (0666), а другой - пе- ременную "имя файла" (**). Затем из адреса 64 процесс вызывает библиотечную функцию creat (адрес 7a), аналогичную соответствую- щей системной функции. Адрес точки возврата из функции - 6a, этот адрес помещается процессом в стек. Библиотечная функция creat за- сылает в регистр 0 константу 8 и исполняет команду прерывания (trap), которая переключает процесс из режима задачи в режим ядра и заставляет его обратиться к системной функции. Заметив, что процесс вызывает системную функцию, ядро выбирает из регистра 0 номер функции (8) и определяет таким образом, что вызвана функция creat. Просматривая внутреннюю таблицу, ядро обнаруживает, что системной функции creat необходимы два параметра; восстанавливая регистровый контекст предыдущего уровня, ядро копирует параметры из пользовательского пространства в пространство процесса. Проце- дуры ядра, которым понадобятся эти параметры, могут найти их в определенных местах адресного пространства процесса. По заверше- нии исполнения кода функции creat управление возвращается прог- рамме обработки обращений к операционной системе, которая прове- ряет, установлено ли поле ошибки в пространстве процесса (то есть имела ли место во время выполнения функции ошибка); если да, программа устанавливает в регистре PS бит переноса, заносит в ре- гистр 0 код ошибки и возвращает управление ядру. Если ошибок не было, в регистры 0 и 1 ядро заносит код завершения. Возвращая уп- ддддддддддддддддддддддддддддддддддддддд (**) Очередность, в которой компилятор вычисляет и помещает в стек параметры функции, зависит от реализации системы. здддддддддддддддддддддддддддддддддддддддд© Ё char name[] = "file"; Ё Ё main() Ё Ё { Ё Ё int fd; Ё Ё fd = creat(name,0666); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддды зддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё Фрагменты ассемблерной программы, сгенерированной в Ё Ё системе Motorola 68000 Ё Ё Ё Ё Адрес Команда Ё Ё Ы Ё Ё Ы Ё Ё # текст главной программы Ё Ё Ы Ё Ё 58: mov &Ox1b6,(%sp) # поместить код 0666 в стек Ё Ё 5e: mov &Ox204,-(%sp) # поместить указатель вершины Ё Ё # стека и переменную "имя файла"Ё Ё # в стек Ё Ё 64: jsr Ox7a # вызов библиотечной функции Ё Ё # создания файла Ё Ё Ы Ё Ё Ы Ё Ё # текст библиотечной функции создания файла Ё Ё 7a: movq &Ox8,%d0 # занести значение 8 в регистр 0Ё Ё 7c: trap &Ox0 # внутреннее прерывание операци-Ё Ё # онной системы Ё Ё 7e: bcc &Ox6 <86> # если бит переноса очищен, Ё Ё # перейти по адресу 86 Ё Ё 80: jmp Ox13c # перейти по адресу 13c Ё Ё 86: rts # возврат из подпрограммы Ё Ё Ы Ё Ё Ы Ё Ё # текст обработки ошибок функции Ё Ё 13c: mov %d0,&Ox20e # поместить содержимое регистра Ё Ё # 0 в ячейку 20e (переменная Ё Ё # errno) Ё Ё 142: movq &-Ox1,%d0 # занести в регистр 0 константу Ё Ё # -1 Ё Ё 144: mova %d0,%a0 Ё Ё 146: rts # возврат из подпрограммы Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.13. Системная функция creat и сгенерированная прог- рамма ее выполнения в системе Motorola 68000 равление из программы обработки обращений к операционной системе в режим задачи, библиотечная функция проверяет состояние бита пе- реноса в регистре PS (по адресу 7): если бит установлен, управле- ние передается по адресу 13c, из нулевого регистра выбирается код ошибки и помещается в глобальную переменную errno по адресу 20, в регистр 0 заносится -1, и управление возвращается на следующую после адреса 64 (где производится вызов функции) команду. Код за- вершения функции имеет значение -1, что указывает на ошибку в вы- полнении системной функции. Если же бит переноса в регистре PS при переходе из режима ядра в режим задачи имеет нулевое значе- ние, процесс с адреса 7 переходит по адресу 86 и возвращает уп- равление вызвавшей программе (адрес 64); регистр 0 содержит возв- ращаемое функцией значение. зддддддддд© Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ёстек ядра для кон-Ё Ё Ы Ё Ётекстного уровня 1Ё цддддддддд╢ Ё Ё Ё 1b6 Ё код режима доступа ЁпоследовательностьЁ Ё Ё (666 в восьмиричной системе) Ёкоманд обращения кЁ Ё 204 Ё адрес переменной "имя файла" Ё функции creat Ё Ё 6a Ё адрес точки возврата после цдддддддддддддддддд╢ Ё Ё вызова библиотечной функции Ёсохраненный регис-Ё цддддддддд╢<ддддд© Ё тровый контекст Ё Ё внутрен-Ё Ё Ё для уровня 0 Ё Ё нее пре-Ё Ё Ё(пользовательско- Ё Ё рывание Ё значение указателя Ё го) Ё Ё в Ё вершины стека в мо- Ё Ё Ё 7c Ё мент внутреннего пре- Ё счетчик команд, Ё юддддддддды рывания операционной Ё установленный на Ё направление системы Ё 7e Ё увеличения стека Ё Ё Ё Ёуказатель вершины Ё Ё Ё стека Ё v Ё Ё Ё регистр PS Ё Ё Ё Ёрегистр 0 (введеноЁ Ё значение 8) Ё Ё Ё Ё другие регистры Ё Ёобщего назначения Ё юдддддддддддддддддды Рисунок 6.14. Конфигурация стека для системной функции creat Несколько библиотечных функций могут отображаться на одну точку входа в список системных функций. Каждая точка входа опре- деляет точные синтаксис и семантику обращения к системной функ- ции, однако более удобный интерфейс обеспечивается с помощью биб- лиотек. Существует, например, несколько конструкций системной функции exec, таких как execl и execle, выполняющих одни и те же действия с небольшими отличиями. Библиотечные функции, соответс- твующие этим конструкциям, при обработке параметров реализуют за- явленные свойства, но в конечном итоге, отображаются на одну и ту же функцию ядра. 6.4.3 Переключение контекста Если обратиться к диаграмме состояний процесса (Рисунок 6.1), можно увидеть, что ядро разрешает производить переключение кон- текста в четырех случаях: когда процесс приостанавливает свое вы- полнение, когда он завершается, когда он возвращается после вызо- ва системной функции в режим задачи, но не является наиболее под- ходящим для запуска, или когда он возвращается в режим задачи после завершения ядром обработки прерывания, но так же не являет- ся наиболее подходящим для запуска. Как уже было показано в главе 2, ядро поддерживает целостность и согласованность своих внутрен- них структур данных, запрещая произвольно переключать контекст. Прежде чем переключать контекст, ядро должно удостовериться в согласованности своих структур данных: то есть в том, что сделаны все необходимые корректировки, все очереди выстроены надлежащим образом, установлены соответствующие блокировки, позволяющие из- бежать вмешательства со стороны других процессов, что нет излиш- них блокировок и т.д. Например, если ядро выделяет буфер, считы- вает блок из файла и приостанавливает выполнение до завершения передачи данных с диска, оно оставляет буфер заблокированным, чтобы другие процессы не смогли обратиться к буферу. Но если про- цесс исполняет системную функцию link, ядро снимает блокировку с первого индекса перед тем, как снять ее со второго индекса, и тем самым предотвращает возникновение тупиковых ситуаций (взаимной блокировки). Ядро выполняет переключение контекста по завершении системной функции exit, поскольку в этом случае больше ничего не остается делать. Кроме того, переключение контекста допускается, когда процесс приостанавливает свою работу, поскольку до момента возоб- новления может пройти немало времени, в течение которого могли бы выполняться другие процессы. Переключение контекста допускается и тогда, когда процесс не имеет преимуществ перед другими процесса- ми при исполнении, с тем, чтобы обеспечить более справедливое планирование процессов: если по выходе процесса из системной функции или из прерывания обнаруживается, что существует еще один процесс, который имеет более высокий приоритет и ждет выполнения, то было бы несправедливо оставлять его в ожидании. Процедура переключения контекста похожа на процедуры обработ- ки прерываний и обращения к системным функциям, если не считать того, что ядро вместо предыдущего контекстного уровня текущего процесса восстанавливает контекстный уровень другого процесса. Причины, вызвавшие переключение контекста, при этом не имеют зна- чения. На механизм переключения контекста не влияет и метод выбо- ра следующего процесса для исполнения. здддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё 1. Принять решение относительно необходимости переклю- Ё Ё чения контекста и его допустимости в данный момент. Ё Ё 2. Сохранить контекст "прежнего" процесса. Ё Ё 3. Выбрать процесс, наиболее подходящий для исполнения,Ё Ё используя алгоритм диспетчеризации процессов, приве-Ё Ё денный в главе 8. Ё Ё 4. Восстановить его контекст. Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.15. Последовательность шагов, выполняемых при пе- реключении контекста Текст программы, реализующей переключение контекста в системе UNIX, из всех программ операционной системы самый трудный для по- нимания, ибо при рассмотрении обращений к функциям создается впе- чатление, что они в одних случаях не возвращают управление, а в других - возникают непонятно откуда. Причиной этого является то, что ядро во многих системных реализациях сохраняет контекст про- цесса в одном месте программы, но продолжает работу, выполняя пе- реключение контекста и алгоритмы диспетчеризации в контексте "прежнего" процесса. Когда позднее ядро восстанавливает контекст процесса, оно возобновляет его выполнение в соответствии с ранее сохраненным контекстом. Чтобы различать между собой те случаи, когда ядро восстанавливает контекст нового процесса, и когда оно продолжает исполнять ранее сохраненный контекст, можно варьиро- вать значения, возвращаемые критическими функциями, или устанав- ливать искусственным образом текущее значение счетчика команд. На Рисунке 6.16 приведена схема переключения контекста. Функ- ция save_context сохраняет информацию о контексте исполняемого процесса и возвращает значение 1. Кроме всего прочего, ядро сох- раняет текущее значение счетчика команд (в функции save_context) и значение 0 в нулевом регистре при выходе из функции. Ядро про- должает исполнять контекст "прежнего" процесса (A), выбирая для выполнения следующий процесс (B) и вызывая функцию resume_context здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё if (save_context()) /* сохранение контекста выполняющегосяЁ Ё процесса */ Ё Ё { Ё Ё /* выбор следующего процесса для выполнения */ Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё resume_context(new_process); Ё Ё /* сюда программа не попадает ! */ Ё Ё } Ё Ё /* возобновление выполнение процесса начинается отсюда */ Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.16. Псевдопрограмма переключения контекста для восстановления его контекста. После восстановления контекста система выполняет процесс B; прежний процесс (A) больше не испол- няется, но он оставил после себя сохраненный контекст. Позже, когда будет выполняться переключение контекста, ядро снова избе- рет процесс A (если только, разумеется, он не был завершен). В результате восстановления контекста A ядро присвоит счетчику ко- манд то значение, которое было сохранено процессом A ранее в функции save_context, и возвратит в регистре 0 значение 0. Ядро возобновляет выполнение процесса A из функции save_context, пусть даже при выполнении программы переключения контекста оно не доб- ралось еще до функции resume_context. В конечном итоге, процесс A возвращается из функции save_context со значением 0 (в нулевом регистре) и возобновляет выполнение после строки комментария "во- зобновление выполнение процесса начинается отсюда". 6.4.4 Сохранение контекста на случай аварийного завершения Существуют ситуации, когда ядро вынуждено аварийно прерывать текущий порядок выполнения и немедленно переходить к исполнению ранее сохраненного контекста. В последующих разделах, где пойдет речь о приостановлении выполнения и о сигналах, будут описаны об- стоятельства, при которых процессу приходится внезапно изменять свой контекст; в данном же разделе рассматривается механизм ис- полнения предыдущего контекста. Алгоритм сохранения контекста на- зывается setjmp, а алгоритм восстановления контекста - longjmp (***). Механизм работы алгоритма setjmp похож на механизм функции save_context, рассмотренный в предыдущем разделе, если не считать того, что функция save_context помещает новый контекстный уровень в стек, в то время как setjmp сохраняет контекст в пространстве процесса и после выхода из него выполнение продолжается в прежнем контекстном уровне. Когда ядру понадобится восстановить контекст, ддддддддддддддддддддддддддддддддддддддд (***) Эти алгоритмы не следует путать с имеющими те же названия библиотечными функциями, которые могут вызываться непос- редственно из пользовательских программ (см. [SVID 85]). Однако действие этих функций похоже. сохраненный в результате работы алгоритма setjmp, оно исполнит алгоритм longjmp, который восстанавливает контекст из пространс- тва процесса и имеет, как и setjmp, код завершения, равный 1. 6.4.5 Копирование данных между адресным пространством сис- темы и адресным пространством задачи До сих пор речь шла о том, что процесс выполняется в режиме ядра или в режиме задачи без каких-либо перекрытий (пересечений) между режимами. Однако, при выполнении большинства системных функций, рассмотренных в последней главе, между пространством яд- ра и пространством задачи осуществляется пересылка данных, напри- мер, когда идет копирование параметров вызываемой функции из пространства задачи в пространство ядра или когда производится передача данных из буферов ввода-вывода в процессе выполнения функции read. На многих машинах ядро системы может непосредствен- но ссылаться на адреса, принадлежащие адресному пространству за- дачи. Ядро должно убедиться в том, что адрес, по которому произ- водится запись или считывание, доступен, как будто бы работа ве- дется в режиме задачи; в противном случае произошло бы нарушение стандартных методов защиты и ядро, пусть неумышленно, стало бы обращаться к адресам, которые находятся за пределами адресного пространства задачи (и, возможно, принадлежат структурам данных ядра). Поэтому передача данных между пространством ядра и прост- ранством задачи является "дорогим предприятием", требующим для своей реализации нескольких команд. здддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё fubyte: # пересылка байта из Ё Ё # пространства задачи Ё Ё prober $3,$1,*4(ap) # байт доступен ? Ё Ё beql eret # нет Ё Ё movzbl *4(ap),r0 Ё Ё ret Ё Ё eret: Ё Ё mnegl $1,r0 # возврат ошибки (-1) Ё Ё ret Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.17. Пересылка данных из пространства задачи в пространство ядра в системе VAX На Рисунке 6.17 показан пример реализованной в системе VAX программы пересылки символа из адресного пространства задачи в адресное пространство ядра. Команда prober проверяет, может ли байт по адресу, равному (регистр указателя аргумента + 4), быть считан в режиме задачи (режиме 3), и если нет, ядро передает уп- равление по адресу eret, сохраняет в нулевом регистре -1 и выхо- дит из программы; при этом пересылки символа не происходит. В противном случае ядро пересылает один байт, находящийся по ука- занному адресу, в регистр 0 и возвращает его в вызывающую прог- рамму. Пересылка 1 символа потребовала пяти команд (включая вызов функции с именем fubyte). 6.5 УПРАВЛЕНИЕ АДРЕСНЫМ ПРОСТРАНСТВОМ ПРОЦЕССА В этой главе мы пока говорили о том, каким образом осущест- вляется переключение контекста между процессами и как контекстные уровни запоминаются в стеке и выбираются из стека, представляя контекст пользовательского уровня как статический объект, не пре- терпевающий изменений при восстановлении контекста процесса. Од- нако, с виртуальным адресным пространством процесса работают раз- личные системные функции и, как будет показано в следующей главе, выполняют при этом операции над областями. В этом разделе расс- матривается информационная структура области; системные функции, реализующие операции над областями, будут рассмотрены в следующей главе. Запись таблицы областей содержит информацию, необходимую для описания области. В частности, она включает в себя следующие по- ля: * Указатель на индекс файла, содержимое которого было первона- чально загружено в область * Тип области (область команд, разделяемая память, область частных данных или стека) * Размер области * Местоположение области в физической памяти * Статус (состояние) области, представляющий собой комбинацию из следующих признаков: - заблокирована - запрошена - идет процесс ее загрузки в память - готова, загружена в память * Счетчик ссылок, в котором хранится количество процессов, ссы- лающихся на данную область. К операциям работы с областями относятся: блокировка области, снятие блокировки с области, выделение области, присоединение об- ласти к пространству памяти процесса, изменение размера области, загрузка области из файла в пространство памяти процесса, осво- бождение области, отсоединение области от пространства памяти процесса и копирование содержимого области. Например, системная функция exec, в которой содержимое исполняемого файла накладыва- ется на адресное пространство задачи, отсоединяет старые области, освобождает их в том случае, если они не являются разделяемыми, выделяет новые области, присоединяет их и загружает содержимым файла. В остальной части раздела операции над областями описыва- ются более детально с ориентацией на модель управления памятью, рассмотренную ранее (с таблицами страниц и группами аппаратных регистров), и с ориентацией на алгоритмы назначения страниц физи- ческой памяти и таблиц страниц (глава 9). 6.5.1 Блокировка области и снятие блокировки Операции блокировки и снятия блокировки для области выполня- ются независимо от операций выделения и освобождения области, по- добно тому, как операции блокирования-разблокирования индекса в файловой системе выполняются независимо от операций назначе- ния-освобождения индекса (алгоритмы iget и iput). Таким образом, ядро может заблокировать и выделить область, а потом снять блоки- ровку, не освобождая области. Точно также, когда ядру понадобится обратиться к выделенной области, оно сможет заблокировать об- ласть, чтобы запретить доступ к ней со стороны других процессов, и позднее снять блокировку. 6.5.2 Выделение области Ядро выделяет новую область (по алгоритму allocreg, Рисунок 6.18) во время выполнения системных функций fork, exec и shmget (получить разделяемую память). Ядро поддерживает таблицу облас- тей, записям которой соответствуют точки входа либо в списке сво- бодных областей, либо в списке активных областей. При выделении записи в таблице областей ядро выбирает из списка свободных об- ластей первую доступную запись, включает ее в список активных об- ластей, блокирует область и делает пометку о ее типе (разделяемая или частная). За некоторым исключением каждый процесс ассоцииру- ется с исполняемым файлом (после того, как была выполнена команда exec), и в алгоритме allocreg поле индекса в записи таблицы об- ластей устанавливается таким образом, чтобы оно указывало на ин- декс исполняемого файла. Индекс идентифицирует область для ядра, поэтому другие процессы могут при желании разделять область. Ядро увеличивает значение счетчика ссылок на индекс, чтобы помешать другим процессам удалять содержимое файла при выполнении функции unlink, об этом еще будет идти речь в разделе 7.5. Результатом алгоритма allocreg является назначение и блокировка области. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм allocreg /* разместить информационную структуру Ё Ё области */ Ё Ё входная информация: (1) указатель индекса Ё Ё (2) тип области Ё Ё выходная информация: заблокированная область Ё Ё { Ё Ё выбрать область из списка свободных областей; Ё Ё назначить области тип; Ё Ё присвоить значение указателю индекса; Ё Ё если (указатель индекса имеет ненулевое значение) Ё Ё увеличить значение счетчика ссылок на индекс; Ё Ё включить область в список активных областей; Ё Ё возвратить (заблокированную область); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.18. Алгоритм выделения области 6.5.3 Присоединение области к процессу Ядро присоединяет область к адресному пространству процесса во время выполнения системных функций fork, exec и shmat (алго- ритм attachreg, Рисунок 6.19). Область может быть вновь назначае- мой или уже существующей, которую процесс будет использовать сов- местно с другими процессами. Ядро выбирает свободную запись в частной таблице областей процесса, устанавливает в ней поле типа таким образом, чтобы оно указывало на область команд, данных, разделяемую память или область стека, и записывает виртуальный адрес, по которому область будет размещаться в адресном прост- ранстве процесса. Процесс не должен выходить за предел установ- ленного системой ограничения на максимальный виртуальный адрес, а виртуальные адреса новой области не должны пересекаться с адреса- ми существующих уже областей. Например, если система ограничила максимально-допустимое значение виртуального адреса процесса 8 мегабайтами, то привязать область размером 1 мегабайт к виртуаль- ному адресу 7.5M не удастся. Если же присоединение области допус- тимо, ядро увеличивает значение поля, описывающего размер области процесса в записи таблицы процессов, на величину присоединяемой области, а также увеличивает значение счетчика ссылок на область. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм attachreg /* присоединение области к процессу */ Ё Ё входная информация: (1) указатель на присоединяемую об- Ё Ё ласть (заблокированную) Ё Ё (2) процесс, к которому присоединяетсяЁ Ё область Ё Ё (3) виртуальный адрес внутри процесса,Ё Ё по которому будет присоединена об-Ё Ё ласть Ё Ё (4) тип области Ё Ё выходная информация: точка входа в частную таблицу областейЁ Ё процесса Ё Ё { Ё Ё выделить новую запись в частной таблице областей про- Ё Ё цесса; Ё Ё проинициализировать значения полей записи: Ё Ё установить указатель на присоединяемую область; Ё Ё установить тип области; Ё Ё установить виртуальный адрес области; Ё Ё проверить правильность указания виртуального адреса и Ё Ё размера области; Ё Ё увеличить значение счетчика ссылок на область; Ё Ё увеличить размер процесса с учетом присоединения облас-Ё Ё ти; Ё Ё записать начальные значения в новую группу аппаратных Ё Ё регистров; Ё Ё возвратить (точку входа в частную таблицу областей про-Ё Ё цесса); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.19. Алгоритм присоединения области Кроме того, в алгоритме attachreg устанавливаются начальные значения группы регистров управления памятью, выделенных процес- су. Если область ранее не присоединялась к какому-либо процессу, ядро с помощью функции growreg (см. следующий раздел) заводит для области новые таблицы страниц; в противном случае используются уже существующие таблицы страниц. Алгоритм завершает работу, возвращая указатель на точку входа в частную таблицу областей процесса, соответствующую вновь присоединенной области. Допустим, например, что ядру нужно подключить к процессу по виртуальному адресу 0 существующую (разделяемую) область, имеющую размер 7 Кбайт (Рисунок 6.20). Оно выделяет новую группу регистров управ- ления памятью и заносит в них адрес таблицы страниц области, вир- туальный адрес области в пространстве процесса (0) и размер таб- лицы страниц (9 записей). 6.5.4 Изменение размера области Процесс может расширять или сужать свое виртуальное адресное пространство с помощью функции sbrk. Точно так же и стек процесса расширяется автоматически (то есть для этого процессу не нужно явно обращаться к определенной функции) в соответствии с глубиной вложенности обращений к подпрограммам. Изменение размера области производится внутри ядра по алгоритму growreg (Рисунок 6.21). При расширении области ядро проверяет, не будут ли виртуальные адреса расширяемой области пересекаться с адресами какой-нибудь другой области и не повлечет ли расширение области за собой выход про- цесса за пределы максимально-допустимого виртуального пространс- тва памяти. Ядро никогда не использует алгоритм growreg для уве- личения размера разделяемой области, уже присоединенной к нескольким процессам; поэтому оно не беспокоится о том, не приве- дет ли увеличение размера области для одного процесса к превыше- Частная таблица областей процесса здддддддддбдддддддддддддбдддддддд© Ё Адрес Ё Виртуальный Ё Размер Ё Ё таблицы Ё адрес в про-Ё и Ё Ё страниц Ё странстве Ё защита Ё Ё Ё процесса Ё Ё цдддддддддедддддддддддддедддддддд╢ Точка входа Ё Ё 0 Ё 9 Ё для области юддддеддддадддддддддддддадддддддды команд юдддд© v зддддддддддддд© Ё пусто Ё цддддддддддддд╢ Ё пусто Ё цддддддддддддд╢ Ё 846K Ё цддддддддддддд╢ Ё 752K Ё цддддддддддддд╢ Ё 341K Ё цддддддддддддд╢ Ё 484K Ё цддддддддддддд╢ Ё 976K Ё цддддддддддддд╢ Ё 342K Ё цддддддддддддд╢ Ё 779K Ё юддддддддддддды Рисунок 6.20. Пример присоединения существующей области ко- манд нию другим процессом системного ограничения, накладываемого на размер процесса. При работе с существующей областью ядро исполь- зует алгоритм growreg в двух случаях: выполняя функцию sbrk по отношению к области данных процесса и реализуя автоматическое увеличение стека задачи. Обе эти области (данных и стека) частно- го типа. Области команд и разделяемой памяти после инициализации не могут расширяться. Этот момент будет пояснен в следующей гла- ве. Чтобы разместить расширенную память, ядро выделяет новые таб- лицы страниц (или расширяет существующие) или отводит дополни- тельную физическую память в тех системах, где не поддерживается подкачка страниц по обращению. При выделении дополнительной физи- ческой памяти ядро проверяет ее наличие перед выполнением алго- ритма growreg; если же памяти больше нет, ядро прибегает к другим средствам увеличения размера области (см. главу 9). Если процесс сокращает размер области, ядро просто освобождает память, отве- денную под область. Во всех этих случаях ядро переопределяет раз- меры процесса и области и переустанавливает значения полей записи частной таблицы областей процесса и регистров управления памятью (так, чтобы они согласовались с новым отображением памяти). Предположим, например, что область стека процесса начинается с виртуального адреса 128К и имеет размер 6 Кбайт и что ядру нуж- но расширить эту область на 1 Кбайт (1 страницу). Если размер процесса позволяет это делать и если виртуальные адреса в диапа- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм growreg /* изменение размера области */ Ё Ё входная информация: (1) указатель на точку входа в частнойЁ Ё таблице областей процесса Ё Ё (2) величина, на которую нужно изме- Ё Ё нить размер области (может быть Ё Ё как положительной, так и отрица- Ё Ё тельной) Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё если (размер области увеличивается) Ё Ё { Ё Ё проверить допустимость нового размера области; Ё Ё выделить вспомогательные таблицы (страниц); Ё Ё если (в системе не поддерживается замещение страниц Ё Ё по обращению) Ё Ё { Ё Ё выделить дополнительную память; Ё Ё проинициализировать при необходимости значения Ё Ё полей в дополнительных таблицах; Ё Ё } Ё Ё } Ё Ё в противном случае /* размер области уменьшается */ Ё Ё { Ё Ё освободить физическую память; Ё Ё освободить вспомогательные таблицы; Ё Ё } Ё Ё Ё Ё провести в случае необходимости инициализацию других Ё Ё вспомогательных таблиц; Ё Ё переустановить значение поля размера в таблице процес- Ё Ё сов; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.21. Алгоритм изменения размера области зоне от 134К до 135К - 1 не принадлежат какой-либо области, ранее присоединенной к процессу, ядро увеличивает размер стека. При этом ядро расширяет таблицу страниц, выделяет новую страницу па- мяти и инициализирует новую запись таблицы. Этот случай проил- люстрирован с помощью Рисунка 6.22. 6.5.5 Загрузка области В системе, где поддерживается подкачка страниц по обращению, ядро может "отображать" файл в адресное пространство процесса во время выполнения функции exec, подготавливая последующее чтение по запросу отдельных физических страниц (см. главу 9). Если же подкачка страниц по обращению не поддерживается, ядру приходится копировать исполняемый файл в память, загружая области процесса по указанным в файле виртуальным адресам. Ядро может присоединить область к разным виртуальным адресам, по которым будет загружать- ся содержимое файла, создавая таким образом "разрыв" в таблице страниц (вспомним Рисунок 6.20). Эта возможность может пригодить- ся, например, когда требуется проявлять ошибку памяти (memory fault) в случае обращения пользовательских программ к нулевому адресу (если последнее запрещено). Переменные указатели в прог- раммах иногда задаются неверно (отсутствует проверка их значений на равенство 0) и в результате не могут использоваться в качестве Частная таблица областей Частная таблица областей процесса процесса здддддддбддддддддддбдддддд© здддддддбддддддддддбдддддд© Ё Адрес Ё Виртуаль-Ё Раз- Ё Ё Адрес Ё Виртуаль-Ё Раз- Ё Ё табли-Ё ный адресЁ мер Ё Ё табли-Ё ный адресЁ мер Ё Ё цы Ё в прост- Ё и Ё Ё цы Ё в прост- Ё и Ё Ё стра- Ё ранстве Ё защи-Ё Ё стра- Ё ранстве Ё защи-Ё Ё ниц Ё процесса Ё та Ё Ё ниц Ё процесса Ё та Ё цдддддддеддддддддддедддддд╢ цдддддддеддддддддддедддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё цдддддддеддддддддддедддддд╢ цдддддддеддддддддддедддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё Точкацдддддддеддддддддддедддддд╢ Точкацдддддддеддддддддддедддддд╢ входаЁ Ё 128K Ё 6K Ё входаЁ Ё 128K Ё 7K Ё для юдддедддаддддддддддадддддды для юдддедддаддддддддддадддддды стека юдд© стека юдд© v v зддддддддддддд© зддддддддддддд© Ё 342K Ё Ё 342K Ё цддддддддддддд╢ цддддддддддддд╢ Ё 779K Ё Ё 779K Ё цддддддддддддд╢ цддддддддддддд╢ Ё 846K Ё Ё 846K Ё цддддддддддддд╢ цддддддддддддд╢ Ё 752K Ё Ё 752K Ё цддддддддддддд╢ цддддддддддддд╢ Ё 341K Ё Ё 341K Ё цддддддддддддд╢ цддддддддддддд╢ Ё 484K Ё Ё 484K Ё цддддддддддддд╢ НОВАЯ цддддддддддддд╢ Ё Ё СТРАНИЦАдд>Ё 976K Ё цддддддддддддд╢ цддддддддддддд╢ Ё Ё Ё Ё цддддддддддддд╢ цддддддддддддд╢ Ё Ё Ё Ё юддддддддддддды юддддддддддддды До увеличения стека После увеличения стека Рисунок 6.22. Увеличение области стека на 1 Кбайт указателей адресов. Если страницу с нулевым адресом соответствую- щим образом защитить, процессы, случайно обратившиеся к этому ад- ресу, натолкнутся на ошибку и будут аварийно завершены, и это ус- корит обнаружение подобных ошибок в программах. При загрузке файла в область алгоритм loadreg (Рисунок 6.23) проверяет разрыв между виртуальным адресом, по которому область присоединяется к процессу, и виртуальным адресом, с которого рас- полагаются данные области, и расширяет область в соответствии с требуемым объемом памяти. Затем область переводится в состояние "загрузки в память", при котором данные для области считываются из файла в память с помощью встроенной модификации алгоритма сис- темной функции read. Если ядро загружает область команд, которая может разделяться несколькими процессами, возможна ситуация, когда процесс попыта- ется воспользоваться областью до того, как ее содержимое будет полностью загружено, так как процесс загрузки может приостано- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм loadreg /* загрузка части файла в область */ Ё Ё входная информация: (1) указатель на точку входа в частнуюЁ Ё таблицу областей процесса Ё Ё (2) виртуальный адрес загрузки Ё Ё (3) указатель индекса файла Ё Ё (4) смещение в байтах до начала считы-Ё Ё ваемой части файла Ё Ё (5) объем загружаемых данных в байтах Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё увеличить размер области до требуемой величины (алгоритмЁ Ё growreg); Ё Ё записать статус области как "загружаемой в память"; Ё Ё снять блокировку с области; Ё Ё установить в пространстве процесса значения параметров Ё Ё чтения из файла: Ё Ё виртуальный адрес, по которому будут размещены счи-Ё Ё тываемые данные; Ё Ё смещение до начала считываемой части файла; Ё Ё объем данных, считываемых из файла, в байтах; Ё Ё загрузить файл в область (встроенная модификация алго- Ё Ё ритма read); Ё Ё заблокировать область; Ё Ё записать статус области как "полностью загруженной в па-Ё Ё мять"; Ё Ё возобновить выполнение всех процессов, ожидающих оконча-Ё Ё ния загрузки области; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.23. Алгоритм загрузки данных области из файла виться во время чтения файла. Подробно о том, как это происходит и почему при этом нельзя использовать блокировки, мы поговорим, когда будем вести речь о функции exec в следующей главе и в главе 9. Чтобы устранить эту проблему, ядро проверяет статус области и не разрешает к ней доступ до тех пор, пока загрузка области не будет закончена. По завершении реализации алгоритма loadreg ядро возобновляет выполнение всех процессов, ожидающих окончания заг- рузки области, и изменяет статус области ("готова, загружена в память"). Предположим, например, что ядру нужно загрузить текст разме- ром 7K в область, присоединенную к процессу по виртуальному адре- су 0, но при этом оставить промежуток размером 1 Кбайт от начала области (Рисунок 6.24). К этому времени ядро уже выделило запись в таблице областей и присоединило область по адресу 0 с помощью алгоритмов allocreg и attachreg. Теперь же ядро запускает алго- ритм loadreg, в котором действия алгоритма growreg выполняются дважды - во-первых, при выделении в начале области промежутка в 1 Кбайт, и во-вторых, при выделении места для содержимого области - и алгоритм growreg назначает для области таблицу страниц. Затем ядро заносит в соответствующие поля пространства процесса устано- вочные значения для чтения данных из файла: считываются 7 Кбайт, начиная с адреса, указанного в виде смещения внутри файла (пара- метр алгоритма), и записываются в виртуальное пространство про- цесса по адресу 1K. Частная таблица областей Частная таблица областей процесса процесса здддддддбддддддддддбдддддд© здддддддбддддддддддбдддддд© Ё Адрес Ё Виртуаль-Ё Раз- Ё Ё Адрес Ё Виртуаль-Ё Раз- Ё Ё табли-Ё ный адресЁ мер Ё Ё табли-Ё ный адресЁ мер Ё Ё цы Ё в прост- Ё и Ё Ё цы Ё в прост- Ё и Ё Ё стра- Ё ранстве Ё защи-Ё Ё стра- Ё ранстве Ё защи-Ё Ё ниц Ё процесса Ё та Ё Ё ниц Ё процесса Ё та Ё цдддддддеддддддддддедддддд╢ цдддддддеддддддддддедддддд╢ ТекстЁ ддд Ё Ё 0 Ё Ё Ё 0 Ё 8 Ё юдддддддаддддддддддадддддды юдддедддаддддддддддадддддды (а) Запись таблицы в перво- юдд© начальном виде Ё v зддддддддддддд© Ё пусто Ё Частная таблица областей цддддддддддддд╢ процесса Ё 779K Ё здддддддбддддддддддбдддддд© цддддддддддддд╢ Ё Адрес Ё Виртуаль-Ё Раз- Ё Ё 846K Ё Ё табли-Ё ный адресЁ мер Ё цддддддддддддд╢ Ё цы Ё в прост- Ё и Ё Ё 752K Ё Ё стра- Ё ранстве Ё защи-Ё цддддддддддддд╢ Ё ниц Ё процесса Ё та Ё Ё 341K Ё цдддддддеддддддддддедддддд╢ цддддддддддддд╢ Ё Ё 0 Ё 1 Ё Ё 484K Ё юдддедддаддддддддддадддддды цддддддддддддд╢ юдд© Ё 976K Ё Ё цддддддддддддд╢ v Ё 794K Ё зддддддддддддд© цддддддддддддд╢ Ё пусто Ё Ё Ё юддддддддддддды юддддддддддддды (б) Запись, указывающая на (в) После второго выполне- промежуток в начале об- ния алгоритма growreg ласти (после первого выполнения алгоритма growreg) Рисунок 6.24. Загрузка области команд (текста) 6.5.6 Освобождение области Если область не присоединена уже ни к какому процессу, она может быть освобождена ядром и возвращена в список свободных об- ластей (Рисунок 6.25). Если область связана с индексом, ядро ос- вобождает и индекс с помощью алгоритма iput, учитывая значение счетчика ссылок на индекс, установленное в алгоритме allocreg. Ядро освобождает все связанные с областью физические ресурсы, та- кие как таблицы страниц и собственно страницы физической памяти. Предположим, например, что ядру нужно освободить область стека, описанную на Рисунке 6.22. Если счетчик ссылок на область имеет нулевое значение, ядро освободит 7 страниц физической памяти вместе с таблицей страниц. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм freereg /* освобождение выделенной области */Ё Ё входная информация: указатель на (заблокированную) областьЁ Ё выходная информация: отсутствует Ё Ё { Ё Ё если (счетчик ссылок на область имеет ненулевое значе- Ё Ё ние) Ё Ё { Ё Ё /* область все еще используется одним из процессов */Ё Ё снять блокировку с области; Ё Ё если (область ассоциирована с индексом) Ё Ё снять блокировку с индекса; Ё Ё возвратить управление; Ё Ё } Ё Ё если (область ассоциирована с индексом) Ё Ё освободить индекс (алгоритм iput); Ё Ё освободить связанную с областью физическую память; Ё Ё освободить связанные с областью вспомогательные таблицы;Ё Ё очистить поля области; Ё Ё включить область в список свободных областей; Ё Ё снять блокировку с области; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.25. Алгоритм освобождения области здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм detachreg /* отсоединить область от процесса */ Ё Ё входная информация: указатель на точку входа в частной Ё Ё таблице областей процесса Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё обратиться к вспомогательным таблицам процесса, имеющим Ё Ё отношение к распределению памяти, Ё Ё освободить те из них, которые связаны с областью; Ё Ё уменьшить размер процесса; Ё Ё уменьшить значение счетчика ссылок на область; Ё Ё если (значение счетчика стало нулевым и область не явля-Ё Ё ется неотъемлемой частью процесса) Ё Ё освободить область (алгоритм freereg); Ё Ё в противном случае /* либо значение счетчика отлично Ё Ё от 0, либо область является не- Ё Ё отъемлемой частью процесса */ Ё Ё { Ё Ё снять блокировку с индекса (ассоциированного с об- Ё Ё ластью); Ё Ё снять блокировку с области; Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.26. Алгоритм отсоединения области 6.5.7 Отсоединение области от процесса Ядро отсоединяет области при выполнении системных функций exec, exit и shmdt (отсоединить разделяемую память). При этом яд- ро корректирует соответствующую запись и разъединяет связь с фи- зической памятью, делая недействительными связанные с областью регистры управления памятью (алгоритм detachreg, Рисунок 6.26). Механизм преобразования адресов после этого будет относиться уже к процессу, а не к области (как в алгоритме freereg). Ядро умень- шает значение счетчика ссылок на область и значение поля, описы- вающего размер процесса в записи таблицы процессов, в соответс- твии с размером области. Если значение счетчика становится равным 0 и если нет причины оставлять область без изменений (область не является областью разделяемой памяти или областью команд с приз- наками неотъемлемой части процесса, о чем будет идти речь в раз- деле 7.5), ядро освобождает область по алгоритму freereg. В про- тивном случае ядро снимает с индекса и с области блокировку, установленную для того, чтобы предотвратить конкуренцию между па- раллельно выполняющимися процессами (см. раздел 7.5), но оставля- ет область и ее ресурсы без изменений. Частные таблицы областей процессов Области здддддддддддддд© зддддддддддддд© Команды Ё цдддддддддддддд>Ё Разделяемая Ё цдддддддддддддд╢ зддддддд>юддддддддддддды Данные Ё цдддд© Ё цдддддддддддддд╢ Ё Ё зддддддддддддд© Стек Ё цдд© юдЁддддддд>Ё Частная цд© юдддддддддддддды Ё Ё юддддддддддддды Ё Копи- Процесс A Ё Ё Ё рова- Ё Ё зддддддддддддд© Ё ние юдддЁддддддд>Ё Частная цдЁд© дан- здддддддддддддд© Ё юддддддддддддды Ё Ё ных Команды Ё цдддддды Ё Ё цдддддддддддддд╢ зддддддддддддд© Ё Ё Данные Ё цдддддддддддддд>Ё Частная Ё<ы Ё цдддддддддддддд╢ юддддддддддддды Ё Стек Ё цдддддд© Ё юдддддддддддддды Ё зддддддддддддд© Ё Процесс B юддддддд>Ё Частная Ё<дды юддддддддддддды Рисунок 6.27. Копирование содержимого области здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм dupreg /* копирование содержимого существующей Ё Ё области */ Ё Ё входная информация: указатель на точку входа в таблице об-Ё Ё ластей Ё Ё выходная информация: указатель на область, являющуюся точ- Ё Ё ной копией существующей области Ё Ё { Ё Ё если (область разделяемая) Ё Ё /* в вызывающей программе счетчик ссылок на об- Ё Ё ласть будет увеличен, после чего будет испол- Ё Ё нен алгоритм attachreg */ Ё Ё возвратить (указатель на исходную область); Ё Ё выделить новую область (алгоритм allocreg); Ё Ё установить значения вспомогательных структур управленияЁ Ё памятью в точном соответствии со значениями существую-Ё Ё щих структур исходной области; Ё Ё выделить для содержимого области физическую память; Ё Ё "скопировать" содержимое исходной области во вновь соз-Ё Ё данную область; Ё Ё возвратить (указатель на выделенную область); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.28. Алгоритм копирования содержимого существующей области 6.5.8 Копирование содержимого области Системная функция fork требует, чтобы ядро скопировало содер- жимое областей процесса. Если же область разделяемая (разделяемый текст команд или разделяемая память), ядру нет надобности копиро- вать область физически; вместо этого оно увеличивает значение счетчика ссылок на область, позволяя родительскому и порожденному процессам использовать область совместно. Если область не являет- ся разделяемой и ядру нужно физически копировать ее содержимое, оно выделяет новую запись в таблице областей, новую таблицу стра- ниц и отводит под создаваемую область физическую память. В ка- честве примера рассмотрим Рисунок 6.27, где процесс A порождает с помощью функции fork процесс B и копирует области родительского процесса. Область команд процесса A является разделяемой, поэтому процесс B может использовать эту область совместно с процессом A. Однако области данных и стека родительского процесса являются его личной принадлежностью (имеют частный тип), поэтому процессу B нужно скопировать их содержимое во вновь выделенные области. При этом даже для областей частного типа физическое копирование об- ласти не всегда необходимо, в чем мы убедимся позже (глава 9). На Рисунке 6.28 приведен алгоритм копирования содержимого области (dupreg). 6.6 ПРИОСТАНОВКА ВЫПОЛНЕНИЯ К настоящему моменту мы рассмотрели все функции работы с внутренними структурами процесса, выполняющиеся на нижнем уровне взаимодействия с процессом и обеспечивающие переход в состояние "выполнения в режиме ядра" и выход из этого состояния в другие состояния, за исключением функций, переводящих процесс в состоя- ние "приостанова выполнения". Теперь перейдем к рассмотрению ал- горитмов, с помощью которых процесс переводится из состояния "выполнения в режиме ядра" в состояние "приостанова в памяти" и из состояния приостанова в состояния "готовности к запуску" с выгрузкой и без выгрузки из памяти. зддддддддддддддддддддддддддддддд© Ё Контекстный уровень ядра 2 Ё Ё Исполнить программу пере- Ё Ё ключения контекста Ё Ё Ё Ё Сохранить регистровый кон- Ё Ё текст обращения к системной Ё Ё функции Ё Запуск алгоритма приостанова Ыцддддддддддддддддддддддддддддддд╢ ^ Ё Контекстный уровень ядра 1 Ё Ё Ё Исполнить обращение к сис- Ё Ё Ё темной функции Ё Ё Ё Ё Ё Ё Сохранить регистровый кон- Ё Ё Ё текст пользовательского Ё Ё Ё уровня Ё Вызов системной функции ЫЫЫЫЫЫюддддддддддддддддддддддддддддддды ^ Ё Ё Ё Ё Ё Ё Исполнение в режиме задачи Рисунок 6.29. Стандартные контекстные уровни приостановленно- го процесса Выполнение процесса приостанавливается обычно во время испол- нения запрошенной им системной функции: процесс переходит в режим ядра (контекстный уровень 1), исполняя внутреннее прерывание опе- рационной системы, и приостанавливается в ожидании ресурсов. При этом процесс переключает контекст, запоминая в стеке свой текущий контекстный уровень и исполняясь далее в рамках системного кон- текстного уровня 2 (Рисунок 6.29). Выполнение процессов приоста- навливается также и в том случае, когда оно наталкивается на от- сутствие страницы в результате обращения к виртуальным адресам, не загруженным физически; процессы не будут выполняться, пока яд- ро не считает содержимое страниц. 6.6.1 События, вызывающие приостанов выполнения, и их адреса Как уже говорилось во второй главе, процессы приостанавлива- ются до наступления определенного события, после которого они "пробуждаются" и переходят в состояние "готовности к выполнению" (с выгрузкой и без выгрузки из памяти). Такого рода абстрактное рассуждение недалеко от истины, ибо в конкретном воплощении сово- купность событий отображается на совокупность виртуальных адресов (ядра). Адреса, с которыми связаны события, закодированы в ядре, и их единственное назначение состоит в их использовании в процес- процесс a ддд© зддд ожидание завершения ддд© Ё Ё ввода-вывода Ё процесс b д©зЁдддды Ё ЁЁЁ цдддд адрес A процесс c дЁыюдддддддд ожидание выделения Ё юдддд©зддд (освобождения) буфера дды процесс d дд© ЁЁзддыЁ Ё ЁЁЁздды процесс e ддЁдддЁыЁЁ ЁзддЁдыЁ процесс f ддЁы юддЁдд ожидание выделения ддддддддд адрес B Ё зддддЁд(освобождения) индекса процесс g ддЁды Ё зЁдддддды процесс h дыюддддддддд ожидание ввода с тер- дддддд адрес C минала Рисунок 6.30. Процессы, приостановленные до наступления собы- тий, и отображение событий на конкретные адреса се отображения ожидаемого события на конкретный адрес. Как для абстрактного рассмотрения, так и для конкретной реализации собы- тия безразлично, сколько процессов одновременно ожидают его нас- тупления. Как результат, возможно возникновение некоторых проти- воречий. Во-первых, когда событие наступает и процессы, ожидающие его, соответствующим образом оповещаются об этом, все они "про- буждаются" и переходят в состояние "готовности к выполнению". Яд- ро выводит процессы из состояния приостанова все сразу, а не по одному, несмотря на то, что они в принципе могут конкурировать за одну и ту же заблокированную структуру данных и большинство из них через небольшой промежуток времени опять вернется в состояние приостанова (более подробно об этом шла речь в главах 2 и 3). На Рисунке 6.30 изображены несколько процессов, приостановленных до наступления определенных событий. Еще одно противоречие связано с тем, что на один и тот же ад- рес могут отображаться несколько событий. На Рисунке 6.30, напри- мер, события "освобождение буфера" и "завершение ввода-вывода" отображаются на адрес буфера ("адрес A"). Когда ввод-вывод в бу- фер завершается, ядро возобновляет выполнение всех процессов, приостановленных в ожидании наступления как того, так и другого события. Поскольку процесс, ожидающий завершения ввода-вывода, удерживает буфер заблокированным, другие процессы, которые ждали освобождения буфера, вновь приостановятся, ибо буфер все еще за- нят. Функционирование системы было бы более эффективным, если бы отображение событий на адреса было однозначным. Однако на практи- ке такого рода противоречие на производительности системы не от- ражается, поскольку отображение на один адрес более одного собы- тия имеет место довольно редко, а также поскольку выполняющийся процесс обычно освобождает заблокированные ресурсы до того, как начнут выполняться другие процессы. Стилистически, тем не менее, механизм функционирования ядра стал бы более понятен, если бы отображение было однозначным. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм sleep Ё Ё входная информация: (1) адрес приостанова Ё Ё (2) приоритет Ё Ё выходная информация: 1, если процесс возобновляется по сиг-Ё Ё налу, который ему удалось уловить; Ё Ё вызов алгоритма longjump, если процессЁ Ё возобновляется по сигналу, который емуЁ Ё не удалось уловить; Ё Ё 0 - во всех остальных случаях; Ё Ё { Ё Ё поднять приоритет работы процессора таким образом, чтобыЁ Ё заблокировать все прерывания; Ё Ё перевести процесс в состояние приостанова; Ё Ё включить процесс в хеш-очередь приостановленных процес- Ё Ё сов, базирующуюся на адресах приостанова; Ё Ё сохранить адрес приостанова в таблице процессов; Ё Ё сделать ввод для процесса приоритетным; Ё Ё если (приостанов процесса НЕ допускает прерываний) Ё Ё { Ё Ё выполнить переключение контекста; Ё Ё /* с этого места процесс возобновляет выполнение, Ё Ё когда "пробуждается" */ Ё Ё снизить приоритет работы процессора так, чтобы вновь Ё Ё разрешить прерывания (как было до приостанова про- Ё Ё цесса); Ё Ё возвратить (0); Ё Ё } Ё Ё Ё Ё /* приостанов процесса принимает прерывания, вызванные Ё Ё сигналами */ Ё Ё если (к процессу не имеет отношения ни один из сигналов)Ё Ё { Ё Ё выполнить переключение контекста; Ё Ё /* с этого места процесс возобновляет выполнение, Ё Ё когда "пробуждается" */ Ё Ё если (к процессу не имеет отношения ни один из сигна-Ё Ё лов) Ё Ё { Ё Ё восстановить приоритет работы процессора таким, Ё Ё каким он был в момент приостанова процесса; Ё Ё возвратить (0); Ё Ё } Ё Ё } Ё Ё удалить процесс из хеш-очереди приостановленных процес- Ё Ё сов, если он все еще находится там; Ё Ё Ё Ё восстановить приоритет работы процессора таким, каким онЁ Ё был в момент приостанова процесса; Ё Ё если (приоритет приостановленного процесса позволяет Ё Ё принимать сигналы) Ё Ё возвратить (1); Ё Ё запустить алгоритм longjump; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.31. Алгоритм приостанова процесса 6.6.2 Алгоритмы приостанова и возобновления выполнения На Рисунке 6.31 приведен алгоритм приостанова процесса. Сна- чала ядро повышает приоритет работы процессора так, чтобы забло- кировать все прерывания, которые могли бы (путем создания конку- ренции) помешать работе с очередями приостановленных процессов, и запоминает старый приоритет, чтобы восстановить его, когда выпол- нение процесса будет возобновлено. Процесс получает пометку "при- остановленного", адрес приостанова и приоритет запоминаются в таблице процессов, а процесс помещается в хеш-очередь приостанов- ленных процессов. В простейшем случае (когда приостанов не допус- кает прерываний) процесс выполняет переключение контекста и бла- гополучно "засыпает". Когда приостановленный процесс "пробуждает- ся", ядро начинает планировать его запуск: процесс возвращает сохраненный в алгоритме sleep контекст, восстанавливает старый приоритет работы процессора (который был у него до начала выпол- нения алгоритма) и возвращает управление ядру. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм wakeup /* возобновление приостановленного про- Ё Ё цесса */ Ё Ё входная информация: адрес приостанова Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё повысить приоритет работы процессора таким образом, что-Ё Ё бы заблокировать все прерывания; Ё Ё найти хеш-очередь приостановленных процессов с указаннымЁ Ё адресом приостанова; Ё Ё для (каждого процесса, приостановленного по указанному Ё Ё адресу) Ё Ё { Ё Ё удалить процесс из хеш-очереди; Ё Ё сделать пометку о том, что процесс находится в состо-Ё Ё янии "готовности к запуску"; Ё Ё включить процесс в список процессов, готовых к запус-Ё Ё ку (для планировщика процессов); Ё Ё очистить поле, содержащее адрес приостанова, в записиЁ Ё таблицы процессов; Ё Ё если (процесс не загружен в память) Ё Ё возобновить выполнение программы подкачки (нуле-Ё Ё вой процесс); Ё Ё в противном случае Ё Ё если (возобновляемый процесс более подходит для ис- Ё Ё полнения, чем ныне выполняющийся) Ё Ё установить соответствующий флаг для планировщи- Ё Ё ка; Ё Ё } Ё Ё восстановить первоначальный приоритет работы процессора;Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 6.32. Алгоритм возобновления приостановленного про- цесса Чтобы возобновить выполнение приостановленных процессов, ядро обращается к алгоритму wakeup (Рисунок 6.32), причем делает это как во время исполнения алгоритмов реализации стандартных систем- ных функций, так и в случае обработки прерываний. Алгоритм iput, например, освобождает заблокированный индекс и возобновляет вы- полнение всех процессов, ожидающих снятия блокировки. Точно так же и программа обработки прерываний от диска возобновляет выпол- нение процессов, ожидающих завершения ввода-вывода. В алгоритме wakeup ядро сначала повышает приоритет работы процессора, чтобы заблокировать прерывания. Затем для каждого процесса, приостанов- ленного по указанному адресу, выполняются следующие действия: де- лается пометка в поле, описывающем состояние процесса, о том, что процесс готов к запуску; процесс удаляется из списка приостанов- ленных процессов и помещается в список процессов, готовых к за- пуску; поле в записи таблицы процессов, содержащее адрес приоста- нова, очищается. Если возобновляемый процесс не загружен в па- мять, ядро запускает процесс подкачки, обеспечивающий подкачку возобновляемого процесса в память (подразумевается система, в ко- торой подкачка страниц по обращению не поддерживается); в против- ном случае, если возобновляемый процесс более подходит для испол- нения, чем ныне выполняющийся, ядро устанавливает для планировщика специальный флаг, сообщающий о том, что процессу по возвращении в режим задачи следует пройти через алгоритм планиро- вания (глава 8). Наконец, ядро восстанавливает первоначальный приоритет работы процессора. При этом на ядро не оказывается ни- какого давления: "пробуждение" (wakeup) процесса не вызывает его немедленного исполнения; благодаря "пробуждению", процесс стано- вится только доступным для запуска. Все, о чем говорилось выше, касается простейшего случая вы- полнения алгоритмов sleep и wakeup, поскольку предполагается, что процесс приостанавливается до наступления соответствующего собы- тия. Во многих случаях процессы приостанавливаются в ожидании со- бытий, которые "должны" наступить, например, в ожидании освобож- дения ресурса (индексов или буферов) или в ожидании завершения ввода-вывода, связанного с диском. Уверенность процесса в немину- емом возобновлении основана на том, что подобные ресурсы могут быть предоставлены только во временное пользование. Тем не менее, иногда процесс может приостановиться в ожидании события, не буду- чи уверенным в неизбежном наступлении последнего, в таком случае у процесса должна быть возможность в любом случае вернуть себе управление и продолжить выполнение. В подобных ситуациях ядро немедленно нарушает "сон" приостановленного процесса, посылая ему сигнал. Более подробно о сигналах мы поговорим в следующей главе; здесь же примем допущение, что ядро может (выборочно) возобнов- лять приостановленные процессы по сигналу и что процесс может распознавать получаемые сигналы. Например, если процесс обратился к системной функции чтения с терминала, ядро не будет в состоянии выполнить запрос процесса до тех пор, пока пользователь не введет данные с клавиатуры термина- ла (глава 10). Тем не менее, пользователь, запустивший процесс, может оставить терминал на весь день, при этом процесс останется приостановленным в ожидании ввода, а терминал может понадобиться другому пользователю. Если другой пользователь прибегнет к реши- тельным мерам (таким как выключение терминала), ядро должно иметь возможность восстановить отключенный процесс: в качестве первого шага ядру следует возобновить приостановленный процесс по сигна- лу. В том, что процессы могут приостановиться на длительное вре- мя, нет ничего плохого. Приостановленный процесс занимает позицию в таблице процессов и может поэтому удлинять время поиска (ожида- ния) путем выполнения определенных алгоритмов, которые не занима- ют время центрального процессора и поэтому выполняются практичес- ки незаметно. Чтобы как-то различать между собой состояния приостанова, яд- ро устанавливает для приостанавливаемого процесса (при входе в это состояние) приоритет планирования на основании соответствую- щего параметра алгоритма sleep. То есть ядро запускает алгоритм sleep с параметром "приоритет", в котором отражается наличие уве- ренности в неизбежном наступлении ожидаемого события. Если прио- ритет превышает пороговое значение, процесс не будет преждевре- менно выходить из приостанова по получении сигнала, а будет про- должать ожидать наступления события. Если же значение приоритета ниже порогового, процесс будет немедленно возобновлен по получе- нии сигнала (****). ддддддддддддддддддддддддддддддддддддддд (****) Словами "выше" и "ниже" мы заменяем термины "высокий прио- ритет" и "низкий приоритет". Однако на практике приоритет может измеряться числами, более низкие значения которых подразумевают более высокий приоритет. Проверка того, имеет ли процесс уже сигнал при входе в алго- ритм sleep, позволяет выяснить, приостанавливался ли процесс ра- нее. Например, если значение приоритета в вызове алгоритма sleep превышает пороговое значение, процесс приостанавливается в ожида- нии выполнения алгоритма wakeup. Если же значение приоритета ниже порогового, выполнение процесса не приостанавливается, но на сиг- нал процесс реагирует точно так же, как если бы он был приоста- новлен. Если ядро не проверит наличие сигналов перед приостано- вом, возможна опасность, что сигнал больше не поступит вновь и в этом случае процесс никогда не возобновится. Когда процесс "пробуждается" по сигналу (или когда он не пе- реходит в состояние приостанова из-за наличия сигнала), ядро мо- жет выполнить алгоритм longjump (в зависимости от причины, по ко- торой процесс был приостановлен). С помощью алгоритма longjump ядро восстанавливает ранее сохраненный контекст, если нет возмож- ности завершить выполняемую системную функцию. Например, если из- за того, что пользователь отключил терминал, было прервано чтение данных с терминала, функция read не будет завершена, но возвратит признак ошибки. Это касается всех системных функций, которые мо- гут быть прерваны во время приостанова. После выхода из приоста- нова процесс не сможет нормально продолжаться, поскольку ожидае- мое событие не наступило. Перед выполнением большинства системных функций ядро сохраняет контекст процесса, используя алгоритм setjump и вызывая тем самым необходимость в последующем выполне- нии алгоритма longjump. Встречаются ситуации, когда ядро требует, чтобы процесс во- зобновился по получении сигнала, но не выполняет алгоритм longjump. Ядро запускает алгоритм sleep со специальным значением параметра "приоритет", подавляющим исполнение алгоритма longjump и заставляющим алгоритм sleep возвращать код, равный 1. Такая ме- ра более эффективна по сравнению с немедленным выполнением алго- ритма setjump перед вызовом sleep и последующим выполнением алго- ритма longjump для восстановления первоначального контекста процесса. Задача заключается в том, чтобы позволить ядру очищать локальные структуры данных. Драйвер устройства, например, может выделить свои частные структуры данных и приостановиться с прио- ритетом, допускающим прерывания; если по сигналу его работа во- зобновляется, он освобождает выделенные структуры, а затем выпол- няет алгоритм longjump, если необходимо. Пользователь не имеет возможности проконтролировать, выполняет ли процесс алгоритм longjump; выполнение этого алгоритма зависит от причины приоста- новки процесса, а также от того, требуют ли структуры данных ядра внесения изменений перед выходом из системной функции. 6.7 ВЫВОДЫ Мы завершили рассмотрение контекста процесса. Процессы в сис- теме UNIX могут находиться в различных логических состояниях и переходить из состояния в состояние в соответствии с установлен- ными правилами перехода, при этом информация о состоянии сохраня- ется в таблице процессов и в адресном пространстве процесса. Кон- текст процесса состоит из пользовательского контекста и системного контекста. Пользовательский контекст состоит из прог- рамм процесса, данных, стека задачи и областей разделяемой памя- ти, а системный контекст состоит из статической части (запись в таблице процессов, адресное пространство процесса и информация, необходимая для отображения адресного пространства) и динамичес- кой части (стек ядра и сохраненное состояние регистров предыдуще- го контекстного уровня системы), которые запоминаются в стеке и выбираются из стека при выполнении процессом обращений к систем- ным функциям, при обработке прерываний и при переключениях кон- текста. Пользовательский контекст процесса распадается на отдель- ные области, которые представляют собой непрерывные участки виртуального адресного пространства и трактуются как самостоя- тельные объекты использования и защиты. В модели управления па- мятью, которая использовалась при описании формата виртуального адресного пространства процесса, предполагалось наличие у каждой области процесса своей таблицы страниц. Ядро располагает целым набором различных алгоритмов для работы с областями. В заключи- тельной части главы были рассмотрены алгоритмы приостанова (sleep) и возобновления (wakeup) процессов. Структуры и алгорит- мы, описанные в данной главе, будут использоваться в последующих главах при рассмотрении системных функций управления процессами и планирования их выполнения, а также при объяснении различных ме- тодов распределения памяти. G6.8 УПРАЖНЕНИЯH 1. Составьте алгоритм преобразования виртуальных адресов в фи- зические, на входе которого задаются виртуальный адрес и ад- рес точки входа в частную таблицу областей. 2. В машинах AT&T 3B2 и NSC серии 32000 используется двухуров- невая схема трансляции виртуальных адресов в физические (с сегментацией). То есть в системе поддерживается указатель на таблицу страниц, каждая запись которой может адресовать фик- сированную часть адресного пространства процесса по смещению в таблице. Сравните алгоритм трансляции виртуальных адресов на этих машинах с алгоритмом, изложенным в тексте при обсуж- дении модели управления памятью. Подумайте над проблемами производительности и потребности в памяти для размещения вспомогательных таблиц. 3. В архитектуре системы VAX-11 поддерживаются два набора ре- гистров защиты памяти, используемых машиной в процессе трансляции пользовательских адресов. Механизм трансляции ис- пользуется тот же, что и в предыдущем пункте, за одним иск- лючением: указателей на таблицу страниц здесь два. Если про- цесс располагает тремя областями - команд, данных и стека - то каким образом, используя два набора регистров, следует производить отображение областей на таблицы страниц ? Увели- чение стека в архитектуре системы VAX-11 идет в направлении младших виртуальных адресов. Какой тогда вид имела бы об- ласть стека ? В главе 11 будет рассмотрена область разделяе- мой памяти: как она может быть реализована в архитектуре системы VAX-11 ? 4. Составьте алгоритм выделения и освобождения страниц памяти и таблиц страниц. Какие структуры данных следует использовать, чтобы достичь наивысшей производительности или наибольшей простоты реализации алгоритма ? 5. Устройство управления памятью MC68451 для семейства микро- процессоров Motorola 68000 допускает выделение сегментов па- мяти размером от 256 байт до 16 мегабайт. Каждое (физичес- кое) устройство управления памятью поддерживает 32 дескриптора сегментов. Опишите эффективный метод выделения памяти для этого случая. Каким образом осуществлялась бы ре- ализация областей ? 6. Рассмотрим отображение виртуальных адресов, представленное на Рисунке 6.5. Предположим, что ядро выгружает процесс (в системе с подкачкой процессов) или откачивает в область сте- ка большое количество страниц (в системе с замещением стра- ниц). Если через какое-то время процесс обратится к вирту- альному адресу 68432, будет ли он должен обратиться к соответствующей ячейке физической памяти, из которой он счи- тывал данные до того, как была выполнена операция выгрузки (откачки) ? Если нижние уровни системы управления памятью реализуются с использованием таблицы страниц, следует ли эти таблицы располагать в тех же, что и сами страницы, местах физической памяти ? *7. Можно реализовать систему, в которой стек ядра располагается над вершиной стека задачи. Подумайте о достоинствах и недос- татках подобной системы. 8. Каким образом, присоединяя область к процессу, ядро может проверить то, что эта область не накладывается на виртуаль- ные адреса областей, уже присоединенных к процессу ? 9. Обратимся к алгоритму переключения контекста. Допустим, что в системе готов к выполнению только один процесс. Другими словами, ядро выбирает для выполнения процесс с только что сохраненным контекстом. Объясните, что произойдет при этом. 10. Предположим, что процесс приостановился, но в системе нет процессов, готовых к выполнению. Что произойдет, когда при- остановившийся процесс переключит контекст ? 11. Предположим, что процесс, выполняемый в режиме задачи, из- расходовал выделенный ему квант времени и в результате пре- рывания по таймеру ядро выбирает для выполнения новый процесс. Объясните, почему переключение контекста произойдет на системном контекстном уровне 2. 12. В системе с замещением страниц процесс, выполняемый в режиме задачи, может столкнуться с отсутствием нужной страницы, ко- торая не была загружена в память. В ходе обработки прерыва- ния ядро считывает страницу из области подкачки и приоста- навливается. Объясните, почему переключение контекста (в момент приостанова) произойдет на системном контекстном уровне 2. 13. Процесс использует системную функцию read с форматом вызова read(fd,buf,1024); в системе с замещением страниц памяти. Предположим, что ядро исполняет алгоритм read для считывания данных в системный буфер, однако при попытке копирования данных в адресное пространство задачи сталкивается с отсутствием нужной стра- ницы, содержащей структуру buf, вследствие того, что она бы- ла ранее выгружена из памяти. Ядро обрабатывает возникшее прерывание, считывая отсутствующую страницу в память. Что происходит на каждом из системных контекстных уровней ? Что произойдет, если программа обработки прерывания приостано- вится в ожидании завершения считывания страницы ? 14. Что произошло бы, если бы во время копирования данных из ад- ресного пространства задачи в память ядра (Рисунок 6.17) об- наружилось, что указанный пользователем адрес неверен ? *15. При выполнении алгоритмов sleep и wakeup ядро повышает приоритет работы процессора так, чтобы не допустить прерыва- ний, препятствующих ей. Какие отрицательные последствия мог- ли бы возникнуть, если бы ядро не предпринимало этих дейс- твий ? (Намек: ядро зачастую возобновляет приостановленные процессы прямо из программ обработки прерываний). *16. Предположим, что процесс пытается приостановиться до наступ- ления события A, но, запуская алгоритм sleep, еще не забло- кировал прерывания; допустим, что в этот момент происходит прерывание и программа его обработки пытается возобновить все процессы, приостановленные до наступления события A. Что случится с первым процессом ? Не представляет ли эта ситуа- ция опасность ? Если да, то может ли ядро избежать ее воз- никновения ? 17. Что произойдет, если ядро запустит алгоритм wakeup для всех процессов, приостановленных по адресу A, в то время, когда по этому адресу не окажется ни одного приостановленного про- цесса ? 18. По одному адресу может приостановиться множество процессов, но ядру может потребоваться возобновление только некоторых из них - тех, которым будет послан соответствующий сигнал. С помощью механизма посылки сигналов можно идентифицировать отдельные процессы. Подумайте, какие изменения следует про- извести в алгоритме wakeup для того, чтобы можно было возоб- новлять выполнение только одного процесса, а не всех процес- сов, приостановленных по заданному адресу. 19. Обращения к алгоритмам sleep и wakeup в системе Multics имеют следующий синтаксис: sleep (событие); wakeup (событие, приоритет); Таким образом, в алгоритме wakeup возобновляемому процессу присваивается приоритет. Сравните форму вызова этих алгорит- мов с формой вызова соответствующих алгоритмов в системе UNIX. УПРАВЛЕНИЕ ПРОЦЕССОМ В предыдущей главе был рассмотрен контекст процесса и описаны алгоритмы для работы с ним; в данной главе речь пойдет об исполь- зовании и реализации системных функций, управляющих контекстом процесса. Системная функция fork создает новый процесс, функция exit завершает выполнение процесса, а wait дает возможность роди- тельскому процессу синхронизировать свое продолжение с завершени- ем порожденного процесса. Об асинхронных событиях процессы инфор- мируются при помощи сигналов. Поскольку ядро синхронизирует выполнение функций exit и wait при помощи сигналов, описание ме- ханизма сигналов предваряет собой рассмотрение функций exit и wait. Системная функция exec дает процессу возможность запускать "новую" программу, накладывая ее адресное пространство на испол- няемый образ файла. Системная функция brk позволяет динамически выделять дополнительную память; теми же самыми средствами ядро динамически наращивает стек задачи, выделяя в случае необходимос- ти дополнительное пространство. В заключительной части главы да- ется краткое описание основных групп операций командного процес- сора shell и начального процесса init. На Рисунке 7.1 показана взаимосвязь между системными функция- ми, рассматриваемыми в данной главе, с одной стороны, и алгорит- мами, описанными в предыдущей главе, с другой. Почти во всех функциях используются алгоритмы sleep и wakeup, отсутствующие на рисунке. Функция exec, кроме того, взаимодействует с алгоритмами работы с файловой системой, речь о которых шла в главах 4 и 5. здддддддддддддддддддддддддддддбдддддддддддддддддддддбдддддддддддд© Ё Системные функции, имеющие Ё Системные функции, Ё Функции Ё Ё ющие дело с управлением па- Ё связанные с синхро- Ё смешанного Ё Ё мятью Ё низацией Ё типа Ё цдддддддбдддддддбдддддддбдддддаддбддддбддддддбддддбдадддддбдддддд╢ Ё fork Ё exec Ё brk Ё exit ЁwaitЁsignalЁkillЁsetrgrpЁsetuidЁ цдддддддедддддддедддддддеддддддддеддддаддддддаддддадддддддадддддд╢ Ёdupreg Ёdetach-ЁgrowregЁ detach-Ё Ё Ёattach-Ё reg Ё Ё reg Ё Ё Ё reg Ёalloc- Ё Ё Ё Ё Ё Ё reg Ё Ё Ё Ё Ё Ёattach-Ё Ё Ё Ё Ё Ё reg Ё Ё Ё Ё Ё ЁgrowregЁ Ё Ё Ё Ё ЁloadregЁ Ё Ё Ё Ё Ёmapreg Ё Ё Ё Ё юдддддддадддддддадддддддаддддддддаддддддддддддддддддддддддддддддды Рисунок 7.1. Системные функции управления процессом и их связь с другими алгоритмами 7.1 СОЗДАНИЕ ПРОЦЕССА Единственным способом создания пользователем нового процесса в операционной системе UNIX является выполнение системной функции fork. Процесс, вызывающий функцию fork, называется родительским (процесс-родитель), вновь создаваемый процесс называется порож- денным (процесс-потомок). Синтаксис вызова функции fork: pid = fork(); В результате выполнения функции fork пользовательский контекст и того, и другого процессов совпадает во всем, кроме возвращаемого значения переменной pid. Для родительского процесса в pid возвра- щается идентификатор порожденного процесса, для порожденного - pid имеет нулевое значение. Нулевой процесс, возникающий внутри ядра при загрузке системы, является единственным процессом, не создаваемым с помощью функции fork. В ходе выполнения функции ядро производит следующую последо- вательность действий: 1. Отводит место в таблице процессов под новый процесс. 2. Присваивает порождаемому процессу уникальный код идентифика- ции. 3. Делает логическую копию контекста родительского процесса. Поскольку те или иные составляющие процесса, такие как об- ласть команд, могут разделяться другими процессами, ядро мо- жет иногда вместо копирования области в новый физический участок памяти просто увеличить значение счетчика ссылок на область. 4. Увеличивает значения счетчика числа файлов, связанных с про- цессом, как в таблице файлов, так и в таблице индексов. 5. Возвращает родительскому процессу код идентификации порожден- ного процесса, а порожденному процессу - нулевое значение. Реализацию системной функции fork, пожалуй, нельзя назвать тривиальной, так как порожденный процесс начинает свое выполне- ние, возникая как бы из воздуха. Алгоритм реализации функции для систем с замещением страниц по запросу и для систем с подкачкой процессов имеет лишь незначительные различия; все изложенное ниже в отношении этого алгоритма касается в первую очередь традицион- ных систем с подкачкой процессов, но с непременным акцентировани- ем внимания на тех моментах, которые в системах с замещением страниц по запросу реализуются иначе. Кроме того, конечно, пред- полагается, что в системе имеется свободная оперативная память, достаточная для размещения порожденного процесса. В главе 9 будет отдельно рассмотрен случай, когда для порожденного процесса не хватает памяти, и там же будут даны разъяснения относительно реа- лизации алгоритма fork в системах с замещением страниц. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм fork Ё Ё входная информация: отсутствует Ё Ё выходная информация: для родительского процесса - идентифи-Ё Ё катор (PID) порожденного процесса Ё Ё для порожденного процесса - 0 Ё Ё { Ё Ё проверить доступность ресурсов ядра; Ё Ё получить свободное место в таблице процессов и уникаль- Ё Ё ный код идентификации (PID); Ё Ё проверить, не запустил ли пользователь слишком много Ё Ё процессов; Ё Ё сделать пометку о том, что порождаемый процесс находитсяЁ Ё в состоянии "создания"; Ё Ё скопировать информацию в таблице процессов из записи, Ё Ё соответствующей родительскому процессу, в запись, соот-Ё Ё ветствующую порожденному процессу; Ё Ё увеличить значения счетчиков ссылок на текущий каталог иЁ Ё на корневой каталог (если он был изменен); Ё Ё увеличить значение счетчика открытий файла в таблице Ё Ё файлов; Ё Ё сделать копию контекста родительского процесса (адресноеЁ Ё пространство, команды, данные, стек) в памяти; Ё Ё поместить в стек фиктивный уровень системного контекста Ё Ё над уровнем системного контекста, соответствующим по- Ё Ё рожденному процессу; Ё Ё фиктивный контекстный уровень содержит информацию, Ё Ё необходимую порожденному процессу для того, чтобы Ё Ё знать все о себе и будучи выбранным для исполнения Ё Ё запускаться с этого места; Ё Ё если (в данный момент выполняется родительский процесс) Ё Ё { Ё Ё перевести порожденный процесс в состояние "готовностиЁ Ё к выполнению"; Ё Ё возвратить (идентификатор порожденного процесса); Ё Ё /* из системы пользователю */ Ё Ё } Ё Ё в противном случае /* выполняется порожденный Ё Ё процесс */ Ё Ё { Ё Ё записать начальные значения в поля синхронизации ад- Ё Ё ресного пространства процесса; Ё Ё возвратить (0); /* пользователю */ Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.2. Алгоритм fork На Рисунке 7.2 приведен алгоритм создания процесса. Сначала ядро должно удостовериться в том, что для успешного выполнения алгоритма fork есть все необходимые ресурсы. В системе с подкач- кой процессов для размещения порождаемого процесса требуется мес- то либо в памяти, либо на диске; в системе с замещением страниц следует выделить память для вспомогательных таблиц (в частности, таблиц страниц). Если свободных ресурсов нет, алгоритм fork за- вершается неудачно. Ядро ищет место в таблице процессов для конс- труирования контекста порождаемого процесса и проверяет, не пре- высил ли пользователь, выполняющий fork, ограничение на макси- мально-допустимое количество параллельно запущенных процессов. Ядро также подбирает для нового процесса уникальный идентифика- тор, значение которого превышает на единицу максимальный из су- ществующих идентификаторов. Если предлагаемый идентификатор уже присвоен другому процессу, ядро берет идентификатор, следующий по порядку. Как только будет достигнуто максимально-допустимое зна- чение, отсчет идентификаторов опять начнется с 0. Поскольку боль- шинство процессов имеет короткое время жизни, при переходе к на- чалу отсчета значительная часть идентификаторов оказывается сво- бодной. На количество одновременно выполняющихся процессов накладывается ограничение (конфигурируемое), отсюда ни один из пользователей не может занимать в таблице процессов слишком много места, мешая тем самым другим пользователям создавать новые про- цессы. Кроме того, простым пользователям не разрешается создавать процесс, занимающий последнее свободное место в таблице процес- сов, в противном случае система зашла бы в тупик. Другими слова- ми, поскольку в таблице процессов нет свободного места, то ядро не может гарантировать, что все существующие процессы завершатся естественным образом, поэтому новые процессы создаваться не бу- дут. С другой стороны, суперпользователю нужно дать возможность исполнять столько процессов, сколько ему потребуется, конечно, учитывая размер таблицы процессов, при этом процесс, исполняемый суперпользователем, может занять в таблице и последнее свободное место. Предполагается, что суперпользователь может прибегать к решительным мерам и запускать процесс, побуждающий остальные про- цессы к завершению, если это вызывается необходимостью (см. раз- дел 7.2.3, где говорится о системной функции kill). Затем ядро присваивает начальные значения различным полям за- писи таблицы процессов, соответствующей порожденному процессу, копируя в них значения полей из записи родительского процесса. Например, порожденный процесс "наследует" у родительского процес- са коды идентификации пользователя (реальный и тот, под которым исполняется процесс), группу процессов, управляемую родительским процессом, а также значение, заданное родительским процессом в функции nice и используемое при вычислении приоритета планирова- ния. В следующих разделах мы поговорим о назначении этих полей. Ядро передает значение поля идентификатора родительского процесса в запись порожденного, включая последний в древовидную структуру процессов, и присваивает начальные значения различным параметрам планирования, таким как приоритет планирования, использование ре- сурсов центрального процессора и другие значения полей синхрони- зации. Начальным состоянием процесса является состояние "созда- ния" (см. Рисунок 6.1). После того ядро устанавливает значения счетчиков ссылок на файлы, с которыми автоматически связывается порождаемый процесс. Во-первых, порожденный процесс размещается в текущем каталоге ро- дительского процесса. Число процессов, обращающихся в данный мо- мент к каталогу, увеличивается на 1 и, соответственно, увеличива- ется значение счетчика ссылок на его индекс. Во-вторых, если родительский процесс или один из его предков уже выполнял смену корневого каталога с помощью функции chroot, порожденный процесс наследует и новый корень с соответствующим увеличением значения счетчика ссылок на индекс корня. Наконец, ядро просматривает таб- лицу пользовательских дескрипторов для родительского процесса в поисках открытых файлов, известных процессу, и увеличивает значе- ние счетчика ссылок, ассоциированного с каждым из открытых фай- лов, в глобальной таблице файлов. Порожденный процесс не просто наследует права доступа к открытым файлам, но и разделяет доступ к файлам с родительским процессом, так как оба процесса обращают- ся в таблице файлов к одним и тем же записям. Действие fork в от- ношении открытых файлов подобно действию алгоритма dup: новая за- пись в таблице пользовательских дескрипторов файла указывает на запись в глобальной таблице файлов, соответствующую открытому файлу. Для dup, однако, записи в таблице пользовательских деск- рипторов файла относятся к одному процессу; для fork - к разным процессам. После завершения всех этих действий ядро готово к созданию для порожденного процесса пользовательского контекста. Ядро выде- ляет память для адресного пространства процесса, его областей и таблиц страниц, создает с помощью алгоритма dupreg копии всех об- ластей родительского процесса и присоединяет с помощью алгоритма attachreg каждую область к порожденному процессу. В системе с подкачкой процессов ядро копирует содержимое областей, не являю- щихся областями разделяемой памяти, в новую зону оперативной па- мяти. Вспомним из раздела 6.2.4 о том, что в пространстве процес- са хранится указатель на соответствующую запись в таблице процес- сов. За исключением этого поля, во всем остальном содержимое ад- ресного пространства порожденного процесса в начале совпадает с содержимым пространства родительского процесса, но может расхо- диться после завершения алгоритма fork. Родительский процесс, например, после выполнения fork может открыть новый файл, к кото- рому порожденный процесс уже не получит доступ автоматически. Итак, ядро завершило создание статической части контекста по- рожденного процесса; теперь оно приступает к созданию динамичес- кой части. Ядро копирует в нее первый контекстный уровень роди- тельского процесса, включающий в себя сохраненный регистровый контекст задачи и стек ядра в момент вызова функции fork. Если в данной реализации стек ядра является частью пространства процес- са, ядро в момент создания пространства порожденного процесса ав- томатически создает и системный стек для него. В противном случае родительскому процессу придется скопировать в пространство памя- ти, ассоциированное с порожденным процессом, свой системный стек. В любом случае стек ядра для порожденного процесса совпадает с системным стеком его родителя. Далее ядро создает для порожденно- го процесса фиктивный контекстный уровень (2), в котором содер- жится сохраненный регистровый контекст из первого контекстного уровня. Значения счетчика команд (регистр PC) и других регистров, сохраняемые в регистровом контексте, устанавливаются таким обра- зом, чтобы с их помощью можно было "восстанавливать" контекст по- рожденного процесса, пусть даже последний еще ни разу не испол- нялся, и чтобы этот процесс при запуске всегда помнил о том, что он порожденный. Например, если программа ядра проверяет значение, хранящееся в регистре 0, для того, чтобы выяснить, является ли данный процесс родительским или же порожденным, то это значение переписывается в регистровый контекст порожденного процесса, сох- раненный в составе первого уровня. Механизм сохранения использу- ется тот же, что и при переключении контекста (см. предыдущую главу). Если контекст порожденного процесса готов, родительский про- цесс завершает свою роль в выполнении алгоритма fork, переводя порожденный процесс в состояние "готовности к запуску, находясь в памяти" и возвращая пользователю его идентификатор. Затем, ис- пользуя обычный алгоритм планирования, ядро выбирает порожденный процесс для исполнения и тот "доигрывает" свою роль в алгоритме fork. Контекст порожденного процесса был задан родительским про- цессом; с точки зрения ядра кажется, что порожденный процесс во- зобновляется после приостанова в ожидании ресурса. Порожденный процесс при выполнении функции fork реализует ту часть программы, на которую указывает счетчик команд, восстанавливаемый ядром из сохраненного на уровне 2 регистрового контекста, и по выходе из функции возвращает нулевое значение. На Рисунке 7.3 представлена логическая схема взаимодействия родительского и порожденного процессов с другими структурами дан- ных ядра сразу после завершения системной функции fork. Итак, оба процесса совместно пользуются файлами, которые были открыты роди- тельским процессом к моменту исполнения функции fork, при этом значение счетчика ссылок на каждый из этих файлов в таблице фай- лов на единицу больше, чем до вызова функции. Порожденный процесс имеет те же, что и родительский процесс, текущий и корневой ката- логи, значение же счетчика ссылок на индекс каждого из этих ката- логов так же становится на единицу больше, чем до вызова функции. Содержимое областей команд, данных и стека (задачи) у обоих про- цессов совпадает; по типу области и версии системной реализации можно установить, могут ли процессы разделять саму область команд в физических адресах. Рассмотрим приведенную на Рисунке 7.4 программу, которая представляет собой пример разделения доступа к файлу при исполне- нии функции fork. Пользователю следует передавать этой программе Родительский процесс зддддддддддддддддддддддддддддддддддддддддддддд© Таблица Ё зддддддддд© Частная Адресное простран- Ё файлов Ё Ё Область Ё таблица ство процесса Ё зддддддддд© Ё Ё данных Ё областей здддддддддддддддддд©Ё Ё Ы Ё Ё юддддддддды процесса Ё Открытые файлы дЫЁЁЫд © Ё Ы Ё Ё Ё здддддд© Ё ЁЁ Ы Ё Ы Ё Ё Ыд д д © Ё ц ©Ё Текущий каталог дЁЁ© Ё цддддддддд╢ Ё Ы цдддддд╢ ЫЁ ЁЁЫ Ыд дЁ Ё Ё зддддддддд© ю ╢ Ё ЁЁ Измененный кореньЁЁЁ Ё Ё Ё Ё Ё Стек Ё цдддддд╢ ЫюддддддддддддддддддыЁЫ Ы цддддддддд╢ Ё Ё задачи ц д ╢ Ё Ёздддддддддддддддддд©ЁЁ Ё Ё Ы Ё Ё юддддддддды юдддддды ЫЁ Ы ЁЁЫ Ы Ё Ы Ё Ё ЁЁ Ы ЁЁЁ Ё Ё Ы Ё Ё ЫЁ Ы ЁЁЫ Ы цддддддддд╢ Ё Ыд д д д д д д д дыЁ Стек ядра ЁЁЁ ц д ╢ Ё Ё Ё юддддддддддддддддддыЁЫ Ы Ё Ё юдддддддддддддддддддддддддддддддддддддддддддддыЁ Ё цддддддддд╢ Ы Ы Ы Ё Ы Ё зддддадддд© Ё Ё Ё Ы Ё ЁРазделяе-Ё Ы Ы Ё Ы Ё Ё мая Ё Ё Ё цддддддддд╢ Ё область Ё Ы Ыд дЁ Ё Ё команд Ё Ё Ё Ё Ё юддддбдддды Ы Ы цддддддддд╢ Ы Ё Ё юддддддддды ю д д д д д д д д © Ы Ы зддддддддддддддддддддддддддддддддддддддддддддд©ю дЁ© Таблица Ё зддддддддд© Частная Ё Адресное простран- Ё ЫЫ файлов Ё Ё Область Ё таблица Ы ство процесса Ё ЁЁ зддддддддд© Ё Ё данных Ё областей Ёздддддддддддддддддд©Ё ЫЫ Ё Ы Ё Ё юддддддддды процесса ЫЁ Открытые файлы дЫЁЁЫд ыЁ Ё Ы Ё Ё Ё здддддд© ЁЁ ЁЁ Ы Ё Ы Ё Ё Ыд д д © Ё цдЫЁ Текущий каталог дЁЁ© Ё цддддддддд╢ Ё Ы цдддддд╢ Ё ЁЁЫ Ыд ╢ Ё Ё зддддддддд© ю ╢ Ё Ё Измененный кореньЁЁю д д дЁ Ё Ё Ё Стек Ё цдддддд╢ юддддддддддддддддддыЁ цддддддддд╢ Ё Ё задачи ц д ╢ Ё здддддддддддддддддд©Ё Ё Ы Ё Ё юддддддддды юдддддды Ё Ы ЁЁ Ё Ы Ё Ё Ё Ы ЁЁ Ё Ы Ё Ё Ё Ы ЁЁ цддддддддд╢ Ё Ё Стек ядра ЁЁ Ё Ё Ё юддддддддддддддддддыЁ Ё Ё юддддддддддддддддддддддддддддддддддддддддддддды цддддддддд╢ Порожденный процесс Ё Ы Ё Ё Ы Ё Ё Ы Ё юддддддддды Рисунок 7.3. Создание контекста нового процесса при выполне- нии функции fork два параметра - имя существующего файла и имя создаваемого файла. Процесс открывает существующий файл, создает новый файл и - при условии отсутствия ошибок - порождает новый процесс. Внутри прог- раммы ядро делает копию контекста родительского процесса для по- рожденного, при этом родительский процесс исполняется в одном ад- ресном пространстве, а порожденный - в другом. Каждый из процес- сов может работать со своими собственными копиями глобальных пе- ременных fdrd, fdwt и c, а также со своими собственными копиями стековых переменных argc и argv, но ни один из них не может обра- щаться к переменным другого процесса. Тем не менее, при выполне- нии функции fork ядро делает копию адресного пространства первого процесса для второго, и порожденный процесс, таким образом, нас- ледует доступ к файлам родительского (то есть к файлам, им ранее открытым и созданным) с правом использования тех же самых деск- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё int fdrd, fdwt; Ё Ё char c; Ё Ё Ё Ё main(argc, argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё if (argc != 3) Ё Ё exit(1); Ё Ё if ((fdrd = open(argv[1],O_RDONLY)) == -1) Ё Ё exit(1); Ё Ё if ((fdwt = creat(argv[2],0666)) == -1) Ё Ё exit(1); Ё Ё Ё Ё fork(); Ё Ё /* оба процесса исполняют одну и ту же программу */ Ё Ё rdwrt(); Ё Ё exit(0); Ё Ё } Ё Ё Ё Ё rdwrt(); Ё Ё { Ё Ё for(;;) Ё Ё { Ё Ё if (read(fdrd,&c,1) != 1) Ё Ё return; Ё Ё write(fdwt,&c,1); Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.4. Программа, в которой родительский и порожденный процессы разделяют доступ к файлу рипторов. Родительский и порожденный процессы независимо друг от друга, конечно, вызывают функцию rdwrt и в цикле считывают по одному байту информацию из исходного файла и переписывают ее в файл вы- вода. Функция rdwrt возвращает управление, когда при считывании обнаруживается конец файла. Ядро перед тем уже увеличило значения счетчиков ссылок на исходный и результирующий файлы в таблице файлов, и дескрипторы, используемые в обоих процессах, адресуют к одним и тем же строкам в таблице. Таким образом, дескрипторы fdrd в том и в другом процессах указывают на запись в таблице файлов, соответствующую исходному файлу, а дескрипторы, подставляемые в качестве fdwt, - на запись, соответствующую результирующему файлу (файлу вывода). Поэтому оба процесса никогда не обратятся вместе на чтение или запись к одному и тому же адресу, вычисляемому с помощью смещения внутри файла, поскольку ядро смещает внутрифай- ловые указатели после каждой операции чтения или записи. Несмотря на то, что, казалось бы, из-за того, что процессы распределяют между собой рабочую нагрузку, они копируют исходный файл в два раза быстрее, содержимое результирующего файла зависит от очеред- ности, в которой ядро запускает процессы. Если ядро запускает процессы так, что они исполняют системные функции попеременно (чередуя и спаренные вызовы функций read-write), содержимое ре- зультирующего файла будет совпадать с содержимым исходного файла. Рассмотрим, однако, случай, когда процессы собираются считать из исходного файла последовательность из двух символов "ab". Предпо- ложим, что родительский процесс считал символ "a", но не успел записать его, так как ядро переключилось на контекст порожденного процесса. Если порожденный процесс считывает символ "b" и записы- вает его в результирующий файл до возобновления родительского процесса, строка "ab" в результирующем файле будет иметь вид "ba". Ядро не гарантирует согласование темпов выполнения процес- сов. Теперь перейдем к программе, представленной на Рисунке 7.5, в которой процесс-потомок наследует от своего родителя файловые дескрипторы 0 и 1 (соответствующие стандартному вводу и стандарт- ному выводу). При каждом выполнении системной функции pipe произ- водится назначение двух файловых дескрипторов в массивах to_par и to_chil. Процесс вызывает функцию fork и делает копию своего кон- текста: каждый из процессов имеет доступ только к своим собствен- ным данным, так же как и в предыдущем примере. Родительский про- цесс закрывает файл стандартного вывода (дескриптор 1) и дублиру- ет дескриптор записи, возвращаемый в канал to_chil. Поскольку первое свободное место в таблице дескрипторов родительского про- цесса образовалось в результате только что выполненной операции закрытия (close) файла вывода, ядро переписывает туда дескриптор записи в канал и этот дескриптор становится дескриптором файла стандартного вывода для to_chil. Те же самые действия родитель- ский процесс выполняет в отношении дескриптора файла стандартного ввода, заменяя его дескриптором чтения из канала to_par. И порож- денный процесс закрывает файл стандартного ввода (дескриптор 0) и так же дублирует дескриптор чтения из канала to_chil. Поскольку первое свободное место в таблице дескрипторов файлов прежде было занято файлом стандартного ввода, его дескриптором становится дескриптор чтения из канала to_chil. Аналогичные действия выпол- няются и в отношении дескриптора файла стандартного вывода, заме- няя его дескриптором записи в канал to_par. И тот, и другой про- цессы закрывают файлы, дескрипторы которых возвратила функция pipe - хорошая традиция, в чем нам еще предстоит убедиться. В ре- зультате, когда родительский процесс переписывает данные в стан- дартный вывод, запись ведется в канал to_chil и данные поступают к порожденному процессу, который считывает их через свой стан- дартный ввод. Когда же порожденный процесс пишет данные в стан- дартный вывод, запись ведется в канал to_par и данные поступают к родительскому процессу, считывающему их через свой стандартный ввод. Так через два канала оба процесса обмениваются сообщениями. Результаты этой программы не зависят от того, в какой очеред- ности процессы выполняют свои действия. Таким образом, нет ника- кой разницы, возвращается ли управление родительскому процессу из функции fork раньше или позже, чем порожденному процессу. И так же безразличен порядок, в котором процессы вызывают системные функции перед тем, как войти в свой собственный цикл, ибо они ис- пользуют идентичные структуры ядра. Если процесс-потомок исполня- ет функцию read раньше, чем его родитель выполнит write, он будет приостановлен до тех пор, пока родительский процесс не произведет запись в канал и тем самым не возобновит выполнение потомка. Если родительский процесс записывает в канал до того, как его потомок приступит к чтению из канала, первый процесс не сможет в свою очередь считать данные из стандартного ввода, пока второй процесс не прочитает все из своего стандартного ввода и не произведет за- пись данных в стандартный вывод. С этого места порядок работы жестко фиксирован: каждый процесс завершает выполнение функций read и write и не может выполнить следующую операцию read до тех пор, пока другой процесс не выполнит пару read-write. Родитель- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё char string[] = "hello world"; Ё Ё main() Ё Ё { Ё Ё int count,i; Ё Ё int to_par[2],to_chil[2]; /* для каналов родителя и Ё Ё потомка */ Ё Ё char buf[256]; Ё Ё pipe(to_par); Ё Ё pipe(to_chil); Ё Ё if (fork() == 0) Ё Ё { Ё Ё /* выполнение порожденного процесса */ Ё Ё close(0); /* закрытие прежнего стандартного ввода */ Ё Ё dup(to_chil[0]); /* дублирование дескриптора чтения Ё Ё из канала в позицию стандартного Ё Ё ввода */ Ё Ё close(1); /* закрытие прежнего стандартного вывода */Ё Ё dup(to_par[0]); /* дублирование дескриптора записи Ё Ё в канал в позицию стандартного Ё Ё вывода */ Ё Ё close(to_par[1]); /* закрытие ненужных дескрипторов Ё Ё close(to_chil[0]); канала */ Ё Ё close(to_par[0]); Ё Ё close(to_chil[1]); Ё Ё for (;;) Ё Ё { Ё Ё if ((count = read(0,buf,sizeof(buf))) == 0) Ё Ё exit(); Ё Ё write(1,buf,count); Ё Ё } Ё Ё } Ё Ё /* выполнение родительского процесса */ Ё Ё close(1); /* перенастройка стандартного ввода-вывода */Ё Ё dup(to_chil[1]); Ё Ё close(0); Ё Ё dup(to_par[0]); Ё Ё close(to_chil[1]); Ё Ё close(to_par[0]); Ё Ё close(to_chil[0]); Ё Ё close(to_par[1]); Ё Ё for (i = 0; i < 15; i++) Ё Ё { Ё Ё write(1,string,strlen(string)); Ё Ё read(0,buf,sizeof(buf)); Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.5. Использование функций pipe, dup и fork ский процесс после 15 итераций завершает работу; порожденный про- цесс наталкивается на конец файла ("end-of-file"), поскольку ка- нал не связан больше ни с одним из записывающих процессов, и тоже завершает работу. Если порожденный процесс попытается произвести запись в канал после завершения родительского процесса, он полу- чит сигнал о том, что канал не связан ни с одним из процессов чтения. Мы упомянули о том, что хорошей традицией в программировании является закрытие ненужных файловых дескрипторов. В пользу этого говорят три довода. Во-первых, дескрипторы файлов постоянно нахо- дятся под контролем системы, которая накладывает ограничение на их количество. Во-вторых, во время исполнения порожденного про- цесса присвоение дескрипторов в новом контексте сохраняется (в чем мы еще убедимся). Закрытие ненужных файлов до запуска процес- са открывает перед программами возможность исполнения в "стериль- ных" условиях, свободных от любых неожиданностей, имея открытыми только файлы стандартного ввода-вывода и ошибок. Наконец, функция read для канала возвращает признак конца файла только в том слу- чае, если канал не был открыт для записи ни одним из процессов. Если считывающий процесс будет держать дескриптор записи в канал открытым, он никогда не узнает, закрыл ли записывающий процесс работу на своем конце канала или нет. Вышеприведенная программа не работала бы надлежащим образом, если бы перед входом в цикл выполнения процессом-потомком не были закрыты дескрипторы записи в канал. 7.2 СИГНАЛЫ Сигналы сообщают процессам о возникновении асинхронных собы- тий. Посылка сигналов производится процессами - друг другу, с по- мощью функции kill, - или ядром. В версии V (вторая редакция) системы UNIX существуют 19 различных сигналов, которые можно классифицировать следующим образом: * Сигналы, посылаемые в случае завершения выполнения процесса, то есть тогда, когда процесс выполняет функцию exit или функ- цию signal с параметром death of child (гибель потомка); * Сигналы, посылаемые в случае возникновения вызываемых процес- сом особых ситуаций, таких как обращение к адресу, находяще- муся за пределами виртуального адресного пространства процес- са, или попытка записи в область памяти, открытую только для чтения (например, текст программы), или попытка исполнения привилегированной команды, а также различные аппаратные ошиб- ки; * Сигналы, посылаемые во время выполнения системной функции при возникновении неисправимых ошибок, таких как исчерпание сис- темных ресурсов во время выполнения функции exec после осво- бождения исходного адресного пространства (см. раздел 7.5); * Сигналы, причиной которых служит возникновение во время вы- полнения системной функции совершенно неожиданных ошибок, таких как обращение к несуществующей системной функции (про- цесс передал номер системной функции, который не соответству- ет ни одной из имеющихся функций), запись в канал, не связанный ни с одним из процессов чтения, а также использова- ние недопустимого значения в параметре "reference" системной функции lseek. Казалось бы, более логично в таких случаях вместо посылки сигнала возвращать код ошибки, однако с прак- тической точки зрения для аварийного завершения процессов, в которых возникают подобные ошибки, более предпочтительным яв- ляется именно использование сигналов (*); ддддддддддддддддддддддддддддддддддддддд (*) Использование сигналов в некоторых обстоятельствах позволяет обнаружить ошибки при выполнении программ, не проверяющих код завершения вызываемых системных функций (сообщил Д.Ричи). * Сигналы, посылаемые процессу, который выполняется в режиме задачи, например, сигнал тревоги (alarm), посылаемый по исте- чении определенного периода времени, или произвольные сигна- лы, которыми обмениваются процессы, использующие функцию kill; * Сигналы, связанные с терминальным взаимодействием, например, с "зависанием" терминала (когда сигнал-носитель на терминаль- ной линии прекращается по любой причине) или с нажатием кла- виш "break" и "delete" на клавиатуре терминала; * Сигналы, с помощью которых производится трассировка выполне- ния процесса. Условия применения сигналов каждой группы будут рассмотрены в этой и последующих главах. Концепция сигналов имеет несколько аспектов, связанных с тем, каким образом ядро посылает сигнал процессу, каким образом про- цесс обрабатывает сигнал и управляет реакцией на него. Посылая сигнал процессу, ядро устанавливает в единицу разряд в поле сиг- нала записи таблицы процессов, соответствующий типу сигнала. Если процесс находится в состоянии приостанова с приоритетом, допуска- ющим прерывания, ядро возобновит его выполнение. На этом роль от- правителя сигнала (процесса или ядра) исчерпывается. Процесс мо- жет запоминать сигналы различных типов, но не имеет возможности запоминать количество получаемых сигналов каждого типа. Например, если процесс получает сигнал о "зависании" или об удалении про- цесса из системы, он устанавливает в единицу соответствующие раз- ряды в поле сигналов таблицы процессов, но не может сказать, сколько экземпляров сигнала каждого типа он получил. Ядро проверяет получение сигнала, когда процесс собирается перейти из режима ядра в режим задачи, а также когда он переходит в состояние приостанова или выходит из этого состояния с доста- точно низким приоритетом планирования (см. Рисунок 7.6). Ядро об- рабатывает сигналы только тогда, когда процесс возвращается из режима ядра в режим задачи. Таким образом, сигнал не оказывает немедленного воздействия на поведение процесса, исполняемого в режиме ядра. Если процесс исполняется в режиме задачи, а ядро тем временем обрабатывает прерывание, послужившее поводом для посылки процессу сигнала, ядро распознает и обработает сигнал по выходе из прерывания. Таким образом, процесс не будет исполняться в ре- жиме задачи, пока какие-то сигналы остаются необработанными. На Рисунке 7.7 представлен алгоритм, с помощью которого ядро определяет, получил ли процесс сигнал или нет. Условия, в которых формируются сигналы типа "гибель потомка", будут рассмотрены поз- же. Мы также увидим, что процесс может игнорировать отдельные сигналы, если воспользуется функцией signal. В алгоритме issig ядро просто гасит индикацию тех сигналов, на которые процесс не желает обращать внимание, и привлекает внимание процесса ко всем остальным сигналам. Выполняется в режиме задачи зддддддд© Ё Ё Проверка Ё 1 Ё и Вызов функ- Ё Ё з д обработка ции, преры- юбдддддды Ыз д сигналов вание Ё ^ ^д дыЫ Преры- зддддд© зддддддды Ёд дЁд д ы вание, Ё Ё Ё зддддддды юддд© Возврат в возвратЁ Ё Ё Ё Возврат Ё режим задачи из пре-Ё Ё Ё Ё Ё рыва-Ё v v Ё Выполняет- Ё зддддддд© ния Ё здддддда©ся в режи- задддддд© Ё Ё юдд>Ё Ёме ядра Ё Ё Ё 9 Ё<ддддддддддд╢ 2 цдддддддддддд>Ё 7 Ё Ё Ё Выход Ё Ё Резервирует-Ё Ё юддддддды юбдддддды ся юддддддды Прекращение Ё ^ Ы Зарезер- существования Ёд д дЁд д д д д д д д © Ы вирован Ё Ёд д д д д д д © ю Ыд д© зддддддддддддддды юдддддд© ЫЫЫЫЫЫЫЫ Проверка Ё Приостанов Запуск Ё Ы ю д д д сигналов v Ё Ы При-зддддддд© здаддддд© Готов к ос- Ё Ё Возобновление Ё Ё запуску та- Ё 4 цддддддддддддддддддддддд>Ё 3 Ё в памяти нов-Ё Ё Ё Ё лен юдддбддды юбдддддды в па- Ё Ё ^ ^ мяти Ё Ё Ё Ё Достаточно Ё Ё Ё Ё памяти Ё Ё Ё юддд© Ё Вы- Вы- Ё Ё Ё Ё грузка грузка Ё Ё Ё Создан Ё Ё ЁЗа- задддддд© Ё Ё Ёгруз-Ё Ё fork Ё Ё Ёка Ё 8 Ё<ддддд Ё Ё Ё Ё Ё Ё Ё Ё юбдддддды Ё Ё Ё Ё Ё Ё Ё Ё Недоста- Ё Ё Ё зддды точно Ё Ё Ё Ё памяти Ё Ё Ё Ё (только система Ё Ё Ё Ё подкачки) v v Ё v зддддддд© здддаддд© Ё Ё Возобновление Ё Ё Ё 6 цддддддддддддддддддддддд>Ё 5 Ё Ё Ё Ё Ё юддддддды юддддддды Приостановлен, Готов к запуску, выгружен выгружен Рисунок 7.6. Диаграмма переходов процесса из состояние в сос- тояние с указанием моментов проверки и обработки сигналов здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм issig /* проверка получения сигналов */ Ё Ё входная информация: отсутствует Ё Ё выходная информация: "истина", если процесс получил сигна- Ё Ё лы, которые его интересуют Ё Ё "ложь" - в противном случае Ё Ё { Ё Ё выполнить пока (поле в записи таблицы процессов, содер- Ё Ё жащее индикацию о получении сигнала, хранит ненулевое Ё Ё значение) Ё Ё { Ё Ё найти номер сигнала, посланного процессу; Ё Ё если (сигнал типа "гибель потомка") Ё Ё { Ё Ё если (сигналы данного типа игнорируются) Ё Ё освободить записи таблицы процессов, которые Ё Ё соответствуют потомкам, прекратившим существо-Ё Ё вание; Ё Ё в противном случае если (сигналы данного типа при-Ё Ё нимаются) Ё Ё возвратить (истину); Ё Ё } Ё Ё в противном случае если (сигнал не игнорируется) Ё Ё возвратить (истину); Ё Ё сбросить (погасить) сигнальный разряд, установленный Ё Ё в соответствующем поле таблицы процессов, хранящем Ё Ё индикацию получения сигнала; Ё Ё } Ё Ё возвратить (ложь); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.7. Алгоритм опознания сигналов 7.2.1 Обработка сигналов Ядро обрабатывает сигналы в контексте того процесса, который получает их, поэтому чтобы обработать сигналы, нужно запустить процесс. Существует три способа обработки сигналов: процесс за- вершается по получении сигнала, не обращает внимание на сигнал или выполняет особую (пользовательскую) функцию по его получении. Реакцией по умолчанию со стороны процесса, исполняемого в режиме ядра, является вызов функции exit, однако с помощью функции signal процесс может указать другие специальные действия, прини- маемые по получении тех или иных сигналов. Синтаксис вызова системной функции signal: oldfunction = signal(signum,function); где signum - номер сигнала, при получении которого будет выполне- но действие, связанное с запуском пользовательской функции, function - адрес функции, oldfunction - возвращаемое функцией значение. Вместо адреса функции процесс может передавать вызывае- мой процедуре signal числа 1 и 0: если function = 1, процесс бу- дет игнорировать все последующие поступления сигнала с номером signum (особый случай, связанный с игнорированием сигнала "гибель потомка", рассматривается в разделе 7.4), если = 0 (значение по умолчанию), процесс по получении сигнала в режиме ядра завершает- ся. В пространстве процесса поддерживается массив полей для обра- ботки сигналов, по одному полю на каждый определенный в системе сигнал. В поле, соответствующем сигналу с указанным номером, ядро сохраняет адрес пользовательской функции, вызываемой по получении сигнала процессом. Способ обработки сигналов одного типа не вли- яет на обработку сигналов других типов. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм psig /* обработка сигналов после проверки их Ё Ё существования */ Ё Ё входная информация: отсутствует Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё выбрать номер сигнала из записи таблицы процессов; Ё Ё очистить поле с номером сигнала; Ё Ё если (пользователь ранее вызывал функцию signal, с по- Ё Ё мощью которой сделал указание игнорировать сигнал дан- Ё Ё ного типа) Ё Ё возвратить управление; Ё Ё если (пользователь указал функцию, которую нужно выпол- Ё Ё нить по получении сигнала) Ё Ё { Ё Ё из пространства процесса выбрать пользовательский Ё Ё виртуальный адрес функции обработки сигнала; Ё Ё /* следующий оператор имеет нежелательные побочные Ё Ё эффекты */ Ё Ё очистить поле в пространстве процесса, содержащее Ё Ё адрес функции обработки сигнала; Ё Ё внести изменения в пользовательский контекст: Ё Ё искусственно создать в стеке задачи запись, ими- Ё Ё тирующую обращение к функции обработки сигнала; Ё Ё внести изменения в системный контекст: Ё Ё записать адрес функции обработки сигнала в поле Ё Ё счетчика команд, принадлежащее сохраненному ре- Ё Ё гистровому контексту задачи; Ё Ё возвратить управление; Ё Ё } Ё Ё если (сигнал требует дампирования образа процесса в па- Ё Ё мяти) Ё Ё { Ё Ё создать в текущем каталоге файл с именем "core"; Ё Ё переписать в файл "core" содержимое пользовательско-Ё Ё го контекста; Ё Ё } Ё Ё немедленно запустить алгоритм exit; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.8. Алгоритм обработки сигналов Обрабатывая сигнал (Рисунок 7.8), ядро определяет тип сигнала и очищает (гасит) разряд в записи таблицы процессов, соответству- ющий данному типу сигнала и установленный в момент получения сиг- нала процессом. Если функции обработки сигнала присвоено значение по умолчанию, ядро в отдельных случаях перед завершением процесса сбрасывает на внешний носитель (дампирует) образ процесса в памя- ти (см. упражнение 7.7). Дампирование удобно для программистов тем, что позволяет установить причину завершения процесса и пос- редством этого вести отладку программ. Ядро дампирует состояние памяти при поступлении сигналов, которые сообщают о каких-нибудь ошибках в выполнении процессов, как например, попытка исполнения запрещенной команды или обращение к адресу, находящемуся за пре- делами виртуального адресного пространства процесса. Ядро не дам- пирует состояние памяти, если сигнал не связан с программной ошибкой. Например, прерывание, вызванное нажатием клавиш "delete" или "break" на терминале, имеет своим результатом посыл- ку сигнала, который сообщает о том, что пользователь хочет раньше времени завершить процесс, в то время как сигнал о "зависании" является свидетельством нарушения связи с регистрационным терми- налом. Эти сигналы не связаны с ошибками в протекании процесса. Сигнал о выходе (quit), однако, вызывает сброс состояния памяти, несмотря на то, что он возникает за пределами выполняемого про- цесса. Этот сигнал, обычно вызываемый одновременным нажатием кла- виш , дает программисту возможность получать дамп состоя- ния памяти в любой момент после запуска процесса, что бывает необходимо, если процесс попадает в бесконечный цикл выполнения одних и тех же команд (зацикливается). Если процесс получает сигнал, на который было решено не обра- щать внимание, выполнение процесса продолжается так, словно сиг- нала и не было. Поскольку ядро не сбрасывает значение соответс- твующего поля, свидетельствующего о необходимости игнорирования сигнала данного типа, то когда сигнал поступит вновь, процесс опять не обратит на него внимание. Если процесс получает сигнал, реагирование на который было признано необходимым, сразу по возв- ращении процесса в режим задачи выполняется заранее условленное действие, однако прежде чем перевести процесс в режим задачи, яд- ро еще должно предпринять следующие шаги: 1. Ядро обращается к сохраненному регистровому контексту задачи и выбирает значения счетчика команд и указателя вершины сте- ка, которые будут возвращены пользовательскому процессу. 2. Сбрасывает в пространстве процесса прежнее значение поля функции обработки сигнала и присваивает ему значение по умол- чанию. 3. Создает новую запись в стеке задачи, в которую, при необходи- мости выделяя дополнительную память, переписывает значения счетчика команд и указателя вершины стека, выбранные ранее из сохраненного регистрового контекста задачи. Стек задачи будет выглядеть так, как будто процесс произвел обращение к пользо- вательской функции (обработки сигнала) в той точке, где он вызывал системную функцию или где ядро прервало его выполне- ние (перед опознанием сигнала). 4. Вносит изменения в сохраненный регистровый контекст задачи: устанавливает значение счетчика команд равным адресу функции обработки сигнала, а значение указателя вершины стека равным глубине стека задачи. Таким образом, по возвращении из режима ядра в режим задачи процесс приступит к выполнению функции обработки сигнала; после ее завершения управление будет передано на то место в программе пользователя, где было произведено обращение к системной функции или произошло прерывание, тем самым как бы имитируется выход из системной функции или прерывания. В качестве примера можно привести программу (Рисунок 7.9), которая принимает сигналы о прерывании (SIGINT) и сама посылает их (в результате выполнения функции kill). На Рисунке 7.10 предс- тавлены фрагменты программного кода, полученные в результате дис- ассемблирования загрузочного модуля в операционной среде VAX 11/780. При выполнении процесса обращение к библиотечной процеду- ре kill имеет адрес (шестнадцатиричный) ee; эта процедура в свою очередь, прежде чем вызвать системную функцию kill, исполняет ко- манду chmk (перевести процесс в режим ядра) по адресу 10a. Адрес возврата из системной функции - 10c. Во время исполнения систем- ной функции ядро посылает процессу сигнал о прерывании. Ядро об- ращает внимание на этот сигнал тогда, когда процесс собирается вернуться в режим задачи, выбирая из сохраненного регистрового контекста адрес возврата 10c и помещая его в стек задачи. При этом адрес функции обработки сигнала, 104, ядро помещает в сохра- ненный регистровый контекст задачи. На Рисунке 7.11 показаны различные состояния стека задачи и сохраненного регистрового кон- текста. В рассмотренном алгоритме обработки сигналов имеются некото- рые несоответствия. Первое из них и наиболее важное связано с очисткой перед возвращением процесса в режим задачи того поля в пространстве процесса, которое содержит адрес пользовательской функции обработки сигнала. Если процессу снова понадобится обра- ботать сигнал, ему опять придется прибегнуть к помощи системной функции signal. При этом могут возникнуть нежелательные последс- зддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main() Ё Ё { Ё Ё extern catcher(); Ё Ё signal(SIGINT,catcher); Ё Ё kill(0,SIGINT); Ё Ё } Ё Ё Ё Ё catcher() Ё Ё { Ё Ё } Ё юддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.9. Исходный текст программы приема сигналов здддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё **** VAX DISASSEMBLER **** Ё Ё Ё Ё _main() Ё Ё e4: Ё Ё e6: pushab Ox18(pc) Ё Ё ec: pushl $Ox2 Ё Ё # в следующей строке вызывается функция signal Ё Ё ee: calls $Ox2,Ox23(pc) Ё Ё f5: pushl $Ox2 Ё Ё f7: clrl -(sp) Ё Ё # в следующей строке вызывается библиотечная процеду-Ё Ё ра kill Ё Ё f9: calls $Ox2,Ox8(pc) Ё Ё 100: ret Ё Ё 101: halt Ё Ё 102: halt Ё Ё 103: halt Ё Ё _catcher() Ё Ё 104: Ё Ё 106: ret Ё Ё 107: halt Ё Ё _kill() Ё Ё 108: Ё Ё # в следующей строке вызывается внутреннее прерываниеЁ Ё операционной системы Ё Ё 10a: chmk $Ox25 Ё Ё 10c: bgequ Ox6 Ё Ё 10e: jmp Ox14(pc) Ё Ё 114: clrl r0 Ё Ё 116: ret Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.10. Результат дисассемблирования программы приема сигналов До После Ё Ё Ё Ё Ё Ё здд>цдддддддддддддддддддд╢ Ё Ё Вершина Ё Ё Новая запись с вы- Ё Ё Ё здд стека дды Ё зовом функции Ё Ё Ё Ё задачи Ё Ё Ё Ё Ё ЫЫЫЫ>ЁАдрес возврата (10c)Ё цдддддддддддддддддддд╢<дды Ы цдддддддддддддддддддд╢ Ё Стек задачи Ё Ы Ё Стек задачи Ё Ё до Ё Ы Ё до Ё Ё получения сигнала Ё Ы Ё получения сигнала Ё юдддддддддддддддддддды Ы юдддддддддддддддддддды Стек задачи Ы Стек задачи Ы здддддддддддддддддддд© Ы здддддддддддддддддддд© Ё Адрес возврата Ё Ы Ё Адрес возврата Ё Ё в процессе (10c) ЫЁЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ Ё в процессе (104) Ё цдддддддддддддддддддд╢ цдддддддддддддддддддд╢ Ё Сохраненный регист-Ё Ё Сохраненный регист-Ё Ё ровый контекст за- Ё Ё ровый контекст за- Ё Ё дачи Ё Ё дачи Ё юдддддддддддддддддддды юдддддддддддддддддддды Системный контекстный Системный контекстный уровень 1 уровень 1 Область сохранения Область сохранения регистров регистров Рисунок 7.11. Стек задачи и область сохранения структур ядра до и после получения сигнала твия: например, могут создасться условия для конкуренции, если второй раз сигнал поступит до того, как процесс получит возмож- ность запустить системную функцию. Поскольку процесс выполняется в режиме задачи, ядру следовало бы произвести переключение кон- текста, чтобы увеличить тем самым шансы процесса на получение сигнала до момента сброса значения поля функции обработки сигна- ла. Эту ситуацию можно разобрать на примере программы, представ- ленной на Рисунке 7.12. Процесс обращается к системной функции signal для того, чтобы дать указание принимать сигналы о прерыва- ниях и исполнять по их получении функцию sigcatcher. Затем он по- рождает новый процесс, запускает системную функцию nice, позволя- ющую сделать приоритет запуска процесса-родителя ниже приоритета его потомка (см. главу 8), и входит в бесконечный цикл. Порожден- ный процесс задерживает свое выполнение на 5 секунд, чтобы дать родительскому процессу время исполнить системную функцию nice и снизить свой приоритет. После этого порожденный процесс входит в цикл, в каждой итерации которого он посылает родительскому про- цессу сигнал о прерывании (посредством обращения к функции kill). Если в результате ошибки, например, из-за того, что родительский процесс больше не существует, kill завершается, то завершается и порожденный процесс. Вся идея состоит в том, что родительскому процессу следует запускать функцию обработки сигнала при каждом получении сигнала о прерывании. Функция обработки сигнала выводит сообщение и снова обращается к функции signal при очередном появ- лении сигнала о прерывании, родительский же процесс продолжает здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё sigcatcher() Ё Ё { Ё Ё printf("PID %d принял сигнал\n",getpid()); /* печать Ё Ё PID */ Ё Ё signal(SIGINT,sigcatcher); Ё Ё } Ё Ё Ё Ё main() Ё Ё { Ё Ё int ppid; Ё Ё Ё Ё signal(SIGINT,sigcatcher); Ё Ё Ё Ё if (fork() == 0) Ё Ё { Ё Ё /* дать процессам время для выполнения установок */ Ё Ё sleep(5); /* библиотечная функция приостанова наЁ Ё 5 секунд */ Ё Ё ppid = getppid(); /* получить идентификатор родите- Ё Ё ля */ Ё Ё for (;;) Ё Ё if (kill(ppid,SIGINT) == -1) Ё Ё exit(); Ё Ё } Ё Ё Ё Ё /* чем ниже приоритет, тем выше шансы возникновения кон-Ё Ё куренции */ Ё Ё nice(10); Ё Ё for (;;) Ё Ё ; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.12. Программа, демонстрирующая возникновение сопер- ничества между процессами в ходе обработки сиг- налов исполнять циклический набор команд. Однако, возможна и следующая очередность наступления событий: 1. Порожденный процесс посылает родительскому процессу сигнал о прерывании. 2. Родительский процесс принимает сигнал и вызывает функцию об- работки сигнала, но резервируется ядром, которое производит переключение контекста до того, как функция signal будет выз- вана повторно. 3. Снова запускается порожденный процесс, который посылает роди- тельскому процессу еще один сигнал о прерывании. 4. Родительский процесс получает второй сигнал о прерывании, но перед тем он не успел сделать никаких распоряжений относи- тельно способа обработки сигнала. Когда выполнение родитель- ского процесса будет возобновлено, он завершится. В программе описывается именно такое поведение процессов, поскольку вызов родительским процессом функции nice приводит к тому, что ядро будет чаще запускать на выполнение порожденный процесс. По словам Ричи (эти сведения были получены в частной беседе), сигналы были задуманы как события, которые могут быть как фаталь- ными, так и проходящими незаметно, которые не всегда обрабатыва- ются, поэтому в ранних версиях системы конкуренция процессов, связанная с посылкой сигналов, не фиксировалась. Тем не менее, она представляет серьезную проблему в тех программах, где осу- ществляется прием сигналов. Эта проблема была бы устранена, если бы поле описания сигнала не очищалось по его получении. Однако, такое решение породило бы новую проблему: если поступающий сигнал принимается, а поле очищено, вложенные обращения к функции обра- ботки сигнала могут переполнить стек. С другой стороны, ядро мог- ло бы сбросить значение функции обработки сигнала, тем самым де- лая распоряжение игнорировать сигналы данного типа до тех пор, пока пользователь вновь не укажет, что нужно делать по получении подобных сигналов. Такое решение предполагает потерю информации, так как процесс не в состоянии узнать, сколько сигналов им было получено. Однако, информации при этом теряется не больше, чем в том случае, когда процесс получает большое количество сигналов одного типа до того, как получает возможность их обработать. В системе BSD, наконец, процесс имеет возможность блокировать полу- чение сигналов и снимать блокировку при новом обращении к систем- ной функции; когда процесс снимает блокировку сигналов, ядро по- сылает процессу все сигналы, отложенные (повисшие) с момента ус- тановки блокировки. Когда процесс получает сигнал, ядро автомати- чески блокирует получение следующего сигнала до тех пор, пока функция обработки сигнала не закончит работу. В этих действиях ядра наблюдается аналогия с тем, как ядро реагирует на аппаратные прерывания: оно блокирует появление новых прерываний на время об- работки предыдущих. Второе несоответствие в обработке сигналов связано с приемом сигналов, поступающих во время исполнения системной функции, ког- да процесс приостановлен с допускающим прерывания приоритетом. Сигнал побуждает процесс выйти из приостанова (с помощью longjump), вернуться в режим задачи и вызвать функцию обработки сигнала. Когда функция обработки сигнала завершает работу, проис- ходит то, что процесс выходит из системной функции с ошибкой, со- общающей о прерывании ее выполнения. Узнав об ошибке, пользова- тель запускает системную функцию повторно, однако более удобно было бы, если бы это действие автоматически выполнялось ядром, как в системе BSD. Третье несоответствие проявляется в том случае, когда процесс игнорирует поступивший сигнал. Если сигнал поступает в то время, когда процесс находится в состоянии приостанова с допускающим прерывания приоритетом, процесс возобновляется, но не выполняет longjump. Другими словами, ядро узнает о том, что процесс проиг- норировал поступивший сигнал только после возобновления его вы- полнения. Логичнее было бы оставить процесс в состоянии приоста- нова. Однако, в момент посылки сигнала к пространству процесса, в котором ядро хранит адрес функции обработки сигнала, может от- сутствовать доступ. Эта проблема может быть решена путем запоми- нания адреса функции обработки сигнала в записи таблицы процес- сов, обращаясь к которой, ядро получало бы возможность решать вопрос о необходимости возобновления процесса по получении сигна- ла. С другой стороны, процесс может немедленно вернуться в состо- яние приостанова (по алгоритму sleep), если обнаружит, что в его возобновлении не было необходимости. Однако, пользовательские процессы не имеют возможности осознавать собственное возобновле- ние, поскольку ядро располагает точку входа в алгоритм sleep внутри цикла с условием продолжения (см. главу 2), переводя про- цесс вновь в состояние приостанова, если ожидаемое процессом со- бытие в действительности не имело места. Ко всему сказанному выше следует добавить, что ядро обрабаты- вает сигналы типа "гибель потомка" не так, как другие сигналы. В частности, когда процесс узнает о получении сигнала "гибель по- томка", он выключает индикацию сигнала в соответствующем поле за- писи таблицы процессов и по умолчанию действует так, словно ника- кого сигнала и не поступало. Назначение сигнала "гибель потомка" состоит в возобновлении выполнения процесса, приостановленного с допускающим прерывания приоритетом. Если процесс принимает такой сигнал, он, как и во всех остальных случаях, запускает функцию обработки сигнала. Действия, предпринимаемые ядром в том случае, когда процесс игнорирует поступивший сигнал этого типа, будут описаны в разделе 7.4. Наконец, когда процесс вызвал функцию signal с параметром "гибель потомка" (death of child), ядро посы- лает ему соответствующий сигнал, если он имеет потомков, прекра- тивших существование. В разделе 7.4 на этом моменте мы остановим- ся более подробно. G7.2.2 Группы процессовH Несмотря на то, что в системе UNIX процессы идентифицируются уникальным кодом (PID), системе иногда приходится использовать для идентификации процессов номер "группы", в которую они входят. Например, процессы, имеющие общего предка в лице регистрационного shell'а, взаимосвязаны, и поэтому когда пользователь нажимает клавиши "delete" или "break", или когда терминальная линия "зави- сает", все эти процессы получают соответствующие сигналы. Ядро использует код группы процессов для идентификации группы взаимос- вязанных процессов, которые при наступлении определенных событий должны получать общий сигнал. Код группы запоминается в таблице процессов; процессы из одной группы имеют один и тот же код груп- пы. Для того, чтобы присвоить коду группы процессов начальное значение, приравняв его коду идентификации процесса, следует вос- пользоваться системной функцией setpgrp. Синтаксис вызова функ- ции: grp = setpgrp(); где grp - новый код группы процессов. При выполнении функции fork процесс-потомок наследует код группы своего родителя. Использова- ние функции setpgrp при назначении для процесса операторского терминала имеет важные особенности, на которые стоит обратить внимание (см. раздел 10.3.5). 7.2.3 Посылка сигналов процессами Для посылки сигналов процессы используют системную функцию kill. Синтаксис вызова функции: kill(pid,signum) где в pid указывается адресат посылаемого сигнала (область дейс- твия сигнала), а в signum - номер посылаемого сигнала. Связь меж- ду значением pid и совокупностью выполняющихся процессов следую- щая: * Если pid - положительное целое число, ядро посылает сигнал процессу с идентификатором pid. * Если значение pid равно 0, сигнал посылается всем процессам, входящим в одну группу с процессом, вызвавшим функцию kill. * Если значение pid равно -1, сигнал посылается всем процессам, у которых реальный код идентификации пользователя совпадает с тем, под которым исполняется процесс, вызвавший функцию kill (об этих кодах более подробно см. в разделе 7.6). Если про- цесс, пославший сигнал, исполняется под кодом идентификации суперпользователя, сигнал рассылается всем процессам, кроме процессов с идентификаторами 0 и 1. * Если pid - отрицательное целое число, но не -1, сигнал посы- лается всем процессам, входящим в группу с номером, равным абсолютному значению pid. Во всех случаях, если процесс, пославший сигнал, исполняется под кодом идентификации пользователя, не являющегося суперпользо- вателем, или если коды идентификации пользователя (реальный и ис- полнительный) у этого процесса не совпадают с соответствующими кодами процесса, принимающего сигнал, kill завершается неудачно. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main() Ё Ё { Ё Ё register int i; Ё Ё Ё Ё setpgrp(); Ё Ё for (i = 0; i < 10; i++) Ё Ё { Ё Ё if (fork() == 0) Ё Ё { Ё Ё /* порожденный процесс */ Ё Ё if (i & 1) Ё Ё setpgrp(); Ё Ё printf("pid = %d pgrp = %d\n",getpid(),getpgrp());Ё Ё pause(); /* системная функция приостанова вы- Ё Ё полнения */ Ё Ё } Ё Ё } Ё Ё kill(0,SIGINT); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.13. Пример использования функции setpgrp В программе, приведенной на Рисунке 7.13, главный процесс сбрасывает установленное ранее значение номера группы и порождает 10 новых процессов. При рождении каждый процесс-потомок наследует номер группы процессов своего родителя, однако, процессы, создан- ные в нечетных итерациях цикла, сбрасывают это значение. Систем- ные функции getpid и getpgrp возвращают значения кода идентифика- ции выполняемого процесса и номера группы, в которую он входит, а функция pause приостанавливает выполнение процесса до момента по- лучения сигнала. В конечном итоге родительский процесс запускает функцию kill и посылает сигнал о прерывании всем процессам, вхо- дящим в одну с ним группу. Ядро посылает сигнал пяти "четным" процессам, не сбросившим унаследованное значение номера группы, при этом пять "нечетных" процессов продолжают свое выполнение. 7.3 ЗАВЕРШЕНИЕ ВЫПОЛНЕНИЯ ПРОЦЕССА В системе UNIX процесс завершает свое выполнение, запуская системную функцию exit. После этого процесс переходит в состояние "прекращения существования" (см. Рисунок 6.1), освобождает ресур- сы и ликвидирует свой контекст. Синтаксис вызова функции: exit(status); где status - значение, возвращаемое функцией родительскому про- цессу. Процессы могут вызывать функцию exit как в явном, так и в неявном виде (по окончании выполнения программы: начальная проце- дура (startup), компонуемая со всеми программами на языке Си, вы- зывает функцию exit на выходе программы из функции main, являю- щейся общей точкой входа для всех программ). С другой стороны, ядро может вызывать функцию exit по своей инициативе, если про- цесс не принял посланный ему сигнал (об этом мы уже говорили вы- ше). В этом случае значение параметра status равно номеру сигна- ла. Система не накладывает никакого ограничения на продолжитель- ность выполнения процесса, и зачастую процессы существуют в тече- ние довольно длительного времени. Нулевой процесс (программа под- качки) и процесс 1 (init), к примеру, существуют на протяжении всего времени жизни системы. Продолжительными процессами являются также getty-процессы, контролирующие работу терминальной линии, ожидая регистрации пользователей, и процессы общего назначения, выполняемые под руководством администратора. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм exit Ё Ё входная информация: код, возвращаемый родительскому про- Ё Ё цессу Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё игнорировать все сигналы; Ё Ё если (процесс возглавляет группу процессов, ассоцииро- Ё Ё ванную с операторским терминалом) Ё Ё { Ё Ё послать всем процессам, входящим в группу, сигнал о Ё Ё "зависании"; Ё Ё сбросить в ноль код группы процессов; Ё Ё } Ё Ё закрыть все открытые файлы (внутренняя модификация алго-Ё Ё ритма close); Ё Ё освободить текущий каталог (алгоритм iput); Ё Ё освободить области и память, ассоциированную с процессомЁ Ё (алгоритм freereg); Ё Ё создать запись с учетной информацией; Ё Ё прекратить существование процесса (перевести его в соот-Ё Ё ветствующее состояние); Ё Ё назначить всем процессам-потомкам в качестве родителя Ё Ё процесс init (1); Ё Ё если кто-либо из потомков прекратил существование, Ё Ё послать процессу init сигнал "гибель потомка"; Ё Ё послать сигнал "гибель потомка" родителю данного процес-Ё Ё са; Ё Ё переключить контекст; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.14. Алгоритм функции exit На Рисунке 7.14 приведен алгоритм функции exit. Сначала ядро отменяет обработку всех сигналов, посылаемых процессу, поскольку ее продолжение становится бессмысленным. Если процесс, вызывающий функцию exit, возглавляет группу процессов, ассоциированную с операторским терминалом (см. раздел 10.3.5), ядро делает предпо- ложение о том, что пользователь прекращает работу, и посылает всем процессам в группе сигнал о "зависании". Таким образом, если пользователь в регистрационном shell'е нажмет последовательность клавиш, означающую "конец файла" (Ctrl-d), при этом с терминалом остались связанными некоторые из существующих процессов, процесс, выполняющий функцию exit, пошлет им всем сигнал о "зависании". Кроме того, ядро сбрасывает в ноль значение кода группы процессов для всех процессов, входящих в данную группу, поскольку не исклю- чена возможность того, что позднее текущий код идентификации про- цесса (процесса, который вызвал функцию exit) будет присвоен дру- гому процессу и тогда последний возглавит группу с указанным кодом. Процессы, входившие в старую группу, в новую группу вхо- дить не будут. После этого ядро просматривает дескрипторы откры- тых файлов, закрывает каждый из этих файлов по алгоритму close и освобождает по алгоритму iput индексы текущего каталога и корня (если он изменялся). Наконец, ядро освобождает всю выделенную задаче память вместе с соответствующими областями (по алгоритму detachreg) и переводит процесс в состояние прекращения существования. Ядро сохраняет в таблице процессов код возврата функции exit (status), а также суммарное время исполнения процесса и его потомков в режиме ядра и режиме задачи. В разделе 7.4 при рассмотрении функции wait бу- дет показано, каким образом процесс получает информацию о времени выполнения своих потомков. Ядро также создает в глобальном учет- ном файле запись, которая содержит различную статистическую ин- формацию о выполнении процесса, такую как код идентификации поль- зователя, использование ресурсов центрального процессора и памяти, объем потоков ввода-вывода, связанных с процессом. Поль- зовательские программы могут в любой момент обратиться к учетному файлу за статистическими данными, представляющими интерес с точки зрения слежения за функционированием системы и организации расче- тов с пользователями. Ядро удаляет процесс из дерева процессов, а его потомков передает процессу 1 (init). Таким образом, процесс 1 становится законным родителем всех продолжающих существование по- томков завершающегося процесса. Если кто-либо из потомков прекра- щает существование, завершающийся процесс посылает процессу init сигнал "гибель потомка" для того, чтобы процесс начальной загруз- ки мог удалить запись о потомке из таблицы процессов (см. раздел 7.9); кроме того, завершающийся процесс посылает этот сигнал сво- ему родителю. В типичной ситуации родительский процесс синхрони- зирует свое выполнение с завершающимся потомком с помощью систем- ной функции wait. Прекращая существование, процесс переключает контекст и ядро может теперь выбирать для исполнения следующий процесс; ядро с этих пор уже не будет исполнять процесс, прекра- тивший существование. В программе, приведенной на Рисунке 7.15, процесс создает но- вый процесс, который печатает свой код идентификации и вызывает системную функцию pause, приостанавливаясь до получения сигнала. Процесс-родитель печатает PID своего потомка и завершается, возв- ращая только что выведенное значение через параметр status. Если бы вызов функции exit отсутствовал, начальная процедура сделала бы его по выходе процесса из функции main. Порожденный процесс продолжает ожидать получения сигнала, даже если его родитель уже завершился. 7.4 ОЖИДАНИЕ ЗАВЕРШЕНИЯ ВЫПОЛНЕНИЯ ПРОЦЕССА Процесс может синхронизировать продолжение своего выполнения с моментом завершения потомка, если воспользуется системной функ- цией wait. Синтаксис вызова функции: здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё main() Ё Ё { Ё Ё int child; Ё Ё Ё Ё if ((child = fork()) == 0) Ё Ё { Ё Ё printf("PID потомка %d\n",getpid()); Ё Ё pause(); /* приостанов выполнения до получения Ё Ё сигнала */ Ё Ё } Ё Ё /* родитель */ Ё Ё printf("PID потомка %d\n",child); Ё Ё exit(child); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.15. Пример использования функции exit pid = wait(stat_addr); где pid - значение кода идентификации (PID) прекратившего свое существование потомка, stat_addr - адрес переменной целого типа, в которую будет помещено возвращаемое функцией exit значение, в пространстве задачи. Алгоритм функции wait приведен на Рисунке 7.16. Ядро ведет поиск потомков процесса, прекративших существование, и в случае их отсутствия возвращает ошибку. Если потомок, прекративший су- ществование, обнаружен, ядро передает его код идентификации и значение, возвращаемое через параметр функции exit, процессу, вызвавшему функцию wait. Таким образом, через параметр функции exit (status) завершающийся процесс может передавать различные значения, в закодированном виде содержащие информацию о причине завершения процесса, однако на практике этот параметр использует- ся по назначению довольно редко. Ядро передает в соответствующие поля, принадлежащие пространству родительского процесса, накоп- ленные значения продолжительности исполнения процесса-потомка в режиме ядра и в режиме задачи и, наконец, освобождает в таблице процессов место, которое в ней занимал прежде прекративший су- ществование процесс. Это место будет предоставлено новому процес- су. Если процесс, выполняющий функцию wait, имеет потомков, про- должающих существование, он приостанавливается до получения ожидаемого сигнала. Ядро не возобновляет по своей инициативе про- цесс, приостановившийся с помощью функции wait: такой процесс мо- жет возобновиться только в случае получения сигнала. На все сиг- налы, кроме сигнала "гибель потомка", процесс реагирует ранее рассмотренным образом. Реакция процесса на сигнал "гибель потом- ка" проявляется по-разному в зависимости от обстоятельств: * По умолчанию (то есть если специально не оговорены никакие другие действия) процесс выходит из состояния останова, в ко- торое он вошел с помощью функции wait, и запускает алгоритм issig для опознания типа поступившего сигнала. Алгоритм issig (Рисунок 7.7) рассматривает особый случай поступления сигнала типа "гибель потомка" и возвращает "ложь". Поэтому ядро не выполняет longjump из функции sleep, а возвращает управление функции wait. Оно перезапускает функцию wait, находит потом- ков, прекративших существование (по крайней мере, одного), освобождает место в таблице процессов, занимаемое этими по- томками, и выходит из функции wait, возвращая управление про- цессу, вызвавшему ее. * Если процессы принимает сигналы данного типа, ядро делает все необходимые установки для запуска пользовательской функции обработки сигнала, как и в случае поступления сигнала любого другого типа. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм wait Ё Ё входная информация: адрес переменной для хранения значенияЁ Ё status, возвращаемого завершающимся Ё Ё процессом Ё Ё выходная информация: идентификатор потомка и код возврата Ё Ё функции exit Ё Ё { Ё Ё если (процесс, вызвавший функцию wait, не имеет потом- Ё Ё ков) Ё Ё возвратить (ошибку); Ё Ё Ё Ё для (;;) /* цикл с внутренним циклом */ Ё Ё { Ё Ё если (процесс, вызвавший функцию wait, имеет потом-Ё Ё ков, прекративших существование) Ё Ё { Ё Ё выбрать произвольного потомка; Ё Ё передать его родителю информацию об использова-Ё Ё нии потомком ресурсов центрального процессора;Ё Ё освободить в таблице процессов место, занимае- Ё Ё мое потомком; Ё Ё возвратить (идентификатор потомка, код возвратаЁ Ё функции exit, вызванной потомком); Ё Ё } Ё Ё если (у процесса нет потомков) Ё Ё возвратить ошибку; Ё Ё приостановиться с приоритетом, допускающим прерыва-Ё Ё ния (до завершения потомка); Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.16. Алгоритм функции wait * Если процесс игнорирует сигналы данного типа, ядро перезапус- кает функцию wait, освобождает в таблице процессов место, за- нимаемое потомками, прекратившими существование, и исследует оставшихся потомков. Например, если пользователь запускает программу, приведенную на Рисунке 7.17, с параметром и без параметра, он получит разные результаты. Сначала рассмотрим случай, когда пользователь запус- кает программу без параметра (единственный параметр - имя прог- раммы, то есть argc равно 1). Родительский процесс порождает 15 потомков, которые в конечном итоге завершают свое выполнение с кодом возврата i, номером процесса в порядке очередности созда- ния. Ядро, исполняя функцию wait для родителя, находит потомка, прекратившего существование, и передает родителю его идентифика- тор и код возврата функции exit. При этом заранее не известно, какой из потомков будет обнаружен. Из текста программы, реализую- щей системную функцию exit, написанной на языке Си и включенной в библиотеку стандартных подпрограмм, видно, что программа запоми- нает код возврата функции exit в битах 8-15 поля ret_code и возв- ращает функции wait идентификатор процесса-потомка. Таким обра- зом, в ret_code хранится значение, равное 256*i, где i - номер потомка, а в ret_val заносится значение идентификатора потомка. Если пользователь запускает программу с параметром (то есть argc > 1), родительский процесс с помощью функции signal делает распоряжение игнорировать сигналы типа "гибель потомка". Предпо- ложим, что родительский процесс, выполняя функцию wait, приоста- новился еще до того, как его потомок произвел обращение к функции exit: когда процесс-потомок переходит к выполнению функции exit, он посылает своему родителю сигнал "гибель потомка"; родительский процесс возобновляется, поскольку он был приостановлен с приори- тетом, допускающим прерывания. Когда так или иначе родительский процесс продолжит свое выполнение, он обнаружит, что сигнал сооб- щал о "гибели" потомка; однако, поскольку он игнорирует сигналы здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё int i,ret_val,ret_code; Ё Ё Ё Ё if (argc >= 1) Ё Ё signal(SIGCLD,SIG_IGN); /* игнорировать гибель Ё Ё потомков */ Ё Ё for (i = 0; i < 15; i++) Ё Ё if (fork() == 0) Ё Ё { Ё Ё /* процесс-потомок */ Ё Ё printf("процесс-потомок %x\n",getpid()); Ё Ё exit(i); Ё Ё } Ё Ё ret_val = wait(&ret_code); Ё Ё printf("wait ret_val %x ret_code %x\n",ret_val,ret_code);Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.17. Пример использования функции wait и игнорирова- ния сигнала "гибель потомка" этого типа и не обрабатывает их, ядро удаляет из таблицы процес- сов запись, соответствующую прекратившему существование потомку, и продолжает выполнение функции wait так, словно сигнала и не бы- ло. Ядро выполняет эти действия всякий раз, когда родительский процесс получает сигнал типа "гибель потомка", до тех пор, пока цикл выполнения функции wait не будет завершен и пока не будет установлено, что у процесса больше потомков нет. Тогда функция wait возвращает значение, равное -1. Разница между двумя способа- ми запуска программы состоит в том, что в первом случае про- цесс-родитель ждет завершения любого из потомков, в то время как во втором случае он ждет, пока завершатся все его потомки. В ранних версиях системы UNIX функции exit и wait не исполь- зовали и не рассматривали сигнал типа "гибель потомка". Вместо посылки сигнала функция exit возобновляла выполнение родительско- го процесса. Если родительский процесс при выполнении функции wait приостановился, он возобновляется, находит потомка, прекра- тившего существование, и возвращает управление. В противном слу- чае возобновления не происходит; процесс-родитель обнаружит "по- гибшего" потомка при следующем обращении к функции wait. Точно так же и процесс начальной загрузки (init) может приостановиться, используя функцию wait, и завершающиеся по exit процессы будут возобновлять его, если он имеет усыновленных потомков, прекращаю- щих существование. В такой реализации функций exit и wait имеется одна нерешен- ная проблема, связанная с тем, что процессы, прекратившие сущест- вование, нельзя убирать из системы до тех пор, пока их родитель не исполнит функцию wait. Если процесс создал множество потомков, но так и не исполнил функцию wait, может произойти переполнение таблицы процессов из-за наличия потомков, прекративших существо- вание с помощью функции exit. В качестве примера рассмотрим текст программы планировщика процессов, приведенный на Рисунке 7.18. Процесс производит считывание данных из файла стандартного ввода до тех пор, пока не будет обнаружен конец файла, создавая при каждом исполнении функции read нового потомка. Однако, про- цесс-родитель не дожидается завершения каждого потомка, поскольку он стремится запускать процессы на выполнение как можно быстрее, тем более, что может пройти довольно много времени, прежде чем процесс-потомок завершит свое выполнение. Если, обратившись к здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main(argc,argv) Ё Ё { Ё Ё char buf[256]; Ё Ё Ё Ё if (argc != 1) Ё Ё signal(SIGCLD,SIG_IGN); /* игнорировать гибель Ё Ё потомков */ Ё Ё while (read(0,buf,256)) Ё Ё if (fork() == 0) Ё Ё { Ё Ё /* здесь процесс-потомок обычно выполняет Ё Ё какие-то операции над буфером (buf) */ Ё Ё exit(0); Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.18. Пример указания причины появления сигнала "ги- бель потомков" функции signal, процесс распорядился игнорировать сигналы типа "гибель потомка", ядро будет очищать записи, соответствующие прекратившим существование процессам, автоматически. Иначе в ко- нечном итоге из-за таких процессов может произойти переполнение таблицы. 7.5 ВЫЗОВ ДРУГИХ ПРОГРАММ Системная функция exec дает возможность процессу запускать другую программу, при этом соответствующий этой программе испол- няемый файл будет располагаться в пространстве памяти процесса. Содержимое пользовательского контекста после вызова функции ста- новится недоступным, за исключением передаваемых функции парамет- ров, которые переписываются ядром из старого адресного пространс- тва в новое. Синтаксис вызова функции: execve(filename,argv,envp) где filename - имя исполняемого файла, argv - указатель на массив параметров, которые передаются вызываемой программе, а envp - указатель на массив параметров, составляющих среду выполнения вы- зываемой программы. Вызов системной функции exec осуществляют несколько библиотечных функций, таких как execl, execv, execle и т.д. В том случае, когда программа использует параметры командной строки main(argc,argv) , массив argv является копией одноименного параметра, передаваемого функции exec. Символьные строки, описывающие среду выполнения вы- зываемой программы, имеют вид "имя=значение" и содержат полезную для программ информацию, такую как начальный каталог пользователя и путь поиска исполняемых программ. Процессы могут обращаться к параметрам описания среды выполнения, используя глобальную пере- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм exec Ё Ё входная информация: (1) имя файла Ё Ё (2) список параметров Ё Ё (3) список переменных среды Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё получить индекс файла (алгоритм namei); Ё Ё проверить, является ли файл исполнимым и имеет ли поль- Ё Ё зователь право на его исполнение; Ё Ё прочитать информацию из заголовков файла и проверить, Ё Ё является ли он загрузочным модулем; Ё Ё скопировать параметры, переданные функции, из старого Ё Ё адресного пространства в системное пространство; Ё Ё для (каждой области, присоединенной к процессу) Ё Ё отсоединить все старые области (алгоритм detachreg);Ё Ё для (каждой области, определенной в загрузочном модуле) Ё Ё { Ё Ё выделить новые области (алгоритм allocreg); Ё Ё присоединить области (алгоритм attachreg); Ё Ё загрузить область в память по готовности (алгоритм Ё Ё loadreg); Ё Ё } Ё Ё скопировать параметры, переданные функции, в новую об- Ё Ё ласть стека задачи; Ё Ё специальная обработка для setuid-программ, трассировка; Ё Ё проинициализировать область сохранения регистров задачи Ё Ё (в рамках подготовки к возвращению в режим задачи); Ё Ё освободить индекс файла (алгоритм iput); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.19. Алгоритм функции exec менную environ, которую заводит начальная процедура Си-интерпре- татора. На Рисунке 7.19 представлен алгоритм выполнения системной функции exec. Сначала функция обращается к файлу по алгоритму namei, проверяя, является ли файл исполнимым и отличным от ката- лога, а также проверяя наличие у пользователя права исполнять программу. Затем ядро, считывая заголовок файла, определяет раз- мещение информации в файле (формат файла). На Рисунке 7.20 изображен логический формат исполняемого фай- ла в файловой системе, обычно генерируемый транслятором или заг- рузчиком. Он разбивается на четыре части: 1. Главный заголовок, содержащий информацию о том, на сколько разделов делится файл, а также содержащий начальный адрес ис- полнения процесса и некоторое "магическое число", описывающее тип исполняемого файла. 2. Заголовки разделов, содержащие информацию, описывающую каждый раздел в файле: его размер, виртуальные адреса, в которых он располагается, и др. 3. Разделы, содержащие собственно "данные" файла (например, текстовые), которые загружаются в адресное пространство про- цесса. 4. Разделы, содержащие смешанную информацию, такую как таблицы идентификаторов и другие данные, используемые в процессе от- ладки. зддддддддддддддддддддддддддд© Ё Тип файла Ё Главный заголовок Ё Количество разделов Ё Ё Начальное состояние регис-Ё Ё тров Ё цддддддддддддддддддддддддддд╢ Ё Тип раздела Ё Заголовок 1-го раздела Ё Размер раздела Ё Ё Виртуальный адрес Ё цддддддддддддддддддддддддддд╢ Ё Тип раздела Ё Заголовок 2-го раздела Ё Размер раздела Ё Ы Ё Виртуальный адрес Ё Ы цддддддддддддддддддддддддддд╢ Ы Ё Ы Ё Ы Ё Ы Ё Ы Ё Ы Ё Ы цддддддддддддддддддддддддддд╢ Ы Ё Тип раздела Ё Заголовок n-го раздела Ё Размер раздела Ё Ё Виртуальный адрес Ё цддддддддддддддддддддддддддд╢ Раздел 1 Ё Данные (например, текст) Ё цддддддддддддддддддддддддддд╢ Раздел 2 Ё Данные Ё Ы цддддддддддддддддддддддддддд╢ Ы Ё Ы Ё Ы Ё Ы Ё Ы Ё Ы Ё Ы цддддддддддддддддддддддддддд╢ Раздел n Ё Данные Ё цддддддддддддддддддддддддддд╢ Ё Другая информация Ё юддддддддддддддддддддддддддды Рисунок 7.20. Образ исполняемого файла Указанные составляющие с развитием самой системы видоизменя- ются, однако во всех исполняемых файлах обязательно присутствует главный заголовок с полем типа файла. Тип файла обозначается коротким целым числом (представляется в машине полусловом), которое идентифицирует файл как загрузочный модуль, давая тем самым ядру возможность отслеживать динамические характеристики его выполнения. Например, в машине PDP 11/70 опре- деление типа файла как загрузочного модуля свидетельствует о том, что процесс, исполняющий файл, может использовать до 128 Кбайт памяти вместо 64 Кбайт (**), тем не менее в системах с замещением страниц тип файла все еще играет существенную роль, в чем нам предстоит убедиться во время знакомства с главой 9. ддддддддддддддддддддддддддддддддддддддд (**) В PDP 11 "магические числа" имеют значения, соответствующие командам перехода; при выполнении этих команд в ранних вер- сиях системы управление передавалось в разные места програм- мы в зависимости от размера заголовка и от типа исполняемого файла. Эта особенность больше не используется с тех пор, как система стала разрабатываться на языке Си. Вернемся к алгоритму. Мы остановились на том, что ядро обра- тилось к индексу файла и установило, что файл является исполни- мым. Ядру следовало бы освободить память, занимаемую пользова- тельским контекстом процесса. Однако, поскольку в памяти, подлежащей освобождению, располагаются передаваемые новой прог- рамме параметры, ядро первым делом копирует их из адресного пространства в промежуточный буфер на время, пока не будут отве- дены области для нового пространства памяти. Поскольку параметрами функции exec выступают пользовательские адреса массивов символьных строк, ядро по каждой строке сначала копирует в системную память адрес строки, а затем саму строку. Для хранения строки в разных версиях системы могут быть выбраны различные места. Чаще принято хранить строки в стеке ядра (ло- кальная структура данных, принадлежащая программе ядра), на не- распределяемых участках памяти (таких как страницы), которые мож- но занимать только временно, а также во внешней памяти (на устройстве выгрузки). С точки зрения реализации проще всего для копирования пара- метров в новый пользовательский контекст обратиться к стеку ядра. Однако, поскольку размер стека ядра, как правило, ограничивается системой, а также поскольку параметры функции exec могут иметь произвольную длину, этот подход следует сочетать с другими подхо- дами. При рассмотрении других вариантов обычно останавливаются на способе хранения, обеспечивающем наиболее быстрый доступ к стро- кам. Если доступ к страницам памяти в системе реализуется доволь- но просто, строки следует размещать на страницах, поскольку обра- щение к оперативной памяти осуществляется быстрее, чем к внешней (устройству выгрузки). После копирования параметров функции exec в системную память ядро отсоединяет области, ранее присоединенные к процессу, ис- пользуя алгоритм detachreg. Несколько позже мы еще поговорим о специальных действиях, выполняемых в отношении областей команд. К рассматриваемому моменту процесс уже лишен пользовательского кон- текста и поэтому возникновение в дальнейшем любой ошибки неизбеж- но будет приводить к завершению процесса по сигналу. Такими ошиб- ками могут быть обращение к пространству, не описанному в таблице областей ядра, попытка загрузить программу, имеющую недопустимо большой размер или использующую области с пересекающимися адреса- ми, и др. Ядро выделяет и присоединяет к процессу области команд и данных, загружает в оперативную память содержимое исполняемого файла (алгоритмы allocreg, attachreg и loadreg, соответственно). Область данных процесса изначально поделена на две части: данные, инициализация которых была выполнена во время компиляции, и дан- ные, не определенные компилятором ("bss"). Область памяти перво- начально выделяется для проинициализированных данных. Затем ядро увеличивает размер области данных для размещения данных типа "bss" (алгоритм growreg) и обнуляет их значения. Напоследок ядро выделяет и присоединяет к процессу область стека и отводит прост- ранство памяти для хранения параметров функции exec. Если пара- метры функции размещаются на страницах, те же страницы могут быть использованы под стек. В противном случае параметры функции раз- мещаются в стеке задачи. В пространстве процесса ядро стирает адреса пользовательских функций обработки сигналов, поскольку в новом пользовательском контексте они теряют свое значение. Однако и в новом контексте рекомендации по игнорированию тех или иных сигналов остаются в силе. Ядро устанавливает в регистрах для режима задачи значения из сохраненного регистрового контекста, в частности первоначаль- ное значение указателя вершины стека (sp) и счетчика команд (pc): первоначальное значение счетчика команд было занесено загрузчиком в заголовок файла. Для setuid-программ и для трассировки процесса ядро предпринимает особые действия, на которых мы еще остановимся во время рассмотрения глав 8 и 11, соответственно. Наконец, ядро запускает алгоритм iput, освобождая индекс, выделенный по алго- ритму namei в самом начале выполнения функции exec. Алгоритмы namei и iput в функции exec выполняют роль, подобную той, которую они выполняют при открытии и закрытии файла; состояние файла во время выполнения функции exec похоже на состояние открытого фай- ла, если не принимать во внимание отсутствие записи о файле в таблице файлов. По выходе из функции процесс исполняет текст но- вой программы. Тем не менее, процесс остается тем же, что и до выполнения функции; его идентификатор не изменился, как не изме- нилось и его место в иерархии процессов. Изменению подвергся только пользовательский контекст процесса. зддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё main() Ё Ё { Ё Ё int status; Ё Ё if (fork() == 0) Ё Ё execl("/bin/date","date",0); Ё Ё wait(&status); Ё Ё } Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.21. Пример использования функции exec В качестве примера можно привести программу (Рисунок 7.21), в которой создается процесс-потомок, запускающий функцию exec. Сра- зу по завершении функции fork процесс-родитель и процесс-потомок начинают исполнять независимо друг от друга копии одной и той же программы. К моменту вызова процессом-потомком функции exec в его области команд находятся инструкции этой программы, в области данных располагаются строки "/bin/date" и "date", а в стеке - за- писи, которые будут извлечены по выходе из exec. Ядро ищет файл "/bin/date" в файловой системе, обнаружив его, узнает, что его может исполнить любой пользователь, а также то, что он представ- ляет собой загрузочный модуль, готовый для исполнения. По условию первым параметром функции exec, включаемым в список параметров argv, является имя исполняемого файла (последняя компонента имени пути поиска файла). Таким образом, процесс имеет доступ к имени программы на пользовательском уровне, что иногда может оказаться полезным (***). Затем ядро копирует строки "/bin/date" и "date" во внутреннюю структуру хранения и освобождает области команд, данных и стека, занимаемые процессом. Процессу выделяются новые области команд, данных и стека, в область команд переписывается командная секция файла "/bin/date", в область данных - секция данных файла. Ядро восстанавливает первоначальный список парамет- ров (в данном случае это строка символов "date") и помещает его в область стека. Вызвав функцию exec, процесс-потомок прекращает выполнение старой программы и переходит к выполнению программы ддддддддддддддддддддддддддддддддддддддд (***) Например, в версии V стандартные программы переименования файла (mv), копирования файла (cp) и компоновки файла (ln), поскольку исполняют похожие действия, вызывают один и тот же исполняемый файл. По имени вызываемой программы процесс узнает, какие действия в настоящий момент требуются пользо- вателю. "date"; когда программа "date" завершится, процесс-родитель, ожи- дающий этого момента, получит код завершения функции exit. Вплоть до настоящего момента мы предполагали, что команды и данные размещаются в разных секциях исполняемой программы и, сле- довательно, в разных областях текущего процесса. Такое размещение имеет два основных преимущества: простота организации защиты от несанкционированного доступа и возможность разделения областей различными процессами. Если бы команды и данные находились в од- ной области, система не смогла бы предотвратить затирание команд, поскольку ей не были бы известны адреса, по которым они распола- гаются. Если же команды и данные находятся в разных областях, система имеет возможность пользоваться механизмами аппаратной за- щиты области команд процесса. Когда процесс случайно попытается что-то записать в область, занятую командами, он получит отказ, порожденный системой защиты и приводящий обычно к аварийному за- вершению процесса. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main() Ё Ё { Ё Ё int i,*ip; Ё Ё extern f(),sigcatch(); Ё Ё Ё Ё ip = (int *)f; /* присвоение переменной ip значения ад-Ё Ё реса функции f */ Ё Ё for (i = 0; i < 20; i++) Ё Ё signal(i,sigcatch); Ё Ё *ip = 1; /* попытка затереть адрес функции f */ Ё Ё printf("после присвоения значения ip\n"); Ё Ё f(); Ё Ё } Ё Ё Ё Ё f() Ё Ё { Ё Ё } Ё Ё Ё Ё sigcatch(n) Ё Ё int n; Ё Ё { Ё Ё printf("принят сигнал %d\n",n); Ё Ё exit(1); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.22. Пример программы, ведущей запись в область ко- манд В качестве примера можно привести программу (Рисунок 7.22), которая присваивает переменной ip значение адреса функции f и за- тем делает распоряжение принимать все сигналы. Если программа скомпилирована так, что команды и данные располагаются в разных областях, процесс, исполняющий программу, при попытке записать что-то по адресу в ip встретит порожденный системой защиты отказ, поскольку область команд защищена от записи. При работе на компь- ютере AT&T 3B20 ядро посылает процессу сигнал SIGBUS, в других системах возможна посылка других сигналов. Процесс принимает сиг- нал и завершается, не дойдя до выполнения команды вывода на пе- чать в процедуре main. Однако, если программа скомпилирована так, что команды и данные располагаются в одной области (в области данных), ядро не поймет, что процесс пытается затереть адрес функции f. Адрес f станет равным 1. Процесс исполнит команду вы- вода на печать в процедуре main, но когда запустит функцию f, произойдет ошибка, связанная с попыткой выполнения запрещенной команды. Ядро пошлет процессу сигнал SIGILL и процесс завершится. Расположение команд и данных в разных областях облегчает по- иск и предотвращение ошибок адресации. Тем не менее, в ранних версиях системы UNIX команды и данные разрешалось располагать в одной области, поскольку на машинах PDP размер процесса был силь- но ограничен: программы имели меньший размер и существенно мень- шую сегментацию, если команды и данные занимали одну и ту же область. В последних версиях системы таких строгих ограничений на размер процесса нет и в дальнейшем возможность загрузки команд и данных в одну область компиляторами не будет поддерживаться. Второе преимущество раздельного хранения команд и данных сос- тоит в возможности совместного использования областей процессами. Если процесс не может вести запись в область команд, команды про- цесса не претерпевают никаких изменений с того момента, как ядро загрузило их в область команд из командной секции исполняемого файла. Если один и тот же файл исполняется несколькими процесса- ми, в целях экономии памяти они могут иметь одну область команд на всех. Таким образом, когда ядро при выполнении функции exec отводит область под команды процесса, оно проверяет, имеется ли возможность совместного использования процессами команд исполняе- мого файла, что определяется "магическим числом" в заголовке фай- ла. Если да, то с помощью алгоритма xalloc ядро ищет существующую область с командами файла или назначает новую в случае ее отсутс- твия (см. Рисунок 7.23). Исполняя алгоритм xalloc, ядро просматривает список активных областей в поисках области с командами файла, индекс которого совпадает с индексом исполняемого файла. В случае ее отсутствия ядро выделяет новую область (алгоритм allocreg), присоединяет ее к процессу (алгоритм attachreg), загружает ее в память (алгоритм loadreg) и защищает от записи (read-only). Последний шаг предпо- лагает, что при попытке процесса записать что-либо в область ко- манд будет получен отказ, вызванный системой защиты памяти. В случае обнаружения области с командами файла в списке активных областей осуществляется проверка ее наличия в памяти (она может быть либо загружена в память, либо выгружена из памяти) и присое- динение ее к процессу. В завершение выполнения алгоритма xalloc ядро снимает с области блокировку, а позднее, следуя алгоритму detachreg при выполнении функций exit или exec, уменьшает значе- ние счетчика областей. В традиционных реализациях системы поддер- живается таблица команд, к которой ядро обращается в случаях, по- добных описанному. Таким образом, совокупность областей команд можно рассматривать как новую версию этой таблицы. Напомним, что если область при выполнении алгоритма allocreg (Раздел 6.5.2) выделяется впервые, ядро увеличивает значение счетчика ссылок на индекс, ассоциированный с областью, при этом значение счетчика ссылок нами уже было увеличено в самом начале выполнения функции exec (алгоритм namei). Поскольку ядро уменьша- ет значение счетчика только один раз в завершение выполнения функции exec (по алгоритму iput), значение счетчика ссылок на ин- декс файла, ассоциированного с разделяемой областью команд и ис- полняемого в настоящий момент, равно по меньшей мере 1. Поэтому когда процесс разрывает связь с файлом (функция unlink), содержи- мое файла остается нетронутым (не претерпевает изменений). После загрузки в память сам файл ядру становится ненужен, ядро интере- сует только указатель на копию индекса файла в памяти, содержа- щийся в таблице областей; этот указатель и будет идентифицировать здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм xalloc /* выделение и инициализация области Ё Ё команд */ Ё Ё входная информация: индекс исполняемого файла Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё если (исполняемый файл не имеет отдельной области команд)Ё Ё вернуть управление; Ё Ё если (уже имеется область команд, ассоциированная с ин- Ё Ё дексом исполняемого файла) Ё Ё { Ё Ё /* область команд уже существует ... подключиться к Ё Ё ней */ Ё Ё заблокировать область; Ё Ё выполнить пока (содержимое области еще не доступно) Ё Ё { Ё Ё /* операции над счетчиком ссылок, предохраняющие Ё Ё от глобального удаления области Ё Ё */ Ё Ё увеличить значение счетчика ссылок на область; Ё Ё снять с области блокировку; Ё Ё приостановиться (пока содержимое области не станетЁ Ё доступным); Ё Ё заблокировать область; Ё Ё уменьшить значение счетчика ссылок на область; Ё Ё } Ё Ё присоединить область к процессу (алгоритм attachreg);Ё Ё снять с области блокировку; Ё Ё вернуть управление; Ё Ё } Ё Ё /* интересующая нас область команд не существует -- соз- Ё Ё дать новую */ Ё Ё выделить область команд (алгоритм allocreg); /* область Ё Ё заблоки- Ё Ё рована */Ё Ё если (область помечена как "неотъемлемая") Ё Ё отключить соответствующий флаг; Ё Ё подключить область к виртуальному адресу, указанному в Ё Ё заголовке файла (алгоритм attachreg); Ё Ё если (файл имеет специальный формат для системы с замеще-Ё Ё нием страниц) Ё Ё /* этот случай будет рассмотрен в главе 9 */ Ё Ё в противном случае /* файл не имеет специального фор-Ё Ё мата */ Ё Ё считать команды из файла в область (алгоритм Ё Ё loadreg); Ё Ё изменить режим защиты области в записи частной таблицы Ё Ё областей процесса на "read-only"; Ё Ё снять с области блокировку; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.23. Алгоритм выделения областей команд файл, связанный с областью. Если бы значение счетчика ссылок ста- ло равным 0, ядро могло бы передать копию индекса в памяти друго- му файлу, тем самым делая сомнительным значение указателя на индекс в записи таблицы областей: если бы пользователю пришлось исполнить новый файл, используя функцию exec, ядро по ошибке свя- зало бы его с областью команд старого файла. Эта проблема устра- няется благодаря тому, что ядро при выполнении алгоритма allocreg увеличивает значение счетчика ссылок на индекс, предупреждая тем самым переназначение индекса в памяти другому файлу. Когда про- цесс во время выполнения функций exit или exec отсоединяет об- ласть команд, ядро уменьшает значение счетчика ссылок на индекс (по алгоритму freereg), если только связь индекса с областью не помечена как "неотъемлемая". Таблица индексов Таблица областей здддддддддддддддд© что могло бы прои- здддддддддддддддд© Ё Ы Ё зойти, если бы счет- Ё Ы Ё Ё Ы Ё чик ссылок на индекс Ё Ы Ё Ё Ы Ё файла /bin/date был Ё Ы Ё Ё Ы Ё равен 0 цдддддддддддддддд╢ Ё Ы Ё Ё область команд Ё Ё Ы Ё Ыд д д д д дЁд для файла Ё Ё Ы Ё Ё Ё /bin/who Ё цдддддддддддддддд╢ Ы цдддддддддддддддд╢ Ё копия индекса дЁд д д д д ды Ё Ы Ё Ё файла /bin/dateЁ Ё Ы Ё Ё в памяти <еддддддддддд© Ё Ы Ё цдддддддддддддддд╢ Ё цдддддддддддддддд╢ Ё Ы Ё Ё Ё область команд Ё Ё Ы Ё юдддддддддддед для файла Ё Ё Ы Ё указатель наЁ /bin/date Ё Ё Ы Ё копию индек-цдддддддддддддддд╢ Ё Ы Ё са в памяти Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юдддддддддддддддды юдддддддддддддддды Рисунок 7.24. Взаимосвязь между таблицей индексов и таблицей областей в случае совместного использования процессами одной области команд Рассмотрим в качестве примера ситуацию, приведенную на Рисун- ке 7.21, где показана взаимосвязь между структурами данных в процессе выполнения функции exec по отношению к файлу "/bin/date" при условии расположения команд и данных файла в раз- ных областях. Когда процесс исполняет файл "/bin/date" первый раз, ядро назначает для команд файла точку входа в таблице облас- тей (Рисунок 7.24) и по завершении выполнения функции exec остав- ляет счетчик ссылок на индекс равным 1. Когда файл "/bin/date" завершается, ядро запускает алгоритмы detachreg и freereg, сбра- сывая значение счетчика ссылок в 0. Однако, если ядро в первом случае не увеличило значение счетчика, оно по завершении функции exec останется равным 0 и индекс на всем протяжении выполнения процесса будет находиться в списке свободных индексов. Предполо- жим, что в это время свободный индекс понадобился процессу, за- пустившему с помощью функции exec файл "/bin/who", тогда ядро мо- жет выделить этому процессу индекс, ранее принадлежавший файлу "/ bin/date". Просматривая таблицу областей в поисках индекса файла "/bin/who", ядро вместо него выбрало бы индекс файла "/bin/date". Считая, что область содержит команды файла "/bin/who", ядро ис- полнило бы совсем не ту программу. Поэтому значение счетчика ссы- лок на индекс активного файла, связанного с разделяемой областью команд, должно быть не меньше единицы, чтобы ядро не могло пере- назначить индекс другому файлу. Возможность совместного использования различными процессами одних и тех же областей команд позволяет экономить время, затра- чиваемое на запуск программы с помощью функции exec. Администра- торы системы могут с помощью системной функции (и команды) chmod устанавливать для часто исполняемых файлов режим "sticky-bit", сущность которого заключается в следующем. Когда процесс исполня- ет файл, для которого установлен режим "sticky-bit", ядро не ос- вобождает область памяти, отведенную под команды файла, отсоеди- няя область от процесса во время выполнения функций exit или exec, даже если значение счетчика ссылок на индекс становится равным 0. Ядро оставляет область команд в первоначальном виде, при этом значение счетчика ссылок на индекс равно 1, пусть даже область не подключена больше ни к одному из процессов. Если же файл будет еще раз запущен на выполнение (уже другим процессом), ядро в таблице областей обнаружит запись, соответствующую области с командами файла. Процесс затратит на запуск файла меньше време- ни, так как ему не придется читать команды из файловой системы. Если команды файла все еще находятся в памяти, в их перемещении не будет необходимости; если же команды выгружены во внешнюю па- мять, будет гораздо быстрее загрузить их из внешней памяти, чем из файловой системы (см. об этом в главе 9). Ядро удаляет из таблицы областей записи, соответствующие об- ластям с командами файла, для которого установлен режим "sticky-bit" (иными словами, когда область помечена как "неотъем- лемая" часть файла или процесса), в следующих случаях: 1. Если процесс открыл файл для записи, в результате соответствующих операций содержимое файла изменится, при этом будет затронуто и содержимое области. 2. Если процесс изменил права доступа к файлу (chmod), отменив режим "sticky-bit", файл не должен оставаться в таблице об- ластей. 3. Если процесс разорвал связь с файлом (unlink), он не сможет больше исполнять этот файл, поскольку у файла не будет точки входа в файловую систему; следовательно, и все остальные про- цессы не будут иметь доступа к записи в таблице областей, со- ответствующей файлу. Поскольку область с командами файла больше не используется, ядро может освободить ее вместе с ос- тальными ресурсами, занимаемыми файлом. 4. Если процесс демонтирует файловую систему, файл перестает быть доступным и ни один из процессов не может его исполнить. В остальном - все как в предыдущем случае. 5. Если ядро использовало уже все пространство внешней памяти, отведенное под выгрузку задач, оно пытается освободить часть памяти за счет областей, имеющих пометку "sticky-bit", но не используемых в настоящий момент. Несмотря на то, что эти об- ласти могут вскоре понадобиться другим процессам, потребности ядра являются более срочными. В первых двух случаях область команд с пометкой "sticky-bit" должна быть освобождена, поскольку она больше не отражает текущее состояние файла. В остальных случаях это делается из практических соображений. Конечно же ядро освобождает область только при том условии, что она не используется ни одним из выполняющихся про- цессов (счетчик ссылок на нее имеет нулевое значение); в против- ном случае это привело бы к аварийному завершению выполнения сис- темных функций open, unlink и umount (случаи 1, 3 и 4, соответс- твенно). Если процесс запускает с помощью функции exec самого себя, алгоритм выполнения функции несколько усложняется. По команде sh script командный процессор shell порождает новый процесс (новую ветвь), который инициирует запуск shell'а (с помощью функции exec) и ис- полняет команды файла "script". Если процесс запускает самого се- бя и при этом его область команд допускает совместное использова- ние, ядру придется следить за тем, чтобы при обращении ветвей процесса к индексам и областям не возникали взаимные блокировки. Иначе говоря, ядро не может, не снимая блокировки со "старой" об- ласти команд, попытаться заблокировать "новую" область, поскольку на самом деле это одна и та же область. Вместо этого ядро просто оставляет "старую" область команд присоединенной к процессу, так как в любом случае ей предстоит повторное использование. Обычно процессы вызывают функцию exec после функции fork; та- ким образом, во время выполнения функции fork процесс-потомок копирует адресное пространство своего родителя, но сбрасывает его во время выполнения функции exec и по сравнению с родителем ис- полняет образ уже другой программы. Не было бы более естественным объединить две системные функции в одну, которая бы загружала программу и исполняла ее под видом нового процесса ? Ричи выска- зал предположение, что возникновение fork и exec как отдельных системных функций обязано тому, что при создании системы UNIX функция fork была добавлена к уже существующему образу ядра сис- темы (см. [Ritchie 84a], стр.1584). Однако, разделение fork и exec важно и с функциональной точки зрения, поскольку в этом слу- чае процессы могут работать с дескрипторами файлов стандартного ввода-вывода независимо, повышая тем самым "элегантность" исполь- зования каналов. Пример, показывающий использование этой возмож- ности, приводится в разделе 7.8. G7.6 КОД ИДЕНТИФИКАЦИИ ПОЛЬЗОВАТЕЛЯ ПРОЦЕССАH Ядро связывает с процессом два кода идентификации пользовате- ля, не зависящих от кода идентификации процесса: реальный (дейс- твительный) код идентификации пользователя и исполнительный код или setuid (от "set user ID" - установить код идентификации поль- зователя, под которым процесс будет исполняться). Реальный код идентифицирует пользователя, несущего ответственность за выполня- ющийся процесс. Исполнительный код используется для установки прав собственности на вновь создаваемые файлы, для проверки прав доступа к файлу и разрешения на посылку сигналов процессам через функцию kill. Процессы могут изменять исполнительный код, запус- кая с помощью функции exec программу setuid или запуская функцию setuid в явном виде. Программа setuid представляет собой исполняемый файл, имеющий в поле режима доступа установленный бит setuid. Когда процесс за- пускает программу setuid на выполнение, ядро записывает в поля, содержащие реальные коды идентификации, в таблице процессов и в пространстве процесса код идентификации владельца файла. Чтобы как-то различать эти поля, назовем одно из них, которое хранится в таблице процессов, сохраненным кодом идентификации пользовате- ля. Рассмотрим пример, иллюстрирующий разницу в содержимом этих полей. Синтаксис вызова системной функции setuid: setuid(uid) где uid - новый код идентификации пользователя. Результат выпол- нения функции зависит от текущего значения реального кода иденти- фикации. Если реальный код идентификации пользователя процесса, вызывающего функцию, указывает на суперпользователя, ядро записы- вает значение uid в поля, хранящие реальный и исполнительный коды идентификации, в таблице процессов и в пространстве процесса. Ес- ли это не так, ядро записывает uid в качестве значения исполни- тельного кода идентификации в пространстве процесса и то только в том случае, если значение uid равно значению реального кода или значению сохраненного кода. В противном случае функция возвращает вызывающему процессу ошибку. Процесс наследует реальный и испол- нительный коды идентификации у своего родителя (в результате вы- полнения функции fork) и сохраняет их значения после вызова функ- ции exec. На Рисунке 7.25 приведена программа, демонстрирующая исполь- зование функции setuid. Предположим, что исполняемый файл, полу- ченный в результате трансляции исходного текста программы, имеет владельца с именем "maury" (код идентификации 8319) и установлен- ный бит setuid; право его исполнения предоставлено всем пользова- телям. Допустим также, что пользователи "mjb" (код идентификации 5088) и "maury" являются владельцами файлов с теми же именами, каждый из которых доступен только для чтения и только своему вла- дельцу. Во время исполнения программы пользователю "mjb" выводит- ся следующая информация: uid 5088 euid 8319 fdmjb -1 fdmaury 3 after setuid(5088): uid 5088 euid 5088 fdmjb 4 fdmaury -1 after setuid(8319): uid 5088 euid 8319 Системные функции getuid и geteuid возвращают значения реального и исполнительного кодов идентификации пользователей процесса, для здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main() Ё Ё { Ё Ё int uid,euid,fdmjb,fdmaury; Ё Ё Ё Ё uid = getuid(); /* получить реальный UID */ Ё Ё euid = geteuid(); /* получить исполнительный UID */Ё Ё printf("uid %d euid %d\n",uid,euid); Ё Ё Ё Ё fdmjb = open("mjb",O_RDONLY); Ё Ё fdmaury = open("maury",O_RDONLY); Ё Ё printf("fdmjb %d fdmaury %d\n",fdmjb,fdmaury); Ё Ё Ё Ё setuid(uid); Ё Ё printf("after setuid(%d): uid %d euid %d\n",uid, Ё Ё getuid(),geteuid()); Ё Ё Ё Ё fdmjb = open("mjb",O_RDONLY); Ё Ё fdmaury = open("maury",O_RDONLY); Ё Ё printf("fdmjb %d fdmaury %d\n",fdmjb,fdmaury); Ё Ё Ё Ё setuid(uid); Ё Ё printf("after setuid(%d): uid %d euid %d\n",euid, Ё Ё getuid(),geteuid()); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.25. Пример выполнения программы setuid пользователя "mjb" это, соответственно, 5088 и 8319. Поэтому про- цесс не может открыть файл "mjb" (ибо он имеет исполнительный код идентификации пользователя (8319), не разрешающий производить чтение файла), но может открыть файл "maury". После вызова функ- ции setuid, в результате выполнения которой в поле исполнительно- го кода идентификации пользователя ("mjb") заносится значение ре- ального кода идентификации, на печать выводятся значения и того, и другого кода идентификации пользователя "mjb": оба равны 5088. Теперь процесс может открыть файл "mjb", поскольку он исполняется под кодом идентификации пользователя, имеющего право на чтение из файла, но не может открыть файл "maury". Наконец, после занесения в поле исполнительного кода идентификации значения, сохраненного функцией setuid (8319), на печать снова выводятся значения 5088 и 8319. Мы показали, таким образом, как с помощью программы setuid процесс может изменять значение кода идентификации пользователя, под которым он исполняется. Во время выполнения программы пользователем "maury" на печать выводится следующая информация: uid 8319 euid 8319 fdmjb -1 fdmaury 3 after setuid(8319): uid 8319 euid 8319 fdmjb -1 fdmaury 4 after setuid(8319): uid 8319 euid 8319 Реальный и исполнительный коды идентификации пользователя во вре- мя выполнения программы остаются равны 8319: процесс может отк- рыть файл "maury", но не может открыть файл "mjb". Исполнительный код, хранящийся в пространстве процесса, занесен туда в результа- те последнего исполнения функции или программы setuid; только его значением определяются права доступа процесса к файлу. С помощью функции setuid исполнительному коду может быть присвоено значение сохраненного кода (из таблицы процессов), т.е. то значение, кото- рое исполнительный код имел в самом начале. Примером программы, использующей вызов системной функции setuid, может служить программа регистрации пользователей в сис- теме (login). Параметром функции setuid при этом является код идентификации суперпользователя, таким образом, программа login исполняется под кодом суперпользователя из корня системы. Она запрашивает у пользователя различную информацию, например, имя и пароль, и если эта информация принимается системой, программа за- пускает функцию setuid, чтобы установить значения реального и ис- полнительного кодов идентификации в соответствии с информацией, поступившей от пользователя (при этом используются данные файла "/etc/passwd"). В заключение программа login инициирует запуск командного процессора shell, который будет исполняться под ука- занными пользовательскими кодами идентификации. Примером setuid-программы является программа, реализующая ко- манду mkdir. В разделе 5.8 уже говорилось о том, что создать каталог может только процесс, выполняющийся под управлением су- перпользователя. Для того, чтобы предоставить возможность созда- ния каталогов простым пользователям, команда mkdir была выполнена в виде setuid-программы, принадлежащей корню системы и имеющей права суперпользователя. На время исполнения команды mkdir про- цесс получает права суперпользователя, создает каталог, используя функцию mknod, и предоставляет права собственности и доступа к каталогу истинному пользователю процесса. 7.7 ИЗМЕНЕНИЕ РАЗМЕРА ПРОЦЕССА С помощью системной функции brk процесс может увеличивать и уменьшать размер области данных. Синтаксис вызова функции: brk(endds); где endds - старший виртуальный адрес области данных процесса (адрес верхней границы). С другой стороны, пользователь может об- ратиться к функции следующим образом: oldendds = sbrk(increment); где oldendds - текущий адрес верхней границы области, increment - число байт, на которое изменяется значение oldendds в результате выполнения функции. Sbrk - это имя стандартной библиотечной подп- рограммы на Си, вызывающей функцию brk. Если размер области дан- ных процесса в результате выполнения функции увеличивается, вновь выделяемое пространство имеет виртуальные адреса, смежные с адре- сами увеличиваемой области; таким образом, виртуальное адресное пространство процесса расширяется. При этом ядро проверяет, не превышает ли новый размер процесса максимально-допустимое значе- ние, принятое для него в системе, а также не накладывается ли но- вая область данных процесса на виртуальное адресное пространство, отведенное ранее для других целей (Рисунок 7.26). Если все в по- рядке, ядро запускает алгоритм growreg, присоединяя к области данных внешнюю память (например, таблицы страниц) и увеличивая значение поля, описывающего размер процесса. В системе с замеще- нием страниц ядро также отводит под новую область пространство основной памяти и обнуляет его содержимое; если свободной памяти нет, ядро освобождает память путем выгрузки процесса (более под- робно об этом мы поговорим в главе 9). Если с помощью функции brk процесс уменьшает размер области данных, ядро освобождает часть ранее выделенного адресного пространства; когда процесс попытает- ся обратиться к данным по виртуальным адресам, принадлежащим ос- вобожденному пространству, он столкнется с ошибкой адресации. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм brk Ё Ё входная информация: новый адрес верхней границы области Ё Ё данных Ё Ё выходная информация: старый адрес верхней границы области Ё Ё данных Ё Ё { Ё Ё заблокировать область данных процесса; Ё Ё если (размер области увеличивается) Ё Ё если (новый размер области имеет недопустимое зна-Ё Ё чение) Ё Ё { Ё Ё снять блокировку с области; Ё Ё вернуть (ошибку); Ё Ё } Ё Ё изменить размер области (алгоритм growreg); Ё Ё обнулить содержимое присоединяемого пространства; Ё Ё снять блокировку с области данных; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.26. Алгоритм выполнения функции brk На Рисунке 7.27 приведен пример программы, использующей функ- цию brk, и выходные данные, полученные в результате ее прогона на машине AT&T 3B20. Вызвав функцию signal и распорядившись прини- мать сигналы о нарушении сегментации (segmentation violation), процесс обращается к подпрограмме sbrk и выводит на печать перво- начальное значение адреса верхней границы области данных. Затем в цикле, используя счетчик символов, процесс заполняет область дан- ных до тех пор, пока не обратится к адресу, расположенному за пределами области, тем самым давая повод для сигнала о нарушении сегментации. Получив сигнал, функция обработки сигнала вызывает подпрограмму sbrk для того, чтобы присоединить к области дополни- тельно 256 байт памяти; процесс продолжается с точки прерывания, заполняя информацией вновь выделенное пространство памяти и т.д. На машинах со страничной организацией памяти, таких как 3B20, наблюдается интересный феномен. Страница является наименьшей еди- ницей памяти, с которой работают механизмы аппаратной защиты, по- этому аппаратные средства не в состоянии установить ошибку в гра- ничной ситуации, когда процесс пытается записать информацию по адресам, превышающим верхнюю границу области данных, но принадле- жащим т.н. "полулегальной" странице (странице, не полностью заня- той областью данных процесса). Это видно из результатов выполне- ния программы, выведенных на печать (Рисунок 7.27): первый раз подпрограмма sbrk возвращает значение 140924, то есть адрес, не дотягивающий 388 байт до конца страницы, которая на машине 3B20 имеет размер 2 Кбайта. Однако процесс получит ошибку только в том случае, если обратится к следующей странице памяти, то есть к лю- бому адресу, начиная с 141312. Функция обработки сигнала прибав- ляет к адресу верхней границы области 256, делая его равным 141180 и, таким образом, оставляя его в пределах текущей страни- цы. Следовательно, процесс тут же снова получит ошибку, выдав на печать адрес 141312. Исполнив подпрограмму sbrk еще раз, ядро вы- деляет под данные процесса новую страницу памяти, так что процесс получает возможность адресовать дополнительно 2 Кбайта памяти, до адреса 143360, даже если верхняя граница области располагается ниже. Получив ошибку, процесс должен будет восемь раз обратиться к подпрограмме sbrk, прежде чем сможет продолжить выполнение ос- новной программы. Таким образом, процесс может иногда выходить за официальную верхнюю границу области данных, хотя это и нежела- тельный момент в практике программирования. Когда стек задачи переполняется, ядро автоматически увеличи- вает его размер, выполняя алгоритм, похожий на алгоритм функции brk. Первоначально стек задачи имеет размер, достаточный для хра- нения параметров функции exec, однако при выполнении процесса зддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё char *cp; Ё Ё int callno; Ё Ё Ё Ё main() Ё Ё { Ё Ё char *sbrk(); Ё Ё extern catcher(); Ё Ё Ё Ё signal(SIGSEGV,catcher); Ё Ё cp = sbrk(0); Ё Ё printf("original brk value %u\n",cp); Ё Ё for (;;) Ё Ё *cp++ = 1; Ё Ё } Ё Ё Ё Ё catcher(signo); Ё Ё int signo; Ё Ё { Ё Ё callno++; Ё Ё printf("caught sig %d %dth call at addr %u\n", Ё Ё signo,callno,cp); Ё Ё sbrk(256); Ё Ё signal(SIGSEGV,catcher); Ё Ё } Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддддды зддддддддддддддддддддддддддддддддддддддддддд© Ё original brk value 140924 Ё Ё caught sig 11 1th call at addr 141312 Ё Ё caught sig 11 2th call at addr 141312 Ё Ё caught sig 11 3th call at addr 143360 Ё Ё ...(тот же адрес печатается до 10-го Ё Ё вызова подпрограммы sbrk) Ё Ё caught sig 11 10th call at addr 143360 Ё Ё caught sig 11 11th call at addr 145408 Ё Ё ...(тот же адрес печатается до 18-го Ё Ё вызова подпрограммы sbrk) Ё Ё caught sig 11 18th call at addr 145408 Ё Ё caught sig 11 19th call at addr 145408 Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.27. Пример программы, использующей функцию brk, и результаты ее контрольного прогона этот стек может переполниться. Переполнение стека приводит к ошибке адресации, свидетельствующей о попытке процесса обратиться к ячейке памяти за пределами отведенного адресного пространства. Ядро устанавливает причину возникновения ошибки, сравнивая теку- щее значение указателя вершины стека с размером области стека. При расширении области стека ядро использует точно такой же меха- низм, что и для области данных. На выходе из прерывания процесс здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё /* чтение командной строки до символа конца файла */ Ё Ё while (read(stdin,buffer,numchars)) Ё Ё { Ё Ё /* синтаксический разбор командной строки */ Ё Ё if (/* командная строка содержит & */) Ё Ё amper = 1; Ё Ё else Ё Ё amper = 0; Ё Ё /* для команд, не являющихся конструкциями командного Ё Ё языка shell */ Ё Ё if (fork() == 0) Ё Ё { Ё Ё /* переадресация ввода-вывода ? */ Ё Ё if (/* переадресация вывода */) Ё Ё { Ё Ё fd = creat(newfile,fmask); Ё Ё close(stdout); Ё Ё dup(fd); Ё Ё close(fd); Ё Ё /* stdout теперь переадресован */ Ё Ё } Ё Ё if (/* используются каналы */) Ё Ё { Ё Ё pipe(fildes); Ё Ё Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.28. Основной цикл программы shell имеет область стека необходимого для продолжения работы размера. 7.8 КОМАНДНЫЙ ПРОЦЕССОР SHELL Теперь у нас есть достаточно материала, чтобы перейти к объ- яснению принципов работы командного процессора shell. Сам команд- ный процессор намного сложнее, чем то, что мы о нем здесь будем излагать, однако взаимодействие процессов мы уже можем рассмот- реть на примере реальной программы. На Рисунке 7.28 приведен фрагмент основного цикла программы shell, демонстрирующий асинх- ронное выполнение процессов, переназначение вывода и использова- ние каналов. Shell считывает командную строку из файла стандартного ввода и интерпретирует ее в соответствии с установленным набором пра- вил. Дескрипторы файлов стандартного ввода и стандартного вывода, используемые регистрационным shell'ом, как правило, указывают на терминал, с которого пользователь регистрируется в системе (см. главу 10). Если shell узнает во введенной строке конструкцию собственного командного языка (например, одну из команд cd, for, while и т.п.), он исполняет команду своими силами, не прибегая к созданию новых процессов; в противном случае команда интерпрети- руется как имя исполняемого файла. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё if (fork() == 0) Ё Ё { Ё Ё /* первая компонента командной строки */Ё Ё close(stdout); Ё Ё dup(fildes[1]); Ё Ё close(fildes[1]); Ё Ё close(fildes[0]); Ё Ё /* стандартный вывод направляется в ка- Ё Ё нал */ Ё Ё /* команду исполняет порожденный про- Ё Ё цесс */ Ё Ё execlp(command1,command1,0); Ё Ё } Ё Ё /* вторая компонента командной строки */ Ё Ё close(stdin); Ё Ё dup(fildes[0]); Ё Ё close(fildes[0]); Ё Ё close(fildes[1]); Ё Ё /* стандартный ввод будет производиться изЁ Ё канала */ Ё Ё } Ё Ё execve(command2,command2,0); Ё Ё } Ё Ё /* с этого места продолжается выполнение родительского Ё Ё * процесса... Ё Ё * процесс-родитель ждет завершения выполнения потомка,Ё Ё * если это вытекает из введенной строки Ё Ё * / Ё Ё if (amper == 0) Ё Ё retid = wait(&status); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.28. Основной цикл программы shell (продолжение) Командные строки простейшего вида содержат имя программы и несколько параметров, например: who grep -n include *.c ls -l Shell "ветвится" (fork) и порождает новый процесс, который и за- пускает программу, указанную пользователем в командной строке. Родительский процесс (shell) дожидается завершения потомка и пов- торяет цикл считывания следующей команды. Если процесс запускается асинхронно (на фоне основной прог- раммы), как в следующем примере nroff -mm bigdocument & shell анализирует наличие символа амперсанд (&) и заносит резуль- тат проверки во внутреннюю переменную amper. В конце основного цикла shell обращается к этой переменной и, если обнаруживает в ней признак наличия символа, не выполняет функцию wait, а тут же повторяет цикл считывания следующей команды. Из рисунка видно, что процесс-потомок по завершении функции fork получает доступ к командной строке, принятой shell'ом. Для того, чтобы переадресовать стандартный вывод в файл, как в следу- ющем примере nroff -mm bigdocument > output процесс-потомок создает файл вывода с указанным в командной стро- ке именем; если файл не удается создать (например, не разрешен доступ к каталогу), процесс-потомок тут же завершается. В против- ном случае процесс-потомок закрывает старый файл стандартного вы- вода и переназначает с помощью функции dup дескриптор этого файла новому файлу. Старый дескриптор созданного файла закрывается и сохраняется для запускаемой программы. Подобным же образом shell переназначает и стандартный ввод и стандартный вывод ошибок. зддддддддддд© Ё Ё Ё Shell Ё Ё Ё юдддддбддддды wait Ё ^ Ё Ё здддддаддддд© exit Ё Ё Ё wc Ё Ё Ё юдддддбддддды read Ё ^ Ё Ё здддддаддддд© write Ё Ё Ё ls - l Ё Ё Ё юддддддддддды Рисунок 7.29. Взаимосвязь между процессами, исполняющими ко- мандную строку ls -lЁwc Из приведенного текста программы видно, как shell обрабатыва- ет командную строку, используя один канал. Допустим, что команд- ная строка имеет вид: ls -lЁwc После создания родительским процессом нового процесса процесс-по- томок создает канал. Затем процесс-потомок создает свое ответвление; он и его потомок обрабатывают по одной компоненте командной строки. "Внучатый" процесс исполняет первую компоненту строки (ls): он собирается вести запись в канал, поэтому он зак- рывает старый файл стандартного вывода, передает его дескриптор каналу и закрывает старый дескриптор записи в канал, в котором (в дескрипторе) уже нет необходимости. Родитель (wc) "внучатого" процесса (ls) является потомком основного процесса, реализующего программу shell'а (см. Рисунок 7.29). Этот процесс (wc) закрывает свой файл стандартного ввода и передает его дескриптор каналу, в результате чего канал становится файлом стандартного ввода. Затем закрывается старый и уже не нужный дескриптор чтения из канала и исполняется вторая компонента командной строки. Оба порожденных процесса выполняются асинхронно, причем выход одного процесса поступает на вход другого. Тем временем основной процесс дожида- ется завершения своего потомка (wc), после чего продолжает свою обычную работу: по завершении процесса, выполняющего команду wc, вся командная строка является обработанной. Shell возвращается в цикл и считывает следующую командную строку. 7.9 ЗАГРУЗКА СИСТЕМЫ И НАЧАЛЬНЫЙ ПРОЦЕСС Для того, чтобы перевести систему из неактивное состояние в активное, администратор выполняет процедуру "начальной загрузки". На разных машинах эта процедура имеет свои особенности, однако во всех случаях она реализует одну и ту же цель: загрузить копию операционной системы в основную память машины и запустить ее на исполнение. Обычно процедура начальной загрузки включает в себя несколько этапов. Переключением клавиш на пульте машины админист- ратор может указать адрес специальной программы аппаратной заг- рузки, а может, нажав только одну клавишу, дать команду машине запустить процедуру загрузки, исполненную в виде микропрограммы. Эта программа может состоять из нескольких команд, подготавливаю- щих запуск другой программы. В системе UNIX процедура начальной загрузки заканчивается считыванием с диска в память блока началь- ной загрузки (нулевого блока). Программа, содержащаяся в этом блоке, загружает из файловой системы ядро ОС (например, из файла с именем "/unix" или с другим именем, указанным администратором). После загрузки ядра системы в память управление передается по стартовому адресу ядра и ядро запускается на выполнение (алгоритм start, Рисунок 7.30). Ядро инициализирует свои внутренние структуры данных. Среди прочих структур ядро создает связные списки свободных буферов и индексов, хеш-очереди для буферов и индексов, инициализирует структуры областей, точки входа в таблицы страниц и т.д. По окон- чании этой фазы ядро монтирует корневую файловую систему и форми- рует среду выполнения нулевого процесса, среди всего прочего соз- давая пространство процесса, инициализируя нулевую точку входа в таблице процесса и делая корневой каталог текущим для процесса. Когда формирование среды выполнения процесса заканчивается, система исполняется уже в виде нулевого процесса. Нулевой процесс "ветвится", запуская алгоритм fork прямо из ядра, поскольку сам процесс исполняется в режиме ядра. Порожденный нулевым новый про- цесс, процесс 1, запускается в том же режиме и создает свой поль- зовательский контекст, формируя область данных и присоединяя ее к своему адресному пространству. Он увеличивает размер области до надлежащей величины и переписывает программу загрузки из адресно- го пространства ядра в новую область: эта программа теперь будет определять контекст процесса 1. Затем процесс 1 сохраняет регист- ровый контекст задачи, "возвращается" из режима ядра в режим за- дачи и исполняет только что переписанную программу. В отличие от нулевого процесса, который является процессом системного уровня, выполняющимся в режиме ядра, процесс 1 относится к пользователь- скому уровню. Код, исполняемый процессом 1, включает в себя вызов системной функции exec, запускающей на выполнение программу из файла "/etc/init". Обычно процесс 1 именуется процессом init, поскольку он отвечает за инициализацию новых процессов. Казалось бы, зачем ядру копировать программу, запускаемую с помощью функции exec, в адресное пространство процесса 1 ? Он мог бы обратиться к внутреннему варианту функции прямо из ядра, одна- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм start /* процедура начальной загрузки системы */Ё Ё входная информация: отсутствует Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё проинициализировать все структуры данных ядра; Ё Ё псевдо-монтирование корня; Ё Ё сформировать среду выполнения процесса 0; Ё Ё создать процесс 1; Ё Ё { Ё Ё /* процесс 1 */ Ё Ё выделить область; Ё Ё подключить область к адресному пространству процессаЁ Ё init; Ё Ё увеличить размер области для копирования в нее ис- Ё Ё полняемого кода; Ё Ё скопировать из пространства ядра в адресное прост- Ё Ё ранство процесса код программы, исполняемой процес-Ё Ё сом; Ё Ё изменить режим выполнения: вернуться из режима ядра Ё Ё в режим задачи; Ё Ё /* процесс init далее выполняется самостоятельно -- Ё Ё * в результате выхода в режим задачи, Ё Ё * init исполняет файл "/etc/init" и становится Ё Ё * "обычным" пользовательским процессом, производя- Ё Ё * щим обращения к системным функциям Ё Ё */ Ё Ё } Ё Ё /* продолжение нулевого процесса */ Ё Ё породить процессы ядра; Ё Ё /* нулевой процесс запускает программу подкачки, управ- Ё Ё * ляющую распределением адресного пространства процес- Ё Ё * сов между основной памятью и устройствами выгрузки. Ё Ё * Это бесконечный цикл; нулевой процесс обычно приоста-Ё Ё * навливает свою работу, если необходимости в нем боль-Ё Ё * ше нет. Ё Ё */ Ё Ё исполнить программу, реализующую алгоритм подкачки; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.30. Алгоритм загрузки системы ко, по сравнению с уже описанным алгоритмом это было бы гораздо труднее реализовать, ибо в этом случае функции exec пришлось бы производить анализ имен файлов в пространстве ядра, а не в прост- ранстве задачи. Подобная деталь, требующаяся только для процесса init, усложнила бы программу реализации функции exec и отрица- тельно отразилась бы на скорости выполнения функции в более общих случаях. Процесс init (Рисунок 7.31) выступает диспетчером процессов, который порождает процессы, среди всего прочего позволяющие поль- зователю регистрироваться в системе. Инструкции о том, какие про- цессы нужно создать, считываются процессом init из файла "/etc/inittab". Строки файла включают в себя идентификатор состо- яния "id" (однопользовательский режим, многопользовательский и т. д.), предпринимаемое действие (см. упражнение 7.43) и специфика- цию программы, реализующей это действие (см. Рисунок 7.32). Про- цесс init просматривает строки файла до тех пор, пока не обнару- жит идентификатор состояния, соответствующего тому состоянию, в котором находится процесс, и создает процесс, исполняющий прог- рамму с указанной спецификацией. Например, при запуске в много- пользовательском режиме (состояние 2) процесс init обычно порож- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм init /* процесс init, в системе именуемый Ё Ё "процесс 1" */ Ё Ё входная информация: отсутствует Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё fd = open("/etc/inittab",O_RDONLY); Ё Ё while (line_read(fd,buffer)) Ё Ё { Ё Ё /* читать каждую строку файлу */ Ё Ё if (invoked state != buffer state) Ё Ё continue; /* остаться в цикле while */ Ё Ё /* найден идентификатор соответствующего состояния Ё Ё */ Ё Ё if (fork() == 0) Ё Ё { Ё Ё execl("процесс указан в буфере"); Ё Ё exit(); Ё Ё } Ё Ё /* процесс init не дожидается завершения потомка */ Ё Ё /* возврат в цикл while */ Ё Ё } Ё Ё Ё Ё while ((id = wait((int*) 0)) != -1) Ё Ё { Ё Ё /* проверка существования потомка; Ё Ё * если потомок прекратил существование, рассматри- Ё Ё * вается возможность его перезапуска */ Ё Ё /* в противном случае, основной процесс просто про- Ё Ё * должает работу */ Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.31. Алгоритм выполнения процесса init здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё Формат: идентификатор, состояние, действие, спецификация Ё Ё процесса Ё Ё Поля разделены между собой двоеточиями Ё Ё Комментарии в конце строки начинаются с символа '#' Ё Ё Ё Ё co::respawn:/etc/getty console console #Консоль в машзалеЁ Ё 46:2:respawn:/etc/getty -t 60 tty46 4800H #комментарии Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.32. Фрагмент файла inittab дает getty-процессы, управляющие функционированием терминальных линий, входящих в состав системы. Если регистрация пользователя прошла успешно, getty-процесс, пройдя через процедуру login, за- пускает на исполнение регистрационный shell (см. главу 10). Тем временем процесс init находится в состоянии ожидания (wait), наб- людая за прекращением существования своих потомков, а также "вну- чатых" процессов, оставшихся "сиротами" после гибели своих роди- телей. Процессы в системе UNIX могут быть либо пользовательскими, либо управляющими, либо системными. Большинство из них составляют пользовательские процессы, связанные с пользователями через тер- миналы. Управляющие процессы не связаны с конкретными пользовате- лями, они выполняют широкий спектр системных функций, таких как администрирование и управление сетями, различные периодические операции, буферизация данных для вывода на устройство построчной печати и т.д. Процесс init может порождать управляющие процессы, которые будут существовать на протяжении всего времени жизни сис- темы, в различных случаях они могут быть созданы самими пользова- телями. Они похожи на пользовательские процессы тем, что они ис- полняются в режиме задачи и прибегают к услугам системы путем вызова соответствующих системных функций. Системные процессы выполняются исключительно в режиме ядра. Они могут порождаться нулевым процессом (например, процесс заме- щения страниц vhand), который затем становится процессом подкач- ки. Системные процессы похожи на управляющие процессы тем, что они выполняют системные функции, при этом они обладают большими возможностями приоритетного выполнения, поскольку лежащие в их основе программные коды являются составной частью ядра. Они могут обращаться к структурам данных и алгоритмам ядра, не прибегая к вызову системных функций, отсюда вытекает их исключительность. Однако, они не обладают такой же гибкостью, как управляющие про- цессы, поскольку для того, чтобы внести изменения в их программы, придется еще раз перекомпилировать ядро. 7.10 ВЫВОДЫ В данной главе были рассмотрены системные функции, предназна- ченные для работы с контекстом процесса и для управления выполне- нием процесса. Системная функция fork создает новый процесс, ко- пируя для него содержимое всех областей, подключенных к родительскому процессу. Особенность реализации функции fork сос- тоит в том, что она выполняет инициализацию сохраненного регист- рового контекста порожденного процесса, таким образом этот про- цесс начинает выполняться, не дожидаясь завершения функции, и уже в теле функции начинает осознавать свою предназначение как потом- ка. Все процессы завершают свое выполнение вызовом функции exit, которая отсоединяет области процесса и посылает его родителю сиг- нал "гибель потомка". Процесс-родитель может совместить момент продолжения своего выполнения с моментом завершения процесса-по- томка, используя системную функцию wait. Системная функция exec дает процессу возможность запускать на выполнение другие програм- мы, накладывая содержимое исполняемого файла на свое адресное пространство. Ядро отсоединяет области, ранее занимаемые процес- сом, и назначает процессу новые области в соответствии с потреб- ностями исполняемого файла. Совместное использование областей ко- манд и наличие режима "sticky-bit" дают возможность более рацио- нально использовать память и экономить время, затрачиваемое на подготовку к запуску программ. Простым пользователям предоставля- ется возможность получать привилегии других пользователей, даже суперпользователя, благодаря обращению к услугам системной функ- ции setuid и setuid-программ. С помощью функции brk процесс может изменять размер своей области данных. Функция signal дает процес- сам возможность управлять своей реакцией на поступающие сигналы. При получении сигнала производится обращение к специальной функ- ции обработки сигнала с внесением соответствующих изменений в стек задачи и в сохраненный регистровый контекст задачи. Процессы могут сами посылать сигналы, используя системную функцию kill, они могут также контролировать получение сигналов, предназначен- ных группе процессов, прибегая к услугам функции setpgrp. Командный процессор shell и процесс начальной загрузки init используют стандартные обращения к системным функциям, производя набор операций, в других системах обычно выполняемых ядром. Shell интерпретирует команды пользователя, переназначает стандартные файлы ввода-вывода данных и выдачи ошибок, порождает процессы, организует каналы между порожденными процессами, синхронизирует свое выполнение с этими процессами и формирует коды, возвращаемые командами. Процесс init тоже порождает различные процессы, в частности, управляющие работой пользователя за терминалом. Когда такой процесс завершается, init может породить для выполнения той же самой функции еще один процесс, если это вытекает из информа- ции файла "/etc/inittab". G7.11 УПРАЖНЕНИЯH 1. Запустите с терминала программу, приведенную на Рисунке 7.33. Переадресуйте стандартный вывод данных в файл и срав- ните результаты между собой. здддддддддддддддддддддддддддддддддддд© Ё main() Ё Ё { Ё Ё printf("hello\n"); Ё Ё if (fork() == 0) Ё Ё printf("world\n"); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддды Рисунок 7.33. Пример модуля, содержащего вызов функции fork и обращение к стандарт- ному выводу 2. Разберитесь в механизме работы программы, приведенной на Ри- сунке 7.34, и сравните ее результаты с результатами програм- мы на Рисунке 7.4. 3. Еще раз обратимся к программе, приведенной на Рисунке 7.5 и показывающей, как два процесса обмениваются сообщениями, ис- пользуя спаренные каналы. Что произойдет, если они попытают- ся вести обмен сообщениями, используя один канал ? 4. Возможна ли потеря информации в случае, когда процесс полу- чает несколько сигналов прежде чем ему предоставляется возможность отреагировать на них надлежащим образом ? (Расс- мотрите случай, когда процесс подсчитывает количество полу- ченных сигналов о прерывании.) Есть ли необходимость в реше- нии этой проблемы ? 5. Опишите механизм работы системной функции kill. 6. Процесс в программе на Рисунке 7.35 принимает сигналы типа "гибель потомка" и устанавливает функцию обработки сигналов в исходное состояние. Что происходит при выполнении програм- мы ? 7. Когда процесс получает сигналы определенного типа и не обра- батывает их, ядро дампирует образ процесса в том виде, кото- рый был у него в момент получения сигнала. Ядро создает в текущем каталоге процесса файл с именем "core" и копирует в него пространство процесса, области команд, данных и стека. Впоследствии пользователь может тщательно изучить дамп обра- за процесса с помощью стандартных средств отладки. Опишите алгоритм, которому на Ваш взгляд должно следовать ядро в процессе создания файла "core". Что нужно предпринять в том случае, если в текущем каталоге файл с таким именем уже су- ществует ? Как должно вести себя ядро, когда в одном и том же каталоге дампируют свои образы сразу несколько процессов? 8. Еще раз обратимся к программе (Рисунок 7.12), описывающей, как один процесс забрасывает другой процесс сигналами, кото- рые принимаются их адресатом. Подумайте, что произошло бы в том случае, если бы алгоритм обработки сигналов был перера- ботан в любом из следующих направлений: здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё int fdrd,fdwt; Ё Ё char c; Ё Ё Ё Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё if (argc != 3) Ё Ё exit(1); Ё Ё fork(); Ё Ё Ё Ё if ((fdrd = open(argv[1],O_RDONLY)) == -1) Ё Ё exit(1); Ё Ё if (((fdwt = creat(argv[2],0666)) == -1) && Ё Ё ((fdwt = open(argv[2],O_WRONLY)) == -1)) Ё Ё exit(1); Ё Ё rdwrt(); Ё Ё } Ё Ё rdwrt() Ё Ё { Ё Ё for (;;) Ё Ё { Ё Ё if (read(fdrd,&c,1) != 1) Ё Ё return; Ё Ё write(fdwt,&c,1); Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.34. Пример программы, в которой процесс-родитель и процесс-потомок не разделяют доступ к файлу * ядро не заменяет функцию обработки сигналов до тех пор, пока пользователь явно не потребует этого; * ядро заставляет процесс игнорировать сигналы до тех пор, пока пользователь не обратится к функции signal вновь. 9. Переработайте алгоритм обработки сигналов так, чтобы ядро автоматически перенастраивало процесс на игнорирование всех последующих поступлений сигналов по возвращении из функции, обрабатывающей их. Каким образом ядро может узнать о завер- шении функции обработки сигналов, выполняющейся в режиме за- дачи ? Такого рода перенастройка приблизила бы нас к трак- товке сигналов в системе BSD. *10. Если процесс получает сигнал, находясь в состоянии приостанова во время выполнения системной функции с допуска- ющим прерывания приоритетом, он выходит из функции по алго- ритму longjump. Ядро производит необходимые установки для запуска функции обработки сигнала; когда процесс выйдет из функции обработки сигнала, в версии V это будет выглядеть так, словно он вернулся из системной функции с признаком ошибки (как бы прервав свое выполнение). В системе BSD сис- темная функция в этом случае автоматически перезапускается. Каким образом можно реализовать этот момент в нашей системе? здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё main() Ё Ё { Ё Ё extern catcher(); Ё Ё Ё Ё signal(SIGCLD,catcher); Ё Ё if (fork() == 0) Ё Ё exit(); Ё Ё /* пауза до момента получения сигнала */ Ё Ё pause(); Ё Ё } Ё Ё Ё Ё catcher() Ё Ё { Ё Ё printf("процесс-родитель получил сигнал\n"); Ё Ё signal(SIGCLD,catcher); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.35. Программа, в которой процесс принимает сигналы типа "гибель потомка" 11. В традиционной реализации команды mkdir для создания новой вершины в дереве каталогов используется системная функция mknod, после чего дважды вызывается системная функция link, привязывающая точки входа в каталог с именами "." и ".." к новой вершине и к ее родительскому каталогу. Без этих трех операций каталог не будет иметь надлежащий формат. Что прои- зойдет, если во время исполнения команды mkdir процесс полу- чит сигнал ? Что если при этом будет получен сигнал SIGKILL, который процесс не распознает ? Эту же проблему рассмотрите применительно к реализации системной функции mkdir. 12. Процесс проверяет наличие сигналов в моменты перехода в сос- тояние приостанова и выхода из него (если в состоянии приос- танова процесс находился с приоритетом, допускающим прерыва- ния), а также в момент перехода в режим задачи из режима ядра по завершении исполнения системной функции или после обработки прерывания. Почему процесс не проверяет наличие сигналов в момент обращения к системной функции ? *13. Предположим, что после исполнения системной функции процесс готовится к возвращению в режим задачи и не обнаруживает ни одного необработанного сигнала. Сразу после этого ядро обра- батывает прерывание и посылает процессу сигнал. (Например, пользователем была нажата клавиша "break".) Что делает про- цесс после того, как ядро завершает обработку прерывания ? *14. Если процессу одновременно посылается несколько сигналов, ядро обрабатывает их в том порядке, в каком они перечислены в описании. Существуют три способа реагирования на получение сигнала - прием сигналов, завершение выполнения со сбросом на внешний носитель (дампированием) образа процесса в памяти и завершение выполнения без дампирования. Можно ли указать наилучший порядок обработки одновременно поступающих сигна- лов ? Например, если процесс получает сигнал о выходе (вызы- вающий дампирование образа процесса в памяти) и сигнал о прерывании (выход без дампирования), то какой из этих сигна- лов имело бы смысл обработать первым ? 15. Запомните новую системную функцию newpgrp(pid,ngrp); которая включает процесс с идентификатором pid в группу про- цессов с номером ngrp (устанавливает для процесса новую группу). Подумайте, для каких целей она может использоваться и какие опасности таит в себе ее вызов. 16. Прокомментируйте следующее утверждение: по алгоритму wait процесс может приостановиться до наступления какого-либо со- бытия и это не отразилось бы на работе всей системы. 17. Рассмотрим новую системную функцию nowait(pid); где pid - идентификатор процесса, являющегося потомком того процесса, который вызывает функцию. Вызывая функцию, процесс тем самым сообщает ядру о том, что он не собирается дожи- даться завершения выполнения своего потомка, поэтому ядро может по окончании существования потомка сразу же очистить занимаемое им место в таблице процессов. Каким образом это реализуется на практике ? Оцените достоинства новой функции и сравните ее использование с использованием сигналов типа "гибель потомка". 18. Загрузчик модулей на Си автоматически подключает к основному модулю начальную процедуру (startup), которая вызывает функ- цию main, принадлежащую программе пользователя. Если в поль- зовательской программе отсутствует вызов функции exit, про- цедура startup сама вызывает эту функцию при выходе из функ- ции main. Что произошло бы в том случае, если бы и в проце- дуре startup отсутствовал вызов функции exit (из-за ошибки загрузчика) ? 19. Какую информацию получит процесс, выполняющий функцию wait, если его потомок запустит функцию exit без параметра ? Име- ется в виду, что процесс-потомок вызовет функцию в формате exit() вместо exit(n). Если программист постоянно использует вызов функции exit без параметра, то насколько предсказуемо значение, ожидаемое функцией wait ? Докажите свой ответ. 20. Объясните, что произойдет, если процесс, исполняющий прог- рамму на Рисунке 7.36 запустит с помощью функции exec самого себя. Как в таком случае ядро сможет избежать возникновения тупиковых ситуаций, связанных с блокировкой индексов ? здддддддддддддддддддддддддддддддддд© Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё execl(argv[0],argv[0],0); Ё Ё } Ё юдддддддддддддддддддддддддддддддддды Рисунок 7.36 21. По условию первым аргументом функции exec является имя (пос- ледняя компонента имени пути поиска) исполняемого процессом файла. Что произойдет в результате выполнения программы, приведенной на Рисунке 7.37 ? Каков будет эффект, если в ка- честве файла "a.out" выступит загрузочный модуль, полученный в результате трансляции программы, приведенной на Рисунке 7.36 ? 22. Предположим, что в языке Си поддерживается новый тип данных "read-only" (только для чтения), причем процесс, пытающийся записать информацию в поле с этим типом, получает отказ сис- темы защиты. Опишите реализацию этого момента. (Намек: срав- ните это понятие с понятием "разделяемая область команд".) В какие из алгоритмов ядра потребуется внести изменения ? Ка- кие еще объекты могут быть реализованы аналогичным с об- ластью образом ? 23. Какие изменения имеют место в алгоритмах open, chmod, unlink и unmount при работе с файлами, для которых установлен режим "sticky-bit" ? Какие действия, например, следует предпринять в отношении такого файла ядру, когда с файлом разрывается связь ? 24. Суперпользователь является единственным пользователем, имею- щим право на запись в файл паролей "/etc/passwd", благодаря чему содержимое файла предохраняется от умышленной или слу- чайной порчи. Программа passwd дает пользователям возмож- ность изменять свой собственный пароль, защищая от изменений чужие записи. Каким образом она работает ? зддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё main() Ё Ё { Ё Ё if (fork() == 0) Ё Ё { Ё Ё execl("a.out",0); Ё Ё printf("неудачное завершение функции exec\n");Ё Ё } Ё Ё } Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.37 *25. Поясните, какая угроза безопасности хранения данных возника- ет, если setuid-программа не защищена от записи. 26. Выполните следующую последовательность команд, в которой "a. out" - имя исполняемого файла: chmod 4777 a.out chown root a.out Команда chmod "включает" бит setuid (4 в 4777); пользователь "root" традиционно является суперпользователем. Может ли в результате выполнения этой последовательности произойти на- рушение защиты информации ? 27. Что произойдет в процессе выполнения программы, представлен- ной на Рисунке 7.38 ? Поясните свой ответ. зддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё main() Ё Ё { Ё Ё char *endpt; Ё Ё char *sbrk(); Ё Ё int brk(); Ё Ё Ё Ё endpt = sbrk(0); Ё Ё printf("endpt = %ud после sbrk\n", (int) endpt); Ё Ё Ё Ё while (endpt--) Ё Ё { Ё Ё if (brk(endpt) == -1) Ё Ё { Ё Ё printf("brk с параметром %ud завершилась Ё Ё неудачно\n",endpt); Ё Ё exit(); Ё Ё } Ё Ё } Ё Ё } Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.38 28. Библиотечная подпрограмма malloc увеличивает область данных процесса с помощью функции brk, а подпрограмма free освобож- дает память, выделенную подпрограммой malloc. Синтаксис вы- зова подпрограмм: ptr = malloc(size); free(ptr); где size - целое число без знака, обозначающее количество выделяемых байт памяти, а ptr - символьная ссылка на вновь выделенное пространство. Прежде чем появиться в качестве па- раметра в вызове подпрограммы free, указатель ptr должен быть возвращен подпрограммой malloc. Выполните эти подпрог- раммы. 29. Что произойдет в процессе выполнения программы, представлен- ной на Рисунке 7.39 ? Сравните результаты выполнения этой программы с результатами, предусмотренными в системном опи- сании. зддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё main() Ё Ё { Ё Ё int i; Ё Ё char *cp; Ё Ё extern char *sbrk(); Ё Ё Ё Ё cp = sbrk(10); Ё Ё for (i = 0; i < 10; i++) Ё Ё *cp++ = 'a' + i; Ё Ё sbrk(-10); Ё Ё cp = sbrk(10); Ё Ё for (i = 0; i < 10; i++) Ё Ё printf("char %d = '%c'\n",i,*cp++); Ё Ё } Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 7.39. Пример программы, использующей подпрограмму sbrk 30. Каким образом командный процессор shell узнает о том, что файл исполняемый, когда для выполнения команды создает новый процесс ? Если файл исполняемый, то как узнать, создан ли он в результате трансляции исходной программы или же представ- ляет собой набор команд языка shell ? В каком порядке следу- ет выполнять проверку указанных условий ? 31. В командном языке shell символы ">>" используются для направления вывода данных в файл с указанной спецификацией, например, команда: run >> outfile открывает файл с именем "outfile" (а в случае отсутствия файла с таким именем создает его) и записывает в него дан- ные. Напишите программу, в которой используется эта команда. здддддддддддддддддд© Ё main() Ё Ё { Ё Ё exit(0); Ё Ё } Ё юдддддддддддддддддды Рисунок 7.40 32. Процессор командного языка shell проверяет код, возвращаемый функцией exit, воспринимая нулевое значение как "истину", а любое другое значение как "ложь" (обратите внимание на не- согласованность с языком Си). Предположим, что файл, испол- няющий программу на Рисунке 7.40, имеет имя "truth". Поясни- те, что произойдет, когда shell будет исполнять следующий набор команд: while truth do truth & done 33. Вопрос по Рисунку 7.29: В связи с чем возникает необходи- мость в создании процессов для конвейерной обработки двух- компонентной команды в указанном порядке ? 34. Напишите более общую программу работы основного цикла про- цессора shell в части обработки каналов. Имеется в виду, что программа должна уметь обрабатывать случайное число каналов, указанных в командной строке. 35. Переменная среды PATH описывает порядок, в котором shell'у следует просматривать каталоги в поисках исполняемых файлов. В библиотечных функциях execlp и execvp перечисленные в PATH каталоги присоединяются к именам файлов, кроме тех, которые начинаются с символа "/". Выполните эти функции. *36. Для того, чтобы shell в поисках исполняемых файлов не обра- щался к текущему каталогу, суперпользователь должен задать переменную среды PATH. Какая угроза безопасности хранения данных может возникнуть, если shell попытается исполнить файлы из текущего каталога ? 37. Каким образом shell обрабатывает команду cd (создать ката- лог) ? Какие действия предпринимает shell в процессе обра- ботки следующей командной строки: cd pathname & ? 38. Когда пользователь нажимает на клавиатуре терминала клавиши "delete" или "break", всем процессам, входящим в группу ре- гистрационного shell'а, терминальный драйвер посылает сигнал о прерывании. Пользователь может иметь намерение остановить все процессы, порожденные shell'ом, без выхода из системы. Какие усовершенствования в связи с этим следует произвести в теле основного цикла программы shell (Рисунок 7.28) ? 39. С помощью команды nohup command_line пользователь может отменить действие сигналов о "зависании" и о завершении (quit) в отношении процессов, реализующих ко- мандную строку (command_line). Как эта команда будет обраба- тываться в основном цикле программы shell ? 40. Рассмотрим набор команд языка shell: nroff -mm bigfile1 > big1out & nroff -mm bigfile2 > big2out и вновь обратимся к основному циклу программы shell (Рисунок 7.28). Что произойдет, если выполнение первой команды nroff завершится раньше второй ? Какие изменения следует внести в основной цикл программы shell на этот случай ? 41. Часто во время выполнения из shell'а непротестированных программ появляется сообщение об ошибке следующего вида: "Bus error - core dumped" (Ошибка в магистрали - содержимое памяти сброшено на внешний носитель). Очевидно, что в прог- рамме выполняются какие-то недопустимые действия; откуда shell узнает о том, что ему нужно вывести сообщение об ошиб- ке ? 42. Процессом 1 в системе может выступать только процесс init. Тем не менее, запустив процесс init, администратор системы может тем самым изменить состояние системы. Например, при загрузке система может войти в однопользовательский режим, означающий, что в системе активен только консольный терми- нал. Для того, чтобы перевести процесс init в состояние 2 (многопользовательский режим), администратор системы вводит с консоли команду init 2 . Консольный shell порождает свое ответвление и запускает init. Что имело бы место в системе в том случае, если бы ак- тивен был только один процесс init ? 43. Формат записей в файле "/etc/inittab" допускает задание действия, связанного с каждым порождаемым процессом. Напри- мер, с getty-процессом связано действие "respawn" (возрожде- ние), означающее, что процесс init должен возрождать getty-процесс, если последний прекращает существование. На практике, когда пользователь выходит из системы процесс init порождает новый getty-процесс, чтобы другой пользователь мог получить доступ к временно бездействующей терминальной ли- нии. Каким образом это делает процесс init ? 44. Некоторые из алгоритмов ядра прибегают к просмотру таблицы процессов. Время поиска данных можно сократить, если исполь- зовать указатели на: родителя процесса, любого из потомков, другой процесс, имеющий того же родителя. Процесс обнаружи- вает всех своих потомков, следуя сначала за указателем на любого из потомков, а затем используя указатели на другие процессы, имеющие того же родителя (циклы недопустимы). Ка- кие из алгоритмов выиграют от этого ? Какие из алгоритмов нужно оставить без изменений ? ДИСПЕТЧЕРИЗАЦИЯ ПРОЦЕССОВ И ЕЕ ВРЕМЕННЫЕ ХАРАКТЕРИСТИКИ В системе разделения времени ядро предоставляет процессу ре- сурсы центрального процессора (ЦП) на интервал времени, называе- мый квантом, по истечении которого выгружает этот процесс и за- пускает другой, периодически переупорядочивая очередь процессов. Алгоритм планирования процессов в системе UNIX использует время выполнения в качестве параметра. Каждый активный процесс имеет приоритет планирования; ядро переключает контекст на процесс с наивысшим приоритетом. При переходе выполняющегося процесса из режима ядра в режим задачи ядро пересчитывает его приоритет, пе- риодически и в режиме задачи переустанавливая приоритет каждого процесса, готового к выполнению. Информация о времени, связанном с выполнением, нужна также и некоторым из пользовательских процессов: используемая ими, напри- мер, команда time позволяет узнать, сколько времени занимает вы- полнение другой команды, команда date выводит текущую дату и вре- мя суток. С помощью различных системных функций процессы могут устанавливать или получать временные характеристики выполнения в режиме ядра, а также степень загруженности центрального процессо- ра. Время в системе поддерживается с помощью аппаратных часов, которые посылают ЦП прерывания с фиксированной, аппаратно-зависи- мой частотой, обычно 50-100 раз в секунду. Каждое поступление прерывания по таймеру (часам) именуется таймерным тиком. В насто- ящей главе рассматриваются особенности реализации процессов во времени, включая планирование процессов в системе UNIX, описание связанных со временем системных функций, а также функций, выпол- няемых программой обработки прерываний по таймеру. 8.1 ПЛАНИРОВАНИЕ ВЫПОЛНЕНИЯ ПРОЦЕССОВ Планировщик процессов в системе UNIX принадлежит к общему классу планировщиков, работающих по принципу "карусели с многоу- ровневой обратной связью". В соответствии с этим принципом ядро предоставляет процессу ресурсы ЦП на квант времени, по истечении которого выгружает этот процесс и возвращает его в одну из нес- кольких очередей, регулируемых приоритетами. Прежде чем процесс завершится, ему может потребоваться множество раз пройти через цикл с обратной связью. Когда ядро выполняет переключение кон- текста и восстанавливает контекст процесса, процесс возобновляет выполнение с точки приостанова. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм schedule_process Ё Ё входная информация: отсутствует Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё выполнять пока (для запуска не будет выбран один из про-Ё Ё цессов) Ё Ё { Ё Ё для (каждого процесса в очереди готовых к выполнению)Ё Ё выбрать процесс с наивысшим приоритетом из загру-Ё Ё женных в память; Ё Ё если (ни один из процессов не может быть избран для Ё Ё выполнения) Ё Ё приостановить машину; Ё Ё /* машина выходит из состояния простоя по преры- Ё Ё /* ванию Ё Ё */ Ё Ё } Ё Ё удалить выбранный процесс из очереди готовых к выполне- Ё Ё нию; Ё Ё переключиться на контекст выбранного процесса, возобно- Ё Ё вить его выполнение; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 8.1. Алгоритм планирования выполнения процессов 8.1.1 Алгоритм Сразу после переключения контекста ядро запускает алгоритм планирования выполнения процессов (Рисунок 8.1), выбирая на вы- полнение процесс с наивысшим приоритетом среди процессов, находя- щихся в состояниях "резервирования" и "готовности к выполнению, будучи загруженным в память". Рассматривать процессы, не загру- женные в память, не имеет смысла, поскольку не будучи загружен, процесс не может выполняться. Если наивысший приоритет имеют сра- зу несколько процессов, ядро, используя принцип кольцевого списка (карусели), выбирает среди них тот процесс, который находится в состоянии "готовности к выполнению" дольше остальных. Если ни один из процессов не может быть выбран для выполнения, ЦП проста- ивает до момента получения следующего прерывания, которое прои- зойдет не позже чем через один таймерный тик; после обработки этого прерывания ядро снова запустит алгоритм планирования. 8.1.2 Параметры диспетчеризации В каждой записи таблицы процессов есть поле приоритета, ис- пользуемое планировщиком процессов. Приоритет процесса в режиме задачи зависит от того, как этот процесс перед этим использовал ресурсы ЦП. Можно выделить два класса приоритетов процесса (Рису- нок 8.2): приоритеты выполнения в режиме ядра и приоритеты выпол- нения в режиме задачи. Каждый класс включает в себя ряд значений, с каждым значением логически ассоциирована некоторая очередь про- цессов. Приоритеты выполнения в режиме задачи оцениваются для процессов, выгруженных по возвращении из режима ядра в режим за- дачи, приоритеты выполнения в режиме ядра имеют смысл только в контексте алгоритма sleep. Приоритеты выполнения в режиме задачи имеют верхнее пороговое значение, приоритеты выполнения в режиме ядра имеют нижнее пороговое значение. Среди приоритетов выполне- ния в режиме ядра далее можно выделить высокие и низкие приорите- ты: процессы с низким приоритетом возобновляются по получении сигнала, а процессы с высоким приоритетом продолжают оставаться в состоянии приостанова (см. раздел 7.2.1). Пороговое значение между приоритетами выполнения в режимах ядра и задачи на Рисунке 8.2 отмечено двойной линией, проходящей между приоритетом ожидания завершения потомка (в режиме ядра) и нулевым приоритетом выполнения в режиме задачи. Приоритеты про- цесса подкачки, ожидания ввода-вывода, связанного с диском, ожи- дания буфера и индекса являются высокими, не допускающими преры- вания системными приоритетами, с каждым из которых связана очередь из 1, 3, 2 и 1 процесса, соответственно, в то время как приоритеты ожидания ввода с терминала, вывода на терминал и за- вершения потомка являются низкими, допускающими прерывания сис- темными приоритетами, с каждым из которых связана очередь из 4, 0 и 2 процессов, соответственно. На рисунке представлены также уровни приоритетов выполнения в режиме задачи (*). Ядро вычисляет приоритет процесса в следующих случаях: * Непосредственно перед переходом процесса в состояние приоста- нова ядро назначает ему приоритет исходя из причины приоста- нова. Приоритет не зависит от динамических характеристик про- цесса (продолжительности ввода-вывода или времени счета), напротив, это постоянная величина, жестко устанавливаемая в момент приостанова и зависящая только от причины перехода процесса в данное состояние. Процессы, приостановленные алго- ритмами низкого уровня, имеют тенденцию порождать тем больше узких мест в системе, чем дольше они находятся в этом состоя- нии; поэтому им назначается более высокий приоритет по срав- нению с остальными процессами. Например, процесс, приостановленный в ожидании завершения ввода-вывода, связан- ного с диском, имеет более высокий приоритет по сравнению с процессом, ожидающим освобождения буфера, по нескольким при- чинам. Прежде всего, у первого процесса уже есть буфер, поэ- тому не исключена возможность, что когда он возобновится, он успеет освободить и буфер, и другие ресурсы. Чем больше ре- сурсов свободно, тем меньше шансов для возникновения взаимной блокировки процессов. Системе не придется часто переключать ддддддддддддддддддддддддддддддддддддддд (*) Наивысшим значением приоритета в системе является нулевое значение. Таким образом, нулевой приоритет выполнения в режи- ме задачи выше приоритета, имеющего значение, равное 1, и т.д. Приоритеты выполнения Уровни приоритетов Процессы в режиме ядра Ё здддддддддддддддддддддд© Ё Ё Процесс Ё здд© Ё Ё подкачки Ёд╢ Ё Ё Не допускающие цдддддддддддддддддддддд╢ юдды Ё ЁОжидание ввода-вывода,Ё здд© здд© здд© Ё прерывания Ё связанного с диском Ёд╢ цд╢ цд╢ Ё Ё цдддддддддддддддддддддд╢ юдды юдды юдды Ё Ё Ожидание Ё здд© здд© Ё Ё буфера Ёд╢ цд╢ Ё Ё цдддддддддддддддддддддд╢ юдды юдды Ё Ё Ожидание Ё здд© Ё Ё индекса Ёд╢ Ё Ё цдддддддддддддддддддддд╢ юдды Ё цдддддддддддддддддддддд╢ Ё Ё Ожидание ввода с тер-Ё здд© здд© здд© здд© Ё Ё минала Ёд╢ цд╢ цд╢ цд╢ Ё Ё Допускающие цдддддддддддддддддддддд╢ юдды юдды юдды юдды Ё Ё Ожидание вывода на Ё Ё прерывания Ё терминал Ё Ё цдддддддддддддддддддддд╢ Ё Ё Ожидание завершения Ё здд© здд© Ё Ё потомка Ёд╢ цд╢ Ё v цдддддддддддддддддддддд╢ юдды юдды Пороговый приоритет цдддддддддддддддддддддд╢ ^ Ё Уровень задачи 0 Ё Ё цдддддддддддддддддддддд╢ здд© здд© здд© здд© Ё Ё Уровень задачи 1 Ёд╢ цд╢ цд╢ цд╢ Ё Ё цдддддддддддддддддддддд╢ юдды юдды юдды юдды Ё Ё Ы Ё Ё Ё Ы Ё Ё Ё Ы Ё Ё цдддддддддддддддддддддд╢ здд© Приоритеты выполнения Ё Уровень задачи n Ёд╢ Ё в режиме задачи юдддддддддддддддддддддды юдды Рисунок 8.2. Диапазон приоритетов процесса контекст, благодаря чему сократится время реакции процесса и увеличится производительность системы. Во-вторых, буфер, ос- вобождения которого ожидает процесс, может быть занят процес- сом, ожидающим в свою очередь завершения ввода-вывода. По за- вершении ввода-вывода будут возобновлены оба процесса, пос- кольку они были приостановлены по одному и тому же адресу. Если первым запустить на выполнение процесс, ожидаю- щий освобождения буфера, он в любом случае снова приостано- вится до тех пор, пока буфер не будет освобожден; следова- тельно, его приоритет должен быть ниже. * По возвращении процесса из режима ядра в режим задачи ядро вновь вычисляет приоритет процесса. Процесс мог до этого на- ходиться в состоянии приостанова, изменив свой приоритет на приоритет выполнения в режиме ядра, поэтому при переходе про- цесса из режима ядра в режим задачи ему должен быть возвращен приоритет выполнения в режиме задачи. Кроме того, ядро "штра- фует" выполняющийся процесс в пользу остальных процессов, от- бирая используемые им ценные системные ресурсы. * Приоритеты всех процессов в режиме задачи с интервалом в 1 секунду (в версии V) пересчитывает программа обработки преры- ваний по таймеру, побуждая тем самым ядро выполнять алгоритм планирования, чтобы не допустить монопольного использования ресурсов ЦП одним процессом. В течение кванта времени таймер может послать процессу нес- колько прерываний; при каждом прерывании программа обработки пре- рываний по таймеру увеличивает значение, хранящееся в поле табли- цы процессов, которое описывает продолжительность использования ресурсов центрального процессора (ИЦП). В версии V каждую секунду программа обработки прерываний переустанавливает значение этого поля, используя функцию полураспада (decay): decay(ИЦП) = ИЦП/2; После этого программа пересчитывает приоритет каждого процесса, находящегося в состоянии "зарезервирован, но готов к выполнению", по формуле приоритет = (ИЦП/2) + (базовый уровень приоритета задачи) где под "базовым уровнем приоритета задачи" понимается пороговое значение, расположенное между приоритетами выполнения в режимах ядра и задачи. Высокому приоритету планирования соответствует ко- личественно низкое значение. Анализ функций пересчета продолжи- тельности использования ресурсов ЦП и приоритета процесса показы- вает: чем ниже скорость полураспада значения ИЦП, тем медленнее приоритет процесса достигает значение базового уровня; поэтому процессы в состоянии "готовности к выполнению" имеют тенденцию занимать большое число уровней приоритетов. Результатом ежесекундного пересчета приоритетов является пе- ремещение процессов, находящихся в режиме задачи, от одной очереди к другой, как показано на Рисунке 8.3. По сравнению с Ри- сунком 8.2 один процесс перешел из очереди, соответствующей уров- ню 1, в очередь, соответствующую нулевому уровню. В реальной сис- теме все процессы, имеющие приоритеты выполнения в режиме задачи, поменяли бы свое местоположение в очередях. При этом следует ука- зать на невозможность изменения приоритета процесса в режиме яд- ра, а также на невозможность пересечения пороговой черты процес- сами, выполняющимися в режиме задачи, до тех пор, пока они не обратятся к операционной системе и не перейдут в состояние приос- танова. Ядро стремится производить пересчет приоритетов всех активных процессов ежесекундно, однако интервал между моментами пересчета может слегка варьироваться. Если прерывание по таймеру поступило тогда, когда ядро исполняло критический отрезок программы (други- ми словами, в то время, когда приоритет работы ЦП был повышен, но, очевидно, не настолько, чтобы воспрепятствовать прерыванию данного типа), ядро не пересчитывает приоритеты, иначе ему приш- лось бы надолго задержаться на критическом отрезке. Вместо этого ядро запоминает то, что ему следует произвести пересчет приорите- тов, и делает это при первом же прерывании по таймеру, поступаю- щем после снижения приоритета работы ЦП. Периодический пересчет приоритета процессов гарантирует проведение стратегии планирова- ния, основанной на использовании кольцевого списка процессов, вы- полняющихся в режиме задачи. При этом конечно же ядро откликается на интерактивные запросы таких программ, как текстовые редакторы или программы форматного ввода: процессы, их реализующие, имеют высокий коэффициент простоя (отношение времени простоя к продол- жительности использования ЦП) и поэтому естественно было бы повы- шать их приоритет, когда они готовы для выполнения (см. [Thompson 78], стр.1937). В других механизмах планирования квант времени, выделяемый процессу на работу с ресурсами ЦП, динамически изменя- ется в интервале между 0 и 1 сек. в зависимости от степени заг- рузки системы. При этом время реакции на запросы процессов может Приоритеты выполнения Уровни приоритетов Процессы в режиме ядра Ё здддддддддддддддддддддд© Ё Ё Процесс Ё здд© Ё Ё подкачки Ёд╢ Ё Ё Не допускающие цдддддддддддддддддддддд╢ юдды Ё ЁОжидание ввода-вывода,Ё здд© здд© здд© Ё прерывания Ё связанного с диском Ёд╢ цд╢ цд╢ Ё Ё цдддддддддддддддддддддд╢ юдды юдды юдды Ё Ё Ожидание Ё здд© здд© Ё Ё буфера Ёд╢ цд╢ Ё Ё цдддддддддддддддддддддд╢ юдды юдды Ё Ё Ожидание Ё здд© Ё Ё индекса Ёд╢ Ё Ё цдддддддддддддддддддддд╢ юдды Ё цдддддддддддддддддддддд╢ Ё Ё Ожидание ввода с тер-Ё здд© здд© здд© здд© Ё Ё минала Ёд╢ цд╢ цд╢ цд╢ Ё Ё Допускающие цдддддддддддддддддддддд╢ юдды юдды юдды юдды Ё Ё Ожидание вывода на Ё Ё прерывания Ё терминал Ё Ё цдддддддддддддддддддддд╢ Ё Ё Ожидание завершения Ё здд© здд© Ё Ё потомка Ёд╢ цд╢ Ё v цдддддддддддддддддддддд╢ юдды юдды Пороговый приоритет цдддддддддддддддддддддд╢ здд© ^ Ё Уровень задачи 0 Ёд╢ Ё<д д д д д д© Ё цдддддддддддддддддддддд╢ юдды Ы Ё Ё Ё здд© здд© здд© зад© Ё Ё Уровень задачи 1 Ёд╢ цд╢ цд╢ ц ╢ Ё Ё цдддддддддддддддддддддд╢ юдды юдды юдды юдды Ё Ё Ы Ё Ё Ё Ы Ё Ё Ё Ы Ё Ё цдддддддддддддддддддддд╢ здд© Приоритеты выполнения Ё Уровень задачи n Ёд╢ Ё в режиме задачи юдддддддддддддддддддддды юдды Рисунок 8.2. Переход процесса из одной очереди в другую сократиться за счет того, что на ожидание момента запуска процес- сам уже не нужно отводить по целой секунде; однако, с другой сто- роны, ядру приходится чаще прибегать к переключению контекстов. 8.1.3 Примеры диспетчеризации процессов На Рисунке 8.4 показана динамика изменений приоритетов про- цессов A, B и C в версии V при следующих допущениях: все эти про- цессы были созданы с первоначальным приоритетом 60, который явля- ется наивысшим приоритетом выполнения в режиме задачи, прерывания по таймеру поступают 60 раз в секунду, процессы не используют вы- зов системных функций, в системе нет других процессов, готовых к выполнению. Ядро вычисляет полураспад показателя ИЦП по формуле: Время Процесс A Процесс B Процесс C Ё Приоритет ИЦП Ы Приоритет ИЦП Ы Приоритет ИЦП 0 ддедд Ы Ы Ё 60 0 Ы 60 0 Ы 60 0 Ё 1 Ы Ы Ё 2 Ы Ы Ё З Ы Ы Ё З Ы Ы Ё З Ы Ы 1 ддедд 60 Ы Ы Ё 75 30 Ы 60 0 Ы 60 0 Ё Ы 1 Ы Ё Ы 2 Ы Ё Ы З Ы Ё Ы З Ы Ё Ы З Ы 2 ддедд Ы 60 Ы Ё 67 15 Ы 75 30 Ы 60 0 Ё Ы Ы 1 Ё Ы Ы 2 Ё Ы Ы З Ё Ы Ы З Ё Ы Ы З 3 ддедд Ы Ы 60 Ё 63 7 Ы 67 15 Ы 75 30 Ё 8 Ы Ы Ё 9 Ы Ы Ё З Ы Ы Ё З Ы Ы Ё З Ы Ы 4 ддедд 67 Ы Ы Ё 76 33 Ы 63 7 Ы 67 15 Ё Ы 8 Ы Ё Ы 9 Ы Ё Ы З Ы Ё Ы З Ы Ё Ы З Ы 5 ддедд Ы 67 Ы Ё 68 16 Ы 76 33 Ы 63 7 Ё Ы Ы Ё Ы Ы Ё Ы Ы Рисунок 8.4. Пример диспетчеризации процессов ИЦП = decay(ИЦП) = ИЦП/2; а приоритет процесса по формуле: приоритет = (ИЦП/2) + 60; Если предположить, что первым запускается процесс A и ему выделя- ется квант времени, он выполняется в течение 1 секунды: за это время таймер посылает системе 60 прерываний и столько же раз программа обработки прерываний увеличивает для процесса A значе- ние поля, содержащего показатель ИЦП (с 0 до 60). По прошествии секунды ядро переключает контекст и, произведя пересчет приорите- тов для всех процессов, выбирает для выполнения процесс B. В те- чение следующей секунды программа обработки прерываний по таймеру 60 раз повышает значение поля ИЦП для процесса B, после чего ядро пересчитывает параметры диспетчеризации для всех процессов и вновь переключает контекст. Процедура повторяется многократно, сопровождаясь поочередным запуском процессов на выполнение. Теперь рассмотрим процессы с приоритетами, приведенными на Рисунке 8.5, и предположим, что в системе имеются и другие про- цессы. Ядро может выгрузить процесс A, оставив его в состоянии "готовности к выполнению", после того, как он получит подряд нес- колько квантов времени для работы с ЦП и снизит таким образом свой приоритет выполнения в режиме задачи (Рисунок 8.5а). Через некоторое время после запуска процесса A в состояние "готовности к выполнению" может перейти процесс B, приоритет которого в тот момент окажется выше приоритета процесса A (Рисунок 8.5б). Если ядро за это время не запланировало к выполнению любой другой про- цесс (из тех, что не показаны на рисунке), оба процесса (A и B) при известных обстоятельствах могут на некоторое время оказаться на одном уровне приоритетности, хотя процесс B попадет на этот уровень первым из-за того, что его первоначальный приоритет был ближе (Рисунок 8.5в и 8.5г). Тем не менее, ядро запустит процесс A впереди процесса B, поскольку процесс A находился в состоянии "готовности к выполнению" более длительное время (Рисунок 8.5д) - это решающее условие, если выбор производится из процессов с оди- наковыми приоритетами. В разделе 6.4.3 уже говорилось о том, что ядро запускает про- цесс на выполнение после переключения контекста: прежде чем пе- рейти в состояние приостанова или завершить свое выполнение про- цесс должен переключить контекст, кроме того он имеет возможность переключать контекст в момент перехода из режима ядра в режим за- дачи. Ядро выгружает процесс, который собирается перейти в режим задачи, если имеется готовый к выполнению процесс с более высоким приоритетом. Такая ситуация возникает, если ядро вывело из состо- яния приостанова процесс с приоритетом, превышающим приоритет те- кущего процесса, или если в результате обработки прерывания по таймеру изменились приоритеты всех готовых к выполнению процес- сов. В первом случае текущий процесс не может выполняться в режи- ме задачи, поскольку имеется процесс с более высоким приоритетом выполнения в режиме ядра. Во втором случае программа обработки прерываний по таймеру решает, что процесс использовал выделенный ему квант времени, и поскольку множество процессов при этом меня- ют свои приоритеты, ядро выполняет переключение контекста. 8.1.4 Управление приоритетами Процессы могут управлять своими приоритетами с помощью сис- темной функции nice: nice(value); где value - значение, в процессе пересчета прибавляемое к приори- тету процесса: приоритет = (ИЦП/константа) + (базовый приоритет) + (значение nice) Системная функция nice увеличивает или уменьшает значение поля nice в таблице процессов на величину параметра функции, при этом только суперпользователю дозволено указывать значения, увеличива- ющие приоритет процесса. Кроме того, только суперпользователь мо- жет указывать значения, лежащие ниже определенного порога. Поль- зователи, вызывающие системную функцию nice для того, чтобы понизить приоритет во время выполнения интенсивных вычислительных работ, "удобны, приятны" (nice) для остальных пользователей сис- зддддддддд© зддддддддд© зддддддддд© 60 Ё Ё Ё Ё Ё Ё ^ цддддддддд╢ цддддддддд╢ цддддддддд╢ Ё Ё Ё Ё Ё Ё B Ё Ё цддддддддд╢ цддддддддд╢ цддддддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё цддддддддд╢ цддддддддд╢ цддддддддд╢ Более Ё Ё Ё B Ё Ё A Ё высокий цддддддддд╢ цддддддддд╢ цддддддддд╢ приори- Ё Ё Ё Ё Ё Ё тет цддддддддд╢ цддддддддд╢ цддддддддд╢ Ё Ё Ё Ё A Ё Ё Ё Ё цддддддддд╢ цддддддддд╢ цддддддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё цддддддддд╢ цддддддддд╢ цддддддддд╢ Ё Ё A Ё Ё Ё Ё Ё юддддддддды юддддддддды юддддддддды (а) (б) (в) зддддддддд© зддддддддд© зддддддддд© 60 Ё B Ё Ё A B Ё Ё B Ё(процесс цддддддддд╢ цддддддддд╢ цддддддддд╢ A за- Ё A Ё Ё Ё Ё Ё пуска- цддддддддд╢ цддддддддд╢ цддддддддд╢ ется Ё Ё Ё Ё Ё Ё первым) цддддддддд╢ цддддддддд╢ цддддддддд╢ Ё Ё Ё Ё Ё Ё цддддддддд╢ цддддддддд╢ цддддддддд╢ Ё Ё Ё Ё Ё Ё цддддддддд╢ цддддддддд╢ цддддддддд╢ Ё Ё Ё Ё Ё Ё цддддддддд╢ цддддддддд╢ цддддддддд╢ Ё Ё Ё Ё Ё Ё цддддддддд╢ цддддддддд╢ цддддддддд╢ Ё Ё Ё Ё Ё Ё юддддддддды юддддддддды юддддддддды (г) (д) (е) Рисунок 8.5. Планирование на основе кольцевого списка и прио- ритеты процессов темы, отсюда название функции. Процессы наследуют значение nice у своего родителя при выполнении системной функции fork. Функция nice действует только для выполняющихся процессов; процесс не мо- жет сбросить значение nice у другого процесса. С практической точки зрения это означает, что если администратору системы пона- добилось понизить приоритеты различных процессов, требующих для своего выполнения слишком много времени, у него не будет другого способа сделать это быстро, кроме как вызвать функцию удаления (kill) для всех них сразу. 8.1.5 Планирование на основе справедливого раздела Вышеописанный алгоритм планирования не видит никакой разницы между пользователями различных классов (категорий). Другими сло- вами, невозможно выделить определенной совокупности процессов, например, половину сеанса работы с ЦП. Тем не менее, такая воз- можность имеет важное значение для организации работы в условиях вычислительного центра, где группа пользователей может пожелать купить только половину машинного времени на гарантированной осно- ве и с гарантированным уровнем реакции. Здесь мы рассмотрим схе- му, именуемую "Планированием на основе справедливого раздела" (Fair Share Scheduler) и реализованную на вычислительном центре Indian Hill фирмы AT&T Bell Laboratories [Henry 84]. Принцип "планирования на основе справедливого раздела" состо- ит в делении совокупности пользователей на группы, являющиеся объектами ограничений, накладываемых обычным планировщиком на об- работку процессов из каждой группы. При этом система выделяет время ЦП пропорционально числу групп, вне зависимости от того, сколько процессов выполняется в группе. Пусть, например, в систе- ме имеются четыре планируемые группы, каждая из которых загружает ЦП на 25% и содержит, соответственно, 1, 2, 3 и 4 процесса, реа- лизующих счетные задачи, которые никогда по своей воле не уступят ЦП. При условии, что в системе больше нет никаких других процес- сов, каждый процесс при использовании традиционного алгоритма планирования получил бы 10% времени ЦП (поскольку всего процессов 10 и между ними не делается никаких различий). При использовании алгоритма планирования на основе справедливого раздела процесс из первой группы получит в два раза больше времени ЦП по сравнению с каждым процессом из второй группы, в 3 раза больше по сравнению с каждым процессом из третьей группы и в 4 раза больше по сравнению с каждым процессом из четвертой. В этом примере всем процессам в группе выделяется равное время, поскольку продолжительность цик- ла, реализуемого каждым процессом, заранее не установлена. Реализация этой схемы довольно проста, что и делает ее прив- лекательной. В формуле расчета приоритета процесса появляется еще один термин - "приоритет группы справедливого раздела". В прост- ранстве процесса также появляется новое поле, описывающее продол- жительность ИЦП на основе справедливого раздела, общую для всех процессов из группы. Программа обработки прерываний по таймеру увеличивает значение этого поля для текущего процесса и ежесе- кундно пересчитывает значения соответствующих полей для всех про- цессов в системе. Новая компонента формулы вычисления приоритета процесса представляет собой нормализованное значение ИЦП для каж- дой группы. Чем больше процессорного времени выделяется процессам группы, тем выше значение этого показателя и ниже приоритет. В качестве примера рассмотрим две группы процессов (Рисунок 8.6), в одной из которых один процесс (A), в другой - два (B и C). Предположим, что ядро первым запустило на выполнение процесс A, в течение секунды увеличивая соответствующие этому процессу значения полей, описывающих индивидуальное и групповое ИЦП. В ре- зультате пересчета приоритетов по истечении секунды процессы B и C будут иметь наивысшие приоритеты. Допустим, что ядро выбирает на выполнение процесс B. В течение следующей секунды значение по- ля ИЦП для процесса B поднимается до 60, точно такое же значение принимает поле группового ИЦП для процессов B и C. Таким образом, по истечении второй секунды процесс C получит приоритет, равный 75 (сравните с Рисунком 8.4), и ядро запустит на выполнение про- цесс A с приоритетом 74. Дальнейшие действия можно проследить на рисунке: ядро по очереди запускает процессы A, B, A, C, A, B и т. д. Время Процесс A Процесс B Процесс C Ё Прио- Ин- Груп-Ы Прио- Ин- Груп-Ы Прио- Ин- Груп- Ё ритет диви- по- Ы ритет диви- по- Ы ритет диви- по- Ё дуал. вое Ы дуал. вое Ы дуал. вое Ё ИЦП ИЦП Ы ИЦП ИЦП Ы ИЦП ИЦП 0 ддедд Ы Ы Ё 60 0 0 Ы 60 0 0 Ы 60 0 0 Ё 1 1 Ы Ы Ё 2 2 Ы Ы Ё З З Ы Ы Ё З З Ы Ы Ё З З Ы Ы 1 ддедд 60 60 Ы Ы Ё 90 30 30 Ы 60 0 0 Ы 60 0 0 Ё Ы 1 1 Ы 1 Ё Ы 2 2 Ы 2 Ё Ы З З Ы З Ё Ы З З Ы З Ё Ы З З Ы З 2 ддедд Ы 60 60 Ы 60 Ё 74 15 15 Ы 90 30 30 Ы 75 0 30 Ё 16 16 Ы Ы Ё 17 17 Ы Ы Ё З З Ы Ы Ё З З Ы Ы Ё З З Ы Ы 3 ддедд 75 75 Ы Ы Ё 96 37 37 Ы 74 15 15 Ы 67 0 15 Ё Ы 16 Ы 1 16 Ё Ы 17 Ы 2 17 Ё Ы З Ы З З Ё Ы З Ы З З Ё Ы З Ы З З 4 ддедд Ы 75 Ы 60 75 Ё 78 18 18 Ы 81 7 37 Ы 93 30 37 Ё 19 19 Ы Ы Ё 20 20 Ы Ы Ё З З Ы Ы Ё З З Ы Ы Ё З З Ы Ы 5 ддедд 78 78 Ы Ы Ё 98 39 39 Ы 70 3 18 Ы 76 15 18 Ё Ы Ы Ё Ы Ы Ё Ы Ы Рисунок 8.6. Пример планирования на основе справедливого раз- дела, в котором используются две группы с тремя процессами 8.1.6 Работа в режиме реального времени Режим реального времени подразумевает возможность обеспечения достаточной скорости реакции на внешние прерывания и выполнения отдельных процессов в темпе, соизмеримом с частотой возникновения вызывающих прерывания событий. Примером системы, работающей в ре- жиме реального времени, может служить система управления жизнео- беспечением пациентов больниц, мгновенно реагирующая на изменение состояния пациента. Процессы, подобные текстовым редакторам, не считаются процессами реального времени: в них быстрая реакция на действия пользователя является желательной, но не необходимой (ничего страшного не произойдет, если пользователь, выполняющий редактирование текста, подождет ответа несколько лишних секунд, хотя у пользователя на этот счет могут быть и свои соображения). Вышеописанные алгоритмы планирования выполнения процессов пред- назначены специально для использования в системах разделения вре- мени и не годятся для условий работы в режиме реального времени, поскольку не гарантируют запуск ядром каждого процесса в течение фиксированного интервала времени, позволяющего говорить о взаимо- действии вычислительной системы с процессами в темпе, соизмеримом со скоростью протекания этих процессов. Другой помехой в поддерж- ке работы в режиме реального времени является невыгружаемость яд- ра; ядро не может планировать выполнение процесса реального вре- мени в режиме задачи, если оно уже исполняет другой процесс в режиме ядра, без внесения в работу существенных изменений. В нас- тоящее время системным программистам приходится переводить про- цессы реального времени в режим ядра, чтобы обеспечить достаточ- ную скорость реакции. Правильное решение этой проблемы - дать таким процессам возможность динамического протекания (другими словами, они не должны быть встроены в ядро) с предоставлением соответствующего механизма, с помощью которого они могли бы сооб- щать ядру о своих нуждах, вытекающих из особенностей работы в ре- жиме реального времени. На сегодняшний день в стандартной системе UNIX такая возможность отсутствует. 8.2 СИСТЕМНЫЕ ОПЕРАЦИИ, СВЯЗАННЫЕ СО ВРЕМЕНЕМ Существует несколько системных функций, имеющих отношение к времени протекания процесса: stime, time, times и alarm. Первые две имеют дело с глобальным системным временем, последние две - с временем выполнения отдельных процессов. Функция stime дает суперпользователю возможность заносить в глобальную переменную, определенную на уровне ядра, значение те- кущего времени: stime(pvalue); где pvalue - целое число двойной длины, показывающее время в се- кундах, прошедшее с полуночи (00:00:00) накануне 1 января 1970 года (по Гринвичу). Программа обработки прерываний по таймеру каждую секунду увеличивает значение глобальной переменной. Выби- рается время из этой переменной с помощью функции time: time(tloc); где tloc - указатель на переменную, принадлежащую процессу, в ко- торую заносится возвращаемое функцией значение. Функция возвраща- ет это значение и из самой себя, например, команде date, которая вызывает эту функцию, чтобы определить текущее время. Функция times возвращает суммарное время выполнения процесса и всех его потомков, прекративших существование, в режимах ядра и задачи. Синтаксис вызова функции: times(tbuffer) struct tms *tbuffer; где tms - имя структуры, в которую помещаются возвращаемые значе- ния и которая описывается следующим образом: здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё #include Ё Ё extern long times(); Ё Ё Ё Ё main() Ё Ё { Ё Ё int i; Ё Ё /* tms - имя структуры данных, состоящей из 4 элемен- Ё Ё тов */ Ё Ё struct tms pb1,pb2; Ё Ё long pt1,pt2; Ё Ё Ё Ё pt1 = times(&pb1); Ё Ё for (i = 0; i < 10; i++) Ё Ё if (fork() == 0) Ё Ё child(i); Ё Ё Ё Ё for (i = 0; i < 10; i++) Ё Ё wait((int*) 0); Ё Ё pt2 = times(&pb2); Ё Ё printf("процесс-родитель: реальное время %u Ё Ё в режиме задачи %u в режиме ядра %u Ё Ё потомки: в режиме задачи %u в режиме ядра %u\n",Ё Ё pt2 - pt1,pb2.tms_utime - pb1.tms_utime, Ё Ё pb2.tms_stime - pb1.tms_stime, Ё Ё pb2.tms_cutime - pb1.tms_cutime, Ё Ё pb2.tms_cstime - pb1.tms_cstime); Ё Ё } Ё Ё Ё Ё child(n); Ё Ё int n; Ё Ё { Ё Ё int i; Ё Ё struct tms cb1,cb2; Ё Ё long t1,t2; Ё Ё Ё Ё t1 = times(&cb1); Ё Ё for (i = 0; i < 10000; i++) Ё Ё ; Ё Ё t2 = times(&cb2); Ё Ё printf("потомок %d: реальное время %u в режиме задачи %uЁ Ё в режиме ядра %u\n",n,t2 - t1, Ё Ё cb2.tms_utime - cb1.tms_utime, Ё Ё cb2.tms_stime - cb1.tms_stime); Ё Ё exit(); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 8.7. Пример программы, использующей функцию times struct tms { /* time_t - имя структуры данных, в которой хранится время */ time_t tms_utime; /* время выполнения процесса в режиме задачи */ time_t tms_stime; /* время выполнения процесса в режиме ядра */ time_t tms_cutime; /* время выполнения потомков в режиме задачи */ time_t tms_cstime; /* время выполнения потомков в режиме ядра */ }; Функция times возвращает время, прошедшее "с некоторого произ- вольного момента в прошлом", как правило, с момента загрузки сис- темы. На Рисунке 8.7 приведена программа, в которой процесс-роди- тель создает 10 потомков, каждый из которых 10000 раз выполняет пустой цикл. Процесс-родитель обращается к функции times перед созданием потомков и после их завершения, в свою очередь потомки вызывают эту функцию перед началом цикла и после его завершения. Кто-то по наивности может подумать, что время выполнения потомков процесса в режимах задачи и ядра равно сумме соответствующих сла- гаемых каждого потомка, а реальное время процесса-родителя явля- ется суммой реального времени его потомков. Однако, время выполнения потомков не включает в себя время, затраченное на ис- полнение системных функций fork и exit, кроме того оно может быть искажено за счет обработки прерываний и переключений контекста. С помощью системной функции alarm пользовательские процессы могут инициировать посылку сигналов тревоги ("будильника") через кратные промежутки времени. Например, программа на Рисунке 8.8 каждую минуту проверяет время доступа к файлу и, если к файлу бы- ло произведено обращение, выводит соответствующее сообщение. Для этого в цикле, с помощью функции stat, устанавливается момент последнего обращения к файлу и, если оно имело место в течение последней минуты, выводится сообщение. Затем процесс с помощью функции signal делает распоряжение принимать сигналы тревоги, с помощью функции alarm задает интервал между сигналами в 60 секунд и с помощью функции pause приостанавливает свое выполнение до мо- мента получения сигнала. Через 60 секунд сигнал поступает, ядро подготавливает стек задачи к вызову функции обработки сигнала wakeup, функция возвращает управление на оператор, следующий за вызовом функции pause, и процесс исполняет цикл вновь. Все перечисленные функции работы с временем протекания про- цесса объединяет то, что они опираются на показания системных ча- сов (таймера). Обрабатывая прерывания по таймеру, ядро обращается к различным таймерным счетчикам и инициирует соответствующее действие. 8.3 ТАЙМЕР В функции программы обработки прерываний по таймеру входит: * перезапуск часов, * вызов на исполнение функций ядра, использующих встроенные ча- сы, * поддержка возможности профилирования выполнения процессов в режимах ядра и задачи; * сбор статистики о системе и протекающих в ней процессах, * слежение за временем, * посылка процессам сигналов "будильника" по запросу, * периодическое возобновление процесса подкачки (см. следующую главу), * управление диспетчеризацией процессов. Некоторые из функций реализуются при каждом прерывании по таймеру, другие - по прошествии нескольких таймерных тиков. Прог- рамма обработки прерываний по таймеру запускается с высоким прио- ритетом обращения к процессору, не допуская во время работы воз- никновения других внешних событий (таких как прерывания от периферийных устройств). Поэтому программа обработки прерываний по таймеру работает очень быстро, за максимально-короткое время здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё #include Ё Ё #include Ё Ё Ё Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё extern unsigned alarm(); Ё Ё extern wakeup(); Ё Ё struct stat statbuf; Ё Ё time_t axtime; Ё Ё Ё Ё if (argc != 2) Ё Ё { Ё Ё printf("только 1 аргумент\n"); Ё Ё exit(); Ё Ё } Ё Ё Ё Ё axtime = (time_t) 0; Ё Ё for (;;) Ё Ё { Ё Ё /* получение значения времени доступа к файлу */ Ё Ё if (stat(argv[1],&statbuf) == -1) Ё Ё { Ё Ё printf("файла с именем %s нет\n",argv[1]); Ё Ё exit(); Ё Ё } Ё Ё if (axtime != statbuf.st_atime) Ё Ё { Ё Ё printf("к файлу %s было обращение\n",argv[1]); Ё Ё axtime = statbuf.st_atime; Ё Ё } Ё Ё signal(SIGALRM,wakeup); /* подготовка к приему Ё Ё сигнала */ Ё Ё alarm(60); Ё Ё pause(); /* приостанов до получения сигнала */Ё Ё } Ё Ё } Ё Ё Ё Ё wakeup() Ё Ё { Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 8.8. Программа, использующая системную функцию alarm здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм clock Ё Ё входная информация: отсутствует Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё перезапустить часы; /* чтобы они снова посылали преры-Ё Ё вания */ Ё Ё если (таблица ответных сигналов не пуста) Ё Ё { Ё Ё установить время для ответных сигналов; Ё Ё запустить функцию callout, если время истекло; Ё Ё } Ё Ё если (профилируется выполнение в режиме ядра) Ё Ё запомнить значение счетчика команд в момент прерыва-Ё Ё ния; Ё Ё если (профилируется выполнение в режиме задачи) Ё Ё запомнить значение счетчика команд в момент прерыва-Ё Ё ния; Ё Ё собрать статистику о самой системе; Ё Ё собрать статистику о протекающих в системе процессах; Ё Ё выверить значение продолжительности ИЦП процессом; Ё Ё если (прошла 1 секунда или более и исполняется отрезок,Ё Ё не являющийся критическим) Ё Ё { Ё Ё для (всех процессов в системе) Ё Ё { Ё Ё установить "будильник", если он активен; Ё Ё выверить значение продолжительности ИЦП; Ё Ё если (процесс будет исполняться в режиме задачи)Ё Ё выверить приоритет процесса; Ё Ё } Ё Ё возобновить в случае необходимости выполнение про- Ё Ё цесса подкачки; Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 8.9. Алгоритм обработки прерываний по таймеру пробегая свои критические отрезки, которые должны выполняться без прерываний со стороны других процессов. Алгоритм обработки преры- ваний по таймеру приведен на Рисунке 8.9. 8.3.1 Перезапуск часов В большинстве машин после получения прерывания по таймеру требуется программными средствами произвести перезапуск часов, чтобы они по прошествии интервала времени могли вновь прерывать работу процессора. Такие средства являются машинно-зависимыми и мы их рассматривать не будем. 8.3.2 Внутренние системные тайм-ауты Некоторым из процедур ядра, в частности драйверам устройств и сетевым протоколам, требуется вызов функций ядра в режиме реаль- ного времени. Например, процесс может перевести терминал в режим ввода без обработки символов, при котором ядро выполняет запросы пользователя на чтение с терминала через фиксированные промежутки времени, не дожидаясь, когда пользователь нажмет клавишу "возвра- та каретки" (см. раздел 10.3.3). Ядро хранит всю необходимую ин- формацию в таблице ответных сигналов (Рисунок 8.9), в том числе имя функции, запускаемой по истечении интервала времени, пара- метр, передаваемый этой функции, а также продолжительность интер- вала (в таймерных тиках) до момента запуска функции. Пользователь не имеет возможности напрямую контролировать за- писи в таблице ответных сигналов; для работы с ними существуют различные системные алгоритмы. Ядро сортирует записи в этой таб- лице в соответствии с величиной интервала до момента запуска функций. В связи с этим для каждой записи таблицы запоминается не общая продолжительность интервала, а только промежуток времени между моментами запуска данной и предыдущей функций. Общая про- должительность интервала до момента запуска функции складывается из промежутков времени между моментами запуска всех функций, на- чиная с первой и вплоть до текущей. Функция Время до запуска Функция Время до запуска здддддддддддддддддддддддддддд© здддддддддддддддддддддддддддд© Ё a() -2 Ё Ё a() -2 Ё цдддддддддддддддддддддддддддд╢ цдддддддддддддддддддддддддддд╢ Ё b() 3 Ё Ё b() 3 Ё цдддддддддддддддддддддддддддд╢ цдддддддддддддддддддддддддддд╢ Ё c() 10 Ё Ё f() 2 Ё юдддддддддддддддддддддддддддды цдддддддддддддддддддддддддддд╢ Ё c() 8 Ё юдддддддддддддддддддддддддддды До После Рисунок 8.10. Включение новой записи в таблицу ответных сиг- налов На Рисунке 8.10 приведен пример добавления новой записи в таблицу ответных сигналов. (К отрицательному значению поля "время до запуска" для функции a мы вернемся несколько позже). Создавая новую запись, ядро отводит для нее надлежащее место и соответс- твующим образом переустанавливает значение поля "время до запус- ка" в записи, следующей за добавляемой. Судя по рисунку, ядро со- бирается запустить функцию f через 5 таймерных тиков: оно отводит место для нее в таблице сразу после функции b и заносит в поле "время до запуска" значение, равное 2 (тогда сумма значений этих полей для функций b и f составит 5), и меняет "время до запуска" функции c на 8 (при этом функция c все равно запускается через 13 таймерных тиков). В одних версиях ядро пользуется связным списком указателей на записи таблицы ответных сигналов, в других - меняет положение записей при корректировке таблицы. Последний способ требует значительно меньших издержек при условии, что ядро не бу- дет слишком часто обращаться к таблице. При каждом поступлении прерывания по таймеру программа обра- ботки прерывания проверяет наличие записей в таблице ответных сигналов и в случае их обнаружения уменьшает значение поля "время до запуска" в первой записи. Способ хранения продолжительности интервалов до момента запуска каждой функции, выбранный ядром, позволяет, уменьшив значение поля "время до запуска" в одной только первой записи, соответственно уменьшить продолжительность интервала до момента запуска функций, описанных во всех записях таблицы. Если в указанном поле первой записи хранится отрицатель- ное или нулевое значение, соответствующую функцию следует запус- тить. Программа обработки прерываний по таймеру не запускает функцию немедленно, таким образом она не блокирует возникновение последующих прерываний данного типа. Текущий приоритет работы процессора вроде бы не позволяет таким прерываниям вмешиваться в выполнение процесса, однако ядро не имеет представления о том, сколько времени потребуется на исполнение функции. Казалось бы, если функция выполняется дольше одного таймерного тика, все пос- ледующие прерывания должны быть заблокированы. Вместо этого, программа обработки прерываний в типичной ситуации оформляет вы- зов функции как "программное прерывание", порождаемое выполнением отдельной машинной команды. Поскольку среди всех прерываний прог- раммные прерывания имеют самый низкий приоритет, они блокируются, пока ядро не закончит обработку всех остальных прерываний. С мо- мента завершения подготовки к запуску функции и до момента воз- никновения вызываемого запуском функции программного прерывания может произойти множество прерываний, в том числе и программных, в таком случае в поле "время до запуска", принадлежащее первой записи таблицы, будет занесено отрицательное значение. Когда же наконец программное прерывание происходит, программа обработки прерываний убирает из таблицы все записи с истекшими значениями полей "время до запуска" и вызывает соответствующую функцию. Поскольку в указанном поле в начальных записях таблицы может храниться отрицательное или нулевое значение, программа обработки прерываний должна найти в таблице первую запись с положительным значением поля и уменьшить его. Пусть, например, функции a соот- ветствует "время до запуска", равное -2 (Рисунок 8.10), то есть перед тем, как функция a была выбрана на выполнение, система по- лучила 2 прерывания по таймеру. При условии, что функция b 2 тика назад уже была в таблице, ядро пропускает запись, соответствующую функции a, и уменьшает значение поля "время до запуска" для функ- ции b. 8.3.3 Построение профиля Построение профиля ядра включает в себя измерение продолжи- тельности выполнения системы в режиме задачи против режима ядра, а также продолжительности выполнения отдельных процедур ядра. Драйвер параметров ядра следит за относительной эффективностью работы модулей ядра, замеряя параметры работы системы в момент прерывания по таймеру. Драйвер параметров имеет список адресов ядра (главным образом, функций ядра); эти адреса ранее были заг- ружены процессом путем обращения к драйверу параметров. Если построение профиля ядра возможно, программа обработки прерывания по таймеру запускает подпрограмму обработки прерываний, принадле- жащую драйверу параметров, которая определяет, в каком из режимов - ядра или задачи - работал процессор в момент прерывания. Если процессор работал в режиме задачи, система построения профиля увеличивает значение параметра, описывающего продолжительность выполнения в режиме задачи, если же процессор работал в режиме ядра, система увеличивает значение внутреннего счетчика, соот- ветствующего счетчику команд. Пользовательские процессы могут об- ращаться к драйверу параметров, чтобы получить значения парамет- ров ядра и различную статистическую информацию. здддддддддддддддддддддддддддддддд© Ё Алгоритм Адрес Счетчик Ё Ё Ё Ё bread 100 5 Ё Ё breada 150 0 Ё Ё bwrite 200 0 Ё Ё brelse 300 2 Ё Ё getblk 400 1 Ё Ё user - 2 Ё юдддддддддддддддддддддддддддддддды Рисунок 8.11. Адреса некоторых алгоритмов ядра На Рисунке 8.11 приведены гипотетические адреса некоторых процедур ядра. Пусть в результате 10 измерений, проведенных в мо- менты поступления прерываний по таймеру, были получены следующие значения счетчика команд: 110, 330, 145, адрес в пространстве за- дачи, 125, 440, 130, 320, адрес в пространстве задачи и 104. Ядро сохранит при этом те значения, которые показаны на рисунке. Ана- лиз этих значений показывает, что система провела 20% своего вре- мени в режиме задачи (user) и 50% времени потратила на выполнение алгоритма bread в режиме ядра. Если измерение параметров ядра выполняется в течение длитель- ного периода времени, результаты измерений приближаются к истин- ной картине использования системных ресурсов. Тем не менее, описываемый механизм не учитывает время, потраченное на обработку прерываний по таймеру и выполнение процедур, блокирующих поступ- ление прерываний данного типа, поскольку таймер не может преры- вать выполнение критических отрезков программ и, таким образом, не может в это время обращаться к подпрограмме обработки прерыва- ний драйвера параметров. В этом недостаток описываемого механиз- ма, ибо критические отрезки программ ядра чаще всего наиболее важны для измерений. Следовательно, результаты измерения парамет- ров ядра содержат определенную долю приблизительности. Уайнбергер [Weinberger 84] описал механизм включения счетчиков в главных блоках программы, таких как "if-then" и "else", с целью повышения точности измерения частоты их выполнения. Однако, данный механизм увеличивает время счета программ на 50-200%, поэтому его исполь- зование в качестве постоянного механизма измерения параметров яд- ра нельзя признать рациональным. На пользовательском уровне для измерения параметров выполне- ния процессов можно использовать системную функцию profil: profil(buff,bufsize,offset,scale); где buff - адрес массива в пространстве задачи, bufsize - размер массива, offset - виртуальный адрес подпрограммы пользователя (обычно, первой по счету), scale - способ отображения виртуальных адресов задачи на адрес массива. Ядро трактует аргумент "scale" как двоичную дробь с фиксированной точкой слева. Так, например, значение аргумента в шестнадцатиричной системе счисления, равное Oxffff, соответствует однозначному отображению счетчика команд на адреса массива, значение, равное Ox7fff, соответствует размещению в одном слове массива buff двух адресов программы, Ox3fff - четы- рех адресов программы и т.д. Ядро хранит параметры, передаваемые при вызове системной функции, в пространстве процесса. Если тай- мер прерывает выполнение процесса тогда, когда он находится в ре- жиме задачи, программа обработки прерываний проверяет значение счетчика команд в момент прерывания, сравнивает его со значением аргумента offset и увеличивает содержимое ячейки памяти, адрес которой является функцией от bufsize и scale. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё int buffer[4096]; Ё Ё main() Ё Ё { Ё Ё int offset,endof,scale,eff,gee,text; Ё Ё extern theend(),f(),g(); Ё Ё signal(SIGINT,theend); Ё Ё endof = (int) theend; Ё Ё offset = (int) main; Ё Ё /* вычисляется количество слов в тексте программы */ Ё Ё text = (endof - offset + sizeof(int) - 1)/sizeof(int); Ё Ё scale = Oxffff; Ё Ё printf Ё Ё ("смещение до начала %d до конца %d длина текста %d\n",Ё Ё offset,endof,text); Ё Ё eff = (int) f; Ё Ё gee = (int) g; Ё Ё printf("f %d g %d fdiff %d gdiff %d\n",eff,gee, Ё Ё eff-offset,gee-offset); Ё Ё profil(buffer,sizeof(int)*text,offset,scale); Ё Ё for (;;) Ё Ё { Ё Ё f(); Ё Ё g(); Ё Ё } Ё Ё } Ё Ё f() Ё Ё { Ё Ё } Ё Ё g() Ё Ё { Ё Ё } Ё Ё theend() Ё Ё { Ё Ё int i; Ё Ё for (i = 0; i < 4096; i++) Ё Ё if (buffer[i]) Ё Ё printf("buf[%d] = %d\n",i,buffer[i]); Ё Ё exit(); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 8.12. Программа, использующая системную функцию profil Рассмотрим в качестве примера программу, приведенную на Ри- сунке 8.12, измеряющую продолжительность выполнения функций f и g. Сначала процесс, используя системную функцию signal, делает указание при получении сигнала о прерывании вызывать функцию theend, затем он вычисляет диапазон адресов программы, в пределах которых будет производиться измерение продолжительности (начиная с адреса функции main и кончая адресом функции theend), и, нако- нец, запускает функцию profil, сообщая ядру о том, что он собира- здддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё смещение до начала 212 до конца 440 длина текста 57 Ё Ё f 416 g 428 fdiff 204 gdiff 216 Ё Ё buf[46] = 50 Ё Ё buf[48] = 8585216 Ё Ё buf[49] = 151 Ё Ё buf[51] = 12189799 Ё Ё buf[53] = 65 Ё Ё buf[54] = 10682455 Ё Ё buf[56] = 67 Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 8.13. Пример результатов выполнения программы, ис- пользующей системную функцию profil ется начать измерение. В результате выполнения программы в тече- ние 10 секунд на несильно загруженной машине AT&T 3B20 были полу- чены данные, представленные на Рисунке 8.13. Адрес функции f пре- вышает адрес начала профилирования на 204 байта; поскольку текст функции f имеет размер 12 байт, а размер целого числа в машине AT&T 3B20 равен 4 байтам, адреса функции f отображаются на эле- менты массива buf с номерами 51, 52 и 53. По такому же принципу адреса функции g отображаются на элементы buf c номерами 54, 55 и 56. Элементы buf с номерами 46, 48 и 49 предназначены для адре- сов, принадлежащих циклу функции main. В обычном случае диапазон адресов, в пределах которого выполняется измерение параметров, определяется в результате обращения к таблице идентификаторов для данной программы, где указываются адреса программных секций. Пользователи сторонятся функции profil из-за того, что она кажет- ся им слишком сложной; вместо нее они используют при компиляции программ на языке Си параметр, сообщающий компилятору о необходи- мости сгенерировать код, следящий за ходом выполнения процессов. 8.3.4 Учет и статистика В момент поступления прерывания по таймеру система может вы- полняться в режиме ядра или задачи, а также находиться в состоя- нии простоя (бездействия). Состояние простоя означает, что все процессы приостановлены в ожидании наступления события. Для каж- дого состояния процессора ядро имеет внутренние счетчики, уста- навливаемые при каждом прерывании по таймеру. Позже пользователь- ские процессы могут проанализировать накопленную ядром статисти- ческую информацию. В пространстве каждого процесса имеются два поля для записи продолжительности времени, проведенного процессом в режиме ядра и задачи. В ходе обработки прерываний по таймеру ядро корректирует значение поля, соответствующего текущему режиму выполнения про- цесса. Процессы-родители собирают статистику о своих потомках при выполнении функции wait, беря за основу информацию, поступающую от завершающих свое выполнение потомков. В пространстве каждого процесса имеется также одно поле для ведения учета использования памяти. В ходе обработки прерывания по таймеру ядро вычисляет общий объем памяти, занимаемый текущим процессом, исходя из размера частных областей процесса и его до- левого участия в использовании разделяемых областей памяти. Если, например, процесс использует области данных и стека размером 25 и 40 Кбайт, соответственно, и разделяет с четырьмя другими процес- сами одну область команд размером 50 Кбайт, ядро назначает про- цессу 75 Кбайт памяти (50К/5 + 25К + 40К). В системе с замещением страниц ядро вычисляет объем используемой памяти путем подсчета числа используемых в каждой области страниц. Таким образом, если прерываемый процесс имеет две частные области и еще одну область разделяет с другим процессом, ядро назначает ему столько страниц памяти, сколько содержится в этих частных областях, плюс половину страниц, принадлежащих разделяемой области. Вся указанная инфор- мация отражается в учетной записи при завершении процесса и может быть использована для расчетов с заказчиками машинного времени. 8.3.5 Поддержание времени в системе Ядро увеличивает показание системных часов при каждом преры- вании по таймеру, измеряя время в таймерных тиках от момента заг- рузки системы. Это значение возвращается процессу через системную функцию time и дает возможность определять общее время выполнения процесса. Время первоначального запуска процесса сохраняется яд- ром в адресном пространстве процесса при исполнении системной функции fork, в момент завершения процесса это значение вычитает- ся из текущего времени, результат вычитания и составляет реальное время выполнения процесса. В другой переменной таймера, устанав- ливаемой с помощью системной функции stime и корректируемой раз в секунду, хранится календарное время. 8.4 ВЫВОДЫ В настоящей главе был описан основной алгоритм диспетчериза- ции процессов в системе UNIX. С каждым процессом в системе связы- вается приоритет планирования, значение которого появляется в мо- мент перехода процесса в состояние приостанова и периодически корректируется программой обработки прерываний по таймеру. Прио- ритет, присваиваемый процессу в момент перехода в состояние при- останова, имеет значение, зависящее от того, какой из алгоритмов ядра исполнялся процессом в этот момент. Значение приоритета, присваиваемое процессу во время выполнения программой обработки прерываний по таймеру (или в тот момент, когда процесс возвраща- ется из режима ядра в режим задачи), зависит от того, сколько времени процесс занимал ЦП: процесс получает низкий приоритет, если он обращался к ЦП, и высокий - в противном случае. Системная функция nice дает процессу возможность влиять на собственный при- оритет путем добавления параметра, участвующего в пересчете прио- ритета. В главе были также рассмотрены системные функции, связанные с временем выполнения системы и протекающих в ней процессов: с ус- тановкой и получением системного времени, получением времени вы- полнения процессов и установкой сигналов "будильника". Кроме то- го, описаны функции программы обработки прерываний по таймеру, которая следит за временем в системе, управляет таблицей ответных сигналов, собирает статистику, а также подготавливает запуск пла- нировщика процессов, программы подкачки и "сборщика" страниц. Программа подкачки и "сборщик" страниц являются объектами расс- мотрения в следующей главе. 8.5 УПРАЖНЕНИЯ 1. При переводе процессов в состояние приостанова ядро назнача- ет процессу, ожидающему снятия блокировки с индекса, более высокий приоритет по сравнению с процессом, ожидающим осво- бождения буфера. Точно так же, процессы, ожидающие ввода с терминала, получают более высокий приоритет по сравнению с процессами, ожидающими возможности производить вывод на тер- минал. Объясните причины такого поведения ядра. *2. В алгоритме обработки прерываний по таймеру предусмотрен пе- ресчет приоритетов и перезапуск процессов на выполнение с интервалом в 1 секунду. Придумайте алгоритм, в котором ин- тервал перезапуска динамически меняется в зависимости от степени загрузки системы. Перевесит ли выигрыш усилия по ус- ложнению алгоритма ? 3. В шестой редакции системы UNIX для расчета продолжительности ИЦП текущим процессом используется следующая формула: decay(ИЦП) = max (пороговый приоритет, ИЦП-10); а в седьмой редакции: decay(ИЦП) = .8 * ИЦП; Приоритет процесса в обеих редакциях вычисляется по формуле: приоритет = ИЦП/16 + (базовый уровень приоритета); Повторите пример на Рисунке 8.4, используя приведенные фор- мулы. 4. Проделайте еще раз пример на Рисунке 8.4 с семью процессами вместо трех, а затем измените частоту прерываний по таймеру с 60 на 100 прерываний в секунду. Прокомментируйте резуль- тат. 5. Разработайте схему, в которой система накладывает ограниче- ние на продолжительность выполнения процесса, при превышении которого процесс завершается. Каким образом пользователь должен отличать такой процесс от процессов, для которых не должны существовать подобные ограничения ? Каким образом должна работать схема, если единственным условием является ее запуск из shell'а ? 6. Когда процесс выполняет системную функцию wait и обнаружива- ет прекратившего существование потомка, ядро приплюсовывает к его ИЦП значение поля ИЦП потомка. Чем объясняется такое "наказание" процесса-родителя ? 7. Команда nice запускает последующую команду с передачей ей указанного значения, например: nice 6 nroff -mm big_memo > output Напишите на языке Си программу, реализующую команду nice. 8. Проследите на примере Рисунка 8.4, каким образом будет осу- ществляться диспетчеризация процессов в том случае, если значение, передаваемое функцией nice для процесса A, равно 5 или -5. 9. Проведите эксперимент с системной функцией renice x y, где x - код идентификации процесса (активного), а y - новое значе- ние nice для указанного процесса. 10. Вернемся к примеру, приведенному на Рисунке 8.6. Предполо- жим, что группе, в которую входит процесс A, выделяется 33% процессорного времени, а группе, в которую входит процесс B, - 66% процессорного времени. В какой последовательности бу- дут исполняться процессы ? Обобщите алгоритм вычисления при- оритетов таким образом, чтобы значение группового ИЦП усред- нялось. 11. Выполните команду date. Команда без аргументов выводит текущую дату: указав аргумент, например: date mmddhhmmyy (супер)пользователь может установить текущую дату в системе (соответственно, месяц, число, часы, минуты и год). Так, date 0911205084 устанавливает в качестве текущего времени 11 сентября 1984 года 8:50 пополудни. 12. В программах можно использовать функцию пользовательского уровня sleep: sleep(seconds); с помощью которой выполнение программы приостанавливается на указанное число секунд. Разработайте ее алгоритм, в котором используйте системные функции alarm и pause. Что произойдет, если процесс вызовет функцию alarm раньше функции sleep ? Рассмотрите две возможности: 1) действие ранее вызванной функции alarm истекает в то время, когда процесс находится в состоянии приостанова, 2) действие ранее вызванной функции alarm истекает после завершения функции sleep. *13. Обратимся еще раз к последней проблеме. Ядро может выполнить переключение контекста во время исполнения функции sleep между вызовами alarm и pause. Тогда есть опасность, что про- цесс получит сигнал alarm до того, как вызовет функцию pause. Что произойдет в этом случае ? Как вовремя распознать эту ситуацию ? АЛГОРИТМЫ УПРАВЛЕНИЯ ПАМЯТЬЮ Алгоритм планирования использования процессорного времени, рассмотренный в предыдущей главе, в сильной степени зависит от выбранной стратегии управления памятью. Процесс может выполнять- ся, если он хотя бы частично присутствует в основной памяти; ЦП не может исполнять процесс, полностью выгруженный во внешнюю па- мять. Тем не менее, основная память - чересчур дефицитный ресурс, который зачастую не может вместить все активные процессы в систе- ме. Если, например, в системе имеется основная память объемом 8 Мбайт, то девять процессов размером по 1 Мбайту каждый уже не смогут в ней одновременно помещаться. Какие процессы в таком слу- чае следует размещать в памяти (хотя бы частично), а какие нет, решает подсистема управления памятью, она же управляет участками виртуального адресного пространства процесса, не резидентными в памяти. Она следит за объемом доступного пространства основной памяти и имеет право периодически переписывать процессы на уст- ройство внешней памяти, именуемое устройством выгрузки, освобож- дая в основной памяти дополнительное место. Позднее ядро может вновь поместить данные с устройства выгрузки в основную память. В ранних версиях системы UNIX процессы переносились между ос- новной памятью и устройством выгрузки целиком и, за исключением разделяемой области команд, отдельные независимые части процесса не могли быть объектами перемещения. Такая стратегия управления памятью называется свопингом (подкачкой). Такую стратегию имело смысл реализовывать на машине типа PDP-11, где максимальный раз- мер процесса составлял 64 Кбайта. При использовании этой страте- гии размер процесса ограничивается объемом физической памяти, доступной в системе. Система BSD (версия 4.0) явилась главным по- лигоном для применения другой стратегии, стратегии "подкачки по обращению" (demand paging), в соответствии с которой основная па- мять обменивается с внешней не процессами, а страницами памяти; эта стратегия поддерживается и в последних редакциях версии V системы UNIX. Держать в основной памяти весь выполняемый процесс нет необходимости, и ядро загружает в память только отдельные страницы по запросу выполняющегося процесса, ссылающегося на них. Преимущество стратегии подкачки по обращению состоит в том, что благодаря ей отображение виртуального адресного пространства про- цесса на физическую память машины становится более гибким: допус- кается превышение размером процесса объема доступной физической памяти и одновременное размещение в основной памяти большего чис- ла процессов. Преимущество стратегии свопинга состоит в простоте реализации и облегчении "надстроечной" части системы. Обе страте- гии управления памятью рассматриваются в настоящей главе. 9.1 СВОПИНГ Описание алгоритма свопинга можно разбить на три части: уп- равление пространством на устройстве выгрузки, выгрузка процессов из основной памяти и подкачка процессов в основную память. 9.1.1 Управление пространством на устройстве выгрузки Устройство выгрузки является устройством блочного типа, кото- рое представляет собой конфигурируемый раздел диска. Тогда как обычно ядро выделяет место для файлов по одному блоку за одну операцию, на устройстве выгрузки пространство выделяется группами смежных блоков. Пространство, выделяемое для файлов, используется статическим образом; поскольку схема назначения пространства под файлы действует в течение длительного периода времени, ее гиб- кость понимается в смысле сокращения числа случаев фрагментации и, следовательно, объемов неиспользуемого пространства в файловой системе. Выделение пространства на устройстве выгрузки, напротив, является временным, в сильной степени зависящим от механизма дис- петчеризации процессов. Процесс, размещаемый на устройстве выг- рузки, в конечном итоге вернется в основную память, освобождая место на внешнем устройстве. Поскольку время является решающим фактором и с учетом того, что ввод-вывод данных за одну мультиб- лочную операцию происходит быстрее, чем за несколько одноблочных операций, ядро выделяет на устройстве выгрузки непрерывное прост- ранство, не беря во внимание возможную фрагментацию. Так как схема выделения пространства на устройстве выгрузки отличается от схемы, используемой для файловых систем, структуры данных, регистрирующие свободное пространство, должны также отли- чаться. Пространство, свободное в файловых системах, описывается с помощью связного списка свободных блоков, доступ к которому осуществляется через суперблок файловой системы, информация о свободном пространстве на устройстве выгрузки собирается в табли- цу, именуемую "карта памяти устройства". Карты памяти, помимо ус- тройства выгрузки, используются и другими системными ресурсами (например, драйверами некоторых устройств), они дают возможность распределять память устройства (в виде смежных блоков) по методу первого подходящего. Каждая строка в карте памяти состоит из адреса распределяемо- го ресурса и количества доступных единиц ресурса; ядро интерпре- тирует элементы строки в соответствии с типом карты. В самом на- чале карта памяти состоит из одной строки, содержащей адрес и общее количество ресурсов. Если карта описывает распределение па- мяти на устройстве выгрузки, ядро трактует каждую единицу ресурса как группу дисковых блоков, а адрес - как смещение в блоках от начала области выгрузки. Первоначальный вид карты памяти для уст- ройства выгрузки, состоящего из 10000 блоков с начальным адресом, равным 1, показан на Рисунке 9.1. Выделяя и освобождая ресурсы, Адрес Число единиц ресурса здддддддддддддддддддддддддддддддддддд© Ё 1 10000 Ё юдддддддддддддддддддддддддддддддддддды Рисунок 9.1. Первоначальный вид карты памяти для устройства выгрузки здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм malloc /* алгоритм выделения пространства с ис-Ё Ё пользованием карты памяти */ Ё Ё входная информация: (1) адрес /* указывает на тип ис- Ё Ё пользуемой карты */ Ё Ё (2) требуемое число единиц ресурса Ё Ё выходная информация: адрес - в случае успешного завершения Ё Ё 0 - в противном случае Ё Ё { Ё Ё для (каждой строки карты) Ё Ё { Ё Ё если (требуемое число единиц ресурса располагается в Ё Ё строке карты) Ё Ё { Ё Ё если (требуемое число == числу единиц в строке) Ё Ё удалить строку из карты; Ё Ё в противном случае Ё Ё отрегулировать стартовый адрес в строке; Ё Ё вернуть (первоначальный адрес строки); Ё Ё } Ё Ё } Ё Ё вернуть (0); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 9.2. Алгоритм выделения пространства с помощью карт памяти ядро корректирует карту памяти, заботясь о том, чтобы в ней пос- тоянно содержалась точная информация о свободных ресурсах в сис- теме. На Рисунке 9.2 представлен алгоритм выделения пространства с помощью карт памяти (malloc). Ядро просматривает карту в поисках первой строки, содержащей количество единиц ресурса, достаточное для удовлетворения запроса. Если запрос покрывает все количество единиц, содержащееся в строке, ядро удаляет строку и уплотняет карту (то есть в карте становится на одну строку меньше). В про- тивном случае ядро переустанавливает адрес и число оставшихся единиц в строке в соответствии с числом единиц, выделенных по запросу. На Рисунке 9.3 показано, как меняется вид карты памяти для устройства выгрузки после выделения 100, 50 и вновь 100 еди- ниц ресурса. В конечном итоге карта памяти принимает вид, показы- вающий, что первые 250 единиц ресурса выделены по запросам, и что теперь остались свободными 9750 единиц, начиная с адреса 251. Освобождая ресурсы, ядро ищет для них соответствующее место в карте по адресу. При этом возможны три случая: Адрес Число единиц ресурса Адрес Число единиц ресурса здддддддддддддддддддддддддддддд© здддддддддддддддддддддддддддддд© Ё 1 10000 Ё Ё 101 9900 Ё юдддддддддддддддддддддддддддддды юдддддддддддддддддддддддддддддды (а) (б) Адрес Число единиц ресурса Адрес Число единиц ресурса здддддддддддддддддддддддддддддд© здддддддддддддддддддддддддддддд© Ё 151 9850 Ё Ё 251 9750 Ё юдддддддддддддддддддддддддддддды юдддддддддддддддддддддддддддддды (в) (г) Рисунок 9.3. Выделение пространства на устройстве выгрузки 1. Освободившиеся ресурсы полностью закрывают пробел в карте па- мяти. Другими словами, они имеют смежные адреса с адресами ресурсов из строк, непосредственно предшествующей и следующей за данной. В этом случае ядро объединяет вновь освободившиеся ресурсы с ресурсами из указанных строк в одну строку карты памяти. 2. Освободившиеся ресурсы частично закрывают пробел в карте па- мяти. Если они имеют адрес, смежный с адресом ресурсов из строки, непосредственно предшествующей или непосредственно следующей за данной (но не с адресами из обеих строк), ядро переустанавливает значение адреса и числа ресурсов в соот- ветствующей строке с учетом вновь освободившихся ресурсов. Число строк в карте памяти остается неизменным. 3. Освободившиеся ресурсы частично закрывают пробел в карте па- мяти, но их адреса не соприкасаются с адресами каких-либо других ресурсов карты. Ядро создает новую строку и вставляет ее в соответствующее место в карте. Возвращаясь к предыдущему примеру, отметим, что если ядро ос- вобождает 50 единиц ресурса, начиная с адреса 101, в карте памяти появится новая строка, поскольку освободившиеся ресурсы имеют ад- реса, не соприкасающиеся с адресами существующих строк карты. Ес- ли же затем ядро освободит 100 единиц ресурса, начиная с адреса 1, первая строка карты будет расширена, поскольку освободившиеся ресурсы имеют адрес, смежный с адресом первой строки. Эволюция состояний карты памяти для данного случая показана на Рисунке 9.4. Предположим, что ядру был сделан запрос на выделение 200 еди- ниц (блоков) пространства устройства выгрузки. Поскольку первая строка карты содержит информацию только о 150 единицах, ядро привлекает для удовлетворения запроса информацию из второй строки (см. Рисунок 9.5). Наконец, предположим, что ядро освобождает 350 Адрес Число единиц ресурса Адрес Число единиц ресурса здддддддддддддддддддддддддддддд© здддддддддддддддддддддддддддддд© Ё 251 9750 Ё Ё 101 50 Ё юдддддддддддддддддддддддддддддды цдддддддддддддддддддддддддддддд╢ Ё 251 9750 Ё (а) юдддддддддддддддддддддддддддддды (б) Адрес Число единиц ресурса здддддддддддддддддддддддддддддд© Ё 1 150 Ё цдддддддддддддддддддддддддддддд╢ Ё 251 9750 Ё юдддддддддддддддддддддддддддддды (в) Рисунок 9.4. Освобождение пространства на устройстве выгрузки Адрес Число единиц ресурса Адрес Число единиц ресурса здддддддддддддддддддддддддддддд© здддддддддддддддддддддддддддддд© Ё 1 150 Ё Ё 1 150 Ё цдддддддддддддддддддддддддддддд╢ цдддддддддддддддддддддддддддддд╢ Ё 251 9750 Ё Ё 451 9550 Ё юдддддддддддддддддддддддддддддды юдддддддддддддддддддддддддддддды (а) (б) Рисунок 9.5. Выделение пространства на устройстве выгрузки, описанного во второй строке карты памяти единиц пространства, начиная с адреса 151. Несмотря на то, что эти 350 единиц были выделены ядром в разное время, не существует причины, по которой ядро не могло бы освободить их все сразу. Яд- ро узнает о том, что освободившиеся ресурсы полностью закрывают разрыв между первой и второй строками карты, и вместо прежних двух создает одну строку, в которую включает и освободившиеся ре- сурсы. В традиционной реализации системы UNIX используется одно уст- ройство выгрузки, однако в последних редакциях версии V допуска- ется уже наличие множества устройств выгрузки. Ядро выбирает устройство выгрузки по схеме "кольцевого списка" при условии, что на устройстве имеется достаточный объем непрерывного адресного пространства. Администраторы могут динамически создавать и уда- лять из системы устройства выгрузки. Если устройство выгрузки удаляется из системы, ядро не выгружает данные на него; если же данные подкачиваются с удаляемого устройства, сначала оно опорож- няется и только после освобождения принадлежащего устройству пространства устройство может быть удалено из системы. 9.1.2 Выгрузка процессов Ядро выгружает процесс, если испытывает потребность в свобод- ной памяти, которая может возникнуть в следующих случаях: 1. Произведено обращение к системной функции fork, которая долж- на выделить место в памяти для процесса-потомка. 2. Произведено обращение к системной функции brk, увеличивающей размер процесса. 3. Размер процесса увеличился в результате естественного увели- чения стека процесса. 4. Ядру нужно освободить в памяти место для подкачки ранее выг- руженных процессов. Обращение к системной функции fork выделено в особую ситуа- цию, поскольку это единственный случай, когда пространство памяти, ранее занятое процессом (родителем), не освобождается. Когда ядро принимает решение о том, что процесс будет выгру- жен из основной памяти, оно уменьшает значение счетчика ссылок, ассоциированного с каждой областью процесса, и выгружает те об- ласти, у которых счетчик ссылок стал равным 0. Ядро выделяет мес- то на устройстве выгрузки и блокирует процесс в памяти (в случаях 1-3), запрещая его выгрузку (см. упражнение 9.12) до тех пор, по- ка не закончится текущая операция выгрузки. Адрес места выгрузки областей ядро сохраняет в соответствующих записях таблицы облас- тей. За одну операцию ввода-вывода, в которой участвуют устройство выгрузки и адресное пространство задачи и которая осуществляется через буферный кеш, ядро выгружает максимально-возможное коли- чество данных. Если аппаратура не в состоянии передать за одну операцию содержимое нескольких страниц памяти, перед программами ядра встает задача осуществить передачу содержимого памяти за несколько шагов по одной странице за каждую операцию. Таким обра- зом, точная скорость и механизм передачи данных определяются, по- мимо всего прочего, возможностями дискового контроллера и страте- гией распределения памяти. Например, если используется страничная организация памяти, существует вероятность, что выгружаемые дан- ные занимают несмежные участки физической памяти. Ядро обязано собирать информацию об адресах страниц с выгружаемыми данными, которую впоследствии использует дисковый драйвер, осуществляющий управление процессом ввода-вывода. Перед тем, как выгрузить сле- дующую порцию данных, программа подкачки (выгрузки) ждет заверше- ния предыдущей операции ввода-вывода. При этом перед ядром не встает задача переписать на устройство выгрузки содержимое виртуального адресного пространс- тва процесса полностью. Вместо этого ядро копирует на устройство выгрузки содержимое физической памяти, отведенной процессу, игно- рируя неиспользуемые виртуальные адреса. Когда ядро подкачивает процесс обратно в память, оно имеет у себя карту виртуальных ад- ресов процесса и может переназначить процессу новые адреса. Ядро считывает копию процесса из буферного кеша в физическую память, в те ячейки, для которых установлено соответствие с виртуальными адресами процесса. Расположение виртуальных адресов Устройство выгрузки Виртуальные, физические адреса здддддддддддддддддддддд© зддддддддддддддддд© Область Ё 0 278К ----╢-------684--╢---> Ё команд цдддддддддддддддддддддд╢ цддддддддддддддддд╢ Ё 1К 432К ----╢------------╢---> Ё цдддддддддддддддддддддд╢ цддддддддддддддддд╢ Ё пусто Ё з--------╢---> Ё цдддддддддддддддддддддд╢ Ы цддддддддддддддддд╢ Ё Ы Ё Ыз-------╢---> Ё Ё Ы Ё ЫЫ цддддддддддддддддд╢ Ё Ы Ё ЫЫз------╢---> Ё цдддддддддддддддддддддд╢ ЫЫЫ цддддддддддддддддд╢ Область Ё 64К 573К ----╢---ыЫЫ з----╢---> Ё данных цдддддддддддддддддддддд╢ ЫЫ Ы цддддддддддддддддд╢ Ё 65К 647К ----╢----ыЫ 690 Ё Ё цдддддддддддддддддддддд╢ Ы Ы юддддддддддддддддды Ё 66К 595К ----╢-----ы Ы цдддддддддддддддддддддд╢ Ы Ё пусто Ё Ы цдддддддддддддддддддддд╢ Ы Ё Ы Ё Ы Ё Ы Ё Ы Ё Ы Ё Ы цдддддддддддддддддддддд╢ Ы Область Ё128К 401К ----╢-------ы стека цдддддддддддддддддддддд╢ Ё пусто Ё юдддддддддддддддддддддды Рисунок 9.6. Отображение пространства процесса на устройство выгрузки На Рисунке 9.6 приведен пример отображения образа процесса в памяти на адресное пространство устройства выгрузки (*). Процесс располагает тремя областями: команд, данных и стека. Область ко- манд заканчивается на виртуальном адресе 2К, а область данных на- чинается с адреса 64К, таким образом в виртуальном адресном пространстве образовался пропуск в 62 Кбайта. Когда ядро выгружа- ет процесс, оно выгружает содержимое страниц памяти с адресами 0, 1К, 64К, 65К, 66К и 128К; на устройстве выгрузки не будет отведе- но место под пропуск в 62 Кбайта между областями команд и данных, как и под пропуск в 61 Кбайт между областями данных и стека, ибо пространство на устройстве выгрузки заполняется непрерывно. Когда ядро загружает процесс обратно в память, оно уже знает из карты памяти процесса о том, что процесс имеет в своем пространстве не- используемый участок размером 62К, и с учетом этого соответствен- но выделяет физическую память. Этот случай проиллюстрирован с по- мощью Рисунка 9.7. Сравнение Рисунков 9.6 и 9.7 показывает, что физические адреса, занимаемые процессом до и после выгрузки, не ддддддддддддддддддддддддддддддддддддддд (*) Для простоты виртуальное адресное пространство процесса на этом и на всех последующих рисунках изображается в виде ли- нейного массива точек входа в таблицу страниц, не принимая во внимание тот факт, что каждая область обычно имеет свою от- дельную таблицу страниц. Расположение виртуальных адресов Устройство выгрузки Виртуальные, физические адреса здддддддддддддддддддддд© зддддддддддддддддд© Область Ё 0 401К <---╢-------684--╢---- Ё команд цдддддддддддддддддддддд╢ цддддддддддддддддд╢ Ё 1К 370К <---╢------------╢---- Ё цдддддддддддддддддддддд╢ цддддддддддддддддд╢ Ё пусто Ё з--------╢---- Ё цдддддддддддддддддддддд╢ Ы цддддддддддддддддд╢ Ё Ы Ё Ыз-------╢---- Ё Ё Ы Ё ЫЫ цддддддддддддддддд╢ Ё Ы Ё ЫЫз------╢---- Ё цдддддддддддддддддддддд╢ ЫЫЫ цддддддддддддддддд╢ Область Ё 64К 788К <---╢---ыЫЫ з----╢---- Ё данных цдддддддддддддддддддддд╢ ЫЫ Ы цддддддддддддддддд╢ Ё 65К 492К <---╢----ыЫ 690 Ё Ё цдддддддддддддддддддддд╢ Ы Ы юддддддддддддддддды Ё 66К 647К <---╢-----ы Ы цдддддддддддддддддддддд╢ Ы Ё пусто Ё Ы цдддддддддддддддддддддд╢ Ы Ё Ы Ё Ы Ё Ы Ё Ы Ё Ы Ё Ы цдддддддддддддддддддддд╢ Ы Область Ё128К 955К <---╢-------ы стека цдддддддддддддддддддддд╢ Ё пусто Ё юдддддддддддддддддддддды Рисунок 9.7. Загрузка процесса в память совпадают между собой; однако, на пользовательском уровне процесс не обращает на это никакого внимания, поскольку содержимое его виртуального пространства осталось тем же самым. Теоретически все пространство памяти, занятое процессом, в том числе его личное адресное пространство и стек ядра, может быть выгружено, хотя ядро и может временно заблокировать область в памяти на время выполнения критической операции. Однако практи- чески, ядро не выгружает содержимое адресного пространства про- цесса, если в нем находятся таблицы преобразования адресов (ад- ресные таблицы) процесса. Практическими соображениями так же диктуются условия, при которых процесс может выгрузить самого се- бя или потребовать своей выгрузки другим процессом (см. упражне- ние 9.4). 9.1.2.1 Выгрузка при выполнении системной функции fork В описании системной функции fork (раздел 7.1) предполага- лось, что процесс-родитель получил в свое распоряжение память, достаточную для создания контекста потомка. Если это условие не выполняется, ядро выгружает процесс из памяти, не освобождая пространство памяти, занимаемое его (родителя) копией. Когда про- цедура выгрузки завершится, процесс-потомок будет располагаться на устройстве выгрузки; процесс-родитель переводит своего потомка в состояние "готовности к выполнению" (см. Рисунок 6.1) и возвра- щается в режим задачи. Поскольку процесс-потомок находится в сос- тоянии "готовности к выполнению", программа подкачки в конце кон- цов загрузит его в память, где ядро запустит его на выполнение; потомок завершит тем самым свою роль в выполнении системной функ- ции fork и вернется в режим задачи. Первоначальное Расширенный расположение формат Виртуальные, Виртуальные, Устройство физические адреса физические адреса выгрузки здддддддддддд© здддддддддддд© зддддддддд© ОбластьЁ 0 278К Ё Ё 0 278К -╢-----684--╢---> Ё команд цдддддддддддд╢ цдддддддддддд╢ цддддддддд╢ Ё 1К 432К Ё Ё 1К 432К -╢----------╢---> Ё цдддддддддддд╢ цдддддддддддд╢ цддддддддд╢ Ё пусто Ё Ё пусто Ё з--------╢---> Ё цдддддддддддд╢ цдддддддддддд╢ Ы цддддддддд╢ Ё Ы Ё Ё Ы Ё Ыз-------╢---> Ё Ё Ы Ё Ё Ы Ё ЫЫ цддддддддд╢ Ё Ы Ё Ё Ы Ё ЫЫз------╢---> Ё цдддддддддддд╢ цдддддддддддд╢ ЫЫЫ цддддддддд╢ ОбластьЁ 64К 573К Ё Ё 64К 573К -╢-ыЫЫ з----╢---> Ё данных цдддддддддддд╢ цдддддддддддд╢ ЫЫ Ы цддддддддд╢ Ё 65К 647К Ё Ё 65К 647К -╢--ыЫ 690 Ё Ё цдддддддддддд╢ цдддддддддддд╢ Ы Ы цддддддддд╢ Ё 66К 595К Ё Ё 66К 595К -╢---ы 691 зЁ---> Ё цдддддддддддд╢ цдддддддддддд╢ Ы Ыюддддддддды Ё пусто Ё Ё пусто Ё Ы Ы цдддддддддддд╢ цдддддддддддд╢ Ы Ы Ё Ы Ё Ё Ы Ё Ы Ы Ё Ы Ё Ё Ы Ё Ы Ы Ё Ы Ё Ё Ы Ё Ы Ы цдддддддддддд╢ цдддддддддддд╢ Ы Ы ОбластьЁ128К 401К Ё Ё128К 401К -╢-----ы Ы стека цдддддддддддд╢ цдддддддддддд╢ Ы Ё пусто Ё Новая Ё129К ... -╢---------ы юдддддддддддды страницацдддддддддддд╢ Ё пусто Ё юдддддддддддды Рисунок 9.8. Перенастройка карты памяти в случае выгрузки с расширением 9.1.2.2 Выгрузка с расширением Если процесс испытывает потребность в дополнительной физичес- кой памяти, либо в результате расширения стека, либо в результате запуска функции brk, и если эта потребность превышает доступные резервы памяти, ядро выполняет операцию выгрузки процесса с рас- ширением его размера на устройстве выгрузки. На устройстве выг- рузки ядро резервирует место для размещения процесса с учетом расширения его размера. Затем производится перенастройка таблицы преобразования адресов процесса с учетом дополнительного вирту- ального пространства, но без выделения физической памяти (в связи с ее отсутствием). Наконец, ядро выгружает процесс, выполняя про- цедуру выгрузки обычным порядком и обнуляя вновь выделенное пространство на устройстве (см. Рисунок 9.8). Когда несколько позже ядро будет загружать процесс обратно в память, физическое пространство будет выделено уже с учетом нового состояния таблицы преобразования адресов. В момент возобновления у процесса уже бу- дет в распоряжении память достаточного объема. 9.1.3 Загрузка (подкачка) процессов Нулевой процесс (процесс подкачки) является единственным про- цессом, загружающим другие процессы в память с устройств выгруз- ки. Процесс подкачки начинает работу по выполнению этой своей единственной функции по окончании инициализации системы (как уже говорилось в разделе 7.9). Он загружает процессы в память и, если ему не хватает места в памяти, выгружает оттуда некоторые из про- цессов, находящихся там. Если у процесса подкачки нет работы (например, отсутствуют процессы, ожидающие загрузки в память) или же он не в состоянии выполнить свою работу (ни один из процессов не может быть выгружен), процесс подкачки приостанавливается; яд- ро периодически возобновляет его выполнение. Ядро планирует за- пуск процесса подкачки точно так же, как делает это в отношении других процессов, ориентируясь на более высокий приоритет, при этом процесс подкачки выполняется только в режиме ядра. Процесс подкачки не обращается к функциям операционной системы, а исполь- зует в своей работе только внутренние функции ядра; он является архетипом всех процессов ядра. Как уже вкратце говорилось в главе 8, программа обработки прерываний по таймеру измеряет время нахождения каждого процесса в памяти или в состоянии выгрузки. Когда процесс подкачки возоб- новляет свою работу по загрузке процессов в память, он просматри- вает все процессы, находящиеся в состоянии "готовности к выполне- нию, будучи выгруженными", и выбирает из них один, который находится в этом состоянии дольше остальных (см. Рисунок 9.9). Если имеется достаточно свободной памяти, процесс подкачки загру- жает выбранный процесс, выполняя операции в последовательности, обратной выгрузке процесса. Сначала выделяется физическая память, затем с устройства выгрузки считывается нужный процесс и освобож- дается место на устройстве. Если процесс подкачки выполнил процедуру загрузки успешно, он вновь просматривает совокупность выгруженных, но готовых к выпол- нению процессов в поисках следующего процесса, который предпола- гается загрузить в память, и повторяет указанную последователь- ность действий. В конечном итоге возникает одна из следующих ситуаций: * На устройстве выгрузки больше нет ни одного процесса, готово- го к выполнению. Процесс подкачки приостанавливает свою рабо- ту до тех пор, пока не возобновится процесс на устройстве выгрузки или пока ядро не выгрузит процесс, готовый к выпол- нению. (Вспомним диаграмму состояний на Рисунке 6.1). * Процесс подкачки обнаружил процесс, готовый к загрузке, но в системе недостаточно памяти для его размещения. Процесс под- качки пытается загрузить другой процесс и в случае успеха пе- резапускает алгоритм подкачки, продолжая поиск загружаемых процессов. Если процессу подкачки нужно выгрузить процесс, он просматри- вает все процессы в памяти. Прекратившие свое существование про- цессы не подходят для выгрузки, поскольку они не занимают физи- ческую память; также не могут быть выгружены процессы, заблокиро- ванные в памяти, например, выполняющие операции над областями. Ядро предпочитает выгружать приостановленные процессы, поскольку процессы, готовые к выполнению, имеют больше шансов быть вскоре выбранными на выполнение. Решение о выгрузке процесса принимается ядром на основании его приоритета и продолжительности его пребы- вания в памяти. Если в памяти нет ни одного приостановленного процесса, решение о том, какой из процессов, готовых к выполне- нию, следует выгрузить, зависит от значения, присвоенного процес- су функцией nice, а также от продолжительности пребывания процес- са в памяти. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм swapper /* загрузка выгруженных процессов, Ё Ё * выгрузка других процессов с целью Ё Ё * расчистки места в памяти */ Ё Ё входная информация: отсутствует Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё loop: Ё Ё для (всех выгруженных процессов, готовых к выполнению)Ё Ё выбрать процесс, находящийся в состоянии выгружен-Ё Ё ности дольше остальных; Ё Ё если (таких процессов нет) Ё Ё { Ё Ё приостановиться (до момента, когда возникнет необ-Ё Ё ходимость в загрузке процессов); Ё Ё перейти на loop; Ё Ё } Ё Ё если (в основной памяти достаточно места для размеще- Ё Ё ния процесса) Ё Ё { Ё Ё загрузить процесс; Ё Ё перейти на loop; Ё Ё } Ё Ё /* loop2: сюда вставляются исправления, внесенные в алго- Ё Ё * ритм */ Ё Ё для (всех процессов, загруженных в основную память, Ё Ё кроме прекративших существование и заблокированных в Ё Ё памяти) Ё Ё { Ё Ё если (есть хотя бы один приостановленный процесс) Ё Ё выбрать процесс, у которого сумма приоритета иЁ Ё продолжительности нахождения в памяти наи- Ё Ё большая; Ё Ё в противном случае /* нет ни одного приостанов- Ё Ё * ленного процесса */ Ё Ё выбрать процесс, у которого сумма продолжи- Ё Ё тельности нахождения в памяти и значения niceЁ Ё наибольшая; Ё Ё } Ё Ё если (выбранный процесс не является приостановленным Ё Ё или не соблюдены условия резидентности) Ё Ё приостановиться (до момента, когда появится воз- Ё Ё можность загрузить процесс); Ё Ё в противном случае Ё Ё выгрузить процесс; Ё Ё перейти на loop; /* на loop2 в исправленном алгорит-Ё Ё * ме */ Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 9.9. Алгоритм подкачки Процесс, готовый к выполнению, должен быть резидентным в па- мяти в течение по меньшей мере 2 секунд до того, как уйти из нее, а процесс, загружаемый в память, должен по меньшей мере 2 секунды пробыть на устройстве выгрузки. Если процесс подкачки не может найти ни одного процесса, подходящего для выгрузки, или ни одного процесса, подходящего для загрузки, или ни одного процесса, перед выгрузкой не менее 2 секунд (**) находившегося в памяти, он при- останавливает свою работу по причине того, что ему нужно загрузить процесс в память, а в памяти нет места для его размеще- ния. В этой ситуации таймер возобновляет выполнение процесса под- качки через каждую секунду. Ядро также возобновляет работу про- цесса подкачки в том случае, когда один из процессов переходит в состояние приостанова, так как последний может оказаться более подходящим для выгрузки процессом по сравнению с ранее рассмот- ренными. Если процесс подкачки расчистил место в памяти или если он был приостановлен по причине невозможности сделать это, он во- зобновляет свою работу с перезапуска алгоритма подкачки (с самого его начала), вновь предпринимая попытку загрузить ожидающие вы- полнения процессы. На Рисунке 9.10 показана динамика выполнения пяти процессов с указанием моментов их участия в реализации алгоритма подкачки. Положим для простоты, что все процессы интенсивно используют ре- сурсы центрального процессора и что они не производят обращений к системным функциям; следовательно, переключение контекста проис- ходит только в результате возникновения прерываний по таймеру с интервалом в 1 секунду. Процесс подкачки исполняется с наивысшим приоритетом планирования, поэтому он всегда укладывается в се- кундный интервал, когда ему есть что делать. Предположим далее, что процессы имеют одинаковый размер и что в основной памяти мо- гут одновременно поместиться только два процесса. Сначала в памя- ти находятся процессы A и B, остальные процессы выгружены. Про- цесс подкачки не может стронуть с места ни один процесс в течение первых двух секунд, поскольку этого требует условие нахождения перемещаемого процесса в течение этого интервала на одном месте (в памяти или на устройстве выгрузки), однако по истечении 2 се- кунд процесс подкачки выгружает процессы A и B и загружает на их место процессы C и D. Он пытается также загрузить и процесс E, но терпит неудачу, поскольку в основной памяти недостаточно места для этого. На 3-секундной отметке процесс E все еще годен для загрузки, поскольку он находился все 3 секунды на устройстве выг- рузки, но процесс подкачки не может выгрузить из памяти ни один из процессов, ибо они находятся в памяти менее 2 секунд. На 4-се- кундной отметке процесс подкачки выгружает процессы C и D и заг- ружает вместо них процессы E и A. Процесс подкачки выбирает процессы для загрузки, основываясь на продолжительности их пребывания на устройстве выгрузки. В ка- честве другого критерия может применяться более высокий приоритет загружаемого процесса по сравнению с остальными, готовыми к вы- полнению процессами, поскольку такой процесс более предпочтителен для запуска. Практика показала, что такой подход "несколько" по- вышает пропускную способность системы в условиях сильной загру- женности (см. [Peachey 84]). Алгоритм выбора процесса для выгрузки из памяти с целью осво- бождения места требуемого объема имеет, однако, более серьезные изъяны. Во-первых, процесс подкачки производит выгрузку на осно- вании приоритета, продолжительности нахождения в памяти и значе- ния nice. Несмотря на то, что он производит выгрузку процесса с единственной целью - освободить в памяти место для загружаемого процесса, он может выгрузить и процесс, который не освобождает место требуемого размера. Например, если процесс подкачки пытает- ся загрузить в память процесс размером 1 Мбайт, а в системе от- сутствует свободная память, будет далеко не достаточно выгрузить процесс, занимающий только 2 Кбайта памяти. В качестве альтерна- ддддддддддддддддддддддддддддддддддддддд (**) В версии 6 системы UNIX процесс не может быть выгружен из памяти с целью расчистки места для загружаемого процесса до тех пор, пока загружаемый процесс не проведет на диске 3 се- кунды. Уходящий из памяти процесс должен провести в памяти не менее 2 секунд. Временной интервал таким образом делится на части, в результате чего повышается производительность системы. Время Процесс A B C D E ддбддддддддддддддбдддддддддддбдддддддддддбдддддддддддбддддддддддд 0Ё 0 Ё 0 Ё выгружен Ё выгружен Ё выгружен Ё запущен Ё Ё 0 Ё 0 Ё 0 Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 1 Ё 1 Ё 1 Ё 1 Ё 1 1Ё Ё запущен Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 2 Ё 2 Ё 2 Ё 2 Ё 2 2Ё выгружен Ё выгружен Ё загружен Ё загружен Ё Ё 0 Ё 0 Ё 0 Ё 0 Ё Ё Ё Ё запущен Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 1 Ё 1 Ё 1 Ё 1 Ё 3 3Ё Ё Ё Ё запущен Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 2 Ё 2 Ё 2 Ё 2 Ё 4 4Ё загружен Ё Ё выгружен Ё выгружен Ё загружен Ё 0 Ё Ё 0 Ё 0 Ё 0 Ё Ё Ё Ё Ё запущен Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 1 Ё 3 Ё 1 Ё 1 Ё 1 5Ё запущен Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 2 Ё 4 Ё 2 Ё 2 Ё 2 6Ё выгружен Ё загружен Ё загружен Ё Ё выгружен Ё 0 Ё 0 Ё 0 Ё Ё 0 Ё Ё запущен Ё Ё Ё Ё Ё Ё Ё Ё v Рисунок 9.10. Последовательность операций, выполняемых процессом подкачки Время Процесс A B C D E ддбддддддддддддддбдддддддддддбдддддддддддбдддддддддддбддддддддддд 0Ё 0 Ё 0 Ё выгружен Ё nice 25 Ё выгружен Ё запущен Ё Ё 0 Ё выгружен Ё 0 Ё Ё Ё Ё 0 Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 1 Ё 1 Ё 1 Ё 1 Ё 1 1Ё Ё запущен Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 2 Ё 2 Ё 2 Ё 2 Ё 2 2Ё выгружен Ё выгружен Ё загружен Ё загружен Ё Ё 0 Ё 0 Ё 0 Ё 0 Ё Ё Ё Ё запущен Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 1 Ё 1 Ё 1 Ё 1 Ё 3 3Ё Ё Ё Ё выгружен Ё загружен Ё Ё Ё Ё 0 Ё 0 Ё Ё Ё Ё Ё запущен Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 2 Ё 2 Ё 2 Ё 1 Ё 1 4Ё загружен Ё Ё выгружен Ё Ё Ё 0 Ё Ё 0 Ё Ё Ё запущен Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 1 Ё 3 Ё 1 Ё 2 Ё 2 5Ё Ё загружен Ё Ё Ё выгружен Ё Ё 0 Ё Ё Ё 0 Ё Ё запущен Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ддед 2 Ё 1 Ё 2 Ё 3 Ё 1 6Ё выгружен Ё Ё Ё загружен Ё Ё 0 Ё Ё Ё 0 Ё Ё Ё Ё Ё запущен Ё Ё Ё Ё Ё Ё v Рисунок 9.11. Загрузка процессов в случае разбивки временных интервалов на части тивы может быть предложена стратегия выгрузки групп процессов при условии, что они освобождают место, достаточное для размещения загружаемых процессов. Эксперименты с использованием машины PDP 11/23 показали, что в условиях сильной загруженности такая стра- тегия может увеличить производительность системы почти на 10 про- центов (см. [Peachey 84]). Во-вторых, если процесс подкачки приостановил свою работу из- за того, что в памяти не хватило места для загрузки процесса, после возобновления он вновь выбирает процесс для загрузки в па- мять, несмотря на то, что ранее им уже был сделан выбор. Причина такого поведения заключается в том, что за прошедшее время в состояние готовности к выполнению могли перейти другие выгружен- ные процессы, более подходящие для загрузки в память по сравнению с ранее выбранным процессом. Однако от этого мало утешения для ранее выбранного процесса, все еще пытающегося загрузиться в па- мять. В некоторых реализациях процесс подкачки стремится к тому, чтобы перед загрузкой в память одного крупного процесса выгрузить большое количество процессов маленького размера, это изменение в базовом алгоритме подкачки отражено в комментариях к алгоритму (Рисунок 9.9). В-третьих, если процесс подкачки выбирает для выгрузки про- цесс, находящийся в состоянии "готовности к выполнению", не иск- лючена возможность того, что этот процесс после загрузки в память ни разу не был запущен на исполнение. Этот случай показан на Ри- сунке 9.11, из которого видно, что ядро загружает процесс D на 2- секундной отметке, запускает процесс C, а затем на 3-секундной отметке процесс D выгружается в пользу процесса E (уступая пос- леднему в значении nice), несмотря на то, что процессу D так и не был предоставлен ЦП. Понятно, что такая ситуация является нежела- тельной. Следует упомянуть еще об одной опасности. Если при попытке выгрузить процесс на устройстве выгрузки не будет найдено свобод- ное место, в системе может возникнуть тупиковая ситуация, при ко- торой: все процессы в основной памяти находятся в состоянии при- останова, все готовые к выполнению процессы выгружены, для новых процессов на устройстве выгрузки уже нет места, нет свободного места и в основной памяти. Эта ситуация разбирается в упражнении 9.5. Интерес к проблемам, связанным с подкачкой процессов, в пос- ледние годы спал в связи с реализацией алгоритмов подкачки стра- ниц памяти. 9.2 ПОДКАЧКА ПО ЗАПРОСУ Алгоритм подкачки страниц памяти поддерживается на машинах со страничной организацией памяти и с ЦП, имеющим прерываемые коман- ды (***). В системах с подкачкой страниц отсутствуют ограничения на размер процесса, связанные с объемом доступной физической па- мяти. Например, в машинах с объемом физической памяти 1 и 2 Мбай- та могут исполняться процессы размером 4 или 5 Мбайт. Ограничение на виртуальный размер процесса, связанное с объемом адресуемой виртуальной памяти, остается в силе и здесь. Поскольку процесс может не поместиться в физической памяти, ядру приходится динами- чески загружать в память отдельные его части и исполнять их, нес- мотря на отсутствие остальных частей. В механизме подкачки стра- ниц все открыто для пользовательских программ, за исключением ддддддддддддддддддддддддддддддддддддддд (***) Если при исполнении команды возникает ошибка, связанная с отсутствием страницы, после обработки ошибки ЦП обязан пе- резапустить команду, поскольку промежуточные результаты, полученные к моменту возникновения ошибки, могут быть утра- чены. разрешенного процессу виртуального размера. Процессы стремятся исполнять команды небольшими порциями, ко- торые именуются программными циклами или подпрограммами, исполь- зуемые ими указатели группируются в небольшие поднаборы, распола- гаемые в информационном пространстве процесса. В этом состоит суть так называемого принципа "локальности". Деннингом [Denning 68] было сформулировано понятие рабочего множества процесса как совокупности страниц, использованных процессом в последних n ссылках на адресное пространство памяти; число n называется окном рабочего множества. Поскольку рабочее множество процесса является частью от целого, в основной памяти может поместиться больше про- цессов по сравнению с теми системами, где управление памятью ба- зируется на подкачке процессов, что в конечном итоге приводит к увеличению производительности системы. Когда процесс обращается к странице, отсутствующей в его рабочем множестве, возникает ошиб- ка, при обработке которой ядро корректирует рабочее множество процесса, в случае необходимости подкачивая страницы с внешнего устройства. На Рисунке 9.12 приведена последовательность используемых процессом указателей страниц, описывающих рабочие множества с ок- нами различных размеров при условии соблюдения алгоритма замеще- ния "стариков" (замещения страниц путем откачки тех, к которым наиболее долго не было обращений). По мере выполнения процесса его рабочее множество видоизменяется в соответствии с используе- мыми процессом указателями страниц; увеличение размера окна вле- чет за собой увеличение рабочего множества и, с другой стороны, сокращение числа ошибок в выполнении процесса. Использование не- изменного рабочего множества не практикуется, поскольку запомина- ние очередности следования указателей страниц потребовало бы слишком больших затрат. Приблизительное соответствие между изме- няемым рабочим множеством и пространством процесса достигается путем установки бита упоминания (reference bit) при обращении к странице памяти, а также периодическим опросом указателей стра- ниц. Если на страницу была сделана ссылка, эта страница включает- ся в рабочее множество; в противном случае она "дозревает" в па- мяти в ожидании своей очереди. В случае возникновения ошибки из-за обращения к странице, отсутствующей в рабочем множестве, ядро приостанавливает выполне- ние процесса до тех пор, пока страница не будет считана в память и не станет доступной процессу. Когда страница будет загружена, процесс перезапустит ту команду, на которой выполнение процесса было приостановлено из-за ошибки. Таким образом, работа подсисте- мы замещения страниц распадается на две части: откачка редко ис- пользуемых страниц на устройство выгрузки и обработка ошибок из-за отсутствия нужной страницы. Такое общее толкование механиз- ма замещения страниц, конечно же, выходит за пределы одной конк- ретной системы. Оставшуюся часть главы мы посвятим более деталь- ному рассмотрению особенностей реализации этого механизма в версии V системы UNIX. 9.2.1 Структуры данных, используемые подсистемой замещения страниц Для поддержки функций управления памятью на машинном (низком) уровне и для реализации механизма замещения страниц ядро исполь- зует 4 основные структуры данных: записи таблицы страниц, деск- рипторы дисковых блоков, таблицу содержимого страничных блоков (page frame data table - сокращенно: pfdata) и таблицу использо- вания области подкачки. Место для таблицы pfdata выделяется один раз на все время жизни системы, для других же структур страницы памяти выделяются динамически. Из главы 6 нам известно, что каждая область располагает свои- ми таблицами страниц, с помощью которых осуществляется доступ к физической памяти. Каждая запись таблицы страниц (Рисунок 9.13) состоит из физического адреса страницы, кода защиты, в разрядах которого описываются права доступа процесса к странице (на чте- ние, запись и исполнение), а также следующих двоичных полей, ис- пользуемых механизмом замещения страниц: Последователь- ность указате- Рабочие множества Размеры окон лей страниц 2 3 4 5 здддддддддддд© здддддддбддддддддддбдддддддддддддбдддддддддддддддд© Ё 24 Ё Ё 24 Ё 24 Ё 24 Ё 24 Ё цдддддддддддд╢ Ё Ё Ё Ё Ё Ё 15 Ё Ё 15 24 Ё 15 24 Ё 15 24 Ё 15 24 Ё цдддддддддддд╢ Ё Ё Ё Ё Ё Ё 18 Ё Ё 18 15 Ё 18 15 24 Ё 18 15 24 Ё 18 15 24 Ё цдддддддддддд╢ Ё Ё Ё Ё Ё Ё 23 Ё Ё 23 18 Ё 23 18 15 Ё 23 18 15 24 Ё 23 18 15 24 Ё цдддддддддддд╢ Ё Ё Ё Ы Ё Ы Ё Ё 24 Ё Ё 24 23 Ё 24 23 18 Ё Ы Ё Ы Ё цдддддддддддд╢ Ё Ё Ё Ы Ё Ы Ё Ё 17 Ё Ё 17 24 Ё 17 24 23 Ё 17 24 23 18 Ё 17 24 23 18 15 Ё цдддддддддддд╢ Ё Ё Ё Ы Ё Ы Ё Ё 18 Ё Ё 18 17 Ё 18 17 24 Ё Ы Ё Ы Ё цдддддддддддд╢ Ё Ё Ы Ё Ы Ё Ы Ё Ё 24 Ё Ё 24 18 Ё Ы Ё Ы Ё Ы Ё цдддддддддддд╢ Ё Ё Ы Ё Ы Ё Ы Ё Ё 18 Ё Ё 18 24 Ё Ы Ё Ы Ё Ы Ё цдддддддддддд╢ Ё Ё Ы Ё Ы Ё Ы Ё Ё 17 Ё Ё 17 18 Ё Ы Ё Ы Ё Ы Ё цдддддддддддд╢ Ё Ё Ы Ё Ы Ё Ы Ё Ё 17 Ё Ё Ё Ы Ё Ы Ё Ы Ё цдддддддддддд╢ Ё Ё Ы Ё Ы Ё Ы Ё Ё 15 Ё Ё 15 17 Ё 15 17 18 Ё 15 17 18 24 Ё Ы Ё цдддддддддддд╢ Ё Ё Ё Ы Ё Ы Ё Ё 24 Ё Ё 24 15 Ё 24 15 17 Ё Ы Ё Ы Ё цдддддддддддд╢ Ё Ё Ы Ё Ы Ё Ы Ё Ё 17 Ё Ё 17 24 Ё Ы Ё Ы Ё Ы Ё цдддддддддддд╢ Ё Ё Ы Ё Ы Ё Ы Ё Ё 24 Ё Ё 24 17 Ё Ы Ё Ы Ё Ы Ё цдддддддддддд╢ Ё Ё Ы Ё Ы Ё Ы Ё Ё 18 Ё Ё 18 24 Ё 18 24 17 Ё Ы Ё Ы Ё юдддддддддддды юдддддддаддддддддддадддддддддддддадддддддддддддддды Рисунок 9.12. Рабочее множество процесса * бит доступности * бит упоминания * бит модификации * бит копирования при записи * "возраст" страницы Установка бита доступности свидетельствует о правильности со- держимого страницы памяти, однако из того, что бит доступности выключен, не следует с необходимостью то, что ссылка на страницу недопустима, в чем мы убедимся позже. Бит упоминания устанавлива- ется в том случае, если процесс делает ссылку на страницу, а бит модификации - в том случае, если процесс скорректировал содержи- мое страницы. Установка бита копирования при записи, производимая во время выполнения системной функции fork, свидетельствует о том, что ядру в случае, когда процесс корректирует содержимое страницы, следует создавать ее новую копию. Наконец, "возраст" страницы говорит о продолжительности ее пребывания в составе ра- бочего множества процесса. Биты доступности, копирования при за- писи и "возраст" страницы устанавливаются ядром, биты упоминания и модификации - аппаратным путем; в разделе 9.2.4 рассматриваются конфигурации, в которых эти возможности не поддерживаются аппара- турой. здддддддд© зд>зддддддддддддддддддддддбддддддддддддддддддддддддддд© Ё Ё Ё Ё Ё Ё Ё Ё Ё цддддддддддддддддддддддеддддддддддддддддддддддддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё Ё цддддддддддддддддддддддеддддддддддддддддддддддддддд╢ Ё ОбластьЁ Ё Ё Ё Ё Ё Ё Ё цддддддддддддддддддддддеддддддддддддддддддддддддддд╢ Ё Ё Ё ЁЗаписи таблицы страницЁДескрипторы дисковых блоковЁ Ё ддддеды цддддддддддддддддддддддеддддддддддддддддддддддддддд╢ Ё Ё Ё Ё Ё Ё Ё цддддддддддддддддддддддеддддддддддддддддддддддддддд╢ Ё Ё Ё Ё Ё юдддддддды цддддддддддддддддддддддеддддддддддддддддддддддддддд╢ Ё Ё Ё цддддддддддддддддддддддеддддддддддддддддддддддддддд╢ Ё Ё Ё цддддддддддддддддддддддеддддддддддддддддддддддддддд╢ Ё Ё Ё цддддддддддддддддддддддеддддддддддддддддддддддддддд╢ Ё Ё Ё юддддддддддддддддддддддаддддддддддддддддддддддддддды Запись таблицы страниц здддддддддддддддддддддддддддддбдддддддбдддддбдддддбддддбдддддбддд© Ё Адрес страницы (физический) ЁВозрастЁКопи-ЁМоди-ЁУпо-ЁДо- ЁЗа-Ё Ё Ё Ёрова-Ёфика-Ёми- Ёпус- Ёщи-Ё Ё Ё Ёние Ёция Ёна- Ёти- Ёта Ё Ё Ё Ёпри Ё Ёние ЁмостьЁ Ё Ё Ё Ёзапи-Ё Ё Ё Ё Ё Ё Ё Ёси Ё Ё Ё Ё Ё юдддддддддддддддддддддддддддддадддддддадддддадддддаддддадддддаддды Дескриптор дискового блока здддддддддддддддддддддддбдддддддддддддддбдддддддддддддддддддддддд© Ё Устройство выгрузки Ё Номер блока Ё Тип (находится на ус- Ё Ё Ё Ё тройстве выгрузки, в Ё Ё Ё Ё файле, при обращении Ё Ё Ё Ё обнуляется, заполняет- Ё Ё Ё Ё ся) Ё юдддддддддддддддддддддддадддддддддддддддадддддддддддддддддддддддды Рисунок 9.13. Записи таблицы страниц и дескрипторы дисковых блоков Каждая запись таблицы страниц связана с дескриптором дисково- го блока, описывающим дисковую копию виртуальной страницы (Рису- нок 9.13). Поэтому процессы, использующие разделяемую область, обращаются к общим записям таблицы страниц и к одним и тем же дескрипторам дисковых блоков. Содержимое виртуальной страницы располагается либо в отдельном блоке на устройстве выгрузки, либо в исполняемом файле, либо вообще отсутствует на устройстве выг- рузки. Если страница находится на устройстве выгрузки, в дескрип- торе дискового блока содержится логический номер устройства и но- мер блока, по которым можно отыскать содержимое страницы. Если страница содержится в исполняемом файле, в дескрипторе дискового блока располагается номер логического блока в файле с содержимым страницы; ядро может быстро преобразовать этот номер в адрес на диске. В дескрипторе дискового блока также имеется информация о двух устанавливаемых функцией exec особых условиях: страница при обращении к ней заполняется ("demand fill") или обнуляется ("demand zero"). Разъяснения по этому поводу даются в разделе 9.2.1.2. В таблице pfdata описывается каждая страница физической памя- ти. Записи таблицы проиндексированы по номеру страницы и состоят из следующих полей: * Статус страницы, указывающий на то, что страница располагает- ся на устройстве выгрузки или в исполняемом файле, что к странице произведено обращение по прямому доступу в память (путем считывания информации с устройства выгрузки), или на то, что страница может быть переназначена. * Количество процессов, ссылающихся на страницу. Счетчик ссылок хранит число записей в таблице страниц, имеющих ссылку на те- кущую страницу. Это значение может отличаться от количества процессов, использующих разделяемую область с данной страни- цей, в чем мы убедимся чуть позже, когда будем снова обра- щаться к алгоритму функции fork. * Логический номер устройства (устройства выгрузки или файловой системы) и номер блока, указывающие расположение содержимого страницы. * Указатели на другие записи таблицы pfdata в соответствии со списком свободных страниц или с хеш-очередью страниц. По аналогии с буферным кешем ядро связывает записи таблицы pfdata в список свободных страниц и хеш-очередь. Список свободных страниц представляет собой буфер, который содержит страницы, дос- тупные для переназначения, однако процесс, обратившийся к этим страницам, может столкнуться с ошибкой адресации, так и не полу- чив соответствующую страницу из списка. Этот список дает ядру возможность сократить число операций чтения с устройства выгруз- ки. Ядро выделяет страницы из этого списка по вышеназванному принципу замещения "стариков". Ядро выстраивает записи таблицы в хеш-очередь в соответствии с номером устройства (выгрузки) и но- мером блока. Используя эти номера, ядро может быстро отыскать страницу, если она находится в памяти. Передавая физическую стра- ницу области, ядро выбирает соответствующую запись из списка сво- бодных страниц, исправляет указанные в ней номера устройства и блока и помещает ее в соответствующее место хеш-очереди. Каждая запись таблицы использования области подкачки соот- ветствует странице, находящейся на устройстве выгрузки. Запись содержит счетчик ссылок, показывающий количество записей таблицы страниц, в которых имеется ссылка на текущую страницу. На Рисунке 9.14 показана взаимосвязь между записями таблицы страниц, дескрипторами дисковых блоков, записями таблицы pfdata и таблицы использования области подкачки. Виртуальный адрес 1493К отображается на запись таблицы страниц, соответствующую странице с физическим номером 794; дескриптор дискового блока, связанный с этой записью, свидетельствует о том, что содержимое страницы рас- полагается на устройстве выгрузки с номером 1 в дисковом блоке с номером 2743. Запись таблицы pfdata, помимо того, что указывает на те же номера устройства и блока, сообщает, что счетчик ссылок на физическую страницу имеет значение, равное 1. О том, почему номер дискового блока дублируется в записи таблицы pfdata, вы уз- наете из раздела 9.2.4.1. Счетчик ссылок на виртуальную страницу (в записи таблицы использования области подкачки) свидетельствует о том, что на копию страницы на устройстве выгрузки ссылается только одна запись таблицы страниц. 9.2.1.1 Функция fork в системе с замещением страниц Как уже говорилось в разделе 7.1, во время выполнения функции fork ядро создает копию каждой области родительского процесса и присоединяет ее к процессу-потомку. В системе с замещением стра- здддддддддддддддддддддддбдддддддддддддддддддддддддд© Виртуальный ЁЗапись таблицы страницЁДескриптор дискового блокаЁ адрес цдддддддддддддддддддддддедддддддддддддддддддддддддд╢ 1493К Ё Номер страницы 794 Ё Устройство 1 Блок 2743 Ё юдддедедддддддддддддддддаддддддддддддддддддддеддддды Ё Ё Ё Ё Ё Запись таблицы pfdata, Ё Ё Ё соответствующая стра- здддддддддддды здддддддддддды v нице с номером 794 Ё Ё зддддддддддддбддддддддддддддддддддддд© Ё Ё Ё Ё Счетчик ссылок 1 Ё Ё Запись таблицы Ё Ё цддддддддддддддддддддддд╢ Ё использования Ё Ё Ё Номер устройства 1 Ё Ё области подкачки Ё Ё цддддддддддддддддддддддд╢ Ё зддддддддддддддддд© Ё Ё Ё Номер блока 2743 Ё Ё ЁСчетчик ссылок 1Ё Ё Ё юдддддддддддддддддеддддды Ё юддддддддедддддддды Ё Ё Ё зддддды Ё Ё Ё Ё Ё здддддддддддддды v v v v v здддддддддддддддддддддддддд© зддддддддддддддддддддддддддд© Ё Физическая страница 794 Ё Ё Номер блока 2743 Ё юдддддддддддддддддддддддддды юддддддддддддддддддддддддддды Рисунок 9.14. Взаимосвязь между структурами данных, участвую- щими в реализации механизма замещения страниц по обращению ниц ядро по традиции создает физическую копию адресного прост- ранства процесса-родителя, что в общем случае является довольно расточительной операцией, поскольку процесс часто после выполне- ния функции fork обращается к функции exec и незамедлительно ос- вобождает только что скопированное пространство. Если область разделяемая, в версии V вместо копирования страницы ядро просто увеличивает значение счетчика ссылок на область (в таблице облас- тей, в таблице страниц и в таблице pfdata). Тем не менее, для частных областей, таких как область данных и стека, ядро отводит в таблице областей и таблице страниц новую запись, после чего просматривает в таблице страниц все записи процесса-родителя: ес- ли запись верна, ядро увеличивает значение счетчика ссылок в таб- лице pfdata, отражающее количество процессов, использующих стра- ницу через разные области (в отличие от тех процессов, которые используют данную страницу через разделяемую область). Если стра- ница располагается на устройстве выгрузки, ядро увеличивает зна- чение счетчика ссылок в таблице использования области подкачки. Теперь на страницу могут ссылаться обе области, использующие эту страницу совместно, пока процесс не ведет на нее запись. Как только страница понадобится процессу для записи, ядро создаст ее копию, с тем, чтобы у каждой области была своя личная версия страницы. Для этого при выполнении функции fork в каждой записи таблицы страниц, соответствующей частным областям родителя и по- томка, ядро устанавливает бит "копирования при записи". Если один из процессов попытается что-то записать на страницу, он получит отказ системы защиты, после чего для него будет создана новая ко- пия содержимого страницы. Таким образом, физическое копирование страницы откладывается до того момента, когда в этом возникнет реальная потребность. В качестве примера рассмотрим Рисунок 9.15. Процессы разделя- ют доступ к таблице страниц совместно используемой области команд T, поэтому значение счетчика ссылок на область равно 2, а на страницы области единице (в таблице pfdata). Ядро назначает про- Процесс-родитель Процесс-потомок Частная таблица Частная таблица областей процесса областей процесса здддддддддддддд© здддддддддддддд© Ё Ы Ё Ё Ы Ё цдЫдддддддддддд╢ цдЫдддддддддддд╢ Ё Ы Ы Ё Ё Ы Ы Ё юдЫддддддддддЫды юдЫддддддддддЫды Ы з -----Ы---------------ы Ы Ы Ы Ы Ы v v v v зддддддддддддддддддд© зддддддддддддддддддд© зддддддддддддддддддд© Ё Область T Ё Ё Область P1 Ё Ё Область C1 Ё Ё Счетчик ссылок 2 Ё Ё Счетчик ссылок 1 Ё Ё Счетчик ссылок 1 Ё Ёзддддддддддддддддд©Ё Ёзддддддддддддддддд©Ё Ёзддддддддддддддддд©Ё ЁЁ Записи таблицы ЁЁ ЁЁ Записи таблицы ЁЁ ЁЁ Записи таблицы ЁЁ ЁЁ страниц ЁЁ ЁЁ страниц ЁЁ ЁЁ страниц ЁЁ Ёцддддддддддддддддд╢Ё Ёцддддддддддддддддд╢Ё Ёцддддддддддддддддд╢Ё ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ Ёцддддддддддддддддд╢Ё ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ ЁЁВиртуаль- Стра-ЁЁ ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ ЁЁный адрес ница ЁЁ ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ ЁЁ 24К 967 ЁЁ ЁЁ Ы ЁЁ ЁЁ Ы ЁЁ ЁцддддддддддддддЫдд╢Ё Ёцддддддддддддддддд╢Ё Ёцддддддддддддддддд╢Ё ЁЁ Ы Ы ЁЁ ЁЁВиртуаль- Стра-ЁЁ ЁЁВиртуаль- Стра-ЁЁ ЁЁ Ы Ы ЁЁ ЁЁный адрес ница ЁЁ ЁЁный адрес ница ЁЁ ЁЁ Ы Ы ЁЁ ЁЁ 97К 613 ЁЁ ЁЁ 97К 613 ЁЁ ЁЁ Ы Ы ЁЁ ЁцддддддддддддддЫдд╢Ё ЁцддддддддддддддЫдд╢Ё ЁЁ Ы Ы ЁЁ ЁЁ Ы Ы ЁЁ ЁЁ Ы Ы ЁЁ ЁЁ Ы Ы ЁЁ ЁЁ Ы Ы ЁЁ ЁЁ Ы Ы ЁЁ ЁЁ Ы Ы ЁЁ ЁЁ Ы Ы ЁЁ ЁЁ Ы Ы ЁЁ ЁЁ Ы Ы ЁЁ ЁЁ Ы Ы ЁЁ ЁЁ Ы Ы ЁЁ ЁюддддддддддддддЫддыЁ ЁюддддддддддддддЫддыЁ ЁюддддддддддддддЫддыЁ юдддддддддддддддЫддды юдддддддддддддддЫддды юдддддддддддддддЫддды Ы Ы Ы---------Ы v v v зддддддддддддддддддддд© зддддддддддддддддддддд© Ё Страничный блок 967 Ё Ё Страничный блок 613 Ё Ё Счетчик ссылок 1 Ё Ё Счетчик ссылок 2 Ё юддддддддддддддддддддды юддддддддддддддддддддды Рисунок 9.15. Адресация страниц, участвующих в процессе вы- полнения функции fork цессу-потомку новую область данных C1, являющуюся копией области P1 процесса-родителя. Обе области используют одни и те же записи таблицы страниц, это видно на примере страницы с виртуальным ад- ресом 97К. Этой странице в таблице pfdata соответствует запись с номером 613, счетчик ссылок в которой равен 2, ибо на страницу ссылаются две области. В ходе выполнения функции fork в системе BSD создается физи- ческая копия страниц родительского процесса. Однако, учитывая на- личие ситуаций, в которых создание физической копии не является обязательным, в системе BSD существует также функция vfork, кото- рая используется в том случае, если процесс сразу по завершении функции fork собирается запустить функцию exec. Функция vfork не копирует таблицы страниц, поэтому она работает быстрее, чем функ- ция fork в версии V системы UNIX. Однако процесс-потомок при этом исполняется в тех же самых физических адресах, что и его роди- тель, и может поэтому затереть данные и стек родительского про- цесса. Если программист использует функцию vfork неверно, может возникнуть опасная ситуация, поэтому вся ответственность за ее использование возлагается на программиста. Различие в подходах к рассматриваемому вопросу в системах UNIX и BSD имеет философский характер, они дают разный ответ на один и тот же вопрос: следует ли ядру скрывать особенности реализации своих функций, превращая их в тайну для пользователей, или же стоит дать опытным пользова- телям возможность повысить эффективность выполнения системных операций ? здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё int global; Ё Ё main() Ё Ё { Ё Ё int local; Ё Ё Ё Ё local = 1; Ё Ё if (vfork() == 0) Ё Ё { Ё Ё /* потомок */ Ё Ё global = 2; /* запись в область данных родителя */Ё Ё local = 3; /* запись в стек родителя */ Ё Ё _exit(); Ё Ё } Ё Ё printf("global %d local %d\n",global,local); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 9.16. Функция vfork и искажение информации процесса В качестве примера рассмотрим программу, приведенную на Ри- сунке 9.16. После выполнения функции vfork процесс-потомок не за- пускает функцию exec, а переустанавливает значения переменных global и local и завершается (****). Система гарантирует, что процесс-родитель приостанавливается до того момента, когда пото- мок исполнит функции exec или exit. Возобновив в конечном итоге свое выполнение, процесс-родитель обнаружит, что значения двух его переменных не совпадают с теми значениями, которые были у них до обращения к функции vfork ! Еще больший эффект может произвес- ти возвращение процесса-потомка из функции, вызвавшей функцию vfork (см. упражнение 9.8). ддддддддддддддддддддддддддддддддддддддд (****) Функция exit используется в варианте _exit, потому что она "очищает" структуры данных, передаваемые через стандартный ввод-вывод (на пользовательском уровне), для обоих процес- сов, так что оператор printf, используемый родителем, не даст правильный результат - еще один нежелательный побоч- ный эффект от применения функции vfork. 9.2.1.2 Функция exec в системе с замещением страниц Как уже говорилось в главе 7, когда процесс обращается к сис- темной функции exec, ядро считывает из файловой системы в память указанный исполняемый файл. Однако в системе с замещением страниц по запросу исполняемый файл, имеющий большой размер, может не уместиться в доступном пространстве основной памяти. Поэтому ядро не назначает ему сразу все пространство, а отводит место в памяти по мере надобности. Сначала ядро назначает файлу таблицы страниц и дескрипторы дисковых блоков, помечая страницы в записях таблиц как "заполняемые при обращении" (для всех данных, кроме имеющих тип bss) или "обнуляемые при обращении" (для данных типа bss). Считывая в память каждую страницу файла по алгоритму read, про- цесс получает ошибку из-за отсутствия (недоступности) данных. Подпрограмма обработки ошибок проверяет, является ли страница "заполняемой при обращении" (тогда ее содержимое будет немедленно затираться содержимым исполняемого файла и поэтому ее не надо очищать) или "обнуляемой при обращении" (тогда ее следует очис- тить). В разделе 9.2.3 мы увидим, как это происходит. Если про- цесс не может поместиться в памяти, "сборщик" страниц освобождает для него место, периодически откачивая из памяти неиспользуемые страницы. В этой схеме видны явные недостатки. Во-первых, при чтении каждой страницы исполняемого файла процесс сталкивается с ошибкой из-за обращения к отсутствующей странице, пусть даже процесс ни- когда и не обращался к ней. Во-вторых, если после того, как "сборщик" страниц откачал часть страниц из памяти, была запущена функция exec, каждая только что выгруженная и вновь понадобившая- ся страница потребует дополнительную операцию по ее загрузке. Чтобы повысить эффективность функции exec, ядро может востребо- вать страницу непосредственно из исполняемого файла, если данные в файле соответствующим образом настроены, что определяется зна- чением т.н. "магического числа". Однако, использование стандарт- ных алгоритмов доступа к файлу (например, bmap) потребовало бы при обращении к странице, состоящей из блоков косвенной адреса- ции, больших затрат, связанных с многократным использованием бу- ферного кеша для чтения каждого блока. Кроме того, функция bmap не является реентерабельной, отсюда возникает опасность нарушения целостности данных. Во время выполнения системной функции read ядро устанавливает в пространстве процесса значения различных па- раметров ввода-вывода. Если при попытке скопировать данные в пространство пользователя процесс столкнется с отсутствием нужной страницы, он, считывая страницу из файловой системы, может зате- реть содержащие эти параметры поля. Поэтому ядро не может прибе- гать к использованию обычных алгоритмов обработки ошибок данного рода. Конечно же алгоритмы должны быть в обычных случаях реенте- рабельными, поскольку у каждого процесса свое отдельное адресное пространство и процесс не может одновременно исполнять несколько системных функций. Для того, чтобы считывать страницы непосредственно из испол- няемого файла, ядро во время исполнения функции exec составляет список номеров дисковых блоков файла и присоединяет этот список к индексу файла. Работая с таблицами страниц такого файла, ядро на- ходит дескриптор дискового блока, содержащего страницу, и запоми- нает номер блока внутри файла; этот номер позже используется при загрузке страницы из файла. На Рисунке 9.17 показан пример, в ко- тором страница имеет адрес расположения в логическом блоке с но- мером 84 от начала файла. В области имеется указатель на индекс, в котором содержится номер соответствующего физического блока на диске (279). Список блоков, Область зд> связанный с индексом зддддддддддддддддддддддддддддддддд© Ё здддддддддддддддд© Ё Индексдддддддддддеды 0 Ё Ё Ё Ё Ы Ё Ё Ё Ё Ы Ё Ё Ё Дескриптор дискового блока Ё Ы Ё Ё Ё зддддддддддддддддддддддддддд© Ё Ы Ё Ё Ё Ё Логический блок 84 Ё Ё Ы цдддддддддддддддд╢ Ё юддддддддддддддддддддддддддды Ё 84 Ё 279 Ё Ё Ё цдддддддддддддддд╢ юддддддддддддддддддддддддддддддддды Ё Ё Ё Ё Ё Ё Ё Ё юдддддддддддддддды Рисунок 9.17. Отображение файла на область 9.2.2 "Сборщик" страниц "Сборщик" страниц (page stealer) является процессом, принад- лежащим ядру операционной системы и выполняющим выгрузку из памя- ти тех страниц, которые больше не входят в состав рабочего множества пользовательского процесса. Этот процесс создается яд- ром во время инициализации системы и запускается в любой момент, когда в нем возникает необходимость. Он просматривает все актив- ные незаблокированные области и увеличивает значение "возраста" каждой принадлежащей им страницы (заблокированные области пропус- каются, но впоследствии, по снятии блокировки, тоже будут учте- ны). Когда процесс при работе со страницей, принадлежащей облас- ти, получает ошибку, ядро блокирует область, чтобы "сборщик" не смог выгрузить страницу до тех пор, пока ошибка не будет обрабо- тана. Страница в памяти может находиться в двух состояниях: либо "дозревать", не будучи еще готовой к выгрузке, либо быть готовой к выгрузке и доступной для привязки к другим виртуальным страни- цам. Первое состояние означает, что процесс обратился к странице и поэтому страница включена в его рабочее множество. При обраще- нии к странице в некоторых машинах аппаратно устанавливается бит упоминания, если же эта операция не выполняется, соответственно, и программные методы скорее всего используются другие (раздел 9.2.4). Если страница находится в первом состоянии, "сборщик" сбрасывает бит упоминания в ноль, но запоминает количество прос- мотров множества страниц, выполненных с момента последнего обра- щения к странице со стороны пользовательского процесса. Таким об- разом, первое состояние распадается на несколько подсостояний в соответствии с тем, сколько раз "сборщик" страниц обратился к странице до того, как страница стала готовой для выгрузки (см. Рисунок 9.18). Когда это число превышает некоторое пороговое зна- чение, ядро переводит страницу во второе состояние - состояние готовности к выгрузке. Максимальная продолжительность пребывания страницы в первом состоянии зависит от условий конкретной реали- зации и ограничивается числом отведенных для этого поля разрядов в записи таблицы страниц. На Рисунке 9.19 показано взаимодействие между процессами, ра- ботающими со страницей, и "сборщиком" страниц. Цифры обозначают номер обращения "сборщика" к странице с того момента, как страни- Ссылка на страницу зддддддддддддбддддддддддбддддддддддбддддддддддддддддд© Ё ^ ^ ^ Ё v Ё Ё Ё Ё Готов- зддддддд© Ё Ё Ё Ё ность Ё Стра- Ё здад© здад© здад© здад© к Ё ница вЁддддд>Ё 1 цддддд>Ё 2 цддддд>Ё 3 цддддЗЗЗЗдддд>Ё n Ё вы- Ё памятиЁ юддды юддды юддды юдбды груз- юддддддды "Дозревание" страницы --- отсутствие Ё ке ^ ссылок Ё Ё Ё Ё здддддддд© Ё Ё Ё Страни-Ё Ё За- юдддддддддддддддддддддддЁ ца вы- Ё<дддддддддддддддддды Выгруз- грузка Ё груженаЁ ка юдддддддды Рисунок 9.18. Диаграмма состояний страницы ца была загружена в память. Процесс, обратившийся к странице пос- ле второго просмотра страниц "сборщиком", сбросил ее "возраст" в 0. После каждого просмотра пользовательский процесс обращался к странице вновь, но в конце концов "сборщик" страниц осуществил три просмотра страницы с момента последнего обращения к ней со стороны пользовательского процесса и выгрузил ее из памяти. Если область используется совместно не менее, чем двумя про- цессами, все они работают с битами упоминания в одном и том же наборе записей таблицы страниц. Таким образом, страницы могут включаться в рабочие множества нескольких процессов, но для "сборщика" страниц это не имеет никакого значения. Если страница включена в рабочее множество хотя бы одного из процессов, она ос- тается в памяти; в противном случае она может быть выгружена. Ни- чего, что одна область, к примеру, имеет в памяти страниц больше, чем имеют другие: "сборщик" страниц не пытается выгрузить равное количество страниц из всех активных областей. Ядро возобновляет работу "сборщика" страниц, когда доступная в системе свободная память имеет размер, не дотягивающий до ниж- ней допустимой отметки, и тогда "сборщик" производит откачку страниц до тех пор, пока объем свободной памяти не превысит верх- нюю отметку. При использовании двух отметок количество производи- мых операций сокращается, ибо если ядро использует только одно пороговое значение, оно будет выгружать достаточное число страниц для освобождения памяти свыше порогового значения, но в результа- те возвращения ошибочно выгруженных страниц в память размер сво- бодного пространства вскоре вновь опустится ниже этого порога. Объем свободной памяти при этом постоянно бы поддерживался около пороговой отметки. Выгрузка страниц с освобождением памяти в объ- еме, превышающем верхнюю отметку, откладывает момент, когда объем свободной памяти в системе станет меньше нижней отметки, поэтому "сборщику" страниц не приходится уже так часто выполнять свою ра- боту. Оптимальный выбор уровней верхней и нижней отметок адми- нистратором повышает производительность системы. Когда "сборщик" страниц принимает решение выгрузить страницу из памяти, он проверяет возможность нахождения копии этой страни- цы на устройстве выгрузки. При этом могут иметь место три случая: Состояние страницы Время (последнего упоминания) зддддддддддддддддддддддбддддддддддд© Ё В памяти Ё 0 Ё цддддддддддддддддддддддеддддддддддд╢ Ё Ё 1 Ё цддддддддддддддддддддддеддддддддддд╢ Ё Ё 2 Ё цддддддддддддддддддддддеддддддддддд╢ Ссылка на страницу Ё Ё 0 Ё цддддддддддддддддддддддеддддддддддд╢ Ё Ё 1 Ё цддддддддддддддддддддддеддддддддддд╢ Ссылка на страницу Ё Ё 0 Ё цддддддддддддддддддддддеддддддддддд╢ Ё Ё 1 Ё цддддддддддддддддддддддеддддддддддд╢ Ё Ё 2 Ё цддддддддддддддддддддддеддддддддддд╢ Ё Ё 3 Ё цддддддддддддддддддддддеддддддддддд╢ Страница выгружена Ё Вне памяти Ё Ё юддддддддддддддддддддддаддддддддддды Рисунок 9.19. Пример "созревания" страницы 1. Если на устройстве выгрузки есть копия страницы, ядро "плани- рует" выгрузку страницы: "сборщик" страниц помещает ее в спи- сок выгруженных страниц и переходит дальше; выгрузка считает- ся логически завершившейся. Когда число страниц в списке пре- высит ограничение (определяемое возможностями дискового конт- роллера), ядро переписывает страницы на устройство выгрузки. 2. Если на устройстве выгрузки уже есть копия страницы и ее содержимое ничем не отличается от содержимого страницы в па- мяти (бит модификации в записи таблицы страниц не установ- лен), ядро сбрасывает в ноль бит доступности (в той же запи- си таблицы), уменьшает значение счетчика ссылок в таблице pfdata и помещает запись в список свободных страниц для буду- щего переназначения. 3. Если на устройстве выгрузки есть копия страницы, но процесс изменил содержимое ее оригинала в памяти, ядро планирует выг- рузку страницы и освобождает занимаемое ее копией место на устройстве выгрузки. "Сборщик" страниц копирует страницу на устройство выгрузки, если имеют место случаи 1 и 3. Чтобы проиллюстрировать различия между последними двумя слу- чаями, предположим, что страница находится на устройстве выгрузки и загружается в основную память после того, как процесс столкнул- ся с отсутствием необходимых данных. Допустим, ядро не стало ав- томатически удалять копию страницы на диске. В конце концов, "сборщик" страниц вновь примет решение выгрузить страницу. Если с момента загрузки в память в страницу не производилась запись дан- ных, содержимое страницы в памяти идентично содержимому ее диско- вой копии и в переписи страницы на устройство выгрузки необходи- мости не возникает. Однако, если процесс успел что-то записать на страницу, старый и новый ее варианты будут различаться, поэтому ядру следует переписать страницу на устройство выгрузки, освобо- див предварительно место, занимаемое на устройстве старым вариан- том. Ядро не сразу использует освобожденное пространство на уст- ройстве выгрузки, поэтому оно имеет возможность поддерживать непрерывное размещение занятых участков, что повышает эффектив- ность использования области выгрузки. "Сборщик" страниц заполняет список выгруженных страниц, кото- рые в принципе могут принадлежать разным областям, и по заполне- нии списка откачивает их на устройство выгрузки. Нет необходимос- ти в том, чтобы все страницы одного процесса непременно выгружа- лись: к примеру, некоторые из страниц, возможно, недостаточно "созрели" для этого. В этом видится различие со стратегией выг- рузки процессов, согласно которой из памяти выгружаются все стра- ницы одного процесса, вместе с тем метод переписи данных на уст- ройство выгрузки идентичен тому методу, который описан для систе- мы с замещением процессов в разделе 9.1.2. Если на устройстве выгрузки недостаточно непрерывного пространства, ядро выгружает страницы по отдельности (по одной странице за операцию), что в конечном итоге обходится недешево. В системе с замещением страниц фрагментация на устройстве выгрузки выше, чем в системе с замеще- нием процессов, поскольку ядро выгружает блоки страниц, но загру- жает в память каждую страницу в отдельности. Когда ядро переписывает страницу на устройство выгрузки, оно сбрасывает бит доступности в соответствующей записи таблицы страниц и уменьшает значение счетчика ссылок в соответствующей записи таблицы pfdata. Если значение счетчика становится равным 0, запись таблицы pfdata помещается в конец списка свободных страниц и запоминается для последующего переназначения. Если зна- чение счетчика отлично от 0, это означает, что страница (в ре- зультате выполнения функции fork) используется совместно несколь- кими процессами, но ядро все равно выгружает ее. Наконец, ядро выделяет пространство на устройстве выгрузки, сохраняет его адрес в дескрипторе дискового блока и увеличивает значение счетчика ссылок на страницу в таблице использования области подкачки. Если в то время, пока страница находится в списке свободных страниц, процесс обнаружил ее отсутствие, получив соответствующую ошибку, ядро может восстановить ее в памяти, не обращаясь к устройству выгрузки. Однако, страница все равно будет считаться выгруженной, если она попала в список "сборщика" страниц. Предположим, к примеру, что "сборщик" страниц выгружает 30, 40, 50 и 20 страниц из процессов A, B, C и D, соответственно, и что за одну операцию выгрузки на дисковое устройство откачиваются 64 страницы. На Рисунке 9.20 показана последовательность имеющих при этом место операций выгрузки при условии, что "сборщик" стра- ниц осуществляет просмотр страниц процессов в очередности: A, B, C, D. "Сборщик" выделяет на устройстве выгрузки место для 64 страниц и выгружает 30 страниц процесса A и 34 страницы процесса B. Затем он выделяет место для следующих 64 страниц и выгружает оставшиеся 6 страниц процесса B, 50 страниц процесса C и 8 стра- ниц процесса D. Выделенные для размещения страниц за две операции участки области выгрузки могут быть и несмежными. "Сборщик" сох- раняет оставшиеся 12 страниц процесса D в списке выгружаемых страниц, но не выгружает их до тех пор, пока список не будет за- полнен до конца. Как только у процессов возникает потребность в подкачке страниц с устройства выгрузки или если страницы больше не нужны использующим их процессам (процессы завершились), в об- ласти выгрузки освобождается место. Чтобы подвести итог, выделим в процессе откачки страницы из памяти две фазы. На первой фазе "сборщик" страниц ищет страницы, подходящие для выгрузки, и помещает их номера в список выгружае- мых страниц. На второй фазе ядро копирует страницу на устройство выгрузки (если на нем имеется место), сбрасывает в ноль бит до- пустимости в соответствующей записи таблицы страниц, уменьшает значение счетчика ссылок в соответствующей записи таблицы pfdata Страницы выгружаются группами по 64 страницы здддддддддддддддддддд© зддддддддддддддддддд© зддддддддддддддддддд© ЁПроцесс A 30 стр-ц Ё ЁПроцесс B 6 стр-ц Ё ЁПроцесс D 12 стр-ц Ё Ё Ё Ё Ё Ё Ы Ё ЁПроцесс B 34 стр-цыЁ ЁПроцесс C 50 стр-цЁ Ё Ы Ё юдддддддддддддддддддды Ё Ё Ё Ы Ё Ы Ы ЁПроцесс D 8 стр-ц Ё юддддддддддддддддддды Ы Ыюддддддддддддддддддды Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы здддддддбдддддддддддддддддддбдддддддбдддддддддддддддддбдддддддддд© Ё Ё A 30 B 34 Ё Ё B 6 C 50 D 8 Ё Ё юдддддддадддддддддддддддддддадддддддадддддддддддддддддадддддддддды Устройство выгрузки Рисунок 9.20. Выделение пространства на устройстве выгрузки в системе с замещением страниц и если оно становится равным 0, помещает эту запись в конец спис- ка свободных страниц. Содержимое физической страницы в памяти не изменяется до тех пор, пока страница не будет переназначена дру- гому процессу. 9.2.3 Отказы при обращениях к страницам В системе встречаются два типа отказов при обращении к стра- нице: отказы из-за отсутствия (недоступности) данных и отказы системы защиты. Поскольку программы обработки прерываний по отка- зу могут приостанавливать свое выполнение на время считывания страницы с диска в память, эти программы являются исключением из общего правила, утверждающего невозможность приостанова обработ- чиков прерываний. Тем не менее, поскольку программа обработки прерываний по отказу приостанавливается в контексте процесса, по- родившего фатальную ошибку памяти, отказ относится к текущему процессу; следовательно, процессы приостанавливаются не произ- вольным образом. 9.2.3.1 Обработка прерываний по отказу из-за недоступности данных Если процесс пытается обратиться к странице, бит доступности для которой не установлен, он получает отказ из-за отсутствия (недоступности) данных и ядро запускает программу обработки пре- рываний по отказу данного типа (Рисунок 9.21). Бит доступности не устанавливается ни для тех страниц, которые располагаются за пре- делами виртуального адресного пространства процесса, ни для тех, которые входят в состав этого пространства, но не имеют в настоя- щий момент физического аналога в памяти. Фатальная ошибка памяти произошла в результате обращения ядра по виртуальному адресу страницы, поэтому ядро выходит на соответствующую этой странице запись в таблице страниц и дескриптор дискового блока. Чтобы пре- дотвратить взаимную блокировку, которая может произойти, если "сборщик" попытается выгрузить страницу из памяти, ядро фиксирует в памяти область с соответствующей записью таблицы страниц. Если в дескрипторе дискового блока отсутствует информация о странице, здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм vfault /* обработка отказа из-за отсутствия Ё Ё (недоступности) данных */ Ё Ё входная информация: адрес, по которому получен отказ Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё найти область, запись в таблице страниц, дескриптор дис-Ё Ё кового блока, связанные с адресом, по которому получен Ё Ё отказ, заблокировать область; Ё Ё если (адрес не принадлежит виртуальному адресному прост-Ё Ё ранству процесса) Ё Ё { Ё Ё послать сигнал (SIGSEGV: нарушение сегментации) про- Ё Ё цессу; Ё Ё перейти на out; Ё Ё } Ё Ё если (адрес указан неверно) /* возможно, процесс нахо-Ё Ё дился в состоянии при- Ё Ё останова */ Ё Ё перейти на out; Ё Ё если (страница имеется в кеше) Ё Ё { Ё Ё убрать страницу из кеша; Ё Ё поправить запись в таблице страниц; Ё Ё выполнять пока (содержимое страницы не станет доступ-Ё Ё ным) /* другой процесс получил такой же отказ, Ё Ё * но раньше */ Ё Ё приостановиться; Ё Ё } Ё Ё в противном случае /* страница отсутствует в кеше */Ё Ё { Ё Ё назначить области новую страницу; Ё Ё Ё Ё поместить новую страницу в кеш, откорректировать за- Ё Ё пись в таблице pfdata; Ё Ё если (страница ранее не загружалась в память и имеет Ё Ё пометку "обнуляемая при обращении") Ё Ё очистить содержимое страницы; Ё Ё в противном случае Ё Ё { Ё Ё считать виртуальную страницу с устройства выгруз-Ё Ё ки или из исполняемого файла; Ё Ё приостановиться (до завершения ввода-вывода); Ё Ё } Ё Ё возобновить процессы (ожидающие загрузки содержимого Ё Ё страницы); Ё Ё } Ё Ё установить бит доступности страницы; Ё Ё сбросить бит модификации и "возраст" страницы; Ё Ё пересчитать приоритет процесса; Ё Ё out: снять блокировку с области; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 9.21. Алгоритм обработки отказа из-за отсутствия (не- доступности) данных сделанная ссылка на страницу является недопустимой и ядро посыла- ет процессу-нарушителю сигнал о "нарушении сегментации" (см. Ри- сунок 7.25). Такой порядок действий совпадает с тем порядком, ко- торого придерживается ядро, когда процесс обратился по неверному адресу, если не принимать во внимание то обстоятельство, что ядро узнает об ошибке немедленно, так как все "доступные" страницы яв- ляются резидентными в памяти. Если ссылка на страницу сделана правильно, ядро выделяет физическую страницу в памяти и считывает в нее содержимое виртуальной страницы с устройства выгрузки или из исполняемого файла. Страница, вызвавшая отказ, находится в одном из пяти состоя- ний: 1. На устройстве выгрузки вне памяти. 2. В списке свободных страниц в памяти. 3. В исполняемом файле. 4. С пометкой "обнуляемая при обращении". 5. С пометкой "заполняемая при обращении". Рассмотрим каждый случай в подробностях. Если страница находится на устройстве выгрузки, вне памяти (случай 1), это означает, что она когда-то располагалась в памя- ти, но была выгружена оттуда "сборщиком" страниц. Обратившись к дескриптору дискового блока, ядро узнает из него номера устройс- тва выгрузки и блока, где расположена страница, и проверяет, не осталась ли страница в кеше. Ядро корректирует запись таблицы страниц так, чтобы она указывала на страницу, которую предполага- ется считать в память, включает соответствующую запись таблицы pfdata в хеш-очередь (облегчая последующую обработку отказа) и считывает страницу с устройства выгрузки. Допустивший ошибку про- цесс приостанавливается до момента завершения ввода-вывода; вмес- те с ним будут возобновлены все процессы, ожидавшие загрузки со- держимого страницы. Обратимся к Рисунку 9.22 и в качестве примера рассмотрим за- пись таблицы страниц, связанную с виртуальным адресом 66К. Если при обращении к странице процесс получает отказ из-за недоступ- ности данных, программа обработки отказа обращается к дескриптору дискового блока и обнаруживает то, что страница находится на уст- ройстве выгрузки в блоке с номером 847 (если предположить, что в системе только одно устройство выгрузки): следовательно, вирту- альный адрес указан верно. Затем программа обработки отказа обра- щается к кешу, но не находит информации о дисковом блоке с номером 847. Таким образом, копия виртуальной страницы в памяти отсутствует и программа обработки отказа должна загрузить ее с устройства выгрузки. Ядро отводит физическую страницу с номером 1776 (Рисунок 9.23), считывает в нее с устройства выгрузки содер- жимое виртуальной страницы и перенастраивает запись таблицы стра- ниц на страницу с номером 1776. В завершение ядро корректирует дескриптор дискового блока, делая указание о том, что страница загружена, а также запись таблицы pfdata, отмечая, что на уст- ройстве выгрузки в блоке с номером 847 содержится дубликат вирту- альной страницы. При обработке отказов из-за недоступности данных ядро не всегда прибегает к выполнению операции ввода-вывода, даже когда из дескриптора дискового блока видно, что страница загружена (в случае 2). Может случиться так, что ядро после выгрузки содержи- мого физической страницы так и не переприсвоило ее или же ка- кой-то иной процесс в результате отказа загрузил содержимое вир- туальной страницы в другую физическую страницу. В любом случае программа обработки отказа обнаруживает страницу в кеше, в ка- честве ключа используя номер блока в дескрипторе дискового блока. Она перенастраивает соответствующую запись в таблице страниц на только что найденную страницу, увеличивает значение счетчика ссы- лок на страницу и в случае необходимости убирает страницу из списка свободных страниц. Предположим, к примеру, что процесс по- Записи Дескрипторы таблицы страниц дисковых блоков Страничные блоки Физи- ческая Диско- Виртуаль- стра- Состо- Состо- Стра- вый Счет- ный адрес ница яние яние Блок ница блок чик здддддбддддддддбббдддддддбддддддддд© зддддбддддддбдддд© 0 Ё Ё ЁЁЁ Ё Ё Ё Ё Ё Ё цдддддеддддддддееедддддддеддддддддд╢ Ё Ё Ё Ё 1К Ё 1648Ё Недо- ЁЁЁВ файлеЁ 3 Ё Ё Ё Ё Ё Ё Ё ступна ЁЁЁ Ё Ё Ё Ё Ё Ё цдддддеддддддддееедддддддеддддддддд╢ Ё Ё Ё Ё 2К Ё Ё ЁЁЁ Ё Ё Ё Ё Ё Ё цдддддеддддддддееедддддддеддддддддд╢ Ё Ё Ё Ё 3К Ё Нет Ё Недо- ЁЁЁЗаполняЁ 5 Ё Ё Ё Ё Ё Ё Ё ступна ЁЁЁется Ё Ё Ё Ё Ё Ё Ё Ё ЁЁЁпри об-Ё Ё Ё Ё Ё Ё Ё Ё ЁЁЁращенииЁ Ё Ё Ё Ё Ё цдддддеддддддддееедддддддеддддддддд╢ цддддеддддддедддд╢ 4К Ё Ё ЁЁЁ Ё Ё Ё1036Ё 387 Ё 0 Ё цдддддеддддддддееедддддддеддддддддд╢ цддддеддддддедддд╢ Ы Ё Ё ЁЁЁ Ё Ё Ё Ы Ё Ё Ё Ы Ё Ё ЁЁЁ Ё Ё Ё Ы Ё Ё Ё Ы Ё Ё ЁЁЁ Ё Ё Ё Ы Ё Ё Ё Ы Ё Ё ЁЁЁ Ё Ё цддддеддддддедддд╢ Ы Ё Ё ЁЁЁ Ё Ё Ё1648Ё 1618 Ё 1 Ё цдддддеддддддддееедддддддеддддддддд╢ цддддеддддддедддд╢ 64К Ё 1917Ё Недо- ЁЁЁНа дис-Ё 1206 Ё Ё Ы Ё Ё Ё Ё Ё ступна ЁЁЁке Ё Ё Ё Ы Ё Ё Ё цдддддеддддддддееедддддддеддддддддд╢ Ё Ы Ё Ё Ё 65К Ё Нет Ё Недо- ЁЁЁОбнуля-Ё Ё Ё Ы Ё Ё Ё Ё Ё ступна ЁЁЁется Ё Ё Ё Ы Ё Ё Ё Ё Ё ЁЁЁпри об-Ё Ё Ё Ы Ё Ё Ё Ё Ё ЁЁЁращенииЁ Ё Ё Ы Ё Ё Ё цдддддеддддддддееедддддддеддддддддд╢ цддддеддддддедддд╢ 66К Ё 1036Ё Недо- ЁЁЁНа дис-Ё 847 Ё Ё1861Ё 1206 Ё 0 Ё Ё Ё ступна ЁЁЁке Ё Ё цддддеддддддедддд╢ цдддддеддддддддееедддддддеддддддддд╢ Ё Ё Ё Ё 67К Ё Ё ЁЁЁ Ё Ё Ё Ё Ё Ё юдддддаддддддддааадддддддаддддддддды юддддаддддддадддды Рисунок 9.22. Иллюстрация к отказу из-за недоступности данных лучил отказ при обращении к виртуальному адресу 64К (Рисунок 9.22). Просматривая кеш, ядро устанавливает, что страничный блок с номером 1861 связан с дисковым блоком 1206. Ядро перенастраива- ет запись таблицы страниц с виртуальным адресом 64К на страницу с номером 1861, устанавливает бит доступности и передает управление программе обработки отказа. Таким образом, номер дискового блока связывает вместе записи таблицы страниц и таблицы pfdata, чем и объясняется его запоминание в обеих таблицах. Как и ядру, программе обработки отказа не нужно считывать страницу в память, если какой-то иной процесс уже получил отказ по той же самой странице, но еще не полностью загрузил ее. Прог- рамма находит область с записью таблицы страниц, которую она уже ранее заблокировала. Она дожидается, пока будет закончен цикл об- работки предыдущего отказа, после чего обнаруживает, что страница стала доступной, и завершает свою работу. Эта процедура прослежи- вается на Рисунке 9.24. Записи Дескрипторы таблицы страниц дисковых блоков Страничные блоки Физи- ческая Диско- Виртуаль- стра- Состо- Состо- Стра- вый Счет- ный адрес ница яние яние Блок ница блок чик здддддбддддддддбббдддддддбддддддддд© зддддбддддддбдддд© 66К Ё 1776Ё Доступ-ЁЁЁНа дис-Ё 847 Ё Ё1776Ё 847 Ё 1 Ё Ё Ё на ЁЁЁке Ё Ё Ё Ё Ё Ё юдддддаддддддддааадддддддаддддддддды юддддаддддддадддды Рисунок 9.23. Результат загрузки страницы в память Процесс A Процесс B здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё Отказ при обращении к стра- Ы Ы Ё нице Ы Ы Ё Виртуальный адрес страницы Ы Ы Ё верен Ы Ы Ё Приостанов до завершения Ы Ы Ё считывания страницы Ы Ы Ё Ы Ы Отказ при обращении к стра- Ё Ы Ы нице Ё Ы Ы Виртуальный адрес страницы Ё Ы Ы верен Ё Ы Ы Загрузка страницы в память Ё Ы Ы Приостанов до окончания Ё Ы Ы загрузки Ё Ы Ы Ы Ё Выход из приостанова -- Ы Ы Ё страница в памяти Ы Ы Ё Страница помечается как Ы Ы Ё доступная Ы Ы Ё Выход из приостанова других Ы Ы Ё процессов Ы Ы Ё Ы Выход из приостанова Ё Возобновление выполнения Ы Ы Ё Ы Ы Ы Ё Ы Ы Возобновление выполнения Ё Ы Ы Ы Ё Ы Ы Ы Ё Время Ы v Рисунок 9.24. Два отказа на одной странице Если копия страницы находится не на устройстве выгрузки, а в исполняемом файле (случай 3), ядро загружает страницу из файла. Программа обработки отказа обращается к дескриптору дискового блока, ищет соответствующий номер логического блока внутри файла, содержащего страницу, и индекс, ассоциированный с записью таблицы областей. Номер логического блока используется программой в ка- честве смещения внутри списка номеров дисковых блоков, присоеди- ненного к индексу во время выполнения функции exec. По номеру блока на диске программа считывает страницу в память. Так, напри- мер, дескриптор дискового блока, связанный с виртуальным адресом 1К, показывает, что содержимое страницы располагается в исполняе- мом файле, внутри логического блока с номером 3 (см. Рисунок 9.22). Если процесс получил отказ при обращении к странице, имеющей пометку "заполняемая при обращении" или "обнуляемая при обраще- нии" (случаи 4 и 5), ядро выделяет свободную страницу в памяти и корректирует соответствующую запись таблицы страниц. Если страни- ца "обнуляемая при обращении", ядро также очищает ее содержимое. В завершение обработки флаги "заполняемая при обращении" и "обну- ляемая при обращении" сбрасываются. Теперь страница находится в памяти, доступна процессам и ее содержимое не имеет аналогов ни на устройстве выгрузки, ни в файловой системе. Так происходит, если процесс обращается к страницам с виртуальными адресами 3К и 65К (см. Рисунок 9.22): ни один из процессов не обращался к этим страницам с тех пор, как файл был запущен на выполнение функцией exec. В завершение своей работы программа обработки отказов из-за отсутствия (недоступности) данных устанавливает бит доступности страницы и сбрасывает бит модификации. Приоритет процесса при этом пересчитывается, ибо во время выполнения программы процесс мог приостановить свое выполнение на уровне ядра, получая тем са- мым по возвращении в режим задачи незаслуженное преимущество пе- ред другими процессами. И, наконец, возвращаясь в режим задачи, программа проверяет, не было ли за время обработки отказа поступ- ления каких-либо сигналов. 9.2.3.2 Обработка прерываний по отказу системы защиты Вторым типом отказа, встречающегося при обращении к странице, является отказ системы защиты, который означает, что процесс об- ратился к существующей странице памяти, но судя по разрядам, опи- сывающим права доступа к странице, доступ к ней со стороны теку- щего процесса не разрешен. (Вспомним пример, описывающий попытку процесса произвести запись данных в область команд; см. Рисунок 7.22). Отказ данного типа имеет место также тогда, когда процесс предпринимает попытку записать что-то на страницу, для которой во время выполнения системной функции fork был установлен бит копи- рования при записи. Ядро должно различать между собой ситуации, когда отказ произошел по причине того, что страница требует копи- рования при записи, и когда имело место действительно что-то не- допустимое. Программа обработки отказа системы защиты автоматически полу- чает виртуальный адрес, по которому произошел отказ, и ведет поиск соответствующей области и записи таблицы страниц (Рисунок 9.25). Она блокирует область, чтобы "сборщик" страниц не мог выг- рузить страницу, пока связанный с ней отказ не будет обработан. Если программа обработки отказа устанавливает, что причиной отка- за послужила установка бита копирования при записи, и если стра- ницу используют сразу несколько процессов, ядро выделяет в памяти новую страницу и копирует в нее содержимое старой страницы; ссыл- ки других процессов на старую страницу сохраняют свое значение. После копирования и внесения в запись таблицы страниц нового но- мера страницы ядро уменьшает значение счетчика ссылок в записи таблицы pfdata, соответствующей старой странице. Вся процедура показана на Рисунке 9.26, где три процесса совместно используют физическую страницу с номером 828. Процесс B считывает страницу, но поскольку бит копирования при записи установлен, получает от- каз системы защиты. Программа обработки отказа выделяет страницу с номером 786, копирует в нее содержимое страницы 828, уменьшает значение счетчика ссылок на скопированную страницу и перенастраи- вает соответствующую запись таблицы страниц на страницу с номером 786. Если бит копирования при записи установлен, но страница ис- пользуется только одним процессом, ядро дает процессу возможность воспользоваться физической страницей повторно. Оно отключает бит копирования при записи и разрывает связь страницы с ее копией на диске (если таковая существует), поскольку не исключена возмож- ность того, что дисковой копией пользуются другие процессы. Затем ядро убирает запись таблицы pfdata из очереди страниц, ибо новая здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм pfault /* обработка отказа системы защиты */ Ё Ё входная информация: адрес, по которому получен отказ Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё найти область, запись в таблице страниц, дескриптор дис-Ё Ё кового блока, связанные с адресом, по которому получен Ё Ё отказ, заблокировать область; Ё Ё если (страница недоступна в памяти) Ё Ё перейти на out; Ё Ё если (бит копирования при записи не установлен) Ё Ё перейти на out; /* программная ошибка - сигнал */Ё Ё если (счетчик ссылок на страничный блок > 1) Ё Ё { Ё Ё выделить новую физическую страницу; Ё Ё скопировать в нее содержимое старой страницы; Ё Ё уменьшить значение счетчика ссылок на старый стра- Ё Ё ничный блок; Ё Ё перенастроить запись таблицы страниц на новую физи- Ё Ё ческую страницу; Ё Ё } Ё Ё в противном случае /* убрать страницу, поскольку она Ё Ё * никем больше не используется */ Ё Ё { Ё Ё если (копия страницы имеется на устройстве выгрузки)Ё Ё освободить место на устройстве, разорвать связьЁ Ё со страницей; Ё Ё если (страница находится в хеш-очереди страниц) Ё Ё убрать страницу из хеш-очереди; Ё Ё } Ё Ё в записи таблицы страниц установить бит модификации, Ё Ё сбросить бит копирования при записи; Ё Ё пересчитать приоритет процесса; Ё Ё проверить, не поступали ли сигналы; Ё Ё out: снять блокировку с области; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 9.25. Алгоритм обработки отказа системы защиты копия виртуальной страницы располагается не на устройстве выгруз- ки. Кроме того, ядро уменьшает значение счетчика ссылок на стра- ницу в таблице использования области подкачки, и если это значе- ние становится равным 0, освобождает место на устройстве (см. упражнение 9.11). Если запись в таблице страниц указывает на то, что страница недоступна, и ее бит копирования при записи установлен, выступая поводом для отказа системы защиты, допустим, что система при об- ращении к странице сначала обрабатывает отказ из-за недоступности данных (обратная очередность рассматривается в упражнении 9.17). Несмотря на это, программа обработки отказа системы защиты все равно обязана убедиться в доступности страницы, поскольку при ус- тановке блокировки на область программа может приостановиться, а "сборщик" страниц тем временем может выгрузить страницу из памя- ти. Если страница недоступна (бит доступности сброшен), программа немедленно завершит работу и процесс получит отказ из-за недос- тупности данных. Ядро обработает этот отказ, но процесс вновь по- лучит отказ системы защиты. Более чем вероятно, что заключитель- ный отказ системы защиты будет обработан без каких-либо препятс- твий и помех, поскольку пройдет довольно значительный период времени, прежде чем страница достаточно "созреет" для выгрузки из памяти. Описанная последовательность событий показана на Рисунке 9.27. Запись таблицы страниц - Процесс A зддддддддддддддддддддддддддддддддддддддддддддддд© Ё Страница 828: доступна, копируется при записи цд© юддддддддддддддддддддддддддддддддддддддддддддддды Ё Ё Запись таблицы страниц - Процесс B Ё зддддддддддд© зддддддддддддддддддддддддддддддддддддддддддддддд© юд>Ё СтраничныйЁ Ё Страница 828: доступна, копируется при записи цддд>Ё блок 828 Ё юддддддддддддддддддддддддддддддддддддддддддддддды зд>Ё Счетчик Ё Ё Ё ссылок 3 Ё Запись таблицы страниц - Процесс C Ё юддддддддддды зддддддддддддддддддддддддддддддддддддддддддддддд© Ё Ё Страница 828: доступна, копируется при записи цды юддддддддддддддддддддддддддддддддддддддддддддддды (а) Перед тем, как процесс B получил отказ системы защиты Запись таблицы страниц - Процесс A зддддддддддддддддддддддддддддддддддддддддддддддд© зддддддддддд© Ё Страница 828: доступна, копируется при записи цд© Ё СтраничныйЁ юддддддддддддддддддддддддддддддддддддддддддддддды Ё Ё блок 828 Ё юд>Ё Счетчик Ё Запись таблицы страниц - Процесс B зд>Ё ссылок 2 Ё зддддддддддддддддддддддддддддддддддддддддддддддд© Ё юддддддддддды Ё Страница 828: доступна ц©Ё юдддддддддддддддддддддддддддддддддддддддддддддддыЁЁ зддддддддддд© юЁд>Ё СтраничныйЁ Запись таблицы страниц - Процесс C Ё Ё блок 786 Ё зддддддддддддддддддддддддддддддддддддддддддддддд© Ё Ё Счетчик Ё Ё Страница 828: доступна, копируется при записи цды Ё ссылок 1 Ё юддддддддддддддддддддддддддддддддддддддддддддддды юддддддддддды (б) После запуска программы обработки отказа системы защиты для процесса B Рисунок 9.26. Отказ системы защиты из-за установки бита копи- рования при записи Перед завершением программа обработки отказа системы защиты устанавливает биты модификации и защиты, но сбрасывает бит копи- рования при записи. Она пересчитывает приоритет процесса и прове- ряет, не поступали ли за время ее работы сигналы, предназначенные процессу, в точности повторяя то, что делается по завершении об- работки отказа из-за недопустимости данных. Процесс, получающий отказы при обращении к странице "Сборщик" страниц здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё Ы Ы Ё Ы Ы Ё Ы Блокирует область Ё Ы Ы Ё Ы Ы Ё Отказ системы защиты Ы Ё Приостанов - область Ы Ё заблокирована Ы Ё Ы Ы Ё Ы Выгрузка страницы - бит Ё Ы допустимости сброшен Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё Ы Выводит из приостанова Ё Ы процессы, ожидающие Ё Ы снятия с области Ё Ы блокировки Ё Выход из приостанова Ы Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё Проверка бита доступ- Ы Ё ности - сброшен Ы Ё Выход из программы обра- Ы Ё ботки отказа системы за- Ы Ё щиты Ы Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё Отказ из-за недоступ- Ы Ё ности данных Ы v Время Рисунок 9.27. Взаимодействие отказа системы защиты и отказа из-за недоступности данных 9.2.4 Замещение страниц на менее сложной технической базе Наибольшая действенность алгоритмов замещения страниц по зап- росу (обращению) достигается в том случае, если биты упоминания и модификации устанавливаются аппаратным путем и тем же путем вызы- вается отказ системы защиты при попытке записи в страницу, имею- щую признак "копирования при записи". Тем не менее, указанные ал- горитмы вполне применимы даже тогда, когда аппаратура распознает только бит доступности и код защиты. Если бит доступности, уста- навливаемый аппаратно, дублируется программно-устанавливаемым би- том, показывающим, действительно ли страница доступна или нет, ядро могло бы отключить аппаратно-устанавливаемый бит и проимити- ровать установку остальных битов программным путем. Так, напри- мер, в машине VAX-11 бит упоминания отсутствует (см. [Levy 82]). Ядро может отключить аппаратно-устанавливаемый бит доступности для страницы и дальше работать по следующему плану. Если процесс ссылается на страницу, он получает отказ, поскольку бит доступ- ности сброшен, и в игру вступает программа обработки отказа, исс- ледующая страницу. Поскольку "программный" бит доступности уста- новлен, ядро знает, что страница действительно доступна и находится в памяти; оно устанавливает "программный" бит упомина- ния и "аппаратный" бит доступности, но ему еще предстоит узнать о том, что на страницу была сделана ссылка. Последующие ссылки на страницу уже не встретят отказ, ибо "аппаратный" бит доступности установлен. Когда с ней будет работать "сборщик" страниц, он вновь сбросит "аппаратный" бит доступности, вызывая тем самым от- Аппарат- Программ- Программ- Аппарат- Программ- Программ- ный бит ный бит ный бит ный бит ный бит ный бит доступ- доступ- упомина- доступ- доступ- упомина- ности ности ния ности ности ния здддддддддбддддддддддбддддддддд© здддддддддбддддддддддбддддддддд© Ё Нет Ё Да Ё Нет Ё Ё Да Ё Да Ё Да Ё юдддддддддаддддддддддаддддддддды юдддддддддаддддддддддаддддддддды (а) До модифицирования (б) После модифицирования страницы страницы Рисунок 9.28. Имитация установки "аппаратного" бита модифика- ции программными средствами казы на все последующие обращения к странице и возвращая систему к началу цикла. Этот случай показан на Рисунке 9.28. 9.3 СИСТЕМА СМЕШАННОГО ТИПА СО СВОПИНГОМ И ПОДКАЧКОЙ ПО ЗАПРОСУ Несмотря на то, что в системах с замещением страниц по запро- су обращение с памятью отличается большей гибкостью по сравнению с системами подкачки процессов, возможно возникновение ситуаций, в которых "сборщик" страниц и программа обработки отказов из-за недоступности данных начинают мешать друг другу из-за нехватки памяти. Если сумма рабочих множеств всех процессов превышает объ- ем физической памяти в машине, программа обработки отказов обычно приостанавливается, поскольку выделять процессам страницы памяти дальше становится невозможным. "Сборщик" страниц не сможет доста- точно быстро освободить место в памяти, ибо все страницы принад- лежат рабочему множеству. Производительность системы падает, пос- кольку ядро тратит слишком много времени на верхнем уровне, с безумной скоростью перестраивая память. Ядро в версии V манипулирует алгоритмами подкачки процессов и замещения страниц так, что проблемы соперничества перестают быть неизбежными. Когда ядро не может выделить процессу страницы памя- ти, оно возобновляет работу процесса подкачки и переводит пользо- вательский процесс в состояние, эквивалентное состоянию "готов- ности к запуску, будучи зарезервированным". В этом состоянии одновременно могут находиться несколько процессов. Процесс под- качки выгружает один за другим целые процессы, пока объем доступ- ной памяти в системе не превысит верхнюю отметку. На каждый выг- руженный процесс приходится один процесс, загруженный в память из состояния "готовности к выполнению, будучи зарезервированным". Ядро загружает эти процессы не с помощью обычного алгоритма под- качки, а путем обработки отказов при обращении к соответствующим страницам. На последующих итерациях процесса подкачки при условии наличия в системе достаточного объема свободной памяти будут об- работаны отказы, полученные другими пользовательскими процессами. Применение такого метода ведет к снижению частоты возникновения системных отказов и устранению соперничества: по идеологии он близок к методам, используемым в операционной системе VAX/VMS ([Levy 82]). 9.4 ВЫВОДЫ Прочитанная глава была посвящена рассмотрению алгоритмов под- качки процессов и замещения страниц, используемых в версии V сис- темы UNIX. Алгоритм подкачки процессов реализует перемещение про- цессов целиком между основной памятью и устройством выгрузки. Ядро выгружает процессы из памяти, если их размер поглощает всю свободную память в системе (в результате выполнения функций fork, exec и sbrk или в результате естественного увеличения стека), или в том случае, если требуется освободить память для загрузки про- цесса. Загрузку процессов выполняет специальный процесс подкачки (процесс 0), который запускается всякий раз, как на устройстве выгрузки появляются процессы, готовые к выполнению. Процесс под- качки не прекращает своей работы до тех пор, пока на устройстве выгрузки не останется ни одного такого процесса или пока в основ- ной памяти не останется свободного места. В последнем случае про- цесс подкачки пытается выгрузить что-нибудь из основной памяти, но в его обязанности входит также слежение за соблюдением требо- вания минимальной продолжительности пребывания выгружаемых про- цессов в памяти (в целях предотвращения холостой перекачки); по этой причине процесс подкачки не всегда достигает успеха в своей работе. Возобновление процесса подкачки в случае возникновения необходимости в нем производит с интервалом в одну секунду прог- рамма обработки прерываний по таймеру. В системе с замещением страниц по запросу процессы могут ис- полняться, даже если их виртуальное адресное пространство загру- жено в память не полностью; поэтому виртуальный размер процесса может превышать объем доступной физической памяти в системе. Ког- да ядро испытывает потребность в свободных страницах, "сборщик" страниц просматривает все активные страницы в каждой области, по- мечая для выгрузки те из них, которые достаточно "созрели" для этого, и в конечном итоге откачивает их на устройство выгрузки. Когда процесс обращается к виртуальной странице, которая в насто- ящий момент выгружена из памяти, он получает отказ из-за недос- тупности данных. Ядро запускает программу обработки отказа, кото- рая назначает области новую физическую страницу памяти и копирует в нее содержимое виртуальной страницы. Повысить производительность системы при использовании алго- ритма замещения страниц по запросу можно несколькими способами. Во-первых, если процесс вызывает функцию fork, ядро использует бит копирования при записи, тем самым в большинстве случаев сни- мая необходимость в физическом копировании страниц. Во-вторых, ядро может запросить содержимое страницы исполняемого файла прямо из файловой системы, устраняя потребность в вызове функции exec для незамедлительного считывания файла в память. Это способствует повышению производительности, поскольку не исключена возможность того, что подобные страницы так никогда и не потребуются процес- су, и устраняет излишнюю холостую перекачку, имеющую место в том случае, если "сборщик" страниц выгружает эти страницы из памяти до того, как в них возникает потребность. 9.5 УПРАЖНЕНИЯ 1. Набросайте схему реализации алгоритма mfree, который осво- бождает пространство памяти и возвращает его таблице свобод- ного пространства. 2. В разделе 9.1.2 утверждается, что система блокирует переме- щаемый процесс, чтобы другие процессы не могли его трогать с места до момента окончания операции. Что произошло бы, если бы система не делала этого ? 3. Предположим, что в адресном пространстве процесса располага- ются таблицы используемых процессом сегментов и страниц. Ка- ким образом ядро может выгрузить это пространство из памяти? 4. Если стек ядра находится внутри адресного пространства про- цесса, почему процесс не может выгружать себя сам ? Какой на Ваш взгляд должна быть системная программа выгрузки процес- сов, как она должна запускаться ? *5. Предположим, что ядро пытается выгрузить процесс, чтобы ос- вободить место в памяти для других процессов, загружаемых с устройства выгрузки. Если ни на одном из устройств выгрузки для данного процесса нет места, процесс подкачки приостанав- ливает свою работу до тех пор, пока место не появится. Воз- можна ли ситуация, при которой все процессы, находящиеся в памяти, приостановлены, а все готовые к выполнению процессы находятся на устройстве выгрузки ? Что нужно предпринять яд- ру для того, чтобы исправить это положение ? 6. Рассмотрите еще раз пример, приведенный на Рисунке 9.10, при условии, что в памяти есть место только для 1 процесса. 7. Обратимся к примеру, приведенному на Рисунке 9.11. Составьте подобный пример, в котором процессу постоянно требуется для работы центральный процессор. Существует ли какой-нибудь способ снятия подобной напряженности ? здддддддддддддддддддддддддддддддддд© Ё main() Ё Ё { Ё Ё f(); Ё Ё g(); Ё Ё } Ё Ё Ё Ё f() Ё Ё { Ё Ё vfork(); Ё Ё } Ё Ё Ё Ё g() Ё Ё { Ё Ё int blast[100],i; Ё Ё for (i = 0; i < 100; i++) Ё Ё blast[i] = i; Ё Ё } Ё юдддддддддддддддддддддддддддддддддды Рисунок 9.29 8. Что произойдет в результате выполнения программы, приведен- ной на Рисунке 9.29, в системе BSD 4.2 ? Каким будет стек процесса-родителя ? 9. Почему после выполнения функции fork процесса-потомка пред- почтительнее запускать впереди процесса-родителя, если на разделяемых страницах биты копирования при записи установле- ны ? Каким образом ядро может заставить потомка запуститься первым ? *10. Алгоритм обработки отказа из-за недоступности данных, изло- женный в тексте, загружает страницы поодиночке. Его эффек- тивность можно повысить, если подготовить к загрузке помимо страницы, вызвавшей отказ, и все соседние с ней страницы. Переработайте исходный алгоритм с учетом указанной операции. 11. В алгоритмах работы "сборщика" страниц и программы обработки отказов из-за недоступности данных предполагается, что раз- мер страницы равен размеру дискового блока. Что нужно изме- нить в этих алгоритмах для того, чтобы они работали и в тех случаях, когда указанное равенство не соблюдается ? *12. Когда процесс вызывает функцию fork (ветвится), значение счетчика ссылок на каждую разделяемую страницу (в таблице pfdata) увеличивается. Предположим, что "сборщик" страниц выгружает разделяемую страницу на устройство выгрузки, и один из процессов (скажем, родитель) впоследствии получает отказ при обращении к ней. Содержимое виртуальной страницы теперь располагается на физической странице. Объясните, по- чему процесс-потомок всегда имеет возможность получить вер- ную копию страницы, даже после того, как процесс-родитель что-то запишет на нее. Почему, когда процесс-родитель ведет запись на страницу, он должен немедленно порвать связь с ее дисковой копией ? 13. Что следует предпринять программе обработки отказов в том случае, если в системе исчерпаны страницы памяти ? *14. Составьте алгоритм выгрузки редко используемых компонент яд- ра. Какие из компонент нельзя выгружать и как их в таком случае следует обозначить ? 15. Придумайте алгоритм, отслеживающий выделение пространства на устройстве выгрузки, используя вместо карт памяти, описанных в настоящей главе, битовый массив. Сравните эффективность обоих методов. 16. Предположим, что в машине нет аппаратно-устанавливаемого би- та доступности, но есть код защиты, устанавливающий права доступа на чтение, запись и "исполнение" содержимого страни- цы. Смоделируйте работу с помощью программно-устанавливаемо- го бита доступности. 17. В машине VAX-11 перед проверкой наличия отказов из-за недос- тупности данных выполняется аппаратная проверка наличия отказов системы защиты. Как это отражается на алгоритмах об- работки отказов ? 18. Системная функция plock дает суперпользователю возможность устанавливать и снимать блокировку (в памяти) на областях команд и данных вызывающего процесса. Процесс подкачки и "сборщик" страниц не могут выгружать заблокированные страни- цы из памяти. Процессам, использующим эту системную функцию, не приходится дожидаться загрузки страниц, поэтому им гаран- тирован более быстрый ответ по сравнению с другими процесса- ми. Следует ли иметь также возможность блокировки в памяти и области стека ? Что произойдет в том случае, если суммарный объем заблокированных областей превысит размер доступной па- мяти в машине ? 19. Что делает программа, приведенная на Рисунке 9.30 ? Подумай- те над альтернативной стратегией замещения страниц, в соот- ветствии с которой в рабочее множество каждого процесса включается максимально-возможное число страниц. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё struct fourmeg Ё Ё { Ё Ё int page[512]; /* пусть int занимает 4 байта */ Ё Ё } fourmeg[2048]; Ё Ё Ё Ё main() Ё Ё { Ё Ё for (;;) Ё Ё { Ё Ё switch(fork()) Ё Ё { Ё Ё case -1: /* процесс-родитель не может выполнить Ё Ё * fork --- слишком много потомков */ Ё Ё case 0: /* потомок */ Ё Ё func(); Ё Ё default: Ё Ё continue; Ё Ё } Ё Ё } Ё Ё } Ё Ё Ё Ё func() Ё Ё { Ё Ё int i; Ё Ё Ё Ё for (;;) Ё Ё { Ё Ё printf("процесс %d повторяет цикл\n",getpid()); Ё Ё for (i = 0; i < 2048; i++) Ё Ё fourmeg[i].page[0] = i; Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 9.30 ПОДСИСТЕМА УПРАВЛЕНИЯ ВВОДОМ-ВЫВОДОМ Подсистема управления вводом-выводом позволяет процессам под- держивать связь с периферийными устройствами, такими как накопи- тели на магнитных дисках и лентах, терминалы, принтеры и сети, с одной стороны, и с модулями ядра, которые управляют устройствами и именуются драйверами устройств, с другой. Между драйверами уст- ройств и типами устройств обычно существует однозначное соответс- твие: в системе может быть один дисковый драйвер для управления всеми дисководами, один терминальный драйвер для управления всеми терминалами и один ленточный драйвер для управления всеми ленточ- ными накопителями. Если в системе имеются однотипные устройства, полученные от разных изготовителей - например, две марки ленточ- ных накопителей, - в этом случае можно трактовать однотипные уст- ройства как устройства двух различных типов и иметь для них два отдельных драйвера, поскольку таким устройствам для выполнения одних и тех же операций могут потребоваться разные последователь- ности команд. Один драйвер управляет множеством физических уст- ройств данного типа. Например, один терминальный драйвер может управлять всеми терминалами, подключенными к системе. Драйвер различает устройства, которыми управляет: выходные данные, пред- назначенные для одного терминала, не должны быть посланы на дру- гой. Система поддерживает "программные устройства", с каждым из которых не связано ни одно конкретное физическое устройство. Нап- ример, как устройство трактуется физическая память, чтобы позво- лить процессу обращаться к ней извне, пусть даже память не явля- ется периферийным устройством. Команда ps обращается к информационным структурам ядра в физической памяти, чтобы сооб- щить статистику процессов. Еще один пример: драйверы могут вести трассировку записей в удобном для отладки виде, а драйвер трасси- ровки дает возможность пользователям читать эти записи. Наконец, профиль ядра, рассмотренный в главе 8, выполнен как драйвер: про- цесс записывает адреса программ ядра, обнаруженных в таблице идентификаторов ядра, и читает результаты профилирования. В этой главе рассматривается взаимодействие между процессами и подсистемой управления вводом-выводом, а также между машиной и драйверами устройств. Исследуется общая структура и функциониро- вание драйверов и в качестве примеров общего взаимодействия расс- матриваются дисковые и терминальные драйверы. Завершает главу описание нового метода реализации драйверов потоковых устройств. 10.1 ВЗАИМОДЕЙСТВИЕ ДРАЙВЕРОВ С ПРОГРАММНОЙ И АППАРАТНОЙ СРЕДОЙ В системе UNIX имеется два типа устройств - устройства ввода- вывода блоками и устройства неструктурированного или посимвольно- го ввода-вывода. Как уже говорилось в главе 2, устройства вво- да-вывода блоками, такие как диски и ленты, для остальной части системы выглядят как запоминающие устройства с произвольной вы- боркой; к устройствам посимвольного ввода-вывода относятся все другие устройства, в том числе терминалы и сетевое оборудование. Устройства ввода-вывода блоками могут иметь интерфейс и с уст- ройствами посимвольного ввода-вывода. Пользователь взаимодействует с устройствами через посредни- чество файловой системы (см. Рисунок 2.1). Каждое устройство име- ет имя, похожее на имя файла, и пользователь обращается к нему как к файлу. Специальный файл устройства имеет индекс и занимает место в иерархии каталогов файловой системы. Файл устройства от- личается от других файлов типом файла, хранящимся в его индексе, либо "блочный", либо "символьный специальный", в зависимости от устройства, которое этот файл представляет. Если устройство имеет как блочный, так и символьный интерфейс, его представляют два файла: специальный файл устройства ввода-вывода блоками и специ- альный файл устройства посимвольного ввода-вывода. Системные функции для обычных файлов, такие как open, close, read и write, имеют то же значение и для устройств, в чем мы убедимся позже. Системная функция ioctl предоставляет процессам возможность уп- равлять устройствами посимвольного ввода-вывода, но не применима в отношении к файлам обычного типа (*). Тем не менее, драйверам устройств нет необходимости поддерживать полный набор системных функций. Например, вышеупомянутый драйвер трассировки дает про- цессам возможность читать записи, созданные другими драйверами, но не позволяет создавать их. 10.1.1 Конфигурация системы Задание конфигурации системы это процедура указания админист- раторами значений параметров, с помощью которых производится настройка системы. Некоторые из параметров указывают размеры таб- лиц ядра, таких как таблица процессов, таблица индексов и таблица файлов, а также сколько буферов помещается в буферном пуле. С по- мощью других параметров указывается конфигурация устройств, то есть производятся конкретные указания ядру, какие устройства включаются в данную системную реализацию и их "адрес". Например, в конфигурации может быть указано, что терминальная плата встав- дддддддддддддддддддддддддддддддддддддддд (*) И наоборот, системная функция fcntl обеспечивает контроль над действиями, производимыми на уровне дескриптора файла, но не на уровне устройства. В других реализациях функция ioctl при- менима для файлов всех типов. лена в соответствующий разъем на аппаратной панели. Существует три стадии, на которых может быть указана конфигу- рация устройств. Во-первых, администраторы могут кодировать ин- формацию о конфигурации в файлах, которые транслируются и компо- нуются во время построения ядра. Информация о конфигурации обычно указывается в простом формате, и программа конфигурации преобра- зует ее в файл, готовый для трансляции. Во-вторых, администраторы могут указывать информацию о конфигурации после того, как система уже запущена; ядро динамически корректирует внутренние таблицы конфигурации. Наконец, самоидентифицирующиеся устройства дают яд- ру возможность узнать, какие из устройств включены. Ядро считыва- ет аппаратные ключи для самонастройки. Подробности задания сис- темной конфигурации выходят за пределы этой книги, однако во всех случаях результатом процедуры задания конфигурации является гене- рация или заполнение таблиц, составляющих основу программ ядра. Интерфейс "ядро - драйвер" описывается в таблице ключей устройств ввода-вывода блоками и в таблице ключей устройств по- символьного ввода-вывода (Рисунок 10.1). Каждый тип устройства имеет в таблице точки входа, которые при выполнении системных функций адресуют ядро к соответствующему драйверу. Функции open и close, вызываемые файлом устройства, "пропускаются" через таблицы ключей устройств в соответствии с типом файла. Функции mount и umount так же вызывают выполнение процедур открытия и закрытия устройств, но для устройств ввода-вывода блоками. Функции read и write, вызываемые устройствами ввода-вывода блоками и файлами в смонтированных файловых системах, запускают алгоритмы работы с буферным кешем, инициирующие реализацию стратегической процедуры работы с устройствами. Некоторые из драйверов запускают эту про- цедуру изнутри из процедур чтения и записи. Более подробно взаи- модействие с каждым драйвером рассматривается в следующем разде- ле. Интерфейс "аппаратура - драйвер" состоит из машинно-зависимых управляющих регистров или команд ввода-вывода для управления уст- ройствами и векторами прерываний: когда происходит прерывание от устройства, система идентифицирует устройство, вызвавшее прерыва- ние, и запускает программу обработки соответствующего прерывания. Очевидно, что "программные устройства", такие как драйвер системы построения профиля ядра (глава 8) не имеют аппаратного интерфей- са, однако программы обработки других прерываний могут обращаться к "обработчику программного прерывания" непосредственно. Напри- мер, программа обработки прерывания по таймеру обращается к прог- рамме обработки прерывания системы построения профиля ядра. Администраторы устанавливают специальные файлы устройств ко- мандой mknod, в которой указывается тип файла (блочный или сим- вольный), старший и младший номера устройства. Команда mknod за- пускает выполнение системной функции с тем же именем, создающей файл устройства. Например, в командной строке mknod /dev/tty13 c 2 13 "/dev/tty13" - имя файла устройства, "c" указывает, что тип файла - "символьный специальный" ("b", соответственно, блочный), "2" - старший номер устройства, "13" - младший номер устройства. Стар- ший номер устройства показывает его тип, которому соответствует точка входа в таблице ключей устройств, младший номер устройства - это порядковый номер единицы устройства данного типа. Если про- цесс открывает специальный блочный файл с именем "/dev/dsk1" и кодом 0, ядро запускает программу gdopen в точке 0 таблицы ключей устройств блочного ввода-вывода (Рисунок 10.2); если процесс чи- тает специальный символьный файл с именем "/dev/mem" и кодом 3, Подсистема управления файлами здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё open close Ё Ё open close read write ioctl read write Ё Ё mount umount Ё юддедддддеддддедддддедддддеддддддддддддедддддедддддедддддеддды Ё Ё Ё Ё Ё Ё Ё здадддддаддд© Ё Ё Ё Ё Ё Ё Ё Ё функции Ё Ё Ё Ё Ё Ё Ё Ё Ё работы с Ё Ё Ё Ё Ё Ё Ё Ё Ё буферным Ё Ё Ё Ё Ё Ё Ё Ё Ё кешем Ё Ё Ё Ё Ё Ё Ё Ё юдддддбддддды Ё Ё Ё Ё Ё Ё Ё Ё зддадддддаддддадддддадддддад© задддддадддддддддаддддд© Ё Таблица ключей устройств Ё Ё Таблица ключей уст- Ё Ё посимвольного ввода-выводаЁ Ё ройств ввода-вывода Ё Ё Ё Ё блоками Ё юддбдддддбддддбдддддбдддддбды юддбддддддбддддддддбддды Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё зддедддддеддддедддддедддддед© зддеддддддеддддддддеддд© Ёopen close read write ioctlЁ Точки Ё open close strategyЁ Ё Ё входа Ё Ё Ё Драйвер Ё для Ё Драйвер Ё Ё Ё драй- Ё Ё Ёпрограмма обработки преры- Ё веров Ё программа обработки Ё Ё ваний от устройства Ё Ёпрерываний от устройстЁ юддддддддддддедддддддддддддды юдддддддддддедддддддддды Ё Ё зддддддддадддддддддд© здддддддддаддддддддд© Ё Вектор прерывания Ё Ё Вектор прерывания Ё юддддддддбдддддддддды юдддддддддбддддддддды Ё Ё юдддддддддддддддддддбдддддддддддддддды Ё Прерывания от устройств Рисунок 10.1. Точки входа для драйверов ядро запускает программу mmread в точке 3 таблицы ключей уст- ройств посимвольного ввода-вывода. Программа nulldev - это "пус- тая" программа, используемая в тех случаях, когда отсутствует не- обходимость в конкретной функции драйвера. С одним старшим номером устройства может быть связано множество периферийных уст- ройств; младший номер устройства позволяет отличить их одно от другого. Не нужно создавать специальные файлы устройств при каж- дой загрузке системы; их только нужно корректировать, если изме- нилась конфигурация системы, например, если к установленной кон- фигурации были добавлены устройства. 10.1.2 Системные функции и взаимодействие с драйверами В этом разделе рассматривается взаимодействие ядра с драйве- рами устройств. При выполнении тех системных функций, которые ис- пользуют дескрипторы файлов, ядро, следуя за указателями, храня- щимися в пользовательском дескрипторе файла, обращается к таблице зддддддддддддддддддддддддддддддддддддддддддддддд© Ё таблица ключей устройств ввода-вывода блоками Ё цдддддддбддддддддбдддддддддбдддддддддддддддддддд╢ Ё вход Ё open Ё close Ё strategy Ё цдддддддеддддддддедддддддддедддддддддддддддддддд╢ Ё 0 Ё gdopen Ё gdclose Ё gdstrategy Ё цдддддддеддддддддедддддддддедддддддддддддддддддд╢ Ё 1 Ё gtopen Ё gtclose Ё gtstrategy Ё юдддддддаддддддддадддддддддадддддддддддддддддддды здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё таблица ключей устройств посимвольного ввода-вывода Ё цддддддбдддддддддддбдддддддддддбдддддддддбдддддддддддбддддддддддд╢ Ё вход Ё open Ё close Ё read Ё write Ё ioctl Ё цддддддедддддддддддедддддддддддедддддддддедддддддддддеддддддддддд╢ Ё 0 Ё conopen Ё conclose Ё conread Ё conwrite Ё conioctl Ё цддддддедддддддддддедддддддддддедддддддддедддддддддддеддддддддддд╢ Ё 1 Ё dzbopen Ё dzbclose Ё dzbread Ё dzbwrite Ё dzbioctl Ё цддддддедддддддддддедддддддддддедддддддддедддддддддддеддддддддддд╢ Ё 2 Ё syopen Ё nulldev Ё syread Ё sywrite Ё syioctl Ё цддддддедддддддддддедддддддддддедддддддддедддддддддддеддддддддддд╢ Ё 3 Ё nulldev Ё nulldev Ё mmread Ё mmwrite Ё nodev Ё цддддддедддддддддддедддддддддддедддддддддедддддддддддеддддддддддд╢ Ё 4 Ё gdopen Ё gdclose Ё gdread Ё gdwrite Ё nodev Ё цддддддедддддддддддедддддддддддедддддддддедддддддддддеддддддддддд╢ Ё 5 Ё gtopen Ё gtclose Ё gtread Ё gtwrite Ё nodev Ё юддддддадддддддддддадддддддддддадддддддддадддддддддддаддддддддддды Рисунок 10.2. Пример заполнения таблиц ключей устройств ввода- вывода блоками и символами файлов ядра и к индексу, где оно проверяет тип файла, и переходит к таблице ключей устройств ввода-вывода блоками или символами. Ядро извлекает из индекса старший и младший номера устройства, использует старший номер в качестве указателя на точку входа в соответствующей таблице и вызывает выполнение функции драйвера в соответствии с выполняемой системной функцией, передавая младший номер в качестве параметра. Важным различием в реализации систем- ных функций для файлов устройств и для файлов обычного типа явля- ется то, что индекс специального файла не блокируется в то время, когда ядро выполняет программу драйвера. Драйверы часто приоста- навливают свою работу, ожидая связи с аппаратными средствами или поступления данных, поэтому ядро не в состоянии определить, на какое время процесс будет приостановлен. Если индекс заблокиро- ван, другие процессы, обратившиеся к индексу (например, посредс- твом системной функции stat), приостановятся на неопределенное время, поскольку один процесс приостановил драйвер. Драйвер устройства интерпретирует параметры вызова системной функции в отношении устройства. Драйвер поддерживает структуры данных, описывающие состояние каждой контролируемой единицы дан- ного типа устройства; функции драйвера и программы обработки пре- рываний реализуются в соответствии с состоянием драйвера и с тем, какое действие выполняется в этот момент (например, данные вво- дятся или выводятся). Теперь рассмотрим каждый интерфейс более подробно. 10.1.2.1 Open При открытии устройства ядро следует той же процедуре, что и при открытии файлов обычного типа (см. раздел 5.1), выделяя в па- мяти индексы, увеличивая значение счетчика ссылок и присваивая значение точки входа в таблицу файлов и пользовательского деск- риптора файла. Наконец, ядро возвращает значение пользовательско- го дескриптора файла вызывающему процессу, так что открытие уст- ройства выглядит так же, как и открытие файла обычного типа. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм open /* для драйверов устройств */ Ё Ё входная информация: имя пути поиска Ё Ё режим открытия Ё Ё выходная информация: дескриптор файла Ё Ё { Ё Ё преобразовать имя пути поиска в индекс, увеличить значе-Ё Ё ние счетчика ссылок в индексе; Ё Ё выделить в таблице файлов место для пользовательского Ё Ё дескриптора файла, как при открытии обычного файла; Ё Ё Ё Ё выбрать из индекса старший и младший номера устройства; Ё Ё Ё Ё сохранить контекст (алгоритм setjmp) в случае передачи Ё Ё управления от драйвера; Ё Ё Ё Ё если (устройство блочного типа) Ё Ё { Ё Ё использовать старший номер устройства в качестве ука-Ё Ё зателя в таблице ключей устройств ввода-вывода бло- Ё Ё ками; Ё Ё вызвать процедуру открытия драйвера по данному индек-Ё Ё су: передать младший номер устройства, режимы откры-Ё Ё тия; Ё Ё } Ё Ё в противном случае Ё Ё { Ё Ё использовать старший номер устройства в качестве ука-Ё Ё зателя в таблице ключей устройств посимвольного вво-Ё Ё да-вывода; Ё Ё вызвать процедуру открытия драйвера по данному индек-Ё Ё су: передать младший номер устройства, режимы откры-Ё Ё тия; Ё Ё } Ё Ё Ё Ё если (открытие в драйвере не выполнилось) Ё Ё привести таблицу файлов к первоначальному виду, Ё Ё уменьшить значение счетчика в индексе; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 10.3. Алгоритм открытия устройства зависящую от устройства процедуру open (Рисунок 10.3). Для уст- ройства ввода-вывода блоками запускается процедура open, закоди- рованная в таблице ключей устройств ввода-вывода блоками, для ус- тройств посимвольного ввода-вывода - процедура open, закодированная в соответствующей таблице. Если устройство имеет как блочный, так и символьный тип, ядро запускает процедуру open, соответствующую типу файла устройства, открытого пользователем: обе процедуры могут даже быть идентичны, в зависимости от конк- ретного драйвера. Зависящая от типа устройства процедура open устанавливает связь между вызывающим процессом и открываемым устройством и ини- циализирует информационные структуры драйвера. Например, проце- дура open для терминала может приостановить процесс до тех пор, пока в машину не поступит сигнал (аппаратный) о том, что пользо- ватель предпринял попытку зарегистрироваться. После этого инициа- лизируются информационные структуры драйвера в соответствии с принятыми установками терминала (например, скоростью передачи ин- формации в бодах). Для "программных устройств", таких как память системы, процедура open может не включать в себя инициализацию. Если во время открытия устройства процессу пришлось приоста- новиться по какой-либо из внешних причин, может так случиться, что событие, которое должно было бы вызвать возобновление выпол- нения процесса, так никогда и не произойдет. Например, если на данном терминале еще не зарегистрировался ни один из пользовате- лей, процесс getty, "открывший" терминал (раздел 7.9), приоста- навливается до тех пор, пока пользователем не будет предпринята попытка регистрации, при этом может пройти достаточно большой промежуток времени. Ядро должно иметь возможность возобновить вы- полнение процесса и отменить вызов функции open по получении сиг- нала: ему следует сбросить индекс, отменить точку входа в таблице файлов и пользовательский дескриптор файла, которые были выделены перед входом в драйвер, поскольку открытие не произошло. Ядро сохраняет контекст процесса, используя алгоритм setjmp (раздел 6.4.4), прежде чем запустить процедуру open; если процесс возоб- новляется по сигналу, ядро восстанавливает контекст процесса в том состоянии, которое он имел перед обращением к драйверу, ис- пользуя алгоритм longjmp (раздел 6.4.4), и возвращает системе все выделенные процедуре open структуры данных. Точно так же и драй- вер может уловить сигнал и очистить доступные ему структуры дан- ных, если это необходимо. Ядро также переустанавливает структуры данных файловой системы, когда драйвер сталкивается с исключи- тельными ситуациями, такими, как попытка пользователя обратиться к устройству, отсутствующему в данной конфигурации. В подобных случаях функция open не выполняется. Процессы могут указывать значения различных параметров, ха- рактеризующие особенности выполнения процедуры открытия. Из них наиболее часто используется "no delay" (без задержки), означаю- щее, что процесс не будет приостановлен во время выполнения про- цедуры open, если устройство не готово. Системная функция open возвращает управление немедленно и пользовательский процесс не узнает, произошло ли аппаратное соединение или нет. Открытие уст- ройства с параметром "no delay", кроме всего прочего, затронет семантику вызова функции read, что мы увидим далее (раздел 10.3.4). Если устройство открывается многократно, ядро обрабатывает пользовательские дескрипторы файлов, индекс и записи в таблице файлов так, как это описано в главе 5, запуская определяемую ти- пом устройства процедуру open при каждом вызове системной функции open. Таким образом, драйвер устройства может подсчитать, сколько раз устройство было "открыто", и прервать выполнение функции open, если количество открытий приняло недопустимое значение. Например, имеет смысл разрешить процессам многократно "откры- вать" терминал на запись для того, чтобы пользователи могли обме- ниваться сообщениями. Но при этом не следует допускать многократ- ного "открытия" печатающего устройства для одновременной записи, так как процессы могут затереть друг другу информацию. Эти разли- чия имеют смысл скорее на практике, нежели на стадии разработки: разрешение одновременной записи на терминалы способствует уста- новлению взаимодействия между пользователями; запрещение одновре- менной записи на принтеры служит повышению читабельности машиног- рамм (**). 10.1.2.2 Close Процесс разрывает связь с открытым устройством, закрывая его. Однако, ядро запускает определяемую типом устройства процедуру close только в последнем вызове функции close для этого устройс- дддддддддддддддддддддддддддддддддддддддд (**) На практике вывод на печать обычно управляется специальными процессами буферизации, и права доступа устанавливаются та- ким образом, чтобы только система буферизации могла обра- щаться к принтеру. тва, и то только если не осталось процессов, которым устройство необходимо открытым, поскольку процедура закрытия устройства за- вершается разрывом аппаратного соединения; отсюда ясно, что ядру следует подождать, пока не останется ни одного процесса, обращаю- щегося к устройству. Поскольку ядро запускает процедуру открытия устройства при каждом вызове системной функции open, а процедуру закрытия только один раз, драйверу устройства неведомо, сколько процессов используют устройство в данный момент. Драйверы могут легко выйти из строя, если при их написании не соблюдалась осто- рожность: когда при выполнении процедуры close они приостанавли- вают свою работу и какой-нибудь процесс открывает устройство до того, как завершится процедура закрытия, устройство может стать недоступным для работы, если в результате комбинации вызовов open и close сложилась нераспознаваемая ситуация. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм close /* для устройств */ Ё Ё входная информация: дескриптор файла Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё выполнить алгоритм стандартного закрытия (глава 5ххх); Ё Ё если (значение счетчика ссылок в таблице файлов не 0) Ё Ё перейти на finish; Ё Ё если (существует еще один открытый файл, старший и млад-Ё Ё ший номера которого совпадают с номерами закрываемого Ё Ё устройства) Ё Ё перейти на finish; /* не последнее закрытие */ Ё Ё если (устройство символьного типа) Ё Ё { Ё Ё использовать старший номер в качестве указателя в Ё Ё таблице ключей устройства посимвольного ввода-выво- Ё Ё да; Ё Ё вызвать процедуру закрытия, определяемую типом драй- Ё Ё вера и передать ей в качестве параметра младший но- Ё Ё мер устройства; Ё Ё } Ё Ё если (устройство блочного типа) Ё Ё { Ё Ё если (устройство монтировано) Ё Ё перейти на finish; Ё Ё переписать блоки устройства из буферного кеша на уст-Ё Ё ройство; Ё Ё использовать старший номер в качестве указателя в Ё Ё таблице ключей устройства ввода-вывода блоками; Ё Ё вызвать процедуру закрытия, определяемую типом драй- Ё Ё вера и передать ей в качестве параметра младший но- Ё Ё мер устройства; Ё Ё сделать недействительными блоки устройства, оставшие-Ё Ё ся в буферном кеше; Ё Ё } Ё Ё finish: Ё Ё освободить индекс; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 10.4. Алгоритм закрытия устройства Алгоритм закрытия устройства похож на алгоритм закрытия файла обычного типа (Рисунок 10.4). Однако, до того, как ядро освобож- дает индекс, в нем выполняются действия, специфичные для файлов устройств. 1. Просматривается таблица файлов для того, чтобы убедиться в том, что ни одному из процессов не требуется, чтобы устройс- тво было открыто. Чтобы установить, что вызов функции close для устройства является последним, недостаточно положиться на значение счетчика ссылок в таблице файлов, поскольку несколь- ко процессов могут обращаться к одному и тому же устройству, используя различные точки входа в таблице файлов. Так же не- достаточно положиться на значение счетчика в таблице индек- сов, поскольку одному и тому же устройству могут соответство- вать несколько файлов устройства. Например, команда ls -l покажет, что одному и тому же устройству символьного типа ("c" в начале строки) соответствуют два файла устройства, старший и младший номера у которых (9 и 1) совпадают. Значе- ние счетчика связей для каждого файла, равное 1, говорит о том, что имеется два индекса. crw--w--w- 1 root vis 9, 1 Aug 6 1984 /dev/tty01 crw--w--w- 1 root unix 9, 1 May 3 15:02 /dev/tty01 Если процессы открывают оба файла независимо один от другого, они обратятся к разным индексам одного и того же устройства. 2. Если устройство символьного типа, ядро запускает процедуру закрытия устройства и возвращает управление в режим задачи. Если устройство блочного типа, ядро просматривает таблицу ре- зультатов монтирования и проверяет, не располагается ли на устройстве смонтированная файловая система. Если такая систе- ма есть, ядро не сможет запустить процедуру закрытия устройс- тва, поскольку не был сделан последний вызов функции close для устройства. Даже если на устройстве нет смонтированной файловой системы, в буферном кеше еще могут находиться блоки с данными, оставшиеся от смонтированной ранее файловой систе- мы и не переписанные на устройство, поскольку имели пометку "отложенная запись". Поэтому ядро просматривает буферный кеш в поисках таких блоков и переписывает их на устройство перед запуском процедуры закрытия устройства. После закрытия уст- ройства ядро вновь просматривает буферный кеш и делает не- действительными все буферы, которые содержат блоки для только что закрытого устройства, в то же время позволяя буферам с актуальной информацией остаться в кеше. 3. Ядро освобождает индекс файла устройства. Короче говоря, процедура закрытия устройства разрывает связь с устройством и инициализирует заново информационные структуры драйвера и аппаратную часть устройства с тем, чтобы ядро могло бы позднее открыть устройство вновь. 10.1.2.3 Read и Write Алгоритмы чтения и записи ядром на устройстве похожи на ана- логичные алгоритмы для файлов обычного типа. Если процесс произ- водит чтение или запись на устройстве посимвольного ввода-вывода, ядро запускает процедуры read или write, определяемые типом драй- вера. Несмотря на часто встречающиеся ситуации, когда ядро осу- ществляет передачу данных непосредственно между адресным прост- ранством задачи и устройством, драйверы устройств могут буферизовать информацию внутри себя. Например, терминальные драй- веры для буферизации данных используют символьные списки (раздел 10.3.1). В таких случаях драйвер устройства выделяет "буфер", ко- пирует данные из пространства задачи при выполнении процедуры write и выводит их из "буфера" на устройство. Процедура записи, управляемая драйвером, регулирует объем выводимой информации (т.н. управление потоком данных): если процессы генерируют инфор- мацию быстрее, чем устройство выводит ее, процедура записи приос- танавливает выполнение процессов до тех пор, пока устройство не будет готово принять следующую порцию данных. При чтении драйвер устройства помещает данные, полученные от устройства, в буфер и Память Ё Ё цддддд╢ 160110Ё CSR Ё здддддд© здддддддддtty00 Ё RDB цддддддддд╢ dz00 цдедддддддддtty01 Ё TDB Ё юдддддды Ё ... цддддд╢ юдддддддддtty07 160120Ё CSR Ё здддддд© здддддддддtty08 160122Ё RDB цддддддддд╢ dz01 цдедддддддддtty09 160126Ё TDB Ё юдддддды Ё ... цддддд╢ юдддддддддtty15 Ё Ё Рисунок 10.5. Отображение в памяти ввода-вывода с использова- нием контроллера VAX DZ11 копирует их из буфера в пользовательские адреса, указанные в вы- зове системной функции. Конкретный метод взаимодействия драйвера с устройством опре- деляется особенностями аппаратуры. Некоторые из машин обеспечива- ют отображение ввода-вывода в памяти, подразумевающее, что конк- ретные адреса в адресном пространстве ядра являются не номерами ячеек в физической памяти, а специальными регистрами, контролиру- ющими соответствующие устройства. Записывая в указанные регистры управляющие параметры в соответствии со спецификациями аппаратных средств, драйвер осуществляет управление устройством. Например, контроллер ввода-вывода для машины VAX-11 содержит специальные регистры для записи информации о состоянии устройства (регистры контроля и состояния) и для передачи данных (буферные регистры), которые формируются по специальным адресам в физической памяти. В частности, терминальный контроллер VAX DZ11 управляет 8 асинхрон- ными линиями терминальной связи (см. [Levy 80], где более подроб- но объясняется архитектура машин VAX). Пусть регистр контроля и состояния (CSR) для конкретного терминала DZ11 имеет адрес 160120, передающий буферный регистр (TDB) - адрес 120126, а при- нимающий буферный регистр (RDB) - адрес 160122 (Рисунок 10.5). Для того, чтобы передать символ на терминал "/dev/tty09", драйвер терминала записывает единицу (1 = 9 по модулю 8) в указанный дво- ичный разряд регистра контроля и состояния и затем записывает символ в передающий буферный регистр. Запись в передающий буфер- ный регистр является передачей данных. Контроллер DZ11 выставляет бит "выполнено" в регистре контроля и состояния, когда готов при- нять следующую порцию данных. Дополнительно драйвер может выста- вить бит "возможно прерывание передачи" в регистре контроля и состояния, что заставляет контроллер DZ11 прерывать работу систе- мы, когда он готов принять следующую порцию данных. Чтение данных из DZ11 производится аналогично. На других машинах имеется программируемый ввод-вывод, подра- зумевающий, что в машине имеются инструкции по управлению уст- ройствами. Драйверы управляют устройствами, выполняя соответствующие инструкции. Например, в машине IBM 370 имеется инструкция "Start I/O" (Начать ввод-вывод), которая инициирует операцию ввода-вывода, связанную с устройством. Способ связи драйвера с периферийными устройствами незаметен для пользователя. Поскольку интерфейс между драйверами устройств и соответству- ющими аппаратными средствами является машинно-зависимым, на этом уровне не существует стандартных интерфейсов. Как в случае ввода- вывода с отображением в памяти, так и в случае программируемого ввода-вывода драйвер может посылать на устройство управляющие последовательности с целью установления режима прямого доступа в память (ПДП) для устройства. Система позволяет осуществлять мас- совую передачу данных между устройством и памятью в режиме ПДП параллельно с работой центрального процессора, при этом устройс- тво прерывает работу системы по завершении передачи данных. Драй- вер организует управление виртуальной памятью таким образом, что- бы ячейки памяти с их действительными номерами использовались для ПДП. Быстродействующие устройства могут иногда передавать данные непосредственно в адресное пространство задачи, без вмешательства буфера ядра. В результате повышается скорость передачи данных, поскольку при этом производится на одну операцию копирования меньше, и, кроме того, объем данных, передаваемых за одну опера- цию, не ограничивается размером буферов ядра. Драйверы, осущест- вляющие такую передачу данных без "обработки", обычно используют блочный интерфейс для процедур посимвольного чтения и записи, ес- ли у них имеется двойник блочного типа. 10.1.2.4 Стратегический интерфейс Ядро использует стратегический интерфейс для передачи данных между буферным кешем и устройством, хотя, как уже говорилось ра- нее, процедуры чтения и записи для устройств посимвольного ввода- вывода иногда пользуются процедурой strategy (их двойника блочно- го типа) для непосредственной передачи данных между устройством и адресным пространством задачи. Процедура strategy может управлять очередностью выполнения заданий на ввод-вывод, связанный с уст- ройством, или выполнять более сложные действия по планированию выполнения подобных заданий. Драйверы в состоянии привязывать пе- редачу данных к одному физическому адресу или ко многим. Ядро пе- редает адрес заголовка буфера стратегической процедуре драйвера; в заголовке содержится список адресов (страниц памяти) и размеры данных, передаваемых на или с устройства. Аналогичное действие имеет место при работе механизма свопинга, описанного в главе 9. При работе с буферным кешем ядро передает данные с одного адреса; во время свопинга ядро передает данные, расположенные по несколь- ким адресам (страницы памяти). Если данные копируются из или в адресное пространство задачи, драйвер должен блокировать процесс (или по крайней мере, соответствующие страницы) в памяти до за- вершения передачи данных. Например, после монтирования файловой системы ядро идентифи- цирует каждый файл в файловой системе по номеру устройства и но- меру индекса. В номере устройства закодированы его старший и младший номера. Когда ядро обращается к блоку, который принадле- жит файлу, оно копирует номер устройства и номер блока в заголо- вок буфера, как уже говорилось ранее в главе 3. Обращения к дис- ку, использующие алгоритмы работы с буферным кешем (например, bread или bwrite), инициируют выполнение стратегической процеду- ры, определяемой старшим номером устройства. Стратегическая про- цедура использует значения полей младшего номера и номера блока из заголовка буфера для идентификации места расположения данных на устройстве, а адрес буфера - для идентификации места назначе- ния передаваемых данных. Точно так же, когда процесс обращается к устройству ввода-вывода блоками непосредственно (например, откры- вая устройство и читая или записывая на него), он использует ал- горитмы работы с буферным кешем, и интерфейс при этом функциони- рует вышеописанным образом. 10.1.2.5 Ioctl Системная функция ioctl является обобщением специфичных для терминала функций stty (задать установки терминала) и gtty (полу- чить установки терминала), имевшихся в ранних версиях системы UNIX. Она выступает в качестве общей точки входа для всех связан- ных с типом устройства команд и позволяет процессам задавать ап- паратные параметры, ассоциированные с устройством, и программные параметры, ассоциированные с драйвером. Специальные действия, вы- полняемые функцией ioctl для разных устройств различны и опреде- ляются типом драйвера. Программы, использующие вызов ioctl, должны должны знать, с файлом какого типа они работают, так как они являются аппаратно-зависимыми. Исключение из общего правила сделано для системы, которая не видит различий между файлами раз- ных типов. Более подробно использование функции ioctl для терми- налов рассмотрено в разделе 10.3.3. Синтаксис командной строки, содержащей вызов системной функ- ции: ioctl(fd,command,arg); где fd - дескриптор файла, возвращаемый предварительно вызванной функцией open, command - действие (команда), которое необходимо выполнить драйверу, arg - параметр команды (может быть указателем на структуру). Команды специфичны для различных драйверов; следо- вательно, каждый драйвер интерпретирует команды в соответствии со своими внутренними спецификациями, от команды, в свою очередь, зависит формат структуры данных, описываемой передаваемым пара- метром. Драйверы могут считывать структуру данных arg из прост- ранства задачи в соответствии с предопределенным форматом или за- писывать установки устройства в пространство задачи по адресу указанной структуры. Например, наличие интерфейса, предоставляе- мого функцией ioctl, дает возможность пользователям устанавливать для терминала скорость передачи информации в бодах, перематывать магнитную ленту, и, наконец, выполнять сетевые операции, задавая номера виртуальных каналов и сетевые адреса. 10.1.2.6 Другие функции, имеющие отношение к файловой сис- теме Такие функции работы с файловой системой, как stat и chmod, выполняются одинаково, как для обычных файлов, так и для уст- ройств; они манипулируют с индексом, не обращаясь к драйверу. Даже системная функция lseek работает для устройств. Например, если процесс подводит головку на лентопротяжном устройстве к ука- занному адресу смещения в байтах с помощью функции lseek, ядро корректирует смещение в таблице файлов но не выполняет никаких действий, специфичных для данного типа драйвера. Когда позднее процесс выполняет чтение (read) или запись (write), ядро пересы- лает адрес смещения из таблицы файлов в адресное пространство за- дачи, подобно тому, как это имеет место при работе с файлами обычного типа, и устройство физически перемещает головку к соот- ветствующему смещению, указанному в пространстве задачи. Этот случай иллюстрируется на примере в разделе 10.3. Периферийные Соединительная Вектор устройства панель прерывания здддддддддддддддддд© Ё Ы Ё tty00 ддддддддддддд© Ё Ы Ё tty01 .... Ё Ё Ы Ё ... Ё здд© цдддддддддддддддддд╢ tty07 дддддддддддддад╢ ЁЫЫЫЫЫЫЫЫЫЫЫЫЁ ttyintr 0 Ё tty08 ддддддддддддд© цдд╢ цдддддддддддддддддд╢ tty09 .... цд╢ ЁЫЫЫЫЫЫЫЫЫЫЫЫЁ ttyintr 1 Ё ... здддддддды цдд╢ цдддддддддддддддддд╢ tty15 дддды здддддддд╢ ЁЫЫЫЫЫЫЫЫЫЫЫЫЁ consintr Ё консоль дддддды цдд╢ цдддддддддддддддддд╢ принтер00 дддддддддддддбд╢ ЁЫЫЫЫЫЫЫЫЫЫЫЫЁ printintr 0 Ё .... Ё цдд╢ цдддддддддддддддддд╢ Ё Ё Ё Ё Ы Ё принтер03 ддддддддддддды Ё Ё Ё Ы Ё Ё Ё Ё Ы Ё юдды юдддддддддддддддддды Рисунок 10.6. Прерывания от устройств 10.1.3 Программы обработки прерываний Как уже говорилось выше (раздел 6.4.1), возникновение преры- вания побуждает ядро запускать программу обработки прерываний, в основе алгоритма которой лежит соотношение между устройством, вызвавшим прерывание, и смещением в таблице векторов прерываний. Ядро запускает программу обработки прерываний для данного типа устройства, передавая ей номер устройства или другие параметры для того, чтобы идентифицировать единицу устройства, вызвавшую прерывание. Например, в таблице векторов прерываний на Рисунке 10.6 показаны две точки входа для обработки прерываний от терми- налов ("ttyintr"), каждая из которых используется для обработки прерываний, поступивших от 8 терминалов. Если устройство tty09 прервало работу системы, система вызывает программу обработки прерывания, ассоциированную с местом аппаратного подключения уст- ройства. Поскольку с одной записью в таблице векторов прерываний может быть связано множество физических устройств, драйвер должен уметь распознавать устройство, вызвавшее прерывание. На рисунке записи в таблице векторов прерываний, соответствующие прерываниям от терминалов, имеют метки 0 и 1, чтобы система различала их меж- ду собой при вызове программы обработки прерываний, используя к примеру этот номер в качестве передаваемого программе параметра. Программа обработки прерываний использует этот номер и другую ин- формацию, переданную механизмом прерывания, для того, чтобы удос- товериться, что именно устройство tty09, а не tty12, прервало ра- боту системы. Этот пример в упрощенном виде показывает то, что имеет место в реальных системах, где на самом деле существует несколько уровней контроллеров и соответствующих программ обра- ботки прерываний, но он иллюстрирует общие принципы. Если подвести итог, можно сказать, что номер устройства, ис- пользуемый программой обработки прерываний, идентифицирует едини- цу аппаратуры, а младший номер в файле устройства идентифицирует устройство для ядра. Драйвер устройства устанавливает соответс- твие между младшим номером устройства и номером единицы аппарату- ры. 10.2 ДИСКОВЫЕ ДРАЙВЕРЫ Так сложилось исторически, что дисковые устройства в системах UNIX разбивались на разделы, содержащие различные файловые систе- мы, что означало "деление [дискового] пакета на несколько управ- ляемых по-своему частей" (см. [System V 84b]). Например, если на диске располагаются четыре файловые системы, администратор может оставить одну из них несмонтированной, одну смонтировать только для чтения, а две других только для записи. Несмотря на то, что все файловые системы сосуществуют на одном физическом устройстве, пользователи не могут ни обращаться к файлам немонтированной фай- ловой системы, используя методы доступа, описанные в главах 4 и 5, ни записывать файлы в файловые системы, смонтированные только для чтения. Более того, так как каждый раздел (и, следовательно, файловая система) занимает на диске смежные дорожки и цилиндры, скопировать всю файловую систему легче, чем в том случае, если бы раздел занимал участки, разбросанные по всему дисковому тому. Дисковый драйвер транслирует адрес файловой системы, состоя- щий из логического номера устройства и номера блока, в точный но- мер дискового сектора. Драйвер получает адрес одним из следующих путей: либо стратегическая процедура использует буфер из буферно- го пула, заголовок которого содержит номера устройства и блока, либо процедуры чтения и записи передают логический (младший) но- мер устройства в качестве параметра; они преобразуют адрес смеще- ния в байтах, хранящийся в пространстве задачи, в адрес соответс- твующего блока. Дисковый драйвер использует номер устройства для идентификации физического устройства и указания используемого раздела, обращаясь при этом к внутренним таблицам для поиска сек- тора, отмечающего начало раздела на диске. Наконец, он добавляет номер блока в файловой системе к номеру блока, с которого начина- ется каждый сектор, чтобы идентифицировать сектор, используемый для ввода-вывода. зддддддддддддддддддддддддддддддддддддддддддддд© Ё Раздел Начальный блок Длина в блоках Ё Ё Ё Ё Размер блока = 512 байт Ё Ё Ё Ё 0 0 64000 Ё Ё 1 64000 944000 Ё Ё 2 168000 840000 Ё Ё 3 336000 672000 Ё Ё 4 504000 504000 Ё Ё 5 672000 336000 Ё Ё 6 840000 168000 Ё Ё 7 0 1008000 Ё юддддддддддддддддддддддддддддддддддддддддддддды Рисунок 10.7. Разделы на диске RP07 Исторически сложилось так, что размеры дисковых разделов ус- танавливаются в зависимости от типа диска. Например, диск DEC RP07 разбит на разделы, характеристика которых приведена на Ри- сунке 10.7. Предположим, что файлы "/dev/dsk0", "/dev/dsk1", "/dev/dsk2" и "/dev/dsk3" соответствуют разделам диска RP07, име- ющим номера от 0 до 3, и имеют аналогичные младшие номера. Пусть размер логического блока в файловой системе совпадает с размером дискового блока. Если ядро пытается обратиться к блоку с номером 940 в файловой системе, хранящейся в "/dev/dsk3", дисковый драй- вер переадресует запрос к блоку с номером 336940 (раздел 3 начи- нается с блока, имеющего номер 336000; 336000 + 940 = 336940) на диске. Размеры разделов на диске варьируются и администраторы распо- лагают файловые системы в разделах соответствующего размера: большие файловые системы попадают в разделы большего размера и т. д. Разделы на диске могут перекрываться. Например, разделы 0 и 1 на диске RP07 не пересекаются, но вместе они занимают блоки с но- мерами от 0 до 1008000, то есть весь диск. Раздел 7 так же зани- мает весь диск. Перекрытие разделов не имеет значения, поскольку файловые системы, хранящиеся в разделах, размещаются таким обра- зом, что между ними нет пересечений. Иметь один раздел, включаю- щий в себя все дисковое пространство, выгодно, поскольку весь том можно быстро скопировать. Использование разделов фиксированного состава и размера огра- ничивает гибкость дисковой конфигурации. Информацию о разделах в закодированном виде не следует включать в дисковый драйвер, но нужно поместить в таблицу содержимого дискового тома. Однако, найти общее место на всех дисках для размещения таблицы содержи- мого дискового тома и сохранить тем самым совместимость с преды- дущими версиями системы довольно трудно. В существующих реализа- циях версии V предполагается, что блок начальной загрузки первой из файловых систем на диске занимает первый сектор тома, хотя по логике это, казалось бы, самое подходящее место для таблицы со- держимого тома. И все же дисковый драйвер должен иметь закодиро- ванную информацию о месте расположения таблицы содержимого тома для каждого диска, не препятствуя существованию дисковых разделов переменного размера. В связи с тем, что для системы UNIX является типичным высокий уровень дискового трафика, драйвер диска должен максимизировать передачу данных с тем, чтобы обеспечить наилучшую производитель- ность всей системы. Новейшие дисковые контроллеры осуществляют планирование выполнения заданий, требующих обращения к диску, по- зиционируют головку диска и обеспечивают передачу данных между диском и центральным процессором; иначе это приходится делать дисковому драйверу. Сервисные программы могут непосредственно обращаться к диску в обход стандартного метода доступа к файловой системе, рассмот- ренного в главах 4 и 5, как пользуясь блочным интерфейсом, так и не прибегая к структурированию данных. Непосредственно работают с диском две важные программы - mkfs и fsck. Программа mkfs форма- тирует раздел диска для файловой системы UNIX, создавая при этом суперблок, список индексов, список свободных дисковых блоков с указателями и корневой каталог новой файловой системы. Программа fsck проверяет целостность существующей файловой системы и исп- равляет ошибки, как показано в главе 5. Рассмотрим программу, приведенную на Рисунке 10.8, в примене- нии к файлам "/dev/dsk15" и "/dev/rdsk15", и предположим, что ко- манда ls выдала следующую информацию: ls -1 /dev/dsk15 /dev/rdsk15 br-------- 2 root root 0,21 Feb 12 15:40 /dev/dsk15 crw-rw---- 2 root root 7,21 Mar 7 09:29 /dev/rdsk15 Отсюда видно, что файл "/dev/dsk15" соответствует устройству блочного типа, владельцем которого является пользователь под име- нем "root", и только пользователь "root" может читать с него не- посредственно. Его старший номер - 0, младший - 21. Файл "/dev/rdsk15" соответствует устройству посимвольного ввода-выво- да, владельцем которого является пользователь "root", однако пра- ва доступа к которому на запись и чтение есть как у владельца, так и у группы. Его старший номер - 7, младший - 21. Процесс, от- крывающий файлы, получает доступ к устройству через таблицу клю- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include "fcntl.h" Ё Ё main() Ё Ё { Ё Ё char buf1[4096], buf2[4096] Ё Ё int fd1, fd2, i; Ё Ё Ё Ё if (((fd1 = open("/dev/dsk5/", O_RDONLY)) == -1) || Ё Ё ((fd2 = open("/dev/rdsk5", O_RDONLY)) == -1))Ё Ё { Ё Ё printf("ошибка при открытии\n"); Ё Ё exit(); Ё Ё } Ё Ё Ё Ё lseek(fd1, 8192L, 0); Ё Ё lseek(fd2, 8192L, 0); Ё Ё Ё Ё if ((read(fd1, buf1, sizeof(buf1)) == -1) || Ё Ё (read(fd2, buf2, sizeof(buf2)) == -1)) Ё Ё { Ё Ё printf("ошибка при чтении\n"); Ё Ё exit(); Ё Ё } Ё Ё Ё Ё for (i = 0; i < sizeof(buf1); i++) Ё Ё if (buf1[i] != buf2[i]) Ё Ё { Ё Ё printf("различие в смещении %d\n", i); Ё Ё exit(); Ё Ё } Ё Ё printf("данные совпадают\n"); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 10.8. Чтение данных с диска с использованием блочного интерфейса и без структурирования данных чей устройств ввода-вывода блоками и таблицу ключей устройств по- символьного ввода-вывода, соответственно, а младший номер устройства 21 информирует драйвер о том, к какому разделу диска производится обращение, например, дисковод 2, раздел 1. Поскольку младшие номера у файлов совпадают, они ссылаются на один и тот же раздел диска, если предположить, что это одно устройство (***). Таким образом, процесс, выполняющий программу, открывает один и тот же драйвер дважды (используя различные интерфейсы), позицио- нирует головку к смещению с адресом 8192 и считывает данные с ддддддддддддддддддддддддддддддддддддддд (***) Не существует иного способа установить, что символьный и блочный драйверы ссылаются на одно и то же устройство, кро- ме просмотра таблиц системной конфигурации и текста про- грамм драйвера. этого места. Результаты выполнения операций чтения должны быть идентичными при условии, что работает только одна файловая систе- ма. Программы, осуществляющие чтение и запись на диск непосредс- твенно, представляют опасность, поскольку манипулируют с чувстви- тельной информацией, рискуя нарушить системную защиту. Админист- раторам следует защищать интерфейсы ввода-вывода путем установки прав доступа к файлам дисковых устройств. Например, дисковые фай- лы "/dev/dsk15" и "/dev/rdsk15" должны принадлежать пользователю с именем "root", и права доступа к ним должны быть определены та- ким образом, чтобы пользователю "root" было разрешено чтение, а всем остальным пользователям и чтение, и запись должны быть зап- рещены. Программы, осуществляющие чтение и запись на диск непосредс- твенно, могут также нарушить целостность данных в файловой систе- ме. Алгоритмы файловой системы, рассмотренные в главах 3, 4 и 5, координируют выполнение операций ввода-вывода, связанных с дис- ком, тем самым поддерживая целостность информационных структур на диске, в том числе списка свободных дисковых блоков и указателей из индексов на информационные блоки прямой и косвенной адресации. Процессы, обращающиеся к диску непосредственно, обходят эти алго- ритмы. Пусть даже их программы написаны с большой осторожностью, проблема целостности все равно не исчезнет, если они выполняются параллельно с работой другой файловой системы. По этой причине программа fsck не должна выполняться при наличии активной файло- вой системы. Два типа дискового интерфейса различаются между собой по использованию буферного кеша. При работе с блочным интерфейсом ядро пользуется тем же алгоритмом, что и для файлов обычного ти- па, исключение составляет тот момент, когда после преобразования адреса смещения логического байта в адрес смещения логического блока (см. алгоритм bmap в главе 4) оно трактует адрес смещения логического блока как физический номер блока в файловой системе. Затем, используя буферный кеш, ядро обращается к данным, и, в ко- нечном итоге, к стратегическому интерфейсу драйвера. Однако, при обращении к диску через символьный интерфейс (без структурирова- ния данных), ядро не превращает адрес смещения в адрес файла, а передает его немедленно драйверу, используя для передачи рабочее пространство задачи. Процедуры чтения и записи, входящие в состав драйвера, преобразуют смещение в байтах в смещение в блоках и ко- пируют данные непосредственно в адресное пространство задачи, ми- нуя буферы ядра. Таким образом, если один процесс записывает на устройство блочного типа, а второй процесс затем считывает с устройства сим- вольного типа по тому же адресу, второй процесс может не считать информацию, записанную первым процессом, так как информация может еще находиться в буферном кеше, а не на диске. Тем не менее, если второй процесс обратится к устройству блочного типа, он автомати- чески попадет на новые данные, находящиеся в буферном кеше. При использовании символьного интерфейса можно столкнуться со странной ситуацией. Если процесс читает или пишет на устройство посимвольного ввода-вывода порциями меньшего размера, чем, к при- меру, блок, результаты будут зависеть от драйвера. Например, если производить запись на ленту по 1 байту, каждый байт может попасть в любой из ленточных блоков. Преимущество использования символьного интерфейса состоит в скорости, если не возникает необходимость в кешировании данных для дальнейшей работы. Процессы, обращающиеся к устройствам ввода -вывода блоками, передают информацию блоками, размер каждого из которых ограничивается размером логического блока в данной файло- вой системе. Например, если размер логического блока в файловой системе 1 Кбайт, за одну операцию ввода-вывода может быть переда- но не больше 1 Кбайта информации. При этом процессы, обращающиеся к диску с помощью символьного интерфейса, могут передавать за од- ну дисковую операцию множество дисковых блоков, в зависимости от возможностей дискового контроллера. С функциональной точки зре- ния, процесс получает тот же самый результат, но символьный ин- терфейс может работать гораздо быстрее. Если воспользоваться при- мером, приведенным на Рисунке 10.8, можно увидеть, что когда процесс считывает 4096 байт, используя блочный интерфейс для фай- ловой системы с размером блока 1 Кбайт, ядро производит четыре внутренние итерации, на каждом шаге обращаясь к диску, прежде чем вызванная системная функция возвращает управление, но когда про- цесс использует символьный интерфейс, драйвер может закончить чтение за одну дисковую операцию. Более того, использование блоч- ного интерфейса вызывает дополнительное копирование данных между адресным пространством задачи и буферами ядра, что отсутствует в символьном интерфейсе. 10.3 ТЕРМИНАЛЬНЫЕ ДРАЙВЕРЫ Терминальные драйверы выполняют ту же функцию, что и осталь- ные драйверы: управление передачей данных от и на терминалы. Од- нако, терминалы имеют одну особенность, связанную с тем, что они обеспечивают интерфейс пользователя с системой. Обеспечивая инте- рактивное использование системы UNIX, терминальные драйверы имеют свой внутренний интерфейс с модулями, интерпретирующими ввод и вывод строк. В каноническом режиме интерпретаторы строк преобра- зуют неструктурированные последовательности данных, введенные с клавиатуры, в каноническую форму (то есть в форму, соответствую- щую тому, что пользователь имел в виду на самом деле) прежде, чем послать эти данные принимающему процессу; строковый интерфейс также преобразует неструктурированные последовательности выходных данных, созданных процессом, в формат, необходимый пользователю. В режиме без обработки строковый интерфейс передает данные между процессами и терминалом без каких-либо преобразований. Программисты, например, работают на клавиатуре терминала до- вольно быстро, но с ошибками. На этот случай терминалы имеют кла- вишу стирания ("erase"; клавиша может быть обозначена таким обра- зом), чтобы пользователь имел возможность стирать часть введенной строки и вводить коррективы. Терминалы пересылают машине всю вве- денную последовательность, включая и символы стирания (*** *). В каноническом режиме строковый интерфейс буферизует информацию в строки (набор символов, заканчивающийся символом возврата каретки (*****)) и процессы стирают символы у себя, прежде чем переслать исправленную последовательность считывающему процессу. В функции строкового интерфейса входят: * построчный разбор введенных последовательностей; * обработка символов стирания; * обработка символов "удаления", отменяющих все остальные сим- волы, введенные до того в текущей строке; * отображение символов, полученных терминалом; * расширение выходных данных, например, преобразование символов табуляции в последовательности пробелов; * сигнализирование процессам о зависании терминалов и прерыва- нии строк или в ответ на нажатие пользователем клавиши удале- ния; * предоставление возможности не обрабатывать специальные символы, такие как символы стирания, удаления и возврата ка- ретки. ддддддддддддддддддддддддддддддддддддддд (****) В этом разделе рассматривается использование терминалов ввода-вывода, которые передают все символы, введенные пользователем, без обработки. (*****) В данной главе используется общий термин "возврат карет- ки" для обозначения символов возврата каретки и перевода строки. Функционирование без обработки подразумевает использование асинхронного терминала, поскольку процессы могут считывать симво- лы в том виде, в каком они были введены, вместо того, чтобы ждать, когда пользователь нажмет клавишу ввода или возврата ка- ретки. Ричи отметил, что первые строковые интерфейсы, используемые еще при разработке системы в начале 70-х годов, работали в соста- ве программ командного процессора и редактора, но не в ядре (см. [Ritchie 84], стр.1580). Однако, поскольку в их функциях нуждает- ся множество программ, их место в составе ядра. Несмотря на то, что строковый интерфейс выполняет такие функции, из которых логи- чески вытекает его место между терминальным драйвером и остальной частью ядра, ядро не запускает строковый интерфейс иначе, чем че- рез терминальный драйвер. На Рисунке 10.9 показаны поток данных, проходящий через терминальный драйвер и строковый интерфейс, и соответствующие ему управляющие воздействия, проходящие через терминальный драйвер. Пользователи могут указать, какой строковый интерфейс используется посредством вызова системной функции ioctl, но реализовать схему, по которой одно устройство использо- вало бы несколько строковых интерфейсов одновременно, при чем каждый интерфейсный модуль, в свою очередь, успешно вызывал бы следующий модуль для обработки данных, довольно трудно. Поток данных Поток управляющих воздействий зддддддддддддддддддддддд© зддддддддддддддддддддддд© Ё Процесс чтения/записи Ё Ё Процесс чтения/записи Ё юддддддддддддддддддддддды юддддддддддддддддддддддды Ы Ё ^ Ы Ё ^ Ы v Ё Ы v Ё Ы зддддддддддддддддддддд© Ы зддддддддддддддддддддддд© вывод Ё Строковый интерфейсЁ ввод Ё Терминальный драйвер Ё Ы юддддддддддддддддддддды Ы юддддддддддддддддддддддды Ы Ё ^ Ы Ё ^ Ы v Ё Ы v Ё зддддддддддддддддддддддд© зддддддддддддддддддддд© Ё Терминальный драйвер Ё Ё Строковый интерфейс Ё юддддддддддддддддддддддды юддддддддддддддддддддды Ё ^ v Ё зддддддддддддддддддддддд© Ё Драйвер ввода-вывода Ё юддддддддддддддддддддддды Ё ^ v Ё зддддддддддддддддддддддддд© Ё Устройство ввода-вывода Ё юддддддддддддддддддддддддды Рисунок 10.9. Последовательность обращений и поток данных че- рез строковый интерфейс Указатель Смещение Смещение на до до следующий начала конца Массив символов блок 0 1 2 3 4 5 6 7 8 9 14 ддддддддддбдддддддддбдддддддддббдбдбдбдбдбдбдбдбдбдбдбдбдбдбдбддд Ё 7 Ё 14 ЁЁgЁaЁrЁbЁaЁgЁeЁЁЁ ЁeЁqЁnЁ ЁЁЁ Ё... ддддедддддадддддддддадддддддддаададададададададададададададададдд Ё v Рисунок 10.10. Символьный блок 10.3.1 Символьные списки Строковый интерфейс обрабатывает данные в символьных списках. Символьный список (clist) представляет собой переменной длины список символьных блоков с использованием указателей и с подсче- том количества символов в списке. Символьный блок (cblock) содер- жит указатель на следующий блок в списке, небольшой массив храни- мой в символьном виде информации и адреса смещений, показывающие место расположения внутри блока корректной информации (Рисунок 10.10). Смещение до начала показывает первую позицию расположения корректной информации в массиве, смещение до конца показывает первую позицию расположения некорректной информации. Ядро обеспечивает ведение списка свободных символьных блоков и выполняет над символьными списками и символьными блоками шесть операций. 1. Ядро назначает драйверу символьный блок из списка свободных символьных блоков. 2. Оно также возвращает символьный блок в список свободных сим- вольных блоков. 3. Ядро может выбирать первый символ из символьного списка: оно удаляет первый символ из первого символьного блока в списке и устанавливает значения счетчика символов в списке и указате- лей в блоке таким образом, чтобы последующие операции не вы- бирали один и тот же символ. Если в результате операции выб- ран последний символ блока, ядро помещает в список свободных символьных блоков пустой блок и переустанавливает указатели в символьном списке. Если в символьном списке отсутствуют сим- волы, ядро возвращает пустой символ. 4. Ядро может поместить символ в конец символьного списка путем поиска последнего символьного блока в списке, включения сим- вола в него и переустановки адресов смещений. Если символьный блок заполнен, ядро выделяет новый символьный блок, включает его в конец символьного списка и помещает символ в новый блок. 5. Ядро может удалять от начала списка группу символов по одному блоку за одну операцию, что эквивалентно удалению всех симво- лов в блоке за один раз. 6. Ядро может поместить блок с символами в конец символьного списка. Символьные списки позволяют создать несложный механизм буфе- ризации, полезный при небольшом объеме передаваемых данных, ти- пичном для медленных устройств, таких как терминалы. Они дают возможность манипулировать с данными с каждым символом в отдель- ности и с группой символьных блоков. Например, Рисунок 10.11 ил- люстрирует удаление символов из символьного списка; ядро удаляет по одному символу из первого блока в списке (Рисунок 10.11а-в) до тех пор, пока в блоке не останется ни одного символа (Рисунок символьный символьные список блоки зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддд>Ё 0 8 Ё Ё p Ё i Ё c Ё Ё f Ё i Ё l Ё e Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддддддддбд© Ё Ё 0 8 Ё Ё * Ё Ё Ё Ё Ё t Ё b Ё l Ё Ё Ё 27 Ё едды юбдддддды юдддадддадддадддадддадддадддаддды Ё символов Ё едд© v юддддддддддады Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© Ё Ё 0 8 Ё Ё Ё Ё Ё t Ё r Ё o Ё f Ё f Ё Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© юддддд>Ё 0 3 Ё Ё - Ё m Ё m Ё Ё Ё Ё Ё Ё юддддддды юдддадддадддадддадддадддадддаддды (а) зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддд>Ё 1 8 Ё Ё Ё i Ё c Ё Ё f Ё i Ё l Ё e Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддддддддбд© Ё Ё 0 8 Ё Ё * Ё Ё Ё Ё Ё t Ё b Ё l Ё Ё Ё 26 Ё едды юбдддддды юдддадддадддадддадддадддадддаддды Ё символов Ё едд© v юддддддддддады Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© Ё Ё 0 8 Ё Ё Ё Ё Ё t Ё r Ё o Ё f Ё f Ё Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© юддддд>Ё 0 3 Ё Ё - Ё m Ё m Ё Ё Ё Ё Ё Ё юддддддды юдддадддадддадддадддадддадддаддды (б) зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддд>Ё 2 8 Ё Ё Ё Ё c Ё Ё f Ё i Ё l Ё e Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддддддддбд© Ё Ё 0 8 Ё Ё * Ё Ё Ё Ё Ё t Ё b Ё l Ё Ё Ё 25 Ё едды юбдддддды юдддадддадддадддадддадддадддаддды Ё символов Ё едд© v юддддддддддады Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© Ё Ё 0 8 Ё Ё Ё Ё Ё t Ё r Ё o Ё f Ё f Ё Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© юддддд>Ё 0 3 Ё Ё - Ё m Ё m Ё Ё Ё Ё Ё Ё юддддддды юдддадддадддадддадддадддадддаддды (в) зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддд>Ё 0 8 Ё Ё * Ё Ё Ё Ё Ё t Ё b Ё l Ё Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды зддддддддддбд© Ё v Ё 19 Ё едды зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© Ё символов Ё едд© Ё 0 8 Ё Ё Ё Ё Ё t Ё r Ё o Ё f Ё f Ё Ё юддддддддддады Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© юддддд>Ё 0 3 Ё Ё - Ё m Ё m Ё Ё Ё Ё Ё Ё юддддддды юдддадддадддадддадддадддадддаддды (г) Рисунок 10.11. Удаление символов из символьного списка символьный символьные список блоки зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддд>Ё 0 8 Ё Ё p Ё i Ё c Ё Ё f Ё i Ё l Ё e Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды зддддддддддбд© Ё v Ё 22 Ё едды зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© Ё символа Ё едд© Ё 0 8 Ё Ё * Ё Ё Ё Ё Ё t Ё b Ё l Ё Ё юддддддддддады Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© юддддд>Ё 0 6 Ё Ё Ё Ё Ё t Ё r Ё o Ё f Ё Ё Ё юддддддды юдддадддадддадддадддадддадддаддды (а) зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддд>Ё 0 8 Ё Ё p Ё i Ё c Ё Ё f Ё i Ё l Ё e Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды зддддддддддбд© Ё v Ё 23 Ё едды зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© Ё символа Ё едд© Ё 0 8 Ё Ё * Ё Ё Ё Ё Ё t Ё b Ё l Ё Ё юддддддддддады Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© юддддд>Ё 0 7 Ё Ё Ё Ё Ё t Ё r Ё o Ё f Ё f Ё Ё юддддддды юдддадддадддадддадддадддадддаддды (б) зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддд>Ё 0 8 Ё Ё p Ё i Ё c Ё Ё f Ё i Ё l Ё e Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды зддддддддддбд© Ё v Ё 24 Ё едды зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© Ё символа Ё едд© Ё 0 8 Ё Ё * Ё Ё Ё Ё Ё t Ё b Ё l Ё Ё юддддддддддады Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© юддддд>Ё 0 8 Ё Ё Ё Ё Ё t Ё r Ё o Ё f Ё f Ё Ё юддддддды юдддадддадддадддадддадддадддаддды (в) зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддд>Ё 0 8 Ё Ё p Ё i Ё c Ё Ё f Ё i Ё l Ё e Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© зддддддддддбд© Ё Ё 0 8 Ё Ё * Ё Ё Ё Ё Ё t Ё b Ё l Ё Ё Ё 25 Ё едды юбдддддды юдддадддадддадддадддадддадддаддды Ё символов Ё едд© v юддддддддддады Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© Ё Ё 0 8 Ё Ё Ё Ё Ё t Ё r Ё o Ё f Ё f Ё Ё Ё юбдддддды юдддадддадддадддадддадддадддаддды Ё v Ё зддддддд© здддбдддбдддбдддбдддбдддбдддбддд© юддддд>Ё 0 1 Ё Ё - Ё Ё Ё Ё Ё Ё Ё Ё юддддддды юдддадддадддадддадддадддадддаддды (г) Рисунок 10.12. Включение символов в символьный список 10.11г); затем оно устанавливает указатель списка на следующий блок, который становится первым блоком в списке. Подобно этому на Рисунке 10.12 показано, как ядро включает символы в символьный список; при этом предполагается, что в одном блоке помещается до 8 символов и что ядро размещает новый блок в конце списка (Рису- нок 10.12г). 10.3.2 Терминальный драйвер в каноническом режиме Структуры данных, с которыми работают терминальные драйверы, связаны с тремя символьными списками: списком для хранения дан- ных, выводимых на терминал, списком для хранения неструктуриро- ванных вводных данных, поступивших в результате выполнения прог- раммы обработки прерывания от терминала, вызванного попыткой пользователя ввести данные с клавиатуры, и списком для хранения обработанных входных данных, поступивших в результате преобразо- вания строковым интерфейсом специальных символов (таких как сим- волы стирания и удаления) в неструктурированном списке. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм terminal_write Ё Ё { Ё Ё выполнить (пока из пространства задачи еще поступают Ё Ё данные) Ё Ё { Ё Ё если (на терминал поступает информация) Ё Ё { Ё Ё приступить к выполнению операции записи данных Ё Ё из списка, хранящего выводные данные; Ё Ё приостановиться (до того момента, когда терми- Ё Ё нал будет готов принять следующую порцию дан- Ё Ё ных); Ё Ё продолжить; /* возврат к началу цикла */ Ё Ё } Ё Ё скопировать данные в объеме символьного блока из Ё Ё пространства задачи в список, хранящий выводные Ё Ё данные: строковый интерфейс преобразует символы Ё Ё табуляции и т.д.; Ё Ё } Ё Ё Ё Ё приступить к выполнению операции записи данных из спис-Ё Ё ка, хранящего выводные данные; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 10.13. Алгоритм переписи данных на терминал Когда процесс ведет запись на терминал (Рисунок 10.13), тер- минальный драйвер запускает строковый интерфейс. Строковый интерфейс в цикле считывает символы из адресного пространства процесса и помещает их в символьный список для хранения выводных данных до тех пор, пока поток данных не будет исчерпан. Строковый интерфейс обрабатывает выводимые символы, например, заменяя сим- волы табуляции на последовательности пробелов. Если количество символов в списке для хранения выводных данных превысит верхнюю отметку, строковый интерфейс вызывает процедуры драйвера, пересы- лающие данные из символьного списка на терминал и после этого приостанавливающие выполнение процесса, ведущего запись. Когда объем информации в списке для хранения выводных данных падает за нижнюю отметку, программа обработки прерываний возобновляет вы- полнение всех процессов, приостановленных до того момента, когда терминал сможет принять следующую порцию данных. Строковый интер- фейс завершает цикл обработки, скопировав всю выводимую информа- цию из адресного пространства задачи в соответствующий символьный список, и вызывает выполнение процедур драйвера, пересылающих данные на терминал, о которых уже было сказано выше. Если на терминал ведут запись несколько процессов, они неза- висимо друг от друга следуют указанной процедуре. Выводимая ин- формация может быть искажена; то есть на терминале данные, запи- сываемые процессами, могут пересекаться. Это может произойти из-за того, что процессы ведут запись на терминал, используя нес- колько вызовов системной функции write. Ядро может переключать контекст, пока процесс выполняется в режиме задачи, между после- довательными вызовами функции write, и вновь запущенные процессы могут вести запись на терминал, пока первый из процессов приоста- новлен. Выводимые данные могут быть также искажены и на термина- ле, поскольку процесс может приостановиться на середине выполне- ния системной функции write, ожидая завершения вывода на терминал из системы предыдущей порции данных. Ядро может запустить другие процессы, которые вели запись на терминал до того, как первый процесс был повторно запущен. По этой причине, ядро не гарантиру- ет, что содержимое буфера данных, выводимое в результате вызова системной функции write, появится на экране терминала в непрерыв- ном виде. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё char form[]="это пример вывода строки из порожденного процесса"Ё Ё Ё main() Ё Ё { Ё Ё char output[128]; Ё Ё int i; Ё Ё Ё Ё for (i = 0; i < 18; i++) Ё Ё { Ё Ё switch (fork()) Ё Ё { Ё Ё case -1: /* ошибка --- превышено максимальное чис-Ё Ё ло процессов */ Ё Ё exit(); Ё Ё Ё Ё default: /* родительский процесс */ Ё Ё break; Ё Ё Ё Ё case 0: /* порожденный процесс */ Ё Ё /* формат вывода строки в переменной output */ Ё Ё sprintf(output,"%%d\n%s%d\n",form,i,form,i); Ё Ё for (;;) Ё Ё write(1,output,sizeof(output)); Ё Ё } Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 10.14. Передача данных через стандартный вывод Рассмотрим программу, приведенную на Рисунке 10.14. Родитель- ский процесс создает до 18 порожденных процессов; каждый из по- рожденных процессов записывает строку (с помощью библиотечной функции sprintf) в массив output, который включает сообщение и значение счетчика i в момент выполнения функции fork, и затем входит в цикл пошаговой переписи строки в файл стандартного выво- да. Если стандартным выводом является терминал, терминальный драйвер регулирует поток поступающих данных. Выводимая строка имеет более 64 символов в длину, то есть слишком велика для того, чтобы поместиться в символьном блоке (длиной 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, и это правильно, посколь- ку другие программы, которые считывают информацию с терминалов (например, редакторы), имеют различный синтаксис команд. Поэтому строковый интерфейс выполняет чтение по получении символа возвра- та каретки. На Рисунке 10.15 показан алгоритм чтения с терминала. Предпо- ложим, что терминал работает в каноническом режиме; в разделе 10.3.3 будет рассмотрена работа в режиме без обработки. Если в настоящий момент в любом из символьных списков для хранения ввод- ной информации отсутствуют данные, процесс, выполняющий чтение, приостанавливается до поступления первой строки данных. Когда данные поступают, программа обработки прерывания от терминала за- пускает "программу обработки прерывания" строкового интерфейса, которая помещает данные в список для хранения неструктурированных вводных данных для передачи процессам, осуществляющим чтение, и в список для хранения выводных данных, передаваемых в качестве эхо- сопровождения на терминал. Если введенная строка содержит символ возврата каретки, программа обработки прерывания возобновляет вы- полнение всех приостановленных процессов чтения. Когда процесс, осуществляющий чтение, выполняется, драйвер выбирает символы из списка для хранения неструктурированных вводных данных, обрабаты- вает символы стирания и удаления и помещает символы в каноничес- кий символьный список. Затем он копирует строку символов в адрес- ное пространство задачи до символа возврата каретки или до исчерпания числа символов, указанного в вызове системной функции read, что встретится раньше. Однако, процесс может обнаружить, что данных, ради которых он возобновил свое выполнение, больше не существует: другие процессы считали данные с терминала и удалили их из списка для неструктурированных вводных данных до того, как первый процесс был запущен вновь. Такая ситуация похожа на ту, которая имеет место, когда из канала считывают данные несколько процессов. Обработка символов в направлении ввода и в направлении вывода асимметрична, что видно из наличия двух символьных списков для ввода и одного - для вывода. Строковый интерфейс выводит данные из пространства задачи, обрабатывает их и помещает их в список для хранения выводных данных. Для симметрии следовало бы иметь здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм terminal_read Ё Ё { Ё Ё если (в каноническом символьном списке отсутствуют дан- Ё Ё ные) Ё Ё { Ё Ё выполнить (пока в списке для неструктурированных Ё Ё вводных данных отсутствует информация) Ё Ё { Ё Ё если (терминал открыт с параметром "no delay" Ё Ё (без задержки)) Ё Ё возвратить управление; Ё Ё если (терминал в режиме без обработки с использо-Ё Ё ванием таймера и таймер не активен) Ё Ё предпринять действия к активизации таймера Ё Ё (таблица ответных сигналов); Ё Ё приостановиться (до поступления данных с термина-Ё Ё ла); Ё Ё } Ё Ё Ё Ё /* в списке для неструктурированных вводных данных Ё Ё есть информация */ Ё Ё если (терминал в режиме без обработки) Ё Ё скопировать все данные из списка для неструктури-Ё Ё рованных вводных данных в канонический список; Ё Ё в противном случае /* терминал в каноническом ре- Ё Ё жиме */ Ё Ё { Ё Ё выполнить (пока в списке для неструктурированных Ё Ё вводных данных есть символы) Ё Ё { Ё Ё копировать по одному символу из списка для Ё Ё неструктурированных вводных данных в кано- Ё Ё нический список: Ё Ё выполнить обработку символов стирания и уда-Ё Ё ления; Ё Ё если (символ - "возврат каретки" или "конец Ё Ё файла") Ё Ё прерваться; /* выход из цикла */ Ё Ё } Ё Ё } Ё Ё } Ё Ё Ё Ё выполнить (пока в каноническом списке еще есть символы Ё Ё и не исчерпано количество символов, указанное в вызове Ё Ё функции read) Ё Ё копировать символы из символьных блоков каноническогоЁ Ё списка в адресное пространство задачи; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 10.15. Алгоритм чтения с терминала только один список для вводных данных. Однако, в таком случае потребовалось бы использование программы обработки прерываний для интерпретации символов стирания и удаления, что сделало бы проце- дуру более сложной и длительной и запретило бы возникновение дру- гих прерываний на все критическое время. Использование двух сим- вольных списков для ввода подразумевает, что программа обработки прерываний может просто сбросить символы в список для неструкту- рированных вводных данных и возобновить выполнение процесса, осу- ществляющего чтение, который собственно и возьмет на себя работу по интерпретации вводных данных. При этом программа обработки прерываний немедленно помещает введенные символы в список для хранения выводных данных, так что пользователь испытывает лишь минимальную задержку при просмотре введенных символов на термина- ле. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё 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. Конкуренция за данные, вводимые с терминала На Рисунке 10.16 приведена программа, в которой родительский процесс порождает несколько процессов, осуществляющих чтение из файла стандартного ввода, конкурируя за получение данных, вводи- мых с терминала. Ввод с терминала обычно осуществляется слишком медленно для того, чтобы удовлетворить все процессы, ведущие чте- ние, поэтому процессы большую часть времени находятся в приоста- новленном состоянии в соответствии с алгоритмом terminal_read, ожидая ввода данных. Когда пользователь вводит строку данных, программа обработки прерываний от терминала возобновляет выполне- ние всех процессов, ведущих чтение; поскольку они были приоста- новлены с одним и тем же уровнем приоритета, они выбираются для запуска с одинаковым уровнем приоритета. Пользователь не в состо- янии предугадать, какой из процессов выполняется и считывает строку данных; успешно созданный процесс печатает значение пере- менной i в момент его создания. Все другие процессы в конце кон- цов будут запущены, но вполне возможно, что они не обнаружат введенной информации в списках для хранения вводных данных и их выполнение снова будет приостановлено. Вся процедура повторяется для каждой введенной строки; нельзя дать гарантию, что ни один из процессов не захватит все введенные данные. Одновременному чтению с терминала несколькими процессами при- суща неоднозначность, но ядро справляется с ситуацией наилучшим образом. С другой стороны, ядро обязано позволять процессам од- новременно считывать данные с терминала, иначе порожденные ко- мандным процессором shell процессы, читающие из стандартного вво- да, никогда не будут работать, поскольку shell тоже обращается к стандартному вводу. Короче говоря, процессы должны синхронизиро- вать свои обращения к терминалу на пользовательском уровне. Когда пользователь вводит символ "конец файла" (Ctrl-d в ASCII), строковый интерфейс передает функции read введенную стро- ку до символа конца файла, но не включая его. Он не передает дан- ные (код возврата 0) функции read, если в символьном списке встретился только символ "конец файла"; вызывающий процесс сам распознает, что обнаружен конец файла и больше не следует считы- вать данные с терминала. Если еще раз обратиться к примерам прог- рамм по shell'у, приведенным в главе 7, можно отметить, что цикл работы shell'а завершается, когда пользователь нажимает : функция read возвращает 0 и производится выход из shell'а. В этом разделе рассмотрена работа терминалов ввода-вывода, которые передают данные на машину по одному символу за одну опе- рацию, в точности как пользователь их вводит с клавиатуры. Интел- лектуальные терминалы подготавливают свой вводной поток на внеш- нем устройстве, освобождая центральный процессор для другой работы. Структура драйверов для таких терминалов походит на структуру драйверов для терминалов ввода-вывода, несмотря на то, что функции строкового интерфейса различаются в зависимости от возможностей внешних устройств. 10.3.3 Терминальный драйвер в режиме без обработки символов Пользователи устанавливают параметры терминала, такие как символы стирания и удаления, и извлекают значения текущих устано- вок с помощью системной функции ioctl. Сходным образом они уста- навливают необходимость эхо-сопровождения ввода данных с термина- ла, задают скорость передачи информации в бодах, заполняют очереди символов ввода и вывода или вручную запускают и останав- ливают выводной поток символов. В информационной структуре терми- нального драйвера хранятся различные управляющие установки (см. [SVID 85], стр.281), и строковый интерфейс получает параметры функции ioctl и устанавливает или считывает значения соответству- ющих полей структуры данных. Когда процесс устанавливает значения параметров терминала, он делает это для всех процессов, использу- ющих терминал. Установки терминала не сбрасываются автоматически при выходе из процесса, сделавшего изменения в установках. Процессы могут также перевести терминал в режим без обработки символов, в котором строковый интерфейс передает символы в точном соответствии с тем, как пользователь ввел их: обработка вводного потока полностью отсутствует. Однако, ядро должно знать, когда выполнить вызванную пользователем системную функцию read, пос- кольку символ возврата каретки трактуется как обычный введенный символ. Оно выполняет функцию read после того, как с терминала будет введено минимальное число символов или по прохождении фик- сированного промежутка времени от момента получения с терминала любого набора символов. В последнем случае ядро хронометрирует ввод символов с терминала, помещая записи в таблицу ответных сиг- налов (глава 8). Оба критерия (минимальное число символов и фик- сированный промежуток времени) задаются в вызове функции ioctl. Когда соответствующие критерии удовлетворены, программа обработки прерываний строкового интерфейса возобновляет выполнение всех приостановленных процессов. Драйвер пересылает все символы из списка для хранения неструктурированных вводных данных в канони- ческий список и выполняет запрос процесса на чтение, следуя тому же самому алгоритму, что и в случае работы в каноническом режиме. Режим без обработки символов особенно важен в экранно-ориентиро- ванных приложениях, таких как экранный редактор vi, многие из ко- манд которого не заканчиваются символом возврата каретки. Напри- мер, команда dw удаляет слово в текущей позиции курсора. На Рисунке 10.17 приведена программа, использующая функцию ioctl для сохранения текущих установок терминала для файла с дескриптором 0, что соответствует значению дескриптора файла стандартного ввода. Функция ioctl с командой TCGETA приказывает здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #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; Ё Ё 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-символьных блоков драйверу извлечь установки и сохранить их в структуре с именем savetty в адресном пространстве задачи. Эта команда часто исполь- зуется для того, чтобы определить, является ли файл терминалом или нет, поскольку она ничего не изменяет в системе: если она за- вершается неудачно, процессы предполагают, что файл не является терминалом. Здесь же, процесс вторично вызывает функцию ioctl для того, чтобы перевести терминал в режим без обработки: он отключа- ет эхо-сопровождение ввода символов и готовится к выполнению опе- раций чтения с терминала по получении с терминала 5 символов, как минимум, или по прохождении 10 секунд с момента ввода первой пор- ции символов. Когда процесс получает сигнал о прерывании, он сбрасывает первоначальные параметры терминала и завершается. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #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. Опрос терминала 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" (прерывания), стирания или выхода, программа обработки прерываний загружает строковый интерфейс, который посылает соот- ветствующий сигнал всем процессам в группе. Подобно этому, когда пользователь "зависает", программа обработки прерываний от терми- нала получает информацию о "зависании" от аппаратуры, и строковый интерфейс посылает соответствующий сигнал всем процессам в груп- пе. Таким образом, все процессы, запущенные с конкретного терми- нала, получают сигнал о "зависании"; реакцией по умолчанию для большинства процессов будет выход из программы по получении сиг- нала; это похоже на то, как при завершении работы пользователя с терминалом из системы удаляются побочные процессы. После посылки сигнала о "зависании" программа обработки прерываний от терминала разъединяет терминал с группой процессов, чтобы процессы из этой группы не могли больше получать сигналы, возникающие на термина- ле. 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 (регистрации в системе), которая требует от пользователей, чтобы они идентифицировали себя указанием регистрационного имени и па- роля. Если пользователь зарегистрировался успешно, программа login наконец запускает командный процессор shell и пользователь приступает к работе. Этот вызов shell'а именуется "login shell" (регистрационный shell, регистрационный интерпретатор команд). Процесс, связанный с shell'ом, имеет тот же идентификатор, что и начальный getty-процесс, поэтому login shell является процессом, возглавляющим группу процессов. Если пользователь не смог успешно зарегистрироваться, программа регистрации завершается через опре- деленный промежуток времени, закрывая открытую терминальную ли- нию, а процесс начальной загрузки порождает для этой линии следу- ющий getty-процесс. Процесс начальной загрузки делает паузу до получения сигнала об окончании порожденного ранее процесса. После возобновления работы он выясняет, был ли прекративший существова- ние процесс регистрационным shell'ом и если это так, порождает еще один getty-процесс, открывающий терминал, вместо прекративше- го существование. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм login /* процедура регистрации */ Ё Ё { Ё Ё исполняется getty-процесс: Ё Ё установить группу процессов (вызов функции setpgrp); Ё Ё открыть терминальную линию; /* приостанов до завершенияЁ Ё открытия */ Ё Ё если (открытие завершилось успешно) Ё Ё { Ё Ё исполнить программу регистрации: Ё Ё запросить имя пользователя; Ё Ё отключить эхо-сопровождение, запросить пароль; Ё Ё если (регистрация прошла успешно) Ё Ё /* найден соответствующий пароль в /etc/passwd */ Ё Ё { Ё Ё перевести терминал в канонический режим (ioctl);Ё Ё исполнить shell; Ё Ё } Ё Ё в противном случае Ё Ё считать количество попыток регистрации, пытатьсяЁ Ё зарегистрироваться снова до достижения опреде- Ё Ё ленной точки; Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 10.19. Алгоритм регистрации 10.4 ПОТОКИ Схема реализации драйверов устройств, хотя и отвечает зало- женным требованиям, страдает некоторыми недостатками, которые с годами стали заметнее. Разные драйверы имеют тенденцию дублиро- вать свои функции, в частности драйверы, которые реализуют сете- вые протоколы и которые обычно включают в себя секцию управления устройством и секцию протокола. Несмотря на то, что секция прото- кола должна быть общей для всех сетевых устройств, на практике это не так, поскольку ядро не имеет адекватных механизмов для об- щего использования. Например, символьные списки могли бы быть по- лезными благодаря своим возможностям в буферизации, но они требу- ют больших затрат ресурсов на посимвольную обработку. Попытки обойти этот механизм, чтобы повысить производительность системы, привели к нарушению модульности подсистемы управления вводом-вы- водом. Отсутствие общности на уровне драйверов распространяется вплоть до уровня команд пользователя, на котором несколько команд могут выполнять общие логические функции, но различными средства- ми. Еще один недостаток построения драйверов заключается в том, что сетевые протоколы требуют использования средства, подобного строковому интерфейсу, в котором каждая дисциплина реализует одну из частей протокола и составные части соединяются гибким образом. Однако, соединить традиционные строковые интерфейсы довольно трудно. Ричи недавно разработал схему, получившую название "потоки" (streams), для повышения модульности и гибкости подсистемы управ- ления вводом-выводом. Нижеследующее описание основывается на его работе [Ritchie 84b], хотя реализация этой схемы в версии V слег- ка отличается. Поток представляет собой полнодуплексную связь между процессом и драйвером устройства. Он состоит из совокупнос- ти линейно связанных между собой пар очередей, каждая из которых (пара) включает одну очередь для ввода и другую - для вывода. Когда процесс записывает данные в поток, ядро посылает данные в очереди для вывода; когда драйвер устройства получает входные данные, он пересылает их в очереди для ввода к процессу, произво- дящему чтение. Очереди обмениваются сообщениями с соседними оче- редями, используя четко определенный интерфейс. Каждая пара оче- редей связана с одним из модулей ядра, таким как драйвер, строковый интерфейс или протокол, и модули ядра работают с данны- ми, прошедшими через соответствующие очереди. Каждая очередь представляет собой структуру данных, состоящую из следующих элементов: * процедуры открытия, вызываемой во время выполнения системной функции open * процедуры закрытия, вызываемой во время выполнения системной функции close * процедуры "вывода", вызываемой для передачи сообщения в оче- редь * процедуры "обслуживания", вызываемой, когда очередь заплани- рована к исполнению * указателя на следующую очередь в потоке * указателя на список сообщений, ожидающих обслуживания * указателя на внутреннюю структуру данных, с помощью которой поддерживается рабочее состояние очереди * флагов, а также верхней и нижней отметок, используемых для управления потоками данных, диспетчеризации и поддержания ра- бочего состояния очереди. Ядро выделяет пары очередей, соседствующие в памяти; следова- тельно, очередь легко может отыскать своего партнера по паре. здддддддддд© Ё Индекс Ё зддддддддддддддддддддддд╢ файла Ё Ё ЁустройстваЁ v юдддддддддды зддддддддддддбддддддддддд© Заголовок Ё Очередь Ё Очередь Ё потока Ё для вывода Ё для ввода Ё юддддддбдддддаддддддддддды Ё ^ Ё Ё v Ё зддддддддддддбдддддаддддд© Драйвер Ё Очередь Ё Очередь ЁЫЫЫЫЫЫЫ пара очередей Ё для вывода Ё для ввода Ё юддддддддддддаддддддддддды Рисунок 10.20. Поток после открытия Устройство с потоковым драйвером является устройством посим- вольного ввода-вывода; оно имеет в таблице ключей устройств соответствующего типа специальное поле, которое указывает на структуру инициализации потока, содержащую адреса процедур, а также верхнюю и нижнюю отметки, упомянутые выше. Когда ядро вы- полняет системную функцию open и обнаруживает, что файл устройс- тва имеет тип "специальный символьный", оно проверяет наличие но- вого поля в таблице ключей устройств посимвольного ввода-вывода. Если в таблице отсутствует соответствующая точка входа, то драй- вер не является потоковым, и ядро выполняет процедуру, обычную для устройств посимвольного ввода-вывода. Однако, при первом же открытии потокового драйвера ядро выделяет две пары очередей - одну для заголовка потока и другую для драйвера. У всех открытых потоков модуль заголовка имеет идентичную структуру: он содержит общую процедуру "вывода" и общую процедуру "обслуживания" и имеет интерфейс с модулями ядра более высокого уровня, выполняющими функции read, write и ioctl. Ядро инициализирует структуру очере- дей драйвера, назначая значения указателям каждой очереди и копи- руя адреса процедур драйвера из структуры инициализации драйвера, и запускает процедуру открытия. Процедура открытия драйвера вы- полняет обычную инициализацию, но при этом сохраняет информацию, необходимую для повторного обращения к ассоциированной с этой процедурой очереди. Наконец, ядро отводит специальный указатель в копии индекса в памяти для ссылки на заголовок потока (Рисунок 10.20). Когда еще один процесс открывает устройство, ядро обнару- живает назначенный ранее поток с помощью этого указателя и запус- кает процедуру открытия для всех модулей потока. Модули поддерживают связь со своими соседями по потоку путем передачи сообщений. Сообщение состоит из списка заголовков бло- ков, содержащих информацию сообщения; каждый заголовок блока со- держит ссылку на место расположения начала и конца информации блока. Существует два типа сообщений - управляющее и информацион- ное, которые определяются указателями типа в заголовке сообщения. Управляющие сообщения могут быть результатом выполнения системной функции ioctl или результатом особых условий, таких как зависание терминала, а информационные сообщения могут возникать в результа- те выполнения системной функции write или в результате поступле- ния данных от устройства. Сообщение 1 Сообщение 2 Сообщение 3 зддддддддд© зддддддддд© зддддддддд© Ё Блок цддддддддд>Ё цдддддддд>Ё Ё юддддбдддды юддддддддды юддддбдддды Ё Ё v v зддддддддд© зддддддддд© Ё Ё Ё Ё юддддбдддды юддддддддды Ё v зддддддддд© Ё Ё юддддддддды Рисунок 10.21. Сообщения в потоках Когда процесс производит запись в поток, ядро копирует данные из адресного пространства задачи в блоки сообщения, которые выде- ляются модулем заголовка потока. Модуль заголовка потока запуска- ет процедуру "вывода" для модуля следующей очереди, которая обра- батывает сообщение, незамедлительно передает его в следующую очередь или ставит в эту же очередь для последующей обработки. В последнем случае модуль связывает заголовки блоков сообщения в список с указателями, формируя двунаправленный список (Рисунок 10.21). Затем он устанавливает в структуре данных очереди флаг, показывая тем самым, что имеются данные для обработки, и планиру- ет собственное обслуживание. Модуль включает очередь в список очередей, требующих обслуживания и запускает механизм диспетчери- зации; планировщик (диспетчер) вызывает процедуры обслуживания для каждой очереди в списке. Ядро может планировать обслуживание модулей по программному прерыванию, подобно тому, как оно вызыва- ет функции в таблице ответных сигналов (см. главу 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); При том, что модуль строкового интерфейса выполняет обычные функции по управлению терминалом, соответствующее ему устройство может быть средством сетевой связи вместо того, чтобы обеспечи- вать связь с одним-единственным терминалом. Модуль строкового ин- терфейса работает одинаково, независимо от того, какого типа мо- дуль расположен ниже него. Этот пример наглядно демонстрирует повышение гибкости вследствие соединения модулей ядра. 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 читает данные из линии физичес- кого терминала и ждет объявления об управляющих событиях, таких как создание нового окна, переключение управления на другое окно, удаление окна и т.п. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё /* предположим, что дескрипторы файлов 0 и 1 уже относятся к Ё Ё физическому терминалу */ Ё Ё для(;;) /* цикл */ Ё Ё { Ё Ё выбрать(ввод); /* ждать ввода из какой-либо линии */ Ё Ё прочитать данные, введенные из линии; Ё Ё переключить(линию с вводимыми данными) Ё Ё { Ё Ё если выбран физический терминал: /* данные вводятся по ли- Ё Ё нии физического терми- Ё Ё нала */ Ё Ё если(считана управляющая команда) /* например, создание Ё Ё нового окна */ Ё Ё { Ё Ё открыть свободный псевдотерминал; Ё Ё пойти по ветви нового процесса: Ё Ё если(процесс родительский) Ё Ё { Ё Ё выдвинуть интерфейс сообщений в сторону mpx; Ё Ё продолжить; /* возврат в цикл "для" */ Ё Ё } Ё Ё /* процесс-потомок */ Ё Ё закрыть ненужные дескрипторы файлов; Ё Ё открыть другой псевдотерминал из пары, выбрать stdin, Ё Ё stdout, stderr; Ё Ё выдвинуть строковый интерфейс терминала; Ё Ё запустить shell; /* подобно виртуальному терминалу */Ё Ё } Ё Ё /* "обычные" данные, появившиеся через виртуальный терминал */ Ё Ё демультиплексировать считывание данных с физического тер-Ё Ё минала, снять заголовки и вести запись на соответствую- Ё Ё щий псевдотерминал; Ё Ё продолжить; /* возврат в цикл "для" */ Ё Ё Ё Ё если выбран логический терминал: /* виртуальный терминал Ё Ё связан с окном */ Ё Ё закодировать заголовок, указывающий назначение информацииЁ Ё окна; Ё Ё переписать заголовок и информацию на физический терминал;Ё Ё продолжить; /* возврат в цикл "для" */ Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 10.24. Псевдопрограмма мультиплексирования окон Когда mpx получает уведомление о том, что пользователю нужно создать новое окно, он создает процесс, управляющий новым окном, и поддерживает связь с ним через псевдотерминал. Псевдотерминал - это программное устройство, работающее по принципу пары: выходные данные, направляемые к одной составляющей пары, посылаются на вход другой составляющей; входные данные посылаются тому модулю потока, который расположен выше по течению. Для того, чтобы отк- рыть окно (Рисунок 10.24), mpx назначает псевдотерминальную пару и открывает одну из составляющих пары, направляя поток к ней (от- крытие драйвера служит гарантией того, что псевдотерминальная па- ра не была выбрана раньше). Mpx ветвится и новый процесс открыва- ет другую составляющую псевдотерминальной пары. Mpx выдвигает модуль управления сообщениями в псевдотерминальный поток, чтобы преобразовывать управляющие сообщения в информационные (об этом в следующем параграфе), а порожденный процесс помещает в псевдотер- минальный поток модуль строкового интерфейса перед запуском shell'а. Этот shell теперь выполняется на виртуальном терминале; для пользователя виртуальный терминал неотличим от физического. Процесс mpx является мультиплексором, направляющим вывод дан- ных с виртуальных терминалов на физический терминал и демультип- лексирующим ввод данных с физического терминала на подходящий виртуальный. Mpx ждет поступления данных по любой из линий, ис- пользуя системную функцию select. Когда данные поступают от физи- ческого терминала, mpx решает вопрос, являются ли поступившие данные управляющим сообщением, извещающим о необходимости созда- ния нового окна или удаления старого, или же это информационное сообщение, которое необходимо разослать процессам, считывающим информацию с виртуального терминала. В последнем случае данные имеют заголовок, идентифицирующий тот виртуальный терминал, к ко- торому они относятся; mpx стирает заголовок с сообщения и перепи- сывает данные в соответствующий псевдотерминальный поток. Драйвер псевдотерминала отправляет данные через строковый интерфейс тер- минала процессам, осуществляющим чтение. Обратная процедура имеет место, когда процесс ведет запись на виртуальный терминал; mpx присоединяет заголовок к данным, информируя физический терминал, для вывода в какое из окон предназначены эти данные. Если процесс вызывает функцию ioctl с виртуального терминала, строковый интерфейс терминала задает необходимые установки терми- нала для его виртуальной линии; для каждого из виртуальных терми- налов установки могут быть различными. Однако, на физический тер- минал должна быть послана и кое-какая информация, зависящая от типа устройства. Модуль управления сообщениями преобразует управ- ляющие сообщения, генерируемые функцией ioctl, в информационные сообщения, предназначенные для чтения и записи их процессом mpx, и эти сообщения передаются на физическое устройство. 10.4.2 Анализ потоков Ричи упоминает о том, что им была предпринята попытка созда- ния потоков только с процедурами "вывода" или только с процедура- ми обслуживания. Однако, процедура обслуживания необходима для управления потоками данных, так как модули должны иногда ставить данные в очередь, если соседние модули на время закрыты для прие- ма данных. Процедура "вывода" так же необходима, поскольку данные должны иногда доставляться в соседние модули незамедлительно. Например, строковому интерфейсу терминала нужно вести эхо-сопро- вождение ввода данных на терминале в темпе с процессом. Системная функция write могла бы запускать процедуру "вывода" для следующей очереди непосредственно, та, в свою очередь, вызывала бы процеду- ру "вывода" для следующей очереди и так далее, не нуждаясь в ме- ханизме диспетчеризации. Процесс приостановился бы в случае пере- полнения очередей для вывода. Однако, со стороны ввода модули не могут приостанавливаться, поскольку их выполнение вызывается программой обработки прерываний, иначе был бы приостановлен со- вершенно безобидный процесс. Связь между модулями не должна быть симметричной в направлениях ввода и вывода, хотя это и делает схему менее изящной. Также было бы желательно реализовать каждый модуль в виде от- дельного процесса, но использование большого количества модулей привело бы к переполнению таблицы процессов. Модули наделяются специальным механизмом диспетчеризации - программным прерыванием, независимым от обычного планировщика процессов. По этой причине модули не могут приостанавливать свое выполнение, так как они приостанавливали бы тем самым произвольный процесс (тот, который прерван). Модули должны хранить внутри себя информацию о своем состоянии, что делает лежащие в их основе программы более гро- моздкими, чем если бы приостановка выполнения была разрешена. В реализации потоков можно выделить несколько отклонений или несоответствий: * Учет ресурсов процесса в потоках затрудняется, поскольку мо- дулям необязательно выполняться в контексте процесса, исполь- зующего поток. Ошибочно предполагать, что все процессы одина- ково используют модули потоков, поскольку одним процессам может потребоваться использование сложных сетевых протоколов, тогда как другие могут использовать простые строковые интер- фейсы. * Пользователи имеют возможность переводить терминальный драй- вер в режим без обработки, в котором функция read возвращает управление через короткий промежуток времени в случае отсутс- твия данных (например, если newtty.c_cc[VMIN] = 0 на Рисунке 10.17). Эту особенность сложно реализовать в потоковой среде без подключения специальной программы на уровне заголовка по- тока. * Потоки выступают средствами линейной связи и не могут позво- лить производить с легкостью мультиплексирование на уровне ядра. В примере использования окон, рассмотренном в предыду- щем разделе, выполнялось мультиплексирование на уровне поль- зовательского процесса. Несмотря на эти несоответствия, с потоками связываются боль- шие надежды в совершенствовании разработки модулей драйвера. 10.5 ВЫВОДЫ Данная глава представляет собой обзор драйверов устройств в системе UNIX. Устройства могут быть либо блочного, либо символь- ного типа; интерфейс между устройствами и остальной частью ядра определяется типом устройств. Интерфейсом для устройств блочного типа выступает таблица ключей устройств ввода-вывода блоками, состоящая из точек входа, соответствующих процедурам открытия и закрытия устройств и стратегической процедуре. Стратегическая процедура управляет передачей данных от и к устройству блочного типа. Интерфейсом для устройств символьного типа выступает табли- ца ключей устройств посимвольного ввода-вывода, которая состоит из точек входа, соответствующих процедурам открытия и закрытия устройства, чтения, записи и процедуре ioctl. Системная функция ioctl использует при обращении к устройствам символьного типа свой собственный интерфейс, который позволяет осуществлять пере- дачу управляющей информации между процессами и устройствами. По получении прерывания от устройства ядро вызывает программу обра- ботки соответствующего прерывания, опираясь на информацию, храня- щуюся в таблице векторов прерываний, и на параметры, сообщенные устройством, от которого поступило прерывание. Дисковые драйверы превращают номера логических блоков, ис- пользуемые файловой системой, в физические адреса на диске. Блоч- ный интерфейс дает возможность ядру буферизовать данные. Взаимо- действие без обработки ускоряет ввод-вывод на диск, но игнорирует буферный кеш, увеличивая тем самым шансы разрушить файловую сис- тему. Терминальные драйверы осуществляют непосредственное взаимо- действие с пользователями. Ядро связывает с каждым терминалом три символьных списка, один для неструктурированного ввода с клавиа- туры, один для ввода с обработкой символов стирания, удаления и возврата каретки и один для вывода. Системная функция ioctl дает процессам возможность следить за тем, как ядро обрабатывает вво- димые данные, переводя терминал в канонический режим или устанав- ливая значения различных параметров для режима без обработки сим- волов. Getty-процесс открывает терминальные линии и ждет связи: он формирует группу процессов во главе с регистрационным shell'ом, инициализирует с помощью функции ioctl параметры терми- нала и обращается к пользователю с предложением зарегистрировать- ся. Установленный таким образом операторский терминал посылает процессам в группе сигналы в ответ на возникновение таких собы- тий, как "зависание" пользователя или нажатие им клавиши прерыва- ния. Потоки выступают средством повышения модульности построения драйверов устройств и протоколов. Поток - это полнодуплексная связь между процессами и драйверами устройств, которая может включать в себя строковые интерфейсы и протоколы для промежуточ- ной обработки данных. Модули потоков характеризуются четко опре- деленным взаимодействием и гибкостью, позволяющей использовать их в сочетании с другими модулями. Эта гибкость имеет особое значе- ние для сетевых протоколов и драйверов. 10.6 УПРАЖНЕНИЯ *1. Предположим, что в системе имеются два файла устройств с од- ними и теми же старшим и младшим номерами, при том, что оба устройства - символьного типа. Если два процесса желают од- новременно открыть физическое устройство, не будет никакой разницы, открывают ли они один и тот же файл устройства или же разные файлы. Что произойдет, когда они станут закрывать устройство ? *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 & Обоснуйте ответ. 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 ста- новится нелегко обнаружить в таблице соответствующие поля. ВЗАИМОДЕЙСТВИЕ ПРОЦЕССОВ Наличие механизмов взаимодействия дает произвольным процессам возможность осуществлять обмен данными и синхронизировать свое выполнение с другими процессами. Мы уже рассмотрели несколько форм взаимодействия процессов, такие как канальная связь, исполь- зование поименованных каналов и посылка сигналов. Каналы (непои- менованные) имеют недостаток, связанный с тем, что они известны только потомкам процесса, вызвавшего системную функцию pipe: не имеющие родственных связей процессы не могут взаимодействовать между собой с помощью непоименованных каналов. Несмотря на то, что поименованные каналы позволяют взаимодействовать между собой процессам, не имеющим родственных связей, они не могут использо- ваться ни в сети (см. главу 13), ни в организации множественных связей между различными группами взаимодействующих процессов: по- именованный канал не поддается такому мультиплексированию, при котором у каждой пары взаимодействующих процессов имелся бы свой выделенный канал. Произвольные процессы могут также связываться между собой благодаря посылке сигналов с помощью системной функ- ции kill, однако такое "сообщение" состоит из одного только номе- ра сигнала. В данной главе описываются другие формы взаимодействия про- цессов. В начале речь идет о трассировке процессов, о том, каким образом один процесс следит за ходом выполнения другого процесса, затем рассматривается пакет IPC: сообщения, разделяемая память и семафоры. Делается обзор традиционных методов сетевого взаимо- действия процессов, выполняющихся на разных машинах, и, наконец, дается представление о "гнездах", применяющихся в системе BSD. Вопросы сетевого взаимодействия, имеющие специальный характер, такие как протоколы, адресация и др., не рассматриваются, пос- кольку они выходят за рамки настоящей работы. 11.1 ТРАССИРОВКА ПРОЦЕССОВ В системе UNIX имеется простейшая форма взаимодействия про- цессов, используемая в целях отладки, - трассировка процессов. Процесс-отладчик, например sdb, порождает трассируемый процесс и управляет его выполнением с помощью системной функции ptrace, расставляя и сбрасывая контрольные точки, считывая и записывая данные в его виртуальное адресное пространство. Трассировка про- цессов, таким образом, включает в себя синхронизацию выполнения процесса-отладчика и трассируемого процесса и управление выполне- нием последнего. зддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё if ((pid = fork()) == 0) Ё Ё { Ё Ё /* потомок - трассируемый процесс */ Ё Ё ptrace(0,0,0,0); Ё Ё exec("имя трассируемого процесса"); Ё Ё } Ё Ё /* продолжение выполнения процесса-отладчика */ Ё Ё for (;;) Ё Ё { Ё Ё wait((int *) 0); Ё Ё read(входная информация для трассировки команд) Ё Ё ptrace(cmd,pid,...); Ё Ё if (условие завершения трассировки) Ё Ё break; Ё Ё } Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.1. Структура процесса отладки Псевдопрограмма, представленная на Рисунке 11.1, имеет типич- ную структуру отладочной программы. Отладчик порождает новый про- цесс, запускающий системную функцию ptrace, в результате чего в соответствующей процессу-потомку записи таблицы процессов ядро устанавливает бит трассировки. Процесс-потомок предназначен для запуска (exec) трассируемой программы. Например, если пользова- тель ведет отладку программы a.out, процесс-потомок запускает файл с тем же именем. Ядро отрабатывает функцию exec обычным по- рядком, но в финале замечает, что бит трассировки установлен, и посылает процессу-потомку сигнал прерывания. На выходе из функции exec, как и на выходе из любой другой функции, ядро проверяет на- личие сигналов, обнаруживает только что посланный сигнал прерыва- ния и исполняет программу трассировки процесса как особый случай обработки сигналов. Заметив установку бита трассировки, про- цесс-потомок выводит своего родителя из состояния приостанова, в котором последний находится вследствие исполнения функции wait, сам переходит в состояние трассировки, подобное состоянию приос- танова (но не показанное на диаграмме состояний процесса, см. Ри- сунок 6.1), и выполняет переключение контекста. Тем временем в обычной ситуации процесс-родитель (отладчик) переходит на пользовательский уровень, ожидая получения известия от трассируемого процесса. Когда соответствующее известие процес- сом-родителем будет получено, он выйдет из состояния ожидания (wait), прочитает (read) введенные пользователем команды и прев- ратит их в серию обращений к функции ptrace, управляющих трасси- ровкой процесса-потомка. Синтаксис вызова системной функции ptrace: ptrace(cmd,pid,addr,data); где в качестве cmd указываются различные команды, например, чте- ния данных, записи данных, возобновления выполнения и т.п., pid - идентификатор трассируемого процесса, addr - виртуальный адрес ячейки в трассируемом процессе, где будет производиться чтение или запись, data - целое значение, предназначенное для записи. Во время исполнения системной функции ptrace ядро проверяет, имеется ли у отладчика потомок с идентификатором pid и находится ли этот потомок в состоянии трассировки, после чего заводит глобальную структуру данных, предназначенную для передачи данных между двумя процессами. Чтобы другие процессы, выполняющие трассировку, не могли затереть содержимое этой структуры, она блокируется ядром, ядро записывает в нее параметры cmd, addr и data, возобновляет процесс-потомок, переводит его в состояние "готовности к выполне- нию" и приостанавливается до получения от него ответа. Когда про- цесс-потомок продолжит свое выполнение (в режиме ядра), он исполнит соответствующую (трассируемую) команду, запишет резуль- тат в глобальную структуру и "разбудит" отладчика. В зависимости от типа команды потомок может вновь перейти в состояние трасси- ровки и ожидать поступления новой команды или же выйти из цикла обработки сигналов и продолжить свое выполнение. При возобновле- нии работы отладчика ядро запоминает значение, возвращенное трас- сируемым процессом, снимает с глобальной структуры блокировку и возвращает управление пользователю. Если в момент перехода процесса-потомка в состояние трасси- ровки отладчик не находится в состоянии приостанова (wait), он не обнаружит потомка, пока не обратится к функции wait, после че- го немедленно выйдет из функции и продолжит работу по вышеописан- ному плану. здддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё int data[32]; Ё Ё main() Ё Ё { Ё Ё int i; Ё Ё for (i = 0; i < 32; i++) Ё Ё printf("data[%d] = %d\n@,i,data[i]); Ё Ё printf("ptrace data addr Ox%x\n",data); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.2. Программа trace (трассируемый процесс) Рассмотрим две программы, приведенные на Рисунках 11.2 и 11.3 и именуемые trace и debug, соответственно. При запуске программы trace с терминала массив data будет содержать нулевые значения; процесс выводит адрес массива и завершает работу. При запуске программы debug с передачей ей в качестве параметра значения, вы- веденного программой trace, происходит следующее: программа запо- минает значение параметра в переменной addr, создает новый про- цесс, с помощью функции ptrace подготавливающий себя к трассиров- ке, и запускает программу trace. На выходе из функции exec ядро посылает процессу-потомку (назовем его тоже trace) сигнал SIGTRAP (сигнал прерывания), процесс trace переходит в состояние трасси- ровки, ожидая поступления команды от программы debug. Если про- цесс, реализующий программу debug, находился в состоянии приоста- нова, связанного с выполнением функции wait, он "пробуждается", обнаруживает наличие порожденного трассируемого процесса и выхо- дит из функции wait. Затем процесс debug вызывает функцию ptrace, записывает значение переменной цикла i в пространство данных про- цесса trace по адресу, содержащемуся в переменной addr, и увели- чивает значение переменной addr; в программе trace переменная addr хранит адрес точки входа в массив data. Последнее обращение процесса debug к функции ptrace вызывает запуск программы trace, и в этот момент массив data содержит значения от 0 до 31. Отлад- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #define TR_SETUP 0 Ё Ё #define TR_WRITE 5 Ё Ё #define TR_RESUME 7 Ё Ё int addr; Ё Ё Ё Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё int i,pid; Ё Ё Ё Ё sscanf(argv[1],"%x",&addr); Ё Ё Ё Ё if ((pid = fork() == 0) Ё Ё { Ё Ё ptrace(TR_SETUP,0,0,0); Ё Ё execl("trace","trace",0); Ё Ё exit(); Ё Ё } Ё Ё for (i = 0; i < 32, i++) Ё Ё { Ё Ё wait((int *) 0); Ё Ё /* записать значение i в пространство процесса с Ё Ё * идентификатором pid по адресу, содержащемуся в Ё Ё * переменной addr */ Ё Ё if (ptrace(TR_WRITE,pid,addr,i) == -1) Ё Ё exit(); Ё Ё addr += sizeof(int); Ё Ё } Ё Ё /* трассируемый процесс возобновляет выполнение */ Ё Ё ptrace(TR_RESUME,pid,1,0); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.3. Программа debug (трассирующий процесс) чики, подобные sdb, имеют доступ к таблице идентификаторов трас- сируемого процесса, из которой они получают информацию об адресах данных, используемых в качестве параметров функции ptrace. Использование функции ptrace для трассировки процессов явля- ется обычным делом, но оно имеет ряд недостатков. * Для того, чтобы произвести передачу порции данных длиною в слово между процессом-отладчиком и трассируемым процессом, ядро должно выполнить четыре переключения контекста: оно пе- реключает контекст во время вызова отладчиком функции ptrace, загружает и выгружает контекст трассируемого процесса и пе- реключает контекст вновь на процесс-отладчик по получении от- вета от трассируемого процесса. Все вышеуказанное необходимо, поскольку у отладчика нет иного способа получить доступ к виртуальному адресному пространству трассируемого процесса, отсюда замедленность протекания процедуры трассировки. * Процесс-отладчик может вести одновременную трассировку нес- кольких процессов-потомков, хотя на практике эта возможность используется редко. Если быть более критичным, следует отме- тить, что отладчик может трассировать только своих ближайших потомков: если трассируемый процесс-потомок вызовет функцию fork, отладчик не будет иметь контроля над порождаемым, вну- чатым для него, процессом, что является серьезным препятстви- ем в отладке многоуровневых программ. Если трассируемый про- цесс вызывает функцию exec, запускаемые образы задач тоже подвергаются трассировке под управлением ранее вызванной функции ptrace, однако отладчик может не знать имени исполня- емого образа, что затрудняет проведение символьной отладки. * Отладчик не может вести трассировку уже выполняющегося про- цесса, если отлаживаемый процесс не вызвал предварительно функцию ptrace, дав тем самым ядру свое согласие на трасси- ровку. Это неудобно, так как в указанном случае выполняющийся процесс придется удалить из системы и перезапустить в режиме трассировки. * Не разрешается трассировать setuid-программы, поскольку это может привести к нарушению защиты данных (ибо в результате выполнения функции ptrace в их адресное пространство произво- дилась бы запись данных) и к выполнению недопустимых дейс- твий. Предположим, например, что setuid-программа запускает файл с именем "privatefile". Умелый пользователь с помощью функции ptrace мог бы заменить имя файла на "/bin/sh", запус- тив на выполнение командный процессор shell (и все программы, исполняемые shell'ом), не имея на то соответствующих полномо- чий. Функция exec игнорирует бит setuid, если процесс подвер- гается трассировке, тем самым адресное пространство setuid-программ защищается от пользовательской записи. Киллиан [Killian 84] описывает другую схему трассировки про- цессов, основанную на переключении файловых систем (см. главу 5). Администратор монтирует файловую систему под именем "/proc"; пользователи идентифицируют процессы с помощью кодов идентифика- ции и трактуют их как файлы, принадлежащие каталогу "/proc". Ядро дает разрешение на открытие файлов, исходя из кода идентификации пользователя процесса и кода идентификации группы. Пользователи могут обращаться к адресному пространству процесса путем чтения (read) файла и устанавливать точки прерываний путем записи (write) в файл. Функция stat сообщает различную статистическую информацию, касающуюся процесса. В данном подходе устранены три недостатка, присущие функции ptrace. Во-первых, эта схема работа- ет быстрее, поскольку процесс-отладчик за одно обращение к ука- занным системным функциям может передавать больше информации, чем при работе с ptrace. Во-вторых, отладчик здесь может вести трас- сировку совершенно произвольных процессов, а не только своих по- томков. Наконец, трассируемый процесс не должен предпринимать предварительно никаких действий по подготовке к трассировке; от- ладчик может трассировать и существующие процессы. Возможность вести отладку setuid-программ, предоставляемая только суперполь- зователю, реализуется как составная часть традиционного механизма защиты файлов. 11.2 ВЗАИМОДЕЙСТВИЕ ПРОЦЕССОВ В ВЕРСИИ V СИСТЕМЫ Пакет IPC (interprocess communication) в версии V системы UNIX включает в себя три механизма. Механизм сообщений дает про- цессам возможность посылать другим процессам потоки сформатиро- ванных данных, механизм разделения памяти позволяет процессам совместно использовать отдельные части виртуального адресного пространства, а семафоры - синхронизировать свое выполнение с вы- полнением параллельных процессов. Несмотря на то, что они реали- зуются в виде отдельных блоков, им присущи общие свойства. * С каждым механизмом связана таблица, в записях которой описы- ваются все его детали. * В каждой записи содержится числовой ключ (key), который представляет собой идентификатор записи, выбранный пользова- телем. * В каждом механизме имеется системная функция типа "get", ис- пользуемая для создания новой или поиска существующей записи; параметрами функции являются идентификатор записи и различные флаги (flag). Ядро ведет поиск записи по ее идентификатору в соответствующей таблице. Процессы могут с помощью флага IPC_PRIVATE гарантировать получение еще неиспользуемой запи- си. С помощью флага IPC_CREAT они могут создать новую запись, если записи с указанным идентификатором нет, а если еще к то- му же установить флаг IPC_EXCL, можно получить уведомление об ошибке в том случае, если запись с таким идентификатором су- ществует. Функция возвращает некий выбранный ядром дескрип- тор, предназначенный для последующего использования в других системных функциях, таким образом, она работает аналогично системным функциям creat и open. * В каждом механизме ядро использует следующую формулу для по- иска по дескриптору указателя на запись в таблице структур данных: указатель = значение дескриптора по модулю от числа записей в таблице Если, например, таблица структур сообщений состоит из 100 за- писей, дескрипторы, связанные с записью номер 1, имеют значе- ния, равные 1, 101, 201 и т.д. Когда процесс удаляет запись, ядро увеличивает значение связанного с ней дескриптора на число записей в таблице: полученный дескриптор станет новым дескриптором этой записи, когда к ней вновь будет произведено обращение при помощи функции типа "get". Процессы, которые будут пытаться обратиться к записи по ее старому дескриптору, потерпят неудачу. Обратимся вновь к предыдущему примеру. Если с записью 1 связан дескриптор, имеющий значение 201, при его удалении ядро назначит записи новый дескриптор, имеющий зна- чение 301. Процессы, пытающиеся обратиться к дескриптору 201, получат ошибку, поскольку этого дескриптора больше нет. В ко- нечном итоге ядро произведет перенумерацию дескрипторов, но пока это произойдет, может пройти значительный промежуток времени. * Каждая запись имеет некую структуру данных, описывающую права доступа к ней и включающую в себя пользовательский и группо- вой коды идентификации, которые имеет процесс, создавший за- пись, а также пользовательский и групповой коды идентифика- ции, установленные системной функцией типа "control" (об этом ниже), и двоичные коды разрешений чтения-записи-исполнения для владельца, группы и прочих пользователей, по аналогии с установкой прав доступа к файлам. * В каждой записи имеется другая информация, описывающая состо- яние записи, в частности, идентификатор последнего из процес- сов, внесших изменения в запись (посылка сообщения, прием со- общения, подключение разделяемой памяти и т.д.), и время последнего обращения или корректировки. * В каждом механизме имеется системная функция типа "control", запрашивающая информацию о состоянии записи, изменяющая эту информацию или удаляющая запись из системы. Когда процесс за- прашивает информацию о состоянии записи, ядро проверяет, име- ет ли процесс разрешение на чтение записи, после чего копиру- ет данные из записи таблицы по адресу, указанному пользователем. При установке значений принадлежащих записи параметров ядро проверяет, совпадают ли между собой пользова- тельский код идентификации процесса и идентификатор пользова- теля (или создателя), указанный в записи, не запущен ли про- цесс под управлением суперпользователя; одного разрешения на запись недостаточно для установки параметров. Ядро копирует сообщенную пользователем информацию в запись таблицы, уста- навливая значения пользовательского и группового кодов иден- тификации, режимы доступа и другие параметры (в зависимости от типа механизма). Ядро не изменяет значения полей, описыва- ющих пользовательский и групповой коды идентификации создате- ля записи, поэтому пользователь, создавший запись, сохраняет управляющие права на нее. Пользователь может удалить запись, либо если он является суперпользователем, либо если идентифи- катор процесса совпадает с любым из идентификаторов, указан- ных в структуре записи. Ядро увеличивает номер дескриптора, чтобы при следующем назначении записи ей был присвоен новый дескриптор. Следовательно, как уже ранее говорилось, если процесс попытается обратиться к записи по старому дескрипто- ру, вызванная им функция получит отказ. 11.2.1 Сообщения С сообщениями работают четыре системных функции: msgget, ко- торая возвращает (и в некоторых случаях создает) дескриптор сооб- щения, определяющий очередь сообщений и используемый другими сис- темными функциями, msgctl, которая устанавливает и возвращает связанные с дескриптором сообщений параметры или удаляет дескрип- торы, msgsnd, которая посылает сообщение, и msgrcv, которая полу- чает сообщение. Синтаксис вызова системной функции msgget: msgqid = msgget(key,flag); где msgqid - возвращаемый функцией дескриптор, а key и flag имеют ту же семантику, что и в системной функции типа "get". Ядро хра- нит сообщения в связном списке (очереди), определяемом значением дескриптора, и использует значение msgqid в качестве указателя на массив заголовков очередей. Кроме вышеуказанных полей, описываю- щих общие для всего механизма права доступа, заголовок очереди содержит следующие поля: * Указатели на первое и последнее сообщение в списке; * Количество сообщений и общий объем информации в списке в бай- тах; * Максимальная емкость списка в байтах; * Идентификаторы процессов, пославших и принявших сообщения последними; * Поля, указывающие время последнего выполнения функций msgsnd, msgrcv и msgctl. Когда пользователь вызывает функцию msgget для того, чтобы создать новый дескриптор, ядро просматривает массив очередей со- общений в поисках существующей очереди с указанным идентификато- ром. Если такой очереди нет, ядро выделяет новую очередь, инициа- лизирует ее и возвращает идентификатор пользователю. В противном случае ядро проверяет наличие необходимых прав доступа и заверша- ет выполнение функции. Для посылки сообщения процесс использует системную функцию msgsnd: msgsnd(msgqid,msg,count,flag); где msgqid - дескриптор очереди сообщений, обычно возвращаемый функцией msgget, msg - указатель на структуру, состоящую из типа в виде назначаемого пользователем целого числа и массива симво- лов, count - размер информационного массива, flag - действие, предпринимаемое ядром в случае переполнения внутреннего буферного пространства. Ядро проверяет (Рисунок 11.4), имеется ли у посылающего сооб- щение процесса разрешения на запись по указанному дескриптору, не выходит ли размер сообщения за установленную системой границу, не содержится ли в очереди слишком большой объем информации, а также является ли тип сообщения положительным целым числом. Если все условия соблюдены, ядро выделяет сообщению место, используя карту сообщений (см. раздел 9.1), и копирует в это место данные из пространства пользователя. К сообщению присоединяется заголовок, после чего оно помещается в конец связного списка заголовков со- общений. В заголовке сообщения записывается тип и размер сообще- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм msgsnd /* послать сообщение */ Ё Ё входная информация: (1) дескриптор очереди сообщений Ё Ё (2) адрес структуры сообщения Ё Ё (3) размер сообщения Ё Ё (4) флаги Ё Ё выходная информация: количество посланных байт Ё Ё { Ё Ё проверить правильность указания дескриптора и наличие Ё Ё соответствующих прав доступа; Ё Ё выполнить пока (для хранения сообщения не будет выделеноЁ Ё место) Ё Ё { Ё Ё если (флаги не разрешают ждать) Ё Ё вернуться; Ё Ё приостановиться (до тех пор, пока место не освобо- Ё Ё дится); Ё Ё } Ё Ё получить заголовок сообщения; Ё Ё считать текст сообщения из пространства задачи в прост- Ё Ё ранство ядра; Ё Ё настроить структуры данных: выстроить очередь заголовковЁ Ё сообщений, установить в заголовке указатель на текст Ё Ё сообщения, заполнить поля, содержащие счетчики, время Ё Ё последнего выполнения операций и идентификатор процес- Ё Ё са; Ё Ё вывести из состояния приостанова все процессы, ожидающиеЁ Ё разрешения считать сообщение из очереди; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.4. Алгоритм посылки сообщения ния, устанавливается указатель на текст сообщения и производится корректировка содержимого различных полей заголовка очереди, со- держащих статистическую информацию (количество сообщений в очере- ди и их суммарный объем в байтах, время последнего выполнения операций и идентификатор процесса, пославшего сообщение). Затем ядро выводит из состояния приостанова все процессы, ожидающие по- полнения очереди сообщений. Если размер очереди в байтах превыша- ет границу допустимости, процесс приостанавливается до тех пор, пока другие сообщения не уйдут из очереди. Однако, если процессу было дано указание не ждать (флаг IPC_NOWAIT), он немедленно возвращает управление с уведомлением об ошибке. На Рисунке 11.5 показана очередь сообщений, состоящая из заголовков сообщений, организованных в связные списки, с указателями на область текста. Рассмотрим программу, представленную на Рисунке 11.6. Процесс вызывает функцию msgget для того, чтобы получить дескриптор для записи с идентификатором MSGKEY. Длина сообщения принимается рав- ной 256 байт, хотя используется только первое поле целого типа, в область текста сообщения копируется идентификатор процесса, типу сообщения присваивается значение 1, после чего вызывается функция msgsnd для посылки сообщения. Мы вернемся к этому примеру позже. Процесс получает сообщения, вызывая функцию msgrcv по следую- щему формату: count = msgrcv(id,msg,maxcount,type,flag); где id - дескриптор сообщения, msg - адрес пользовательской структуры, которая будет содержать полученное сообщение, maxcount - размер структуры msg, type - тип считываемого сообщения, flag - действие, предпринимаемое ядром в том случае, если в очереди со- Заголовки Область очередей текста здддддд© Заголовки сообщений зд>здддддд© Ё Ё здддддд© здддддд© здддддд© Ё Ё Ё Ё ддедддд>Ё цддд>Ё цддд>Ё Ё Ё Ё Ё Ё Ё юдддедды юдддедды юдддедды Ё Ё Ё цдддддд╢ Ё Ё юдддды Ё Ё Ё Ё юдддддддддддЁдддддддддддддддддд>цдддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё цдддддд╢ Ё Ё Ё Ё Ё здддддд© Ё Ё Ё Ё ддедддд>Ё Ё Ё Ё Ё Ё Ё юдддедды Ё Ё Ё цдддддд╢ Ё Ё Ё Ё Ё Ы Ё Ё Ё цдддддд╢ Ё Ы Ё юдддддддддддЁдддддддддддддддддд>цдддддд╢ Ё Ы Ё Ё Ё Ё Ё Ы Ё Ё Ё Ё Ё Ы Ё юдддддддддддддддддд>цдддддд╢ Ё Ы Ё Ё Ё Ё Ы Ё цдддддд╢ Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юдддддды юдддддды Рисунок 11.5. Структуры данных, используемые в организации сообщений общений нет. В переменной count пользователю возвращается число прочитанных байт сообщения. Ядро проверяет (Рисунок 11.7), имеет ли пользователь необхо- димые права доступа к очереди сообщений. Если тип считываемого сообщения имеет нулевое значение, ядро ищет первое по счету сооб- щение в связном списке. Если его размер меньше или равен размеру, указанному пользователем, ядро копирует текст сообщения в пользо- вательскую структуру и соответствующим образом настраивает свои внутренние структуры: уменьшает счетчик сообщений в очереди и суммарный объем информации в байтах, запоминает время получения сообщения и идентификатор процесса-получателя, перестраивает связный список и освобождает место в системном пространстве, где хранился текст сообщения. Если какие-либо процессы, ожидавшие по- лучения сообщения, находились в состоянии приостанова из-за от- сутствия свободного места в списке, ядро выводит их из этого сос- тояния. Если размер сообщения превышает значение maxcount, ука- занное пользователем, ядро посылает системной функции уведомление об ошибке и оставляет сообщение в очереди. Если, тем не менее, процесс игнорирует ограничения на размер (в поле flag установлен бит MSG_NOERROR), ядро обрезает сообщение, возвращает запрошенное количество байт и удаляет сообщение из списка целиком. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё #include Ё Ё #include Ё Ё Ё Ё #define MSGKEY 75 Ё Ё Ё Ё struct msgform { Ё Ё long mtype; Ё Ё char mtext[256]; Ё Ё }; Ё Ё Ё Ё main() Ё Ё { Ё Ё struct msgform msg; Ё Ё int msgid,pid,*pint; Ё Ё Ё Ё msgid = msgget(MSGKEY,0777); Ё Ё Ё Ё pid = getpid(); Ё Ё pint = (int *) msg.mtext; Ё Ё *pint = pid; /* копирование идентификатора Ё Ё * процесса в область текста Ё Ё * сообщения */ Ё Ё msg.mtype = 1; Ё Ё Ё Ё msgsnd(msgid,&msg,sizeof(int),0); Ё Ё msgrcv(msgid,&msg,256,pid,0); /* идентификатор Ё Ё * процесса используется в Ё Ё * качестве типа сообщения */ Ё Ё printf("клиент: получил от процесса с pid %d\n", Ё Ё *pint); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.6. Пользовательский процесс Процесс может получать сообщения определенного типа, если присвоит параметру type соответствующее значение. Если это поло- жительное целое число, функция возвращает первое значение данного типа, если отрицательное, ядро определяет минимальное значение типа сообщений в очереди, и если оно не превышает абсолютное зна- чение параметра type, возвращает процессу первое сообщение этого типа. Например, если очередь состоит из трех сообщений, имеющих тип 3, 1 и 2, соответственно, а пользователь запрашивает сообще- ние с типом -2, ядро возвращает ему сообщение типа 1. Во всех случаях, если условиям запроса не удовлетворяет ни одно из сооб- щений в очереди, ядро переводит процесс в состояние приостанова, разумеется если только в параметре flag не установлен бит IPC_NOWAIT (иначе процесс немедленно выходит из функции). Рассмотрим программы, представленные на Рисунках 11.6 и 11.8. Программа на Рисунке 11.8 осуществляет общее обслуживание запро- сов пользовательских процессов (клиентов). Запросы, например, мо- гут касаться информации, хранящейся в базе данных; обслуживающий процесс (сервер) выступает необходимым посредником при обращении к базе данных, такой порядок облегчает поддержание целостности данных и организацию их защиты от несанкционированного доступа. Обслуживающий процесс создает сообщение путем установки флага IPC _CREAT при выполнении функции msgget и получает все сообщения ти- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм msgrcv /* получение сообщения */ Ё Ё входная информация: (1) дескриптор сообщения Ё Ё (2) адрес массива, в который заноситсяЁ Ё сообщение Ё Ё (3) размер массива Ё Ё (4) тип сообщения в запросе Ё Ё (5) флаги Ё Ё выходная информация: количество байт в полученном сообщенииЁ Ё { Ё Ё проверить права доступа; Ё Ё loop: Ё Ё проверить правильность дескриптора сообщения; Ё Ё /* найти сообщение, нужное пользователю */ Ё Ё если (тип сообщения в запросе == 0) Ё Ё рассмотреть первое сообщение в очереди; Ё Ё в противном случае если (тип сообщения в запросе > 0) Ё Ё рассмотреть первое сообщение в очереди, имеющее Ё Ё данный тип; Ё Ё в противном случае /* тип сообщения в запросе < 0 */Ё Ё рассмотреть первое из сообщений в очереди с наи- Ё Ё меньшим значением типа при условии, что его тип Ё Ё не превышает абсолютное значение типа, указанно-Ё Ё го в запросе; Ё Ё если (сообщение найдено) Ё Ё { Ё Ё переустановить размер сообщения или вернуть ошиб-Ё Ё ку, если размер, указанный пользователем слишкомЁ Ё мал; Ё Ё скопировать тип сообщения и его текст из прост- Ё Ё ранства ядра в пространство задачи; Ё Ё разорвать связь сообщения с очередью; Ё Ё вернуть управление; Ё Ё } Ё Ё /* сообщений нет */ Ё Ё если (флаги не разрешают приостанавливать работу) Ё Ё вернуть управление с ошибкой; Ё Ё приостановиться (пока сообщение не появится в очере- Ё Ё ди); Ё Ё перейти на loop; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.7. Алгоритм получения сообщения па 1 - запросы от процессов-клиентов. Он читает текст сообщения, находит идентификатор процесса-клиента и приравнивает возвращае- мое значение типа сообщения значению этого идентификатора. В дан- ном примере обслуживающий процесс возвращает в тексте сообщения процессу-клиенту его идентификатор, и клиент получает сообщения с типом, равным идентификатору клиента. Таким образом, обслуживаю- щий процесс получает сообщения только от клиентов, а клиент - только от обслуживающего процесса. Работа процессов реализуется в виде многоканального взаимодействия, строящегося на основе одной очереди сообщений. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё #include Ё Ё #include Ё Ё Ё Ё #define MSGKEY 75 Ё Ё struct msgform Ё Ё { Ё Ё long mtype; Ё Ё char mtext[256]; Ё Ё }msg; Ё Ё int msgid; Ё Ё Ё Ё main() Ё Ё { Ё Ё int i,pid,*pint; Ё Ё extern cleanup(); Ё Ё Ё Ё for (i = 0; i < 20; i++) Ё Ё signal(i,cleanup); Ё Ё msgid = msgget(MSGKEY,0777ЁIPC_CREAT); Ё Ё Ё Ё for (;;) Ё Ё { Ё Ё msgrcv(msgid,&msg,256,1,0); Ё Ё pint = (int *) msg.mtext; Ё Ё pid = *pint; Ё Ё printf("сервер: получил от процесса с pid %d\n",Ё Ё pid); Ё Ё msg.mtype = pid; Ё Ё *pint = getpid(); Ё Ё msgsnd(msgid,&msg,sizeof(int),0); Ё Ё } Ё Ё } Ё Ё Ё Ё cleanup() Ё Ё { Ё Ё msgctl(msgid,IPC_RMID,0); Ё Ё exit(); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.8. Обслуживающий процесс (сервер) Сообщения имеют форму "тип - текст", где текст представляет собой поток байтов. Указание типа дает процессам возможность вы- бирать сообщения только определенного рода, что в файловой систе- ме не так легко сделать. Таким образом, процессы могут выбирать из очереди сообщения определенного типа в порядке их поступления, причем эта очередность гарантируется ядром. Несмотря на то, что обмен сообщениями может быть реализован на пользовательском уров- не средствами файловой системы, представленный вашему вниманию механизм обеспечивает более эффективную организацию передачи дан- ных между процессами. С помощью системной функции msgctl процесс может запросить информацию о статусе дескриптора сообщения, установить этот ста- тус или удалить дескриптор сообщения из системы. Синтаксис вызова функции: msgctl(id,cmd,mstatbuf) где id - дескриптор сообщения, cmd - тип команды, mstatbuf - ад- рес пользовательской структуры, в которой будут храниться управ- ляющие параметры или результаты обработки запроса. Более подробно об аргументах функции пойдет речь в Приложении. Вернемся к примеру, представленному на Рисунке 11.8. Обслужи- вающий процесс принимает сигналы и с помощью функции cleanup уда- ляет очередь сообщений из системы. Если же им не было поймано ни одного сигнала или был получен сигнал SIGKILL, очередь сообщений остается в системе, даже если на нее не ссылается ни один из про- цессов. Дальнейшие попытки исключительно создания новой очереди сообщений с данным ключом (идентификатором) не будут иметь успех до тех пор, пока старая очередь не будет удалена из системы. 11.2.2 Разделение памяти Процессы могут взаимодействовать друг с другом непосредствен- но путем разделения (совместного использования) участков вирту- ального адресного пространства и обмена данными через разделяемую память. Системные функции для работы с разделяемой памятью имеют много сходного с системными функциями для работы с сообщениями. Функция shmget создает новую область разделяемой памяти или возв- ращает адрес уже существующей области, функция shmat логически присоединяет область к виртуальному адресному пространству про- цесса, функция shmdt отсоединяет ее, а функция shmctl имеет дело с различными параметрами, связанными с разделяемой памятью. Про- цессы ведут чтение и запись данных в области разделяемой памяти, используя для этого те же самые машинные команды, что и при рабо- те с обычной памятью. После присоединения к виртуальному адресно- му пространству процесса область разделяемой памяти становится доступна так же, как любой участок виртуальной памяти; для досту- па к находящимся в ней данным не нужны обращения к каким-то до- полнительным системным функциям. Синтаксис вызова системной функции shmget: shmid = shmget(key,size,flag); где size - объем области в байтах. Ядро использует key для веде- ния поиска в таблице разделяемой памяти: если подходящая запись обнаружена и если разрешение на доступ имеется, ядро возвращает вызывающему процессу указанный в записи дескриптор. Если запись не найдена и если пользователь установил флаг IPC_CREAT, указыва- ющий на необходимость создания новой области, ядро проверяет на- хождение размера области в установленных системой пределах и вы- деляет область по алгоритму allocreg (раздел 6.5.2). Ядро записывает установки прав доступа, размер области и указатель на соответствующую запись таблицы областей в таблицу разделяемой па- мяти (Рисунок 11.9) и устанавливает флаг, свидетельствующий о том, что с областью не связана отдельная память. Области выделя- ется память (таблицы страниц и т.п.) только тогда, когда процесс присоединяет область к своему адресному пространству. Ядро уста- навливает также флаг, говорящий о том, что по завершении послед- него связанного с областью процесса область не должна освобож- даться. Таким образом, данные в разделяемой памяти остаются в сохранности, даже если она не принадлежит ни одному из процессов (как часть виртуального адресного пространства последнего). Таблица раз- Таблица процессов - деляемой па- Таблица областей частная таблица об- мяти ластей процесса здддддддддд© здддддддддддддд© зддддддддд© Ё ддддедддд© Ё Ё зддддедддд Ё цдддддддддд╢ зЁд>цдддддддддддддд╢<дддды цддддддддд╢ Ё ддддедддыЁ Ё Ё здддедддд Ё цдддддддддд╢ Ё цдддддддддддддд╢<дддд©Ё цддддддддд╢ Ё ддддедд© Ё Ё Ё юЁдддедддд Ё цдддддддддд╢ Ё Ё цдддддддддддддд╢ Ё цддддддддд╢ Ё Ы Ё Ё Ё Ё Ё Ё Ё Ё Ё Ы Ё Ё юд>цдддддддддддддд╢ Ё цддддддддд╢ Ё Ы Ё Ё Ё Ё Ё Ё Ё Ё Ы Ё юддд>цдддддддддддддд╢<ддддды цддддддддд╢ Ё Ы Ё Ё Ё (после Ё Ё Ё Ы Ё цдддддддддддддд╢ shmat) цддддддддд╢ Ё Ы Ё Ё Ы Ё Ё Ё Ё Ы Ё Ё Ы Ё цддддддддд╢ Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юдддддддддддддды Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юдддддддддды юддддддддды Рисунок 11.9. Структуры данных, используемые при разделении памяти Процесс присоединяет область разделяемой памяти к своему вир- туальному адресному пространству с помощью системной функции shmat: virtaddr = shmat(id,addr,flags); Значение id, возвращаемое функцией shmget, идентифицирует область разделяемой памяти, addr является виртуальным адресом, по которо- му пользователь хочет подключить область, а с помощью флагов (flags) можно указать, предназначена ли область только для чтения и нужно ли ядру округлять значение указанного пользователем адре- са. Возвращаемое функцией значение, virtaddr, представляет собой виртуальный адрес, по которому ядро произвело подключение области и который не всегда совпадает с адресом, указанным пользователем. В начале выполнения системной функции shmat ядро проверяет наличие у процесса необходимых прав доступа к области (Рисунок 11.10). Оно исследует указанный пользователем адрес; если он ра- вен 0, ядро выбирает виртуальный адрес по своему усмотрению. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм shmat /* подключить разделяемую память */ Ё Ё входная информация: (1) дескриптор области разделяемой Ё Ё памяти Ё Ё (2) виртуальный адрес для подключения Ё Ё области Ё Ё (3) флаги Ё Ё выходная информация: виртуальный адрес, по которому областьЁ Ё подключена фактически Ё Ё { Ё Ё проверить правильность указания дескриптора, права до- Ё Ё ступа к области; Ё Ё если (пользователь указал виртуальный адрес) Ё Ё { Ё Ё округлить виртуальный адрес в соответствии с фла- Ё Ё гами; Ё Ё проверить существование полученного адреса, размерЁ Ё области; Ё Ё } Ё Ё в противном случае /* пользователь хочет, чтобы ядро Ё Ё * само нашло подходящий адрес */ Ё Ё ядро выбирает адрес: в случае неудачи выдается Ё Ё ошибка; Ё Ё присоединить область к адресному пространству процесса Ё Ё (алгоритм attachreg); Ё Ё если (область присоединяется впервые) Ё Ё выделить таблицы страниц и отвести память под нее Ё Ё (алгоритм growreg); Ё Ё вернуть (виртуальный адрес фактического присоединения Ё Ё области); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.10. Алгоритм присоединения разделяемой памяти Область разделяемой памяти не должна пересекаться в виртуаль- ном адресном пространстве процесса с другими областями; следова- тельно, ее выбор должен производиться разумно и осторожно. Так, например, процесс может увеличить размер принадлежащей ему облас- ти данных с помощью системной функции brk, и новая область данных будет содержать адреса, смежные с прежней областью; поэтому, ядру не следует присоединять область разделяемой памяти слишком близко к области данных процесса. Так же не следует размещать область разделяемой памяти вблизи от вершины стека, чтобы стек при своем последующем увеличении не залезал за ее пределы. Если, например, стек растет в направлении увеличения адресов, лучше всего размес- тить область разделяемой памяти непосредственно перед началом об- ласти стека. Ядро проверяет возможность размещения области разделяемой па- мяти в адресном пространстве процесса и присоединяет ее с помощью алгоритма attachreg. Если вызывающий процесс является первым про- цессом, который присоединяет область, ядро выделяет для области все необходимые таблицы, используя алгоритм growreg, записывает время присоединения в соответствующее поле таблицы разделяемой памяти и возвращает процессу виртуальный адрес, по которому об- ласть была им подключена фактически. Отсоединение области разделяемой памяти от виртуального ад- ресного пространства процесса выполняет функция shmdt(addr) где addr - виртуальный адрес, возвращенный функцией shmat. Нес- мотря на то, что более логичной представляется передача идентифи- катора, процесс использует виртуальный адрес разделяемой памяти, поскольку одна и та же область разделяемой памяти может быть подключена к адресному пространству процесса несколько раз, к то- му же ее идентификатор может быть удален из системы. Ядро произ- водит поиск области по указанному адресу и отсоединяет ее от ад- ресного пространства процесса, используя алгоритм detachreg (раз- дел 6.5.7). Поскольку в таблицах областей отсутствуют обратные указатели на таблицу разделяемой памяти, ядру приходится просмат- ривать таблицу разделяемой памяти в поисках записи, указывающей на данную область, и записывать в соответствующее поле время пос- леднего отключения области. Рассмотрим программу, представленную на Рисунке 11.11. В ней описывается процесс, создающий область разделяемой памяти разме- ром 128 Кбайт и дважды присоединяющий ее к своему адресному пространству по разным виртуальным адресам. В "первую" область он записывает данные, а читает их из "второй" области. На Рисунке 11.12 показан другой процесс, присоединяющий ту же область (он получает только 64 Кбайта, таким образом, каждый процесс может использовать разный объем области разделяемой памяти); он ждет момента, когда первый процесс запишет в первое принадлежащее об- ласти слово любое отличное от нуля значение, и затем принимается считывать данные из области. Первый процесс делает "паузу" (pause), предоставляя второму процессу возможность выполнения; когда первый процесс принимает сигнал, он удаляет область разде- ляемой памяти из системы. Процесс запрашивает информацию о состоянии области разделяе- мой памяти и производит установку параметров для нее с помощью системной функции shmctl: shmctl(id,cmd,shmstatbuf); Значение id идентифицирует запись таблицы разделяемой памяти, cmd определяет тип операции, а shmstatbuf является адресом пользова- тельской структуры, в которую помещается информация о состоянии области. Ядро трактует тип операции точно так же, как и при уп- равлении сообщениями. Удаляя область разделяемой памяти, ядро ос- вобождает соответствующую ей запись в таблице разделяемой памяти и просматривает таблицу областей: если область не была присоеди- нена ни к одному из процессов, ядро освобождает запись таблицы и все выделенные области ресурсы, используя для этого алгоритм freereg (раздел 6.5.6). Если же область по-прежнему подключена к каким-то процессам (значение счетчика ссылок на нее больше 0), ядро только сбрасывает флаг, говорящий о том, что по завершении последнего связанного с нею процесса область не должна освобож- даться. Процессы, уже использующие область разделяемой памяти, продолжают работать с ней, новые же процессы не могут присоеди- нить ее. Когда все процессы отключат область, ядро освободит ее. Это похоже на то, как в файловой системе после разрыва связи с файлом процесс может вновь открыть его и продолжать с ним работу. 11.2.3 Семафоры Системные функции работы с семафорами обеспечивают синхрони- зацию выполнения параллельных процессов, производя набор действий единственно над группой семафоров (средствами низкого уровня). До использования семафоров, если процессу нужно было заблокировать некий ресурс, он прибегал к созданию с помощью системной функции creat специального блокирующего файла. Если файл уже существовал, функция creat завершалась неудачно, и процесс делал вывод о том, что ресурс уже заблокирован другим процессом. Главные недостатки такого подхода заключались в том, что процесс не знал, в какой момент ему следует предпринять следующую попытку, а также в том, что блокирующие файлы случайно оставались в системе в случае ее здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё #include Ё Ё #include Ё Ё #define SHMKEY 75 Ё Ё #define K 1024 Ё Ё int shmid; Ё Ё Ё Ё main() Ё Ё { Ё Ё int i, *pint; Ё Ё char *addr1, *addr2; Ё Ё extern char *shmat(); Ё Ё extern cleanup(); Ё Ё Ё Ё for (i = 0; i < 20; i++) Ё Ё signal(i,cleanup); Ё Ё shmid = shmget(SHMKEY,128*K,0777ЁIPC_CREAT); Ё Ё addr1 = shmat(shmid,0,0); Ё Ё addr2 = shmat(shmid,0,0); Ё Ё printf("addr1 Ox%x addr2 Ox%x\n",addr1,addr2); Ё Ё pint = (int *) addr1; Ё Ё Ё Ё for (i = 0; i < 256, i++) Ё Ё *pint++ = i; Ё Ё pint = (int *) addr1; Ё Ё *pint = 256; Ё Ё Ё Ё pint = (int *) addr2; Ё Ё for (i = 0; i < 256, i++) Ё Ё printf("index %d\tvalue %d\n",i,*pint++); Ё Ё Ё Ё pause(); Ё Ё } Ё Ё Ё Ё cleanup() Ё Ё { Ё Ё shmctl(shmid,IPC_RMID,0); Ё Ё exit(); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.11. Присоединение процессом одной и той же области разделяемой памяти дважды зддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё #include Ё Ё #include Ё Ё Ё Ё #define SHMKEY 75 Ё Ё #define K 1024 Ё Ё int shmid; Ё Ё Ё Ё main() Ё Ё { Ё Ё int i, *pint; Ё Ё char *addr; Ё Ё extern char *shmat(); Ё Ё Ё Ё shmid = shmget(SHMKEY,64*K,0777); Ё Ё Ё Ё addr = shmat(shmid,0,0); Ё Ё pint = (int *) addr; Ё Ё Ё Ё while (*pint == 0) Ё Ё ; Ё Ё for (i = 0; i < 256, i++) Ё Ё printf("%d\n",*pint++); Ё Ё } Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.12. Разделение памяти между процессами аварийного завершения или перезагрузки. Дийкстрой был опубликован алгоритм Деккера, описывающий реа- лизацию семафоров как целочисленных объектов, для которых опреде- лены две элементарные операции: P и V (см. [Dijkstra 68]). Опера- ция P заключается в уменьшении значения семафора в том случае, если оно больше 0, операция V - в увеличении этого значения (и там, и там на единицу). Поскольку операции элементарные, в любой момент времени для каждого семафора выполняется не более одной операции P или V. Связанные с семафорами системные функции явля- ются обобщением операций, предложенных Дийкстрой, в них допуска- ется одновременное выполнение нескольких операций, причем опера- ции уменьшения и увеличения выполняются над значениями, превышающими 1. Ядро выполняет операции комплексно; ни один из посторонних процессов не сможет переустанавливать значения сема- форов, пока все операции не будут выполнены. Если ядро по ка- ким-либо причинам не может выполнить все операции, оно не выпол- няет ни одной; процесс приостанавливает свою работу до тех пор, пока эта возможность не будет предоставлена. Семафор в версии V системы UNIX состоит из следующих элемен- тов: * Значение семафора, * Идентификатор последнего из процессов, работавших с семафо- ром, * Количество процессов, ожидающих увеличения значения семафора, * Количество процессов, ожидающих момента, когда значение сема- фора станет равным 0. Для создания набора семафоров и получения доступа к ним ис- пользуется системная функция semget, для выполнения различных уп- равляющих операций над набором - функция semctl, для работы со значениями семафоров - функция semop. Таблица семафоров Массивы семафоров зддддддд© Ё Ё здддбдддбдддбдддбдддбдддбддд© Ё цддддддд>Ё 0 Ё 1 Ё 2 Ё 3 Ё 4 Ё 5 Ё 6 Ё Ё Ё юдддадддадддадддадддадддаддды цддддддд╢ Ё Ё здддбдддбддд© Ё цддддддд>Ё 0 Ё 1 Ё 2 Ё Ё Ё юдддадддаддды цддддддд╢ Ё Ё зддд© Ё цддддддд>Ё 0 Ё Ё Ё юддды цддддддд╢ Ё Ё здддбдддбддд© Ё цддддддд>Ё 0 Ё 1 Ё 2 Ё Ё Ё юдддадддаддды цддддддд╢ Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юддддддды Рисунок 11.13. Структуры данных, используемые в работе над семафорами Синтаксис вызова системной функции semget: id = semget(key,count,flag); где key, flag и id имеют тот же смысл, что и в других механизмах взаимодействия процессов (обмен сообщениями и разделение памяти). В результате выполнения функции ядро выделяет запись, указывающую на массив семафоров и содержащую счетчик count (Рисунок 11.13). В записи также хранится количество семафоров в массиве, время пос- леднего выполнения функций semop и semctl. Системная функция semget на Рисунке 11.14, например, создает семафор из двух эле- ментов. Синтаксис вызова системной функции semop: oldval = semop(id,oplist,count); где id - дескриптор, возвращаемый функцией semget, oplist - ука- затель на список операций, count - размер списка. Возвращаемое функцией значение oldval является прежним значением семафора, над здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё #include Ё Ё #include Ё Ё Ё Ё #define SEMKEY 75 Ё Ё int semid; Ё Ё unsigned int count; Ё Ё /* определение структуры sembuf в файле sys/sem.h Ё Ё * struct sembuf { Ё Ё * unsigned shortsem_num; Ё Ё * short sem_op; Ё Ё * short sem_flg; Ё Ё }; */ Ё Ё struct sembuf psembuf,vsembuf; /* операции типа P и V */Ё Ё Ё Ё main(argc,argv) Ё Ё int argc; Ё Ё char *argv[]; Ё Ё { Ё Ё int i,first,second; Ё Ё short initarray[2],outarray[2]; Ё Ё extern cleanup(); Ё Ё Ё Ё if (argc == 1) Ё Ё { Ё Ё for (i = 0; i < 20; i++) Ё Ё signal(i,cleanup); Ё Ё semid = semget(SEMKEY,2,0777ЁIPC_CREAT); Ё Ё initarray[0] = initarray[1] = 1; Ё Ё semctl(semid,2,SETALL,initarray); Ё Ё semctl(semid,2,GETALL,outarray); Ё Ё printf("начальные значения семафоров %d %d\n", Ё Ё outarray[0],outarray[1]); Ё Ё pause(); /* приостанов до получения сигнала */ Ё Ё } Ё Ё Ё Ё /* продолжение на следующей странице */ Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.14. Операции установки и снятия блокировки которым производилась операция. Каждый элемент списка операций имеет следующий формат: * номер семафора, идентифицирующий элемент массива семафоров, над которым выполняется операция, * код операции, * флаги. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё else if (argv[1][0] == 'a') Ё Ё { Ё Ё first = 0; Ё Ё second = 1; Ё Ё } Ё Ё else Ё Ё { Ё Ё first = 1; Ё Ё second = 0; Ё Ё } Ё Ё Ё Ё semid = semget(SEMKEY,2,0777); Ё Ё psembuf.sem_op = -1; Ё Ё psembuf.sem_flg = SEM_UNDO; Ё Ё vsembuf.sem_op = 1; Ё Ё vsembuf.sem_flg = SEM_UNDO; Ё Ё Ё Ё for (count = 0; ; count++) Ё Ё { Ё Ё psembuf.sem_num = first; Ё Ё semop(semid,&psembuf,1); Ё Ё psembuf.sem_num = second; Ё Ё semop(semid,&psembuf,1); Ё Ё printf("процесс %d счетчик %d\n",getpid(),count); Ё Ё vsembuf.sem_num = second; Ё Ё semop(semid,&vsembuf,1); Ё Ё vsembuf.sem_num = first; Ё Ё semop(semid,&vsembuf,1); Ё Ё } Ё Ё } Ё Ё Ё Ё cleanup() Ё Ё { Ё Ё semctl(semid,2,IPC_RMID,0); Ё Ё exit(); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.14. Операции установки и снятия блокировки (продолжение) Ядро считывает список операций oplist из адресного пространс- тва задачи и проверяет корректность номеров семафоров, а также наличие у процесса необходимых разрешений на чтение и корректи- ровку семафоров (Рисунок 11.15). Если таких разрешений не имеет- ся, системная функция завершается неудачно. Если ядру приходится приостанавливать свою работу при обращении к списку операций, оно возвращает семафорам их прежние значения и находится в состоянии приостанова до наступления ожидаемого события, после чего систем- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм semop /* операции над семафором */ Ё Ё входная информация: (1) дескриптор семафора Ё Ё (2) список операций над семафором Ё Ё (3) количество элементов в списке Ё Ё выходная информация: исходное значение семафора Ё Ё { Ё Ё проверить корректность дескриптора семафора; Ё Ё start: считать список операций над семафором из простран- Ё Ё ства задачи в пространство ядра; Ё Ё проверить наличие разрешений на выполнение всех опера- Ё Ё ций; Ё Ё Ё Ё для (каждой операции в списке) Ё Ё { Ё Ё если (код операции имеет положительное значение) Ё Ё { Ё Ё прибавить код операции к значению семафора; Ё Ё если (для данной операции установлен флаг UNDO)Ё Ё скорректировать структуру восстановления Ё Ё для данного процесса; Ё Ё вывести из состояния приостанова все процессы, Ё Ё ожидающие увеличения значения семафора; Ё Ё } Ё Ё в противном случае если (код операции имеет отрица-Ё Ё тельное значение) Ё Ё { Ё Ё если (код операции + значение семафора >= 0) Ё Ё { Ё Ё прибавить код операции к значению семафо- Ё Ё ра; Ё Ё если (флаг UNDO установлен) Ё Ё скорректировать структуру восстанов- Ё Ё ления для данного процесса; Ё Ё если (значение семафора равно 0) Ё Ё /* продолжение на следующей страни- Ё Ё * це */ Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.15. Алгоритм выполнения операций над семафором ная функция запускается вновь. Поскольку ядро хранит коды опера- ций над семафорами в глобальном списке, оно вновь считывает этот список из пространства задачи, когда перезапускает системную функцию. Таким образом, операции выполняются комплексно - или все за один сеанс или ни одной. Ядро меняет значение семафора в зависимости от кода операции. Если код операции имеет положительное значение, ядро увеличивает значение семафора и выводит из состояния приостанова все процес- сы, ожидающие наступления этого события. Если код операции равен 0, ядро проверяет значение семафора: если оно равно 0, ядро пере- ходит к выполнению других операций; в противном случае ядро уве- личивает число приостановленных процессов, ожидающих, когда зна- чение семафора станет нулевым, и "засыпает". Если код операции имеет отрицательное значение и если его абсолютное значение не превышает значение семафора, ядро прибавляет код операции (отри- цательное число) к значению семафора. Если результат равен 0, яд- ро выводит из состояния приостанова все процессы, ожидающие обну- ления значения семафора. Если результат меньше абсолютного здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё вывести из состояния приостанова все Ё Ё процессы, ожидающие обнуления значе-Ё Ё ния семафора; Ё Ё продолжить; Ё Ё } Ё Ё выполнить все произведенные над семафором в Ё Ё данном сеансе операции в обратной последова- Ё Ё тельности (восстановить старое значение сема- Ё Ё фора); Ё Ё если (флаги не велят приостанавливаться) Ё Ё вернуться с ошибкой; Ё Ё приостановиться (до тех пор, пока значение се- Ё Ё мафора не увеличится); Ё Ё перейти на start; /* повторить цикл с самого Ё Ё * начала * / Ё Ё } Ё Ё в противном случае /* код операции равен нулю */Ё Ё { Ё Ё если (значение семафора отлично от нуля) Ё Ё { Ё Ё выполнить все произведенные над семафором Ё Ё в данном сеансе операции в обратной по- Ё Ё следовательности (восстановить старое Ё Ё значение семафора); Ё Ё если (флаги не велят приостанавливаться) Ё Ё вернуться с ошибкой; Ё Ё приостановиться (до тех пор, пока значениеЁ Ё семафора не станет нулевым); Ё Ё перейти на start; /* повторить цикл */ Ё Ё } Ё Ё } Ё Ё } /* конец цикла */ Ё Ё /* все операции над семафором выполнены */ Ё Ё скорректировать значения полей, в которых хранится вре-Ё Ё мя последнего выполнения операций и идентификаторы Ё Ё процессов; Ё Ё вернуть исходное значение семафора, существовавшее в Ё Ё момент вызова функции semop; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.15. Алгоритм выполнения операций над семафором (продолжение) значения кода операции, ядро приостанавливает процесс до тех пор, пока значение семафора не увеличится. Если процесс приостанавли- вается посреди операции, он имеет приоритет, допускающий прерыва- ния; следовательно, получив сигнал, он выходит из этого состоя- ния. Перейдем к программе, представленной на Рисунке 11.14, и предположим, что пользователь исполняет ее (под именем a.out) три раза в следующем порядке: a.out & a.out a & a.out b & Если программа вызывается без параметров, процесс создает на- бор семафоров из двух элементов и присваивает каждому семафору значение, равное 1. Затем процесс вызывает функцию pause и приос- танавливается для получения сигнала, после чего удаляет семафор из системы (cleanup). При выполнении программы с параметром 'a' процесс (A) производит над семафорами в цикле четыре операции: он уменьшает на единицу значение семафора 0, то же самое делает с семафором 1, выполняет команду вывода на печать и вновь увеличи- вает значения семафоров 0 и 1. Если бы процесс попытался умень- шить значение семафора, равное 0, ему пришлось бы приостановить- ся, следовательно, семафор можно считать захваченным (недоступным для уменьшения). Поскольку исходные значения семафоров были равны 1 и поскольку к семафорам не было обращений со стороны других процессов, процесс A никогда не приостановится, а значения сема- форов будут изменяться только между 1 и 0. При выполнении прог- раммы с параметром 'b' процесс (B) уменьшает значения семафоров 0 и 1 в порядке, обратном ходу выполнения процесса A. Когда про- цессы A и B выполняются параллельно, может сложиться ситуация, в которой процесс A захватил семафор 0 и хочет захватить семафор 1, а процесс B захватил семафор 1 и хочет захватить семафор 0. Оба процесса перейдут в состояние приостанова, не имея возможности продолжить свое выполнение. Возникает взаимная блокировка, из ко- торой процессы могут выйти только по получении сигнала. Чтобы предотвратить возникновение подобных проблем, процессы могут выполнять одновременно несколько операций над семафорами. В последнем примере желаемый эффект достигается благодаря использо- ванию следующих операторов: struct sembuf psembuf[2]; psembuf[0].sem_num = 0; psembuf[1].sem_num = 1; psembuf[0].sem_op = -1; psembuf[1].sem_op = -1; semop(semid,psembuf,2); Psembuf - это список операций, выполняющих одновременное уменьше- ние значений семафоров 0 и 1. Если какая-то операция не может вы- полняться, процесс приостанавливается. Так, например, если значе- ние семафора 0 равно 1, а значение семафора 1 равно 0, ядро оста- вит оба значения неизменными до тех пор, пока не сможет уменьшить и то, и другое. Установка флага IPC_NOWAIT в функции semop имеет следующий смысл: если ядро попадает в такую ситуацию, когда процесс должен приостановить свое выполнение в ожидании увеличения значения се- мафора выше определенного уровня или, наоборот, снижения этого значения до 0, и если при этом флаг IPC_NOWAIT установлен, ядро выходит из функции с извещением об ошибке. Таким образом, если не приостанавливать процесс в случае невозможности выполнения от- дельной операции, можно реализовать условный тип семафора. Если процесс выполняет операцию над семафором, захватывая при этом некоторые ресурсы, и завершает свою работу без приведения семафора в исходное состояние, могут возникнуть опасные ситуации. Причинами возникновения таких ситуаций могут быть как ошибки программирования, так и сигналы, приводящие к внезапному заверше- нию выполнения процесса. Если после того, как процесс уменьшит значения семафоров, он получит сигнал kill, восстановить прежние значения процессу уже не удастся, поскольку сигналы данного типа не анализируются процессом. Следовательно, другие процессы, пыта- ясь обратиться к семафорам, обнаружат, что последние заблокирова- ны, хотя сам заблокировавший их процесс уже прекратил свое су- ществование. Чтобы избежать возникновения подобных ситуаций, в функции semop процесс может установить флаг SEM_UNDO; когда про- цесс завершится, ядро даст обратный ход всем операциям, выполнен- ным процессом. Для этого в распоряжении у ядра имеется таблица, в которой каждому процессу в системе отведена отдельная запись. За- пись таблицы содержит указатель на группу структур восстановле- Заголовки частных структур восстановления Структуры восстановления здддддд© Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё здддддддддд© здддддддддд© здддддддддд© цдддддд╢ ЁДескрипторЁ ЁДескрипторЁ ЁДескрипторЁ Ё цдд>Ё Номер цдд>Ё Номер цдд>Ё Номер Ё цдддддд╢ Ё Значение Ё Ё Значение Ё Ё Значение Ё Ё Ё юдддддддддды юдддддддддды юдддддддддды Ё Ё здддддддддд© цдддддд╢ ЁДескрипторЁ Ё цдд>Ё Номер Ё цдддддд╢ Ё Значение Ё Ё Ы Ё юдддддддддды Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё Ё Ы Ё юдддддды Рисунок 11.16. Структуры восстановления семафоров ния, по одной структуре на каждый используемый процессом семафор (Рисунок 11.16). Каждая структура восстановления состоит из трех элементов - идентификатора семафора, его порядкового номера в на- боре и установочного значения. Ядро выделяет структуры восстановления динамически, во время первого выполнения системной функции semop с установленным флагом SEM_UNDO. При последующих обращениях к функции с тем же флагом ядро просматривает структуры восстановления для процесса в поис- ках структуры с тем же самым идентификатором и порядковым номером семафора, что и в формате вызова функции. Если структура обнару- жена, ядро вычитает значение произведенной над семафором операции из установочного значения. Таким образом, в структуре восстанов- ления хранится результат вычитания суммы значений всех операций, произведенных над семафором, для которого установлен флаг SEM_UNDO. Если соответствующей структуры нет, ядро создает ее, сортируя при этом список структур по идентификаторам и номерам семафоров. Если установочное значение становится равным 0, ядро удаляет структуру из списка. Когда процесс завершается, ядро вы- зывает специальную процедуру, которая просматривает все связанные с процессом структуры восстановления и выполняет над указанным семафором все обусловленные действия. Ядро создает структуру восстановления всякий раз, когда про- цесс уменьшает значение семафора, а удаляет ее, когда процесс увеличивает значение семафора, поскольку установочное значение здддддддддддддддббддддддд© здддддддддддддддббдддддддбддддддд© Ё идентификатор ЁЁ Ё Ё идентификатор ЁЁ Ё Ё Ё семафора ЁЁ semid Ё Ё семафора ЁЁ semid Ё semid Ё цдддддддддддддддееддддддд╢ цдддддддддддддддеедддддддеддддддд╢ Ё номер семафораЁЁ 0 Ё Ё номер семафораЁЁ 0 Ё 1 Ё цдддддддддддддддееддддддд╢ цдддддддддддддддеедддддддеддддддд╢ Ё установочное ЁЁ Ё Ё установочное ЁЁ Ё Ё Ё значение ЁЁ 1 Ё Ё значение ЁЁ 1 Ё 1 Ё юдддддддддддддддааддддддды юдддддддддддддддаадддддддаддддддды (а) После первой операции (б) После второй операции здддддддддддддддббддддддд© Ё идентификатор ЁЁ Ё Ё семафора ЁЁ semid Ё цдддддддддддддддееддддддд╢ Ё номер семафораЁЁ 0 Ё пусто цдддддддддддддддееддддддд╢ Ё установочное ЁЁ Ё Ё значение ЁЁ 1 Ё юдддддддддддддддааддддддды (в) После третьей операции (г) После четвертой операции Рисунок 11.17. Последовательность состояний списка структур восстановления структуры равно 0. На Рисунке 11.17 показана последовательность состояний списка структур при выполнении программы с параметром 'a'. После первой операции процесс имеет одну структуру, состоя- щую из идентификатора semid, номера семафора, равного 0, и уста- новочного значения, равного 1, а после второй операции появляется вторая структура с номером семафора, равным 1, и установочным значением, равным 1. Если процесс неожиданно завершается, ядро проходит по всем структурам и прибавляет к каждому семафору по единице, восстанавливая их значения в 0. В частном случае ядро уменьшает установочное значение для семафора 1 на третьей опера- ции, в соответствии с увеличением значения самого семафора, и удаляет всю структуру целиком, поскольку установочное значение становится нулевым. После четвертой операции у процесса больше нет структур восстановления, поскольку все установочные значения стали нулевыми. Векторные операции над семафорами позволяют избежать взаимных блокировок, как было показано выше, однако они представляют из- вестную трудность для понимания и реализации, и в большинстве приложений полный набор их возможностей не является обязательным. Программы, испытывающие потребность в использовании набора сема- форов, сталкиваются с возникновением взаимных блокировок на поль- зовательском уровне, и ядру уже нет необходимости поддерживать такие сложные формы системных функций. Синтаксис вызова системной функции semctl: semctl(id,number,cmd,arg); Параметр arg объявлен как объединение типов данных: union semunion { int val; struct semid_ds *semstat; /* описание типов см. в При- * ложении */ unsigned short *array; } arg; Ядро интерпретирует параметр arg в зависимости от значения параметра cmd, подобно тому, как интерпретирует команды ioctl (глава 10). Типы действий, которые могут использоваться в пара- метре cmd: получить или установить значения управляющих парамет- ров (права доступа и др.), установить значения одного или всех семафоров в наборе, прочитать значения семафоров. Подробности по каждому действию содержатся в Приложении. Если указана команда удаления, IPC_RMID, ядро ведет поиск всех процессов, содержащих структуры восстановления для данного семафора, и удаляет соот- ветствующие структуры из системы. Затем ядро инициализирует ис- пользуемые семафором структуры данных и выводит из состояния при- останова все процессы, ожидающие наступления некоторого связанно- го с семафором события: когда процессы возобновляют свое выполне- ние, они обнаруживают, что идентификатор семафора больше не явля- ется корректным, и возвращают вызывающей программе ошибку. 11.2.4 Общие замечания Механизм функционирования файловой системы и механизмы взаи- модействия процессов имеют ряд общих черт. Системные функции типа "get" похожи на функции creat и open, функции типа "control" пре- доставляют возможность удалять дескрипторы из системы, чем похожи на функцию unlink. Тем не менее, в механизмах взаимодействия про- цессов отсутствуют операции, аналогичные операциям, выполняемым системной функцией close. Следовательно, ядро не располагает све- дениями о том, какие процессы могут использовать механизм IPC, и, действительно, процессы могут прибегать к услугам этого механиз- ма, если правильно угадывают соответствующий идентификатор и если у них имеются необходимые права доступа, даже если они не выпол- нили предварительно функцию типа "get". Ядро не может автомати- чески очищать неиспользуемые структуры механизма взаимодействия процессов, поскольку ядру неизвестно, какие из этих структур больше не нужны. Таким образом, завершившиеся вследствие возник- новения ошибки процессы могут оставить после себя ненужные и не- используемые структуры, перегружающие и засоряющие систему. Нес- мотря на то, что в структурах механизма взаимодействия после завершения существования процесса ядро может сохранить информацию о состоянии и данные, лучше все-таки для этих целей использовать файлы. Вместо традиционных, получивших широкое распространение фай- лов механизмы взаимодействия процессов используют новое прост- ранство имен, состоящее из ключей (keys). Расширить семантику ключей на всю сеть довольно трудно, поскольку на разных машинах ключи могут описывать различные объекты. Короче говоря, ключи в основном предназначены для использования в одномашинных системах. Имена файлов в большей степени подходят для распределенных систем (см. главу 13). Использование ключей вместо имен файлов также свидетельствует о том, что средства взаимодействия процессов яв- ляются "вещью в себе", полезной в специальных приложениях, но не имеющей тех возможностей, которыми обладают, например, каналы и файлы. Большая часть функциональных возможностей, предоставляемых данными средствами, может быть реализована с помощью других сис- темных средств, поэтому включать их в состав ядра вряд ли следо- вало бы. Тем не менее, их использование в составе пакетов прик- ладных программ тесного взаимодействия дает лучшие результаты по сравнению со стандартными файловыми средствами (см. Упражнения). 11.3 ВЗАИМОДЕЙСТВИЕ В СЕТИ Программы, поддерживающие межмашинную связь, такие, как электронная почта, программы дистанционной пересылки файлов и удаленной регистрации, издавна используются в качестве специаль- ных средств организации подключений и информационного обмена. Так, например, стандартные программы, работающие в составе элект- ронной почты, сохраняют текст почтовых сообщений пользователя в отдельном файле (для пользователя "mjb" этот файл имеет имя "/usr /mail/mjb"). Когда один пользователь посылает другому почтовое сообщение на ту же машину, программа mail (почта) добавляет сооб- щение в конец файла адресата, используя в целях сохранения це- лостности различные блокирующие и временные файлы. Когда адресат получает почту, программа mail открывает принадлежащий ему почто- вый файл и читает сообщения. Для того, чтобы послать сообщение на другую машину, программа mail должна в конечном итоге отыскать на ней соответствующий почтовый файл. Поскольку программа не может работать с удаленными файлами непосредственно, процесс, протекаю- щий на другой машине, должен действовать в качестве агента ло- кального почтового процесса; следовательно, локальному процессу необходим способ связи со своим удаленным агентом через межмашин- ные границы. Локальный процесс является клиентом удаленного обс- луживающего (серверного) процесса. Поскольку в системе UNIX новые процессы создаются с помощью системной функции fork, к тому моменту, когда клиент попытается выполнить подключение, обслуживающий процесс уже должен существо- вать. Если бы в момент создания нового процесса удаленное ядро получало запрос на подключение (по каналам межмашинной связи), возникла бы несогласованность с архитектурой системы. Чтобы избе- жать этого, некий процесс, обычно init, порождает обслуживающий процесс, который ведет чтение из канала связи, пока не получает запрос на обслуживание, после чего в соответствии с некоторым протоколом выполняет установку соединения. Выбор сетевых средств и протоколов обычно выполняют программы клиента и сервера, осно- вываясь на информации, хранящейся в прикладных базах данных; с другой стороны, выбранные пользователем средства могут быть зако- дированы в самих программах. В качестве примера рассмотрим программу uucp, которая обслу- живает пересылку файлов в сети и исполнение команд на удалении (см. [Nowitz 80]). Процесс-клиент запрашивает в базе данных адрес и другую маршрутную информацию (например, номер телефона), откры- вает автокоммутатор, записывает или проверяет информацию в деск- рипторе открываемого файла и вызывает удаленную машину. Удаленная машина может иметь специальные линии, выделенные для использова- ния программой uucp; выполняющийся на этой машине процесс init порождает getty-процессы - серверы, которые управляют линиями и получают извещения о подключениях. После выполнения аппаратного подключения процесс-клиент регистрируется в системе в соответс- твии с обычным протоколом регистрации: getty-процесс запускает специальный интерпретатор команд, uucico, указанный в файле "/etc /passwd", а процесс-клиент передает на удаленную машину последо- вательность команд, тем самым заставляя ее исполнять процессы от имени локальной машины. Сетевое взаимодействие в системе UNIX представляет серьезную проблему, поскольку сообщения должны включать в себя как информа- ционную, так и управляющую части. В управляющей части сообщения может располагаться адрес назначения сообщения. В свою очередь, структура адресных данных зависит от типа сети и используемого протокола. Следовательно, процессам нужно знать тип сети, а это идет вразрез с тем принципом, по которому пользователи не должны обращать внимания на тип файла, ибо все устройства для пользова- телей выглядят как файлы. Традиционные методы реализации сетевого взаимодействия при установке управляющих параметров в сильной степени полагаются на помощь системной функции ioctl, однако в разных типах сетей этот момент воплощается по-разному. Отсюда возникает нежелательный побочный эффект, связанный с тем, что программы, разработанные для одной сети, в других сетях могут не заработать. Чтобы разработать сетевые интерфейсы для системы UNIX, были предприняты значительные усилия. Реализация потоков в последних редакциях версии V располагает элегантным механизмом поддержки сетевого взаимодействия, обеспечивающим гибкое сочетание отдель- ных модулей протоколов и их согласованное использование на уровне задач. Следующий раздел посвящен краткому описанию метода решения данных проблем в системе BSD, основанного на использовании гнезд. 11.4 ГНЕЗДА В предыдущем разделе было показано, каким образом взаимодейс- твуют между собой процессы, протекающие на разных машинах, при этом обращалось внимание на то, что способы реализации взаимо- действия могут быть различаться в зависимости от используемых протоколов и сетевых средств. Более того, эти способы не всегда применимы для обслуживания взаимодействия процессов, выполняющих- ся на одной и той же машине, поскольку в них предполагается су- ществование обслуживающего (серверного) процесса, который при вы- полнении системных функций open или read будет приостанавливаться драйвером. В целях создания более универсальных методов взаимо- действия процессов на основе использования многоуровневых сетевых протоколов для системы BSD был разработан механизм, получивший название "sockets" (гнезда) (см. [Berkeley 83]). В данном разделе мы рассмотрим некоторые аспекты применения гнезд (на пользова- тельском уровне представления). Процесс-клиент Процесс-сервер Ё Ё Ё Ё юдд© здды здддддддддддддддддддддддддедд© зддедддддддддддддддддддддддддд© Ё Уровень гнезд Ё Ё Уровень гнезд Ё цдддддддддддддддддддддддддедд╢ цддедддддддддддддддддддддддддд╢ Ё TCP Ё Ё TCP Ё Ё Уровень протоколов Ё Ё Ё Ё Уровень протоколов Ё Ё IP Ё Ё IP Ё цдддддддддддддддддддддддддедд╢ цддедддддддддддддддддддддддддд╢ Ё ДрайверЁ Ё Драйвер Ё Ё Уровень устройств EthernetЁ ЁEthernet Уровень устройств Ё юдддддддддддддддддддддддддедды юддедддддддддддддддддддддддддды юддд© зддды Ё Ё Ё Ё С е т ь Рисунок 11.18. Модель с использованием гнезд Структура ядра имеет три уровня: гнезд, протоколов и уст- ройств (Рисунок 11.18). Уровень гнезд выполняет функции интерфей- са между обращениями к операционной системе (системным функциям) и средствами низких уровней, уровень протоколов содержит модули, обеспечивающие взаимодействие процессов (на рисунке упомянуты протоколы TCP и IP), а уровень устройств содержит драйверы, уп- равляющие сетевыми устройствами. Допустимые сочетания протоколов и драйверов указываются при построении системы (в секции конфигу- рации); этот способ уступает по гибкости вышеупомянутому потоко- вому механизму. Процессы взаимодействуют между собой по схеме клиент-сервер: сервер ждет сигнала от гнезда, находясь на одном конце дуплексной линии связи, а процессы-клиенты взаимодействуют с сервером через гнездо, находящееся на другом конце, который мо- жет располагаться на другой машине. Ядро обеспечивает внутреннюю связь и передает данные от клиента к серверу. Гнезда, обладающие одинаковыми свойствами, например, опираю- щиеся на общие соглашения по идентификации и форматы адресов (в протоколах), группируются в домены (управляемые одним узлом). В системе BSD 4.2 поддерживаются домены: "UNIX system" - для взаи- модействия процессов внутри одной машины и "Internet" (межсете- вой) - для взаимодействия через сеть с помощью протокола DARPA (Управление перспективных исследований и разработок Министерства обороны США) (см. [Postel 80] и [Postel 81]). Гнезда бывают двух типов: виртуальный канал (потоковое гнездо, если пользоваться терминологией Беркли) и дейтаграмма. Виртуальный канал обеспечи- вает надежную доставку данных с сохранением исходной последова- тельности. Дейтаграммы не гарантируют надежную доставку с сохра- нением уникальности и последовательности, но они более экономны в смысле использования ресурсов, поскольку для них не требуются сложные установочные операции; таким образом, дейтаграммы полезны в отдельных случаях взаимодействия. Для каждой допустимой комби- нации типа домен-гнездо в системе поддерживается умолчание на ис- пользуемый протокол. Так, например, для домена "Internet" услуги виртуального канала выполняет протокол транспортной связи (TCP), а функции дейтаграммы - пользовательский дейтаграммный протокол (UDP). Существует несколько системных функций работы с гнездами. Функция socket устанавливает оконечную точку линии связи. sd = socket(format,type,protocol); Format обозначает домен ("UNIX system" или "Internet"), type - тип связи через гнездо (виртуальный канал или дейтаграмма), а protocol - тип протокола, управляющего взаимодействием. Дескрип- тор гнезда sd, возвращаемый функцией socket, используется другими системными функциями. Закрытие гнезд выполняет функция close. Функция bind связывает дескриптор гнезда с именем: bind(sd,address,length); где sd - дескриптор гнезда, address - адрес структуры, определяю- щей идентификатор, характерный для данной комбинации домена и протокола (в функции socket). Length - длина структуры address; без этого параметра ядро не знало бы, какова длина структуры, поскольку для разных доменов и протоколов она может быть различ- ной. Например, для домена "UNIX system" структура содержит имя файла. Процессы-серверы связывают гнезда с именами и объявляют о состоявшемся присвоении имен процессам-клиентам. С помощью системной функции connect делается запрос на подк- лючение к существующему гнезду: connect(sd,address,length); Семантический смысл параметров функции остается прежним (см. функцию bind), но address указывает уже на выходное гнездо, обра- зующее противоположный конец линии связи. Оба гнезда должны ис- пользовать одни и те же домен и протокол связи, и тогда ядро удостоверит правильность установки линии связи. Если тип гнезда - дейтаграмма, сообщаемый функцией connect ядру адрес будет исполь- зоваться в последующих обращениях к функции send через данное гнездо; в момент вызова никаких соединений не производится. Пока процесс-сервер готовится к приему связи по виртуальному каналу, ядру следует выстроить поступающие запросы в очередь на обслуживание. Максимальная длина очереди задается с помощью сис- темной функции listen: listen(sd,qlength) где sd - дескриптор гнезда, а qlength - максимально-допустимое число запросов, ожидающих обработки. здддддддддддддддддддд© зддддддддддддддддддддддддд© Ё Процесс-клиент Ё Ё Процесс-сервер Ё Ё Ё Ё Ё Ё Ы Ё Ё Ё Ё Ё здддды ЫЫЫЫЫЫ Ё Ё Ё Ё Ё Ё Ы Ё Ё Ё Ё Ёlisten addr accept addrЁ юдддддддддедддддддддды юдддддеддддддддддддЫдддддды Ё Ё Ы юддддддддддддддддддддддддддыЫЫЫЫЫЫЫЫЫЫЫЫЫ Рисунок 11.19. Прием вызова сервером Системная функция accept принимает запросы на подключение, поступающие на вход процесса-сервера: nsd = accept(sd,address,addrlen); где sd - дескриптор гнезда, address - указатель на пользователь- ский массив, в котором ядро возвращает адрес подключаемого клиен- та, addrlen - размер пользовательского массива. По завершении вы- полнения функции ядро записывает в переменную addrlen размер пространства, фактически занятого массивом. Функция возвращает новый дескриптор гнезда (nsd), отличный от дескриптора sd. Про- цесс-сервер может продолжать слежение за состоянием объявленного гнезда, поддерживая связь с клиентом по отдельному каналу (Рису- нок 11.19). Функции send и recv выполняют передачу данных через подклю- ченное гнездо. Синтаксис вызова функции send: count = send(sd,msg,length,flags); где sd - дескриптор гнезда, msg - указатель на посылаемые данные, length - размер данных, count - количество фактически переданных байт. Параметр flags может содержать значение SOF_OOB (послать данные out-of-band - "через таможню"), если посылаемые данные не учитываются в общем информационном обмене между взаимодействующи- ми процессами. Программа удаленной регистрации, например, может послать out-of-band сообщение, имитирующее нажатие на клавиатуре терминала клавиши "delete". Синтаксис вызова системной функции recv: count = recv(sd,buf,length,flags); где buf - массив для приема данных, length - ожидаемый объем дан- ных, count - количество байт, фактически переданных пользователь- ской программе. Флаги (flags) могут быть установлены таким обра- зом, что поступившее сообщение после чтения и анализа его содер- жимого не будет удалено из очереди, или настроены на получение данных out-of-band. В дейтаграммных версиях указанных функций, sendto и recvfrom, в качестве дополнительных параметров указыва- ются адреса. После выполнения подключения к гнездам потокового типа процессы могут вместо функций send и recv использовать функ- ции read и write. Таким образом, согласовав тип протокола, серве- ры могли бы порождать процессы, работающие только с функциями read и write, словно имеют дело с обычными файлами. Функция shutdown закрывает гнездовую связь: shutdown(sd,mode) где mode указывает, какой из сторон (посылающей, принимающей или обеим вместе) отныне запрещено участие в процессе передачи дан- ных. Функция сообщает используемому протоколу о завершении сеанса сетевого взаимодействия, оставляя, тем не менее, дескрипторы гнезд в неприкосновенности. Освобождается дескриптор гнезда толь- ко в результате выполнения функции close. Системная функция getsockname получает имя гнездовой связи, установленной ранее с помощью функции bind: getsockname(sd,name,length); Функции getsockopt и setsockopt получают и устанавливают зна- чения различных связанных с гнездом параметров в соответствии с типом домена и протокола. Рассмотрим обслуживающую программу, представленную на Рисунке 11.20. Процесс создает в домене "UNIX system" гнездо потокового типа и присваивает ему имя sockname. Затем с помощью функции listen устанавливается длина очереди поступающих сообщений и на- чинается цикл ожидания поступления запросов. Функция accept при- останавливает свое выполнение до тех пор, пока протоколом не бу- дет зарегистрирован запрос на подключение к гнезду с означенным именем; после этого функция завершается, возвращая поступившему запросу новый дескриптор гнезда. Процесс-сервер порождает потом- ка, через которого будет поддерживаться связь с процессом-клиен- том; родитель и потомок при этом закрывают свои дескрипторы, что- бы они не становились помехой для коммуникационного траффика другого процесса. Процесс-потомок ведет разговор с клиентом и за- вершается после выхода из функции read. Процесс-сервер возвраща- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё #include Ё Ё Ё Ё main() Ё Ё { Ё Ё int sd,ns; Ё Ё char buf[256]; Ё Ё struct sockaddr sockaddr; Ё Ё int fromlen; Ё Ё Ё Ё sd = socket(AF_UNIX,SOCK_STREAM,0); Ё Ё Ё Ё /* имя гнезда - не может включать пустой символ */ Ё Ё bind(sd,"sockname",sizeof("sockname") - 1); Ё Ё listen(sd,1); Ё Ё Ё Ё for (;;) Ё Ё { Ё Ё Ё Ё ns = accept(sd,&sockaddr,&fromlen); Ё Ё if (fork() == 0) Ё Ё { Ё Ё /* потомок */ Ё Ё close(sd); Ё Ё read(ns,buf,sizeof(buf)); Ё Ё printf("сервер читает '%s'\n",buf); Ё Ё exit(); Ё Ё } Ё Ё close(ns); Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.20. Процесс-сервер в домене "UNIX system" ется к началу цикла и ждет поступления следующего запроса на подключение. На Рисунке 11.21 показан пример процесса-клиента, ведущего общение с сервером. Клиент создает гнездо в том же домене, что и сервер, и посылает запрос на подключение к гнезду с именем sockname. В результате подключения процесс-клиент получает вирту- альный канал связи с сервером. В рассматриваемом примере клиент передает одно сообщение и завершается. Если сервер обслуживает процессы в сети, указание о том, что гнездо принадлежит домену "Internet", можно сделать следующим об- разом: socket(AF_INET,SOCK_STREAM,0); здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё #include Ё Ё Ё Ё main() Ё Ё { Ё Ё int sd,ns; Ё Ё char buf[256]; Ё Ё struct sockaddr sockaddr; Ё Ё int fromlen; Ё Ё Ё Ё sd = socket(AF_UNIX,SOCK_STREAM,0); Ё Ё Ё Ё /* имя в запросе на подключение не может включать Ё Ё /* пустой символ */ Ё Ё if (connect(sd,"sockname",sizeof("sockname") - 1) == -1)Ё Ё exit(); Ё Ё Ё Ё write(sd,"hi guy",6); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 11.21. Процесс-клиент в домене "UNIX system" и связаться с сетевым адресом, полученным от сервера. В системе BSD имеются библиотечные функции, выполняющие эти действия. Вто- рой параметр вызываемой клиентом функции connect содержит адрес- ную информацию, необходимую для идентификации машины в сети (или адреса маршрутов посылки сообщений через промежуточные машины), а также дополнительную информацию, идентифицирующую приемное гнездо машины-адресата. Если серверу нужно одновременно следить за сос- тоянием сети и выполнением локальных процессов, он использует два гнезда и с помощью функции select определяет, с каким клиентом устанавливается связь в данный момент. 11.5 ВЫВОДЫ Мы рассмотрели несколько форм взаимодействия процессов. Пер- вой формой, положившей начало обсуждению, явилась трассировка процессов - взаимодействие двух процессов, выступающее в качестве полезного средства отладки программ. При всех своих преимуществах трассировка процессов с помощью функции ptrace все же достаточно дорогостоящее и примитивное мероприятие, поскольку за один сеанс функция способна передать строго ограниченный объем данных, тре- буется большое количество переключений контекста, взаимодействие ограничивается только формой отношений родитель-потомок, и нако- нец, сама трассировка производится только по обоюдному согласию участвующих в ней процессов. В версии V системы UNIX имеется па- кет взаимодействия процессов (IPC), включающий в себя механизмы обмена сообщениями, работы с семафорами и разделения памяти. К сожалению, все эти механизмы имеют узкоспециальное назначение, не имеют хорошей стыковки с другими элементами операционной системы и не действуют в сети. Тем не менее, они используются во многих приложениях и по сравнению с другими схемами отличаются более вы- сокой эффективностью. Система UNIX поддерживает широкий спектр вычислительных се- тей. Традиционные методы согласования протоколов в сильной степе- ни полагаются на помощь системной функции ioctl, однако в разных типах сетей они реализуются по-разному. В системе BSD имеются системные функции для работы с гнездами, поддерживающие более универсальную структуру сетевого взаимодействия. В будущем в вер- сию V предполагается включить описанный в главе 10 потоковый ме- ханизм, повышающий согласованность работы в сети. 11.6 УПРАЖНЕНИЯ 1. Что произойдет в том случае, если в программе debug будет отсутствовать вызов функции wait (Рисунок 11.3) ? (Намек: возможны два исхода.) 2. С помощью функции ptrace отладчик считывает данные из прост- ранства трассируемого процесса по одному слову за одну опе- рацию. Какие изменения следует произвести в ядре операцион- ной системы для того, чтобы увеличить количество считываемых слов ? Какие изменения при этом необходимо сделать в самой функции ptrace ? 3. Расширьте область действия функции ptrace так, чтобы в ка- честве параметра pid можно было указывать идентификатор про- цесса, не являющегося потомком текущего процесса. Подумайте над вопросами, связанными с защитой информации: При каких обстоятельствах процессу может быть позволено читать данные из адресного пространства другого, произвольного процесса ? При каких обстоятельствах разрешается вести запись в адрес- ное пространство другого процесса ? 4. Организуйте из функций работы с сообщениями библиотеку поль- зовательского уровня с использованием обычных файлов, поиме- нованных каналов и элементов блокировки. Создавая очередь сообщений, откройте управляющий файл для записи в него ин- формации о состоянии очереди; защитите файл с помощью средств захвата файлов и других удобных для вас механизмов. Посылая сообщение данного типа, создавайте поименованный ка- нал для всех сообщений этого типа, если такого канала еще не было, и передавайте сообщение через него (с подсчетом пере- данных байт). Управляющий файл должен соотносить тип сообще- ния с именем поименованного канала. При чтении сообщений уп- равляющий файл направляет процесс к соответствующему поиме- нованному каналу. Сравните эту схему с механизмом, описанным в настоящей главе, по эффективности, сложности реализации и функциональным возможностям. 5. Какие действия пытается выполнить программа, представленная на Рисунке 11.22 ? *6. Напишите программу, которая подключала бы область разделяе- мой памяти слишком близко к вершине стека задачи и позволяла бы стеку при увеличении пересекать границу разделяемой об- ласти. В какой момент произойдет фатальная ошибка памяти ? 7. Используйте в программе, представленной на Рисунке 11.14, флаг IPC_NOWAIT, реализуя условный тип семафора. Продемонс- трируйте, как за счет этого можно избежать возникновения взаимных блокировок. 8. Покажите, как операции над семафорами типа P и V реализуются при работе с поименованными каналами. Как бы вы реализовали операцию P условного типа ? 9. Составьте программы захвата ресурсов, использующие (а) пои- менованные каналы, (б) системные функции creat и unlink, (в) функции обмена сообщениями. Проведите сравнительный анализ их эффективности. 10. На практических примерах работы с поименованными каналами сравните эффективность использования функций обмена сообще- ниями, с одной стороны, с функциями read и write, с другой. 11. Сравните на конкретных программах скорость передачи данных при работе с разделяемой памятью и при использовании меха- низма обмена сообщениями. Программы, использующие разделяе- мую память, для синхронизации завершения операций чтения-за- писи должны опираться на семафоры. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё #include Ё Ё #include Ё Ё #include Ё Ё #define ALLTYPES 0 Ё Ё Ё Ё main() Ё Ё { Ё Ё struct msgform Ё Ё { Ё Ё long mtype; Ё Ё char mtext[1024]; Ё Ё } msg; Ё Ё register unsigned int id; Ё Ё Ё Ё for (id = 0; ; id++) Ё Ё while (msgrcv(id,&msg,1024,ALLTYPES,IPC_NOWAIT) > 0)Ё Ё ; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды МНОГОПРОЦЕССОРНЫЕ СИСТЕМЫ В классической постановке для системы UNIX предполагается ис- пользование однопроцессорной архитектуры, состоящей из одного ЦП, памяти и периферийных устройств. Многопроцессорная архитектура, напротив, включает в себя два и более ЦП, совместно использующих общую память и периферийные устройства (Рисунок 12.1), располагая большими возможностями в увеличении производительности системы, связанными с одновременным исполнением процессов на разных ЦП. Каждый ЦП функционирует независимо от других, но все они работают с одним и тем же ядром операционной системы. Поведение процессов в такой системе ничем не отличается от поведения в однопроцессор- ной системе - с сохранением семантики обращения к каждой систем- ной функции - но при этом они могут открыто перемещаться с одного процессора на другой. Хотя, к сожалению, это не приводит к сниже- нию затрат процессорного времени, связанного с выполнением процес- са. Отдельные многопроцессорные системы называются системами с присоединенными процессорами, поскольку в них периферийные уст- ройства доступны не для всех процессоров. За исключением особо оговоренных случаев, в настоящей главе не проводится никаких раз- личий между системами с присоединенными процессорами и остальными классами многопроцессорных систем. Параллельная работа нескольких процессоров в режиме ядра по выполнению различных процессов создает ряд проблем, связанных с сохранением целостности данных и решаемых благодаря использованию соответствующих механизмов защиты. Ниже будет показано, почему классический вариант системы UNIX не может быть принят в многоп- роцессорных системах без внесения необходимых изменений, а также будут рассмотрены два варианта, предназначенные для работы в ука- занной среде. зддддддддддд© зддддддддддд© зддддддддддд© Ё Процессор Ё Ё Процессор Ё Ё Процессор Ё Ё 1 Ё Ё 2 Ё ........... Ё n Ё юдддддбддддды юдддддбддддды юдддддбддддды Ё Ё Ё ддддддддддадддддддбдддддддаддддддддддддддбддддддддддддаддддддддддд Ё Ё здддадддд© здддддддддддаддддддддддддд© Ё Память Ё Ё Периферийные устройства Ё юдддддддды юддддддддддддддддддддддддды Рисунок 12.1. Многопроцессорная конфигурация 12.1 ПРОБЛЕМЫ, СВЯЗАННЫЕ С МНОГОПРОЦЕССОРНЫМИ СИСТЕМАМИ В главе 2 мы говорили о том, что защита целостности структур данных ядра системы UNIX обеспечивается двумя способами: ядро не может выгрузить один процесс и переключиться на контекст другого, если работа производится в режиме ядра, кроме того, если при вы- полнении критического участка программы обработчик возникающих прерываний может повредить структуры данных ядра, все возникающие прерывания тщательно маскируются. В многопроцессорной системе, однако, если два и более процессов выполняются одновременно в ре- жиме ядра на разных процессорах, нарушение целостности ядра может произойти даже несмотря на принятие защитных мер, с другой сторо- ны, в однопроцессорной системе вполне достаточных. зддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё struct queue { Ё Ё Ё Ё } *bp, *bp1; Ё Ё bp1->forp=bp->forp; Ё Ё bp1->backp=bp; Ё Ё bp->forp=bp1; Ё Ё /* рассмотрите возможность переключения контекста в Ё Ё * этом месте */ Ё Ё bp1->forp->backp=bp1; Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 12.2. Включение буфера в список с двойными указателя- ми В качестве примера рассмотрим фрагмент программы из главы 2 (Рисунок 12.2), в котором новая структура данных (указатель bp1) помещается в список после существующей структуры (указатель bp). Предположим, что этот фрагмент выполняется одновременно двумя процессами на разных ЦП, причем процессор A пытается поместить вслед за структурой bp структуру bpA, а процессор B - структуру bpB. По поводу сопоставления быстродействия процессоров не прихо- дится делать никаких предположений: возможен даже наихудший слу- чай, когда процессор B исполняет 4 команды языка Си, прежде чем процессор A исполнит одну. Пусть, например, выполнение программы процессором A приостанавливается в связи с обработкой прерывания. В результате, даже несмотря на блокировку остальных прерываний, целостность данных будет поставлена под угрозу (в главе 2 этот момент уже пояснялся). Ядро обязано удостовериться в том, что такого рода нарушение не сможет произойти. Если вопрос об опасности возникновения нару- шения целостности оставить открытым, как бы редко подобные нару- шения ни случались, ядро утратит свою неуязвимость и его поведе- ние станет непредсказуемым. Избежать этого можно тремя способами: 1. Исполнять все критические операции на одном процессоре, опираясь на стандартные методы сохранения целостности данных в однопроцессорной системе; 2. Регламентировать доступ к критическим участкам программы, ис- пользуя элементы блокирования ресурсов; 3. Устранить конкуренцию за использование структур данных путем соответствующей переделки алгоритмов. Первые два способа здесь мы рассмотрим подробнее, третьему спосо- бу будет посвящено отдельное упражнение. 12.2 ГЛАВНЫЙ И ПОДЧИНЕННЫЙ ПРОЦЕССОРЫ Систему с двумя процессорами, один из которых - главный (master) - может работать в режиме ядра, а другой - подчиненный (slave) - только в режиме задачи, впервые реализовал на машинах типа VAX 11/780 Гобл (см. [Goble 81]). Эта система, реализованная вначале на двух машинах, получила свое дальнейшее развитие в сис- темах с одним главным и несколькими подчиненными процессорами. Главный процессор несет ответственность за обработку всех обраще- ний к операционной системе и всех прерываний. Подчиненные процес- соры ведают выполнением процессов в режиме задачи и информируют главный процессор о всех производимых обращениях к системным функциям. Выбор процессора, на котором будет выполняться данный про- цесс, производится в соответствии с алгоритмом диспетчеризации (Рисунок 12.3). В соответствующей записи таблицы процессов появ- ляется новое поле, в которое записывается идентификатор выбранно- го процессора; предположим для простоты, что он показывает, явля- ется ли процессор главным или подчиненным. Когда процесс производит обращение к системной функции, выполняясь на подчинен- ном процессоре, подчиненное ядро переустанавливает значение поля идентификации процессора таким образом, чтобы оно указывало на главный процессор, и переключает контекст на другие процессы (Ри- сунок 12.4). Главное ядро запускает на выполнение процесс с наи- высшим приоритетом среди тех процессов, которые должны выполнять- ся на главном процессоре. Когда выполнение системной функции за- вершается, поле идентификации процессора перенастраивается обрат- но, и процесс вновь возвращается на подчиненный процессор. Если процессы должны выполняться на главном процессоре, жела- тельно, чтобы главный процессор обрабатывал их как можно скорее и не заставлял их ждать своей очереди чересчур долго. Похожая моти- вировка приводится в объяснение выгрузки процесса из памяти в од- нопроцессорной системе после выхода из системной функции с осво- бождением соответствующих ресурсов для выполнения более насущных счетных операций. Если в тот момент, когда подчиненный процессор делает запрос на исполнение системной функции, главный процесс выполняется в режиме задачи, его выполнение будет продолжаться до следующего переключения контекста. Главный процессор реагировал бы гораздо быстрее, если бы подчиненный процессор устанавливал при этом глобальный флаг; проверяя установку флага во время обра- ботки очередного прерывания по таймеру, главный процессор произ- вел бы в итоге переключение контекста максимум через один таймер- ный тик. С другой стороны, подчиненный процессор мог бы прервать здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм schedule_process (модифицированный) Ё Ё входная информация: отсутствует Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё выполнять пока (для запуска не будет выбран один из про-Ё Ё цессов) Ё Ё { Ё Ё если (работа ведется на главном процессоре) Ё Ё для (всех процессов в очереди готовых к выполне- Ё Ё нию) Ё Ё выбрать процесс, имеющий наивысший приоритет Ё Ё среди загруженных в память; Ё Ё в противном случае /* работа ведется на подчинен- Ё Ё * ном процессоре */ Ё Ё для (тех процессов в очереди, которые не нуждают-Ё Ё ся в главном процессоре) Ё Ё выбрать процесс, имеющий наивысший приоритет Ё Ё среди загруженных в память; Ё Ё если (для запуска не подходит ни один из процессов) Ё Ё не загружать машину, переходящую в состояние про-Ё Ё стоя; Ё Ё /* из этого состояния машина выходит в результатеЁ Ё * прерывания */ Ё Ё } Ё Ё убрать выбранный процесс из очереди готовых к выполне- Ё Ё нию; Ё Ё переключиться на контекст выбранного процесса, возобно- Ё Ё вить его выполнение; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 12.3. Алгоритм диспетчеризации здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм syscall /* исправленный алгоритм вызова систем- Ё Ё * ной функции */ Ё Ё входная информация: код системной функции Ё Ё выходная информация: результат выполнения системной функцииЁ Ё { Ё Ё если (работа ведется на подчиненном процессоре) Ё Ё { Ё Ё переустановить значение поля идентификации процессо-Ё Ё ра в соответствующей записи таблицы процессов; Ё Ё произвести переключение контекста; Ё Ё } Ё Ё выполнить обычный алгоритм реализации системной функции;Ё Ё перенастроить значение поля идентификации процессора, Ё Ё чтобы оно указывало на "любой" (подчиненный); Ё Ё если (на главном процессоре должны выполняться другие Ё Ё процессы) Ё Ё произвести переключение контекста; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 12.4. Алгоритм обработки обращения к системной функции работу главного и заставить его переключить контекст немедленно, но данная возможность требует специальной аппаратной реализации. Программа обработки прерываний по таймеру на подчиненном про- цессоре следит за периодичностью перезапуска процессов, не допус- кая монопольного использования процессора одной задачей. Кроме того, каждую секунду эта программа выводит подчиненный процессор из состояния бездействия (простоя). Подчиненный процессор выбира- ет для выполнения процесс с наивысшим приоритетом среди тех про- цессов, которые не нуждаются в главном процессоре. Единственным местом, где целостность структур данных ядра еще подвергается опасности, является алгоритм диспетчеризации, пос- кольку он не предохраняет от выбора процесса на выполнение сразу на двух процессорах. Например, если в конфигурации имеется один главный процессор и два подчиненных, не исключена возможность то- го, что оба подчиненных процессора выберут для выполнения в режи- ме задачи один и тот же процесс. Если оба процессора начнут вы- полнять его параллельно, осуществляя чтение и запись, это неиз- бежно приведет к искажению содержимого адресного пространства процесса. Избежать возникновения этой проблемы можно двумя способами. Во-первых, главный процессор может явно указать, на каком из под- чиненных процессоров следует выполнять данный процесс. Если на каждый процессор направлять несколько процессов, возникает необ- ходимость в сбалансировании нагрузки (на один из процессоров наз- начается большое количество процессов, в то время как другие про- цессоры простаивают). Задача распределения нагрузки между процес- сорами ложится на главное ядро. Во-вторых, ядро может проследить за тем, чтобы в каждый момент времени в алгоритме диспетчеризации принимал участие только один процессор, для этого используются механизмы, подобные семафорам. 12.3 СЕМАФОРЫ Поддержка системы UNIX в многопроцессорной конфигурации может включать в себя разбиение ядра системы на критические участки, параллельное выполнение которых на нескольких процессорах не до- пускается. Такие системы предназначались для работы на машинах AT&T 3B20A и IBM 370, для разбиения ядра использовались семафоры (см. [Bach 84]). Нижеследующие рассуждения помогают понять суть данной особенности. При ближайшем рассмотрении сразу же возникают два вопроса: как использовать семафоры и где определить критичес- кие участки. Как уже говорилось в главе 2, если при выполнении критическо- го участка программы процесс приостанавливается, для защиты участка от посягательств со стороны других процессов алгоритмы работы ядра однопроцессорной системы UNIX используют блокировку. Механизм установления блокировки: выполнять пока (блокировка установлена) /* операция проверки*/ приостановиться (до снятия блокировки); установить блокировку; механизм снятия блокировки: снять блокировку; вывести из состояния приостанова все процессы, приостановлен- ные в результате блокировки; Блокировки такого рода охватывают некоторые критические участки, но не работают в многопроцессорных системах, что видно из Рисунка 12.5. Предположим, что блокировка снята и что два про- Процесс A/Процессор A Процесс B/Процессор B зддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё зддддддддддддддддддддддддддд© Ё Ё Блокировка НЕ установлена Ё Ё Ы юддддддддддддддддддддддддддды Ы Ё Ы Ы Ё Ы Ы Ё Проверяет, установлена Проверяет, установлена Ё ли блокировка ли блокировка Ё (нет) (нет) t - е - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Ё Устанавливает Устанавливает Ё блокировку блокировку Ё Ё Использует ресурс Использует ресурс v ^ ^ Время Ё Ё юдддддд© здддддды Опасность нарушения целостности Рисунок 12.5. Конкуренция за установку блокировки в многопро- цессорных системах цесса на разных процессорах одновременно пытаются проверить ее наличие и установить ее. В момент t они обнаруживают снятие бло- кировки, устанавливают ее вновь, вступают в критический участок и создают опасность нарушения целостности структур данных ядра. В условии одновременности имеется отклонение: механизм не сработа- ет, если перед тем, как процесс выполняет операцию проверки, ни один другой процесс не выполнил операцию установления блокировки. Если, например, после обнаружения снятия блокировки процессор A обрабатывает прерывание и в этот момент процессор B выполняет проверку и устанавливает блокировку, по выходе из прерывания про- цессор A так же установит блокировку. Чтобы предотвратить возник- новение подобной ситуации, нужно сделать так, чтобы процедура блокирования была неделимой: проверку наличия блокировки и ее ус- тановку следует объединить в одну операцию, чтобы в каждый момент времени с блокировкой имел дело только один процесс. 12.3.1 Определение семафоров Семафор представляет собой обрабатываемый ядром целочисленный объект, для которого определены следующие элементарные (недели- мые) операции: * Инициализация семафора, в результате которой семафору присва- ивается неотрицательное значение; * Операция типа P, уменьшающая значение семафора. Если значение семафора опускается ниже нулевой отметки, выполняющий опера- цию процесс приостанавливает свою работу; * Операция типа V, увеличивающая значение семафора. Если значе- ние семафора в результате операции становится больше или рав- но 0, один из процессов, приостановленных во время выполнения операции P, выходит из состояния приостанова; * Условная операция типа P, сокращенно CP (conditional P), уменьшающая значение семафора и возвращающая логическое зна- чение "истина" в том случае, когда значение семафора остается положительным. Если в результате операции значение семафора должно стать отрицательным или нулевым, никаких действий над ним не производится и операция возвращает логическое значение "ложь". Определенные таким образом семафоры, безусловно, никак не связаны с семафорами пользовательского уровня, рассмотренными в главе 11. 12.3.2 Реализация семафоров Дийкстра [Dijkstra 65] показал, что семафоры можно реализо- вать без использования специальных машинных инструкций. На Рисун- ке 12.6 представлены реализующие семафоры функции, написанные на языке Си. Функция Pprim блокирует семафор по результатам проверки значений, содержащихся в массиве val; каждый процессор в системе управляет значением одного элемента массива. Прежде чем заблоки- ровать семафор, процессор проверяет, не заблокирован ли уже сема- фор другими процессорами (соответствующие им элементы в массиве val тогда имеют значения, равные 2), а также не предпринимаются ли попытки в данный момент заблокировать семафор со стороны про- цессоров с более низким кодом идентификации (соответствующие им элементы имеют значения, равные 1). Если любое из условий выпол- няется, процессор переустанавливает значение своего элемента в 1 и повторяет попытку. Когда функция Pprim открывает внешний цикл, переменная цикла имеет значение, на единицу превышающее код иден- тификации того процессора, который использовал ресурс последним, тем самым гарантируется, что ни один из процессоров не может мо- нопольно завладеть ресурсом (в качестве доказательства сошлемся на [Dijkstra 65] и [Coffman 73]). Функция Vprim освобождает сема- фор и открывает для других процессоров возможность получения иск- лючительного доступа к ресурсу путем очистки соответствующего те- кущему процессору элемента в массиве val и перенастройки значения lastid. Чтобы защитить ресурс, следует выполнить следующий набор команд: Pprim(семафор); команды использования ресурса; Vprim(семафор); В большинстве машин имеется набор элементарных (неделимых) инструкций, реализующих операцию блокирования более дешевыми средствами, ибо циклы, входящие в функцию Pprim, работают медлен- но и снижают производительность системы. Так, например, в машинах серии IBM 370 поддерживается инструкция compare and swap (срав- нить и переставить), в машине AT&T 3B20 - инструкция read and clear (прочитать и очистить). При выполнении инструкции read and clear процессор считывает содержимое ячейки памяти, очищает ее (сбрасывает в 0) и по результатам сравнения первоначального со- держимого с 0 устанавливает код завершения инструкции. Если ту же инструкцию над той же ячейкой параллельно выполняет еще один про- цессор, один из двух процессоров прочитает первоначальное содер- жимое, а другой - 0: неделимость операции гарантируется аппарат- ным путем. Таким образом, за счет использования данной инструкции функцию Pprim можно было бы реализовать менее сложными средствами (Рисунок 12.7). Процесс повторяет инструкцию read and clear в цикле до тех пор, пока не будет считано значение, отличное от ну- ля. Начальное значение компоненты семафора, связанной с блокиров- кой, должно быть равно 1. Как таковую, данную семафорную конструкцию нельзя реализовать в составе ядра операционной системы, поскольку работающий с ней процесс не выходит из цикла, пока не достигнет своей цели. Если здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё struct semaphore Ё Ё { Ё Ё int val[NUMPROCS]; /* замок---1 элемент на каждый про- Ё Ё /* цессор */ Ё Ё int lastid; /* идентификатор процессора, полу- Ё Ё /* чившего семафор последним */ Ё Ё }; Ё Ё int procid; /* уникальный идентификатор процес- Ё Ё /* сора */ Ё Ё int lastid; /* идентификатор процессора, полу- Ё Ё /* чившего семафор последним */ Ё Ё Ё Ё INIT(semaphore) Ё Ё struct semaphore semaphore; Ё Ё { Ё Ё int i; Ё Ё for (i = 0; i < NUMPROCS; i++) Ё Ё semaphore.val[i] = 0; Ё Ё } Ё Ё Pprim(semaphore) Ё Ё struct semaphore semaphore; Ё Ё { Ё Ё int i,first; Ё Ё Ё Ё loop: Ё Ё first = lastid; Ё Ё semaphore.val[procid] = 1; Ё Ё /* продолжение на следующей странице */ Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 12.6. Реализация семафорной блокировки на Си семафор используется для блокирования структуры данных, процесс, обнаружив семафор заблокированным, приостанавливает свое выполне- ние, чтобы ядро имело возможность переключиться на контекст дру- гого процесса и выполнить другую полезную работу. С помощью функ- ций Pprim и Vprim можно реализовать более сложный набор семафор- ных операций, соответствующий тому составу, который определен в разделе 12.3.1. Для начала дадим определение семафора как структуры, состоя- щей из поля блокировки (управляющего доступом к семафору), значе- ния семафора и очереди процессов, приостановленных по семафору. Поле блокировки содержит информацию, открывающую во время выпол- нения операций типа P и V доступ к другим полям структуры только одному процессу. По завершении операции значение поля сбрасывает- ся. Это значение определяет, разрешен ли процессу доступ к крити- ческому участку, защищаемому семафором. В начале выполнения алгоритма операции P (Рисунок 12.8) ядро с помощью функции Pprim предоставляет процессу право исключительного доступа к семафору и уменьшает значение семафора. Если семафор имеет неотрицательное значение, текущий процесс получает доступ к критическому участку. По завершении работы процесс сбрасывает блокировку семафора (с помощью функции Vprim), открывая доступ к семафору для других процессов, и возвращает признак успешного завершения. Если же в результате уменьшения значение семафора становится отрицательным, ядро приостанавливает выполнение процесса, используя алгоритм, здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё forloop: Ё Ё for (i = first; i < NUMPROCS; i++) Ё Ё { Ё Ё if (i == procid) Ё Ё { Ё Ё semaphore.val[i] = 2; Ё Ё for (i = 1; i < NUMPROCS; i++) Ё Ё if (i != procid && semaphore.val[i] == 2) Ё Ё goto loop; Ё Ё lastid = procid; Ё Ё return; /* успешное завершение, ресурс Ё Ё /* можно использовать Ё Ё */ Ё Ё } Ё Ё else if (semaphore.val[i]) Ё Ё goto loop; Ё Ё } Ё Ё first = 1; Ё Ё goto forloop; Ё Ё } Ё Ё Vprim(semaphore) Ё Ё struct semaphore semaphore; Ё Ё { Ё Ё lastid = (procid + 1) % NUMPROCS; /* на следующий Ё Ё /* процессор */ Ё Ё semaphore.val[procid] = 0; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 12.6. Реализация семафорной блокировки на Си (про- должение) подобный алгоритму sleep (глава 6): основываясь на значении прио- ритета, ядро проверяет поступившие сигналы, включает текущий про- цесс в список приостановленных процессов, в котором последние представлены в порядке поступления, и выполняет переключение кон- текста. Операция V (Рисунок 12.9) получает исключительный доступ к семафору через функцию Pprim и увеличивает значение семафора. Если очередь приостановленных по семафору процессов непуста, ядро выбирает из нее первый процесс и переводит его в состояние "го- товности к запуску". Операции P и V по своему действию похожи на функции sleep и wakeup. Главное различие между ними состоит в том, что семафор является структурой данных, тогда как используемый функциями sleep и wakeup адрес представляет собой всего лишь число. Если начальное значение семафора - нулевое, при выполнении операции P над семафором процесс всегда приостанавливается, поэтому операция P может заменять функцию sleep. Операция V, тем не менее, выводит из состояния приостанова только один процесс, тогда как однопро- цессорная функция wakeup возобновляет все процессы, приостанов- ленные по адресу, связанному с событием. С точки зрения семантики использование функции wakeup означа- ет: данное системное условие более не удовлетворяется, следова- тельно, все приостановленные по условию процессы должны выйти из состояния приостанова. Так, например, процессы, приостановленные в связи с занятостью буфера, не должны дальше пребывать в этом состоянии, если буфер больше не используется, поэтому они возоб- зддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё struct semaphore { Ё Ё int lock; Ё Ё }; Ё Ё Ё Ё Init(semaphore) Ё Ё struct semaphore semaphore; Ё Ё { Ё Ё semaphore.lock = 1; Ё Ё } Ё Ё Ё Ё Pprim(semaphore) Ё Ё struct semaphore semaphore; Ё Ё { Ё Ё while (read_and_clear(semaphore.lock)) Ё Ё ; Ё Ё } Ё Ё Ё Ё Vprim(semaphore) Ё Ё struct semaphore semaphore; Ё Ё { Ё Ё semaphore.lock = 1; Ё Ё } Ё юддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 12.7. Операции над семафором, использующие инструкцию read and clear новляются ядром. Еще один пример: если несколько процессов выво- дят данные на терминал с помощью функции write, терминальный драйвер может перевести их в состояние приостанова в связи с не- возможностью обработки больших объемов информации. Позже, когда драйвер будет готов к приему следующей порции данных, он возобно- вит все приостановленные им процессы. Использование операций P и V в тех случаях, когда устанавливающие блокировку процессы полу- чают доступ к ресурсу поочередно, а все остальные процессы - в порядке поступления запросов, является более предпочтительным. В сравнении с однопроцессорной процедурой блокирования (sleep-lock) данная схема обычно выигрывает, так как если при наступлении со- бытия все процессы возобновляются, большинство из них может вновь наткнуться на блокировку и снова перейти в состояние приостанова. С другой стороны, в тех случаях, когда требуется вывести из сос- тояния приостанова все процессы одновременно, использование опе- раций P и V представляет известную сложность. Если операция возвращает значение семафора, является ли она эквивалентной функции wakeup ? while (value(semaphore) < 0) V(semaphore); Если вмешательства со стороны других процессов нет, ядро пов- торяет цикл до тех пор, пока значение семафора не станет больше или равно 0, ибо это означает, что в состоянии приостанова по се- мафору нет больше ни одного процесса. Тем не менее, нельзя исклю- здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм P /* операция над семафором типа P */ Ё Ё входная информация: (1) семафор Ё Ё (2) приоритет Ё Ё выходная информация: 0 - в случае нормального завершения Ё Ё -1 - в случае аварийного выхода из Ё Ё состояния приостанова по сигналу, при-Ё Ё нятому в режиме ядра Ё Ё { Ё Ё Pprim(semaphore.lock); Ё Ё уменьшить (semaphore.value); Ё Ё если (semaphore.value >= 0) Ё Ё { Ё Ё Vprim(semaphore.lock); Ё Ё вернуть (0); Ё Ё } Ё Ё /* следует перейти в состояние приостанова */ Ё Ё если (проверяются сигналы) Ё Ё { Ё Ё если (имеется сигнал, прерывающий нахождение в сос- Ё Ё тоянии приостанова) Ё Ё { Ё Ё увеличить (semaphore.value); Ё Ё если (сигнал принят в режиме ядра) Ё Ё { Ё Ё Vprim(semaphore.lock); Ё Ё вернуть (-1); Ё Ё } Ё Ё в противном случае Ё Ё { Ё Ё Vprim(semaphore.lock); Ё Ё longjmp; Ё Ё } Ё Ё } Ё Ё } Ё Ё поставить процесс в конец списка приостановленных по се-Ё Ё мафору; Ё Ё Vprim(semaphore.lock); Ё Ё выполнить переключение контекста; Ё Ё проверить сигналы (см. выше); Ё Ё вернуть (0); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 12.8. Алгоритм выполнения операции P чить и такую возможность, что сразу после того, как процесс A при тестировании семафора на одноименном процессоре обнаружил нулевое значение семафора, процесс B на своем процессоре выполняет опера- цию P, уменьшая значение семафора до -1 (Рисунок 12.10). Процесс A продолжит свое выполнение, думая, что им возобновлены все при- остановленные по семафору процессы. Таким образом, цикл выполне- ния операции не дает гарантии возобновления всех приостановленных процессов, поскольку он не является элементарным. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм V /* операция над семафором типа V */ Ё Ё входная информация: адрес семафора Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё Pprim(semaphore.lock); Ё Ё увеличить (semaphore.value); Ё Ё если (semaphore.value <= 0) Ё Ё { Ё Ё удалить из списка процессов, приостановленных по се-Ё Ё мафору, первый по счету процесс; Ё Ё перевести его в состояние готовности к запуску; Ё Ё } Ё Ё Vprim(semaphore.lock); Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 12.9. Алгоритм выполнения операции V Рассмотрим еще один феномен, связанный с использованием сема- форов в однопроцессорной системе. Предположим, что два процесса, A и B, конкурируют за семафор. Процесс A обнаруживает, что сема- фор свободен и что процесс B приостановлен; значение семафора равно -1. Когда с помощью операции V процесс A освобождает сема- фор, он выводит тем самым процесс B из состояния приостанова и вновь делает значение семафора нулевым. Теперь предположим, что процесс A, по-прежнему выполняясь в режиме ядра, пытается снова заблокировать семафор. Производя операцию P, процесс приостано- вится, поскольку семафор имеет нулевое значение, несмотря на то, что ресурс пока свободен. Системе придется "раскошелиться" на до- полнительное переключение контекста. С другой стороны, если бы блокировка была реализована на основе однопроцессорной схемы (sleep-lock), процесс A получил бы право на повторное использова- ние ресурса, поскольку за это время ни один из процессов не смог бы заблокировать его. Для этого случая схема sleep-lock более подходит, чем схема с использованием семафоров. Когда блокируются сразу несколько семафоров, очередность бло- кирования должна исключать возникновение тупиковых ситуаций. В качестве примера рассмотрим два семафора, A и B, и два алгоритма, требующих одновременной блокировки семафоров. Если бы алгоритмы устанавливали блокировку на семафоры в обратном порядке, как сле- дует из Рисунка 12.11, последовало бы возникновение тупиковой си- туации; процесс A на одноименном процессоре захватывает семафор SA, в то время как процесс B на своем процессоре захватывает се- мафор SB. Процесс A пытается захватить и семафор SB, но в резуль- тате операции P переходит в состояние приостанова, поскольку зна- чение семафора SB не превышает 0. То же самое происходит с про- цессом B, когда последний пытается захватить семафор SA. Ни тот, ни другой процессы продолжаться уже не могут. Для предотвращения возникновения подобных ситуаций использу- ются соответствующие алгоритмы обнаружения опасности взаимной блокировки, устанавливающие наличие опасной ситуации и ликвидиру- ющие ее. Тем не менее, использование таких алгоритмов "утяжеляет" ядро. Поскольку число ситуаций, в которых процесс должен одновре- менно захватывать несколько семафоров, довольно ограничено, легче было бы реализовать алгоритмы, предупреждающие возникновение ту- пиковых ситуаций еще до того, как они будут иметь место. Если, к примеру, какой-то набор семафоров всегда блокируется в одном и том же порядке, тупиковая ситуация никогда не возникнет. Но в том случае, когда захвата семафоров в обратном порядке избежать не Процесс A/Процессор A Процесс B/Процессор B зддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё Ы здддддддддддддддддддддддд© Ы Ё Ы Ё Значение семафора = -1 Ё Ы Ё Ы юдддддддддддддддддддддддды Ы Ё проверяет(значение сема- Ы Ё фора < 0) ? Ы Ё (да) Ы Ё V(семафор) Ы Ё Ы Ы Ё Ы здддддддддддддддддддддддд© Ы Ё Ы Ё Значение семафора = 0 Ё Ы Ё Ы юдддддддддддддддддддддддды Ы Ё проверяет(значение сема- Ы Ё фора < 0) ? Ы Ё Ы Ы Ё Ы P(семафор) Ё Ы Значение семафора = -1 Ё Ы Ё (нет) Ё НЕВЕРНО !! v Время Рисунок 12.10. Неудачное имитация функции wakeup при исполь- зовании операции V удается, операция CP предотвратит возникновение тупиковой ситуа- ции (см. Рисунок 12.12): если операция завершится неудачно, про- цесс B освободит свои ресурсы, дабы избежать взаимной блокировки, и позже запустит алгоритм на выполнение повторно, скорее всего тогда, когда процесс A завершит работу с ресурсом. Чтобы предупредить одновременное обращение процессов к ресур- су, программа обработки прерываний, казалось бы, могла воспользо- ваться семафором, но из-за того, что она не может приостанавли- вать свою работу (см. главу 6), использовать операцию P в этой программе нельзя. Вместо этого можно использовать "циклическую блокировку" (spin lock) и не переходить в состояние приостанова, как в следующем примере: while (! CP(semaphore)); Операция повторяется в цикле до тех пор, пока значение семафора не превысит 0; программа обработки прерываний не приостанавлива- ется и цикл завершается только тогда, когда значение семафора станет положительным, после чего это значение будет уменьшено операцией CP. Чтобы предотвратить ситуацию взаимной блокировки, ядру нужно запретить все прерывания, выполняющие "циклическую блокировку". Иначе выполнение процесса, захватившего семафор, будет прервано еще до того, как он сможет освободить семафор; если программа об- работки прерываний попытается захватить этот семафор, используя "циклическую блокировку", ядро заблокирует само себя. В качестве примера обратимся к Рисунку 12.13. В момент возникновения преры- Процесс A/Процессор A Процесс B/Процессор B зддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё P(семафор SA); Ы Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё Ы P(семафор SB); Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё Ы P(семафор SA); Ё Ы приостанавливается Ё Ы Ё P(семафор SB); Ё приостанавливается Ё v Взаимная блокировка !! Время Рисунок 12.11. Возникновение тупиковой ситуации из-за смены очередности блокирования вания значение семафора не превышает 0, поэтому результатом вы- полнения операции CP всегда будет "ложь". Проблема решается путем запрещения всех прерываний на то время, пока семафор захвачен процессом. 12.3.3 Примеры алгоритмов В данном разделе мы рассмотрим четыре алгоритма ядра, реали- зованных с использованием семафоров. Алгоритм выделения буфера иллюстрирует сложную схему блокирования, на примере алгоритма wait показана синхронизация выполнения процессов, схема блокиро- вания драйверов реализует изящный подход к решению данной пробле- мы, и наконец, метод решения проблемы холостой работы процессора показывает, что нужно сделать, чтобы избежать конкуренции между процессами. 12.3.3.1 Выделение буфера Обратимся еще раз к алгоритму getblk, рассмотренному нами в главе 3. Алгоритм работает с тремя структурами данных: заголовком буфера, хеш-очередью буферов и списком свободных буферов. Ядро связывает семафор со всеми экземплярами каждой структуры. Другими словами, если у ядра имеются в распоряжении 200 буферов, заголо- вок каждого из них включает в себя семафор, используемый для зах- вата буфера; когда процесс выполняет над семафором операцию P, другие процессы, тоже пожелавшие захватить буфер, приостанавлива- ются до тех пор, пока первый процесс не исполнит операцию V. У каждой хеш-очереди буферов также имеется семафор, блокирующий доступ к очереди. В однопроцессорной системе блокировка хеш-оче- Процесс A/Процессор A Процесс B/Процессор B зддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё P(семафор SA); Ы Ё Ы Ы Ё Ы Ы Ё Ы P(семафор SB); Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё Ы если (! CP(семафор SA)) Ё Ы { Ё Ы V(семафор SB); Ё Ы перезапустить алго- Ё Ы ритм Ё Ы } Ё Ы Ё P(семафор SB); Ё приостанавливается v Время Рисунок 12.12. Использование операции P условного типа для предотвращения взаимной блокировки реди не нужна, ибо процесс никогда не переходит в состояние при- останова, оставляя очередь в несогласованном (неупорядоченном) виде. В многопроцессорной системе, тем не менее, возможны ситуа- ции, когда с одной и той же хеш-очередью работают два процесса; в каждый момент времени семафор открывает доступ к очереди только для одного процесса. По тем же причинам и список свободных буфе- ров нуждается в семафоре для защиты содержащейся в нем информации от искажения. На Рисунке 12.14 показана первая часть алгоритма getblk, реа- лизованная в многопроцессорной системе с использованием семафо- ров. Просматривая буферный кеш в поисках указанного блока, ядро с помощью операции P захватывает семафор, принадлежащий хеш-очере- ди. Если над семафором уже кем-то произведена операция данного типа, текущий процесс приостанавливается до тех пор, пока про- цесс, захвативший семафор, не освободит его, выполнив операцию V. Когда текущий процесс получает право исключительного контроля над хеш-очередью, он приступает к поиску подходящего буфера. Предпо- ложим, что буфер находится в хеш-очереди. Ядро (процесс A) пыта- ется захватить буфер, но если оно использует операцию P и если буфер уже захвачен, ядру придется приостановить свою работу, ос- тавив хеш-очередь заблокированной и не допуская таким образом об- ращений к ней со стороны других процессов, даже если последние ведут поиск незахваченных буферов. Пусть вместо этого процесс A захватывает буфер, используя операцию CP; если операция заверша- ется успешно, буфер становится открытым для процесса. Процесс A захватывает семафор, принадлежащий списку свободных буферов, вы- полняя операцию CP, поскольку семафор захватывается на непродол- жительное время и, следовательно, приостанавливать свою работу, выполняя операцию P, процесс просто не имеет возможности. Ядро убирает буфер из списка свободных буферов, снимает блокировку со списка и с хеш-очереди и возвращает захваченный буфер. Ё Ё P(семафор); Ё (Значение семафора теперь равно 0) Ё Ё Прерывание Ё Ё CP(семафор) завершается неудачно --- Ё семафор захвачен Ё Ё Семафор не освобождается до выхода из прерывания. Ё Ё Выход из прерывания без его обработки невозможен. Ё Ё Тупиковая ситуация (взаимная блокировка) v Время Рисунок 12.13. Взаимная блокировка при выполнении программы обработки прерывания Предположим, что операция CP над буфером завершилась неудачно из-за того, что семафор, принадлежащий буферу, оказался захвачен- ным. Процесс A освобождает семафор, связанный с хеш-очередью, и приостанавливается, пытаясь выполнить операцию P над семафором буфера. Операция P над семафором будет выполняться, несмотря на то, что операция CP уже потерпела неудачу. По завершении выполне- ния операции процесс A получает власть над буфером. Так как в ос- тавшейся части алгоритма предполагается, что буфер и хеш-очередь захвачены, процесс A теперь пытается захватить хеш-очередь (*). Поскольку очередность захвата здесь (сначала семафор буфера, по- том семафор очереди) обратна вышеуказанной очередности, над сема- фором выполняется операция CP. Если попытка захвата заканчивается неудачей, имеет место обычная обработка, требующаяся по ходу за- дачи. Но если захват удается, ядро не может быть уверено в том, что захвачен корректный буфер, поскольку содержимое буфера могло быть ранее изменено другим процессом, обнаружившим буфер в списке свободных буферов и захватившим на время его семафор. Процесс A, ожидая освобождения семафора, не имеет ни малейшего представления о том, является ли интересующий его буфер тем буфером, который ему нужен, и поэтому прежде всего он должен убедиться в правиль- ности содержимого буфера; если проверка дает отрицательный ре- зультат, алгоритм запускается сначала. Если содержимое буфера корректно, процесс A завершает выполнение алгоритма. ддддддддддддддддддддддддддддддддддддддд (*) Вместо захвата хеш-очереди в этом месте можно было бы устано- вить соответствующий флаг, проверяемый далее перед выполнени- ем операции V, но чтобы проиллюстрировать схему захвата сема- форов в обратной последовательности, в изложении мы будем придерживаться ранее описанного варианта. здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм getblk /* многопроцессорная версия */ Ё Ё входная информация: номер файловой системы Ё Ё номер блока Ё Ё выходная информация: захваченный буфер, предназначенный дляЁ Ё обработки содержимого блока Ё Ё { Ё Ё выполнять (пока буфер не будет обнаружен) Ё Ё { Ё Ё P(семафор хеш-очереди); Ё Ё если (блок находится в хеш-очереди) Ё Ё { Ё Ё если (операция CP(семафор буфера) завершается не- Ё Ё удачно) /* буфер занят */ Ё Ё { Ё Ё V(семафор хеш-очереди); Ё Ё P(семафор буфера); /* приостанов до момен-Ё Ё * та освобождения Ё Ё */ Ё Ё если (операция CP(семафор хеш-очереди) заверша-Ё Ё ется неудачно) Ё Ё { Ё Ё V(семафор буфера); Ё Ё продолжить; /* выход в цикл "выполнять" Ё Ё */ Ё Ё } Ё Ё в противном случае если (номер устройства или Ё Ё номер блока изменились) Ё Ё { Ё Ё V(семафор буфера); Ё Ё V(семафор хеш-очереди); Ё Ё } Ё Ё } Ё Ё выполнять (пока операция CP(семафор списка свобод-Ё Ё ных буферов) не завершится успешно) Ё Ё ; /* "кольцевой цикл" */ Ё Ё пометить буфер занятым; Ё Ё убрать буфер из списка свободных буферов; Ё Ё V(семафор списка свободных буферов); Ё Ё V(семафор хеш-очереди); Ё Ё вернуть буфер; Ё Ё } Ё Ё в противном случае /* буфер отсутствует в хеш- Ё Ё * очереди Ё Ё */ Ё Ё /* здесь начинается выполнение оставшейся части алго-Ё Ё * ритма Ё Ё */ Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 12.14. Выделение буфера с использованием семафоров здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё многопроцессорная версия алгоритма wait Ё Ё { Ё Ё для (;;) /* цикл */ Ё Ё { Ё Ё перебор всех процессов-потомков: Ё Ё если (потомок находится в состоянии "прекращения Ё Ё существования") Ё Ё вернуть управление; Ё Ё P(zombie_semaphore); /* начальное значение - 0 */Ё Ё } Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 12.15. Многопроцессорная версия алгоритма wait Оставшуюся часть алгоритма можно рассмотреть в качестве уп- ражнения. 12.3.3.2 Wait Из главы 7 мы уже знаем о том, что во время выполнения сис- темной функции wait процесс приостанавливает свою работу до мо- мента завершения выполнения своего потомка. В многопроцессорной системе перед процессом встает задача не упустить при выполнении алгоритма wait потомка, прекратившего существование с помощью функции exit; если, например, в то время, пока на одном процессо- ре процесс-родитель запускает функцию wait, на другом процессоре его потомок завершил свою работу, родителю нет необходимости при- останавливать свое выполнение в ожидании завершения второго по- томка. В каждой записи таблицы процессов имеется семафор, именуе- мый zombie_semaphore и имеющий в начале нулевое значение. Этот семафор используется при организации взаимодействия wait/exit (Рисунок 12.15). Когда потомок завершает работу, он выполняет над семафором своего родителя операцию V, выводя родителя из состоя- ния приостанова, если тот перешел в него во время исполнения функции wait. Если потомок завершился раньше, чем родитель запус- тил функцию wait, этот факт будет обнаружен родителем, который тут же выйдет из состояния ожидания. Если оба процесса исполняют функции exit и wait параллельно, но потомок исполняет функцию exit уже после того, как родитель проверил его статус, операция V, выполненная потомком, воспрепятствует переходу родителя в сос- тояние приостанова. В худшем случае процесс-родитель просто пов- торяет цикл лишний раз. 12.3.3.3 Драйверы В многопроцессорной реализации вычислительной системы на базе компьютеров AT&T 3B20 семафоры в структуру загрузочного кода драйверов не включаются, а операции типа P и V выполняются в точ- ках входа в каждый драйвер (см. [Bach 84]). В главе 10 мы говори- ли о том, что интерфейс, реализуемый драйверами устройств, харак- теризуется очень небольшим числом точек входа (на практике их около 20). Защита драйверов осуществляется на уровне точек входа в них: P(семафор драйвера); открыть (драйвер); V(семафор драйвера); Если для всех точек входа в драйвер использовать один и тот же семафор, но при этом для разных драйверов - разные семафоры, критический участок программы драйвера будет исполняться процес- сом монопольно. Семафоры могут назначаться как отдельному уст- ройству, так и классам устройств. Так, например, отдельный сема- фор может быть связан и с отдельным физическим терминалом и со всеми терминалами сразу. В первом случае быстродействие системы выше, ибо процессы, обращающиеся к терминалу, не захватывают се- мафор, имеющий отношение к другим терминалам, как во втором слу- чае. Драйверы некоторых устройств, однако, поддерживают внутрен- нюю связь с другими драйверами; в таких случаях использование одного семафора для класса устройств облегчает понимание задачи. В качестве альтернативы в вычислительной системе 3B20A предостав- лена возможность такого конфигурирования отдельных устройств, при котором программы драйвера запускаются на точно указанных процес- сорах. Проблемы возникают тогда, когда драйвер прерывает работу сис- темы и его семафор захвачен: программа обработки прерываний не может быть вызвана, так как иначе возникла бы угроза разрушения данных. С другой стороны, ядро не может оставить прерывание необ- работанным. Система 3B20A выстраивает прерывания в очередь и ждет момента освобождения семафора, когда вызов программы обработки прерываний не будет иметь опасные последствия. 12.3.3.4 Фиктивные процессы Когда ядро выполняет переключение контекста в однопроцессор- ной системе, оно функционирует в контексте процесса, уступающего управление (см. главу 6). Если в системе нет процессов, готовых к запуску, ядро переходит в состояние простоя в контексте процесса, выполнявшегося последним. Получив прерывание от таймера или дру- гих периферийных устройств, оно обрабатывает его в контексте того же процесса. В многопроцессорной системе ядро не может простаивать в кон- тексте процесса, выполнявшегося последним. Посмотрим, что прои- зойдет после того, как процесс, приостановивший свою работу на процессоре A, выйдет из состояния приостанова. Процесс в целом готов к запуску, но он запускается не сразу же по выходе из сос- тояния приостанова, даже несмотря на то, что его контекст уже на- ходится в распоряжении процессора A. Если этот процесс выбирается для запуска процессором B, последний переключается на его кон- текст и возобновляет его выполнение. Когда в результате прерыва- ния процессор A выйдет из простоя, он будет продолжать свою рабо- ту в контексте процесса A до тех пор, пока не произведет переклю- чение контекста. Таким образом, в течение короткого промежутка времени с одним и тем же адресным пространством (в частности, со стеком ядра) будут вести работу (и, что весьма вероятно, произво- дить запись) сразу два процессора. Решение этой проблемы состоит в создании некоторого фиктивно- го процесса; когда процессор находится в состоянии простоя, ядро переключается на контекст фиктивного процесса, делая этот кон- текст текущим для бездействующего процессора. Контекст фиктивного процесса состоит только из стека ядра; этот процесс не является выполнимым и не выбирается для запуска. Поскольку каждый процес- сор простаивает в контексте своего собственного фиктивного про- цесса, навредить друг другу процессоры уже не могут. 12.4 СИСТЕМА TUNIS Пользовательский интерфейс системы Tunis совместим с анало- гичным интерфейсом системы UNIX, но ядро этой системы, разрабо- танное на языке Concurrent Euclid, состоит из процессов, управля- ющих каждой частью системы. Проблема взаимного исключения решается в системе Tunis довольно просто, так как в каждый момент времени исполняется не более одной копии управляемого ядром про- цесса, кроме того, процессы работают только с теми структурами данных, которые им принадлежат. Системные процессы активизируются запросами на ввод, защиту очереди запросов осуществляет процедура программного монитора. Эта процедура усиливает взаимное исключе- ние, разрешая доступ к своей исполняемой части в каждый момент времени не более, чем одному процессу. Механизм монитора отлича- ется от механизма семафоров тем, что, во-первых, благодаря пос- ледним усиливается модульность программ (операции P и V присутс- твуют на входе в процедуру монитора и на выходе из нее), а во-вторых, сгенерированный компилятором код уже содержит элементы синхронизации. Холт отмечает, что разработка таких систем облег- чается, если используется язык, поддерживающий мониторы и включа- ющий понятие параллелизма (см. [Holt 83], стр.190). При всем при этом внутренняя структура системы Tunis отличается от традицион- ной реализации системы UNIX радикальным образом. 12.5 УЗКИЕ МЕСТА В ФУНКЦИОНИРОВАНИИ МНОГОПРОЦЕССОРНЫХ СИСТЕМ В данной главе нами были рассмотрены два метода реализации многопроцессорных версий системы UNIX: конфигурация, состоящая из главного и подчиненного процессоров, в которой только один про- цессор (главный) функционирует в режиме ядра, и метод, основанный на использовании семафоров и допускающий одновременное исполнение в режиме ядра всех имеющихся в системе процессов. Оба метода ин- вариантны к количеству процессоров, однако говорить о том, что с ростом числа процессоров общая производительность системы увели- чивается с линейной скоростью, нельзя. Потери производительности возникают, во-первых, как следствие конкуренции за ресурсы памя- ти, которая выражается в увеличении продолжительности обращения к памяти. Во-вторых, в схеме, основанной на использовании семафо- ров, к этой конкуренции добавляется соперничество за семафоры; процессы зачастую обнаруживают семафоры захваченными, больше про- цессов находится в очереди, долгое время ожидая получения доступа к семафорам. Первая схема, основанная на использовании главного и подчиненного процессоров, тоже не лишена недостатков: по мере увеличения числа процессоров главный процессор становится узким местом в системе, поскольку только он один может функционировать в режиме ядра. Несмотря на то, что более внимательное техническое проектирование позволяет сократить конкуренцию до разумного мини- мума и в некоторых случаях приблизить скорость повышения произво- дительности системы при увеличении числа процессоров к линейной (см., например, [Beck 85]), все построенные с использованием сов- ременной технологии многопроцессорные системы имеют предел, за которым расширение состава процессоров не сопровождается увеличе- нием производительности системы. 12.6 УПРАЖНЕНИЯ 1. Решите проблему функционирования многопроцессорных систем таким образом, чтобы все процессоры в системе могли функцио- нировать в режиме ядра, но не более одного одновременно. Та- кое решение будет отличаться от первой из предложенных в тексте схем, где только один процессор (главный) предназна- чен для реализации функций ядра. Как добиться того, чтобы в режиме ядра в каждый момент времени находился только один процессор ? Какую стратегию обработки прерываний при этом можно считать приемлемой ? 2. Используя системные функции работы с разделяемой областью памяти, протестируйте программу, реализующую семафорную бло- кировку (Рисунок 12.6). Последовательности операций P-V над семафором могут независимо один от другого выполнять нес- колько процессов. Каким образом в программе следует реализо- вать индикацию и обработку ошибок ? 3. Разработайте алгоритм выполнения операции CP (условный тип операции P), используя текст алгоритма операции P. 4. Объясните, зачем в алгоритмах операций P и V (Рисунки 12.8 и 12.9) нужна блокировка прерываний. В какие моменты ее следу- ет осуществлять ? 5. Почему при выполнении "циклической блокировки" вместо строки: while (! CP(семафор)); ядро не может использовать операцию P безусловного типа ? (В качестве наводящего вопроса: что произойдет в том случае, если процесс запустит операцию P и приостановится ?) 6. Обратимся к алгоритму getblk, приведенному в главе 3. Опиши- те реализацию алгоритма в многопроцессорной системе для слу- чая, когда блок отсутствует в буферном кеше. *7. Предположим, что при выполнении алгоритма выделения буфера возникла чрезвычайно сильная конкуренция за семафор, принад- лежащий списку свободных буферов. Разработайте схему ослаб- ления конкуренции за счет разбиения списка свободных буферов на два подсписка. *8. Предположим, что у терминального драйвера имеется семафор, значение которого при инициализации сбрасывается в 0 и по которому процессы приостанавливают свою работу в случае пе- реполнения буфера вывода на терминал. Когда терминал готов к приему следующей порции данных, он выводит из состояния ожи- дания все процессы, приостановленные по семафору. Разрабо- тайте схему возобновления процессов, использующую операции типа P и V. В случае необходимости введите дополнительные флаги и семафоры. Как должна вести себя схема в том случае, если процессы выводятся из состояния ожидания по прерыванию, но при этом текущий процессор не имеет возможности блокиро- вать прерывания на других процессорах ? *9. Если точки входа в драйвер защищаются семафорами, должно соблюдаться условие освобождения семафора в случае перехода процесса в состояние приостанова. Как это реализуется на практике ? Каким образом должна производиться обработка пре- рываний, поступающих в то время, пока семафор драйвера заб- локирован ? 10. Обратимся к системным функциям установки и контроля систем- ного времени (глава 8). Разные процессоры могут иметь раз- личную тактовую частоту. Как в этом случае указанные функции должны работать ? РАСПРЕДЕЛЕННЫЕ СИСТЕМЫ В предыдущей главе нами были рассмотрены сильносвязанные мно- гопроцессорные системы с общей памятью, общими структурами данных ядра и общим пулом, из которого процессы вызываются на выполне- ние. Часто, однако, бывает желательно в целях обеспечения сов- местного использования ресурсов распределять процессоры таким об- разом, чтобы они были автономны от операционной среды и условий эксплуатации. Пусть, например, пользователю персональной ЭВМ нуж- но обратиться к файлам, находящимся на более крупной машине, но сохранить при этом контроль над персональной ЭВМ. Несмотря на то, что отдельные программы, такие как uucp, поддерживают передачу файлов по сети и другие сетевые функции, их использование не бу- дет скрыто от пользователя, поскольку пользователь знает о том, что он работает в сети. Кроме того, надо заметить, что программы, подобные текстовым редакторам, с удаленными файлами, как с обыч- ными, не работают. Пользователи должны располагать стандартным набором функций системы UNIX и, за исключением возможной потери в быстродействии, не должны ощущать пересечения машинных границ. Так, например, работа системных функций open и read с файлами на удаленных машинах не должна отличаться от их работы с файлами, принадлежащими локальным системам. Архитектура распределенной системы представлена на Рисунке 13.1. Каждый компьютер, показанный на рисунке, является автоном- ным модулем, состоящим из ЦП, памяти и периферийных устройств. Соответствие модели не нарушается даже несмотря на то, что компь- ютер не располагает локальной файловой системой: он должен иметь периферийные устройства для связи с другими машинами, а все при- надлежащие ему файлы могут располагаться и на ином компьютере. Физическая память, доступная каждой машине, не зависит от процес- сов, выполняемых на других машинах. Этой особенностью распреде- ленные системы отличаются от сильносвязанных многопроцессорных систем, рассмотренных в предыдущей главе. Соответственно, и ядро зддддддддддддддддддддддддддддд© зддддддддддддддддддддддддддддд© Ё здддддддддддд© Ё Ё здддддддддддд© Ё Ё Ё Процессоры Ё Ё Ё Ё Процессоры Ё Ё Ё юдддддбдддддды Ё Ё юдддддбдддддды Ё Ё ддддбдддддддаддддддбддддддд Ё Ё ддддбдддддддаддддддбддддддд Ё Ё здддадддд© здддддддадддддд© Ё Ё здддадддд© здддддддадддддд© Ё Ё Ё Память Ё Ё Периферийные Ё Ё Ё Ё Память Ё Ё Периферийные Ё Ё Ё Ё Ё Ё устройства Ё Ё Ё Ё Ё Ё устройства Ё Ё Ё юдддддддды юдддддддддддддды цд©зд╢ юдддддддды юдддддддддддддды Ё юддддддддддддддддддддддддддддды цы юддддддддддддддддддддддддддддды Ё здддддддддддддаддддддддддддддд© Ё здддддддддддд© Ё Ё Ё Процессоры Ё Ё Ё юдддддбдддддды Ё Ё ддддбдддддддаддддддбддддддд Ё Ё здддадддд© здддддддадддддд© Ё Ё Ё Память Ё Ё Периферийные Ё Ё Ё Ё Ё Ё устройства Ё Ё Ё юдддддддды юдддддддддддддды Ё юддддддддддддддддддддддддддддды Рисунок 13.1. Модель системы с распределенной архитектурой системы на каждой машине функционирует независимо от внешних ус- ловий эксплуатации распределенной среды. Распределенные системы, хорошо описанные в литературе, тради- ционно делятся на следующие категории: * периферийные системы, представляющие собой группы машин, от- личающихся ярковыраженной общностью и связанных с одной (обычно более крупной) машиной. Периферийные процессоры делят свою нагрузку с центральным процессором и переадресовывают ему все обращения к операционной системе. Цель периферийной системы состоит в увеличении общей производительности сети и в предоставлении возможности выделения процессора одному про- цессу в операционной среде UNIX. Система запускается как от- дельный модуль; в отличие от других моделей распределенных систем, периферийные системы не обладают реальной автономией, за исключением случаев, связанных с диспетчеризацией процес- сов и распределением локальной памяти. * распределенные системы типа "Newcastle", позволяющие осу- ществлять дистанционную связь по именам удаленных файлов в библиотеке (название взято из статьи "The Newcastle Connection" - см. [Brownbridge 82]). Удаленные файлы имеют спецификацию (составное имя), которая в указании пути поиска содержит специальные символы или дополнительную компоненту имени, предшествующую корню файловой системы. Реализация это- го метода не предполагает внесения изменений в ядро системы, вследствие этого он более прост, чем другие методы, рассмат- риваемые в этой главе, но менее гибок. * абсолютно "прозрачные" распределенные системы, в которых для обращения к файлам, расположенным на других машинах, доста- точно указания их стандартных составных имен; распознавание этих файлов как удаленных входит в обязанности ядра. Маршруты поиска файлов, указанные в их составных именах, пересекают машинные границы в точках монтирования, сколько бы таких то- чек ни было сформировано при монтировании файловых систем на дисках. В настоящей главе мы рассмотрим архитектуру каждой модели; все приводимые сведения базируются не на результатах конкретных разработок, а на информации, публиковавшейся в различных техни- ческих статьях. При этом предполагается, что забота об адресации, маршрутизации, управлении потоками, обнаружении и исправлении ошибок возлагается на модули протоколов и драйверы устройств, другими словами, что каждая модель не зависит от используемой се- ти. Примеры использования системных функций, приводимые в следую- щем разделе для периферийных систем, работают аналогичным образом и для систем типа Newcastle и для абсолютно "прозрачных" систем, о которых пойдет речь позже; поэтому в деталях мы их рассмотрим один раз, а в разделах, посвященных другим типам систем, остано- вимся в основном на особенностях, отличающих эти модели от всех остальных. 13.1 ПЕРИФЕРИЙНЫЕ ПРОЦЕССОРЫ Архитектура периферийной системы показана на Рисунке 13.2. Цель такой конфигурации состоит в повышении общей производитель- ности сети за счет перераспределения выполняемых процессов между центральным и периферийными процессорами. У каждого из периферий- ных процессоров нет в распоряжении других локальных периферийных устройств, кроме тех, которые ему нужны для связи с центральным процессором. Файловая система и все устройства находятся в распо- ряжении центрального процессора. Предположим, что все пользова- тельские процессы исполняются на периферийном процессоре и между периферийными процессорами не перемещаются; будучи однажды пере- даны процессору, они пребывают на нем до момента завершения. Пе- риферийный процессор содержит облегченный вариант операционной системы, предназначенный для обработки локальных обращений к сис- теме, управления прерываниями, распределения памяти, работы с се- тевыми протоколами и с драйвером устройства связи с центральным процессором. При инициализации системы на центральном процессоре ядро по линиям связи загружает на каждом из периферийных процессоров ло- кальную операционную систему. Любой выполняемый на периферии про- цесс связан с процессом-спутником, принадлежащим центральному процессору (см. [Birrell 84]); когда процесс, протекающий на пе- риферийном процессоре, вызывает системную функцию, которая нужда- ется в услугах исключительно центрального процессора, периферий- ный процесс связывается со своим спутником и запрос поступает на обработку на центральный процессор. Процесс-спутник исполняет системную функцию и посылает результаты обратно на периферийный процессор. Взаимоотношения периферийного процесса со своим спут- ником похожи на отношения клиента и сервера, подробно рассмотрен- ные нами в главе 11: периферийный процесс выступает клиентом сво- его спутника, поддерживающего функции работы с файловой системой. При этом удаленный процесс-сервер имеет только одного клиента. В разделе 13.4 мы рассмотрим процессы-серверы, имеющие несколько клиентов. Центральный процессор Периферийный процессор зддддддддддддддддддддддддддддд© зддддддддддддддддддддддддддддд© Ё здддддддддддд© Ё Ё здддддддддддд© Ё Ё Ё Процессоры Ё Ё Ё Ё Процессоры Ё Ё Ё юдддддбдддддды Ё Ё юдддддбдддддды Ё Ё ддддбдддддддаддддддбддддддд Ё Ё ддддбдддддддадддддддддддддд Ё Ё здддадддд© здддддддадддддд© Ё Ё здддадддд© Ё Ё Ё Память Ё Ё Периферийные Ё Ё Ё Ё Память Ё Ё Ё Ё Ё Ё устройства Ё Ё Ё Ё Ё Ё Ё юдддддддды юдддддддддддддды цд©зд╢ юдддддддды Ё юддддддддддддддддддддддддддддды цы юддддддддддддддддддддддддддддды Ё здддддддддддддаддддддддддддддд© Ё здддддддддддд© Ё Периферийный Ё Ё Процессоры Ё Ё процессор Ё юдддддбдддддды Ё Ё ддддбдддддддадддддддддддддд Ё Ё здддадддд© Ё Ё Ё Память Ё Ё Ё Ё Ё Ё Ё юдддддддды Ё юддддддддддддддддддддддддддддды Рисунок 13.2. Конфигурация периферийной системы Когда периферийный процесс вызывает системную функцию, кото- рую можно обработать локально, ядру нет надобности посылать запрос процессу-спутнику. Так, например, в целях получения допол- нительной памяти процесс может вызвать для локального исполнения функцию sbrk. Однако, если требуются услуги центрального процес- сора, например, чтобы открыть файл, ядро кодирует информацию о передаваемых вызванной функции параметрах и условиях выполнения процесса в некое сообщение, посылаемое процессу-спутнику (Рисунок 13.3). Сообщение включает в себя признак, из которого следует, что системная функция выполняется процессом-спутником от имени клиента, передаваемые функции параметры и данные о среде выполне- ния процесса (например, пользовательский и групповой коды иденти- фикации), которые для разных функций различны. Оставшаяся часть сообщения представляет собой данные переменной длины (например, составное имя файла или данные, предназначенные для записи функ- цией write). Процесс-спутник ждет поступления запросов от периферийного процесса; при получении запроса он декодирует сообщение, опреде- ляет тип системной функции, исполняет ее и преобразует результаты в ответ, посылаемый периферийному процессу. Ответ, помимо резуль- татов выполнения системной функции, включает в себя сообщение об Формат сообщения зддддддддддддддддбддддддддддбдддддддддддддддбдддддддддддддддддддд© Ё Признак вызова ЁПараметры ЁДанные о среде Ё Составное имя Ё Ё системной функ-Ёсистемной Ёвыполнения про-ЁЫЫЫЫЫЫЫ или ЫЫЫЫЫЫЁ Ё ции Ёфункции Ёцесса Ё поток данных Ё юддддддддддддддддаддддддддддадддддддддддддддадддддддддддддддддддды Ответ зддддддддддддбдддддддддддбдддддддддбддддддддддддддддддддд© Ё Результаты Ё Сообщение Ё Номер Ё Ё Ё выполнения Ё об ошибке Ё сигнала ЁЫЫЫЫ Поток данных ЫЫЫЁ Ё системной Ё Ё Ё Ё Ё функции Ё Ё Ё Ё юддддддддддддадддддддддддадддддддддаддддддддддддддддддддды Рисунок 13.3. Форматы сообщений ошибке (если она имела место), номер сигнала и массив данных пе- ременной длины, содержащий, например, информацию, прочитанную из файла. Периферийный процесс приостанавливается до получения отве- та, получив его, производит расшифровку и передает результаты пользователю. Такова общая схема обработки обращений к операцион- ной системе; теперь перейдем к более детальному рассмотрению от- дельных функций. Для того, чтобы объяснить, каким образом работает периферий- ная система, рассмотрим ряд функций: getppid, open, write, fork, exit и signal. Функция getppid довольно проста, поскольку она связана с простыми формами запроса и ответа, которыми обменивают- ся периферийный и центральный процессоры. Ядро на периферийном процессоре формирует сообщение, имеющее признак, из которого сле- дует, что запрашиваемой функцией является функция getppid, и по- сылает запрос центральному процессору. Процесс-спутник на цент- ральном процессоре читает сообщение с периферийного процессора, расшифровывает тип системной функции, исполняет ее и получает идентификатор своего родителя. Затем он формирует ответ и переда- ет его периферийному процессу, находящемуся в состоянии ожидания на другом конце линии связи. Когда периферийный процессор получа- ет ответ, он передает его процессу, вызвавшему системную функцию getppid. Если же периферийный процесс хранит данные (такие, как идентификатор процесса-родителя) в локальной памяти, ему вообще не придется связываться со своим спутником. Если производится обращение к системной функции open, перифе- рийный процесс посылает своему спутнику соответствующее сообще- ние, которое включает имя файла и другие параметры. В случае ус- пеха процесс-спутник выделяет индекс и точку входа в таблицу фай- лов, отводит запись в таблице пользовательских дескрипторов файла в своем пространстве и возвращает дескриптор файла периферийному процессу. Все это время на другом конце линии связи периферийный процесс ждет ответа. У него в распоряжении нет никаких структур, которые хранили бы информацию об открываемом файле; возвращаемый функцией open дескриптор представляет собой указатель на запись в Центральный процессор Периферийный процессор здддддддддддддддддддддддддддддддддддддд© зддддддддддддддддддддд© Ё таблица Ё Ё Ё Ё пользо- Ё Ё Ё Ё ватель- Ё Ё Ё Ё ских Ё Ё Ё Ё дескрип- Ё Ё Ё Ё таблица таблица торов Ё Ё Ё Ё индексов файлов файла здддддддд©Ё Ё зддддддддд© Ё Ё зддддд© зддддд© зддддд© ЁПроцесс-ЁЫЫЫЫЫЫЫЫЫЫЁ Процесс Ё Ё Ё Ё Ё Ё Ё Ё Ё Ёспутник ЁЁ Ё юддддддддды Ё Ё цддддд╢ цддддд╢ цддддд╢ юдбддддддыЁ Ё Ё Ё Ё дед© Ё Ё зед деддды Ё Ё Ё Ё цддддд╢ Ё цддддд╢ Ёцддддд╢ дескрип- Ё Ё Ё Ё Ё Ё юдед дедыЁ Ё тор файла Ё Ё Ё Ё цддддд╢ цддддд╢ юддддды Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё цддддд╢ цддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё юддддды юддддды Ё Ё Ё юдддддддддддддддддддддддддддддддддддддды юддддддддддддддддддддды Рисунок 13.4. Вызов функции open из периферийного процесса таблице пользовательских дескрипторов файла, принадлежащей про- цессу-спутнику. Результаты выполнения функции показаны на Рисунке 13.4. Если производится обращение к системной функции write, пери- ферийный процессор формирует сообщение, состоящее из признака функции write, дескриптора файла и объема записываемых данных. Затем из пространства периферийного процесса он по линии связи копирует данные процессу-спутнику. Процесс-спутник расшифровывает полученное сообщение, читает данные из линии связи и записывает их в соответствующий файл (в качестве указателя на индекс которо- го и запись о котором в таблице файлов используется содержащийся в сообщении дескриптор); все указанные действия выполняются на центральном процессоре. По окончании работы процесс-спутник пере- дает периферийному процессу посылку, подтверждающую прием сообще- ния и содержащую количество байт данных, успешно переписанных в файл. Операция read выполняется аналогично; спутник информирует периферийный процесс о количестве реально прочитанных байт (в случае чтения данных с терминала или из канала это количество не всегда совпадает с количеством, указанным в запросе). Для выпол- нения как той, так и другой функции может потребоваться многок- ратная пересылка информационных сообщений по сети, что определя- ется объемом пересылаемых данных и размерами сетевых пакетов. Единственной функцией, требующей внесения изменений при рабо- те на центральном процессоре, является системная функция fork. Когда процесс исполняет эту функцию на ЦП, ядро выбирает для него периферийный процессор и посылает сообщение специальному процессу -серверу, информируя последний о том, что собирается приступить к выгрузке текущего процесса. Предполагая, что сервер принял зап- рос, ядро с помощью функции fork создает новый периферийный про- цесс, выделяя запись в таблице процессов и адресное пространство. Центральный процессор выгружает копию процесса, вызвавшего функ- цию fork, на периферийный процессор, затирая только что выделен- ное адресное пространство, порождает локальный спутник для связи с новым периферийным процессом и посылает на периферию сообщение о необходимости инициализации счетчика команд для нового процес- са. Процесс-спутник (на ЦП) является потомком процесса, вызвавше- го функцию fork; периферийный процесс с технической точки зрения выступает потомком процесса-сервера, но по логике он является по- томком процесса, вызвавшего функцию fork. Процесс-сервер не имеет логической связи с потомком по завершении функции fork; единс- твенная задача сервера состоит в оказании помощи при выгрузке по- Центральный процессор Периферийный процессор здддддддддддддддддддддд© здддддддддддддддддддддд© Ё здддддддддддддддддд© Ё Ё здддддддддддддддддд© Ё Ё Ё Процесс-родитель ЁЫЫЫЫЫЫЫЫЫЫЫЫЫЁ Процесс-сервер Ё Ё Ё юдддддддддбдддддддды Ё Ё юдддддддддддддддддды Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё здддддддддадддддддд© Ё Ё здддддддддддддддддд© Ё Ё Ё Порожденный спут-ЁЫЫЫЫЫЫЫЫЫЫЫЫЫЁ Порожденный про- Ё Ё Ё Ё ник Ё Ё Ё Ё цесс Ё Ё Ё юдддддддддддддддддды Ё Ё юдддддддддддддддддды Ё юдддддддддддддддддддддды юдддддддддддддддддддддды Рисунок 13.5. Выполнение функции fork на центральном процес- соре томка. Из-за сильной связи между компонентами системы (периферий- ные процессоры не располагают автономией) периферийный процесс и процесс-спутник имеют один и тот же код идентификации. Взаимос- вязь между процессами показана на Рисунке 13.5: непрерывной лини- ей показана связь типа "родитель-потомок", пунктиром - связь меж- ду равноправными партнерами. Когда процесс исполняет функцию fork на периферийном процес- соре, он посылает сообщение своему спутнику на ЦП, который и ис- полняет после этого всю вышеописанную последовательность дейс- твий. Спутник выбирает новый периферийный процессор и делает необходимые приготовления к выгрузке образа старого процесса: по- сылает периферийному процессу-родителю запрос на чтение его обра- за, в ответ на который на другом конце канала связи начинается передача запрашиваемых данных. Спутник считывает передаваемый об- раз и переписывает его периферийному потомку. Когда выгрузка об- раза заканчивается, процесс-спутник исполняет функцию fork, соз- давая своего потомка на ЦП, и передает значение счетчика команд периферийному потомку, чтобы последний знал, с какого адреса на- чинать выполнение. Очевидно, было бы лучше, если бы потомок про- цесса-спутника назначался периферийному потомку в качестве родителя, однако в нашем случае порожденные процессы получают возможность выполняться и на других периферийных процессорах, а не только на том, на котором они созданы. Взаимосвязь между про- цессами по завершении функции fork показана на Рисунке 13.6. Ког- да периферийный процесс завершает свою работу, он посылает соот- ветствующее сообщение процессу-спутнику и тот тоже завершается. От процесса-спутника инициатива завершения работы исходить не мо- жет. Центральный процессор зддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё Ё Ё здддддддддддддддддд© зддддддддддддддддд© Ё Ё Ё Спутник-родитель цдддддддддд╢ Спутник-потомок Ё Ё Ё юдддддддддддддддддды юддддддддддддддддды Ё Ё Ы Ы Ё юддддддддддддЫддддддддддддддддддддддддддддддЫддддддддддды Ы Ы зддддддддддддддЫдддддддддддд© зддддддддддддЫдддддддддддддд© Ё Ы Ё Ё Ы Ё Ё зддддддддддддддддддддддд© Ё Ё здддддддддддддддддддддд© Ё Ё Ё Периферийный родитель Ё Ё Ё Ё Периферийный потомок Ё Ё Ё юддддддддддддддддддддддды Ё Ё юдддддддддддддддддддддды Ё Ё Ё Ё Ё юддддддддддддддддддддддддддды юддддддддддддддддддддддддддды Периферийный процессор Периферийный процессор Рисунок 13.6. Выполнение функции fork на периферийном процес- соре И в многопроцессорной, и в однопроцессорной системах процесс должен реагировать на сигналы одинаково: процесс либо завершает выполнение системной функции до проверки сигналов, либо, напро- тив, получив сигнал, незамедлительно выходит из состояния приос- танова и резко прерывает работу системной функции, если это сог- ласуется с приоритетом, с которым он был приостановлен. Поскольку процесс-спутник выполняет системные функции от имени периферийно- го процесса, он должен реагировать на сигналы, согласуя свои действия с последним. Если в однопроцессорной системе сигнал зас- тавляет процесс завершить выполнение функции аварийно, процес- су-спутнику в многопроцессорной системе следует вести себя тем же образом. То же самое можно сказать и о том случае, когда сигнал побуждает процесс к завершению своей работы с помощью функции exit: периферийный процесс завершается и посылает соответствующее сообщение процессу-спутнику, который, разумеется, тоже завершает- ся. Когда периферийный процесс вызывает системную функцию signal, он сохраняет текущую информацию в локальных таблицах и посылает сообщение своему спутнику, информируя его о том, следует ли ука- занный сигнал принимать или же игнорировать. Процессу-спутнику безразлично, выполнять ли перехват сигнала или действие по умол- чанию. Реакция процесса на сигнал зависит от трех факторов (Рису- нок 13.7): поступает ли сигнал во время выполнения процессом сис- темной функции, сделано ли с помощью функции signal указание об игнорировании сигнала, возникает ли сигнал на этом же периферий- ном процессоре или на каком-то другом. Перейдем к рассмотрению различных возможностей. Допустим, что периферийный процесс приостановил свою работу на то время, пока процесс-спутник исполняет системную функцию от его имени. Если сигнал возникает в другом месте, процесс-спутник здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд© Ё алгоритм sighandle /* алгоритм обработки сигналов */ Ё Ё входная информация: отсутствует Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё если (текущий процесс является чьим-то спутником или Ё Ё имеет прототипа) Ё Ё { Ё Ё если (сигнал игнорируется) Ё Ё вернуть управление; Ё Ё если (сигнал поступил во время выполнения системной Ё Ё функции) Ё Ё поставить сигнал перед процессом-спутником; Ё Ё в противном случае Ё Ё послать сообщение о сигнале периферийному процес-Ё Ё су; Ё Ё } Ё Ё в противном случае /* периферийный процесс */ Ё Ё { Ё Ё /* поступил ли сигнал во время выполнения системной Ё Ё * функции или нет Ё Ё */ Ё Ё послать сигнал процессу-спутнику; Ё Ё } Ё Ё } Ё Ё Ё Ё алгоритм satellite_end_of_syscall /* завершение систем- Ё Ё * ной функции, выз- Ё Ё * ванной периферийнымЁ Ё * процессом */ Ё Ё входная информация: отсутствует Ё Ё выходная информация: отсутствует Ё Ё { Ё Ё если (во время выполнения системной функции поступило Ё Ё прерывание) Ё Ё послать периферийному процессу сообщение о прерыва- Ё Ё нии, сигнал; Ё Ё в противном случае /* выполнение системной функции неЁ Ё * прерывалось */ Ё Ё послать ответ: включить флаг, показывающий поступле- Ё Ё ние сигнала; Ё Ё } Ё юдддддддддддддддддддддддддддддддддддддддддддддддддддддддддддды Рисунок 13.7. Обработка сигналов в периферийной системе обнаруживает его раньше, чем периферийный процесс. Возможны три случая. 1. Если в ожидании некоторого события процесс-спутник не перехо- дил в состояние приостанова, из которого он вышел бы по полу- чении сигнала, он выполняет системную функцию до конца, посы- лает результаты выполнения периферийному процессу и показыва- ет, какой из сигналов им был получен. 2. Если процесс сделал указание об игнорировании сигнала данного типа, спутник продолжает следовать алгоритму выполнения сис- темной функции, не выходя из состояния приостанова по longjmp. В ответе, посылаемом периферийному процессу, сообще- ние о получении сигнала будет отсутствовать. 3. Если по получении сигнала процесс-спутник прерывает выполне- ние системной функции (по longjmp), он информирует об этом периферийный процесс и сообщает ему номер сигнала. Периферийный процесс ищет в поступившем ответе сведения о по- лучении сигналов и в случае обнаружения таковых производит обра- ботку сигналов перед выходом из системной функции. Таким образом, поведение процесса в многопроцессорной системе в точности соот- ветствует его поведению в однопроцессорной системе: он или завер- шает свою работу, не выходя из режима ядра, или обращается к пользовательской функции обработки сигнала, или игнорирует сигнал и успешно завершает выполнение системной функции. Периферийный процесс Процесс-спутник здддддддддддддддддддддддддддддддддддддддддддддддддддддддддддд Ё Вызывает системную функцию read Ы Ё Посылает сообщение о вызове функции Ы Ё процессу-спутнику Ы Ё Приостанавливается до получения Ы Ё ответа от процесса-спутника Получает сообщение о Ё Ы вызове системной функ- Ё Ы ции read Ё Ы Читает данные с тер- Ё Ы минала Ё Ы Приостанавливается в Ё Ы ожидании получения Ё Ы порции данных Ё Ы Ы Ё Ы Ы Ё Ы Ы Ё Ы Сигнал (пользователь Ё Ы нажал клавишу "break") Ё Ы Выходит из состояния Ё Ы приостанова Ё Ы Прекращает выполнение Ё Ы системной функции Ё Ы Посылает ответ пери- Ё Ы ферийному процессу: Ё Ы выполнение функции Ё Ы прервано Ё Выходит из состояния приостанова Ё Анализирует ответ v Обрабатывает сигнал Рисунок 13.8. Прерывание во время выполнения системной функ- ции Предположим, например, что периферийный процесс вызывает функцию чтения с терминала, связанного с центральным процессором, и приостанавливает свою работу на время выполнения функции про- цессом-спутником (Рисунок 13.8). Если пользователь нажимает кла- вишу прерывания (break), ядро ЦП посылает процессу-спутнику соот- ветствующий сигнал. Если спутник находился в состоянии приостано- ва в ожидании ввода с терминала порции данных, он немедленно вы- ходит из этого состояния и прекращает выполнение функции read. В своем ответе на запрос периферийного процесса спутник сообщает код ошибки и номер сигнала, соответствующий прерыванию. Перифе- рийный процесс анализирует ответ и, поскольку в сообщении гово- рится о поступлении сигнала прерывания, отправляет сигнал самому себе. Перед выходом из функции read периферийное ядро осуществля- ет проверку поступления сигналов, обнаруживает сигнал прерывания, поступивший от процесса-спутника, и обрабатывает его обычным по- рядком. Если в результате получения сигнала прерывания периферий- ный процесс завершает свою работу с помощью функции exit, данная функция берет на себя заботу об уничтожении процесса-спутника. Если периферийный процесс перехватывает сигналы о прерывании, он вызывает пользовательскую функцию обработки сигналов и по выходе из функции read возвращает пользователю код ошибки. С другой сто- роны, если спутник исполняет от имени периферийного процесса сис- темную функцию stat, он не будет прерывать ее выполнение при по- лучении сигнала (функции stat гарантирован выход из любого приос- танова, поскольку для нее время ожидания ресурса ограничено). Спутник доводит выполнение функции до конца и возвращает перифе- рийному процессу номер сигнала. Периферийный процесс посылает сигнал самому себе и получает его на выходе из системной функции. Если сигнал возник на периферийном процессоре во время выпол- нения системной функции, периферийный процесс будет находиться в неведении относительно того, вернется ли к нему вскоре управление от процесса-спутника или же последний перейдет в состояние приос- танова на неопределенное время. Периферийный процесс посылает спутнику специальное сообщение, информируя его о возникновении сигнала. Ядро на ЦП расшифровывает сообщение и посылает сигнал спутнику, реакция которого на получение сигнала описана в преды- дущих параграфах (аварийное завершение выполнения функции или до- ведение его до конца). Периферийный процесс не может послать со- общение спутнику непосредственно, поскольку спутник занят испол- нением системной функции и не считывает данные из линии связи. Если обратиться к примеру с функцией read, следует отметить, что периферийный процесс не имеет представления о том, ждет ли его спутник ввода данных с терминала или же выполняет другие действия. Периферийный процесс посылает спутнику сообщение о сиг- нале: если спутник находится в состоянии приостанова с приорите- том, допускающим прерывания, он немедленно выходит из этого сос- тояния и прекращает выполнение системной функции; в противном случае выполнение функции доводится до успешного завершения. Рассмотрим, наконец, случай поступления сигнала во время, не связанное с выполнением системной функции. Если сигнал возник на другом процессоре, спутник получает его первым и посылает сообще- ние о сигнале периферийному процессу, независимо от того, касает- ся ли этот сигнал периферийного процесса или нет. Периферийное ядро расшифровывает сообщение и посылает сигнал процессу, который реагирует на него обычным порядком. Если сигнал возник на перифе- рийном процессоре, процесс выполняет стандартные действия, не прибегая к услугам своего спутника. Когда периферийный процесс посылает сигнал другим периферий- ным процессам, он кодирует сообщение о вызове функции kill и по- сылает его процессу-спутнику, который исполняет вызываемую функ- цию локально. Если часть процессов, для которых предназначен сигнал, имеет местонахождение на других периферийных процессорах, сигнал получат (и прореагируют на его получение вышеописанным об- разом) их спутники. 13.2 СВЯЗЬ ТИПА NEWCASTLE В предыдущем разделе мы рассмотрели тип сильносвязанной сис- темы, для которого характерна посылка всех возникающих на перифе- рийном процессоре обращений к функциям подсистемы управления файлами на удаленный (центральный) процессор. Теперь перейдем к рассмотрению систем с менее сильной связью, которые состоят из машин, производящих обращение к файлам, находящимся на других ма- шинах. В сети, состоящей из персональных компьютеров и рабочих станций, например, пользователи часто обращаются к файлам, распо- ложенным на большой машине. В последующих двух разделах мы расс- мотрим такие конфигурации систем, в которых все системные функции выполняются в локальных подсистемах, но при этом имеется возмож- ность обращения к файлам (через функции подсистемы управления файлами), расположенным на других машинах. Для идентифицирования удаленных файлов в этих системах ис- пользуется один из следующих двух путей. В одних системах в сос- тавное имя файла добавляется специальный символ: компонента име- ни, предшествующая этому символу, идентифицирует машину, осталь- ная часть имени - файл, находящийся на этой машине. Так, напри- мер, составное имя "sftig!/fs1/mjb/rje" идентифицирует файл "/fs1/mjb/rje", находящийся на машине "sftig". Такая схема идентифицирования файла соответствует согла- шению, установленному программой uucp относительно передачи фай- лов между системами типа UNIX. В другой схеме удаленные файлы идентифицируются добавлением к имени специального префикса, нап- ример: /../sftig/fs1/mjb/rje где "/../" - префикс, свидетельствующий о том, что файл удален- ный; вторая компонента имени файла является именем удаленной ма- шины. В данной схеме используется привычный синтаксис имен файлов в системе UNIX, поэтому в отличие от первой схемы здесь пользова- тельским программам нет необходимости приноравливаться к исполь- зованию имен, имеющих необычную конструкцию (см. [Pike 85]). Процесс-клиент Процесс-сервер зддддддддддддддддддддддддддддд© здддддддддддддддддддддддддддд© Ё Таблица Ё Ё Процесс- Ё Ё Си-библиотека открытых Ё Ё спутник Запрос Ё Ё файлов Ё Ё (пользо- на чтение Ё Ё здддддд© Ё Ё вательский Ё Ё Ё Ё зддддддддддддддеддд Ё Ё Ё уровень) Ё Ё Ё Ё Ё цдддддд╢ Ё Ё Ё Ё Ё Ё Ё локальный Ё Ё Ё Ё зддддддддддддды Ё Ё Ё Ё цдддддд╢ Ё Ё Ё Ё Ё Ё Ё зддддеддд Ё Ё Ё Ё Ё Ё Ё Ё Ё цдддддд╢ Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё юдддддды Ё Ё Ё Ё Ё Ё Ё юддддд© Ё Ё Ё Ё Ё юддддедддддддддддддддедддддддды юддддедддддддддддддддддеддддды Ё Ё удаленный Ё Ё зддддедддддддддддддддедддддддд© зддддедддддддддддддддддеддддд© Ё Ё Сетевой Ё Ё Ё Сетевой Ё Ё Ядро интерфейс Ё Ё Ядро интерфейс Ё Ё Ё Ё Ё Ё Ё юддддддддддддддддддддедддддддды юддддддддддддддддддддддеддддды Ё с е т ь Ё юддддддддддддддддддддддддддддддддддддды Рисунок 13.9. Формулирование запросов к файловому серверу (процессору) Всю оставшуюся часть раздела мы посвятим рассмотрению модели системы, использующей связь типа Newcastle, в которой ядро не за- нимается распознаванием удаленных файлов; эта функция полностью возлагается на подпрограммы из стандартной Си-библиотеки, выпол- няющие в данном случае роль системного интерфейса. Эти подпрог- раммы анализируют первую компоненту имени файла, в обоих описан- ных способах идентифицирования содержащую признак удаленности файла. В этом состоит отступление от заведенного порядка, при ко- тором библиотечные подпрограммы не занимаются синтаксическим раз- бором имен файлов. На Рисунке 13.9 показано, каким образом форму- лируются запросы к файловому серверу. Если файл локальный, ядро локальной системы обрабатывает запрос обычным способом. Рассмот- рим обратный случай: open("/../sftig/fs1/mjb/rje/file",O_RDONLY); Подпрограмма open из Си-библиотеки анализирует первые две компо- ненты составного имени файла и узнает, что файл следует искать на удаленной машине "sftig". Чтобы иметь информацию о том, была ли ранее у процесса связь с данной машиной, подпрограмма заводит специальную структуру, в которой запоминает этот факт, и в случае отрицательного ответа устанавливает связь с файловым сервером, работающим на удаленной машине. Когда процесс формулирует свой первый запрос на дистанционную обработку, удаленный сервер подт- верждает запрос, в случае необходимости ведет запись в поля поль- зовательского и группового кодов идентификации и создает процесс- спутник, который будет выступать от имени процесса-клиента. Чтобы выполнять запросы клиента, спутник должен иметь на уда- ленной машине те же права доступа к файлам, что и клиент. Другими словами, пользователь "mjb" должен иметь и к удаленным, и к ло- кальным файлам одинаковые права доступа. К сожалению, не исключе- на возможность того, что код идентификации клиента "mjb" может совпасть с кодом идентификации другого клиента удаленной машины. Таким образом, администраторам систем на работающих в сети маши- нах следует либо следить за назначением каждому пользователю кода идентификации, уникального для всей сети, либо в момент формули- рования запроса на сетевое обслуживание выполнять преобразование кодов. Если это не будет сделано, процесс-спутник будет иметь на удаленной машине права другого клиента. Более деликатным вопросом является получение в отношении ра- боты с удаленными файлами прав суперпользователя. С одной сторо- ны, клиент-суперпользователь не должен иметь те же права в отно- шении удаленной системы, чтобы не вводить в заблуждение средства защиты удаленной системы. С другой стороны, некоторые из прог- рамм, если им не предоставить права суперпользователя, просто не смогут работать. Примером такой программы является программа mkdir (см. главу 7), создающая новый каталог. Удаленная система не разрешила бы клиенту создавать новый каталог, поскольку на удалении права суперпользователя не действуют. Проблема создания удаленных каталогов служит серьезным основанием для пересмотра системной функции mkdir в сторону расширения ее возможностей в автоматическом установлении всех необходимых пользователю связей. Тем не менее, получение setuid-программами (к которым относится и программа mkdir) прав суперпользователя по отношению к удаленным файлам все еще остается общей проблемой, требующей своего реше- ния. Возможно, что наилучшим решением этой проблемы было бы уста- новление для файлов дополнительных характеристик, описывающих доступ к ним со стороны удаленных суперпользователей; к сожале- нию, это потребовало бы внесения изменений в структуру дискового индекса (в части добавления новых полей) и породило бы слишком большой беспорядок в существующих системах. Если подпрограмма open завершается успешно, локальная библио- тека оставляет об этом соответствующую отметку в доступной для пользователя структуре, содержащей адрес сетевого узла, идентифи- катор процесса-спутника, дескриптор файла и другую аналогичную информацию. Библиотечные подпрограммы read и write устанавливают, исходя из дескриптора, является ли файл удаленным, и в случае по- ложительного ответа посылают спутнику сообщение. Процесс-клиент взаимодействует со своим спутником во всех случаях обращения к системным функциям, нуждающимся в услугах удаленной машины. Если процесс обращается к двум файлам, расположенным на одной и той же удаленной машине, он пользуется одним спутником, но если файлы расположены на разных машинах, используются уже два спутника: по одному на каждой машине. Два спутника используются и в том слу- чае, когда к файлу на удаленной машине обращаются два процесса. Вызывая системную функцию через спутника, процесс формирует сооб- щение, включающее в себя номер функции, имя пути поиска и другую необходимую информацию, аналогичную той, которая входит в струк- туру сообщения в системе с периферийными процессорами. Механизм выполнения операций над текущим каталогом более сло- жен. Когда процесс выбирает в качестве текущего удаленный ката- лог, библиотечная подпрограмма посылает соответствующее сообщение спутнику, который изменяет текущий каталог, при этом подпрограмма запоминает, что каталог удаленный. Во всех случаях, когда имя пу- ти поиска начинается с символа, отличного от наклонной черты (/), подпрограмма посылает это имя на удаленную машину, где про- цесс-спутник прокладывает маршрут, начиная с текущего каталога. Если текущий каталог - локальный, подпрограмма просто передает имя пути поиска ядру локальной системы. Системная функция chroot в отношении удаленного каталога выполняется похоже, но при этом ее выполнение для ядра локальной системы проходит незамеченным; строго говоря, процесс может оставить эту операцию без внимания, поскольку только библиотека фиксирует ее выполнение. Когда процесс вызывает функцию fork, соответствующая библио- течная подпрограмма посылает сообщения каждому спутнику. Процессы -спутники выполняют операцию ветвления и посылают идентификаторы своих потомков клиенту-родителю. Процесс-клиент запускает систем- ную функцию fork, которая передает управление порождаемому потом- ку; локальный потомок ведет диалог с удаленным потомком-спутни- ком, адреса которого сохранила библиотечная подпрограмма. Такая трактовка функции fork облегчает процессам-спутникам контроль над открытыми файлами и текущими каталогами. Когда процесс, работаю- щий с удаленными файлами, завершается (вызывая функцию exit), подпрограмма посылает сообщения всем его удаленным спутникам, чтобы они по получении сообщения проделали то же самое. Отдельные моменты реализации системных функций exec и exit затрагиваются в упражнениях. Преимущество связи типа Newcastle состоит в том, что обращение процесса к удаленным файлам становится "прозрачным" (незаметным для пользователя), при этом в ядро системы никаких изменений вносить не нужно. Однако, данной разработке присущ и ряд недостатков. Прежде всего, при ее реализации возможно сниже- ние производительности системы. В связи с использованием расши- ренной Си-библиотеки размер используемой каждым процессом памяти увеличивается, даже если процесс не обращается к удаленным фай- лам; библиотека дублирует функции ядра и требует для себя больше места в памяти. Увеличение размера процессов приводит к удлинению продолжительности периода запуска и может вызвать большую конку- ренцию за ресурсы памяти, создавая условия для более частой выг- рузки и подкачки задач. Локальные запросы будут исполняться мед- леннее из-за увеличения продолжительности каждого обращения к яд- ру, замедление может грозить и обработке удаленных запросов, зат- раты по пересылке которых по сети увеличиваются. Дополнительная обработка удаленных запросов на пользовательском уровне увеличи- вает количество переключений контекста, операций по выгрузке и подкачке процессов. Наконец, для того, чтобы обращаться к удален- ным файлам, программы должны быть перекомпилированы с использова- нием новых библиотек; старые программы и поставленные объектные модули без этого работать с удаленными файлами не смогут. Все эти недостатки отсутствуют в системе, описываемой в следующем разде- ле. 13.3 "ПРОЗРАЧНЫЕ" РАСПРЕДЕЛЕННЫЕ ФАЙЛОВЫЕ СИСТЕМЫ Термин "прозрачное распределение" означает, что пользователи, работающие на одной машине, могут обращаться к файлам, находящим- ся на другой машине, не осознавая того, что тем самым они пересе- кают машинные границы, подобно тому, как на своей машине они при переходе от одной файловой системе к другой пересекают точки мон- тирования. Имена, по которым процессы обращаются к файлам, нахо- дящимся на удаленных машинах, похожи на имена локальных файлов: отличительные символы в них отсутствуют. В конфигурации, показан- ной на Рисунке 13.10, каталог "/usr/src", принадлежащий машине B, "вмонтирован" в каталог "/usr/src", принадлежащий машине A. Такая конфигурация представляется удобной в том случае, если в разных системах предполагается использовать один и тот же исходный код системы, традиционно находящийся в каталоге "/usr/src". Пользова- тели, работающие на машине A, могут обращаться к файлам, располо- женным на машине B, используя привычный синтаксис написания имен файлов (например: "/usr/src/cmd/login.c"), и ядро уже само реша- ет вопрос, является файл удаленным или же локальным. Пользовате- ли, работающие на машине B, имеют доступ к своим локальным файлам (не подозревая о том, что к этим же файлам могут обращаться и пользователи машины A), но, в свою очередь, не имеют доступа к файлам, находящимся на машине A. Конечно, возможны и другие вари- анты, в частности, такие, в которых все удаленные системы монти- руются в корне локальной системы, благодаря чему пользователи по- лучают доступ ко всем файлам во всех системах. Машина A Машина B зддддддддддддддддддддддддддддд© зддддддддддддддддддддддддддддд© Ё / Ё Ё / Ё Ё Ё Ё Ё Ё Ё Ё зддддддддадддддддд© Ё Ё здддддддддддеддддддддддд© Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё bin usr Ё Ё usr bin etc Ё Ё Ё Ё Ё Ё Ё Ё Ё зддддадддд© зддддадд© Ё Ё цддддддд© Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ёlogin mail bin srcЁ зддд>src bin Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё здддаддд© Ё Ё Ё Ё цддддддбддддд© Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё troff vi Ё Ё Ё Ё lib cmd uts Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё здддаддд© Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё login.c mail.c Ё юдддддддддддддддддддддддддддЁды Ё юддддддддддддддддддддддддддддды юддды Рисунок 13.10. Файловые системы после удаленного монтирования Наличие сходства между монтированием локальных файловых сис- тем и открытием доступа к удаленным файловым системам послужило поводом для адаптации функции mount применительно к удаленным файловым системам. В данном случае ядро получает в свое распоря- жение таблицу монтирования расширенного формата. Выполняя функцию mount, ядро организует сетевую связь с удаленной машиной и сохра- няет в таблице монтирования информацию, характеризующую данную связь. Интересная проблема связана с именами путей, включающих "..". Если процесс делает текущим каталог из удаленной файловой систе- мы, последующее использование в имени символов ".." скорее вернет процесс в локальную файловую систему, чем позволит обращаться к файлам, расположенным выше текущего каталога. Возвращаясь вновь к Рисунку 13.10, отметим, что когда процесс, принадлежащий машине A, выбрав предварительно в качестве текущего каталог "/usr/src/cmd", расположенный в удаленной файловой системе, ис- полнит команду cd ../../.. текущим каталогом станет корневой каталог, принадлежащий машине A, а не машине B. Алгоритм namei, работающий в ядре удаленной системы, получив последовательность символов "..", проверяет, яв- ляется ли вызывающий процесс агентом процесса-клиента, и в случае положительного ответа устанавливает, трактует ли клиент текущий рабочий каталог в качестве корня удаленной файловой системы. Связь с удаленной машиной принимает одну из двух форм: вызов удаленной процедуры или вызов удаленной системной функции. В пер- вой форме каждая процедура ядра, имеющая дело с индексами, прове- ряет, указывает ли индекс на удаленный файл, и если это так, по- сылает на удаленную машину запрос на выполнение указанной операции. Данная схема естественным образом вписывается в абс- трактную структуру поддержки файловых систем различных типов, описанную в заключительной части главы 5. Таким образом, обраще- ние к удаленному файлу может инициировать пересылку по сети нес- кольких сообщений, количество которых определяется количеством подразумеваемых операций над файлом, с соответствующим увеличени- ем времени ответа на запрос с учетом принятого в сети времени ожидания. Каждый набор удаленных операций включает в себя, по крайней мере, действия по блокированию индекса, подсчету ссылок и т.п. В целях усовершенствования модели предлагались различные оп- тимизационные решения, связанные с объединением нескольких опера- ций в один запрос (сообщение) и с буферизацией наиболее важных данных (см. [Sandberg 85]). Сервер Клиент (процесс/процессор) здддддддддддддддддддд© здддддддддддддддддддддддддддддддддддддддд© Ё таблица Ё Ё таблица таблица таблица Ё Ё индексов зддддддд© Ё Ё индексов файлов пользо- Ё Ё зддддд© ЁСпутникЁЫЁ Ё зддддд© зддддд© ватель- Ё Ё Ё Ё юдбддддды ЁЫ Ё Ё Ё Ё Ё ских Ё Ё цддддд╢ Ё Ё Ы цддддд╢ цддддд╢ дескрип- зддддддд©Ё Ё Ё Ё Ё зддддддед дед© Ё Ё торов здд╢ПроцессЁЁ Ё цддддд╢ Ё Ё Ё Ё цддддд╢ Ё цддддд╢ файла Ё юдддддддыЁ Ё Ё Ё Ё Ё Ё Ё Ё Ё юдед де© зддддд© Ё Ё Ё цддддд╢ Ё Ё Ё Ё цддддд╢ цддддд╢Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё Ё ЁЁ цддддд╢ Ёдескриптор Ё Ё цддддд╢ Ё Ё Ё Ё цддддд╢ цддддд╢юдед дедыфайла Ё Ё Ё ддеддддаддддды Ё Ё Ё Ё Ё Ё цддддд╢ Ё Ё юддддды Ё Ё юддддды юддддды Ё Ё Ё Ё Ё Ё юддддды Ё юдддддддддддддддддддды юдддддддддддддддддддддддддддддддддддддддды Рисунок 13.11. Открытие удаленного файла Рассмотрим процесс, который открывает удаленный файл "/usr/src/cmd/login.c", где "src" - точка монтирования. Выполняя синтаксический разбор имени файла (по схеме namei-iget), ядро об- наруживает, что файл удаленный, и посылает на машину, где он на- ходится, запрос на получение заблокированного индекса. Получив желаемый ответ, локальное ядро создает в памяти копию индекса, корреспондирующую с удаленным файлом. Затем ядро производит про- верку наличия необходимых прав доступа к файлу (на чтение, напри- мер), послав на удаленную машину еще одно сообщение. Выполнение алгоритма open продолжается в полном соответствии с планом, при- веденным в главе 5, с посылкой сообщений на удаленную машину по мере необходимости, до полного окончания алгоритма и освобождения индекса. Взаимосвязь между структурами данных ядра по завершении алгоритма open показана на Рисунке 13.11. Если клиент вызывает системную функцию read, ядро клиента блокирует локальный индекс, посылает запрос на блокирование уда- ленного индекса, запрос на чтение данных, копирует данные в ло- кальную память, посылает запрос на освобождение удаленного индек- са и освобождает локальный индекс. Такая схема соответствует семантике существующего однопроцессорного ядра, но частота ис- пользования сети (несколько обращений на каждую системную функ- цию) снижает производительность всей системы. Однако, чтобы уменьшить поток сообщений в сети, в один запрос можно объединять несколько операций. В примере с функцией read клиент может пос- лать серверу один общий запрос на "чтение", а уж сервер при его выполнении сам принимает решение на захват и освобождение индек- са. Сокращения сетевого трафика можно добиться и путем использо- вания удаленных буферов (о чем мы уже говорили выше), но при этом нужно позаботиться о том, чтобы системные функции работы с файла- ми, использующие эти буферы, выполнялись надлежащим образом. При второй форме связи с удаленной машиной (вызов удаленной системной функции) локальное ядро обнаруживает, что системная функция имеет отношение к удаленному файлу, и посылает указанные в ее вызове параметры на удаленную систему, которая исполняет функцию и возвращает результаты клиенту. Машина клиента получает результаты выполнения функции и выходит из состояния вызова. Большинство системных функций может быть выполнено с использова- нием только одного сетевого запроса с получением ответа через достаточно приемлемое время, но в такую модель вписываются не все функции. Так, например, по получении некоторых сигналов ядро соз- дает для процесса файл с именем "core" (глава 7). Создание этого файла не связано с конкретной системной функцией, а завершает вы- полнение нескольких операций, таких как создание файла, проверка прав доступа и выполнение ряда операций записи. В случае с системной функцией open запрос на исполнение функ- ции, посылаемый на удаленную машину, включает в себя часть имени файла, оставшуюся после исключения компонент имени пути поиска, отличающих удаленный файл, а также различные флаги. В рассмотрен- ном ранее примере с открытием файла "/usr/src/cmd/login.c" ядро посылает на удаленную машину имя "cmd/login.c". Сообщение также включает в себя опознавательные данные, такие как пользователь- ский и групповой коды идентификации, необходимые для проверки прав доступа к файлам на удаленной машине. Если с удаленной маши- ны поступает ответ, свидетельствующий об успешном выполнении функции open, локальное ядро выбирает свободный индекс в памяти локальной машины и помечает его как индекс удаленного файла, сох- раняет информацию об удаленной машине и удаленном индексе и по заведенному порядку выделяет новую запись в таблице файлов. В сравнении с реальным индексом на удаленной машине индекс, принад- лежащий локальной машине, является формальным, не нарушающим кон- фигурацию модели, которая в целом совпадает с конфигурацией, используемой при вызове удаленной процедуры (Рисунок 13.11). Если вызываемая процессом функция обращается к удаленному файлу по его дескриптору, локальное ядро узнает из индекса (локального) о том, что файл удаленный, формулирует запрос, включающий в себя вызыва- емую функцию, и посылает его на удаленную машину. В запросе со- держится указатель на удаленный индекс, по которому процесс-спут- ник сможет идентифицировать сам удаленный файл. Получив результат выполнения любой системной функции, ядро может для его обработки прибегнуть к услугам специальной програм- мы (по завершении которой ядро закончит работу с функцией), ибо не всегда локальная обработка результатов, применяемая в однопро- цессорной системе, подходит для системы с несколькими процессора- ми. Вследствие этого возможны изменения в семантике системных ал- горитмов, направленные на обеспечение поддержки выполнения уда- ленных системных функций. Однако, при этом в сети циркулирует ми- нимальный поток сообщений, обеспечивающий минимальное время реакции системы на поступающие запросы. 13.4 РАСПРЕДЕЛЕННАЯ МОДЕЛЬ БЕЗ ПЕРЕДАТОЧНЫХ ПРОЦЕССОВ Использование передаточных процессов (процессов-спутников) в "прозрачной" распределенной системе облегчает слежение за удален- ными файлами, однако при этом таблица процессов удаленной системы перегружается процессами-спутниками, бездействующими большую часть времени. В других схемах для обработки удаленных запросов используются специальные процессы-серверы (см. [Sandberg 85] и [Cole 85]). Удаленная система располагает набором (пулом) процес- сов-серверов, время от времени назначаемых ею для обработки пос- тупающих удаленных запросов. После обработки запроса процесс-сер- вер возвращается в пул и переходит в состояние готовности к вы- полнению обработки других запросов. Сервер не сохраняет пользова- тельский контекст между двумя обращениями, ибо он может обрабаты- вать запросы сразу нескольких процессов. Следовательно, каждое поступающее от процесса-клиента сообщение должно включать в себя информацию о среде его выполнения, а именно: коды идентификации пользователя, текущий каталог, сигналы и т.д. Процессы-спутники получают эти данные в момент своего появления или во время выпол- нения системной функции. Когда процесс открывает удаленный файл, ядро удаленной систе- мы назначает индекс для последующих ссылок на файл. Локальная ма- шина располагает таблицей пользовательских дескрипторов файла, таблицей файлов и таблицей индексов с обычным набором записей, причем запись в таблице индексов идентифицирует удаленную машину и удаленный индекс. В тех случаях, когда системная функция (нап- ример, read) использует дескриптор файла, ядро посылает сообще- ние, указывающее на ранее назначенный удаленный индекс, и переда- ет связанную с процессом информацию: код идентификации пользова- теля, максимально-допустимый размер файла и т.п. Если удаленная машина имеет в своем распоряжении процесс-сервер, взаимодействие с клиентом принимает вид, описанный ранее, однако связь между клиентом и сервером устанавливается только на время выполнения системной функции. Если вместо процессов-спутников воспользоваться услугами сер- веров, управление потоком данных, сигналами и удаленными устройс- твами может усложниться. Поступающие в большом количестве запросы к удаленной машине при отсутствии достаточного числа серверов должны выстраиваться в очередь. Для этого нужен протокол более высокого уровня, чем тот, который используется в основной сети. В модели, использующей спутник, с другой стороны, перенасыщенность запросами исключается, ибо все запросы клиента обрабатываются синхронно. Клиент может иметь не более одного запроса, ожидающего обработки. Обработка сигналов, прерывающих выполнение системной функции, при использовании серверов также усложняется, поскольку удаленной машине приходится при этом искать соответствующий сервер, обслу- живающий выполнение функции. Не исключается даже и такая возмож- ность, что в связи с занятостью всех серверов запрос на выполне- ние системной функции находится в состоянии ожидания обработки. Условия для возникновения конкуренции складываются и тогда, когда сервер возвращает результат выполнения системной функции вызываю- щему процессу и ответ сервера заключает в себе посылку через сеть соответствующего сигнального сообщения. Каждое сообщение должно быть помечено таким образом, чтобы удаленная система могла рас- познать его и в случае необходимости прервать работу процес- сов-серверов. При использовании спутников тот процесс, который обслуживает выполнение запроса клиента, идентифицируется автома- тически, и в случае поступления сигнала проверка того, закончена ли обработка запроса или нет, не составляет особого труда. Наконец, если вызываемая клиентом системная функция заставля- ет сервер приостановиться на неопределенное время (например, при чтении данных с удаленного терминала), сервер не может вести об- работку других запросов, чтобы освободить тем самым серверный пул. Если к удаленным устройствам обращаются сразу несколько про- цессов и если при этом количество серверов ограничено сверху, имеет место вполне ощутимое узкое место. При использовании спут- ников этого не происходит, поскольку спутник выделяется каждому процессу-клиенту. Еще одна проблема, связанная с использованием серверов для удаленных устройств, будет рассмотрена в упражнении 13.14. Несмотря на преимущества, которые предоставляет использование процессов-спутников, потребность в свободных записях таблицы про- цессов на практике становится настолько острой, что в большинстве случаев для обработки удаленных запросов все-таки прибегают к ус- лугам процессов-серверов. Пользователь здддддддддддддддддддддддддддддд© Ё Ё Библиотека системных функций Ё Ё цдддддддддддддддддддддддддддддд╢ Ё Ё Уровень связи типа Newcastle Ё v цдддддддддддддддддддддддддддддд╢ ^ цдддддддддддддддддддддддддддддд╢ Ё Ё Подпрограмма обработки обра- Ё Ё Ё щения к системной функции Ё Ё цдддддддддддддддддддддддддддддд╢ з Периферийная Ё Ё Подпрограмма взаимодействия с<ЫЫЫЫ╢ система, Ё Ё удаленной файловой системой Ё Ё вызов удален- Ё цдддддддддддддддддддддддддддддд╢ ю ной системы Ё Ё Подсистема управления файлами<ЫЫЫЫЫЫВызов удален- Ядро юдддддддддддддддддддддддддддддды ной процедуры Рисунок 13.12. Концептуальная схема взаимодействия с удален- ными файлами на уровне ядра 13.5 ВЫВОДЫ В данной главе нами были рассмотрены три схемы работы с рас- положенными на удаленных машинах файлами, трактующие удаленные файловые системы как расширение локальной. Архитектурные различия между этими схемами показаны на Рисунке 13.12. Все они в свою очередь отличаются от многопроцессорных систем, описанных в пре- дыдущей главе, тем, что здесь процессоры не используют физическую память совместно. Система с периферийными процессорами состоит из сильносвязанного набора процессоров, совместно использующих фай- ловые ресурсы центрального процессора. Связь типа Newcastle обес- печивает скрытый ("прозрачный") доступ к удаленным файлам, но не средствами ядра операционной системы, а благодаря использованию специальной Си-библиотеки. По этой причине все программы, предпо- лагающие использовать связь данного типа, должны быть перекомпи- лированы, что в общем-то является серьезным недостатком этой схемы. Удаленность файла обозначается с помощью специальной пос- ледовательности символов, описывающих машину, на которой располо- жен файл, и это является еще одним фактором, ограничивающим мо- бильность программ. В "прозрачных" распределенных системах для доступа к удален- ным файлам используется модификация системной функции mount. Ин- дексы в локальной системе содержат отметку о том, что они отно- сятся к удаленным файлам, и локальное ядро посылает на удаленную систему сообщение, описывающее запрашиваемую системную функцию, ее параметры и удаленный индекс. Связь в "прозрачной" распреде- ленной системе поддерживается в двух формах: в форме вызова уда- ленной процедуры (на удаленную машину посылается сообщение, содержащее перечень операций, связанных с индексом) и в форме вы- зова удаленной системной функции (сообщение описывает запрашивае- мую функцию). В заключительной части главы рассмотрены вопросы, имеющие отношение к обработке дистанционных запросов с помощью процессов-спутников и серверов. 13.6 УПРАЖНЕНИЯ *1. Опишите реализацию системной функции exit в системе с пери- ферийными процессорами. В чем разница между этим случаем и тем, когда процесс завершает свою работу по получении непе- рехваченного сигнала ? Каким образом ядру следует сохранить дамп содержимого памяти ? 2. Процессы не могут игнорировать сигналы типа SIGKILL; объяс- ните, что происходит в периферийной системе, когда процесс получает такой сигнал. *3. Опишите реализацию системной функции exec в системе с пери- ферийными процессорами. *4. Каким образом центральному процессору следует производить распределение процессов между периферийными процессорами с тем, чтобы сбалансировать общую нагрузку ? *5. Что произойдет в том случае, если у периферийного процессора не окажется достаточно памяти для размещения всех выгружен- ных на него процессов ? Каким образом должны производиться выгрузка и подкачка процессов в сети ? 6. Рассмотрим систему, в которой запросы к удаленному файловому серверу посылаются в случае обнаружения в имени файла специ- ального префикса. Пусть процесс вызывает функцию execl("/../sftig/bin/sh","sh",0); Исполняемый модуль находится на удаленной машине, но должен выполняться в локальной системе. Объясните, каким образом удаленный модуль переносится в локальную систему. 7. Если администратору нужно добавить в существующую систему со связью типа Newcastle новые машины, то как об этом лучше всего проинформировать модули Си-библиотеки ? *8. Во время выполнения функции exec ядро затирает адресное пространство процесса, включая и библиотечные таблицы, ис- пользуемые связью типа Newcastle для слежения за ссылками на удаленные файлы. После выполнения функции процесс должен сохранить возможность обращения к этим файлам по их старым дескрипторам. Опишите реализацию этого момента. *9. Как показано в разделе 13.2, вызов системной функции exit в системах со связью типа Newcastle приводит к посылке сообще- ния процессу-спутнику, заставляющего последний завершить свою работу. Это делается на уровне библиотечных подпрог- рамм. Что происходит, когда локальный процесс получает сиг- нал, побуждающий его завершить свою работу в режиме ядра ? *10. Каким образом в системе со связью типа Newcastle, где уда- ленные файлы идентифицируются добавлением к имени специаль- ного префикса, пользователь может, указав в качестве компо- ненты имени файла ".." (родительский каталог), пересечь удаленную точку монтирования ? 11. Из главы 7 нам известно о том, что различные сигналы побуж- дают процесс сбрасывать дамп содержимого памяти в текущий каталог. Что должно произойти в том случае, если текущим яв- ляется каталог из удаленной файловой системы ? Какой ответ вы дадите в том случае, если в системе используется связь типа Newcastle ? *12. Какие последствия для локальных процессов имело бы удаление из системы всех процессов-спутников или серверов ? *13. Подумайте над тем, как в "прозрачной" распределенной системе следует реализовать алгоритм link, параметрами которого мо- гут быть два имени удаленных файлов, а также алгоритм exec, связанный с выполнением нескольких внутренних операций чте- ния. Рассмотрите две формы связи: вызов удаленной процедуры и вызов удаленной системной функции. *14. При обращении к устройству процесс-сервер может перейти в состояние приостанова, из которого он будет выведен драйве- ром устройства. Естественно, если число серверов ограничено, система не сможет больше удовлетворять запросы локальной ма- шины. Придумайте надежную схему, по которой в ожидании за- вершения ввода-вывода, связанного с устройством, приостанав- ливались бы не все процессы-серверы. Системная функция не прекратит свое выполнение, пока все серверы будут заняты. здддддддддд© здддддддддд© здддддддддд© Ё Клиент A Ё Ё Клиент B Ё Ё Клиент C Ё юддЫддддЫдды юддЫддддЫдды юддЫддддЫдды Ы Ы Ы Ы Ы Ы getty- Ы Ы Ы Ы Ы Ы процессыЫ Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы Ы зддЫддддЫддддддддддЫддддЫдддддддддддЫддддЫдд© терминаль- Ё Ы Ы Ы Ы Ы Ы Ё ный сервер юддбддддбддддддддддбддддбдддддддддддбддддбдды Ё Ё Ё Ё Ё Ё tty00 tty01 tty02 tty03 tty04 tty05 Рисунок 13.13. Конфигурация с терминальным сервером *15. Когда пользователь регистрируется в системе, дисциплина тер- минальной линии сохраняет информацию о том, что терминал яв- ляется операторским, ведущим группу процессов. По этой при- чине, когда пользователь на клавиатуре терминала нажимает клавишу "break", сигнал прерывания получают все процессы группы. Рассмотрим конфигурацию системы, в которой все тер- миналы физически подключаются к одной машине, но регистрация пользователей логически реализуется на других машинах (Рису- нок 13.13). В каждом отдельном случае система создает для удаленного терминала getty-процесс. Если запросы к удаленной системе обрабатываются с помощью набора процессов-серверов, следует отметить, что при выполнении процедуры открытия сер- вер останавливается в ожидании подключения. Когда выполнение функции open завершается, сервер возвращается обратно в сер- верный пул, разрывая свою связь с терминалом. Каким образом осуществляется рассылка сигнала о прерывании, вызываемого нажатием клавиши "break", по адресам процессов, входящих в одну группу ? *16. Разделение памяти - это особенность, присущая локальным ма- шинам. С логической точки зрения, выделение общей области физической памяти (локальной или удаленной) можно осущест- вить и для процессов, принадлежащих разным машинам. Опишите реализацию этого момента. *17. Рассмотренные в главе 9 алгоритмы выгрузки процессов и под- качки страниц по обращению предполагают использование ло- кального устройства выгрузки. Какие изменения следует внести в эти алгоритмы для того, чтобы создать возможность поддерж- ки удаленных устройств выгрузки ? *18. Предположим, что на удаленной машине (или в сети) случился фатальный сбой и локальный протокол сетевого уровня зафикси- ровал этот факт. Разработайте схему восстановления локальной системы, обращающейся к удаленному серверу с запросами. Кро- ме того, разработайте схему восстановления серверной систе- мы, утратившей связь с клиентами. *19. Когда процесс обращается к удаленному файлу, не исключена возможность того, что в поисках файла процесс обойдет нес- колько машин. В качестве примера возьмем имя "/usr/src/uts/3b2/os", где "/usr" - каталог, принадлежащий машине A, "/usr/src" - точка монтирования корня машины B, "/usr/src/uts/3b2" - точка монтирования корня машины C. Про- ход через несколько машин к месту конечного назначения назы- вается "мультискачком" (multihop). Однако, если между маши- нами A и C существует непосредственная сетевая связь, пере- сылка данных через машину B была бы неэффективной. Опишите особенности реализации "мультискачка" в системе со связью Newcastle и в "прозрачной" распределенной системе. ПРИЛОЖЕНИЕ СИСТЕМНЫЕ ОПЕРАЦИИ В приложении дается краткий обзор функций системы UNIX. Пол- ное описание этих функций содержится в руководстве программис- та-пользователя версии V системы UNIX. Сведений, приведенных здесь, вполне достаточно для того, чтобы разобраться в примерах программ, представленных в книге. Имена файлов, упоминаемые в тексте, представляют собой после- довательности символов, завершающиеся пустым символом и состоящие из компонент, разделенных наклонной чертой. В случае ошибки все функции возвращают код завершения, равный -1, а код самой ошибки засылается в переменную errno, имеющую тип external. В случае ус- пешного завершения код возврата имеет значение, равное 0. Некото- рые из обращений к операционной системе являются точкой входа сразу для нескольких функций: это означает, что данные функции используют один и тот же ассемблерный интерфейс. Приводимый спи- сок функций удовлетворяет стандартным условиям, принятым в спра- вочных руководствах по системе UNIX, при этом вопросы, связанные с тем, является ли одно обращение к операционной системе точкой входа для одной или нескольких функций, рассматриваются отдельно. access access(filename,mode) char *filename; int mode; Функция access проверяет, имеет ли процесс разрешение на чте- ние, запись или исполнение файла (проверяемый тип доступа зависит от значения параметра mode). Значение mode является комбинацией двоичных масок 4 (для чтения), 2 (для записи) и 1 (для исполне- ния). Вместо исполнительного кода идентификации пользователя в проверке участвует фактический код. acct acct(filename) char *filename; Функция acct включает учет системных ресурсов, если параметр filename непустой, и выключает - в противном случае. alarm unsigned alarm(seconds) unsigned seconds; Функция alarm планирует посылку вызывающему ее процессу сиг- нала тревоги через указанное количество секунд (seconds). Она возвращает число секунд, оставшееся до посылки сигнала от момента вызова функции. brk int brk(end_data_seg) char *end_data_seg; Функция brk устанавливает верхнюю границу (старший адрес) об- ласти данных процесса в соответствии со значением параметра end_data_seg. Еще одна функция, sbrk, использует ту же точку вхо- да и увеличивает адрес верхней границы области на указанную вели- чину. chdir chdir(filename) char *filename; Функция chdir делает текущим каталогом вызывающего процесса каталог, указанный в параметре filename. chmod chmod(filename,mode) char *filename; Функция chmod изменяет права доступа к указанному файлу в со- ответствии со значением параметра mode, являющимся комбинацией из следующих кодов (в восьмеричной системе): 04000 бит установки кода идентификации пользователя 02000 бит установки группового кода идентификации 01000 признак sticky bit 00400 чтение владельцем 00200 запись владельцем 00100 исполнение владельцем 00040 чтение групповым пользователем 00020 запись групповым пользователем 00010 исполнение групповым пользователем 00004 чтение прочим пользователем 00002 запись прочим пользователем 00001 исполнение прочим пользователем chown chown(filename,owner,group) char *filename; int owner,group; Функция chown меняет коды идентификации владельца и группы для указанного файла на коды, указанные в параметрах owner и group. chroot chroot(filename) char *filename; Функция chroot изменяет частный корень вызывающего процесса в соответствии со значением параметра filename. close close(fildes) int fildes; Функция close закрывает дескриптор файла, полученный в ре- зультате выполнения функций open, creat, dup, pipe или fcntl, или унаследованный от функции fork. creat creat(filename,mode) char *filename; int mode; Функция creat создает новый файл с указанными именем и права- ми доступа. Параметр mode имеет тот же смысл, что и в функции access, при этом признак sticky-bit очищен, а разряды, установ- ленные функцией umask, сброшены. Функция возвращает дескриптор файла для последующего использования в других функциях. dup dup(fildes) int fildes; Функция dup создает копию указанного дескриптора файла, возв- ращая дескриптор с наименьшим номером из имеющихся в системе. Старый и новый дескрипторы используют один и тот же указатель на файл, а также и другие совпадающие атрибуты. exec execve(filename,argv,envp) char *filename; char *argv[]; char *envp[]; Функция execve исполняет файл с именем filename, загружая его в адресное пространство текущего процесса. Параметр argv соот- ветствует списку аргументов символьного типа, передаваемых запус- каемой программе, параметр envp соответствует массиву, описываю- щему среду выполнения нового процесса. exit exit(status) int status; Функция exit завершает вызывающий процесс, возвращая его ро- дителю 8 младших разрядов из слова состояния процесса. Ядро само может вызывать эту функцию в ответ на поступление определенных сигналов. fcntl fcntl(fildes,cmd,arg) int fildes,cmd,arg; Функция fcntl обеспечивает выполнение набора разнообразных операций по отношению к открытым файлам, идентифицируемым с по- мощью дескриптора fildes. Параметры cmd и arg интерпретируются следующим образом (определение буквенных констант хранится в фай- ле "/usr/include/fcntl.h"): F_DUPFD вернуть наименьшее значение дескриптора, большее или равное значению arg F_SETFD установить флаг "close-on-exec" в младшем разря- де arg (файл будет закрыт функцией exec) F_GETFD вернуть состояние флага "close-on-exec" F_SETFL установить флаги, управляющие состоянием файла (O_NDELAY - не приостанавливаться в ожидании за- вершения ввода-вывода, O_APPEND - записываемые данные добавлять в конец файла) F_GETFL получить значения флагов, управляющих состоянием файла struct flock short l_type; /* F_RDLCK - блокировка чтения, F_WRLCK - блокировка записи, F_UNLCK - снятие блокировки */ short l_whence; /* адрес начала блокируемого участ- ка дается в виде смещения отно- сительно начала файла (0), отно- сительно текущей позиции указа- теля (1), относительно конца файла (2) */ long l_start; /* смещение в байтах, интерпретиру- емое в соответствии со значением l_whence */ long l_len; /* длина блокируемого участка в байтах. Если указан 0, блокиру- ется участок от l_start до конца файла */ long l_pid; /* идентификатор процесса, блокиру- ющего файл */ long l_sysid; /* системный идентификатор процес- са, блокирующего файл */ F_GETLK прочитать первый код блокировки, мешающей ис- пользовать значение arg и затирать его. Если блокировка отсутствует, поменять значение l_type в arg на F_UNLCK F_SETLK установить или снять блокировку файла в зависи- мости от значения arg. В случае невозможности установить блокировку вернуть -1 F_SETLKW установить или снять блокировку содержащихся в файле данных в зависимости от значения arg. В случае невозможности установить блокировку при- остановить выполнение Блокировки, связанные с чтением из файла, могут перекрывать друг друга. Блокировки, связанные с записью, перекрываться не мо- гут. fork fork() Функция fork создает новый процесс. Порождаемый процесс представляет собой логическую копию процесса-родителя. На выходе из функции процессу-родителю возвращается код идентификации по- томка, потомку - нулевое значение. getpid getpid() Функция getpid возвращает идентификатор вызывающего процесса. Эту же точку входа используют функции: getpgrp, возвращающая идентификатор группы, в которую входит вызывающий процесс, и getppid, возвращающая идентификатор процесса, который является родителем текущего процесса. getuid getuid() Функция getuid возвращает фактический код идентификации поль- зователя вызывающего процесса. Эту же точку входа используют функции: geteuid, возвращающая исполнительный код идентификации пользователя, getgid, возвращающая групповой код, и getegid, возвращающая исполнительный групповой код идентификации вызываю- щего процесса. ioctl ioctl(fildes,cmd,arg) int fildes,cmd; Функция ioctl выполняет набор специальных операций по отноше- нию к открытому устройству, дескриптор которого указан в парамет- ре fildes. Тип команды, выполняемой по отношению к устройству, описывается параметром cmd, а параметр arg является аргументом команды. kill kill(pid,sig) int pid,sig; Функция kill посылает процессам, идентификаторы которых ука- заны в параметре pid, сигнал, описываемый параметром sig. pid имеет сигнал посылается процессу с идентифика- положитель- тором pid ное значение pid = 0 сигнал посылается процессам, групповой идентификатор которых совпадает с иден- тификатором отправителя pid = -1 если процесс-отправитель исполняется под идентификатором суперпользователя, сиг- нал посылается всем процессам, в против- ном случае, сигнал посылается процессам, фактический код идентификации пользова- теля у которых совпадает с идентификато- ром суперпользователя pid < -1 сигнал посылается процессам, групповой идентификатор которых совпадает с pid Исполнительный код идентификации пользователя процесса-отпра- вителя должен указывать на суперпользователя, в противном случае, фактический или исполнительный коды идентификации отправителя должны совпадать с соответствующими кодами процессов-получателей. link link(filename1,filename2) char *filename1,*filename2; Функция link присваивает файлу filename1 новое имя filename2. Файл становится доступным под любым из этих имен. lseek lseek(fildes,offset,origin) int fildes,origin; long offset; Функция lseek изменяет положение указателя чтения-записи для файла с дескриптором fildes и возвращает новое значение. Положе- ние указателя зависит от значения параметра origin: 0 установить указатель на позицию, соответствующую ука- занному смещению в байтах от начала файла 1 сдвинуть указатель с его текущей позиции на указанное смещение 2 установить указатель на позицию, соответствующую ука- занному смещению в байтах от конца файла mknod mknod(filename,modes,dev) char *filename; int mode,dev; Функция mknod создает специальный файл, каталог или поимено- ванный канал (очередь по принципу "первым пришел - первым вышел") в зависимости от значения параметра modes: 010000 поименованный канал 020000 специальный файл устройства ввода-вывода символами 040000 каталог 060000 специальный файл устройства ввода-вывода блоками 12 младших разрядов параметра modes имеют тот же самый смысл, что и в функции chmod. Если файл имеет специальный тип, параметр dev содержит старший и младший номера устройства. mount mount(specialfile,dir,rwflag) char *specialfile,*dir; int rwflag; Функция mount выполняет монтирование файловой системы, на ко- торую указывает параметр specialfile, в каталоге dir. Если млад- ший бит параметра rwflag установлен, файловая система монтируется только для чтения. msgctl #include #include #include msgctl(id,cmd,buf) int id,cmd; struct msgid_ds *buf; В зависимости от операции, указанной в параметре cmd, функция msgctl дает процессам возможность устанавливать или запрашивать информацию о статусе очереди сообщений с идентификатором id, а также удалять очередь из системы. Структура msquid_ds определена следующим образом: struct ipc_perm { ushort uid; /* идентификатор текущего пользователя */ ushort gid; /* идентификатор текущей группы */ ushort cuid; /* идентификатор пользователя-создателя */ ushort cgid; /* идентификатор группы создателя */ ushort mode; /* права доступа */ short pad1; /* используется системой */ long pad2; /* используется системой */ }; struct msquid_ds { struct ipc_perm msg_perm; /* структура, описывающая права доступа */ short pad1[7]; /* используется системой */ ushort msg_qnum; /* количество сообщений в очереди */ ushort msg_qbytes; /* максимальный размер очереди в байтах */ ushort msg_lspid; /* идентификатор процесса, связанного с последней посылкой сообщения */ ushort msg_lrpid; /* идентификатор процесса, связанного с последним получением сообщения */ time_t msg_stime; /* время последней посылки сообщения */ time_t msg_rtime; /* время последнего полу- чения сообщения */ time_t msg_ctime; /* время последнего изме- нения */ }; Типы операций: IPC_STAT Прочитать в буфер заголовок очереди сообщений, ас- социированный с идентификатором id IPC_SET Установить значения переменных msg_perm.uid, msg_perm.gid, msg_perm.mode (9 младших разрядов структуры msg_perm) и mgr_qbytes в соответствии со значениями, содержащимися в буфере IPC_RMID Удалить из системы очередь сообщений с идентифика- тором id msgget #include #include #include msgget(key,flag) key_t key; int flag; Функция msgget возвращает идентификатор очереди сообщений, имя которой указано в key. Параметр key может указывать на то, что возвращаемый идентификатор относится к частной очереди (IPC_PRIVATE), в этом случае создается новая очередь сообщений. С помощью параметра flag можно сделать указание о необходимости создания очереди (IPC_CREAT), а также о том, что создание очереди должно выполняться монопольно (IPC_EXCL). В последнем случае, ес- ли очередь уже существует, функция msgget дает отказ. msgsnd и msgrcv #include #include #include msgsnd(id,msgp,size,flag) int id,size,flag; struct msgbuf *msgp; msgrcv(id,msgp,size,type,flag) int id,size,type,flag; struct msgbuf *msgmp; Функция msgsnd посылает сообщение указанного размера в байтах (size) из буфера msgp в очередь сообщений с идентификатором id. Структура msgbuf определена следующим образом: struct msgbuf { long mtype; char mtext[]; }; Если в параметре flag бит IPC_NOWAIT сброшен, функция msgsnd будет приостанавливаться в тех случаях, когда размер отдельного сообщения или число сообщений в системе превышают допустимый мак- симум. Если бит IPC_NOWAIT установлен, функция msgsnd в этих слу- чаях прерывает свое выполнение. Функция msgrcv принимает сообщение из очереди с идентификато- ром id. Если параметр type имеет нулевое значение, из очереди бу- дет выбрано сообщение, первое по счету; если положительное значе- ние, из очереди выбирается первое сообщение данного типа; если отрицательное значение, из очереди выбирается сообщение, имеющее самый младший тип среди тех типов, значение которых не превышает абсолютное значение параметра type. В параметре size указывается максимальный размер сообщения, ожидаемого пользователем. Если в параметре flag установлен бит MSG_NOERROR, в том случае, когда размер получаемого сообщения превысит предел, установленный пара- метром size, ядро обрежет это сообщение. Если же соответствующий бит сброшен, в подобных случаях функция будет возвращать ошибку. Если в параметре flag бит IPC_NOWAIT сброшен, функция msgrcv при- остановит свое выполнение до тех пор, пока сообщение, удовлетво- ряющее указанному в параметре type условию, не будет получено. Если соответствующий бит сброшен, функция завершит свою работу немедленно. Функция msgrcv возвращает размер полученного сообще- ния (в байтах). nice nice(increment) int increment; Функция nice увеличивает значение соответствующей компоненты, участвующей в вычислении приоритета планирования текущего процес- са, на величину increment. Увеличение значения nice ведет к сни- жению приоритета планирования. open #include open(filename,flag,mode) char *filename; int flag,mode; Функция open выполняет открытие указанного файла в соответс- твии со значением параметра flag. Значение параметра flag предс- тавляет собой комбинацию из следующих разрядов (причем из первых трех разрядов может быть использован только один): O_RDONLY открыть только для чтения O_WRONLY открыть только для записи O_RDWR открыть для чтения и записи O_NDELAY если файл является специальным файлом устрой- ства, функция возвращает управление, не дожида- ясь ответного сигнала; если файл является поиме- нованным каналом, функция в случае неудачи возвращает управление немедленно (с индикацией ошибки, когда бит O_WRONLY установлен), не дожи- даясь открытия файла другим процессом O_APPEND добавляемые данные записывать в конец файла O_CREAT если файл не существует, создать его; режим соз- дания (mode) имеет тот же смысл, что и в функции creat; если файл уже существует, данный флаг иг- норируется O_TRUNC укоротить длину файла до 0 O_EXCL если этот бит и бит O_CREAT установлены и файл существует, функция не будет выполняться; это так называемое "монопольное открытие" Функция open возвращает дескриптор файла для последующего ис- пользования в других системных функциях. pause pause() Функция pause приостанавливает выполнение текущего процесса до получения сигнала. pipe pipe(fildes) int fildes[2]; Функция pipe возвращает дескрипторы чтения и записи (соот- ветственно, в fildes[0] и fildes[1]) для данного канала. Данные передаются через канал в порядке поступления; одни и те же данные не могут быть прочитаны дважды. plock #include plock(op) int op; Функция plock устанавливает и снимает блокировку областей процесса в памяти в зависимости от значения параметра op: PROCLOCK заблокировать в памяти области команд и данных TXTLOCK заблокировать в памяти область команд DATLOCK заблокировать в памяти область данных UNLOCK снять блокировку всех областей profil profil(buf,size,offset,scale) char *buf; int size,offset,scale; Функция profil запрашивает у ядра профиль выполнения процес- са. Параметр buf определяет массив, накапливающий число копий процесса, выполняющихся в разных адресах. Параметр size определя- ет размер массива buf, offset - начальный адрес участка профили- рования, scale - коэффициент масштабирования. ptrace ptrace(cmd,pid,addr,data) int cmd,pid,addr,data; Функция ptrace дает текущему процессу возможность выполнять трассировку другого процесса, имеющего идентификатор pid, в соот- ветствии со значением параметра cmd: 0 разрешить трассировку потомку (по его указанию) 1,2 вернуть слово, расположенное по адресу addr в прост- ранстве трассируемого процесса с идентификатором pid 3 вернуть слово, расположенное в пространстве трассиру- емого процесса по адресу со смещением addr 4,5 записать значение по адресу addr в пространстве трас- сируемого процесса 6 записать значение по адресу со смещением addr 7 заставить трассируемый процесс возобновить свое вы- полнение 8 заставить трассируемый процесс завершить свое выпол- нение 9 машинно-зависимая команда - установить в слове состо- яния программы бит для отладки в режиме пошагового выполнения read read(fildes,buf,size) int fildes; char *buf; int size; Функция read выполняет чтение из файла с дескриптором fildes в пользовательский буфер buf указанного в параметре size коли- чества байт. Функция возвращает число фактически прочитанных байт. Если файл является специальным файлом устройства или кана- лом и если в вызове функции open был установлен бит O_NDELAY, функция read в случае отсутствия доступных для чтения данных возвратит управление немедленно. semctl #include #include #include semctl(id,num,cmd,arg) int id,num,cmd; union semun { int val; struct semid_ds *buf; ushort *array; } arg; Функция semctl выполняет указанную в параметре cmd операцию над очередью семафоров с идентификатором id. GETVAL вернуть значение того семафора, на который указы- вает параметр num SETVAL установить значение семафора, на который указыва- ет параметр num, равным значению arg.val GETPID вернуть идентификатор процесса, выполнявшего пос- ледним функцию semop по отношению к тому семафо- ру, на который указывает параметр num GETNCNT вернуть число процессов, ожидающих того момента, когда значение семафора станет положительным GETZCNT вернуть число процессов, ожидающих того момента, когда значение семафора станет нулевым GETALL вернуть значения всех семафоров в массиве arg.array SETALL установить значения всех семафоров в соответствие с содержимым массива arg.array IPC_STAT считать структуру заголовка семафора с идентифи- катором id в буфер arg.buf IPC_SET установить значения переменных sem_perm.uid, sem_perm.gid и sem_perm.mode (младшие 9 разрядов структуры sem_perm) в соответствии с содержимым буфера arg.buf IPC_RMID удалить семафоры, связанные с идентификатором id, из системы Параметр num возвращает на количество семафоров в обрабатыва- емом наборе. Структура semid_ds определена следующим образом: struct semid_ds { struct ipc_perm sem_perm; /* структура, описыва- ющая права досту- па */ int * pad; /* используется систе- мой */ ushort sem_nsems; /* количество семафо- ров в наборе */ time_t sem_otime; /* время выполнения последней операции над семафором */ time_t sem_ctime; /* время последнего изменения */ }; Структура ipc_perm имеет тот же вид, что и в функции msgctl. semget #include #include #include semget(key,nsems,flag) key_t key; int nsems,flag; Функция semget создает массив семафоров, корреспондирующий с параметром key. Параметры key и flag имеют тот же смысл, что и в функции msgget. semop semop(id,ops,num) int id,num; struct sembuf **ops; Функция semop выполняет набор операций, содержащихся в струк- туре ops, над массивом семафоров, связанных с идентификатором id. Параметр num содержит количество записей, составляющих структуру ops. Структура sembuf определена следующим образом: struct sembuf { short sem_num; /* номер семафора */ short sem_op; /* тип операции над семафором */ short sem_flg; /* флаг */ }; Переменная sem_num содержит указатель в массиве семафоров, ассоциированный с данной операцией, а переменная sem_flg - флаги для данной операции. Переменная sem_op может принимать следующие значения: отрицательное если сумма значения семафора и значения sem_op >= 0, значение семафора изменяется на величину sem_op; в противном случае, функция приостанавливает свое выполнение, если это разрешено флагом положительное увеличить значение семафора на величину sem_op нулевое если значение семафора равно 0, продол- жить выполнение; в противном случае, при- остановить выполнение, если это разреша- ется флагом Если для данной операции в переменной sem_flg установлен флаг IPC_NOWAIT, функция semop возвращает управление немедленно в тех случаях, когда она должна была бы приостановиться. Если установ- лен флаг SEM_UNDO, восстанавливается предыдущее значение семафора (sem_op вычитается из текущей суммы типов операций). Когда про- цесс завершится, значение семафора будет увеличено на эту сумму. Функция semop возвращает значение последней операции над семафо- ром. setpgrp setpgrp() Функция setpgrp приравнивает значение идентификатора группы, к которой принадлежит текущий процесс, значению идентификатора самого процесса и возвращает новое значение идентификатора груп- пы. setuid setuid(uid) int uid; setgid(gid) int gid; Функция setuid устанавливает значения фактического и исполни- тельного кодов идентификации пользователя текущего процесса. Если вызывающий процесс исполняется под управлением суперпользователя, функция сбрасывает значения указанных кодов. В противном случае, если фактический код идентификации пользователя имеет значение, равное значению uid, функция setuid делает равным этому значению и исполнительный код идентификации пользователя. То же самое про- исходит, если значению uid равен код, сохраненный после выполне- ния setuid-программы, запускаемой с помощью функции exec. Функция setgid имеет тот же смысл по отношению к аналогичным групповым кодам. shmctl #include #include #include shmctl(id,cmd,buf) int id,cmd; struct shmid_ds *buf; Функция shmctl выполняет различные операции над областью раз- деляемой памяти, ассоциированной с идентификатором id. Структура shmid_ds определена следующим образом: struct shmid_ds { struct ipc_perm shm_perm; /* структура, описываю- щая права доступа */ int shm_segsz; /* размер сегмента */ int * pad1; /* используется систе- мой */ ushort shm_lpid; /* идентификатор про- цесса, связанного с последней операцией над областью */ ushort shm_cpid; /* идентификатор про- цесса-создателя */ ushort shm_nattch; /* количество присоеди- нений к процессам */ short pad2; /* используется систе- мой */ time_t shm_atime; /* время последнего присоединения */ time_t shm_dtime; /* время последнего отсоединения */ time_t shm_ctime; /* время последнего внесения измене- ний */ }; Операции: IPC_STAT прочитать в буфер buf содержимое заголовка об- ласти, ассоциированной с идентификатором id IPC_SET установить значения переменных shm_perm.uid, shm_perm.gid и shm_perm.mode (9 младших разря- дов структуры) в заголовке области в соответс- твии с содержимым буфера buf IPC_RMID удалить из системы область разделяемой памяти, ассоциированной с идентификатором id shmget #include #include #include shmget(key,size,flag) key_t key; int size,flag; Функция shmget обращается к области разделяемой памяти или создает ее. Параметр size задает размер области в байтах. Пара- метры key и flag имеют тот же смысл, что и в функции msgget. shmop #include #include #include shmat(id,addr,flag) int id,flag; char *addr; shmdt(addr) char *addr; Функция shmat присоединяет область разделяемой памяти, ассо- циированную с идентификатором id, к адресному пространству про- цесса. Если параметр addr имеет нулевое значение, ядро само выби- рает для присоединения области подходящий адрес. В противном слу- чае оно пытается присоединить область, используя в качестве значение параметра addr в качестве адреса. Если в параметре flag установлен бит SHM_RND, ядро в случае необходимости округляет ад- рес. Функция shmat возвращает адрес, по которому область присое- диняется фактически. Функция shmdt отсоединяет область разделяемой памяти, присое- диненную ранее по адресу addr. signal #include signal(sig,function) int sig; void (*func)(); Функция signal дает текущему процессу возможность управлять обработкой сигналов. Параметр sig может принимать следующие зна- чения: SIGHUP "зависание" SIGINT прерывание SIGQUIT прекращение работы SIGILL запрещенная команда SIGTRAP внутреннее прерывание, связанное с трассировкой SIGIOT инструкция IOT SIGEMT инструкция EMT SIGFPE особая ситуация при работе с числами с плавающей запятой SIGKILL удаление из системы SIGBUS ошибка в шине SIGSEGV нарушение сегментации SIGSYS недопустимый аргумент в вызове системной функции SIGPIPE запись в канал при отсутствии считывающих процес- сов SIGALRM сигнал тревоги SIGTERM завершение программы SIGUSR1 сигнал, определяемый пользователем SIGUSR2 второй сигнал, определяемый пользователем SIGCLD гибель потомка SIGPWR отказ питания Параметр function интерпретируется следующим образом: SIG_DFL действие по умолчанию. Означает завершение про- цесса в случае поступления любых сигналов, за ис- ключением SIGPWR и SIGCLD. Если сигнал имеет тип SIGQUIT, SIGILL, SIGTRAP, SIGIOT, SIGEMT, SIGFPE, SIGBUS, SIGSEGV или SIGSYS, создается файл "core", содержащий дамп образа процесса в памяти SIG_IGN игнорировать поступление сигнала функция адрес процедуры в пространстве процесса. По воз- вращении в режим задачи производится обращение к указанной функции с передачей ей номера сигнала в качестве аргумента. Если сигнал имеет тип, отлич- ный от SIGILL, SIGTRAP и SIGPWR, ядро автомати- чески переустанавливает имя программы обработки сигнала в SIG_DFL. Сигналы типа SIGKILL процессом не обрабатываются stat stat(filename,statbuf) char *filename; struct stat *statbuf; fstat(fd,statbuf) int fd; struct stat *statbuf; Функция stat возвращает информацию о статусе (состоянии) ука- занного файла. Функция fstat выполняет то же самое в отношении открытого файла, имеющего дескриптор fd. Структура statbuf опре- делена следующим образом: struct stat { dev_t st_dev; /* номер устройства, на котором на- ходится файл */ ino_t st_ino; /* номер индекса */ ushort st_mode; /* тип файла (см. mknod) и права доступа к нему (см. chmod) */ short st_nlink; /* число связей, указывающих на файл */ ushort st_uid; /* код идентификации владельца фай- ла */ ushort st_gid; /* код идентификации группы */ dev_t st_rdev; /* старший и младший номера устрой- ства */ off_t st_size; /* размер в байтах */ time_t st_atime; /* время последнего обращения */ time_t st_mtime; /* время последнего внесения изме- нений */ time_t st_ctime; /* время последнего изменения ста- туса */ }; stime stime(tptr) long *tptr; Функция stime устанавливает системное время и дату в соот- ветствие со значением, указанным в параметре tptr. Время указыва- ется в секундах от 00:00:00 1 января 1970 года по Гринвичу. sync sync() Функция sync выгружает содержащуюся в системных буферах ин- формацию (относящуюся к файловой системе) на диск. time time(tloc) long *tloc; Функция time возвращает системное время в секундах от 00:00:00 1 января 1970 года по Гринвичу. times #include #include times(tbuf) struct tms *tbuf; Функция times возвращает время в таймерных тиках, реально прошедшее с любого произвольного момента в прошлом, и заполняет буфер tbuf следующей учетной информацией: struct tms { time_t tms_utime; /* продолжительность использова- ния ЦП в режиме задачи */ time_t tms_stime; /* продолжительность использова- ния ЦП в режиме ядра */ time_t tms_cutime; /* сумма значений tms_utime и tms_cutime у потомков */ time_t tms_sutime; /* сумма значений tms_stime и tms_sutime у потомков */ }; ulimit ulimit(cmd,limit) int cmd; long limit; Функция ulimit дает процессу возможность устанавливать раз- личные ограничения в зависимости от значения параметра cmd: 1 вернуть максимальный размер файла (в блоках по 512 байт), в который процесс может вести запись 2 установить ограничение сверху на размер файла равным значению параметра limit 3 вернуть значение верхней точки прерывания (максималь- ный доступный адрес в области данных) umask umask(mask) int mask; Функция umask устанавливает значение маски, описывающей режим создания файла (mask), и возвращает старое значение. При создании файла биты разрешения доступа, которым соответствуют установлен- ные разряды в mask, будут сброшены. umount umount(specialfile) char *specialfile Функция umount выполняет демонтирование файловой системы, расположенной на устройстве ввода-вывода блоками specialfile. uname #include uname(name) struct utsname *name; Функция uname возвращает информацию, идентифицирующую систему в соответствии со следующей структурой: struct utsname { char sysname[9]; /* наименование */ char nodename[9]; /* имя сетевого узла */ char release[9]; /* информация о версии системы */ char version[9]; /* дополнительная информация о версии */ char machine[9]; /* технический комплекс */ }; unlink unlink(filename) char *filename; Функция unlink удаляет из каталога запись об указанном файле. ustat #include #include ustat(dev,ubuf) int dev; struct ustat *ubuf; Функция ustat возвращает статистические данные, характеризую- щие файловую систему с идентификатором dev (старший и младший но- мера устройства). Структура ustat определена следующим образом: struct ustat { daddr_t f_tfree; /* количество свободных бло- ков */ ino_t f_tinode; /* количество свободных индек- сов */ char f_fname[6]; /* наименование файловой систе- мы */ char f_fpack[6]; /* сокращенное (упакованное) имя файловой системы */ }; utime #include utime(filename,times) char *filename; struct utimbuf *times; Функция utime переустанавливает время последнего обращения к указанному файлу и последнего внесения изменений в соответствии со значениями, на которые указывает параметр times. Если параметр содержит нулевое значение, используется текущее время. В против- ном случае параметр указывает на следующую структуру: struct utimbuf { time_t axtime; /* время последнего обращения */ time_t modtime; /* время последнего внесения изме- нений */ }; Все значения отсчитываются от 00:00:00 1 января 1970 года по Гринвичу. wait wait(wait_stat) int *wait_stat; Функция wait побуждает процесс приостановить свое выполнение до момента завершения потомка или до момента приостанова трасси- руемого процесса. Если значение параметра wait_stat ненулевое, оно представляет собой адрес, по которому функция записывает возвращаемую процессу информацию. При этом используются только 16 младших разрядов кода возврата. Если обнаружен завершивший свое выполнение потомок, 8 младших разрядов кода возврата содержат 0, а 8 старших разрядов - код возврата (аргумент) функции exit. Если потомок завершил свое выполнение в результате получения сигнала, код возврата функции exit содержит номер сигнала. Кроме того, ес- ли образ процесса-потомка сохранен в файле "core", производится установка бита 0200. Если обнаружен приостановивший свое выполне- ние трассируемый процесс, 8 старших разрядов кода возврата функ- ции wait содержат номер приведшего к его приостанову сигнала, а 8 младших разрядов - восьмиричное число 0177. write write(fd,buf,count) int fd,count; char *buf; Функция write выполняет запись указанного в count количества байт данных, начиная с адреса buf, в файл с дескриптором fd.