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 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.
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().
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.
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