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.
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.
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:
dirfd
értékét;.. kijuthat a könyvtárból;O_NOFOLLOW csak a végső útvonalkomponens
szimbolikuslink-követését tiltja.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.
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ó |
Legyen a kívánt kezdőoffset , a hossz , a lapméret . A leképezés kezdete és hossza:
Az mmap offsetje
,
hossza
;
a kívánt nézet a visszakapott címhez képest
bájtnál kezdődik. A hossz nem szükségképpen laptöbbszörös.
Bizonyításvázlat. Az osztható -vel, továbbá . A kívánt rész a mappingen belül a tartomány, tehát annak végén nem lép túl. C-ben az összeadás előtt szükséges az ellenőrzés, valamint a típuskonverziók érvényességének igazolása. Ha a fájlméret , akkor és ellenőrzendő; ez nem igényli a potenciálisan túlcsorduló kiszámítását.
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-clangA 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
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.
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.
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.
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:
openat segítségével,
O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC zászlókkal.fsync; hiba esetén nincs
publikálás.renameat az ideiglenes névről a célra.fsync a tartalmazó könyvtáron.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.
mmap-et.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., , . Ellenőrzés: , és . A mapping hossza nem laptöbbszörös, ami megengedett.
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.
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.
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.