Успешный вызов функции WaitForSingleObject или WaitForMuttipleObjects на самом деле меняет состояние некоторых объектов ядра. Успешным считается такой вызов, который завершается освобождением соответствующего объекта или объектов. При этом функция возвращает значение WAIT_OBJECT_0 или значение, являющееся смещением относительно WAIT_OBJECT_0. Вызов считается неудачным, если возвращается значение WAIT_TIMEOUT или WAIT_FAILED. В этом случае состояние каких-либо объектов не меняется.
Пусть, например, поток вызвал функцию WaitForSingleObject и ждет освобождения объекта «событие с автосбросом» (объекты-события рассматриваются в следующем разделе). Когда объект переходит в свободное состояние, функция обнаруживает это и возвращает значение WAIT_OBJECT_0. Но перед самым возвратом из функции объект-событие будет переведен в занятое состояние. Это и есть побочный эффект успешного ожидания.
Объекты ядра «событие с автосбросом» ведут себя подобным образом, потому что таково одно из правил, определенных Microsoft для объектов этого типа. Другие объекты дают иные побочные эффекты, а некоторые — вообще никаких. К последним относятся объекты ядра «процесс» и «поток».
Многие объекты ядра могут находиться либо в свободном {signaled state), либо
в занятом состоянии (nonsignaled state). К таким объектам относятся:
процессы;
потоки;
задания;
файлы;
консольный ввод;
уведомления об изменении файлов;
события;
ожидаемые таймеры;
семафоры;
мьютексы.
Wait-функции позволяют потоку в любой момент приостановиться и ждать освобождения какого-либо объекта ядра. Важным свойством функций этого семейства является то, что они не тратят процессорное время, пока ждут освобождения объекта или наступления тайм-аута.
Потоки одного процесса не имеют доступа к адресному пространству другого процесса. Однако существуют специализированные механизмы для передачи данных между процессами. Вообще обмен данными между процессами (Interprocess Communication, IPC) в свое время начинался в виде процедуры перемещения перфокарт из выходного отверстия одного вычислительного устройства в устройство чтения другого вычислительного устройства. Позже, с развитием операционных систем, обмен данными между процессами проделал определенный путь в сторону высоких технологий. Наиболее впечатляющим примером обмена информацией является создание Всемирной паутины (World Wide Web). Приведем краткий список механизмов IPC, встроенных в Windows:
Буфер обмена (clipboard) — одна из самых примитивных и хорошо известных форм IPC. Основная его задача состоит в поддержке обмена данными между программами по желанию и под контролем пользователя.
Библиотеки динамической компоновки. Когда в рамках DLL объявляется переменная, то ее можно сделать разделяемой (shared). Все процессы из DLL, обращающиеся к такой переменной, будут использовать одно и то же место в физической памяти.
Сообщение WM_COPYDATA, которое применяется для передачи участка памяти другому процессу.
Разделяемая память (shared memory) реализуется при помощи объектов ядра «проекция файла», которые осуществляют отображение файлов на оперативную память.
Протокол динамического обмена данными (Dynamic Data Exchange, DDE), который определяет все основные функции для обмена данными между приложениями. DDE широко использовался до тех пор, пока Microsoft не приняла решение использовать OLE, которую затем переименовали в ActiveX, в качестве основной технологии взаимодействия программ.
Удаленный вызов процедур (Remote Procedure Call, RPC), строго говоря, не является механизмом IPC. Это скорее, технологическая оболочка, расширяющая возможности традиционных механизмов IPC. Благодаря RPC сеть становится совершенно прозрачной как для сервера, так и для клиента.
ActiveX является универсальной технологией, и одним из ее применений является обмен данными между процессами. Специально для этой цели Microsoft определила стандартный интерфейс IDataObject А для обмена данными по сети используется Distibuted Component Object Model (DCOM), которую можно рассматривать как объединение ActiveX и RPC.
Каналы (pipes) — мощная технология обмена данными. В общем случае канал можно представить в виде трубы, соединяющей два процесса, через которую идет непрерывный поток данных. Каналы делятся на анонимные (anonymous pipes) и именованные (named pipes). Анонимные каналы используются довольно редко. Именованные каналы, которые поддерживаются только в системах WinNT/2000, передают произвольные данные и могут работать через сеть. В последнее время вместо именованных каналов все чаще используют сокеты.
Сокеты (sockets) — очень важная технология, так как именно она отвечает за обмен данными в сети Интернет. Сокеты также часто используются в крупных локальных сетях. Сокеты можно рассматривать как «разъемы», представляющие собой абстракцию конечных точек коммуникационной линии, которая соединяет два приложения. Windows содержит достаточно мощный API для работы с сокетами.
Почтовые слоты (mailslots) — это механизм однонаправленного IPC. Если приложению известно имя слота, то оно может помещать туда сообщения, а приложение, которое является владельцем этого слота, может их оттуда извлекать и обрабатывать. Основное преимущество этого способа заключается в возможности передавать сообщения по локальной сети сразу нескольким компьютерам за одну операцию, если, конечно, несколько приемников имеют почтовые слоты с одним и тем же именем.
Microsoft Message Queue (MSMQ) — обеспечивает посылку сообщений между приложениями с помощью очереди сообщений. В отличие от других форм IPC, эта технология позволяет посылать сообщения процессу, который в данное время недоступен, например, если приложение не запущено, сервер вышел из строя или сетевой канал связи перегружен. Механизм MSMQ ставит сообщение в очередь до тех пор, пока не появится возможность переслать его адресату.
В данной книге будут рассмотрены только два механизма обмена данными между процессами: обмен с помощью разделяемой памяти (или объекта ядра «проекция файла») и обмен при помощи сообщения WM_COPYDATA.
Но сначала — несколько слов об архитектуре памяти Win32.
Так как на современных компьютерах оперативная память RAM имеет размеры, по крайней мере, на порядок меньше 4 Гбайт, то система имитирует задекларированную память за счет дискового пространства. При этом на диске создается страничный файл (page file), который вместе с физической памятью RAM образует виртуальную память, доступную всем процессам. Другое название страничного файла — файл подкачки (swap file).
Процессор сам управляет отображением виртуальных адресов из машинных команд в эквивалентные физические адреса в ходе выполнения команды. Осуществляя это отображение, процессор оперирует страницами памяти размером 4 Кбайта. Этот же размер страниц используется Windows для управления виртуальной памятью.
Теперь посмотрим, что происходит, когда поток пытается получить доступ к блоку данных в адресном пространстве своего процесса. В этом случае возможны три ситуации:
Данные, к которым обращается поток, находятся в оперативной памяти. Тогда процессор проецирует виртуальный адрес данных на физический, и поток получает доступ к этим данным.
Данные отсутствуют в оперативной памяти, но размещены где-то в страничном файле. Попытка доступа к данным генерирует прерывание, называемое Page Fault (Ошибка страницы). Тогда система начинает искать свободную страницу в оперативной памяти. Если такой страницы нет, то система вынуждена освободить одну из занятых страниц. Если занятая страница не модифицировалась, она просто освобождается. В ином случае она сначала копируется из оперативной памяти в страничный файл. После этого система отыскивает в страничном файле запрошенный блок данных, загружает этот блок на свободную страницу оперативной памяти и, наконец, проецирует виртуальный адрес данных на соответствующий адрес в физической памяти.
Иногда из-за программной ошибки или сбоя в аппаратной части требуемая страница отсутствует и в оперативной памяти, и в страничном файле. Тогда система генерирует ошибку Invalid Page Fault, и работающее приложение закрывается.
Диспетчер виртуальной памяти (Virtual Memory Manager — VMM) является составной частью ядра операционной системы. Он отображает виртуальные адреса на физические, используя механизм подкачки страниц памяти (page swapping). Кроме того, он предоставляет прикладным программам различные интерфейсы (API) для работы с виртуальной памятью:
Virtual Memory API — набор функций, позволяющих приложению работать с виртуальным адресным пространством. Например, функции VirtualAlloc и VirtualFree позволяют процессу получать страницы из памяти или возвращать их системе.
Memory Mapped File API — набор функций, позволяющих работать с файлами, проецируемыми в память. Это новый механизм, предоставляемый Windows для работы с файлами и взаимодействия процессов.
Heap Memory API — набор функций, позволяющих работать с динамически распределяемыми областями памяти (кучами).
Проецирование файла данных в адресное пространство процесса предоставляет разработчику мощный механизм работы с файлами. Спроецировав файл на адресное пространство процесса, программа получает возможность работать с ним, как с массивом. Это очень удобно при манипуляциях с большими потоками данных. Для проецирования файла в память необходимо выполнить три операции:
1. Создать объект ядра «файл», идентифицирующий дисковый файл, который вы хотите использовать как проецируемый в память (функция CreateFile).
2. Создать объект ядра «проекция файла» при помощи функции CreateFileMapping. При этом используется дескриптор файла, возвращенный функцией CreateFile.
3. Указать отображение объекта «проекция файла» или его части на адресное пространство процесса (функция MapViewOfFile).
Закончив работу с проецируемым в память файлом, тоже следует выполнить три операции:
1. Отменить отображение на адресное пространство процесса объекта «проекция файла» (функция UnmapViewOfFile).
2. Закрыть объект ядра «проекция файла».
3. Закрыть объект ядра «файл».
Описанную технологию можно проиллюстрировать следующим фрагментом кода:
HANDLE hFile. hFileMapping:
PVOID pArray;
hFile = CreateFileC'File Name", ... );
hFileMapping = CreateFileMapping(hFile, ... ):
CloseHandle(hFile) :
pArray - MapViewOfFile(hFileMapping. ... );
CloseHandle(hFileMapping) ;
//
/ Работаем с файлом, как с массивом рАггау /
//
UnmapVi ewOfFi1е(pArray);
В этом примере «закрывающие» операции выполняются сразу после использования соответствующего дескриптора объекта. Это уменьшает вероятность утечки ресурсов.
Самый низкоуровневый механизм совместного использования данных в одной системе — проецирование файла в память. На нем так или иначе базируются все другие механизмы разделения данных. Поэтому, если вы хотите получить максимальное быстродействие с минимумом издержек, лучше всего применять именно проецирование.
Совместное использование данных в этом случае происходит по следующей схеме. Несколько процессов проецируют в память представления одного и того же объекта «проекция файла», то есть делят одни и те же страницы физической памяти. Поэтому, когда один процесс записывает данные в представление общего объекта «проекция файла», эти изменения немедленно отражаются в других процессах. Но для этого все процессы должны использовать одинаковое имя объекта «проекция файла».
В предыдущем разделе рассматривалось проецирование представления файла, размещенного на диске. Но для целей обмена данными между разными процессами хранение этих данных на диске было бы очень неудобным. К счастью, Win32 API предусматривает возможность проецирования файлов непосредственно на физическую память из страничного файла, а не из специально создаваемого дискового файла.
Этот способ даже проще стандартного, рассмотренного в предыдущем разделе. Во-первых, не нужно вызывать функцию CreateFile. Вы просто вызываете функцию CreateFileMapping и передаете значение INVALID_HANDLE_VALUE (или константу -1) в параметре hFile. Но при вызове функции CreateFileMapping следует передать в последнем ее параметре С-строку, содержащую имя этого объекта. Тогда другие процессы, если им понадобится доступ к разделяемой памяти, смогут вызвать функцию OpenFileMapping и передать ей то же самое имя.
Учтите, что если разделяемая память используется в режиме записи данных более чем одним потоком, то вы должны предусмотреть синхронизацию работы этих потоков. Пример такой синхронизации при помощи объектов-событий рассматривается чуть ниже.
Когда работа с объектом «проекция файла» завершена, процесс должен вызвать функцию CloseHandle. Как только все дескрипторы объекта будут закрыты, система освободит память, переданную из страничного файла.
Чтобы показать применение механизмов обмена между процессами с помощью разделяемой памяти (файла, проецируемого в память) и с помощью сообщения WM_COPYDATA, мы разработаем две программы, имитирующее функции сервера и клиента.
Клиентом называется объект, запрашивающий доступ к службе или ресурсу. Сервер — это объект, выполняющий некую службу или обладающий ресурсом.
Клиент и сервер могут работать на одной и той же машине, используя локальные механизмы коммуникации, или на разных машинах, применяя для связи сетевые средства.
Поведение клиента и сервера асимметрично. Процесс-сервер инициализируется и переходит в состояние ожидания запросов от возможных клиентов. Как правило, процесс-клиент запускается в интерактивном режиме и посылает запросы серверу. Сервер исполняет полученный запрос, причем это может подразумевать диалог с клиентом, а может — и нет. Затем сервер вновь переходит в состояние ожидания запросов от других клиентов.
Каждому процессу операционная система выделяет собственное виртуальное адресное пространство. В Win32 его размер составляет 4 Гбайт, что определяется разрядностью регистра команд. Соответственно, 32-битный указатель может быть любым числом в интервале от 0x00000000 до OxFFFFFFFF. Таким образом, адресуется 4 294 967 296 значений, что как раз и перекрывает указанный диапазон памяти.
Верхняя половина этого пространства, то есть адреса от 0x80000000 до OxFFFFFFFF, резервируется за операционной системой, а нижняя половина почти вся доступна процессу.
Виртуальное адресное пространство процесса доступно всем потокам этого процесса. С другой стороны, потоки одного процесса не имеют доступа к адресному пространству других процессов.
Событие (event) — самая простая разновидность объектов ядра. Оно содержит счетчик количества пользователей и две булевы переменные. Одна переменная указывает тип данного объекта-события, а другая — его состояние.
События просто уведомляют об окончании какой-либо операции. Объекты-события бывают двух типов: со сбросом вручную (manual-reset events) или с автосбросом (auto-reset events). Первые события позволяют возобновить выполнение сразу нескольких ждущих потоков, а вторые — только одного потока.
Параметр bManualReset определяет тип объекта-события. Значение TRUE создает событие со сбросом вручную, а значение FALSE — событие с автосбросом.
Параметр blnitialState определяет начальное состояние события — свободное (TRUE) или занятое (FALSE).
Параметр pszName содержит указатель на С-строку, в которой указывается имя объекта. Если pszName имеет значение NULL, то создается неименованный объект.
Например, следующий вызов функции CreateEvent из процесса А создает событие с автосбросом и именем EventName:
HANDLE hEvent = CreateEvent(NULL. FALSE. FALSE. "EventName");
На самом деле все обстоит немножко сложнее. При таком вызове система проверяет, не существует ли уже объект ядра с таким именем. Если подобный объект существует, то ядро проверяет тип этого объекта. Допустим, что некий процесс В
уже создал ранее объект-событие с таким же именем EventName. В этом случае система проверяет права доступа процесса А к этому объекту. Если с правами доступа все в порядке, то в таблице дескрипторов процесса А создается новая запись с дескриптором hEvent. To есть процесс А получает свой дескриптор уже существующего именованного объекта-события, а счетчик пользователей этого объекта увеличивается на единицу.
Предположим, что имеет место другая ситуация, когда некий процесс В уже создал ранее объект ядра с таким же именем, но другого типа, например семафор. Тогда функция CreateEvent вернет значение NULL, а если после этого вызвать функцию GetLastError, то она вернет код ошибки, равный 6 (ERROR_INVALID_HANDLE).
Наконец, самая простая ситуация — когда объект ядра с таким именем (EventName) в момент вызова функции CreateEvent не существует. Тогда действительно будет создан новый объект ядра «событие» с именем EventName.
Так что будьте аккуратны с именованием объектов ядра и учитывайте, что пространство имен объектов ядра является общим для объектов всех типов.
Объекты-события могут разделяться разными процессами. Допустим, что некий процесс А создал новое событие hEvent с именем EventName. Потоки из других процессов могут получить доступ к этому объекту несколькими способами:
вызовом функции CreateEvent с передачей в параметре pszName такого же имени (эта ситуация только что рассматривалась);
наследованием дескриптора;
применением функции DuplicateHandle;
вызовом функции OpenEvent с передачей в параметре pszName такого же имени.