developing.ru - клуб программистов Здесь может быть и ваша реклама.
developing.ru >технология COM >
Михаил Безверхов
aspid@developing.ru

Обмен данными между процессами

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

Внешне, "для пользователя", примеры одинаковы - они оба имеют схожий графический интерфейс на разделяемую область. Интерфейс представляет собой EditBox в котором отображается содержимое раздеяемой области и две кнопки -"Прочитать" и "Записать". При нажатии кнопки "Прочитать" программа читает разделяемую область памяти и показывает её значение в EditBox. При нажатии кнопки "Записать" программа читает EditBox и сохраняет его значение в разделяемой области.

"Тестирование концепции" нужно производить следующим образом. Необходимо запустить несколько копий одного и того же примера. В окне одной копии примера следует ввести какую-нибудь строку с клавиатуры и нажать кнопку "Записать". Программа поместит эту строку в разделяемую память и она, в момент помещения, станет доступна всем прочим копиям этого же примера, которые используют ту же самую проекцию. Но, наличие строки в разделяемой памяти не приводит к тому, что эта строка автоматически же появляется в EditBox других копий, ведь разделяется область памяти, а не управление локальным control! Для того, чтобы увидеть эти данные в другом процессе надо в другой копии примера нажать кнопку "Прочитать" - программа прочитает разделяемую область и покажет её содержимое в своём EditBox. По теории оно должно быть тем же самым, каким оно было в окне EditBox того экземпляра примера, в котором последний раз нажималась кнопка "Записать". Ну, а подтверждает ли практика теорию - судить читателю...

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

В первом примере разделяемая область создаётся предложениями языка C/C++:

 #pragma data_seg ("MY_DATA") TCHAR cSharedData[100] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; #pragma data_seg () 

Инструкции препроцессора #pragma data_seg обрамляют "свой" сегмент данных. Первое предложение его "открывает", т.е. предписывает компилятору размещать данные, чьи объявления встретятся ниже по тексту в обособленном сегменте данных с именем "MY_DATA". А вот второе предложение в MSDN не упоминается - оно "закрывает" "свой" сегмент данных. Буквально его семантика означает "использовать сегмент данных по умолчанию", т.е. все данные, которые описываются предложениями ниже второй директивы #pragma будут размещаться в стандартном сегменте данных...

Термины "открытие" и "закрытие" в данном случае несколько не точны, а совсем корректно сказать, что они имеют неподходящую семантику - сегмент не создаётся до начала кодогенерации. Директива #pragma data_seg просто заставляет компилятор запоминать - в каком сегменте данных следует размещать указанный элемент данных. Поэтому ничто не запрещает "разбрасывать" эти директивы по ходу текста - приступая к кодогенерации компилятор все объявления сгруппирует соответственно затребованному сегменту. Но, хотя так сделать возможно, делать так всё же не очень хорошо - компилятор-то как раз всё запомнит правильно, но запомнит ли сам программист?

Следует особо отметить - для размещения непроинициализированных данных (т.е. таких, от которых в сегменте данных исполняемого файла требуется только упоминание об их длине, но не требуется сохранять их начального содержимого) компилятор всегда использует сегмент с именем .bss. Поэтому, если объявленные в собственном сегменте элементы данных не проинициализировать, они в собственном сегменте не окажутся - они окажутся в .bss И пример, соответственно, работать не будет. Не будет работать и ваша программа, если вы не проинициализируете хотя бы один элемент данных. Это нужно знать.

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

Во-первых, для проекции запрошен регион меньший 64K. Как было упомянуто в статье, в таком случае система сама увеличивает регион до 64K. Во-вторых, для выделения памяти запрошено всего 100 байтов. Как также было упомянуто в статье, система не может выделить меньше, чем одну физическую страницу. Поэтому, хотя запрошено 100 байтов в реальности программист располагает целой страницей - для процессора Intel это 4096 байтов памяти, хоть бы для проекции и запрашивался всего один байт. Не верящие в это обстоятельство могут "под отладчиком" попытаться записать, скажем, 150-й байт строки... Но! "Зарезервировано в адресном пространстве" - 64К, а "физически предоставлено" - всего 4K. Больше в данном случае система не предоставит - её об этом не просили. Что будет наблюдаться "в оставшейся части региона"? Нужно специально сказать, что "в оставшейся части региона" не будет наблюдаться ни правильных данных (т.е. тех, которые по данному адресу записывал программист), ни исключения memory access violation. Т.е. система будет считать память "правильной" и не будет возбуждать исключения обращения по неверному адресу, но, одновременно, ничего туда записывать тоже не будет. А попытка чтения будет приносить "какие-то данные". Общее впечатление, которое при этом возникнет у программиста будет напоминать впечатление от попытки записи в ПЗУ. Это следует где-то на задворках памяти (уже своей, программистской) помнить и иметь в виду - иначе случай этот может быть очень похож на случай бедного учителя географии, который не нашёл на карте мира берингова пролива...

Здесь же можно специально заметить и один практический программистский приём, уместный в ситуациях "дескриптор, порождающий дескриптор". Сущности, которые можно закрывать при помощи функции CloseHandle обычно являются дескрипторами глобальных объектов. Поэтому можно передавать дескриптор проекции файла в другой процесс (наследованием и посредством DuplicateHandle) и, главное, счётчики использования объектов, денотируемых дескрипторами, система ведёт самостоятельно! Поэтому, если в данной программе требуется на данном файле построить только одну проекцию файла, то с целью сэкономить свои интеллектуальные усилия при написании программного завершения, можно немедленно после вызова CreateFileMapping вызвать CloseHandle для дескриптора, полученного от CreateFile. Этим дескритор файла "инвалидизируется" и доступ к нему посредством этого дескриптора будет уже невозможен. Но - файл-то не закроется, ибо его дескриптор используется и функцией CreateFileMapping. Сам файл фактически "отсоединится" тогда и только тогда, когда и CreateFileMapping "отпустит" его. По общепринятому правилу глобальный объект, денотируемый дескриптором, разрушается только тогда, когда на него исчезает последняя ссылка от выданных системой дескрипторов. Для глобального объекта не имеет даже значения в какой процесс выдавался дескриптор, т.е. CloseHandle разрушает только сам дескриптор в данном процессе.

Поскольку CreateFileMapping тоже создаёт дескриптор глобального объекта и поскольку MapViewOfFile использует этот дескриптор так же, как CreateFileMapping использует дескриптор от CreateFile, вполне возможно повторить приём - немедленно после вызова MapViewOfFile можно вызвать CloseHandle для дескриптора самого спроецированного файла, т.е. дескриптора, созданного функцией CreateFileMapping. После этого будет невозможно создавать новые view, т.к. MapViewOfFile требует дескриптора, а его мы только что разрушили. Но сама проекция файла будет разрушена только после того, как вызов UnMapViewOfFile её "отпустит"... - вовсе не обязательно CloseHandle размещать именно в деструкторе!

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

к статье
Авторские права © 2001 - 2004, Михаил Безверхов
Публикация требует разрешения автора

© 2000-2004 Клуб программистов developing.ru