* Под виртуальной памятью могут понимать одну или обе из двух совершенно независимых вещей:
- виртуальное адресное пространство: каждый процесс получает собственное, изолированное, непрерывное пространство адресов памяти;
- подкачка памяти (своппинг): неиспользуемые данные могут автоматически выгружаться на диск для освобождения физической памяти, и при необходимости -- вновь загружаться с диска в физическую память.
- в Windows 3.11 (и даже раньше) был своппинг, но не было виртуального АП;
- в Windows XP (и даже раньше) можно отключить своппинг: виртуальное АП каждого процесса от этого никуда не денется.
По чистому совпадению, 4Гб -- это ещё и размер ФП, поддерживаемой Windows без PAE. Совпадение заключается в том, что указатели, используемые ядром Windows для управления ФП, также 32-битные. (Включение PAE их не расширяет; вместо этого, используется дополнительный уровень преобразования адресов.) Если режимы 3GB и PAE включены одновременно, то машина может даже не загрузиться: 3GB вдвое сокращает размер АП ядра, а PAE многократно увеличивает размер таблиц, которые должны им храниться для управления памятью. При этом в АП ядра проецируется видеобуфер (256Мб видеопамяти съедает четверть всего АП ядра), и в оставшееся место должны поместиться все драйверы. Поэтому, когда оба эти режима включены одновременно, доступная физическая память ограничивается до 16Гб: на таблицы для поддержки большей ФП не хватает места.
Из-за этого совпадения распространился миф, что Windows не может выделить больше 4ГБ памяти всем программам в сумме. На самом деле, даже без PAE наличие своппинга позволяет выделить всем процессам в сумме 64ГБ памяти: Windows позволяет иметь до 16 файлов подкачки, размером до 4ГБ каждый. С PAE это ограничение ещё расширяется до 256ТБ (16x16ТБ).
* Для упрощения работы с виртуальной памятью применяется её страничная организация: вся память дробится на равные страницы (в Windows -- по 4Кб каждая), и к странице целиком применяются одинаковые параметры. Страница виртуального АП может находиться в одном из трёх состояний: свободна, зарезервирована, занята. Разница между свободной и зарезервированной страницей только в том, что зарезервированная страница не может быть возвращена функциями выделения памяти; поэтому динамически растущие непрерывные структуры (например, стеки потоков) при своём создании резервируются целиком. Это гарантирует, что пустая часть зарезервированного под стек объёма останется пустой к тому моменту, когда её потребуется занять расширившимся стеком.
Занятая страница может быть одного из двух видов: file-backed (образ файла: загруженный исполнимый модуль, либо явно созданный файл-маппинг) либо swap-backed; и независимо от своего вида, занятая страница может быть в одном из двух положений: загружена (т.е. соответствует странице ФП) либо выгружена. Выгруженные file-backed страницы нигде не хранятся; при необходимости, они вновь загружаются из того же файла, из которого были созданы. Выгруженные swap-backed страницы хранятся в файле подкачки (win386.swp на Win9x, pagefile.sys на WinNT). К слову: своппинг впервые появился именно в Windows/386, т.к. процессор 386 впервые поддерживал страничную организацию памяти. Виртуальные АП были возможны начиная с 286, но в Windows были задействованы только начиная с WinNT 3.1. Выгрузка неиспользуемых ресурсов из памяти и подгрузка их из ехе-файла на диске, равно как и объединение в памяти секций кода нескольких экземпляров одной программы, существовало начиная с Windows 1.0.
Важно, что swap-backed страницы добавляются в файл подкачки на диске не в момент своей выгрузки, а в момент создания -- чтобы к моменту выгрузки была уверенность, что страницу есть куда выгружать. Поэтому файл подкачки такой огромный: даже если во всей системе нет ни одной выгруженной swap-backed страницы, для всех них там зарезервировано место "на случай внезапного запуска 15 экземпляров Фотошопа."
Как уже было сказано, вся память в защищённом режиме 386 поделена на страницы. Часть памяти ядра является невыгружаемой: она всегда соответствует страницам ФП, и под неё не резервируется место в файле подкачки. (Например, именно в невыгружаемой памяти размещается часть ядра, обеспечивающая подкачку: если бы её можно было выгрузить, то некому бы было загрузить её обратно.) Эта невыгружаемая память в документации называется "нестраничной" (nonpaged), по-видимому -- как производное от глаголов "page in" и "page out" (загружать/выгружать страницу). "Нестраничная" память, так же как и вся остальная, состоит из страниц; при желании, можно откопать даже словосочетание "nonpaged page" ("нестраничная страница"). Либо я глубоко заблуждаюсь, либо авторы документации сами не читали, что пишут.
Изменить тип страницы можно только в одном направлении -- из file-backed в swap-backed (это делается вызовом VirtualProtect). После этого изменения страница начинает занимать место в файле подкачки. Если приложение самомодифицирует свой код (например, если оно запаковано UPX или подобным упаковщиком), то оно при запросе доступа на запись в свою секцию кода переводит её страницы в тип swap-backed, и в итоге это приложение занимает больше места на диске (потому что в файле подкачки теперь хранится его полный распакованный образ). Если такое сжатое приложение запустить второй раз, то и в памяти, и в файле подкачки создастся ещё одна копия распакованного образа -- тогда как без упаковщика в новый процесс были бы спроецированы те же страницы ФП, в которые был прежде загружен код первого экземпляра. Итак, упакованные приложения занимают больше места, чем распакованные -- и на диске, и в памяти. Ума не приложу, зачем и кому пришло в голову заниматься подобной дурью.
Последнее в этой области -- понятие "working set". Это набор страниц АП процесса, которые заняты и находятся в загруженном положении (независимо от их типа). Именно этот объём отображается в столбце "Mem Usage" вкладки "Processes" Диспетчера задач. Это число может меняться, даже если процесс ничего не делает: система сама решает, когда загружать и выгружать страницы его памяти. А график "Mem Usage" соседней вкладки "Performance" отражает уже другой объём -- объём всех swap-backed страниц (независимо от их положения). (В последних версиях Windows его, наконец, переименовали в "Page File Usage".) Этот график показывает выделение и освобождение процессами виртуальной памяти и никак не связан с загрузкой ФП. Нет ничего удивительного, что сумма всех чисел на одной вкладке не будет равна числу на другой: эти числа отражают совсем разные вещи.
* У страниц ВП, кроме ассоциации с некоторым хранилищем (ФП и/или файл), есть ещё режим доступа. Кроме стандартных прав (чтение, запись, выполнение), поддерживается ещё один необычный флаг -- PAGE_GUARD. Страница с этим флагом "заминирована": она работает как обычная занятая страница, но при первой попытке доступа к ней происходит исключение STATUS_GUARD_PAGE_VIOLATION, и флаг снимается. Может быть, для этого флага и можно придумать кучу разнообразных применений, но я знаю только одно -- обработка переполнения стека.
Саморасширяющийся стек работает, в общем, следующим образом: изначально резервируется указанный в заголовке приложения объём (по умолчанию -- 1Мб), его верхняя часть (начало стека) коммитится, и сразу под закоммиченной частью создаётся "заминированная страница". Обработчик исключения от этой страницы, соответственно, коммитит стек дальше вниз, создавая под вновь закоммиченной частью новую ЗС. Когда вся зарезервированная часть стека закоммичена, дальше расти он уже не может: новая ЗС не создаётся, и попытка доступа за пределы стека приводит к обычной ошибке защиты памяти "memory cannot be 'written'".
На самом деле, всё чуть хитрее. Что, если во время расширения стека не удастся выделить память -- например, если кончилось место для файла подкачки? Ведь наш стек переполнен -- значит, мы больше не можем сделать ни одного вызова; не можем даже показать месседжбокс с надписью "приехали" и грустным смайликом. Именно заэтим и используются ЗС, а не перехватывается ошибка защиты в границах зарезервированной части стека -- ведь после срабатывания эта страница "разминируется", и у нас для обработчика исключения EXCEPTION_STACK_OVERFLOW остаётся ещё целая страница стека. (Этот размер можно ещё дальше увеличить при помощи функции SetThreadStackGuarantee.) Если обработчик этого исключения "починил" стек так, что выполнение программы можно продолжать, то он должен вновь заминировать нижнюю страницу стека -- иначе второе переполнение стека приведёт к ошибке защиты. Всё это применимо не только к случаю нехватки памяти при расширении стека, но и при его расширении за нижнюю границу -- когда срабатывает ЗС на нижнем краю зарезервированной части стека, и система не может увеличить стек дальше, она точно так же генерирует EXCEPTION_STACK_OVERFLOW, обработчик которого точно так же должен вновь заминировать эту страницу, если он хочет продолжать выполнение программы.
Ну и, раз уже зашла речь о зарезервированной под стек памяти: как выбирать её размер, и что от этого зависит? Как я уже писал, под стеки всех потоков процесса резервируется как минимум объём, указанный в заголовке приложения. Если там оставлено значение по умолчанию (1Мб), то удастся создать лишь чуть менее 2000 потоков -- вся память окажется зарезервированной под их стеки. Значит, если планируется создание нескольких тысяч потоков, то размер зарезервированной части стека нужно уменьшить. Никаких других негативных эффектов от слишком большого объёма памяти, зарезервированной под стек, нет -- только преждевременное исчерпание АП процесса. С другой стороны, если под стек зарезервировано слишком мало памяти, то больше этого объёма он вырасти уже никак не может. И наконец, от размера изначально закоммиченной части стека зависит время запуска приложения (т.к. под неё выделяется место в памяти и файле подкачки). Если изначально под стек закоммитить слишком много памяти, она будет израсходована впустую; если слишком мало, то каждое расширение стека (с переносом ЗС вниз) будет занимать больше времени, чем изначальный коммит сразу нужного объёма.