Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

2курсИБ(ОС) / lab5теория

.pdf
Скачиваний:
19
Добавлен:
07.06.2015
Размер:
520.63 Кб
Скачать

{

System.err.println( "\nAll buffers empty. " + name + " waits." );

wait(); // wait until buffer contains new data } // end try

// if waiting thread interrupted, print stack trace catch ( InterruptedException exception )

{

exception.printStackTrace();

}// end catch

}// end while

//obtain value at current readLocation int readValue = buffers[ readLocation ];

//output consumed value

System.err.println( "\n" + name + " reads " + readValue + " " );

//decrement occupied buffers value --occupiedBuffers;

//update readLocation for future read operation readLocation = ( readLocation + 1 ) % buffers.length;

//display contents of shared buffers System.err.println( createStateOutput() );

notify(); // return a waiting thread to ready state

return readValue; } // end method get

// create state output

public String createStateOutput()

{

// first line of state information String output = "(buffers occupied: " +

occupiedBuffers + ")\nbuffers: ";

for ( int i = 0; i < buffers.length; ++i )

{

output += " " + buffers[ i ] + " "; } // end for

// second

line of state information

output +=

"\n

";

for ( int i = 0; i < buffers.length; ++i )

{

output += "---- ";

 

} // end

for

 

// third

line of state information

output += "\n

";

//append readLocation (R) and writeLocation (W)

//indicators below appropriate buffer locations for ( int i = 0; i < buffers.length; ++i )

{

if ( i == writeLocation && writeLocation == readLocation )

{

+= " WR

";

output

} // end if

 

else if (

i == writeLocation )

{

+= " W

";

output

} // end if

 

else if (

i == readLocation )

{

+= " R

";

output

} // end if

 

else

 

 

{

+= "

";

output

}// end else

}// end for output += "\n";

return output;

}// end method createStateOutput

}// end class CircularBuffer

Запустить эту конструкцию можно следующим образом

//CircularBufferTest.java

//CircularBufferTest shows two threads manipulating a circular buffer.

//set up the producer and consumer threads and start them

public class CircularBufferTest

{

public static void main ( String args[] )

{

//create shared object for threads; use a reference

//to a CircularBuffer rather than a Buffer reference

//to invoke CircularBuffer method createStateOutput CircularBuffer sharedLocation = new CircularBuffer();

//display initial state of buffers in CircularBuffer System.err.println( sharedLocation.createStateOutput() );

//set up threads

Producer producer = new Producer( sharedLocation );

Consumer consumer = new Consumer( sharedLocation );

producer.start(); // start producer thread consumer.start(); // start consumer thread

}// end main

}// end class CircularBufferTest

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

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

Основные направления исследований в области взаимоблокировок — это prevention (предотвращение), avoidance (обход), detection (обнаружение) и recovery (восстановление после них). Также рассмотрим родственную проблему бесконечного откладывания (indefinite postponement или starvation), когда процесс, не находящийся в состоянии взаимной блокировки, ожидает события, которое может никогда не произойти.

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

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

Большинство взаимоблокировок в системе возникают в результате конкуренции за право обладания выделенными ресурсами (dedicated resources), т. е. ресурсами, которые могут в разные моменты времени использоваться разными процессами, но в любой конкретный момент времени этот ресурс может использовать только один процесс.

Для обозначения таких ресурсов еще используется термин «ресурсы последовательного повторного использования» (serially reusable resources).

Например, принтер должен выполнять задания только одного потока (иначе листы в распечатке перемешаются). Простейший пример взаимоблокировки изображен на рисунке ниже. На изображенном здесь графе распределения ресурсов (resource allocation graph) продемонстрировано «круговое ожидание» (circular wait). Иногда эту ситуацию называют клинч (deadly embrace).

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

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

В «старых» ОС размер буферных файлов для спулинга задавался «вручную» (системным администратором). Наиболее простое решение — предусмотреть размер больше, чем может в принципе потребоваться. Однако оно не всегда реализуемо. Более распространенное решение состояло в том, чтобы установить ограничение: процессы входного спулинга не должны принимать дополнительные задания, если заполнение файлов спулинга достигает порога насыщения (saturation treshold) — например, они оказываются заполненными на 75%.

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

Хорошей иллюстрацией проблем параллельного программирования может служить так называемая проблема обедающих философов (Dining Philosophers). Изначально она была сформулирована Э.Дейкстрой (1965); современная формулировка принадлежит Э.Хоару.

Суть ее такова: пять философов сидят возле круглого стола. На столе стоят пять тарелок со спагетти, между ними лежат ровно пять вилок (существует версия, когда философы сидят в китайском ресторане, а между тарелками положены пять палочек).

Будем полагать, что длинные спагетти съесть при помощи одной вилки невозможно, и, чтобы пообедать, философ должен орудовать двумя вилками (классический вариант правильного поедания длинной пасты — вилка в правой руке, ложка в левой; но итальянцев среди придумавших этот пример не было. Равно как и китайцев, впрочем).

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

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

Как можно описать поведение типичного философа?

void typicalPhilosopher() { while (true) {

think();

eat();

}

}

Проблемным здесь является метод eat(). Рассмотрим простой, но опасный с точки зрения возникновения взаимоблокировок вариант реализации этого метода.

void eat() { pickUpLeftFork(); pickUpRightFork(); eatForSomeTime(); putDownRightFork(); putDownLeftFork();

}

Может случиться так, что все философы (ибо они действуют асинхронно и параллельно), выполнят строку pickUpLeftFork() до того, как кто-либо из них выполнит строку pickUpRightFork(). Тогда каждый из философов получит в свое распоряжение одну вилку, и ни одной вилки не останется на столе.

Можно, к примеру, предложить заменить эти два метода методом pickUpBothForkAtOnce() — в этом случае философы будут брать обе вилки сразу (если, конечно, полагать метода атомарным). Однако возможность бесконечного откладывания все равно сохраняется — может случиться так, что поедать спагетти будут все время одни и те же философы, в то время как другие будут страдать от голода.

Аналогичная ситуация в операционной системе может происходить из-за «дискриминационной политики» планировщика ресурсов системы. Например, когда ресурсы распределяются по приоритетному принципу, может случиться, что какой-то процесс окажется постоянно ожидающим, поскольку все время будут приходить процессы с более высоким приоритетом. Одной из мер, позволяющих этого избежать, является повышение приоритета процесса по мере его старения (aging) — времени, в течение которого он ожидает ресурс.

Если вы вспомните алгоритмы Деккера и Петерсона, то они предотвращают бесконечное откладывание при помощи концепции предпочтительного ресурса (по сути, аналогичной

концепции старения).

Скажем несколько слов о концепции ресурсов.

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

Некоторые виды ресурсов являются динамически неперераспределяемыми (nonpreemptible)

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

Некоторые виды ресурсов допускают разделение (shared) между процессами, другие же допускают лишь монопольное использование их одним процессом (дисковая подсистема vs процессор).

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

Поскольку с таким кодом могут работать многие пользователи одновременно, он не должен изменяться. Такой код называется реентерабельным (reentrant). Код, который может изменяться, но предусматривает повторную инициализацию при каждом выполнении, называется кодом последовательного многократного использования (serially reusable). С таким кодом может работать только один процесс в каждый конкретный момент времени.

Коффман, Элфик и Шошани (Coffman, E.G., Elphick, M.J., A.Shoshani) сформулировали следующие четыре необходимых условия возникновения взаимоблокировки:

1.Право монопольного управления ресурсом может запрашиваться только одним процессом в каждый конкретный момент времени — условие взаимоисключения (mutual exclusion condititon)

2.Процессы удерживают за собой ресурсы, уже выделенные им, ожидая в то же время выделения дополнительных ресурсов — условие ожидания дополнительных ресурсов (waitfor condition или hold-and-wait condition)

3.Ресурсы нельзя отобрать у удерживающих их процессов, пока эти ресурсы используются этими процессами — условие неперераспределяемости (nonpreemption condition).

4.Существует замкнутая цепочка процессов, в которой каждый процесс удерживает за собой один или более ресурсов, требующихся следующему процессу в цепочке — условие кругового ожидания (circullar-wait condition)

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

Это знание позволяет нам разрабатывать схемы для предотвращения возникновения взаимоблокировок.

В исследованиях по проблеме взаимоблокировок можно выделить четыре основных направления — предотвращение взаимоблокировок (deadlock prevention), обход взаимоблокировок (deadlock avoidance), обнаружение взаимоблокировок (deadlock detection),

восстановление после взаимоблокировок (deadlock recovery).

Предотвращение взаимоблокировок.

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

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

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

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

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

Отметим, что первое условие — а именно условие взаимоисключения — Хавендер не предлагает нарушать. Т.е. процессам разрешается получить право на монопольное управление ресурсами (и нам нужно предусмотреть работы с ресурсами последовательного повторного использования).

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

Эта стратегия существенно повышает вероятность бесконечного откладывания. Процесс, пытающийся «накопить» много ресурсов для использования, будет «уступать» процессам, которые требуют небольшого числа ресурсов. Один из способов избежать этого — придерживаться дисциплины FIFO (но неэффективность использования ресурсов высока).

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

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

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

Третья стратегия: исключить круговое ожидание. Всем ресурсам присваиваются уникальные номера, благодаря чему осуществляется линейное упорядочивание (linear ordering) ресурсов. Процессы должны запрашивать ресурсы строго в порядке возрастания номеров (т. е. если процесс запросил ресурс #3, то в дальнейшем он может обратиться только

к ресурсам с номерами > 3). Эта стратегия реализована в ряде ОС, но и здесь есть ряд трудностей. Номера ресурсов назначаются при установке машины, при введении новых типов ресурсов может потребоваться переработка существующих программ (и систем). Кроме того, назначаемые номера ресурсов должны следовать «нормальному порядку» выполнения программ. Для заданий, выполнение которых соответствует этому порядку, стратегия будет работать весьма эффективно. А вот если заданию требуются ресурсы не в том порядке, то потеря эффективности обеспечена. В целом — это плохо влияет на переносимость программ.

Обход взаимоблокировок.

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

Основная идея состоит в том, что система классифицирует все ресурсы, с которыми она работает, по типам (resource type). Каждый тип объединяет ресурсы, схожие по функциональности.

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

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

В системе существует фиксированное количество ресурсов t и фиксированное количество процессов n.

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

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

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

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

Вероятно, многие слышали акроним WYSIWYG (What You See Is What You Get). Общение процесса и ОС тоже описывается акронимами (хотя они менее известны): IWWIWWIWI и YGWIGWIGI: I Want What IWant When I What It (я хочу то, что я хочу и когда я хочу это) — You'll Get What I've Got When I Get It (ты получишь то, что у меня будет, когда я это получу).

Говорят, что система находится в надежном состоянии (safe state), если система в состоянии гарантировать, что все текущие процессы смогут завершить работу за разумное время. В противном случае состояние системы называется ненадежным (unsafe state).

Определим также понятия, описывающие распределение ресурсов между процессами.

Пусть max(Pi) — максимальное количество ресурсов, которые необходимы процессу Pi во время работы.

Пусть loan(Pi ) — количество ресурсов, выделенных процессу системой.

Пусть claim(Pi ) — количество дополнительных ресурсов, необходимых процессу

для выполнения работы (т. е. это max(Pi)−loan(Pi) .

Пусть в распоряжении операционной системы имеется t устройств. Число устройств,

n

остающихся для распределения, обозначим через a: a=tloan( Pi ) .

i =1

Например, если в системе 3 процесса, 12 ресурсов, при этом 2 ресурса были выделены P1 , один — процессу P2 и еще 4 — процессу P3 , то в распоряжении системы останется еще 5 свободных ресурсов.

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

Приведем примеры.

Процесс Pi

 

max(Pi)

 

 

loan(Pi )

 

claim(Pi )

 

 

 

надежное состояние

 

 

 

 

 

 

 

 

 

 

P1

 

4

 

 

1

 

3

P2

 

6

 

 

4

 

2

P3

 

8

 

 

5

 

3

 

 

Общее

количество

 

Доступных

ресурсов

 

 

 

ресурсов 12

 

2

 

 

Пояснение: Если 2 ресурса выделить процессу 2, он завершит работу, высвободив все 6

ресурсов, после чего процессы 1 и 3 также смогут завершить работу.

 

 

 

ненадежное состояние

 

 

 

 

 

 

 

 

 

 

P1

 

10

 

 

8

 

2

P2

 

5

 

 

2

 

3

P3

 

3

 

 

1

 

2

 

 

Общее

количество

 

Доступных

ресурсов

 

 

 

ресурсов 12

 

1

 

 

Пояснение: Ни один из процессов, запросив ресурсы, не сможет завершить свою работу.

Замечание: Тупик может и не возникнуть, если процессы ни разу не будут запрашивать

ресурсы полностью, не возвращая в пул ресурсов какие-то уже использованные. Но в

случае, если в текущей ситуации единственный способ завершить работу процессов — это

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

Чтобы состояние стало надежным, необходимо добавить еще один ресурс.

 

переход из надежного состояния в ненадежное

 

 

 

 

 

 

 

 

P1

 

4

 

 

1

 

3

P2

 

6

 

 

4

 

2

P3

 

8

 

 

6

 

2

 

 

Общее

количество

 

Доступных

ресурсов

 

 

 

ресурсов 12

 

1

 

 

Пояснение: Пусть состояние было надежным (см. выше), после чего процесс 3 запросил 1

ресурс (у него было 5). Если система выделит ему этот ресурс, то количество доступных

ресурсов уже

не будет достаточным для

завершения

хотя бы какого-то процесса, и

состояние тем самым станет ненадежным.

 

 

 

 

 

 

 

вопрос: надежное ли состояние?

 

 

 

 

 

 

P1

5

 

1

 

4

P2

3

 

1

 

2

P3

10

 

5

 

5

 

Общее

количество

Доступных

ресурсов

 

 

ресурсов 9

 

2

 

 

Пояснение: Нет, не является. Процесс 2 может закончить свою работу, но 3 освобожденных

им ресурсов не хватит для завершения ни одного из оставшихся процессов. Чтобы

состояние стало надежным, необходимо добавить 1 ресурс. Тогда процессы смогут

завершаться в последовательности: 2, 1, 3.

 

 

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

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

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

Алгоритм банкира исходит из фиксированного количества распределяемых ресурсов. Это зачастую не так: во-первых, устройства могут требовать технического обслуживания (при неисправности), во-вторых, ряд устройств может работать по принципу «горячей замены» (те же USB устройства) — и, таким образом, количество устройств в системе может динамически изменяться.

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

Алгоритм требует, чтобы распределитель ресурсов (т. е. ОС) гарантированно удовлетворял все запросы за некоторый конечный период времени. Но для реальных ОС, и особенно — для систем реального времени, необходимы более конкретные гарантии.

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

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

Соседние файлы в папке 2курсИБ(ОС)