Ich möchte an dieser Stelle einen kleinen Einblick in den Linux Kernel geben. Und zwar anhand der Entwicklung eines simplen Gerätetreibers.
Allerdings setze ich in diesem Beitrag den Umgang mit Linux, Editoren, dem Kernel und natürlich der Programmiersprache C voraus, da ich momentan nich die Zeit besitze, da auch noch einzuführen.
Was wir brauchen sind also die Header-Dateien unseres laufenden Kernels, einen Editor, C-Compiler und Make. Make ist nicht zwingend erforderlich, aber sehr hilfreich.
Ich persönlich empfehle den Editor Vi.
Da mein Treiber nicht besonders viel macht, habe ich ihn einfach erstmal lu_driver benannt. Der Quelltext besteht aus einer einzelnen Datei (lu_driver.c), welche ganz am Anfang Header-Dateien einbindet.
In unserem Fall:
#include <linux/fs.h>
#include <linux/module.h>
#include <asm/uaccess.h>
#include <linux/init.h>
Als nächstes nutzen wir zwei Makros, um bestimmte Formalien festzulegen. An dieser Stelle nur die beiden:
MODULE_AUTHOR(”Lukas Elsner”);
MODULE_LICENSE(”GPL”);
Sehr wichtig ist hier die Wahl der Lizenz, was ich hier aber auch nicht näher ausführen kann.
Jeder Treiber wird durch eine eindeutige Nummer angesprochen. Die legen wir zur besseren Übersicht auch schon hier fest:
#define DRIVER_MAJOR 125
Damit haben wir auch schon das Grundgerüst fast fertig. Jetzt lege ich einfach mal fest, dass der Treiber ein Character Device darstellen wird und durch den Syscall ‘read’ und gesteuert werden kann.
Ein ‘read’ soll uns ein “Hello World” zurückgeben.
Das “Hello World” legen wir in einen statischen Puffer:
static char msg[] = “Hello World\n”;
Im header <linux/init.h> ist die Struktur ‘file_operations definiert, die wir hier nutzen um die Einstiegspunkte zu unseren Funktionen festzulegen.
static struct file_operations fops = {
.open = driver_open,
.read = driver_read,
.release = driver_close,
};
Das bedeutet, dass wir vier Methoden implementieren müssen, wobei die beiden ‘driver_open’ und ‘driver_close’ hier nicht weiter behandelt werden. Sie werden bei jedem Zugriffsbeginn und -ende aufgerufen.
Hier müssen wir beachten, dass die Methoden vor der Struktur stehen müssen, da diese ja sonst noch nicht bekannt sind ;)
Die Methode ‘driver_read’ wird nun bei unserem Syscall ‘read’ aufgerufen.
ssize_t driver_read(struct file *instanz, char __user *userbuf, size_t count, loff_t *offset) {
int not_copied = 0;
count = min(count, strlen(msg) + 1);
if((not_copied = copy_to_user(userbuf, msg, count))) {
printk(”Driver was unable to copy %d byte\n”, not_copied);
}
return count – not_copied;
}
Als Parameter bekommen wir den Pointer auf den Puffer in den wir schreiben können und die Länge, die wir schreiben dürfen übergeben. Die anderen beiden Parameter interessieren uns vorerst nicht.
‘count’ überschreiben wir nun mit dem Rückgabewert aus ‘min(count, strlen(msg) + 1)’, um genau zu wissen wieviele Bytes wir in den Userspace schreiben müssen.
Nun können wir auch schon mit ‘copy_to_user’ unseren statuschen Puffer ‘msg’ in an die Adresse kopieren, die uns der Benutzer übergeben hat. Zurück bekommen wir die Anzahl der Bytes, die nicht kopiert werden konnten. Wenn der Rückgabewert größer als 0 ist, schreiben wir eine entsprechende Meldung mit ‘printk’ ins Syslog.
Nun wird nur noch die Anzahl der wirklich gelesenen Bytes zurückgegeben.
Zum Schluss brauchen wir noch zwei Methoden, die aufgerufen werden, wenn das Modul geladen, respektive entladen wird.
static int __init lu_module_init(void) {
printk(”loading lu_driver\n”);
if (register_chrdev(DRIVER_MAJOR, “lu_driver”, &fops)) {
printk(”couldn’t load module lu_driver\n”);
return -EIO;
}
return 0;
}static void __exit lu_module_exit(void) {
unregister_chrdev(DRIVER_MAJOR, “lu_driver”);
printk(”lu_driver unloaded\n”);
}module_init(lu_module_init);
module_exit(lu_module_exit);
Die beiden letzten Zeilen sind Makros, die es uns ermöglichen die beiden Methoden individuell zu benennen, welches wir auch hier genutzt haben.
Wichtig sind hier eigentlich nur die beiden Methoden
register_chrdev(DRIVER_MAJOR, “lu_driver”, &fops)
und
unregister_chrdev(DRIVER_MAJOR, “lu_driver”);
Welche unser Modul in das System ein- bzw. aushängen.
Somit ist unser Treiber auch schon fertig. Damit er automatisch übersetzt werden kann, gibt es eine passende Makefile dazu:
ifneq ($(KERNELRELEASE),)
obj-m := lu_driver.oelse
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
DEVNAME := /dev/lu
DRVNAME := lu_driver.kodefault:
$(MAKE) -C $(KDIR) M=$(PWD) modulesclean:
rm -f *~ lu_driver *.odevices:
rm -f $(DEVNAME)
mknod $(DEVNAME) c 125 0
chmod 666 $(DEVNAME)install:
insmod $(DRVNAME)uninstall:
rmmod $(DRVNAME)reinstall:
rmmod $(DRVNAME)
insmod $(DRVNAME)endif
Mit dem Befahl ‘make’ wird der Treiber übersetzt und man erhält eine lu_driver.ko im selbigen Verzeichnis.
‘make clean’ bereinigt unseren ordner
‘make devices’ erstellt mit Hilfe von mknod eine Gerätedatei um auf unserem Treiber zuzugreifen (wir erinnern uns an die Nummer 125)
und
‘make install’ läd unser Modul neu in den Kernel.
Nachdem wir nun das Modul übersetzt, die Gerätedatei erstellt und das Modul geladen haben, können wir über /dev/lu den String “Hello world” auslesen.
Entweder ganz einfach per ‘cat /dev/lu’, was aber solange liest, bis man per STRG+C abbricht, oder mit einem eigenen Programm, welches den Syscall ‘read’ auf die Datei /dev/lu aufruft.
Mit ‘make uninstall’ lässt sich das Modul wieder entladen. ;)
Quelltextdownload:
lu_driver (59)
Großer Dank geht an Jürgen Quade, welcher gerade an der Uni Passau einen super Workshop hierzu gemacht hat.
