Inhaltsverzeichnis

Dateisystem & I/O mit Lyx

Lyx trennt Datei-Arbeit in vier Ebenen: Builtins (rohe Syscalls, ohne Import), std.fs (Komfort: Pfade, Verzeichnisse, Kopieren), std.fs_ext (Metadaten, positioniertes Lesen, Nullkopie) und die Spezialisten std.mmap_ext, std.inotify, std.xattr, std.io_uring.

Es gibt keinen Datei-Handle-Typ und keine Streams. Ein geöffnetes Objekt ist ein int64-Deskriptor, ein Puffer ist eine Adresse — wem die Freigabe gehört, klärt Handles & FD, den Umgang mit rohen Puffern Rohspeicher. Wer das akzeptiert, kommt mit sehr wenig API aus.

Guides · Standard Library · Netzwerk-Guide

import std.fs;         // Pfade, Verzeichnisse, ReadFile/WriteFile, Glob
import std.fs_ext;     // stat, PRead/PWrite, Fsync, Sendfile, GetCwd
import std.alloc;      // alloc / free für Puffer


Die vier Ebenen

Ebene Import nötig? Wofür
Builtins: open close read write lseek stat unlink rename mkdir rmdir pipe truncate ioctl getdents64 symlink readlink mmap munmap nein Rohzugriff, wenn genau ein Syscall gebraucht wird
std.fs ja Datei in einem Aufruf lesen/schreiben, Pfade zerlegen, Verzeichnisse auflisten und rekursiv durchlaufen, Glob
std.fs_ext ja stat-Auswertung, pread/pwrite, readv/writev, fsync, sendfile, Arbeitsverzeichnis
std.mmap_ext / std.inotify / std.xattr / std.io_uring ja Speicherabbildung, Dateiüberwachung, erweiterte Attribute + Dateisystem-Belegung, asynchrone I/O

<WRAP center round important 60%> std.fs und std.fs_ext exportieren beide FileExists und FileSize. Wer beide importiert, bekommt:

sema error: mehrdeutiges Symbol 'FileExists' — exportiert von 'std.fs' und 'std.fs_ext'

Lyx hat keine qualifizierten Zugriffe (std.fs.FileExists gibt es nicht), also: eine der beiden Units wählen. Faustregel — Pfad-orientiert arbeiten → std.fs; über offene Deskriptoren arbeiten → std.fs_ext. </WRAP>


Datei lesen und schreiben

Der kürzeste Weg braucht keinen Deskriptor und kein open:

unit main;
import std.fs;
import std.io;
import std.string;

fn main(): int64 {
    var path: pchar := "/tmp/lyx_fs_demo.txt"c;
    var text: pchar := "Hallo Lyx\n"c;

    if (WriteFile(path, text, StrLen(text)) < 0) {
        PrintLn("Schreiben fehlgeschlagen");
        return 1;
    }

    var buf: [256]char;
    var n: int64 := ReadFile(path, buf as pchar, 255);
    PrintLn(StrConcat("Bytes gelesen: ", IntToStr(n)));
    PrintLn(StrConcat("Groesse: ", IntToStr(FileSize(path))));

    if (FileExists(path)) { PrintLn("existiert"); }
    DeleteFile(path);
    return 0;
}

Bytes gelesen: 10
Groesse: 10
existiert
Funktion Bedeutung
WriteFile(path, buf, len) Anlegen/Überschreiben, gibt geschriebene Bytes zurück (<0 = Fehler)
AppendFile(path, buf, len) Anhängen
ReadFile(path, buf, max) Liest bis max Bytes, gibt gelesene Bytes zurück
FileSize(path) / FileSizeFast(path) Größe (letzteres über stat, ohne Öffnen)
FileCopy / FileMove / Rename / DeleteFile / Remove Datei-Operationen, bool
Chmod(path, mode) Rechte setzen (DEFAULT_MODE = 0644)

Puffer nicht vergessen zu terminieren. ReadFile schreibt keine abschließende 0 — wer den Puffer als pchar ausgeben will, setzt sie selbst: poke8(buf + n, 0).


Verzeichnisse, rekursiv und per Muster

unit main;
import std.fs;
import std.io;
import std.string;

fn main(): int64 {
    var dl: int64 := DirList("demo"c);
    if (dl == 0) { PrintLn("DirList fehlgeschlagen"); return 1; }

    var n: int64 := DirEntryCount(dl);
    var i: int64 := 0;
    while (i < n) {
        var name: pchar := DirEntryName(dl, i);
        var t:    int64 := DirEntryTypeAt(dl, i);
        var kind: pchar := "Datei";
        if (t == DT_DIR) { kind := "Verz."; }
        PrintLn(StrConcat(StrConcat(kind, "  "), name));
        i := i + 1;
    }
    DirFree(dl);

    var g: int64 := FileGlob("demo/*.txt"c);
    PrintLn(StrConcat("Glob-Treffer: ", IntToStr(GlobCount(g))));
    GlobFree(g);

    var w: int64 := DirWalk("demo"c);          // rekursiv
    PrintLn(StrConcat("Rekursiv gefunden: ", IntToStr(DirWalkCount(w))));
    DirWalkFree(w);
    return 0;
}

Datei  a.txt
Verz.  sub
Datei  b.log
Glob-Treffer: 1
Rekursiv gefunden: 4

. und .. werden von DirList bereits herausgefiltert. Die Reihenfolge ist die des Dateisystems, nicht sortiert.

Gruppe Funktionen
Auflisten DirListDirEntryCount / DirEntryName(dl,i) / DirEntryTypeAt(dl,i) / DirEntryType(dl,name,len)DirFree
Rekursiv DirWalkDirWalkCount / DirWalkPath(w,i,outBuf) / DirWalkTypeDirWalkFree
Muster FileGlobGlobCount / GlobGet(g,i,outBuf)GlobFree; GlobMatch(pattern, name) für Einzelprüfung
Anlegen Mkdir(path, mode), MkDirAll(path) (wie mkdir -p), Rmdir
Temporär MkTemp(pathBuf), TmpFile()

Jede der drei Listen-APIs braucht ihr eigenes Free. DirFree / DirWalkFree / GlobFree sind nicht austauschbar.

Eintragstypen: DT_REG (Datei), DT_DIR, DT_LNK, DT_FIFO, DT_SOCK, DT_CHR, DT_BLK, DT_UNKNOWN.

Pfade zerlegen ohne String-Gefummel

Alle Pfad-Funktionen schreiben in einen vom Aufrufer gestellten Zielpuffer und geben ihn zurück:

Funktion Ergebnis für /var/log/app.tar.gz
PathDir(dest, path) /var/log
PathBase(dest, path) app.tar.gz
PathExt(dest, path) gz
PathJoin(dest, a, b) fügt mit genau einem / zusammen
PathNormalize(dest, src) löst . und .. auf
PathResolve(dest, base, rel) relativ gegen Basis auflösen
IsAbsolutePath(path) bool
PathContainedIn(root, path) boolSchutz gegen Path-Traversal, vor jedem Zugriff auf vom Nutzer gelieferte Pfade verwenden

Metadaten, positioniertes Lesen, Nullkopie (std.fs_ext)

unit main;
import std.fs_ext;
import std.alloc;
import std.io;
import std.string;

fn main(): int64 {
    var fd: int64 := open("demo/a.txt"c, 0, 0);      // O_RDONLY
    if (fd < 0) { PrintLn("open fehlgeschlagen"); return 1; }

    var st: int64 := alloc(STAT_SIZE);
    if (Fstat(fd, st) < 0) { PrintLn("fstat fehlgeschlagen"); return 1; }
    PrintLn(StrConcat("Groesse: ", IntToStr(StatFileSize(st))));
    PrintLn(StrConcat("mtime:   ", IntToStr(StatMtime(st))));
    if (StatIsFile(st)) { PrintLn("regulaere Datei"); }
    free(st, 0);

    var buf: int64 := alloc(64);
    var n: int64 := PRead(fd, buf, 16, 0);           // ohne lseek
    PrintLn(StrConcat("PRead Bytes: ", IntToStr(n)));
    free(buf, 0);
    close(fd);

    var cwd: int64 := alloc(PATH_MAX);
    GetCwd(cwd, PATH_MAX);
    PrintLn(StrConcat("CWD: ", cwd as pchar));
    free(cwd, 0);
    return 0;
}

Groesse: 17
mtime:   1786470295
regulaere Datei
PRead Bytes: 16
CWD: /tmp/…/scratchpad
Aufgabe Funktion
Metadaten Fstat(fd, statBuf) mit statBuf = alloc(STAT_SIZE); auswerten mit StatFileSize / StatMtime / StatMode / StatIsDir / StatIsFile
Positioniert lesen/schreiben PRead(fd, buf, n, off) / PWrite(…) — thread-sicher, verändert die Dateiposition nicht
Streuend lesen/schreiben IovecSet(iov, idx, base, len) + ReadV / WriteV (IOVEC_SIZE = 16 Bytes pro Eintrag)
Auf Platte zwingen Fsync(fd) (Daten + Metadaten), Fdatasync(fd) (nur Daten, schneller)
Kürzen/Verlängern Truncate(path, len) / Ftruncate(fd, len)
Rechte prüfen FileAccess(path, mode) mit R_OK/W_OK/X_OK/F_OK, bequemer: FileReadable / FileWritable
Arbeitsverzeichnis GetCwd(buf, size), Chdir(path)
Nullkopie Sendfile(outFd, inFd, offsetPtr, count) — kopiert im Kernel, ohne Userspace-Puffer

Sendfile ist der schnellste Weg Datei→Datei und Datei→Socket:

var src: int64 := open("demo/a.txt"c, 0, 0);
var dst: int64 := open("demo/kopie.txt"c, 65 | 512, 420);   // O_WRONLY|O_CREAT|O_TRUNC, 0644
var moved: int64 := Sendfile(dst, src, 0, 4096);            // offsetPtr = 0 → ab aktueller Position
Fsync(dst);
close(src); close(dst);


Große Dateien speicherabbilden (std.mmap_ext)

mmap und munmap sind Builtins; std.mmap_ext liefert die Feinsteuerung.

unit main;
import std.fs_ext;
import std.mmap_ext;
import std.io;
import std.string;

con PROT_READ:   int64 := 1;
con MAP_PRIVATE: int64 := 2;

fn main(): int64 {
    var fd: int64 := open("demo/a.txt"c, 0, 0);
    if (fd < 0) { return 1; }

    var size: int64 := 4096;
    var addr: int64 := mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (addr <= 0) { PrintLn("mmap fehlgeschlagen"); close(fd); return 1; }

    MmapAdvise(addr, size, MADV_SEQUENTIAL);                  // Kernel-Hinweis
    PrintLn(StrConcat("erstes Byte: ", IntToStr(peek8(addr))));
    if (MemIsResident(addr)) { PrintLn("Seite ist im RAM"); }

    munmap(addr, size);
    close(fd);
    return 0;
}

erstes Byte: 105
Seite ist im RAM
Funktion Zweck
MmapAdvise(addr, len, advice) MADV_SEQUENTIAL (Readahead hoch), MADV_RANDOM, MADV_WILLNEED (vorladen), MADV_DONTNEED, MADV_HUGEPAGE, MADV_COLD, MADV_PAGEOUT
MmapSync(addr, len, flags) Änderungen zurückschreiben — MS_SYNC (blockierend) oder MS_ASYNC
MmapResize / MmapResizeFixed mremap — Abbildung vergrößern, ohne neu zu kopieren
MemFdCreate(name, flags) anonyme Datei im RAM (MFD_CLOEXEC, MFD_ALLOW_SEALING) — ideal für IPC ohne Dateisystem
MemLock / MemUnlock Seiten gegen Swap sperren (Schlüsselmaterial!)
MemIsResident(addr) / MemInCore(addr, len, vecOut) prüfen, ob Seiten im RAM liegen
MmapFreePages(addr, len) Speicher an den Kernel zurückgeben, Abbildung bleibt

Größe immer auf PAGE_SIZE (4096) aufrunden — der Kernel arbeitet nur seitenweise, und munmap mit falscher Länge lässt Reste liegen.


Änderungen überwachen (std.inotify)

unit main;
import std.inotify;
import std.alloc;
import std.fs;
import std.io;
import std.string;

fn main(): int64 {
    var ifd: int64 := InotifyCreate();
    if (ifd < 0) { PrintLn("inotify nicht verfuegbar"); return 1; }

    var wd: int64 := InotifyWatch(ifd, "demo"c, IN_CREATE | IN_DELETE | IN_MODIFY);
    if (wd < 0) { PrintLn("Watch fehlgeschlagen"); return 1; }

    WriteFile("demo/neu.txt"c, "x"c, 1);          // löst Ereignisse aus

    var buf: int64 := alloc(4096);
    var n:   int64 := InotifyRead(ifd, buf, 4096);   // blockiert bis Ereignis
    var p:    int64 := buf;
    var ende: int64 := buf + n;
    while (p < ende) {
        var mask: int64 := InotifyEventMask(p);
        var name: int64 := InotifyEventName(p);
        var was:  pchar := "sonstiges";
        if ((mask & IN_CREATE) != 0) { was := "erstellt"; }
        if ((mask & IN_MODIFY) != 0) { was := "geaendert"; }
        if ((mask & IN_DELETE) != 0) { was := "geloescht"; }
        PrintLn(StrConcat(StrConcat(name as pchar, ": "), was));
        p := InotifyEventNext(p);                    // nächster Eintrag im Puffer
    }
    free(buf, 0);

    InotifyUnwatch(ifd, wd);
    close(ifd);
    return 0;
}

neu.txt: erstellt
neu.txt: geaendert

Ein InotifyRead liefert mehrere Ereignisse variabler Länge am Stück. Deshalb immer die Schleife mit InotifyEventNext verwenden — feste Schrittweiten sind falsch (INOTIFY_EVENT_HDR_SIZE = 16 plus Namenslänge).

Maske Bedeutung
IN_CREATE / IN_DELETE / IN_MODIFY Datei erstellt / gelöscht / geändert
IN_CLOSE_WRITE Schreib-Datei geschlossen — das richtige Signal für „Datei fertig geschrieben„, nicht IN_MODIFY
IN_MOVED_FROM / IN_MOVED_TO Verschiebung; über InotifyEventCookie paarbar
IN_DELETE_SELF / IN_MOVE_SELF überwachtes Objekt selbst betroffen
IN_ISDIR zusätzliches Bit: Ereignis betrifft ein Verzeichnis
IN_Q_OVERFLOW Queue übergelaufen — Ereignisse wurden verloren, Zustand neu einlesen
IN_ALL_EVENTS alles

InotifyCreate überwacht nicht rekursiv. Für Bäume: DirWalk laufen lassen und jedes Verzeichnis einzeln registrieren, bei IN_CREATE | IN_ISDIR nachregistrieren.


Erweiterte Attribute & Dateisystem-Belegung (std.xattr)

unit main;
import std.xattr;
import std.fs_ext;
import std.alloc;
import std.io;
import std.string;

fn main(): int64 {
    var val: pchar := "andreas"c;
    if (XattrSet("demo/a.txt"c, "user.autor"c, val as int64, StrLen(val), 0) < 0) {
        PrintLn("xattr nicht unterstuetzt (Dateisystem?)");
    } else {
        var buf: int64 := alloc(256);
        var n:   int64 := XattrGet("demo/a.txt"c, "user.autor"c, buf, 256);
        poke8(buf + n, 0);
        PrintLn(StrConcat("Autor: ", buf as pchar));
        free(buf, 0);
        XattrRemove("demo/a.txt"c, "user.autor"c);
    }

    var sfb: int64 := alloc(STATFS_SIZE);
    if (StatFs("/tmp"c, sfb) == 0) {
        PrintLn(StrConcat("frei (MB): ", IntToStr(StatFsFreeBytes(sfb) / 1048576)));
    }
    free(sfb, 0);
    return 0;
}

Autor: andreas
frei (MB): 39079

Asynchrone I/O (std.io_uring)

Für viele parallele Lese-/Schreibvorgänge ohne Threads. Ablauf: Ring anlegen → SQE holen → Operation eintragen → abschicken → CQE einsammeln → als gesehen markieren.

unit main;
import std.io_uring;
import std.alloc;
import std.io;
import std.string;

fn main(): int64 {
    var ring: int64 := IoUringCreate(8);              // Ringgröße (Zweierpotenz)
    if (ring == 0) { PrintLn("io_uring nicht verfuegbar"); return 1; }

    var fd:  int64 := open("demo/a.txt"c, 0, 0);
    var buf: int64 := alloc(256);

    var sqe: int64 := IoUringGetSQE(ring);
    IoUringSQESetRead(sqe, fd, buf, 64, 0, 42);       // len=64, offset=0, userData=42
    IoUringSubmitAndWait(ring, 1);

    var cqe: int64 := IoUringWaitCQE(ring);
    var res: int64 := IoUringCQEResult(cqe);          // >=0 Bytes, <0 negativer errno
    var ud:  int64 := IoUringCQEUserData(cqe);
    IoUringCQESeen(ring, cqe);                        // Pflicht — sonst läuft der Ring voll

    PrintLn(StrConcat("gelesen: ", IntToStr(res)));
    PrintLn(StrConcat("userData: ", IntToStr(ud)));

    free(buf, 0);
    close(fd);
    IoUringFree(ring);
    return 0;
}

gelesen: 17
userData: 42

Entscheidungshilfe

Aufgabe Empfehlung
Kleine Datei ganz lesen/schreiben ReadFile / WriteFile (std.fs)
Konfigurationsdatei zeilenweise ReadFile + std.string-Funktionen
Datei > RAM durchsuchen mmap + MADV_SEQUENTIAL
Datei kopieren FileCopy (bequem) oder Sendfile (schnell, kein Userspace-Puffer)
Nebenläufig aus einer Datei lesen PRead — kein gemeinsamer Positionszeiger
Datei→Socket ausliefern Sendfile
Verzeichnis überwachen std.inotify, Signal IN_CLOSE_WRITE
Hunderte parallele I/Os std.io_uring
Datenbank-artige Haltbarkeit schreiben → Fdatasync(fd)Rename (atomarer Austausch)
Metadaten an Dateien hängen std.xattr, Namensraum user.
Archive/Kompression eigene Units: std.tar, std.zip, std.gzip, std.zstd, std.brotli, std.zlib, std.rar

Fallstricke


Weiterführend: Netzwerk-Guide (epoll, Sockets) · Daten & Serialisierung · RTOS & Nebenläufigkeit · Audio & Video