A hibakezelési szerződés nem pusztán azt mondja meg, hogy egy művelet sikertelen volt. A hívónak azt is tudnia kell, melyik művelet hibázott, milyen hibakategóriában, és meddig érvényesek a kapott hibaadatok. Különösen fontos ez cleanup közben: a felszabadítás vagy lezárás maga is hibázhat, és közben felülírhatja az eredeti hibára utaló állapotot.
A Tulajdonlási szerződés és cleanup ágak részben kialakított egyetlen lezárási felelősséget most hibamegőrzéssel egészítjük ki. A Formázás és megbízható szövegfeldolgozás tanulságát is megtartjuk: az eredmény értékét és a művelet sikerességét csak a konkrét API szerződése alapján lehet megkülönböztetni.
A hibajelző visszatérési érték, vagy sentinel, az
eredmények közül fenntartott érték: például a fopen null
pointerrel jelzi a sikertelenséget. Az explicit hibakód
közvetlenül az eredmény része. Az errno
ezzel szemben különálló, módosítható diagnosztikai állapot.
| Modell | Példa | Mit kell vizsgálni? |
|---|---|---|
| Sentinel | ISO C17 fopen |
A visszaadott pointer null-e |
| Sentinel és további hibakód | POSIX fopen |
Null eredmény után az errno |
| Több összetevőből álló konverziós eredmény | ISO C17 strtol |
Végpointer, visszatérési érték és az előírt módon előkészített
errno |
| Explicit hibakód | POSIX pthread_create |
A visszaadott kód: nulla siker, egyébként maga a hibaszám |
| Saját eredménytípus | Az alábbi példa | A visszaadott struktúra kategóriája és mezői |
ISO C17: az errno az
<errno.h> fejlécben meghatározott makró, amely
módosítható, int típusú balértéket biztosít. Ne deklaráljuk
saját kezűleg extern int errno alakban. Állapota szálanként
különálló; nem egyetlen, minden szál által közösen írt egész
változó.
Az errno nem általános sikerességi
teszt. Korábbi hiba értéke megmaradhat benne, és egy sikeresen
visszatérő könyvtári függvény is módosíthatja. Ezért az alábbi
gondolatmenet hibás:
„Meghívtam egy függvényt, az
errnonem nulla, tehát a függvény hibázott.”
Először a dokumentált sikertelenségi feltételt kell ellenőrizni. Az
errno csak akkor értelmezhető, ha az adott API szerződése
azt ehhez a helyzethez hozzárendeli. Az ISO C17
fopen null eredményére vonatkozó garanciájából önmagában
nem következik POSIX-szerű errno-diagnosztika; ezt a
POSIX-szerződés adja hozzá.
A strtol külön figyelmet igényel, mert például a nulla
érvényes eredmény is lehet. Előtte errno = 0 szükséges a
tartományhiba felismeréséhez; utána a végpointer jelzi, történt-e
konverzió, az ERANGE pedig a dokumentált tartományhibát. Ez
speciális szerződés, nem univerzális „nullázd, hívd, nézd” recept.
Az alkalmazási tartományhiba (domain error)
azt jelenti, hogy a bemenet kívül esik az alkalmazás által elfogadott
értékek vagy állapotok halmazán. Például egy időkorlát csak az
tartományban lehet érvényes. Ez nem azonos a matematikai könyvtár
EDOM hibakódjával.
Az OS-hiba egy rendszerhez kötődő művelet hibája,
például sikertelen fájlmegnyitás. A hiba eredetét nem az dönti el, hogy
errno közvetítette-e: a strtol által jelzett
ERANGE konverziós tartományhiba, nem fájlrendszer- vagy
kernelhiba.
Az újrapróbálhatóság (retriable/non-retriable) ettől független döntés:
EAGAIN megfelelő várakozás után
kezelhető lehet;A retry-döntéshez tehát a hibakód mellett kell a művelet, a már
bekövetkezett mellékhatás, az idempotencia és az időkeret is. A
fclose sikertelensége például nem engedély arra, hogy
ugyanazzal a FILE * értékkel ismét lezárást próbáljunk.
Írjunk olyan függvényt, amely időkorlátot ment egy szövegfájlba. Szerződése:
errno-ba is; más eredménynél az errno nem
része a szerződésnek.A path előfeltétele: a hívás teljes ideje alatt
olvasható, érvényes nullterminált sztringre mutat. A függvény nem
ellenőrizheti általánosan egy tetszőleges pointer érvényességét.
Profil: ISO C17 nyelv, Linux/POSIX libc-szerződés a hibaszámokhoz. Választott fordítási profil: GCC 14.2.0, glibc 2.40, x86-64 Linux. Az alábbi példa nem mért vagy igazoltan tesztelt futás eredménye.
/* error_contract.c */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
enum error_kind {
APP_OK,
APP_TIMEOUT_RANGE,
APP_OS_ERROR
};
enum operation {
OP_NONE,
OP_VALIDATE,
OP_OPEN,
OP_WRITE,
OP_CLOSE
};
struct app_error {
enum error_kind kind;
enum operation operation;
int os_errno;
int cleanup_errno;
};
static struct app_error
write_timeout(const char *path, unsigned seconds)
{
struct app_error error = { APP_OK, OP_NONE, 0, 0 };
if (seconds < 1U || seconds > 3600U) {
return (struct app_error) {
APP_TIMEOUT_RANGE, OP_VALIDATE, 0, 0
};
}
FILE *file = fopen(path, "w");
if (file == NULL) {
const int original_errno = errno;
return (struct app_error) {
APP_OS_ERROR, OP_OPEN, original_errno, 0
};
}
if (fprintf(file, "timeout=%u\n", seconds) < 0) {
const int original_errno = errno;
error = (struct app_error) {
APP_OS_ERROR, OP_WRITE, original_errno, 0
};
}
if (fclose(file) == EOF) {
const int close_errno = errno;
if (error.kind == APP_OK) {
error = (struct app_error) {
APP_OS_ERROR, OP_CLOSE, close_errno, 0
};
} else {
error.cleanup_errno = close_errno;
}
}
/* A stream lezárult; a file értékét többé nem használjuk. */
if (error.kind == APP_OS_ERROR) {
errno = error.os_errno;
}
return error;
}
int main(int argc, char *argv[])
{
if (argc != 2) {
(void)fprintf(stderr, "usage: error_contract PATH\n");
return EXIT_FAILURE;
}
const struct app_error error = write_timeout(argv[1], 30U);
if (error.kind != APP_OK) {
(void)fprintf(stderr,
"kind=%d operation=%d errno=%d cleanup_errno=%d\n",
(int)error.kind, (int)error.operation,
error.os_errno, error.cleanup_errno);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}Fordítás a választott profilhoz tartozó gcc
használatával:
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 \
error_contract.c -o error_contractA fprintf sikeressége még nem jelenti, hogy a pufferelt
adat később is hibamentesen kiírható. Ezért a fclose
eredményét önállóan ellenőrizzük. A sikeres lezárás sem tartóssági
garancia; annak követelményeit a Fájlrendszer-műveletek, mmap és tartósság
tárgyalja.
| Objektum vagy erőforrás | Tulajdonos | Élettartam és érvényesség | Hibakimenet |
|---|---|---|---|
path mögötti sztring |
Hívó | A függvény csak kölcsönzi a hívás idejére; felszabadítás vagy idő előtti élettartamvég érvényteleníti | Előfeltétel-sértést a hibakód nem tesz biztonságossá |
| Megnyitott stream | write_timeout |
Sikeres megnyitástól az egyszeri fclose hívásig |
Megnyitási, írási vagy lezárási OS-hiba |
Visszaadott app_error |
Hívó | Saját érték; könyvtári hívások nem érvénytelenítik, saját felülírása vagy élettartamvége igen | Elsődleges és opcionális másodlagos hiba |
Bizonyításvázlat. Megnyitási hiba esetén nincs
lezárandó stream. Sikeres megnyitás után minden vezérlési út pontosan
egyszer jut el a fclose hívásig. Írási hiba esetén az
elsődleges hibakódot még a cleanup előtt lemásoljuk. A lezárási ág ezt
csak akkor tölti ki, ha korábban nem volt hiba; egyébként kizárólag a
másodlagos mezőt módosítja. Így az első hiba nem veszhet el.
A függvény nem tranzakciós: a "w" mód
már megnyitáskor csonkolhatja a létező fájlt. Sikertelen eredmény
mellett is lehet mellékhatás, ezért automatikus újrapróbálás vagy „hiba
esetén minden változatlan” garancia nem következik belőle.
ISO C17: a perror az aktuális
errno alapján ír diagnosztikát a standard hibakimenetre. Ha
a hibás művelet és a perror közé cleanup vagy naplózás
kerül, már más hibát magyarázhat. Előbb mentsük el a hibaszámot; ha
később perror szükséges, közvetlenül előtte állítsuk
vissza.
A strerror(code) a megadott hibaszámhoz tartozó szöveget
adja vissza. Ez nem hívó által birtokolt sztring: nem szabad
felszabadítani vagy módosítani. Az ISO C17 szerződése alapján egy
későbbi strerror hívás felülírhatja a tartalmát, és
általános párhuzamos használati biztonságot sem feltételezhetünk.
A szálanként különálló errno tehát nem teszi
automatikusan szálbiztossá a hibaüzenetek tárolását. Másik
szálnak a hibaszám másolatát kell megfelelő szinkronizációval átadni,
nem arra számítani, hogy ott ugyanaz az errno
olvasható.
A strerror_r nem ISO C API, és neve mögött eltérő
interfészek találhatók:
| Változat | Visszatérés | Következmény |
|---|---|---|
| POSIX/XSI alak | int státusz |
A hívó pufferébe ír; a sikerességet ellenőrizni kell |
| GNU/glibc alak | char * |
A visszaadott pointer nem feltétlenül a megadott pufferre mutat |
A glibc által láthatóvá tett alakot feature-test makrók, például
_GNU_SOURCE, is befolyásolják. Hordozható wrapperhez a
konkrét libc-verzió deklarációját és hibaszerződését ellenőrizni kell; a
két alakot casttal összemosni hibás.
Tartós diagnosztikához általában jobb a hibakategória, művelet és numerikus kód érték szerinti tárolása, majd a szöveg előállítása a megjelenítés helyén. Ha a szöveget is meg kell őrizni, saját tárolóba kell másolni, explicit kapacitással és csonkolási szabállyal. A lokalizált üzenetszöveg nem stabil gépi hibakód.
A cleanup utáni errno elmentése túl késő: mindig
közvetlenül a dokumentált hiba felismerése után másoljuk ki. Ugyanez
vonatkozik a naplózó függvények elé kerülő mentésre.
Az első hibát egy későbbi lezárási hibával felülírni diagnosztikai információvesztés. A két esemény külön mezőt érdemel. Ha viszont korábban minden sikerült, a lezárási hiba lesz az elsődleges.
Hasonlóan hibás az összes nemnulla hibaszámot „OS-hiba” vagy „újrapróbálható hiba” kategóriába sorolni. A kategóriát a művelet szerződése, az újrapróbálhatóságot pedig a teljes végrehajtási helyzet határozza meg.
Egy sikeres fopen után az errno értéke
EACCES. Bizonyít-e ez megnyitási hibát? Mit kell
vizsgálni?
Bővítsd a példát úgy, hogy a seconds == 0 és a
seconds > 3600 külön alkalmazási hibakódot kapjon. Hibás
bemenet esetén semmilyen fájlművelet ne történjen.
Tegyük fel, hogy a tesztadapter írási hibát jelez ENOSPC
kóddal, majd a lezárás is hibázik EIO kóddal. Milyen
mezőértékeket kell kapnia a hívónak? Mi történjen, ha csak a lezárás
hibázik?
Nem bizonyít hibát. A nemnulla pointer a fopen sikerét
jelzi; az errno ilyenkor nem használható a siker
felülbírálására. Ellenőrzési feltétel: a hibakezelési ág kizárólag null
eredmény esetén fusson.
Az alábbi ISO C17 részletek az előző teljes program megfelelő deklarációját és validációs ágát helyettesítik:
enum error_kind {
APP_OK,
APP_TIMEOUT_ZERO,
APP_TIMEOUT_TOO_LARGE,
APP_OS_ERROR
};if (seconds == 0U) {
return (struct app_error) {
APP_TIMEOUT_ZERO, OP_VALIDATE, 0, 0
};
}
if (seconds > 3600U) {
return (struct app_error) {
APP_TIMEOUT_TOO_LARGE, OP_VALIDATE, 0, 0
};
}Ellenőrzési értékek: 0, 1,
3600, 3601. A két szélső érvénytelen esetben a
megnyitást helyettesítő tesztadapter hívásszáma nulla legyen.
Két hiba esetén az elvárt állapot: APP_OS_ERROR,
OP_WRITE, os_errno == ENOSPC,
cleanup_errno == EIO. Visszatéréskor az errno
ismét ENOSPC.
Ha csak a lezárás hibázik: APP_OS_ERROR,
OP_CLOSE, os_errno == EIO,
cleanup_errno == 0. Ellenőrzési feltétel mindkét esetben az
egyszeri lezárás és az elsődleges hibát megőrző eredmény.
A visszatérési érték mutatja meg, mikor vizsgálható a hibajelzés; az
errno önmagában nem sikerességi teszt. Az eredeti hibát
cleanup előtt kell elmenteni. A tartós hibaszerződés saját értékként
tárolja a kategóriát, műveletet és hibakódot, a szöveges magyarázatot
pedig külön kezeli. A tartományhiba, az OS-hiba és az újrapróbálhatóság
három külön döntési szempont.