Приведенный в предыдущем разделе пример поможет нам проиллюстрировать еще одну проблему синхронизации - взаимные блокировки, называемые также дедлоками (deadlocks), клинчами (clinch) или тупиками. Если переставить местами операции P(e) и P(b) в программе "писателе", то при некотором стечении обстоятельств эти два процесса могут взаимно заблокировать друг друга. Действительно, пусть "писатель" первым войдет в критическую секцию и обнаружит отсутствие свободных буферов; он начнет ждать, когда "читатель" возьмет очередную запись из буфера, но "читатель" не сможет этого сделать, так как для этого необходимо войти в критическую секцию, вход в которую заблокирован процессом "писателем".
Рассмотрим еще один пример тупика. Пусть двум процессам, выполняющимся в режиме мультипрограммирования, для выполнения их работы нужно два ресурса, например, принтер и диск. На рисунке 4.1,а показаны фрагменты соответствующих программ. И пусть после того, как процесс А занял принтер (установил блокирующую переменную), он был прерван. Управление получил процесс В, который сначала занял диск, но при выполнении следующей команды был заблокирован, так как принтер оказался уже занятым процессом А. Управление снова получил процесс А, который в соответствии со своей программой сделал попытку занять диск и был заблокирован: диск уже распределен процессу В. В таком положении процессы А и В могут находиться сколь угодно долго.
Рисунок 4.1. (a) фрагменты программ А и В, разделяющих
принтер и диск;
(б) взаимная блокировка (клинч);(в) очередь к разделяемому
диску;
(г) независимое использование ресурсов
В зависимости от соотношения скоростей процессов, они могут либо совершенно независимо использовать разделяемые ресурсы (г), либо образовывать очереди к разделяемым ресурсам (в), либо взаимно блокировать друг друга (б). Тупиковые ситуации надо отличать от простых очередей, хотя и те и другие возникают при совместном использовании ресурсов и внешне выглядят похоже: процесс приостанавливается и ждет освобождения ресурса. Однако очередь - это нормальное явление, неотъемлемый признак высокого коэффициента использования ресурсов при случайном поступлении запросов. Она возникает тогда, когда ресурс недоступен в данный момент, но через некоторое время он освобождается, и процесс продолжает свое выполнение. Тупик же, что видно из его названия, является в некотором роде неразрешимой ситуацией.
В рассмотренных примерах тупик был образован двумя процессами, но взаимно блокировать друг друга могут и большее число процессов.
Проблема тупиков включает в себя следующие задачи:
· предотвращение тупиков,
· распознавание тупиков,
· восстановление системы после тупиков.
Тупики могут быть предотвращены на стадии написания программ, то есть программы должны быть написаны таким образом, чтобы тупик не мог возникнуть ни при каком соотношении взаимных скоростей процессов. Так, если бы в предыдущем примере процесс А и процесс В запрашивали ресурсы в одинаковой последовательности, то тупик был бы в принципе невозможен. Второй подход к предотвращению тупиков называется динамическим и заключается в использовании определенных правил при назначении ресурсов процессам, например, ресурсы могут выделяться в определенной последовательности, общей для всех процессов.
В некоторых случаях, когда тупиковая ситуация образована многими процессами, использующими много ресурсов, распознавание тупика является нетривиальной задачей. Существуют формальные, программно-реализованные методы распознавания тупиков, основанные на ведении таблиц распределения ресурсов и таблиц запросов к занятым ресурсам. Анализ этих таблиц позволяет обнаружить взаимные блокировки.
Если же тупиковая ситуация возникла, то не обязательно снимать с выполнения все заблокированные процессы. Можно снять только часть из них, при этом освобождаются ресурсы, ожидаемые остальными процессами, можно вернуть некоторые процессы в область свопинга, можно совершить "откат" некоторых процессов до так называемой контрольной точки, в которой запоминается вся информация, необходимая для восстановления выполнения программы с данного места. Контрольные точки расставляются в программе в местах, после которых возможно возникновение тупика.
Из всего вышесказанного ясно, что использовать семафоры нужно очень осторожно, так как одна незначительная ошибка может привести к останову системы. Для того, чтобы облегчить написание корректных программ, было предложено высокоуровневое средство синхронизации, называемое монитором. Монитор - это набор процедур, переменных и структур данных. Процессы могут вызывать процедуры монитора, но не имеют доступа к внутренним данным монитора. Мониторы имеют важное свойство, которое делает их полезными для достижения взаимного исключения: только один процесс может быть активным по отношению к монитору. Компилятор обрабатывает вызовы процедур монитора особым образом. Обычно, когда процесс вызывает процедуру монитора, то первые несколько инструкций этой процедуры проверяют, не активен ли какой-либо другой процесс по отношению к этому монитору. Если да, то вызывающий процесс приостанавливается, пока другой процесс не освободит монитор. Таким образом, исключение входа нескольких процессов в монитор реализуется не программистом, а компилятором, что делает ошибки менее вероятными.
Рассмотрим подробнее методы предотвращения, распознования и выхода из тупиков.
Для модели, рассматриваемой в этой главе, состояние ОС будет сводиться к состоянию различных ресурсов в этой системе (свободны они или распреденены). Состояние системы изменяется процессами, когда они запрашивают, приобретают или освобождают ресурсы; это будет единственно возможные действия процессов. Если процесс не блокирован в данном состоянии системы, он может изменить это состояние на новое. Однако, так как в общем случае не возможно (это неразрешимая проблема) знать априори, какой путь может избрать произвольный процесс в своей программе, то новое состояние может быть любым из конечного числа возможных; следовательно, процессы моделируются как недетерминированные обекты. Введенные понятия приводят к следующим формальным определениям:
Система есть пара ás,pñ, где s - множество состояний системы {S,T,U,V...} и p - множество процессов {p1,p2,p3…}.
Процесс pi есть частичная функция, отображающая состояния системы в непустые подмножества ее же состояний; это обозначается так: pi: s® {s}
Если pi определен на состоянии S, то область значений pi обозначается как pi(S). Если TÎpi(S), то мы говорим, что pi может изменить состояние из S в T посредством операции, и используем обозначение Si® T. Наконец, нотация S *® W обозначает, что
(a) S = W или
(b) S i® W для некоторого pi или
S i® T для некоторого pi и T, и T *® W.
Другими словами, система может быть переведена посредством n ³ 0 операций с помощью m³0 различных процессов из состояния S в состояние W.
Процесс pi заблокирован в состоянии S, если не существует ни одного T, такого, что S i®T.
Процесс находится в тупике в данном состоянии S, если он заблокирован в состоянии S и если вне зависимости от того, какие операции (изменения состояний) произойдут в будущем, процесс остается заблокированным.
Процесс pi находится в тупике в состоянии S, если для всех T, таких, что Si®T, pi блокирован в T.
Состояние называется тупиковым, если существует процесс pi, находящийся в тупике в S. Тупик предотвращается, по определению, при введении ограничения на систему, такого, чтобы каждое возможное состояние не было тупиковым состоянием.
Состояние S есть безопасное состояние, если для всех T, таких, что Si®T, T не является тупиковым состоянием.
Надежным и простым методом предотвращения клинчей является требование предварительного объявления пользователем всех необходимых его заданию ресурсов еще на стадии представления задания. В этом случае планировщик заданий просто не пропускает задание на обработку до тех пор, пока все затребованные ресурсы не станут доступными. Ресурсы закрепляются за заданием на все время его выполнения. Метод имеет широкое распространение, однако ему присущи определенные недостатки:
пользователь не всегда может определить, какие устройства (ресурсы) потребуются его заданию на стадии выполнения;
задание вынуждено ждать освобождения всех ресурсов, хотя некоторые из них могут понадобиться ему значительно позже, например при его завершении, которое наступит лишь несколько часов спустя.
Если известно, что вероятность использования заданием определенного устройства невелика, постоянное закрепление такого устройства за заданием является непозволительным расточительством. Например, выдача на печать содержимого памяти может потребоваться только в случае возникновения ошибок в работе программы, тем не менее печатающее устройство будет монопольно закреплено за этим заданием.
Каждое конкретное задание не использует непрерывно все устройства в течение всего периода выполнения, следовательно, эти устройства могли бы освобождаться и предоставляться другим заданиям в системе с мультипрограммированием.
Существует еще одна форма контроля над распределением ресурсов, а именно стандартная последовательность распределения. Согласно этому методу, каждому ресурсу приписывается определенный номер (например, HDD – 1, CDROM – 2, FDD – 3, LPT- 4, COM1 – 5, COM2 – 6 и т.д.). Все запросы должны быть упорядочены по возрастанию этих номеров. Например, последовательность запросов á HDD (1), FDD (3), LPT (4)ñ будет правильной, а последовательность á HDD (1), LPT (4), FDD (3)ñ будет неверной. Если запросы следуют в порядке возрастания номеров, то они удовлетворяются по мере того, как ресурсы становятся доступными. В противном случае процесс вынужден ждать.
Этот метод гарантирует отсутствие клинчей. Отметим, что нам также не обязательно знать максимальную потребность в ресурсах. Однако и у него есть свои недостатки. Стандартная последовательность может отличаться от порядка, в котором процессу в действительности могут потребоваться ресурсы. Если, например, задание использует сначала COM, а печатающее устройство значительно позже, ему все равно придется сначала заказать печатающее устройство.
Однако разумное назначение номеров может сделать эти неудобства гораздо менее заметными.
Схема предотвращения основана на точке зрения, что систему нельзя допускать до опасного состояния. Поэтому, когда какой-нибудь процесс делает запрос, который может привести к тупику, система принимает меры к тому, чтобы избежать опасного состояния: либо неудовлетворяет этот запрос, либо отбирает ресурс у другого процесса, чтобы избежать возможного попадания в тупик. Преимущество этого метода состоит в том, что он всегда предотвращает тупиковые ситуации. Однако ресурсы часто присваивают, поскольку запросы на имеющиеся в распоряжении системы ресурсы отклоняются иногда с целью предотвратить попадание в опасное состояние. К тому же и сам предотвращающий алгоритм может внести большие накладные расходы. Рассмотрим в качестве примера один из таких алгоритмов.
НАЧАЛО «БАНКИР»
/*ПОДГОТОВИТЬ*/
/*N - число процессов; К – число устпойств*/
СВОБУСТР = 0 ; ОБЩУСТР = К
ЦИКЛ i = 1 ДО N
ОСТ (i) = МАХ (i) – ВЫД (i)
СВОБУСТР = ОБЩУСТР – ВЫД (i)
ВСЕ – ЦИКЛ
УПОРЯДОЧИТЬ ОСТАТКИ ПО ВОЗРОСТАНИЮ
/*ПОПЫТАТЬСЯ ЗАВЕРШИТЬ ВСЕ ПРЦЕССЫ*/
ЦИКЛ i = 1 ДО N ПОКА ОСТ (i) < СВОБУСТР
СВОБУСТР = СВОБУСТР + ВЫД (i)
ВСЕ – ЦИКЛ
/*ЗАВЕРШИТЬ*/
ЕСЛИ СВОБУСТР = ОБЩУСТР
СОСТОЯНИЕ БЕЗОПАСНОЕ
ИНАЧЕ
СОТОЯНИЕ ОПАСНОЕ
ВСЕ – ЕСЛИ
КОНЕЦ «БАНКИР»
Исходными данными являются:
Система S, содержащая N процессов и К устройств.
Для каждого процесса i задано:
МАХ (i) - максим.потребность i – процесса в устройствах;
ВЫД (i) - число устройств выделенных i-му процессу.
Пусть в какой-то период времени 3 процесса разделяют 5 устройств одного типа. Факты запроса устройств процессами представим в виде таблицы:
T |
У1 |
У2 |
У3 |
У4 |
У5 |
1 |
3 |
|
1 |
2 |
|
2 |
|
2 |
|
|
2 |
3 |
1 |
|
|
|
|
4 |
|
3 |
|
|
|
5 |
|
|
2 |
|
|
Для предотвращения тупиков необходимо определить МАХ (i): МАХ (1) = 2, МАХ(2) = 4 и т.д. Обшее число устройств (ОБЩУСТР) равно 5 и все они одного типа.
На обслуживание принимаются процессы, которым необходимо не более ОБЩУСТР устройств и для каждого из них определяются остатки (ОСТ(i)). В первый момент времени 1-й процесс запросил 1 устройство, 2-й процесс – 1, 3-й процесс – 1.
Если им выделить эти 3 устройства, то получим:
Имя процесса МАХ потребность Выделено Остаток
1 2 1 1
2 4 1 3
3 2 1 1
С точки зрения вероятности тупика худший случай из всех возможных - это если каждый процесс запросит все, что ему еще полагается. Если при таких обстоятельствах все процессы могут окончиться, то рассматриваемое состояние для системы безопасно.
В нашем случае система имеет в резерве 2 устройства 5 и может «рассчитаться» с каждым из процессов (сначала с первым или третьим, а затем со вторым). Поэтому состояние системы – безопасное.
В следующий момент времени второй процесс запрашивает 2 устройства. Если ему их выделить, то получим:
Имя процесса МАХ потребность Выделено Остаток
1 2 1 1
2 4 3 1
3 2 1 1
В резерве у системы нет одного устройства и, очевидно, состояние – опасное. Заметим, что по условию задачи процессы не отдают предоставленные им ресурсы. Во второй момент времени второй процесс блокируется, и ресурсы ему не выделяются. В третий момент времени первый процесс запрашивает устройство. Если его ему выделить, то получим:
Имя процесса МАХ потребность Выделено Остаток
1 2 2 0
2 4 1 3
3 2 1 1
В резерве – одно устройство, а после окончания процесса 1 - он освободит 2 устройства и можно будет рассчитаться с процессом 2. Таким образом, и это состояние – безопасное. Легко показать, что в четвертый момент времени – состояние также будет безопасным, а в пятый момент времени – опасный.
Предыдущий метод гарантирует отсутствие тупиковых ситуаций за счет весьма осторожной политики. Метод обнаружения и восстановления действует гораздо более решительно. Не препятствуя возникновению тупика, метод предполагает его обнаружение и восстановление прерванной им нормальной работы.
Ниже, на примере, рассматривается один из алгоритмов обнаружения тупиков (алгоритмы Медника).
Произвольно нумеруем все ресурсы и процессы в системе. Создаем таблицы, в которых собиралась бы вся информация о назначении ресурсов процессами (РАСПРЕД) и о процессах, блокированных при попытке обращения к ресурсу (БЛК).
Заносим в эти таблицы соответствующую информацию при всяком назначении или освобождении ресурса согласно следующему алгоритму:
НАЧАЛО «МЕДНИК»
(*Процесс Рi запрашивает занятый ресурс Уi*)
(*ПОДГОТОВИТЬ*)
К = РАСПРЕД (i) (*К – номер процесса владеющего ресурсом Уi*)
ЦИКЛ-ПОКА БЛК (К) и J¹K
J = К
N = БЛК (К) (*N – номер ресурса, которого ожидает*)
К = РАСПРЕД (N) (*рассматриваемый процесс*)
ВСЕ-ЦИКЛ
ЕСЛИ
J = К
ТУПИК
ИНАЧЕ
БЛК (J) = i (*Перевести процесс Рi в состояние ожидания*)
ВСЕ-ЕСЛИ
КОНЕЦ «МЕДНИК»
Например, в первый момент времени заполняются строчки 1,3,4 таблицы РАСПРЕДЕЛ, а во второй – строчки 2,5.
В момент времени отработки запроса процесса Рj на уже занятый ресурс Уi проверить наличие тупика, используя алгоритм, представленный на рис. 3.
В третий момент времени Р1 запрашивает У1, которое принадлежит процессу Р3 (К=РАСПРЕДЕЛ (1)=3).
Так как J¹К, а также БЛК (3) = « » - тело цикла в алгоритме на рис. 3 не выполняется и БЛК (1) = 1.
В четвертый момент времени Р3 запрашивает У2. Получаем :
К = 2 J = 3 и БЛК (2) = « »
Поэтому : БЛК (3) = 2.
ТАБЛИЦА РАСПРЕДЕЛЕНИЯ РЕСУРСОВ /РАСПРЕД/
Номер ресурса |
Номер процесса, которому распределен ресурс |
1 |
3 (1) |
2 |
2 (2) |
3 |
1 (1) |
4 |
2 (1) |
5 |
2 (2) |
ТАБЛИЦА БЛОКИРОВАННЫХ ПРОЦЕССОВ /БЛК/
Номер процесса |
Номер ресурса, которого ожидает данный процесс |
1 |
1 (3) |
2 |
|
3 |
2 (4) |
Рис. 2. Таблицы распределения ресурсов и блокированных процессов.
В пятый момент времени Р2 запрашивает У3, которым владеет процесс Рi (К = 1). Вто же время процесс Р1 ожидает момента времени, когда освободится устройство У1, которым владеет процесс 3. В свою очередь процесс 3 ожидает освобождения устройства У2, которым владеет процесс Р2 (J = К). Таким образом, система попала в тупик в пятый момент времени.
Существуют два общих подхода для восстановления из тупиковых состояний. Первый основан на прекращении процессов. Процессы в тупике постедовательно прекращаются (уничтожаются) в некотором систематическом порядке до тех пор, пока не станет доступным достаточное количество ресурсов для устранения тупика; в худшем случае уничтожаются все процессы, первоначально находившиеся в тупике, кроме одного. Второй выход основан на перехвате ресурсов. У процессов отнимается достаточное количество ресурсов и отдается процессам, находящимся в тупике, чтобыликвидировать тупик; процессы в первом множестве остаются с выданными запросами на перехваченные у них ресурсы.
Возможно, самым практичным и простым методом является стратегия завершения, по которой первым и уничтожаются процессы с наименьшей ценой прекращения. «Ценой» прекращения процесса может быть, например:
- приоритет прцесса;
- цена повторного запуска процесса и выполнение до текущей точки согласно обычным нормальным системным учетным процедурам.
Простая внешняя цена, основанная на типе задания, связанного с процессом; например, задания студентов, административные задания, производственные задания, научные задания, а также задания системного программирования, причем каждое может иметь фиксированные цены прекращения.
Алгоритмы выхода обычно завершает наименее дорогой процесс, уничтожая его и освобождая его ресурсы, а затем, насколько возможно, сокращает граф; эти шаги повторяются до тех пор, пока граф не станет содержать только изолированные вершины. После выхода из тупика начальное состояние системы-это исходное тупиковое состояние без тех процессов, которые были прекращены. Достоинство этого метода-простота, но, к сожалению, он без разбора разрушает те процессы, которые оказывают незначительное влияние на тупик.