Программирование >>  Обобщенные обратные вызовы 

1 ... 44 45 46 [ 47 ] 48 49 50 ... 84


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

Заметим, что если оператор new использует возможности операционной системы непосредственно, то он всегда завершается успешно, но последующий за ним невинный код наподобие buf[100] = с; может привести к генерации исключения или аварийному останову. С точки зрения стандарта С++ оба действия некорректны, поскольку, с одной стороны, стандарт С++ требует, чтобы в случае, когда оператор new не может регыьио вьщелить запрошенную память, он генерировал исключение (этого не происходит), и чтобы код наподобие buf [100] = с не приводил к генерации исключений или другим сбоям (что может произойти).

Почему некоторые операционные системы выполняют такое отложенное выделение памяти? В основе такой схемы лежит прагматичный довод: процессу, который запросил данную память, сразу в полном объеме она может не понадобиться, более того - процесс может никогда не использовать ее полностью или не использовать ее всю одновременно - а тем временем сю мог бы воспользоваться другой процесс, которому эта память нужна ненадолго. Зачем выделять процессу всю память немедленно при запросе, если реально она ему может и не понадобиться? Поэтому описанная схема имеет ряд преимуществ.

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

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

Пример 23-1: инициализация вручную с обращением к каждому

байту выделенной памяти.

char* р = new char[1000000000]; memsetC р, О, 1000000000 );

Если память выделяется для типа, который является типом класса, а не обычным старым типом (POD), то обращение к памяти выполняется автоматически.

POD означает plain old data - простые старые данные . Неформально POD означает любой тип, представляющий собой набор простых данных, возможно, с пользовательскими функциями-членами для удобства. Говоря более строго, POD представляет собой класс или объединение, у которого нет пользовательского конструктора, копирующего присваивания, и деструктора, а также нет (нестатических) членов-данных, являющихся ссылками, указателями на члены или не являющимися POD.



Пример 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. который я видел в промышленных системах - это создание буферов, размер которых поступает в программу извне, с некоторого устройства ввода. Рассмотрим, например, коммуникационное приложение, в котором каждый передаваемый пакет предваряется значением длины пакета, и первое, что должен сделать получатель -- это прочесть длину получаемого пакета и выделить для него буфер достагочиого размера. В этой ситуации я видел попытки выделения монстрообразных блоков памяти, в первую очередь из-за искажения потока передаваемых данных (или ошибки в программе обработки). В этом случае приложение должно проверять наличие искажения данных (а еще лучше исполь-зовать протокол, позволяющий избежать такого рода искажений данных) и отбрасывать неверные данные или заведомо некорректные размеры буфера, поскольку при такой стратегии программа остается в состоянии продолжать выполнение разумных действий, в частности, использовать повторную передачу информации с меньшим размером пакета или даже просто отбросив пакет с некорректным размером и продолжать обработку других пакетов - вместо того, чтобы просто рухнуть под тяжестью запроса блока памяти непомерного размера.



1 ... 44 45 46 [ 47 ] 48 49 50 ... 84

© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки.
Яндекс.Метрика