Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
MS Windows. Элементы архитектуры и системное программирование..pdf
Скачиваний:
273
Добавлен:
01.05.2014
Размер:
1.98 Mб
Скачать

Рисунок 4. Системный файл подкачки и файлы программ.

Файлы отображаемые в память могут быть легко использованы для организации общей памяти. Любой процесс может открыть файл, зная его имя. Для организации обмена данными через файл приложения отображают какой либо участок этого файла в адресные пространства своих процессов. Этим файлом может быть, например системный файл подкачки (Рисунок 5). Такая же технология используется для организации совместного использования кода несколькими экземплярами приложения.

Виртуальная память

 

ДИСК

 

 

 

 

 

 

 

 

процесса 1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Страница кода

 

 

 

 

 

 

 

 

 

Файл программы 1

 

 

 

Общие данные

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Страница данных

 

 

 

 

 

 

 

 

 

 

 

 

Системный файл подкачки

 

 

 

Виртуальная память

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

процесса 1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Страница данных

 

 

 

 

 

 

 

 

 

Общие данные

 

 

Файл программы 2

 

 

Страница кода

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рисунок 5. Организация общей памяти.

Функции для организации отображения файлов в память.

К этим функциям относятся:

CreateFileMapping, OpenFileMapping, MapViewOf File, MapViewOfFileEx,

UnMapViewOfFile, FlushViewOfFile и CloseHandle.

Создание Отображения файла (File Mapping)

Перед отображением файла в память его следует открыть или создать. Эта операция пропускается, если будет использоваться файл подкачки. Файл открывается или создаётся функцией API CreateFile:

hFile = CreateFile(FileName,

GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALLWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);

Если операция завершается без ошибки функция возвращает ссылку на открытый файл. Эта ссылка используется в дальнейшем для доступа к открытому файлу. Атрибуты GENERIC_READ и _WRITE разрешают чтение и запись в файл. Атрибут FILE_SHARE_WRITE разрешает запись в файл из других процессов, которые совместно используют этот файл. Флаг CREATE_ALLWAYS указывает на то, что файл должен быть создан заново.

Затем следует создать файл отображаемый в память (MMF). MMF создаётся на основе открытого файла или внутри файла подкачки. В последнем случае операционная система резервирует в файле подкачки место для MMF. Для создания MMF используется функция CreateFileMapping.

36

CreateFileMapping(HandleOfDiskFile, SecurityAttributes,

Protection, MaxSizeHigh, MaxSizeLow,

ObjectName);

Эта функция создаёт объект MMF и возвращает ссылку на него. Функция имеет следующие параметры:

HandleOfDiskFile - ссылка на открытый файл или –1, если используется системный файл подкачки.

SecurityAttributes - атрибуты защиты. Protection - флаги: PAGE_READONLY - только чтение. PAGE_READWRITE - чтение и запись

PAGE_WRITECOPY - разрешено применение технологии копирования при записи (Только NT).

MaxSizeHigh старшее двойное слово размера отображаемой области памяти. MaxSizeLow младшее двойное слово размера отображаемой области памяти. Размер это 64-х битовое значение.

ObjectName строка, содержащая имя объекта MMF. Если имя объекта задано, оно доступно любому процессу в системе. Если один из процессов создаёт именованный объект MMF, другой процесс может получить доступ к объекту по его имени. Имя используется, например, когда файл отображаемый в память применяется для организации общей памяти.

Пример вызова функции CreateFileMapping:

hMMFile = CreateFileMapping(hFile, NULL, PAGE_READ_WRITE,

0, 0x01400000, NULL);

Если объект MMF совместно используется несколькими процессами ,один из них назначает объекту уникальное имя и создаёт его, а другие могут получить ссылку на объект с помощью функции OpenFileMapping.

OpenFileMapping(DesiredAccess, IheritanceFlag, ObjectName);

Параметр DesiredAccess по смыслу соответствует параметру Protection функции

CreateFileMapping и может принимать значения: FILE_MAP_READ, FILE_MAP_WRITE и FILE_MAP_COPY.

Параметр InheritanceFlag может принимать значения TRUE или FALSE. Он определяет, может ли ссылка быть наследована. По умолчанию разные процессы не могут использовать одну и ту же ссылку (одно и то же численное значение) на объект MMF потому, что ссылка это виртуальный адрес объекта. (Подробнее см. следующую главу.) Каждый процесс имеет своё собственное адресное пространство. Одни и те же виртуальные адреса в разных процессах отображаются в разные физические адреса. Исключение из этого правила можно сделать, когда один процесс создаётся из другого. В этом случае создаваемый процесс называется дочерним процессом, а создающий родительским процессом. Дочерний процесс может наследовать некоторые ссылки на объекты созданные в родительском процессе и использовать их для доступа к объектам. Когда в процессе создаётся ссылка на объект, следует специально указать что эта ссылка может быть наследована дочерними процессами. Функции, которые используются для создания объектов, например CreateFileMapping, позволяют указать возможность наследования ссылки в структуре SecurityAttributes. Указатель на эту структуру передаётся в параметрах функций.

Следует отметить, что ссылка на существующий объект MMF может быть так же получена с помощью функции CreateFileMapping. Объект идентифицируется по уникальному имени. Если объект с заданным именем уже существует, функция не создаёт новый экземпляр объекта. Функция просто возвращает ссылку на существующий объект. При этом атрибуты объекта заданные в параметрах функции игнорируются. Для того чтобы выяснить был ли создан новый объект или открыта ссылка на существующий объект сразу после вызова функции CreateFileMapping следует вызвать функцию GetLastError. Если эта функция возвращает ошибку ERROR_ALREADY_EXISTS объект уже существовал до вызова функции

CreateFileMapping.

37

Создание окна (view) в файле отображаемом в память.

После создания объекта MMF следует отобразить нужный фрагмент файла в виртуальные адреса процесса. Эта операция производится с помощью функций

MapViewOfFile или MapViewOfFileEx. Функция MapViewOfFileEx позволяет прямо указать базовый виртуальный адрес фрагмента файла.

MapViewOfFile(MMFObjectHandle, DesiredAccess,

FileOffsetHigh, FileOffsetLow,

NumberOfBytestoMap);

MapViewOfFileEx(MMFObjectHandle, DesiredAccess,

FileOffsetHigh, FileOffsetLow,

NumberObBytestoMap,

BaseVirtualAddress);

MMFObjectHandle – ссылка на объект MMF, которую возвращает функция CreateFileMapping. DesiredAccess определяет права доступа к памяти. Например «только чтение».FileOffsetHigh и FileOffsetLow соответственно старшее и младшее двойное слово 64-х битового значения смещения фрагмента относительно начала файла. NumberOfBytestoMap 32-х разрядное значение, определяющее размер окна. Если размер и смещение равны нулю, создаётся окно, содержащее весь файл:

lpViewPtr = MapViewOfFile (hMMFile, FILE_MAP_WRITE,

0, 0, 0);

Функция возвращает начальный (базовый) виртуальный адрес окна или NULL в случае ошибки. Теперь с файлом можно работать так же как с блоком памяти.

Несколько процессов могут совместно использовать один и тот же объект MMF. Каждый процесс может разместить несколько окон в одном и том же файле. Окна могут иметь различные размеры и могут перекрываться. Примеры размещения окон приведены на рисунке 6.

Виртуальная

память процесса 1

Окно 1

Окно 2

Виртуальная

память процесса 2

Окно 1

Окно 2

Окно 3

Файл отображаемый в память (MMF)

Рисунок 6. Примеры окон (Views).

Деинициализация MMF.

Каждое окно должно быть удалено, после того как работа с ним завершается. Для удаления окон MMF используется функция UnmapViewOfFile.

UnmapViewOfFile (lpViewPtr);

38

В параметрах функции следует указать виртуальный адрес окна, который возвращает функция MapViewOfFile (Ex).

Ссылка на объект MMF удаляется вызовом функции CloseHandle.

CloseHandle(hMMFile);

Если объект MMF создан не в файле подкачки, следует закрыть файл, на основе которого был создан этот объект.

CloseHandle(hFile);

Принудительная запись на диск содержимого окон MMF.

Страницы памяти, в которых размещается содержимое окна, загружаются в физическую память по запросу. Т.е. при обращении к этим страницам. Если страница загружена в физическую память, существует две копии её содержимого – в памяти и на диске. Изменение содержимого страницы в памяти и сохранение изменений на диске разделены во времени. Модифицированная страница сохраняется в файле в «удобное» с точки зрения операционной системы время. Этот процесс скрыт от приложений. Однако в случае необходимости можно принудительно записать содержимое страниц окна MMF на диск. Для этого используется функция

FlushViewOfFile.

FlushViewOfFile(StartingAddress, Size)

Первый параметр задаёт виртуальный адрес внутри окна MMF, а второй размер блока, который должен быть сохранён на диске.

39

Объекты и ссылки.

Автор: Сидякин И.М.

Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (11.1998)

Email:sidiakin@iu3.bmstu.ru

Понятия объектов и ссылок определяются в документации Windows Platform SDK следующим образом:

Объект это структура данных, которая описывает системный ресурс, например файл, поток, или графическое изображение. Приложение не имеет прямого доступа к объекту или системному ресурсу, который описывает объект. Вместо этого, приложение должно получить ссылку на объект (handle), которую оно может использовать для доступа к системному ресурсу.

Простой пример объекта это файл. Файл имеет набор атрибутов которые доступны приложениям. Например, имя, путь, размер, права доступа (открыт только чтения, только для записи), текущая позиция в файле указателя чтения и записи и т.п. Все эти параметры (они могут быть статичными или динамически изменяемыми в процессе работы) собраны в единой структуре - объекте. Структура имеет поля которые содержат текущие значения каждого параметра. Этот упрощённый пример иллюстрирует основную идею объектно-ориентированной структуры операционной системы. Система использует объекты для управления своими ресурсами. Например, система может запретить операции чтения в файл, установив в соответствующем поле объекта файла атрибут «только чтение». Как правило, пользовательские программы не имеют возможности модифицировать поля объекта напрямую. Вместо этого, система экспортирует набор функций API, предназначенных для работы с объектами различных типов. Эти функции работают с сылками на объекты. Ссылка это 32-х битовое число. Для некоторых типов объектов ссылка это просто виртуальный адрес объекта. Для других типов ссылка может быть индексом в системной таблице объектов данного типа. В любом случае по ссылке можно получить доступ к объекту. Ссылка идентифицирует объект в системе в целом или в только виртуальном адресном пространстве одного из процессов.

Основные операции с объектами.

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

Этот заголовок позволяет использовать единый механизм управления объектами, который включает следующий набор операций:

Создание объекта

Проверка прав доступа к объекту

Создание ссылок на объект

Ограничение использования объекта (например, ограничение максимального числа ссылок на объект)

Дублирование ссылок на объект

Закрытие ссылок и уничтожение объекта

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

Интерфейс объектов.

Операционная система поддерживает единый интерфейс работы с объектами для приложений. Существует набор функций, которые могут быть использованы для выполнения стандартных операций над объектами. К этим функциям относятся:

Создание объекта

Получение ссылки на объект

Получение параметров объекта

Установка параметров объекта

Закрытие ссылки на объект

Уничтожение объекта

40

К некоторым объектам могут применяться не все вышеперечисленные функции.

Типы объектов.

Все объекты разделяются на три типа: объекты user, объекты GDI и объекты kernel. Объекты User используются для управления оконным интерфейсом Windows. К ним относятся иконки, курсоры, окна, меню. Объекты GDI (графического интерфейса)

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

Объекты User.

Система может создать только одну ссылку на каждый объект User. Однако эту ссылку может использовать любой процесс. Объекты, относящиеся к этому типу доступны из любого процесса в системе. Для объектов User ссылка на объект это индекс в глобальной системной таблице, а не виртуальный адрес объекта. Записи таблицы в свою очередь содержат адреса объектов.

Ниже приводится пример работы с объектом такого типа. В примере один процесс получает доступ к окну созданному в другом процессе. Практический смысл примера заключается в том, что бы запретить загрузку более чем одного экземпляра приложения Win32. Загрузка первого экземпляра приложения должна проходить нормально. Однако попытка загрузки второго экземпляра при работающем первом должна пресекаться. Приложение каждый раз в процессе загрузки (до создания главного окна) должно проверять наличие в системе объекта окна, заголовок которого совпадает с заголовком какого либо (например, главного) окна приложения. Если окно с таким заголовком уже существует в системе, экземпляр приложения завершает работу. Если окна с таким заголовком нет, (первый) экземпляр приложения продолжает работу. Определить наличие в системе окна с заданным заголовком можно с помощью функции FindWindow. Эта функция возвращает ссылку на объект окна, если он существует или NULL, если окно не найдено.

HWND hWnd;

hWnd = FindWindow(WindowName, WindowClassName);

Если FindWindow возвращает не нулевое значение, один экземпляр приложения уже загружен. В этом случае второй экземпляр приложения должен завершить свою работу. Перед завершением работы он может активизировать окно первого экземпляра:

if (hWnd != NULL) {

//экземпляр не первый //активизируем окно первого экземпляра по полученной //ссылке

SetForegroundWindow(hWnd);

//завершаем работу

}

else

//продолжаем работу, так как это первый экземпляр.

Объекты GDI.

Объекты GDI не могут совместно использоваться разными процессами. Каждый процесс должен иметь свою собственную копию объекта такого типа. Однако, как и для объектов User, система может создать только одну ссылку на объект GDI. Эта ссылка используется процессом внутри которого объект был создан. Примером функции создающей объект GDI является функция CreateBitmap.

Объекты Kernel.

Объекты kernel могут быть созданы в одном из процессов. При этом доступ к ним можно организовать из нескольких процессов. Ссылки на объекты kernel это виртуальные адреса объектов, поэтому любая ссылка имеет смысл только в контексте одного процесса. Если несколько процессов работают с одним и тем же объектом,

41

каждый из них должен получить свою собственную ссылку. Объекту Kernel может быть присвоено уникальное имя. Любой процесс может получить на объект ссылку по его имени. Ссылки на один и тот же объект могут иметь разные права доступа. Например, файл может быть открыт только на чтение или на запись. Один процесс может получить несколько ссылок на объект с разными правами доступа.

На рисунке 1 приводится пример операции создания объекта индикатора события (Event) принадлежащего к типу объектов kernel.

 

 

 

 

 

Виртуальное

 

 

 

 

 

 

 

 

адресное

 

 

 

 

 

 

пространство

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Приложение

 

 

Handle

 

 

 

Объект Event

 

 

 

3.CreateEvent

 

 

 

 

 

 

 

 

 

 

 

 

возвращает

 

 

 

 

 

 

 

 

 

 

 

 

 

 

handle на объект

 

 

 

 

 

 

 

Event.

 

 

 

 

 

 

 

 

 

 

 

 

1.Выполнить CreateEvent

 

 

 

 

 

 

 

 

 

 

2. CreateEvent создаёт

 

 

 

 

 

в памяти объект Event

Рисунок 1. CreateEvent.

 

 

 

 

 

Процесс может

создать объект или получить одну или несколько ссылок на

существующий объект, указав имя этого объекта. Получить ссылку на существующий объект Event можно с помощью функции OpenEvent. (Рисунок 2).

 

 

 

3.CreateEvent

 

 

 

 

 

 

 

 

возвращает

 

 

 

 

 

 

 

 

handle объекта

Виртуальное

 

 

 

 

Event.

 

 

 

 

адресное

 

 

 

 

 

 

 

 

пространство

 

 

 

 

 

Handle 1

 

 

 

 

 

 

 

 

 

Объект Event

 

 

 

 

 

 

 

 

 

 

 

Приложение

 

 

Handle 2

 

 

 

 

 

 

 

5.OpenEvent

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

создаёт второй

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

handle объекта

 

 

 

 

 

 

 

 

 

Event

 

 

 

 

 

 

 

1.Вызов CreateEvent

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

4.Вызов OpenEvent

 

 

 

 

 

 

 

 

 

 

 

 

 

2. CreateEvent создаёт

 

 

 

 

 

 

 

 

 

 

 

 

 

 

в памяти объект Event

Рисунок 2. CreateEvent/OpenEvent

 

 

 

 

 

HANDLE OpenEvent(

 

 

// права доступа

DWORD

dwDesiredAccess,

BOOL

bInheritHandle,

// разрешение наследования ссылки

LPCTSTR lpName

 

 

// указатель на строку с именем объекта

);

 

 

 

 

 

 

 

 

 

 

 

Пары функций Create/Open имеются практически для всех объектов типа Kernel.

По завершении работы с объектом ссылка на него должна быть закрыта. Ссылки всех объектов kernel закрываются функцией CloseHandle. Единственным параметром этой функции является ссылка на объект. Объект удаляется, после того, как будут закрыты все ссылки на него.

42

Имена объектов Kernel.

Как уже было отмечено, объект типа kernel может иметь уникальное имя. Именование объектов используется для идентификации объекта в системе. Если несколько процессов совместно используют объект, они должны получить ссылки на этот объект. Процесс может получить ссылку на объект по имени объекта. Имя назначается объекту при его создании. Ссылку на объект можно получить с помощью функций Open или Create. Выбор зависит от логики программы. Если только один из процессов имеет право создать объект, он должен использовать функцию Create. Другие процессы должны вызывать функцию Open. Если это не имеет значения, все процессы могут применять функцию Creаte. При этом первый процесс, вызвавший эту функцию, создаст объект, а остальные получат на него ссылку.

Примеры:

HANDLE hEvent;

1.Создание объекта без имени.

hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

2.Создание объекта с именем или получение ссылки на объект, если он существует.

hEvent = CreateEvent(NULL, FALSE, FALSE,

«MyEventObject»);

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

if (GetLastError() == ERROR_ALREADY_EXISTS) {

//объект с именем «MyEventObject» уже существовал

}

3.Попытка получения ссылки на объект.

hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE,

«MyEventObject);

В переменную hEvent возвращается NULL если объект не существует.

После окончания работы со ссылкой она должна быть закрыта:

CloseHandle(hEvent);

Наследование и дублирование ссылок на объекты Kernel.

Ссылка на объект kernel это виртуальный адрес объекта. Каждый процесс имеет свой собственный контекст памяти, поэтому ссылка не может совместно использоваться разными процессами. Разные процессы могут получить ссылку на объект по его имени, однако существуют дополнительные способы передачи ссылки на объект из процесса в процесс. Один из этих способов называется наследование. Рассмотрим случай, когда один процесс создаётся из другого. Создаваемый процесс называется дочерним (child process), а создающий родительским процессом (parent process). Дочерний процесс можно создать с помощью функции CreateProcess. Дочерний процесс при соблюдении нескольких условий может использовать (наследовать) ссылки родительского процесса. Разрешить наследование ссылки в дочерних процессах необходимо при её создании. Интерфейсы функции Create различных объектов, как правило, содержат указатель на структуру SECURITY_ATTRIBUTES. Эта структура содержит информацию о правах доступа к объекту, в том числе и атрибут разрешающий наследование ссылки. Например для создания объекта синхронизации Event и получения ссылки на него используется функция CreateEvent.

HANDLE CreateEvent(

43

LPSECURITY_ATTRIBUTES

lpEventAttributes,

//

указатель

на

атрибуты

// защиты

 

 

BOOL bManualReset,

 

 

 

 

 

 

BOOL bInitialState,

// указатель на строку с именем объекта

LPCTSTR lpName

);

Первый параметр функции это указатель на структуру SECURITY_ATTRIBUTES. Структура имеет следующий формат:

typedef struct _SECURITY_ATTRIBUTES { DWORD nLength;

LPVOID lpSecurityDescriptor; BOOL bInheritHandle;

} SECURITY_ATTRIBUTES;

Структура размещается и инициализируется в вызывающей программе. Если полю bInheritHandle этой структуры присвоить значение TRUE ссылка которую вернёт функция CreateEvent в дальнейшем может быть наследована дочерними процессами.

Кроме этого, при создании дочернего процесса следует разрешить наследование всех ссылок созданных в родительском процессе с атрибутом SECURITY_ATTRIBUTES. bInheritHandle = TRUE. Интерфейс функции CreateProcess содержит параметр bInheritHandles . Если он равен TRUE, созданный дочерний процесс может наследовать ссылки родительского процесса.

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

Процесс с точки зрения операционной системы это так же объект типа kernel. Процесс, который дублирует свою ссылку на какой либо объект, должен указать ссылку на объект процесса, для которого производится дублирование. Процесс в системе можно идентифицировать не только по ссылке на его объект, но и по так называемому идентификатору процесса. В отличие от ссылки, идентификатор это глобальное значение однозначно определяющее процесс в любом контексте памяти. Ссылку и идентификатор процесса можно получить при его создании функцией CreateProcess. Функция OpenProcess возвращает ссылку на объект процесса по его идентификатору. Дублирование ссылок производится с помощью функции DuplicateHandle.

BOOL DuplicateHandle(

HANDLE hSourceProcessHandle, // ссылка на процесс в котором находится

ссылка

// ссылка

HANDLE hSourceHandle,

HANDLE hTargetProcessHandle, // ссылка на процесс для которого дублируется

ссылка

// указатель на дубликат ссылки

LPHANDLE lpTargetHandle,

DWORD dwDesiredAccess,

// права доступа к дубликату ссылки

BOOL bInheritHandle,

// флаг наследования

DWORD dwOptions

//дополнительные флаги

);

 

44

Многозадачность Windows.

Автор: Сидякин И.М.

Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (11.1998)

Email:sidiakin@iu3.bmstu.ru

Процессы и потоки.

Одним из основных понятий, которые вводятся при рассмотрении механизма организации мультизадачного режима исполнения программ в 32-х разрядных операционных системах MS Windows, является поток (thread). Поток это последовательность инструкций микропроцессора. Каждая работающая программа содержит как минимум один поток. Одновременно в системе может существовать несколько потоков, которые исполняются в отведённые им интервалы времени. Система выделяет эти интервалы времени каждому потоку периодически. Иначе говоря, система распределяет процессорное время между потоками. Продолжительность этих интервалов относительно небольшая (несколько десятков миллисекунд), что создаёт иллюзию параллельного исполнения потоков. Операция смены текущего исполняемого потока называется переключением потоков. Операционная система при поддержке аппаратуры сохраняет и восстанавливает параметры потоков при их переключении. Параметры каждого потока хранятся в специальной системной структуре, которая называется контекстом потока. Контекст потока в частности содержит значения регистров МП и стек потока на момент переключения потока.

Для каждого приложения Win32 ОС создаёт отдельный процесс. Контекст процесса включает виртуальное адресное пространство приложения и ряд других системных ресурсов, которые используются потоками. Один или несколько потоков могут быть организованы внутри одного процесса. Такие потоки совместно используют память и другие ресурсы выделенные процессу. Переключение потоков, которые принадлежат различным процессам является более сложной процедурой чем переключение потоков, которые принадлежат одному процессу. Когда происходит переключение потоков принадлежащих разным процессам система должна изменить контекст процесса и выполнить ряд других дополнительных операций.

Процессорное время

...

 

 

 

 

. . .

 

 

 

 

 

...

Поток 1

Поток 2

Поток 3

Процесс 1

Поток 1

Поток 2

Процесс 2

Рисунок 1. Общая концепция Процессов/Потоков. (Process/Thread)

Каждый процесс должен содержать, по крайней мере, один поток который называется первым потоком (primary thread). Этот поток создаётся при создании процесса. Другие

45

потоки процесса могут быть созданы впоследствии из любого существующего потока процесса с помощью функций API.

Совместная и вытесняющая мультизадачность.

ОС Windows 3.x использует механизм, который называется совместная мультизадачность. 32-х разрядные ОС Windows поддерживают этот механизм, только для 16-ти разрядных приложений. Приложения Win32 используют механизм вытесняющей мультизадачности. В Windows 3.x отсутствуют понятия потока и процесса. Вместо них используется термин задача (task). Для каждого приложения Win16 создаётся отдельная задача, которая объединяет атрибуты процесса и потока. Приложение Win16 может иметь только одну последовательность инструкций - один поток. Таким образом, количество потоков в ОС Windows 3.x равно количеству запущенных программ или, что тоже самое, количеству запущенных задач. Другим, возможно более существенным недостатком совместной мультизадачности является то, что каждая задача самостоятельно определяет длительность интервала времени, в течение которого она исполняется. Переключение задач происходит после того, как задача завершает цикл обработки сообщений. Таким образом, временной интервал исполнения каждой 16-ти разрядной задачи, зависит от времени обработки сообщения. Любое приложение Windows 3.x может остановить процесс переключения задач, если его обработчик сообщений не возвращает управление. Это известная проблема 16-ти разрядных ОС Windows. Если одно из приложений зависает в результате ошибки, вместе с ним перестаёт функционировать вся система, потому, что другие приложения не получают процессорное время. Вытесняющая мультизадачность лишена этих недостатков. Переключение потоков производится операционной системой по «внутренним соображениям» вне зависимости от процесса обработки сообщений. Каждое приложение Win32 (процесс) может содержать один или несколько потоков. Это свойство позволяет более эффективно решать самые различные задачи. Например, с помощью известной программы Internet Explorer вы можете одновременно копировать несколько файлов по протоколу ftp и рассматривать страницы Internet. В данном случае операции записи файлов и просмотра страниц реализуются как отдельные потоки, принадлежащие процессу приложения Internet Explorer. Как правило, приложения используют первый поток для организации пользовательского интерфейса, а дополнительно созданные потоки для выполнения фоновых операций, например длительных операций копирования файлов, ожидания ввода данных и т.п.

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

Система быстрее выполняет переключение потоков, если они принадлежат одному процессу. В этом случае не требуется переключение контекста потока (коррекция таблиц страниц и другие операции)

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

Все потоки, принадлежащие одному процессу могут совместно использовать системные ресурсы доступные процессу - файлы, объекты синхронизации и др.

При всех достоинствах механизма мультизадачного исполнения программ, не следует, однако им злоупотреблять. Каждый поток и процесс использует ограниченные системные ресурсы: память, занимаемую структурами контекстов и время исполнения. Если, например, интервал времени исполнения потока равен 20мс и общее количество потоков в системе равно 40, то каждый поток будет исполняться 20мс и ожидать своёй очереди 20*39 = 780мс.

Приоритеты потоков.

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

46

Класс приоритета (priority class), который приписывается процессу, которому принадлежит поток

Уровень приоритета (priority level), который назначается потоку в пределах класса приоритета процесса

Процесс (Process)

Поток (Tread)

Базовый приоритет =

Класс приоритета процесса (Process Priority Class)

+ Уровень приоритета потока (Thread Priority Level)

Рисунок 2. Базовый приоритет.

Комбинация этих значений называется базовым приоритетом потока ( base priority). Это число в диапазоне от 0 до 31. 0 - низший, 31 - высший приоритет. Следует отметить, что разделение базового приоритета на класс и уровень имеет место только в интерфейсах API функций, которые используются для управления потоками и процессами. Системное ПО нулевого кольца защиты работает непосредственно с базовым приоритетом.

Классы приоритета.

Существует шесть различных классов приоритета:

IDLE_PRIORITY_CLASS

BELOW_NORMAL_PRIORITY_CLASS

NORMAL_PRIORITY_CLASS

ABOVE_NORMAL_PRIORITY_CLASS

HIGH_PRIORITY_CLASS

REALTIME_PRIORITY_CLASS

IDLE_PRIORITY_CLASS низший класс приоритета. Этот класс назначается процессам, которые исполняются в фоновом режиме и выполняют какие либо операции периодически и относительно редко. Пример такого процесса - хранитель экрана. Все процессы по умолчанию создаются с классом приоритета NORMAL_PRIORITY_CLASS. Класс приоритета может быть изменён при создании процесса или впоследствии. Не рекомендуется надолго увеличивать класс приоритета до HIGH_PRIORITY_CLASS так как это сокращает время выделяемое остальным процессам в системе. Кратковременное повышение приоритета используется, как правило, для исполнения операций критичных ко времени исполнения. Класс REALTIME_PRIORITY_CLASS используется в редких случаях, когда исполняемая операция столь критична ко времени, что на время её выполнения следует блокировать ряд системных потоков, включая потоки отвечающие за ввод с клавиатуры и мыши.

Для создания процесса используется функция API CreateProcess. Класс приоритета задаётся параметром этой функции dwCreationFlags. Windows API включает дополнительно две функции GetPriorityClass и SetPriorityClass которые используются для определения и динамического изменения класса приоритета процесса. При вызове этих функций следует указать ссылку (handle) на процесс. Эту ссылку возвращает функция CreateProcess.

BOOL SetPriorityClass(

HANDLE hProcess, // ссылка на процесс

DWORD dwPriorityClass // значение класса приоритета

);

Уровни приоритета.

47

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

THREAD_PRIORITY_IDLE

THREAD_PRIORITY_LOWEST

THREAD_PRIORITY_BELOW_NORMAL

THREAD_PRIORITY_NORMAL

THREAD_PRIORITY_ABOVE_NORMAL

THREAD_PRIORITY_HIGHEST

THREAD_PRIORITY_TIME_CRITICAL

Все потоки создаются с уровнем приоритета THREAD_PRIORITY_NORMAL. Уровень приоритета может быть впоследствии изменён с помощью функции SetThreadProirity. Эта функция требует в качестве параметра ссылку (handle) на поток. Эту ссылку возвращает функция создания потока CreateThread.

BOOL SetThreadPriority(

HANDLE hThread, // ссылка на поток

int nPriority // уровень приоритета потока

);

Уровень приоритета потока можно получить с помощью функции GetThreadPriority.

Базовый приоритет потока.

Базовый приоритет потока может изменяться от 0 до 31. Базовый приоритет вычисляется как сумма класса приоритета и уровня приоритета. Комбинация

THREAD_PRIORITY_IDLE и IDLE_PRIORITY_CLASS даёт базовый приоритет равный 1.

Наивысший базовый приоритет (31) получается комбинированием класса

REALTIME_PRIORITY_CLASS и уровня THREAD_PRIORITY_TIME_CRITICAL.

Переключение потоков.

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

Система производит переключение потоков в случае если:

Интервал времени выделенный потоку истёк.

Создан или разблокирован поток с более высоким приоритетом.

Текущий исполняемый поток завершился или блокирован.

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

Вслучае, если интервал времени, выделенный потоку завершился (1), система помещает его контекст в конец очереди его приоритета. Затем сканирует очереди контекстов с убыванием приоритета. В процессор загружается контекст потока из начала первой непустой очереди. Это может быть и только что исполнявшийся поток. Контексты блокированных потоков не заносятся в очереди.

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

48

1. Сохранить контекст потока закончившего исполнение.

2.Поместить поток закончивший исполнение в конец очереди потоков с этим приоритетом.

3.Найти очередь с наивысшим приоритетом содержащую готовые к исполнению потоки.

4.Удалить поток из начала очереди, загрузить его контекст, и начать его исполнение.

Рисунок 3. Алгоритм переключения задач (Scheduler algorithm).

Блокировка потоков.

Поток может находиться в состоянии готовности или в блокированном состоянии (suspended). Каждый блокированный поток удаляется из цикла переключения потоков. Поток может быть блокирован с момента создания, если он создаётся с атрибутом CREATE_SUSPENDED, или впоследствии с помощью функции SuspendThread. Поток блокируется также в случае ожидания ввода или объекта синхронизации. Объекты синхронизации используются, как следует из названия, для синхронизации исполнения потоков. Объект синхронизации это флаг, который может находиться в одном из двух состояний. Поток может ожидать переключения состояния объекта в блокированном состоянии. Разумеется, должен существовать другой поток, который способен изменить состояние объекта синхронизации. Контексты блокированных потоков исключаются из очередей диспетчера потоков и поэтому не используют процессорное время. Когда такой поток выходит из блокированного состояния, диспетчер потоков сравнивает его приоритет с приоритетом текущего исполняемого потока (текущим приоритетом). Если приоритет потока выше текущего, он передаётся на исполнение, иначе контекст потока записывается в очередь соответствующую его приоритету.

Динамическое повышение приоритета (Priority boosts).

Приоритет потока не является статическим параметром. Он может быть временно изменён операционной системой. Система динамически увеличивает приоритеты потоков базовые приоритеты которых меньше или равны 15 (16 это самый низкий базовый приоритет потоков принадлежащих процессам с классом приоритета REALTIME_PRIORITY_CLASS). Такое повышение приоритета производится в случае:

Когда приложение которому принадлежит поток помещается на передний план.

Когда поток получает ввод (от клавиатуры, таймера или мыши)

Когда выполняется условие ожидания объекта синхронизации.

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

В Windows NT имеется возможность разрешать/запрещать повышения приоритета потока отдельного потока или всех потоков заданного процесса. Для этого используются функции SetThreadPriorityBoost и SetProcessPriorityBoost.

Инверсия приоритета (Priority inversion).

Процессорное время получают только потоки с наивысшим приоритетом. Однако безусловное соблюдение этого правила может привести к нарушению функционирования программного обеспечения. На рисунке 4 приводится пример такой ситуации.

49

Поток 2 захватил всё процессорное время так как его приоритет выше приоритета Потока 3

Поток 1

Поток 1 ждёт пока поток 3

Высокий Приоритет

освободит общий ресурс.

 

 

 

 

 

 

 

 

Поток 2

 

 

 

Средний Приоритет

 

 

Общий ресурс

 

 

 

 

 

 

 

Поток 3

 

 

 

Низкий Приоритет

 

 

 

 

Поток 3 занимает общий

 

 

 

 

ресурс и не может

 

 

 

освободить его так как не

 

 

получает процессорного

 

 

времени

 

 

 

 

Рисунок 4. Инверсии приоритета (Priority inversion).

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

для ресурса создаётся флаг занятости (объект синхронизации)

поток, прежде чем начать работу с ресурсом проверяет флаг занятости ресурса

если флаг сброшен (ресурс свободен) поток устанавливает флаг занятости ресурса, работает с ресурсом и затем сбрасывает флаг занятости.

если флаг установлен (ресурс занят, с ним работает другой поток) поток переходит в блокированное состояние до тех пор пока флаг не будет сброшен(пока ресурс не освободится), а затем захватывает ресурс.

В приведённом примере Поток 3, который имеет низший приоритет, захватывает общий ресурс. Поток 1 должен ожидать пока ресурс не освободится. Но Поток 3 не имеет возможности сбросить флаг занятости ресурса потому что, система выделяет всё процессорное время Потоку 2 , который имеет более высокий приоритет, чем Поток 3. Таким образом, несмотря на то, что в системе имеется поток с более высоким приоритетом (Поток 1) в первую очередь исполняется Поток 2.

Эта проблема решается разными методами в операционных системах Windows 95 и Windows NT. Диспетчер потоков NT периодически повышает приоритеты неблокированных потоков. Поток при этом выбирается случайно. В данном примере достаточно увеличить приоритет Потока 3 до приоритета Потока 2 или выше. Тогда Поток 3 получает возможность завершить работу с общим ресурсом и освободить его. Как только это произойдёт Поток 1 продолжит исполняться. Такое повышение приоритета, конечно, носит временный характер. ОС Windows 95 имеет механизм определения ситуаций, когда поток с высшим приоритетом зависит от потока имеющего более низкий приоритет. В этом случае система временно увеличивает приоритет низкоприоритетного пока до приоритета высокоприоритетного потока.

Системы с несколькими процессорами.

Windows NT может работать в вычислительной системе с симметричной многопроцессорной архитектурой. В такой системе потоки могут параллельно выполнятся на нескольких процессорах. Общая идеология переключения задач в таких системах имеет рассмотренную выше основу, однако, включает ряд особенностей. В Windows NT поток имеет дополнительный атрибут маску процессоров (affinity). Маска определяет подмножество микропроцессоров, на которых может исполняться поток. Этот атрибут задаётся функцией SetThreadAffinity.

DWORD SetThreadAffinityMask (

HANDLE hThread, // ссылка на поток

DWORD dwThreadAffinityMask // маска affinity

50

);

Маска может быть задана сразу для всех потоков принадлежащих одному процессу. Для этого используется функция SetProcessAffinityMask. Кроме маски для потока можно программно задать так называемый идеальный процессор (ideal processor). Идеальный процессор это процессор на котором предпочтительно исполнение потока. Если в момент переключения потока свободно несколько процессоров, включая идеальный, поток будет исполняться на идеальном процессоре. Идеальный процессор назначается функцией SetThreadIdealProcessor.

DWORD SetThreadIdealProcessor( HANDLE hThread, // ссылка на поток

DWORD dwIdealProcessor // номер идеального процессора

);

Вышеприведённые операции допустимы только в Windows NT. Windows 95 рассчитана на работу в системе с одним процессором.

Создание потоков.

При запуске приложения Win32 операционная система автоматически создаёт новый процесс и первый поток процесса. Все остальные потоки могут быть созданы из существующих потоков. Второй поток может быть создан из первого, третий из первого или второго и т.п. Нет никаких ограничений того, из какой точки программы создаётся новый поток. Windows API включает две основные функции для создания потоков: CreateThread и CreateRemoteThread. Первая функция создаёт поток в контексте процесса из которого она вызвана, а вторая может создать поток в любом заданном процессе (эта функция используется отладчиками). Рассмотрим интерфейс функции

CreateThead.

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes, // указатель на атрибуты

DWORD dwStackSize, // начальный

 

// защиты

размер стека потока

LPTHREAD_START_ROUTINE

lpStartAddress,

// указатель на функцию

LPVOID lpParameter, //

значение

 

//потока

параметра функции потока

DWORD dwCreationFlags,

// флаги

создания

потока

LPDWORD lpThreadId // указатель

на возвращаемый идентификатор потока

);

Функция возвращает ссылку на поток или NULL в случае ошибки.

Первый параметр это указатель на структуру содержащую атрибуты защиты. Поток это объект типа kernel и как любой объект такого типа может иметь атрибуты защиты. Атрибуты защиты объекта потока в частности определяют, может ли ссылка на поток быть наследована дочерними процессами (см. ниже). Этот параметр необязателен и может быть равен NULL.

dwStackSize определяет начальный размер стека потока в байтах. Если этот параметр равен нулю, по умолчанию создаётся стек размером 1MБ. Большая часть стека перед началом работы потока размещается в зарезервированной памяти. По мере заполнения стека система может увеличивать его размер и переводить память стека из зарезервированного в используемое (committed) состояние.

LpStartAddress виртуальный адрес инструкции с которой начинается поток. Как правило, поток оформляется в виде функции которая имеет приведённый ниже интерфейс:

DWORD WINAPI ThreadProc(

LPVOID lpParameter // параметр

);

где lpParameter значение, которое передаётся функции потока при запуске. Это значение может быть задано при вызове функции CreateThread. Смысл его определяется разработчиком. Например, это может быть указатель на глобальную

51

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

Функция ThreadProc создаётся разработчиком, однако одна должна иметь указанный интерфейс. Имя этой функции, конечно, не имеет значения. Структура мультипоточного приложения достаточно проста. Каждый поток реализуется в виде функции. Функция запускается, как отдельный поток с помощью CreateThread. Одна и та же функция может быть запущена в нескольких потоках.

DwCreationFlags может быть равен 0 или CREATE_SUSPENDED. В последнем случае поток создаётся в блокированном состоянии. Иногда возникает необходимость разделить во времени процедуры создания запуска потока. Для разблокирования потока следует вызвать функцию ResumeThread, которой требуется указать ссылку на объект потока.

Кроме ссылки на объект потока CreateThread возвращает уникальный идентификатор потока в системе lpThreadId. Напомню, что объект потока принадлежит к объектам ядра (kernel), и ссылка на объект имеет смысл только в контексте процесса, к которому принадлежит поток. В отличие от ссылки идентификатор это глобальный ресурс. Объект поток может быть доступен из других процессов по его идентификатору. Некоторые API функции при работе с потоками используют идентификаторы вместо ссылок.

Ниже приводится пример создания потока:

//функция дополнительного потока

DWORD WINAPI

MyThreadFunction( LPVOID lpParam )

{

//DebugMsg: Поток создан

//здесь размещается код потока

//DebugMsg: Поток завершается return 0;

}

//первый поток приложения

VOID main( VOID )

{

DWORD dwThreadId, dwThrdParam = 1; HANDLE hThread;

//создать дополнительный поток

hThread = CreateThread(

 

 

NULL,

// атрибуты защиты по умолчанию

0,

// размер стека потока по умолчанию (1MB)

MyThreadFunction,

//

адрес функции потока

&dwThrdParam,

// аргумент функции потока

0,

// флаги

= 0; запустить после создания

&dwThreadId); // адрес

для идентификатора

// Проверка результата вызова функции.

if (hThread == NULL) {

// ошибка

CloseHandle( hThread );

}

В этом примере поток создаётся и немедленно запускается. Поток автоматически удаляется когда функция MyThreadFunction завершается.

Функция CreateRemoteThread имеет дополнительный параметр hProcess - ссылку на процесс в котором создаётся поток.

52

Завершение работы потоков.

Исполнение потока завершается, когда функция потока возврашает управление. Это нормальный и рекомендуемый способ завершения работы потока. Однако в ряде случаев применяются другие методы. Поток может быть завешён функциями

ExitThread, TerminateThread или функциями ExitProcess, TerminateProcess. Последняя пара функций закрывает поток вместе с процессом, которому он принадлежит. Функции

ExitXXX более «правильные». TerminateThread и TerminateProcess используются только в критических обстоятельствах, например при завершении потоков и процессов в случае ошибки, после которой нормальное функционирование программы невозможно. Когда поток завершается ор возвращает код выхода. Этот код может быть указан в параметрах команды return функции потока или в параметрах функций Exit/Terminate. Код выхода может быть получен с помощью функции GetExitCodeThread.

Создание потоков в Visual C++ с использованием библиотеки классов Microsoft Foundation Classes (MFC).

Библиотека Microsoft Visual C имеет ряд функций которые упрощают операции создания и управления потоками.

В концепции MFC потоки разделяются на два типа: рабочие потоки (worker threads) и потоки имеющие пользовательский интерфейс (user-interface threads). Рабочие потоки реализуются в виде обычных функций, как было рассмотрено выше. Рабочие потоки предназначены для выполнения различных задач в фоновом режиме. Потоки, имеющие пользовательский интерфейс связаны с одним или несколькими окнами Windows. Примером такого потока является первый поток процесса (primary thread). В этом потоке работает оконный интерфейс программы и цикл обработки сообщений. Для создания потоков MFC предоставляет класс CThreadWnd и функцию AfxBeginThread. Эта функция имеет две версии интерфейса для создания потоков двух типов. Ниже рассматривается пример создания рабочего потока. Функция потока MyThreadProc имеет такой же вид как и в предыдущем примере.

CWinThread * hThread;

hThread = AfxBeginThread( (AFX_THREADPROC)MyThreadProc, pParam, nPriority, nStackSize, dwCreateFlags, lpSecurityAttrs);

Параметры функции AfxBeginThread фактически те же самые что использовались при создании потока с помошью функции CreateThread. Это очевидно, так как

AfxBeginThread вызывает функцию API CreateThread.

Создание потоков в Delphi с помощью класса TThread.

Borland Delphi в полном соответствии со своей объектно ориентированной моделью, предлагает класс TThread который так же как класс CThreadWnd в VC++ является надстройкой над функциями Windows API.

Для создания потока (в том случае если вы не желаете использовать функции API) необходимо создать класс потомок TThread. Приведённый ниже пример иллюстрирует процедуру создания потока.

type

MyThread = class(TThread) private

{ Private declarations }

protected

{описание функции потока } procedure Execute; override;

public

Constructor Create(Parameter1 : LongInt; Parameter2 : Integer); end;

Constructor MyThread.Create(Parameter1 : LongInt; Parameter2 :

Integer);

begin

53

{здесь производится инициализация полей объекта и затем вызывается конструктор предка }

inherited Create(FALSE); end;

{функция потока } procedure MyThread.Execute;

begin

{ Здесь размещается код функции потока } end;

Метод Execute класса MyThread, это тоже самое что функция

MyThreadFunction в предыдущем примере. Поток, который создаёт этот объект, может передать через параметры конструктора любые данные. Конструктор класса TThread имеет только один параметр, который определяет флаг создания потока. Если этот параметр равен TRUE, поток создаётся в блокированном состоянии:

constructor Create(CreateSuspended: Boolean);

Поток может быть создан и запущен следующим образом:

MyThread.Create(Par1, Par2);

Псевдопотоки (Fibers).

Система переключает обычные потоки автоматически. Однако существует особый класс потоков, которые переключаются программно. Эти потоки называются Fibers. Каждый поток может быть преобразован в файбер. Для этого используется функция ConvertThreadtoFiber. Эта функция имеет один необязательный параметр двойное слово, которое может быть передано файберу в момент при его создании. Функция возвращает указатель на файбер, который используется для управления им. После такого преобразования поток исключается из процедуры диспетчеризации, которую выполняет система. Для того чтобы файбер получил процессорное время необходимо вызвать функцию SwitchtoFiber. Файбер так же может быть создан с помощью функции CreateFiber. Однако эта функция может быть вызвана только из другого файбера. Таким образом, для того, чтобы создать несколько файберов необходимо сначала преобразовать существующий поток в файбер, а затем создать из него или созданных в последствии файберов другие. Файбер удаляется функцией DeleteFiber, которая использует в качестве входного параметра указатель на файбер полученный при его создании.

Локальные переменные потоков.

Потоки расположенные внутри одного процесса совместно используют его адресное пространство и следовательно имеют доступ к глобальным переменным приложения. Они работают с одним контекстом памяти.

Например, операция myvar = 4; при условии, что myvar объявлена как глобальная переменная, записывает число 4 по одному и тому же физическому адресу в независимости от того, из какого потока приложения она вызывается. Иногда, однако, возникает необходимость размещения частных копий переменных для каждого потока. Если переменная myvar объявлена как локальная переменная потока, операция myvar = 4 приводит к записи числа в разные физические ячейки памяти в том случае, если она производится из разных потоков. Локальные переменные потоков объявляются в Visuаl C++ директивой __declspec(thread). Например:

__declspec( thread ) int myvar = 1;

Заметим, что этот способ не работает, если локальная переменная объявлена в динамической библиотеке, которая загружается программно функцией LoadLibrary. Описание динамических библиотек будет приведено позднее. Локальные переменные потоков также задаются с помощью функций API Tls. Этот способ будет описан при рассмотрении динамических библиотек.

Компилятор размещает локальные переменные потоков в специальную секцию файла приложения или библиотеки. Эта секция имеет название .tls. Система переключает контекст памяти для этой секции каждый раз при переключении потоков, вне

54

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

Синхронизация потоков.

Организация доступа к общим ресурсам и управление различными зависимостями между потоками является одной из наиболее важных проблем в режиме мультизадачного исполнения программ. Операционная система должна иметь средства синхронизации исполнения потоков. Потоки могут использовать совместно глобальные переменные приложения. Например, один из потоков, имеет право считывать значение глобальной переменной, только после того как другой поток выполнит операцию инициализации этой переменной. Такая операция может включать несколько инструкций МП. В этом случае доступ к такому общему ресурсу из других потоков должен быть запрещён до тех пор, пока данные не будут подготовлены. Если какая либо операция разделяется между несколькими потоками необходимо обеспечить механизм ожидания в ситуациях, когда одни потоки ждут промежуточных результатов работы других потоков. Потоки внутри одного процесса могут использовать глобальные переменные для организации флагов готовности. Например, один поток устанавливает такой флаг для индикации события, которого ждут другие потоки. Потоки, исполнение которых зависит от значения этого флага, периодически проверяют его состояние. При изменении состояния флага потоки выполняют предусмотренные действия. Этот метод обмена информацией между потоками называется поллинг (polling).

//глобальная переменная

BOOL ReadyFlag;

Поток 1.

ReadyFlag = False;

//операция в процессе исполнения

ReadyFlag = True;

//операция завершена

Поток 2 //Ожидание результатов операции

while (ReadyFlag == FALSE);

Этот метод прост, однако имеет, по крайней мере, два существенных недостатка:

Поток 2 занимает процессорное время не делая ничего, кроме проверки флага готовности.

Этот метод просто реализуется в случае обмена информацией между потоками внутри одного процесса. Для того, чтобы потоки в разных процессах имели доступ к переменной ReadyFlag её необходимо поместить в общую память. (Например поместить её файл отображаемый в память MMF). Это требует дополнительных программных затрат.

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

Механизм синхронизации потоков Windows использует два основных понятия Объекты синхронизации и Функции ожидания. Объекты синхронизации выполняют роль логического флага RadyFlag в приведённом выше примере. Объект синхронизации может находиться в одном из двух состояний: Signaled (как TRUE для ReadyFlag) и Nonsignaled (как FALSE). Однако объекты синхронизации обладают более широкими возможностями, чем глобальные переменные. Они могу быть доступны из разных процессов. Они так же имеют ряд полезных свойств, которые используются в специальных случаях. Поэтому существует несколько типов объектов синхронизации, каждый из которых используется для решения определённых задач. Объекты синхронизации совместно с функциями ожидания замещают опрецию поллинга на более приемлемый метод проверки и ожидания событий. Функция ожидания в этом методе играет роль цикла while. Однако процессорное время при ожидании

55

переключения состояния объекта синхронизации не тратится. Функция ожидания блокирует поток, из которого она вызвана до тех пор, пока условие ожидания не будет выполнено. Это условие выполняется, когда объект синхронизации переключается в состояние Signaled.

Объекты синхронизации.

Операционная система поддерживает ряд объектов предназначенных исключительно для синхронизации работы потоков. К ним относятся: события (еvents),

мьютексы(Mutexes), семафоры (semaphores) и таймеры (waitable timers).

Дополнительно некоторые другие объекты могут принимать состояния Signalled и NonSignalled. Например, объекты потоков и процессов. Объекты синхронизации относятся к классу объектов ядра ОС (kernel objects). Они имеют ряд общих атрибутов, таких как, уникальное имя и атрибуты защиты.

Индикаторы событий (Events).

Эти объекты используется для информирования потоков о возникновении какого либо события. Event используется в ситуациях подобных описанной выше в примере поллинга. Когда, например, один поток завершает операцию, которую ожидают другие, он может установить объект Event в состояние Signalled.

Event создаётся или открывается функцией CreateEvent. Эта функция возвращает ссылку на объект. Event может иметь имя. Если имя объекта указано в параметре функции CreateEvent система пытается найти существующий объект с таким именем. В случае если объект уже существует, CreateEvent возвращает ссылку на объект, иначе она создаёт новый объект и так же возвращает ссылку на него. Назначение имени объекта имеет смысл, если требуется обеспечить доступ к нему из разных процессов или если в одном процессе требуется две различных ссылки, с разными правами доступа (права определяются в структуре атрибутов защиты при создании ссылки). Все задачи внутри одного процесса могут использовать для обращения к объекту одну ссылку.

Рассмотрим подробнее интерфейс функции CreateEvent:

HANDLE CreateEvent(

LPSECURITY_ATTRIBUTES lpEventAttributes, // указатель на атрибуты

// защиты BOOL bManualReset, // флаг программного сброса события

BOOL bInitialState, // флаг начального состояния объекта LPCTSTR lpName // указатель на имя объекта

);

bManualReset логический флаг который определяет метод сброса объекта в состояние NonSignaled. Программно или автоматически. Предварительно следует пояснить, как объект может изменить своё состояние.

Установка в состояние signaled выполняется просто. Для этой цели можно использовать функции SetEvent и PulseEvent. В параметрах этих функций указывается ссылка на объект, полученная при его создании или открытии. Обе эти функции устанавливают объект Event в состояние signaled. Сброс более сложная операция, которая зависит от метода использования объекта. Во первых объект может быть переключён в состояние nonsignaled функцией ResetEven. Эта функция так же требует ссылку на объект. Во вторых, event может быть сброшен ,когда функция ожидания этого объекта возвращает управление. Функция ожидания работает следующим образом. Если объект находится в состоянии non-signaled, функция ждёт. Функция возвращает управление сразу после того, как объект переключается в состояние signaled. Когда поток вызывает функцию ожидания объекта Event он блокируется до тех пор пока какой либо другой поток не вызовет функцию SetEvent или PulseEvent для этого объекта. В двух случаях при возврате управления функцией ожидания объект будет переключён обратно в состояние non-signaled: если параметр bManualReset при создании объекта функцией CreateEvent был равен FALSE или если объект был переключён в состояние signaled функцией PulseEvent.

56

 

Поток 1

 

 

 

 

 

Поток 2

 

 

 

 

 

 

 

 

 

 

 

 

 

hEvent = CreateEvent(..имя)

 

 

 

 

 

 

 

hEvent = CreateEvent(..имя)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

WaitForSingleObject(hEvent)

 

 

 

Объект

 

 

 

SetEvent(hEvent)

 

 

Выполнение приостановлено в

 

 

 

Event

 

 

 

установка объекта в состояние

 

 

 

 

 

 

 

 

 

 

ожидании Event = signaled

 

 

 

 

 

 

 

signaled

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

CloseHandle(hEvent)

 

 

 

 

 

 

 

CloseHandle(hEvent)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рисунок 5. Управление объектом Event.

Рассмотрим практический пример использования объекта Event. В одной из предыдущих глав приводился метод предотвращения загрузки второй копии приложения. Эта задача решается просто для приложений Win16, так как каждая копия такого приложения размещается в различных виртуальных адресах. Однако для приложений Win32 это не так. При запуске каждой копии приложения Win32 система создаёт отдельный процесс, при этом виртуальные адресные пространства процессов наложены друг на друга. Для определения того, что одна копия приложения Win32 уже загружена, можно использовать функцию FindWindow. Эта функция позволяет получить ссылку на какое либо (кроме дочерних) окно приложения по заголовку окна, т.е. позволяет проверить существует ли в системе окно с заданным заголовком. Таким образом, приложение при запуске перед созданием своего главного окна может проверить наличие окна с таким именем в системе и по результатам проверки продолжить или завершить исполнение. Однако это не вполне надёжная проверка. Вопервых, поиск по строке заголовка содержит определённый риск совпадения заголовков окон разных программ. Во вторых, между загрузкой приложения и созданием его окна проходит время, за которое может быть запущена вторая копия. Наиболее корректный способ решения этой задачи основан на использовании объекта синхронизации (например, события) и дополнительно функций FindWindow и SetWindowToForeground.

HANDLE hEvent;

HWND hWnd;

hEvent = CreateEvent(NULL, FALSE, FALSE, «ApplicationEventName»); if (GetLastError() == ERROR_ALLREADY_EXISTS) {

hWnd = FindWindow(NULL, «MyWindowName»); if (hWnd != NULL)

SetWindowToForeground(hWnd);

CloseHandle(hEvent);

//завершить приложение

...

}

else

//продолжить исполнение

...

//конец программы

CloseHandle(hEvent);

Этот код следует разместить в самом начале программы (CloseHandle в самом конце) Только первая копия приложения создаст объект event. Вторая копия просто получит ссылку на существующий объект. Приложение может проверить действительное положение вещей с помощью функции API GetLastErrorFunction и завершить исполнение, в случае если объект уже был создан. Следует отметить так же и то, что полученная ссылка на объект должна быть обязательно закрыта при завершении работы копии приложения.

57

Взаимоисключения (Mutexеs).

Mutex это аббревиатура термина mutual exclusion (взаимоисключение). Этот объект используется для организации защиты общих ресурсов от одновременного доступа из нескольких потоков. Мьютексу всегда ставится в соответствие (логически) какой либо ресурс, с которым одновременно имеет право работать только один поток. Мьютекс, который имеет имя может быть доступен из процессов, которым это имя известно и которые обладают достаточными правами доступа к объекту. Mutex использует концепцию владельца ресурса (owner). Ресурс, связанный с объектом может иметь одновременно только одного владельца. Объект имеет стандартную пару функций

Create/Open.

 

Поток 1

 

 

 

 

Поток 2

 

 

 

 

 

 

 

 

 

 

 

 

 

hMutex = CreateMutex(..имя)

 

 

 

 

 

 

hMutext = CreateMutex(..имя)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

WaitForSingleObject(hMutex)

 

 

 

Объект

 

 

WaitForSingleObject(hMutex)

 

 

Выполнение приостановлено в

 

 

 

Mutex

 

 

Выполнение приостановлено в

 

 

 

 

 

 

 

 

 

ожидании Mutex = signaled

 

 

 

 

 

 

ожидании Mutex = signaled

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ReleaseMutex(hMutex)

 

 

 

 

 

 

ReleaseMutex(hMutex)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

CloseHandle(hMutex)

 

 

 

 

 

 

CloseHandle(hMutex)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рисунок 6. Управление объектом Mutextes.

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

Ресурс в данном случае это нечто, доступное из нескольких потоков, при условии, что пока один поток является владельцем ресурса, другие потоки не имеют права с ним работать.

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

Сценарий достаточно простой. Поток создаёт или открывает Мьютекс. При этом поток может сразу запросить статус владельца мьютекса. Однако если мьютекс уже имеет владельца, запрос игнорируется. После создания объекта для его захвата может быть использована одна из функций ожидания. Если мьютекс уже захвачен, функция ожидания блокирует исполнение потока до тех пор, пока текущий владелец не освободит объект. После этого функция захватывает мьютекс и возвращает управление. Если одновременно несколько потоков пытаются захватить мьютекс, они выстраиваются в очередь. После того как владелец мьютекса завершает работу с ресурсом, он обязан освободить мьютекс, для того чтобы его могли захватить другие потоки. Эта операция выполняется функцией ReleaseMutex. Ниже приводятся интерфейсы функций, которые используются для создания и управления мьютексом:

HANDLE CreateMutex(

LPSECURITY_ATTRIBUTES lpMutexAttributes, // указатель на атрибуты

// защиты объекта BOOL bInitialOwner, // флаг захвата мьютекса при создании LPCTSTR lpName // указатель на сторку с именем объекта

);

HANDLE OpenMutex(

DWORD dwDesiredAccess, // флаги прав доступа

BOOL bInheritHandle, // флаг разрешения наследования объекта

58

LPCTSTR lpName //указатель на сторку с именем объекта

);

Функции возвращают ссылку на объект.

bInitialOwner равен TRUE, если при создании объекта сразу же происходит его захват. Следует отметить, что если объект уже создан и захвачен, функция не ждёт пока он будет освобождён и немедленно возвращает управление.

BOOL ReleaseMutex(

HANDLE hMutex // ссылка на объект мьютекс

);

Пример использования мьютекса:

//Код потока

HANDLE hMutex; int res;

hMutex = CreateMutex(NULL, FALSE, «MyMutexName»);

//запрашивается захват мьютекса. Если он уже имеет владельца, ждём 1 //секунду.

if(WaitForSingleObject(hMutex, 1000) == WAIT_OBJECT_0) {

//мьютекс захвачен

//в этом месте поток является владельцем мьютекса

//освобождение мьютекса

ReleaseMutex(hMutex);

//в этом месте владельцем мьютекса может стать другой поток

}

else

// время ожидания (1 сек.) истекло или произошла какая либо ошибка.

Семафоры (Semaphores).

По принципу действия объект семафор похож на мьютекс. Однако с его помощью можно организовать одновременный доступ к ресурсу нескольких потоков. Семафор позволяет ограничить число таких потоков. Семафор имеет дополнительный атрибут – счётчик владельцев ресурса. Если значение счётчика равно нулю объект находится в состоянии non-signaled. Если значение счетчика больше нуля объект находится в состоянии signaled. При создании объекта счётчик устанавливается в заданное значение. Каждый раз, когда какой либо поток вызывает функцию ожидания, счётчик уменьшается на единицу. До тех пор пока значение счётчика выше нуля объект находится в состоянии signaled. Следовательно, функции ожидания немедленно возвращают управление и исполнение потоков продолжается. Как только значение счётчика уменьшится до нуля, объект переключается в состояние non-signaled и последующие вызовы функций ожидания будут блокировать вызывающие их потоки. Счётчик увеличивается на единицу каждый раз при вызове функции ReleaseSemaphore. Объект семафор удобно использовать для контроля доступа к ресурсу, который может быть обновременно доступен некоторому ограниченному числу пользователей.

59

 

 

 

Объект

 

 

 

 

 

 

Semaphore

 

 

 

 

 

 

 

 

 

 

Поток 1

 

 

 

 

 

Поток 1

hSemaphore =

 

 

Счётчик = N

 

 

hSemaphore =

CreateSemaphore

 

 

 

 

CreateSemaphore

 

 

 

 

(N, имя);

 

 

 

 

 

(N, имя);

WaitForSingleObject

 

 

Счётчик = N-1

 

 

 

 

 

 

 

 

(hSemaohore);

 

 

Счётчик = N-2

 

 

WaitForSingleObject

 

 

 

 

 

 

 

 

 

 

ReleaseSemaphore

 

 

Счётчик = N-1

 

 

(hSemaohore);

(hSemaphore);

 

 

 

 

ReleaseSemaphore

 

 

 

 

 

 

 

Счётчик = N

 

 

CloseHandle

 

 

 

 

(hSemaphore);

 

 

 

 

(hSemaphore);

 

 

 

 

 

CloseHandle

 

 

 

 

 

 

 

 

 

 

 

 

(hSemaphore);

 

 

 

 

 

 

 

Рисунок 7. Управление объектом Semaphore.

Функции для работы с объектом семафор:

HANDLE CreateSemaphore(

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,//указатель на атрибуты

//защиты LONG lInitialCount, //начальное значение счётчика

LONG lMaximumCount, //максимально допустимое значение счётчика LPCTSTR lpName // указатель на сторку с именем объекта

);

HANDLE OpenSemaphore(

DWORD dwDesiredAccess, //флаги прав доступа

BOOL bInheritHandle, // флаг разрешения наследования объекта LPCTSTR lpName // указатель на сторку с именем объекта

);

BOOL ReleaseSemaphore(

HANDLE hSemaphore, // сылка на объект

LONG lReleaseCount, // значение добавляемое к счётчику

LPLONG lpPreviousCount // адрес по которому возвращается предыдущее //значение счётчика

);

60

Ждущие таймеры (Waitable timers).

Объект таймер преключается в состояние signaled когда истекает заданный период времени. Имеется три различных типа таймеров.

Программный Таймер (Manual-reset timer) сбрасывается в состояние non-signaled функцией SetWaitableTimer которая устанавливает временной интервал срабатывания таймера. Таймер Синхронизации (Synchronization timer) остаётся в состоянии signaled до тех пор, пока все функции ожидания применённые к нему не возвратят управление. Периодический таймер (Periodic timer) перезапускается каждый раз по истечении заданного временного интервала. Периодический таймер может быть таймером синхронизации или программным таймером. Объект таймера может быть создан или открыт соответственно функциями CreateWaitableTimer или OpenWaitableTimer. Ниже приводятся основные функции предназначенные для работы с ждущими таймерами.

HANDLE CreateWaitableTimer(

LPSECURITY_ATTRIBUTES lpTimerAttributes, // указатель на атрибуты

// защиты BOOL bManualReset, //флаг программного сброса

LPCTSTR lpTimerName //указатель на строку с именем объекта

);

Пареметр bManualReset определяет тип таймера программный или таймер синхронизации.

Интерфейс функции OpenWaitableTimer похож на интерфейс функций Open других объектов синхронизации.

BOOL SetWaitableTimer(

HANDLE hTimer, // ссылка на объект

const LARGE_INTEGER *pDueTime, // время задержки

LONG lPeriod, // период таймера

PTIMERAPCROUTINE pfnCompletionRoutine, // процедура таймера LPVOID lpArgToCompletionRoutine, // параметр процедуры таймера

BOOL fResume // флаг состояния таймера

);

Функция SetWaitableTimer используется для активизации таймера и настройки его параметров. pDueTime указатель не переменную, которая задаёт интервал времени после которого таймер изменяет своё состояние. Время задаётся в единицах равных 100нс. Если этот параметр имеет отрицательное значение время считается относительно времени вызова функции, иначе время считается как “абсолютное” число 100 нс интервалов прошедшее с 1 января 1601 года. Это 64-х битовое целое значение типа FILETIME.

lPeriod определяет период таймера. Таймер активизируется периодически до тех пор пока не будет вызвана функция CancelWaitableTimer. Если этот параметр равен нулю таймер не периодический.

pfnCompletionRoutine необязательный параметр –адрес процедуры которая вызывается когда объект переходит в состояние signaled. Значение LpArgToCompletionRoutine передаётся этой процедуре при вызове.

Флаг fResume используется для перевода вычислительной системы в состояние экономного потребления энергии (power-save mode) до тех пор пока таймер не будет переключён в состояние signaled. Это свойство может не поддерживаться системой.

Критические секции (Critical sections).

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

61

секциями вместо ссылок используются переменные типа CRITICAL_SECTION. Перед использованием критической секции её неодходимо проинициализировать с помощью функции InitializeCriticalSection.

VOID InitializeCriticalSection(

LPCRITICAL_SECTION lpCriticalSection // адрес объекта КС

);

После инициализации поток может захватить КС функцией EnterCriticalSection и освободить её функцией LeaveCriticalSection. Эти функции имеют единственный параметр указатель на переменную CRITICAL_SECTION. Если секция уже захвачена другим потоком, функция EnterCriticalSection блокирует исполнение потока. Критические секции не используют функции ожидания применимые к другим объектам синхронизации.

Функции ожидания.

Основное назначение и принципы работы функций ожидания уже были проиллюстрированы рядом примеров. Они используются совместно с объектами синхронизации для организации обмена сообщениями между процессами и совместного доступа к ресурсам. Функция ожидания проверяет состояние объекта синхронизации и блокирует вызывающий поток до тех пор пока объект не переключается в состояние signaled. После этого функция возвращает упрвление и исполнение потока продолжается. Основное достоинство этого механизма состоит в том, что блокированные потоки исключаются из очередей диспетчера потоков и не используют процессорное время.

Рассмотрим некоторые имеющиеся в Windows API функции ожидания.. Наиболее проста в использовании функция WaitForSingleObject:

DWORD WaitForSingleObject(

HANDLE hHandle, //ссылка на объект синхронизации DWORD dwMilliseconds //время ожидания в миллисекундах

);

Первый параметр это ссылка на ранее созданный объект синхронизации. Второй параметр задаёт временной интервал ожидания изменения состояния объекта. Если объект находится в состоянии non-signaled, функция блокирует поток и по истечении указанного временного интервала возвращает ошибку. Если параметр dwMilliseconds равен нулю, функция просто проверяет состояние объекта и немедленно возвращает управление. Если задана константа INFINITE ( = -1) функция блокирует поток до тех пор пока объект не будет установлен в состояние signaled. Функция возвращает значения, которые используются для определения причины её завершения. Функция может вернуть одну из следующих констант:

WAIT_FAILED – системная ошибка. Детальную информацию можно получить, вызвав функцию GetLastError.

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

WAIT_OBJECT_0 – условие ожидания выполнено. Объект был установлен в состояние signaled.

WAIT_TIMEOUT – указанный временной интервал ожидания истёк, но объект по прежнему находится в состоянии non-signaled.

Ниже приводится пример использования функции:

HANDLE hEvent;

DWORD WaitResult;

//создать или открыть объект event

hEvent = CreateEvent(NULL, FALSE, FALSE, «MyEventName»);

...

62

//ждём и перрываем ожидание в случае, если объект будет находится в //состоянии nonsignaled более 1й секунды

WaitResult = WaitForSingleObject(hEvent, 1000); switch(WaitResult) {

case WAIT_OBJECT_0:

//объект установлен в состояние signaled. Ok break;

case WAIT_TIMEOUT:

//время ожидания истекло break;

default:

//ошибка

ErrorCode = GetLastError();

}

Иногда более полезно использовать функцию ожидания WaitForMultipleObjects. Эта функция позволяет наблюдать сразу за несколькими объектами.

DWORD WaitForMultipleObjects(

DWORD nCount, // количество записей в массиве ссылок объектов LPHANDLE pHandles, // указатель на массив ссылок объектов BOOL fWaitAll, // ждать всех или одного

DWORD dwMilliseconds, // время ожидания в миллисекундах

);

Перед вызовом этой функции все ссылки объектов синхронизации должны быть занесены в специально подготовленный массив. nCount определяет число записей в этом массиве.

Функция (это определяется значением параметра fWaitAll), может ожидать изменение состояния одного или всех объектов указанных в массиве. В последнем случае функция не возвращает управление до тех пор, пока все объекты не изменят своё состояние на signaled.

Функция может вернуть одно из следующих значений:

WAIT_OBJECT_0 + n, где n это номер (считая от нуля) ссылки объекта который переключился в состояние signaled в массиве ссылок. Если одновременно несколько объектов находятся в состоянии signaled, возвращается наименьший номер. WAIT_ABANDONED + n, то-же для неосвобождённых мьютексов.

WAIT_TIME_OUT и WAIT_FAILED имеют тот же смысл что и для функции

WaitForSingleObject.

Пример:

HANDLE hEventArray[2];

DWORD WaitResult;

//создать или открыть объекты event

hEventArray[0] = CreateEvent(NULL, FALSE, FALSE, «MyEventName1»); hEventArray[1] = CreateEvent(NULL, FALSE, FALSE, «MyEventName2»);

...

//ждём и прерываем ожидание в случае если оба объекта будут находится //в состоянии nonsignaled более 1й секунды

WaitResult = WaitForMultipleObjects(2, hEventArray, FALSE, 1000); switch(WaitResult) {

case WAIT_OBJECT_0:

//event1 был установлен в состояние signaled.

//возможно event2 был так же установлен в состояние signaled break;

case WAIT_OBJECT_0 + 1:

//event2 был установлен в состояние signaled.

break;

case WAIT_TIMEOUT:

63

//время ожидания истекло break;

default:

//ошибка

ErrorCode = GetLastError();

}

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

Перейдём к рассмотрению примера использования механизма синхронизации исполнения потоков.

Приложение создаёт и запускает фоновый поток, который ожидает поступления данных и производит их обработку. Первый поток приложения управляет пользовательским интерфейсом программы. В его задачу может, например, входить отображение обработанных данных. Поток ввода производит низкоуровневые операции ввода данных и передаёт их по мере поступления фоновому потоку. Поток ввода и Фоновый поток совместно используют объект синхронизации InputEvent. С помощью этого объекта Поток ввода информирует Фоновый поток о наличии новых данных. CancelEvent используется совместно Первым потоком и фоновым потоком. Он используется для передачи Фоновому потоку команды прекращения работы. Эту команду формирует Первый поток, при завершении работы приложения. Потоки могут устанавливать объекты синхронизации в состояние signaled функцией SetEvent. Предполагается, что объекты автоматически сбрасываются в состояние nonsignaled при возврате из функций ожидания.

Основной поток (PrimaryThread).

CreateThread(BackGroundThread)

SetEvent(CancelEvent);

 

Завершение работы потока BackGround из

 

 

Объект Event

 

потока PrimaryThread

 

 

 

CancelEvent

 

 

 

 

 

 

 

Фоновый поток (BackGroundThread)

WaitForMultipleObjects(CancelEvent, InputEvent)

Объект Event

InputEvent

Данные готовы

Поток ввода (InputThread)

SetEvent(InputEvent);

Рисунок 8. Пример синхронизации потоков.

Функция WaitForMultipleObject вызывается с параметром fWaitAll равным FALSE. Это означает, что функция возвращает управление, в случае если хотя бы один из объектов InputEvent или CancelEvent установился в состояние signaled. Массив ссылок, подготовленный для функции WaitForMultipleObjects, содержит две ссылки на объекты CancelEvent и InputEvent. Операция завершения работы фонового потока проверяется в первую очередь, так как ссылка на объект CancelEvent расположена первой в массиве. Функция вызывается так:

64

hEventArray[0] = CancelEvent;

hEventArray[1] = InputEvent;

while (TRUE) { //бесконечный цикл

WaitResult = WaitForMultipleObjects(2, hEventArray, FALSE, INFINITE);

if (WaitResult == WAIT_OBJECT_0) {

//команда завершения работы (из первого потока) return 0; //завершаем Фоновый поток

}

else if (WaitResult == WAIT_OBJECT_0 + 1) {

//получены новые данные от Потока ввода

... //обработка данных

}

}

Приложение завершает исполнение фонового потока вызовом SetEvent(BackGroundEvent). Когда Поток ввода подготавливает новые данные, он сообщает об этом фоновому потоку вызовом SetEvent(InputEvent);

Потоки и графический интерфейс.

В приведённом выше примере подразумевается, что Фоновый поток должен сообщать Первому потоку приложения о наличии новых обработанных данных. Первый поток управляет пользовательским интерфейсом программы. Он может выполнять операцию отображения результатов обработки по мере их поступления. В примере организована одностороння связь Первого и фонового потока. Первый поток, используя объект CancelEvent, может передать команду завершения работы фоновому потоку. Дополнительно для обмена информацией между потоками допускается использовать глобальные переменные программы. Первый поток, например, может считывать данные, подготовленные фоновым потоком и помещённые им в глобальные переменные. Но первый поток должен получать сообщения всякий раз, когда появляются новые данные. В данном случае применение механизма, который используют Фоновый поток и Поток ввода вызывает затруднения. Это связано с тем, что первый поток отвечает за пользовательский интерфейс и не может ожидать в блокированном состоянии ввода данных от фонового потока. Можно решить эту задачу введением глобальной переменной - флага который Фоновый поток устанавливает при появлении новых данных а первый поток периодически проверяет (например, в цикле обработки сообщений). Это известный метод поллинга с недостатками, которые обсуждались выше. Более эффективный способ заключается в том, чтобы посылать сообщения какому либо окну приложения из фоновой задачи всякий раз когда появляются новые данные. Фоновая задача может использовать для этой цели функцию PostMessage. Эта функция помещает сообщение в очередь сообщений приложения и немедленно возвращает управление фоновому потоку, который продолжает исполнение. Позднее в интервал времени выделенный Первому потоку сообщение будет извлечено из очереди и передано для обработки указанному окну. PostMessage возвращает FALSE, если сообщение не может быть доставлено получателю. При вызове PostMessage следует указать ссылку на окно, которому отправляется сообщение. Первый поток может, например, сохранить эту ссылку в глобальной переменной или передать её фоновому потоку при его создании через параметр функции потока lpParameter (см. CreateThread).

65

Основной поток (Primary Thread).

Окно nWnd

Обработчик сообщения

Очередь сообщений приложения

Фоновый поток

WaitForMultipleObjects(CancelEvent, InputEvent)

PostMessage(MY_MESSAGE_ID)

Объект Event

InputEvent

Данные готовы

Поток ввода

SetEvent(InputEvent);

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

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

Вводится глобальная логическая переменная MsgEnable. Первоначально ей присваивается значение TRUE.

Перед отправкой очередного сообщения фоновый поток проверяет этот флаг. Если значение флага равно TRUE оно инвертируется и затем посылается сообщение, иначе сообщение не посылается.

Когда Первый поток получает сообщение он обрабатывает его и затем снова устанавливает флаг MsgEnable в TRUE разрешая фоновому потоку отправку очередного сообщения.

Ниже приводится пример использования такой техники:

Global data:

BOOL MsgEnable = TRUE; //флаг разрешения передачи сообщений

BYTE

InputBlock[INPUT_BLOCK_SIZE]; //данные передаваемые первому

HWND

hWnd;

//потоку

// ссылка на окно приложения

В фононовом потоке:

если WaitForMultipleObjects возвращает WAIT_OBJECT_0 + 1 читаем данные

обрабатываем их и помещаем результат в глобальный массив InputBlock.

if (MsgEnable) { MsgEnable = FALSE;

if (PostMessage(hWnd, MY_MESSAGE_ID, 0, 0) == FALSE )

MsgEnable = TRUE; //если сообщение не может быть отправлено //разрешаем посылку сообщений

66

}

В первом потоке:

Выбирать окно приёмник сообщений от фонового потока. (Например, главное окно приложения)

Создать функцию обработки пользовательского сообщения с кодом

MY_MESASGE_ID.

Запомнить ссылку на окно в глобальной переменной hWnd.

Запустить фоновый поток функцией CreateThread.

Каждый раз при получении сообщения с кодом MY_MESSAGE_ID считываем данные из массива InputBlock и выполняем необходимую обработку (например, отображение на экране). В конце обработчика необходимо установить флаг MsgEnable в TRUE для разрешения посылки следующих сообщений. В такой схеме без буферизации, конечно возможны потери данных. Они возникают в случае когда Первый поток не успевает обработать сообщение до прихода следующей порции данных от фонового потока.

Следует отметить, что фоновый поток должен присвоить флагу MsgEnable значение FALSE перед вызовом функции PostMessage. причина проста. Система может переключить потоки в любой момент времени. Если, например, имеется следующий код в фоновом потоке:

if (PostMessage(hWnd, MY_MESSAGE_ID, 0, 0) != FALSE ) { MsgEnable = FALSE; }

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

PostMessage помещает сообщение в очередь

Первый поток получает управление. (переключение потоков)

Сообщение извлекается из очереди, передаётся окну и обрабатывается.

В конце обработчика флагу MsgEnable присваивается значение TRUE.

Фоновый поток получает управление. (переключение потоков)

MsgEnable = FALSE;

Таким образом флаг останется равным FALSE и посылка сообщений будет окончательно запрещена.

Тупиковые ситуации (Deadlocks).

Этот термин определяет одну общую проблему многозадачных систем. Тупиковые ситуации возникают в лучаях когда несколько потоков совместно используют общие ресурсы. Потоки захватывают ресурсы с помощью объектов синхронизации мьютексов, семафоров или критических секций. простейшее условие которое приводит к тупиковой ситуации показано на рисунке 10.

Поток 1

Захват ресурса A

Поток 2

Захват ресурса B

Ожидание ресурса B

 

Ожидание ресурса A

 

 

 

Рисунок 10. Пример тупиковой ситуации (Deadlock).

67

Синхронный и асинхронный Ввод/вывод.

Windows API содержит ряд функций, которые предназначены для организации вводавывода. Такие функции используются, например, для записи и чтения файлов. Однако понятие ввода-вывода имеет более широкое значение. Оно включает операции обращения к различным устройствам ввода-вывода и ресурсам ОС. Функции вводавывода API используются для работы с последовательным портом, передачи данных по сети и доступа к драйверам аппаратуры. Операции ввода-вывода это способ связи приложений и драйверов устройств, которые могут передавать и принимать данные. Запись в файл подразумевает передачу команды записи и записываемых данных драйверу файловой системы, который выполняет эту операцию. Интерфейс чтения и записи данных одинаков для всех драйверов устройств ввода-вывода. При записи данных в последовательный порт задаются те же параметры (код команды записи и данные) что и при записи в файл. Эта информация будет передана драйверу последовательного порта. Высокоуровневый интерфейс передачи данных одинаков для различных устройств ввода-вывода.

К основным функциям ввода-вывода относятся: ReadFile - чтение данных из файла, или какого либо устройства ввода-вывода и WriteFile -запись данных в файл или устройство ввода-вывода. Операции ввода-вывода деляться на два типа: синхронные и асинхронные ( используется также термин оverlapped). Синхронный вызов функции ввода-вывода возвращает управление после окончания операции. Асинхронный вызов возвращает управление немедленно. При этом операция ввода-вывода продолжает исполняться в фоновом режиме. Процесс исполнения операции скрыт от вызывающей программы, однако, она получает сообщение об окончании операции. Такой метод позволяет приложению использовать время исполнения асинхронной операции для выполнения какой либо другой работы. Поток вызывающий асинхронную операцию может перейти в блокированное состояние и не использовать процессорное время до тех пор, пока операция не будет выполнена. Асинхронный ввод-вывод используется при обращении к некоторым устройствам и для передачи данных по сети. Асинхронные вызовы применяются для организации обмена информацией между пользовательскими приложениями и драйверами устройств. Асинхронная операция должна быть поддержана со стороны драйвера и вызывающего приложения. Интерфейс драйверов устройств будет рассмотрен в соответствующей главе. Ниже приводится информация о том, как асинхронные операции ввода-вывода реализуются в пользовательских приложениях.

Не все устройства ввода-вывода поддерживают асинхронные операции чтения и записи. Такой механизм ввода-вывода, как правило, применяется для работы с «медленными» устройствами. Примером такого устройства может служить последовательный порт с относительно низкой скоростью передачи данных. Исключение составляют операции чтения записи файлов, которые всегда выполняются синхронно. Доступ к файлу по сравнению ,например, с обращением к физической памяти можно так же считать медленной операцией, однако файловая система использует ряд методов для повышения быстродействия таких операций. Для чтения и записи файлов система использует кэширование данных. Операция записи в файл фактически приводит к записи данных в кэш и немедленному возврату из функции WriteFile. Система использует механизм поздней записи (lazy writing). Специальный системный поток переносит данные из кэш памяти на диск уже после того, как функция WriteFile вернула управление. Для операций чтения используется техника опережающего чтения (read-ahead). Система может считывать больше данных, чем требуется. При чтении файла в память попадают данные, которые вероятно потребуются при следующей операции чтения. Таким образом, большинство операций чтения и записи в файл фактически сводятся к быстрому копированию областей памяти.

BOOL ReadFile(

HANDLE hFile, // ссылка на открытый файл или увв

LPVOID lpBuffer, // указатель на буфер для приёма данных DWORD nNumberOfBytesToRead, // размер буфера данный в байтах

LPDWORD lpNumberOfBytesRead, // указатель на число принятых байтов LPOVERLAPPED lpOverlapped // структура для организации асинхронного //чтения

);

68

HFile это ссылка объект файла или устройства ввода-вывода. lpBuffer указатель на буфер для приёма данных, а nNumberOfBytesToRead размер этого буфера. Функция возвращает по адресу lpNumberOfBytesRead действительное число байтов, которое было передано в lpBuffer. lpOverlapped указатель на структуру, которая используется для организации асинхронного чтения. Если этот параметр равен нулю, операция выполняется синхронно. Асинхронные операции как и синхронные могут быть выполнены ещё до возврата из функции. Это происходит в случае, если драйвер устройства уже имеет данные готовые для передачи. В таком случае функция ReadFile возвращает TRUE. Иначе операция переходит в состояние запроса (pending) и передаётся на исполнение в фоновом режиме. При этом ReadFile возвращает FALSE. Функция может вернуть FALSE так же в случае ошибки. Если ReadFile возвращает FALSE, следует определить причину вызвав функцию GetLastError. Если GetLastError возвращает константу ERROR_IO_PENDING запрошенная операция чтения выполняется в фоновом режиме. Все эти проверки выполняются только в том случае, если операция задана как асинхронная (поле lpOverlapped указывает на правильно инициализированную структуру типа OVERLAPPED).

Функция WriteFile имеет схожий интерфейс. Различие состоит в направлении передачи данных.

BOOL WriteFile(

HANDLE hFile, // ссылка на объект файла или увв LPCVOID lpBuffer, // указатель на буфер данных

DWORD nNumberOfBytesToWrite, // размер буфера в байтах

LPDWORD lpNumberOfBytesWritten, // указатель на число записанных //байтов

LPOVERLAPPED lpOverlapped // структура для организации асинхронного //чтения

);

lpOverlapped указывает на структуру OVERLAPPED, которая приводится ниже:

typedef struct _OVERLAPPED { DWORD Internal;

DWORD InternalHigh; DWORD Offset;

DWORD OffsetHigh; HANDLE hEvent;

} OVERLAPPED;

Поля Offset и OffsetHigh соответственно младшее и старшее двойное слово 64-х битового смещения в файле для операций чтения и записи. Эти значения не используются для обмена данными с устройствами ввода-вывода. Поля Internal зарезервированы для использования системой. Наиболее важный параметр для нас это hEvent. Это ссылка на объект синхронизации Event который должен быть предварительно создан приложением. Приложение передаёт ссылку на объект синхронизации драйверу через структуру OVERLAPPED при вызове функции ReadFile или WriteFile. Функция возвращает управление немедленно. Драйвер сохраняет ссылку на объект до конца операции и использует объект для индикации её завершения. Приложение может проверить состояние объекта с помощью одной из функций ожидания и таким образом определить момент когда операция будет в действительности завершена. Обычно для этой цели используется функция GetOverlappedResult. Эта функция ожидания предназначена специально для организации асинхронного ввода-вывода.

BOOL GetOverlappedResult(

HANDLE hFile, // ссылка на объект увв

LPOVERLAPPED lpOverlapped, // адрес структуры overlapped

LPDWORD lpNumberOfBytesTransferred, // указатель на число переданых

//байт

BOOL bWait // флаг режима ожидания

);

Эта функция не позволяет указать время ожидания. Вместо этого праметр bWait определяет два возможных режима работы: bWait = FALSE -> время ожидания = 0,

69

bWait = TRUE -> время ожидания = INFINITE. LpNumberOfBytesTransferred указывает на переменную, в которую возвращается действительное число переданных байт. Функция может быть использована как при записи, так и при чтении данных.

Устройство, которое поддерживает асинхронные операции должно быть открыто с флагом FILE_FLAG_OVERLAPPED. Это указывается в параметре dwFlagAttributes

функции CreateFile. Функция CreateFile используется для открытия и получения ссылки файл или усторйство ввода-вывода.

Следующий пример иллюстрирует чтение данных из COM порта.

OVERLAPPED Ovl;

HANDLE hEvent;

HANDLE hFile;

//открыть com1

hCom = CreateFile( "COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL,

OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);

//создать объект event

hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

// инициализация структуры overlapped

Ovl.Offset = 0; Ovl.OffsetHigh = 0; Ovl.hEvent = hEvent;

bResult = ReadFile(hCom, &inBuffer, nBytesToRead,

&nBytesRead, &Ovl) ;

//функция возвращает FALSE если операция в состоянии запроса //или в случае ошибки

if (!bResult) {

switch (dwError = GetLastError()) { case ERROR_IO_PENDING:

//асинхронная операция выполняется

//делаем что либо

GoDoSomethingElse() ;

// проверка результатов асинхронного чтения bResult = GetOverlappedResult(hFile, &gOverlapped,

&nBytesRead, TRUE);

// в случае ошибки ...

if (!bResult) {

// обрабатываем код ошибки

}

else {

// данные приняты асинхронно

}

break;

default:

//другая ошибка

}

}

else //данные приняты синхронно

Создание дочерних процессов. Запуск приложений.

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

WinExec.

UINT WinExec(

LPCSTR lpCmdLine, // указатель на командную строку UINT uCmdShow // стиль главного окна приложения

);

В параметрах этой функции указывается путь и имя файла приложения вместе с командной строкой, а также стиль отображения uCmdShow главного окна приложения.

70

Функция WinExec для создания процесса вызывает CreateProcess с заданным по умолчанию набором параметров. Интерфейс функции CreateProcess приводится ниже.

BOOL CreateProcess(LPCTSTR lpszImageName, LPCTSTR lpszCommandLine,

LPSECURITY_ATTRIBUTES lpsaProcess, LPSECURITY_ATTRIBUTES lpsaThread, BOOL fInheritHandles,

DWORD fdwCreate, LPVOID lpvEnvironment, LPTSTR lpszCurDir,

LPSTARTUPINFO lpsiStartInfo, LPPROCESS_INFORMATION lppiProcInfo)

Эта функция создает процесс приложения имя файла которого задаётся первым параметром lpszImageName. Если строка содержит только имя файла без пути к нему CreateProcess последовательно пытается найти файл с указанным именем в текущем каталоге, в каталоге Windows\System directory затам в каталоге Windows и наконец в каталогах указанных в блоке переменных среды окружения. LpszCommandLine задаёт командную строку которая передаётся как аргумент при вызове функции WinMain процесса. Процесс может получить ссылку на эту строку с помощью функции GetCommandLine. Третий и четвертый параметры указывают на структуры с атрибутами защиты объектов соответственно процесса и первого потока процесса. Объекты процессов и потоков принадлежат к типу объектов ядра (kernel) и имеют, как и другие объекты этого типа, атрибуты защиты SECURITY_ATTRIBUTES. (Подробнее см. предыдущую главу). Атрибуты защиты процесса и его первого потока задаются отдельно.

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

DwCreationFlags содержит набор флагов, которые задают состояние процесса и первого потока после его создания (например, CREATE_SUSPENDED) а так же класс приоритета процесса.

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

LpszCurDir задаёт рабочий каталог процесса. Если этот параметр равен нулю, используется текущий каталог.

LpsiStatrInfo указатель на следующую структуру:

typedef struct _STARTUPINFO {

DWORD

cb;

LPSTR

lpReserved;

LPSTR

lpDesktop;

LPSTR

lpTitle;

DWORD

dwX;

DWORD

dwY;

DWORD

dwXSize;

DWORD

dwYSize;

DWORD

dwXCountChars;

DWORD

dwYCountChars;

DWORD

dwFillAttribute;

DWORD

dwFlags;

WORD

wShowWindow;

WORD

cbReserved2;

LPBYTE

lpReserved2;

} STARTUPINFO, *LPSTARTUPINFO;

Некоторые поля этой структуры используются только для процессов графических приложений. Некоторые применяются только для процессов консольных приложений. Пары dwX - dwY и dwXSize - dwYSize определяют начальную позицию на экране и размер главного окна приложения. wShowWindow (этот параметр так же используется функцией WinExec) задаёт вид и состояние главного окна приложения. Для этого

71

параметра имеется несколько предопределённых констант. Например, SW_HIDE, SW_MINIMIZE, SW_SHOW SW_MAXIMIZED.

dwFlags комбинация констант, которые определяют какие из полей структуры содержат заданные значения. Например, если необходимо задать позицию главного окна приложения эта позиция заносится в поля dwX и dwY, и дополнительно к полю dwFlag добавляется константа STARTF_USERPOSITION.

Функция CreateProcess копирует структуру PROCESS_INFORMATION в буфер на который указывает параметр lppiProcInfo.

typedef struct _PROCESS_INFORMATION { HANDLE hProcess;

HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId;

} PROCESS_INFORMATION;

Эта структура содержит пары ссылок и идентификаторов процесса и первого потока процесса. Ссылки процесса и потока могут быть закрыты до завершения работы созданного процесса. Лучше всего сделать это сразу после вызова функции CreateProcess. В этом случае объекты процесса и потока не будут уничтожены. Однако ссылки могут быть использованы для доступа к созданному процессу. Например, создающий процесс может по ссылке определить, когда созданный процесс завершил исполнение. Объект процесс не предназначен непосредственно для решения задач синхронизации исполнения потоков, но всё же имеет атрибут состояние. Он может находиться в состоянии signaled или non-signaled. Во время работы процесса его объект находится в состоянии non-signaled и непосредственно перед завершением процесса переходит в состояние signaled. Состояние объекта можно проверить с помощью любой функции ожидания.

72

Динамические библиотеки.

Автор: Сидякин И.М.

Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (11.1998)

Email:sidiakin@iu3.bmstu.ru

Динамические библиотеки (DLL) содержат код, данные или ресурсы которые могут быть подключены к приложениям во время исполнения. Такие библиотеки размещаются в файлах с расширением .dll. DLL обладают рядом преимуществ по сравнению со статическими библиотеками. Использование DLL приводит к экономии дискового пространства, так как несколько приложений могут одновременно использовать одну и ту же динамическую библиотеку. Другим преимуществом является то, что при внесении изменений в DLL не требуется перекомпиляция приложений, которые используют библиотеку. Файлы 32-х разрядных приложений Windows (.exe) и динамических библиотек (.dll) имеют одинаковый формат. С системной точки зрения приложения отличаются от динамических библиотек в нижеследующем::

Система создаёт новый процесс при загрузке приложения (EXE). Динамическая библиотека загружается в адресное пространство процесса, который использует функции DLL.

Приложение поддерживает обработку сообщений. Каждое 32-х разрядное приложение имеет свою собственную очередь сообщений и код реализующий цикл обработки сообщений.

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

Обычно с приложением связаны одно или несколько окон. DLL может содержать или не содержать элементы пользовательского интерфейса.

Как уже было отмечено, система не создаёт процесс при загрузке DLL, вместо этого код и данные DLL отображаются в адресное пространство процесса, который использует библиотеку. Эта операция производится для каждого процесса, который обращается к функциям DLL.

 

 

Физическая память и

 

 

Виртуальное

 

диск

 

Виртуальное

 

 

 

адресное

 

 

 

адресное

 

 

пространство

 

Приложение 1

 

пространство

Приложения 1

 

 

 

Приложения 2

 

 

 

 

 

Код приложения

 

Приложение 2

 

Код приложения

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Код DLL

 

 

 

Код DLL

 

DLL

 

 

 

 

общие данные

 

общие данные

 

 

 

частные данные

 

 

 

частные данные

 

общие данные

 

 

 

 

 

 

 

частные данные 1

 

 

 

 

 

 

 

 

 

частные данные 2

 

 

 

 

 

 

 

Рисунок 1. Отображение кода и данных DLL в адресное пространство процесса.

73

На рисунке 1 показано, как код и данные DLL отображаются в адресное пространство двух процессов, которые используют эту динамическую библиотеку. Данные, размещаемые в DLL, могут быть общими или частными. Виртуальные адреса общих переменных отображаются в одинаковые физические адреса для всех процессов использующих DLL. Виртуальные адреса частных данных (per process, instance data) отображаются в разные физические адреса для разных процессов.

Динамическая компоновка.

Динамическая компоновка это метод позволяющий приложению получить адрес внешней функции на этапе исполнения. В данном случае внешняя функция это функция которая расположена в каком либо другом исполняемом модуле (EXE или DLL). Так как код и данные DLL загружаются в адресное пространство процесса приложения, любая экспортируемая DLL функция может быть вызвана из приложения. Основная проблема состоит в том, что адрес функции зависит от того, в какое место виртуального адресного пространства процесса загружается DLL. Другими словами, этот адрес может быть определён только после того, как динамическая библиотека будет загружена.

Для более детального знакомства с вопросом рассмотрим некоторые особенности формата 32-х битовых исполняемых файлов Windows. Этот формат носит название

Windows Portable Executable File Format (сокращённо PE формат). Это формат 32-

разрядных файлов EXE, DLL и OBJ. Следует отметить, что операционная система использует технологию отображения файлов в память(Memory Mapped Files) для загрузки кода и данных исполняемого модуля в память. Эта технология позволяет упростить доступ к коду, данным и служебным таблицам, расположенным в файле. В действительности структура и содержание файла исполняемого модуля почти не меняется при его загрузке в память. Файл разделяется на несколько секций, которые содержат код приложения, данные (общие и частные отдельно), ресурсы и служебную информацию. При загрузке приложения система создаёт в памяти образы отдельных секций файла. После этого к содержимому секций файла можно обращаться командами, предназначенными для работы с памятью (mov и т.п). T.е. работа с файлом в дальнейшем ничем не отличается от работы с памятью. После создания образа файла система должна выполнить несколько дополнительных операций, включая инициализацию указателей на функции расположенные во внешних модулях - динамических библиотеках. Исполняемый файл может содержать одну или две секции, использующиеся для динамической компоновки. Эти секции: .idata содержащая информацию необходимую для импортирования функций из внешних модулей и .edata которая используется для обратной операции и описывает функции экспортируемые модулем. Эти секции так же называются Таблица Импорта (import table) и Таблица Экспорта (export table). Секции размещаются в файле во время компиляции. С помощью Таблицы Импорта система определяет, какие внешние модули, и какие функции в этих модулях используются приложением. Затем, используя информацию Таблиц Экспорта в указанных модулях, система получает адреса требуемых функций и записывает эти адреса в специально отведённые поля Таблицы Импорта.

Импорт функций.

Код приложения размешается в exe файле в секции с именем .text. Вызов внешней процедуры из этого кода происходит не прямо а косвенно. Компилятор подставляет в инструкцию Call адрес команды

Jmp Dword Ptr [XXXXXXXX]

Эта команда так же расположена в секции .text модуля. В свою очередь XXXXXXXX это указатель, расположенный в секции .idata (в Таблице Импорта). Заметим, что указанные вставки делает компилятор. Значение указателя это адрес внешней функции. Этот адрес заносится в Таблицу Импорта при запуске приложения и загрузке динамической библиотеки, которая содержит внешнюю функцию. Компилятор заменяет прямой вызов внешней функции вызовом по адресу инструкции Jmp и размещает служебную таблицу адресов которые подставляются в параметры инструкций Jmp. Каждая внешняя функция имеет поле для записи адреса в секции .idata и свой собственный Jmp в секции .text. При загрузке динамической библиотеки система инициализирует Таблицу Импорта реальными значениями адресов функций DLL. (Рисунок 2)

74

 

Приложение

DLL

АдресY

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

АдресZ

 

 

 

 

внешняя

 

 

 

 

 

 

 

.idata

функция

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

АдресX Jmp Dword Ptr [АдресY]

.text

Call АдресX

(вызов внешней функции)

Рисунок 2. Вызов функции из другого модуля.

Секция idata содержит несколько таблиц. В первой таблице записаны структуры типа IMAGE_IMPORT_DESCRIPTOR. Каждая запись в таблице соответствует одной из динамических библиотек подключённых к приложению.

Структура IMAGE_IMPORT_DESCRIPTOR включает следующие поля:

Name - адрес ASCIIZ строки содержащей имя импортируемой DLL.

Два поля структуры, Characteristics и FirstThunk, содержат адреса двух массивов указателей. Массив на который указывает поле Characteristic называется HintName, а массив на который указывает поле FirstThunk называется Таблица Импортируемых Адресов (Import Address Table IAT). Первоначально оба массива содержат указатели на структуры типа IMAGE_IMPORT_BY_NAME. Каждая структура описывает одну из внешних функций.

typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint;

BYTE Name[1];

} IMAGE_IMPORT_BY_NAME;

Hint или индекс это уникальный номер присвоенный функции. Этот номер может быть назначен программно при проектировании модуля экспортирующего функцию. Name это ASCIIZ строка содержащая имя функции. Перед инициализацией адреса каждой структуры IMAGE_IMPORT_BY_NAME имеются в двух таблицах. Один в массиве HintName а другой в Таблице Импортируемых Адресов. Система при загрузке приложения через IAT получает ссылки на имя и индекс функции. По этим параметрам загрузчик может определить адрес функции. Загрузчик заменяет в таблице IAT указатели на структуры IMAGE_IMPORT_BY_NAME адресами функций. Таким образом, в файле исполняемого модуля каждая запись в таблице IAT указывает на одну из структур IMAGE_IMPORT_BY_NAME, но после загрузки модуля записи этой таблицы прямо указывают на внешние функции. Другая таблица HintName не модифицируется в процессе загрузки модуля и по прежнему указывает на структуры IMAGE_IMPORT_BY_NAME. Команды Jmp о которых говорилось выше выполняют передачу управления по адресам записанным в Таблице Импортируемых Адресов (IAT). В команде

Jmp Dword Ptr [XXXXXXXX] XXXXXXXX это адрес записанный в соответствующем элементе таблицы IAT.

75

Image Import

Descriptor (IID)

Characteristics

Имя импорт. DLL

FirstThunk

Следующий IID

 

Структуры

Import

Массив

Image Import

Address

HintName

by Name

Table

 

Индекс функции

 

 

Имя функции

 

 

Индекс функции

 

 

Имя функции

 

 

Индекс функции

 

 

Имя функции

 

 

Индекс функции

 

 

Имя функции

Эта таблица

 

 

Имя DLL (ASCIIZ)

перезаписывае

тся

 

 

 

 

загрузчиком

Рисунок 3. Таблица импортируемых функций (Import Table).

Такая технология позволяет настроить вызовы внешней функции из разных мест программы просто записав адрес функции в Таблицу Импортируемых Адресов.

Экспорт функций.

Исполняемый модуль, который экспортирует функции, должен иметь таблицу экспорта. Эта таблица используется загрузчиком для определения адреса экспортируемой функции по её имени или индексу. Таблица экспорта размешается в секции файла

.edata. Эта секция размещается в файле исполняемого модуля (DLL) компилятором. Следует заметить, что exe файл тоже может содержать секцию .edata. Эта секция содержит три параллельных массива с именами, индексами и адресами экспортируемых функций, а так же таблицу (каталог) которая содержит ссылки на перечисленные массивы. Формат записи каталога описывается структурой IMAGE_EXPORT_DIRECTORY. Некоторые поля этой структуры приведены ниже:

Name - указатель на ASCIIZ строку содержащую имя DLL.

Base - начальный индекс экспортируемых функций. Каждой функции может быть присвоен уникальный индекс. Индексы функций располагаются в массиве AddressOfNameOrdinals. Действительный индекс функции, по которому она может быть найдена, вычисляется как сумма поля Base и значения записи в массиве индексов, соответствующего этой функции.

Поля NumberOfFunctions и NumberOfNames определяют общее количество функций экспортируемых модулем. Значения этих полей идентичны.

AddressOfFunctions указатель на массив адресов функций. AddressOfNames указатель на массив указателей на строки. Строки содержат имена экспортируемых функций. AddressOfNameOrdinals указатель на массив индексов функций. Это массив слов (WORD). Информация о функциях размещается в трёх отдельных массивах. Для того чтобы получить имя, индекс и адрес функции требуется просмотреть все три массива. Данные относящиеся к одной функции естественно сохраняются в элементах массивов под одинаковыми номерами. Структура Таблицы Экспорта приведена на рисунке 4.

76

IMAGE_EXPORT_DIRECTORY

Другие поля

.

.

.

NumberOfFunctions

NumberOfNames

AddressOfFunctions

AddressOfNames

AddressOfNameOrdinals

3 параллельных массива

Таблица адресов функций

Адрес1 Адрес2 Адрес3

Таблица имён функций

MyFunc1 MyFunc2 MyFunc2

Таблица индексов функций

 

1

2

3

 

 

 

 

Рисунок 4. Таблица экспортируемых функций (Export Table).

Динамическая компоновка (загрузка библиотеки и подстановка адресов функций) может быть выполнена прямо (implicitly) или косвенно(explicitly). Косвенная компоновка производится системой при загрузке приложения, так как это было описано выше. Windows предоставляет дополнительно набор функций которые позволяют загрузить и выгрузить динамическую библиотеку а так же получить адрес требуемой функции в загруженной библиотеке. Это прямая компоновка.

Прямая компоновка (Explicit loading).

Динамическая библиотека может быть загружена функцией LoadLibrary.

HINSTANCE LoadLibrary(

LPCTSTR lpLibFileName // имя файла динамической библиотеки

);

Эта функция возвращает ссылку (instance) на DLL. Для 32-х битовых DLL instance это базовый виртуальный адрес области памяти, в которую система загрузила DLL. Это значение используется как параметр в других API функциях работающих с DLL.

Функция FreeLibrary используется для выгрузки DLL. Функция имеет единственный параметр - instance возвращаемый предыдущей функцией. Заметим, что подобно другим объектам Windows DLL загружается в память (создаётся) только при первом вызове LoadLibrary. Последующие вызовы этой функции из одного процесса возвращают то же значение instance, что и первый вызов, и увеличивают счётчик загрузки DLL. Напротив, каждый вызов FreeLibrary уменьшает этот счётчик, и когда он станет равным нулю, система выгружает DLL.

После загрузки библиотеки следует определить адрес нужной библиотечной функции. Для этого используется функция API GetProcAddress.

FARPROC GetProcAddress(

//

ссылка на DLL (instance)

HMODULE hModule,

LPCSTR lpProcName

//

имя функции

);

 

 

FARPROC это стандартный тип указателя на функцию. hModule параметр instance возвращённый функцией LoadLibrary и lpProcName указатель на имя функции. Адрес функции может быть так же получен по её индексу. В этом случае lpProcName интерпретируется не как указатель на строку, а как значение типа DWORD, старшее слово которого равно нулю а младшее содержит индекс функции. GetProcAddress интерпретирует lpProcName как индекс функции если старшее слово равно нулю. Следующие примеры на языках Pascal (Delphi) и C иллюстрируют приёмы работы с функцией GetProcAddress:

Пример 1. Pascal.

77

Var

{указатель на импортируемую функцию}

MyDLLFunction : function (A, B : Integer) : Integer; {DLL handle}

HANDLE : hDLL;

xA, xB xC : Ineger;

...

{загрузить dll}

hDLL := LoadLibrary(‘DLLName’);

{получить адрес функции с именем MyFunction из dll}

@MyDLLFunction := GetProcAddress(hDLL, ‘MyFunction’);

{вызвать функцию}

xC := MyDLLFunction(xA, xB);

{выгрузить dll}

FreeLibrary(hDLL);

Пример 2. C:

typedef UINT (CALLBACK* LPMYFUNCTION)(int, int);

HINSTANCE hDLL;

//

ссылка на

DLL

LPMYFUNCTION MyDLLFunction; //

указатель

на функцию

int xA, xB, xC;

//загрузить dll

hDLL = LoadLibrary(«DLLName»);

//получить адрес функции с именем MyFunction из dll

MyDLLFunction = (LPMYFUNCTION)GetProcAddress(hDLL, "MyFunction");

//вызвать функцию

xC = MyFunction(xA, xB);

//выгрузить dll

FreeLibrary(hDLL);

LoadLibrary и GetProcAddress возвращают 0 в случае ошибки.

Косвенная компоновка (Implicit loading).

Как правило, для подключения динамических библиотек к приложению используется косвенная компоновка. Этот тип компоновки основан на описанной выше технологии использующей таблицы импорта и экспорта. Библиотека автоматически подключается к приложению во время загрузки последнего. Для организации косвенной компоновки требуется внести ряд дополнений в исходный текст приложения и динамической библиотеки. Языки программирования C и Pascal имеют специальные директивы компилятора для размещения и инициализации таблиц импорта и экспорта.

Delphi.

В Delphi для объявления импортируемых функций используется директива external. Функции можно импортировать по имени, по имени по умолчанию и по индексу. Во всех случаях функция должна быть объявлена с использованием директивы external.

Импорт по имени:

Function MyDLLFunction(A, B : Integer) : Integer;

external «DLLName» name «MyFunction»;

Имя функции в динамической библиотеке «MyFunction» но в исходном тексте приложения используется имя «MyDLLFunction»

Импорт по имени по умолчанию:

Function MyFunction(A, B : Integer) : Integer;

external «DLLName»;

Имена функции в DLL и в тексте приложения одинаковы.

78

Импорт по индексу:

Function MyFunction(A, B : Integer) : Integer;

external «DLLName» index 10;

Индекс функции в DLL равен 10. В тексте приложения используется имя «MyFunction».

Как правило при разработке DLL создаётся отдельный модуль (unit) с прототипами функций экспортируемых библиотекой (как в приведённом выше примере). Этот модуль с помощью директивы Uses подключается к приложению.

{Интерфейсный модуль DLL MyDLL.pas}

Unit MyDLL;

interface

function MyFunction(A, B : Integer) : Integer;

implementation

function MyFunction(A, B : Integer) : Integer; external «MyDLL»; end.

{основной модуль приложения }

Uses MyDLL;

...

C := MyFunction(10, 20);

В отличие от C, Delphi не использует каких либо дополнительных файлов для подключения библиотеки к приложению в процессе компиляции. Рассмотрим теперь пример для Visual C.

Visual C.

C требует создания .lib файла для динамической библиотеки. Компилятор автоматически создаёт при компиляции DLL.Этот файл содержит информацию необходимую для создания таблицы импорта в файле приложения, которое будет использовать DLL. Вместе с файлом lib разработчик DLL должен предоставить .h файл с описанием прототипов экспортируемых функций. Эти прототипы объявляются с директивой __declspec(dllimport). Например:

__declspec(dllimport) __stdcall int MyFunction(int A, int B);

Вызов функции из приложения выглядит, например, так:

C = MyFunction(A, B);

В этом примере функция объявлена с использованием директивы __stdcall. Для таких функций компилятор использует правила вызова языка Паскаль. Разница в правилах вызова C и stdcall заключается в том, в какой последовательности параметры функции помещаются в стек перед вызовом. Для stdcall первый параметр помещается первым в стек, а для C последний параметр помещается первым. Например, вызов функции MyFunction по правилам C выглядит следующим образом:

Push B

Push A

Call MyFunction

, а если MyFunction объявлена с директивой __stdcall:

Push A

Push B

Call MyFunction

Как правило, для функций экспортируемых DLL используется правила вызова stdcall, так как DLL может быть использована программами написанными на разных языках.

79

Следует заметить, что загрузчик должен обнаружить динамические библиотеки, подключённые к приложению при его загрузке. Загрузчик последовательно ищет динамические библиотеки в текущем каталоге, в каталоге Window\System в каталоге \Windows и затем в каталогах указанных в переменных среды окружения программы

(environment block).

Процедура создания динамической библиотки.

Динамические библиотеки используются пользовательскими и системными программами. Функции API Windows размещаются в системных динамических библиотеках. Часто возникает необходимость в создании собственных DLL. Ниже приводятся правила написания динамических библиотек на Delphi и Visual.

Delphi.

Проект dll создаётся из пункта меню File->New. Delphi создаёт основной файл проекта с директивой Library в первой строке. Эта директива указывает компилятору на то, что исполняемый файл динамическая библиотека в не приложение.

Library MyDLL; {MyDLL имя DLL}

Каждая экспортируемая функция должна быть объявлена с директивой export.

Function MyFunction(A, B : Integer) : Integer; StdCall; Export; begin

...

end;

Дополнительно в исходном тексте Dll необходимо разместить список всех экспортируемых функций. Список задаётся директивой exports.

Exports MyFunction;

MyFunction2 index 10; MyFunction3 name ‘MyFunction100’ {...}

End;

В этом списке можно дополнительно указать индекс функции или изменить имя, под которым она будет экспортирована. Как уже было отмечено выше, вместе с DLL следует поставлять интерфейсный модуль (*.PAS) содержащий прототипы функций.

Visual C.

С помощью Visual C 5.0 можно создавать динамические библиотеки двух типов. DLL которые используют библиотеку Microsoft Foundation Class (MFC) и DLL которые ёё не используют (non-MFC DLLs).

Обычная, не-MFC DLL, создаётся в пункте меню File->New->Project->Win32 Dynamic-link Library. Существует два способа объявления экспортируемых функций. Первый (рекомендуемый) способ поддерживается в версиях VC, начиная с 5.0. Экспортируемая функция объявляется директивой __declspec(dllexport):

__declspec(dllexport) __stdcall int MyFunction(int A, int B );

Объявление функции обычно помещается в отдельный файл заголовок (header file) библиотеки. Этот файл может применяться при компиляции библиотеки и при компиляции приложений которые используют эту библиотеку. Однако для библиотеки эти функции экспортируемые, а для приложений импортируемые. Импортируемые функции объявляются директивой __declspec(dllimport) а экспортируемые директивой __declspec(dllexport). Для того чтобы использовать в обоих случаях один и тот же заголовочный файл применяют директивы условной компиляции и вносят изменения в исходный текст библиотеки:

Исходный текст DLL:

80

#define _MYDLLNAME_

Заголовочный файл:

#if !defined(_MYDLLNAME_)

#define MYDLLAPI __declspec(dllimport) #else

#define MYDLLAPI __declspec(dllexport) #endif

//объявления функций

...

int MYDLLAPI MyFunction(int A, int B);

В приложениях символ _MYDLLNAME_ не определяется, поэтому для них все функции объявлены с директивой __declspec(dllimport) ,а для библиотеки с директивой

__declspec(dllexport).

Экспортируемые функции могут быть так же указаны в .DEF файле проекта DLL. Это другой метод. .DEF файл должен содержать оператор LIBRARY, который определяет имя библиотеки и оператор EXPORT содержащий список экспортируемых функций. Ниже приводится пример .DEF файла:

LIBRARY MYDLL

EXPORTS

MyFunction @1

MyFunction1 @2

MyFunction2 @3

MyFunction3 @4

Воператоре EXPORT также задаются индексы функций.

Вобоих методах компилятор создаёт .lib файл для динамической библиотеки. Этот lib файл вместе с заголовочным файлом необходим для создания Visual C приложений которые используют динамическую библиотеку.

Имена функций C и C++.

Компилятор помещает в файлы lib и dll имена функций отличающиеся от указанных в тексте программы. Компилятор использует специальные соглашения - правила преобразования имён функций. Имена функций в C и в C++ преобразуются по разным правилам. Одинаковые имена функций в текстах программ написанных на С и на C++ будут разными в исполняемых модулях. Это не имеет значения если скажем библиотека написана на С++ и приложения которые её используют также написаны на С++. Проблема возникает в случае если написанную на С++ библиотеку требуется подключить к приложению написанному на языке С. Одним из способов решения является прямое указание правила преобразования имени функции в тексте программы. Следующий пример иллюстрирует способ объявления правила преобразования имени С для функции в файле C++.

#ifdef __cplusplus //если это текст c++ extern «C» {

#endif

int MYDLLAPI MyFunction(int A, int B);

#ifdef __cplusplus //если это текст c++

}

#endif

Символ __cplusplus объявляется автоматически для файлов c++.

В приведённом примере имя функции будет преобразовано по правилам С в независимости от типа файла исходного текста.

81

Точка входа DLL.

Каждая динамическая библиотека имеет точку входа. Точка входа это функция с определённым именем, которая вызывается операционной системой в нескольких специальных случаях. По умолчанию имя этой функции DllMain. Имя можно изменить с помощью директивы командной строки компоновщика /ENTRY. Ниже приводится шаблон функции DllMain:

BOOL APIENTRY DllMain(HANDLE

hModule,

DWORD

ul_reason_for_call,

LPVOID

lpReserved)

{

 

switch( ul_reason_for_call ) { case DLL_PROCESS_ATTACH:

...

case DLL_THREAD_ATTACH:

...

case DLL_THREAD_DETACH:

...

case DLL_PROCESS_DETACH:

...

}

return TRUE;

}

Система при вызове функции передаёт следующие параметры:

hModule ссылка на DLL. В действительности для 32-ч разрядных библиотек этот параметр содержит базовый (начальный) адрес по которому загружена библиотека в виртуальном адресном пространстве процесса.Двойное слово ul_reason_for_call содержит код события которое было причиной вызова функции. Последний параметр зарезервирован и не используется. Используя эту функцию DLL может перехватить четыре различных события коды которых указываются в параметре ul_reson_for_call. DLL_PROCESS_ATTACH новый процесс загружает динамическую библиотеку автоматически или функцией LoadLibrary.

DLL_PROCESS_DETACH процесс который использовал динамическую библиотеку завершается или выгружает её функцией FreeLibrary.

DLL_THREAD_ATTACH в одном из процессов использующих DLL создан новый поток. DLL_THREAD_DETACH в одном из процессов использующих DLL завершился поток.

Следует отметить, что если процесс или поток завершаются функциями соответственно TerminateProcess и TerminateThread динамическая библиотека не получает сообщений DLL_PROCESS_DETACH и DLL_TREAD_DETACH. Так же важно учесть, что общее количество сообщений DLL_TERAD_DATACH может быть больше чем количество сообщений DLL_TREAD_ATTACH так как некоторые потоки процесса могут быть созданы в перед загрузкой динамической библиотеки. Это происходит в частности если библиотека загружается функцией LoadLibrary после того как первый поток процесса уже создан.

Использование Локальной Области Потока (Thread Local Storage) в динамических библиотеках.

По умолчанию данные процесса являются общими для всех потоков этого процесса. Однако в ряде случаев необходимо организация частных областей памяти для каждого потока приложения так чтобы одни и те же виртуальные адреса в указанном диапазоне отображались в разные физические адреса для разных потоков одного процесса. Эта задача может быть решена в Visual C с помощью директивы __declspec(thread) (см. выше). Однако этот метод имеет недостаток. Он не работает в динамической библиотеке, если она загружена функцией LoadLibrary. Он может быть применён только в случае, если DLL загружается автоматически. Есть другой более универсальный способ. Он основан на использовании Локальной Области Потока (TLS). Windows API включает четыре функции предназначенные для размещения и управления данными в TLS. Эти данные являются частными для каждого потока приложения.

DWORD TlsAlloc(VOID)

LPVOID TlsGetValue(DWORD dwTlsIndex)

BOOL TlsSetValue(DWORD dwTlsIndex, DWORD lpvTlsValue)

82

BOOL TlsFree(DWORD dwTlsIndex);

TLS организована в виде так называемых слотов которые используются для ссылок на данные размещённые в TLS отдельно для каждого потока. Каждый процесс может использовать до 64-х слотов. Функция TlsAlloc резервирует слот и возвращает его индекс. Этот индекс используется потоками процесса для ссылки на 32-х разрядный параметр. Для каждого потока значение этого параметра уникально. Как правило, параметр это указатель на частные данные потока (см. пример). Поток сохраняет значение параметра в TLS с помощью функции TlsSetValue и может получить это значение вызвав функцию TlsGetValue. Использующийся слот должен быть освобождён перед завершением работы приложения. Для этой операции используется функция

TlsFree.

 

 

Поток 1

Индекс TLS

 

1

 

 

2

 

Поток 2

3

свободно

...

 

 

64

свободно

 

TlsIndex = TlsAlloc();

Поток 3

 

Рисунок 5. Организация частной памяти для потоков. Thread Local Storage(TLS).

Процедуры инициализации и деинициализации TLS, как правило, помещают в функцию DllMain библиотеки.

static DWORD dwTlsIndex;

BOOL WINAPI DllMain (HINSTANCE hInstance, DWORD dwReason, LPVOID lpvRsv)

{

switch(dwReason) {

case DLL_PROCESS_ATTACH:

//резервируем слот TLS

dwTlsIndex = TlsAlloc();

//продолжаем инициализацию параметра для первого потока.

case DLL_THREAD_ATTACH:

//размещаем память под данные потока и сохраняем указатель //на них в TLS

TlsSetValue(dwTlsIndex, malloc (SIZE_OF_PER_THREAD_DATA)); break;

case DLL_TREAD_DETACH:

//освобождаем память выделенную под данные потока. Функция //TlsGetValue возвращает указатель на эти данные.

free(TlsGetValue(dwTlsIndex));

break;

case DLL_PROCESS_DETACH:

free (TlsGetValue (TlsIndex));

//освобождаем слот TLS

TlsFree(dwTlsIndex) ; break;

}

return TRUE;

}

83

Каждый поток может получить указатель на свои данные, вызвав функцию TlsGetValue.

84