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

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

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

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

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

Имя раздела

Описание

.debug

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

.didat

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

 

table)

.edata

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

.idata

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

.rdata

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

.reloc

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

 

table)

.rsrc

Ресурсы

.text

Код EXE или DLL

.tls

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

.xdata

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

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

#pragma data_seg("sectionname")

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

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

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

#pragma data_seg("Shared") L0NG g_lInstanceCount; #pragma data_seg()

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

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

//создаем раздел 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;

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

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

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

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

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

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

/SECTION:Shared,RWS

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

После запятой мы задаем требуемые атрибуты. При этом используются такие сокращения: 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 С++.

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

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

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

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

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

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

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

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

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

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

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

//указываем компилятору поместить эту инициализированную переменную

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

#pragma data_seg("Shared")

volatile LONG g_lApplicationInstances = 0; #pragma data_seg()

//указываем компоновщику, что раздел Shared должен быть

//читаемым, записываемым и разделяемым

#pragma comment(linker, "/Section:Shared,RWS")

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

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

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

PostMessage(HWND_BROADCAST, g_uMsgAppInstCountUpdate, 0, 0);

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

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

AppInst.cpp

/****************************************************************************** Module: AppInst.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"

///////////////////////////////////////////////////////////////////////////////

// общесистемное оконное сообщение с уникальным идентификатором

UINT g_uMsgAppInstCountUpdate = WM_APP+123;

///////////////////////////////////////////////////////////////////////////////

//указывает компилятору поместить эту инициализированную переменную

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

#pragma data_seg("Shared")

volatile LONG g_lApplicationInstances = 0; #pragma data_seg()

//указываем компоновщику, что раздел Shared должен быть

//читаемым, записываемым и разделяемым.

#pragma comment(linker, "/Section:Shared,RWS")

///////////////////////////////////////////////////////////////////////////////

BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {

chSETDLGICONS(hWnd, IDI_APPINST);

// Force the static control to be initialized correctly. PostMessage(HWND_BROADCAST, g_uMsgAppInstCountUpdate, 0, 0); return(TRUE);

}

///////////////////////////////////////////////////////////////////////////////

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

void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {

switch (id) { case IDCANCEL:

EndDialog(hWnd, id); break;

}

}

///////////////////////////////////////////////////////////////////////////////

INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

if (uMsg == g_uMsgAppInstCountUpdate) {

SetDlgItemInt(hWnd, IDC_COUNT, g_lApplicationInstances, FALSE);

}

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) {

//получаем числовое значение из общесистемного оконного сообщения,

//которое применяется для уведомления всех окон верхнего уровня

//об изменении счетчика числа пользователей данного модуля.

g_uMsgAppInstCountUpdate = RegisterWindowMessage(TEXT("MsgAppInstCountUpdate"));

// запущен еще один экземпляр этой программы

InterlockedExchangeAdd(&g_lApplicationInstances, 1);

DialogBox(hInstExe, MAKEINTRESOURCE(IDD_APPINST), NULL, Dlg_Proc);

//данный экземпляр закрывается

InterlockedExchangeAdd(&g_lApplicationInstances, -1);

//сообщаем об этом остальным экземплярам программы.

PostMessage(HWND_BROADCAST, g_uMsgAppInstCountUpdate, 0, 0);

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

return(0);

}

//////////////////////////////// End of File //////////////////////////////////

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

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

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

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

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

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

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

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

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

Этот метод посложнее первого, зато позволяет гораздо эффективнее использовать память, так как требует выделения лишь 8 Кб. Но и здесь не без

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

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

к третьему методу.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

HANDLE CreateFile(

PCSTR pszFileName,

DWORD dwDesiredAccess,

DWORD dwShareMode,

PSECURITY_ATTRIBUTES psa,

DWORD dwCreationDisposition,

DWORD dwFlagsAndAttributes,

HANDLE hTemplateFile);

Как видите, у функции CreateFile довольно много параметров. Здесь я сосре-

доточусь только на первых трех: pszFileName, dwDesiredAccess и dwShareMode.

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

Табл. 17-3. Права доступа к файлу

Значение

Описание

0

Содержимое файла нельзя считывать или записывать; указывайте это

 

значение, если вы хотите всего лишь получить атрибуты файла

GENERIC_READ

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

GENERIC_WRITE

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

GENERIC_READ |

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

GENERIC_WRITE

 

Создавая или открывая файл данных с намерением использовать его в качестве проецируемого в память, можно установить либо флаг GENERIC_

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

READ (только для чтения), либо комбинированный флаг GENERIC_READ | GENERIC_WRITE (чтение/запись).

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

Табл. 17-4. Режимы совместного доступа к файлу

Значение

Описание

0

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

FILE_SHARE_READ

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

 

GENERIC_WRITE не удастся

FILE_SHARE_WRITE

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

 

GENERIC_READ не удастся

FILE_SHARE_READ|

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

FILE_SHARE_WRITE

 

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

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

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

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

HANDLE CreateFileMapping(

HANDLE hFile,

PSECURITY_ATTRIBUTES psa,

DWORD fdwProtect,

DWORD dwMaximumSizeHigh,

DWORD dwMaximumSizeLow,

PCTSTR pszName);

Первый параметр, hFile, идентифицируетописательфайла, проецируемого на адресное пространство процесса. Этот описатель вы получили после вызова CreateFile. Параметр psa — указатель на структуру SECURITY_ATTRIBUTES, которая относится к объекту ядра «проекция файла»; для установки защиты по умолчанию ему присваивается NULL.

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

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