Debugging mit Abfragen

Im vorigen Abschnitt sind wir darauf eingegangen, wie printk arbeitet und eingesetzt werden kann. Über die Nachteile haben wir allerdings noch nicht gesprochen.

Das System kann durch die intensive Verwendung von printk deutlich verlangsamt werden, weil syslogd ständig seine Ausgabedateien synchronisiert und so jede Zeile zu einem Festplattenzugriff führt. Aus der Sicht von syslogd ist das auch die korrekte Implementation. Der Daemon versucht, alles sofort auf die Platte zu schreiben, um für den Fall vorzubeugen, daß das System unmittelbar nach der Ausgabe einer Meldung abstürzt. Andererseits wollen Sie Ihr System natürlich nicht bloß wegen der Debugging-Meldungen verlangsamen. Dieses Problem kann gelöst werden, indem Sie dem Namen der Protokolldatei in /etc/syslogd.conf ein Minuszeichen voranstellen, aber manchmal wollen Sie Ihre Konfigurationsdateien vielleicht nicht ändern.[1] Das Ändern der Konfiguration bringt das Problem mit sich, daß die Änderung vermutlich da verbleiben wird, nachdem Sie mit dem Debuggen fertig sind, obwohl während des normalen Systembetriebs Meldungen tatsächlich so schnell es geht auf die Festplatte geschrieben werden sollen. Alternativ zu so einer permanenten Änderung können Sie ein anderes Programm als klogd (wie etwa das bereits vorgeschlagene cat /proc/mksg) verwenden, was aber möglicherweise keine passende Umgebung für den normalen Systembetrieb ist.

Häufig besteht die beste Möglichkeit, an relevante Informationen heranzukommen, darin, das System nach diesen Informationen zu fragen, wenn Sie sie benötigen, anstatt ständig Daten zu produzieren. Jedes Unix-System stellt eine Reihe von Werkzeugen zur Verfügung, mit denen Systeminformationen abgefragt werden können: ps, netstat, vmstat usw.

Es gibt zwei Techniken für Treiber-Entwickler, mit denen Anfragen an das System gestellt werden können: das Erzeugen einer Datei im /proc-Dateisystem und das Verwenden der Treiber-Methode ioctl. Als Alternative zu /proc steht auch devfs zur Verfügung, aber /proc ist für die Abfrage von Informationen normalerweise einfacher zu verwenden.

Das /proc-Dateisystem verwenden

Das /proc-Dateisystem ist ein spezielles, von der Software erzeugtes Dateisystem, das vom Kernel dazu verwendet wird, Informationen unter die Leute zu bringen. Jede Datei in /proc gehört zu einer Kernel-Funktion, die den "Inhalt" der Datei bei Bedarf erzeugt, wenn die Datei gelesen wird. Einige dieser Dateien haben wir bereits in Aktion gesehen, etwa /proc/modules, die immer eine Liste der gerade geladenen Module zurückgibt.

/proc wird im Linux-System intensiv verwendet. Viele Hilfsprogramme in einer modernen Linux-Distribution wie ps, top und uptime holen sich ihre Informationen aus /proc. Manche Gerätetreiber stellen ebenfalls Informationen über /proc bereit, und auch Ihnen steht diese Möglichkeit offen. Das /proc-Dateisystem ist dynamisch; Ihr Modul kann also Einträge jederzeit nach Belieben hinzufügen und entfernen.

Vollständige /proc-Einträge können komplizierte Angelegenheiten sein; unter anderem kann man daraus sowohl lesen als auch dorthinein schreiben. Meistens sind /proc-Einträge aber Dateien, die nur gelesen werden können. In diesem Abschnitt beschäftigen wir uns mit dem letzteren, einfacheren Fall. Diejenigen, die etwas Komplizierteres implementieren wollen, können diesen Abschnitt als Grundlage verwenden und dann in den Kernel-Quellen weiterlesen.

Alle Module, die mit /proc arbeiten, sollten <linux/proc_fs.h> einbinden, um die passenden Funktionen zu definieren.

Um eine nur lesbare /proc-Datei zu erzeugen, muß Ihr Treiber eine Funktion implementieren, die die Daten erzeugt, wenn die Datei gelesen wird. Wenn ein Prozeß die Datei liest (normalerweise mit dem Systemaufruf read), dann erreicht diese Anfrage Ihr Modul über eine von zwei verschiedenen Schnittstellen, je nachdem, was Sie registriert haben. Wir sparen uns die Registrierung für später auf und springen direkt zur Beschreibung der Schnittstellen zum Lesen.

In beiden Fällen alloziert der Kernel eine Speicherseite (also PAGE_SIZE Bytes), in die der Treiber Daten schreiben kann, die er in den User-Space übergeben möchte.

Die empfohlene Schnittstelle ist read_proc, aber es gibt auch eine ältere namens get_info.

int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);

Der Zeiger page ist der Puffer, in den Sie Ihre Daten schreiben; start wird von der Funktion verwendet, um mitzuteilen, wo in page die interessanten Daten hingeschrieben worden sind (mehr dazu später); offset und count haben die gleiche Bedeutung wie in der Implementation von read. Das Argument eof verweist auf einen Integer-Wert, der vom Treiber gesetzt werden muß, um anzuzeigen, daß er keine weiteren Daten mehr zurückgeben möchte, während data ein treiberspezifischer Daten-Zeiger ist, den Sie für interne Verwaltungsaufgaben verwenden können.[2] Diese Funktion steht in der Version 2.4 des Kernels zur Verfügung, außerdem in 2.2, wenn Sie unsere Header-Datei sysdep.h verwenden.

int (*get_info)(char *page, char **start, off_t offset, int count);

get_info ist eine ältere Schnittstelle zum Lesen aus einer /proc-Datei. Die Argumente haben die gleiche Bedeutung wie bei read_proc. Was hier fehlt, ist der Zeiger, um das Dateiende zu melden, sowie die Objektorientierung, die der data-Zeiger mit sich bringt. Die Funktion steht in allen Kernel-Versionen, die wir in diesem Buch behandeln, zur Verfügung (hatte aber in der 2.0-Implementation ein zusätzliches, nicht verwendetes Argument).

Beide Funktionen sollten die Anzahl der Bytes, die sie in den page-Buffer geschrieben haben, zurückgeben, genau wie read das für andere Dateien macht. Andere Ausgabewerte sind *eof und *start. eof ist ein einfaches Flag, aber die Verwendung des start-Werts ist etwas komplizierter.

Das Hauptproblem mit der ursprünglichen Implementation der benutzerdefinierten Erweiterungen des /proc-Dateisystems bestand darin, daß eine einzige Speicherseite für die Datenübertragung verwendet wurde. Dies schränkte die Gesamtgröße auf eine Benutzerdatei von 4 KByte (oder was auch immer auf der jeweiligen Plattform galt) ein. Das start-Argument dient dazu, große Datendateien zu implementieren, kann aber ignoriert werden.

Wenn Ihre proc_read-Funktion den *start-Zeiger nicht zuweist (initial ist er NULL), dann nimmt der Kernel an, daß der offset-Parameter ignoriert worden ist und daß die Datenseite die gesamte Datei enthält, die Sie an den User-Space übergeben wollen. Wenn Sie dagegen eine größere Datei aus mehreren Stückchen zusammensetzen müssen, dann können Sie *start auf den gleichen Wert wie page setzen, so daß der Aufrufer weiß, daß Ihre neuen Daten am Anfang des Puffers stehen. Dann müssen Sie natürlich die ersten offset Datenbytes überspringen, die bereits in einem früheren Aufruf übergeben worden sind.

Es gibt seit langem ein weiteres größeres Problem mit /proc-Dateien, das start ebenfalls lösen soll. Manchmal ändert sich die ASCII-Repräsentation der Kernel-Datenstrukturen zwischen den read-Aufrufen, so daß der lesende Prozeß inkonsistente Daten vorfinden kann. Wenn *start auf einen kleinen Integer-Wert gesetzt wird, dann wird der Aufrufer diesen verwenden, um filp->f_pos unabhängig von der zurückgegebenen Datenmenge zu inkrementieren, und so f_pos zu einer internen Datensatznummer Ihrer read_proc- oder get_info-Funktion machen. Wenn Ihre read_proc-Funktion beispielsweise Informationen aus einem großen Array von Datenstrukturen zurückgibt und fünf dieser Strukturen im ersten Aufruf zurückgegeben wurden, dann könnte start auf 5 gesetzt werden. Der nächste Aufruf liefert den gleichen Wert als Offset und der Treiber weiß dann, daß er Daten ab der sechsten Struktur im Array zurückgeben muß. Die Autoren bezeichnen dies als “Hack”, in fs/proc/generic.c können Sie sich selbst eine Meinung bilden.

Es ist Zeit für ein Beispiel. Hier ist eine einfache Implementation von read_proc für das scull-Gerät:


int scull_read_procmem(char *buf, char **start, off_t offset,
                   int count, int *eof, void *data)
{
    int i, j, len = 0;
    int limit = count - 80; /* Nicht mehr als diesen Wert ausgeben */

    for (i = 0; i < scull_nr_devs && len <= limit; i++) {
        Scull_Dev *d = &scull_devices[i];
        if (down_interruptible(&d->sem))
                return -ERESTARTSYS;
        len += sprintf(buf+len,"\nDevice %i: qset %i, q %i, sz %li\n",
                       i, d->qset, d->quantum, d->size);
        for (; d && len <= limit; d = d->next) { /* die Liste durchsuchen */
            len += sprintf(buf+len, "  item at %p, qset at %p\n", d,
                                    d->data);
            if (d->data && !d->next) /* nur das letzte Element
                                                    ausgeben - Platz sparen */
                for (j = 0; j < d->qset; j++) {
                    if (d->data[j])
                        len += sprintf(buf+len,"    % 4i: %8p\n",
                                                    j,d->data[j]);
                }
        }
        up(&scull_devices[i].sem);
    }
    *eof = 1;
    return len;
}

Dies ist eine recht typische Implementation von read_proc, die davon ausgeht, daß es nie notwendig ist, mehr als eine Seite voll Daten auszugeben, und deswegen die Werte start und offset ignoriert. Die Implementation paßt allerdings für alle Fälle darauf auf, den Puffer nicht zu überschreiben.

Eine /proc-Funktion, die die get_info-Schnittstelle verwendet, würde der eben gezeigten Funktion sehr ähnlich sehen, aber die letzten beiden Argumente nicht haben. Das Ende der Datei wird in diesem Fall dadurch angezeigt, daß weniger Daten zurückgegeben werden, als der Aufrufer erwartet (also weniger als count).

Wenn Sie eine read_proc-Funktion definiert haben, dann müssen Sie diese mit einem Eintrag in der /proc-Hierarchie verbinden. Dazu gibt es je nach verwendetem Kernel zwei Möglichkeiten. Die einfachste, die nur im 2.4-Kernel (und in 2.2, wenn Sie unsere Header-Datei sysdep.h verwenden) zur Verfügung steht, besteht darin, einfach create_proc_read_entry aufzurufen. Hier ist dem von scull verwendete Aufruf, mit der die /proc-Funktion als /proc/scullmem verfügbar gemacht wird:


create_proc_read_entry("scullmem",
                       0    /* Default-Modus */,
                       NULL /* Elternverzeichnis */,
                       scull_read_procmem,
                       NULL /* Client-Daten */);

Die Argumente dieser Funktion sind (in der gezeigten Reihenfolge):

ark=bullet>

Der Zeiger für den Verzeichniseintrag kann dazu verwendet werden, eine ganze Verzeichnishierarchie unterhalb von /proc anzulegen. Beachten Sie aber, daß ein Eintrag einfacher in ein Unterverzeichnis von /proc gestellt werden kann, indem Sie den Verzeichnisnamen als Bestandteil des Namens angeben. Das setzt aber voraus, daß das Verzeichnis selbst existiert. Beispielsweise kristallisiert sich langsam eine Konvention heraus, nach der /proc-Einträge von Gerätetreibern in das Unterverzeichnis driver/ gehören; scull könnte seinen Eintrag einfach durch Angabe von driver/scullmem verlegen.

Einträge in /proc sollten natürlich beim Entladen des Moduls entfernt werden. remove_proc_entry ist die Funktion, die create_proc_read_entry wieder rückgängig macht:


 remove_proc_entry("scullmem", NULL /* Elternverzeichnis */);

Alternativ zum bisher gezeigten Verfahren kann man eine proc_dir_entry-Struktur erzeugen und initialisieren und an proc_register_dynamic (Version 2.0) oder proc_register übergeben (Version 2.2 nimmt an, daß es sich um eine dynamische Datei handelt, wenn die Inode-Nummer der Struktur 0 ist). Schauen Sie sich als Beispiel den folgenden Code an, den scull verwendet, wenn der Treiber mit 2.0-Headern kompiliert wird:


static int scull_get_info(char *buf, char **start, off_t offset,
                int len, int unused)
{
    int eof = 0;
    return scull_read_procmem (buf, start, offset, len, &eof, NULL);
}

struct proc_dir_entry scull_proc_entry = {
        namelen:    8,
        name:       "scullmem",
        mode:       S_IFREG | S_IRUGO,
        nlink:      1,
        get_info:   scull_get_info,
};

static void scull_create_proc()
{
    proc_register_dynamic(&proc_root, &scull_proc_entry);
}

static void scull_remove_proc()
{
    proc_unregister(&proc_root, scull_proc_entry.low_ino);
}

Der Code deklariert eine Funktion mit der get_info-Schnittstelle und füllt eine proc_dir_entry-Struktur aus, die beim Dateisystem registriert ist.

> > > > Dieser Code sorgt für die Kompatibilität zwischen 2.0- und 2.2-Kerneln, mit ein wenig Unterstützung durch die Makrodefinitionen in sysdep.h. Er verwendet die get_info-Schnittstelle, weil der 2.0-Kernel read_proc noch nicht unterstützte. Mit etwas mehr Einsatz von #ifdef-Arbeit könnte man auch read_proc unter Linux 2.2 verwenden, aber der Gewinn wäre nur gering.

Die Methode ioctl

ioctl (näher im nächsten Kapitel besprochen) ist ein Systemaufruf, der auf einem Dateideskriptor arbeitet. Dazu wird eine “Befehls”-Nummer und (optional) ein weiteres Argument, üblicherweise ein Zeiger, übergeben.

Als Alternative zur Verwendung des /proc-Dateisystems können Sie einige wenige ioctl-Befehle implementieren, die speziell auf das Debugging ausgerichtet sind. Diese Befehle können relevante Datenstrukturen vom Treiber in den User-Space kopieren, wo Sie sie näher untersuchen können.

Die Verwendung von ioctl auf diese Art und Weise ist etwas schwieriger als die Verwendung von /proc, weil Sie ein zusätzliches Programm benötigen, das ioctl aufruft und die Ergebnisse ausgibt. Auch dieses Programm muß geschrieben, kompiliert und mit dem Modul, das Sie gerade testen, synchronisiert werden. Auf der anderen Seite ist es einfacher, den Treiber-Code zu schreiben, als eine /proc-Datei zu implementieren.

Trotzdem gibt es Situationen, in denen ioctl die beste Möglichkeit ist, an Informationen heranzukommen, weil es schneller ist, als /proc auszulesen. Wenn Daten vor der Ausgabe bearbeitet werden müssen, kann es effizienter sein, die Daten in binärer Form anstatt als Textdatei zu bekommen. Außerdem erfordert ioctl auch nicht das Aufteilen der Daten in Fragmente, die auf eine Seite passen.

Ein weiterer interessanter Vorteil des ioctl-Ansatzes besteht darin, daß Befehle zur Informationsabfrage auch dann im Treiber verbleiben können, wenn das Debugging ansonsten abgeschaltet ist. Im Gegensatz zu einer /proc-Datei, die für jeden sichtbar ist, der in das Verzeichnis schaut (und viel zu viele Leute werden sich vermutlich wundern, “was das für eine merkwürdige Datei ist”), bleiben undokumentierte ioctl-Befehle höchstwahrscheinlich unentdeckt. Außerdem sind sie immer noch da, wenn etwas Merkwürdiges mit dem Treiber passiert. Der einzige Nachteil ist ein geringfügig größeres Modul.

Fußnoten

[1]

Das Minuszeichen ist eine “magische” Markierung, mit der syslogd davon abgehalten wird, die Datei bei jeder neuen Meldung auf die Festplatte herauszuschreiben. Dies ist in der Manual-Page syslog.conf(5) dokumentiert, die ohnehin eine sehr lesenswerte Lektüre ist.

[2]

Von diesen Zeigern werden wir noch mehrere in diesem Buch antreffen; sie repräsentieren das “Objekt”, um das es geht, und entsprechen in etwa dem this-Zeiger in C++.