Aus Linux-Magazin 07/2010

Kernel- und Treiberprogrammierung mit dem Kernel 2.6 - Folge 52

Simulierende Module ermöglichen es, auf Hardware zuzugreifen, die gar nicht im Rechner eingebaut ist. Dreh- und Angelpunkt dafür ist ein modifizierter Gerätetreiber.

Virtualisierung macht eine typischerweise nur einmal vorhandene Hardware mehreren Komponenten scheinbar exklusiv zugänglich. Es geht aber noch mehr: Per Simulation greifen Linux-Anwender auf Hardware zu, die in ihren Rechnern überhaupt nicht existiert.

Das ist nützlich, wenn diese sich beispielsweise noch in der Entwicklung befindet, ihre Entwickler aber bereits notwendige Treiber und erste Applikationen programmieren und testen wollen – oder wenn eine Hardware schlichtweg zu teuer ist. Dann sammeln die Entwickler bereits erste Erfahrung mit der nicht vorhandenen Hardware, selbst wenn Zeitverhalten und eventuell auch der Funktionsumfang nicht vollständig dem Original entsprechen.

Hardwarezugriffe umbiegen

Was Linux-Interessierte dafür benötigen: ein Simulationsprogramm und ein Treiber (siehe Abbildung 1). Bei ihm darf es sich auch um ein bereits existierendes Exemplar handeln, das für die Simulation nur ein wenig zu modifizieren ist. In jedem Fall aber ist der Treiber der Dreh- und Angelpunkt des Ganzen, denn er leitet die Anfragen der Applikation entweder an die real existierende Hardware oder eben an die alternativ oder ausschließlich eingesetzte Simulation weiter.

Abbildung 1: Ein modifizierter Treiber in Kombination mit einem Simulationsprogramm ermöglicht es, Hardware vorzutäuschen, ohne dass Entwickler ihre Applikationen dafür verändern müssen.

Abbildung 1: Ein modifizierter Treiber in Kombination mit einem Simulationsprogramm ermöglicht es, Hardware vorzutäuschen, ohne dass Entwickler ihre Applikationen dafür verändern müssen.

Ein gutes Softwaredesign vorausgesetzt muss der Entwickler einen Treiber nur minimal verändern, um eine Simulation möglich zu machen. Konkret heißt dies, die Zugriffe auf die reale Hardware (siehe Kasten “Memory-mapped versus IO-mapped”), die über die Maschinenbefehle »inX«, »outX« beziehungsweise über die Funktionen »readX()« sowie »writeX()« erfolgen, auf »simulation_inX()«, »simulation_outX()«, »simulation_readX()« und »simulation_writeX()« umzubiegen (siehe Abbildung 2). Die so modifizierten Funktionen reichen die übergebenen Daten dann bei Bedarf an das eigentliche Simulationsprogramm weiter.

Abbildung 2: Funktionspointer biegen im Treiber den Hardwarezugriff auf die Simulation um. Dargestellt ist hier nur das Schreiben auf die Hardware.

Abbildung 2: Funktionspointer biegen im Treiber den Hardwarezugriff auf die Simulation um. Dargestellt ist hier nur das Schreiben auf die Hardware.

Für den erfahrenen C-Programmierer ist das Umbiegen von Funktionen ein klarer Fall von Funktionspointern. Die könnten sie beispielsweise »mapper_inX«, »mapper_outX«, »mapper_readX« oder »mapper_writeX« nennen. Das Anhängsel »X« steht übrigens für den jeweils ersten Buchstaben von Byte, Word oder Long, je nachdem, ob ein 1-, 2- oder 4-Byte-Zugriff erfolgt. »mapper_readb« zeigt also auf eine Funktion, die genau ein einzelnes Byte einliest.

Das Simulationsprogramm selbst lässt sich bei hohen Anforderungen an das Zeitverhalten im Kernel realisieren. Attraktiver ist jedoch eine Applikation im Userland, die vor allem einfacher zu implementieren und zu konfigurieren ist. Sie erleichtert auch eine Visualisierung der simulierten Hardware.

Memory-mapped versus
IO-mapped
Peripherie, die ein Rechner wie Hauptspeicher über den normalen Adressbus in den Adressraum einer CPU einblendet, nennt man Memory-mapped. Einige Prozessorhersteller – allen voran Intel – haben zusätzlich zum normalen Adressbus einen zweiten, abgespeckten Adressbus eingebaut, der vor allem der Ankopplung von IO-Hardware dient (IO-mapped). Historisch betrachtet erweiterte das nicht nur den Adressraum, sondern sparte gleichzeitig den Hardware-Aufwand für die Dekodierung der Peripherie. Software-technisch muss die CPU natürlich erfahren, wenn sie eine Adressinformation an den neuen Adressbus, dem so genannten IO-Bus, anlegen soll. Die Assemblerbefehle, die den Standardbefehl »mov« für den Zugriff auf Speicher ergänzen, nennt Intel »in« und »out«.

Linux stellt dem Treiberprogrammierer Wrapperfunktionen für den direkten Hardwarezugriff zur Verfügung: Per »readX()« und »writeX()« greift er auf Memory-mapped-Hardware zu, mit den Makros »inX« und »outX« auf die Port-mapped-Hardware.

Simulator ankoppeln

Als Schnittstelle zwischen Simulationsprogramm und modifiziertem Treiber kommen vor allem das Proc-Filesystem [1], das Sys-Filesystem [2], das Netlink-Interface [3] oder die normale Treiberschnittstelle in Frage. Letztere ist mit den Systemcalls »open()«, »close()«, »read()« und »write()« gerade dem Applikationsprogrammierer gut bekannt. Das Simulationsprogramm erhält Informationen vom Treiber dadurch, dass es per »read()« auf eine zusätzliche Gerätedatei zugreift. Einen neuen Hardwarestatus übermittelt der Kernel dem modifizierten Treiber mit Hilfe von »write()«.

Etwas tricky

Die zusätzliche Gerätedatei stellt der Treiber zur Verfügung. Implementierungstechnisch reserviert der Kernelentwickler dazu einfach eine zusätzliche Gerätenummer und legt das zugehörige Sys-FS-Verzeichnis an. Dadurch erzeugt Udev automatisch die Gerätedatei, über die die Simulationsapplikation zugreift [4]. Allerdings muss der Treiber später differenzieren, ob das Simulationsprogramm oder die eigentliche, unveränderte Applikation die Daten transferiert.

Dieses Verhalten implementiert häufig Code in den Funktionen »driver_read()« und »driver_write()«, indem sie die Gerätenummer auswerten. Die sich hierdurch ergebende Modifikation der Lese- und Schreibfunktionen im Treiber ist unschön und lässt sich mit einem kleinen Trick umgehen. Typischerweise sind nämlich die Adressen der Zugriffsfunktionen »driver_read()« und »driver_write()« in der Struktur »struct file_operations« hinterlegt. Die »struct file«, die die zugreifende Applikationsinstanz repräsentiert, referenziert die Funktionen.

Die Idee besteht also darin, die »struct file_operations« auszutauschen, sobald ein Simulationsprogramm den Treiber öffnet (»driver_open()«). Als Ersatz kommt dann eine neue Struktur, die auf die speziellen Zugriffsfunktionen »driver_read_simulation()« und »driver_write_simulation()« verweist (siehe Listing 2, Zeile 55). Die nachfolgenden Aufrufe von »read()« und »write()« des Simulationsprogramms sind damit automatisch auf die eigenen Zugriffsroutinen umgelenkt (siehe Abbildung 3).

Abbildung 3: Beim Öffnen eines simulierten Geräts verweist das Modul fortan auf einen Satz neuer Routinen. Durch den Austausch der Zugriffsfunktionen bleibt der ursprüngliche Treibercode weitgehend unverändert.

Abbildung 3: Beim Öffnen eines simulierten Geräts verweist das Modul fortan auf einen Satz neuer Routinen. Durch den Austausch der Zugriffsfunktionen bleibt der ursprüngliche Treibercode weitgehend unverändert.

Listing 2: Modifizierter Treiber
»lm_driver.c«
001 #include <linux/fs.h>
002 #include <linux/cdev.h>
003 #include <linux/device.h>
004 #include <asm/uaccess.h>
005 
006 #define DEV_NAME "linux-magazin"
007 #define DEV_NAME_SIMU "linux-magazin.simu"
008 
009 static dev_t lm_dev_number;
010 static struct cdev *driver_object;
011 struct class *lm_class;
012 static struct device *lm_dev, *lm_dev_simu;
013 
014 static u32 data_register;
015 static u32 (*mapper_readl)(void *addr);
016 
017 static int driver_open(struct inode *devicefile, struct file *filp);
018 
019 ssize_t
020 application_read(struct file *instance, char __user *buffer,
021     size_t max_bytes_to_read, loff_t *offset)
022 {
023     size_t to_copy, not_copied;
024     u32 reg = mapper_readl((void *)0x12345678);;
025 
026     to_copy    = min(sizeof(reg), max_bytes_to_read);
027     not_copied = copy_to_user(buffer, &reg, to_copy);
028     return to_copy - not_copied;
029 }
030 
031 static ssize_t
032 simulation_write(struct file *instance, const char *userbuf,
033                  size_t length, loff_t *o)
034 {
035     int to_copy, not_copied;
036 
037     to_copy    = min(length, sizeof(data_register));
038     not_copied = copy_from_user(&data_register, userbuf, to_copy);
039     printk("simulation_write: data_register=0x%xn", data_register);
040     return to_copy - not_copied;
041 }
042 
043 static struct file_operations fops_application = {
044     .open = driver_open,
045     .read = application_read,
046 };
047 
048 static struct file_operations fops_simu = {
049     .write = simulation_write,
050 };
051 
052 static int
053 driver_open(struct inode *devicefile, struct file *filp)
054 {
055     if (MINOR(devicefile->i_rdev) == 0)
056         filp->f_op = &fops_application;
057     else
058         filp->f_op = &fops_simu;
059     return 0;
060 }
061 static u32
062 simulation_readl(void *addr)
063 {
064     return data_register;
065 }
066 
067 static int
068 __init mod_init(void)
069 {
070     mapper_readl = simulation_readl; // Simulation aktivieren
071 
072     if (alloc_chrdev_region(&lm_dev_number, 0, 2, DEV_NAME) < 0)
073         return -EIO;
074     driver_object = cdev_alloc();
075     if (driver_object == NULL)
076         goto free_device_number;
077 
078     driver_object->owner = THIS_MODULE;
079     driver_object->ops = &fops_application;
080 
081     if (cdev_add(driver_object, lm_dev_number, 2))
082         goto free_cdev;
083 
084     lm_class = class_create(THIS_MODULE, DEV_NAME);
085     if (IS_ERR(lm_class)) {
086         pr_err("template: no udev supportn");
087         goto free_cdev;
088     }
089     lm_dev = device_create(lm_class, NULL, lm_dev_number,
090                            NULL, "%s", DEV_NAME);
091     lm_dev_simu = device_create(lm_class, NULL, lm_dev_number + 1,
092                                 NULL, "%s", DEV_NAME_SIMU);
093     return 0;
094 
095 free_cdev:
096     kobject_put(&driver_object->kobj);
097 
098 free_device_number:
099     unregister_chrdev_region(lm_dev_number, 2);
100     return -EIO;
101 }
102 
103 static void
104 __exit mod_exit(void)
105 {
106     device_destroy(lm_class, lm_dev_number);
107     device_destroy(lm_class, lm_dev_number + 1);
108   
109     class_destroy(lm_class);
110   
111     cdev_del(driver_object);
112   
113     unregister_chrdev_region(lm_dev_number, 2);
114   
115     return;
116 }
117 
118 module_init(mod_init);
119 module_exit(mod_exit);
120 MODULE_LICENSE("GPL");

Bei einem Treiber, der sowohl reale als auch die simulierte Hardware kennt, fehlt noch die Mimik zur Aktivierung beziehungsweise Deaktivierung der Simulation. Dazu gibt es wieder einmal eine Reihe Möglichkeiten: Entwickler könnten beispielsweise einen Parameter definieren, den sie beim Laden des Treibers setzen [5].

Elegant aktivieren

Etwas eleganter ist es, eine Sys-Datei anzulegen, die nach dem Laden des Treibers zwischen realer Hardware und Simulation hin- und herschaltet. Am elegantesten gelingt der Code, wenn er im Treiber zusätzlich zum Sys-File automatisch die Simulation aktiviert, sollte er keine Hardware erkennen. Bei einem Treiber für eine PCI-Hardware wertet der Programmierer dazu zum Beispiel den Rückgabewert von »pci_register_driver()« in der Funktion »mod_init()« aus. Falls beim Anmelden kein Gerät existiert, gibt die Funktion »-ENODEV« zurück (siehe Listing 1).

Listing 1: Ohne PCI auf
Simulation umschalten
01 if (pci_register_driver(&pci_drv) == -ENODEV) {
02     /* Simulation aktivieren */
03     mapper_inl  = simulation_inl;
04     mapper_outl = simulation_outl;
05 } else {
06     mapper_inl  = inl;
07     mapper_outl = outl;
08 }

Listing 2 zeigt zusammen mit Listing 3 und Listing 4 exemplarisch die Komponenten einer simulierten Hardware. Anschauungsobjekt ist ein Hardwarezähler, der seinen Wert im Sekundentakt erhöht. In der realen Variante liest der Code den Zählwert durch Zugriff mittels »readl()« auf eine Memory-Adresse aus. Damit das Beispiel übersichtlich bleibt, ist der Treiber selbst auf die wesentliche Funktionalität reduziert und legt zugreifende Applikationen nicht schlafen. Im Normalfall aktiviert er die Simulation (siehe Listing 2, Zeile 70). Weitere Details des Treibers beschreibt [4].

Hardware im Userland

Listing 3 zeigt das zugehörige Simulationsprogramm. Es erhöht sekündlich in einer Endlosschleife den Zählerwert und übergibt ihn per Systemcall »write()« auf das Gerät »/dev/linux-magazin.simu« an den Treiber. Der Simulationscode im Treiber speichert den Zählwert in der globalen Variablen »data_register« ab, sodass die Funktion »simulation_readl()« nur deren Wert zurückgibt (siehe Listing 2, Zeile 64). Die Applikation (siehe Listing 4) greift per »read()«-Systemcall auf die Gerätedatei »/dev/linux-magazin« zu und gibt den Wert unspektakulär auf der Konsole aus. Sie merkt dabei nicht, ob die Hardware real vorhanden ist oder ob Linux sie nur simuliert.

Listing 3: Simulator
»simulator.c«
01 #include <stdio.h>
02 #include <fcntl.h>
03 #include <linux/types.h>
04 
05 int main(int argc, char **argv, char **envp)
06 {
07     int fd; 
08     __u32 data_register = 0;
09 
10     fd = open("/dev/linux-magazin.simu",
11               O_RDWR);
12     if (fd < 0) {
13        perror("/dev/linux-magazin.simu");
14        return -1;
15     }
16     while (1) {
17        sleep(1);
18 
19        write(fd, &data_register,
20              sizeof(data_register));
21        printf("simulator: 0x%xn",
22               data_register);
23        data_register++;
24     }
25     return 0;
26 }
Listing 4: Testanwendung
»application.c«
01 #include <stdio.h>
02 #include <fcntl.h>
03 #include <linux/types.h>
04 
05 int main(int argc, char **argv, char **envp)
06 {
07     int fd;
08     __u32 data_register;
09 
10     fd = open("/dev/linux-magazin", O_RDWR);
11     if (fd < 0) {
12        perror("can't open /dev/linux-magazin");
13        return -1;
14     }
15 
16     while (1) {
17         read(fd, &data_register,
18              sizeof(data_register));
19         printf("%s: data_register=0x%xn",
20                argv[0], data_register);
21         sleep(1);
22     }
23     return 0;
24 }
Abbildung 4: Der Zählwert der Applikation (links) ändert sich nur bei aktivem Simulator (rechts).

Abbildung 4: Der Zählwert der Applikation (links) ändert sich nur bei aktivem Simulator (rechts).

Wer den Code testen will, kompiliert den Treiber (siehe Listing 2), das Simulationsprogramm (siehe Listing 3) und die Applikation (siehe Listing 4). Er lädt zunächst den Treiber, startet dann das Simulationsprogramm und parallel dazu die Applikation. Die beiden Userland-Programme erfordern meist Root-Rechte, da Udev die Gerätedateien ohne weitere Konfiguration zunächst nur für den Superuser freigibt. Die Konsole der Applikation zeigt, dass sich der Wert des Zählers so lange erhöht, wie die Simulation aktiv ist (siehe Abbildung 4).

Noch ein Hinweis zum Simulationsprogramm: Damit es ein- und ausgehende Daten unabhängig behandelt, wählen erfahrene Programmierer einen auf Threads fußenden Ansatz. Der erste von ihnen wartet auf die Daten, die an die simulierte Hardware gerichtet sind, der zweite Thread generiert die Daten, die von der simulierten Hardware kommen.

Taktfrequenz erhöhen

Übungswillige ergänzen das Beispiel, das Abbildung 5 noch einmal zusammenfasst, um schreibenden Hardwarezugriff, mit dem sie zum Beispiel die Zählfrequenz ändern. Die Implementation erfolgt analog: Die Schreibfunktion »driver_write()« kopiert den von der Applikation zur Verfügung gestellten Frequenzwert vom Userspace in den Kernel und übergibt ihn per »mapper_outl()« an die Hardware. Da die Funktion »mapper_outl()« auf den Simulator zeigt, ruft Linux im Treiber »simulator_outl()« auf. Im einfachsten Fall pollt der Simulator ständig den Treiber nach neuen Daten, effizienter ist es natürlich, wenn er per blockierendem »read()« auf neue Informationen wartet. Die »simulator_outl()«-Funktion würde dann die schlafende Instanz aufwecken. Sowohl Simulation wie auch Umlenkung im Treiber sind für die Darstellung auf das Minimum reduziert.

Abbildung 5: Sechs Schritte benötigen Entwickler, um bestehende Treiber simulationsfähig zu machen.

Abbildung 5: Sechs Schritte benötigen Entwickler, um bestehende Treiber simulationsfähig zu machen.

Schwieriges Hardwaredesign

Die gezeigte Basisfunktionalität lässt sich leicht erweitern, etwa um simulierte Interrupts oder auch um vorgegaukelte USB-Geräte. Allerdings soll die kompakte Darstellung nicht den Blick dafür verdunkeln, dass die Simulation realer Hardware oft komplex gerät und wesentlich mehr Code verlangt. Das ändert aber nichts an der grundsätzlichen Lösung, im Treiber die Hardwarezugriffe umzubiegen und über beispielsweise ein zweites Device dem Simulator zuzuführen. (mg)

Kern-Technik-Diplom
Zusammen mit der 50. Folge der Kern-Technik-Serie stellte das Linux-Magazin das anspruchsvolle Wissens-Quiz “Kern-Technik-Diplom” online (siehe Abbildung 6). Dabei ging es ins Eingemachte: Gefragt war unter anderem, wie viele Bits der Datentyp »int« auf einer 64-Bit-Maschine breit ist (32 Bit), was die Kernelfunktion »round_jiffies()« mit Powermanagement zu tun hat (der Kernel spart durch längeres Schlafen Strom), wofür im Kontext von USB die Abkürzung URB steht (USB Request Block) oder mit welchen Funktionen Kern-Hacker Zeitvergleiche programmieren (»time_after()«, »time_before()«, »time_after_eq()« und »time_before_eq()«). 49 Multiple-Choice-Fragen – zu jeder Folge der Kern-Technik eine – prüften das Know-how in der Breite und der Tiefe.

Abbildung 6: Insgesamt 49 anspruchsvolle Fragen waren beim Kern-Technik-Diplom zu beantworten.

Abbildung 6: Insgesamt 49 anspruchsvolle Fragen waren beim Kern-Technik-Diplom zu beantworten.

Harter Tobak trotz NSA-Fragen

Für Auflockerung sorgten einige der vordefinierte Antworten: Selbstverständlich steht “Inotify-Subsystem” nicht für “Intelligence Notification System”, das Informationen an einen geheimen NSA-Port weiterleitet, bei “Darthvader” handelt es sich nicht um einen IO-Scheduler, und die “Seek-Distanz” gibt keineswegs die räumliche Entfernung der Festplatte zu Fort Meade an.

Kern-Techniker schnitten gut im Test ab

Das Ergebnis der Diplomprüfung kann sich sehen lassen: Im Durchschnitt 80 Prozent korrekte Antworten zeugen von der hohen Kompetenz der Teilnehmer. Mit 98 Prozent richtigen Antworten sticht Stefan Roas hervor, der als bester Teilnehmer nahezu jede Aufgabe korrekt löste. Die “Top Ten beim Kern-Technik-Diplom” erhalten vom Linux-Magazin neben einer Urkunde das digitale Themenpaket “Kern-Technik” mit den Folgen 1 bis 49. Und sobald das Buch der Autoren “Linux-Treiber entwickeln” Ende des Jahres in aktualisierter Auflage erscheint, erhält Muster-Entwickler Stefan Roas ein Exemplar für sein beeindruckendes Kernel-Wissen.

Wer neugierig auf die Fragen ist, kann seine Kenntnisse immer noch unter [https://www.linux-magazin.de/kernspalterei] unter Beweis stellen. Anders als noch bei der Prüfung anlässlich der Jubiläumsausgabe teilt der Web-Test den Teilnehmern aber jetzt die korrekte Lösung sofort mit.

Top Ten beim
Kern-Technik-Diplom
Platz Name Punkte
1 Stefan Roas 97,7
2 Erik Braun 95,0
3 Marcus Nutzinger 94,0
4 Tobias Klauser 90,8
5 Bernhard M. Wiedemann 90,7
6 Matthias Vogt 88,2
7 Rolf Eike Beer 87,8
8 Andreas Bießmann 87,5
9 Florian Pfanner 83,8
10 Jan Engelhardt 83,2
Infos
[1] Eva-Katharina Kunst und Jürgen Quade, “Kern-Technik”, Folge 7:Linux-Magazin 02/04, S. 48

[2] Eva-Katharina Kunst und Jürgen Quade, “Kern-Technik”, Folge 6:Linux-Magazin 01/04, S. 94

[3] Eva-Katharina Kunst und Jürgen Quade, “Kern-Technik”, Folge 40:Linux-Magazin 07/08, S. 96

[4] Eva-Katharina Kunst und Jürgen Quade, “Kern-Technik”, Folge 48:Linux-Magazin 11/09, S. 98

[5] Eva-Katharina Kunst und Jürgen Quade, “Kern-Technik”, Folge 10:Linux-Magazin 05/04, S. 94

Die Autoren
Eva-Katharina Kunst, Journalistin, und Jürgen Quade, Professor an der Hochschule Niederrhein, sind seit den Anfängen von Linux Fans von Open Source. Zur Zeit arbeiten sie an der dritten Auflage ihres Buches “Linux-Treiber entwickeln”.
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben