7.1. Proc-Filesystem

Der Linux-Kernel bietet seinen Subsystemen – also auch den Treibern – eine elegante Möglichkeit, Zustandsinformationen zu publizieren bzw. Konfigurationsinformationen entgegenzunehmen: das Proc-Filesystem. Aus Anwendersicht sind in diesem Dateisystem eine Reihe von Dateien, teilweise in Verzeichnissen gruppiert, abgelegt. Da die Proc-Dateien ihre Daten im Regelfall ASCII-kodiert verarbeiten, können diese Dateien mit Programmen wie cat oder echo gelesen und geschrieben werden. Wird ein solches Procfile gelesen, erzeugt die zugehörige Kernelkomponente dynamisch – zum Zeitpunkt des Zugriffes – aus den internen Informationen die ASCII-kodierten Inhalte (siehe Abbildung Interne Informationen lesbar aufbereiten). Schreibt ein Anwender in eine Proc-Datei, werden die Daten zur Verarbeitung an eine Schreibfunktion des Treibers weitergereicht.

Abbildung 7-1. Interne Informationen lesbar aufbereiten

Das Proc-Filesystem ist ein virtuelles Dateisystem; eine Festplatte oder sonstiger Hintergrundspeicher werden daher nicht benötigt. Der Treiberentwickler findet die Dateien und Verzeichnisse normalerweise unter /proc gemountet.

Ein jeder Eintrag in dem Proc Filesystem – egal ob Datei oder Verzeichnis – ist über ein Element der Struktur struct proc_dir_entry repräsentiert. Eine solche Struktur wird erzeugt, wenn der Treiber den Auftrag erteilt, einen Eintrag anzulegen. Dazu stehen ihm zwei Funktionen zur Verfügung:

  1. proc_mkdir

  2. create_proc_entry

Die Funktion proc_mkdir erzeugt ein Verzeichnis, die Funktion create_proc_entry einen normalen Dateieintrag. Beide Funktionen geben den Zeiger auf die zugehörige struct proc_dir_entry zurück. Wenn es darum geht, das Verzeichnis festzulegen, in dem eine Proc-Datei oder ein weiteres Unterverzeichnis abgelegt werden soll, wird ein eben solcher Zeiger verwendet. Ein Wert von »NULL« repräsentiert stets das Root-Verzeichnis des Proc-Filesystems. Das folgende Codefragment erzeugt im Rootverzeichnis des Proc-Filesystems (/proc) das Unterverzeichnis ExampleDir und danach in ExampleDir den Ordner SampleDir.

    proc_dir = proc_mkdir( "ExampleDir", NULL );
    if( !proc_dir ) {
        ... // Fehlerbehandlung
    }
    sample_dir = proc_mkdir( "SampleDir", proc_dir );
    if( !sample_dir ) {
        ... // Fehlerbehandlung

Die Funktion create_proc_entry erzeugt einen Dateieintrag. Zusätzlich zum Namen der Proc-Datei lassen sich hierbei noch die Zugriffsrechte und das Verzeichnis angeben, in dem die Proc-Datei angelegt werden soll. Das Verzeichnis wird über das Element struct proc_dir_entry angegeben. Die Zugriffsmodi werden wie bei Unix gewohnt als 3-stellige Oktalzahl angegeben: Die erste Oktalziffer spezifiziert die Rechte des Besitzers, die zweite Stelle die Rechte der Gruppe und die dritte die Rechte aller übrigen. Gibt man hier für den Modus »0« an, werden für alle drei Benutzergruppen (Besitzer, Gruppe, Übrige) jeweils die Leserechte gesetzt. Sollen gegenüber dieser Default-Einstellung besondere Rechte vergeben werden, sollten die Definitionen aus der Header-Datei <linux/stat.h> verwendet werden. Tabelle Definitionen für Zugriffsrechte zeigt die wichtigsten Definitionen. Beispiel Proc-Dateien werden durch Aufruf von create_proc_entry angelegt zeigt das Codefragment zum Anlegen von Proc-Dateien.

Tabelle 7-1. Definitionen für Zugriffsrechte

DefineBedeutung
S_IRUSR Der Besitzer darf lesen.
S_IWUSR Der Besitzer darf schreiben.
S_IXUSR Der Besitzer darf ausführen.
S_IRGRP Die Gruppe darf lesen.
S_IWGRP Die Gruppe darf schreiben.
S_IXGRP Die Gruppe darf ausführen.
S_IROTH Alle übrigen dürfen lesen.
S_IWOTH Alle übrigen dürfen schreiben.
S_IXOTH Alle übrigen dürfen ausführen.
S_IRWXUGO Die Kombination von S_IRWXU|S_IRWXG|S_IRWXO
S_IALLUGO Die Kombination von S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO
S_IRUGO Die Kombination von S_IRUSR|S_IRGRP|S_IROTH
S_IWUGO Die Kombination von S_IWUSR|S_IWGRP|S_IWOTH
S_IXUGO Die Kombination von S_IXUSR|S_IXGRP|S_IXOTH

Beispiel 7-1. Proc-Dateien werden durch Aufruf von create_proc_entry angelegt

#include <linux/stat.h>                                    (1)

...
    procdirentry=create_proc_entry( "ProcExample1", 0, NULL );(2)
    procdirentry=create_proc_entry( "ProcExample2", 0, proc_dir );(3)
    procdirentry=create_proc_entry( "ProcExample3",        (4)
        S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, NULL );
(1)
Die Include-Datei ist nur notwendig, wenn spezifische Zugriffsrechte über die dafür vorgesehenen Definitionen gesetzt werden sollen. Dies ist beispielsweise unumgänglich, falls auf eine Proc-Datei auch ein schreibender Zugriff möglich sein soll.
(2)
Dieser Aufruf legt ein Procfile mit dem Namen ProcExample1 im Verzeichnis /proc an.
(3)
Dieser Aufruf legt ein Procfile mit dem Namen ProcExample2 in einem Unterverzeichnis an, welches durch proc_dir repräsentiert ist.
(4)
Dieser Aufruf legt ein Procfile mit dem Namen ProcExample3 im Verzeichnis /proc an. In diesem Fall werden aber Zugriffsrechte gesetzt. Der Besitzer (USR=user) des Procfiles darf sowohl lesend (S_IRUSR) als auch schreibend (S_IWUSR) zugreifen. Die Gruppe (GRP=group) darf nur lesend (S_IRGRP) und die übrigen (OTH=other) ebenfalls nur lesend (S_IROTH) zugreifen.

Da die Inhalte der einzelnen Proc-Dateien durch beispielsweise den Treiber erzeugt werden, wird jeder Lese- bzw. Schreibaufruf des Anwenders an die zuständige Komponente im Kernel – also an den Treiber – weitergereicht. Folglich muss der Treiber seinerseits eine Lese- und – falls unterstützt – auch eine Schreibfunktion zur Verfügung stellen. Die Lesefunktion wird hier mit proc_read und die Schreibfunktion mit proc_write bezeichnet.

Damit der Kernel proc_read und proc_write aufrufen kann, müssen die zugehörigen Adressen dieser Funktionen in die Struktur struct proc_dir_entry eingetragen werden. Darüber hinaus übergibt der Kernel den Funktionen beim Aufruf einen Parameter, den man ebenfalls in proc_dir_entry eintragen kann. Beispiel Initialisierung der Struktur proc_dir_entry verdeutlicht dies.

Beispiel 7-2. Initialisierung der Struktur proc_dir_entry

    procdirentry=create_proc_entry( "ProcExample", 0, NULL );
    if( procdirentry ) {
        procdirentry->read_proc  = proc_read;
        procdirentry->write_proc = proc_write;
        procdirentry->data = NULL;
        return 0;
    }

Wird – wie in den meisten Fällen üblich – nur eine Lesefunktion realisiert (ein Schreibzugriff ist dann nicht möglich), kann der Entwickler auch auf die Funktion create_proc_read_entry zurückgreifen. Diese Funktion bekommt den Namen, den Modus für die Zugriffsrechte, den Zeiger auf das Verzeichnis, in dem die Proc-Datei angelegt werden soll, die Adresse der Lesefunktion und den Zeiger auf mögliche, funktionsspezifische Daten übergeben.

    procdirentry=create_proc_read_entry( "ProcExample, S_IRUSR, NULL, proc_read, NULL );

Natürlich müssen die im Proc-Filesystem angelegten Einträge bei der Deinitialisierung des Moduls wieder aufgeräumt werden. Der Aufruf von remove_proc_entry reicht dazu aus. Hierbei ist wieder einmal die Reihenfolge einzuhalten: die Einträge, die als letzte erstellt worden sind, werden als erste aufgeräumt. Die Einträge, die als erste erstellt worden sind, kommen als letzte an die Reihe.

Abbildung 7-2. Einsparung von Code durch Verwendung von »#define«

Der Entwickler eingebetteter Systeme, der auf geringen Ressourcenverbrauch achtet, wird möglicherweise seinen gesamten Code, der das Proc-Filesystems behandelt, im Treiber mit einem #ifdef CONFIG_PROC_FS (bedingte Kompilierung) umgeben. Die Folge: Ist im Kernel die Unterstützung für das Proc-Filesystem nicht ausgewählt, wird der eigene Proc-Filesystemcode erst gar nicht erzeugt, was Ressourcen spart (siehe Abbildung Einsparung von Code durch Verwendung von »#define«). In anderen Situationen ist es jedoch nicht notwendig, auf bedingte Kompilierung zurückzugreifen: Die Funktionsprototypen in der Header-Datei <linux/proc_fs.h> stimmen den Code für den Treiberentwickler ab, je nachdem, ob die Konfigurationseinstellung einen Kernel mit oder aber ohne Proc-Filesystem darstellt. In letzterem Fall wird create_proc_entry mit »NULL« ersetzt. Die nachfolgende Bedingung ist damit nicht erfüllt, die Zuweisung (entry->read_proc=...) wird nie ausgeführt.

7.1.1. Der lesende Zugriff auf die Proc-Datei

Abbildung 7-3. Aufruf von proc_read aus der Kernelfunktion proc_file_read heraus

Um einer Applikation Daten ASCII-kodiert zur Verfügung stellen zu können, ist von den in Abbildung Aufruf von proc_read aus der Kernelfunktion proc_file_read heraus dargestellten Schritten nur die Lesefunktion proc_read zur Verfügung zu stellen.

Wesentliche Aufgabe dieser Lesefunktion ist es, treiberinterne Informationen für die Anwendung lesbar aufzubereiten. Anders als bei der verwandten Funktion driver_read werden die Daten nicht direkt in den User-Space kopiert, sondern vielmehr in einen im Kernel-Space liegenden Speicherbereich (Speicherseite). Die Adresse dieses Speicherbereiches wird der Funktion mitsamt der Längenangabe übergeben. Den eigentlichen Datentransfer der Daten aus dieser Speicherseite hin zur Applikation übernimmt der Kernel selbst. Zur einfachen Formatierung (ASCII-Kodierung) stellt der Kern zudem die aus der Applikationsprogrammierung bekannten Hilfsfunktionen snprintf und strncat bereit.

Neben diesen beiden Funktionen gibt es zwar weitere Funktionen – insbesondere die einfacheren Varianten sprintf und strcat –, diese sollten jedoch weder in Applikationen und erst recht nicht im Kernel verwendet werden. Schließlich machen sie es leicht möglich, Speicherbereiche zu überschreiben; aus sicherheitstechnischer Sicht ein K.-o.-Kriterium. Dies ist auch der Grund, warum für das Kopieren eines Strings nicht die Funktion strcpy, sondern die Funktion strlcpy verwendet werden sollte. Denn hierbei wird der String nicht nur bis zu einer maximalen Länge hin kopiert, sondern darüber hinaus sichergestellt, dass der String wirklich mit einer »0« abgeschlossen wird.

Der Prototyp der Lesefunktion

    int proc_read(char *buffer, char **start, off_t offset, int count, int *peof, void *dat)
zeigt, dass neben der Adresse der Speicherseite (»buffer«), der zugehörigen Längenangabe (»count«), einem möglichen Offset (»offset«) und einer Variablen für funktionsinterne Daten (»dat«) vom Kernel zwei weitere Parameter übergeben werden (»peof« und »start«), die zur Aufnahme von Rückgabewerten dienen. Über »peof« teilt der Treiber der aufrufenden Funktion mit, dass sämtliche Daten ausgegeben wurden. »Start« teilt mit, wo sich die Daten im Speicher befinden oder aber auch wie der Offset der Proc-Datei aufgrund der übergebenen Daten berechnet wird.

Es lassen sich zwei Fälle unterscheiden:

Sämtliche Daten passen in die Speicherseite

Im einfachsten Fall liefert die Proc-Datei so wenig Daten, dass diese ASCII-kodiert problemlos in einer Speicherseite Platz haben (zum Beispiel 3072 Bytes).

In diesem Fall wird die Lesefunktion folgende Aktionen durchführen:

  1. Sie kopiert sämtliche Daten in den Buffer, also in die Speicherseite.

  2. Zum Zeichen, dass alle Daten übertragen wurden, setzt sie den Parameter »peof« auf »1«.

  3. Sie gibt die Anzahl der in der Speicherseite befindlichen Daten zurück.

Beispiel 7-3. Programmierung einer Proc-Datei

#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");

static struct proc_dir_entry *proc_dir, *proc_file;

static int proc_read( char *buf, char **start, off_t offset,
                int size, int *eof, void *data)
{
    int bytes_written=0, ret;

    ret=snprintf(buf+bytes_written,(size-bytes_written),"proc_read wurde\n");
    bytes_written += (ret>(size-bytes_written)) ? (size-bytes_written):ret;
    ret=snprintf(buf+bytes_written, size-bytes_written, "aufgerufen.\n");
    bytes_written += (ret>(size-bytes_written)) ? (size-bytes_written):ret;

    *eof = 1;
    return bytes_written;
}

static int __init proc_init(void)
{
    proc_dir = proc_mkdir( "ExampleDir", NULL );
    proc_file=create_proc_entry( "ProcExample", S_IRUGO, proc_dir );
    if( proc_file ) {
        proc_file->read_proc = proc_read;
        proc_file->data = NULL;
    }
    return 0;
}

static void __exit proc_exit(void)
{
    if( proc_file ) remove_proc_entry( "ProcExample", proc_dir );
    if( proc_dir )  remove_proc_entry( "ExampleDir", NULL );
}

module_init( proc_init );
module_exit( proc_exit );

Der Rückgabe-Parameter »start« wird von proc_read nicht geändert, sondern bleibt auf dem Initialwert »NULL«. Für die aufrufende Funktion proc_file_read ist das das Zeichen dafür, dass die Daten am Anfang des Speicherbereiches zu finden sind. Falls die Applikation die Daten mit einem Offset lesen wollte, sorgt der Kernel selbst dafür, dass die Applikation die Daten ab der angeforderten Position erhält. Ebenso wie die Variable start unangetastet blieb, muss daher auch offset nicht ausgewertet werden. Eine Beispiel-Implementierung dieser Variante findet sich in Programmierung einer Proc-Datei.

Um zum einen die Anzahl der geschriebenen Bytes zu berechnen und zum anderen die Funktion snprintf richtig zu parametrieren, muss deren Rückgabewert ausgewertet werden. Die Funktion snprintf gibt die Anzahl der Zeichen zurück, die geschrieben werden sollten. Um die Anzahl der tatsächlich geschriebenen Zeichen zu bestimmen, muss zunächst überprüft werden, ob mehr Bytes auszugeben waren, als Speicher zur Verfügung stand. Falls nicht, entspricht der Rückgabewert von snprintf den tatsächlich geschriebenen Zeichen. Falls doch, ist die Größe des zur Verfügung gestellten Speicherbereiches die gesuchte Zahl.

Die Zahl der tatsächlich geschriebenen Zeichen wird nun genutzt, um damit für die nächste Ausgabe die Startadresse innerhalb des Buffers zu berechnen (buf+bytes_written) und die verbleibende Buffergröße zu bestimmen (size-bytes_written). Anstelle von snprintf kann ab Kernel 2.6.4 auch die Funktion scnprintf verwendet werden. Diese gibt direkt die Anzahl der tatsächlich geschriebenen Zeichen zurück, womit sich die Überprüfung, ob mehr Zeichen geschrieben werden sollten, als Speicher vorhanden ist, erübrigt.

Die Ausgabe umfangreicher Daten über eine Proc-Datei

Wenn der von proc_file_read reservierte und übergebene Speicherplatz »buf« nicht ausreicht, um dort alle internen Daten kodiert abzulegen, wird proc_read mehrfach aufgerufen. Anhand des Offsets kann proc_read entscheiden, welche internen Daten als Nächstes auszugeben sind. Grundregel 1 bei der Ausgabe besagt, einen einzelnen Datensatz immer vollständig auszugeben. Reicht der verfügbare Platz im Puffer »buf« nicht mehr aus, um einen Datensatz komplett abzulegen, wird der Datensatz erst beim nächsten Aufruf von proc_read in ASCII umgewandelt.

Allerdings lässt sich aufgrund des Offsets nur sehr schwer das als Nächstes zu kodierende Datum bestimmen. Das liegt daran, dass es oftmals keine 1:1-Abbildung vom Offset der kodierten Form auf die ursprünglichen Daten gibt. Schließlich können sich die Daten zwischen zwei Aufrufen von proc_read ändern. Eine Variable, die eben noch den Wert »0« hatte und damit bei der Ausgabe in einem Byte kodiert wurde, trägt beim zweiten Aufruf bereits den Wert »1000« und belegt bei der ASCII-Ausgabe nun 4 Byte.

Abbildung 7-4. Berechnung des neuen Offsets

Falls die Bestimmung der internen Position aufgrund des Offsets schwierig ist, kann der Programmierer einen Trick anwenden. Normalerweise berechnet sich der Offset auf Basis der kopierten Zeichen, also aufgrund des Rückgabewertes der Funktion proc_read. Befindet sich aber in der Speicherstelle, auf die »*start« zeigt, ein Wert kleiner als »buf« (siehe Aufrufparameter der Funktion proc_read), aber größer als »NULL«, wird dieser Wert anstelle des Returnwertes auf den bisherhigen Offset aufaddiert. Abbildung Berechnung des neuen Offsets verdeutlicht das. Diese Funktionalität lässt sich nutzen, um eben nicht die Bytes im Ausgabestrom zu zählen, sondern die bereits ausgegebenen Datensätze oder Feldelemente.

Insbesondere, wenn der Offset im Ausgabestrom verwendet wird, um den aktuell auszugebenen Datensatz zu bestimmen, müssen typischerweise alle vorher ausgegebenen Daten bis zum besagten Offset nochmals kodiert werden. Allerdings werden die so kodierten Daten gleich wieder verworfen. Wenn die Position bestimmt ist, wird der neue Datensatz aufbereitet und der aufrufenden Funktion proc_file_read zurückgegeben.

Wird der Offset zur Bestimmung des nächsten Datensatzes bestimmt, muss proc_read folgende Operationen durchführen:

  1. Es kopiert sämtliche Daten ab dem Offset »offset« an eine beliebige Stelle des übergebenen Buffers; im Regelfall werden die Daten am Anfang des Buffers abgelegt.

  2. Der Parameter »start« wird auf die Adresse der ersten Daten gesetzt.

  3. »peof« bleibt unverändert auf »0«. Erst wenn beim Aufruf das letzte Datum kopiert worden ist, wird hier eine »1« abgelegt.

  4. Die Funktion gibt die Anzahl der in der Speicherseite abgelegten Bytes zurück.

Falls jedoch die Anzahl der Datensätze gezählt werden soll, ergeben sich die folgenden Schritte:

  1. proc_read berechnet den nächsten, auszugebenden Datensatz. Handelt es sich bei den zu kodierenden Daten um ein Feld, kann »offset« direkt als Feldindex verwendet werden. Sollen die Elemente einer Liste ausgegeben werden, werden »offset«-Listenelemente verworfen.

  2. Es werden so viele vollständige Datensätze im Ausgabepuffer »buf« abgelegt, wie reinpassen.

  3. Der Parameter »start« wird mit der Anzahl der abgelegten Datensätze belegt.

  4. »peof« bleibt unverändert auf »0«.

  5. Die Funktion gibt die Anzahl der in der Speicherseite abgelegten Bytes zurück.

Ansonsten gilt auch hier: Sind alle Daten übergeben worden, wird »peof« auf »1« gesetzt und »0« zurückgegeben. Eine Beispielimplementierung, bei der über den Offset ein Feldindex verwaltet wird, findet sich in Ausgabe von umfangreichen Daten in einer Proc-Datei.

Beispiel 7-4. Ausgabe von umfangreichen Daten in einer Proc-Datei

static int Beispieldaten[ANZAHL_ELEMENTE];
...
static int proc_read( char *buf, char **start, off_t offset,
    int size, int *peof, void *data)
{
    int count;

    if( offset >= ANZAHL_ELEMENTE ) {
        *peof = 1;
        return 0;
    }
    count = snprintf( buf, size, "Beispieldaten %ld: %d\n",
        offset, Beispieldaten[offset] );
    *start = 1; // Ein weiteres Element verarbeitet.
    return (count>size)? size : count;
}

7.1.2. Schreibzugriffe unterstützen

Mit dem Proc-Filesystem lassen sich intuitiv Konfigurationsinformationen wie auch Managementaufträge an Kernelkomponenten weiterreichen. So nutzt beispielsweise das SCSI-Subsystem das Proc-Filesystem, um durch den schreibenden Zugriff Aktionen, wie das Scannen des Busses nach neuen Geräten, anzustoßen.

Soll auf eine Proc-Datei auch geschrieben werden können, muss eine Schreibfunktion (hier mit proc_write bezeichnet) zur Verfügung gestellt werden. Diese Schreibfunktion bekommt vier Parameter übergeben:

  1. die zugreifende (Rechenprozess-)Instanz (Element vom Typ struct file),

  2. die Adresse des Speicherbereichs, an der die zu schreibenden Daten zu finden sind,

  3. die Anzahl der zu schreibenden Bytes und

  4. ein privates Datum, das bei der Einrichtung der Proc-Datei spezifiziert werden konnte und vom Kernel an die Funktion durchgereicht wird.

int proc_write(struct file *file, const char __user *buffer, unsigned long count, void *data);

Die zu schreibenden Daten werden innerhalb der Funktion mit Hilfe von copy_from_user vom Userspace in den Kernel-Space kopiert. Nach ihrer Auswertung werden die entsprechenden Aktionen eingeleitet.

Zur Auswertung (Dekodierung) der im Regelfall ebenfalls ASCII-kodierten Daten stehen dem Programmierer die Funktionen strncmp und strstr zur Verfügung. Beispiel Schreibender Zugriff auf eine Proc-Datei zeigt die Implementierung einer Funktion proc_write.

Beispiel 7-5. Schreibender Zugriff auf eine Proc-Datei

static int proc_write( struct file *instanz, const char __user *userbuffer,
        unsigned long count, void *data )
{
    char *kernel_buffer;
    int not_copied;

    kernel_buffer = kmalloc( count, GFP_KERNEL );
    if( !kernel_buffer ) {
        return -ENOMEM;
    }
    not_copied = copy_from_user( kernel_buffer, userbuffer, count );
    if( strncmp( "Konfiguration", kernel_buffer, 14-1 )==0 )
        configured = 1;
    kfree( kernel_buffer );
    return count-not_copied;
}

7.1.3. Sequence-Files

Virtuelle Dateien, die Informationen lesbar, also ASCII-kodiert ausgeben, lassen sich auch einfach als so genannte Sequence-Files implementieren (siehe auch Abschnitt Der lesende Zugriff auf die Proc-Datei). Virtuelle Dateien haben meistens das Problem, dass von der Position innerhalb der kodierten Ausgabedaten nicht auf das zugehörige interne Datum geschlossen werden kann. Das ist aber notwendig, wenn umfangreiche Informationen über die virtuelle Datei ausgegeben werden sollen. In einem solchen Fall wird die Lesefunktion im Treiber (zum Beispiel driver_read oder proc_read) nämlich mehrfach aufgerufen und das einzige Indiz darüber, welche Daten als Nächstes ausgegeben werden müssen, ist die Position – der Offset – im Ausgabedatenstrom.

Sequence-Files übernehmen die Aufgabe, für die Aufbereitung der Informationen Speicher zu reservieren, die Daten in diesen Speicher ablegen zu lassen und schließlich der anfragenden Applikation zu übergeben. Außerdem steuern sie die Auswahl der auszugebenden Daten, indem sie den Treiber auffordern, zunächst den ersten Datensatz und anschließend immer einen Datensatz nach dem anderen aufzubereiten.

Der Treiberprogrammierer muss im Wesentlichen nur noch eine Funktion zur Kodierung der Daten sowie einen Satz von Funktionen schreiben, über den die Auswahl des auzugebenden Datensatzes stattfindet.

Ein solcher Datensatz wird aus Sicht des Sequence-Files über einen Zeiger (void *object_ident) referenziert. Was sich jedoch letztlich hinter der Speicherstelle befindet – ob ein Zeiger oder ein Index – entscheidet der Treiberprogrammierer. Das Sequence-File ruft eine Funktion im Treiber (hier sf_show genannt) auf und übergibt ihr diesen Zeiger.

sf_show selbst nutzt zur Kodierung der Daten einige vom Sequence-File zur Verfügung gestellte Funktionen, nämlich seq_printf, seq_putc oder auch seq_puts. In der Regel gibt sf_show »0« zurück, nur im Fehlerfall einen davon abweichenden Wert.

Zur Auswahl des Datensatzes muss der Treiber insgesamt 3 Funktionen implementieren, die hier iterator_start, iterator_next und iterator_stop genannt werden.

void *iterator_start( struct seq_file *m, loff_t *index );
void *iterator_next( struct seq_file *m, void *object_ident, loff_t *index );
void *iterator_stop( struct seq_file *m, void *object_ident );

Jedes Mal, wenn die Applikation einen Leseaufruf startet (Systemcall read), wird zunächst iterator_start aufgerufen. Der Parameter »index« gibt die Anzahl der bisher ausgegebenen Datensätze an. Mit Hilfe von »index« bestimmt die Funktion den nächsten, auszugebenden Datensatz (object_ident), ihren Rückgabewert. Sind die auszugebenden Datensätze beispielsweise in einer Liste organisiert, wird iterator_start »index« Listenelemente verwerfen und das dann folgende Listenelement zurückgeben.

iterator_next wird vom Sequence-File aufgerufen, um den nächsten Datensatz auszuwählen (weiter zu schalten). Hierzu können sowohl der letzte Datensatz (object_ident) als auch der »index« verwendet werden. Ist kein Element mehr vorhanden, gibt die Funktion »NULL« zurück. Während bei Aufruf von iterator_start der bisherige »index« automatisch inkrementiert wird, muss iterator_next das selbst erledigen.

Dass keine weiteren Daten mehr auszugeben sind, erkennt das Sequence-File daran, dass sowohl iterator_next als auch iterator_start »NULL« zurückgeben.

iterator_stop wird aufgerufen, bevor die bisher aufbereiteten Daten per copy_to_user der Applikation übergeben werden. In dieser Funktion werden die Initialisierungen, die in iterator_start vorgenommen wurden, wieder rückgängig gemacht.

Die vorgestellten Funktionen werden dem Kernel durch Aufruf der Funktion seq_open bekannt gegeben. Dazu werden sie zunächst in eine Struktur vom Typ struct seq_operations eingetragen:

static struct seq_operations sops = {
    .start = iterator_start;
    .next  = iterator_next;
    .stop  = iterator_stop;
    .show  = sf_show;
};

Der Aufruf von seq_open erfolgt am günstigsten aus der Open-Funktion des Treibers (driver_open oder auch proc_open):

static int driver_open( struct inode *geraetedatei, struct file *instanz )
{
    return seq_open( instanz, &sops );
}

Da das Lesen der virtuellen Datei jetzt durch das Sequence-File kontrolliert werden soll, muss in den »file_operations« noch die interne Lesefunktion durch die Lesefunktion des Sequence-Files (seq_read) ersetzt werden. Entsprechend steht für die Seek-Funktion seq_lseek und für die Funktion zum Schließen seq_release:

static struct file_operations fops = {
    .open    = driver_open,
    .read    = seq_read,
    .llseek  = seq_lseek;
    .release = seq_release;
};

Greift jetzt eine Applikation auf die Datei zu – zum Beispiel auf eine Proc-Datei – werden jedes Mal (ausgenommen bei driver_open) zunächst die Funktionen des SF-Subsystems aufgerufen.

Abbildung 7-5. Komponenten eines Sequence-Files

Abbildung Komponenten eines Sequence-Files zeigt die Variablen und Funktionen eines Sequence-Files. Insbesondere sind vereinfacht die Abläufe dargestellt, wenn eine Applikation lesend auf das Sequence-File zugreift. Die unten im Bereich »Modul« dargestellten Komponenten müssen vom Treiberprogrammierer erstellt werden.

Beispiel Verwendung des Sequence-File-Interface zeigt die Ausgabe der zum aktuellen Rechenprozess gehörigen Elternprozesse über ein Procfile, das wiederum zur Ausgabe ein Sequence-File verwendet.

Beispiel 7-6. Verwendung des Sequence-File-Interface

#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/init.h>
#include <linux/seq_file.h>

MODULE_LICENSE("GPL");

static void *iterator_start(struct seq_file *m, loff_t *index)
{
    int i;
    struct task_struct *tptr = current;

    for( i=(int)*index; i && tptr!=tptr->parent; i-- )     (1)
        tptr = tptr->parent;
    if( tptr==tptr->parent )
        return NULL;                                       (2)
    return (void *)tptr;
}

static void iterator_stop(struct seq_file *m, void *obj_ident)
{
    return;
}

static void *iterator_next(struct seq_file *m, void *obj_ident, loff_t *index)
{
    struct task_struct *tptr = (struct task_struct *)obj_ident;

    if( tptr == tptr->parent ) {
        return NULL;                                       (3)
    }
    (*index)++;
    return (void *)tptr->parent;
}

static int sf_show(struct seq_file *m, void *obj_ident)
{
    struct task_struct *tptr = (struct task_struct *)obj_ident;

    seq_printf(m, "Prozess PID: %d\n", tptr->pid );        (4)
    seq_printf(m, "     Eltern: %p\n", tptr->parent );
    return 0;
}

static struct seq_operations sops = {
    .start = iterator_start,
    .next  = iterator_next,
    .stop  = iterator_stop,
    .show  = sf_show,
};

static int proc_open( struct inode *geraete_datei, struct file *instanz )
{
    return seq_open( instanz, &sops );
}

static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = proc_open,
    .read    = seq_read,
    .llseek  = seq_lseek,
    .release = seq_release,
};

static int __init proc_init(void)
{
    static struct proc_dir_entry *procdirentry;

    procdirentry=create_proc_entry( "SequenceFileTest", 0, NULL );
    if( procdirentry ) {
        procdirentry->proc_fops = &fops;
    }
    return 0;
}

static void __exit proc_exit(void)
{
    remove_proc_entry( "SequenceFileTest", NULL );
}

module_init( proc_init );
module_exit( proc_exit );

	
(1)
Der Parameter »index« ist ein 64-Bit-Wert, der gecastet wird, damit der Compiler keine Warnung herausgibt.
(2)
In diesem Beispiel ist der letzte Datensatz augegeben worden: Der Zeiger auf »parent« ist ein Zeiger auf sich selbst. In diesem Fall muss »NULL« zurückgegeben werden.
(3)
Auch diese Funktion muss »NULL« zurückgeben, falls die letzte Datenstruktur ausgegeben wurde.
(4)
Die Ausgabe selbst muss über diese Funktionen erfolgen.

Gibt es nur genau einen Datensatz, stellt ein »Single-File« eine Alternative dar. Der Kernel übernimmt das Handling, insbesondere auch das Positionieren (Seek) innerhalb der Daten. Der Entwickler hat letztlich nur eine Show-Funktion zur Verfügung zu stellen.

Um die Ausgabe auf Basis eines Single-Files zu realisieren, ist Folgendes zu tun:

  1. Innerhalb der Open-Funktion des Moduls (zum Beispiel driver_open oder auch proc_open) wird die Funktion single_open aufgerufen.

  2. Die Funktion single_open bekommt neben dem Zeiger auf die Instanz (struct file) noch die Adresse der Funktion sf_show übergeben.

  3. Da single_open dynamisch Speicher alloziert, muss ebenfalls die Funktion DriverRelease mit einer Funktion namens single_release substituiert werden.

  4. Die Lese- und die Seek-Funktionen werden wieder mit seq_read und seq_lseek initialisiert.

  5. Die Funktion sf_show ist zu implementieren.

Die Initialisierung der struct file_operations sieht damit wie folgt aus:

static struct file_operations fops = {
    .open    = single_open,
    .release = single_release,
    .read    = seq_read,
    .llseek  = seq_lseek,
};

Die Funktion single_open bekommt als ersten Parameter die Treiberinstanz (wie bei seq_open) und die Adresse der Showfunktion (sf_show) übergeben. Als dritter Parameter kann zusätzlich ein Zeiger auf private Daten übergeben werden.

Die Aufrufparameter der Funktion single_release stellen – wie bei release üblich – einen Zeiger auf die die Gerätedatei identifizierende Datenstruktur struct inode und einen Zeiger auf die Treiberinstanz (struct file).

Beispiel Single-File zeigt exemplarisch die Implementierung eines Single-Files.

Beispiel 7-7. Single-File

#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/init.h>
#include <linux/seq_file.h>

MODULE_LICENSE("GPL");

static int sf_show(struct seq_file *m, void *v)
{
    struct task_struct *tptr = current;

    seq_printf(m, "Prozess PID: %d\n", tptr->pid );
    seq_printf(m, "     Eltern: %p\n", tptr->parent );
    return 0;
}

static int call_seq_open( struct inode *geraete_datei, struct file *instanz )
{
    return single_open( instanz, sf_show, NULL );
}

static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = call_seq_open,
    .read    = seq_read,
    .llseek  = seq_lseek,
    .release = single_release,
};

static int __init proc_init(void)
{
    static struct proc_dir_entry *procdirentry;

    procdirentry=create_proc_entry( "SequenceFileTest", 0, NULL );
    if( procdirentry ) {
        procdirentry->proc_fops = &fops;
    }
    return 0;
}

static void __exit proc_exit(void)
{
    remove_proc_entry( "SequenceFileTest", NULL );
}

module_init( proc_init );
module_exit( proc_exit );


Lizenz