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