Скачать презентацию
Идет загрузка презентации. Пожалуйста, подождите
Презентация была опубликована 11 лет назад пользователемweb-local.rudn.ru
1 Архитектура вычислительных систем Константин Ловецкий Декабрь 2011 Кафедра систем телекоммуникаций 1 Взаимоисключения
2 При одновременном доступе нескольких процессов к разделяемым данным могут возникать определенные проблемы, связанные с очередностью действий. Рассмотрим пример. Два различных процесса имеют доступ к разделяемой памяти, в которой содержится целочисленная переменная. Пусть ее начальное значение равно 5. Оба процесса в некий момент пытаются увеличить значение переменной на 1 (то есть в итоге переменная должна получить значение 7). Для этого необходимо загрузить значение в регистр, увеличить значение регистра на 1 и выгрузить его значение обратно в память. Ситуация гонок (race condition)
3 3 Представим, что первый процесс выполнил команду mov для загрузки значения переменной в регистр (то есть в регистре теперь содержится число 5), и в этот момент произошло прерывание по таймеру, в результате которого процесс оказался снят с исполнения и помещен в очередь процессов, готовых к выполнению. В это время второй процесс также выполняет команду mov, потом команду inc для увеличения значения в регистре и команду mov для выгрузки содержимого регистра в память. Переменная теперь равна 6. Между тем, первый процесс дождался своей очереди и снова поставлен на выполнение. Первую команду mov он уже выполнил, так что в его регистре сейчас число 5. Он выполняет команду inc (в регистре теперь 6) и команду mov (значение 6 выгружается в память). Таким образом, получается, что в итоге значение переменной оказалось равно 6, а не 7. Отметим, что значением было бы именно 7, если бы первый процесс не оказался прерван после первой команды. Ситуация гонок (race condition)
4 Также заметим, что узнать, был процесс прерван или нет, вообще говоря, невозможно, как и запретить прерывать процесс. Получается так, что конечный результат зависит от того, в какой конкретно последовательности произойдут события в независимых процессах. Это называется ситуацией гонок (англ. race condition); встречается также перевод «ситуация состязания». Представим себе теперь процесс, вычисляющий общую сумму остатков на счетах всех клиентов банка. Очевидное решение прочитать в цикле остатки на каждом счете и просуммировать их. Ситуация гонок (race condition)
5 Однако, если во время работы этого процесса какой-либо другой процесс произведет перевод денег с одного из счетов в конце таблицы (то есть из тех, остатки которых еще не учтены) на один из счетов в начале (то есть из тех, остатки которых уже учтены), полученная в результате итоговая общая сумма будет отличаться от реальной (как раз на сумму транзакции по переводу, поскольку переведенные деньги окажутся не попавшими в подсчет ни на новом, ни на старом месте ведь на новый счет они пришли уже после того, как он был учтен, а со старого сняты до того, как он был учтен). Таким образом, нежелательные эффекты возможны даже тогда, когда один из участников не производит никаких изменений, а только читает данные Ситуация гонок (race condition)
6 Очевидным способом избежания ситуации гонок является такое построение работы, при котором процессы, работающие с разделяемыми данными, не могут работать одновременно: например, пока работает один процесс, обращающийся к базе данных, другой процесс, которому также нужна эта база данных, блокируется при запуске до тех пор, пока первый не завершится. Такое решение представляется неудачным, поскольку внесение изменений в базу данных может быть не единственной задачей процесса. Вполне возможно, что процесс бОльшую часть времени занимается работой, никак с разделяемыми данными не связанной, причем время жизни процесса может оказаться слишком большим, чтобы на все это время запрещать доступ к данным кому бы то ни было еще. Например, вполне можно представить себе постоянно работающий сервер, с помощью которого клиенты банка могут узнавать остатки на своих счетах. Непосредственно он достаточно редко обращается к базе данных, но при этом работать может месяцами без перерыва. Ясно, что блокировать на все это время доступ к базе данных других процессов нельзя, ведь это парализовало бы работу банка Взаимоисключения. Критические секции
7 В связи с этим вводится понятие критической секции. Под критической секцией понимается такая часть программы, в которой производятся логически связанные манипуляции с разделяемыми данными. Так, в предыдущих примерах критическими секциями в процессах, осуществляющих перевод денег со счета на счет, являются действия с момента чтения первого остатка до момента записи последнего остатка; в процессе, подсчитывавшем сумму всех остатков, критическая секция начинается в момент начала суммирования и заканчивается с его окончанием. Ясно, что о критической секции можно говорить только в применении к конкретным разделяемым данным. Так, если два процесса обращаются к разным данным, пусть и разделяемым с кем-то еще, это не может привести к ошибкам. Поэтому часто можно встретить выражение критическая секция по переменной x или критическая секция по файлу f, и т.п Взаимоисключения. Критические секции
8 Такая организация работы процессов, при которой два (и более) процесса не могут одновременно находиться в критических секциях по одним и тем же данным, называется взаимным исключением (англ. mutual exception). Следует отметить, что взаимные исключения могут породить дополнительные проблемы, так что при выборе метода их реализации следует соблюдать осторожность. Сформулируем требования, налагаемые на механизм взаимного исключения: Взаимоисключения. Критические секции
9 1. Два и более процесса не должны ни при каких условиях находиться одновременно в критических секциях, связанных с одними и теми же данными. 2. В программе не должно быть никаких предположений о скорости выполнения процессов и о количестве процессоров в системе. 3. Процесс, находящийся вне критических секций, не должен при этом быть причиной блокировки других процессов. 4. Недопустима ситуация «вечного ожидания» (то есть такая ситуация, при которой некоторый процесс никогда не получит доступ в нужную ему критическую секцию). 5. Процесс, заблокированный в ожидании разрешения на вход в критическую секцию, не должен расходовать процессорное время (то есть не должно быть активного ожидания) Требования к механизму взаимного исключения
10 1.Блокировочная переменная Пусть имеются некоторые данные, доступ к которым осуществляют несколько процессов. Заведем в разделяемой памяти целочисленную переменную (будем называть ее s) и примем соглашение, что значение переменной 1 означает, что с разделяемыми данными никто не работает, а значение 0 – что один из процессов в настоящее время работает с разделяемыми данными и необходимо подождать, пока он не закончит работу. При запуске системы присвоим переменной s значение 1. Доступ к данным будем осуществлять следующим образом: while(s == 0) {} /* пустой цикл, пока нельзя входить в критическую секцию */ s = 0; /* запретили доступ другим процессам */ section(); /*... работа с разделяемыми данными... */ s = 1; /* разрешили доступ */ Устаревшие подходы к организации взаимного исключения
11 2. Запрет внешних прерываний Логично приходит в голову идея о запрете внешних (аппаратных) прерываний на время выполнения критической секции. К сожалению, этот вариант неприемлем по целому ряду причин. Рассмотрим эти причины. Во-первых, запрет прерываний годится лишь для кратковременных критических секций: длительное запрещение прерываний нарушит работу аппаратуры (например, перестанут приниматься и передаваться данные по сети). Во-вторых, запрет прерываний пригоден только для работы с данными, находящимися в оперативной памяти, поскольку для любого обмена с внешними устройствами (в том числе для чтения и записи файлов) аппарат прерываний должен работать. В-третьих, запрет прерываний обычно касается только одного процессора. В системе с несколькими процессорами это эффекта не даст Устаревшие подходы к организации взаимного исключения
12 Чередование Следующий способ взаимоисключения заключается в том, что процессы по очереди передают друг другу право работы с разделяемыми данными на манер эстафетной палочки Устаревшие подходы к организации взаимного исключения for(;;) { while(turn != 0) {} section(); turn = 1; noncritical_job(); } for(;;) { while(turn != 1) {} section(); turn = 0; noncritical_job(); } Здесь показаны два процесса, осуществляющие доступ к разделяемым данным (функция section()) в соответствии с маркером чередования, хранящимся в переменной turn. Значение 0 означает, что право на доступ к разделяемым данным имеет первый процесс, значение 1 соответствует праву второго процесса. Завершив работу в критической секции, процесс передает «ход» другому процессу и приступает к выполнению действий, не требующих доступа к разделяемым данным (функция noncritical_job()).
13 Такой способ действительно не дает процессам оказаться в критической секции одновременно, но имеет, к сожалению, другой недостаток. Если один из процессов, передав ход другому, быстро выполнит все некритические действия и снова попытается войти в критическую секцию, может получиться так, что второй процесс в это время до своей критической секции так и не дошел (и, соответственно, не передал ход первому процессу). В результате второй процесс, не нуждаясь в доступе к разделяемым данным, тем не менее будет мешать осуществлять такой доступ первому процессу, то есть нарушится второе из сформулированных выше условий Устаревшие подходы к организации взаимного исключения
14 Поддержка взаимоисключения на уровне ОС Подходы к построению взаимного исключения, перечисленные ранее, характерны наличием активного ожидания такого состояния процесса, при котором он в ожидании момента, когда можно будет войти в критическую секцию, вынужден постоянно опрашивать определенные переменные в разделяемой памяти, при этом не выполняя никаких полезных действий, но занимая время центрального процессора. Чтобы процесс, ожидающий входа в критическую секцию, не расходовал попусту процессорное время, следует, очевидно, заблокировать его до тех пор, пока нужные ему разделяемые ресурсы не окажутся свободны, то есть не выделять ему квантов времени до освобождения ресурсов. С другой стороны, в момент освобождения ресурсов процесс необходимо «разбудить», то есть перевести из состояния блокировки в состояние готовности; желательно при этом снова пометить разделяемые ресурсы как занятые, чтобы процессу не пришлось снова выдерживать конкурентный поединок с другими процессами за соответствующий ресурс. Мьютексы и семафоры
15 Блокировать процесс может только операционная система. Если бы процессу было точно известно, через какой промежуток времени нужный ему ресурс окажется освобожден, он мог бы выполнить системный вызов, подобный функции sleep(), чтобы отказаться от выполнения на заданный период. Однако момент освобождения нужного ресурса процессу не известен, т.к. зависит от функционирования других процессов. Получается, что управление пометками занятости/освобождения ресурсов следует возложить на операционную систему, создав еще один способ взаимодействия процессов. Следует отметить, что такой подход, кроме избавления от активного ожидания, имеет и другое важное преимущество. Операционная система, в отличие от процесса, имеет возможность при необходимости запрещать прерывания на время исполнения определенных действий внутри ядра, обеспечивая, таким образом, атомарность сколь угодно сложных операций Мьютексы и семафоры
16 Под мьютексом в общем случае понимается объект, имеющий два состояния (открыт/заперт) и, соответственно, две операции: lock() (запереть) и unlock() (открыть). Операция unlock() проходит успешно (и немедленно возвращает управление) в любом случае, переводя объект в состояние «открыт». Для операции lock() может быть два варианта: 1. Операция может быть реализована как булевская функция. При применении ее к открытому мьютексу она закрывает его и возвращает значение «истина» (успех). При применении к закрытому мьютексу функция возвращает значение «ложь» (неудача). Такой вариант реализации называется неблокирующим. 2. Операцию можно реализовать и как процедуру, не возвращающую никакого значения. В этом случае при применении ее к открытому мьютексу она закрывает его и возвращает управление; при применении ее к закрытому мьютексу она блокирует вызвавший процесс до тех пор, пока мьютекс не окажется открыт, после чего закрывает его и возвращает управление Мьютексы
17 Важнейшим свойством операций lock() и unlock() является их атомарность. Это означает, что обе операции выполняются как единое целое и не могут быть прерваны. В частности, операция lock() не может быть прервана между проверкой текущего значения мьютекса и изменением этого значения на «закрыто». Рассмотрим для начала неблокирующую реализацию lock(): while(!lock(s)) {} /* ждем, пока не удастся закрыть мьютекс */ section(); /*... работа с разделяемыми данными... */ unlock(s); /* разрешаем другим процессам доступ */ В отличие от варианта с использованием блокирующей переменной, данный вариант является корректным. Поскольку операция lock() атомарна, выход из цикла (по истинному значению функции lock()) означает, что в некий момент мьютекс оказался открыт (то есть никто в это время не работал с разделяемыми данными), и нашему процессу удалось его закрыть, причем никто другой вклиниться между проверкой и закрытием не мог. Мьютексы и семафоры
18 Если применить второй тип реализации операции lock() (при котором она блокируется до тех пор, пока не удастся закрыть открытый мьютекс), наш код станет еще проще, и из него исчезнет цикл активного ожидания: lock(s); /* ждем, пока не удастся закрыть мьютекс */ section(); /*... работа с разделяемыми данными... */ unlock(s); /* разрешаем другим процессам доступ */ Ясно, что такой мьютекс может быть реализован только при содействии ядра операционной системы: либо целиком как объект ядра, либо как набор операций, осуществляемых ядром над переменной, расположенной в пользовательском процессе. Отметим с другой стороны, что введение такого сравнительно простого сервиса в ядре ОС позволяет избавиться разом от всех недостатков рассмотренных выше подходов к взаимному исключению Мьютексы и семафоры
19 Предложенные Эдсгером Дейкстрой семафоры представляют собой обобщение понятия мьютекса. Семафор Дейкстры в классическом варианте представляет собой целочисленную переменную, про которую известно, что она принимает только неотрицательные значения, и над которой определены две операции: up() и down(). Операция up() всегда проходит успешно, увеличивая значение переменной на 1, и немедленно возвращает управление. Операция down() должна, наоборот, уменьшать значение на 1, но сделать это она может только в случае, если текущее значение строго больше нуля, ведь значение семафора не может быть отрицательным. Соответственно, при положительном значении семафора операция down() уменьшает его значение на 1 и немедленно возвращает управление. В случае же нулевого значения семафора операция блокирует вызвавший процесс до тех пор, пока значение не станет положительным, после чего уменьшает значение и возвращает управление Мьютексы и семафоры Семафоры Дейкстры
20 Как и в случае с мьютексом, операции над семафором обязательно должны быть реализованы атомарно: необходимо полностью исключить ситуации, когда результат нескольких независимых операций над семафором может зависеть от времени вызова операций процессами (как это происходило в примере, приведенном в начале лекции). Семафор, как и мьютекс, может быть реализован целиком как объект ядра, либо как набор операций ядра над переменной из памяти пользовательского процесса. Ясно, что с помощью семафора можно имитировать функционирование мьютекса, если считать значение 0 состоянием «закрыт», значение 1 состоянием «открыт», а операции lock() и unlock() заменить на down() и up(). Необходимо только следить, чтобы операция up() никогда не применялась к положительному семафору, а этого можно достичь, если применять операцию up() только в начале программы (один раз), а также после того, как прошла операция down(), и никогда иначе. Однако семафор можно использовать в более сложной роли, а именно, как счетчик доступных ресурсов Мьютексы и семафоры
21 Мьютексы и семафоры
Еще похожие презентации в нашем архиве:
© 2024 MyShared Inc.
All rights reserved.