СПО (Корнилов) / Лекции / вар2 / Операционные системы (Корнилов)
.pdfСПО Лекция 1 [23.12.04] |
51 |
if (hMutex == NULL)
printf("CreateMutex error: %d\n", GetLastError() );
else
if ( GetLastError() == ERROR_ALREADY_EXISTS ) printf("CreateMutex opened existing mutex\n");
else
printf("CreateMutex created new mutex\n");
// Процесс В: открывает доступ к существующему объекту.
HANDLE hMutex;
hMutex = OpenMutex( MUTEX_ALL_ACCESS, // доступ без ограничений
FALSE, |
// |
описатель |
не наследуемый |
"NameOfObject"); |
// |
имя объекта |
|
if (hMutex == NULL) |
|
|
); |
printf("OpenMutex error: %d\n", GetLastError() |
|||
Использование объекта mutex
Можно использовать mutex для защиты общего ресурса от одновременного доступа многими нитями. Каждая нить должна ждать освобождения mutex прежде, чем выполнять код, в котором осуществляется доступ к общему ресурсу. Например, если несколько нитей, обращаются к базе данных, нити могут использовать mutex, чтобы разрешить только одной нити в любой момент времени модифицировать базу данных.
BOOL WriteToDatabase(HANDLE hMutex)
{
DWORD dwWaitResult;
dwWaitResult = WaitForSingleObject( hMutex, 5000L);
switch (dwWaitResult)
{
case WAIT_OBJECT_0: // нить владеет объектом mutex. //***********************
//Запись в базу данных. //***********************
//Освобождаем mutex.
if (! ReleaseMutex(hMutex)) { // Обработка ошибки.
}
break;
}
case WAIT_TIMEOUT: // Истекло время ожидания (5 сек). return FALSE;
}
return TRUE;
}
Семафор
Объект семафор - объект синхронизации, который поддерживает счетчик между нулем и указанным максимальным значением. Состояние семафора устанавливается «сигнализированным», когда счетчик больше нуля, и «несигнализированным», когда счетчик нулевой.
Нить использует функцию CreateSemaphore, чтобы создать объект. Нити в других процессах могут открывать дескриптор к существующему объекту семафора, определяя его имя в вызове OpenSemaphore. Нити, которые ожидают семафор, помещаются в очередь в порядке поступления.
СПО Лекция 1 [23.12.04] |
52 |
Каждый раз при возврате из wait функции счетчик семафора уменьшен на один. Функция ReleaseSemaphore увеличивает счетчик семафора на указанное количество. Счетчик никогда не может быть меньше нуля или больше максимального значения.
В отличие от объекта mutex, функцию ReleaseSemaphore может использовать любая нить, чтобы увеличить счетчик семафора.
Использование семафора
В следующем примере, процесс использует семафор для ограничения числа создаваемых окон.
HANDLE hSemaphore;
LONG cMax = 10;
LONG cPreviousCount;
// Create a semaphore with initial and max. counts of 10.
hSemaphore = CreateSemaphore(
NULL, |
// no security attributes |
||
cMax, |
// initial count |
||
cMax, |
// |
maximum |
count |
NULL); |
// |
unnamed |
semaphore |
if (hSemaphore == NULL) { // Check for error. }
// Try to enter the semaphore gate.
dwWaitResult = WaitForSingleObject( hSemaphore, 0L);
switch (dwWaitResult) {
case WAIT_OBJECT_0: // The semaphore object was signaled. // OK to open another window.
break;
case WAIT_TIMEOUT: // А time-out occurred. // Cannot open another window.
break;
}
// When thread closes a window, increment the count of the semaphore.
if (!ReleaseSemaphore(
hSemaphore, |
// handle to semaphore |
|||
1, |
// |
increase count |
by |
one |
NULL) ) |
// |
not interested |
in |
previous count |
{
// Deal with the error.
}
Событие
Объект события - объект синхронизации, состояние которого может быть явно установлено сигнализированным при помощи функции SetEvent или PulseEvent. Существуют два типа событий.
События с ручным сбросом. События, состояние которых остается сигнализированным, пока явно не сброшено вызовом функцией ResetEvent. В то время как состояние объекта сигнализировано, любое число ожидающих нитей могут быть освобождены.
События с автоматическим сбросом. События, состояние которых остается «сигнализирован» до момента выполнения функции wait, в это время система автоматически сбрасывает состояние «сигнализирован». Если никакие нити не ожидают данное событие, то его состояние остается «сигнализирован».
Нить использует функцию CreateEvent, чтобы создать объект при этом определяется начальное состояние объекта и тип события (с ручным или автоматическим сбросом). Можно также определить имя объекта,
СПО Лекция 1 [23.12.04] |
53 |
тогда нити в других процессах могут открывать дескриптор к существующему объекту в вызове функции
OpenEvent.
Нить может использовать функцию PulseEvent, чтобы установить состояние « сигнализирован » и затем сбросить его после завершения функции wait. Для событий с ручным сбросом, все ожидающие нити освобождаются. Для событий с автоматическим сбросом, освобождается только одна нить, даже если это событие ожидают несколько нитей. Если никто не ждет события, то PulseEvent не имеет никаких последствий.
Использование событий
Объект события используется для посылки сигнала, указывающего, что специфическое событие произошло. Например, в асинхронных операциях вводе и выводе, система устанавливает событие, которое сигнализирует завершение операции.
Пример. Событие используется для предотвращения чтения из общего буфера одновременно с записью в него. Сначала, главная нить создает объект события с ручным сбросом. Главная нить сбрасывает состояние «сигнализирован» непосредственно перед записью в буфер и затем (после записи) в переводит объект в состояние «сигнализирован». Главная нить создает несколько читающих нитей и по одному объекту события с авто сбросом для каждой нити. Каждая читающая нить устанавливает событие в сигнализированное состояние, после каждого чтения из буфера.
#define NUMTHREADS 4
HANDLE hWriteEvent;
void CreateEventsAndThreads(void)
{
HANDLE hReadEvents[NUMTHREADS], hThread; DWORD i, IDThread;
//Create a manual-reset event object. The master thread sets
//this to nonsignaled when it writes to the shared buffer.
hWriteEvent = CreateEvent(
NULL, |
// no security attributes |
|
TRUE, |
// manual-reset event |
|
TRUE, |
// |
initial state is signaled |
"WriteEvent" |
// |
object name |
); |
|
|
if (hWriteEvent == NULL) { // error exit }
//Create multiple threads and an auto-reset event object
//for each thread. Each thread sets its event object to
//signaled when it is not reading from the shared buffer.
for(i = 1; i <= NUMTHREADS; i++)
{
// Create the auto-reset event. hReadEvents[i] = CreateEvent(
NULL, |
// no security attributes |
||
FALSE, |
// auto-reset |
event |
|
TRUE, |
// |
initial state is signaled |
|
NULL); |
// |
object not |
named |
if (hReadEvents[i] == NULL) { // Error exit. }
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) ThreadFunction, &hReadEvents[i], // pass event handle 0, &IDThread);
if (hThread == NULL) { // Error exit. }
}
}
VOID WriteToBuffer(VOID)
{
DWORD dwWaitResult, i;
// Reset hWriteEvent to nonsignaled, to block readers.
СПО Лекция 1 [23.12.04] |
54 |
if (! ResetEvent(hWriteEvent) ) { // Error exit. }
// Wait for all reading threads to finish reading.
dwWaitResult = WaitForMultipleObjects(
NUMTHREADS, |
// number of handles in array |
||
hReadEvents, |
// array of read-event handles |
||
TRUE, |
// |
wait until |
all are signaled |
INFINITE); |
// |
indefinite |
wait |
switch (dwWaitResult)
{
case WAIT_OBJECT_0: // All read-event objects were signaled. // Write to the shared buffer.
break;
default: // An error occurred.
printf("Wait error: %d\n", GetLastError()); ExitProcess(0);
}
// Set hWriteEvent to signaled.
if (! SetEvent(hWriteEvent) ) { // Error exit. }
}
VOID ThreadFunction(LPVOID lpParam)
{
DWORD dwWaitResult, i; HANDLE hEvents[2];
hEvents[0] = |
(HANDLE) *lpParam; // thread's read event |
||
hEvents[1] = |
hWriteEvent; |
|
|
dwWaitResult |
= WaitForMultipleObjects( |
in array |
|
2, |
|
// number of handles |
|
hEvents, |
|
// array of event handles |
|
TRUE, |
|
// wait till all are |
signaled |
INFINITE); |
// indefinite wait |
|
|
switch (dwWaitResult)
{
case WAIT_OBJECT_0: // Both event objects were signaled. // Read from the shared buffer.
break;
default: // An error occurred.
printf("Wait error: %d\n", GetLastError()); ExitThread(0);
}
// Set the read event to signaled.
if (! SetEvent(hEvents[0]) ) { // Error exit. }
}
Ожидаемый таймер
"Ожидаемый" таймер - объект синхронизации, состояние которого становится «сигнализирован», когда наступает указанное время. Имеются два типа ожидаемых таймеров: со сбросом вручную и синхронизирующие. Таймер любого типа может также быть периодическим.
Таймер с ручным сбросом. Таймер, состояние которого остается «сигнализирован» до вызова SetWaitableTimer, который устанавливает новое время срабатывания.
Синхронизирующий таймер. Таймер, сигнализированное состояние которого снимается wait функцией.
Периодический таймер. Таймер, состояние которого становится сигнализированным каждый раз, когда истекает установленный период. Периодический таймер является или периодическим таймером с ручным сбросом или периодическим синхронизирующим таймером.
СПО Лекция 1 [23.12.04] |
55 |
Нить использует функцию CreateWaitableTimer, чтобы создать объект таймера. Нить может определить имя для таймера. Нити в других процессах могут открывать дескриптор на существующий таймер, определяя имя в вызове к функции OpenWaitableTimer.
HANDLE CreateWaitableTimer(
LPSECURITY_ATTRIBUTES lpTimerAttributes, // pointer to security attributes
BOOL bManualReset, |
// |
flag for manual reset state |
LPCTSTR lpTimerName |
// |
pointer to timer object name |
); |
|
|
Нить вызывает функцию SetWaitableTimer, чтобы инициировать таймер.
BOOL SetWaitableTimer( |
// handle to a timer object |
|
HANDLE hTimer, |
||
const LARGE_INTEGER *pDueTime, |
// when timer will become signaled |
|
LONG lPeriod, |
// periodic |
timer interval |
PTIMERAPCROUTINE pfnCompletionRoutine, // completion routine |
||
LPVOID lpArgToCompletionRoutine, // data for |
completion routine |
|
BOOL fResume |
// flag for |
resume system state |
); |
|
|
Нить может использовать функцию CancelWaitableTimer, чтобы установить таймер в неактивное состояние. Чтобы сбрасывать таймер, вызывается SetWaitableTimer. Для удаления таймера используется вызов CloseHandle.
Неименованый канал (магистраль) - Anonymous pipe
Функция CreatePipe создает неименованный канал и возвращает два дескриптора: дескрипторы чтения и записи. Для взаимодействия по каналу, необходимо передать один из дескрипторов другому процессу. Обычно, это делается через механизм наследования; то есть процесс позволяет дескриптору быть унаследованным дочерним процессом.
BOOL CreatePipe( |
// pointer to read handle |
|
PHANDLE hReadPipe, |
||
PHANDLE hWritePipe, // pointer to write |
handle |
|
LPSECURITY_ATTRIBUTES lpPipeAttributes, |
// security attributes |
|
DWORD nSize |
// pipe buffer size |
|
); |
|
|
Чтобы читать из канала, используется дескриптор чтения в запросе ReadFile. ReadFile завершается, когда другой процесс записал данные в канал. Запрос ReadFile может также завершиться, если все дескрипторы записи к каналу были закрыты или если произошла ошибка чтения.
Чтобы записывать в канал, используется дескриптор записи в запросе к функции WriteFile. Запрос WriteFile завершается, когда записано указанное число байтов или происходит ошибка. Если буфер канала полон и еще есть байты для записи, WriteFile не завершается пока другой процесс не считает из канала и не освободит достаточное место для записи оставшихся байт.
Асинхронное чтение и запись не поддерживается для неименованных каналов. Неименованный канал существует до тех пор, пока не закрыты все его дескрипторы. Процесс может закрывать дескрипторы, используя функцию CloseHandle. Все дескрипторы канала закрываются, когда процесс заканчивается.
Наследование дескрипторов
•В параметрах функции CreatePipe в структуре SECURITY_ATTRIBUTES необходимо установить bInheritHandle равным TRUE, тогда дескрипторы, созданные CreatePipe, могут быть унаследованы.
•Функция CreateProcess должна разрешить наследование дескрипторов дочерними процессами.
Когда дочерний процесс наследует дескриптор канала он получает возможность обратиться к каналу. Обычно схема передачи дескрипторов дочернему процессу (для случая, когда дочерний процесс пишет в канал) выглядит следующим образом.
Процесс – родитель:
СПО Лекция 1 [23.12.04] |
56 |
1.Вызывае функцию GetStdHandle, чтобы получить текущий дескриптор стандартного вывода и сохраняет предыдущее значение.
2.Вызывает функцию SetStdHandle, чтобы переназначить стандартный вывод на дескриптор записи канала.
3.Создает дочерний процесс.
4.Вызывает функцию CloseHandle, чтобы закрыть дескриптор записи к каналу. (После того, как дочерний процесс наследует дескриптор записи, родительский процесс больше не нуждается в копии.)
5.Вызывает SetStdHandle, чтобы восстановить первоначальный дескриптор стандартного вывода.
6.Использует функцию ReadFile для получения данных от дочернего процесса.
Дочерний процесс:
1.Вызывае функцию GetStdHandle, чтобы получить текущий дескриптор стандартного вывода (дескриптор записи в канал).
2.Использует функцию WriteFile для передачи данных процессу – родителю.
Данные записываются в неименованный канал как поток байтов. Эти означает, что родительский процесс, читающий из канала не может различать сколько отдельных операций записи выполнено в дочернем процессе. Когда все дескрипторы записи к каналу закрыты, ReadFile возвращает нуль. Важно, чтобы родитель закрыл дескриптор записи канала, если это не выполнено, то операция ReadFile не сможет вернуть нуль, потому что родительский процесс имеет открытый дескриптор записи к каналу.
Именованый канал – Named Pipe
Каждый именованный канал имеет уникальное имя, которое отличает его от других именованных каналов в системе. Имя канала задается при вызове функции CreateNamedPipe. Канал может быть создан только на локальном компьютере. Клиент канала указывает имя, когда вызывают функцию CreateFile или CallNamedPipe, чтобы соединиться с существующим именованным каналом.
Имя задается в следующей форме: \\ServerName\pipe\PipeName
Здесь ServerName является именем удаленного компьютера или точкой (локальный компьютер). В имени канала PipeName верхний и нижний регистры не различаются.
Пример: Многонитевой сервер каналов.
Основная нить (сервер) в цикле создает канал и ждет подключение клиента к каналу. Когда клиент соединяется, сервер канала создает нить для обслуживания этого клиента и затем продолжает цикл. Клиент может успешно соединиться с каналом в интервале между запросами к функциям CreateNamedPipe и ConnectNamedPipe. Если это случается, ConnectNamedPipe возвращает нуль и GetLastError возвращает
ERROR_PIPE_CONNECTED.
Нить, созданная для обслуживания канала, читает запросы и записывает ответы обратно в канал, пока клиент не закроет дескриптор. Когда это случается, нить отсоединяется, закрывает дескриптор канала, и заканчивается.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <windows.h>
VOID InstanceThread(LPVOID);
VOID GetAnswerToRequest(LPTSTR, LPTSTR, LPDWORD); int xx = 0;
DWORD main(VOID)
{
BOOL fConnected; DWORD dwThreadId;
HANDLE hPipe, hThread;
LPTSTR lpszPipename = "\\\\.\\pipe\\mynamedpipe"; for (;;)
СПО Лекция 1 [23.12.04] |
57 |
{
hPipe = CreateNamedPipe( |
// pipe name |
lpszPipename, |
|
PIPE_ACCESS_DUPLEX, |
// read/write access |
PIPE_TYPE_MESSAGE | |
// message type pipe |
PIPE_READMODE_MESSAGE | |
// message-read mode |
PIPE_WAIT, |
// blocking mode |
PIPE_UNLIMITED_INSTANCES, // max. instances |
|
BUFSIZE, |
// output buffer size |
BUFSIZE, |
// input buffer size |
PIPE_TIMEOUT, |
// client time-out |
NULL); |
// no security attribute |
if (hPipe == INVALID_HANDLE_VALUE) MyErrExit("CreatePipe");
fConnected = ConnectNamedPipe(hPipe, NULL) ?
TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
if (fConnected)
{
//Create a thread for this client. hThread = CreateThread(
NULL, |
// no security attribute |
0, |
// default stack size |
(LPTHREAD_START_ROUTINE) InstanceThread, |
|
(LPVOID) hPipe, |
// thread parameter |
0, |
// not suspended |
&dwThreadId); |
// returns thread ID |
if (hThread == NULL) MyErrExit("CreateThread");
}
else
// The client could not connect, so close the pipe. CloseHandle(hPipe);
}
return 1;
}
VOID InstanceThread(LPVOID lpvParam)
{
CHAR chRequest[BUFSIZE]; CHAR chReply[BUFSIZE];
DWORD cbBytesRead, cbReplyBytes, cbWritten; BOOL fSuccess;
HANDLE hPipe;
//The thread's parameter is a handle to a pipe instance. hPipe = (HANDLE) lpvParam;
while (1)
{
//Read client requests from the pipe. fSuccess = ReadFile(
hPipe, |
// handle |
to |
pipe |
|
chRequest, |
// buffer |
to |
receive data |
|
BUFSIZE, |
// size of buffer |
|||
&cbBytesRead, // |
number of bytes read |
|||
NULL); |
// |
not overlapped I/O |
||
if (! fSuccess || cbBytesRead == 0) break;
GetAnswerToRequest(chRequest, chReply, &cbReplyBytes);
//Write the reply to the pipe. fSuccess = WriteFile(
hPipe, |
// |
handle |
to |
pipe |
chReply, |
// |
buffer |
to |
write from |
cbReplyBytes, // number |
of |
bytes to |
write |
||
&cbWritten, |
// |
number |
of |
bytes written |
|
NULL); |
// |
not overlapped I/O |
|
||
if (! fSuccess || cbReplyBytes != cbWritten) break;
}
FlushFileBuffers(hPipe);
DisconnectNamedPipe(hPipe);
CloseHandle(hPipe);}
СПО Лекция 1 [23.12.04] |
58 |
Лекция 11. Тупики (DeadLock)
9Условия возникновения тупиков
9Обнаружение тупиков
9Предотвращение тупиков
9Обход тупиков
Условия возникновения тупиков
Тупики возникают в ситуациях, когда процесс пытается получить эксклюзивный доступ к ресурсам (устройствам, файлам, объектам ядра), т.е. везде где есть ожидание.
Различие между бесконечным откладыванием и тупиком:
•Бесконечное откладывание, в принципе, может разрешиться.
•Тупик, если возник, не может разрешиться без внешнего воздействия. Пример
... ...
s1.P(); s2.P();
s2.P(); s1.P();
---Возможен Тупик ---
В1971 Коффман (Coffman) сформулировал следующие необходимые условия возникновения тупика:
•Ограниченный (взаимоисключающий) доступ
•Неперераспределяемость ресурсов (предоставленный ресурс нельзя забрать назад пока процесс сам не вернет)
•Ожидание с удерживанием (ждем предоставления ресурса, не отдавая те, что успели захватить)
•Круговое ожидание (ждем предоставление ресурса, выделенного другому процессу в цепочке)
Нить А Предоставлен Запрашивает
Ресурс Y |
Ресурс X |
|
|
Запрашивает Предоставлен Нить В 
Выполнение необходимых условий не означает, что тупик обязательно возникнет. Рассмотрим пример.
Нить А |
|
Нить В |
|
1. |
Запрашивает X (Req.X) |
1. |
Запрашивает Y (Req.Y) |
2. |
Запрашивает Y (Req.Y) |
2. |
Запрашивает X (Req.X) |
3. |
Освобождает X (Rel.X) |
3. |
Освобождает X (Rel.X) |
СПО Лекция 1 [23.12.04] |
59 |
4. Освобождает Y (Rel.Y) |
4. Освобождает Y (Rel.Y) |
B
Конфликт по Y
ВНить Конфликт по X
C
A
Нить А
Траектория A: B-Req.Y, A- Req.X, A- Req.Y, A- Rel.X, A- Rel.Y, B- Req.X, B- Rel.X, B- Rel.Y Траектория B: B-Req.Y, A- Req.X, B- Req.X, B- Rel.X, A- Req.Y, B- Rel.Y, A- Rel.X, A- Rel.Y, Траектория C: B-Req.Y, A- Req.X, B- Req.X,
1. |
Запрашивает X (Req.X) |
1. |
Запрашивает Y (Req.Y) |
2. |
Запрашивает Y (Req.Y) |
2. |
Запрашивает X (Req.X) |
3. |
Освобождает X (Rel.X) |
3. |
Освобождает X (Rel.X) |
4. |
Освобождает Y (Rel.Y) |
4. |
Освобождает Y (Rel.Y) |
Методы борьбы с тупиками
1.Алгоритм страуса (игнорировать). Если система виснет 1 раз в год из-за тупика и 1 раз в день по какой-либо другой причине, то это правильное решение. Борьба с тупиками не дешева.
2.Обнаружение тупика и последующее восстановление работоспособности системы.
3.Предотвращение возможности возникновения тупика
4.Обход тупика. Осторожное предоставление ресурсов таким образом, чтобы тупик не возник.
Обнаружение тупика и восстановление
Обнаружение тупика возможно на основе анализа циклов в графе распределения ресурсов.
Как быть с восстановлением?
(а) «Убить» один из процессов в циклической цепочке. Это не всегда возможно. (б) Отменить ряд последних назначений ресурсов для устранения цикла.
СПО Лекция 1 [23.12.04] |
60 |
В случае (б) процессы должны быть запрограммированы специальным образом, например, использовать технологию транзакций. Транзакция считается завершенной лишь в том случае, если выполнены все действия, входящие в транзакцию. Если транзакция не завершена, то она может быть повторена.
Предотвращение тупика
Направлено на нарушение одного из необходимых условий тупика. Как правило, навязывает некоторые соглашения или ограничения.
Взаимное исключение
•Отказаться от разделяемых ресурсов, использовать полностью независимые нити.
•Обеспечить «неограниченное» число совместно используемых ресурсов. Неперераспределяемость ресурсов
•Механизм аналогичен восстановлению (откат назад) на основе механизма транзакций
•Автоматическое перераспределение. Возможно для некоторых типов ресурсов, например, для
оперативной памяти возможна выгрузка на диск.
Ожидание с удерживанием
•Запрашивать все необходимые ресурсы сразу и не начинать работу, пока запрос не будет удовлетворен. Как узнать сколько требуется?
•Не разрешать удерживание ресурса бесконечно долго. Формировать отказ и освобождать занятый ресурс (можно отнести к предыдущей категории методов). Так, например, поступают телефонные
компании – линия отключается.
Круговое ожидание
•Нумеровать ресурсы и всегда запрашивать ресурсы в порядка возрастания номеров.
Обход тупика (алгоритм банкира)
Условия
1.Распределяется конечное число ресурсов (возможно разных типов) между конечным и фиксированным числом нитей (процессов).
2.Процессы заявляют о своих максимальных потребностях в ресурсах каждого типа.
3.Процесс не может запрашивать больше, чем заявлено в максимальных потребностях.
4.Занимать (освобождать) ресурсы можно как по одному, так и группами. Т.е. особых ограничений нет.
5.Диспетчер ресурсов может отказать в предоставлении ресурса. Заметим, что это не может привести к бесконечному откладыванию в предоставлении ресурса.
6.Процесс обязан вернуть выделенные ему ресурсы в течение конечного интервала времени.
Состояние считается надежным, если диспетчер ресурсов гарантирует всем процессам завершение в течение конечного времени.
Алгоритм банкира состоит в такой стратегии предоставления ресурсов, которая сохраняет надежное состояние системы распределения ресурсов.
Пример надежного состояния
Нить |
Использует |
Макс. |
|
|
потребность |
A |
1 |
5 |
B |
1 |
6 |
C |
2 |
4 |
D |
4 |
7 |
|
Резерв: 2 |
Всего: 10 |
Существует последовательность предоставления ресурсов, которая гарантирует всем процессам завершение:
