On-Line Библиотека www.XServer.ru - учебники, книги, статьи, документация, нормативная литература.
       Главная         В избранное         Контакты        Карта сайта   
    Навигация XServer.ru








 

Глава_23. Управление памятью

Прежде чем изучать управление памятью в Windows, надо сначала разобраться в том, что такое процесс (process). Программа - это ЕХЕ-файл, который можно запустить из Windows разными способами. После запуска программа становится процессом. У процесса есть своя память, дескрипторы файлов и другие системные ресурсы. Если дважды подряд запустить одну и ту же программу, то получается два отдельных процесса.

Устройство памяти

Процессы и адресное прос транство

Процессу выделяется свое "частное" 4-гигабайтное виртуальное адресное пространство (вообразите, что у компьютера оперативная память размером в сотни гигабайт и что каждый процесс получает свои 4 Гб). Программа может обращаться к любому байту в этом адресном пространстве, используя единственный 32-битный линейный адрес . При этом в адресном пространстве каждого процесса содержится множество самых разных элементов, в том числе:

  • образ ЕХЕ-файла программы;
  • все несистемные DLL, загруженные Вашей программой (включая DLL-модули MFC),
  • глобальные данные программы (доступные как по чтению и записи, так и только по чтению);
  • стек программы;
  • динамически выделяемая память, в том числе кучи Windows и библиотеки С периода выполнения;
  • файлы, спроецированные в память;
  • блоки памяти, совместно используемые несколькими процессами;
  • память, локальная для данного выполняемого потока;
  • особые системные блоки памяти, в том числе таблицы виртуальной памяти;
  • ядро и DLL-компоненты Windows.

Слова о том, что адресное пространство процесса - его частная собственность, слегка не верны. Верхние 2 Гб вовсе не являются таковой, и их содержимое - общее для всех процессов. А вот нижние 2 Гб по-настоящему закрыты, в частности, это относится к стеку, кучами глобальной памяти с доступом по чтению/записи . Однако, двух идентичных процессов были отдельные копии кода и данных только для чтения, то какой бы в том был смысл? И действительно, образ ЕХЕ-файла проецируется на адресное пространство каждого процесса по одному и тому же адресу - обычно начиная с 0х40000000. То же относится и к DLL-модулям, если есть возможность их загрузки по одному и тому же адресу для каждого процесса. Закрытая память процесса занимает нижние 2 Гб (от 0 до Ox7FFFFFFF), но младшие 4 Мб (64 Кб под Windows NT) не используются.

Насколько все это надежно? Посторонний процесс практически не имеет возможности перезаписать стек, кучи или глобальную память другого процесса, поскольку эта память, расположенная в нижних 2 Гб виртуального адресного пространства, принадлежит только тому, второму процессу. Пространство, занятое ЕХЕ- и DLL-модулями, помечено как "только для чтения", поэтому в том, что они проецируются на несколько разных процессов, никакой проблемы нет. Однако со старшим гигабайтом адресного пространства дело обстоит хуже, так как сюда отображаются важные данные Windows, доступные по чтению и записи. Сбойная программа вполне может уничтожить расположенные в этой области системные таблицы. (В Windows NT такой проблемы нет, потому что верхние 2 Гб защищены от записи.) Существует также вероятность того, что один процесс испортит содержимое спроецированного в память файла, используемого другим процессом, поскольку файлы проецируются на область , разделяемую всеми процессами. (В Windows NT и это не проблема, так как файлы проецируются на адреса ниже 0 x80000000 (в нижние 2 Гб)).

Как ус троена виртуальная память

Естественно, на самом деле никаких сотен гигабайт оперативной памяти в компьютере нет. Нет и сотен гигабайт дискового пространства. Здесь Windows идет на определенные ухищрения. Во-первых, 4-гигабайтное адресное пространство процесса фрагментировано. Программы и элементы данных разбросаны по адресному пространству блоками по 4 Кб, выровненных по границам, кратным 4 Кб. Каждый такой блок называется страницей (page) и может содержать либо код, либо данные. Когда страница действительно используется, она занимает физическую память, но программист никогда не встретится с физическими адресами. Микропроцессорный чип фирмы Intel эффективно преобразует 32-битный виртуальный адрес в номер физической страницы и смещение внутри нее, пользуясь двумя уровнями таблиц 4-килобайтных страниц. Отметим: отдельные страницы могут быть помечены либо как "только для чтения", либо как "для чтения и записи". Кроме того, у каждого процесса свой набор таблиц страниц. Регистр чипа CR3 содержит указатель на страницу каталога - переключаясь с одного процесса на другой, Windows просто обновляет этот регистр.

Страница памяти может быть отмечена в таблице страниц как "присутствующая",что говорит о том, что данная 4-килобайтная страница находится ли сейчас в памяти. При попытке обращения к странице, отсутствующей в памяти, генерируется прерывание, и Windows приступает к анализу ситуации, просматривая свои внутренние таблицы. Если обращение к памяти было неверным, выдается сообщение об ошибке страницы (page fault), и программа завершается. В ином случае Windows считает в оперативную память нужную страницу из дискового файла и обновит таблицу страниц, записав в нее физический адрес и установив бит присутствия. Таковы азы виртуальной памяти в Win32.

Момент чтения и записи страницы (чтобы достичь максимальной производительности) определяет диспетчер виртуальной памяти Windows. Если какой-то процесс не использовал страницу в течение определенного периода и эта память нужна другому процессу, данная страница выгружается из памяти, а вместо нее загружается страница нового процесса.

Все процессы совместно используют один большой общесистемный файл подкачки (swap file), в который помещаются (при необходимости) все виды данных "для чтения и записи" и некоторые виды данных "только для чтения". (Windows NT поддерживает одновременную работу с несколькими файлами подкачки.) Windows определяет размер файла подкачки в зависимости от размера ОЗУ и свободного дискового пространства, но существуют способы тонкой настройки размера и физического расположения этого файла.

Однако файл подкачки - не единственный файл, используемый диспетчером виртуальной памяти. Нет особого смысла в том, чтобы записывать в этот файл страницы кода. Вместо этого Windows проецирует ЕХЕ- и DLL-модули непосредственно на их дисковые файлы. Поскольку страницы кода помечены как "только для чтения", то необходимости в их записи обратно на диск не возникает. Если два процесса используют один и тот же ЕХЕ-файл, то данный файл отображается на адресные пространства обоих процессов. Файлы, проецируемые в память, о которых мы поговорим позже, также отображаются напрямую. Они доступны "для чтения и записи" и разделяются несколькими процессами.

Функция VirtualAlloc: переданная и зарезервированная память

Если программе нужна динамически распределяемая память, то рано или поздно ей придется вызвать функцию VirtualAlloc . Скорее всего вызовете ее не программист, а функции Windows или библиотеки С периода выполнения, выделяющие память из кучи. Зная, как работает VirtuаlAlloc, можно лучше понять функции, которые обращаются к ней.

Сначала разберемся с понятиями зарезервированной (reserved) и переданной (committed памяти. При резервировании памяти выделяется непрерывный диапазон виртуальных адресов. Если, допустим, известно, что программа будет оперировать с одним 5-мегабайтным блоком памяти (блоки памяти называют также регионами (regions)), но весь он сейчас не нужен, тогда следует вызвать VirtualAlloc и в параметре, определяющем тип выделения памяти, указать MEM_RESERVE, а в параметре, задающем размер выделяемой памяти, - 5 Мб. Windows округляет начальный и конечный адреса региона до значений, кратных 64 Кб, и уже не даст процессу повторно зарезервировать память из этого региона. И хотя программист может задавать начальный адрес региона, лучше оставить это занятие самой Windows. Ну вот, собственно, и все. Больше ничего не происходит - не выделяется ни оперативной памяти, ни пространства в файле подкачки.

Когда программе всерьез понадобится эта память, она снова вызовет VirtualAlloc , на этот раз ука, МЕМ_СОММ IТ, чтобы передать память из этого региона. Теперь начальный и конечный адреса региона округляются до значений, кратных 4 Кб, и в файле подкачки выделяются соответствующие страницы, а также создается нужная таблица страниц. Блок помечается либо как "только для чтения", либо как "для чтения и записи". Однако оперативная память по-прежнему не выделяется; это произойдет, лишь когда программа попытается что-то записать в этот блок памяти. Если передаваемая память не была ранее зарезервирована, ошибки все равно не возникает. А если память уже была передана, то и в этом случае - никаких проблем. Главное в том, что перед использованием память должна быть передана.

Чтобы вернуть (decommit) переданную память (по сути, вернуть соответствующим страницам статус зарезервированных), применяется функция VirtualFree . Она может также освободить и зарезервированный регион памяти, но для этого ей надо передать базовый адрес, возвращенный VirtualAlloc в момент резервирования памяти.

Функции управ ления памятью

Куча Wind ows и семейство функций GlobalAlloc

Куча (heap) - это пул памяти какого-либо процесса. Когда программе требуется блок памяти, онавызывает функцию, выделяющую память из кучи, а чтобы освободить ранее выделенную память, - функцию, парную первой. Выравнивание по границам, кратным 4 Кб, в этом случае не производится; диспетчер кучи использует пространство на выделенных ранее страницах или обращается к VirtualAlloc , чтобы получить дополнительные страницы. Одна из двух куч, с которыми программа работает, - куча Windows. Для выделения из нее памяти служит функция HeapAlloc , а для освобождения - функция HeapFree . HeapAlloc особенно удобна для выделения "крупных" блоков памяти.

Может быть, приложение и не будет явно вызывать НеарА lloc , но за него это сделает функция GlobalAlloc , унаследованная от Winl6. В идеальном мире 32-разрядных программ функция GlobalAlloc не понадобилась бы , но мы имеем дело с реальным миром. Все еще остается колоссальный объем кода, перенесенного из Winl6, в том числе OLE, в котором вместо 32-разрядных адресов применяются параметры типа "описатель памяти" (HGLOBAL).

Работа функции GlobalAlloc зависит от передаваемых ей атрибутов. Если ей указывается GMEM_FIXED, она просто вызывает НеарА lloс и возвращает адрес, приводя тип данных к 32-битному значению HGLOBAL. Если же ей передается GMEM_MOVEABLE, возвращаемое значение HGLOBAL является указателем на элемент "таблицы описателей" в данном процессе. В этом элементе содержится указатель на память, выделенную функцией HeapAlloc .

Зачем нужна "перемещаемая" (moveable) память, если она вводит еще один слой "непрямого управления"? Здесь мы встречаемся с наследием Winl6, в котором операционная система когда-то действительно перемещала блоки памяти. В Win32 перемещаемые блоки памяти существуют лишь для поддержки GlobalReAlloc, которая выделяет новый блок памяти, копирует в него содержимое старого блока, освобождает последний и помещает адрес нового блока в существующий элемент таблицы описателей.

К сожалению, многие библиотечные функции используют в качестве параметров и возвращаемых значений HGLOBAL, а не адреса памяти. Если такая функция возвращает HGLOBAL, приложение должно считать, что данная память выделена с атрибутом GMEM_MOVEABLE, и, следовательно, чтобы получить адрес памяти, надо вызвать функцию GlobalLock . (В случае фиксированной памяти GlobalLock просто возвращает переданный ей описатель как адрес.) Если от приложения требуется передать параметр HGLOBAL, то следует получить это значение с помощью GlobalAlloc (GMEM_MOVEABLE,...) - на тот случай, если вызываемая функция обращается к GlobalReAlloc и ожидает, что значение описателя не изменится.

Буфер обмена Windows использует фиксированную память, тем самым можно передавать адреса, возвращенные HeapAlloc , и приводить возвращаемые значения HGLOBAL к void-указателям. Можно также передавать фиксированный адрес OLE-функции, которая принимает параметр типа HGLOBAL, однако значения HGLOBAL, возвращаемые OLE-функциями, являются перемещаемыми, поэтому приложение должны вызывать GlobalLock .

Куча библиотеки С периода выполнения, _heapmin и С++-операторы new и delete

Однако, куча Windows (и функция HeapAlloc ) - это не та куча, с которой приложение будете работать чаще всего. Существует еще одна куча - ею управляет библиотека С периода выполнения (С RunTime library, CRT). Доступ к CRT-куче реализуется функциями malloc и free, напрямую вызываемыми операторами C++ new и delete. Эта куча оптимизирована для выделения блоков малого размера.

Конечно, функция malloc вызывает VirtualAlloc, но делает это очень хитро. При первом вызове она резервирует регион размером 1 Мб (в будущих версиях Visual C++ эти значения могут измениться) и передает блок памяти, размер которого кратен 64 Кб (если malloc вызван чтобы выделить память размером 64 Кб или менее, выделяется один 64-килобайтный блок. При последующих вызовах память выделяется по возможности из этого блока; в ином случае диспетчер кучи вызывает VirtualAlloc , чтобы передать дополнительную память. После того, как весь регион размером 1 Мб израсходован, malloc резервирует еще один регион размер 2 Мб, потом другой, но уже размером 4 Мб и т.д., передавая память по мере необходимости.

При вызове функции free диспетчер кучи помещает дескрипторы блоков памяти в односвязный циклический список свободных блоков памяти (free list), находящийся вне CRT кучи. Функция malloc использует этот список для последующего выделения памяти (если это возможно). Так как данный список весьма компактен, поиск свободных страниц ocyществляется быстро, без просмотра большого числа страниц виртуальной памяти.

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

Отображаемые в память файлы и разделяемая память

Что такое отображаемые файлы? Обычно механизм виртуальной памяти позволяет операционной системе отображать несуществующую память в дисковый файл, называемый страничным файлом. Можно посмотреть на это с другой стороны и рассмотреть механизм виртуальной памяти как метод ссылок на содержимое файла, называемого страничным файлом, через указатели так, как будто страничный файл является объектом памяти. Другими словами, этот механизм отображает содержимое страничного файла в адреса памяти. Если это можно сделать со страничным файлом, то почему нельзя с другими файлами? Отображаемые в память файлы представляют естественное расширение механизма управления виртуальной памятью.

Файловое отображение можно создать с помощью функции CreateFileMapping . Для открытия существующего отображения с определенным именем приложения могут использовать функцию OpenFileMapping . Функция MapViewOfFile отображает часть файла в блок виртуальной памяти.

Особенностью отображаемых в память файлов является их совместное использование разными приложениями, т.е., если два приложения открыли файловое отображение с одним и тем же именем, то они, по сути, создали блок разделяемой памяти.

Не излишне ли использовать дисковый файл, если необходимо лишь передать несколько байт между приложениями? В действительности нет необходимости явно открывать и использовать дисковый файл для получения отображения в памяти. Приложения могут передать специальное значение дескриптору 0хFFFFFFFF в функцию CreateFileMapping для получения отображения непосредственно в системный страничный файл. Это, по сути, создает блок разделяемой памяти.

Неско лько советов по работе с динамической памятью

Чем интенсивнее используется куча, тем в большей мере она фрагментируется и тем медленнее работает программа. Если предполагается, что время непрерывного функционирования программы будет исчисляться часами или днями, то следует проявить осторожность. Лучше выделить всю необходимую память при запуске программы и освободить ее при закрытии, но это не всегда возможно. Тут может помешать класс CString, который постоянно выделяет и освобождает крошечные порции памяти. Однако разработчики MFC недавно внесли в него ряд улучшений.

Если программа в основном выделяет блоки малого размера, но изредка требует 6ольших блоков (занимая их на время, отличное от того, на которое она занимает малые блоки), тогда стоит подумать о выделении крупных блоков функцией HeapAlloc , а не new . Это несколько уменьшит степень фрагментированности CRT-кучи. Не следует забывать время от времени вьзывать _heapmin . Стоит тщательно следить за тем, как был получен тот или иной блок памяти - приложению не миновать серьезных проблем, если оно, например, вызовет HeapFree для указателя, полученного с помощью new .

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

Назад       Содержание       Вперёд