ГЛАВА 17 Проецируемые в память файлы

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

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

Проецируемые файлы применяются для:

Эти области применения проецируемых файлов мы и рассмотрим в данной главе.

Проецирование в память EXE- и DLL-файлов

При вызове из потока функции CreateProcess система действует так:

  1. Отыскивает ЕХЕ-файл, указанный при вызове CreateProcess. Если файл не най ден, новый процесс не создастся, а функция возвращает FALSE.
  2. Создает новый объект ядра «процесс»
  3. Создает адресное пространство нового процесса
  4. Резервирует регион адресного пространства — такой, чтобы в него поместил ся данный ЕХЕ-файл Желательное расположение этого региона указывается внут ри самого ЕХЕ-файла По умолчанию базовый адрес ЕХЕ-файла — 0x00400000 (в 64-разрядном приложении под управлением 64-разрядпой Windows 2000 этот адрес может быть другим). При создании исполняемого файла приложе ния базовый адрес может быть изменен через параметр компоновщика /BASE.
  5. Отмечает, что физическая память, связанная с зарезервированным регионом, — ЕХЕ-файл на диске, а нс страничный файл.

Спроецировав ЕХЕ-файл на адресное пространство процесса, система обращает ся к разделу ЕХЕ-файла со списком DLL, содержащих необходимые программе функ ции. После этого система, вызывая LoadLibrary, поочередно загружает указанные (а при необходимости и дополнительные) DLL-модули. Всякий раз, когда для загрузки DLL вызывается LoadLibrary, система выполняет действия, аналогичные описанным выще в пп. 4 и 5:

  1. Резервирует регион адресного пространства - такой, чтобы в него мог поме ститься заданный DLL-файл Желательное расположение этого региона указы вается внутри самого DLL-файла. По умолчанию Microsoft Visual C++ присваи вает DLL-модулям базовый адрес 0x10000000 (в 64-разрядной DLL под управ лением 64-разрядной Windows 2000 этот адрес может быть другим). При ком поновке DLL это значение можно изменить с помощью параметра /BASE. У всех стандартных системных DLL, поставляемых с Windows, разные базовые здре ca, чтобы не допустить их перекрытия при загрузке в одно адресное простран ство
  2. Если зарезервировать регион по желательному для DLL базовому адресу не удается (из-за того, что он слишком мал либо занят каким-то еще EXE- или DLL файлом), система пытается найти другой регион. Но по двум причинам такая ситуация весьма неприятна. Во-первых, если в DLL нет информации о возмож ной переадресации (relocation information), загрузка может вообще не полу читься. (Такую информацию можно удалить из DLL при компоновке с парамет ром /FIXED. Это уменьшит размер DLL-файла, но тогда модуль должен грузить ся только по указанному базовому адресу) Во-вторых, системе приходится выполнять модификацию адресов (relocations) внутри DLL. В Windowы 98 эта операция осуществляется по мере подкачки сграниц в оперативную память. Но в Windows 2000 на это уходит дополнительная физическая память, выделяе мая из страничного файла, да и загрузка такой DLL займет больше времени.
  3. Отмечает, что физическая память, связанная с зарезервированным регионом, — DLL-файл на диске, а не страничный файл. Если Windows 2000 пришлось вы полнять модификацию адресов из-за того, что DLL не удалось загрузить по желательному базовому адресу, она запоминает, что часть физической памяти для DLL связана со страничным файлом.

Если система почему-либо не свяжет ЕХЕ-файл с необходимыми сму DLL, на эк ране появится соответствующее сообщение, а адресное пространство процесса и объект «процесс" будут освобождены При этом CreateProcess вернет FALSE; прояснить причину сбоя поможет функция GetLastError.

После увязки EXE- и DLL-файлов с адресным пространством процесса начинает исполняться стартовый код EXE-файла. Подкачку страниц, буферизацию и кэширо вание система берет на себя. Например, если код в ЕХЕ-файле переходит к команде, не загруженной в память, возникает ошибка. Обнаружив ее, система перекачивает нужную страницу кода из образа файла на страницу оперативной памяти. Затем ото бражает страницу оперативной памяти на должный участок адресного пространства процесса, тем самым позволяя потоку продолжить выполнение кода Все эти опера ции скрыты от приложения и периодически повторяются при каждой попытке про цесса обратиться к коду или данным, отсутствующим в оперативной памяти.

Статические данные не разделяются несколькими экземплярами EXE или DLL

Когда Вы создаете новый процесс для уже выполняемого приложения, система про сти открывает другое проецируемое в память представление (view) объекта "проек ция файла" (file-mapping object), идентифицирующего образ исполняемого файла, и создает новые объекты "процесс" и «поток» (для первичного потока) Этим объектам присваиваются идентификаторы процесса и потока. С помощью проецируемых в память файлов несколько одновременно выполняемых экземпляров приложения мо жет совместно использовать один и тот же код, загруженный в оперативную память. Здесь возникает небольшая проблема. Процессы используют линейное (flat) ад ресное пространство. При компиляции и компоновке программы весь ее код и дан ные объединяются в нечто, так сказать, большое и цельное Данные, конечно, отделе ны от кода, но только в том смысле, что они расположены вслед за кодом в ЕХЕ-фай ле<snoska На самом деле содержимое файла разбито на отдельные разделы (sections). Код находится в одном разделе, а глобальные переменные — в другом Разделы выравниваются по грани цам страниц Приложение определяет размер страницы через функцию GetSystemInfo. В EXE- или DLL-флйле раздел кода обычно предшествует разделу данных.

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

rihter17-1.jpg

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

rihter17-2.jpg

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

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

rihter17-3.jpg

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

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

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

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

Статические данные разделяются несколькими экземплярами EXE или DLL

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

В этом разделе мы рассмотрим метод, обеспечивающий совместное использова ние переменных всеми экземплярами EXE или DLL. Но сначала Вам понадобятся кое какие базовые сведения.

Любой образ EXE- или DLL-файла состоит из группы разделов. По соглашению имя каждого стандартного раздела начинается с точки Например, при компиляции про граммы весь код помещается в раздел .text, неинициализированные данные - в раз дел .bss, а инициализированные — в раздел .data.

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

Атрибут

Описание

READ

Разрешает чтение из раздела

WRITE

Разрешает запись в раздел

EXECUTЕ

Содержимое раздела можно исполнять

SHARED

Раздел доступен нескольким экземплярам приложения (этот атрибут отклю чает механизм копирования при записи)

Запустив утилиту DumpBin из Microsoft Visual Studio (c ключом /Headers), Вы уви дите список разделов в файле образа EXE или DLL Пример такого списка, показан ный ниже, относится к ЕХЕ-файлу.

SECTION HEADER #1 text name 11A70 virtual size 1000 virtual address 12000 size of raw data 1000 file pointer to raw data

0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 60000020 flags Code Execute Read

SECTION HEADER #2 rdata name

1F6 virtual size

13000 virtual address 1000 size of raw data 13000 file pointer to raw data

0 file poinLer lo relocation tabie 0 file pointer to line numbers 0 number ot relocations 0 number of line numbers 40000040 flags

Initialized Data Read Only

SECTION HEADER #3 .data name

560 virtual size 14000 virtual address 1000 size of raw data 14000 file pointer to raw data

0 filc pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags

Initialized Data Read Write

SECTION HtADER #4 .idata name

58D virtual size 15000 virtual address 1000 size of raw data 15000 file pointer to raw data

0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags

Initialized Data Read Write

SECTION HEADER #5 .didat name

7A2 vi rtual size 16000 virtual address 1000 size of raw data 16000 file pointer to raw data

0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags

Initialized Data Read Write

SECTION HEADER #6 .reloc name

26D virtual size 17000 virtual address 1000 size of raw data 17000 file pointer to raw data

0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 42000040 flags

Initialized Data Discardable Read Only

Summary

1000 data 1000 didat 1000 idata 1000 rdata 1000 .reloc 12000 text

Некоторые из часто встречающихся разделов перечислены в таблице ниже

Имя раздела

Описание

bss

Неинициализированные данные

CRT

Неизменяемые данные библиотеки С

data

Инициализированные данные

.debug

Отладочная информация

.didat

Таблица имен для отложенного импорта (delay imported names table)

edata

Таблица экспортируемых имен

idata

Таблица импортируемых имен

.rdata

Неизменяемые данные периода выполнения

.reloc

Настроечная информация — таблица переадресации (relocation table)

.rsrc

Ресурсы

.text

Код ЕХЕ или DLL

.tls

Локальная память потока

.xdata

Таблица для обработки исключений

Кроме стандартных разделов, генерируемых компилятором и компоновщиком, можно создавать свои разделы в EXE- или DLL-файле, используя директиву компи лятора:

#pragma data_seg("имя_раздела")

Например, можно создать раздел Shared, в котором содержится единственная пе ременная типа LONG:

#pragma data_seg("Shared") LONG g_lInstanceCount = 0;
#pragma data_seg()

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

#pragma data_seg("Shared") LONG g_lInslanceCount;
#pragma data_seg()

Однако в компиляторе Microsoft Visual C++ 6.0 предусмотрен спецификатор allo cate, который позволяет помещать неинициализированные данные в любой раздел. Взгляните на этот код:

// создаем раздел Shared и заставляем компилятор
// поместить в него инициализированные данные
#pragma data_seg("Shared")

// инициализированная переменная, по умолчанию помещается в раздел Shared
int а = 0;

// неинициализированная переменная, по умолчанию помещается в другой раздел
int b;

// сообщаем компилятору прекратить включение инициализированных данных
// в раздел Shared
#pragma data_seg()

// инициализированная переменная, принудительно помещается в раздел Shared
__declspec(allocate("Shared")) int с = 0;

// неинициализированная переменная, принудительно помещается в раздел Shared
__declspec(allocate("Shared")) int d;

// инициализированная переменная, по умолчанию помещается в другой раздел
int e = 0;

// неинициализированная переменная, по умолчанию помещается в другой раздел
int f;

Чтобы спецификатор allocate работал корректно, сначала должен быть создан соответствующий раздел. Так что, убрав из предыдущего фрагмента кода первую стро ку #pragma data_seg, Вы нс смогли бы его скомпилировать.

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

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

этом разделе должны быть общими Для этого предназначен ключ /SECTION компоновщика

/SECTION:имя,атрибуты

За двоеточием укажите имя раздела, атрибуты которого Вы хотите изменить В нашем примере нужно изменить атрибуты раздела Shared, поэтому ключ должен выг лядеть так

/SECTION:Shared,RWS

После запятой мы задаем требуемые атрибуты При этом используются такие со кращения R (READ), W (WRITE), E (EXECUTE) и S(SHARED) В данном случае мы ука чали, что раздел Shared должен быть «читаемым», «записываемым" и "разделяемым» Если Вы хотите изменить атрибуты более чем у одного раздела, указывайте ключ /SECTION для каждого такого раздела

Соответствующие директивы для компоновщика можно вставлять прямо в исходный код

#pragma comment(linker, /SECTION Shared,RWS )

Эта строка заставляет компилятор включить строку "/SECTION Shared,RWS" в осо бый раздел drectve Компоновщик, собирая OBJ-модули, проверяет этот раздел в каж дом OBJ-модуле и действует так, словно все эти строки переданы ему как аргументы в командной строке Я всегда применяю этот оченъ удобный метод перемещая файл исходного кода в новый проект, не надо изменять никаких параметров в диалоговом окне Project Settings в Visual C++

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

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

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

Трудолюбивая хакерская программа может также предпринять серию попыток угадать пароль, записывая его варианты в общую память А угадав, сможет посылать любые команды этим двум приложениям Данную проблему можно было бы решить, если бы существовал какой-нибудь способ разрешать загрузку ULL только определен ным программам Но пока эю невозможно — любая протрамма, вызвав IoadLibrary, способна явно загрузить любую DLL

Программа-пример Applnst

Эта программа, "17 AppInst.exe" (см листинг на рис 17-1), демонстрирует, как выяс нить, сколько экземпляров приложения уже выполняется в системе Файлы исходно го кода и ресурсов этои программы находятся в каталоге 17-AppInst на компакт-дис

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

rihter17-4.jpg

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

rihter17-5.jpg

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

Где-то в начале файла AppInst.cpp Вы заметите следующие строки:

// указываем компилятору поместить эту инициализированную переменную
// в раздел Shared, чтобы она стала диступной всем окземплярам программы

#pragma data_seg("Shared")
volatile LONG g_lApplicationInstances = 0;
#pragma data_seg()

// указываем компоновщику, что раздел Shared должен быть
// читаемым, записываемым и разделяемым
#pragma comment(linker, "/Section.Shared,RWS")

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

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

Когда на экране появляется диалоговое окпо каждого экземпляра программы, вызывается функция Dlg__OnJnitDialog. Она рассылает всем окнам верхнего уровня зарегиорированное оконное сообщение (идентификатор которого содержится в переменной g_aMsgAppInstCоuntUpdate).

PostMessage(HWND_BROADCAST, g_aMsgAppInstCountUpdate, 0, 0);

Это сообщение игнорируется всеми окнами в системе, кроме окон AppInst. Когда его принимает одно из окон нашей программы, код в DlgProc просто обновляет в диалоговом окнс значение, отражающее текущее количество экземпляров (а эта ве личина хранится в переменной g_lApplicattonInstances).

AppInst

 

Файлы данных, проецируемые в память

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

Метод 1: один файл, один буфер

Первый (и теоретически простейший) метод — выделение блока памяти, достаточ ного для размещения всего файла Открываем файл, считываем eго содержимое в блок памяти, закрываем. Располагая в памяти содержимым файла, можно поменять первый

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

Этот довольно простой в реализации метод имеет два существенных недостатка Во-первых, придется выделить блок памяти такого же размера, что и файл. Это тер пимо, если файл небольшой. А если он занимает 2 Гб? Система просто не позволит приложению передать такой объем физической памяти. Значит, к болыпим файлам нужен совершенно иной подход.

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

Метод 2: два файла, один буфер

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

Этот метод посложнее первого, зато позволяет гораздо эффективнее использовать память, так как требует выделения лишь 8 Кб, Но и здесь не без проблем, и вот двс главных.. Во-первых, обработка идет медленнее, чем при первом методе: на каждой итерации перед считыванием приходится находить нужный фрагмент исходного файла. Во-вторых, может понадобиться огромное пространство па жестком диске. Если длина исходного файла 400 Мб, новый файл постепенно вырастет до этой вели чины, и перед самым удалением исходного файла будет занято 800 Мб, т. e. на 400 Мб больше, чем следовало бы. Так что все пути ведут... к третьему методу

Метод 3: один файл, два буфера

Программа инициализирует два раздельных буфера, допустим, по 8 Кб и считывает первые 8 Кб файла в один буфер, а последние 8 Кб — в другой. Далее содержимое обоих буферов обменивается в обратном порядке и первый буфер записывается в конец, а второй — в начало того же файла. На каждой итерации программа переме щает восьмикилобайтовые блоки из одной половины файла в другую. Разумеется, нужно предусмотреть какую-то обработку на случай, ссли длина файла не кратна 16 Кб, и эта обработка будет куда сложнее, чем в предыдущем методе. Но разве это испугает опытного программиста?

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

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

Метод 4: один файл и никаких буферов

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

Использование проецируемых в память файлов

Для этого нужно выполнить три операции:

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

2. Создать объект ядра "проекция файла", чтобы сообщить системе размер фай ла и способ доступа к нему.

3. Указать системе, как спроецировать в адресное пространство Вашего процес са объект «проекция файла» — целиком или частично.

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

1. Сообщить системе об отмене проецирования на адресное пространство про цесса объекта ядра "проекция файла".

2. Закрыть этот объект.

3. Закрыть объект ядра "файл".

Детальное рассмотрение этих операций — в следующих пяти разделах.

Этап1: создание или открытие объекта ядра «файл»

Для этого Вы должны применять только функцию CreateFile

HANDLE CreateFile( PCSTR pszFileName, DWORD dwDesiredAccess, DWORD dwShareMode, PSECURITY_AIIRIBUTES psa, DWORD dwCreationDisposition, DWORD dwFlagsAndAttribules, HANDLE hTemplateFile);

Как видите, у функции CrealeFile довольно много параметров. Здесь я сосредото чусь только на первых трех: pszFileName, dwDesiredAccess и dwSbareMode.

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

Значение

Описание

0

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

GENERIC _READ

Чтение файла разрешено

GENERIC_WRITE

Запись в файл разрешена

GENERIC_READ | ENERIC_WRITE

Разрешено и то и другое

Создавая или открывая файл данных с намерением использовать его в качестве проецируемого в память, можно установить либо флаг GENERIC_READ (только для чтения), либо комбинированный флаг GENERIC_READ | GENУRIC_WRITE (чтение/ча пись)

Третий параметр, dwShareMode, указывает тип совместного доступа к данному файлу(см следующуютаблицу)

Значение

Описание

0

Другие попытки открыть файл закончатся неудачно

FILE_SHARE_REAU

Попытка постороннего процесса открыть файл с флагом GENERIC_WRITE не удается

FILb_SHARF_WRlTE

Попьлка постороннего процесса открыть файл с флагом GENERIC_READ не удается

FILE SHARE RFAD | FILE_SHARE_WRTTE

Посторонний процесс может открывать файл без ограничений

Создав или открыв указанный файл, CreateFile возвращает его описатель, в ином случае — идентификатор INVALID_HANDLE_VALUE

NOTE:
Большинство функций Windows, возвращающих те или иные описатели, при неудачном вызове дает NULL Ho CreateFile — исключение и в таких случаях возвращает идентификатор INVALID_HANDIF_VALUE, определенный как ((HANDLE) -1)

Этап 2: создание объекта ядра «проекция файла»

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

HANDLE CreateFileMapping( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD fdwProtect, DWOPD dwMaximumSizeHigh, DWORD dwMaximumSizcLow, PCSTR pszName);

Первый параметр, hFile, идентифицирует описатель файла, проецируемою на ад реснос пространство процесса этот описатель Вы получили после вызова CreateFile Параметр psa — указатель на структуру SECURITY_ATTRIBUTES, которая относится к обьекту ядра "проекция файла", для установки защиты по умолчанию ему присваива ется NULL

Как я уже говорил в начале этой главы, создание файла, проецируемого в память, аналогично резервированию региона адресного пространства с последующей пере дачей сму физической памяти Разница лишь в том, что физическая память для про ецируемого файла — сам файл на диске, и для него не нужно выделять пространство в страничном файле. При создании объекта «проекция файла» система не резервиру ет регион адресного пространства и не увязывает его с физической памятью из фай ла (кяк это сделать, я расскажу в следующем разделе). Но, как только дело дойдет до отображения физической памяти на адресное пространство процесса, системе пона добится точно знать атрибут защиты, присваиваемый страницам физической памя ти Поэтому в fdwProteсе надо указать желательные атрибуты защиты. Обычно ис пользуется один из перечисленных в следующей таблице.

Атрибут защиты

Описание

PAGE_READONLY

Отобразив объект «проекция файла» на адресное пространство, можно считывать данные из файла. При этом Вы должны были пе редать в CreateFile флаг GENERIC_READ.

PAGE_READWRITE

Отобразив объект «проекция файла» на адресное пространство, можно считывать данные из файла и записывать их При этом Вы должны были передать в CreateFile комбинацию флагов GENERIC_READ | GENERIC_WRITE.

PAGE_WRITECOPY

Отобразив объект "проекция файла" на адресное пространство, можно считывать данные из файла и записывать их. Запись приве дет к созданию закрытой копии страницы При этом Вы должны были передать в CreateFile либо GENERIC_READ, либо GENERIC_READ | GENERIC_WRITE

WINDOWS 98
Windows 98 функции CreateFileMapping можно передать флаг PAGE_WRITE COPY; тем самым Вы скажете системе передать физическую память из странич ного файла. Эта память резервируется для копии информации из файла дан ных, и лишь модифицированные страницы действительно записываются в страничный файл. Изменения не распространяются на исходный файл данных. Результат применения флага PAGE_WRITECOPY одинаков в Windows 2000 и в Windows 98

Кроме рассмотренных выше атрибутов защиты страницы, существует еще и че тыре атрибута раздела; их можно ввести в параметр fdwProtect функции CreateFile Mapping побитовой операцией OR. Раздел (section) — всего лишь еще одно название проекции памяти.

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

WINDOWS 98
Windows 98 игнорирует флаг SEC_NOCACHE.

Второй атрибут, SEC_IMAGE, указывает системе, что данный файл является пере носимым исполняемым файлом (portable executable, PE). Отображая его на адресное пространство процесса, система просматривает содержимое файла, чтобы опреде лить, какие атрибуты защиты следует присвоить различным страницам проецируе

мого образа (mapped image). Например, раздел кода РЕ-файла (text) обычно проеци руется с атрибутом PAGE_EXECUTE_READ, тогда как раздел данных этого же файла (.data) — с атрибутом PAGE_READWRITE Атрибут SEC_IMAGE заставляет систему спро ецировать образ файла и автоматически подобрать подходящие атрибуты защиты страниц

WINDOWS 98
Windows 98 игнорирует флаг SEC_IMAGE.

Последние два атрибута (SEC_RESERVE и SEC_COMMIT) взаимоисключают друг друга и неприменимы для проецирования в память файла данных. Эти флаги мы рас смотрим ближе к концу главы. CreateFileMapping их игнорирует

Следующие два параметра этой функции (dwMaximumSizeHigh и dwMaximum SizeLow) самые важные. Основное назначение CreateFileMapping — гарантировать, что объекту "проекция файла" доступен нужный объем физической памяти Через эти параметры мы сообщаем системе максимальный размер файла в байтах. Так как Win dows позволяет работать с файлами, размеры которых выражаются 64-разрядными числами, в параметре dwMaximumSizeHigh указываются старшие 32 бита, а в dwMaxi mumSizeI.ow - младшие 32 бита этого значения. Для файлов размером менее 4 Гб dwMaximumSizeHigh всегда равен 0. Наличие 64-разрядного значения подразумевает, что Windows способна обрабатывать файлы длиной до l6 экзабайтов

Для создания объекта «проекция файла" таким, чтобы он отражал текущий раз мер файла, передайте в обоих параметрах нули. Так же следует поступить, если Вы собираетесь ограничиться считыванием или как-то обработать файл, не меняя его раз мер Для дозаписи данных в файл выбирайте его размер максимальным, чтобы оста вить пространство «для маневра» Если в данный момент файл на диске имеет нуле вую длину, в параметрах dwMaximumSizeHigh и dwMaximumSizeLow нельзя передавать нули Иначе система решит, что Вам нужна проекция файла с объемом памяти, рав ным 0. А это ошибка, и CreateFileMapping вернет NULL

Если Вы еще следите за моими рассуждениями, то, должно быть, подумали: что-то тут нс все ладно. Очень, конечно, мило, что Windows поддерживает файлы и их про екции размером вплоть до 16 экзабайтов, но как, интересно, спроецировать такой файл на адресное пространство 32-разрядного процесса, ограниченное 4 Гб, из ко торых и использовать-то можно только 2 Гб? На этот вопрос я отвечу в следующем разделе. (Конечно, адресное пространство 64-разрядного процесса, размер которого составляет 16 экзабайтов, позволяет работать с еще большими проекциями файлов, но аналогичное ограничение существует и там)

Чтобы досконально разобраться, как работают функции CreateFtle и CreateFile Mapping, предлагаю один эксперимент Возьмите код, приведенный ниже, соберите его и запустите под отладчиком. Пошагово выполняя операторы, переключитесь в окно командного процессора и запросите содержимое каталога «C:\» командой dir Обратите внимание на изменения, происходящие в каталоге при выполнении каждо го оператора.

int WINAPI _tWinMain(HINSIANCE hinstExe, HINSTANCE, PISTR pszCmdLine, int nCmdShow)
{

// перед выполнением этого оператора, в каталоге C:\
// еще нет файла "MMFTest.dat"

HANOLE hfile = CreateFile("C.\\MMFTest dat", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE_, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

// перед выполнением этого оператора файл MMFTest.dat существует,
// но имеет нулевую длину
HANDLE htilemap = CreateFileMapping(hfile, NULL, PAGE_READWRITE, 0, 100, NULL);

// после выполнения предыдущею оператора размер файла MMFTest.dat
// возрастает до 100 байтов

// очистка

CloseHandle(hfilemap);
CloseHandle(hfile);

// по завершении процесса файл MMFTest.dat останется
// на диске и будет иметь длину 100 байтов

return(0);

}

Вызов CreateFileMapping с флагом PAGE_READWRITE заставляет систему проверять, чтобы размер соответствующего файла данных на диске был нс меньше, чем указано в параметрах dwMaximumSizeHigh и dwMaximumSizeLow. Если файл окажется меньше заданного, CreateFileMapping увеличит его размер до указанной величины. Это дела ется специально, чтобы выделить физическую память перед использованием файла в качестве проецируемого в память. Если объект "проекция файла" создан с флагом PAGE_READONLY или PAGE_WRITECOPY, то размер, переданный функции Create FileMapping, не должен превышать физический размер файла на диске (так как Вы не сможете что-то дописать в файл).

Последний параметр функции CreateFileMapping pszName — строка с нулевым байтом в конце; в ней указывается имя объекта "проекция файла", которое использу ется для доступа к данному объекту из другого процесса (пример см, в главе 3). Но обычно совместное использование проецируемого в память файла не требуется, и поэтому в данном параметре передают NULL.

Система создает объект «проекция файла» и возвращает его описатель в вызвав ший функцию поток. Если объект создать не удалось, возвращается нулевой описа тель (NULL). И здесь еще раз обратите внимание на отличительную особенность фун кции CreateFile — при ошибке она возвращает не NULL, а идентификатор INVALID_ HANDLE_VALlJE (определенный как - 1).

Этап 3: проецирование файловых данных на адресное пространство процесса

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

PVOID MapViewOfFile( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap);

Параметр hFileMappingObject идентифицирует описатель объекта "проекция фай ла", возвращаемый предшествующим вызовом либо CreateFtleMapping, либо OpenFile Mapping (ее мы рассмотрим чуть позже) Параметр dwDesiredAccess идентифицирует вид доступа к данным Bce правильно придется опять указывать, как именно мы хо тим обращалься к файловым данным Можно задать одно из четырех значений, опи санных в следующей таблице

Значение

Описание

FILE_MAP_WRITE

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

FILE MAF_READ

Файловые данные можно только считывать Вы должны были вызвать CreateFileMapping с любым из следующих атрибутов PAGE_READONLY, PAGE_READWRITE или PAGE_WRITECOPY

FILE_MAP_ALL_ACCESS

То же, что и FILE_MAP_WRITE

FILE_MAP_COPY

Файловые данные можно считывать и записывать, но запись приводит к созданию закрытой копии страницы Вы должны были вызвать CrealeFileMapping с любым из следующих атрибу тов PAGE_READONIY, PAGE_READWRITE или РАСЕ_WRITECOPY (Windows 98 требует вызывать CreateFileMapping с атрибутом PACE_WRITECOPY)

Кажется странным и немного раздражает, что Windows требует бесконечно ука зывать все эти атрибуты защиты Могу лишь предположить, что это сделано для того, чтобы приложение максимально полно контролировало защиту данных

Остальные три параметра относятся к резервированию региона адресного про странства и к отображению на него физической памяти При этом необязательно проецировать на адресное пространство весь файл сразу Напротив, можно спроеци ровать лишь малую его часть, которая в таком случае называется представлением (view) — теперь-то Вам, наверное, понятно, откуда произошло название функции MapViewOfFile

Проецируя на адресное пространство процесса представление файла, нужно сде лать двс вещи Во-первых, сообщить системе, какой байт файла данных считать в представлении первым Для этого предназначены параметры dwFileOffsetHigh и dwFile OffsetLow Поскольку Windows поддерживает файлы длиной до 16 экзабайтов, прихо дится определять смещение в файле как 64 разрядное число старшие 32 бита пере даются в параметре dwFileOffsetHigh, а младшие 32 бита — в параметре dwFileOffsetLow Заметьте, что смещение в файле должно быть кратно гранулярности выделения па мяти в данной системе (В настоящее время во всех реализациях Windows она состав ляет 64 Кб) О гранулярности выделения памяти см раздел "Системная информация" в ]лаве 14

Во-вторых, от Baс потребуется указать размер представления, т.e. сколько байтов файла данных должно быть спроецировано на адресное пространство Это равносиль но тому, как если бы Вы задали размер региона, резервируемого в адресном простран стве Размер указывается впараметре dwNumberOfBytesToMap Если этот параметр ра вен 0, система попытается спроецировать представление, начиная с указанного сме щения и до конца файла

WINDOWS 98
Windows 98, если MapViewOfFile не найдет регион, достаточно большой для размещения всего объекта «проекция файла», возвращается NULL — независи мо от того, какой размер представления был запрошен

WINDOWS 2000
B Windows 2000 функция MapViewOfFile ищет регион, достаточно большой для размещения запрошенного представления, не обращая внимания на размер самого объекта "проекция файла".

Если при вызове MapViewOfFile указан флаг FILE_MAP_COPY, система передает физическую память из страничного файла. Размер передаваемого пространства оп ределяется параметром dwNumberOfBytesToMap. Пока Вы лишь считываете данные из представления файла, страницы, переданные из страничного файла, пе используют ся, Но стоит какому-нибудь потоку в Вашем процессе совершить попытку записи по адресу, попадающему в границы представления файла, как система тут же берет из страничного файла одну из переданных страниц, копирует на нее исходные данные и проецирует ее на адресное пространство процесса. Так что с этого момента пото ки Вашего процесса начинают обращаться к локальной копии данных и теряют дос туп к исходным данным.

Создав копию исходной страницы, система меняет ее атрибут защиты с PAGE_WRI TECOPY на PAGE_READWRITE. Рассмотрим пример:

// открываем файл, который мы собираемся спроецировать
HANDLE hFile = CreaTeFile(pszFileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

// создаем для файла объект "проекция файла"
HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_WRITECOPY, 0, 0, NULL);

// Проецируем представление файла с атрибутом "копирование при записи";
// система передаст столько физической памяти из сфаничного файла,
// сколько нужно для размещения всего файла. Первоначально все страницы
// в представлении получат атрибут PAGE_WRITECOPY.
PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_COPY, 0, 0, 0);

// считываем байт из представления файла
BYTE bSomeByte = pbFile[0];

// при чтении система не трогает страницы, переданные из страничного файла;
// страница сохраняет свой атрибут PAGE_WRITECOPY

// записываем байт в представление файла
pbFile[0] = 0;

// При первой записи система берет страницу, переданную из страничного файла,
// копирует исходное содержимое страницы, расположенной по запрашиваемому адресу
// в памяти, и проецирует новую страницу (копию) на адресное пространство процесса.
// Новая страница получает атрибут PAGE_READWRITE.

// записываем еще один байт в представление файла
pbFile[1] = 0;

// поскольку теперь байт располагается на странице с атрибутом PAGE_RFADWRITE,
// система просто записывает его на эту страницу (она связана со страничным файлом)

// закончив работу с представлением проецируемого файла, прекращаем проецирование;
// функция UnmapViewOfFile обсуждается в следующем разделе
UnmapViewOfFile(pbFile);

// вся физическая память, взятая из страничного файпа, возвращается системе;
// все, что было записано на эти страницы, теряется

// "уходя, гасите свет"
CloseHandle(hFileMapping);
CloseHandle(hFile);

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

Этап 4: отключение файла данных от адресного пространства процесса

Когда необходимость в данных файла (спроецированного на регион адресного про странства процесса) отпадет, освободите регион вызовом:

BOOL UnmapViewOfFile(PVOID pvBaseAddress);

Ее единственный параметр, pvBaseAddress, указывает базовый адрес возвращаемо го системе региона. Он должен совпадать со значением, полученным после вызова MapViewOfFile. Вы обязаны вызывать функцию UnmapViewOfFile. Если Вы не сделаете этoro, регион не освободится до завершения Вашего процесса. И еще: повторный вызов MapVietvOfFile приводит к резервированию нового региона в пределах адрес ного пространства процесса, но ранее выделенные регионы не освобождаются.

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

BOOL FlushViewOfFile( PVOID pvAddress, SIZE_T dwNuuiberOfBytesToFlush);

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

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

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

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

екция файла» с атрибутом PAGE_READWRITE и спроецировать его представление на адресное пространство процесса с флагом FILE_MAP_WRITE. Затем просмотреть пер вое представление, отыскивая страницы с атрибутом PAGE_READWRITE. Найдя стра ницу с таким атрибутом. Вы анализируете ее содержимое и решаете: записывать ее или нет Если обновлять файл не нужно, Вы продолжаете просмотр страниц. А для сохранения страницы с измененными данными достаточно вызвать MoveMemory и скопировать страницу из первого представления файля во второе. Поскольку второе представление создано с атрибутом PAGE_READWRITE, функция MoveMemory обновит содержимое дискового файла. Так что этот метод вполне пригоден для анализа изме нений и сохранения их в файле.

WINDOWS 98
Windows 98 нс поддерживает атрибут защиты «копирование при записи», по этому при просмотре первого представления файла, проецируемого в память, Вы не сможете проверить страницы по флагу PAGE_READWRITE Вам придется разработать свой метод.

Этапы 5 и 6: закрытие объектов «проекция файла» и «файл»

Закончив работу с любым открытым Вами объектом ядра, Вы должны его закрыть, иначе в процессе начнется утечка ресурсов. Конечно, по завершении процесса сис тема автоматически закроет объекты, оставленные открытыми Но, если процесс по работает еще какое-то время, может накопиться слишком много незакрытых описа телей. Поэтому старайтесь придерживаться правил хорошего тона и пишите код так, чтобы открытые объекты всегда закрывались, как только они станут не нужны. Для закрытия объектов «проекция файла» и «файл» дважды вызовите функцию CloseHandle. Рассмотрим это подробнее на фрагменте псевдокода.

HANDLE hFile = CreateFile(...);
HANDLE hFileMapping = CreateFileMapping(hFile,...);
PVOID pvFilfi = MapViewOfFile(hFileMapping, );

// работаем с файлом, спроецированным в память
UnmapViewOfFile(pvFile);

CloseHandle(hFileMapping);
CloseHandle(hFile);

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

HANDLE hFile = CreateFile( . );
HANDLE hFileMapping = CreateFileMapping(hFile, );

CloseHandle(hFile);

PVOID pvFile = MapViewOfFile(hFileMapping,...);

CloseHandle(hFileMapping);

// работаем с файлом, спроецированным в память
UnmapViewOfFile(pvFile);

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

данных на адресное пространство процесса. Поскольку система увеличивает внутрен ние счетчики объектов "файл" и «проекция файла», их можно закрыть в начале кода, тем самым исключив возможную утечку ресурсов.

Если Вы будете создавать из одного файла несколько объектов "проекция файла" или проецировать несколько представлений этого объекта, применить функцию CloseHandle в начале кода не удается — описатели еще понадобятся Вам для дополни тельных вызовов CreateFileMapping и MapViewOfFile

Программа-пример FileRev

Эта программа, «17 FileRev.exe» (см. листинг на рис. 17-2), демонстрирует, как с помо щью механизма проецирования записать в обратном порядке содержимое текстово го ANSI- или Unicode-файла. Файлы исходного кода и ресурсов чтой программы на ходятся в каталоге 17-FileRev на компакт-диске, прилагаемом к книге. После запуска FileRev на экране появляется диалоговое окно, показанное ниже.

rihter17-6.jpg

Выбрав имя файла и щелкнув кнопку Reverse File Contents, Вы активизируете фун кцию, которая меняет порядок символов в файле на обратный, Программа коррект но работает только с текстовыми файлами. В какой кодировке создан текстовый файл (ANSI или Unicode), FileRev определяет вызовом IsTextUnicode (см. главу 2).

WINDOWS 98
В Windows 98 функция IsTextUnitode определена, но не реализована, она про сто возвращает FALSE, а последующий вызов GetLastError дает ERROR_CALL_ NOT_IMPLEMENTED. Это значит, что программа FileRev, выполняемая в Win dows 98, всегда считает, что файл содержит текст в ANSI-кодировке.

После щелчка кнопки Reverse File Contents программа создает копию файла с именем FileRev.dat, Делается это для того, чтобы не испортить исходный файл, изме нив порядок следования байтов на обратный. Далее программа вызывает функцию FileReverse — она меняет порядок байтов на обратный и после этого вызывает Create File, открывая FileRev.dat для чтения и записи

Как я уже говорил, простейший способ «перевернуть* содержимое файла — выз вать функцию _strrev из библиотеки С. Но для этого последний символ в строке дол жен быть нулевой. И поскольку текстовые файлы не заканчиваются нулевым симво лом, программа FileRev подписывает его в конец файла. Для этого сначала вызывает ся функция GetFileSize:

dwFileSize = GetFileSize(hFile, NULL);

Теперь, вооружившись знанием длины файла, можно создать объект "проекция файла", вызвав CreateFileMapping. При этом размер объекта равен dwFileSize плюс размер «широкого» символа, чтобы учесть дополнительный нулевой символ в конце файла. Создав объект "проекция файла", программа проецирует на свое адресное пространство представление этого объекта. Переменная pvFile содержит значение, возвращенное функцией MapViewOfFile, и указывает на первый байт текстового файла.

Следующий шаг — запись нулевого символа в конец файла и реверсия строки:

PSTR pchANSI = (PSFR) pvFile;
pchANSI[dwFileSize / sizeof(CHAR)] = 0;

// "переворачиваем" содержимое файла
_strrev(pchANSI);

В текстовом файле каждая строка завершается символами возврата каретки ("\r") и перевода строки ('\n') К сожалению, после вызова функции _strrev эти символы тоже меняются местами. Поэтому для загрузки преобразованного файла в текстовый ре дактор придется заменить все пары «\n\r» на исходные «\r\n». B программе этим за нимается следующий цикл.

while (pchANSI != NULL)
{

// вхождение найдено

*pchANSI++ = '\r ; // заменяем '\n' на '\r'

*pchANSI++ = '\n', // заменяем '\r на \n'

pchANSI = strchr(pchANSI, \n');

// ищем следующее вхождение

}

Закончив обработку файла, программа прекращает отображение на адресное про странство представления объекта «проекция файла» и закрывает описатель всех объ ектов ядра Кроме того, программа должна удалить нулевой символ, добавленный в конец файла (функция strrev не меняет позицию этого символа) Если бы програм ма не убрала нулевой символ, то полученный файл оказался бы на 1 символ длиннее, и тогда повторный запуск программы FileRev не позволил бы вернуть этот файл в исходное состояние Чтобы удалить концевой нулевой символ, надо спуститься на уровень ниже и воспользоваться функциями, предназначенными для работы непос редственно с файлами на диске.

Прежде всего установите указатель файла в требуемую позицию (в данном слу чае — в конец файла) и вызовите функцию SetEndOfFile:

SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN);
SetEndOfFile(hFile);

rihter17-7.jpg

NOTE:
Функцию SetEndOfFile нужно вызывать после отмены проецирования представ ления и закрытия объекта «проекция файла», иначе она вернет FALSE, а функ ция GetLastError — ERROR_USER_MAPPED_FILE. Данная ошибка означает, что операция перемещения указателя в конец файла невозможна, пока этот файл связан с объектом "проекция файла".

Последнее, что делает FileRev, — запускает экземпляр Notepad, чтобы Вы могли увидеть преобразованный файл. Вот как выглядит результат работы программы FileRev применительно к собственному файлу FileRev.cpp.

FileRev

 

Обработка больших файлов

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

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

__int64 CountOs(void)
{

// начальные границы представлений всегда начинаются no адресам,
// кратным гранулярности выделения памяти
SYSTEM_INFO sinf;
GetSystemInfo(&sinf);

// открываем файл данных
HANOLE hFile = CreateFile( "С:\\HugeFile.Big , GENERIC_READ, FILE_SHARE_READ NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL SCAN, NULL);

// создаем объект проекция файла
HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
DWORD dwFileSizeHigh;

__int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh);
qwFileSize += (((__int64) dwFileSizeHigh) << 32);

// доступ к описателю объекта файл нам больше не нужен
CloseHandle(hFile);

__int64 qwFileOffset = 0;
qwNumOfOs = 0;

while (qwFileSize > 0)
{

// определяем, сколько байтов надо спроецировать
DWORD dwBytesInBlock = sinf.dwAllocationGranularity;

if (qwFileSize < sinf.dwAllocationGranularity)

dwBytesInBlock = (DWORD)qwFileSize;

PBYTE pbFile = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_READ, (DWORD) (qwFileOffset >> 32), // начальный байт (DWORD) (qwFileOffset & 0xFFFFFFFF), // в файле dwBytesInBlock); // число проецируемых байтов

// подсчитываем количество нулевых байтов в этом блоке
for (DWORD dwByte = 0; dwByte < dwBytesInBlock; dwByte++)
{

if (pbFilfe[dwByte] == 0)

qwNumOfOs++;

}

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

UnmapViewOfFiie(pbFile);

// переходим к следующей группе байтов в файле
qwFileOffset += dwBytesInBlock;
qwFileSize -= dwBytesInBlock;

}

CloseHandle(hFileMapping);

return(qwNumOfOs);

}

Этот алгоритм проецирует представления по 64 Кб (в соответствии с грануляр ностью выделения памяти) или менее Кроме того, функция MapViewOfFile требует, чтобы передаваемое ей смещение в файле тоже было кратно гранулярности выделе ния памяти. Подпрограмма проецирует на адресное пространство сначала одно пред ставление, подсчитывает в нем количество нулей, затем переходит к другому пред ставлению, и все повторяется. Спроецировав и просмотрев все 64-килобайтовые бло ки, подпрограмма закрывает объект «проекция файла».

Проецируемые файлы и когерентность

Система позволяет проецировать сразу несколько представлений одних и тех же файловых данных. Например, можно спроецировать в одно представление первые 10 Кб файла, а затем — первые 4 Кб того же файла в другое представление Пока Вы проецируете один и тот же объект, система гарантирует когерентность (согласован ность) отображаемых данных. Скажем, если программа изменяет содержимое файла в одном представлении, это приводит к обновлению данных и в другом. Так проис ходит потому, что система, несмотря па многократную проекцию страницы на вир туальное адресное пространство процесса, хранит данные на единственной страни це оперативной памяти Поэтому, ссли представления одного и того же файла дан ных создаются сразу несколькими процессами, данные по-прежнему сохраняют ко герентность — ведь они сопоставлены только с одним экземпляром каждой страни цы в оперативной памяти Bcc это равносильно тому, как если бы страницы опера тивной памяти были спроецированы на адресные пространства нескольких процес сов одновременно.

NOTE:
Windows позволяет создавать несколько объектов «проекция файла», связан ных с одним и тем же файлом данных. Но тогда у Вас не будет гарантий, что содержимое представлений этих объектов когерентно Такую гарантию Win dows дает только для нескольких представлений одного объекта «проекция файла".

Кстати, функция CreateFile позволяет Вашему процессу открывать файл, проеци руемый в память другим процессом После этого Ваш процесс сможет считывать или записывать данные в файл (с помощью функций ReadFite или WriteFile), Разумеется, при вызовах упомянутых функций Ваш процесс будет считывать или записывать дан ные не в файл, а в некий буфер памяти, который должен быть создан именно этим процессом; буфер не имеет никакого отношения к участку памяти, используемому для проецирования данного файла. Но надо учитывать, что, когда два приложения откры вают один файл, могут возникнуть проблемы. Дсло в том, что один процесс может вызвать ReadFile, считать фрагмент файла, модифицировать данные и записать их обратно в файл с помощью WriteFile, а объект «проекция файла», принадлежащий вто рому процессу, ничего об этом не узнает. Поэтому, вызывая для проецируемого фай ла функцию CreateFile, всегда указывайте нуль в параметре dwShareMode. Тем самым Вы сообщите системе, что Вам нужен монопольный доступ к файлу и никакой посто ронний процесс не должен его открывать.

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

Базовый адрес файла, проецируемого в память

Помните, как Вы с помощью функции VirtualAlloc указывали базовый адрес региона, резервируемого в адресном пространстве? Примерно так же можно указать системе спроектировать файл по определенному адресу — только вместо функции MapView PVOID нужна MapViewOfFileEx;

PVOID MapViewOfFileEx( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap, PVOID pvBaseAddress);

Все параметры и возвращаемое этой функцией значение идентичны применяе мым в MapViewOfFile, кроме последнего параметра — pvBaseAddress. B нем можно за дать начальный адрес файла, проецируемого в память Как и в случае VirtualAlloc, ба повый адресдолжен быть кратным гранулярности выделения памяти в системе (обыч но 64 Кб), иначе MapViewOfFileEx вернет NULL. сообщив тем самым об ошибке.

Если Вы укажете базовый адрес, не кратный гранулярности выделения памяти, то MapViewOfFileEx в Windows 2000 завершится с ошибкой, и GetLastError вернет код 1132 (ERROR_MAPPED_ALIGNMENT) а в Windows 98 базовый адрес будет округлен до бли жайшего меньшего значения, кратного гранулярности выделения памяти.

Если система не в состоянии спроецировать файл по этому адресу (чаще всего из за того, что файл слишком велик и мог бы перекрыть другие регионы зарезервиро ванного адресного пространства), функция также возвращает NULL B этом случае она не пытается подобрать диапазон адресов, подходящий для данного файла Но если Вы укажете NUI,L в параметре pvBaseAddress, она поведет себя идентично MapViewOfFile.

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

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

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

А можно и так Процесс, создающий связанный список, должен записывать в каж дый узел смещение следующего узла в пределах адресного пространства. Тогда про грамма, чтобы получить доступ к каждому узлу, будет суммировать это смещение с базовым адресом проецируемого файла. Несмотря на простоту, этот способ не луч ший: дополнительные операции замедлят работу программы и увеличат объем ее кода (так как компилятор для выполнения всех вычислений, естественно, сгенерирует до полнительный код) Кроме того, при этом способе вероятность ошибок значительно выше. Тем не менее он имеет право на существование, и поэтому компиляторы Micro soft поддерживают указатели со смещением относительно базового значения (based pointers), для чего предусмотрено ключевое слово _based

WINDOWS 98
В Windows 98 при вызове MapViewOfFileEx следует указывать адрес в диапазо не от 0x80000000 до 0xBFFFFFFF, иначе функция вернет NULL

WINDOWS 2000
В Windows 2000 при вызове MapViewOfFileEx следует указывать адрес в грани цах пользовательского раздела адресного пространства процесса, иначе фун кция вернет NULL.

Особенности проецирования файлов на разных платформах

Механизм проецирования файлов в Windows 2000 и Windows 98 реализован по-раз ному Вы должны знать об этих отличиях, поскольку они могут повлиять на код про грамм и целостность используемых ими данных

В Windows 98 представление всегда проецируется на раздел адресного простран ства, расположенный в диапазоне от 0x80000000 до 0xBFFFFFFF. Значит, после успеш ного вызова функция MapViewOfFile вернет какой-нибудь адрес из этого диапазона. Но вспомните: данные в этом разделе доступны всем процессам. Если один из про цессов отображает сюда представление объекта «проекция файла, то принадлежащие этому объекту данные физически доступны всем процессам, и уже неважно: проеци руют ли они сами представление того же объекта. Если другой процесс вызывает MapViewOJFile, используя тот же объект «проекция файла», Windows 98 возвращает адрес памяти, идентичный тому, что она сообщила первому процессу. Поэтому два процесса обращаются к одним и тсм же данным и представления их объектов коге рентны.

В Windows 98 один процесс может вызвать MapViewOJFile и, воспользовавшись какой-либо формой межпроцессной связи, передать возвращенный ею адрес памяти потоку другого процесса. Как только этот поток получит нужный адрес, ему уже нич то не помешает получить доступ к тому же представлению объекта "проекция фай ла". Но прибегать к такой возможности не следует по двум причинам:

Чтобы второй процесс получил доступ к представлению проецируемого файла, его поток тоже должен вызвать MapViewOfFile Тогда система увеличит счетчик числа пользователей объекта "проекция файла". И если первый процесс обратится к Unmap ViewOfFile, регион адресного пространства, занятый представлением, не будет осво божден, пока второй процесс тоже не вызовет UnmapViewOfFile. А вызвав MapView OfFile, второй процесс получит тот же адрес, что и первый. Таким образом, необхо димость в передаче адреса от первого процесса второму отпадает.

В Windows 2000 механизм проецирования файлов реализован удачнее, чем в Win dows 98, потому что Windows 2000 для доступа к файловым данным в адресном про странстве требует вызова MapViewOfFile. При обращении к этой функции система резервирует для проецируемого файла закрытый регион адресного пространства, и никакой другой процесс нс получает к нему доступ автоматически. Чтобы посторон ний процесс мог обратиться к данным того же объекта «проекция файла», сго поток тоже должен вызвать MapViewOfFile, и система отведет регион для представления объекта в адресном пространстве второго процесса.

Адрес, полученный при вызове MapViewOfFile первым процессом, скорее всего не совпадет с тем, что получит при ее вызове второй процесс, - даже несмотря на то что оба процесса проецируют представление одного и того же объекта. И хотя в Windows 98 адреса, получаемые процессами при вызове MapViewOfFile, совпадают,

лучше не полагаться на эту особенность — иначе приложение не станет работать в Windows 2000!

Рассмотрим еще одно различие механизмов проецирования файлов у Windows 2000 и Windows 98. Взгляните на текст программы, проецирующей два представления един ственного объекта «проекция файла».

#include <Windows.h>

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int nCmdShow)
{

// открываем существующий файл; он должен быть больше 64 Кб
HANDLE hFile = CreateFile(pszCmdLine, GENERIC_READ | GENERlC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

// создаем объект "проекция файла", связанный с файлом данных
HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);

// проецируем представление всего файла на наше адресное пространство
PBYTE pbFilc = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_WRITE, 0, 0, 0);

// проецируем второе представление файла, начиная со смещения 64 Кб

PBYTE pbFile2 = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_WRITE, 0, 65536, 0);

if ((pbFile + 65536) == pbFilft2))
{

// если адреса перекрываются, оба предсгавления проецируются на один
// регион, и мы работаем в Windows 98
MessageBox(NULL, "We are running under Windows 98", NULL, MB_OK);

}
else
{

// если адреса не перекрываются, каждое представление размещается в
// своем регионе адресного пространства, и мы работаем в Windows 2000
MessageBox(NULL, "We are running under Windows 2000", NULL, MB_OK),

}

UnmapViewOfFile(pbFile2);
UnmapViewOfFile(pbFile);
CloseHandle(hFileMapping);
CloseHandle(hFile};

return(0);

}

Когда приложение в Windows 98 отображает на адресное пространство представ ление объекта «проекция файла», ему отводится регион, достаточно большой для раз мещения всего объекта Это происходит, даже если Вы просите MapViewOfFile спрое цировать лишь малую часть такого объекта. Поэтому спроецировать объект размером 1 Гб не удастся, даже если указать, что представление должно быть не более 64 Кб.

При вызове каким-либо процессом функции MapViewOfFile ему возвращается ад рес в пределах региона, зарезервированного для целого объекта "проекция файла". Так что в покязанной выше программе первый вызов этой функции дает базовый адрес региона, содержащего весь спроецированный файл, а второй — адрес, смещенный «вглубь» того же региона на 64 Кб.

Wmdows 2000 и здесь ведет себя совершенно иначе Два вызова функции МарView OfFile (как в показанном выше коде) приведут к тому, что будут зарезервированы два

региона адресного пространства. Объем первого будет равен размеру объекта «про екция файла", объем второго — размеру объекта минус 64 Кб. Хотя регионы - раз ные, система гарантирует когерентность данных, так как оба представления созданы на основе одного объекта «проекция файла» А в Windows 98 такие представления когерентны потому, что они расположены в одном участке памяти

Совместный доступ процессов к данным через механизм проецирования

В Windows всегда было много механизмов, позволяющих приложениям легко и быс тро разделять какие-либо данные. К этим механизмам относятся RPC, COM, OLE, DDE, оконные сообщения (особенно WM_COPYDATA), буфер обмена, почтовые ящики, сокеты и т. д. Самый низкоуровневый механизм совместного использования данных на одной машине — проецирование файла в память. На нем так или иначе базируют ся все перечисленные мной механизмы разделения данных Поэтому, если Вас инте ресует максимальное быстродействие с минимумом издержек, лучше всего применять именно проецирование.

Совместное использование данных в этом случае происходит так: два или более процесса проецируют в намять представления одного и того же объекта «проекция файла", т. e. делягодни и те жс страницы физической памяти. В результате, когда один процесс записывает данные в представление общего объекта «проекция файла», из менения немедленно отражаются на представлениях в других процессах. Но при этом все процессы должны использовать одинаковое имя объекта "проекция файла»

А вот что происходит при запуске приложения. При открытии ЕХЕ-файла на дис ке система вызывает CreateFile, с помощью CreateFileMapping создает объект «проек ция файла" и, наконец, вызывает MapVtewQfFileEx (с флагом SEC_IMAGE) для отобра жения ЕХЕ-файла на адресное пространство только что созданного процесса. Map ViewOfFileEx вызывается вместо MapViewOfFile, чтобы представление файла было спро ецировано по базовому адресу, значение которого хранится в самом ЕХЕ-файле По том создается первичный поток процесса, адрес первого байта исполняемого кода в спроецированном представлении заносится в регистр указателя команд (IP), и про цессор приступает к исполнению кода.

Если пользователь запустит второй экземпляр того же приложения, система уви дит, что объект "проекция файла» для нужного ЕХЕ-файла уже существует и не станет создавать новый объект. Она просто спроецирует еще одно представление файла — на этот раз в контексте адресного пространства только что созданного второго про цесса, т. e. одновременно спроецирует один и тот же файл на два адресных простран ства Это позволяет эффективнее использовать память, так как оба процесса делят одни и те же страницы физической памяти, содержащие порции исполняемого кода.

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

Файлы, проецируемые на физическую память из страничного файла

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

Прекрасно понимая это, Microsoft добавила возможность проецирования файлов непосредственно на физическую память из страничного файла, а не из специально создаваемого дискового файла. Этот способ даже проще стандартного — основанно го на создании дискового файла, проецируемого в память. Во-первых, не надо вызы вать CreateFile, так как создавать или открывать специальный файл не требуется Вы просто вызываете, как обычно, CreateFileMapping и передаете INVALID_HANDLE_VALUE в параметре hFite. Тем самым Вы указываете системе, что создавать объект «проекция файла», физическая память которого находится на диске, не надо; вместо этого сле дует выделить физическую память из страничного файла. Объем выделяемой памяти определяется параметрами dwMaximumStzeHigh и dwMaximumSizeLow.

Создав объект «проекция файла" и спроецировав его представление на адресное пространство своего процесса, его можно использовать так же, как и любой другой регион памяти. Если Вы хотите, чтобы данные стали доступны другим процессам, вызовите CreateFileMapping и передайте в параметре pszName строку с нулевым сим волом в конце. Тогда посторонние процессы — если им понадобится сюда доступ — смогут вызвать CreateFileMapping или OpenFileMapping и передать ей то же имя.

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

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

HANDLE hFile = CreateFile(...);
HANDLE hMap = CreateFileMapping(hFile, ...);

if (hMap == NULL)

return(GetLasttrror());

...

Если вызов CreateFile не удастся, она вернет INVALID_HANDLE_VALUE. Но программист, написавший этот код, не дополнил его проверкой на успешное создание файла. Поэтому, когда в дальнейшем код обращается к функции Create FileMapping, в параметре hFile ей передается INVALID_HANDLE_VALUE, что зас тавляет систему создать объект "проекция файла» из ресурсов страничного файла, а не из дискового файла, как предполагалось в программе. Весь после дующий код, который используег проецируемый файл, будет работать правиль но. Но при уничтожении объекта «проекция файла" все данные, записанные в спроецированную память (страничный файл), пропадут. И разработчик будет долго чесать затылок, пытаясь понять, в чем дело!

Программа-пример MMFShare

Эта программа, «17 MMFShare.exe» (см. листинг на рис. 17-3), демонстрирует, как про исходит обмен данными между двумя и более процессами с помощью файлов, про ецируемых в память. Файлы исходного кода и ресурсов этой программы находятся в каталоге 17-MMFShare на компакт-диске, прилагаемом к книге.

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

Чтобы переслать данные из одной копии MMFShare в другую, наберите какой нибудь текст в поле Data. Затем щелкните кнопку Create Mapping Of Data. Программа вызовет функцию CreateFileMapping, чтобы создать объект «проекция файла» разме ром 4 Кб и присвоить ему имя MMFSharedData (ресурсы выделяются объекту из стра

ничного файла) Увидев, что объект с таким именем уже существует, программа вы даст сообщение, что не может создать объект. Л если такого объекта нет, программа создаст объект, спроецирует представление файла на адресное пространство процесса и скопирует данные из поля Data в проецируемый файл.

rihter17-8.jpg

Далее MMFShare прекратит проецировать представление файла, отключит кнопку Create Mapping Of Data и активизирует кнопку Close Mapping Of Data. На этот момент проецируемый в память файл с именем MMFSharedData будет просто «сидеть» где-то в системе. Никакие процессы пока не проецируют представление на данные, содер жащиеся в файле.

Если Вы теперь перейдете в другую копию MMFShare и щелкнете там кнопку Open Mapping And Get Data, программа попытается найти объект «проекция файла" с име нем MMFSharedData через функцию OpenFileMapping, Если ей не удастся найли объект с таким именем, программа выдаст соответствующее сообщение В ином случае она спроецирует представление объекта на адресное пространство своего процесса и скопирует данные из проецируемого файла в поле Data. Вот и все! Вы переслали дан ные из одного процесса в другой

Кнопка Close Mapping OfData служит для закрытия объекта «проекция файла», что высвобождает физическую память, занимаемую им в страничном файле. Если же объект «проекция файла» не существует, никакой другой экземпляр программы MMFShare не сможет открыть зтот объект и получить от него данные Кроме того, если один экземпляр программы создал объект «проекция файла", то остальным повторить его создание и тем самым перезаписать данные, содержащиеся в файле, уже не удастся.

MMFShare

 

Частичная передача физической памяти проецируемым файлам

До сих пор мы видели, что система требует передавать проецируемым файлам всю физическую память либо из файла данных на дигке, либо из страничного файла Это значит, что память используется не очень эффективно Давайте вспомним то, что я говорил в разделе "B какой момен! региону передают физическую память" главы 15 Допустим, Вы xoтитe сделать всю таблицу доступной другому процессу Если приме нить для этого механизм проецирования файлов, придется передать физическую па мять целой таблице

CELLDATA CellData[200][256];

Если структура CELLDATA занимает 128 байтов, показанный массив потребует 6 553 600 (200 x 256 x 128) байтов физической памяти Это слишком много — тем бо лее, что в таблице обычно заполняют всего несколько строк

Очевидно, что в данном случае, создав объект "проекцин файла", желательно не передавать ему заранее всю физическую память Функция CreateFtleMapping предус матривает такую возможность, для чего в параметр fdwProtect нужпо передать один из флагов SEC_RESRVE или SEC_COMMlT

Эти флаги имеют смысл, только если Вы создаете объект «проекция файла", ис пользующий физическую память из страничного файла. Флаг SEC_COMMIT заставля ет CreateFileMapping сразу же передать память из страничного файла. (То же самое происходит, если никаких флагов не указано.) Но когда Вы задаете флаг SEC_RESERVE, система не передает физическую память из страничного файла, а просто возвращает описатель объекта «проекция файла". Далее, вызвав MapViewOfFile или MapViewOfFileEx, можно создать представление этого объекта. При этом MapViewOfFile или MapView OfFileEx резервирует регион адресного пространства, не передавая ему физической памяти. Любая попытка обращения по одному из адресов зарезервированного регио на приведёт к нарушению доступа

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

А теперь самое интересное. Оказывается, все, что нужно для передачи физической памяти общему (совместно используемому) региону, - вызвать функцию VirtualAlloc:

PVOID VirtualAlloc( PVOID pvAddress, SIZE_T dwSize, DWORD fdwAllocationType, DWORD friwProtect);

Эту функцию мы уже рассматривали (и очень подробно) в главе 15. Вызвать Virtual Alloc для передачи физической памяти представлению региона — то же самое, что вызвать VirtualAlloc для передачи памяти региону, ранее зарезервированному вызовом VirtualAlloc с флагом MEM_RESERVE. Получается, что региону, зарезервированному функциями MapViewOfFile или MapViewOfFileEx, — как и региону, зарезервированно му функцией VirtualAlloc, — тоже можно передавать физическую память порциями, а не всю сразу. И если Вы поступаете именно так, учтите, что все процессы, спроеци ровавшие на этот регион представление одного и того же объекта «проекция файла», теперь тоже получат доступ к страницам физической памяти, переданным региону.

Итак, флаг SEC_RESERVE и функция VirtualAlloc позволяют сделать табличную мат рицу CellData «общедоступной" и эффективнее использовать память.

WINDOWS 98
Обычно VirtualAlloc не срабатывает, если Вы передаете ей адрес памяти, выхо дящий за пределы диапазона от 0x00400000 до 0x7FFFFFFF. Однако при перс даче физической памяти проецируемому файлу, созданному с флагом SEC_RE SERVE, в VirtualAlloc нужно передать адрес, укладывающийся в диапазон от 0x80000000 до 0xBFFFFFFE Только тогда Windows 98 поймет, что физическая память передается региону, зарезервированному под проецируемый файл, и даст благополучно выполнить вызов функции.

WINDOWS 2000
В Windows 2000 функция VirtualFree не годится для воврата физической па мяти, переданной в свос время проецируемому файлу (созданному с флагом SEC_RESERVE). Однако в Windows 98 такого ограничения нет.

Файловая система NTFS 5 поддерживает так называемые разреженные файлы (spar se files). Это потрясяющая новинка. Она позволяет легко создавать и использовать

разреженные проецируемые файлы (sparse memory-mapped files), которым физичес кая память предоставляется не из страничного, а из обычного дискового файла

Вот пример гого, как можно было бы воспользоваться этой новинкой Допустим, Вы хотите создать проецируемый в память файл (MMF) для записи аудиоданных При этом Вы должны записывать речь в виде цифровых аудиоданных в буфер памяти, связанный с дисковым файлом. Самый простой и эффективный способ решить эту задачу — применить разреженный MMF Все дело в том, что Вам заранее не известно, сколько времени будет говорить пользователь, прежде чем щелкнет кнопку Stop. Mo жет, пять минут, а может, пять часов — разница большая! Однако при использовании разреженного MMF это не проблема.

Программа-пример MMFSparse

Эта программа, «17 MMFSparseexe" (см листинг на рис. 17-4), демонстрирует, как создать проецируемый в память файл, связанный с разреженным файлом NTFS 5 Файлы исходного кода и ресурсов этой программы находятся в каталоге 17-MMFSparse на компакт-диске, прилагаемом к книге После запуска MMFSparse па экране появля ется окно, показанное ниже.

rihter17-9.jpg

Когда Вы щелкнсте кнопку Create а 1 MB (1024 KB) Sparse MMF, программа попы тается создать разреженный файл «C:\MMFSpanse». Если Ваш диск С не является томом NTFS 5, у программы ничего не получится, и ее процесс завершится А если Вы созда ли том NTFS 5 на каком-то другом диске, модифицируйте мою программу и переком пилируйте ее, чтобы посмотреть, как она работает

После создания разреженный файл проецируется на адресное пространство про цесса. В поле Allocated Kangcs (внизу окна) показывается, какие части файла действи тельно связаны с дисковой памятью. Изначально файл не связан ни с какой памятью, и в этом поле сообщается «No allocated ranges in the file» («В файле нет выделенных диапазонов»).

Чтобы считать байт, просто введите число в поле Offset и щелкните кнопку Read Byte. Введенное Вами число умножается на 1024 (1 Кб), и программа, считав байт по полученному адресу, выводит его значение в поле Byte Если адрес попадает в область, не связанную с физической памятью, в этом поле всегда показывается нулевой байт.

Для записи байта введите число в поле Offset, a значение байта (0-255) — в поле Byte. Потом, когда Вы щелкнете кнопку Wrice Byte, смещение будет умножено на 1024, и байт по соответствующему адресу получит новое значение Операция записи мо жет заставить файловую систему передать физическую память какой-либо части фай ла Содержимое поля Allocated Ranges обновляется после каждой операции чтения или записи, показывая, какие части файла связаны с физической памятью на данный мо мент. Вот как вьплядит окно программы после записи всего одного байта по смеще нию 1 024 000 (1000 x 1024).

rihter17-10.jpg

На этой иллюстрации видно, что физическая память выделена только одномуди апазону адресов — размером 65 536 байтов, начиняя с логического смещения 983 040 от начала файла С помощью ExpIorer Вы можете просмотреть свойства файла C:\MMFSparbe, как показано ниже.

rihter17-11.jpg

Заметьте: на этой странице свойств сообщается, что длина файла равна 1 Мб (это виртуальный размер фаЙла), по на деле он занимает на диске только 64 Кб.

Последняя кнопка, Free All Allocated Regions, заставляет программу высвободить всю физическую память, выделенную для файла; таким образом, соответствующее дисковое пространство освобождается, а все байты в файле обнуляются.

Теперь поговорим о том, как работает эта программа. Чтобы упростить ее исход ный код, я создал С++-класс CSparseStream (который содержится в файле Sparse Stream.h) Этот класс инкапсулирует поддержку операций с разреженным файлом или потоком данных (stream). В файле MMFSparse.cpp я создал другой С++-класс, CMMFSparse, производный от CSparseStream. Так что объект класса CMMFSparse обла дает не только функциональностью CSparseStream, но и дополнительной, необходи мой для использования разреженного потока данных как проецируемого в память файла. В процессе создается единственный глобальный экземпляр класса CMMF Sparse — переменная g_mmf. Манипулируя разреженным проецируемым файлом, про грамма часто ссылается на эту глобальную переменную.

Когда пользователь щелкает кнопку Create а 1MB (1024 KB) Sparse MMF, програм ма вызывает CreateFile для создания нового файла в дисковом разделе NTFS 5. Пока что это обычный, самый заурядный файл Но потом я вызываю метод Initialize гло бального объекта g_mmf, передавая ему описатель и максимальный размер файла (1 Мб). Метод Initialize в свою очередь обращается к CreateFileMapping и создает объект ядра «проекция файла» указанного размера, а затем вызывает MapViewOfFile, чтобы сделать разреженный файл видимым в адресном пространстве данного процесса

Когда Initialize возвращает управление, вызывается функция Dlg_ShowAllocated Ranges Используя Windows-функции, она перечисляет диапазоны логических адре сов в разреженном файле, которым передана физическая память. Начальное смеще ние и длина каждого такого диапазона показываются в нижнем поле диалогового окна В момент инициализации объекта g_mmf файлу на диске еще не выделена физичес кая память, и данное поле отражает этот факт.

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

При чтении объекта g_mmf возвращается либо реальное значение байта, если дан ному диапазону адресов передана физическая память, либо 0, если память не передана,

Моя программа также демонстрирует, как вернуть файл в исходное состояние, высвободив все выделенные сму диапазоны адресов (после этого он фактически не занимает места ня диске) Реализуется это так. Пользователь щелкает кнопку Free All Allocated Regions. Однако освободить все диапазоны адресов, выделенные файлу, ко торый проецируется в память, нельзя Поэтому первое, что делает программа, — вы зывает метод ForceClose объекта g_mmf Этот метод обращается к UnmapViewOfFile, а потом — к CloseHandle, передавая описатель объекта ядра «проекция файла».

Далее вызывается метод DecommitPortionOfStream, который освобождает всю па мять, выделенную логическим байтам в файле. Наконец, программа вновь обращает ся к методу Initialize объекча g_mmf, и тот повторно инициализирует файл, проеци руемый на адресное пространство данного процесса. Чтобы подтвердить освобожде ние всей выделенной памяти, программа вызывает функцию Dlg_ShowAllocatedRanges которая выводит в поле строку «No allocated ranges in the file».

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

байты, не влияет на занимаемый им объем дискового пространства, но позволяет Explorer и команде dir сообщать точный размер файла С этой целью Вы должны пос ле вызова метода ForceClose использовать функции SetFilePointer и SetEndOfFile.

MMFSparse