Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6269
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

554 Часть III. Управление памятью

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

Кроме рассмотренных выше атрибутов защиты страницы, существует еще и четыре атрибута раздела; их можно ввести в параметр fdwProtect функции CreateFileMapping побитовой операцией OR. Раздел (section) — всего лишь еще одно название проекции памяти, отображаемое программой Process Explorer от Sysinternals (см. http://www.microsoft.com/technet/sysinternals/Security/ProcessExplorer.mspx).

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

Табл. 17-5. Атрибуты защиты страниц

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

Описание

PAGE_READONLY

Отобразив объект «проекция файла» на адресное пространство,

 

можно считывать данные из файла. При этом вы должны были пе-

 

редать в CreateFile флаг GENERIC_READ

PAGE_READWRITE

Отобразив объект «проекция файла» на адресное пространство,

 

можно считывать данные из файла и записывать их. При этом вы

 

должны были передать в CreateFile комбинацию флагов

 

GENERIC_READ | GENERIC_WRITE

PAGE_WRITECOPY

Отобразив объект «проекция файла» на адресное пространство,

 

можно считывать данные из файла и записывать их. Запись приве-

 

дет к созданию закрытой копии страницы. При этом вы должны

 

были передать в CreateFile либо GENERIC_READ, либо

 

GENERIC_READ | GENERIC_WRITE

PAGE_EXECUTE_READ

Отобразив объект «проекция файла» на адресное пространство,

 

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

 

вы должны были передать в CreateFile комбинацию флагов GE-

 

NERIC_READ и GENERIC_EXECUTE

 

Глава 17. Проецируемые в память файлы.docx 555

Табл. 17-5. (окончание)

 

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

Описание

PAGE_EXECUTE_READWRITE

Отобразив объект «проекция файла» па адресное простран-

 

ство, можно считывать данные из файла, записывать их, а

 

также запускать файл. При этом мы должны были передать

 

в CreateFile комбинацию флагов GENERIC_READ, GE-

 

NERIC_WRITE и GENERIC_EXECUTE

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

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

Последний атрибут, SEC_LARGE_PAGES, приказывает Windows использовать больших страницы для проецирования в память файлов образов. Этот атрибут применим только к файлам образов в формате РЕ, использовать его для отображения в память ваших файлов с данными не удастся. Как сказано выше в описании функции VirtualAlloc, для использования больших страниц необходимо соблюдать следующие условия:

при передаче физической памяти необходимо комбинировать (с помощью операции OR) флаг SEC_COMMIT с параметром fdwAllocationType во время вызо-

ва CreateFileMapping;

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

MaximumSizeHigh и dwMaximumSizeLow ниже);

переданная память должна быть помечена атрибутом защиты

PAGE_READWRITE;

у пользователя, вызывающего CreateFileMapping, должно быть право Lock Pages in Memory.

Следующие два параметра этой функции (dwMaximumSizeHigh и dwMaximumSizeLow) самые важные. Основное назначение CreateFileMapping

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

556 Часть III. Управление памятью

которых выражаются 64-разрядными числами, в параметре dwMaximumSizeHigh указываются старшие 32 бита, а в dwMaximumSizeLow — младшие 32 бита этого значения. Для файлов размером менее 4 Гб dwMaximurnSizeHigh всегда равен 0. Наличие 64-разрядного значения подразумевает, что Windows способна обрабатывать файлы длиной до 16 экзабайтов.

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

NULL

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

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

int WINAPI _tWinMain(HINSTANCE, HINSTANCE. PTSTR, int) {

// перед выполнением этого оператора, в каталоге C:\

//еще нет файла "MMFTest.dat" HANDLE hFile = CreateFile(TEXT("C:\\MMFTest.Dat"),

GENERIC_READ | GENERIC_WRITE,

FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

//перед выполнением этого оператора файл MMFTest.dat существует,

//но имеет нулевую длину

HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE,

0, 100, NULL);

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

//возрастает до 100 байтов

Глава 17. Проецируемые в память файлы.docx 557

//очистка

CloseHandle(hFileHap);

CloseHandle(hFile);

//no завершении процесса файл MMFTest.dat останется

//на диске и будет иметь длину 100 байтов return(0);

}

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

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

«проекция файла» создан с флагом PAGE_READONLY или PAGE_WRITECOPY,

то размер, переданный функции CreateFileMapping, не должен превышать физический размер файла на диске (так как вы не сможете что-то дописать в файл).

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

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

тор INWALID_HANDLE_VALUE (определенный как -1).

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

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

PV0ID MapViewOfFile(

HANDLE hFileMappingObject,

DW0RD dwDesiredAccess,

DWORD dwFileOffsetHigh,

DWORD dwFileOffsetLow,

SIZE_T dwNumberOfBytesToMap);

Параметр hFileMappingObject идентифицирует описатель объекта «проекция файла», возвращаемый предшествующим вызовом либо CreateFileMapping, либо OpenFileMapping (ее мы рассмотрим чуть позже). Параметр

558 Часть III. Управление памятью

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

Табл. 17-6. Права доступа к файлу, спроецированному в память

Значение

 

 

Описание

 

 

FILE_MAP_WRITE

Файловые данные можно считывать и записывать; вы должны бы-

 

ли

передать

функции

CreateFileMapping

атрибут

 

PAGE_READWRITE

 

 

 

 

FILE_MAP_READ

Файловые данные можно только считывать; вы должны были вы-

 

звать CreateFileMapping с любым из следующих атрибутов:

 

PAGE_READONLY,

PAGE_

READWRITE

или

 

PAGE_WRITECOPY

 

 

 

 

FILE_MAP_ALL_ACCESS

То же, что и FILE_MAP_WRITE

 

 

 

FILE_MAP_COPY

Файловые данные можно считывать и записывать, но запись при-

 

водит к созданию закрытой копии страницы; вы должны были вы-

 

звать CreateFileMapping с любым из следующих атрибутов:

 

PAGE_READONLY,

PAGE_READWRITE

или

 

PAGE_WRITECOPY

 

 

 

 

FILE_MAP_EXECUTE

Содержимое файла может быть исполнено как код; вы должны

 

были

вызвать

CreateFileMapping

с

атрибутом

 

PAGE_EXECUTE_READWRITE или PAGE_EXECUTE_READ

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

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

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

метры dwFileOffsetHigh и dwFileOffsetLow. Поскольку Windows поддержи-

вает файлы длиной до 16 экзабайтов, приходится определять смещение в файле как 64-разрядное число: старшие 32 бита передаются в параметре dwFileOffsetHigh, а младшие 32 бита—в параметре dwFileOffsetLow. Заметьте,

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

Глава 17. Проецируемые в память файлы.docx 559

составляст64 Кб.) О гранулярности выделения памяти см. раздел «Системная информация» в главе 14.

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

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

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

PAGE_WRITECOPY на 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_WRITEC0PY

560 Часть III. Управление памятью

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

//При первой записи система берет страницу, переданную из страничного файла,

//копирует исходное содержимое страницы, расположенной по запрашиваемому адресу

//в памяти, и проецирует новую страницу (копию) на адресное пространство процесса.

//Новая страница получает атрибут PAGE_READWRITE.

//записываем еще один байт в представление файла

pbFile[1] = 0;

//поскольку теперь байт располагается на странице с атрибутом PAGE_ READWRITE,

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

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

//функция UnmapViewOfFile обсуждается в следующем разделе

UnmapViewOfFile(pbFile);

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

//все, что было записано на эти страницы, теряется

//"уходя, гасите свет"

CloseHandle(hFileMapping);

CloseHandle(hFile);

Примечание. Если ваше приложение работает на компьютере с архитектурой NUMA, можно повысить быстродействие, если передавать потоку физическую память, смонтированную на той же плате, что и исполняющий его процессор. Когда поток создает представление для спроецированного файла, Windows по умолчанию так и делает. Если же известно, что поток может подключиться к другому процессору, можно изменить заданное по умолчанию поведение, вызвав функцию CreateFileMappingNuma и явно указав плату NUMA-компьютера (в последнем параметре dwPreferredNumaNode), память которой следует передавать потоку:

HANDLE CreateFileMappingNuma( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD fdwProtect,

DWORD dwMaximumSizeHigh, DWORD dwMaxirounSizeLow, PCTSTR pszName,

DWORD dwPreferredNumaNode

);

Теперь при вызове MapViewOfFile поток получит память из банка, заданного при вызове CreateFileMappingNuma. Windows также поддерживает

Глава 17. Проецируемые в память файлы.docx 561

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

метра dwPreferredNumaNode;

PVOID MapViewOfFileExNuma( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap, LPVOID lpBaseAddress,

DWORD dwPreferredNumaNode

);

Подробнее о Windows-функциях для управления памятью на NUMAкомпьютерах см. в разделах «Управление памятью на компьютерах с архитектурой NUMA» и «Резервирование региона в адресном пространстве» в главах 14 и 15.

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

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

BOOL UnmapViewOfFile(PVOID pvBaseAddress);

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

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

BOOL FlushViewOfFile(

PVOID pvAddress,

SIZE_T dwNumberOfBytesToFlush);

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

562Часть III. Управление памятью

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

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

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

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

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

Рассмотрим это подробнее на фрагменте псевдокода:

HANDLE hFile = CreateFile(…);

HANDLE hFileMapping = CreateFileMapping(hFile, …);

PVOID pvFile = MapViewOfFile(hFileMapping, …);

Глава 17. Проецируемые в память файлы.docx 563

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

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) демонстрирует, как с помощью механизма проецирования записать в обратном порядке содержимое текстового ANSI-или Unicode-файла. Файлы исходного кода и ресурсов этой программы находятся в каталоге 17-FileRev внутри архива, доступного на веб-сайте поддержки этой книги. После запуска FileRev на экране появляется диалоговое окно, показанное ниже.

Выбрав имя файла и щелкнув кнопку Reverse File Contents, вы активизируете функцию, которая меняет порядок символов в файле на обратный.

Соседние файлы в предмете Программирование на C++