Анимация
JavaScript


Главная  Библионтека 

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 [ 48 ] 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

Пример 23-2: Инициализация по умолчанию: если т - не POD-ТИП, то этот код инициализирует все

объекты т немедленно по выделении памяти и

обращается к каждому (значащему, не

заполняющему) байту.

т* р = new т[1000000000];

Если т - не POD-тип, то каждый объект инициализируется по умолчанию, что означает, что записываются все значащие байты каждого объекта, т.е. выполняется обращение ко всей выделенной памяти"".

Вы можете решить, что это полезно, но это не так. Да, если вызов функции mem-set в примере 23-1 или оператора new в примере 23-2 завершится успешно, значит, память действительно выделена и фиксирована. Но если произойдет описанный ранее сбой при обращении к памяти, то мы не получим ни нулевого указателя, ни исключения Ьас1 аПос - нет, произойдет все та же ошибка доступа и программа аварийно завершится, и с этим ничего не поделаешь (если только нет возможности перехватить и обработать этот сбой некоторыми платформо-зависимыми способами). Этот способ ничуть не лучше и не безопаснее выделения памяти без обращения к ней, в надежде, что память окажется "на месте" в тот момент, когда она будет нам нужна.

Это возвращает нас к вопросу о соответствии стандарту, которого все же могли бы достичь разработчики компиляторов, например, они могли бы использовать знания об операционной системе для перехвата ошибок доступа к памяти и тем самым предотвратить аварийное завершение программы. Т.е. они могли бы разработать оператор new так, как мы только что рассматривали, - выделяющим память и выполняющим запись каждого ее байта (или, по крайней мере, каждой страницы) с использованием перехвата обращения к иссуществуюшей памяти средствами операционной системы и преобразования его в стандартное исключение bad alloc (или нулевой указатель в случае не генерирующего исключений оператора new). Тем не менее, я сомневаюсь, чтобы разработчики компиляторов пошли на это, по двум причинам: во-первых, .это существенно снижает производительность, и, во-вторых, ошибка оператора new - слишком большая редкость в реальной жизни. И эго подводит нас к следующему пункту.

На практике ошибки выделения памяти встречаются очень редко. Действительно, многие современные серверные профаммы крайне редко встречаются с исчерпанием памяти.

В системе виртуальной памяти каждая программа работает в своем собственном адресном пространстве. Это приводит к активному использованию страничной организации памяти и, при больших запросах к памяти, к активному использованию диска для подкачки памяти. В результате задолго до того, как память будет исчерпана, активная работа с диском приведет к существенному палению производительности системы в целом, и процессы так и не дождутся отказа оператора new просто потому, что вследствие падения производительности из-за постоянного свопинга в дело придется вмешаться системному администратору, так что программы не умрут непосредственно из-за нехватки памяти, а будут убиты его рукой.

Конечно, вполне возможно создать программу, которая будет требовать все больше и больше памяти, при этом не используя большую ее часть. Это возможно, хотя и необычно (во всяком случае, для меня). Сказанное также не относится к системам без виртуальной памяти, напри.мер, ко встроенным системам или системам, работающим в реальном времени. Некоторые из них настолько критичны ко всякого рода сбоям, что не используют динамическую память в принципе, не говоря уж о виртуальной памяти.

Когда вы обнаруживаете ошибку оператора new, вы можете сделать не так уж много. Как указывал Эндрю Кёниг в своей статье "Когда памяти не хватает" ("When Memory Runs Low" [Koenig96]), поведение по умолчанию при ошибке оператора new.

Мы не рассматриваем случай патологического типа Т, конструктор которого не инициализирует данные объекта.



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

Конечно, когда оператор new выполняется неуспешно, иногда можно сделать и другие вещи. Если вы хотите вывести диагностическую информацию, то подключае-мая функция-обработчик ошибок оператора new - вполне подходящее для этого место. Иногда можно воспользоваться стратегией с использованием резервного "неприкосновенного" буфера памяти для чрезвычайных ситуаций. Однако любой, кто захочет воспользоваться одним из таких способов, должен четко понимать, что именно он делает, и тщательно тестировать обработку ошибки на целевой платформе, поскольку зачастую на самом деле все работает не совсем так, как вы себе это представляете. И наконец, если память действительно исчерпана, вам может не удаться сгенерировать нетривиальное (т.е. нсвстроенное) исключение. Даже такая тривиальная инструкция как throw string("fai led") ; скорее всего, будет пытаться выделить динамическую память с использованием оператора new (в зависимости от степени оптимизации вашей реализации класса string).

Да, бывают ситуации, когда можно сделать что-то полезное в ответ на неуспешное завершение оператора new, но, как правило, не стоит делать что-то большее, чем свертка стека или использование обработчика ошибки, выполняющего журнальную запись о ней.

Что надо проверять

Бывают отдельные случаи, когда проверка исчерпания памяти и попытка восстановления после него имеют смысл. Некоторые из них перечислены в статье [Koenig96]. Например, можно выполнить выделение всей памяти и ее инициализацию в начале программы, а после самостоятельно ее распределять. В таком случае, если ваша программа аварийно завершится из-за нехватки памяти (обращения к некорректному блоку), то, по крайней мере, это произойдет сразу же, т.е. до того, как программа приступит к реальной работе. Такой подход требует приложения дополнительных усилий и годится лишь в ситуации, когда вы заранее знаете требуемое программе количество памяти.

Основной тип восстанавливаемой ошибки оператора new, который я видел в промышленных системах - это создание буферов, размер которых поступает в программу извне, с некоторого устройства ввода. Рассмотрим, например, коммуникационное приложение, в котором каждый передаваемый пакет предваряется значением длины пакета, и первое, что должен сделать получатель -- это прочесть длину получаемого пакета и выделить для него буфер достагочного размера. В этой ситуации я видел попытки выделения "монстрообразных" блоков памяти, в первую очередь из-за искажения потока передаваемых данных (или ошибки в программе обработки). В этом случае приложение должно проверять наличие искажения данных (а еще лучше исполь-зовать протокол, позволяющий избежать такого рода искажений данных) и отбрасывать неверные данные или заведомо некорректные размеры буфера, поскольку при такой стратегии программа остается в состоянии продолжать выполнение разумных действий, в частности, использовать повторную передачу информации с меньшим размером пакета или даже просто отбросив пакет с некорректным размером и продолжать обработку других пакетов - вместо того, чтобы просто "рухнуть" под тяжестью запроса блока памяти непомерного размера.



в конце концов, смешно говорить об "исчерпании памяти" при попытке выделения блока размером 2 Гбайт, в то время, как в системе остается доступным 1 Гбайт памяти!"*

Еще один случай, когда восстановление после сбоя оператора new имеет смысл, - это когда ваша программа оптимистично пытается выделить огромный рабочий буфер, но при невозможности снижает свои требования до тех пор, пока не сможет получить требуемое. В этом случае программа должна быть создана таким образом, чтобы уметь подстраиваться под имеющийся буфер памяти, а при необходимости и работать с несколькими не последоватсльными блоками памяти.

Резюме

Избегайте использования не генерирующего исключения оператора new, поскольку он не дает никаких дополнительных преимуществ, зато обычно приводит к худшему поведению программы при ошибке выделения памяти, чем обычный оператор new, генерирующий исключения.

Помните, что проверка неуспешного выполнения оператора new зачастую бесполезна по ряду причин.

Если же вы действительно беспокоитесь о возможном исчерпании памяти, то убедитесь, что вы проверяете именно то, что намерены, поскольку:

• проверка отказов оператора new обычно бесполезна в операционной системе, в которой реальное предоставление памяти процессу не происходит до тех пор, пока память не начинает реально использоваться;

• в системе виртуальной памяти задолго до исчерпания памяти резко снижается производительность программ, что приводит к вмешательству со стороны системного администратора;

• за исключением некоторых специальных случаев, даже обнаружив отказ оператора new. вы можете мало что сделать для обработки этой ситуации - если памяти в системе действительно не осталось.

Интересно, что выделение буфера памяти, размер которого определяется извне, - классический пример уязвимости системы защиты. Атака злонамеренными пользователями или программами, пытающимися вызвать проблемы с буфером динамической памяти, - классика жанра, которая до сих пор остается любимым средством пля взлома (или слома) систем. Заметим, что крах программы, вызванный отказом в выделении блока памяти требуемого размера, - разновидность атаки DOS (Denial-Of-Service - отказ в обслуживании), но не взлома системы в целях получения несанкционированного доступа к ней. Близкая к этому, но отличная методика, связанная с переполнением буфера, также остается излюбленным методом у хакеров, и остается только удивляться, как много программистов все еще продолжают использовать функции типа strcpy и другие, которые не проверяют размеры буфера и оставляют широкий простор для взломщиков.



0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 [ 48 ] 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85