Egy folyamat megszűnése és egy alkalmazási művelet szabályos befejezése nem ugyanaz. A kernel visszaveheti a folyamat memóriáját és lezárhatja file descriptorait, miközben az utolsó naplóüzenet még egy felhasználói térben élő pufferben van, a konfigurációs fájl félkész, a távoli tranzakció pedig rendezetlen.
A main, startup, argumentumok és környezet részben megismert programindulás párja ezért nem egyetlen „kilépés” művelet. Meg kell különböztetnünk a C runtime által végzett lezárást, a kernel erőforrás-visszavételét és az alkalmazás saját helyreállítási feladatait.
Az alábbi nyelvi szabályok ISO C17 szerint értendők; a Linux/POSIX sajátosságokat külön jelöljük. A példák választott laborprofilja Linux, x86-64 System V ABI, LP64, glibc 2.39, GCC 14.2 vagy Clang 19.1, explicit C17 módban. A parancsok fordítási útmutatók, nem dokumentált teszteredmények.
A normál programleállás a C szabvány szerinti
kategória: nem feltétlenül jelent sikeres üzleti műveletet. Az
exit(EXIT_FAILURE) például normál leállás, sikertelenséget
jelző állapottal.
| Művelet | Hatás | Automatikus alkalmazásszintű cleanup |
|---|---|---|
return egy közönséges függvényből |
Visszatérés a hívóhoz | Csak a ténylegesen végrehajtott kód |
return status a main kezdeti
hívásából |
Normál leállás, az exit(status) leállítási
viselkedésével |
atexit callbackek és standard streamlezárás |
exit(status) |
Normál programleállás | atexit callbackek, streamürítés és -zárás |
_Exit(status) |
Normál leállás kilépési callbackek nélkül | Az ISO C szerint a streamkezelés implementáció által meghatározott |
POSIX _exit(status) |
Folyamatleállítás a szokásos C cleanup nélkül | Nincs atexit, nincs stdio-pufferürítés |
quick_exit(status) |
Korlátozott normál leállás | at_quick_exit callbackek, majd _Exit |
abort() |
Rendellenes leállás | Nincs atexit vagy at_quick_exit
callback |
ISO C17: az exit, _Exit,
quick_exit, abort és a callback-regisztráló
függvények deklarációja a <stdlib.h> fejlécben
található. A main végének elérése return 0
hatású. Hordozható sikerjelzés a 0 vagy az
EXIT_SUCCESS, hordozható sikertelenségjelzés az
EXIT_FAILURE; más állapotértékek értelmezése
implementációfüggő.
Az exit nem tér vissza, és nem hajtja végre az elhagyott
függvények hátralévő utasításait. C-ben nincs olyan általános
mechanizmus, amely minden lokális erőforráshoz automatikus lezárást
kapcsolna.
Linux/POSIX: a <unistd.h>
fejlécben deklarált _exit és az _Exit hatása
egyenértékű. Nem ürítik a C FILE streamek felhasználói
térbeli puffereit, de a folyamat file descriptorai lezáródnak. Az
„azonnali” ezért nem valós idejű késleltetési garancia: a kerneloldali
lezárás is igénybe vehet időt.
Az abort() rendellenes leállást kezdeményez, a
SIGABRT jelzéshez kapcsolódva. Core dump keletkezése nem
garantált: rendszerbeállításoktól is függ. ISO C17
szerint a streamek ürítése és lezárása ennél is implementáció
által meghatározott; naplómegőrzési stratégiát nem szabad erre
építeni.
Az
int atexit(void (*func)(void));deklaráció egy paraméter és visszatérési érték nélküli függvényt regisztrál normál leállásra. A regisztráció sikere nulla, hibája nem nulla visszatérési értékkel jelezhető.
ISO C17: az előre regisztrált callbackek a regisztráció fordított sorrendjében futnak. Ha előbb az alapinfrastruktúra, majd az arra épülő komponens cleanupját regisztráljuk, utóbbi záródik le előbb. Ez azonban csak akkor helyes, ha a valódi függőségek megfelelnek ennek a sorrendnek.
Az at_quick_exit külön callbacklistát kezel. A
quick_exit nem futtatja az atexit listáját; az
exit pedig nem futtatja az at_quick_exit
listáját.
Egy kilépési callbacknél négy szerződést kell tisztázni:
Az utolsó különösen fontos: a callback void
visszatérésű, tehát egy fclose hibáját nem tudja egyszerű
visszatérési értékkel átadni az exit hívójának. A kritikus
lezárást ezért még a végső kilépési állapot kiválasztása előtt végezzük
el. Kilépési callbackből ne próbáljunk longjmp segítségével
„visszamenekülni”; ez nem megengedett helyreállítási út.
Egy sikeres fputs jelentheti csupán azt, hogy az adat
bekerült a stdio-pufferbe. A tényleges továbbítás hibája később, az
fflush vagy az fclose során is
jelentkezhet.
ISO C17: az exit a callbackek után
üríti és lezárja a nyitott streameket, és eltávolítja a
tmpfile által létrehozott fájlokat. Ebből nem következik,
hogy a folyamat kilépési állapota automatikusan sikertelenségre változik
egy ürítési hiba miatt.
Az alábbi teljes program, shutdown.c, ezért explicit
ellenőrzi a lezárást. Külön laborkönyvtárban használjuk: a
shutdown-demo.txt meglévő tartalmát felülírja.
#include <stdio.h>
#include <stdlib.h>
static FILE *owned;
static int close_owned(void)
{
FILE *file = owned;
owned = NULL;
return file == NULL ? 0 : fclose(file);
}
static void fallback_cleanup(void)
{
(void)close_owned();
}
int main(void)
{
if (atexit(fallback_cleanup) != 0) {
return EXIT_FAILURE;
}
owned = fopen("shutdown-demo.txt", "w");
if (owned == NULL) {
return EXIT_FAILURE;
}
int failed = fputs("completed\n", owned) == EOF;
if (fflush(owned) == EOF) {
failed = 1;
}
if (close_owned() == EOF) {
failed = 1;
}
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
}Fordítás a megadott profil valamelyik fordítójával:
gcc -std=c17 -Wall -Wextra -Wpedantic -O2 shutdown.c -o shutdown-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -O2 shutdown.c -o shutdown-clang| Elem | Tulajdonlás és élettartam | Érvénytelenítés, hibakimenet |
|---|---|---|
owned |
Statikus tárolási idejű pointerobjektum; a program streamtulajdonlását jelöli | NULL jelentése: nincs birtokolt stream |
| Megnyitott stream | Sikeres fopen után a program birtokolja |
Az fclose után nem használható, hiba esetén sem |
Lokális file |
A lezáráshoz átvett handle | Nem őrizhető meg a lezárás után |
fallback_cleanup |
A runtime normál leálláskor hívja; csak statikus állapotot ér el | Hibát nem továbbít a kilépési állapotba |
Az owned nullázása még az fclose előtt
kizárja, hogy a későbbi fallback ugyanazt a streamet ismét lezárja. Az
explicit út minden hibát sikertelenségre fordít; a callback csak
tartalék.
Az explicit fflush itt ellenőrzési pont: az
fclose egyébként maga is megkísérli a pufferelt adatok
továbbítását. Ürítési hiba után sem szabad kihagyni a lezárást.
Linux/POSIX: a sikeres fflush nem
jelent tartós tárolóra írást. A kernelhez továbbított adat még elveszhet
rendszerösszeomláskor. A tartóssági szerződés további műveleteket
igényel; ezt a fájlrendszer-műveletek és
tartósság rész tárgyalja.
Linux/POSIX: folyamatmegszűnéskor a kernel felszabadítja a folyamat címterét, és lezárja a folyamat file descriptorait. Ez nem egyenértékű a következőkkel:
Az alapvető különbség: az erőforrás visszavétele nem alkalmazási tranzakció. A kernel azt tudja, mely descriptorok tartoznak a folyamathoz; azt nem, hogy egy konfigurációcsere mikor tekinthető befejezettnek.
Ezért a helyes terv két rétegű: normál működésben ellenőrzött cleanup, megszakadás esetére pedig helyreállítható állapot és újraindítási protokoll.
setjmp és longjmpISO C17: a <setjmp.h> fejléc
setjmp makrója egy jmp_buf objektumba ment
visszatérési környezetet. Közvetlen meghívásakor nullát ad; egy
megfelelő longjmp után ugyanaz a setjmp nem
nulla értékkel tér vissza.
A longjmp(env, value) által előidézett visszatérési
érték value, kivéve, hogy a nulla argumentum helyett egy
lesz. Ez vezérlésátadás, nem állapot-visszagörgetés: fájlírást,
memóriamódosítást és erőforrásfoglalást nem von vissza.
A teljes jump.c példa:
#include <setjmp.h>
#include <stdlib.h>
static jmp_buf checkpoint;
static void deep_call(void)
{
longjmp(checkpoint, 7);
}
int main(void)
{
volatile int phase = 0;
if (setjmp(checkpoint) == 0) {
phase = 1;
deep_call();
}
return phase == 1 ? EXIT_SUCCESS : EXIT_FAILURE;
}gcc -std=c17 -Wall -Wextra -Wpedantic -O2 jump.c -o jump-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -O2 jump.c -o jump-clangA checkpoint statikus tárolója a programé, de a bele
mentett környezet csak addig használható, amíg a hozzá tartozó
main-hívás aktív. A tároló hosszú élettartama nem
hosszabbítja meg a mentett hívási környezet érvényességét. Másik szálból
ugrani, vagy már visszatért függvény környezetébe visszaugrani nem
definiált viselkedés.
volatile?A longjmp után indeterminate értékűek azok a lokális
változók, amelyek egyszerre:
setjmp hívását tartalmazó függvényhez tartoznak;volatile minősítésűek;setjmp és a longjmp között
megváltoztak.A phase az első, második és negyedik feltételt
teljesíti. A volatile nélkül a visszaugrás utáni
összehasonlítására nem építhetnénk helyes programot. A
volatile itt nem szálbiztonságot és nem
erőforrás-biztonságot jelent.
Helyességi indoklás: a közvetlen visszatéréskor
belépünk az if törzsébe, majd a phase egy
lesz. A longjmp után a feltételben szereplő
setjmp értéke hét, így a törzs kimarad. A
volatile változó megtartott értéke alapján a példa sikeres
vezérlés-visszatérést jelez.
A setjmp használati helye is korlátozott: a bemutatott
if-feltétel megengedett, de az
int result = setjmp(checkpoint); inicializálás nem
hordozható, szabályos C17 használat.
Ha a deep_call erőforrást foglalna, majd annak lezárása
előtt ugrana, az elhagyott cleanup utasítás nem futna le. Biztonságosabb
az erőforrást a helyreállítási pont tulajdonában tartani, vagy az ugrás
előtt explicit felszabadítani. Általános C hibakezeléshez rendszerint
egyszerűbbek a visszatérési értékek és a közös cleanup ágak.
Linux/POSIX: egy alapértelmezett leállító signal nem
az exit hívásán keresztül szünteti meg a folyamatot. Az
atexit callbackekre és a stdio ürítésére ilyenkor nem
támaszkodhatunk. A SIGKILL nem fogható el és nem
blokkolható.
Egy SIGTERM kezelőjéből sem helyes tetszőleges
cleanupkódot meghívni. A handler megszakíthatott egy könyvtári
műveletet; a stdio-függvények és az exit nem általánosan
async-signal-safe műveletek.
A szokásos terv: a handler megengedett módon jelzi a leállítási igényt, a normál vezérlés pedig elvégzi a cleanupot. Ennek pontos feltételeit a signal és graceful shutdown rész adja meg. Még ilyen terv mellett is kell helyreállítási stratégia a kikényszerített leállásra.
Linux/POSIX alatt egy teljesen pufferelt streamben ki nem írt adat
van. Mi várható exit, _exit és
abort esetén? Melyik garantálja a tartósságot?
Készíts finish_output(int prior_error) függvényt a
shutdown.c programhoz. Előfeltétele, hogy
owned érvényes stream. Ürítési hiba után is zárjon, és
minden hibát őrizzen meg a kilépési állapotban.
A jump.c programban elhagyják a volatile
minősítőt. Emellett a deep_call fájlt nyit, de a
longjmp miatt nem éri el az fclose hívását. Mi
a két különálló probléma?
Az exit megkísérli az ürítést és a lezárást; az I/O
sikere nem garantált. A POSIX _exit nem üríti a
stdio-puffert. Az abort esetén nem szabad hordozható
ürítési garanciát feltételezni. Tartósságot egyik sem garantál.
Ellenőrzési feltétel: különítsük el az ürítési kísérletet, annak sikerét és a tartós tárolást.
A következő részlet a close_owned után illeszthető; az
eredeti fordítási parancsok használhatók:
static int finish_output(int prior_error)
{
int failed = prior_error != 0;
if (fflush(owned) == EOF) {
failed = 1;
}
if (close_owned() == EOF) {
failed = 1;
}
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
}A main sikeres megnyitást követő része így
helyettesíthető:
return finish_output(fputs("completed\n", owned) == EOF);Ellenőrzési feltétel: minden hibaág eléri a
lezárást, a korábbi hiba nem vész el, és a fallback már
NULL értéket talál.
A módosított, nem volatile phase értéke a
visszaugrás után indeterminate; nincs garantált eredmény. Ettől
függetlenül kimarad a megnyitott stream explicit lezárása.
A volatile visszaállítása csak az első problémát
javítja. A stream tulajdonlását olyan szintre kell helyezni, amely a
visszaugrás után is elvégzi és ellenőrzi a lezárást.
Ellenőrzési feltétel: mind a változóérték érvényességére, mind az erőforrás minden vezérlési úton történő lezárására külön indoklás szükséges.
A leállítási utak eltérő runtime-garanciákat adnak. A kritikus I/O-t
a végső kilépési állapot kiválasztása előtt kell ellenőrizni; az
atexit legfeljebb kiegészítő védelem. A kernel
erőforrás-visszavétele nem helyettesíti a tranzakciólezárást. A
longjmp pedig csak a vezérlést helyezi vissza: sem az
erőforrásokat, sem az alkalmazási állapotot nem állítja automatikusan
helyre.