17.3 Nonblocking TCP adapter és leállítás

A TCP adapter feladata nem a parser újraírása, hanem a bájtfolyam, a kapcsolat élettartama és a korlátozott erőforrások összehangolása. Egy sikeres recv() nem jelent teljes kérést, egy sikeres send() pedig nem jelenti feltétlenül a teljes válasz elküldését. A kapcsolat ezért nem pusztán egy file descriptor: saját bemeneti és kimeneti tárolóval, feldolgozási állapottal, határidőkkel és eseményazonossággal rendelkező objektum.

A C könyvtári mag és CLI megvalósítási labor parserét szinkron adapterhatáron használjuk. A poll/epoll és explicit eseményállapotgép alapjaira építve most azt rögzítjük, mikor szabad új adatot fogadni, mikor érvénytelenednek a parser nézetei, és hogyan bizonyítható a kapcsolat lezárásának helyessége.

17.3.1 Platformprofil és adapterhatár

Platformlabor: Linux hosted környezet, x86-64 System V ABI, LP64, nyolcbites C bájt. A nyelvi alap ISO C17; az epoll, az accept4() és a signalfd Linux API. A profil a központi profiltábla lab-gcc profilja; a clang receptek a lab-clang profilhoz tartoznak. A ténylegesen elvégzett fordítások és futtatások jegyzőkönyve az egyes kódrészek után áll.

A szolgáltatás egyetlen szálon, level-triggered epoll ciklussal működik. Oktatási konfigurációként csak loopback címen figyel, egyszerre legfeljebb 64 kapcsolatot kezel.

A parser köré készülő diag_reply_fn adapter (a projektben a diag_core_reply, lásd lent) három eredményt adhat:

A függvény nem végez I/O-t, nem tart meg pointert, és nem allokál korlátlan mennyiségű tárolót. A bemenet hosszjelölt bájtszelet, nem szükségképpen nullterminált sztring. DIAG_REPLY esetén pozitív fogyasztási és válaszhosszt ad vissza; a válasz teljes egészében a kapcsolat kimeneti tömbjébe kerül.

Projektbeli elfogadási pont: a kérés akkor számít elfogadottnak, amikor a teljes válasz bekerült a kimeneti sorba. A kernelből már átvett, de még fel nem dolgozott bájtok önmagukban nem jelentenek elfogadott kérést. Ez a graceful shutdown szerződésének fontos része.

17.3.1.1 Tulajdonlási és érvényességi térkép

Erőforrás vagy nézet Tulajdonos Érvényesség vége, invalidáló művelet Hibakezelés
Elfogadott socket FD Átadásig az accept ág, utána a kapcsolatrekesz Egyszeri close() Sikertelen inicializáláskor is lezárandó
Bemeneti tömb Kapcsolatrekesz Rekesz megszűnése; tartalma tömörítéskor vagy újrafelhasználáskor változik Túlméretes frame: kapcsolatbontás
Parser bemeneti nézete Nem tulajdonló, csak a szinkron hívás alatt él A hívás vége; későbbi memmove() megváltoztatja a hivatkozott tartalmat Nem tehető várakozó eseménybe
Kimeneti tömb Kapcsolatrekesz Teljes elküldés vagy megszakítás után újrahasználható Részleges küldéskor megőrzendő
Eseménytoken Értékként tárolt slot- és generációazonosító Generációváltás után elavult El kell dobni, nem dereferálható pointer

A parser ideiglenes eredménye például mutathat a bemeneti tömbben található mezőre. Ebből a választ még a parseradapter visszatérése előtt szerializálni kell. A válaszsorba nem a mező pointere, hanem a szükséges bájtok másolata kerül.

17.3.2 Socketállapotok és accept loop

A kapcsolat állapotát a küldési sor foglaltságával együtt értelmezzük.

Állapot Új kérés feldolgozható? Eseményérdeklődés Kilépés
ACTIVE, üres kimenet Igen EPOLLIN, EPOLLRDHUP Válasz, EOF, hiba vagy timeout
ACTIVE, függő válasz Nem EPOLLOUT Válasz elküldése után újra feldolgozás
DRAIN_SEND Nem EPOLLOUT, ha szükséges Kimenet kiürülése után féllezárás
DRAIN_PEER Nem; a bejövő adat eldobandó EPOLLIN, EPOLLRDHUP EOF vagy shutdown-határidő
DEAD Nem Nincs Rekesz később új generációval használható

Linux API. Az elfogadott socket nonblocking és close-on-exec tulajdonságát explicit kérjük:

/* Részlet a listener eseménykezelőjéből; Linux API. */
int fd = accept4(listener_fd, NULL, NULL,
                 SOCK_NONBLOCK | SOCK_CLOEXEC);

Ez részlet, nem önálló program; a listener már inicializált, nonblocking socket.

Az accept loop nem korlátlan ciklus. Egy eseménykezelés legfeljebb például 16 accept-kísérletet végez:

  1. Szabad kapcsolatrekesz hiányában megszünteti a listener eseményfigyelését.
  2. EAGAIN vagy EWOULDBLOCK esetén visszatér az eseményciklushoz.
  3. EINTR esetén újrapróbálhat, de a kísérlet beleszámít a keretbe.
  4. Siker után inicializálja a rekeszt, regisztrálja az FD-t, majd átadja a tulajdonlást.
  5. Bármely közbülső hiba esetén lezárja a még át nem adott FD-t.

Az EMFILE és ENFILE nem indokolhat gyors, végtelen újrapróbálást. Ilyenkor a listener figyelését időlegesen fel kell függeszteni, és későbbi, határidővezérelt próbát ütemezni. A kapcsolatlimit elérésekor ugyanez akadályozza meg a folyamatosan olvashatónak jelzett listener miatti pörgést. Rekesz felszabadulásakor a figyelés újra engedélyezhető.

A kernel listen backlogja nem a felhasználói kapcsolattábla része: a 64-es limit önmagában nem korlátozza az összes kerneloldali hálózati memóriát.

17.3.3 Korlátos input/output sor

A legegyszerűbb backpressure-szabály: kapcsolatonként egyszerre legfeljebb egy teljes válasz várakozhat. Amíg az nincs elküldve, nem fogadunk új adatot, és nem dolgozunk fel további kérést.

Az alábbi komponens az aktív I/O-t és a kimenetürítést valósítja meg. A listener, az epoll-regisztráció, az időzítés és a végső teardown a szerveroldali vezérlő feladata.

17.3.3.1 Fordítható I/O-komponens

Az include/diag_tcp.h fájl:

#ifndef DIAG_TCP_H
#define DIAG_TCP_H

#include <stdbool.h>
#include <stddef.h>

#define DIAG_IN_CAP  4096u
#define DIAG_OUT_CAP 1024u

enum diag_core_result { DIAG_MORE, DIAG_REPLY, DIAG_BAD };
enum diag_step {
    DIAG_KEEP, DIAG_DONE, DIAG_BAD_FRAME,
    DIAG_SYS_ERROR, DIAG_CORE_ERROR
};

typedef enum diag_core_result (*diag_reply_fn)(
    const unsigned char *input, size_t input_size,
    size_t *consumed,
    unsigned char *output, size_t output_capacity,
    size_t *produced);

struct diag_io {
    int fd;
    bool draining;
    size_t used;
    size_t off, end;
    unsigned char input[DIAG_IN_CAP];
    unsigned char output[DIAG_OUT_CAP];
};

/* A core-adapter (src/tcp_reply.c): egy kérés = egy rekordsor. */
enum diag_core_result diag_core_reply(
    const unsigned char *input, size_t input_size,
    size_t *consumed,
    unsigned char *output, size_t output_capacity,
    size_t *produced);

enum diag_step diag_read(struct diag_io *, diag_reply_fn);
enum diag_step diag_write(struct diag_io *, diag_reply_fn);
enum diag_step diag_quiesce(struct diag_io *);

#endif

Az src/tcp_io.c fájl:

#include "diag_tcp.h"

#include <errno.h>
#include <string.h>
#include <sys/socket.h>

static enum diag_step prepare(struct diag_io *c,
                              diag_reply_fn reply)
{
    if (c->draining)
        return DIAG_DONE;
    if (c->used == 0)
        return DIAG_KEEP;

    size_t consumed = 0;
    size_t produced = 0;
    enum diag_core_result r = reply(
        c->input, c->used, &consumed,
        c->output, sizeof c->output, &produced);

    if (r == DIAG_BAD)
        return DIAG_BAD_FRAME;

    if (r == DIAG_MORE) {
        if (consumed != 0 || produced != 0)
            return DIAG_CORE_ERROR;
        return c->used == sizeof c->input
             ? DIAG_BAD_FRAME : DIAG_KEEP;
    }

    if (r != DIAG_REPLY ||
        consumed == 0 || consumed > c->used ||
        produced == 0 || produced > sizeof c->output)
        return DIAG_CORE_ERROR;

    /*
     * A reply() már visszatért: nincs élő borrowed
     * parsernézet, ezért a bemenet tömöríthető.
     */
    memmove(c->input, c->input + consumed,
            c->used - consumed);
    c->used -= consumed;
    c->off = 0;
    c->end = produced;
    return DIAG_KEEP;
}

enum diag_step diag_read(struct diag_io *c,
                         diag_reply_fn reply)
{
    if (c->draining || c->off != c->end)
        return DIAG_CORE_ERROR;
    if (c->used == sizeof c->input)
        return DIAG_BAD_FRAME;

    ssize_t n = recv(c->fd, c->input + c->used,
                     sizeof c->input - c->used, 0);

    if (n > 0) {
        c->used += (size_t)n;
        return prepare(c, reply);
    }
    if (n == 0)
        return c->used == 0 ? DIAG_DONE : DIAG_BAD_FRAME;
    if (errno == EINTR || errno == EAGAIN ||
        errno == EWOULDBLOCK)
        return DIAG_KEEP;
    return DIAG_SYS_ERROR;
}

enum diag_step diag_write(struct diag_io *c,
                          diag_reply_fn reply)
{
    if (c->off >= c->end)
        return DIAG_CORE_ERROR;

    ssize_t n = send(c->fd, c->output + c->off,
                     c->end - c->off, MSG_NOSIGNAL);

    if (n < 0) {
        if (errno == EINTR || errno == EAGAIN ||
            errno == EWOULDBLOCK)
            return DIAG_KEEP;
        return DIAG_SYS_ERROR;
    }
    if (n == 0) {
        errno = EIO;
        return DIAG_SYS_ERROR;
    }

    c->off += (size_t)n;
    if (c->off != c->end)
        return DIAG_KEEP;

    c->off = c->end = 0;
    return prepare(c, reply);
}

enum diag_step diag_quiesce(struct diag_io *c)
{
    c->draining = true;
    c->used = 0;  /* A még el nem fogadott kérések eldobása. */
    return c->off == c->end ? DIAG_DONE : DIAG_KEEP;
}

Fordítási receptek a két profilhoz (lab-gcc, illetve lab-clang):

gcc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Wpedantic \
  -Wconversion -Wshadow -O2 -g -Iinclude \
  -c src/tcp_io.c -o tcp_io.gcc.o

clang -std=c17 -D_GNU_SOURCE -Wall -Wextra -Wpedantic \
  -Wconversion -Wshadow -O2 -g -Iinclude \
  -c src/tcp_io.c -o tcp_io.clang.o

Ezek objektumfájlt készítenek, nem teljes szervert. A diag_reply_fn megvalósítása a core parser adaptere; ezt a következő szakasz adja meg. A komponenseket a lentebbi „Teljes referencia-szerver” szakasz köti össze futtatható programmá. Kezdetben egy még nem aktív rekesz I/O-része így inicializálható: (struct diag_io){ .fd = fd }. Élő kapcsolatot ezzel felülírni FD-szivárgás lenne.

17.3.3.2 A core-adapter: src/tcp_reply.c

A projektszerződés szerint egy TCP-kérés pontosan egy rekord az első LF-ig, a válasz pedig az a sor, amelyet a CLI erre az egy rekordból álló bemenetre írna. Az adapter ezért a C könyvtári mag változatlan dg_* API-ját hívja: kérésenként rövid életű parsert hoz létre, a sort betáplálja, lezárja, a jelentést vagy a hibát a CLI-vel azonos formátumban a kimeneti tömbbe írja, majd a parsert megszünteti.

#include "diag.h"
#include "diag_tcp.h"

#include <stdio.h>
#include <string.h>

/*
 * Egy TCP-kérés pontosan egy LF-fel lezárt rekordsor. A válasz az
 * a sor, amelyet a CLI erre az egyetlen sorból álló bemenetre a
 * stdout-ra (OK) vagy a stderr-re (ERR) írna.
 */
enum diag_core_result diag_core_reply(
    const unsigned char *input, size_t input_size,
    size_t *consumed,
    unsigned char *output, size_t output_capacity,
    size_t *produced)
{
    const unsigned char *lf = memchr(input, '\n', input_size);
    if (lf == NULL)
        return DIAG_MORE;   /* a felső korlátot a tcp_io.c tartja */

    size_t line = (size_t)(lf - input) + 1;
    dg_parser *parser = dg_create(NULL);
    int n;

    if (parser == NULL) {
        n = snprintf((char *)output, output_capacity,
                     "ERR code=alloc\n");
    } else if (dg_feed(parser, (dg_view){ input, line }) &&
               dg_finish(parser)) {
        dg_report r = dg_get_report(parser);
        n = snprintf((char *)output, output_capacity,
                     "OK records=%zu sum=%llu bytes=%zu\n",
                     r.records, r.sum, r.bytes);
    } else {
        dg_error e = dg_get_error(parser);
        n = snprintf((char *)output, output_capacity,
                     "ERR code=%s offset=%zu\n",
                     dg_code_name(e.code), e.offset);
    }
    dg_destroy(parser);   /* abort: lezárás finish nélkül is */

    if (n < 0 || (size_t)n >= output_capacity)
        return DIAG_BAD;
    *consumed = line;
    *produced = (size_t)n;
    return DIAG_REPLY;
}

A szerződés kapcsolóelemei itt konkrétak:

A függvény nem végez I/O-t, nem tart meg pointert a bemenetre, és egyetlen, rögzített méretű allokációt végez kérésenként. Ha ez a terhelésnél számít, a parserobjektum a kapcsolatrekeszbe is áthelyezhető, dg_allocator segítségével, a viselkedés megváltoztatása nélkül.

A core, az I/O-komponens és az adapter együttes linkelését egy socketpáron futó teszt ellenőrzi. A kliens két érvényes, egy hibás és egy félbemaradt kérést küld egyetlen írással, majd lezárja a küldési irányát:

/* Linkpróba: core + TCP I/O + core-adapter, valódi socketpáron. */
#include "diag_tcp.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define CHECK(condition) do {                                      \
    if (!(condition)) {                                            \
        (void)fprintf(stderr, "CHECK failed at line %d\n", __LINE__); \
        exit(EXIT_FAILURE);                                        \
    }                                                              \
} while (0)

int main(void)
{
    static const char request[] = "a=7\ncpu=12\nA=1\nmem=";
    static const char expected[] =
        "OK records=1 sum=7 bytes=4\n"
        "OK records=1 sum=12 bytes=7\n"
        "ERR code=syntax offset=0\n";
    int sv[2];
    char reply[256];
    size_t got = 0;

    CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0);
    CHECK(write(sv[1], request, sizeof request - 1)
          == (ssize_t)(sizeof request - 1));
    CHECK(shutdown(sv[1], SHUT_WR) == 0);

    struct diag_io c = { .fd = sv[0] };
    enum diag_step step = diag_read(&c, diag_core_reply);

    while (step == DIAG_KEEP) {
        step = c.off != c.end ? diag_write(&c, diag_core_reply)
                              : diag_read(&c, diag_core_reply);
    }
    /* A félbemaradt "mem=" kérés EOF-nál keretszintű hiba. */
    CHECK(step == DIAG_BAD_FRAME);

    for (;;) {
        ssize_t n = recv(sv[1], reply + got, sizeof reply - got,
                         MSG_DONTWAIT);
        if (n <= 0)
            break;
        got += (size_t)n;
    }
    CHECK(got == sizeof expected - 1);
    CHECK(memcmp(reply, expected, got) == 0);

    close(sv[0]);
    close(sv[1]);
    return 0;
}
gcc -std=c17 -O2 -g -Wall -Wextra -Wpedantic -Wconversion -Wshadow \
    -Werror -D_GNU_SOURCE -Iinclude \
    src/tcp_io.c src/tcp_reply.c tests/test_tcp_link.c \
    build/gcc/libdiag.a -o build/gcc/test_tcp_link
build/gcc/test_tcp_link

A libdiag.a a megvalósítási labor buildjéből származik. A teszt AF_UNIX socketpárt használ, mert a recv/send szemantikája ehhez elég, és nem kell portot foglalni; a valódi loopback TCP-teszt ezt kiegészíti, nem helyettesíti.

Buildjegyzőkönyv (lab-gcc profil). A tcp_io.c, a tcp_reply.c és a linkpróba Linux x86-64 környezetben, GCC 13.3.0 és glibc 2.39 mellett -Werror beállítással figyelmeztetés nélkül fordult; a teszt sikeresen lefutott, -fsanitize=address,undefined builddel is. A teljes, epoll-alapú szerver-vezérlő nem része ennek az alfejezetnek, ezért arról futtatási eredményt nem állítunk.

API-előfeltételek: a kapcsolatpointer nem null, az objektum él, az FD érvényes és nonblocking; a callback nem null, a hívás teljes ideje alatt elérhető, és betartja a kapacitásokat. A visszaadott hossz ellenőrzése nem teszi utólag biztonságossá a callback esetleges túlindexelését.

A DIAG_SYS_ERROR után az errno értékét más rendszerhívás előtt kell elmenteni. A DIAG_CORE_ERROR programozási szerződéssértés, nem egyszerűen hibás klienskérés.

17.3.3.3 Részleges frame és backpressure

Tegyük fel, hogy a kérés a 7 oktettes cpu=12\n, de a socket először csak 4-et ad át.

  1. used = 4, a bemenetben még nincs LF, az adapter eredménye DIAG_MORE.
  2. A következő olvasás további 3 oktettet ad: used = 7.
  3. Az adapter 7 oktettet fogyaszt, és a 28 oktettes OK records=1 sum=12 bytes=7\n választ készíti el.
  4. Az első send() például csak 3 oktettet küld: off = 3, end = 28.
  5. A következő küldés címe output + 3, hossza 28−3=2528-3=25.

A kérés és a válasz hossza a szerződésből következik; a darabolás (4+3, illetve 3+25) illusztratív, nem mért eredmény.

Ha ugyanabban az olvasásban a következő kérés eleje is megérkezett, azt a memmove() megőrzi. Különösen fontos, hogy az utolsó válaszbájt elküldése után a prepare() azonnal megvizsgálja a már tárolt bemenetet. Ellenkező esetben a szerver olyan EPOLLIN eseményre várhatna, amely nem érkezik meg: az adat már felhasználói tárolóban van.

Bizonyításvázlat – tárolókorlátok. Az invariánsok:

0≤used≤I,0≤off≤end≤O, 0 \leq used \leq I,\qquad 0 \leq off \leq end \leq O,

ahol I=4096I=4096, O=1024O=1024, az egység C bájt.

A recv() legfeljebb I−usedI-used bájtot kap célterületként. A parser csak üres kimeneti sor mellett fut, és legfeljebb OO bájtot írhat. A send() legfeljebb end−offend-off bájtot kap, ezért pozitív eredménye nem lépteti off értékét end fölé. EAGAIN egyik határt sem módosítja. A megtelt, mégis befejezetlen bemenet elutasítása kizárja a korlátlan növekedést.

A 64 kapcsolat puszta adattömbjeinek felső korlátja:

64(4096+1024)=327680C bájt=320KiB 64(4096+1024)=327680\ \text{C bájt}=320\ \text{KiB}

a választott nyolcbites profilban. Ehhez hozzáadódnak a struktúratagok, padding, időzítők, eseménytömb és a core korlátozott munkaterülete. Ez felhasználói tárolókorlát, nem teljes RSS- vagy kernelmemória-korlát.

A tömörítés egy kérés után legrosszabb esetben O(I)O(I) másolás; folytonos tárolón dolgozik, allokáció nélkül, és érvényteleníti a bemenet korábbi tartalmára építő nézeteket.

17.3.4 FD-generáció és teardown

Az FD egész száma újrahasználható. Egy lezárt fd = 12 helyére később másik kapcsolat kerülhet, miközben egy már kiolvasott eseményköteg még tartalmaz régi bejegyzést.

Ezért az eseményadat nem puszta FD, hanem:

token=(generation≪32)|slot. token = (generation \ll 32)\mathbin{|}slot.

A profilban rendelkezésre álló uint32_t és uint64_t típusokkal a shiftet 64 bites értéken kell végezni. A slotok stabil helyen maradnak a szerver teljes élettartama alatt.

/* Dispatch-részlet; a speciális listener/signal tokenek
   feldolgozása ezt megelőzi. */
uint64_t token = events[k].data.u64;
uint32_t index = (uint32_t)token;
uint32_t generation = (uint32_t)(token >> 32);

if (index >= CONNECTION_LIMIT)
    continue;
if (!slots[index].active ||
    slots[index].generation != generation)
    continue;

Új kapcsolatnál a generáció nő. A körbefordulás nem megengedett: UINT32_MAX elérése után a rekeszt nyugdíjazni kell. Így a stale event, vagyis az elavult esemény nem válhat később ismét érvényessé.

A teardown sorrendje:

  1. A rekesz logikailag inaktívvá válik.
  2. Az FD-t kivesszük az epoll figyeléséből.
  3. A tárolt FD-t -1-re állítjuk, a korábbi értéket egyszer lezárjuk.
  4. Érvénytelenítjük a kapcsolati határidőket és ürítjük a számlálókat.

Linux-specifikus szabály: a close() hibájára nem szabad automatikusan újabb close()-zal válaszolni; az FD száma közben újra felhasználható lehet. A hibát rögzítjük.

Bizonyításvázlat – egyszeri lezárás. Minden hibakimenet ugyanabba a teardown útvonalba vezet. Az útvonal első művelete megszünteti az aktív tulajdonosi állapotot, így későbbi esemény már nem indíthat újabb lezárást. A parserhívás szinkron, ezért teardown közben nincs élő parsernézet. Az új generáció pedig kizárja, hogy régi esemény az új tulajdonost érje el.

17.3.5 Timeout és graceful shutdown

Az időzítők monotón órára épülnek. Példapolicy:

A frame-határidőt nem hosszabbítjuk meg minden beérkező bájttal. Különben a nagyon lassan csöpögtető kliens korlátlan ideig foglalhatna rekeszt. A válasz határideje sem csúszik minden részleges küldéskor.

A következő várakozás a legközelebbi aktív határidőhöz igazodik. A lejáratokat minden eseményköteg után ellenőrizni kell, nem csak akkor, amikor az epoll_wait() timeouttal tér vissza. A folyamatos forgalom különben kiéheztethetné az időzítőket.

Linux API. A blokkolt SIGTERM és SIGINT jeleket signalfd teheti a közös eseményfolyam részévé. A shutdown kezdetekor:

  1. megszüntetjük a listener figyelését és lezárjuk a listenert;
  2. rögzítjük a közös, abszolút shutdown-határidőt;
  3. minden kapcsolatnál meghívjuk a diag_quiesce() függvényt;
  4. csak a már sorba állított válaszokat ürítjük.

A kimenet kiürülése után shutdown(fd, SHUT_WR) következik, majd DRAIN_PEER: korlátozott mennyiségekben olvasunk és eldobunk adatot az EOF-ig vagy a közös határidőig. Ez elkerüli azt a leegyszerűsítést, hogy olvasatlan TCP-adattal azonnal bezárjuk a socketet, ami resethez vezethet. A határidő utáni kényszerlezárás már vállaltan megszakíthat adatátvitelt.

Az EPOLLRDHUP nem azonos a kapcsolat azonnali eldobásával: a peer küldési iránya zárult le, miközben adat még olvasható lehet. Az EOF-ot a recv() == 0 igazolja. A HUP sem helyettesíti a maradék bemenet kezelését; EPOLLERR esetén a socket hibáját külön vizsgáljuk.

A sikeres send() csak a helyi kernel általi átvételt jelenti. Az alkalmazásszintű feldolgozás igazolásához külön protokoll-visszaigazolás kellene.

17.3.6 Teljes referencia-szerver

Az eddigi elemeket — az accept loopot, a generációs tokent, a korlátos I/O-komponenst, a core-adaptert, a határidőket és a signalfd-alapú leállítást — egyetlen futtatható program köti össze. A szerver egyszálú, level-triggered epoll ciklus; a kapcsolatrekeszek statikus tömbben élnek, így címük a futás teljes ideje alatt stabil.

A parancssori felület: diag_server [PORT]. A szerver kizárólag a 127.0.0.1 címen figyel; a 0 vagy a hiányzó port esetén a kernel választ portot. Sikeres indulás után a standard kimenetre pontosan egy sort ír, például LISTEN 127.0.0.1:40123, így egy tesztkliens a tényleges portot is megtudja. Kilépési kód: rendes leállítás után 0, hibás argumentumnál 2, indítási vagy eseményciklus-hibánál 5.

/* src/diag_server.c -- referencia-szerver: listener, epoll, határidők,
 * signalfd-alapú graceful shutdown. ISO C17 + Linux API. */
#include "diag_tcp.h"

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <sys/signalfd.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>

/* A 17.1 határidői; tesztbuildben -D kapcsolóval rövidíthetők. */
#ifndef DIAG_IDLE_MS
#define DIAG_IDLE_MS 30000
#endif
#ifndef DIAG_FRAME_MS
#define DIAG_FRAME_MS 5000
#endif
#ifndef DIAG_REPLY_MS
#define DIAG_REPLY_MS 5000
#endif
#ifndef DIAG_SHUTDOWN_MS
#define DIAG_SHUTDOWN_MS 3000
#endif

#define CONNECTION_LIMIT 64
#define ACCEPT_BURST 16
#define RETRY_MS 100
#define TOKEN_LISTENER UINT64_MAX
#define TOKEN_SIGNAL (UINT64_MAX - 1)

enum phase { PH_IDLE, PH_FRAME, PH_REPLY, PH_DRAIN_PEER };

struct slot {
    struct diag_io io;
    uint32_t generation;
    bool active;
    bool retired;
    enum phase phase;
    int64_t deadline;             /* monoton óra, ms */
};

static struct slot slots[CONNECTION_LIMIT];
static size_t active_count;
static int ep = -1;
static int listener = -1;
static bool listening;            /* a listener az epollban van-e */
static int64_t listener_retry = -1;
static bool stopping;
static int64_t stop_deadline;

static int64_t now_ms(void)
{
    struct timespec ts;
    (void)clock_gettime(CLOCK_MONOTONIC, &ts);
    return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}

static uint64_t token_of(size_t i)
{
    return (uint64_t)slots[i].generation << 32 | (uint64_t)i;
}

static int set_interest(size_t i, int op)
{
    struct slot *s = &slots[i];
    struct epoll_event ev = { .data.u64 = token_of(i) };

    if (s->phase != PH_DRAIN_PEER && s->io.off < s->io.end)
        ev.events = EPOLLOUT;
    else
        ev.events = EPOLLIN | EPOLLRDHUP;
    return epoll_ctl(ep, op, s->io.fd, &ev);
}

static void resume_listener(void)
{
    struct epoll_event ev = { .events = EPOLLIN,
                              .data.u64 = TOKEN_LISTENER };

    if (stopping || listening || listener < 0)
        return;
    if (epoll_ctl(ep, EPOLL_CTL_ADD, listener, &ev) == 0) {
        listening = true;
        listener_retry = -1;
    }
}

static void pause_listener(int64_t retry_at)
{
    if (listening) {
        (void)epoll_ctl(ep, EPOLL_CTL_DEL, listener, NULL);
        listening = false;
    }
    listener_retry = retry_at;    /* -1: szabad rekeszre várunk */
}

static void teardown(size_t i)
{
    struct slot *s = &slots[i];
    int fd = s->io.fd;

    s->active = false;            /* 1. logikailag inaktív */
    (void)epoll_ctl(ep, EPOLL_CTL_DEL, fd, NULL);
    s->io.fd = -1;
    if (close(fd) == -1)          /* egyszer; hiba esetén sem újra */
        perror("close");
    if (s->generation == UINT32_MAX)
        s->retired = true;        /* körbefordulás helyett nyugdíj */
    --active_count;
    resume_listener();
}

static void refresh(size_t i, bool new_reply)
{
    struct slot *s = &slots[i];
    enum phase p;

    if (s->io.off < s->io.end)
        p = PH_REPLY;
    else if (s->io.used > 0)
        p = PH_FRAME;
    else
        p = PH_IDLE;

    /* Fázisváltáskor indul új határidő; bájtonként nem hosszabbodik. */
    if (p != s->phase || (p == PH_REPLY && new_reply)) {
        int64_t span = p == PH_REPLY ? DIAG_REPLY_MS
                     : p == PH_FRAME ? DIAG_FRAME_MS : DIAG_IDLE_MS;
        s->phase = p;
        s->deadline = now_ms() + span;
    }
}

static void begin_drain_peer(size_t i)
{
    struct slot *s = &slots[i];

    if (shutdown(s->io.fd, SHUT_WR) == -1) {
        teardown(i);
        return;
    }
    s->phase = PH_DRAIN_PEER;
    s->deadline = stop_deadline;
    if (set_interest(i, EPOLL_CTL_MOD) == -1)
        teardown(i);
}

static void handle_step(size_t i, enum diag_step step, bool new_reply)
{
    switch (step) {
    case DIAG_KEEP:
        refresh(i, new_reply);
        if (set_interest(i, EPOLL_CTL_MOD) == -1)
            teardown(i);
        return;
    case DIAG_DONE:
        if (slots[i].io.draining)
            begin_drain_peer(i);  /* leállítás: a kimenet kiürült */
        else
            teardown(i);          /* rendezett EOF, nincs függő válasz */
        return;
    case DIAG_CORE_ERROR:
        (void)fprintf(stderr, "diag_server: core contract violation\n");
        teardown(i);
        return;
    case DIAG_BAD_FRAME:
    case DIAG_SYS_ERROR:
        teardown(i);
        return;
    }
}

static void drain_peer(size_t i)
{
    unsigned char sink[1024];
    ssize_t n = recv(slots[i].io.fd, sink, sizeof sink, 0);

    if (n > 0)
        return;                   /* eldobjuk; a szint-trigger újra jelez */
    if (n == -1 && (errno == EINTR || errno == EAGAIN ||
                    errno == EWOULDBLOCK))
        return;
    teardown(i);                  /* EOF vagy hiba */
}

static void accept_burst(void)
{
    for (int k = 0; k < ACCEPT_BURST; ++k) {
        size_t i = 0;
        while (i < CONNECTION_LIMIT && (slots[i].active || slots[i].retired))
            ++i;
        if (i == CONNECTION_LIMIT) {
            pause_listener(-1);
            return;
        }

        int fd = accept4(listener, NULL, NULL,
                         SOCK_NONBLOCK | SOCK_CLOEXEC);
        if (fd == -1) {
            if (errno == EINTR || errno == ECONNABORTED)
                continue;
            if (errno == EMFILE || errno == ENFILE ||
                errno == ENOBUFS || errno == ENOMEM)
                pause_listener(now_ms() + RETRY_MS);
            else if (errno != EAGAIN && errno != EWOULDBLOCK)
                perror("accept4");
            return;
        }

        struct slot *s = &slots[i];
        s->io = (struct diag_io){ .fd = fd };
        ++s->generation;
        s->phase = PH_IDLE;
        s->deadline = now_ms() + DIAG_IDLE_MS;
        if (set_interest(i, EPOLL_CTL_ADD) == -1) {
            s->io.fd = -1;
            (void)close(fd);      /* még az accept ág a tulajdonos */
            continue;
        }
        s->active = true;         /* tulajdonátadás a rekesznek */
        ++active_count;
    }
}

static void start_stop(void)
{
    if (stopping)
        return;
    stopping = true;
    stop_deadline = now_ms() + DIAG_SHUTDOWN_MS;

    pause_listener(-1);
    if (close(listener) == -1)
        perror("close");
    listener = -1;

    for (size_t i = 0; i < CONNECTION_LIMIT; ++i) {
        if (!slots[i].active || slots[i].phase == PH_DRAIN_PEER)
            continue;
        if (diag_quiesce(&slots[i].io) == DIAG_DONE)
            begin_drain_peer(i);
        else
            handle_step(i, DIAG_KEEP, false);
    }
}

static void dispatch(const struct epoll_event *ev, int sfd)
{
    uint64_t token = ev->data.u64;

    if (token == TOKEN_LISTENER) {
        accept_burst();
        return;
    }
    if (token == TOKEN_SIGNAL) {
        struct signalfd_siginfo info;
        while (read(sfd, &info, sizeof info) == (ssize_t)sizeof info)
            start_stop();
        return;
    }

    uint32_t i = (uint32_t)token;
    uint32_t generation = (uint32_t)(token >> 32);
    if (i >= CONNECTION_LIMIT || !slots[i].active ||
        slots[i].generation != generation)
        return;                   /* elavult esemény */

    struct slot *s = &slots[i];
    if (s->phase == PH_DRAIN_PEER) {
        drain_peer(i);
    } else if (ev->events & EPOLLERR) {
        teardown(i);
    } else if (s->io.off < s->io.end) {
        size_t used = s->io.used;
        enum diag_step step = diag_write(&s->io, diag_core_reply);
        handle_step(i, step, s->io.used < used);
    } else {
        handle_step(i, diag_read(&s->io, diag_core_reply), true);
    }
}

static void expire(int64_t now)
{
    if (listener_retry >= 0 && now >= listener_retry)
        resume_listener();

    for (size_t i = 0; i < CONNECTION_LIMIT; ++i) {
        if (slots[i].active &&
            (now >= slots[i].deadline || (stopping && now >= stop_deadline)))
            teardown(i);
    }
}

static int next_timeout(void)
{
    int64_t next = -1;

    if (listener_retry >= 0)
        next = listener_retry;
    if (stopping && (next < 0 || stop_deadline < next))
        next = stop_deadline;
    for (size_t i = 0; i < CONNECTION_LIMIT; ++i) {
        if (slots[i].active && (next < 0 || slots[i].deadline < next))
            next = slots[i].deadline;
    }
    if (next < 0)
        return -1;

    int64_t wait = next - now_ms();
    return wait <= 0 ? 0 : wait > 60000 ? 60000 : (int)wait;
}

static int setup(unsigned long port, int *sfd)
{
    sigset_t mask;
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons((uint16_t)port),
        .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
    };
    socklen_t length = sizeof addr;
    int one = 1;

    if (sigemptyset(&mask) == -1 || sigaddset(&mask, SIGTERM) == -1 ||
        sigaddset(&mask, SIGINT) == -1 ||
        sigprocmask(SIG_BLOCK, &mask, NULL) == -1)
        return -1;
    *sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
    ep = epoll_create1(EPOLL_CLOEXEC);
    listener = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
    if (*sfd == -1 || ep == -1 || listener == -1)
        return -1;

    struct epoll_event ev = { .events = EPOLLIN, .data.u64 = TOKEN_SIGNAL };
    if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one) ||
        bind(listener, (struct sockaddr *)&addr, sizeof addr) == -1 ||
        listen(listener, 128) == -1 ||
        getsockname(listener, (struct sockaddr *)&addr, &length) == -1 ||
        epoll_ctl(ep, EPOLL_CTL_ADD, *sfd, &ev) == -1)
        return -1;

    resume_listener();
    if (!listening)
        return -1;
    if (printf("LISTEN 127.0.0.1:%u\n", (unsigned)ntohs(addr.sin_port)) < 0 ||
        fflush(stdout) == EOF)
        return -1;
    return 0;
}

int main(int argc, char **argv)
{
    unsigned long port = 0;
    int sfd = -1;
    int status = 0;

    if (argc > 2) {
        (void)fprintf(stderr, "ERR code=usage\n");
        return 2;
    }
    if (argc == 2) {
        char *end;
        errno = 0;
        port = strtoul(argv[1], &end, 10);
        if (errno != 0 || end == argv[1] || *end != '\0' || port > 65535) {
            (void)fprintf(stderr, "ERR code=usage\n");
            return 2;
        }
    }

    if (setup(port, &sfd) == -1) {
        perror("diag_server: setup");
        status = 5;
    } else {
        struct epoll_event events[CONNECTION_LIMIT + 2];

        while (!stopping || active_count > 0) {
            int n = epoll_wait(ep, events, CONNECTION_LIMIT + 2,
                               next_timeout());
            if (n == -1) {
                if (errno == EINTR)
                    continue;
                perror("epoll_wait");
                status = 5;
                break;
            }
            for (int k = 0; k < n; ++k)
                dispatch(&events[k], sfd);
            expire(now_ms());     /* minden köteg után, nem csak timeoutkor */
        }
    }

    for (size_t i = 0; i < CONNECTION_LIMIT; ++i) {
        if (slots[i].active)
            teardown(i);
    }
    if (listener >= 0)
        (void)close(listener);
    if (sfd >= 0)
        (void)close(sfd);
    if (ep >= 0)
        (void)close(ep);
    return status;
}

Néhány döntés, amely a kódból nem feltétlenül olvasható ki:

Fordítás a megvalósítási labor magjával együtt:

gcc -std=c17 -O2 -g -Wall -Wextra -Wpedantic -Wconversion -Wshadow \
    -Werror -D_GNU_SOURCE -Iinclude \
    src/diag_server.c src/tcp_io.c src/tcp_reply.c src/diag.c \
    -o build/gcc/diag_server

A határidők fordítási makrók, ezért az elfogadási teszt rövidített határidőkkel fordított példányt használ; a szerver logikája ugyanaz:

gcc -std=c17 -O1 -g -D_GNU_SOURCE -Iinclude \
    -DDIAG_IDLE_MS=1500 -DDIAG_FRAME_MS=500 \
    -DDIAG_REPLY_MS=500 -DDIAG_SHUTDOWN_MS=1000 \
    -fsanitize=address,undefined -fno-omit-frame-pointer \
    src/diag_server.c src/tcp_io.c src/tcp_reply.c src/diag.c \
    -o build/gcc/diag_server_test
python3 tests/server_test.py build/gcc/diag_server_test

Az elfogadási teszt egy valódi loopback TCP-kliens. Pipelined és hibás kéréseket, bájtonként érkező bemenetet, a leghosszabb érvényes kérés minden kettévágását, rendezett és félbemaradt EOF-ot, a három határidőt, a 64 kapcsolatos limitet, nagy mennyiségű kérést egyidejű olvasással (backpressure), valamint graceful shutdownt ellenőriz. A leállítási tesztben egy kliens folyamatosan küld, de nem olvas; a szervernek ennek ellenére a közös határidőn belül ki kell lépnie.

#!/usr/bin/env python3
"""tests/server_test.py SERVER -- a referencia-szerver elfogadási tesztje.

A szervert rövidített határidőkkel fordítva kell átadni:
IDLE=1500, FRAME=500, REPLY=500, SHUTDOWN=1000 ms.
"""
import signal
import socket
import subprocess
import sys
import threading
import time

IDLE, FRAME, SHUTDOWN = 1.5, 0.5, 1.0
LIMIT = 64


def start(binary):
    proc = subprocess.Popen([binary], stdout=subprocess.PIPE, text=True)
    line = proc.stdout.readline().strip()
    assert line.startswith("LISTEN 127.0.0.1:"), line
    return proc, int(line.rsplit(":", 1)[1])


def connect(port):
    s = socket.create_connection(("127.0.0.1", port), timeout=3)
    return s


def recv_until_closed(s, timeout=3):
    s.settimeout(timeout)
    data = b""
    while True:
        chunk = s.recv(4096)
        if not chunk:
            return data
        data += chunk


def recv_lines(s, count, timeout=3):
    s.settimeout(timeout)
    data = b""
    while data.count(b"\n") < count:
        chunk = s.recv(4096)
        assert chunk, "a kapcsolat idő előtt bezárult"
        data += chunk
    return data


def closed_within(s, seconds):
    """Igaz, ha a szerver a megadott időn belül lezárja a kapcsolatot."""
    s.settimeout(seconds)
    try:
        while s.recv(4096):
            pass
        return True
    except (socket.timeout, TimeoutError):
        return False
    except ConnectionResetError:
        return True


def test_pipelined_and_error(port):
    s = connect(port)
    s.sendall(b"a=7\ncpu=12\nA=1\n")
    assert recv_lines(s, 3) == (b"OK records=1 sum=7 bytes=4\n"
                                b"OK records=1 sum=12 bytes=7\n"
                                b"ERR code=syntax offset=0\n")
    s.close()


def test_partial_input(port):
    s = connect(port)
    for b in b"cpu=12\n":                # bájtonként, szünetekkel
        s.sendall(bytes([b]))
        time.sleep(0.02)
    assert recv_lines(s, 1) == b"OK records=1 sum=12 bytes=7\n"
    s.close()


def test_max_request_splits(port):
    request = b"abcdefghijklmnop=65535\n"    # 23 bájt, a leghosszabb
    s = connect(port)
    s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
    for split in range(len(request) + 1):
        s.sendall(request[:split])
        time.sleep(0.005)
        s.sendall(request[split:])
        assert recv_lines(s, 1) == b"OK records=1 sum=65535 bytes=23\n"
    s.close()


def test_eof(port):
    s = connect(port)
    s.sendall(b"a=1\n")
    s.shutdown(socket.SHUT_WR)            # rendezett EOF teljes kérés után
    assert recv_until_closed(s) == b"OK records=1 sum=1 bytes=4\n"
    s.close()

    s = connect(port)
    s.sendall(b"mem=")
    s.shutdown(socket.SHUT_WR)            # félbemaradt kérés: nincs válasz
    assert recv_until_closed(s) == b""
    s.close()


def test_timeouts(port):
    s = connect(port)
    s.sendall(b"a=")                      # befejezetlen kérés
    t0 = time.monotonic()
    assert closed_within(s, FRAME + 1.0)
    assert time.monotonic() - t0 >= FRAME - 0.05
    s.close()

    s = connect(port)                     # tétlen kapcsolat
    t0 = time.monotonic()
    assert closed_within(s, IDLE + 1.0)
    assert time.monotonic() - t0 >= IDLE - 0.05
    s.close()

    s = connect(port)                     # csöpögtetés nem hosszabbít
    t0 = time.monotonic()
    s.sendall(b"a")
    closed = False
    for _ in range(40):
        time.sleep(0.05)
        try:
            s.sendall(b"a")
        except (BrokenPipeError, ConnectionResetError):
            closed = True
            break
    assert closed or closed_within(s, 0.5)
    assert time.monotonic() - t0 < FRAME + 1.0
    s.close()


def test_connection_limit(port):
    conns = [connect(port) for _ in range(LIMIT)]
    for c in conns:                       # mind a 64 kiszolgált
        c.sendall(b"a=1\n")
        assert recv_lines(c, 1) == b"OK records=1 sum=1 bytes=4\n"

    extra = connect(port)                 # a kernel elfogadja, mi nem
    extra.sendall(b"b=2\n")
    extra.settimeout(0.3)
    try:
        assert extra.recv(64) == b"", "a 65. kapcsolat nem kaphat választ"
    except (socket.timeout, TimeoutError):
        pass

    conns.pop().close()                   # egy rekesz felszabadul
    assert recv_lines(extra, 1) == b"OK records=1 sum=2 bytes=4\n"
    extra.close()
    for c in conns:
        c.close()


def test_backpressure(port):
    count = 50000
    s = connect(port)
    replies = []

    def reader():
        replies.append(recv_lines(s, count, timeout=10))

    t = threading.Thread(target=reader)
    t.start()
    s.sendall(b"".join(b"a=%d\n" % (i % 65536) for i in range(count)))
    t.join()
    expected = b"".join(b"OK records=1 sum=%d bytes=%d\n"
                        % (i % 65536, len(b"a=%d\n" % (i % 65536)))
                        for i in range(count))
    assert replies[0] == expected, "elveszett, duplikált vagy sérült válasz"
    s.close()


def test_graceful_shutdown(binary):
    proc, port = start(binary)
    done = connect(port)                  # kész válasz, majd tétlen
    done.sendall(b"a=1\n")
    assert recv_lines(done, 1) == b"OK records=1 sum=1 bytes=4\n"
    partial = connect(port)               # félbemaradt kérés
    partial.sendall(b"b=")
    flood = connect(port)                 # küld, de nem olvas
    flood.setblocking(False)
    stop = threading.Event()

    def flooder():
        while not stop.is_set():
            try:
                flood.send(b"c=1\n" * 1024)
            except BlockingIOError:
                time.sleep(0.01)
            except OSError:
                return

    t = threading.Thread(target=flooder)
    t.start()
    time.sleep(0.3)

    t0 = time.monotonic()
    proc.send_signal(signal.SIGTERM)
    assert recv_until_closed(done) == b""        # féllezárás, nincs új adat
    assert recv_until_closed(partial) == b""     # el nem fogadott kérés
    rc = proc.wait(timeout=SHUTDOWN + 2)
    elapsed = time.monotonic() - t0
    stop.set()
    t.join()
    assert rc == 0, rc
    assert elapsed < SHUTDOWN + 1.0, elapsed     # a flood sem tartja fel
    for c in (done, partial, flood):
        c.close()
    try:
        connect(port).close()
        raise AssertionError("leállás után új kapcsolat")
    except ConnectionRefusedError:
        pass


def main():
    binary = sys.argv[1]
    proc, port = start(binary)
    try:
        for test in (test_pipelined_and_error, test_partial_input,
                     test_max_request_splits, test_eof,
                     test_timeouts, test_connection_limit, test_backpressure):
            test(port)
            print("ok", test.__name__)
    finally:
        proc.send_signal(signal.SIGTERM)
        rc = proc.wait(timeout=5)
    assert rc == 0, rc
    test_graceful_shutdown(binary)
    print("ok test_graceful_shutdown")


if __name__ == "__main__":
    main()

Buildjegyzőkönyv (lab-gcc profil). A diag_server.c GCC 13.3.0-val, glibc 2.39 mellett, Linux x86-64 (6.x kernel) környezetben -Werror beállítással, az alapértelmezett és a rövidített határidőkkel is figyelmeztetés nélkül fordult. A tests/server_test.py mind a nyolc tesztesete sikeres volt Python 3-mal, egyszer ASan/UBSan builddel (a szerver kilépési kódja ekkor is 0 volt, tehát a sanitizer sem jelzett), háromszor optimalizált builddel. A teszt nem garantálja a rövid send() és a spontán EAGAIN előfordulását; ezekhez a lent leírt syscall-helyettesítő réteg kell.

17.3.7 Tesztstratégia és gyakori hibák

Az in-process parserteszt determinisztikusan vizsgálhatja ugyanazon kérés minden kettévágását, a maximális frame-et, hibás prefixeket és a fogyasztási hosszakat. Fontos tulajdonság: időtúllépés nélkül, megfelelő kimenetürítés mellett a bemenet darabolása nem változtathatja meg a válaszok sorrendjét és tartalmát.

A socket integration teszt valódi loopback TCP kapcsolaton vizsgálja az acceptet, a half-close-t, az eseményérdeklődést, a lassú olvasót, a kapcsolatlimitet és a shutdown-határidőt; a projektben ez a tests/server_test.py. A port kiosztását a kernelre lehet bízni, majd getsockname()-mel lekérdezni; a szerver ezért írja ki a LISTEN sort.

A rövid send() és az EAGAIN nem garantálható pusztán kis üzenetekkel. Ezekhez determinisztikus syscall-helyettesítő tesztréteg is szükséges; a valódi TCP-teszt ezt kiegészíti, nem helyettesíti. A stale event szintén közvetlen tokeninjektálással ellenőrizhető.

Tipikus hibák:

17.3.8 Feladatok

17.3.8.1 Élettartam és memóriakorlát

A parser egy mezőre mutató pointert ad. Miért nem elegendő ezt a kimeneti sorban eltárolni? Mennyi a puszta input/output tárolókorlát 48 kapcsolat esetén?

17.3.8.2 Eseményérdeklődés megvalósítása

Írj függvényt, amely aktív kapcsolatnál csak függő kimenet esetén kér EPOLLOUT eseményt; egyébként olvasást és peer-féllezárást figyel.

17.3.8.3 Leállítási hiba diagnosztikája

A szerver shutdownkor bezárja a listenert, de a kapcsolatoknál továbbra is olvas és új válaszokat állít elő. Miért nem garantált a leállás? Milyen teszt bizonyítja a javítást?

17.3.9 Megoldások

17.3.9.1 Élettartam és memóriakorlát

A pointer nem tulajdonolja a mező bájtjait. A memmove() vagy egy új olvasás megváltoztathatja a hivatkozott tartalmat. A választ még a szinkron parserhívás alatt saját kimeneti tárolóba kell másolni.

48(4096+1024)=245760C bájt=240KiB. 48(4096+1024)=245760\ \text{C bájt}=240\ \text{KiB}.

Ellenőrzési feltétel: a már sorba állított válasz akkor sem változik, ha a bemeneti tömböt tesztben felülírjuk.

17.3.9.2 Eseményérdeklődés megvalósítása

Az alábbi részlet a szerver fordítási egységébe illeszthető:

#include "diag_tcp.h"
#include <stdint.h>
#include <sys/epoll.h>

static uint32_t active_interest(const struct diag_io *c)
{
    return c->off < c->end
         ? EPOLLOUT
         : EPOLLIN | EPOLLRDHUP;
}

Előfeltétele az aktív állapot; a shutdown állapotait külön kezeljük. Linuxon az EPOLLERR és EPOLLHUP külön kérés nélkül is jelentkezhet.

Ellenőrzési feltétel: a válasz utolsó bájtjának elküldése és a tárolt bemenet feldolgozása után az érdeklődés megfelel az új sorállapotnak.

17.3.9.3 Leállítási hiba diagnosztikája

A listener lezárása csak az új kapcsolatokat akadályozza meg. A meglévő kliensek végtelen munkát termelhetnek.

A javítás az új kérések elfogadásának leállítása, a már elfogadott válaszok ürítése, majd a féllezárás és a közös határidő. Tesztben egy kliens shutdown után is folyamatosan küld, és nem olvassa a válaszokat. A szervernek nem szabad új válaszokat előállítania; a határidő után minden kapcsolat lezárandó. A mérési ellenőrzéshez kis, dokumentált ütemezési tolerancia szükséges.

17.3.10 Összefoglaló

A helyes nonblocking adapter három határt tart fenn: tárolóhatárt, érvényességi határt és időhatárt. A korlátos válaszsor adja a backpressure-t, a szinkron parseradapter lezárja a borrowed nézetek használatát, a generációs token védi az újrahasznált kapcsolatrekeszeket. A graceful shutdown pedig nem puszta close(), hanem új munka tiltásából, válaszürítésből, féllezárásból és végső határidőből álló állapotátmenet. A „Teljes referencia-szerver” szakasz ezeket egyetlen futtatható programmá köti össze, és loopback TCP-teszttel ellenőrzi.