Aus Linux-Magazin 11/2003

Kernel- und Treiberprogrammierung mit dem künftigen Kernel 2.6 - Folge 4

Die Treiber aus den ersten Kern-Technik-Folgen reagieren nur auf Anfragen einer Userspace-Applikation. Andere Vorgänge laufen im Kernel asynchron ab. Sie reagieren auf Hardware- und Software-Interrupts, sind Timer-gesteuert oder arbeiten als eigenständiger Kernel-Thread.

Es spart Zeit, Vorgänge automatisiert im Kernel ablaufen zu lassen. Das bewies beispielsweise der im 2.4er Kernel integrierte Webserver »khttpd«, auch Tux genannt[1]. Er ist im Kernel 2.6 allerdings wieder verschwunden. Die Technik, einzelne Funktionen oder ganze Applikationen im Kernel ablaufen zu lassen, wurde aber erweitert und verfeinert. Wer ein bestimmtes Zeitverhalten implementieren oder zyklisch auf Daten zugreifen muss, Statistiken erstellen oder Informationen puffern will, der verwendet asynchrone Funktionen.

Aufgeräumt

Dem im Kernel 2.4 vorherrschenden Wildwuchs hat Linus Torvalds in Version 2.6 Einhalt geboten. Neben den in der dritten Kern-Technik-Folge[4] vorgestellten Interrupts (Hard-IRQs) gibt es jetzt die beiden Basistechnologien Soft-IRQ und Kernel-Thread.

Zur Unterscheidung: Beim Abarbeiten eines Hard-IRQ sind weitere Interrupts auf dem gleichen Prozessor im Regelfall nicht zugelassen. Soft-IRQs und Kernel-Threads hingegen können sehr wohl von Interrupts unterbrochen werden. Soft-IRQs laufen im so genannten Interrupt-Kontext ab, Kernel-Threads im Prozess-Kontext (siehe Kasten “Begriffe aus der Kernelwelt”).

Diese Unterschiede sind gerade für Kernel-Programmierer wesentlich, da ihnen einige Kernelfunktionen im Interrupt-Kontext nicht zur Verfügung stehen. Die Basistechnologien Soft-IRQ und Kernel-Thread existieren in unterschiedlichen Ausprägungen. So sind Soft-IRQs feiner differenziert in Soft-IRQ, Tasklet und Timer. Bei Kernel-Threads unterscheidet man Kernel-Thread, Workqueue und Event-Workqueue.

Kernel-Programmierer sollten die Ausprägung Soft-IRQ nach Möglichkeit meiden und dafür auf Tasklets und Timer zurückgreifen. Ein Tasklet übernimmt länger dauernde Aufgaben von einer ISR (Interrupt Service Routine). Ein Timer ist dann nützlich, wenn zu einem bestimmten Zeitpunkt oder periodisch Jobs auszuführen sind.

Wenn es um komplexe Aufgaben geht, greift der Entwickler gern zu Kernel-Threads. In der Ausprägung Workqueue kann er Funktionen durch den Betriebssystemkern aufrufen lassen. Noch weniger Aufwand fällt bei der vordefinierten Event-Workqueue an; allerdings ist bei ihr am wenigsten genau bestimmbar, wann der Auftrag an der Reihe ist.

Weiche Interrupts und Tasklets

Soft-IRQs sind als Erste an der Reihe, wenn alle Hardware-ISRs abgearbeitet sind. Von den 32 möglichen sind in den Kernelquellen (»linux/interrupt.h«) die sechs bereits vordefinierten Soft-IRQs aufgelistet. Für den Treiberentwickler sind hiervon die beiden Ausprägungen Tasklet sowie Timer interessant.

Tasklets übernehmen komplexe Aufgaben von Hardware-ISRs. Lange Hardware-ISRs führen zu ungünstigen Interrupt-Latenzzeiten und sind daher zu vermeiden. Sinnvoller ist es, solche Routinen in zwei Teile zu trennen. Der erste führt die (zeit-)kritischen Aktionen aus, während die Interrupts gesperrt sind. Die übrigen Berechnungen finden im zweiten Teil statt, bei freigegebenen Interrupts. Für diesen früher Bottom Half genannten Teil (siehe Kasten “Portierungshilfe”) eignen sich Tasklets.

Der Kernel sorgt dafür, dass ein Tasklet zu einem Zeitpunkt maximal einmal abläuft. Das gilt auch für Mehrprozessorsysteme. Unterschiedliche Tasklets zur gleichen Zeit einzusetzen ist allerdings möglich. Da Tasklets zur Gruppe der Soft-IRQs gehören, genießen sie im System die zweithöchste Priorität, direkt nach den Hardware-Interrupts. Allerdings kommen Tasklets in zwei Prioritätsstufen vor.

Abbildung 1: Asynchrone Verarbeitung im Kernel ist mit mehreren Technologien möglich, die in unterschiedlichen Kontexten ablaufen. Bei Soft-IRQs und Kernel-Threads gibt es je drei Ausprägungen.

Abbildung 1: Asynchrone Verarbeitung im Kernel ist mit mehreren Technologien möglich, die in unterschiedlichen Kontexten ablaufen. Bei Soft-IRQs und Kernel-Threads gibt es je drei Ausprägungen.

Immer der Reihe nach: Priorität der IRQs

Die hoch priorisierten Tasklets sind als erste Soft-IRQs an der Reihe, unmittelbar nach den Hardware-Interrupts. Niedrig priore Tasklets zuletzt. Entwickler sind aufgefordert, wenn möglich die niedrig priore Variante zu verwenden.

Um ein Tasklet zu spezifizieren, muss die ISR zwei Informationen angeben: die Adresse der Funktion, die der Kernel aufrufen soll, sowie einen Parameter, den der Kernel beim Aufruf an die Funktion weitergibt. Beide Angaben liegen in »struct tasklet_struct«. Das Makro »DECLARE_TASKLET« hilft dabei, diese Datenstruktur mit Inhalt zu füllen.

Dass er das Tasklet ausführen soll, erfährt der Kernel durch »tasklet_schedule()«. Diese Funktion muss zum richtigen Zeitpunkt aufgerufen werden, typischerweise innerhalb einer Hardware-ISR. Um ein übersichtliches und abgeschlossenes Beispiel zur Hand zu haben, ruft der Code in Listing 1 die Tasklet-Schedule-Funktion schon während der Modulinitialisierung in Zeile 18 auf.

Achtung: Race Condition

Bei Tasklets kann sich der Entwickler leicht in eine Race Condition verstricken: Wird das Modul entladen (User-Kommando »rmmod«) und ruft der Kernel danach ein von diesem Modul schedultes Tasklet auf, dann soll der Prozessor Code abarbeiten, der nicht mehr vorhanden ist. Das aber ist unmöglich. Der Kernel reagiert mit einer Oops-Meldung.

Der Treiberentwickler muss dafür sorgen, dass sein Modul vor dem Entladen alle Tasklets entfernt, die der Kernel noch nicht abgearbeitet hat. Die entsprechende Funktion heißt »tasklet_kill()«. Der Treiber kann sie gefahrlos aufrufen, selbst wenn er das Tasklet noch gar nicht schedult hat.

Nach dem Kompilieren und Laden (siehe erste Kern-Technik-Folge[2]) des Moduls in Listing 1 sollte im Syslog[4] die Meldung »Tasklet called…« auftauchen. Die Syslogs lassen sich übrigens sehr gut über »tail -f Logfile« überwachen. Also: weiteres Fenster mit einer Shell öffnen und »tail -f /var/log/messages« oder Ähnliches aufrufen.

Die in der Listing-Zeile 13 angelegte Struktur »TlDescr« erklärt die Funktion »TaskletFunction()« zum Einstiegspunkt des Tasklets. In Zeile 18 sorgt »tasklet _schedule()« dafür, dass der Kernel das Tasklet als niedrig prioren Soft-IRQ abarbeitet. Wer einen hoch prioren Soft-IRQ verwenden möchte, setzt die Funktion »tasklet_hi_schedule()« ein.

Ebenso wichtig wie Tasklets sind Timer, die zweite Variante der Soft-IRQs. Mit ihrer Hilfe beauftragt der Entwickler den Kernel damit, eine Funktion zu einem bestimmten Zeitpunkt aufzurufen. Der Kernel misst Zeitpunkte nicht absolut, sondern relativ zum Einschaltzeitpunkt. Als Basis dient die Anzahl der seit dem Einschalten ausgelösten periodischen Timer-Interrupts: Der Kernel zählt sie mit der Variablen »jiffies«.

Abbildung 2: Hard- und Software-Interrupts sind hoch prior. Erst wenn alle Interrupts abgearbeitet sind, kommen die Rechenprozesse, zu denen auch die Kernel-Threads zählen, an die Reihe.

Abbildung 2: Hard- und Software-Interrupts sind hoch prior. Erst wenn alle Interrupts abgearbeitet sind, kommen die Rechenprozesse, zu denen auch die Kernel-Threads zählen, an die Reihe.

Begriffe aus der Kernelwelt

Kontext: Der Kontext (die Umgebung) gibt an, auf welche Dienste und welche Ressourcen ein Codefragment Zugriff hat.

User-Kontext: Der Programmcode einer Applikation läuft im so genannten User-Kontext ab. Ihm stehen die Dienste des Betriebssystems zur Verfügung, wie sie im Systemcall-Interface spezifiziert sind.

Kernel-Kontext: Jeglicher Code des Betriebssystemkerns wird im Kernel-Kontext abgearbeitet. Man unterscheidet Prozess-Kontext und Interrupt-Kontext.

Prozess-Kontext: Die reguläre Umgebung, in der der Kernelcode abläuft, heißt Prozess-Kontext. Hier stehen sämtliche Funktionalitäten des Betriebssystemkerns zur Verfügung, auch das Verarbeiten des Codes für einige Zeit anzuhalten (schlafen zu legen) oder Daten zwischen den Speicherbereichen der Applikationen und des Kernels zu übertragen. Applikations-getriggerte Funktionen eines Gerätetreibers (wie »DriverRead()«), Systemcalls, Kernel-Threads und damit auch Workqueues werden im Prozess-Kontext abgearbeitet.

Interrupt-Kontext: Im Interrupt-Kontext stehen dem Kernelcode nicht alle Dienste des Betriebssystemkerns zur Verfügung. Funktionen in diesem Kontext können sich nicht schlafen legen. Sie dürfen auch keine Funktionen wie »kmalloc()« aufrufen, da diese die Routine unter Umständen schlafen legen wollen. Der Zugriff auf Speicherbereiche der Applikationen (zum Beispiel per »copy_from_user()«) ist ebenfalls tabu. Die Verarbeitung im Interrupt-Kontext ist möglichst kurz zu halten, um reguläre Rechenprozesse nicht unnötig zu verzögern. Routinen, die im Interrupt-Kontext ablaufen, sind ISRs (Interrupt Service Routine), Soft-IRQs, Tasklets und Timer.

Kernelspace: Als Kernelspace bezeichnet man Speicherbereiche, auf die eine Routine innerhalb des Kernels direkt (über Zeiger) zugreifen kann, ohne besondere Funktionen zu bemühen. Nur der Kernel hat Zugriff auf den Kernelspace. Zugang zum Userspace geben ihm die Funktionen »copy_to_user()«, »copy_from_user()«, »put_user()« und »get_user()«, allerdings nur auf den Speicher des gerade aktiven Rechenprozesses.

Userspace: Die Speicherbereiche, auf die eine Applikation direkt, zum Beispiel über Zeiger, zugreifen kann, heißen Userspace. Zum Kernelspace hat eine Applikation keinen Zugang, auch nicht mit Superuser-Rechten.

Augenblicke zählen: Jiffies

Um eine Funktion zu einem bestimmten Zeitpunkt aufrufen zu lassen, muss das Modul eine Datenstruktur vom Typ »struct timer_list« definieren und mit »init_timer()« initialisieren. Danach sind die Adresse der abzuarbeitenden Funktion (Feld »function«), der Abarbeitungszeitpunkt (»expires«) und Eingabedaten für die Funktion (»data«) zu spezifizieren. Der Zeitpunkt ist absolut in »jiffies« anzugeben. Soll ein Modul eine Funktion in einem bestimmte Zeitabstand auslösen, muss es nur eine einfache Rechenaufgabe lösen: aktuellen Zeitpunkt und Relativwert addieren.

Listing 1: Tasklet-Modul

01 #include <linux/version.h>
02 #include <linux/module.h>
03 #include <linux/init.h>
04 #include <linux/interrupt.h>
05 
06 MODULE_LICENSE("GPL");
07 
08 static void TaskletFunction( unsigned long data )
09 {
10     printk("Tasklet called...n");
11 }
12 
13 DECLARE_TASKLET( TlDescr, TaskletFunction, 0L );
14 
15 static int __init ModInit(void)
16 {
17     printk("ModInit calledn");
18     tasklet_schedule( &TlDescr ); // Tasklet so bald als möglich ausführen
19     return 0;
20 }
21 
22 static void __exit ModExit(void)
23 {
24     printk("ModExit calledn");
25     tasklet_kill( &TlDescr );
26 }
27 
28 module_init( ModInit );
29 module_exit( ModExit );

Listing 2: Timer-Funktionen

01 #include <linux/module.h>
02 #include <linux/version.h>
03 #include <linux/timer.h>
04 #include <linux/sched.h>
05 #include <linux/init.h>
06 
07 MODULE_LICENSE("GPL");
08 
09 static struct timer_list MyTimer;
10 
11 static void IncCount(unsigned long arg)
12 {
13     printk("IncCount called (%ld)...n",
14         MyTimer.expires);
15     MyTimer.expires = jiffies + (2*HZ); // zwei Sekunden
16     add_timer( &MyTimer );
17 }
18 
19 static int __init ktimerInit(void)
20 {
21     init_timer( &MyTimer );
22     MyTimer.function = IncCount;
23     MyTimer.data = 0;
24     MyTimer.expires = jiffies + (2*HZ); // zwei Sekunden
25     add_timer( &MyTimer );
26     return 0;
27 }
28 
29 static void __exit ktimerExit(void)
30 {
31     if( del_timer_sync(&MyTimer) )
32         printk("Activ timer deactivated.n");
33     else
34         printk("No timer activ.n");
35 }
36 
37 module_init( ktimerInit );
38 module_exit( ktimerExit );

Zeitgesteuerte Abläufe

Ist der Timer in Form der Datenstruktur definiert und initialisiert, übergibt die Funktion »add_timer()« die Aufgabe an den Kernel. Sobald der im Expires-Feld angegebene Zeitpunkt erreicht oder überschritten ist, ruft der Kernel die darin spezifizierte Funktion genau ein Mal auf. Soll er eine Funktion periodisch aufrufen, muss diese selbst ihre »timer_list« mit dem nächsten Aufrufzeitpunkt initialisieren und dann dem Kernel erneut übergeben.

In Listing 2 sorgt Zeile 24 dafür, dass der Kernel die Funktion »IncCount()« zwei Sekunden nach dem Laden des Moduls aufruft. Zeile 15 wiederholt dies, sodass die Funktion periodisch alle zwei Sekunden abläuft. Beleg für den Funktionsaufruf ist eine Ausgabe im Syslog. Achtung: Wer den Kernel damit beauftragt, zu einem Zeitpunkt ein einzelnes Timer-Objekt mehrfach abzuarbeiten, löst eventuell einen Crash aus.

Erfahrene Entwickler werden sicherlich bemerken, dass auch bei den Timern ein kritischer Abschnitt vorliegt. Das Modul darf nur dann entladen werden, wenn keine Timer-Funktion mehr im System anhängig ist. Die Lösung: »del_timer_sync()« entfernt eine als Timer eingehängte Funktion wieder.

Prozess im System: Der Kernel-Thread

Anders als Tasklets und Timer können und müssen sich Kernel-Threads während ihrer Laufzeit schlafen legen. Ein Kernel-Thread entspricht einem Thread auf Userebene, aber mit dem Unterschied, dass er komplett im Kernelspace abläuft. Er ist wie jeder andere Rechenprozess durch eine Taskstruktur repräsentiert und erscheint beim Kommando »ps auxw« auch in der Taskliste (siehe Abbildung 4). Der Abarbeitungszeitpunkt eines Kernel-Thread wird vom Scheduler festgelegt.

Diese Threads sind einfach zu erzeugen, ein Aufruf von »kernel_thread()« genügt. Die Funktion benötigt drei Parameter:

  • Die Adresse der Funktion, die der Scheduler beim ersten Aktivieren aufrufen soll.
  • Ein Argument für die aufzurufende Funktion.
  • Ein Bitfeld, das steuert, wie der Kernel den neuen Rechenprozesses erzeugt.

Im Regelfall genügt im Bitfeld der Eintrag »CLONE_KERNEL«, damit läuft das Erzeugen des Thread sehr effizient ab. Der Kernel muss keine Filesystem-Informationen (»CLONE_FS«), keine Filedeskriptoren (»CLONE_FILES«) und Signal-Handler (»CLONE_SIGHAND«) kopieren oder neu anlegen.

Der Rückgabewert der Funktion ist die Prozess-Identifikation (PID) des neuen Rechenprozesses. Sie ist später nötig, um den Kernel-Thread wieder zu entfernen. Der Rückgabewert »0« bedeutet, dass das Erzeugen schief gegangen ist:

ThreadID = kernel_thread( ThreadCode, NULL,
    CLONE_KERNEL );

Sobald er erzeugt ist, muss der neue Thread einige Initialisierungen durchführen. Zunächst gibt er per »daemonize()« alle Ressourcen im Userbereich frei, die ihm der Kernel standardmäßig mitgegeben hat.

Abbildung 3: Das Timer-Modul schreibt alle zwei Sekunden eine Ausgabe in den Syslog. Am Wert in Klammern ist erkennbar, dass im Kernel jeweils 2000 Timer-Ticks verstrichen sind.

Abbildung 3: Das Timer-Modul schreibt alle zwei Sekunden eine Ausgabe in den Syslog. Am Wert in Klammern ist erkennbar, dass im Kernel jeweils 2000 Timer-Ticks verstrichen sind.

Abbildung 4: Das Kommando »ps auxw« zeigt neben den Userprozessen auch die Kernel-Threads. Letztere sind in der Taskliste durch eckige Klammern markiert.

Abbildung 4: Das Kommando »ps auxw« zeigt neben den Userprozessen auch die Kernel-Threads. Letztere sind in der Taskliste durch eckige Klammern markiert.

Zum Daemon mutieren

Die Daemonize-Funktion erwartet als ersten Parameter den Namen des neuen Thread. Sie interpretiert ihn – ähnlich wie »printf()« – als Format-String, die zugehörigen Parameter sind in der Argumentliste ebenfalls enthalten. Sie erzeugt aus dem Format-String den Namen des Kernel-Thread. Er darf maximal 16 Zeichen lang sein, den Rest schneidet »daemonize()« ab.

Ein Kernel-Thread muss sich entweder rechtzeitig beenden oder selbst schlafen legen. Andernfalls würde er den anderen Rechenprozessen, insbesondere den Benutzerapplikationen, alle Rechenzeit wegnehmen. Wie in der zweiten Kern-Technik-Folge[3] gezeigt, hilft eine Waitqueue weiter.

Schlafender Thread

In Listing 3, Zeile 22 legt sich der Kernel-Thread mit Hilfe der Waitqueue eine Sekunde lang schlafen: »wait_event_interruptible_timeout()«. In der Praxis gibt es bessere Kriterien für das Schlafen und Aufwecken des Kernel-Thread: zum Beispiel auf Daten warten, die über eine TCP/IP-Verbindung kommen.

Problematisch ist aber das Beenden des Thread. Alle Rechenprozesse, sowohl die im Userbereich als auch die Kernel- Threads, kommen in der Regel selbstständig zu ihrem Ende. Will ein Prozess einen anderen beenden, sendet er ihm ein Signal. Allerdings hat die Funktion »daemonize()« alle Signale blockiert. Der Kernel-Thread muss sie daher erst wieder freigeben, er ruft dazu die Funktion »allow_signal()« auf.

Schwieriges Ende

Ob während des Schlafens ein Signal angekommen ist, zeigt der Rückgabewert des Makros »wait_event_interruptible _timeout()«. Zeile 25 prüft, ob der Wert »-ERESTARTSYS« entspricht, er bedeutet in diesem Zusammenhang “Thread beenden”. An beliebiger Stelle im Code stellt die Funktion »signal_pending()« fest, ob ein Signal angekommen ist oder nicht. Die globale Variable »current« zeigt auf den aktiven Thread:

if( signal_pending(current) ) {
    // Signal eingetroffen
} else {
    // Kein Signal
}

Der Kernel-Thread beendet sich, indem er die Thread-Funktion per »return« verlässt. Das Signal kann man zum Beispiel in der Shell mit »kill« absenden oder innerhalb des Kernels mit der Funktion »kill_proc()«. In beiden Fällen wird der Thread über die PID ausgewählt.

Listing 3: Kernel-Thread

01 #include <linux/module.h>
02 #include <linux/version.h>
03 #include <linux/init.h>
04 #include <linux/completion.h>
05 
06 MODULE_LICENSE("GPL");
07 
08 static int ThreadID=0;
09 static wait_queue_head_t wq;
10 static DECLARE_COMPLETION( OnExit );
11 
12 static int ThreadCode( void *data )
13 {
14     unsigned long timeout;
15     int i;
16 
17     daemonize("MyKThread");
18     allow_signal( SIGTERM ); // Funktion ab 2.5.61 nutzbar
19     printk("ThreadCode startet ...n");
20     for( i=0; i<10; i++ ) {
21         timeout=HZ; // Eine Sekunde
22         timeout=wait_event_interruptible_  timeout(
23             wq, (timeout==0), timeout);
24         printk("ThreadCode: woke up ...n");
25         if( timeout==-ERESTARTSYS ) {
26             printk("got signal, breakn");
27             ThreadID = 0;
28             break;
29         }
30     }
31     complete_and_exit( &OnExit, 0 );
32 }
33 
34 static int __init kthreadInit(void)
35 {
36     init_waitqueue_head(&wq);
37     ThreadID=kernel_thread(ThreadCode, NULL, CLONE_KERNEL );
38     if( ThreadID==0 )
39         return -EIO;
40     return 0;
41 }
42 
43 static void __exit kthreadExit(void)
44 {
45     kill_proc( ThreadID, SIGTERM, 1 );
46     wait_for_completion( &OnExit );
47 }
48 
49 module_init( kthreadInit );
50 module_exit( kthreadExit );

Completion-Objekt

Erneut droht die Gefahr eines ungeschützten kritischen Abschnitts. Wie bei den übrigen Methoden muss der Entwickler auch hier sicherstellen, dass sich der Kernel-Thread beendet hat, bevor das Modul entladen wird. Die Aufforderung, sich zu beenden, ist über »kill_proc()« einfach realisiert.

Da sich der Rechenprozess selbst beendet, muss das Modul jetzt noch auf das wirkliche Ende warten. Der Schutz dieses kritischen Abschnitts wird dadurch kompliziert, besonders wenn man bedenkt, dass bei einem Mehrprozessorsystem der Code zum Entladen des Moduls eventuell auf einem anderen Prozessor läuft als der Kernel-Thread.

Aus diesem Grund hat die Torvaldsche Entwicklergemeinde das Completion-Objekt entwickelt. Das Kernelmodul muss es global per »DECLARE_COMPLETION«-Makro definieren (Zeile 10 in Listing 3). Sobald sich der Thread beendet, ruft er die Funktion »complete_and_exit()« auf (Zeile 31). Deren Parameter sind die Adresse des Completion-Objekts und der Exitcode des Thread. Die Modul-Entladefunktion wählt »wait_for_completion()« (Zeile 46), die als Parameter ebenfalls die Adresse des Completion-Objekts erhält. Sie kehrt erst zurück, wenn ein passendes »complete_and_exit()« aufgerufen wurde.

Selbst wenn ein Modul mehrere Kernel-Threads startet, genügt ein einzelnes Completion-Objekt. Jeder Aufruf von »wait_for_completion()« im Modul muss sich mit einem Aufruf von »complete_and_exit()« in einem Thread decken. Wartet das Modul auf zwei Kernel- Threads, ruft es in der Entladefunktion zweimal »wait_for_completion()« auf, jeder der beiden Threads muss »complete_and_exit()« einmal aufrufen.

Listing 4: Workqueue

01 #include <linux/version.h>
02 #include <linux/module.h>
03 #include <linux/init.h>
04 #include <linux/workqueue.h>
05 
06 MODULE_LICENSE("GPL");
07 
08 static struct workqueue_struct *wq;
09 
10 static void WorkQueueFunction( void *data )
11 {
12     printk("WorkQueueFunctionn");
13 }
14 
15 static DECLARE_WORK( work, WorkQueueFunction, NULL );
16 
17 static int __init ModInit(void)
18 {
19     wq = create_workqueue( "DrvrSmpl" );
20     if( queue_work( wq, &work ) )
21         printk("work has been queued ...n");
22     else
23         printk("queue_work failed ...n");
24     return 0;
25 }
26 
27 static void __exit ModExit(void)
28 {
29     pr_debug("ModExit calledn");
30     if( wq ) {
31         destroy_workqueue( wq );
32         printk("workqueue destroyedn");
33     }
34 }
35 
36 module_init( ModInit );
37 module_exit( ModExit );

Workqueue statt Schlafstörung

Einen Thread schlafen legen ist meist mit einigem Aufwand verbunden. Wer mit niedriger Priorität Funktionen im Prozess-Kontext aufrufen will, ist mit der Workqueue oft besser bedient. Dieser Kernel-Thread nimmt die Adressen von Funktionen entgegen, die er dann abarbeitet. Zwei Objekte sind nötig: »struct work_queue_struct« beschreibt die Workqueue und »struct work_struct« referenziert die Funktion.

Der Speicher für das Workqueue-Objekt wird von der Kernelfunktion »create_workqueue()« angelegt (siehe Listing 4, Zeile 19). Ihr einziger Parameter ist der Name der Workqueue oder des Kernel-Thread. Achtung: Der Name darf maximal zehn Zeichen umfassen.

Den Speicher für das Work-Objekt stellt das Modul selbst zur Verfügung. Zum Definieren und Initialisieren nutzt es das Makro »DECLARE_WORK« (Zeile 15). Dessen drei Parameter sind der Name des Work-Objekts, die Adresse der aufzurufenden Funktion und der Wert des Parameters, den die Workqueue an die Funktion übergibt.

Die Funktion »queue_work()« (Zeile 20) trägt das Work-Objekt in die Workqueue ein. Ihre beiden Parameter kennzeichnen die Adresse der Workqueue und die Adresse des Work-Objekts. Bei nächster Gelegenheit ruft die Workqueue die im Work-Objekt spezifizierte Funktion auf. Es ist sogar möglich, beliebig viele Funktionen (Work-Objekte) in einer Workqueue unterzubringen. Sie ruft jedes Objekt genau einmal auf und entfernt es anschließend wieder.

Ein Work-Objekt darf sich nicht selbst in die Workqueue einhängen, denn damit würde es sofort wieder aufgerufen. Die Folge: Das System ist dann nur noch mit dem Abarbeiten dieser einen Funktion beschäftigt. Die im Work-Objekt angegebene Funktion sollte sich auch nicht schlafen legen: Der gesamte Workqueue-Thread würde einschlummern und auf diese Weise alle übrigen eingehängten Funktionen verzögern.

Verzögerungstaktik: Arbeit später erledigen

Muss eine Funktion nicht sofort, sondern erst zeitverzögert laufen, sind Workqueues ebenfalls ein geeignetes Mittel. Hierfür ist die Funktion »queue_delayed_work()« an Stelle von »queue_work()« zuständig. Sie nimmt als zusätzlichen Parameter einen Timeout-Wert (in Jiffies gemessen) auf.

Auch Workqueues sind vor dem Entladen des Treibers wieder zu entfernen: »destroy_workqueue()«. Diese Funktion wartet so lange, bis alle Work-Objekte der Workqueue abgearbeitet sind. Daher muss der Entwickler dafür sorgen, dass keine Instanz seines Treibers das Work-Objekt ständig wieder einhängt.

Wer den Code in Listing 4 kompiliert und das generierte Modul lädt, kann in der Prozesstabelle den Kernel-Thread der Workqueue entdecken. Die Workqueue heißt »DrvrSmpl« (Zeile 19), Abbildung 4 listet sie als »[DrvrSmpl/0]«. Das vom System angefügte »/0« bedeutet, dass es die Workqueue auf der ersten CPU abarbeitet. Achtung: Workqueues lassen sich nur von Kernelerweiterungen nutzen, die selbst der GPL oder einer BSD-Lizenz unterliegen. Für proprietäre Erweiterungen ist diese Funktionalität tabu.

Ereignisreich

Zum Abschluss sei noch die Event-Workqueue erwähnt (siehe Listing 5). Der Kernel erzeugt per Default für jeden Prozessor eine solche Workqueue. In der Prozessliste (»ps auxw«) erscheinen sie unter den Namen »[events/0]«, »[events/1]« und so fort. Die Event-Workqueue tritt an die Stelle des aus früheren Versionen bekannten »keventd«. Ihr Vorteil ist, dass das lästige Aufsetzen einer Workqueue entfällt.

Dank Event-Workqueue kann der Programmierer mit »schedule_work()« (Zeile 19) ohne Umschweife Funktionen zur Abarbeitung an den Kernel übergeben. »schedule_work()« erhält als einzigen Parameter die Adresse des Work-Objekts. »schedule_delayed_work()« startet die Funktion zeitversetzt. Die Aufräumarbeiten übernimmt die Funktion »flush _scheduled_work()« (Zeile 28). Sie stößt die Abarbeitung der Event-Workqueue an und wartet dann auf deren Ende.

Vorschau

Alle Kern-Technik-Folgen weisen auf eine besondere Herausforderung für den Entwickler hin: Race Conditions vermeiden und kritische Abschnitte absichern. Für die Stabilität von Linux ist dies entscheidend. Die nächste Folge wird daher einen Blick auf die entsprechenden Methoden im Kernel 2.6 werfen. (fjl)

Portierungshilfe

Wichtige Veränderungen zwischen Kernel 2.4 und Kernel 2.6:

Bottom Half: Bottom Halves gibt es im Kernel 2.6 nicht mehr. Stattdessen kommen Tasklets zum Einsatz, der Entwickler hat die Wahl zwischen der hoch prioren und der niedrig prioren Variante. Er kann sicher sein, dass ein Tasklet zu einem Zeitpunkt nur ein einziges Mal aktiv ist, egal wie oft er es eingehängt hat. Tasklets laufen – wie Bottom Halves – im Interrupt-Kontext.

Task-Queue: Die Zeit der Task-Queues ist vorbei. Programmierer nutzten in der Regel die drei Ausprägungen »tq_immediate«, »tq_ timer« und »tq_scheduler«. Die Task-Queue »tq_immediate« lässt sich durch ein Tasklet ersetzen. Statt »tq_timer« kommt ein Timer-Objekt zum Einsatz; als Zeitpunkt ist jetzt auf einer x86-Plattform der zehnfache Jiffies-Wert einzutragen. Als Ersatz für »tq_scheduler« eignet sich die Event-Workqueue.

Keventd: Der Kernel-Event-Daemon »keventd« existiert in Kernel 2.6 nicht mehr. Für ihn ist die Event-Workqueue zu verwenden.

Jiffy: Die globale Variable »jiffies« war in früheren Kernelversionen ein 32-Bit-Wert. Die Folge: Auf einer x86-Plattform kam es normalerweise nach 407 Tagen zum Zählerüberlauf. Theoretisch waren dabei inkonsistente Systemzustände möglich – für Entwickler eingebetteter Systeme ein Albtraum.

Kernel 2.6 enthält drei Änderungen. Erstens gibt es mit »jiffies_64« einen auf 64 Bit erweiterten Zähler. Zweitens wurde die Frequenz des Timer-Ticks, der den Zählerwert erhöht, von 100 Hz auf 1000 Hz erhöht. Drittens ist der Zähler so vorinitialisiert, dass er bereits nach fünf Minuten überläuft. Entwickler spüren damit die Konsequenzen wesentlich schneller und können den Code für diesen Fall debuggen.

Daemonize: Die Funktion »daemonize()« war im Kernel 2.4 ohne Parameter. Der Kernel-Thread, von dem diese Funktion aufgerufen wurde, setzte anschließend den Namen des Thread selbst. In Kernel 2.6 erhält Daemonize den Namen als Parameter und übernimmt das Setzen des Thread-Namens.

Abbildung 5: Die Techniken zur asynchronen Verarbeitung im alten und im neuen Kernel sind recht unterschiedlich. Für jede Aufgabe gibt es aber passenden Ersatz.

Abbildung 5: Die Techniken zur asynchronen Verarbeitung im alten und im neuen Kernel sind recht unterschiedlich. Für jede Aufgabe gibt es aber passenden Ersatz.

Listing 5: Die Event-Workqueue

01 #include <linux/version.h>
02 #include <linux/module.h>
03 #include <linux/init.h>
04 #include <linux/workqueue.h>
05 
06 MODULE_LICENSE("GPL");
07 
08 static struct work_struct work;
09 
10 static void WorkQueueFunction( void *data )
11 {
12     printk("WorkQueueFunctionn");
13 }
14 
15 static DECLARE_WORK( work, WorkQueueFunction, NULL );
16 
17 static int __init ModInit(void)
18 {
19     if( schedule_work( &work )==0 )
20         printk("schedule_work not successful ...n");
21     else
22         printk("schedule_work successful ...n");
23     return 0;
24 }
25 
26 static void __exit ModExit(void)
27 {
28     flush_scheduled_work();
29 }
30 
31 module_init( ModInit );
32 module_exit( ModExit );

Infos:

[1] Lothar Wieske, “Turbo-Pinguin – Tux, ein schneller Webserver als Kernelmodul”: Linux-Magazin 03/02, S. 61

[2] Eva-Katharina Kunst und Jürgen Quade, “Kern-Technik”, Folge 1: Linux-Magazin 08/03, S. 88

[3] Eva-Katharina Kunst und Jürgen Quade, “Kern-Technik”, Folge 2: Linux-Magazin 09/03, S. 86

[4] Eva-Katharina Kunst und Jürgen Quade, “Kern-Technik”, Folge 3: Linux-Magazin 10/03, S. 81

Die Autoren

Eva-Katharina Kunst, Journalistin, und Jürgen Quade, Professor an der Hochschule Niederrhein, sind seit den Anfängen von Linux auch Fans von Open Source.

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
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben