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

Реализация семафоров

Семафор, по существу, является структурой из двух полей – целого значения и указателя на список ждущих процессов:

typedef struct {

int value;

struct process * L;

} semaphore;

При реализации операций над семафором будем предполагать наличие в системе следующих простейших примитивов, и использовать их:

block - задерживает исполнение процесса, выполнившего эту операцию;

wakeup (P) – возобновляет исполнение приостановленного процессаP.

Определим семафорные операции следующим образом:

wait (S):

S.value--;

if (S.value < 0) {

добавление текущего процесса к S.L;

block;

}

signal (S):

S.value++;

if (S.value <= 0) {

удаление процесса P из S.L;

wakeup (P);

}

Семафоры как общее средство синхронизации

Наиболее простой вид синхронизации действий, выполняемых в двух процессах, - это исполнение действия B в процессе Pj после того, как действие A исполнено в процессе Pi . Рассмотрим, как такую синхронизацию осуществить с помощью семафоров.

Используем семафор flag, инициализированный 0.

Код процесса Pi:

. . .

A;

signal (flag);

Код процесса Pj:

. . .

wait (flag);

B;

Общие и двоичные семафоры

Из рассмотренного ясно, что имеется два вида семафоров: общий - целая переменная с теоретически неограниченным значением - и двоичный - целая переменная, значениями которой могут быть только 0 или 1. Преимуществом двоичного семафора является его возможная более простая аппаратная реализация. Например, в системах "Эльбрус" и Burroughs 5000 реализованы команды атомарного семафорного считывания с проверкой семафорного бита.

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

Вариант операции wait (S) для системных процессов ("Эльбрус")

Для системного процесса лишние прерывания нежелательны, и может оказаться важным удерживать процессор за собой некоторое время (например, для быстрого выполнения планирования и диспетчеризации процессов). С этой целью в системе "Эльбрус" реализованаа, в дополнение к операции ждать (S) русифицированной версии wait(S), операция жуж(S) - "жужжать" на процессоре, т.е. ждать на закрытом семафоре, но не прерываться и не отдавать процессор, пока семафор не будет открыт операцией открыть(S)

Реализация общего семафора с помощью двоичных семафоров

Общий семафор может быть представлен тройкой из двух двоичных семафоров и целой переменной:

binary-semaphore S1 = 1;

binary-semaphore S2 = 0;

int C = начальное значение общего семафора S;

Операция wait:

wait (S1);

C--;

if (C < 0) {

signal (S1);

wait (S2);

}

signal (S1);

Операция signal:

wait (S1);

C++;

if (C >= 0) {

signal (S2);

};

signal (S1);

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

Решение классических задач синхронизации с помощью семафоров

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

  • ограниченный буфер (bounded buffer problem)

  • читателиписатели (readers – writers problem)

  • - обедающие философы (dining philosophers problem).

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

Будем использовать три общих семафора:

semaphore full = n;

semaphore empty = 0;

semaphore mutex = 1;

Семафор full сигнализирует о переполнении буфера, empty – об исчерпании буфера, mutex – используется для взаимного исключения действий над буфером.

Код процесса-производителя имеет вид:

do {

. . .

сгенерировать элемент в nextp

. . .

wait (full);

wait (mutex);

. . .

добавить nextp к буферу

. . .

signal (mutex);

signal (empty);

} while (1);

Код процесса-потребителя:

do {

wait (empty);

wait (mutex);

. . .

взять и удалить элемент из буфера в nextc

. . .

signal (mutex);

signal (full);

. . .

использовать элемент из nextc

. . .

} while (1);

Поясним использование семафоров. Семафор mutex используется "симметрично"; над ним выполняется пара операций: wait … signal – семафорные скобки. Его роль – чисто взаимное исключение критических секций. Семафор empty сигнализирует об исчерпании буфера. В начале он закрыт, так как элементов в буфере нет. Поэтому при закрытом семафоре empty потребитель вынужден ждать. Открывает семафор empty производитель, после того, как он записывает в буфер очередной элемент. Семафор full сигнализирует о переполнении буфера. В начале он равен n – максимальному числу элементов в буфере. Производитель перед записью элемента в буфер выполняет операцию wait (full), гарантируя, что, если буфер переполнен, записи нового элемента в буфер не будет. Открывает семафор full потребитель, после того, как он освободил очередной элемент буфера.

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

Решение с помощью семафоров задачи "читатели – писатели"

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

Будем использовать два семафора и целую переменную:

semaphore mutex = 1;

semaphore wrt = 1;

int readcount = 0;

Семафор mutex используется читателями для взаимного исключения операций над переменной readcount (счетчиком читателей). Семафор wrt используется для взаимного исключения писателей.

Реализация процесса-писателя особых комментариев не требует:

wait (wrt);

. . .

изменение ресурса

. . .

signal (wrt);

Реализация процесса-читателя несколько более сложна:

wait (mutex);

readcount++;

if (readcount == 1) {

wait (wrt);

}

signal (mutex);

. . .

чтение ресурса

. . .

wait (mutex);

readcount--;

if (readcount == 0) {

signal (wrt);

}

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

Решение с помощью семафоров задачи "обедающие философы"

Суть задачи обедающие философы в следующем. Имеется круглый стол, за которым сидят пять философов (впрочем, их число принципиального значения не имеет – для другого числа философов решение будет аналогичным). Перед каждым из них лежит тарелка с едой, слева и справа от каждого – две китайские палочки. Философ может находиться в трех состояниях: проголодаться, думать и обедать. На голодный желудок философ думать не может. Но начать обедать он может, только если обе палочки слева и справа от него свободны. Требуется синхронизировать действия философов. В данном случае общим ресурсом являются палочки. Иллюстрацией условий задачи является рис. 12.1.

Рис. 12.1.  Задача "обедающие философы".

Данная задача является одной из излюбленных задач профессора Ч. Хоара, который использовал ее, как пример в своих работах по параллельному программированию.

При решении задачи будем использовать массив семафоров chopstick, описывающий текущее состояние палочек: chopstick[i] закрыт, если палочка занята, открыт – если свободна:

semaphore chopstick [5] = { 1, 1, 1, 1, 1};

Алгоритм реализации действий философа i имеет вид:

do {

wait (chopstick [i]); /* взять левую палочку */

wait (chopstick [(I + 1) % 5]); /* взять правую палочку */

. . .

обедать

. . .

signal (chopstick [i]); /* освободить левую палочку */

signal (chopstick [(i+1) % 5]); /* освободить правую палочку */

. . .

думать

. . .

while (1);

Решение данной задачи с помощью семафоров оказывается особенно простым и красивым.

Критические области

Критические области (critical regions) – более высокоуровневая и надежная конструкция для синхронизации, чем семафоры. Общий ресурс описывается в виде особого описания переменной:

v: shared T

Доступ к переменной v возможен только с помощью специальной конструкции:

region v when B do S

где v – общий ресурс; B – булевское условие, S – оператор (содержащий действия над v).

Семантика данной конструкции следующая. Пока B ложно, процесс, ее исполняющий, должен ждать. Когда B становится истинным, процесс получает доступ к ресурсу v и выполняет над ним операции S. Пока исполняется оператор S, больше ни один процесс не имеет доступа к переменной v.

Решение с помощью критических областей задачи "ограниченный буфер"

Опишем буфер как структуру:

struct buffer {

int pool [n];

int count, in, out

}

buf: shared buffer;

Алгоритм процесса-производителя:

region buf when (count < n) {

pool [in] = nextp;

in = (in+1) % n;

count++;

}

Заметим, что проверка переполнения буфера выполняется при входе в конструкцию region, и потребитель ждет, пока в буфере освободится хотя бы один элемент.

Алгоритм процесса-потребителя:

region buf when (count > 0) {

nextc = pool [out];

out = (out+1) % n;

count--;

}

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

Схема реализации критических областей с помощью семафоров

Будем использовать для реализации конструкции region x when B do S следующие семафоры и целые переменные:

semaphore mutex, first-delay, second-delay;

int first-count, second-count;

Семафор mutex используется для взаимного исключения доступа к критической секции S. Семафор first-delay используется для ожидания процессов, которые не могут войти в критическую секцию S, так как условие B ложно. Число таких процессов хранится в переменной first-count. Семафор second-delay используется для ожидания тех процессов, которые вычислили условие B один раз и ожидают, когда им будет позволено повторно вычислить B (second-count – счетчик таких процессов). Реализация предоставляется студентам в качестве упражнения.

Мониторы

Конструкция монитор предложена в 1974 г. Ч. Хоаром [18]. Она является более высокоуровневой и более надежной конструкцией для синхронизации, чем семафоры.

Описание монитора имеет вид:

monitor monitor-name

{

описания общих переменных

procedure body P1 ( … ) {

. . .

}

procedure body P2 ( … ) {

. . .

}

. . .

procedure body Pn( … ) {

. . .

}

{

код инициализации монитора

}

}

Монитор является многовходовым модулем особого рода. Он содержит описания общих для нескольких параллельных процессов данных и операций над этими данными в виде процедур P1, …, Pn. Пользователи монитора – параллельные процессы – имеют доступ к описанным в нем общим данным только через его операции, причем в каждый момент времени не более чем один процесс может выполнять какую-либо операцию монитора; остальные процессы, желающие выполнить операцию монитора, должны ждать, пока первый процесс закончит выполнять мониторную операцию.

По сути дела, концепция монитора явилась развитием предложенной также Ч. Хоаром концепции абстрактного типа данных (АТД)– определения типа данных как совокупности описания его конкретного представления и абстрактных операций над ним (в виде процедур). Концепция монитора добавляет к АТД возможность синхронизации процессов по общим данным.

Для реализации ожидания внутри монитора по различным условиям, вводятся условные переменные (condition variables) – переменные с описаниями вида condition x,y, доступ к которым возможен только операциями wait и signal: например, x.wait(); x.signal(). Операция x.wait() означает, что выполнивший ее процесс задерживается до того момента, пока другой процесс не выполнит операцию x.signal(). Операция x.signal() возобновляет ровно один приостановленный процесс. Если приостановленных процессов нет, она не выполняет никаких действий.

Схематическое изображение монитора приведено на рис. 12.2.

Рис. 12.2.  Схематическое изображение монитора.

Схема монитора с условными переменными приведена на рис. 12.3.

Рис. 12.3.  Монитор с условными переменными.

Решение задачи "обедающие философы" с помощью мониторов

Реализуем решение задачи "обедающие философы" (см. Решение с помощью семафоров задачи "обедающие философы") с помощью монитора. Для каждого философа определим состояния (голодный, обедает, думает), и для их хранения будем использовать массив state. Для управления переходом философа из состояния в состояние используем массив условных переменных self. Для каждого философа определим операции pickup – взять палочку; putdown – освободить палочку; test – проверить состояние философа и, если это возможно и если он голоден, перевести его в состояние eating.

Код монитора, реализующего решение задачи:

monitor dp

{

enum {thinking, hungry, eating} state [5];

condition self [5];

void pickup (int i) {

state [i] = hungry;

test (i);

if (state[i] != eating) {

self[i].wait ();

}

} /* pickup */

void putdown (int i) {

state [i] = thinking;

test ((i+4) % 5));

test ((i-1) % 5));

/* когда палочка свободна, проверить соседей */

} /* putdown */

void test (int i) {

if (state((i+4)%5) != eating &&

state [i] = hungry &&

state((i+1)%5) != eating)) {

state[i] = eating;

self[i].signal;

void init () {

for (int i = 0; i < 5; i++) {

state[i] = thinking;

}

}

Реализация мониторов с помощью семафоров

Используем семафоры mutex – для взаимного исключения процессов, next – для реализации очереди входа в монитор; переменную next-count – счетчик процессов в очереди на вход:

semaphore mutex = 1;

semaphore next = 0;

int next-count = 0;

Каждую внешнюю процедуру монитора F реализуем следующим кодом:

wait (mutex);

. . .

тело F;

. . .

if (next-count > 0) {

signal next;

} else {

signal mutex;

}

Таким образом, будет обеспечено взаимное исключение внутри монитора.

Каждую условную переменную x реализуем следующим образом:

semaphore x-sem = 0;

int x-count = 0;

Реализация операции x.wait():

x-count++;

if (next-count > 0) {

signal (next);

} else {

signal (mutex);

}

wait(x-sem);

x-count--;

Реализация операции x.signal():

if (x-count > 0) {

next-count++;

signal (x-sem);

wait (next);

next-count--;

}

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

Дополнительная операция над монитором, обеспечивающая организацию очереди к условной переменной по приоритетам, - x.wait(с), где c – целочисленный параметр, играющий роль приоритета. При выполнении операции signal первым будет освобожден из очереди процесс с меньшим значением приоритета.

При реализации монитора необходимо проверять следующие условия:

  • процессы должны выполнять вызовы операций монитора в правильной последовательности, своевременно вызывая все семафорные операции;

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

Синхронизация в ОС Solaris

Система Solaris предоставляет разнообразные виды блокировщиков для поддержки многозадачности, многопоточности (включая потоки реального времени) и мультипроцессирования. Используются адаптивные мюьтексы (adaptive mutexes) – эффективное средство синхронизации доступа к данным при их обработке короткими сегментами кода. Для более длинных сегментов кода используются условные переменные и блокировщики читателей-писателей (reader-writer locks; rwlocks). Для синхронизации потоков используются "вертушки" (turnstiles) – синхронизирующие примитивы, которые позволяют использовать либо adaptive mutex, либо rwlock.

Синхронизация в Windows 2000

Для защиты доступа к данным на однопроцессорных системах используются маски прерываний. Для многопроцессорных систем используются spinlocks («вертящиеся замки»). В системе реализованы также объекты-диспетчеры, которые могут функционировать как мьютексы и как семафоры. Объекты-диспетчеры генерируют события, семантика которых аналогична семантике условной переменной.

Ключевые термины

Interleaving – одновременное выполнение нескольких машинных команд, работающих с общими данными.

Абстрактный тип данных (АТД) – определение типа данных как совокупности описания его конкретного представления и абстрактных операций над ним.

Адаптивный мюьтекс (adaptive mutex) – эффективное средство синхронизации доступа к данным при их обработке короткими сегментами кода в операционной системе Solaris.

Алгоритм булочной (bakery algorithm) – алгоритм синхронизации процессов (Л. Лампорт), основанный на присвоении каждому процессу уникального номера в очереди (приоритета).

Блокировщик читателей-писателей (reader-writer lock; rwlock) – средство синхронизации в ОС Solaris для поддержки схем синхронизации типа "читатели-писатели".

"Вертушка" (turnstile) – синхронизирующий примитив в ОС Solaris, который позволяет использовать для синхронизации, при необходимости, либо адаптивный мьютекс, либо блокировщик читателей-писателей.

"Вертящийся замок" (spinlock) - средство синхронизации в ОС Windows 2000, используемое в многопроцессорных системах.

Взаимное исключение – режим совместного выполнения процессов, при котором, если некоторый процесс исполняет свою критическую секцию, то никакой другой процесс не должен в этот момент исполнять свою.

жуж - В системе "Эльбрус": "жужжать" на процессоре; Специализированная операция (для системных процессов) ожидания на закрытом семафоре без прерываний; занятие процессора, пока семафор не будет открыт операцией открыть (S).

Конкуренция за общие данные (race condition) - ситуация, при которой взаимодействующие процессы могут параллельно (одновременно) обращаться к общим данным без какой-либо синхронизации.

Критическая область (critical region) – высокоуровневая конструкция для синхронизации, основанная на описаниях разделяемых (shared) ресурсов и конструкции region, обеспечивающей взаимное исключение при работе с общим ресурсом.

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

Обедающие философы (dining philosophers) – классическая задача синхронизации следующего содержания: имеется круглый стол, за которым пять философов. Перед каждым из них тарелка с едой, слева и справа – две китайские палочки. Философ может находиться в трех состояниях: проголодаться, думать и обедать. На голодный желудок философ думать не может. Начать обедать философ может, только если обе палочки слева и справа от него свободны.

Общий семафор (counting semaphore) - целая переменная S, над которой определены две атомарных семафорных операции wait (S) и signal (S).

Объект-диспетчер (dispatcher object) – средство синхронизации в ОС Windows 2000, которое может функционировать как мьютекс и как семафор; генерирует события, семантика которых аналогична семантике условной переменной.

Ограниченный буфер (bounded buffer): схема взаимодействия процессов, при которой имеются процесс-производитель и процесс-потребитель, взаимодействующие с помощью циклического буфера ограниченной длины; производитель генерирует элементы информации и записывает в буфер; потребитель использует информационные элементы из буфера и удаляет их.

Семафорный бит – В вычислительных комплексах Burroughs 5000 и "Эльбрус": особый бит слова, над которым выполняется команда семафорного считывания; по определенному значению бита (например, 1) происходит прерывание.

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

Условная переменная (condition variable) – часть конструкции монитор: Переменная с описанием вида condition x, доступ к которой возможен только операциями wait и signal; операция x.wait() задерживает выполнивший ее процесс до момента, пока другой процесс не выполнит операцию x.signal().

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

Соседние файлы в папке все лекции по ОС