Просмотр содержимого каталога
Для просмотра содержимого каталогов в программном интерфейсе Microsoft Windows NT предусмотрены функции FindFirstFile, FindNextFile и FindClose. Просмотр с помощью этих функций выполняется в цикле.
Перед началом цикла вызовается функция FindFirstFile:
HANDLE FindFirstFile(
LPCTSTR lpFileName, // адрес пути для поиска
LPWIN32_FIND_DATA lpFindFileData); // адрес структуры
// LPWIN32_FIND_DATA, куда будет записана
// информация о файлах
Через параметр lpFileName вы должны передать функции адрес строки, содержащей путь к каталогу и шаблон для поиска. В шаблоне можно использовать символы “?” и “*”.
Через параметр lpFindFileData следует передать адрес структуры типа WIN32_FIND_DATA, в которую будет записана информация о найденных файлах. Эта структура определена следующим образом:
typedef struct _WIN32_FIND_DATA
{
DWORD dwFileAttributes; // атрибуты файла
FILETIME ftCreationTime; // время создания файла
FILETIME ftLastAccessTime; // время доступа
FILETIME ftLastWriteTime; // время записи
DWORD nFileSizeHigh; // размер файла (старшее слово)
DWORD nFileSizeLow; // размер файла (младшее слово)
DWORD dwReserved0; // зарезервировано
DWORD dwReserved1; // зарезервировано
TCHAR cFileName[MAX_PATH]; // имя файла
TCHAR cAlternateFileName[14]; // альтернативное имя файла
} WIN32_FIND_DATA;
Если поиск завершился успешно, функция FindFirstFile возвращает идентификатор поиска, который будет затем использован в цикле при вызове функции FindNextFile. При ошибке возвращается значение INVALID_HANDLE_VALUE.
Заметим, что поля cFileName и cAlternateFileName структуры WIN32_FIND_DATA содержат, соответственно, длинное имя файла и короткое, альтернативное имя файла “в формате 8.3”.
После вызова функции FindFirstFile вы должны выполнять в цикле вызов функции FindNextFile:
BOOL FindNextFile(
HANDLE hFindFile, // идентификатор поиска
LPWIN32_FIND_DATA lpFindFileData); // адрес структуры
// WIN32_FIND_DATA
Через параметр hFindFile этой функции следует передать идентификатор поиска, полученный от функции FindFirstFile. Что же касается параметра lpFindFileData, то через него вы должны передать адрес той же самой структуры типа WIN32_FIND_DATA, что была использована при вызове функции FindFirstFile.
Если функция FindNextFile завершилась успешно, она возвращает значение TRUE. При ошибке возвращается значение FALSE. Код ошибки вы можете получить от функции GetLastError. В том случае, когда были просмотрены все файлы в каталоге, эта функция возвращает значение ERROR_NO_MORE_FILES. Вы должны использовать такую ситуацию для завершения цикла просмотра содержимого каталога.
После завершения цикла просмотра необходимо закрыть идентификатор поиска, вызвав для этого функцию FindClose:
BOOL FindClose(HANDLE hFindFile);
В качестве единственного параметра этой функции передается идентификатор поиска, полученный от функции FindFirstFile.
Пулы памяти в Microsoft Windows NT
Как мы уже говорили, приложения Microsoft Windows версии 3.1 могли заказывать память из двух областей или двух пулов - из глобального пула, доступного всем приложениям, и локального, создаваемого для каждого приложения.
Адресные пространства приложений Microsoft Windows NT разделены, поэтому в этой операционной системе нет глобальных пулов памяти. Вместо этого каждому приложению по умолчанию выделяется один стандартный пул памяти в его адресном пространстве. При необходимости приложение может создавать (опять же в своем адресном пространстве) произвольное количество так называемых динамических пулов памяти.
По умолчанию для стандартного пула резервируется 1 Мбайт сплошного адресного пространства, причем 4 Кбайта памяти выделяются приложению для непосредственного использования. Если приложению требуется больше памяти, в адресном пространстве резервируется еще один или несколько Мбайт памяти.
Если ваше приложение работает с большими объемами данных, для загрузки этих данных в непрерывное адресное пространство можно увеличить размер стандартного пула двумя способами.
Во-первых, параметры стандартного пула можно задать в параметре /HEAP редактора связи:
/HEAP: 0x2000000, 0x10000
В данном случае для стандартного пула будет зарезервировано 2 Мбайта памяти, причем сразу после загрузки приложения 10 Кбайт памяти будет получено в пользование.
Во-вторых, параметры стандартного пула можно указать в файле определения модуля (который является необязательным). Например, так:
HEAPSIZE 0x2000000 0x10000
Однако для резервирования очень больших адресных пространств памяти лучше создавать динамические пулы. Создание динамического пула не ведет к излишней загрузке физической оперативной памяти, так как пока вы не получаете из этого пула память, соответствующее адресное пространство является зарезервированным. Как мы уже говорили, резервирование адресного пространства не вызывает выделения памяти и изменения файлов страниц. Так что резервируйте сколько угодно, но в только в пределах 2 Гбайт.
существует глобальная область памяти
Напомним, что в Microsoft Windows версии 3. 1 существует глобальная область памяти (глобальный пул Global Heap), доступная для всех приложений. Кроме того, для каждого приложения выделяется “в личное пользование” локальный пул Local Heap размером 64 Кбайт.
Если одно приложение получает область памяти из глобального пула, оно может передать адрес этой области другому приложению и то сможет им воспользоваться (так как все приложения работают в одном адресном пространстве). В операционной системе Microsoft Windows NT этот прием работать не будет, так как каждое приложение работает в отдельном адресном пространстве с использованием отдельной локальной таблицы дескрипторов.
Для получения памяти из глобального и локального пула в программном интерфейсе Microsoft Windows версии 3.1 были предусмотрены отдельные функции с именами GlobalAlloc и LocalAlloc. Выделенную этими функциями область памяти перед использованием было необходимо зафиксировать, вызвав, соответственно, функции GlobalLock и LocalLock. Для выделяемых областей памяти автоматически создавались дескрипторы в локальной таблице дескрипторов.
При необходимости приложение могло изменять содержимое глобальной или локальной таблицы дескрипторов, для чего в программном интерфейсе Microsoft Windows версии 3.1 были предусмотрены соответствующие функции. Эти функции, описанные нами в 13 томе “Библиотеки системного программиста”, давали обычным приложениям Windows почти ничем не ограниченные права доступа к памяти. Поэтому можно утверждать, что несмотря на использование защищенного режима работы процессора, по надежности Microsoft Windows версии 3.1 недалеко ушла от операционной системы MS-DOS.
Работа с динамическим пулом памяти
Вначале наше приложение создает динамический пул памяти, вызывая для этого функцию HeapCreate. Для пула резервируется 2 Кбайта памяти, причем для непосредственного использования выделяется только 1 Кбайт.
При возникновении ошибки ее код определяется с помощью функции GetLastError и отображается в консольном окне хорошо знакомой вам из MS-DOS функцией fprintf. Затем работа приложения завершается.
Заметим, что многие (но не все) функции программного интерфейса Microsoft Windows NT в случае возникновения ошибки перед возвращением управления устанавливают код ошибки, вызывая для этого функцию SetLastError. При необходимости приложение может извлечь этот код сразу после вызова функции, как это показано в нашем приложении.
Далее приложение пытается получить блок памяти размером 0x1500 байт, вызывая функцию HeapAlloc:
__try
{
lpszBuff = (char*)HeapAlloc(hHeap,
HEAP_GENERATE_EXCEPTIONS, 0x1500);
}
Так как во втором параметре мы передали этой функции значение HEAP_GENERATE_EXCEPTIONS, в случае ошибки возникнет исключение. Поэтому вызов функции HeapAlloc выполняется с обработкой исключений. Соответствующий обработчик получает код исключения при помощи функции GetExceptionCode и отображает его в консольном окне.
В нашем приложении мы пытаемся получить больше памяти, чем доступно, поэтому исключение действительно произойдет.
На следующем шаге, невзирая на исключение, наше приложение пытается записать в блок памяти, указатель на который находится в переменной lpszBuff, текстовую строку:
__try
{
strcpy(lpszBuff, "Строка для проверки");
}
Так как при получении блока памяти произошло исключение, в указателе lpszBuff находится неправильный адрес. Это, в свою очередь, приведет к возникновению исключения при попытке записи строки. Поэтому на рис. 1.19 в верхней части консольного окна находятся два сообщения об исключениях.
После обработки второго исключения наше приложение выполняет вторую попытку получения блока памяти, но уже меньшего размера. Эта попытка закончится удачно, после чего приложение выведет строку в консольное окно, пользуясь другой хорошо знакомой вам функцией printf.
Затем приложение пытается изменить размер полученного блока памяти, вызывая функцию HeapReAlloc:
__try
{
HeapReAlloc(hHeap, HEAP_GENERATE_EXCEPTIONS |
HEAP_REALLOC_IN_PLACE_ONLY, lpszBuff, 150);
}
Так как указан флаг HEAP_REALLOC_IN_PLACE_ONLY, при изменении размера блок не будет перемещен, поэтому мы игнорируем значение, возвращаемое функцией HeapReAlloc.
А что произойдет, если размер блока увеличится настолько, что он не поместится в адресном пространстве, отведенном для него ранее?
Мы указали флаг HEAP_GENERATE_EXCEPTIONS, поэтому в этом случае произойдет исключение, которое наше приложение обработает.
После изменения размера блока памяти приложение освобождает его функцией HeapFree, а затем удаляет динамический пул памяти, так как мы больше не будем с ним работать.
Работа с каталогами
Кратко перечислим несколько функций, предусмотренных в программном интерфейсе Microsoft Windows NT для работы с каталогами.
Работа с пулами памяти
Функции, предназначенные для работы с виртуальной памятью, которые мы рассмотрели выше, обычно используют для получения в пользование блоков памяти относительно большого размера (больше одной страницы). Однако наиболее часто приложению требуется всего несколько десятков байт, например, для создания динамических структур или для загрузки ресурсов приложения в оперативную память. Очевидно, оперируя с отдельными страницами, вы едва ли сможете легко и эффективно работать с блоками памяти небольшого объема.
Поэтому в программном интерфейсе Microsoft Windows NT были предусмотрены другие функции, к изучению которых мы и переходим. Все эти функции вызывают только что рассмотренные нами функции, работающие с виртуальной памятью.
Работа со стандартным пулом памяти
Второй способ выделения блока памяти основан на использовании стандартного пула. Для получения памяти из стандартного пула мы пользуемся функцией HeapAlloc, передавая ей в качестве первого параметра значение идентификатора стандартного пула памяти, полученное от функции GetProcessHeap:
lpszBuff = (char*)HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY, 0x1000);
Так как мы указали флаг HEAP_ZERO_MEMORY, полученный блок памяти будет расписан нулями. Флаг HEAP_GENERATE_EXCEPTIONS не указан, поэтому после вызова функции мы должны проверить значение, полученное от нее.
На следующем этапе приложение выполняет копирование строки в блок памяти и отображение ее в консольном окне:
strcpy(lpszBuff, "Test string");
printf("String:>%s<\n", lpszBuff);
Так как исключения не обрабатываются, при их возникновении работа приложения завершится аварийно.
После использования приложение освобождает блок памяти, полученный из стандартного пула, для чего вызывается функция HeapFree:
HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, lpszBuff);
Последний фрагмент приложения демонстрирует использование функций malloc и free для работы со стандартным пулом памяти и в комментариях не нуждается.
Работа задачи с несколькими критическими секциями
В том случае, когда задача работает с двумя ресурсами, доступ к которым должен выполняться последовательно, она может создать несколько критических секций. Например, приложение MultiMDI, описанное в этой книге, создает критические секции для каждого MDI-окна. Эти секции выполняют синхронизацию главной задачи приложения с задачами, создаваемыми для MDI-окон. Такая синхронизация выполняется с целью предотвращения одновременного рисования в MDI-окне главной задачей и задачей, запущенной для этого MDI-окна.
Заметим, что когда задачи работают с несколькими критическими секциями, все они должны использовать одинаковую последовательность входа в эти критические секции и выхода из них, иначе возможны взаимные блокировки задач.
Пусть, например, в приложении определены две критические секции, синхронизирующие рисование в двух окнах:
CRITICAL_SECTION csWindowOnePaint;
CRITICAL_SECTION csWindowTwoPaint;
Пусть первая задача выполняет рисование в этих окнах следующим образом:
EnterCriticalSection(&csWindowOnePaint);
EnterCriticalSection(&csWindowTwoPaint);
PaintClientWindow(hWndOne);
PaintClientWindow(hWndTwo);
LeaveCriticalSection(&csWindowTwoPaint);
LeaveCriticalSection(&csWindowOnePaint);
Пусть вторая задача использует другой порядок входа в критические секции и выхода из них:
EnterCriticalSection(&csWindowTwoPaint);
EnterCriticalSection(&csWindowOnePaint);
PaintClientWindow(hWndOne);
PaintClientWindow(hWndTwo);
LeaveCriticalSection(&csWindowOnePaint);
LeaveCriticalSection(&csWindowTwoPaint);
При этом есть вероятность того что когда первая задача войдет в критическую секцию csWindowOnePaint, управление будет передано второй задаче, которая войдет в критическую секцию csWindowTwoPaint и перейдет в состояние ожидания. Она будет ждать освобождения критической секции csWindowOnePaint, занятой первой задачей.
Однако первая задача тоже перейдет в состояние ожидания, так как ей нужна критическая секция csWindowTwoPaint, занятая второй задачей. В результате обе задачи навсегда останутся в состоянии ожидания, так как они не смогут освободить критические секции, нужные друг другу.
Если у вас нет необходимости выполнять рисование во втором окне сразу после рисования в первом окне, вы можете избежать взаимной блокировки задач не только используя правильную последовательность входа и выхода в критические секции, но и выполняя последовательное использование этих критических секций:
// Рисование в первом окне
EnterCriticalSection(&csWindowOnePaint);
PaintClientWindow(hWndOne);
LeaveCriticalSection(&csWindowOnePaint);
// Рисование во втором окне
EnterCriticalSection(&csWindowTwoPaint);
PaintClientWindow(hWndTwo);
LeaveCriticalSection(&csWindowTwoPaint);
Распределение времени между задачами
Важно заметить, что кванты времени для работы выделяются не процессам, а запущенным ими задачам (рис. 2.1). При этом если в системе установлен только один процессор, то задачи выполняются по очереди, создавая иллюзию параллельного выполнения.
Рис. 2.1. Квантование времени выполняется для задач
Если же в компьютере установлено несколько процессоров, то операционная система выделяет процессоры для выполнения задач и тогда в действительности несколько задач могут работать параллельно. Обычно для совместимости приложения составляются таким образом, чтобы они “не знали” о количестве процессоров в системе.
Для оптимальной работы системы необходимо правильно установить закон, по которому кванты времени выделяются задачам. В операционной системе Microsoft Windows NT используется приоритетное планирование задач, когда и процессы, и задачи имеют свои уровни приоритета. При необходимости операционная система может автоматически в небольших пределах изменять приоритеты, повышая, например, приоритет задач, активно работающих с периферийными устройствами компьютера.
Операционная система устанавливает уровень приоритета задач в диапазоне от 1 до 31, причем значение 31 соответствует максимальному приоритету.
В процессе планирования кванты времени выделяются задачам с максимальным приоритетом. Менее приоритетные задачи получают управление только в том случае, если более приоритетные задачи переходят в состояние ожидания. Так как рано или поздно это обязательно происходит, то даже задачи с приоритетом, равным 1, имеют большие шансы получить кванты времени.
С другой стороны, если во время работы менее приоритеных задач запускается задача с более высоким приоритетом, все низкоприоритетные задачи приостанавливаются, а кванты времени выделяются более приоритетной задаче.
На первый взгляд может показаться странным, что приложения не могут устанавливать конкретное значение приоритета задач из указанного интервала значений. Вместо этого используется двухступенчатая система установки приоритетов для процессов и задач.
Размер файла
Размер файла определить очень просто - достаточно вызвать функцию GetFileSize, прототип которой приведен ниже:
DWORD GetFileSize(
HANDLE hFile, // идентификатор файла
LPDWORD lpFileSizeHigh); // адрес старшего слова для
// размера файла
Функция GetFileSize возвращает младшее 32-разрядное слово 64-разрядного размера файла с идентификатором hFile. Старшее слово размера файла записывается в переменную типа DWORD, адрес которой передается функции через параметр lpFileSizeHigh.
Если функция завершилась без ошибок, вызванная вслед за ней функция GetLastError возвращает значение NO_ERROR. Если же произошла ошибка, функция GetFileSize возвращает значение 0xFFFFFFFF. При этом в слово, адрес которого задается параметром lpFileSizeHigh, записывается значение NULL. Код ошибки можно определить при помощи все той же функции GetLastError.
Для изменения размера файла вы можете выполнить операцию записи в него или использовать описанные выше функции SetFilePointer и SetEndOfFile.
Рекурсивное использование объектов Mutex
Так же как и критические секции, объекты Mutex допускают рекурсивное использование. Задача может выполнять рекурсивные попытки завладеть одним и тем же объектом Mutex и при этом она не будет переводиться в состояние ожидания.
В случае рекурсивного использования каждому вызову функции ожидания должен соответствовать вызов функции освобождения объекта Mutex ReleaseMutex.
Рекурсивный вход в критическую секцию
Операционная система Microsoft Windows NT допускает рекурсивный вход в критическую секцию. Например, приведенный выше фрагмент кода можно было бы составить следующим образом:
EnterCriticalSection(&csWindowPaint);
PaintClient(hWnd);
LeaveCriticalSection(&csWindowPaint);
. . .
void PaintClient(HWND hWnd)
{
. . .
EnterCriticalSection(&csWindowPaint);
hdc = BeginPaint(hWnd, &ps);
GetClientRect(hWnd, &rc);
DrawText(hdc, "SDI Window", -1, &rc,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hWnd, &ps);
LeaveCriticalSection(&csWindowPaint);
}
Здесь мы выполняем вызов функции PaintClient, находясь в критической секции csWindowPaint. При этом сама функция PaintClient также пользуется той же критической секцией.
Рекурсивный вход задачи в ту же самую критическую секцию не приводит к тому, что задача переходит в состояние ожидания. Однако для освобождения критической секции необходимо вызывать функцию LeaveCriticalSection столько же раз, сколько раз вызывается функция EnterCriticalSection.
Синхронизация с использованием семафоров
Последнее средство, предназначенное для синхронизации задач, которое мы рассмотрим в нашей книге, это объекты-семафоры. В отличие от объектов Mutex, которые используются для организации последовательного доступа задач к ресурсу, семафоры позволяют обеспечить доступ к ресурсу для заранее определенного, ограниченного приложением количества задач. Все остальные задачи, пытающиеся получить доступ сверх установленного лимита, будут переведены при этом в состояние ожидания до тех пор, пока какая либо задача, получившая доступ к ресурсу раньше, не освободит ресурс, связанный с данным семафором.
Примером области применения семафоров может послужить программное обеспечение какого-либо аппаратного устройства, с которым может работать только ограниченное количество задач. Все остальные задачи, пытающиеся получить доступ к этому устройству, должны быть переведены в состояние ожидания и пребывать в нем до тех пор, пока не завершит свою работу одна из задач, уже получившая доступ к устройству.
Ресурс, доступ к которому управляется семафором, может быть не только физическим устройством, но и чисто логическим.
Поясним это на примере.
Раньше в нашей книге мы приводили исходные тексты приложения MultiMDI. В этом приложении пользователь мог создавать MDI-окна, для каждого из которых запускалась отдельная задача, выполняющая рисование эллипсов произвольной формы и цвета. Сколько окон создаст пользователь, столько и будет запущено задач. Все эти задачи будут работать параллельно.
Предположим теперь, что нам нужно изменить приложение таким образом, чтобы пользователь мог по-прежнему создавать любое количество MDI-окон и соответствующих им задач, но чтобы при этом в каждый момент работало только ограниченное количество задач, например, не больше трех. Для этого мы создаем семафор, котрый будет использован всеми запускаемыми задачами и который будет допускать работу только трех задач.
В этом случае если пользователь, например, создаст пять MDI-окон, в трех из них будут хаотически отображаться эллипсы, а содержимое остальных не будет перерисовываться, так как соответствующие задачи будут находиться в состоянии ожидания.
Если теперь в нашем новом варианте приложения пользователь закроет MDI-окно, в котором выполняется рисование, то одно из двух “спящих” MDI-окон должно “проснутся”. При этом в нем начнется процесс рисования эллипсов. Если закрыть еще одно активное окно, процесс рисования должен начаться во втором “спящем” окне.
Таким образом, сколько бы MDI-окон не создал пользователь, эллипсы будут отображаться только в трех из них, так как только три задачи будут работать, а остальные будут находиться в состоянии ожидания.
Для реализации такого приложения нам нужен объект синхронизации, который имеет в своем составе счетчик задач, получивших доступ к этому объекту. Этим объектом синхронизации может послужить семафор. Что же касается приложения, ведущего себя подобным образом, то далее в этой главе мы приведем его исходные тексты.
Синхронизация задач с помощью событий
Завершение работы задачи или процесса можно считать событием, которое произошло в системе, и в этом смысле только что расмотренные приемы синхронизации являются ничем иным, как ожиданием события. Однако синхронизация по завершению работы задачи далеко не всегда удобна, так как две активные задачи могут выполнять некоторые действия, требующие синхронизации во время их работы.
Операционная система Microsoft Windows NT позволяет создавать объекты синхронизации, которые называются событиями (event object). Эти объекты могут находиться в отмеченном или неотмеченном состоянии, причем установка состояния выполняется вызовом соответствующей функции.
Схема использования событий достаточно проста.
Одна из задач создает объект-событие, вызывая для этого функцию CreateEvent. При этом событие имеет имя, которое доступно всем задачам активных процессов. В процессе создания или позже эта задача устанавливает событие в исходное состояние (отмеченное или неотмеченное).
Вызывая функции WaitForSingleObject или WaitForMultipleObjects, задача может выполнять ожидание момента, когда событие перейдет в отмеченное состояние.
Другая задача, принадлежащая тому же самому или другому процессу, может получить идентификатор события по его имени, например, с помощью функции OpenEvent. Далее, пользуясь функциями SetEvent, ResetEvent или PulseEvent, эта задача может изменить состояние события.
На рис. 4.2 приведен пример использования события для синхронизации двух задач, работающих одновременно.
Рис. 4.2. Пример использования события для синхронизации двух задач
Представим себе, что первая задача занимается отображением данных, которые готовятся второй задачей для отображения небольшими порциями (например, читаются с диска).
После создания неотмеченного события первая задача переходит в состояние ожидания, пока вторая задача не подготовит для нее данные. Как только это произойдет, вторая задача отмечает и затем сбрасывает событие, что приводит к завершению ожидания первой задачей.
Отобразив подготовленные данные, первая задача опять входит в состояние ожидания, пока вторая задача не подготовит данные и не отметит событие. Таким образом две задачи синхронизуют свою работу с помощью объекта-события.
Состояние страниц памяти
В дополнение к трем битам состояния страниц, хранящихся в дескрипторах страниц, система управления виртуальной памятью хранит состояние страниц в специальной базе данных страниц. В этой базе данных страница может быть отмечена как имеющая одно из следующих состояний:
Состояние | Описание | ||
Свободная | Страница доступна для использования после ее заполнения нулями | ||
Заполненная нулями | Свободная страница, заполненная нулями и доступная для использования приложениями | ||
Правильная | Страница используется активным процессом | ||
Измененная | Содержимое страницы было изменено, однако она не быле еще сохранена на диске в файле страниц | ||
Запасная | Страница удалена из рабочего набора страниц процесса | ||
Плохая | При обращении к этой странице возникла аппаратная ошибка |
Обратите внимание, что если часть оперативной памяти неисправна, есть вероятность, что операционная система Microsoft Windows NT сможет продолжить работу. Неисправные страницы будут отмечены в базе данных страниц как плохие и к ним не будет выполняться обращение.
Создание динамического пула
Если вам нужен динамический пул, вы можете его создать при помощи функции HeapCreate:
HANDLE HeapCreate(
DWORD flOptions, // флаг создания пула
DWORD dwInitialSize, // первоначальный размер пула в байтах
DWORD dwMaximumSize);// максимальный размер пула в байтах
Параметры dwMaximumSize и dwInitialSize определяют, соответственно, размер зарезервированной для пула памяти и размер памяти, полученной для использования.
Через параметр flOptions вы можете передать нулевое значение, а также значения HEAP_NO_SERIALIZE и HEAP_GENERATE_EXCEPTIONS.
Параметр HEAP_NO_SERIALIZE имеет отношение к мультизадачности, которая будет рассмотрена в отдельной главе нашей книги. Если этот параметр не указан, работающие параллельно задачи одного процесса не могут одновременно получать доступ к такому пулу. Вы можете использовать флаг HEAP_NO_SERIALIZE для повышения производительности, если создаваемым вами пулом будет пользоваться только одна задача процесса.
При выделении памяти из пула могут возникать ошибочные ситуации. Если не указан флаг HEAP_GENERATE_EXCEPTIONS, при ошибках соотвтетвующий функции будут возвращать значение NULL. В противном случае в приложении будут генерироваться исключения. Флаг HEAP_GENERATE_EXCEPTIONS удобен в тех случаях, когда в вашем приложении предусмотрена обработка исключений, позволяющая исправлять возникающие ошибки.
В случае удачи функция HeapCreate возвращает идентификатор созданного динамического пула памяти. При ошибке возвращается значение NULL (либо возникает исключение, если указан флаг HEAP_GENERATE_EXCEPTIONS).
Создание каталога
Вы можете создать новый каталог при помощи функции CreateDirectory:
BOOL CreateDirectory(
LPCTSTR lpPathName, // путь к создаваемому каталогу
LPSECURITY_ATTRIBUTES lpSecurityAttributes); // дескриптор
// защиты
Если вам не нужно определять специальные права доступа к создаваемому каталогу (например, запретить некоторым пользователям удалять каталог), вы можете указать для параметра lpSecurityAttributes значение NULL.
Создание нового дочернего окна
При выборе из меню File строки New создается новое дочернее MDI-окно и задача для него. Первое действие выполняется с помощью функции CreateMDIWindow, специально созданной для мультизадачных MDI-приложений. Задача создается при помощи функции CreateThread. Рассмотрим процесс создания дочернего окна подробнее.
Параметры фукнции CreateMDIWindow являются комбинацией значений, сохраняемых в структуре MDICREATESTRUCT полюс идентификатор окна Client Window. Напомним, что адрес этой структуры передается через второй параметр сообщения WM_MDICREATE, предназначенного для создания дочерних MDI-окон в однозадачных приложениях. Вот как создается такое окно в нашем приложении:
hwndChild = CreateMDIWindow(
LPSTR)szChildClassName, // класс окна
"MDI Child Window", // заголовок окна
0, // дополнительные стили
CW_USEDEFAULT, CW_USEDEFAULT, // размеры окна
CW_USEDEFAULT, CW_USEDEFAULT, // Document Window
hwndClient, // идентификатор окна Client Window
hInst, // идентификатор приложения
0); // произвольное значение
Вы можете сравнить это со способом, описанным нами в 17 томе “Библиотеки системного программиста” в разделе “Создание и уничтожение окна Document Window”.
Для каждого дочернего окна мы создаем и инициализируем структуру типа CHILD_WINDOW_TAG, содержащую такие значения, как идентификатор задачи, запущенной для окна, признак активности задачи и критическую секцию. Память для структуры мы получаем простейшим способом - с помощью функции malloc:
lpTag = malloc(sizeof(CHILD_WINDOW_TAG));
Далее в процессе инициализации этой структуры мы устанавливаем начальное знзачение для признака активности и выполняем инициализацию критической секции:
lpTag->fActive = 1;
InitializeCriticalSection(&(lpTag->csChildWindowPaint));
Это нужно сделать обязательно до запуска задачи, которая будет пользоваться критической секцией и проверять флаг активности.
Адрес структуры мы сохраняем в области данных окна, используя для этого функцию SetWindowLong:
SetWindowLong(hwndChild, GWL_USERDATA, (LONG)lpTag);
Впоследствии этот адрес будет извлечен функцией задачи.
На следующем шаге мы выполняем создание задачи для дочернего MDI-окна, которая будет заниматься рисованием произвольных эллипсов. Для запуска используется функция CreateThread:
hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)ThreadRoutine,
(LPVOID)hwndChild, 0,(LPDWORD)&dwIDThread);
В качестве третьего параметра мы передаем этой функции адрес функции задачи ThreadRoutine, а в качестве четвертого - идентификатор дочернего MDI-окна, который необходим для выполенния рисования.
Заметим, что мы могли бы передать через четвертый параметр функции CreateThread адрес структуры CHILD_WINDOW_TAG, дополнив эту структуру полем для хранения идентификатора дочернего MDI-окна. При этом функция задачи получила бы доступ ко всем необходимым ей параметрам. Однако в нашем приложении мы продемонстрировали оба способа, связанных с использованием параметра функции задачи и памяти окна.
После того как задача создана, ее идентификатор сохраняется в структуре CHILD_WINDOW_TAG:
lpTag->hThread = hThread;
В дальнейшем это значение будет использовано для управления задачей - для ее приостановки, возобновления выполнения, изменения относительного приоритета и так далее.
На заключительном шаге в заголовке дочернего MDI-окна отображается системный номер задачи, который записывается функцией CreateThread в переменную dwIDThread:
sprintf(szBuf, "Thread ID = %lX", dwIDThread);
SetWindowText(hwndChild, szBuf);
Напомним, что системный номер задачи и ее идентификатор - разные по смыслу и значению величины.
Создание объекта Mutex
Для создания объекта Mutex вы должны использовать функцию CreateMutex, прототип которой мы привели ниже:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // атрибуты защиты
BOOL bInitialOwner, // начальное состояние
LPCTSTR lpName); // имя объекта Mutex
В качестве первого параметра (атрибуты защиты) вы можете указать значение NULL (как и во всех наших примерах).
Параметр bInitialOwner определяет начальное состояние объекта Mutex. Если он имеет значение TRUE, задача, создающая объект Mutex, будет им владеть сразу после создания. Если же значение этого параметра равно FALSE, после создания объект Mutex не будет принадлежать ни одной задаче, пока не будет захвачен ими явным образом.
Через параметр lpName вы должны передать указатель на имя объекта Mutex, для которого действуют те же правила, что и для имени объекта-события. Это имя не должно содержать символ ‘\’ и его длина не должна превышать значение MAX_PATH.
Если объект Mutex будет использован только задачами одного процесса, вместо адреса имени можно указать значение NULL. В этом случае будет создан “безымянный” объект Mutex.
Функция CreateMutex возвращает идентификатор созданного объекта Mutex или NULL при ошибке.
Возможно возникновение такой ситуации, когда приложение пытается создать объект Mutex с именем, которое уже используется в системе другим объектом Mutex. В этом случае функция CreateMutex вернет идентификатор существующего объекта Mutex, а функция GetLastError, вызыванная сразу после вызова функции CreateMutex, вернет значение ERROR_ALREADY_EXISTS. Заметим, что функция создания объектов-событий CreateEvent ведет себя в данной ситуации аналогичным образом.
Создание семафора
Для создания семафора приложение должно вызвать функцию CreateSemaphore, прототип которой приведен ниже:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // атрибуты
// защиты
LONG lInitialCount, // начальное значение счетчика семафора
LONG lMaximumCount, // максимальное значение
// счетчика семафора
LPCTSTR lpName); // адрес строки с именем семафора
Этой функцией пользоваться достаточно просто.
В качестве атрибутов защиты вы можете передать значение NULL.
Через параметры lInitialCount и lMaximumCount передается, соответственно, начальное и максимальное значение счетчика, связанного с создаваемым семафором. Начальное значение счетчика lInitialCount должно быть больше или равно нулю и не должно превосходить максимальное значение счетчика, передаваемое через параметр lMaximumCount.
Имя семафора указывается аналогично имени рассмотренного нами ранее объекта Mutex с помощью параметра lpName.
В случае удачного создания семафора функция CreateSemaphore возвращает его идентификатор. В случае возникновения ошибки возвращается значение NULL, при этом код ошибки можно узнать при помощи функции GetLastError.
Так как имена семафоров доступны всем приложениям в системе, возможно возникновение ситуации, когда приложение пытается создать семафор с уже использованным именем. При этом новый семафор не создается, а приложение получает идентификатор для уже существующего семафора. Если возниклав такая ситуация, функция GetLastError, вызванная сразу после функции CreateSemaphore, возвращает значение ERROR_ALREADY_EXISTS.
Создание события
Для создания события задача должна вызвать функцию CreateEvent, прототип которой приведен ниже:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // атрибуты защиты
BOOL bManualReset, // флаг ручного сброса события
BOOL bInitialState, // флаг начального состояния события
LPCTSTR lpName); // адрес имени объекта-события
Параметр lpEventAttributes задает атрибуты защиты и в большинстве случаев может быть указан как NULL.
С помощью параметра bManualReset вы можете выбрать один из двух режимов работы объекта-события: ручной или автоматический. Если значение этого параметра равно TRUE, событие нужно сбрасывать вручную при помощи функции ResetEvent, которую мы рассмотрим немного позже. Если же для параметра bManualReset указать значение FALSE, событие будет сброшено (то есть переведено в неотмеченное состояние) автоматически сразу после того как задача завершит ожидание этого события.
Параметр bInitialState определяет начальное состояние события. Если этот параметр равен TRUE, объект-событие создается в отмеченном состоянии, а если FALSE - в неотмеченном.
Для того чтобы событием могли пользоваться задачи, созданные разными процессами, необходимо с помощью параметра lpName задать имя объекта-события. В качестве имени вы можете выбрать любое имя размером не более MAX_PATH символов, не содержащее символ “\”.
В том случае, когда событие используется задачами, работающими в рамках одного процесса, имя события можно не задавать, указав параметр lpName как NULL. При этом создается безымянное событие.
В случае успешного завершения функция CreateEvent возвращает идентификатор события, которым нужно будет пользоваться при выполнении всех операций над объектом-событием. При ошибке возвращается значение NULL (код ошибки можно получить при помощи функции GetLastError).
Возможна ситуация, когда при создании события вы указали имя уже существующего в системе события, созданного ранее другой задачей. В этом случае функция GetLastError, вызванная сразу после вызова функции CreateEvent, возвращает значение ERROR_ALREADY_EXISTS.
Еще один существенный недостаток невытесняющей мультизадачности проявляется при запуске недостаточно хорошо отлаженных приложений. Если по какой либо причине приложение не сможет периодически передавать управление другим запущенным приложениям, работа всей системы будет заблокирована и пользователю останется только нажать комбинацию из трех известных клавиш либо кнопку аппаратного сброса, расположенную на корпусе компьютера.
Однако не следует думать, что у Microsoft не хватило ума организовать вытесняющую мультизадачность, когда всем запущенным приложениям выделяются кванты времени с использованием системного таймера. Вытесняющая мультизадачность была использована в операционной системе OS/2 версий 1.0 - 1.3, которая в те времена разрабатывалась совместно Microsoft и IBM. Однако слабая архитектура процессора Intel 80286, недостаточная производительность выпускавшихся тогда компьютеров и малый объем оперативной памяти, установленной в компьютерах подавляющего числа пользователей (1 - 2 Мбайта) помешали широкому распространению OS/2. Эта операционная система с истинной вытесняющей мультизадачностью работала очень медленно и была вытеснена более легковесной оболочкой Microsoft Windows версии 3.1.
Сегодня ситуация изменилась. Современные операционные системы для персональных компьютеров, такие как Microsoft Windows 95, Microsoft Windows NT, IBM OS/2 Warp работают в режиме вытесняющей мультизадачности, когда все приложения гарантированно получают для себя кванты времени по прерыванию от таймера. При этом накладные расходы на мультизадачность компенсируются высокой производительностью компьютеров, поэтому пользователь не будет их чуствовать (конечно, если для уменьшения свопинга в компьютере установлено не менее 16 Мбайт оперативной памяти, что уже не редкость).
В операционной системе Microsoft Windows NT реализовано практически все, что было создано за время развития компьютеров в области мультизадачности. Как результат, у пользователя появилась возможность не просто переключаться с одной задачи на другую, а реально работать одновременно с несколькими активными приложениями. Программисты же получили в свои руки новый инструмент, с помощью которого они могут реализовать многозадачную обработку данных, даже не заостряя на этом внимание пользователя. Например, в процессе редкатирования документа текстовый процессор может заниматься нумерацией листов или подготовкой документа для печати на принтере.
/Span>Мультизадачность
Способность человека выполнять несколько задач сразу ни у кого сомнений не вызывает. Несмотря на то что врачи считают это вредным, многие любят читать во время еды или смотреть телевизор, а то и делать все это одновременно, ухитряясь при этом еще и реагировать на реплики окружающих. Как это ни странно, в мир персональных компьютеров мультизадачность вторглась относительно недавно, и далеко не каждый владелец компьютера умеет использовать все ее преимущества.
В те времена, когда повсеместно наибольшей популярностью пользовалась однозадачная операционная система MS-DOS, пользователю была доступна так называемая переключательная мультизадачность, основанная главном образом на резидентных программах. Резидентные программы калькуляторов позволяли, например, не прерывая работу программы редактора текста или другой программы, выполнить арифметические вычисления. Для переключения от выполнения основной задачи к работе с резидентной программой было нужно нажать ту или иную комбинацию клавиш.
К моменту появления мультизадачных операционных систем OS/2 и Windows было создано великое множество самых разнообразных и часто несовместимых между собой резидентных программ для MS-DOS. Среди них были достаточно мощные системы, такие, например, как Borland SideKick.
Появление операционной системы Microsoft Windows версии 3.0, работавшей как оболочка для MS-DOS, стимулировало появление приложений для Microsoft Windows, работавших в режиме невытесняющей мультизадачности. При этом приложения, составленные определенным образом, время от времени передавали друг другу управление, в результате чего создавалась иллюзия одновременной работы нескольких приложений. Аналогичный принцип использовалася в сетевой операционной системе Novell NetWare и в компьютерах фирмы Apple.
Невытесняющая мультизадачность решила проблемы совместимости, которые были слабым местом резидентных программ. Теперь пользователь мог запустить сразу несколько приложений и переключаться между ними при необходимости. Многие пользователи так и делали, однако возможности мультизадачности при этом были фактически не задействованы, так как пользователи работали с приложениями по очереди в режиме переключательной мультизадачности. Несмотря на то что формально операционная система Microsoft Windows версии 3.1 позволяет запустить, например, форматирование дискеты и на этом фоне работать с другими приложениями, едва ли найдется много желающих поступать таким образом. Дело, очевидно, в том, что пока дискета не будет отформатирована, все остальные запущенные приложения будут работать очень медленно.
/Span>процессЫ
Использование мультизадачности в рамках одного процесса позволяет относительно просто организовать параллельную работу. Однако необходимо помнить, что при этом все задачи работают в одном адресном пространстве, а значит, они не защищены друг от друга.
При необходимости организации параллельной обработки данных в отдельном адресном пространстве приложение может запустить отдельный процесс. Разумеется, процесс требует намного больше ресурсов, чем задача, и, кроме того, возникает проблема организации обмена данными. Так как дочерний процесс работает в своем адресном пространстве, родительский процесс не может использовать для передачи данных, например, глобальные переменные, - в адресном пространстве дочернего процесса они будут недоступны.
Тем не менее, передача данных между различными процессами возможна, например, с использованием динамической передачи данных DDE, файлов, отображаемых в память и так далее. В дальнейшем в одной из наших следующих книг мы подробно рассмотрим средства взаимодействия процессов, встроенные в операционную систему Microsoft Windows NT, в том числе процессов, запущенных на различных компьютерах в сети.
Предметом же этой главы будет изучение способов запуска процессов и управления процессами.
/Span>Работа с файлами
Операционная система Microsoft Windows NT имеет очень развитые средства работы с файлами. Она способна выполнять операции над файлами, расположенными в нескольких файловых системах, таких как FAT (старая добрая файловая система MS-DOS), HPFS (файловая система, созданная операционной системой IBM OS/2), NTFS (файловая система Microsoft Windows NT) и CDFS (еще одна файловая система Microsoft Windows NT, предназначенная для доступа к накопителю CD-ROM). При необходимости можно разработать собственную файловую систему и подключить ее к Microsoft Windows NT.
Файловая система FAT используется операционной системой Microsoft Windows NT для записи файлов на дискеты или на жесткий диск. Заметим, что в среде этой операционной системы вы можете отфоматировать дискеты только в формате FAT.
С помощью команды FORMAT или приложения Disk Administrator (пиктограмма которого находится в группе Administrative Tools) вы сможете отформатировать разделы жесткого диска для работы с файловыми системами FAT или NTFS.
Что же касается файловой системы HPFS, то возможность работы с ней в среде Microsoft Windows NT оставлена исключительно для совместимости с операционной системой IBM OS/2. В среде Microsoft Windows NT вы не сможете создать новый раздел HPFS. Однако если по каким-либо причинам нежелательно удалять существующий раздел в этом формате (например, вы работаете попеременно с операционными системами Microsoft Windows NT и IBM OS/2), то в среде Microsoft Windows NT вы будете иметь доступ к файлам, расположенным в разделе HPFS. Кстати, операционная система IBM OS/2 Warp версии 3.0 не предоставляет доступ к разделам NTFS.
В этой главе мы расскажем вам о преимуществах файловой системы NTFS над другими перечисленными выше файловыми системами и кратко опишем наиболее важные функции программного интерфейса операционной системы Microsoft Windows NT, предназначенные для работы с файлами. Вопросы использования файлов, отображаемых в виртуальную память, мы отложим до следующего тома “Библиотеки системного программиста”, посвященного Microsoft Windows NT. Там же вы найдете исходные тексты приложений, работающих с файлами.
/Span>Синхронизация задач и процессов
Если вы создаете мультизадачное приложение или приложение, запускающее процессы, вам необходимо уделить много внимания обеспечению синхронизации создаваемых задач и процессов. В противном случае могут появиться ошибки, которые трудно поддаются локализации. Плохо спроектированное и плохо отлаженное мультизадачное приложение может по-разному вести себя в однопроцессорных и мультипроцессорных конфигурациях компьютера, поэтому крайне желательно выполнять отладку мультизадачных приложений на мультипроцессорных конфигурациях.
В чем заключается синхронизация задач и приложений?
Если сказать кратко, то синхронизация задач (в том числе принадлежащих разным приложениям) заключается в том, что некоторые задачи должны приостанавливать свое выполнение до тех пор, пока не произойдут те или иные события, связанные с другими задачами.
Здесь возможно очень много вариантов. Например, главная задача создает задачу, которая выполняет какую-либо длительную работу, например, подсчет страниц в текстовом документе. Если теперь потребуется отобразить количество страниц на экране, главная задача должна дождаться завершения работы, выполняемой созданной задачей.
Другая проблема возникает, если, например, вы собираетесь организовать параллельную работу различных задач с одними и теми же данными. Особенно сложно выполнить необходимую синхронизацию в том случае, когда некоторые из этих задач выполняют чтение общих данных, а некоторые - изменение. Типичный пример - текстовый процессор, в котором одна задача может заниматься редактированием документа, другая - подсчетом страниц, третья - печатью документа на принтере и так далее.
Синхронизация необходима и в том случае, когда вы, например, создаете ядро системы управления базой данных, допускающее одновременную работу нескольких пользователей. При этом вам, вероятно, потребуется выполнить синхронизацию задач, принимающих запросы от пользователей, и выполняющих эти запросы.
Как мы уже говорили, если несколько задач пользуются одними и теми же графическими объектами (контекстом отображения, кистями, палитрами и так далее), необходимо обеспечить последовательное обращение этих задач к графическим ресурсам.
В программном интерфейсе операционной системы Microsoft Windows NT предусмотрены различные средства синхронизации задач, как выполняющихся в рамках одного процесса, так и принадлежащих разным процессам. Это уже упоминавшиеся ранее криические секции, события, семафоры и так далее. Такие средства синхронизации будут предметом изучения в данной главе.
Старые функции управления памятью
В 32-разрядных приложениях Microsoft Windows NT вы можете пользоваться многими функциями управления памятью операционной системы Microsoft Windows версии 3.1, которые оставлены в новой операционной системе для совместимости. Мы подробно рассмотрели эти функции в главе “Управление памятью” 13 тома “Библиотеки системного программиста”.
Напомним, что в 16-разрядном программном интерфейсе Microsoft Windows версии 3.1 существует два набора функций (глобальные и локальные), предназначенных для работы с глобальным и локальным пулом памяти. Это такие функции, как GlobalAlloc, LocalAlloc, GlobalFree, LocalFree и так далее. В 32-разрядных приложениях Microsoft Windows NT вы можете пользоваться как глобальными, так и локальными функциями, причем результат будет совершенно одинаковый. Причина этого заключается в том, что все эти функции пользуются функциями программного интерфейса Microsoft Windows NT, предназначенными для работы со стандартным пулом памяти: HeapAlloc, HeapReAlloc, HeapFree и так далее.
Вот список функций старого программного интерфейса, доступных приложениям Microsoft Windows NT:
Имя функции | Описание | ||
GlobalAlloc, LocalAlloc | Получение глобального (локального для функции LocalAlloc) блока памяти | ||
GlobalReAlloc, LocalReAlloc | Изменение размера глобального (локального) блока памяти | ||
GlobalFree, LocalFree | Освобождение глобального (локального) блока памяти | ||
GlobalLock, LocalLock | Фиксирование глобального (локального) блока памяти | ||
GlobalUnlock, LocalUnlock | Расфиксирование глобального (локального) блока памяти | ||
GlobalSize, LocalSize | Определение размера глобального (локального) блока памяти | ||
GlobalDiscard, LocalDiscard | Принудительное удаление глобального (локального) блока памяти | ||
GlobalFlags, LocalFlags | Определение состояния глобального (локального) блока памяти | ||
GlobalHandle, LocalHandle | Определение идентификатора глобального (локального) блока памяти |
Заметим, что хотя при получении памяти с помощью функции GlobalAlloc вы по-прежнему можете указывать флаг GMEM_DDESHARE, другие приложения, запущенные в среде Microsoft Windows NT, не будут иметь к этой памяти доступ. Причина очевидна - адресные пространства приложений изолированы. Однако в документации SDK сказано, что этот флаг можно использовать для увеличения производительности приложений, использующих механизм динамической передачи сообщений DDE. Этот механизм мы подробно описали в главе “Обмен данными через DDE” в 17 томе “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1. Дополнительные главы”.
Обратим ваше внимание также на то, что в среде Microsoft Windows версии 3.1 вы могли получать фиксированную (fixed), перемещаемую (moveable) и удаляемую (discardable) память.
В среде Microsoft Windows NT вы по-прежнему можете пользоваться различными типами памяти, если для получения блоков памяти используете функции GlobalAlloc или LocalAlloc. Однако теперь вам едва ли потребуется перемещаемая память, так как новая система управления памятью выполняет операцию перемещения с помощью механизма страничной адресации, не изменяя значение логического адреса.
В том случае, если вы все же решили получить блок перемещаемой памяти, перед использованием его необходимо зафиксировать функцией GlobalLock или LocalLock (соответственно, для блоков памяти, полученных функциями GlobalAlloc и LocalAlloc). Это нужно сделать потому что если вы заказываете перемещаемый блок памяти, функции GlobalAlloc и LocalAlloc возвращают не адрес блока памяти, а его идентификатор.
Если же вы получаете фиксированный блок памяти, то функции GlobalAlloc и LocalAlloc вернут вам его адрес, который можно немедленно использовать. При этом надо иметь в виду, что операционная система сможет перемещать этот блок памяти без изменения его логического адреса.
Что же касается удаляемой памяти, то ее можно использовать для хранения таких данных, которые можно легко восстановить, например, прочитав их из ресурсов приложений.
Три состояния страниц виртуальной памяти
Страницы виртуальной памяти, принадлежащие адресному пространству процесса в Microsoft Windows NT, могут находиться в одном из трех состояний. Они могут быть свободными (free), зарезервированными (reserved) или выделенными для использования (committed). В адресном пространстве приложения есть также относительно небольшое количество страниц, зарезервированных для себя операционной системой. Эти страницы недоступны приложению.
Функция VirtualAlloc может либо зарезервировать свободные страницы памяти (для чего ее нужно вызвать с параметром MEM_RESERVE), либо выделить свободные или зарезервированные страницы для непосредственного использования (для этого функция вызывается с параметром MEM_COMMIT). Приложение может либо сразу получить страницы памяти в использование, либо предварительно зарезервировать их, обеспечив доступное сплошное адресное пространство достаточного размера.
Для того чтобы зарезервированная или используемая область памяти стала свободной, вы должны вызвать для нее функцию VirtualFree с параметром MEM_RELEASE.
Вы можете перевести страницы используемой области памяти в зарезервированное состояние, не освобождая соответствующего адресного пространства. Это можно сделать при помощи функции VirtualFree с параметром MEM_DECOMMIT.
На рис. 1.11 мы показали три состояния страниц виртуальной памяти и способы перевода страниц из одного состояния в другое.
Рис. 1.11. Три состояния страниц виртуальной памяти
Удаление динамического пула
Для удаления динамического пула памяти, созданного функцией HeapCreate, вы должны использовать функцию HeapDestroy:
BOOL HeapDestroy(HANDLE hHeap);
Через единственный параметр этой функции передается идентификатор удаляемого динамического пула. Заметим, что вам не следует удалять стандартный пул, передавая этой функции значение, полученное от функции GetProcessHeap.
Функция HeapDestroy выполняет безусловное удаление пула памяти, даже если из него были получены блоки памяти и на момент удаления пула они не были возвращены системе.
Удаление файла
Для удаления файла вы должны использовать функцию DeleteFile:
BOOL DeleteFile(LPCTSTR lpFileName);
Параметр lpFileName задает путь к удаляемому файлу.
В случае успеха функция возвращает значение TRUE. При ошибке (например, при попытке удаления файла, открытого этим же или другим процессом), возвращается значение FALSE.
Удаление каталога
Для удаления каталога следует использовать функцию RemoveDirectory:
BOOL RemoveDirectory(LPCTSTR lpszDir);
Через единственный параметр этой функции следует передать путь к удаляемому каталогу.
Удаление критической секции
Если критическая секция больше не нужна, ее нужно удалить при помощи функции DeleteCriticalSection, как это показано ниже:
DeleteCriticalSection(&csWindowPaint);
При этом освобождаются все ресурсы, созданные операционной системой для критической секции.
Уменьшение значения счетчика семафора
В программном интерфейсе операционной системы Microsoft Windows NT нет фукнции, специально предназначенной для уменьшения значения счетчика семафора. Этот счетчик уменьшается, когда задача вызывает функции ожидания, такие как WaitForSingleObject или WaitForMultipleObject. Если задача вызывает несколько раз функцию ожидания для одного и того же семафора, содержимое его счетчика каждый раз будет уменьшаться.
Уничтожение семафора
Для уничтожения семафора вы должны передать его идентификатор функции CloseHandle. Заметим, что при завершении процесса все созданные им семафоры уничтожаются автоматически.
Универсальная функция CreateFile
Можно было бы подумать, что функция CreateFile предназначена только для создания файлов, аналогично функции _lcreat из программного интерфейса Microsoft Windows версии 3.1, однако это не так. Во-первых, с помощью функции CreateFile можно выполнять не только создание нового файла, но и открывание существующего файла или каталога, а также изменение длины существующего файла. Во-вторых, эта функция может выполнять операции не только над файлами, но и над каналами передачи данных, трубами (pipe), дисковыми устройствами и консолями. В этом томе мы, однако, ограничимся лишь файлами.
Прототип функции CreateFile мы привели ниже:
HANDLE CreateFile(
LPCTSTR lpFileName, // адрес строки имени файла
DWORD dwDesiredAccess, // режим доступа
DWORD dwShareMode, // режим совместного использования файла
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // дескриптор
// защиты
DWORD dwCreationDistribution, // параметры создания
DWORD dwFlagsAndAttributes, // атрибуты файла
HANDLE hTemplateFile); // идентификатор файла с атрибутами
Через параметр lpFileName вы должны передать этой функции адрес строки, содержащей имя файла, который вы собираетесь создать или открыть. Строка должна быть закрыта двоичным нулем.
Параметр dwDesiredAccess определяет тип доступа, который должен быть предоставлен к открываемому файлу. Здесь вы можете использовать логическую комбинацию следующих констант:
Константа | Описание | ||
0 | Доступ запрещен, однако приложение может определять атрибуты файла или устройства, открываемого при помощи функции CreateFile | ||
GENERIC_READ | Разрешен доступ на чтение | ||
GENERIC_WRITE | Разрешен доступ на запись |
С помощью параметра dwShareMode задаются режимы совместного использования открываемого или создаваемого файла. Для этого параметра вы можете указать комбинацию следующих констант:
Константа | Описание | ||
0 | Совместное использование файла запрещено | ||
FILE_SHARE_READ | Другие приложения могут открывать файл с помощью функции CreateFile для чтения | ||
FILE_SHARE_WRITE | Аналогично предыдущему, но на запись |
Через параметр lpSecurityAttributes необходимо передать указатель на дескриптор защиты или значение NULL, если этот дескриптор не используется. В наших приложениях мы не работаем с дескриптором защиты.
Параметр dwCreationDistribution определяет действия, выполняемые функцией CreateFile, если приложение пытается создать файл, который уже существует. Для этого параметра вы можете указать одну из следующих констант:
Константа |
Описание |
CREATE_NEW |
Если создаваемый файл уже существует, функция CreateFile возвращает код ошибки |
CREATE_ALWAYS |
Существующий файл перезаписывается, при этом содержимое старого файла теряется |
OPEN_EXISTING |
Открывается существующий файл. Если файл с указанным именем не существует, функция CreateFile возвращает код ошибки |
OPEN_ALWAYS |
Если указанный файл существует, он открывается. Если файл не существует, он будет создан |
TRUNCATE_EXISTING |
Если файл существует, он открывается, после чего длина файла устанавливается равной нулю. Содержимое старого файла теряется. Если же файл не существует, функция CreateFile возвращает код ошибки |
При этом можно использовать любые логические комбинации следующих атрибутов (кроме атрибута FILE_ATTRIBUTE_NORMAL, который можно использовать только отдельно):
Атрибут |
Описание |
FILE_ATTRIBUTE_ARCHIVE |
Файл был архивирован (выгружен) |
FILE_ATTRIBUTE_COMPRESSED |
Файл, имеющий этот атрибут, динамически сжимается при записи и восстанавливается при чтении. Если этот атрибут имеет каталог, то для всех расположенных в нем файлов и каталогов также выполняется динамическое сжатие данных |
FILE_ATTRIBUTE_NORMAL |
Остальные перечисленные в этом списка атрибуты не установлены |
FILE_ATTRIBUTE_HIDDEN |
Скрытый файл |
FILE_ATTRIBUTE_READONLY |
Файл можно только читать |
FILE_ATTRIBUTE_SYSTEM |
Файл является частью операционной системы |
Флаг |
Описание |
FILE_FLAG_WRITE_THROUGH |
Отмена промежуточного кэширования данных для уменьшения вероятности потери данных при аварии |
FILE_FLAG_NO_BUFFERING |
Отмена промежуточной буферизации или кэширования. При использовании этого флага необходимо выполнять чтение и запись порциями, кратными размеру сектора (обычно 512 байт) |
FILE_FLAG_OVERLAPPED |
Выполнение чтения и записи асинхронно. Во время асинхронного чтения или записи приложение может продолжать обработку данных |
FILE_FLAG_RANDOM_ACCESS |
Указывает, что к файлу будет выполняться произвольный доступ. Флаг предназначен для оптимизации кэширования |
FILE_FLAG_SEQUENTIAL_SCAN |
Указывает, что к файлу будет выполняться последовательный доступ от начала файла к его концу. Флаг предназначен для оптимизации кэширования |
FILE_FLAG_DELETE_ON_CLOSE |
Файл будет удален сразу после того как приложение закроет его идентификтор. Этот флаг удобно использовать для временных файлов |
FILE_FLAG_BACKUP_SEMANTICS |
Файл будет использован для выполнения операции выгрузки или восстановления. При этом выполняется проверка прав доступа |
FILE_FLAG_POSIX_SEMANTICS |
Доступ к файлу будет выполняться в соответствии со спецификацией POSIX |
В случае успешного завершения функция CreateFile возвращает идентификатор созданного или открытого файла (или каталога). При ошибке возвращается значение INVALID_HANDLE_VALUE (а не NULL, как можно было бы предположить). Код ошибки можно определить при помощи функции GetLastError.
В том случае, если файл уже существует и были указаны константы CREATE_ALWAYS или OPEN_ALWAYS, функция CreateFile не возвращает код ошибки. В то же время в этой ситуации функция GetLastError возвращает значение ERROR_ALREADY_EXISTS.
и соответствующие функции программного интерфейса
Подробно схема управления памятью в Microsoft Windows версии 3.1 и соответствующие функции программного интерфейса мы описали в 13 томе “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1 для программиста. Часть третья”. Здесь же мы остановимся только на самых важных моментах, необходимых для понимания отличий этой схемы от схемы адресации памяти в Microsoft Windows NT.
Управление памятью в MS-DOS
Принципы управления памятью в операционной системе MS-DOS и соответствующий программный интерфейс были рассмотрены нами в 19 томе “Библиотеки системного программиста”, который называется “MS-DOS для программиста”.
Напомним, что MS-DOS использует так называемый реальный режим работы процессора. Такие операционные системы, как Microsoft Windows версии 3.1, Microsoft Windows 95, Microsoft Windows NT и IBM OS/2 на этапе своей загрузки переводят процессор в защищенный режим работы. Подробно эти режимы мы описали в 6 томе “Библиотеки системного программиста”, который называется “Защищенный режим работы процессоров 80286/80386/80486”.
В реальном режиме работы процессора физический адрес, попадающий на шину адреса системной платы компьютера составляется из двух 16-разрядных компонент, которые называются сегментом и смещением (рис.1.1).
Рис.1.1. Получение физического адреса в реальном режиме работы процессора
Процедура получения физического адреса из сегмента и смещения очень проста и выполняется аппаратурой процессора. Значение сегментной компоненты сдвигается влево на 4 бита и дополняется справа четырьмя нулевыми битами. Компонента смещения расширяется до 20 разрядов записью нулей в четыре старших разряда. Затем полученные числа складываются, в результате чего образуется 20-разрядный физический адрес.
Задавая произвольные значения для сегмента и смещения, программа может сконструировать физический адрес для обращения к памяти размером чуть больше одного мегабайта. Точное значение с учетом наличия области старшей памяти High Memory Area равно 1 Мбайт + 64 Кбайт - 16 байт.
Хорошо знакомая вам из программирования для MS-DOS комбинация компонет [сегмент : смещение] называется логическим адресом реального режима. В общем виде процедура преобразования логического адреса в физический схематически изображена на рис. 1.2.
Рис. 1.2. Преобразование логического адреса в физический
Программы никогда не указывают физические адреса памяти, а всегда работают только с логическими адресами. Это верно как в реальном режиме работы процессора, так и в защищенном. Так как перобразователь адреса реализован аппаратно в процессоре, процесс преобразования не замедляет работу программы.
Логические адреса реального режима находятся в диапазоне от [0000h : 0000h] до [FFFFh : 000Fh]. Это соответствует диапазону физических адресов от 00000h до FFFFFh, лежащих в пределах первого мегабайта оперативной памяти. Задавая логические адреса в пределах от [FFFFh : 0010h] до [FFFFh : FFFFh], можно адресовать область старшей памяти High Memory Area, имеющей размер 64 Кбайт - 16 байт.
Таким образом, схема преобразования адреса реального режима не позволяет адресовать больше одного мегабайта памяти, что явно недостаточно для работы современных приложений. Даже если в вашем компьютере установлено 16 Мбайт памяти, операционная система MS-DOS не сможет адресовать непосредственно старшие 15 Мбайт (рис. 1.3).
Рис. 1.3. Адресация памяти в MS-DOS
Несмотря на то что, как мы только что сказали, программы используют не физические, а логические адреса, преобразователь адреса реального режима позволяет программам легко сконструировать логический адрес для любого нужного ей физического адреса. В этом смысле можно говорить о возможности физической адресации памяти в реальном режиме.
Такая возможность сильно снижает надежность операционной системы MS-DOS, так как любая программа может записать данные в любую область памяти. В том числе, например, в область памяти, принадлежащей операционной системе или в векторную таблицу прерываний. Неудивительно, что компьютер, работающий под управлением MS-DOS, часто зависает, особенно при использовании плохо отлаженных программ.
Что же касается программного интерфейса для управления памятью в MS-DOS, то такой существует в рамках прерывания INT 21h. Он позволяет программам заказывать и освобождать области памяти, лежащие в границах первого мегабайта. Однако ничто не мешает программам выйти за пределы полученной области памяти, выполнив при этом самоликвидацию или уничтожение операционной системы.
Управление запущенными задачами
После того как задача запущена, запустившая задача знает идентификатор дочерней задачи. Этот идентификатор возвращается функциями CreateThread, _beginthread и _beginthreadex. Пользуясь этим идентификатором, запустившая задача может управлять состоянием дочерней задачи, изменяя ее приоритет, приостанавливая, возобновляя или завершая ее работу.
Установка события
Для установки объекта-события в отмеченное состояние используется функция SetEvent:
BOOL SetEvent(HANDLE hEvent);
В качестве единственного параметра этой функции необходимо передать идентификатор объекта-события, полученного от функции CreateEvent или OpenEvent. При успешном завершении возвращается значение TRUE, при ошибке - FALSE. В последнем случае можно получить код ошибки при помощи функции GetLastError.
Увеличение значения счетчика семафора
Для увеличения значения счетчика семафора приложение должно использовать функцию ReleaseSemaphore:
BOOL ReleaseSemaphore(
HANDLE hSemaphore, // идентификатор семафора
LONG cReleaseCount, // значение инкремента
LPLONG lplPreviousCount); // адрес переменной для записи
// предыдущего значения счетчика семафора
Функция ReleaseSemaphore увеличивает значение счетчика семафора, идентификатор которого передается ей через параметр hSemaphore, на значение, указанное в параметре cReleaseCount.
Заметим, что через параметр cReleaseCount вы можете передавать только положительное значение, большее нуля. При этом если в результате увеличения новое значение счетчика должно будет превысить максимальное значение, заданное при создании семафора, функция ReleaseSemaphore возвращает признак ошибки и не изменяет значение счетчика.
Предыдущее значение счетчика, которое было до использования функции ReleaseSemaphore, записывается в переменную типа LONG. Адрес этой переменной передается функции через параметр lplPreviousCount.
Если функция ReleaseSemaphore завершилась успешно, она возвращает значение TRUE. При ошибке возвращается значение FALSE. Код ошибки в этом случае можно определить, как обычно, при помощи функции GetLastError.
Функция используется обычно для решения двух задач.
Во-первых, с помощью этой функции задачи освобождают ресурс, доступ к которому регулируется семафором. Они могут делать это после использования ресурса или перед своим завершением.
Во-вторых, эта функция может быть использована на этапе инициализации мультизадачного приложения. Создавая семафор с начальным значением счетчика, равным нулю, главная задача блокирует работу задач, выполняющих ожидание этого семафора. После завершения инициализации главная задача с помощью функции ReleaseSemaphore может увлеичить значение счетчика семафора до максимального, в результате чего известное количество ожидающих задач будет активизировано.
Вход в критическую секцию и выход из нее
Две основные операции, выполняемые задачами над критическими секциями, это вход в критическую секцию и выход из критической секции. Первая операция выполняется при помощи функции EnterCriticalSection, вторая - при помощи функции LeaveCriticalSection. Эти функции, не возвращающие никакого значения, всегда используются в паре, как это показано в следующем фрагменте исходного текста рассмотренного нами ранее приложения MultiSDI:
EnterCriticalSection(&csWindowPaint);
hdc = BeginPaint(hWnd, &ps);
GetClientRect(hWnd, &rc);
DrawText(hdc, "SDI Window", -1, &rc,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hWnd, &ps);
LeaveCriticalSection(&csWindowPaint);
В качестве единственного параметра функциям EnterCriticalSection и LeaveCriticalSection необходимо передать адрес стрктуры типа CRITICAL_SECTION, проинициализированной предварительно функцией InitializeCriticalSection.
Как работают критические секции?
Если одна задача вошла в критическую секцию, но еще не вышла ее, то при попытке других задач войти в ту же самую критическую секцию они будут переведены в состояние ожидания. Задачи пробудут в этом состоянии до тех пор, пока задача, которая вошла в критическую секцию, не выйдет из нее.
Таким образом гарантируется, что фрагмент кода, заключенный между вызовами функций EnterCriticalSection и LeaveCriticalSection, будет выполняться задачами последовательно, если все они работают с одной и той же критической секцией.
Виртуальная память в Microsoft Windows NT
В основе всей системы управления памятью Microsoft Windows NT лежит система виртуальной памяти, встроенная в ядро операционной системы. Эта система позволяет приложениям использовать области памяти, размер которых значительно превышает объем установленной в компьютере физической оперативной памяти.
Насколько значительно?
Каждое приложение, запущенное в среде Microsoft Windows NT, может адресовать до 2 Гбайт виртуальной памяти. Причем для каждого приложения выделяется отдельное пространство виртуальной памяти, так что если, например, вы запустили 10 приложений, то в сумме они могут использовать до 20 Гбайт виртуальной памяти. При всем при этом в компьютере может быть установлено всего лишь 16 Мбайт физической оперативной памяти.
Казалось бы, зачем приложениям столько памяти?
Заметим, однако, что требования программ к объему оперативной памяти растут очень быстро. Еще совсем недавно были в ходу компьютеры с объемом оперативной памяти всего 1 - 4 Мбайт. Однако современные приложения, особенно связанные с обработкой графической или мультимедийной информации предъявляют повышенные требования к этому ресурсу. Например, размер файла с графическим изображением True Color может достигать десятков Мбайт, а размер файла с видеоизображением - сотен Мбайт. Без использования виртуальной памяти возможность работы с такими файлами была бы проблематичной.
Так же как и в операционной системе Microsoft Windows версии 3.1, в Microsoft Windows NT для создания виртуальной памяти используются дисковые устройства (рис. 1.8). Система виртуальной памяти Microsoft Windows NT позволяет создать до 16 отдельных файлов страниц, расположенных на разных дисковых устройствах, установленных в компьютере. Так как максимальный объем доступной виртуальной памяти определяется объемом использованных для нее дисковых устройств, то при необходимости вы можете, например, подключить к компьютеру несколько дисков большой емкости и разместить на них файлы страниц. Сегодня стали уже вполне доступными дисковые устройства с объемом 4 - 10 Гбайт, что позволяет в Microsoft Windows NT создавать виртуальную память действительно большого размера.
Рис. 1.8. Виртуальная память в Microsoft Windows NT создается с использованием оперативной памяти и дисковых устройств
Еще один случай, когда вашему приложению может пригодится виртуальная память объемом 2 Гбайта, это использование файлов, отображаемых на память. Выполнив такое отображение в свое адресное пространство, приложение может обращаться к файлу, как к обычному массиву, расположенному в оперативной памяти. При этом все необходимые операции ввода/вывода выполняются автоматически системой управления виртуальной памятью, так что приложение не должно об этом заботиться. Отобразив на память файл реляционной базы данных, вы можете обращаться к записям этого файла по индексу как к элементам массива, что значительно упрощает программирование.
Временная приостановка работы задачи
С помощью функции Sleep задача может приостановить свою работу на заданный период времени:
VOID Sleep(DWORD cMilliseconds); // время в миллисекундах
Задача, выполняющая ожидание с помощью этой функции, не снижает производительность системы, так как ей не распределяются кванты времени. Через единственный параметр вы можете задать функции время ожидания в миллисекундах.
Если для времени ожидания указать нулевой значение, то при наличии в системе запущенных задач с таким же приоритетом, как и у задачи, вызвавшей эту функцию, ожидающая задача отдает оставшееся время от своего кванта другой задаче. В том случае, когда в системе нет других задач с таким же значением приоритета, функция Sleep немедленно возвращает управление вызвавшей ее задаче, которая сама будет использовать остаток своего кванта времени.
Есть еще одна возможность: можно организовать бесконечную задержку, передав функции Sleep значение INFINITE.
Если вы думаете, что такая
Если вы думаете, что такая серьезная операционная система, как Microsoft Windows NT, не для вас, то не исключено, что находитесь в заблуждении. Благодаря значительному прогрессу в технологии изготовления модулей оперативной памяти, дисков и других периферийных устройств, стала возможной установка этой операционной системы в компьютер стоимостью не более 1300 - 1500 долларов. За последний год цена основных компонент компьютера снизилась почти в два раза. Поэтому теперь вряд ли у программиста возникнет зависть при взгляде на компьютер с 16 Мбайтами оперативной памяти и диском с объемом 540 Мбайт - такая конфигурация стала доступна не только разработчикам программного обеспечения, но и для многим пользователям. А ведь этих ресурсов вполне достаточно для нормальной работы операционной системы Microsoft Windows NT Workstation версии 3.51 или 4.0.
Обладатель компьютера, оснащенного 16 Мбайт оперативной памяти, может выбирать между операционными системами Microsoft Windows NT, Microsoft Windows 95 или IBM OS/2 Warp. Что касается операционной системы IBM OS/2 Warp, то ей мы посвятили 20 и 25 тома “Библиотеки системного программиста”, рассмотрев вопросы установки, использования и программирования для системы Presentation Manager. Однако многие пользователи, работавшие ранее с операционной системой Microsoft Windows версии 3.1, выберут Microsoft Windows 95 или Microsoft Windows NT.
Чем же отличаются друг от друга эти две операционные системы?
Операционная система Microsoft Windows 95 создавалась таким образом, чтобы максимально облегчить переход от 16-разрядной Microsoft Windows версии 3.1 к 32-разрядной операционной среде. При разработке Microsoft Windows 95 очень много внимания уделялось вопросам совместимости с программами MS-DOS и старыми 16-разрядными приложениями Windows, в результате чего пострадала устойчивость работы этой операционной системы. Несмотря на то, что в целом Microsoft Windows 95 ведет себя значительно устойчивее по сравнению с Microsoft Windows версии 3.1, плохо спроектированные приложения или приложения, содержащие ошибки, могут полностью вывести ее из строя. А что может быть хуже для пользователя, чем зависание системы или ее аварийное завершение, в результате которого оказываются потеряны важные данные!
Что же касается того, как Microsoft Windows 95 работает с файлами, то в этом она недалеко ушла от операционной системы MS-DOS. Фактически кроме разрешения использовать для имен каталогов и файлов строки увеличенного размера, новая файловая система VFAT не дает пользователям ничего нового. В частности, по-прежнему никак не решен вопрос разграничения доступа к данным и защиты данных от несанкционированного доступа.
Есть и другие, менее заметные для пользователей недостатки, источником которых является совместимость со старыми программами. Например, использование дисковых драйверов реального режима, загружаемых через файл config.sys (например, драйвера магнитооптического устройства), может привести к снижению производительности системы.
Теперь сделаем замечание относительно требований Microsoft Windows 95 к объему оперативной памяти.
Хотя нам удавалось запустить эту операционную систему в 4 Мбайтах памяти, работала она при этом исключительно медленно, так как выполнялся непрерывный свопинг. Если мы устанавливали 8 Мбайт оперативной памяти, ситуация сильно улучшалась, однако при использовании “тяжеловесных” офисных приложений, таких как Microsoft Word или Microsoft Excel, быстродействие системы оставляло желать лучшего. Особенно, если было запущенно одновременно несколько таких приложений или выполнялось редактирование объекта OLE. Лишь только после установки 16 Мбайт памяти свопинг сократился настолько, что он перестал существенным образом ухудшать производительность системы. Кстати, операционная система Microsoft Windows Workgroups версии 3.11 ведет себя аналогичным образом, особенно если используются ее сетевые возможности.
Таким образом, для нормальной работы офисных приложений в среде Microsoft Windows 95 вам придется установить не менее 16 Мбайт оперативной памяти. Но именно столько нужно и для работы Microsoft Windows NT Workstation. При этом последняя будет обладать такой же (если не большей) производительнстью, что и Microsoft Windows 95. У вас нет возможности работать с Microsoft Windows NT только в том случае, если в вашем компьютере установлена оперативная память объемом 8 Мбайт и вы не в состоянии расширить ее до 16 Мбайт, заплатив 150 - 160 долларов.
Что же касается операционной системы Microsoft Windows NT, то при ее создании разработчики из Microsoft руководствовались в первую очередь вопросами обеспечения надежности, а не совместимости со старыми программами. Сказанное не означает, что вы не сможете запустить в среде Microsoft Windows NT программу MS-DOS или 16-разрядное приложение Windows. Однако в отличие от Microsoft Windows 95, операционная система Microsoft Windows NT контролирует выполняемые такими программами действия намного строже. В результате у вас, например, могут возникнуть трудности при запуске некторых игровых программ, рассчитанных на MS-DOS, или других программ, обращающихся непосредственно к аппаратуре компьютера.
Раньше, когда 32-разрядных программ, предназначенных специально для Microsoft Windows NT, было очень мало, ограниченная совместимость со старыми программами MS-DOS и повышенные требования к системным ресурсам сдерживали распространение этой операционной системы. На сегодняшний день 32-разрядных программ появилось очень много. Более того, практически прекратился выпуск новых 16-разрядных приложений Windows и программ MS-DOS. Вы можете приобрести 32-разрядную версию большинства известных 16-разрядных приложений, а также игровые программы, специально предназначенные для работы в среде Microsoft Windows NT или Microsoft Windows 95. Например, потребности большинства фирм удовлетворяет пакет Microsoft Office for Windows 95, который прекрасно работает в среде Microsoft Windows NT. В результате развития рынка 32-разрядных приложений актуальность совместимости со старыми программами значительно снизилась.
Что же дает пользователю переход от операционных систем Microsoft Windows версии 3.1 или Microsoft Windows 95 к Microsoft Windows NT?
Прежде всего, увеличится надежность и производительность. К другим преимуществам можно отнести возможность использования мощных средств разграничения доступа.
Файловая система NTFS, использованная в Microsoft Windows NT, является на сегодняшний день одной из лучших и надежных. У нее отсутствуют все недостатки файловой системы FAT, такие как сильные ограничения на длину имени файлов и каталогов, быстрая фрагментация дискового пространства при интенсивной работе. Система разграничения доступа, встроенная в Microsoft Windows NT, очень удобна, если компьютером пользуются несколько человек. Она позволяет организовать работу таким образом, чтобы пользователи имели доступ только к своим каталогам, а также каталогам, выделенным в совместное пользование. При необходимости администратор компьютера (да, теперь вы можете завести в вашем домашнем компьютере отдельного пользователя с правами администратора) может защитить некоторые каталоги, предоставив к ним доступ только на чтение. Это значительно сократит ущерб, например, в результате вирусной атаки или неосторожных действий начинающего пользователя.
К другим преимуществам файловой системы NTFS можно отнести использование системы транзакций, а также B-деревьев для поиска файлов в каталогах. Первое из этих преимуществ увеличивает надежность файловой системы, второе - быстродействие при поиске файлов в каталогах.
Конечно, при установке Microsoft Windows NT могут возникнуть и трудности. Так как эта операционная система, в отличие от Microsoft Windows 95 и Microsoft Windows версии 3.1, не может работать с драйверами для MS-DOS, вы должны иметь полный комплект драйверов от ваших периферийных устройств для Microsoft Windows NT. Если вы устанавливаете Microsoft Windows NT на новый компьютер, проблемы с драйверами у вас скорее всего не возникнут. Однако в любом случае перед установкой вам необходимо убедиться, что контроллер диска, видеоадаптер, устройство чтения CD-ROM и звуковой адаптер имеют драйверы для работы в Microsoft Windows NT. Если нужного драйвера нет, вы можете попытаться отыскать его в сети Internet. О работе в этой сети мы рассказали в 23 томе “Библиотеки системного программиста”, который называется “Глобальные сети компьютеров”.
Пользовательский интерфейс операционной системы Microsoft Windows NT версии 3.51 напоминает интерфейс Microsoft Windows версии 3.1, что в значительной мере упрощает переучивание пользователей. Тем не менее, будущее за объектно-ориентированным интерфейсом, использованном в Microsoft Windows 95. Уже сейчас вы можете бесплатно получить бета-версию объектно-ориентированной оболочки с сервера WWW корпорации Microsoft. А в самое ближайшее время выйдет Microsoft Windows NT версии 4.0, в которую такая оболочка будет встроена.
Подводя итог сказанному выше, мы делаем заключение, что операционная система Microsoft Windows NT готовится корпорацией Microsoft “на первые роли”. При общей тенденции снижения цен на компьютерное оборудование и лавинообразное появление новых 32-разрядных приложений пользователи смогут, наконец, выполнять всю свою работу в среде высоконадежной и удобной операционной системы Microsoft Windows NT.
Теперь о том, что вам потребуется для работы с нашей книгой.
Прежде всего, мы предполагаем, что вы уже умеете программировать для операционной системы Microsoft Windows версии 3.1. Несмотря на то что в Microsoft Windows NT появилось очень много нового, базовые понятия, такие как окна или обработка сообщений, не изменились. Создавая новую операционную систему, в Microsoft позаботились о том, чтобы программисты смогли легче перейти к ней от старой, 16-разрядной версии. Во многом это получилось. Поэтому мы решили рассказывать о программировании для Microsoft Windows NT не с самого начала, а в предположении о наличии у программиста определенных знаний о Microsoft Windows версии 3.1.
Если же вы ранее создавали программы только для MS-DOS или IBM OS/2, имеет смысл вначале прочитать 11 - 17 тома “Библиотеки системного программиста”, где мы рассказываем о программировании для Microsoft Windows версии 3.1.
Мы также очень рекомендуем вам ознакомиться с 22 томом “Библиотеки системного программиста”, который называется “Операционная система Windows 95 для программиста”. Практически все, изложенное в этой книге, применимо и для операционной системы Microsoft Windows NT.
Что же касается сетевых возможностей этой операционной системы, то в 23 томе “Библиотеки системного программиста”, который называется “Глобальные сети персональных компьютеров”, мы рассказали об использовании программного интерфейса Windows Sockets. С помощью этого интферфеса вы сможете создавать приложения для Microsoft Windows NT и Microsoft Windows 95, способные передавать данные по локальным или глобальным сетям с использованием протокола TCP/IP.
Примеры приложений, приведенные в книге, транслировались в системе разработки Microsoft Visual C++ версии 4.0. Вы таже можете воспользоваться версией 2.0 или 4.1 этой системы. Для того чтобы не набирать исходные тексты вручную и избежать ошибок, мы рекомендуем приобрести дискету с исходными текстами приложений, которая продается вместе с книгой.
Для проверки приложений мы использовали компьютер на базе процессора Pentium-90 с объемом оперативной памяти 16 Мбайт. Хотя такой объем памяти вполне достаточен для трансляции наших примеров, при возможности имеет смысл увеличить его до 32 Мбайт. В этом случае работа над большими проектами пойдет заметно быстрее. Объем дисковой памяти в нашем компьютере составляет 2,5 Гбайт, однако для установки Microsoft Windows NT и Microsoft Visual C++ вполне хватит 540 Мбайт.
Для установки Microsoft Windows NT и системы разработки Microsoft Visual C++ удобно иметь устройство чтения компакт-дисков. Выбирая это устройство, поинтересуйтесь, есть ли к нему драйвер для Microsoft Windows NT. Мы использовали 4-скоростное устройство Mitsumi FX-400.
Что касается видеомонитора, то мы рекомендуем использовать такой, который может работать с разрешением 800х600, а лучше 1024х768 пикселов или даже с еще более высоким разрешением. В этом случае использовать систему Microsoft Visual C++ будет намного удобнее. Мы приобрели 15-дюймовый монитор Sony Multiscan 15sf, отличающийся достаточно малым размером зерна - 0,25 мм, что позволило нам работать при разрешении 1024х768 пикселов. В качестве видеоадаптера был применен Diamond Stealth 64 DRAM PCI, для которого в составе Microsoft Windows NT имеется подходящий драйвер. Для работы с мультимедиа необходим звуковой адаптер. Мы использовали адаптер Creative Sound Blaster 16.
Отладку мультизадачных приложений мы выполняли на четырехпроцессорном компьютере Compaq Proliant-2000.
Если вы собираетесь заниматься разработкой приложений для Microsoft Windows NT профессионально, вам следует приобрести набор компакт-дисков “Microsoft Developer Network. Developer Platform”. В состав этого набора входят многочисленные версии Microsoft Windows, MS-DOS, огромный запас различной документации и средств разработки. Стоимость комплекта невысока (около 250 долларов США), особенно если учесть все, что в него входит.
WShowWindow
Поле wShowWindow определяет значение по умолчанию, которое будет использовано при первом вызове функции ShowWindow для главного окна приложения.
Остановимся на этом подробнее.
Как вы, наверное, помните, в приложениях Microsoft Windows версии 3.1 функция WinMain получала параметр nCmdShow, определяющий, в каком виде должно отображаться окно - в нормальном, минимизированном, максимизированном и так далее.
В среде Microsoft Windows NT параметр nCmdShow функции WinMain всегда имеет значение SW_SHOWDEFAULT. В этом случае для определения внешнего вида главного окна прилоджения используется содержимое поля wShowWindow структуры STARTUPINFO. Здесь, а также в качестве параметров функции ShowWindow, вы можете использовать следующие значения:
Значение | Внешний вид окна приложения | ||
SW_MINIMIZE | Минимизировано | ||
SW_MAXIMIZE | Максимизировано | ||
SW_RESTORE | Восстановлено в исходное состояние (это значение используется при восстановлении размеров минимизированного ранее окна) | ||
SW_HIDE | Скрыто | ||
SW_SHOW | Отображается с использованием текущих размеров и расположения | ||
SW_SHOWDEFAULT | Отображается с использованием размеров и расположения, заданных в структуре STARTUPINFO при создании процесса функцией CreateProcess | ||
SW_SHOWMAXIMIZED | Окно активизируется и отображается в максимизированном виде | ||
SW_SHOWMINIMIZED | Окно активизируется и отображается в минимизированном виде | ||
SW_SHOWMINNOACTIVE | Минимизируется, но не становится активным | ||
SW_SHOWNA | Окно отображается в текущем виде, но не активизируется | ||
SW_SHOWNOACTIVATE | Устанавливаются размеры и расположение окна, которые оно только что имело. Активизация окна не выполняется | ||
SW_SHOWNORMAL | Окно активизируется и отображается. Минимизированное окно восстанавливается |
Вернемся к полям структуры STARTUPINFO.
Запуск процесса
Пользователь запускает процесс при помощи приложений Program Manager и File Manager. Он может также воспользоваться командной строкой в системном приглашении консоли Microsoft Windows NT.
Что же касается приложения, то оно может выполнить эту задачу как при помощи уже известных вам из программирования для Microsoft Windows версии 3.1 функций WinExec и LoadModule, так и при помощи функции CreateProcess, специально предназначенной для запуска процессов. Заметим, что с помощью этой функции в середе Microsoft Windows NT версии 3.51 для платформы Intel вы можете запустить как 32-разрядные, так и 16-разрядные приложения Windows, а также программы MS-DOS и 16-разрядные консольные приложения OS/2 (напомним, что первые версии операционной системы OS/2 разрабатывались совместно фирмами Microsoft и IBM).
Запуск задач
Существует три способа запустить задачу в приложениях, составленных на языке программирования C.
Во-первых, можно использовать фукнцию CreateThread, которая входит в программный интерфейс операционной системы Microsoft Windows NT. Этот способ предоставляет наибольшие возможности по управлению запущенными задачами, позволяя, в частности, присваивать запущенным задачам атрибуты защиты и создавать задачи в приостановленном состоянии.
Во-вторых, в вашем распоряжении имеется функция из библиотеки системы разработки Microsoft Visual C++ с названием _beginthread. Задачи, созданные с использованием этой функции, могут обращаться ко всем стандартным функциям библиотеки и к переменной errno.
И, наконец, в-третьих, можно запустить задачу при помощи функции _beginthreadex, которая определена в библиотеке Microsoft Visual C++, но имеет возможности, аналогичные функции CreateThread.
Мы рассмотрим все эти способы.
Завершение процесса
Для завершения процесса используются функции ExitProcess и TerminateProcess. Первая из этих функций нужна в том случае, если процесс сам желает завершить свою работу. Именно эта функция вызывается библиотекой времени выполнения при возвращении управления функцией WinMain. Функция TerminateProcess используется родительским процессом для завершения своего дочернего процесса или любого другого процесса, идентификатор которого ей известен (и к которому имеется соотвтетствующий доступ).
Функция ExitProcess имеет один параметр - код завершения процесса:
VOID ExitProcess(UINT uExitCode);
Этот код после завершения работы процесса родительский процесс может определить при помощи функции GetExitCodeProcess.
Функции TerminateProcess необходимо передать два параметра - идентификатор завершаемого процесса и код завершения процесса:
BOOL TerminateProcess(
HANDLE hProcess, // идентификатор завершаемого процесса
UINT uExitCode); // код завершения процесса
Независимо от способа, при завершении процесса закрываются идентификаторы всех объектов, созданных задачами процесса, а все задачи процесса завершают свое выполнение.
Завершение задачи
Задача может завершиться как по собственной инициативе, так и по инициативе другой задачи. В первом случае задача либо выполняет оператор возврата из функции задачи, либо пользуется специальными функциями.
Для того чтобы завершить свое выполнение, задача, запущенная с помощью фукнции CreateThread, может вызвать функцию ExitThread, передав ей код завершения:
VOID ExitThread(DWORD dwExitCode);
Для принудительного завершения дочерней задачи родительская задача может использовать функцию TerminateThread, передав ей идентификатор завершаемой задачи и код завершения:
BOOL TerminateThread(
HANDLE hThread, // идентификатор завершаемой задачи
DWORD dwExitCode); // код завершения
Как родительская задача может получить код завершения дочерней задачи?
Для этого она должна вызвать фукнцию GetExitCodeThread:
BOOL GetExitCodeThread(
HANDLE hThread, // идентификатор завершаемой задачи
LPDWORD lpExitCode); // адрес для приема кода завершения
Если задача, для которой вызвана фукнция GetExitCodeThread, все еще работает, вместо кода завершения возвращается значение STILL_ACTIVE.
Как мы уже говорили, если задача была запущена с помощью функций _beginthread или _beginthreadex, она может завершать свое выполнение только с помощью функций, соответственно, _endthread и _endthreadex. Функцию ExitThread в этом случае использовать нельзя, так как при этом не будут освобождены ресурсы, заказанные для работы с мультизадачным вариантом библиотеки времени выполнения.