Лекция 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 "Обработка данных"