Операционные системы. Часть I. Построение и функционирование операционных систем. Учебное пособие [А. С. Деревянко] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ УКРАИНЫ
Национальный технический университет
“Харьковский политехнический институт”

А.С.Деревянко, М.Н.Солощук

ОПЕРАЦИОННЫЕ СИСТЕМЫ
Часть I
Построение и функционирование операционных систем
Учебное пособие

Харьков 2002

ББК 32 - 973.018
УДК 681.3.06
Рецензенти: Г.І.Загарій, д-р техн. наук, проф., завідувач кафедри
“Автоматика і комп’ютерні системи управління” Української державної
академії залізничного транспорту; З.В.Дудар, канд. техн. наук, проф.,
завідувач кафедри “Програмне забезпечення ЕОМ” Харківського
національного університету радіоелектроніки.
Интеллектуальная собственность НТУ “ХПИ”. Все права защищены.

Операционные системы: Деревянко А.С., Солощук М.Н.Учебное пособие.
- Харьков: НТУ “ХПИ”, 2002. - …с.
Представлена концепция операционной системы как набора программных
модулей, выполняющих планирование аппаратных и программных
ресурсов. Рассмотрены дисциплины и алгоритмы планирования для
различных ресурсов при различных задачах, стоящих перед системами.
Предназначено
для
студентов
и
специалистов
направлений
“Компьютерные науки” и “Компьютерная инженерия”
ІSBN
Подано концепцію операційної системи як набору програмних модулів, які
виконують планування апаратних та програмних ресурсів. Розглянуто
дисципліни та алгоритми планування для різних ресурсів при різних
задачах, що стоять перед системами. Призначається для студентів та
спеціалістів напрямків “Комп’ютерні науки” та “Комп’ютерна інженерія”
Илл. - 55, библиогр. - 46 назв.

 А.С. Деревянко, М.Н. Солощук, 2002
 Национальный технический университет “ХПИ”, 2002

2

Содержание
Введение
Глава 1. Основные понятия
1.1. Операционная система с точки зрения системного
программиста
1.2. Классификация и предварительный обзор операционных
систем
1.3. Точка зрения пользователя
1.4. Аппаратная архитектура и поддержка ОС
1.5. Ядро и процессы
1.6. Архитектурные концепции операционных систем
Контрольные вопросы
Глава 2. Планирование процессов
2.1. Дисциплины планирования – требования, показатели,
классификация
2.2. Базовые дисциплины планирования
2.3. Планирование процессов в реальных системах
2.4. Другие уровни планирования
Контрольные вопросы
Глава 3. Управление памятью
3.1. Виртуальная и реальная память
3.2. Фиксированные разделы.
3.3. Односегментная модель
3.4. Многосегментная модель
3.5. Страничная модель
3.6. Сегментно-страничная модель
3.7. Плоская модель
3.8 Одноуровневая модель
Контрольные вопросы
Глава 4. Порождение программ и процессов
4.1. Компиляция
4.2. Компоновка и загрузка
4.3. Цикл жизни процесса
4.4. Нити
Контрольные вопросы
Глава 5. Монопольно используемые ресурсы
3

5.1. Свойства ресурсов и их представление
5.2. Обедающие философы
5.3. Тупики: предупреждение, обнаружение, развязка
5.4. Бесконечное откладывание
Контрольные вопросы
Глава 6. Управление вводом-выводом
6.1. Виртуализация устройств и структура драйвера
6.2. Интерфейсы устройств
6.3. Управление устройствами
6.4. Примеры драйверов устройств
6.5. Потоки и многоуровневые драйверы
6.6. Интерфейс процесса
6.7. Буферизация
Контрольные вопросы
Глава 7. Файловые системы
7.1. Иерархическая модель файловой системы
7.2. Логическая организация файлов. Интерфейсы
7.3. Логическая файловая система. Каталоги
7.4. Логическая файловая система. Системные вызовы
7.5. Базовая файловая система
7.6. Физическая структура файлов
7.7. Пример
7.8. Целостность данных и файловой системы
7.9. Загружаемая файловая система
Контрольные вопросы
Глава 8. Параллельное выполнение процессов
8.1. Постановка проблемы
8.2. Взаимное исключение запретом прерываний
8.3. Взаимное исключение через общие переменные
8.4. Команда testAndSet и блокировки
8.5. Семафоры
8.6. "Производители-потребители"
8.7. Конструкции критических секций в языках
программирования
8.8. Мониторы
8.9. "Читатели-писатели" и групповые мониторы
8.10. Примитивы синхронизации в языках программирования
8.11 Рандеву
Контрольные вопросы
4

Глава 9. Системные средства взаимодействия процессов
9.1. Скобки критических секций
9.2. Виртуальные прерывания или сигналы
9.3. Модель виртуальных коммуникационных портов
9.4. Общие области памяти
9.5. Семафоры
9.6. Программные каналы
9.7. Очереди сообщений
Контрольные вопросы
Глава 10. Защита ресурсов
10.1. Общие требования безопасности
10.2. Объектно-ориентированная модель доступа и механизмы
защиты
10.3. Представление прав доступа
10.4. Дополнительные возможности
Контрольные вопросы
Глава 11. Интерфейс пользователя
11.1. Командный язык и командный процессор
11.2. Командные файлы и язык процедур
11.3. Проблема идентификации адресата
11.4. WIMP-интерфейс
Контрольные вопросы
Заключение
Список литературы

5

Введение
Предлагаемое вниманию читателей учебное пособие написано по
материалам курсов "Системное программное обеспечение" и "Системное
программирование и операционные системы", читаемых студентам
направлений "Компьютерные науки" и "Компьютерная инженерия"
Национального

политехнического

университета

"ХПИ",

а

также

слушателям Межотраслевого института повышения квалификации при
НТУ "ХПИ". Изложение этих курсов сопровождается неизменным
интересом слушателей и неизменной нехваткой учебной литературы. Дело
в том, что курсы базируются на общих концепциях, сложившихся в начале
70-х годов. Произошедшая в середине 80-х "персональная революция"
создала ошибочное впечатление об устарелости этих концепций и
обусловила перерыв в издании учебной литературы, рассматривающей их.
Так, последнее известное нам "раннее" издание такого рода на русском
языке

датировано

1989

годом

[26].

Однако

последующее

совершенствование средств вычислительной техники и ее программного
обеспечения показало, что эти концепции отнюдь не устарели, но
продолжают применяться и развиваться. Старые издания не могут
удовлетворить растущего интереса студентов и специалистов к этой теме,
во-первых, потому, что они уже стали библиографической редкостью, а вовторых, потому, что в них, естественно, не рассматриваются современные
версии ОС и те (пусть и немногие) новые концепции, которые появились в
последние годы. Два же известных нам современных издания [7, 22]
посвящены рассматриваемой нами теме лишь отчасти, содержат далеко не
полные обзоры и также не могут удовлетворить спрос в полной мере. Мы
6

надеемся, что предлагаемое издание в какой-то мере уменьшит этот
информационный дефицит.
В первой части данного учебного пособия мы не привязываемся к
какой-либо конкретной ОС, рассматривая лишь общие принципы
построения и функционирования ОС. Вторая же часть посвящена тому, как
рассмотренные принципы реализованы в конкретных современных
системах.
Системные вызовы мы по возможности именовали в соответствии с
традициями, сложившимися в ОС Unix и зафиксированными в стандарте
POSIX, однако, с легкостью отступали от этих традиций там, где это нам
представлялось необходимым. В описании алгоритмов и данных мы
ориентировались на язык программирования C, однако, опять-таки
отступали от синтаксических правил языка там, где строгое следование им
вело, по нашему мнению, к излишней конкретизации.
В тексте книги шрифтом Courier New выделены фрагменты,
представляющие

собой

лексические

конструкции

языков

программирования или командных языков и элементы формальной
математической записи.
Авторы посвящают эту работу памяти своего друга и коллеги
почетного доктора НТУ "ХПИ" Хартмуда Штира (IBM development
Laboratory, Boblingen, Germany).

7

Глава 1. Основные понятия
1.1. Операционная система с точки зрения системного
программиста

Операционная система (ОС) есть набор программ, которые
распределяют ресурсы процессам.
Приведенная выше формулировка является ключевой для понимания
всего курса. Прежде, чем мы ее раскроем, дадим определение входящих в
нее терминов. Ресурс – "средство системы обработки данных, которое
может быть выделено процессу обработки данных на определенный
интервал времени" [8]. Иными словами: ресурс – это все те аппаратные и
программные средства и данные, которые необходимы для выполнения
программы. Ресурсы можно подразделить на первичные и вторичные. К
первой

группе

относятся

те

ресурсы,

которые

обеспечиваются

аппаратными средствами, например: процессор, память – оперативная и
внешняя, устройства и каналы ввода-вывода и т.п.; ко второй – ресурсы,
порождаемые ОС, например, системные коды и структуры данных, файлы,
семафоры, очереди и т.п. В последнее время в связи с развитием
распределенных вычислений и распределенного хранения данных все
большее значение приобретают такие ресурсы как данные и сообщения.
В [12] приведено около десяти определений термина "процесс", из
которых автор выбирает: "программа в стадии выполнения". Это
определение близко к тому, что интуитивно понимают под "процессом"
программисты, но оно не является строгим. Более строгое определение

8

процесса, которое дает терминологический стандарт, представляется нам
гораздо более удачным, поэтому ниже мы приводим его полностью.
"Процесс обработки данных – система действий, реализующая
определенную функцию в системе обработки информации и оформленная
так, что управляющая программа данной системы может перераспределять
ресурсы этой системы в целях обеспечения мультипрограммирования.
Примечания:
1.

Процесс характеризуется состояниями, которые определяются

наличием тех
следовательно,

или

иных

ресурсов в распоряжении процесса

возможностью

фактически

выполнять

и,

действия,

относящиеся к процессу.
2.

Перераспределение

ресурсов,

выполняемое

управляющей

программой, влияет на продолжительность процесса обработки данных, но
не на его конечный результат.
3.

Процесс оформляют с помощью специальных структур

управляющих данных, которыми манипулирует управляющий механизм.
4.

В конкретных системах обработки информации встречаются

разновидности процессов, которые различаются способом оформления и
составом ресурсов, назначаемых процессу и отнимаемых у него, и
допускается вводить специальные названия для таких разновидностей, как,
например, задача в операционной системе ОС ЕС ЭВМ" [8].
(В соответствии со сложившейся в литературе традицией мы часто
будем употреблять термин "задача" как синоним термина "процесс".)
На примечания к определению процесса мы обратим внимание
позже, а пока сосредоточимся на основной его части. С точки зрения ОС
процесс – это "юридическое лицо", которое получает в свое распоряжение
ресурсы. Процесс может иметь сложную структуру, но его составные
части либо оформляются как отдельные процессы и тогда предстают перед
ОС как независимые от процесса-родителя "юридические лица", либо
9

используют ресурсы от имени всего процесса и тогда они "невидимы" для
ОС. (Промежуточный случай – нити – мы рассматриваем в главе 4)
Такой взгляд на разработку и анализ ОС сложился в конце 60-х – начале
70-х годов, в значительной степени под влиянием ОС Unix [9, 33], в
которой принцип процессов и ресурсов реализован наиболее
последовательно и изящно. Большое количество изданий, посвященных
ОС и отражающих как эмпирический (например, [12, 17-19, 36]), так и
аналитический (например, [1, 2, 16]) подходы, разделяет именно такой
взгляд. Следование принципу процессов – ресурсов позволяет
структурировать изучение ОС в виде таблицы, приведенной на рисунке
1.1. Столбцами этой таблицы являются классы ресурсов, которыми
управляют ОС, а строками – конкретные ОС.

Рисунок 1.1 Операционные системы и ресурсы

В идеале исчерпывающее изложение курсов "Системное программное
обеспечение ЭВМ" и "Операционные системы" должно привести к
заполнению всех клеток этой таблицы, но в данном учебном курсе мы
сосредоточили внимание на изучении "структуры записи" (строки) этой
таблицы. Владение этой структурой позволит специалисту
10

самостоятельно заполнить пробелы в таблице и при необходимости
дополнить таблицу новыми строками. В связи с конкурентной борьбой
на рынке программных продуктов описания современных ОС,
появляющиеся в печати, по большей части акцентируют внимание на
тех свойствах, которые придают системе "товарный вид", хотя и
необязательно определяют фундаментальные возможности и
эффективность системы. Понимание таких возможностей вооружает
специалиста инструментом для сравнительного анализа различных ОС
по общим объективным критериям.
Попытку "эскизного" заполнения таблицы на рисунке 1.1 мы делаем
во второй части этой книги.

1.2.

Классификация

и

предварительный

обзор

операционных систем
В изданиях, упомянутых выше, классификация совмещается с
историческим обзором, показывающим, как со временем увеличивались
ресурсы вычислительных систем и соответственно усложнялись функции
управления ими. Наряду с этим, нам представляется интересным провести
классификацию также и в одном (сегодняшнем) временном срезе.
Мы будем классифицировать ОС по количеству пользователей и
количеству задач (процессов), одновременно управляемых системой. Чем
вызывается стремление увеличить эти показатели?
Доводы за многопользовательский режим составляют две группы.
Во-первых,

возьмем

персональный

на

себя

компьютер

смелость
возможен
11

утверждать,
только

что

как

сегодня
игрушка.

Профессиональный программист или пользователь ЭВМ не может сегодня
работать на персональном компьютере – он может (и должен) работать на
сколь угодно интеллектуальном персональном терминале в глобальной
компьютерной сети. Естественно, что стоимость обработки данных в такой
сети может быть существенно снижена при концентрации программ и
данных, относящихся к одному, например, проекту или предприятию, в
одном узле этой сети с обеспечением доступа к ним всех пользователей
этой

информации.

Во-вторых,

в

70-е

годы

состояние

средств

вычислительной техники и их программного обеспечения позволило
специалистам вывести правило о том, что при линейном возрастании
стоимости вычислительной системы ее возможности возрастают в
квадрате [12]. В середине 80-х годов это правило было нарушено из-за
значительного снижения стоимости ПЭВМ за счет их массового
производства,

но

в

настоящее

время

технологии

производства

компонентов больших ЭВМ (мейнфреймов) по стоимостному показателю
сравнялись с ПЭВМ [31, 38, 40], и это правило вновь становится
актуальным.

Но

более

мощную

вычислительную

систему

один

пользователь будет просто не в состоянии загрузить – отсюда и
необходимость в многопользовательском режиме.
Многозадачность (синоним: мультипрограммирование – "режим
работы, предусматривающий поочередное выполнение двух или более
программ одним процессором"

[8])

при

ее

возникновении была

обусловлена стремлением наиболее полно использовать ресурсы. При
работе системы в пакетном режиме целью, к которой стремится ОС,
является повышение пропускной способности – обслуживание как можно
большего числа заданий в единицу времени. Поскольку аппаратная
архитектура большинства вычислительных систем допускает параллельное
функционирование центрального процессора и каналов ввода-вывода,
очевидным представляется программное решение, использующее это
12

распараллеливание:

один

процесс

выполняется

на

центральном

процессоре, в то время как другой (другие) работает с каналом (каналами)
ввода-вывода. С заменой перфокарточных и перфоленточных устройств
ввода терминалами стал активно развиваться интерактивный режим.
Понятие "задание" (job) сменяется понятием "сеанс" (session). В
отличие от задания, в котором исходные данные готовились до начала
выполнения программы и вводились в ЭВМ вместе с программой, в сеансе
эти данные вводятся уже в ходе выполнения, зачастую они просто не
могут быть подготовлены заранее. Пока в одном сеансе происходит
подготовка и ввод данных, система может обслуживать другие сеансы.
Поскольку ввод данных, выполняемый оператором или пользователем, –
процесс очень медленный, уровень мультипрограммирования (количество
параллельно выполняемых процессов) в такой системе значительно
повышается. При управлении ресурсами в интерактивном режиме на
передний

план

выдвигается

цель

справедливого

обслуживания:

обеспечение минимальной дисперсии времени ответа системы на ввод
данных пользователем и приемлемого времени ожидания ответа.
Разновидностью интерактивного режима можно считать вычисления
в режиме клиент/сервер [34].

В этом режиме управление каким-либо

ресурсом (например, базой данных) осуществляется отдельным процессом
(возможно, и отдельным компьютером в сети) – сервером. Приложенияклиенты для получения доступа к ресурсу обращаются к серверу. При
любой обработке данных имеются три основных уровня манипулирования
данными, как показано на рисунке 1.2:
• хранение данных;
• бизнес-логика,

т.е. выборка и обработка данных для нужд

прикладной задачи;
• представление данных и результатов обработки конечному
пользователю.
13

Рисунок 1.2 Уровни обработки и модели клиент/серверных вычислений

В вычислительных системах, построенных по персональной идеологии,
все три функции в полной мере сосредоточены на одном компьютере.
При построении неперсональных систем выполняется
перераспределение функций между компьютерами в сети.
Распределение функций манипулирования данными между клиентом и
сервером может быть различным. Различные варианты распределения
функций между сервером и клиентами образуют различные варианты
архитектуры клиент/сервер (см. рисунок 1.2):
• если сервер выполняет только хранение данных, и при необходимости
вся единица хранения данных (файл) пересылается клиенту, и всю
дальнейшую работу с данными выполняет клиент, то это вариант
файлового сервера;
• если на сервер возлагается выполнение одной из самых трудоемких
функций логики приложения – выборки необходимых для обработки
данных, то это вариант сервера данных;
14

• если вся логика приложений выполняется на сервере, а в клиентскую
часть передаются лишь результаты обработки, то это вариант сервера
приложений.
В

любом

из

этих

вариантов

клиентские

ОС

работают

в

интерактивном режиме, обслуживая пользователей-операторов, а ОС
сервера – тоже в интерактивном режиме, но пользователями для нее
являются

приложения-клиенты.

Отличия

режима

клиент/сервер

от

обычного интерактивного скорее количественные, чем качественные: ОС
сервера

выполняет

несколько

более

длинные

последовательности

процессорных команд без обращения к операциям ввода-вывода и
несколько реже получает внешние прерывания. Поэтому дисциплины
управления ресурсами в интерактивных и клиент/сервер ОС различаются
не структурами алгоритмов, а их параметрами.
Сходные задачи стоят и перед системами реального времени, как
правило, работающими в непосредственной связи (on-line) с объектом
управления и выполняющими некоторые операции по управлению либо
периодически, либо по требованию. Но в отличие от интерактивных или
клиент/серверных ОС, для систем реального времени основной целью
является обеспечение гарантированного времени ответа, ни в коем случае
не превышающего некоторого критического значения.
Наконец, современная (и перспективная) модель вычислений
предполагает разнесение всех трех уровней клиент/серверной архитектуры
– клиент, сервер приложений, сервер данных – по разным ЭВМ. Функции
клиента сводятся к презентации информации для конечного пользователя.
Сервер

приложений

обеспечивает

разнообразные

вычислительные

возможности; сервер данных, прежде всего, – хранение и выборку данных,
хотя может выполнять и значительную часть их обработки. В условиях
развития глобальных коммуникаций каждый клиент может получать
обслуживание от многих серверов приложений, а каждый сервер
15

приложений – получать данные из многих источников, как показано на
рисунке 1.3.

Рисунок 1.3 Трехуровневая архитектура клиент/сервер

Прогнозируется (см., например, [44]), что в ближайшие годы ПЭВМ
должны будут существенно "потесниться" в роли клиента, уступив
значительную

часть

этого

ареала

устройствам

с

ограниченными

вычислительными возможностями (так называемым "тонким" клиентам), в
том числе, и мобильным. Вычисления, таким образом, становятся все
более сервер-центрическими, распределяясь между серверами приложений
и серверами баз данных. При работе с мобильными клиентами и
удаленными источниками данных получение обслуживания клиента у
сервера приложений, а сервера приложений – у сервера данных может
происходить и без установления непосредственной связи между клиентом
и сервером, а состоять из посылки клиентом сообщения – запроса на
обслуживание и получения им ответного сообщения с результатами
выполнения запроса. В этом случае мы как бы возвращаемся к пакетному
режиму, хотя и с иными характеристиками пакетов-заданий.
Хотя

описанное

нами

развитие

методов

обработки

данных

происходит во времени, новый подход никогда полностью не отменяет
предыдущие.

В

настоящее

время
16

в

эксплуатации

находятся

вычислительные системы с самым разным объемом ресурсов и с
применением самых разных методов обработки информации.
Целью настоящего издания не является исчерпывающий обзор ОС,
однако в тексте мы часто будем приводить примеры организации тех или
иных функций в конкретных системах. В сумме эти примеры,
рассредоточенные по разным главам, могут составить не исчерпывающее,
но довольно полное представление о нескольких ОС. Поэтому в
приводимой ниже классификации мы дадим вводную характеристику тем
ОС, которые составляют наш "банк примеров". Некоторые приводимые
нами характеристики ОС, возможно, будут непонятны начинающему
читателю, их объяснение вы найдете в следующих главах настоящего
пособия.
Простейшим является класс однозадачных однопользовательских
систем. Их аппаратной платформой является IBM PC (XT, AT), ОС – MS
DOS. Поскольку ресурсы такой системы весьма ограничены, ее
рассмотрение не представляет для целей данного пособия существенного
интереса.
Класс многозадачных однопользовательских систем начинается с
тандема MS DOS + Windows, но настоящими ОС этого класса являются
OS/2 и Windows 9x. Эти ОС работают на аппаратной платформе не ниже
процессора Intel 80386, ресурсы, поддерживаемые такими ОС, – более
мощные, управление ими усложняется. Вместе с тем в функции системы
не

входит

защита

ресурсов

от

других

пользователей:

в

однопользовательской системе "украсть" ресурсы можно только у самого
себя.
Windows 1.x - 3.x представляет собой надстройку над MS DOS,
обеспечивающую управление виртуальной памятью (сегментную или
сегментно-страничную – в зависимости от процессора – модель) и
кооперативную многозадачность.
17

Операционные системы OS/2 и Windows 95/98/ME – системы
многозадачные, однопользовательские. (Хотя OS/2 позиционируется на
рынке

как

серверная

система,

однопользовательским.)

Они

ядро

ее

продолжает

обеспечивают

оставаться

вытесняющую

многозадачность и работу с нитями, а также богатый набор средств
взаимодействия процессов. В них используется 32-разрядная (плоская)
модель памяти
Многозадачные многопользовательские системы в настоящее время
эксплуатируются

на

ЭВМ,

работающих

в

многопользовательском

интерактивном режиме или выполняющих функции серверов в сетях, их
современные аппаратные платформы – на базе серверов Intel-Pentium и
RISC-процессоров. Управление ресурсами здесь усложняется не только изза простого возрастания их объема, но и из-за изменения задач. Система
исходит из "презумпции нечестности" пользователей – предположения о
том, что любой процесс будет стремиться захватить как можно больше
ресурсов в ущерб процессам других пользователей. ОС должна обеспечить
справедливое распределение ресурсов между пользователями и их учет
(возможно, для оплаты). Важной составляющей таких ОС является также
обеспечение безопасности: защита программ и данных пользователя от их
чтения или изменения или уничтожения другими пользователями.
Первым примером ОС такого класса, естественно, должна быть
названа ОС Unix, которая существует и развивается с 1968 г. ОС Unix
оказала огромное влияние на развитие концепций построения ОС,
породила множество клонов (BSD Unix, Solaris, AIX, Linux и т.д.) и
является основой стандартов на ОС.
Windows NT (начиная с версии 5 – Windows 2000) является
полностью 32-разрядной ОС с объектно-ориентированной структурой и
строится на базе микроядра.

18

Семейство вычислительных систем AS/400 является результатом
длительного эволюционного развития, начавшегося с IBM System/38 (1978
г.). По ряду идей и решений эволюционный ряд System/38 - AS/400
является лидером в развитии ОС. Среди особенностей, делающих эту
систему интересным примером для нас, следует назвать: объектноориентированную ее структуру и архитектуру на базе микроядра,
одноуровневую модель памяти, мощные средства защиты. Системное
программное обеспечение AS/400 двухуровневое: нижний уровень
выполняется Лицензионным Внутренним Кодом (LIC – Licensed Internal
Code) и обеспечивает аппаратную независимость верхнего уровня,
который

составляет

собственно

ОС

OS/400.

AS/400

отличается

значительной степенью системной интеграции и высоким уровнем
системных интерфейсов.
Наконец, последний рассматриваемый нами класс – гигаресурсные
(термин введен нами) системы. Являясь также многозадачными и
многопользовательскими, они отличаются от предыдущего класса тем,
что ресурсы, управляемые ими, на несколько порядков больше. Их
аппаратной платформой являются мейнфреймы System/390 или ESA
(Enterprise System Architecture) фирмы IBM, представляющие собой
эволюционное развитие ряда System/360 – System/370. Современные
мейнфреймы отличаются большим объемом возможностей,
реализованных на аппаратном уровне, таких как мультипроцессорная
обработка, средства создания системных комплексов, объединяющих
несколько мейнфреймов, средства логического разделения ресурсов
вычислительной системы, высокоэффективная архитектура каналов
ввода-вывода, и т.д. Современные ОС ESA – VSE/ESA, VM/ESA, OS/390
представляют собой развитие работавших на System/360, System/370.

19

• VSE/ESA ориентирована на использование в конечных и
промежуточных узлах сетей. Она функционирует на наименее
мощных моделях мейнфреймов. VSE эффективно выполняет
пакетную обработку и обработку транзакций в реальном времени.
• VM/ESA



гибкая

интерактивная

ОС,

поддерживающая

одновременное функционирование нескольких "гостевых" ОС на
одной вычислительной системе.
• OS/390 (в последней версии – z/OS) – основная ОС для
применения на наиболее мощных аппаратных средствах. Она
обеспечивает наиболее эффективное управление ресурсами при
пакетном и интерактивном режимах и обработке в реальном
времени, возможно совмещение любых режимов. Обеспечивает
также комплексирование вычислительных систем, динамическую
реконфигурацию

ввода-вывода,

расширенные

средства

управления производительностью.

1.3. Точка зрения пользователя

ОС есть набор программ, которые скрывают от пользователя
детали управления оборудованием (hardware) и обеспечивают
ему более удобную среду
Этот принцип иллюстрируется рисунком 1.4.

20

Рисунок 1.4 Операционная система, процессы, оборудование

Как видно из рисунка 1.4, ОС играет роль "прослойки" между
процессами пользователей и оборудованием системы. (Под оборудованием
понимаются, как правило, внешние устройства, но можно трактовать этот
термин и шире, включая в него все первичные ресурсы). Процессы
пользователей не имеют непосредственного доступа к оборудованию и,
говоря шире, к системным ресурсам. Если процессу необходимо
выполнить операцию с системным ресурсом, в том числе и с
оборудованием, процесс выдает системный вызов. ОС интерпретирует
системный вызов, проверяет его корректность, возможно, помещает в
очередь запросов и выполняет его. Если выполнение вызова связано с
операциями на оборудовании, ОС формирует и выдает на оборудование
требуемые управляющие воздействия. Оборудование, выполнив операцию,
заданную

управляющими

воздействиями,

сигнализирует

об

этом

прерыванием. Прерывание поступает в ядро ОС, которое анализирует его и
формирует отклик для процесса, выдавшего системный вызов. Если
выполнение системного вызова не требует операций на оборудовании,
отклик может быть сформирован немедленно.
Управляющие воздействия и прерывания составляют интерфейс
оборудования, системные вызовы и отклики на них – интерфейс
процессов. В качестве синонима интерфейса процессов мы в соответствии
со сложившейся в последнее время традицией часто будем употреблять
21

аббревиатуру API (Application Programm Interface – интерфейс прикладной
программы).
Отделение процессов пользователя от оборудования преследует две
цели.
Во-первых – безопасность. Если пользователь не имеет прямого
доступа к оборудованию и вообще к системным ресурсам, то он не может
вывести их из строя или монопольно использовать в ущерб другим
пользователям.

Обеспечение

этой

цели

нуждается

в

аппаратной

поддержке, рассматриваемой в следующем разделе.
Во-вторых – обеспечение абстрагирования пользователя от деталей
управления оборудованием. Вывод на диск, например, требует сложного
программирования

контролера

дискового

устройства,

однако,

все

пользователи используют для этих целей простое обращение к драйверу
устройства. Более того, в большинстве систем имеются библиотеки
системных вызовов, обеспечивающие API для языков высокого уровня
(прежде всего – для языка C). Можно также говорить о том, что ОС
интегрирует ресурсы: из ресурсов низкого (физического) уровня она
конструирует более сложные ресурсы, которые, с одной стороны, сложнее
(по функциональным возможностям), а, с другой, проще (по управлению)
низкоуровневых.

1.4. Аппаратная архитектура и поддержка ОС
Существует несколько различных определений того, что следует
считать аппаратной архитектурой ЭВМ, каждое из таких определений
"работает" для определенного класса задач. Мы как программисты
воспользуемся таким определением:
Аппаратной

архитектурой

называются
22

те

компоненты

вычислительной

системы,

через

которые

программное

обеспечение взаимодействует с аппаратурой.
Таким образом, в аппаратную архитектуру попадают не все
компоненты компьютера, а только программно доступные – те,
состоянием и действием которых программа может управлять или с
которых программа может считать информацию. В состав этих средств
входят:
• система команд процессора;
• регистры процессора;
• память;
• система ввода-вывода;
• система прерываний.
Аппаратную поддержку управления памятью и вводом-выводом мы
рассматриваем отдельно (в главах 3 и 6 соответственно).
Система команд процессора обеспечивает выполнение программой
действий по обработке данных. Большинство команд в системе команд
процессора имеет прикладное назначение, однако некоторые команды из
набора команд процессора предназначены для организации управления
вычислительным процессом и, таким образом, непосредственно
поддерживают функционирование ОС. Такие команды в современных
системах являются привилегированными – это, например, команды вводавывода и изменения состояния системы. Современные ОС рассчитаны на
наличие в вычислительной системе двух (как минимум) режимов
функционирования процессора – привилегированного режима (режим ядра
в терминологии Unix) и непривилегированного режима (режим процесса в
Unix). Если программа, выполняющаяся в режиме ядра, может выполнять
любые команды, то для программы, выполняющейся в режиме процесса,
привилегированные команды запрещены. Попытка программы выполнить
23

привилегированную команду в режиме процесса вызывает исключение
(см. ниже). В системе ESA, например, таких основных состояний два (есть
еще ряд промежуточных) [20, 45], они называются "супервизор" и
"задача", такие же названия они имеют в процессоре Power PC. В
процессорах Intel-Pentium аналогичную роль играют уровни привилегий,
они же – кольца защиты [32], причем из четырех аппаратно
обеспечиваемых уровней привилегий в современных ОС используются два
или три. Возможность для пользователя разрабатывать модули,
работающие в режиме ядра, обычно строго регламентируется ОС. Хорошо
защищенная ОС должна безоговорочно пресекать попытки процесса
перейти в состояние ядра.
В число регистров процессора входят регистры общего назначения,
которые в основном используются для манипулирования с прикладными
данными, но также и специальные регистры, такие как регистр адреса
команды, регистр флагов-признаков, регистр режима процессора и т.п.
Содержимое регистра режима процессора определяет привилегированное
или непривилегированное состояние процессора, команды, изменяющие
содержимое этого регистра, обязательно являются привилегированными. В
различных архитектурах специальные регистры могут либо представлять
собой отдельные аппаратные компоненты, либо интегрироваться в более
сложные аппаратные структуры.
Содержимое специальных аппаратных регистров процессора
(обязательно включая регистр адреса команды) составляет вектор
состояния программы/процесса. В большинстве процессорных
архитектур вектор состояния может быть загружен в соответствующие
регистры или считан из них в память одной или несколькими
командами. Так, в процессорах Intel-Pentium имеется структура данных,
называемая TSS (Task State Segment – сегмент состояния задачи),
24

содержимое которой играет роль вектора состояния. При выполнении
команд JMP или CALL, адресующих дескриптор TSS, процессор среди
прочих действий сохраняет содержимое регистров в TSS текущей задачи
и загружает регистры из TSS новой задачи [32]. В процессоре S/390 [20]
имеется 8-байтная структура PSW (Program Status Word – слово
состояния программы), содержащая значительную часть информации
вектора состояния (кроме содержимого регистров общего назначения), и
имеются две команды – LPSW и SPSW – для загрузки и запоминания
PSW соответственно.
Прерывание

состоит

в

прекращении

выполнения

текущей

программы и передаче управления на другую программу – программу
обработки прерывания. При этом сохраняется возможность возврата в
прерванную программу, в ту точку, в которой ее выполнение было
прервано. При всем разнообразии аппаратных архитектур выполнение
прерывания в них происходит примерно по одному сценарию:
• сохраняется вектор состояния прерванной программы (в стеке или в
специально предназначенной для этого области оперативной памяти);
• в регистры процессора загружается некоторый вектор состояния,
заранее "заготовленный";
• в "заготовленном" векторе состояния регистр адреса команды содержит
адрес программы обработки прерывания, таким образом, управление
передается на программу обработки прерывания;
• как правило, программа обработки прерывания сохраняет содержимое
регистров

общего

назначения,

а

затем

выполняет

действия,

предусмотренные для данного прерывания;
• после выполнения своих действий программа обработки прерывания
восстанавливает содержимое регистров общего назначения прерванной

25

программы, а затем восстанавливает ее запомненный ранее вектор
состояния;
• прерванная

программа

продолжает

свое

выполнение

с

точки

прерывания, даже "не заметив", что было принято и обработано
прерывание.
Различаются прерывания трех типов: внешние, программные и
исключения.
Внешние прерывания поступают от источников, внешних по
отношению к процессору. Такими источниками являются внешние
устройства, другие процессоры и т.д. При помощи такого прерывания
внешний источник сигнализирует о каком-либо изменении своего
состояния, требующем реакции системы. Внешние прерывания являются
важнейшим

компонентом

прерывание

является

управления

асинхронным,

вводом-выводом.
то

есть

оно

Внешнее

поступает

в

непредсказуемые моменты и невозможно предугадать, какой участок
программного кода будет прерван внешним прерыванием. Команды
процессора обладают свойством атомарности в отношении внешних
прерываний: внешнее прерывание не может быть принято, пока не
закончится выполнение текущей команды. При сохранении вектора
состояния в нем запоминается адрес той команды, которая должна
выполняться после команды, во время выполнения которой произошло
внешнее прерывание.
Программное

прерывание

вызывается

специальной

командой

процессора (в Intel-Pentium мнемоника этой команды – INT, в S/390 –
SVC). Выполняется программное прерывание так же, как и внешнее, но, в
отличие от внешних, программные прерывания являются синхронными,
так как они вызываются самой программой. Программные прерывания
являются средством обращения процесса к ОС, механизмом системного
вызова. Обычные команды передачи управления – типа команд CALL или
26

JMP – изменяют регистр адреса команды, но не весь вектор состояния.
Прерывание же позволяет изменить весь вектор состояния, то есть не
только передать управление на другую программу, но и перевести
процессор из непривилегированного режима в привилегированный.
Прерывания, называемые исключениями (exception) или ловушками
(trap), вызываются ошибочными ситуациями при выполнении команды. В
отличие

от

внешних

или

программных

прерывают выполнение команды на
запоминаемый

при

выполнении

прерываний,

исключения

середине. Вектор состояния,

исключения,

таков,

что

его

восстановление приводит к повторному выполнению команды, вызвавшей
исключение. Исключение, например, генерируется при неправильном коде
команды, при попытке выполнить привилегированную команду в не
привилегированном

режиме,

при

попытке

команды

обращения

к

недоступной области памяти и т.д. Как правило, обработка ОС
прерывания-исключения

приводит

к

принудительному

завершению

процесса, в котором произошло исключение (и запомненный вектор
состояния уже не восстанавливается). Однако в некоторых случаях
(некоторые из таких случаев рассматриваются нами в последующих
главах) исключение является штатной ситуацией, замаскированной
формой системного вызова, сигнализирующего ОС о необходимости
выполнить для процесса некоторое обслуживание.

1.5. Ядро и процессы
Прежде чем приступить к изложению основного материала этого
раздела, сделаем несколько общих замечаний о принципах управления
ресурсами и о представлении их в нашем пособии.
27

Ядром (kernel) называется часть ОС, выполняющая некоторый
минимально необходимый набор функций по управлению ресурсами.
Дополнительные функции управления ресурсами выполняются
вспомогательными модулями – утилитами. Точное определение ядра
дать трудно, так как оно по-разному понимается в разных ОС. В
"старых", не работавших с виртуальной памятью системах под ядром
обычно понималась часть системы, резидентная в оперативной памяти.
В современных ОС ядро резидентно в виртуальной памяти, и это также
может служить его классификационным признаком. Более узкое
определение, трактующую ядро как часть ОС, которая работает в
привилегированном режиме, представляется нам более подходящей для
определения микроядра (см. раздел 1.6).
На ядро, как правило, возлагаются такие основные функции:
• обработка прерываний;
• создание и уничтожение процессов;
• переключение процессов из одного состояния в другое;
• управление памятью;
• синхронизация и взаимодействие процессов;
• поддержка операций ввода-вывода (не всегда);
• учет работы системы и использования ресурсов пользователями;
• и т.д.
Для ОС процесс представляется блоком контекста процесса (вспомните
примечание 3 к определению процесса). Блок контекста содержит
информацию о процессе, необходимую для ОС. Обязательной
составляющей блока контекста является вектор состояния процессора.
Остальная часть блока контекста срдержит описание выделенных
процессу ресурсов, например:
• идентификатор пользователя-владельца процесса;
28

• информацию для планирования процесса на выполнение;
• информацию об оперативной и вторичной памяти;
• информацию о других выделенных процессу ресурсах;
• информацию об открытых устройствах и файлах;
• учетную информацию.
Составляющие блока контекста могут храниться в разных местах
памяти и даже вытесняться на внешнюю память. Действия ОС по
управлению процессамисводятся к манипуляциям с блоками контекста
процессов и с отдельными полями этих блоков.
Вспомним теперь примечание 1 к определению процесса: процесс
в системе может находиться в различных состояниях. Количество
состояний процесса разное в разных ОС (так, в ОС Unix различают 9
возможных состояний процесса), но все они сводятся к трем основным,
показанным на рисунке 1.5.

Рисунок 1.5 Состояния процесса

Активное состояние – процесс имеет все необходимые для
выполнения ресурсы, в том числе и ресурс центрального процессора;
активный процесс выполняется.
Готовое

состояние



процесс

имеет

все

необходимые

выполнения ресурсы, кроме ресурса центрального процессора.

29

для

Заблокированное (ожидающее) состояние – процессу не хватает еще
какого-либо ресурса (ресурсов).
Рассмотрим (пока в общих чертах) переходы между состояниями.
Прежде всего – вход в систему (1 на рисунке 1.5). Мы указали в
числе функций ядра порождение процессов, то есть создание для процесса
блока контекста. Интерактивный процесс создается, когда пользователь за
терминалом вводит команду logon. Пакетный процесс выбирается ОС из
очереди введенных заданий. В последнем случае ОС сама выбирает, какое
задание и в какой момент времени выбрать. Кроме того, новый процесс
может

быть

порожден

из

уже

выполняющегося

при

помощи

соответствующего системного вызова. Операции по принятию решений
ОС о создании нового процесса называются планированием заданий, или
долгосрочным планированием.
Активный процесс может перейти в блокированное состояние (2 на
рисунке 1.5) по двум причинам: по собственной инициативе – процесс
выдает системный вызов – запрос на ресурсы, которые не могут быть ему
предоставлены немедленно (например, выполнение операции вводавывода); по инициативе ОС. Во втором случае ОС "принудительно"
отбирает у процесса ресурсы, чтобы отдать их другому (более
приоритетному) процессу. По этой же причине ОС может забрать ресурсы
и у процесса, находящегося в готовом состоянии (4 на рисунке 1.5). Когда
ресурс, которого не хватает процессу, освобождается, ОС назначает его
процессу и, если у процесса теперь есть все ресурсы, переводит процесс в
готовое состояние (5 на рисунке 1.5). Теперь процесс будет состязаться с
другими готовыми процессами за обладание ресурсом центрального
процессора. В некоторых случаях (в зависимости от принятой в ОС
дисциплины планирования) высокоприоритетный процесс может сразу
после получения ресурса переводиться в активное состояние (3 на рисунке
1.5), вытесняя текущий активный процесс. (Как правило, переход 3
30

непосредственно не реализуется, а выполняется через переходы 5 и 7.)
Перемещение процессов между активным/готовым и заблокированным
состояниями называется планированием ресурсов, или среднесрочным
планированием.
Процесс может перейти из активного состояния в готовое (6 на
рисунке 1.5) либо по собственной инициативе, добровольно отказавшись
от использования центрального процессора, либо по инициативе ОС.
Перевод процессов из готового состояния в активное (7 на рисунке 1.5)
выполняет ОС в соответствии с принятой дисциплиной планирования
процессов, или краткосрочного планирования.
Для каждого из состояний ОС создает список или списки процессов,
находящихся в этом состоянии. В системе с одним центральным
процессором список активных процессов содержит только один элемент.
Список готовых процессов может содержать несколько элементов. Что же
касается списка заблокированных процессов, то в ОС, как правило,
имеется несколько таких списков – свой список для каждого класса
ожидаемых ресурсов. Смена состояния процесса вызывает перемещение
процессов между списками. Рассмотрим с этой точки зрения выполнение
системного вызова. Если процесс выдает системный вызов, то обязательно
происходит переключение контекста – из контекста процесса в контекст
ядра. Если системный вызов может быть выполнен немедленно (например,
запрос текущего времени), то он выполняется, и сразу же происходит
обратное переключение контекста. Если же выполнение вызова требует
времени, то текущий активный процесс переносится в соответствующий
список заблокированных, из списка готовых выбирается и назначается
активным другой процесс, и контекст переключается на новый активный
процесс, то есть в этом случае происходит переключение процессов.
Прерывание вызывает переключение контекста на ядро ОС. Обработав
прерывание, ОС либо выполняет обратное переключение на контекст
31

прерванного процесса, либо переводит прерванный процесс в готовое
состояние, а активным назначает другой процесс и возвращается из
прерывания в его контекст. Как мы показали ранее, переключение
контекстов – операция быстрая, а вот переключение процессов –
значительно более медленная. Поэтому для минимизации "накладных
расходов" по управлению ОС стремится по возможности уменьшить число
переключений процессов. Само переключение процессов (но не его
планирование) часто называют диспетчеризацией (dispatching).
Механизм переключения контекста с одного процесса на другой
обычно реализуется следующим образом. Решение о переключении
принимает ОС, следовательно, текущий процесс прерывается (например,
по внешнему прерыванию от таймера), его вектор состояния запоминается,
как было описано в разделе 1.4, а в регистры процессора загружается
вектор состояния модуля-планировщика ОС. В таблице процессов
сохраняются

векторы

состояния

всех

выполняющихся

в

системе

процессов, соответствующие тем моментам, в которые их выполнение
было прервано в последний раз. Если планировщик принимает решение о
том, что активным должен стать другой процесс, то он переписывает
вектор состояния прерванного процесса в его блок контекста, а в стек (или
в ту область памяти, из которой восстанавливается вектор состояния)
записывает вектор состояния того процесса, который должен стать
активным. При возврате из обработки прерывания, таким образом,
восстанавливается вектор состояния нового процесса.
Каждый

уровень

планирования

осуществляется

отдельным

планировщиком (или планировщиками) в составе ОС – процедурой или
процессом. В последующих главах мы рассмотрим работу большей части
этих планировщиков. В литературе часто собственно планировщиком
(scheduler) называют планировщик процессорного времени, планировщики
различных ресурсов называют менеджерами (manager) или мониторами
32

(monitor) соответствующих ресурсов, а планировщик заданий носит
название диспетчера заданий (jobs dispatcher).
В заключение раздела необходимо сделать некоторые обобщения,
важные для нашей дальнейшей работы. Мы показали, что процесс, с точки
зрения ОС, представляется блоком контекста. Обобщение этого принципа
можно сформулировать так: активные объекты с точки зрения ниже
лежащего уровня представляются структурами данных. Вот пример такого
подхода: машинная команда, с точки зрения программиста, является
активной единицей, так как она выполняет некоторые действия, но, с точки
зрения процессора, команда – структура данных, содержащая поля кода
операции и операндов и подлежащая обработке по алгоритмам процессора.
Пример из совершенно другой области: сотрудник любого учреждения,
безусловно, считает себя активной личностью, но, с точки зрения отдела
кадров, он – всего лишь стандартная карточка учета, и все его
перемещения по службе осуществляются простым изменением в графах
этой карточки.
Поскольку наше рассмотрение будет сосредоточено в основном на
нижних уровнях, становится очевидным, что первостепенную важность
для нас имеют структуры данных, описывающие объекты, которые
обрабатывается в ОС и алгоритмы их обработки. Блок контекста процесса
– первая из таких структур данных. При представлении системных
структур данных мы в большинстве случаев решили отказаться от попыток
представить их сколько-нибудь формализованно, например, средствами
какого-либо языка программирования. Такой отказ объясняется тем, что
мы стремились избежать даже намека на то, что та или иная структура
является универсальной, обязательной, фиксированной для всех ОС – это
ни в коем случае не так. Состав компонентов таких структур, их
именование, взаимное расположение, типы данных и т.д. чрезвычайно
разнятся для разных ОС в соответствии с их назначением и даже личными
33

вкусами разработчиков. Поэтому мы ограничиваемся самым свободным
описанием структур – простым перечислением той информации, которая в
большинстве случаев должна в них содержаться. Описания структур
средствами языка программирования или в виде таблиц мы будем
применять только там, где речь будет идти о конкретных ОС.
При решении некоторых задач управления ресурсами ОС должна
создавать интегрированные структуры данных, содержащие набор
объектов одинакового типа. Списки процессов – пример таких
интегрированных структур. Как известно, имеется два общих метода
представления таких структур в памяти: смежное и связное.
Конструктор ОС также свободен в выборе того или иного метода для
представления той или иной интегрированной структуры. Мы в
дальнейшем будем употреблять термин "таблица" – для обозначения
интегрированной структуры, которая скорее всего (но не обязательно)
реализуется смежным представлением, и "список" – для структуры,
которая реализуется скорее всего связным представлением.

1.6. Архитектурные концепции операционных систем
ОС является сложным программным изделием, поэтому при ее
проектировании невозможно пренебрегать вопросами структурирования,
что подчас допустимо при разработке небольших программ. Все
архитектуры ОС в том или ином варианте используют модульноинтерфейсные методы структурирования [10, 14]. ОС представляется
состоящей из ряда модулей (планирование процессов, управление
памятью, подсистема ввода-вывода и т.д.), для каждого из которых
определены спецификации функционирования и интерфейсы. ОС, однако,
различаются по способам оформления модулей и связей между ними.
34

Модульный состав и организация межмодульного взаимодействия и
составляют архитектуру ОС.
Первой из четко сформулированных архитектурных концепций ОС
была иерархия абстрактных машин, предложенная Дейкстрой в 1968
году для ОС THE (описание можно найти в [29]). Иерархия абстрактных
машин в этой системе показана на рисунке 1.6. Самый нижний (нулевой)
уровень иерархии составляет реальная машина с ее интерфейсом
оборудования. Нижний слой программного обеспечения составляет
первый уровень. Совместно с аппаратными средствами он представляет
некоторую абстрактную машину со своим, более высокоуровневым
интерфейсом оборудования. На основе этого интерфейса строится
абстрактная машина второго уровня и т.д. Последовательным
наращиванием слоев программного обеспечения интерфейс абстрактной
машины доводится до уровня интерфейса процессов.

Рисунок 1.6 Иерархия абстрактных машин в системе THE

Реализация

архитектуры

абстрактных

машин

сопряжена

со

значительными трудностями, связанными с правильным выбором уровней
и их иерархическим упорядочением. Система THE представляет только
один из возможных вариантов, применимый далеко не во всех случаях.
Успешность решения этой проблемы во многом зависит от выбранного
метода

проектирования.

В

первоисточнике
35

реализация

иерархии

абстрактных машин производилась методом "снизу вверх". Другие авторы
(например, [15]) настаивают на реализации методом "сверху вниз". Повидимому, наиболее продуктивным является комбинированный метод,
пример применения которого приведен в [14]: спецификации уровней
разрабатываются "сверху вниз", а реализация ведется "снизу вверх". При
любом методе проектирования обеспечиваются некоторые общие свойства
уровней абстракции, важнейшие из которых следующие:
• каждый уровень обеспечивает некоторую абстракцию данных в системе
и, располагая определенными ресурсами, либо скрывает их от других
уровней, либо предоставляет другим уровням виртуальные ресурсы;
• на каждом уровне ничего не известно о свойствах более высоких
уровней;
• на каждом уровне ничего не известно о внутреннем строении других
уровней;
• связь между уровнями осуществляется только через жесткие, заранее
определенные сопряжения.
Иногда иерархию абстрактных машин иллюстрируют набором
концентрических окружностей (например, [41]), чтобы подчеркнуть, что
каждый следующий уровень иерархии полностью скрывает все лежащие
ниже него уровни и каждый уровень может обращаться только к
непосредственно нижележащему уровню. Обращения, адресованные к
более низким уровням, последовательно проходят все промежуточные
уровни.
Популярными современными вариациями на тему иерархической
архитектуры являются концепции виртуальной машины и микроядра. В
обоих случаях некоторый уровень иерархии получает особый статус и
служит

границей

между

двумя

основными

уровнями

системного

программного обеспечения. Спецификации интерфейса между двумя
36

основными уровнями четко определены, что делает их независимыми друг
от друга.
В концепции виртуальной машины интерфейс процесса выглядит как
интерфейс

оборудования.

В

предельном

случае,

который

можно

наблюдать, например, в VM/ESA [28, 46] внешние формы этих двух
интерфейсов полностью совпадают. В этом случае процессу доступны все
машинные команды, в том числе и привилегированные. Но эта
доступность

кажущаяся.

На

самом

деле,

выдача

процессом

привилегированной команды вызывает исключение. В большинстве ОС
обработка такого исключения включает в себя аварийное завершение
процесса, но в z/VM управление по исключению получает нижний уровень
системы – CP (управляющая программа). CP определяет причину
исключения и выполняет для процесса требуемую команду или
моделирует выполнение этой команды на виртуальном оборудовании. У
процесса создается иллюзия, что в его полном распоряжении находится
реальная вычислительная система (с той, однако, поправкой, что
временные

соотношения

выполнения

некоторых

команд

не

выдерживаются). А если так, то процесс, в свою очередь может быть ОС,
так называемой гостевой (guest) ОС, которая в полном объеме управляет
выделенным ей подмножеством ресурсов.
Другой вариант концепции виртуальной машины представляет
интерфейс нижнего уровня системного программного обеспечения как
полнофункциональный набор команд некоторой воображаемой машины.
Все вышележащие уровни программного обеспечения пишутся в этом
наборе команд (или компилируются в него). Программный модуль,
готовый для выполнения, представляет собой двоичный код программы в
командах виртуальной машины. Перевод команд виртуальной машины в
команды конкретной аппаратной платформы выполняется нижележащим
уровнем программного обеспечения в режиме перекомпиляции (например,
37

AS/400 [11, 39]) или интерпретации (например, технология Java [5, 37]),
компилятор или интерпретатор входит в состав нижнего уровня.
Использование промежуточного кода в командах виртуальной машины
обеспечивает переносимость программного обеспечения на другие
платформы, так как все программное обеспечение (в том числе и
системное), лежащее выше уровня интерфейса виртуальной машины,
является платформенно-независимым и при переносе не требует даже
перекомпиляции.
Другая вариация на тему иерархической архитектуры – концепция
микроядра. Суть ее заключается в том, что части системного
программного обеспечения, которые выполняются в режиме ядра,
сосредоточены на нижнем уровне иерархии, они и составляют
микроядро. Объем микроядра минимизируется, что повышает
надежность системы. Прочие модули ОС выполняются в режиме
процесса и, с точки зрения микроядра, ничем не отличаются от
процессов пользователя. В микроядро включаются также наиболее
важные платформенно-зависимые функции с тем, чтобы обеспечить
оптимизацию их выполнения и относительную независимость от
платформы модулей ОС, не входящих в микроядро. Минимальный
набор функций микроядра включает:
• управление реальной памятью (это всегда платформенно-зависимая
функция);
• переключение контекстов (но не процессов!), решение о том, какой
процесс в какое состояние должен перейти, принимает планировщик,
который не должен работать в режиме ядра, а в мультипроцессорных
системах – и управление загрузкой процессоров;

38

• предварительная обработка аппаратных прерываний (для полной
обработки прерывания перенаправляются тем процессам, которым они
адресованы);
• обеспечение коммуникаций между всеми процессорами вне микроядра


системными

и

пользовательскими,

в

системах,

изначально

ориентированных на распределенную обработку – также и сетевых
коммуникаций.
Архитектурная

концепция

микроядра

также

обеспечивает

переносимость системного программного обеспечения верхнего уровня
(хотя и с необходимостью его перекомпиляции).
Набор преимуществ, обеспечиваемых микроядром, очень велик и в
разных системах это понятие трактуется по-разному – в зависимости от
того, какие требования к системе являются доминирующими. Так,
описанный выше подход минимизации кода, выполняемого в режиме ядра,
и повышения эффективности в полной мере реализован, например, в ОС
QNX [42]. В Windows NT/2000 [25, 43] микроядром называют часть,
обеспечивающую независимость от внешнего оборудования и ряд
функций

режима ядра, но

одним микроядром эти

функции

не

исчерпываются. В AS/400 [27] часть кода, лежащую ниже интерфейса
виртуальной машины, тоже иногда называют микроядром, хотя для
программного обеспечения, состоящего из более, чем 1 млн. строк кода на
языке C++, префикс "микро" вряд ли уместен.
Еще одной тенденцией в развитии ОС является объектноориентированный подход к их проектированию. Как известно, основными
свойствами

объектно-ориентированного

программирования

являются

инкапсуляция, полиморфизм и наследование. Из указанных свойств в
объектно-ориентированных ОС в полной мере реализуется, прежде всего,
первое. Ресурсы в таких системах представляются в виде экземпляров тех
или иных классов, внутренняя структура класса недоступна вне класса, но
39

для класса определены методы работы с ним. Наряду с повышением
степени интеграции тех базовых элементов, из которых строится ОС,
инкапсуляция обеспечивает также защиту ресурсов и возможность менять
в новых версиях ОС или при переносе на новую платформу структуру
системных объектов без изменения тех программ, которые оперируют
объектами. Для каждого типа объектов определен набор операций,
допустимых над ним. Свойство полиморфизма состоит в том, что для
различных системных классов могут быть определены одноименные
операции, выполнение которых для разных классов будет включать в себя
как общие, так и специфические для каждого класса действия. Важнейшей
из таких операций является получение доступа к объекту, отдельно
рассматриваемое нами в главе 10. Свойство наследования реализуется в
объектно-ориентированных ОС лишь отчасти, в связи с чем некоторые
авторы (например, [27]) считают, что правильнее называть эти ОС
объектно-базированными. В системах с иерархической структурой
(Windows NT, AS/400) объекты более высокого уровня могут включать в
себя объекты нижних уровней, однако производные классы не наследуют
методы базовых и, следовательно, их экземпляры не могут обрабатываться
как экземпляры базового класса. Нельзя, однако, говорить об этом
ограничении как о недостатке, так как оно диктуется концепцией
иерархической архитектуры: каждый уровень должен оперировать только
объектами своего уровня.
Архитектурные

концепции

построения

ОС

не

являются

взаимоисключающими. Как вы, по-видимому, заметили из приводимых
примеров, существуют системы, в архитектурах которых комбинируются
несколько подходов.
Важным архитектурным вопросом является оформление модулей
ОС. Модули могут представлять собой процедуры или процессы. В первом
случае все ядро ОС представляется как один многомодульный процесс и
40

передача управления между модулями ОС выполняется просто командами
типа CALL. Во втором случае каждый модуль представляется в виде
отдельного

процесса

ядра),

(процесса

и

передача

управления

сопровождается переключением процессов. Хотя во втором случае
передача

управления

занимает

больше

времени,

такой

подход

обеспечивает, во-первых, лучшую защиту ресурсов, используемых и
управляемых ОС, а во-вторых, делает модульную структуру ОС более
гибкой. Архитектура процессов ядра может совмещаться с архитектурой
иерархии абстрактных машин: каждый уровень иерархии обеспечивается
своим набором процессов ядра. Промежуточным случаем является подход,
характерный, например, для ОС Unix: обращение процесса к ОС вызывает
переключение контекста на ядро, но не переключение процессов, то есть
модули ядра выполняются в контексте вызвавшего их процесса. В тех ОС,
в которых отношения между процессами строятся по схеме "предок–
потомок", иерархия может непосредственно отображаться в "родственных
отношениях" процессов. Что касается процедурной архитектуры, то такие
отношения в ней естественным образом отображается на вложенности
вызовов процедур.
Оформление модулей ОС непосредственно связано с проблемой
управления ресурсами, особенно ресурсами разделяемыми. В случае
процедурного оформления управление каждым видом (классом) ресурса
выполняется

отдельной

процедурой-монитором.

процедурой,

используемой

всеми

Монитор

функционирующими

в

является
системе

процессами, и процедура эта всегда выполняется в контексте вызвавшего
ее

процесса.

Структура

монитора

предотвращает конфликты

при

одновременном обращении к нему двух или более процессов. При
оформлении модулей ОС в виде процессов ядра каждый ресурс
обслуживается своим процессом-менеджером. Доступ к ресурсу из любого

41

другого

процесса

выполняется

через обращение

к

менеджеру и

переключение в контекст менеджера.
Еще один важный вопрос организация взаимодействия между
модулями, и здесь можно выделить две модели [19]: интерфейс процедур и
интерфейс

сообщений.

Интерфейс

процедур

подразумевает

непосредственное обращение вызывающего модуля к вызываемому,
подобное обращению к подпрограмме в языках программирования.
Обращение может быть либо, действительно, обращением к процедуре
(команда CALL), либо сводиться к прерыванию и переключению
процессов. Модель интерфейса процедур синхронная, то есть как и при
вызове

подпрограммы,

выполнение

вызывающего

модуля

приостанавливается до получения результата вызова. Эта модель может
быть построена как на базе процедурных модулей ОС, так и на базе
модулей-процессов.

Другая

модель

обеспечивает

взаимодействие

процессов через единый системный механизм очередей. Процесс-клиент (в
этой модели модули ОС должны быть именно процессами) оформляет
свой запрос в виде сообщения и отправляет его процессу-серверу.
Процесс-сервер получает сообщение из своей входной очереди, выполняет
содержащийся в сообщении запрос и отправляет результат в виде
сообщения процессу-клиенту. Процесс-клиент после отправки своего
сообщения может либо продолжать выполняться, либо ожидать прихода
ответного

сообщения.

Взаимодействие

процессов,

таким

образом,

происходит асинхронно.
Подходы, которые выбирают в современных ОС, в значительной
степени

определяются

их

назначением.

Однопользовательские

ОС

стремятся в максимальной степени повысить быстродействие выполнения
приложений. Хотя в сверхвысоком быстродействии зачастую нет
функциональной необходимости, оно является существенным фактором в
конкурентной борьбе. Поэтому в таких ОС проявляется тенденция к
42

предельной минимизации числа переключений процессов и к выполнению
системных вызовов в контексте процесса пользователя. Отсюда –
процедурная форма модулей ядра, мониторное управление ресурсами,
процедурная

модель

взаимодействия.

С

другой

стороны,

многопользовательские ОС, как правило, представляют модули ядра в виде
процессов-менеджеров ресурсов и для взаимодействия между ними
предпочтение отдается модели сообщений. Это обеспечивает значительно
лучшую защиту ресурсов, особенно в среде с высоким уровнем
мультипрограммирования.

КОНТРОЛЬНЫЕ ВОПРОСЫ
1.

Один из авторов заявляет, что не может дать определения ОС,

но сразу узнает ОС, если ее увидит. В чем, по-вашему, состоит
ошибочность такого утверждения?
2.

Прокомментируйте примечания 1-3 к определению ОС,

данному в разделе 1.1. Покажите их отображения на реальные ОС.
3.

Дайте определение пакетному и интерактивному режимам

функционирования ОС. Какой из режимов представляется вам более
полезным?
4.

В чем сходство работы многопользовательской интерактивной

ОС с ОС-сервером? В чем их различия?
5.

Каковы достоинства и недостатки изоляции пользователя от

реальных ресурсов?
6.

Назовите

основные

состояния

процесса

в

системе

и

охарактеризуйте переходы между ними. Какие состояния вы считаете
необязательными?
7.

Почему

ОС,

называемые

объектно-ориентированными,

правильнее называть объектно-базированными?
43

8.

Назовите общие черты архитектурных концепций микроядра,

виртуальной машины и иерархической ОС. В чем различия между ними?
9.

В

чем

достоинства

архитектуры

микроядра?

Почему

разработчики стремятся минимизировать объем микроядра?
10.

Сравните способы обращения процесса к ОС: через вызов

процедур и через прерывания. В чем достоинства и недостатки этих
способов?

ГЛАВА 2. ПЛАНИРОВАНИЕ ПРОЦЕССОВ
В предыдущей главе мы определили три уровня планирования,
теперь остановимся более подробно на стратегиях или дисциплинах
планирования. Многие дисциплины применимы на любых уровнях
планирования, но мы сосредоточимся, прежде всего, на планировании
краткосрочном или планировании процессорного времени. Процессорное
время является ключевым ресурсом любой вычислительной системы и
наличие или отсутствие этого ресурса в распоряжении процесса отличает
активное состояние процесса от остальных его состояний. Дисциплины
распределения этого ресурса в значительной степени определяют
эффективность функционирования всей системы в целом.

2.1. Дисциплины планирования – требования, показатели,
классификация
В общем случае планирование (на любом уровне) может быть
представлено как система массового обслуживания, показанная на рисунке
2.1. Применительно к планированию процессорного времени компоненты
этой системы могут быть интерпретированы следующим образом: заявкой
44

является процесс, обслуживающим прибором – центральный процессор
(ЦП), очередь заявок – это очередь готовых процессов. Процессы-заявки
поступают в очередь, при освобождении ЦП один процесс выбирается из
очереди и обслуживается в ЦП. Обслуживание может быть прервано по
следующим причинам:
• выполнение процесса завершилось;
• процесс запросил выполнение операции, требующей ожидания
какого-либо другого ресурса;
• выполнение прервано системой.

Рисунок2.1 Представление планирования процессов в виде системы массового
обслуживания

Первые два случая с точки зрения системы массового обслуживания
одинаковы: в любом случае процесс выходит из данной системы. Если
процесс не завершился, то после получения запрошенного ресурса процесс
вновь поступит во входную очередь. В случае прерывания процесса по
инициативе системы прерванный (вытесненный) процесс поступает во
входную очередь сразу же. Порядок обслуживания входной очереди,
очередность выбора из нее заявок на обслуживания и составляет
дисциплину или стратегию планирования. Методы теории массового
обслуживания применяются для аналитического моделирования процесса
планирования, хотя формальному анализу поддаются только простейшие
дисциплины (см., например, [16]).
Для оценки эффективности функционирования данной системы
массового

обслуживания

могут

быть
45

применены

количественные

показатели. Обозначим через t – процессорное время, необходимое
процессу для выполнения (будем его называть длительностью процесса).
Обозначим через T – общее время пребывания процесса в системе. Эту
величину – интервал между моментом ввода процесса в систему и
моментом получения результатов – также называют иногда временем
реакции процесса. Наряду с временем реакции могут быть полезны также
и другие показатели.
Потерянное время
M = T - t;
определяет время, в течение которого процесс находился в системе, но не
выполнялся.
Отношение реактивности
R = t / T;
показывает долю процессорного времени (времени выполнения) или долю
потерянного времени в общем времени реакции.
Штрафное отношение
P = T / t;
показывает, во сколько раз общее время выполнения процесса превышает
необходимое процессорное время.
Средние значения величин T,

M,

R

и

P могут служить

количественными показателями эффективности. Реальные системы, как
правило, ориентированы

на конкретные характеристики процессов, в

частности, на определенные диапазоны значений t, поэтому указанные
показатели удобно рассматривать как функции длительности процесcа:
T(t), M(t), R(t), P(t).
К дисциплине планирования в общем случае может применяться
широкий спектр требований, наиболее существенные из которых
следующие:
46

• дисциплина должна быть справедливой – она не должна давать
преимуществ одним процессам за счет других и ни в коем случае не
должна допускать бесконечного откладывания процессов;
• дисциплина

должна

обеспечивать

максимальную

пропускную

способность системы – выполнение максимального количества единиц
работы (процессов) в единицу времени;
• дисциплина должна обеспечивать приемлемое время реакции для
интерактивных пользователей;
• дисциплина должна обеспечивать гарантированное время реакции для
процессов реального времени;
• дисциплина

должна

быть

предсказуемой



дисперсия

времен

выполнения процессов, обладающих одинаковыми характеристиками,
должна быть минимальной;
• дисциплина должна учитывать внешние приоритеты, присваиваемые
процессам пользователями и/или администратором системы;
• накладные расходы по реализации дисциплины (затраты процессорного
времени и других ресурсов) должны быть минимизированы;
• дисциплина должна учитывать комплексное использование ресурсов
вычислительной системы, обеспечивая высокую загрузку системы в
целом и рациональное использование ключевых ресурсов.
Очевидно, что выполнение всех перечисленных требований в
одинаковой степени невозможно, так как некоторые из них противоречат
друг другу. В конкретных системах те или иные требования выдвигаются
на передний план – в зависимости от задач системы и характеристик
выполняемых в ней процессов, возможно и выдвижение на первый план
новых требований, не упомянутых в нашем списке.
В большинстве случаев рассмотрение оценок эффективности
планирования процессорного времени производится при условии одного
47

существенного допущения: не принимаются во внимание другие уровни
планирования. Выполнение реального процесса состоит из выполнения
программы в ЦП и операций ввода-вывода. Последние, во-первых,
занимают значительно больше времени (но не времени процессорного), вовторых, могут включать в себя ожидание ресурсов ввода-вывода. Реальные
процессы могут быть классифицированы как вычислительные или
обменные. Первые состоят в основном из вычислений в ЦП, вторые –
содержат большое количество обращений к вводу-выводу. Оценки
эффективности планирования процессорного времени выполняются в
предположении, что все процессы относятся к вычислительному типу.
Обменные процессы могут быть приведены к этой модели путем
представления каждой последовательности процессорных команд между
двумя

операциями

вычислительного

ввода-вывода

типа.

Для

как

систем,

отдельного

требующих

процесса

комплексного

сбалансированного управления ресурсами, стратегия затем расширяется
учетом факторов, определяемых другими ресурсами.
С

точки

зрения

реализации

дисциплины

планирования

подразделяются, прежде всего, на дисциплины вытесняющие (preemptive)
и невытесняющие (non-preemptive), иначе – кооперативные (cooperative).
Для первых возможно прерывание активного процесса и лишение его
ресурса ЦП по инициативе планировщика, для вторых – нет. Дисциплины
с вытеснением выполняют более частые переключения процессов,
следовательно, имеют большие накладные расходы. Но в большинстве
случаев только дисциплины с вытеснением могут обеспечить требуемые
показатели справедливости обслуживания.
Другие

размерности

классификации

дисциплин

связаны

со

способами определения и реализации приоритетов процессов. Различают
приоритеты:

48

• внешние или внутренние – первые назначаются администратором
системы или пользователем, вторые определяются самой системой по
характеристикам процесса;
• статические

или

динамические



первые

определяются

при

поступлении процесса в систему и не изменяются впоследствии, вторые
перевычисляются планировщиком периодически или/и при событиях,
влияющих на планирование процессов;
• абсолютные или относительные – в абсолютных к выполнению
допускается только процессы, имеющие наивысший приоритет, в
относительных

допускается

планирование

на

выполнение

и

низкоприоритетных процессов.
Еще одной важной с точки зрения реализации характеристикой
дисциплины планирования является объем априорной информации о
процессе, необходимой планировщику. Если дисциплина не учитывает
использование других ресурсов, кроме ЦП, то такой информацией может
быть длительность процесса, так как показатели эффективности являются
функциями именно этого аргумента. Если дисциплина использует
комплексные приоритеты, то может появиться необходимость и в другой
априорной информации. При наличии априорной информации появляется
возможность более эффективной реализации, но обязанность подготовки
такой информации возлагается на пользователя-владельца процесса, что
снижает удобства применения системы. Для процессов, не являющихся
чисто счетными, информация, логически эквивалентная априорной, может
быть получена методами экстраполяции: на основании предшествовавшего
поведения процесса делается предположение о его последующем
поведении, например, так, как описано ниже.
Пусть процесс использовал S единиц времени ЦП до перехода в
ожидание ввода-вывода. Тогда прогноз на следующий интервал времени
ЦП, который понадобится процессу, может быть сделан так:
49

E' = W1 * E + W2 * S,
где E – прогноз, сделанный на предыдущем интервале для текущего
интервала, W1 и W2 – весовые коэффициенты, подбираемые так, что
W1 + W2 = 1.
При изменении соотношения весовых коэффициентов в сторону
увеличения

W2

прогноз

становится

более

реактивным

(более

чувствительным к изменению поведения процесса), в обратную сторону –
более инерционным.

2.2. Базовые дисциплины планирования
Ниже

приводятся

описания

некоторых

базовых

дисциплин

планирования. Эти дисциплины достаточно просты в реализации и хорошо
исследованы методами как аналитического (например, [16]), так и
имитационного (например, [36]) моделирования. Мы называем их
базовыми, поскольку в реальных системах они служат основой для
построения более сложных и гибких модификаций и комбинаций, для
которых аналитические модели построить, как правило, невозможно.
FCFS (first come – first serve; первым пришел – первым
обслуживается) – простейшая дисциплина, работа которой понятна из ее
названия. Это дисциплина без вытеснения, то есть процесс, выбранный для
выполнения на ЦП, не прерывается, пока не завершится (или не перейдет в
состояние ожидания по собственной инициативе). Как дисциплина без
вытеснения FCFS обеспечивает минимум накладных расходов. Среднее
потерянное время при применении этой дисциплины не зависит от
длительности процесса, но штрафное отношение при равном потерянном
времени будет большим для коротких процессов. Поэтому дисциплина
FCFS

считается

лучшей

для

длинных

процессов.

Существенным

достоинством этой дисциплины наряду с ее простотой является то
обстоятельство,

что

FCFS

гарантирует
50

отсутствие

бесконечного

откладывания процессов: любой поступивший в систему процесс будет в
конце концов выполнен независимо от степени загрузки системы.
На рисунке 2.2 показан пример планирования по дисциплине FCFS
для трех процессов – A, B и C. На временной диаграмме каждый
прямоугольник представляет интервал времени, в течение которого
процесс находится в системе. Над верхним левым углом такого
прямоугольника указан идентификатор процесса, а в скобках
длительность.

Незатемненные

участки

соответствуют



его

активному

состоянию процесса, затемненные – состоянию ожидания. Процесс A
поступает в момент времени 0 и требует для выполнения 6 единиц
процессорного времени. ЦП в этот момент свободен, и процесс A сразу же
активизируется. В момент времени 2 поступает процесс B, требующий 11
единиц. Поскольку ЦП занят процессом A, процесс B ожидает в очереди
готовых процессов до момента 6, когда процесс A закончится и освободит
ЦП. Только после этого процесс B начинает выполняться. Пока процесс B
выполняется, поступают еще два процесса: C – в момент времени 8 и D – в
момент 10, которые ждут завершения процесса B. Когда процесс B
завершится, ЦП будет отдан процессу C, поступившему раньше, а процесс
D остается в ожидании. В линейке, расположенной под временной шкалой,
указаны идентификаторы процессов, активных в данный момент времени.
Читатель может сам определить показатели эффективности планирования
– для каждого процесса и усредненные. Следует, однако, предупредить,
что к усредненным показателям надо относиться с осторожностью, так как
достоверными могут считаться только результаты, полученные на
статистически значимой выборке.

51

Рисунок 2.2 Планирование процессов по дисциплине FCFS

RR (round robin – карусель) – простейшая дисциплина с вытеснением.
Процесс получает в свое распоряжение ЦП на некоторый квант времени Q
(в простейшем случае размер кванта фиксирован). Если за время Q процесс
не завершился, он вытесняется из ЦП и направляется в конец очереди
готовых процессов, где ждет выделения ему следующего кванта, и т.д.
Показатели эффективности RR существенно зависят от выбора величины
кванта Q. RR обеспечивает наилучшие показатели, если длительность
большинства процессов приближается к

размеру

кванта,

но не

превосходит его. Тогда большинство процессов укладываются в один
квант и не становятся в очередь повторно. При величине кванта,
стремящейся к бесконечности, RR вырождается в FCFS. При Q,
стремящемся к 0, накладные расходы на переключение процессов
возрастают настолько, что поглощают весь ресурс ЦП. RR обеспечивает
наилучшие показатели справедливости: штрафное отношение P на
большом участке длительностей процессов t остается практически
постоянным. Только на участке t=1
– в FCFS. Собственно дисциплина SRR обеспечивается в диапазоне
значений 0V[i-1].

Последнее

обстоятельство

делает

возможным

дублирование информации на уровнях: если данные имеются на i-м
уровне, то их копии сохраняются и на всех уровнях с большими номерами.
Обозначим через h[i] отношение присутствия – вероятность того, что
данные, запрошенные на i-м уровне памяти, уже имеются на этом уровне.
78

Если мы имеем n уровней памяти, то для n-го уровня отношение
присутствия равно 1 и среднее время доступа tau[n] совпадает с t[n].
Для всех уровней с меньшими номерами среднее время доступа может
быть определено рекурсивно:
tau[i] = h[i] * t[i] + ( 1 - h[i] ) * tau[i-1].
На программном уровне мы не можем воздействовать ни на t[i], ни
на V[i], которое в значительной степени определяет и h[i]. Но мы
можем влиять на величину h[i], выбирая для хранения на уровне с
меньшим номером только те данные, обращение к которым производится
наиболее часто.
В общем случае проектирование менеджера памяти в составе ОС
требует выбора трех основных стратегий:
• Стратегии размещения: какую область реальной памяти выделить
процессу; как вести учет свободной/занятой реальной памяти?
• Стратегии подкачки: когда размещать процесс (или часть его) в
реальной памяти?
• Стратегии вытеснения: если реальной памяти не хватает для
удовлетворения очередного запроса, то у какого процесса
отобрать ранее выделенный ресурс реальной памяти (или часть
его)?
Ниже мы рассмотримспособы реализации этих стратегий для
различных моделей памяти. Порядок рассмотрения будет соответствовать
принципу "от простого к сложному" и в основном отображать также и
историческое развитие моделей памяти:
• фиксированные разделы – модель, не использующая аппаратную
трансляцию адресов;
• односегментная виртуальная память – развитие фиксированных
разделов для аппаратной трансляции адресов;
79

• модели виртуальной памяти, использующие развитые средства
аппаратной трансляции адресов;
o многосегментная;
o страничная;
o комбинированная сегментно-страничная;
• модели виртуальной памяти, представляющие собой возврат к
простым моделям, но на более высоком уровне:
o плоская;
o одноуровневая.

3.2. Фиксированные разделы
Эта модель памяти применяется в вычислительных системах, не
имеющих аппаратных средств трансляции адресов. Процесс загружается в
непрерывный участок памяти (раздел), привязка адресов выполняется при
загрузке.

Размер

раздела

равен

размеру

виртуального

адресного

пространства процесса, который, следовательно, не может превышать
размера доступной реальной памяти. Процесс в ходе своего выполнения
может выдавать запросы на выделение/освобождение памяти. Все эти
запросы удовлетворяются только в пределах виртуального адресного
пространства процесса, а, следовательно, – в пределах выделенного ему
раздела реальной памяти.
Примером ОС, работающей в такой модели памяти, может быть
OS/360, ныне уже не применяемая, но существовавшая в двух основных
вариантах: MFT (с фиксированным числом задач) и MVT (с переменным
числом задач). В первом варианте при загрузке ОС реальная память
разбивалась на разделы оператором. Каждая задача (процесс) занимала
один раздел и выполнялась в нем. Во втором варианте число разделов и их
80

положение в памяти не было фиксированным. Раздел создавался в
свободном участке памяти перед началом выполнения задачи и имел
размер, равный объему памяти, заказанному задачей. Созданный раздел
фиксировался в памяти на время выполнения задачи, но уничтожался при
окончании ее выполнения.
В более общем случае для процесса может выделяться и несколько
разделов памяти, причем их выделение/освобождение может выполняться
динамически (пример – MS DOS). Однако общими всегда являются
следующие правила:
• раздел занимает непрерывную область реальной памяти;
• выделенный раздел фиксируется в реальной памяти;
• после выделения раздела процесс работает с реальными адресами
в разделе.
Задача эффективного распределения памяти (в любой ее модели)
сводится, прежде всего, к минимизации суммарного объема дыр. Ниже мы
даем определения дыр, общие для всех моделей памяти.
Дырой называется область реальной памяти, которая не может быть
использована. Различают дыры внешние и внутренние. Рисунок 3.2
иллюстрирует внешние и внутренние дыры в системе OS/360.

Рисунок 3.2 Разделы в реальной памяти OS/360

81

Внешней дырой называется область реальной памяти, которая не
выделена никакому процессу, но слишком мала, чтобы удовлетворить
запрос на память. На рисунке 3.2.а суммарный размер свободных областей
превышает запрос, но каждая из этих областей в отдельности меньше
запроса, поэтому все эти свободные области являются внешними дырами.
Внутренней дырой называется память, которая выделена процессу,
но им не используется. Так, на рисунке 3.2.б процессу 1 выделен раздел P1,
но виртуальное адресное пространство процесса меньше размера раздела,
оставшееся пространство раздела составляет внутреннюю дыру.
Для управления памятью формируются те или иные управляющие
структуры (заголовки), которые также занимают память. В некоторых
системах общий объем заголовочной памяти может быть очень большим, и
в таких случаях следует учитывать также и заголовочные дыры – области
памяти,

которые

содержат

не

используемую

в

данный

момент

управляющую информацию. В системах с реальной памятью заголовочные
дыры практически отсутствуют.
Хотя в любом случае невозможность использовать память является
потерей ресурса, внешние дыры, являются "меньшим злом", так как, вопервых, блок свободной памяти становится дырой из-за малого своего
размера, а "малый" – понятие относительное, во-вторых, в моделях памяти
с динамической трансляцией адресов существуют методы борьбы с
внешними дырами.
Модель

с

фиксированными

разделами

представляет

весьма

ограниченную версию управления памятью. Вытеснение здесь вообще не
реализуется, процесс, которому недостает памяти, просто блокируется до
освобождения требуемого ресурса (в OS/360 MFT можно было наблюдать
даже такое явление: задача, требующая объема памяти, превышающего
размер раздела, просто "запирала" этот раздел до перезагрузки системы).
82

Стратегия подкачки здесь примитивная: весь процесс размещается в
реальной памяти при его создании. В варианте MFT практически
отсутствует и стратегия размещения: процесс размещается с начала
раздела, а решение о размещении раздела и о распределении задач по
разделам принимает оператор. А вот в варианте MVT, поскольку границы
разделов не зафиксированы, ОС необходимо принимать решение о
размещении. В этой ОС уровень мультипрограммирования был невысок
(обычно 4 - 5 процессов), поэтому стратегия размещения принималась
простейшая,

а

вот

в

мультипрограммирования

системах
и,

с

более

следовательно,

высоким
со

уровнем

значительной

фрагментацией памяти может оказаться целесообразным выбор более
сложной и гибкой стратегии.
Первый вопрос, решаемый в стратегии размещения, – способ
представления свободной памяти. Существует два основных метода такого
представления: списки свободных блоков и битовые карты. В первом
методе ОС из свободных блоков памяти организует линейный список и
хранит адрес начала этого списка. При обработке такого списка должна
учитываться необходимость слияния двух смежных свободных блоков в
один свободный блок суммарного размера. Эта задача может быть
существенно упрощена, если список упорядочивается по возрастанию
адресов блоков. Во втором методе память условно разбивается на
некоторые единицы распределения (параграфы) и создается "карта памяти"
(memory map) – битовый массив, в котором каждый бит соответствует
одному параграфу памяти и отображает его состояние: 1 – занят, 0 –
свободен. Поиск свободного блока требуемого размера сводится к поиску
цепочки нулевых бит требуемой длины. Выбор размера параграфа
определяется компромиссом между потерями памяти на внутренних дырах
(при большом размере параграфа) и потерями на размещение в памяти
самой карты (при малом размере параграфа).
83

При не очень большой фрагментации памяти списки свободных
блоков занимают меньше места, чем карта, но их обработка несколько
сложнее.
Выбранная

структура

представления

свободной

памяти

не

ограничивает выбор стратегии размещения.
Простейшей стратегией размещения является стратегия "первый
подходящий" (first hit): просматривается список свободных блоков (или
карта памяти) и выбирается первый же найденный блок, размер которого
не меньше требуемого. Если размер найденного блока превышает
запрошенный, то оставшаяся его часть оформляется как свободный блок.
При всей своей простоте эта стратегия дает неплохие результаты и
применяется в большинстве систем с фиксированными разделами и с
сегментацией (см. ниже). При значительной фрагментации алгоритм может
быть модифицирован кольцевым поиском. Если всякий раз поиск
начинается с начала списка/карты, то маленькие свободные участки будут
накапливаться в списке/карте и для нахождения свободного блока
значительной длины надо будет сначала выбрать и отбросить много
маленьких блоков. При кольцевом поиске поиск всякий раз начинается с
того места, на котором он закончился в прошлый раз, а при достижении
конца списка/карты – продолжается с самого начала. Таким приемом
сокращается среднее время поиска.
Другой несложной стратегией является "самый подходящий" (best
hit): просматривается весь список свободных блоков (или карта памяти) и
выбирается блок, размер которого равен запросу или превышает его на
минимальную величину. Хотя, на первый взгляд, этот метод может
показаться более эффективным, он дает в среднем худшие результаты, чем
"первый подходящий". Во-первых, это объясняется тем, что здесь следует
обязательно просмотреть весь список или карту; во-вторых, тем, что здесь
более интенсивно накапливаются внешние дыры, так как остатки от
84

"самых

подходящих"

блоков

оказываются

маленького

размера.

Существенным же преимуществом этой стратегии является то, что она
охраняет большие свободные блоки от "разбазаривания по мелочам".
Первый недостаток метода "самый подходящий" может быть
преодолен при упорядочении списка свободных блоков по возрастанию
размеров: полный просмотр списка тогда не потребуется, но будет
усложнена задача слияния смежных блоков. Еще более ускоряет поиск
разбиение списка свободных блоков на несколько подсписков, каждый из
которых содержит блоки, размер которых ограничен сверху и снизу. Поиск
ускоряется за счет просмотра только подсписка, соответствующего
запросу, если в подсписке выбирать первый подходящий, то отчасти
сглаживается и острота проблемы внешних дыр. При представлении
свободной памяти в виде карты аналогичный эффект может быть получен
при создании нескольких карт разного масштаба и поиске по той карте,
масштаб которой соответствует запрошенному размеру блока.
Поскольку модель с фиксированными разделами ограничивает
виртуальное адресное пространство размерами реальной памяти, возникает
проблема больших программ, не вмещающихся в доступную память.
Преодоление этого ограничения достигается созданием программ с
оверлейной (overlay – перекрытие) структурой. Структура межмодульных
вызовов оверлейной программы имеет вид дерева. В корне дерева (нулевой
уровень) находится модуль-монитор, из которого происходят обращения к
модулям, расположенным на ветвях дерева. Каждый модуль первого
уровня в свою очередь может быть корнем поддерева и т.д. Принцип
оверлейности или перекрытия заключается в том, что ветви дерева
занимают одни и те же области в виртуальном адресном пространстве
программы. Поскольку модули, расположенные в разных ветвях дерева, не
могут

выполняться

одновременно,

то

только

монитор

является

резидентным в оперативной памяти все время выполнения программы,
85

ветви же сменяют друг друга в одной и той же области памяти. На самом
деле построить программу, имеющую идеальную древовидную структуру,
довольно трудно, поскольку при этом обязательно догматическое
соблюдение дисциплины программирования "сверху вниз". Необходимо
тщательно следить, чтобы ветви дерева не пересекались: никакой модуль
не имеет права передавать управление в модуль, расположенный на другой
ветви, или обращаться к данным, определенным в модуле другой ветви.
Реальные программы обычно нарушают эти правила, так как при их
разработке используется комбинация дисциплин "сверху вниз" и "снизу
вверх", за счет чего появляются низкоуровневые процедуры, обращения к
которым производится из любых ветвей дерева. Выход из противоречия
состоит либо в помещении этих процедур в корневой модуль, либо
создании еще одной

или

размещения

содержащих

модулей,

нескольких

резидентных областей для

эти

процедуры.

В

системах,

поддерживающих развитые средства создания оверлейных программ (та
же OS/360), выполнение функции именования – отображения имен
входных

точек

и

общих

переменных

на

виртуальное

адресное

пространство возлагается на редактор связей (link editor), который и
формирует содержимое адресного пространства процесса в соответствии с
оверлейной

структурой,

описываемой

программистом

с

помощью

специального языка. На Рисунке 3.3 представлен пример оверлейной
структуры программы.

86

a) межмодульные связи

б). распределение памяти
Рисунок 3.3 Пример оверлейной структуры программы

В модели с фиксированными разделами после загрузки процесса все
обращения к памяти в нем производятся по реальным адресам.
Следовательно, ничто не может помешать процессу обратиться к памяти,
принадлежащей другому процессу или даже самой ОС, и последствия
такого обращения могут быть фатальными. Защита от такого доступа
должна поддерживаться аппаратно. Приведем два примера такой защиты.
1) В вычислительной системе имеются специальные регистры, в
которых содержатся значения верхней и нижней границ адресов, к
которым доступ разрешается. При любом обращении к памяти аппаратура
сравнивает заданный адрес с этими граничными значениями и, если адрес
не лежит в границах, выполняет прерывание-ловушку. При размещении
процесса в памяти ОС определяет для него эти границы и записывает их
значения в контекст процесса (вектор состояния процессора). При
переключении контекста на данный процесс эти значения загружаются в
87

регистры границ и таким образом ограничивается область памяти, к
которой имеет доступ процесс. В контексте самой ОС граничные адреса,
естественно, соответствуют всему имеющемуся объему памяти.
2) Реальная память разбивается на "блоки защиты" одинакового
размера. С каждым таким блоком связан некоторый код – ключ. При
размещении процесса ОС дает ему какой-то ключ доступа, сохраняет ключ
в векторе состояния и присваивает тот же ключ всем блокам памяти,
выделенным этому процессу. При любом обращении к памяти аппаратура
сравнивает ключ текущего процесса с ключом блока, к которому
производится обращение и выполняет прерывание-ловушку, если эти
ключи не совпадают. Сама ОС, конечно же, имеет "универсальный ключ"
дающий ей доступ к любому блоку.

3.3. Односегментная модель
Нам неизвестны ОС, поддерживающие односегментную модель "в
чистом виде", но ее рассмотрение облегчит понимание более сложных
моделей.
Внешне (с точки зрения программиста) эта модель очень похожа на
модель с фиксированными разделами: программа-процесс готовится в
плоском

виртуальном

адресном

пространстве.

Процесс

занимает

непрерывное пространство виртуальной памяти, и в реальную память он
также загружается в один непрерывный раздел (сегмент). Сегмент может
начинаться с любого адреса реальной памяти и иметь любой раздел, не
превышающий, однако, размера реальной памяти. Существенное отличие
сегментной модели состоит в том, что она использует аппаратную
динамическую трансляцию адресов. Загруженный в реальную память и
выполняющийся процесс продолжает обращаться к памяти, используя
виртуальные адреса, и лишь при каждом конкретном обращении
88

виртуальный адрес аппаратно переводится в реальный. В вычислительной
системе, поддерживающей односегментную модель, должен существовать
регистр дескриптора сегмента, содержимое которого состоит из двух
полей: начального (базового) адреса сегмента в реальной памяти и длины
сегмента. Когда процесс размещается в памяти, для выделенного ему
сегмента формируется дескриптор, который записывается в вектор
состояния в контексте процесса. При переключении контекста дескриптор
сегмента загружается в аппаратный регистр дескриптора сегмента и
служит той "таблицей трансляции", по которой аппаратура переводит
виртуальные адреса в реальные. Сама трансляция адресов происходит по
простейшему алгоритму. Поскольку виртуальное адресное пространство
процесса представляет собой линейную последовательность адресов,
начинающуюся с 0, виртуальный адрес является простым смещением
относительно начала сегмента. Реальный адрес получается сложением
виртуального адреса с базовым адресом, выбранным из дескриптора
сегмента, как показано на рисунке 3.4. Единственный путь для выхода
процесса за пределы своего виртуального адресного пространства –
задание виртуального адреса, большего, чем размер сегмента. Этот путь
легко может быть перекрыт, если аппаратура при трансляции адресов
будет сравнивать виртуальный адрес с длиной сегмента и выполнять
прерывание-ловушку, если виртуальный адрес больше.

Рисунок 3.4 Односегментная модель

89

То обстоятельство, что процесс работает в виртуальных адресах,
делает возможным перемещение сегментов в реальной памяти. Переместив
процесс в другую область реальной памяти, ОС просто изменяет поле
базового адреса в дескрипторе его сегмента. Поскольку, как и в модели с
фиксированными

разделами,

реальная

память

распределяется

непрерывными блоками переменной длины, здесь применяются те же
стратегии размещения. Но возможное здесь перемещение сегментов
является эффективным способом борьбы с внешними дырами. Сегменты
переписываются в реальной памяти таким образом, чтобы свободных мест
между ними не оставалось, все свободное пространство сливается в один
большой свободный блок и, таким образом, оказывается доступным для
последующего распределения.
Другой возможностью, которую открывает динамическая трансляция
адресов, является вытеснение сегментов. Если даже после перемещения
сегментов запрос на память не может быть удовлетворен, то ОС может
переписать какой-либо сегмент на внешнюю память и освободить
занимаемую им реальную память. Поскольку контекст процесса, который
содержится в вытесненном сегменте, сохраняется, то впоследствии ОС
может вновь загрузить этот сегмент в реальную память, откорректировать
базовый адрес в его дескрипторе и возобновить выполнение процесса.
Перемещение сегментов и (см. ниже) страниц между оперативной и
внешней памятью и наоборот – называется свопингом (swapping), а
составные его части – вытеснением (swap out) и подкачкой (swap in).
Поскольку в модели происходит вытеснение сегментов, должна быть
реализована какая-то его стратегия. Естественно, что наилучшим
кандидатом на вытеснение должен быть сегмент процесса, находящегося в
заблокированном состоянии. Но следует при этом иметь в виду, что
процесс может быть заблокирован потому, что он ожидает завершения
операции ввода-вывода. При вводе-выводе, использующем канал или
90

прямой доступ к памяти, аппаратура ввода-вывода не выполняет
трансляцию адресов (см. главу 6), а производит обмен данными с областью
памяти, реальный адрес которой был задан ей при инициировании
операции. Сегмент, участвующий в такой операции ввода-вывода, должен
быть заблокирован не только от вытеснения, но и от перемещения в
реальной памяти. (Эти же соображения должны учитываться и в других
моделях памяти). При отсутствии подходящих кандидатов на вытеснение в
очереди заблокированных процессов жертва может быть выбрана из
очереди готовых процессов. Естественно назначить жертвой процесс,
имеющий самый низкий приоритет у планировщика процессов. Но если
брать этот приоритет единственным критерием, то имеется потенциальная
опасность возникновения избыточных перемещений. Процесс, имеющий
низкий приоритет, может быть несколько раз вытеснен и вновь подкачан,
но между всеми подкачками и вытеснениями может так и не получить ни
одного кванта обслуживания на центральном процессоре. В системах с
динамическими приоритетами процесс (например, Unix), подкачанный в
оперативную память, защищается от вытеснения некоторой временной
выдержкой, в течение которой он имеет шанс повысить свой приоритет и
получить квант обслуживания. Вытесненный процесс должен быть также
некоторое время выдержан на внешней памяти, прежде чем он может быть
подкачан. В системах со статическими приоритетами приоритет процесса у
планировщика определяет и его приоритет в очереди к ресурсу реальной
памяти.
Процесс, работающий в односегментной модели памяти, имеет
возможность динамически изменять размер своего виртуального адресного
пространства. При выполнении такого запроса от процесса ОС просто
изменяет поле длины в дескрипторе его сегмента, если в реальной памяти
вслед за сегментом процесса имеется свободный участок достаточного

91

размера. Если же такого участка нет, ОС может переместить процесс или
заблокировать его в ожидании освобождения ресурса.

3.4. Многосегментная модель
Расширим модель, рассмотренную в предыдущем разделе, на случай
N сегментов.
Виртуальное пространство процесса разбивается на сегменты,
которые нумеруются от 0 до N-1. Виртуальный адрес, таким образом,
состоит из двух частей: номера сегмента и смещения в сегменте. Эти части
могут либо представляться по отдельности каждая, либо упаковываться в
одно адресное слово, в котором определенное число старших разрядов
будет интерпретироваться как номер сегмента, а оставшаяся часть – как
смещение. В первом случае сегменты могут размещаться произвольным
образом в виртуальном адресном пространстве. Во втором случае
создается впечатление плоского адресного пространства с адресами от 0 до
максимально возможного при данной разрядности виртуального адреса, но
в этом пространстве могут быть дыры – виртуальные адреса, для процесса
недоступные: из-за отсутствия соответствующих сегментов или из-за
сегментов, длина которых меньше максимально возможной.
Количество

сегментов

и

максимальный

размер

сегмента

ограничивается аппаратурой – разрядностью полей адресного слова. При
выделении процессу реальной памяти каждый сегмент размещается в
непрерывной области реальной памяти, но сегменты, смежные в
виртуальной памяти, могут попадать в несмежные области памяти
реальной. Теперь для процесса уже недостаточно одного дескриптора
сегмента: он должен иметь таблицу таких дескрипторов в составе своего
блока контекста. Аппаратный регистр дескриптора сегмента превращается
в регистр адреса таблицы дескрипторов, он хранит указатель на таблицу
92

дескрипторов активного процесса и перезагружается при смене активного
процесса.

Вычисление

реального

адреса

аппаратурой

несколько

усложняется, как показано на рисунке 3.5:
• выбирается сегментная часть виртуального адреса, она служит
индексом в таблице дескрипторов; по индексу выбирается запись
той таблицы, адрес которой находится в регистре адреса таблицы
дескрипторов;
• выбранная

запись

является

дескриптором

сегмента,

часть

виртуального адреса, соответствующая смещению, сравнивается с
полем длины в дескрипторе;
• если смещение в сегменте не превышает его длины, вычисляется
реальный адрес как сумма базового адреса из дескриптора
сегмента и смещения из виртуального адреса.

Рисунок 3.5 Трансляция адресов. Многосегментная модель

Примерная структура аппаратно поддерживаемого дескриптора
сегмента приведена на рисунке 3.6. Подчеркиваем, что данная структура
не является обязательной, для всех компьютерных архитектур, мы привели
лишь наиболее распространенный ее вариант и использовали наиболее
часто употребляемые имена полей.

93

Рисунок 3.6. Примерная структура дескриптора сегмента

Допустимое

количество

сегментов определяется

разрядностью

соответствующего поля виртуального адреса и может быть весьма
большим. Либо аппаратура должна иметь специальный регистр размера
таблицы дескрипторов (такой регистр есть в Intel-Pentium), либо ОС
должна подготавливать для процесса таблицу максимально возможного
размера, отмечая в ней дескрипторы несуществующих сегментов
(например, нулевым значением поля size). Отметим, что для систем,
упаковывающих номер сегмента и смещение в одно адресное число,
разрядность смещения не является ограничением на длину виртуального
сегмента. Виртуальный сегмент большего размера представляется в
таблице двумя и более обязательно смежными дескрипторами. С точки
зрения процесса он обращается к одному сегменту, задавая в нем большое
смещение, на самом же деле переполнение поля смещения переносится в
поле номера сегмента. Если же простая двоичная арифметика не
обеспечивает модификацию номера сегмента, возможность работы с
большими сегментами может поддерживаться ОС путем особой обработки
прерывания-ловушки "защита памяти".
Каковы преимущества многосегментной модели памяти?
Самое первое преимущество заключается в том, что у процесса
появляется возможность разместить данные, обрабатываемые различным
образом, в разных сегментах своего виртуального пространства (так, в ОС
94

Unix, например, каждый процесс имеет при начале выполнения три
сегмента: кодов, данных и стека). Каждому сегменту могут быть
определены свои права доступа. Поскольку обращения к памяти могут
быть трех видов: чтение, запись и передача управления, то для описания
прав доступа достаточно 3-битного поля Read-Write-eXecute, каждый
разряд которого определяет разрешение одного из видов доступа.
Аппаратные средства большинства архитектур обеспечивают контроль
права доступа при трансляции адресов: поле прав доступа включается в
дескриптор сегмента и если поступивший вид обращения не разрешен, то
выполняется прерывание-ловушка "нарушение доступа".
Другое важное преимущество многосегментной модели заключается
в том, что процесс имеет возможность использовать виртуальное адресное
пространство, размер которого больше, чем размер доступной реальной
памяти. Это достигается за счет того, что не обязательно все сегменты
процесса

должны

Дескриптор

одновременно

каждого

сегмента

находиться
содержит

бит

в

реальной
present,

памяти.
который

установлен в 1, если сегмент подкачан в оперативную память, или в 0, –
если сегмент вытеснен из нее. Аппаратура трансляции адресов проверяет
этот бит и при нулевом его значении выполняет прерывание-ловушку
"отсутствие сегмента" (segment falure). В отличие от большинства других
ловушек, которые в основном сигнализируют об ошибках, при которых
дальнейшее выполнение процесса невозможно, эта не приводит к
фатальным для процесса последствиям. ОС, обрабатывая это прерывание,
находит образ вытесненного сегмента на внешней памяти и подкачивает
его в реальную память. Естественно, что процесс, обратившийся к
вытесненному сегменту, переводится в ожидание; это ожидание может
затянуться, если у ОС имеются проблемы с ресурсом реальной памяти.
Когда сегмент будет подкачан, процесс перейдет в очередь готовых и
будет активизирован вновь с той команды, которая вызвала прерывание95

ловушку. В тех аппаратных системах, которые не обрабатывают бит
присутствия в дескрипторе сегмента, можно вместо него использовать
поле size: ОС должна сбрасывать это поле в 0 при вытеснении сегмента и
восстанавливать при его подкачке.
Естественно, что для подкачки сегмента ОС должна знать его адрес
на внешней памяти. Подавляющее большинство систем не поддерживают
аппаратно поле внешнего адреса в дескрипторе сегмента. Для хранения его
ОС может либо использовать поле base, либо хранить этот адрес в
расширении таблицы дескрипторов, в котором для каждого сегмента
хранится информация о нем, обрабатываемая только программно. В том
же расширении может храниться истинное значение поля size при
использовании его вместо поля present.
Очевидно, что в многосегментной модели свопинг приобретает более
интенсивный

характер.

Следовательно,

эффективность

стратегий

управления памятью имеет еще больший вес. Поскольку в реальной
памяти размещаются сегменты разной длины, проблемы размещения
остаются те же. Для многосегментной организации характерны внешние
дыры, борьба с которыми ведется перемещением сегментов. Если
перемещение сегментов не освобождает такого количества реальной
памяти, которое требуется для удовлетворения запроса, ОС может принять
решение о вытеснении какого-либо сегмента и освобождении занимаемой
им реальной памяти. Некоторые системы (Unix) вытесняют из памяти
какой-либо процесс целиком со всеми принадлежащими ему сегментами,
но такое решение может быть эффективным только в том случае, если
процессы имеют по незначительному числу сегментов каждый. Стратегии
вытеснения мы подробно рассмотрим позже – в страничной модели
памяти.
Отметим, что вытеснение сегмента – не обязательно долгий процесс.
Если сегмент не имеет прав доступа для записи, то он содержит команды
96

или статические инициализированные данные, копия этого сегмента уже
есть на внешней памяти – в файле, содержащем загрузочный образ
процесса. Поэтому вытеснение такого сегмента не сопровождается
перезаписью его на внешнюю память. Даже если в сегмент и разрешена
запись, то, возможно, запись еще не производилась, такой сегмент тоже
незачем перезаписывать на внешнюю память. Поля used и dirty в
дескрипторе

сегмента

используются

при

принятии

стратегических

решений о вытеснении.
Вытеснение одного или нескольких сегментов процесса еще не
приводит к его блокированию. Процесс может продолжать выполняться до
тех пор, пока не обратится к одному из вытесненных сегментов.
В многосегментной модели значительно увеличивается объем
управляющей информации, сохраняемой в блоке контекста процесса, даже
когда процесс не выполняется. Реальная память, занимаемая блоками
контекста неактивных и даже полностью вытесненных процессов, не
может быть отдана другим процессам. Такие участки памяти носят
название

заголовочных

дыр.

Если

потери

реальной

памяти

на

заголовочные дыры оказываются недопустимо большими, то имеет смысл
разбить блок контекста процесса на две части: меньшую, обязательно
сохраняемую в реальной памяти, и большую, которая может быть
вытеснена. В литературе [29, 41] описана ОС MULTICS, в которой для
номера сегмента отводится 18 двоичных разрядов. Таблица сегментов
процесса может быть настолько большой, что и одна она не поместится в
оперативной памяти. Для преодоления этого противоречия таблица
сегментов разбивается на страницы, которые подвергались свопингу. (ОС
MULTICS была признана неудачным проектом и не получила широкого
распространения, но значительно повлияла на последующие проекты ОС,
прежде всего, – на Unix. Небольшое число инсталляций MULTICS
эксплуатируется еще и сейчас.)
97

Чрезвычайно важным преимуществом многосегментной модели
является возможность разделения сегментов процессами. Процессы могут
быть разработаны так, чтобы виртуальные пространства двух или более
процессов

перекрывались

в

каких-то

областях.

Процессы

могут

использовать общее виртуальное пространство для обмена данными.
Реализация этой возможности в какой-то мере зависит от аппаратных
средств. Во многих вычислительных системах процесс может работать
более чем с одной таблицей дескрипторов, поскольку в системе имеется
несколько регистров адресов таблиц (так, в процессоре Intel 80286 и
последующих предусмотрены две таблицы, называемые локальной и
глобальной). Решение, использующее это свойство, заключается в том, что
дескриптор разделяемого сегмента помещается в общую для всех
процессов (глобальную) таблицу, такой сегмент может быть доступным
для всех процессов и имеет общий виртуальный номер для всех процессов.
Другое решение, возможное и при наличии только одной таблицы для
каждого процесса, заключается в том, что для общего сегмента создается
по записи в таблице каждого процесса, с ним работающего, для каждого
процесса этот сегмент имеет свой виртуальный номер. Второе решение
представляется более удачным с точки зрения защиты, так как , во-первых,
доступ к сегменту имеют только те процессы, в таблицах которых созданы
соответствующие дескрипторы, во-вторых, есть возможность дать разным
процессам разные права доступа к разделяемому сегменту. Но за такое
решение приходится платить тем, что при свопинге разделяемого сегмента
и

при

учете

его

использования

необходимо

корректировать

его

дескрипторы в таблицах всех процессов.
Что касается стратегии подкачки, то все ОС применяют в сегментной
модели "ленивую" политику: вытесненный сегмент подкачивается в
реальную память только при обращении к нему. Некоторые системы
(например, OS/2) позволяют управлять начальной подкачкой сегментов
98

при запуске процесса: сегмент может быть определен программистом как
предварительно загружаемый (preload) или загружаемый по вызову (load
on

call).

Разработать

неубыточную

стратегию

упреждающей

(до

обращения) подкачки сегментов при свопинге пока не представляется
возможным из-за отсутствия надежных методов предсказания того, к
какому сегменту будет обращение в ближайшем будущем.
В многосегментной модели процесс имеет возможность динамически
изменять структуру своего виртуального адресного пространства. Для этих
целей ему должен быть доступен минимальный набор системных вызовов,
приводимый ниже.
Получить сегмент:
seg = getSeg (segsize, access);
ОС выделяет новый сегмент заданного размера и с заданными правами
доступа и возвращает процессу виртуальный номер выделенного сегмента.
Освободить сегмент:
freeSeg(seg);
сегмент с заданным виртуальным номером становится недоступным для
процесса.
Изменить размер сегмента:
chSegSize(seg, newsize).
Изменить права доступа к сегменту:
chSegAccess(seg, newaccess).
В конкретных системах этот минимальный набор может быть
расширен дополнительным системным сервисом.
Системные вызовы, связанные с разделяемыми сегментами, мы
рассмотрим в главе, посвященной взаимодействию между процессами.

99

3.5. Страничная модель
Страничную

организацию

памяти

легко

представить

как

многосегментную модель с фиксированным размером сегмента. Такие
сегменты называются страницами. Вся доступная реальная память
разбивается на страничные кадры (page frame), причем границы кадров в
реальной памяти фиксированы. Иными словами, реальная память
представляется как массив страничных кадров.
Виртуальный адрес состоит из номера страницы и смещения в
странице, система поддерживает таблицу дескрипторов страниц для
каждого процесса. Дескриптор страницы в основном подобен дескриптору
сегмента, но в нем может быть сокращена разрядность поля base, так как
в нем хранится не полный реальный адрес, а только номер страничного
кадра, а необходимость в поле size вообще отпадает, так как размер
страниц фиксирован.
Проблема размещения значительно упрощается, так как любой
страничный кадр подходит для размещения любой страницы, необходимо
только вести учет свободных кадров. За счет этого страничная организация
оказывается удобной даже при отсутствии свопинга, так как позволяет
разместить непрерывное виртуальное адресное пространство в несмежных
страничных кадрах. (Иногда для обозначения свопинга на уровне страниц
применяют специальный термин paging – страничный обмен.) Внешние
дыры в страничной модели отсутствуют, зато появляются внутренние
дыры за счет недоиспользованных страниц. При наличии в системе
свопинга нулевое значение бита present вызывает прерывание-ловушку
"страничный отказ" (page falure) и подкачку страницы в реальную память.
Для учета занятых/свободных страниц подходит техника битовой карты,
100

но большинство ОС используют в качестве элементов карты (таблицы
страничных кадров) не биты, а куда более сложные структуры, из которых
могут составляться и многосвязные списки (в том числе и списки
свободных кадров).
Какой

размер

страницы

выгоднее



большой

или

малый?

Соображения, которые могут повлиять на выбор размера, следующие:
• при малых страницах получаются меньшие внутренние дыры;
• при малых страницах меньше вероятность страничного отказа (так
как больше страниц помещается в памяти);
• при больших страницах меньшие аппаратные затраты (так как
разбиение памяти на большие блоки обойдется дешевле);
• при больших страницах меньшие заголовочные дыры и затраты на
поиск и управление страницами (таблицы имеют меньший
размер);
• при больших страницах выше эффективность обмена с внешней
памятью.
Интересно, что разные авторы [12] и [36], приводя почти идентичные
соображения

по

этому

поводу,

приходят

к

диаметрально

противоположным выводам – о преимуществе малых [12] или больших
[36] страниц. Вообще можно проследить по большинству источников, что
рекомендуемые размеры страниц растут с возрастанием года издания. Повидимому, решающим фактором здесь является вопрос стоимости памяти.
Со временем стоимость этого ресурса уменьшается, а доступные объемы
увеличиваются, поэтому более поздние авторы придают меньше значения
потерям памяти.
Поскольку размер страницы обычно выбирается намного меньше,
чем размер сегмента в предыдущей модели, страничная организация
позволяет значительно увеличить уровень мультипрограммирования за
счет того, что в реальную память могут в каждый момент отображаться
101

только самые необходимые части виртуальных пространств процессов. Но
при этом еще более возрастает интенсивность свопинга и соответственно
роль стратегии вытеснения/подкачки страниц. При проектировании систем
со страничным свопингом разработчики должны придерживаться двух
основополагающих правил:
• процесс при большинстве своих обращений к памяти должен
находить требуемую страницу уже в реальной памяти;
• если потери на свопинг превышают допустимую норму, то должен
понижаться уровень мультипрограммирования.
Несоблюдение этих правил делает весьма вероятным возникновение
такой

ситуации,

когда

система

будет

занята

только

хаотичным

перемещением страниц. Например, выполнение команды процессора
S/390:
MVС память,память
может потребовать трех страниц памяти: команды, первого и второго
операндов. Если одной из страниц недостает, процесс блокируется в
ожидании ее подкачки. Но при неудачной стратегии за время этого
ожидания он может потерять другие необходимые страницы, и повторная
попытка выполнить команду вновь приведет к прерыванию-ловушке. В
англоязычной литературе эту ситуацию называют trash – толкотня, в
русскоязычной часто используется транскрипция – треш.
При оценке эффективности стратегии свопинга показательной
является зависимость частоты страничных отказов от числа доступных
страничных кадров. Качественный вид этой зависимости, присущий всем
стратегиям, показан на рисунке 3.7. Из него видно, что при уменьшении
объема реальной памяти ниже некоторого ограниченного значения число
страничных отказов начинает расти экспоненциально. Естественно, что
показателем эффективности стратегии может служить степень близости
колена этой кривой к оси ординат. Другой возможный критерий
102

эффективности – площадь области, расположенной под этой кривой; чем
она меньше, тем выше эффективность.

Рисунок 3.7 Зависимость частоты отказов от объема реальной памяти

С точки зрения стратегии размещения не имеет значения, какой
страничный кадр занимать при подкачке. Но как выбрать один кадр из
всего пула кадров? Конечно, наилучшим кандидатом является полностью
свободный кадр – такой, который еще не занимался или был когда-то
выделен процессу, ныне уже закончившемуся. Но если суммарный объем
виртуальных

адресных

пространств

всех

процессов

существенно

превышает объем реальной памяти, то такие страницы являются более чем
дефицитным ресурсом. Выход состоит в том, что для каждого
распределенного кадра в таблице страничных кадров (и/или в таблице
дескрипторов) ведется учет факторов, определяющих выбор его в качестве
жертвы для вытеснения, и из всех кадров выбирается тот, который
является лучшим (с точки зрения принятой стратегии) кандидатом в
жертвы (OS/2, Windows 95). Несколько более сложное решение: ОС ведет
список свободных (условно свободных) кадров, и очередная жертва
выбирается из этого списка. Страничный кадр попадает в список жертв,
103

если его "показатель жертвенности" превышает некоторое граничное
значение, но может быть еще "спасен" из списка жертв, если во время
пребывания в этом списке он будет востребован. Помещение кадра в
список жертв может выполняться либо сразу по достижении "показателя
жертвенности" граничного значения (VM), либо (ленивая политика – Unix)
размер списка поддерживается в определенных границах за счет
периодического

его

пополнения

путем

просмотра

всей

таблицы

страничных кадров.
В качестве образцов для сравнения в литературе рассматриваются
стратегии вытеснения RANDOM и OPT. Стратегия RANDOM заключается
в том, что страница для вытеснения выбирается случайным образом.
Понятно, что достичь высокой эффективности при такой стратегии
невозможно, и любая другая введенная нами стратегия может считаться
сколько-нибудь разумной только в том случае, если она, по крайней мере,
не хуже стратегии RANDOM. Стратегия OPT требует, чтобы в первую
очередь вытеснялась страница, к которой дольше всего не будет
обращений в будущем. Интуитивно понятно и строго доказано, что эта
стратегия является наилучшей из всех возможных. Но, к сожалению, эта
стратегия в идеальном варианте нереализуема из-за невозможности точно
прогнозировать требования. Реально применяемые стратегии могут
оцениваться по степени приближения ихрезультатов к результатам OPT.
Стратегия FIFO (первый на входе – первый на выходе) является
простейшей. Согласно этой стратегии из реальной памяти вытесняется та
страница, которая была раньше других в нее подкачана. Для реализации
этой стратегии ОС достаточно организовать список-очередь страниц в
реальной памяти с занесением подкачиваемой страницы в "голову" списка
и выборкой страницы для вытеснения из "хвоста" списка. Хотя стратегия
FIFO и лучше, чем RANDOM, она не учитывает частоты обращений:
может быть вытеснена страница, к которой происходят постоянные
104

обращения. Более того, при некоторых комбинациях страничных
требований FIFO может давать аномальные результаты: увеличение числа
страничных отказов при увеличении числа доступных страничных кадров
(см. задания в конце главы).
Многие используемые в современных ОС стратегии вытеснения
могут рассматриваться как разновидности стратегии LRU (least recently
used) – наименее используемая в настоящее время: вытесняется та
страница, к которой дольше всего не было обращений. Это можно
рассматривать

как

попытку

приближения

стратегии

OPT

путем

экстраполяции потока страничных требований из прошлого на будущее.
Разновидности LRU различаются тем, как они учитывают время
использования страницы. Очевидно, что запоминание точного времени
обращения к каждой странице обошлось бы слишком дорого. Стратегии
LRU используют биты used и dirty дескриптора страницы для оценки
этого времени. Бит used устанавливается в 1 аппаратно при любом
обращении к странице, бит dirty устанавливается аппаратно при
обращении к странице для записи; оба бита сбрасываются ОС по своему
усмотрению. Все множество присутствующих в реальной памяти страниц
разбивается на четыре подмножества, в зависимости от значений этих
полей:
1) неиспользованные чистые (used=0, dirty=0);
2) неиспользованные грязные (used=0, dirty=1);
3) использованные чистые (used=1, dirty=0);
4) использованные грязные (used=1, dirty=1).
Чем меньше номер подмножества, в которое входит страница, тем
желательнее она в роли жертвы. Внутри одного подмножества жертва
может выбираться методом циклического поиска или случайным образом.
ОС должна выбрать момент, когда она будет сбрасывать биты used в 0.
105

Ленивая политика состоит в том, чтобы делать это только, когда уже не
остается неиспользованных страниц. Противоположные варианты – при
каждом поиске жертвы сбрасывать биты used для всех страниц или
только для проверенных в ходе поиска. Наконец, общий сброс битов used
может производиться по таймеру.
Интересным образом используется бит dirty в стратегии SCC
(second cycle chance) – цикл второго шанса. Алгоритм этого варианта
стратегии LRU осуществляет циклический просмотр таблицы страничных
кадров. Разумеется, лучшим кандидатом является неиспользованная
страница (подмножества 1 и 2). Но если таковых нет, то выбирается
страница с нулевым полем dirty (подмножество 3). Просмотренные
страницы, оказавшиеся грязными, ОС не трогает (пока), но сбрасывает в
них поле dirty. Таким образом, даже если при полном обороте поиска не
будет найдена жертва, она обязательно найдется при втором обороте.
"Второй шанс" здесь состоит в том, что страница, принудительно
отмеченная как чистая, может еще в промежутке между двумя поисками
восстановить поле dirty. При такой стратегии поле dirty уже не
отражает

истинного

состояния

страницы,

ОС

должна

сохранять

действительное состояние в расширении дескриптора страницы.
Более сложные варианты стратегии LRU предусматривают более чем
одноразрядный учет времени. Метод временного штампа (timestamp)
предусматривает хранение для каждой страницы многоразрядного кода.
Периодически этот код сдвигается на разряд влево, а в освободившийся
старший разряд заносится текущее значение поля used, после чего поле
used сбрасывается в 0. Код временного штампа хранит, таким образом,
предысторию использования страниц. Наилучшей жертвой оказывается та
страница, у которой значение штампа (если интерпретировать его как
целое число без знака) минимальное.
106

Метод возраста страницы (Unix) подобен предыдущему, но здесь с
каждой страницей связывается число – ее "возраст". При периодическом
просмотре таблицы страничных кадров для страницы, у которой бит used
равен 0, возраст увеличивается на 1, если же бит used установлен в 1, то
он сбрасывается в 0, а возраст не меняется. Когда системный процесс
"сборщик страниц" пополняет список свободных страниц, он заносит в
него те страницы, для которых превышен установленный граничный
возраст.
Выше мы использовали биты used и dirty, предполагая, что они
поддерживаются аппаратно. Однако возможна реализация указанных
стратегий и при отсутствии такой аппаратной поддержки. В этом случае
недостающие поля содержатся в расширении дескриптора страницы, а
вместо них ОС сбрасывает в 0 бит present. Бит present в дескрипторе
уже не отражает истинного состояния и дублируется в расширении.
Обращение к такой странице вызывает ловушку "страничный отказ",
однако ОС убеждается по расширению дескриптора, что страница на
самом деле уже есть в реальной памяти, и вместо ее подкачки
корректирует бит present в основном дескрипторе, одновременно
устанавливая used в расширении.
Указанные стратегии применимы как к локальному, так и к
глобальному управлению памятью. В первом случае каждому процессу
выделяется статический пул страничных кадров, и свопинг ведется в
пределах этого пула (например, AS/400). Это дает возможность применять
для разнотипных процессов разные стратегии, но требует принятия
решения о размере каждого пула. В случае же глобального управления
выбранная стратегия применяется по всему доступному множеству
страничных кадров и производится постоянное перераспределение
ресурсов между процессами (например, VM, MVS). Динамическое
107

перераспределение – качество полезное, но если оно не учитывает уровень
мультипрограммирования, то может привести к толкотне (трешу).
Ряд

глобальных

стратегий,

управляющих

уровнем

мультипрограммирования, основывается на идее рабочего набора WS
(working set). Идея эта базируется на явлении локализации обращений к
памяти. Любая программа не обращается ко всему своему адресному
пространству одновременно. На каждом временном отрезке программа
работает только с некоторым подмножеством адресов и соответственно с
некоторым подмножеством страниц. Временной отрезок, на котором это
подмножество квазипостоянно, называется фазой программы. Почти
полное обновление этого подмножества называется сменой фазы. Рабочим
набором процесса S(w) называется перечень страниц, к которым процесс
обращался в течение последнего интервала виртуального времени w.
Методы, основанные на идее рабочего набора, стремятся к тому, чтобы
выполняющийся процесс постоянно имел свой рабочий набор в реальной
памяти и страницы, входящие в рабочие наборы выполняющихся
процессов, не вытеснялись. Если у ОС нет возможности разместить в
реальной памяти весь рабочий набор процесса, она снижает уровень
мультипрограммирования, переводя процесс в состояние ожидания. При
разблокировании процесса ОС имеет возможность перед его активизацией
выполнить упреждающую подкачку (preload) всего его рабочего набора и
тем самым значительно снизить частоту страничных отказов.
На практике, однако, идеальная реализация стратегии WS не
представляется возможной по тем же соображениям, что и для идеальной
LRU:

нет

возможности

Адаптированный

метод

запоминать
WS

состоит

время
в

его

каждого

обращения.

определении

через

фиксированные интервалы времени. В начале каждого интервала все
страницы, для которых распределены страничные кадры, считаются
входящими в рабочий набор. В течение интервала все запросы процесса на
108

новые страницы удовлетворяются. По окончании интервала те страницы,
которые не были использованы (бит used), удаляются из рабочего набора.
Зафиксированный в конце интервала рабочий набор служит исходным для
следующего интервала.
Некоторые методы стратегии WS не сохраняют полный перечень
страниц, входящих в рабочий набор, а только определяют его обобщенные
показатели. Алгоритм метода рабочего размера замеряет только размер
рабочего набора и выделяет процессу соответствующее число страничных
кадров (VM). Метод частоты страничных отказов основан на измерении
интервала виртуального времени между двумя страничными отказами,
если этот интервал меньше нижней границы, процессу выделяется
дополнительный страничный кадр, если интервал больше верхней
границы, у процесса отбирается один страничный кадр. Естественно, что
методы, не сохраняющие всего списка рабочего набора, не имеют
возможности выполнять упреждающую его подкачку.
При страничном свопинге, как и при сегментном, применяется, как
правило, стратегия подкачки по запросу (demand paging), так как
реализовать полностью безубыточную стратегию упреждающей подкачки
невозможно. Тем не менее, в стратегии WS появляется возможность
упреждающей подкачки с минимальными убытками. В системах, имеющих
большой объем памяти и не особенно заботящихся о минимизации ее
потерь, иногда применяется также кластеризация подкачки. Этот метод
также базируется на локализации и исходит из того, что если произошло
обращение к некоторой странице, то с большой вероятностью можно
ожидать в ближайшем будущем обращений к соседним с ней страницам,
которые и подкачиваются, не дожидаясь этих обращений.
Системные вызовы страничной модели "не видят" страничной
структуры и обращаются к памяти как к линейному пространству
виртуальных адресов.
109

Выделение памяти происходит при помощи системного вызова:
vaddr = getMem(size),
который возвращает виртуальный адрес выделенной области заданного
размера. На самом деле размер выделенной области кратен размеру
страницы, а ее адрес выровнен по границе страницы.
Освобождение памяти:
freeMem(vaddr)
Поскольку память выделяется страницами, при выделении памяти для
маленьких (существенно меньше размера страницы) объектов образуются
значительные внутренние дыры. Для того, чтобы избежать этих потерь,
некоторые системы обеспечивают работу с кучей (heap) – заранее
выделенной областью памяти размером в одну или несколько страниц с
последующим выделением памяти для маленьких объектов в куче.
Соответствующие системные вызовы:
heapId = createHeap(size);
vaddr = getHeapMem(heapID,size);
freeHeapMem(vaddr)

3.6. Сегментно-страничная модель
Из предыдущего изложения должно быть ясно, что сегментная
модель памяти ориентирована в большей степени на программиста, а
страничная – на ОС. Программисту удобно компоновать команды и
данные своего процесса в блоки переменной длины (сегменты). ОС же
удобнее управлять памятью, разбитой на блоки постоянной длины
(страницы). Естественным путем развития моделей памяти явилось
объединение достоинств этих двух моделей в одной – сегментностраничной.
110

Виртуальный адрес теперь состоит из трех частей: номера сегмента,
номера страницы в сегменте и смещения в странице. Аппарат трансляции
адресов, представленный на рисунке 3.8, по крайней мере трехшаговый:
• регистр адреса дескриптора указывает на таблицу сегментов, из
нее выбирается дескриптор сегмента, а из последнего – адрес
таблицы страниц;
• из таблицы страниц выбирается дескриптор страницы, а из него
номер страничного кадра;
• реальный

адрес

получается

сложением

базового

адреса

страничного кадра со смещением в странице.

Рисунок 3.8 Трансляция адресов. Cегментно-страничная модель

Такой аппарат трансляции адресов поддерживается во многих
современных процессорных архитектурах. Иногда алгоритм вычисления
адреса состоит и из большего числа шагов. Серьезным недостатком этой
модели является многоступенчатость трансляции адресов. Эта проблема
решается

на

аппаратном

уровне

путем

применения

сверхбыстродействующей (обычно ассоциативной) памяти для хранения
части таблиц.
Поскольку в модели, приведенной на рисунке 3.8, каждый сегмент
имеет собственную таблицу страниц, сами таблицы страниц могут
занимать значительный объем в памяти. Простейшее решение этой
111

проблемы представляет Windows 3.x: в системе существует единственная
таблица страниц. Сегментная часть трансляции адреса имеет, таким
образом, на выходе адрес в общем для всех процессов виртуальном
страничном пространстве, объем которого превышает объем реальной
памяти не более, чем в 4 раза. Подобное же, хотя и более гибкое и
защищенное решение, представляет VSE: система обеспечивает общий
объем виртуальной памяти (до 2 Гбайт), который разбивается на разделы
(до 12 статических и до 200 динамических), суммарный объем адресных
пространств всех разделов не превышает общего объема виртуальной
памяти. Простота решения, однако, может существенно сказываться на его
эффективности: во-первых, из-за ограничений на размер виртуальной
памяти, во-вторых, из-за необходимости выделять смежные дескрипторы в
таблице страниц для страниц, смежных в виртуальной памяти. Поэтому
действительно

многозадачные

системы

применяют

множественные

таблицы страниц и включают память, занимаемую таблицами страниц, в
страничный обмен. Вытеснение и загрузка частных таблиц страниц
производится либо исключительно самой ОС (Unix), либо ОС использует
для этого имеющиеся аппаратные средства (так, ОС, ориентированные на
Intel-Pentium, используют каталоги страниц).
С точки зрения программиста, он имеет дело с сегментами, API
управления памятью – такой же, как и в сегментной модели. Страничная
часть адресации для процессов совершенно прозрачна, но зато эта часть
используется ОС для эффективной организации свопинга.

3.7. Плоская модель
Начиная с модели Intel 80386, в микропроцессорах Intel-Pentium
адрес состоит из 16-разрядного номера сегмента и 32-разрядного
смещения. 32-разрядное поле смещения позволяет адресовать до 4 Гбайт в
112

пределах одного сегмента, что более чем достаточно для большинства
мыслимых приложений и позволяет реализовать действительно "плоскую"
(flat) модель виртуальной памяти процесса, представляющую собой
линейное непрерывное пространство адресов..
Однако при размере страницы 4 Кбайт таблица страниц должна
содержать более 106 элементов и занимать 4 Мбайт памяти. Для экономии
памяти аппаратура трансляции адреса микропроцессора поддерживает
таблицы страниц двух уровней. Страничная таблица верхнего уровня
называется каталогом страниц. Старшие 10 байт 32-разрядного смещения
являются номером элемента в страничном каталоге. Элемент страничного
каталога адресует таблицу страниц второго уровня. Следующие 10 байт
смещения являются номером элемента в таблице страниц второго уровня.
Элемент таблицы второго уровня адресует страничный кадр в реальной
памяти, а младшие 12 байт смещения являются смещением в странице.
Сегментная часть аппарата трансляции адреса оказывается излишней, но в
Intel-Pentium она не может быть отключена, но тот же эффект достигается,
если каждому процессу назначается только один сегмент и для процесса
создается таблица сегментов, содержащая только один элемент. Поле
base этого элемента адресует страничный каталог процесса, а каждый
элемент страничного каталога – одну таблицу страниц. Структуры
элементов каталога страниц и таблицы страниц второго уровня идентичны,
каждая таблица страниц (каталог или таблица второго уровня) содержит
1024 элемента и сама занимает страничный кадр в памяти. Таблицы
страниц участвуют в страничном обмене так же, как и страницы,
содержащие любые другие данные и коды.
В 4-Гбайтном адресном пространстве появляется возможность
разместить не только коды и данные процесса, но и объекты,
используемые им совместно с другими процессами, в том числе и модули
самой ОС. В этом случае обращение процесса к ОС происходит как
113

обращение к процедуре, размещенной в адресном пространстве самого
процесса. В современных ОС структура адресного пространства процесса
обычно бывает следующей:
• самая младшая часть адресного пространства обычно для
процесса недоступна, она используется ОС для поддержки
реального режима; размер этой части адресного пространства
обычно не менее 4 Мбайт, что соответствует одному элементу
страничного каталога;
• далее размещается частное адресное пространство процесса,
содержащее его коды, локальные данные, стек;
• выше

размещаются

"прикладные"

общие

области

памяти,

используемые несколькими процессами совместно;
• еще

выше



непривилегированном

системные
режиме,

модули,
эти

работающие
модули

в

совместно

используются всеми процессами;
• наконец, в самой верхней части размещаются системные модули,
работающие в режиме ядра (уровень привилегий – 0), эти модули
также используются совместно.
Совместное использование памяти обеспечивается либо тем, что
элементы каталогов разных процессов адресуют одну и ту же таблицу
страниц второго уровня, либо тем, что таблицы страниц второго уровня
разных процессов адресуют один и тот же страничный кадр. В первом
случае виртуальные адреса совместно используемых объектов являются
одинаковыми для всех процессов, во втором – разными. Все системы
используют первый способ для системных модулей, но разные способы –
для "прикладных" общих областей памяти.
Большинство разработчиков приложений горячо приветствовали
введение плоской модели памяти в современных ОС (OS/2 Warp, Windows
95, Windows NT), так как представление виртуального адреса в виде
114

одного 32-разрядного слова избавляет программиста от необходимости
различать ближние и дальние указатели и упрощает программирование. Но
справедливы и предупреждения о том, что отказ от сегментного
структурирования виртуального адресного пространства кое в чем
ограничивает возможности программиста [23]. Большая же эффективность
плоской модели памяти является объективным фактором, так как, вопервых, оперирование с 32-разрядными адресными словами уменьшает
число команд в программе, а во-вторых, в 4-Гбайтном виртуальном
адресном пространстве процесса могут быть размещены и процедуры,
реализующие системные вызовы, таким образом, обращения процесса к
ОС происходят, как к собственным локальным процедурам и не требуют
переключений контекста.
Несколько усложняется защита памяти при фактическом отказе от
сегментирования. В Intel-Pentium в аппаратном дескрипторе сегмента
предусмотрено

пять

двоичных

разрядов,

которые

могут

быть

использованы для целей защиты, а в дескрипторе страницы – только два
таких разряда. Однако объединение средств защиты на уровне каталога
страниц

и

таблиц

второго

уровня

образует

достаточно

богатые

возможности. Надежность защиты памяти в современных ОС определяется
только

тем,

насколько

активно

и

аккуратно

эти

возможности

используются.

3.8. Одноуровневая модель
Дальнейшее расширение разрядной сетки процессоров может
привести к появлению совершенно новых моделей памяти. Сейчас трудно
с уверенностью прогнозировать, какая модель будет доминировать, на
сегодняшний день большинство 64-разрядных ОС представляют собой
клоны Unix, и расширенный виртуальный адрес используется в них для
115

отображения в память файлов. Более активно использует 64-разрядный
адрес AS/400 [27]. На примере последней мы и рассмотрим одноуровневую
(single-level) модель памяти. Эта модель была реализована уже в System/38
и в ранних моделях AS/400 на базе 48-разрядного адреса, но мы
сосредоточимся на ее 64-разрядной реализации в Advanced Series на
процессоре Power PC
Отметим, прежде всего, что эта "новая" модель памяти на самом деле
является "хорошо забытой старой": в вычислительной системе Atlas
(Англия, 1996 г.) – первой системе с виртуальной памятью – была
реализована именно эта модель. Впоследствии эта модель не нашла
применения из-за ограниченных возможностей аппаратных средств,
современное же состояние аппаратных средств позволяет вновь к ней
вернуться на качественно ином уровне.
64-разрядное адресное слово позволяет процессу иметь плоскую
виртуальную память размером до 16 Эбайт (эксабайт). В AS/400 эта
возможность позволяет реализовать два принципиально важных свойства
модели памяти:
• в виртуальное адресное пространство процесса включается не
только оперативная память (memory), но вся память (storage) – и
оперативная, и внешняя – имеющаяся в системе;
• все процессы работают в одном и том же виртуальном адресном
пространстве, разделяя его.
В традиционных системах любые объекты – обрабатываемые или
выполняемые – должны быть прежде размещены в памяти. Это не
обязательно означает их размещения в реальной оперативной памяти,
такое размещение может быть отложено и выполняться по требованию
механизмами подкачки сегментов или страниц, но виртуальная память
процесса должна быть сформирована – в виде таблиц сегментов и/или
страниц. В системе с одноуровневой памятью, строго говоря, концепция
116

памяти отсутствует, она заменена концепцией пространства (space).
Новосозданный процесс сразу получает свое распоряжение пространство
(виртуальное адресное пространство), в котором уже размещены все
имеющиеся в системе объекты, в том числе и программные коды процесса.
Имеется также достаточно пространства для размещения любых новых
объектов. Для работы с объектом процесс должен не размещать его в
памяти, а должен только получить его адрес в пространстве. Для процесса
прозрачно местонахождение объекта – в оперативной или на внешней
памяти. Физически все объекты размещаются именно на внешней памяти,
а

оперативная

память

(ее

размер

исчисляется

сотнями

Мбайт)

используется почти исключительно как пул страничных кадров.
Одноуровневая
управлением

модель

памятью,

делает

поскольку

излишним
процессам

связанный

API,
нет

с

необходимости

запрашивать и освобождать память. Системные вызовы этой группы
заменяются вызовами доступа к объектам. Защита памяти, таким образом,
реализуется в рамках более общего механизма контроля доступа, который
мы

подробнее

рассмотрим

в

главе

10.

Задача

же

совместного

использования памяти решается совершенно элементарно, так как все
процессы работают в одном виртуальном адресном пространстве.
Плоская структура адресного пространства в одноуровневой модели
снимает необходимость в сегментной фазе динамической трансляции
адреса. В процессоре Power PC имеется возможность программного
отключения трансляции сегмента из аппаратного процесса трансляции
адреса. AS/400 может работать как в режиме с сегментацией, так и без нее.
Режим с сегментацией включается только при повышенном уровне
защиты.
Размер страницы в современных моделях AS/400 – 4Кбайт. Даже в
процессоре Intel 80386 таблица страниц для 32-разрядного адресного
пространства не размещается в оперативной памяти и применяется
117

двухэтапная трансляция номера страницы, как же решается эта проблема
для 64-разрядного адресного пространства? Здесь применяется так
называемая инверсная таблица страниц. В оперативной памяти хранятся
дескрипторы не всех виртуальных страниц, а только тех, которые уже
размещены в оперативной памяти. Поскольку виртуальное адресное
пространство общее для всех процессов, таблица страниц также одна. При
трансляции виртуального адреса требуемая страница станчла ищется в
таблице страниц реальной памяти, а при неуспешном результате такого
поиска – на внешней памяти. Для поиска в таблице страниц применяется
метод хеширования. 64-разрядный виртуальный адрес путем несложных
преобразований, состоящих их ряда побитовых логических операций,
преобразуется в номер элемента таблицы страниц. Число элементов в
таблице страниц зависит от размера оперативной памяти в системе и
выбирается таким образом, что таблица страниц занимает фиксированный
процент реальной оперативной памяти. Поскольку разрядность номера
элемента значительно меньше разрядности виртуального адреса, в
процессе преобразования виртуальных адресов неизбежны коллизии –
случаи преобразования разных виртуальных адресов в один и тот же номер
элемента. Поэтому в элементе таблицы страниц зарезервировано место для
нескольких (восьми) дескрипторов страниц и после выбора элемента
продолжается линейный поиск страницы в элементе таблицы. В таблице
страниц также предусмотрена область переполнения – для случая, если
число коллизий на один элемент таблицы превысит размер элемента, но на
практике до ее использования дело не доходит.
Механизм поиска в таблице страниц может показаться достаточно
сложным и времяемким, но, во-первых, архитектура микропроцессора
Power PC включает в себя конвейер, а во-вторых, значительный объем
ассоциативного буфера страниц (512 и более элементов) позволяет более
чем в 90% случаев даже не производить поиск в таблице страниц.
118

В случае, если страница не найдена в таблице, генерируется
прерывание-ловушка – страничный отказ. Модуль управления памятью в
микроядре при обработке этой ловушки рассматривает виртуальный адрес
как адрес на внешней памяти и передает его подсистеме ввода-вывода для
подкачки страницы в оперативную память. Подкачка может потребовать
освобождения страничного кадра – применяется дисциплина замещения
LRU в пределах страничного пула, выделенного данному процессу. В
системе предусмотрена также возможность использования реальных
адресов памяти – оперативной и внешней, но команды, работающие с
ними, используются только ниже уровня интерфейса MI и недоступны
даже для OS/400.

КОНТРОЛЬНЫЕ ВОПРОСЫ
1.

Часто

единственным

достоинством

виртуальной

памяти

называют возможность обеспечить для процесса объем виртуального
адресного пространства, превышающий объем реальной памяти. Назовите
другие достоинства виртуальной памяти.
2.

В чем достоинства и недостатки преобразования виртуальных

адресов в реальные во время выполнения программы? Какая часть работы
по этому преобразованию выполняется аппаратным обеспечением, а какая
– ОС?
3.

Иногда считают, что

виртуальная память может быть

обеспечена только в системах с аппаратной поддержкой динамической
трансляции адреса. Докажите, что это не так.
4.

Почему при поиске свободной памяти стратегия "самый

подходящий" оказывается хуже, чем "первый подходящий".
5.

Сравните сегментную и страничную модели виртуальной

памяти. Какая из них представляется вам лучшей и почему?
119

6.

Дополните приведенные в разделе 3.5. соображения по поводу

выбора размера страницы.
7.

Смоделируйте ситуацию применения дисциплины вытеснения

FCFS, в которой увеличение числа реальных страниц приведет к
увеличению числа страничных отказов.
8.

Что

такое

кластерная

подкачка

страниц?

Почему

в

современных ОС она становится все более популярной?
9.

Каким образом ОС может определять, к каким страницам

будут обращения в ближайшее время?
10.

Большой

размер

виртуальной

памяти

процесса

может

приводить к тому, что даже таблица страниц не будет помещаться в
реальной памяти. Какими путями решается эта проблема в современных
ОС?
11.

Каким образом снижение стоимости памяти влияет на

дисциплины управления памятью?
12.

Какие принципиальные изменения в концепции памяти может

повлечь за собой увеличение разрядности адреса?

120

Глава 4. Порождение программ и процессов
В первой главе мы дали строгое определение понятия процесса.
Прикладной

программист, однако, разрабатывает не

"процесс", а

"программу", не задумываясь обычно над тем, как и какие механизмы ОС
обеспечат ее представление в виде процесса. Ряд авторов (например, [12,
41]) нестрого определяют процесс как "программу в стадии выполнения".
Такое

определение,

"адаптированное"

для

уровня

прикладного

программиста, в ряде случаев может считаться справедливым и весьма
удобным, так как соответствует интуитивному пониманию этого термина.
Программа превращается в процесс в тот момент, когда ОС создает
для нее блок контекста; последний отвечает за состояние процесса и
представляет процесс в состязаниях за обладание ресурсами. Блок
контекста, однако, не определяет содержание процесса. Содержательная
часть процесса представляется для ОС другой структурой – адресным
пространством процесса. Ядро ОС не обрабатывает содержимое адресного
пространства, а только отвечает за размещение его в памяти.
В первых двух разделах данной главы мы рассматриваем начальное
формирование содержимого адресного пространства: этапы компиляции,
компоновки и загрузки. Эти этапы в терминах функций управления
памятью (рисунок 3.1) выполняют функцию именования. Как правило, эти
этапы выполняются не ядром ОС, а утилитами, задолго до создания блока
контекста процесса. Таким образом, на этих этапах мы имеем дело не с
процессом, а с программой. Два следующих раздела рассматривают
управление адресным пространством программы при ее превращении в
процесс.

121

4.1. Компиляция
Этот этап реализуется не ОС, а системами программирования,
которые

представляют

собой

"системы,

образуемые

языком

программирования, компиляторами или интерпретаторами программ,
представленных на этом языке, соответствующей документацией, а также
вспомогательными средствами для подготовки программ в форме,
пригодной для выполнения" [8]. Системы программирования – предмет
изучения

отдельного

курса,

им

посвящена

обширная

литература

(например, [3, 7, 24]), здесь мы остановимся только на тех их аспектах,
которые имеют отношение к ОС и аппаратным средствам вычислительной
системы.
Основным

функциональным

назначением

системы

программирования является генерация объектного кода программы
(машинных команд). Компиляторы предварительно формируют структуру
виртуального адресного пространства: определяют состав сегментов и
формируют содержимое (образы) кодовых сегментов и сегментов
инициализированных статических данных.
Система

программирования

создает

также

дополнительный

интерфейс между программистом и ОС. Состав и спецификации этого
интерфейса могут быть либо стандартными для языка, либо определяться
конкретной системой программирования. Везде в этом пособии мы
описываем системные вызовы ОС как некоторые процедуры или функции
высокоуровневого языка программирования. В конкретных системах
программирования

набор

таких

процедур

составляет

библиотеки

системных вызовов, эти процедуры обеспечивают передачу вызовов ОС в
той форме, в какой они специфицированы для данной ОС (например, в
виде программного прерывания с передачей параметров через регистры
общего назначения). Многие процедуры систем программирования
122

включают в себя интегрированный системный сервис – выполнение в
составе одной процедуры нескольких системных вызовов с некоторой
обработкой их результатов. Можно говорить о том, что системы
программирования продолжают тенденцию виртуализации ресурсов: они
формируют на базе примитивов, обеспечиваемых системными вызовами
ОС, ресурсы более высокого уровня, доступные через средства системы
программирования. Так, работая на языке высокого уровня, мы имеем в
своем распоряжении виртуальную ЭВМ, в которой "система команд"
представлена операциями, операторами и стандартными процедурами
языка, а адресация выполняется в пространстве символьных имен.
Некоторые языки или их конкретные системы программирования могут
включать в себя и более сложные средства управления ресурсами, такие
как: буферизацию ввода/вывода (см. главу 6), работу с файлами сложной
логической

структуры

(см.

главу

7),

средства

синхронизации

и

взаимодействия процессов (см. главу 8) и т.д.
С целью получения наиболее эффективного объектного кода
компиляторы могут выполнять оптимизацию обрабатываемой программы.
Можно выделить три стадии такой оптимизации:
• системно-независимая;
• системно-зависимая, но аппаратно-независимая;
• аппаратно-зависимая.
Ни одна из указанных стадий не является строго обязательной. На
первой

стадии

выполняется

оптимизация

логической

структуры

программы и ее виртуальной памяти. К оптимизационным процедурам
этого этапа можно отнести: оптимизацию циклов, устранение избыточных
вычислений, преобразование индексных выражений, сокращение числа
переменных и т.п. Эти методы требуют не пооператорного просмотра, а
анализа всей программы или ее участка. Такая оптимизация может
осуществляться либо на уровне исходного языка, либо на уровне
123

некоторого

промежуточного

языка.

Подавляющее

большинство

современных языков высокого уровня воплощает принципы структурного
программирования [10], а

это означает, что, с одной стороны,

эффективность алгоритма зачастую приносится в жертву его ясности, но, с
другой, – что логическая структура программы получается строгой и,
следовательно, легко анализируемой и оптимизируемой. Общность
принципов структурного программирования для разных языков позволяют
также формировать единое промежуточное представление разноязыковых
программ и применять оптимизационные процедуры, не зависящие от
языка.
Системно-зависимая стадия оптимизации связана более всего с
дисциплинами управления памятью в ОС. Если компилятор "знает", какие
алгоритмы

управления

памятью

(например,

страничного

обмена)

применяет ОС, он может "помочь" ОС. Поскольку компилятор, в отличие
от ОС, обладает возможностью глобального анализа программы, он может
предсказывать будущую потребность в тех или иных страницах памяти,
влиять на размер и состав рабочего набора процесса. Компилятор может
также перестроить программу таким образом, чтобы повысить степень
локализации обращений к памяти и тем самым снизить интенсивность
страничного обмена.
Роль аппаратно-зависимой оптимизации все более возрастает с
развитием процессорных архитектур. Мы

уже

отмечали, что на

компилятор, таким образом, возлагается задача расположения команд
программы в такой последовательности, чтобы обеспечить максимальную
загрузку

конвейерных

линий.

Рассмотрим

пример

возможного

дальнейшего развития этой идеи.
Недостатком современных суперскалярных процессоров является
необходимость для процессора в каждом цикле анализировать поток
команд, чтобы определить, какие конвейерные линии могут быть
124

использованы. Это является препятствием как для увеличения числа
линий, так и для сокращения времени цикла. Перспективной для RISСпроцессоров, по-видимому, является идея упаковки нескольких простых
команд в одну большую команду фиксированной длины. Такая команда
называется VLIW (very long instruction word – очень длинное командное
слово). Составляющие VLIW-команды должны выполняться строго
последовательно, сами VLIW-команды могут выполняться параллельно.
Процессор, таким образом, просто загружает очередную VLIW-команду в
очередную конвейерную линию, не занимаясь анализом командного
потока. Задача формирования VLIW-команд с оптимизацией их под
данную платформу ложится на компилятор. На сегодняшний день
подобные подходы применяются, например, в процессорах фирмы HewlettPackard и процессоре Itanium фирмы Intel.

4.2. Компоновка и загрузка
Очевидно, что ни одно сколько-нибудь сложное программное
изделие не может быть реализовано в виде единственного модуля.
Модульность программ и данных является необходимым условием
структурного программирования при применении его как нисходящего,
так и восходящего вариантов. Преимущества модульного конструирования
могут, однако, в полной мере проявиться только, если обеспечивается
раздельная компиляция модулей. Но если результаты раздельной
компиляции представляют собой также отдельные объектные модули, то
должен быть этап в подготовке программы, который бы компоновал из
объектных модулей единую программу, готовую к выполнению, –
загрузочный модуль. Кроме того, если компилятор обрабатывает каждый
модуль отдельно, он не имеет возможности полностью выполнить
функцию

именования,



в

объектном
125

модуле

остаются

непреобразованными обращения к процедурам и данным, определенным в
других модулях. Обеспечение обращений к внешним по отношению к
модулю именам называется связыванием. Операции компоновки и
связывания выполняются специальными программами-компоновщиками
(синоним – редактор связей, linker) и связывающими загрузчиками.
В соответствии с декларированной нами в предыдущей главе
"свободой выбора" операцию связывания можно выполнять на разных
этапах подготовки программы:
• статическое

связывание

этапа

подготовки



выполняется

компоновщиком, вызываемые модули включаются в загрузочный
модуль программы;
• статическое

связывание

этапа

загрузки



выполняется

связывающим загрузчиком, вызываемые модули подключаются к
программе уже в оперативной памяти;
• динамическое связывание этапа загрузки – отличается от
предыдущего

варианта

тем,

что

обеспечивает

совместное

использование одних и тех же копий модулей в памяти разными
программами;
• динамическое связывание этапа выполнения – загрузка модулей и
установка связей происходит при выполнении программы и
управляет ими сама программа.
Статическое связывание обеспечивается соглашениями о формате
объектных модулей. В любых вариантах эти форматы предусматривают
наличие в объектном модуле двух таблиц – таблицы входов и таблицы
внешних ссылок. В первой перечисляются имена и относительные адреса в
модуле тех процедур и элементов данных, к которым разрешены
обращения из других модулей. Во второй – имена внешних точек, к
которым имеются обращения из данного модуля, с каждым таким именем
связан также список адресов внутри модуля, которые содержат обращения
126

к этому внешнему имени. При статическом связывании создается таблица
входных точек – глобальная для всей программы. По мере компоновки
программы из входящих в нее модулей их входные точки заносятся в эту
таблицу, а их локальные адреса в модуле заменяются адресами в общем
адресном пространстве программы. Затем в каждом модуле находятся (по
таблице внешних ссылок) обращения к внешним модулям и имена в этих
обращениях заменяются на адреса, получаемые из глобальной таблицы
входных точек.
При

статическом

связывании

может

происходить

также

и

формирование оверлейной структуры адресного пространства программы,
рассмотренное в предыдущей главе.
Вспомним, что на этапе загрузки может происходить также
трансляция виртуальных адресов программы в физические адреса. В этом
случае загрузочный модуль должен содержать таблицу перемещений –
список тех адресов в программе, которые должны быть модифицированы
при загрузке базовым адресом программы. Эти операции выполняются
перемещающим загрузчиком, пример такого загрузчика – загрузчик EXEфайлов в MS DOS.
Статическое связывание и загрузка выполняются системными
утилитами, не входящими в ядро ОС, они не составляют основной предмет
нашего рассмотрения. Алгоритмы функционирования редакторов связей и
перемещающих загрузчиков подробно рассматриваются, например, в [3,
13], здесь же мы остановимся на проблемах динамического связывания,
выполняемого ОС.
Современные ОС позволяют сочетать статическую компоновку с
динамической. Хотя в литературе, посвященной этим ОС, возможность
динамического связывания описывается как принципиально новая, на
самом деле она была впервые реализована еще в 1965 году в ОС MULTICS
[30], и с тех пор ее механизмы не претерпели значительных изменений. В
127

современных ОС модули, подключаемые к программам динамически,
носят название библиотек динамической компоновки (dynamic link library),
соответственно, файлы, содержащие образы таких модулей имеют
расширения DLL.
Выполнение динамической компоновки иллюстрируется рисунком
4.1. Структуры данных динамической компоновки в принципе сходны со
структурами

для

компоновки

статической.

Для

модуля

основной

программы компоновщик создает таблицу, функционально идентичную
таблице внешних ссылок. В этой таблице указывается имя файла для
каждого динамически подключаемого модуля и имена тех процедур в
модуле, к которым имеются обращения в программе. Вызовы процедур в
теле программы содержат обращения к соответствующим элементам этой
таблицы. С другой стороны, при компоновке модуля, предназначенного
для динамического подключения, для него создается таблица входов. При
компоновке загрузчик (на этапе загрузки) или ядро ОС (на этапе
выполнения) выполняет установку связей: находит в памяти или загружает
в память динамически подключаемый модуль и по его таблице входов
находит требуемую процедуру. Строка таблицы внешних ссылок
модифицируется – теперь она содержит переход по адресу найденной
процедуры. Последующие обращения кэтой же входной точке уже не
вызывают каких-либо дополнительных действий.

128

а) модули до связывания

б) модули после связывания
Рисунок 4.1 Установка межмодульных связей при динамической компоновке

При совместном использовании процедур, входящих в состав
модулей DLL, несколькими процессами все процессы используют один и
тот же экземпляр кода DLL, но для каждого использования создается
отдельный стек, таким образом, для каждого использования имеется свой
набор экземпляров локальных переменных процедуры.
Динамическая компоновка при загрузке совершенно прозрачна для
программы. При подготовке программных и DLL-файлов требуются
специальные директивы для компоновщика, но в самом тексте программы
обращения

к

динамически

подключаемым

процедурам

ничем

не

отличаются от обращений к процедурам, подключенным статически. В
современных

ОС динамически

подключаемые библиотеки

широко

используются для системного программного обеспечения. API ОС,
средства работы с терминалом, графические средства и т.п. представляют
собой библиотеки динамической компоновки, совместно используемые
всеми программами. При загрузке любой программы эти модули, как
правило, уже находятся в памяти.
Динамическая компоновка несколько ухудшает

быстродействие

программы: во-первых, расходуется время на установление связей при
129

выполнении, во-вторых, при уже установленных связях обращение
производится через посредство таблицы внешних ссылок. Преимущества
же

динамической

компоновки

экономия оперативной памяти

бесспорны:
за

во-первых,

достигается

счет совместного использования

модулей, во-вторых, достигается экономия внешней памяти за счет
уменьшения

объема

загрузочных

модулей,

в-третьих,

создается

возможность модификации и полной замены модулей динамической
компоновки без изменения двоичного кода главной программы.
Динамическая компоновка на этапе выполнения дает возможность
программе самой управлять порядком загрузки модулей и установки
связей. Программа может, например, менять загруженный в память набор
модулей в разных фазах своего выполнения или использовать разные
модули в зависимости от конфигурации вычислительной системы или
условий выполнения и т.п. Для обеспечения таких возможностей ОС
должна предоставлять в распоряжение программиста соответствующий
набор системных вызовов. Этот набор может быть следующим.
Системный вызов:
mod_handle = loadModule (mod_name);
загружает модуль из файла с указанным именем. Если указанного модуля
нет в памяти, ОС производит загрузку его из файла, если же модуль в
памяти уже есть, ОС просто увеличивает на 1 счетчик использований этого
модуля. Вызов должен возвращать манипулятор модуля, используемый
для его идентификации во всех последующих операциях с ним.
Возможна модификация этого вызова:
mod_handle = getModuleHandle (mod_name);
получить манипулятор модуля: если модуль уже есть в памяти, вызов
возвращает его манипулятор (и увеличивает счетчик использований), если
нет – возвращает признак ошибки. В последнем случае программа может

130

либо загрузить модуль вызовом loadModule, либо перейти на такую
свою ветвь, которая не предусматривает обращений к данному модулю.
Системный вызов:
freeModule (mod_handle);
выгружает модуль. ОС уменьшает на 1 счетчик использования модуля,
если этот счетчик обратился в 0, освобождает память.
Системный вызов:
vaddr

=

getProcAddress

(mod_handle,

proc_name,

proc_addr);
возвращает виртуальный адрес процедуры с заданным именем в уже
загруженном модуле. Все дальнейшие обращения к данной процедуре
программа производит по этому адресу.

4.3. Цикл жизни процесса
Программа, готовая к выполнению, превратится в процесс только
тогда, когда ОС создаст для нее блок контекста и запись в системной
таблице процессов. ОС существенно различаются по тому признаку,
насколько часто они создают новые процессы и сколько процессов могут
одновременно существовать в системе.
В однозадачных системах существует один процесс (или несколько
процессов, только один из которых – пользовательский), который
последовательно выполняет одну программу за другой.
В многозадачных ОС, осуществляющих пакетную обработку,
процессов, обслуживающих пользователей, несколько, но число их либо
фиксировано и устанавливается при загрузке, либо меняется оператором, и
такие изменения происходят весьма редко.
Различные подходы могут применяться в интерактивных системах.
131

Во-первых, в интерактивной системе может копироваться стратегия
многозадачной системы с пакетной обработкой: с каждым терминалом
связывается единственный процесс-сеанс (session). Таким образом,
предельное число процессов в системе ограничивается числом терминалов.
Пользователь каждого терминала работает как бы в однозадачной среде
(VM).
Во-вторых, для преодоления стесненности пользователя в сеансе
система может позволять в ходе сеанса порождать дополнительные,
фоновые (background) процессы. Такие процессы выполняют программы,
не требующие в ходе выполнения взаимодействия с оператором. Фоновые
процессы

работают

параллельно

с

процессом,

поддерживающим

интерактивную работу в сеансе.
Наконец, в-третьих, система может позволять порождать любые
процессы и в любом количестве. Для каждой новой выполняемой
программы
завершением

создается

новый

программы.

процесс,

Процессы

который
могут

уничтожается
выполняться

с
как

последовательно, так и параллельно, ограничением на количество
параллельно

выполняемых

процессов

является

объем

ресурсов

вычислительной системы и ОС, в частности, возможные ограничения на
предельный размер таблицы процессов. Подход без ограничений на число
процессов иногда называют философией "дешевых" процессов. В таких
системах "накладные расходы" на создание процессов минимальны и
наблюдается тенденция к предельному упрощению отдельных процессов:
каждый отдельный процесс реализует некоторую весьма элементарную
функцию, а сложные действия реализуются как комбинации тех или иных
элементарных действий.
Процессы в системе могут либо существовать как отдельные, не
связанные друг с другом единицы, либо образовывать какие-либо
структуры. Обычно модель независимого существования процессов
132

используется в тех ОС, которые накладывают ограничения на возможности
создания процессов (такая возможность доступна только самой ОС). В тех
ОС, которые разрешают процессам в свою очередь
процессы,

применяется

иерархическая

структура

порождать новые
связей

между

процессами, в которой между процессами существуют отношения
"родитель – потомок". Такая структура позволяет установить четкие
правила в отношении использования ресурсов и организации управления.
В такой структуре все действующие процессы могут в конечном счете
являться потомками (более или менее близкими) одного системного
процесса.
Передача ресурсов от предка к потомку позволяет родственным
процессам легко устанавливать связи друг с другом. Наиболее важными в
этом отношении ресурсами являются файлы, каналы и другие средства
взаимодействия (см. главу 9). В соответствии с принятыми в конкретной
ОС правилами потомок может получать либо все ресурсы родителя, либо
только те, для которых установлен признак "наследуемые" (inheritance),
этот признак может быть установлен либо при выделении ресурса, либо
для уже выделенного ресурса – специальным системным вызовом.
Если пользовательскому процессу дана возможность порождать
потомков, то в его распоряжении должны быть соответствующие
системные вызовы. Конкретные возможные вызовы мы рассмотрим ниже,
но сначала остановимся на общей семантике порождающих вызовов.
Процесс-потомок может быть запущен синхронно или асинхронно с
родителем. При синхронном запуске процесс-родитель блокируется до
завершения процесса-потомка. Естественно, что в этом случае родитель не
может влиять на выполнение потомка. При асинхронном запуске родитель
и

потомок

продолжают

взаимодействовать.

Если

выполняться
родителю

теперь

параллельно
необходимо

и

могут

дождаться

завершения потомка, он выдает системный вызов ожидания. Синхронный
133

запуск не является обязательной возможностью, так как тот же эффект
может

быть

обеспечен

парой

вызовов:

"асинхронный

запуск"



"ожидание". Для того, чтобы родитель мог воздействовать на потомка,
вызов

"запуск"

возвращает

ему

идентификатор

(манипулятор)

порожденного процесса. Этот идентификатор используется родителем при
последующих воздействиях на потомка. Поскольку потомок может в свою
очередь создавать новые процессы, то есть стать во главе целого
подсемейства (поддерева) процессов, интерпретация идентификатора
процесса в системных вызовах, связанных с воздействиями на него,
должна

быть

четко

определена

системными

соглашениями

или

дополнительным параметром таких вызовов. Возможные интерпретации
идентификатора следующие:
• только тот процесс, идентификатор которого задан;
• процесс, идентификатор которого задан и все его потомки;
• процесс, идентификатор которого задан, или любой из его
потомков.
В конкретных системах могут вводиться некоторые разумные
ограничения на число порождаемых процессов, связанные с предельными
размерами таблицы процессов и очереди к планировщику. Может
ограничиваться, например, число потомков у процесса или число
процессов, принадлежащих одному пользователю.
Процесс-потомок при запуске не знает идентификатора своего
родителя, но, как правило, может его получить при помощи системного
вызова.
Ниже мы приводим набор системных вызовов, обеспечивающих
порождение процессов и "родственные отношения" между ними.
Порождение нового процесса и выполнение в нем программы:
pid = load(filename);
134

для нового процесса создается новая запись в системной таблице
процессов и блок контекста. В блоке контекста формируется описание
адресного пространства процесса – например, таблица сегментов.
Выполняется формирование адресного пространства – образы некоторых
частей адресного пространства (сегментов) процесса (коды и
инициализированные статические данные) загружаются из файла, имя
которого является параметром вызова, выделяется память для
неинициализированных данных. Формирование всех сегментов не
обязательно происходит сразу же при создании процесса: во-первых, если
ОС придерживается "ленивой" тактики, то формирование памяти может
быть отложено до обращения к соответствующему сегменту; во-вторых, в
загрузочный модуль могут быть включены характеристики сегментов:
предзагружаемый (preload) или загружаемый по вызову (load-on-call).
Новому процессу должна быть выделена также и вторичная память – для
сохранения образов сегментов/страниц при свопинге. Часть вторичной
памяти для процесса уже существует: это образы неизменяемых сегментов
процесса в загрузочном модуле. Для более эффективного поиска таких
сегментов ОС может включать в блок контекста специальную таблицу,
содержащую адреса сегментов в загрузочном модуле. При выделении
вторичной памяти для изменяемых сегментов все ОС обычно следуют
"ленивой" тактике. Ресурсы процесса-родителя копируются в блок
контекста потомка. В вектор состояния нового процесса заносятся
значения, выбор которых в регистры процессора приведет к передаче
управления на стартовую точку программы. Новый процесс ставится в
очередь готовых к выполнению. Вызов load возвращает идентификатор
порожденного процесса.
Смена программы процесса:
exec (filename);
135

Завершается программа, выдавшая этот системный вызов, вместо нее
запускается другая программа. Вызов exec может быть реализован как
комбинация вызовов exit (завершить текущий процесс) и load (создать
новый процесс), но может и не порождать смену процессов, а только
обновлять адресное пространство (включая и блок контекста) текущего
процесса. В последнем случае сохраняются также и ресурсы процесса.
Идентификатор процесса не изменяется.
Расщепление процесса:
pid = fork();


порождается

новый

процесс



копия

процесса-родителя.

При

копировании таблицы сегментов родителя в блок контекста потомка
принимаются во внимание характеристики сегментов:
• уникальный – сегмент может принадлежать только одному
процессу, в таблицу потомка этот сегмент не копируется;
• разделяемый – элемент таблицы сегментов потомка совершенно
идентичен элементу таблицы родителя, включая базовый адрес в
реальной памяти;
• копируемый – для потомка выделяется новый сегмент в реальной
памяти, в него копируется содержимое соответствующего
сегмента родителя и элемент таблицы сегментов потомка
содержит базовый адрес нового сегмента.
Для копируемых сегментов часто применяется "ленивая" тактика
копирования при записи (copy-on-write). В этом случае выделение сегмента
и копирование данных откладывается, родитель и потомок используют
один и тот же сегмент в реальной памяти, но в их таблицах этот сегмент
помечается как недоступный для записи. Совместное использование
сегмента для чтения продолжается, пока один из процессов не попытается
писать в него. Попытка записи вызовет прерывание-ловушку по
нарушению доступа, обработчик этого прерывания обеспечит создание
136

копии сегмента, изменит его базовый адрес в таблице потомка и снимет с
сегмента защиту записи.
При выполнении вызова fork копируется также и счетчик команд
процесса-родителя. Выполнение потомка, таким образом, начнется с
возврата

из

системного

вызова

fork

и

возникает

проблема

самоидентификации процесса. Процесс, вернувшийся из системного
вызова fork, должен определиться, кто он – родитель или потомок?
Семантика вызова fork обусловливает его применение в таком контексте:
if ( ( childpid = fork() ) == 0 )
< процесс-потомок >;
else < процесс-родитель >;
т.е., вызов возвращает 0 процессу-потомку и идентификатор потомка –
процессу-родителю.
В ОС Unix вызов fork является единственным средством создания
нового процесса. Поэтому, как правило, ветвь, соответствующая процессупотомку, содержит вызов exec, меняющий программу, выполняемую
потомком. Эффект в этом случае будет тот же, что и при выполнении
вызова load. Применение именно такой схемы, видимо, объясняется
легкостью передачи ресурсов. Ниже мы покажем структуру отношений
системных и пользовательских процессов в Unix.
Ожидание завершения потомка:
exitcode = waitchild(pid);
процесс-родитель блокируется
идентификатор

которого

до

является

завершения процесса-потомка,
параметром

вызова

(или

всего

семейства, возглавляемого этим потомком, или любого из членов этого
семейства). Вызов может возвращать код завершения процесса-потомка
(задается вызовом exit) и идентификатор завершившегося потомка.

137

Разумеется,

применение

этого

вызова

имеет

смысл

только

при

асинхронном запуске потомка.
Выход из процесса:
exit(exitcode);
приводит к освобождению занятых процессом ресурсов, в том числе и
ресурса памяти. Ресурсы, запрошенные процессом динамически, требуют
явного освобождения процессом (например, процесс должен закрыть все
открытые им файлы), но если процесс "забыл" это сделать, это сделает за
него ОС при выполнении данного вызова. При выполнении exit также
могут выполняться процедуры, заданные вызовами exitlist (см. ниже).
Вызов exit не обязательно должен приводить к немедленному полному
уничтожению процесса. Может сохраняться соответствующая ему запись в
таблице процессов и часть блока контекста, но процесс помечается как
завершенный. Неполное удаление процесса объясняется тем, что после
процесса остается еще некоторая информация, которая может быть
востребована, статистические данные о его выполнении, код завершения
(параметр вызова exit), который будет прочитан вызовом waitchilde в
родителе и т.п. Полное удаление процесса произойдет после того, как вся
остаточная информация будет обработана.
Формирование списка выхода:
exitlist(procaddr);
при помощи этого вызова процесс может установить процедуру (адрес
такой процедуры – параметр вызова), которая должна быть выполнена при
его завершении. Процедуры выхода обычно используются для сохранения
параметров программы и аккуратного закрытия каких-либо важных
ресурсов при аварийном завершении программы. Процесс может выдать
несколько вызовов exitlist, назначив несколько процедур выхода,
которые будут выполняться в неопределенном порядке. Процедура выхода
может также иметь параметр, через который ей будет передаваться
138

причина завершения: нормальное / по сигналу / по программной ошибке /
по аппаратной ошибке.
Принудительное завершение:
kill(pid);
завершает

процесс-потомок

или

все

семейство

процессов,

им

возглавляемое. Выполнение этого вызова заключается в выдаче сигнала
kill (механизм сигналов описывается в главе 9), по умолчанию
обработка этого сигнала вызывает выполнение exit с установкой
специального кода завершения.
Изменить приоритет:
setPriority ( pid, priority );
изменяет приоритет потомка или всего его семейства. Приоритет может
задаваться как абсолютный, так и в виде приращения (положительного
или

отрицательного)

к

текущему

приоритету.

Как

правило,

пользовательские процессы не могут изменять свой приоритет в сторону
увеличения.
Получение идентификаторов:
pid = getpid(mode);
вызов возвращает процессу его собственный идентификатор и/или
идентификатор процесса-родителя.
На рисунке 4.2 приведена в качестве примера схема наследования
процессов в ОС Unix. Корнем дерева процессов является процесс init,
создаваемый при загрузке ОС. Процесс init порождает для каждой линии
связи (терминала) при помощи пары системных вызовов fork–exec свой
процесс getty и переходит в ожидание. Каждый процесс getty ожидает
ввода со своего терминала. При вводе процесс getty сменяется
(системный вызов exec) процессом logon, выполняющим проверку
пароля. При правильном вводе пароля процесс logon сменяется (exec)
139

процессом shell. Командный интерпретатор shell (подробнее мы
рассмотрим его в главе 11) является корнем поддерева для всех процессов
пользователя, выполняющихся в данном сеансе. При завершении сеанса
shell завершается (exit), при этом "пробуждается" init и порождает
для этого терминала новый процесс getty.

Рисунок 4.2 Процессы в ОС Unix

Хотя на рисунке 4.2. между процессами init и shell находятся
еще два процесса, shell может считаться прямым потомком init, так
как находящиеся между ними процессы завершаются при запуске потомка.
В других ОС может выстраиваться и более сложная иерархия
системных процессов. Так, например, в OS/390 между ядром ОС и задачей
(task – синоним процесса в терминах IBM) пользователя находятся еще
задачи:
• управления разделом;
• системного дампа;
• управления командой START;

140

• инициатор (для пакетных задач) или LOGON (для интерактивных
задач).
Эти задачи существуют параллельно и выполняют управление
ресурсами на разных уровнях.
При

завершении

родительского

процесса

не

обязательно

завершаются все его потомки. Некоторые процессы могут продолжать
выполнение даже после завершения их непосредственного предка.
Предком таких осиротевших процессов обычно становится процесс,
находящийся в корне всего дерева наследования. Такие процессы обычно
применяются для выполнения каких-либо фоновых задач, например, для
ожидания и обработки каких-то внешних событий, и за ними закрепилось
название демоны (daemon).

4.4. Нити
Философия дешевых процессов подразумевает, что процесс может
быть создан легко и быстро. С одной стороны, это позволяет в
максимальной степени обеспечивать распараллеливание работ по решению
нескольких задач или внутри одной задачи. Но, с другой стороны, если ОС
выполняет большой объем работ по управлению ресурсами, то создание
нового процесса и выделение ему ресурсов не может обойтись без
значительных "накладных расходов". Преодоление этого противоречия
было найдено в концепции "нитей" (thread, часто переводится также как
"поток"),

реализованной

в

большинстве

современных

ОС

и

зафиксированной в стандартах POSIX и DCE. Нитью называется отдельная
ветвь выполнения процесса. Процесс может состоять из одной или
нескольких нитей, которые совместно используют ресурсы процесса, но
являются самостоятельными объектами при планировании процессорного
141

времени. Таким образом, нити одного процесса могут выполняться
параллельно. Концепция не оговаривает, какие именно ресурсы являются
общими, а какие – локальными для нити. В большинстве современных ОС
помимо ресурса процессорного времени и вектора состояния процесса,
нить имеет собственный:
• вектор состояния,
• приоритет,
• стек (в стеке же размещаются и локальные переменные).
Прочие

же

ресурсы:

общие

переменные,

файлы,

средства

взаимодействия и т. д. – являются общими для нити и породившего ее
процесса.
Любой процесс состоит из одной или нескольких нитей. Первая нить
– основная – порождается системой при запуске процесса. Основная нить
может порождать дочерние нити вызовом типа:
tid = createThread(thr_addr, stack).
Параметрами вызова являются: thr_addr – адрес нити; stack –
адрес стека, выделяемого для нити (стек может также назначаться
системой по умолчанию). Вызов возвращает идентификатор нити.
В программе (на языке C, например) нить выглядит как обычная
функция. Приведенный выше вызов передает управление на эту функцию,
и далее выполнение функции происходит параллельно с выполнением
основной программы. Поскольку при порождении каждой новой нити
выделяется отдельный стек, одна и та же функция может выполняться в
составе двух и более параллельных нитей. При порождении новая нить
наследует приоритет породившей ее, но далее этот приоритет может быть
изменен. Отношения между основной и дочерней нитями похожи на
отношения между процессами – родителем и потомком, рассмотренные в
предыдущем разделе.
142

Для управления выполнением нитей в рамках одного процесса
обычно в составе API ОС имеются системные вызовы, позволяющие
приостановить выполнение нити и вновь разрешить ее выполнение:
suspendThread(tid);
resumeThread(tid).
Отметим две особенности, связанные с возможностью порождения
нитей, которые создают некоторые дополнительные трудности для
пользователей и для ОС. Во-первых, трудность для пользователей –
поскольку нити разделяют общие ресурсы процесса, на программиста
ложится задача обеспечить корректность совместного использования
ресурсов (см. главы 8 и 9). Во-вторых, трудность для ОС – усложняется
задача

планировщика

процессорного

времени:

теперь

единицей

планирования становится не процесс, а нить и планировщик должен
обеспечивать справедливость распределения ЦП как между процессами,
так и между нитями в каждом процессе.

КОНТРОЛЬНЫЕ ВОПРОСЫ
1.

Каким образом при различных внутренних структурах и даже

механизмах обращения к ОС может быть обеспечен одинаковый API для
разных ОС?
2.

Какие стадии оптимизации может проходить программа?

Какие стадии оптимизации могут быть одинаковыми для программ,
написанных на языках C, Pascal, Cobol, Fortran и на языке Ассемблера?
Почему для современных процессорных архитектур оптимизация является
обязательной?
3.

В чем преимущества динамического связывания по сравнению

со статическим?
143

4.

Почему во многих современных ОС значительная часть

системы выполняется в виде библиотек динамической компоновки?
5.

Являются ли "родственные отношения" между процессами

обязательными? Являются ли они полезными?
6.

Сравните стратегии систем, в которых порождение процессов

выполняется вызовом fork и вызовом load.
7.

Для чего могут быть полезны списки выхода? Приведите

примеры задач, для которых было бы целесообразно иметь список выхода,
состоящий из более чем одной процедуры.
8.

Дайте

определение

нити.

Какие

задач,

решение

ресурсы

являются

собственными для нити?
9.

Приведите

примеры

которых

требует

применения нитей?
10.

В некоторых клонах ОС Unix нет специального механизма

нитей, но нити реализованы как процессы, наследующие адресное
пространство родителя. Чем вы объясните такое решение?

144

Глава 5. Монопольно используемые ресурсы
5.1. Свойства ресурсов и их представление
Процессорное время и оперативная память являются ключевыми
ресурсами любой ОС, без них не может выполняться ни один процесс.
Ресурсы, которые мы рассматриваем в этой главе, являются монопольно
используемыми: неперераспределяемыми и неразделяемыми. Свойство
неперераспределяемости означает, что ресурс не может быть отобран у
процесса во время его использования. Представьте себе, что процесс
выводит платежную ведомость на принтер. Если мы в середине печати
отберем у процесса ресурс-принтер и отдадим его другому процессу, то
когда первый процесс вновь обретет этот ресурс, ему придется начать
печать сначала. Как мы увидим дальше, неотбираемых ресурсов в системе
быть не должно, поэтому уточним понятие неперераспределяемости:
ресурс не может быть отобран без фатальных для процесса последствий.
На том же примере печати поясним понятие неразделяемости: два
процесса не могут выводить данные на один и тот же принтер
одновременно. Ресурсы процессорного времени и памяти, как мы увидели
выше,

свойствами

неперераспределяемости

и

неразделяемости

не

обладают.
К группе монопольных ресурсов относятся многие устройства вводавывода: принтеры, магнитные ленты, каналы связи и т.п., файлы (они
могут быть разделяемыми, но со значительными оговорками). Не забудем
145

также вторичные ресурсы, порождаемые самой ОС, – системные
структуры данных и коды. Так, например, при создании нового процесса
необходимо занести новую запись в таблицу процессов. Эта запись также
является ресурсом, причем неперераспределяемым и неразделяемым.
Ресурсы, которые мы рассматриваем, являются также повторно
используемыми. Это означает, что ресурсы после их использования
процессами не пропадают и не убывают, а могут быть использованы
другим процессом. Альтернативу им составляют потребляемые ресурсы,
которыми чаще всего могут быть входные данные и сообщения,
поступающие в процесс извне.
Наши

ресурсы

ограниченности.

обладают

Первое

также

означает,

свойствами

что

дискретности

ресурсы

и

распределяются

некоторыми неделимыми единицами (не может быть полтора принтера).
Второе



то,

что

число

единиц

ресурса

всегда

небесконечно.

(Процессорное время бесконечно: его достаточно для выполнения любого
процесса, и оно может дробиться планировщиком. Реальная память всегда
конечна,

виртуальная

тоже

конечна,

ограничена

разрядностью

виртуального адреса, а непрерывность или дискретность ее зависит от
принятой модели).
Мы

будем

называть

классом

ресурса

пул

идентичных

неименованных единиц ресурса. Неименованными мы считаем их в том
смысле, что процесс при запросе ресурса не указывает, какую именно
единицу из пула он хочет получить, все единицы ресурса одинаковы. Все
ресурсы одного класса управляются одним менеджером.
Из определений ОС (как с точки зрения разработчика, так и с точки
зрения пользователя), которые мы дали в первой главе, однозначно
следует, что процесс ни в коем случае не может самостоятельно завладеть
ресурсом, а только через посредство ОС. Для предоставления процессам

146

такой возможности в составе API ОС должны быть системные вызовы
типа:
resourceHandle = getResource
(class, number [,action] );
releaseResource(resourceHandle).
Первый вызов выделяет процессу number ресурсов из класса class
и возвращает манипулятор (handle) выделенного ресурса, который при
всех

дальнейших

идентификации
дескриптор

операциях

ресурса.

ресурса.

В

процесса

с

ресурсом

служит

Манипулятор

каким-то

образом

защищенных

системах

такой

для

адресует

дескриптор

располагается в недоступном для процесса адресном пространстве.
Манипулятор обычно является номером в системной таблице или списке
дескрипторов, и по нему ядро (но не процесс) выбирает требуемый
дескриптор ресурса.
Второй вызов открепляет от процесса ранее выделенный ему ресурс.
Возможно, форма выделения/освобождения ресурса напомнила вам
знакомые операции открытия/закрытия файла – и недаром. Поскольку
файлы также являются ресурсами, операции open/close – частные
случаи

операций getResource/releaseResource. Как правило, в

реальных API ОС нет общих операций выделения/освобождения ресурсов,
но для каждого ресурса имеется своя пара операций, отличающаяся от
других названием и, возможно, составом параметров.
Третий, необязательный параметр операции getResource задает
действия ОС в ситуации, когда выделить ресурс невозможно (не все ОС
предоставляют процессам возможности такого выбора). Во-первых (и это
действие обычно выполняется по умолчанию), ОС может заблокировать
процесс, выдавший запрос, до освобождения требуемого ресурса. Вовторых, ОС может не блокировать процесс, а вернуть ему отказ – сразу или
после некоторой временной выдержки (timeout), в этом случае "умный"
147

процесс может пока заняться другой работой, которую он может
выполнить и без этого ресурса, а позже повторить запрос. Как ОС должна
реагировать на запрос, который вообще не может быть выполнен
(требуемого объема ресурсов просто нет в системе)? По нашему
убеждению, такой запрос должен приводить к аварийному завершению
выдавшего

его

процесса.

Но

если

ОС

допускает

динамическую

реконфигурацию ресурсов, то запрос может быть поставлен в очередь в
ожидании момента, когда ресурс будет введен в систему. Такие
нереализуемые запросы могут составлять серьезную неприятность в работе
ОС, так как ресурс может никогда и не быть введен. Для избежания таких
запросов целесообразно иметь в составе API вызов, возвращающий общее
число

ресурсов

данного

класса.

Наконец,

при

невозможности

удовлетворить запрос ОС может потребовать от процесса освободить уже
имеющиеся в его распоряжении ресурсы. Если такая возможность имеется,
то она может быть очень полезна для развязки тупиков (см. ниже).
Для каждого класса ресурсов ОС должна поддерживать дескриптор
класса, в который должны входить:
• идентификатор класса;
• общее число единиц в классе;
• число свободных единиц;
• таблица единиц ресурса;
• список процессов, ожидающих ресурс этого класса;
• точка входа в менеджер класса;
• и т.д.
Для каждой единицы ресурса имеется запись в таблице единиц,
содержащая, как минимум, индикатор занятости ресурса и идентификатор
процесса, которому ресурс распределен (если он не свободен).

148

Манипулятор ресурса каким-то образом адресует дескриптор
ресурса. В защищенных системах сам дескриптор ресурса располагается в
адресном

пространстве,

недоступном

для

процесса.

Манипулятор

представляет собой номер дескриптора в системной таблице или в
системном списке дескрипторов ресурсов данного типа. При выполнении
системного вызова, параметром которого является манипулятор ресурса,
происходит переключение в контекст ядра, для модуля ОС, выполняющего
системный вызов, таблица дескрипторов становится непосредственно
доступной, и этот модуль выполняет действия над дескриптором, номер
которого он получил в качестве параметра.
Информация о ресурсах, выделенных процессу, хранится также в
блоке контекста процесса.

5.2. Обедающие философы
Уже

классической

стала

неформальная

постановка

задачи

распределения ресурсов, носящая название "проблемы обедающих
философов" и показанная на рисунке 5.1.

149

Рисунок 5.1 Обедающие философы. Тупик

Пять философов сидят за круглым столом, в центре которого стоит
блюдо с рисом. Между каждой парой философов лежит палочка для еды,
палочек, следовательно, тоже пять. Для того, чтобы начать есть, философ
должен взять две палочки – слева и справа от себя. Таким образом, если
один из философов ест, его соседи справа и слева лишены такой
возможности, так как им недостает палочек. Каждый философ "работает"
по зацикленному алгоритму: сначала он некоторое время думает, затем
берет палочки и ест, затем опять думает и т.д. Временные интервалы
мышления и еды случайны, действия философов, следовательно, не
синхронизированы. Ничего не говорится в условии о том, каким образом
философ берет палочки – наша задача как раз и состоит в том, чтобы
обеспечить такую стратегию выделения палочек, которая бы исключала
тупики и бесконечное откладывание.
Если мы установим, что каждый философ должен взять одну палочку
и не выпускать ее из рук до тех пор, пока не возьмет вторую палочку, то
150

мы можем получить ситуацию, показанную на рисунке 5.1. (Стрелка от
философа к палочке означает, что философ хочет взять эту палочку,
стрелка в обратном направлении – что эту палочку этот философ уже
взял.) Каждый из философов взял палочку справа от себя, но не может
взять палочку слева. Ни один из философов не может ни есть, ни думать.
Эта ситуация и называется тупиком (deadlock).
Если же мы установим, что философ должен взять обе палочки сразу,
то может возникнуть ситуация, показанная на рисунке 5.2. Философ Чжуан
хочет взять палочки, но обнаруживает, что его правая палочка занята
философом Мо. Чжуан ожидает. Тем временем философ Мэн берет свои
палочки и начинает есть. Мо есть заканчивает, но Чжуан не может начать
есть, так как теперь занята его левая палочка. Если Мо и Мэн едят
попеременно, то Чжуан попадает в положение, которое называется
голоданием (starvation) или бесконечным откладыванием.

Рисунок 5.2 Обедающие философы. Бесконечное откладывание

151

Переходя от философов к вычислительным системам, мы можем
проиллюстрировать тупик таким примером. Процесс A использует
магнитную ленту, но для завершения ему нужен еще и принтер. В это
время процесс B удерживает за собой принтер, но ему нужна еще
магнитная лента. Процессы A и B блокируют друг друга, то есть находятся
в тупике. В системах с множественными ресурсами и с высоким уровнем
мультипрограммирования тупиковые ситуации могут быть и не столь
очевидными. Тупики могут быть локальными и глобальными. Так, если в
приведенном выше примере уровень мультипрограммирования выше 2, то
процессы A и B находятся в локальном тупике, другие процессы, которым
не требуются ресурсы, занятые процессами A и B, могут выполняться.
Философы же на рисунке 5.1 находятся в глобальном тупике.
Бесконечное

откладывание

ситуация



даже

более

общая,

свойственная управлению любыми ресурсами, а не только монопольными.
Так,

при

планировании

процессорного

времени

по

статическим

приоритетам низкоприоритетный процесс может откладываться до
бесконечности, если в систему постоянно поступают процессы с более
высокими приоритетами.
Тупик

представляет

собой

ситуацию

более

опасную,

чем

бесконечное откладывание: процессы, попавшие в тупик, удерживают при
этом системные ресурсы. Даже если тупик не глобальный, система
продолжает работать с уменьшенным объемом ресурсов, следовательно, с
пониженной производительностью. Бесконечное же откладывание одного
или нескольких процессов может и не повлиять на среднюю пропускную
способность

системы,

но,

конечно

же,

влияет

на

показатели

справедливости обслуживания.

5.3. Тупики: предупреждение, обнаружение, развязка
152

Борьба с тупиками включает в себя три задачи:
• предупреждение тупиков – какую стратегию распределения
ресурсов выбрать, чтобы тупики не возникали вообще?
• обнаружение тупиков – если не удалось применить стратегию,
предупреждающую тупики, то как обнаружить возникший тупик?
• развязка тупиков – если тупик обнаружен, то как от него
избавиться?
Возможные стратегии распределения ресурсов располагаются между
двумя полюсами – от самых либеральных до самых консервативных. Чем
либеральнее стратегия, тем "охотнее" ОС удовлетворяет запросы на
ресурсы.

Но

расплачиваться

за

слишком

либеральную

стратегию

приходится

возможностью возникновения тупика. Консервативные

стратегии делают тупики невозможными в принципе, задачи обнаружения
и развязки при применении таких стратегий не ставятся, но плата за это –
частые отказы в выделении ресурсов, следовательно, снижение уровня
мультипрограммирования, а следовательно, – и снижение пропускной
способности. Ниже мы будем рассматривать стратегии предотвращения,
двигаясь от консервативного полюса к либеральному в таком порядке:
• последовательное выделение;
• залповое выделение;
• иерархическое выделение;
• выделение по предварительным заявкам.
Последовательное выделение
Любыми ресурсами может одновременно пользоваться только один
процесс. Если процесс A из предыдущего примера получил ресурспринтер, то процессу B будет отказано даже в выделении ресурса-ленты.
Очевидно, что такая стратегия делает тупики совершенно невозможными.
153

Очевидно также, что некоторые процессы будут при этом простаивать
совершенно неоправданно. Так, например, будет приостановлен некий
процесс C, которому принтер и не нужен, а нужна только лента. Поскольку
в число распределяемых ресурсов входят устройства ввода-вывода, а
работают они медленно (например, печать на принтере), простой может
затянуться.

Эта

стратегия

неоправданно

снижает

уровень

мультипрограммирования и неэффективно использует ресурсы (они тоже
простаивают) и может применяться только в таких ОС, в которых и
расчетный уровень мультипрограммирования невысок.

Залповое выделение
Процесс должен запрашивать / освобождать все используемые им
ресурсы сразу. Эта стратегия позволяет параллельно выполняться
процессам, использующим непересекающиеся подмножества ресурсов.
(Процесс C работает с лентой, процесс D – с принтером.) Тупики попрежнему невозможны, однако неоправданное удерживание ресурсов
продолжается. Так, если процессу в ходе выполнения нужны ресурсы R1 и
R2, причем ресурс R1 он использует все время своего выполнения t1, а
ресурс R2 требуется ему только на время t2mutEx);
}
269

В нашей реализации вы видите "скобки критической секции" как
элементарные операции. Они обеспечивают атомарность выполнения
семафоров и могут быть реализованы любым из описанных выше
корректных способов. Здесь мы ориентируемся на команду testAndSet с
использованием поля семафора mutEx в качестве замка, но это может
быть и любая другая корректная реализация (в многопроцессорных
версиях Unix, например, используется алгоритм Деккера). Вопрос: в чем
же мы выигрываем, если в csBegin все равно используется занятое
ожидание? Дело в том, что это занятое ожидание не может быть долгим.
Этими "скобками критической секции" защищается не сам ресурс, а только
связанный с ним семафор. Выполнение же семафорных операций
происходит быстро, следовательно, и потери на занятое ожидание будут
минимальными.
Если при выполнении P-операции оказывается, что значение
семафора нулевое, выдается системный вызов block, который блокирует
активный процесс – переводит его в список ожидающих, в тот самый
список, который связан с данным семафором. Важно, что процесс
прервется именно в контексте строки 3 своей P-операции и впоследствии
он возобновится в том же контексте. Поскольку заблокированный таким
образом процесс не успеет закончить критическую секцию, это должен
сделать за него системный вызов block, чтобы другие процессы получили
доступ к семафору.
Когда процесс выполняет V-операцию (освобождает ресурс),
проверяется очередь ожидающих процессов и разблокируется один из них.
В системном вызове unBlock можно реализовать любую дисциплину
обслуживания очереди, в том числе и такую, которая предупреждает
возможность бесконечного откладывания процессов в очереди. Если
270

разблокируется какой-либо процесс, то значение семафора так и остается
нулевым, если же очередь ожидающих пуста, то значение семафора
увеличивается на 1.
Укажем еще вот на какую особенность. После того, как процесс
разблокирован, он включается в список готовых. Может случиться так, что
этот процесс будет выбран планировщиком и активизирован даже раньше,
чем

процесс,

освободивший

ресурс,

закончит

свою

V-операцию.

Поскольку разблокированный процесс восстанавливается в контексте
своей P-операции, то получится, что два процесса одновременно
выполняют семафорные операции. В данном случае ничего страшного в
этом нет, потому что для разблокированного процесса уже снято взаимное
исключение (это было сделано при его блокировании), и этот процесс
после разблокирования уже не изменяет значения семафора. Запомним,
однако, этуособенность, которая в других примитивах взаимного
исключения может приобретать более серьезный характер.
Итак, общие свойства решения задачи взаимного исключения с
помощью семафоров таковы:
• дисциплина либеральна по тем же соображениям, что и
предыдущая;
• метод справедлив для любого числа процессов и процессоров;
• когда процесс блокируется, он не расходует процессорное время
на занятое ожидание;
• возможность бесконечного откладывания зависит от принятой
дисциплины обслуживания очереди ожидающих процессов, при
дисциплине FCFS бесконечное откладывание исключается;
• сами семафоры (но не защищаемые ими ресурсы) представляют
собой монопольно используемые ресурсы, следовательно, могут
порождать тупики; для борьбы с тупиками возможно применение
271

любого

из

известных

нам

методов,

предпочтителен



иерархический;
• как и все методы, рассмотренные выше, семафоры требуют от
программиста корректного применения "скобок", в роли которых
выступают P- и V-операции.
Для решения задачи взаимного исключения достаточно двоичных
семафоров. Мы, однако, описали тип поля value как целое число. В
приведенном нами выше определении Дейкстры речь тоже идет о
целочисленном, а не о двоичном значении. Семафор, который может
принимать неотрицательные значения, большие, чем 1, называется общим
семафором. Такой семафор может быть очень удобен, например, при
управлении не единичным ресурсом, а классом ресурсов. Начальное
значение поля value для такого семафора устанавливается равным числу
единиц ресурса в классе. Каждое выделение единицы ресурса процессу
сопровождается P-операцией, уменьшающей значение семафора. Семафор,
таким образом, играет роль счетчика свободных единиц ресурса. Когда
этот счетчик достигнет нулевого значения, процесс, выдавший следующий
запрос на ресурс, будет заблокирован в своей P-операции. Освобождение
ресурса сопровождается V-операцией, которая разблокирует процесс,
ожидающий ресурс или наращивает счетчик ресурсов.
Общие семафоры могут быть использованы и для простого решения
задачи синхронизации. В этом случае семафор связывается с каким-либо
событием и имеет начальное значение 0. (Событие может рассматриваться
как ресурс, и до наступления события этот ресурс недоступен). Процесс,
ожидающий события, выполняет P-операцию и блокируется до установки
семафора в 1. Процесс, сигнализирующий о событии, выполняет над
семафором V-операцию. Для графа синхронизации, например, показанного
на рисунке 8.1, мы свяжем с каждым действием графа одноименный
272

семафор. Тогда каждое действие (например, E) должно быть оформлено
следующим образом:

procE () {
/*ожидание событий B и D*/
P(B); P(D);
. . .
/* сигнализация о событии E для двух
ожидающих его действий (F и H) */
V(E); V(E);
}

Ниже мы рассмотрим применение семафоров для более сложного
варианта задачи синхронизации.

8.6. "Производители–потребители"
Пусть мы имеем два циклических процесса, которые мы назовем
"производитель" и "потребитель". Производитель в каждой итерации
своего цикла вырабатывает (производит) порцию информации, которую он
помещает в общий для обоих процессов буфер. Предположим для начала,
что емкость буфера неограничена. Потребитель в каждой итерации своего
цикла

выбирает

из

буфера

порцию

информации,

выработанную

производителем, и обрабатывает (потребляет) ее. Потребитель не должен
начинать обработку порции, пока ее производство не будет закончено.
Задача состоит в синхронизации действий производителя и потребителя
таким образом, чтобы не допустить потерь и искажений информации, вопервых, и голодания процессов, во-вторых.
273

Решение достигается при помощи единственного общего семафора,
играющего роль счетчика числа порций в буфере (здесь и далее мы
предполагаем структуру семафора, определенную в предыдущем разделе):

1

static semaphore *portCnt =

2

{ 0, 0, NULL };

3

static ... buffer ...;

4

/* процесс-производитель */

5

void producer ( void ) {

6

while (1) {

7

< производство порции >

8

< добавление порции в буфер >

9

V(portCnt);

10

}

11

}

12

/* процесс-потребитель */

12

void consumer ( void ) {

14

while (1) {

15

P(portCnt)

16

< выборка порции из буфера >

17

< обработка порции >

18
19

}
}

Исходное значение семафора portCnt – 0. Производитель каждую
итерацию

своего

цикла

заканчивает

V-операцией,

увеличивающей

значение счетчика. Потребитель каждую свою итерацию начинает Pоперацией. Если буфер пуст, то потребитель задержится в своей Pоперации до появления в буфере очередной порции. Таким образом, если
274

потребитель работает быстрее производителя, он будет время от времени
простаивать, если производитель работает быстрее – в буфере будут
накапливаться порции.
В реальных задачах такого рода (например, кольцевой буферизации,
рассмотренной нами в главе 6) буфер всегда имеет некоторую конечную
емкость. Если производитель работает быстрее, то при заполнении буфера
он должен приостанавливаться, ожидая освобождения места в буфере. Это
легко обеспечить, введя новый семафор freeCnt, выполняющий роль
счетчика свободных мест в буфере:

1

static semaphore *portCnt =
{ 0, 0, NULL },

2
3

*freeCnt = { BSIZE, 0, NULL },

4

static ... buffer [BSIZE];

5

/* процесс-производитель */

6

void producer ( void ) {

7

while (1) {

8

< производство порции >

9

P(freeCnt);

10

< добавление порции в буфер >

11

V(portCnt);

12

}

13

}

14

/* процесс-потребитель */

15

void consumer ( void ) {

16

while (1) {

17

P(portCnt)

18

< выборка порции из буфера >

19

V(freeCnt);
275

20
21
22

< обработка порции >
}
}

Попытаемся теперь обобщить решение для произвольного числа
производителей и потребителей. Но в таком обобщении мы сталкиваемся с
еще одной существенной особенностью. В приведенных выше решениях
мы допускали одновременное выполнение операций и . Очевидно, что при любой
организации буфера производитель и потребитель могут одновременно
работать только с разными порциями информации. Иначе обстоит дело с
многочисленными производителями и потребителями. Два производителя
могут попытаться одновременно добавить порцию в буфер и выбрать для
этого одно и то же место в буфере. Аналогично два потребителя могут
попытаться выбрать из буфера одну и ту же порцию. В следующем
примере мы для наглядности представляем буфер в виде кольцевой
очереди с элементами (порциями) одинакового размера:

1

typedef ... portion; /* порция информации */

2

static portion buffer [BSIZE];

3

static int wIndex = 0, rIndex = 0;

4

static semaphore *portCnt = { 0, 0, NULL },

5

*freeCnt = { BSIZE, 0, NULL },

6

*rAccess = { 1, 0, NULL },

7

*wAccess = { 1, 0, NULL };

8

/* имеется NP

9
10
11

аналогичных процессов-производителей */
void producer ( void ) {
portion work;
276

12

while (1) {

13

< производство порции в work >

14

P(wAccess);

15

P(freeCnt);

16

/* добавление порции в буфер */

17

memcpy(buffer+wIndex,&work,

18

sizeof(portion) );

19

if ( ++wIndex == BSIZE ) w_index = 0;

20

V(portCnt);

21

V(wAccess);

22

}

23

}

24

/* имеется NC

25
26

аналогичных процессов-потребителей */
void consumer ( void ) {

27

portion work;

28

while (1) {

29

P(rAccess);

30

P(portCnt)

31

/* выборка порции из буфера */

32

memcpy(&work, buffer+rIndex,

33

sizeof(portion) );

34

if ( ++rIndex == BSIZE ) rIndex = 0;

35

V(freeCnt);

36

V(rAaccess);

37

< обработка порции в work>

38
39

}
}

277

Мы оформляем обращения к буферу как критические секции,
защищая их семафорами rAccess и wAccess. Поскольку конфликтовать
(пытаться работать с одной и той же порцией в буфере) могут только
однотипные процессы, мы рассматриваем буфер как два ресурса: ресурс
для чтения и ресурс для записи, и каждый такой ресурс защищается своим
семафором. Таким образом, запрещается одновременный доступ к буферу
двух

производителей

или

двух

потребителей,

но

разрешается

одновременный доступ одного производителя и одного потребителя.

8.7. Конструкции критических секций в языках
программирования

Семафоры являются удобными и эффективными примитивами, при
помощи которых решаются задачи взаимного исключения и
синхронизации, поэтому они широко используются во многих ОС. API
ОС, обеспечивающий для пользователя работу с семафорами, мы
рассмотрим в следующей главе, а здесь остановимся на некоторых
возможных расширенных механизмах в системном программном
обеспечении (прежде всего – в системах программирования), которые
будут использовать семафоры в скрытой от пользователя форме.
Некоторые неудобства использования критических секций могут
быть

преодолены

введением

специальных

конструкций

в

язык

программирования. Например, если у нас в программе есть разделяемая
переменная x, то удобно защитить ее конструкцией типа:

shared int x;
. . .
section ( x ) {
278

< операторы, работающие с переменной x >
}

Конструкция section определяет критическую секцию. Вместо
специальных "скобок критической секции" используется заголовок
section и обычные операторные скобки. Последнее дает возможность
проверять

правильность

оформления

критической

секции

на

синтаксическом уровне – на этапе трансляции программы, а не ее
выполнения,



распространенной

и

предупреждает
ошибки

возможность

программистов



появления
непарных

самой
скобок.

Определение переменной x со специальным описателем shared
позволяет выявить (опять на этапе компиляции) все попытки доступа к ней
вне критической секции.
Реализация этой конструкции очевидна: переменная x защищается
скрытым семафором, над которым производится P-операция при входе в
секцию и V-операция – при выходе из нее.
Отчасти такая конструкция позволяет решить и проблему тупиков.
Если описать в программе иерархию разделяемых переменных, то можно
спокойно разрешить программисту делать вложенные критические секции
и проверять правильность вложения на этапе компиляции. Этот метод,
однако, не универсален: если в критической секции есть обращение к
процедуре, а в последней – другая критическая секция, то правильность
такого вложения компилятор проверить не сможет. Другой путь –
запретить вложенные секции, но разрешить в заголовке секции указывать
не одну разделяемую переменную, а целый их список. Этот вариант более
прост и надежен, но он использует дисциплину залпового выделения
ресурсов и, следовательно, более консервативен.
Критические секции как языковые конструкции обеспечивают
взаимное исключение, но не синхронизацию процессов. Инструментом для
279

синхронизации может быть оператор типа await, операндом которого
является логическое выражение. Этим оператором процесс блокируется до
тех пор, пока операнд не примет значение "истина". Поскольку ожидание
связано с внешним событием, в логическом выражении должна
участвовать хотя бы одна разделяемая переменная, следовательно,
применение await может быть разрешено только в критической секции.
В качестве примера приведем решение задачи "производители–
потребители" для процессов, использующих буфер, организованный в виде
стека. Такая организация буфера диктует нам необходимость запретить
одновременный доступ к нему любых двух процессов.

1

shared struct {

2

portion buffer [BSIZE];

3

int stPtr;

4

} stack = { {...}, 0 };

5

void producer ( void ) {

6

portion work;

7

while (1) {

8

< производство порции в work >

9

section ( stack ) {

10

await ( stack.stRtr < BSIZE );

11

memcpy ( stack.buffer +

12

stack.stPtr++,

13

&work, sizeof(portion) );
}

14
15

}

16

}

17

void consumer ( void ) {

18

portion work;
280

19

while (1) {

20

section ( stack ) {

21

await( stack.stPtr > 0 );

22

memcpy ( &work, stack.buffer +

23

--stack.stPtr,

24

sizeof(portion) );

25

}

26

< обработка порции в work>

27

}

28

}

При реализации возможности await мы должны решить проблему
исключения. В приведенном выше примере процесс-производитель,
заблокированный в строке 10, ждет уменьшения значения указателя стека
(если стек полон). Это значение может быть уменьшено процессомпотребителем в строке 23, но эта строка находится в критической секции
потребителя, а последний не может войти в свою критическую секцию, так
как производитель уже вошел в свою, – в строке 9. Одним из способов
разрешения этого противоречия является запрещение употребления await
где-либо, кроме самого начала критической секции, возможно, даже
включение await-условия в заголовок секции. Исключение в этом случае
начинает работать только после выхода из await. Естественно, что сама
проверка

условия

должна

выполняться

как

атомарная

операция

(защищаться скрытым семафором).
Можно разрешить await где угодно в критической секции, но на
время блокировки процесса в await снимать взаимное исключение.
Конечно, такой вариант дает программисту более гибкий инструмент, но
перекладывает на него ответственность за целостность, так как за время, на
281

которое

исключение

снимается,

разделяемые

данные

могут

быть

изменены.
Еще один вопрос, который мы должны решить при реализации
await: если процесс заблокировался, то в какие моменты следует
проверять условие разблокирования? Вопрос непраздный, так как для
вычисления условия, являющегося операндом await, необходимо
активизировать контекст заблокированного процесса. Если проделывать
это слишком часто, то накладные расходы на переключение неоправданно
возрастают. Для варианта, в котором мы жестко привязываем await к
началу критической секции, общая переменная, входящая в условие,
может быть изменена только другим процессом и ее новое значение станет
доступным при выходе этого другого процесса из его критической секции.
Выход другого процесса из критической секции – единственное событие,
по которому имеет смысл переключаться в контекст заблокированного
процесса и проверять условие в этом варианте. Если же мы разрешаем
употребление await где угодно в критической секции, то добавляется еще
один тип события – блокировка другого процесса в операторе await его
критической секции.

8.8. Мониторы
Монитор представляет собой набор информационных структур и
процедур, которые используются в режиме разделения. Некоторые из этих
процедур являются внешними и доступны процессам пользователей, их
имена представляют входные точки монитора. Пользователь не имеет
доступа к информационным структурам монитора и может воздействовать
на них, только обращаясь к входным точкам. Монитор, таким образом,
воплощает принцип инкапсуляции данных. В терминах объектно282

ориентированного программирования ресурс, обслуживаемый монитором,
представляет собой объект, а входные точки – методы работы с этим
объектом. Особенностью монитора, однако, является то, что в его состав
входят так называемые "процедуры с охраной", которые не могут
выполняться двумя процессами одновременно.
Не

являясь

средством

более

мощным

или

гибким,

чем

рассмотренные выше примитивы, мониторы, однако представляют
значительно более удобный инструмент для программиста, избавляя его от
необходимости формировать критические секции, обеспечивая более
высокий уровень интеграции данных и предупреждая возможные ошибки
во взаимном исключении.
Если

в

задаче

"производители–потребители"

процессы

программируются пользователем, то вид этих процессов может быть
таким:

1

#include

2

/* процесс-производитель

3
4

(может быть отдельным модулем) */
void producer ( void ) {

5

portion work;

6

while (1) {

7

< производство порции в work >

8

putPortion ( &work );

9

}

10

}

11

/* процесс-потребитель

12
13
14

(может быть отдельным модулем) */
void consumer ( void ) {
portion work;
283

15

while (1) {

16

getPortion ( &work );

17

< обработка порции в work>

18
19

}
}

Обратим внимание на то, что процессы, во-первых, никоим образом
не заботятся о разделении данных, во-вторых, не используют никакие
общие данные. Такая "беззаботная" работа процессов, однако, должна
быть поддержана монитором, входные точки которого описаны в файле
monitor.h, а определение его имеет такой вид:

1

/* монитор производителей-потребителей

2

(отдельный модуль) */

3

#define BSIZE ...

4

/* буфер */

5

static portion buffer [BSIZE];

6

/* индексы буфера для чтения и записи*/

7

static int rIndex = 0, wIndex = 0;

8

/* счетчик заполнения */

9

static int cnt = 0;

10

/* события НЕ_ПУСТ, НЕ_ПОЛОН */

11

static event nonEmpty, nonFull;

12

/* процедура занесения порции в буфер*/

13

void guard putPortion ( portion *x ) {

14
15

/* если буфер полон ожидать события НЕ_ПОЛОН */

16

if ( cnt == BSIZE ) wait (nonFull);

17

/* запись порции в буфер */
284

18

memcpy ( buffer + wIndex,

19

x, sizeof(portion) ) ;

20

/* модификация индекса записи */

21

if ( ++wIndex == BSIZE ) wIndex = 0;

22

cnt++; /* подсчет порций в буфере */

23

/* сигнализация о том,

24

что буфер НЕ_ПУСТ */

25

signal (nonEmpty);

26

}

27

void guard getPortion ( portion *x ) {

28

if ( cnt == 0 ) wait (nonEmpty);

29

memcpy ( x, buffer + rIndex,

30

sizeof(portion) ) ;

31

if ( ++rIndex == BSIZE ) rIndex = 0;

32

cnt++;

33

signal (nonFull);

34

}

В реализации монитора нам пришлось прибегнуть к некоторым
новым обозначениям. Во-первых, функции монитора даны с описателем
guard (охрана). Это означает, что они должны выполняться в режиме
взаимного исключения. В литературе часто употребляется образное
сравнение мониторов с комнатой, в которой может находиться только один
человек. Такая комната показана на рисунке 8.2. Если человек (процесс)
желает войти в комнату (охраняемую процедуру монитора), то он
становится во входную очередь к двери 1, в которой он ожидает
(блокируется) до тех пор, пока комната (монитор) не освободится. Дверь 1
(вход) отпирается только в том случае, если комната пуста, пропускает

285

только одного человека и запирается за ним. Дверь 2 (выход) не заперта,
когда она открывается, отпирается и дверь 1.

Рисунок 8.2 Простая модель монитора

Обратите внимание на то, что взаимное исключение обеспечивается для
всех охраняемых процедур, а не только для одноименных. В сущности,
такая процедура представляет собой ту же критическую секцию, и ее
охрана реализуется любым из методов защиты критической секции,
скорее всего, в роли "охранника" будет выступать скрытый семафор.
Другие наши нововведения связаны с блокировками внутри
охраняемой процедуры. Мы ввели тип данных, названный нами event.
Этот тип представляет некоторое событие. Примитив wait проверяет
наступление этого события и переводит процесс в ожидание, если событие
еще не произошло. Примитив signal сигнализирует о наступлении
события. Событие является потребляемым ресурсом: если два процесса
ожидают одного и того же события, то при наступлении события
разблокирован будет только один из процессов, другой будет вынужден
ждать повторного наступления такого же события.
Примитивы ожидания-сигнализации требуют принятия решений по
ряду проблем их реализации. Если один процесс выдает сигнал о
наступлении некоторого события, то в какой момент должен быть
разблокирован процесс, ожидающий этого события? Если немедленно, то
тогда

в нашей

"комнате"

окажется

два

процесса одновременно:

разблокированный процесс и процесс, выдавший сигнал, но еще не
покинувший монитор. Если позже, то за это время в монитор может войти
286

какой-то третий процесс. Если несколько процессов ожидают одного и
того же события, то какой (или какие) из них должен быть разблокирован?
Если в момент выдачи сигнала нет процессов, ожидающих этого события,
то должен ли сигнал сохраняться или его можно "потерять"?
Общий

подход

к

решению

этой

проблемы

иллюстрируется

расширением модели "одноместной комнаты", показанным на рисунке 8.3.
Для каждого события, которое может ожидаться в мониторе, мы вводим
свою очередь с соответствующими входными и выходными дверями для
нее. На рисунке эти очереди показаны внизу монитора. Мы вводим также
очередь, которую мы называем приоритетной. Процессы, находящиеся в
очередях, не считаются находящимися в мониторе. "Правила для
посетителей" комнаты-монитора следующие.
1. Новый процесс поступает во входную очередь. Новый процесс
может войти в монитор через дверь 1 только, если в мониторе нет
других процессов.
2. Если процесс выходит из монитора через дверь 2 (выход), то в
монитор входит процесс из двери 4 – из приоритетной очереди.
Если в приоритетной очереди нет ожидающих, входит процесс из
двери 1 (если есть ожидающие за ней).
3. Процесс, выполняющий операцию wait, выходит из монитора в
дверь, ведущую в соответствующую очередь (5 или 7).
4. Если процесс выполняет операцию signal, то проверяется
очередь, связанная с событием. Если эта очередь непуста, то
сигнализирующий процесс уходит в приоритетную очередь (дверь
3), а в монитор входит один процесс из очереди к событию (дверь
6 или 8). Если очередь пуста, сигнализирующий процесс остается
в мониторе.
5. Все очереди обслуживаются по дисциплине FCFS.
287

Рисунок 8.3 Расширенная модель монитора

Эти правила предполагают, что процесс будет разблокирован
немедленно (речь идет не о немедленной активизации процесса, а о его
разблокировании – перемещении в очередь готовых к выполнению).
Разблокированный процесс имеет преимущество перед процессом,
ожидающим во входной очереди.
В нашем примере производителей–потребителей мы употребляли
операцию signal в конце процедуры. Такое употребление характерно для
очень большого числа задач. Наши правила требуют перемещения
сигнализирующего процесса в приоритетную очередь. Однако, если
сигнализирующий процесс после выдачи сигнала больше не выполняет
никаких действий с разделяемыми данными, необходимости в таком
перемещении (и вообще в приоритетной очереди) нет. Жесткая привязка
сигнала к окончанию охраняемой процедуры снижает гибкость монитора,
но значительно упрощает диспетчеризацию процессов в мониторе.
Возможно решение, в котором операция signal разблокирует все
процессы, находящиеся в соответствующей очереди. Поскольку все
ожидавшие процессы не могут вместе войти в монитор, в нем остается
только один из них, успевший "подхватить" событие, а остальные
направляются

в

приоритетную

очередь.

Процесс,

который

разблокировался таким образом, уже не может, однако, быть уверенным в
288

том, что его разблокирование гарантирует наступление события (событие
могло быть перехвачено другим процессом). Поэтому в проверке условия
ожидания оператор if для такой реализации должен быть заменен
оператором while, например, строки 14 - 16 последнего примера должны
выглядеть так:

14
15
16

/* если буфер полон ожидать события НЕ_ПОЛОН */
while ( cnt == BSIZE ) wait (nonFull);

Поскольку охраняемые процедуры есть критические секции, для
поддержания высокого уровня мультипрограммирования нахождение в
них процессов должно быть кратковременным. Проблема времени
может возникнуть в том случае, когда в процедуре монитора имеется
обращение к другому монитору. По нашим правилам такое обращение
не выводит процесс из первого монитора. Однако во втором
(вложенном) мониторе процесс может быть заблокирован, тогда его
пребывание в первом мониторе недопустимо затянется. Возможно
несколько вариантов решения этой проблемы:
• переложить ответственность за возникновение такой ситуации на
программиста;
• запретить вложенные вызовы вообще;
• при вхождении во вложенный монитор автоматически снимать
исключение с внешнего монитора, когда процесс возвращается из
вложенного монитора, он попадает в приоритетную очередь
внешнего монитора;
• предоставить программисту выбор из перечисленных выше
возможностей.
289

8.9. "Читатели–писатели" и групповые мониторы
Еще одна классическая задача синхронизации называется задачей
"читателей–писателей" и формулируется следующим образом. Имеется
произвольное число процессов-писателей и процессов-читателей, которые
совместно используют какие-то данные (обычно имеется в виду файл). В
любой момент процесс-читатель может потребовать прочитать данные. В
любой момент процесс-писатель может потребовать прочитать или
записать данные. Чтение и запись данных – операции длительные, но
конечные. В то время, когда процесс записывает данные, никакие другие
читатели или писатели не должны иметь доступа к данным. Любое число
процессов

может читать

обеспечивать

целостность

данные

одновременно. Решение

данных

и

отсутствие

должно

бесконечного

откладывания процессов.
Существенные отличия этой задачи от задачи производителей–
потребителей состоят в следующем:
• процессы чтения и записи длительные;
• данные представляют собой повторно используемые ресурсы, а не
потребляемые, как в предыдущей задаче;
• требуются различные режимы доступа для чтения и записи.
Нам не удастся решить эту задачу при помощи мониторов,
описанных в предыдущем разделе, так как, если мы сделаем процедуры
read и write охраняемыми, то, во-первых, у нас получатся слишком
большие (длительные) критические секции, а во-вторых, мы исключим
параллельное выполнение читателей.
Решение может быть получено при помощи так называемого
группового монитора. В такой монитор входят как охраняемые, так и
290

неохраняемые процедуры. Для того, чтобы процесс получил возможность
доступа к неохраняемой процедуре, он должен быть членом группы, за
которой

право

такого

доступа

закреплено.

Группы

формируются

динамически. Для прикрепления к группе процесс должен обратиться к
охраняемой процедуре. Если в данный момент прикрепление к группе
возможно, эта процедура прикрепит процесс к группе, если нет –
заблокирует процесс до появления такой возможности. После окончания
доступа

процесс

должен

вызвать

также

охраняемую

процедуру

открепления от группы. Для задачи "читатели–писатели" предполагается
такой порядок доступа процессов к данным:


для читателей:
startRead ( proc );
read( proc, ... );
endRead ( proc );

• для писателей:
startWrite ( proc );
write ( proc, ... );
endWrite ( proc );
где proc – идентификатор процесса.
Структура самого монитора в общих чертах следующая:

1

/* счетчик читателей /*

2

int rdCnt = 0;

3

/* признак активности записи */

4

char wrFlag = 0;

5

/* списки - писателей и читателей */

6

process *wrCrowd=NULL, *rdrowd=NULL;

7

/* события:
291

МОЖНО_ЧИТАТЬ, МОЖНО_ПИСАТЬ */

8
9

event mayRead, mayWrite;

10

/* процедура регистрации читателя */

11

void guard startRead ( process *p ) {

12

rdCnt++;

/* подсчет читателей */

13

/* если идет запись ожидать МОЖНО_ЧИТАТЬ */

14
15

if ( wrFlag ) wait (mayRead);

16

/* дублирование сигнала
для другого читателя */

17
18

signal (mayRead);

19

/* включение в список читателей */

20

inCrowd ( rdCrowd, p );

21

}

22

/* процедура открепления читателя */

23

void guard endRead ( process *p ) {

24

/* исключение из списка читателей */

25

fromCrowd ( rdCrowd, p );

26

/* уменьшение числа читателей,

27

если читателей больше нет -

28

сигнализация МОЖНО_ПИСАТЬ */

29

if ( --rdCnt==0 ) signal(mayWrite);

30

}

31

/* процедура регистрации писателя */

32

void guard startWrite ( process *p ) {

33

/* если есть другие читатели

34

или писатели - ждать */

35

if ( wrFlag||rdCnt ) wait(mayWrite);

36

/* установка признака записи */
292

37

wrFlag = 1;

38

/* писатель включается в оба списка*/

39

inCrowd ( rdCrowd, p );

40

inCrowd ( wrCrowd, p );

41

}

42

/* процедура открепления писателя */

43

void guard endWrite ( process *p ) {

44

wrFlag=0; /* сброс признака записи */

45

/* исключение из списков */

46

fromCrowd ( rdCrowd, p );

47

fromCrowd ( wdCrowd, p );

48

/* если есть претенденты-читатели разрешение им */

49
50

if ( rdCnt ) signal (mayRead);

51

/* иначе - разрешение на запись */

52

else signal (mayWrite);

53

}

54

/* процедура чтения */

55

void read ( process *p,

56

< другие параметры > ) {

57

/* если процесс не зарегистрирован
читателем - отказ */

58
59

if (!checkCrowd(rdCrowd, p)) ;

60

else < чтение данных >;

61

}

62

/* процедура записи */

63

void write ( process *p,

64
65

< другие параметры > ) {
/* если процесс не зарегистрирован
293

66

писателем - отказ */

67

if (!checkCrowd(wrCrowd,p)) ;

68

else < запись данных >;

69

}

Прежде чем процесс получит доступ к данным, он должен
зарегистрироваться как читатель или как писатель. В нашем примере
переменные rdCrowd и wrCrowd (строка 6) являются указателями на
списки читателей и писателей соответственно, хотя можно интегрировать
процессы в группы и любым другим способом. Используемые (но не
определенные) нами функции inCrowd и fromCrowd обеспечивают
включение процесса в группу и исключение из группы, а функция
checkCrowd возвращает 1, если указанный процесс входит в группу
(иначе – 0). Процедура read выполняется только для процессов,
включенных в группу читателей (строки 59, 60), а write – только для
включенных в группу писателей (строки 67, 68). Переменная rdCnt (строка
2) – счетчик текущего числа читателей, переменная wrFlag (строка 4) –
счетчик писателей (или признак наличия писателя, так как писателей не
может быть более одного). События mayRead и mayWrite (строка 9)
являются разрешениями читать и писать соответственно.
Входная точка startRead (строка 11) выполняет регистрацию
читателя. Это охраняемая процедура. Она наращивает счетчик
читателей, но если в данный момент работает писатель, то
потенциальный читатель блокируется до наступления события mayRead
(строка 15). После того, как процесс будет разблокирован (или если он
не блокировался вообще), он выдает сигнал mayRead (строка 18),
который предназначается для другого читателя, возможно, также
ждущего разрешения на чтение (можно сказать, что процесс этим
294

восстанавливает потребленный им ресурс), и включается в список
читателей. После завершения доступа читатель обращается к входной
точке endRead (строка 23). Эта процедура исключает процесс из списка
читателей, уменьшает счетчик читателей и, если читателей больше не
осталось, сигнализирует разрешение на запись.
Писатель регистрируется/разрегистрируется через входные точки
startWrite/endWrite

(строки

32/43).

При

регистрации

потенциальный писатель может быть заблокирован, если в настоящий
момент зарегистрирован другой писатель или хотя бы один читатель
(строка 35). Сигнал mayWrite разблокирует писателя, и он включается в
обе группы (строки 39, 40), так как имеет право и читать, и писать. При
откреплении писатель исключается из групп. Если за время его работы
попытался

зарегистрироваться

хотя

бы

один

читатель,

выдается

разрешение на чтение (строка 50), в противном случае – разрешение на
запись для другого писателя, возможно, ждущего своей очереди (строка
52).

8.10. Примитивы синхронизации в языках
программирования
До сих пор мы рассматривали методы, которые обеспечивают и
взаимное исключение, и синхронизацию. Целый ряд прикладных задач,
однако, требует только синхронизации без взаимного исключения. Отказ
от последнего, если это не мешает решению задачи, всегда оправдан, так
как взаимное исключение усложняет решение и может снижать уровень
мультипрограммирования.
Один из возможных примитивов, обеспечивающих синхронизацию
без взаимного исключения, называется счетчиком событий. Счетчик
295

событий – тип данных, представляемый неуменьшающимся целым числом
с начальным значением 0. Его значение в любой момент времени – число
событий определенного типа, происшедших от некоторой точки начала
отсчета. Над этим типом данных возможны следующие операции:
• advance(E) – увеличение значения счетчика событий E на 1,
атомарная операция;
• eread(E) – возвращает текущее значение счетчика E, эта
операция не взаимоисключающая с advance, так что к моменту,
когда значение попадет в читающий его процесс, текущее
значение счетчика может быть уже изменено;
• await(E,value) – ждать – ожидание (блокировка процесса),
пока значение счетчика E не станет большим, чем value, или
равным ему.
Существенно, что из перечисленных операций только advance
является взаимоисключающей, остальные могут выполняться параллельно
друг с другом и с advance.
Вот как решается с помощью счетчиков событий задача для одного
производителя и одного потребителя:

1

/* тип данных - счетчик событий */

2

typedef unsigned int eventcounter

3

/* счетчики для чтения и записи */

4

static eventcounter inCnt = 0,

5

outCnt = 0;

6

/* буфер */

7

static portion buffer [BUFSIZE];

8

/* процесс-потребитель */

9

void consumer ( void ) {
296

10

int portNum;

11

/* рабочая область порции */

12

/* номер порции */

portion work;

13

/* цикл потребления */

14

for ( portNum = 1; ; portNum++ ) {
/* ожидание доступности порции

15

по номеру */

16
17

await (inCnt, portNum);

18

/* выборка из буфера */

19

memcpy (&work,

20

buffer + portNum % BSIZE,

21

sizeof(portion) );

22

/* продвижение счетчика записи */

23

advance (outCnt);

24

< обработка порции в work>

25

}

26

}

27

/* процесс-производитель */

28

void producer ( void ) {

29

int portNum;

/* номер порции */

30

/* рабочая область для порции */

31

portion work;

32

/* цикл производства */

33

for ( portNum = 1; ; portNum++ ) {

34

< производство порции в work >

35

/* ожидание доступности порции

36

по номеру */

37

await (outCnt, portNum - BSIZE);

38

/* запись в буфер */
297

39

memcpy (buffer + portNum % BSIZE,

40

&work, sizeof(portion) );

41

/* продвижение счетчика чтения */

42

advance (inCnt);

43
44

}
}

Как мы уже отмечали выше, производитель и потребитель работают
с разными секциями буфера и взаимное исключение для них не требуется.
Процессы – производитель и потребитель – могут перекрываться в любых
своих фазах, кроме операций advance (строки 23 и 42). Переменные
inCnt и outCnt являются счетчиками событий – производства порции и
потребления порции соответственно. Кроме того, каждый процесс хранит в
собственной локальной переменной portNum номер порции, с которой
ему предстоит работать (счет начинается с 1). Потребитель ждет, пока
счетчик производств не достигнет номера очередной его порции, затем
выбирает порцию из буфера и увеличивает счетчик потреблений.
Производитель работает симметрично. Обратите внимание на второй
параметр операции await в производителе (строка 37). Он задается таким,
чтобы обеспечить отсутствие ожидания при наличии хотя бы одной
свободной секции в буфере.
Другой механизм

синхронизации носит название секвенсоров

(sequencer). Буквальный перевод этого слова – "упорядочиватель"; так
называются средства, которые выстраивают неупорядоченные события в
определенном порядке. Как и счетчик событий, секвенсор представляется
целым числом, над которым выполняется единственная операция:
ticket. Операция ticket(S) возвращает текущее значение секвенсора
и увеличивает его на 1. Операция является атомарной. Начальное значение
секвенсора – 0.
298

Имея в своем распоряжении секвенсоры, мы можем так записать
решение задачи производителей–потребителей для произвольного числа
процессов:

1
2

/* типы данных - счетчик событий
и секвенсор */

3

typedef unsigned int eventcounter;

4

typedef unsigned int sequencer;

5

/* счетчики для чтения и записи */

6

static eventcounter inCnt = 0,

7

outCnt = 0;

8

/* секвенсоры для чтения и записи */

9

static sequencer inSeq = 0, outSeq = 0;

10

/* буфер */

11

static portion buffer [BUFSIZE];

12

/* процесс-производитель */

13

void producer ( void ) {

14

int portNum;

/* номер порции */

15

/* рабочая область для порции */

16

portion work;

17

/* цикл производства */

18

while (1) {

19

< производство порции в work >

20

/* получение "билета"

21

на запись порции */

22

portNum = ticket (inSeq);

23

/* ожидание номера порции */

24

await (inCnt, portNum);

25

/* ожидание свободного места
299

в буфере */

26
27

await (outCnt, portNum - BSIZE+1);

28

/* запись в буфер */

29

memcpy (buffer + portNum % BSIZE,

30

&work, sizeof(portion) );

31

/* продвижение счетчика чтения */

32

advance (inCnt);

33

}

34

}

35

/* процесс-потребитель */

36

void consumer ( void ) {

37

int portNum;

38

/* рабочая область для порции */

39

portion work;

40

/* цикл потребления */

41

while (1) {

42
43

/* номер порции */

/* получение "билета"
на выборку порции */

44

portNum = ticket (outSeq);

45

/* ожидание номера порции */

46

await (outCnt, portNum);

47

/* ожидание появления в буфере */

48

await (inCnt, portNum+1);

49

/* выборка порции */

50

memcpy (&work,

51

buffer + portNum % BSIZE,

52

sizeof(portion) );

53

/* продвижение счетчика записи */

54

advance (outCnt);
300

55
56
57

< обработка порции в work>
}
}

Каждый производитель получает "билет" со своим номером в
очереди на запись в буфер (строка 22). Затем он ожидает, когда до него
дойдет очередь (строка 24), ожидает освобождения места в буфере (строка
27), записывает информацию (строки 29, 30) и наращивает счетчик
производств (строка 32). Увеличение счетчика событий inCnt является
сигналом к разблокированию как для потребителя, получившего "билет"
на выборку этой порции и ожидающего в строке 46, так и для
производителя, получившего "билет" на запись следующей порции и
ожидающего в строке 27. Полученный процессом "билет" определяет и
адрес в буфере той секции, с которой будет работать процесс. Хотя
каждый процесс работает со своей секцией в буфере, одновременный
доступ к буферу однотипных процессов исключается ожиданием в строке
24 или 46. Если разрешить одновременный доступ к буферу двух,
например, производителей, то процесс, получивший "билет" на запись
порции в n-ю секцию буфера может закончить запись раньше, чем
процесс, пишущий порцию в n-1-ю секцию, даже если последний начал
запись раньше. Процесс, закончивший запись, увеличит счетчик inCnt и
выйдет из ожидания потребитель, имеющий билет на n-1-ю секцию,
запись в которую еще не закончена.

8.11. Рандеву
Модель
рассматривает

взаимодействия
синхронизацию

процессов,
и

передачу
301

названная
данных

как

рандеву,
единую

деятельность. Когда процесс A намерен передать данные процессу B, оба
процесса должны объявить о своей готовности установить связь, выдав
запросы на передачу и прием данных соответственно. Если процесс A
выдает заявку на передачу прежде, чем процесс B выдал заявку на прием,
то процесс A приостанавливается до выдачи заявки процессом B. И
наоборот: если процесс B выдал заявку на прием раньше, чем процесс A на
передачу, то приостанавливается процесс B. Таким образом, процессы
взаимодействуют только при встрече (рандеву) их заявок на передачу и
прием.
В

абстрактной

записи

взаимодействие

между

процессами

записывается так:

1

processA {

2

объявление локальной переменной x;

3

. . .

4

B!x;

5

. . .

6

}

7

processB {

8

объявление локальной переменной y;

9

. . .

10

A?y;

11

. . .

12

}

Нотация B!x в строке 4 означает, что процесс A передает процессу B
значение своей переменной x. A?y в строке 10 означает, что процесс B
принимает значение, переданное процессом A, и записывает его в свою
переменную y.
302

Эта запись отражает так называемую синхронную модель рандеву.
Запись асинхронной модели мы можем получить, заменив строку 10 на:

10

?y;

В синхронной модели оба процесса должны указывать в операторах
приема или передачи имя процесса-корреспондента. В асинхронной
модели только процесс-передатчик указывает имя процесса-приемника.
"Безадресный" оператор приема соответствует идеям структуризации
данных и программирования "снизу вверх", развивавшимся автором
моделей рандеву и мониторов – К.Хоаром [10]. Асинхронная модель
делает возможным разработку библиотечных процессов, которые, вопервых, могут использоваться в разных разработках, а во-вторых, играть
роль

процессов-серверов,

обрабатывающих

запросы

от

разных,

параллельно выполняющихся процессов-клиентов.
Асинхронная модель рандеву лежит в основе взаимодействия
процессов в языке ADA [6]. Мы не имеем возможности привести здесь
полное описание языка (его синтаксис во многом подобен синтаксису
языка Pascal) и ограничимся только средствами, интересующими нас в
первую очередь. Во всех последующих примерах ключевые слова языка
ADA записаны строчными буквами.
Процесс в языке ADA называется задачей и описание задачи состоит
из спецификаций задачи и ее тела. Спецификация имеет структуру:

task ИМЯ_ЗАДАЧИ is
< описания входных точек >
end;

Тело имеет структуру:
303

task body ИМЯ_ЗАДАЧИ is
< переменные и операторы >
end ИМЯ_ЗАДАЧИ;

В спецификации указываются точки входа задачи для рандеву. Их
описания идентичны описаниям процедур: имя и параметры с указанием
направления передачи параметров: in, out или inout. В задаче,
обращающейся к входной точке, обращение выглядит точно так же, как
обращение к процедуре. Однако, выполняется такоеобращение иначе. В
задаче-приемнике такое обращение обрабатывается оператором приема. В
простейшем случае такой оператор имеет вид:

accept ИМЯ_ВХОДА ( < параметры > ) do
< операторы >
end;

Оператор приема входит в структуру последовательно выполняемых
операторов тела задачи. Если при обращении к данному входу выполнение
задачи-приемника еще не дошло до оператора приема, то задачапередатчик блокируется. Если выполнение дошло до оператора приема, но
обращения к данному входу не было, блокируется задача-приемник.
В ряде случаев неизвестно заранее, в какой последовательности
будут

поступать

запросы.

Для

недетерминированного

выбора

нескольких возможных запросов используется оператор отбора:

select
< оператор accept >
< другие операторы >
304

из

or
< оператор accept >
< другие операторы >
or
. . .
else
< другие операторы >
end;

Когда выполнение приемника доходит до оператора отбора,
приемник готов выполнить любой из операторов приема, перечисленных
среди альтернатив отбора. Если к этому моменту уже поступили
обращения к нескольким входам, включенным в отбор, принимается одно
из обращений (какое именно – правила языка не определяют). Если
обращений нет, то либо выполняется альтернатива else, либо (если эта
альтернатива не задана) процесс-приемник ожидает.
Операторы accept, составляющие альтернативы отбора, могут быть
"защищены" условиями. Заголовок оператора в этом случае выглядит так:

when =>
accept
...

Защищенный оператор приема включается в число альтернатив
отбора только в том случае, если логическое выражение имеет значение
"истина".
Наше краткое описание средств языка само по себе, видимо,
недостаточно для его понимания, поэтому проиллюстрируем его примером
– все той же задачей производителей–потребителей:
305

1

PROD_CONS: declare;

2

/* пустая спецификация производителя */

3

task PRODUCER is

4

end;

5

/* тело производителя */

6

task body PRODUCER is

7

/* рабочая область для порции */

8

WORK : PORTION;

9

begin

10

loop /* цикл производства */

11

< производство порции в WORK >

12

/* запись порции */

13

PUTPORTION(WORK);

14

/* конец цикла производства */

15

end loop;

16

/* конец тела производителя */

17

end PRODUCER;

18

/* пустая спецификация потребителя */

19

task CONSUMER is

20

end;

21

/* тело потребителя */

22

task body CONSUMER is

23

/* рабочая область для порции */

24

WORK : PORTION;

25

begin

26

loop /* цикл потребления */

27

/* выборка порции */

28

GETPORTION ( WORK );
306

29

< обработка порции в WORK >

30

/* конец цикла потребления */

31

end loop;

32

/* конец тела потребителя */

33

end CONSUMER;

34

/* спецификация задачи-сервера */

35

task SERVER is

36

/* описание входных точек сервера */

37

entry GETPORTION(PORT : out PORTION);

38

entry PUTPORTION(PORT : in PORTION);

39

end;

40

/* тело сервера */

41

task body SERVER is

42

/* буфер */

43

BUFFER : array [1..BSIZE] of PORTION;

44

/* индексы для чтения и записи */

45

INCNT, OUTCNT :
INTEGER range 1..BSIZE := 1;

46
47

/* счетчик порций в буфере */

48

PORTCNT :
INTEGER range 0..BSIZE := 0;

49
50

begin

51

loop

/* цикл обслуживания */

52

/* выбор из наступивших событий */

53

select when PORTCNT < BSIZE =>

54
55
56
57

/* если буфер не полон,
обслуживается запись */
accept
PUTPORTION(PORT:in PORTION) do
307

58

/* запись */

59

BUFFER[INCNT] := PORT;

60

end;

61

/* модификация счетчиков */

62

INCNT := INCNT mod BSIZE + 1;

63

PORTCNT := PORTCNT + 1;

64

or

65

/* или если буфер не пуст,

66

обслуживается выборка */

67

accept

68

GETPORTION(PORT:out PORTION) do

69

/* выборка */

70

PORT := BUFFER[OUTCNT];

71

end;

72

/* модификация счетчиков */

73

OUTCNT := OUTCNT mod BSIZE + 1;

74

PORTCNT := PORTCNT - 1;

75

end select;

/* конец выбора */

76

/* конец цикла обслуживания */

77

end loop;

78

/* конец тела сервера */

79

end SERVER;

80

/* главная процедура */

81

begin

82

/* запуск всех задач */

83

initiate SERVER, PRODUCER, CONSUMER;

84

end.

В нашу программу входят:
308

• главная процедура (строки 80 - 84);
• задача-производитель (строки 2 - 17);
• задача-потребитель (строки 18 - 33);
• задача-сервер

(строки

34

-

обеспечивающая

79),

обмен

производителя и потребителя с буфером.
Главная процедура запускает три другие задачи оператором
initiate (строка 83) и переходит в ожидание. Она завершится, когда
завершатся все запущенные ею задачи. Задачи PRODUCER и CONSUMER не
имеют операторов приема, поэтому их спецификации (строки 2 - 4 и 18 20) вырожденные – пустые. Тела этих задач содержат простые
бесконечные циклы (loop), в которых выполняется подготовка или
обработка порции и обращение к соответствующей входной точке сервера.
Задача SERVER является аналогом монитора. В ее спецификации (строки
34 - 39) описаны две входные точки: GETPORTION и PUTPORTION. Сам
буфер является локальным в теле сервера (строка 43), также локальны и
индексы чтения и записи (строки 45, 46) и счетчик порций (строки 48 - 49).
Выполнение сервера представляет собой бесконечный цикл (строки 51 77), в каждой итерации которого обрабатывается одно обращение.
Оператор select (строки 52 - 75) обеспечивает выбор из обращений:
GETPORTION или PUTPORTION. В зависимости от значения счетчика
PORTCNT из числа альтернатив может исключаться GETPORTION – если
буфер пуст или PUTPORTION – если он полон. Если к началу очередной
итерации обращений нет или есть обращение, которое не позволяет
принять

защита

when,

сервер

ожидает.

Обратите

внимание

на

операторные скобки do ... end, следующие за операторами accept
(строки 57 - 60 и 68 - 71). Они ограничивают критическую секцию.
Выполнение процесса-передатчика не возобновится до тех пор, пока
процесс-приемник не выйдет из критической секции. Мы включили в
309

критические

секции

приемов

только

операторы,

непосредственно

работающие с параметрами вызовов, так как основное предназначение
этой секции – защита параметров от преждевременного их изменения.
Остальные

операторы,

выполняемые

в

ходе

обработки

вызовов

(модификация индексов и счетчика), выполняются уже вне критической
секции.
В нашем примере мы запустили по одному производителю и
потребителю. В языке, однако, имеется возможность запускать любое
число

однотипных

задач:

как

полностью

идентичных,

так

и

различающихся по значениям параметров.
Модель

рандеву

как

достаточно

универсальный

метод

взаимодействия позволяет легко реализовать и примитивы взаимного
исключения и синхронизации. Двоичный семафор, например, выглядит
так:

task SEMAPHORE is
entry P;
entry V;
end;
task body SEMAPHORE is
begin
loop
accept P;
accept V;
end loop;
end SEMAPHOR;

В этой задаче операторы приема не являются альтернативными, а
выполняются строго последовательно. Если какая-либо внешняя задача
310

выполнит P-обращение, то любая задача, выдавшая еще одно P-обращение,
будет заблокирована до тех пор, пока не будет выполнено V-обращение и
семафор не войдет в следующую итерацию своего цикла.
Асимметричные рандеву являются дальнейшим развитием идеи
мониторов. В большинстве ADA-приложений задачи четко разделяются на
задачи-клиенты, выдающие вызовы, и задачи-серверы, их принимающие.
Однако, концептуально рандеву являются более универсальным и гибким
средством взаимодействия процессов. Обратите внимание на то, что
взаимодействующие задачи не используют общих переменных. Это делает
язык ADA независимым от конкретной реализации параллельной работы в
системе: это может быть однопроцессорная система с разделением
времени,

мультипроцессорная

система

с

общей

памятью

или

многомашинная система (сеть).
Существенным недостатком модели рандеву является то, что
большинство

решений,

ее

использующих,

требует

введения

дополнительных процессов (в наших примерах – задача-сервер или
семафор, как отдельная задача). Это увеличивает число переключений
процессов и накладные расходы системы.

КОНТРОЛЬНЫЕ ВОПРОСЫ
1. Покажите, что задача синхронизации является частным случаем
задачи взаимного исключения.
2. Для каких задач использование единственной общей переменной
исключения может быть оправданным?
3. Сопоставьте
использующих

свойства

атомарность

алгоритмов
команд

обращений к памяти.
311

и

взаимного

использующих

исключения,
атомарность

4. В состав семафора входит переменная взаимного исключения и
скобки критической секции. Почему же потери на занятое ожидание в
семафоре не могут быть значительными?
5. Какие ограничения имеются в решении задачи "производители–
потребители" методом семафоров?
6. В чем преимущества встраивания критической секции в язык
программирования? Покажите, как используются скрытые семафоры для
реализации встроенной критической секции.
7. В чем преимущество использования мониторов? Покажите, как
используются скрытые семафоры для реализации защищенных процедур.
8. Проблема вложенных вызовов мониторов может быть решена при
помощи иерархической дисциплины, описанной в разделе 5.3. Покажите
пути такой реализации.
9. Почему при применении групповых мониторов процедуры read и
write не должны быть защищенными?
10.Покажите
исключением

реализацию

возможности

задачи

бесконечного

"читатели–писатели"
откладывания

с

процесса-

писателя.
11.В

чем

преимущества

решения

задачи

"производители–

потребители" методами счетчиков событий или секвенсоров перед
методом семафоров?
12.Как, используя семафоры, реализовать счетчики событий и
секвенсоры?
13.

В каких ситуациях процесс, участвующий во взаимодействии по
модели рандеву, может быть заблокирован?

14.

Объясните реализацию семафора методом рандеву.

Глава 9. Системные средства взаимодействия
процессов
312

В предыдущей главе мы уже фактически затронули эту тему, однако
средства, которые мы рассматривали, в основном ограничивались теми,
которые реализуются самим программистом или компилятором. Здесь мы
сосредоточимся на тех средствах, использование которых поддерживается
API ОС.

9.1. Скобки критических секций.
Выделение

критических

секций

как

системное

средство

целесообразно применять для относительно сильно связанных процессов –
таких, которые разделяют большой объем данных. Кроме того, поскольку,
как мы показали в предыдущей главе, при применении программистом
скобок критических секций возможны ошибки, приводящие к подавлению
одних процессов другими, важно, чтобы конфликты между процессами не
приводили к конфликтам между пользователями. Эти свойства характерны
для нитей – параллельно выполняющихся частей одного и того же
процесса: они все принадлежат одному процессу – одному пользователю и
разделяют почти все ресурсы этого процесса. Следовательно, критические
секции целесообразно применять только для взаимного исключения нитей.
ОС может предоставлять для этих целей элементарные системные вызовы,
функционально аналогичные рассмотренным нами в предыдущей главе
csBegin и csEnd. Когда нить входит в критическую секцию, все
остальные нити этого процесса блокируются. Блокировка не затрагивает
другие процессы и их нити. Естественно, что такая политика весьма
консервативна и снижает уровень мультипрограммирования, но это может
повлиять

на

эффективность

только

в

рамках

одного

процесса.

Программист может самостоятельно организовать и более либеральную
313

политику доступа к разделяемым ресурсам, используя, например,
семафоры, которые будут описаны ниже.
Кроме того, роль таких скобок могут играть системные вызовы типа
suspend и release, первый из которых приостанавливает выполнение нити, а
второй – отменяет приостановку.

9.2. Виртуальные прерывания или сигналы
Мы уже говорили о виртуальных прерываниях, как о средстве, при
помощи которого ОС сигнализирует процессу об окончании асинхронно
выполняемой операции ввода-вывода. Расширяя эту концепцию, можно
применять виртуальные прерывания для сообщения процессу о любом
внешнем по отношению к нему событии. В частности, виртуальное
прерывание

может

использоваться

для

того,

чтобы

выдавать

синхронизирующий сигнал из одного процесса в другой. ОС может
предоставлять в распоряжение процессов системный вызов:
raiseInterrupt (pid, intType );
где pid – идентификатор процесса, которому посылается прерывание,
intType – тип (возможно, номер) прерывания. Идентификатор процесса –
это не внешнее его имя, а манипулятор, устанавливаемый для каждого
запуска процесса ОС. Для того, чтобы процесс мог послать сигнал другому
процессу, процесс-отправитель должен знать идентификатор процессаполучателя, то есть находиться с ним в достаточно "конфиденциальных"
отношениях.

Чтобы

предотвратить

возможность

посылки

непредусмотренных прерываний, могут быть введены дополнительные
ограничения: разрешить посылку прерываний только от процессовпредков к потомкам или ограничить обмен прерываниями только
процессами одного и того же пользователя.
314

Когда процессу посылается прерывание, управление передается на
обработчик этого прерывания в составе процесса. Процесс должен
установить адрес обработчика при помощи системного вызова типа:
setInterruptHandler
(intType, action, procedure );
где action – вид реакции на прерывание. Вид реакции может задаваться
из перечня стандартных, в число которых могут входить: реакция по
умолчанию, игнорировать прерывание, восстановить прежнюю установку
или

установить

в

качестве

обработчика

прерывания

процедуру

procedure, адрес которой является параметром системного вызова.
Разумеется, в системе должны быть определены допустимые типы
виртуальных прерываний. Виртуальные прерывания могут генерироваться в
следующих случаях:

• завершение или другое изменение статуса процесса-потомка;
• программные ошибки (прерывания-ловушки);
• ошибки в выполнении системных вызовов или неправильные
обращения к системным вызовам;
• терминальные

воздействия

(например,

нажатие

клавиши

"Внимание" или Ctrl+Break);
• при необходимости завершения процесса (системный вызов
kill);
• сигнал от таймера;
• сигналы, которыми процессы обмениваются друг с другом;
• и т.д.
Если процесс получает прерывание, для которого он не установил
обработчик,

то

процесс

должен

аварийно

завершиться

(это

устанавливаемый по умолчанию вид реакции на прерывание). Такая
установка может показаться чрезмерно жесткой, но вспомните, например,

315

какова будет реакция системы на реальное прерывание, для которого не
определен его обработчик (вектор прерывания в Intel-Pentium).
Еще одно решение, которое должен принять конструктор ОС, –
является ли установка обработчика постоянной (до ее явной отмены) или
одноразовой (для обработки только одного прерывания). Второй вариант
является более гибким, так как каждая процедура обработки прерывания
может при необходимости заканчиваться новым системным вызовом
setInterruptHandler, которым будет задана установка на обработку
следующего прерывания этого типа. Это решение можно также
переложить на программиста, включив соответствующий параметр в
спецификации системного вызова.
Как

должна

несуществующему

реагировать
процессу?

ОС

на

По-видимому,

посылку

прерывания

аварийное

завершение

процесса, выдавшего такое прерывание, может быть нормальной реакцией
системы. Возможно, впрочем, и более либеральное решение – завершить
вызов raiseInterrupt с признаком ошибки. Аналогичный эффект
может вызвать выполнение прерывания, для которого в процессеприемнике установлен специальный режим обработки – недопустимое
прерывание.
Как и для реальных прерываний, процесс должен иметь средства
запрещения виртуальных прерываний (например, при вхождении в
критическую секцию) – всех или выборочно по типам. Для этих целей
должны использоваться специальные системные вызовы. Если прерывание
запрещено, то его обработка откладывается до разрешения прерываний.
Когда обработка разрешается, она выполняется по тому виду реакции,
который установлен на момент выполнения (он может отличаться от
установленного на момент выдачи прерывания). Среди зарезервированных
за ОС типов прерываний обязательно должны быть такие, запретить
316

которые или переопределить обработку которых процесс не имеет
возможности, – обязательно в этом списке должно быть прерывание kill.
В большинстве современных ОС (Unix, OS/2 и др.) виртуальные
прерывания носят название сигналов и используются прежде всего для
сигнализации о чрезвычайных событиях. Сигнальные системы конкретных
ОС, как правило, не предоставляют в составе API универсального вызова
типа raiseInterrupt, который позволял бы пользователю выдавать
сигналы любого типа. Набор зарезервированных типов сигналов ограничен
(в Unix, например, их 19, а в OS/2 – всего 7), не все из них доступны
процессам и для каждого из доступных имеется собственный системный
вызов. Недопустимы также незарезервированные типы сигналов. В набор
включается несколько (по 3 – в упомянутых ОС) типов сигналов,
зарезервированных

за

процессами,



эти

типы

и

используют

взаимодействующие процессы для посылки друг другу сигналов, которые
они интерпретируют по предварительной договоренности.
В момент, когда для процесса генерируется виртуальное прерывание,
процесс, возможно (в однопроцессорной системе – наверняка), пребывает в
неактивном состоянии. Поэтому обработка прерывания откладывается до
момента активизации процесса (в порядке очереди к планировщику), а
прерывание запоминается в блоке контекста процесса. Как должно
обрабатываться виртуальное прерывание, если во время его поступления
процесс выполняет системный вызов? Выполнение системного вызова
включает в себя как фрагменты кода, выполняемые в привилегированном
режиме, так и фрагменты, выполняемые в режиме задачи. Очевидно, что
привилегированные фрагменты прерываться не могут – их выполнение
может быть связано с изменениями системных структур данных, которые
должны выполняться транзакционно (т.е. не должны прерываться). В этом
случае пришедшее виртуальное прерывание запоминается в блоке контекста
процесса и обрабатывается при переходе процесса из состояния ядра в
состояние

задачи.

Но

системный

вызов

может

содержать

и

непривилегированную часть, к тому же выполняющуюся весьма длительно

317

(например, ввод с клавиатуры с ожиданием). Разумным решением будет
разрешение прерывать такой системный вызов, но в этом случае
выполнение прерванного системного вызова может заканчиваться с
ошибкой – и процесс должен быть готов к этому.

9.3. Модель виртуальных коммуникационных портов
Большинство средств взаимодействия процессов соответствуют
концепции коммуникационных портов – виртуальных устройств, через
которые

процессы

обмениваются

данными.

Как

устройства

коммуникационные порты могут "вписываться" в файловую систему в
качестве специальных файлов. Такое сведение средств взаимодействия к
файловой модели в общем случае обеспечивает три преимущества:
• возможность

единообразных

операций

со

средствами

взаимодействия и с файлами;
• возможность доступа к удаленным средствам как к удаленным
файлам;
• возможность использования единых средств контроля доступа для
файловой системы и для коммуникаций.
Концепция коммуникационных портов, однако, в реальных ОС
выдерживается далеко не строго. Реально манипулирование далеко не
всеми средствами взаимодействия между процессами возможно свести к
однотипным операциям. Доступ к удаленным средствам решается
методами сетевых модулей ОС. Разграничение доступа в полном объеме
мы наблюдали только в AS/400, и то не в рамках файловой системы, а в
контексте общей объектно-ориентированной структуры этой системы (см..
разд. 13.7). Тем не менее, тенденция к модели портов в той или иной
степени наблюдается в современных ОС, прежде всего, в части
именования средств взаимодействия. В этом разделе мы рассмотрим
318

общие свойства средств взаимодействия, называя их виртуальными
коммуникационными портами.
Виртуальный коммуникационный порт представляет собой ресурс
ОС. Он, разумеется, имеет и физическое представление: структуры
данных, области памяти, скрытые семафоры и т.п. Порты, как ресурсы,
конструируемые ОС, не имеют жесткого ограничения по количеству –
новые порты могут создаваться по мере надобности и уничтожаться, когда
необходимость в них отпадает. При использовании порта несколькими
процессами один процесс создает порт, а другие – получают доступ к уже
существующему порту.
Как и при работе со всяким ресурсом, процесс должен получить
доступ к этому порту – открыть его. Системный вызов открытия
коммуникационного порта (или его создания) возвращает процессу
манипулятор, который процесс использует как идентификатор порта во
всех последующих операциях с ним. Порт используется одновременно как
минимум двумя процессами, поэтому важно, чтобы манипуляторы у
процессов-корреспондентов,

взаимодействующих

через

этот

порт,

связывались с одним и тем же физическим представлением порта.
Возможны два варианта: либо все использующие порт процессы имеют
один и тот же манипулятор порта, либо для каждого процесса этот
манипулятор индивидуальный. В любом случае ОС поддерживает в ядре
таблицу открытых портов (точнее, несколько таких таблиц – по одной для
каждого

типа

средств

взаимодействия

процессов).

В

качестве

манипулятора может использоваться либо указатель на элемент этой
таблицы – тогда манипулятор будет одинаковым для разных процессов,
либо указатель на элемент индивидуальной таблицы, входящей в состав
контекста процесса, а уже элемент таблицы процесса содержит ссылку на
общесистемную таблицу. Очевидно, что второй вариант более надежен,
так как исключает случайный доступ к порту. Даже в тех случаях, когда
319

ОС выдает один и тот же манипулятор нескольким процессам, она требует,
чтобы процесс выполнил системный вызов получения доступа к ресурсу.
Для доступа двух процессов к одному и тому же физическому
представлению порта в вызове открытия порта необходимо указать
параметры, позволяющие системе это физическое представление найти. С
точки зрения идентификации порты могут быть именованными или
неименованными.
Именованный порт имеет внешнее имя. Системный вызов открытия
именованного порта требует указания этого имени в качестве параметра
вызова.

Пользователи-разработчики

взаимодействующих

процессов

заранее договариваются об используемых именах портов. Система
именования портов и открытия именованных портов аналогична файловой
системе. Имена средств взаимодействия формируются по соглашениям
именования файлов и выглядят как имена файлов, расположенных в
специальных каталогах, например: каталог \shrmem – для общих
областей памяти, каталог \sem – для системных семафоров, \pipe – для
каналов, \queues – для очередей.
Неименованный порт внешнего имени не имеет. При создании такого
порта системный вызов возвращает его манипулятор – и это единственное,
чем располагает процесс для идентификации порта. Манипулятор порта
почти наверняка будет разным при разных выполнениях одной и той же
программы. Для установления связи между процессами процесс-создатель
порта должен передать процессу-корреспонденту манипулятор созданного
им порта. Процесс-корреспондент в системном вызове открытия порта
указывает идентификатор процесса-создателя и манипулятор порта у
процесса-создателя, а в ответ получает манипулятор того же порта для
себя.

Передача

манипулятора

процессу-корреспонденту

может

производиться как передача ресурса от предка к потомку или (если
процессы не связаны родством) через именованный порт. Как правило,
320

неименованные порты используются для связи между процессами –
предком и потомком, в этом случае потомок наследует от предка уже
открытый коммуникационный порт. Неименованные порты используются
для связи между независимыми процессами.
В

связи

с

использованием

портов

несколькими

процессами

возникают проблемы закрытия портов. Закончив работу с портом, процесс
выполняет

системный

вызов

закрытия.

В

ОС

поддерживается

общесистемная таблица портов и обязательным для системы требованием
является закрытие при завершении процесса всех портов, которые процесс
"забыл" закрыть явным образом. Если с портом работают два процесса, и
один из них закрыл порт, а другой обращается к этому порту, то для
последнего процесса этот системный вызов заканчивается с признаком
ошибки – и это единственно возможное решение. Еще одна проблема –
уничтожение физического представления портов. Она может решаться
двумя путями: либо порт уничтожается, когда он закрывается последним
из использующих его процессов, либо он уничтожается (специальным
системным вызовом или простым закрытием) тем процессом, который его
создал. Последний случай более привлекательный с точки зрения
упорядочения доступа, но он может порождать больше ошибок, когда
процессы-корреспонденты обращаются к уже несуществующему порту.
Средства взаимодействия, рассматриваемые нами ниже, являются
частными случаями модели виртуальных коммуникационных портов.

9.4. Общие области памяти
Два и более процессов могут использовать одну и ту же физическую
область памяти. Наиболее просто это достигается в тех моделях памяти,
которые обеспечивают динамическую трансляцию адресов. Напомним, что
321

каждый процесс имеет собственную таблицу сегментов или страниц, в
которой содержится помимо прочего базовый адрес сегмента/страницы в
физической памяти. Для разделяемой области памяти создается по
элементу в таблице для каждого процесса, ее использующего. В чисто
страничной модели, однако, возникают трудности, связанные с тем, что
разделяемая область памяти может иметь объем, некратный размеру
страницы, обычно в таких случаях разделяемая область выравнивается до
границы страницы. В сегментной или сегментно-страничной модели таких
проблем нет. Поскольку для каждого процесса разделяемый сегмент
описывается своим дескриптором, права доступа к сегменту могут быть
установлены различными для разных процессов.
В случае именованных областей памяти один процесс создает общую
область памяти:
vAddr =
createNamedMemorySegment(segmentName,
segmentSize);
а второй ее "открывает":
vAddr =
openNamedMemorySegment(segmentName);
В этих вызовах segmentName – имя области, segmentSize – ее
размер. Оба вызова возвращают виртуальный адрес общей области памяти
в виртуальном адресном пространстве процесса – vAddr.
Для

неименованной

области

памяти

создание

осуществляется вызовом:
vAddr = createMemorySegment(segmentSize);
а "открытие":
vAddr =
openMemorySegment(hostAddr, hostPid);

322

области

где hostAddr – виртуальный адрес области памяти у процесса-создателя
области, hostPid – идентификатор процесса-создателя области. Этот
вызов возвращает виртуальный адрес области в адресном пространстве
процесса, открывшего область.
Разумеется,

в

составе

API

имеются

системные

вызовы

"закрытия"/уничтожения общей области памяти.
Разделяемые области памяти, однако, порождают ряд проблем, как
для программистов, так и для ОС. Проблемы программистов – те, что
рассматривались в предыдущем разделе: взаимное исключение процессов
при доступе к общей памяти. Программисты могут дифференцировать
права доступа для процессов или организовать взаимное исключение,
используя семафоры (см. ниже). Проблемы ОС – организация свопинга.
Очевидно, что вероятность вытеснения разделяемого сегмента или
страницы должна быть тем меньше, чем больше процессов разделяют этот
сегмент/страницу. Если каждый процесс имеет собственный дескриптор
разделяемого сегмента или страницы, то учет использования сегмента или
страницы (поля used и dirty) будут вестись по каждому процессу
отдельно. ОС должна обрабатывать, например, такой случай: два процесса
A и B разделяют сегмент; процесс A произвел запись в сегмент и в его
дескрипторе сегмент помечен как "грязный". В то время, когда активен
процесс B, принимается решение о вытеснении этого сегмента из памяти.
Но в дескрипторе процесса B этот сегмент имеет признак "чистый",
поэтому сегмент может быть освобожден в физической памяти без
сохранения на внешней памяти и изменения в сегменте, сделанные
процессом A, будут утеряны. ОС приходится вести отдельную таблицу
разделяемых сегментов, в которой отражать истинное их состояние.
С точки зрения идентификации, разделяемые области памяти могут
рассматриваться как виртуальные коммуникационные порты. Для общей
области

может

быть

по

соглашению
323

между

разработчиками

взаимодействующих процессов установлено внешнее имя, которое будет
использовано для получения доступа. (В Windows 95, например, такие
области называются "файлами отображаемой памяти" – memory mapped
file – и для установления доступа к ним используются системные вызовы
типа create и open). Для неименованных областей памяти возможна
передача селектора (номера в таблице дескрипторов) от одного процесса к
другому.
В системах, которые ориентированы на процессор Intel-Pentium,
может использоваться то обстоятельство, что адресация возможна через
две таблицы дескрипторов: LDT и GDT. За счет этого общее адресное
пространство процесса может достигать 4 Гбайт. Из них младшие 2 Гбайт
адресуются через LDT, а старшие – через GDT. Глобальная таблица
дескрипторов – общая для всех процессов, и именно она может
использоваться

для

доступа

к

совместно

используемой

памяти.

Размещение в общих виртуальных адресах удобно для системных
программ и динамических библиотек, к которым происходят частые
обращения из приложений: если эти компоненты ОС находятся в адресном
пространстве процесса, то обращения к ним не требуют переключения
контекста. Но с другой стороны, это снижает надежность: если системные
компоненты доступны для приложения, то они могут быть им испорчены.
Поэтому

такая

"роскошь"

может

быть

допущена

только

в

однопользовательских системах.

9.5. Семафоры
В предыдущей главе мы достаточно подробно рассмотрели сущность
и свойства семафоров. ОС может предоставлять семафоры в распоряжение
пользователя, как средство для самостоятельного решения задач,
требующих

взаимного

исключения и/или
324

синхронизации.

Помимо

основных для семафоров P- и V-операций конкретные семафорные API ОС
могут включать в себя расширенные и сервисные функции.
При работе с именованным семафором один из процессов должен
создать системный семафор при помощи вызова createSemaphore,
другие процессы получают доступ к созданному системному семафору при
помощи вызова openSemaphore. Среди входных параметров этих
вызовов имеется внешнее имя семафора, вызовы возвращают манипулятор
для семафора, используемый для его идентификации при последующей
работе с ним. При окончании работы с системным семафором процесс
должен выполнить вызов closeSemaphore. Семафор уничтожается,
когда он закрыт во всех процессах, его использовавших.
Выполнение операций над

семафором может обеспечиваться

системным вызовом вида:
flag = semaphoreOp(semaphorId, opCode,
waitOption);
где semaphorId – манипулятор семафора, opCode – код операции,
waitOption – опция ожидания, flag – возвращаемое значение, признак
успешного выполнения операции или код ошибки.
Помимо основных для семафоров P- и V-операций конкретные
семафорные API ОС могут включать в себя расширенные и сервисные
функции, такие как безусловная установка семафора, установка семафора с
ожиданием его очистки, ожидание очистки семафора. При выполнении
системных вызовов – аналогов P-операции, как правило, имеется
возможность задать опцию ожидания – блокировать процесс, если
выполнение P-операции невозможно, или завершить системный вызов с
признаком ошибки.
Во многих современных ОС наряду с семафорами "в чистом виде"
API представляет те же семафоры и в виде "прикладных" объектов –
объектов взаимного исключения и событий. Хотя содержание этих
325

объектов одно и то же – семафор, ОС в отношении этих объектов
представляет для прикладных процессов специфическую семантику API,
соответствующую задачам взаимного исключения и синхронизации.
Все

современные

ОС

предоставляют

прикладному

процессу

возможность работать с "массивами семафоров", то есть задавать список
семафоров и выполнять операцию над всем списком, например, ожидать
очистки любого в заданном списке. Наиболее развито это средство в ОС
Unix, где имеется возможность выполнять за один системный вызов
semop (аналог нашего semaphoreOp) сразу нескольких различных
операций над несколькими семафорами, причем весь список операций
выполняется как одна транзакция.
Неименованные семафоры обычно используются как средство
взаимного исключения и синхронизации работы нитей одного процесса.

9.6. Программные каналы
Программный канал по-английски называется pipe (труба), и это
весьма удачное название. Канал действительно можно представить как
трубопровод

пневматической

почты,

проложенный

между

двумя

процессами, как показано на рисунке 9.1. По этому трубопроводу данные
передаются от одного процесса к другому. Как и трубопровод, программный
канал однонаправленный (хотя, например, в Unix одним системным вызовом
создаются сразу два разнонаправленных канала). Как и трубопровод,
программный канал имеет собственную емкость: данные, записанные в
канал, не обязательно должны немедленно выбираться на противоположном
его конце, но могут накапливаться в канале, пока это позволяет его емкость.
Как и трубопровод, канал работает по дисциплине FIFO: первый вошел –
первый вышел.

326

Рисунко 9.1 Программные каналы

Из всех средств взаимодействия между процессами программные
каналы

лучше

всего

вписываются

в

модель

виртуальных

коммуникационных портов. Канал для процесса практически аналогичен
файлу. Специальные системные вызовы типа createPipe, openPipe
используются для создания канала и получения доступа к каналу, а для
работы с каналом используются те же вызовы read и write, что и для
файлов, и даже закрытие канала выполняется файловым системным
вызовом close. При создании канала для него создается дескриптор, как
для открытого файла, что позволяет работать с ним далее, как с файлом.
Канал, однако, представляет собой не внешние данные, а область памяти.
Для канала выделяется память в системной области, что может
ограничивать емкость канала.
Наиболее часто используются неименованные каналы как средство
связи между родителем и потомком. Операция создания неименованного
канала возвращает два файловых манипулятора: для чтения и для записи.
Процесс-предок передает эти манипуляторы процессу-потомку. Если связь
между предком и потомком однонаправленная, то каждый из них
закрывает канал по одному из манипуляторов. Например, если данные
передаются только от предка к потомку, то предок после передачи
манипуляторов закрывает канал на чтение, а потомок – на запись. Пример
установления такой связи в ОС Unix приведен в главе 11, во фрагменте
программы командного интерпретатора.

327

С точки зрения реализации канал представляет собой классический
вариант задачи "производитель–потребитель": один процесс пишет в канал
данные, другой – читает их из канала. Если при попытке записи данных в
канал обнаруживается, что канал полон, пишущий процесс блокируется до
освобождения места в канале, если при попытке чтения данных
обнаруживается, что канал пуст, читающий процесс блокируется до
появления

в

канале

данных.

Внутренним

механизмом

ОС,

обеспечивающим синхронизацию в таких ситуациях, является, конечно же,
семафор.
Именованные

каналы

представляют

собой

удобное

средство

клиент/серверных коммуникаций. Именованные каналы в некоторых ОС
(например,

OS/2)

существенно

отличаются

от

неименованных.

Именованные каналы ориентированы в этих системах прежде всего на
взаимодействие процессов в сетевой среде (или, точнее, для них
прозрачно, находятся ли оба процесса на одном компьютере или в разных
узлах сети). Такой канал двунаправленный, то есть к нему возможны
обращения и для чтения, и для записи. Данные в канале могут
передаваться как потоком байт, так и сообщениями. В последнем случае
каждое сообщение снабжается заголовком, в котором указывается его
длина. К одному концу канала постоянно подключен процесс, его
создавший, – владелец или сервер, к другому концу могут подключаться
различные процессы-клиенты, таким образом, обмен данными по
именованному каналу между процессами, ни один из которых не является
владельцем канала, невозможен. При создании канала (системный вызов
createNamedPipe)

указывается

максимально

возможное

число

клиентов, которые могут быть одновременно подключены к каналу (это
число, впрочем, может и не ограничиваться). Когда владелец создает
канал, последний находится в так называемом отключенном состоянии.
Системным вызовом connectNamedPipe владелец переводит канал в
328

ждущее состояние. Теперь процессы-клиенты могут подключаться к
другому концу канала при помощи файловых системных вызовов
openNamedPipe. Канал, открытый хотя бы одним клиентом, считается
подключенным, сервер и подключенные клиенты могут работать с ним,
используя вызовы файлового API. Клиенты отключаются от канала
вызовом

close,

в

распоряжении

владельца

есть

вызов

disconnectNamedPipe – разрыва канала. Помимо обычных вызовов
файлового обмена для работы с именованным каналом в составе API могут
быть специальные системные вызовы, обеспечивающие выполнение
сложных

транзакций

на

именованном

канале,

например:

transactNamedPipe – взаимный обмен данными (вывод и ввод) за одну
операцию, callNamedPipe – обеспечивает также открытие канала,
взаимный обмен, закрытие канала. Кроме того, к именованному каналу
или к нескольким именованным каналам может быть подключен семафор,
который сбрасывается при изменении состояния канала (заполнен – не
заполнен, пуст – не пуст).

9.7. Очереди сообщений
Очереди воплощают модель взаимодействия процессов "много
отправителей – один получатель". Эту модель часто называют почтовым
ящиком (mailbox) из-за сходства с почтовым ящиком, висящим на дверях
каждой квартиры.
Процесс-получатель является владельцем очереди, он создает
очередь, а остальные процессы получают к ней доступ, "открывая" ее.
Очередь обычно имеет внешнее имя. Передача данных в очереди
происходит всегда сообщениями, причем каждое сообщение имеет
заголовок и тело. Заголовок всегда имеет фиксированный для данной
329

системы формат. В него обязательно входит длина сообщения, а другая
информация зависит от спецификаций конкретной системы: это может
быть приоритет сообщения, тип сообщения, идентификатор процесса,
пославшего сообщение, и т.п. Тело сообщения интерпретируется по
правилам,

устанавливаемым

самими

процессами:

отправителем

и

получателем. Заголовок и тело представляют собой существенно разные
структуры данных и располагаются в разных местах в памяти. Собственно
очередь (чаще всего – линейный список) ОС составляет из заголовков
сообщений. В элементы очереди включаются указатели на тела
сообщений, располагающиеся в памяти системы или процессов (об этом –
ниже).
Как правило, возможности процесса-получателя сообщений не
ограничиваются чтением по дисциплине FIFO, ему предоставляется более
богатый выбор дисциплин: LIFO, по приоритету, по типам, по
идентификаторам отправителя и т.п. В распоряжении владельца имеются
также средства определения размера очереди, а возможно, и просмотра
очереди – неразрушающего чтения из нее. В распоряжении процессаотправителя имеется только вызов типа sendMessage – посылки
сообщения в очередь. Если при попытке процесса послать сообщение
обнаруживается, что очередь заполнена, процесс-отправитель блокируется.
Это, впрочем, довольно редкий случай, так как системные ограничения на
размер очередей никогда не бывают слишком жесткими. Процессполучатель блокируется при попытке читать сообщение, когда очередь
пуста.
Существенным вопросом при конструировании механизма очередей
является вопрос о включении или невключении в ОС системной
буферизации сообщений. При включении такого средства (рисунок 9.2.)
тело посылаемого сообщения копируется в системную область памяти, а при
чтении – копируется из нее в адресное пространство процесса-получателя.

330

При отсутствии системной буферизации тела сообщений хранятся в общей
для отправителя и получателя памяти, а передается только указатель на тело
сообщения (рисунок 9.3.). В первом случаевыполняются дополнительные
пересылки, затрачивается дополнительная память и вводятся более жесткие
ограничения на объем сообщений, но достигается надежность передачи и
значительно более простой интерфейс процессов. Во втором случае
значительно экономится память, но сами процессы должны заботиться об
управлении совместно используемой памяти и о сохранности сообщений в
ней. При отсутствии системной буферизации сообщений применяются
обычно два метода передачи тела сообщения: либо процесс-отправитель
помещает тело сообщения в отдельный разделяемый сегмент, получает у ОС
манипулятор этого сегмента для процесса-получателя и передает этот
манипулятор в составе сообщения; либо для всех сообщений выделяется
одна общая область памяти с общим манипулятором и для размещения
сообщения в нем используются системные вызовы выделения памяти в куче.

Рис.9.2. Размещение сообщений в адресном пространстве ядра

331

Рис.9.3. Размещение сообщений в адресном пространстве процессаотправителя

Контрольные вопросы

1. Почему

системные

вызовы



скобки

критических

секций

применяются для нитей, но не для процессов?
2. В чем сходство и в чем различия между сигналами и реальными
прерываниями?
3. Процесс, которому посылается сигнал, как правило, в момент
посылки неактивен. Как поступает ОС с сигналом в таком случае?
4. Опишите различия между именованными и неименованными
программными средствами взаимодействия процессов.
5. Общие

области

памяти

могут

располагаться

либо

в

перекрывающейся части виртуальных адресных пространств процессов,
либо в изолированных частях виртуальных адресных пространств. Каким
образом реализуется тот и другой метод размещения? Сопоставьте их
достоинства и недостатки.
6. Какими внутренними механизмами обеспечивается защита от
записи в заполненный программный канал и защита от чтения из пустого
программного канала?
332

7. Покажите, как представить семафор в виде "переменной взаимного
исключения" и "события".
8. Каким образом используются скрытые семафоры во внутренней
реализации механизма очередей?
9. Покажите, что задачи взаимного исключения и синхронизации
могут быть решены при помощи очередей сообщений.

Глава 10. Защита ресурсов
10.1. Общие требования безопасности
В системе ценностей современного мира информация занимает одно
из ведущих мест и рейтинг ее в этой системе возрастает с течением
времени почти экспоненциально. Как всякая ценность, информация
должна быть надежно защищена. Защита информации достигается
комплексом мер безопасности (security), включающим в себя как
обеспечение

целостности

(непротиворечивости)

и

сохранности

информации, так и контроль доступа к ней.
Требования

к

безопасности



целостности,

сохранности

и

конфиденциальности пользовательских и системных программ и данных, а
также к потреблению ресурсов пользователями в пределах установленных
им бюджетов всегда являлось одним из важнейших в вычислительных
системах. В обеспечении этих требований был некоторый период
"тяжелых времен" – в начале бума персональных компьютеров, что было
связано с внедрением в сферу полупрофессионального использования
вычислительной техники большого числа неподготовленных лиц с
задачами, ценность которых не стоила расходов по обеспечению их
безопасности. Но с неизбежным разделением сфер использования
333

компьютеров на персональную и производственную вопросы безопасности
в производственной сфере не только восстанавливают свои позиции, но и
приобретают все больший вес в связи с развитием компьютерных
коммуникаций. В настоящее время все сколько-нибудь серьезные
пользователи в производственной сфере готовы платить за гарантии
безопасности в своей работе – как материальными затратами, так и
некоторым снижением эффективности выполнения своих приложений.
Целостность

информации



обеспечение

непротиворечивости

данных. Правила целостности определяются законами прикладной
области, к которой относится информация, так называемыми, бизнесправилами.

Поскольку

ОС

является

универсальным

программным

обеспечением, предназначенным для поддержки выполнения процессов
обработки данных практически любых прикладных областей, поддержание
целостности прикладных данных не входит в ее функции. Целостность,
определяемая пользователем, поддерживается средствами промежуточного
программного обеспечения, наиболее развиты эти средства в системах
управления базами данных. Целостность же, за которую отвечает ОС – это
целостность общей структуры хранения информации, прежде всего она
касается файловой системы. Целостность файловой системы состоит в
соответствии метаданных файловой (дисковых и файловых справочников)
системы реальному ее состоянию. В задачи ОС входит также обеспечение
сохранности данных при ошибках. Ошибок не должно быть при
правильном функционировании всех звеньев вычислительной системы, но
такое функционирование относится к области идеалов. Источниками
ошибок являются ошибки в системном программном обеспечении, сбои
оборудования и неправильные (иногда – злонамеренные) действия
пользователей. "Первые – возможны, вторые – неизбежны, третьи –
гарантированы" [4] Применительно к данным на внешней памяти мы уже
рассматривали методы защиты целостности и сохранности в разделах
334

главы

7,

такие

дополнительные

методы

(избыточность,

указатели)

применимы

резервное
и

к

копирование,

другим

ресурсам

вычислительной системы.
Контроль доступа – обеспечение доступа к информации только тому,
кто имеет на это право. Этот аспект безопасности является основным
предметом рассмотрения в данной главе. В современных системах
обработки

данных

применяется

обязательное

или

избирательное

управление безопасностью.
Основными понятиями обязательного управления безопасностью
являются уровень секретности и уровень доступа. Каждый объект,
включенный в систему защиты, имеет некоторый уровень секретности
(например: совершенно секретно, секретно, для служебного пользования и
т.д.). Каждый пользователь, получающий доступ к защищенным ресурсам,
имеет некоторый уровень допуска. Число уровней допуска равно числу
уровней секретности. Если обозначить уровни секретности и уровни
допуска числовыми кодами таким образом, чтобы больший числовой код
соответствовал большей секретности или более высокому допуску, то
правила предоставления разрешений на доступ к ресурсам можно
сформулировать следующим образом:
• пользователь получает разрешение на чтение объекта только в том
случае, если его уровень допуска равен уровню секретности
объекта или больше него;
• пользователь получает разрешение на запись в объект или
модификацию объекта только в том случае, если его уровень
допуска равен уровню секретности объекта.
Другими словами второе правило можно сформулировать так: любая
информация, записанная пользователем с уровнем допуска L, получает
уровень секретности L. Таким образом, например, пользователь, имеющий
допуск к совершенно секретной информации, не может записать
335

информацию в открытый документ для служебного пользования (документ
с более низким уровнем секретности), так как это может нарушить
безопасность.
Избирательное управление безопасностью базируется на правах
доступа. В случае избирательного управления каждый пользователь
обладает определенными правами доступа к определенным ресурсам.
Права доступа разных пользователей к одному и тому же ресурсу могут
различаться. Избирательное управление часто базируется на принципе
владения. В этом случае у каждого ресурса имеется владелец, который
обладает всей полнотой прав доступа к ресурсу. Владелец определяет
права доступа к своему ресурсу других пользователей.
Министерством обороны США разработана общая классификация
уровней безопасности, которая применяется и во всем мире. Эта
классификация определяет четыре класса безопасности (в порядке
возрастания):
• D – минимальная защита;
• C – избирательная защита (с подклассами C1 и С2; C1 < C2);
• B – обязательная защита (с подклассами B1 < B2 < B3);
• A – проверенная защита (с математическим доказательством
адекватности).
На сегодняшний день некоторые компьютерные системы в бизнесе
удовлетворяют требованиям класса B1, однако большинство коммерческих
применений компьютерных систем ограничиваются требованиями класса
C2.
Основные требования класса C2 сводятся к следующим:
• владелец ресурса должен управлять доступом к ресурсу;

336

• ОС

должна

защищать

объекты

от

несанкционированного

использования другими процессами (в том числе и после их
удаления);
• перед получением доступа к системе каждый пользователь
должен идентифицировать себя, введя уникальное имя входа в
систему и пароль; система должна быть способной использовать
эту

уникальную

информацию

для

контроля

действий

пользователя;
• администратор системы должен иметь возможность контроля
(audit) связанных с безопасностью событий, доступ к этим
контрольным данным должен ограничиваться администратором;
• система должна защищать себя от внешнего вмешательства типа
модификации выполняющейся системы или хранимых файлов.
Сертификация на соответствие уровню безопасности – длительный
процесс, и ему подвергается не только программное обеспечение, а весь
комплекс средств системы обработки данных, включая аппаратные,
системные

и

прикладные

программные

средства,

каналы

связи,

организацию эксплуатации системы и т.д. Но операционная система может
оцениваться на предмет того, может или не может она поддерживать
функционирование

системы

обработки

данных,

соответствующей

определенному уровню.

10.2. Объектно-ориентированная модель доступа и
механизмы защиты

В

главе

7

мы

рассмотрели

модель

управления

доступом

применительно к файловой системе. Управление доступом к файлам
является частным случаем более общей модели управления доступом
337

субъектов к объектам. Другой частный случай мы рассмотрели в главе 3
применительно к памяти. Модель рассматривает объекты – элементы
системы, которым требуется защита, и субъекты – активные элементы,
стремящиеся получить доступ к объектам. Типичный пример объекта –
файл, типичный пример субъекта – процесс. Элемент, выступающий в
одной ситуации в роли субъекта, в другой ситуации может выступать в
роли объекта. Так, например, необходимо обеспечивать защиту адресных
пространств одних процессов (объектов) от других процессов (субъектов).
Механизмы защиты должны обеспечивать ограничение доступа
субъектов к объектам: во-первых, доступ к объекту должен быть разрешен
только для определенных субъектов, во-вторых, даже имеющему доступ
субъекту должно быть разрешено выполнение только определенного
набора операций.
Для обеспечения защиты могут применяться следующие механизмы:
• кодирование объектов;
• сокрытие местоположения объектов;
• инкапсуляция объектов.
Кодирование предполагает шифрование информации, составляющей
объект. Любой субъект может получить доступ к информации, но
воспользоваться ею может лишь привилегированный субъект, знающий
ключ к коду. Другой вариант защиты через кодирование предполагает, что
расшифровка информации производится системными средствами, но
только для привилегированных субъектов. Кодирование не защищает
объект от порчи, поэтому оно может использоваться только для защиты
узкого класса специфических объектов или в качестве дополнительного
средства в сочетании с другими механизмами защиты. Рассмотрение
способов кодирования не входит в задачи нашего пособия, оно должно
происходить при изучении курса "Защита информации".
338

Сокрытие местоположения объекта предполагает, что адрес объекта
в памяти системы известен только тем субъектам, которые имеют право
доступа к объекту. Такие привилегированные субъекты могут выполнять
любые операции над объектом. Непривилегированные субъекты могут
запрашивать

доступ

к

объекту

и

получать

некий

внутренний

идентификатор объекта. Используя этот идентификатор, они могут
выполнять

над

объектом

ограниченный

набор

операций,

но

не

непосредственно, а обращаясь к привилегированным субъектам. Примером
такого

механизма

является

сокрытие

дескрипторов

ресурсов.

Местоположение (адрес в памяти) дескрипторов известно ОС, прикладные
же процессы получают манипулятор ресурса, который, как правило,
адресует дескриптор только косвенно (через таблицы). Если сокрытие
является единственным механизмом защиты, то ее нельзя назвать
непроницаемой. Субъект может получить адрес объекта случайно или
намеренно (получив доступ к внутренней документации).
Инкапсуляция

предполагает

полное

закрытие

для

субъектов

возможности оперирования с внутренним содержимым объектов. Субъект
даже не должен знать структуры этого содержимого. Для каждого объекта,
однако, определено множество допустимых операций над ним, которые
реализуются монитором объекта. Объект, таким образом, предстает в виде
"черного ящика" с четко специфицированными входами и выходами и с
абсолютно непроницаемыми стенками. Механизм инкапсуляции может
обеспечивать наиболее полную защиту, но его реализация требует
решения

двух

важных

вопросов:

во-первых,

"непрозрачность

стенок";

во-вторых,

как

как

принимать

обеспечить
решения

о

предоставлении доступа или об отказе в нем.
Непроницаемость ящика обеспечивается чаще всего средствами
защиты памяти. Область памяти, в которой расположен объект, делается
недоступной для любых субъектов, кроме монитора, реализующего
339

операции над объектом. Как мы знаем (см. главу 3), защита памяти
поддерживается аппаратными средствами вычислительных систем и
защита объектов, таким образом, представляется реализованной на
аппаратном уровне. Надежность такой защиты, однако, в значительной
степени иллюзорна. Во-первых, память также представляет собой объект,
нуждающийся в защите. От того, насколько сама защита памяти защищена
от

перепрограммирования

ее

произвольным

субъектом,

зависит

эффективность всей системы защиты в целом. Во-вторых, аппаратно
поддерживается только конечное число уровней защиты памяти, на
практике же используются только два уровня: доступ к ограниченному
подмножеству адресов и полный доступ. Если в системе имеется большое
количество объектов разного типа, то каждый тип обеспечивается своим
монитором. Если все мониторы работают с полным доступом, то нет
гарантии в том, что монитор в результате, например, ошибки в нем не
воздействует на объект, не предусмотренный в данной операции.
Наиболее надежным образом защита памяти достигается при
помощи изоляции адресных пространств процессов и мониторов. Если
таблицы дескрипторов ресурсов и сами ресурсы располагаются в
отдельных адресных пространствах, то процесс просто не может получить
к ним доступа, а может воздействовать на них только косвенно, передавая
в системном вызове индекс элемента в недоступной для него таблице.
Адресные

пространства

различных

мониторов

тоже

могут

быть

изолированы друг от друга, что (при надежной изоляции) исключит
воздействие ошибки в мониторе на другие ресурсы.
Для каждого объекта определено множество допустимых для него
операций. Применительно к файлам, например, возможны в общем случае
следующие операции:
• Read – получение информации из объекта;
• Write – обновление информации в объекте;
340

• Append – добавление в объект новой информации (не изменяя
старой);
• Execute – интерпретация объекта как исполняемого кода;
• Delete – уничтожение объекта;
• Getinfo – получение информации об объекте;
• Setinfo – установка информации об объекте;
• Privilege – установка прав доступа к объекту (частный случай
Setinfo, по понятным причинам выделенный в отдельную
операцию).
Неизбыточный набор операций повышает надежность объекта,
лишая потенциального "взломщика" возможностей воздействовать на
объект. Так, например, для объекта "программа" могут отсутствовать
операции Read и Write. Действия всех компьютерных вирусов
заключаются в том, что они читают программный файл как файл данных и
дописывают

в

него

(как

в

файл

данных)

коды,

выполняющие

несанкционированные действия. Если программу невозможно читать и
писать, то у вируса просто нет возможности "заразить" ее своим кодом.
Перечень всех операций, допустимых для данной пары субъект–
объект составляет привилегию доступа (access privilege) или право доступа
(access right) данного субъекта к данному объекту.
Каждая

пара

субъект–объект

при

каждом

акте

доступа

взаимодействует в определенном режиме доступа (access mode). Условием
разрешения доступа является совпадение запрошенного субъектом режима
доступа с его привилегиями по отношению к запрошенному объекту.
Проведение политики контроля доступа включает в себя процедуры
аутентификации и авторизации, показанные на рисунке 10.1.

341

Рисунок 10.1 Процедуры аутентификации и авторизации

В соответствии с требованиями безопасности система должна
различать пользователей. Пользователь – это некоторое имя, определенное
в системе и связанное с некоторым субъектом, который может выполнять
доступ к ее ресурсам. Система должна "знать" своих пользователей,
следовательно, в системе должна быть некоторая база данных, хранящая
дескрипторы пользователей. Информацию, хранящуюся в дескрипторе
пользователя,

иногда

называют

профилем

(profile)

пользователя.

Различение пользователей (и, соответственно, ведение базы профилей)
может быть заложено в ядро ОС или выполняться утилитами. По тому
признаку, различает ли ядро ОС пользователей, мы и делим ОС на одноили многопользовательские. (Так, например, OS/2 на уровне ядра является
однопользовательской
обеспечение
утилиты

системой,

позволяет
могут

но

дополнительное

программное

вводить регистрацию пользователей.)

превратить

однопользовательскую
342

ОС

Хотя
в

многопользовательскую, возможности защиты ресурсов в той, ОС у
которой эти свойства заложены в ядро, принципиально большие.
Помимо имени и пароля, в профиль могут входить настройки
интерфейса рабочего места, процедура, автоматически выполняемая при
начале пользователем сеанса, установки библиотек и каталогов для поиска
и т.д. В профиль может также входить "бюджетная информация" (account)
– сведения о правах доступа и полномочиях пользователя. Даже в системах
защиты, ориентированных на списки контроля доступа, некоторые данные
бюджета все равно содержатся в профиле пользователя: его имя и пароль,
его полномочия, список групп, к которым он принадлежит. (Некоторые из
составляющих бюджета объясняются ниже.) Профили пользователей
должны быть одними из наиболее строго защищаемых объектов в системе.
При входе пользователя в систему или при запуске приложения от
его имени выполняется аутентификация (authentification). Эта процедура
состоит в представлении пользователя системе при установлении связи с
ней и подтверждении его подлинности. Подтверждение подлинности
выполняется паролем. При выполнении аутентификации процесс входа в
систему находит имя пользователя в базе профилей и сверяет введенный
пользователем пароль с паролем, хранящимся в профиле. Как правило,
пароль хранится в профиле пользователя в закодированном виде, причем
используется кодирование на основе хеширования, не допускающее
восстановление пароля по его хеш-коду. (Если пользователь забыл свой
пароль, то даже системный администратор не может сообщить ему его
пароль, а может только предоставить ему возможность назначить новый
пароль.) Поэтому введенный пользователем пароль кодируется по тому же
алгоритму и сравниваются уже хеш-коды паролей. При подтверждении
системой

легальности

пользователя

выбирается

идентификатор

безопасности для этого пользователя (косвенный указатель на бюджет
пользователя) и этот идентификатор включается в контексты всех
343

процессов,

создаваемых

пользователь

(или

данным

пользователем.

приложение),

не

Естественно,

прошедший

что

процедуру

аутентификации, к работе не допускается.
В системах обычно предусматривается возможность входа в систему
пользователя-гостя (guest). Такой пользователь имеет доступ только к
полностью открытым ресурсам и не имеет никаких полномочий.
После входа в систему пользователь (или работающее от его имени
приложение) может запрашивать доступ к ресурсам. Каждый случай
получения пользователем тех ресурсов, которые включены в число
защищаемых, сопровождается процедурой авторизации (authorization),
которая заключается в проверке того, может ли данный пользователь
выполнять запрошенное действие над данным объектом. В процессе
выполнения
связанная

авторизации
с объектом, и

привлекается

информация

безопасности,

информация безопасности, связанная

с

пользователем, и выносится решение о предоставлении или отклонении
доступа. В объектно-ориентированных системах процедура авторизации
может быть решена единообразно. Любая операция получения ресурса
(getResource)

должна

сопровождаться

единообразной

проверкой

полномочий и привилегий субъекта по отношению к данному объекту.

10.3. Представление прав доступа
Полный набор прав доступа для всех субъектов и всех объектов
может быть представлен в виде матрицы доступа, строки которой
соответствуют субъектам, а столбцы – объектам (или наоборот). На
пересечениях строк и столбцов указываются права доступа для данной
пары. Права доступа могут задаваться, например, в виде битовых строк с
позиционными кодами. На рисунке 10.2 представлен пример такой
матрицы.
344

Рисунок 10.2 Матрица доступа

Как видно даже из рисунка 10.2, в матрице присутствует
избыточность – пустые клетки, так как некоторые объекты недоступны для
некоторых субъектов. В многопользовательских системах же с большим
количеством как объектов, так и субъектов, во-первых, размер матрицы
может быть чрезвычайно большим, во-вторых, сама матрица наверняка
будет очень сильно разрежена. Поэтому матричное представление прав
доступа является неэффективным. Матрица может быть представлена либо
в виде списков привилегий (privileges list), либо в виде списков управления
доступом (rights ist). Списковые структуры позволяют экономить память за
счет исключения из матрицы позиций с пустыми значениями доступа.
В первом случае в системе существует таблица субъектов и с каждым
элементом этой таблицы связывается список объектов, к которым субъект
имеет доступ (рисунок 10.3).

Рисунок 10.3 Списки привилегий

345

Во втором случае имеется таблица объектов, с элементами которой
связаны списки субъектов (рисунок 10.4).

Рисунок 10.4 Списки управления доступом

10.4. Дополнительные возможности
Среди действий, которые может выполнять пользователь, есть и
такие, которые не связаны с конкретным объектом, а относятся к большой
группе объектов или даже ко всей системы в целом, например, выполнение
тех или иных команд, создание объектов определенного типа и т.п.
Возможность выполнять такие действия иногда называют полномочиями
(authority),

для

присваивания

и

учета

полномочий

существуют

специальные механизмы. Независимо от того, как представляется
информация о правах доступа субъектов к объектам, список полномочий
связывается

обязательно

возможности

связать

последовательно

с

его

профилем
с

реализующих

пользователя,

объектом.

Впрочем,

так
в

объектно-ориентированный

как

нет

системах,
подход,

полномочия, точнее – средства, через которые они реализуются (команды,
системные вызовы) сами

являются объектами, для доступа к ним

назначаются права, как и для других объектов, поэтому здесь нет
необходимости выделять их в отдельную категорию.
Для большинства задач управления доступом может оказаться
удобной интеграция некоторого подмножества объектов и связанных с
346

ними подмножеств возможностей в единый элемент, называемый доменом
(domain). Если несколько субъектов имеют доступ к одному и тому же
набору объектов, причем, с одинаковыми правами, то говорят, что
субъекты работают в одном домене. Домены представляют собой удобное
средство для сокращения представления информации о защите: в списке
возможностей, например, перечисление ряда объектов может быть
заменено идентификатором домена, который эти объекты составляют.
Домены могут быть пересекающимися. Субъект может работать в
нескольких

доменах

одновременно.

Концепция

доменов

широко

используется в современных ОС.
Различаются домены системные и специфицируемые. Системные
домены являются предустановленными и неизменяемыми. С системными
доменами

связываются

классы

пользователей:

принадлежность

пользователя к тому или иному классу разрешает пользователю работать в
том или ином домене. Обычно в системные домены входят объекты и
соответствующие

полномочия

позволяющие,

например,

выполнять

управление системой. Простейший пример пользовательских классов –
обычный пользователь и привилегированный (суперпользователь). Домен
привилегированного пользователя охватывает все объекты системы со
всеми правами доступа к ним. Предустановленный домен обычного
пользователя разрешает ему доступ к такому подмножеству объектов и
возможностей, оперируя которыми, он не может вывести из строя ОС или
повредить другим пользователям. Дополнительные права обычного
пользователя

обеспечиваются

специфицируемыми

доменами

и

персональными разрешениями.
Специфицируемые домены могут создаваться и конфигурироваться
динамически.

Во

всех

ОС

пользователи,

разделяющие

специфицированный домен, объединяются в группы. ОС поддерживает
список групп и с каждой группой связывает отдельный домен. В примере
347

раздела 7.7 мы видели, что Unix различает "группу, в которую входит
владелец файла". В более развитых системах защиты пользователь может
включаться в несколько групп, и домены групп могут пересекаться. Состав
пользователей в группе, состав и возможности домена группы могут
изменяться администратором или теми пользователями, которые имеют на
это право. Группам могут даваться также и полномочия. С точки зрения
представления информации о безопасности группа выступает как один
пользователь, и для каждой группы создается собственная запись в базе
профилей. Вместо того, чтобы в список прав включать отдельные записи
для всех членов какой-то группы, в список заносится единственная запись
для всей группы. Имеются обычно также и системные, предустановленные
группы – такие, как системные администраторы, операторы и т.п., но
состав системных групп может меняться динамически.
Объединение объектов составляет группу объектов, права доступа в
которой ко всем объектам одинаковы. Права могут быть разные для разных
пользователей и групп, но каждый пользователь имеет одни и те же
привилегии в отношении всех объектов группы. Так же, как и группы
пользователей, группы доступа позволяют сократить объем информации по
безопасности, представляя всю группу доступа как один объект.

Еще одним средством сокращения объема такой информации
являются

интегрированные

права,

сформированные

из

нескольких

элементарных прав, например, право данных – права читать и изменять
данные.
Среди стандартных прав может быть также право Exclude, явный
запрет на доступ к объекту. Записи о правах Exclude размещаются
первыми в списках информации о безопасности, таким образом, при
поиске они находятся в первую очередь.
В разделе 10.2 мы упоминали о возможности работы в системе
"гостя". "Гость" не может иметь никаких индивидуальных или групповых
348

прав, так как он не зарегистрирован в базе профилей, но некоторые
объекты в системе могут быть для него доступны. Говоря шире, в системе
могут быть объекты, доступные для всех. Права доступа "для всех"
называются публичными (public), они реализуются либо в специальной
группе

пользователей

PUBLIC

(гости

получают

идентификатор

безопасности этой группы), либо специальным разделом публичных прав в
списках прав объектов и групп доступа.
С учетом перечисленных компонентов системы безопасности
процедура авторизации выполняется в такой последовательности:
• поиск в индивидуальных правах пользователя;
• поиск в групповых правах пользователя;
• поиск в публичных правах.
Поиск прекращается, если на любом из этапов будут найдены права,
соответствующие запрошенному доступу, доступ разрешается. Поиск
прекращается, если на любом из этапов будет найдено право Exclude,
доступ не разрешается. Если права, соответствующие запрошенному
доступу, не найдены, доступ не разрешается. Индивидуальные права,
таким образом, имеют приоритет над групповыми, а групповые – над
публичными.
В ряде случаев пользователю может понадобиться выполнить какуюлибо корректную задачу, включающую в себя доступ к тем системным
объектам, к которым процесс права доступа не имеет. Для выполнения
этой задачи ОС предоставляет в распоряжение пользователя процесса
программы-утилиты, которые гарантируют корректность манипуляций с
защищенными объектами. Но если такая утилита будет выполняться в
контексте процесса пользователя, то есть с его идентификатором
безопасности, то в доступе ей будет отказано. Для решения этой проблемы
применяется так называемый адаптивный доступ (adopted access). В
349

дескрипторе исполняемого модуля – системной утилиты сохраняется
признак, по которому ОС на время выполнения (только не время
выполнения!) такого модуля предоставляет процессу, его вызвавшему, те
же права, которые имеет создатель утилиты (привилегированный
пользователь). Поскольку права предоставляются только на время
выполнения утилиты, пользователь, вызвавший ее, этими правами
воспользоваться не может.
Еще одно требование к системе безопасности – возможность
контроля связанных с безопасностью событий, таких как вход в систему,
попытки доступа к объектам (разрешенные или отклоненные), попытки
использования полномочий и т.д. Контроль заключается в том, что
информация о таких событиях сохраняется в системном журнале аудита и
может быть впоследствии проанализирована системным администратором.
Администратор имеет возможность задавать события и выбирать объекты,
подлежащие

контролю.

Такой

контроль

обеспечивается

списками

контроля доступа, которые подобны спискам прав. Элемент такого списка
для объекта содержит вид доступа, который подлежит протоколированию,
и

результат

авторизации.

После

любого

выполнения

процедуры

авторизации происходит поиск в списке контроля доступа, и если найден
элемент, в котором вид доступа и результат авторизации совпадают с
произошедшим, в журнал аудита заносится отметка о событии (включая
время и идентификатор пользователя).
Любые действия ОС по обеспечению безопасности (внутренние
действия) не дадут желаемого эффекта, если они не будут поддержаны
внешними

действиями:

выполняемых

системой

администраторами

организационных
системы,

мероприятий,

пользователями

и

их

руководителями. Рассмотрение внешних действий по обеспечению
безопасности не входит в наши задачи, упомянем только, что введение в
эту область можно найти в [12, 29].
350

КОНТРОЛЬНЫЕ ВОПРОСЫ
1. В

чем

различие

между

избирательным

и

обязательным

управлением доступом? Какой из этих подходов более надежен?
2. В

чем

преимущества

объектно-ориентированных

(объектно-

базированных) ОС с точки зрения защиты?
3. Назовите те операции доступа, которые должны быть общими для
любых типов объектов.
4. Назовите специфические операции доступа для объектов типа:
файл, папка, программа.
5. В чем состоят процессы аутентификации и авторизации?
6. В каком виде может храниться в системе информация о правах
доступа?
7. Что представляет собой "право" Exclude? Как оно применяется?
8. В чем состоит отличие полномочий от прав?
9. Опишите типовой сценарий процесса авторизации.
10. Чем объясняется необходимость введения классов пользователей?

Глава 11. Интерфейс пользователя
До сих пор мы рассматривали взаимодействие с ОС, выполняемое из
программы

при

помощи

вызовов

API



интерфейса

прикладного

программиста. Теперь рассмотрим взаимодействие вне программы – через
команды, вводимые с клавиатуры терминала в интерактивных системах или
поступающие во вводном потоке в пакетных системах. В первом случае, как
правило, новая команда вводится после выполнения предыдущей и сама
новая команда или ее параметры могут выбираться в зависимости от
результатов этого выполнения. Во втором случае задается сразу целая
последовательность команд и возможные отклонения от последовательного

351

их выполнения должны задаваться явным образом. Из таких различий в
технологии

взаимодействия

пользователей

с

системой

вытекают

естественные различия в интерактивных и пакетных командных языках, но
по мере расширения командных языков они имеют тенденцию к сближению:
в интерактивные командные языки включаются возможности задания
последовательностей команд, а в пакетные – более гибкие средства
управления последовательностью выполнения.

11.1. Командный язык и командный процессор
Команды представляют собой инструкции, сообщающие ОС, что
нужно делать. Команды могут восприниматься и выполняться либо
модулями ядра ОС, либо отдельным процессом, в последнем случае такой
процесс называется командным интерпретатором (оболочкой – shell).
Набор допустимых команд ОС и правил их записи образует командный
язык (CL – control language).
Большинство запросов пользователя к ОС состоят из двух
компонент, показывающих: какую операцию следует выполнить и в каком
окружении (environment) должно происходить выполнение операции.
Могут различаться внутренние и внешние операции-команды. Выполнение
внутренних операций производится самим командным интерпретатором,
выполнение внешних требует вызова программ-утилит. Наличие в составе
командного

языка

внутренних

команд

представляется

совершенно

необходимым для тех случаев, когда командный интерпретатор является
отдельным процессом. Среди команд имеются такие, в которых
выполняются

системные

вызовы,

изменяющие

состояние

самого

командного интерпретатора; если для них интерпретатор будет создавать
новые процессы, то изменяться будет состояние нового процесса, а не
интерпретатора. Примеры таких команд: chdir – изменение рабочего
каталога для интерпретатора, wait – интерпретатор должен ждать
352

завершения порожденного им процесса и т.п. Программы-утилиты (их
загрузочные модули) записаны в файлах на внешней памяти. При их
вызове порождаются процессы, и утилиты выполняются в контексте этих
процессов. Вызов и выполнение программ-утилит ничем не отличаются от
вызова и выполнения приложений. Командный интерпретатор порождает
процессы-потомки и выполняет в них заданные программы, используя для
этого те же самые системные вызовы, которые мы рассмотрели в главе 4.
Задание операции в командном языке, таким образом, может иметь вид
имени программного файла, который должен быть выполнен.
Окружением или средой (далее эти слова используются как
синонимы) называется то, что отличает одно выполнение программы от
другого. Например, при выполнении программы-компилятора должны
быть определены следующие параметры выполнения:
• какую программу следует выполнить;
• откуда программа должна взять исходные данные;
• куда программа должна поместить результат компиляции;
• где находятся библиотеки системы программирования;
• должен или не должен формироваться листинг;
• должны ли выдаваться предупреждения о возможных ошибках;
• и т.д.
Только первая из перечисленных составляющих задает саму
программу, остальные составляют окружение. Окружение конкретного
выполнения может формироваться одним из следующих способов или их
комбинациями:

• командами установки локального окружения;
• параметрами программы;
• командами установки глобального окружения.
Окружение может быть локальным или глобальным. В первом случае
параметры окружения устанавливаются только для данного конкретного
353

выполнения данной конкретной программы-процесса и теряются по
окончании

выполнения.

Во

втором

случае

параметры

окружения

сохраняются и действуют все время до их явной отмены или
переустановки.
Команды

для

установки

локальных

параметров

окружения

применяются обычно в системах, работающих в пакетном режиме. В таких
системах основным понятием является задание – единица работы с точки
зрения пользователя. Выполнение задания состоит из одного или
нескольких шагов. Каждый шаг задания включает в себя оператор JCL (job
control language – язык управления заданиями) вызова программы и
операторов установки локальной среды. Интерпретатор JCL сначала
вводит все операторы, относящиеся к шагу задания, и лишь затем их
выполняет. Тот же способ может применяться и в интерактивных системах
– в этом случае команды установки параметров локальной среды должны
предшествовать команде вызова.
Параметры программы также задают локальную среду выполнения.
Они являются дополнениями к команде вызова, вводятся в той же
командной строке, что и команда вызова, или являются параметрами
оператора вызова в JCL. Формы задания параметров можно, по-видимому,
свести к трем вариантам: позиционные, ключевые и флаговые. Возможны
также и их комбинации. Позиционная форма передачи параметров
программе похожа на общепринятую форму передачи параметров
процедурам:

параметры

передаются

в

виде

списка

значений,

интерпретация значения зависит от его места в списке. При передаче
параметров в ключевой форме задается обозначение (имя) параметра и его
значение; имя от значения отделяется специфицированным разделителем.
Иногда другой специфицированный разделитель ставится перед именем
как признак ключевой формы. Флаговая форма применяется для
параметров, которые могут иметь только логические значения типа
354

"да"/"нет"; такой параметр обычно кодируется одним определенным для
него

символом,

иногда

перед

ним

ставится

специфицированный

разделитель. Для флагового параметра информативным является само его
задание в команде вызова: если параметр не задан, применяется его
значение по умолчанию, если задан – противоположное значение.
Ниже приводятся примеры передачи параметров:
• позиционная форма:
pgm1 data1.dat data2.dat list
pgm2 10,33,p12.txt
• ключевая форма:
pgm1
fille1=data1.dat,file2=data2.dat,list=yes
pgm2 /bsize:10 /ssize:33 /name:p12.txt
• флаговая форма:
pgm1 /1 /2 /p
pgm2 s,m,l
• комбинированная форма:
pgm1 data1.dat data2.dat #bsize=10 #ssize=33
#l #f
Наиболее универсальным типом, который позволяет передать
программе любые параметры, является строка символов. Некоторые CL
требуют сформировать такую строку при вызове явным образом в виде
строковой константы с использованием соответствующих символовограничителей,
интерпретатором.

в

других

эта

Программа

задача

сама

выполняется

интерпретирует

командным
свою

строку

параметров, выполняя ее лексический разбор. В реальных системах
суммарный размер строки параметров обычно имеет некоторые разумные
ограничения.

Иногда

командный

интерпретатор

выполняет

предварительный лексический разбор строки параметров, выделяя из нее
355

"слова", разделенные пробелами, и передавая программе параметры в виде
массива строк переменной размерности.
Каким образом параметры могут быть переданы программе? Можно
назвать такие возможные механизмы передачи параметров:
• если командный интерпретатор выполняется как процесс, то он
может послать процессу-программе параметры в виде сообщения;
• если командный интерпретатор является ядром ОС, то он
копирует параметры в системную область памяти и программа
может получить их при помощи специального системного вызова;
• если командный интерпретатор участвует в порождении процессапрограммы (а это обычно так и бывает, независимо от того,
является интерпретатор модулем ядра или процессом), то
параметры могут быть записаны в адресное пространство нового
процесса сразу при его создании.
В языкахпрограммирования, однако, механизм передачи параметров
прозрачен

для

программиста,

доступ

к

параметрам

обеспечивает

компилятор и в любом случае программа получает значения параметров
уже в своем адресном пространстве. Так, в языке C для главной функции
программы предопределен прототип::
int main(int argn, char *argv[]);
где argn – число строк-параметров, argv – указатель на массив строкпараметров.
Для установки глобального окружения применяются команды типа
set. Операндами такой команды могут быть символьные строки, задающие в
ключевой форме значения параметров окружения. Например:

set tempdir=d:\util\tmp
set listmode=NO, BLKSIZE=1024
Переменные

окружения

могут

быть

системными

или

пользовательскими. Системные имеют зарезервированные символьные
356

имена и интерпретируются командным интерпретатором либо другими
системными

утилитами.

Например,

типичной

является

системная

переменная окружения path, значение которой задает список каталогов
для поиска программ командным интерпретатором. Пользовательские
переменные создаются, изменяются и интерпретируются пользователями и
приложениями. Чтобы окружение могло быть использовано, в системе
должны быть средства доступа к нему. На уровне команд это должна быть
команда типа show, выводящая на терминал имена и значения всех
переменных глобального окружения, на уровне API – системный вызов
getEvironment, возвращающий адрес блока глобального окружения.
Для внутреннего представления глобального окружения в ОС
возможны два варианта: либо хранить в системе единую таблицу со
значениями всех переменных окружения, либо для каждого процесса
создавать собственную копию такой таблицы. Чаще используется второй
вариант, который обеспечивает, во-первых, лучшую защиту глобального
окружения, а во-вторых, возможность варьировать окружения для разных
процессов, используя глобальное окружение отчасти как локальное. Очень
удобным является этот вариант для систем с иерархической структурой
отношений между процессами: в этом случае глобальное окружение
является частью наследства, передаваемого от предка к потомку.
Системные вызовы, связанные с порождением новых процессов, должны
обеспечивать возможность передавать потомку как точную копию
глобального окружения родителя, так и оригинальное окружение,
специально созданное родителем для потомка.
Внутреннее

представление

глобального

окружения



всегда

текстовое, представленное в ключевой форме, как в команде set. Это
объясняется

тем,

что

окружение

интерпретируется

прикладными

процессами, а именно текстовый тип может быть интерпретирован
наиболее гибким образом.
357

В ОС, применяющих "философию дешевых процессов" (см. главу 4),
предполагается выполнение сложных действий как результата совместной
(последовательной или параллельной) работы нескольких простых
процессов. Поэтому командный язык должен включать в себя средства
интеграции процессов. К числу таких средств относятся:
• командные списки;
• переадресация системного ввода-вывода;
• конвейеризация;
• параллельное выполнение.
Командные списки представляют собой простое перечисление в
одной

командной

строке

нескольких

команд.

Например,

результат

выполнения такой командной строки:

pgm1 param11 param12; pgm2; pgm3 param31
будет таким же, как при последовательным выполнении трех строк:
pgm1 param11 param12
pgm2
pgm3 param31
Командные списки представляют более удобную форму, но не
открывают никаких новых возможностей.
Переадресация системного ввода-вывода использует возможности,
описанные нами в разделе 7.6. Процессы вводят данные из файла
системного ввода, с которым по умолчанию связана клавиатура, а выводят
в файл системного вывода, по умолчанию – на экран терминала.
Переадресация ввода дает возможность использовать в качестве входных
данных программы данные, заранее записанные в файл, причем программа
вводит и интерпретирует эти данные как введенные с клавиатуры.
Переадресация вывода сохраняет данные, которые должны выводиться на
экран, в файле. Примеры:
pgm1 < infile
358

pgm2 > outfile
Соединение командного списка с переадресацией ввода-вывода
обеспечивает конвейеризацию. В примере:
pgm1 param11 param12 | pgm2 | pgm3 param31
выходные данные программы pgm1 направляются не на экран, а
сохраняются и затем используются, как входные для программы pgm2.
Выходные данные последней в свою очередь используются как входные
для pgm3.
В ОС с "философией дешевых процессов" допускается параллельное
выполнение любого количества процессов. В обычном режиме командный
интерпретатор готов к приему следующей команды только после
окончания выполнения предыдущей. Специальная команда запуска
программы или какой-либо признак в командной строке (например,
символ-амперсанд в конце ее) может применяться в качестве указания
командному процессору вводить и выполнять следующую команду, не
дожидаясь окончания выполнения предыдущей. Так, последовательность
командных строк:
pgm1 param11 param12 &
pgm2 &
pgm3 param31
может привести к параллельному выполнению трех процессов (если
программа pgm1 не закончится прежде, чем будет введена третья строка).
Ниже приведен программный текст, представляющий в значительно
упрощенном виде макет командного интерпретатора shell ОС Unix
(синтаксис

и

семантика

системных

вызовов

в

основном

тоже

соответствуют этой ОС). Из этого примера можно судить о том, как shell
реализует некоторые из описанных выше возможностей.

359

. . .
1

int fd, fds [2];

2

int status, retid, numchars=256;

3

char buffer [numchars], outfile[80];

4

. . .

5

while( read(stdin,buffer,numchars) ) {

6



7

if () {

8

if() amp=1;

9

else amp=0;

10

if( fork() == 0 ) {

11

if( < переадресация вывода > ) {

12

fd = create(outfile,);

13

close(stdout);

14

dup(fd);

15

close(fd);

16

}

17

if() {


18
19

}

20

if() {

21

pipe (fds);

22

if ( fork() == 0 ) {

23

close(stdin);

24

dup(fds[0]);

25

close(fds[0]);

26

close(fds[1]);

27

exec(pgm2, );

28

}
360

29

else {

30

close(stdout);

31

dup(fds[1]);

32

close(ds[1]);

33

close(fds[0]);

34

}

35

}

36

exec(pgm1, );

37

}

38

if ( amp == 0 ) retid = wait(&status);

39
40

}
}
. . .

Наш макет рассчитан на интерпретацию командной строки,
содержащей либо вызов одной программы (pgm1), либо конвейер из двух
программ (pgm1|pgm2). Макет также обрабатывает переадресацию вводавывода и параллельное выполнение.
Тело shell представляет собой цикл (5 - 39), в каждой итерации
которого из файла стандартного ввода вводится (5) строка символов –
командная строка. Далее shell выполняет разбор командной строки,
выделяя и распознавая собственно команду (или команды), параметры и
т.д. Если (7) распознанная команда не является внутренней командой shell
(обработку внутренних команд мы не рассматриваем), а требует
выполнения какой-то программы – безразлично, системной утилиты или
приложения, – то shell проверяет наличие в командной строке признака
параллельности и соответственно устанавливает значение флага amp (8, 9).
Затем shell порождает новый процесс (10) и весь следующий блок (11 - 37)
выполняется только в процессе-потомке. Если shell распознал в команде
361

переадресацию системного вывода (11), то выполняются соответствующие
действия (11 - 16). Они состоят в том, что shell создает файл, в который
будет перенаправлен поток вывода и получает его манипулятор (12). Затем
закрывается файл системного вывода (13). Системный вызов dup (14)
дублирует манипулятор fd в первый свободный манипулятор таблицы
файлов процесса. Поскольку только что освободился файл системного
вывода, манипулятор которого – 1, дублирование произойдет именно в
него. Теперь два элемента таблицы файлов процесса – элемент с номером 1
и элемент с номером fd – адресуют один и тот же файловый дескриптор –
дескриптор только что созданного файла. Но элемент fd сразу же
освобождается

(15),

и

теперь

только

манипулятор

1,

который

интерпретируется в программе как манипулятор системного вывода,
адресует

новый

файл.

Мы

предлагаем

читателю

самостоятельно

запрограммировать действия shell при переадресации ввода (17 - 19). (Для
справки: манипулятор системного ввода – 0.)
Если в командной строке задан конвейер (20), то процесс-потомок
прежде всего создает канал (21). Параметром системного вызова pipe
является массив из двух элементов, в который этот вызов помещает
манипуляторы канала: fds[0] – для чтения и fds[1] – для записи. Затем
процесс-потомок порождает еще один процесс (22). Следующий блок (23 27) выполняется только во втором процессе-потомке. Этот потомок
переадресует свой стандартный ввод на манипулятор чтения канала (23 25). Манипулятор записи канала освобождается за ненадобностью (26).
Затем второй потомок загружается программой, являющейся вторым
компонентом конвейера (27). Следующий блок (28 - 34) выполняется
только в первом потомке: он переадресует свой стандартный вывод на
манипулятор записи канала и освобождает манипулятор чтения канала.

362

Системный вызов exec (36) выполняется в первом потомке (он
является единственным, если конвейер не задан), процесс загружается
программой,

являющейся

первым

компонентом

конвейера

или

единственным компонентом командной строки. Обратите внимание на то,
что из функции exec нет возврата. При ее вызове выполняется новая
программа, с завершением которой завершается и процесс. Процессродитель, который все время сохраняет контекст интерпретатора shell,
после запуска потомка проверяет (38) флаг параллельного выполнения
amp. Если этот флаг не установлен, то родитель выполняет вызов wait,
блокирующий shell до завершения потомка. (Этот вызов возвращает
идентификатор закончившегося процесса, а в параметре – код завершения,
но в нашем макете эти результаты не обрабатываются). Если флаг
параллельности установлен, то shell начинает следующую свою итерацию,
не дожидаясь завершения потомка.
При разработке командного интерпретатора необходимо следовать
"принципу пользователя", который может быть сформулирован так: те
операции, которые часто выполняются, должны легко вызываться. Для
более полного воплощения этого принципа в командный интерпретатор
могут быть встроены сервисные возможности. Можно привести такие
примеры этих возможностей:

• установки по умолчанию – относятся, прежде всего, к параметрам
глобального окружения, обычно эти установки записываются в
отдельный файл – командный файл (cм. следующий раздел)
начальной загрузки типа AUTOEXEC.BAT или в пользовательский
профиль;
• встроенные

сокращенные

формы

команд;

в

некоторых

интерпретаторах применяется метод автоматического завершения:
пользователь,

введя

первые

363

символы

команды,

нажимает

определенную клавишу, и интерпретатор выводит в командную
строку остаток команды;
• интеграция команд – введение в состав CL сложных команд,
эквивалентных цепочке простых команд, иногда пользователь
имеет возможность создавать в командном интерпретаторе
собственные

интегрированные

команды,

но

чаще

такая

возможность реализуется через командные файлы;
• сохранение истории – введенные командные строки запоминаются
в стеке, и по определенной клавише содержимое стека выбирается
в командную строку.
В тех ОС, где командный интерпретатор является процессом,
имеется

возможность

запуска

вторичного

интерпретатора.

Эта

возможность является очень удобной, если при работе в среде какого-либо
приложения возникает необходимость выполнить команды ОС, но
завершать приложение из-за этого нежелательно. Приложение является
процессом-потомком командного интерпретатора. Оно порождает новый
процесс (потомок приложения), в котором запускается еще одна копия
интерпретатора, как показано на рисунке 11.1. (Новый процессинтерпретатор

может

разделять

сегмент

кодов

с

первичным

интерпретатором). Теперь весь ввод в командную строку обрабатывается
этим вторичным интерпретатором. Вторичный интерпретатор может
запустить другое приложение и т.д. Вторичный интерпретатор запускается
в синхронном режиме и при его завершении (команда exit) продолжает
работать запускавшее его приложение.

364

Рисунок 11.1 Запуск вторичного командного интерпретатора

11.2. Командные файлы и язык процедур
Принцип пользователя диктует необходимость короткого обращения
к

часто

выполняемым

эффективным

решением

последовательностям
этой

задачи

команд.

является

Простым

запись

и

такой

последовательности в текстовый файл и обращение к ней в дальнейшем по
имени файла. Мы будем называть такие файлы командными. В
интерактивных системах их иногда также называют пакетными файлами
(batch file), а в пакетных – файлами процедур JCL. Командный
интерпретатор должен при вводе команды-обращения к такому файлу
распознать тип файла (иногда признак обращения к командному файлу
требуется указать в самой команде) и далее считывать и интерпретировать
команды из файла.
В простейшем случае командный файл содержит неизменяемую
последовательность команд и является просто аббревиатурой этой
последовательности.

Но

сервис,

обеспечиваемый

инвариантными

последовательностями, явно недостаточен. Например, для программиста
типичным является "сценарий" работы, состоящий из многократного
повторения таких шагов:
• редактирование исходного модуля;
365

• компиляция;
• компоновка;
• выполнение.
Каждый шаг связан с вызовом новой программы, следовательно, с
новой командой. Очевидно, что при записи такого сценария в командный
файл мы должны обеспечить для него хотя бы один параметр – имя
исходного

модуля.

Параметры

являются

совершенно

необходимым

свойством для командных файлов. Параметры нетрудно обрабатывать
простой текстовой подстановкой.

Очевидно также, что в том же сценарии не имеет смысла
компоновать, а тем более выполнять программу, если при компиляции в
ней

обнаружены

ошибки.

последовательностью

Отсюда

выполнения

необходимость



команд

в

управлять

командном

сценарии.

Простейшим вариантом такого управления является включение в команду
условия ее выполнения, более сложный и гибкий вариант – условный
переход на ту или иную команду. В условии выполнения или перехода
должен

анализироваться

код

завершения

одной

или

нескольких

предыдущих команд. Общепринятым является успешное завершение
программ и команд с кодом 0. Код завершения может формироваться
программой, выполняющей команду, как параметр системного вызова
exit

и

восприниматься

процессом-родителем

(командным

интерпретатором) в системном вызове wait.
Параметры и условия являются тем набором средств, который
обеспечивает минимальный необходимый сервис. Некоторые ОС на этом и
останавливаются. Другие же идут дальше. Пионером в этой области (как и
во многих других) является ОС Unix. Это неудивительно, так как Unix
воплощает

"философию

дешевых

процессов",

а

для

того,

чтобы

скомпоновать из "дешевых" процессов сложные действия, нужны развитые
средства

интеграции.

Просматривая

публикации

по

ОС

Unix

в

хронологическом порядке, можно наблюдать, как языковые средства shell
наращивались новыми алгоритмическими возможностями, все более

366

приближая его к процедурному языку программирования. В итоге shell
обладает полным набором средств процедурного программирования
(операций и операторов), включая манипулирование с переменными shellпрограммы,

условный

оператор,

оператор

множественного

выбора,

операторы циклов, оператор обработки исключительных ситуаций, а также
возможность создания и вызова функций.

По иному пути пошла фирма IBM, в середине 80-х годов
представившая в составе ОС CMS (гостевой ОС в среде ОС виртуальных
машин VM/370) реструктурированный расширенный язык процедур –
REXX (Restructured EXtended eXecutor language) [46]. Разработчики этого
языка

пошли

не

по

пути

наращивания

командного

языка

алгоритмическими возможностями, а по пути включения в мощный
алгоритмический язык (за основу был взят язык PL/1) средств выполнения
команд. Подход оказался настолько продуктивным, что за прошедшее с
тех пор время REXX практически не претерпел изменений и сейчас входит
в базовый комплект поставки не только CMS VM/ESA, но всех ОС фирмы.
Наряду

со

средствами

процедурного

программирования,

"унаследованными" от PL/1, в REXX включен в качестве базовых
операций языка ряд операций расширенной обработки строк и большое
количество встроенных функций, также прежде всего связанных с
обработкой строк, которыми компенсируется отсутствие того богатого
набора утилит, который имеется в Unix. Кроме того, в REXX имеются
возможности (эти возможности системно-зависимые) работы с текстовыми
файлами и обмена данными через очереди или перенаправление вводавывода не только в файлы, но и в буферы, подобные программным
каналам, но со структурой дека (очереди с двумя концами). Вообще
область применения REXX шире, чем только применение его в качестве
командного интерпретатора ОС. Целый ряд продуктов системного и
промежуточного программного обеспечения IBM использует REXX как
367

интерпретатор своих команд. Оператор ADDRESS задает имя программысреды, в которую передается команда.
Оба типа развитых командных языков наряду с одинаковыми
алгоритмическими

возможностями

обладают

также

еще

одним

принципиально важным общим свойством – они являются языками
интерпретирующего типа. Командный файл REXX или sell не требует
компиляции. Эта означает, что полный анализ такого файла не
производится (или производится только в первом приближении), и
интерпретатор выполняет его команда за командой, "не заглядывая"
вперед. Переменные командного файла имеют единственный тип – "строка
символов", и основные манипуляции над ними представляют собой
строковые операции. При выполнении арифметики строковые данные
прозрачно преобразуются в числовые, а результат операции вновь
преобразуется в строку. Результаты выполнения программ, вызываемых в
командном файле. При выполнении каждого очередного оператора
командного файла производится подстановка вместо переменных shellили REXX-программы их значений. В обоих языках предусмотрены
средства

"экранирования",

защищающие

строковые

литералы

от

интерпретации их как переменных. Строка, полученная после выполнения
подстановки, интерпретируется как оператор командного языка или – если
это невозможно – как команда ОС (или другой целевой среды). В REXX
имеется

возможность

даже

сформировать

символьную

строку

в

переменной REXX-программы, а затем выполнить ее как оператор языка.
Таким

образом,

командные

языки

ОС

обладают

всеми

возможностями языков программирования, и, в принципе, пригодны для
создания не только командных процедур, но и некоторых программ
обработки

данных.

Выполнение

командных

файлов

в

режиме

интерпретации, конечно, делает такие программы менее эффективными,
чем программы, написанные на языках компилирующего типа, но создает
368

в них некоторые дополнительные возможности и вырабатывает некоторый
особый стиль программирования.

11.3. Проблема идентификации адресата
При параллельном выполнении нескольких процессов возникает
проблема взаимодействия пользователя с ними: при появлении на экране
сообщения, как определит пользователь, какой из процессов это
сообщение выдал; при вводе сообщения пользователем, как ОС определит,
какому процессу сообщение адресовано? Решение этой проблемы зависит,
прежде всего, от степени интерактивности процессов (насколько часто они
обмениваются сообщениями с пользователем). Если процессы не очень
интерактивны, то им можно разрешить квазиодновременный вывод на
терминал: их сообщения могут чередоваться, но каждое сообщение
должно быть неделимым. В этом случае выводимое сообщение должно
нести в себе и идентификацию процесса, его выдавшего. Дейкстра [11]
подробно рассмотрел реализацию диалогового взаимодействия такого рода
с помощью семафоров. Им показано, что для этого случая ответы
пользователя должны либо также содержать идентификатор процессаадресата, либо

пара действий

вопрос–ответ должна

быть одной

транзакцией. В последнем случае, однако, задержка ответа надолго
блокирует общий канал обмена между процессами и пользователем,
поэтому у пользователя также должна быть возможность откладывания
ответа с последующим возвращением к диалогу с этим же процессом.
Подход, предполагающий явное адресование сообщений, становится
излишне

громоздким,

если

пользователь

работает

с

существенно

интерактивными процессами. Общая идея обеспечения взаимодействия
для такого случая состоит в том, что в каждый момент времени только
одному

процессу

(по

выбору

пользователя)
369

разрешается

быть

интерактивным (foreground – на переднем плане), т.е. иметь доступ к
терминалу. Остальные процессы (background – фоновые) выполняются без
доступа к терминалу. В распоряжении пользователя имеются средства
изменения статуса процесса – перемещения его с заднего плана на
передний (текущий процесс переднего плана при этом отодвигается на
задний план).
Что

делать

с

фоновым

процессом,

если

у

него

возникла

необходимость выдать сообщение на терминал? Одним из решений может
быть блокировка такого процесса до его выдвижения. Более эффективным
является "скрытый" вывод, при котором обеспечивается для выполнения
процессов несколько параллельных сеансов. В каждом сеансе могут
выполняться один или несколько процессов. С каждым сеансом связан
собственный

буфер

логического

терминала,

включающий

в

себя

логический видеобуфер, логический буфер клавиатуры и логический
буфер мыши. Весь обмен процесс ведет с логическим видеобуфером
терминала. В каждый момент логический буфер только одного (так
называемого

активного)

сеанса

связан

с

физическими

буферами

терминальных устройств. Таким образом, фоновый сеанс может вести
обмен с терминалом (в основном – вывод) без отображения на терминал.
Когда сеанс становится активным, его логический буфер связывается с
физическими устройствами, и на экране видеотерминала отображается
содержимое его логического видеобуфера. В составе API есть также
средства аварийной активизации сеанса: если процессу, работающему в
фоновом сеансе, необходимо выдать срочное сообщение, то он может
воспользоваться
существования

так

называемым

иллюминатора

ОС

"иллюминатором".
выделяет

временный

На

время

активный

видеобуфер, и все средства обмена с консолью работают с этим буфером.
Это очень сильное средство, используемое только в исключительных
случаях, так как на время существования иллюминатора блокируются
370

запросы активного сеанса, а переключать сеансы в это время не может
даже оператор.

11.4. WIMP-интерфейс
Интерфейс командной строки (но не командные файлы!) на
сегодняшний день уже можно считать отходящим в прошлое, хотя
прогнозировать

его

Программируемые

окончательный

видеотерминалы

уход
дают

мы

не

беремся.

возможность

выводить

информацию в любую позицию экрана и, следовательно, использовать все
пространство экрана для организации взаимодействия между ОС и
пользователем. Современные интерфейсы как приложений, так и ОС,
можно охарактеризовать как полноэкранные, графические, объектноориентированные.
Полноэкранный

интерфейс

строится

на

основе

принципа

согласованности, который состоит в том, что у пользователя формируется
система

ожидания одинаковых реакций на одинаковые

действия.

Основными компонентами интерфейса являются панели, диалог и окна.
Панель – это информация, определенным образом сгруппированная и
расположенная на экране. Основные типы панелей:
• меню



содержит

один

или

более

списков

объектов,

представляющих группы действий, доступных пользователю;
• панель ввода – отображает поля, в которые пользователь вводит
информацию;
• информационная панель – отображает защищенную информацию
(данные, сообщения, справки);

371

• списковая панель – содержит один или более списков объектов, из
которых

пользователь

выбирает

один

или

несколько

и

запрашивает одно или несколько действий над ними;
• панель-канва – свободное пространство, на котором можно
размещать другие объекты интерфейса, такие как:
• кнопки различных видов и различной функциональности;
• элементы выбора;
• статические текстовые поля;
• протяжки и т.д.
Диалог – это последовательность запросов между пользователем и
компьютером:

запрос

пользователем

действия,

реакция

и

запрос

компьютера, ответное действие пользователя и т.д. Диалог включает в себя
запросы и навигацию – переходы из одной панели в другую. Информация,
вводимая пользователем в ходе диалога, может удерживаться только на
уровне данной панели или сохраняться.
Панели могут располагаться в отдельных ограниченных частях
экрана, называемых окнами. Окна могут быть трех типов:
• первичное – окно, в котором начинается диалог;
• вторичные – вызываемые из первичного окна, в которых диалог
ведется параллельно диалогу в первичном окне;
• всплывающие (pop-up) – расширяющие диалог в первичном или
вторичном окне; перед тем, как продолжить диалог с окном
пользователь должен завершить работу со связанным с ним
всплывающим окном.
Общие принципы панельного интерфейса в основном не зависят от
типа

применяемых

видеоадаптеров

с

терминалов.
высокой

Однако

разрешающей

сочетание

графических

способностью

с

общим

увеличением вычислительной мощности (быстродействие и объем памяти)
372

персональных вычислительных систем позволяет существенно изменить
общий

облик

экрана.

Можно

определить

следующие

основные

направления этих изменений: многооконность, объемность, иконика.
Такие интерфейсы получили название WIMP (Windows, Icons, Menus,
Pointer). Первое воплощение идеи WIMP нашли в разработках фирмы
Xerox, а первая их коммерчески успешная реализация состоялась в
компьютерах Apple Macintosh в 1985 году. Позднее идеи WIMP были
приняты в Microsoft Windows, а сейчас они воплощены практически во
всех операционных системах.
Интерфейс

WIMP

обладает

концептуальной

целостностью,

достигаемой принятием знакомой идеальной модели – метафоры рабочего
стола – ровной поверхности, на которой расположены объекты и папки, и
ее

тщательного

последовательного

развития

для

использования

воплощения в компьютерной графике. Главное изменение в облике
интерфейса – иконика – представление объектов в виде миниатюрных
графических изображений – пиктограмм. Помимо чисто внешних
изменений иконика породила возможность манипулировать объектами
через манипулирование их изображениями. Значки и вложенные папки и
мусорная корзина являются точными аналогами документов на столе.
Вырезание, копирование и вставка точно имитируют операции, которые
обычно осуществляются с документами на столе. Транспортировка
непосредственно вытекает из метафоры рабочего стола; выбор значков или
окон с помощью курсора является прямой аналогией захвата предметов
рукой. Из метафоры рабочего стола непосредственно следует решение о
перекрытии окон вместо расположения их одно рядом с другим.
Представление активного окна как документа, "лежащего сверху",
интуитивно понятным образом решает проблему идентификации адресата.
Возможность менять размер и форму окон не имеет прямой аналогии с
бумажными документами, но является последовательным расширением,
373

дающим

пользователю

новые

возможности,

обеспечиваемые

компьютерной графикой
В некоторых случаях интерфейс WIMP отходит от метафоры
рабочего стола. Основные отличия: меню и работа одной рукой. Меню
представляет

собой

не

совершение

действия,

а

выдачу

кому-то

(системе/приложению) команды на осуществление действия, причем,
команда эта не формулируется языковыми средствами, а выбирается из
списка.
Даже на чисто текстовых видеотерминалах имелась возможность
вывода на экран нескольких окон одновременно, но для графического
режима эта возможность значительно расширилась. Поскольку появление
графического интерфейса в Mac OS и Windows совпало с введением
многозадачности (сначала – без вытеснения), естественным образом
возникло решение о выделении каждому из работающих приложений
собственного окна (первичной панели). При одновременной работе
нескольких приложений их окна могут перекрывать друг друга – частично
или полностью, но на переднем плане всегда находится окно активного в
данный момент приложения. Поскольку обилие окон может затруднить
ориентацию пользователя, вводится возможность минимизации или
сокрытия окон: окна неактивных приложений могут уменьшаться в
размерах или вообще не выводиться на экран. Для предотвращения
"потерь" скрытых окон у пользователя должна быть возможность в любой
момент просмотреть список работающих приложений и восстановить
нормальную визуализацию выбранных окон.
Высокая

разрешающая

способность

графических

дисплеев

позволяет также имитировать объемные панели, создавая на плоском
экране иллюзию светотеней. На "объемной" панели применяются
графические элементы – органы управления, такие как: кнопки, линейка
протяжки и т.д. Общепринятым является представление полей ввода в
374

"утопленном" виде,

а органов управления –

в

"приподнятом". К

настоящему времени облик объемного интерфейса в современных ОС
сформировался почти окончательно и включает в себя единый "источник
света" и однотипное расположение органов управления на всех панелях.
Объектно-ориентированные

свойства

интерфейса

совершенно

необязательно связаны с объектно-ориентированной структурой ОС. Так,
например,

OS/400

является

объектно-ориентированной

системой

с

объектно-ориентированным интерфейсом, Windows NT v.3.51 была
объектно-ориентированной

ОС

без

объектно-ориентированного

интерфейса, OS/2 и Windows 9x – не объектно-ориентированные ОС с
объектно-ориентированными интерфейсами. Объектно-ориентированный
интерфейс обычно связывают с графическим интерфейсом, но это
необязательно. Так, в той же OS/400 предусмотрены две модели
интерфейса: текстовая и графическая, обе в полной мере объектноориентированные.
В

противовес

обычному

интерфейсу,

который

представляет

пользователю практически единственный тип объекта – файл, единицу
хранения информации в ОС, объектно-ориентированный интерфейс
представляет объекты различных типов. Файлы могут быть разными
типами объектов – в зависимости от типа информации, в них хранящейся,
и способов ее обработки. Кроме того, объектами могут быть устройства,
сетевые ресурсы и т.д. В объектно-ориентированном программировании
под объектом понимается абстрактный тип данных, включающий в себя
как сами данные, так и процедуры их обработки. Аналогично объекты
понимаются

и

в

объектно-ориентированном

интерфейсе.

Объект

обязательно обладает некоторым набором свойств, и значения этих
свойств доступно пользователю. Среди свойств, присущих объекту,
имеется и указание на способ его обработки – в том числе и на
приложение, обрабатывающее данные этого типа. Выполнение некоторых
375

действий над объектом включает в себя автоматический запуск
приложений, которые эти действия выполняют.
Концептуально важным объектом интерфейса является папка (folder).
Папка – это контейнерный объект, содержащий в себе другие объекты и
папки.

Уместность

папки

в

метафоре

рабочего

стола

очевидна.

Существенно то, что папка дает возможность пользователю создавать
собственную структуру хранения объектов, альтернативную структуре
хранения объектов в ОС (в файловой

системе). Важным свойством,

обеспечивающим эту возможность, является создание указателей на
объекты. Если папка является физическим аналогом каталога файловой
системы, то в нее может быть помещен указатель на объект, физически
расположенный в другой папке-каталоге файловой системы (аналог
косвенных файлов). Ссылка на объект с точки зрения пользователя
выглядит так же, как оригинал объекта (хотя может иметь какие-то
отличительные признаки ссылки), выполнение операции открытия над
ссылкой

приводит

к

открытию

объекта-оригинала,

но

операции

перемещения, удаления, переименования и т.п. выполняются не над
объектом, а только над ссылкой.

Возникает, однако, проблема

согласования интерфейсной структуры хранения объектов с логической
структурой

файловой

системы.

Например,

требуется,

чтобы

при

перемещении объекта-оригинала в файловой системе все ссылки на него
перенаправлялись на новое его место. Не все интерфейсы ОС успешно
справляются с этой задачей.
Важным аспектом объектной ориентации является настройка
интерфейса для конкретного пользователя. Обычно, если интерфейс
рассматривается с точки зрения приложений, отмечается полезность
создания нескольких форм интерфейса, ориентированных на пользователя
разной квалификации – новичка, опытного, профессионала. Хотя та же
задача может ставиться и перед интерфейсом ОС, более важной, на наш
376

взгляд является интеграция интерфейса с системой безопасности ОС.
Интерфейс должен показывать пользователю только те объекты и
предоставлять ему только те команды, к которым данный пользователь
имеет доступ. Такое возможно в тех ОС, где система безопасности тесно
связана

с объектно-ориентированными

свойствами ОС. Настройки

интерфейса могут являться частью профиля пользователя.
Каково место интерфейса WIMP в ОС? Можно назвать три подхода к
выбору такого места.
Графический интерфейс может встраиваться в саму ОС и быть ее
неотъемлемой частью. Такой подход применяется во всех продуктах
семейства Windows и в ОС компьютеров Apple (в последних WIMP даже
встроен в ПЗУ компьютера). Это дает возможность тесно интегрировать
интерфейс с ОС и повысить производительность интерфейсных модулей,
выполняя часть из них в режиме ядра. Однако такой подход в то же время
является неэкономным, так как интерфейс WIMP расходует много
ресурсов и до некоторой степени опасным, так как модули WIMP могут
явиться дополнительным источником ошибок в системе.
Графический интерфейс может представлять собой отдельное
приложение, поставляемое в составе операционной системы и, возможно,
достаточно тесно интегрированное с ней. Пример такого приложения –
Workplace Shell OS/2. Такое приложение не допускается в режим ядра, но
может использовать API более низкого уровня, чем обычно используемый
в приложениях. Такое приложение WIMP не является обязательным
компонентом ОС, система может работать и без него, в режиме командной
строки или загрузить другое приложение WIMP.
Наконец,

графический

интерфейс

может

представлять

собой

приложение, никак не связанное с ОС, выполняющееся в тех же условиях,
что и другие приложения, и выполняющее действия, задаваемые
пользователем, используя обычный API ОС. В этом случае ОС не связана
377

жестко с одним модулем WIMP, и графический интерфейс может
выбираться по желанию пользователя. Примером такой ОС с большим
выбором интерфейсов является Linux.
Нам представляется, что второй и третий подходы, дающие
пользователю возможность выбора, являются предпочтительными.
Принцип согласованности интерфейса диктует необходимость для
всех разработчиков приложений обеспечивать однотипный интерфейс в
разных приложениях. Естественным решением является возможность для
разработчиков приложений использовать те же модули и объекты, которые
используются

для

встроенного

в

ОС

обеспечивающие
пользователей

построения

графического

интерфейса

интерфейсные функции,

через

ОС.

WIMP-интерфейса

соответствующий

В

системные

случае
объекты,

делаются доступными для
API

(Windows).

В

случае

интерфейса, представляющего собой интегрированное с ОС приложение,
библиотека интерфейсных функций и объектов поставляется в составе ОС
(Object Class Library в OS/2). Основой независимых графических
интерфейсов являются независимые инструментальные средства, на
основе которых может быть построен тот или иной WIMP-интерфейс.
Одной из наиболее успешных систем для построения таких
интерфейсов

является

X

Window,

созданная

в

Массачусетском

Технологическом Институте. Архитектура X Window построена по
принципу

клиент/сервер.

Взаимодействие

X-клиента

и

X-сервера

происходит в рамках прикладного уровня – X-протокола. Для X Window
безразличен транспортный уровень передачи, таким образом, X-клиент и
X-сервер могут располагаться на разных компьютерах, в разных
аппаратных

и

операционных

средах,

то

есть

программа

может

осуществлять ввод-вывод графической информации на экран другого
компьютера. Все различия в аппаратных и программных архитектурах Xклиента и X-сервера сглаживаются стандартом X-протокола. На базе
378

инструментальных средств X Window было создано несколько сред WIMPинтерфейсов, наиболее популярный из которых, по-видимому, Motif,
являющийся стандартом Open Software Foundation.
По-видимому,

WIMP-интерфейс

не

является

окончательным

решением, по мере развития аппаратных и программных средств
обработки данных он, вполне вероятно, будет вытеснен новыми подходами
и новыми средствами. Скорее всего на смену метафоре рабочего стола
придет какая-то новая модель, обладающая собственной концептуальной
целостностью. Сегодня представляется вероятным, что новая модель будет
найдена

где-то в области средств мультимедиа, которые

сейчас

развиваются весьма стремительно. Возможно, это будет голосовой ввод,
возможно, что-нибудь другое. Однако, это будет достоянием уже
следующего поколения. Пока, во всяком случае, мы не можем назвать
какой-либо

новой

модели,

обладающей

такой

же

понятийной

целостностью, как концепция рабочего стола.

КОНТРОЛЬНЫЕ ВОПРОСЫ
1.

В каких случаях целесообразно задавать условия выполнения

программы в виде параметров глобального окружения, в виде параметров
локального окружения, в виде параметров вызова?
2.

Почему параметры вызова программы всегда имеют тип

строки символов?
3.

Что такое конвейеризация и чем она обеспечивается?

Приведите примеры задач, легко решаемых при помощи конвейеризации.
4.

Какие необязательные свойства командного интерпретатора

могут сделать его более удобным в использовании?

379

Как при параллельном выполнени нескольких процессов в

5.

режиме командной строки определить процесс, выдавший сообщение на
экран? Как адресовать ответ оператора определенному процессу?

Синтаксис

6.

язык

создавался

C-shell

на

основе

языка

программирования C, синтаксис языка REXX – на основе PL/1. В чем
принципиальное отличие языков C-shell и REXX от их прототипов?
7.

Назовите основные элементы интерфейса WIMP и их функции.

8.

Какие

свойства

интерфейса

WIMP

полностью

следуют

метафоре рабочего стола? Какие ее расширяют?
Приведите

9.

соображения

"за"

и

встраивания

"против"

графического интерфейса непосредственно в ОС.
10.

На примере X Window объясните концепцию аппаратно-

независимого интерфейса.

Заключение
Принципы управления вычислительными ресурсами, как они
изложены в данном пособии, сложились в конце 60-х - начале 70-х годов.
Лишь

очень

ограниченный

ряд

общих

решений

(нити,

файлы,

отображаемые в память, объектно-ориентированные системы, WIMPинтерфейс и немногие другие) датируется более поздним периодом, хотя
некоторые

из

этих

решений

локально



отдельных

системах)

существовали и ранее. Прогресс в развитии ОС и информационных
технологий вообще идет не столько по пути изобретения новых подходов,
сколько по пути адаптации уже известных подходов к новым условиям. На
разных этапах этого развития те или иные решения (концепции,
дисциплины, алгоритмы) становятся более популярными,возможно,
господствующими,

но

при

изменении
380

условий

оказываются

востребованными и другие решения, которые казались отжившими. У
авторов

складывается

впечатление,

что

в

настоящее

время

информационные технологии завершают виток своего развития, и
сегодняшние

решения

гораздо

более

базируются

на

концепциях

начального этапа, чем решения, например, 10-летней давности.
Это позволяет сделать вывод о том, что вопросы, рассмотренные
нами в данной книге относятся к багажу фундаментальных знаний
специалиста в области информационных технологий, и значение этих
знаний не зависит от рыночной коньюнктуры.
Во второй части учебного пособия будут рассмотрены реализации
общих принципов управления ресурсами в конкретных современных ОС.

Список литературы
1. Авен О.И., Коган Я.А. Управление вычислительным процессом в
ЭВМ. – М.: Энергия, 1978.
2. Артамонов Г.Т., Брехов О.М. Аналитические вероятностные
модели функционирования ЭВМ. – М.:Энергия, 1978.
3. Бек Л. Введение в системное программирование. – М.:Мир, 1986.
4. Брукс П.Ф. Мифический человеко-месяц или как создаются
программные системы. – СПб.:Символ-Плюс, 2001.
5. Вебер Д. Технология JavaTM в подлиннике. – СПб.: БХВПетербург, 2000.
6. Вегнер П. Программирование на языке АДА. – М.:Мир, 1983.
7.

Гордеев

А.В.,

Молчанов

А.Ю.

Системное

программное

обеспечение. – СПб: Питер, 2001.
8. ГОСТ 19781-90. Обеспечение систем обработки информации
программное. Термины и определения. – М.:Изд-во стандартов, 1990.

381

9. Готье Р. Руководство по операционной системе Unix. –
М.:Финансы и статистика, 1985.
10. Дал У., Дейкстра Э., Хоор К. Структурное программирование. –
М.:Мир, 1972.
11. Дейкстра Э. Взаимодействие последовательных процессов. //
"Языки программирования"; под ред. Ф.Женюи. – М.:Мир, 1972.
12. Дейтел Г. Введение в операционные системы – М.:Мир, 1986 –
Т.1, 2.
13. Донован Дж. Системное программирование. М.:Мир, 1975.
14. Зелковиц М., Шоу А., Гэннон Дж. Принципы разработки
программного обеспечения. – М.:Мир, 1982.
15. Кейслер С. Проектирование операционных систем для малых
ЭВМ. – М.:Мир, 1986.
16. Клейнрок Л. Вычислительные системы с очередями. – М.:Мир,
1979.
17. Колин А. Введение в операционные системы. – М.,Мир, 1975.
18. Коэн Л.Дж. Анализ и разработка операционных систем. –
М.:Мир, 1975.
19. Краковяк С. Основы организации и функционирования OС ЭВМ.
– М.:Мир, 1988.
20. Кузьминский М. Z-архитектура. // "Открытые системы", 2001,
№10 (http://www.osp.ru/os/2001/10/010.htm).
21. Операционная система IBM/360. Супервизор и управление
данными. – М.:Сов.радио, 1973.
22. Олифиер В.Г, Олифиер Н.А. Сетевые операционные системы. –
СПб.: Питер, 2000.
23. Петцольд Ч. Windows 95 – вызов программистам // "PC Magazine.
Russian Edition", 1995, N8.

382

24. Пратт Т. Языки программирования. Разработка и реализация. –
М.:Мир, 1979.
25. Ресурсы Windows NT. – С-Пб.: BVH–Санкт-Петербург, 1995.
26. Соловьев Г.Н., Никитин В.Д. Операционные системы ЭВМ. –
М.:Высшая школа, 1989.
27. Солтис Ф. Основы AS/400. – М.: Изд.отд. "Русская редакция"
ТОО "Channel Trading Ltd." – 1998.
28. Тимонин В.М. СВМ ЕС. Основы функционирования и средства
обеспечения пользователя. – М.:Изд-во МАИ, 1990.
29. Цикритзис Д., Бернстайн Ф. Операционные системы – М.:Мир,
1977.
30. Шоу А. Логическое проектирование операционных систем –
М.:Мир, 1981.
31. Якушевский И. Мэйнфреймы – мифы и реальность. –
"Hard'n'Soft", 1995, N11.
32. i486 процессор. Кн.1, 2. – М.:И.В.К.–СОФТ, 1993.
33. Bach M.J. Design of the Unix Operating System. – Prentice-Hall Inc,
Englewood, 1986. (Русский перевод есть, например, в http://lib.ru/BACH/).
34. Bolthouse D. Exploring IBM Client/Server Computing. – Maximum
Press, NJ, 1997.
35. Enterprise System/9000. 9221 Processor. Processors Characteristics. –
IBM, Publ.No SA33-1609-03, 1994.
36. Finkel R.A. An Operating Systems Vade Mecum. – Prentice-Hall Inc,
Englewood, 1988.
37. Gosling J., Joy B., Steele G., Bracha G. The Java Language
Specification. Second Edition. – Sun Microsystems Inc., 2000.
38. Hoskins J. IBM System/390. A Business Perspective. – Maximum
Press, NJ, 1997.

383

39.

iSeries

Library



http://www-1.ibm.com/servers/eserver/

iseries/library/
40. Joung J. Exploring IBM's New Age Mainframes. – Maximum Press,
NJ, 1995.
41. Madnick S., Donovan J. Operating Systems. – McGraw-Hill, 1986.
(Есть русский перевод более раннего издания: Мэдник С., Донован Дж.
Операционные системы. – М.:Мир, 1975)
42. QNX System Architecture. –

http://www.qnx.com/literature/

qnx_sysarch/
44. Solomon D.A., Russinovich M. Inside Microsoft Windows 2000. –
Microsoft Press, 2000
44. Vajapeyam S. Early 21st Centiry Processors. – "Computer", April
2001. – pp. 47 – 50.
45. z/Architecture. Principles of Operation, IBM SA22-7832-00
46. z/VM Internet Library – http://www.vm.ibm.com/library/

384