СПО (Корнилов) / Лекции / вар2 / Операционные системы (Корнилов)
.pdf
СПО Лекция 1 [23.12.04] |
41 |
Writer() {
lock.Acquire();
while((AW+AR)>0) {
WW++; ok_to_write->Wait(&lock); WW--;
}
AW++;
lock.Release();
//====================== ACCESS DB //====================== lock.Acquire();
AW--; if( WW>0)
ok_to_write->Signal(&lock); else
ok_to_read->Broadcast(&lock); lock.Release();
}
Вопросы:
Возможно ли бесконечное откладывание читателей? (Да) Зачем нужен while у читателя?
Задача об обедающих философах
Моделирует использование общих ресурсов несколькими процессами. Суть задачи такова. Пять философов сидят за круглым столом. На столе 5 тарелок с макаронами и 5 вилок по одной между тарелками. Предполагается, что есть надо двумя вилками сразу. Поведение каждого из философов таково: Думает – Хочет есть (пытается взять две вилки) – Ест.
Ограничения:
•Философ должен ждать, пока не освободятся две вилки рядом с ним.
•Модификация переменных состояния должно выполняться в критической секции.
Задача о спящем парикмахере
Моделирует обслуживание потока запросов по очереди. Клиенты приходят в парикмахерскую и встают в очередь. Парикмахер приглашает и обслуживает клиентов по одному.
Ограничения:
•Клиент должен ждать если есть очередь или парикмахер занят обслуживанием.
•Парикмахер спит, если нет клиентов.
•Первый клиент будит парикмахера.
СПО Лекция 1 [23.12.04] |
42 |
Задача о железнодорожном перегоне
Моделирует обмен по полудуплексному каналу связи. Поезда движутся по одноколейному участку дороги в двух направлениях. Необходимо организовать движение таким образом, чтобы избежать столкновения. Ограничения:
•Поезд, подходящий к одноколейному участку дороги должен остановиться, если по нему движется поезд во встречном направлении.
•Модификация переменных состояния должно выполняться в критической секции.
Примитивы синхронизации в Win32 API
Критическая секция
Критическая секция обеспечивает синхронизацию и может использоваться только нитями одного процесса. Подобно объекту mutex, критическая секция может принадлежать только одной нити одновременно, что позволяет защищать общие ресурсы от одновременного доступа. Порядок, в котором нити получают доступ к критической секции не документирован, однако, система будет справедлива к всем нитям.
Процесс должен распределить память для критической секции. Обычно, для этого достаточно просто объявить переменную типа CRITICAL_SECTION. Прежде, чем использовать критическую секцию, ее необходимо инициализировать, используя InitializeCriticalSection.
Доступ к критической секции защищается функцией EnterCriticalSection или TryEnterCriticalSection. Если критическая секция в настоящее время принадлежит другой нити, то вызов EnterCriticalSection приведет к блокировке нити до тех пор, пока нить, находящаяся в критической секции, выполнит функцию LeaveCriticalSection. Функция TryEnterCriticalSection проверяет занятость критической секции без блокировки нити и обычно вызывается в цикле.
Нить, захватившая критическую секцию, может повторно вызывать EnterCriticalSection, при этом блокировки не происходит. Чтобы освободить критическую секцию, нить должна вызвать LeaveCriticalSection столько раз, сколько вызывала EnterCriticalSection.
Любая нить процесса может использовать функцию DeleteCriticalSection, чтобы освободить системные ресурсы, которые были распределены при инициализации критической секции.
Использование критической секции
CRITICAL_SECTION CS;
InitializeCriticalSection(&CS);
EnterCriticalSection(&CS); // Request ownership
// Access the shared resource.
LeaveCriticalSection(&CS); // Release ownership
СПО Лекция 1 [23.12.04] |
43 |
Функции для атомарного доступа к переменным
Функции InterlockedCompareExchange, InterlockedExchangeAdd, InterlockedDecrement,
InterlockedExchange, и InterlockedIncrement обеспечивают простой механизм для синхронизации доступа к общим переменным. Нити различных процессов могут использовать этот механизм, если переменная находится в общедоступной памяти.
Атомарные функции InterlockedIncrement и InterlockedDecrement объединяют два действия: увеличение (уменьшение) значения переменной и возврат результирующего значения.
LONG InterlockedIncrement(LPLONG var)
LONG InterlockedDecrement(LPLONG var)
Атомарная функция InterlockedExchange копирует значение второго параметра в первый параметр и возвращает старое значение первого параметра.
LONG InterlockedExchange( LPLONG target, LONG var); // var -> target & old_target -> rezult
Функция InterlockedExchangeAdd увеличивает значение первого параметра на величину второго параметра и возвращает старое значение первого параметра.
LONG InterlockedExchangeAdd ( PLONG var, LONG incr); // var += incr & old_var -> rezult
Функция InterlockedCompareExchange сравнивает первый и третий параметры и, если равны, присваивает первому параметру значение второго.
PVOID InterlockedCompareExchange(PVOID *dest, PVOID xchg, PVOID comp); // if(dest==comp) dest <- xchg
Объекты ядра
Система позволяет создавать разнообразные объекты, доступ к которым осуществляется с помощью системных вызовов. Принадлежность объектов процессам отслеживается системой, память занятая объектами автоматически освобождается при завершении процесса. Такими объектами являются: Каналы
(Pipe), Семафоры (Semaphore), Мьютексы (Mutex), Проекции файлов, События, Нити, Процессы, Файлы, ...
Объект ядра – это структура данных, выделенная ядром ОС, содержащая информацию об объекте и доступная только ядру ОС. Некоторые элементы (имя объекта, дескриптор защиты, счетчик числа пользователей и .др. присутствуют по всех объектах, но большая их часть специфична для объектов конкретною типа. Например, у объекта «процесс» есть идентификатор процесса, базовый приоритет и код завершения.
СПО Лекция 1 [23.12.04] |
44 |
Объекты создаются функциями Win32API, например, CreateProcess. При создании объекта ядра, система возвращает описатель (HANDLE), идентифицирующий созданный объект. Описатель следует рассматривать как «непрозрачное» 32-битное значение, которое может быть использовано любой нитью процесса. Win32API предоставляет набор функций для работы с объектами ядра.
ОС Microsoft сделана так, что значения описателей являются уникальными для конкретного процесса. Поэтому, невозможно передать такое значение (с помощью какого-либо механизма межпроцессной связи) для использования другому процессу. Далее рассмотрим три механизма использования одного объекта ядра несколькими процессами.
Защита объектов ядра
С объектами ядра связан дескриптор защиты (security descriptor), который описывает, кто создал объект и кто имеет права на доступ к нему. Дескрипторы защиты обычной используют при написании серверных приложений. В клиентском приложение можно игнорировать это свойство объектов (Windows 95 не поддерживает дескрипторы защиты).
Почти все функции, создающие объекты ядра, имеют аргумент - указатель на структуру SECURITY_ATTRIBUTES. Большинство приложений вместо этого аргумента передают NULL и создают объект с защитой по умолчанию (такая защита подразумевает, что администратор и создатель объекта получают к нему полный доступ, а все прочие к объекту не допускаются).
typedef struct _SECURITY_ATTRIBUTES
{
DWOFD |
nLength; |
// размер структуры |
|
LPVOIDlpSecurityDescriptor; |
// |
указатель на дескриптор защиты |
|
BOOL |
bIneritHandle; |
// |
флаг наследования атрибутов |
} SECURITY_ATTRIBUTES;
Таблица описателей объектов ядра
При инициализации процесса система создает в нем таблицу описателей объектов ядра. Сведения о структуре этой таблицы и управлении ею не документированы, к тому же эта таблица по-разному реализуется в Windows NT, Windows 95 и Windows СЕ.
На рис. 2-1 показано, как выглядит таблица описателей, принадлежащая процессу. Это просто массив структур данных. Каждая структура содержит 32-битный указатель на объект ядра, маску доступа и некоторые флаги.
Индекс |
Указатель на объект ядра |
Маска доступа |
... |
|
1 |
Ox???????? |
Ox???????? |
|
|
2 |
Ox???????? |
Ox???????? |
|
|
… |
… |
… |
… |
|
СПО Лекция 1 [23.12.04] |
45 |
Рис. 2-1. Структура таблицы описателей, принадлежащей процессу
Создание объекта ядра
Когда процесс инициализируется таблица описателей пуста. Но стоит одному из его потоков вызвать функцию, создающую объект ядра (например, CreateFileMapping), как ядро выделяет для этого объекта блок памяти и инициализирует его: далее; ядро просматривает таблицу описателей, принадлежащую данному процессу, и отыскивает свободную запись. В указатель записывается адрес структуры данных объекта ядра (в области ОС), маска доступа — на доступ без ограничений, и, наконец, определяется последний компонент
— флаги.
Значение возвращаемого описателя представляет собой индекс в таблице объектов, принадлежащей процессу, и таким образом идентифицирует место, где хранится информация, связанная с объектом ядра. Значение описателей не документировано и может быть изменено, оно может быть индексом или смещением в таблице, может комбинироватся с идентификаторм процесса и т.п.
Если вызов функции, создающей объект ядра, неудачен, то обычно возвращается О (NULL). Такая ситуация возможна только при острой нехватке памяти или при наличии проблем с защитой. К сожалению, отдельные функции возвращают в таких случаях не 0, а -1 (INVALID_HANDLE_VAULE). Например, если CreateFile не сможет открыть указанный файл, она вернет именно INVALID_HANDLE_VALUE. Так что следует быть осторожны при проверке значения, возвращаемого функцией.
Удаление объектов ядра
Объекты ядра принадлежат ядру, а не процессу Важно учитывать, что при завершении процесса объект ядра может быть не разрушен. В большинстве случаев такой объект все же разрушается; но если созданный объект используется другим процессом, ядро запретит разрушение объекта до тех пор, пока от него не откажется и тот, другой процесс.
Ядру известно, сколько процессов использует конкретный объект ядра, поскольку в каждом объекте есть счетчик числа его пользователей. В момент создания объекта счетчику присваивается единица. Когда к существующему объекту ядра обращается другой процесс, счетчик увеличивается на единицу. А когда процесс завершается, счетчики всех, еще используемых им объектом ядра автоматически уменьшаются на единицу. Как только счетчик какого-либо объекта обнуляется, ядро уничтожает этот объект.
По окончанию работы с объектом ядра его нужно закрыть вызовом CloseHandle.
Эта функция сначала проверяет таблицу описателей, принадлежащую процессу, чтобы убедиться, идентифицирует ли переданный ей описатель объект, к которому этот процесс действительно имеет доступ. Если переданный описатель неверен, функция возвращает FALSE, a GetLastError— код
СПО Лекция 1 [23.12.04] |
46 |
ERROR_INVALID_HANDLE. Если же индекс достоверен, система получает адрес объекта ядра и уменьшает счетчик числа пользователей; если счетчик станет равным нулю, ядро удалит объект из памяти. CloseHandle удаляет соответствующую запись об объекте из таблицы описателей. При этом запись удаляется независимо от того, разрушен объект ядра или нет! После вызова CloseHandle процесс больше не имеет доступа к объекту, но, если счетчик не обнулен, объект остается в памяти. Это значит лишь, что объект используется другим процессом. Когда остальные процессы завершат свою работу с этим объектом (вызвав CloseHandle), он будет разрушен.
По завершении процесса операционная система освобождает все ресурсы, принадлежавшие этому процессу.
Совместное использование объектов ядра несколькими процессами
Часто возникает необходимость в доступе к объектам ядра нитями, принадлежащими разным процессам. Причин тому несколько:
•объекты «проекции файлов» позволяют двум процессам совместно использовать одни и те же области памяти;
•почтовые ящики (mail slots) и именованные каналы (named pipes) дают возможность программам обмениваться данными с процессами, исполняемыми на других машинах в сети;
•мьютексы (mutexes), семафоры (semaphores) и события (events) позволяют синхронизировать нити, исполняемые в разных процессах, так что одно приложение может уведомить другое об окончании той или иной операции.
Поскольку описатели объектов ядра имеют смысл только в контексте процесса, совместное использование объектов ядра между несколькими процессами в Win32 — задача весьма непростая.
Предусмотрены три механизма, позволяющие процессам совместно использовать одни и те же объекты ядра:
-наследование описателей;
-дублирование описателей;
-использование именованных объектов
Наследование описателя объекта
Наследование применимо, только когда процессы связаны «родственными» отношениями. Например, родительскому процессу доступны один или несколько описателей объектов ядра, и он передает дочернему процессу право доступа к своим объектам ядра.
Чтобы такой сценарий наследования сработал, родительский процесс должен выполнить несколько операций.
Первое: При создании объекта ядра следует сообщить системе, что нужен наследуемый описатель этого объекта. (наследуются описатели объектов ядра, но сами объекты ядра — нет.)
СПО Лекция 1 [23.12.04] |
47 |
Чтобы создать наследуемый описатель, родительский процесс выделяет и инициализирует структуру SECURITY_ATTRIBUTES, а затем передает ее адрес требуемой Create функции. Следующий код создает наследуемый объект мьютекс с защитой по умолчанию и возвращает его описатель:
SECURITY_ATTRIBUTES sa sa.Length = siseof(sa); sa.lpSecurityDescnptor = NULL sa.bInheritHandle = TRUE
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
Когда операционная система создает дочерний процесс, то создает в нем новую (пустую) таблицу описателей, затем копирует из таблицы родительского процесса все ее действительные записи в таблицу дочернего — причем в те же позиции. Последний факт чрезвычайно важен, так как означает, что значения описателей будут идентичны в обоих процессах (родительском и дочернем). Далее, система увеличивает значения счетчиков объектов ядра.
Второе: Для наследования описателей объектов характерно одно очень странное свойство: дочерний процесс не имеет ни малейшего понятия, что он унаследовал какие-то описатели. Поэтому наследование описателей объектов ядра полезно должно сопровождаться передачей информации о наследуемых описателях.
Для этого в дочерний процесс обычно передают значение описателя объекта ядра как аргумент в командной строке. Инициализирующий код дочернего процесса анализирует командную строку, извлекает из нее значение описателя, и дочерний процесс получает доступ к объекту. При этом механизм наследования срабатывает только потому, что значение описателя объекта ядра в родительском и дочернем процессах одинаково, — и именно по этой причине родительский процесс может передать значение описателя как аргумент в командной строке.
Для передачи информации о наследуемом описателе объекта ядра от родительского процесса дочернему, конечно же, годятся и другие формы межпроцессной связи. Один из приемов заключается в том, что родительский процесс дожидается окончания инициализации дочернего (через функцию WaitForInputIdle), а затем посылает сообщение окну, созданному дочерним процессом.
Еще один прием: родительский процесс добавляет в свой блок переменных окружения непустую переменную. Она должна быть «узнаваема» дочерним процессом и содержать значение наследуемого описателя объекта ядра. Далее родительский процесс создает дочерний, тот наследует переменные окружения родительского процесса и, вызвав GetEnvironmentVariable получает нужный описатель. Такой прием особенно хорош, когда дочерний процесс тоже порождает процессы, — ведь все переменные окружения вновь наследуются.
СПО Лекция 1 [23.12.04] |
48 |
Именованные объекты
Второй способ, позволяющий нескольким процессам совместно использовать одни и те же объекты ядра, опирается на имена объектов. Например, следующие функции создают именованные объекты ядра:
CreateMutex, CreateEvent, CreateSemaphore, CreateFileMapping. У всех этих функций в последнем параметре (lpszName) задается имя объекта. Передавая в нем NULL, Вы создаете безымянный объект ядра.
Объекты без имени могут разделятся между процессами либо через механизм наследования, либо за счет дублирования описателей. Microsoft не дает рекомендаций о правилах именования объектов ядра. Например, создавая объект с именем JeffObj, Вы не застрахованы о того, что в системе уже есть объект ядра с таким именем. Более того, объекты разных типов (мьютексы, события, семафоры, проекции файлов, таймеры) — делят единое пространство имен.
Для доступа к именованному объекту, созданному в другом процессе можно использовать функцию CreateXxx. При выполнении этого вызова система сначала проверяет, существование объекта ядра с таким именем. Обнаружив, что такой объект существует, ядро проверяет его тип. Поскольку мы пытаемся создать объект с именем, которое уже существует, система считает вызов успешным и создает в таблице описателей, принадлежащей процессу, новую запись, а затем инициализирует ее так, чтобы она указывала на существующий объект ядра. Если объекта с указанным именем нет в системе, то будет создан новый объект ядра.
Вместо вызова Create-функции, процесс может обратиться к одной из Ореn-функций, в которых последний параметр определяет имя объекта ядра. В этом случае новый объект никогда не создается.
HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPSTR lpszName);
В этом вызове имя обязательно. Функция просматривают единое пространство имен объектов ядра, пытаясь найти совпадение. Если объекта с указанным именем нет, функции во возвращают NULL, a GetLastError— код 2 (ERROR_FILE_NOT_FOUND). Если объект с заданным именем существует и имеет правильный тип, то проверяются права доступа к данному объекту (параметр dwDesiredAccess). Если доступ разрешен, таблица описателей в вызывающем процессе обновляется и счетчик числа пользователей объекта возрастает на единицу.
Дублирование описателей объектов
Третий механизм совместного использования объектов ядра несколькими процессами опирается на функцию DliplicateHandle.
СПО Лекция 1 [23.12.04] |
49 |
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle, HANDLE hSourceHandle,
HANDLE hTargetProcessHandle, HANDLE lpTargetHandle,
DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwOptions);
Функция берет запись (hSourceHandle) в таблице описателей одною процесса и создает ее копию в таблице другого (lpTargetHandle). Предпоследние два параметра позволяют задать маску доступа и флаги наследования, устанавливаемые для данною описателя объекта ядра в процессе-приемнике.
Параметр dwOptions может быть 0 или комбинацией двух флагов: DUPLICATE_SAME_ACCESS и DUPLICATE_CLOSE_SOURCE. Первый флаг указывает, что новый описатель должен иметь маску доступа источника (параметр dwDesiredAccess игнорируется). Второй флаг приводит к закрытию описателя в процессе-источнике. Он позволяет процессам обмениваться объектом ядра как эстафетной палочкой. При этом счетчик объекта не меняется.
Пример
Пусть процесс A имеет доступ к объекту мьютексу, к которому хочет обратиться процесс В
//приведенный ниже код исполняется процессом А
//создаем обьект-мьютекс доступный процессу A
HANDLE hObjProcessA = CreateMutex(NULL, FALSE, NULL); // открываем описатель объекта ядра "процесс В"
HANDLE hProcessB = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessIdB); HANDLE hObjProcessB; // неинициализированный описатель объекта для процесса В
//предоставляем процессу В доступ к объекту-мьютексу
DuplicateHandle(GetCurrentProcess(), hObjProcessA, hProcessB, &hObJProcessB, 0, FALSE, DUPLICATE_SAME_ACCESS);
//используем какую-нибудь форму межпроцессной связи, чтобы. передать
//значение описателя hObjProcessB в процесс В
//связь с процессом В больше не нужна
CloseHandle(hProcessB);
// если процессу А не нужен объект-мьютекс. он должен закрыть его
CloseHandle(nObjProcessA):
Лекция 9,10. Примитивы синхронизации в Win32 API
9Функции ожидания
9Взаимное исключение (mutex)
9Семафор
9Событие
9Ожидаемый таймер
9Магистраль (pipe)
СПО Лекция 1 [23.12.04] |
50 |
Функции ожидания
Win32 ® API обеспечивает, набор функций WaitForxxx, позволяющих нити блокировать собственное выполнение.
Эти функции переводят вызывающую нить в состояние ожидания, пока указанные критерии не будут выполнены.
Функция WaitForSingleObject требует дескриптор одного объекта синхронизации. Эта функция возвращает управление нити, когда выполняется одно из следующих условий:
•Указанный объект переходит в состоянии "сигнализирован".
•Истекает таймаут. Время ожидания может быть бесконечным.
DWORD WaitForSingleObject( |
// объект |
HANDLE hHandle, |
|
DWORD dwMilliseconds |
// таймуат в мс |
); |
|
Функция WaitForMultipleObjects дает возможность определить массив, содержащий один или большее количество дескрипторов объектов. Эта функции возвращает управление нити, когда выполняется одно из следующих условий:
•Состояние одного из указанных объектов становится "сигнализированным". (параметр fWaitAll = false).
•Состояние всех указанных объектов становится "сигнализированным" (параметр fWaitAll = true).
•Истекает таймаут ожидания.
Возвращаемое функцией значение является индексом объекта, перешедшего в сигнальное состояние, или указывает на выход по таймауту.
DWORD WaitForMultipleObjects( |
// размер массива объектов |
DWORD nCount, |
|
CONST HANDLE *lpHandles, // указатель на массив объектов |
|
BOOL fWaitAll, |
// флаг режима ожидания |
DWORD dwMilliseconds |
// таймаут в мс |
); |
|
Взаимное исключение (mutex)
Объект mutex - объект синхронизации, состояние которого становится «сигнализированным», когда объект не принадлежит никакой нити, и «несигнализированным», когда объект принадлежит какой-либо нити. Только одна нить одновременно может владеть объектом mutex.
Чтобы захватить mutex, нить должна использовать одну из wait функций. Если объект mutex принадлежит другой нити, выполнение нити блокируется до освобождения объекта функцией ReleaseMutex. Нити, ожидающие освобождение mutex, упорядочиваются в очереди в порядке поступления (FIFO)..
Объект создается при вызове функции CreateMutex. В параметрах функции можно установить режим захвата объекта в момент его создания. Нити в других процессах могут открывать дескриптор для доступа к существующему объекту mutex, определяя его имя в параметрах функции OpenMutex. Последнее справедливо для всех объектов синхронизации: объекты без имени могут использоваться только для синхронизации нитей в контексте одного процесса, а объекты с именем– для синхронизации нитей, принадлежащих разным процессам.
Пример:
// Процесс А: создает Mutex с именем « NameOfObject ».
HANDLE hMutex;
DWORD dwErr;
hMutex = CreateMutex( NULL, |
// атрибуты защиты не заданы |
|
FALSE, |
// |
создается «свободным» |
"NameOfObject"); |
// |
имя объекта |
