Элементы u1.Flink / u2.Blink поддерживают связанность шести списков страниц, про которые говорилось выше, используются, когда u3.e1.PageLocation < ActiveAndValid.
Если u3.e1.PageLocation >= ActiveAndValid, тогда второе объединение трактуется как u2.ShareCount и содержит счетчик числа пользователей - количество PTE, ссылающихся на эту страницу. Для страниц, содержащих массивы PTE, содержит число действительных PTE на странице.
Если u3.e1.PageLocation == ActiveAndValid, u1 трактуется как u1.WsIndex - индекс страницы в рабочем наборе (или 0 если страница в неподкачиваемой области памяти).
Если u3.e1.PageLocation == TransitionPage, u1 трактуется как u1.Event - адрес объекта "событие", на котором будет ожидать менеджер памяти для разрешения доступа на страницу.
Если u4.InPageError == 1, то u1 трактуется как ReadStatus и содержит статус ошибки чтения.
ReferenceCount содержит счетчик ссылок действительных PTE на эту страницу или использования ее внутри менеджера памяти (например, во время записи страницы на диск, счетчик ссылок увеличивается на единицу). Он всегда >= ShareCount
PteAddress содержит обратную ссылку на PTE, который указывает на эту физическую cтраницу. Младший бит означает, что PFN удаляется.
OriginalPte содержит оригинальный PTE, используемый для восстановления его в случае выгрузки страницы.
u4.PteFrame - номер PTE, поддерживающего страницу, где находится текущая структура MMPFN.
Кроме того объединение u4 содержит еще и следующие дополнительные флаги:
- InPageError - показывает, что при чтении страницы с диска произошла ошибка. u1.ReadStatus хранит статус этой ошибки.
- VerifierAllocation устанавливается в единицу для аллокаций, защищаемых Driver Verifier.
- AweAllocation устанавливается в единицу для Address Windowing Extension
- Назначение поля LockCharged и одноименного поля MMPFNENTRY мне, к сожалению, не известно. Если кто знает - поделитесь.
- KernelStack, видимо, устанавливается в единицу для страниц, принадлежащих стеку ядра.
Если страница находится в списке обнуленных или простаивающих страниц, второе объединение трактуется как указатель, связывающий списки обнуленных или свободных страниц по вторичному цвету (т.н. Secondary Color). Различие по цвету делается по следующей причине: количество цветов устанавливается в количество страниц, которые может вместить в себя кеш-память второго уровня процессора и различие делается, чтобы два соседних выделения памяти не использовали страницы одного цвета для правильного использования кеша.
Объединение u3, фактически, содержит флаги данного PFN. Рассмотрим что же они означают:
- Modified. Установлен для подкачиваемых или спроецированных с диска страниц, что ее содержимое было изменено и должно быть сброшено на диск.
- ReadInProgress, он же StartOfAllocation
- WriteInProgress, он же EndOfAllocation
- PrototypePte, он же LargeSessionAllocation
Для неподкачиваемых системных адресов эти три поля трактуется как StartOfAllocation, EndOfAllocation и LargeSessionAllocation и обозначают следующее:
StartOfAllocation установлено в 1, если эта страница является началом неподкачиваемого пула.
EndOfAllocation установлено в 1, если эта страница является концом неподкачиваемого пула.
LargeSessionAllocation установлено в 1 для больших аллокаций в пространстве сессии.
Для подкачиваемых адресов эти поля означают следующее:
ReadInProgress установлен, пока страница находится в процессе чтения с диска
WriteInProgress установлен, пока страница записывается на диск
PrototypePte установлен, когда PTE, который ссылается на эту PFN, является прототипным.
- PageColor, он же иногда называемый Primary Page Color, или цвет страницы. Используется на некоторых платформах для равномерного распределения списков страниц (аллокации вида MiRemoveAnyPage выдаются страницу каждый раз из другого списка другого цвета и несколько списков, поддерживающих, например, свободные страницы, расходуются равномерно). В x86 и x64 используется всего один цвет страниц и это поле всегда равно нулю.
Не путать с Secondary Color, который используется для равномерного распределения страниц по кешу второго уровня и используется в функциях MiRemoveZeroPage, MiRemoveAnyPage и др. Кроме простых списков свободных и обнуленных страниц так же поддерживаются списки свободных и обнуленных страниц по цвету - MmFreePagesByColor[список][SecondaryColor], где _список_ - это ZeroedPageList или FreePageList. Списки поддерживаются вместе с общими списками свободных и обнуленных страниц, при обнаружении несоответствия генерируется синий экран PFN_LIST_CORRUPT.
- PageLocation - тип страницы (как раз один из восьми вышеперечисленных от ZeroedPageList до TransitionPage)
- RemovalRequested - этим битом помечаются страницы, запрошенные к удалению. После уменьшения их счетчика ссылок до нуля, PTE станет недействительным переходным, а страница попадет в список плохих (BadPageList)
- CacheAttribute - атрибут кеширования страницы. MmNonCached или MmCached.
- Rom - новшество WinXP: физическая страница доступна только для чтения.
- ParityError - на странице произошла ошибка честности
Лучше усвоить написанное поможет пример, содержащийся в приложении к статье. В примере драйвер, который показывает доступные Memory Runs и демонстрирует обращение с PDE/PTE/PFN. Код примера хорошо откомментирован и, с учетом материала статьи, не должен вызвать вопросов.
IV. Управление виртуальной памятью - файл подкачки
Однако размещать все данные постоянно в физической памяти невыгодно - к каким-то данным обращения происходят редко, к каким-то часто, к тому иногда требуются объемы памяти большие, чем доступно физической памяти в системе. Поэтому во всех современных ОС реализован механизм подкачки страниц. Называется он по-разному - выгрузка, подкачка, своп. В Windows этот механизм представляет собой часть менеджера памяти, управляющего подкачкой, и максимально до 16 различных страничных файлов (paging files в терминологии Windows). В Windows есть подкачиваемая и неподкачиваемая память, соответственно, они могут и не могут быть выгружены на диск. Подкачиваемую память в ядре можно выделить из пула подкачиваемой памяти, неподкачиваемую - соответственно из пула неподкачиваемой (для небольших аллокаций). В пользовательском режиме память обычно подкачиваемая, если только она не была заблокирована в рабочем наборе с помощью вызова VirtualLock.
Страничные файлы в ядре Windows представлены переменной ядра MmPagingFile[MAX_PAGE_FILES] (максималное число страничных файлов, как можно было догадаться еще в самом начале по размеру поля номера страницы в страничном файле в 4 бита, составляет 16 штук). Каждый страничный файл в этом массиве представлен указателем на структуру вида:
Код:
typedef struct _MMPAGING_FILE {
PFN_NUMBER Size;
PFN_NUMBER MaximumSize;
PFN_NUMBER MinimumSize;
PFN_NUMBER FreeSpace;
PFN_NUMBER CurrentUsage;
PFN_NUMBER PeakUsage;
PFN_NUMBER Hint;
PFN_NUMBER HighestPage;
PVOID Entry[MM_PAGING_FILE_MDLS];
PRTL_BITMAP Bitmap;
PFILE_OBJECT File;
UNICODE_STRING PageFileName;
ULONG PageFileNumber;
BOOLEAN Extended;
BOOLEAN HintSetToZero;
BOOLEAN BootPartition;
HANDLE FileHandle;
} MMPAGING_FILE, *PMMPAGING_FILE;
- Size - текущий размер файла подкачки (стр.)
- MaximumSize - максимальный размер файла подкачки (стр.)
- MinimumSize - минимальный размер файла подкачки (стр.)
- FreeSpace - число свободных страниц
- CurrentUsage - число занятых страниц. Всегда верна формула Size = FreeSpace+CurrentUsage+1 (первая страница не используется)
- PeakUsage - пиковая нагрузка на файл подкачки
- Hint, HighestPage, HintSetToZero - [назначение неизвестно]
- Entry - массив из двух указателей на блоки MMMOD_WRITER_MDL_ENTRY, используемые потоком записи модифицированных страниц.
- Bitmap - битовая карта RTL_BITMAP занятости страниц в файле подкачки.
- File - объект "файл" файловой системы, используемый для чтения/записи в файл подкачки
- PageFileName - имя файла подкачки, например, \??\C:\pagefile.sys
- PageFileNumber - номер файла подкачки
- Extended - флаг, предположительно указывающий на то, расширялся ли файл подкачки когда-либо с момента создания
- BootPartition - флаг, указывающий на то, располагается ли файл подкачки на загрузочном разделе. Если нет ни одного страничного файла, размещенного на загрузочном разделе, то во время BSoD аварийный дамп записываться не будет.
- FileHandle - хендл файла подкачки.
В приложении к статье есть откомментированный пример с выводом полей структуры MmPagingFile[0] рабочей системы.
Когда системе нужна страница, а свободных страниц осталось мало, происходит усечение рабочих наборов процессов (оно происходит и по другим причинам, это лишь одна из них). Допустим, что усечение рабочих наборов было инициировано функцией MmTrimAllSystemPagableMemory(0). Во время усечения рабочих наборов, PTE страниц переводятся в состояние Transition, счетчик ссылок Pfn->u3.e2.ReferenceCount уменьшеается на 1 (это выполняет функция MiDecrementReferenceCount). Если счетчик ссылок достиг нуля, сами страницы заносятся в списки StandbyPageList или ModifiedPageList, в зависимости от Pfn->u3.e1.Modified. Страницы из списка StandbyPageList могут быть использованы сразу, как только потребуется - для этого достаточно лишь перевести PTE в состояние Paged-Out. Страницы из списка ModifiedPageList должны быть сперва записаны потоком записи модифицированных страниц на диск, а уж после чего они переводятся в StandbyPageList и могут быть использованы (за выгрузку отвечает функция MiGatherPagefilePages()).
Псевдокод снятия страницы из рабочего набора (сильно обрезанный код MiEliminateWorkingSetEntry и вызываемых из нее функций):
Код:
TempPte = *PointerPte;
PageFrameNumber = PointerPte->u.Hard.PageFrameNumber;
if( Pfn->u3.e1.PrototypePte == 0)
{
//
// Приватная страница, сделать переходной.
//
MI_ZERO_WSINDEX (Pfn); // Pfn->u1.WsIndex = 0;
//
// Следующий макрос делает это:
//
// TempPte.u.Soft.Valid = 0;
// TempPte.u.Soft.Transition = 1;
// TempPte.u.Soft.Prototype = 0;
// TempPte.u.Trans.Protection = PROTECT;
//
MI_MAKE_VALID_PTE_TRANSITION (TempPte,
Pfn->OriginalPte.u.Soft.Protection);
//
// Этот вызов на самом деле заменяет текущий PTE на TempPte и очищает буфера
// ассоциативной трансляции
//
// ( *PointerPte = TempPte );
//
PreviousPte.u.Flush = KeFlushSingleTb(
Wsle[WorkingSetIndex].u1.VirtualAddress,
TRUE,
(Wsle == MmSystemCacheWsle),
&PointerPte->u.Hard,
TempPte.u.Flush);
//
// Декремент счетчика использования. Если он стал равен нулю, страница переводится в переходное состояние
// и уменьшается на единицу счетчик ссылок.
//
// MiDecrementShareCount()
Pfn->u2.ShareCount -= 1;
if( Pfn->u2.ShareCount == 0 )
{
if( Pfn->u3.e1.PrototypePte == 1 )
{
// ... Дополнительная обработка прототипных PTE ...
}
Pfn->u3.e1.PageLocation = TransitionPage;
//
// Уменьшаем на 1 счетчик ссылок. Если он тоже стал равен нулю, перемещаем
// страницу в список модифицированных или простаивающих страниц, либо полностью удаляем
// (помещая в список плохих страниц) в зависимости от MI_IS_PFN_DELETED() и RemovalRequested.
//
// MiDecrementReferenceCount()
Pfn->u3.e2.ReferenceCount -= 1;
if( Pfn->u3.e2.ReferenceCount == 0 )
{
if( MI_IS_PFN_DELETED(Pfn) )
{
// PTE больше не ссылаются на эту страницу. Переместить ее в список свободных либо удалить, если нужно.
MiReleasePageFileSpace (Pfn->OriginalPte);
if( Pfn->u3.e1.RemovalRequested == 1 )
{
// Страница помечена к удалению. Перемещаем ее в список плохих страниц. Она не будет использована,
// пока кто-либо не удалит ее из этого списка.
MiInsertPageInList (MmPageLocationList[BadPageList],
PageFrameNumber);
}
else
{
// Помещаем страницу в список свободных
MiInsertPageInList (MmPageLocationList[FreePageList],
PageFrameNumber);
}
return;
}
if( Pfn->u3.e1.Modified == 1 )
{
// Страница модифицирована. Помещаем в список модифицированных страниц,
// поток записи модифицированных страниц запишет ее на диск.
MiInsertPageList (MmPageLocationList[ModfifiedPageList], PageFrameIndex);
}
else
{
if (Pfn->u3.e1.RemovalRequested == 1)
{
// Удалить страницу, но оставить ее состояние как простаивающее.
Pfn->u3.e1.Location = StandbyPageList;
MiRestoreTransitionPte (PageFrameIndex);
MiInsertPageInList (MmPageLocationList[BadPageList],
PageFrameNumber);
return;
}
// Помещаем страницу в список простаивающих страниц.
if (!MmFrontOfList) {
MiInsertPageInList (MmPageLocationList[StandbyPageList],
PageFrameNumber);
} else {
MiInsertStandbyListAtFront (PageFrameNumber);
}
}
}
}
}
В приложении к статье есть программа с исходными кодами для демонстрации усечения рабочих наборов из пользовательского режима с помощью вызова SetProcessWorkingSetSize(hProcess, -1, -1).
Напротив, когда поток обращается к странице, которая была удалена из рабочего набора, происходит ошибка страницы. К страничным файлам относятся два типа PTE: Transition и Paged-Out. Если страница была удалена из рабочего набора, но еще не была записана на диск или ей вообще не нужно быть записанной на диск и она ЕЩЕ НАХОДИТСЯ в физической памяти (состояние Transition PTE), то вызывается MiResolveTransitionFault() и PTE просто переводится в состояние Valid с соответствующей корректировкой MMPFN и удалением страницы из списка простаивающих или модифицированных страниц. Если страница уже была записана на диск, либо ей не нужно было быть записанной на диск и ее уже использовали для каких-то других целей (состояние Paged-Out PTE), то вызывается MiResolvePageFileFault() и инициируется операция чтения страницы из файла подкачки со снятием соответствующего бита в битовой карте.
Псевдокод разрешения Transition Fault (обрезанный код MiResolveTransitionFault):
Код:
if( Pfn->u4.InPageError )
{
return Pfn->u1.ReadStatus; // #PF на странице, чтение которой не удалось.
}
if (Pfn->u3.e1.ReadInProgress)
{
// Повторная ошибка страницы. Если снова у того же потока,
// то возвращается STATUS_MULTIPLE_FAULT_VIOLATION;
// Если у другого - тогда ожидаем завершения чтения.
}
MiUnlinkPageFromList (Pfn);
Pfn->u3.e2.ReferenceCount += 1;
Pfn->u2.ShareCount += 1;
Pfn->u3.e1.PageLocation = ActiveAndValid;
MI_MAKE_TRANSITION_PTE_VALID (TempPte, PointerPte);
MI_WRITE_VALID_PTR (PointerPte, TempPte);
MiAddPageToWorkingSet (...);
Псевдокод загрузки страницы с диска (обрезанный код MiResolvePageFileFault):
Код:
TempPte = *PointerPte;
// Подготовить параметры для чтения
PageFileNumber = TempPte.u.Soft.PageFileLow;
StartingOffset.QuadPart = TempPte.u.Soft.PageFileHigh << PAGE_SHIFT;
FilePointer = MmPagingFile[PageFileNumber]->File;
// Выбрать свободную страницу
PageColor = (PFN_NUMBER)((MmSystemPageColor++) & MmSecondaryColorMask);
PageFrameIndex = MiRemoveAnyPage( PageColor );
// build MDL...
// Скорректировать ее запись в базе данных страниц
Pfn = MI_PFN_ELEMENT (PageFrameIndex);
Pfn->u1.Event = &Event;
Pfn->PteAddress = PointerPte;
Pfn->OriginalPte = *PointerPte;
Pfn->u3.e2.ReferenceCount += 1;
Pfn->u2.ShareCount = 0;
Pfn->u3.e1.ReadInProgress = 1;
Pfn->u4.InPageError = 0;
if( !MI_IS_PAGE_TABLE_ARRESS(PointerPte) ) Pfn->u3.e1.PrototypePte = 1;
Pfn->u4.PteFrame = MiGetPteAddress(PointerPte)->PageFrameNumber;
// Временно перевести страницу в Transition состояние на время чтения
MI_MAKE_TRANSITION_PTE ( TempPte, ... );
MI_WRITE_INVALID_PTE (PointerPte, TempPte);
// Прочитать страницу.
Status = IoPageRead (FilePointer,
Mdl,
StartingOffset,
&Event,
&IoStatus);
if( Status == STATUS_SUCCESS )
{
MI_MAKE_TRANSITION_PTE_VALID (TempPte, PointerPte);
MI_WRITE_VALID_PTE (PointerPte, TempPte);
MiAddValidPageToWorkingSet (...);
}
Рабочие наборы
Рабочий набор по определению это совокупность резидентных страниц процесса (системы). Существуют три вида рабочих наборов:
- Рабочий набор процесса содержит резидентные страницы, принадлежащие процессу - код, данные процесса и все последующие аллокации пользовательского режима. Хранится в EPROCESS::Vm
- Рабочий набор системы содержит резидентные подкачиваемые страницы системы. В него входят страницы подкачиваемого кода, данных ядра и драйверов устройств, системного кеша и пула подкачиваемой памяти. Указатель на него хранится в переменной ядра MmSystemCacheWs.
- Рабочий набор сеанса содержит резидентные страницы сеанса, например, графической подсистемы Windows (win32k.sys). Указатель хранится в MmSessionSpace->Vm.
Когда системе нужны свободные страницы, инициируется операция усечения рабочих наборов - страницы отправляются в списки Standby или Modified, в зависимости от того, была ли запись в них, а PTE переводятся в состояние Transition. Когда страницы окончательно отбираются, то PTE переводятся в состояние Paged-Out (если это были страницы, выгружаемые в файл подкачки) или в Invalid, если это были страницы проецируемого файла.
Когда процесс обращается к странице, то страница либо удаляется из списков Standy/Modified и становится ActiveAndValid, либо инициируется операция загрузки страницы с диска, если она была окончательно выгружена. Если памяти достаточно, процессу позволяется расширить свой рабочий набор и даже превысить максимум для загрузки страницы, иначе для загрузки страницы выгружается какая-то другая, то есть новая страница замещает старую.
Имеется системный поток управления рабочими наборами или т.н. диспетчер баланса. Он ожидает на двух объектах KEVENT, первое из которых срабатывает по таймеру раз в секунду, а второе срабатывает, когда нужно изменить рабочие наборы. Диспетчер настройки баланса так же проверяет ассоциативные списки, регулируя из глубину для оптимальной производительности.
V. Ядерные функции управления памятью
В этой части речь пойдет о некоторых полезных функциях управления памятью в режиме ядра.
Слои функции управления памятью ядра можно разделить следующим образом от низшего уровня к высшему:
- макросы MI_WRITE_VALID_PTE/MI_WRITE_INVALID_PTE
- низкоуровневые функции MiResolve..Fault, MiDeletePte и другие функции работы с PDE/PTE, а так же функции работы с MMPFN и списками страниц - MiRemovePageByColor, MiRemoveAnyPage, MiRemoveZeroPage.
- функции, предоставляемые драйверам для работы с физической памятью: MmAllocatePagesForMdl, MmFreePagesFromMdl, MmAllocateContiguousMemory.
- функции, предоставляемые драйверам для работы с пулом: ExAllocatePoolWith..., ExFreePoolWith..., MmAllocateContiguousMemory (относится и к предыдущему слою и к этому)
Для пользовательской памяти дело обстоит немного по-другому:
- макросы MI_WRITE_VALID_PTE/MI_WRITE_INVALID_PTE
- функции работы с VAD и пользовательской памятью - MiAllocateVad, MiCheckForConflictingVad, и др.
- функции работы с виртуальной памятью - NtAllocateVirtualMemory, NtFreeVirtualMemory, NtProtectVirtualMemory.
Описывать начнем их от низшего уровня к высшему, сначала для управления памятью ядра, затем пользовательской памятью.
Управление памятью режима ядра
1. макросы MI_WRITE_VALID_PTE/MI_WRITE_INVALID_PTE
Эти макросы используются во всех функциях, которые как-либо затрагивают выделение или освобождение физической (в конечном итоге) памяти. Соответственно, они записывают действительный и недействительный PTE в таблицу страниц процесса.