Подтвердить что ты не робот

Должен ли я беспокоиться об обнаружении ошибок OOM (из памяти) в моем C-коде?

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

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

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

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

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

4b9b3361

Ответ 1

Невыполнение условий памяти может случиться даже на современных компьютерах с большим объемом памяти, если пользователь или системный администратор ограничивают (см. 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;
}

Valerie Aurora article Программное обеспечение, предназначенное только для кражи, может освещаться.

Ответ 2

Посмотрите на другую сторону вопроса: если вы 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 в parser.c: 447".

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

Update

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

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

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

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

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

Ответ 3

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

Из памяти не является простой ошибкой. Это катастрофа на сегодняшних системах.

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

Ответ 4

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

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

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

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

Ответ 5

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

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

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

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

Ответ 6

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

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

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

Ответ 7

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

Ответ 8

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

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

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

Ответ 9

Ну. Все зависит от ситуации.

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

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

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

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

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

Ответ 10

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

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

Ответ 11

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

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