Время на прочтение
8 мин
Количество просмотров 172K
Каталог:
Один
Два
Три
Менеджер памяти (и связанные с ним вопросы контроллера кеша, менеджера ввода/вывода и пр) — одна из вещей, в которой (наряду с медициной и политикой) «разбираются все». Но даже люди «изучившие винду досконально» нет-нет, да и начинают писать чепуху вроде (не говоря уже о другой чепухе, написанной там же):
Грамотная работа с памятью!!! За все время использования у меня своп файл не увеличился ни на Килобайт. По этому Фаерфокс с 10-20 окнами сворачивается / разворачивается в/из трея как пуля. Такого эффекта я на винде добивался с отключенным свопом и с переносом tmp файлов на RAM диск.
Или к примеру μTorrent — у меня нет никаких оснований сомневаться в компетентности его авторов, но вот про работу памяти в Windows они со всей очевидностью знают мало. Не забываем и товарищей, производящих софт для слежения за производительностью и не имеющих ни малейшего понятия об управлении памятью в Windows (и поднявших по этому поводу истерику на пол интернета, на Ars-е даже был разбор полетов). Но самое потрясающее, что я видел всвязи с управлением памятью — это совет переместить pagefile на RAM-диск:
Из моих трех гигабайт под RAM disk был выделен один (на тот момент, когда на лаптопе еще была установлена XP), на котором я создал своп на 768МБ …
Цель данной статьи — не полное описание работы менеджера памяти (не хватит ни места ни опыта), а попытка пролить хоть немного света на темное царство мифов и суеверий, окружающих вопросы управления памятью в Windows.
Disclaimer
Сам я не претендую на то, чтобы знать все и никогда не ошибаться, поэтому с радостью приму любые сообщения о неточностях и ошибках.
Введение
С чего начать не знаю, поэтому начну с определений.
Commit Size — количество памяти, которое приложение запросило под собственные нужды.
Working Set (на картинке выше он так и называется Working Set) — это набор страниц физической памяти, которые в данный момент «впечатаны» в адресное пространство процесса. Рабочий набор процесса System принято выделять в отдельный «Системный рабочий набор», хотя механизмы работы с ним практически не отличаются от механизмов работы с рабочими наборами остальных процессов.
И уже здесь зачастую начинается непонимание. Если присмотреться, можно увидеть, что Commit у многих процессов меньше Working Set-а. То есть если понимать буквально, «запрошено» меньше памяти, чем реально используется. Так что уточню, Commit — это виртуальная память, «подкрепленная» (backed) только физической памятью или pagefile-ом, в то время как Working Set содержит еще и страницы из memory mapped файлов. Зачем это делается? Когда делается NtAllocateVirtualMemory (или любые обертки над heap manager-ом, например malloc или new) — память как бы резервируется (чтоб еще больше запутать, это не имеет никакого отношения к MEM_RESERVE, который резервирует адресное пространство, в данном же случае речь идет о резервировании именно физических страниц, которые система действительно может выделить), но физические страницы впечатываются только при фактическом обращении по выделенному адресу виртуальной памяти. Если позволить приложениям выделить больше памяти, чем система реально может предоставить — рано или поздно может случиться так, что все они попросят реальную страницу, а системе неоткуда будет ее взять (вернее некуда будет сохранить данные). Это не касается memory mapped файлов, так как в любой момент система может перечитать/записать нужную страницу прямо с/на диск(а).
В общем, суммарный Commit Charge в любой момент времени не должен превышать системный Commit Limit (грубо, суммарный объем физической памяти и всех pagefile-ов) и с этим связана одна из неверно понимаемых цифр на Task Manager-ах до Висты включительно.
Commit Limit не является неизменным — он может увеличиваться с ростом pagefile-ов. Вообще говоря, можно считать, что pagefile — это такой очень специальный memory mapped файл: привязка физической страницы в виртуальной памяти к конкретному месту в pagefile-е происходит в самый последний момент перед сбросом, в остальном же механизмы memory mapping-а и swapping-а очень схожи.
Working Set процесса делится на Shareable и Private. Shareable — это memory mapped файлы (в том числе и pagefile backed), вернее те части, которые в данный момент действительно представлены в адресном пространстве процесса физической страницей (это же Working Set в конце концов), а Private — это куча, стеки, внутренние структуры данных типа PEB/TEB и т.д. (опять таки, повторюсь на всякий случай: речь идет только той части кучи и прочих структур, которые физически находятся в адресном пространстве процесса). Это тот минимум информации, с которой уже можно что то делать. Для сильных духом есть Process Explorer, который показывает еще больше подробностей (в частности какая часть вот той Shareable действительно Shared).
И, самое главное, ни один из этих параметров по отдельности не позволяет сделать более менее полноценных выводов о происходящем в программе/системе.
Task Manager
Столбец «Memory» в списке процессов и практически вся вкладка «Performance» настолько часто понимаются неправильно, что у меня есть желание, чтоб Task Manager вообще удалили из системы: те, кому надо смогут воспользоваться Process Explorer-ом или хотя бы Resource Monitor-ом, всем остальным Task Manager только вредит. Для начала, собственно о чем речь
Начну с того, о чем я уже упоминал: Page File usage. XP показывает текущее использование pagefile-а и историю (самое забавное, что в статус баре те же цифры названы правильно), Виста — показывает Page File (в виде дроби Current/Limit), и только Win7 называет его так, чем оно на самом деле является: Commit Charge/Commit Limit.
Эксперимент. Открываем таск менеджер на вкладке с «использованием пейджфайла», открываем PowerShell и копируем в него следующее (для систем, у которых Commit Limit ближе, чем на 3 Гб от Commit Charge можно в последней строчке уменьшить 3Gb, а лучше увеличить pagefile):
add-type -Namespace Win32 -Name Mapping -MemberDefinition @" [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr CreateFileMapping( IntPtr hFile, IntPtr lpFileMappingAttributes, uint flProtect, uint dwMaximumSizeHigh, uint dwMaximumSizeLow, [MarshalAs(UnmanagedType.LPTStr)] string lpName); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr MapViewOfFile( IntPtr hFileMappingObject, uint dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, uint dwNumberOfBytesToMap); "@ $mapping = [Win32.Mapping]::CreateFileMapping(-1, 0, 2, 1, 0, $null) [Win32.Mapping]::MapViewOfFile($mapping, 4, 0, 0, 3Gb)
Это приводит к мгновенному повышению «использования свопфайла» на 3 гигабайта. Повторная вставка «использует» еще 3 Гб. Закрытие процесса мгновенно освобождает весь «занятый свопфайл». Самое интересное, что, как я уже говорил memory mapped файлы (в том числе и pagefile backed) являются shareable и не относятся к какому либо конкретному процессу, поэтому не учитываются в Commit Size никакого из процессов, с другой стороны pagefile backed секции используют (charged against) commit, потому что именно физическая память или пейджфайл, а не какой нибудь посторонний файл, будут использоваться для того, чтобы хранить данные, которые приложение захочет разместить в этой секции. С третьей стороны, после меппинга секции себе в адресное пространство, процесс не трогает ее — следовательно, физические страницы по этим адресам не впечатываются и никаких изменений в Working Set процесса не происходит.
Строго говоря, пейджфайл действительно «используется» — в нем резервируется место (не конкретное положение, а именно место, как размер), но при этом реальная страница, для которой это место было зарезервировано может находиться в физической памяти, на диске или И ТАМ И ТАМ одновременно. Вот такая вот циферка, признайтесь честно, сколько раз глядя на «Page File usage» в Task Manager-е Вы действительно понимали, что она означает.
Что же до Processes таба — там все еще по дефолту показывается Memory (Private Working Set) и несмотря на то, что он называется совершенно правильно и не должен вызывать недоразумений у знающих людей — проблема в том, что подавляющее большинство людей, которые смотрят на эти цифры совершенно не понимают, что они означают. Простой эксперимент: запускаем утилилиту RamMap (советую скачать весь комплект), запускаете Task Manager со списком процессов. В RamMap выбираете в меню Empty->Empty Working Sets и смотрите на то, что происходит с памятью процессов.
Если кого-то все еще раздражают циферки в Task Manager-е, можете поместить следующий код в профайл павершелла:
add-type -Namespace Win32 -Name Psapi -MemberDefinition @" [DllImport("psapi", SetLastError=true)] public static extern bool EmptyWorkingSet(IntPtr hProcess); "@ filter Reset-WorkingSet { [Win32.Psapi]::EmptyWorkingSet($_.Handle) } sal trim Reset-WorkingSet
После чего станет возможно «оптимизировать» использование памяти одной командой, например для «оптимизации» памяти, занятой хромом: ps chrome | trim
Или вот «оптимизация» памяти всех процессов хрома, использующих больше 100 Мб физической памяти: ps chrome |? {$_.WS -gt 100Mb} | trim
Если хотя бы половина прочитавших отметет саму идею о подобной «оптимизации», как очевиднейший абсурд — можно будет сказать, что я не зря старался.
Кеш
В первую очередь отмечу, что кеш в Windows не блочный, а файловый. Это дает довольно много преимуществ, начиная от более простого поддержания когерентности кеша например при онлайн дефрагментации и простого механизма очистки кеша при удалении файла и заканчивая более консистентными механизмами его реализации (кеш контроллер реализован на основе механизма memory mapping-а), возможностью более интеллектуальных решений на основе более высокоуровневой информации о читаемых данных (к примеру интеллектуальный read-ahead для файлов открытых на последовательный доступ или возможность назначать приоритеты отдельным файловым хендлам).
В принципе из недостатков я могу назвать только значительно более сложную жизнь разработчиков файловых систем: слышали о том, что написание драйверов — это для психов? Так вот, написание драйверов файловых систем — для тех, кого даже психи считают психами.
Если же описывать работу кеша, то все предельно просто: когда файловая система запрашивает у кеш-менеджера какую нибудь часть файла, последний просто меппит часть этого файла в специальный «слот», описываемый структурой VACB (посмотреть все смепленные файлы можно из отладчика ядра с помощью расширения !filecache) после чего просто выполняет операцию копирования памяти (RtlCopyMemory). Происходт Page Fault, так как сразу после отображения файла в память все страницы невалидны и дальше система может либо найти необходимую страницу в одном из «свободных» списков либо выполнить операцию чтения.
Для того, чтобы понять рекурсию нужно понять рекурсию. Каким же образом выполняется операция чтения файла, необходимая для завершения операции чтения этого самого файла? Здесь опять все достаточно просто: пакет запроса на ввод/вывод (IRP) создается с флагом IRP_PAGING_IO и при получении такого пакета файловая система уже не обращается к кешу, а идет непосредственно к нижележащему дисковому устройству за данными. Все эти смепленные слоты идут в System Working Set и составляют ЧАСТЬ кеша.
Страница из лекции какого то токийского университета (эх, мне бы так):
На этом работа собственно кеш-менеджера заканчивается и начинается работа менедера памяти. Когда выше мы делали EmptyWorkingSet это не приводило ни к какой дисковой активности, но тем не менее, физическая память используемая процессом сокращалась (и все физические страницы действительно уходили из адресного пространства процесса делая его почти полностью невалидным). Так куда же она уходит после того, как отбирается у процесса? А уходит она, в зависимости от того, соответствует ли ее содержимое тому, что было прочитано с диска, в один из двух списков: Standby (начиная с Висты это не один список, а 8, о чем позже) или Modified:
Standby список таким образом — это свободная память, содержащая какие то данные с диска (в том числе возможно и pagefile-а).
Если Page Fault происходит по адресу, который спроецирован на часть файла, которая все еще есть в одном из этих списков — она просто возвращается обратно в рабочий набор процесса и впечатывается по искомому адресу (этот процесс называется softfault). Если нет — то, как и в случае со слотами кеш менеджера, выполняется PAGING_IO запрос (называется hardfault).
Modified список может содержать «грязные» страницы достаточно долго, но либо когда размер этого списка чрезмерно вырастает, либо по когда система видит недостаток свободной памяти, либо по таймеру, просыпается modified page writer thread и начинает частями сбрасывать этот список на диск, и перемещая страницы из modified списка в standby (ведь эти страницы опять содержат неизмененную копию данных с диска).
Upd:
Пользователь m17 дал ссылки на выступление Руссиновича на последнем PDC на ту же тему (хм, я честно его до этого не смотрел, хотя пост во много перекликается). Если понимание английского на слух позволяет, то чтение данного топика можно заменить прослушиванием презентаций:
Mysteries of Windows Memory Management Revealed, Part 1 of 2
Mysteries of Windows Memory Management Revealed, Part 2 of 2
Пользователь DmitryKoterov подсказывает, что перенос пейджфайла на RAM диск иногда действительно может иметь смысл (вот уж никогда б наверное и не догадался, если б не написал топик), а именно, если RAM-диск использует физическую память, недоступную остальной системе (PAE + x86 + 4+Gb RAM).
Пользователь Vir2o в свою очередь подсказывает что хотя при некоторых условиях и пожертвовав стабильностью системы ram-диск, использующий физическую память, невидимую остальной системе написать можно, но такое очень маловероятно.
The memory management in the operating system is to control or maintain the main memory and transfer processes from the primary memory to disk during execution. Memory management keeps track of all memory locations, whether the process uses them or not. Determines how much memory should be allocated to each process. Specifies how much memory each process should be given. It decides which processes will be remembered and when. It tracks when memory is released or when it is shared and changes the status accordingly.
Windows Memory Management
Microsoft Windows has its own virtual address space for each 32-bit process, allowing up to 4 gigabytes of memory to be viewed. Each process has 8-terabyte address space on 64-bit Windows. All threads have access to the visible address space of the process. Threads, on the other hand, do not have access to the memory of another process, which protects one process from being damaged by another.
Architecture for 32-bit Windows: The automatic configuration of the 32-bit Windows Operating System (OS) allocates 4 GB (232) of accessible memory space to the kernel and user programs equally. With 4 GB physical memory available, the kernel will receive 2 GB and the app memory will receive 2 GB. Kernel-mode address space is shared by all processes, but application mode access space is provided for each user process.
Architecture for 64-bit Windows: The automatic configuration of the 64-bit Windows Operating System (OS) allocates up to 16 TB (254) of accessible memory space to the kernel and user programs equally. As 16 TB real memory is available, the kernel will have 8 TB of virtual address (VA) space and user application memory will have 8 TB of VA space. Visible address space in the kernel is allocated for all processes. Each 64-bit functionality gets its place, but each 32-bit system works on a 2 GB (Windows) virtual machine.
Virtual Address Space
The process’ visible address space is the range of memory addresses that you can use. The address area of each process is private, and can only be accessed through other processes if it is shared.
A virtual address does not reflect the actual location of an object in memory; instead, the system stores a table for each process, which is an internal data structure that converts visible addresses into local addresses. The program converts the virtual address into a local address every time the chain refers to it.
The virtual address area of Windows is divided into two parts: one for process use and the other for system usage.
Virtual Memory Functions
A process can alter or determine the state of pages in its virtual address space using virtual memory functions.
The width of the visible address space is reserved for the process. Although saving address space does not provide material storage, it prevents scope from using other sharing processes. It does not affect other active address spaces for other processes. Page storage reduces unnecessary use of virtual storage while allowing the process of setting aside part of its address space for the flexible data structure. As required, the procedure can provide a physical repository for this area.
Provide a set of cached pages in the address of the process so that only a shared process can access real storage (either RAM or disk).
For the most dedicated pages, specify read/write, read-only, or no access. This differs from the general distribution procedures, which often provide read/write access to the pages.
Release a set of saved pages, making the visible address set accessible for the following call process sharing actions.
We can withdraw a group of committed pages, freeing up portable storage that can be assigned to any process in the future.
To prevent the program from changing pages in the file, lock one or more memory pages bound to the virtual memory (RAM). Find information about a set of pages in a call process or the address space of a specific process. It may change the access protection of a set of pages bound to the physical address of the call process.
Heap Functions
The system provides a default heap for each process. Private heaps can help applications that make frequent allocations from the heap perform better. A private heap is a block of one or more pages in the caller process’s address space. After constructing the private heap, the process manages the memory in it via operations like HeapAlloc and HeapFree.
File Mapping
The association of file content with a piece of visible address space in the process is known as a file map. To track this relationship, the system creates a file map maker (also known as a category object). File view is the physical address area are used for the file content access process. The process may use both input and outgoing sequences (I/O) thanks to the file map. It also allows the process to work effectively with large data files, such as websites, without requiring the entire file to be mapped to memory. Files with a memory map can be used with many processes to exchange data.
Last Updated :
01 Feb, 2022
Like Article
Save Article
Управление памятью в Windows
Прежде чем перейти к рассмотрению формата PE, необходимо поговорить об особенностях управления памятью в Windows, так как без знания этих особенностей невозможно понять некоторые существенные детали формата.
Управление памятью в Windows NT/2k/XP/2k3 осуществляет менеджер виртуальной памяти (virtual-memory manager). Он использует страничную схему управления памятью, при которой вся физическая память делится на одинаковые отрезки размером в 4096 байт, называемые физическими страницами. Если физических страниц не хватает для работы системы, редко используемые страницы могут вытесняться на жесткий диск, в один или несколько файлов подкачки (pagefiles). Вытесненные страницы затем могут быть загружены обратно в память, если возникнет необходимость. Таким образом, программы могут использовать значительно большее количество памяти, чем реально присутствует в системе.
Виртуальное адресное пространство процесса
Каждый процесс в Windows запускается в своем виртуальном адресном пространстве размером в 4 Гб. При этом первые 2 Гб адресного пространства могут непосредственно использоваться процессом, а остальные 2 Гб резервируются операционной системой для своих нужд (рис. 2.1).
Рис.
2.1.
Виртуальное адресное пространство процесса
Виртуальное адресное пространство также делится на виртуальные страницы размером в 4096 байт. При этом процессу выделяется только то количество виртуальных страниц, которое ему реально нужно. Поэтому тот факт, что процесс может адресовать 4 Гб виртуального адресного пространства, еще не означает, что каждому процессу выделяется по 4 Гб оперативной памяти. Как правило, процесс использует только малую часть своего адресного пространства, хотя стремительное удешевление модулей памяти способно в самое ближайшее время существенно изменить картину и вызвать повсеместно переход к 64-разрядным архитектурам.
Виртуальные страницы могут отображаться операционной системой в страницы физической памяти, могут храниться в файле подкачки, а также могут быть вообще недоступны процессу. Обращение к недоступной виртуальной странице вызывает аварийное завершение процесса с сообщением «Access violation«.
Адресное пространство процесса называется виртуальным, потому что процесс для работы с памятью использует не реальные адреса физической памяти, а так называемые виртуальные адреса. При обращении по некоторому виртуальному адресу происходит перевод этого виртуального адреса в физический адрес. Перевод виртуальных адресов в физические адреса реализован на аппаратном уровне в процессоре и поэтому осуществляется достаточно быстро.
Рис.
2.2.
Перевод виртуального адреса в физический адрес
Рассмотрим на примере, как осуществляется перевод некоторого виртуального адреса vx в физический адрес px (рис 2.2). Сначала вычисляется номер vnum виртуальной страницы, соответствующий виртуальному адресу vx, а также смещение delta виртуального адреса относительно начала этой виртуальной страницы:
vnum := vx div 4096; delta := vx mod 4096;
Далее возможны три варианта развития событий:
- Виртуальная страница vnum недоступна. В этом случае перевод виртуального адреса vx в физический адрес невозможен, и процесс завершается с сообщением «Access Violation«;
- Виртуальная страница находится в файле страничной подкачки, и ее надо сначала загрузить в память. Тогда пусть pnum будет номером физической страницы, в которую мы загружаем нашу виртуальную страницу;
- Виртуальная страница уже находится в памяти, и ей соответствует некоторая физическая страница. В этом случае pnum — номер этой физической страницы.
После чего адрес px вычисляется следующим образом:
Такая организация памяти процесса обладает следующими свойствами:
- Процессы изолированы друг от друга. Один процесс не может обратиться к памяти другого процесса.
- Передача виртуальных адресов между процессами совершенно бессмысленна. Один и тот же виртуальный адрес в адресных пространствах разных процессов соответствует разным физическим адресам.
- Процессы используют преимущества плоской адресации памяти. Виртуальный адрес представляет собой 32-разрядное целое значение, что делает возможной легкую реализацию адресной арифметики.
Отображаемые в память файлы
Отображаемые в память файлы (memory-mapped files) — это мощная возможность операционной системы. Она позволяет приложениям осуществлять доступ к файлам на диске тем же самым способом, каким осуществляется доступ к динамической памяти, то есть через указатели. Смысл отображения файла в память заключается в том, что содержимое файла (или часть содержимого) отображается в некоторый диапазон виртуального адресного пространства процесса, после чего обращение по какому-либо адресу из этого диапазона означает обращение к файлу на диске. Естественно, не каждое обращение к отображенному в память файлу вызывает операцию чтения/записи. Менеджер виртуальной памяти кэширует обращения к диску и тем самым обеспечивает высокую эффективность работы с отображенными файлами.
В Win32 API используется
плоская 32-разрядная модель памяти.
Каждому процес-
су выделяется
“личное” (private) изолированное адресное
пространство, размер которого
составляет
4Gb. Это пространство разбивается на регионы, немного отличные для
Windows9x и Windows NT. В
общем для той и другой системы можно
сказать, что ниж-
ние 2Gb этого
пространства отведены процессу для
свободного использования, а верх-
ние 2Gb зарезервированы
для использования операционной системой.
На рис.4.3. пред-
ставлена архитектура памяти Windows 9x, а
на рис.4.4 – Windows NT.
Рис.4.3.
Архитектура
памяти
Windows 9x.
104
Рис.4.4.
Архитектура
памяти
Windows NT.
Как видно из данных рисунков, код системы
Windows NT лучше защищен от про-
цесса пользователя,
чем код Windows 9x. Это обуславливает большую
устойчивость ОС
к ошибкам в прикладной программе.
Используемые
прикладными программами Windows 32-разрядные
адреса для дос-
тупа к коду и данным, не являются
32-разрядными физическими адресами, которые
микропроцессор
использует для адресации физической
памяти (В настоящее время фи-
зическая память
размером 2-4 Гб не используется в ПЭВМ).
Поэтому адрес, который ис-
пользуется приложением, является
виртуальным адресом (virtual address).
API-функции Windows,
работающие в логическом адресном
пространстве объемом
до 2 Гб, поддерживаются менеджером виртуальной памяти
— VMM (Virtual Memory
Manager), который, в свою очередь, оптимизирован для работы на современных
32-
разрядных
процессорах. Для того чтобы аппаратное
обеспечение системы могло исполь-
зовать
32-разрядную адресацию памяти,
Windows обеспечивает отображение физиче-
ских адресов в виртуальном адресном пространстве и
поддерживает страничную орга-
низацию памяти. На этой основе VMM формирует
собственные механизмы и алгоритмы
управления виртуальной памятью.
Подобно виртуальной
реальности, виртуальной оперативной
памяти не существу-
ет, однако у системы
возникает полная иллюзия ее наличия. 2
Гб адресного пространст-
ва, к которому может
обращаться любая программа, представляют
собой виртуальную
память. На самом
деле в системе нет 2 Гб физической памяти,
однако каким-то образом
программы могут использовать весь диапазон адресов.
Очевидно, присутствует некий
фоновый «переводчик»,
молчаливо преобразующий каждое обращение
к памяти в реаль-
ный физический адрес. Благодаря такому преобразованию реализуется большинство
возможностей
менеджера виртуальной памяти (VMM). VMM —
это часть операционной
системы
Windows, которая отвечает за преобразование ссылок на виртуальные адреса
памяти в ссылки на
реальную физическую память. Менеджер
виртуальной памяти при-
нимает
решения о том, где размещать каждый
объект памяти и какие страницы памяти
записать на диск. Он также выделяет каждому выполняемому процессу отдельное ад-
ресное пространство,
отказываясь преобразовывать виртуальные
адреса одного процесса
в адреса физической
памяти, используемой другим процессом.
VMM поддерживает ил-
люзию «идеализированного»
адресного пространства объемом 2 Гб
(подобно GDI, кото-
рый создает
видимость того, что каждая программа
выводит графические данные в ко-
ординатном пространстве
«идеализированного» логического устройства). Система
105
преобразует логические адреса памяти
или логические координаты в физические
и пре-
дотвращает конфликты
между программами из-за попыток
одновременного использова-
ния одного и того
же ресурса. Рассмотрим более подробно
механизм образования вирту-
альной памяти.
Физическая память
делится на страницы (pages) размером 4096
байт (4 КБ). Следо-
вательно, каждая страница начинается с адреса, в котором младшие
12 бит нулевые.
Машина, оснащенная
8 МБ памяти, содержит
2048 страниц. Операционная система
Windows
хранит набор таблиц страниц (каждая
таблица сама представляет собой стра-
ницу) для преобразования виртуального
адреса в физический.
Каждый процесс,
выполняемый в Windows, имеет свою собственную
страницу ка-
талога (directory page)
таблиц страниц, которая содержит до
1024 32-разрядных дескрип-
тора таблиц страниц. Физический адрес
страницы каталога таблиц страниц
хранится в
регистре CR3
микропроцессора. Содержимое этого
регистра изменяется при переключе-
нии
Windows управления между процессами. Старшие
10 бит виртуального адреса оп-
ределяют один из
1024 возможных дескрипторов в каталоге таблиц страниц. В свою
очередь, старшие
20 бит дескриптора таблицы страниц определяют физический адрес
таблицы страниц
(младшие 12 бит физического адреса равны нулю). Каждая таблица
страниц содержит,
в свою очередь, до 1024 32-разрядных дескриптора
страниц. Выбор
одного из этих дескрипторов определяется содержимым средних
10 битов исходного
виртуального адреса. Старшие
20 бит дескриптора страницы определяют физический
адрес начала
страницы, а младшие 12 бит виртуального
адреса определяют физическое
смещение в пределах
этой страницы. На рис. 4.5. представлена
схема организации вир-
туального адресного пространства.
Рис.4.5.
Схема
организации
виртуального
адресного
пространства.
Очевидно, что это
сложно понять с первого раза.
Проиллюстрируем этот процесс
еще раз в символьной
форме [6]. Вы можете представить 32-разрядный
виртуальный ад-
рес (с которым
оперирует программа) в виде 10-разрядного
индекса в таблице каталога
таблиц страниц
(d), 10-разрядного индекса в таблице страниц
(р), 12-разрядного смеще-
ния (о):
dddd-dddd-ddpp-pppp-pppp-oooo-oooo-oooo
106
Для каждого процесса микропроцессор
хранит в регистре CR3 (r) старшие 20 бит
физи-
ческого адреса таблицы каталога таблиц
страниц:
rrrr-rrrr-rrrr-rrrr-rrrr
Начальный физический адрес каталога
таблиц страниц определяется как:
rrrr-rrrr-rrrr-rrrr-rrrr-0000-0000-0000
Необходимо
запомнить, что каждая страница имеет
размер 4 КБ и начинается с адреса, у
которого
12 младших бит нулевые. Сначала
микропроцессор получает физический ад-
рес:
rrrr-rrrr-rrrr-rrrr-rrrr-dddd-dddd-dd00
По этому адресу содержится другое
20-разрядное значение (t-table):
tttt-tttt-tttt-tttt-tttt
соответствующее начальному физическому
адресу таблицы страниц:
tttt-tttt-tttt-tttt-tttt-0000-0000-0000
Затем, микропроцессор осуществляет
доступ по физическому адресу:
tttt-tttt-tttt-tttt-tttt-pppp-pppp-pp00
Здесь хранится
20-битная величина, являющаяся основой
для физического адреса начала
страницы памяти (f-page frame):
ffff-ffff-ffff-ffff-ffff
Результирующий
32-разрядный
физический адрес получается в результате комбиниро-
вания основы физического адреса страницы и
12-разрядного смещения виртуального
адреса:
ffff-ffff-ffff-ffff-ffff-oooo-oooo-oooo
Это и есть результирующий физический
адрес.
Преимущества разделения памяти
на страницы огромны. Во-первых, приложения
изолированы друг
от друга. Никакой процесс не может
случайно или преднамеренно ис-
пользовать адресное пространство
другого процесса, т. к. он не имеет
возможности его
адресовать без
указания соответствующего значения
регистра CR3 этого процесса, кото-
рое устанавливается только внутри ядра
Windows.
Во-вторых, такой
механизм разделения на страницы решает
одну из основных про-
блем в многозадачной
среде — объединение свободной памяти.
При более простых схе-
мах адресации в то время, как множество
программ выполняются и завершаются,
память
может
стать фрагментированной. В случае, если
память сильно фрагментирована, про-
граммы не могут выполняться из-за
недостатка непрерывной памяти, даже
если общего
количества свободной памяти вполне достаточно. При использовании разделения на
страницы нет необходимости объединять свободную физическую память, поскольку
107
страницы необязательно должны быть
расположены последовательно. Все
управление
памятью производится
с помощью манипуляций с таблицами
страниц. Потери связаны
только собственно с самими таблицами
страниц и с их 4 Кб размером.
В-третьих, в
32-битных дескрипторах страниц существует
еще 12 бит, кроме тех,
которые используются для адреса страницы. Один из этих битов показывает возмож-
ность доступа к
конкретной странице (он называется
битом доступа, «accessed bit»); дру-
гой
показывает, была ли произведена запись
в эту страницу (он называется битом
мусо-
ра, «dirty bit»). Windows может использовать
эти биты для того чтобы определить,
можно
ли сохранить эту страницу в файле
подкачки для освобождения памяти. Еще
один бит —
бит
присутствия (present bit) показывает, была ли
страница сброшена на диск и должна
ли быть подкачена обратно в память.
Другой бит
(«чтения/записи») показывает,
разрешена ли запись в данную страницу
памяти. Этот бит обеспечивает защиту кода от
«блуждающих» указателей. Например,
если включить следующий оператор в
программу для Windows:
*(int*) WinMain = 0 ;
то на экран будет
выведено следующее окно сообщение:
«This program has performed an
illegal operation and will be
shutdown.» («Эта программа выполнила
недопустимую опера-
цию и будет завершена»). Этот бит не
препятствует компилированной и
загруженной в
память программе быть запущенной на
выполнение.
Приведем несколько замечаний по поводу
управления памятью в Windows 9x:
Виртуальные адреса имеют разрядность
32 бита. Программа и данные имеют адреса
в
диапазоне от
0х00000000 до 0x7FFFFFFF. Сама Windows использует адреса от
0х80000000 до 0xFFFFFFFF. В
этой области располагаются точки входа
в динамически
подключаемые библиотеки.
Общее количество
свободной памяти, доступной программе,
определяется как ко-
личество свободной физической памяти
плюс количество свободного места на
жестком
диске, доступного
для свопинга страниц. Как правило, при
управлении виртуальной па-
мятью Windows использует
алгоритм LRU (least recently used) для определения
того, ка-
кие страницы будут
сброшены на диск. Бит доступа и бит
мусора помогают осуществить
эту операцию. Страницы кода не должны
сбрасываться на диск: поскольку запись
в его
страницы запрещена,
они могут быть просто загружены из
файла с расширением .ЕХЕ
или из динамически подключаемой
библиотеки.
Организацией
свопинга занимается VMM. При генерации
системы на диске обра-
зуется специальный
файл свопинга, куда записываются
те страницы, которым не нахо-
дится места в физической памяти. Процессы могут захватывать память в своем
32-
битном адресном
пространстве и, затем, использовать ее.
При обращении потока к ячей-
ке памяти могут возникнуть три различные
ситуации [12]:
• Страница
существует и находится в памяти
• Страница
существует и выгружена на диск
• Страница не
существует
При этом VMM использует
алгоритм организации доступа к
данным, представленный
на рис.4.6 [8].
Запуск на исполнение EXE — модуля происходит
следующим образом: EXE — файл
проецируется
на память. При этом он не переписывается
в файл подкачки. Просто эле-
менты каталога и таблиц страниц настраиваются так, чтобы они указывали на
EXE —
файл, лежащий на
диске. Затем передается управление на
точку входа программы. При
этом происходит возникает исключение, обрабатывая которое стандартным образом,
VMM
загружает в память требуемую страницу
и программа начинает исполняться. Та-
кой механизм существенно ускоряет процедуру запуска программ, так как загрузка
108
страниц EXE — модуля происходит по мере
необходимости. Образно говоря, программа
сперва начинает
исполняться, а потом загружается в память.
Если программа записана
на дискете, то она перед началом исполнения
переписывается в файл подкачки.
Рис.4.6.
Алгоритм
организации
доступа
к
данным.
Для поддержания иллюзии огромного адресного пространства менеджеру вирту-
альной памяти необходимо знать, как правильно организовать данные. Все операции
распределения
памяти, которые процесс выполняет в
выделенном ему диапазоне вирту-
альных адресов,
записываются в виде дерева дескрипторов
виртуальных адресов — VAD
(Virtual Address Descriptor).
Каждый раз при выделении программе
памяти VMM создает
дескриптор виртуального адреса
(VAD) и добавляет его» к дереву (рис.4.7)
[12]. VAD
содержит
информацию о запрашиваемом диапазоне
адресов, статусе защиты всех стра-
ниц в указанном диапазоне, а также о том, могут ли дочерние процессы наследовать
объекты, которые
находятся в данном диапазоне адресов.
Если поток использует адрес,
который не
определен ни в одном дескрипторе,
менеджер виртуальной памяти воспри-
нимает его как адрес, который никогда
не резервировался, вследствие
чего возникает
ошибка доступа.
Намного проще
формировать VAD, чем создавать таблицу
страниц и заполнять ее
адресами
действительных страничных блоков. Кроме
того, объем выделенной памяти не
влияет на скорость
проведения операции. Резервирование 2
Кб происходит не быстрее,
чем выделение
2 Мб: по каждому запросу создается один дескриптор.
Если поток ис-
109
пользует зарезервированную память, VMM
закрепляет за ним страничные блоки,
копи-
руя информацию из дескриптора в новую
запись таблицы страниц.
0x40100000-0x40110000
чтение/запись не наследуется
0x200C0000-0x200D1000
только чтение не наследуется
0x60FD000-0x60FE1000
нет доступа не наследуется
0x100F7000-0x111D9000
чтение/запись не наследуется
0x372D5000-0x372EF000
копирование при записи
не наследуется
Рис
.4.7
Дерево
дескрипторов
виртуальных
адресов
(VAD)
Диспетчер управления памятью (VMM)
является составной частью ядра операци-
онной системы. Приложения не могут
получить к нему прямой доступ.
В прошлый раз мы рассмотрели вопросы использования глобальных переменных. Прежде, чем двинуться дальше, нужно дать небольшой вводный курс по памяти в Windows.
Любая вещь в вашей программе занимает «память компьютера». Это может быть строка, число, открытый файл, запись, объект, форма и даже сам код. Даже хотя вы явно никого не просили выделять память, она всё равно выделена автоматически — либо компилятором, либо операционной системой.
Адресное пространство и все, все, все…
Кратко говоря, память программы может рассматриваться как один очень-очень длинный ряд байтов. Байт — это единица измерения количества информации, в мире Delphi и Windows он равен восьми битам и может хранить одно из 256
различных значений (от 0
до 255
). На память можно смотреть как на массив байт. Что именно содержат эти байты — зависит от того, как интерпретировать их содержимое, т.е. от того, как их используют. Значение 97 может означать число 97
, или же ANSI букву 'a'
. Если вы рассматриваете вместе несколько байт, то вы можете хранить и большие значения. Например, в 2-х байтах вы можете хранить одно из 256*256 = 65536
различных значений, две ANSI буквы 'ab'
или Unicode букву 'a'
— и т.д.
Чтобы обратиться к конкретному байту в памяти (адресовать его), можно присвоить каждому байту номер, пронумеровав их целыми положительными числами, включив ноль за начало отсчёта. Индекс байта в этом огромном массиве и называется его адресом, а весь массив целиком — памятью программы. Диапазон адресов от 0
до максимума называется адресным пространством программы. А максимум (длина) массива называется размером адресного пространства.
(примечание: ну, на самом деле, есть тысяча и один способ адресовать память, но в рамках современного мира и этой статьи мы ограничимся только этим способом).
Адресное пространство (вернее, его размер) определяет способность программы работать с данными. Чем оно больше — тем с большим количеством данных программа сможет работать (в один момент времени). Если у программы заканчивается свободное место в адресном пространстве (т.е. все адреса в нём выделены под какие-то объекты в программе) — то у программы заканчивается память (out of memory).
Как адресное пространство соотносится с вашим исходным кодом
С точки зрения языка высокого уровня (Паскаль) все вещи в вашей программе характеризуются именем (идентификатором), типом («сколько памяти выделять») и семантикой («что с этим можно делать»). Например, целое число занимает 4
байта и их можно читать, писать, складывать и т.п. И число A — это не то же самое, что число B. Строки же занимают переменный объём памяти, их, к примеру, можно соединять и редактировать. И так далее.
Но на уровне машинного языка, железа и операционной системы все они характеризуются только местоположением, размером (в байтах) и атрибутами доступа. Местоположение — это адрес объекта. К примеру, число A может иметь адрес 1234
, а число B — 1238
. И поэтому это два разных числа — потому что у них разный адрес, т.е. они лежат в разных местах. Атрибут доступа является упрощённой «семантикой», которая определяет то, что можно делать с памятью. А таких вещей всего три: читать, писать и выполнять. Последнее означает исполнение машинного кода. Тут нужно пояснить, что ваши данные (числа, строки, формы и т.п.) находятся в одном «контейнере» (том самом «массиве памяти из байт») вместе с кодом программы — .exe файлом. Иными словами, код рассматривается наравне с данными, а чтобы их отличать и служат атрибуты доступа.
Можно увидеть, как понятия языка высокого уровня («имя», «тип» и «семантика») проецируются в понятия низкого уровня («адрес», «размер» и «атрибуты доступа»).
Древний мир
В давние времена память программы была тождественно равна оперативной памяти машины (т.н. ОЗУ или RAM — Random Access Memory). Иными словами, размер адресного пространства программы был равен размеру установленной оперативной памяти. Вот, установлено на вашей машине две планки памяти по 64
Кб — значит, у вашей программы есть 128
Кб памяти. Ну, за вычетом той памяти, что уже занята, конечно же. Адрес объекта программы был равен адресу физической ячейке оперативной памяти (физическому адресу). И если у вас заканчивалось место в ОЗУ, то у вас заканчивалась память в программе.
Конечно, такой способ хотя и весьма прост, имеет две проблемы:
- Память программы ограничена оперативной памятью. А раньше эта память была дорогой и её было очень мало.
- Если нужно запустить две программы, то они будут работать «в одной песочнице»: и первая и вторая программа будут размещать свои данные в одном месте — оперативной памяти. И если первая программа по ошибке запишет что-то в данные второй, то… ой.
Виртуальная память и виртуальное адресное пространство
Поэтому в современном мире используется совершенно другая схема: во-первых, память программы теперь больше не тождественна оперативной памяти. Теперь программа работает исключительно с так называемой «виртуальной памятью». Виртуальная память — это имитация реальной памяти. Она позволяет каждой программе:
- считать, что установлено максимальное теоретически возможное количество оперативной памяти;
- считать, что она является единственной программой, запущенной на машине.
Иными словами, адресное пространство программы более не ограничено размером физической памяти (так называют оперативную память компьютера, чтобы специально указать на её отличие от виртуальной памяти) — адресное пространство имеет теперь максимально возможный размер. К примеру, если для адресации используются 32-битные указатели (4 байта), то размер адресного пространства равен 2^32 = 4'294'967'296
байт. Т.е. 4 миллиарда (если угодно: биллионов) или 4 Гб. А размерность адресного пространства — равна 32
.
В связи с новомодным «переходом на 64
бита» нужно упомянуть, что этот переход заключается в замене 4
-байтных (32
-битных) указателей на 8-байтные (64-битные) — что увеличивает размер адресного пространства программы аж до 2^64 = 18'446'744'073'709'551'616
байт. Т.е. 18
с лишним квинтиллионов байт или 16
Эб (эксабайт) для краткости. Соответственно, 32
-битный указатель может быть любым числом от 0
до 4'294'967'296
(от $00000000
до $FFFFFFFF
). 64
-разрядный указатель может варьироваться от $00000000'00000000
до $FFFFFFFF'FFFFFFFF
.
А из второго пункта следует, что 4
Гб или 16
Эб есть у каждой программы. Т.е. каждой программе отводится своё личное закрытое адресное пространство. Такая изолированность означает, что программа А в своем адресном пространстве может хранить какую-то запись данных по адресу $12345678
, и одновременно у программы В по тому же адресу $12345678
(но уже в его адресном пространстве) может находиться совершенно иная запись данных. Если программа A попробует прочитать данные по адресу $12345678
, то она получит доступ к своей записи (записи программы A), а не данным программы B. Но если к адресу $12345678
обратится программа B, то она получит свою запись, а не запись программы А. Иными словами, программа A не может обратиться в памяти (адресному пространству) программы B и наоборот.
Таким образом, при использовании виртуальной памяти упрощается программирование, так как программисту больше не нужно учитывать ограниченность памяти, или согласовывать использование памяти с другими приложениями. Для программы выглядит доступным и непрерывным всё допустимое адресное пространство, вне зависимости от наличия в компьютере соответствующего объема ОЗУ. Если программы выделяют в их адресных пространствах больше памяти, чем есть в системе физической памяти, то часть памяти из ОЗУ переносится на диск («винчестер») — в т.н. файл подкачки (его ещё называют страничным файлом, page file, SWAP-файлом или «свопом»). Когда программа обращается к своим данным, которые были выгружены на диск, то операционная система автоматически загрузит данные из файла подкачки в ОЗУ. И всё это происходит под капотом — т.е. совершенно незаметно для программы. С точки зрения программы, ей кажется, что она работает с 4
Гб или 16
Эб RAM.
Применение механизма виртуальной памяти позволяет:
- упростить адресацию памяти программами;
- рационально управлять оперативной памятью компьютера (хранить в ней только активно используемые области памяти);
- изолировать программы друг от друга (программа полагает, что монопольно владеет всей памятью).
А теперь, пока вы не перевозбудились от колоссального объема адресного пространства, предоставляемого вашей программе: вспомните, что оно — виртуальное, а не физическое. Другими словами, (виртуальное) адресное пространство — всего лишь диапазон адресов памяти. Конечно, нехватка памяти теперь не происходит, когда заканчивается свободное место в оперативной памяти. И на машине с 256
Мб ОЗУ, любая программа может выделить, скажем, один кусок в 512
Мб памяти. Конечно же, это не означает, что вы можете выделить аж 16
эксабайт — ведь реальный размер ограничен размером диска. И не факт, что в системе будет диск на 16
эксабайт. Тем не менее, это значительно лучше, чем просто 256
Мб оперативной памяти, установленные на вашем «старичке».
(примечание: по непонятной мне причине, некоторые люди не верят в тот простой факт, что программа может спокойно выделить больше памяти, чем установлено физической памяти в системе; звучит как сюжет для разрушителей легенд (MythBusters)).
Чем чаще системе приходится копировать данные из оперативной памяти в файл подкачки и наоборот, тем больше нагрузка на жесткий диск и тем медленнее работает операционная система (при этом может получиться так, что операционная система будет тратить всё свое время на подкачку памяти, вместо выполнения программ). Поэтому, добавив компьютеру оперативной памяти, вы снизите частоту обращения к жёсткому диску и, тем самым, увеличите общую производительность системы. Кстати, во многих случаях увеличение оперативной памяти дает больший выигрыш в производительности, чем замена старого процессора на новый. А с падением цен на память уже не проблема собрать систему с 16
или 32
Гб оперативной памяти по доступной цене.
Факты о виртуальном адресном пространстве
Хотя в самом начале мы рассматривали память программы (адресное пространство) как один непрерывный однородный блок, сейчас настало время сделать уточнение, что я вам наврал: таковым он не является. Адресное пространство, хотя действительно однородно и непрерывно более чем на 99%
, но в нём есть несколько специальных областей. Я не буду подробно разбирать их все, скажу только о самых важных.
Во-первых, это область для отлова нулевых указателей. Это, определённо, самая важная специальная часть адресного пространства. Начинается она в нуле и заканчивается на адресе 65'535
. Т.е. имеет размер в 64
Кб и расположена в диапазоне $00000000-$0000FFFF
— самом начале адресного пространства. Специальна эта область тем, что она всегда заблокирована: в ней нельзя выделить память, а любое обращение по этим адресам всегда возбуждает исключение access violation (примечание: это не единственная причина возбуждения access violation). Эта область сделана исключительно для нашего удобства. Как вы узнаете потом (или уже знаете), нулевой указатель nil
по числовому значению равен 0
. И если вы случайно (по ошибке) обратитесь к нулевому указателю — то эта область поможет вам возбудить исключение и поймать вашу ошибку.
А что такого особенного в числе 65'535
? Ну, 64
Кб — это гранулярность выделения памяти. Гранулярность выделения памяти определяет, блоками каких размеров вы можете оперировать при выделении и освобождении памяти. Т.е. гранулярность выделения памяти в 64
Кб означает, что вы можете выделять только блоки памяти, размер которых кратен 64
Кб. Зачем так делается? Ну, если вы попробуете вести учёт «выделенности» каждого байта в программе, то размер управляющих структур у вас превысит размер самих данных. Поэтому память выделяют «кластерами». Иными словами, если вы хотите расположить область в начале адресного пространства, то вы не можете выделить меньше, чем 64
Кб. А больше? Больше — можно. Например, 64 + 64 = 128
Кб. Но большого смысла в этом нет.
Почему гранулярность выделения памяти равна именно 64
Кб, а не, скажем, 8
Кб? Ну, на это есть исторические причины.
(примечание: полностью аналогичный блок расположен на границе 2
Гб — но уже по совершенно другим причинам).
Далее, что вам ещё нужно знать про виртуальное адресное пространство — оно доступно вам не полностью. Грубо говоря, в виртуальном адресном пространстве каждой программы сосуществуют сама программа и операционная система. Та часть, где работает ваша программа (и о котором мы говорили всё это время выше), называется разделом для кода и данных пользовательского режима (user mode). Та часть, где работает операционная система, называется разделом для кода и данных режима ядра (kernel mode). Обе эти части находятся в едином адресном пространстве программы.
Чем они отличаются? Про пользовательский раздел мы уже много чего сказали: он свой у каждой программы и это полностью ваш раздел — делайте что хотите. Раздел ядра является здесь особенным в двух моментах: во-первых, у вашей программы нет к нему никакого доступа. Вообще и в принципе это невозможно. Там орудует только операционная система, но не вы. Если вы попробуете обратиться к памяти в этом разделе, то получите просто access violation. Во-вторых, особенность раздела в том, что он разделяется между всеми программами. Да, вот так: пользовательская часть у каждого адресного пространства своя, но часть ядра — одна и та же, общая. По сути, раздел ядра является «адресным пространством режима ядра».
Какой размер имеют эти две части адресного пространства? Ну, если мы говорим про 32
-разрядную программу, то пользовательский раздел занимает от 2
до 4
Гб (по умолчанию — 2
Гб). Соответственно, режим ядра занимает от 0
до 2
Гб (ибо суммарно должно быть 4
Гб). Конечно же, это за вычетом уже упоминаемых специальных областей. Итого: по умолчанию адресное пространство 32
-разрядной программы делится пополам. Половина — вам, и половина — операционной системе.
(примечание: 0 Гб под режим ядра — это специальный особый случай, достижимый только при запуске 32-битной программы на 64-битной машине. В обычных условиях граница между разделами может двигаться от 2 до 3 Гб).
Если говорить совсем точно, то раздел для ваших данных в случае 32
-х бит имеет диапазон $0000FFFF-$7FFEFFFF
(или $BFFFFFFF
в максимуме на 3 Гб, с дыркой на 64
Кб в районе 2
Гб), а раздел режима ядра — $80000000-$FFFFFFFF
(или $C0000000-$FFFFFFFF
в максимуме для user mode). В случае 64
-разрядной программы ситуация будет несколько иная. На сегодняшний день в Windows соотношение выглядит так: user mode — $00000000'00010000-$000003FF'FFFEFFFF
(8
Тб); kernel mode — $00000400'00000000-$FFFFFFFF'FFFFFFFF
. Ну, это всё ещё недостаточно точно, ведь, на самом деле, режим ядра в случае 64
-х бит использует только максимум несколько сотен Гб, оставляя большую часть адресного пространства попросту неиспользуемой. Т.е. у нас в дополнение к двум областям (user mode и kernel mode) появляется ещё и третья: зарезервированная область. Которую, впрочем, со стороны user mode удобно считать частью kernel mode. Сделано это по той простой причине, что 64
-битное адресное пространство настолько огромно, что user mode и kernel mode выглядели бы в нём тонюсенькими полосочками, вздумай бы вы изобразить их графически и в масштабе. А если место просто зарезервировано, то и не нужно делать для него управляющих данных. Даже 8
Тб памяти для user mode — это очень много. Если бы вы выделяли мегабайт памяти в секунду, у вас бы ушло три месяца, чтобы исчерпать такое адресное пространство.
Это что касается изолированности одной программы от других и от операционной системы. Внутри программы её модули (exe, DLL, bpl) друг от друга, вообще говоря, никак не изолированы. Однако на практике граница всё же появляется, но связана она с языковыми различиями и особенностью управления памятью в разных языках программирования. Но это разговор для другого раза.
Если вы забудете всё то, что я тут говорил, то вот факт, который вы должны вынести из этого обсуждения: размер памяти программы ограничен 2 Гб (32-битная программа) или 8 Тб (64-битная программа), либо суммарным размером оперативной памяти и файлом подкачки — смотря что меньше, а что больше. Т.е. на практике вы получаете «out of memory» только когда превышаете размер в 2 Гб.
Операции, производимые с виртуальной памятью
Ну, вполне очевидно, что прежде чем использовать память, вы должны её выделить (commit), а после окончания работы — освободить (release). Конечно, вам не обязательно делать это в явном виде — как я уже сказал, часто за вас это делает кто-то другой автоматически. Но об этом в следующий раз.
Помимо двух операций (выделения и освобождения памяти) существует и третья операция — резервирование (reserve) памяти. Смысл её заключается в том, что под зарезервированную виртуальную память не выделяется никакой реальной памяти (будь то оперативная память или файл подкачки), но при этом память считается занятой, как если бы она была выделена. Позднее, вы можете выделить реальную память этому зарезервированному блоку (полностью или частями).
Зачем нужна такая операция? Ну, предположим, вам нужен непрерывный блок памяти для работы (к примеру, чтобы обработать его за один проход одним циклом), но вы выделяете память не сразу а частями — по мере надобности. Вы не можете просто выделить первый блок, а потом — второй: ведь тогда нет гарантии, что они будут идти друг за другом. Вот поэтому и придумали операцию резервирования: вы резервируете достаточно большой регион. Это — «бесплатно». Потом вы выделяете в нём реальную память. Обе цели достигнуты: вы и выделяете память по мере необходимости (а не сразу целиком), и вы получаете свою непрерывную область памяти.
Кстати, все три операции выполняются функциями VirtualAlloc
и VirtualFree
. Не забудьте только, что мы говорили про гранулярность выделения памяти в 64
Кб.
И снова: какое это имеет отношение к Delphi?
Ну, почти самое прямое. Ведь программа на Delphi должна выделять и освобождать память. Это значит, что ей нужно вызывать функции VirtualAlloc
и VirtualFree
. А выделять память она будет в своём (виртуальном) адресном пространстве — причём, только в пользовательской его части.
Операции с памятью в Delphi проводятся через функции GetMem
и FreeMem
. Конечно же, кроме этих функций в Delphi существует и много других — но они являются лишь обёртками или переходниками к GetMem
и FreeMem
. Эти обёртки (например: AllocMem
, New
, Dispose
, SetLength
и т.п.) предоставляют дополнительную функциональность и удобство (кстати, в системе тоже есть обёртки к вызовам VirtualAlloc
и VirtualFree
). В некоторых случаях, эти вызовы и вовсе скрыты и происходят автоматически под капотом языка. Например, при сложении двух строк:
var S1, S2, S3: String; begin S1 := S2 + S3;
вы не видите вызов GetMem
, но он здесь есть.
Зачем нужны «свои» подпрограммы управления памятью? Почему нельзя просто использовать VirtualAlloc
и VirtualFree
? Ну, Delphi тут не уникальна — большинство языков используют т.н. менеджеры памяти — это код, который в Delphi стоит за вызовами GetMem
и FreeMem
, который служит переходником к VirtualAlloc
и VirtualFree
. А делается это по причине всё той же гранулярности выделения в 64
Кб. Т.е. если вы создаёте 100
объектов по, скажем, 12
байт, то вместо двух килобайт (12
б * 100 = 1.2
Кб + служебные данные менеджера памяти) вы занимаете уже почти 6.5
Мб (64 * 100 = 6'400
Кб) — на несколько порядков больше! Использовали бы вы VirtualAlloc — вы бы очень быстро исчерпали свободную память. Менеджер памяти же «упаковывает» несколько запросов на выделение памяти в один блок.
(примечание: «упаковка» ни в коем случае не означает «сжатие» или «кодирование» — это просто размещение нескольких маленьких кусочков памяти в одном 64 Кб блоке).
Заметьте, что операции резервирования памяти у Delphi нет, т.к. подобная операции не имеет большого смысла при «упаковке» запросов менеджером памяти. Для работы с резервированием используются функции операционной системы.
Продолжение следует…
Вот и все базовые сведения про устройство памяти в Windows, которые вам нужно знать для начала. В следующий раз мы более близко посмотрим на то, как архитектура памяти соотносится с переменными в ваших программах.
См. также: Архитектура памяти в Windows: мифы и легенды (spin-off).
Читать далее: Адресное пространство под микроскопом.