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.
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.
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.
| 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, ®, 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 }
|
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.
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.
Harter Tobak trotz NSA-FragenFü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 abDas 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”. |









