como - ¿Debo molestarme en detectar errores OOM(falta de memoria) en mi código C?
malloc sizeof (11)
He dedicado una gran cantidad de líneas de código C a cleanup-labels / condicionales para la asignación de memoria fallida (indicada por la familia alloc
devuelve NULL
). Me enseñaron que esta era una buena práctica para que, en caso de fallas en la memoria, se pudiera marcar un estado de error apropiado y la persona que realizaba la llamada podría realizar una "limpieza elegante de la memoria" y volver a intentarlo. Ahora tengo algunas dudas sobre esta filosofía que espero aclarar.
Supongo que es posible que una persona que llama pueda desasignar el exceso de espacio en el búfer o despojar a los objetos relacionales de sus datos, pero me parece que el que llama rara vez tiene la capacidad (o está en el nivel de abstracción apropiado) para hacerlo. Además, el regreso temprano de la función llamada sin efectos secundarios a menudo no es trivial.
También descubrí el asesino OOM de Linux, que parece hacer que estos esfuerzos sean totalmente inútiles en mi plataforma de desarrollo principal.
Por defecto, Linux sigue una estrategia de asignación de memoria optimista. Esto significa que cuando malloc () devuelve un valor no nulo, no hay garantía de que la memoria esté realmente disponible. Este es un error realmente malo. En caso de que el sistema se quede sin memoria, el infame asesino OOM matará uno o más procesos.
Me imagino que probablemente haya otras plataformas que sigan el mismo principio. ¿Hay algo pragmático que haga que valga la pena verificar las condiciones de OOM?
Bien. Todo depende de la situación.
Ante todo. Si ha detectado que una memoria no es suficiente para su necesidad, ¿qué hará? El uso más común es:
if (ptr == NULL) {
fprintf(log /* stderr or anything */, "Cannot allocate memory");
exit(2);
}
Bien. Incluso si no utiliza malloc, puede asignar los búferes. Además, es una lástima si se trata de una aplicación GUI: es poco probable que el usuario la detecte. Si su usuario es "lo suficientemente listo" para ejecutar la aplicación desde la consola para verificar los errores, probablemente verá que algo se comió toda su memoria. De acuerdo. Entonces, ¿se puede mostrar un cuadro de diálogo? Pero mostrar el diálogo puede consumir recursos, y generalmente lo hará.
En segundo lugar, ¿por qué necesita la información sobre OOM? Sucede en dos casos:
- Otro software tiene errores. No puedes hacer nada con eso
- Tu programa tiene errores. En tal caso, es un programa de GUI más rápido en el que es poco probable que notifique al usuario de ninguna manera (sin mencionar que el 99% de los usuarios no lee los mensajes y dirá que el programa se bloqueó sin más detalles). Si no es así, es probable que el usuario lo detecte de todos modos (al utilizar los monitores del sistema o al usar un software más especializado).
- Para liberar algunos cachés, etc. Debe verificar el sistema, pero tenga cuidado de que no funcione. Puede manejar solo sbrk / mmap / etc. llamadas y en Linux obtendrá OOM de todos modos
Con las computadoras de hoy en día y la cantidad de RAM instalada normalmente, es probable que verifique en todas partes los errores de asignación de memoria sea demasiado detallado. Como ha visto, a menudo es difícil o imposible tomar una decisión racional sobre qué desasignar. Como su proceso está asignando más y más memoria, el sistema operativo reducirá la cantidad de memoria disponible para los almacenamientos intermedios de disco. Cuando eso cae por debajo de algún umbral, entonces el sistema operativo comenzará la búsqueda de memoria en el disco. (Esto es una simplificación, ya que hay muchos factores en la administración de la memoria).
Una vez que el sistema operativo inicia la paginación de la memoria, todo el sistema se vuelve progresivamente más lento y más lento, y es probable que pase bastante tiempo hasta que la aplicación realmente vea un NULL desde malloc (si es que lo hace).
Con la gran cantidad de memoria disponible en los sistemas actuales, un error de "falta de memoria" significa que un error en su código intentó asignar una cantidad arbitraria de memoria. En ese caso, ninguna cantidad de liberación y reintentos por parte de su proceso solucionará el problema.
Depende de lo que estés escribiendo. ¿Es una biblioteca de propósito general? Si es así, quiere lidiar con la falta de memoria de la forma más elegante posible, particularmente si es razonable esperar que se use en sistemas el-cheapo o en dispositivos integrados.
Considere esto: un programador está usando su biblioteca. Hay un error (tal vez una variable no inicializada) en su programa que transmite un argumento tonto a su código, que consecuentemente trata de asignar un solo bloque de 3.6 GB de memoria. Obviamente malloc()
devuelve NULL. ¿Prefiere una segfault inexplicada generada en algún lugar del código de la biblioteca, o un valor de retorno para indicar el error?
Para evitar tener comprobaciones de errores en todo su código, un enfoque es asignar una cantidad razonable de memoria al inicio, y subasignarla según sea necesario.
Con respecto al asesino OOM de Linux, escuché que este comportamiento ahora está deshabilitado por defecto en las principales distribuciones. Incluso si está habilitado, no se haga una idea equivocada: malloc()
puede devolver NULL, y ciertamente lo hará si el uso total de memoria de su programa superaría 4GiB (en un sistema de 32 bits). En otras palabras, incluso si malloc()
realidad no le asegura algo de espacio de RAM / intercambio, se reservará parte de su espacio de direcciones.
Independientemente de la plataforma (excepto tal vez los sistemas integrados), es una buena idea buscar NULL
y luego simplemente salir sin realizar ninguna (o mucha) limpieza a mano.
La falta de memoria no es un simple error. Es una catástrofe en los sistemas de hoy.
El libro La práctica de la programación (Brian W. Kernighan y Rob Pike, 1999) define funciones como emalloc()
que simplemente sale con un mensaje de error si no queda memoria.
Las condiciones de falta de memoria pueden suceder incluso en computadoras modernas con mucha memoria, si el usuario o administrador del sistema restringe (vea ulimit) el espacio de memoria para un proceso, o el sistema operativo admite límites de asignación de memoria por usuario. En casos patológicos, la fragmentación hace que esto sea bastante probable, incluso.
Sin embargo, dado que el uso de la memoria asignada dinámicamente prevalece en los programas modernos, por buenas razones, se vuelve muy difícil manejar los errores de falta de memoria. Controlar y manejar errores de este tipo tendría que hacerse en todas partes, a un alto costo de complejidad.
Me parece que es mejor diseñar el programa para que pueda bloquearse en cualquier momento. Por ejemplo, asegúrese de que los datos que el usuario ha creado se guarden en el disco todo el tiempo, incluso si el usuario no los guarda explícitamente. (Consulte vi -r, por ejemplo). De esta forma, puede crear una función para asignar memoria que finaliza el programa si hay un error. Dado que su aplicación está diseñada para manejar bloqueos en cualquier momento, está bien que bloquee. El usuario se sorprenderá, pero no perderá (mucho) trabajo.
La función de asignación ininterrumpida podría ser algo como esto (código sin comprimir, no compilado, solo para fines de demostración):
/* 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;
}
El artículo de Valerie Aurora sobre el software Crash-only podría ser esclarecedor.
Los procesos usualmente se ejecutan con un límite de recursos (vea ulimit (3)) en el tamaño de la pila, pero no en el tamaño del montón. malloc (3) administrará el aumento de memoria de su área de almacenamiento página por página desde el sistema operativo, y el sistema operativo hará los arreglos para que esta página se asigne de alguna manera físicamente y corresponda a su pila para su proceso. Si no hay más memoria RAM en su computadora, la mayoría de los sistemas operativos tienen algo así como una partición de intercambio en el disco. Cuando su sistema comienza a necesitar el cambio, las cosas gradualmente se vuelven lentas. Si un proceso lleva a esto, puede identificarse fácilmente con alguna utilidad como ps (1).
A menos que su código se ejecute con un límite de recursos o en un sistema con un tamaño de memoria deficiente y sin intercambio, creo que uno puede programar asumiendo que malloc (3) tiene éxito. Si no está seguro, simplemente haga una envoltura ficticia que algún día pueda hacer el control y simplemente salga. Un valor de retorno de estado de error no tiene sentido, ya que su programa requiere la memoria que ya tiene asignada. Si tu malloc (3) falla y no verificas NULL, tu proceso morirá de todos modos cuando comience a acceder al puntero (NULL) que obtuvo.
Los problemas con malloc (3) en la mayoría de los casos no surgen de la falta de memoria, sino de un error lógico en su programa que conduce a llamadas mal ejecutadas a malloc y gratuitas. Este problema habitual no se detectará al verificar el éxito de malloc.
Mira el otro lado de la pregunta: si malloc la memoria, falla, y no la detectas en el malloc, ¿cuándo la detectarás?
Obviamente, cuando intenta desreferenciar el puntero.
¿Cómo lo detectarás? Al obtener un Bus error
o algo similar, en algún lugar después del malloc tendrá que rastrear con un volcado de núcleo y el depurador.
Por otro lado, puedes escribir
#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);
}
y obtener "OOM en parser.c: 447".
Usted escoge.
Actualizar
Buena pregunta sobre el retorno elegante. La dificultad de asegurar un retorno elegante es que, en general, no se puede establecer un paradigma o patrón de cómo se hace eso, especialmente en C, que es, después de todo, un lenguaje de ensamblaje elegante. En un entorno recolectado de basura, puede forzar un GC; en un idioma con excepciones, puede lanzar una excepción y relajar cosas. En C tienes que hacerlo tú mismo y debes decidir cuánto esfuerzo deseas poner en él.
En la mayoría de los programas, la terminación anormal es lo mejor que puedes hacer. En este esquema, (con suerte) recibirá un mensaje útil en stderr, por supuesto, también podría ser para un registrador o algo así, y un valor conocido como el código de retorno.
Los programas de alta confiabilidad con tiempos de recuperación cortos lo empujan a algo así como a los bloques de recuperación , donde se escribe un código que intenta hacer que un sistema regrese a un estado de supervivencia. Estos son geniales, pero complicados; el documento con el que me relacioné habla sobre ellos en detalle.
En el medio, puede llegar a un esquema de administración de memoria más complicado, por ejemplo administrar su propio conjunto de memoria dinámica; después de todo, si alguien más puede escribir malloc, usted también puede hacerlo.
Pero simplemente no hay un patrón general (del que estoy consciente de todos modos) para limpiar lo suficiente como para poder regresar de manera confiable y permitir que el programa circundante continúe.
Sí, creo que sí, si sigues la práctica de manera consistente. Esto puede ser poco práctico para un programa grande escrito en C debido al grado de trabajo manual que esto puede requerir, pero en un lenguaje más moderno, la mayor parte de este trabajo se realiza porque una condición de falta de memoria produce una excepción.
Los beneficios de hacer esto consistentemente son que el programa no entrará en un estado indefinido debido a la condición de falta de memoria que da como resultado un desbordamiento del búfer (esto obviamente deja la posibilidad de un estado indefinido debido a una salida temprana de la función, aunque esto es una clase diferente de error). Una vez hecho esto, su programa puede manejar consistentemente la condición de error, o si la falla fue crítica, decida renunciar de manera elegante.
Sugiero un experimento: escriba un pequeño programa que siga asignando memoria sin liberarla y luego imprima un pequeño mensaje (fijo) cuando falle la asignación. ¿Qué efectos notas en tu sistema cuando ejecutas este programa? ¿Alguna vez se imprime el mensaje?
Si el sistema se comporta normalmente y sigue siendo receptivo hasta el momento en que se muestra el error, entonces diría que sí, vale la pena verificarlo. OTOH, si el sistema se vuelve lento, no responde y, por lo general, inutilizable antes de que se muestre el mensaje (si es que alguna vez lo está), entonces II diría que no, no vale la pena buscarlo.
Importante: antes de ejecutar esta prueba, guarde todo el trabajo importante. No lo ejecute en un servidor de producción.
Respecto al comportamiento de OOM de Linux, esto es realmente deseable y es la forma en que funcionan la mayoría de los sistemas operativos. Es importante darse cuenta de que cuando malloc () parte de la memoria NO la está obteniendo directamente del SO, la obtiene de la biblioteca C runtime. Esto normalmente le habrá pedido al sistema operativo una gran cantidad de memoria por adelantado (o en la primera solicitud) que luego gestionará a través de la interfaz malloc / free. Como muchos programas nunca usan memoria dinámica, sería indeseable que el sistema operativo entregue memoria "real" al tiempo de ejecución de C, sino que entrega un vM euncomitted que realmente se realizará cuando realice sus llamadas malloc.
Tienes que sopesar qué es mejor o peor para ti: poner todo el trabajo en buscar OOM o hacer que tu programa falle en momentos inesperados
Verificar las condiciones de OOM y tomar las medidas adecuadas puede ser difícil si diseña mal el software. Si realmente necesita verificar estas situaciones depende de la confiabilidad del software que desea obtener.
Por ejemplo, el hipervisor de VirtualBox detectará errores de falta de memoria y pausará con gracia la máquina virtual, lo que permitirá al usuario cerrar algunas aplicaciones para liberar memoria. Observé tal comportamiento en Windows. En realidad, casi todas las llamadas en VirtualBox tienen un indicador de éxito como valor de retorno y usted puede simplemente devolver VERR_NO_MEMORY
para denotar que la asignación de memoria falló. Esto introduce algunos controles adicionales, pero en este caso vale la pena.