“Недостаточно Памяти” не относится к физической памяти
Я начал программировать на x86 машинах во время периода значительных и быстрых изменений в стратегиях управления памятью, применяемых процессорами Intel. Мучения от необходимости понимать различия между «расширенной памятью» и «дополнительной памятью» с годами, к счастью, утихли, вместе с точными знаниями об этих различиях.
Вследствие этого жизненного опыта, я иногда удивляюсь тому факту, что многие профессиональные программисты, похоже, имеют представления об управлении памятью, которые перестали соответствовать истине со времён «защищенного режима 80286».
Например, меня порой спрашивают «я получил ошибку ‘недостаточно памяти’, но я проверил, что в машине много ОЗУ, в чём тут дело?»
Представьте – полагать, что количество установленной в машине памяти имеет значение, когда она заканчивается! Как мило! :-)
Проблема с большинством подходов к описанию современного управления виртуальной памятью, я полагаю, в том, что они начинают с предположений из мира DOS – что «память» - это ОЗУ, или «физическая память», и что «виртуальная память» - это просто ловкий трюк, чтобы заставить физическую память казаться больше. Хотя исторически виртуальная память в Windows развивалась именно так, и это обоснованный подход, это не то, как лично я концептуализирую управление памятью.
Итак, вот быстрый набросок моей несколько странной концептуализации виртуальной памяти. Но сначала предостережение. Современная система управления памятью в Windows значительно сложнее и интереснее, чем этот краткий эскиз, который предназначен для объяснения систем управления виртуальной памятью в общем духе и некоторых ментальных инструментов для прояснения мышления насчёт взаимоотношений между памятью и адресацией. Ни в коей мере это не справочник по настоящему менеджеру памяти. (Дополнительные подробности о том, как он на самом деле работает, поищите в этой статье MSDN)
Я собираюсь начать с предположения о том, что вы понимаете две концепции, которые не нуждаются в дополнительных пояснениях: операционная система управляет процессами, и операционная система управляет файлами на диске.
Каждый процесс может получить столько памяти, сколько захочет. Он просит операционную систему выделить ему некоторое количество памяти, и операционная система так и делает.
Тут, я уверен, мифы и предубеждения уже начали бурлить. Конечно же процесс не может запросить «столько, сколько хочет». Конечно же 32хразрядный процесс может запросить максимум 2 Гб. Или, конечно же 32хразрядный процесс может запросить столько памяти, сколько там есть ОЗУ. Ни одно из этих предположений не является верным. Количество памяти зарезервированной процессом ограничено только объемом пространства, которое операционная система может получить на диске. (*)
Это ключевой момент: та память, которую мы называем «памятью процесса», по моему мнению лучше всего представляется как большой файл на диске.
Так, представьте, что 32хразрядному процессу требуются огромные количества памяти, и он запрашивает её много раз. Пускай ему нужно в сумме 5 Гб памяти. Операционная система находит достаточно места на диске для файлов на 5 Гб и говорит процессу что да, конечно, память доступна. Как тогда процесс пишет в эту память? У процесса есть только 32хразрядные указатели, но для однозначной идентификации каждого байта в памяти размером в 5 Гб потребовалось бы как минимум 33 бита.
С решения этой проблемы и начинаются некоторые сложности.
5 Гб памяти режут на кусочки, обычно по 4 Кб, которые называют «страницами». Операционная система даёт процессу 4 Гб «виртуального адресного пространства» - более миллиона страниц, которые могут быть адресованы 32хразрядным указателем. Затем процесс говорит операционной системе, какие из 5 Гб дискового пространства должны быть «отображены» в 32хразрядное адресное пространство. (Как? Вот страничка, где Рэймонд Чен приводит пример того, как выделить кусочек в 4 Гб и отобразить его порцию.)
После того, как отображение произведено, операционная система знает, что когда процесс №98 пытается обратиться по указателю 0x12340000 в своём адресном пространства, то это соответствует, скажем, байту в начале страницы №2477, и операционная система знает, где эта страница хранится на диске. Когда происходит чтение или запись по этому указателю, операционная система может выяснить, к какому байту дискового пространства это относится, и выполнить соответствующую операцию чтения или записи.
Ошибка «out of memory» почти никогда не происходит из-за того, что не хватает доступной памяти; как мы видели, память – это дисковое пространство, а диски в наше время огромны. Скорее, «недостаточно памяти» происходит из-за того, что процесс не может найти достаточно большую непрерывную секцию неиспользованных страниц в своём адресном пространстве для выполнения запрошенного отображения.
Половина (или, в некоторых случаях, четверть) четырёхгигабайтного адресного пространства резервируется операционной системой для хранения её данных, специфичных для процесса. Из остающейся «пользовательской» половины адресного пространства заметные количества потребляются файлами EXE и DLL, из которых состоит код приложения. Даже если в сумме места хватает, может не оказаться достаточно большой для нужд процесса неотображённой «дыры» в адресном пространстве.
Процесс может справиться с этой ситуацией, попытавшись определить порции виртуального адресного пространства, которые больше не нужно отображать, «деотобразить» их, а потом отобразить в некоторые другие страницы на диске. Если 32хразрядный процесс спроектирован для обработки многогигабайтных массивов данных, то, очевидно, это то, чем он должен заниматься. Обычно такие программы занимаются обработкой видео или чем-то вроде того, и могут легко и безопасно переотображать большие фрагменты адресного пространства в различные части «файла памяти».
Но что, если нет? Что, если процесс – скорее обычная, благонравная программа, которая хочет еще всего лишь несколько сот миллионов байт памяти? Если такой процесс просто тикает себе, а потом пробует выделить большой массив, операционная система скорее всего сможет предоставить дисковое пространство. Но как процесс станет отображать страницы этого массива в адресное пространство?
Если там случайно не хватает непрерывного адресного пространства, то процесс не сможет получить указатель на эти данные, и они на практике бесполезны. В этом случае процесс выбрасывает ошибку с уже неверным в наши дни текстом «недостаточно памяти». Это должна бы быть ошибка «не удаётся найти достаточно непрерывного адресного пространства»; там много памяти, потому что память эквивалентна дисковому пространству.
Я всё еще не упомянул ОЗУ. ОЗУ можно рассматривать как всего лишь оптимизацию производительности. Доступ к данным в памяти, где информация хранится в крошечных, лёгких электронах, движущихся со скоростью, близкой к световой, значительно быстрее, чем доступ к данным на диске, где информация хранится в огромных, тяжелых молекулах оксида железа, движущихся со скоростью, близкой к скорости моей Миаты.
Операционная система следит за тем, обращения к каким страницам каких процессов происходят чаще всего, и делает их копию в ОЗУ, чтобы увеличить скорость. Когда процесс обращается по указателю, относящемуся к странице, которая еще не закэширована в ОЗУ, операционная система выполняет «page fault», идет на диск и копирует страницу с диска в ОЗУ, разумно предполагая, что скоро к странице обратятся снова.
Операционная система такеже весьма сообразительна насчёт разделения неизменяемых ресурсов. Если два процесса загружают одну и ту же страницу кода из одной и той же DLL, то операционная система может разделять кэш в ОЗУ между обоими процессами. Поскольку код, предположительно, не будет изменяться ни одним из процессов, экономия дубликата страницы в ОЗУ путем её разделения вполне обоснована.
Но даже с умным разделением, рано или поздно у этой системы кэширования заканчивается ОЗУ. Когда это происходит, операционная система пытается угадать, какие страницы наименее вероятно вскоре потребуются, записывает их на диск, если они изменились, и освобождает их ОЗУ, чтобы прочитать в него то, что более вероятно потребуется в ближайшее время.
Когда операционная система угадывает неверно, или, более вероятно, когда просто не хватает ОЗУ для хранения всех частоиспользуемых страниц во всех исполняемых процессах, тогда машина начинает «тормозить». Операционная система проводит всё своё время в дорогостоящих записях и чтениях, диск непрерывно шуршит, а вы не можете делать ничего полезного.
Это также означает, что «нехватка ОЗУ» редко (**) приводит к ошибке «недостаточно памяти». Вместо ошибки это приводит к плохой производительности, потому что цена того факта, что память на самом деле хранится на диске, внезапно оказывается заметной.
Другой способ смотреть на это – в том, что полное количество виртуальной памяти, потребляемой вашей программой на самом деле не слишком соотносится с её производительностью. Важно не общее количество потребляемой памяти, а, скорее (1) сколько из этой памяти не разделяется с другими процессами, (2) насколько велик «рабочий набор» частоиспользуемых страниц, и (3) больше ли сумма рабочих наборов всех активных процессов, чем доступное ОЗУ.
Теперь должно быть уже понятно, почему ошибки «недостаточно памяти» обычно не имеют никакого отношения к тому, сколько у вас физической памяти, или даже сколько у вас памяти доступно. Почти всегда дело в адресном пространстве, которое относительно невелико в 32хбитной Windows, и легко фрагментируется.
И, конечно, многие из этих проблем фактически исчезают на 64хбитной Windows, где адресное пространство в миллиарды раз больше, и, следовательно, его значительно труднее фрагментировать. (Проблема с тормозами, конечно же, всё еще возникает, если физическая память меньше полного рабочего набора – неважно, насколько велико адресное пространство).
Этот способ концептуализации виртуальной памяти полностью противоположен тому, как её обычно представляют. Обычно её представляют так, что память – это кусок физической памяти, и что содержимое физической памяти вытесняется на диск, когда физическая память чрезмерно заполняется. Но я предпочитаю думать о памяти как о куске дискового пространства, а о физической памяти – как об умном механизме кэширования, который заставляет диск выглядеть быстрее. Возможно, я псих, но это помогает мне лучше её понять.
*************
(*) Хорошо, я соврал. 32хбитная Windows ограничивает общее количество памяти процесса на диске 16ю терабайтами, а в 64хбитной лимит равен 256 Tб. Но нет никакой причины, по которой отдельный процесс не мог бы выделить несколько Гб, если хватает места на диске.
(**) В некоторых системах виртуальной памяти можно пометить страницу флагом «производительность этой страницы так важна, что она должна всегда оставаться в ОЗУ». Если таких страниц больше, чем страниц ОЗУ, то вы можете получить ошибку «недостаточно памяти» от нехватки ОЗУ. Но это значительно более редкое событие, чем исчерпание адресного пространства.
Comments
- Anonymous
July 02, 2012
Интересно, спасибо!