blog · git · desktop · images · contact
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?
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:
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.
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.
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.
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:
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.Kleine Schritte, aber man lernt sehr viel.
Der Code ist erstmal hier:
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.