2012-01-29 14 views
18

Digamos que estoy escribiendo una pequeña biblioteca en C, alguna estructura de datos, por ejemplo. ¿Qué debo hacer si no puedo asignar memoria?C Diseño de API: ¿qué hacer cuando malloc devuelve NULL?

Puede ser bastante importante, p. Necesito un poco de memoria para inicializar la estructura de datos en primer lugar, o estoy insertando un par de clave-valor y quiero envolverlo en una pequeña estructura. También podría ser menos crítico, por ejemplo, algo así como una función pretty_print que crea una buena representación de cadena de los contenidos. Sin embargo, suele ser más grave que el error promedio: puede que no tenga sentido continuar. Una tonelada de usos de muestra de malloc en línea simplemente directamente salga del programa si devuelve NULL. Supongo que una gran cantidad de código de cliente real también lo hace: solo aparece algún error, o escríbalo en stderr, y aborta. (Y una gran cantidad de código real probablemente no verifique el valor de retorno de malloc en absoluto.)

A veces tiene sentido devolver NULL, pero no siempre. Los códigos de error (o simplemente algún valor booleano success), ya sea como valores devueltos o parámetros de salida funcionan bien, pero parece que pueden complicar o perjudicar la legibilidad de la API (entonces, de nuevo, ¿eso es algo esperado en un lenguaje como C?) Otra opción es tener algún tipo de estado de error interno que la persona que llama pueda consultar posteriormente, p. con una función get_error, pero luego debe tener cuidado con la seguridad de los hilos, y puede ser fácil pasarla por alto; las personas tienden a ser poco estrictas a la hora de buscar errores de todos modos, y si se trata de una función separada, es posible que no las conozcan o que no se molesten (pero supongo que ese es su problema).

(a veces me he visto envuelto en malloc una función que solo intenta de nuevo hasta que la memoria está disponible ...

void *my_malloc(size_t size) 
{ 
    void *result = NULL; 
    while (result == NULL) 
     result = malloc(size); 
    return result; 
} 

Pero eso parece un poco tonto y tal vez peligroso.)

¿Qué es un forma correcta de manejar esto?

+0

La salida elegante sería una opción mía. – Till

+3

@Till: Eso es absolutamente inaceptable para el código de la biblioteca. –

+0

@R. whoopy, salté por completo la primera línea al leer esta pregunta. En ese caso, devuelva FALSE/NULL y agregue una función de estado de error para más detalles. – Till

Respuesta

12

Si la asignación falla de una manera que impide el progreso hacia adelante, la única solución aceptable para código de biblioteca es hacer retroceder las asignaciones y otros cambios que ya se hayan realizado en la operación parcialmente completada y devolver un código de falla a llamador.Solo la aplicación de llamada puede saber cuál es la forma correcta de proceder. Algunos ejemplos:

  1. Un reproductor de música podría simplemente cancelar o volver a un estado inicial/detenido y esperar a que el usuario ingrese nuevamente.
  2. Un procesador de textos podría necesitar almacenar un volcado de emergencia del estado del documento actual en un archivo de recuperación y luego abortar.
  3. Un servidor de base de datos de alta gama podría necesitar rechazar y anular toda la transacción e informar al cliente.

Si usted sigue la idea advertido tantas veces pero al revés que su biblioteca sólo debe abortar la persona que llama en fracasos asignaciones, que o bien tienen muchos programas que determinan que no pueden utilizar su biblioteca por esta razón, los usuarios de la los programas que usan su biblioteca serán extremadamente enojados cuando una falla de asignación hace que sus datos valiosos sean desechados.

Editar: Una objeción alguna del campo de "abortar" levantarán contra mi respuesta es que, en los sistemas con overcommit, incluso llama a malloc que parecía tener éxito puede fallar cuando el núcleo intenta crear una instancia de almacenamiento físico para el memoria virtual asignada. Esto ignora el hecho de que cualquier persona que necesite alta confiabilidad se habrá deshabilitado en exceso, así como el hecho de que (al menos en sistemas de 32 bits) la falla de asignación es más probable debido al agotamiento del espacio de direcciones virtuales que el agotamiento del almacenamiento físico.

4

Simplemente devuelva un error de la forma en que lo hace normalmente. Ya que estamos hablando de una API, no sabes de qué entorno estás siendo llamado, así que simplemente devuelve NULL o sigue cualquier otro procedimiento de manejo de errores que ya utilices. No desea realizar un ciclo para siempre, ya que la persona que llama puede no necesitar realmente esa memoria y prefiere saber que no puede manejarla, o quizás la persona que llama tiene una interfaz de usuario a la que pueden enviar el error.

La mayoría de las API tendrán algún tipo de valor de retorno que indique un error en todas las funciones, otras API requieren que la persona que llama invoque una función especial "check_error" para determinar si hay un error. También puede querer que una función "get_error" devuelva una cadena de error que la persona que llama puede mostrar opcionalmente al usuario o incluir en un registro. Debe ser descriptivo: "Fulano de API ha encontrado un error en la función: no puede asignar memoria". O lo que sea. Es suficiente que cuando alguien obtiene el error, sepa qué componente lo arrojó, y cuando te envía un mensaje de correo electrónico con el mensaje de registro, sabes exactamente qué salió mal.

Por supuesto, también puede bloquearse, pero eso evita que la persona que llama cierre cualquier otra cosa que hayan estado haciendo y se vea feo, si su código tiene la costumbre de morir cuando se lo llama en lugar de devolver un error, las personas van a buscar una biblioteca que no intente activamente matar su programa.

1

Es difícil diseñar software para manejar limpiamente problemas de falta de memoria y continuar. Pocas aplicaciones reales hacen un intento serio de hacer esto. Como autor de la biblioteca, lo único razonable es informar un error a la persona que llama. (devolver una falla; lanzar una excepción; dependiendo del idioma, etc.)

Definitivamente no desea realizar un ciclo y bloquear para una biblioteca general. @R tiene un buen punto, si se produce una falla intente restaurar el estado a su condición original.

La gestión de la memoria insuficiente y los problemas de espacio en disco probablemente requieran coordinación en todas las partes de la aplicación. Es posible que desee tener memoria de contingencia preasignada. Probablemente bucle/vuelva a intentar mallocs como lo estaba intentando pero con algunas demoras intermedias con un tiempo de espera excedido. Realmente está más allá del alcance de una biblioteca típica.

2

En un lenguaje como Java o C#, la respuesta inequívoca es por lo general "¡lanza una excepción!".

En C, un enfoque común es manejar el error de forma síncrona (por ejemplo, con un código de resultado, y/o un valor de indicador como "nulo").

También se puede generar una señal asíncrona (muy similar a una "excepción" de Java ... o un "abort()" de mano dura). Con este enfoque, también puede permitir que el usuario instale un "controlador de errores" personalizado.

Aquí hay un ejemplo usando setjmp/longjmp:

Y aquí están algunas ideas interesantes:

y aquí es un buen dis cusión sobre las ventajas/desventajas de la utilización de las devoluciones de llamada C para el tratamiento de errores:

2

Puede escribir software que no necesita malloc en el primer lugar - cosas crítico segura.

Al comienzo de la ejecución que se asigna, solo se asegura de que se defina lo que necesita y de que los algoritmos no superen esas barreras.

Difícil, pero no imposible.

3

La API estándar de BLAS para llamadas de funciones de álgebra lineal utiliza un enfoque algo diferente de las sugerencias de "devolver un código de error" que se dan aquí: Llama a una función documentada específica, y luego regresa.

Ahora, la biblioteca también proporciona una implementación de esta función documentada, que imprime un mensaje de error útil y (cuando es posible) un seguimiento de pila, y luego cancela. Esa es una forma de manejar las cosas, y significa que el usuario ocasional no se encontrará con problemas extraños porque se olvidó de buscar un código de error.

Sin embargo, el punto crítico de que esto sea una función específica documentada es que significa que el usuario puede también optar por proporcionar su propia implementación de esa función, que reemplazará la predeterminada. Y esa implementación puede hacer muchas cosas: puede establecer un código de error global que luego el usuario verifique, o puede hacer algo que intente borrar algo de memoria y continuar.

Esa es una especie de solución de peso pesado, tanto para la implementación como para el usuario, pero en los casos en que un código de error obvio no es apropiado, proporciona una gran flexibilidad.

Editar para agregar algunos detalles más: La función BLAS es xerbla (o cblas_xerbla, en la interfaz C) con la expectativa de que se anularía en el tiempo del enlace - la suposición es la vinculación estática. También hay algunas notas relevantes sobre cómo se debe ajustar esto para las bibliotecas dinámicas en this header from Apple (ver los comentarios sobre SetBLASParamErrorProc, cerca de la parte inferior del archivo) - en el caso de enlace dinámico, una devolución de llamada debe registrarse en el tiempo de ejecución.

Vea también las notas útiles por "R." en los comentarios a continuación sobre cómo esta anulación es desafortunadamente global, lo que puede causar problemas si un usuario usa su biblioteca tanto directa como indirectamente a través de una segunda biblioteca, y tanto el usuario como la segunda biblioteca desean anular el manejador.

+0

Esa es una idea realmente buena. Siguiendo con el ejemplo de la estructura de datos, es posible que la persona que llama ya esté proporcionando devoluciones de llamadas para, p. liberación, comparación, hashing o conversión de valores a cadenas; además, una devolución de llamada para el manejo de errores tiene sentido. –

+1

Esto es realmente un diseño realmente malo porque es con estado (implica variables globales/estado), a menos que todas las funciones de la biblioteca tengan un argumento "puntero al contexto". Imagine el caso de una aplicación que usa tanto libfoo como libbar que usa indirectamente libfoo. Su aplicación establece la función de enlace para saber cómo lidiar con las fallas de asignación, y luego libbar reemplaza eso con su propio gancho. Ahora no se llama al controlador de su aplicación. No genial. Las bibliotecas nunca deberían tener este tipo de estado global; es extremadamente mal diseño. –

+0

Ah, no lo había pensado de esa manera; Estaba pensando en bibliotecas cuyas funciones se comportan más o menos como métodos en estructuras opacas (por ejemplo, 'mylib_append (mylib_list * l, void * element)'). En ese caso, la devolución de llamada sería un atributo de la estructura, también estaría actuando como el "contexto" que mencionas. –

3

Para una biblioteca, tiene dos opciones. Sin la cooperación de aplicaciones, prácticamente todo lo que puede hacer es pasar un error a la aplicación.

Con la cooperación de aplicaciones, puede hacer mucho más. Por ejemplo, puede ofrecerle a la aplicación que registre una devolución de llamada a la que llama su biblioteca cuando malloc devuelva NULL. Puede pasar la devolución de llamada a la cantidad de bytes que necesita y con qué urgencia los necesita. (Por ejemplo, "fallará totalmente en la operación", "tendrá que cancelar una operación", etc.). Entonces, el autor de la aplicación puede decidir si le da la memoria o no.

En el nivel de la aplicación, puede hacer mucho más. Por ejemplo, puede malloc un montón de bloques de memoria para usar como un "grupo de emergencia". Si malloc falla, puede liberar bloques de la agrupación e iniciar el deslastre de carga, la reducción de la memoria caché o cualquier otra opción que tenga para reducir el consumo de memoria.

Pero generalmente no se puede hacer mucho en una biblioteca, excepto informar a una aplicación cooperante.

Cuestiones relacionadas