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
  • Namensraum beachten: Nutzer-Attribute müssen mit user. beginnen, sonst lehnt der Kernel ab.
  • XATTR_CREATE = nur anlegen, XATTR_REPLACE = nur ersetzen, 0 = beides.
  • Über offene Deskriptoren: FXattrSet / FXattrGet / FXattrList / FXattrRemove.
  • Belegung: StatFs(path, buf) + StatFsTotalBytes / StatFsFreeBytes / StatFsBlockSize.
  • Platz vorab reservieren (keine Fragmentierung, kein ENOSPC mitten im Schreiben): FileAllocate(fd, offset, size); Löcher stanzen mit FileAllocateMode(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, off, size).

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
  • userData ist frei wählbar und der einzige Weg, eine Completion ihrer Anfrage zuzuordnen — typischerweise ein Index oder eine Objektadresse.
  • Operationen: IoUringSQESetRead / IoUringSQESetWrite / IoUringSQESetNop; die Opcodes für Netzwerk (IORING_OP_ACCEPT, IORING_OP_SEND, IORING_OP_RECV, IORING_OP_CONNECT) sind als Konstanten vorhanden und können direkt ins SQE gepokt werden.
  • Ohne Warten: IoUringSubmit + IoUringPeekCQE (gibt 0 zurück, wenn nichts fertig ist) — das ist die Ereignisschleifen-Variante.
  • Puffer müssen bis zur Completion gültig bleiben. Ein Stack-Array einer bereits verlassenen Funktion ist der klassische Absturz.

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

  • Zwei Allokator-Familien mit unterschiedlichem free. std.alloc bietet alloc(n) / free(ptr, size) und malloc(n) / calloc(n,sz) / realloc_mem / free_mem(ptr). Beide arbeiten seit 1.0.17C ohne C-Bibliothek über eigene mmap-Belegung (bis dahin gab malloc im statischen Binary 0 zurück — #1179). Nicht mischen: ein alloc-Zeiger gehört an free, ein malloc-Zeiger an free_mem. Rückgabe immer gegen 0 prüfen.
  • Fehlerkonvention ist uneinheitlich. std.fs gibt oft bool zurück, std.fs_ext und die Builtins geben 0 bei Erfolg bzw. den negativen errno bei Fehler, ReadFile/WriteFile geben Bytezahlen. Immer gegen < 0 prüfen, nie gegen != 0.
  • Deskriptoren lecken. Es gibt kein defer und keine Destruktoren — jeder Pfad, der die Funktion verlässt, muss close selbst aufrufen. Bei Fehlerbehandlung mit frühem return ist das die häufigste Lücke.
  • O_*-Flags sind Zahlen, keine Namen. std.fs exportiert O_RDONLY (0), O_WRONLY (1), O_RDWR (2), O_CREAT (64), O_TRUNC (512), O_APPEND (1024), O_EXCL (128), O_DIRECTORY (65536). Wer nur std.fs_ext importiert, definiert sie selbst — mit genau diesen Werten, sonst öffnet open scheinbar zufällig etwas anderes.
  • mode ist dezimal, nicht oktal. 0644 schreibt man in Lyx als 420, 0755 als 493. Dafür gibt es DEFAULT_MODE und DEFAULT_DIR_MODE.
  • Pfade aus Fremdeingabe niemals direkt verwenden. Erst PathNormalize, dann PathContainedIn(root, path) prüfen.
  • DirWalk lädt das Ergebnis komplett in den Speicher. Bei sehr großen Bäumen besser DirList pro Ebene und selbst absteigen.

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