Размер виртуального адресного пространства ос windows

Аннотация: Виртуальная память. Реализация виртуальной памяти в Windows. Структура виртуального адресного пространства. Выделение памяти процессам. Дескрипторы виртуальных адресов. Трансляция адресов. Ошибки страниц. Пределы памяти.

Виртуальная память

Всем процессам в операционной системе Windows предоставляется важнейший ресурсвиртуальная память (virtual memory). Все данные, с которыми процессы непосредственно работают, хранятся именно в виртуальной памяти.

Название «виртуальная» произошло из-за того что процессу неизвестно реальное (физическое) расположение памяти – она может находиться как в оперативной памяти (ОЗУ), так и на диске. Операционная система предоставляет процессу виртуальное адресное пространство (ВАП, virtual address space) определенного размера и процесс может работать с ячейками памяти по любым виртуальным адресам этого пространства, не «задумываясь» о том, где реально хранятся данные.

Размер виртуальной памяти теоретически ограничивается разрядностью операционной системы. На практике в конкретной реализации операционной системы устанавливаются ограничения ниже теоретического предела. Например, для 32-разрядных систем (x86), которые используют для адресации 32 разрядные регистры и переменные, теоретический максимум составляет 4 ГБ (232 байт = 4 294 967 296 байт = 4 ГБ). Однако для процессов доступна только половина этой памяти – 2 ГБ, другая половина отдается системным компонентам. В 64 разрядных системах (x64) теоретический предел равен 16 экзабайт (264 байт = 16 777 216 ТБ = 16 ЭБ). При этом процессам выделяется 8 ТБ, ещё столько же отдается системе, остальное адресное пространство в нынешних версиях Windows не используется.

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

Реализация виртуальной памяти в Windows

Схема реализации виртуальной памяти в 32-разрядной операционной системе Windows представлена на рис.11.1. Как уже отмечалось, процессу предоставляется виртуальное адресное пространство размером 4 ГБ, из которых 2 ГБ, расположенных по младшим адресам (0000 0000 – 7FFF FFFF), процесс может использовать по своему усмотрению (пользовательское ВАП), а оставшиеся два гигабайта (8000 0000 – FFFF FFFF) выделяются под системные структуры данных и компоненты (системное ВАП)1Специальный ключ /3GB в файле boot.ini увеличивает пользовательское ВАП до 3 ГБ, соответственно, уменьшая системное ВАП до 1 ГБ. Начиная с Windows Vista вместо файла boot.ini используется утилита BCDEDIT. Чтобы увеличить пользовательское ВАП, нужно выполнить следующую команду: bcdedit /Set IncreaseUserVa 3072. При этом, чтобы приложение могло использовать увеличенное ВАП, оно должно компилироваться с ключом /LARGEADDRESSAWARE.. Отметим, что каждый процесс имеет свое собственное пользовательское ВАП, а системное ВАП для всех процессов одно и то же.

Реализация виртуальной памяти в 32-разрядных Windows

Рис.
11.1.
Реализация виртуальной памяти в 32-разрядных Windows

Виртуальная память делится на блоки одинакового размера – виртуальные страницы. В Windows страницы бывают большие (x86 – 4 МБ, x64 – 2 МБ) и малые (4 КБ). Физическая память (ОЗУ) также делится на страницы точно такого же размера, как и виртуальная память. Общее количество малых виртуальных страниц процесса в 32 разрядных системах равно 1 048 576 (4 ГБ / 4 КБ = 1 048 576).

Обычно процессы задействуют не весь объем виртуальной памяти, а только небольшую его часть. Соответственно, не имеет смысла (и, часто, возможности) выделять страницу в физической памяти для каждой виртуальной страницы всех процессов. Вместо этого в ОЗУ (говорят, «резидентно») находится ограниченное количество страниц, которые непосредственно необходимы процессу. Такое подмножество виртуальных страниц процесса, расположенных в физической памяти, называется рабочим набором процесса (working set).

Те виртуальные страницы, которые пока не требуются процессу, операционная система может выгрузить на диск, в специальный файл, называемый файлом подкачки (page file).

Каким образом процесс узнает, где в данный момент находится требуемая страница? Для этого служат специальные структуры данных – таблицы страниц (page table).

Структура виртуального адресного пространства

Рассмотрим, из каких элементов состоит виртуальное адресное пространство процесса в 32 разрядных Windows (рис.11.2).

В пользовательском ВАП располагаются исполняемый образ процесса, динамически подключаемые библиотеки (DLL, dynamic-link library), куча процесса и стеки потоков.

При запуске программы создается процесс (см. лекцию 6 «Процессы и потоки»), при этом в память загружаются код и данные программы (исполняемый образ, executable image), а также необходимые программе динамически подключаемые библиотеки (DLL). Формируется куча (heap) – область, в которой процесс может выделять память динамическим структурам данных (т. е. структурам, размер которых заранее неизвестен, а определяется в ходе выполнения программы). По умолчанию размер кучи составляет 1 МБ, но при компиляции приложения или в ходе выполнения процесса может быть изменен. Кроме того, каждому потоку предоставляется стек (stack) для хранения локальных переменных и параметров функций, также по умолчанию размером 1 МБ.

Структура виртуального адресного пространства

Рис.
11.2.
Структура виртуального адресного пространства

В системном ВАП расположены:

  • образы ядра (ntoskrnl.exe), исполнительной системы, HAL (hal.dll), драйверов устройств, требуемых при загрузке системы;
  • таблицы страниц процесса;
  • системный кэш;
  • пул подкачиваемой памяти (paged pool) – системная куча подкачиваемой памяти;
  • пул подкачиваемой памяти (nonpaged pool) – системная куча неподкачиваемой памяти;
  • другие элементы (см. [5]).

Переменные, в которых хранятся границы разделов в системном ВАП, приведены в [5, стр. 442]. Вычисляются эти переменные в функции MmInitSystem (файл base\ntos\mm\mminit.c, строка 373), отвечающей за инициализацию подсистемы памяти. В файле base\ntos\mm\i386\mi386.h приведена структура ВАП и определены константы, связанные с управлением памятью (например, стартовый адрес системного кэша MM_SYSTEM_CACHE_START, строка 199).

Выделение памяти процессам

Существует несколько способов выделения виртуальной памяти процессам при помощи Windows API2См. обзор в MSDN «Comparing Memory Allocation Methods» (http://msdn.microsoft.com/en-us/library/windows/desktop/aa366533(v=vs.85).aspx).. Рассмотрим два основных способа – с помощью функции VirtualAlloc и с использованием кучи.

1. WinAPI функция VirtualAlloc позволяет резервировать и передавать виртуальную память процессу. При резервировании запрошенный диапазон виртуального адресного пространства закрепляется за процессом (при условии наличия достаточного количества свободных страниц в пользовательском ВАП), соответствующие виртуальные страницы становятся зарезервированными (reserved), но доступа к этой памяти у процесса нет – при попытке чтения или записи возникнет исключение. Чтобы получить доступ, процесс должен передать память зарезервированным страницам, которые в этом случае становятся переданными (commit).

Отметим, что резервируются участки виртуальной памяти по адресам, кратным значению константы гранулярности выделения памяти MM_ALLOCATION_GRANULARITY (файл base\ntos\inc\mm.h, строка 54). Это значение равно 64 КБ. Кроме того, размер резервируемой области должен быть кратен размеру страницы (4 КБ).

WinAPI функция VirtualAlloc для выделения памяти использует функцию ядра NtAllocateVirtualMemory (файл base\ntos\mm\allocvm.c, строка 173).

2. Для более гибкого распределения памяти существует куча процесса, которая управляется диспетчером кучи (heap manager). Кучу используют WinAPI функция HeapAlloc, а также оператор языка C malloc и оператор C++ new. Диспетчер кучи предоставляет возможность процессу выделять память с гранулярностью 8 байтов (в 32-разрядных системах), а для обслуживания этих запросов использует те же функции ядра, что и VirtualAlloc.

Дескрипторы виртуальных адресов

Для хранения информации о зарезервированных страницах памяти используются дескрипторы виртуальных адресов (Virtual Address Descriptors, VAD). Каждый дескриптор содержит данные об одной зарезервированной области памяти и описывается структурой MMVAD (файл base\ntos\mm\mi.h, строка 3976).

Границы области определяются двумя полями – StartingVpn (начальный VPN) и EndingVpn (конечный VPN). VPN (Virtual Page Number) – это номер виртуальной страницы; страницы просто нумеруются, начиная с нулевой. Если размер страницы 4 КБ (212 байт), то VPN получается из виртуального адреса начала страницы отбрасыванием младших 12 бит (или 3 шестнадцатеричных цифр). Например, если виртуальная страница начинается с адреса 0x340000, то VPN такой страницы равен 0x340.

Дескрипторы виртуальных адресов для каждого процесса организованы в сбалансированное двоичное АВЛ дерево3АВЛ дерево – структура данных для организации эффективного поиска; двоичное дерево, сбалансированное по высоте. Названо в честь разработчиков – советских ученых Г. М. Адельсон Вельского и Е. М. Ландиса. (AVL tree). Для этого в структуре MMVAD имеются поля указатели на левого и правого потомков: LeftChild и RightChild.

Для хранения информации о состоянии области памяти, за которую отвечает дескриптор, в структуре MMVAD содержится поле флагов VadFlags.

From Wikipedia, the free encyclopedia

In computing, a virtual address space (VAS) or address space is the set of ranges of virtual addresses that an operating system makes available to a process.[1] The range of virtual addresses usually starts at a low address and can extend to the highest address allowed by the computer’s instruction set architecture and supported by the operating system’s pointer size implementation, which can be 4 bytes for 32-bit or 8 bytes for 64-bit OS versions. This provides several benefits, one of which is security through process isolation assuming each process is given a separate address space.

In the following description, the terminology used will be particular to the Windows NT operating system, but the concepts are applicable to other virtual memory operating systems.

When a new application on a 32-bit OS is executed, the process has a 4 GiB VAS: each one of the memory addresses (from 0 to 232 − 1) in that space can have a single byte as a value. Initially, none of them have values (‘-‘ represents no value). Using or setting values in such a VAS would cause a memory exception.

           0                                           4 GiB
VAS        |----------------------------------------------|

Then the application’s executable file is mapped into the VAS. Addresses in the process VAS are mapped to bytes in the exe file. The OS manages the mapping:

           0                                           4 GiB
VAS        |---vvv----------------------------------------|
mapping        |||
file bytes     app

The v’s are values from bytes in the mapped file. Then, required DLL files are mapped (this includes custom libraries as well as system ones such as kernel32.dll and user32.dll):

           0                                           4 GiB
VAS        |---vvv--------vvvvvv---vvvv-------------------|
mapping        |||        ||||||   ||||
file bytes     app        kernel   user

The process then starts executing bytes in the EXE file. However, the only way the process can use or set ‘-‘ values in its VAS is to ask the OS to map them to bytes from a file. A common way to use VAS memory in this way is to map it to the page file. The page file is a single file, but multiple distinct sets of contiguous bytes can be mapped into a VAS:

           0                                           4 GiB
VAS        |---vvv--------vvvvvv---vvvv----vv---v----vvv--|
mapping        |||        ||||||   ||||    ||   |    |||
file bytes     app        kernel   user   system_page_file

And different parts of the page file can map into the VAS of different processes:

           0                                           4 GiB
VAS 1      |---vvvv-------vvvvvv---vvvv----vv---v----vvv--|
mapping        ||||       ||||||   ||||    ||   |    |||
file bytes     app1 app2  kernel   user   system_page_file
mapping             ||||  ||||||   ||||       ||   |
VAS 2      |--------vvvv--vvvvvv---vvvv-------vv---v------|

On Microsoft Windows 32-bit, by default, only 2 GiB are made available to processes for their own use.[2] The other 2 GiB are used by the operating system. On later 32-bit editions of Microsoft Windows, it is possible to extend the user-mode virtual address space to 3 GiB while only 1 GiB is left for kernel-mode virtual address space by marking the programs as IMAGE_FILE_LARGE_ADDRESS_AWARE and enabling the /3GB switch in the boot.ini file.[3][4]

On Microsoft Windows 64-bit, in a process running an executable that was linked with /LARGEADDRESSAWARE:NO, the operating system artificially limits the user mode portion of the process’s virtual address space to 2 GiB. This applies to both 32- and 64-bit executables.[5][6] Processes running executables that were linked with the /LARGEADDRESSAWARE:YES option, which is the default for 64-bit Visual Studio 2010 and later,[7] have access to more than 2 GiB of virtual address space: up to 4 GiB for 32-bit executables, up to 8 TiB for 64-bit executables in Windows through Windows 8, and up to 128 TiB for 64-bit executables in Windows 8.1 and later.[4][8]

Allocating memory via C’s malloc establishes the
page file as the backing store for any new virtual address space. However, a process can also explicitly map file bytes.

For x86 CPUs, Linux 32-bit allows splitting the user and kernel address ranges in different ways: 3G/1G user/kernel (default), 1G/3G user/kernel or 2G/2G user/kernel.[9]

  • Linear address space
  • Single address space operating system
  1. ^ «What is an address space?». IBM. Retrieved May 5, 2024.
  2. ^ «Virtual Address Space». MSDN. Microsoft.
  3. ^ «LOADED_IMAGE structure». MSDN. Microsoft.
  4. ^ a b «4-Gigabyte Tuning: BCDEdit and Boot.ini». MSDN. Microsoft.
  5. ^ «/LARGEADDRESSAWARE (Handle Large Addresses)». MSDN. Microsoft.
  6. ^ «Virtual Address Space». MSDN. Microsoft.
  7. ^ «/LARGEADDRESSAWARE (Handle Large Addresses)». MSDN. Microsoft.
  8. ^ «/LARGEADDRESSAWARE (Handle Large Addresses)». MSDN. Microsoft.
  9. ^ «Linux kernel — x86: Memory split».
  • «Advanced Windows» by Jeffrey Richter, Microsoft Press

Оперативная память (ОЗУ) является тем компонентом персональных компьютеров, важность которого, при современной архитектуре вычислительных систем, сложно переоценить, и без которого работа их (в силу архитектурных особенностей) не представляется возможной. Было бы интересно посмотреть, как именно ОС управляет доступной ей памятью? Как она распределяет её между загруженными приложениями? Как происходит организация (создание) в памяти нового процесса, как код программы получает управление и как процессу выделяется дополнительная память по запросу, в случае, когда выделенная изначально память заканчивается? Как организовано адресное пространство процесса? Подобные вопросы возникали и продолжают возникать у многих довольно часто, но далеко не на все из них находятся вразумительные ответы.
Поскольку круг вопросов, касающихся оперативной памяти настолько велик, что не может быть освещен в одной статье, здесь мы коснемся лишь части огромного механизма управления памяти в ОС Microsoft Windows, а именно изучим адресное пространство процесса, увидим, что же размещается [системой] в пространстве памяти, выделяемой процессу.

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

Код приложений (программ), исполняемый в произвольный момент времени на процессоре, оперирует данными. Данные, которые в текущий момент необходимы коду для выполнения, должны быть размещены в физической (оперативной) памяти или регистрах общего назначения. Если данные размещены в памяти, то их положение нужно каким-либо образом определить — проще всего сделать это при помощи некоего числа (порядкового номера), называемым адресом [в массиве]. Иными словами, оперативную память легче всего себе представить в виде массива байт. Чтобы обратиться к конкретному байту данных (массива), или адресовать его, логично было бы использовать порядковый номер. Подобным образом можно поступить со всеми байтами, пронумеровав их целыми положительными числами, установив ноль за начало отсчёта. Индекс байта в этом огромном массиве и будет его адресом.

Адресация на уровне процессора

В первых микропроцессорах компании Intel (архитектуры x86) был доступен единственный режим работы процессора, впоследствии названный реальным режимом. Адресация памяти в процессорах того времени была достаточно простой и носила название сегментной. Суть её заключалась в том, что ячейка памяти адресовалась при помощи двух составляющих: сегмент : смещение (сегмент — область адресного пространства фиксированного размера, смещение — адрес ячейки памяти относительно начала сегмента). Специфика архитектуры упомянутых [первых] микропроцессоров накладывала ограничения на размер физического адресного пространства (16 килобайт, 64 килобайта, 1 мегабайт…), и память, доступная программно, была не более размера оперативной (физически установленной) памяти компьютера. Это было просто, логично и понятно. Тем не менее, описанная архитектура имела ряд недостатков, к тому же в индустрии появились тенденции дальнейшего развития:

  • Была актуальна проблема согласования выделения памяти различным приложениям. Размещение кода/данных приложений в едином для всех программ пространстве памяти требовало от операционной системы (а иногда и от самой программы) сложного механизма постоянного отслеживания занятого пространства.
  • Наметился переход к многозадачным операционным системам, в которых большое количество задач должно было выполняться [псевдо]параллельно, что затрудняло использование общего пространства памяти.
  • В условиях множества одновременно выполняющихся задач встала проблема безопасности, необходимости ограничения доступа к «чужим» процессам в памяти.

Эти, а так же некоторые другие, проблемы явились отправной точкой для работы над усовершенствованием, в следствии чего в процессоре 80286 появился защищенный режим и концепция сегментной адресации памяти была значительно расширена для обеспечения новых требований. Например в защищенном режиме сегменты могли располагаться (начинаться) в памяти в произвольном месте (база), иметь нефиксированный размер (лимит), уровни доступа, типы содержимого и прочее. И наконец, по прошествии некоторого времени была создана новая архитектура, получившая название IA-32, в которой были введены несколько новых моделей организации оперативной памяти:

  • Базовая плоская модель (basic flat model) — наиболее простая модель памяти [системы], операционная система и приложения получают в своё распоряжение непрерывное, не сегментированное адресное пространство.
  • Защищенная плоская модель (protected flat model) — более сложная модель памяти [системы], может применяться страничный механизм изоляции пользовательского/системного кода/данных, описываются четыре сегмента: кода/данных (для уровня привилегий 3, пользовательский уровень) и кода/данных (для уровня привелегий 0, ядро).
  • Мульти-сегментная модель (multi-segment Model) — самая сложная модель памяти [системы], предоставляется аппаратная защита кода, данных, программ и задач. Каждой программе (или задаче) назначаются их собственные таблицы [сегментных] дескрипторов и собственные сегменты.

И главным завоеванием защищенного режима явилось появление механизма страничной организации/адресации памяти.

Страничная организация памяти это альтернативный тип управления памятью, разработанный для обеспечения организации виртуального адресного пространства (виртуальной памяти) в многозадачных операционных системах. В отличие от сегментной адресации, которая делила адресное пространство на сегменты определенной длины, страничная адресация делит пространство на множество страниц равного размера. Изменения коснулись и принципов адресации: если в реальном режиме работы процессора пара сегментный_регистр : смещение могла адресовать ячейку памяти, то в защищенном режиме сегмент был заменен на селектор, который содержал индекс в таблице дескрипторов и биты вида таблицы дескрипторов + биты привилегий.

Механизм трансляции адресов (преобразование адреса)

Поскольку одним из нововведений защищенного режима было создание виртуальной адресации, потребовались механизм трансляции виртуальных адресов в физические, а так же механизм трансляции страниц. Именно благодаря трансляции операционная система имеет возможность создавать для каждого процесса иллюзию полноразмерного обособленного адресного пространства и обеспечивать защиту собственного ядра [от пользовательских процессов]. Адрес, по которому код программы (процесса) пытается обратиться к данным (где-то в оперативной памяти), изначально закодирован в исполняемой инструкции (пример фрагмента кода):

. . .

cmp     [edi+ecx*4+4], esi

. . .

..и проходит через некоторое количество преобразований, начиная от декодирования инструкции процессором и заканчивая выставлением адреса на шину. Давайте посмотрим, какие же этапы проходит преобразование адреса:

  1. Эффективный адрес — адрес, задаваемый в аргументах машинной инструкции при помощи регистров, смещений, коэффициентов. Фактически эффективный адрес представляет собой смещение от начала сегмента (базы). Как раз для нашего примера (выше): эффективный адрес = EDI + ECX * 4 + 4;
  2. Логический адрес – адрес, представляющий собой пару селектор : смещение. Традиционно селектор (левая часть) располагается в сегментном регистре, смещение (правая часть) в регистре общего назначения или указывается непосредственно, для нашего примера это: DS:[EDI+ECX*4+4]. Как мы видим, часто сегментный регистр (левая часть) не указывается (выбирается неявно). Фактически с логическими адресами и имеет дело программист в своих программах;
  3. Линейный адрес — это 32-/64-разрядный адрес, получаемый путем использования селектора (содержащегося в левой части виртуального адреса, в сегментном регистре, для нашего примера задан неявно, в DS) в качестве индекса в таблице дескрипторов (для вычисления базы сегмента) и добавления к ней смещения (правая часть виртуального адреса, в нашем случае значение, вычисляемое на основе выражения EDI+ECX*4+4). Линейный адрес = база сегмента + эффективный адрес.
  4. Гостевой физический адрес — при использовании аппаратной виртуализации. В случае, когда в системе работают виртуальные машины, физические адреса (получаемые в каждой из них), необходимо транслировать ещё раз.
  5. Физический адрес — это финальная часть преобразований адреса внутри процессора. Физический адрес:
    • (для сегментной адресации) полностью совпадает с линейным адресом;
    • (для сегментно-страничной адресации) получается путем преобразования трех частей значения линейного адреса на основании: каталога страниц, таблицы страниц и смещения внутри страницы;

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

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

Механизм трансляции страниц (страничное преобразование)

Алгоритмы преобразования линейного адреса в физический (этапы 3 → 5) варьируются в зависимости от множества причин (состояния определенных регистров). В некоторых режимах линейный адрес делится на несколько частей, при этом каждая часть является индексом в специализированной системной таблице (все они расположены в памяти), а число и размер описанных таблиц различаются в зависимости от режима работы процессора. Запись в таблице первого уровня представляет собой адрес начала таблицы следующего уровня, а для последнего уровня — информация о физическом адресе страницы в памяти и её свойствах. Иначе говоря:

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

Соответственно, на данном этапе, мы уже имеем дело уже с разбиением адресного пространства на страницы (определенного размера) или со страничной организацией памяти. Иными словами, мы имеем дело с виртуальной памятью. Процессор как бы делит линейное адресное пространство на блоки (страницы) фиксированного размера (в зависимости от установок — 4Кб, 2Мб, 4Мб), которые уже могут отображаются в физической памяти или на жестком диске. И вот тут стоит обратить внимание на один крайне важный аппаратный механизм:

В произвольный момент времени та или иная страница может «находиться» (быть сопоставлена) в физической памяти, а может и не находиться в ней.

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

  • страницы в данный момент нет в физической памяти;
  • таблицы не содержат необходимых данных;
  • недостаточно прав доступа;

Во всех этих случаях возникает аппаратное событие — так называемое исключение Page Fault (#PF), которое предписывает обработчику исключения произвести дополнительные действия по устранению возникшей проблемы: подгрузить (отсутствующую) страницу с диска (либо скинуть (ненужную) страницу на диск). Как только страница была подгружена, то выполнение прерванного кода продолжится с инструкции, которая вызвала #PF. Именно механизм страничной адресации (преобразования) и позволяет операционной системе организовать виртуальное адресное пространство, о котором речь пойдет далее. К сожалению, подробное описание механизмов преобразования адресов и типов адресации выходит за рамки данной статьи, далее мы переходим к «программному» уровню, то есть непосредственно к механизмам операционной системы.

Адресация на уровне ОС

Сами понимаете, что было бы не совсем корректно называть излагаемое в данной главе некоей «программной» частью адресации в операционной системе, поскольку:

Механизмы, используемые операционными системами, имеют аппаратную поддержку на уровне процессора.

..поэтому операционная система Windows эксплуатирует особенности той архитектуры, на которой она в данный момент функционирует (выполняется) и всего-лишь использует аппаратные механизмы [процессора]. Становится очевидным, что если Windows исполняется на станциях, построенных на базе процессоров архитектуры IA-32, то используется защищенный режим работы процессора. Версии операционной системы Windows для архитектуры IA-32, пользуются механизмом сегментации защищенного режима лишь в минимальном объёме:

  • используются всего два уровня привилегий: 0 и 3;
  • и из всех доступных способов организации памяти используется защищенная плоская модель со страничной адресацией (protected flat model);

Защищенная плоская модель в Windows имеет свои особенности: память представляется программе [задаче] в виде единого непрерывного адресного пространства (линейное адресное пространство). Код, данные и стек — всё содержатся в этом адресном пространстве, то есть объединены в один физический сегмент.

Есть соглашение, что все селекторы для процесса идентичны, это значит что адресация внутри процесса фактически базируется на смещении (а селекторы сегментов остаются неизменными). Исходя из этого, если каждый процесс [в системе] использует собственное адресное пространство линейным способом, то есть адресация базируется (фактически) на смещении, селекторы сегментов остаются неизменными, то сегментные регистры не нужны и их можно «опустить», «упразднить». На основе плоской модели памяти базируется часто упоминаемый в литературе механизм виртуальной памяти.

Виртуальная память – стратегия организации памяти [операционной системой], основанная на идее создания единого виртуального адресного [псевдо]пространства, состоящего из физической памяти (ОЗУ) и дисковой памяти (жесткий/твердотельный диск).

Возникает вопрос: почему это пространство называется виртуальным? А потому что виртуальный адрес может и не присутствовать в физической памяти, все механизмы [защищенного режима] созданы лишь для имитации (создания иллюзии для программы) его существования, ведь используются селекторы:смещения (которые могут ссылаться на любой адрес) совместно со страничным преобразованием (страницы могут быть сопоставлены с физической памятью, а могут и не быть), то есть все сущности по сути эфемерны, пользователь не знает как и где они размещены!!

Размерность адресных пространств

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

Физическое адресное пространство

Итак, размерность физического (процессорного) адресного пространства зависит от особенностей аппаратной архитектуры:

  • 32-бита: используются 32-битные указатели (размерность 4 байта), и размер адресного пространства равен 232 = 4294967296 байт (4 гигабайта, Гб). Шестнадцатеричное представление диапазона: 00000000FFFFFFFF.
  • 64-бита: используются 64-битные указатели (размерность 8 байт) и размер адресного пространства процесса равен 264 = 18446744073709551616 байт (16 экзабайт, Эб. ~17 миллиардов гигабайт). Шестнадцатеричное представление диапазона: 0000000000000000FFFFFFFFFFFFFFFF.
Разрядность (битность) приложения Разрядность указателя Размер адресного пространства [процесса] Адреса диапазона (шестнадцатеричные)
32 бита 32 бита (4 байта) 232 (4294967296 байт = 4 гигабайта) 00000000FFFFFFFF
64 бита 64 бита (8 байт) 264 (18446744073709551616 байт = ~17 миллиардов гигабайт = 16 экзабайт) 0000000000000000FFFFFFFFFFFFFFFF

Но это, опять же, теоретическая адресация на основе разрядности.

Линейное адресное пространство

Линейное адресное пространство процесса теоретически могло бы быть идентично физическому адресному пространству, но на практике вступают в действие ограничения операционной системы, которые зависят от: версии операционной системы, [определенных] настроек (флагов) операционной системы и приложений, типа запуска: 32-битное приложение на 32-битной ОС, 32-битное приложение на 64-битной ОС, 64-битное на 64-битной ОС.

  • 32-бита: размер линейного адресного пространства процесса равен 4Гб, верхние 2Гб (или 1Гб, в зависимости от флагов) из которых защищены на уровне страниц. Поэтому для пользовательского приложения в 32-битной ОС определен лимит в 2Гб (или 3, в зависимости от флагов), за пределы которого процесс выбраться не может (без использования специализированных технологий вида AWE).
  • 64-бита: размер линейного адресного пространства процесса равен 16Тб или 256Тб, из которых (верхняя) часть защищена на уровне страниц. Поэтому 32/64-битному пользовательским приложениям может быть определен лимит в 2Гб, 4Гб, 8Тб и 128Тб (в зависимости от разрядности/версии/флагов).

Виртуальные адреса используются приложениями, однако сама операционная система (равно как и процессор) не способна по этим виртуальным адресам непосредственно обращаться к данным, потому как виртуальные адреса не являются адресами физического устройства хранения информации (ОЗУ/ДИСК), другими словами физически по этим адресам информация не хранится.

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

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

Еще раз: код и данные, которые в данный момент обрабатываются/исполняются, физически располагаются в ОЗУ.

Использование страничной организации операционной системой

Получается интересная ситуация: с одной стороны, для каждого процесса в операционной системе Windows выделяется адресное пространство, которое фактически эквивалентно размерности теоретического адресного пространства; с другой стороны размер физически установленной оперативной памяти (ОЗУ) компьютера может быть в разы меньше суммы всех адресных пространств процессов, исполняемых в данный момент в системе. Как нам в подобной ситуации обеспечить нормальное функционирование операционной системы? А очень просто, поскольку:

  1. виртуальные адреса суть иллюзия, они могут не ссылаться на физическую память;
  2. [в подавляющем большинстве случаев] процесс не использует всё виртуальное адресное пространство, отведенное для него; то есть адресное пространство процесса не обязательно заполнено [под завязку] данными;
  3. общие для всех процессов данные могут разделяться множеством процессов (экономия оперативной памяти);
  4. не обязательно код и данные всех процессов [постоянно] держать в ОЗУ (экономия оперативной памяти);

И в обеспечении всех этих механизмов нам на помощь приходит страничная организация (о которой говорилось выше): она позволяет операционной системе прозрачно (незаметно) для пользователя/приложения выполнять ряд очень нужных системе манипуляций:

  • подгружать/выгружать неиспользуемые страницы с/на носитель информации (жесткий диск: HDD, SSD);
  • проецировать общие страницы [общих ключевых библиотек] в несколько адресных пространств одновременно;

Страницы, которые в определенный промежуток времени не используются, из ОЗУ переносятся (перепроецируются) на любой физический носитель, установленный в системе — в файл (файл подкачки, страничный файл, page file, swap-файл, «своп») либо [в некоторых ОС] в область подкачки (специализированный раздел).
Сопоставлением (отображением) виртуальных адресов на физические адреса ОЗУ или файла подкачки занимается так называемый диспетчер виртуальной памяти (VMM, Virtual Memory Manager).

Диспетчер виртуальной памяти (Virtual memory manager, Kernel-mode memory manager) — модуль ядра ОС Windows, предназначающийся для организации подсистемы виртуальной памяти: создания таблицы адресов для процессов, организации общего доступа к памяти, осуществления защиты на уровне страниц, поддержки возможность отображения файлов на память, распределения физической памяти между процессами, организации выгрузки/загрузки страниц между физической памятью и файлом подкачки, обеспечения всех процессов достаточным для функционирования объемом физической памяти.

Упрощенная схема процесса «отображения» выглядит следующим образом:

сопоставление виртуальных страниц процесса

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

Виртуальное адресное пространство создается для каждого процесса, работающего в операционной в системе и напрямую не связано с адресацией физической памяти (ОЗУ).

и теперь вы, надеюсь, понимаете, что:

Виртуальный адрес может быть просто не сопоставлен с физической памятью!!

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

  • Виртуальная память, доступная программе, напрямую не связана с физической памятью.
  • Каждая программа работает в своей виртуальном адресном пространстве. Размер этого пространства может быть больше размера фактически установленной в машине оперативной памяти.
  • Адресное пространство [каждого] процесса (программы), исполняющегося в ОС, изолировано [от подобных адресных пространств других процессов].

Когда какая-либо программа обращается к своим данным, которые [в этот момент] отсутствуют в ОЗУ, то функция обработчика страничного нарушения диспетчера виртуальной памяти производит следующие манипуляции:

  1. сохраняет в стек адрес инструкции, следующей за инструкцией, вызвавшей #PF;
  2. производит поиск свободной (незанятой) физической страницы;
  3. создает новый элемент в таблице страниц;
  4. подгружает недостающие данные из файла подкачки в ОЗУ;
  5. производит проецирование виртуальной страницы на физическую;
  6. производит восстановление адреса из стека и выполняет «перезапуск» инструкции (следующей за той, на которой было прервано выполнение);

Все эти процессы происходят на уровне ядра операционной системы, поэтому они «прозрачны» или «неразличимы» для пользовательского приложения (а программисту, в реалиях высокоуровневого программирования, зачастую и вовсе не интересны).
Теперь несколько слов об изоляции или закрытости [адресного пространства] процесса. Виртуальное пространство [каждого] процесса изолировано, или, можно сказать по-другому — процессы отделены друг от друга в своем собственном виртуальном адресном пространстве. Поэтому любой поток в рамках некоего процесса получает доступ только лишь к той памяти, которая принадлежит родительскому процессу. Наглядно, изолированность выражается в том, что некая программа A в своем адресном пространстве может хранить запись данных по условному адресу 12345678h, и в то же время у программы B по абсолютно тому же адресу 12345678h (но уже в его собственном адресном пространстве) могут находиться совершенно другие данные. Изолированность, к тому же подразумевает, что код одной программы (если быть точным, то потока в рамках процесса) не может получить доступ к памяти другой программы (без дополнительных манипуляций). Достоинства виртуальной памяти:

  • Упрощается программирование. Программисту больше не нужно учитывать ограниченность памяти, или согласовывать использование памяти с другими приложениями.
  • Повышается безопасность. Адресное пространство процесса изолировано.
  • Однородность массива. Адресное пространство линейно.

[пример] Виртуальное адресное пространство процесса

Я думаю, после некоторого количества теоретических выкладок, самое время перейти ближе к рассмотрению основной темы статьи. Напомню, что мы будем исследовать структуру памяти 32-битного процесса Windows. Для исследования памяти процесса нам потребуется специализированное программное средство, которое поможет нам увидеть адресное пространство процесса в деталях. Использовать мы будем утилиту VMMap от Марка Руссиновича, отличное приложение, которое выводит подробную информацию по использованию памяти в рамках того или иного процесса. Однако, не обошлось, что называется, и без ложки дегтя. Бытует мнение, что данное ПО отражает карту процесса не достаточно подробно, игнорируя кое-какие структуры памяти, однако, как отправная точка для понимания принципов размещения объектов в памяти вполне нас устроит.
Для практического эксперимента я буду использовать самописный модуль test2.exe, написанный на ассемблере, код которого предельно прост, отображает всего-лишь некоторые оконные элементы и выводит информационное окно перед выходом. В модуле используются (импортируются) функции SetFocus, SendMessageA, MessageBoxA, CreateWindowExA, DefWindowProcA, DispatchMessageA, ExitProcess, GetMessageA, GetModuleHandleA, LoadCursorA, LoadIconA, PostQuitMessage, RegisterClassA, ShowWindow, TranslateMessage, UpdateWindow из библиотек user32.dll и kernel32.dll. Итак, запускаем на исполнение тестовый файл test2.exe а затем, пока приложение исполняется, загружаем программу VMMap, указывая ей открыть наш целевой процесс. Вот что мы наблюдаем:

Карта памяти процесса Windows

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

Наименование столбца Описание
Address Стартовый адрес региона в виртуальном пространстве процесса. Шестнадцатеричное представление.
Type Тип региона (см. таблицу далее).
Size Полный размер выделенной области. Отражает максимальный размер физической памяти, которая необходима для хранения региона. Так же включает зарезервированные области.
Committed Количество памяти региона, которое «отдано», «передано» или «зафиксировано» — то есть эта память уже связана с ОЗУ, страничным файлом [подкачки], или с отображенным файлом (mapped file) [на диске].
Private Часть всей памяти, выделенной для региона, которая приватна, то есть принадлежит исключительно процессу-владельцу и не может быть разделена с другими процессами.
Total WS Общее количество физической памяти, выделенной для региона (ОЗУ + файл подкачки).
Private WS Приватная часть физической памяти [региона/файла]. Принадлежит исключительно владельцу и не может быть разделена (использована совместно) с другими процессами.
Shareable WS Общедоступная часть физической памяти [региона/файла]. Может быть использована совместно другими процессами, которым так же необходим данный регион (файл).
Shared WS Общедоступная часть физической памяти [региона/файла]. Уже используется совместно с другими процессами.
Locked WS Часть физической памяти [региона/файла], которая гарантированно находится в ОЗУ и не вызывает ошибок страниц (необходимость подгрузки из файла подкачки), когда к ней пытаются получить доступ.
Blocks Количество выделенных в регионе блоков памяти. Блок — неразрывная группа страниц с идентичными атрибутами защиты, сопоставленная с одним регионом физической памяти. Если Вы посмотрите внимательно то заметите, что значения параметра «Blocks», отличные от нуля, встречаются в регионах, которые разбиты на несколько частей (подрегионы, блоки). Обычно имеется несколько подрегионов: основной регион + резервные.
Protection Типы операций, которые могут быть применены к региону. Для регионов, которые подразделяются на подблоки (+), колонка указывает общую (сводную) информацию по типам защиты в подблоках. В случае применения к региону неразрешенного типа операций, возникает «Ошибка доступа». Ошибка доступа происходит в случаях: когда происходит попытка запустить код из региона, который не помечен как исполняемый (если DEP включена), или при попытке записи в регион, который не помечен как предназначенный для записи или для «копирования-при-записи» (copy-on-write), или в случае попытки доступа к региону, который маркирован как «нет доступа» или просто зарезервирован, но не подтвержден. Атрибуты защиты присваиваются регионам виртуальной памяти на основе атрибутов сопоставленных регионов физической памяти.
Details Дополнительная информация по региону. Тут могут отображаться: путь файла бэкапа, идентификатор кучи (для региона heap), идентификатор потока (для стека), указатель на .NET-домен и прочее.

WS (Working Set) — так называемый рабочий набор, то есть множество (массив) страниц физической памяти (ОЗУ), уже выделенных для процесса и использующихся для фактического хранения кода/данных. Когда требуется доступ к каким-либо адресам виртуальной памяти, фактически с этими адресами должна быть связана физическая память (потому что операции чтения/записи/выполнения могут производиться только с физической памятью). Поэтому, когда с данными адресами будет сопоставлена физическая память, она добавляется как раз к рабочему набору процесса (working set).

Ну и необходимо описать все виды типов (type) регионов. Типы регионов у можно наблюдать на карте процесса в столбце Type:

Тип региона Описание
Free Диапазон виртуальных адресов не сопоставленных с физической памятью. Это память, которая еще не занята. Регион или часть региона доступны для резервирования (выделения).
Shareable Регион, который может быть разделен с другими процессами и забекаплен в физической памяти либо файле подкачки. Подобные регионы обычно содержат данные, которые разделены между процессами, то есть используются несколькими программами, через общие, специально оформленные, секции DLL или другие объекты.
Private Data Частные данные. Это регион, выделенный через функцию VirtualAlloc. Эта часть памяти не управляется менеджером кучи (Heap Manager), функциями .NET и не выделяется стеку. Обычно содержит данные приложения, которые используются только нашей программой и не доступны другим процессам. Так же содержит локальные структуры процесса/потока, такие как PEB или TEB. Типичная «память программы». Регион сопоставлен со страничным файлом.
Unusable Виртуальная память, которая не может быть использована из-за фрагментации. Это осколки, которые уже закреплены за регионом. Гранулярность выделения памяти в Windows — регионы по 64Кб. Когда Вы пытаетесь выделить память с помощью функции VirtualAlloc и запрашиваете, к примеру 8 килобайт, VirtualAlloc возвращает адрес региона в 64 килобайта. Оставшиеся 56Кб помечаются как неиспользуемые (unusable). Обратите внимание на то, что области Unusable «следуют» в карте за не кратными 64Кб регионами, на самом же деле, это всего-лишь память, которая входит в регион (принадлежит региону-владельцу), но на данный момент не используется.
Image Регион сопоставлен с образом исполняемого EXE- или DLL-файла, проецируемого в память. Это именно тот регион, куда загружается образ пользовательского приложения со всеми его секциями (в нашем случае test2.exe).
Image (ASLR) Образы системных библиотек, загружаемые с использованием механизма безопасности ОС под названием ASLR (Address Space Layout Randomization). ASLR — рандомизация расположения в адресном пространстве процесса таких структур как: образ исполняемого файла, подгружаемая библиотека, куча и стек. Вкратце, ОС игнорирует предпочитаемый базовый адрес загрузки, который задан в заголовке PE и загружает библиотеку в адрес по выбору «менеджера загрузки». Для поддержки ASLR, библиотека должна быть скомпилирована со специализированной опцией, либо без неё, когда используется принудительная рандомизация (ForceASLR). Таким образом, усиливается безопасность процесса и исключаются конфликты базовых адресов образов [подгружаемых модулей]. Применяется начиная с Windows Vista. Технология так же известна под псевдонимом Rebasing.
Thread Stack Стек. Регион сопоставлен со стеком потока. Каждый поток имеет свой собственный стек, соответственно под каждый поток выделяется регион для хранения его собственного стека. Когда в процессе создается новый поток, система резервирует регион адресного пространства для стека потока. Для чего обычно используется стек? Ну как и все стеки, стек потока предназначается для хранения локальных переменных, содержимого регистров и адресов возврата из функций.
Mapped File Проецируемые файлы. Это немного не то же, что «проецирование» образа самой программы и необходимых библиотек. Все отображаемые в адресное пространство процесса файлы могут быть трех видов: самой программой, библиотеками, и рабочими объектами. Проецируемые (mapped) файлы это и есть вот эти самые рабочие объекты, которые может создавать и использовать код программы. Обычно это файлы, которые содержат какие-либо требующиеся приложению данные и с которыми приложение работает напрямую. Проецирование файлов — наиболее удобный способ обработки внешних данных, поскольку данные из файла становятся доступны непосредственно в адресном пространстве процесса (регион памяти сопоставлен с файлом или частью файла), а на самом деле они размещаются на диске. Таким образом программе файл доступен в виде большого массива, нет необходимости писать собственный код загрузки файла в память, на лицо экономия на операциях ввода-вывода и операциях с блоками памяти. ОС делает всё это прозрачно для разработчика, собственными механизмами, получается для кода область проецируемых файлов — это обычная память. Проецируемые файлы предназначены для операций с файлами из кода основной программы, ведь рано или поздно подобные операции с файлами приходится использовать практически во всех проектах, и зачастую это влечет за собой большое количество дополнительной работы, поскольку пользовательское приложение должно уметь работать с файлами: открывать, считывать и закрывать файлы, переписывать фрагменты файла в буфер и оттуда в другую область файла. В Windows все подобные проблемы решаются как раз при помощи проецируемых в память файлов (memory-mapped files). Проецируемый в память файл может иметь имя и быть разделяемым, то есть совместно использоваться несколькими приложениями. Работа с проецируемыми файлами в пользовательском режиме обеспечивается функциями CreateFileMapping и MapViewOfFile.
Heap (Private Data) Куча. Это регион зарезервированного адресного пространства процесса, предназначенный для динамического распределения небольших областей памяти. Представляет из себя закрытую область памяти, которая управляется так называемым «Менеджером кучи» (Heap Manager). Данные в этой области хранятся «в куче» (или «свалены в кучу»), то есть друг за другом, разнородные, без какой-либо систематизации. Смысл кучи сводится к обработке множества запросов на создание/уничтожение множества мелких объектов (блоков памяти). Куча используется различными функциями WinAPI, вызываемыми кодом Вашего приложения, либо функциями самого приложения, для выделения различных временных буферов хранения строк, переменных, структур, объектов. Память в куче выделяется участками (индексами), которые имеют фиксированный размер (8 байт).

Как Вы видите из карты процесса, всё адресное пространство процесса разбито на множество неких зон различного назначения, называемых регионами. Регионов в адресном пространстве достаточно много. Однако, для начала, давайте посмотрим на «общее» разбиение адресного пространства процесса, дабы возникло понимание, как что и где может размещаться. Разбиение адресного пространства в определенной степени зависит от версии ядра Windows.
Общая концепция разбиения виртуального адресного пространства 32-битных программ:

Начало Конец Размер Описание
00000000 0000FFFF 64Кб Область нулевых указателей. Зарезервировано. Данная область всегда маркируется как свободная (Free). Попытка доступа к памяти по этим адресам вызывает генерацию исключения нарушения доступа STATUS_ACCESS_VIOLATION. Область применяется для выявления программистами некорректных, нулевых указателей, тем самым позволяя выявлять некорректно работающий код. Если по каким-то причинам (напр.: возврат значения функцией) переменная или регистр вдруг принимает нулевое (неинициализированное) значение, то дальнейшая попытка обращения к памяти (запись/чтение) с использованием данной переменной/регистра приведет к генерации исключения (напр.: mov eax, dword ptr [esi], где ESI=0).
00010000 7FFEFFFF 2Гб (3Гб) Пользовательский режим (User mode). Пользовательская часть кода и данных. В это пространство загружается пользовательское приложение, с разбивкой по секциям. Отображаются все проецируемые в память файлы, доступные данному процессу. В этом пространстве создаются пользовательская часть стеков потоков приложения. Тут присутствуют основные системные библиотеки ntdll.dll, kernel32.dll, user32.dll, gdi32.dll.
7FFF0000 7FFFFFFF 64Кб Область некорректных указателей. Зарезервировано. Данная область всегда маркируется как свободная (Free). Попытка доступа к памяти по этим адресам вызывает генерацию исключения нарушения доступа STATUS_ACCESS_VIOLATION. Хотя эта область формально и относится к области памяти пользовательского режима, она является «пограничной», то есть имеется риск при операциях с большими блоками памяти выйти за границы пользовательского режима и перезаписать данные режима ядра, поэтому Microsoft предпочла заблокировать доступ к данной области. Область применяется для выявления некорректных (вышедших за пределы пользовательской памяти) указателей (переменные/регистры) в коде (например: mov eax, dword ptr [esi], где ESI=значение, входящее в диапазон 7FFF0000-7FFFFFFF).
80000000 FFFFFFFF 2Гб (1Гб) Режим ядра (Kernel mode). Код и данные модулей ядра, код драйверов устройств, код низкоуровневого управления потоками, памятью, файловой системой, сетевой подсистемой. Размещается кеш буферов ввода/вывода, области памяти, не сбрасываемые в файл подкачки. Таблицы, используемые для контроля страниц памяти процесса (PTE?). В этом пространстве создаются ядерная часть стеков для каждого потока в каждом процессе. Пространство недоступно из пользовательского режима, и попытка обращения из кода режима пользователя приведет к исключению нарушения доступа. Пространство «общее», то есть идентично (одинаково) для всех процессов системы.

Ну, регионы мы бегло рассмотрели, давайте разберемся, как же происходит построение адресного пространства для конкретного процесса? Ведь должен существовать в системе механизм выделения и заполнения регионов. Все начинается с того, что пользователь либо некий код инициируют выполнение исполняемого модуля (программы). Одни из примеров подобного действия может быть двойной щелчок в проводнике по имени исполняемого файла. В этом случае код инициирующего выполнение потока вызывает функцию CreateProcess либо родственную из набора функций, предназначающихся для создания нового процесса. Какие же действия выполняет ядро после вызова данной функции:

  • Находит исполняемый файл (.exe), указанный в параметре функции CreateProcess. В случае каких проблем просто возвращает управление со статусом false.
  • Создает новый объект ядра «процесс».
  • Создает адресное пространство процесса.
  • Во вновь созданном адресном пространстве резервирует регион (набор страниц). Размер региона выбирается с расчетом, чтобы в него мог уместиться исполняемый .exe-файл. Загрузчик образа смотрит на параметр заголовка .exe-файла, который указывает желательное расположение (адрес) этого региона. По-умолчанию = 00400000, однако может быть изменен при компиляции.
  • Отмечает, что физическая память, связанная с зарезервированным регионом это сам .exe-файл на диске.
  • После окончания процесс проекции .exe-файла на адресное пространство процесса, система анализирует секцию import directory table, в которой представлен список DLL-библиотек (которые содержат функции необходимые коду исполняемого файла) и список самих функций.
  • Для каждой найденной DLL-библиотеки производится «отображение», то есть вызывается функция LoadLibrary, которая выполняет следующие действия:
    • Резервирует регион в адресном пространстве процесса. Размер выбирается таковым, чтобы в регион мог поместиться загружаемый DLL-файл. Желаемый адрес загрузки DLL указывается в заголовке. Если размер региона по желаемому адрес меньше размера загружаемого DLL, либо регион занят, ядро пытается найти другой регион.
    • Отмечает, что физическая память, связанная с зарезервированным регионом это DLL-файл на диске.
    • Производится настройка образа библиотеки, сопоставление функций. Результатом этого является заполненная таблица (массив) адресов импортируемых функций, чтобы в процессе работы код обращается к своему массиву для определения точки входа в необходимую функцию.

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

Резервирование (reserving) — операция выделения региона (выделения блока памяти по те или иные нужды).

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

Страница (page) — минимальная единица [объема памяти], используемая системой при управлении памятью (как мы и писали выше).

Размещение в адресном пространстве структур и библиотек

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

Адрес Модуль Описание
00040000 apisetschema.dll Предназначена для организации и разделения на уровни выполнения огромного количества функций базовых DLL системы. Подобная технология была названа «Наборами API» (API Sets), появилась в Windows 7 и предназначалась для группировки всех [многочисленных] функций в 34 различных типа и уровня выполнения, с целью предотвратить циклические зависимости между модулями и минимизировать проблемы с производительностью, которые обусловлены обеспечением зависимости новых DLL от набора Win32 API в адресном пространстве процесса. Перенаправляет вызовы, адресованные базовым DLL к их новым копиям, разделенным на уровни.
00050000 Стек потока [64-битный] стек потока.
00090000 Стек потока. Одномерный массив элементов с упорядоченными адресами (организованный по принципу «последний пришел — первым ушел» (LIFO)), предназначенный для хранения небольших объемов данных фиксированного размера (слово/двойное слово/четверное слово): стековых фреймов, передаваемых в функцию аргументов, локальных переменных функций, временно сохраняемых значений регистров. Для каждого потока выделяется собственный (отдельный) стек. Каждый раз при создании нового потока в контексте процесса, система резервирует регион адресного пространства для стека потока и передает данному региону определенный объем [физической] памяти. Для стека система резервирует 1024Кб (1Мб) адресного пространства и передает ему всего две страницы (2х8Кб?) памяти. Но перед фактическим выполнением потока система устанавливает указатель стека на конец верхней страницы региона стека, это именно та страница, с которой поток начнет использовать свой стек. Вторая страница сверху называется сторожевой (guard page). Как только активная страница «переполняется», поток вынужден обратиться к следующей (сторожевой) странице. В этом случае система уведомляется о данном факте и передает память еще одной странице, расположенной непосредственно за сторожевой. После чего флаг PAGE_GUARD переходит к странице, которой только что была передана память. Благодаря описанному механизму объем памяти стека увеличивается исключительно по мере необходимости.
00280000 msctf.dll.mui Файл локализации библиотеки msctf.dll, описанной ниже. В общем смысле представляет собой переведенные на русский язык текстовые строки/константы, используемые библиотекой.
00400000 test2.exe Собственно образ нашей программы. Отображается в виртуальном адресном пространстве благодаря системному механизму проецирования файлов. Исполняемый .exe-файл проецируется на адресное пространство программы по определенным адресам и становится его частью. Проецирование состоит в том, что данные [из файла] не копируются в память, а как бы связываются с данными на физическом носителе, то есть любое обращение к памяти по этим адресам инициирует чтение данных с диска, память как бы «читается» из файла на диске. Виртуальный адрес 00400000 является «предпочитаемой базой образа» (Image base), константой, которую можно изменять при компиляции. По традиции, никто этим не заморачивается, и, в большинстве случаев, данный адрес актуален для подавляющего большинства программ (но встречаются и исключения).

Не путайте «базу образа» (image base) с «точкой входа» (entry point). Вторая представляет из себя адрес, с которого начинается исполнение кода программы. Обычно лежит по некоторому смещению относительно «базы образа».

Образ исполняемого файла (test2.exe) содержит в себе секции. Данный факт можно подтвердить, раскрыв (+) содержимое образа. Объясняется это тем, что exe-файл состоит из множества частей: непосредственно секция кода, секция данных, секция ресурсов, констант. Все эти секции загрузчик размещает по собственным областям памяти и назначает различные атрибуты доступа.

00410000 locale.nls NLS предоставляет поддержку местной раскладки клавиатуры и шрифтов. NLS позволяет приложениям устанавливать локаль для пользователя и получать (отображать) местные значения времени, даты, и других величин, отображаемых в формате региональных настроек.
01F80000 SoftDefault.nls
02250000 StaticCache.dat
735A0000 uxtheme.dll Тема оформления. Функционал библиотеки позволяет менять визуальное представление интерфейса (вид многочисленных элементов управления) программ без необходимости менять базовый (в ядре) функционал операционной системы.
73A30000 comctl.dll Библиотека реализует готовые элементы управления (контролы), которые используются в графическом интерфейсе.
74B20000 dwmapi.dll Интерфейс диспетчера окон рабочего стола (DWM, Desktop Windows Manager). DWM — графический интерфейс рабочего стола, использующийся в Windows Aero. Управляет объединением различных выполняющихся и визулизируемых окон с рабочим столом. В своей программе я никаких специфических функций Windows Aero не использую, но, могу предположить, что образ библиотеки dwmapi.dll отображается на адресное пространство процесса по причине включенного на уровне системы интерфейса Aero.
751F0000 75250000 752D0000 wow64win.dll wow64.dll wow64cpu.dll В адресном пространстве процесса по данным адресам находятся библиотеки DLL пользовательского режима, отвечающие за работу подсистемы Wow64. Они появились в нашем адресном пространстве не случайно, поскольку, напомню, что наш 32-разрядный процесс test2.exe запущен в 64-разрядной ОС Windows 7 Professional.

Wow64 (Windows 32-bit on Windows 64-bit) — эмуляция Win32 приложений на 64-разрядной ОС Windows.

Представляет из себя программную среду, позволяющую исполнять 32-разрядные приложения на 64-разрядной версии Windows. Механизм используется в 64-разрядных версиях Windows в виде набора библиотек DLL пользовательского режима. Помимо данных библиотек в 64-разрядной версии ОС присутствует поддержка со стороны ядра (изменение контекста). Перехватывает системные вызовы 32-битных версий ntdll.dll и user32.dll, поступающих от 32-битных приложений и транслирует их в 64-битные вызовы ядра. С помощью Wow64 создаются 32-разрядные версии структур данных для процесса, например PEB, TEB и другие, на основе их 64-разрядных прототипов.

wow64win.dll Библиотека, предназначенная для эмуляции системных вызовов графической оболочки пользователя (GUI), экспортируемых Win32k.sys.
wow64.dll Обеспечивает инфраструктуру эмуляции (преобразования) системных вызовов, экспортируемых ядром ntoskrnl.exe. По сути, организует перенаправление всего основного функционала, включающего операции с файловой системой и реестром. Управляет созданием процесса и потоков в нём.
wow64cpu.dll Библиотека, отвечающая за эмуляцию x86-инструкций на процессорах Itanium. Управляет 32-разрядным контекстом процессора для каждого потока, запущенного в рамках Wow64. Поддерживает переключение режима работы с 32-разрядного в 64-разрядный и наоборот, обеспечивая поддержку всей логики. Не обязательна для x64-процессоров, поскольку они выполняют x86-32-инструкции напрямую.
75500000 msctf.dll Библиотека расширяет функционал, предоставляемый службами Microsoft для работы с текстом (Microsoft Text Services). Среди функций библиотеки имеются функции усовершенствованной текстовой обработки и ввода текста. Функционал библиотеки msctf.dll предоставляет двунаправленный обмен между приложением и службами работы с текстом. Предоставляет поддержку различных языков.
75810000 msvcrt.dll Microsoft Visual C++ Runtime. Библиотека времени выполнения языка C, обеспечивающая вспомогательные функций для работы с памятью, устройствами ввода/вывода, математическими функциями. Довольно много прототипов функций, используемых в языках C/C++ содержится в данной библиотеке.
758C0000 gdi32.dll Одна из четырех основных библиотек поддержки Win32 API. Часть интерфейса графического устройства (GDI, Graphics Device Interface) или интерфейса между приложениями и графическими драйверами видеокарты, работающая в режиме пользователя. Содержит функции и методы для представления графических объектов и вывода их на устройство отображения, отвечает за отрисовку линий, кривых, обработку палитры и управление шрифтами, можно сказать полностью отвечает на графику. Приложения посылают запросы коду GDI, работающему в режиме пользователя, который пересылает их GDI режима ядра, а тот уже перенаправляет данные запросы драйверам графического адаптера. Моя программа [напрямую] не импортирует функции из gdi32.dll напрямую, однако библиотека проецируется в адресное пространство любого процесса, использующего оконный интерфейс.
75A80000 advapi32.dll Одна из четырех основных библиотек поддержки Win32 API. Содержит большое количество часто востребованных функций: работа с реестром, сервисами, выключение (перезагрузка) ПК, и прч. В системе присутствует огромное количество библиотек, которые статически слинкованы с библиотекой advapi32.dll. Поэтому, без проецирования её в адресное пространство процесса никак не обойтись.
76С00000 kernel32.dll Одна из четырех основных библиотек поддержки Win32 API. В библиотеке содержатся основные подпрограммы для поддержки работы подсистемы Win32. Много ключевых процедур и функций, которые используются в пользовательских программах, содержатся в библиотеке kernel32.dll. Это работа с процессами (GetModuleHandle, GetProcAddress, ExitProcess), вводом-выводом, памятью, синхронизацией. Ранее kernel32.dll загружался во всех контекстах процесса по одному и тому же адресу. Теперь, думаю именно из-за ASLR, в адресному пространстве каждого процесса он загружается по разным адресам? Большинство экспортируемых библиотекой kernel32.dll функций используют «родной» API ядра напрямую.
77070000 user32.dll Одна из четырех основных библиотек поддержки Win32 API. Эта библиотека проецируется практически в каждый процесс Win32. Библиотека содержит часто используемые функции для обработки сообщений, меню, взаимодействия. Напомню, что в моей программе используются такие функции как: SetFocus, SendMessage, MessageBox, CreateWindowEx, DefWindowProcA, DispatchMessageA, GetMessageA, LoadCursorA, LoadIconA, PostQuitMessage, RegisterClassA, ShowWindow, TranslateMessage, UpdateWindow. Все эти функции предоставляются системной библиотекой user32.dll, поэтому без проецирования её в адресное пространство моего процесса моя программа (test2.exe) работать не будет.
771F0000 kernelbase.dll Результат технологии разделения на уровни выполнения базовых функций DLL. Содержит так называемые низкоуровневые функции, которые ранее помещались в библиотеках kernel32.dll и advapi32.dll. Теперь код направляет запросы к этой библиотеке низкоуровневых функций, вместо того, чтобы, как раньше, выполнять их напрямую.
77C00000 ntdll.dll Библиотека, обеспечивающая «родной» интерфейс (Native API) функций ядра как для приложений раннего этапа загрузки ОС, так и для функций интерфейса WinAPI. Все функции подсистемы Win32 можно разделить на две части: функции, требующие перехода в режим ядра и функции не требующие перехода в режим ядра. Для обработки API-функций пользовательского режима, которые требуют перехода в режим ядра и существует библиотека ntdll.dll. По своей структуре ntdll.dll представляет собой обычную библиотеку пользовательского режима, представляющую собой своеобразный «мост» (переходник) между функциями библиотек пользовательского режима и кодом, который реализует соответствующий функционал в ядре. Пользовательский режим (user mode) и режим ядра (kernel mode) существенно отличаются в реализации, однако пользовательский режим должен максимально сохранять совместимость с привычными (старыми) форматами входных/выходных данных функций, в то время как режим ядра может потребовать существенного видоизменения кода от версии Windows к версии. С этой точки зрения, ntdll.dll играет роль интерфейса совместимости, именно благодаря ему разработчики Microsoft могут свободно менять [при выпуске новых версий/пакетов обновлений Windows] внутреннюю реализацию функций в ядре, сохраняя, при этом, формат параметров функций пользовательского режима. Можно сказать, что Native API создан с единственной целью — вызывать функции ядра, код которого располагается в нулевом кольце защиты. Большинство точек входа в Native API являются «заглушками», которые передают параметры и управление коду режима ядра.
77DE0000 ntdll.dll То же самое, что и описанный ntdll.dll, только для 32-битного процесса.
7EFDB000 TEB Блок переменных окружения потока (Thread Environment Block). Структура данных, размещаемая в адресном пространстве процесса, которая содержит информацию о конкретном потоке в пределах основного (текущего) процесса (в нашем случае — test2.exe). Каждый поток имеет свой TEB. Заполняется через функцию MmCreateTeb и заполняется загрузчиком потока. Создается, контролируется и разрушается исключительно самой ОС. Подобные регионы создаются и уничтожаются по мере появления/уничтожения потоков в процессе. Wow64 процессы имеют два TEB для каждого потока?
7EFDE000 PEB Блок переменных окружения процесса (Process Environment Block). Структура данных, размещенная в адресном пространстве процесса, которая содержит информацию о загруженных модулях (LDR_DATA), окружении, базовой информации и другие данные, которые требуются для нормального функционирования процесса. Создается через функцию MmCreatePeb и заполняется загрузчиком процесса на этапе создания адресного пространства процесса. PEB создается, контролируется и уничтожается исключительно самой ОС. Wow64 процессы имеют два PEB для каждого процесса?
80000000 Ядро Память выше данного значения принадлежит ядру. В этой области памяти находятся модули ядра, объекты ядра и пользовательские объекты, доступные всем процессам — проекции системных файлов. Но это все уже тема отдельной статьи.

Выводы

К каким выводам можно придти после изучения адресного пространства процесса? Первый состоит в том, что понятие «памяти» для пользовательских программ это достаточно условное обозначение, поскольку регионы адресного пространства могут по разному отображаться на различные объекты операционной системы. Второй состоит в том, что адресное пространство процесса это огромный линейный массив байтов, в котором хранится всё, с чем непосредственно работает процесс (программа). Массив этот виртуален, не ограничен физической памятью, уникален для каждого приложения и обладает достаточной размерностью, дабы программист не задумывался о его ограничениях. Механизм создания адресного пространства процесса достаточно сложен, и в статье удалось рассмотреть лишь малую часть его логики. В добавок, мы вовсе не касались 64-битных реалий, грозно смотрящих на нас из недалекого будущего :) но, пожалуй это тема отдельной статьи.

В прошлый раз мы рассмотрели вопросы использования глобальных переменных. Прежде, чем двинуться дальше, нужно дать небольшой вводный курс по памяти в 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 Кб памяти. Ну, за вычетом той памяти, что уже занята, конечно же. Адрес объекта программы был равен адресу физической ячейке оперативной памяти (физическому адресу). И если у вас заканчивалось место в ОЗУ, то у вас заканчивалась память в программе.

Конечно, такой способ хотя и весьма прост, имеет две проблемы:

  1. Память программы ограничена оперативной памятью. А раньше эта память была дорогой и её было очень мало.
  2. Если нужно запустить две программы, то они будут работать «в одной песочнице»: и первая и вторая программа будут размещать свои данные в одном месте — оперативной памяти. И если первая программа по ошибке запишет что-то в данные второй, то… ой.

Виртуальная память и виртуальное адресное пространство

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

  1. считать, что установлено максимальное теоретически возможное количество оперативной памяти;
  2. считать, что она является единственной программой, запущенной на машине.

Иными словами, адресное пространство программы более не ограничено размером физической памяти (так называют оперативную память компьютера, чтобы специально указать на её отличие от виртуальной памяти) — адресное пространство имеет теперь максимально возможный размер. К примеру, если для адресации используются 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).
Читать далее: Адресное пространство под микроскопом.

В моей первой публикации из серии «Преодолевая ограничения Windows» я рассказал об ограничениях, накладываемых на физическую память, включая ограничения, связанные с лицензированием, реализацией и совместимостью драйверов. В этот раз я решил обратить ваше внимание на другой фундаментальный системный ресурс – виртуальную память. Виртуальная память призвана в некотором смысле отвлечь внимание приложения от физической памяти, при этом операционная система должна решить, когда сохранять код и иную информацию в физической памяти (и стоит ли это делать вообще) и когда стоит сохранить их в файл. Главное преимущество виртуальной памяти заключается в том, что она позволяет одновременно выполняться большому количеству процессов, которые все сразу не смогли бы поместиться в физической памяти.

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

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

Приложения могли бы использовать для распределения виртуальной памяти Heap API, сборщик мусора .Net или Malloc-библиотеку C, но все они так или иначе опираются на VirtualAlloc API. Когда приложение исчерпывает свое адресное пространство, VirtualAlloc, а, следовательно, и диспетчеры памяти, построенные на его основе, возвращают ошибки (представленные адресом NULL). Утилита Testlimit, которую я написал для четвертого издания Windows Internals для того, чтобы продемонстрировать различные ограничения Windows, многократно вызывает VirtualAlloc до тех пор, пока не получит ошибку, вызванную указанием параметра -r. Таким образом, когда вы запускаете 32-битную версию Testlimit на 32-битной Windows, она займет все 2 Гб своего адресного пространства:

vm2

2010 Мб – это чуть меньше 2-х гигабайт, но наличие в памяти кодов и данных Testlimit, включая исполнительные и системные DLL, объясняют эту разницу. В графе Virtual Size утилиты Process Explorer вы можете увидеть общий объем адресного пространства программы:

Некоторые приложения, такие как SQL Server и Active Directory, управляют большими структурами данных, объем которых намного превышает доступное для них адресное пространство. Поэтому в Windows NT 4 SP3 ввели загрузочную опцию /3GB, позволяющую предоставить процессу 3 Гб из его четырех гигабайтного адресного пространства, уменьшая системное адресной пространство до 1 Гб, а в Windows XP и Windows Server 2003 встроили функцию /userva, которая перемещает границу разделения в пределах с 2 до 3 Гб:

Однако, чтобы использовать адресное пространство выше отметки 2 Гб, в исполняемом образе приложения обязательно должен содержаться набор флагов «large address space aware». Доступ к дополнительной виртуальной памяти является опциональным, потому что некоторые приложения рассчитаны на то, что им будет предоставлено не более 2 Гб адресного пространства. Так как старший разряд указателя, ссылающегося на адрес ниже отметки 2 Гб, всегда является нулем, они использовали бы его как флаг для их собственных данных, очищая его прежде, чем ссылаться на данные. Если бы такие приложения работали в 3-х гигабайтном адресном пространстве, они бы отсекли указатели, значения которых находятся выше 2 Гб, вызывая тем самым ошибки, в том числе и возможное нарушение целостности данных.

Все серверные продукты Microsoft и приложения Windows, интенсивно использующие память, имеют поддержку специального флага, позволяющего использовать адресное пространство выше отметки 2 ГБ; среди таких приложений Chkdsk.exe, Lsass.exe (которая являются главными сервисами Active Directory на контроллере домена), Smss.exe (менеджер сессий) и Esentutl.exe (инструмент восстановления базы данных Jet Active Directory). При помощи утилиты Dumpbin, входящей в состав Visual Studio, вы можете увидеть, у образов каких приложений есть этот флаг:

vm5

Testlimit также имеет этот флаг, так что если вы запустите его с параметром -r в 3-х гигабайтном пользовательском адресном пространстве, вы увидите что-то наподобие этого:

vm6

Поскольку адресное пространство в 64-битных Windows намного больше 4 Гб, такие версии Windows могут дать 32-битным процессам максимальные 4 Гб, которые они могут адресовать, и использовать оставшуюся виртуальную память для нужд операционной системы. Если вы запустите Testlimit на 64-битной Windows, то увидите, что она занимает все адресное пространство, к которому могут обратиться 32-разрядные приложения:

vm7

64-битные процессы используют 64-битные указатели, так что их теоретическое максимальное адресное пространство равно 16 экзабайтам (2 в 64-ой степени). Однако, Windows не делит адресное пространство равномерно между активными процессами и системой, а вместо этого определяет область в адресном пространстве для процессов и других системных ресурсов памяти, таких как системные записи таблицы страниц (PTE), файловые кэши, резидентный и нерезидентный (paged и non-paged) пулы.

Размеры адресного пространства процессов отличаются в IA64 и x64-версиях Windows, где он определяется так, чтобы соблюдался баланс между тем, сколько памяти требуется приложению, и издержками верхней памяти (страницы таблицы страниц и записи буфера трансляции адресов TLB), необходимых для поддержания адресного пространства. Для x64 это 8192 Гб (8 Тб), а для IA64 – 7168 Гб (7Тб) (разница в 1 Тб объясняется тем, что каталоги страниц верхнего уровня в IA64 резервируют слоты для распределений Wow64). И в IA64- и в x64-версиях Windows размер областей адресного пространства для различных ресурсов составляет 128 Гб (например, для нерезидентного пула отводится 128 Гб адресного пространства), за исключением кэша файла, которому выделяется 1 Тб. Поэтому адресное пространство 64-битного процесса выглядит примерно вот так:

На этом рисунке не соблюден масштаб, поскольку в ином случае даже 8 Тб, не говоря уже о 128 Гб, выглядели бы на нем тоненькой полоской. Я использовал этот рисунок, чтобы показать вам, что, как и в случае с нашей вселенной, в адресном пространстве 64-битного процесса есть много незанятного места.

Когда вы запустите 64-битную версию Testlimit (Testlimit64) на 64-битной Windows с параметром -r, вы увидите, что она занимает 8 Тб, которые являются той частью адресного пространства, которыми она может управлять:

vm9

Выделенная память
Параметр -r в утилите Testlimit резервирует виртуальную память, но не выделяет ее. Зарезервированная виртуальная память не может хранить данные или код, но приложения иногда используют резервирование для создания больших блоков виртуальной памяти, которую впоследствии они могут выделять, когда необходимо, чтобы выделенная память находилась в смежной с адресным пространством области. Когда процесс выделяет область в виртуальной памяти, операционная система гарантирует, что может предоставить для размещения всех данных процесса область или в физической памяти, или на диске. Это означает, что процесс может столкнуться с еще одним видом ограничений: ограничением на объем выделяемой памяти.

Из вышеизложенного следует, что граница у объема выделяемой памяти равна сумме физической памяти и размера файла подкачки. В действительности в определении границы учитывается не вся физическая память, потому как ее часть операционная система резервирует для своих нужд. Объем выделенной виртуальной памяти для всех активных процессов, называемый текущей выделенной памятью (current commit charge), не может превысить предела, установленного системой. Когда этот предел достигнут, службы, занимающиеся выделением виртуальной памяти, выдают ошибку. Это означает, что даже стандартный 32-битный процесс может столкнуться с ошибкой выделения виртуальной памяти еще до того, как он достигнет предела на объем виртуального адресного пространства в 2 Гб.

Текущий объем выделенной памяти и ее ограничение можно посмотреть в окне System Information утилиты Process Explorer в разделе Commit Charge и на графике Commit History:

Диспетчер задач в системах до Vista и Windows Server 2008 точно показывает эти параметры, но там текущий объем выделенной памяти на графике назван «PF Usage»:

В Vista и Server 2008 диспетчер задач не показывает график текущей выделенной памяти, а отображает эти два значения в строке Page File (даже если вы отключите файл подкачки, эти значения будут ненулевыми):

Чтобы проверить значение границы выделенной памяти, вы можете запустить Testlimit с параметром -m, чтобы программа распределила выделенную память. 32-битная версия Testlimit может и не достигнуть предела своего адресного пространства до того, как достигнет предела на выделенную память; это зависит от объема физической памяти, размера файла подкачки и значения текущей выделенной памяти на момент запуска программы. Если вы используете 32-битную версию Windows и хотите увидеть, как поведет себя система при достижении предела на выделенную память, просто запустите несколько экземпляров Testlimit до тех пор, пока один из них не достигнет предела выделенной памяти до того, как исчерпает свое адресное пространство.

Здесь следует отметить, что по умолчанию файл подкачки настроен так, чтобы увеличиваться в размерах. Это означает, что предел для выделенной памяти также будет расти по мере приближения к нему значения выделенной памяти. И даже тогда, когда файл подкачки достигнет своего максимального размера, Windows может освободить еще немного памяти, которую она зарезервировала, аналогично тому, как некоторые приложения создают кэш данных. Testlimit знает об этом, и когда она достигает предела для выделенной памяти, приложение несколько секунд бездействует, а затем пытается получить еще немного памяти. Приложение будет повторять попытки до тех пор, пока вы его не отключите.

Если вы запустите 64-битную версию Testlimit, то она почти наверняка достигнет предела для выделенной памяти до того, как исчерпает свое адресное пространство, если, конечно, размеры файлов подкачки и физической памяти не дают в сумме более 8 Тб, которые, как описано ранее, являются пределом 64-битного адресного пространства, доступного приложению. Вот фрагмент результатов работы 64-битной версии Testlimit, запущенной на моей системе с 8Гб памяти (я указал размер на выделяемую память в 100 Мб, чтобы быстрее достигнуть предела):

vm14

А вот график для выделяемой памяти, на котором шаги соответствуют паузам в работе Testlimit, сделанным для того, чтобы файл подкачки смог увеличиться:

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

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

Память, выделяемая процессу
Поскольку предел для выделенной памяти является общим показателем для всей системы, достижение которого может привести к общему снижению производительности, ошибкам приложений или даже к зависанию системы, вполне логичным выглядит вопрос «сколько выделенной памяти должно быть выделено процессу»? Чтобы точно ответить на этот вопрос, вам нужно знать, какие еще типы виртуальной памяти могут быть заняты процессом.

Не вся виртуальная память, которой владеет процесс, имеет отношение к пределу выделенной памяти. Как вы уже знаете, примером здесь может служить зарезервированная виртуальная память. Виртуальная память, которая представляет файл на диске (такое представление носит название File Mapping View), также не участвует в расчете этого предела, за исключением случаев, когда приложение требует семантики copy-on-write, потому что Windows может стереть любые данные, связанные с образами в физической памяти, а после восстановить их из этого файла. Поэтому виртуальная память из адресного пространства Testlimit, в которую отображаются ее исполнительные и системные DLL, не учитывается при расчете предела выделенной памяти. Есть два типа виртуальной памяти процесса, которые учитываются при определении границы выделенной памяти: эти области называются «private» и «pagefile-backed».

Закрытая виртуальная память является тем видом виртуальной памяти, на основе которого строятся Garbage Collector Heap, Native Heap и Language Allocator. Она называется закрытой, потому что к ней не может быть открыт общий доступ для других процессов. По этой причине ее легко приписать к процессу, а Windows может отслеживать ее использование с помощью счетчика Private Bytes. Process Explorer отображает объем используемой закрытой памяти процесса в графе Private Bytes секции Virtual Memory на страницу Performance диалогового окна свойств процесса, а также отображает эти показатели в графическом виде на странице Performance Graph. Вот как выглядел Testlimit64 после достижения предела выделенной памяти:

vm19

Виртуальную память Pagefile-backed сложно отнести к конкретному процессу, потому что она может одновременно использоваться несколькими процессами. Фактически, нет никакого счетчика, привязанного к одному конкретному процессу, позволяющего посмотреть, сколько памяти этому процессу выделено или на какой объем этой памяти он ссылается. Когда вы запускаете утилиту Testlimit с параметром -s, она начинает занимать участки виртуальной памяти pagefile-backed до тех пор, пока не достигнет предела для выделенной памяти, но даже когда объем выделенной памяти превысит 29 Гб, статистика потребления виртуальной памяти для данного процесса не будет содержать ни одного указателя на то, что он один ответственен за это:

Именно по этой причине в программу Handle я добавил параметр -l. Процесс должен открыть объект в виртуальной памяти pagefile-backed, носящий название секции, для создания отображения виртуальной памяти pagefile-backed в его адресном пространстве. Так как Windows сохраняет существующую виртуальную память, даже если приложение закроет дескриптор к секции, из которой он был создан, большинство приложений сохранят этот дескриптор открытым. Параметр -l отображает размеры областей для секций pagefile-backed, которые открыты процессами. Вот часть результатов работы Handle для утилиты Testlimit, после того, как она была запущена с параметром -s:

Как видите, Testlimit занимает память pagefile-backed блоками по 1 Мб, и если просуммировать размер всех блоков, открытых этой программой, можно увидеть, что она является одним из процессов, занявших наибольший объем выделенной памяти.

Насколько большим следует сделать файл подкачки?
Возможно, наиболее частым вопросом, связанным с виртуальной памятью, является вопрос «насколько большим следует сделать файл подкачки?». Ни в сети, ни в информационных изданиях, освещающих вопросы Windows, нет конкретного ответа на этот вопрос, и даже Microsoft опубликовала на этот счет довольно запутанные рекомендации. Почти все советы основаны на умножении объема оперативной памяти на некоторое значение, например, на 1.2, 1.5 или 2. Теперь, когда вы понимаете, какую-роль играет файл подкачки в определении системного лимита на выделенную память и как процессы влияют на объем выделенной памяти, мы легко можете увидеть, насколько бесполезны такие формулы в действительности.

Так как предел для выделенной памяти устанавливается на основе того, сколько закрытой и pagefile-backed виртуальной памяти может быть одновременно выделено выполняющимся процессам, единственный способ правильно установить размер файла подкачки заключается в том, чтобы узнать, какой максимальный объем выделенной памяти может быть занят программами, которые вы часто запускаете одновременно. Если предел для выделенной памяти будет меньше этого значения, то ваши программы не смогут получить необходимую им виртуальную память и будут некорректно работать.
Так как же узнать, сколько выделенной памяти требуется вашей рабочей среде? На снимках экрана вы, возможно, заметили, что это число отслеживается Windows и Process Explorer показывает его: Peak Commit Charge. Чтобы установить оптимальный размер для вашего файла подкачки, вы должны запустить все приложения, с которыми вы работаете одновременно, загрузить типичный для вас объем данных и посмотреть пиковое значения выделенной памяти (или же посмотреть это значение по прошествии некоторого времени, когда, по вашему мнению, будет достигнута максимальная загруженность памяти). В качестве минимального размера файла подкачки установите это значение, отняв от него размер установленной на вашей системе оперативной памяти (если получиться отрицательное значения, выберите размер, позволяющий сохранить возможное количество отказов на минимальном уровне). Если вы хотите сохранить некоторый запас для потенциально больших расходов выделенной памяти, в качестве максимума установить это значение, умноженное на 2.

У вас может сложиться впечатление, что отсутствие файла подкачки может благотворно сказаться на производительности, однако в общем случае то, что у Windows в распоряжении будет файл подкачки, означает, что ОС сможет размещать некоторые записи (которые используются нечасто и не сохранены на диск) в файл подкачки, освобождая тем самым память для более полезных задач (процессы и кэши файлов). Так что даже если в некоторых случаях отсутствие файла подкачки может увеличить производительность, в общем случае его наличие означает, что в распоряжении системы будет больше доступной памяти (Windows в случае сбоя не сможет сделать дамп памяти, занятой под процессы ядра, если в ее распоряжении не будет достаточно большого файла подкачки).
Настройки файла подкачки находятся в окне Свойства системы, которое вы можете открыть, введя в окне Выполнить строчку «sysdm.cpl», после этого, зайдя на вкладку Дополнительно, нажать кнопку Параметры раздела Быстродействие, затем на вкладке Дополнительно нажать кнопку “Изменить”:

vm22

Там вы можете заметить, что по умолчанию Windows сама управляет размером файла подкачки. Когда это опция установлена в Windows XP и Server 2003, ОС создает один файл подкачки, минимальный размер которого равен 1,5 объема ОП системы; если объем оперативной памяти больше 1 Гб, то такой же размер устанавливается для файла подкачки; максимальный размер файла подкачки равен трем объема ОП. В Windows Vista и Server 2008 минимальное значения должно быть достаточно для того, чтобы в случае сбоя системы сделать дамп памяти, занятой под процессы ядра, и равно ОП+300 Мб или 1 Гб, в зависимости от того, какое значение окажется больше. Максимально значение – три объема оперативной памяти или 4 Гб, в зависимости от того, что окажется больше. Это объясняет тот факт, что на моей 64-битной системе с 8 Гб ОП пиковое значения выделенной памяти равно 32 Гб.

Другими ограничениями, связанными с виртуальной памятью Windows, являются максимальный размер и количество файлов подкачки. Для 32-битной Windows максимальный размер файла подкачки равен 16 Тб (4 Гб, если вы по какой-то причине работаете в режиме non-PAE), а 64-битная Windows может иметь файл подкачки размером до 16 Тб для x64 и 32 Тб в случае IA64. Для всех версий Windows есть возможность создавать до 16 файлов подкачки, каждый из которых должен располагаться на разных томах.

Оригинал записи

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Функция связь с windows на андроид что это
  • Приложения metro windows 10 что это
  • Скорость интернета на панели задач windows 11
  • Список сочетаний клавиш для windows
  • Windows server 2012 r2 mbr или gpt