Восстановление после сбоя динамического выделения памяти

Я работаю над встроенным процессором (Intel PXA255 XScale 400 МГц), и мне показалось, что я видел один случай, когда не хватало памяти для выполнения «новой» операции. Программа не аварийно завершилась, поэтому я предположил, что другие потоки освободили свою память, и это было временным явлением. Это довольно критический код, поэтому выход не является вариантом, и удаленному пользователю необходимо возвращать какую-то ошибку.

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

char someArr[];
do{ 
    someArr = new char[10]; 
    Sleep(100); // no justification for choosing 100 ms
} while ( someArr == NULL );

Сон помогает? Должен ли я установить максимальное количество попыток? Можно ли везде использовать статическую инициализацию?

ЗАКЛЮЧИТЕЛЬНОЕ ОБНОВЛЕНИЕ: Большое спасибо за полезные ответы, но оказалось, что при проверке кода на сбой выделения памяти произошла ошибка. Я буду помнить все эти ответы и заменю как можно больше malloc и new (особенно в коде обработки ошибок).


person Nate Parsons    schedule 15.12.2008    source источник


Ответы (9)


Есть несколько разных способов атаковать это - обратите внимание, что инструкции инструмента будут немного отличаться в зависимости от того, какую версию Windows CE / Windows Mobile вы используете.

Некоторые вопросы, на которые нужно ответить:

1. Не вызывает ли утечка памяти в вашем приложении утечку памяти?

2. Ваше приложение просто использует слишком много памяти на определенных этапах, что приводит к нехватке памяти?

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

http://msdn.microsoft.com/en-us/library/aa446904.aspx

3. Вы очень часто выделяете и освобождаете память в этом процессе?

Windows CE до версии ОС 6.0 (не путать с Windows Mobile 6.x) имела ограничение виртуальной памяти 32 МБ на процесс, что обычно вызывает множество забавных проблем с фрагментацией. В этом случае, даже если у вас достаточно свободной физической памяти, у вас может не хватить виртуальной памяти. Использование настраиваемых распределителей блоков обычно помогает избежать этой проблемы.

4. Вы выделяете очень большие блоки памяти? (> 2 МБ)

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

5. Вы используете большое количество DLL?

Также относится к 3: в зависимости от версии ОС, библиотеки DLL также могут очень быстро уменьшить общий доступный объем виртуальных машин.

Дальнейшие отправные точки:

Обзор средств памяти CE

http://blogs.msdn.com/ce_base/archive/2006/01/11/511883.aspx

Окно управления целью инструмент 'mi'

http://msdn.microsoft.com/en-us/library/aa450013.aspx

person Teman    schedule 28.12.2008

Вы пытаетесь решить глобальную проблему с помощью локальных рассуждений. Глобальная проблема заключается в том, что все устройство имеет ограниченный объем оперативной памяти (и, возможно, резервной памяти) для операционной системы и всех приложений. Чтобы убедиться, что этот объем ОЗУ не превышен, у вас есть несколько вариантов:

  • Каждый процесс работает в фиксированном объеме ОЗУ, который определяется для каждого процесса во время запуска; программист рассуждает, чтобы убедиться, что все подходит. Итак, да, можно все разместить статически. Это просто большая работа, и каждый раз, когда вы меняете конфигурацию своей системы, вам нужно пересматривать распределения.

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

  • Каждый процесс осознает, что память может быть исчерпана и может переключиться в состояние, в котором он потребляет минимальный объем памяти. Часто эта стратегия реализуется с помощью исключения нехватки памяти. Исключение обрабатывается в main () или рядом с ним, а событие нехватки памяти по существу перезапускает программу с нуля. Этот режим аварийного переключения может работать, если объем памяти увеличивается в ответ на запросы пользователей; если требования программы к памяти растут независимо от действий пользователя, это может привести к сбоям в работе.

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

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

person Norman Ramsey    schedule 15.12.2008
comment
Проблема в том, что около 99% всего кода не имеют представления о том, как изящно обрабатывать неудачные выделения памяти. Приложения терпят неудачу по загадочным причинам. Отсутствие свободной памяти приводит к остановке почти всех операционных систем. Печально, но факт. - person Thorsten79; 29.12.2008

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

«Правильный» ответ на эту ситуацию, вероятно, состоит в том, чтобы установить строгие ограничения для различных частей программы, чтобы гарантировать, что они не потребляют чрезмерно память. Это, вероятно, потребует переписывания основных разделов во всех частях программы.

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

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

Вы также можете попробовать sleep (0), который просто передаст управление любому другому потоку, готовому к запуску. Это позволит вашему потоку немедленно восстановить контроль, если все другие потоки перейдут в спящий режим, вместо того, чтобы ждать своего 100-миллисекундного предложения. Но если хотя бы один поток все еще хочет работать, вам все равно придется подождать, пока он не откажется от управления. На машинах Linux это обычно 10 миллисекунд, последний раз я проверял. Насчет других платформ не знаю. Ваш поток также может иметь более низкий приоритет в планировщике, если он добровольно перешел в спящий режим.

person markets    schedule 15.12.2008

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

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

Если куча является общей, то, вероятно, все вышеперечисленное сработает. Однако, если у вас есть общая куча, то вызов «new», вероятно, приведет либо к блокировке вращения (цикл, подобный тому, который у вас есть, но с использованием инструкций CAS), либо он будет заблокирован в зависимости от некоторых ресурсов ядра.

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

Я бы подумал об отмене операторов «новый» и «удалить». Когда новый не работает, вы можете заблокировать (или заблокировать спин-блокировку какой-либо переменной счетчика), ожидая, пока другой поток освободит память, а затем удаление может либо сигнализировать заблокированному «новому» потоку, либо увеличить переменную счетчика с помощью CAS.

Это должно дать вам лучшую пропускную способность и быть немного более эффективным.

person Scott Wisniewski    schedule 15.12.2008

Несколько моментов:

  • Встроенные программы часто выделяют всю память при запуске или используют только статическую память, чтобы избежать подобных ситуаций.
  • Если на устройстве не запущено что-то еще, что регулярно освобождает память, ваше решение вряд ли будет эффективным.
  • У Viper, который у меня есть, 64 МБ ОЗУ, я не думаю, что у них меньше 32 МБ, сколько памяти использует ваше приложение?
person Robert Gamble    schedule 15.12.2008

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

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

person jakobengblom2    schedule 17.12.2008

Вы используете C ++. Таким образом, вы можете использовать некоторые утилиты C ++, чтобы облегчить себе жизнь. Например, почему бы не использовать new_handler?

void my_new_handler() {
    // make room for memory, then return, or throw bad_alloc if
    // nothing can be freed.
}

int main() {
    std::set_new_handler(&my_new_handler);

    // every allocation done will ask my_new_handler if there is
    // no memory for use anymore. This answer tells you what the
    // standard allocator function does: 
    // https://stackoverflow.com/questions/377178
}

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

person Johannes Schaub - litb    schedule 28.12.2008

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

Как упоминается в другом сообщении, было бы хорошо обернуть логику в некоторые служебные функции, чтобы не писать все время код нехватки памяти.

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

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

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

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

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

Примечание: это, очевидно, не идеальное решение, и, как упоминалось на других плакатах, для правильного решения проблемы необходим более глобальный взгляд на приложение в целом, но приведенный выше является практическим методом, который должен работа в краткосрочной перспективе.

person benno    schedule 29.12.2008

Конечно, это будет зависеть от того, есть ли у вас разумные основания полагать, что память станет доступной в 100 (миллисекундном?) Режиме сна? Конечно, вы должны ограничить количество попыток.

Мне здесь что-то не пахнет. Хм...

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

person Lawrence Dol    schedule 15.12.2008