Преодолевая ограничения Windows: выгружаемый и невыгружаемый пулы

В предыдущих статьях из цикла "Преодолевая границы Windows" я рассказал о двух основных ресурсах системы: физической памяти и виртуальной памяти. В этот раз я собираюсь поведать о двух фундаментальных ресурсах ядра, выгружаемом (от англ. paged) и невыгружаемом (от англ. nonpaged) пулах, которые основаны на первых двух ресурсах и которые имеют непосредственное влияние на многие другие ограничения системных ресурсов, включая максимальное число процессов, объектов синхронизации и дескрипторов.

Выгружаемый и невыгружаемый пулы представляют собой ресурсы памяти, которые операционная система и драйвера устройств используют для сохранения своих структур данных. Диспетчер пула работает в режиме ядра, используя области системного виртуального адресного пространства (которое описано в статье про виртуальную память) для памяти, которую он выделяет. Диспетчер пула ядра также работает с диспетчером C-runtime и диспетчером динамической памяти Windows, которые выполняются в пользовательском режиме. Поскольку минимальный размер выделяемой виртуальной памяти кратен размеру системной страницы (4KB для систем x86 и x64), эти вспомогательные диспетчеры памяти делят большие выделяемые участки памяти на маленькие части, чтобы память не расходовалась впустую.

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

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

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

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

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

Просмотр информации об использовании выгружаемого и невыгружаемого пулов.
Есть три счетчика, показывающих информацию об использовании пула:

  • Пул невыгружаемых байтов;
  • Пул выгружаемых байтов (виртуальный размер выгружаемого пула);
  • Пул выгружаемых резидентных байтов.

Однако, не существует каких-либо счетчиков производительности (performance counters) указывающих максимальный размера этих пулов. Такие данные можно получить воспользовавшись командой отладчика ядра !vm, но, чтобы использовать отладчик ядра в режиме локальной отладки в Windows Vista и более поздних версиях системы, вам необходимо загрузить систему в режиме отладки, в котором отключено воспроизведение MPEG2.

Так что вместо этого для просмотра текущего и максимального выделенного размера пула лучше использовать Process Explorer. Чтобы увидеть максимум, вам нужно настроить Process Explorer на использование файлов отладочных символов операционной системы. Во-первых, установите последнюю версию пакета Debugging Tools for Windows. Затем запустите Explorer и откройте диалоговое окно Symbol Configuration из меню Options и укажите в нем путь на файл dbghelp.dll в установочной директории Debugging Tools for Windows, после чего задайте в поле Symbol path сервера символов Microsoft:

 

image_4

После того, как вы настроите символы, откройте диалоговое окно System Information (кликните на пункте System Information меню View или нажмите Ctrl+I), чтобы увидеть информацию о пуле в секции Kernel Memory. Вот как это выглядит в системе Windows XP с 2Гб памяти:

image_6

32-x битная Windows XP с 2-мя Гб ОЗУ

Ограничения на размер невыгружаемого пула.

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

До Vista диспетчер памяти в 32-битных Windows вычислял, сколько адресного пространства назначать каждому типу ресурсов во время загрузки. Его формулы принимали во внимание множество факторов, главным из которых являлось количество физической памяти в системе. Объем адресного пространства, выделяемого им для невыгружаемого пула, начинается с 128Мб для системы с 512Мб системной памяти и достигает 256Мб для систем с 1Гб памяти и более. На системе, загруженной с опцией /3GB, которая расширяет адресное пространство пользовательского режима до 3Гб за счет адресного пространства ядра, максимальный размер невыгружаемого пула составляет 128Мб. На предыдущем снимке Process Explorer отображается максимум в 256Мб на системе Windows XP с 2Гб системной памяти, загруженной без флага /3GB.

Диспетчер памяти в Windows Vista и в более поздних версиях системы, включая Server 2008 и Windows 7 (32-битной версии Windows Server 2008 R2 нет) не реализует статическое разделение системного адресного пространства; вместо этого, он динамически назначает диапазоны адресов различным типам памяти согласно изменяющимся требованиям. Однако, он все еще назначает максимальный размер невыгружаемого пула, который основывается на количестве физической памяти, равный чуть больше чем 75% от физической памяти или двум гигабайтам, в зависимости от того, что меньше. Вот максимум для системы Windows Server 2008 с 2Гб оперативной памяти:

image_8

32-x битный Windows Server 2008 с 2-мя Гб ОЗУ

64-х битные системы Windows обладают гораздо большим адресным пространством, так что диспетчер памяти может спокойно распределять его статически, не беспокоясь о том, что какому-то типу данных не будет хватать места. 64-битная Windows XP и Windows Server 2003 устанавливают максимальный размер невыгружаемого пула немногим более 400Кб на каждый мегабайт оперативной памяти или в 128Гб, в зависимости от того, что меньше. Вот снимок из системы с 64-битной Windows XP и 2Гб памяти:

image_10

64-x битная Windows XP с 2-мя Гб ОЗУ

Диспетчеры памяти 64-битных Windows Vista, Windows Server 2008, Windows 7 и Windows Server 2008 R2, так же как и их 32-битные аналоги (за исключением Windows Server 2008 R2, у которого, как уже упоминалось ранее, нет 32-битной версии) устанавливают ограничение на размер невыгружаемого пула приблизительно в 75% от RAM, однако максимальное значение для него равно 128Гб вместо 2Гб. Вот скриншот системы с 64-битной Windows Vista с 2Гб системной памяти, для которой ограничение на размер невыгружаемого пула равно таковому для системы с 32-битной Windows Server 2008, снимок которой приведен ранее

image_12

32-x битная Windows XP с 2-мя Гб ОЗУ

И наконец, вот данное ограничение для системы с 64-битной Windows 7 с 8Гб памяти:

image_24

Вот сводная таблица ограничений на размер невыгружаемого пула для различных версий Windows:

  32-бита 64-bit
XP, Server 2003 до 1.2ГБ ОЗУ: 32-256 МБ  > 1.2ГБ ОЗУ: 256 МБ минимум ( ~400K/МБ ОЗУ, 128ГБ)
Vista, Server 2008, Windows 7, Server 2008 R2 минимум ( ~75% of RAM, 2GB) минимум (~75% ОЗУ, 128ГБ)

Ограничения на размер выгружаемого пула
Ядро и драйверы устройств используют выгружаемый пул для хранения любых структур данных, которые никогда не будут вызываться изнутри DPC или ISR, или когда спинлок (spinlock) занят. Именно поэтому содержимое выгружаемого пула может либо находиться в физической памяти, либо, если алгоритмы работы диспетчера памяти решат использовать данную физическую память для других целей, быть записано в файл подкачки, откуда его, если это понадобиться, можно возвратить обратно в физическую память. Потому ограничение на размер выгружаемого пула прежде всего зависит от объема системного адресного пространства, выделяемого для выгружаемого пула диспетчером памяти.

Для 32-х битной Windows XP данный предел вычисляется исходя из того, сколько адресного пространства выделено другим ресурсам, в особенности таблице PTE, с максимальным значением в 491Мб. Для системы с Windows XP и 2Гб системной памяти данное ограничение равно 360Мб:

image_6

32-x битная Windows XP с 2-мя Гб ОЗУ

 

32-х битный Windows Server 2003 резервирует для выгружаемого пула больше места, так что верхний предел в этом случае равен 650Мб.
Так как 32-битная Windows Vista и все последующие системы реализуют динамическое адресное пространство ядра, для них данное ограничение просто установлено в 2Гб. Потому увеличение размера выгружаемого пула прекратится тогда, когда системное адресное пространство заполнится, либо когда будет достигнут установленный системой предел.

64-х битные Windows XP и Windows Server 2003 устанавливают этот максимум равным ограничению на размер невыгружаемого пула, умноженного на четыре, либо 128 Гб, в зависимости от того, что окажется меньше. Вот снимок системы с 64-битной версией Windows XP, на котором предел размера выгружаемого пула равен как раз четырем размерам невыгружаемого пула:

image_10

И наконец, 64-х битные версии Windows Vista, Windows Server 2008, Windows 7 и Windows Server 2008 R2, просто устанавливают данный максимум в 128Гб, позволяя тем самым с помощью ограничения на размер выгружаемого пула определить системное ограничение. Вот снимок системы с 64-битной Windows 7:

image_24

64-x битная Windows 7 с 8-ю Гб ОЗУ

 

Вот сводная таблица ограничений на размер выгружаемого пула для различных операционных систем:

  32-бита 64-бита
XP, Server 2003 XP: до 491МБ Server 2003: до 650МБ минимум ( 4 * невыгружаемого пула, 128ГБ)
Vista, Server 2008, Windows 7, Server 2008 R2 минимум ( system commit limit, 2GB) минимум ( system commit limit, 128ГБ)

 

Тестирование ограничений на размер пула
Поскольку пулы ядра используются практически каждой операцией ядра, полное их истощение может привести к непредсказуемым результатом. Если вы хотите на своем опыте посмотреть, как ведет себя система в случае интенсивного использования пула используйте утилиту Notmyfault. У нее есть функции, которые позволяют осуществить утечку некоторого указанного пользователем объема выгружаемого или невыгружаемого пулов. Вы можете изменять этот размер по своему желанию, а после закрытия Notmyfault вся занятая этой утилитой память освобождается:

image_14

Вам не стоит запускать это приложение в своей системе, если вы не готовы к возможной потере данных, так как после того, как свободное место пула закончится, приложения и операции ввода/вывода начнут завершаться с ошибкой. Вы даже можете получить синий экран смерти (BSOD), если какой-то драйвер не сможет корректно обработать условие выгрузки из памяти (что считается ошибкой драйвера). Windows Hardware Quality Laboratory (WHQL) тестирует драйверы, используя Driver Verifier - утилиту, встроенную в Windows - чтобы убедиться в том, что они могут обрабатывать выгрузку из пула без ошибок, однако у вас могут быть установлены драйверы сторонних производителей, которые либо не прошли такое тестирование, либо имеют ошибки, которые не были обнаружены во время тестов WHQL.

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

image_16

В 32-битной Windows Server 2008 с уже запущенной командной строкой после того, как невыгружаемый пул был исчерпан, даже простые операции, такие как изменения текущей директории или вывод списка директорий, завершались с ошибкой:

image_18

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

image_42

Такие же ошибки возникали и в случае исчерпания выгружаемого пула. Вот результаты попытки запуска Блокнота из командной строки на системе с 32-битной Windows XP, после того как выгружаемый пул был исчерпан. Обратите внимание на то, что Windows не смогла запустить перерисовку заголовка окна, а также выдавала различные ошибки при каждой новой попытке запуска приложения:

image_20

А вот пример того, как на системе с 64-битной Windows Server 2008 после исчерпания выгружаемого пула в папке Стандартные меню Start не оказалось ни одного пункта:

image_22

На данном снимке вы можете видеть максимальный уровень занимаемой памяти (который также отображается в диалоговом окне System Infomations программы Process Explorer), который стремительно повышается по мере того, как Notmyfault совершает утечку крупных участков выгружаемого пула и достигает максимума в 2Гб на системе, работающей по управлением 32-битной версии Windows Server 2008 с 2Гб оперативной памяти:

image_26

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

Отслеживание утечек пула
В случае, если вы подозреваете наличие утечки пула и система все еще может запускать приложения, Poolmon - инструмент из набора Windows Driver Kit - покажет вам число выделенных областей и количество незанятых байтов в этих областях, разделенных по типу пула, а также теги запросов ExAllocatePoolWithTag. Различные комбинации горячих клавиш позволяют Poolmon сортировать содержимое по различным колонкам; чтобы найти выделенные участки памяти, являющие собой утечки, нажмите клавишу "b" для сортировки по байтам, или "d" для сортировки по разности между числом выделенных и свободных участков памяти. Вот снимок, демонстрирующий работу Poolmon на системе, где с помощью Notmyfault была организована утечка 14 участков памяти пула, каждый примерно по 100Мб:

image_38

После того, как вы нашли искомый тег в левой колонке (в данном случае это "Leak"), следующим шагом будет нахождение драйвера, использующего этот тег. Так как эти теги хранятся в образе драйвера, вы можете просканировать данный образ на наличие рассматриваемого тега. Утилита Strings от Sysinternals сохраняет искомые строки в указанном вами файле (искомая строка по умолчанию должна быть не короче 3-х символов, и, так как большинство образов драйверов находятся в директории %Systemroot%\System32\Drivers, вы можете открыть командную строку, изменить текущую директорию на указанную и выполнить команду "strings * | findstr <tag>". После того, как вы найдете соответствия, вы можете получить информацию о версии драйвера с помощью утилиты Sigcheck от Sysinternals. Вот как выглядит процесс поиска драйвера, использующего тег "Leak":

image_30

Если произошел сбой системы и вы подозреваете, что это виной этому истощение пула, загрузите дамп-файл этого сбоя в отладчике Windbg, который включен в состав пакета Debugging Tools for Windows, и удостоверьтесь в этом с помощью команды !vm. Вот результат запуска этой команды на системе, в которой Notmyfault исчерпал невыгружаемый пул:

image_34

Как только вы убедитесь в наличии утечки, воспользуйтесь командой !poolused, чтобы просмотреть информацию об использовании пула, как это было в Poolmon. По умолчанию эта команда выдает несортированные данные, так что используйте параметр 1 для сортировки по использованию выгружаемого пула, и 2 - для сортировки по использованию невыгружаемого пула:

image_36

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

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

 

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