Библиотека Интернет Индустрии I2R.ru |
|||
|
Управление памятью. Часть 1Сервер ресурсов памяти.
Несколько слов: Я начинаю очень долгий цикл статей, посвящённых одному из самых больных вопросов в программировании. Правда, несмотря на всю остроту вопроса, и до сегодняшнего момента лишь немногие крупные фирмы уделяют ему лишь сколь нибудь внимания со своей стороны. Не вдаваясь в подробности API операционных систем, будут рассмотрены все базовые случаи и пути их решения. Желаю удачи!!! Общая классификация памяти Память классифицируется по следующим признакам.
Теперь будут даны теоретические определения каждого из этих видов. Советую вам сразу пропустить их и перейти к следующему пункту. Вы сможете по ходу текста возвращаться к этим определениям, чтобы уточнить какой-либо момент.
Особенности систем виртуальной памяти Всё ниже написанное вероятно всего будет справедливо для многих операционных систем, однако речь пойдёт конкретно об особенностях управления памятью в Win32. Вы никогда не ошибетесь, если разделите всю память Windows на размещённую физически в оперативном блоке, и находящуюся на диске. И всё это вместе удобно обозвать виртуальной памятью. Другое деление состоит в том, откуда отображается память: из файла подкачки, или просто из файла. Оказывается, это будет иметь существенную разницу. Всё громадное API Windows по управлению памяти можно легко поделить на пять логических групп:
Что, где лучше? Это уже гамлетовский вопрос. Но никогда не стоит забывать, что вся память процесса Windows - виртуальная (за исключением некоторой части ядра ОС, но об этом можно забыть). Выделение памяти в кучи - это тот сервис, которому не стоит особенно доверять. Реализация этих функций основана на тех же VirtualXXX, и включает в себя решение всех проблем, которых, оказывается, не мало. Конечно, программистов привлекает готовый набор API. Но проигрыш в серьёзных проектах не стоит того выигрыша от экономии времени при использовании функций памяти кучи процесса. Именно рассказу о том, как следует реализовывать подобную память самостоятельно, и будут посвящаться последующие части. Но в этой статье настал момент очертить основной круг задач, который предстаёт перед программистом при решении вопросов управления памятью. Windows совместно с системой страничной памяти, даёт в руки разработчикам возможность управлять доступам к различным областям памяти - страницам, и что самое важное описывать адресное пространство виртуально, а физически размещать страницы, их содержимое как удобно, и где удобно. Абстрагируя систему станичной адресации, Windows определяет набор флагов защиты страницы. Многие из этих флагов не участвуют в описании доступа к странице, а обеспечивают выполнения некоторых механизмов ОС. К таким флагам относятся, например: PAGE_WRITECOPY, PAGE_GUARD. В действительности это никакие не флаги доступа. PAGE_WRITECOPY - это по сути PAGE_READONLY на физическом уровне, а PAGE_GUARD - PAGE_NOACCESS. Вся разница кроется только в различии работы ОС с такими страницами. В частности нас будет интересовать флаг PAGE_GUARD, который позволяет организовывать постепенное выделение памяти по мере её использования. Одной из проблем при работе в Win32 является требование к гранулярности адреса памяти 64k - Microsoft пишет, что данная величина зависит от типа процессора, хотя это не совсем так. Таким образом, если вы желаете выделить несколько блоков виртуальной памяти размером менее 64k, то получите некоторое незаполненное пространство. А поэтому несколько важных принципов, которых желательно придерживаться при работе с памятью в Win32:
Анализируя вышеприведенные принципы, легко прийти к выводу, что чтобы использовать память в приложении наиболее эффективно следует взять полный контроль над её выделением и созданием в <одни руки>. Понятие о нехватке памяти И как нестранно в статье автор поднимает этот вопрос. Говоря о нехватки памяти, многие Win32 программисты откровенно не беспокоятся, да и вероятно, они ещё не разу не видели, чтобы можно было бы исчерпать лимит Win32 User'ского адресного пространства в 2Гб. Для начала это не так уж сложно. Наоборот, 2Гб это довольно мало, если, например, предполагается одновременная работа с несколькими форматно сложными документами. Хорошая доля пространства тратиться на USER объекты, которых немало, ещё часть на COM, если вы это используйте, и т.д., и т.д. Теоретически, например, чтобы иметь высокую скорость выполнения какого-нибудь алгоритма, следует убрать всякие ограничения с выделения памяти. Хорошим примером могут послужить словари, или базы данных. Они будут работать эффективно, только в том случае, если вы полностью заберёте весь ресурс памяти (физической, виртуальной:.) на их данные. Но в действительности об этом следует беспокоиться не всегда, или в последнюю очередь, но не забывать. Конечно, в недалёком будущем приход 64bits систем сведёт всю эту проблему на нет. Поэтому и не в ней дело. Нехватка физической памяти. Нехватка физической памяти, чем она опасна? Многим. 1. Машина резко снижает скорость выполнения. Вообще говоря, перед нами очень тонкий момент. Да, снижается общая производительность системы, но отчего? Как известно, это происходит в том случае, если система постоянно вынуждена списывать одни страницы на диск, а закачивать другие. Ведь причиной падения производительности не есть сама нехватка физической памяти, это только некое выражение, которое совсем не показывает сути. Если вы открыли несколько больших программ, то совсем не почувствуйте нехватки оперативной памяти, когда будете работать только с одним из них, и изредка переключаться на другие. Но вот если начнёте нажимать кнопки по всем окнам, то вашей системе очень скоро станет плохо. Впрочем, есть и другой путь. Откройте, сколько позволит система, окон IE с каким-нибудь не маленьким по объёму рисунком. Что получиться? Миф о неисчерпаемой памяти исчезнет, и вы увидите, как беззащитна ваша операционная система. А ведь это вовсе не хакерская атака. Меня давно убедили эти эксперименты, и поэтому автор разработал принцип <Трёх режимов>, о котором позже. Важно то, что эффект нехватки физической памяти наблюдается тогда, когда код обращается к недоступным страницам. Из этого читатель должен сделать важный вывод - если код вашего приложения оперирует большими объёмами памяти, он не должен <метаться> от адреса к адресу, если не хочет испортить нервы пользователю, и жизнь операционной системе. Его обращения, по возможности, следует делать более меткими и последовательными. Выполнение этого условия большей частью относится к другой большой теме, теме <редактирования>, однако часть успеха зависит так же и от мудрого управления ресурсом памяти. 2. Windows полна неприятными особенностями работы. Одна из таких особенностей, например такая. Ваше приложение хочет, предположим, завершиться, и вызывает функцию CloseHandle. Обычно данный код располагают в главном потоке, где идёт выборка GetMessage. На первый взгляд всё невинно, ну что здесь неучтено. Но вот, предположим, у системы сейчас нет времени на выполнения данной операции, так как в данный момент на диск происходит запись, или есть другая причина, зависящая от особенностей ОС. Что происходит? Ваша программа виснет! Да, по вине системы. Бедный пользователь отчаянно пытается закрыть ваше приложение, но на попытку принудительного закрытия, как говориться с потерей всех данных, он видит окно (В Windows 2000 и выше, в 98 не наблюдалось:): <Невозможно завершить приложение: закройте отладчик>. В чём здесь дело? Дело в том, что выполнение приложения остановилось на коде, выполняющемся в режиме ядра, и поэтому разрушить процесс невозможно, так как это и обязано сделать ядро. Хочется отметить, что этот случай наблюдается не только при нехватке памяти, а и при других проблемах. Например, ему подвержен Windows Commander. Здесь можно долго спорить, кто виноват: Microsoft за несовершенные алгоритмы, или неполную документацию, либо программист, за своё незнание. В любом случае нехватку физической памяти следует рассматривать как стрессовую ситуацию при управлении ресурсами. Исчерпание ресурсов файла подкачки. Это как ни странно кажется невозможным. Однако плохие алгоритмы, и некоторые ошибки довольно часто приводят к таким результатам. Если ваша программа работает нормально в тех случаях, которые вы потенциально предусмотрели, то это не значит, что она будет стабильна во всех других случаях, о которых вы даже не догадываетесь. Примером больших проблем в управлении памятью можно наблюдать даже у такой <вылизанной> программы как Word, которая по оценкам автора занимает первое место по совершенству механизма управления ресурсами, в отличие от пакетов Photoshop или 3DMAX. Конечно, здесь сказывается неоспоримый плюс Microsoft, в большей проинформированности своих программистов, а так же видимо что их Developed раздел, работая над сложными архитектурами форматов (таких как *.doc) первые столкнулись с проблемами в управления ресурсами и фрагментацией, а потому достаточно преуспели. Идея единого сервера Совокупность кода, выполняющая монопольное управление, чем-либо (ресурсом) автор называет сервером, либо серво-ядром.
Как получить систему, эффективно использующую память? Создать объект-сервер, который бы занимался выделением памяти, её распределением, и решением многих других задач связанных с данным ресурсом. После, для каждого отдельного объекта, использующего память, создать ещё один уровень API - уже уровень 3 (если API Win32 - это уровень 1). Именно такая трёхуровневая система обеспечивает наилучшую эффективность кода. Причина создания API не кроется в принципе инкапсуляции, здесь преследуется другой принцип, который автор называется принципом сервера. В данном случае лексема сервер - это вовсе не тот сервер, что в Интернет ?. Под сервером понимается такая архитектура кода, его совокупность, где код, монопольно управляет ресурсом (в данном случае памятью), он единый имеет право произвести операцию выделения памяти, освобождения памяти, дефрагментации пространства, и т.д. Она получила так же названия серво-кода. Причина трёхуровневости объясняется следующими измышлениями. Посмотрите на схему 3.1 Уровень один - ОС занимается абстрагированием низкоуровневых механизмов работы с памятью. Уровень 2 - серво-ядро пользуясь сервисом ОС, довершает все задачи по управлению памятью. Третий уровень - непосредственно сам объект, использующий память. Каждый уровень осуществляет круг задач, ориентируясь на блоки памяти своего масштаба. Уровень два - серво ядро, выделяет память не конкретно объекту, а всему его классу или группе объектов. Это важно, так как код сервера не может осуществлять эффективное управление памятью, не зная конкретных особенностей данного класса. Для примера разберём случай с каким-либо окном. В данном примере будет иметься не само окно, что вы видите на экране, а код, который его обслуживает. С полной уверенностью данную совокупность кода можно назвать классом <Окно>. Объектам класса <Окно> требуется пространство памяти для размещения переменных. Поэтому на этапе инициализации класс, заказывает память у серво-ядра. Объём этой памяти намного превышает объем, который потребен для размещения данных одного объекта данного класса, и выделением памяти конкретным объектам, а так же многими другими задачами, занимается не само серво ядро, а код класса <Окно>. Иначе быть и не может, так как для эффективного управления ресурсом памяти серво-ядру должно быть <известно> как именно устроены заказы объектов данного класса. Короче говоря, только тот, которому надо, знает, как это сделать лучше. Автор надеется, что данный пример был достаточно убедительный, чтобы принять данную схему за основную. Конечно, это не означает, что её следует употреблять во всех случаях, так как всегда существует совокупность так называемых стандартных задач, которые можно предусмотреть при создании нашего сервера ресурсов памяти. В дальнейшем для экономии места сервер ресурсов памяти будет называться MRS, по первым буквам его англоязычного варианта. Три режима работы. Невозможно построить идеальный алгоритм, который верно бы работал <как в воде так в огне>. Несколько мыслей по этому поводу были изложены в понятие о нехватке памяти. Чтобы сделать систему управления памятью наиболее устойчивой, гибкой к состоянию системы полезно воспользоваться методикой разграничения режимов работы. Этот несколько технический термин скрывает в себе принцип изменения алгоритмов сервера MRS в зависимости от состояния ресурсов системы. Для краткости такой принцип получил от автора название: <Принципа трёх режимов>. В действительности режимов четыре: три рабочих и один стрессовый. Вот их перечень:
Несколько слов по каждому режиму. Первый режим предполагает, что ресурсов памяти много, а это значит, что не стоит тратить времени на слежение за эффективностью использования ресурсов памяти. Второй режим привносит некоторый баланс, между эффективностью и временем. Третий режим отдаёт больший вес эффективности и заставляет сервер <быть аккуратны в операциях с ресурсами>. А четвёртый режим - стрессовый, позволяет либо выйти из опасной ситуации, либо завершить приложение с сохранением всех данных. В действительности, некоторые алгоритмы MRS стоит продумывать в трёх-четырёх вариантах, а некоторые, в двух или, как обычно, в одном. Это значит, что не стоит, где попало пользоваться принципом <Трёх режимов>, хотя предусмотрение нескольких режимов работы - очень эффективный механизм, используемый не только в системах управления ресурсами. Но деление на три режима и четвёртый стрессовый не единственно, так как на работу MRS будет влиять наличие нескольких разнородных ресурсов памяти:
Конечно, следить за состоянием ресурса физической памяти очень накладно, но этого и не нужно, как было отмечено в понятие о нехватке памяти. Определение текущего режима первоначально происходит на этапе установке приложения, запуска (инициализации MRS), после чего специального слежения за состоянием памяти не проводится (здесь имеются системные средства). Как же обнаружит смену режима? Основная идея состоит в том, чтобы сменить режим, как только алгоритм <заподозрит> увеличение <напряжения> при выполнении. Эта подозрительность выражается в нескольких областях. Она может возникнуть, в свободном режиме, когда алгоритм из-за фрагментности пространства не может обнаружить непрерывный, свободный блок памяти, или увеличить размер блока памяти, без того чтобы не произвести дефрагментацию адресного пространства. Или переход в другой режим возникает с первым отказом обнаружит свободный блок памяти. Так, например, если произошёл заказ на ускользающую память, сервер обошел, все блоки и не может обнаружить свободный - значит, пора переключаться в более тяжёлый режим. Отсюда следует важный вывод, что более тяжелые режимы отличаются не только некоторыми отличиями в алгоритмах поиска и выделения свободного блока памяти, а так же, больше времени отдают на сервисные процедуры, поднимающие эффективность использования ресурса памяти, такие как дефрагментация и др. Инициализация сервера MRS, основные вопросы. В предыдущем пункте было отмечено, что MRS будет выделять память у системы большими блоками, а распределять и следить за ней сама. Следующий вопрос, который необходимо решить: где и как выделять память, в зависимости от её вида (см. классификацию). То есть где выделить быструю память, где медленную, как разместить блоки буферов, где лучше выделять ускользающую память. В таком случае рассматриваются два основных места размещения:
Конечно, не следует забывать о размещении памяти ещё в нескольких особых местах, о которых попозже. Для начала, следует объяснить по какой причине вообще заходит вопрос о специальном файле, почему бы не пользоваться только страничным файлом. Причин здесь несколько. Одна - это сохранность данных на диске при аварийном завершении программы. То есть, если вашу программу насильно закрыли, не предоставив ни единого шанса, либо, если операционная система чинит беспредел, все данные, находящиеся в страничном файле пропадут безвозвратно. Такая память нужна обычно при редактировании файлов, когда в её области создаются буфера, либо очереди, или осуществляются другие алгоритмы фрагментационного редактирования. В этом случае в данном файле будет сохранена та информация, которую система успела скинуть в файл. Конечно во многих программах, создавать такую схему нет смысла, она без ложной скромности принадлежит к сложным проектам. Однако она необходима, если вы желаете создать в вашем приложении механизм очереди транзакций наподобие того, что в NTFS. Короче говоря, этот файл, из которого можно брать память пригодится во всех случаях, когда вам будет необходимо иметь возможность знать при запуске программы, как она завершилась в прошлый раз. Ещё одна причина, по которой возможно понадобиться использовать этот файл - возможность преодоления границы в 2 Гб, который устанавливает Windows. Иногда этот метод диктуется вовсе не потребностью иметь память более 2Гб, а, например, для создания процедур дефрагментации, и как мы увидим в дальнейшем, в случае стрессовых ситуаций нехватки памяти. Как же это делается? В действительности довольно интересная схема. Итак, имеется специальный файл, который вы отображаете в память функциями Map, и осуществляете выделение ресурсов памяти в нём. В случае стрессовой ситуации, или другой причины (их несколько) вам просто следует переотобразить файл, уменьшив размер отображения. Таким образом, в случае нехватки, можно обеспечить нужное количество памяти для качественного надёжного завершения приложения. Среди этих причин могут быть, например, какие-то в данный момент ненужные данные, которые используют много места. Лучше не играться с огнём и освободить занимаемое ими пространство. Однако, если это данные, которые имеют некую вероятность дальнейшего использования, их можно не удалять, а при том условии, что они содержаться в нашем специальном файле, просто уменьшить память, занимаемую отображением, и не отображать ту часть файла, которая их содержит. В результате, если эти данные понадобятся, можно отобразить часть файла, содержащую их. Конечно такие операции очень медленные, но они позволяют экономно использовать системные ресурсы, не опасаясь угрозы нехватки памяти. Существуют так же ещё два особенных места работы с ресурсом памяти. Первый вам должен быть известен по часть 2 - это так называемая статическая память, которая уже существует на момент работы приложения. Ещё один вид памяти - это тоже память, существующая на момент работы, но в отличие от предыдущей - разделяемая несколькими процессами. Речь идёт о блоке Shared. Таким образом, мы определили четыре базовых источника ресурса памяти для MRS. И следственно этап инициализации должен состоять в том, чтобы:
Размер первоначально выделяемых блоков зависит от многих факторов:
Судя из материала предыдущих пунктов, размер блоков выделяемой памяти в страничном файле лучше делать не менее 64k, чтобы снизить вероятность фрагментации. Говоря о резервировании памяти в страничном файле и, поскольку резервирование не является выделением, можно смело зарезервировать пространство 1 - 10M, в зависимости от аппетитов программы. Даже если никто не будет использовать эту память системе от этого хуже не станет, при том условии, что страничный файл достаточно свободен. Приемлемым можно считать, если из всего зарезервированного пространства программа в среднем оставит без использования в свободном режиме роботы 10-20%, даже 30%, 1-10% - в оптимальном режиме работы. (Здесь имеется в виду вероятность использования) Выигрыш, который вы получаете - это предупреждение фрагментации пространства. Проецирование специального файла следует выполнять с объёмом в 10 раз меньше объёма выделенного в страничном файле. Размещение памяти по видам Каждый вид памяти (смотреть классификацию) удобно разместить в каком-то определённом блоке. Подобный подход был приведён в части 3, когда рассказывалось о том, как использовать местоположение, чтобы определять приоритет сброса. Выполняя процедуру инициализации MRS должен определить такие блоки, чтобы облегчить слежение за выделенными ресурсами. Основные задачи управления Перед тем как начать описание проблемы создания MRS, следует определиться с областью применения такой панацеи. Это чрезвычайно важно, так как очень многие программисты любят использовать одну и ту же технологию везде, где только можно. Очевидно, что такой подход, является опасным, однако он свойственен природе человека. Автор не может очертить жёстких правил по использованию всего того, что вы прочитаете, это невозможно, поэтому автор даёт некоторые рекомендации по внедрению сервера MRS в ваш проект. Когда использование MRS неэффективно:
Когда использование MRS эффективно:
Как видите, эти пункты нередко противоречат друг другу, и поэтому выбор за вами. В действительности реализация MRS представляет собой объединение так же ещё нескольких серверов управляющих ресурсами, и все они вместе являют один сложный комплекс.
Из всех задач по управлению памятью выделяются такие основные:
Поиск и выделение памяти Некоторые вопросы этой задачи были уже рассмотрены в части 2, когда разговор зашёл о технологии ускользающих. И, тем не менее, автор снова в полном объёме вернётся к нему, но теперь уже в общем. Итак, поскольку видов памяти несколько, то и видов процедур поиска, алгоритмов обязано быть несколько. Очевидно хотя бы, что для медленной памяти - это один алгоритм, для быстрой - другой, не вспоминая ускользающую память и т.д. Разница в этих алгоритмах поиска концентрируется на предпочтении толи времени выполнения, толи эффективности выделения. Ясно, что для медленной памяти MRS может думать столько, сколько потребуется лишь бы выделить память наиболее эффективно, а для быстрой памяти постараться всё сделать как можно быстрее. Что, значит, выделить память эффективно? Этот вопрос оказывается не совсем понятным. Степень эффективности памяти объясняется одной целью: выделить память так, чтобы при последующем её выделении, время выполнение данной операции было минимальным, а ресурс использовался полностью. Говоря проще, эффективно выделить память это значит:
Прежде чем определить стратегию поиска, хорошо вспомнить, про стратегию выделения памяти. Итак, как было сказано выше, MRS выделяет у системы сразу большие блоки оперативной памяти, а потом самостоятельно их распределяет. Алгоритмы поиска и выделения напрямую зависят от модели блоков памяти. То есть, имеется в виду, что MRS будет обрабатывать запросы на память различного вида (см. классификацию). Именно поэтому сам алгоритм поиска должен уметь не просто найти свободную память, а найти свободную память данного вида. Распределение памяти Задача распределения памяти, жёстко связана со всеми остальными задачами. Задача распределения памяти состоит в том, чтобы разместить память данного вида так, чтобы достичь наилучших условий для поиска, и выделения памяти. Освобождение памяти В части 3 говориться о том, когда стоит освобождать буферную память. Было сказано, что наиболее эффективней производить освобождение при операциях CloseXX, нежели других. Однако процесс освобождения памяти следует считать идентичным процессу выделения, так как и он участвует в оптимизации пространства памяти. Расширение объёма памяти Задача расширение объёма памяти - есть оптимизационной, и зависит от <тяжести> приложения. Расширение объёма памяти должно стремиться удовлетворить следующие требования:
Задача дефрагментации пространства памяти состоит в том, чтобы, не занимая активное время приложения, оптимизировать пространство памяти. Что дальше? Далее автор продолжит рассказ по созданию MRS. В следующей статье, мы перейдём от теории к практике и попытаемся создать MRS сервер. Кашинский Дмитрий Владимирович |
|
2000-2008 г. Все авторские права соблюдены. |
|