Aus Linux-Magazin 10/2005

Kernel- und Treiberprogrammierung mit dem Kernel 2.6 - Folge 23

Inodes und Dentries entschlüsselt: Das virtuelle Filesystem im Kernel lässt Applikationen auf unterschiedlichste Dateisysteme einheitlich zugreifen. Dieser Artikel erklärt die Grundlagen, entschlüsselt Inodes und Dentries und zeigt, wie man ein eigenes Filesystem implementiert.

In Sachen Dateisysteme ist Linux ein echtes Multitalent: Die Anzahl der von Linux unterstützten Filesysteme ist riesig, wie der Befehl »cat /proc/filesystems« auf der Konsole offenbart. Neben den Klassikern wie Ext 3, Reiser, JFS, NTFS oder FAT32 finden sich diverse virtuelle Kollegen. Proc-FS, Sys-FS und UBS-FS sind dabei nur die bekanntesten. Hinzu kommen noch Dateisysteme zur Verschlüsselung oder das “Filesystem in der Datei” (Loop).

Ordnung durch Abstraktion

Eine einheitliche Schnittstelle im Kernel macht diese Vielzahl für Programmierer beherrschbar. Die technische Lösung für eine solche Anforderung lautet Abstraktion. Die Kernelprogrammierer haben das Modell eines universalen Filesystems definiert, auf das sich die unterschiedlichsten Dateisysteme abbilden lassen.

Dieses Modell definiert, dass eine Datei aus Datenblöcken zusammengesetzt ist und eine Dateigröße, einen Besitzer, eine Gruppe und Zugriffsrechte hat (Abbildung 1). Außerdem gibt es unterschiedliche Dateiarten wie normale Dateien, Gerätedateien oder Verzeichnisdateien (Directories). Hinzu kommen noch Daten über die Zeitpunkte des Anlegens, der letzten Modifikation sowie des letzten Zugriffs auf das File.

Alle diesen Informationen stehen im so genannten Inode, der zentralen Datenstruktur jedes Unix-Dateisystems. In diesem Modell ist jedes Directory selbst eine Datei, deren Inhalt den Datei- oder Verzeichnisnamen einem Inode zuordnet (Abbildung 2).

Inodes und Dentries

Wie die Daten auf die einzelnen Blöcke eines Dateisystems verteilt werden oder wo die Inode-Informationen liegen, ist Sache des jeweiligen Filesystems (Abbildung 3). Die Implementierung bildet also das reale Dateisystem auf das VFS ab. Ein Ext-2-Filesystem orientiert sich dabei mit seinen Meta-Informationen sehr am VFS-Modell (genau genommen haben sich die Entwickler am realen FS orientiert). Ein FAT-Filesystem kennt dagegen beispielsweise keine Inodes. Also generiert der Filesystem-Code die Inodes allein auf Basis der vorhandenen Informationen. Spezifische Zugriffsrechte, die das FAT-Filesystem nicht kennt, ersetzt er dabei durch Defaultwerte.

Um auf ein Dateisystem zugreifen zu können, muss zunächst der Inode des Root-Verzeichnisses bekannt sein. Zumindest im Modell ordnet er die Subdirectories und die im Wurzelverzeichnis abgelegten Dateien den jeweils zugehörigen Inodes zu. Eine einzelne derartige Zuordnung zwischen Datei- respektive Verzeichnisname und Inode wird als Dentry (Directory Entry) bezeichnet (Abbildung 4).

Schneller mit Dcache

Erst die Kenntnis des Inode erlaubt den Zugriff auf die Daten der Datei. Die wichtigste Operation des VFS ist daher das Auffinden eines Inode, wenn ein vollständiger Dateiname gegeben ist. Dazu zerlegt es den gegebenen Dateinamen zunächst in seine Bestandteile. Aus »/usr/bin/vim« wird dabei »/«, »usr«, »bin« und »vim«. Im Folgenden sucht das VFS dann für die Pfadkomponente »usr« im Wurzelverzeichnis »/« (dessen Inode ja bekannt ist) nach dem zugehörigen Inode.

Da das VFS-Modell nicht festlegt, wie (und ob überhaupt) die Daten in einer Verzeichnisdatei abgelegt sind, muss jedes Dateisystem die Suchfunktion »lookup()« zur Verfügung stellen. Sie gibt den zur Pfadkomponente gehörigen »dentry« oder einen Fehlercode zurück. Wenn es den Dentry kennt, sucht das Filesystem im Beispielfall die Komponente »bin« im Verzeichnis »usr«. Mit jeder weiteren Pfadkomponente wiederholt es diesen Vorgang, bis es den gesuchten Dentry (und damit den Inode) gefunden hat.

Aus Performancegründen sucht der Kernel nicht mit jeder Pfadkomponente den zugehörigen Inode aufs Neue. Er speichert die Suchanfrage – bestehend aus dem darüber liegenden Inode und der Pfadkomponente – zusammen mit dem Ergebnis (dem zugehörigen Inode) in einem Dentry-Objekt. Die Dentries wiederum speichert er im so genannten Dcache »dentry_cache«.

Abbildung 1: Neben den eigentlichen Daten gehören zu jeder Datei vielfältige Attribute wie Größe, Eigentümer, Gruppe und so weiter.

Abbildung 1: Neben den eigentlichen Daten gehören zu jeder Datei vielfältige Attribute wie Größe, Eigentümer, Gruppe und so weiter.

Bei einer erneuten Anfrage sucht er zuerst im Cache. Erst wenn er hier keinen Dentry findet, beauftragt er den Filesystem-Code mit der Suche. Natürlich lassen sich im Dcache nicht unendlich viele Dentries ablegen. Nach dem Least-Recently-Used-Algorithmus (LRU) ersetzt der Kernel die jeweils ältesten Einträge durch neuere.

Hashes statt Suche

Den für die Suche notwendigen Vergleich zwischen der Pfadangabe im Verzeichnis und der gesuchten Pfadkomponente beschleunigt ein Hash-Algorithmus. Vereinfacht ausgedrückt ersetzt ein solches Verfahren einen teuren Suchalgorithmus durch eine einfache und damit schnelle Berechnung. Dazu trägt der Kernel im Dentry sowohl den realen Namen als auch dessen Länge sowie den zugehörigen Hashwert ein.

Abbildung 2: Inodes und Dentries realisieren die Filesystem-Hierarchie.

Abbildung 2: Inodes und Dentries realisieren die Filesystem-Hierarchie.

Im Detail ist das hier vorgestellte VFS-Modell natürlich etwas komplexer, schließlich unterstützt es auch noch Gerätedateien, Named Pipes und Links. Hardlinks sind dabei sehr einfach zu realisieren. Für sie legt der Kernel einfach einen weiteren Dentry an. Softlinks wiederum sind Pfadnamen, die nicht einem Inode, sondern einem anderen Pfadnamen zugeordnet sind.

Die beschriebenen Grundlagen reichen bereits aus, um praktische Erfahrungen durch die Implementierung eines eigenen virtuellen Filesystems zu sammeln. Der Betriebssystemkern unterstützt den Programmierer bei der Erstellung seines Filesystems nicht nur durch das skizzierte Modell, sondern zusätzlich mit einigen Hilfsfunktionen. Die meisten von ihnen sind in der Bibliothek »libfs.a« (in den Kernelquellen in »/usr/src/linux/fs/libfs.c«) zu finden.

Abbildung 3: Beim Zugriff auf Daten sind mehrere Komponenten des Kernels beteiligt, die schichtartig aufeinander aufbauen. Unten im Kernel befinden sich die Gerätetreiber, ganz oben das VFS.

Abbildung 3: Beim Zugriff auf Daten sind mehrere Komponenten des Kernels beteiligt, die schichtartig aufeinander aufbauen. Unten im Kernel befinden sich die Gerätetreiber, ganz oben das VFS.

Das hier vorgeführte Kernelmodul in Listing 1 implementiert ein Filesystem-Objekt, für jedes dann wirklich gemountete Filesystem einen Superblock und für jede Datei ein Inode-Objekt. Zusätzlich braucht das Beispiel die beschriebenen Dentry-Objekte, die Inodes und Dateinamen einander zuordnen.

Ein neues Dateisystem registriert sich beim Betriebssystemkern über »register_filesystem()«. Parameter dieser Funktion ist erwartungsgemäß das Filesystem-Objekt, welches das neue Dateisystem typisiert: Es legt also den Namen (zum Beispiel »ext2«) fest und stellt eine Methode zur Verfügung, mit der der Kernel den Superblock für eine Instanz des Filesystems erhält.

Zeile 109 in Listing 1 ordnet dem zugehörigen Feld ».get_sb« die Funktion »get_superblock()« (Zeile 99) zu. Diese Methode wird typischerweise beim Mounten des Filesystems aufgerufen. Sie bekommt unter anderem den Gerätenamen übergeben, also zum Beispiel »/dev/hda1«. Bei virtuellen Dateisystemen ist der Gerätename nicht relevant, es hat sich daher eingebürgert, beim Mounten dafür das Schlüsselwort »none« zu verwenden.

Die vom Kernel bereitgestellte Funktion »get_sb_single()« reserviert den Speicher für den Superblock und führt die Grundinitialisierung durch. Die Beispielfunktion »fill_superblock()« schließt die Initialisierung ab (Listing 1, Zeile 70).

Listing 1:
»fs.c«

002 #include <linux/init.h>
003 #include <linux/pagemap.h>
004 #include <linux/fs.h>
005 
006 static ssize_t read_hello_world(struct file *, char *, size_t , loff_t *);
007 
008 static struct super_operations superblock_ops = {
009     .statfs     = simple_statfs,
010     .drop_inode = generic_delete_inode,
011 };
012 
013 static struct file_operations file_ops = {
014     .read = read_hello_world,
015 };
016 
017 static ssize_t read_hello_world(struct file *instanz, char *buf,
018     size_t tocopy, loff_t *offset)
019 {
020     char *text = "hello worldn";
021     int c, len=strlen(text)+1;
022 
023     if( tocopy<len )
024         tocopy=len;
025     if( (c=copy_to_user(buf,text,len))<0 )
026         return -EFAULT;
027     return tocopy-c;
028 }
029 
030 static struct inode *make_new_inode(struct super_block *sb, int mode)
031 {
032     struct inode *ret = new_inode(sb);
033 
034     if (ret) {
035         ret->i_mode = mode;
036         ret->i_uid = ret->i_gid = 0;
037         ret->i_blksize = PAGE_CACHE_SIZE;
038         ret->i_blocks = 0;
039         ret->i_atime = ret->i_mtime = ret->i_ctime = CURRENT_TIME;
040     }
041     return ret;
042 }
043 
044 static struct dentry *create_file(struct super_block *sb,
045     struct dentry *parent, char *name)
046 {
047     struct qstr qname;
048     struct inode *inode;
049     struct dentry *dentry;
050 
051     inode = make_new_inode(sb, S_IFREG | 0644);
052     if( !inode )
053         return 0;
054     inode->i_fop = &file_ops;
055     inode->i_size = 14; // strlen("hello worldn")+1;
056 
057     qname.name = name;
058     qname.len = strlen(name);
059     qname.hash = full_name_hash(name, qname.len);
060 
061     dentry = d_alloc(parent, &qname);
062     if( !dentry ) {
063         iput( inode );
064         return 0;
065     }
066     d_add(dentry, inode);
067     return dentry;
068 }
069 
070 static int fill_superblock(struct super_block *sb, void *data, int silent)
071 {
072     struct inode *root;
073     struct dentry *root_dentry;
074 
075     sb->s_blocksize = PAGE_CACHE_SIZE;
076     sb->s_blocksize_bits = PAGE_CACHE_SHIFT;
077     sb->s_magic = 0x20040804;
078     sb->s_op = &superblock_ops;
079 
080     // Inode fuer das Root-Verzeichnis anlegen...
081     root = make_new_inode(sb, S_IFDIR | 0755);
082     if( !root )
083         return -ENOMEM;
084     root->i_op = &simple_dir_inode_operations;
085     root->i_fop = &simple_dir_operations;
086     root_dentry = d_alloc_root(root);
087     printk("root_dentry->name: %sn", root_dentry->d_iname );
088     if( !root_dentry ) {
089         iput( root );
090         return -ENOMEM;
091     }
092     sb->s_root = root_dentry;
093 
094     create_file( sb, root_dentry, "hello" );
095 
096     return 0;
097 }
098 
099 static struct super_block *get_superblock(struct file_system_type *fst,
100     int flags, const char *devname, void *data)
101 {
102     printk("get_superblock (%s, %s)...n", fst->name, devname);
103     return get_sb_single(fst, flags, data, fill_superblock);
104 }
105 
106 struct file_system_type hello={
107     .owner              = THIS_MODULE,
108     .name               = "hello",
109     .get_sb             = get_superblock,
110     .kill_sb    = kill_litter_super
111 };
112 
113 static int __init mod_init(void)
114 {
115     return register_filesystem(&hello);
116 }
117 
118 static void __exit mod_exit(void)
119 {
120     unregister_filesystem(&hello);
121 }
122 
123 module_init( mod_init );
124 module_exit( mod_exit );
125 MODULE_LICENSE("GPL");

Beim Unmounten des Filesystems ruft der Kernel die Methode »kill_sb()« auf. Wenn das Filesystem keine besonderen De-Initialisierungen durchführen muss, kann der Programmierer einfach auf die ebenfalls vorhandene Funktion »kill_litter_super()« zurückgreifen.

Jede Filesystem-Instanz benötigt nicht nur den Superblock, sondern auch einen Inode und ein »dentry«-Objekt für das Wurzelverzeichnis. Beides legt das Beispiel beim Mounten an. Das Filesystem soll eine (virtuelle) Datei enthalten, die beim Lesen den String »Hello World« zurückgibt. »struct file_operations« ordnet dem Inode dieser Datei die Lesefunktion »read_hello_world()« zu (Zeile 13). Die Implementierung der »struct file_operations«-Funktionen hatte die zweite Kern-Technik-Folge vorgestellt [1]. Im Ernstfall müsste man für die Inodes noch die beschrieben »lookup()«-Funktionen implementieren. Stattdessen erzeugt das Beispiel direkt das »dentry«-Objekt und legt es über die Funktion »d_add()« im Dcache ab (Zeile 66).

Abbildung 4: Das VFS-Modell hat vier verschiedene Objekte, Inodes und Dentries realisieren die Hierarchie.

Abbildung 4: Das VFS-Modell hat vier verschiedene Objekte, Inodes und Dentries realisieren die Hierarchie.

Listing 1 zeigt den vollständigen Code des virtuellen Dateisystems. Der Name des Filesystemtyps lautet »hello« (Zeile 108). Der Code und das zugehörige Makefile ist unter [2] zu finden. Ist das kompilierte Modul geladen, mountet der Befehl »mount -t hello none /mnt« das virtuelle Filesystem.

Ein »ls -l /mnt« zeigt die Datei »hello«, die sich per »cat« lesen lässt. Der Einfachheit halber ist »hello« als unendliche Datei realisiert, also als eine, die beim Lesen nie ein End-of-File liefert. Der Befehl »cat /mnt/hello« lässt sich mit [Strg]+[C] abbrechen.

Nicht stehen bleiben

Wer nach dieser Einführung auf den Geschmack gekommen ist, kann versuchen die Moduldatei »hello« um eine Schreibfunktion zu erweitern. Danach könnte die Implementierung eines Unterverzeichnisses auf dem Plan stehen. Hilfestellung dazu leisten [3], [4] und [5]. Ein weiterer wichtiger Ausgangspunkt für eigene Dateisystem-Projekte ist der offene Quellcode des Kernels, vor allen Dingen das von Linus Torvalds selbst erstellte Template »/usr/src/linux/fs/ramfs/inode.c«. (ofr)

Die Autoren

Eva-Katharina Kunst, Journalistin, und Jürgen Quade, Professor an der Hochschule Niederrhein, sind seit den Anfängen von Linux Fans von Open Source. Unter dem Titel “Linux Treiber entwickeln” haben sie zusammen ein Buch zum Kernel 2.6 veröffentlicht.

Infos

[1] Eva-Katharina Kunst und Jürgen Quade, “Kern-Technik”, Folge 2: Linux-Magazin 9/03, S. 86

[2] Listings und Makefile: [https://www.linux-magazin.de/Service/Listings/2005/10/Kern-Technik/]

[3] Robert Love, “Linux Kernel Development, Second Edition”: Novell Press 2005

[4] Wolfgang Mauerer, “Linux Kernelarchitektur”: Hanser Verlag 2004

[5] Jonathan Corbet, “Creating Linux virtual Filesystems”: [http://lwn.net/Articles/57369/]

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben