- •Организация мультизадачности для приложений WIN32
- •ОС создает и манипулирует несколькими различными типами управляющих структур данных, называемых объектами ядра
- •Каждый раз при создании нового процесса, и, в частности, при исполнении стартового кода
- •Эта функция создает для нового процесса виртуальное адресное пространство, создает объект ядра процесс
- •Любой поток имеет возможность запустить дочерний (по отношению к процессу, владеющему данным потоком)
- •Для создания в рамках процесса нового параллельного потока используется функция CreateThread:
- •Функция потока получает управление после создания потока, адрес этой функции является параметром функции
- •В отличие от дочерних процессов, потоки, созданные в рамках одного процесса, работают в
- •Синхронизация параллельных потоков
- •Эта функция ждет освобождения (перехода в состояние signaled) заданного в качестве первого параметра
- •Более сложная синхронизация осуществляется при помощи синхронизирующих объектов ядра. Над любым синхронизирующим объектом
- •Во избежание возможных ошибок, связанных с использованием уже закрытого синхронизирующего объекта, рекомендуется при
- •параметра идентификатор объекта ядра событие. Считается, что событие произошло, если объект ядра событие
- •Примитив выхода информирует систему о том, что текущий поток вышел из критического участка.
- •CRITICAL_SECTION critsect; InitializeCriticalSection(&critsect);
- •Если счетчик содержит ненулевое значение, его значение декрементируется и потоку разрешается выполнение критического
- •Библиотеки динамической компоновки
- •обращения к функциям , необходимым параллельно работающим приложениям. При этом совместно используемые функции
- •DLL-библиотека WIN32 состоит из одной специальной функции DLLEntryPoint и произвольного набора функций, выполняющих
- •BOOL WINAPI DllEntryPoint(
- •LIBRARY mydll
- •Виртуальная память
- •Любая страница виртуального адресного пространства процесса может находиться в одном из трех состояний:
- •ретий параметр функции должен иметь значение MEM_RESERVEдля резервирования участка виртуальной памяти, значение MEM_COMMIT
- •В виртуальном адресном пространстве каждого процесса ОС резервирует участок памяти, предназначенный для использования
- •Внешняя память
- •Параметр “Режим доступа” может быть равен нулю (доступ запрещен), GENERIC_READ (доступ на чтение),
- •Внешняя память
- •Файлы, проецируемые в память
- •Имя отображаемого файла – это произвольная ASCIIZ-строка. Под этим именем отображенный файл будет
- •Другой процесс может получить доступ к файлу, отображенному в виртуальную память при помощи
- •Система команд ЭВМ. Структура машинной команды
- •5.Команды ввода и вывода информации для обмена с внешними устройствами. В некоторых ЭВМ
- •Дальнейшее упрощение команды привело к созданию одноадресных машин. Рассмотрим систему команд такой ЭВМ
- •Трехадресная команда легко расшифровывалась и была удобна в использовании, но с ростом объемов
Эта функция ждет освобождения (перехода в состояние signaled) заданного в качестве первого параметра объекта ядра в течение интервала времени, определяемого вторым параметром. В случае, если указанный объект ядра в момент вызова функции WaitForSingleObject занят (находится в состоянии nonsignaled), вызвавший функцию поток переводится в состояние ожидания (блокируется) и ему не выделяется процессор до тех пор, пока либо ожидаемый объект перейдет в состояние signaled, либо закончится время ожидания. Для того, чтобы сделать интервал ожидания бесконечным, следует в качестве второго параметра функции указать значение INFINITE. В этом случае поток возобновится только тогда, когда освободится ожидаемый объект ядра. Если во время ожидания соответствующий объект ядра был переведен в состояние signaled, функция возвращает значение WAIT_OBJECT_0, в противном случае – WAIT_TIMEOUT. В случае ошибки возвращаются значения WAIT_FAILEDили WAIT_ABANDONED (для объекта взаимоисключения mutex).
Простейшим случаем синхронизации является ожидание завершения какого-либо процесса или потока. В этом случае в качестве первого параметра функции WaitForSingleObject следует указать идентификатор соответствующего объекта ядра. Представленный ниже фрагмент кода иллюстрирует порождение дочернего процесса и ожидание его завершения 74
родительским процессом. Подобным же образом осуществляется и ожидание завершения потока. CreateProcess (NULL, lpCommandLine, NULL, NULL, FALSE, dwCreationFlags,
NULL, NULL, &StartupInfo, &ProcessInformation); WaitForSingleObject (ProcessInformation.hProcess, INFINITE); CloseHandle(ProcessInformation.hProcess); CloseHandle(ProcessInformation.hThread);
Более сложная синхронизация осуществляется при помощи синхронизирующих объектов ядра. Над любым синхронизирующим объектом определены следующие операции:
-создание синхронизирующего объекта;
-открытие синхронизирующего объекта;
-изменение состояния (signaledили nonsignaled) синхронизирующего объекта;
-закрытие синхронизирующего объекта.
Обычно синхронизирующий объект создается одним из потоков, участвующих в синхронизации, при помощи соответствующих функций API. При создании синхронизирующего объекта ему присваивается определенное программистом имя (некоторая текстовая строка). Поток, требующий синхронизации с другими потоками, должен сначала открыть синхронизирующий объект при помощи соответствующей функции API, возвращающей идентификатор объекта типа HANDLE. Для этого он должен знать имя, присвоенное синхронизирующему объекту при его создании. Заметим, что открывать и использовать синхронизирующий объект может любой поток в системе (разумеется, если он знает имя объекта), а не только поток того процесса, которым был создан объект. Другими словами, синхронизирующие объекты могут быть использованы как для синхронизации потоков одного процесса, так и потоков, принадлежащих разным процессам. Поток, создавший синхронизирующий объект , может не вызывать функцию открытия объекта, так как он уже имеет идентификатор объекта типа HANDLE, возвращаемый функцией создания объекта. Однако, вызов функции открытия объекта в этой ситуации не приведет к ошибке. По окончании работы с синхронизирующим объектом, он должен быть закрыт при помощи функции CloseHandle, причем каждому вызову функции создания либо открытия объекта должен соответствовать вызов функции CloseHandle.
Во избежание возможных ошибок, связанных с использованием уже закрытого синхронизирующего объекта, рекомендуется при программировании использовать следующую схему. Синхронизирующий объект должен создаваться потоком, непосредственно не участвующим в процессе синхронизации (наиболее часто – это первичный поток). После создания синхронизирующего объекта рассматриваемый поток должен создать дочерние потоки (или процессы), которые будут непосредственно участвовать в процессе синхронизации. Порожденные таким образом потоки должны открывать созданный родительским потоком синхронизирующий объект перед использованием и закрывать после использования. Родительский поток должен дождаться завершения всех порожденных им потоков, а затем закрыть созданный им синхронизирующий объект. Эта схема будет корректно работать вне зависимости от того, наследуют ли порожденные потоки объекты ядра родительского процесса или нет. Напомним также, что для того, чтобы дочерний процесс унаследовал от родительского созданные последним объекты ядра, необходимо при создании процесса указать соответствующий параметр функции CreateProcess. Кроме того, при создании наследуемого синхронизирующего объекта требуется соответствующим образом заполнить поля структуры SECURITY_ATTRIBUTES, передаваемой в качестве одного из параметров функции создания объекта.
Объект ядра событие (event) используется для порождения некоторым потоком события, ожидаемого другим потоком. Для создания рассматриваемого объекта ядра служит функция CreateEvent, а для открытия уже созданного объекта ядра – функция OpenEvent. Для
ожидания события поток должен вызвать функцию WaitForSingleObject, передав ей в качестве
параметра идентификатор объекта ядра событие. Считается, что событие произошло, если объект ядра событие перешел в состояние signaled. Таким образом, поток, ожидающий событие (вызвавший функцию WaitForSingleObjectдля объекта ядра событие) будет блокирован системой до тех пор, пока не произойдет событие или не истечет заданный вторым параметром функции интервал ожидания. Для генерации события соответствующий поток должен воспользоваться функцией SetEvent, переводящей объект ядра событие в состояние signaled. Для сброса события используется функция ResetEvent, переводящая объект ядра событие в состояние nonsignaled. При создании события функцией CreateEvent можно задать режим автоматического сброса события. В этом случае функция ResetEvent не требуется, а объект ядра событие переводится в состояние nonsignaled функцией WaitForSingleObject по окончании ожидания события потоком.
Объект взаимоисключения (mutex) используется для организации взаимоисключения потоков при доступе к общему ресурсу. Этот объект ядра создается функцией CreateMutex, а для его открытия служит функция OpenMutex. Взаимоисключение потоков состоит в том, что критический участок (некоторый участок кода, обычно осуществляющий доступ к общему ресурсу) одного потока не должен выполняться параллельно с критическими участками других потоков. Перед входом в критический участок поток должен вызывать функцию, называемую примитивом входа в критический участок. Назначение примитива входа состоит в блокировании вызвавшего его потока до тех пор, пока другие потоки не выйдут из своих критических участков. Перед выходом из критического участка поток должен вызвать функцию, называемую примитивом выхода из критического участка.
Примитив выхода информирует систему о том, что текущий поток вышел из критического участка. В качестве примитива входа в WIN32 используется функция WaitForSingleObject, которой в качестве параметра передается идентификатор объекта ядра mutex. Сразу после создания этот объект переводится в состояние signaled. Если функция WaitForSingleObject вызывается для объекта mutex, находящегося в состоянии signaled, то она просто возвращает управление, предварительно поменяв состояние объекта ядра на nonsignaled. Тем самым, первый вызвавший функцию WaitForSingleObjectпоток начинает выполнение своего критического участка, а остальные потоки, пытающиеся войти в свои критические участки, будут заблокированы. В качестве примитива выхода используется функция ReleaseMutex, переводящая объект ядра mutex в состояние signaled, и позволяющая, тем самым, другим потокам входить в их критические участки.
Объект взаимоисключения mutex может быть использован для организации взаимоисключения потоков как одного, так и разных процессов. Однако, для организации взаимоисключения потоков в рамках одного процесса в WIN32 предусмотрен более простой в использовании механизм критических секций. Критическая секция объявляется как структура типа CRITICAL_SECTIONв области глобальных переменных, доступной всем потокам процесса. Перед использованием критической секции ее необходимо проинициализировать при помощи функции InitializeCriticalSection. Для входа в критическую секцию вызывается функция EnterCriticalSection, а для выхода – функция LeaveCriticalSection. Если критическая секция больше не нужна, ее следует удалить при помощи функции DeleteCriticalSection. Ниже приведен фрагмент кода, иллюстрирующий использование критических секций:
CRITICAL_SECTION critsect; InitializeCriticalSection(&critsect);
. . .
EnterCriticalSection(&critsect); Критический участок LeaveCriticalSection(&critsect);
. . .
DeleteCriticalSection(&critsect);
Объект ядра семафор (semaphore) используется для организации последовательногодоступа потоков к общему ресурсу. По своему назначению семафор похож на объект взаимоисключения mutex, но в отличие от последнего позволяет обеспечить параллельное выполнение критических участков (доступ к ресурсу) для ограниченного числа потоков. Поток, пытающийся получить доступ к ресурсу сверх установленного лимита будет блокирован на семафоре до тех пор, пока какой-либо поток не завершит выполнение критического участка, связанного с семафором (освободит ресурс). Для создания семафора используется функция CreateSemaphore, а для его открытия – функция OpenSemaphore. При создании семафора производится инициализация счетчика семафора значением, переданным через один из параметров функции CreateSemaphore, и объект ядра семафор переводится в состояние signaled. Над любым семафором определены две операции: операция P и операция V. Перед входом в критический участок, контролируемый семафором, поток должен выполнить операцию P, а перед выходом из критического участка – операцию V. Операция P состоит в проверке счетчика семафора.
Если счетчик содержит ненулевое значение, его значение декрементируется и потоку разрешается выполнение критического участка. В противном случае поток блокируется на семафоре до тех пор, пока не увеличится значение счетчика. Операция V инкрементирует значение счетчика, тем самым позволяя очередному потоку, заблокированному на семафоре, продолжить выполнение. В WIN32 операцию P над семафором выполняет функция WaitForSingleObject. Перед входом в критический участок, контролируемый семафором, поток должен вызвать эту функцию, передав ей в качестве параметра идентификатор объекта ядра семафор. Функция WaitForSingleObjectдекрементирует значение счетчика семафора, а если он равен нулю переводит объект ядра семафор в состояние nonsignaled. Операцию V над семафором выполняет функция ReleaseSemaphore, которая инкрементирует значение счетчика семафора и переводит объект ядра семафор в состояние signaled.
В заключение отметим, что кроме функции WaitForSingleObject в состав API WIN32 входит функция WaitForMultipleObjects, аналогичная по назначению функции WaitForSingleObject, но позволяющая ожидать освобождения сразу нескольких объектов ядра. Подробную информацию об этой и других вышеупомянутых функциях можно получить в справочной документации или в электронном справочнике WIN32 Programmer’s Reference.
Библиотеки динамической компоновки
При создании загрузочных модулей для однозадачных операционных систем, таких как MS DOS, применяется статическая компоновка. Исходный текст приложения оформляется в виде текстового файла, называемого исходным модулем. Затем осуществляется обработка исходного модуля транслятором, результатом которой является объектный модуль. На последнем этапе объектный модуль обрабатывается редактором связей. Редактор связей компонует в один загрузочный модуль все объектные модули, входящие в состав проекта, а также объектные модули из внешних библиотек транслятора. Таким образом, редактор связей записывает в один исполнимый файл весь код , который может понадобиться при выполнении программы. Если для загрузки подобного файла не хватает оперативной памяти, программист должен самостоятельно разрешать эту проблему, создавая программы, имеющие оверлейную структуру. В мультизадачных операционных системах статическая компоновка оказывается неэффективной, так как приводит к неэкономному использованию одного из самых дефицитных ресурсов - оперативной памяти. Действительно, если в системе одновременно работает несколько приложений и все они используют некоторую функцию, то при статической компоновке в памяти одновременно будет присутствовать несколько копий этой функции. Очевидно, использование памяти было бы гораздо более эффективным, если бы в памяти находилась только одна копия функции, а все работающие приложения могли бы ее вызывать. Практически в любой мультизадачной среде используется именно такой способ
обращения к функциям , необходимым параллельно работающим приложениям. При этом совместно используемые функции объединяются в библиотеки динамической компоновки
( Dynamic-Link Libraries - DLL). При использовании динамической компоновки код совместно используемых приложениями функций помещается в отдельные файлы - DLL-библиотеки, загружаемые в память в единственном экземпляре. Главным отличием динамической компоновки от статической является то, что при динамической компоновке связывание объектных модулей производится не на этапе формирования загрузочного модуля, а уже после загрузки модуля в оперативную память. При использовании механизма динамической компоновки в загрузочном модуле приложения обычно располагаются только те функции, которые являются специфическими для данной программы. Совместно используемые функции объединяются в другом загрузочном модуле - библиотеке DLL (обычно это файл * . dll , хотя может быть использовано любое другое расширение). DLL - библиотека обычно загружается в память в тот момент , когда какому-то приложению потребуется функция из этой библиотеки, хотя возможна и явная загрузка библиотеки при помощи функции LoadLibrary. DLL-библиотека располагается в собственном виртуальном адресном пространстве, недоступном другим процессам. Сегмент кода библиотеки отображается в адресное пространство “заинтересованных” процессов при помощи механизма проецирования файлов в память, который мы кратко рассмотрим в следующей главе. Для каждого процесса, использующего библиотеку, в ее адресном пространстве создается копия сегмента данных библиотеки, которая отображается в адресное пространство соответствующего процесса описанным выше способом. После этого функции DLL-библиотеки будут доступны всем использующим их приложениям. Отметим, что DLL-библиотека не имеет сегмента стека и ее функции пользуются стеком вызвавшего их потока. Существуют также библиотеки, которые не имеют сегментов кода и данных, а включают в себя только ресурсы, доступные нескольким приложениям.
DLL-библиотека WIN32 состоит из одной специальной функции DLLEntryPoint и произвольного набора функций, выполняющих ту работу, для которой разрабатывалась данная библиотека. В заголовке загрузочного модуля DLL-библиотеки должны быть описаны экспортируемые точки входа, соответствующие всем или некоторым определенным в ней функциям. Приложения могут вызывать только те функции DLL-библиотеки, которые экспортируются ей. Неэкспортируемые библиотекой функции могут использоваться как вспомогательные только внутри библиотеки. Функция DLLEntryPoint вызывается каждый раз, когда выполняется инициализация процесса или потока, обращающихся к функциям библиотеки, при завершении этих процессов и потоков, а также при явной загрузке и выгрузке библиотеки функциями LoadLibrary и FreeLibrary. Эта функция создается программистом и служит обычно для выполнения некоторых предварительных операций необходимых для инициализации библиотеки (например, выделение блока памяти для его дальнейшего использования функциями библиотеки), а также выполнения операций, необходимых перед выгрузкой библиотеки (например, освобождение ранее выделенного блока). Функция DLLEntryPoint имеет следующий прототип:
