blog · git · desktop · images · contact


Experimente mit eigenen Dateisystemen unter Linux

2017-09-09

Ich halte Dateisysteme für ein äußerst spannendes Thema, weil man sie täglich benutzt und kein Weg um sie herum führt. Jedes Bit, das man produziert, muss von einem Dateisystem organisiert und abgelegt werden.

Trotzdem hatte ich mir bisher nicht die Zeit genommen, ein konkretes Dateisystem genau zu betrachten und gänzlich zu verstehen. Die meisten grundlegenden Konzepte sind mir zwar klar: Was ist eine Inode, was ist ein Superblock, wieso funktionieren Hardlinks nicht über Dateisystemgrenzen hinweg, … Dieses Level an Verständnis. Viel mehr aber auch nicht und wenn, dann kannte ich es nur aus der Theorie.

Ich wusste und weiß auch fast nichts darüber, was im Kernel passiert. Den Begriff „VFS“ hatte ich schon gehört, aber was genau macht das? Wie funktioniert das?

Solange ich nicht versuche, ein Thema tatsächlich in der Praxis umzusetzen, besteht für mich nur eine geringe Chance, dieses Thema wirklich zu verstehen. Die Theorie kann ich rauf und runter lesen, wirklich „Klick“ macht es aber erst, wenn ich es selbst mal gemacht habe. Damit ist die Mission ja klar: Einen eigenen Dateisystemtreiber schreiben. Wie schwer kann das schon sein?

Erste Schritte

Wo fängt man da an? Ein Kernelmodul muss ich schreiben. Naja, streng genommen muss ich das nicht, denn ich könnte auch FUSE benutzen. Das habe ich in 2011 auch schon einmal gemacht, bin aber nicht allzu weit gekommen. Vielleicht war ich damals nicht genug motiviert. Spielt auch keine Rolle. Diesmal musste es ein Kernelmodul sein, weil ich es wollte.

Einer der ersten Blogposts, den man zu dem Thema findet, ist dieser hier:

https://kukuruku.co/post/writing-a-file-system-in-linux-kernel/

Am Ende des Artikels ist dieses Repo verlinkt:

https://github.com/psankar/simplefs

Mit diesen beiden Ressourcen habe ich angefangen. Dort lernt man dann die absoluten Basics:

  1. Wie schreibt und kompiliert man überhaupt ein Kernelmodul?
  2. Wie registriert man einen Dateisystemtreiber und was sind die ersten Schritte, um etwas zu „mounten“?

Schon noch kurzer Zeit hatte ich dann mountablefs fertig: Das tut erstmal nichts anderes, als ein Device zu „mounten“ und eine Dummy-Inode zur Verfügung zu stellen, damit man einen leeren Verzeichnisbaum sehen kann. Da sind noch keinerlei echte Daten im Spiel, daher ist das „Mounten“ auch nur ein Dummy-Schritt.

To the sky and beyond

Meine nächste Übung war dann, echte Daten von der Festplatte zu lesen. „Festplatte“? Natürlich lasse ich erst einmal alles in einer virtuellen Maschine laufen. Wenn Code im Userspace abstürzt, ist das nicht schlimm. Im Kernelspace kann es aber ein echtes Problem sein und den Rechner lahmlegen. Das muss man ja nicht riskieren.

onefilerofs war dann ins Leben gerufen. Funktioniert ganz einfach:

Das „ro“ zeigt an, dass alles read-only ist. Um Daten zu schreiben, habe ich einen Hexedit benutzt und dann das Dateisystem neu gemountet.

Hat aber funktioniert! Mein erstes Kernelmodul, welches Daten von einer Festplatte lesen und dem Userspace übergeben kann.

Schon bald darauf bin ich auf „address space operations“ gestoßen. Das ist eine Abstraktionsschicht im VFS, mit der man sich die Implementierung von read und write sparen kann. Stattdessen fragt der Kernel das Module nach dem n-ten Block einer Datei und du sagst ihm, dass dieser im m-ten Block auf der Festplatte zu finden ist. Das versteckt also die ganzen dreckigen Details wie „seek“ oder die Problematik, die entsteht, wenn ein Prozess ein Byte schreiben will. Es kommt auch nicht einmal großartig darauf an, ob der Prozess lesen oder schreiben will – man muss ihm nur das Mapping zur Verfügung stellen.

So wurde dann asopfs fertig. Nach wie vor gab es nur eine Datei, sie konnte nun aber vom Userspace aus geschrieben werden. Das heißt, dass sich die Größe der Datei auch ändern konnte, weshalb asopfs sich um das Updaten von Inodes kümmern muss. Eine „Inode“ ist hier aber immer noch nur die ersten 64 Bits von Block 0.

Zu meiner Überraschung musste ich feststellen, dass die Größe einer Page im RAM mit der eines Blocks auf der Festplatte übereinstimmt. Beide sind 4096 Bytes groß. Allerdings bin ich mir nach wie vor nicht sicher, ob das nur an meinem Setup liegt oder ob das im Kernel mal vereinheitlicht wurde. „Das Internet“ spricht oft davon, dass es eigentlich einen Unterschied geben sollte.

Auf die Nase gefallen

Das Obige passierte alles innerhalb von drei oder vier Tagen. Ich habe viel gelesen und mich durch den Code vom Minix-Dateisystem gewühlt. Da führt kaum ein Weg daran vorbei, denn „Dokumentation“ ist rar gesät. Sobald man das Userland verlässt und in den Kernelspace vordringt, hört das Internet auf, hilfreich zu sein. Seien wir ehrlich: Wir benutzen alle ständig Google. Wenn es ein Problem gibt, wird danach gesucht und gehofft, dass es schon jemand anderes gelöst hat.

Das funktioniert nicht mehr, sobald man ein Kernelmodul schreiben will.

Es gibt keine „Tutorials“. Es gibt fast keine Blogposts zu dem Thema. Vielleicht gibt es Bücher, aber ich wollte nicht schon wieder 100€ dafür ausgeben. Zumindest noch nicht.

Was man lesen muss, ist vfs.txt. Auch muss man den Quellcode „einfacher“ Dateisysteme lesen, zum Beispiel den von Minix. Man kann sich so durchaus viele Informationen zusammenkratzen.

Manchmal muss man auch wissen, wie eine bestimmte Funktion im Kernel implementiert ist. Hier hilft dann eine Cross-Reference weiter.

Und man muss kleine Schritte machen. Nachdem ich mit asopfs „fertig“ war, dachte ich mir: „Gut, dann kann ich ja jetzt ein echtes Dateisystem schreiben.“ Habe ich dann auch versucht und mit basicfs angefangen. Das hatte immer noch ein einfaches Design, aber es gab schonmal eine Bitmap für freie Blöcke und Objekte wie Verzeichnisse und Dateien konnten beliebig groß werden. Es gab echte Inodes auf der Platte, Symlinks, Hardlinks und Special Files hätte man implementieren können. Wie schwer kann’s schon sein?

Schwerer als erwartet. Nach etwa einem Monat brach ich ab und warf den Code weg.

Schritt für Schritt

basicfs war einfach zu viel. Zu viel Komplexität, zu viele Dinge, die ich gleichzeitig hätte lernen müssen. Ich wusste nach wie vor zu wenig über VFS, um meinen Code sinnvoll strukturieren zu können.

Ich warf es also weg und fing mit oneblockfs an. Das erlaubte mir dann, weitere Operationen zu implementieren (vor allen Dingen das Erstellen von Dateien und Verzeichnissen), war aber noch einfach genug, um lästigere Probleme wie das Verwalten von Blöcken einer Datei zu vermeiden:

Dabei gibt es zwei wichtige Beschränkungen:

  1. Ein „Objekt“ im Dateisystem (also ein Verzeichnis oder eine Datei) ist immer genau einen Block groß. Block n enthält dabei die Inode für dieses Objekt und Block n + 1 dann die Nutzdaten. Dadurch ist das Dateisystem nicht in der echten Welt benutzbar, mein Leben im Kernelmodul wird aber wieder einfacher.
  2. Nummern von Inodes werden nicht wiederbenutzt. Dadurch wird das Finden einer neuen Inode sehr einfach: Man sucht einfach einen gerade benummerten Block, der als frei markiert ist, und gibt der Inode die nächste verfügbare Nummer. Diesen Counter speichert man dann im Superblock ab. Das heißt natürlich, dass man irgendwann keine freien Nummern für Inodes mehr hat, und ab da ist das Dateisystem unbenutzbar. Löschen hilft auch nicht.

Kleine Schritte, aber man lernt sehr viel.

Wie geht es jetzt weiter?

Der Code ist erstmal hier:

filesystem-experiments

Auf gar keinen Fall behaupte ich, dass das perfekter Code sei. Vermutlich genau das Gegenteil. Es gibt zum Beispiel überhaupt keine Locks. Das heißt, wenn zwei Prozessse gleichzeitig im Dateisystem rumwuseln, könnte der Kernel explodieren.

Es gibt noch so viel zu lernen. VFS, Caching, Locking, was auch immer. Pull-Requests oder Kommentare wären natürlich großartig. :-)

Warum habe ich das Repo überhaupt veröffentlicht? Weil ich denke, dass es gut ist, mehr Codebeispiele für Kernelcode zu haben. Vor allen Dingen die einfachen Sachen, die einem beim Einstieg in das Thema helfen.

Und wie geht’s weiter? Ich hoffe, dass mir das alles beim Verstehen anderer Dateisysteme hilft. Vielleicht kann ich irgendwann in ferner Zukunft den Code von Ungetümen wie ext4, XFS or btrfs dann lesen und verstehen. Vielleicht.

Comments?