Projekte.PCI-Tutorial-BTKernel (Struktur)


Ein einfacher Kerneltreiber für den Entwurf "Board-Test"

Übersicht

In diesem Abschnitt wird die Entwicklung eines einfachen Treibers für den Beispiel-Entwurf "Board-Test" beschrieben. Die drei Register sollen mittels I/O-Controls gelesen und - soweit möglich - auch geschrieben werden.
Der Treiber basiert auf den PCI-Funktionen des Linux-Kernels 2.4. Zugunsten der Übersichtlichkeit und Einfachheit wird vorausgesetzt, dass nur ein PCI-Entwicklungsboard im System vorhanden ist, d.h. es gibt nur eine Karte, auf die das im Treiber festgelegte Paar (Vendor ID, Device ID) passt.

Die Struktur des Gerätetreibers

Die Abbildung illustriert schematisch das Zusammenspiel zwischen Kernelmodul, Anwendungsprogramm und Gerätedatei.


Abb.: Zusammenhang zwischen Hardwaretreiber und Anwendungsprogramm
(Quelle: Linux-Magazin 10/1999)

Beim Laden bzw. Einbinden des Moduls in den laufenden Kernel wird die Funktion init_module() ausgeführt - dieses Code-Fragment soll die Zielhardware identifizieren und entsprechende Ressourcen (z.B. den von der Karte belegten Speicheradressraum, I/O-Ports oder die Gerätedatei) beim Kernel anmelden. Analog dazu wird die Funktion cleanup_module() beim Entfernen des Moduls aus dem System aufgerufen.
Wird in einem Anwendungsprogramm eine der Dateifunktionen wie open(), close(), read(), write(), ... im Zusammenhang mit der Gerätedatei /dev/mydevice aufgerufen, so hat dies eine Aktivierung der entsprechenden Funktionen im Kernelmodul zur Folge, sofern die gewüschte Funktion dort überhaupt definiert ist. (Bei der Abstraktion einer Hardware-Komponente als Datei /dev/mydevice lassen sich oft nicht alle Dateifunktionen sinnvoll auf das Gerät übertragen, da ein Stück Hardware eben doch etwas anderes ist als eine Datei.) Wird keine eigene Funktion angegeben, hat der Kernel ein eingebautes Default-Verhalten, wenn ein Programm versucht, die Funktion auf die Gerätedatei anzuwenden.
Die eigenen Routinen werden in einer Struktur vom Typ file_operations eingetragen. Sie ist im wesentlichen ein Feld von Zeigern auf die Funktionen. Die genaue Definition der Struktur file_operations sowie alle theoretisch anwendbaren Dateioperationen der Kernelversion 2.4 befinden sich in der oben angegebenen Literatur.
Die Registrierung der eigenen Funktionen für das Device passiert in der Funktion init_module() durch den Aufruf der Funktion register_chrdev(). Neben einem Zeiger auf die Dateioperationen-Struktur werden noch ein Name für den Treiber sowie die sogenannte Major-Nummer der Gerätedatei übergeben. In dem abgebildeten Beispiel handelt es sich um eine zeichenorientierte Gerätedatei mit Major-Nummer MY_MAJOR und Minor-Nummer 0. Weitere Gerätetypen sind u.a. Block- und Netzwerkgeräte. Die Major-Nummer bezeichnet eine Gerätefamilie, während die Minor-Nummer einem bestimmten Gerät innerhalb der Familie entspricht. Gültige Werte und die zugeordneten Geräte werden in der Datei /usr/src/linux/Documentation/devices.txt eingehend beschrieben. Die Major-Nummer 240 ist für experimentelle Zwecke vorgesehen und kann hier deshalb gefahrlos verwendet werden.

Die verwendeten Kernelfunktionen

Hier steht natürlich nur ein sehr kleiner Teil der für die Treiberprogrammierung nötigen Kernelfunktionen. Die beschriebenen Funktionen sollen lediglich den Beispieltreiber für das PCI-Entwicklungsboard leichter verständlich machen.
int register_chrdev(unsigned int major, const char* name,
                    struct file_operations *fops);
Zeichengerätetreiber beim Kernel registrieren. major muss der Nummer der im Verzeichnis /dev zugeordneten Gerätedatei entsprechen. name ist eine den Treiber bezeichnende nullterminierte Zeichenkette und fops ein Zeiger auf die Tabelle mit den Funktionszeigern für die definierten Dateioperationen. Ist der Rückgabewert ungleich 0, so liegt ein Fehler vor.
int unregister_chrdev(unsigned int major, const char* name);
Konträr zu register_chrdev() wird mit dieser Funktion die Bindung der Major-Nummer zum Treiber gelöst. major und die Zeichenkette fops müssen dieselben Werte wie bei der Registrierung aufweisen. Bei der Programmierung sollte sichergestellt werden, dass diese Funktion auch dann noch aufgerufen wird, wenn der Treiber aufgrund einer Fehlerbedingung abbricht. Die Bindung bleibt sonst bestehen und blockiert ein erneutes Laden des Treibers.
void request_mem_region(unsigned long start, unsigned long len, const char *name);
Belegen eines I/O-Speicherbereichs im Gesamtadressraum (Memory Mapped I/O). start bezeichnet die Startadresse des gewünschten Speicherbereichs und len dessen Größe. Die angegebene Zeichenkette name kann nach der Belegung zusammen mit den I/O-Anfangs- und Endadressen mit cat /proc/iomem/ abgefragt werden.
void release_mem_region(unsigned long start, unsigned long len);
Freigabe von I/O-Speicher. start und len haben dieselbe Bedeutung wie bei request_mem_region.
unsigned long copy_to_user(void *to, const void *from, unsigned long count);
Umlagern von Daten aus dem Kernelspace in den Userspace. Das Kernelmodul und das darauf zugreifende Anwendungsprogramm befinden sich in logisch getrennten Adressräumen (Speicherschutzmechanismus). Dabei ist from ein Zeiger auf die zu kopierende Datenstruktur im Kernelspace und to ein Zeiger auf das Ziel im Userspace. count gibt die Anzahl der zu kopierenden Bytes an.
unsigned long copy_from_user(void *to, const void *from, unsigned long count)
Umlagern von count Daten aus dem Userspace mit Quelladresse from in den Kernelspace mit Zieladdresse to.
put_user(datum, ptr)
Dieses Makro schreibt ein elementares Datum an die Stelle prt im Userspace; es relativ schnell und sollte anstelle von copy_to_user verwendet werden, wenn nur einzelne Werte übertragen werden. Da bei der Expansion von Makros keine Typenüberprüfung stattfindet, können beliebige Zeigertypen an put_user übergeben werden, die Adressen im Userspace enthalten sollten. Die Größe der übertragenen Daten hängt vom Typ des Argumentes ptr ab und wird während des Übersetzens mit einer speziellen gcc-Pseudofunktion bestimmt.
put_user versucht sicherzustellen, dass der Prozess an die angegebene Speicheradresse schreiben darf. Im Erfolgsfall wird 0, ansonsten -EFAULT zurückgegeben.
get_user(local, ptr)
Dieses Makro wird dazu verwendet, ein einziges Datum von der Stelle prt aus dem Userspace zu holen. Es verhält sich genauso wie put_user, überträgt aber die Daten in die entgegengesetzte Richtung. Der abgeholte Wert wird in der lokalen Variablen local gespeichert; der Rückgabewert gibt an, ob die Operation erfolgreich war oder nicht.
int pci_present();
Prüfen, ob PCI-Funktionalität überhaupt im System besteht. Die Funktion liefert true, wenn PCI-Geräte vorhanden sind.
struct pci_dev *pci_find_device(unsigned int vendor, unsigned int device,
      const struct pci_dev *from);
Diese Funktion durchsucht die Liste der installierten PCI-Geräte nach einem Gerät, das auf die gegebene Signatur (vendor, device) paßt. Der Parameter from wird gebraucht, um auch alle Geräte mit identischer Hersteller- und Gerätenummer finden zu können. Der Zeiger from muss dann auf das letzte gefundene Gerät zeigen. Ein erneuter Suchaufruf setzt dann an dieser Stelle in der Liste fort und beginnt nicht wieder am Anfang. Für das erste zu findende Gerät ist from auf NULL zu setzen. Wenn kein weiteres Geät gefunden wird, wird NULL zurückgegeben.
Die Datenstruktur pci_dev ist die Software-Repräsentation des PCI-Geräts und daher Grundlage jeder PCI-Operation im System.
int pci_enable_device(struct pci_dev *dev);
Diese Funktion schaltet das Gerät ein. Sie weckt das Gerät und weist in manchen Fällen auch eine Interrupt-Leitung und I/O-Bereiche zu. Dies passiert beispielsweise bei CardBus-Geräten, die auf Treiber-Ebene vollständig äquivalent mit PCI sind.
unsigned long pci_resource_start(struct pci_dev *dev, int bar)
Die Funktion liefert die Anfangsadresse (Speicheradresse oder I/O Port), die dem entsprechenden Basisadressregister bar (0 bis 5) zugeordnet ist.
unsigned long pci_resource_end(struct pci_dev *dev, int bar)
Die Funktion liefert die Endadresse (Speicheradresse oder I/O Port), die dem entsprechenden Basisadressen-Register bar (0 bis 5) zugeordnet ist.
unsigned long pci_resource_len(struct pci_dev *dev, int bar)
Die Funktion liefert die Größe des Adressraums (Speicher oder I/O), die dem entsprechenden Basisadressen-Register bar (0 bis 5) zugeordnet ist.
void *ioremap(unsigned long phys_addr, unsigned long size)
Abbildung der (physikalischen) Adresse des PCI-Gerätes in den (virtuellen) Kerneladressraum. Je nach Computer-Plattform und verwendetem Bus kann auf I/O-Speicher über Seitentabellen zugegriffen werden oder nicht. Wenn der Zugriff über Seitentabellen erfolgt, muß der Kernel zunächst mit dafür sorgen, dass die physikalische Adresse von ihrem Treiber aus sichtbar ist. Wenn keine Seitentabellen verwendet werden, sehen die I/O-Speicherstellen I/O-Ports ziemlich ähnlich, und man kann mit den passenden Wrapper-Funktionen einfach darauf schreiben und daraus lesen. ioremap() sollte in jedem Fall verwendet werden. Der Parameter phys_addr gibt die physikalische Anfangsadresse an, size den Umfang des Adressbereichs.
void iounmap(void *addr)
Das Gegenstück zu ioremap().

Kernelspace und Userspace

Wie schon aus der Funktionsübersicht ersichtlich, gibt es spezielle Methoden, um Daten zwischen dem Adressraum des Kernels und dem Benutzerprogramm im Userspace hin- und herzutransprotieren. Die Operation kann nicht auf die übliche Weise mittels Zeigern oder memcpy() durchgeführt werden. Userspace-Adressen können aus einer Reihe von Gründen nicht direkt im Kernelspace verwendet werden: Ein großer Unterschied zwischen Adressen im Kernelspace und Adressen im Userspace besteht darin, dass Speicher im Userspace ausgelagert werden kann. Wenn der Kernel auf einen Zeiger im Userspace zugreift, ist die zugehörige Seite möglicherweise nicht im Speicher vorhanden, und es wird ein Seitenfehler (Page Fault) erzeugt. Die oben eingeführten Funktionen verwenden ein paar versteckte Zaubertricks, um auch dann noch korrekt mit Seitenfehlern umzugehen, wenn die CPU sich gerade im Kernelspace befindet.
Wenn das Zielgerät eine Erweiterungskarte anstelle von RAM ist, entsteht das gleiche Problem, weil der Treiber trotzdem noch Daten zwischen den Puffern im Benutzerprogramm und dem Kernelspace (sowie möglicherweise zwischen dem Kernelspace und dem I/O-Speicher) übertragen muss.

Obwohl sich die Funktionen wie normale memcpy()-Funktionen verhalten, mss man ein wenig zusätzliche Vorsicht walten lassen, wenn von Kernelcode aus auf den Userspace zugegriffen wird. Die angesprochenen Seiten im Userspace sind möglicherweise nicht im Speicher vorhanden, und der Page Fault-Handler kann den Prozess schlafen legen, während die Seite geholt wird. Dies passiert beispielsweise, wenn die Seite aus dem Swapspace (meist Festplatte) geholt werden muss. Daraus folgt, dass jede Funktion, die auf den Userspace zugreift, reentrant sein muss und gleichzeitig mit anderen Treiberfunktionen laufen können muss. Um nebenläfige Zugriffe zu steuern, verwendet man deswegen Semaphore.

Die Rolle der Funktionen ist nicht darauf beschränkt, Daten in den oder aus dem Userspace zu kopieren: Sie überprüfen auch, ob der Zeiger in den Userspace gültig ist. Wenn das nicht der Fall ist, wird auch nicht kopiert; wenn aber während des Kopierens eine ungültige Adresse vorgefunden wird, werden nur Teile der Daten kopiert.

Einbinden des Moduls - Praktische Hinweise

Die angegebenen Listings können als grobes Grundgerüst für eigene Treiber verwendet werden. In dem vorgestellten Beispiel wird allerdings auch nur eine PCI-Karte eines Types erkannt und es werden keine - für viele Anwendung wichtige - Datenstrom-Operationen (read(), write()) berücksichtigt, siehe dazu nächstes Kapitel.

Ein Kernel-Modul muss zwingend mit den beiden Präprozessor-Optionen -D__KERNEL__ und -DMODULE kompilert werden. Dies dient zur Freigabe bestimmter Datenstrukturen in den importierten, systemnahen Header-Dateien.
Dem Quelltextcerzeichnis liegt ein passendes Makefile bei, mit dem das Testprogramm board-test.c und das Kernelmodul xilinx_pci.c bequem übersetzt werden können.

Bevor das Modul in den laufenden Kernel eingebunden werden kann, muss noch die Gerätedatei angelegt werden:

    mknod /dev/xilinx_pci c 240 0
    chmod a+rw /dev/xilinx_pci
Das 'c' steht für einen zeichenorientiertes Gerät (character), 240 ist die Major-Nummer und 0 die Minor-Nummer.
Das kompilierte Modul xilinx_pci.o wird mit
    insmod xilinx_pci.o
geladen (insert module). Aufschluss über erfolgreich geladene Module gibt das Kommando
    lsmod
oder auch
    cat /proc/modules
Für das Entladen des Moduls gibt es den Befehl
   rmmod xilinx_pci
Erst danach kann ein ggf. verändertes neu kompiliertes Modul erneut für das Gerät geladen werden.
Wertvolle Hilfen beim Debuggen sind die Log-Dateien
    /var/log/kern.log
    /var/log/dmesg
in denen die Fehler- und Statusmeldungen des Moduls (und die durch es verursachten) gespeichert werden.


Autor: gkemnitz, Letzte Änderung: 14.04.2011 15:09:59


 TU Clausthal 2020  Impressum