====== 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 [[lyx_-_programmiersprache:sprache:handles-und-fd|Handles & FD]], den Umgang mit rohen Puffern [[lyx_-_programmiersprache:sprache:rohspeicher|Rohspeicher]]. Wer das akzeptiert, kommt mit sehr wenig API aus. → [[lyx_-_programmiersprache:guides|Guides]] · [[lyx_-_programmiersprache:units|Standard Library]] · [[lyx_-_programmiersprache:guides:netzwerk|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 | ''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''. ---- ===== 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 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:** [[lyx_-_programmiersprache:guides:netzwerk|Netzwerk-Guide]] (epoll, Sockets) · [[lyx_-_programmiersprache:guides:daten-serialisierung|Daten & Serialisierung]] · [[lyx_-_programmiersprache:guides:rtos-embedded-concurrency|RTOS & Nebenläufigkeit]] · [[lyx_-_programmiersprache:guides:audio-und-video|Audio & Video]]