6.3. Softirqs

Direkt nach Abarbeitung einer Hardware-ISR beziehungsweise nach Freigabe der Interrupts überprüft der Kernel, ob weitere wichtige Funktionen/Routinen abzuarbeiten sind. Diese Funktionen werden Softirqs genannt.

In den Kernelquellen ist hierzu ein Feld von 32 Softirq-Routinen (siehe Datei <kernel/softirq.c> im Kernel-Quellcode) angelegt, wovon sechs bereits vordefiniert sind. Ein zugehöriges Bit im Variablenfeld irq_stat gibt an, ob eine Routine aufgerufen werden muss (Bit=1) oder nicht (Bit=0).

Abbildung 6-3. Vordefinierte Softirqs

Das Feld der Softirq-Routinen wird über die Funktion open_softirq belegt. Die vordefinierten Softirqs sind in Abbildung Vordefinierte Softirqs dargestellt.

Für Softirqs gilt generell:

Für den Treiberentwickler sind insbesondere die Ausprägungen Tasklet sowie Timer interessant. Sie werden im Folgenden genauer vorgestellt.

6.3.1. Tasklets

Der Kernelprogrammierer setzt Tasklets ein, um längere Berechnungen, die im Kontext eines Interrupts notwendig werden, abarbeiten zu lassen. Fänden diese Berechnungen innerhalb der Hardware-ISR statt, hätte das längere Interrupt-Latenzzeiten zur Folge, was in jedem Fall zu vermeiden ist. Daher teilt man die Verarbeitung einer ISR in zwei Schritte auf: Während des ersten Schrittes sind Interrupts gesperrt, nur die (zeit-)kritischen Aktionen werden durchgeführt. Die übrigen Berechnungen werden in einem zweiten Schritt – bei freigegebenen Interrupts – behandelt. Dieser früher Bottom-Half genannte Teil (siehe Anhang Portierungs-Guide) wird typischerweise in Form eines Tasklets realisiert.

Tasklets sind vom Entwickler kodierte Funktionen, die vom Kernel zusammen mit einem Parameter aufgerufen werden. Die Adresse der Tasklet-Funktion und das ihr als Parameter zu übergebende Datum werden in einer Instanz der Datenstruktur struct tasklet_struct (Prototyp in <linux/interrupt.h>) beschrieben. Die Initialisierung der Datenstruktur struct tasklet_struct kann statisch (also durch den Compiler) durch das Makro DECLARE_TASKLET oder dynamisch (also innerhalb einer Funktion zur Laufzeit) mit Hilfe der Funktion tasklet_init erfolgen (siehe Beispiele Statische Initialisierung einer Tasklet-Struktur und Dynamische Initialisierung einer Tasklet-Struktur).

Beispiel 6-2. Statische Initialisierung einer Tasklet-Struktur

static void tasklet_function( unsigned long data );        (1)
DECLARE_TASKLET( tl_descr, tasklet_function, 0L ); // Statische Definition(2)
(1)
Deklaration der Funktion, die abgearbeitet werden soll, wenn das Tasklet aufgerufen wird.
(2)
Mit DECLARE_TASKLET wird ein Element vom Typ struct tasklet_struct mit Namen »tl_descr« angelegt. Dieses Element wird mit der Adresse der aufzurufenden Funktion (tasklet_function) und einem zu übergebenden Datum (hier »0L«) initialisiert.

Beispiel 6-3. Dynamische Initialisierung einer Tasklet-Struktur

struct tasklet_struct tl_descr;
...
static int __init mod_init(void)
{
    tasklet_init( &tl_descr, tasklet_function, 0L );
    ...
}

Abbildung 6-4. Tasklets als Teil der Softirq-Verarbeitung

Das so definierte Tasklet muss nur noch zum richtigen Zeitpunkt zur Abarbeitung freigegeben werden. Typischerweise findet dieser Vorgang innerhalb einer Hardware-ISR durch Aufruf der Funktion tasklet_schedule oder der Funktion tasklet_hi_schedule statt. Welche der beiden Funktionen verwendet wird, hängt von der Wichtigkeit der innerhalb des Tasklets zu erledigenden Aufgaben ab. Für die Bearbeitung stehen nämlich zwei Prioritätsstufen zur Verfügung. Ein mit tasklet_hi_schedule eingehängtes Tasklet wird auf der Prioritätsstufe HI_SOFTIRQ abgearbeitet. Das bedeutet, dass die Tasklet-Funktion wirklich direkt nach dem Ende einer Hardware-ISR die CPU zugeteilt bekommt (siehe Abbildung Tasklets als Teil der Softirq-Verarbeitung). Ein mit der Funktion tasklet_schedule eingehängtes Tasklet dagegen wird erst dann abgearbeitet, wenn kein anderer Softirq mehr anliegt; dieses Tasklet hat also innerhalb der Gruppe der Softirqs die niedrigste Priorität (TASKLET_SOFTIRQ).

Um ein übersichtliches und abgeschlossenes Beispiel zur Hand zu haben, wird das Tasklet im Beispiel Einfaches Tasklet nicht während einer ISR, sondern bereits während der Modulinitialisierung »geschedult«. Wird der Code kompiliert und das so generierte Modul geladen, erscheint in den Syslogs die Meldung »Tasklet called...«.

Beispiel 6-4. Einfaches Tasklet

#include <linux/version.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>

MODULE_LICENSE("GPL");

static void tasklet_func( unsigned long data )
{
    printk("Tasklet called...\n");
}

DECLARE_TASKLET( tl_descr, tasklet_func, 0L );

static int __init drv_init(void)
{
    printk("drv_init called\n");
    tasklet_schedule( &tl_descr ); // Tasklet sobald als möglich ausführen
    return 0;
}

static void __exit drv_exit(void)
{
    printk("drv_exit called\n");
    tasklet_kill( &tl_descr );
}

module_init( drv_init );
module_exit( drv_exit );

Zum Schedulen eines Tasklets werden innerhalb der Funktionen tasklet_schedule beziehungsweise tasklet_hi_schedule Interrupts gesperrt. Zwar ist dies in den meisten Fällen unproblematisch, doch gibt es Situationen, in denen Interrupts nicht gesperrt werden sollen oder können. Für solche Fälle bietet Kernel 2.6 die Möglichkeit, das Tasklet vorzeitig einzuhängen, die Abarbeitung aber durch Setzen eines Flags zu verhindern. Zum Setzen dieses Flags dient die Funktion tasklet_disable. Die Ausführung der Tasklet-Funktion kann zum gewünschten Zeitpunkt – ohne Interrupts sperren zu müssen – eingefordert werden. Der Aufruf von tasklet_enable reicht hierzu aus. Wird anstelle des Makros DECLARE_TASKLET das Makro DECLARE_TASKLET_DISABLED verwendet, wird das Disable-Flag bereits direkt bei einer statischen Initialisierung gesetzt.

Mit einem Tasklet lässt sich zwar auf der einen Seite die Interrupt-Latenzzeit des Kernels verbessern, es beeinflusst aber auf der anderen Seite auch die Task-Latenzzeit. Aus diesem Grund sollte ein Tasklet so kurz wie möglich gehalten werden. Hinzu kommt, dass Tasklets im Interrupt-Kontext abgearbeitet werden, also sich nicht schlafen legen dürfen! Spätestens wenn diese Funktionalität benötigt wird, muss der Treiberentwickler auf die Verwendung von Kernel-Threads ausweichen.

Ein Tasklet wird zu einem Zeitpunkt maximal einmal aufgerufen – so die Spezifikation. Das gilt auch für Mehrprozessorsysteme. Unterschiedliche Tasklets zur gleichen Zeit einzusetzen, ist allerdings möglich.

Abschließend noch eine Warnung: Beim Gebrauch von Tasklets kann der Entwickler leicht in eine Race Condition verstrickt werden. Wird nämlich das Modul entladen und ruft der Kernel im Anschluss ein von diesem Modul geschedultes Tasklet auf, soll der Prozessor Code abarbeiten, der längst nicht mehr vorhanden ist. Das aber ist unmöglich. Es kommt zu einer Oops-Meldung. Zu den Aufgaben des Treiberentwicklers gehört von daher, sicherzustellen, dass jegliches Tasklet vor Entladen des Moduls entweder abgearbeitet oder entfernt wurde. Die entsprechende Funktion heißt tasklet_kill. Sie kann auch dann gefahrlos aufgerufen werden, wenn zum Zeitpunkt des Aufrufes das Tasklet nicht geschedult ist.

6.3.2. Timer-Funktionen

Neben Tasklets ist für Treiberentwickler noch eine weitere Variante der Softirqs überaus interessant, die so genannten Timer. Mit ihrer Hilfe kann der Entwickler den Kernel beauftragen, Funktionen zu einem definierten, späteren Zeitpunkt auszuführen.

Dazu muss man zunächst wissen, dass Zeiten innerhalb des Kernels nicht absolut, sondern relativ zum Einschaltzeitpunkt verarbeitet werden. Als Basis gilt die Anzahl der seit dem Einschalten ausgelösten, periodischen Timerinterrupts. Sie wird in einem Zähler mit dem Namen jiffies gezählt (siehe Kapitel Relativ- und Absolutzeiten).

Um einen Kernel-Timer zu verwenden, wird im Treiber eine Datenstruktur vom Typ struct timer_list alloziert. Die für den Kernel spezifischen Felder dieser Datenstruktur werden über die Funktion init_timer initialisiert (hier ist nur eine dynamische Initialisierung, also innerhalb einer Funktion, möglich). Danach sind noch die übrigen Felder mit der Adresse der aufzurufenden Funktion (Feld function), mögliche Daten für die Funktion (Feld data) und dem Zeitpunkt, zu dem die Funktion aufgerufen werden soll (Feld expires), zu spezifizieren (Beispiel Initialisierung der timer_list). Der Abarbeitungszeitpunkt wird absolut in jiffies angegeben. Um eine Funktion relativ zum momentanen Zeitpunkt aufzurufen, wird zum aktuellen Zeitpunkt der Relativwert aufaddiert. Liegt der Zeitpunkt, zu dem die Timer-Funktion aufgerufen werden soll, bereits in der Vergangenheit, wird die Routine sofort ausgeführt.

Beispiel 6-5. Initialisierung der timer_list

static struct timer_list ptimer;
static void timer_funktion(unsigned long);
...
static int __init mod_init(void)
{
    init_timer( &ptimer );
    ptimer.function = timer_funktion;
    ptimer.data = 0;
    ptimer.expires = jiffies + (2*HZ); // alle 2 Sekunden
    ...

Sobald der im Expires-Feld angegebene Zeitpunkt erreicht beziehungsweise überschritten ist, wird die dort spezifizierte Funktion genau einmal aufgerufen. Soll eine Funktion periodisch abgearbeitet werden, muss sie die zugehörige timer_list mit dem nächsten Aufrufzeitpunkt initialisieren und dann dem Kernel erneut übergeben (so zu sehen in Beispiel Verwendung eines Timers; hier wird die Timer-Funktion periodisch alle zwei Sekunden aufgerufen).

Die Funktion add_timer schließlich übergibt den Treiber zur Ausführung an den Kernel. Damit ist der Timer »aktiviert«. Der Kernel sorgt dafür, dass die in der Struktur angegebene Funktion mit den Daten als Parameter zum spezifizierten Zeitpunkt aufgerufen wird.

Es ist nicht erlaubt, einen bereits aktivierten Timer ein zweites Mal zu aktivieren!

Wie andere Softirqs auch, wird die Timer-Funktion im Interrupt-Kontext aufgerufen. Das bedeutet: Die Timer-Funktion ist möglichst kurz zu halten. Sie kann sich nicht schlafen legen! Überdies ist ein Zugriff auf User-Prozesse (Applikationen) innerhalb der Timer-Funktion nicht möglich.

Ist der Timer Teil eines Moduls und soll das Modul wieder entladen werden, ist jeder noch nicht abgelaufene Timer aus dem System zu entfernen (Deaktivieren des Timers). Dazu existiert die Funktion del_timer_sync. Auf SMP-Maschinen deaktiviert diese Funktion nicht nur einen Timer, sondern wartet darüber hinaus noch so lange, bis die möglicherweise auf einer anderen CPU aktive Timer-Funktion beendet ist. Auf einer Einprozessormaschine wird del_timer_sync auf die Funktion del_timer abgebildet, die den Timer nur deaktiviert.

static void __exit mod_exit(void)
{
    del_timer_sync( &ptimer );
    ...

Bevor ein Timer deaktiviert wird, ist zu verhindern, dass der Timer erneut eingehängt (add_timer) wird. Anders als bei den Tasklets muss dazu der Treiber seinen eigenen Mechanismus implementieren (beispielsweise über ein Flag, wie in Beispiel Stoppen periodischer Funktionen).

Beispiel 6-6. Stoppen periodischer Funktionen

static atomic_t nicht_mehr_einhaengen = 0;                 (1)

void timer_funktion( unsigned long parameter )
{
    if( atomic_read( &nicht_mehr_einhaengen ) ) {          (2)
        complete( &on_exit );
    } else {
        add_timer( &ptimer );
    }
    ...
}

static void __exit mod_exit(void)                          (3)
{
    atomic_set( &nicht_mehr_einhaengen, 1 );
    wait_for_completion( &on_exit );
    del_timer_sync( &ptimer ); // nicht mehr notwendig     (4)
    ...
(1)
Mit diesem Flag wird synchronisiert, ob der Timer noch eingehängt werden darf oder nicht. Der Datentyp »atomic_t« wird in Kapitel Atomare Operationen in der Übersicht vorgestellt.
(2)
Wenn das Flag nicht gesetzt ist, darf der Timer weiterhin verwendet werden.
(3)
Das Modul wird entladen, der Timer ist aus dem System zu entfernen. Durch das Setzen des Flags wird sichergestellt, dass nicht zwischenzeitlich der Timer erneut aktiviert wird.
(4)
Mit dieser Funktion wird ein Timer aus dem Kernel entfernt. Es sollte grundsätzlich die »sync«-Variante (del_timer_sync) verwendet werden.

Um festzustellen, ob ein Timer aktiviert ist oder nicht, gibt es die Funktion timer_pending. Diese gibt »1« zurück, falls der Timer aktiviert ist, ansonsten »0«.

    if( timer_pending( &ptimer ) ) {
        printk( "Timer ist aktiviert.\n" );
    } else {
        printk( "Timer ist nicht aktiviert.\n" );
    }

Noch zwei weitere Funktionen erleichtern den Umgang mit Timern. Die Funktion mod_timer ermöglicht es, bei einem aktivierten Timer den Zeitpunkt zu modifizieren, zu dem die Timer-Funktion aufgerufen werden soll.

Mit der Funktion add_timer_on ist der Entwickler in der Lage, für ein SMP-System festzulegen, auf welchem Prozessor eine Timer-Funktion ablaufen soll.

Beispiel 6-7. Verwendung eines Timers

#include <linux/module.h>
#include <linux/version.h>
#include <linux/timer.h>
#include <linux/sched.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");

static struct timer_list mytimer;

static void inc_count(unsigned long arg)
{
    printk("inc_count called (%ld)...\n", mytimer.expires );
    mytimer.expires = jiffies + (2*HZ); // 2 second
    add_timer( &mytimer );
}

static int __init ktimer_init(void)
{
    init_timer( &mytimer );
    mytimer.function = inc_count;
    mytimer.data = 0;
    mytimer.expires = jiffies + (2*HZ); // 2 second
    add_timer( &mytimer );
    return 0;
}

static void __exit ktimer_exit(void)
{
    if( timer_pending( &mytimer ) )
        printk("Timer ist aktiviert ...\n");
    if( del_timer_sync( &mytimer ) )
        printk("Aktiver Timer deaktiviert\n");
    else
        printk("Kein Timer aktiv\n");
}

module_init( ktimer_init );
module_exit( ktimer_exit );

Etwas komplizierter wird der Umgang mit Timern, wenn ein und derselbe Treiber an mehreren Stellen die Funktion add_timer mit dem gleichen Timer-Objekt aufruft. Da es zu Abstürzen kommen kann, falls der Kernel damit beauftragt wird, ein einzelnes Timer-Objekt zu einem Zeitpunkt gleich mehrfach abzuarbeiten, muss der Entwickler vor dem Aktivieren des Timers überprüfen, ob der Timer nicht bereits aktiviert wurde. Der Vorgang des Überprüfens und das Einhängen stellen einen kritischen Abschnitt dar, der zu schützen ist. Dazu bietet sich ein Spinlock an (siehe Beispiel Sicheres Einhängen eines Timer-Objekts).

Beispiel 6-8. Sicheres Einhängen eines Timer-Objekts

static struct spinlock_t timer_lock;
...
	
void secure_add_timer( struct timer_list *ptimer )
{
    unsigned long flags;

    spin_lock_irqsave( &timer_lock, flags );
    if( !timer_pending( ptimer ) ) {
        add_timer( ptimer );
    }
    spin_unlock_irqrestore( &timer_lock, flags );
}


Lizenz