A záróreview nem egyszerűen annak ellenőrzése, hogy a szolgáltatás néhány helyes kérésre válaszol-e. Azt kell megmutatnia, hogy a könyvtári mag, a CLI és a TCP-adapter ugyanazt a dokumentált szerződést valósítja meg, erőforrásaikat hibautakon is rendezik, és viselkedésük reprodukálhatóan vizsgálható.
A Nonblocking TCP adapter és leállítás állapotgépét most nem tervezzük újra: az állapotátmenetekhez ellenőrizhető invariánsokat és teszteket rendelünk. A C minőségi kapuk és hibajavítási jegyzőkönyv módszeréből pedig kiadási bizonyítékcsomagot készítünk. Ez lesz a későbbi C++-port összehasonlítási alapja.
Az alábbi kód és parancsok laborreceptek; végrehajtott fordítást, sikeres tesztet vagy mért teljesítményt nem dokumentálnak.
Az auditálható szerződés előfeltételek, megfigyelhető eredmények és megőrzendő invariánsok együttese. Például az „allokációs hibát kezelünk” önmagában nem szerződés. Pontosabb állítás:
Ha a konstrukció bármely allokációja meghiúsul, a függvény hibát jelez, nem ad át részlegesen inicializált objektumot, a kimeneti paraméter változatlan marad, és minden addig megszerzett erőforrást felszabadít.
Az audit három eltérő bizonyítékot kapcsol össze:
Egyik sem helyettesíti a másikat. Egy sikeres sanitizer-futás nem bizonyítja az összes lehetséges végrehajtás helyességét; egy helyesnek tűnő cleanup-gráf pedig nem igazolja, hogy a kiadott bináris valóban abból a forrásból készült.
A példák választott, itt nem ellenőrzött referenciaprofilja: Linux hosted környezet, x86-64 System V ABI, LP64 adatmodell, nyolcbites C bájt; GCC 14.2.0 és Clang 19.1.7, glibc 2.40. A tényleges laborban rögzítsük a kernel, a linker és az elemzőeszközök verzióját is. Más verzió használata nem önmagában hiba, hanem másik dokumentált profil.
A megadott anyag nem tartalmaz ellenőrzött külső forrásjegyzetet. A verziófüggő eszközviselkedést ezért a tényleges labor eszközdokumentációjával kell megerősíteni; az alábbi kapcsolók nem helyettesítik ezt az ellenőrzést.
Az ownership, vagyis tulajdonlás itt a lezárási felelősség egyértelmű hozzárendelése. A kölcsönzött hozzáférés nem hoz létre új tulajdonost, és nem hosszabbítja meg az erőforrás élettartamát.
Az audit első kézzelfogható eredménye egy kitöltött ownership-térkép:
| Erőforrás | Tulajdonos | Átadás vagy élettartamhatár | Invalidálás | Hibautas kötelezettség |
|---|---|---|---|---|
| Figyelő socket | Szolgáltatásvezérlő | Sikeres létrehozástól lezárásig | Lezárás | Inicializálási hiba után is lezárni |
| Elfogadott socket | Kezdetben az elfogadó ág, majd a kapcsolatobjektum | Csak sikeres kapcsolat-inicializáláskor kerül át | Kapcsolat lezárása | Sikertelen átadáskor az elfogadó ág zárja |
| Bemeneti és kimeneti tároló | Kapcsolatobjektum | Sikeres foglalástól felszabadításig | Felszabadítás, esetleges újrafoglalás | Részleges inicializálásnál is rendezni |
| Parser bemeneti nézete | Nincs saját tulajdonlása | Legfeljebb az API-ban engedett időtartamig | Tömörítés, felülírás, újrafoglalás | Nem tárolható el engedély nélkül |
| Válaszhoz tartozó adatok | A konkrét válaszszerződés szerint | A küldés befejezéséig szükségesek | Korai felszabadítás vagy felülírás | Hiba esetén is pontosan egyszer rendezni |
Különösen veszélyes a feldolgozott bemenetre mutató válasz. Ha a parser kölcsönzött szeletet ad vissza, a TCP-adapter pedig később küldi el, akkor a bemeneti buffer tömörítése már a küldés előtt invalidálhatja a szeletet. Javításként vagy saját válaszpéldányt készítünk, vagy az eredeti tároló élettartamát és változatlanságát biztosítjuk a küldés végéig.
A kapcsolatállapot egyik alapinvariánsa:
Az elküldendő tartomány . Ez nem a teljes kapacitás, és nem nullterminált sztringszerződés.
Bizonyításvázlat. Kezdetben
sent == out_len == 0. A válasz előállítása csak elegendő
kapacitás mellett növelheti az out_len értékét. Pozitív,
hosszúságú írás után kizárólag akkor növeljük sent értékét
-val,
ha
.
Átmeneti írhatatlanság nem módosítja a számlálókat. A buffer
újrafelhasználása csak a küldés befejezésekor vagy a kapcsolat
megszüntetésekor engedett.
A cleanup-ágakat megszerzési sorrendben járjuk végig: mi történik a
socket létrehozása, a kapcsolatobjektum foglalása, majd valamelyik
buffer foglalása után bekövetkező hibánál? A már lezárt descriptor
tulajdonosi mezője kapjon érvénytelen őrértéket, például
-1-et.
Linux-specifikus megjegyzés: a close()
hibájára nem szabad automatikus újrapróbálkozó ciklust építeni. A
numerikus descriptor időközben új erőforráshoz tartozhat; a tényleges
célrendszer lezárási szabályait külön kell ellenőrizni.
A sanitizer, a hibainjektálás és a fuzzolás különböző kérdésekre válaszol.
GCC/Clang eszközprofil: az AddressSanitizer, röviden ASan, számos tiltott memória-hozzáférést felismerhet. Az UndefinedBehaviorSanitizer, röviden UBSan, többek között bizonyos túlcsordulási és igazítási hibákat jelezhet. Nem teljes UB-detektorok. A szabályos tartományon belüli, de szemantikailag hibás válasz egyiknek sem feltétlenül feltűnő. A leak-ellenőrzés támogatását és bekapcsolását külön rögzítsük.
A hibainjektálás egy hibaforrás irányított helyettesítése. Allokációnál például a kijelölt sorszámú foglalás tér vissza sikertelenül. A számlálás minden teszteset elején újraindul, és csak a vizsgált komponens foglalásaira vonatkozik.
Az alábbi önálló audit_cleanup.c nem a projekt parserét
helyettesíti: a kétlépcsős konstrukció auditmintáját mutatja. Minimum
ISO C17; a fenti hosted GCC/Clang-profilhoz használható, platform-API
nélkül.
Az allocator tesztkontextusnak az utolsó felszabadításig
élnie kell. A result tulajdonolja a data
tárolót; a hívó tulajdonolja a sikeresen visszaadott result
objektumot. A create_result() előfeltétele érvényes
a és out, továbbá *out == NULL. A
hibakimenet -1, változatlan kimeneti pointerrel.
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
struct allocator {
size_t calls;
size_t fail_at;
size_t live;
};
struct result {
unsigned char *data;
size_t size;
};
static void check(int condition)
{
if (!condition)
abort();
}
static void *take(struct allocator *a, size_t n)
{
++a->calls;
if (a->calls == a->fail_at)
return NULL;
void *p = malloc(n);
if (p != NULL)
++a->live;
return p;
}
static void release(struct allocator *a, void *p)
{
if (p != NULL) {
check(a->live != 0);
--a->live;
free(p);
}
}
static int create_result(struct allocator *a, struct result **out)
{
struct result *r = take(a, sizeof *r);
if (r == NULL)
return -1;
r->data = take(a, 32);
if (r->data == NULL) {
release(a, r);
return -1;
}
r->size = 32;
memset(r->data, 0, r->size);
*out = r;
return 0;
}
static void destroy_result(struct allocator *a, struct result **out)
{
struct result *r = *out;
if (r != NULL) {
release(a, r->data);
release(a, r);
*out = NULL;
}
}
int main(void)
{
for (size_t fail = 1; fail <= 3; ++fail) {
struct allocator a = {0, fail, 0};
struct result *r = NULL;
int rc = create_result(&a, &r);
if (fail <= 2) {
check(rc == -1 && r == NULL);
} else {
check(rc == 0 && r != NULL);
check(r->size == 32);
}
destroy_result(&a, &r);
check(r == NULL && a.live == 0);
}
return 0;
}A data elemeire mutató bármely kölcsönzött pointer a
destroy_result() során invalidálódik. A tesztkontextus nem
általános allokátorkönyvtár: egyszálú, rövid, korlátos tesztre
készült.
Fordítás külön diagnosztikai binárisokba:
gcc -std=c17 -O1 -g -Wall -Wextra -Wpedantic \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-fno-sanitize-recover=undefined \
audit_cleanup.c -o audit-cleanup-gcc
clang -std=c17 -O1 -g -Wall -Wextra -Wpedantic \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-fno-sanitize-recover=undefined \
audit_cleanup.c -o audit-cleanup-clangA várt eredmény az első két esetben konstrukciós hiba, a harmadikban
siker; minden eset végén nulla élő foglalás. A sikeres eset feltétele,
hogy a valódi malloc() hívások sikerüljenek. A
tesztellenőrzések szándékosan nem assert alapúak:
NDEBUG mellett sem tűnnek el.
A projektben ugyanígy járjuk be a konstrukció, a buffernövelés és a válaszelőállítás foglalási pontjait. A sikeres út foglalásszáma csak kiindulópont: a hibakezelés maga is tartalmazhat új foglalást.
A fuzz-orákulum az a szabály, amely eldönti, hogy egy végrehajtás hibás-e. A „nem omlott össze” szükséges, de gyenge orákulum.
A parserhez célszerű azonos bemenetet kétféleképpen beadni: egyetlen
darabban és több részletben. Előfeltétel, hogy ugyanaz a
parserkonfiguráció, ugyanaz az EOF-esemény és elegendő kimeneti
kapacitás álljon rendelkezésre. Az összehasonlítás a végső
üzenetsorozatra és hibakategóriára vonatkozik, nem az egyes
feed hívások köztes státuszára.
A harness ellenőrizze továbbá, hogy:
Clang/LibFuzzer eszközprofilban az adapter belépési
pontja LLVMFuzzerTestOneInput; a
-fsanitize=fuzzer,address,undefined kapcsolóval készített
célprogram nem tartalmaz saját main() függvényt. A fuzzer
bemenete kölcsönzött: a projekt nem szabadíthatja fel és nem őrizheti
meg a híváson túl.
POSIX socket-szerződés: a TCP bájtfolyam, ezért a küldési hívások határai nem képeznek üzenethatárokat. Az olvashatósági vagy írhatósági jelzés sem ígéri a teljes protokollüzenet feldolgozhatóságát.
A determinisztikus tesztben ezért helyettesíthető I/O-réteg adja elő a részleges eredményeket; a valódi socketes teszt külön ellenőrzi az integrációt. A socketbufferek csökkentése önmagában nem garantál egy meghatározott rövid írást.
| Teszthelyzet | Elvárt ellenőrzés |
|---|---|
| Egy keret több bemeneti részletben | Pontosan egy feldolgozott kérés |
| Több keret egy olvasásban | Minden teljes keret feldolgozódik, helyes sorrendben |
Rövid írás, majd EAGAIN |
A küldési pozíció megmarad, nincs duplikáció |
EINTR adatátvitel nélkül |
A számlálók nem változnak |
| Peer féllezárása | A teljes kérések és a csonka maradék külön kezelése |
| Nem olvasó kliens | Korlátos kimeneti memória és érvényes leállítási határidő |
| Peer eltűnése küldés közben | Kapcsolati hiba, nem kontrollálatlan folyamatleállás |
Kidolgozott példa: egy hét C bájtos válasznál a helyettesített író
előbb 3, majd EAGAIN, végül 4
eredményt ad. A sent sorozata:
A második pozitív írásnak a negyedik bájttól kell indulnia. Az első címtől történő újraküldés tartalmilag hibás, még akkor is, ha egyetlen memóriaellenőrző sem jelez.
A leállítás tesztelése előtt rögzítsük, mit jelent a befogadott kérés. Egy használható auditpolicy szerint ez olyan teljes, érvényes kérés, amelyhez az adapter már biztosította a válaszhoz szükséges helyet. A stop esemény észlelése után nincs új befogadás; a már sorban álló válaszok a határidőig ürülhetnek, majd minden kapcsolat lezárul. A félkész bemenet nem válik utólag befogadott kéréssé.
A teszt egy befogadott és egy csonka kéréssel indít leállítást. Nem rögzített idejű alvásokkal találgat: megfigyelhető állapotokra és határidős várakozásokra támaszkodik. Külön eset vizsgálja a válaszokat nem olvasó klienst. A határidő számítása monoton órát használjon; a leállítás észlelési késleltetését külön korlátozzuk.
A review tárgya a kiadott bináris is. Az auditcsomagban szerepeljen a forrás commitazonosítója és módosítottsága, a teljes fordítási és linkelési parancs, a fordítóverzió, a célarchitektúra, valamint az artifact ellenőrző összege.
Tartsunk külön:
GNU binutils eszközprofil: ha az auditált artifact
neve build/release/diag, az alábbi parancsok futtatás
nélkül vizsgálják a binárist:
readelf -hW build/release/diag
readelf -dW build/release/diag
readelf --dyn-syms -W build/release/diag
nm -u build/release/diagAz ELF-fejlécből a formátumot és géptípust, a dinamikus szakaszból többek között a deklarált közvetlen függőségeket vizsgáljuk. A feloldatlan dinamikus szimbólum önmagában nem linkelési hiba: annak futásidejű feloldása lehet szándékos.
A DT_NEEDED lista nem teljes futásidejű függőségtérkép:
tranzitív és dinamikusan betöltött komponensek is létezhetnek.
Ellenőrizzük, hogy a release-be nem került sanitizer-runtime, és a
megosztott könyvtár exportlistáján nem jelentek meg véletlenül belső
segédfüggvények.
Mérés előtt rögzítsük a kérdést. Más kísérlet kell a kapcsolatmemória felső korlátjához, és más az egy kérésre jutó rendszerhívások számához.
A jegyzőkönyv tartalmazza a CPU-t, a kernelt, a libc-t, a konténer- és cgroup-korlátokat, a terhelésgenerátor helyét, a kéréseloszlást, a párhuzamos kapcsolatok számát, a bemelegítést és az ismétlésszámot. A localhostteszt elsősorban helyi adapterköltséget mutat, nem távoli hálózati teljesítményt.
| Mennyiség | Lehetséges megfigyelés | Korlát |
|---|---|---|
| Élő kért heapméret | Saját allokátorhatár számlálói | Nem tartalmazza az allokátor teljes overheadjét |
| RSS/PSS | Linux /proc memóriaadatok |
Nem azonos a heapfoglalással |
| Allokációhívások | malloc/calloc/realloc körüli
számlálás |
Csak a lefedett foglalási útvonalakat látja |
| Rendszerhívásszám | Linux strace összesítés |
A tracing módosítja a futást |
| Késleltetés és throughput | Külön, tracing nélküli mérés | Terheléstől és ütemezéstől függ |
Ha a kapcsolatobjektum külön foglalt, és két külön foglalt buffert tulajdonol, akkor a közvetlenül kért tároló:
Itt minden tag C bájtban értendő. Beágyazott tömböt nem adunk hozzá még egyszer. A képlet nem tartalmaz allokátormetadata-, stack- vagy kernel-socketbuffer-költséget, ezért nem RSS-becslés.
Egy rögzített mérési intervallumban, sikeresen kiszolgált kérésre:
ahol az intervallumban számlált allokációhívások, a kiválasztott rendszerhívások száma. Rögzíteni kell, hogy a kapcsolatlétrehozás benne van-e az intervallumban, és mely syscallok kerültek -be.
Ne vonjuk össze a sanitizeres, tracinges és tiszta release-mérést. Mindegyik más kérdésre válaszol. A felszabadítás után magas maradó RSS sem bizonyít automatikusan leaket: az allokátor megtarthat felszabadult tárolót. Az ownership-számlálás és az OS-szintű memóriaadat együtt értelmezendő.
A későbbi összehasonlítás feltétele a megfigyelhető viselkedési szerződés befagyasztása. Ez nem azonos belső adatszerkezeteket követel, hanem azonos külső eredményt.
Rögzítsük a protokoll pontos bájtsorozatait, keret- és kapcsolatszámkorlátait, a hibakategóriákat, a válaszok sorrendjét, az EOF és a leállítás jelentését. A CLI-nél ide tartoznak a kilépési kódok és a standard kimenetre kerülő géppel feldolgozható adatok is.
A közös tesztcsomag tartalmazzon bemenet–eredmény párokat, darabolási menetrendeket és hibainjektálási forgatókönyveket. Időbélyeges diagnosztikai sorok helyett strukturált eseményeket hasonlítsunk össze, ha a szöveges forma nem része a szerződésnek.
A C++-változatnak nem kell ugyanannyi malloc() hívást
vagy ugyanazokat a belső szimbólumokat produkálnia. Az allokáció,
memória és syscallszám összehasonlítási mutató; a funkcionális
megfelelés külön kapu.
Az elfogadó ág létrehoz egy socketet, majd kapcsolatobjektumot foglal. Sikertelen foglaláskor hibát naplóz és visszatér. Kié a socket, és mely ellenőrzés hiányzik?
Egészítsd ki a result típust egy második, külön foglalt
tárolóval. A konstrukció most három allokációt végezzen. Ellenőrizd
mindhárom meghibásodási pontot és a sikeres esetet.
A helyettesített író 3, EAGAIN,
4 eredményt ad. A kliens hét helyett tíz bájtot kap, mert
az első három bájt megismétlődik. Miért lehet tiszta az ASan-futás, és
milyen állapotot kell naplózni?
A C-programot sanitizerrel, a C++-változatot optimalizált release-ben mérték. A C++-változat kisebb RSS-t mutatott. Levonható-e következtetés a nyelvek memóriahatékonyságáról?
A sikeres átadásig az elfogadó ág a tulajdonos. Sikertelen kapcsolatfoglaláskor neki kell lezárnia a socketet. Ellenőrzési feltétel: ismételt injektált hibák után a folyamat nyitott descriptorainak száma nem növekszik tartósan, és ugyanazt a descriptort nem zárják kétszer.
A harmadik foglalás hibájánál fel kell szabadítani a második lépésben
létrehozott tárolót és a result objektumot. A destruktor
mindkét tárolót rendezze. A fail_at értékei legyenek
1, 2, 3, majd 4; az
utolsó nem talál foglalási pontot. Minden eset után
live == 0, hibánál pedig a kimeneti pointer változatlanul
null marad.
A hiba protokollszintű duplikáció, nem feltétlenül tiltott
memória-hozzáférés. Naplózandó a küldés előtti sent, a kért
hossz és a visszatérési eredmény. A harmadik hívás kezdőpozíciója
3, kért hossza 4 legyen. Az orákulum a teljes
fogadott bájtsorozatot is ellenőrizze.
Nem. A sanitizer saját memóriaköltsége és az eltérő optimalizálási profil összekeveredik az implementáció különbségével. Azonos terheléssel, összevethető release-profilban, több ismétléssel kell újramérni. Az RSS mellé kértbájt- és allokációszámlálás is szükséges.
A záróreview eredménye nem egyetlen „sikeres” jelzés, hanem visszakövethető bizonyítékcsomag: ownership-térkép, invariánsok, hibautas tesztek, parser-fuzzolás, I/O- és shutdown-vizsgálat, binárisaudit és dokumentált mérés. A C++-port akkor hasonlítható tisztességesen a C-változathoz, ha előbb ugyanazt a funkcionális szerződést teljesíti, és csak ezután értékeljük az eltérő erőforrás- és teljesítményköltségeket.