Copyright c 1986 Корпорация Bell Telephone Laboratories. Издано корпорацией Prentice-Hall. Отделение Simon & Schuster Энглвуд Клиффс, Нью-Джерси 07632 Серия книг по программному обеспечению издательства Prentice Hall. Консультант Брайан В. Керниган Настоящее издание предназначено для распространения в тех странах, которые получили на это право от корпорации Prentice-Hall International. Не подлежит вывозу и распространению в США, Мексике и Канаде. UNIX - зарегистрированная торговая марка корпорации AT&T. DEC, PDP и VAX - торговые марки корпорации Digital Equipment Corp. Series 32000 - торговая марка корпорации National Semiconductor Corp. Ada - зарегистрированная торговая марка пра- вительства США (Ada Joint Program Office). UNIVAC - торговая мар- ка корпорации Sperry Corp. Настоящий документ набран на фотонаборном устройстве APS-5 корпорации AUTOLOGIC с помощью форматера TROFF в операционной среде UNIX ЭВМ AT&T 3B20. В процессе подготовки книги ее автором и издателем было при- ложено немало усилий. Эти усилия были связаны с проведением тео- ретических разработок и исследований и с всесторонней проверкой действенности теоретических выкладок и программ. Автор и издатель не дают никаких гарантий, как ясно выраженных, так и подразумева- ющихся, относительно включенных в книгу программ и документации. Автор и издатель не отвечают за случайные или производные непо- ладки, возникающие в связи или в результате установки, выполнени или использования этих программ. Все права сохраняются. Запрещается воспроизведение отдельных частей книги без письменного разрешения издателя. Отпечатано в Соединенных Штатах Америки. 10 9 8 7 6 5 ISBN 0-13-201757-1 025 Prentice-Hall International (UK) Limited, Лондон Prentice-Hall of Australia Pty. Limited, Сидней Prentice-Hall Canada Inc., Торонто Prentice-Hall Hispanoamericana, S.A., Мехико Prentice-Hall of India Private Limited, Нью-Дели Prentice-Hall of Japan, Inc., Токио Prentice-Hall of Southeast Asia Pte. Ltd., Сингапур Editora Prentice-Hall do Brasil, Ltda., Рио-де-Жанейро Prentice-Hall, Inc., Энглвуд Клиффс, Нью-Джерси ПРЕДИСЛОВИЕ Впервые система 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 оказали самую разнообразную помощь в придании книге ее окончательного вида. Последней по списку, но не по величине явилась помощь моей жены, Дебби, оказавшей мне эмоциональную поддержку, без которой я бы не достиг успеха. ГЛАВА 1 ОБЩИЙ ОБЗОР ОСОБЕННОСТЕЙ EСИСТЕМЫ За время, прошедшее с момента ее появления в 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]. Би был ин- терпретируемым языком со всеми недостатками, присущими подобным языкам, поэ- тому Ричи переделал его в новую разновидность, получившую название Си | и разрешающую генерировать машинный код, объявлять типы данных и определять структуру данных. В 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 (**), и эта версия стала официально распространяться корпорацией 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. Операционная система взаимодействует с аппаратурой непосредственно --------------------------- (**) А что же версия IV ? Модификация внутреннего варианта системы получила название "версия V". (***), обеспечивая обслуживание программ и их независимость от деталей аппа- ратной конфигурации. Если представить систему состоящей из пластов, в ней можно выделить системное ядро, изолированное от пользовательских +----------------------------------------------------------------+ | | Другие прикладные программы | | +----+-----------------------------------------------+ | | | 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 операционная система взаимодейст- вует с собственной операционной системой, которая, в свою очередь, взаимодействует с аппаратурой и выполняет необходимые функции по обс- луживанию системы. В таких реализациях допускается инсталляция других операционных систем с загрузкой под их управлением прикладных программ параллельно с системой UNIX. Классическим примером подобной реализации явилась система MERT [Lycklama 78a]. Более новым примером могут слу- жить реализации для компьютеров серии IBM 370 [Felton 84] и UNIVAC 1100 [Bodenstab 84]. вания, принятый в системе 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 в качестве параметра, что указывает на успешное заверше- ние выполнения программы. Программа копирует любые файлы, указанные при ее вызове в качестве аргу- ментов, при условии, что разрешено открытие существующего файла и создание нового файла. Файл может включать в себя как текст, который может быть выве- ден на печатающее устройство, например, исходный текст программы, так и сим- волы, не выводимые на печать, даже саму программу. Таким образом, оба вызо- ва: copy copy.c newcopy.c copy copy newcopy являются допустимыми. Существующий файл также может быть каталогом. Напри- мер, по вызову: copy . dircontents копируется содержимое текущего каталога, обозначенного символом ".", в обыч- ный файл "dircontents"; информация в новом файле совпадает, вплоть до каждо- го байта, с содержимым каталога, только этот файл обычного типа (для созда- ния нового каталога предназначена операция mknod). Наконец, любой из файлов может быть файлом устройства. Например, программа, вызванная следующим обра- зом: +------------------------------------------------------------+ | #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 /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, пользователь запускает ее следующим образом: +------------------------------------------------------------+ | 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. Программа порождения нового процесса, выполняющего копиро- вание файлов run oldfile newfile Процесс выполняет копирование файла с именем "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 ../.. --------------------------- (****) Каталог "/bin" содержит большинство необходимых команд и обычно вхо- дит в число каталогов, в которых ведет поиск командный процессор shell. делает текущим каталог, который на две вершины "ближе" к корню (корневому каталогу): параметр ".." относится к каталогу, являющемуся родительским дл текущего. Поскольку shell является пользовательской программой и не входит в сос- тав ядра операционной системы, его легко модифицировать и помещать в конк- ретные условия эксплуатации. Например, вместо командного процессора Баурна (называемого так по имени его создателя, Стива Баурна), являющегося частью версии V стандартной системы, можно использовать процессор команд Си, обес- печивающий работу механизма ведения истории изменений и позволяющий избегать повторного ввода только что использованных команд. В некоторых случаях при желании можно воспользоваться командным процессором shell с ограниченными возможностями, являющимся предыдущей версией обычного shell'а. Система может работать с несколькими командными процессорами одновременно. Пользователи имеют возможность запускать одновременно множество процессов, процессы же в свою очередь могут динамически порождать новые процессы и синхронизировать их выполнение. Все эти возможности обеспечиваются благодаря наличию мощных программных и аппаратных средств, составляющих среду выполнения процессов. Хотя привлекательность shell'а в наибольшей степени определяется его возмож- ностями как языка программирования и его возможностями в обработке аргумен- тов, в данном разделе основное внимание концентрируется на среде выполнени процессов, управление которой в системе возложено на командный процессор shell. Другие важные особенности 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 уровень ядра операционной системы изображен непосредст- венно под уровнем прикладных программ пользователя. Выполняя различные эле- ментарные операции по запросам пользовательских процессов, ядро обеспечивает функционирование пользовательского интерфейса, описанного выше. Среди функ- ций ядра можно отметить: * Управление выполнением процессов посредством их создания, завершения или приостановки и организации взаимодействия между ними. * Планирование очередности предоставления выполняющимся процессам времени центрального процессора (диспетчеризация). Процессы работают с центральным процессором в режиме разделения времени: центральный процессор (*****) вы- полняет процесс, по завершении отсчитываемого ядром кванта времени процесс приостанавливается и ядро активизирует выполнение другого процесса. Позд- нее ядро запускает приостановленный процесс. * Выделение выполняемому процессу оперативной памяти. Ядро операционной сис- темы дает процессам возможность совместно использовать участки адресного пространства на определенных условиях, защищая при этом адресное простран- ство, выделенное процессу, от вмешательства извне. Если системе требуетс свободная память, ядро освобождает память, временно выгружая процесс на внешние запоминающие устройства, которые называют устройствами выгрузки. Если ядро выгружает процессы на устройства выгрузки целиком, такая реализация систе- мы UNIX называется системой со свопингом (подкачкой); если же на устройст- во выгрузки выводятся страницы памяти, такая система называется системой с замещением страниц. * Выделение внешней памяти с целью обеспечения эффективного хранения инфор- мации и выборка данных пользователя. Именно в процессе реализации этой функции создается файловая система. Ядро выделяет внешнюю память под поль- зовательские файлы, мобилизует неиспользуемую память, структурирует файло- вую систему в форме, доступной для понимания, и защищает пользовательские файлы от несанкционированного доступа. * Управление доступом процессов к периферийным устройствам, таким как терми- налы, ленточные устройства, дисководы и сетевое оборудование. Выполнение ядром своих функций довольно очевидно. Например, оно узнает, что данный файл является обычным файлом или устройством, но скрывает это различие от пользовательских процессов. Так же оно, форматируя информацию файла для внутреннего хранения, защищает внутренний формат от пользователь- ских процессов, возвращая им неотформатированный поток байтов. Наконец, ядро реализует ряд необходимых функций по обеспечению выполнения процессов поль- зовательского уровня, за исключением функций, которые могут быть реализованы на самом пользовательском уровне. Например, ядро выполняет действия, необхо- димые shell'у как интерпретатору команд: оно позволяет процессору shell чи- тать вводимые с терминала данные, динамически порождать процессы, синхрони- зировать выполнение процессов, открывать каналы и переадресовывать ввод-вы- вод. Пользователи могут разрабатывать свои версии командного процессора shell с тем, чтобы привести рабочую среду в соответствие со своими требова- ниями, не затрагивая других пользователей. Такие программы пользуются теми же услугами ядра, что и стандартный процессор shell. 1.5 ПРЕДПОЛАГАЕМАЯ АППАРАТНАЯ СРЕДА Выполнение пользовательских процессов в системе UNIX осуществляется на двух уровнях: уровне пользователя и уровне ядра. Когда процесс производит обращение к операционной системе, режим выполнения процесса переключается с режима задачи (пользовательского) на режим ядра: операционная система пыта- ется обслужить запрос пользователя, возвращая код ошибки в случае неудачного завершения операции. Даже если пользователь не нуждается в каких-либо опре- деленных услугах операционной системы и не обращается к ней с запросами, -------------------------- (*****) В главе 12 рассматриваются многопроцессорные системы; до того речь будет идти об однопроцессорной модели. система еще выполняет учетные операции, связанные с пользовательским процес- сом, обрабатывает прерывания, планирует процессы, управляет распределением памяти и т.д. Большинство вычислительных систем разнообразной архитектуры (и соответствующие им операционные системы) поддерживают большее число уровней, чем указано здесь, однако уже двух режимов, режима задачи и режима ядра, вполне достаточно для системы 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 намеренно не включены многие функции, являющиеся частью других операци- онных систем, поскольку набор обращений к системе позволяет процессам выпол- нять все необходимые операции на пользовательском уровне. В следующей главе содержится более детальная информация о ядре, описывающая его архитектуру и вводящая некоторые основные понятия, которые используются при описании его функционирования. ГЛАВА 2 ВВЕДЕНИЕ В АРХИТЕКТУРУ ЯДРА ОПЕРАЦИОННОЙ СИСТЕМЫ В предыдущей главе был сделан только поверхностный обзор особенностей операционной среды UNIX. В этой главе основное внимание уделяется ядру опе- рационной системы, делается обзор его архитектуры и излагаются в общих чер- тах основные понятия и структуры, существенные для понимания всего последую- щего материала книги. 2.1 АРХИТЕКТУРА ОПЕРАЦИОННОЙ СИСТЕМЫ UNIX Как уже ранее было замечено (см. [Christian 83], стр.239), в системе UNIX создается иллюзия того, что файловая система имеет "места" и что у про- цессов есть "жизнь". Обе сущности, файлы и процессы, являются центральными понятиями модели операционной системы UNIX. На Рисунке 2.1 представлена блок-схема ядра системы, отражающая состав модулей, из которых состоит ядро, и их взаимосвязи друг с другом. В частности, на ней слева изображена файло- вая подсистема, а справа подсистема управления процессами, две главные ком- поненты ядра. Эта схема дает логическое представление о ядре, хотя в дейст- вительности в структуре ядра имеются отклонения от модели, поскольку отдель- ные модули испытывают внутреннее воздействие со стороны других модулей. Схема на Рисунке 2.1 имеет три уровня: уровень пользователя, уровень яд- ра и уровень аппаратуры. Обращения к операционной системе и библиотеки сос- тавляют границу между пользовательскими программами и ядром, проведенную на Рисунке 1.1. Обращения к операционной системе выглядят так же, как обычные вызовы функций в программах на языке Си, и библиотеки устанавливают соответ- ствие между этими вызовами функций и элементарными системными операция- ми, о чем более подробно см. в главе 6. При этом программы на ассемблере мо- гут обращаться к операционной системе непосредственно, без использовани библиотеки системных вызовов. Программы часто обращаются к другим библиоте- кам, таким как библиотека стандартных подпрограмм ввода-вывода, достигая тем самым более полного использования системных услуг. Для этого во время компи- ляции библиотеки связываются с программами и частично включаются в программу пользователя. Далее мы проиллюстрируем эти моменты на примере. На рисунке совокупность обращений к операционной системе разделена на те обращения, которые взаимодействуют с подсистемой управления файлами, и те, которые взаимодействуют с подсистемой управления процессами. Файловая под- система управляет файлами, размещает записи файлов, управляет свободным пространством, доступом к файлам и поиском данных для пользователей. Процес- сы взаимодействуют с подсистемой управления файлами, используя при этом со- вокупность специальных обращений к операционной системе, таких как open (для того, чтобы открыть файл на чтение или запись),close, read, write, stat (запросить атрибуты файла), chown (изменить запись с информацией о владельце файла) и chmod (изменить права доступа к файлу). Эти и другие операции расс- матриваются в главе 5. Подсистема управления файлами обращается к данным, которые хранятся в файле, используя буферный механизм, управляющий потоком данных между ядром и устройствами внешней памяти. Буферный механизм, взаимодействуя с драйверами устройств ввода-вывода блоками, инициирует передачу данных к ядру и обратно. Драйверы устройств являются такими модулями в составе ядра, которые управля- ют работой периферийных устройств. Устройства ввода-вывода блоками относятс программы пользовател ^ | +----------------------+ точка пере- | | библиотеки | сечения | +----------------------+ | ^ Уровень пользователя | | --------------------------|---------------------|----------------- Уровень ядра v v +---------------------------------------------------+ | ^ обращения к операционной системе ^ | +------+------------------------------------+-------+ | | +-----------------+---------------+ +----------------+---------+ | v | | v | | | | | | подсистема управле- | | ............| | ния файлами | | . взаимо- .| | <---+-+ | . действие .| | | | | . процессов.| | ^ ^ | | | подсистема ............| | | | | | | ............| +-------+--------------+----------+ | | . планиров-.| | v +-+> управления . щик .| | +--------------+ | ............| | | буфер сверх- | | ............| | | оперативной | | процессами . распреде-.| | | памяти (кеш) | | . ление .| | +--------------+ | . памяти .| | ^ | ^ ............| | | | | | | v +-------+------------------+ +-------+----------------------+ | | v . | | | символ . блок | | | . | | +------------------------------| | | | | | драйверы устройств | | | ^ | | +--------------+---------------+ | | | +--------------+------------------------------+------------------+ | v аппаратный контроль v | +----------------------------------------------------------------+ Уровень ядра ------------------------------------------------------------------ Уровень аппаратуры +----------------------------------------------------------------+ | технические средства (аппаратура) | +----------------------------------------------------------------+ Рисунок 2.1. Блок-схема ядра операционной системы к типу запоминающих устройств с произвольной выборкой; их драйверы построены таким образом, что все остальные компоненты системы воспринимают эти устрой- ства как запоминающие устройства с произвольной выборкой. Например, драйвер запоминающего устройства на магнитной ленте позволяет ядру системы восприни- мать это устройство как запоминающее устройство с произвольной выборкой. Подсистема управления файлами также непосредственно взаимодействует с драй- верами устройств "неструктурированного" ввода-вывода, без вмешательства бу- ферного механизма. К устройствам неструктурированного ввода-вывода, иногда именуемым устройствами посимвольного ввода-вывода (текстовыми), относятс устройства, отличные от устройств ввода-вывода блоками. Подсистема управления процессами отвечает за синхронизацию процессов, взаимодействие процессов, распределение памяти и планирование выполнени процессов. Подсистема управления файлами и подсистема управления процессами взаимодействуют между собой, когда файл загружается в память на выполнение (см. главу 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. Таблицы файлов, дескрипторов файла и индексов а также информация о правах доступа к открываемому процессу. Таблица деск- рипторов файла идентифицирует все открытые для процесса файлы. На Рисунке 2.2 показаны эти таблицы и связи между ними. В системных операциях open (от- крыть) и creat (создать) ядро возвращает дескриптор файла, которому соответ- ствует указатель в таблице дескрипторов файла. При выполнении операций read (читать) и write (писать) ядро использует дескриптор файла для входа в таб- лицу дескрипторов и, следуя указателям на таблицу файлов и на таблицу индек- сов, находит информацию в файле. Более подробно эти информационные структуры рассматриваются в главах 4 и 5. Сейчас достаточно сказать, что использование этих таблиц обеспечивает различную степень разделения доступа к файлу. Обычные файлы и каталоги хранятся в системе 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" ("блок, на- чинающийся с символа"). мые функции, ее локальные переменные, а также данные, необходимые для восс- тановления предыдущей записи активации, в том числе значения счетчика команд и указателя вершины стека в момент вызова функции. Текст программы включает Стек задачи Направление Стек ядра +--------------+ увеличения стека +------------------+ | Локальные | ^ | | | переменные | | | ^ | | (не показаны)| | | . | |--------------| | | . | |Адрес записи 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. Стеки задачи и ядра для программы копирования. последовательности команд, управляющие увеличением стека, а ядро системы вы- деляет, если нужно, место под стек. В программе на Рисунке 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 в програм- промежуточна таблица облас- таблица тей процессов областей +---------------------+ +-------------+ +------------+ | часть адресного про-| | | | | | странства задачи, | | | | | | выделенная процессу | | | | | +---------------------+ +-------------| +------------| ^ +--+-> ---+-----+-> | | | +-------------| +-----+------| +----------+----------+ +--+-> ---+--+ | | | | | | | +-------------| | | | | | | | | | | | +-----+------| +----------+----------| | | | +--+-> | | | v -----+--+ | | +-----+---+--| +---------------------| | | | | | | | | +-------------+ +-----+---+--+ | | | | +---------------------+ | | таблица процессов +-------------------------+---+--+ | оперативная память v v | +--------------------------------+ Рисунок 2.5. Информационные структуры для процессов ме copy. Подробно алгоритмы выполнения системной операции write будут описа- ны в последующих разделах. Каждому процессу соответствует точка входа в таблице процессов ядра, кроме того, каждому процессу выделяется часть оперативной памяти, отведенна под задачу пользователя. Таблица процессов включает в себя указатели на про- межуточную таблицу областей процессов, точки входа в которую служат в качес- тве указателей на собственно таблицу областей. Областью называется непрерыв- ная зона адресного пространства, выделяемая процессу для размещения текста, данных и стека. Точки входа в таблицу областей описывают атрибуты области, как напри- мер, хранятся ли в области текст программы или данные, закрытая ли эта об- ласть или же совместно используемая, и где конкретно в памяти размещаетс содержимое области. Внешний уровень косвенной адресации (через промежуточную таблицу областей, используемых процессами, к собственно таблице областей) позволяет независимым процессам совместно использовать области. Когда про- цесс запускает системную операцию 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 представлена диаграмма переходов для состояний, перечисленных выше. Как уже говорилось выше, в режиме разделения времени может выполнятьс одновременно несколько процессов, и все они могут одновременно работать в режиме ядра. Если им разрешить свободно выполняться в режиме ядра, то они могут испортить глобальные информационные структуры, принадлежащие ядру. Запрещая произвольное переключение контекстов и управляя возникновением со- бытий, ядро защищает свою целостность. Ядро разрешает переключение контекста только тогда, когда процесс пере- ходит из состояния "запуск в режиме ядра" в состояние "сна в памяти". Про- цессы, запущенные в режиме ядра, не могут быть выгружены другими процессами; поэтому иногда говорят, что ядро невыгружаемо, при этом процессы, находящие- ся в режиме задачи, могут выгружаться системой. Ядро поддерживает целост- ность своих информационных структур, поскольку оно невыгружаемо, таким обра- зом решая проблему "взаимного исключения" - обеспечения того, что критичес- кие секции программы выполняются в каждый момент времени в рамках самое большее одного процесса. В качестве примера рассмотрим программу (Рисунок 2.7) включения информа- ционной структуры, чей адрес содержится в указателе bp1, в список с исполь- зованием указателей после структуры, чей адрес содержится в bp. Если система разрешила переключение контекста при выполнении ядром фрагмента программы, возможно возникновение следующей ситуации. Предположим, ядро выполняет прог- рамму запуск +-------+ в режи- | | ме за- | 1 | дачи | | +-------+ обращение | ^ возврат к системе | | или пре- | | рывание | | v | запуск +-------+ в режи- | |<---------+ прерывание, ме яд- | 2 | | возврат из ра | |<---------+ прерывани +-------+ приоста-| ^ процесс пла- +-------+ нов | | нирования +-------+ ожидание | |<----------+ +------------| | готовность ("сон")| 4 +--------------------------->| 3 | к выполнению +-------+ пробуждение +-------+ переключение контекста допустимо Рисунок 2.6. Состояния процесса и переходы между ними до комментариев и затем осуществляет переключение контекста. Список с ис- пользованием сдвоенных указателей имеет противоречивый вид: структура bp1 только наполовину входит в этот список. Если процесс следует за передними указателями, он обнаружит bp1 в данном списке, но если он последует за фоно- выми указателями, то вообще не найдет структуру bp1 (Рисунок 2.8). Если дру- гие процессы будут обрабатывать указатели в списке до момента повторного за- пуска первого процесса, структура списка может постоянно разрушаться. Систе- ма UNIX предупреждает возникновение подобных ситуаций, запрещая переключение контекстов на время выполнения процесса в режиме ядра. Если процесс перехо- дит в состояние "сна", делая допустимым переключение контекста, алгоритмы ядра обеспечивают защиту целостности информационных структур системы. Проблемой, которая может привести к нарушению целостности информации яд- ра, является обработка прерываний, могущая вносить изменения в информацию о состоянии ядра. Например, если ядро выполняло программу, приведенную на Ри- сунке 2.7, и получило прерывание по достижении комментариев, программа обра- ботки прерыва- +-----------------------------------------------------------+ | 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. Список с указателями, некорректный из-за переключения контек- ста ний может разрушить ссылки, если будет манипулировать указателями, как было показано ранее. Чтобы решить эту проблему, система могла бы запретить все прерывания на время работы в режиме ядра, но при этом затянулась бы обработ- ка прерывания, что в конечном счете нанесло бы ущерб производительности сис- темы. Вместо этого ядро повышает приоритет прерывания процессора, запреща прерывания на время выполнения критических секций программы. Секция програм- мы является критической, если в процессе ее выполнения запуск программ обра- ботки произвольного прерывания может привести к возникновению проблем, имею- щих отношение к нарушению целостности. Например, если программа обработки прерывания от диска работает с буферными очередями, то часть прерываемой программы, при выполнении которой ядро обрабатывает буферные очереди, явля- ется критической секцией по отношению к программе обработки прерывания от диска. Критические секции невелики по размеру и встречаются нечасто, поэтому их существование не оказывает практически никакого воздействия на производи- тельность системы. В других операционных системах данный вопрос решается пу- тем запрещения любых прерываний при работе в системных режимах или путем разработки схем блокировки, обеспечивающих целостность. В главе 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. Что произошло бы в том случае, если ядро попыталось бы возобновить выпол- нение всех процессов, приостановленных по событию, но в системе не было бы к этому моменту ни одного такого процесса ? ГЛАВА 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. Более подробно эти случаи мы разберем с помощью упражнений. При выполнении алгоритма getblk имеет место случай 2, когда ядро прос- матривает хеш-очередь, в которой должен был бы находиться блок, но не нахо- дит его там. Так как блок не может быть ни в какой другой хеш-очереди, пос- кольку он не должен "хешироваться" в заголовки хеш-очередей +-----------------+ +-----------------+ | | |+----+ +----+| +----+ | блок 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. Второй случай выделения буфера другом месте, следовательно, его нет в буферном кеше. Поэтому ядро удаляет первый буфер из списка свободных буферов; этот буфер был уже выделен другому дисковому блоку и также находится в хеш-очереди. Если буфер не помечен дл отложенной переписи, ядро помечает буфер занятым, удаляет его из хеш-очере- ди, где он находится, назначает в заголовке буфера номера устройства и бло- ка, соответствующие данному дисковому блоку, и помещает буфер в хеш-очередь. Ядро использует буфер, не переписав информацию, которую буфер прежде хранил для другого дискового блока. Тот процесс, который будет искать прежний дис- ковый блок, не обнаружит его в пуле и получит для него точно таким же обра- зом новый буфер из списка свободных буферов. Когда ядро заканчивает работу с буфером, оно освобождает буфер вышеописанным способом. На Рисунке 3.7, нап- ример, ядро ищет блок 18, но не находит его в хеш-очереди, помеченной как "блок 2 модуль 4". Поэтому ядро удаляет первый буфер из списка свободных бу- феров (блок 3), назначает его блоку 18 и помещает его в соответствующую хеш-очередь. Если при выполнении алгоритма getblk имеет место случай 3, ядро так же должно выделить буфер из списка свободных буферов. Однако, оно обнаруживает, что удаляемый из списка буфер был помечен для отложенной переписи, поэтому прежде чем использовать буфер ядро должно переписать его содержимое на диск. Ядро приступает к асинхронной записи на диск и пытается выделить другой бу- фер из списка. Когда асинхронная запись заканчивается, ядро освобождает бу- фер и помещает его в начало списка свободных буферов. Буфер сам продвинулс от конца списка свободных буферов к началу списка. Если после асинхронной переписи ядру бы понадобилось поместить буфер в конец списка, буфер получил бы "зеленую улицу" по всему списку свободных буферов, результат такого пере- мещения противоположен действию алгоритма поиска буферов, к которым наиболее долго не было обращений. Например, если обратиться к Рисунку 3.8, ядро не смогло обнаружить блок 18, но когда попыталось выделить первые два буфера (по очереди) в списке свободных буферов, то оказалось, что они оба помечены для отложенной переписи. Ядро удалило их из списка, запустило операции пере- писи на диск в соответствующие блоки, и выделило третий буфер из списка, блок 4. Далее ядро присвоило новые значения полям буфера "номер устройства" и "номер блока" и включило буфер, получивший имя "блок 18", в новую хеш-оче- редь. В четвертом случае (Рисунок 3.9) ядро, работая с процессом A, не смогло найти дисковый блок в соответствующей хеш-очереди и предприняло попытку вы- делить из списка свободных буферов новый буфер, как в случае 2. Однако, в списке не оказалось ни одного буфера, поэтому процесс A приостановился до тех пор, пока другим процессом не будет выполнен алгоритм brelse, высвобож- дающий буфер. Планируя выполнение процесса A, ядро вынуждено снова просмат- ривать хеш-очередь в поисках блока. Оно не в состоянии немедленно выделить буфер из списка свободных буферов, так как возможна ситуация, когда свобод- ный буфер ожидают сразу несколько процессов и одному из них будет выделен вновь освободившийся буфер, на который уже нацелился процесс A. Таким обра- зом, алгоритм поиска блока снова гарантирует, что только один буфер включает содержимое дискового блока. На Рисунке 3.10 показана конкуренция между двум процессами за освободившийся буфер. Последний случай (Рисунок 3.11) наиболее сложный, поскольку он связан с комплексом взаимоотношений между несколькими процессами. Предположим, что ядро, работая с процессом A, ведет поиск дискового блока и выделяет буфер, но приостанавливает выполнение процесса перед освобождением буфера. Напри- мер, если процесс A по- пытается считать дисковый блок и выделить буфер, как в случае 2, то он при- остановится до момента завершения передачи данных с диска. Предположим, что пока процесс A приостановлен, ядро активизирует второй процесс, B, который пытается обратиться к дисковому блоку, чей буфер был только что заблокирован процессом A. Процесс B (случай 5) обнаружит этот захваченный блок в хеш-оче- реди. Так как использовать захваченный буфер не разрешается и, кроме того, нельзя выделить для одного и того же дискового блока второй буфер, процесс B помечает буфер как "запрошенный" и затем приостанавливается до того момента, когда процесс A освободит данный буфер. В конце концов процесс A освобождает буфер и замечает, что он запрошен. Тогда процесс A "будит" все процессы, приостановленные по событию "буфер становится свободным", включая и процесс B. Когда же ядро вновь запустит на выполнение процесс B, процесс B должен будет убедиться в том, что буфер сво- заголовки хеш-очередей +-----------------+ +-----------------+ | | |+----+ +----+| +----+ | блок 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. Четвертый случай выделения буфера боден. Возможно, что третий процесс, 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 показано содержимое занятого буфера. Алгоритм выделения буфера должен быть надежным; процессы не должны "за- сыпать" навсегда и рано или поздно им нужно выделить буфер. Ядро гарантирует такое положение, при котором все процессы, ожидающие выделения буфера, про- должат свое выполнение, благодаря тому, что ядро распределяет буферы во вре- мя обработки обращений к операционной системе и освобождает их перед возвра- том управления процессам (***). В режиме задачи процессы непосредс- ---------------------------------------- (***) Исключением является системная операция mount, которая зах- ватывает буфер до тех пор, пока не будет исполнена операци umount. Это исключение не является существенным, поскольку общее количество буферов намного превышает число активных монтированных файловых систем. заголовки хеш-очередей +-----------------+ +-----------------+ | | |+----+ +----+| +----+ | блок 0 модуль 4 | +| 28 ++ +| 4 ++ | 64 | | | +----+| |+----+ +----+ +-----------------| | +------+ | | +----+| +----+| +----+ | блок 1 модуль 4 | | 17 || +| 5 ++ +| 97 ++ | | +----+| |+----+ +-++----+| +-----------------| +---|--------+ +------+ | | +----+ |+----+ |+----+ | блок 2 модуль 4 | | 98 |+---+| 50 | +| 10 ++ | | +----+| +----+ +----+| +-----------------| | | | | +----+| +----+ +----+| | блок 3 модуль 4 | +>| 3 ++ | 35 | | 99 || | | | +----+ +----+ +----+| +-----------------+ | занят| +-----------------+ | | |заголовок списка +----+ | | | | |свободных буферов|<---------------------------------+ +-----------------+ Поиск блока 99, блок занят Рисунок 3.11. Пятый случай выделения буфера твенно не контролируют выделение буферов ядром системы, поэтому они не могут намеренно "захватывать" буферы. Ядро теряет контроль над буфером только тог- да, когда ждет завершения операции ввода-вывода между буфером и диском. Было задумано так, что если дисковод испорчен, он не может прерывать работу цент- рального процессора, и тогда ядро никогда не освободит буфер. Дисковод дол- жен следить за работой аппаратных средств в таких случаях и возвращать ядру код ошибки, сообщая о плохой работе диска. Короче говоря, ядро может гаран- тировать, что процессы, приостановленные в ожидании буфера, в конце концов возобновят свое выполнение. Можно также представить себе ситуацию, когда процесс "зависает" в ожида- нии получения доступа к буферу. В четвертом случае, например, если несколько процессов приостанавливаются, ожидая освобождения буфера, ядро не гарантиру- ет, что они получат доступ к буферу в той очередности, в которой они запро- сили доступ. Процесс может приостановить и возобновить свое выполнение, ког- да буфер станет свободным, только для того, чтобы приостановиться вновь из -за того, что другой процесс получил управление над буфером первым. Теорети- чески, так может продолжаться вечно, но практически такой проблемы не возни- кает в связи с тем, что в системе обычно заложено большое количество буфе- ров. 3.4 ЧТЕНИЕ И ЗАПИСЬ ДИСКОВЫХ БЛОКОВ Теперь, когда алгоритм выделения буферов нами уже рассмотрен, будет лег- че понять процедуру чтения и записи дисковых блоков. Чтобы считать дисковый Процесс A Процесс B Процесс C +------------------------------------------------------------- | Буфер выделен блоку b | | Буфер заблокирован | | Начат ввод-вывод | | Приостановлен до | завершения ввода-вывода | | Поиск блока b | в хеш-очереди | | Буфер заблокирован, | приостановка | | Приостановлен | в ожидании освобождени | любого буфера | (случай 4) | +---------------------------+ | | Ввод-вывод закончен, | | | выполнение возобновляется | | +---------------------------+ | brelse(): возобновляются | другие процессы | Получает буфер, | первоначально | назначенный | блоку b | | Переназначение | буфера блоку b' | Буфер не содержит | блок b | | Поиск начинается | снова | Врем v Рисунок 3.12. Состязание за свободный буфер блок (Рисунок 3.13), процесс использует алгоритм getblk для поиска блока в буферном кеше. Если он там, ядро может возвратить его немедленно без физи- ческого считывания блока с диска. Если блок в кеше отсутствует, ядро прика- зывает дисководу "запланировать" запрос на чтение и приостанавливает работу, ожидая завершения ввода-вывода. Дисковод извещает контроллер диска о том, что он собирается считать информацию, и контроллер тогда передает информацию в буфер. Наконец, дисковый контроллер прерывает работу процессора, сообщая о завершении операции во- да-вывода, и программа обработки прерываний от диска возобновляет выполнение приостановленного процесса; теперь содержимое дискового блока находится в буфере. Модули, запросившие информацию данного блока, получают ее; когда бу- фер им уже не потребуется, они освободят его для того, чтобы другие процессы +------------------------------------------------------------+ | алгоритм bread /* чтение блока */ | | входная информация: номер блока в файловой системе | | выходная информация: буфер, содержащий данные | | { | | получить буфер для блока (алгоритм getblk); | | если (данные в буфере правильные) | | возвратить буфер; | | приступить к чтению с диска; | | приостановиться (до завершения операции чтения); | | возвратить (буфер); | | } | +------------------------------------------------------------+ Рисунок 3.13. Алгоритм чтения дискового блока получили к нему доступ. В главе 5 будет показано, как модули более высокого уровня (такие как подсистема управления файлами) могут предчувствовать потребность во втором дисковом блоке, когда процесс читает информацию из файла последовательно. Эти модули формируют запрос на асинхронное выполнение второй операции вво- да-вывода, надеясь на то, что информация уже будет в памяти, когда вдруг возникнет необходимость в ней, и тем самым повышая быстродействие системы. Для этого ядро выполняет алгоритм чтения блока с продвижением breada (Рису- нок 3.14). Ядро проверяет, находится ли в кеше первый блок, и если его там нет, приказывает дисководу считать этот блок. Если в буферном кеше отсутст- вует и второй блок, ядро дает команду дисководу считать асинхронно и его. Затем процесс приостанавливается, ожидая завершения операции ввода-вывода над первым блоком. Когда выполнение процесса возобновляется, он возвращает буфер первому блоку и не обращает внимание на то, когда завершится операци ввода-вывода для второго блока. После завершения этой операции контроллер диска прерывает работу системы; программа обработки прерываний узнает о том, что ввод-вывод выполнялся асинхронно, и освобождает буфер (алгоритм brelse). Если бы она не освободила буфер, буфер остался бы заблокированным и по этой причине недоступным для всех процессов. Невозможно заранее разблокировать буфер, так как операция ввода-вывода, связанная с буфером, активна и, следо- вательно, содержимое буфера еще не адекватно. Позже, если процесс пожелает считать второй блок, он обнаружит его в буферном кеше, поскольку к тому вре- мени операция ввода-вывода закончится. Если же, в начале выполнения алгорит- ма breada, первый блок обнаружился в буферном кеше, ядро тут же проверяет, находится там же и второй блок, и продолжает работу по только что описанной схеме. Алгоритм записи содержимого буфера в дисковый блок (Рисунок 3.15) похож на алгоритм чтения. Ядро информирует дисковод о том, что есть буфер, содер- жимое которого должно быть выведено, и дисковод планирует операцию ввода-вы- вода блока. Если запись производится синхронно, вызывающий процесс приоста- навливается, ожидая ее завершения и освобождая буфер в момент возобновления своего выполнения. Если запись производится асинхронно, ядро запускает операцию записи на диск, но не ждет ее завершения. Ядро освободит буфер, когда завершится ввод-вывод. Могут возникнуть ситуации, и это будет показано в следующих двух главах, когда ядро не записывает данные немедленно на диск. Если запись "откладыва- ется", ядро соответствующим образом помечает буфер, освобождая его по алго- ритму brelse, и продолжает работу без планирования ввода-вывода. Ядро запи- сывает блок на диск перед тем, как другой процесс сможет переназначить буфер другому блоку, как показано в алгоритме getblk (случай 3). Между тем, ядро надеется на то, что процесс получает доступ до того, как буфер будет перепи- +------------------------------------------------------------+ | алгоритм breada /* чтение блока с продвижением */ | | входная информация: (1) в файловой системе номер блока для | | немедленного считывания | | (2) в файловой системе номер блока для | | асинхронного считывания | | выходная информация: буфер с данными, считанными немедленно| | { | | если (первый блок отсутствует в кеше) | | { | | получить буфер для первого блока (алгоритм getblk);| | если (данные в буфере неверные) | | приступить к чтению с диска; | | } | | если (второй блок отсутствует в кеше) | | { | | получить буфер для второго блока (алгоритм getblk);| | если (данные в буфере верные) | | освободить буфер (алгоритм brelse); | | в противном случае | | приступить к чтению с диска; | | } | | если (первый блок первоначально находился в кеше) | | { | | считать первый блок (алгоритм bread); | | возвратить буфер; | | } | | приостановиться (до того момента, когда первый буфер | | будет содержать верные данные); | | возвратить буфер; | | } | +------------------------------------------------------------+ Рисунок 3.14. Алгоритм чтения блока с продвижением сан на диск; если этот процесс впоследствии изменит содержимое буфера, ядро произведет дополнительную операцию по сохранению изменений на диске. Отложенная запись отличается от асинхронной записи. Выполняя асинхронную +------------------------------------------------------------+ | алгоритм bwrite /* запись блока */ | | входная информация: буфер | | выходная информация: отсутствует | | { | | приступить к записи на диск; | | если (ввод-вывод синхронный) | | { | | приостановиться (до завершения ввода-вывода); | | освободить буфер (алгоритм brelse); | | } | | в противном случае если (буфер помечен для отложенной | | записи) | | пометить буфер для последующего размещения в | | "голове" списка свободных буферов; | | } | +------------------------------------------------------------+ Рисунок 3.15. Алгоритм записи дискового блока запись, ядро запускает дисковую операцию немедленно, но не дожидается ее за- вершения. Что касается отложенной записи, ядро отдаляет момент физической переписи на диск насколько возможно; затем по алгоритму getblk (случай 3) оно помечает буфер как "старый" и записывает блок на диск асинхронно. После этого контроллер диска прерывает работу системы и освобождает буфер, используя алгоритм brelse; буфер помещается в "голову" списка свободных буферов, поскольку он имеет пометку "старый". Благодаря наличию двух выполняющихся асинхронно опе- раций ввода-вывода - чтения блока с продвижением и отложенной записи - ядро может запускать программу brelse из программы обработки прерываний. Следова- тельно, ядро вынуждено препятствовать возникновению прерываний при выполне- нии любой процедуры, работающей со списком свободных буферов, поскольку brelse помещает буферы в этот список. 3.5 ПРЕИМУЩЕСТВА И НЕУДОБСТВА БУФЕРНОГО КЕША Использование буферного кеша имеет, с одной стороны, несколько преиму- ществ и, с другой стороны, некоторые неудобства. * Использование буферов позволяет внести единообразие в процедуру обраще- ния к диску, поскольку ядру нет необходимости знать причину ввода-выво- да. Вместо этого, ядро копирует данные в буфер и из буфера, невзирая на то, являются ли данные частью файла, индекса или суперблока. Буферизаци ввода-вывода с диска повышает модульность разработки программ, поскольку те составные части ядра, которые занимаются вводом-выводом на диск, име- ют один интерфейс на все случаи. Короче говоря, упрощается проектирова- ние системы. * Система не накладывает никаких ограничений на выравнивание информации пользовательскими процессами, выполняющими ввод-вывод, поскольку ядро производит внутреннее выравнивание информации. В различных аппаратных реализациях часто требуется выравнивать информацию для ввода-вывода с диска определенным образом, т.е. производить к примеру двухбайтное или четырехбайтное выравнивание данных в памяти. Без механизма буферизации программистам пришлось бы заботиться самим о правильном выравнивании данных. По этой причине на машинах с ограниченными возможностями в вы- равнивании адресов возникает большое количество ошибок программировани и, кроме того, становится проблемой перенос программ в операционную сре- ду UNIX. Копируя информацию из пользовательских буферов в системные бу- феры (и обратно), ядро системы устраняет необходимость в специальном вы- равнивании пользовательских буферов, делая пользовательские программы более простыми и мобильными. * Благодаря использованию буферного кеша, сокращается объем дискового тра- фика и время реакции и повышается общая производительность системы. Про- цессы, считывающие данные из файловой системы, могут обнаружить информа- ционные блоки в кеше и им не придется прибегать ко вводу-выводу с диска. Ядро часто применяет "отложенную запись", чтобы избежать лишних обраще- ний к диску, оставляя блок в буферном кеше и надеясь на попадание блока в кеш. Очевидно, что шансы на такое попадание выше в системах с большим количеством буферов. Тем не менее, число буферов, которые можно заложить в системе, ограничивается объемом памяти, доступной выполняющимся про- цессам: если под буферы задействовать слишком много памяти, то система будет работать медленнее в связи с тем, что ей придется заниматься под- качкой и замещением выполняющихся процессов. * Алгоритмы буферизации помогают поддерживать целостность файловой систе- мы, так как они сохраняют общий, первоначальный и единственный образ дисковых блоков, содержащихся в кеше. Если два процесса одновременно по- пытаются обратиться к одному и тому же дисковому блоку, алгоритмы буфе- ризации (например, getblk) параллельный доступ преобразуют в последова- тельный, предотвращая разрушение данных. * Сокращение дискового трафика является важным преимуществом с точки зре- ния обеспечения хорошей производительности или быстрой реакции системы, однако стратегия кеширования также имеет некоторые неудобства. Так как ядро в случае отложенной записи не переписывает данные на диск немедлен- но, такая система уязвима для сбоев, которые оставляют дисковые данные в некорректном виде. Хотя в последних версиях системы и сокращен ущерб, наносимый катастрофическими сбоями, основная проблема остается: пользо- ватель, запрашивающий выполнение операции записи, никогда не знает, в какой момент данные завершат свой путь на диск (****). * Использование буферного кеша требует дополнительного копирования инфор- мации при ее считывании и записи пользовательскими процессами. Процесс, записывающий данные, передает их ядру и ядро копирует данные на диск; процесс, считывающий данные, получает их от ядра, которое читает данные с диска. При передаче большого количества данных дополнительное копиро- вание отрицательным образом отражается на производительности системы, однако при передаче небольших объемов данных производительность повыша- ется, поскольку ядро буферизует данные (используя алгоритм getblk и от- ложенную запись) до тех пор, пока это представляется эффективным с точки зрения экономии времени работы с диском. 3.6 ВЫВОДЫ В данной главе была рассмотрена структура буферного кеша и различные способы, которыми ядро размещает блоки в кеше. В алгоритмах буферизации со- четаются несколько простых идей, которые в сумме обеспечивают работу меха- низма кеширования. При работе с блоками в буферном кеше ядро использует ал- горитм замены буферов, к которым наиболее долго не было обращений, предпола- гая, что к блокам, к которым недавно было обращение, вероятно, вскоре обратятся снова. Очередность, в которой буферы появляются в списке свободных буферов, соот- ветствует очередности их предыдущего использования. Остальные алгоритмы обс- луживания буферов, типа "первым пришел - первым вышел" и замещения редко ис- пользуемых, либо являются более сложными в реализации, либо снижают процент попадания в кеш. Использование функции хеширования и хеш-очередей дает ядру возможность ускорить поиск заданных блоков, а использование двунаправленных указателей в списках облегчает исключение буферов. Ядро идентифицирует нужный ему блок по номеру логического устройства и номеру блока. Алгоритм getblk просматривает буферный кеш в поисках блока и, если буфер присутствует и свободен, блокирует буфер и возвращает его. Если буфер заблокирован, обратившийся к нему процесс приостанавливается до тех пор, пока буфер не освободится. Механизм блокирования гарантирует, что толь- ко один процесс в каждый момент времени работает с буфером. Если в кеше блок отсутствует, ядро назначает блоку свободный буфер, блокирует и возвращает его. Алгоритм bread выделяет блоку буфер и при необходимости читает туда ин- формацию. Алгоритм bwrite копирует информацию в предварительно выделенный буфер. Если при выполнении указанных алгоритмов ядро не увидит необходимости в немедленном копировании данных на диск, оно пометит буфер для "отложенной записи", чтобы избежать излишнего ввода-вывода. К сожалению, процедура отк- --------------------------------------- (****) Стандартный набор операций ввода-вывода в программах на языке Си включает операцию fflush. Эта функция занимается подкачиванием данных из буферов в пользовательском адресном пространстве в рабочую область ядра. Тем не менее пользователю не известно, когда ядро запишет дан- ные на диск. ладывания записи сопровождается тем, что процесс никогда не уверен, в какой момент данные физически попадают на диск. Если ядро записывает данные на диск синхронно, оно поручает драйверу диска передать блок файловой системе и ждет прерывания, сообщающего об окончании ввода-вывода. Существует множество способов использования ядром буферного кеша. Пос- редством буферного кеша ядро обеспечивает обмен данными между прикладными программами и файловой системой, передачу дополнительной системной информа- ции, например, индексов, между алгоритмами ядра и файловой системой. Ядро также использует буферный кеш, когда читает программы в память для выполне- ния. В следующих главах будет рассмотрено множество алгоритмов, использующих процедуры, описанные в данной главе. Другие алгоритмы, которые кешируют ин- дексы и страницы памяти, также используют приемы, похожие на те, что описаны для буферного кеша. 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. Определим время реакции системы как среднее время выполнения системного вызова. Определим пропускную способность системы как количество процес- сов, которые система может выполнять в данный период времени. Объясните, как буферный кеш может способствовать повышению реакции системы. Способ- ствует ли он с неизбежностью увеличению пропускной способности системы ? ГЛАВА 4 ВНУТРЕННЕЕ ПРЕДСТАВЛЕНИЕ ФАЙЛОВ Как уже было замечено в главе 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, и это значит, что ядро может переназначить свободный индекс в памяти другому дисковому индексу. Таким об- разом, список свободных индексов выступает в роли кеша для неактивных индек- сов. Если процесс пытается обратиться к файлу, чей индекс в этот момент от- сутствует в индексном пуле, ядро переназначает свободный индекс из списка для использования этим процессом. С другой стороны, у буфера нет счетчика ссылок; он находится в списке свободных буферов тогда и только тогда, когда он разблокирован. 4.1.2 Обращение к индексам Ядро идентифицирует индексы по имени файловой системы и номеру индекса и выделяет индексы в памяти по запросам соответствующих алгоритмов. Алгоритм iget назначает индексу место для копии в памяти (Рисунок 4.3); он почти идентичен алгоритму getblk для поиска дискового блока в буферном кеше. Ядро преобразует номера устройства и индекса в имя хеш-очереди и просматривает эту хеш-очередь в поисках индекса. Если индекс не обнаружен, ядро выделяет +------------------------------------------------------------+ | алгоритм iget | | входная информация: номер индекса в файловой системе | | выходная информация: заблокированный индекс | | { | | выполнить | | { | | если (индекс в индексном кеше) | | { | | если (индекс заблокирован) | | { | | приостановиться (до освобождения индекса); | | продолжить; /* цикл с условием продолжения */ | | } | | /* специальная обработка для точек монтирования | | (глава 5) */ | | если (индекс в списке свободных индексов) | | убрать из списка свободных индексов; | | увеличить счетчик ссылок для индекса; | | возвратить (индекс); | | } | | /* индекс отсутствует в индексном кеше */ | | если (список свободных индексов пуст) | | возвратить (ошибку); | | убрать новый индекс из списка свободных индексов; | | сбросить номер индекса и файловой системы; | | убрать индекс из старой хеш-очереди, поместить в новую;| | считать индекс с диска (алгоритм bread); | | инициализировать индекс (например, установив счетчик | | ссылок в 1); | | возвратить (индекс); | | } | | } | +------------------------------------------------------------+ Рисунок 4.3. Алгоритм выделения индексов в памяти его из списка свободных индексов и блокирует его. Затем ядро готовится к чтению с диска в память индекса, к которому оно обращается. Ядро уже знает номера индекса и логического устройства и вычисляет номер логического блока на диске, содержащего индекс, с учетом того, сколько дисковых индексов поме- щается в одном дисковом блоке. Вычисления производятся по формуле номер блока = ((номер индекса - 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). Процессы обращаются к информации в файле, задавая смещение в байтах. Они рассматривают файл как поток байтов и ведут подсчет байтов, начиная с нуле- вого адреса и заканчивая адресом, равным размеру файла. Ядро переходит от байтов к блокам: файл начинается с нулевого логического блока и заканчивает- ся блоком, номер которого определяется исходя из размера файла. Ядро обраща- ется к индексу и превращает логический блок, принадлежащий файлу, в соответ- Информацион- Индекс ные блоки +-------------+ +-----+ | прямой адр. +----------------------------------->| | | 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. Преобразование адреса смещения в номер блока в файловой системе ствующий дисковый блок. На Рисунке 4.8 представлен алгоритм bmap пересчета смещения в байтах от начала файла в номер физического блока на диске. Рассмотрим формат файла в блоках (Рисунок 4.9) и предположим, что диско- вый блок занимает 1024 байта. Если процессу нужно обратиться к байту, имею- щему смещение от начала файла, равное 9000, в результате вычислений ядро приходит к выводу, что этот байт располагается в блоке прямой адресации с номером 8 (начиная с 0). Затем ядро обращается к блоку с номером 367; 808-й байт в этом блоке (если вести отсчет с 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 использует при анализе составного имени пути поиска про- межуточные индексы; назовем их рабочими индексами. Индекс каталога, откуда поиск берет начало, является первым рабочим индексом. На каждой итерации цикла алгоритма ядро проверяет совпадение рабочего индекса с индексом ката- лога. В противном случае, нарушилось бы утверждение, что только файлы, не являющиеся каталогами, могут быть листьями дерева файловой системы. Процесс также должен иметь право производить поиск в каталоге (разрешения на чтение недостаточно). Код идентификации пользователя для процесса должен соответст- вовать коду индивидуального или группового вла- дельца файла и должно быть предоставлено право исполнения, либо поиск нужно разрешить всем пользователям. В противном случае, поиск не получится. Ядро выполняет линейный поиск файла в каталоге, ассоциированном с рабо- чим индексом, пытаясь найти для компоненты имени пути поиска подходящую за- пись в каталоге. Исходя из адреса смещения в байтах внутри каталога (начина с 0), оно определяет местоположение дискового блока в соответствии с алго- ритмом bmap и считывает этот блок, используя алгоритм bread. По имени компо- --------------------------------------- (**) Чтобы изменить для себя корневой каталог файловой системы, процесс мо- жет запустить системную операцию chroot. Новое значение корня сохраня- ется в рабочей области процесса. +------------------------------------------------------------+ | алгоритм namei /* превращение имени пути поиска в индекс */| | входная информация: имя пути поиска | | выходная информация: заблокированный индекс | | { | | если (путь поиска берет начало с корня) | | рабочий индекс = индексу корня (алгоритм iget); | | в противном случае | | рабочий индекс = индексу текущего каталога | | (алгоритм iget); | | | | выполнить (пока путь поиска не кончился) | | { | | считать следующую компоненту имени пути поиска; | | проверить соответствие рабочего индекса каталогу | | и права доступа; | | если (рабочий индекс соответствует корню и компо- | | нента имени "..") | | продолжить; /* цикл с условием продолжения */| | считать каталог (рабочий индекс), повторяя алго- | | ритмы bmap, bread и brelse; | | если (компонента соответствует записи в каталоге | | (рабочем индексе)) | | { | | получить номер индекса для совпавшей компонен-| | ты; | | освободить рабочий индекс (алгоритм iput); | | рабочий индекс = индексу совпавшей компоненты | | (алгоритм iget); | | } | | в противном случае /* компонента отсутствует в | | каталоге */ | | возвратить (нет индекса); | | } | | возвратить (рабочий индекс); | | } | +------------------------------------------------------------+ Рисунок 4.11. Алгоритм превращения имени пути поиска в индекс ненты ядро производит в блоке поиск, представляя содержимое блока как после- довательность записей каталога. При обнаружении совпадения ядро переписывает номер индекса из данной точки входа, освобождает блок (алгоритм 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 назначения новых индексов. По причинам, о которых пойдет речь ниже, ядро сначала проверяет, не заблокиро- вал ли какой-либо процесс своим обращением список свободных индексов в су- +------------------------------------------------------------+ | алгоритм ialloc /* выделение индекса */ | | входная информация: файловая система | | выходная информация: заблокированный индекс | | { | | выполнить | | { | | если (суперблок заблокирован) | | { | | приостановиться (пока суперблок не освободится); | | продолжить; /* цикл с условием продолжения */ | | } | | если (список индексов в суперблоке пуст) | | { | | заблокировать суперблок; | | выбрать запомненный индекс для поиска свободных | | индексов; | | искать на диске свободные индексы до тех пор, пока| | суперблок не заполнится, или пока не будут най- | | дены все свободные индексы (алгоритмы bread и | | brelse); | | снять блокировку с суперблока; | | возобновить выполнение процесса (как только супер-| | блок освободится); | | если (на диске отсутствуют свободные индексы) | | возвратить (нет индексов); | | запомнить индекс с наибольшим номером среди най- | | денных для последующих поисков свободных индек- | | сов; | | } | | /* список индексов в суперблоке не пуст */ | | выбрать номер индекса из списка индексов в супербло- | | ке; | | получить индекс (алгоритм iget); | | если (индекс после всего этого не свободен) /* !!! */| | { | | переписать индекс на диск; | | освободить индекс (алгоритм iput); | | продолжить; /* цикл с условием продолжения */ | | } | | /* индекс свободен */ | | инициализировать индекс; | | переписать индекс на диск; | | уменьшить счетчик свободных индексов в файловой сис- | | теме; | | возвратить (индекс); | | } | | } | +------------------------------------------------------------+ Рисунок 4.12. Алгоритм назначения новых индексов перблоке. Если список номеров индексов в суперблоке не пуст, ядро назначает номер следующего индекса, выделяет для вновь назначенного дискового индекса свободный индекс в памяти, используя алгоритм iget (читая индекс с диска, если необходимо), копирует дисковый индекс в память, инициализирует поля в индексе и возвращает индекс заблокированным. Затем ядро корректирует диско- вый индекс, указывая, что к индексу произошло обращение. Ненулевое значение поля типа файла говорит о том, что дисковый индекс назначен. В простейшем случае с индексом все в порядке, но в условиях конкуренции делается необхо- димым проведение дополнительных проверок, на чем мы еще кратко остановимся. Грубо говоря, конкуренция возникает, когда несколько процессов вносят изме- нения в общие информационные структуры, так что результат зависит от очеред- ности выполнения процессов, пусть даже все процессы будут подчиняться прото- колу блокировки. Здесь предполагается, например, что процесс мог бы получить уже используемый индекс. Конкуренция связана с проблемой взаимного исключе- ния, описанной в главе 2, с одним замечанием: различные схемы блокировки ре- шают проблему взаимного исключения, но не могут сами по себе решить все проблемы конкуренции. Если список свободных индексов в суперблоке пуст, ядро просматривает диск и помещает в суперблок как можно больше номеров свободных индексов. При этом ядро блок за блоком считывает индексы с диска и наполняет список номе- ров индексов в суперблоке до отказа, запоминая индекс с номером, наибольшим среди найденных. Назовем этот индекс "запомненным"; это последний индекс, записанный в суперблок. В следующий раз, когда ядро будет искать на диске свободные индексы, оно использует запомненный индекс в качестве стартовой точки, благодаря чему гарантируется, что ядру не придется зря тратить врем на считывание дисковых блоков, в кото- рых свободные индексы наверняка отсутствуют. После формирования нового набо- ра номеров свободных индексов ядро запускает алгоритм назначения индекса с самого начала. Всякий раз, когда ядро назначает дисковый индекс, оно умень- шает значение счетчика свободных индексов, записанное в суперблоке. Рассмотрим две пары массивов номеров свободных индексов (Рисунок 4.13). Если список свободных индексов в суперблоке имеет вид первого массива на Ри- сунке 4.13(а) при назначении индекса ядром, то значение указателя на следую- щий номер индекса уменьшается до 18 и выбирается индекс с номером 48. Если же список выглядит как первый массив на Рисунке 4.13(б), ядро заметит, что массив пуст и обратится в поисках свободных индексов к диску, при этом поиск будет производиться, начиная с индекса с номером 470, который был ранее за- помнен. Когда ядро заполнит список свободных индексов в суперблоке до отка- Список свободных индексов в суперблоке +-------------------------------------------------------+ | свободные индексы | | | пустота | || 83 | 48 || +-------------------------------------------------------+ 18 19 20 массив 1 ^ | указатель Список свободных индексов в суперблоке +-------------------------------------------------------+ | свободные индексы | | | пустота | || 83 | | +-------------------------------------------------------+ 18 19 20 массив 1 ^ | указатель (а) Назначение свободного индекса из середины списка Список свободных индексов в суперблоке +-------------------------------------------------------+ | 470 | пустота | || +-------------------------------------------------------+ 0 o массив 1 ^ o |указатель o(запомненный индекс) o o Список свободных индексов в суперблоке +-------------------------------------------------------+ | 535 | свободные индексы | 476 | 475 | 471 | || +-------------------------------------------------------+ 0 48 49 50 ^ указатель | (б) Назначение свободного индекса, когда список в супер- блоке пуст Рисунок 4.13. Два массива номеров свободных индексов за, оно запомнит последний индекс в качестве начальной точки для последующих просмотров диска. Ядро производит назначение файлу только что выбранного с диска индекса (под номером 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), оно выделяет следующий из блоков, имеющихся в списке в суперб- локе. Выделенный однажды, блок не может быть переназначен до тех пор, пока не освободится. Если выделенный блок является последним блоком, имеющимся в кеше суперблока, ядро трактует его как указатель на блок, в котором хранитс список свободных блоков. Ядро читает блок, заполняет массив в суперблоке но- вым списком номеров блоков и после этого продолжает работу с первоначальным номером блока. Оно выделяет буфер для блока и очищает содержимое буфера (об- нуляет его). Дисковый блок теперь считается назначенным и у ядра есть буфер для работы с ним. Если в файловой системе нет свободных блоков, вызывающий процесс получает сообщение об ошибке. Если процесс записывает в файл большой объем информации, он неоднократно запрашивает у системы блоки для хранения информации, но ядро назначает каж- список в суперблоке +---------------------------------------------+ | 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. Список номеров свободных дисковых блоков с указателями дый раз только по одному блоку. Программа mkfs пытается организовать перво- начальный связанный список номеров свободных блоков так, чтобы номера бло- ков, передаваемых файлу, были рядом друг с другом. Благодаря этому повышает- ся производительность, поскольку сокращается время поиска на диске и врем ожидания при последовательном чтении файла процессом. На Рисунке 4.18 номера блоков даны в настоящем формате, определяемом скоростью вращения диска. К сожалению, очередность номеров блоков в списке свободных блоков перепутана в связи с частыми обращениями к списку со стороны процессов, ведущих запись в файлы и удаляющих их, в результате чего номера блоков поступают в список и покидают его в случайном порядке. Ядро не предпринимает попыток сортиро- вать номера блоков в списке. Алгоритм освобождения блока free - обратный алгоритму выделения блока. Если список в суперблоке не полон, номер вновь освобожденного блока включа- ется в этот список. Если, однако, список полон, вновь освобожденный блок становится связным блоком; ядро переписывает в него список из суперблока и копирует блок на диск. Затем номер вновь освобожденного блока включается в список свободных блоков в суперблоке. Этот номер становится единственным но- мером в списке. На Рисунке 4.20 показана последовательность операций alloc и free дл случая, когда в исходный момент список свободных блоков содержал один эле- мент. Ядро освобождает блок 949 и включает номер блока в список. Затем оно выделяет этот блок и удаляет его номер из списка. Наконец, оно выделяет блок 109 и удаляет его номер из списка. Поскольку список свободных блоков в су- перблоке теперь пуст, ядро снова наполняет список, копируя в него содержимое блока 109, являющегося следующей связью в списке с указателями. На Рисунке +------------------------------------------------------------+ | алгоритм alloc /* выделение блока файловой системы */ | | входная информация: номер файловой системы | | выходная информация: буфер для нового блока | | { | | выполнить (пока суперблок заблокирован) | | приостановиться (до того момента, когда с суперблока| | будет снята блокировка); | | удалить блок из списка свободных блоков в суперблоке; | | если (из списка удален последний блок) | | { | | заблокировать суперблок; | | прочитать блок, только что взятый из списка свобод- | | ных (алгоритм bread); | | скопировать номера блоков, хранящиеся в данном бло- | | ке, в суперблок; | | освободить блочный буфер (алгоритм brelse); | | снять блокировку с суперблока; | | возобновить выполнение процессов (после снятия бло- | | кировки с суперблока); | | } | | получить буфер для блока, удаленного из списка (алго- | | ритм getblk); | | обнулить содержимое буфера; | | уменьшить общее число свободных блоков; | | пометить суперблок как "измененный"; | | возвратить буфер; | | } | +------------------------------------------------------------+ Рисунок 4.19. Алгоритм выделения дискового блока 4.20(г) показан заполненный список в суперблоке и следующий связной блок с номером 211. Алгоритмы назначения и освобождения индексов и дисковых блоков сходятс в том, что ядро использует суперблок в качестве кеша, хранящего указатели на свободные ресурсы - номера блоков и номера индексов. Оно поддерживает список номеров блоков с указателями, такой, что каждый номер свободного блока в файловой системе появляется в некотором элементе списка, но ядро не поддер- живает такого списка для свободных индексов. Тому есть три причины. 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. Суперблок является дисковым блоком и содержит кроме списка свободных блоков и другую информацию, как показано в данной главе. Поэтому список свободных блоков в суперблоке не может содержать больше номеров свободных блоков, чем может поместиться в одном дисковом блоке в связанном списке свободных дисковых блоков. Какое число номеров свободных блоков было бы оптимальным для хранения в одном блоке из связанного списка ? ГЛАВА 5 СИСТЕМНЫЕ ОПЕРАЦИИ ДЛЯ РАБОТЫ С ФАЙЛОВОЙ СИСТЕМОЙ В последней главе рассматривались внутренние структуры данных для файло- вой системы и алгоритмы работы с ними. В этой главе речь пойдет о системных функциях для работы с файловой системой с использованием понятий, введенных в предыдущей главе. Рассматриваются системные функции, обеспечивающие обра- щение к существующим файлам, такие как open, read, write, lseek и close, за- тем функции создания новых файлов, а именно, creat и mknod, и, наконец, фун- кции для работы с индексом или для передвижения по файловой системе: chdir, chroot, chown, stat и fstat. Исследуются более сложные системные функции: pipe и dup имеют важное значение для реализации каналов в shell'е; mount и umount расширяют видимое для пользователя дерево файловых систем; link и unlink изменяют иерархическую структуру файловой системы. Затем дается пред- ставление об абстракциях, связанных с файловой системой, в отношении поддер- жки различных файловых систем, подчиняющихся стандартным интерфейсам. В пос- леднем разделе главы речь пойдет о сопровождении файловой системы. Глава знакомит с тремя структурами данных ядра: таблицей файлов, в которой кажда запись связана с одним из открытых в системе файлов, таблицей пользователь- ских дескрипторов файлов, в которой каждая запись связана с файловым деск- риптором, известным процессу, и таблицей монтирования, в которой содержитс информация по каждой активной файловой системе. Функции для работы с файловой системой +----------------------------------------------------------------+ +----------------------------------------------------------------| | Воз- | Используют | Назна- | Рабо- | Ввод- | Работа- | Управ-| | вра- | алгоритм | чают | тают | вывод | ют со | ление | | щают | 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. Функции для работы с файловой системой и их связь с другими алгоритмами На Рисунке 5.1 показана взаимосвязь между системными функциями и алго- ритмами, описанными ранее. Системные функции классифицируются на несколько категорий, хотя некоторые из функций присутствуют более, чем в одной катего- рии: * Системные функции, возвращающие дескрипторы файлов для использовани другими системными функциями; * Системные функции, использующие алгоритм namei для анализа имени пути поиска; * Системные функции, назначающие и освобождающие индекс с использованием алгоритмов ialloc и ifree; * Системные функции, устанавливающие или изменяющие атрибуты файла; * Системные функции, позволяющие процессу производить ввод-вывод данных с использованием алгоритмов alloc, free и алгоритмов выделения буфера; * Системные функции, изменяющие структуру файловой системы; * Системные функции, позволяющие процессу изменять собственное представле- ние о структуре дерева файловой системы. 5.1 OPEN Вызов системной функции open (открыть файл) - это первый шаг, который должен сделать процесс, чтобы обратиться к данным в файле. Синтаксис вызова функции open: fd = open(pathname,flags,modes); где pathname - имя файла, flags указывает режим открытия (например, для чте- ния или записи), а modes содержит права доступа к файлу в случае, если файл создается. Системная функция open возвращает целое число (*), именуемое пользовательским дескриптором файла. Другие операции над файлами, такие как чтение, запись, по- зиционирование головок чтения-записи, воспроизведение дескриптора файла, ус- тановка параметров ввода-вывода, определение статуса файла и закрытие файла, используют значение дескриптора файла, возвращаемое системной функцией open. Ядро просматривает файловую систему в поисках файла по его имени, ис- пользуя алгоритм namei (см. Рисунок 5.2). Оно проверяет права на открытие файла после того, как обнаружит копию индекса файла в памяти, и выделяет от- крываемому файлу запись в таблице файлов. Запись таблицы файлов содержит указатель на индекс открытого файла и поле, в котором хранится смещение в байтах от начала файла до места, откуда предполагается начинать выполнение последующих операций чтения или записи. Ядро сбрасывает это смещение в 0 во время открытия файла, имея в виду, что исходная операция чтения или записи по умолчанию будет производиться с начала файла. С другой стороны, процесс может открыть файл в режиме записи в конец, в этом случае ядро устанавливает значение смещения, равное размеру файла. Ядро выделяет запись в личной (зак- рытой) таблице в адресном пространстве задачи, выделенном процессу (таблица эта называется таблицей пользовательских дескрипторов файлов), и запоминает указатель на эту запись. Указателем выступает дескриптор файла, возвращаемый пользователю. Запись в таблице пользовательских файлов указывает на запись в глобальной таблице файлов. --------------------------------------- (*) Все системные функции возвращают в случае неудачного завершения код -1. Код возврата, равный -1, больше не будет упоминаться при рассмотрении синтаксиса вызова системных функций. +------------------------------------------------------------+ | алгоритм open | | входная информация: имя файла | | режим открытия | | права доступа (при создании файла) | | выходная информация: дескриптор файла | | { | | превратить имя файла в идентификатор индекса (алгоритм | | namei); | | если (файл не существует или к нему не разрешен доступ) | | возвратить (код ошибки); | | выделить для индекса запись в таблице файлов, инициали- | | зировать счетчик, смещение; | | выделить запись в таблице пользовательских дескрипторов | | файла, установить указатель на запись в таблице файлов;| | если (режим открытия подразумевает усечение файла) | | освободить все блоки файла (алгоритм free); | | снять блокировку (с индекса); /* индекс заблокирован | | выше, в алгоритме | | namei */ | | возвратить (пользовательский дескриптор файла); | | } | +------------------------------------------------------------+ Рисунок 5.2. Алгоритм открытия файла Предположим, что процесс, открывая файл "/etc/passwd" дважды, один раз только для чтения и один раз только для записи, и однажды файл "local" дл чтения и для записи (**), выполняет следующий набор операторов: fd1 = open("/etc/passwd",O_RDONLY); fd2 = open("local",O_RDWR); fd3 = open("/etc/passwd",O_WRONLY); На Рисунке 5.3 показана взаимосвязь между таблицей индексов, таблицей файлов и таблицей пользовательских дескрипторов файла. Каждый вызов функции open возвращает процессу дескриптор файла, а соответствующая запись в табли- це пользовательских дескрипторов файла указывает на уникальную запись в таб- лице файлов ядра, пусть даже один и тот же файл ("/etc/passwd") открываетс дважды. Записи в таблице файлов для всех экземпляров одного и того же открытого файла указывают на одну запись в таблице индексов, хранящихся в памяти. Про- цесс может обращаться к файлу "/etc/passwd" с чтением или записью, но только через дескрипторы файла, имеющие значения 3 и 5 (см. рисунок).Ядро запомина- ет разрешение на чтение или запись в файл в строке таблицы файлов,выделенной во время выполнения функции open. Предположим, что второй процесс выполняет следующий набор операторов: --------------------------------------- (**) В описании вызова системной функции 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("private",O_RDONLY); На Рисунке 5.4 показана взаимосвязь между соответствующими структурами дан- ных, когда оба процесса (и больше никто) имеют открытые файлы. Снова резуль- татом каждого вызова функции open является выделение уникальной точки входа в таблице пользовательских дескрипторов файла и в таблице файлов ядра, и яд- ро хранит не более одной записи на каждый файл в таблице индексов, размещен- ных в памяти. Запись в таблице пользовательских дескрипторов файла по умолчанию хранит смещение в файле до адреса следующей операции вводавывода и указывает непос- редственно на точку входа в таблице индексов для файла, устраняя необходи- мость в отдельной таблице файлов ядра. Вышеприведенные примеры показывают взаимосвязь между записями таблицы пользовательских дескрипторов файла и за- таблицы пользова- тельских дескрип- торов файла (процесс 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. Структуры данных после того, как два процесса произвели открытие файлов писями в таблице файлов ядра типа "один к одному". Томпсон, однако, отмеча- ет, что им была реализована таблица файлов как отдельная структура, позволя- ющая совместно использовать один и тот же указатель смещения нескольким пользовательским дескрипторам файла (см. [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, выполняющий чтение обычного файла. Ядро обращается в таблице файлов к записи, котора соответствует значению пользовательского дескриптора файла, следу за указателем (см. Рисунок 5.3). Затем оно устанавливает значения нескольких параметров ввода-вывода в адресном пространстве процесса (Рисунок 5.6), тем самым устраняя необходимость в их передаче в качестве параметров функции. В частности, ядро указывает в качестве режима ввода-вывода "чтение", устанав- ливает флаг, свидетельствующий о том, что ввод-вывод направляется в адресное пространство пользователя, значение поля счетчика байтов приравнивает коли- честву байт, которые будут прочитаны, устанавливает адрес пользовательского буфера данных и, наконец, значение смещения (из таблицы файлов), равное сме- щению в байтах внутри файла до места, откуда начинается ввод-вывод. После того, как ядро установит значения параметров ввода-вывода в адресном прост- ранстве процесса, оно обращается к индексу, используя указатель из таблицы файлов, и блокирует его прежде, чем начать чтение из файла. Затем в алгоритме начинается цикл, выполняющийся до тех пор, пока опера- ция чтения не будет произведена до конца. Ядро преобразует смещение в байтах внутри файла в номер блока, используя ал- +------------------------------------------------------------+ | алгоритм read | | входная информация: пользовательский дескриптор файла | | адрес буфера в пользовательском про- | | цессе | | количество байт, которые нужно прочи- | | тать | | выходная информация: количество байт, скопированных в поль-| | зовательское пространство | | { | | обратиться к записи в таблице файлов по значению пользо-| | вательского дескриптора файла; | | проверить доступность файла; | | установить параметры в адресном пространстве процесса, | | указав адрес пользователя, счетчик байтов, параметры | | ввода-вывода для пользователя; | | получить индекс по записи в таблице файлов; | | заблокировать индекс; | | установить значение смещения в байтах для адресного | | пространства процесса по значению смещения в таблице | | файлов; | | выполнить (пока значение счетчика байтов не станет удов-| | летворительным) | | { | | превратить смещение в файле в номер дискового блока | | (алгоритм bmap); | | вычислить смещение внутри блока и количество байт, | | которые будут прочитаны; | | если (количество байт для чтения равно 0) | | /* попытка чтения конца файла */ | | прерваться; /* выход из цикла */ | | прочитать блок (алгоритм breada, если производится | | чтение с продвижением, и алгоритм bread - в против- | | ном случае); | | скопировать данные из системного буфера по адресу | | пользователя; | | скорректировать значения полей в адресном простран- | | стве процесса, указывающие смещение в байтах внутри | | файла, количество прочитанных байт и адрес для пе- | | редачи в пространство пользователя; | | освободить буфер; /* заблокированный в алгоритме | | bread */ | | } | | разблокировать индекс; | | скорректировать значение смещения в таблице файлов для | | следующей операции чтения; | | возвратить (общее число прочитанных байт); | | } | +------------------------------------------------------------+ Рисунок 5.5. Алгоритм чтения из файла +------------------------------------------------------+ | 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, производимыми первым процессом, первый процесс может прочитать непредвиденные данные, однако структуры данных ядра сохранят свою согласо- ванность. Предположим, к примеру, что ядро выполняет два процесса, конкурирующие +------------------------------------------------------------+ | #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.8). Если допустить, что оба процесса выполняют опера- цию open до того, как любой из них вызывает системную функцию read или write, ядро может выполнять функции чтения и записи в любой из шести после- довательностей: чтение1, чтение2, запись1, запись2, или чтение1, запись1, чтение2, запись2, или чтение1, запись1, запись2, чтение2 и т.д. Состав ин- формации, считываемой процессом A, зависит от последовательности, в которой система выполняет функции, вызываемые двумя процессами; система не гаранти- рует, что данные в файле останутся такими же, какими они были после открыти файла. Использование возможности захвата файла и записей (раздел 5.4) позво- ляет процессу гарантировать сохранение целостности файла после его открытия. Наконец, программа на Рисунке 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" и быстро удаляют их. Использование отложенной записи может сократить количество обращений к диску для записи во временные файлы. 5.4 ЗАХВАТ ФАЙЛА И ЗАПИСИ В первой версии системы 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| | | | | | +---------| | | | | 1| | | | | | +---------| +------------| | | 2| | | | | | +---------| | | | | 3| ----+----+ | | | | +---------| | | | +--------------| 4| ----+---+| | | +---->| счет- | +---------| || | | | | чик (/etc/ | 5| ----+--+|| +------------| | +-->| 2 passwd)| +---------| ||| | счет- | | | +--------------| | | ||+-->| чик +--+ | | | | | || | 1 | | | | | | || +------------| | | | +---------+ || | | | | | || | | | | | +---------+ || | | | | | 0| | || +------------| | | | +---------| || | счет- | | | | 1| | |+--->| чик +----|+ | | +---------| | | 1 | || | | 2| | | +------------| || | | +---------| | | | || +--------------| 3| NULL | | | | || | счет- | +---------| | | | |+->| чик (local)| 4| NULL | | | | | | 1 | +---------| | | | | +--------------| 5| | | | | | | | +---------| | +------------| | | | | | | | счетчик 0 | | | | +---------+ | +------------| | | | | | | | +--------------| | | | | | счет- | | +------------| | | чик (private)| | | счетчик 1 | | | 0 | +---->| +----+ +--------------| +------------| | | | | +--------------+ | | +------------| | счетчик 0 | +------------+ Рисунок 5.11. Таблицы после закрытия файла дескрипторов файлов, пусты. Счетчики в записях таблицы файлов теперь имеют значение 0, а сами записи пусты. Счетчики ссылок на файлы "/etc/passwd" и "private" в индексах также уменьшились. Индекс для файла "private" находитс в списке свободных индексов, поскольку счетчик ссылок на него равен 0, но запись о нем не пуста. Если еще какой-нибудь процесс обратится к файлу "private", пока индекс еще находится в списке свободных индексов, ядро востребует индекс обратно, как показано в разделе 4.1.2. 5.7 СОЗДАНИЕ ФАЙЛА Системная функция open дает процессу доступ к существующему файлу, а системная функция creat создает в системе новый файл. Синтаксис вызова сис- темной функции creat: fd = creat(pathname,modes); где переменные pathname, modes и fd имеют тот же смысл, что и в системной функции open. Если прежде такого файла не существовало, ядро создает новый файл с указанным именем и указанными правами доступа к нему; если же такой файл уже существовал, ядро усекает файл (освобождает все существующие блоки +------------------------------------------------------------+ | алгоритм creat | | входная информация: имя файла | | установки прав доступа к файлу | | выходная информация: дескриптор файла | | { | | получить индекс для данного имени файла (алгоритм namei);| | если (файл уже существует) | | { | | если (доступ не разрешен) | | { | | освободить индекс (алгоритм iput); | | возвратить (ошибку); | | } | | } | | в противном случае /* файл еще не существует */ | | { | | назначить свободный индекс из файловой системы (алго- | | ритм ialloc); | | создать новую точку входа в родительском каталоге: | | включить имя нового файла и номер вновь назначенного | | индекса; | | } | | выделить для индекса запись в таблице файлов, инициализи-| | ровать счетчик; | | если (файл существовал к моменту создания) | | освободить все блоки файла (алгоритм free); | | снять блокировку (с индекса); | | возвратить (пользовательский дескриптор файла); | | } | +------------------------------------------------------------+ Рисунок 5.12. Алгоритм создания файла данных и устанавливает размер файла равным 0) при наличии соответствующих прав доступа к нему (***). На Рисунке 5.12 приведен алгоритм создания файла. Ядро проводит синтаксический анализ имени пути поиска, используя алго- ритм namei и следуя этому алгоритму буквально, когда речь идет о разборе имен каталогов. Однако, когда дело касается последней компоненты имени пути поиска, а именно идентификатора создаваемого файла, namei отмечает смещение в байтах до первой пустой позиции в каталоге и запоминает это смещение в пространстве процесса. Если ядро не обнаружило в каталоге компоненту имени пути поиска, оно в ко- нечном счете впишет имя компоненты в только что найденную пустую позицию. Если в каталоге нет пустых позиций, ядро запоминает смещение до конца ката- лога и создает новую позицию там. Оно также запоминает в пространстве про- цесса индекс просматриваемого каталога и держит индекс заблокированным; ка- талог становится по отношению к новому файлу родительским каталогом. Ядро не записывает пока имя нового файла в каталог, так что в случае возникновени ошибок ядру приходится меньше переделывать. Оно проверяет наличие у процесса разрешения на запись в каталог. Поскольку процесс будет производить запись в каталог в результате выполнения функции creat, наличие разрешения на запись в каталог означает, что процессам дозволяется создавать файлы в каталоге. Предположив, что под данным именем ранее не существовало файла, ядро назначает новому файлу индекс, используя алгоритм ialloc (раздел 4.6). Затем оно записывает имя нового файла и номер вновь выделенного индекса в роди- тельский каталог, а смещение в байтах сохраняет в пространстве процесса. Впоследствии ядро освобождает индекс родительского каталога, удерживаемый с того времени, когда в каталоге производился поиск имени файла. Родительский каталог теперь содержит имя нового файла и его индекс. Ядро записывает вновь выделенный индекс на диск (алгоритм bwrite), прежде чем записать на диск ка- талог с новым именем. Если между операциями записи индекса и каталога прои- зойдет сбой системы, в итоге окажется, что выделен индекс, на который не ссылается ни одно из имен путей поиска в системе, однако система будет функ- ционировать нормально. Если, с другой стороны, каталог был записан раньше вновь выделенного индекса и сбой системы произошел между ними, файловая сис- тема будет содержать имя пути поиска, ссылающееся на неверный индекс (более подробно об этом см. в разделе 5.16.1). Если данный файл уже существовал до вызова функции creat, ядро обнаружи- вает его индекс во время поиска имени файла. Старый файл должен позволять процессу производить запись в него, чтобы можно было создать "новый" файл с тем же самым именем, так как ядро изменяет содержимое файла при выполнении функции creat: оно усекает файл, освобождая все информационные блоки по ал- горитму free, так что файл будет выглядеть как вновь созданный. Тем не ме- нее, владелец и права доступа к файлу остаются прежними: ядро не передает право собственности на файл владельцу процесса и игнорирует права доступа, указанные процессом в вызове функции. Наконец, ядро не проверяет наличие разрешения на запись в каталог, являющийся родительским для существующего файла, поскольку оно не меняет содержимого каталога. Функция creat продолжает работу, выполняя тот же алгоритм, что и функци open. Ядро выделяет созданному файлу запись в таблице файлов, чтобы процесс мог читать из файла, а также запись в таблице пользовательских дескрипторов файла, и в конце концов возвращает указатель на последнюю запись в виде пользовательского дескриптора файла. --------------------------------------- (***) Системная функция open имеет два флага, O_CREAT (создание) и O_TRUNC (усечение). Если процесс устанавливает в вызове функции флаг O_CREAT и файл не существует, ядро создаст файл. Если файл уже существует, он не будет усечен, если только не установлен флаг O_TRUNC. 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. Тем не менее, совместное использова- ние смещений чтения и записи в индексе наблюдалось и до реализации поимено- ванных каналов. Процессы, обращающиеся к непоименованным каналам, разделяют доступ к каналу через общие точки входа в таблицу файлов, поэтому они могли бы по умолчанию хранить смещения записи и чтения в таблице файлов, как это принято для обычных файлов. Это не было сделано, так как процедуры низкого уровня, работающие в ядре, больше не имеют доступа к записям в таблице фай- лов: программа упростилась за счет того, что процессы совместно используют значения смещений, хранящиеся в индексе. .te1 5.12.4 Закрытие каналов При закрытии канала процесс выполняет ту же самую процедуру, что и при закрытии обычного файла, за исключением того, что ядро, прежде чем освобо- дить индекс канала, выполняет специальную обработку. Оно уменьшает количест- во процессов чтения из канала или записи в канал в зависимости от типа фай- лового дескриптора. Если значение счетчика числа записывающих в канал про- цессов становится равным 0 и имеются процессы, приостановленные в ожидании чтения данных из канала, ядро возобновляет выполнение последних и они завер- шают свои операции чтения без возврата каких-либо данных. Если становитс равным 0 значение счетчика числа считывающих из канала процессов и имеютс процессы, приостановленные в ожидании возможности записи данных в канал, яд- ро возобновляет выполнение последних и посылает им сигнал (глава 7) об ошиб- ке. В обоих случаях не имеет смысла продолжать держать процессы приостанов- ленными, если нет надежды на то, что состояние канала когда-нибудь изменит- ся. Например, если процесс ожидает возможности производить чтение из непои- менованного канала и в системе больше нет процессов, записывающих в этот ка- нал, значит, записывающий процесс никогда не появится. Несмотря на то, что если канал поименованный, в принципе возможно появление нового считывающего или записывающего процесса, ядро трактует эту ситуацию точно так же, как и для непоименованных каналов. Если к каналу не обращается ни один записываю- щий или считывающий процесс, ядро освобождает все информационные блоки кана- ла и переустанавливает индекс таким образом, чтобы он указывал на то, что канал пуст. Когда ядро освобождает индекс обычного канала, оно освобождает для переназначения и дисковую копию этого индекса. 5.12.5 Примеры Программа на Рисунке 5.18 иллюстрирует искусственное использование кана- лов. Процесс создает канал и входит в бесконечный цикл, записывая в канал +---------------------------------+ | 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. Чтение из канала и запись в канал строку символов "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 дублирует дескриптор файла, она увеличивает значение счетчика в соответствующей записи таблицы файлов - за- таблица пользова- тельских дескрип- торов файла таблица файлов таблица индексов +---------+ +------------+ +--------------+ 0| ----+----+ | | | | +---------| | | | | | 1| ----+---++-->| | | | +---------| | +------------| | | 2| ----+--++--->| | | | +---------| +---->| | | | 3| ----+----+ | | | | +---------| | | | +--------------| 4| ----+---+| | | +---->| счет- | +---------| || | | | | чик (/etc/ | 5| ----+--+|| +------------| | +-->| 2 passwd)| +---------| ||| | счет- | | | +--------------| 6| ----++ ||+-->| чик +--+ | | | +---------|+-||--->| 2 | | | | 7| | || +------------| | | | +---------| || | | | | | | | || +------------| | | | +---------+ || | счетчик | | | | |+--->| 1 +----|+ | | | +------------| || | | | | | || +--------------| | | | || | счет- | | | | |+->| чик (local)| | | | | | 1 | | | | | +--------------| | +------------| | | | | | счетчик | | | | +---->| 1 +----+ | | +------------| | | | | +--------------+ | | +------------+ Рисунок 5.20. Структуры данных после выполнения функции dup писи, на которую указывают связанные с ней точки входа в таблице файловых дескрипторов, которых теперь стало на одну больше. Например, обзор структур данных, изображенных на Рисунке 5.20, показывает, что процесс вызывает сле- дующую последовательность функций: он открывает (open) файл с именем "/etc/passwd" (файловый дескриптор 3), затем открывает файл с именем "local" (файловый дескриптор 4), снова файл с именем "/etc/passwd" (файловый деск- риптор 5) и, наконец, дублирует (dup) файловый дескриптор 3, возвращая деск- риптор 6. Возможно, 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 (Рисунок 5.26). Она похожа на версию алгоритма, приведенную на Рисунке 4.11. Однако, после обнаружени в каталоге номера индекса для данной компоненты пути поиска ядро проверяет, не указывает ли номер индекса на то, что это корневой индекс файловой систе- мы. Если это так и если текущий рабочий индекс так же является корневым, а +------------------------------------------------------------+ | алгоритм namei /* превращение имени пути поиска в индекс */| | входная информация: имя пути поиска | | выходная информация: заблокированный индекс | | { | | если (путь поиска берет начало с корня) | | рабочий индекс = индексу корня (алгоритм iget); | | в противном случае | | рабочий индекс = индексу текущего каталога | | (алгоритм iget); | | | | выполнить (пока путь поиска не кончился) | | { | | считать следующую компоненту имени пути поиска; | | проверить соответствие рабочего индекса каталогу | | и права доступа; | | если (рабочий индекс соответствует корню и компо- | | нента имени "..") | | продолжить; /* цикл с условием продолжения */| | поиск компоненты: | | считать каталог (рабочий индекс), повторяя алго- | | ритмы bmap, bread и brelse; | | если (компонента соответствует записи в каталоге | | (рабочем индексе)) | | { | | получить номер индекса для совпавшей компонен-| | ты; | | если (найденный индекс является индексом кор- | | ня и рабочий индекс является индексом корня | | и имя компоненты "..") | | { | | /* пересечение точки монтирования */ | | получить запись в таблице монтирования для | | рабочего индекса; | | освободить рабочий индекс (алгоритм iput); | | рабочий индекс = индексу точки монтирования;| | заблокировать индекс точки монтирования; | | увеличить значение счетчика ссылок на рабо- | | чий индекс; | | перейти к поиску компоненты (для ".."); | | } | | освободить рабочий индекс (алгоритм iput); | | рабочий индекс = индексу с новым номером | | (алгоритм iget); | | } | | в противном случае /* компонента отсутствует в | | каталоге */ | | возвратить (нет индекса); | | } | | возвратить (рабочий индекс); | | } | +------------------------------------------------------------+ Рисунок 5.26. Модификация алгоритма синтаксического анализа имени файла компонента пути поиска, в свою очередь, имеет имя "..", ядро идентифицирует индекс как точку монтирования. Оно находит в таблице монтирования запись, номер устройства в которой совпадает с номером устройства для последнего из найденных индексов, получает индекс для каталога, в котором производитс монтирование, и продолжает поиск компоненты с именем "..", используя только что полученный индекс в качестве рабочего. В корне файловой системы, тем не менее, корневым каталогом является "..". В вышеприведенном примере (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), записывает на диск все недав- но скорректированные суперблоки и корректирует дисковые копии всех индексов, которые требуют этого. Казалось, было бы достаточно откорректировать диско- вые блоки, суперблок и индексы только для демонтируемой файловой системы, однако в целях сохранения преемственности изменений ядро выполняет аналогичные действия для всей системы в целом. Затем ядро ос- вобождает корневой индекс монтированной файловой системы, удерживаемый с мо- мента первого обращения к нему во время выполнения функции mount, и запуска- +------------------------------------------------------------+ | алгоритм umount | | входная информация: имя специального файла, соответствую- | | щего демонтируемой файловой системе | | выходная информация: отсутствует | | { | | если (пользователь не является суперпользователем) | | возвратить (ошибку); | | получить индекс специального файла (алгоритм namei); | | извлечь старший и младший номера демонтируемого устрой-| | ства; | | получить в таблице монтирования запись для демонтируе- | | мой системы, исходя из старшего и младшего номеров; | | освободить индекс специального файла (алгоритм iput); | | удалить из таблицы областей записи с разделяемым текс- | | том для файлов, принадлежащих файловой | | системе; /* глава 7ххх */ | | скорректировать суперблок, индексы, выгрузить буферы | | на диск; | | если (какие-то файлы из файловой системы все еще ис- | | пользуются) | | возвратить (ошибку); | | получить из таблицы монтирования корневой индекс монти-| | рованной файловой системы; | | заблокировать индекс; | | освободить индекс (алгоритм iput); /* iget был при | | монтировании */ | | запустить процедуру закрытия для специального устрой- | | ства; | | сделать недействительными (отменить) в пуле буферы из | | демонтируемой файловой системы; | | получить из таблицы монтирования индекс точки монтиро- | | вания; | | заблокировать индекс; | | очистить флаг, помечающий индекс как "точку монтирова- | | ния"; | | освободить индекс (алгоритм iput); /* iget был при | | монтировании */ | | освободить буфер, используемый под суперблок; | | освободить в таблице монтирования место, занятое ранее;| | } | +------------------------------------------------------------+ Рисунок 5.27. Алгоритм демонтирования файловой системы ет из драйвера процедуру закрытия устройства, содержащего файловую систему. Впоследствии ядро просматривает буферы в буферном кеше и делает недействи- тельными те из них, в которых находятся блоки демонтируемой файловой систе- мы; в хранении информации из этих блоков в кеше больше нет необходимости. Делая буферы недействительными, ядро вставляет их в начало списка свободных буферов, в то время как блоки с актуальной информацией остаются в буферном кеше. Ядро сбрасывает в индексе системы, где производилось монтирование, флаг "точки монтирования", установленный функцией 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 устраняет необходимость в связывании каталогов. На Рисунке 5.29 показан алгоритм функции link. Сначала ядро, использу алгоритм namei, определяет местонахождение индекса исходного файла, увеличи- вает значение счетчика связей в индексе, корректирует дисковую копию индекса (для обеспечения согласованности) и снимает с индекса блокировку. Затем ядро ищет файл с новым именем; если он существует, функция link завершается неу- дачно и ядро восстанавливает прежнее значение счетчика связей, измененное ранее. В противном случае ядро находит в родительском каталоге свободную за- пись для файла с новым именем, записывает в нее новое имя и номер индекса исходного файла и освобождает индекс родительского каталога, используя алго- +------------------------------------------------------------+ | алгоритм link | | входная информация: существующее имя файла | | новое имя файла | | выходная информация: отсутствует | | { | | получить индекс для существующего имени файла (алгоритм | | namei); | | если (у файла слишком много связей или производится | | связывание каталога без разрешения суперпользователя) | | { | | освободить индекс (алгоритм iput); | | возвратить (ошибку); | | } | | увеличить значение счетчика связей в индексе; | | откорректировать дисковую копию индекса; | | снять блокировку с индекса; | | получить индекс родительского каталога для включения но-| | вого имени файла (алгоритм namei); | | если (файл с новым именем уже существует или существую- | | щий файл и новый файл находятся в разных файловых сис- | | темах) | | { | | отменить корректировку, сделанную выше; | | возвратить (ошибку); | | } | | создать запись в родительском каталоге для файла с но- | | вым именем: | | включить в нее новое имя и номер индекса существую- | | щего файла; | | освободить индекс родительского каталога (алгоритм | | iput); | | освободить индекс существующего файла (алгоритм iput); | | } | +------------------------------------------------------------+ Рисунок 5.29. Алгоритм связывания файлов ритм 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). Следовательно, система не может выйти из тупика без перезагрузки. Если к файлам, заблокированным процессами, нет обращений со стороны других процес- сов, взаимная блокировка не затрагивает остальные процессы в системе. Одна- ко, любые процессы, обратившиеся к этим файлам (или обратившиеся к другим файлам через заблоки- рованный каталог), непременно зайдут в тупик. Таким образом, если заблокиро- ваны файлы "/bin" или "/usr/bin" (обычные хранилища команд) или файл "/bin/sh" (командный процессор shell), последствия для системы будут гибель- ными. 5.16 UNLINK Системная функция unlink удаляет из каталога точку входа для файла. Син- таксис вызова функции unlink: unlink(pathname); где pathname указывает имя файла, удаляемое из иерархии каталогов. Если про- цесс разрывает данную связь файла с каталогом при помощи функции unlink, по указанному в вызове функции имени файл не будет доступен, пока в каталоге не Процесс 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 создана еще одна запись с этим именем. Например, при выполнении следующего фрагмента программы: unlink("myfile"); fd = open("myfile",O_RDONLY); функция open завершится неудачно, поскольку к моменту ее выполнения в теку- щем каталоге больше не будет файла с именем myfile. Если удаляемое имя явля- ется последней связью файла с каталогом, ядро в итоге освобождает все инфор- мационные блоки файла. Однако, если у файла было несколько связей, он оста- ется все еще доступным под другими именами. На Рисунке 5.31 представлен алгоритм функции unlink. Сначала для поиска файла с удаляемой связью ядро использует модификацию алгоритма namei, кото- рая вместо индекса файла возвращает индекс родительского каталога. Ядро об- ращается к индексу файла в памяти, используя алгоритм iget. (Особый случай, связанный с удалением имени файла ".", будет рассмотрен в упражнении). После проверки отсутствия ошибок и (для исполняемых файлов) удаления из таблицы областей записей с неактивным разделяемым текстом (глава 7) ядро стирает им файла из родительского каталога: сделать значение номера индекса равным 0 достаточно для очистки места, занимаемого именем файла в каталоге. Затем яд- ро производит синхронную запись каталога на диск, гарантируя тем самым, что под своим прежним именем файл уже не будет доступен, уменьшает значение счетчика связей и с помощью алгоритма iput освобождает в памяти индексы ро- дительского каталога и файла с удаляемой связью. При освобождении в памяти по алгоритму iput индекса файла с удаляемой связью, если значения счетчика ссылок и счетчика связей становятся равными 0, ядро забирает у файла обратно дисковые блоки, которые он занимал. На этот индекс больше не указывает ни одно из файловых имен и индекс неактивен. Дл +------------------------------------------------------------+ | алгоритм unlink | | входная информация: имя файла | | выходная информация: отсутствует | | { | | получить родительский индекс для файла с удаляемой | | связью (алгоритм namei); | | /* если в качестве файла выступает текущий каталог... */| | если (последней компонентой имени файла является ".") | | увеличить значение счетчика ссылок в индексе; | | в противном случае | | получить индекс для файла с удаляемой связью (алго-| | ритм iget); | | если (файл является каталогом, но пользователь не явля- | | ется суперпользователем) | | { | | освободить индексы (алгоритм iput); | | возвратить (ошибку); | | } | | если (файл имеет разделяемый текст и текущее значение | | счетчика связей равно 1) | | удалить записи из таблицы областей; | | в родительском каталоге: обнулить номер индекса для уда-| | ляемой связи; | | освободить индекс родительского каталога (алгоритм | | iput); | | уменьшить число связей файла; | | освободить индекс файла (алгоритм iput); | | /* iput проверяет, равно ли число связей 0, если | | * да, | | * освобождает блоки файла (алгоритм free) и | | * освобождает индекс (алгоритм ifree); | | */ | | } | +------------------------------------------------------------+ Рисунок 5.31. Алгоритм удаления связи файла с каталогом того, чтобы забрать дисковые блоки, ядро в цикле просматривает таблицу со- держимого индекса, освобождая все блоки прямой адресации немедленно (в соот- ветствии с алгоритмом free). Что касается блоков косвенной адресации, ядро освобождает все блоки, появляющиеся на различных уровнях косвенности, рекур- сивно, причем в первую очередь освобождаются блоки с меньшим уровнем. Оно обнуляет номера блоков в таблице содержимого индекса и устанавливает размер файла в индексе равным 0. Затем ядро очищает в индексе поле типа файла, ука- зывая тем самым, что индекс свободен, и освобождает индекс по алгоритму ifree. Ядро делает необходимую коррекцию на диске, так как дисковая копи индекса все еще указывает на то, что индекс используется; теперь индекс сво- боден для назначения другим файлам. 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). Как бы то ни было, система сохраняет свою целостность; самое худшее, что может произойти, это обращение не к тому файлу - с возможным на- Процесс 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. Удаление связи с открытым файлом рушением защиты - но соперничества такого рода на практике довольно редки. Процесс может удалить связь файла в то время, как другому процессу нуж- но, чтобы файл оставался открытым. (Даже процесс, удаляющий связь, может быть процессом, выполнившим это открытие). Поскольку ядро снимает с индекса блокировку по окончании выполнения функции open, функция unlink завершитс успешно. Ядро будет выполнять алгоритм unlink точно так же, как если бы файл не был открыт, и удалит из каталога запись о файле. Теперь по имени удален- ной связи к файлу не сможет обратиться никакой другой процесс. Однако, так как системная функция open увеличила значение счетчика ссылок в индексе, яд- ро не очищает содержимое файла при выполнении алгоритма iput перед заверше- нием функции unlink. Поэтому процесс, открывший файл, может производить над файлом все обычные действия по его дескриптору, включая чтение из файла и запись в файл. Но когда процесс закрывает файл, значение счетчика ссылок в алгоритме iput становится равным 0, и ядро очищает содержимое файла. Короче говоря, процесс, открывший файл, продолжает работу так, как если бы функци unlink не выполнялась, а unlink, в свою очередь, работает так, как если бы файл не был открыт. Другие системные функции также могут продолжать выпол- няться в процессе, открывшем файл. В приведенном на Рисунке 5.33 примере процесс открывает файл, указанный в качестве параметра, и затем удаляет связь только что открытого файла. Фун- кция stat завершится неудачно, поскольку первоначальное имя после unlink больше не указывает на файл (предпо- лагается, что тем временем никакой другой процесс не создал файл с тем же именем), но функция 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. Затрагивая тему абстрактных обращений к файловым системам, ответьте на вопрос: следует ли файловой системе каждого типа иметь личную операцию блокирования, вызываемую из общей программы, или же достаточно общей операции блокирования ? ГЛАВА 6 СТРУКТУРА ПРОЦЕССОВ В главе 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 представляет собой полную диаграмму переходов процесса из состояния в состояние. Рассмотрим с помощью модели переходов ти- пичное поведение процесса. Ситуации, которые будут обсуждаться, несколько искусственны и процессы не всегда имеют дело с ними, но эти ситуации вполне Выполняется в режиме задачи +-------+ | 1 | Вызов функ- | | ции, преры- +-------+ вание | ^ ^ Преры- +-----+ +-------+ | | вание, | | | +-------+ +---+ Возврат в возврат| | | | Возврат | режим задачи из пре-| | | | | рыва-| v v | Выполняет- | +-------+ ния | +-------+ся в режи- +-------+ | | +-->| |ме ядра | | | 9 |<-----------| 2 +------------>| 7 | | | Выход | | Резервирует-| | +-------+ +-------+ ся +-------+ Прекращение | ^ Зарезер- существования | | вирован +---------------+ +------+ | Приостанов Запуск | v | При-+-------+ +-------+ Готов к ос- | | Возобновление | | запуску та- | 4 +----------------------->| 3 | в памяти нов-| | | | лен +-------+ +-------+ в па- | | ^ ^ мяти | | | | Достаточно | | | | памяти | | | +---+ | Вы- Вы- | | | | грузка грузка | | | Создан | | |За- +-------+ | | |груз-| | fork | | |ка | 8 |<----- | | | +-------+ | | | | Недоста- | | | +---+ точно | | | | памяти | | | | (только система | | | | подкачки) v v | v +-------+ +-------+ | | Возобновление | | | 6 +----------------------->| 5 | +-------+ +-------+ Приостановлен, Готов к запуску, выгружен выгружен Рисунок 6.1. Диаграмма переходов процесса из состояния в сос- тояние применимы для иллюстрации различных переходов. Начальным состоянием модели является создание процесса родительским процессом с помощью системной функ- ции fork; из этого состояния процесс неминуемо переходит в состояние готов- ности к запуску (3 или 5). Для простоты предположим, что процесс перешел в состояние "готовности к запуску в памяти" (3). Планировщик процессов в ко- нечном счете выберет процесс для выполнения и процесс перейдет в состояние "выполнения в режиме ядра", где доиграет до конца роль, отведенную ему функ- цией fork. После всего этого процесс может перейти в состояние "выполнения в режиме задачи". По прохождении определенного периода времени может произойти преры- вание работы процессора по таймеру и процесс снова перейдет в состояние "вы- полнения в режиме ядра". Как только программа обработки прерывания закончит работу, ядру может понадобиться подготовить к запуску другой процесс, поэто- му первый процесс перейдет в состояние "резервирования", уступив дорогу вто- рому процессу. Состояние "резервирования" в действительности не отличаетс от состояния "готовности к запуску в памяти" (пунктирная линия на рисунке, соединяющая между собой оба состояния, подчеркивает их эквивалентность), но они выделяются в отдельные состояния, чтобы подчеркнуть, что процесс, выпол- няющийся в режиме ядра, может быть зарезервирован только в том случае, если он собирается вернуться в режим задачи. Следовательно, ядро может при необ- ходимости подкачивать процесс из состояния "резервирования". При известных условиях планировщик выберет процесс для исполнения и тот снова вернется в состояние "выполнения в режиме задачи". Когда процесс выполняет вызов системной функции, он из состояния "выпол- нения в режиме задачи" переходит в состояние "выполнения в режиме ядра". Предположим, что системной функции требуется ввод-вывод с диска и поэтому процесс вынужден дожидаться завершения ввода-вывода. Он переходит в состоя- ние "приостанова в памяти", в котором будет находиться до тех пор, пока не получит извещения об окончании ввода-вывода. Когда ввод-вывод завершится, произойдет аппаратное прерывание работы центрального процессора и программа обработки прерывания возобновит выполнение процесса, в результате чего он перейдет в состояние "готовности к запуску в памяти". Предположим, что система выполняет множество процессов, которые одновре- менно никак не могут поместиться в оперативной памяти, и программа подкачки (нулевой процесс) выгружает один процесс, чтобы освободить место для другого процесса, находящегося в состоянии "готов к запуску, но выгружен". Первый процесс, выгруженный из оперативной памяти, переходит в то же состояние. Когда программа подкачки выбирает наиболее подходящий процесс для загрузки в оперативную память, этот процесс переходит в состояние "готовности к запуску в памяти". Планировщик выбирает процесс для исполнения и он переходит в сос- тояние "выполнения в режиме ядра". Когда процесс завершается, он исполняет системную функцию 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К, а процесс Частные таблицы областей Области процесса (Виртуальные адреса) +--------+ Коман-+--------------+ | | ды | 8К +-----+ +-->| b | Процесс Дан-+--------------| | | | | A ные | 16К +-----|---+ +--------+ +--------+ +--------------| | | | Стек | 32К +-----|-------------------->| c | +--------------+ | +--------+ | | +------>| | +--------+ | a | Коман-+--------------+ +------>| | ды | 4К +-----+ +--------+ +--------+ Процесс Дан-+--------------| | | B ные | 8К +-------------------------->| e | +--------------| +--------+ | | Стек | 32К +-----+ | | +--------+ +--------------+ +------>| d | | | +--------+ Рисунок 6.2. Процессы и области B читает ячейку с адресом 4К, то они читают одну и ту же ячейку в области 'a'. Область данных и область стека у каждого процесса свои. Область является понятием, не зависящим от способа реализации управлени памятью в операционной системе. Управление памятью представляет собой сово- купность действий, выполняемых ядром с целью повышения эффективности совмес- тного использования оперативной памяти процессами. Примерами способов управ- ления памятью могут служить рассматриваемые в главе 9 замещение страниц па- мяти и подкачка по обращению. Понятие области также не зависит и от собст- венно распределения памяти: например, от того, делится ли память на страницы или на сегменты. С тем, чтобы заложить фундамент для перехода к описанию ал- горитмов подкачки по обращению (глава 9), все приводимые здесь рассуждени относятся, в первую очередь, к организации памяти, базирующейся на страни- цах, однако это не предполагает, что система управления памятью основываетс на указанных алгоритмах. 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 - дл дополнительного использования при передаче информации между процессами и ядром. Системный контекст процесса имеет "статическую часть" (первые три элемента в нижеследующем списке) и "динамическую часть" (последние два элемента). На протяжении всего времени выполнения процесс постоянно рас- полагает одной статической частью системного контекста, но может иметь переменное число динамических частей. Динамическую часть системного кон- текста можно представить в виде стека, элементами которого являются контекстные уровни, которые помеща- ются в стек ядром или выталкиваются из стека при наступлении различных событий. Системный контекст включает в себя следующие компоненты: * Запись в таблице процессов, описывающая состояние процесса (раздел 6.1) и содержащая различную управляющую информацию, к которой ядро всегда мо- жет обратиться. * Часть адресного пространства задачи, выделенная процессу, где хранитс управляющая информация о процессе, доступная только в контексте процес- са. Общие управляющие параметры, такие как приоритет процесса, хранятс в таблице процессов, поскольку обращение к ним должно производиться за пределами контекста процесса. --------------------------------------- (*) Используемые в данном разделе термины "пользовательский контекст" (user-level context), "регистровый контекст" (register context), "сис- темный контекст" (system-level context) и "контекстные уровни" (context layers) введены автором. * Записи частной таблицы областей процесса, общие таблицы областей и таб- лицы страниц, необходимые для преобразования виртуальных адресов в физи- ческие, в связи с чем в них описываются области команд, данных, стека и другие области, принадлежащие процессу. Если несколько процессов совмес- тно используют общие области, эти области входят составной частью в кон- текст каждого процесса, поскольку каждый процесс работает с этими облас- тями независимо от других процессов. В задачи управления памятью входит идентификация участков виртуального адресного пространства процесса, не являющихся резидентными в памяти. * Стек ядра, в котором хранятся записи процедур ядра, если процесс выпол- няется в режиме ядра. Несмотря на то, что все процессы пользуются одними и теми же программами ядра, каждый из них имеет свою собственную копию стека ядра для хранения индивидуальных обращений к функциям ядра. Пусть, например, один процесс вызывает функцию 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, являются машинно-зависимыми. На Рисунке 6.11 показан пример, в котором процесс запрашивает выполнение системной функции (см. следующий раздел) и получает прерывание от диска при ее выполнении. Запустив программу обработки прерывания от диска, система по- лучает прерывание по таймеру и вызывает уже программу обработки прерывани по таймеру. Каждый раз, когда система получает прерывание (или вызывает сис- темную функцию), она создает в стеке новый контекстный уровень и сохраняет регистровый контекст предыдущего уровня. 6.4.2 Взаимодействие с операционной системой через вызовы системных функций Такого рода взаимодействие с ядром было предметом рассмотрения в преды- дущих главах, где шла речь об обычном вызове функций. Очевидно, что обычна последовательность команд обращения к функции не в состоянии переключить вы- полнения процесса с режима задачи на режим ядра. Компилятор с языка Си ис- пользует библиотеку функций, имена которых совпадают с именами системных функций, иначе ссылки на системные функции в пользовательских программах бы- ли бы ссылками на неопределенные имена. В библиотечных функциях обычно ис- полняется команда, переводящая выполнение процесса в режим ядра и побуждаю- щая ядро к запуску исполняемого кода системной функции. В дальнейшем эта ко- манда именуется "внутренним прерыванием операционной системы". Библиотечные процедуры исполняются в режиме задачи, а взаимодействие с операционной сис- темой через вызов системной функции можно определить в нескольких словах как Последовательность прерываний +-------------------------------+ | Контекстный уровень ядра 3 | | Исполнить программу обра- | | ботки прерывания по таймеру | | | | Сохранить регистровый кон- | | текст программы обработки | | прерывания от диска | Прерывание по таймеру +-------------------------------| ^ | Контекстный уровень ядра 2 | | | Исполнить программу обра- | | | ботки прерывания от диска | | | | | | Сохранить регистровый кон- | | | текст обращения к системной | | | функции | Прерывание от диска +-------------------------------| ^ | Контекстный уровень ядра 1 | | | Исполнить обращение к сис- | | | темной функции | | | | | | Сохранить регистровый кон- | | | текст пользовательского | | | уровня | Вызов системной функции +-------------------------------+ ^ | | Исполнение в режиме задачи Рисунок 6.11. Примеры прерываний особый случай программы обработки прерывания. Библиотечные функции передают ядру уникальный номер системной функции одним из машинно-зависимых способов - либо как параметр внутреннего прерывания операционной системы, либо через отдельный регистр, либо через стек - а ядро таким образом определяет тип вы- зываемой функции. Обрабатывая внутреннее прерывание операционной системы, ядро по номеру системной функции ведет в таблице поиск адреса соответствующей процедуры яд- ра, то есть точки входа системной функции, и количества передаваемых функции параметров (Рисунок 6.12). Ядро вычисляет адрес (пользовательский) первого параметра функции, прибавляя (или вычитая, в зависимости от направления уве- личения стека) смещение к указателю вершины стека задачи (аналогично дл всех параметров функции). Наконец, ядро копирует параметры задачи в прост- ранство процесса и вызывает соответствующую процедуру, которая выполняет системную функцию. После исполнения процедуры ядро выясняет, не было ли ошибки. Если ошибка была, ядро делает соответствующие установки в сохранен- ном регистровом контексте задачи, при этом в регистре PS обычно устанавлива- ется бит переноса, а в нулевой регистр заносится номер ошибки. Если при вы- полнении системной функции не было ошибок, ядро очищает в регистре PS бит переноса и заносит возвращаемые функцией значения в регистры 0 и 1 в сохра- ненном регистровом контексте задачи. Когда ядро возвращается после обработки внутреннего прерывания операционной системы в режим задачи, оно попадает в следующую библиотечную инструкцию после прерывания. Библиотечная функция ин- терпретирует возвращенные ядром значения и передает их программе пользовате- ля. +------------------------------------------------------------+ | алгоритм syscall /* алгоритм запуска системной функции */| | входная информация: номер системной функции | | выходная информация: результат системной функции | | { | | найти запись в таблице системных функций, соответствую-| | щую указанному номеру функции; | | определить количество параметров, передаваемых функции;| | скопировать параметры из адресного пространства задачи | | в пространство процесса; | | сохранить текущий контекст для аварийного завершения | | (см. раздел 6.44); | | запустить в ядре исполняемый код системной функции; | | если (во время выполнения функции произошла ошибка) | | { | | установить номер ошибки в нулевом регистре сохра- | | ненного регистрового контекста задачи; | | включить бит переноса в регистре PS сохраненного | | регистрового контекста задачи; | | } | | в противном случае | | занести возвращаемые функцией значения в регистры 0 | | и 1 в сохраненном регистровом контексте задачи; | | } | +------------------------------------------------------------+ Рисунок 6.12. Алгоритм обращения к системным функциям В качестве примера рассмотрим программу, которая создает файл с разреше- нием чтения и записи в него для всех пользователей (режим доступа 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 устанавливаются начальные значени группы регистров управления памятью, выделенных процессу. Если область ранее не присоединялась к какому-либо процессу, ядро с помощью функции growreg (см. следующий раздел) заводит для области новые таблицы страниц; в против- ном случае используются уже существующие таблицы страниц. Алгоритм завершает работу, возвращая указатель на точку входа в частную таблицу областей про- цесса, соответствующую вновь присоединенной области. Допустим, например, что ядру нужно подключить к процессу по виртуальному адресу 0 существующую (раз- деляемую) область, имеющую размер 7 Кбайт (Рисунок 6.20). Оно выделяет новую +------------------------------------------------------------+ | алгоритм attachreg /* присоединение области к процессу */ | | входная информация: (1) указатель на присоединяемую об- | | ласть (заблокированную) | | (2) процесс, к которому присоединяется| | область | | (3) виртуальный адрес внутри процесса,| | по которому будет присоединена об-| | ласть | | (4) тип области | | выходная информация: точка входа в частную таблицу областей| | процесса | | { | | выделить новую запись в частной таблице областей про- | | цесса; | | проинициализировать значения полей записи: | | установить указатель на присоединяемую область; | | установить тип области; | | установить виртуальный адрес области; | | проверить правильность указания виртуального адреса и | | размера области; | | увеличить значение счетчика ссылок на область; | | увеличить размер процесса с учетом присоединения облас-| | ти; | | записать начальные значения в новую группу аппаратных | | регистров; | | возвратить (точку входа в частную таблицу областей про-| | цесса); | | } | +------------------------------------------------------------+ Рисунок 6.19. Алгоритм присоединения области группу регистров управления памятью и заносит в них адрес таблицы страниц области, виртуальный адрес области в пространстве процесса (0) и размер таб- лицы страниц (9 записей). 6.5.4 Изменение размера области Процесс может расширять или сужать свое виртуальное адресное пространст- во с помощью функции sbrk. Точно так же и стек процесса расширяется автома- тически (то есть для этого процессу не нужно явно обращаться к определенной функции) в соответствии с глубиной вложенности обращений к подпрограммам. Изменение размера области производится внутри ядра по алгоритму growreg (Ри- сунок 6.21). При расширении области ядро проверяет, не будут ли виртуальные адреса расширяемой области пересекаться с адресами какой-нибудь другой об- ласти и не повлечет ли расширение области за собой выход процесса за пределы максимально-допустимого виртуального пространства памяти. Ядро никогда не использует алгоритм growreg для увеличения размера разделяемой области, уже присоединенной к нескольким процессам; поэтому оно не беспокоится о том, не приведет ли увеличение размера области для одного процесса к превыше- нию другим процессом системного ограничения, накладываемого на размер про- цесса. При работе с существующей областью ядро использует алгоритм growreg в двух случаях: выполняя функцию sbrk по отношению к области данных процесса и реализуя автоматическое увеличение стека задачи. Обе эти области (данных и стека) частного типа. Области команд и разделяемой памяти после инициализа- Частная таблица областей процесса +--------------------------------+ | Адрес | Виртуальный | Размер | | таблицы | адрес в про-| и | | страниц | странстве | защита | | | процесса | | +---------+-------------+--------| Точка входа | | 0 | 9 | для области +----+---------------------------+ команд +----+ v +-------------+ | пусто | +-------------| | пусто | +-------------| | 846K | +-------------| | 752K | +-------------| | 341K | +-------------| | 484K | +-------------| | 976K | +-------------| | 342K | +-------------| | 779K | +-------------+ Рисунок 6.20. Пример присоединения существующей области команд ции не могут расширяться. Этот момент будет пояснен в следующей главе. Чтобы разместить расширенную память, ядро выделяет новые таблицы страниц (или расширяет существующие) или отводит дополнительную физическую память в тех системах, где не поддерживается подкачка страниц по обращению. При выде- лении дополнительной физической памяти ядро проверяет ее наличие перед вы- полнением алгоритма growreg; если же памяти больше нет, ядро прибегает к другим средствам увеличения размера области (см. главу 9). Если процесс сок- ращает размер области, ядро просто освобождает память, отведенную под об- ласть. Во всех этих случаях ядро переопределяет размеры процесса и области и переустанавливает значения полей записи частной таблицы областей процесса и регистров управления памятью (так, чтобы они согласовались с новым отображе- нием памяти). Предположим, например, что область стека процесса начинается с виртуаль- ного адреса 128К и имеет размер 6 Кбайт и что ядру нужно расширить эту об- ласть на 1 Кбайт (1 страницу). Если размер процесса позволяет это делать и если виртуальные адреса в диапа- зоне от 134К до 135К - 1 не принадлежат какой-либо области, ранее присоеди- ненной к процессу, ядро увеличивает размер стека. При этом ядро расширяет таблицу страниц, выделяет новую страницу памяти и инициализирует новую за- пись таблицы. Этот случай проиллюстрирован с помощью Рисунка 6.22. 6.5.5 Загрузка области В системе, где поддерживается подкачка страниц по обращению, ядро может +------------------------------------------------------------+ | алгоритм growreg /* изменение размера области */ | | входная информация: (1) указатель на точку входа в частной| | таблице областей процесса | | (2) величина, на которую нужно изме- | | нить размер области (может быть | | как положительной, так и отрица- | | тельной) | | выходная информация: отсутствует | | { | | если (размер области увеличивается) | | { | | проверить допустимость нового размера области; | | выделить вспомогательные таблицы (страниц); | | если (в системе не поддерживается замещение страниц | | по обращению) | | { | | выделить дополнительную память; | | проинициализировать при необходимости значения | | полей в дополнительных таблицах; | | } | | } | | в противном случае /* размер области уменьшается */ | | { | | освободить физическую память; | | освободить вспомогательные таблицы; | | } | | | | провести в случае необходимости инициализацию других | | вспомогательных таблиц; | | переустановить значение поля размера в таблице процес- | | сов; | | } | +------------------------------------------------------------+ Рисунок 6.21. Алгоритм изменения размера области "отображать" файл в адресное пространство процесса во время выполнения функ- ции exec, подготавливая последующее чтение по запросу отдельных физических страниц (см. главу 9). Если же подкачка страниц по обращению не поддержива- ется, ядру приходится копировать исполняемый файл в память, загружая области процесса по указанным в файле виртуальным адресам. Ядро может присоединить область к разным виртуальным адресам, по которым будет загружаться содержи- мое файла, создавая таким образом "разрыв" в таблице страниц (вспомним Рису- нок 6.20). Эта возможность может пригодиться, например, когда требуется про- являть ошибку памяти (memory fault) в случае обращения пользовательских программ к нулевому адресу (если последнее запрещено). Переменные указатели в программах иногда задаются неверно (отсутствует проверка их значений на равенство 0) и в результате не могут использоваться в качестве указателей адресов. Если страницу с нулевым адресом соответствующим образом защитить, процессы, случайно обратившиеся к этому адресу, натолкнутся на ошибку и будут аварийно завершены, и это ускорит обнаружение подобных ошибок в программах. При загрузке файла в область алгоритм loadreg (Рисунок 6.23) проверяет разрыв между виртуальным адресом, по которому область присоединяется к про- цессу, и виртуальным адресом, с которого располагаются данные области, и расширяет область в соответствии с требуемым объемом памяти. Затем область Частная таблица областей Частная таблица областей процесса процесса +-------------------------+ +-------------------------+ | Адрес | Виртуаль-| Раз- | | Адрес | Виртуаль-| Раз- | | табли-| ный адрес| мер | | табли-| ный адрес| мер | | цы | в прост- | и | | цы | в прост- | и | | стра- | ранстве | защи-| | стра- | ранстве | защи-| | ниц | процесса | та | | ниц | процесса | та | +-------+----------+------| +-------+----------+------| | | | | | | | | +-------+----------+------| +-------+----------+------| | | | | | | | | Точка+-------+----------+------| Точка+-------+----------+------| входа| | 128K | 6K | входа| | 128K | 7K | для +---+---------------------+ для +---+---------------------+ стека +--+ стека +--+ v v +-------------+ +-------------+ | 342K | | 342K | +-------------| +-------------| | 779K | | 779K | +-------------| +-------------| | 846K | | 846K | +-------------| +-------------| | 752K | | 752K | +-------------| +-------------| | 341K | | 341K | +-------------| +-------------| | 484K | | 484K | +-------------| НОВАЯ +-------------| | | СТРАНИЦА-->| 976K | +-------------| +-------------| | | | | +-------------| +-------------| | | | | +-------------+ +-------------+ До увеличения стека После увеличения стека Рисунок 6.22. Увеличение области стека на 1 Кбайт переводится в состояние "загрузки в память", при котором данные для области считываются из файла в память с помощью встроенной модификации алгоритма системной функции read. Если ядро загружает область команд, которая может разделяться нескольки- ми процессами, возможна ситуация, когда процесс попытается воспользоватьс областью до того, как ее содержимое будет полностью загружено, так как про- цесс загрузки может приостано- виться во время чтения файла. Подробно о том, как это происходит и почему при этом нельзя использовать блокировки, мы поговорим, когда будем вести речь о функции exec в следующей главе и в главе 9. Чтобы устранить эту проб- лему, ядро проверяет статус области и не разрешает к ней доступ до тех пор, пока загрузка области не будет закончена. По завершении реализации алгоритма loadreg ядро возобновляет выполнение всех процессов, ожидающих окончани загрузки области, и изменяет статус области ("готова, загружена в память"). Предположим, например, что ядру нужно загрузить текст размером 7K в об- ласть, присоединенную к процессу по виртуальному адресу 0, но при этом оста- вить промежуток размером 1 Кбайт от начала области (Рисунок 6.24). К этому +------------------------------------------------------------+ | алгоритм loadreg /* загрузка части файла в область */ | | входная информация: (1) указатель на точку входа в частную| | таблицу областей процесса | | (2) виртуальный адрес загрузки | | (3) указатель индекса файла | | (4) смещение в байтах до начала считы-| | ваемой части файла | | (5) объем загружаемых данных в байтах | | выходная информация: отсутствует | | { | | увеличить размер области до требуемой величины (алгоритм| | growreg); | | записать статус области как "загружаемой в память"; | | снять блокировку с области; | | установить в пространстве процесса значения параметров | | чтения из файла: | | виртуальный адрес, по которому будут размещены счи-| | тываемые данные; | | смещение до начала считываемой части файла; | | объем данных, считываемых из файла, в байтах; | | загрузить файл в область (встроенная модификация алго- | | ритма read); | | заблокировать область; | | записать статус области как "полностью загруженной в па-| | мять"; | | возобновить выполнение всех процессов, ожидающих оконча-| | ния загрузки области; | | } | +------------------------------------------------------------+ Рисунок 6.23. Алгоритм загрузки данных области из файла времени ядро уже выделило запись в таблице областей и присоединило область по адресу 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. Загрузка области команд (текста) +------------------------------------------------------------+ | алгоритм freereg /* освобождение выделенной области */| | входная информация: указатель на (заблокированную) область| | выходная информация: отсутствует | | { | | если (счетчик ссылок на область имеет ненулевое значе- | | ние) | | { | | /* область все еще используется одним из процессов */| | снять блокировку с области; | | если (область ассоциирована с индексом) | | снять блокировку с индекса; | | возвратить управление; | | } | | если (область ассоциирована с индексом) | | освободить индекс (алгоритм iput); | | освободить связанную с областью физическую память; | | освободить связанные с областью вспомогательные таблицы;| | очистить поля области; | | включить область в список свободных областей; | | снять блокировку с области; | | } | +------------------------------------------------------------+ Рисунок 6.25. Алгоритм освобождения области 6.5.6 Освобождение области Если область не присоединена уже ни к какому процессу, она может быть освобождена ядром и возвращена в список свободных областей (Рисунок 6.25). Если область связана с индексом, ядро освобождает и индекс с помощью алго- ритма iput, учитывая значение счетчика ссылок на индекс, установленное в ал- горитме allocreg. Ядро освобождает все связанные с областью физические ре- сурсы, такие как таблицы страниц и собственно страницы физической памяти. Предположим, например, что ядру нужно освободить область стека, описанную на Рисунке 6.22. Если счетчик ссылок на область имеет нулевое значение, ядро освободит 7 страниц физической памяти вместе с таблицей страниц. +------------------------------------------------------------+ | алгоритм 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) процессов. Структуры и алгоритмы, опи- санные в данной главе, будут использоваться в последующих главах при расс- мотрении системных функций управления процессами и планирования их выполне- ния, а также при объяснении различных методов распределения памяти. 6.8 УПРАЖНЕНИЯ 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 для того, чтобы можно было возобновлять выполнение только одного процесса, а не всех процессов, приостановленных по заданному адресу. ГЛАВА 7 УПРАВЛЕНИЕ ПРОЦЕССОМ В предыдущей главе был рассмотрен контекст процесса и описаны алгоритмы для работы с ним; в данной главе речь пойдет об использовании и реализации системных функций, управляющих контекстом процесса. Системная функция 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 в системах с замещением страниц. На Рисунке 7.2 приведен алгоритм создания процесса. Сначала ядро должно удостовериться в том, что для успешного выполнения алгоритма fork есть все необходимые ресурсы. В системе с подкачкой процессов для размещения порожда- емого процесса требуется место либо в памяти, либо на диске; в системе с за- мещением страниц следует выделить память для вспомогательных таблиц (в част- ности, таблиц страниц). Если свободных ресурсов нет, алгоритм fork заверша- ется неудачно. Ядро ищет место в таблице процессов для конструирования кон- текста порождаемого процесса и проверяет, не превысил ли пользователь, вы- полняющий fork, ограничение на максимально-допустимое количество параллельно запущенных процессов. Ядро также подбирает для нового процесса уникальный идентификатор, значение которого превышает на единицу максимальный из сущес- твующих идентификаторов. Если предлагаемый идентификатор уже присвоен друго- му процессу, ядро берет идентификатор, следующий по порядку. Как только бу- дет достигнуто максимально-допустимое значение, отсчет идентификаторов опять начнется с 0. Поскольку большинство процессов имеет короткое время жизни, при переходе к началу отсчета значительная часть идентификаторов оказываетс свободной. На количество одновременно выполняющихся процессов накладывается ограни- чение (конфигурируемое), отсюда ни один из пользователей не может занимать в таблице процессов слишком много места, мешая тем самым другим пользователям создавать новые процессы. Кроме того, простым пользователям не разрешаетс создавать процесс, занимающий последнее свободное место в таблице процессов, в противном случае система зашла бы в тупик. Другими словами, поскольку в таблице процессов нет свободного места, то ядро не может гарантировать, что все существующие процессы завершатся естественным образом, поэтому новые +------------------------------------------------------------+ | алгоритм fork | | входная информация: отсутствует | | выходная информация: для родительского процесса - идентифи-| | катор (PID) порожденного процесса | | для порожденного процесса - 0 | | { | | проверить доступность ресурсов ядра; | | получить свободное место в таблице процессов и уникаль- | | ный код идентификации (PID); | | проверить, не запустил ли пользователь слишком много | | процессов; | | сделать пометку о том, что порождаемый процесс находится| | в состоянии "создания"; | | скопировать информацию в таблице процессов из записи, | | соответствующей родительскому процессу, в запись, соот-| | ветствующую порожденному процессу; | | увеличить значения счетчиков ссылок на текущий каталог и| | на корневой каталог (если он был изменен); | | увеличить значение счетчика открытий файла в таблице | | файлов; | | сделать копию контекста родительского процесса (адресное| | пространство, команды, данные, стек) в памяти; | | поместить в стек фиктивный уровень системного контекста | | над уровнем системного контекста, соответствующим по- | | рожденному процессу; | | фиктивный контекстный уровень содержит информацию, | | необходимую порожденному процессу для того, чтобы | | знать все о себе и будучи выбранным для исполнения | | запускаться с этого места; | | если (в данный момент выполняется родительский процесс) | | { | | перевести порожденный процесс в состояние "готовности| | к выполнению"; | | возвратить (идентификатор порожденного процесса); | | /* из системы пользователю */ | | } | | в противном случае /* выполняется порожденный | | процесс */ | | { | | записать начальные значения в поля синхронизации ад- | | ресного пространства процесса; | | возвратить (0); /* пользователю */ | | } | | } | +------------------------------------------------------------+ Рисунок 7.2. Алгоритм fork процессы создаваться не будут. С другой стороны, суперпользователю нужно дать возможность исполнять столько процессов, сколько ему потребуется, ко- нечно, учитывая размер таблицы процессов, при этом процесс, исполняемый су- перпользователем, может занять в таблице и последнее свободное место. Пред- полагается, что суперпользователь может прибегать к решительным мерам и за- пускать процесс, побуждающий остальные процессы к завершению, если это вызы- вается необходимостью (см. раздел 7.2.3, где говорится о системной функции kill). Затем ядро присваивает начальные значения различным полям записи таблицы процессов, соответствующей порожденному процессу, копируя в них значения по- лей из записи родительского процесса. Например, порожденный процесс "насле- дует" у родительского процесса коды идентификации пользователя (реальный и тот, под которым исполняется процесс), группу процессов, управляемую роди- тельским процессом, а также значение, заданное родительским процессом в фун- кции nice и используемое при вычислении приоритета планирования. В следующих разделах мы поговорим о назначении этих полей. Ядро передает значение пол идентификатора родительского процесса в запись порожденного, включая послед- ний в древовидную структуру процессов, и присваивает начальные значения раз- личным параметрам планирования, таким как приоритет планирования, использо- вание ресурсов центрального процессора и другие значения полей синхрониза- ции. Начальным состоянием процесса является состояние "создания" (см. Рису- нок 6.1). После того ядро устанавливает значения счетчиков ссылок на файлы, с ко- торыми автоматически связывается порождаемый процесс. Во-первых, порожденный процесс размещается в текущем каталоге родительского процесса. Число процес- сов, обращающихся в данный момент к каталогу, увеличивается на 1 и, соответ- ственно, увеличивается значение счетчика ссылок на его индекс. Во-вторых, если родительский процесс или один из его предков уже выполнял смену корне- вого каталога с помощью функции chroot, порожденный процесс наследует и но- вый корень с соответствующим увеличением значения счетчика ссылок на индекс корня. Наконец, ядро просматривает таблицу пользовательских дескрипторов дл родительского процесса в поисках открытых файлов, известных процессу, и уве- личивает значение счетчика ссылок, ассоциированного с каждым из открытых файлов, в глобальной таблице файлов. Порожденный процесс не просто наследует права доступа к открытым файлам, но и разделяет доступ к файлам с родитель- ским процессом, так как оба процесса обращаются в таблице файлов к одним и тем же записям. Действие fork в отношении открытых файлов подобно действию алгоритма dup: новая запись в таблице пользовательских дескрипторов файла указывает на запись в глобальной таблице файлов, соответствующую открытому файлу. Для dup, однако, записи в таблице пользовательских дескрипторов файла относятся к одному процессу; для fork - к разным процессам. После завершения всех этих действий ядро готово к созданию для порожден- ного процесса пользовательского контекста. Ядро выделяет память для адресно- го пространства процесса, его областей и таблиц страниц, создает с помощью алгоритма dupreg копии всех областей родительского процесса и присоединяет с помощью алгоритма attachreg каждую область к порожденному процессу. В систе- ме с подкачкой процессов ядро копирует содержимое областей, не являющихс областями разделяемой памяти, в новую зону оперативной памяти. Вспомним из раздела 6.2.4 о том, что в пространстве процесса хранится указатель на соот- ветствующую запись в таблице процессов. За исключением этого поля, во всем остальном содержимое адресного пространства порожденного процесса в начале совпадает с содержимым пространства родительского процесса, но может расхо- диться после завершения алгоритма fork. Родительский процесс, например, пос- ле выполнения fork может открыть новый файл, к которому порожденный процесс уже не получит доступ автоматически. Итак, ядро завершило создание статической части контекста порожденного процесса; теперь оно приступает к созданию динамической части. Ядро копирует в нее первый контекстный уровень родительского процесса, включающий в себ сохраненный регистровый контекст задачи и стек ядра в момент вызова функции fork. Если в данной реализации стек ядра является частью пространства про- цесса, ядро в момент создания пространства порожденного процесса автомати- чески создает и системный стек для него. В противном случае родительскому процессу придется скопировать в пространство памяти, ассоциированное с по- рожденным процессом, свой системный стек. В любом случае стек ядра для по- рожденного процесса совпадает с системным стеком его родителя. Далее ядро создает для порожденного процесса фиктивный контекстный уровень (2), в кото- ром содержится сохраненный регистровый контекст из первого контекстного уровня. Значения счетчика команд (регистр PC) и других регистров, сохраняе- мые в регистровом контексте, устанавливаются таким образом, чтобы с их по- мощью можно было "восстанавливать" контекст порожденного процесса, пусть да- же последний еще ни разу не исполнялся, и чтобы этот процесс при запуске всегда помнил о том, что он порожденный. Например, если программа ядра про- веряет значение, хранящееся в регистре 0, для того, чтобы выяснить, являетс ли данный процесс родительским или же порожденным, то это значение переписы- вается в регистровый контекст порожденного процесса, сохраненный в составе первого уровня. Механизм сохранения используется тот же, что и при переклю- чении контекста (см. предыдущую главу). Родительский процесс +---------------------------------------------+ Таблица | +---------+ Частная Адресное простран- | файлов | | Область | таблица ство процесса | +---------+ | | данных | областей +------------------+| | | | +---------+ процесса | Открытые файлы - || - + | | | | +------+ | || | | | - - - + | + +| Текущий каталог -||+ | +---------| | +------| | || - -| | | +---------+ + | | || Измененный корень||| | | | | | Стек | +------| +------------------+| +---------| | | задачи + - | | |+------------------+|| | | | | +---------+ +------+ | || | | | || ||| | | | | | || +---------| | - - - - - - - - -+| Стек ядра ||| + - | | | | +------------------+| | | +---------------------------------------------+| | +---------| | | +---------+ | | | | |Разделяе-| | | | мая | | | +---------| | область | - -| | | команд | | | | | +---------+ +---------| | | +---------+ + - - - - - - - - + +---------------------------------------------++ -|+ Таблица | +---------+ Частная | Адресное простран- | файлов | | Область | таблица ство процесса | || +---------+ | | данных | областей |+------------------+| | | | +---------+ процесса | Открытые файлы - || - +| | | | | +------+ || || | | | - - - + | +- | Текущий каталог -||+ | +---------| | +------| | || - | | | +---------+ + | | | Измененный корень||+ - - -| | | | Стек | +------| +------------------+| +---------| | | задачи + - | | +------------------+| | | | +---------+ +------+ | || | | | | || +---------| | | Стек ядра || | | | +------------------+| | | +---------------------------------------------+ +---------| Порожденный процесс | | | | +---------+ Рисунок 7.3. Создание контекста нового процесса при выполне- нии функции fork Если контекст порожденного процесса готов, родительский процесс заверша- ет свою роль в выполнении алгоритма fork, переводя порожденный процесс в состояние "готовности к запуску, находясь в памяти" и возвращая пользователю его идентификатор. Затем, используя обычный алгоритм планирования, ядро вы- бирает порожденный процесс для исполнения и тот "доигрывает" свою роль в ал- горитме fork. Контекст порожденного процесса был задан родительским процес- сом; с точки зрения ядра кажется, что порожденный процесс возобновляетс после приостанова в ожидании ресурса. Порожденный процесс при выполнении функции fork реализует ту часть программы, на которую указывает счетчик ко- манд, восстанавливаемый ядром из сохраненного на уровне 2 регистрового кон- текста, и по выходе из функции возвращает нулевое значение. На Рисунке 7.3 представлена логическая схема взаимодействия родительско- го и порожденного процессов с другими структурами данных ядра сразу после завершения системной функции fork. Итак, оба процесса совместно пользуютс файлами, которые были открыты родительским процессом к моменту исполнени функции fork, при этом значение счетчика ссылок на каждый из этих файлов в таблице файлов на единицу больше, чем до вызова функции. Порожденный процесс имеет те же, что и родительский процесс, текущий и корневой каталоги, значе- ние же счетчика ссылок на индекс каждого из этих каталогов так же становитс на единицу больше, чем до вызова функции. Содержимое областей команд, данных и стека (задачи) у обоих процессов совпадает; по типу области и версии сис- темной реализации можно установить, могут ли процессы разделять саму область команд в физических адресах. Рассмотрим приведенную на Рисунке 7.4 программу, которая представляет собой пример разделения доступа к файлу при исполнении функции fork. Пользо- вателю следует передавать этой программе два параметра - имя существующего файла и имя создаваемого файла. Процесс открывает существующий файл, создает новый файл и - при условии отсутстви ошибок - порождает новый процесс. Внутри программы ядро делает копию контек- ста родительского процесса для порожденного, при этом родительский процесс исполняется в одном адресном пространстве, а порожденный - в другом. Каждый из процессов может работать со своими собственными копиями глобальных пере- менных fdrd, fdwt и c, а также со своими собственными копиями стековых пере- менных argc и argv, но ни один из них не может обращаться к переменным дру- гого процесса. Тем не менее, при выполнении функции fork ядро делает копию адресного пространства первого процесса для второго, и порожденный процесс, таким образом, наследует доступ к файлам родительского (то есть к файлам, им ранее открытым и созданным) с правом использования тех же самых деск- рипторов. Родительский и порожденный процессы независимо друг от друга, конечно, вызывают функцию rdwrt и в цикле считывают по одному байту информацию из ис- ходного файла и переписывают ее в файл вывода. Функция rdwrt возвращает уп- равление, когда при считывании обнаруживается конец файла. Ядро перед тем уже увеличило значения счетчиков ссылок на исходный и результирующий файлы в таблице файлов, и дескрипторы, используемые в обоих процессах, адресуют к одним и тем же строкам в таблице. Таким образом, дескрипторы fdrd в том и в другом процессах указывают на запись в таблице файлов, соответствующую ис- ходному файлу, а дескрипторы, подставляемые в качестве fdwt, - на запись, соответствующую результирующему файлу (файлу вывода). Поэтому оба процесса никогда не обратятся вместе на чтение или запись к одному и тому же адресу, вычисляемому с помощью смещения внутри файла, поскольку ядро смещает внутри- файловые указатели после каждой операции чтения или записи. Несмотря на то, что, казалось бы, из-за того, что процессы распределяют между собой рабочую нагрузку, они копируют исходный файл в два раза быстрее, содержимое резуль- тирующего файла зависит от очередности, в которой ядро запускает процессы. Если ядро запускает процессы так, что они исполняют системные функции попе- ременно (чередуя и спаренные вызовы функций read-write), содержимое резуль- +------------------------------------------------------------+ | #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. Программа, в которой родительский и порожденный процессы разделяют доступ к файлу тирующего файла будет совпадать с содержимым исходного файла. Рассмотрим, однако, случай, когда процессы собираются считать из исходного файла после- довательность из двух символов "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. И тот, и другой процессы закрывают файлы, дескрипторы +------------------------------------------------------------+ | #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 которых возвратила функция pipe - хорошая традиция, в чем нам еще предстоит убедиться. В результате, когда родительский процесс переписывает данные в стандартный вывод, запись ведется в канал to_chil и данные поступают к по- рожденному процессу, который считывает их через свой стандартный ввод. Когда же порожденный процесс пишет данные в стандартный вывод, запись ведется в канал to_par и данные поступают к родительскому процессу, считывающему их через свой стандартный ввод. Так через два канала оба процесса обмениваютс сообщениями. Результаты этой программы не зависят от того, в какой очередности про- цессы выполняют свои действия. Таким образом, нет никакой разницы, возвраща- ется ли управление родительскому процессу из функции fork раньше или позже, чем порожденному процессу. И так же безразличен порядок, в котором процессы вызывают системные функции перед тем, как войти в свой собственный цикл, ибо они используют идентичные структуры ядра. Если процесс-потомок исполняет функцию read раньше, чем его родитель выполнит write, он будет приостановлен до тех пор, пока родительский процесс не произведет запись в канал и тем са- мым не возобновит выполнение потомка. Если родительский процесс записывает в канал до того, как его потомок приступит к чтению из канала, первый процесс не сможет в свою очередь считать данные из стандартного ввода, пока второй процесс не прочитает все из своего стандартного ввода и не произведет запись данных в стандартный вывод. С этого места порядок работы жестко фиксирован: каждый процесс завершает выполнение функций read и write и не может выпол- нить следующую операцию read до тех пор, пока другой процесс не выполнит па- ру read-write. Родитель- ский процесс после 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 представлен алгоритм, с помощью которого ядро определяет, --------------------------------------- (*) Использование сигналов в некоторых обстоятельствах позволяет обнаружить ошибки при выполнении программ, не проверяющих код завершения вызываемых системных функций (сообщил Д.Ричи). Выполняется в режиме задачи +-------+ | | Проверка | 1 | и Вызов функ- | | + - обработка ции, преры- +-------+ + - сигналов вание | ^ ^- -+ Преры- +-----+ +-------+ |- -|- - + вание, | | | +-------+ +---+ Возврат в возврат| | | | Возврат | режим задачи из пре-| | | | | рыва-| v v | Выполняет- | +-------+ ния | +-------+ся в режи- +-------+ | | +-->| |ме ядра | | | 9 |<-----------| 2 +------------>| 7 | | | Выход | | Резервирует-| | +-------+ +-------+ ся +-------+ Прекращение | ^ Зарезер- существования |- - -|- - - - - - - - + вирован | |- - - - - - - + + - -+ +---------------+ +------+ Проверка | Приостанов Запуск | + - - - сигналов v | При-+-------+ +-------+ Готов к ос- | | Возобновление | | запуску та- | 4 +----------------------->| 3 | в памяти нов-| | | | лен +-------+ +-------+ в па- | | ^ ^ мяти | | | | Достаточно | | | | памяти | | | +---+ | Вы- Вы- | | | | грузка грузка | | | Создан | | |За- +-------+ | | |груз-| | fork | | |ка | 8 |<----- | | | | | | | | +-------+ | | | | | | | | Недоста- | | | +---+ точно | | | | памяти | | | | (только система | | | | подкачки) v v | v +-------+ +-------+ | | Возобновление | | | 6 +----------------------->| 5 | | | | | +-------+ +-------+ Приостановлен, Готов к запуску, выгружен выгружен Рисунок 7.6. Диаграмма переходов процесса из состояние в состояние с указанием моментов проверки и обработки сигналов получил ли процесс сигнал или нет. Условия, в которых формируются сигналы типа "гибель потомка", будут рассмотрены позже. Мы также увидим, что процесс может игнорировать отдельные сигналы, если воспользуется функцией signal. В алгоритме issig ядро просто гасит индикацию тех сигналов, на которые процесс не желает обращать внимание, и привлекает внимание процесса ко всем осталь- ным сигналам. +------------------------------------------------------------+ | алгоритм 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 на этом моменте мы остановимся более подробно. 7.2.2 Группы процессов Несмотря на то, что в системе 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 за- вершается неудачно. В программе, приведенной на Рисунке 7.13, главный процесс сбрасывает ус- тановленное ранее значение номера группы и порождает 10 новых процессов. При рождении каждый процесс-потомок наследует номер группы процессов своего ро- дителя, однако, процессы, созданные в нечетных итерациях цикла, сбрасывают это значение. Системные функции getpid и getpgrp возвращают значения кода идентификации выполняемого процесса и номера группы, в которую он входит, а функция pause приостанавливает выполнение процесса до момента получения сиг- нала. В конечном итоге родительский процесс запускает функцию 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.3 ЗАВЕРШЕНИЕ ВЫПОЛНЕНИЯ ПРОЦЕССА В системе UNIX процесс завершает свое выполнение, запуская системную функцию exit. После этого процесс переходит в состояние "прекращения сущест- вования" (см. Рисунок 6.1), освобождает ресурсы и ликвидирует свой контекст. Синтаксис вызова функции: exit(status); где status - значение, возвращаемое функцией родительскому процессу. Процес- сы могут вызывать функцию exit как в явном, так и в неявном виде (по оконча- нии выполнения программы: начальная процедура (startup), компонуемая со все- ми программами на языке Си, вызывает функцию exit на выходе программы из функции main, являющейся общей точкой входа для всех программ). С другой стороны, ядро может вызывать функцию exit по своей инициативе, если процесс не принял посланный ему сигнал (об этом мы уже говорили выше). В этом случае значение параметра status равно номеру сигнала. Система не накладывает никакого ограничения на продолжительность выпол- нения процесса, и зачастую процессы существуют в течение довольно длительно- го времени. Нулевой процесс (программа подкачки) и процесс 1 (init), к при- меру, существуют на протяжении всего времени жизни системы. Продолжительными процессами являются также getty-процессы, контролирующие работу терминальной линии, ожидая регистрации пользователей, и процессы общего назначения, вы- полняемые под руководством администратора. На Рисунке 7.14 приведен алгоритм функции exit. Сначала ядро отменяет обработку всех сигналов, посылаемых процессу, поскольку ее продолжение ста- новится бессмысленным. Если процесс, вызывающий функцию exit, возглавляет +------------------------------------------------------------+ | алгоритм exit | | входная информация: код, возвращаемый родительскому про- | | цессу | | выходная информация: отсутствует | | { | | игнорировать все сигналы; | | если (процесс возглавляет группу процессов, ассоцииро- | | ванную с операторским терминалом) | | { | | послать всем процессам, входящим в группу, сигнал о | | "зависании"; | | сбросить в ноль код группы процессов; | | } | | закрыть все открытые файлы (внутренняя модификация алго-| | ритма close); | | освободить текущий каталог (алгоритм iput); | | освободить области и память, ассоциированную с процессом| | (алгоритм freereg); | | создать запись с учетной информацией; | | прекратить существование процесса (перевести его в соот-| | ветствующее состояние); | | назначить всем процессам-потомкам в качестве родителя | | процесс init (1); | | если кто-либо из потомков прекратил существование, | | послать процессу init сигнал "гибель потомка"; | | послать сигнал "гибель потомка" родителю данного процес-| | са; | | переключить контекст; | | } | +------------------------------------------------------------+ Рисунок 7.14. Алгоритм функции 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) , +------------------------------------------------------------+ | алгоритм exec | | входная информация: (1) имя файла | | (2) список параметров | | (3) список переменных среды | | выходная информация: отсутствует | | { | | получить индекс файла (алгоритм namei); | | проверить, является ли файл исполнимым и имеет ли поль- | | зователь право на его исполнение; | | прочитать информацию из заголовков файла и проверить, | | является ли он загрузочным модулем; | | скопировать параметры, переданные функции, из старого | | адресного пространства в системное пространство; | | для (каждой области, присоединенной к процессу) | | отсоединить все старые области (алгоритм detachreg);| | для (каждой области, определенной в загрузочном модуле) | | { | | выделить новые области (алгоритм allocreg); | | присоединить области (алгоритм attachreg); | | загрузить область в память по готовности (алгоритм | | loadreg); | | } | | скопировать параметры, переданные функции, в новую об- | | ласть стека задачи; | | специальная обработка для setuid-программ, трассировка; | | проинициализировать область сохранения регистров задачи | | (в рамках подготовки к возвращению в режим задачи); | | освободить индекс файла (алгоритм iput); | | } | +------------------------------------------------------------+ Рисунок 7.19. Алгоритм функции exec массив argv является копией одноименного параметра, передаваемого функции 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. Вернемся к алгоритму. Мы остановились на том, что ядро обратилось к ин- дексу файла и установило, что файл является исполнимым. Ядру следовало бы освободить память, занимаемую пользовательским контекстом процесса. Однако, поскольку в памяти, подлежащей освобождению, располагаются передаваемые но- вой программе параметры, ядро первым делом копирует их из адресного прост- ранства в промежуточный буфер на время, пока не будут отведены области дл нового пространства памяти. Поскольку параметрами функции exec выступают пользовательские адреса массивов символьных строк, ядро по каждой строке сначала копирует в систем- ную память адрес строки, а затем саму строку. Для хранения строки в разных версиях системы могут быть выбраны различные места. Чаще принято хранить строки в стеке ядра (локальная структура данных, принадлежащая программе яд- ра), на нераспределяемых участках памяти (таких как страницы), которые можно занимать только временно, а также во внешней памяти (на устройстве выгруз- ки). С точки зрения реализации проще всего для копирования параметров в новый пользовательский контекст обратиться к стеку ядра. Однако, поскольку размер стека ядра, как правило, ограничивается системой, а также поскольку парамет- ры функции exec могут иметь произвольную длину, этот подход следует сочетать с другими подходами. При рассмотрении других вариантов обычно останавливают- ся на способе хранения, обеспечивающем наиболее быстрый доступ к строкам. Если доступ к страницам памяти в системе реализуется довольно просто, строки следует размещать на страницах, поскольку обращение к оперативной памяти осуществляется быстрее, чем к внешней (устройству выгрузки). После копирования параметров функции exec в системную память ядро отсое- диняет области, ранее присоединенные к процессу, используя алгоритм detachreg. Несколько позже мы еще поговорим о специальных действиях, выпол- няемых в отношении областей команд. К рассматриваемому моменту процесс уже лишен пользовательского контекста и поэтому возникновение в дальнейшем любой ошибки неизбежно будет приводить к завершению процесса по сигналу. Такими ошибками могут быть обращение к пространству, не описанному в таблице облас- тей ядра, попытка загрузить программу, имеющую недопустимо большой размер или использующую области с пересекающимися адресами, и др. Ядро выделяет и присоединяет к процессу области команд и данных, загружает в оперативную па- мять содержимое исполняемого файла (алгоритмы allocreg, attachreg и loadreg, соответственно). Область данных процесса изначально поделена на две части: --------------------------------------- (**) В PDP 11 "магические числа" имеют значения, соответствующие командам перехода; при выполнении этих команд в ранних версиях системы управле- ние передавалось в разные места программы в зависимости от размера за- головка и от типа исполняемого файла. Эта особенность больше не исполь- зуется с тех пор, как система стала разрабатываться на языке Си. данные, инициализация которых была выполнена во время компиляции, и данные, не определенные компилятором ("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", в 207 --------------------------------------- (***) Например, в версии V стандартные программы переименования файла (mv), копирования файла (cp) и компоновки файла (ln), поскольку исполняют похожие действия, вызывают один и тот же исполняемый файл. По имени вызываемой программы процесс узнает, какие действия в настоящий момент требуются пользователю. область данных - секция данных файла. Ядро восстанавливает первоначальный список параметров (в данном случае это строка символов "date") и помещает его в область стека. Вызвав функцию exec, процесс-потомок прекращает выпол- нение старой программы и переходит к выполнению программы "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. 7.6 КОД ИДЕНТИФИКАЦИИ ПОЛЬЗОВАТЕЛЯ ПРОЦЕССА Ядро связывает с процессом два кода идентификации пользователя, не зави- сящих от кода идентификации процесса: реальный (действительный) код иденти- фикации пользователя и исполнительный код или 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 и т.п.), он исполняет команду своими силами, не прибегая к созданию новых процессов; в противном случае команда интерпретируется как имя испол- няемого файла. Командные строки простейшего вида содержат имя программы и несколько па- раметров, например: who grep -n include *.c ls -l +------------------------------------------------------------+ | 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 (продолжение) 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 прямо из ядра, поскольку сам процесс исполняется в режиме яд- +------------------------------------------------------------+ | алгоритм start /* процедура начальной загрузки системы */| | входная информация: отсутствует | | выходная информация: отсутствует | | { | | проинициализировать все структуры данных ядра; | | псевдо-монтирование корня; | | сформировать среду выполнения процесса 0; | | создать процесс 1; | | { | | /* процесс 1 */ | | выделить область; | | подключить область к адресному пространству процесса| | init; | | увеличить размер области для копирования в нее ис- | | полняемого кода; | | скопировать из пространства ядра в адресное прост- | | ранство процесса код программы, исполняемой процес-| | сом; | | изменить режим выполнения: вернуться из режима ядра | | в режим задачи; | | /* процесс init далее выполняется самостоятельно -- | | * в результате выхода в режим задачи, | | * init исполняет файл "/etc/init" и становится | | * "обычным" пользовательским процессом, производя- | | * щим обращения к системным функциям | | */ | | } | | /* продолжение нулевого процесса */ | | породить процессы ядра; | | /* нулевой процесс запускает программу подкачки, управ- | | * ляющую распределением адресного пространства процес- | | * сов между основной памятью и устройствами выгрузки. | | * Это бесконечный цикл; нулевой процесс обычно приоста-| | * навливает свою работу, если необходимости в нем боль-| | * ше нет. | | */ | | исполнить программу, реализующую алгоритм подкачки; | | } | +------------------------------------------------------------+ Рисунок 7.30. Алгоритм загрузки системы ра. Порожденный нулевым новый процесс, процесс 1, запускается в том же режи- ме и создает свой пользовательский контекст, формируя область данных и при- соединяя ее к своему адресному пространству. Он увеличивает размер области до надлежащей величины и переписывает программу загрузки из адресного прост- ранства ядра в новую область: эта программа теперь будет определять контекст процесса 1. Затем процесс 1 сохраняет регистровый контекст задачи, "возвра- щается" из режима ядра в режим задачи и исполняет только что переписанную программу. В отличие от нулевого процесса, который является процессом сис- темного уровня, выполняющимся в режиме ядра, процесс 1 относится к пользова- тельскому уровню. Код, исполняемый процессом 1, включает в себя вызов сис- темной функции exec, запускающей на выполнение программу из файла "/etc/init". Обычно процесс 1 именуется процессом init, поскольку он отвеча- ет за инициализацию новых процессов. Казалось бы, зачем ядру копировать программу, запускаемую с помощью фун- кции exec, в адресное пространство процесса 1 ? Он мог бы обратиться к внут- реннему варианту функции прямо из ядра, одна- ко, по сравнению с уже описанным алгоритмом это было бы гораздо труднее реа- лизовать, ибо в этом случае функции exec пришлось бы производить анализ имен файлов в пространстве ядра, а не в пространстве задачи. Подобная деталь, требующаяся только для процесса init, усложнила бы программу реализации фун- кции exec и отрицательно отразилась бы на скорости выполнения функции в бо- лее общих случаях. Процесс init (Рисунок 7.31) выступает диспетчером процессов, который по- рождает процессы, среди всего прочего позволяющие пользователю регистриро- ваться в системе. Инструкции о том, какие процессы нужно создать, считывают- ся процессом init из файла "/etc/inittab". Строки файла включают в себ идентификатор состояния "id" (однопользовательский режим, многопользователь- ский и т. д.), предпринимаемое действие (см. упражнение 7.43) и спецификацию программы, реализующей это действие (см. Рисунок 7.32). Процесс init прос- матривает строки файла до тех пор, пока не обнаружит идентификатор состоя- ния, соответствующего тому состоянию, в котором находится процесс, и создает процесс, исполняющий программу с указанной спецификацией. Например, при за- пуске в многопользовательском режиме (состояние 2) процесс init обычно по- рождает getty-процессы, управляющие функционированием терминальных линий, входящих в состав системы. Если регистрация пользователя прошла успешно, getty-процесс, пройдя через процедуру login, запускает на исполнение регист- рационный shell (см. главу 10). Тем временем процесс init находится в состо- янии ожидания (wait), наблюдая за прекращением существования своих потомков, а также "внучатых" процессов, оставшихся "сиротами" после гибели своих роди- телей. Процессы в системе UNIX могут быть либо пользовательскими, либо управля- ющими, либо системными. Большинство из них составляют пользовательские про- цессы, связанные с пользователями через терминалы. Управляющие процессы не связаны с конкретными пользователями, они выполняют широкий спектр системных функций, таких как администрирование и управление сетями, различные периоди- ческие операции, буферизация данных для вывода на устройство построчной пе- чати и т.д. Процесс init может порождать управляющие процессы, которые будут существовать на протяжении всего времени жизни системы, в различных случаях они могут быть созданы самими пользователями. Они похожи на пользовательские процессы тем, что они исполняются в режиме задачи и прибегают к услугам сис- темы путем вызова соответствующих системных функций. Системные процессы выполняются исключительно в режиме ядра. Они могут порождаться нулевым процессом (например, процесс замещения страниц vhand), который затем становится процессом подкачки. Системные процессы похожи на управляющие процессы тем, что они выполняют системные функции, при этом они обладают большими возможностями приоритетного выполнения, поскольку лежащие в их основе программные коды являются составной частью ядра. Они могут обра- щаться к структурам данных и алгоритмам ядра, не прибегая к вызову системных функций, отсюда вытекает их исключительность. Однако, они не обладают такой +------------------------------------------------------------+ | алгоритм 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 же гибкостью, как управляющие процессы, поскольку для того, чтобы внести из- менения в их программы, придется еще раз перекомпилировать ядро. 7.10 ВЫВОДЫ В данной главе были рассмотрены системные функции, предназначенные дл работы с контекстом процесса и для управления выполнением процесса. Систем- ная функция fork создает новый процесс, копируя для него содержимое всех об- ластей, подключенных к родительскому процессу. Особенность реализации функ- ции fork состоит в том, что она выполняет инициализацию сохраненного регист- рового контекста порожденного процесса, таким образом этот процесс начинает выполняться, не дожидаясь завершения функции, и уже в теле функции начинает осознавать свою предназначение как потомка. Все процессы завершают свое вы- полнение вызовом функции exit, которая отсоединяет области процесса и посы- лает его родителю сигнал "гибель потомка". Процесс-родитель может совместить момент продолжения своего выполнения с моментом завершения процесса-потомка, используя системную функцию wait. Системная функция exec дает процессу воз- можность запускать на выполнение другие программы, накладывая содержимое ис- полняемого файла на свое адресное пространство. Ядро отсоединяет области, ранее занимаемые процессом, и назначает процессу новые области в соответст- вии с потребностями исполняемого файла. Совместное использование областей команд и наличие режима "sticky-bit" дают возможность более рационально ис- пользовать память и экономить время, затрачиваемое на подготовку к запуску программ. Простым пользователям предоставляется возможность получать приви- легии других пользователей, даже суперпользователя, благодаря обращению к услугам системной функции setuid и setuid-программ. С помощью функции brk процесс может изменять размер своей области данных. Функция signal дает про- цессам возможность управлять своей реакцией на поступающие сигналы. При по- лучении сигнала производится обращение к специальной функции обработки сиг- нала с внесением соответствующих изменений в стек задачи и в сохраненный ре- гистровый контекст задачи. Процессы могут сами посылать сигналы, использу системную функцию kill, они могут также контролировать получение сигналов, предназначенных группе процессов, прибегая к услугам функции setpgrp. Командный процессор shell и процесс начальной загрузки init используют стандартные обращения к системным функциям, производя набор операций, в дру- гих системах обычно выполняемых ядром. Shell интерпретирует команды пользо- вателя, переназначает стандартные файлы ввода-вывода данных и выдачи ошибок, порождает процессы, организует каналы между порожденными процессами, синхро- низирует свое выполнение с этими процессами и формирует коды, возвращаемые командами. Процесс init тоже порождает различные процессы, в частности, уп- равляющие работой пользователя за терминалом. Когда такой процесс завершает- ся, init может породить для выполнения той же самой функции еще один про- цесс, если это вытекает из информации файла "/etc/inittab". 7.11 УПРАЖНЕНИЯ 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" - имя исполняемого файла: +-----------------------------------------------------+ | 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 chmod 4777 a.out chown root a.out Команда chmod "включает" бит setuid (4 в 4777); пользователь "root" традиционно является суперпользователем. Может ли в результате выполне- ния этой последовательности произойти нарушение защиты информации ? 27. Что произойдет в процессе выполнения программы, представленной на Ри- сунке 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" (а в случае отсутствия файла с таким именем создает его) и записывает в него данные. Напишите прог- рамму, в которой используется эта команда. 32. Процессор командного языка shell проверяет код, возвращаемый функцией exit, воспринимая нулевое значение как "истину", а любое другое значе- ние как "ложь" (обратите внимание на несогласованность с языком Си). Предположим, что файл, исполняющий программу на Рисунке 7.40, имеет им "truth". Поясните, что произойдет, когда shell будет исполнять следую- щий набор команд: while truth +------------------+ | main() | | { | | exit(0); | | } | +------------------+ Рисунок 7.40 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. Некоторые из алгоритмов ядра прибегают к просмотру таблицы процессов. Время поиска данных можно сократить, если использовать указатели на: родителя процесса, любого из потомков, другой процесс, имеющий того же родителя. Процесс обнаруживает всех своих потомков, следуя сначала за указателем на любого из потомков, а затем используя указатели на другие процессы, имеющие того же родителя (циклы недопустимы). Какие из алго- ритмов выиграют от этого ? Какие из алгоритмов нужно оставить без изме- ГЛАВА 8 ДИСПЕТЧЕРИЗАЦИЯ ПРОЦЕССОВ И ЕЕ ВРЕМЕННЫЕ ХАРАКТЕРИСТИКИ В системе разделения времени ядро предоставляет процессу ресурсы цент- рального процессора (ЦП) на интервал времени, называемый квантом, по истече- нии которого выгружает этот процесс и запускает другой, периодически переу- порядочивая очередь процессов. Алгоритм планирования процессов в системе UNIX использует время выполнения в качестве параметра. Каждый активный про- цесс имеет приоритет планирования; ядро переключает контекст на процесс с наивысшим приоритетом. При переходе выполняющегося процесса из режима ядра в режим задачи ядро пересчитывает его приоритет, периодически и в режиме зада- чи переустанавливая приоритет каждого процесса, готового к выполнению. Информация о времени, связанном с выполнением, нужна также и некоторым из пользовательских процессов: используемая ими, например, команда time поз- воляет узнать, сколько времени занимает выполнение другой команды, команда date выводит текущую дату и время суток. С помощью различных системных функ- ций процессы могут устанавливать или получать временные характеристики вы- полнения в режиме ядра, а также степень загруженности центрального процессо- ра. Время в системе поддерживается с помощью аппаратных часов, которые посы- лают ЦП прерывания с фиксированной, аппаратно-зависимой частотой, обычно 50-100 раз в секунду. Каждое поступление прерывания по таймеру (часам) име- нуется таймерным тиком. В настоящей главе рассматриваются особенности реали- зации процессов во времени, включая планирование процессов в системе UNIX, описание связанных со временем системных функций, а также функций, выполняе- мых программой обработки прерываний по таймеру. 8.1 ПЛАНИРОВАНИЕ ВЫПОЛНЕНИЯ ПРОЦЕССОВ Планировщик процессов в системе UNIX принадлежит к общему классу плани- ровщиков, работающих по принципу "карусели с многоуровневой обратной связью". В соответствии с этим принципом ядро предоставляет процессу ресурсы ЦП на квант времени, по истечении которого выгружает этот процесс и возвра- щает его в одну из нескольких очередей, регулируемых приоритетами. Прежде чем процесс завершится, ему может потребоваться множество раз пройти через цикл с обратной связью. Когда ядро выполняет переключение контекста и восс- танавливает контекст процесса, процесс возобновляет выполнение с точки при- останова. 8.1.1 Алгоритм Сразу после переключения контекста ядро запускает алгоритм планировани выполнения процессов (Рисунок 8.1), выбирая на выполнение процесс с наивыс- шим приоритетом среди процессов, находящихся в состояниях "резервирования" и "готовности к выполнению, будучи загруженным в память". Рассматривать про- цессы, не загруженные в память, не имеет смысла, поскольку не будучи загру- жен, процесс не может выполняться. Если наивысший приоритет имеют сразу нес- колько процессов, ядро, используя принцип кольцевого списка (карусели), вы- бирает среди них тот процесс, который находится в состоянии "готовности к выполнению" дольше остальных. Если ни один из процессов не может быть выбран для выполнения, ЦП простаивает до момента получения следующего прерывания, которое произойдет не позже чем через один таймерный тик; после обработки этого прерывания ядро снова запустит алгоритм планирования. +------------------------------------------------------------+ | алгоритм schedule_process | | входная информация: отсутствует | | выходная информация: отсутствует | | { | | выполнять пока (для запуска не будет выбран один из про-| | цессов) | | { | | для (каждого процесса в очереди готовых к выполнению)| | выбрать процесс с наивысшим приоритетом из загру-| | женных в память; | | если (ни один из процессов не может быть избран для | | выполнения) | | приостановить машину; | | /* машина выходит из состояния простоя по преры- | | /* ванию | | */ | | } | | удалить выбранный процесс из очереди готовых к выполне- | | нию; | | переключиться на контекст выбранного процесса, возобно- | | вить его выполнение; | | } | +------------------------------------------------------------+ Рисунок 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----| +---------| +---------| | +---------+ +---------+ +---------+ | (а) (б) (в) +----B----+ +-A-----B-+ +----B----+ 60 +----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 и т.д. 8.1.6 Работа в режиме реального времени Режим реального времени подразумевает возможность обеспечения достаточ- ной скорости реакции на внешние прерывания и выполнения отдельных процессов в темпе, соизмеримом с частотой возникновения вызывающих прерывания событий. Примером системы, работающей в режиме реального времени, может служить сис- тема управления жизнеобеспечением пациентов больниц, мгновенно реагирующа на изменение состояния пациента. Процессы, подобные текстовым редакторам, не считаются процессами реального времени: в них быстрая реакция на действи пользователя является желательной, но не необходимой (ничего страшного не произойдет, если пользователь, выполняющий редактирование текста, подождет ответа несколько лишних секунд, хотя у пользователя на этот счет могут быть и свои соображения). Вышеописанные алгоритмы планирования выполнения процес- сов предназначены специально для использования в системах разделения времени и не годятся для условий работы в режиме реального времени, поскольку не га- рантируют запуск ядром каждого процесса в течение фиксированного интервала времени, позволяющего говорить о взаимодействии вычислительной системы с процессами в темпе, соизмеримом со скоростью протекания этих процессов. Дру- гой помехой в поддержке работы в режиме реального времени является невыгру- жаемость ядра; ядро не может планировать выполнение процесса реального вре- мени в режиме задачи, если оно уже исполняет другой процесс в режиме ядра, без внесения в работу существенных изменений. В настоящее время системным программистам приходится переводить процессы реального времени в режим ядра, чтобы обеспечить достаточную скорость реакции. Правильное решение этой проб- лемы - дать таким процессам возможность динамического протекания (другими словами, они не должны быть встроены в ядро) с предоставлением соответствую- Время Процесс 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. Пример планирования на основе справедливого раздела, в ко- тором используются две группы с тремя процессами щего механизма, с помощью которого они могли бы сообщать ядру о своих нуж- дах, вытекающих из особенностей работы в режиме реального времени. На сегод- няшний день в стандартной системе UNIX такая возможность отсутствует. 8.2 СИСТЕМНЫЕ ОПЕРАЦИИ, СВЯЗАННЫЕ СО ВРЕМЕНЕМ Существует несколько системных функций, имеющих отношение к времени про- текания процесса: stime, time, times и alarm. Первые две имеют дело с гло- бальным системным временем, последние две - с временем выполнения отдельных процессов. Функция stime дает суперпользователю возможность заносить в глобальную ние глобальной переменной. Выбирается время из этой переменной с помощью функции time: time(tloc); где tloc - указатель на переменную, принадлежащую процессу, в которую зано- сится возвращаемое функцией значение. Функция возвращает это значение и из самой себя, например, команде date, которая вызывает эту функцию, чтобы оп- ределить текущее время. Функция times возвращает суммарное время выполнения процесса и всех его потомков, прекративших существование, в режимах ядра и задачи. Синтаксис вы- +------------------------------------------------------------+ | #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 зова функции: times(tbuffer) struct tms *tbuffer; где tms - имя структуры, в которую помещаются возвращаемые значения и кото- рая описывается следующим образом: 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 ТАЙМЕР В функции программы обработки прерываний по таймеру входит: * перезапуск часов, * вызов на исполнение функций ядра, использующих встроенные часы, * поддержка возможности профилирования выполнения процессов в режимах ядра и задачи; * сбор статистики о системе и протекающих в ней процессах, * слежение за временем, * посылка процессам сигналов "будильника" по запросу, * периодическое возобновление процесса подкачки (см. следующую главу), * управление диспетчеризацией процессов. Некоторые из функций реализуются при каждом прерывании по таймеру, дру- гие - по прошествии нескольких таймерных тиков. Программа обработки прерыва- ний по таймеру запускается с высоким приоритетом обращения к процессору, не допуская во время работы возникновения других внешних событий (таких как прерывания от периферийных устройств). Поэтому программа обработки прерыва- ний по таймеру работает очень быстро, за максимально-короткое врем пробегая свои критические отрезки, которые должны выполняться без прерываний со стороны других процессов. Алгоритм обработки прерываний по таймеру приве- ден на Рисунке 8.9. +------------------------------------------------------------+ | #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.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. Рассмотрим в качестве примера программу, приведенную на Рисунке 8.12, измеряющую продолжительность выполнения функций f и g. Сначала процесс, ис- пользуя системную функцию signal, делает указание при получении сигнала о прерывании вызывать функцию theend, затем он вычисляет диапазон адресов программы, в пределах которых будет производиться измерение продолжительнос- ти (начиная с адреса функции main и кончая адресом функции theend), и, нако- нец, запускает функцию 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 из-за того, что она кажется им слиш- ком сложной; вместо нее они используют при компиляции программ на языке Си параметр, сообщающий компилятору о необходимости сгенерировать код, следящий за ходом выполнения процессов. +------------------------------------------------------------+ | #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 +------------------------------------------------------+ | смещение до начала 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 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. Что произойдет в этом случае ? Как воврем распознать эту ситуацию ? ГЛАВА 9 АЛГОРИТМЫ УПРАВЛЕНИЯ ПАМЯТЬЮ Алгоритм планирования использования процессорного времени, рассмотренный в предыдущей главе, в сильной степени зависит от выбранной стратегии управ- ления памятью. Процесс может выполняться, если он хотя бы частично присутст- вует в основной памяти; ЦП не может исполнять процесс, полностью выгруженный во внешнюю память. Тем не менее, основная память - чересчур дефицитный ре- сурс, который зачастую не может вместить все активные процессы в системе. Если, например, в системе имеется основная память объемом 8 Мбайт, то девять процессов размером по 1 Мбайту каждый уже не смогут в ней одновременно поме- щаться. Какие процессы в таком случае следует размещать в памяти (хотя бы частично), а какие нет, решает подсистема управления памятью, она же управ- ляет участками виртуального адресного пространства процесса, не резидентными в памяти. Она следит за объемом доступного пространства основной памяти и имеет право периодически переписывать процессы на устройство внешней памяти, именуемое устройством выгрузки, освобождая в основной памяти дополнительное место. Позднее ядро может вновь поместить данные с устройства выгрузки в ос- новную память. В ранних версиях системы UNIX процессы переносились между основной па- мятью и устройством выгрузки целиком и, за исключением разделяемой области команд, отдельные независимые части процесса не могли быть объектами переме- щения. Такая стратегия управления памятью называется свопингом (подкачкой). Такую стратегию имело смысл реализовывать на машине типа PDP-11, где макси- мальный размер процесса составлял 64 Кбайта. При использовании этой страте- гии размер процесса ограничивается объемом физической памяти, доступной в системе. Система BSD (версия 4.0) явилась главным полигоном для применени другой стратегии, стратегии "подкачки по обращению" (demand paging), в соот- ветствии с которой основная память обменивается с внешней не процессами, а страницами памяти; эта стратегия поддерживается и в последних редакциях вер- сии V системы UNIX. Держать в основной памяти весь выполняемый процесс нет необходимости, и ядро загружает в память только отдельные страницы по запро- су выполняющегося процесса, ссылающегося на них. Преимущество стратегии под- качки по обращению состоит в том, что благодаря ей отображение виртуального адресного пространства процесса на физическую память машины становится более гибким: допускается превышение размером процесса объема доступной физической памяти и одновременное размещение в основной памяти большего числа процес- сов. Преимущество стратегии свопинга состоит в простоте реализации и облег- чении "надстроечной" части системы. Обе стратегии управления памятью расс- матриваются в настоящей главе. 9.1 СВОПИНГ Описание алгоритма свопинга можно разбить на три части: управление прос- транством на устройстве выгрузки, выгрузка процессов из основной памяти и подкачка процессов в основную память. 9.1.1 Управление пространством на устройстве выгрузки Устройство выгрузки является устройством блочного типа, которое предс- тавляет собой конфигурируемый раздел диска. Тогда как обычно ядро выделяет место для файлов по одному блоку за одну операцию, на устройстве выгрузки пространство выделяется группами смежных блоков. Пространство, выделяемое для файлов, используется статическим образом; поскольку схема назначени пространства под файлы действует в течение длительного периода времени, ее гибкость понимается в смысле сокращения числа случаев фрагментации и, следо- вательно, объемов неиспользуемого пространства в файловой системе. Выделение пространства на устройстве выгрузки, напротив, является временным, в сильной степени зависящим от механизма диспетчеризации процессов. Процесс, размещае- мый на устройстве выгрузки, в конечном итоге вернется в основную память, ос- вобождая место на внешнем устройстве. Поскольку время является решающим фак- тором и с учетом того, что ввод-вывод данных за одну мультиблочную операцию происходит быстрее, чем за несколько одноблочных операций, ядро выделяет на устройстве выгрузки непрерывное пространство, не беря во внимание возможную фрагментацию. Так как схема выделения пространства на устройстве выгрузки отличаетс от схемы, используемой для файловых систем, структуры данных, регистрирующие свободное пространство, должны также отличаться. Пространство, свободное в файловых системах, описывается с помощью связного списка свободных блоков, доступ к которому осуществляется через суперблок файловой системы, информа- ция о свободном пространстве на устройстве выгрузки собирается в таблицу, именуемую "карта памяти устройства". Карты памяти, помимо устройства выгруз- ки, используются и другими системными ресурсами (например, драйверами неко- торых устройств), они дают возможность распределять память устройства (в ви- де смежных блоков) по методу первого подходящего. Каждая строка в карте памяти состоит из адреса распределяемого ресурса и количества доступных единиц ресурса; ядро интерпретирует элементы строки в соответствии с типом карты. В самом начале карта памяти состоит из одной строки, содержащей адрес и общее количество ресурсов. Если карта описывает распределение памяти на устройстве выгрузки, ядро трактует каждую единицу ресурса как группу дисковых блоков, а адрес - как смещение в блоках от нача- ла области выгрузки. Первоначальный вид карты памяти для устройства выгруз- ки, состоящего из 10000 блоков с начальным адресом, равным 1, показан на Ри- сунке 9.1. Выделяя и освобождая ресурсы, ядро корректирует карту памяти, заботясь о том, чтобы в ней постоянно содер- жалась точная информация о свободных ресурсах в системе. На Рисунке 9.2 представлен алгоритм выделения пространства с помощью карт памяти (malloc). Ядро просматривает карту в поисках первой строки, со- держащей количество единиц ресурса, достаточное для удовлетворения запроса. Если запрос покрывает все количество единиц, содержащееся в строке, ядро удаляет строку и уплотняет карту (то есть в карте становится на одну строку меньше). В противном случае ядро переустанавливает адрес и число оставшихс единиц в строке в соответствии с числом единиц, выделенных по запросу. На Рисунке 9.3 показано, как меняется вид карты памяти для устройства выгрузки после выделения 100, 50 и вновь 100 единиц ресурса. В конечном итоге карта памяти принимает вид, показывающий, что первые 250 единиц ресурса выделены по запросам, и что теперь остались свободными 9750 единиц, начиная с адреса 251. Адрес Число единиц ресурса +------------------------------------+ | 1 10000 | +------------------------------------+ Рисунок 9.1. Первоначальный вид карты памяти для устройства выгрузки +------------------------------------------------------------+ | алгоритм malloc /* алгоритм выделения пространства с ис-| | пользованием карты памяти */ | | входная информация: (1) адрес /* указывает на тип ис- | | пользуемой карты */ | | (2) требуемое число единиц ресурса | | выходная информация: адрес - в случае успешного завершения | | 0 - в противном случае | | { | | для (каждой строки карты) | | { | | если (требуемое число единиц ресурса располагается в | | строке карты) | | { | | если (требуемое число == числу единиц в строке) | | удалить строку из карты; | | в противном случае | | отрегулировать стартовый адрес в строке; | | вернуть (первоначальный адрес строки); | | } | | } | | вернуть (0); | | } | +------------------------------------------------------------+ Рисунок 9.2. Алгоритм выделения пространства с помощью карт памяти Освобождая ресурсы, ядро ищет для них соответствующее место в карте по адресу. При этом возможны три случая: Адрес Число единиц ресурса Адрес Число единиц ресурса +------------------------------+ +------------------------------+ | 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) до тех пор, пока не закончится текущая операция выгрузки. Адрес места выгрузки об- ластей ядро сохраняет в соответствующих записях таблицы областей. За одну операцию ввода-вывода, в которой участвуют устройство выгрузки и адресное пространство задачи и которая осуществляется через буферный кеш, ядро выгружает максимально-возможное количество данных. Если аппаратура не в состоянии передать за одну операцию содержимое нескольких страниц памяти, перед программами ядра встает задача осуществить передачу содержимого памяти за несколько шагов по одной странице за каждую операцию. Таким образом, точ- ная скорость и механизм передачи данных определяются, помимо всего прочего, возможностями дискового контроллера и стратегией распределения памяти. Нап- ример, если используется страничная организация памяти, существует вероят- ность, что выгружаемые данные занимают несмежные участки физической памяти. Ядро обязано собирать информацию об адресах страниц с выгружаемыми данными, которую впоследствии использует дисковый драйвер, осуществляющий управление процессом ввода-вывода. Перед тем, как выгрузить следующую порцию данных, программа подкачки (выгрузки) ждет завершения предыдущей операции ввода-вы- вода. При этом перед ядром не встает задача переписать на устройство выгрузки содержимое виртуального адресного пространства процесса полностью. Вместо этого ядро копирует на устройство выгрузки содержимое физической памяти, от- веденной процессу, игнорируя неиспользуемые виртуальные адреса. Когда ядро подкачивает процесс обратно в память, оно имеет у себя карту виртуальных ад- ресов процесса и может переназначить процессу новые адреса. Ядро считывает копию процесса из буферного кеша в физическую память, в те ячейки, для кото- рых установлено соответствие с виртуальными адресами процесса. На Рисунке 9.6 приведен пример отображения образа процесса в памяти на адресное пространство устройства выгрузки (*). Процесс располагает тремя об- ластями: команд, данных и стека. Область команд заканчивается на виртуальном --------------------------------------- (*) Для простоты виртуальное адресное пространство процесса на этом и на всех последующих рисунках изображается в виде линейного массива точек входа в таблицу страниц, не принимая во внимание тот факт, что кажда область обычно имеет свою отдельную таблицу страниц. Расположение виртуальных адресов Устройство выгрузки Виртуальные, физические адреса +----------------------+ +-----------------+ Область | 0 278К ----|-------684--|---> | команд +----------------------| +-----------------| | 1К 432К ----|------------|---> | +----------------------| +-----------------| | пусто | +--------|---> | +----------------------| +-----------------| | | +-------|---> | | | +-----------------| | | +------|---> | +----------------------| +-----------------| Область | 64К 573К ----|---+ +----|---> | данных +----------------------| +-----------------| | 65К 647К ----|----+ 690 | | +----------------------| +-----------------+ | 66К 595К ----|-----+ +----------------------| | пусто | +----------------------| | | | | +----------------------| Область |128К 401К ----|-------+ стека +----------------------| | пусто | +----------------------+ Рисунок 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 и вернется в режим задачи. 9.1.2.2 Выгрузка с расширением Если процесс испытывает потребность в дополнительной физической памяти, либо в результате расширения стека, либо в результате запуска функции brk, и если эта потребность превышает доступные резервы памяти, ядро выполняет опе- рацию выгрузки процесса с расширением его размера на устройстве выгрузки. На устройстве выгрузки ядро резервирует место для размещения процесса с учетом Первоначальное Расширенный расположение формат Виртуальные, Виртуальные, Устройство физические адреса физические адреса выгрузки +------------+ +------------+ +---------+ Область| 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.8). Когда нес- колько позже ядро будет загружать процесс обратно в память, физическое прос- транство будет выделено уже с учетом нового состояния таблицы преобразовани адресов. В момент возобновления у процесса уже будет в распоряжении память достаточного объема. 9.1.3 Загрузка (подкачка) процессов Нулевой процесс (процесс подкачки) является единственным процессом, заг- ружающим другие процессы в память с устройств выгрузки. Процесс подкачки на- чинает работу по выполнению этой своей единственной функции по окончании инициализации системы (как уже говорилось в разделе 7.9). Он загружает про- цессы в память и, если ему не хватает места в памяти, выгружает оттуда неко- торые из процессов, находящихся там. Если у процесса подкачки нет работы (например, отсутствуют процессы, ожидающие загрузки в память) или же он не в состоянии выполнить свою работу (ни один из процессов не может быть выгру- жен), процесс подкачки приостанавливается; ядро периодически возобновляет его выполнение. Ядро планирует запуск процесса подкачки точно так же, как делает это в отношении других процессов, ориентируясь на более высокий прио- ритет, при этом процесс подкачки выполняется только в режиме ядра. Процесс подкачки не обращается к функциям операционной системы, а использует в своей работе только внутренние функции ядра; он является архетипом всех процессов ядра. Как уже вкратце говорилось в главе 8, программа обработки прерываний по таймеру измеряет время нахождения каждого процесса в памяти или в состоянии выгрузки. Когда процесс подкачки возобновляет свою работу по загрузке про- цессов в память, он просматривает все процессы, находящиеся в состоянии "го- товности к выполнению, будучи выгруженными", и выбирает из них один, который находится в этом состоянии дольше остальных (см. Рисунок 9.9). Если имеетс достаточно свободной памяти, процесс подкачки загружает выбранный процесс, выполняя операции в последовательности, обратной выгрузке процесса. Сначала выделяется физическая память, затем с устройства выгрузки считывается нужный процесс и освобождается место на устройстве. Если процесс подкачки выполнил процедуру загрузки успешно, он вновь просматривает совокупность выгруженных, но готовых к выполнению процессов в поисках следующего процесса, который предполагается загрузить в память, и повторяет указанную последовательность действий. В конечном итоге возникает одна из следующих ситуаций: * На устройстве выгрузки больше нет ни одного процесса, готового к выпол- нению. Процесс подкачки приостанавливает свою работу до тех пор, пока не возобновится процесс на устройстве выгрузки или пока ядро не выгрузит процесс, готовый к выполнению. (Вспомним диаграмму состояний на Рисунке 6.1). * Процесс подкачки обнаружил процесс, готовый к загрузке, но в системе не- достаточно памяти для его размещения. Процесс подкачки пытается загру- зить другой процесс и в случае успеха перезапускает алгоритм подкачки, продолжая поиск загружаемых процессов. Если процессу подкачки нужно выгрузить процесс, он просматривает все процессы в памяти. Прекратившие свое существование процессы не подходят дл выгрузки, поскольку они не занимают физическую память; также не могут быть выгружены процессы, заблокированные в памяти, например, выполняющие операции над областями. Ядро предпочитает выгружать приостановленные процессы, пос- кольку процессы, готовые к выполнению, имеют больше шансов быть вскоре выб- ранными на выполнение. Решение о выгрузке процесса принимается ядром на ос- новании его приоритета и продолжительности его пребывания в памяти. Если в памяти нет ни одного приостановленного процесса, решение о том, какой из процессов, готовых к выполнению, следует выгрузить, зависит от значения, присвоенного процессу функцией nice, а также от продолжительности пребывани процесса в памяти. Процесс, готовый к выполнению, должен быть резидентным в памяти в тече- ние по меньшей мере 2 секунд до того, как уйти из нее, а процесс, загружае- мый в память, должен по меньшей мере 2 секунды пробыть на устройстве выгруз- ки. Если процесс подкачки не может найти ни одного процесса, подходящего дл выгрузки, или ни одного процесса, подходящего для загрузки, или ни одного процесса, перед выгрузкой не менее 2 секунд (**) находившегося в памяти, он приостанавливает свою работу по причине того, что ему нужно загрузить про- цесс в память, а в памяти нет места для его размещения. В этой ситуации тай- мер возобновляет выполнение процесса подкачки через каждую секунду. Ядро --------------------------------------- (**) В версии 6 системы UNIX процесс не может быть выгружен из памяти с целью расчистки места для загружаемого процесса до тех пор, пока загру- жаемый процесс не проведет на диске 3 секунды. Уходящий из памяти про- цесс должен провести в памяти не менее 2 секунд. Временной интервал та- ким образом делится на части, в результате чего повышается производи- тельность системы. +------------------------------------------------------------+ | алгоритм swapper /* загрузка выгруженных процессов, | | * выгрузка других процессов с целью | | * расчистки места в памяти */ | | входная информация: отсутствует | | выходная информация: отсутствует | | { | | loop: | | для (всех выгруженных процессов, готовых к выполнению)| | выбрать процесс, находящийся в состоянии выгружен-| | ности дольше остальных; | | если (таких процессов нет) | | { | | приостановиться (до момента, когда возникнет необ-| | ходимость в загрузке процессов); | | перейти на loop; | | } | | если (в основной памяти достаточно места для размеще- | | ния процесса) | | { | | загрузить процесс; | | перейти на loop; | | } | | /* loop2: сюда вставляются исправления, внесенные в алго- | | * ритм */ | | для (всех процессов, загруженных в основную память, | | кроме прекративших существование и заблокированных в | | памяти) | | { | | если (есть хотя бы один приостановленный процесс) | | выбрать процесс, у которого сумма приоритета и| | продолжительности нахождения в памяти наи- | | большая; | | в противном случае /* нет ни одного приостанов- | | * ленного процесса */ | | выбрать процесс, у которого сумма продолжи- | | тельности нахождения в памяти и значения nice| | наибольшая; | | } | | если (выбранный процесс не является приостановленным | | или не соблюдены условия резидентности) | | приостановиться (до момента, когда появится воз- | | можность загрузить процесс); | | в противном случае | | выгрузить процесс; | | перейти на loop; /* на loop2 в исправленном алгорит-| | * ме */ | | } | +------------------------------------------------------------+ Рисунок 9.9. Алгоритм подкачки также возобновляет работу процесса подкачки в том случае, когда один из про- цессов переходит в состояние приостанова, так как последний может оказатьс более подходящим для выгрузки процессом по сравнению с ранее рассмотренными. Если процесс подкачки расчистил место в памяти или если он был приостановлен по причине невозможности сделать это, он возобновляет свою работу с переза- пуска алгоритма подкачки (с самого его начала), вновь предпринимая попытку загрузить ожидающие выполнения процессы. На Рисунке 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 Кбайта па- мяти. В качестве альтернативы может быть предложена стратегия выгрузки групп процессов при условии, что они освобождают место, достаточное для размещени загружаемых процессов.Эксперименты с использованием машины PDP 11/23 показа- ли,что в условиях сильной загруженности такая стратегия может увеличить про- изводительность системы почти на 10 процентов (см. [Peachey 84]). Во-вторых, если процесс подкачки приостановил свою работу изза того, что в памяти не хватило места для загрузки процесса, после возобновления он вновь выбирает процесс для загрузки в память, несмотря на то, что ранее им уже был сделан выбор. Причина такого поведения заключается в том, что за прошедшее время в состояние готовности к выполнению могли перейти другие выгруженные процессы, более подходящие для загрузки в память по сравнению с ранее выбранным процессом. Однако от этого мало утешения для ранее выбранно- го процесса, все еще пытающегося загрузиться в память. В некоторых реализа- циях процесс подкачки стремится к тому, чтобы перед загрузкой в память одно- го крупного процесса выгрузить большое количество процессов маленького раз- мера, это изменение в базовом алгоритме подкачки отражено в комментариях к алгоритму (Рисунок 9.9). В-третьих, если процесс подкачки выбирает для выгрузки процесс, находя- щийся в состоянии "готовности к выполнению", не исключена возможность того, что этот процесс после загрузки в память ни разу не был запущен на исполне- ние. Этот случай показан на Рисунке 9.11, из которого видно, что ядро загру- жает процесс D на 2- секундной отметке, запускает процесс C, а затем на 3-секундной отметке процесс D выгружается в пользу процесса E (уступая пос- леднему в значении nice), несмотря на то, что процессу D так и не был пре- доставлен ЦП. Понятно, что такая ситуация является нежелательной. Следует упомянуть еще об одной опасности. Если при попытке выгрузить процесс на устройстве выгрузки не будет найдено свободное место, в системе может возникнуть тупиковая ситуация, при которой: все процессы в основной Время Процесс 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. Загрузка процессов в случае разбивки временных интервалов на части памяти находятся в состоянии приостанова, все готовые к выполнению процессы выгружены, для новых процессов на устройстве выгрузки уже нет места, нет свободного места и в основной памяти. Эта ситуация разбирается в упражнении 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). 9.2.1.2 Функция exec в системе с замещением страниц Как уже говорилось в главе 7, когда процесс обращается к системной функ- ции exec, ядро считывает из файловой системы в память указанный исполняемый файл. Однако в системе с замещением страниц по запросу исполняемый файл, --------------------------------------- (****) Функция exit используется в варианте _exit, потому что она "очищает" структуры данных, передаваемые через стандартный ввод-вывод (на поль- зовательском уровне), для обоих процессов, так что оператор printf, используемый родителем, не даст правильный результат - еще один неже- лательный побочный эффект от применения функции vfork. имеющий большой размер, может не уместиться в доступном пространстве основ- ной памяти. Поэтому ядро не назначает ему сразу все пространство, а отводит место в памяти по мере надобности. Сначала ядро назначает файлу таблицы страниц и дескрипторы дисковых блоков, помечая страницы в записях таблиц как "заполняемые при обращении" (для всех данных, кроме имеющих тип bss) или "обнуляемые при обращении" (для данных типа bss). Считывая в память каждую страницу файла по алгоритму read, процесс получает ошибку из-за отсутстви (недоступности) данных. Подпрограмма обработки ошибок проверяет, является ли страница "заполняемой при обращении" (тогда ее содержимое будет немедленно затираться содержимым исполняемого файла и поэтому ее не надо очищать) или "обнуляемой при обращении" (тогда ее следует очистить). В разделе 9.2.3 мы увидим, как это происходит. Если процесс не может поместиться в памяти, "сборщик" страниц освобождает для него место, периодически откачивая из па- мяти неиспользуемые страницы. В этой схеме видны явные недостатки. Во-первых, при чтении каждой стра- ницы исполняемого файла процесс сталкивается с ошибкой из-за обращения к от- сутствующей странице, пусть даже процесс никогда и не обращался к ней. Во-вторых, если после того, как "сборщик" страниц откачал часть страниц из памяти, была запущена функция exec, каждая только что выгруженная и вновь понадобившаяся страница потребует дополнительную операцию по ее загрузке. Чтобы повысить эффективность функции exec, ядро может востребовать страницу непосредственно из исполняемого файла, если данные в файле соответствующим образом настроены, что определяется значением т.н. "магического числа". Од- нако, использование стандартных алгоритмов доступа к файлу (например, bmap) потребовало бы при обращении к странице, состоящей из блоков косвенной адре- сации, больших затрат, связанных с многократным использованием буферного ке- ша для чтения каждого блока. Кроме того, функция bmap не является реентера- бельной, отсюда возникает опасность нарушения целостности данных. Во врем выполнения системной функции read ядро устанавливает в пространстве процесса значения различных параметров ввода-вывода. Если при попытке скопировать данные в пространство пользователя процесс столкнется с отсутствием нужной страницы, он, считывая страницу из файловой системы, может затереть содержа- щие эти параметры поля. Поэтому ядро не может прибегать к использованию обычных алгоритмов обработки ошибок данного рода. Конечно же алгоритмы долж- ны быть в обычных случаях реентерабельными, поскольку у каждого процесса свое отдельное адресное пространство и процесс не может одновременно испол- нять несколько системных функций. Для того, чтобы считывать страницы непосредственно из исполняемого фай- ла, ядро во время исполнения функции exec составляет список номеров дисковых блоков файла и присоединяет этот список к индексу файла. Работая с таблицами страниц такого файла, ядро находит дескриптор дискового блока, содержащего страницу, и запоминает номер блока внутри файла; этот номер позже использу- Список блоков, Область +-> связанный с индексом +---------------------------------+ | +----------------+ | Индекс-----------+-+ 0 | | | | | | | | | | | Дескриптор дискового блока | | | | +---------------------------+ | | | | | Логический блок 84 | | +----------------| | +---------------------------+ | 84 | 279 | | | +----------------| +---------------------------------+ | | | | +----------------+ Рисунок 9.17. Отображение файла на область ется при загрузке страницы из файла. На Рисунке 9.17 показан пример, в кото- ром страница имеет адрес расположения в логическом блоке с номером 84 от на- чала файла. В области имеется указатель на индекс, в котором содержится но- мер соответствующего физического блока на диске (279). 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). Бит доступности не устанавливается ни для тех страниц, которые располагаются за пределами виртуального адресного пространства процесса, ни для тех, которые входят в состав этого пространства, но не имеют в настоящий момент физического аналога в памяти. Фатальная ошибка памяти произошла в ре- зультате обращения ядра по виртуальному адресу страницы, поэтому ядро выхо- дит на соответствующую этой странице запись в таблице страниц и дескриптор дискового блока. Чтобы предотвратить взаимную блокировку, которая может про- изойти, если "сборщик" попытается выгрузить страницу из памяти, ядро фикси- рует в памяти область с соответствующей записью таблицы страниц. Если в дес- крипторе дискового блока отсутствует информация о странице, сделанная ссылка на страницу является недопустимой и ядро посылает процес- су-нарушителю сигнал о "нарушении сегментации" (см. Рисунок 7.25). Такой по- рядок действий совпадает с тем порядком, которого придерживается ядро, когда процесс обратился по неверному адресу, если не принимать во внимание то обс- тоятельство, что ядро узнает об ошибке немедленно, так как все "доступные" страницы являются резидентными в памяти. Если ссылка на страницу сделана правильно, ядро выделяет физическую страницу в памяти и считывает в нее со- держимое виртуальной страницы с устройства выгрузки или из исполняемого фай- ла. Страница, вызвавшая отказ, находится в одном из пяти состояний: 1. На устройстве выгрузки вне памяти. 2. В списке свободных страниц в памяти. 3. В исполняемом файле. 4. С пометкой "обнуляемая при обращении". 5. С пометкой "заполняемая при обращении". Рассмотрим каждый случай в подробностях. Если страница находится на устройстве выгрузки, вне памяти (случай 1), это означает, что она когда-то располагалась в памяти, но была выгружена от- туда "сборщиком" страниц. Обратившись к дескриптору дискового блока, ядро узнает из него номера устройства выгрузки и блока, где расположена страница, и проверяет, не осталась ли страница в кеше. Ядро корректирует запись табли- цы страниц так, чтобы она указывала на страницу, которую предполагается счи- тать в память, включает соответствующую запись таблицы pfdata в хеш-очередь (облегчая последующую обработку отказа) и считывает страницу с устройства выгрузки. Допустивший ошибку процесс приостанавливается до момента заверше- ния ввода-вывода; вместе с ним будут возобновлены все процессы, ожидавшие загрузки содержимого страницы. Обратимся к Рисунку 9.22 и в качестве примера рассмотрим запись таблицы страниц, связанную с виртуальным адресом 66К. Если при обращении к странице процесс получает отказ из-за недоступности данных, программа обработки отка- за обращается к дескриптору дискового блока и обнаруживает то, что страница находится на устройстве выгрузки в блоке с номером 847 (если предположить, что в системе только одно устройство выгрузки): следовательно, виртуальный адрес указан верно. Затем программа обработки отказа обращается к кешу, но не находит информации о дисковом блоке с номером 847. Таким образом, копи виртуальной страницы в памяти отсутствует и программа обработки отказа долж- на загрузить ее с устройства выгрузки. Ядро отводит физическую страницу с номером 1776 (Рисунок 9.23), считывает в нее с устройства выгрузки содержи- мое виртуальной страницы и перенастраивает запись таблицы страниц на страни- цу с номером 1776. В завершение ядро корректирует дескриптор дискового бло- +------------------------------------------------------------+ | алгоритм vfault /* обработка отказа из-за отсутствия | | (недоступности) данных */ | | входная информация: адрес, по которому получен отказ | | выходная информация: отсутствует | | { | | найти область, запись в таблице страниц, дескриптор дис-| | кового блока, связанные с адресом, по которому получен | | отказ, заблокировать область; | | если (адрес не принадлежит виртуальному адресному прост-| | ранству процесса) | | { | | послать сигнал (SIGSEGV: нарушение сегментации) про- | | цессу; | | перейти на out; | | } | | если (адрес указан неверно) /* возможно, процесс нахо-| | дился в состоянии при- | | останова */ | | перейти на out; | | если (страница имеется в кеше) | | { | | убрать страницу из кеша; | | поправить запись в таблице страниц; | | выполнять пока (содержимое страницы не станет доступ-| | ным) /* другой процесс получил такой же отказ, | | * но раньше */ | | приостановиться; | | } | | в противном случае /* страница отсутствует в кеше */| | { | | назначить области новую страницу; | | | | поместить новую страницу в кеш, откорректировать за- | | пись в таблице pfdata; | | если (страница ранее не загружалась в память и имеет | | пометку "обнуляемая при обращении") | | очистить содержимое страницы; | | в противном случае | | { | | считать виртуальную страницу с устройства выгруз-| | ки или из исполняемого файла; | | приостановиться (до завершения ввода-вывода); | | } | | возобновить процессы (ожидающие загрузки содержимого | | страницы); | | } | | установить бит доступности страницы; | | сбросить бит модификации и "возраст" страницы; | | пересчитать приоритет процесса; | | out: снять блокировку с области; | | } | +------------------------------------------------------------+ Рисунок 9.21. Алгоритм обработки отказа из-за отсутствия (не- доступности) данных ка, делая указание о том, что страница загружена, а также запись таблицы 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, чем и объясняется его запоминание в обеих таблицах. Как и ядру, программе обработки отказа не нужно считывать страницу в па- мять, если какой-то иной процесс уже получил отказ по той же самой странице, но еще не полностью загрузил ее. Программа находит область с записью таблицы Записи Дескрипторы таблицы страниц дисковых блоков Страничные блоки Физи- ческая Диско- Виртуаль- стра- Состо- Состо- Стра- вый Счет- ный адрес ница яние яние Блок ница блок чик +----------------------------------+ +----------------+ 66К | 1776| Доступ-|||На дис-| 847 | |1776| 847 | 1 | | | на |||ке | | | | | | +----------------------------------+ +----------------+ Рисунок 9.23. Результат загрузки страницы в память Процесс A Процесс B +------------------------------------------------------------ | Отказ при обращении к стра- | нице | Виртуальный адрес страницы | верен | Приостанов до завершения | считывания страницы | Отказ при обращении к стра- | нице | Виртуальный адрес страницы | верен | Загрузка страницы в память | Приостанов до окончани | загрузки | | Выход из приостанова -- | страница в памяти | Страница помечается как | доступная | Выход из приостанова других | процессов | Выход из приостанова | Возобновление выполнения | | Возобновление выполнени | | | Время v Рисунок 9.24. Два отказа на одной странице страниц, которую она уже ранее заблокировала. Она дожидается, пока будет за- кончен цикл обработки предыдущего отказа, после чего обнаруживает, что стра- ница стала доступной, и завершает свою работу. Эта процедура прослеживаетс на Рисунке 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]290ge[0] = i; | | } } | +------------------------------------------------------------+ ГЛАВА 10 ПОДСИСТЕМА УПРАВЛЕНИЯ ВВОДОМ-ВЫВОДОМ Подсистема управления вводом-выводом позволяет процессам поддерживать связь с периферийными устройствами, такими как накопители на магнитных дис- ках и лентах, терминалы, принтеры и сети, с одной стороны, и с модулями яд- ра, которые управляют устройствами и именуются драйверами устройств, с дру- гой. Между драйверами устройств и типами устройств обычно существует одноз- начное соответствие: в системе может быть один дисковый драйвер для управле- ния всеми дисководами, один терминальный драйвер для управления всеми терми- налами и один ленточный драйвер для управления всеми ленточными накопителя- ми. Если в системе имеются однотипные устройства, полученные от разных изго- товителей - например, две марки ленточных накопителей, - в этом случае можно трактовать однотипные устройства как устройства двух различных типов и иметь для них два отдельных драйвера, поскольку таким устройствам для выполнени одних и тех же операций могут потребоваться разные последовательности ко- манд. Один драйвер управляет множеством физических устройств данного типа. Например, один терминальный драйвер может управлять всеми терминалами, подк- люченными к системе. Драйвер различает устройства, которыми управляет: вы- ходные данные, предназначенные для одного терминала, не должны быть посланы на другой. Система поддерживает "программные устройства", с каждым из которых не связано ни одно конкретное физическое устройство. Например, как устройство трактуется физическая память, чтобы позволить процессу обращаться к ней изв- не, пусть даже память не является периферийным устройством. Команда ps обра- щается к информационным структурам ядра в физической памяти, чтобы сообщить статистику процессов. Еще один пример: драйверы могут вести трассировку за- писей в удобном для отладки виде, а драйвер трассировки дает возможность пользователям читать эти записи. Наконец, профиль ядра, рассмотренный в гла- ве 8, выполнен как драйвер: процесс записывает адреса программ ядра, обнару- женных в таблице идентификаторов ядра, и читает результаты профилирования. В этой главе рассматривается взаимодействие между процессами и подсисте- мой управления вводом-выводом, а также между машиной и драйверами устройств. Исследуется общая структура и функционирование драйверов и в качестве приме- ров общего взаимодействия рассматриваются дисковые и терминальные драйверы. Завершает главу описание нового метода реализации драйверов потоковых уст- ройств. 10.1 ВЗАИМОДЕЙСТВИЕ ДРАЙВЕРОВ С ПРОГРАММНОЙ И АППАРАТНОЙ СРЕДОЙ В системе UNIX имеется два типа устройств - устройства вводавывода бло- ками и устройства неструктурированного или посимвольного ввода-вывода. Как уже говорилось в главе 2, устройства ввода-вывода блоками, такие как диски и ленты, для остальной части системы выглядят как запоминающие устройства с произвольной выборкой; к устройствам посимвольного ввода-вывода относятс все другие устройства, в том числе терминалы и сетевое оборудование. Устрой- ства ввода-вывода блоками могут иметь интерфейс и с устройствами посимволь- ного ввода-вывода. Пользователь взаимодействует с устройствами через посредничество файло- вой системы (см. Рисунок 2.1). Каждое устройство имеет имя, похожее на им файла, и пользователь обращается к нему как к файлу. Специальный файл уст- ройства имеет индекс и занимает место в иерархии каталогов файловой системы. Файл устройства отличается от других файлов типом файла, хранящимся в его индексе, либо "блочный", либо "символьный специальный", в зависимости от ус- тройства, которое этот файл представляет. Если устройство имеет как блочный, так и символьный интерфейс, его представляют два файла: специальный файл ус- тройства ввода-вывода блоками и специальный файл устройства посимвольного ввода-вывода. Системные функции для обычных файлов, такие как open, close, read и write, имеют то же значение и для устройств, в чем мы убедимся позже. Системная функция ioctl предоставляет процессам возможность управлять уст- ройствами посимвольного ввода-вывода, но не применима в отношении к файлам обычного типа (*). Тем не менее, драйверам устройств нет необходимости под- держивать полный набор системных функций. Например, вышеупомянутый драйвер трассировки дает процессам возможность читать записи, созданные другими драйверами, но не позволяет создавать их. 10.1.1 Конфигурация системы Задание конфигурации системы это процедура указания администраторами значений параметров, с помощью которых производится настройка системы. Неко- торые из параметров указывают размеры таблиц ядра, таких как таблица процес- сов, таблица индексов и таблица файлов, а также сколько буферов помещается в буферном пуле. С помощью других параметров указывается конфигурация устрой- ств, то есть производятся конкретные указания ядру, какие устройства включа- ются в данную системную реализацию и их "адрес". Например, в конфигурации может быть указано, что терминальная плата вставлена в соответствующий разъ- ем на аппаратной панели. Существует три стадии, на которых может быть указана конфигурация уст- ройств. Во-первых, администраторы могут кодировать информацию о конфигурации в файлах, которые транслируются и компонуются во время построения ядра. Ин- формация о конфигурации обычно указывается в простом формате, и программа конфигурации преобразует ее в файл, готовый для трансляции. Во-вторых, адми- нистраторы могут указывать информацию о конфигурации после того, как система уже запущена; ядро динамически корректирует внутренние таблицы конфигурации. Наконец, самоидентифицирующиеся устройства дают ядру возможность узнать, ка- кие из устройств включены. Ядро считывает аппаратные ключи для самонастрой- ки. Подробности задания системной конфигурации выходят за пределы этой кни- ги, однако во всех случаях результатом процедуры задания конфигурации явля- ется генерация или заполнение таблиц, составляющих основу программ ядра. Интерфейс "ядро - драйвер" описывается в таблице ключей устройств вво- да-вывода блоками и в таблице ключей устройств посимвольного ввода-вывода (Рисунок 10.1). Каждый тип устройства имеет в таблице точки входа, которые при выполнении системных функций адресуют ядро к соответствующему драйверу. Функции open и close, вызываемые файлом устройства, "пропускаются" через таблицы ключей устройств в соответствии с типом файла. Функции mount и umount так же вызывают выполнение процедур открытия и закрытия устройств, но для устройств ввода-вывода блоками. Функции read и write, вызываемые устрой- ствами ввода-вывода блоками и файлами в смонтированных файловых системах, запускают алгоритмы работы с буферным кешем, инициирующие реализацию страте- гической процедуры работы с устройствами. Некоторые из драйверов запускают эту процедуру изнутри из процедур чтения и записи. Более подробно взаимодей- ствие с каждым драйвером рассматривается в следующем разделе. Интерфейс "аппаратура - драйвер" состоит из машинно-зависимых управляю- щих регистров или команд ввода-вывода для управления устройствами и вектора- ми прерываний: когда происходит прерывание от устройства, система идентифи- цирует устройство, вызвавшее прерывание, и запускает программу обработки со- ---------------------------------------- (*) И наоборот, системная функция fcntl обеспечивает контроль над действия- ми, производимыми на уровне дескриптора файла, но не на уровне устройст- ва. В других реализациях функция ioctl применима для файлов всех типов. ответствующего прерывания. Очевидно, что "программные устройства", такие как драйвер системы построения профиля ядра (глава 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), приостановятся на неопределенное время, поскольку один процесс приостановил драйвер. Драйвер устройства интерпретирует параметры вызова системной функции в отношении устройства. Драйвер поддерживает структуры данных, описывающие состояние каждой контролируемой единицы данного типа устройства; функции драйвера и программы обработки прерываний реализуются в соответствии с сос- тоянием драйвера и с тем, какое действие выполняется в этот момент (напри- мер, данные вводятся или выводятся). Теперь рассмотрим каждый интерфейс бо- лее подробно. +------------------------------------------------------------+ | алгоритм open /* для драйверов устройств */ | | входная информация: имя пути поиска | | режим открытия | | выходная информация: дескриптор файла | | { | | преобразовать имя пути поиска в индекс, увеличить значе-| | ние счетчика ссылок в индексе; | | выделить в таблице файлов место для пользовательского | | дескриптора файла, как при открытии обычного файла; | | | | выбрать из индекса старший и младший номера устройства; | | | | сохранить контекст (алгоритм setjmp) в случае передачи | | управления от драйвера; | | | | если (устройство блочного типа) | | { | | использовать старший номер устройства в качестве ука-| | зателя в таблице ключей устройств ввода-вывода бло- | | ками; | | вызвать процедуру открытия драйвера по данному индек-| | су: передать младший номер устройства, режимы откры-| | тия; | | } | | в противном случае | | { | | использовать старший номер устройства в качестве ука-| | зателя в таблице ключей устройств посимвольного вво-| | да-вывода; | | вызвать процедуру открытия драйвера по данному индек-| | су: передать младший номер устройства, режимы откры-| | тия; | | } | | | | если (открытие в драйвере не выполнилось) | | привести таблицу файлов к первоначальному виду, | | уменьшить значение счетчика в индексе; | | } | +------------------------------------------------------------+ Рисунок 10.3. Алгоритм открытия устройства 10.1.2.1 Open При открытии устройства ядро следует той же процедуре, что и при откры- тии файлов обычного типа (см. раздел 5.1), выделяя в памяти индексы, увели- чивая значение счетчика ссылок и присваивая значение точки входа в таблицу файлов и пользовательского дескриптора файла. Наконец, ядро возвращает зна- чение пользовательского дескриптора файла вызывающему процессу, так что отк- рытие устройства выглядит так же, как и открытие файла обычного типа. Одна- ко, перед тем, как вернуться в режим задачи, ядро запускает зависящую от устройства процедуру open (Рисунок 10.3). Для устройства вво- да-вывода блоками запускается процедура open, закодированная в таблице клю- чей устройств ввода-вывода блоками, для устройств посимвольного ввода-вывода - процедура open, закодированная в соответствующей таблице. Если устройство имеет как блочный, так и символьный тип, ядро запускает процедуру open, со- ответствующую типу файла устройства, открытого пользователем: обе процедуры могут даже быть идентичны, в зависимости от конкретного драйвера. Зависящая от типа устройства процедура open устанавливает связь между вызывающим процессом и открываемым устройством и инициализирует информацион- ные структуры драйвера. Например, процедура open для терминала может приос- тановить процесс до тех пор, пока в машину не поступит сигнал (аппаратный) о том, что пользователь предпринял попытку зарегистрироваться. После этого инициализируются информационные структуры драйвера в соответствии с приняты- ми установками терминала (например, скоростью передачи информации в бодах). Для "программных устройств", таких как память системы, процедура open может не включать в себя инициализацию. Если во время открытия устройства процессу пришлось приостановиться по какой-либо из внешних причин, может так случиться, что событие, которое дол- жно было бы вызвать возобновление выполнения процесса, так никогда и не про- изойдет. Например, если на данном терминале еще не зарегистрировался ни один из пользователей, процесс getty, "открывший" терминал (раздел 7.9), приоста- навливается до тех пор, пока пользователем не будет предпринята попытка ре- гистрации, при этом может пройти достаточно большой промежуток времени. Ядро должно иметь возможность возобновить выполнение процесса и отменить вызов функции open по получении сигнала: ему следует сбросить индекс, отменить точку входа в таблице файлов и пользовательский дескриптор файла, которые были выделены перед входом в драйвер, поскольку открытие не произошло. Ядро сохраняет контекст процесса, используя алгоритм setjmp (раздел 6.4.4), преж- де чем запустить процедуру open; если процесс возобновляется по сигналу, яд- ро восстанавливает контекст процесса в том состоянии, которое он имел перед обращением к драйверу, используя алгоритм longjmp (раздел 6.4.4), и возвра- щает системе все выделенные процедуре open структуры данных. Точно так же и драйвер может уловить сигнал и очистить доступные ему структуры данных, если это необходимо. Ядро также переустанавливает структуры данных файловой сис- темы, когда драйвер сталкивается с исключительными ситуациями, такими, как попытка пользователя обратиться к устройству, отсутствующему в данной конфи- гурации. В подобных случаях функция open не выполняется. Процессы могут указывать значения различных параметров, характеризующие особенности выполнения процедуры открытия. Из них наиболее часто использует- ся "no delay" (без задержки), означающее, что процесс не будет приостановлен во время выполнения процедуры open, если устройство не готово. Системна функция open возвращает управление немедленно и пользовательский процесс не узнает, произошло ли аппаратное соединение или нет. Открытие устройства с параметром "no delay", кроме всего прочего, затронет семантику вызова функ- ции read, что мы увидим далее (раздел 10.3.4). Если устройство открывается многократно, ядро обрабатывает пользователь- ские дескрипторы файлов, индекс и записи в таблице файлов так, как это опи- сано в главе 5, запуская определяемую типом устройства процедуру open при каждом вызове системной функции open. Таким образом, драйвер устройства мо- жет подсчитать, сколько раз устройство было "открыто", и прервать выполнение функции open, если количество открытий приняло недопустимое значение. Напри- мер, имеет смысл разрешить процессам многократно "открывать" терминал на за- пись для того, чтобы пользователи могли обмениваться сообщениями. Но при этом не следует допускать многократного "открытия" печатающего устройства для одновременной записи, так как процессы могут затереть друг другу инфор- мацию. Эти различия имеют смысл скорее на практике, нежели на стадии разра- ботки: разрешение одновременной записи на терминалы способствует установле- нию взаимодействия между пользователями; запрещение одновременной записи на принтеры служит повышению читабельности машинограмм (**). +------------------------------------------------------------+ | алгоритм close /* для устройств */ | | входная информация: дескриптор файла | | выходная информация: отсутствует | | { | | выполнить алгоритм стандартного закрытия (глава 5ххх); | | если (значение счетчика ссылок в таблице файлов не 0) | | перейти на finish; | | если (существует еще один открытый файл, старший и млад-| | ший номера которого совпадают с номерами закрываемого | | устройства) | | перейти на finish; /* не последнее закрытие */ | | если (устройство символьного типа) | | { | | использовать старший номер в качестве указателя в | | таблице ключей устройства посимвольного ввода-выво- | | да; | | вызвать процедуру закрытия, определяемую типом драй- | | вера и передать ей в качестве параметра младший но- | | мер устройства; | | } | | если (устройство блочного типа) | | { | | если (устройство монтировано) | | перейти на finish; | | переписать блоки устройства из буферного кеша на уст-| | ройство; | | использовать старший номер в качестве указателя в | | таблице ключей устройства ввода-вывода блоками; | | вызвать процедуру закрытия, определяемую типом драй- | | вера и передать ей в качестве параметра младший но- | | мер устройства; | | сделать недействительными блоки устройства, оставшие-| | ся в буферном кеше; | | } | | finish: | | освободить индекс; | | } | +------------------------------------------------------------+ Рисунок 10.4. Алгоритм закрытия устройства ---------------------------------------- (**) На практике вывод на печать обычно управляется специальными процессами буферизации, и права доступа устанавливаются таким образом, чтобы толь- ко система буферизации могла обращаться к принтеру. 10.1.2.2 Close Процесс разрывает связь с открытым устройством, закрывая его. Однако, ядро запускает определяемую типом устройства процедуру close только в пос- леднем вызове функции close для этого устройства, и то только если не оста- лось процессов, которым устройство необходимо открытым, поскольку процедура закрытия устройства завершается разрывом аппаратного соединения; отсюда яс- но, что ядру следует подождать, пока не останется ни одного процесса, обра- щающегося к устройству. Поскольку ядро запускает процедуру открытия устройс- тва при каждом вызове системной функции open, а процедуру закрытия только один раз, драйверу устройства неведомо, сколько процессов используют устрой- ство в данный момент. Драйверы могут легко выйти из строя, если при их напи- сании не соблюдалась осторожность: когда при выполнении процедуры close они приостанавливают свою работу и какой-нибудь процесс открывает устройство до того, как завершится процедура закрытия, устройство может стать недоступным для работы, если в результате комбинации вызовов open и close сложилась не- распознаваемая ситуация. Алгоритм закрытия устройства похож на алгоритм закрытия файла обычного типа (Рисунок 10.4). Однако, до того, как ядро освобождает индекс, в нем вы- полняются действия, специфичные для файлов устройств. 1. Просматривается таблица файлов для того, чтобы убедиться в том, что ни одному из процессов не требуется, чтобы устройство было открыто. Чтобы установить, что вызов функции close для устройства является последним, недостаточно положиться на значение счетчика ссылок в таблице файлов, поскольку несколько процессов могут обращаться к одному и тому же уст- ройству, используя различные точки входа в таблице файлов. Так же недос- таточно положиться на значение счетчика в таблице индексов, поскольку одному и тому же устройству могут соответствовать несколько файлов уст- ройства. Например, команда ls -l покажет, что одному и тому же устройст- ву символьного типа ("c" в начале строки) соответствуют два файла уст- ройства, старший и младший номера у которых (9 и 1) совпадают. Значение счетчика связей для каждого файла, равное 1, говорит о том, что имеетс два индекса. crw--w--w- 1 root vis 9, 1 Aug 6 1984 /dev/tty01 crw--w--w- 1 root unix 9, 1 May 3 15:02 /dev/tty01 Если процессы открывают оба файла независимо один от другого, они обра- тятся к разным индексам одного и того же устройства. 2. Если устройство символьного типа, ядро запускает процедуру закрытия уст- ройства и возвращает управление в режим задачи. Если устройство блочного типа, ядро просматривает таблицу результатов монтирования и проверяет, не располагается ли на устройстве смонтированная файловая система. Если такая система есть, ядро не сможет запустить процедуру закрытия устройс- тва, поскольку не был сделан последний вызов функции close для устройст- ва. Даже если на устройстве нет смонтированной файловой системы, в бу- ферном кеше еще могут находиться блоки с данными, оставшиеся от смонти- рованной ранее файловой системы и не переписанные на устройство, пос- кольку имели пометку "отложенная запись". Поэтому ядро просматривает бу- ферный кеш в поисках таких блоков и переписывает их на устройство перед запуском процедуры закрытия устройства. После закрытия устройства ядро вновь просматривает буферный кеш и делает недействительными все буферы, которые содержат блоки для только что закрытого устройства, в то же вре- мя позволяя буферам с актуальной информацией остаться в кеше. 3. Ядро освобождает индекс файла устройства. Короче говоря, процедура закрытия устройства разрывает связь с устройст- вом и инициализирует заново информационные структуры драйвера и аппаратную часть устройства с тем, чтобы ядро могло бы позднее открыть устройство вновь. 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. Процесс, открывающий файлы, получает доступ к устройству через таблицу клю- чей устройств ввода-вывода блоками и таблицу ключей устройств посимвольного ввода-вывода, соответственно, а младший номер устройства 21 информирует драйвер о том, к какому разделу диска производится обращение, например, дис- ковод 2, раздел 1. Поскольку младшие номера у файлов совпадают, они ссылают- ся на один и тот же раздел диска, если предположить, что это одно устройство (***). Таким образом, процесс, выполняющий программу, открывает один и тот --------------------------------------- (***) Не существует иного способа установить, что символьный и блочный драй- веры ссылаются на одно и то же устройство, кроме просмотра таблиц сис- темной конфигурации и текста программ драйвера. же драйвер дважды (используя различные интерфейсы), позиционирует головку к смещению с адресом 8192 и считывает данные с этого места. Результаты выпол- нения операций чтения должны быть идентичными при условии, что работает только одна файловая система. +------------------------------------------------------------+ | #include "fcntl.h" | | main() | | { | | char buf1[4096], buf2[4096] | | int fd1, fd2, i; | | | | if (((fd1 = open("/dev/dsk5/", O_RDONLY)) == -1) || | | ((fd2 = open("/dev/rdsk5", O_RDONLY)) == -1))| | { | | printf("ошибка при открытии\n"); | | exit(); | | } | | | | lseek(fd1, 8192L, 0); | | lseek(fd2, 8192L, 0); | | | | if ((read(fd1, buf1, sizeof(buf1)) == -1) || | | (read(fd2, buf2, sizeof(buf2)) == -1)) | | { | | printf("ошибка при чтении\n"); | | exit(); | | } | | | | for (i = 0; i < sizeof(buf1); i++) | | if (buf1[i] != buf2[i]) | | { | | printf("различие в смещении %d\n", i); | | exit(); | | } | | printf("данные совпадают\n"); | | } | +------------------------------------------------------------+ Рисунок 10.8. Чтение данных с диска с использованием блочного интерфейса и без структурирования данных Программы, осуществляющие чтение и запись на диск непосредственно, пред- ставляют опасность, поскольку манипулируют с чувствительной информацией, рискуя нарушить системную защиту. Администраторам следует защищать интерфей- сы ввода-вывода путем установки прав доступа к файлам дисковых устройств. Например, дисковые файлы "/dev/dsk15" и "/dev/rdsk15" должны принадлежать пользователю с именем "root", и права доступа к ним должны быть определены таким образом, чтобы пользователю "root" было разрешено чтение, а всем ос- тальным пользователям и чтение, и запись должны быть запрещены. Программы, осуществляющие чтение и запись на диск непосредственно, могут также нарушить целостность данных в файловой системе. Алгоритмы файловой системы, рассмотренные в главах 3, 4 и 5, координируют выполнение операций ввода-вывода, связанных с диском, тем самым поддерживая целостность информа- ционных структур на диске, в том числе списка свободных дисковых блоков и указателей из индексов на информационные блоки прямой и косвенной адресации. Процессы, обращающиеся к диску непосредственно, обходят эти алгоритмы. Пусть даже их программы написаны с большой осторожностью, проблема целостности все равно не исчезнет, если они выполняются параллельно с работой другой файло- вой системы. По этой причине программа fsck не должна выполняться при нали- чии активной файловой системы. Два типа дискового интерфейса различаются между собой по использованию буферного кеша. При работе с блочным интерфейсом ядро пользуется тем же ал- горитмом, что и для файлов обычного типа, исключение составляет тот момент, когда после преобразования адреса смещения логического байта в адрес смеще- ния логического блока (см. алгоритм bmap в главе 4) оно трактует адрес сме- щения логического блока как физический номер блока в файловой системе. За- тем, используя буферный кеш, ядро обращается к данным, и, в конечном итоге, к стратегическому интерфейсу драйвера. Однако, при обращении к диску через символьный интерфейс (без структурирования данных), ядро не превращает адрес смещения в адрес файла, а передает его немедленно драйверу, используя дл передачи рабочее пространство задачи. Процедуры чтения и записи, входящие в состав драйвера, преобразуют смещение в байтах в смещение в блоках и копиру- ют данные непосредственно в адресное пространство задачи, минуя буферы ядра. Таким образом, если один процесс записывает на устройство блочного типа, а второй процесс затем считывает с устройства символьного типа по тому же адресу, второй процесс может не считать информацию, записанную первым про- цессом, так как информация может еще находиться в буферном кеше, а не на диске. Тем не менее, если второй процесс обратится к устройству блочного ти- па, он автоматически попадет на новые данные, находящиеся в буферном кеше. При использовании символьного интерфейса можно столкнуться со странной ситуацией. Если процесс читает или пишет на устройство посимвольного вво- да-вывода порциями меньшего размера, чем, к примеру, блок, результаты будут зависеть от драйвера. Например, если производить запись на ленту по 1 байту, каждый байт может попасть в любой из ленточных блоков. Преимущество использования символьного интерфейса состоит в скорости, если не возникает необходимость в кешировании данных для дальнейшей работы. Процессы, обращающиеся к устройствам ввода -вывода блоками, передают инфор- мацию блоками, размер каждого из которых ограничивается размером логического блока в данной файловой системе. Например, если размер логического блока в файловой системе 1 Кбайт, за одну операцию ввода-вывода может быть передано не больше 1 Кбайта информации. При этом процессы, обращающиеся к диску с по- мощью символьного интерфейса, могут передавать за одну дисковую операцию множество дисковых блоков, в зависимости от возможностей дискового контрол- лера. С функциональной точки зрения, процесс получает тот же самый резуль- тат, но символьный интерфейс может работать гораздо быстрее. Если воспользо- ваться примером, приведенным на Рисунке 10.8, можно увидеть, что когда про- цесс считывает 4096 байт, используя блочный интерфейс для файловой системы с размером блока 1 Кбайт, ядро производит четыре внутренние итерации, на каж- дом шаге обращаясь к диску, прежде чем вызванная системная функция возвраща- ет управление, но когда процесс использует символьный интерфейс, драйвер мо- жет закончить чтение за одну дисковую операцию. Более того, использование блочного интерфейса вызывает дополнительное копирование данных между адрес- ным пространством задачи и буферами ядра, что отсутствует в символьном ин- терфейсе. 10.3 ТЕРМИНАЛЬНЫЕ ДРАЙВЕРЫ Терминальные драйверы выполняют ту же функцию, что и остальные драйверы: управление передачей данных от и на терминалы. Однако, терминалы имеют одну особенность, связанную с тем, что они обеспечивают интерфейс пользователя с системой. Обеспечивая интерактивное использование системы UNIX, терминальные драйверы имеют свой внутренний интерфейс с модулями, интерпретирующими ввод и вывод строк. В каноническом режиме интерпретаторы строк преобразуют нест- руктурированные последовательности данных, введенные с клавиатуры, в канони- ческую форму (то есть в форму, соответствующую тому, что пользователь имел в виду на самом деле) прежде, чем послать эти данные принимающему процессу; строковый интерфейс также преобразует неструктурированные последовательности выходных данных, созданных процессом, в формат, необходимый пользователю. В режиме без обработки строковый интерфейс передает данные между процессами и терминалом без каких-либо преобразований. Программисты, например, работают на клавиатуре терминала довольно быст- ро, но с ошибками. На этот случай терминалы имеют клавишу стирания ("erase"; клавиша может быть обозначена таким образом), чтобы пользователь имел воз- можность стирать часть введенной строки и вводить коррективы. Терминалы пе- ресылают машине всю введенную последовательность, включая и символы стирани (*** *). В каноническом режиме строковый интерфейс буферизует информацию в строки (набор символов, заканчивающийся символом возврата каретки (*****)) и процессы стирают символы у себя, прежде чем переслать исправленную последо- вательность считывающему процессу. В функции строкового интерфейса входят: * построчный разбор введенных последовательностей; * обработка символов стирания; * обработка символов "удаления", отменяющих все остальные символы, введен- ные до того в текущей строке; * отображение символов, полученных терминалом; * расширение выходных данных, например, преобразование символов табуляции в последовательности пробелов; * сигнализирование процессам о зависании терминалов и прерывании строк или в ответ на нажатие пользователем клавиши удаления; * предоставление возможности не обрабатывать специальные символы, такие как символы стирания, удаления и возврата каретки. Функционирование без обработки подразумевает использование асинхронного терминала, поскольку процессы могут считывать символы в том виде, в каком они были введены, вместо того, чтобы ждать, когда пользователь нажмет клави- шу ввода или возврата каретки. Ричи отметил, что первые строковые интерфейсы, используемые еще при раз- работке системы в начале 70-х годов, работали в составе программ командного процессора и редактора, но не в ядре (см. [Ritchie 84], стр.1580). Однако, поскольку в их функциях нуждается множество программ, их место в составе яд- ра. Несмотря на то, что строковый интерфейс выполняет такие функции, из ко- торых логически вытекает его место между терминальным драйвером и остальной частью ядра, ядро не запускает строковый интерфейс иначе, чем через терми- нальный драйвер. На Рисунке 10.9 показаны поток данных, проходящий через терминальный драйвер и строковый интерфейс, и соответствующие ему управляю- щие воздействия, проходящие через терминальный драйвер. Пользователи могут указать, какой строковый интерфейс используется посредством вызова системной функции ioctl, но реализовать схему, по которой одно устройство использовало бы несколько строковых интерфейсов одновременно, при чем каждый интерфейсный модуль, в свою очередь, успешно вызывал бы следующий модуль для обработки данных, довольно трудно. 10.3.1 Символьные списки Строковый интерфейс обрабатывает данные в символьных списках. Символьный список (clist) представляет собой переменной длины список символьных блоков с использованием указателей и с подсчетом количества символов в списке. Сим- --------------------------------------- (****) В этом разделе рассматривается использование терминалов ввода-вывода, которые передают все символы, введенные пользователем, без обработки. (*****) В данной главе используется общий термин "возврат каретки" для обоз- начения символов возврата каретки и перевода строки. Поток данных Поток управляющих воздействий +-----------------------+ +-----------------------+ | Процесс чтения/записи | | Процесс чтения/записи | +-----------------------+ +-----------------------+ | ^ | ^ v | v | +---------------------+ +-----------------------+ вывод | Строковый интерфейс| ввод | Терминальный драйвер | +---------------------+ +-----------------------+ | ^ | ^ v | v | +-----------------------+ +---------------------+ | Терминальный драйвер | | Строковый интерфейс | +-----------------------+ +---------------------+ | ^ v | +-----------------------+ | Драйвер ввода-вывода | +-----------------------+ | ^ v | +-------------------------+ | Устройство ввода-вывода | +-------------------------+ Рисунок 10.9. Последовательность обращений и поток данных че- рез строковый интерфейс Указатель Смещение Смещение на до до следующий начала конца Массив символов блок 0 1 2 3 4 5 6 7 8 9 14 ----------------------------------------------------------------- | 7 | 14 ||g|a|r|b|a|g|e||| |e|q|n| ||| |... ----+------------------------------------------------------------ | v Рисунок 10.10. Символьный блок вольный блок (cblock) содержит указатель на следующий блок в списке, неболь- шой массив хранимой в символьном виде информации и адреса смещений, показы- вающие место расположения внутри блока корректной информации (Рисунок 10.10). Смещение до начала показывает первую позицию расположения корректной информации в массиве, смещение до конца показывает первую позицию расположе- ния некорректной информации. Ядро обеспечивает ведение списка свободных символьных блоков и выполняет над символьными списками и символьными блоками шесть операций. 1. Ядро назначает драйверу символьный блок из списка свободных символьных блоков. 2. Оно также возвращает символьный блок в список свободных символьных бло- ков. 3. Ядро может выбирать первый символ из символьного списка: оно удаляет первый символ из первого символьного блока в списке и устанавливает зна- чения счетчика символов в списке и указателей в блоке таким образом, чтобы последующие операции не выбирали один и тот же символ. Если в ре- зультате операции выбран последний символ блока, ядро помещает в список свободных символьных блоков пустой блок и переустанавливает указатели в символьном списке. Если в символьном списке отсутствуют символы, ядро возвращает пустой символ. 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 Терминальный драйвер в каноническом режиме Структуры данных, с которыми работают терминальные драйверы, связаны с тремя символьными списками: списком для хранения данных, выводимых на терми- нал, списком для хранения неструктурированных вводных данных, поступивших в результате выполнения программы обработки прерывания от терминала, вызванно- го попыткой пользователя ввести данные с клавиатуры, и списком для хранени обработанных входных данных, поступивших в результате преобразования строко- вым интерфейсом специальных символов (таких как символы стирания и удаления) в неструктурированном списке. Когда процесс ведет запись на терминал (Рисунок 10.13), терминальный драйвер запускает строковый интерфейс. Строковый интерфейс в цикле считывает символы из адресного пространства процесса и помещает их в символьный список для хранения выводных данных до тех пор, пока поток данных не будет исчер- пан. Строковый интерфейс обрабатывает выводимые символы, например, замен символы табуляции на последовательности пробелов. Если количество символов в списке для хранения выводных данных превысит верхнюю отметку, строковый ин- терфейс вызывает процедуры драйвера, пересылающие данные из символьного списка на терминал и после этого приостанавливающие выполнение процесса, ве- +------------------------------------------------------------+ | алгоритм terminal_write | | { | | выполнить (пока из пространства задачи еще поступают | | данные) | | { | | если (на терминал поступает информация) | | { | | приступить к выполнению операции записи данных | | из списка, хранящего выводные данные; | | приостановиться (до того момента, когда терми- | | нал будет готов принять следующую порцию дан- | | ных); | | продолжить; /* возврат к началу цикла */ | | } | | скопировать данные в объеме символьного блока из | | пространства задачи в список, хранящий выводные | | данные: строковый интерфейс преобразует символы | | табуляции и т.д.; | | } | | | | приступить к выполнению операции записи данных из спис-| | ка, хранящего выводные данные; | | } | +------------------------------------------------------------+ Рисунок 10.13. Алгоритм переписи данных на терминал дущего запись. Когда объем информации в списке для хранения выводных данных падает за нижнюю отметку, программа обработки прерываний возобновляет выпол- нение всех процессов, приостановленных до того момента, когда терминал смо- жет принять следующую порцию данных. Строковый интерфейс завершает цикл об- работки, скопировав всю выводимую информацию из адресного пространства зада- чи в соответствующий символьный список, и вызывает выполнение процедур драй- вера, пересылающих данные на терминал, о которых уже было сказано выше. Если на терминал ведут запись несколько процессов, они независимо друг от друга следуют указанной процедуре. Выводимая информация может быть иска- жена; то есть на терминале данные, записываемые процессами, могут пересе- каться. Это может произойти из-за того, что процессы ведут запись на терми- нал, используя несколько вызовов системной функции write. Ядро может перек- лючать контекст, пока процесс выполняется в режиме задачи, между последова- тельными вызовами функции write, и вновь запущенные процессы могут вести за- пись на терминал, пока первый из процессов приостановлен. Выводимые данные могут быть также искажены и на терминале, поскольку процесс может приостано- виться на середине выполнения системной функции write, ожидая завершения вы- вода на терминал из системы предыдущей порции данных. Ядро может запустить другие процессы, которые вели запись на терминал до того, как первый процесс был повторно запущен. По этой причине, ядро не гарантирует, что содержимое буфера данных, выводимое в результате вызова системной функции write, поя- вится на экране терминала в непрерывном виде. Рассмотрим программу, приведенную на Рисунке 10.14. Родительский процесс создает до 18 порожденных процессов; каждый из порожденных процессов записы- вает строку (с помощью библиотечной функции sprintf) в массив output, кото- рый включает сообщение и значение счетчика i в момент выполнения функции fork, и затем входит в цикл пошаговой переписи строки в файл стандартного вывода. Если стандартным выводом является терминал, терминальный драйвер ре- гулирует поток поступающих данных. Выводимая строка имеет более 64 символов +----------------------------------------------------------------+ | char form[]="это пример вывода строки из порожденного процесса"| | | main() | | { | | char output[128]; | | int i; | | | | for (i = 0; i < 18; i++) | | { | | switch (fork()) | | { | | case -1: /* ошибка --- превышено максимальное чис-| | ло процессов */ | | exit(); | | | | default: /* родительский процесс */ | | break; | | | | case 0: /* порожденный процесс */ | | /* формат вывода строки в переменной output */ | | sprintf(output,"%%d\n%s%d\n",form,i,form,i); | | for (;;) | | write(1,output,sizeof(output)); | | } | | } | | } | +----------------------------------------------------------------+ Рисунок 10.14. Передача данных через стандартный вывод в длину, то есть слишком велика для того, чтобы поместиться в символьном блоке (длиной 64 байта) в версии V системы. Следовательно, терминальному драйверу требуется более одного символьного блока для каждого вызова функции write, иначе выводной поток может стать искаженным. Например, следующие строки были частью выводного потока, полученного в результате выполнени программы на машине AT&T 3B20: this is a sample output string from child 1 this is a sample outthis is a sample output string from child 0 Чтение данных с терминала в каноническом режиме более сложная операция. В вызове системной функции read указывается количество байт, которые процесс хочет считать, но строковый интерфейс выполняет чтение по получении символа перевода каретки, даже если количество символов не указано. Это удобно с практической точки зрения, так как процесс не в состоянии предугадать, сколько символов пользователь введет с клавиатуры, и, с другой стороны, не имеет смысла ждать, когда пользователь введет большое число символов. Напри- мер, пользователи вводят командные строки для командного процессора shell и ожидают ответа shell'а на команду по получении символа возврата каретки. При этом нет никакой разницы, являются ли введенные строки простыми командами, такими как "date" или "who", или же это более сложные последовательности ко- манд, подобные следующей: pic file* | tbl | eqn | troff -mm -Taps | apsend Терминальный драйвер и строковый интерфейс ничего не знают о синтаксисе командного процессора shell, и это правильно, поскольку другие программы, которые считывают информацию с терминалов (например, редакторы), имеют раз- личный синтаксис команд. Поэтому строковый интерфейс выполняет чтение по по- лучении символа возврата каретки. На Рисунке 10.15 показан алгоритм чтения с терминала. Предположим, что терминал работает в каноническом режиме; в разделе 10.3.3 будет рассмотрена работа в режиме без обработки. Если в настоящий момент в любом из символьных списков для хранения вводной информации отсутствуют данные, процесс, выпол- +------------------------------------------------------------+ | алгоритм terminal_read | | { | | если (в каноническом символьном списке отсутствуют дан- | | ные) | | { | | выполнить (пока в списке для неструктурированных | | вводных данных отсутствует информация) | | { | | если (терминал открыт с параметром "no delay" | | (без задержки)) | | возвратить управление; | | если (терминал в режиме без обработки с использо-| | ванием таймера и таймер не активен) | | предпринять действия к активизации таймера | | (таблица ответных сигналов); | | приостановиться (до поступления данных с термина-| | ла); | | } | | | | /* в списке для неструктурированных вводных данных | | есть информация */ | | если (терминал в режиме без обработки) | | скопировать все данные из списка для неструктури-| | рованных вводных данных в канонический список; | | в противном случае /* терминал в каноническом ре- | | жиме */ | | { | | выполнить (пока в списке для неструктурированных | | вводных данных есть символы) | | { | | копировать по одному символу из списка для | | неструктурированных вводных данных в кано- | | нический список: | | выполнить обработку символов стирания и уда-| | ления; | | если (символ - "возврат каретки" или "конец | | файла") | | прерваться; /* выход из цикла */ | | } | | } | | } | | | | выполнить (пока в каноническом списке еще есть символы | | и не исчерпано количество символов, указанное в вызове | | функции read) | | копировать символы из символьных блоков канонического| | списка в адресное пространство задачи; | | } | +------------------------------------------------------------+ Рисунок 10.15. Алгоритм чтения с терминала няющий чтение, приостанавливается до поступления первой строки данных. Когда данные поступают, программа обработки прерывания от терминала запускает "программу обработки прерывания" строкового интерфейса, которая помещает данные в список для хранения неструктурированных вводных данных для передачи процессам, осуществляющим чтение, и в список для хранения выводных данных, передаваемых в качестве эхосопровождения на терминал. Если введенная строка содержит символ возврата каретки, программа обработки прерывания возобновля- ет выполнение всех приостановленных процессов чтения. Когда процесс, осущес- твляющий чтение, выполняется, драйвер выбирает символы из списка для хране- ния неструктурированных вводных данных, обрабатывает символы стирания и уда- ления и помещает символы в канонический символьный список. Затем он копирует строку символов в адресное пространство задачи до символа возврата каретки или до исчерпания числа символов, указанного в вызове системной функции read, что встретится раньше. Однако, процесс может обнаружить, что данных, ради которых он возобновил свое выполнение, больше не существует: другие процессы считали данные с терминала и удалили их из списка для неструктури- рованных вводных данных до того, как первый процесс был запущен вновь. Така ситуация похожа на ту, которая имеет место, когда из канала считывают данные несколько процессов. Обработка символов в направлении ввода и в направлении вывода асиммет- рична, что видно из наличия двух символьных списков для ввода и одного - дл вывода. Строковый интерфейс выводит данные из пространства задачи, обрабаты- вает их и помещает их в список для хранения выводных данных. Для симметрии следовало бы иметь только один список для вводных данных. Однако, в таком случае потребовалось бы использование программы обработки прерываний для интерпретации символов +--------------------------------------------------------------+ | char input[256]; | | | | main() | | { | | register int i; | | | | for (i = 0; i < 18; i++) | | { | | switch (fork()) | | { | | case -1: /* ошибка */ | | printf("операция fork не выполнена из-за ошибки\n");| | exit(); | | | | default: /* родительский процесс */ | | break; | | | | case 0: /* порожденный процесс */ | | for (;;) | | { | | read(0,input,256); /* чтение строки */ | | printf("%d чтение %s\n",i,input); | | } | | } | | } | | } | +--------------------------------------------------------------+ Рисунок 10.16. Конкуренция за данные, вводимые с терминала стирания и удаления, что сделало бы процедуру более сложной и длительной и запретило бы возникновение других прерываний на все критическое время. Ис- пользование двух символьных списков для ввода подразумевает, что программа обработки прерываний может просто сбросить символы в список для неструктури- рованных вводных данных и возобновить выполнение процесса, осуществляющего чтение, который собственно и возьмет на себя работу по интерпретации вводных данных. При этом программа обработки прерываний немедленно помещает введен- ные символы в список для хранения выводных данных, так что пользователь ис- пытывает лишь минимальную задержку при просмотре введенных символов на тер- минале. На Рисунке 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 приказывает драйверу извлечь установки и сохранить их в структуре с именем savetty в ад- ресном пространстве задачи. Эта команда часто используется для того, чтобы определить, является ли файл терминалом или нет, поскольку она ничего не из- меняет в системе: если она завершается неудачно, процессы предполагают, что файл не является терминалом. Здесь же, процесс вторично вызывает функцию ioctl для того, чтобы перевести терминал в режим без обработки: он отключает эхо-сопровождение ввода символов и готовится к выполнению операций чтения с +----------------------------------------------------------------+ | #include | | #include | | struct termio savetty; | | main() | | { | | extern sigcatch(); | | struct termio newtty; | | int nrd; | | char buf[32]; | | signal(SIGINT,sigcatch); | | if (ioctl(0,TCGETA,&savetty) == -1) | | { | | printf("ioctl завершилась неудачно: нет терминала\n"); | | exit(); | | } | | newtty = savetty; | | newtty.c_lflag &= ~ICANON;/* выход из канонического режима */| | newtty.c_lflag &= ~ECHO; /* отключение эхо-сопровождения*/ | | newtty.c_cc[VMIN] = 5; /* минимум 5 символов */ | | newtty.c_cc[VTIME] = 100; /* интервал 10 секунд */ | | if (ioctl(0,TCSETAF,&newtty) == -1) | | { | | printf("не могу перевести тер-л в режим без обработки\n");| | exit(); | | } | | for(;;) | | { | | nrd = read(0,buf,sizeof(buf)); | | buf[nrd] = 0; | | printf("чтение %d символов '%s'\n",nrd,buf); | | } | | } | | sigcatch() | | { | | ioctl(0,TCSETAF,&savetty); | | exit(); | | } | +----------------------------------------------------------------+ Рисунок 10.17. Режим без обработки - чтение 5-символьных блоков +----------------------------------------------------------------+ | #include | | | | main() | | { | | register int i,n; | | int fd; | | char buf[256]; | | | | /* открытие терминала только для чтения с опцией "no delay" */ | | if((fd = open("/dev/tty",O_RDONLY|O_NDELAY)) == -1) | | exit(); | | | | n = 1; | | for(;;) /* всегда */ | | { | | for(i = 0; i < n; i++) | | ; | | | | if(read(fd,buf,sizeof(buf)) > 0) | | { | | printf("чтение с номера %d\n",n); | | n--; | | } | | else | | /* ничего не прочитано; возврат вследствие "no delay" */ | | n++; | | } | | } | +----------------------------------------------------------------+ Рисунок 10.18. Опрос терминала терминала по получении с терминала 5 символов, как минимум, или по прохожде- нии 10 секунд с момента ввода первой порции символов. Когда процесс получает сигнал о прерывании, он сбрасывает первоначальные параметры терминала и за- вершается. 10.3.4 Опрос терминала Иногда удобно производить опрос устройства, то есть считывать с него данные, если они есть, или продолжать выполнять обычную работу - в противном случае. Программа на Рисунке 10.18 иллюстрирует этот случай: после открыти терминала с параметром "no delay" (без задержки) процессы, ведущие чтение с него, не приостановят свое выполнение в случае отсутствия данных, а вернут управление немедленно (см. алгоритм terminal_read, Рисунок 10.15). Этот ме- тод работает также, если процесс следит за множеством устройств: он может открыть каждое устройство с параметром "no delay" и опросить всех из них, ожидая поступления информации с каждого. Однако, этот метод растрачивает вы- числительные мощности системы. В системе BSD есть системная функция select, позволяющая производить оп- рос устройства. Синтаксис вызова этой функции: select(nfds,rfds,wfds,efds,timeout) где nfds - количество выбираемых дескрипторов файлов, а rfds, wfds и efds указывают на двоичные маски, которыми "выбирают" дескрипторы открытых фай- лов. То есть, бит 1 << fd (сдвиг на 1 разряд влево значения дескриптора фай- ла) соответствует установке на тот случай, если пользователю нужно выбрать этот дескриптор файла. Параметр timeout (тайм-аут) указывает, на какое врем следует приостановить выполнение функции select, ожидая поступления данных, например; если данные поступают для любых дескрипторов и тайм-аут не закон- чился, select возвращает управление, указывая в двоичных масках, какие деск- рипторы были выбраны. Например, если пользователь пожелал приостановиться до момента получения данных по дескрипторам 0, 1 или 2, параметр rfds укажет на двоичную маску 7; когда select возвратит управление, двоичная маска будет заменена маской, указывающей, по каким из дескрипторов имеются готовые дан- ные. Двоичная маска wfds выполняет похожую функцию в отношении записи деск- рипторов, а двоичная маска efds указывает на существование исключительных условий, связанных с конкретными дескрипторами, что бывает полезно при рабо- те в сети. 10.3.5 Назначение операторского терминала Операторский терминал - это терминал, с которого пользователь регистри- руется в системе, он управляет процессами, запущенными пользователем с тер- минала. Когда процесс открывает терминал, драйвер терминала открывает стро- ковый интерфейс. Если процесс возглавляет группу процессов как результат вы- полнения системной функции setpgrp и если процесс не связан с одним из опе- раторских терминалов, строковый интерфейс делает открываемый терминал опера- торским. Он сохраняет старший и младший номера устройства для файла термина- ла в адресном пространстве, выделенном процессу, а номер группы процессов, связанной с открываемым процессом, в структуре данных терминального драйве- ра. Открываемый процесс становится управляющим процессом, обычно входным (начальным) командным процессором, что мы увидим далее. Операторский терминал играет важную роль в обработке сигналов. Когда пользователь нажимает клавиши "delete" (удаления), "break" (прерывания), стирания или выхода, программа обработки прерываний загружает строковый ин- терфейс, который посылает соответствующий сигнал всем процессам в группе. Подобно этому, когда пользователь "зависает", программа обработки прерываний от терминала получает информацию о "зависании" от аппаратуры, и строковый интерфейс посылает соответствующий сигнал всем процессам в группе. Таким об- разом, все процессы, запущенные с конкретного терминала, получают сигнал о "зависании"; реакцией по умолчанию для большинства процессов будет выход из программы по получении сигнала; это похоже на то, как при завершении работы пользователя с терминалом из системы удаляются побочные процессы. После по- сылки сигнала о "зависании" программа обработки прерываний от терминала раз- ъединяет терминал с группой процессов, чтобы процессы из этой группы не мог- ли больше получать сигналы, возникающие на терминале. 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 читает данные из линии физического терминала и ждет объявления об управляющих событиях, таких как создание нового окна, переключение управле- ния на другое окно, удаление окна и т.п. Когда mpx получает уведомление о том, что пользователю нужно создать но- вое окно, он создает процесс, управляющий новым окном, и поддерживает связь с ним через псевдотерминал. Псевдотерминал - это программное устройство, ра- ботающее по принципу пары: выходные данные, направляемые к одной составляю- щей пары, посылаются на вход другой составляющей; входные данные посылаютс тому модулю потока, который расположен выше по течению. Для того, чтобы отк- рыть окно (Рисунок 10.24), mpx назначает псевдотерминальную пару и открывает одну из составляющих пары, направляя поток к ней (открытие драйвера служит гарантией того, что псевдотерминальная пара не была выбрана раньше). Mpx ветвится и новый процесс открывает другую составляющую псевдотерминальной +----------------------------------------------------------------+ | /* предположим, что дескрипторы файлов 0 и 1 уже относятся к | | физическому терминалу */ | | для(;;) /* цикл */ | | { | | выбрать(ввод); /* ждать ввода из какой-либо линии */ | | прочитать данные, введенные из линии; | | переключить(линию с вводимыми данными) | | { | | если выбран физический терминал: /* данные вводятся по ли- | | нии физического терми- | | нала */ | | если(считана управляющая команда) /* например, создание | | нового окна */ | | { | | открыть свободный псевдотерминал; | | пойти по ветви нового процесса: | | если(процесс родительский) | | { | | выдвинуть интерфейс сообщений в сторону mpx; | | продолжить; /* возврат в цикл "для" */ | | } | | /* процесс-потомок */ | | закрыть ненужные дескрипторы файлов; | | открыть другой псевдотерминал из пары, выбрать stdin, | | stdout, stderr; | | выдвинуть строковый интерфейс терминала; | | запустить shell; /* подобно виртуальному терминалу */| | } | | /* "обычные" данные, появившиеся через виртуальный терминал */ | | демультиплексировать считывание данных с физического тер-| | минала, снять заголовки и вести запись на соответствую- | | щий псевдотерминал; | | продолжить; /* возврат в цикл "для" */ | | | | если выбран логический терминал: /* виртуальный терминал | | связан с окном */ | | закодировать заголовок, указывающий назначение информации| | окна; | | переписать заголовок и информацию на физический терминал;| | продолжить; /* возврат в цикл "для" */ | | } | | } | +----------------------------------------------------------------+ Рисунок 10.24. Псевдопрограмма мультиплексирования окон пары. Mpx выдвигает модуль управления сообщениями в псевдотерминальный по- ток, чтобы преобразовывать управляющие сообщения в информационные (об этом в следующем параграфе), а порожденный процесс помещает в псевдотерминальный поток модуль строкового интерфейса перед запуском shell'а. Этот shell теперь выполняется на виртуальном терминале; для пользователя виртуальный терминал неотличим от физического. Процесс mpx является мультиплексором, направляющим вывод данных с вирту- альных терминалов на физический терминал и демультиплексирующим ввод данных с физического терминала на подходящий виртуальный. Mpx ждет поступления дан- ных по любой из линий, используя системную функцию select. Когда данные пос- тупают от физического терминала, mpx решает вопрос, являются ли поступившие данные управляющим сообщением, извещающим о необходимости создания нового окна или удаления старого, или же это информационное сообщение, которое не- обходимо разослать процессам, считывающим информацию с виртуального термина- ла. В последнем случае данные имеют заголовок, идентифицирующий тот вирту- альный терминал, к которому они относятся; mpx стирает заголовок с сообщени и переписывает данные в соответствующий псевдотерминальный поток. Драйвер псевдотерминала отправляет данные через строковый интерфейс терминала про- цессам, осуществляющим чтение. Обратная процедура имеет место, когда процесс ведет запись на виртуальный терминал; mpx присоединяет заголовок к данным, информируя физический терминал, для вывода в какое из окон предназначены эти данные. Если процесс вызывает функцию ioctl с виртуального терминала, строковый интерфейс терминала задает необходимые установки терминала для его виртуаль- ной линии; для каждого из виртуальных терминалов установки могут быть раз- личными. Однако, на физический терминал должна быть послана и кое-какая ин- формация, зависящая от типа устройства. Модуль управления сообщениями преоб- разует управляющие сообщения, генерируемые функцией ioctl, в информационные сообщения, предназначенные для чтения и записи их процессом mpx, и эти сооб- щения передаются на физическое устройство. 10.4.2 Анализ потоков Ричи упоминает о том, что им была предпринята попытка создания потоков только с процедурами "вывода" или только с процедурами обслуживания. Однако, процедура обслуживания необходима для управления потоками данных, так как модули должны иногда ставить данные в очередь, если соседние модули на врем закрыты для приема данных. Процедура "вывода" так же необходима, поскольку данные должны иногда доставляться в соседние модули незамедлительно. Напри- мер, строковому интерфейсу терминала нужно вести эхо-сопровождение ввода данных на терминале в темпе с процессом. Системная функция write могла бы запускать процедуру "вывода" для следующей очереди непосредственно, та, в свою очередь, вызывала бы процедуру "вывода" для следующей очереди и так да- лее, не нуждаясь в механизме диспетчеризации. Процесс приостановился бы в случае переполнения очередей для вывода. Однако, со стороны ввода модули не могут приостанавливаться, поскольку их выполнение вызывается программой об- работки прерываний, иначе был бы приостановлен совершенно безобидный про- цесс. Связь между модулями не должна быть симметричной в направлениях ввода и вывода, хотя это и делает схему менее изящной. Также было бы желательно реализовать каждый модуль в виде отдельного процесса, но использование большого количества модулей привело бы к перепол- нению таблицы процессов. Модули наделяются специальным механизмом диспетче- ризации - программным прерыванием, независимым от обычного планировщика про- цессов. По этой причине модули не могут приостанавливать свое выполнение, так как они приостанавливали бы тем самым произвольный процесс (тот, который прерван). Модули должны хранить внутри себя информацию о своем состоянии, что делает лежащие в их основе программы более громоздкими, чем если бы при- остановка выполнения была разрешена. В реализации потоков можно выделить несколько отклонений или несоответс- твий: * Учет ресурсов процесса в потоках затрудняется, поскольку модулям необя- зательно выполняться в контексте процесса, использующего поток. Ошибочно предполагать, что все процессы одинаково используют модули потоков, пос- кольку одним процессам может потребоваться использование сложных сетевых протоколов, тогда как другие могут использовать простые строковые интер- фейсы. * Пользователи имеют возможность переводить терминальный драйвер в режим без обработки, в котором функция read возвращает управление через корот- кий промежуток времени в случае отсутствия данных (например, если newtty.c_cc[VMIN] = 0 на Рисунке 10.17). Эту особенность сложно реализо- вать в потоковой среде без подключения специальной программы на уровне заголовка потока. * Потоки выступают средствами линейной связи и не могут позволить произво- дить с легкостью мультиплексирование на уровне ядра. В примере использо- вания окон, рассмотренном в предыдущем разделе, выполнялось мультиплек- сирование на уровне пользовательского процесса. Несмотря на эти несоответствия, с потоками связываются большие надежды в совершенствовании разработки модулей драйвера. .te1 10.5 ВЫВОДЫ Данная глава представляет собой обзор драйверов устройств в системе UNIX. Устройства могут быть либо блочного, либо символьного типа; интерфейс между устройствами и остальной частью ядра определяется типом устройств. Ин- терфейсом для устройств блочного типа выступает таблица ключей устройств ввода-вывода блоками, состоящая из точек входа, соответствующих процедурам открытия и закрытия устройств и стратегической процедуре. Стратегическа процедура управляет передачей данных от и к устройству блочного типа. Интер- фейсом для устройств символьного типа выступает таблица ключей устройств по- символьного ввода-вывода, которая состоит из точек входа, соответствующих процедурам открытия и закрытия устройства, чтения, записи и процедуре ioctl. Системная функция ioctl использует при обращении к устройствам символьного типа свой собственный интерфейс, который позволяет осуществлять передачу уп- равляющей информации между процессами и устройствами. По получении прерыва- ния от устройства ядро вызывает программу обработки соответствующего преры- вания, опираясь на информацию, хранящуюся в таблице векторов прерываний, и на параметры, сообщенные устройством, от которого поступило прерывание. Дисковые драйверы превращают номера логических блоков, используемые фай- ловой системой, в физические адреса на диске. Блочный интерфейс дает возмож- ность ядру буферизовать данные. Взаимодействие без обработки ускоряет ввод-вывод на диск, но игнорирует буферный кеш, увеличивая тем самым шансы разрушить файловую систему. Терминальные драйверы осуществляют непосредственное взаимодействие с пользователями. Ядро связывает с каждым терминалом три символьных списка, один для неструктурированного ввода с клавиатуры, один для ввода с обработ- кой символов стирания, удаления и возврата каретки и один для вывода. Сис- темная функция ioctl дает процессам возможность следить за тем, как ядро об- рабатывает вводимые данные, переводя терминал в канонический режим или уста- навливая значения различных параметров для режима без обработки символов. Getty-процесс открывает терминальные линии и ждет связи: он формирует группу процессов во главе с регистрационным shell'ом, инициализирует с помощью фун- кции ioctl параметры терминала и обращается к пользователю с предложением зарегистрироваться. Установленный таким образом операторский терминал посы- лает процессам в группе сигналы в ответ на возникновение таких событий, как "зависание" пользователя или нажатие им клавиши прерывания. Потоки выступают средством повышения модульности построения драйверов устройств и протоколов. Поток - это полнодуплексная связь между процессами и драйверами устройств, которая может включать в себя строковые интерфейсы и протоколы для промежуточной обработки данных. Модули потоков характеризуютс четко определенным взаимодействием и гибкостью, позволяющей использовать их в сочетании с другими модулями. Эта гибкость имеет особое значение для сете- вых протоколов и драйверов. .te1 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 становится нелегко обнаружить в таблице соответствующие поля. Разработайте драйвер, нечувствительный к изменениям среды. ГЛАВА 11 ВЗАИМОДЕЙСТВИЕ ПРОЦЕССОВ Наличие механизмов взаимодействия дает произвольным процессам возмож- ность осуществлять обмен данными и синхронизировать свое выполнение с други- ми процессами. Мы уже рассмотрели несколько форм взаимодействия процессов, такие как канальная связь, использование поименованных каналов и посылка сигналов. Каналы (непоименованные) имеют недостаток, связанный с тем, что они известны только потомкам процесса, вызвавшего системную функцию pipe: не имеющие родственных связей процессы не могут взаимодействовать между собой с помощью непоименованных каналов. Несмотря на то, что поименованные каналы позволяют взаимодействовать между собой процессам, не имеющим родственных связей, они не могут использоваться ни в сети (см. главу 13), ни в организа- ции множественных связей между различными группами взаимодействующих процес- сов: поименованный канал не поддается такому мультиплексированию, при кото- ром у каждой пары взаимодействующих процессов имелся бы свой выделенный ка- нал. Произвольные процессы могут также связываться между собой благодаря по- сылке сигналов с помощью системной функции kill, однако такое "сообщение" состоит из одного только номера сигнала. В данной главе описываются другие формы взаимодействия процессов. В на- чале речь идет о трассировке процессов, о том, каким образом один процесс следит за ходом выполнения другого процесса, затем рассматривается пакет IPC: сообщения, разделяемая память и семафоры. Делается обзор традиционных методов сетевого взаимодействия процессов, выполняющихся на разных машинах, и, наконец, дается представление о "гнездах", применяющихся в системе BSD. Вопросы сетевого взаимодействия, имеющие специальный характер, такие как протоколы, адресация и др., не рассматриваются, поскольку они выходят за рамки настоящей работы. 11.1 ТРАССИРОВКА ПРОЦЕССОВ В системе UNIX имеется простейшая форма взаимодействия процессов, ис- пользуемая в целях отладки, - трассировка процессов. Процесс-отладчик, нап- +-------------------------------------------------------+ | if ((pid = fork()) == 0) | | { | | /* потомок - трассируемый процесс */ | | ptrace(0,0,0,0); | | exec("имя трассируемого процесса"); | | } | | /* продолжение выполнения процесса-отладчика */ | | for (;;) | | { | | wait((int *) 0); | | read(входная информация для трассировки команд) | | ptrace(cmd,pid,...); | | if (условие завершения трассировки) | | break; | | } | +-------------------------------------------------------+ Рисунок 11.1. Структура процесса отладки ример sdb, порождает трассируемый процесс и управляет его выполнением с по- мощью системной функции ptrace, расставляя и сбрасывая контрольные точки, считывая и записывая данные в его виртуальное адресное пространство. Трасси- ровка процессов, таким образом, включает в себя синхронизацию выполнени процесса-отладчика и трассируемого процесса и управление выполнением послед- него. Псевдопрограмма, представленная на Рисунке 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 (сигнал прерывания), про- +------------------------------------------------------------+ | #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 (трассирующий процесс) цесс trace переходит в состояние трассировки, ожидая поступления команды от программы debug. Если процесс, реализующий программу debug, находился в сос- тоянии приостанова, связанного с выполнением функции wait, он "пробуждает- ся", обнаруживает наличие порожденного трассируемого процесса и выходит из функции wait. Затем процесс debug вызывает функцию ptrace, записывает значе- ние переменной цикла i в пространство данных процесса trace по адресу, со- держащемуся в переменной addr, и увеличивает значение переменной addr; в программе trace переменная addr хранит адрес точки входа в массив data. Пос- леднее обращение процесса debug к функции ptrace вызывает запуск программы trace, и в этот момент массив data содержит значения от 0 до 31. Отлад- чики, подобные 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 при +------------------------------------------------------------+ | алгоритм msgrcv /* получение сообщения */ | | входная информация: (1) дескриптор сообщения | | (2) адрес массива, в который заносится| | сообщение | | (3) размер массива | | (4) тип сообщения в запросе | | (5) флаги | | выходная информация: количество байт в полученном сообщении| | { | | проверить права доступа; | | loop: | | проверить правильность дескриптора сообщения; | | /* найти сообщение, нужное пользователю */ | | если (тип сообщения в запросе == 0) | | рассмотреть первое сообщение в очереди; | | в противном случае если (тип сообщения в запросе > 0) | | рассмотреть первое сообщение в очереди, имеющее | | данный тип; | | в противном случае /* тип сообщения в запросе < 0 */| | рассмотреть первое из сообщений в очереди с наи- | | меньшим значением типа при условии, что его тип | | не превышает абсолютное значение типа, указанно-| | го в запросе; | | если (сообщение найдено) | | { | | переустановить размер сообщения или вернуть ошиб-| | ку, если размер, указанный пользователем слишком| | мал; | | скопировать тип сообщения и его текст из прост- | | ранства ядра в пространство задачи; | | разорвать связь сообщения с очередью; | | вернуть управление; | | } | | /* сообщений нет */ | | если (флаги не разрешают приостанавливать работу) | | вернуть управление с ошибкой; | | приостановиться (пока сообщение не появится в очере- | | ди); | | перейти на loop; | | } | +------------------------------------------------------------+ Рисунок 11.7. Алгоритм получения сообщени выполнении функции msgget и получает все сообщения ти- па 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, ядро выбирает виртуальный адрес по своему усмотрению. Область разделяемой памяти не должна пересекаться в виртуальном адресном пространстве процесса с другими областями; следовательно, ее выбор должен производиться разумно и осторожно. Так, например, процесс может увеличить размер принадлежащей ему области данных с помощью системной функции brk, и новая область данных будет содержать адреса, смежные с прежней областью; по- этому, ядру не следует присоединять область разделяемой памяти слишком близ- ко к области данных процесса. Так же не следует размещать область разделяе- мой памяти вблизи от вершины стека, чтобы стек при своем последующем увели- чении не залезал за ее пределы. Если, например, стек растет в направлении увеличения адресов, лучше всего разместить область разделяемой памяти непос- редственно перед началом области стека. Ядро проверяет возможность размещения области разделяемой памяти в ад- +------------------------------------------------------------+ | алгоритм shmat /* подключить разделяемую память */ | | входная информация: (1) дескриптор области разделяемой | | памяти | | (2) виртуальный адрес для подключения | | области | | (3) флаги | | выходная информация: виртуальный адрес, по которому область| | подключена фактически | | { | | проверить правильность указания дескриптора, права до- | | ступа к области; | | если (пользователь указал виртуальный адрес) | | { | | округлить виртуальный адрес в соответствии с фла- | | гами; | | проверить существование полученного адреса, размер| | области; | | } | | в противном случае /* пользователь хочет, чтобы ядро | | * само нашло подходящий адрес */ | | ядро выбирает адрес: в случае неудачи выдается | | ошибка; | | присоединить область к адресному пространству процесса | | (алгоритм attachreg); | | если (область присоединяется впервые) | | выделить таблицы страниц и отвести память под нее | | (алгоритм growreg); | | вернуть (виртуальный адрес фактического присоединения | | области); | | } | +------------------------------------------------------------+ Рисунок 11.10. Алгоритм присоединения разделяемой памяти ресном пространстве процесса и присоединяет ее с помощью алгоритма 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 завершалась неудачно, и процесс делал вывод о том, что ресурс уже заблокирован другим процессом. Главные недостатки такого подхода заключались в том, что процесс не знал, в какой момент ему следует предпринять следующую попытку, а также в том, что блокирующие файлы случайно оставались в системе в случае ее аварийного завершения или перезагрузки. Дийкстрой был опубликован алгоритм Деккера, описывающий реализацию сема- форов как целочисленных объектов, для которых определены две элементарные операции: P и V (см. [Dijkstra 68]). Операция P заключается в уменьшении значения семафора в том случае, если оно больше 0, операция V - в увеличении этого значения (и там, и там на единицу). Поскольку операции элементарные, в любой момент времени для каждого семафора выполняется не более одной опера- ции P или V. Связанные с семафорами системные функции являются обобщением операций, предложенных Дийкстрой, в них допускается одновременное выполнение нескольких операций, причем операции уменьшения и увеличения выполняются над значениями, превышающими 1. Ядро выполняет операции комплексно; ни один из посторонних процессов не сможет переустанавливать значения семафоров, пока все операции не будут выполнены. Если ядро по каким-либо причинам не может выполнить все операции, оно не выполняет ни одной; процесс приостанавливает свою работу до тех пор, пока эта возможность не будет предоставлена. Семафор в версии V системы UNIX состоит из следующих элементов: * Значение семафора, * Идентификатор последнего из процессов, работавших с семафором, * Количество процессов, ожидающих увеличения значения семафора, * Количество процессов, ожидающих момента, когда значение семафора станет равным 0. Для создания набора семафоров и получения доступа к ним используетс системная функция semget, для выполнения различных управляющих операций над набором - функция semctl, для работы со значениями семафоров - функци semop. +------------------------------------------------------------+ | #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. Разделение памяти между процессами Таблица семафоров Массивы семафоров +-------+ | | +---------------------------+ | +------->| 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). Каждая структура восстановления состоит из трех элементов - иденти- фикатора семафора, его порядкового номера в наборе и установочного значения. Ядро выделяет структуры восстановления динамически, во время первого вы- полнения системной функции semop с установленным флагом SEM_UNDO. При после- дующих обращениях к функции с тем же флагом ядро просматривает структуры восстановления для процесса в поисках структуры с тем же самым идентификато- Заголовки частных структур восстановления Структуры восстановлени +------+ | | | | | | | | +----------+ +----------+ +----------+ +------| |Дескриптор| |Дескриптор| |Дескриптор| | +-->| Номер +-->| Номер +-->| Номер | +------| | Значение | | Значение | | Значение | | | +----------+ +----------+ +----------+ | | +----------+ +------| |Дескриптор| | +-->| Номер | +------| | Значение | | | +----------+ | | | | | | +------+ Рисунок 11.16. Структуры восстановления семафоров ром и порядковым номером семафора, что и в формате вызова функции. Если структура обнаружена, ядро вычитает значение произведенной над семафором операции из установочного значения. Таким образом, в структуре восстановле- ния хранится результат вычитания суммы значений всех операций, произведенных над семафором, для которого установлен флаг 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. Процесс-сервер возвраща- ется к началу цикла и ждет поступления следующего запроса на подключение. На Рисунке 11.21 показан пример процесса-клиента, ведущего общение с сервером. Клиент создает гнездо в том же домене, что и сервер, и посылает запрос на подключение к гнезду с именем sockname. В результате подключени +------------------------------------------------------------+ | #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" +------------------------------------------------------------+ | #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" процесс-клиент получает виртуальный канал связи с сервером. В рассматривае- мом примере клиент передает одно сообщение и завершается. Если сервер обслуживает процессы в сети, указание о том, что гнездо при- надлежит домену "Internet", можно сделать следующим образом: socket(AF_INET,SOCK_STREAM,0); и связаться с сетевым адресом, полученным от сервера. В системе 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)| | ; | | } | +------------------------------------------------------------+ ГЛАВА 12 МНОГОПРОЦЕССОРНЫЕ СИСТЕМЫ В классической постановке для системы 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 используют блокировку. Механизм установления блокировки: выполнять пока (блокировка установлена) /* операция проверки */ приостановиться (до снятия блокировки); установить блокировку; механизм снятия блокировки: снять блокировку; вывести из состояния приостанова все процессы, приостановленные в ре- зультате блокировки; Процесс A/Процессор A Процесс B/Процессор B +--------------------------------------------------------- | +---------------------------+ | | Блокировка НЕ установлена | | +---------------------------+ | | | Проверяет, установлена Проверяет, установлена | ли блокировка ли блокировка | (нет) (нет) t - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | Устанавливает Устанавливает | блокировку блокировку | | Использует ресурс Использует ресурс v ^ ^ Время | | +------+ +------+ Опасность нарушения целостности Рисунок 12.5. Конкуренция за установку блокировки в многопро- цессорных системах Блокировки такого рода охватывают некоторые критические участки, но не работают в многопроцессорных системах, что видно из Рисунка 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 означает: данное системное условие более не удовлетворяется, следовательно, все приостанов- ленные по условию процессы должны выйти из состояния приостанова. Так, нап- ример, процессы, приостановленные в связи с занятостью буфера, не должны дальше пребывать в этом состоянии, если буфер больше не используется, поэто- му они возоб- новляются ядром. Еще один пример: если несколько процессов выводят данные на терминал с помощью функции write, терминальный драйвер может перевести их в +-------------------------------------------------------+ | 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 состояние приостанова в связи с невозможностью обработки больших объемов ин- формации. Позже, когда драйвер будет готов к приему следующей порции данных, он возобновит все приостановленные им процессы. Использование операций P и V в тех случаях, когда устанавливающие блокировку процессы получают доступ к ресурсу поочередно, а все остальные процессы - в порядке поступления запро- сов, является более предпочтительным. В сравнении с однопроцессорной проце- дурой блокирования (sleep-lock) данная схема обычно выигрывает, так как если при наступлении события все процессы возобновляются, большинство из них мо- жет вновь наткнуться на блокировку и снова перейти в состояние приостанова. С другой стороны, в тех случаях, когда требуется вывести из состояния приос- танова все процессы одновременно, использование операций P и V представляет известную сложность. Если операция возвращает значение семафора, является ли она эквивалент- ной функции wakeup ? while (value(semaphore) < 0) V(semaphore); Если вмешательства со стороны других процессов нет, ядро повторяет цикл до тех пор, пока значение семафора не станет больше или равно 0, ибо это оз- начает, что в состоянии приостанова по семафору нет больше ни одного процес- са. Тем не менее, нельзя исклю- чить и такую возможность, что сразу после того, как процесс A при тестирова- нии семафора на одноименном процессоре обнаружил нулевое значение семафора, процесс B на своем процессоре выполняет операцию P, уменьшая значение сема- фора до -1 (Рисунок 12.10). Процесс A продолжит свое выполнение, думая, что им возобновлены все приостановленные по семафору процессы. Таким образом, цикл выполнения операции не дает гарантии возобновления всех приостановлен- ных процессов, поскольку он не является элементарным. +------------------------------------------------------------+ | алгоритм 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, конкурируют за семафор. Процесс A обнаруживает, что семафор свободен и что процесс B приос- тановлен; значение семафора равно -1. Когда с помощью операции V процесс A освобождает семафор, он выводит тем самым процесс B из состояния приостанова и вновь делает значение семафора нулевым. Теперь предположим, что процесс A, по-прежнему выполняясь в режиме ядра, пытается снова заблокировать семафор. Производя операцию P, процесс приостановится, поскольку семафор имеет нуле- вое значение, несмотря на то, что ресурс пока свободен. Системе придетс "раскошелиться" на дополнительное переключение контекста. С другой стороны, если бы блокировка была реализована на основе однопроцессорной схемы +------------------------------------------------------------+ | алгоритм V /* операция над семафором типа V */ | | входная информация: адрес семафора | | выходная информация: отсутствует | | { | | Pprim(semaphore.lock); | | увеличить (semaphore.value); | | если (semaphore.value <= 0) | | { | | удалить из списка процессов, приостановленных по се-| | мафору, первый по счету процесс; | | перевести его в состояние готовности к запуску; | | } | | Vprim(semaphore.lock); | | } | +------------------------------------------------------------+ Рисунок 12.9. Алгоритм выполнения операции V (sleep-lock), процесс A получил бы право на повторное использование ресурса, поскольку за это время ни один из процессов не смог бы заблокировать его. Для этого случая схема sleep-lock более подходит, чем схема с использованием семафоров. Когда блокируются сразу несколько семафоров, очередность блокировани должна исключать возникновение тупиковых ситуаций. В качестве примера расс- мотрим два семафора, A и B, и два алгоритма, требующих одновременной блоки- ровки семафоров. Если бы алгоритмы устанавливали блокировку на семафоры в обратном порядке, как следует из Рисунка 12.11, последовало бы возникновение тупиковой ситуации; процесс A на одноименном процессоре захватывает семафор SA, в то время как процесс B на своем процессоре захватывает семафор SB. Процесс A пытается захватить и семафор SB, но в результате операции P пере- ходит в состояние приостанова, поскольку значение семафора SB не превышает 0. То же самое происходит с процессом B, когда последний пытается захватить семафор SA. Ни тот, ни другой процессы продолжаться уже не могут. Для предотвращения возникновения подобных ситуаций используются соответ- ствующие алгоритмы обнаружения опасности взаимной блокировки, устанавливаю- щие наличие опасной ситуации и ликвидирующие ее. Тем не менее, использование таких алгоритмов "утяжеляет" ядро. Поскольку число ситуаций, в которых про- цесс должен одновременно захватывать несколько семафоров, довольно ограниче- но, легче было бы реализовать алгоритмы, предупреждающие возникновение тупи- ковых ситуаций еще до того, как они будут иметь место. Если, к примеру, ка- кой-то набор семафоров всегда блокируется в одном и том же порядке, тупико- вая ситуация никогда не возникнет. Но в том случае, когда захвата семафоров в обратном порядке избежать не удается, операция CP предотвратит возникновение тупиковой ситуации (см. Ри- сунок 12.12): если операция завершится неудачно, процесс B освободит свои ресурсы, дабы избежать взаимной блокировки, и позже запустит алгоритм на вы- полнение повторно, скорее всего тогда, когда процесс A завершит работу с ре- сурсом. Чтобы предупредить одновременное обращение процессов к ресурсу, програм- ма обработки прерываний, казалось бы, могла воспользоваться семафором, но из-за того, что она не может приостанавливать свою работу (см. главу 6), ис- пользовать операцию P в этой программе нельзя. Вместо этого можно использо- вать "циклическую блокировку" (spin lock) и не переходить в состояние приос- танова, как в следующем примере: while (! CP(semaphore)); Процесс A/Процессор A Процесс B/Процессор B +----------------------------------------------------------- | +------------------------+ | | Значение семафора = -1 | | +------------------------+ | проверяет(значение сема- | фора < 0) ? | (да) | V(семафор) | | +------------------------+ | | Значение семафора = 0 | | +------------------------+ | проверяет(значение сема- | фора < 0) ? | | P(семафор) | Значение семафора = -1 | | (нет) | НЕВЕРНО !! v Врем Рисунок 12.10. Неудачное имитация функции wakeup при исполь- зовании операции V Операция повторяется в цикле до тех пор, пока значение семафора не превысит 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 теперь пытается захватить хеш-очередь (*). Поскольку очеред- --------------------------------------- (*) Вместо захвата хеш-очереди в этом месте можно было бы установить соот- ветствующий флаг, проверяемый далее перед выполнением операции V, но чтобы проиллюстрировать схему захвата семафоров в обратной последова- тельности, в изложении мы будем придерживаться ранее описанного вариан- та. ность захвата здесь (сначала семафор буфера, потом семафор очереди) обратна вышеуказанной очередности, над семафором выполняется операция CP. Если по- пытка захвата заканчивается неудачей, имеет место обычная обработка, требую- щаяся по ходу задачи. Но если захват удается, ядро не может быть уверено в +------------------------------------------------------------+ | алгоритм getblk /* многопроцессорная версия */ | | входная информация: номер файловой системы | | номер блока | | выходная информация: захваченный буфер, предназначенный для| | обработки содержимого блока | | { | | выполнять (пока буфер не будет обнаружен) | | { | | P(семафор хеш-очереди); | | если (блок находится в хеш-очереди) | | { | | если (операция CP(семафор буфера) завершается не- | | удачно) /* буфер занят */ | | { | | V(семафор хеш-очереди); | | P(семафор буфера); /* приостанов до момен-| | * та освобождения | | */ | | если (операция CP(семафор хеш-очереди) заверша-| | ется неудачно) | | { | | V(семафор буфера); | | продолжить; /* выход в цикл "выполнять" | | */ | | } | | в противном случае если (номер устройства или | | номер блока изменились) | | { | | V(семафор буфера); | | V(семафор хеш-очереди); | | } | | } | | выполнять (пока операция CP(семафор списка свобод-| | ных буферов) не завершится успешно) | | ; /* "кольцевой цикл" */ | | пометить буфер занятым; | | убрать буфер из списка свободных буферов; | | V(семафор списка свободных буферов); | | V(семафор хеш-очереди); | | вернуть буфер; | | } | | в противном случае /* буфер отсутствует в хеш- | | * очереди | | */ | | /* здесь начинается выполнение оставшейся части алго-| | * ритма | | */ | | } | | } | +------------------------------------------------------------+ Рисунок 12.14. Выделение буфера с использованием семафоров том, что захвачен корректный буфер, поскольку содержимое буфера могло быть ранее изменено другим процессом, обнаружившим буфер в списке свободных буфе- ров и захватившим на время его семафор. Процесс A, ожидая освобождения сема- фора, не имеет ни малейшего представления о том, является ли интересующий его буфер тем буфером, который ему нужен, и поэтому прежде всего он должен убедиться в правильности содержимого буфера; если проверка дает отрицатель- ный результат, алгоритм запускается сначала. Если содержимое буфера коррект- но, процесс A завершает выполнение алгоритма. +------------------------------------------------------------+ | многопроцессорная версия алгоритма 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). Разные процессоры могут иметь различную тактовую частоту. Как в этом случае указанные функции должны работать ? ГЛАВА 13 РАСПРЕДЕЛЕННЫЕ СИСТЕМЫ В предыдущей главе нами были рассмотрены сильносвязанные многопроцессор- ные системы с общей памятью, общими структурами данных ядра и общим пулом, из которого процессы вызываются на выполнение. Часто, однако, бывает жела- тельно в целях обеспечения совместного использования ресурсов распределять процессоры таким образом, чтобы они были автономны от операционной среды и условий эксплуатации. Пусть, например, пользователю персональной ЭВМ нужно обратиться к файлам, находящимся на более крупной машине, но сохранить при этом контроль над персональной ЭВМ. Несмотря на то, что отдельные программы, такие как 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. Если производится обращение к системной функции write, периферийный про- цессор формирует сообщение, состоящее из признака функции write, дескриптора файла и объема записываемых данных. Затем из пространства периферийного про- цесса он по линии связи копирует данные процессу-спутнику. Процесс-спутник расшифровывает полученное сообщение, читает данные из линии связи и записы- вает их в соответствующий файл (в качестве указателя на индекс которого и запись о котором в таблице файлов используется содержащийся в сообщении дес- криптор); все указанные действия выполняются на центральном процессоре. По Центральный процессор Периферийный процессор +--------------------------------------+ +---------------------+ | таблица | | | | пользо- | | | | ватель- | | | | ских | | | | дескрип- | | | | таблица таблица торов | | | | индексов файлов файла +--------+| | +---------+ | | +-----+ +-----+ +-----+ |Процесс-| | Процесс | | | | | | | | | |спутник || | +---------+ | | +-----| +-----| +-----| +--------+| | | | | -+-+ | | ++- -+---+ | | | | +-----| | +-----| |+-----| дескрип- | | | | | | +-+- -+-+| | тор файла | | | | +-----| +-----| +-----+ | | | | | | | | | | | | +-----| +-----| | | | | | | | | | | | | +-----+ +-----+ | | | +--------------------------------------+ +---------------------+ Рисунок 13.4. Вызов функции open из периферийного процесса окончании работы процесс-спутник передает периферийному процессу посылку, подтверждающую прием сообщения и содержащую количество байт данных, успешно переписанных в файл. Операция 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 где "/../" - префикс, свидетельствующий о том, что файл удаленный; втора компонента имени файла является именем удаленной машины. В данной схеме ис- Процесс-клиент Процесс-сервер +-----------------------------+ +----------------------------+ | Таблица | | Процесс- | | Си-библиотека открытых | | спутник Запрос | | файлов | | (пользо- на чтение | | +------+ | | вательский | | | | +--------------+--- | | | уровень) | | | | | +------| | | | | | | | локальный | | | | +-------------+ | | | | +------| | | | | | | | +----+--- | | | | | | | | | +------| | | | | | | | | | | | | | | | | | | +------+ | | | | | | | +-----+ | | | | | +----+---------------+--------+ +----+-----------------+-----+ | | удаленный | | +----+---------------+--------+ +----+-----------------+-----+ | | Сетевой | | | Сетевой | | Ядро интерфейс | | Ядро интерфейс | | | | | | | +--------------------+--------+ +----------------------+-----+ | с е т ь | +-------------------------------------+ Рисунок 13.9. Формулирование запросов к файловому серверу (процессору) пользуется привычный синтаксис имен файлов в системе UNIX, поэтому в отличие от первой схемы здесь пользовательским программам нет необходимости прино- равливаться к использованию имен, имеющих необычную конструкцию (см. [Pike 85]). Всю оставшуюся часть раздела мы посвятим рассмотрению модели системы, использующей связь типа 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 процессом не обрабатываютс .te1 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. БИБЛИОГРАФИЯ [Babaoglu 81] Babaoglu, O., and W.Joy, "Converting a Swap-Based System to do Paging in an Architecture Lacking Page-Referenced Bits", Proceedings of the 8th Symposium on Operating Systems Principles, ACM Operating Systems Review, Vol. 15(5), Dec. 1981, pp. 78-86. [Bach 84] Bach, M.J., and S.J.Buroff, "Multiprocessor UNIX Systems", AT&T Bell Laboratories Technical Journal, Oct. 1984, Vol. 63, No. 8, Part 2, pp. 1733-1750. [Barak 80] Barak, A.B. and Aapir, "UNIX with Satellite Processors", Software - Practice and Experience, Vol. 10, 1980, pp. 383-392. [Beck 85] Beck, B. and B.Kasten, "VLSI Assist in Building a Multiprocessor UNIX System", Proceedings of the USENIX Association Summer Conference, June 1985, pp. 255-275. [Berkeley 83] UNIX Programmer's Manual, 4.2 Berkeley Software Distribution, Virtual VAX-11 Version, Computer Science Division, Department of Electrical Engineering and Computer Science, University of California at Berkeley, August 1983. [Birrell 84] Birrell, A.D. and B.J.Nelson, "Implementing Remote Procedure Calls", ACM Transactions on Computer Systems, Vol. 2, No. 1, Feb. 1984, pp. 39-59. [Bodenstab 84] Bodenstab, D.E., T.F.Houghton, K.A.Kelleman, G.Ronkin, and E.P.Schan, "UNIX Operating System Porting Experiences", AT&T Bell Laboratories Technical Journal, Vol. 63, No. 8, Oct. 1984, pp. 1769-1790. [Bourne 78] Bourne, S.R., "The UNIX Shell", The Bell System Technical Journal, July-August 1978, Vol. 57, No. 6, Part 2, pp. 1971-1990. [Bourne 83] Bourne, S.R., The UNIX System, Addison-Wesley, Reading, MA, 1983. [Brownbridge 82] Brownbridge, D.R., L.F.Marshall, and B.Randell, "The Newcastle Connection or UNIXes of the World Unite!" in Software - Practice and Experience, Vol. 12, 1982, pp. 1147-1162. [Bunt 76] Bunt, R.B., "Scheduling Techniques for Operating Systems", Computer, Oct. 1976, pp. 10-17. [Christian 83] Christian, K., The UNIX Operating System, John Wiley & Sons Inc., New York, NY, 1983. [Coffman 73] Coffman, E.G., and P.J.Denning, Operating Systems Theory, Prentice-Hall Inc., Englewood Cliffs, NJ, 1973. [Cole 85] Cole, C.T., P.B.Flinn, and A.B.Atlas, "An Implementation of an Extended File System for UNIX", Proceedings of the USENIX Conference, Summer 1985, pp. 131-149. [Denning 68] Denning, P.J., "The Working Set Model for Program Behavior, Communications of the ACM, Volume 11, No. 5, May 1968, pp. 323-333. [Dijkstra 65] Dijkstra, E.W., "Solution of a Problem in Concurrent Program Control", CACM, Vol. 8, No. 9, Sept. 1965, p. 569. [Dijkstra 68] Dijkstra, E.W., "Cooperating Sequential Processes", in Programming Languages, ed. F.Genuys, Academic Press, New York, NY, 1968. [Felton 84] Felton, W.A., G.L.Miller, and J.M.Milner, "A UNIX Implementation for System/370", AT&T Bell Laboratories Technical Journal, Vol. 63, No. 8, Oct. 1984, pp. 1751- 1767. [Goble 81] Goble, G.H. and M.H.Marsh, "A Dual Processor VAX 11/780", Purdue University Technical Report, TR-EE 81-31, Sept. 1981. [Henry 84] Henry, G.J., "The Fair Share Scheduler", AT&T Bell Laboratories Technical Journal, Oct. 1984, Vol. 63, No. 8, Part 2, pp. 1845-1858. [Holley 79] Holley, L.H., R.P421rmelee, C.A.Salisbury, and D. N.Saul, "VM/370 Asymmetric Multiprocessing", IBM Systems Journal, Vol. 18, No. 1, 1979, pp. 47-70. [Holt 83] Holt, R.C., Concurrent Euclid, the UNIX System, and Tunis, Addison-Wesley, Reading, MA, 1983. [Horning 73] Horning, J.J., and B.Randell, "Process Structuring", Computing Surveys, Vol. 5, No. 1, March 1973, pp. 5-30. [Hunter 84] Hunter, C.B. and E.Farquhar, "Introduction to the NSI16000 Architecture", IEEE Micro, April 1984, pp. 26- 47. [Johnson 78] Johnson, S.C. and D.M.Ritchie, "Portability of C Programs and the UNIX System", The Bell System Technical Journal, Vol. 57, No. 6, Part 2, July-August, 1978, pp. 2021-2048. [Kavaler 83] Kavaler, P. and A.Greenspan, "Extending UNIX to Local-Area Networks", Mini-Micro Systems, Sept. 1983, pp. 197-202. [Kernighan 78] Kernighan, B.W., and D.M.Ritchie, The C Programming Language, Prentice-Hall, Englewood Cliffs, NJ, 1978. [Kernighan 84] Kernighan, B.W., and R.Pike, The UNIX Programming Environment, Prentice-Hall, Englewood Cliffs, NJ, 1984. [Killian 84] Killian, T.J., "Processes as Files", Proceedings of the USENIX Conference, Summer 1984, pp. 203-207. [Levy 80] Levy, H.M., and R.H.Eckhouse, Computer Programming and Architecture: The VAX-11, Digital Press, Bedford, MA, 1980. [levy 82] Levy, H.M., and P.H.Lipman, "Virtual Memory Management in the VAX/VMS Operating System", Computer, Vol. 15, No. 3, March 1982, pp. 35-41. [Lu 83] Lu, P.M., W.A.Dietrich, et. al., "Architecture of a VLSI MAP for BELLMAC-32 Microprocessor", Proc. of IEEE Spring Compcon, Feb. 28, 1983, pp. 213-217. [Luderer 81] Luderer, G.W.R., H.Che, J.P.Haggerty, P.A.Kirslis, and W.T.Marshall, "A Distributed UNIX System Based on a Virtual Circuit Switch", Proceedings of the Eighth Symposium on Operating Systems Principles, Asilomar, California, December 14-16, 1981. [Lycklama 78a] Lycklama, H. and D.L.Bayer, "The MERT Operating System", The Bell System Technical Journal, Vol. 57, No. 6, Part 2, July-August 1978, pp. 2049-2086. [Lycklama 78b] Lycklama, H. and C.Christensen, "A Minicomputer Satellite Processor System", The Bell System Technical Journal, Vol. 57, No. 6, Part 2, July- August 1978, pp. 2103-2114. [McKusick 84] McKusick, M.K., W.N.Joy, S.J.Leffler, and R.S. Fabry, "A Fast File System for UNIX", ACM Transactions on Computer Systems, Vol. 2(3), August 1984, pp. 181-197. [Mullender 84] Mullender, S.J. and A.S.Tanenbaum, "Immediate Files", Software - Practice and Experience, Vol. 14(4), April 1984, pp. 365-368. [Nowitz 80] Nowitz, D.A. and M.E.Lesk, "Implementation of a Dial-Up Network of UNIX Systems", IEEE Proceedings of Fall 1980 COMPCON, Washington, D.C., pp. 483-486. [Organick 72] Organick, E.J., The Multics System: An Examination of Its Structure", The MIT Press, Cambridge, MA, 1972. [Peachey 84] Peachey, D.R., R.B.Bunt, C.L.Williamson, and T.B.Brecht, "An Experimental Investigation of Scheduling Strategies for UNIX", Performance Evaluation Review, 1984 SIGMETRICS Conference on Measurement and Evaluation of Computer Systems, Vol. 12(3), August 1984, pp. 158-166. [Peterson 83] Peterson, James L. and A.Silberschatz, Operating System Concepts, Addison-Wesley, Reading, MA, 1983. [Pike 84] Pike, R., "The Blit: A Multiplexed Graphics Terminal", AT&T Bell Laboratories Technical Journal, Oct. 1984, Vol. 63, No. 8, Part 2, pp. 1607-1632. [Pike 85] Pike, R., and P.Weinberger, "The Hideous Name", Proceedings of the USENIX Conference, Summer 1985, pp. 563-568. [Postel 80] Postel, J. (ed.), "DOD Standart Transmission Control Protocol", ACM Computer Communication Review, Vol. 10, No. 4, Oct. 1980, pp. 52-132. [Postel 81] Postel, J., C.A.Sunshine, and D.Cohen, "The ARPA Internet Protocol", Computer Networks, Vol. 5, No. 4, July 1981, pp. 261-271. [Raleigh 76] Raleigh, T.M., "Introduction to Scheduling and Switching under UNIX", Proceedings of the Digital Equipment Computer Users Society, Atlanta, Ga., May 1976, pp. 867-877. [Richards 69] Richards, M., "BCPL: A Tool for Compiler Writing and Systems Programming", Proc. AFIPS SJCC 34, 1969, pp. 557-566. [Ritchie 78a] Ritchie, D.M. and K.Thompson, "The UNIX Time-Sharing System", The Bell System Technical Journal, July-August 1978, Vol. 57, No. 6, Part 2, pp. 1905-1930. [Ritchie 78b] Ritchie, D.M., "A Retrospective", The Bell System Technical Journal, July-August 1978, Vol. 57, No. 6, Part 2, pp. 1947-1970. [Ritchie 81] Ritchie, D.M. and K.Thompson, "Some Further Aspects of the UNIX Time-Sharing System", Mini-Micro Software, Vol. 6, No. 3, 1981, pp. 9-12. [Ritchie 84a] Ritchie, D.M., "The Evolution of the UNIX Time- sharing System", AT&T Bell Laboratories Technical Journal, Oct. 1984, Vol. 63, No. 8, Part 2, pp. 1577-1594. [Ritchie 84b] Ritchie, D.M., "A Stream Input Output System", AT&T Bell Laboratories Technical Journal, Oct. 1984, Vol. 63, No. 8, Part 2, pp. 1897-1910. [Rochkind 85] Rochkind, M.J., Advanced UNIX Programming, Prentice-Hall, 1985. [Saltzer 66] Saltzer, J.H., Traffic Control in a Multiplexed Computer System, Ph.D. Thesis, MIT, 1966. [Sandberg 85] Sandberg, R., D.Goldberg, S.Kleiman, D.Walsh, and B.Lyon, "Design and Implementation of the Sun Network Filesystem", Proceedings of the USENIX Conference, Summer 1985, pp. 119-131. [SVID 85] System V Interface Definition, Spring 1985, Issue 1, AT&T Customer Information Center, Indianapolis, IN. [System V 84a] UNIX System V User Reference Manual. [System V 84b] UNIX System V Administrator's Manual. [Thompson 74] Thompson, K. and D.M.Ritchie, "The UNIX Time-Sharing System", Communications of the ACM, Vol. 17, No. 7, July, 1974, pp. 365-375 (исправлено и перепечатано в [Ritchie 78a]). [Thompson 78] Thompson, K., "UNIX Implementation", The Bell System Technical Journal, Vol. 57, No. 6, Part 2, July- August, 1978, pp. 1931-1946. [Weinberger 84] Weinberger, P.J., "Cheap Dynamic Instruction Counting", The AT&T Bell Laboratories Technical Journal, Vol. 63, No. 6, Part 2, October 1984, pp. 1815-1826. "VM/370 Asymmetric Multiprocessing", IBM Systems Journal, Vol. 18, No. 1, 1979, pp. 47-70. [Holt 83] Holt, R.C., Concurrent Euclid, the UNIX System, and Tunis, Addison-Wesley, Reading, MA, 1983.