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

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

Модератор: tyomitch

tyomitch
Пользователь #1352
Пользователь #1352
Аватара пользователя
 
Сообщения: 12822
Зарегистрирован: 20.10.2002 (Вс) 17:02
Откуда: חיפה

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

Сообщение tyomitch » 07.10.2006 (Сб) 16:06

Решил затронуть эту тему в связи с большим числом заблуждений и недопониманий на эту тему (в т.ч. и здесь на форуме).

* Под виртуальной памятью могут понимать одну или обе из двух совершенно независимых вещей:
  • виртуальное адресное пространство: каждый процесс получает собственное, изолированное, непрерывное пространство адресов памяти;
  • подкачка памяти (своппинг): неиспользуемые данные могут автоматически выгружаться на диск для освобождения физической памяти, и при необходимости -- вновь загружаться с диска в физическую память.
По историческим причинам, в Windows словосочетание "виртуальная память" используется для обозначения обоих этих понятий, поэтому и возникают вопросы в духе "останется ли ВП в Windows виртуальной, если на машине установлено 4Гб ФП". И тем не менее, непосредственной связи между ними нет:
  • в Windows 3.11 (и даже раньше) был своппинг, но не было виртуального АП;
  • в Windows XP (и даже раньше) можно отключить своппинг: виртуальное АП каждого процесса от этого никуда не денется.
4Гб -- это размер АП каждого процесса. Один процесс не может без хитрых трюков обращаться более чем к 4Гб памяти. (Хитрый трюк, позволяющий это: создать огромный файл-маппинг и отображать в АП его отдельные куски.) Это ограничение взято не с потолка: это всё пространство, которое можно адресовать 32-битными указателями. При этом младшие 2Гб, по традиции, отдаются приложению под его код и данные, а старшие 2Гб ("отрицательные" адреса) заняты ядром системы. (Ядро, точно так же как и приложения, имеет своё виртуальное АП.) Адресам ядра соответствуют одна и та же физическая память для всех процессов; таким образом, размер виртуального АП для каждого процесса -- 2Гб. Его можно расширить до 3Гб при помощи опции /3GB: тогда часть адресов, выделяемых приложению, будут "отрицательными" (см. тж. серию статей от Чена про эту опцию). Чтобы указать, что адресная арифметика в приложении не сломается от работы с "отрицательными" адресами, нужно установить определённый флаг в заголовке приложения: только тогда ему будет выделено расширенное АП. Даже если нет ни одного приложения, которому нужно 3Гб АП, ядро всё равно стискивается до 1Гб на случай, если такие приложения вдруг появятся. Поэтому включать этот режим без особых причин не стоит.

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

BV
Thinker
Thinker
Аватара пользователя
 
Сообщения: 3987
Зарегистрирован: 12.09.2004 (Вс) 0:55
Откуда: Молдавия, г. Кишинёв

Сообщение BV » 07.10.2006 (Сб) 17:19

Отличная статья :)
const char *out = "|*0>78-,+<|"; size_t cc = char_traits<char>::length(out);
for (size_t i=0;i<cc;i++){cout<<static_cast<char>((out[i]^89));}cout<<endl;

PAV
Начинающий
Начинающий
 
Сообщения: 1
Зарегистрирован: 22.12.2006 (Пт) 21:17

Сообщение PAV » 22.12.2006 (Пт) 21:27

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

tyomitch
Пользователь #1352
Пользователь #1352
Аватара пользователя
 
Сообщения: 12822
Зарегистрирован: 20.10.2002 (Вс) 17:02
Откуда: חיפה

Сообщение tyomitch » 23.12.2006 (Сб) 9:23

Про источники не знаю.

А когда файл подкачки отключен, работа происходит просто: вся выделяемая память остаётся "загруженной" в физическую память до тех пор, пока не будет освобождена. В частности, если запущены, свёрнуты и давно не используются несколько копий пресловутого Фотошопа, больше ничего сделать с системой уже не удастся.
Изображение

Ennor
Конструктивный критик
Конструктивный критик
 
Сообщения: 2504
Зарегистрирован: 18.12.2001 (Вт) 3:58
Откуда: Калуга -> Москва

Сообщение Ennor » 23.12.2006 (Сб) 15:41

tyomitch писал(а):если запущены, свёрнуты и давно не используются несколько копий пресловутого Фотошопа, больше ничего сделать с системой уже не удастся.

Проверял? Фотошоп использует свой собственный механизм свопа, независимый от виндового.

Еще, насколько я помню, для работы /3GB требуется как минимум Advanced Server.

FaKk2
El rebelde gur&#250;
El rebelde gur&#250;
Аватара пользователя
 
Сообщения: 2031
Зарегистрирован: 09.03.2003 (Вс) 22:10
Откуда: Los Angeles

Сообщение FaKk2 » 23.12.2006 (Сб) 20:50

Ennor
Фотошоп наверно не самый удачный пример. А вот если на одном гигабайте оперативке запустить Quake 4 без свопа, система закричит что не хватает оперативной памяти.

_ae_
Продвинутый пользователь
Продвинутый пользователь
 
Сообщения: 165
Зарегистрирован: 08.10.2006 (Вс) 14:37

Сообщение _ae_ » 23.12.2006 (Сб) 21:32

Ennor
Проверял? Фотошоп использует свой собственный механизм свопа, независимый от виндового.
Неужели и правда не понятно что имел ввиду автор?

tyomitch
Пользователь #1352
Пользователь #1352
Аватара пользователя
 
Сообщения: 12822
Зарегистрирован: 20.10.2002 (Вс) 17:02
Откуда: חיפה

Сообщение tyomitch » 23.12.2006 (Сб) 22:22

Ennor писал(а):
tyomitch писал(а):если запущены, свёрнуты и давно не используются несколько копий пресловутого Фотошопа, больше ничего сделать с системой уже не удастся.

Проверял? Фотошоп использует свой собственный механизм свопа, независимый от виндового.

Даже и не думал проверять. Это была цитата из поста Остермана, причём закавыченная. Ссылка на источник -- в исходном посте.

Ennor писал(а):Еще, насколько я помню, для работы /3GB требуется как минимум Advanced Server.

В MSDN, аккурат по приведённой мной ссылке, перечислены поддерживаемые системы. Скопипастить сюда специально для лентяев, что ли? В любом случае, WinXP Pro в этом списке есть.
Изображение

Ennor
Конструктивный критик
Конструктивный критик
 
Сообщения: 2504
Зарегистрирован: 18.12.2001 (Вт) 3:58
Откуда: Калуга -> Москва

Сообщение Ennor » 24.12.2006 (Вс) 0:37

Вижу, да; перепутал с серверной платформой, пардон.

Twister
Теоретик
Теоретик
Аватара пользователя
 
Сообщения: 2251
Зарегистрирован: 28.06.2005 (Вт) 12:32
Откуда: Алматы

Сообщение Twister » 25.12.2007 (Вт) 10:49

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

и куча статей (http://bbs.vbstreets.ru/viewtopic.php?t=28535)
подтверждала мои мысли. оказалось НИ ХРЕНА!
EXCEPTION_STACK_OVERFLOW выскивает при первом обращении
к _последней_ странице стека. но только один раз. после чего
ее можно использовать по своему усмотрению... с учетом того,
что стек растет вверх, мы имеем почти страницу стека, но...
может и не страницу. это смотря что за код будет. скажем, если
PUSH EBP/SUB ESP, 1FF0h/PUSH ESI и если PUSH EBP попадет
на предпоследнюю страницу стека, то после уменьшения ESP
оставшегося стекового пространства не хватает для обработки
исключения, сгенерированного PUSH ESI и процесс тихо умирает.

а вот при доступе за границы стеке генериться EXCEPTION_ACCESS_VIOLATION

Здесь оригинальное сообщение.
А я все практикую лечение травами...

tyomitch
Пользователь #1352
Пользователь #1352
Аватара пользователя
 
Сообщения: 12822
Зарегистрирован: 20.10.2002 (Вс) 17:02
Откуда: חיפה

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

Сообщение tyomitch » 19.09.2008 (Пт) 19:03

(Машу кулаками после драки)

Предполагалось, что во фразе "когда срабатывает ЗС на нижнем краю зарезервированной части стека, и система не может увеличить стек дальше, она точно так же генерирует EXCEPTION_STACK_OVERFLOW" слова "на нижнем краю зарезервированной части стека" указывают именно на последнюю страницу стека. А вот где в этом тексте можно было вычитать намёк на предпоследнюю страницу, я не знаю.

Но то, что Крис Касперски сослался на мой текст -- это уже такое достижение, что будет чем похвастаться потомкам :-)
Изображение


Вернуться в Tyomitch

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 8

    TopList  
cron