Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Создание эффективных приложений для Windows Джеффри Рихтер 2004 (Книга).pdf
Скачиваний:
375
Добавлен:
15.06.2014
Размер:
8.44 Mб
Скачать

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

И последнее, почему я использую отдельный поток: физическую память, отведен ную под его стек, можно освободить. Рассмотрим такой сценарий: пользователь про сит функцию Sum вычислить сумму целых чисел от 0 до 30 000. Это требует передачи региону стека весьма ощутимого объема памяти. Затем пользователь проводит не

сколько операций суммирования — максимум до 5000 И окажется, что стеку передан порядочный объем памяти, который больше не используется А ведь эта физическая память выделяется из страничною файла Так что лучше бы освободить се и вернуть системе И поскольку программа завершает поток SumThreadFunc, система автомати чески освобождает физическую память, переданную региону стека

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

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

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

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

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

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

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

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

Проецирование в память 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флйле раздел кода обычно предшествует разделу данных.

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

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

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

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

Система выделяет новую страницу и копирует на нее содержимое страницы дан ных 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 компоновщика