Динамическое изменение уровня приоритета потока



Тема: Многозадачность и многопоточность в ОС Windows

 

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

Процесс – это загруженная в память и готовая к исполнению программа. Каждый процесс имеет свое собственное виртуальное адресное пространство (до 4Gb). Процесс состоит из программного кода, данных и системных ресурсов, необходимых для работы программы.

Поток ( thread ) – часть процесса, которой операционная система выделяет время процессора. Любой процесс включает один первичный поток. В дальнейшем он может порождать другие потоки. Ресурсы процесса доступны всем его потокам. Все потоки одного процесса совместно используют его виртуальное адресное пространство.

Контекст исполнения потока - специальная структура данных, в которую сохраняются регистры процессора, переменные окружения, стеки ядра и пользователя на время, когда у потока отнимается процессорное время.

Дополнительные потоки в процессе (многопоточность) организуются в следующих случаях:

· управление вводом от нескольких устройств.

· наличие в приложении задач с различными приоритетами.

· для приложений с многодокументным интерфейсом (MDI)

· при выполнении программы на многопроцессорной системе.

 

Распределение времени между потоками

Процессорное время распределяется по очереди между потоками, а не между процессами. Продолжительность выделяемого кванта времени составляет около 20 мс.

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

Уровни приоритетов варьируются в диапазоне от 0 (низший) до 31 (высший).

Уровень приоритета каждого потока состоит из трех составляющих:

· класс приоритета процесса

· уровень приоритета потока внутри класса приоритета процесса

· динамически установленный уровень приоритета.

Классы и базовые уровни приоритетов процессов

ОС Windows 2000/XP поддерживают 6 классов приоритета, которые приведены в таблице.

Класс приоритета Базовый уровень приоритета
Idle (простаивающий) 4
Below normal (ниже обычного) 6
Normal (обычный) 8
Above normal (выше обычного) 10
High (высокий) 13
Real-time (реального времени) 24

 

В базовой версии Windows NT использовалось 4 класса приоритета: Idle, Normal, High, Real-time.

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

Уровень Idle назначается процессу, который ничего не должен делать в случае активности других процессов (например, хранитель экрана).

Процессам, запускаемым пользователем, присваивается нормальный уровень (Normal). Пользователь может запустить несколько процессов. Тому процессу, с которым пользователь непосредственно работает, уровень приоритета увеличивается на две единицы.

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

Класс Real-time назначается процессу только в особых случаях, например, когда необходимо постоянно контролировать работу оборудования (опрос датчиков).

Самый распространенный класс приоритета — Normal, его использует 99% приложений.

Уровни приоритетов потоков

ОС Windows 2000/XP поддерживает 7 относительных приоритетов потоков, которые приведены в таблице. Эти приоритеты относительны классу приоритета процесса.

Обозначение Уровень приоритета потока
Idle (простаивающий)   Равен 16 в классе Real-time и 1 в других классах
Lowest (низший) На 2 ниже уровня класса
Below normal (ниже обычного) На 1 ниже уровня класса
Normal (обычный) Равен уровню класса
Above normal (выше обычного) На 1 выше уровня класса
Highest (высший) На 2 выше уровня класса
Time-critical (критичный по времени)   Равен 31 в классе Real-time и 15 в других классах

В базовой версии Windows NT использовалось 5 относительных приоритетов потоков: Lowest, Below normal, Normal, Above normal, Highest.

 

Динамическое изменение уровня приоритета потока

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

 

ПРИМЕЧАНИЕ:

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

 

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

В библиотеке Win32 API имеется набор функций для работы с процессами. Список основных функций этой группы приведен в таблице:

Название функции Выполняемое действие
CreateProcess() Создает процесс
ExitProcess() Завершает процесс и все его потоки
GetCurrentProcess() Возвращает описатель (handle) текущего процесса
GetPriorityClass() Возвращает класс приоритета процесса
SetPriorityClass() Устанавливает класс приоритета процесса
TerminateProcess() Завершает указанный процесс из другого процесса

Подробное описание этих функций приведено в справочном руководстве по Win32 API. Рассмотрим функцию создания процесса.

 

Функция CreateProcess ()

Создает новый процесс и его первичный поток. Новый процесс выполняет указанный исполняемый файл.

Формат функции:

BOOL CreateProcess(

LPCTSTR lpApplicationName

LPTSTR lpCommandLine,

LPSECURITY_ATTRIBUTES lpProcessAttributes,

LPSECURITY_ATTRIBUTES lpThreadAttributes,

BOOL bInheritHandles,   

DWORD dwCreationFlags,     

LPVOID lpEnvironment,  

LPCTSTR lpCurrentDirectory

LPSTARTUPINFO lpStartupInfo,     

LPPROCESS_INFORMATION lpProcessInformation);

Функция возвращает TRUE в случае успеха и FALSE - в случае неудачи.

Параметры функции:

lpApplicationName - указатель на строку, содержащую имя исполняемой программы. Если оно не полное, то поиск файла производится в текущем каталоге. Параметру может быть присвоено значение NULL;

lpCommandLine - указатель командной строки. Если параметр lpApplicationName имеет значение NULL, то имя исполняемого файла выделяется из lpCommandLine , а поиск исполняемого файла производится в соответствии с правилами, действующими в системе;

lpProcessAttributes - указатель на структуру, описывающую параметры защиты процесса. Если параметру присвоено значение NULL, то устанавливаются атрибуты «по умолчанию»;

lpThreadAttributes - указатель на структуру, описывающую параметры защиты первичного потока. Если параметру присвоено значение NULL, то устанавливаются атрибуты «по умолчанию»;

bInheritHandles - определяет, будет ли порожденный процесс наследовать описатели (handles) объектов родительского процесса;

dwCreationFlags - определяет некоторые дополнительные условия создания процесса и его класс приоритета;

lpEnvironment - указатель на блок переменных среды порожденного процесса. Если этот параметр равен NULL, то порожденный процесс наследует среду родителя;

lpCurrentDirectory - указатель на строку, содержащую полное имя текущего каталога порожденного процесса. Если этот параметр равен NULL, то порожденный процесс наследует каталог родителя;

lpStartupInfo - указатель на структуру STARTUPINFO, которая определяет параметры главного окна порожденного процесса;

lpProcessInformation - указатель на структуру PROCESS_INFORMATION, которая будет заполнена информацией о порожденном процессе после возврата из функции. Содержание этой структуры следующее:

typedef struct _PROCESS_INFORMATION {

HANDLE hProcess; // Дескриптор процесса

HANDLE hThread; // Дескриптор его первичного потока

DWORD dwProcessId; // Идентификатор процесса

DWORD dwThreadId; // Идентификатор первичного потока

} PROCESS_INFORMATION;

 

Пример: консольное приложение, запускающее программу Microsoft Word.

#include <windows.h>

#include <conio.h>

#include <stdio.h>

main()

{     

PROCESS_INFORMATION pi;

   STARTUPINFO si;

   ZeroMemory( &si, sizeof(si)); // Очистка si нулями

   si.cb = sizeof(si);

   printf( "Press any key to start WinWord -- " );

   getch() ;

CreateProcess( NULL,"WinWord", NULL, NULL, FALSE, 0,NULL, NULL, &si, &pi ) ;

   return 0 ;

}       


Тема: Управление потоками в приложениях

 

Функции для работы с потоками

В библиотеке Win32 API имеется набор функций для работы с потоками. Список основных функций этой группы приведен в таблице:

Название функции Выполняемое действие
CreateThread() Создает поток
ExitThread() Завершает поток
GetCurrentThread() Возвращает описатель (handle) текущего потока
GetThreadPriority() Сообщает приоритет указанного потока
SetThreadPriority() Устанавливает приоритет указанного потока
Sleep() Задерживает исполнение потока на указанное количество миллисекунд
ResumeThread() Запускает исполнение потока
SuspendThread() Приостанавливает исполнение указанного потока
TerminateThread() Завершает указанный поток

Подробное описание этих функций приведено в справочном руководстве по Win32 API. Рассмотрим некоторые из этих функций.

 

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

Функция CreateThread() создает новый поток в адресном пространстве процесса. Формат функции:

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes,    

DWORD dwStackSize,

LPTHREAD_START_ROUTINE lpStartAddress,      

LPVOID lpParameter, 

DWORD dwCreationFlags,

LPDWORD lpThreadId);

Функция возвращает описатель созданного потока.

Параметры:

lpThreadAttributes - указатель на структуру, описывающую параметры защиты потока. Если параметру присвоено значение NULL, то устанавливаются атрибуты «по умолчанию»;

dwStackSize - устанавливает размер стека, который отводится потоку. Если параметр равен нулю, то устанавливается стек, равный стеку первичного потока;

lpStartAddress - адрес функции, которую будет исполнять поток. Функция имеет один 32-битный аргумент и возвращает 32-битное значение;

lpParameter - параметр, передаваемый в функцию, которую будет исполнять поток;

dwCreationFlags - дополнительный флаг, который управляет созданием потока. Если этот параметр равен CREATE_SUSPENDED, то поток после порождения не запускается на исполнение до вызова функции ResumeThread();

lpThreadId - указатель на 32-битную переменную, которой будет присвоено значение уникального идентификатора потока.

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

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

 

Функция потока

Каждый поток начинает выполнение с некоей входной функции. В первичном потоке таковой является main() или WinMain(). Если в программе создается вторичный поток, в нем тоже должна быть входная функция, которая выглядит примерно так:

DWORD WINAPI ThreadFunc(PVOID pvParam)

{

DWORD dwResult = 0;

... // Действия, выполняемые потоком

return dwResult;

}

Основные особенности функций потоков:

- в отличие от входной функции первичного потока, у которой может быть одно из имен: main, WinMain, — функцию потока можно назвать произвольно.

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

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

Имя функции потока указывается в третьем параметре функции CreateThread().

Пример: создание нового потока, функция которого выводит на экран символ А с интервалом 0,5 сек.

#include <stdio.h>

#include <conio.h>

#include <windows.h>

// Описание функции, выполняемой в потоке

DWORD WINAPI Output(PVOID pvParam )

{ while( TRUE ) {

           printf( "A" );

           Sleep(500);

   }

   return 0;

}

main()

{     

HANDLE hThread;

DWORD ThreadId;

hThread=CreateThread(NULL, 0, Output,NULL,0, &ThreadId);

getch();

TerminateThread(hThread, 0 ); // Уничтожение потока

return 0;

}

 

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

Поток можно завершить четырьмя способами:

1)функция потока сама завершает выполнение и возвращает управление (рекомендуемый способ);

2) поток самоуничтожается вызовом функции ExitThread() (нежелательный способ);

3) один из потоков данного или стороннего процесса вызывает функцию TerminateThread() (нежелательный способ);

4) завершается процесс, содержащий данный поток (тоже нежелательно).

 

Возврат управления функцией потока (1-й способ)

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

- любые С++-объекты, созданные данным потоком, уничтожаются соответствующими деструкторами;

- система корректно освобождает память, которую занимал стек потока;

- система устанавливает код завершения данного потока и его возвращает функция потока;

- счетчик пользователей данного объекта ядра "Поток" уменьшается на 1.

 

Функция ExitThread (2-й способ)

Поток можно завершить принудительно, вызвав в нем функцию:

VOID ExitThread (DWORD dwExitCode);

При этом освобождаются все ресурсы ОС, выделенные данному потоку, уничтожается его стек, но C++- объекты не очищаются.

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

 

Функция TerminateThread (3-й способ)

Вызов этой функции также завершает поток:

BOOL TerminateThread (HANDLE hThread, DWORD dwExitCode);

В отличие от ExitThread(), которая уничтожает только вызывающий поток, эта функция завершает поток, указанный в параметре h T h read.

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

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

 

4-й способ

При завершении процесса ОС автоматически освобождает все ресурсы, связанные с этим процессом.

 

Примечание. Для создания и завершения потоков кроме Win32 API функций (CreateThread , ExitThread, TerminateThread) можно использовать функции стандартной библиотеки языка С/С++: _beginthreadex() и _endthreadex().

Создание потока :

unsigned long _beginthreadex(

void *security,

unsigned stack_size,

unsigned (*start_address)(void *),

void *arglist,

unsigned initflag,

unsigned *thrdaddr);

У функции _begint h readex() тот же список параметров, что и у CreateT h read(), но их имена и типы несколько отличаются. Как и CreateThread(), функция _beginthreadex() возвращает описатель только что созданного потока.

Завершение потока :

void _endthreadex (unsigned retcode);

параметр retcode - код завершения потока.

 

Приостановка и возобновление потоков

В объекте ядра «Поток» имеется переменная — счетчик числа простоев данного потока. При создании потока онинициализируется значением, равным 1, которое запрещает системе сразу выделять новому потоку процессорное время.

Если при этом в функцию CreateProcess или CreateThread передан флаг CREATE_SUSPENDED, то поток остается в приостановленном состоянии. В ином случае счетчик простоев обнуляется, и поток включается в число планируемых.

Запустить поток в работу можно с помощью функции: DWORD ResumeThread(HANDLE hThread);

где hThread - описатель потока, возвращенный функцией CreateThread.

Если вызов ResumeThread прошел успешно, она возвращает предыдущее значение счетчика простоев данного потока; в ином случае — OxFFFFFFFF.

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

DWORD SuspendThread(HANDLE hThread);

Даннаяфункция возвращает предыдущее значение счетчика простоев данного потока.

 

Задержка исполнения потока на указанное время

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

VOID Sleep(DWORD dwMilliseconds);

где dwMilliseconds задает интервал времени вмиллисекундах, в течение которого приостанавливается работа потока.

Если передать в dwMilliseconds значение INFINITE, то будет запрещено ОС выделять потоку кванты времени. Это равноценно завершению работы потока до конца работы процесса.

 

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

Функция SwitchToThread позволяет подключить к процессору другой поток:

BOOL SwitchToThread();

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

Она возвращает FALSE, если на момент ее вызова в системе нет ни одного потока, готового к исполнению; в ином случае — ненулевое значение.

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

 

Управление приоритетами потоков

Только что созданный поток получает относительный приоритет, равный уровню класса приоритета процесса.

Изменение приоритета потока осуществляется вызовом функции:

BOOL SetThreadPriority(HANDLE hThread, int nPriority);

Параметр h Thread указывает на поток, чей приоритет необходимо изменить, а через nPriority передается один из идентификаторов приоритета (см. таблицу).

Уровень приоритета Обозначение уровня приоритета
Idle THREAD_PRIORITY_IDLE
Lowest THREAD_PRIORITY_LOWEST
Below normal THREAD_PRIORITY_BELOW_NORMAL
Normal THREAD_PRIORITY_NORMAL
Above normal THREAD_PRIORITY_ABOVE_NORMAL
Highest THREAD_PRIORITY_HIGHEST
Time critical THREAD_PRIORITY_TIME_CRITICAL

 

Относительный приоритет потока позволяет узнать функция:

int GetThreadPriority(HANDLE hThread);

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

Чтобы изменить относительный приоритет потока необходимо приостановить его, затем задать новый приоритет и возобновить его работу, например:

HANDLE hThread = CreateThread(…);

...

SuspendThread(HANDLE hThread);

SetThreadPriority(hThread, THREAD_PRIORITY_IDLE);

ResumeThread(hThread);

...

 

Пример многопоточной Windows-программы

В оконной процедуре главного окна приложения создается вторичный поток (Поток1) в приостановленном состоянии. Функция этого потока выводит в главное окно прямоугольник, закрашенный случайным цветом, с интервалом 0,3 сек. Изменение состояния потока (запуск, приостановка, завершение) производится с помощью соответствующих кнопок на немодальной диалоговой панели.

#include "stdafx.h"

#include "stdlib.h"

// Прототип функции главного окна

LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

// Прототип функции потока

DWORD WINAPI ThreadFunc1(PVOID pvParam);

// Прототип диалоговой процедуры немодального диалога

BOOL CALLBACK DlgModalessProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);

// Структура для передачи данных в поток1

struct PARAM

{  

HWND hWnd; // окно, в котором создается поток

BOOL stop; // признак завершения работы потока

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

};

// Описание дескриптора и идентификатора потока

HANDLE hThread1;

DWORD ThreadId1;

// дескриптор окна немодального диалога

HWND hDlgModaless=NULL;

// --- Функция WinMain

int APIENTRY WinMain( …)

{

 …

 // Создание главного окна приложения.

 hWndMain=CreateWindow( …);

 …

 // Создание окна немодального диалога

 hDlgModaless=CreateDialog(hInst, MAKEINTRESOURCE(

           IDR_DIALOG2), hWnd, DlgModalessProc);

 ...

}

// Оконная процедура главного окна

LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)

{

 static struct PARAM p; // структура для управления потоком

 switch(msg)

 {

 // Сообщение о создании окна.

case WM_CREATE:

{

// запись в поля структуры p

p.hWnd=hWnd; p.stop=0;

// создание потока1 в приостановленном состоянии

hThread1=CreateThread(NULL,0,ThreadFunc1,(PVOID)(&p),  

              CREATE_SUSPENDED,&ThreadId1);

}; break;

...

}

 return 0L;

}

// Функция потока1

DWORD WINAPI ThreadFunc1(PVOID pvParam)

{

 DWORD dwResult=0;

 // получение адреса общей памяти потоков

struct PARAM *p=(struct PARAM *)pvParam;

// контекст устройства для рисования

HDC hdc=GetDC(p->hWnd); 

// В цикле выполняются действия потоком1

while(!p->stop)

{

// Создание кисти со случайным цветом заливки

HBRUSH hRndBrush=CreateSolidBrush(RGB(rand()%256,

rand()%256, rand()%256));

SelectObject(hdc,hRndBrush);

Rectangle(hdc,200,150,400,300);

Sleep(300); // задержка на 0,3 сек

 }

 ReleaseDC(p->hWnd, hdc);

 return(dwResult);

}

// Диалоговая процедура немодального диалога

BOOL CALLBACK DlgModalessProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)

{

switch(message) {

...

case WM_COMMAND: {

switch(LOWORD(wParam) {

    case IDC_BUTTON1: // Нажата кнопка Запустить

   ResumeThread(hThread1); // Запуск потока1 в работу

   break;

    case IDC_BUTTON2: // Нажата кнопка Приостановить

   SuspendThread(hThread1); // Приостановка потока1   

   break;

    case IDC_BUTTON3: // Нажата кнопка Завершить

   p.stop=1; // установка в 1 признака завершения

// или TerminateThread(hThread1,0);  

   break;

    ...

   }

}; break;

...

}

return FALSE;

}

Тема: Синхронизация потоков

 

Необходимость синхронизации потоков

Все потоки в ОС Windows должны иметь доступ к различным системным ресурсам — памяти, окнам, файлам, портам ввода-вывода и т. д. Если одному из потоков будет предоставлен монопольный доступ к какому-либо ресурсу, другие потоки, которым тоже нужен этот ресурс, не смогут выполнить свои задачи. С другой стороны, бесконтрольное использование ресурсов потоками может привести к конфликтам или неправильным результатам. Например, один поток пишет в блок памяти, из которого другой поток в это время производит считывание и вывод данных.

DWORD a[5]; // Объявление глобального массива

// Функция потока 1

DWORD WINAPI ThreadFunc1(PVOID pvParam)

{ int i; DWORD dwResult = 0, num=0;

struct PARAM *p= (struct PARAM *) pvParam;

while (!p->stop)

{

for ( i = 0; i < 5; i++ ) a[ i ] = num;

num++;

}

return(dwResult);

}

// Функция потока 2

DWORD WINAPI ThreadFunc2(PVOID pvParam)

{ int i; DWORD dwResult = 0;

struct PARAM *p= (struct PARAM *) pvParam;

while (!p->stop)

{

for ( i = 0; i < 5; i++ ) printf("%ld ", a[ i ]);

printf("\n");

}

return(dwResult);

}

В результате, 1-й поток последовательно заполняет массив a[5] возрастающими сериями по 5 значений, а второй поток, работая параллельно, считывает и выводит на экран фрагменты этих серий чисел.

Работа двух и более потоков должна быть синхронизирована в следующих случаях:

- потоки совместно используют разделяемый ресурс;

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

 

Средства синхронизации

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

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

Для синхронизации потоков в Windows предусмотре-ны следующие средства:

- критический раздел (секция);

- семафор;

- мьютекс;

- событие;

- ожидаемый таймер.

 

Критические разделы (секции)

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

Работа с критическими разделами производится следующим образом.

Вначале необходимо определить объект критический раздел, который объявляется как глобальная переменная типа CRITICAL_SECTION. Например:

CRITICAL_SECTION cs;

Тип данных CRITICAL_SECTION является структурой, но ее поля используются только самой Windows.

Имеется четыре функции для работы с критическими разделами.

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

VOID InitializeCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
где lpCriticalSection - указатель на переменную типа CRITICAL_SECTION.

Например: InitializeCriticalSection(&cs);

Эта функция создает критический раздел с именем cs. Чаще всего этот объект создается при инициализации процесса, т.е. первичным потоком в функции WinMain.

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

VOID EnterCriticalSection (LPCRITICAL_SECTION lpCriticalSection);

Например: EnterCriticalSection(&cs);

В этот момент именно этот поток становится владельцем объекта.

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

VOID LeaveCriticalSection (LPCRITICAL_SECTION lpCriticalSection);

Например: LeaveCriticalSection(&cs);

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

4. Когда объект критический раздел больше не нужен процессу, его можно удалить (обычно в функции WinMain) с помощью функции:

VOID DeleteCriticalSection (LPCRITICAL_SECTION lpCriticalSection);

Например: DeleteCriticalSection(&cs);
Примечания.

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

EnterCriticalSection(&cs);

// критичекий фрагмент кода потока

..............

LeaveCriticalSection(&cs);

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

 

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

Синхронизируется работа двух потоков при заполнении элементов массива значениями и выводе их на экран.

CRITICAL_SECTION cs;

DWORD a[5]; // Объявление глобального массива

// Функция потока 1

DWORD WINAPI ThreadFunc1(PVOID pvParam)

{ int i; DWORD dwResult = 0, num=0;

struct PARAM *p= (struct PARAM *) pvParam;

while (!p->stop)

{

EnterCriticalSection(&cs);

for ( i = 0; i < 5; i++ ) a[ i ] = num;

LeaveCriticalSection(&cs);

num++;

}

return(dwResult);

}

// Функция потока 2

DWORD WINAPI ThreadFunc2(PVOID pvParam)

{ int i; DWORD dwResult = 0;

struct PARAM *p= (struct PARAM *) pvParam;

while (!p->stop)

{

EnterCriticalSection(&cs);

for ( i = 0; i < 5; i++ ) printf("%ld ", a[ i ]);

printf("\n");

LeaveCriticalSection(&cs);

}

return(dwResult);

}

// --- Функция WinMain

int APIENTRY WinMain()

{

// Создание объекта критический раздел

InitializeCriticalSection(&cs);

 …

// Удаление объекта критический раздел

// (при необходимости)

DeleteCriticalSection(&cs);

 …

}

Потоки по очереди заполняют и правильно выводят элементы массива.


Средства синхронизации потоков с использованием объектов ядра.

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

 

Функции ожидания потоков ( Wait -функции)

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

1. Функция WaitForSingleObject() обеспечивает ожи-дание освобождения синхронизирующего объекта :

DWORD WaitForSingleObject (HANDLE hOblect, DWORD dwMilliseconds);

где hOblect - дескриптор синхронизирующего объекта ядра;

dwMilliseconds - время (в миллисекундах), в течение которого происходит ожидание освобождения объекта ядра. Если передать значение INFINITE (бесконечность), то функция будет ждать бесконечно долго.

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

WAIT_OBJECT_0 - объект освободился;

WAIT_TIMEOUT - время ожидания освобождения прошло, а объект не освободился;

WAIT_FAILED - произошла ошибка.

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

Например:

DWORD res = WaitForSingleObject(hProcess, 5000);

switch (res) {

case WAIT_OBJECT_0:

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

break;

case WAIT_TIMEOUT:

// процесс не завершился в течение 5000 мс

break;

case WAIT_FAILED:

// произошла ошибка

break;

}

2. Функция WaitForMultipleObjects() позволяет ждать освобождения сразу нескольких объектов или какого-то одного из списка объектов:

DWORD WaitForMultipleObjects(DWORD dwCount,

CONST HANDLE* phObjects, BOOL fWaitAll,

DWORD dwMilliseconds);

Параметр dwCount определяет количество синхронизирующих объектов ядра. Его значение должно быть в пределах от 1 до MAXIMUM_WAIT_OBJECTS (в заголовочных файлах Windows оно определено как 64).

Параметр phObjects — это указатель на массив описателей объектов ядра.

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

Параметр dwMilliseconds идентичен одноименному параметру функции WaitForSingleObject.

Возвращаемые функцией WaitForMultipleObjects значения следующие:

- WAIT_TIMEOUT и WAIT_FAILED аналогичны предыдущей функции.

- если параметр f WaitAll равен TRUE и все объекты перешли в свободное состояние, функция возвращает значение WAIT_OBJECT_0.

- если f WaitAll равен FALSE, функция возвращает управление, как только освобождается любой из объектов. При этом возвращается значение от WAIT_OBJECT_0 до WAIT_OBJECT_0 + dwCount - 1.

Таким образом, если возвращаемое значение не равно WAIT_TIMEOUT или WAIT_FAILED, то вычтя из него значение WAIT OBJECT_0, получим индекс в массиве описателей, который указывает какой объект перешел в незанятое состояние.

Например:

HANDLE h[3];

h[0] = hObject1;

h[1] = hObject2;

h[2] = hObject3;

DWORD res=WaitForMultipleObjects(3,h,FALSE,3000);

switch (res) {

case WAIT_FAILED:

// произошла ошибка

break;

case WAIT_TIMEOUT:

// ни один из объектов не освободился

// в течение 3000 мс

break;

case WAIT_OBJECT_0 + 0:

// освободился объект с описателем hObject1

break;

case WAIT_OBJECT_0 + 1:

// освободился объект с описателем hObject2

break;

case WAIT_OBJECT_0 + 2:

// освободился объект с описателем hObject3

break;

}

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

 


Мьютексы

Объекты ядра «мьютексы» (mutual exclusion - mutex) обеспечивают потокам взаимоисключающий доступ к общему ресурсу. Они содержат счетчик числа пользователей, счетчик рекурсии и переменную, в которой запоминается идентификатор потока.

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

В любой момент времени мьютексом может владеть только один поток. Доступ к объекту разрешается, когда поток, владеющий объектом, освободит его.

Наиболее часто, с помощью мьютексов защищают блоки памяти, к которым обращается множество потоков.

Функции для работы с мьютексами следующие:

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

HANDLE CreateMutex (LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName )

где

- lpMutexAttributes - указатель на структуру SECURITY_ATTRIBUTES;
- bInitialOwner - указывает первоначальное состояние созданного объекта (TRUE - объект сразу становится занятым, FALSE - объект свободен);

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

Функция возвращает дескриптор объекта mutex. В дальнейшем этот дескриптор используется для управления мьютексом.

2. Освобождение объекта mutex

BOOL ReleaseMutex( HANDLE hMutex ) - освобождает объект mutex с описателем hMutex, переводя его из занятого в свободное состояние.

3. Закрытие (уничтожение) объекта mutex и освобождение его дескриптора

BOOL CloseHandle( HANDLE hMutex ) 

Взаимодействие потока и мьютекса происходит следующим образом.

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

Wait-функцияпроверяет у мьютекса идентификатор потока: если его значение равно 0, мьютекс свободен; в этом случае он принимает значение идентификатора вызывающего потока, и этот поток сразу получает доступ к ресурсу.

Если Wait-функция определяет, что у мьютекса идентификатор потока не равен 0 (мьютекс занят), вызывающий поток переходит в состояние ожидания.

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

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

 

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

// Объявление глобальных данных

FLOAT y[20], s;

HANDLE hMutex;

// Функция потока 1

DWORD WINAPI ThreadFunc1(PVOID pvParam)

{ int i; DWORD dwResult = 0, n=1;

struct PARAM *p= (struct PARAM *) pvParam;

while (!p->stop)

{

WaitForSingleObject( hMutex, INFINITE );

for ( i = 0; i < 20; i++ ) y[i]=2.5*(n+i)*sin(3.9*i+n);

n++;

ReleaseMutex( hMutex );

}

return(dwResult);

}

// Функция потока 2

DWORD WINAPI ThreadFunc2(PVOID pvParam)

{ int i; DWORD dwResult = 0;

struct PARAM *p= (struct PARAM *) pvParam;

while (!p->stop)

{

WaitForSingleObject( hMutex, INFINITE );

for ( i = 0, s=0; i < 20; i++ ) s+=y[i];

s/=20;

ReleaseMutex( hMutex );

}

return(dwResult);

}

// --- Функция WinMain

int APIENTRY WinMain()

{

// Создание объекта мьютекс

hMutex = CreateMutex( NULL, FALSE, NULL );

// Удаление объекта мьютекс (при необходимости)

CloseHandle( hMutex );

 …

}

Потоки по очереди заполняют массив значениями и производят его обработку.

 


Семафоры.

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

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

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

Объекту «семафор» при инициализации передается максимальное число потоков или ресурсов, после каждого "захвата" счетчик семафора уменьшается. Когда счетчик равен нулю, семафор считается не установленным (сброшенным) и ни один поток не сможет "захватить" семафор. При освобождении семафора одним из потоков счетчик увеличивается на 1 и с семафором может работать любой другой поток.

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

1. Создание объекта ядра «семафор» производится функцией CreateSemap h ore:

HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphAttrib, LONG lInitialCount,

LONG lMaximumCount, LPCTSTR lpName);

где

- lpSemaphAttrib - указатель на структуру SECURITY_ATTRIBUTES;
- lMaximumCount - указывает максимальное число ресурсов (потоков) для семафора, должен быть больше 1;

- lInitialCount – задает начальное значение доступных ресурсов в пределах от 0 до lMaximumCount;

- lpName - указывает на строку, содержащую имя объекта. Имя необходимо для доступа к объекту других процессов. Если не нужен именованный объект, то указывается NULL.

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

2. Освобождение семафора и увеличение его счетчика производится функцией ReleaseSemap h ore:

BOOL ReleaseSemaphore( HANDLE hSem,

LONG lReleaseCount, LPLONG lpPreviousCount);

где hSem – дескриптор семафора;

lReleaseCount – значение, на которое увеличивается счетчик семафора, обычно это 1 или большее;

lpPreviousCount – указатель на предыдущее значе-ние счетчика, если оно не используется, то NULL.

Функция возвращает значение TRUE, если новое значение счетчика не превысило максимальное. В противном случае возвращается FALSE и значение счетчика не изменяется.

3. Закрытие (уничтожение) объекта semaphore и освобождение его дескриптора

BOOL CloseHandle( HANDLE hSem );

Взаимодействие потоков с семафором происходит следующим образом.

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

Если Wait-функция определяет, что счетчик текущего числа ресурсов равен О (семафор занят), система переводит вызывающий поток в состояние ожидания. Когда другой поток увеличит значение этого счетчика, вызвав функцию ReleaseSemap h ore, система снова начнет выделять ему процессорное время (а он, захватив ресурс, уменьшит значение счетчика на 1).

Пример использования семафоров.

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

// Описание глобальных данных

HANDLE hSem;

1.1.1.1 CRITICAL_SECTION cs;

1.1.1.2 struct Query { // описание структуры запроса

1.1.1.3   LONG lClientId;

1.1.1.4   char cQueryText[256]; };

1.1.1.5 struct Query Queue[20]; // создание очереди

1.1.1.6 LONG lPreviousCount=0;

// Функция потока 1

DWORD WINAPI ThreadFunc1(PVOID pvParam)

{ int i; DWORD dwResult=0;

struct Query Query1; // обрабатываемый запрос

struct PARAM *p= (struct PARAM *) pvParam;

while (!p->stop)

{

// ожидание наличия в очереди запросов

WaitForSingleObject(hSem, INFINITE); // извлечение 1-го запроса из очереди

EnterCriticalSection(&cs);

Query1 = Queue[0];

for (i=0; i< lPreviousCount; i++)

  Queue [i]= Queue [i+1];

LeaveCriticalSection(&cs);

// обработка запроса Query1 в потоке

}

return(dwResult);

}

// Функция потока 2

DWORD WINAPI ThreadFunc2(PVOID pvParam)

{ // содержимое функции потока 2

// аналогично потоку 1

}

// Функция потока 3

DWORD WINAPI ThreadFunc3(PVOID pvParam)

{ // содержимое функции потока 3

// аналогично потоку 1

}

// --- Функция WinMain

int APIENTRY WinMain()

{

struct Query InQuery; // буфер для нового запроса

// Создание объекта семафор

hSem = CreateSemaphore( NULL, 0, 20, NULL );

// Создание объекта критический раздел

InitializeCriticalSection(&cs);

 …

// получение запроса от клиента и помещение

// его в очередь

InQuery = GetQuery();

// увеличение числа запросов и счетчика на 1

if (ReleaseSemaphore(hSem, 1, &lPreviousCount))

{

EnterCriticalSection(&cs);

Queue[lPreviousCount] = InQuery;

LeaveCriticalSection(&cs);

}

else // очередь заполнена, запросу отказано

RejectQuery(InQuery);

 …

// Удаление объектов семафор и

// критической раздел (при необходимости)

CloseHandle(hSem);

DeleteCriticalSection(&cs);

 …

}

1.1.1.7

1.1.1.8

1.1.1.9 События

1.1.1.10

Объекты ядра "события" содержат счетчик числа пользователей (как и все объекты ядра) и две булевы переменные: одна сообщает тип данного объекта, другая — его состояние (свободен или занят).

Объекты-события бывают двух типов: со сбросом вручную (manual-reset events) и с автосбросом (auto-reset events). Первые позволяют возобновлять выполнение сразу нескольких ждущих потоков, вторые — только одного.

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

Функции для работы с событиями следующие.

1. Объект ядра "событие" создается функцией CreateEvent:

HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttrib, BOOL fManualReset,

BOOL flnitialState, LPCTSTR lpName);

Параметры функции:

- lpEventAttrib является указателем на структуру типа SECURITY_ATTRIBUTES, если не используется, то задается NULL;

- fManualReset указывает, какого типа событие будет создано: событие со сбросом вручную (TRUE) или с автосбросом (FALSE);

- flnitialState определяет начальное состояние события — свободное (TRUE) или занятое (FALSE);

- lpName указывает на строку, содержащую имя объекта, которое необходимо для доступа к объекту других процессов. Если не нужен именованный объект, то указывается NULL.

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

2. Перевод события в свободное состояние производится следующей функцией:

BOOL SetEvent(HANDLE hEvent);

где hEvent – дескриптор события, возвращенный функцией CreateEvent.

3. Изменение состояния события (с ручным сбросом) на занятое выполняется следующей функцией:

BOOL ResetEvent(HANDLE hEvent);

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

BOOL PulseEvent(HANDLE hEvent);

Выполнение этой функции эквивалентно последовательному вызову SetEvent и ResetEvent.

5. Для закрытия (уничтожения) объекта Event и освобождения его дескриптора используется функция:

BOOL CloseHandle(HANDLE hEvent);

Взаимодействие потоков с объектом ядра "событие" происходит следующим образом.

Когда поток, вызвав Wait-функцию, дождался требуемого события в свободном состоянии дальнейшие действия зависят от типа события:

1) если событие с автосбросом, такое событие автоматически сбрасывается в занятое состояние, при этом только один поток будет работать, а остальные потоки останутся ждать. Для возобновления работы потоков, ожидающих данное событие необходимо вызвать функцию SetEvent();

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

Количество объектов "событие", используемых в программе для работы с потоками зависит от порядка работы этих потоков:

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

б) если потоки должны выполнять свои операции в определенной последовательности, то количество событий будет равно количеству потоков, тип этих событий – с автосбросом.

 

Пример 1. Два потока по очереди выполняют операции записи-чтения в блоке памяти. Используется одно событие с автосбросом.

HANDLE hEvent;

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

// Функция потока 1

DWORD WINAPI ThreadFunc1(PVOID pvParam)

{

while (!p->stop)

{

// ожидание освождения события hEvent

WaitForSingleObject(hEvent, INFINITE); // выполнение операций записи-чтения потоком 1

SetEvent( hEvent );

}

}

// Функция потока 2

DWORD WINAPI ThreadFunc2(PVOID pvParam)

{

while (!p->stop)

{

// ожидание освождения события hEvent

WaitForSingleObject(hEvent, INFINITE); // выполнение операций записи-чтения потоком 2

SetEvent( hEvent );

}

}

 

Пример 2. Два потока параллельно выполняют операции чтения из блока памяти. Используется одно событие с ручным сбросом.

HANDLE hEvent;

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

// Функция потока 1

DWORD WINAPI ThreadFunc1(PVOID pvParam)

{

while (!p->stop)

{

// ожидание освождения события hEvent

WaitForSingleObject(hEvent, INFINITE); // выполнение операций чтения потоком 1

}

}

// Функция потока 2

DWORD WINAPI ThreadFunc2(PVOID pvParam)

{

while (!p->stop)

{

// ожидание освождения события hEvent

WaitForSingleObject(hEvent, INFINITE); // выполнение операций чтения потоком 2

}

}

 

Пример 3. Два потока последовательно выполняют операции ввода данных и их обработки. Используются два события с автосбросом, одно в свободном состоянии, другое – в занятом.

HANDLE hEvent1, hEvent2;

hEvent1=CreateEvent( NULL, FALSE, TRUE, NULL );

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

// Функция потока 1

DWORD WINAPI ThreadFunc1(PVOID pvParam)

{

while (!p->stop)

{

// ожидание освождения события hEvent1

WaitForSingleObject(hEvent1, INFINITE); // выполнение операций ввода данных потоком 1

// перевод события hEvent2 в свобод. состояние

SetEvent(hEvent2);

}

}

// Функция потока 2

DWORD WINAPI ThreadFunc2(PVOID pvParam)

{

while (!p->stop)

{

// ожидание освождения события hEvent2

WaitForSingleObject(hEvent2, INFINITE); // выполнение обработки данных потоком 2

// перевод события hEvent1 в свобод. состояние

  SetEvent(hEvent1);

}

}

 

 

Ожидаемые таймеры

 

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

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

Функции для работы с ожидаемыми таймерами следующие:

1. Создание объекта "ожидаемый таймер" производится следующей функцией:

HANDLE CreateWaitableTimer( LPSECURITY_ATTRIBUTES lpWTimerAttrib,

BOOL fManualReset, LPCTSTR lpName);

Параметры функции:

- lpWTimerAttrib является указателем на структуру типа SECURITY_ATTRIBUTES, если не используется, то задается NULL;

- fManualReset указывает, какого типа таймер будет создан: со сбросом вручную (TRUE) или с автосбросом (FALSE);

- lpName указывает на строку, содержащую имя таймера, которое необходимо для доступа к объекту других процессов. Если не нужен именованный объект, то указывается NULL.

Функция CreateWaitableTimerвозвращает описатель таймера, используемый для работы с ним.

Объекты «ожидаемый таймер» всегда создаются в занятом состоянии.

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

BOOL SetWaitableTimer( HANDLE hWTimer,

const LARGE_INTEGER *pDueTime, LONG lPeriod,

PTIMERAPCROUTINE pfnCompletionRoutine,

PVOID pvArgToCompletionRoutine, BOOL fResume);

где

- hWTimer – дескриптор ожидаемого таймера, возвращенный функцией CreateWaitableTimer;

- pDueTime указывает на целое 64-битное значение, где хранится дата и время (в формате UTC- Coordinated Universal Time) первого перехода таймера в свободное состояние. Если это значение имеет отрицательный знак, то его абсолютная величина указывает, через какой промежуток (в интервалах по 100 нс) после вызова данной функции таймер станет свободным;

- lPeriod задает периодичность (в миллисекундах) следующих переходов таймера в свободное состояние;

- pfnCompletionRoutine и pvArgToCompletionRoutine задают имя APC-функции, вызываемой при переходе

таймера в свободное состояние и адрес передаваемых ей данных. APC – это асинхронный вызов функций (Asynchronous Procedure Call). Если такая функция не используется, то в обеих параметрах указываются NULL;

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

Функция возвращает TRUE, если ее вызов выполнен успешно и FALSE в противном случае.

3. Отключение срабатываний таймера производится функцией:

BOOL CancelWaitableTimer(HANDLE hWTimer);

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

4. Уничтожение объекта hWTimer и освобождение его дескриптора производится функцией:

BOOL CloseHandle(HANDLE hWTimer);

 

Взаимодействие потоков с ожидаемыми таймерами происходит следующим образом.

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

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

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

Ожидаемые таймеры отличаются от обычных таймеров Windows (работающих в пользовательском режиме и настраиваемых функцией SetTimer()) следующим: во-первых они являются объектами ядра, а значит более защищенными; во-вторых они более точные, т.к. сообщения WM_TIMER, посылаемые обычными таймерами имеют низкий приоритет и в очереди сообщений обрабатываются последними.

 

 

Пример 1. Два потока синхронизируются объектом "ожидаемый таймер" с автосбросом. Время срабатывания таймера является относительным и задается в количестве интервалов по 100 нс. Первое срабатывание – через 10 мс после создания таймера, последующие – через 2 мс после завершения потоком своих операций. (1 мс равна 10000 интервалов по 100 нс)

HANDLE hWTimer;

LARGE_INTEGER li;

hWTimer=CreateWaitableTimer(NULL, FALSE, NULL);

// Задаем отрицательное число в количестве

// интервалов по 100 нс

li.QuadPart = - (10 * 10000);

// Устанавливаем первое срабатывание через 10 мс

SetWaitableTimer(hWTimer,&li, 0, NULL, NULL, FALSE);

// Функция потока 1

DWORD WINAPI ThreadFunc1(PVOID pvParam)

{

while (!p->stop)

{

// ожидание освобождения таймера hWTimer

WaitForSingleObject(hWTimer, INFINITE); // выполнение операций потоком 1 ...

// Задаем следующее срабатывание через 2 мс       

li.QuadPart = - (2 * 10000);

SetWaitableTimer(hWTimer,&li,0,NULL,NULL,FALSE);

}

}

// Функция потока 2

DWORD WINAPI ThreadFunc2(PVOID pvParam)

{

while (!p->stop)

{

// ожидание освобождения таймера hWTimer

WaitForSingleObject(hWTimer, INFINITE); // выполнение операций потоком 2 ...

// Задаем следующее срабатывание через 2 мс       

li.QuadPart = - (2 * 10000);

SetWaitableTimer(hWTimer,&li,0,NULL,NULL,FALSE);

}

}

 

Пример 2. Устанавливаем ожидаемый таймер, чтобы он срабатывал ежедневно в 12.00 (время перерыва на обед) и вызывал APC-функцию, которая выдает звуковой сигнал 1000 гц.

HANDLE hWTimer;

SYSTEMTIME st;

FILETIME ftLocal, ftUTC;

LARGE_INTEGER liUTC;

// callback функция таймераVOID CALLBACK TimerAPCProc(LPVOID, DWORD, DWORD){ Beep(1000,500); // выдается звуковой сигнал};

...

// создаем таймер с автосбросом

hWTimer = CreateWaitableTimer(NULL, FALSE, NULL);

// узнаем текущую дату/время GetLocalTime(&st); // если назначенный час уже наступил, // то ставим время на завтраif (st.wHour > 12) st.wDay++; st.wHour = 12;st.wMinute = 0;st.wSecond = 0;// преобразуем время из SYSTEMTIME в FILETIME

SystemTimeToFileTime(&st, &ftLocal);

// преобразуем местное время в UTC-время

LocalFileTimeToFileTime(&ftLocal, &ftUTC);

// преобразуем FILETIME в LARGE_INTEGER из-за

// различий в выравнивании данных

liUTC.LowPart = ftUTC.dwLowDateTime;

liUTC.HighPart = ftUTC.dwHighDateTime;

// устанавливаем таймер

SetWaitableTimer(hWTimer, &liUTC, 24*60*60*1000, TimerAPCProc, NULL, FALSE);

. . .


Синхронизация потоков разных процессов

 

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

Если именованный объект синхронизации создан и используется потоком в одном процессе, то поток в другом процессе может получить доступ к этому объекту путем вызова Create-функции или Open-функции с указанием имени объекта;

Рассмотрим подробнее эти способы.

 

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

Именованный объект синхронизации может быть создан одной из Create-функций, если в последнем ее параметре lpName указать не пустое значение NULL, а строку с именем этого объекта, например:

hMutex1=CreateMutex(NULL, FALSE, "MyMutex");

Аналогично именованные объекты создаются другими Create-функциями: CreateSemaphore(), CreateEvent(), CreateWaitableTimer().

Имя объекту можно задать произвольно, но следует избегать повторения имен объектов разных типов, т.к. это приведет к ошибке при создании нового объекта.

 

Получение доступа к именованному объекту с помощью Create -функции

В другом процессе вызывается Create-функция, соответствующая типу объекта, в ней задается имя уже существующего объекта:

hMutex2=CreateMutex(NULL, FALSE, "MyMutex");

Перед тем, как создавать новый объект, Create-функция проверяет, не существует ли уже объект с таким именем. Если существует, то она возвращает описатель этого объекта и увеличивает счетчик числа его пользователей на 1. Если объект с таким именем не существует, то функция создает новый объект с этим именем.

 

Получение доступа к именованному объекту с помощью Open -функции

В другом процессе вызывается Open-функция, соответствующая типу объекта, в ней задается имя уже существующего объекта:

HANDLE OpenMutex( DWORD dwDesiredAccess,

BOOL blnheritHandle, LPCTSTR lpName);

или

HANDLE OpenSemaphore(...);

HANDLE OpenEvent(...);

HANDLE OpenWaitableTimer(...);

с аналогичным списком параметров.

Параметры функций:

- dwDesiredAccess определяет права доступа в этом процессе к существующему объекту. Если при создании объекта Create-функцией в первом параметре был указан NULL, то объект имеет стандартный набор прав доступа. В этом случае в Open-функции в первом параметре обычно указывается MUTEX_ALL_ACCESS (аналогично для других объектов с заменой MUTEX на тип объекта);

- blnheritHandle указывает, что описатель объекта будет наследуемым от родительского процесса (при TRUE) и не наследуемым в противном случае;

- lpName задает имя объекта.

Например:

hMutex2 = OpenMutex( MUTEX_ALL_ACCESS, FALSE,           "MyMutex");

Функция проверяет, существует ли в системе объект ядра типа мьютекс с именем MyMutex. Если такого объекта нет или есть объект с таким именем, но другого типа, то возвращается пустой дескриптор (NULL). Если требуемый объект существует, то разрешен ли к нему доступ запрошенного вида. При положительном исходе проверки счетчик количества пользователей объекта увеличивается на 1 и функция возвращает его описатель.

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

 

Другие средства синхронизации потоков

К ним можно отнести семейство Interlocked -функций. Эти функции обеспечивают атомарный доступ (монопольный захват) к глобальным переменным, которые используются разными потоками.

Имеется несколько видов Interlocked-функций:

1. Функции изменения переменных:

- увеличение значения переменной на 1

LONG InterlockedIncrement(PLONG plAddend);

- уменьшение значения переменной на 1

LONG InterlockedDecrement(PLONG plAddend);

- изменение значения переменной

LONG InterlockedExchangeAdd( PLONG plAddend,

LONG lIncrement);

Параметры функций:

- plAddend это указатель на переменную, значение которой будет изменено;

- lIncrement величина, на которую будет изменена исходная переменная.

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

Все три функции возвращают исходное значение, хранящееся в *plAddend.

Пример изменения глобальной переменной.

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

long g_x = 0;

DWORD WINAPI ThreadFunc1(PVOID pvParam) {

InterlockedExchangeAdd(&g_x, 1);

return(0);

}

DWORD WINAPI ThreadFunc2(PVOID pvParam) {

InterlockedExchangeAdd(&g_x, 1);

return(0);

}

2. Функция замены значения переменной на другое

LONG InterlockedExchange( PLONG plTarget,

LONG lValue);

Эта функция монопольно заменяет текущее значение переменной типа LONG, адрес которой передается в первом параметре (plTarget), на значение, передаваемое во втором параметре (lValue).

Функция InterlockedExchange возвращает исходное значение, хранящееся в *plTarget.

Пример.

// глобальная переменная, используется как

// индикатор того, занят ли разделяемый ресурс

BOOL g_fResourceInUse = FALSE;

void Func1 () {

// ожидаем доступа к ресурсу

while (InterlockedExchange(&g_fResourceInUse, TRUE) == TRUE)

Sleep(0);

// получаем ресурс в свое распоряжение

...

// доступ к ресурсу больше не нужен

InterlockedExchange(&g_fResourceInUse, FALSE);

}

В цикле while переменной g _ fResourceInUse присваивается значение TRUE и проверяется ее предыдущее значение. Если оно было равно FALSE, значит, ресурс не был занят, но вызывающий поток только что занял его; на этом цикл завершается. В ином случае (значение было равно TRUE) ресурс занимал другой поток, и цикл повторяется.

3. Функция сравнения переменной с заданным значением

PVOID InterlockedCompareExchange(

PLONG plDestination,

LONG lExchange,

LONG lComparand):

Функция сравнивает текущее значение переменной типа LONG (на которую указывает параметр plDestination) со значением, передаваемым в параметре lComparand. Если значения совпадают, *plDestination получает значение параметра lExcbange; в ином случае *plDestination остается без изменений.

Функция возвращает исходное значение, хранящееся в *plDestination.

Пример кода функции, иллюстрирующий ее работу.

LONG InterlockedCompareExchange(

PLONG plDestination, LONG lExchange,

LONG lComparand)

{

LONG IRet = *plDestination; // исходное значение

if (*plDestination == lComparand)

*plDestination = lExchange;

return(lRet);

}

 

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

 

Сравнение объектов синхронизации

Объект Относительная скорость Доступ нескольких процессов Подсчет числа обращений к ресурсу
Критическая секция быстро Нет Нет
Мьютекс медленно Да Нет
Семафор медленно Да Автоматически
Событие медленно Да Да

 

 

Тема: Работа с дисками, каталогами и файлами

в Win 32 API

Файловые системы

Файлы представляют собой наборы данных различных типов, размещаемые на внешнем носителе (жесткие и гибкие диски, компакт-диски, Flash-память) и в оперативной памяти (RAM-диски). Для работы с файлами в ОС Windows используется несколько файловых систем.

ОС Windows 95/98 поддерживают две файловые системы: FAT и CDFS.

1. FAT (File Allocation Table) – система, обеспечивающая создание и хранение файлов на жестких и гибких дисках. Имеется две версии этой системы:

а) FAT16 (устаревшая), в которой используются короткие (до 8 символов) имена файлов. Эта система использовалась еще в MS DOS и Windows 3.1;

б) FAT32 поддерживает имена длиной до 255 символов.

2. CDFS (Compact Disc File System) - файловая система, предназначенная для работы с компакт-дисками.

ОС Windows NT /2000/ XP поддерживают следующие файловые системы: FAT32, CDFS, NTFS, UDFS и HPFS.

- NTFS (New Technology File System) – система, предназначенная для организации данных на жестком диске. NTFS предоставляет все возможности FAT, значительно расширяя их. Доступ к файлам в NTFS более быстрый, чем в FAT. NTFS самовосстанавливающаяся файловая система. Она устойчива к крахам приложений, системы, операций ввода-вывода и разработана таким образом, чтобы восстанавливать корректность данных без использования специальных сервисных программ проверки дисков. NTFS поддерживает разграничение доступа к файлам, имена файлов в произвольном алфавите.

- UDFS (Universal Disc File System) предназначена для работы с DVD (Digital Video Disc) дисками.

- HPFS (High-Performance File System) - так же организует данные на жестком диске. Она разработана фирмой IBM для операционной системы OS/2. По сравнению с FAT она повышает производительность системы за счет использования кэширования. Поддерживается Windows в целях совместимости.

 

Получение информации о дисках на компьютере

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

1. Определение списка логических дисков, которые имеются в системе. Имеется 2 функции:

а) функция GetLogicalDrives(), которая имеет вид

DWORD GetLogicalDrives( void );

Двойное слово, которое возвращает эта функция, явля­ется набором битов, нулевой бит (самый младший) в котором соответ­ствует диску А, первый бит - диску В, второй - диску С и так далее.

Например, если в системе существуют диски A:, C: и D:, то возвращаемое функцией значение равно 0хD или 11012 или 1310.

Для выделения информации о каком-либо логическом диске используется побитовая логическая операция & (and) и битовая маска:

if(GetLogicalDrives() & 0x8L)

   printf("Логический диск D: присутствует\n");

else printf("Логический диск D: отсутствует\n");

б) функция GetLogicalDrivesStrings(), которая имеет вид

DWORD GetLogicalDrivesStrings(

DWORD nBufferLength, LPTSTR lpBuffer);

Параметры функции: nBufferLength задает длину буфера, который будет заполнен списком корневых директорий дисков, установленных на компью­тере; lpBuffer - указатель на этот буфер.

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

Данные, записанные в буфер представляют собой несколько строк, завершённых нулями. В конце списка стоит ещё один нуль. В приведенном выше примере буфер будет заполнен символами:

A:\<null>C:\<null>D:\<null><null>

2. Для определения типов дисков используется функция

UINT GetDriveType( LPTSTR lpszRootPathName );

В качестве параметра ей передается символьное имя корневого каталога диска (например "С:\\"), а возвращаемое значение может быть одно из следующих:

Идентификатор Значе- ние Описание
DRIVE_UNKNOWN 0 Тип устройства определить не уда­лось
DRIVE_NO_ROOT_DIR 1 Корневой директории не существует
DRIVE_REMOVABLE 2 Дисковод со сменным носителем - гибкий диск
DRIVE_FIXED 3 Дисковод с несменным носителем - жёсткий диск
DRIVE_REMOTE 4 Удалённый дисковод - сетевой диск
DRIVE_CDROM 5 Дисковод для компакт-дисков
DRIVE_RAMDISK 6 Эмулируемый в опера-тивной памя­ти диск - RAM-диск

 

3. Для получения подробной информации о носителе используется функция GetVolumeInformation().

BOOL GetVolumeInformation(LPCTSTR lpRootPathName, LPTSTR lpVolumeNameBuffer, DWORD nVolumeNameSize, LPDWORD lpVolumeSerialNumber, LPDWORD lpMaximumComponentLength, LPDWORD lpFileSystemFlags, LPTSTR lpFileSystemNameBuffer, DWORD nFileSystemNameSize);

Назначение параметров функции:

- lpRootPathName (входной параметр) указывает на имя корневого каталога диска, информацию о котором мы хотим получить. Если он равен NULL, используется корневой каталог текущего диска;

- lpVolumeNameBuffer указывает на буфер, в который будет записано имя диска;

- nVolumeNameSize определяет размер этого буфера в байтах;

- lpVolumeSerialNumber указывает на переменную, в которую функция записывает серийный номер диска;

- lpMaximumComponentLength указывает на двойное слово, в которое будет записана мак­симальная длина имени файла вместе с путём, которая допускается в этой системе. Для FAT32 и NTFS это значение будет равно 255;

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

- lpFileSystemNameBuffer указывает на буфер, в который будет записано название файловой системы, такое как FAT, HPFS или NTFS;

- nFileSystemNameSize определяет размер буфера, предназначенного для имени файловой системы.

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

Возвращаемое значение – TRUE при успешном выполнении функции, FALSE – в противном случае.

 

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

а) функция GetDiskFreeSpace(), которая имеет вид:

BOOL GetDiskFreeSpace(LPCTSTR lpRootPathName, LPDWORD lpSectorsPerCluster, LPDWORD lpBytesPerSector, LPDWORD lpNumberOfFreeClusters, LPDWORD lpTotalNumberOfClusters);

Параметры функции:

- lpRootPathName (входной параметр) указывает на строку с именем кор­невого каталога диска, информацию о котором мы хотим получить. Если он равен NULL, используется корневой каталог те­кущего диска;

- lpSectorsPerCluster указывает на двойное слово, в которое функция запишет количество секторов в одном кластере;

- lpBytesPerSector является указателем на двойное слово, в которое будет записано количество байтов в одном секторе.

- lpNumberOfFreeClusters содержит указатель на двойное слово, предназначенное для записи в него количества свободных кластеров на диске.

- lpTotalNumberOfClusters указывает на двойное слово, в которое будет записано общее число кластеров на диске.

б) функция GetDiskFreeSpaceEx(), которая имеет вид:

BOOL GetDiskFreeSpaceEx(LPCTSTR lpDirectoryName, PULARGE_INTEGER lpFreeBytesAvailable, PULARGE_INTEGER lpTotalNumberOfBytes, PULARGE_INTEGER lpTotalNumberOfFreeBytes);

Параметры функции:

- lpDirectoryName (входной параметр) указывает на строку с кор­невым каталогом интересующего нас диска. Если этот параметр равен NULL, используется корневой каталог те­кущего диска;

- lpFreeBytesAvailable указывает на переменную типа ULARGE_INTEGER (беззнаковую длиной 64 бита), в которую будет записано число свободных байтов на диске, доступных данному пользователю;

- lpTotalNumberOfBytes указывает на переменную типа ULARGE_INTEGER, в которую будет записано общее число байтов на диске, доступных данному пользователю;

- lpTotalNumberOfFreeBytes указывает на переменную типа ULARGE_INTEGER, в которую будет записано общее число свободных байтов на диске.

Выходные параметры, которые не требуется определять, могут быть заданы равными NULL.

 

Пример определения и отображения параметров диска С:\.

char cBuffer[1024], cVolumeNameBuffer[128], cFileSystemNameBuffer[128];

DWORD dwVolumeNameSize, dwVolumeSerialNumber,

dwMaximumComponentLength, dwFileSystemFlags, dwFileSystemNameSize;

if( GetVolumelnformation("С:\\", cVolumeNameBuffer, 128, &dwVolumeSerialNumber, &dwMaximumComponentLength, &dwFileSystemFlags, cFileSystemNameBuffer, 128))

{sprintf(cBuffer, "Drive - %03s\nVolume name - %s\n

Volume serial number - %08x\n Maximum component 

length - %08x\n File system flags - %08x\n File 

system name - %s", "С:\\", cVolumeNameBuffer,

dwVolumeSerialNumber,

dwMaximumComponentLength, dwFileSystemFlags,

cFileSystemNameBuffer, dwSectorsPerCluster,

dwBytesPerSector, dwNumberOfFreeClusters,

dwTotalNumberOfClusters);

MessageBox(NULL, cBuffer, "Drive info", MB_OK); }

else

MessageBox(NULL,"Error in drive info","Error", MB_OK);

 

Работа с каталогами

Для работы с каталогами в Win32 API имеется следующий набор функций:

Функция Выполняемое действие
GetCurrentDirectory Получение текущего каталога
SetCurrentDirectory Смена текущего каталога
GetSystemDirectory Получение системного каталога
GetWindowsDirectory Получение основного каталога системы
CreateDirectory Создание каталога
RemoveDirectory Удаление каталога

Рассмотрим форматы этих функций.

1. Функция получения текущего каталога

DWORD GetCurrentDirectory(DWORD nBufferLength, LPTSTR lpBuffer);где nBufferLength – размер буфера в байтах для записи наименования текущего каталога; lpBuffer – указатель на этот буфер.

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

2. Функция смены текущего каталога

BOOL SetCurrentDirectory(LPCTSTR lpPathName);

где lpPathName - указатель на строку, в которой записано наименование директории, кото­рую необходимо сделать текущей.

Если функция завершает свою работу нормально, то возвращаемое ею значение равно TRUE. При наличии ошибки функция возвращает FALSE.

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

UINT GetSystemDirectory(LPTSTR lpBuffer, UINT uSize);

где lpBuffer является указателем на буфер, в ко­торый будет записано наименование системной директории; uSize - размер этого буфера.

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

4. Функция получения основного каталога, где расположена ОС Windows:

UINT GetWindowsDirectory(LPTSTR lpBuffer, UINT uSize);

Назначение параметров этой функции и возвращаемое значение совпадают с предыдущей функцией.

5. Функция для создания каталога

BOOL CreateDirectory(LPCTSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes);

где lpPathName - это указатель на буфер, в котором
записано наименование каталога, который будет создан; lpSecurityAttributes – указатель на структуру типа SECURITY_ATTRIBUTES, в которой задаются права доступа и атрибуты безопасности нового каталога, по умолчанию - NULL.

При нормальном завершении функция создаёт каталог и воз­вращает значение TRUE. В случае возникновения ошибки функция возвращает FALSE.

6. Функция для удаления каталога

BOOL RemoveDirectory(LPCTSTR lpPathName);

где lpPathName - это указатель на буфер, в котором
записано наименование каталога, который будет удален.

Как и предыдущая функция, эта функция также возвращает TRUE при удачном завершении и FALSE в случае возникновения какой-либо ошибки.

 

Получение информации об ошибках

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

DWORD GetLastError(void);Функция возвращает код возникшей ошибки. Комментарии к кодам ошибок находятся в за­головочном файле error.h.

 

Пример получения информации о каталогах

char cBuffer[1280], cBufferForCurrentDirectory[256],

cBufferForWindowsDirectory[256],

cBufferForSystemDirectory[256];

GetCurrentDirectory(256, cBufferForCurrentDirectory);

GetWindowsDirectory(cBufferForWindowsDirectory, 256);

GetSystemDirectory(cBufferForSystemDirectory, 256);

sprintf(cBuffer, "Текущий каталог - %s\n Каталог Windows - %s\n Системный каталог - %s", cBufferForCurrentDirectory, cBufferForWindowsDirector, cBufferForSystemDirectory);

MessageBox(hWnd, cBuffer, "Directories", MB_OK);

 

 

Манипулирование файлами

Для манипулирования файлами в Win32 API имеется следующий набор функций:

Функция Выполняемое действие
CopyFile Копирование файла
DeleteFile Удаление файла
MoveFile MoveFileEx Перемещение или переименование файла

Рассмотрим форматы этих функций.

1. Функция копирования файла

BOOL CopyFile(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName, BOOL bFailIfExists);Параметры функции:- lpExistingFileName указывает на буфер, в котором записано имя файла, подлежащего копированию; - lpNewFileName указывает на буфер, в котором хранится имя нового файла; - bFailIfExists определяет, что должна делать функ­ция в том случае, если файл с именем, указанным во втором аргументе, уже существует. Если он равен TRUE и файл уже существует, то функция вернёт значение FALSE, которое фактически яв­ляется сообщением об ошибке. Если этот аргумент равен FALSE и файл уже существует, то функция удалит старый файл и создаст новый. При успешном копировании файла функция возвращает значение TRUE, а в случае возникновения ошибки возвращается значение FALSE.

2. Функция удаления файла

BOOL DeleteFile(LPCTSTR lpFileName);

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

3. Функции перемещения (переименования) файла

BOOL MoveFile(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName);иBOOL MoveFileEx(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName, DWORD dwFlags);

Аргументы этих функций:

- lpExistingFileName указывает на буфер, в котором записано имя перемещаемого файла;

- lpNewFileName указы­вает на другой буфер, в котором записан другой путь с именем файла. Если задать его равным NULL, то произойдёт удаление, а не перемещение файла;

- dwFlags представляет собой набор битовых флагов, определяющих поведение функции MoveFileEx (подробно флаги описаны в справочном руководстве по Win32 API).

В случае успешного выполнения обе функции возвращают TRUE или значение FALSE при возникновении ошибки.

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

 

Пример. На диске С:\ создать каталог "Demo directory", в него из текущего каталога скопировать файл File_1.txt. Затем этот файл переименовать в File_2.txt. Удалить этот файл и созданный каталог.

char cBuffer[256], cBufferForCurrentDirectory[256];

// Сохраняем текущий каталог

GetCurrentDirectory(256, cBufferForCurrentDirectory);

strcpy(cBuffer, "c:\\Demo Directory");

// Создаём новый каталог

if(CreateDirectory(cBuffer, NULL))

MessageBox(NULL, "Каталог c:\\Demo Directory

создан!", "Info", MB_OK);

else

{ MessageBox(NULL, "He могу создать каталог

c:\\Demo Directory", "Error", MB_OK); return 0;

}

// Формируем путь файла File_1.txt в

// текущем каталоге

strcpy(cBuffer, cBufferForCurrentDirectory);

strcat(cBuffer, "\\File_1.txt ");

// Делаем попытку скопировать файл

if(!CopyFile(cBuffer, "c:\\Demo Directory\\File_1 .txt", FALSE))

 MessageBox(NULL, "He могу скопировать файл!", 

               "Error", MB_OK);

// Переименовываем файл

if(!MoveFile("c:\\Demo Directory\\File_1 .txt", "c:\\Demo Directory\\File_2 .txt"))

{

MessageBox(NULL, "He могу переименовать файл", 

               "Error", MB_OK); return 0;

}

// Делаем попытку удалить каталог, в котором ещё

// есть файлы, поэтому получаем сообщение об

// ошибке.

if(!RemoveDirectory("c:\\Demo Directory")) 

MessageBox(NULL, "He могу удалить каталог\n

Возможно в нем ещё есть файлы!", "Error", MB_OK);

// Удаляем файл из нового каталога

DeleteFile("c:\\Demo Directory\\File_2. txt");

// Удаляем новый каталог

RemoveDirectory("c:\\Demo Directory");

 


Операции с файлами в Win 32 API

Библиотека Win32 API содержит набор функций, позволяющих выполнять следующие операции с файлами:

Функция Выполняемое действие
CreateFile Создание нового или открытие существующего файла
CloseHandle Закрытие файла
ReadFile Чтение данных из файла
WriteFile Запись данных в файл
SetFilePointer Позиционирование указателя
SetEndOfFile Изменение размера файла
FlushFileBuffers Принудительная запись буферов кэширования на диск
LockFile LockFileEx Блокировка части файла от записи другими потоками
UnlockFile UnlockFileEx Разблокировка ранее заблокированной части файла

Рассмотрим форматы некоторых из этих функций.

1. Функция для создания нового или открытия существующего файла

HANDLE CreateFile(LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);

Параметры функции:

- lpFileName это указатель на имя файла (вместе с путём), который необходимо создать или открыть;

- dwDesiredAccess определяет, какие действия можно производить с содержимым файла. Он может принимать сле­дующие значения:

Значение Описание
0 Не разрешены ни чтение, ни запись. Используется, когда необходимо получить инф. о файле
GENERIC_READ Разрешается чтение из файла
GENERIC_WRITE Разрешена запись в файл
GENERIC_READ | GENERIC_WRITE Разрешено чтение из файла и запись в файл

 

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

Значение Описание
0 Исключительный доступ к файлу, сто­ронние пользователи открыть файл не могут
FILE_SHARE_READ Разрешено чтение из файла сторонним пользователям
FILE_SHARE_WRITE Разрешена запись в файл сторонним пользователям
FILE_SHARE_READ| FILE_SHARE_WRITE Разрешены чтение из файла и запись в файл сторонними пользователями

 

- lpSecurityAttributes представляет собой указатель на структуру типа SECURITY_ATTRIBUTES. Если никакой особой защиты файлу не требуется, то в это поле можно занести NULL;

- dwCreationDisposition определяет, какие действия необходимо произвести в тех случаях, когда файл уже су­ществует и когда файл ещё не существует. Этот параметр может принимать одно из следующих значений:

Значение Описание
CREATE_NEW Создаётся новый файл. Если файл с указанным именем уже существует, функция завершает работу с ошибкой.
CREATE_ALWAYS Создаётся новый файл. Если файл с указанным именем уже существует, то он стирается, записывает­ся новый.
OPEN_EXISTING Открывается существующий файл. Если файла с указанным именем не существует, то функ­ция завершает работу с ошибкой.
OPEN_ALWAYS Если файл с указанным именем существует, то он открывается. Если такой файл не существует, то он создаётся.
TRUNCATE_EXISTING Открывается существующий файл и сразу усекается до нулевой дли­ны. Если файл с указанным име­нем не существует, то функция завершает работу с ошибкой.

 

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

Значение Описание
FILE_ATTRIBUTE _READONLY Файл открывается только для чтения.
FILE_ATTRIBUTE _HIDDEN Скрытый файл, в обычных условиях в списке файлов не появляется.
FILE_ATTRIBUTE _SYSTEM Файл является частью опе­рационной системы или используется только опера­ционной системой.
FILE_ATTRIBUTE _TEMPORARY Создаётся временный файл, система пытается не запи­сывать его на диск, а дер­жать в памяти, что ускоряет доступ к файлу.
FILE_FLAG_DELETE _ON_CLOSE Файл после закрытия удаля­ется. Обычно этот флаг ис­пользуется для временных файлов.
FILE_FLAG _SEQUENTIAL_SCAN Режим кэширования файла оптимизируется для после­довательного доступа к файлу.
FILE_FLAG_RANDOM _ACCESS Режим кэширования файла оптимизируется для произ­вольного доступа к файлу.
FILE_FLAG _OVERLAPPED Устанавливается асинхрон­ный доступ к файлу (в Windows 95/98 не реализован).

 

- hTemplateFile указы­вает либо дескриптор временного файла, либо равен NULL. В первом случае значение параметра dwFlagsAndAttributes игнорируется и функция исполь­зует флаги и атрибуты файла, на который указывает hTemplateFile.

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

 

2. Функция закрытия файла

BOOL CloseHandle(HANDLE hFile);

где hFile является дескриптором того фай­ла, который необходимо закрыть.

 

3. Функция чтения данных из файла

BOOL ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped);

Параметры функции:

- hFile является дескриптором файла, из которого будет производиться чтение данных. Файл должен быть от­крыт с флагом GENERIC_READ;

- lpBuffer ука­зывает на буфер, в который будет производиться чтение данных;

- nNumberOfBytesToRead определяет число байтов, которые необходимо прочесть из файла;

- lpNumberOfBytesRead указывает на переменную, в которую будет записано число реально прочитанных из файла байтов;

- lpOverlapped является указателем на структуру типа OVERLAPPED, которая используется только для асин­хронного ввода-вывода. При обычном (синхронном) вводе этот аргумент имеет значение NULL.

Если функция завершена нормально, она возвращает значение TRUE, в противном случае - FALSE.

Чтение данных производится с текущей позиции указателя файла. Например, если файл только что открыт и производится чтение 1 Кбайт данных из начала файла, то после этой операции чтения указатель файла будет установлен на 1025-й байт. После второй такой операции чтения указатель будет установлен на 2049-й байт, и так далее.

Установить указатель в нужную позицию можно с помощью функции SetFilePointer().

 

4. Функция записи данных в файл

BOOL WriteFile(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped);

Параметры функции:

- hFile является дескриптором файла, в который будет производиться запись данных. Файл должен быть от­крыт с флагом GENERIC_WRITE;

- lpBuffer ука­зывает на буфер, из которого будет производиться запись данных;

- nNumberOfBytesToWrite указывает число байтов, которые необходимо записать в файл;

- lpNumberOfBytesWritten указывает на переменную, в которую будет записано число реально записанных в файл байтов;

- lpOverlapped является указателем на структуру типа OVERLAPPED, которая используется только для асин­хронного ввода-вывода. При обычном (синхронном) выводе этот аргумент имеет значение NULL.

Если функция произвела запись нормально, то она возвращает значение TRUE. Если произошла ошибка, возвращается значение FALSE.

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

 

5. Функция изменения размеров файла

BOOL SetEndOfFile(HANDLE hFile);

Эта функция используется, когда необходимо увеличить или уменьшить размер файла. Она устанавливает конец файла hFile в том месте, где находится в данный момент его указатель. После того, как будет установлен конец файла, необходимо закрыть файл. (Конец файла – маркер EOF.)

Например, необходимо изменить длину файла и сделать её равной 65535 байтов. Для этого потребуется выполнить следующие дейст­вия:

HFILE hFile = CreateFile(...);

SetFilePointer(hFile, 65535, NULL, FILE_BEGIN);

SetEndOfFile(hFile);

CloseHandle(hFile);

 

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

char cBuffer[1024], cDirectoryBuffer[256];

char cTextBuffer[] = {"Текстовая строка 1 …\n",

"Текстовая строка 2 …\n",

"Текстовая строка 3 …\n"};

HANDLE hFile;

DWORD dwBytes; int i;

// Определяем текущую директорию и запоминаем

// её в массиве cDirectoryBuffer.

GetCurrentDirectory(256, cDirectoryBuffer);

// Создаём директорию, в которой будем

// производить все манипуляции.

CreateDirectory("c:\\Demo_Directory", NULL);

// Делаем вновь созданную директорию текущей.

SetCurrentDirectory("c:\\Demo_Directory");

// Создаём файл, в который запишем данные.

if(INVALID_HANDLE_VALUE == (hFile =

CreateFile("TestFile.txt", GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL)))

{ MessageBox(NULL,"Can't create file","Error", MB_OK); return 0; }

// Записываем данные в файл.

WriteFile(hFile, cTextBuffer, sizeof(cTextBuffer), &dwBytes, NULL);

// Закрываем файл.

CloseHandle(hFile);

// Очищаем текстовый буфер.

for(i=0;i<sizeof(cTextBuffer);*(cTextBuffer+i)=0,i++);

// Выводим содержимое буфера на экран.

MessageBox(NULL, cTextBuffer, "Clear buffer", MB_OK); // Открываем файл для чтения.

if(INVALID_HANDLE VALUE == (hFile =

CreateFile("TestFile.txt",

GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)))

{ MessageBox(NULL, "Can't open file", "Error", MB_OK); return 0; }

// Читаем из файла.

ReadFile(hFile, cTextBuffer, sizeof(cTextBuffer), &dwBytes, NULL);

// Выдаём содержимое буфера на экран.

MessageBox(NULL,cTextBuffer,"Fulled buffer", MB_OK);

// Закрываем файл.

CloseHandle(hFile);

// Удаляем созданный файл.

DeleteFile("TestFile.txt");

// Делаем текущей ту же директорию, которая была

// текущей до начала работы программы.

SetCurrentDirectory(cDirectoryBuffer);

// Удаляем созданную директорию.

RemoveDirectory("c:\\Demo_Directory");

 

 

Получение и установка характеристик файла

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

Функция Выполняемое действие
GetFileAttributes Получение атрибутов файла
SetFileAttributes Установка атрибутов файла
GetFileSize Получение размера файла
GetFileTime Получение информации о временах создания, последнего обращения и последней записи в файл
FileTimeToSystemTime Преобразование времени из формата FileTime в формат SystemTime
SystemTimeToFileTime Преобразование времени из формата SystemTime в формат FileTime
SetFileTime Изменение меток времени, связанных с файлом
GetFileInformation- ByHandle Получение комплексной информации о файле (атрибуты, размер, времена и др.)

 

Рассмотрим форматы некоторых из этих функций.

1. Функция получения атрибутов файла

DWORD GetFileAttributes(LPCTSTR lpFileName);

где lpFileName – строка с именем файла с путем.

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

FILE_ATTRIBUTE_READONLY - 0x00000001

FILE_ATTRIBUTE_NORMAL - 0x00000080

FILE_FLAG_DELETE_ON_CLOSE - 0x04000000

FILE_FLAG_SEQUENTIAL_SCAN - 0x08000000 и т.д.

Получить значение требуемого атрибута или флага можно с помощью побитовой логической операции &. Например:

DWORD dwFlagsAndAttributes = GetFileAttributes( "C:\\TestFile.txt");

if(dwFlagsAndAttributes & FILE_ATTRIBUTE_NORMAL)

или if(dwFlagsAndAttributes & 0x00000080)

// Формирование сообщения, что файл имеет

// соответствующий атрибут

 

2. Функция получения размера файла

DWORD GetFileSize(HANDLE hFile, LPDWORD lpFileSizeHigh);

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

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

 

3. Функция получения информации о метках времени файла

BOOL GetFileTime(HANDLE hFile, LPFILETIME lpCreationTime, LPFILETIME lpLastAccessTime, LPFILETIME lpLastWriteTime);

Аргументы функции:

- hFile является дескриптором файла, информацию для которого необходимо получить. Этот файл должен быть предварительно открыт;

- lpCreationTime, lpLastAccessTime, lpLastWriteTime являются указателями на структуру типа FILETIME. По этим адресам будут записаны соответственно время создания файла, время последнего обращения к файлу и время последней записи в файл. Если какая-то из этих меток не нужна, то в качестве указателя на соответствующую структуру нужно передать NULL.

Структура FILETIME является 64-битным значением, равным числу 100-наносекундных интервалов, прошедших с 1 января 1601 г. и выглядит сле­дующим образом:

typedef struct _FILETIME {

DWORD dwLowDateTime;

DWORD dwHighDateTime;

} FILETIME, *PFILETIME, *LPFILETIME;

В поле dwLowDateTime, хранятся младшие тридцать два разряда метки времени, а во втором, dwHighDateTime, - старшие тридцать два разряда.

 

4. Функция преобразования времени из формата FILETIME в формат SYSTEMTIME

BOOL FileTimeToSystemTime(const LPFILETIME lpFileTime, LPSYSTEMTIME lpSystemTime);

где lpFileTime является указателем на структуру типа FILETIME, а lpSystemTime - указателем на структуру типа SYSTEMTIME. Эта структура имеет следующий вид:

typedef struct _SYSTEMTIME {
WORD wYear;
WORD wMonth;
WORD wDayOfWeek;
WORD wDay;
WORD wHour;
WORD wMinute;
WORD wSecond;
WORD wMilliseconds;} SYSTEMTIME, *PSYSTEMTIME, *LPSYSTEMTIME;

Поля этой структуры:

- wYear – год, должен быть больше или равен 1601;

- wDayOfWeek – день недели, Воскресенье (Sunday) = 0, Понедельник (Monday) = 1, и т.д.

Назначение остальных полей понятно по их именам.

Не рекомендуется с помощью структуры SYSTEMTIME получать относительное время. Для этого необходимо: 1) время с помощью функции SystemTimeToFileTime() перевести в формат FILETIME; 2) скопировать структуру FILETIME в структуру типа ULARGE_INTEGER; 3) выполнить необходимые арифметические вычисления с 64-битными значениями типа ULARGE_INTEGER.

 

5. Функция получения комплексной информации о файле

BOOL GetFileInformationByHandle(HANDLE hFile,LPBY_HANDLE_FILE_INFORMATION lpFileInformation);

где hFile - дескриптор файла, информацию о котором необходимо получить; lpFileInformation является указателем на структуру типа BY_HANDLE_FILE_INFORMATION, которая заполняется функцией. Структура вы­глядит следующим образом:

typedef struct _BY_HANDLE_FILE_INFORMATION {
DWORD dwFileAttributes; // атрибуты файла
FILETIME ftCreationTime; // время создания файла
FILETIME ftLastAccessTime; // время последнего // доступа к файлу
FILETIME ftLastWriteTime; // время последней записи // в файл
DWORD dwVolumeSerialNumber; // серийный номер // тома, на котором находится файл
DWORD nFileSizeHigh; // старшие тридцать два // разряда размера файла
DWORD nFileSizeLow; // младшие тридцать два // разряда размера файла
DWORD nNumberOfLinks; // число ссылок на файл
DWORD nFileIndexHigh; // старшие тридцать два // разряда идентификатора файла
DWORD nFileIndexLow; // младшие тридцать два // разряда идентификатора файла} BY_HANDLE_FILE_INFORMATION, *PBY_HANDLE_FILE_INFORMATION;

Пример. Получение некоторых характеристик файла.

HANDLE hFile;

char cBuffer[1024];

OPENFILENAME OpenFileName;

... // Заполнение полей структуры OPENFILENAME

// Выбор файла и получение его имени

if(!GetOpenFileName(&OpenFileName))

{ MessageBox(hWnd, "Не могу открыть файл", "Ошибка открытия файла", MB_OK); return 0; }

else

// Открытие выбранного файла

if(INVALID_HANDLE_VALUE == (hFile =  

 CreateFile(OpenFileName.lpstrFile,

GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)))

{ MessageBox(hWnd, "Не могу открыть файл", "Ошибка открытия файла", MB_OK); return 0; }

else

{ // Заполнение cBuffer инф. об атрибутах и флагах

 strcpy(cBuffer,"");

 DWORD dwFileAttributes =

GetFileAttributes(OpenFileName.lpstrFile);

if(dwFileAttributes & FILE_ATTRIBUTE_READONLY)

strcat(cBuffer, "FILE_ATTRIBUTE_READONLY"); if(dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)

strcat(cBuffer," | FILE_ATTRIBUTE_HIDDEN");

... // Проверка других атрибутов файла

if(dwFileAttributes & FILE_FLAG_DELETE ON_CLOSE)

strcat(cBuffer,"| FILE_FLAG_DELETE_ON_CLOSE");

... // Проверка других флагов файла

// Вывод информации об атрибутах и флагах

MessageBox(hWnd, cBuffer, "Атрибуты и флаги файла", MB_OK);

// Определение размера файла

DWORD dwFileSize = GetFileSize(hFile, NULL);

sprintf(cBuffer, "Размер файла %ld bytes", dwFileSize);

MessageBox(hWnd, cBuffer, "Размер файла", MB_OK);

// Определение временных меток файла

FILETIME ftCreationTime, ftLastAccessTime,ftLastWriteTime;SYSTEMTIME stCreationTime, stLastAccessTime,stLastWriteTime;GetFileTime(hFile, &ftCreationTime, &ftLastAccessTime,

&ftLastWriteTime);

FileTimeToSystemTime(&ftCreationTime, &stCreationTime);

FileTimeToSystemTime(&ftLastAccessTime, &stLastAccessTime);

FileTimeToSystemTime(&ftLastWriteTime, &stLastWriteTime);

// Вывод данных о временных метках файла

sprintf(cBuffer, "Время создания: %d.%d.%d г. %d.%d\nВремя последнего доступа: ... ", stCreationTime.wDay,  stCreationTime.wMonth, stCreationTime.wYear, stCreationTime.wHour, stCreationTime.wMinute, ...);

MessageBox(hWnd, cBuffer, " Информация о файле ", MB_OK);

 

Тема: Проецируемые в память файлы

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

Проецируемые файлы применяются для:

- загрузки и выполнения ЕХЕ- и DLL-файлов. Это позволяет существенно уменьшить время, необходимое для подготовки приложения к выполнению;

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

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

Для работы с этим видом файлов используются следующие Win32 API функции:

Функция Выполняемое действие
CreateFile Создание нового или открытие существующего файла
CreateFileMapping Создание объекта «проецируемый файл»
MapViewOfFile Проецирование данных файла на адресное пространство процесса
UnmapViewOfFile Отмена отображения файла на адресное пространство процесса
CloseHandle Закрытие файла. Закрытие объекта «проецируемый файл»

 

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

1. Функция создания объекта «проецируемый файл», сообщает ОС какой файл будет проецируемым, его размер и способ доступа к нему

HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName);

Аргументы функции:

- hFile является дескриптором файла, который необходимо спрое­цировать в память и который был возвращён при вы­зове функции CreateFile();

- lpAttributes представляет собой указатель на структуру типа SECURITY_ATTRIBUTES;

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

Атрибут Описание
PAGE_READONLY После проецирования из файла можно считывать данные. При открытии файла необходимо использовать флаг GENERIC_READ
PAGE_READWRITE После проецирования можно считывать данные из файла и записывать данные в файл. При открытии файла необходимо использовать флаги GENERIC_READ | GENERIC_WRITE.
PAGE_WRITECOPY После проецирования можно считывать данные из файла и записывать данные в файл. Запись приводит к созданию копии страницы. При открытии файла необходимо использовать флаги GENERIC READ или GENERIC READ | GENERIC WRITE.

Кроме того, в этом аргументе могут указываться дополнительные атрибуты защиты страницы памяти, которые объединяются с предыдущими атрибутами побитовой логической операцией | (ИЛИ):

Атрибут Описание
SEC_COMMIT Обеспечивается физическое сохранение в памяти или в страничном файле на диске для всех страниц секции. Это значение, устанавливаемое по умолчанию.
SEC_NOCACHE Данные из файла, спроецированного в па­мять, кэшировать не нужно.
SEC_RESERVE Резервируются все страницы секции без выделения физической памяти.

 

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

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

При нормальном завершении этой функции система возвращает дескриптор объекта «спроецированный файл». Если же создание объекта заверши­лось с какой-то ошибкой, то функция возвращает NULL.

 

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

LPVOID MapViewOfFile(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap);

Аргументы функции:

- hFileMappingObject представляет собой дескриптор ото­бражённого в память на предыдущем этапе файла;

- dwDesiredAccess определяет права доступа к проекции файла, может принимать следующие значения:

Атрибут Описание
FILE_MAP_WRITE Данные файла доступны как по чтению, так и по записи. Объект «проецируемый файл» должен быть создан с атрибутом PAGE_READWRITE
FILE_MAP_READ Данные файла доступны только для чтения. Объ­ект «проецируемый файл» должен быть создан с атрибутом PAGE_ READONLY, PAGE_READWRITE или PAGE_WRITECOPY.
FILE_MAP_ALL _ACCESS To же, что и FILE_MAP_WRITE
FILE_MAP_COPY Данные файла доступны только для чтения. При записи создаётся закрытая копия страницы. Объ­ект «проецируемый файл» должен быть создан с атрибутом PAGE_ READONLY, PAGE_READWRITE или PAGE_WRITECOPY.

 

- dwFileOffsetHigh, dwFileOffsetLow являются старшими и младшими 32 битами смещения, которое указывет системе, какой байт файла необходимо считать первым;

- dwNumberOfBytesToMap указывает раз­мер отображаемой части файла. Если этот аргу­мент равен нулю, то система будет отобра­жать весь файл.

Функция в случае нормального завершения возвращает указатель на начало области отображения файла. Если же функция за­вершает свою работу с ошибкой, то она возвращает NULL. Получив указа­тель на область отображения, можно рабо­тать с файлом так, словно это вовсе не файл, а область памяти.

 

3. Функция отмены отображения файла на адресное пространство процесса

BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);где lpBaseAddress - указатель на начало области отображения файла, возвращенный функцией MapViewOfFile(). Примечание. Открытие файла, закрытие его и объекта «проецируемый файл» производится соответственно функциями CreateFile и CloseHandle, которые рассматривались ранее. Пример. Фрагмент программы, которая в текстовом файле все пробелы заменяет символом '#'. Для просмотра измененного содержимого файла вызывается редактор Notepad.

OPENFILENAME OpenFileName;

... // Заполнение полей структуры OPENFILENAME

// Выбор файла и получение его имени

if(!GetOpenFileName(&OpenFileName))

{ MessageBox(hWnd, "Не могу открыть файл",

"Ошибка открытия файла", MB_OK); return 0; }

else

// Открытие выбранного файла для чтения-записи

HANDLE hFile = CreateFile(OpenFileName.lpstrFile,

GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if(INVALID_HANDLE_VALUE == hFile)

 { MessageBox(hWnd, "Не могу открыть файл",

"Ошибка открытия файла", MB_OK); return 0; }

// Получение размера файла

DWORD dwFileSize = GetFileSize(hFile, NULL);

// Создаем объект "проецируемый файл"

HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);

if (hFileMap == NULL)

{ MessageBox(hWnd,"Не могу создать проекцию файла", "Ошибка создания проекции файла", MB_OK); return 0; }

// Получение адреса, по которому проецируется

// в память первый байт файла

PVOID lpMapViewFile = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0);

if (lpMapViewFile == NULL)

{ MessageBox(hWnd,"Не могу получить адрес проекции файла", "Ошибка проекцирования файла", MB_OK); return 0; }

// Изменение содержимого файла в памяти

for (long i=0; i < dwFileSize; i++)

if(*(lpMapViewFile+i) == ' ')

*(lpMapViewFile+i) = '#'; // Замена пробела на #

// Отмена отображения файла на адресное

// пространство процесса

UnmapViewOfFile((LPVOID) lpMapViewFile);

// Закрытие объекта "проекция файла"

CloseHandle(hFileMap);

// Закрытие файла

CloseHandle(hFile);

// Просмотр измененного содержимого файла

STARTUPINFO si;

PROCESS_INFORMATION pi;

ZeroMemory( &si, sizeof(si)); // Очистка si нулями

si.cb = sizeof( si );

// Формирование строки с именем запускаемого

// процесса и передаваемого ему файла

TCHAR szPath[256];

strcpy(szPath, "Notepad ");

strcat(szPath, OpenFileName.lpstrFile);

CreateProcess( NULL, szPath, NULL, NULL, FALSE, 0,NULL, NULL, &si, &pi );

...

 


Тема: Управление памятью в ОС Windows

 

Виртуальное адресное пространство процесса

Каждому процессу выделяется собственное виртуальное адресное пространство, которое не пересекается с адресными пространствами других процессов. Для 32-разрядных процессов его размер составляет до 4 Гб. Для адресации используются 32-битные указатели, которые могут принимать значения от 0x00000000 до 0xFFFFFFFF.

Поток, выполняемый в каком-либо процессе, получает доступ только к той памяти, которая принадлежит его процессу. Память, отведенная другим процессам, скрыта от этого потока и недоступна ему.

Виртуальное адресное пространство — это диапазон адресов памяти. Для обращения к каким-либо данным виртуальный адрес преобразуется в реальный адрес физической памяти.

Это пространство разбивается на регионы (разделы), различные для Windows 95/98 и Windows NT/2000/XP.

Регионы в адресном пространстве процесса Windows 95/98

 

Регионы в адресном пространстве процесса Windows NT /2000/ XP

 

Код системы Windows NT/2000/XP лучше защищен от процесса пользователя, чем код Windows 95/98. Это обуславливает большую устойчивость этой ОС к ошибкам в прикладных программах.

 

Управление виртуальной памятью

В Win32 используется страничная организация памяти. Размер страниц памяти для платформ Intel и MIPS составляет 4K, а для платформы DEC Alpha - 8K.

Механизм управления памятью в Windows состоит из трех фаз:

1) резервирование памяти в адресном пространстве процесса;

2) выделение реальной, физической памяти в зарезервированный участок;

3) освобождение или возврат физической памяти.

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

Вторая фаза - это выделение реальной, физической памяти в зарезервированный участок виртуального адресного пространства. Такая операция называется передачей физической памяти (committing physical storage). Выделение реальной памяти происходит постранично и гранулярно. Каждой странице может быть назначен свой собственный атрибут доступа. Желательно указывать тот же самый атрибут, что имеет зарезервированный участок адресного пространства.

Третья фаза. Когда физическая память, переданная зарезервированному участку, больше не нужна, ее освобождают. Эта операция называется возврат физической памяти (decommitting physical storage).

 

Использование свопинга в Windows

Свопингом называется механизм динамической выгрузки и загрузки страниц памяти. При генерации системы на диске образуется специальный своп-файл (swap file) или файл подкачки, куда записываются страницы, которым не находится места в физической памяти. Другое название этого файла – страничный файл (paging file).

ОС Windows 95/98 и NT имеют один файл подкачки, а Windows 2000/XP могут работать с несколькими своп-файлами.

За счет этого файла увеличивается общий объем физической памяти, доступный всем процессам. Так, если ПК имеет 256 Мб оперативной памяти и создан своп-файл размером 200 Мб, то общий объем физической памяти становится равным 456 Мб.

Свопинг работает следующим образом.

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

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

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

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

 

Получение информации о памяти

1. Win32 API функция GetSystemInfo() позволяет получить информацию о различных параметрах данного компьютера, в т.ч. и о памяти:

void GetSystemInfo(LPSYSTEM_INFO lpSystemInfo);

где lpSystemInfo является указателем на структуру типа SYSTEM_INFO, которая имеет следующий вид:

typedef struct _SYSTEM_INFO {
union {
DWORD dwOemId;
struct {
WORD wProcessorArchitecture;
WORD wReserved;
};
};
DWORD dwPageSize;
LPVOID lpMinimumApplicationAddress;
LPVOID lpMaximumApplicationAddress;
DWORD_PTR dwActiveProcessorMask;
DWORD dwNumberOfProcessors;
DWORD dwProcessorType;
DWORD dwAllocationGranularity;
WORD wProcessorLevel;
WORD wProcessorRevision;

} SYSTEM_INFO, *LPSYSTEM_INFO;

Назначение полей этой структуры:

- dwOemId не используется, устарело;

- wProcessorArchitecture сообщает тип архитектуры процессора, может принимать следующие значения: PROCESSOR_ARCHITECTURE_UNKNOWN,
PROCESSOR_ARCHITECTURE_INTEL, PROCESSOR_ARCHITECTURE_MIPS, PROCESSOR_ARCHITECTURE_ALPHA, PROCESSOR_ARCHITECTURE_PPC

- wReserved зарезервировано на будущее, пока не используется;

- dwPageSize сообщает размер страницы памяти;

- lpMinimumApplicationAddress и lpMaximumApplicationAddress указывают минимальный и максимальный адреса памяти доступного адресного пространства для каждого процесса;

- dwActiveProcessorMask является битовой маской, которая сообщает, какие процессоры активны (выполняют потоки), бит 0 указывает на процессор 0 и т.д.;

- dwNumberOfProcessors указывает число процессоров в компьютере;

- dwProcessorType указывает тип процессора, используется только в Windows 95/98;

- dwAllocationGranularity сообщает гранулярность резервирования регионов адресного пространства. Для всех платформ Windows это значение составляет 64 Кб;

- wProcessorLevel сообщает дополнительные подробности об уровне данной архитектуры процессора, используется только в Windows 2000/XP;

- wProcessorRevision сообщает дополнительные подробности о номере модели и версии данного процессора, используется только в Windows 2000/XP.

Использование функции GetSystemInfo():

SYSTEM_INFO SystemInfo;

GetSystemInfo(&SystemInfo);

... // Вывод информации из полей стр-ры SystemInfo

 

2. Функция GlobalMemoryStatus предоставляет информацию об использовании физической и виртуальной памяти компьютера:

VOID GlobalMemoryStatus (LPMEMORYSTATUS lpBuffer);где lpBuffer является указателем на структуру типа MEMORYSTATUS, которая имеет следующий вид:typedef struct _MEMORYSTATUS { DWORD dwLength; // Размер структуры DWORD dwMemoryLoad; // Процент использования памяти DWORD dwTotalPhys;   // Физическая память, байт DWORD dwAvailPhys;// Свободная физическая память, байт DWORD dwTotalPageFile; // Размер файла подкачки, байт DWORD dwAvailPageFile; // Свободных байт в файле подкачки DWORD dwTotalVirtual; // Виртуальная память, используемая процессом  DWORD dwAvailVirtual; // Свободная виртуальная память} MEMORYSTATUS, *LPMEMORYSTATUS;

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

Использование функции GlobalMemoryStatus():

MEMORYSTATUS MemStat;

MemStat.dwLength=sizeof(MEMORYSTATUS);

GlobalMemoryStatus(&MemStat);

... // Вывод информации из полей стр-ры MemStat

 

Использование виртуальной памяти

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

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

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

Для резервирования региона памяти в адресном пространстве процесса или выделения ее, используется функция VirtualAlloc(), а для освобождения - функция VirtualFree(). Рассмотрим подробнее эти функции.

1. Функция VirtualAlloc() имеет формат:

LPVOID VirtualAlloc(LPVOID lpAddress, DWORD dwSize,       

DWORD flAllocationType,   DWORD flProtect);

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

Параметры:

- lpAddress задает адрес, по которому надо зарезервировать или выделить память. Если этот параметр равен NULL, то система самостоятельно выбирает место в адресном пространстве процесса;

- dwSize указывает размер выделяемого региона;

- flAllocationType определяет тип распределения памяти, может принимать следующие значения:

MEM_RESERVE Резервируется блок адресов без выделения памяти
MEM_COMMIT Выделяется физическая память для ранее зарезервированного блока адресов. При комбинировании с флагом MEM_RESERVE одновременно выполняется резервирование и выделение памяти.
MEM_TOP_DOWN Выделяет память по наибольшему возможному адресу. Имеет смысл только при lpAddress = NULL.

- flProtect задает тип защиты доступа для выделяемого региона, может принимать следующие значения:

PAGE_READONLY Разрешается только чтение
PAGE_READWRITE Допускается чтение и запись
PAGE_EXECUTE Разрешается только исполнение
PAGE_EXECUTE_READ Допускается исполнение и чтение
PAGE_EXECUTE _READWRITE Разрешается исполнение, чтение и запись
PAGE_GUARD Дополнительный флаг защиты, который комбинируется с другими флагами.
PAGE_NOCACHE Запрещается кэширование страниц.

2. Функция VirtualFree() имеет формат:

BOOL VirtualFree( LPVOID lpAddress,  

DWORD dwSize, DWORD dwFreeType);

Возвращает TRUE в случае успеха и FALSE - в случае неудачи.

Параметры:

- lpAddress задает адрес региона, который надо освободить;

- dwSize указывает размер освобождаемого региона;

- dwFreeType определяет тип освобождения, может принимать следующие значения:

MEM_DECOMMIT Освободить выделенную память
MEM_RELEASE Освободить зарезервированный регион. При этом параметр dwSize должен быть равен нулю.

 

Другие функции для работы с виртуальной памятью

1. Выделенные страницы можно заблокировать в памяти, т.е. запретить их вытеснение в файл подкачки. Для этих целей служит функция VirtualLock(). Функция VirtualUnlock() отменяет выполненную ранее блокировку страниц. Процессу не разрешается блокировать более 30 страниц.

2. Для изменения атрибутов защиты регионов используются функции VirtualProtect() и VirtualProtectEx(). Первая функция позволяет изменять атрибуты защиты в адресном пространстве текущего процесса, а вторая - произвольного.

3. Функции VirtualQuery()и VirtualQueryEx() позволяют определить статус указанного региона адресов (размер региона, атрибуты защиты и др.). Первая функция позволяет получить такую информацию для текущего процесса, вторая – для любого другого.

 

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

HANDLE hFile;

OPENFILENAME OpenFileName;

... // Заполнение полей структуры OPENFILENAME

// Выбор файла и получение его имени

if(!GetOpenFileName(&OpenFileName))

{ MessageBox(hWnd, "Не могу открыть файл", "Ошибка открытия файла", MB_OK); return 0; }

else

// Открытие выбранного файла

hFile = CreateFile(OpenFileName.lpstrFile,

GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if(INVALID_HANDLE_VALUE == hFile)

{ MessageBox(hWnd, "Не могу открыть файл", "Ошибка открытия файла", MB_OK); return 0; }

// Определение размера файла

DWORD dwFileTextSize = GetFileSize(hFile, NULL);

// Резервируем и выделяем память для текста

PCHAR pcTextMemory = (PCHAR) VirtualAlloc( NULL,

dwFileTextSize, MEM_RESERVE | MEM_COMMIT,     

PAGE_READONLY);

DWORD dwReadBytes, dwCount=0;

// Чтение из файла

ReadFile(hFile, pcTextMemory, dwFileTextSize, &dwReadBytes, NULL);

// Подсчет количества символов ! в тексте

for (DWORD dwI=0; dwI<dwFileTextSize; dwI++)

if(pcTextMemory[dwI]=='!') dwCount++;

// Вывод результатов

char cBuffer[256];

sprintf(cBuffer, "В тексте имеется %ld символов !", dwCount);

MessageBox(hWnd, cBuffer, "Результаты", MB_OK);

// Освобождаем выделенную память

VirtualFree(pcTextMemory, dwFileTextSize, MEM_DECOMMIT);

// Закрываем файл.

CloseHandle(hFile);
    Динамически распределяемая память. Кучи.

В ОС Windows имеется еще один механизм управления памятью — динамически распределяемые области памяти или кучи (heaps). Они удобны при создании множества небольших блоков данных, например для связанных списков, деревьев, массивов переменной длины и др.

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

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

Стандартная куча

Она создается при инициализации процесса и по умолчанию имеет размер 1 Мб. Можно увеличивать этот размер, для чего надо указать компоновщику при сборке программы ключ /HEAP.

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

Стандартная куча процесса автоматически уничтожается по его завершении — самостоятельно уничтожить ее невозможно.

Для работы со стандартной кучей используются следующие Win32 API функции:

1. Описатель стандартной кучи процесса возвращает функция GetProcessHeap:

HANDLE GetProcessHeap();

2. Выделение блока памяти в куче выполняет функция HeapAlloc:

LPVOID HeapAlloc(HANDLE hHeap, DWORD dwFlags,

                        SIZE_T dwBytes);

Параметры:

- hHeap идентифицирует описатель кучи, из которой выделяется память;

- dwFlags указывает флаги, влияющие на характер выделения памяти, может принимать 0 или следующие значения:

HEAP_ZERO_MEMORY Производится заполнение выделенного блока памяти нулями
HEAP_GENERATE _EXCEPTIONS Генерируется программное исключение, если в куче не хватает памяти для создания блока требуемого размера
HEAP_NO_SERIALIZE Отменяется принцип последовательного доступа потоков к куче. Этот флаг необходимо использовать осторожно, т.к. могут быть повреждены данные в куче

- dwBytes определяет число выделяемых в куче байтов.

При попытке выделения из кучи блока памяти функция HeapAlloc делает следующее:

а) Просматривает связанный список выделенных и свободных блоков памяти.

б) Находит адрес свободного блока требуемого размера.

в) Выделяет новый блок, помечая свободный как занятый.

г) Добавляет новый элемент в связанный список блоков памяти.

Если процесс выделения блока памяти завершился успешно, функция HeapAlloc возвращает указатель на этот блок, в противном случае – NULL.

 

3. Изменение размера выделенного блока памяти выполняет функция Heap Re Alloc:

LPVOID HeapReAlloc(HANDLE hHeap,DWORD dwFlags,

             LPVOID lpMem, SIZE_T dwBytes);

Параметры:

- hHeap является описателем кучи;

- dwFlags указывает флаги, используемые при изменении размера блока. Может принимать следующие значения: HEAP_ZERO_MEMORY, HEAP_GENERATE_EXCEPTIONS, HEAP_NO_SERIALIZE, HEAP_REALLOC_IN_PLACE_ONLY. Последний флаг указывает, что данный блок памяти перемещать внутри кучи не разрешается;

- lpMem указывает текущий адрес изменяемого блока;

-dwBytes задает новый размер блока памяти (в байтах).

Если функция сможет расширить блок без его перемещения, она сделает это и вернет исходный адрес блока. С другой стороны, если для расширения блока его надо переместить, она возвращает адрес нового, большего по размеру блока. Если блок затем снова уменьшается, функция вновь возвращает исходный адрес первоначального блока. В случае невозможности изменения размера блока функция Heap Re Alloc возвращает NULL.

 

4. Освобождение выделенного блока памяти из кучи выполняет функция Heap Free :

BOOL HeapFree(HANDLE hHeap, DWORD dwFlags,

                   LPVOID lpMem);

Параметры:

- hHeap является описателем кучи;

- dwFlags указывает флаги, используемые при освобождении блока. Может принимать следующие значения: 0 или HEAP_NO_SERIALIZE;

- lpMem указывает адрес освобождаемого блока памяти.

При успешном выполнении функция возвращает TRUE и FALSE - в противном случае.

 

Дополнительные кучи

Они могут создаваться в адресном пространстве процесса в следующих случаях:

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

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

в) в дополнительной куче всю ее память можно быстро освободить, не перебирая требуемые блоки в стандартной куче.

Для работы с дополнительными кучами используются следующие Win32 API функции:

1. Создание дополнительной кучи производится функцией HeapCreate:

HANDLE HeapCreate(DWORD flOptions,

     SIZE_T dwInitialSize, SIZE_T dwMaximumSize);

Параметры:

- flOptions задает флаги атрибутов создаваемой кучи. Может принимать следующие значения: 0, HEAP_GENERATE_EXCEPTIONS, HEAP_NO_SERIALIZE;

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

- dwMaximumSize указывает максимальный объем адресного пространства, резервируемого под кучу. Если он больше 0, создается куча именно такого размера и ее нельзя увеличить. А если он равен 0, система резервирует регион и, если надо, расширяет его до максимально возможного объема.

При успешном создании кучи HeapCreate возвращает ее описатель, идентифицирующий новую кучу, в противном случае – NULL.

 

2. Уничтожение дополнительной кучи производится

функцией Heap Destroy :

BOOL HeapDestroy(HANDLE hHeap);

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

 

3. Для выделения блоков памяти в дополнительных кучах, изменения их размеров и освобождения используются ранее рассмотренные функции: HeapAlloc, HeapReAlloc, HeapFree.

 

Пример. В стандартной куче создать динамический массив Х на 100 элементов вещественного типа и заполнить его. Определить среднее арифметическое элементов массива Х.

// Получение описателя стандартной кучи

HANDLE hHeap=GetProcessHeap();

FLOAT *fArrayX, fAverageX=0;

// Выделение массиву Х блока памяти в куче

fArrayX= (FLOAT *) HeapAlloc(hHeap, 0, 100*sizeof(FLOAT));

// Заполнение массива и его обработка

for (UINT uI=0; uI<100; uI++)

{ *(fArrayX+uI)=2.7*uI+6.3*sin(1.2*uI);

fAverageX+=*(fArrayX+uI); }

fAverageX/=100;

...

// Освобождение блока памяти в куче

HeapFree(hHeap, 0, fArrayX);

 

Тема: Проецируемые в память файлы

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

Проецируемые файлы применяются для:

а) загрузки и выполнения ЕХЕ- и DLL-файлов. Это позволяет существенно уменьшить время, необходимое для подготовки приложения к выполнению;

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

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

Для работы с этим видом файлов используются следующие Win32 API функции:

Функция Выполняемое действие
CreateFile Создание нового или открытие существующего файла
CreateFileMapping Создание объекта «проецируемый файл»
MapViewOfFile Проецирование данных файла на адресное пространство процесса
UnmapViewOfFile Отмена отображения файла на адресное пространство процесса
CloseHandle Закрытие файла. Закрытие объекта «проецируемый файл»

 

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

1. Функция создания объекта «проецируемый файл», сообщает ОС какой файл будет проецируемым, его размер и способ доступа к нему

HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName);

Аргументы функции:

- hFile является дескриптором файла, который необходимо спрое­цировать в память и который был возвращён при вы­зове функции CreateFile();

- lpAttributes представляет собой указатель на структуру типа SECURITY_ATTRIBUTES;

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

Атрибут Описание
PAGE_READONLY После проецирования из файла можно считывать данные. При открытии файла необходимо использовать флаг GENERIC_READ
PAGE_READWRITE После проецирования можно считывать данные из файла и записывать данные в файл. При открытии файла необходимо использовать флаги GENERIC_READ | GENERIC_WRITE.
PAGE_WRITECOPY После проецирования можно считывать данные из файла и записывать данные в файл. Запись приводит к созданию копии страницы. При открытии файла необходимо использовать флаги GENERIC READ или GENERIC READ | GENERIC WRITE.

Кроме того, в этом аргументе могут указываться дополнительные атрибуты защиты страницы памяти, которые объединяются с предыдущими атрибутами побитовой логической операцией | (ИЛИ):

Атрибут Описание
SEC_COMMIT Обеспечивается физическое сохранение в памяти или в страничном файле на диске для всех страниц секции. Это значение, устанавливаемое по умолчанию.
SEC_NOCACHE Данные из файла, спроецированного в па­мять, кэшировать не нужно.
SEC_RESERVE Резервируются все страницы секции без выделения физической памяти.

 

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

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

При нормальном завершении этой функции система возвращает дескриптор объекта «спроецированный файл». Если же создание объекта заверши­лось с какой-то ошибкой, то функция возвращает NULL.

 

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

LPVOID MapViewOfFile(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap);

Аргументы функции:

- hFileMappingObject представляет собой дескриптор ото­бражённого в память на предыдущем этапе файла;

- dwDesiredAccess определяет права доступа к проекции файла, может принимать следующие значения:

Атрибут Описание
FILE_MAP_WRITE Данные файла доступны как по чтению, так и по записи. Объект «проецируемый файл» должен быть создан с атрибутом PAGE_READWRITE
FILE_MAP_READ Данные файла доступны только для чтения. Объ­ект «проецируемый файл» должен быть создан с атрибутом PAGE_ READONLY, PAGE_READWRITE или PAGE_WRITECOPY.
FILE_MAP_ALL _ACCESS To же, что и FILE_MAP_WRITE
FILE_MAP_COPY Данные файла доступны только для чтения. При записи создаётся закрытая копия страницы. Объ­ект «проецируемый файл» должен быть создан с атрибутом PAGE_ READONLY, PAGE_READWRITE или PAGE_WRITECOPY.

 

- dwFileOffsetHigh, dwFileOffsetLow являются старшими и младшими 32 битами смещения, которое указывает системе, какой байт файла необходимо считать первым;

- dwNumberOfBytesToMap указывает раз­мер отображаемой части файла. Если этот аргу­мент равен нулю, то система будет отобра­жать весь файл.

Функция в случае нормального завершения возвращает указатель на начало области отображения файла. Если же функция за­вершает свою работу с ошибкой, то она возвращает NULL. Получив указа­тель на область отображения, можно рабо­тать с файлом так, словно это вовсе не файл, а область памяти.

 

3. Функция отмены отображения файла на адресное пространство процесса

BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);где lpBaseAddress - указатель на начало области отображения файла, возвращенный функцией MapViewOfFile(). Примечание. Открытие файла, закрытие его и объекта «проецируемый файл» производится соответственно функциями CreateFile и CloseHandle, которые рассматривались ранее. Пример. Фрагмент программы, которая в текстовом файле все пробелы заменяет символом '#'. Для просмотра измененного содержимого файла вызывается редактор Notepad.

OPENFILENAME OpenFileName;

... // Заполнение полей структуры OPENFILENAME

// Выбор файла и получение его имени

if(!GetOpenFileName(&OpenFileName))

{ MessageBox(hWnd, "Не могу открыть файл",

"Ошибка открытия файла", MB_OK); return 0; }

else

// Открытие выбранного файла для чтения-записи

HANDLE hFile = CreateFile(OpenFileName.lpstrFile,

GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if(INVALID_HANDLE_VALUE == hFile)

 { MessageBox(hWnd, "Не могу открыть файл",

"Ошибка открытия файла", MB_OK); return 0; }

// Получение размера файла

DWORD dwFileSize = GetFileSize(hFile, NULL);

// Создаем объект "проецируемый файл"

HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);

if (hFileMap == NULL)

{ MessageBox(hWnd,"Не могу создать проекцию файла", "Ошибка создания проекции файла", MB_OK); return 0; }

// Получение адреса, по которому проецируется

// в память первый байт файла

PVOID lpMapViewFile = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0);

if (lpMapViewFile == NULL)

{ MessageBox(hWnd,"Не могу получить адрес проекции файла", "Ошибка проекцирования файла", MB_OK); return 0; }

// Изменение содержимого файла в памяти

for (long i=0; i < dwFileSize; i++)

if(*(lpMapViewFile+i) == ' ')

*(lpMapViewFile+i) = '#'; // Замена пробела на #

// Отмена отображения файла на адресное

// пространство процесса

UnmapViewOfFile((LPVOID) lpMapViewFile);

// Закрытие объекта "проекция файла"

CloseHandle(hFileMap);

// Закрытие файла

CloseHandle(hFile);

// Просмотр измененного содержимого файла

STARTUPINFO si;

PROCESS_INFORMATION pi;

ZeroMemory( &si, sizeof(si)); // Очистка si нулями

si.cb = sizeof( si );

// Формирование строки с именем запускаемого

// процесса и передаваемого ему файла

TCHAR szPath[256];

strcpy(szPath, "Notepad");

strcat(szPath, OpenFileName.lpstrFile);

CreateProcess( NULL, szPath, NULL, NULL, FALSE, 0,NULL, NULL, &si, &pi );

...

 

 

ТЕМА: ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ

Динамически подключаемые библиотеки (Dynamic-Link Libraries, DLL или библиотеки динамической компоновки) являются одной из наиболее важных составных частей всех ОС Windows.

В DLL содержатся все функции Win32 API. Основными DLL, обеспечивающими функционирование Windows, являются: Kernel32.dll (управление памятью, процессами и потоками); User32.dll (поддержка пользовательского интерфейса); GDI32.dll (вывод текста и графики). Также используются другие DLL с различными наборами функций.

1. Основы динамически подключаемых библиотек

Общие принципы работы DLL

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

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

Стандартным расширением файла динамически подключаемой библиотеки в Windows является .dll, однако допускается указывать любое расширение, например .exe или .fon. Исходный текст библиотеки размещается в одном или нескольких .cpp (.c) файлах, в заголовочном .h файле.

В отличие от статических библиотек (файлов .lib), библиотеки DLL не присоединены непосредственно к выполняемым файлам с помощью компоновщика. Они динамически связываются с вызывающим процессом путем проецирования образа файла DLL на его адресное пространство.

Экспорт идентификаторов из DLL

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

Для указания компилятору, какие идентификаторы будут экспортированы из DLL, используется модификатор __declspec (dllexport), например:

__declspec (dllexport) int MyDLLFunction(int iParam);

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

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

Например, для библиотеки MyDLL.dll её .def файл будет иметь следующий вид:

Файл MyDLL.def LIBRARY             “MyDLL”   DESCRIPTION “MyDLL – пример DLL-библиотеки” EXPORTS            MyDLLFunction    @1

В строке экспорта EXPORTS кроме имени функции указывается ее порядковый номер после символа @. Этот номер может использоваться при обращении к функции в приложении.

На основании описания экспортируемых DLL идентификаторов компоновщик:

- создает библиотеку импорта (.lib файл) со списком идентификаторов, экспортируемых из DLL;

- вставляет в DLL-файл секцию экспорта, в которой содержится список (в алфавитном порядке) экспортируемых идентификаторов.

Получить содержимое секции экспорта в любом DLL-модуле можно с помощью утилиты DUMPBIN.EXE (с ключом /EXPORTS) из состава Microsoft Visual Studio.

Например, для получения содержимого секции экспорта в MyLib.dll надо записать:

DUMPBIN.EXE /EXPORTS MyLib.dll /OUT:"ExportSection.txt"

Импорт идентификаторов в EXE -файле

В приложениях, которые используют функции, классы и данные из DLL, эти величины являются импортируемыми. Их имена помещаются в секцию импорта (imports section) приложения.

Для указания компилятору, какие идентификаторы будут импортированы из DLL, используется модификатор __declspec (dll im port), например:

__declspec (dllimport) int MyDLLFunction(int iParam);

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

Компоновщик, производя сборку приложения, проверяет наличие в библиотеках импорта (LIB-файлы) для каждой из DLL идентификаторов, на которые имеются ссылки в коде приложения и формирует секцию импорта в ЕХЕ-файле.

Получить содержимое секции импорта в ЕХЕ-файле, который использует DLL, можно с помощью утилиты DUMPBIN.EXE (с ключом /IMPORTS). Например:

DUMPBIN.EXE /IMPORTS MyApp.exe /OUT:"ImportSection.txt"

Согласование интерфейсов функций в языках C и C ++

Если создание DLL и ЕХЕ-файла производится на одном языке (C или C++), то никаких проблем с вызовами функций не возникает. Проблема с именами функций возникает, когда DLL разработана на языке C, а приложение – на языке C++.

По умолчанию в языке C++ компилятором производится расширение имен функций (name mangling), что позволяет редактору связей различать перегруженные функции. Так для функции MyDLLFunction() компилятор языка C++ сформирует расширенное имя в виде:

"?MyDLLFunction@@YGXPAD@Z"

В языке С имена функций компилятором не расширяются.

Если необходимо подключить DLL на С к приложению на C++, все функции из этой библиотеки потребуется объявить как внешние в формате C с помощью модификатора extern "C":

extern "С" int MyOldCFunction(int myParam);

Модификатор extern "C" можно применить и к целому блоку, в котором с помощью директивы #include подключен заголовочный файл DLL на С:

extern "C"

{

#include "MyCLib.h"

}

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

Способы подключения DLL к программе

Существует два основных способа динамического подключения библиотек:

– неявное подключение (Implicit Linking) или динамическое связывание при загрузке приложения (Load-Time Dynamic Linking); 

– явное подключение (Explicit Linking) или динамическое связывание в период выполнения приложения (Run-Time Dynamic Linking).

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

При явном подключении DLL приложение вызывает функцию LoadLibrary(), чтобы загрузить DLL, затем использует функцию GetProcAddress(), чтобы получить указатели на требуемые функции (или переменные), а по окончании работы с ними вызывает FreeLibrary(), чтобы выгрузить библиотеку и освободить занимаемые ею ресурсы.

2. Создание DLL

Этапы создания DLL

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

1. Подготовить заголовочный (.h) файл библиотеки с прототипами функций, объявлениями переменных и определениями классов, экспортируемых из DLL.

2. Написать на C/C++ исходный код библиотеки с телами функций, которые должны находиться в DLL.

3. Выполнить компиляцию и компоновку DLL. При этом компилятор преобразует исходный код DLL в OBJ-файл, а компоновщик собирает DLL-модуль.

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

Если компоновщик обнаружит, что DLL экспортирует переменные или функции, то создаст дополнительно библиотеку импорта (.lib-файл).

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

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

Microsoft Visual C++ генерирует заготовку такого универсального заголовочного файла. Например, заголовочный файл MyDLL.h для создаваемой библиотеки MyDLL.dll имеет следующий вид:

Файл MyDLL . h

// Условное определение макроса MyDLL_API

#ifdef MyDLL_EXPORTS

#define MyDLL_API __declspec(dllexport)

#else

#define MyDLL_API __declspec(dllimport)

#endif

// Прототип экспортируемой функции MyDLLFunction

MyDLL_API int MyDLLFunction(void);

… // Другие экспортируемые функции

// Описание экспортируемого класса MyDLLClass

class MyDLL_API MyDLLClass {

public:

MyDLLClass(void); // Конструктор класса

~MyDLLClass(void); // Деструктор класса

int ClMyFunc(void); // Не виртуальная функция

virtual int ClVirtFunc(void); // Виртуальная функция

   ... //Другие методы этого класса

};

… // Другие экспортируемые классы

// Описание экспортируемой переменной nMyDLLVar

extern MyDLL_API int nMyDLLVar;

… // Другие экспортируемые переменные

Когда этот заголовочный файл включается в исходный код DLL, в файле проекта .dsp, в опции компилятора Visual C++ автоматически добавляет строку "MyDLL_EXPORTS" и тогда макрос MyDLL_API получит значение __ declspec ( dllexport ).

При включении его в исходный код приложений, использующих эту DLL, величина MyDLL_EXPORTS нигде не определяется. Поэтому макрос MyDLL_API получит значение __ declspec ( dllimport ).

Структура исходного кода DLL

Файл с исходным кодом динамически подключаемой библиотеки должен состоять из следующих частей:

а) включение необходимых заголовочных файлов, в том числе и заголовочного файла данной DLL;

б) описание функции DllMain (), являющейся точкой входа в DLL;

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

Например, файл MyDLL.cpp с исходным кодом создаваемой библиотеки MyDLL.dll имеет следующий вид:

Файл MyDLL . cpp

#include "stdafx.h"

// Объявление экспортируемых DLL

// идентификаторов внешними в формате C

extern "C"

{

#include "MyDLL.h" // Заголовочный файл DLL

}

// Описание главной функции DLL DllMain()

BOOL WINAPI DllMain(HINSTANCE hInstDLL,

DWORD fdwReason, LPVOID lpvReserved)

{

switch (fdwReason) {     case DLL_PROCESS_ATTACH:     case DLL_THREAD_ATTACH:     case DLL_THREAD_DETACH:     case DLL_PROCESS_DETACH:               break; } return TRUE;

}

// Описание экспортируемой функции MyDLLFunction

MyDLL_API int MyDLLFunction(void)

{

   return 123;

}

// Описание методов класса MyDLLClass

MyDLLClass::MyDLLClass() // конструктор класса

{

   return;

}

MyDLLClass::~MyDLLClass() // деструктор класса

{

   return;

}

int MyDLLClass::ClMyFunc() // Метод ClMyFunc

{

   return 234;

}

int MyDLLClass::ClVirtFunc() // Метод ClVirtFunc

{

   return 345;

}

// Инициализация экспортируемой переменной

MyDLL_API int nMyDLLVar =0;

В начале с помощью модификатора extern " C " все экспортируемые DLL функции и переменные из включаемого заголовочного файла MyDLL.h объявлены внешними в формате C. Это необходимо для того, чтобы в секции экспорта библиотеки компилятор языка C++ не производил расширение имен. Такая операция нужна, если предполагается явная загрузка этой DLL и получение адресов её функций по их именам.

Функция входа/выхода DllMain ()

В состав каждой DLL обязательно входит функция DllMain(). Ее называют функцией входа/выхода, она обычно используется для инициализации и очистки ресурсов в процессах и потоках. Windows вызывает эту функцию: при загрузке/выгрузке библиотеки, при завершении процесса, при создании и уничтожении новых потоков в приложении.

Общий вид функции DllMain() следующий:

BOOL WINAPI DllMain(HINSTANCE hInstDLL,

DWORD fdwReason, LPVOID lpvReserved)

{

switch (fdwReason)

{

   case DLL_PROCESS_ATTACH:

    // Инициализация при загрузке DLL процессом,

   // возвращается значение FALSE при неудаче

    …

    break;

   case DLL_THREAD_ATTACH:

    // Инициализация при создании нового потока

    …

    break;

   case DLL_THREAD_DETACH:

    // Очистка ресурсов при уничтожении потока

    …

    break;

   case DLL_PROCESS_DETACH:

    // Очистка ресурсов при выгрузке DLL или

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

    …

    break;

}

return TRUE;

}

Назначение параметров функции:

1. Параметр hInstDLL - это описатель экземпляра DLL, передаваемый Windows.

2. Параметр f dwReason определяет причину вызова функции DllMain() операционной системой. Он может принимать следующие четыре значения:

- DLL_PROCESS_ATTACH – когда DLL загружена процессом (неявно или явно). При этом DLL может выполнить в этом процессе инициализацию, необходимую ее функциям. При возникновении ошибки возвращается значение FALSE;

- DLL_THREAD_ATTACH – когда в процессе создается новый поток, ОС просматривает все загруженные им DLL и в каждой из них вызывает DllMain (). При этом DLL могут выполнить инициализацию ресурсов, связанных с данным потоком. Если в момент загрузки DLL в процессе уже выполняется несколько потоков, система не вызывает DllMain() с этим значением ни для одного из них;

- DLL_THREAD_DETACH – когда завершается поток путем возврата из его стартовой функции или вызова функции ExitT h read(). При этом ОС вызывает DllMain() в каждой из загруженных DLL, которые могут провести очистку ресурсов, связанных с данным потоком. Если поток завершается из-за того, что другой поток вызвал для него функцию TerminateThread (), Windows не вызывает DllMain () с этим значением;

- DLL_PROCESS_DETACH – когда происходит отключение DLL от адресного пространства процесса либо при завершении процесса, либо при выгрузке DLL с помощью функции FreeLibrary(). При этом DLL может провести очистку ресурсов в данном процессе. Если процесс завершается в результате вызова функции TerminateProcess(), Windows не вызывает DllMain() с этим значением.

3. Параметр l p v Reserved является указателем, заре-зервированным для внутреннего использования Windows. Приложение не должно изменять его, но может проверить его значение. Если библиотека DLL была загружена явно, оно будет равно NULL, при неявной загрузке этот указатель будет ненулевым.

Возвращаемое функцией DllMain() значение указывает, корректно (TRUE) или нет (FALSE) прошла инициализация DLL.

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

3. Неявное подключение DLL к приложению

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

Создание приложения, неявно загружающего DLL

Для создания такого приложения необходимо:

1. В исходный код приложения, в котором будут обращения к DLL, включить заголовочный файл, полученный при разработке этой DLL;

2. Если при создании DLL не производилось расширение имен с помощью модификатора extern " C ", то и в приложении необходимо применить этот модификатор для заголовочного файла библиотеки;

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

4. Передать компоновщику имя библиотеки импорта DLL. Сделать это можно двумя способами:

а) указать имя библиотеки импорта в опциях компоновщика. Для этого необходимо открыть окно настроек проекта (Project->Settings...) и добавить в поле Object/Library modules на вкладке Link имя библиотеки импорта;

б) встроить ссылку на библиотеку импорта прямо в исходный код программы с помощью директивы #pragma c ключем comment. Например, для библиотеки импорта MyDLL.lib необходимо записать:

#pragma comment(lib, "MyDLL.lib")

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

Выполнение приложения при неявной загрузке DLL

При запуске приложения загрузчик ОС выполняет следующие действия:

1. Создает виртуальное адресное пространство для нового процесса и проецирует на него ЕХЕ-файл.

2. Выбирает в секции импорта все необходимые DLL, ищет их файлы на дисках компьютера и проецирует на адресное пространство процесса. Поиск файлов DLL производится в следующем порядке:

- каталог, содержащий ЕХЕ-файл;

- текущий каталог процесса;

- системный каталог Windows;

- основной каталог Windows;

- каталоги в переменной окружения PATH.

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

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

4. Настраивает ссылки на импортируемые идентифи-каторы. Для этого просматривает секцию импорта приложения и для каждого импортируемого идентификатора определяет в секции экспорта DLL его RVA (Relative Virtual Adress). Затем прибавляет этот RVA к виртуальному адресу размещения DLL в адресном пространстве процесса и сохраняет полученный адрес в секции импорта ЕХЕ-модуля. Этот адрес указывает непосредственно на соответствующую переменную, функцию или метод С++-класса в DLL.

После отображения всех DLL на адресное пространство процесса его первичный поток готов к выполнению и приложение начинает работу.

Доступ к идентификаторам, импортируемым из DLL

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

Например, в заголовочном файле MyDLL.h библиотеки MyDLL.dll, включенном в приложение, описаны следующие импортируемые идентификаторы:

extern __declspec (dllimport) int nMyDLLVar;

__declspec (dllimport) int MyDLLFunction(void);

class __declspec (dllimport) MyDLLClass {

public:

MyDLLClass(void); // Конструктор класса

int ClMyFunc(void); // Не виртуальная функция

virtual int ClVirtFunc(void); // Виртуальная функция

};

Тогда в исходном тексте приложения, которое неявно загружает эту DLL, можно обращаться к указанным выше идентификаторам как обычно:

// Изменение значения переменной nMyDLLVar в DLL

nMyDLLVar = 56;

// Вызов функции MyDLLFunction() из DLL

int iRes1 = MyDLLFunction();

// Создание статического объекта класса MyDLLClass

MyDLLClass MyStatObj;

// Вызов методов класса для статического объекта

int iRes2 = MyStatObj.ClMyFunc();

int iRes3 = MyStatObj.ClVirtFunc();

// Создание динамического объекта класса

MyDLLClass *pMyDynObj=new MyDLLClass;

// Вызов методов класса для динамического объекта

// через указатель на него

iRes2 = pMyDynObj->ClMyFunc();

iRes3 = pMyDynObj->ClVirtFunc();

4. Явное подключение DLL к приложению

Для явного подключения DLL, при создании приложения не нужно использовать ее библиотеку импорта (.lib). Однако потребуется для каждой DLL дополнительно в программе выполнить следующее:

а) явно загрузить DLL в адресное пространство процесса;

б) получить виртуальные адреса необходимых DLL-функций и вызвать их по этим адресам;

в) выгрузить DLL из адресного пространства процесса до его завершения.

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

Явная загрузка DLL приложением

Явно загрузить требуемую DLL в ходе выполнения программы можно с помощью функции LoadLibrary ():

HMODULE LoadLibrary(LPCTSTR lpFileName);

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

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

Функция возвращает дескриптор загруженной библиотеки или NULL, если DLL не загружена.

Примечание. Явно загрузить DLL можно также с помощью функции LoadLibraryEx ().

Windows поддерживает в каждом процессе свой счетчик пользователей DLL. При первом вызове LoadLibrary() система проецирует образ DLL-файла на адресное пространство процесса и присваивает 1 этому счетчику. Если другой поток того же процесса вызывает LoadLibrary() для той же DLL еще раз, эта библиотека больше не проецируется, система увеличивает счетчик для этой DLL на 1.

Пример явной загрузки приложением библиотеки MyDLL.dll и получения её описателя:

HMODULE hMyDll = LoadLibrary("MyDLL.dll");

Явная выгрузка DLL из памяти

Функция FreeLibrary () выгружает DLL из памяти:

BOOL FreeLibrary(HMODULE hModule);

Параметр hModule - это дескриптор выгружаемой библиотеки, полученный функцией LoadLibrary(Ex).

При успешной выгрузке DLL возвращается значение TRUE, в противном случае – FALSE.

При вызове функции FreeLibrary () ОС уменьшает на 1 счетчик числа пользователей DLL. Если он станет равным 0, то библиотека выгружается из памяти. Иначе выгрузка DLL не производится.

Если вызов FreeLibrary () отсутствует, то DLL будет оставаться в памяти до завершения приложения.

Пример выгрузки явно загруженной ранее приложением библиотеки MyDLL.dll:

FreeLibrary(hMyDll);

Вызов функций из явно загруженной DLL

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

1) Объявить указатель на функцию в виде:

тип (*указатель) (список параметров);

или c помощью typedef определить новый тип в виде указателя на функцию и объявить указатель:

typedef тип (*имя_типа) (список параметров);

имя_типа указатель;

2) Получить адрес функции из DLL и сохранить его в указателе на эту функцию;

3) Вызывать функцию через указатель с заданием фактических параметров.

Получение адреса функции из DLL. С помощью функции GetProcAddress(), для загруженной в память DLL, можно получить адрес её функции:

FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpProcName);

Параметр hModule – это описатель экземпляра загруженной ранее DLL. Параметр lpProcName – это строка с именем функции, находящейся в DLL или ее порядковый номер, определенный в .def файле или в секции экспорта этой DLL.

GetProcAddress() возвращает адрес функции в DLL в виде значения типа FARPROC, т.е. адреса функции, которая не имеет параметров и возвращает значение типа int. Поэтому требуется приведение его к реальному прототипу функции из DLL или наоборот.

Если требуемая функция в DLL не найдена, то возвращается значение NULL.

Пример вызова функции MyDLLFunction () из явно загруженной библиотеки MyDLL.dll.

// Прототип функции в заголовочном файле MyDLL.h

MyDLL_API int MyDLLFunction();…// Определение нового типаtypedef int (*PMYDLLFUNC)(); // Объявление указателя на функциюPMYDLLFUNC pMyDLLFunc; // Загрузка DLL и получение её описателяHMODULE hMyDll = LoadLibrary("MyDLL.dll");//Получение адреса функцииpMyDLLFunc = (PMYDLLFUNC) GetProcAddress(hMyDll, "MyDLLFunction");//Вызов функции из DLL через указательint iRes1 = pMyDLLFunc();

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

// Объявление указателя на MyDLLFunc()int (*pMyDLLFunc)(); // Загрузка DLL и получение её описателяHMODULE hMyDll = LoadLibrary("MyDLL.dll");//Получение адреса функции(FARPROC &) pMyDLLFunc = GetProcAddress(hMyDll, "MyDLLFunction");//Вызов функции из DLL через указатель

int iRes1 = pMyDLLFunc();

Если в параметре lpProcName задается для функции из DLL не имя, а её порядковый номер, то получение адреса будет иметь вид:

pMyDLLFunc = (PMYDLLFUNC) GetProcAddress(hMyDll, MAKEINTRESOURCE(N));

где N - порядковый номер функции, определенный в .def файле DLL или в её секции экспорта.

Недостаток: если номер функции будет задан неправильно и она не будет найдена в DLL, то GetProcAddress() возвращает не NULL, что при вызове этой функции приведет к ошибке.

Доступ к переменным в явно загруженной DLL

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

Пример. Получение доступа к переменной MyDLLVar из явно загруженной библиотеки MyDLL.dll:

// Объявление указателя на переменную MyDLLVar

int *pMyDLLVar;// Получение адреса переменной из DLL(FARPROC &) pMyDLLVar = GetProcAddress(hMyDll, "MyDLLVar");

// Изменение значения переменной

*pMyDLLVar = 72;

Работа с классами при явной загрузке DLL

Работа с объектами классов, размещенных в явно загружаемых DLL, в отличие от неявного подключения DLL, имеет свои особенности:

- на классы, как конструкции языка C++, не распостраняется действие модификатора extern " C ", поэтому компилятор C++ расширяет имена экспортируемых DLL классов и их методов;

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

- виртуальные методы класса можно вызывать напрямую, по их именам через таблицу адресов виртуальных функций vtable в классе;

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

На основании вышеизложенного можно предложить два варианта работы в приложении с классами из явно загружаемых DLL:

1. Имеется .def файл или секция экспорта DLL, из которых можно определить порядковые номера всех методов класса в библиотеке. Тогда доступ к методам класса в DLL, в том числе и к конструктору, деструктору можно получить с помощью функции GetProcAddress() через их порядковые номера;

2. Отсутствуют .def файл или секция экспорта DLL, поэтому порядковые номера методов класса в библиотеке не известны. В связи с этим потребуется в DLL дополнительно описать две функции для создания и уничтожения объекта класса, а в приложении получить адреса этих функций по именам и вызвать их.

ВНИМАНИЕ!

1-й и 2-й варианты работы в приложении с классами приведены в электронном виде в дополнительном материале к этой теме.

5. DLL отложенной загрузки

DLL отложенной загрузки (delay-load DLL) – это неявно связываемая DLL, которая не загружается до тех пор, пока в приложении не будет обращения к экспортируемому из нее идентификатору.

Отложенная загрузка DLL

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

1) Создать, как обычно, динамически подключаемую библиотеку и ее библиотеку импорта, например MyDLL.dll и MyDLL.lib;

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

#include "MyDLL.h"

#pragma comment(lib, "MyDLL.lib")

3) Дополнительно в приложении потребуется:

а) включить в него заголовочный файл delayimp.h и подключить статическую библиотеку Delayimp.lib, в которой содержатся функции отложенной загрузки:

#include <delayimp.h>

#pragma comment(lib, "Delayimp.lib")

б) указать компоновщику с помощью директивы #pragma ключ /DelayLoad:<DLLName>, сообщающий, что загрузку DLL с именем DLLName необходимо отложить до первого обращения к одной из ее функций:

#pragma comment(linker, "/DelayLoad:MyDLL.dll")

Этот ключ указывается отдельно для каждой DLL, загрузку которой требуется отложить.

При этом компоновщик: 1) удаляет MyDLL.dll из секции импорта; 2) добавляет секцию отложенного импорта (.didat) и включает в неё MyDLL.dll; 3)приводит вызовы функций из MyDLL.dll к вызовам __delayLoadHelper() из библиотекиDelayimp.lib. Эта функция производит загрузку указанной библиотеки и получает в ней адрес требуемой функции.

Особенности работы с переменными из DLL отложенной загрузки. Описанный выше механизм не действует, когда необходимо обращаться к переменным из DLL отложенной загрузки, так как не происходит вызовов функции __delayLoadHelper(). Выходом является явная загрузка DLL с помощью LoadLibrary () и получение адреса требуемого объекта с помощью GetProcAddress ().

Пример работы с переменной MyDLLVar из библиотеки MyDLL.dll отложенной загрузки:

// Сообщение линкеру об отложенной загрузке DLL

#pragma comment(linker, "/DelayLoad:MyDLL.dll")

// Объявление указателя на переменную MyDLLVar

int *pMyDLLVar;// Загрузка MyDLL.dll и получение её описателяHMODULE hMyDll = LoadLibrary("MyDLL.dll");// Получение адреса переменной MyDLLVar из DLL(FARPROC &) pMyDLLVar = GetProcAddress(hMyDll, "MyDLLVar");

// Изменение переменной в DLL через указатель

*pMyDLLVar = 184;

Выгрузка из памяти DLL отложенной загрузки

DLL отложенной загрузки можно выгружать из памяти в ходе выполнения приложения, для этого необходимо:

1) Указать компоновщику с помощью директивы #pragma ключ /Delay:unload, который сообщает, что возможна выгрузка DLL:

#pragma comment(linker, "/Delay:unload")

По этому ключу компоновщик создает в .exe-файле дополнительную секцию выгрузки (unload section).

2) В точке выгрузки DLL поместить вызов функции __FUnloadDelayLoadedDLL() из Delayimp.lib:

BOOL __FUnloadDelayLoadedDLL(PCSTR szDLLName);

Параметр sz DLL Name - это указатель на строку с именем выгружаемой DLL. Функция просматривает секцию выгрузки приложения, удаляет адреса всех функций этой DLL ивызывает FreeLibrary(). Возвращаемое значение сообщает об успешной (TRUE) или не успешной (FALSE) выгрузке библиотеки.

Пример организации отложенной загрузки MyDLL.dll и её последующей выгрузки:

#include "stdafx.h"

#include <delayimp.h>

#include "MyDLL.h"

// Подключение библиотеки импорта DLL

#pragma comment(lib, "MyDLL.lib")

// Задание дополнительных ключей компоновщику

#pragma comment(lib, "Delayimp.lib")

#pragma comment(linker, "/DelayLoad:"MyDLL.dll"")

#pragma comment(linker, "/Delay:unload")

// MyDLL.dll будет загружена в этом месте

int iRes1=MyDLLFunction(); //1-й вызов ф-ции из DLL

// MyDLL.dll будет выгружена в этом месте

// Проверка результата выгрузки библиотеки

if (!__FUnloadDelayLoadedDLL("MyDLL.dll"))

 { MessageBox(NULL, "DLL не была выгружена",

           "Результат выгрузки MyLib.dll", MB_OK);

return 0; }

 

Дополнительный материал по теме:

"Динамически подключаемые библиотеки"

 

Явное подключение DLL

Работа с классами при явной загрузке DLL

 

1-й вариант. Доступ к методам класса через их порядковые номера

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

- получение адресов конструктора, деструктора и не виртуальных методов класса в DLL можно выполнять с помощью функции GetProcAddress() через их порядковые номера;

- вызовы виртуальных методов класса можно производить по их именам, а остальных методов – через их указатели, полученные ранее;

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

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

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

Пример. В лекции п. 2. Создание DLL приведено описание заголовочного файла и исходного кода библиотеки динамической компоновки MyDLL.dll, содержащей класс MyDLLClass, в котором имеются: конструктор MyDLLClass(), деструктор ~MyDLLClass(), не виртуальная функция ClMyFunc() и виртуальная функция ClVirtFunc().

Необходимо создать приложение MyDLLApp1.exe, которое явно загрузит указанную DLL, создаст объект класса MyDLLClass и вызовет названные выше методы класса (кроме виртуальных) через их порядковые номера.

С помощью утилиты DUMPBIN.EXE (с ключом /EXPORTS) можно получить содержимое секции экспорта в библиотеке MyDLL.dll, чтобы определить порядковые номера функций класса MyDLLClass. Для этого необходимо записать в командной строке для текущего каталога следующее:

DUMPBIN.EXE /EXPORTS MyDLL.dll /OUT:"ExportSection.txt"

В результате выполнения этой команды в текстовом файле ExportSection.txt будет записано содержимое секции экспорта библиотеки MyDLL.dll, которое в сокращенном виде выглядит следующим образом:

Dump of file MyDLL.dll

File Type: DLL

Section contains the following exports for MyDLL.dll

ordinal hint RVA name

 

     1 0 00001023 ??0MyDLLClass@@QAE@ABV0@@Z

     2 1 00001014 ??0MyDLLClass@@QAE@XZ

     3 2 00001028 ??1MyDLLClass@@QAE@XZ

     4 3 00001005 ??4MyDLLClass@@QAEAAV0@ABV0@@Z

     5 4 0002901C ??_7MyDLLClass@@6B@

     6 5 0000101E ?ClMyFunc@MyDLLClass@@QAEHXZ

     7 6 0000100F ?ClVirtFunc@MyDLLClass@@UAEHXZ

     8 7 0000100A MyDLLFunction

     9 8 0002E3F0 nMyDLLVar

    Из этого файла можно определить порядковые номера (ordinal) требуемых методов класса:

- конструктор MyDLLClass() – 2;

- деструктор ~MyDLLClass() – 3;

- не виртуальная функция ClMyFunc() – 6;

- виртуальная функция ClVirtFunc() – 7, но этот номер не потребуется.

    В файле MyDLLApp1.cpp с исходным кодом программы MyDLLApp1.exe, которая явно загружает библиотеку MyDLL.dll необходимо записать следующее:

Файл MyDLLApp 1. cpp

#include "stdafx.h"

#include "MyDLL.h" …// Явная загрузка библиотеки MyDLL.dllHMODULE hMyDll = LoadLibrary("MyDLL.dll"); …  

// Объявление указателя на конструктор класса и получение его адреса

void (MyDLLClass :: *pConstructor)();

(FARPROC &) pConstructor = GetProcAddress(hMyDll, MAKEINTRESOURCE(2));// Выделение памяти в куче для объекта класса MyDLLClassMyDLLClass *pObj = (MyDLLClass *) new char[sizeof(MyDLLClass)];// Вызов конструктора через указатель и создание объекта

pObj->pConstructor();

// Объявление указателя на метод ClMyFunc() и получение его адреса

int (MyDLLClass :: *pClMyFunc)();(FARPROC &) pClMyFunc = GetProcAddress(hMyDll, MAKEINTRESOURCE(6));// Вызов метода ClMyFunc() через указатель на негоint iRez1 = pObj->pClMyFunc();

// Вызов виртуальной функции ClVirtFunc() производится напрямую –

// по имени, через таблицу адресов виртуальных функций vtable

int iRez2 = pObj->ClVirtFunc();

// Объявление указателя на деструктор класса и получение его адреса

void (MyDLLClass :: *pDestructor)();

(FARPROC &) pDestructor = GetProcAddress(hMyDll, MAKEINTRESOURCE(3));// Вызов деструктора через указатель и уничтожение объектаpObj->pDestructor();// Освобождение памяти в куче

delete [ ] pObj;

// Выгрузка библиотеки MyDLL.dll

FreeLibrary(hMyDll);


Дата добавления: 2019-09-13; просмотров: 583; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!