11.4 Idő, véletlen és környezeti állapot

Egy szolgáltatás naplóbejegyzéséhez naptári időpont kell, egy hálózati kérés timeoutjához eltelt idő, teljesítményelemzéshez pedig akár CPU-idő. Ezek nem ugyanannak a mennyiségnek különböző formátumai. Hasonlóképpen más szerződés szükséges egy reprodukálható teszt véletlenszámaihoz és egy hozzáférési tokenhez.

Az időzóna, a locale, a véletlengenerátor és a környezeti változók további közös tulajdonsága a rejtett, megosztott állapot. A megfelelő függvény kiválasztása ezért egyszerre jelent időmodell-, biztonsági és élettartam-döntést. A hibák kezelésénél a Hibakód, errno és hibaadatok élettartama, a numerikus átalakításoknál a Lebegőpontos reprezentáció és numerikus hibák szabályaira építünk.

11.4.1 Naptári idő, falióra és CPU-idő

A falióra (wall clock) a naptári időhöz kapcsolódó óra. Alkalmas események időbélyegzésére, de az értéke adminisztratív beavatkozás vagy időszinkronizáció miatt változhat. A monoton óra nem lép visszafelé; elsősorban időtartamokhoz és határidőkhöz használjuk. A CPU-idő azt közelíti, mennyi processzoridőt használt fel a program, nem azt, meddig volt életben.

API és szabványhatár Jelentés Hibajelzés és korlát
time, ISO C17 Aktuális naptári idő (time_t)-1, ha nem érhető el
timespec_get, ISO C17 A megadott időalap szerinti idő; szabványos alap: TIME_UTC Siker: a megadott alap értéke; hiba: 0
clock, ISO C17 A program processzoridejének közelítése (clock_t)-1, ha nem áll rendelkezésre vagy nem ábrázolható
clock_gettime, POSIX Kiválasztott rendszeróra Siker: 0; hiba: -1, részletek az errno-ban

11.4.1.1 time_t, struct tm és időpontok különbsége

ISO C17: a time_t naptári idő reprezentálására szolgáló aritmetikai típus. A hordozható C-program nem feltételezheti, hogy egy adott szélességű egész, vagy hogy Unix-epoch óta eltelt másodperceket tartalmaz.

Két érvényes naptári időpont különbségéhez ezért:

/* ISO C17-részlet; szükséges fejléc: <time.h>.
   begin és end érvényes, sikeresen lekért naptári időpontok. */
double elapsed_seconds = difftime(end, begin);

A difftime az első argumentum mínusz a második különbségét adja másodpercben. A közvetlen end - begin nem helyettesíti hordozhatóan: a reprezentációra támaszkodna, és a kivonás a time_t típusában történne.

A struct tm felbontott naptári időt tárol. Például a tm_year az 1900 óta eltelt éveket, a tm_mon a januártól számított, nullával kezdődő hónapszámot tartalmazza. Nem célszerű saját mezőaritmetikával kezelni a hónapváltást vagy a nyári időszámítást.

ISO C17: a gmtime UTC szerinti, a localtime helyi idő szerinti felbontást készít. Mindkettő null pointerrel jelezheti, hogy az átalakítás nem végezhető el. A visszaadott struct tm nem a hívó tulajdona.

11.4.1.2 Nagyobb felbontás nem jelent monotonitást

ISO C17: a

/* ISO C17-részlet; szükséges fejléc: <time.h>. */
struct timespec stamp;
int ok = timespec_get(&stamp, TIME_UTC);

hívás után csak ok == TIME_UTC esetén használható az eredmény. A tv_sec az implementáció által meghatározott epochhoz viszonyított másodpercrészt, a tv_nsec a másodpercen belüli nanoszekundumos mezőt tartalmazza.

A mező mértékegysége nem azonos az óra tényleges felbontásával vagy pontosságával. A TIME_UTC nem biztosít monoton órát: timeout mérésére ugyanúgy veszélyes lehet, mint a time.

A clock más problémát old meg. Két sikeres mérésből, ábrázolható és körbefordulástól mentes intervallumon:

TCPU=c1−c0CLOCKS_PER_SEC. T_{\mathrm{CPU}}=\frac{c_1-c_0}{\mathrm{CLOCKS\_PER\_SEC}}.

A kivonást és konverziót úgy kell megvalósítani, hogy ne történjen előbb egész túlcsordulás. A CLOCKS_PER_SEC skálatényező, nem garantált mérési felbontás. Alvás vagy I/O-várakozás alatt a falióra halad, miközben a program CPU-ideje alig nőhet.

11.4.2 Timeout monoton órával

POSIX: a clock_gettime(CLOCK_MONOTONIC, ...) időtartamokhoz használható. Az óra kezdőpontjának nincs naptári jelentése. Két értéket csak azonos órából és összehasonlítható rendszerkörnyezetből szabad összevetni.

Linux: a CLOCK_MONOTONIC nem számolja bele a rendszer felfüggesztésében töltött időt. Ha a timeoutnak ezt is tartalmaznia kell, külön Linux-specifikus óraválasztás, például CLOCK_BOOTTIME szükséges. A monotonitás nem jelent változatlan frekvenciát vagy valós idejű ütemezési garanciát.

Timeoutot célszerű egyszer kiszámított határidővel (deadline) kezelni. Ha minden megszakítás után újraindítjuk a teljes várakozást, a teljes művelet túllépheti az eredeti keretet.

Legyen a kezdőérték (s,n)(s,n), a megengedett idő pedig (a,b)(a,b), normalizált nanoszekundumos mezőkkel. Ekkor:

u=n+b,ds=s+a+⌊u109⌋,dn=umod109. u=n+b,\qquad d_s=s+a+\left\lfloor\frac{u}{10^9}\right\rfloor,\qquad d_n=u\bmod 10^9.

Az összeadásokat csak akkor szabad elvégezni, ha az eredmények ábrázolhatók a használt típusokban. Szemléltető példa:

(120,900000000)+(0,250000000)=(121,150000000). (120,\ 900\,000\,000)+(0,\ 250\,000\,000) =(121,\ 150\,000\,000).

A következő teljes fordítási egység egy már létrehozott határidőt ellenőriz:

/* deadline.c — ISO C17 szintaxis, POSIX API */
#define _POSIX_C_SOURCE 200809L
#include <time.h>

/*
 * Előfeltétel:
 * deadline nem NULL, élő, olvasható objektumra mutat;
 * CLOCK_MONOTONIC alapján készült;
 * 0 <= tv_nsec < 1000000000.
 *
 * Eredmény: 1 = lejárt, 0 = még nem járt le,
 *          -1 = óraleolvasási hiba, errno szerint.
 */
int deadline_expired(const struct timespec *deadline)
{
    struct timespec now;

    if (clock_gettime(CLOCK_MONOTONIC, &now) == -1) {
        return -1;
    }

    return now.tv_sec > deadline->tv_sec ||
           (now.tv_sec == deadline->tv_sec &&
            now.tv_nsec >= deadline->tv_nsec);
}

Bizonyításvázlat: normalizált mezők esetén egy nagyobb másodpercérték minden lehetséges nanoszekundumos mező mellett későbbi időpontot jelent. Azonos másodpercnél a nanoszekundumos mezők döntenek. Az egyenlőség már lejárt határidőt jelent.

A függvény nem várakoztat. Egy blokkoló műveletnek a még hátralévő időt kell átadni, megszakítás után pedig ugyanahhoz a határidőhöz kell visszatérni. Az ellenőrzés önmagában nem tudja megakadályozni, hogy az ütemező később engedje tovább a folyamatot.

Javasolt, itt nem fordítással ellenőrzött laborprofil: Linux x86-64, glibc 2.39, GCC 14.2 és Clang 19.1, explicit C17 módban:

gcc -std=c17 -Wall -Wextra -Wpedantic -c deadline.c -o deadline-gcc.o
clang -std=c17 -Wall -Wextra -Wpedantic -c deadline.c -o deadline-clang.o

Ez objektumfájlt készítő modul, ezért nincs main függvénye. Más, különösen régebbi platformon a linkelési követelményeket külön ellenőrizni kell.

11.4.3 Időzóna, locale és kölcsönzött tároló

A naptári időpont és annak megjelenítése külön réteg. Ugyanazt az időpontot a localtime az időzóna szerint bontja fel, a strftime pedig a választott formátum és részben a locale szerint írja ki.

ISO C17: a setlocale megosztott locale-állapotot módosít; a strftime többek között a LC_TIME kategóriától függ. POSIX: a TZ környezeti változó és a tzset az időzóna-kezelés része. Ezeket nem szabad kérésenként, párhuzamos kiszolgálás közben biztonságosan elszigetelt beállításnak tekinteni.

Hozzáférés Tulajdonos és élettartam Érvénytelenítés, hibakimenet
gmtime/localtime eredménye Könyvtári statikus tároló; kölcsönzött pointer Kapcsolódó időkonverziós hívások felülírhatják; hiba: null pointer
getenv eredménye Implementáció által kezelt tároló; kölcsönzött sztring Újabb getenv felülírhatja; környezetmódosításra sem tartható meg biztonságosan; hiány: null pointer
deadline_expired argumentuma A hívó által fenntartott objektum Élettartamvége után nem használható; párhuzamos, szinkronizálatlan módosítása hibás
Hívói struct tm másolat A hívó saját objektuma A könyvtár későbbi hívása nem írja felül

Ez például hibás mentális modell:

/* ISO C17, szemléltető részlet; <time.h>.
   Nem megfelelő: a két pointer nem jelent két független eredményt. */
struct tm *local = localtime(&now);
struct tm *utc = gmtime(&now);

A második hívás felülírhatja az első eredményt. Egyszálú, közbeeső hívástól mentes kódban a null pointer ellenőrzése utáni azonnali struktúramásolat megoldás. Párhuzamos hívásoknál azonban a másolás önmagában nem szinkronizáció.

POSIX: a localtime_r és gmtime_r hívói tárolóba ír. Ez megszünteti a visszatérési objektum megosztását, de nem teszi problémamentessé az időzóna vagy környezet egyidejű átállítását.

11.4.4 Véletlen: tesztadat vagy biztonsági titok?

ISO C17: a rand a [0,RAND_MAX][0,\mathrm{RAND\_MAX}] tartományból ad pszeudovéletlen értéket. A srand inicializálja a generátor állapotát. Ugyanaz a seed ugyanazon implementációban ugyanazt a sorozatot eredményezi; különböző C könyvtárak között nem garantált azonos sorozat.

A rand nem kriptográfiailag biztonságos generátor, azaz nem CSPRNG. A

/* ISO C17-részlet; <stdlib.h>, <time.h>.
   Biztonsági token előállítására alkalmatlan. */
srand((unsigned)time(NULL));

nem változtat ezen: a kezdőérték gyakran becsülhető, és azonos kezdőértékek ismétlődhetnek. A megosztott generátorállapot miatt egy másik komponens hívásai a tesztek reprodukálhatóságát is befolyásolhatják.

A rand() % k még ideálisan egyenletes bemenetből is torzíthat. Ha matematikailag

R=RAND_MAX+1=qk+r,0≤r<k, R=\mathrm{RAND\_MAX}+1=qk+r,\qquad 0\le r<k,

akkor rr maradékhoz q+1q+1, a többihez qq bemeneti érték tartozik. Ez matematikai képlet: a RAND_MAX + 1 C-kifejezés int típusban túlcsordulhat. A modulo-torzítás javítása sem teszi kriptográfiailag biztonságossá a generátort.

Linux-specifikus: titkokhoz a kernel véletlenforrását használó getrandom megfelelő kiindulópont, nem ISO C17 API. A visszaadott bájtszámot ellenőrizni kell; rövid eredmény, megszakítás és inicializálási várakozás is része lehet a szerződésnek. Hibakor nem elfogadható „tartalékmegoldásként” rand-ra váltani.

11.4.5 Környezeti változók és konfigurációs pillanatkép

ISO C17: a getenv null pointert ad, ha a változó nem található. A megtalált üres sztring ettől különböző állapot. A visszaadott sztringet nem szabad módosítani vagy free-vel felszabadítani.

POSIX: a setenv és unsetenv környezetet módosít. A párhuzamos olvasás és módosítás biztonságát nem szabad automatikusan feltételezni. A másolatkészítés is olvasás: ha közben a forrás változhat vagy érvénytelenné válhat, a másolás sem biztonságos.

Szolgáltatásnál jó szerződés az induláskori konfigurációs pillanatkép: még a munkaszálak előtt kiolvasni, validálni és saját tárolóba másolni az értékeket. Ezután a program ne a folyamat környezetét használja élő konfigurációs adatbázisként.

11.4.6 Feladatok

  1. Óraválasztás. Válassz API-t napló-időbélyeghez, kétmásodperces timeout méréséhez és egy számítás CPU-költségéhez. Mi történhet, ha a faliórát közben visszaállítják?

  2. Kódolás. Készíts copy_service_mode függvényt, amely a SERVICE_MODE értékét hívói bufferbe másolja. Eredmény: 0 siker, 1 hiányzó változó, -1 elégtelen kapacitás. Hibánál maradjon változatlan a buffer.

  3. Diagnosztika. Egy szolgáltatás minden kérés előtt újrahívja a srand(time(NULL)) kifejezést, rand-ból készít hozzáférési tokent, globálisan módosítja a TZ értékét, és minden megszakítás után új teljes timeouttal vár. Nevezd meg a négy független hibát.

11.4.7 Megoldások

11.4.7.1 Óraválasztás

Naplóhoz ISO C17 time vagy timespec_get(TIME_UTC), timeout méréséhez POSIX CLOCK_MONOTONIC, CPU-költséghez ISO C17 clock megfelelő.

A falióra visszaállítása negatív időeltérést vagy megnyúlt timeoutot okozhat. Ellenőrzési feltétel: a timeout minden újrapróbálkozása ugyanahhoz a monoton határidőhöz viszonyítson.

11.4.7.2 Konfiguráció másolása

/* env_copy.c — ISO C17 */
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

/*
 * dst legalább cap C bájt írható tárolóra mutat, ha cap > 0.
 * A forrás és cél nem fed át.
 * A környezet és a getenv-hozzáférés a művelet alatt stabil.
 */
int copy_service_mode(char *dst, size_t cap)
{
    const char *value = getenv("SERVICE_MODE");
    if (value == NULL) {
        return 1;
    }

    size_t length = strlen(value);
    if (length >= cap) {
        return -1;
    }

    memcpy(dst, value, length + 1);
    return 0;
}

A korábbi laborprofilban:

gcc -std=c17 -Wall -Wextra -Wpedantic -c env_copy.c

A cél tulajdonosa a hívó; a függvény nem őrzi meg egyik pointert sem. A kapacitásellenőrzés a másolás előtt történik, ezért a hibautak nem módosítják a célt. Ellenőrizendő: hiányzó és üres érték, pontosan elegendő kapacitás, valamint a lezáró nullkarakter számára már elégtelen buffer.

11.4.7.3 Diagnosztikai javítás

Az ismételt seedelés ismétlődő sorozatokat okozhat; a rand nem alkalmas tokenhez; a globális időzóna-módosítás összekeverheti a párhuzamos kérések állapotát; a teljes timeout újraindítása túllépi a teljes időkeretet.

A javítás: megfelelő OS-véletlenforrás ellenőrzött hibakezeléssel, rögzített vagy explicit időzóna-kezelés, valamint egyetlen monoton határidő. A tesztgenerátor seedjét külön, reprodukálható tesztkonfigurációként kell kezelni.

11.4.8 Összefoglaló

Naptári időpont, eltelt idő és CPU-idő külön fogalom. Timeout méréséhez monoton óra kell; a nanoszekundumos mező önmagában nem biztosít sem monotonitást, sem nanoszekundumos pontosságot. A rand nem biztonsági eszköz. Az időkonverzió, locale és környezet pedig megosztott állapotot és kölcsönzött tárolót hozhat az API-ba: az ownership, az élettartam és a párhuzamos módosítás szerződését ezeknél is explicitté kell tenni.