
- •15. Планирование в системах реального времени: статический алгоритм rms
- •16. Планирование в системах реального времени: динамический алгоритм edf
- •17. Основные понятия межпроцессного взаимодействия
- •18. Взаимное исключение с активным ожиданием.
- •19. Примитивы межпроцессного взаимодействия. Проблема производителя и потребителя.
- •21. Мониторы.
19. Примитивы межпроцессного взаимодействия. Проблема производителя и потребителя.
Теперь рассмотрим некоторые примитивы взаимодействия процессов, которые блокируют работу, пока им не разрешается входить в критическую область, вместо напрасной траты времени центрального процессора. Представителями простейшей пары таких примитивов являются sleep и wakeup. Системный вызов sleep блокирует вызывающий его процесс, который приостанавливается до тех пор, пока его не активизирует другой процесс. Активизирующий вызов wakeup использует один аргумент — активизируемый процесс. Дополнительно и sleep и wakeup используют еще один аргумент, адрес памяти, используемой для соотнесения вызовов sleep с вызовами wakeup.
Задача производителя и потребителя
В качестве примера применения этих примитивов рассмотрим задачу производителя и потребителя (также известную как задача ограниченного буфера). Два процесса используют общий буфер фиксированного размера. Один из них, производитель, помещает информацию в буфер, а другой, потребитель, извлекает ее оттуда. (Можно также расширить проблему до m производителей и n потребителей, но мы будем рассматривать только случай с одним производителем и одним потребителем, поскольку такое допущение упрощает решение.) Проблемы возникают в тот момент, когда производителю требуется поместить новую запись в уже заполненный буфер. Решение заключается в блокировании производителя до тех пор, пока потребитель не извлечет как минимум одну запись. Также, если потребителю нужно извлечь запись из буфера и он видит, что буфер пуст, он блокируется до тех пор, пока производитель не поместит что-нибудь в буфер и не активизирует этого потребителя. На первый взгляд этот подход выглядит достаточно простым, но он приводит к той же разновидности состязательной ситуации, которая нам уже встречалась в примере с каталогом спулера. Для отслеживания количества записей в буфере нам потребуется переменная count. Если максимальное количество записей, которое может содержаться в буфере, равно N, то программа производителя должна сначала проверить, не имеет ли count значение N. Если переменная имеет такое значение, производитель должен заблокировать свое выполнение, а если не имеет, производитель должен добавить запись и увеличить показание счетчика count .
Программа потребителя работает схожим образом: сначала проверяет, не является ли значение count нулевым. Если оно равно нулю, процесс блокирует свое выполнение, а если оно не равно нулю, он извлекает запись и уменьшает значение счетчика. Каждый из процессов также проверяет, не нужно ли активизировать другой процесс, и если нужно, проводит эту активизацию. Программы производителя и потребителя показаны в листинге 2.5. Чтобы выразить вызовы sleep и wakeup на языке С, мы покажем их в виде вызовов библиотечных процедур. Они не являются частью стандартной библиотеки С, но, по-видимому, были бы доступны на любой системе, имеющей эти системные вызовы.
Не показанные в листинге процедуры insert_item и remove Jitem занимаются помещением записей в буфер и извлечением их оттуда. Вернемся теперь к состязательной ситуации. Причиной ее появления может стать свободный доступ к счетчику count. Может сложиться следующая ситуация. Буфер пуст, и потребитель только что считал показания count, чтобы увидеть, что его значение равно 0. В этот самый момент планировщик решает временно приостановить выполнение процесса потребителя и возобновить выполнение процесса производителя. Производитель помещает запись в буфер, увеличивает значение четчика count, и замечает, что теперь оно равно 1. Приняв во внимание, что только что счетчик имел нулевое значение, при котором потребитель должен находиться в заблокированном состоянии, производитель вызывает процедуру makeup, чтобы активизировать выполнение процесса потребителя.
К сожалению, с точки зрения логики программы потребитель не находился в бездействующем состоянии, поэтому сигнал на активизацию будет утрачен. Когда подойдет очередь возобновить выполнение процесса потребителя, он проверит ранее считанное значение счетчика, определит, что оно было равно 0, и снова перейдет в заблокированное состояние. Рано или поздно производитель заполнит буфер и тоже перейдет в заблокированное состояние. И оба процесса впадут в вечную спячку. Суть возникшей проблемы заключается в утрате вызова wakeup в отношении процесса, который не находится в состоянии блокировки по собственной воле. Если бы этот вызов не утрачивался, то все бы работало должным образом. Быстро устранить проблему позволит изменение правил за счет добавления к картине событий бита ожидания активизации. Этот бит устанавливается, когда в отношении процесса, который не находится в состоянии бездействия, вызывается процедура wakeup. Затем, когда процесс попытается заблокироваться при установленном бите ожидания активизации, этот бит снимается, но процесс не блокируется. Бит ожидания активизации становится своеобразной копилкой, хранящей сигналы активизации. Хотя в нашем простом примере бит ожидания активизации спасает положение, нетрудно создать такие примеры, где фигурируют три и более процесса, для которых одного бита ожидания активизации явно недостаточно. Можно внести другие правки и добавить второй бит ожидания активизации, а может быть 8 или 32 таких бита, но, в принципе, проблема останется нерешенной.
20. Семафоры. Мьютексы.
В 1965 году Дейкстра предложил использовать целочисленную переменную для подсчета количества активизаций, отложенных на будущее. Он предложил учредить новый тип переменной — семафор (semaphore). Значение семафора может быть равно 0, что будет свидетельствовать об отсутствии сохраненных активизаций или иметь какое-нибудь положительное значение, если ожидается не менее одной активизации. Дейкстра предложил использовать две операции, down и up (обобщения sleep и wakeup соответственно). Операция down выясняет, отличается ли значение семафора от 0. Если отличается, она уменьшает это значение на 1 (то есть использует одну сохраненную активизацию) и продолжает свою работу. Если значение равно 0, процесс приостанавливается, не завершая в этот раз операцию down. И проверка значения, и его изменение, и, возможно, приостановка процесса осуществляются как единое и неделимое атомарное действие. Тем самым гарантируется, что с началом семафорной операции никакой другой процесс не может получить доступ к семафору до тех пор, пока операция не будет завершена или заблокирована. Атомарность является абсолютно необходимым условием для решения проблем синхронизации и исключения состязательных ситуаций. Атомарные действия, в которых группа взаимосвязанных операций либо выполняется без каких-либо прерываний, либо вообще не выполняется, приобрели особую важность и во многих других областях информатики.
Операция up увеличивает значение, адресуемое семафором, на 1. Если с этим семафором связаны один или более приостановленных процессов, способных завершить ранее начатые операции down, система выбирает один из них (к примеру, произвольным образом) и позволяет ему завершить его операцию down. Таким образом, после применения операции up в отношении семафора, с которым были связанны приостановленные процессы, значение семафора так и останется нулевым, но количество приостановленных процессов уменьшится на 1. Операция увеличения значения семафора на 1 и активизации одного из процессов также является неделимой. Ни один из процессов не может быть заблокирован при выполнении операции up, ровно как ни один из процессов не может быть заблокирован при выполнении wakeup в предыдущей модели. В первоначальном варианте своей работы Дейкстра вместо down и up использовал имена Р и V, Proberen (пытаться) и Verhogen (поднимать выше), вместо этих терминов употребляют down и up. Впервые они были представлены в языке программирования Algol 68. (можно так же почитать Решение задачи производителя-потребителя с помощью семафоров стр. 163)
Мьютексы
Иногда, при невостребованности возможностей семафоров в качестве счетчиков, используется их упрощенная версия, называемая мьютексом. Мьютексы справляются лишь с управлением взаимным исключением доступа к общим ресурсам или фрагментам кода. Простота и эффективность реализации мьютексов делает их особенно полезными для совокупности потоков, целиком реализованных в пользовательском пространстве. Мьютекс — это переменная, которая может находиться в одном из двух состояний: в заблокированном или незаблокированном. Следовательно, для их представления нужен только один бит, но на практике зачастую используется целое число, при этом нуль означает незаблокированное, а все остальные значения заблокированное состояние. Для работы с мьютексами используются две процедуры. Когда
потоку (или процессу) необходим доступ к критической области, он вызывает процедуру mutex_lock. Если мьютекс находится в незаблокированном состоянии (означающем доступность входа в критическую область), вызов проходит удачно и вызывающий поток может свободно войти в критическую область. С другой стороны, если мьютекс уже заблокирован, вызывающий поток блокируется до тех пор, пока поток, находящийся в критической области, не завершит свою работу и не вызовет процедуру mutex_unlock. Если на мьютексе заблокировано несколько потоков, то произвольно выбирается один из них, которому разрешается воспользоваться заблокированностью других потоков. Благодаря исключительной простоте мьютексов они легко могут быть реализованы в пользовательском пространстве при условии доступности команды TSL или команды XCHG.