Информация о файловой системе
В этом разделе мы кратко опишем функции программного интерфейса Microsoft Windows NT, предназначенные для получения информации о дисковых устройствах и состоянии файловой системы.
Инициализация критической секции
Перед использованием критической секции ее необходимо проинициализировать, вызвав для этого функцию InitializeCriticalSection:
CRITICAL_SECTION csWindowPaint;
InitializeCriticalSection(&csWindowPaint);
Функция InitializeCriticalSection имеет только один параметр (адрес структуры типа CRITICAL_SECTION) и не возвращает никакого значения.
Исходные тексты приложения EVENT
Рассмотрим сначала исходные тексты приложения EVENT, представленные в листинге 4.1.
Листинг 4.1. Файл event/event1.c
#include <windows.h>
#include <stdio.h>
#include <conio.h>
// Идентификаторы объектов-событий, которые используются
// для синхронизации задач, принадлежащих разным процессам
HANDLE hEvent;
HANDLE hEventTermination;
// Имя объекта-события для синхронизации ввода и отображения
CHAR lpEventName[] =
"$MyVerySpecialEventName$";
// Имя объекта-события для завершения процесса
CHAR lpEventTerminationName[] =
"$MyVerySpecialEventTerminationName$";
int main()
{
DWORD dwRetCode;
printf("Event demo application, master process\n"
"(C) A. Frolov, 1996, Email: frolov@glas.apc.org\n");
// Создаем объект-событие для синхронизации
// ввода и отображения, выполняемого в разных процессах
hEvent = CreateEvent(NULL, FALSE, FALSE, lpEventName);
// Если произошла ошибка, получаем и отображаем ее код,
// а затем завершаем работу приложения
if(hEvent == NULL)
{
fprintf(stdout,"CreateEvent: Error %ld\n",
GetLastError());
getch();
return 0;
}
// Если объект-событие с указанным именем существует,
// считаем, что приложение EVENT уже было запущено
if(GetLastError() == ERROR_ALREADY_EXISTS)
{
printf("\nApplication EVENT already started\n"
"Press any key to exit...");
getch();
return 0;
}
// Создаем объект-событие для определения момента
// завершения работы процесса ввода
hEventTermination = CreateEvent(NULL,
FALSE, FALSE, lpEventTerminationName);
if(hEventTermination == NULL)
{
fprintf(stdout,"CreateEvent (Termination): Error %ld\n",
GetLastError());
getch();
return 0;
}
// Цикл отображения. Этот цикл завершает свою работу
// при завершении процесса ввода
while(TRUE)
{
// Проверяем состояние объекта-события, отвечающего
// за контроль завершения процесса ввода. Так как
// указано нулевое время ожидания, такая проверка
// не уменьшает заметно скорость работы приложения
dwRetCode = WaitForSingleObject(hEventTermination, 0);
// Если объект-событие перешел в отмеченное состояние,
// если процесс ввода завершил свою работу, или
// если при ожидании произошла ошибка,
// останавливаем цикл отображения
if(dwRetCode == WAIT_OBJECT_0
dwRetCode == WAIT_ABANDONED
dwRetCode == WAIT_FAILED)
break;
// Выполняем ожидание ввода символа в процессе,
// который работает с клавиатурой
dwRetCode = WaitForSingleObject(hEvent, INFINITE);
// При возникновении ошибки прерываем цикл
if(dwRetCode == WAIT_FAILED
dwRetCode == WAIT_ABANDONED)
break;
// В ответ на каждый символ, введенный процессом, который
// работает с клавиатурой, отображаем символ '*'
putch('*');
}
// Закрываем идентификаторы объектов-событий
CloseHandle(hEvent);
CloseHandle(hEventTermination);
return 0;
}
В глобальных переменных lpEventName и lpEventTerminationName мы храним имена двух объектов-событий, которые будут использоваться для синхронизации. Эти же имена будут указаны функции OpenEvent в приложении EVENTGEN, которое мы рассмотрим чуть позже.
Объекты-события создаются нашим приложением при помощи функции CreateEvent, вызываемой следующим способом:
hEvent = CreateEvent(NULL, FALSE, FALSE, lpEventName);
hEventTermination = CreateEvent(NULL, FALSE, FALSE,
lpEventTerminationName);
Здесь в качестве атрибутов защиты мы указываем значение NULL. Через второй и третий параметр функции CreateEvent имеют значение FALSE, поэтому создается автоматический объект-событие, которое изначально находится в неотмеченном состоянии. Имя этого события, доступное всем запущенным приложениям, передается функции CreateEvent через последний параметр.
Обратите внимание на следующий фрагмент кода, который расположен сразу после вызова функции, создающей объект-событие с именем lpEventName:
if(GetLastError() == ERROR_ALREADY_EXISTS)
{
printf("\nApplication EVENT already started\n"
"Press any key to exit...");
getch();
return 0;
}
Функция CreateEvent может завершиться с ошибкой и в этом случае она возвращает значение NULL. Однако пользователь может попытаться запустить приложение EVENT два раза. В первый раз при этом будет создан объект-событие с именем lpEventName. Когда же функция CreateEvent будет вызвана для этого имени еще раз при попытке повторного запуска приложения, она не вернет признак ошибки, несмотря на то что объект-событие с таким именем уже существует в системе. Вместо этого функция вернет идентификатор для уже существующего объекта-события.
Как распознать такую ситуацию?
Очень просто - достаточно сразу после вызова функции CreateEvent проверить код завершения при помощи функции GetLastError. Как мы уже говорили, в случае попытки создания объекта-события с уже существующим в системе именем эта функция вернет значение ERROR_ALREADY_EXISTS. Как только это произойдет, наше приложение выдает сообщение о том, что его копия уже запущена, после чего завершает свою работу.
Таким образом мы нашли еще один способ (далеко не последний), с помощью которого в операционной системе Microsoft Windows NT можно предотвратить повторный запуск приложений. До сих пор мы решали эту задачу в графических приложениях с помощью функции FindWindow.
Продолжим изучение исходный текстов приложения EVENT.
После того как приложение создаст два объекта-события, оно входит в цикл отобаржения символа ‘*’.
В этом цикле прежде всего проверяется состояние объекта-события с идентификатором hEventTermination:
dwRetCode = WaitForSingleObject(hEventTermination, 0);
if(dwRetCode == WAIT_OBJECT_0
dwRetCode == WAIT_ABANDONED
dwRetCode == WAIT_FAILED)
break;
Приложение EVENTGEN переводит этот объект в отмеченное состояние перед своим завершением.
Обращаем ваше внимание на то что функции WaitForSingleObject указано нулевое время ожидания. В этом случае функция проверяет состояние объекта и сразу же возвращает управление.
Далее выполняется ожидание объекта-события с идентификатором hEvent:
dwRetCode = WaitForSingleObject(hEvent, INFINITE);
if(dwRetCode == WAIT_FAILED
dwRetCode == WAIT_ABANDONED)
break;
Если это ожидание завершилось с ошибкой, выполняется выход из цикла. Если же оно было выполнено без ошибок, приложение EVENT отображает символ ‘*’, пользуясь для этого функцией putch, известной вам из практики программирования для MS-DOS.
Перед завершением работы приложения мы выполняем освобождение идентификаторов созданных объектов-событий, пользуясь для этого функцией CloseHandle:
CloseHandle(hEvent);
CloseHandle(hEventTermination);
Исходные тексты приложения EVENTGEN
Исходные тексты приложения EVENTGEN, которое работает в паре с только что рассмотренным приложением EVENT, приведены в листинге 4.2.
Листинг 4.2. Файл event/eventgen/event2.c
#include <windows.h>
#include <stdio.h>
#include <conio.h>
// Идентификаторы объектов-событий, которые используются
// для синхронизации задач, принадлежащих разным процессам
HANDLE hEvent;
HANDLE hEventTermination;
// Имя объекта-события для синхронизации ввода и отображения
CHAR lpEventName[] =
"$MyVerySpecialEventName$";
// Имя объекта-события для завершения процесса
CHAR lpEventTerminationName[] =
"$MyVerySpecialEventTerminationName$";
int main()
{
CHAR chr;
printf("Event demo application, slave process\n"
"(C) A. Frolov, 1996, Email: frolov@glas.apc.org\n"
"\n\nPress <ESC> to terminate...\n");
// Открываем объект-событие для синхронизации
// ввода и отображения
hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, lpEventName);
if(hEvent == NULL)
{
fprintf(stdout,"OpenEvent: Error %ld\n",
GetLastError());
getch();
return 0;
}
// Открываем объект-событие для сигнализации о
// завершении процесса ввода
hEventTermination = OpenEvent(EVENT_ALL_ACCESS,
FALSE, lpEventTerminationName);
if(hEventTermination == NULL)
{
fprintf(stdout,"OpenEvent (Termination): Error %ld\n",
GetLastError());
getch();
return 0;
}
// Цикл ввода. Этот цикл завершает свою работу,
// когда пользователь нажимает клавишу <ESC>,
// имеющую код 27
while(TRUE)
{
// Проверяем код введенной клавиши
chr = getche();
// Если нажали клавишу <ESC>, прерываем цикл
if(chr == 27)
break;
// Устанавливаем объект-событие в отмеченное
// состояние. В ответ на это процесс отображения
// выведет на свою консоль символ '*'
Исходный текст приложения
Исходный текст приложения HEAPMEM представлен в листинге 1.5. Файлы описания ресурсов и определения модуля не используются.
Листинг 1.5. Файл heapmem/heapmem.c
#include <windows.h>
#include <stdio.h>
#include <conio.h>
int main()
{
// Идентификатор динамического пула
HANDLE hHeap;
// Указатель, в который будет записан адрес
// полученного блока памяти
char *lpszBuff;
// =================================================
// Работа с динамическим пулом
// Используем структурную обработку исключений
// =================================================
// Создаем динамический пул
hHeap = HeapCreate(0, 0x1000, 0x2000);
// Если произошла ошибка, выводим ее код и
// завершаем работу приложения
if(hHeap == NULL)
{
fprintf(stdout,"HeapCreate: Error %ld\n",
GetLastError());
getch();
return 0;
}
// Пытаемся получить из пула блок памяти
__try
{
lpszBuff = (char*)HeapAlloc(hHeap,
HEAP_GENERATE_EXCEPTIONS, 0x1500);
}
// Если память недоступна, происходит исключение,
// которое мы обрабатываем
__except (EXCEPTION_EXECUTE_HANDLER)
{
fprintf(stdout,"1. HeapAlloc: Exception %lX\n",
GetExceptionCode());
}
// Пытаемся записать в буфер текстовую строку
__try
{
strcpy(lpszBuff, "Строка для проверки");
}
// Если содержимое указателя lpszBuff равно NULL,
// произойдет исключение
__except (EXCEPTION_EXECUTE_HANDLER)
{
fprintf(stdout,"1. strcpy: Exception %lX \n",
GetExceptionCode());
}
// Выполняем повторную попытку, указывая меньший
// размер блока памяти
__try
{
lpszBuff = (char*)HeapAlloc(hHeap,
HEAP_GENERATE_EXCEPTIONS, 0x100);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
fprintf(stdout,"2. HeapAlloc: Exception %lX\n",
GetExceptionCode());
}
__try
{
strcpy(lpszBuff, "Test string");
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
fprintf(stdout,"2. strcpy: Exception %lX \n",
GetExceptionCode());
}
// Отображаем записанную строку
if(lpszBuff != NULL)
printf("String:>%s<\n", lpszBuff);
// Изменяем размер блока памяти
__try
{
HeapReAlloc(hHeap, HEAP_GENERATE_EXCEPTIONS |
HEAP_REALLOC_IN_PLACE_ONLY, lpszBuff, 150);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
fprintf(stdout,"HeapReAlloc: Exception %lX \n",
GetExceptionCode());
}
// Освобождаем блок памяти
if(lpszBuff != NULL)
HeapFree(hHeap, HEAP_NO_SERIALIZE, lpszBuff);
// Удаляем пул памяти
if(!HeapDestroy(hHeap))
fprintf(stdout,"Ошибка %ld при удалении пула\n",
GetLastError());
// =================================================
// Работа со стандартным пулом
// Исключения не обрабатываем
// =================================================
// Получаем блок памяти из стандартного пула
lpszBuff = (char*)HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY, 0x1000);
// Если памяти нет, выводим сообщение об ошибке
// и завершаем работу программы
if(lpszBuff == NULL)
{
fprintf(stdout,"3. HeapAlloc: Error %ld\n",
GetLastError());
getch();
return 0;
}
// Выполняем копирование строки
strcpy(lpszBuff, "Test string");
// Отображаем скопированную строку
printf("String:>%s<\n", lpszBuff);
// Освобождаем блок памяти
if(lpszBuff != NULL)
HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, lpszBuff);
// =================================================
// Работа со стандартными функциями
// Исключения не обрабатываем
// =================================================
lpszBuff = malloc(1000);
if(lpszBuff != NULL)
{
strcpy(lpszBuff, "Test string");
printf("String:>%s<\n", lpszBuff);
free(lpszBuff);
}
printf("Press any key...");
getch();
return 0;
}
В отличие от обычного приложения Microsoft Windows NT, исходный текст консольного приложения должен содержать функцию main (аналогично программе MS-DOS). В теле этой функции мы определили переменные hHeap и lpszBuff. Первая из них используется для хранения идентификатора динамического пула памяти, вторая - для хранения указателя на полученный блок памяти.
Использование функций malloc и free
В библиотеке Microsoft Visual C++ имеются стандартные функции, предназначенные для динамического получения и освобождения памяти, такие как malloc и free.
У нас есть хорошая новость для вас - в среде Microsoft Windows NT вы можете использовать эти функции с той же эффективностью, что и функции, предназначенные для работы с пулами - HeapAlloc, HeapFree и так далее. Правда, эти функции получают память только из стандартного пула.
Одно из преимуществ функций malloc и free заключается в возможности их использования на других платформах, отличных от Microsoft Windows NT.
Изменение имени каталога
В программном интерфейсе Microsoft Windows NT нет специальной функции, предназначенной для изменения имени каталогов, однако вы можете выполнить эту операцию при помощи функций MoveFile или MoveFileEx, предназначенных для перемещения файлов. Разумеется, при этом новый и старый каталоги должны располагаться на одном и том же диске.
Изменение метки тома
При необходимости вы можете легко изменить метку тома, вызвав для этого функцию SetVolumeLabel:
BOOL SetVolumeLabel(
LPCTSTR lpRootPathName, // адрес пути
// к корневому каталогу тома
LPCTSTR lpVolumeName); // новая метка тома
Если параметр lpVolumeName указать как NULL, метка тома будет удалена.
Изменение приоритета задачи
В разделе “Относительный приоритет задач” нашей книги мы рассказали вам о том, как в операционной системе Microsoft Windows NT устанавливаются приоритеты задач. Родительская задача может изменить относительный приоритет запущенной ей дочерней задачи с помощью функции SetThreadPriority:
BOOL SetThreadPriority(
HANDLE hThread, // идентификатор задачи
int nPriority); // новый уровень приоритета задачи
Через параметр hThread этой функции передается идентификатор задачи, для которой необходимо изменить относительный приоритет.
Новое значение относительного приоритета передается через параметр nPriority и может принимать одно из следующих значений:
THREAD_PRIORITY_TIME_CRITICAL;
THREAD_PRIORITY_HIGHEST;
THREAD_PRIORITY_ABOVE_NORMAL;
THREAD_PRIORITY_NORMAL;
THREAD_PRIORITY_BELOW_NORMAL;
THREAD_PRIORITY_LOWEST;
THREAD_PRIORITY_IDLE
Абсолютный уровень приоритета, который получит задача, зависит от класса приоритета процесса. Забегая вперед, скажем, что класс приоритета процесса можно изменить при помощи функции SetPriorityClass.
Изменение размера блока памяти
С помощью функции HeapReAlloc приложение может изменить размер блока памяти, выделенного ранее функцией HeapAlloc, уменьшив или увеличив его. Прототип функции HeapReAlloc приведен ниже:
LPVOID HeapReAlloc(
HANDLE hHeap, // идентификатор пула
DWORD dwFlags, // флаг изменения размера блока памяти
LPVOID lpMem, // адрес блока памяти
DWORD dwBytes); // новый размер блока памяти в байтах
Для пула hHeap эта функция изменяет размер блока памяти, расположенного по адресу lpMem. Новый размер составит dwBytes байт.
В случае удачи функция HeapReAlloc возвратит адрес нового блока памяти, который не обязательно будет совпадать с адресом, полученным этой функцией через параметр lpMem.
Через параметр dwFlags вы можете передавать те же параметры, что и через аналогичный параметр для функции HeapAlloc. Дополнительно можно указать параметр HEAP_REALLOC_IN_PLACE_ONLY, определяющий, что при изменении размера блока памяти его нужно оставить на прежнем месте адресного пространства. Очевидно, что если указан этот параметр, в случае успешного завершения функция HeapReAlloc вернет то же значение, что было передано ей через параметр lpMem.
Изменение текущего каталога
Для изменения текущего каталога используйте функцию SetCurrentDtirecory:
BOOL SetCurrentDirectory(LPCTSTR lpszCurDir);
Через параметр lpszCurDir вы должны передать функции SetCurrentDtirecory путь к новому каталогу.
Изменение типа разрешенного доступа для страниц памяти
При получении страниц памяти в пользование функцией VirtualAlloc вы можете в последнем параметре указать тип доступа, разрешенного для этих страниц. В процессе работы приложение может изменять тип доступа для полученных им страниц при помощи функции VirtualProtect, прототип которой представлен ниже:
BOOL VirtualProtect(
LPVOID lpvAddress, // адрес области памяти
DWORD cbSize, // размер области памяти в байтах
DWORD fdwNewProtect, // новый тип разрешенного доступа
PDWORD pfdwOldProtect); // указатель на переменную,
// в которую будет записан прежний код доступа
Через параметр lpvAddress вы должны передать адрес области памяти, расположенной в готовых для использования страницах (а не в зарезервированных страницах).
Новый тип доступа передается через параметр fdwNewProtect. Здесь вы можете использовать все константы, что и для последнего параметра функции VirtualAlloc, например, PAGE_READWRITE или PAGE_READONLY.
Зачем вам может пригодиться изменение кода доступа?
Например, вы можете получить страницы страницы памяти, доступные на запись, а затем, записав туда данные, разрешить доступ только на чтение или на исполнение. Устанавливая тип доступа PAGE_NOACCESS для страниц, которые в данный момент не используются приложением, вы можете, например, обнаружить во время исполнения кода ошибки, связанные с использованием неправильных указателей. Для решения аналогичных задач можно также устанавливать тип доступа PAGE_GUARD.
Заметим, что функция VirtualProtect позволяет изменить код доступа только для тех страниц, которые созданы вызывающим ее процессом. При необходимости приложение может изменить код доступа страниц другого процесса, имеющего код доступа PROCESS_VM_OPERATION (например, процесса, созданного приложением). Это можно сделать при помощи функции VirtualProtectEx, прототип которой представлен ниже:
BOOL VirtualProtectEx(
HANDLE hProcess, // идентификатор процесса
LPVOID lpvAddress, // адрес области памяти
DWORD cbSize, // размер области памяти в байтах
DWORD fdwNewProtect, // новый тип разрешенного доступа
PDWORD pfdwOldProtect); // указатель на переменную,
// в которую будет записан прежний код доступа
Через параметр hProcess функции VirtualProtectEx следует передать идентификатор процесса. Подробнее об этом идентификаторе вы узнаете из главы, посвященной мультизадачности в операционной системе Microsoft Windows NT.
Изолированные адресные пространства
Для обеспечения необходимой надежности работы в Microsoft Windows NT адресные пространства всех запущенных приложений разделены. Такое разделение выполняется с помощью назначения приложениям индивидуальных наборов таблиц страниц вируальной памяти. В результате для каждого приложения выполняется отображение линейных адресов в собственный набор страниц виртуальной памяти, не пересекающийся с набором страниц других приложений.
Заметим, что приложение не имеет физической возможности выполнить адресацию памяти в пространстве, принадлежащем другому приложению. Какой бы линейный адрес ни задавало приложение, этот адрес всегда будет соответствовать одной из страниц, принадлежащих самому приложению. Такое положение дел обеспечивается системой управления виртуальной памятью Microsoft Windows NT и значительно повышает устойчивость операционной системы.
Однако полное изолирование адресных пространств создает трудности при необходимости организации обмена данными между различными приложениями. Вы не можете просто так передать указатель на область памяти из одного приложения в другое, так как в контексте другого приложения содержимое этого указателя не будет иметь смысла. Так как адресные пространства приложений изолированы, одному и тому же значению линейного адреса будут соотвтетсвовать ячейки памяти, расположенные в разных страницах.
В операционной системе Microsoft Windows версии 3.1 все приложения работали в одном адресном пространстве. Поэтому для передачи данных между приложениями можно было заказать область памяти в глобальном пуле с помощью функции GlobalAlloc и затем передать адрес этой области другому приложению. В Microsoft Windows NT этот метод работать не будет.
Выход, тем не менее, есть. Ничто не мешает операционной системе создать в каталоге страниц нескольких приложений специальный дескриптор, указывающий на страницы виртуальной памяти общего пользования. Такие дескрипторы называются дескрипторами прототипа PTE (Prototype Page Table Entry) и используются для совместного использования страниц памяти в операционной системе Microsoft Windows NT.
Дескрипторы PTE создаются для совместного использования страниц, содержащих исполнимый программный код, а также для работы с файлами, отображаемыми на память. Есть также способ организации общей памяти при помощи библиотек динамической компоновки DLL.
Поэтому если вам потребуется организовать передачу данных между приложениями, вы сможете всегда это сделать, например, через файл, отображаемый на память.
Извещения от файловой системы
Приложение Microsoft Windows NT может динамически следить за содержимым выбранного каталога или даже дерева каталогов, получая от файловой системы извещения при изменении содержимого каталога. Механизм таких извещений основан на использования объектов-событий и функций FindFirstChangeNotification, FindNextChangeNotification, FindCloseChangeNotification.
Прежде всего приложение, которое хочет получать извещения от файловой системы, должно вызвать функцию FindFirstChangeNotification, сообщив ей путь к каталогу и события, при возникновении которых необходимо присылать извещения. Прототип функции FindFirstChangeNotification представлен ниже:
HANDLE FindFirstChangeNotification(
LPCTSTR lpPathName, // адрес пути к каталогу
BOOL bWatchSubtree, // флаг управления каталогом или деревом
DWORD dwNotifyFilter); // флаги событий
Через параметр lpPathName вы должны передать функции адрес строки, содержащей путь к каталогу, за которым будет следить файловая система и при изменении в котором ваше приложение получит извещение.
Если при вызове функции флаг bWatchSubtree будет равен TRUE, будут отслеживаться изменения не только в каталоге, указанном в параметре lpPathName, но и во всех его подкаталогах. Если же значение этого флага будет равно FALSE, при изменении содержимого подкаталогов ваше приложение не получит извещение.
При помощи параметра dwNotifyFilter вы можете указать события, при возникновении которых в указанном каталоге или дереве каталогов ваше приложение получит извещение. Здесь вы можете указать комбинацию следующих констант:
Константа | Описание изменений | ||
FILE_NOTIFY_CHANGE_FILE_NAME | Изменение имен файлов, расположенных в указанном каталоге и его подкаталогах, создание и удаление файлов | ||
FILE_NOTIFY_CHANGE_DIR_NAME | Изменение имен каталогов, создание и удаление каталогов | ||
FILE_NOTIFY_CHANGE_ATTRIBUTES | Изменение атрибутов | ||
FILE_NOTIFY_CHANGE_SIZE | Изменение размеров файлов (после записи содержимого внутренних буферов на диск) | ||
FILE_NOTIFY_CHANGE_LAST_WRITE | Изменение времени записи для файлов (после записи содержимого внутренних буферов на диск) | ||
FILE_NOTIFY_CHANGE_SECURITY | Изменение дескриптора защиты |
Функция FindFirstChangeNotification создает объект-событие и возвращает его идентификатор. При ошибке возвращается значение INVALID_HANDLE_VALUE.
Полученный идентификатор может быть использован в операциях ожидания, выполняемых при помощи функций WaitForSingleObject и WaitForMultipleObjects. Когда произойдет одно из событий, указанных функции FindFirstChangeNotification в параметре dwNotifyFilter, этот объект-событие перейдет в отмеченное значение.
Таким образом, приложение может создать задачу, которая вызывает функцию FindFirstChangeNotification и затем выполняет ожидание для созданного ей объекта синхронизации. При возникновении события эта задача перейдет в активное состояние. При этом она, например, может выполнить сканирование каталога, указанного в параметре lpPathName, для обнаружения возникших изменений.
Заметим, что несмотря на свое название, функция FindFirstChangeNotification не обнаруживает изменения. Она только создает объект синхронизации, который переходит в отмеченное состояние при возникновении таких изменений.
После того как ваше приложение выполнило обнаружение и обработку изменений, оно должно вызвать функцию FindNextChangeNotification:
BOOL FindNextChangeNotification(HANDLE hChangeHandle);
В качестве параметра этой функции передается идентификатор объекта синхронизации, созданного функцией FindFirstChangeNotification. Функция FindNextChangeNotification переводит объект синхронизации в неотмеченное состояние таким образом, что его можно использовать повторно для обнаружения последующих изменений.
Если ваше приложение больше не собирается получать извещения от файловой системы, оно должно закрыть идентификатор соответствующего объекта синхронизации. Для этого следует вызвать функцию FindCloseChangeNotification, передав ей через единственный параметр идентфикатор, полученный от функции FindFirstChangeNotification:
BOOL FindCloseChangeNotification(HANDLE hChangeHandle);
Как работает семафор
В отличие от железнодорожного семафора, который может быть либо открыт, разрашая движение, либо закрыт, запрещая его, семаформы в операционной системе Microsoft Windows NT действуют более сложным образом.
Так же как и объект Mutex, семафор может находиться в отмеченном или неотмеченном состоянии. Приложение выполняет ожидание для семафора при помощи таких функций, как WaitForSingleObject или WaitForMultipleObject (точно также, как и для объекта Mutex). Если семафор находится в неотмеченном состоянии, задача, вызвавшая для него функцию WaitForSingleObject, находится в состоянии ожидания. Когда же состояние семафора становится отмеченным, работа задачи возобновляется. В такой логике работы для вас, однако, нет ничего нового.
В отличие от объекта Mutex, с каждым семафором связывается счетчик, начальное и максимальные значения которого задаются при создании семафора. Значение этого счетчика уменьшается, когда задача вызывает для семафора функцию WaitForSingleObject или WaitForMultipleObject, и увеличивается при вызове другой, специально предназначенной для этого функции.
Если значение счетчика семафора равно нулю, он находится в неотмеченном состоянии. Если же это значение больше нуля, семафор переходит в отмеченное состояние.
Пусть, например, приложение создало семафор, указав для него максимальное значение счетчика, равное трем. Пусть начальное значение этого счетчика также будет равно трем.
Если в этой ситуации несколько запускаемых по очереди задач будут выполнять с помощью функции WaitForSingleObject ожидание семафора, то первые три запущенные задачи будут работать, а все остальные перейдут в состояние ожидания. Это связано с тем, что первые три вызова функции WaitForSingleObject приведут к последовательному уменьшению значения счетчика семафора до нуля, в результате чего семафор переключится в неотмеченное состояние.
Задача, запущенная четвертой, вызовет функцию WaitForSingleObject для неотмеченного семафора, в результате чего она будет ждать. Точно также, задачи, запущенные после запуска четвертой задачи, будут выполнять ожидание для того же семафора.
Как долго продлится ожидание?
До тех пор, пока одна из первых трех задач не освободит семафор, увеличив его счетчик на единицу вызовом специальной функции. Сразу после этого будет запущена одна из задач, ожидающих наш семафор.
На рис. 4.4 мы показали последовательное изменение счетчика семафора (обозначенного символом N) при запуске первых трех задач. Так как счетчик больше нуля, семафор открыт, то есть находится в отмеченном состоянии и поэтому функция WaitForSingleObject не будет выполнять ожидание.
Рис. 4.4. Изменение значения счетчика семафора от трех до единицы
После запуска четвертой задачи, выполняющей ожидание семафора, значение счетчика уменьшится до нуля. При этом семафор будет закрыт, то есть перейдет в неотмеченное состояние (рис. 4.5).
Рис. 4.5. После запуска четвертой задачи семафор закрывается
Как завладеть объектом Mutex
Зная идентификатор объекта Mutex, полученный от функций CreateMutex или OpenMutex, задача может завладеть объектом при помощи функций ожидания событий, например, при помощи функций WaitForSingleObject или WaitForMultipleObjects.
Напомним, что функция WaitForSingleObject возвращает управление, как только идентификатор объекта, передаваемый ей в качестве параметра, перейдет в отмеченное состояние. Если объект Mutex не принадлежит ни одной задаче, его состояние будет отмеченным.
Когда вы вызываете функцию WaitForSingleObject для объекта Mutex, который никому не принадлежит, она сразу возвращает управление. При этом задача, вызвавшая функцию WaitForSingleObject, становится владельцем объекта Mutex. Если теперь другая задача вызовет функцию WaitForSingleObject для этого же объекта Mutex, то она будет переведена в сотояние ожидания до тех пор, пока первая задача не “откажется от своих прав” на данный объект Mutex. Освобождение объекта Mutex выполняется функцией ReleaseMutex, которую мы сейчас рассмотрим.
Захват объекта Mutex во владение по своему значению аналогичен входу в критическую секцию.
Классы приоритета процессов
При запуске процесса с помощью функции CreateProcess (которая будет рассмотрена позже), ему можно назначить один из четырех классов приоритета:
Класс приоритета | Уровень приоритета | ||
REALTIME_PRIORITY_CLASS | 24 - процессы реального времени | ||
HIGH_PRIORITY_CLASS | 13 - высокоприоритетные процессы | ||
NORMAL_PRIORITY_CLASS | 9 или 7 - обычные процессы | ||
IDLE_PRIORITY_CLASS | 4 - низкоприоритетные процессы |
Когда приоритет процесса не указывается, то по умолчанию он получает приоритет класса NORMAL_PRIORITY_CLASS. Если это приложение работает в фоновом режиме, операционная система снижает его уровень приоритета до 7, если же окно приложения выдвигается на передний план - увеличивает до 9. Таким образом уровень приоритета приложения, с которым в данный момент работает пользователь, автоматически увеличивается.
Класс IDLE_PRIORITY_CLASS используется для приложений, которые не должны тормозить работу других приложений. Это могут быть приложения, предназначенные для выполнения какой-либо фоновой работы, отображения постоянно меняющейся информации или приложения, выполняющий большой объем вычислений, сильно загружающих процессор. Например, если ваше приложение выполняет многочасовой расчет, имеет смысл назначить ему класс приоритета IDLE_PRIORITY_CLASS. При этом во время расчета пользователь сможет выполнять и другую работу, например, редактирование текста.
В тех случаях, когда приложение должно немедленно отзываться на действия пользователя, ему, возможно, следует назначить класс приоритета HIGH_PRIORITY_CLASS. Этот класс приоритета имеет, например, приложение Task Manager, с помощью которого пользователь может переключаться между запущенными приложениями а также завершать приложения. Не следует увлекаться созданием высокоприоритетных приложений, так как если в системе их будет запущенно много, то работа схемы, обеспечивающей оптимальный баланс производительности процессов, будет нарушена. В результате вы не получите желаемого эффекта.
Что же касается класса приоритета REALTIME_PRIORITY_CLASS, то он должен использоваться только системными процессами или драйверами. В противном случае будет блокирована работа клавиатуры и мыши, так как они обслуживаются с меньшим уровнем приоритета, чем приоритет классса REALTIME_PRIORITY_CLASS.
Копирование файла
Для копирования файла вы можете использовать функцию CopyFile:
BOOL CopyFile(
LPCTSTR lpExistingFileName, // адрес пути
// существующего файла
LPCTSTR lpNewFileName, // адрес пути копии файла
BOOL bFailIfExists); // флаг перезаписи файла
Через параметр lpExistingFileName вы должны передать путь к исходному файлу, кторый будет копироваться. Путь к файлу копии задается с помощью параметра lpNewFileName.
Параметр bFailIfExists определяет действия функции CopyFile, когда указанный в параметре lpNewFileName файл копии уже существует. Если значение параметра bFailIfExists равно TRUE, при обнаружении существующего файла копии функция CopyFile вернет признак ошибки (значение FALSE). Если же значение параметра bFailIfExists равно FALSE, существующий файл копии будет перезаписан.
Критические секции
Критические секции являются наиболее простым средством синхронизации задач для обеспечения последовательного доступа к ресурсам. Они работают быстро, не снижая заметно производительность системы, но обладают одним существенным недостатком - их можно использовать только для синхронизации задач в рамках одного процесса. В отличие от критических секций объекты Mutex, которые мы рассмотрим немного позже, допускают синхронизацию задач, созданных разными процессами.
Критическая секция создается как структура типа CRITICAL_SECTION:
CRITICAL_SECTION csWindowPaint;
Обычно эта структура располагается в области глобальных переменных, доступной всем запущенным задачам процесса. Так как каждый процесс работает в своем собственном адресном пространстве, вы не сможете передать адрес критической секции из одного процесса в другой. Именно поэтому критические секции нельзя использовать для синхронизации задач, созданных разными процессами.
В файле winbase.h (который включается автоматически при включении файла windows.h) структура CRITICAL_SECTION и указатели на нее определены следующим образом:
typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;
typedef PRTL_CRITICAL_SECTION PCRITICAL_SECTION;
typedef PRTL_CRITICAL_SECTION LPCRITICAL_SECTION;
Определение недокументированной структуры RTL_CRITICAL_SECTION вы можете найти в файле winnt.h:
typedef struct _RTL_CRITICAL_SECTION
{
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
LONG LockCount; // счетчик блокировок
LONG RecursionCount; // счетчик рекурсий
HANDLE OwningThread; // идентификатор задачи, владеющей
// секцией
HANDLE LockSemaphore; // идентификатор семафора
DWORD Reserved; // зарезервировано
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
Заметим, однако, что нет никакой необходимости изменять поля этой структуры вручную, так как для этого есть специальные функции. Более того, только эти функции и можно использовать для работы с критическими секциями. В документации SDK также отмечено, что структуру типа CRITICAL_SECTION нельзя перемещать или копировать.
Легко ли ждать
Начнем с простого: расскажем о том, как задача может дождаться завершения другой задачи либо завершения процесса.
Если вам нужно сделать так, чтобы одна задача дожидалась завершения другой задачи, работающей в рамках того же процесса, вы можете впасть в соблазн выполнить такое ожидание с помощью глобальной переменной, содержимое которой изменяется при завершении работы второй задачи. Первая задача могла бы при этом опрашивать содержимое этой глобальной переменной в цикле, дожидаясь момента, когда оно изменится.
Очевидный недостаток такого подхода заключается в том, что ожидающая задача получает кванты процессорного времени. При этом снижается общая производительность системы, поэтому необходимо найти другой способ выполнения ожидания. Было бы хорошо организовать ожидание таким образом, чтобы планировщик задач не выделял процессорное время тем задачам, которые чего-то ждут.
Литература
1. Фролов А.В., Фролов Г.В. Библиотека системного программиста. М.: ДИАЛОГ-МИФИ
Т.11 - 13. Операционная система Microsoft Windows 3.1 для программиста, 1994
Т.14. Графический интерфейс GDI в Microsoft Windows, 1994
Т.15. Мультимедиа для Windows, 1994
Т.17. Операционная система Microsoft Windows 3.1 для программиста. Дополнительные главы, 1995
Т.20. Операционная система IBM OS/2 Warp, 1995
Т.22. Операционная система Windows 95 для программиста, 1996
2. J. Richter. Advanced Windows NT, Microsoft Press, One Microsoft Way, Redmond, Washington, 1994
3. Norton D. Writing Windows Device Drivers. Addison-Wesley Publishing Company, 1992
4. Petzold C. Programming Windows 3.1. Microsoft Press. One Microsoft Way. Redmont, Washington, 1992
5. Rector B. Developing Windows 3.1 Applications with Microsoft C/C++. SAMS, 1992
6. Дженнингс Р. Windows-95 в подлиннике. “BHV-Санкт-Петербург”, С.-П.:, 1995
7. M. A. Jacobs. Не все приложения Windows 95 работают под управлением Windows NT //COMPUTER WEEK-MOSCOW, N40, с. 23
8. Брайан Просис. Нужно ли отказываться от FAT //PC MAGAZINE/RUSSIAN EDITION, N8,1995, с. 154
9. Джефф Просис. Готова ли VFAT выйти на первые роли? //PC MAGAZINE/RUSSIAN EDITION, N9,1995, с. 170
10. Чарльз Петцольд. Точное отображение расширенного метафайла //PC MAGAZINE/RUSSIAN EDITION, N7,1995, с. 150
11. Джефф Просис. Укрощение строптивых окон //PC MAGAZINE/RUSSIAN EDITION, N6,1996, с. 170
12. Брайан Проффит. Высокопроизводительная файловая система HPFS //PC MAGAZINE/RUSSIAN EDITION, N10,1995, с. 184
LpCommandLine
Параметры lpApplicationName и lpCommandLine используются для указания имя программного файла и параметров запуска процесса. Вы можете использовать один или оба этих параметра.
Через параметр lpApplicationName вы можете передать указатель на строку, закрытую двоичным нулем и содержащую полный либо частичный путь к программному файлу. При этом параметр lpCommandLine может иметь значение NULL либо указывать на строку параметров запуска приложения. Если это потребуется, приложение может извлечь адрес строки параметров через параметы функции WinMain, либо при помощи функции GetCommandLine, не имеющей параметров. Последняя возвращает адрес искомой строки.
Вы можете также передать через параметр lpApplicationName значение NULL, указав адрес строки пути к программному файлу через параметр lpCommandLine. В этом случае можно передать параметры процессу, добавив их в этой строке к пути файла через пробел. Таким образом, в параметре lpCommandLine можно указать адрес командной строки для запуска процесса.
LpCurrentDirectory
Параметр lpCurrentDirectory задает путь к символьной строке, закрытой двоичным нулем, содержащей путь к каталогу, который будет использован как рабочий при запуске процесса. Если указать этот параметр как NULL, в качестве рабочего для нового процесса будет использован рабочий каталог родительского процесса.
LpDesktop
Поле lpDesktop используется для выбора рабочего стола и рабочей станции, где будет запущен процесс. Если вы собираетесь использовать текущий рабочий стол и текущую рабочую станцию, укажите в этом поле значение NULL.
LpEnvironment
Наряду с регистрационной базой данных, в которой приложения могут хранить необходимые им для выполнения программы, операционная система Microsoft Windows NT сохранила такой атавизм MS-DOS, как среда выполнения программ (очевидно, сохранила для обратной совместимости с этой операционной системой). В MS-DOS переменные среды устанавливались при помощи команд в файле autoexec.bat. Операционная система Microsoft Windows NT версии 3.51 позволяет это сделать через приложение Control Panel.
Если дочерний процесс должен использовать родительский блок среды выполнения, через параметр lpEnvironment необходимо передать значение NULL.
Родительский процесс может также подготовить новый блок среды, состоящий из расположенных друг за другом символьных строк, закрытых двоичным нулем. Последняя из этих строк должна быть закрыта двумя двоичными нулями.
При необходимости процесс может получить адрес блока среды при помощи функции GetEnvironmentStrings, не имеющей параметров. Функция GetEnvironmentVariable позовляет узнать значение отдельных переменных блока среды. Описание этой функции вы найдете в SDK.
LpProcessInformation
Перед вызовом функции CreateProcess вы должны передать ей через параметр lpProcessInformation адрес структуры типа PROCESS_INFORMATION, в которую будут записаны идентификаторы и системные номера созданного процесса и его главной задачи:
typedef struct _PROCESS_INFORMATION
{
HANDLE hProcess; // идентификатор процесса
HANDLE hThread; // идентификатор главной задачи процесса
DWORD dwProcessId; // системный номер процесса
DWORD dwThreadId; // системный номер главной задачи
// процесса
} PROCESS_INFORMATION;
Пользуясь идентификаторами, полученными из полей hProcess и hThread, родительский процесс может управлять порожденным процессом и его главной задачей, например, изменяя класс приоритета процесса или относительный приоритет его главной задачи.
Важно отметить, что после использования родительский процесс обязан закрыть полученные идентификаторы порожденного процесса и задачи при помощи функции CloseHandle.
LpStartupInfo
Через параметр lpStartupInfo вы должны передать функции CreateProcess указатель на структуру типа STARTUPINFO, определяющую внешний вид окна, создаваемого для процесса:
typedef struct _STARTUPINFO
{
DWORD cb; // размер структуры в байтах
LPTSTR lpReserved; // зарезервировано
LPTSTR lpDesktop; // рабочий стол и станция для процесса
LPTSTR lpTitle; // заголовок окна консольного процесса
DWORD dwX; // координата угла окна в пикселах
DWORD dwY; // координата угла окна в пикселах
DWORD dwXSize; // ширина окна в пикселах
DWORD dwYSize; // высота окна в пикселах
DWORD dwXCountChars; // ширина консольного окна
DWORD dwYCountChars; // высота консольного окна
DWORD dwFillAttribute; // атрибуты текста консольного окна
DWORD dwFlags; // заполненные поля структуры
WORD wShowWindow; // размеры окна по умолчанию
WORD cbReserved2; // зарезервировано
LPBYTE lpReserved2; // зарезервировано
HANDLE hStdInput; // консольный буфер ввода
HANDLE hStdOutput; // консольный буфер вывода
HANDLE hStdError; // консольный буфер вывода сообщений
// об ошибках
} STARTUPINFO, *LPSTARTUPINFO;
Несмотря на внушительный размер этой структуры, ее заполнение не вызовет у вас особых трудностей, так как большинство полей можно не использовать.
LpThreadAttributes
Параметры lpProcessAttributes и lpThreadAttributes указывают атрибуты защиты, соответвтвенно, процесса и его главной задачи, которая получает управление при запуске процесса. В наших примерах мы будем указывать оба этих параметра как NULL, используя значения атрибутов защиты, принятые по умолчанию.
LpTitle
Для консольного процесса (и только для него) вы можете указать заголовок окна в поле lpTitle. Если же в этом поле задать значение NULL, для заголовка будет использовано имя файла.
Набор флагов файла
Так же как и MS-DOS, операционная система Microsoft Windows NT присваивает файлам при их создании различные флаги (атрибуты). Вы можете определить атрибуты файла при помощи функции GetFileAttributes:
DWORD GetFileAttributes(LPCTSTR lpFileName);
В качестве единственного параметра этой функции необходимо передать полный или частичный путь к файлу. Функция вернет слово, значение которого является логической комбинацией следующих атрибутов:
Атрибут | Описание | ||
FILE_ATTRIBUTE_ARCHIVE | Файл был архивирован (выгружен) | ||
FILE_ATTRIBUTE_COMPRESSED | Файл, имеющий этот атрибут, динамически сжимается при записи и восстанавливается при чтении | ||
FILE_ATTRIBUTE_NORMAL | Остальные перечисленные в этом списка атрибуты не установлены | ||
FILE_ATTRIBUTE_HIDDEN | Скрытый файл | ||
FILE_ATTRIBUTE_READONLY | Файл можно только читать | ||
FILE_ATTRIBUTE_SYSTEM | Файл является частью операционной системы |
Для установки новых атрибутов вы можете воспользоваться функцией SetFileAttributes:
BOOL SetFileAttributes(
LPCTSTR lpFileName, // адрес строки пути к файлу
DWORD dwFileAttributes); // адрес слова с новыми
// атрибутами
Если вам нужно изменить только один из битов слова атрибутов, необходимо вначале получить старое слово атрибутов при помощи функции GetFileAttributes, а затем, изменив в нем только нужные биты, установить новое значение слова атрибутов функцией SetFileAttributes.
Немного истории
Для того чтобы вам было легче разобраться с системой управления памятью Microsoft Windows NT и ощутить, насколько она совершенна, сделаем краткий обзор принципов управления памятью, положенных в основу операционных систем MS-DOS и Microsoft Windows версии 3.1.
Несегментированная модель памяти FLAT
Операционная система Microsoft Windows NT использует все возможности защищенного режима процессора, в частности, переключение задач и страничную адресацию. Схема преобразования адреса похожа на ту, что была применена в Microsoft Windows версии 3.1, однако есть много отличий.
Наиболее значительное - полный отказ от использования сегментированной модели памяти в 32-разрядных приложениях, к которой вы привыкли, создавая программы для MS-DOS и Microsoft Windows версии 3.1. В самом деле, используя 32-разрядное смещение, приложение может работать с памятью объемом 4 Гбайт без использования сегментных регистров процессора.
Поэтому, хотя логический адрес по-прежнему состоит из компонент селектора и смещения, приложения Microsoft Windows NT при обращении к памяти указывают только компоненту смещения. Сегментные регистры процессора, хранящие селекторы, заполняются операционной системой и приложение не должно их изменять.
Несегментированная модель памяти называется сплошной моделью памяти FLAT. При программировании в этой модели памяти вам не потребуются ключевые слова near и far, так как все объекты, расположенные в памяти, адресуются с использованием только одного смещения. В этом модель памяти FLAT напоминает модель памяти TINY, с тем исключением, однако, что в последней размер адресуемой памяти не может превышать 64 Кбайт (из-за того что в модели TINY используется один сегмент и 16-разрядное смещение).
Таким образом, в распоряжении каждого приложения (или точнее говоря, каждого процесса) Microsoft Windows NT предоставляется линейное адресное пространство размером 4 Гбайта. Область размером 2 Гбайта с диапазоном адресов от 00000000h до 7FFFFFFFh предоставлена в распоряжение приложению, другие же 2 Гбайта адресуемого пространства зарезервированы для использования операционной системой (рис. 1.9).
Рис. 1.9. Адресное пространство приложения Microsoft Windows NT
Как это показано на рис. 1.9, небольшая часть адресного пространства в пределах первых 2 Гбайт также зарезервирована для операционной системы. Это области размером 64 Кбайта, расположенные в самом начале адресного пространства, а также в конце первых 2 Гбайт, и предназначенные для обнаружения попыток использования неправильных указателей. Таким образом, приложения не могут адресовать память в диапазонах адресов 00000000h - 0000FFFFh и 7FFF0000h - 7FFFFFFFh.
Объекты Mutex
Если необходимо обеспечить последовательное использование ресурсов задачами, созданными в рамках разных процессов, вместо критических секций необходимо использовать объекты синхронизации Mutex. Свое название они получили от выражения “mutually exclusive”, что означает “взаимно исключающий”.
Также как и объект-событие, объект Mutex может находится в отмеченном или неотмеченном состоянии. Когда какая-либо задача, принадлежащая любому процессу, становится владельцем объекта Mutex, последний переключается в неотмеченное состояние. Если же задача “отказывается” от владения объектом Mutex, его состояние становится отмеченным.
Организация последовательного доступа к ресурсам с использованием объектов Mutex возможна потому, что в каждый момент только одна задача может владеть этим объектом. Все остальные задачи для того чтобы завладеть объектом, который уже захвачен, должны ждать, например, с помощью уже известных вам функции WaitForSingleObject.
Для того чтобы объект Mutex был доступен задачам, принадлежащим различным процессам, при создании вы должны присвоить ему имя (аналогично тому, как вы это делали для объекта-события).
Обработка сообщений от меню Priority
Меню Priority позволяет изменить класс приоритета процесса, в рамках которого работает приложение.
Процессам в нашей книге посвящена отдельная глава, однако изменение класса приоритета - достаточно простая операция, которая выполняется с помощью функции SetPriorityClass:
SetPriorityClass(GetCurrentProcess(),
REALTIME_PRIORITY_CLASS);
В качестве первого параметра этой функции необходимо передать идентификатор процесса, класс приоритета которого будет изменяться. Мы получаем идентификатор текущего процесса с помощью функции GetCurrentProcess.
Через второй параметр функции SetPriorityClass передается новое значение класса приоритета.
Обработка сообщений от меню Semaphore
Если из меню Semaphore выбрать строку Increment, содержимое счетчика семафора будет увеличено на единицу:
case ID_SEMAPHORE_INCREMENT:
{
ReleaseSemaphore(hSemaphore, 1, NULL);
break;
}
Для этого используется функция ReleaseSemaphore.
Обработка сообщений от меню Window
Меню Window предназначено для управления дочерними MDI-окнами. Выбирая строки этого меню, пользователь может упорядочить расположение окон одним из двух способов, упорядочить расположение пиктограмм минимизированных окон, а также закрыть все дочерние MDI-окна. Все эти операции выполняются посылкой соответствующих сообщений окну Client Window. Для экономии места мы не будем останавливаться на подробном описании соответствующих фрагментов кода. При необходимости обратитесь к 17 тому “Библиотеки системного программиста”.
Обработка сообщения WM_CLOSE
Это сообщение поступает в функцию дочернего окна при уничтожении последнего (например, если пользователь сделал двойной щелчок левой клавишей мыши по пиктограмме системного меню дочернего MDI-окна).
Задача обработчика сообщения WM_CLOSE заключается в сбросе признака активности задачи, в результате чего задача завершает свою работу:
lpMyWndTag = (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd,
GWL_USERDATA);
lpMyWndTag->fActive = 0;
Обработка сообщения WM_COMMAND
Сообщение WM_COMMAND поступает в функцию дочернего MDI-окна, когда пользователь выбирает строки плавающего меню.
Если пользователь выбирает из этого меню, например, строку Suspend, выполняется приостановка работы задачи, запущенной для данного дочернего MDI-окна. Приостановка выполняется при помощи функции SuspendThread. Идентификатор задачи, необходимый для нее, извлекается из поля hThread структуры типа CHILD_WINDOW_TAG:
lpMyWndTag = LPCHILD_WINDOW_TAG)GetWindowLong(hwnd,
GWL_USERDATA);
EnterCriticalSection(&(lpMyWndTag->csChildWindowPaint));
SuspendThread(lpMyWndTag->hThread);
LeaveCriticalSection(&(lpMyWndTag->csChildWindowPaint));
Для возобновления выполнения приостановленной задачи мы использовали функцию ResumeThread:
ResumeThread(lpMyWndTag->hThread);
Заметим, что перед выполнением приостановки задачи мы входим в критическую секцию. Это необходимо для того, чтобы избежать полной блокировки главной задачи процесса в момент, когда блокировка рисующей задачи выполняется после входа одной из задач в критическую секцию. В самом деле, если этой произойдет, главная задача не сможет войти в критическую секцию, так как она уже занята другой задачей. Если при этом задача, вошедшая в критическую секцию, оказалась заблокирована до своего выхода из критической секции, главная задача так и останется в состоянии ожидания. При этом пользователь не сможет, например, работать с меню приложения.
Относительный приоритет задачи изменяется фукнцией SetThreadPriority, как это показано ниже:
SetThreadPriority(lpMyWndTag->hThread,
THREAD_PRIORITY_LOWEST);
Если выбрать из плавающего меню строку Get priority, с помощью функции GetThreadPriority определяется текущий относительный приоритет задачи, запущенной для данного дочернего MDI-окна. Значение этого приоритета отображается затем на экране при помощи простейшей диалоговой панели, создаваемой функцией MessageBox.
При выборе из плавающего меню строки Kill Thread задача будет принудительно уничтожена функцией TerminateThread:
TerminateThread(lpMyWndTag->hThread, 5);
В качестве кода завершения здесь передается произвольно выбранное нами значение 5.
С помощью плавающего меню вы можете удалить дочернее MDI-окно, завершив работу соответствующей задачи. Для этого окну Client Window посылается сообщение WM_MDIDESTROY:
SendMessage(hwndClient, WM_MDIDESTROY, (WPARAM)hwnd, 0);
Все остальное делает обработчик сообщения WM_CLOSE, который получит управление при удалении дочернего MDI-окна. А именно, этот обработчик сбрасывает признак активности задачи, в результате чего задача завершает свою работу.
Обработка сообщения WM_PAINT
Обработчик сообщения WM_PAINT получает управление, когда возникает необходимость перерисовать внутреннюю область дочернего MDI-окна. Однако заметим, что в то же самое время во внутренней области дочернего окна может рисовать функция задачи, запущенная для этого окна. В результате возникает необходимость выполнить синхронизацию главной задачи приложения и задачи дочернего окна.
Так как задачи, запущенные для дочерних окон, работают независимо друг от друга и от главной задачи приложения, мы будем выполнять синхронизацию каждой задачи при помощи отдельной критической секции. Эта критическая секция располагается в структуре типа CHILD_WINDOW_TAG и ее инициализация была выполнена перед созданием соответствующей задачи.
Для получения адреса структуры, который был записан в память дочернего окна, мы вызываем функцию GetWindowLong:
lpMyWndTag = (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd, GWL_USERDATA);
После этого выполняется вход в критическую секцию, получение контекста отображения, рисование во внутренней области дочернего MDI-окна строки Child Window, освобождение контекста отображения и выход из критической секции:
EnterCriticalSection(&(lpMyWndTag->csChildWindowPaint));
hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &rc);
DrawText(hdc, "Child Window", -1, &rc,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
LeaveCriticalSection(&(lpMyWndTag->csChildWindowPaint));
Обработка сообщения WM_RBUTTONDOWN
Для управления задачами, запущенными в дочерних MDI-окнах, в нашем приложении используется плавающее меню. Это меню создается когда пользователь делает щелчок правой клавишей мыши во внутренней области дочернего MDI-окна.
Плавающее меню определено в ресурсах приложения с идентификатором IDR_POPUPMENU. Оно отображается на экране с помощью функции TrackPopupMenu. Соответствующая техника была нами описана в главе “Меню” 13 тома “Библиотеки системного программиста”.
При разработке операционной системы Microsoft
При разработке операционной системы Microsoft Windows 95 была создана новая модификация файловой системы, которая получила название VFAT. Ее также называют файловой системой с таблицей размещения файлов защищенного режима - Protected mode FAT.
Несмотря на свое название, в файловой системе VFAT используется самый обычный формат таблицы размещения файлов. Этого, однако, не скажешь о формате дескрипторов файлов, расположенных в каталогах. В этот формат были внесены добавления, позволяющие пользователям указывать имена файлов и каталогов размером до 260 символов.
Каким образом это было достигнуто?
Для каждого файла или каталога, имеющего длинное имя, было задействовано несколько дополнительных дескрипторов, расположенных рядом. При этом имя (наряду с другими дополнительными атрибутами) было разбито на несколько частей и хранилось в нескольких дополнительных дескрипторах.
Однако самое интересное, что при создании файловой системы VFAT была обеспечена совместимость со старыми программами MS-DOS, которые могли работать только с короткими именами “в стандарте 8.3”. Для этого во всех дополнительных дескрипторах были установлены атрибуты “скрытый”, “системный”, “метка тома”, благодаря чему программы MS-DOS пропускали такие дескрипторы, не обращая на них внимания. В то же время для каждого файла или каталога с длинным именем файловая система VFAT создавала дескриптор специального вида, содержащий альтернативное (алиасное) имя. Альтернативное имя состоит из первых шести символов длинного имени, из которого убраны пробелы, символа “тильда” (~) и числа. Например, для имени The Mircosoft Network создается альтернативное имя THEMIC~1.
В результате пользователи могли работать с длинными именами как в приложениях Microsoft Windows 95, так и в программах MS-DOS (хотя, конечно, альтернативное имя в ряде случаев может мало напоминать исходное длинное имя).
Разработчики операционной системы Microsoft Windows 95 добивались максимальной совместимости с приложениями Microsoft Windows 3.1 и программами MS-DOS. Поэтому для работы с дисковыми устройствами (особенно экзотическими) в ряде случаев можно использовать драйверы реального режима, загружаемые с помощью файла config.sys. Это очень удобно, так как вы можете пользоваться устройством, для которого в составе Microsoft Windows 95 пока нет специального драйвера. Однако при использовании драйверов реального режима в процессе обращения к устройству происходит переключение процессора из защищенного режима в режим виртуального процессора 8086, что снижает производительность системы.
Операционная система Microsoft Windows for Workgroups
Известная своими средствами работы в сети операционная система Microsoft Windows for Workgroups версии 3.11 имела одно усовершенствование, заметно увеличивающее скорость работы с файлами. Эта операционная система позволяла устанавливать режим 32-разрядного доступа не только к диску, но и к файлам, полностью исключая необходимость переключения процессора из защищенного режима работы в режим виртуального процессора 8086. И хотя по-прежнему 32-разрядный драйвер диска, поставляющийся в составе Microsoft Windows for Workgroups версии 3.11, мог работать далеко не со всеми дисковыми контроллерами, многие фирмы, изготавляющие такие контроллеры, продавали 32-разрядные драйверы, позволяющие воспользоваться преимуществами доступа к диску и файлам в 32-разрядном режиме.
Однако доступ к дискам и файлам в защищенном режиме работы процессора - это все, что изменилось в файловой системе. Операционная система Microsoft Windows for Workgroups, так же как и предыдущие версии Microsoft Windows, работает с файловой системой FAT.
Операционная система Microsoft Windows версии
Операционная система Microsoft Windows версии 3.1 и более ранних версий не внесла ничего нового в теорию и практику файловых систем, ограничившись использованием все той же системы FAT. Более того, для доступа к файлам процессор переключался из защищенного в реальный режим или в режим виртуального процессора 8086, а затем вызывалось прерывание MS-DOS с номером 21h.
Постоянные переключения режимов работы процессора приводили к снижению производительности. И хотя для подавляющего большинства дисковых контроллеров IDE можно было включить режим 32-разрядного доступа к диску, исключающий вызов модулей BIOS (работающих только в реальном режиме), заметного влияния на быстродействие системы этот режим не оказывал. К тому же, для большинства контроллеров SCSI 32-разрядный доступ вообще нельзя было использовать.
В программном интерфейсе операционной системы Microsoft Windows версии 3.1 были предусмотрены несколько функций для работы с файлами. Мы описали их в 113 томе “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1 для программиста. Часть третья”. Это такие функции, как OpenFile, _lopen, _lclose, _lcreat, _lread, _lwrite, _hread, _hwrite, _llseek.
Все перечисленные выше функции, а также функции стандартной библиотеки времени выполнения танслятора Microsoft Visual C++ можно использовать в приложениях Microsoft Windows NT. Это сделано для обеспечения совместимости на уровне исходных текстов. Однако лучше работать с новыми функциями программного интерфейса Microsoft Windows NT, обладающими намного более широкими возможностями.
Операционная система MS-DOS и файловая система FAT
Все вы хорошо знаете недостатки файловой системы FAT, разработанной на заре развития операционной системы MS-DOS. Однако в те времена жесткие диски персональных компьютеров имели объем 10 - 40 Мбайт, и файловая система FAT в целом неплохо подходила для работы с такими дисками и дискетами.
Один из недостатков файловой системы FAT, наиболее очевидный для пользователей, заключается в жестких ограничениях на имена файлов и каталогов. Имя должно состоять не более чем из 8 символов, плюс еще три символа расширения имени. Так как расширение имени всегда используется для обозначения типа документа (например, txt - текстовый файл, doc - файл документа Microsoft Word), пользователь был вынужден изобретать восьмисимвольные имена, отражающие содержимое файла. Если пользователь работает с десятками или сотнями документов (что совсем не редкость), ему нужно иметь незаурядную фантазию, чтобы суметь придумать для всех документов осмысленные имена.
Другой, менее очевидный недостаток, заключается в том что информация о каждом файле хранится в нескольких удаленных друг от друга местах диска. Для того чтобы прочесть файл, операционная система должна сначала найти его имя и номер первого распределенного файлу кластера в каталоге, затем ей следует обратиться к таблице размещения файлов FAT, чтобы получить список кластеров, распределенных файлу, и, наконец, прочитать кластеры, которые находятся совсем в другом месте диска.
Если с диском работает только одна программа, накладные расходы на перемещения блока магнитных головок между этими областями диска, возможно, не приведут к заметному снижению производительности (особенно при использования кэширования диска). Однако в мультизадачной среде, когда несколько программ пытаются получить доступ одновременно к нескольким разным файлам, указанные накладные расходы будут заметнее.
У файловой системы FAT есть еще одна большая проблема - увеличение фрагментации диска и файлов при интенсивной работе с файлами. Фрагментация приводит к тому, что файл как бы размазывается по диску. Для чтения такого файла нужно много времени, так как перемещение головок выполняется относительно медленно. Поэтому пользователям операционной системы очень хорошо знакомы утилиты дефрагментации, такие как Microsoft Defrag и Norton Speedisk.
Файловой системе FAT и средствам работы с диском на разных уровнях в среде операционной системы MS-DOS мы посвятили 19 том “Библиотеки системного программиста”, который называется “MS-DOS для программиста. Часть вторая”. Вы найдете в этой книге подробное изложение принципов построения файловой системы FAT.
Описание функций
Приведем описание функций, определенных в нашем приложении.