Kernel-Timer

Die ultimative Ressource zur Verwaltung von Zeit im Kernel sind die Timer. Timer werden dazu benutzt, die Ausführung einer Funktion (eines sogenannten Timer-Handlers) zu einem vorbestimmten Zeitpunkt auszulösen. Der Unterschied zu den Task-Schlangen und Tasklets besteht darin, daß Sie angeben können, wann Ihre Funktion aufgerufen werden soll, während Sie bei einer Task-Schlange nicht exakt vorhersagen können, wann Ihre Funktion aufgerufen wird. Ansonsten ähneln Kernel-Timer den Task-Schlangen: Eine in einem Kernel-Timer registrierte Funktion wird nur einmal ausgeführt — Timer sind nicht zyklisch.

Manchmal müssen Sie eine Operation losgelöst vom Prozeß-Kontext ausführen. Das könnte beispielsweise das Abschalten des Floppy-Motors oder eine andere, länger dauernde Operation zum Herunterfahren eines Gerätes sein. In diesem Fall wäre es nicht fair der Anwendung gegenüber, wenn einfach der Rücksprung aus dem close verzögert werden würde. Die Verwendung einer Task-Schlange wäre auch zuviel, denn ein Task in einer Schlange muß sich immer wieder selbst registrieren, während er seine Zeit-Berechnungen vornimmt.

Ein Timer ist viel leichter zu benutzen. Sie registrieren die Funktion einmalig, und der Kernel ruft die Funktion auf, wenn der Timer abläuft. Diese Funktionalität wird oft im Kernel selbst verwendet, manchmal aber auch — wie im Beispiel mit dem Floppy-Motor — von Treibern benutzt.

Die Kernel-Timer sind als doppelt verkettete Liste organisiert. Das bedeutet, daß Sie beliebig viele Timer erzeugen können. Ein Timer zeichnet sich durch seinen Timeout-Wert (in Jiffies) und die bei Ablauf des Timers aufzurufende Funktion aus. Der Timer-Handler bekommt ein Argument, das in der Datenstruktur gespeichert ist, sowie einen Zeiger auf den Handler selbst.

Die Datenstruktur eines Timers sieht folgendermaßen aus und ist in <linux/timer.h> zu finden:


struct timer_list {
    struct timer_list *next;          /* niemals beruehren! */
    struct timer_list *prev;          /* niemals beruehren! */
    unsigned long expires;            /* der Timeout, in Jiffies */
    unsigned long data;               /* Argument für den Handler */
    void (*function)(unsigned long);  /* Handler des Timeouts */
    volatile int running;             /* neu in 2.4, nicht anfassen */
};

Der Timeout eines Timers ist ein Wert in Jiffies; timer->function soll also gestartet werden, wenn jiffies größer oder gleich timer->expires ist. Der Timeout ist ein absoluter Wert; er wird normalerweise erzeugt, indem man den aktuellen Wert von jiffies nimmt und die gewünschte Verzögerung hinzuaddiert.

Sobald eine timer_list-Struktur initialisiert ist, kann sie mit add_timer in eine sortierte Liste eingefügt werden, die dann etwa einhundertmal pro Sekunde durchsucht wird. Selbst Systeme (wie Alphas), auf denen die Interrupt-Frequenz höher ist, überprüfen die Timer-Liste nicht öfter; die zusätzliche Timer-Auflösung würde die Kosten der zusätzlichen Durchläufe durch die Liste nicht rechtfertigen.

Die folgenden Funktionen arbeiten auf Timern:

void init_timer(struct timer_list * timer);

Diese Inline-Funktion wird verwendet, um die Timer-Struktur zu initialisieren. Derzeit werden nur die prev- und next-Zeiger mit 0 gefüllt. Programmierer sollten aber immer diese Funktion verwenden, um einen Timer zu initialisieren und nie direkt auf die Zeiger zugreifen, damit sie aufwärts-kompatibel bleiben.

void add_timer(struct timer_list * timer);

Diese Funktion fügt einen Timer in die globale Liste der aktiven Timer ein.

int mod_timer(struct timer_list *timer, unsigned long expires);

Sollten Sie den Zeitpunkt ändern müssen, an dem der Timer abläuft, können Sie mod_timer verwenden. Nach dem Aufruf wird der neue Wert für expires verwendet.

int del_timer(struct timer_list * timer);

Wenn ein Timer aus der Liste entfernt werden muß, bevor er abläuft, sollte del_timer verwendet werden. Wenn der Timer dagegen abläuft, wird er automatisch entfernt.

int del_timer_sync(struct timer_list *timer);

Diese Funktion arbeitet wie del_timer, garantiert aber auch, daß bei der Rückkehr die Timer-Funktion auf keiner CPU ausgeführt wird. del_timer_sync wird verwendet, um Race Conditions zu vermeiden, wenn eine Timer-Funktion zu unerwarteten Zeitpunkten ausgeführt wird; sie sollte in den meisten Situationen benutzt werden. Der Aufrufer von del_timer_sync muß sicherstellen, daß die Timer-Funktion nicht add_timer verwendet, um sich selbst wieder hinzuzufügen.

Das Modul jiq ist ein Beispiel für die Verwendung von Timern. Die Datei /proc/jitimer verwendet einen Timer, um die beiden Datenzeilen zu erzeugen; die Ausgabefunktion ist die gleiche wie in den Beispielen für Task-Schlangen. Die erste Datenzeile wird aus dem read-Aufruf (aufgerufen von einem Benutzer-Prozeß, der /proc/jitimer ausliest) erzeugt, während die zweite Zeile von der Timer-Funktion ausgegeben wird, nachdem eine Sekunde verstrichen ist.

Der Code von /proc/jitimer sieht folgendermaßen aus:


 

struct timer_list jiq_timer;

void jiq_timedout(unsigned long ptr)
{
    jiq_print((void *)ptr);            /* eine Zeile ausgeben */
    wake_up_interruptible(&jiq_wait);  /* Prozeß aufwecken */
}


int jiq_read_run_timer(char *buf, char **start, off_t offset,
                   int len, int *eof, void *data)
{

    jiq_data.len = 0;      /* Argument für jiq_print() vorbereiten */
    jiq_data.buf = buf;
    jiq_data.jiffies = jiffies;
    jiq_data.queue = NULL;      /* nicht erneut vormerken */

    init_timer(&jiq_timer);              /* Timer-Struktur initialsieren */
    jiq_timer.function = jiq_timedout;
    jiq_timer.data = (unsigned long)&jiq_data;
    jiq_timer.expires = jiffies + HZ; /* eine Sekunde */

    jiq_print(&jiq_data);   /* ausgeben und schlafen gehen */
    add_timer(&jiq_timer);
    interruptible_sleep_on(&jiq_wait);
    del_timer_sync(&jiq_timer);  /* falls uns ein Signal aufgeweckt hat */

    *eof = 1;
    return jiq_data.len;
}

Wenn Sie head /proc/jitimer aufrufen, bekommen Sie die folgende Ausgabe:


    time  delta interrupt  pid cpu command
 45584582   0        0    8920   0 head
 45584682 100        1       0   1 swapper

Aus der Ausgabe können Sie ersehen, daß die Timer-Funktion, die hier die letzte Zeile ausgegeben hat, im Interrupt-Modus lief.

Es kann merkwürdig erscheinen, daß der Timer genau zum richtigen Zeitpunkt abläuft, auch wenn der Prozessor gerade einen Systemaufruf ausführte. Wir haben schon angedeutet, daß ein Prozeß, der sich im Kernel-Space befindet, nicht vom Scheduler die CPU entzogen bekommt. Der Clock-Tick hat aber eine Sonderstellung und erledigt alle seine Aufgaben unabhängig vom aktuellen Prozeß. Sie können sich anschauen, was passiert, wenn Sie /proc/jitbusy im Vordergrund und /proc/jitimer im Hintergrund ausführen. Obwohl das System durch den Busy-Wait-Systemaufruf massiv blockiert zu sein scheint, laufen sowohl die Timer-Warteschlange als auch die Kernel-Timer.

Timer können also eine weitere Quelle von Race Conditions sein, selbst auf Einzelprozessor-Systemen. Alle Datenstrukturen, auf die aus einer Timer-Funktion heraus zugegriffen wird, sollten vor nebenläufigem Zugriff geschützt werden, entweder durch atomare Typen (werden in Kapitel 10 besprochen) oder durch Spinlocks.

Man muß auch beim Löschen von Timern auf Race Conditions achten. Denken Sie beispielsweise an eine Situation, in der eine Timer-Funktion eines Moduls auf einem Prozessor ausgeführt wird, während ein verwandtes Ereignis (eine Datei wird geschlossen oder das Modul entfernt) auf einem anderen Prozessor stattfindet. Als Folge davon könnte die Timer-Funktion eine Situation erwarten, die nicht mehr gegeben ist, was zu einem Systemabsturz führen kann. Um solche Race Conditions zu vermeiden, sollte Ihr Modul del_timer_sync anstelle von del_timer verwenden. Wenn die Timer-Funktion den Timer selbst neu starten kann (was oft gemacht wird), dann sollten Sie auch ein “Stop-Timer”-Flag haben, das Sie vor den Aufruf von del_timer_sync setzen. Die Timer-Funktion sollte dann dieses Flag abfragen und sich selbst nicht mehr mit add_timer vormerken, wenn das Flag gesetzt ist.

Ein weiteres Muster, das Race Conditions verursachen kann, tritt auf, wenn man Timer durch das Löschen mit del_timer modifiziert und dann einen neuen Timer mit add_timer erzeugt. Es ist in dieser Situation besser, mod_timer zu verwenden, um die notwendigen Änderungen vorzunehmen.