Программирование для Windows NT

         

Управление памятью


Система управления памятью, встроенная в ядро Microsoft Windows NT, является самой сложной и самой совершенной из тех, с которыми нам приводилось встречаться до сих пор. Она наилучшим образом отвечает потребностям современных приложений, “пожирающих” оперативную память мегабайтами и десятками мегабайт.

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

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

В распоряжении программиста имеются функции, предназначенные для работы с памятью на разных уровнях. Наиболее низкоуровневые средства обеспечивают работу с виртуальной памятью на уровне отдельных страниц. Более высокоуровневые средства позволяют получать блоки памяти практически любого размера из отдельных пулов, принадлежащих процессу. Можно также использовать классические функции стандартной библиотеки транслятора языка C, такие как malloc и free. Кроме того, вам доступны функции, которые пришли из программного интерфейса 16-разрядной операционной системы Microsoft Windows весии 3.1. Особое место занимают функции, предназначенные для отображения файлов в виртуальную оперативную память, позволяющие работать с файлами как с обычными массивами.

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



Адресация памяти


Приложения, запущенные в среде Microsoft Windows версии 3.1, работают с логическими адресами защищенного режима. Логический адрес защищенного режима, так же как и реального режима, состоит из двух компонент. Однако для защищенного режима это не сегмент и смещение, а селектор и смещение.

В стандартном режиме Microsoft Windows селектор и смещение всегда являются 16-разрядными. Когда Microsoft Windows работает в расширенном режиме, одна из компонент этой операционной системы (виртуальные драйверы VxD) работает в режиме 32-разрядной адресации. При этом селектор является 16-разрядным, а смещение - 32-разрядным. Что же касается обычных приложений, то и в расширенном режиме они не используют все возможности процессора 80386. В частности, размеры сегментов обычного приложения Windows не превышают 64 Кбайт.

Тем не менее расширенный режим работы обладает большим преимуществом: в нем используется страничная адресация и виртуальная память. Схема адресации для стандартного режима была описана в 13 томе “Библиотеки системного программиста”. Виртуальные драйверы описаны в 17 томе этой же серии книг.

В 32-разрядном режиме адресации (который в Microsoft Windows версии 3.1 используется только виртуальными драйверами) выполняется двухступенчатое преобразование логического адреса в физический, показанное на рис. 1.4.

Рис. 1.4. Преобразование логического адреса в физический для 32-разрядного режима

На первом этапе логический адрес, состоящий из 16-разрядного селектора и 32-разрядного смещения, преобразуется в 32-разрядный линейный адрес.

Если бы линейный адрес отображался один к одному на физический (что возможно), то с его помощью можно было бы адресовать 232= 4294967296 байт памяти, то есть 4 Гбайт. Дальнейшие преобразования линейного адреса в 32-разрядный физический адрес с использованием механизма страничной адресации позволяют еще больше расширить размер адресуемой памяти. И хотя в Microsoft Windows версии 3.1 приложения не могут использовать больше 256 Мбайт виртуальной памяти, операционная система Microsoft Windows NT успешно преодалевает этой барьер. Забегая вперед, скажем, что каждому приложению Microsoft Windows NT доступно ни много ни мало как… 2 Гбайта виртуальной памяти! Лишь бы в компьютере были установлены диски подходящей емкости.


Как же выполняется преобразование логического адреса в линейный в защищенном режиме работы процессора?

Как мы говорили в 6 и 13 томах “Библиотеки системного программиста”, для данного преобразования используется одна глобальная таблица дескрипторов GDT (Global Descriptor Table) и несколько локальных таблиц дескрипторов LDT (Local Descroptor Table). Это справедливо как для Microsoft Windows версии 3.1, так и для Microsoft Windows NT.

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



Селектор предназначен для индексации внутри одной из перечисленных выше таблиц дескрипторов. Селектор состоит из трех полей - индекса, индикатора TI и поля уровня привилегий RPL (рис. 1.5).



Рис. 1.5. Формат селектора

Поле TI (Table Indicator) используется для выбора глобальной или локальной таблицы дескрипторов. В любой момент времени может использоваться одна глобальная таблица дескрипторов и одна локальная таблица дескрипторов. Если бит TI равен 0, для выборки базового адреса используется глобальная таблица дескрипторов GDT, если 1 - локальная LDT.

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

Упрощенная схема преобразования логического адреса в линейный показана на рис. 1.6.





Рис. 1.6. Преобразование логического адреса в линейный

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

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

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

Таким образом, в защищенном режиме приложение не может делать с адресами все, что ему вздумается, как это было в реальном режиме. Более подробную информацию об этом вы можете получить из 6 тома “Библиотеки системного программиста”.

Зачем нужно использовать дескрипторные таблицы двух типов?

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

Заметим, что операционная система Microsoft Windows версии 3.1 создает одну общую глобальную таблицу дескрипторов, одну таблицу локальных дескрипторов для системной виртуальной машины, в рамках которой работают все приложения Windows, и по одной локальной таблице дескрипторов для каждой виртуальной машины DOS. Подробнее об этом вы можете узнать из главы “Драйверы для Windows” 17 тома “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1 для программиста. Дополнительные главы”.



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

Займемся теперь преобразователем линейного адреса в физический. Процесс такого преобразования имеет самое непосредственное отношение к страничной адресации памяти.

Линейный адрес разделяется на три поля:

номер таблицы в каталоге таблиц страниц (10 бит);

номер страницы в таблице страниц (10 бит);

смещение внутри страницы (12 бит)

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

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



Рис. 1.7. Преобразование линейного адреса в физический

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

Основное преимущество системы управления памятью расширенного режима Windows заключается в возможности  использования виртуальной памяти.

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

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


Аннотация


Новая книга в серии “Библиотека системного программиста” посвящена программированию для одной из наиболее перспективных операционных систем - Microsoft Windows NT. В ней мы рассмотрим наиболее интересные архитектурные особенности этой операционной системы, расскажем о системе управления памятью, об использовании мультизадачности, о файловой системе и затронем другие важнейшие вопросы. Книга будет полезна также тем, кто создает приложения для операционной системы Windows 95.

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



Асинхронные операции с файлами


Когда вы работали с файлами в операционных системах MS-DOS или Microsoft Windows версии 3.1, то вы могли выполнять только синхронные операции с файлами. Это означает, что программа MS-DOS или 16-разрядное приложение Microsoft Windows, вызывая функции для работы с файлами, приостанавливали свою работу до тех пор, пока нужная операция (запись или чтение) не будет выполнена. Это и понятно - если приложение вызывает функцию _lread или _lwrite, она не вернет управление до тех пор, пока не будет завершена, соответственно, операция чтения или записи.

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

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

Однако операционная система Microsoft Windows NT позволяет решить задачу совмещения файловых операций с другой работой намного проще. Для этого вы должны выполнять файловые операции асинхронно при помощи уже известных вам функций ReadFile и WriteFile.

Как это сделать?

Прежде всего, открывая или создавая файл функцией CreateFile вы должны указать флаг FILE_FLAG_OVERLAPPED. Далее, перед вызовом функций ReadFile или WriteFile вы должны подготовить структуру типа OVERLAPPED и передать ее адрес этим функциям через параметр lpOverlapped.

Структура OVERLAPPED определена следующим образом:

typedef struct _OVERLAPPED


{

  DWORD  Internal;      // зарезервировано

  DWORD  InternalHigh;  // зарезервировано

  DWORD  Offset;        // младшее слово позиции в файле

  DWORD  OffsetHigh;    // старшее слово позиции в файле

  HANDLE hEvent;   // идентификатор события, который будет

                   // установлен в отмеченное состояние

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

} OVERLAPPED;   

В этой структуре вы должны заполнить поля Offset, OffsetHigh и hEvent. Поля Internal и InternalHigh зарезервированы для использования операционной системой.

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

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

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

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

Самый простой способ синхронизации заключается в том, что задача, вызывающая функции ReadFile или WriteFile в асинхронном режиме, записывает в поле hEvent структуры OVERLAPPED значение NULL. После вызова указанных выше функций, когда задаче нужно дождаться завершения выполнения асинхронной операции, она вызывает функцию WaitForSingleObject, передвая ей в качестве первого параметра идентификатор файла:



WaitForSingleObject(hFile, INFINITE);

Другой способ предполагает создание отдельного объекта-события, идентификатор которого записывается в поле hEvent структуры OVERLAPPED при инициализации последней. В этом случае в качестве первого параметра функции WaitForSingleObject следует передать идентификатор этого объекта-события. Как только файловая операция будет завершена, объект-событие перейдет в отмеченное сосотояние.

Третий способ синхронизации заключается в использовании функции GetOverlappedResult. Эта функция обычно используется для проверки результата выполнения асинхронной файловой операции:

BOOL GetOverlappedResult(

  HANDLE  hFile,                      // идентификатор файла

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

  LPDWORD lpNumberOfBytesTransferred, // адрес счетчика байт

  BOOL    bWait);                     // флаг ожидания

Через параметры hFile и lpOverlapped передются, соответственно, идентификатор файла, для которого выполнялась асинхронная операция, и адрес адрес структуры OVERLAPPED, подготовленной перед выполнением операции.

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

Параметр bWait может принимать значения TRUE или FALSE. В первом случае функция GetOverlappedResult будет дожидаться завершения выполнения операции (вот вам еще одно средство синхронизации). Если же значение параметра bWait равно FALSE, то если при вызове функции операция еще не завершилась, функция GetOverlappedResult вернет значение FALSE (признак ошибки).

  При нормальном завершении (без ошибок) функция GetOverlappedResult возвращает значение TRUE.

В программном интерфейсе операционной системы Microsoft Windows NT есть еще две функции, специально предназначенные для выполнения асинхронных операций с файлами. Это функции ReadFileEx и WriteFileEx:

BOOL ReadFileEx(

  HANDLE hFile,                // идентификатор файла

  LPVOID lpBuffer,             // адрес буфера



  DWORD  nNumberOfBytesToRead, // количество байт для чтения

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

  LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);//адрес

             // функции, вызываемой после завершения операции

BOOL WriteFileEx(

  HANDLE hFile,                 // идентификатор файла

  LPVOID lpBuffer,              // адрес буфера

  DWORD  nNumberOfBytesToWrite, // количество байт для записи

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

  LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);//адрес

             // функции, вызываемой после завершения операции

Этим функциям через параметр hFile необходимо передать идентификатор файла, созданного или открытого функцией CreateFile c bcgjkmpjdfybtv флагf FILE_FLAG_OVERLAPPED.

Через параметр lpBuffer передается адрес буфера, который будет использоваться функциями ReadFileEx и WriteFileEx, соответственно, для чтения и записи. При этом параметр nNumberOfBytesToRead определяет количество байт, которые будут прочитаны функцией ReadFileEx, а параметр nNumberOfBytesToWrite - количество байт, которые будут записаны функцией WriteFileEx.

Назначение параметра lpOverlapped аналогично назначению этого же параметра в функциях ReadFile и WriteFile.

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

VOID WINAPI CompletionRoutine(

  DWORD dwErrorCode,               // код завершения

  DWORD dwNumberOfBytesTransfered, // количество прочитанных

                               // или записанных байт данных

  LPOVERLAPPED lpOverlapped);  // адрес структуры OVERLAPPED

Более подробное описание функций ReadFileEx и WriteFileEx вы найдете в документации, которая поставляется вместе с SDK.


Атрибуты файла


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



BInheritHandles


Флаг наследования идентификатора процесса и задачи bInheritHandles определяет, как это видно из названия, возможность использования данных идентификаторов в порожденных процессах и задачах. Если такая возможность вам необходима, указите для этого параметра значение TRUE. В наших приложениях мы будем указывать значение FALSE, отменяющее наследование.



Блокирующие функции


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

Функции InterlockedIncrement и InterlockedDecrement выполняют, соответственно, увеличение и уменьшение на единицу значения переменной типа LONG, адрес которой передается им в качестве единственного параметра:

LONG InterlockedIncrement(LPLONG lpAddend);

LONG InterlockedDecrement(LPLONG lpAddend);

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

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

А как может произойти ошибка?

Пусть, например, первая задача увеличивает содержимое глобальной переменной lAddend следующим образом:

lAddend += 1;

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

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


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

lTaget = lNewValue;

Специально для того чтобы избежать такой опасности, в программном интерфейсе Microsoft Windows NT предусмотрена функция InterlockedExchange:

LONG InterlockedExchange(

  LPLONG lpTarget,   // адрес изменяемой переменной

  LONG   lNewValue); // новое значение для переменной

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

   Функция InterlockedExchange возвращает старое значение изменяемой переменной.

Что же касается значения, возвращаемого функциями InterlockedIncrement и InterlockedDecrement, то оно равно нулю, если в результате изменений значение переменной стало равно нулю. Если в результате увеличения или уменьшения значение переменной стало больше или меньше нуля, то эти функции возвращают, соответственно, значение, большее или меньшее нуля. Это значение, однако, можно использовать только для сравнения, так как абсолютная величина возвращенного значения не равна новому значению изменяемой переменной.


Cb


Поле cb должно содержать размер структуры STARTUPINFO в байтах.



Cброс события


Сброс события (то есть установка его в неотмеченное состояние) выполняется функцией ResetEvent:

BOOL ResetEvent(HANDLE hEvent);

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



Дескрипторы страниц памяти


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

На рис. 1.10 показан формат дескриптора страницы.

Рис. 1.10. Формат дескриптора страницы

Физический адрес страницы имеет 20 разрядов. Для получения 32-разрядного физического адреса байта внутри страницы к нему добавляются 12 байт смещения, взятые из линейного адреса, как это было показано на рис. 1.7.

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

Биты с 3 по 6 содержат номер файла страниц, в котором находится страница, соответствующая данному дескриптору. Напомним, что в отличие от Microsoft Windows версии 3.1, операционная система Microsoft Windows NT позволяет создать до 16 файлов страниц.

Биты  с 0 по 2 описывают состояние страницы памяти. Станица может быть отмечена флагами T (находится в переходном состоянии), D (обновленная, но не сохраненная в файле страниц), и P (присутствующая в памяти). Если приложение выполняет попытку обращения к странице памяти, которой нет в памяти, возникает аппаратное прерывание и нужная страница автоматически читается из соответствующего файла страниц в физическую оперативную память. После этого работа приложения продолжается.



DwCreationFlags


Рассмотрим теперь параметр dwCreationFlags.

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

Флаги создания процесса перечислены ниже:

CREATE_SUSPENDED

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

DEBUG_PROCESS

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

DEBUG_ONLY_THIS_PROCESS

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

CREATE_UNICODE_ENVIRONMENT

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

Рассмотрение кодировки Unicode выходит за рамки этой книги, однако мы, возможно, расскажем вам о ней в одной из следующих наших книг, посвященных операционной системе Microsoft Windows NT.

CREATE_NEW_CONSOLE

Используется для консольных процессов. Если указан флаг CREATE_NEW_CONSOLE, для нового процесса создается новая консоль. Консольные процессы мы не будем пока рассматривать для экономии места в книге. Этот флаг несовместим с флагом DETACHED_PROCESS.

DETACHED_PROCESS

Используется для консольных процессов. Если указан этот флаг, новый консольный процесс не имеет доступа к родительской консоли. При необходимости он может создать новую консоль. Этот флаг несовместим с флагом CREATE_NEW_CONSOLE.

CREATE_NEW_PROCESS_GROUP




Используется для консольных процессов. Новый процесс будет корневым для группы процессов.

CREATE_SEPARATE_WOW_VDM

Используется для запуска 16-разрядных приложоений Microsoft Windows. Если установлен флаг CREATE_SEPARATE_WOW_VDM, для работы приложения создается отдельная виртуальная машина DOS. Если произойдет ошибка в этом приложении, то она не скажется на работе остальных 16-разрядных приложений Microsoft Windows, работающих на других виртуальных машинах (так как последние находятся в другом адресном пространстве).

CREATE_DEFAULT_ERROR_MODE

Новый процесс не наследует режим обработки ошибок, установленный родительским процессом при помощи функции SetErrorMode, и должен устанавливать этот режим самостоятельно.

REALTIME_PRIORITY_CLASS

HIGH_PRIORITY_CLASS

NORMAL_PRIORITY_CLASS

IDLE_PRIORITY_CLASS

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


DwFillAttribute


Содержимое поля dwFillAttribute задает цвет текста и фона для окна консольного приложнения.



DwFlags


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

Значение

Используемые поля

STARTF_USESHOWWINDOW

wShowWindow

STARTF_USEPOSITION

dwX, dwY

STARTF_USESIZE

dwXSize, dwYSize

STARTF_USECOUNTCHARS

dwXCountChars, dwYCountChars

STARTF_USEFILLATTRIBUTE

dwFillAttribute

STARTF_USESTDHANDLES

hStdInput, hStdOutput и hStdError

Дополнительными являются флаги STARTF_FORCEONFEEDBACK, STARTF_FORCEOFFFEEDBACK и STARTF_SCREENSAVER.

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

Если же указан флаг STARTF_FORCEOFFFEEDBACK, в процессе запуска приложения курсор имеет обычную форму.

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



DwY


Поля dwX и dwY определяют, соответственно, координаты X и Y левого верхнего угла окна графического приложения в пикселах. Эти значения используются для позиционирования главного окна приложения только в том случае, если при создании окна функцией CreateWindow расположение окна было указано как CW_USEDEFAULT.



DwYCountChars


Поля dwXCountChars и dwYCountChars задают, соответственно, ширину и высоту окна консольного приложения в символах.



DwYSize


Аналогично, поля dwXSize и dwYSize определяют, соответственно, ширину и высоту окна графического приложения в пикселах. Эти значения используются в том случае, если при создании окна функцией CreateWindow размыры окна были указаны как CW_USEDEFAULT.



Еще несколько операций с файлами


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



Файловая система HPFS


Высокопроизводительная файловая система HPFS (High Performance File System), использованная в операционной системе IBM OS/2, лишена большинства недостатков файловой системы FAT.

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

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

При использовании HPFS пользователь может указывать имена файлов размером до 254 символов, причем имя может состоять из заглавных и прописных букв, а также пробелов и некоторых других символов, например, символов “.” (в произвольном количестве).

В дополнение к таким атрибутам файлов, как “только читаемый”, “скрытый”, “системный” и “архивированный”, IBM OS/2 хранит для каждого файла набор расширенных атрибутов. Это тип файла, комментарий и ключевые слова для поиска, пиктограмма для визуального представления файла и т. д.

В распоряжении программиста имеются многочисленные функции программного интерфейса IBM OS/2, с помощью которых можно выполнять операции с файлами, в том числе и многозадачные. Например, можно запустить операцию чтения или записи фрагмента файла как отдельную задачу, которая будет выполняться автономно от запустившей ее задачи. Есть средства и для работы с расширенными атрибутами файлов, для создания, удаления и переименования файлов и каталогов, а также другие необходимые функции.

Заметим, что файловая система HPFS не содержит никаких средств разграничения доступа. Однако если она используется совместно с файл-сервером IBM Lan Server, в операционную систему IBM OS/2 добавляется специальный драйвер, обеспечивающий такое разграничение.

Другое ограничение файловой системы HPFS заключается в том, что виртуальным машинам DOS, работающим под управлением IBM OS/2, недоступны каталоги и файлы с длинными именами. Причина заключается в том, что в этой операционной системе не предусмотрено никакого механизма, обеспечивающего генерацию коротких альтернативных имен, как это сделано в операционных системах Microsoft Windows 95 и Microsoft Windows NT.

Подробнее об операционной системе IBM OS/2 вы можете прочитать в 20 томе “Библиотеки системного программиста”, который называется “Операционная система IBM OS/2 Warp” и в 25 томе этой же серии с названием “Программирование для операционной системы IBM OS/2 Warp”.



Фиксирование страниц виртуальной памяти


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

В программном интерфейсе Microsoft Windows NT есть функция VirtualLock, с помощью которой нетрудно зафиксировать нужное вам количество страниц в физической памяти.

Прототип функции VirtualLock представлен ниже:

BOOL VirtualLock(

  LPVOID lpvAddress, // адрес начала фиксируемой

                     // области памяти

  DWORD  cbSize);    // размер области в байтах

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

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

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

BOOL VirtualUnlock(

  LPVOID lpvAddress, // адрес начала расфиксируемой

                     // области памяти

  DWORD  cbSize);    // размер области в байтах

Сколько страниц памяти можно зафиксировать функцией VirtualLock?

Не очень много. По умолчанию приложение может зафиксировать не более 30 страниц виртуальной памяти. И это сделано не зря - фиксирование большого количества страниц одним приложением уменьшает объем физической памяти, доступной для других приложений и может отрицательно сказаться на производительности всей системы в целом. Однако при необходимости вы можете увеличить это значение при помощи функции SetProcessWorkingSetSize, описанной в SDK.

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



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


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



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


Итак, в распоряжении приложения Microsoft Windows NT имеется один стандартный пул и произвольное количество динамических пулов памяти.

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



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


Рассмотрим функции программного интерфейса операционной системы Microsoft Windows NT, предназначенные для работы с семафорами.



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


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



Функции LockFile и UnlockFile


Так как операционная система Microsoft Windows NT является мультизадачной и допускает одновременную работу многих процессов, возможно возникновение ситуаций, в которых несколько задач попытаются выполнять запись или чтение для одних и тех же файлов. Например, два процесса могут попытаться изменить одни и те же записи файла базы данных, при этом третий процесс будет в то же самое время выполнять выборку этой записи.

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

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

Блокировка участка файла выполняется функцией LockFile, прототип которой представлен ниже:

BOOL LockFile(

  HANDLE hFile, // идентификатор файла

  DWORD  dwFileOffsetLow,  // младшее слово смещения области

  DWORD  dwFileOffsetHigh, // старшее слово смещения области

  DWORD  nNumberOfBytesToLockLow,   // младшее слово длины

                                    // области

  DWORD  nNumberOfBytesToLockHigh); // старшее слово длины

                                    // области

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

Смещение блокируемой области (64-разрядное) задается при помощи параметров dwFileOffsetLow (младшее слово) и dwFileOffsetHigh (старшее слово). Размер области в байтах задается параметрами nNumberOfBytesToLockLow (младшее слово) и nNumberOfBytesToLockHigh (старшее слово). Заметим, что если в файле блокируется несколько областей, они не должны перекрывать друг друга.


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

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

BOOL UnlockFile(

  HANDLE hFile, // идентификатор файла

  DWORD  dwFileOffsetLow,  // младшее слово смещения области

  DWORD  dwFileOffsetHigh, // старшее слово смещения области

  DWORD  nNumberOfBytesToUnlockLow,   // младшее слово длины

                                      // области

  DWORD  nNumberOfBytesToUnlockHigh); // старшее слово длины

                                      // области

Заметим, что в программном интерфейсе операционной системы Microsoft Windows NT есть еще две функции, предназначенные для блокирования и разблокирования областей файлов. Эти функции имеют имена, соответственно, LockFileEx и UnlockFileEx.

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

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


Функции ReadFile и WriteFile


С помощью функций ReadFile и WriteFile приложение может выполнять, соответственно, чтение из файла и запись в файл. По своему назначению эти функции аналогичны функциям _lread, _lwrite, _hread и _hwrite из программного интерфейса Microsoft Windows версии 3.1.

Приведем прототипы функций ReadFile и WriteFile:

BOOL ReadFile(

  HANDLE  hFile,                // идентификатор файла

  LPVOID  lpBuffer,             // адрес буфера для данных

  DWORD   nNumberOfBytesToRead, // количество байт, которые

                                // необходимо прочесть в буфер

  LPDWORD lpNumberOfBytesRead,  // адрес слова, в которое

                // будет записано количество прочитанных байт

  LPOVERLAPPED lpOverlapped); // адрес структуры типа

                              // OVERLAPPED

BOOL WriteFile(

  HANDLE  hFile,                 // идентификатор файла

  LPVOID  lpBuffer,       // адрес записываемого блока данных

  DWORD   nNumberOfBytesToWrite, // количество байт, которые

                                 // необходимо записать

  LPDWORD lpNumberOfBytesWrite,  // адрес слова, в котором

               // будет сохранено количество записанных байт

  LPOVERLAPPED lpOverlapped); // адрес структуры типа

                              // OVERLAPPED

Через параметр hFile этим функциям необходимо передать идентификатор файла, полученный от функции CreateFile.

Параметр lpBuffer должен содержать адрес буфера, в котором будут сохранены прочитанные данные (для функции ReadFile), или из которого будет выполняться запись данных (для функции WriteFile).

Параметр nNumberOfBytesToRead используется для функции ReadFile и задает количество байт данных, которые должны быть прочитаны в буфер lpBuffer. Аналогично, параметр nNumberOfBytesToWrite задает функции WriteFile размер блока данных, имеющего адрес lpBuffer, который должен быть записан в файл.

Так как в процессе чтения возможно возникновение ошибки или достижение конца файла, количество прочитанных или записанный байт может отличаться от значений, заданных, соответственно, параметрами nNumberOfBytesToRead и nNumberOfBytesToWrite. Функции ReadFile и WriteFile записывают количество действительно прочитанных или записанных байт в двойное слово с адресом, соответственно, lpNumberOfBytesRead и lpNumberOfBytesWrite.


Параметр lpOverlapped используется в функциях ReadFile и WriteFile  для организации аснхронного режима чтения и записи. Если запись выполняется синхронно, в качестве этого параметра следует указать значение NULL. Способы выполнения асинхронного чтения и записи мы рассмотрим позже. Заметим только, что для использования асинхронного режима файл должен быть открыт функцией CreateFile с использованием флага FILE_FLAG_OVERLAPPED. Если указан этот флаг, параметр lpOverlapped не может иметь значение NULL. Он обязательно должен содержать адрес подготовленной структуры типа OVERLAPPED.

Если функции ReadFile и WriteFile были выполнены успешно, они возвращают значение TRUE. При возникновении ошибки возвращается значение FALSE. В последнем случае вы можете получить код ошибки, вызвав функцию GetLastError.

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



Заметим, что для однозадачных приложений используется библиотека с именем libc.lib, а для мультизадачных - с именем libcmt.lib.

После того как вы указали мультизадачную библиотеку, вы можете использовать функции _beginthread и _beginthreadex для запуска задач. Приведем прототип функции _beginthread, описанный в файле process.h:

unsigned long _beginthread(

  void(*StartAddress)(void*), // адрес функции задачи

  unsigned uStackSize,    // начальный размер стека в байтах

  void     *ArgList);     // параметры для задачи

Заметим, что функция задачи, запускаемой при помощи функции _beginthread, не возвращает никакого значения. Ее адрес передается функции _beginthread через параметр StartAddress. Через параметр ArgList вы можете передать функции задачи один параметр.

Начальный размер стека, выделяемого задаче, указывается через параметр uStackSize. Так же как и в случае с функцией CreateThread, для размера стека можно указать нулевое значение. При этом для задачи создается стек такого же размера, что и для главной задачи процесса.

В случае успеха функция _beginthread возвращает идентификатор запущенной задачи. Если же произошла ошибка, возвращается значение -1.

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

_beginthread(ThreadRoutine, 0, (void*)(Param));

Здесь запускается задача, функция которой имеет имя ThreadRoutine. Ей передается в качестве параметра значение Param.

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

void ThreadRoutine(void *Param)

{

  . . .

  _endthread();

}

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


Функция _beginthread


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

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

Для решения этой проблемы в системе разработки Microsoft Visual C++ предусмотрены отдельные библиотеки для создания однозадачных и мультизадачных приложений.

Для выбора правильной библиотеки для проекта Microsoft Visual C++ версии 4.0 или 4.1 выберите из меню Build строку Settings. На экране появится блокнот настройки параметров проекта Project Settings. Открыв страницу C/C++ этого блокнота, выберите в списке Category строку Code Generation. После этого на странице появится список Use run-time library, в котором имеются следующие строки:

Single-Treaded;

Multithreaded;

Multithreaded DLL;

Debug Single-Treaded;

Debug Multithreaded;

Debug Multithreaded DLL

Если ваше приложение является однозадачным, имеет смысл выбрать библиотеку Single-Treaded или Debug Single-Treaded (при отладке). Функции из этой библиотеки будут работать быстрее, чем из библиотеки Multithreaded (которую тоже можно использовать в однозадачных приложениях), так как не будет накладных расходов на мультизадачность.

В том случае, если вы создаете мультизадачное приложение, необходимо использовать библиотеки Multithreaded и Multithreaded DLL (для создания мультизадачных библиотек DLL) либо отладочные версии этих библиотек.



Функция _beginthreadex


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

unsigned long _beginthreadex(

 

void     *Security,       // указатель на дескриптор защиты

 

unsigned StackSize,              // начальный размер стека

 

unsigned (*StartAddress)(void*), // адрес функции задачи

 

void     *ArgList,             // параметры для задачи

 

unsigned Initflag,             // параметры создания задачи

 

unsigned *ThrdAddr);           // адрес созданной задачи

Для запуска задачи в приостановленном состоянии через параметр Initflag необходимо передать значение CREATE_SUSPENDED.

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

В случае успеха функция _beginthreadex возвращает идентификатор запущенной задачи. Если же произошла ошибка, возвращается значение 0 (а не -1, как это было для функции _beginthread).



Функция ChildWndProc


Функция ChildWndProc обрабатывает сообщения, поступающие в дочерние MDI-окна.



Функция CloseHandle


Функция CloseHandle позволяет закрыть файл. Она имеет единственный параметр - идентификатор закрываемого файла. Заметим, что если мы указали функции CreateFile флаг FILE_FLAG_DELETE_ON_CLOSE, сразу после закрывания файл будет удален. Как мы уже говорили, такая методика очень удобна при работе со временными файлами.



Функция CreateThread


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

HANDLE CreateThread(

  LPSECURITY_ATTRIBUTES lpThreadAttributes,// атрибуты защиты

  DWORD dwStackSize,       // начальный размер стека в байтах

  LPTHREAD_START_ROUTINE lpStartAddress,// адрес функции

                                        // задачи

  LPVOID  lpParameter,           // параметры для задачи

  DWORD   dwCreationFlags,       // параметры создания задачи

  LPDWORD lpThreadId);           // адрес переменной для

                                 // идентификатора задачи

Через параметр lpThreadAttributes передается адрес структуры SECURITY_ATTRIBUTES, определяющей атрибуты защиты для создаваемой задачи, или значение NULL. В последнем случае для задачи будут использованы атрибуты защиты, принятые по умолчанию. Это означает, что идентификатор созданной задачи можно использовать в любых функциях, выполняющих любые операции над задачами. Указывая атрибуты защиты, вы можете запретить использование тех или иных функций.

Приведем структуру SECURITY_ATTRIBUTES:

typedef struct _SECURITY_ATTRIBUTES 

{

  DWORD  nLength;              // размер структуры в байтах

  LPVOID lpSecurityDescriptor; // указатель на дескриптор

                               //   защиты

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

                               //   идентификатора

} SECURITY_ATTRIBUTES;

При подготовке структуры в поле nLength следует записать размер структуры SECURITY_ATTRIBUTES.

Поле указателя на дескриптор защиты lpSecurityDescriptor не заполняется приложением непосредственно. Вместо этого для установки дескритора защиты используется набор функций, которым в качестве одного из параметров передается указатель на структуру SECURITY_ATTRIBUTES. Эти функции подробно описаны в SDK. В нашей книге для экономии места мы не будем на них останавливаться. Система защиты Microsoft Windows NT достаточно мощная и потому заслуживает отдельного рассмотрения.

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


Таким образом, первые два параметра функции CreateThread не вызывают затруднений. Они могут быть в большинстве случаев указаны как NULL и 0, соответственно.

Параметр lpStartAddress задает адрес функции, которая будет выполняться как отдельная задача. Здесь вы можете просто указать имя этой функции. Функция задачи имеет один 32-разрядный параметр и возвращает 32-разрядное значение. Указанный параметр передается функции CreateThread через параметр lpParameter.

Если значение параметра dwCreationFlags равно нулю, после вызова функции CreateThread задача немедленно начнет свое выполнение. Если же в этом параметре указать значение CREATE_SUSPENDED, задача будет загружена, но приостановлена. Возобновить выполнение приостановленной задачи можно будет позже с помощью функции ResumeThread.

И, наконец, через параметр lpThreadId вы должны передать адрес переменной типа DWORD, в которую бедет записан системный номер созданной задачи (thread identifier).

В случае успеха функция CreateThread возвращает идентификатор задачи (thread handle), пользуясь которым можно выполнять над задачей те или иные операции. Не путайте этот идентификатор с системным номером задачи. При ошибке функция CreateThread возвращает значение NULL.

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

hThread = CreateThread(NULL, 0,

  (LPTHREAD_START_ROUTINE)ThreadRoutine,

  (LPVOID)hwndChild, 0,(LPDWORD)&dwIDThread);

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

В качестве функции задачи мы использовали функцию с именем ThreadRoutine, имеющую следующий вид:

DWORD ThreadRoutine(HWND hwnd)

{

  . . .

  // Оператор return завершает выполнение задачи

  return 0;

}

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

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

Так как при запуске задачи мы указали значение параметра dwCreationFlags, равное нулю, сразу после запуска задача начнет свою работу. Системный номер созданной задачи будет записан в переменную dwIDThread.


Функция DlgProc


Функция диалога DlgProc обарбатывает сообщения WM_INITDIALOG и WM_COMMAND, поступающие от диалоговой панели Start Options. Для обработки этих сообщений она вызывает, соответственно, функции DlgProc_OnInitDialog и DlgProc_OnCommand.



Функция DlgProc_OnCommand


Задачей функции DlgProc_OnCommand является обработка сообщения WM_COMMAND, поступающего в функцию диалогоа от органов управления, расположенных в диалоговой панели Start Options.

Когда пользователь нажимает кнопку OK, функция DlgProc_OnCommand определяет текущее состояние органов управления и устанавливает соответствующим образом содержимое двух глобальных переменных с именами dwCreationFlags и fWaitTermination.

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

Что же касается глобальной переменной fWaitTermination, то ее значение устанавливается в соответствии с сотоянием переключателя с независимой фиксацией Wait process termination.

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

Если пользователь отменяет работу с диалоговой панели, то происходит удаление последней без изменения содержимого глобальных переменных dwCreationFlags и fWaitTermination.



Функция DlgProc_OnInitDialog


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

Установка переключателей выполняется макрокомандой CheckDlgButton, подробно описанной в 12 томе “Библиотеки системного программиста”:

CheckDlgButton(hdlg, IDC_NORMAL, 1);



Функция FlushFileBuffers


Так как ввод и вывод данных на диск в операционной системе Microsoft Windows NT буферизуется, запись данных на диск может быть отложена до тех пор, пока система не освободится от выполнения текущей работы. С помощью функции FlushFileBuffers вы можете принудительно заставить операционную систему записать на диск все изменения для файла, идентификатор которого передается этой функции через единственный параметр:

BOOL FlushFileBuffers(HANDLE hFile);

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

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



Функция FrameWndProc


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

При создании окна функция FrameWndProc получает сообщение WM_CREATE. В ответ на это сообщение она сохраняет в структуре clcs типа CLIENTCREATESTRUCT идентификатор временного меню Window. При создании дочерних MDI-окон это меню будет расширяться.

Затем обработчик сообщения WM_CREATE сохраняет в структуре clcs идентификатор первого дочернего MDI-окна, для которого выбрано произвольное число 500. После этого выполняется создание дочернего MDI-окна, для чего вызывается функция CreateWindow. Описание этой функции вы найдете в 11 томе “Библиотеки системного программиста”.

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



Функция PulseEvent


Функция PulseEvent выполняет установку объекта-события в отмеченное состояние с последующим сбросом события в неотмеченное состояние:

BOOL PulseEvent(HANDLE hEvent);

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

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



Функция SetEndOfFile


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

BOOL SetEndOfFile(HANDLE hFile);

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



Функция SetFilePointer


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

Функция SetFilePointer позволяет выполнить установку текущей позиции:

DWORD SetFilePointer(

  HANDLE hFile,                // идентификатор файла

  LONG   lDistanceToMove, // количество байт, на которое будет

                          // передвинута текущая позиция

  PLONG  lpDistanceToMoveHigh, // адрес старшего слова,

            // содержащего расстояние для перемещения позиции

  DWORD  dwMoveMethod);        // способ перемещения позиции

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

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

Если вы работаете с файлами, размер которых не превышает 232

- 2 байта, для параметра lpDistanceToMoveHigh можно указать значение NULL. В том случае, когда ваш файл очень большой, для указания смещения может потребоваться 64-разрядное значение. Для того чтобы указать очень большое смещение, вы должны записать старшее 32-разрядное слово этого 64-разрядного значения в переменную, и передать функции SetFilePointer адрес этой переменной через параметр lpDistanceToMoveHigh. Младшее слово смещения следует передавать как и раньше, через параметр lDistanceToMove.

Параметр dwMoveMethod определяет способ изменения текущей позиции и может принимать одно из перечисленных ниже значений:

Значение

Описание

FILE_BEGIN

Смещение отсчитывается от начала файла, при этом значение смещения трактуется как беззнаковая величина

FILE_CURRENT

Смещение отсчитывается от текущей позиции в файле и может принимать как положительные, так и отрицательные значения

FILE_END

Смещение отсчитывается от конца файла и трактуется как отрицательная величина

В случае успешного завершения функция SetFilePointer возвращает младшее слово новой 64-разрядной позиции в файле. Старшее слово при этом записывается по адресу, заданному параметром lpDistanceToMoveHigh.

При ошибке функция возвращает значение 0xFFFFFFFF. При этом в слово по адресу lpDistanceToMoveHigh записывается значение NULL. Код ошибки вы можете получить при помощи функции GetLastError.



Функция StartProcess


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

Для выбора программного файла используется функция GetOpenFileName, которую мы подробно описали в разделе “Стандартные диалоговые панели для открытия файлов” 13 тома “Библиотеки системного программиста”. Путь к выбранному файлу записывается в поле lpstrFile структуры ofn.

Запуск процесса выполняется функцией CreateProcess, которая вызывается следующим образом:

if(CreateProcess(NULL, ofn.lpstrFile, NULL, NULL,

        FALSE, dwCreationFlags, NULL, NULL, &si, &pi))

{

   . . .

}

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

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

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

Через шестой параметр функции CreateProcess передаются флаги создания процесса. Здесь мы используем глобальную переменную dwCreationFlags, содержимое которой устанавливается функцией диалога диалоговой панели Create Options. Эта функция будет описана ниже.

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

Через девятый параметр функции CreateProcess передается адрес структуры si типа STARTUPINFO. Ни одно из полей этой структуры, кроме поля cb, не используется, поэтому инициализация структуры выполняется очень просто:

memset(&si, 0, sizeof(STARTUPINFO));


si.cb = sizeof(si);

И, наконец, через последний, десятый параметр функции CreateProcess передается адрес структуры pi типа PROCESS_INFORMATION. Напомним, что после удачного запуска процесса в эту структуру записываются идентификаторы, а также системные номера процесса и его главной задачи.

В случае успешного запуска процесса функция StartProcess проверяет необходимость ожидания его завершения. По умолчанию ожидание не выполняется, однако если пользователь включил в диалоговой панели Start Options переключатель Wait process termination, функция этой диалоговой панели записывает в глобальную переменную fWaitTermination значение TRUE. При этом функция StartProcess будет ожидать завершение запущенного процесса.

В режиме ожидания функция StartProcess вначале закрывает ненужный ей больше идентификатор главной задачи процесса pi.hThread, а затем вызывает функцию WaitForSingleObject, передавая ей в качестве первого параметра идентификатор процесса pi.hProcess, завершение которого необходимо дождаться. Второй параметр задает ожилание в течении неограниченного времени:

if(WaitForSingleObject(pi.hProcess, INFINITE) != WAIT_FAILED)

{

  . . .

}

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

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

Для получения кода завершения процесса мы воспользовались функцией GetExitCodeProcess:

GetExitCodeProcess(pi.hProcess, &dwExitCode);

Через первый параметр этой функции передается идентификатор завершившегося процесса, а через второй - адрес переменной типа DWORD, в которую будет записан код завершения.

После отображения кода завершения идентификатор процесса освобождается функцией CloseHandle.

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

CloseHandle(pi.hProcess);

CloseHandle(pi.hThread);

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


Функция WinMain


Изучая исходный текст функции WinMain, вы по большому счету не найдете никаких отличий от исходных текстов этой функции, приведенных в приложениях Microsoft Windows 95 из 22 тома “Библиотеки системного программиста”. В этом нет ничего удивительного, так как и в Microsoft Windows 95, и в Microsoft Windows NT реализован 32-разрядный интерфейс WIN32, которым мы должны пользоваться. Более того, разрабатывая приложения для Microsoft Windows 95, вы должны проверять их работоспособность в среде Microsoft Windows NT, так как в противном случае вы не сможете стать соискателем логотипа “Designed for Microsoft Windows 95”.

Итак, вернемся к нашей функции WinMain.

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

Затем выполняется поиск нашего приложения среди уже запущенных. При этом используется функция FindWindow. Если эта функция среди активных приложений найдет приложение с именем szAppName, она возвратит идентификатор его окна, если нет - значение NULL.

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

В среде Microsoft Windows NT, так же как и в среде Microsoft Windows 95, мы не можем проверить, было ли наше приложение запущенно ранее, анализируя значение параметра hPrevInstance функции WinMain, так как в отличие от Microsoft Windows версии 3.1, в указанных операционных системах через этот параметр всегда передается значение NULL. Об этом мы подробно говорили в 22 томе “Библиотеки системного программиста”.

Если приложение не найдено, выполняется регистрация класса окна. В отличие от операционной системы Microsoft Windows версии 3.1, в Microsoft Windows 95 и Microsoft Windows NT следует использовать регистрацию при помощи функции RegisterClassEx, подготовив для нее структуру WNDCLASSEX.

По сравнению со знакомой вам структурой WNDCLASS операционной системы Microsoft Windows версии 3.1 в структуре WNDCLASSEX имеются два дополнительных поля с именами cbSize и hIconSm. В первое из них необходимо записать размер структуры WNDCLASSEX, а во второе - идентификатор пиктограммы малого размера. Эта пиктограмма в Microsoft Windows NT версии 4.0 будет отображаться в левом верхнем углу главного окна приложения, играя роль пиктограммы системного меню.

Для загрузки пиктограмм из ресуросв приложения мы воспользовались функцией LoadImage, описанной нами в 22 томе “Библиотеки системного программиста”.

Заметим, что если вызов функции регистрации класса окна RegisterClassEx закончился неудачно (это может произойти, например, если вы запустите приложение в среде Microsoft Windows NT версии 3.5 или более ранней версии), мы выполняем регистрацию старым способом, который тоже работает, - при помощи функции RegisterClass.

После регистрации функция WinMain создает главное окно приложения, отображает и обновляет его, а затем запускает цикл обработки сообщений. Все эти процедуры мы описывали в 11 томе “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1 для программиста. Часть первая”.


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

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




Функция WinMain характерна для MDI-приложений. В ней вызывается функция инициализации приложения InitApp, которая регистрирует классы главного окна Frame Window и дочернего MDI-окна Document Window. Затем создается и отображается главное окно, а затем запускается цикл обработки сообщений. В этом цикле вызывается функция трансляции TranslateMDISysAccel, котурую используют MDI-приложения.




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




Дополнительно к действиям, выполняемым этой функцией в приложении MultiMDI, новый вариант функции WinMain создает семафор с именем lpSemaphoreName:

hSemaphore = CreateSemaphore(NULL, 2, 2, lpSemaphoreName);

if(hSemaphore == NULL)

{

  MessageBox(NULL, "Ошибка при создании семафора",

    szWindowTitle, MB_OK | MB_ICONEXCLAMATION);

  return FALSE;

}

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

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

CloseHandle(hSemaphore);



Функция WndProc


Функция WndProc обрабатывает сообщения WM_CREATE, WM_DESTROY и WM_COMMAND. Соответствующие функции обработки WndProc_OnCreate, WndProc_OnDestroy и WndProc_OnCommand назначаются для этих сообщений при помощи макрокоманды HANDLE_MSG. Этот метод обработки сообщений был нами подробно рассмотрен в 22 томе “Библиотеки системного программиста”, поэтому сейчас мы не будем его описывать.


Функция главного окна приложения WndProc обрабатывает сообщения WM_CREATE, WM_DESTROY, WM_PAINT и WM_COMMNAD. Для этого с помощью макрокоманды HANDLE_MSG она вызывает, соответственно, функции WndProc_OnCreate, WndProc_OnDestroy, WndProc_OnPaint и WndProc_OnCommand.

Необработанные сообщения передаются фукнции DefWindowProc.




В задачу функции WndProc входит обработка сообщения WM_COMMAND, которая выполняется с помощью функции WndProc_OnCommand. Все остальные сообщения передаются функции DefWindowProc.



Функция WndProc_OnCommand


Функция WndProc_OnCommand обрабатывает сообщение WM_COMMAND, поступающее от главного меню приложения. Выбирая строки меню Set protection, пользователь может изменять тип доступа, разрешенного для блока памяти, заказанного приложением при обработке сообщения WM_CREATE. Меню Memory позволяет пользователю выполнять над этим блоком операции чтения, записи, фиксирования и расфиксирования.

Изменение типа доступа выполняется при помощи функции VirtualProtect. Например, установка типа доступа PAGE_NOACCESS выполняется следующим образом:

VirtualProtect(lpMemoryBuffer, MEMBLOCK_SIZE,

  PAGE_NOACCESS, &dwOldProtect);

При этом старый тип доступа записывается в переменную dwOldProtect, но никак не используется нашим приложением.

После изменения типа доступа обработчик сообщения WM_COMMAND изменяет соответствующим образом отметку строк меню Set protection, для чего используется макрокоманда CheckMenuItem.

Теперь рассмотрим обработку сообщения WM_COMMAND в том случае, когда оно приходит от меню Memory.

Если пользователь выполняет попытку чтения блока памяти, выбирая из меню Memory строку Read, выполняется следующий фрагмент кода:

case ID_MEMORY_READ:

{

  __try

  {

    test = *((int *)lpMemoryBuffer);

  }

  __except (EXCEPTION_EXECUTE_HANDLER)

  {

    sprintf(chBuff, "Исключение с кодом\n"

      "%lX\nпри чтении блока памяти", GetExceptionCode());

    MessageBox(hWnd, chBuff,

      szAppTitle, MB_OK | MB_ICONEXCLAMATION);

    break;

  }

  MessageBox(hWnd, "Чтение выполнено",

    szAppTitle, MB_OK | MB_ICONEXCLAMATION);

  break;

}

Здесь в области действия оператора __try, ограниченной фигурными скобками, содержимое первого слова буфера lpMemoryBuffer читается во временную переменную test. Эта, безопасная на первый взгляд операция может привести в приложении Microsoft Windows NT к возникновению исключения, так как соответствующая страница памяти может оказаться недоступной для чтения. Если не предусмотреть обработку исключения, при его возникновении работа приложения завершится аварийно.


Эта функция обрабатывает сообщение WM_COMMAND, поступающее от главного меню приложения. Она не имеет никаких особенностей.




Эта функция обрабатывает сообщение WM_COMMAND, поступающее в главное окно приложения от меню. Когда пользователь выбирает из меню File строку Start process, функция WndProc_OnCommand вызывает функцию StartProcess, определенную в нашем приложении. Последняя отображает на экране стандартную панель выбора программного файла и в случае успешного выбора запускает этот файл на выполнение как отдельный процесс. При этом используются параметры запуска, установленные при помощи диалоговой панели Start Options.

В том случае когда пользователь выбирает из меню File строку Options, на экране отображается модальная диалоговая панель Start Options, имеющая идентификатор MAKEINTRESOURCE(IDD_DIALOG1).

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

DialogBox(hInst, MAKEINTRESOURCE(IDD_DIALOG1), hWnd, DlgProc);

Эта функция, а также все, что относится к диалоговым панелям, мы описали в главе “Диалоговые панели” 12 тома “Библиотеки системного программиста”.

Заметим, что в среде 32-разрядных операционных систем Microsoft Windows 95 и Microsoft Windows NT в качестве последнего параметра функции DialogBox можно указывать имя функции диалога. При этом вам не требуется создавать переходник диалоговой функции, вызывая функцию MakeProcInstance. Функция MakeProcInstance не используется 32-разрядными приложениями.



Функция WndProc_OnCreate


Напомним, что при создании окна его функции окна передается сообщение WM_CREATE. Функция WndProc_OnCreate, определенная в нашем приложении, выполняет обработку этого сообщения.

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

lpReserved = VirtualAlloc(NULL, MEMBLOCK_SIZE,

  MEM_RESERVE, PAGE_NOACCESS);

Через первый параметр мы передаем функции VirtualAlloc значение NULL, поэтому операционная система сама определит для нас начальный адрес резервируемой области. Этот адрес мы сохраняем во временной локальной переменной lpReserved.

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

lpMemoryBuffer = VirtualAlloc(lpReserved,

  MEMBLOCK_SIZE, MEM_COMMIT, PAGE_NOACCESS);

Так как в качестве первого параметра функции VirtualAlloc передается значение lpReserved, выделение страниц памяти выполняется в зарезервированной ранее области адресов.

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

VirtualFree(lpReserved, 0, MEM_RELEASE);

return FALSE;

Заметим, что мы могли бы и не вызывать функцию VirtualFree, так как после завершения процесса операционная система Microsoft Windows NT автоматически освобождает все распределенные для него ранее страницы виртуальной памяти.

Последнее, что делает обработчик сообщения WM_CREATE, это получение идентификатора меню Set protection и отметку в этом меню строки PAGE_NOACCESS:

hSetMenu = GetSubMenu(GetMenu(hWnd), 1);

CheckMenuItem(hSetMenu,

  ID_SETPROTECTION_PAGENOACCESS, MF_CHECKED);

Использованные при этом функции были описаны в главе “Меню” 13 тома “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1 для программиста. Часть третья”.



Функция WndProc_OnDestroy


Обработчик сообщения WM_DESTROY проверяет содрежимое указателя lpMemoryBuffer и, если оно не равно NULL, освобождает память при помощи функции VirtualFree:

if(lpMemoryBuffer != NULL)

  VirtualFree(lpMemoryBuffer, 0, MEM_RELEASE);

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

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



Функция WndProc_OnPaint


Функция WndProc_OnPaint выполняет очень простую вещь - она рисует в центре главного окна приложения текстовую строку SDI Window, используя для этого известную вам из программирования для Microsoft Windows версиии 3.1 функцию DrawText.

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

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);

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

Почему это важно?

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



Функция задачи PaintEllipse


Функция задачи PaintEllipse (наряду с другими двумя функциями задач PaintRect и PaintText) запускается при помощи функции _createthread во время инициализации главного окна приложения.

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

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

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

Способ рисования эллипса не имеет никаких особенностей. Соответствующая функция Ellipse была нами описана в 14 томе “Библиотеки системного программиста”, который называется “Графичский интерфейс GDI в MS Windows”.

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



Функция задачи PaintRect


Функция задачи PaintRect аналогична только что рассмотренной функции PaintEllipse, за исключением того что она рисует прямоугольники. Для инициализации генератора случайных чисел используется другое значение, а рисование прямоугольника выполняется функцией Rectangle. Эта функция имеет такие же параметры, что и функция Ellipse.

Для синхронизации задача PaintRect использует все ту же критическую секцию csWindowPaint.



Функция задачи PaintText


Задача PaintText рисует текстовую строку TEXT, используя для этого функцию TextOut, описанную в 11 томе “Библиотеки системного программсита”. Синхронизация задачи выполняется с помощью критической секции csWindowPaint.



Функция задачи ThreadRoutine


Задача ThreadRoutine запускается для каждого вновь создаваемого дочернего MDI-окна. Ее функция получает один параметр - идентификатор этого дочернего окна, который необходим для выполнения рисования. Другие параметры, нужные для работы функции задачи ThreadRoutine, извлекаются из структуры типа CHILD_WINDOW_TAG. В свою очередь, адрес этой структуры извлекается из памяти окна перед началом цикла рисования:

lpMyWndTag = (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd,

  GWL_USERDATA);

Бесконечный цикл, в котором работает задача, прерывается в том случае, если признак активности задачи не равен единице:

if(!lpMyWndTag->fActive)

  break;

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

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

DeleteCriticalSection(&(lpMyWndTag->csChildWindowPaint));

free(lpMyWndTag);

Затем задача завершает свое выполнение с помощью оператора return.



HStdError


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