Какой изящный способ обработки нехватки памяти в C / C ++?

Я пишу приложение для кэширования, которое потребляет большие объемы памяти.

Надеюсь, я справлюсь с памятью достаточно хорошо, но я просто думаю о том, что to do if I do run out of memory.

If a call to allocate even a simple object fails, is it likely that even a syslog call will also fail?

EDIT: Ok perhaps I should clarify the question. If malloc or new returns a NULL or 0L value then it essentially means the call failed and it can't give you the memory for some reason. So, what would be the sensible thing to do in that case?

EDIT2: I've just realised that a call to "new" can throw an exception. This could be caught at a higher level so I can perhaps gracefully exit further up. At that point, it may even be possible to recover depending on how much memory is freed. In the least I should by that point hopefully be able to log something. So while I have seen code that checks the value of a pointer after new, it is unnecessary. While in C, you should check the return value for malloc.

45
задан Matt 28 September 2011 в 04:25
поделиться

9 ответов

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

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

Разматывание стека - ваш друг ;)

EDIT: Только что понял, что вопрос также был помечен C - если это так, то ваши функции должны освобождать свои внутренние структуры вручную при выходе найдены состояния памяти; не делать этого - утечка памяти.

EDIT2: Пример:

#include <iostream>
#include <vector>

void DoStuff()
{
    std::vector<int> data;
    //insert a whole crapload of stuff into data here.
    //Assume std::vector::push_back does the actual throwing
    //i.e. data.resize(SOME_LARGE_VALUE_HERE);
}

int main()
{
    try
    {
        DoStuff();
        return 0;
    }
    catch (const std::bad_alloc& ex)
    {   //Observe that the local variable `data` no longer exists here.
        std::cerr << "Oops. Looks like you need to use a 64 bit system (or "
                     "get a bigger hard disk) for that calculation!";
        return -1;
    }
}

EDIT3: Хорошо, согласно комментаторам, существуют системы, которые не соответствуют стандарту в этом отношении. С другой стороны, в таких системах вы в любом случае будете SOL, поэтому я не понимаю, почему они заслуживают обсуждения. Но если вы находитесь на такой платформе, об этом нужно помнить.

18
ответ дан 26 November 2019 в 21:28
поделиться

Если ваше приложение, вероятно, будет выделять большие блоки памяти и рискует превысить ограничения для каждого процесса или виртуальной машины, ожидание фактического сбоя выделения — сложная ситуация для восстановления. К тому времени, когда malloc вернет NULL или new throws std::bad_alloc, все может быть уже слишком далеко для надежного восстановления. В зависимости от вашей стратегии восстановления, многие операции могут по-прежнему требовать выделения кучи, поэтому вы должны быть чрезвычайно осторожны при выборе подпрограмм, на которые вы можете положиться.

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

Кроме того, в зависимости от ваших моделей использования памяти, использование специального распределителя может дать вам лучшие результаты, чем стандартный встроенный malloc. Например, определенные шаблоны распределения могут фактически привести к фрагментации памяти с течением времени, поэтому, даже если у вас есть свободная память, доступные блоки на арене кучи могут не иметь доступного блока нужного размера. Хорошим примером этого является Firefox, который переключился на dmalloc и значительно увеличил эффективность использования памяти.

4
ответ дан 26 November 2019 в 21:28
поделиться

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

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

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

2
ответ дан 26 November 2019 в 21:28
поделиться

Как уже было сказано, исчерпание памяти означает, что все ставки сняты. ИМХО, лучший способ справиться с этой ситуацией - изящно потерпеть неудачу (в отличие от простого сбоя!). Ваш кеш может выделить разумный объем памяти при создании экземпляра. Размер этой памяти будет равняться объему, который при освобождении позволит программе завершиться в разумных пределах. Когда ваш кеш обнаруживает, что памяти становится мало, он должен освободить эту память и инициировать корректное завершение работы.

1
ответ дан 26 November 2019 в 21:28
поделиться

Я пишу приложение для кэширования, которое потребляет много памяти. Надеюсь, я буду достаточно хорошо управлять своей памятью, но я просто думаю о том, что делать, если у меня кончится память.

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

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

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

Если malloc или new возвращают значение NULL или 0L, это, по сути, означает, что вызов не удался и по какой-то причине он не может предоставить вам память. Итак, что было бы разумно сделать в таком случае?

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

Для простой памяти кучи malloc() возвращает 0 обычно означает:

  • что вы исчерпали кучу и если ваше приложение не освободит часть памяти, далее malloc ()s не удастся.

  • неправильный размер выделения: довольно распространенной ошибкой кодирования является смешение типов со знаком и без знака при расчете размера блока.Если размер оказывается ошибочно отрицательным, переданным в malloc(), где ожидается size_t, он становится очень большим числом.

Таким образом, в некотором смысле также правильно abort() для создания основного файла, который можно проанализировать позже, чтобы понять, почему malloc() вернул 0 . Хотя я предпочитаю (1) включать размер попытки выделения в сообщение об ошибке и (2) пытаться двигаться дальше. Если приложение выйдет из строя из-за другой проблемы с памятью в будущем (*), оно все равно создаст основной файл.

(*) Из моего опыта создания программного обеспечения с динамическим управлением памятью, устойчивого к ошибкам malloc(), я вижу, что часто malloc() возвращает 0 не надежно. За первыми попытками возврата 0 следует успешный malloc(), возвращающий действительный указатель. Но первый доступ к указанной памяти приведет к краху приложения. Это мой опыт работы как с Linux, так и с HP-UX, и я видел аналогичную картину и в Solaris 10. Такое поведение не является уникальным для Linux. Насколько мне известно, единственный способ сделать приложение на 100% устойчивым к проблемам с памятью — заранее выделить всю память. И это обязательно для критически важных приложений, безопасности, жизнеобеспечения и приложений операторского класса — им не разрешено динамическое управление памятью после фазы инициализации.

1
ответ дан 26 November 2019 в 21:28
поделиться

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

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

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

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

Если вы не нейрохирург, вам больше нечего делать.

Кроме того, очень часто вы даже не получите std::bad_alloc или что-то в этом роде, вы просто получите указатель в ответ на ваш malloc/new и умрете только тогда, когда вы на самом деле попробуй прикоснуться ко всей этой памяти. Этого можно избежать, отключив overcommit в операционной системе, но все же.

Не рассчитывайте на то, что сможете справиться с SIGSEGV, когда вы касаетесь памяти, на что ядро ​​надеялось, что вы не будете. Я не совсем уверен, как это работает на стороне Windows, но я уверен, что они сделать overcommit тоже.

В целом, это не одно из сильных мест C++.

-1
ответ дан 26 November 2019 в 21:28
поделиться

Разве этот вопрос не делает предположений относительно перераспределенной памяти?

То есть ситуация нехватки памяти может быть неустранимой! Даже если у вас не осталось памяти, вызовы malloc и других распределителей могут быть успешными, пока программа не попытается использовать память. Затем БАМ! , какой-то процесс прерывается ядром, чтобы удовлетворить нагрузку на память.

18
ответ дан 26 November 2019 в 21:28
поделиться

У меня нет особого опыта работы с Linux, но я провел много времени, работая над видеоиграми на игровых консолях, где нехватка памяти запрещена, и над инструментами на базе Windows. .

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

Напишите/украдите свой собственный менеджер памяти и направьте его на выделение из этих буферов. Затем последовательно используйте его в своем приложении или воспользуйтесь опцией gcc --wrap для переадресации вызовов от malloc и друзей соответствующим образом. Если вы используете какие-либо библиотеки, которые не могут быть направлены на вызов вашего диспетчера памяти, выбросьте их, потому что они будут только мешать вам. Отсутствие переопределяемых вызовов управления памятью свидетельствует о более глубоких проблемах; вам лучше без этого конкретного компонента.(Примечание: даже если вы используете --wrap, поверьте мне, это все равно свидетельствует о проблеме! Жизнь слишком коротка, чтобы использовать библиотеки, которые не позволяют вам перегрузить их управление памятью!)

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

Такой подход может показаться занозой в заднице. Ну... так и есть. Но это просто, и для этого стоит приложить немного усилий. Я думаю, что об этом есть цитата Кернигана и/или Рича.

6
ответ дан 26 November 2019 в 21:28
поделиться

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

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

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

Что происходит со сложным управлением памятью Squid, так это то, что он вступает в конфликт со сложным управлением памятью ядра...

Squid создает HTTP-объект в ОЗУ, и он используется несколько раз быстро после создания. Затем через некоторое время он больше не получает попаданий, и ядро ​​​​замечает это. Затем кто-то пытается получить память из ядра для чего-то, и ядро ​​решает вытолкнуть эти неиспользуемые страницы памяти в пространство подкачки и более разумно использовать (кеш-RAM) для некоторых данных, которые фактически используются программой. Однако это делается без ведома Squid. Squid по-прежнему думает, что эти http-объекты находятся в оперативной памяти, и они будут в ту же секунду, когда он попытается получить к ним доступ, но до тех пор оперативная память используется для чего-то продуктивного. ...

Через некоторое время Squid также заметит, что эти объекты не используются, и решит переместить их на диск, чтобы ОЗУ можно было использовать для более загруженных данных. Итак, Squid выходит, создает файл, а затем записывает в него объекты http.

Здесь мы переключаемся на высокоскоростную камеру: Squid вызывает write(2), адрес, который он дает, является «виртуальным адресом», и ядро ​​помечает его как «не дома». ...

Ядро пытается найти свободную страницу, если таковых нет, то возьмет откуда-то малоиспользуемую страницу, скорее всего другой малоиспользуемый объект Squid, запишет в пейджинг...пространство на диске («область подкачки»), когда эта запись завершится, он прочитает из другого места в пуле подкачки данные, которые он «выгружает» на теперь неиспользуемую страницу ОЗУ, исправит таблицы подкачки и повторит инструкцию. который потерпел неудачу. ...

Итак, теперь Squid имеет объект на странице в оперативной памяти и записывается на диск в двух местах: одна копия в пространстве подкачки операционной системы и одна копия в файловой системе. ...

Вот как это делает Varnish:

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

Если/когда ядро ​​решит, что ему нужно использовать ОЗУ для чего-то другого, страница будет записана в резервный файл, а страница ОЗУ будет повторно использована в другом месте.

Когда Varnish в следующий раз обратится к виртуальной памяти, операционная система найдет страницу ОЗУ, возможно, освободив ее, и прочитает содержимое из резервного файла.

Вот и все. Varnish на самом деле не пытается контролировать, что кешируется в ОЗУ, а что нет, у ядра есть программная и аппаратная поддержка, чтобы хорошо с этим справляться, и оно делает хорошую работу.

Возможно, вам вообще не потребуется писать код кэширования.

1
ответ дан 26 November 2019 в 21:28
поделиться