СПО (Корнилов) / Лекции / вар2 / Операционные системы (Корнилов)
.pdfСПО Лекция 1 [23.12.04] |
31 |
Void V(); |
|
}; |
|
void Semaphore::P() { |
void Semaphore::V() { |
If(count>0) count--; |
if(queue не пуста) { |
else { |
//Один в очередь готовых |
//Поставить в очередь queue |
} |
// Позвать планировщик |
else |
} |
count++; |
} |
} |
Двоичный семафор: Начальное значение переменной семафора равно 1. Операция P ставит процесс в очередь, если значение переменной семафора 0, сбрасывает переменную в 0, если она равнялась 1. Операция V освобождает один процесс из очереди к семафору, если очередь не пуста. Если очередь пуста, то присваивает переменной семафора 1.
Применение семафора
Взаимное исключение
// Начальное значение счетчика семафора 1. P();
Критическая секция; V();
Ожидание некоторого события
P() – при нулевом начальном значении будет ждать, пока кто-то другой сообщит о наступлении события выполнив операцию V().
Задача о «производителе и потребителе»
Производитель передает данные потребителю через кольцевой буфер ограниченной емкости. Условия синхронизации:
1.Производитель должен ждать, если буфер полон.
2.Потребитель должен ждать, если буфер пуст.
3.Операции с буфером – критическая секция, т.е. работать с буфером может только один процесс. Правило большого пальца: число семафоров, необходимое для решения задачи равно числу условий синхронизации.
int size = 10;
СПО Лекция 1 [23.12.04] |
32 |
Semaphore mutex(1); |
|
Semaphore fill(0); |
|
Semaphore empty(size); |
|
void Producer() { |
Void Consumer() { |
// Подготовить data |
fill.P(); |
empty.P(); |
mutex.P(); |
mutex.P(); |
RemoveFromBuffer(&data); |
AddToBuffer(data); |
mutex.V(); |
mutex.V(); |
empty.V(); |
fill.V(); |
// Обработать data |
} |
} |
Вопросы:
•Важен ли порядок P операций?
•Важен ли порядок V операций?
•Нужны ли изменения для 2 производителей и 2 потребителей?
Реализация семафоров для многопроцессорных машин
P: |
V: |
|
// Запрет прерываний |
|
// Запрет прерываний |
while(test&set(v)==1); |
|
while(test&set(v)==1); |
if(count>0) { |
|
if(очередь пуста) { |
count--; |
|
count++; |
v = 0; |
|
} else { |
// разрешить прерывания |
// взять одного из очереди |
|
return; |
|
// и в очередь готовых |
} |
|
} |
// поставить в очередь |
|
v = 0; |
v = 0; |
|
// разрешить прерывания |
Вызвать планировщик |
|
|
Счетчик событий
Аналогичен семафорам, реализуется на основе следующих примитивов:
Read(E) |
- получить значение счетчика |
Advance(E) |
- Увеличить счетчик на 1 |
Wait(E,v) |
- ждать пока E < v |
СПО Лекция 1 [23.12.04] |
33 |
Лекция 7: Мониторы и условные переменные
9Монитор и условные переменные
9Задача о производителе и потребителе
Монитор и переменные условия
Семафоры – это большой шаг вперед в области синхронизации. Однако, их использование для взаимного исключения и для ожидания не всегда удобно, делает код трудно читаемым.
Мониторы предложены с целью разделить эти задачи и упростить программирование. Монитор содержит общие переменные, разделяемые процессами и функции доступа к общим переменным. Тело функции монитора является критической секцией и должно быть защищено от повторного входа. Другими словами, только один процесс может выполнять функцию монитора.
Первоначально (Хоар, Бринч Хансен) определяли монитор как конструкцию в языке программирования, при этом lock/unlock подставлялись в ходе компиляции соответственно в начале и на выходе из функций монитора. Во многих ОС OS/2, Windows NT, Solaris мониторы строятся на основе явного вызова примитивов взаимного.
Рассмотрим пример взаимодействия двух процессов – передача (прием) данных через кольцевой буфер.
class Buffer { int buf[N]; int head, tail public:
Buffer() { head = tail = 0; } void Put(int value);
void Get(int& value); };
void Buffer::Put(int value)
{
while( (tail+1)%N == head); // пока буфер полон - ждать
// или while( (tail+1)%N == head) Sleep(1); buf[tail] = value; // Разместить данные в буфере tail = (tail+1)%N;
}
void Buffer::Get(int& value)
{
while( tail == head); // пока буфер пуст - ждать
// или while( tail == head) Sleep(1);
value = buf[head]; // Взять данные из буфера head = (head +1)%N;
СПО Лекция 1 [23.12.04] |
34 |
}
Приведенное решение справедливо только для двух процессов: один передает данные, а другой принимает. Процессы используют разные функции и модифицируют разные переменные: один – head, а другой – tail. Если допускается существование нескольких передающих или нескольких читающих процессов, то операции чтения и записи должны быть защищены критической секцией. В определение класса необходимо добавить переменную MUTEX lock, а в начало и в конец функций Put и Get добавить соответственно захват (lock.Acquire()) и освобождение (lock.Release()) критической секции.
Однако, это решение не верно. Проблема в том, что ожидание внутри критической секции в данном случае приводит к блокировке. Например, если буфер пуст, то мы ждем в цикле while внутри критической секции и другой процесс не сможет выполнить функцию Put, поскольку критическая секция занята.
Для реализации ожидания в мониторах определены специальные переменные (переменныя условия – condition variables), с каждой связана очередь нитей ожидающих событие вне критической секции.
Для переменных условия определены функции: wait(suspend), signal(resume), broadcast.
• wait выход из критической секции, ожидание, возврат в критическую секцию
•signal будит одного ожидающего, если кто-то ждет
•broadcast будит всех ожидающих, если кто-то ждет
Получаем следующее решение: class Buffer {
MUTEX lock
CONDITION full,empty; int buf[N];
int head, tail public:
Buffer() { head = tail = 0; } void Put(int value);
void Get(int& value); };
void Buffer::Put(int value)
{
lock.Acquire();
while( (tail+1)%N == head) full.wait(lock); // ждать buf[tail] = value; // Разместить данные в буфере tail = (tail+1)%N;
empty.signal();
lock.Release();
}
СПО Лекция 1 [23.12.04] |
35 |
void Buffer::Get(int& value)
{
lock.Acquire();
while( tail == head) empty.wait(lock); // ждать value = buf[head]; // Взять данные из буфера head = (head +1)%N;
full.signal();
lock.Release();
}
Два стиля мониторов: Mesa vs. Hoar
Различие в том, как осуществляется возврат в критическую секцию после wait.
•MESA (реально применяется): после signal не происходит принудительного переключения контекста, разбуженный просто ставится в очередь готовых и должен вновь проверять право на вход в монитор!
•HOAR (описан в учебниках): после signal происходит принудительное переключения контекста, управление получает разбуженный процесс и только в момент его выхода из монитора процесс, выполнивший signal, получает возможность продолжить выполнение.
9Рассмотренный пример справедлив для обоих стилей. Если заменить while на if, то решение останется справедливым для монитора Хоара, но не будет работать для монитора стиля MESA.
9wait и signal не эквивалентны семафорным примитивам - нет счетчика, поэтому signal, выполненный раньше wait эквивалентен пустому оператору.
Сравнение семафоров и мониторов
Попробуем создать на основе семафоров все необходимые для мониторов конструкции. Во-первых с помощью семафора легко создается lock.
Как насчет такого?
Wait() { semaphore->P(); }
Signal() { semaphore->V(); }
Так грубо нельзя моделировать переменные условия, вспомним, что wait и signal вызываются из критической секции и такое решение может привести к блокировке.
Уточним.
Wait(Lock *lock) {
lock->Release();
semaphore->P();
СПО Лекция 1 [23.12.04] |
36 |
lock->Acquire();
}
Signal() { semaphore->V(); }
Переменные условий не имеют памяти, поэтому signal , когда никто не ждет эквивалентен пустому оператору. Последующий wait приведет к ожиданию. С семафорами это не так.
Можно ли решить проблему таким образом?
Signal() {
if(очередь семафора не пуста)
semaphore->V();
}
Нет. Во-первых не корректно смотреть на очередь семафора. Во-вторых, если остановиться в wait после освобождения lock , но до P операции, то сигнал не сработает.
В принципе проблема решается. Это для самостоятельного решения.
Поддержка синхронизации в языках программирования
Проблема в том, что программист должен расставить lock в начале функции монитора и release в каждой точке выхода.
C
Поддержки нет необходимо отследить все выходы из функций монитора.
C++
Более проблематично, так как язык поддерживает обработку исключительных ситуаций.
Java
Имеет явную поддержку для нитей и синхронизации.
class Shared { private int value;
public Shared(int initial){ value = initial;} public synchronized int get() { return value; }
public synchronized void add(int incr) { value += incr; }
}
СПО Лекция 1 [23.12.04] |
37 |
С объектом Shared связан lock, который захватывается и освобождается автоматически при входе и выходе из synchronized методов.
Кроме того, в Java имеется оператор synchronized с помощью которого можно защищать любой участок кода. Важно, что lock будет освобожден, даже если выход из этого участка кода происходит в результате exception.
synchronized (object) {
...
some_function();
...
}
Как организуется ожидание в синхронизированных методах?
void wait(long timeout);
void wait();
Как сигнализировать о наступлении события в синхронизированных методах?
void notify();
void notifyAll();
Заметим, что фактически, имеется одна условная переменная (неявная).
Разные Java машины могут вести себя по разному. Это обусловлено тем, что в языке не специфицирован механизм планирования и он может быть разным.
Передача сообщений
Семафоры и мониторы используются для синхронизации доступа к разделяемым переменным, но не имеют средств для передачи данных от одного к другому.
Механизм передачи сообщений позволяет организовать синхронизацию и взаимодействие процессов с разделенными адресными пространствами, как на одной так и на нескольких машинах.
•Сообщения – передаются в виде пакетов фиксированной длины адресату
Sender: |
Receiver: |
SendMessage(msg, msgsize, dest); |
ReceiveMessage(&msg) или |
SendMessage(msg, msgsize, mailbox); |
ReceiveMessage(&buf, mailbox) |
•Потоки – неструктурированный поток байтов от отправителя к адресату
Sender: |
Receiver: |
Handle = OpenConnection(destAddr); |
Handle = WaitForConnection(); |
Handle.write(buf, nBytes); |
While( !handle.eof() ) { |
. . . |
Handle.read(buf, nBytes); |
Handle.write(buf, nBytes); |
// Обработка |
Handle.Close(); |
} |
Множество разновидностей: однонаправленные и двунаправленные, с ожиданием и без ожидания, …
СПО Лекция 1 [23.12.04] |
38 |
Адресация:
•Прямая (threadID)
•Косвенная через «почтовый ящик». Допускает присвоение прав на чтение-запись в почтовый ящик
•Косвенная по имени сервиса. Принимающий процесс регистрирует себя в общедоступном списке сервисов. Передающий шлет сообщения в адрес сервиса.
Взаимодействие процессов на разных машинах
Особенность: потеря, искажение, дублирование.
Надежный поток:
•Open – устанавливает соединение, так что оба ожидают что-то друг о друга. Sender шлет пакет Open до тех пор пока не получит подтверждение об установлении соединения или пока не исчерпан счетчик попыток.
•Обе стороны ведут протокол переданных - принятых данных. В каждом пакете передается пересылаемый диапазон байтов.
•Sender буферизует переданные данные и держит их до тех пор, пока не получит подтверждение об их успешном приеме.
•Если подтверждение о приеме данных не приходит в течение установленного таймаута, то передача данных повторяется и так до тех пор, пока не исчерпан счетчик попыток.
•Receiver отслеживает принятые данные с тем, чтобы обнаружить потерю пакета или появление повторного пакета. В первом случае посылается отрицательное подтверждение, во втором - данные игнорируются и посылается положительное подтверждение.
•Receiver контролирует правильность приема данных по контрольной сумме.
•Любая сторона может закрыть поток послав пакет Close. Close должен подтверждаться и лишь после получения подтверждения можно уничтожать все что связано с соединением.
Надежные сообщения:
•В целом действия аналогичны потокам. Каждое сообщение снабжается порядковым номером для контроля потери и повтора сообщений.
•Нет Open – Close. Когда освобождать ресурсы? Когда истекло время необходимое на выполнение установленного числа попыток ретрансляции.
Всегда ли параллельные процессы дают выигрыш?
1988 Microsoft OS/2 – все не нитях (позже в IBM переписали ядро полностью). Нить – 9K стек. Реально до 90 нитей, из которых лишь несколько активны. Результат: около 1M дополнительной памяти. В те времена 1М памяти стоил примерно $200.
СПО Лекция 1 [23.12.04] |
39 |
Мораль: нити очень удобны, но не бесплатны
Лекция 8. Типовые задачи синхронизации. Примитивы синхронизации в
Win32 API
9Типовые задачи синхронизации
9Критическая секция
9Атомарные операции
9Объекты ядра Win32 API
Типовые задачи синхронизации
Задача о производителе и потребителе
Моделирует взаимодействие (обмен данными) между двумя процессами: один пишет в буфер, а другой читает из буфера.
Ограничения:
•«Производитель» должен ждать, если буфер полон.
•«Потребитель» должен ждать, если буфер пуст.
•Запись и чтение из буфера – критическая секция.
Задача о читателях и писателях
Моделирует обращение процессов к базе данных. «Читатели» не модифицирую базу данных и могут читать одновременно, если никто не пишет. «Писатель» модифицирует базу данных и может это делать только если никто не пишет и никто не читает.
Ограничения:
•«Читатель» должен ждать, пока кто-то пишет.
•«Писатель» должен ждать, пока кто-то пишет или кто-то читает.
•Модификация переменных состояния (счетчик читающих и т.п.) должно выполняться в критической секции.
Задача о Читателях и Писателях
Эта одна из модельных задач синхронизации, описывающих доступ процессов к разделяемой базе данных. Процессы подразделяются на две категории:
•Читатели никогда не модифицирую базу данных
•Писатели читают и модифицируют базу данных Правила игры таковы:
•Читатели могут работать параллельно, если никто из писателей не модифицирует базу данных
СПО Лекция 1 [23.12.04] |
40 |
•Модифицировать базу может только один писатель и когда никто не читает из базы данных
•Переменные состояния может просматривать и менять только один процесс
Схема решения
Читатель:
Ждать пока закончит работу писатель (если кто-то пишет) Читать из базы данных При необходимости разбудить ожидающего писателя
Писатель:
Ждать пока закончат работу все читатели (если кто-то читает) Модифицировать базу данных При необходимости разбудить ожидающих читателей
Переменные состояния:
AR – число активных (читающих из БД в данное время) читателей AW – число активных писателей
WR – число ждущих читателей
WW – число ждущих писателей Условные переменные:
ok_to_read – NULL ok_to_write – NULL lock - free
Решение
Reader() {
lock.Acquire();
while((AW+WW)>0) {
WR++; ok_to_read->Wait(&lock); WR--;
}
AR++;
lock.Release();
//====================== ACCESS DB //====================== lock.Acquire();
AR--;
if( AR==0 && WW>0) ok_to_write->Signal(&lock);
lock.Release();
}
