Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009
.pdf574 Часть III. Управление памятью
Все параметры и возвращаемое этой функцией значение идентичны применяемым в MapViewOfFile, кроме последнего параметра — pvBaseAddress. В нем можно задать начальный адрес файла, проецируемого в память. Как и в случае VirtualAlloc, базовый адрес должен быть кратным гранулярности выделения памяти в системе (обычно 64 Кб), иначе MapViewOfFileEx вернет NULL, сообщив тем самым об ошибке. Если вы укажете базовый адрес, не кратный гранулярности выделения памяти, то MapViewOfFileEx в Windows 2000 завершится с ошибкой, и
GetLastError вернет код 1132 (ERROR_MAPPED_ALIGNMENT).
Если система не в состоянии спроецировать файл по этому адресу (чаще всего из-за того, что файл слишком велик и мог бы перекрыть другие регионы зарезервированного адресного пространства), функция также возвращает NULL В этом случае она не пытается подобрать диапазон адресов, подходящий для данного файла. Но если вы укажете NULL в параметре pvBaseAddress, она поведет себя идентично MapViewOfFile.
MapViewOfFileEx удобна, когда механизм проецирования файлов в память применяется для совместного доступа нескольких процессов к одним данным. Поясню. Допустим, нужно спроецировать файл в память по определенному адресу; при этом два или более приложений совместно используют одну группу структур данных, содержащих указатели на другие структуры данных. Отличный тому пример — связанный список. Каждый узел, или элемент, такого списка хранит адрес другого узла списка. Для просмотра списка надо узнать адрес первого узла, а затем сделать ссылку на то его поле, где содержится адрес следующего узла. Но при использовании файлов, проецируемых в память, это весьма проблематично.
Если один процесс подготовил в проецируемом файле связанный список, а затем разделил его с другим процессом, не исключено, что второй процесс спроецирует этот файл в своем адресном пространстве на совершенно иной регион. А дальше будет вот что. Попытавшись просмотреть связанный список, второй процесс проверит первый узел списка, прочитает адрес следующего узла и, сделав на него ссылку, получит совсем не то, что ему было нужно, — адрес следующего элемента в первом узле некорректен для второго процесса.
У этой проблемы два решения. Во-первых, второй процесс, проецируя файл со связанным списком на свое адресное пространство, может вызвать MapViewOfFileEx вместо MapViewOfFile. Для этого второй процесс должен знать адрес, по которому файл спроецирован на адресное пространство первого процесса на момент создания списка. Если оба приложения разработаны с учетом взаимодействия друг с другом (а так чаще всего и делают), нужный адрес может быть просто заложен в код этих программ или же один процесс как-то уведомляет другой (скажем, посылкой сообщения в окно).
А можно и так. Процесс, создающий связанный список, должен записывать в каждый узел смещение следующего узла в пределах адресного пространства. Тогда программа, чтобы получить доступ к каждому узлу, будет суммировать
Глава 17. Проецируемые в память файлы.docx 575
это смещение с базовым адресом проецируемого файла. Несмотря на простоту, этот способ не лучший: дополнительные операции замедлят работу программы и увеличат объем ее кода (гак как компилятор для выполнения всех вычислений, естественно, сгенерирует дополнительный код). Кроме того, при этом способе вероятность ошибок значительно выше. Тем не менее он имеет право на существование, и поэтому компиляторы Майкрософт поддерживают указатели со смещением относительно базового значения (based-pointers), для чего предусмотрено ключевое слово __based.
Примечание. При вызове MapViewOfFileEx следует указывать адрес в границах пользовательского раздела адресного пространства процесса, иначе функция вернет NULL
Особенности проецирования файлов
Windows для доступа к файловым данным в адресном пространстве требует вызова MapViewOfFile. При обращении к этой функции система резервирует для проецируемого файла закрытый регион адресного пространства, и никакой другой процесс не получает к нему доступ автоматически. Чтобы посторонний процесс мог обратиться к данным того же объекта «проекция файла», его поток тоже должен вызвать MapViewOfFile, и система отведет регион для представления объекта в адресном пространстве второго процесса. Здесь важно заметить, что адреса, которые MapViewOfFile вернет первому и второму процессу, скорее всего, будут разными. Это верно, даже когда процессы работают с представлениями одного и того спроецированного файла.
Взгляните на текст программы, проецирующей два представления единственного объекта «проекция файла».
int WINAPI _tWinMain (HINSTANCE, HINSTANCE, PTSTR, int) {
// открываем существующий файл; он должен быть больше 64 Кб
HANDLE hFile = CreateFile(pszCmdLine, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXIST_NG, FILE_ATTRIBUTE_NORMAL, NULL);
//создаем объект "проекция файла", связанный с файлом данных
HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
//проецируем представление всего файла на наше адресное пространство
PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_WRITE, 0, 0, 0);
//проецируем второе представление файла, начиная со смещения 64 Кб
PBYTE pbFile2 = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_WRTTE, 0, 65536, 0);
576Часть III. Управление памятью
//если адреса перекрываются, оба представления проецируются на один
//регион, и мы работаем в Windows 98
int iDifference = int(pbFile2 - pbFile); TCHAR szMsg[100]; StringCchPrintf(szMsg, _countof(szMsg),
TEXT("Pointers difference = %d KB"), iDifference / 1024); MessageBox(NULL, szNsg, NULL, MB_OK);
UnmapViewOfFile(pbFile2);
UnmapViewOfFile(pbFile);
CloseHandle(hFileHapplng);
CloseHandle(hFile);
return(0);
}
Два вызова функции MapViewOfFile (как в показанном выше коде) приведут к тому, что будут зарезервированы два региона адресного пространства. Объем первого будет равен размеру объекта «проекция файла», объем второго — размеру объекта минус 64 Кб. Хотя регионы — разные, система гарантирует когерентность данных, так как оба представления созданы на основе одного объекта «проекция файла».
Совместный доступ процессов к данным через механизм проецирования
В Windows всегда было много механизмов, позволяющих приложениям легко и быстро разделять какие-либо данные. К этим механизмам относятся RPC, COM, OLE, DDE, оконные сообщения (особенно WM_COPYDATA), буфер обмена, почтовые ящики, сокеты и т. д. Самый низкоуровневый механизм совместного использования данных на одной машине — проецирование файла в память. На нем так или иначе базируются все перечисленные мной механизмы разделения данных. Поэтому, если вас интересует максимальное быстродействие с минимумом издержек, лучше всего применять именно проецирование.
Совместное использование данных в этом случае происходит так: два или более процесса проецируют в память представления одного и того же объекта «проекция файла», т. е. делят одни и те же страницы физической памяти. В результате, когда один процесс записывает данные в представление общего объекта «проекция файла», изменения немедленно отражаются на представлениях в других процессах. Но при этом все процессы должны использовать одинаковое имя объекта «проекция файла».
А вот что происходит при запуске приложения. При открытии ЕХЕ-файла на диске система вызывает CreateFile, с помощью CreateFileMapping создает объект «проекция файла» и, наконец, вызывает MapViewOfFileEx (с флагом SEC_IMAGE) для отображения ЕХЕ-файла на адресное пространство
Глава 17. Проецируемые в память файлы.docx 577
только что созданного процесса. MapViewOfFileFx вызывается вместо MapViewOfFile, чтобы представление файла было спроецировано но базовому адресу, значение которого хранится n самом ЕХЕ-файле. Потом создается первичный ноток процесса, адрес первого байта исполняемого кода в спроецированном представлении заносится в регистр указателя команд (IP), и процессор приступает к исполнению кода.
Если пользователь запустит второй экземпляр того же приложения, система увидит, что объект «проекция файла» для нужного ЕХЕ-файла уже существует и не станет создавать новый объект. Она просто спроецирует еще одно представление файла — на этот раз в контексте адресного пространства только что созданного второго процесса, т. е. одновременно спроецирует один и тот же файл на два адресных пространства. Это позволяет эффективнее использовать память, так как оба процесса делят одни и те же страницы физической памяти, содержащие порции исполняемого кода.
Как и все объекты ядра, проекции файлов можно совместно использовать из нескольких процессов тремя методами: наследованием описателей, именованием и дублированием описателей. Подробное объяснение этих трех методов см. в главе 3.
Файлы, проецируемые на физическую память из страничного файла
До сих пор мы говорили о методах, позволяющих проецировать представление файла, размещенного на диске. В то же время многие программы при выполнении создают данные, которые им нужно разделять с другими процессами. А создавать файл на диске и хранить там данные только с этой целью очень неудобно.
Прекрасно понимая это, Майкрософт добавила возможность проецирования файлов непосредственно на физическую память из страничного файла, а не из специально создаваемого дискового файла. Этот способ даже проще стандартного — основанного на создании дискового файла, проецируемого в память. Вопервых, не надо вызывать CreateFile, так как создавать или открывать специальный файл не требуется. Вы просто вызываете, как обычно, CreateFileMapping и передаете INVALID_HANDLE_VALUE в параметре hFile. Тем самым вы указываете системе, что создавать объект «проекция файла», физическая память которого находится на диске, не надо; вместо этого следует выделить физическую память из страничного файла. Объем выделяемой памяти определяется параметрами dwMaxhnumSizeHigh и dwMaximumSizeLow.
Создав объект «проекция файла» и спроецировав его представление на адресное пространство своего процесса, его можно использовать так же, как и любой другой регион памяти. Если вы хотите, чтобы данные стали доступны другим процессам, вызовите CreateFileMapping и передайте в параметре pszName строку с нулевым символом в конце. Тогда посторонние процессы — если им понадобится сюда доступ — смогут вызвать CreateFileMapping или OpenFileMapping и передать ей то же имя.
578 Часть III. Управление памятью
Когда необходимость в доступе к объекту «проекция файла» отпадет, процесс должен вызвать CloseHandle. Как только все описатели объекта будут закрыты, система освободит память, переданную из страничного файла.
Примечание. Есть одна интересная ловушка, в которую может попасть неискушенный программист. Попробуйте догадаться, что неверно в этом фрагменте кода:
HANDLE hFile = CreateFile(…);
HANDLE hMap = CreateFileNapping(hFile, …); if (hMap == NULL)
return(GetLastError());
…
Если вызов CreateFile не удастся, она вернет INVALID_HANDLE_VALUE. Но программист, написавший этот код, не дополнил его проверкой на успешное создание файла. Поэтому, когда в дальнейшем код обращается к функции CreateFileMapping, в параметре hFile ей передается INVALID_HANDLE_VALUE, что заставляет систему создать объект «проекция файла» из ресурсов страничного файла, а не из дискового файла, как предполагалось в программе. Весь последующий код, который использует проецируемый файл, будет работать правильно. Но при уничтожении объекта «проекция файла» все данные, записанные в спроецированную память (страничный файл), пропадут. И разработчик будет долго чесать затылок, пытаясь понять, в чем дело! Всегда проверяйте значение, которое возвращает CreateFile, так как причин, по которым ее вызов может закончиться неудачей, множество.
Программа-пример MMFShare
Эта программа (17-MMFShare.exe) демонстрирует, как происходит обмен данными между двумя и более процессами с помощью файлов, проецируемых в память. Файлы исходного кода и ресурсов этой программы находятся в каталоге 17MMFShare внутри архива, доступного на веб-сайте поддержки этой книги.
Чтобы понаблюдать за происходящим, нужно запустить минимум две копии MMFShare. Каждый экземпляр программы создаст свое диалоговое окно.
Чтобы переслать данные из одной копии MMFShare в другую, наберите какойнибудь текст в поле Data. Затем щелкните кнопку Create Mapping Of Data. Программа вызовет функцию CreateFileMapping, чтобы создать объект «проекция файла» размером 4 Кб и присвоить ему имя MMFSharedData (ресурсы выделяются объекту из страничного файла). Увидев, что объект с таким именем уже существует, программа выдаст сообщение, что не может создать объект. А если такого объекта нет, программа создаст объект, спроецирует представление файла на адресное пространство процесса и скопирует данные из поля Data в проецируемый файл.
Глава 17. Проецируемые в память файлы.docx 579
Далее MMFShare прекратит проецировать представление файла, отключит кнопку Create Mapping Of Data и активизирует кнопку Close Mapping Of Data. На этот момент проецируемый в память файл с именем MMFSharedData будет просто «сидеть» где-то в системе. Никакие процессы пока не проецируют представление на данные, содержащиеся в файле.
Если вы теперь перейдете в другую копию MMFShare и щелкнете там кнопку Open Mapping And Get Data, программа попытается найти объект «проекция файла» с именем MMFSharedData через функцию OpenFileMapping. Если ей не удастся найти объект с таким именем, программа выдаст соответствующее сообщение. В ином случае она спроецирует представление объекта на адресное пространство своего процесса и скопирует данные из проецируемого файла в поле Data. Вот и все! Вы переслали данные из одного процесса в другой.
Кнопка Close Mapping Of Data служит для закрытия объекта «проекция файла», что высвобождает физическую память, занимаемую им в страничном файле. Если же объект «проекция файла» не существует, никакой другой экземпляр программы MMFShare не сможет открыть этот объект и получить от него данные. Кроме того, если один экземпляр программы создал объект «проекция файла», то остальным повторить его создание и тем самым перезаписать данные, содержащиеся в файле, уже не удастся.
MMFShare.cpp
/****************************************************************************** Module: MMFShare.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h" /* See Appendix A. */ #include <windowsx.h>
#include <tchar.h> #include "Resource.h"
///////////////////////////////////////////////////////////////////////////////
BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {
chSETDLGICONS(hWnd, IDI_MMFSHARE);
580 Часть III. Управление памятью
// инициализируем поле ввода текстовыми данными
Edit_SetText(GetDlgItem(hWnd, IDC_DATA), TEXT("Some test data"));
//отключаем кнопку Close, так как файл нельзя закрыть,
//если он не создан или не открыт
Button_Enable(GetDlgItem(hWnd, IDC_CLOSEFILE), FALSE); return(TRUE);
}
///////////////////////////////////////////////////////////////////////////////
void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {
// описатель открытого файла, проецируемого в память static HANDLE s_hFileMap = NULL;
switch (id) { case IDCANCEL:
EndDialog(hWnd, id); break;
case IDC_CREATEFILE:
if (codeNotify != BN_CLICKED) break;
//создаем в памяти проецируемый файл с данными, набранными
//в поле ввода; он занимает 4 KB и называется MMFSharedData
//(память выделяется из страничного файла)
s_hFileMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL,
PAGE_READWRITE, 0, 4 * 1024, TEXT("MMFSharedData"));
if (s_hFileMap != NULL) {
if (GetLastError() == ERROR_ALREADY_EXISTS) { chMB("Mapping already exists - not created."); CloseHandle(s_hFileMap);
}else {
//создание проецируемого файла завершилось успешно;
//проецируем представление файла на адресное пространство
PVOID pView = MapViewOfFile(s_hFileMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
if (pView != NULL) {
// поместим содержимое поля ввода в проецируемый файл
Глава 17. Проецируемые в память файлы.docx 581
Edit_GetText(GetDlgItem(hWnd, IDC_DATA),
(PTSTR) pView, 4 * 1024);
//прекращаем проецирование; это защитит
//данные от «блуждающих» указателей
UnmapViewOfFile(pView);
//пользователь не может создать сейчас еще один файл
Button_Enable(hWndCtl, FALSE);
//пользователь закрыл файл
Button_Enable(GetDlgItem(hWnd, IDC_CLOSEFILE), TRUE);
} else {
chMB("Can't map view of file.");
}
}
} else {
chMB("Can't create file mapping.");
}
break;
case IDC_CLOSEFILE:
if (codeNotify != BN_CLICKED) break;
if (CloseHandle(s_hFileMap)) {
//пользователь закрыл файл; новый файл создать можно,
//но закрыть его нельзя
Button_Enable(GetDlgItem(hWnd, IDC_CREATEFILE), TRUE); Button_Enable(hWndCtl, FALSE);
}
break;
case IDC_OPENFILE:
if (codeNotify != BN_CLICKED) break;
//смотрим: ну существует ли проецируемый в память файл
//с именем MMFSharedData.
HANDLE hFileMapT = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE,
FALSE, TEXT("MMFSharedData"));
if (hFileMapT != NULL) {
// такой файл есть; проецируем его представление
582 Часть III. Управление памятью
// на адресное пространство процесса
PVOID pView = MapViewOfFile(hFileMapT, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
if (pView != NULL) {
// помещаем содержимое файла в поле ввода
Edit_SetText(GetDlgItem(hWnd, IDC_DATA), (PTSTR) pView); UnmapViewOfFile(pView);
} else {
chMB("Can't map view.");
}
CloseHandle(hFileMapT);
} else {
chMB("Can't open mapping.");
}
break;
}
}
///////////////////////////////////////////////////////////////////////////////
INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, |
Dlg_OnInitDialog); |
chHANDLE_DLGMSG(hWnd, WM_COMMAND, |
Dlg_OnCommand); |
} |
|
return(FALSE); |
|
}
///////////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR, int) {
DialogBox(hInstExe, MAKEINTRESOURCE(IDD_MMFSHARE), NULL, Dlg_Proc); return(0);
}
//////////////////////////////// End of File //////////////////////////////////
Глава 17. Проецируемые в память файлы.docx 583
Частичная передача физической памяти проецируемым файлам
До сих пор мы видели, что система требует передавать проецируемым файлам иск» физическую память либо из файла данных на диске, либо из страничного файла. Это значит, что намять используется не очень эффективно. Давайте вспомним то, что я говорил в разделе «В какой момент региону передают физическую память» главы 15. Допустим, вы хотите сделать всю таблицу доступной другому процессу. Если применить для этого механизм проецирования файлов, придется передать физическую память целой таблице:
CELLDATA CellData[200][256];
Если структура CELLDATA занимает 128 байтов, показанный массив потребует 6 553 600 (200х256х128) байтов физической памяти. Это слишком много — тем более, что в таблице обычно заполняют всего несколько строк.
Очевидно, что в данном случае, создав объект «проекция файла», желательно не передавать ему заранее всю физическую память. Функция CreateFileMapping предусматривает такую возможность, для чего в параметр fdwProtect нужно пере-
дать один из флагов: SEC_RESERVE или SEC_COMMIT.
Эти флаги имеют смысл, только если нм создаете объект «проекция файла», использующий физическую память из страничного файла. Флаг SEC_COMMIT заставляет CreateFileMapping сразу же передать память из страничного файла. (То же самое происходит, если никаких флагов не указано.) Но когда вы задаете флаг SEC_RESERVE, система не передает физическую память из страничного файла, а просто возвращает описатель объекта «проекция файла». Далее, вызвав MapViewOfFile или MapViewfFielEx, можно создать представление этого объекта. При этом MapViewOfFile или MapViewOfFileEx резервирует регион адресного пространства, не передавая ему физической памяти. Любая попытка обращения по одному из адресов зарезервированного региона приведет к нарушению доступа.
Таким образом, мы имеем регион зарезервированного адресного пространства и описатель объекта «проекция файла», идентифицирующий этот регион. Другие процессы могут использовать данный объект для проецирования представления того же региона адресного пространства. Физическая намять региону попрежнему не передается, гак что, если потоки в других процессах попытаются обратиться по одному из адресов представлении в своих регионах, они тоже вызовут нарушение доступа.
А теперь самое интересное. Оказывается, все, что нужно для передачи физической памяти общему (совместно используемому) региону, — вызвать функцию
VirtualAlloc:
PVOID VirtualAlloc(
PVOID pvAddross,
SIZE_T dwSize,
