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

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

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

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

Это необычно, но тема данного раздела - файлы, проецируемые в память (memory-mapped files) - как раз и есть лучший пример того, что реальное положение дел обстоит именно так... И вначале - о причинах. О том, почему абстракция "файла, проецируемого в память" вообще возникла к жизни.

Со времён первых компьютеров, оснащённых хоть какой-то внешней памятью, известна проблема - программа, хранимая во внешней памяти совершенно непригодна для исполнения. Для того, чтобы эту программу можно было исполнить приходилось читать внешнюю память (диск, ленту и т.п.) и переносить её записи в память оперативную. В оперативной памяти строился образ исполняемого файла, он немного дорабатывался программой-загрузчиком (настройка перемещаемых ссылок) и лишь затем он мог быть исполнен процессором. Итого - требовалось удвоенное пространство памяти (в ОЗУ и в файле) и процедура переписывания потока байт из одного вида памяти в другой. Альтернативой было размещение программы в ПЗУ - ни переписывания, ни настройки и требуемое пространство ограничивалось только объёмом самой программы.

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

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

Требовалось какое-то решение, причём, по возможности, такое, которое бы решило все имеющиеся в комплекте абстракций для обмена данными проблемы комплексно: уменьшило совокупную потребность в памяти (в файле и в ОЗУ ); поскольку доступ к файлу буферизуется, то уменьшило бы объём "перекачки" данных между видами памяти; и которое бы позволяло обращаться с файлом не как с образом его части в ОЗУ , а "как с целым файлом". Иными словами, требовалось устройство внешней памяти, в котором бы параллельно потоку байтов этого устройства существовал и поток байтов оперативной памяти. Такой, что изменение любого байта в этом потоке немедленно приводит к сохранению сделанного изменения во внешней памяти, а выборка возможна как и положено из памяти - побайтно, а не поблочно.

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

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

Именно это решение в совокупности с программно-аппаратным механизмом, позволяющим сохранять страницы оперативной памяти во внешней памяти (мы говорим "файл" только потому, что пространство внешней памяти в виде файлов и организовано) и составляет концепцию файлов, проецируемых в память. А состоит она всего-навсего в том, что в адресном пространстве процесса можно зарезервировать регион и сказать - страницы этого региона должны загружаться из указанного физического файла. Понятно, что механизм страничного обмена посредством обработки страничной ошибки позволяет: 1) избежать специального буфера для обмена данными; 2) обмениваться между видами памяти минимально возможной порцией данных. При этом он, сам собой, привносит в этот процесс свои преимущества: 1) программист может не заботиться об обеспечении идентичности копий (на профессиональном сленге в таких случаях употребляют термин "когерентность") порции файла и страницы оперативной памяти, и 2) загрузка требуемой порции файла производится также автоматически - только тогда, когда к этой порции последует реальное обращение. В совокупности это всё экономит и оперативную память и обмен между видами памяти, т.е. со всем основанием можно сказать, что иного механизма обмена данными между двумя видами памяти, который бы был быстрее и потреблял бы для своего функционирования меньше ресурсов - в данной системе придумать невозможно.

Так что всё просто, но из описанного инженерного решения можно сделать и философский вывод. Что, как и положено в системе с виртуальной памятью, распределяются-то отнюдь не страницы реальной памяти (они в данном случае служат только средством доступа), а - пространство файла! Это ведь именно оно и заполняет собой адресное пространство региона, зарезервированного а адресном пространстве процесса? Выходит, основным распределяемым ресурсом как раз и является не пространство ОЗУ , а пространство файла? Да и сам "файл подкачки" можно рассматривать просто как частный случай "файла, проецируемого в память", частный тем, что вместо общего для таких файлов API для операций с ним определён какой-то собственный API .

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

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

Имея "в руках" дескриптор файла необходимо вызвать функцию CreateFileMapping , которая создаёт уже другие, "проецирующие", внутренние структуры и связывает "сущность на диске" с "сущностью в адресном пространстве", по своему месту в последовательности действий она создаёт что-то наподобие "адресного пространства из файла". Далее можно "резервировать регион" в этом "пространстве" - создавать "окно" (view). Оно делается функцией MapViewOfFile , которая возвращает адрес ОЗУ . Далее с этим view можно обращаться так, как будто это есть буфер файла, в который загружены его данные... - читать, писать оперативную память. При этом, то, что вернёт функция MapViewOfFile , будет адресом региона страницы физической памяти в который будут выделяться системой самостоятельно по мере надобности - можно прямо обращаться к любому адресу в этом регионе и система выделит этому месту страницу. Но это будет "настоящая страница" - если системе понадобится её вытеснить, то сама система и сохранит её содержимое в файл!

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

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

HANDLE CreateFileMapping(   HANDLE  hFile,                                 
//дескриптор физического файла
   LPSECURITY_ATTRIBUTES lpFileMappingAttributes, 
//игнорируется. Должно быть NULL
   DWORD   flProtect,                             
//флажки защиты доступа
   DWORD   dwMaximumSizeHigh,                     
//старшие байты размера
   DWORD   dwMaximumSizeLow,                      
//младшие байты размера
   LPCTSTR lpName                                 
//имя проекции
 );

Видно, что функция концептуально довольно проста. Она принимает дескриптор физического файла, который нужно спроецировать в память и взамен выдаёт дескриптор глобального объекта "проецируемый файл" (или - "проекция файла", это кому как нравится). Флажки защиты доступа позволяют задать режим доступа READONLY или READWRITE и ограничить доступ к проекции, т.е. если сам физический файл открыт в режиме доступа READWRITE , то всё равно можно сделать его проекцию только для чтения. А вот обратное - неверно. Функция CreateFileMapping завершится неуспешно, если файл, описываемый дескриптором hFile открыт в режиме READONLY , а его проекцию программист пытается создать READWRITE .

Функция принимает также и два других параметра - проецируемую длину файла и - имя объекта. Файл, или часть файла, которую необходимо спроецировать может быть весьма большой. А адресное пространство с адресом в 32 разряда - маленьким для такого файла. Именно поэтому "оконный" подход к решению этой проблемы сохраняется - зарезервировать регион памяти можно не большего размера, чем это позволено сделать по характеристикам адресации. А вот саму проекцию файла можно иметь значительно длиннее - длиной до 2 64 байтов. Именно поэтому длина и указывается двумя параметрами, а не одним, и, естественно, для файлов короче 4х гигабайтов значение параметра dwMaximumSizeHigh всегда будет нуль. По своему же смыслу "проецируемая длина файла" сообщает системе до каких пор позволены операции с этим файлом, или, что то же самое - как много пространства можно (нужно) будет спроецировать в виде физических страниц. Если физический файл - короче указанной длины, то система допишет его двоичными нулями до требуемой. Если же физический файл длиннее, то только первая часть файла, длиной не более указанной, будет доступна для проекции. И, наконец, если требуемая длина указана и вовсе нулевой, то система принимает в качестве "проецируемой длины" текущую длину файла. Об этом следует помнить, поскольку взять да и устроить проекцию этак на 500 - 1000 Мб в современном адресном пространстве и не очень "тяжёлом" приложении - труда не составляет. Нужно только знать, что после закрытия этой проекции система и на диске оставит файл такой же длины! И свободное пространство для размещения файла такого размера - на диске иметь надо, даже, если вся эта длина фактически использована не будет.

Необходимость аргумента, указываемого параметром lpName не очевидна. А между тем - весьма даже понятна. Ведь проекция файла - глобальный объект. И этот глобальный объект должен как-то именоваться, так чтобы из разных процессов его можно было однозначно идентифицировать. По сложившемуся обычаю делается это некоторой текстовой строкой. К ней не предъявляется никаких специальных требований (разве что - не все символы позволены), но она - должна быть уникальной во всём пространстве глобальных системных объектов, т.е. не пересекаться, например, с именами объектов mutex и других глобальных объектов. Параметр этот может быть и NULL - в таком случае из другого процесса сослаться на данную проекцию будет нельзя.

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

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

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

LPVOID MapViewOfFile(   HANDLE hFileMappingObject,   
//дескриптор проекции файла
   DWORD  dwDesiredAccess,      
//режим доступа к физическим страницам
   DWORD  dwFileOffsetHigh,     
//старшие байты смещения
   DWORD  dwFileOffsetLow,      
//младшие байты смещения
   SIZE_T dwNumberOfBytesToMap, 
//размер "окна"
 );

И эта функция не сложнее предыдущей. Её назначение - организовать "окно" в памяти в которое "подвести" указанное место спроецированного файла. Можно считать, что алгоритм её выполнения состоит из двух фаз - она резервирует регион в адресном пространстве длиной в dwNumberOfBytesToMap байтов, подводит указатель физического файла в позицию, указываемую параметрами dwFileOffsetHigh и dwFileOffsetLow и совмещает стартовый адрес зарезервированного региона с указателем файла. Поскольку длина проекции может быть указана 64х-разрядным числом, то и позиция указателя такого файла тоже должна указываться такими же числами.

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

Об ограничениях на значения параметров можно прочитать в MSDN , в частности все длины должны быть кратны размеру страницы процессора, мы же отметим два обстоятельства. Во-первых, можно организовать несколько "окон" к одному и тому же спроецированному файлу. Это могут быть "окна" из разных процессов, но могут быть "окна" и из одного и того же процесса. Они могут быть относительно разных позиций указателя файла, а могут быть относительно одной и той же. Длины этих "окон" не обязаны как-то соотноситься между собой и могут быть произвольными. Каждое "окно" представляет собой регион в виртуальном адресном пространстве, поэтому длина и возможное количество "окон" определяется тем, насколько хватит ресурсов адресного пространства. Как уже отмечалось - система гарантирует, что во всех "окнах" будет обеспечена когерентность их содержимого. Во-вторых - "окно" невозможно передвинуть по файлу, т.е. место файла, спроецированное в регион, нельзя изменить. Поэтому, если обработка длинного файла ведётся в несколько "кадров", "окно" следует закрывать и создавать вновь с другими значениями параметров dwFileOffsetHigh и dwFileOffsetLow . Закрытие "окна" делает функция UnMapViewOfFile .

Функция MapViewOfFile резервирует регион в памяти указанной длины где придётся, т.е. там, где сама функция посчитает это удобным. В большинстве случаев этого и достаточно, но а если содержимое файла - значения указателей, которые будут действительны только, если рассматриваются относительно какого-то совершенно определённого стартового адреса, и если MapViewOfFile сделает проекцию "не туда"? Для разрешения этой проблемы в системе существует функция MapViewOfFileEx . Она отличается от MapViewOfFile наличием дополнительного параметра - начального адреса проекции, которую она делает в точности по указанному этим параметром адресу. Но если региона требуемой длины по указанному стартовому адресу зарезервировать не удаётся, то она завершается неуспешно в отличие от MapViewOfFile , которая может в этом случае поискать регион и в другом месте.

По окончании же работы нужно вернуть ресурсы системе. Спроецированный регион и его связь с ресурсом "проецируемый файл" разрушает функция UnMapViewOfFile . Она принимает единственный параметр - стартовый адрес освобождаемого региона, который ранее был выделен функциями MapViewOfFile или MapViewOfFileEx .

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

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

разделы сайта

форум

технология COM

оптимизация сайтов


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