Czy powinniśmy używać exit () w C?

80

Pojawia się pytanie o używanie exitw C ++. W odpowiedzi jest mowa o tym, że nie jest to dobry pomysł głównie z powodu RAII, np. Jeśli exitzostanie wywołany gdzieś w kodzie to destruktory obiektów nie zostaną wywołane, stąd jeśli np. Destruktor miał zapisywać dane do pliku, to się nie stanie , ponieważ destruktor nie został wywołany.

Interesowało mnie, jak wygląda ta sytuacja w C. Czy podobne kwestie dotyczą również C? Pomyślałem, że skoro w C nie używamy konstruktorów / destruktorów, sytuacja może wyglądać inaczej w C. Czy można używać exitw C?

Widziałem funkcje, takie jak poniżej, które uważam za dobre w niektórych przypadkach, ale byłem zainteresowany, czy mamy podobne problemy w C z używaniem exit, jak opisano powyżej w C ++? (co spowodowałoby, że używanie funkcji takich jak poniżej nie byłoby dobrym pomysłem).

gmoniava
źródło
2
„Destruktory obiektów nie będą nazywane” - To nie jest do końca poprawne (patrz: cplusplus.com/reference/cstdlib/exit ). Myślisz o quick_exit (patrz: cplusplus.com/reference/cstdlib/quick_exit/?kw=quick_exit ).
Coder
Możesz także mieć pewne problemy specyficzne dla systemu operacyjnego, np. Konwencjonalna rola SIGTERMsygnału w POSIX i Linuxie… Dobrze zachowujące się serwery powinny sobie z tym dobrze radzić. I powinieneś unikać używania SIGKILL(tj. Spróbuj kill -TERMwtedy kill -QUITi dopiero później kill -KILLjako administrator)
Basile Starynkevitch 19.07.15
20
Dlaczego to nie jest duplikat prawie 7 lat po uruchomieniu Stack Overflow?
Peter Mortensen
7
@PeterMortensen: to jest zaskakujące, ale nie jestem świadomy dobrej alternatywy, która to duplikaty. Wysoko głosowane użycie exit()funkcji nie jest istotne (a wysoka liczba głosów jest zaskakująca - nie jest to dobre pytanie). Dlaczego nie powinienem używać funkcji exit () w C? byłby dobrym kandydatem, gdyby miał odpowiedzi na to pytanie. Nie ma; odwrotne zamknięcie tego jako duplikatu jest właściwe - i zrobiłem to.
Jonathan Leffler
Nie jestem pewien, czy chcesz użyć „printf” w przypadku błędu. Funkcja używa buforów, ale jeśli masz uszkodzenie pamięci, te bufory mogą nie być dobre. Możesz po prostu użyć 'write (2, "ERROR:", 7); write (2, wiadomość, strlen (wiadomość));
Alex

Odpowiedzi:

82

Zamiast abort()tego exit()funkcja w C jest uważana za „pełne wdzięku” wyjście.

Z C11 (N1570) 7.22.4.4/p2 Funkcja wyjścia (moje wyróżnienie ):

Ta exitfunkcja powoduje normalne zakończenie programu.

Norma mówi również w 7.22.4.4/p4, że:

Następnie wszystkie otwarte strumienie z niepisanymi buforowanymi danymi są opróżniane , wszystkie otwarte strumienie są zamykane , a wszystkie pliki utworzone przez tmpfilefunkcję są usuwane.

Warto też patrząc na 7.21.3 / P5 Files :

Jeśli mainfunkcja powróci do swojego pierwotnego obiektu wywołującego lub jeśli exit funkcja zostanie wywołana, wszystkie otwarte pliki są zamykane (stąd wszystkie strumienie wyjściowe są opróżniane) przed zakończeniem programu. Inne ścieżki do zakończenia programu, takie jak wywołanie abortfunkcji, nie muszą prawidłowo zamykać wszystkich plików.

Jednak, jak wspomniano w komentarzach poniżej, nie można zakładać, że obejmie on każdy inny zasób , więc może być konieczne atexit()indywidualne odwołanie się do wywołań zwrotnych w celu ich wydania. W rzeczywistości jest to dokładnie to, co atexit()jest przeznaczone do zrobienia, jak jest to napisane w 7.22.4.2/p2 Funkcja atexit :

Te atexitrejestry funkcyjne funkcji wskazywanej przez func, być wywołana bez argumentów podczas normalnego zakończenia programu.

Warto zauważyć, że standard C nie mówi dokładnie, co powinno się stać z obiektami o przydzielonym czasie przechowywania (tj. malloc()), Co wymaga, abyś był świadomy tego, jak to się robi w konkretnej implementacji. W przypadku nowoczesnych systemów operacyjnych zorientowanych na hosta jest prawdopodobne, że system zajmie się tym problemem, ale nadal możesz chcieć zająć się tym samodzielnie, aby wyciszyć debuggery pamięci, takie jak Valgrind .

Grzegorz Szpetkowski
źródło
3
Wydaje mi się, że to poprawna odpowiedź. Połączenie gniazda jest zamykane przy wyjściu, ponieważ zakończenie procesu zamyka wszystkie deskryptory plików, niezależnie od tego, czy zostały otwarte za pomocą stdio, czy nie , czyli jest równoważne a close()na FD. Nie wiem, w jakim sensie @BlueMoon uważa, że ​​to nieprawda, ale nie mniej błędne niż wezwanie close(), a jeśli wymagane są dalsze wyjaśnienia, to właśnie po to atexit().
abligh
1
@abligh Nowoczesne systemy operacyjne zamkną wszystkie pliki FDS związane z tym procesem; ale to jest coś brutalnego i niestandardowego (co mówi Blue Moon).
edmz
3
@black, jeśli potrzebne jest dalsze czyszczenie, atexit()można użyć. Używanie exit()samego siebie nie jest bardziej brutalne niż robienie returnz main().
abligh
5
Pisanie `` właściwego '' oprogramowania przy użyciu `` dobrych praktyk '' jest powodem, dla którego mam tak wiele aplikacji, które nie zamykają się natychmiast, gdy zostaniesz o to poproszony. :( Jeśli system operacyjny może coś zrobić, powinieneś pozwolić mu to zrobić, zamiast bawić się kodem użytkownika próba zrobienia czegoś, co już istnieje, jest już przetestowane i zdebugowane. Jeśli system oprogramowania nie jest w stanie wytrzymać nagłego, nieoczekiwanego zamknięcia (np. z jakiegoś wątku wzywającego do natychmiastowego zakończenia procesu, Menedżer zadań „Zakończ proces”, „zabij -9 'lub awaria zasilania), i tak jest kiepskiej jakości
Martin James
byłoby miło, gdybyś mógł dodać -jeśli to możliwe- do swojej odpowiedzi, jakiego rodzaju zasoby mogą być potrzebne do zwolnienia, w atexitprzeciwnym razie jest w porządku. @BlueMoon: Nie sądzę, aby używanie takiej funkcji jak dieoznaczało, że nie można programować, w kilku przypadkach można chcieć jej użyć. W przeciwnym razie możesz sobie z tym poradzić, używając po prostu zwracanych wartości itp.
gmoniava,
23

Tak, używanie exitw C.

Aby zapewnić wszystkie bufory i bezpieczne, uporządkowane zamykanie, zaleca się użycie tej funkcji atexit, więcej informacji na ten temat tutaj

Przykładowy kod wyglądałby tak:

Teraz, za każdym razem, gdy exitzostanie wywołana, funkcja cleanupzostanie wykonana, co może pomieścić wdzięczne zamknięcie, wyczyszczenie buforów, pamięci itp.

t0mm13b
źródło
@IlmariKaronen Przydzielona pamięć nie jest w żaden sposób obsługiwana automatycznie. Jest to system operacyjny, który może zapewnić odzyskanie pamięci z powrotem do systemu operacyjnego. Argument Grzegorza Szpetkowskiego jest przeciwny twojemu stwierdzeniu: Zwłaszcza standard C nie mówi dokładnie, co powinno się stać z obiektami o przydzielonym czasie przechowywania (iemalloc ()), co wymaga, abyś był świadomy tego, jak to się dzieje na konkretnej implementacji. W przypadku nowoczesnych systemów operacyjnych zorientowanych na hosta jest prawdopodobne, że system zajmie się tym problemem, ale nadal możesz chcieć zająć się tym samodzielnie, aby wyciszyć debuggery pamięci, takie jak Valgrind.
to
15

Nie masz konstruktorów i destruktorów, ale możesz mieć zasoby (np. Pliki, strumienie, gniazda) i ważne jest, aby je poprawnie zamknąć. Bufor nie mógł zostać zapisany synchronicznie, więc wyjście z programu bez wcześniejszego prawidłowego zamknięcia zasobu może doprowadzić do uszkodzenia.

enrico.bacis
źródło
8
Ta odpowiedź jest doskonałym przykładem „nie ucz się języka programowania; naucz się programować”. Ogólnie rzecz biorąc, nie można uniknąć zrozumienia problemu, wybierając inny język.
Kerrek SB,
10
Uważam, że to nieprawda. Jeśli wywołujesz exit()(zamiast _exit()), atexitprocedury są wywoływane, a stdiobuforowanie jest usuwane na dysk. exit()jest właśnie po to, aby umożliwić uporządkowane wyjście z programu.
abligh
2
@abligh Systemy czasu rzeczywistego nie zwalniają pamięci malloc'ed na przykład przy exit (), co powoduje wycieki. Pamięć współdzielona POSIX to kolejny przykład. Zależy to więc od środowiska, a nie ściśle przestrzegać normy C.
PP
2
@abligh: Nie wszystkie zasoby są dostarczane w postaci biblioteki standardowej. Chociaż z pewnością masz rację, że biblioteka standardowa zapewnia pewne gwarancje, nadal musisz pomyśleć o globalnym przepływie kontroli programu i obowiązkach każdej z jego części oraz upewnić się, że każdy oczekujący obowiązek jest odpowiednio obsługiwany . Termin „zasób” jest zwięzłym określeniem tego ogólnego pojęcia i wykracza poza jakąkolwiek konkretną bibliotekę, a obsługa zasobów jest ostatecznie obowiązkiem programisty. Takie udogodnienia atexitmogą pomóc, choć nie zawsze są odpowiednie.
Kerrek SB,
2
@abligh: Użytkownik może użytkowania oczywiście exit, jeśli chcesz, ale jest to globalny skok sterowania, który jest dostarczany ze wszystkimi downsides o nieuporządkowanej kontroli, które omówiliśmy. Jako sposób na zakończenie stanu awarii może to być właściwe (i wydaje się, że jest to, czego chce OP), chociaż dla normalnego przepływu sterowania prawdopodobnie wolałbym zaprojektować główną pętlę z odpowiednim warunkiem wyjścia, aby rzeczywiście skończyć wracając z main.
Kerrek SB,
10

Używanie exit()jest OK

Dwa główne aspekty projektowania kodu, o których jeszcze nie wspomniano, to „wątkowanie” i „biblioteki”.

W programie jednowątkowym, w kodzie, który piszesz w celu zaimplementowania tego programu, użycie exit()jest w porządku. Moje programy używają go rutynowo, gdy coś poszło nie tak i kod nie zostanie odzyskany.

Ale…

Jednak wezwanie exit()jest działaniem jednostronnym, którego nie można cofnąć. Dlatego zarówno „wątkowanie”, jak i „biblioteki” wymagają dokładnego przemyślenia.

Programy gwintowane

Jeśli program jest wielowątkowy, użycie exit()jest dramatyczną akcją, która kończy wszystkie wątki. Wyjście z całego programu będzie prawdopodobnie niewłaściwe. Może być właściwe wyjście z wątku i zgłoszenie błędu. Jeśli znasz projekt programu, być może jednostronne wyjście jest dopuszczalne, ale generalnie nie będzie do zaakceptowania.

Kod biblioteki

I ta klauzula „świadomy projektu programu” odnosi się również do kodu w bibliotekach. Bardzo rzadko jest poprawne wywołanie funkcji biblioteki ogólnego przeznaczenia exit(). Słusznie byłbyś zdenerwowany, gdyby jedna ze standardowych funkcji biblioteki C nie zwróciła się tylko z powodu błędu. (Oczywiście, jak funkcjonuje exit(), _Exit(), quick_exit(), abort()nie są przeznaczone do zwrotu, to co innego.) Funkcje w bibliotece C dlatego też „nie może upaść” lub powrót wskazanie błędu jakoś. Jeśli piszesz kod, który ma trafić do biblioteki ogólnego przeznaczenia, musisz dokładnie rozważyć strategię obsługi błędów w swoim kodzie. Powinien pasować do strategii obsługi błędów programów, z którymi ma być używany, lub można skonfigurować obsługę błędów.

Mam szereg funkcji bibliotecznych (w pakiecie z nagłówkiem "stderr.h", nazwą, która krąży po cienkim lodzie), które są przeznaczone do zakończenia, ponieważ są używane do raportowania błędów. Te funkcje kończą się zgodnie z projektem. W tym samym pakiecie znajduje się szereg funkcji, które zgłaszają błędy i nie zamykają się. Wychodzące funkcje są oczywiście implementowane w kategoriach funkcji nie wychodzących, ale jest to wewnętrzny szczegół implementacji.

Mam wiele innych funkcji bibliotecznych i wiele z nich polega na "stderr.h"kodzie do raportowania błędów. To decyzja projektowa, którą podjąłem i z którą się zgadzam. Ale kiedy błędy są zgłaszane za pomocą funkcji, które kończą pracę, ogranicza to ogólną użyteczność kodu biblioteki. Jeśli kod wywołuje funkcje raportowania błędów, które nie kończą się, to główne ścieżki kodu w funkcji muszą poradzić sobie z błędami, zwracając je normalnie - wykryj je i przekaż wskazanie błędu do kodu wywołującego.


Kod mojego pakietu raportowania błędów jest dostępny w moim repozytorium SOQ (Stack Overflow Pytania) na GitHub jako pliki stderr.coraz stderr.hw podkatalogu src / libsoq .

Jonathan Leffler
źródło
To dobrze określone. Możesz zobaczyć tutaj przykład biblioteki, która wywołuje, abort()kiedy trzeba przydzielić pamięć przez jedną malloc()lub realloc(), Wyobraź sobie, że masz aplikację, która łączy się ze 100 bibliotekami i zastanawiasz się, która z nich i jak spowodowała awarię aplikacji. Co więcej, nie znalazłem żadnej wzmianki o tym abort()w ich dokumentacji (ale nie zrozumcie mnie źle. To świetna biblioteka do tego celu).
Grzegorz Szpetkowski
@GrzegorzSzpetkowski I nawet nie wywołuje ręcznie koloru po fprintingu na stderr. Auć!
to
1
@this: Nie powinno to robić. Wyjście z stderrjest zwykle buforowane liniowo. Jeśli wyjście kończy się znakiem nowej linii, i tak zostanie opróżnione przez system.
Jonathan Leffler,
@JonathanLeffler Poleganie na użytkowniku, a nie sprytnym posunięciu.
to
@this: Nie, opierając się na implementacji zgodnej z wymaganiami standardu C: §7.21.3 ¶7 Podczas uruchamiania programu trzy strumienie tekstowe są predefiniowane i nie muszą być otwierane jawnie - standardowe wejście (do odczytu konwencjonalnego wejścia), standardowe wyjście (do zapisu wyjścia konwencjonalnego) i błąd standardowy (do zapisu wyjścia diagnostycznego). Jak początkowo otworzono, standardowy strumień błędów nie jest w pełni buforowany; … Wymaga to aktywnego kodowania przez programistę, aby standardowy błąd był w pełni buforowany (i nie ma żadnej pomocy, jeśli programista jest tak tępy - z wyjątkiem „nie używaj kodu”).
Jonathan Leffler,
6

Jednym z powodów, których należy unikać exitw funkcjach innych niż main()możliwość wyrwania kodu z kontekstu. Pamiętaj, że wyjście jest rodzajem nielokalnego przepływu sterowania . Jak nieuchwytne wyjątki.

Na przykład możesz napisać niektóre funkcje zarządzania pamięcią masową, które kończą się w przypadku krytycznego błędu dysku. Wtedy ktoś decyduje się przenieść je do biblioteki. Wyjście z biblioteki to coś, co spowoduje, że program wywołujący zakończy pracę w stanie niespójnym, na który może nie być przygotowany.

Lub możesz uruchomić go w systemie wbudowanym. Nie ma nigdzie wyjść na cały tras rzeczą w while(1)pętli main(). Może nawet nie być zdefiniowany w bibliotece standardowej.

pjc50
źródło
2
A ludziom nie powinno się pozwalać na posiadanie noży kuchennych, ponieważ ktoś mógłby chwycić ich garść i spróbować nimi żonglować lub bawić się w „łapanie” z dzieckiem. Oczywiście się nie zgadzam. Jeśli ktoś zdecyduje się skopiować mój kod do swojego programu, a okaże się, że mój kod nie odpowiada jego celom, to jest to jego problem, ponieważ nie czytał kodu, który przyswaja.
G-Man mówi „Przywróć Monikę”
3
Dość prostacka analogia. C jest pełen rzeczy, które można zrobić, ale prawdopodobnie należy ich unikać. Stąd istnienie IOCCC. Zauważ, że mój post nie mówi „nie powinien”.
pjc50
2

W zależności od tego, co robisz, wyjście może być najbardziej logicznym sposobem wyjścia z programu w C. Wiem, że jest to bardzo przydatne do sprawdzania, czy łańcuchy wywołań zwrotnych działają poprawnie. Weźmy przykład callback, którego ostatnio używałem:

W głównej pętli ustawiam wszystko dla tego systemu, a następnie czekam chwilę (1) pętla. Możliwe jest utworzenie flagi globalnej, aby zamiast tego wyjść z pętli while, ale jest to proste i robi to, co trzeba. Jeśli masz do czynienia z otwartymi buforami, takimi jak pliki i urządzenia, powinieneś je wyczyścić przed zamknięciem, aby zachować spójność.

Dom
źródło
-1

To straszne w dużym projekcie, kiedy każdy kod może wyjść poza coredump. Śledzenie jest bardzo ważne w utrzymaniu serwera online.

user4531555
źródło