12.2 Fájlrendszer-műveletek, mmap és tartósság

Egy konfigurációs fájl lecserélése három külön kérdést vet fel: melyik fájlobjektumot módosítjuk, mit látnak közben az olvasók, és mi marad meg rendszerösszeomlás után? A sikeres write, a memóriában látható módosítás és a tartós állapot nem ugyanaz.

A file descriptor és robusztus I/O ciklus tulajdonlási szabályaira építünk: minden megszerzett erőforráshoz lezárási felelősség tartozik. Az igazításról tanultakat most a memórialapokhoz igazított fájlleképezésre alkalmazzuk. Az alábbi rendszerhívások nem az ISO C részei; a POSIX-felületet és a Linux-specifikus feltételeket külön jelöljük.

12.2.1 Fájlazonosság, méret és directory-relative műveletek

POSIX: a stat(path, &st) útvonal alapján kér metaadatokat, és követi a végső szimbolikus linket. A fstat(fd, &st) a már megnyitott fájlhoz tartozó adatokat adja vissza. Szabályos fájlnál a struct stat st_size mezője a logikai méret, típusa off_t.

Az off_t előjeles egész típus; nem szabad automatikusan long vagy size_t helyett használni, illetve ezekre ellenőrzés nélkül konvertálni. A fájl lehet nagyobb a folyamatban egyetlen tartományként kezelhető memóriánál.

Linux/glibc: 32 bites célon a fordítás elejétől érvényes _FILE_OFFSET_BITS=64 makró választja ki a nagyfájl-interfészeket. Ez nem ISO C- vagy általános POSIX-mechanizmus. Az x86-64 LP64 laborban az off_t eleve 64 bites; más libc és ABI esetén a szerződést külön ellenőrizni kell. A fordítási egységek és könyvtári határok beállításai legyenek következetesek.

12.2.1.1 TOCTOU és az útvonal rögzített kiindulópontja

A time-of-check to time-of-use (TOCTOU) hiba akkor keletkezik, amikor az ellenőrzés és a használat között megváltozhat az ellenőrzött állapot. Például a stat(path) után végzett open(path) már más fájlt nyithat meg.

Az openat(dirfd, name, flags, ...) relatív name esetén a dirfd által azonosított könyvtárból indul, nem a folyamat aktuális munkakönyvtárából. Ez stabil kiindulópont, de önmagában nem teljes útvonalbiztonsági megoldás:

A későbbi példában ezért pontosan egy, nem üres komponens engedélyezett; a . és .. tiltott. Először megnyitjuk a fájlt, majd ugyanazon descriptoron végzünk fstat ellenőrzést. Ez a fájlazonosság problémáját kezeli, de más folyamat későbbi írását vagy csonkítását nem akadályozza meg.

12.2.2 Fájlleképezés: határok és élettartam

POSIX: az mmap fájlrészhez rendel virtuális címtartományt. Sikertelenségkor MAP_FAILED értéket ad, nem null pointert. A leképezési hossz pozitív, az offset pedig a rendszer által megkövetelt lapméret többszöröse legyen. A lapméret a sysconf(_SC_PAGESIZE) hívással kérdezhető le; annak hibáját is kezelni kell.

A MAP_SHARED leképezés módosításai a fájl állapotába kerülhetnek. A MAP_PRIVATE módosításai nem írják át a fájlt, és msync sem teszi őket fájlmódosítássá. A MAP_PRIVATE ugyanakkor nem garantál változatlan pillanatképet egy más által közben módosított fájlról.

Erőforrás Tulajdonos Érvényesség és megszüntetés
Könyvtár-FD megnyitó kódrész close lezárja a directory-relative kiindulópontot
Fájl-FD megnyitó kódrész close lezárja ezt a descriptort
Mapping címtartománya mmap hívója munmap megszünteti; a fájl-FD lezárása nem
Mappingre mutató pointer kölcsönzött hozzáférés csak a leképezés és a hozzáférhető fájltartomány fennállásáig használható

12.2.2.1 Tetszőleges fájlrész leképezése

Legyen a kívánt kezdőoffset s≥0s\geq 0, a hossz L>0L>0, a lapméret P>0P>0. A leképezés kezdete és hossza:

a=s−(smodP),d=s−a,n=d+L. a=s-(s\bmod P),\qquad d=s-a,\qquad n=d+L.

Az mmap offsetje aa, hossza nn; a kívánt nézet a visszakapott címhez képest dd bájtnál kezdődik. A hossz nem szükségképpen laptöbbszörös.

Bizonyításvázlat. Az aa osztható PP-vel, továbbá 0≤d<P0\leq d<P. A kívánt rész a mappingen belül a [d,d+L)=[d,n)[d,d+L)=[d,n) tartomány, tehát annak végén nem lép túl. C-ben az összeadás előtt szükséges az L≤𝚂𝙸𝚉𝙴_𝙼𝙰𝚇−dL\leq\texttt{SIZE\_MAX}-d ellenőrzés, valamint a típuskonverziók érvényességének igazolása. Ha a fájlméret SS, akkor s≤Ss\leq S és L≤S−sL\leq S-s ellenőrzendő; ez nem igényli a potenciálisan túlcsorduló s+Ls+L kiszámítását.

12.2.2.2 Kidolgozott példa: nullák számlálása

A program egy megbízható könyvtárútvonalból indul, és egyetlen fájlnévkomponenst fogad. Előfeltétel: a megnyitott fájlt a vizsgálat alatt senki nem módosítja vagy csonkítja. Ezt a program nem kényszeríti ki.

A példa ISO C17 nyelvet és POSIX API-kat használ. Választott laborprofil: Linux 6.8, x86-64 System V ABI, LP64, 8 bites C bájt, glibc 2.39; fordítóként GCC 14.2.0 vagy Clang 19.1.7. Ezek reprodukciós célverziók; a példához itt nem tartozik dokumentált fordítási vagy futtatási eredmény.

#define _POSIX_C_SOURCE 200809L

#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    if (argc != 3 || argv[2][0] == '\0' ||
        strchr(argv[2], '/') != NULL ||
        strcmp(argv[2], ".") == 0 ||
        strcmp(argv[2], "..") == 0) {
        fprintf(stderr, "Hasznalat: mapzeros KONYVTAR FAJLNEV\n");
        return EXIT_FAILURE;
    }

    int dirfd = -1, fd = -1;
    int result = EXIT_FAILURE;
    void *mapping = MAP_FAILED;
    size_t length = 0, zeros = 0;
    struct stat st;

    dirfd = open(argv[1], O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    if (dirfd == -1) {
        perror("open directory");
        goto cleanup;
    }

    fd = openat(dirfd, argv[2], O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd == -1) {
        perror("openat");
        goto cleanup;
    }

    if (fstat(fd, &st) == -1) {
        perror("fstat");
        goto cleanup;
    }

    if (!S_ISREG(st.st_mode) || st.st_size < 0) {
        fprintf(stderr, "Nem megfelelo fajltipus vagy meret\n");
        goto cleanup;
    }

    if ((uintmax_t)st.st_size > (uintmax_t)SIZE_MAX ||
        (uintmax_t)st.st_size > (uintmax_t)PTRDIFF_MAX) {
        fprintf(stderr, "A fajlhoz ablakos feldolgozas szukseges\n");
        goto cleanup;
    }

    length = (size_t)st.st_size;
    if (length != 0) {
        mapping = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0);
        if (mapping == MAP_FAILED) {
            perror("mmap");
            goto cleanup;
        }

        const unsigned char *bytes = mapping;
        for (size_t i = 0; i < length; ++i) {
            if (bytes[i] == 0)
                ++zeros;
        }
    }

    if (printf("meret=%zu, nullak=%zu\n", length, zeros) < 0)
        goto cleanup;

    result = EXIT_SUCCESS;

cleanup:
    if (mapping != MAP_FAILED && munmap(mapping, length) == -1) {
        perror("munmap");
        result = EXIT_FAILURE;
    }
    if (fd != -1 && close(fd) == -1) {
        perror("close file");
        result = EXIT_FAILURE;
    }
    if (dirfd != -1 && close(dirfd) == -1) {
        perror("close directory");
        result = EXIT_FAILURE;
    }
    return result;
}

Fordítás külön laborlépésként:

gcc-14 -std=c17 -D_FILE_OFFSET_BITS=64 -O2 \
  -Wall -Wextra -Wpedantic -Wconversion mapzeros.c -o mapzeros-gcc

clang-19 -std=c17 -D_FILE_OFFSET_BITS=64 -O2 \
  -Wall -Wextra -Wpedantic -Wconversion mapzeros.c -o mapzeros-clang

A verziózott parancsnevek telepítésfüggők; a tényleges fordítókiadást rögzíteni kell. A PTRDIFF_MAX korlát konzervatív programbeli döntés, nem általános mmap-mérethatár.

Az üres fájlt külön ág kezeli, mert nulla hosszúságú mapping nem készíthető. A ciklus csak a [0,𝚕𝚎𝚗𝚐𝚝𝚑)[0,\texttt{length}) tartományt olvassa. A borrowed bytes pointert a munmap érvényteleníti. A Linux-profilnak megfelelően a program a sikertelen close hívást jelenti, de nem ismétli meg vakon.

12.2.3 Sparse fájl, csonkítás és SIGBUS

A sparse fájl logikai méretéhez nem feltétlenül tartozik ugyanannyi fizikailag lefoglalt tároló. A lyukak olvasáskor nullákat szolgáltatnak. A példaprogram nullaszámlálása ezért nem bizonyítja a sparse tárolást: fizikailag tárolt nullák ugyanazt az eredményt adják.

POSIX/Linux: az ftruncate módosítja a fájl logikai méretét. Növeléskor az új tartomány nullaként olvasható, de ebből nem következik előzetes fizikai tárhelyfoglalás. A már létező mapping hossza nem növekszik automatikusan.

Csonkításkor a mapping címtartománya megmaradhat, miközben a mögöttes fájlrész eltűnik. Linuxon a fájlvégen teljesen túlra került laphoz való hozzáférés SIGBUS jelzést okozhat. Az utolsó részleges lap nullakitöltése sem jogosít fel a program szerződésében rögzített fájlméreten túli olvasásra.

Ez különösen fontos hibakimenet: a sikeres mmap nem garantálja, hogy minden későbbi memóriahozzáférés sikerül. Egy újabb fstat sem oldja meg a versenyhelyzetet, mert utána ismét történhet csonkítás. Megoldás lehet az együttműködő kizárás, vagy a publikált fájlok módosításának tiltása és új fájl rename-alapú publikálása.

12.2.4 Visszaírás, atomikus csere és tartósság

A kernel pufferében jelen levő adat még nem feltétlenül tartós. A fontos műveletek eltérő hatókörűek:

Művelet Szerződésének lényegi része
msync(..., MS_SYNC) a megosztott mapping módosításainak szinkron visszaírása
fsync(fd) fájladatok és hozzájuk tartozó metaadatok szinkronizálása
fdatasync(fd) adatok és későbbi elérésükhöz szükséges metaadatok szinkronizálása
Könyvtár-FD fsync Linuxon, támogató fájlrendszeren a névtérmódosítás tartóssági lépése

A fájlméret a tartalom eléréséhez szükséges metaadat, tehát a fdatasync nem hagyhatja figyelmen kívül pusztán azért, mert metaadat. Az msync kezdőcímének lapra igazítottnak kell lennie; eltolt borrowed view helyett a megfelelő mappingtartományt kell megadni. Egyik fájlszinkronizáció sem helyettesíti automatikusan a könyvtárbejegyzés tartósítását.

12.2.4.1 Tartós konfigurációcsere

POSIX: azonos fájlrendszeren a sikeres renameat atomikusan cserélheti le a célnevet. A név szerinti olvasók nem félkész fájlt kapnak; a korábban megnyitott descriptorok továbbra is a régi fájlobjektumra hivatkoznak. Az atomikus névcsere azonban nem összeomlás utáni tartóssági garancia.

Linux-laborminta, azonos, megbízható könyvtárban, konkurens publikáló nélkül:

  1. Ideiglenes fájl létrehozása openat segítségével, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC zászlókkal.
  2. Teljes tartalom kiírása robusztus ciklussal; a szükséges jogosultságok beállítása.
  3. Az ideiglenes fájlon fsync; hiba esetén nincs publikálás.
  4. renameat az ideiglenes névről a célra.
  5. fsync a tartalmazó könyvtáron.
  6. A descriptorok lezárása, a hibák jelentése.

A sorrend indoka: először az új fájl állapotát, utána az arra mutató névcsere állapotát tesszük tartóssá. Eltérő könyvtárak közötti mozgatásnál mindkét könyvtár érintett lehet.

Ha a rename sikerül, de a könyvtárszinkronizáció hibázik, az új állapot már látható: az eredmény publikálva, tartóssága bizonytalan, nem egyszerűen „nem történt semmi”. A garancia a fájlrendszer és a tárolási réteg helyesen teljesített szinkronizációs szerződésétől is függ.

12.2.5 Feladatok

  1. Magyarázó: P=4096P=4096, s=5000s=5000, L=3000L=3000 mellett számítsd ki a mapping offsetjét, hosszát és a borrowed view eltolását.
  2. Kódolási: alakítsd át a példaprogramot úgy, hogy legfeljebb az első 4096 bájtot vizsgálja. Üres fájlra se hívjon mmap-et.
  3. Diagnosztikai: egy szolgáltatás felülírja az aktív fájlt, majd rename-et végez egy másik fájllal. Egy olvasó SIGBUS miatt leáll, áramszünet után pedig az új név sem megbízható. Azonosíts két független szerződéshibát.

12.2.6 Megoldások

12.2.6.1 Igazított tartomány

a=4096a=4096, d=904d=904, n=3904n=3904. Ellenőrzés: a+d=5000a+d=5000, és d+L=nd+L=n. A mapping hossza nem laptöbbszörös, ami megengedett.

12.2.6.2 Korlátozott vizsgálat

A nemnegatív fájlméret ellenőrzése után, még a konverzió előtt:

/* C17/POSIX részlet a fenti main függvényhez.
   A teljes fájl méretkorlát-ellenőrzését és konverzióját helyettesíti. */
length = (uintmax_t)st.st_size > (uintmax_t)4096
       ? (size_t)4096
       : (size_t)st.st_size;

A meglévő nemnulla-hossz ág és cleanup megtartható. Ellenőrzési esetek: 0, 1, 4096 és 4097 bájtos fájl. A tartalom stabilitásának előfeltétele változatlan.

12.2.6.3 Két külön hibamodell

Az aktív fájl csonkítása érvénytelenítheti az olvasó által elérhető backing tartományt. Új fájlt kell írni, a publikált fájlt pedig változatlanul hagyni.

A rename önmagában nem tartósítás: előtte fájl-, utána könyvtárszinkronizáció szükséges. Ellenőrzéskor minden lépésnél vizsgálni kell a hibakimenetet. Egy folyamat kilövése önmagában nem modellezi az áramvesztést, mert a kernel még visszaírhatja a puffereket.

12.2.7 Rövid összefoglaló

A fájlazonosságot descriptorral, a mapping használhatóságát határ- és élettartam-szerződéssel, a tartósságot explicit szinkronizációval kezeljük. A mapping nem pillanatkép, a rename nem tartóssági művelet, a sikeres írás pedig nem bizonyítja az összeomlás utáni megmaradást.