¿Deberíamos usar exit () en C?

80

Hay dudas sobre el uso exiten C ++. La respuesta discute que no es una buena idea principalmente debido a RAII, por ejemplo, si exitse llama en algún lugar del código, no se llamará a los destructores de objetos, por lo tanto, si, por ejemplo, un destructor estaba destinado a escribir datos en un archivo, esto no sucederá , porque no se llamó al destructor.

Me interesaba saber cómo es esta situación en C. ¿Son aplicables cuestiones similares también en C? Pensé que dado que en C no usamos constructores / destructores, la situación podría ser diferente en C. Entonces, ¿está bien usarlo exiten C?

He visto funciones como las siguientes, que encuentro bien para usar en algunos casos, pero estaba interesado si tenemos problemas similares en C con el uso exit, como se describe anteriormente con C ++. (lo que haría que usar funciones como las que se muestran a continuación no sea una buena idea).

gmoniava
fuente
2
"No se llamará a los destructores de objetos" - Eso no es del todo correcto (ver: cplusplus.com/reference/cstdlib/exit ). Está pensando en quick_exit (consulte: cplusplus.com/reference/cstdlib/quick_exit/?kw=quick_exit ).
Codificador
También puede tener algunos problemas específicos del sistema operativo, por ejemplo, el papel convencional de la SIGTERMseñal en POSIX y Linux ... Se espera que los servidores con buen comportamiento lo manejen bien. Y debe evitar el uso SIGKILL(es decir, intente kill -TERMentonces kill -QUITy solo más tarde kill -KILLcomo administrador de sistemas)
Basile Starynkevitch
20
¿Cómo no es esto un duplicado casi 7 años después del lanzamiento de Stack Overflow?
Peter Mortensen
7
@PeterMortensen: es sorprendente, pero no conozco una buena alternativa que esto duplique. El uso de la exit()función más votado no es pertinente (y el alto número de votos es sorprendente, no es una buena pregunta). ¿Por qué no debería usar la función exit () en C? Sería un buen candidato si tuviera respuestas del calibre de esta pregunta. No es así; un cierre inverso de eso como un duplicado de esto es apropiado, y lo he hecho.
Jonathan Leffler
No estoy seguro de que quiera usar 'printf' en caso de error. La función usa búferes, pero si tiene una memoria dañada, es posible que esos búferes no sean buenos. Es posible que desee utilizar 'write (2, "ERROR:", 7); escribir (2, mensaje, strlen (mensaje));
Alex

Respuestas:

82

En lugar de ello abort(), la exit()función en C se considera una salida "elegante".

De C11 (N1570) 7.22.4.4/p2 La función de salida (el énfasis es mío):

La exitfunción provoca la terminación normal del programa.

El Estándar también dice en 7.22.4.4/p4 que:

A continuación, se vacían todos los flujos abiertos con datos almacenados en búfer no escritos , se cierran todos los flujos abiertos y tmpfilese eliminan todos los archivos creados por la función.

También vale la pena mirar los archivos 7.21.3 / p5 :

Si la mainfunción vuelve a su llamador original, o si exit se llama a la función, todos los archivos abiertos se cierran (por lo tanto, todos los flujos de salida se vacían) antes de la finalización del programa. Otras rutas para la terminación del programa, como llamar a la abortfunción, no necesitan cerrar todos los archivos correctamente.

Sin embargo, como se menciona en los comentarios a continuación, no puede asumir que cubrirá todos los demás recursos , por lo que es posible que deba recurrir atexit()y definir devoluciones de llamada para su lanzamiento individualmente. De hecho, es exactamente lo que atexit()se pretende hacer, como dice 7.22.4.2/p2 La función atexit :

La atexitfunción registra la función apuntada por func, para ser llamada sin argumentos en la terminación normal del programa.

En particular, el estándar C no dice con precisión qué debería suceder con los objetos de duración de almacenamiento asignada (es decir malloc()), por lo que requiere que usted sepa cómo se hace en una implementación particular. Para los sistemas operativos modernos orientados a host, es probable que el sistema se encargue de ello, pero aún así, es posible que desee manejar esto usted mismo para silenciar los depuradores de memoria como Valgrind .

Grzegorz Szpetkowski
fuente
3
Ésta me parece la respuesta correcta. Una conexión de socket se cierra al salir porque la finalización del proceso cierra todos los descriptores de archivo, estén abiertos o no con stdio, es decir, es equivalente a a close()en el FD. No sé en qué sentido @BlueMoon piensa que eso es incorrecto, pero no es menos incorrecto que una llamada a close(), y si se requiere una mayor aclaración, para eso es precisamente atexit().
abligh el
1
@abligh Los sistemas operativos modernos cerrarán todos los fds asociados con ese proceso; pero eso es algo brutal y no estándar (lo que dice Blue Moon).
edmz
3
@black si se necesita más aclaración, atexit()se puede usar. Usarse a exit()sí mismo no es más brutal que hacerlo returndesde main().
abligh el
5
Escribir software 'adecuado' usando 'buenas prácticas' es la razón por la que tengo tantas aplicaciones que no se apagan inmediatamente cuando se lo piden. :( Si el sistema operativo puede hacer algo, debe dejar que lo haga en lugar de perder el tiempo con el código de usuario tratando de hacer algo que ya existe, que ya está probado y ya depurado. Si su sistema de software no puede soportar un apagado repentino e inesperado, (por ejemplo, desde algún hilo que solicita una terminación inmediata en el proceso, Administrador de tareas 'Finalizar proceso', 'matar -9 'o falla de energía), es de mala calidad de todos modos.
Martin James
Sería bueno si puede agregar, si es posible, a su respuesta qué tipo de recursos podría necesitar atexit, de lo contrario, está bien. @BlueMoon: No creo que usar una función como diesignifica que uno no puede programar, en varias ocasiones uno podría querer usarla. De lo contrario, de hecho, también se puede manejar esto usando sólo los valores de retorno, etc.
gmoniava
23

Sí, está bien usarlo exiten C.

Para garantizar todos los búferes y un cierre ordenado y elegante, se recomienda utilizar esta función atexit, más información sobre esto aquí

Un código de ejemplo sería así:

Ahora, siempre que exitse llame, la función cleanupse ejecutará, lo que puede albergar un cierre elegante, limpiar búferes, memoria, etc.

t0mm13b
fuente
@IlmariKaronen La memoria asignada no se maneja automáticamente de ninguna manera. Es el sistema operativo el que podría asegurarse de que la memoria se recupere al sistema operativo. El argumento de Grzegorz Szpetkowski se opone a su afirmación: en particular, el estándar C no dice con precisión qué debería suceder con los objetos de duración de almacenamiento asignada (iemalloc ()), por lo que requiere que sepa cómo se hace en una implementación particular. Para los sistemas operativos modernos orientados a host, es probable que el sistema se encargue de ello, pero aún así, es posible que desee manejar esto usted mismo para silenciar los depuradores de memoria como Valgrind.
este
15

No tiene constructores ni destructores, pero podría tener recursos (por ejemplo, archivos, flujos, sockets) y es importante cerrarlos correctamente. Un búfer no se pudo escribir de forma síncrona, por lo que salir del programa sin cerrar correctamente el recurso primero podría provocar daños.

enrico.bacis
fuente
8
Esta respuesta es un gran ejemplo de "no aprenda un lenguaje de programación; aprenda a programar". Por lo general, no puede evitar comprender el problema eligiendo un idioma diferente.
Kerrek SB
10
Creo que esto es incorrecto. Si llama exit()(en lugar de _exit()), atexitse llaman las rutinas y el stdioalmacenamiento en búfer se descarga en el disco. exit()está ahí precisamente para permitir una salida ordenada del programa.
Abligh el
2
@abligh Los sistemas en tiempo real no liberan la memoria mal ubicada en exit (), por ejemplo, lo que genera fugas. La memoria compartida POSIX es otro ejemplo. Por lo tanto, depende del entorno en lugar de seguir estrictamente el estándar C.
PP
2
@abligh: no todos los recursos son una biblioteca estándar. Si bien ciertamente tiene razón en que la biblioteca estándar ofrece ciertas garantías, aún debe pensar en el flujo de control global de su programa y las responsabilidades de cada una de sus partes, y asegurarse de que cada responsabilidad pendiente se trate adecuadamente. El término "recurso" es un término conciso para esta noción general y trasciende cualquier biblioteca en particular, y el manejo de los recursos es, en última instancia, responsabilidad del programador. Instalaciones como atexitpueden ayudar, aunque no siempre son adecuadas.
Kerrek SB
2
@abligh: Por supuesto, puede usarlo exitsi lo desea, pero es un salto global en el control que viene con todas las desventajas del control no estructurado que hemos discutido. Como una forma de terminar de una condición de falla, puede ser apropiado (y parece ser lo que quiere el OP), aunque para el flujo de control normal probablemente preferiría diseñar el bucle principal con una condición de salida adecuada para que realmente termine volviendo de main.
Kerrek SB
10

El uso exit()está bien

Dos aspectos importantes del diseño de código que aún no se han mencionado son el "subproceso" y las "bibliotecas".

En un programa de un solo subproceso, en el código que está escribiendo para implementar ese programa, el uso exit()está bien. Mis programas lo usan de forma rutinaria cuando algo sale mal y el código no se recupera.

Pero…

Sin embargo, llamar exit()es una acción unilateral que no se puede deshacer. Es por eso que tanto el "subproceso" como las "bibliotecas" requieren una reflexión cuidadosa.

Programas enhebrados

Si un programa tiene varios subprocesos, entonces usar exit()es una acción dramática que termina todos los subprocesos. Probablemente sea inapropiado salir de todo el programa. Puede ser apropiado salir del hilo y reportar un error. Si conoce el diseño del programa, entonces tal vez esa salida unilateral esté permitida, pero en general, no será aceptable.

Código de biblioteca

Y esa cláusula "consciente del diseño del programa" también se aplica al código de las bibliotecas. Rara vez es correcto llamar a una función de biblioteca de propósito general exit(). Estaría muy molesto si una de las funciones estándar de la biblioteca de C no regresara solo por un error. (Obviamente, las funciones como exit(), _Exit(), quick_exit(), abort()no están destinados a retorno; que es diferente). Las funciones de la biblioteca C, por lo tanto, ya sea "no pueden fallar" o devolver una indicación de error de alguna manera. Si está escribiendo código para ir a una biblioteca de propósito general, debe considerar cuidadosamente la estrategia de manejo de errores para su código. Debe encajar con las estrategias de manejo de errores de los programas con los que se pretende usar, o el manejo de errores puede ser configurable.

Tengo una serie de funciones de biblioteca (en un paquete con encabezado "stderr.h", un nombre que camina sobre hielo fino) que están destinadas a salir ya que se usan para informar de errores. Esas funciones salen por diseño. Hay una serie de funciones relacionadas en el mismo paquete que informan de errores y no salen. Las funciones salientes se implementan en términos de las funciones no salientes, por supuesto, pero eso es un detalle de implementación interna.

Tengo muchas otras funciones de biblioteca, y muchas de ellas se basan en el "stderr.h"código para informar de errores. Esa es una decisión de diseño que tomé y estoy de acuerdo. Pero cuando los errores se reportan con las funciones que salen, limita la utilidad general del código de la biblioteca. Si el código llama a las funciones de informe de errores que no salen, entonces las rutas del código principal en la función tienen que lidiar con los retornos de error de manera sensata: detectelos y transmita una indicación de error al código de llamada.


El código para mi paquete de informes de errores está disponible en mi repositorio SOQ (Stack Overflow Questions) en GitHub como archivos stderr.cy stderr.hen el subdirectorio src / libsoq .

Jonathan Leffler
fuente
Eso está bien apuntado. Puede ver aquí un ejemplo de biblioteca que llama abort()cuando es necesario asignar memoria por malloc()o realloc(), Imagine, que tiene una aplicación, que se vincula con 100 bibliotecas y se pregunta cuál y cómo se bloqueó su aplicación. Es más, no he encontrado ninguna mención abort()en su documentación (pero no me malinterpreten. Es una gran biblioteca para su propósito).
Grzegorz Szpetkowski
@GrzegorzSzpetkowski Y ni siquiera llama manualmente a flush después de fprinting a stderr. ¡Ay!
este
1
@this: No debería ser necesario hacerlo. La salida de stderrnormalmente se almacena en búfer de línea. Si la salida termina con una nueva línea, el sistema la eliminará de todos modos.
Jonathan Leffler
@JonathanLeffler Confiar en el usuario, no es un movimiento inteligente.
este
@this: No, confiando en que la implementación se adhiera a los requisitos del estándar C: §7.21.3 ¶7 Al inicio del programa, tres flujos de texto están predefinidos y no es necesario abrirlos explícitamente: entrada estándar (para leer la entrada convencional), estándar salida (para escribir salida convencional) y error estándar (para escribir salida de diagnóstico). Tal como se abrió inicialmente, el flujo de errores estándar no se almacena completamente en búfer; ... Requiere codificación activa por parte del programador para hacer que el error estándar esté completamente almacenado en el búfer (y no hay ayuda para nadie si el programador es tan obtuso, excepto "no use el código").
Jonathan Leffler
6

Una razón para evitar exiten funciones que no sean main()es la posibilidad de que su código se saque de contexto. Recuerde, la salida es un tipo de flujo de control no local . Como excepciones inalcanzables.

Por ejemplo, puede escribir algunas funciones de administración de almacenamiento que se cierran en un error crítico del disco. Entonces alguien decide trasladarlos a una biblioteca. Salir de una biblioteca es algo que hará que el programa que realiza la llamada salga en un estado inconsistente para el que puede que no esté preparado.

O puede ejecutarlo en un sistema integrado. No hay ningún lugar para salir a , todo el asunto se ejecuta en un while(1)bucle main(). Puede que ni siquiera esté definido en la biblioteca estándar.

pjc50
fuente
2
Y a la gente no se le debería permitir tener cuchillos de cocina, porque alguien podría agarrar un puñado de ellos e intentar hacer malabares con ellos, o jugar a "atrapar" con su hijo. Evidentemente, no estoy de acuerdo. Si alguien decide copiar mi código en su programa, y ​​mi código resulta que no se ajusta a sus propósitos, ese es su problema, por no leer el código que está asimilando.
G-Man dice 'Reincorporar a Monica'
3
Una analogía bastante burda. C está lleno de cosas que se pueden hacer pero que probablemente deberían evitarse. De ahí la existencia del IOCCC. Tenga en cuenta que mi publicación no dice "no debería".
pjc50
2

Dependiendo de lo que esté haciendo, salir puede ser la forma más lógica de salir de un programa en C. Sé que es muy útil para verificar que las cadenas de devoluciones de llamada funcionen correctamente. Tome este ejemplo de devolución de llamada que utilicé recientemente:

En el ciclo principal, configuro todo para este sistema y luego espero en un ciclo while (1). Es posible crear una bandera global para salir del ciclo while, pero esto es simple y hace lo que debe hacer. Si está tratando con búferes abiertos, como archivos y dispositivos, debe limpiarlos antes de cerrar para mantener la coherencia.

Dom
fuente
-1

Es terrible en un gran proyecto cuando cualquier código puede salir excepto coredump. Trace es muy importante para mantener un servidor en línea.

usuario4531555
fuente