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.
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):
Das Makro time_after vergleicht die beiden in jiffies angegebenen Zeitpunkte a und b. Falls a einen späteren Zeitpunkt als b kennzeichnet, ergibt das Makro »1«, ansonsten »0«.
Das Makro time_after_eq vergleicht ebenfalls die beiden Zeitpunkte a und b. Es gibt »1« (true) zurück, falls a einen späteren oder den gleichen Zeitpunkt wie b repräsentiert.
Das Makro time_before vergleicht die beiden Zeitpunkte a und b. Es ergibt »1«, falls der Zeitpunkt a vor dem Zeitpunkt b liegt.
Das Makro time_before_eq. vergleicht die beiden Zeitpunkte a und b. Es ergibt »1«, falls der Zeitpunkt a vor dem Zeitpunkt b liegt oder mit diesem identisch ist.
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:
Taktzyklenregister sind nicht bei allen Prozessoren vorhanden.
Um aus dem erfassten Zählwert einen Zeitwert zu berechnen, ist die Kenntnis der Taktfrequenz notwendig.
Ist das Taktzyklenregister nur 32 Bit breit, läuft dieses bei hohen Taktfrequenzen bereits nach kurzer Zeit über (bei einer Taktfrequenz von 1 Gigahertz wären dies etwa 4 Sekunden).
Ein 64 Bit breites Taktzyklenregister kann auf einer 32-Bit-Maschine nicht mit einem Register verarbeitet werden.
Zum Lesen des Taktzyklenregisters gibt es prinzipiell drei Makros, die in der Header-Datei <asm/msr.h> (machine-specific registers) deklariert sind:
Dem Makro werden zwei 32-Bit-Variablen übergeben, in denen das niederwertige und das hochwertige 32-Bit-Wort eines 64-Bit-Taktzyklenregisters abgelegt werden.
Mit diesem Makro werden nur die niederwertigen 32 Bit des Taktzyklenregisters gelesen.
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.
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:
Länge der Wartezeit
Bei Verarbeitungszeiten im Millisekundenbereich (abhängig von der Leistung des eingesetzten Systems) sollte – soweit möglich – passives Warten verwendet werden. Unterhalb von 1 Millisekunde kann dagegen auf aktives Warten zurückgegriffen werden. Hier ist der Overhead durch den Taskwechsel größer als der zu erwartende Gewinn.
Anforderungen an die Genauigkeit der Wartezeit
Prinzipiell ist die Einhaltung von Genauigkeiten beim aktiven Warten höher als beim passiven Warten. Bei Letzterem gehen noch Latenzzeiten durch den Scheduler und den notwendigen Kontext-Switch in die Ungenauigkeit mit ein.
Verarbeitungskontext
Passives Warten ist nur im Prozess- bzw. im Kernel-Kontext möglich:
in einer durch die Applikation getriggerten Funktion,
in einem Kernel-Thread,
in einer Workqueue-Funktion und
in einer Eventqueue-Funktion.
Passives Warten ist dagegen nicht innerhalb
einer Hardware Interrupt-Service-Routine,
einem Softirq,
einem Tasklet oder
einer Timer-Funktion
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.
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.
Zurück | Zum Anfang | Weiter |
Kritische Abschnitte sichern | Nach oben | Systemaspekte |