Должен ли я беспокоиться об обнаружении ошибок OOM (нехватка памяти) в моем коде C? - PullRequest
23 голосов
/ 18 апреля 2009

Я посвятил большое количество строк кода C меткам очистки / условным обозначениям для неудачного выделения памяти (на что указывает семейство alloc, возвращающее NULL). Меня учили, что это хорошая практика, чтобы при сбое памяти можно было пометить соответствующий статус ошибки и вызывающий мог потенциально выполнить «изящную очистку памяти» и повторить попытку. Теперь у меня есть некоторые сомнения по этому поводу философия, которую я надеюсь прояснить.

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

Я также только что обнаружил убийцу Linux OOM, который, кажется, делает эти усилия совершенно бессмысленными на моей основной платформе разработки.

По умолчанию Linux следует оптимистичная стратегия выделения памяти. Это означает, что когда malloc () возвращает не NULL, нет никакой гарантии, что память действительно доступна. Это действительно плохая ошибка. В случае, если это Оказывается, что система вышла из памяти, один или несколько процессов будут быть убитым печально известной ООМ убийца.

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

Ответы [ 11 ]

20 голосов
/ 18 апреля 2009

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

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

Я считаю, что лучше спроектировать программу так, чтобы она могла аварийно завершиться в любое время. Например, убедитесь, что созданные пользователем данные постоянно сохраняются на диске, даже если пользователь явно не сохраняет их. (См., Например, vi -r.) Таким образом, вы можете создать функцию для выделения памяти, которая завершает программу в случае ошибки. Так как ваше приложение предназначено для обработки сбоев в любое время, это нормально для сбоя. Пользователь будет удивлен, но не потеряет (много) работы.

Непрерывная функция выделения может выглядеть примерно так (непроверенный, не скомпилированный код, только для демонстрационных целей):

/* Callback function so application can do some emergency saving if it wants to. */
static void (*safe_malloc_callback)(int error_number, size_t requested);

void safe_malloc_set_callback(void (*callback)(int, size_t))
{
    safe_malloc_callback = callback;
}

void *safe_malloc(size_t n)
{
    void *p;

    if (n == 0)
        n = 1; /* malloc(0) is not well defined. */
    p = malloc(n);
    if (p == NULL) {
        if (safe_malloc_callback)
            safe_malloc_callback(errno, n);
        exit(EXIT_FAILURE);
    }
    return p;
}

Статья Валери Авроры Программное обеспечение только для сбоя может светиться.

14 голосов
/ 18 апреля 2009

Посмотрите на другую сторону вопроса: если у вас malloc-память, она выходит из строя, и вы не обнаруживаете ее в malloc, когда обнаружит вы обнаружите ее?

Очевидно, что при попытке разыменовать указатель.

Как вы это обнаружите? Получив Bus error или что-то подобное, где-нибудь после malloc, что вам придется отследить дамп ядра и отладчик.

С другой стороны, вы можете написать

  #define OOM 42 /* just some number */

  /* ... */

  if((ptr=malloc(size))==NULL){
      /* a well-behaved fprintf should NOT malloc, so it can be used
       * in this sort of context
       */
      fprintf(stderr,"OOM at %s: %s\n", __FILE__, __LINE__);
      exit(OOM);
   }

и получите «OOM at parser.c: 447».

Вы выбираете.

Обновление

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

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

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

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

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

8 голосов
/ 18 апреля 2009

Независимо от платформы (за исключением, может быть, встроенных систем), неплохо проверить NULL, а затем просто выйти без выполнения какой-либо (или значительной) очистки вручную.

Недостаточно памяти - не простая ошибка. Это катастрофа в современных системах.

Книга Практика программирования (Brian W. Kernighan and Rob Pike, 1999) определяет функции, такие как emalloc(), которая просто выходит с сообщением об ошибке, если не осталось памяти.

6 голосов
/ 18 апреля 2009

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

Учтите это: программист использует вашу библиотеку. В его программе есть ошибка (возможно, неинициализированная переменная), которая передает глупый аргумент вашему коду, который, следовательно, пытается выделить один блок памяти объемом 3,6 ГБ. Очевидно, malloc() возвращает NULL. Предпочитает ли он необъяснимую ошибку сегмента, сгенерированную где-то в коде библиотеки, или возвращаемое значение, указывающее на ошибку?

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

Что касается Linux OOM killer, я слышал, что это поведение теперь отключено по умолчанию на основных дистрибутивах. Даже если он включен, не поймите неправильно: malloc() может вернуть NULL, и это, безусловно, произойдет, если общее использование памяти вашей программой превысит 4 ГБ (в 32-битной системе). Другими словами, даже если malloc() на самом деле не защищает вас некоторое пространство ОЗУ / подкачки, оно будет резервировать часть вашего адресного пространства.

4 голосов
/ 18 апреля 2009

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

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

Внимание! Перед запуском этого теста сохраните всю важную работу. Не запускайте его на рабочем сервере.

Относительно поведения Linux OOM - это на самом деле желательно и так работает большинство ОС. Важно понимать, что когда вы используете malloc () некоторую память, вы НЕ получаете ее непосредственно из ОС, вы получаете ее из библиотеки времени выполнения C. Это обычно запрашивает у ОС большой кусок памяти (или при первом запросе), которым она управляет через интерфейс malloc / free. Поскольку многие программы вообще никогда не используют динамическую память, было бы нежелательно, чтобы ОС передавала «реальную» память среде выполнения C - вместо этого она передает некий euncomited vM, который будет фактически выполняться, когда вы будете выполнять вызовы malloc.

2 голосов
/ 18 апреля 2009

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

Как только ОС начинает пейджинговую память, вся система становится все медленнее и медленнее, и, вероятно, пройдет довольно много времени, прежде чем ваше приложение действительно увидит NULL из malloc (если вообще).

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

1 голос
/ 19 апреля 2009

Хорошо. Все зависит от ситуации.

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

if (ptr == NULL) {
    fprintf(log /* stderr or anything */, "Cannot allocate memory");
    exit(2);
}

Хорошо. Даже если он не использует malloc, он может распределить буферы. Кроме того, это плохо, если это приложение с графическим интерфейсом - ваш пользователь вряд ли заметит это. Если ваш пользователь достаточно умный, чтобы запускать приложение из консоли для проверки ошибок, он, вероятно, увидит, что что-то съело всю его память. Хорошо. Так может быть отображать диалог? Но отображение диалога может израсходовать ресурсы - и обычно это будет.

Во-вторых, зачем вам информация о ООМ? Это происходит в двух случаях:

  1. Другое программное обеспечение глючит. С этим ничего не поделаешь
  2. Ваша программа глючит. В таком случае это более простая программа с графическим интерфейсом, в которой вы вряд ли будете уведомлять пользователя каким-либо образом (не говоря уже о том, что 99% пользователей не читают сообщения и скажут, что программное обеспечение зависло без дополнительных подробностей). Если это не так, пользователь, вероятно, все равно обнаружит это (отслеживание системных мониторов или использование более специализированного программного обеспечения).
  3. Чтобы освободить некоторые кэши и т. Д. Вы должны проверить систему, однако, имейте в виду, что она, вероятно, не будет работать. Вы можете работать только с собственным sbrk / mmap / и т. Д. звонки и в Linux вы все равно получите OOM
1 голос
/ 18 апреля 2009

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

Если ваш код не запускается с ограничением ресурсов или в системе с недостаточным объемом памяти и без подкачки, я думаю, что можно программировать с предположением, что malloc (3) будет успешным. Если вы не уверены, просто создайте пустышку, которая может когда-нибудь выполнить проверку и просто выйти. Возвращаемое значение состояния ошибки не имеет смысла, поскольку вашей программе требуется память, которая уже выделена. Если ваш malloc (3) завершается неудачно и вы не проверяете NULL, ваш процесс все равно умрет, когда начнет получать доступ к полученному указателю (NULL).

Проблемы с malloc (3) в большинстве случаев возникают не из-за нехватки памяти, а из-за логической ошибки в вашей программе, которая приводит к неправильным вызовам malloc и free. Эта обычная проблема не будет обнаружена при проверке успеха malloc.

1 голос
/ 18 апреля 2009

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

0 голосов
/ 18 апреля 2009

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

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

...