Sollten wir exit () in C verwenden?

79

Es gibt Fragen zur Verwendung exitin C ++. In der Antwort wird diskutiert, dass es hauptsächlich aufgrund von RAII keine gute Idee ist, z. B. wenn exitirgendwo im Code aufgerufen wird, werden Destruktoren von Objekten nicht aufgerufen. Wenn beispielsweise ein Destruktor Daten in eine Datei schreiben sollte, geschieht dies nicht , weil der Destruktor nicht aufgerufen wurde.

Ich war interessiert, wie ist diese Situation in C. Sind ähnliche Probleme auch in C anwendbar? Ich dachte, da wir in C keine Konstruktoren / Destruktoren verwenden, könnte die Situation in C anders sein. Ist es also in Ordnung, exitin C zu verwenden?

Ich habe Funktionen wie die folgenden gesehen, die ich in einigen Fällen gut gebrauchen kann, war aber interessiert, ob wir in C ähnliche Probleme mit der Verwendung haben exit, wie oben mit C ++ beschrieben? (was die Verwendung von Funktionen wie unten keine gute Idee machen würde.)

void die(const char *message)
{
    if(errno) {
        perror(message);
    } else {
        printf("ERROR: %s\n", message);
    }

    exit(1);
}
Giorgim
quelle
2
"Destruktoren von Objekten werden nicht aufgerufen" - Das ist nicht ganz richtig (siehe: cplusplus.com/reference/cstdlib/exit ). Sie denken an quick_exit (siehe: cplusplus.com/reference/cstdlib/quick_exit/?kw=quick_exit ).
Coder
Möglicherweise haben Sie auch einige betriebssystemspezifische Probleme, z. B. die herkömmliche Rolle des SIGTERMSignals unter POSIX und Linux. Von gut erzogenen Servern wird erwartet, dass sie gut damit umgehen. Und Sie sollten es vermeiden SIGKILL(dh versuchen Sie es kill -TERMdann kill -QUITund erst später kill -KILLals Systemadministrator)
Basile Starynkevitch
20
Wie ist dies nicht ein Duplikat fast 7 Jahre nach dem Start von Stack Overflow?
Peter Mortensen
7
@PeterMortensen: Es ist überraschend, aber mir ist keine gute Alternative bekannt, die dies dupliziert. Die Verwendung von exit()Funktionen mit hohen Stimmen ist nicht von Belang (und die hohe Stimmenzahl ist überraschend - es ist keine gute Frage). Warum sollte ich die Funktion exit () in C nicht verwenden? wäre ein guter Kandidat, wenn es Antworten vom Kaliber dieser Frage hätte. Es tut nicht; Ein umgekehrter Abschluss davon als Duplikat davon ist angemessen - und ich habe es getan.
Jonathan Leffler
Ich bin nicht sicher, ob Sie im Fehlerfall 'printf' verwenden möchten. Die Funktion verwendet Puffer, aber wenn Sie eine Speicherbeschädigung hatten, sind diese Puffer möglicherweise nicht gut. Vielleicht möchten Sie einfach 'write (2, "ERROR:", 7) verwenden; schreiben (2, Nachricht, strlen (Nachricht));
Alex

Antworten:

82

Stattdessen abort()wird die exit()Funktion in C als "anmutiger" Ausgang betrachtet.

Ab C11 (N1570) 7.22.4.4/p2 Die Exit-Funktion (Hervorhebung meiner):

Die exitFunktion bewirkt eine normale Programmbeendigung.

Der Standard sagt auch in 7.22.4.4/p4, dass:

Als nächstes werden alle offenen Streams mit ungeschriebenen gepufferten Daten gelöscht , alle offenen Streams werden geschlossen und alle von der tmpfileFunktion erstellten Dateien werden entfernt.

Es lohnt sich auch einen Blick auf 7.21.3 / p5- Dateien zu werfen :

Wenn die mainFunktion zu ihrem ursprünglichen Aufrufer zurückkehrt oder wenn die exit Funktion aufgerufen wird, werden alle geöffneten Dateien vor dem Beenden des Programms geschlossen (daher werden alle Ausgabestreams geleert). Andere Pfade zur Programmbeendigung, z. B. das Aufrufen der abortFunktion, müssen nicht alle Dateien ordnungsgemäß schließen.

Wie in den Kommentaren unten erwähnt, können Sie jedoch nicht davon ausgehen, dass alle anderen Ressourcen abgedeckt werden. Daher müssen Sie möglicherweise auf atexit()Rückrufe für deren Freigabe zurückgreifen und diese individuell definieren. Tatsächlich ist es genau das, was atexit()beabsichtigt ist, wie es in 7.22.4.2/p2 heißt. Die atexit-Funktion :

Die atexitFunktion registriert die Funktion, auf die von verwiesen wird func, um bei normaler Programmbeendigung ohne Argumente aufgerufen zu werden.

Insbesondere sagt der C-Standard nicht genau vor, was mit Objekten mit zugeordneter Speicherdauer (dh malloc()) geschehen soll , sodass Sie wissen müssen, wie dies bei einer bestimmten Implementierung geschieht. Für moderne, hostorientierte Betriebssysteme ist es wahrscheinlich, dass sich das System darum kümmert, aber dennoch möchten Sie dies möglicherweise selbst erledigen, um Speicher-Debugger wie Valgrind zum Schweigen zu bringen .

Grzegorz Szpetkowski
quelle
3
Dies scheint mir die richtige Antwort zu sein. Eine Socket-Verbindung wird beim Beenden geschlossen, da das Prozessende alle Dateideskriptoren schließt, unabhängig davon, ob sie mit geöffnet wurden oder nicht stdio, dh einem close()auf dem FD entspricht. Ich weiß nicht, in welchem ​​Sinne @BlueMoon das für falsch hält, aber es ist nicht weniger falsch als ein Anruf bei close(), und wenn weitere Aufklärung erforderlich ist, ist genau das der Zweck atexit().
Abligh
1
@abligh Moderne Betriebssysteme schließen alle mit diesem Prozess verbundenen FDS. Aber das ist etwas Brutales und nicht Standard (was Blue Moon sagt).
Edmz
3
@black kann verwendet werden, wenn weitere Aufräumarbeiten erforderlich atexit()sind. Mit exit()selbst ist nicht brutaler als tun returnaus main().
Abligh
5
Das Schreiben von "richtiger" Software unter Verwendung von "bewährten Methoden" ist der Grund, warum ich so viele Apps habe, die nicht sofort heruntergefahren werden, wenn Sie dazu aufgefordert werden. :( Wenn das Betriebssystem etwas kann, sollten Sie es tun lassen, anstatt mit Benutzercode herumzuspielen Der Versuch, etwas zu tun, das bereits vorhanden ist, ist bereits getestet und bereits getestet. Wenn Ihr Softwaresystem einem plötzlichen, unerwarteten Herunterfahren nicht standhalten kann (z. B. von einem Thread, der eine sofortige Beendigung des Prozesses erfordert, Task-Manager 'Prozess beenden', 'Beenden') -9 'oder Stromausfall), es ist sowieso von schlechter Qualität.
Martin James
Es ist schön, wenn Sie Ihrer Antwort hinzufügen können, welche Art von Ressourcen möglicherweise freigegeben werden müssen atexit, andernfalls ist dies in Ordnung. @BlueMoon: Ich glaube nicht, dass man Funktionen verwenden diekann, die nicht programmieren können. In einigen Fällen möchte man sie möglicherweise verwenden. Andernfalls können Sie dies auch nur mit Rückgabewerten usw. behandeln
giorgim
23

Ja, es ist in Ordnung, exitin C zu verwenden .

Um alle Puffer und ein ordnungsgemäßes Herunterfahren zu gewährleisten, wird empfohlen, diese Funktion zu verwenden atexit. Weitere Informationen hierzu finden Sie hier

Ein Beispielcode wäre wie folgt:

void cleanup(void){
   /* example of closing file pointer and free up memory */
   if (fp) fclose(fp);
   if (ptr) free(ptr);
}

int main(int argc, char **argv){
   /* ... */
   atexit(cleanup);
   /* ... */
   return 0;
}

Jetzt wird bei jedem exitAufruf die Funktion cleanupausgeführt, die ein ordnungsgemäßes Herunterfahren, Bereinigen von Puffern, Speicher usw. ermöglichen kann.

t0mm13b
quelle
@IlmariKaronen Der zugewiesene Speicher wird in keiner Weise automatisch behandelt. Es ist das Betriebssystem, das sicherstellen kann , dass der Speicher an das Betriebssystem zurückgegeben wird. Das Argument von Grzegorz Szpetkowski widerspricht Ihrer Aussage: Insbesondere sagt der C-Standard nicht genau vor, was mit Objekten mit zugeordneter Speicherdauer (iemalloc ()) geschehen soll, sodass Sie wissen müssen, wie dies bei einer bestimmten Implementierung geschieht. Für moderne, hostorientierte Betriebssysteme ist es wahrscheinlich, dass sich das System darum kümmert, aber dennoch möchten Sie dies möglicherweise selbst erledigen, um Speicher-Debugger wie Valgrind zum Schweigen zu bringen.
dieser
15

Sie haben keine Konstruktoren und Destruktoren, aber Sie könnten Ressourcen (z. B. Dateien, Streams, Sockets) haben, und es ist wichtig, diese korrekt zu schließen. Ein Puffer konnte nicht synchron geschrieben werden. Wenn Sie das Programm verlassen, ohne die Ressource zuerst korrekt zu schließen, kann dies zu einer Beschädigung führen.

enrico.bacis
quelle
8
Diese Antwort ist ein großartiges Beispiel für "Lernen Sie keine Programmiersprache, sondern lernen Sie, wie man programmiert". Sie können das Problem im Allgemeinen nicht vermeiden, indem Sie eine andere Sprache auswählen.
Kerrek SB
10
Ich halte das für falsch. Wenn Sie exit()(anstatt _exit()) aufrufen, werden die atexitRoutinen aufgerufen und die stdioPufferung auf die Festplatte geleert. exit()ist genau da, um ein ordentliches Beenden des Programms zu ermöglichen.
Abligh
2
@abligh Echtzeitsysteme geben beispielsweise bei exit () keinen malloc'ed Speicher frei, was zu Lecks führt. POSIX Shared Memory ist ein weiteres Beispiel. Es kommt also eher auf die Umgebung an als auf die strikte Einhaltung des C-Standards.
PP
2
@abligh: Nicht alle Ressourcen sind Standardbibliotheken. Sie haben sicherlich Recht, dass die Standardbibliothek bestimmte Garantien bietet, aber Sie müssen dennoch über den globalen Kontrollfluss Ihres Programms und die Verantwortlichkeiten der einzelnen Teile nachdenken und sicherstellen, dass jede anstehende Verantwortung angemessen behandelt wird. Der Begriff "Ressource" ist ein prägnanter Begriff für diesen allgemeinen Begriff und geht über eine bestimmte Bibliothek hinaus. Der Umgang mit Ressourcen liegt letztendlich in der Verantwortung des Programmierers. Einrichtungen wie atexitkönnen helfen, obwohl sie nicht immer angemessen sind.
Kerrek SB
2
@abligh: Sie können natürlich verwenden, exitwenn Sie möchten, aber es ist ein globaler Kontrollsprung, der mit allen Nachteilen der unstrukturierten Kontrolle einhergeht, die wir besprochen haben. Um von einer Fehlerbedingung zu beenden, kann dies angemessen sein (und es scheint das zu sein, was das OP will), obwohl ich es für einen normalen Kontrollfluss wahrscheinlich vorziehen würde, die Hauptschleife mit einer geeigneten Ausgangsbedingung zu entwerfen, damit Sie tatsächlich enden Rückkehr von der Hauptleitung.
Kerrek SB
10

Die Verwendung exit()ist in Ordnung

Zwei Hauptaspekte des Code-Designs, die noch nicht erwähnt wurden, sind "Threading" und "Bibliotheken".

In einem Single-Thread-Programm ist die Verwendung in dem Code, den Sie zur Implementierung dieses Programms schreiben, in exit()Ordnung. Meine Programme verwenden es routinemäßig, wenn etwas schief gelaufen ist und der Code nicht wiederhergestellt werden kann.

Aber…

Das Anrufen exit()ist jedoch eine einseitige Aktion, die nicht rückgängig gemacht werden kann. Aus diesem Grund erfordern sowohl "Threading" als auch "Bibliotheken" sorgfältige Überlegungen.

Threaded-Programme

Wenn ein Programm über mehrere Threads verfügt, ist die Verwendung exit()eine dramatische Aktion, bei der alle Threads beendet werden. Es wird wahrscheinlich unangemessen sein, das gesamte Programm zu beenden. Es kann angebracht sein, den Thread zu beenden und einen Fehler zu melden. Wenn Sie mit dem Design des Programms vertraut sind, ist dieser einseitige Ausstieg möglicherweise zulässig, aber im Allgemeinen nicht akzeptabel.

Bibliothekscode

Und diese Klausel „Kenntnis des Programmdesigns“ gilt auch für Code in Bibliotheken. Es ist sehr selten richtig, eine Allzweckbibliotheksfunktion aufzurufen exit(). Sie wären zu Recht verärgert, wenn eine der Standardfunktionen der C-Bibliothek nur aufgrund eines Fehlers nicht zurückgegeben werden könnte. (Offensichtlich Funktionen wie exit(), _Exit(), quick_exit(), abort()soll nicht auf Rückkehr, das anders ist.) Die Funktionen in der C - Bibliothek daher entweder „nicht versagen“ oder eine Fehleranzeige zurückzukehren irgendwie. Wenn Sie Code schreiben, um in eine Allzweckbibliothek zu gelangen, müssen Sie die Fehlerbehandlungsstrategie für Ihren Code sorgfältig abwägen. Es sollte zu den Fehlerbehandlungsstrategien der Programme passen, mit denen es verwendet werden soll, oder die Fehlerbehandlung kann konfigurierbar gemacht werden.

Ich habe eine Reihe von Bibliotheksfunktionen (in einem Paket mit Header "stderr.h", einem Namen, der auf dünnem Eis steht), die beendet werden sollen, wenn sie für die Fehlerberichterstattung verwendet werden. Diese Funktionen werden standardmäßig beendet. Es gibt eine verwandte Reihe von Funktionen im selben Paket, die Fehler melden und nicht beendet werden. Die vorhandenen Funktionen werden natürlich in Bezug auf die nicht vorhandenen Funktionen implementiert, aber das ist ein internes Implementierungsdetail.

Ich habe viele andere Bibliotheksfunktionen, und viele von ihnen verlassen sich auf den "stderr.h"Code für die Fehlerberichterstattung. Das ist eine Designentscheidung, die ich getroffen habe und mit der ich einverstanden bin. Wenn die Fehler jedoch mit den Funktionen gemeldet werden, die beendet werden, wird der allgemeine Nutzen des Bibliothekscodes eingeschränkt. Wenn der Code die Fehlerberichtsfunktionen aufruft, die nicht beendet werden, müssen die Hauptcodepfade in der Funktion Fehlerrückgaben ordnungsgemäß behandeln - erkennen Sie sie und leiten Sie eine Fehleranzeige an den aufrufenden Code weiter.


Der Code für mein Fehlerberichtspaket ist in meinem SOQ- Repository (Stack Overflow Questions) auf GitHub als Dateien stderr.cund stderr.him Unterverzeichnis src / libsoq verfügbar .

Jonathan Leffler
quelle
Das ist gut spitz. Möglicherweise sehen Sie hier ein Beispiel für eine Bibliothek, die aufgerufen wird, abort()wenn Speicher zugewiesen werden soll, malloc()oder stellen realloc()Sie sich vor, Sie haben eine Anwendung, die mit 100 Bibliotheken verknüpft ist, und Sie fragen sich, welche und wie Ihre Anwendung abgestürzt ist. Außerdem habe ich abort()in ihrer Dokumentation keine Erwähnung gefunden (aber versteh mich nicht falsch. Es ist eine großartige Bibliothek für diesen Zweck).
Grzegorz Szpetkowski
@GrzegorzSzpetkowski Und es ruft nicht einmal manuell Flush auf, nachdem es an stderr gedruckt wurde. Autsch!
dieser
1
@this: Es sollte nicht nötig sein. Die Ausgabe von stderrist normalerweise zeilengepuffert. Wenn die Ausgabe mit einem Zeilenumbruch endet, wird sie trotzdem vom System geleert.
Jonathan Leffler
@ JonathanLeffler Sich auf den Benutzer verlassen, kein kluger Schachzug.
dieser
@this: Nein, unter Berufung auf die Implementierung, die den Anforderungen des C-Standards entspricht: §7.21.3 ¶7 Beim Programmstart sind drei Textströme vordefiniert und müssen nicht explizit geöffnet werden - Standardeingabe (zum Lesen herkömmlicher Eingaben), Standard Ausgabe (zum Schreiben der herkömmlichen Ausgabe) und Standardfehler (zum Schreiben der Diagnoseausgabe). Beim ersten Öffnen ist der Standardfehlerstrom nicht vollständig gepuffert. Es erfordert eine aktive Codierung durch den Programmierer, um den Standardfehler vollständig zu puffern (und es gibt für niemanden Hilfe, wenn der Programmierer so stumpf ist - außer "Verwenden Sie den Code nicht").
Jonathan Leffler
6

Ein Grund, den Sie exitbei anderen Funktionen als vermeiden sollten, main()ist die Möglichkeit, dass Ihr Code aus dem Kontext gerissen wird. Denken Sie daran, dass exit eine Art nicht lokaler Kontrollfluss ist . Wie unauffindbare Ausnahmen.

Beispielsweise können Sie einige Speicherverwaltungsfunktionen schreiben, die bei einem kritischen Festplattenfehler beendet werden. Dann beschließt jemand, sie in eine Bibliothek zu verschieben. Das Verlassen einer Bibliothek führt dazu, dass das aufrufende Programm in einem inkonsistenten Zustand beendet wird, auf den es möglicherweise nicht vorbereitet ist.

Oder Sie können es auf einem eingebetteten System ausführen. Es gibt nirgendwo zu verlassen , um das Ganze läuft in einer while(1)in Schleife main(). Es ist möglicherweise nicht einmal in der Standardbibliothek definiert.

pjc50
quelle
2
Und die Leute sollten keine Küchenmesser besitzen dürfen, weil jemand eine Handvoll davon nehmen und versuchen könnte, sie zu jonglieren oder mit seinem Kind "fangen" zu spielen. Offensichtlich bin ich anderer Meinung. Wenn jemand beschließt, meinen Code in sein Programm zu kopieren, und sich herausstellt, dass mein Code nicht seinen Zwecken entspricht, ist dies sein Problem, weil er den von ihm assimilierten Code nicht gelesen hat.
G-Man sagt "Reinstate Monica"
3
Eine ziemlich krasse Analogie. C ist voll von Dingen, die getan werden können , aber wahrscheinlich vermieden werden sollten. Daher die Existenz des IOCCC. Beachten Sie, dass in meinem Beitrag nicht "sollte nicht" steht.
pjc50
2

Je nachdem, was Sie tun, ist das Beenden möglicherweise der logischste Ausweg aus einem Programm in C. Ich weiß, dass es sehr nützlich ist, um zu überprüfen, ob Rückrufketten ordnungsgemäß funktionieren. Nehmen Sie diesen Beispiel-Rückruf, den ich kürzlich verwendet habe:

unsigned char cbShowDataThenExit( unsigned char *data, unsigned short dataSz,unsigned char status)
{

    printf("cbShowDataThenExit with status %X (dataSz %d)\n", status, dataSz);
    printf("status:%d\n",status);
    printArray(data,dataSz);
    cleanUp();
    exit(0);
}

In der Hauptschleife richte ich alles für dieses System ein und warte dann in einer Weile (1). Es ist möglich, stattdessen ein globales Flag zu erstellen, um die while-Schleife zu verlassen. Dies ist jedoch einfach und macht das, was es tun muss. Wenn Sie mit offenen Puffern wie Dateien und Geräten arbeiten, sollten Sie diese aus Gründen der Konsistenz vor dem Schließen bereinigen.

Dom
quelle
-1

In einem großen Projekt ist es schrecklich, wenn jeder Code außer Coredump beendet werden kann. Trace ist sehr wichtig, um einen Online-Server zu warten.

user4531555
quelle