6.6. Vom Umgang mit Zeiten

Innerhalb eines Treibers spielen Zeiten eine vielfältige Rolle. Zuweilen geht es nur darum, einen Zeitstempel oder Zeitdifferenzen zu erfassen. Andere Male muss eine Funktion für eine bestimmte Zeit warten oder zu bestimmten Zeiten aufgerufen werden und manchmal müssen sich Funktionen auf zeitlicher Basis synchronisieren.

6.6.1. Relativ- und Absolutzeiten

Innerhalb des Kernels werden Zeiten primär relativ zum Einschaltzeitpunkt des Systems gemessen. Die globale Variable jiffies erhöht sich mit jedem Timertick (Timerinterrupt). Dabei ist der zeitliche Abstand, in dem Timerticks auftreten, von Plattform zu Plattform unterschiedlich. Er wird durch die Konstante HZ in <asm/param.h> angegeben und ist beim PC typischerweise 1000, was einem zeitlichen Abstand von 1ms entspricht. Auf die Variable jiffies kann direkt zugegriffen werden.

Die Variable jiffies ist vom Typ »unsigned long«; auf einem x86-System stehen also 32 Bit zur Verfügung. Damit gibt es bei 1000 Ticks pro Sekunde (HZ=1000) etwa nach 49 Tagen einen Überlauf. Allerdings zählt der Kernel selbst die Timerticks in einer 64 Bit breiten Variablen (jiffies_64), so dass hier der Überlauf erst nach mehr als 584 Millionen Jahren erfolgt.

Da in der Vergangenheit viele Treiber nicht auf einen Zählerüberlauf eingerichtet waren, forciert der Kernel selbigen, so dass er bereits 5 Minuten nach dem Hochfahren auftritt. Damit können Fehler schneller und leichter gefunden und schließlich behandelt werden.

Ein Überlauf ist bei Zeitvergleichen kritisch. Der Treiberentwickler muss sorgfältig darauf achten, die Rechnungen mit jiffies sicher zu gestalten. In der Header-Datei <linux/timer.h> finden sich die entsprechenden Makros (siehe auch Zeitvergleiche per Makro):

Beispiel 6-16. Zeitvergleiche per Makro

    if( time_after(a,b) ) {
        ... // Zeitpunkt a ist später als Zeitpunkt b
    } else {
        ... // Zeitpunkt a ist früher oder gleich mit Zeitpunkt b
    }

Der Systemcall times, mit dem die verwendeten Rechenzeiten ausgelesen werden können, verwendet aus Kompatibilitätsgründen eine andere Zeitbasis. Diese ist innerhalb des Kernels mit USER_HZ (Header-Datei <linux/timer.h>) ausgewiesen und beträgt im Regelfall 100 (demnach erfolgen pro Sekunde 100 Timerticks bzw. jeder Zählwert entspricht 10 ms). 1000 Hz im Kernel, 100 Hz in den Applikationen: Treiber, die den Applikationen Zeiten in Form von Ticks übergeben, müssen ebenfalls eine entsprechende Umrechnung vornehmen.

Neben Timerticks (jiffies) werden Zeiten innerhalb und außerhalb des Kernels in einer Datenstruktur vom Typ struct timespec oder vom Typ struct timeval zur Verfügung gestellt (beide Deklarationen befinden sich ebenfalls in der Header-Datei <linux/time.h>). Während sich die Datenstruktur struct timespec aus den beiden Feldern Sekunde und Nanosekunde zusammensetzt, beinhaltet die Struktur struct timeval die beiden Felder Sekunde und Mikrosekunde.

Per Makro lassen sich jiffies in diese Datenformate konvertieren. Umgekehrt lassen sich die in Sekunden, Mikro- oder Nanosekunden vorhandenen Zeiten in jiffies umrechnen. timespec_to_jiffies und jiffies_to_timespec ist für die Konvertierung in die Struktur struct timespec zuständig. timeval_to_jiffies und jiffies_to_timeval sorgt für die Konvertierung in die Struktur struct timeval.

Die globale Variable xtime (definiert in kernel/timer.c) vom Typ struct timespec zählt die Sekunden und Nanosekunden, die seit dem 1.1.1970 (die berühmte Unixzeit) vergangen sind (Absolutzeit). Da es sich um eine Datenstruktur handelt, auf die von mehreren Seiten gleichzeitig zugegriffen werden kann, muss der Zugriff synchronisiert werden. Dazu wird das Sequencelock xtime_lock verwendet (siehe auch Kapitel Sequencelocks).

    struct timespec kopie_von_xtime;
    unsigned long seq;
    ...
    do {
        seq = read_seqbegin(&xtime_lock);
        kopie_von_xtime = xtime;
    } while (read_seqretry(&xtime_lock, seq));
    printk("Sekunden    =%ld\n", kopie_von_xtime.tv_sec );
    printk("Nanosekunden=%ld\n", kopie_von_xtime.tv_nsec );

Da dieser Code häufiger gebraucht wird, lässt sich für den Zugriff auch die Funktion current_kernel_time verwenden:

    struct timespec kopie_von_xtime;
    ...
    kopie_von_xtime = current_kernel_time();
    printk("Sekunden    =%ld\n", kopie_von_xtime.tv_sec );
    printk("Nanosekunden=%ld\n", kopie_von_xtime.tv_nsec );

Wird die Zeit, die seit dem 1.1.1970 vergangen ist, in Sekunden und Mikrosekunden (struct timeval) benötigt, hilft die Funktion do_gettimeofday weiter. Sie gibt die Zeit auf vielen Plattformen genauer zurück als die Variable xtime, da hier als Zeitbasis der interne Taktzyklenzähler (tsc) mit verrechnet wird.

    struct timeval zeit_seit_1970;
    ...
    do_gettimeofday(&zeit_seit_1970);
    printk("Sekunden     =%ld\n", zeit_seit_1970.tv_sec );
    printk("Mikrosekunden=%ld\n", zeit_seit_1970.tv_usec );

Auf diesen Taktzyklenzähler kann der Kernelentwickler auch direkt zugreifen. Beim Taktzyklenzähler handelt sich um ein prozessorspezifisches Register, das innerhalb des Mikroprozessors mit jedem Taktzyklus inkrementiert wird. Um eine Zeitdifferenz zu berechnen, werden die beiden später voneinander zu subtrahierenden Zeitpunkte durch Auslesen dieses Registers bestimmt.

Das Verfahren bringt allerdings folgende Nachteile mit sich:

Zum Lesen des Taktzyklenregisters gibt es prinzipiell drei Makros, die in der Header-Datei <asm/msr.h> (machine-specific registers) deklariert sind:

  1. rdtsc

    Dem Makro werden zwei 32-Bit-Variablen übergeben, in denen das niederwertige und das hochwertige 32-Bit-Wort eines 64-Bit-Taktzyklenregisters abgelegt werden.

  2. rdtscl

    Mit diesem Makro werden nur die niederwertigen 32 Bit des Taktzyklenregisters gelesen.

  3. rdtscll

    Dieses Makro legt den Inhalt des Taktzyklenregisters in eine Variable vom Typ unsigned long long ab.

Da das Bereitstellen dieser Funktionalität vom Mikroprozessor abhängt, muss der Treiberentwickler verifizieren, ob ein solches Register existiert oder nicht. Das in der Header-Datei <asm/cpufeature.h> deklarierte Makro cpu_has_tsc gibt ihm darüber Auskunft.

#include <asm/cpufeature.h>
#include <asm/msr.h>
    ...
    long tsclow;

    if( cpu_has_tsc ) {
        rdtscl( tsclow );
    }

Für viele Plattformen ist darüber hinaus die Funktion get_cycles definiert, die die Anzahl der Taktzyklen als Wert vom Typ cycles_t zurückgibt. Auf einer x86-Architektur ist dies ein long long.

#include <asm/timex.h>
    ...
    cycles_t tscall;

    tscall = get_cycles();

Um aus den Taktzyklen einen Zeitwert zu erhalten, muss die Anzahl an Taktzyklen noch mit der Dauer eines Taktzyklusses multipliziert werden. Auf einem x86-System kann die Taktfrequenz aus der Variablen cpu_khz ausgelesen werden.

6.6.2. Zeitverzögerungen

Zeitverzögerungen werden über Wartefunktionen realisiert. Dabei wird das aktive Warten vom passiven Warten unterschieden.

Bei einem aktiven Warten (Busy Loop) führt der Mikroprozessor in einer Schleife so lange nutzlose Befehle aus, bis die Wartezeit abgelaufen ist. Beim passiven Warten hingegen wird die Bearbeitung des gerade aktiven Codes verschoben. Die eigentliche Wartezeit wird vom Prozessor sinnvoll durch Abarbeitung eines anderen Rechenprozesses genutzt.

Ob es sinnvoller ist, aktiv oder aber passiv zu warten, hängt von unterschiedlichen Faktoren ab:

6.6.2.1. Aktives Warten

Zum aktiven Warten stellt der Linux-Kernel eine Funktion und ein Makro (deklariert in <linux/delay.h>) zur Verfügung. Der Funktion udelay wird die Anzahl der zu wartenden Mikrosekunden übergeben. Soll jedoch (entgegen unserer Warnung) mehrere Millisekunden gewartet werden, ist das Makro mdelay zu verwenden. Dieses bekommt als Parameter die Anzahl in Millisekunden übergeben.

Die Funktion udelay darf nicht mit einem beliebig großen Wert initialisiert werden, da es – abhängig vom verwendeten Mikroprozessor – zu einem Überlauf kommen kann. Ein Wert unterhalb von 5000 (entsprechend 5 Millisekunden) ist jedoch unbedenklich.

An dieser Stelle muss noch einmal explizit gewarnt werden: Busy-Loops verschlechtern die Reaktionszeit des Systems und sollten wirklich nur in Ausnahmefällen und mit gutem Grund eingesetzt werden.

6.6.2.2. Passives Warten

Werden innerhalb des Treibers Zeitverzögerungen benötigt, so wird die den Treiber aufrufende Task (bzw. der Thread) für die entsprechende Zeit in den Zustand wartend versetzt. Dazu kann – falls sich der Treiber im Prozess-Kontext befindet – die Funktion schedule_timeout verwendet werden:

schedule_timeout( TIME_TO_WAIT );

Diese Funktion zieht einen Timer (Relativzeit in Jiffies) auf die gewünschte Zeit auf, nach der der zugehörige Rechenprozesses in den Zustand lauffähig (TASK_RUNNING) versetzt wird. Allerdings muss vor dem Aufruf der Funktion schedule_timeout noch der Task-Zustand explizit gesetzt werden. Abhängig vom Task-Zustand verhält sich die Funktion unterschiedlich. Ist dieser vor Aufruf der Funktion auf TASK_RUNNING gesetzt, kehrt die Funktion sofort zurück, sprich der Rechenprozess wird erst gar nicht schlafen gelegt. Wird dagegen vor Aufruf der Funktion der Taskzustand auf warten, also entwender TASK_INTERRUPTIBLE oder TASK_UNINTERRUPTIBLE gesetzt, wird die zugehörige Treiberinstanz bzw. der zugehörige Kernel-Thread in den Zustand warten versetzt. Abhängig davon, ob TASK_INTERRUPTIBLE oder TASK_UNINTERRUPTIBLE verwendet wurde, wird der Warteaufruf durch ein vorzeitig eintreffendes Signal abgebrochen bzw. nicht abgebrochen.

    set_task_state( current, TASK_UNINTERRUPTIBLE );
    uebrige_zeit=schedule_timeout( 10*HZ ); // Task wartet in jedem Fall
    // die 10 Sekunden sind sicher vorbei

Beim unterbrechbaren Warten muss der Treiberentwickler noch überprüfen, ob das Warten als solches nicht durch ein Signal unterbrochen wurde. Das vollständige Codestück zum passiven Warten ergibt sich damit wie folgt:

    set_task_state( current, TASK_INTERRUPTIBLE );
    uebrige_zeit=schedule_timeout( 10*HZ ); // Task wartet 10 Sekunden
    // vielleicht hat ein Signal den Warteprozess unterbrochen?
    if( signal_pending(current) ) {
        ... // Signal bearbeiten
        if( uebrige_zeit ) {
            schedule_timeout( uebrige_zeit );
        }
    }

Wie die Code-Beispiele veranschaulichen, gibt die Funktion die Anzahl der jiffies zurück, die nicht gewartet wurde. Das ist sinnvoll, wenn die Funktion beispielsweise durch ein Signal unterbrochen wurde. Die Wartefunktion kann dann mit dem Rückgabewert direkt erneut gestartet werden.

Man beachte, dass die Funktion schedule_timeout nicht innerhalb von Interrupt-Service-Routinen oder Softirqs (z.B. Tasklets oder Timer) verwendet werden kann.


Lizenz