Assignment Chef icon Assignment Chef
All German tutorials

Programming lesson

Eigenes Dateisystem mit FUSE: Schritt-für-Schritt-Anleitung für CS3650 Projekt 2

Lerne, wie du mit FUSE ein einfaches Dateisystem auf einem 1MB-Disk-Image implementierst. Diese Anleitung führt dich durch die Grundlagen der Dateisystementwicklung – von der Installation bis zu Big Files.

CS3650 Projekt 2 Dateisystem mit FUSE FUSE Dateisystem Tutorial eigenes Dateisystem implementieren 1MB Disk Image Dateisystem Inode Struktur Blockverwaltung Bitmap große Dateien Dateisystem FUSE Callbacks getattr readdir Dateisystem Programmierung Studium Betriebssysteme Projekt FUSE libfuse-dev Installation Dateisystem Debugging GDB CS3650 Abgabe Tipps Dateisystem Design OSTEP Speicherverwaltung Dateisystem

Einführung in die Dateisystementwicklung mit FUSE

In diesem Tutorial zeige ich dir, wie du ein benutzerdefiniertes Dateisystem mit FUSE (Filesystem in Userspace) für das CS3650 Projekt 2 implementierst. Die Aufgabe ähnelt dem Bau einer digitalen Ablage: Du erhältst eine 1 MB große „Festplatte“ (ein Disk-Image) und musst Funktionen wie Dateien erstellen, lesen, schreiben, umbenennen und löschen ermöglichen. Klingt nach viel Arbeit? Keine Sorge – mit der richtigen Struktur wird daraus ein spannendes Projekt, das dir tiefe Einblicke in Betriebssysteme und Dateisystemdesign gibt.

Stell dir vor, du entwickelst ein eigenes Dateisystem für einen USB-Stick, der nur 1 MB fasst – ähnlich wie bei älteren MP3-Playern oder eingebetteten Geräten. Genau das machst du hier, aber in der virtuellen Maschine. Und das Beste: Du lernst dabei Konzepte, die in modernen Cloud-Speichern und KI-Anwendungen eine Rolle spielen, denn auch dort müssen Daten effizient organisiert werden.

Voraussetzungen und Installation

Bevor es losgeht, stelle sicher, dass du folgende Pakete installiert hast:

  • libfuse-dev
  • libbsd-dev
  • pkg-config

Führe im Terminal deiner VM aus:

sudo apt-get install libfuse-dev libbsd-dev pkg-config

Überprüfe die Installation mit:

pkg-config --modversion fuse > fuse_version

Diese Datei fuse_version musst du in dein Repository committen – ein erster kleiner Schritt, der zeigt, dass du die Umgebung eingerichtet hast.

Grundstruktur des Dateisystems

Dein Dateisystem arbeitet auf einem 1 MB großen Disk-Image (z. B. data.nufs). Die Datei wird in Blöcke unterteilt – typischerweise 4 KB pro Block. Du benötigst eine Block-Verwaltung (ähnlich einer Bitmap), die freie und belegte Blöcke markiert. Der Starter-Code enthält bereits Hilfsfunktionen in blocks.{c,h} und bitmap.{c,h} – nutze sie, um Zeit zu sparen.

Ein einfaches Dateisystem besteht aus:

  • Superblock: Enthält Metadaten wie Anzahl der Blöcke, Größe der Bitmap usw.
  • Inode-Tabelle: Jede Datei und jedes Verzeichnis hat einen Inode, der Zeiger auf Datenblöcke speichert.
  • Datenblöcke: Hier liegen die eigentlichen Dateiinhalte.

Inspiriert von Unix-Dateisystemen (wie ext2) kannst du eine einfache Inode-Struktur definieren:

struct inode {
    int mode;          // Typ (Datei/Verzeichnis)
    int size;          // Größe in Bytes
    int blocks[10];    // Zeiger auf Datenblöcke (für kleine Dateien)
    int indirect;      // Zeiger auf indirekten Block (für große Dateien)
};

Für Dateien unter 4 KB reichen direkte Zeiger. Für größere Dateien benötigst du einen indirekten Block, der auf weitere Blöcke verweist – wie bei einer verschachtelten Playlist in einer Musik-App, wo eine Playlist auf mehrere Songlisten verweist.

Implementierung der Basisoperationen (Schritt 2)

Beginne mit der Implementierung in nufs.c. Die wichtigsten FUSE-Callbacks sind:

  • getattr: Gibt Attribute einer Datei zurück (Größe, Rechte, Typ). Ohne funktionierendes getattr läuft nichts – es ist die Grundlage für alle anderen Operationen.
  • readdir: Listet den Inhalt eines Verzeichnisses auf.
  • create: Erstellt eine neue Datei.
  • write: Schreibt Daten in eine Datei (unter 4 KB).
  • read: Liest Daten aus einer Datei (unter 4 KB).
  • rename: Benennt eine Datei um.
  • unlink: Löscht eine Datei.

Ein typischer Ablauf für create:

  1. Prüfe, ob der Name bereits existiert.
  2. Allokiere einen freien Inode.
  3. Setze den Modus auf Datei und die Größe auf 0.
  4. Füge einen Eintrag im Elternverzeichnis hinzu.

Vergiss nicht, Fehler korrekt zurückzugeben – z. B. -ENOENT für „Datei nicht gefunden“. FUSE erwartet negative Fehlercodes.

Verzeichnisse implementieren (Schritt 3)

Verzeichnisse sind spezielle Dateien, deren Inhalt eine Liste von Directory-Einträgen ist. Jeder Eintrag enthält den Dateinamen und die Inode-Nummer. Für verschachtelte Verzeichnisse musst du Pfade wie /home/user/datei.txt parsen.

Implementiere folgende Callbacks:

  • mkdir: Erstellt ein neues Verzeichnis.
  • rmdir: Löscht ein leeres Verzeichnis.
  • rename für Verzeichnisse: Ermöglicht das Verschieben zwischen Ordnern.
  • readdir für Unterverzeichnisse: Zeige den Inhalt jedes Ordners an.

Eine clevere Methode ist, den Pfad in einzelne Komponenten zu zerlegen und Schritt für Schritt durch die Verzeichnisstruktur zu navigieren – ähnlich wie du in einer Datei-App auf dem Smartphone durch Ordner wischst.

Große Dateien unterstützen (Schritt 4)

Bisher konntest du nur Dateien bis 4 KB speichern. Für größere Dateien (z. B. 100 KB oder 500 KB) benötigst du indirekte Blockzeiger. Ein indirekter Block ist ein Datenblock, der nur Zeiger auf andere Datenblöcke enthält. Bei 4 KB Blockgröße und 4 Byte pro Zeiger passen 1024 Zeiger in einen indirekten Block – das erlaubt Dateien bis zu 4 MB (4 KB * 1024).

Passe deine Inode-Struktur an:

struct inode {
    int mode;
    int size;
    int direct[10];     // 10 direkte Zeiger
    int single_indirect; // Zeiger auf indirekten Block
    // optional: double_indirect für noch größere Dateien
};

Beim Schreiben einer großen Datei musst du:

  1. Zunächst die direkten Blöcke füllen.
  2. Wenn mehr als 10 Blöcke benötigt werden, einen indirekten Block allokieren und dort die weiteren Blocknummern speichern.
  3. Beim Lesen entsprechend die Zeigerkette verfolgen.

Teste dein System mit 100 Dateien à 4 KB, 5 Dateien à 100 KB und einer Datei mit 500 KB – so wie es die Aufgabenstellung verlangt.

Testen und Debuggen

Nutze das mitgelieferte Makefile:

  • make mount: Mountet das Dateisystem unter mnt/.
  • make unmount: Hängt es aus.
  • make test: Führt eine Testsuite aus (Achtung: kein vollständiger Autograder).
  • make gdb: Startet das Dateisystem im Debugger – ideal, um Fehler zu finden.

Arbeite in zwei Terminals: In einem mountest du das Dateisystem, im anderen führst du Befehle wie ls, touch, echo und rm auf dem Mountpunkt aus. So testest du, ob alles funktioniert.

Ein häufiger Fehler: getattr gibt falsche Werte zurück. Überprüfe, ob die Größe und der Typ (Datei vs. Verzeichnis) korrekt gesetzt sind. Auch die Berechtigungen (z. B. 0777) sollten stimmen, sonst verweigert das System den Zugriff.

Häufige Stolperfallen und Tipps

  • Fehlercodes: Gib immer negative Fehlercodes zurück, z. B. -ENOENT. Positive Werte werden als Erfolg gewertet.
  • Bitmap-Verwaltung: Vergiss nicht, Blöcke beim Löschen freizugeben. Sonst geht dir schnell der Speicher aus.
  • Namenslänge: Dein Dateisystem muss Namen mit mindestens 10 Zeichen unterstützen – plane den Directory-Eintrag großzügig (z. B. 256 Bytes).
  • Zeitüberschreitung: Die Tests haben ein 30-Sekunden-Limit. Stelle sicher, dass deine Operationen effizient sind (z. B. kein lineares Durchsuchen aller Inodes bei jedem Zugriff).

Zusammenfassung

Mit dieser Anleitung hast du einen Fahrplan, um dein eigenes Dateisystem für das CS3650 Projekt 2 zu bauen. Von der Installation über die Basisoperationen bis hin zu Verzeichnissen und großen Dateien – du lernst die Kernkonzepte der Dateisystementwicklung. Denke daran: Der Code muss sauber sein (Style zählt 20%). Schreibe kurze Funktionen, füge Kommentare hinzu und erkläre deine Annahmen. Viel Erfolg beim Bau deines eigenen Dateisystems – es ist eine der lohnendsten Aufgaben im Studium!