Лекция 5.2. Аппаратные средства поддержки проектирования и отладки систем реального времени. 162 8 страница

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

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

Каждому набору критических данных ставится в соответствие двоич­ная переменная, которой поток присваивает значение 0, когда он входит в критическую секцию, и значение 1, когда он ее покидает.

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

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

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

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

3. Семафоры

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

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

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

Если несколько процессов ждут одного и того же семафора, то после выполнения операции signalтолько один из них может продолжить свое раз­витие. В зависимости от реализации процессы могут ждать в очереди, упоря­доченной либо по принципу FIFO (Firstln, FirstOut - первым вошел, первым вышел), либо в соответствии с приоритетами, или выбираться случайным об­разом.

Названия управляющей структуры "семафор" и операций signalи waitимеют очевидное мнемоническое значение. В литературе вместо signalи waitприменяются и другие названия с тем же самым функциональным смыслом.

С помощью семафоров проблема защиты ресурсов решается следую­щим образом:

programsem_example (* защита ресурса *)

varP1: semaphore

Begin

P1 := 1;

Cobegin

while true do(* бесконечный цикл *)

begin(* процесс А *)

wait(P1);

(* защищенный ресурс *)

signal(P1);

end; (* процесс А *)

while true do(* бесконечный цикл *)

begin(* процесс В *)

wait(Pl);

(* защищенный ресурс *)

signal(Pl);

end;(* процесс В *)

coend;

end. (* sem_example *)

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

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

wait(Pl) wait(P2)

wait(P2) wait(Pl)

… …

(* защищенный ресурс *) (* защищенный ресурс *)

… …

signal(Pl) signal(P2)

signal(P2) signal(Pl)

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

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

Process "Чтение данных" Process "Обработка данных"