Каков изящный способ обработки нехватки памяти в C / C ++? - PullRequest
44 голосов
/ 30 августа 2010

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

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

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

РЕДАКТИРОВАТЬ: Хорошо, возможно, я должен уточнить вопрос.Если malloc или new возвращает значение NULL или 0L, то это, по сути, означает, что вызов не выполнен, и по какой-то причине не может дать вам память.Итак, что было бы разумно сделать в этом случае?

EDIT2: Я только что понял, что вызов «new» может вызвать исключение.Это можно поймать на более высоком уровне, поэтому я, возможно, изящно выйду дальше.В этот момент может быть даже возможно восстановить в зависимости от того, сколько памяти освобождено.По крайней мере, к этому моменту я надеюсь, что смогу что-то записать.Так что, хотя я видел код, который проверяет значение указателя после нового, он не нужен.Находясь в C, вы должны проверить возвращаемое значение для malloc.

Ответы [ 9 ]

18 голосов
/ 30 августа 2010

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

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

17 голосов
/ 30 августа 2010

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

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

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

РЕДАКТИРОВАТЬ: Просто осознал, что вопрос также был помечен буквой 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, поэтому я не понимаю, почему они заслуживают обсуждения.Но если вы находитесь на такой платформе , об этом следует помнить.

6 голосов
/ 30 августа 2010

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

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

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

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

4 голосов
/ 30 августа 2010

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

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

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

2 голосов
/ 30 августа 2010

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

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

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

1 голос
/ 30 августа 2010

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

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

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

Не следует.Это частично объясняет, почему 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 голос
/ 30 августа 2010

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

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

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

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

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

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

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

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

Так что теперь Squid имеет объект на странице в ОЗУ и записывает на диск два места: одну копию в пространстве подкачки операционной системы и одну копию в файловой системе. ...

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

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

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

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

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

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

1 голос
/ 30 августа 2010

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

0 голосов
/ 30 августа 2010

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

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

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...