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 | DirList → DirEntryCount / DirEntryName(dl,i) / DirEntryTypeAt(dl,i) / DirEntryType(dl,name,len) → DirFree |
| Rekursiv | DirWalk → DirWalkCount / DirWalkPath(w,i,outBuf) / DirWalkType → DirWalkFree |
| Muster | FileGlob → GlobCount / 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) | bool — Schutz 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 mitFileAllocateMode(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
userDataist 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.allocbietetalloc(n)/free(ptr, size)undmalloc(n)/calloc(n,sz)/realloc_mem/free_mem(ptr). Beide arbeiten seit 1.0.17C ohne C-Bibliothek über eigenemmap-Belegung (bis dahin gabmallocim statischen Binary 0 zurück — #1179). Nicht mischen: einalloc-Zeiger gehört anfree, einmalloc-Zeiger anfree_mem. Rückgabe immer gegen 0 prüfen. - Fehlerkonvention ist uneinheitlich.
std.fsgibt oftboolzurück,std.fs_extund die Builtins geben0bei Erfolg bzw. den negativen errno bei Fehler,ReadFile/WriteFilegeben Bytezahlen. Immer gegen< 0prüfen, nie gegen!= 0. - Deskriptoren lecken. Es gibt kein
deferund keine Destruktoren — jeder Pfad, der die Funktion verlässt, musscloseselbst aufrufen. Bei Fehlerbehandlung mit frühemreturnist das die häufigste Lücke. O_*-Flags sind Zahlen, keine Namen.std.fsexportiertO_RDONLY(0),O_WRONLY(1),O_RDWR(2),O_CREAT(64),O_TRUNC(512),O_APPEND(1024),O_EXCL(128),O_DIRECTORY(65536). Wer nurstd.fs_extimportiert, definiert sie selbst — mit genau diesen Werten, sonst öffnetopenscheinbar zufällig etwas anderes.modeist dezimal, nicht oktal.0644schreibt man in Lyx als420,0755als493. Dafür gibt esDEFAULT_MODEundDEFAULT_DIR_MODE.- Pfade aus Fremdeingabe niemals direkt verwenden. Erst
PathNormalize, dannPathContainedIn(root, path)prüfen. DirWalklädt das Ergebnis komplett in den Speicher. Bei sehr großen Bäumen besserDirListpro Ebene und selbst absteigen.
Weiterführend: Netzwerk-Guide (epoll, Sockets) · Daten & Serialisierung · RTOS & Nebenläufigkeit · Audio & Video
