Aus Linux-Magazin 09/2006

Workshop: Kernel- und Treiberprogrammierung mit dem Kernel 2.6 - Folge 29

Das Relay-Subsystem ermöglicht den Datenaustausch großer Datenmengen zwischen Kernel- und Userspace in Hochgeschwindigkeit. Diese Kern-Technik-Folge erklärt dies Schritt für Schritt.

Debug- oder Log-Informationen werden innerhalb des Kernels klassischerweise mit Hilfe von »printk()« in den Userspace übertragen und dort vom Syslog weiterverarbeitet. Neben dieser einfachen Variante hat sich in der Vergangenheit das Proc- und seit neuestem das Debug-Filesystem etabliert. Ob per »printk()« oder mit der Ausgabe über ein virtuelles Dateisystem: Der Transfer großer Datenmengen zwischen Kernel und Applikationen ist aufwändig und zudem programmiertechnisch kompliziert. Daten werden nämlich zuerst in Puffern (die überlaufen können) zwischengespeichert, bevor sie im Speicherbereich einer Applikation landen.

Um diese Probleme zu lösen, entwickelten einige Kernelhacker mit dem Relay-FS ein neues, virtuelles Dateisystem, das einerseits einfach zu handhaben ist und andererseits dank »mmap()« einer Anwendung den direkten Zugriff auf die Daten ohne weiteren Kopiervorgang ermöglicht. Mit dem Kernel 2.6.14 nahm Linus Torvalds dieses Dateisystem in den Kernel auf.

Suchen Sie allerdings in einem aktuellen Kernel nach dem Relay-Filesystem, wird die Suche vergeblich sein: Getreu dem Motto “Keep it simple” haben die Entwickler den Code kräftig abgespeckt, einige Betriebsmodi wie Lockless und den Part des virtuellen Dateisystems komplett entfernt und nur das Kernfeature übrig gelassen: schnelle Transfers großer Datenmengen zwischen Kernel- und Userspace. Statt eines speziellen Filesystems sollen die Nutzer jetzt die vorhandenen virtuellen Dateisysteme nutzen, insbesondere das Debug-Filesystem. An einer einfachen Anbindung des Sys-FS arbeiten die Entwickler noch.

Debug-Filesystem hilft

Wollen Sie also von einem Treiber oder einem anderen Kernelmodul aus einer Anwendung Daten zur Verfügung stellen, bietet sich eine Relay-Datei im Debug-Filesystem an. Das Verfahren ist denkbar einfach (siehe Abbildung 1): Als Erstes legen Sie mit »debugfs_create_dir()« im Debug-Filesystem ein Verzeichnis an und erzeugen dann einen so genannten Relay-Kanal (»rchan«, »relay_open()«, siehe Tabelle 1).

Abbildung 1: In vier Schritten zum eigenen Relay-Kanal. Dann ist er mit der Funktion »relay_write()« verwendbar.

Abbildung 1: In vier Schritten zum eigenen Relay-Kanal. Dann ist er mit der Funktion »relay_write()« verwendbar.

Tabelle 1: Interface des
Relay-Subsystems

Dafür ist es nötig, zwei kurze Callback-Funktionen zu implementieren (siehe Abbildung 2). Den ersten Callback ruft der Kernel auf, wenn das Relay-Subsystem die Relay-Datei anlegen möchte. Erzeugen Sie dazu die Datei mit Hilfe des Debug-Filesystems (siehe Kasten “Debug-Filesystem”). Der zweite Callback wird aufgerufen, um die Relay-Datei wieder zu entfernen. Damit löschen Sie die zuvor erzeugte Datei im Debug-Filesystem wieder.

Abbildung 2: Bei den zu implementierenden Callbacks für eine Relay-Datei sind optionale und obligatorische zu unterscheiden.

Abbildung 2: Bei den zu implementierenden Callbacks für eine Relay-Datei sind optionale und obligatorische zu unterscheiden.

Debug-Filesystem

Mit dem Debug-Filesystem haben die Kernelentwickler eine elegante Möglichkeit geschaffen, einzelne Variablen und ganze Speicherbereiche (Blobs) des Kernels einer Applikation zum Lesen und auch zum Schreiben zur Verfügung zu stellen. Die Variablen selbst sind aus Sicht der Anwendung auf Pseudodateien abgebildet, die der Anwendungsentwickler auch über die Systemcalls »read()« und »write()« lesen und schreiben kann. Beim Lesen wird der Wert der Variablen in eine Ascii-Repräsentation umgewandelt. Entsprechend übergibt die Anwendung den Wert ebenfalls in Ascii.

Die Programmierschnittstelle zu dem Debug-Filesystem ist einfach, Tabelle 2 listet die Prototypen der Funktionen auf. Angenehm ist: Jeweils nur ein Funktionsaufruf legt sowohl ein Verzeichnis im Debug-Filesystem als auch eine Variablendatei an. Der Funktion zum Erstellen der Variablendatei übergeben Sie unter anderem die Adresse der Variablen, die beim Zugriff auf die Datei ausgelesen respektive geschrieben werden soll. Als Datentypen stehen neben den Standardformaten (»u8«: Unsigned Char, »u16«: Unsigned Short, »u32« Unsigned Int) auch »boolean« und »blob« zur Verfügung. Bei Blobs handelt es sich um normale Speicherbereiche zum Austausch binärer Daten.

Pseudodateien selbst gemacht

Zusätzlich erlaubt das Debug-FS das Anlegen eigener Pseudodateien. Dabei implementieren Sie selbst die Lese- und die Schreibfunktion »read()« und »write()«. Diesen Mechanismus nutzt auch das Relay-Subsystem. Das Aufräumen übernimmt »debugfs_remove()«.

Listing 1 zeigt ausschnittweise den Einsatz des Debug-Filesystems an einem einfachen Beispiel, der vollständige Code ist auf der Website des Linux-Magazins zu finden [3]. Zeile 7 erzeugt zunächst ein Verzeichnis, Zeile 12 legt innerhalb des neuen Verzeichnisses die Debug-FS-Datei »index« an. Eine Applikation kann über diese Datei die Variable »index« lesen und schreiben. Der Datenaustausch zwischen User- und Kernelspace findet ebenfalls im Ascii-Format statt.

Und noch zwei Hinweise. Erstens: Bei den über das Debug-Filesystem exportierten Variablen muss es sich um globale Variablen handeln. Zweitens: Auf Blobs lässt sich nur lesend zugreifen. Überhaupt ist der Zugriff auf die Variablen natürlich nur dann möglich, wenn das Debug-Filesystem gemountet ist.

Listing 1:
»debugfs.c«

01 ...
02 static struct dentry *vardir, *varfile;
03 static u32 index=99;
04 
05 static int __init mod_init(void)
06 {
07     vardir = debugfs_create_dir("vardir", NULL);
08     if( vardir==NULL ) {
09         printk("Kann debugfs-dir nicht anlegenn" );
10         return -EIO;
11     }
12     varfile = debugfs_create_u32("index", S_IRUGO|S_IWUGO, vardir, &index);
13     if( varfile==NULL ) {
14         printk("Kann debugfs-file nicht erzeugenn");
15         debugfs_remove( vardir );
16         return -EIO;
17     }
18     printk("Laden: index hat den Wert: %dn", index );
19     return 0;
20 }
21 
22 static void __exit mod_exit(void)
22 {
23     printk("Entladen: index hat den Wert: %dn", index );
24     debugfs_remove( varfile );
25     debugfs_remove( vardir );
26 }
27 ...

Tabelle 2: Kernel-Schnittstelle
zum Debug-Filesystem

Mehrere CPUs

Die über den so angelegten Relay-Kanal publizierten Informationen bildet der Userspace auf einen Dateizugriff ab, wobei für jede CPU eine eigene Ausgabedatei existiert. Auf einer Einprozessor-Maschine gibt es also eine, auf einer Zweiprozessor-Maschine zwei Relay-Dateien. Hintergrund dieser Maßnahme: Jede CPU schreibt die Daten in eigene Per-CPU-Variablen (Ringpuffer), Locking ist nicht notwendig (siehe [1]). Der für jede CPU einmal vorhandene Ringpuffer unterteilt sich wiederum in Teilpuffer (siehe Abbildung 3).

Abbildung 3: Kernel-intern ist die Relay-Datei als Ringpuffer realisiert, der seinerseits aus mehreren Teilpuffern besteht.

Abbildung 3: Kernel-intern ist die Relay-Datei als Ringpuffer realisiert, der seinerseits aus mehreren Teilpuffern besteht.

Größe und Anzahl dieser Puffer werden beim Erzeugen des Kanals festgelegt. Die Gesamtgröße des verwendeten Speichers ergibt sich über die Formel: Gesamtgröße = (Anzahl_Puffer * Puffergröße) * Anzahl_Prozessoren.

Mehrere Kriterien bestimmen die Größe der einzelnen Puffer. Zum einen sollte sie ein Vielfaches von 2 sein und am besten in Beziehung zu einer Page (4096 Bytes) stehen. Das Relay-Subsystem reserviert den notwendigen Speicher nämlich auf Basis von Speicherseiten. Zum anderen sollte die längste, durch einen Funktionsaufruf zu schreibende Nachricht in den Puffer passen. Denn das Relay-Subsystem weigert sich aus Performance-Gründen, Log-Messages auf mehrere Puffer aufzusplitten. Wählen Sie beispielsweise eine Puffergröße von 128 Bytes, können Sie nicht mit einem einzelnen Aufruf 129 Bytes schreiben, die Daten würden verworfen.

Sind die zu schreibenden Häppchen klein genug, werden sie hintereinander in den aktiven Puffer geschrieben, solange noch ausreichend Platz ist, um die Nachricht komplett abzulegen. Sollte der Puffer bereits so weit gefüllt sein, dass die Daten des aktuellen Auftrags nicht mehr hineinpassen, gilt der Puffer als gefüllt, auch wenn einige Bytes ungenutzt bleiben, die man Padding-Bytes nennt. Die Daten des aktuellen Auftrags fließen dann in den nächsten Puffer.

Sobald der letzte Puffer gefüllt ist, legt das Relay-Subsystem die Daten wieder im ersten Puffer ab. Ob es tatsächlich dazu kommt, hängt allerdings davon ab, ob eine Applikation die Daten in der Zwischenzeit gelesen hat. Ist dies nicht der Fall, werden die neuen Daten verworfen. Zur Realisierung zählt das Relay-Subsystem beim Aufruf von »read()« mit, wie viele Daten bereits in den Userspace transferiert wurden.

Zwei Modi zur Auswahl

Einfacher ist es, die Relay-Datei nicht im Non-Override-Modus, sondern im Override-Modus zu betreiben. Diese Betriebsart gestattet es dem Relay-Subsystem, einen Puffer zu überschreiben und damit unter Umständen den Verlust von (alten) Daten in Kauf zu nehmen. Um den Überschreibmodus (Override) zu aktivieren, implementieren Sie einen eigenen Callback »subbuf_start«. Diese Callback-Funktion wird jedes Mal aufgerufen, wenn das Relay-Subsystem einen Puffer neu beschreiben möchte (also beim Wechsel der Puffer). Die Callback-Funktion hat drei Aufgaben:

  • Den nächsten Puffer zu initialisieren, falls dies
    nötig ist.
  • Den soeben beschriebenen Puffer zu finalisieren.
  • Über den Rückgabewert dem Relay-Subsystem
    mitzuteilen, ob es den nächsten Puffer beschreiben darf oder
    nicht.

Gibt die Callback-Funktion 0 zurück, bedeutet dies, dass die Message nicht in den nächsten Puffer wandert. Wahrscheinlich hat bisher keine Applikation die Daten in diesem Puffer abgeholt. Der Rückgabewert 1 zeigt an, dass der Puffer beschreibbar ist. Befinden sich dort nicht abgeholte Daten, werden sie überschrieben. Die Callback-Funktion »subbuf_cb()« gibt für diesen Override-Fall den Wert 1 zurück:

static int subbuf_cb (struct rchan_buf *buf, void *subbuf, void *prev_subbuf,size_t prev_padding) { return 1; }

Der Parameter »subbuf« spezifiziert die Speicheradresse des Puffers, in den die nächsten Daten wandern sollen. »prev_subbuf« enthält die Speicheradresse des soeben beschriebenen Puffers, »prev_padding« enthält die Anzahl der im letzten Puffer ungenutzten Bytes.

Zusätzlich gibt es die Callbacks »buf_mapped« und »buf_unmapped«. Der Kernel ruft sie immer dann auf, wenn eine Applikation ein »mmap()« respektive ein »munmap()« durchführt (siehe Abbildung 2).

Daten ausgeben

Die eigentliche Ausgabe der Daten übernimmt innerhalb des Moduls die Funktion »relay_write()«. Sie ähnelt in ihren Parametern dem aus dem Userspace bekannten Systemaufruf »write()«: die Channel-ID als Referenz auf den Ausgabekanal, die Adresse der zu schreibenden Daten und die Anzahl der zu schreibenden Bytes. Die Funktion darf im Kernel sowohl im Interrupt- als auch im Kernel- respektive im Prozess-Kontext ablaufen. Sind Sie sicher, niemals Daten im Interrupt-Kontext zu protokollieren, können Sie auch die Variante »__relay_write()« verwenden. So ersparen Sie dem Kernel eine Lock-Operation.

Listing 2 zeigt den Kernelcode, der eine Relay-Datei anlegt und darüber Log-Informationen ausgibt. Um den Code zu testen, müssen Sie allerdings einen Kernel mindestens in Version 2.6.17. verwenden. Er muss sowohl das Debug-Filesystem als auch das Relay-Subsystem aktiviert haben. Falls nicht, kommen Sie um Kernelkonfiguration und -generierung nicht herum. Hinweise hierzu finden Sie unter [2]. Den Quellcode des Moduls übersetzen Sie am einfachsten mit dem Makefile von [3].

Listing 2:
»log_debugfs.c«

01 ... 
02 static char *TEXT="hello worldn";
03 static struct rchan *channel_id;
04 static struct dentry *logdir;
05 
06 static int lremove_buf_file(struct dentry *dentry)
07 {
08     debugfs_remove(dentry);
09     return 0;
10 }
11 
12 static struct dentry *lcreate_buf_file(const char *filename,
13     struct dentry *parent, int mode, struct rchan_buf *buf, int *is_global)
14 {
15     return debugfs_create_file(filename, mode, parent, buf,
16         &relay_file_operations);
17 }
18 
19 static int lsubbuf_start(struct rchan_buf *buf, void *subbuf, void *prev_subbuf,
20     unsigned int prev_padding)
21 {
22     return 1; // bufferchange can occur
23 }
24 
25 struct rchan_callbacks cb = {
26     subbuf_start:  lsubbuf_start,
27     create_buf_file: lcreate_buf_file,
28     remove_buf_file: lremove_buf_file,
29 };
30 
31 static int __init mod_init(void)
32 {
33     int i;
34 
35     logdir = debugfs_create_dir("relay", NULL);
36     if( logdir==NULL ) {
37         return -EIO;
38     }
39 
40     channel_id = relay_open( "logfile", logdir, 32, 4, &cb );
41     for( i=0; i<20; i++ ) { // Probehalber die Puffer fuellen
42         relay_write( channel_id, TEXT, strlen(TEXT)+1 );
43     }
44     return 0;
45 }
46 
47 static void __exit mod_exit(void)
48 {
49     relay_close( channel_id );
50     debugfs_remove( logdir );
51 }
52 ...

Problemfall »mmap()«

Auf Anwendungsseite lesen Sie den Inhalt der Relay-Datei beispielsweise mit Hilfe von »cat«. Viele Daten schreibt das Testmodul jedoch nicht, einige Hello-World-Strings müssen zur Demonstration genügen.

Der schnelle Zugriff auf die Daten per »mmap()« ist gar nicht so einfach umzusetzen. Da die Anwendung unbemerkt vom Kernel auf den Datenbereich zugreift, muss er sie auf irgendeinem Weg informieren, wann sie das darf. Umgekehrt muss sie den Kernel – im Non-Override-Modus – davon in Kenntnis setzen, welche Puffer sie bereits konsumiert hat. Beide Mechanismen lassen sich auf Basis des Debug-Filesystems realisieren.

Notwendige Variablen wie die Puffergröße und die Anzahl der Teilpuffer exportiert die Funktion »debugfs_create_uXX()« in den Userspace. Indem der Kernel mit der Funktion »read()« eine mit »debugfs_create_file()« erzeugte Datei liest, teilt er der Anwendung mit, dass sie einen neuen Teilpuffer lesen kann. Damit erfährt die Anwendung außerdem, wie viele gültige Bytes der Puffer enthält. Mit »write()« signalisiert sie dem Kernel, dass sie den Puffer konsumiert hat. Der Kernel ruft daraufhin die Funktion »subbufs_consumed()« aus dem Relay-Filesystem auf.

Kleiner und weniger Bugs

Als das Relay-Subsystem noch das Relay-Filesystem war, hatte es – wie der leider nicht mehr aktuellen Dokumentation [4] und [5] zu entnehmen ist – noch einige Features mehr zu bieten. Doch der Devise “Weniger ist mehr” folgend reduzierten die Kernelentwickler den neuen Mechanismus auf seine Grundfunktionalität (siehe den Artikel [6]). Erfreulicherweise haben sie dabei gleich noch einige Fehler behoben. Nutzer des neuen Relay-Subsystems gibt es im Übrigen im Kernel bisher überraschend wenige – was sich sicherlich bald ändern wird. (ofr)

Infos

[1] Eva-Katharina Kunst und Jürgen Quade, “Kern-Technik”, Folge 26: Linux-Magazin 02/06, S. 102

[2] Peter Kreußel, “Kernel übersetzen und installieren”: LinuxUser 6/06, S. 88

[3] Listings online: [https://www.linux-magazin.de/Service/Listings/2006/09/Kern-Technik]

[4] Zanussi et.al., “Relayfs: An Efficient Unified Approach for Transmitting Data from Kernel to User Space”: [http://www.research.ibm.com/K42/papers/ols03.ps]

[5] Relayfs-Dokumentation in den Kernelquellen: »/usr/src/linux-2.6.17/Documentation/filesystems/relayfs.txt«

[6] Statt Relay-Filesystem nur noch Relay-Subsystem: [http://kerneltrap.org/node/4593]

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. Ihr Buch “Linux-Treiber entwickeln” ist soeben in zweiter Auflage erschienen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
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