![]() |
Raspberry Pi: Sleep- und Timerfunktionen in CProf. Jürgen Plate |
It's still the same old story
A fight for love and glory
A case of do or die.
The world will always welcome lovers
As time goes by.
In diesem Text geht es um die verschiedenen Möglichkeiten, ein C-Programm für eine bestimmte Zeit anzuhalten und um das Setzen von Timern unter Linux im Userland. Kernel-Timer und -Treiber bleiben unberücksichtigt. Die Alarm-Funktion und die Reaktion auf Signale wird hier nur soweit berücksichtigt wie nötig, da diese an anderer Stelle schon behandelt wurden (siehe Links am Schluss). Alle Programme wurden auf einem Raspberry Pi, Modell B, unter Raspbian getestet.
Für die folgenden Experimente muss irgendwie die verbrauchte Zeit möglichst präzise gemessen werden. Dabei hilft die Funktion gettimeofday(), welche die aktuelle Systemzeit in Mikrosekunden (ausgehend von der UNIX-Epoche) liefert. Dazu wird ihr die Adresse einer Struktur übergeben, die Sekunden und Mikrosekunden getrennt speichert:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
Zur Zeitmessung muss lediglich vor und nach der zu beobachtenden Codesequenz gettimeofday()
aufgerufen und dann die beiden Werte voneinander subtrahiert werden. Das folgende Programm
demonstriert das Vorgehen.
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
struct timeval t1, t2;
long long elapsedTime;
/* Startwert */
gettimeofday(&t1, NULL);
/* machwas */
sleep(1);
/* Endwert */
gettimeofday(&t2, NULL);
/* Berechne die verbrauchte Zeit in Microsekunden */
elapsedTime = ((t2.tv_sec * 1000000) + t2.tv_usec)
- ((t1.tv_sec * 1000000) + t1.tv_usec);
printf("Aufruf dauerte %lld us\n", elapsedTime);
return 0;
}
Im folgenden Kapitel wird auch untersucht, in wie weit der Aufruf von gettimeofday()
in die Messung eingeht. Analog zu gettimeofday() gibt es auch noch settimeofday(),
mit der man die Systemzeit setzen kann.
Noch genauer geht es mit clock_gettime(). Hier werden die Werte in Nanosekunden zurückgegeben. Die Struktur ist aber sehr ähnlich:
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
Die Funktion clock_gettime() legt über den ersten Parmeter die verwendete Zeitbasis
fest:
/* Compile with: gcc -Wall -o timediff2 timediff2.c -lrt */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <time.h>
int main()
{
struct timespec t1, t2, clock_resolution;
long long elapsedTime;
clock_getres(CLOCK_REALTIME, &clock_resolution);
printf("Resolution von CLOCK_REALTIME ist %ld Sekunden, %ld Nanosekunden\n",
clock_resolution.tv_sec, clock_resolution.tv_nsec);
/* Startwert */
clock_gettime(CLOCK_REALTIME, &t1);
/* machwas */
sleep(1);
/* Endwert */
clock_gettime(CLOCK_REALTIME, &t2);
/* Berechne die verbrauchte Zeit in Nanosekunden */
elapsedTime = ((t2.tv_sec * 1000000000L) + t2.tv_nsec)
- ((t1.tv_sec * 1000000000L) + t1.tv_nsec);
printf("Aufruf dauerte %lld ns\n", elapsedTime);
return 0;
}
Der Systemaufruf times() gibt die Zeit in clock tics an. Die Anzahl der tics pro Sekunde werden durch die Systemkonstante _SC_CLK_TCK gegeben. Ein typischer Wert für _SC_CLK_TCK ist 100, was einem clock tic von 10 Millisekunden entspricht. Die Funktion times() speichert die aktuellen Prozesszeiten in einer Struktur tms, deren Adresse an die Funktion übergeben wird. Sie ähnelt damit dem UNIX-Kommando time. Die Struktur tms ist definiert als:
struct tms {
clock_t tms_utime; /* user time */
clock_t tms_stime; /* system time */
clock_t tms_cutime; /* user time of children */
clock_t tms_cstime; /* system time of children */
};
Das Feld tms_utime enthält die CPU-Zeit für die Ausführung von Anweisungen
des aufrufenden Prozesses. Das Feld tms_stime enthält die CPU-Zeit, die
beim Aufruf von von System-Funktionen verbraucht wurde. Das Feld tms_cutime
enthält die Summe der tms_utime- und tms_cutime-Werte für alle beendeten
Kindprozesse. Das Feld tms_cstime enthält die Summe der tms_stime- und
tms_cstime-Werte für alle beendeten Kindprozesse.
Zeiten für beendet Kindprozesse (und ihre Nachkommen) werden erst in dem Augenblick
ermittelt, in dem die Funktionen wait() oder waitpid() die Prozess-ID
des beendeten Kindprozesses zurückliefern. Alle Zeiten werden in clock ticks gemessen.
Mann kann den Funktionsaufruf sysconf(_SC_CLK_TCK) verwenden, um die Anzahl
der dafür nötigen Taktzyklen pro Sekunde festzustellen.
times() liefert die Anzahl der Taktzyklen, die seit einem beliebigen Punkt
in der Vergangenheit abgelaufen sind. Der Rückgabewert kann den möglichen Bereich
des Typs clock_t überschreiten. Bei Fehler wird -1 zurückgegeben.
Das folgende Programm zeigt beispielhaft die Anwendung. Zuerst wird die Zeitbasis
aus der Konstanten _SC_CLK_TCK ermittelt. Danach wird die Anfangszeit
genommen. Hier sind die Zeiten immer 0, aber man könnte die Zeitnahme ja auch erst
mitten im Programm beginnen. Nach sinnloser Zeitvergeudung wird die Endzeit
ermittelt und dann werden die Zeiten ausgegeben, einmal als clock ticks und einmal
als Sekunden.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h>
int main(void)
{
double tbase;
struct tms tms0, tms1;
int i, j;
float s;
FILE *f;
/* Anfangswerte der Zeiten (in der Regel 0) */
if (times (&tms0) == -1)
perror ("times");
/* Zeitbasis in Sekunden ermitteln */
tbase = (double)(1.0/sysconf(_SC_CLK_TCK));
printf("Zeitbasis: _SC_CLK_TCK: %ld, Tbase: %lf Sekunden\n",
sysconf(_SC_CLK_TCK),tbase);
/* sinnlose Operationen */
for (i = 0; i < 1000; i++)
{
for (j = 0; j < 100000; j++)
s = (double)(i*j);
f = fopen("/bin/ls","r");
fclose(f);
}
/* Endwerte der Zeiten */
if (times (&tms1) == -1)
perror ("times");
/* Ausgabe in clock ticks */
printf("times 0 - usr: %ld sys: %ld ch-usr: %ld ch-sys: %ld\n",
tms0.tms_utime, tms0.tms_stime, tms0.tms_cutime, tms0.tms_cstime);
printf("times 1 - usr: %ld sys: %ld ch-usr: %ld ch-sys: %ld\n",
tms1.tms_utime, tms1.tms_stime, tms1.tms_cutime, tms1.tms_cstime);
/* Ausgabe in Sekunden */
printf("user: %lf system: %lf\n",
tbase*(tms1.tms_utime - tms0.tms_utime),
tbase*(tms1.tms_stime - tms0.tms_stime));
return 0;
}
Die Programmausgabe auf dem Raspberry Pi lautet:
pi@raspberrypi ~ $ ./times _SC_CLK_TCK: 100, Tbase: 0.010000 Sekunden times 0 - usr: 0 sys: 0 ch-usr: 0 ch-sys: 0 times 1 - usr: 643 sys: 3 ch-usr: 0 ch-sys: 0 user: 6.430000 system: 0.030000
Muss ein Prozess nur für eine gewisse Zeit angehalten (suspendiert) werden, können die Funktionen sleep(), usleep() oder nanosleep() dazu verwendet werden. Aber auch die hier nicht betrachteten Funktionen select() und poll() lassen sich dafür missbrauchen. Die Funktionen suspendieren einen Prozess jeweils für die im Parameter angegebenen Zeiten. Bei sleep() handelt es sich um Sekunden, bei usleep() sind es Mikrosekunden und bei nanosleep() Nanosekunden.
Die maximal erreichbare Präzision hängt vom System und von der Methodik ab. Da ein normales Linux auch kein Realzeit-Betriebssystem ist, kommt es zu zetilichen Schwankungen bei der Ausführung der Sleep-Funktionen - je nachdem, was da sonst noch an Programmen aktiv ist. Auf den meisten Linux-Systemen betrug die Prazision bis Kernel 2.6 eine hundertstel Sekunde, inzwischen sind es (konfigurierbar) eine Millisekunde. Insofern spiegeln die Funktionen usleep() oder nanosleep() eine Präzision vor, die es gar nicht gibt. Wer es etwas präziser haben will, kann über sched_setpriority() die Prozesspriorität hochsetzen, was aber zur Folge hat, dass während der Suspendierung andere Prozesse gebremst werden!
Die Alternative einer "Busy Loop" (z. B. eine while-Schleife) blockiert zwar nicht, jedoch ist die zeitliche Länge einer solchen Schleife natürlich abhängig von der Prozessorgeschwindigkeit und -auslastung. Ausserdem nimmt auch hier der Prozess nahezu alle CPU-Leisung auf, was ebenfalls andere Prozesse behindert (und die CPU auf maximale Temperatur bringt). Grund genung, den Sleep-Komplex etwas näher zu untersuchen. Es sein an dieser Stelle nicht verschwiegen, dass der Kernel selbst Zeiten mit wesentlich höherer Präzision messen kann, aber um das zu nutzen, müsste man einen Kerneltreiber programmieren.
Im Vergleich zu sleep() und usleep() hat nanosleep() den Vorteil, keine Signale zu beeinflussen, es ist POSIX-standardisiert, es bietet eine höhere Zeitauflösung und es macht es leichter, eine Schlafperiode fortzusetzen, die durch ein Signal unterbrochen wurde.
Die aktuelle Implementierung von nanosleep() beruht auf dem normalen Timer-Mechanismus des Kernels, der eine Auflösung von 1/HZ Sekunden. Die Kernel-Konstante HZ ist seit Kernel 2.6.20 konfigurierbar und kann die Werte 100, 250 (Standard), 300 oder 1000 annehmen. Näheres dazu weiter unten im Timer-Abschnitt. Daher dauern nanosleep()-Pausen immer mindestens die angegebene Zeit. Es können aber bis zu 10 ms zusätzlich vergehen, bis der Prozess wird wieder lauffähig gesetzt ist. Aus dem gleichen Grund wird im Fall einer Untergrechung durch ein Signal der im Remainder *rem zurückgegebene Wert in der Regel auf das nächsthöhere Vielfache von 1/HZ Sekunden gerundet.
Probieren wir einen ersten Vergleich der Funktionen. Als Zeitangabe dienen einheitlich Mikrosekunden. Für den Aufruf von nanosleep() wurde eine Funktion delay() geschrieben, die den Parameterwert in Sekunden un Nanosekunden umrechnet und dann nanosleep() aufruft:
int delay(unsigned long mikros)
{
struct timespec ts;
int err;
ts.tv_sec = mikros / 1000000L;
ts.tv_nsec = (mikros % 1000000L) * 1000L;
err = nanosleep(&ts, (struct timespec *)NULL);
return(err);
}
Damit das Ganze spannender wird, probiere ich neben den drei Standardfunktionen
auch eine Variante der Zeitmessung mittels Zählschleife aus. Mit clock_gettime
wird in einer Schleife ständig die Differenz zwischen alktueller Zeit und Startzeit
ermittelt und die Schleife erst verlassen, wenn die per Parameter übergebene Zeit
abgelaufen ist. Zu beachten ist, dass man den Sekundenüberlauf in der Funktion selbst
abhandeln muss. Auf die Funktion clock_gettime() wird im Abschnitt über
Tmer noch genauer eingegangen.
void udelay (unsigned long mikros)
{
/* busy wait */
long int start_time;
long int time_difference;
struct timespec gettime_now;
mikros = mikros * 1000L;
time_difference = 0;
clock_gettime(CLOCK_REALTIME, &gettime_now);
start_time = gettime_now.tv_nsec; /* Startzeit holen */
while (time_difference <= mikros)
{
clock_gettime(CLOCK_REALTIME, &gettime_now);
time_difference = gettime_now.tv_nsec - start_time;
if (time_difference < 0)
time_difference += 1000000000; /* Ueberlauf bei jeder Sekunde */
}
}
Als letztes wurde noch eine Funktion hinzugefügt, welche die im Folgenden noch öfter
verwendete Funktion gettimeofday() untersucht. In der Funktion gettimeofday_benchmark()
wird gettimeofday() einfach 10 Millionen mal aufgerufen und gemessen, wie lange
das dauert.
void gettimeofday_benchmark()
{
long i;
struct timespec tv_start, tv_end;
struct timeval tv_tmp;
long long diff;
long count = 10000000L;
clockid_t clockid;
clock_getcpuclockid(0, &clockid);
clock_gettime(clockid, &tv_start);
for(i = 0; i < count; i++)
gettimeofday(&tv_tmp, NULL);
clock_gettime(clockid, &tv_end);
diff = (long long)(tv_end.tv_sec - tv_start.tv_sec)*1000000000L;
diff += (tv_end.tv_nsec - tv_start.tv_nsec);
printf("%ld cycles in %lld ns = %.1f ns/cycle\n",
count, diff, (double)diff / (double)count);
}
Das Hauptprogramm ruft nacheinander die oben besprochenen Funktionen auf und
misst deren Laufzeit. Natürlich geht auch der Aufruf von gettimeofday()
nach dem Funktionsaufruf mit in die Messung ein, weshalb mit dem
gettimeofday_benchmark() ermittelt wird, in wie weit das relevant ist.
Damit das Ganze läuft, muss die Realtimebibliothek mit eingebunden werden
(Compileroption -lrt). Dagegen spielen Optimierungsstufen beim
Compilieren keine Rolle.
/* Compile with: gcc -Wall -o pre pre.c -lrt */
#include <sys/time.h>
#include <sys/resource.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
/* Funktionen, siehe oben */
int delay(unsigned long mikros);
void udelay (unsigned long mikros);
void gettimeofday_benchmark();
int main ()
{
struct timeval t1, t2;
long long t;
long microseconds = 999000;
//nanosleep test
gettimeofday(&t1, NULL);
delay(microseconds);
gettimeofday(&t2, NULL);
t = ((t2.tv_sec * 1000000) + t2.tv_usec) - ((t1.tv_sec * 1000000) + t1.tv_usec);
printf("Aufruf von delay(%ld) dauerte %lld us\n", microseconds, t);
//usleep test
gettimeofday(&t1, NULL);
usleep(microseconds);
gettimeofday(&t2, NULL);
t = ((t2.tv_sec * 1000000) + t2.tv_usec) - ((t1.tv_sec * 1000000) + t1.tv_usec);
printf("Aufruf von usleep(%ld) dauerte %lld us\n", microseconds, t);
//udelay test
gettimeofday(&t1, NULL);
udelay(microseconds);
gettimeofday(&t2, NULL);
t = ((t2.tv_sec * 1000000) + t2.tv_usec) - ((t1.tv_sec * 1000000) + t1.tv_usec);
printf("Aufruf von udelay(%ld) dauerte %lld us\n", microseconds, t);
//sleep test
gettimeofday(&t1, NULL);
sleep(1);
gettimeofday(&t2, NULL);
t = ((t2.tv_sec * 1000000) + t2.tv_usec) - ((t1.tv_sec * 1000000) + t1.tv_usec);
printf("Aufruf von sleep(1) dauerte %lld us\n", t);
gettimeofday_benchmark();
return 0;
}
Das Ergebnis des Programmlaufs zeigt, dass man den Aufruf von gettimeofday()
mit seiner Dauer von ca. 0,75 us vernachlässigen kann. Mehrfache Programmaufrufe zeigen
nur geringe Abweichungen in den Zeiten. delay(), das ja auf nanosleep
aufbaut, zeigt ähnliche Zeiten wie usleep() (das auch auf nanosleep
basiert). Nur das "busy waiting" von udelay() kommt der gewünschten Zeit einen
Hauch näher.
pi@raspberrypi ~ $ ./pre Aufruf von delay(999000) dauerte 999138 us Aufruf von usleep(999000) dauerte 999143 us Aufruf von udelay(999000) dauerte 999086 us Aufruf von sleep(1) dauerte 1000378 us 10000000 cycles in 7512165000 ns = 751.2 ns/cycle pi@raspberrypi ~ $ ./pre Aufruf von delay(999000) dauerte 999134 us Aufruf von usleep(999000) dauerte 999129 us Aufruf von udelay(999000) dauerte 999062 us Aufruf von sleep(1) dauerte 1000182 us 10000000 cycles in 7511631000 ns = 751.2 ns/cycle pi@raspberrypi ~ $ ./pre Aufruf von delay(999000) dauerte 999133 us Aufruf von usleep(999000) dauerte 999142 us Aufruf von udelay(999000) dauerte 999064 us Aufruf von sleep(1) dauerte 1000272 us 10000000 cycles in 7510856000 ns = 751.1 ns/cycle pi@raspberrypi ~ $ ./pre Aufruf von delay(999000) dauerte 999137 us Aufruf von usleep(999000) dauerte 999138 us Aufruf von udelay(999000) dauerte 999063 us Aufruf von sleep(1) dauerte 1000183 us 10000000 cycles in 7509742000 ns = 751.0 ns/cycleProgrammläufe mit 99000 oder 9000 Mikrosekunden zeigen übrigens in etwa die selben Abweichungen von der "Wunschzeit".
Ein zweites Testprogramm zeigte einen Unterschied zwischen "busy waiting" und nanosleep(). An einen GPIO-Port des Raspberry Pi wurde ein Piezo-Lautsprecher angeschlossen und per Programm an diesem Port ein Rechtecksignal mit symmetrischem Tastverhältnis ausgegeben (Tonerzeugung). Beim Einsatz von nanosleep() klang das Signal (rein subjektiv) nicht ganz sauber. Mit udelay() hörte es sich sauberer an - soweit sich das mit einem quäkenden Piezo-Lautsprecher von ca. 3 cm Durchmesser feststellen liess. Zusätzlich wurde noch die Prozesspriorität angehoben, weshalb das Programm mit root-Berechtigung starten muss.
Die Funktion set_max_priority() setzt die Prozesspriorität hoch, wozu sie zwei Systemaufrufe verwendet:
void set_max_priority(void)
{
struct sched_param sched;
memset(&sched, 0, sizeof(sched));
/* Use FIFO scheduler with highest priority for the
lowest chance of the kernel context switching. */
sched.sched_priority = sched_get_priority_max(SCHED_FIFO);
sched_setscheduler(0, SCHED_FIFO, &sched);
}
Mehr zu dieser Funktion und zu Prozessen erfahren Sie im Kapitel
Prozess-Priorität.
Das Programm besitzt zwei Kommandozeilenparameter, die gewünschte Frequenz in Hz und die Dauer in Millisekunden. Bei der Fequenz ist wegen der GPIO-Funktionen usw. bei etwa 5000 Hz die OBergrenze des Möglichen erreicht. Ddie Wartezeit zwischen den Pegelwechseln wird aus der Frequenz berechnet. Eine Million Mikrosekunden muss zuerst durch 2 geteilt (wegen der 1- und 0-Phase) und dann durch die Frequenz dividiert werden. Es gilt also: Delayzeit = 500000 / Frequenz. Die Anzahl der auszuführenden Zyklen ergibt sich aus der Frequenz und der gewünschten Dauer des Tons (Divisor 1000 wegen der Angabe der Dauer in Millisekunden): Zyklen = Frequenz * Dauer / 1000. In einer Schleife wird dann der Port abwechselnd ein- und ausgeschaltet und dazwischen jeweils die Delayzeit gewartet:
// Compile with: gcc -Wall -otone -lrt tone.c gpiolib.c
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <fcntl.h>
#include <stdio.h>
#include <sched.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include "gpiolib.h" /* Siehe: RasPi_GPIO_C.html */
#define PORT 25
/* Funktionen siehe oben */
void set_max_priority(void);
void udelay (unsigned long mikros);
int main(int argc, char **argv)
{
long i;
long long t;
struct timeval t1, t2;
if (argc != 3)
{
printf("Aufruf %s Frequenz[Hz] Dauer[ms]\n",argv[0]);
return (3);
}
long frequency = atol(argv[1]);
long duration = atol(argv[2]);
if ((frequency < 1) || (frequency > 5000)) return(4);
/* Port als Ausgang aktivieren */
if (gpio_export(PORT) < 0) return(1);
if (gpio_direction(PORT, OUT) < 0) return(2);
set_max_priority();
/* Parameter berechnen */
long delayvalue = 500000/frequency;
long cycles = frequency * duration / 1000;
/* Ton ausgeben */
gettimeofday(&t1, NULL);
for (i = 0; i < cycles; i++)
{
gpio_write(PORT,HIGH);
udelay(delayvalue);
gpio_write(PORT,LOW);
udelay(delayvalue);
}
gettimeofday(&t2, NULL);
t = ((t2.tv_sec * 1000000) + t2.tv_usec) - ((t1.tv_sec * 1000000) + t1.tv_usec);
/* Messergebnis */
printf("Delay: %ld, Cycles: %ld\n", delayvalue, cycles);
printf("Aufruf dauerte %lld us (%lld us pro Cycle)\n", t, t/cycles);
if (gpio_unexport(PORT) < 0) return(1);
return(0);
}
Auch hier zeigt die Ausgabe Schwankungen, die aber sehr moderat sind und
im Bereich von etwa zwei Prozent liegen:
pi@raspberrypi ~ $ sudo ./tone 1000 1000 Delay: 500, Cycles: 1000 Aufruf dauerte 1189760 us (1189 us pro Cycle) pi@raspberrypi ~ $ sudo ./tone 1000 1000 Delay: 500, Cycles: 1000 Aufruf dauerte 1185369 us (1185 us pro Cycle) pi@raspberrypi ~ $ sudo ./tone 1000 1000 Delay: 500, Cycles: 1000 Aufruf dauerte 1186828 us (1186 us pro Cycle) pi@raspberrypi ~ $ sudo ./tone 1000 1000 Delay: 500, Cycles: 1000 Aufruf dauerte 1189841 us (1189 us pro Cycle)
Aus den bisherigen Experimenten lassen sich folgende Erkenntnisse ableiten:
Falls Ihnen nun jemand erzählt, die Funktion gettimeofday() sei doch für die Angabe von Datum und Uhrzeit zuständig, dann stimmen Sie einfach zu. Das folgende Beispiel zeigt - gewissermassen ausser der Reihe - die profane Anwendung:
#include <stdio.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
int main(void)
{
struct timeval tv;
struct tm* ptm;
char time_string[40];
long milliseconds;
/* Exakte Zeit holen und in einer timeval-Struktur ablegen */
gettimeofday(&tv, NULL);
ptm = localtime(&tv.tv_sec);
/* Formatierung von Datum und Uhrzeit */
strftime(time_string, sizeof (time_string), "%Y-%m-%d %H:%M:%S", ptm);
/* Mikrosekunden --> Millisekunden */
milliseconds = tv.tv_usec / 1000;
/* Ausgabe mit Millisekunden hinter dem Dezimalpunkt */
printf("%s.%03ld\n", time_string, milliseconds);
return 0;
}
Auch der Linux-Kernel hat sich jahrelang auf eine interne, konstant laufende Zeitbasis verlassen. Doch mit der Übernahme des High Resolution Timer Code von Thomas Gleixner und Ingo Molnar ("Hrtimer") gibt es nicht nur eine Basis für hochauflösende Timer, sondern auch für ein recht genaues Zeitverhalten (High Precision Timer).
Ein Problem bei der Zeitzählung im Linux-System war auch die Tatsache, dass bei manuellen Korrekturen an der aktuellen Systemzeit (z. B. mit dem date-Kommando) die Systemuhr einen Sprung macht, was mitunter nicht alle Systemkomponenten gut verkrafteten: Bei einer abrupten Sommerzeitumstellung wird beispielsweise aus einer Sekunde Wartezeit eventuell mehr als eine Stunde. Programme wie ntp und adjtimex können hingegen Zeitanpassungen systemfreundlich vollziehen.
Mit dem neuen Timercode (ab Kernel 2.6.16) funktioniert das Timing besser. Jetzt kann der Programmierer angeben, ob eine Funktion nach einer angegebenen Zeitdauer (also relativ) aufgerufen werden soll oder zu einem bestimmten Zeitpunkt (absolut). Dazu muss er lediglich ein Hrtimer-Objekt reservieren und bei der Initialisierung dieses Objekt einer von zwei Zeitquellen zuweisen.
Die Zeitquelle CLOCK_MONOTONIC zählt die Timer-Ticks im System. Auf einem Linux-PC sind es pro Minute 250 Zählerschritte und der Zähler wächst monoton, selbst wenn man die Uhr zurückstellt. Auf derartige Massnahmen reagiert jedoch die Zeitquelle CLOCK_REALTIME, indem beim Vorstellen der Systemuhr zugeordnete Timerfunktionen länger warten und beim Zurückdrehen der Uhr sich die Wartezeit entsprechend verkürzt.
Das Prinzip aller Timer ist ähnlich. Der Timer löst nach einer bestimmten Zeitspanne (relativ zum Programmstart oder einen frei wählbaren Zeitpunkt oder auch abhängig von der Systemzeit) einen bestimmten Interrupt aus, der bei Linux "Signal" genannt wird (siehe auch man 7 signal). Für dieses Signal wird eine Funktion geschrieben (sogenannter Signalhandler), die einen int-Parameter besitzt und mit Hilfe der Funktion sigaction() für das Signal aktiviert wird.
An der gewünschten Stelle im Hauptprogramm wird dann der Timer auf einen bestimmten Wert gesetzt (entweder nur einmalig oder ständig wiederholend) und nach Ablauf der vorgegebnenen Zeit löst der Timer dann den Interrupt aus und sendet sein spezifisches Signal an den laufenden Prozess, worauf der Signalhandler aktiviert wird. Das geschieht, wie gesagt, unabhängig davon, was der Prozess gerade macht.
Es gibt zwei Timer-Subsysteme in Linux, das "Main Timing Subsystem" und das "High Resolution Timing Subsystem". In ersterem ist die kleinste Zeiteinheit ein "jiffy", das der Dauer eines Ticks des Systemzeitgeber-Interrupt entspricht. Die Anzahl der jiffies pro Sekunde bzw. die Frequenz des "system tick", wird durch die bereits oben erwähnte Konstante HZ festgelegt, die konfigurierbar ist und Werte 100, 250 (Standard), 300 oder 1000 annehmen kann, was einem Jiffies-Wert (1/HZ) von 0.01, 0.004, 0.003 oder 0.001 Sekunden entspricht.
Unter Linux gibt es zwei Interfaces für hoch auflösende Timer, den "Intervall-Timer" (itimer) und den "POSIX-Timer".
Jeder Prozess besitzt drei Intervall-Timer. Nach dem Setzten werden die Timer dekrementiert und sobald ein Timer 0 ist, wird ein spezifischer Alarm an den Prozess gesendet. Die drei Intervall-Timer sind:
Die von den Intervall-Timern verwendeten Strukturen sind:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
struct itimerval {
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
};
Die Timer verringern jeweils nur den Wert it_value. Ist it_value = 0,
also sowohl tv_sec = 0 als auch tv_usec = 0, wird der entsprechende
Alarm ausgelöst. Danach wird it_value wieder auf den Timer Reset-Wert
it_interval gesetzt.
Die Intervall-Timer werden über zwei Funktionen angesprochen:
/* Timer setzen und starten */
int setitimer (int which, const struct itimerval *new_value,
struct itimerval *old_value);
/* Timerwert auslesen */
int getitimer (int which, struct itimerval *curr_value);
Der erste Parameter which bezeichnet einen Zeitgeber, ITIMER_REAL, ITIMER_VIRTUAL
oder ITIMER_PROF.
Die Funktion settimer() setzt den Timer mit dem Parameter new_value. Lief
der Timer bereits, werden die aktuellen Werte von it_value und it_interval
in den Parameter old_value kopiert, bevor der Timer die neuen Werte übernimmt.
Nach dem Anruf von getitimer() erhält den Parameter curr_value den
Stand des Timers.
Das folgende Programm zeigt die Verwendung, um die Ausführungszeit eines Programms
zu verfolgen. Ein Timer ist so konfiguriert, dass er alle 500 Millisekunden abläuft
und ein SIGVTALRM sendet.
Mehr über Signale und das Einrichten des Signal-Handlers wird im Programmierskript Signale in C anhand der Alarmfunktion beschrieben. Die dort beschriebene Funktion alarm() ist gewissermaßen ein einfacher Timer, dessen Timeout nur in vollen Sekunden angegeben werden kann.
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
void timer_handler (int signum)
{
/* Ganz einfacher Signalhandler */
static int count = 0;
printf("Timer abgelaufen, Zähler: %d\n", ++count);
}
int main(void)
{
struct sigaction sa;
struct itimerval timer;
/* Installiere timer_handler als Signal Handler fuer SIGVTALRM. */
memset(&sa, 0, sizeof (sa));
sigemptyset(&sa.sa_mask);
sa.sa_handler = &timer_handler;
sigaction(SIGVTALRM, &sa, NULL);
/* Timer konfigurieren fuer 500 ms ... */
timer.it_value.tv_sec = 0;
timer.it_value.tv_usec = 500000;
/* ... und alle 500 ms danach */
timer.it_interval.tv_sec = 0;
timer.it_interval.tv_usec = 500000;
/* Timer starten */
setitimer(ITIMER_VIRTUAL, &timer, NULL);
/* Mach irgendwas Wichtiges */
while(1);
return 0;
}
Die Ausgabe dazu zeigt alle halbe Sekunden, dass der Zähler hochgezählt wird. Das
Programm wurde dann mittels [Strg][C] beendet - übrigens auch ein Signal.
pi@raspberrypi ~ $ ./itimer Timer abgelaufen, Zähler: 1 Timer abgelaufen, Zähler: 2 Timer abgelaufen, Zähler: 3 Timer abgelaufen, Zähler: 4 Timer abgelaufen, Zähler: 5 ^CDie Timer-Interrupt-Frequenz ist ein wichtiger Parameter für Anwendungen unter Linux, die kurze Reaktionszeiten benötigen. Der Begriff "kurz" bedeutet in diesem Zusammenhang, dass die Frequenz in der Regel größer als 100 Hz (10 ms) sein sollte. Früher war die Kernel-Konstante CONFIG_HZ die wichtigste Einstellung für die Timer-Frequenz des Linux-Kerns. Heute spielt eine ganze Reihe von Kernel-Einstellungen eine Rolle, z. B. NO_HZ, HIGH_RES_TIMERS und andere. Die Timer-Interrupt-Frequenz kann recht gut mit dem Intervall-Timer abgeschätzt werden. Das folgende kleine Beispielprogramm zeigt einen Algorithmus, der die tatsächliche erreichbare Timer-Interrupt-Frequenz unter Verwendung bestimmt.
/* Compile with: gcc -Wall -o getres -lrt getres.c */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
#define USECREQ 250
#define LOOPS 1000
/* Signalhandler fuer den Timer */
void event_handler (int signum)
{
static unsigned long cnt = 0;
static struct timeval tsFirst;
struct timeval tsNow;
struct timeval diff;
unsigned long long udiff;
double delta;
unsigned int hz;
if (cnt == 0)
/* Anfangszeit */
gettimeofday (&tsFirst, 0);
cnt ++;
if (cnt >= LOOPS)
{
/* Timer stoppen und Statistik ausgeben */
setitimer (ITIMER_REAL, NULL, NULL);
/* Endezeit */
gettimeofday (&tsNow, 0);
/* Zeitdifferenz und daraus Frequenz berechnen */
timersub(&tsNow, &tsFirst, &diff);
udiff = (diff.tv_sec * 1000000) + diff.tv_usec;
delta = (double)(udiff/cnt)/1000000;
hz = (unsigned)(1.0/delta);
/* Ausgabe und Ende */
printf("Kernel-Timer Interruptfrequenz ist ca. %d Hz", hz);
if (hz >= (unsigned)(1.0/((double)(USECREQ)/1000000)))
printf(" oder hoeher");
printf("\n");
exit(0);
}
}
int main(void)
{
struct timespec clock_resolution;
struct sigaction sa;
struct itimerval timer;
/* Installiere timer_handler als Signal Handler fuer SIGVALRM. */
memset(&sa, 0, sizeof (sa));
sigemptyset(&sa.sa_mask);
sa.sa_handler = &event_handler;
sa.sa_flags = SA_NOMASK;
sigaction(SIGALRM, &sa, NULL);
/* Timer konfigurieren */
timer.it_value.tv_sec = 0;
timer.it_value.tv_usec = USECREQ;
timer.it_interval.tv_sec = 0;
timer.it_interval.tv_usec = USECREQ;
/* Timer starten */
setitimer (ITIMER_REAL, &timer, NULL);
/* Mach irgendwas Wichtiges */
while(1);
return 0;
}
Beim Raspberry Pi lautet die Ausgabezeile:
Kernel-Timer Interruptfrequenz ist ca. 4016 Hz oder hoeherACHTUNG: alarm() und setitimer() verwenden den gleichen Timer. Die Aufrufe der Funktionen führen also zu seltsamen Interferenzen. Übrigens verwendet auch sleep() das Signal SIGALRM, was ebenfalls zu ungewollten Effekten führen kann. Auch sollten in produktiven Systemen Funktionen wie printf() (die hier zur Demonstration dienen) im Timer-Handler nicht verwendet werden, weil sie nicht sicher in Bezug auf asynchrone Signale sind.
Wird der Timer mit der Zeit 0 besetzt, stoppt er. Im folgenden Listing sind zwei Funktionen start_timer() und stop_timer()definiert, welche die Anwendung etwas vereinfachen. start_timer() setzt den itimer auf die angegebene Zeit in Millisekunden. Als zweiter Parameter wird die Adresse der Timmer-Handler-Funktion übergeben. Der dritte Parameter leifert die urspruengliche Adresse des Handlers fuer SIGALRM zurück, die in old_handler gespeichert wird. Die zweite Funktion stop_timer() hölt den Timer an und restaurieren die Adresse des urspruenglichen Handlers fuer SIGALRM, die als Parameter übergeben wird. Das Hauptprogramm demonstriert die Anwendung.
#include <sys/time.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
/* globale Variablen fuer den itimer */
struct sigaction old_handler;
/* Funktion, die beim Timer-Interrupt aufgerufen wird */
int var = 0;
void timer_handler(void)
{
printf("timer: var is %i\n", var++);
}
/* Setzt den itimer auf die angegebene Zeit
* mSec: Timer-Zeit in Millisekunden
* timer_handler: Adresse der Timmer-Handler-Funktion
* Die urspruengliche Adresse des Handlers fuer SIGALRM
* wird in old_handler gespeichert.
*/
int start_timer(int mSec, void *timer_handler, struct sigaction *old_handler)
{
struct itimerval timervalue;
struct sigaction new_handler;
timervalue.it_interval.tv_sec = mSec / 1000;
timervalue.it_interval.tv_usec = (mSec % 1000) * 1000;
timervalue.it_value.tv_sec = mSec / 1000;
timervalue.it_value.tv_usec = (mSec % 1000) * 1000;
if(setitimer(ITIMER_REAL, &timervalue, NULL))
{
/* setitimer() error */
return(1);
}
memset(&new_handler, 0, sizeof (new_handler));
sigemptyset(&new_handler.sa_mask);
new_handler.sa_handler = (void *)timer_handler;
new_handler.sa_flags = SA_NOMASK;
if(sigaction(SIGALRM, &new_handler, old_handler))
{
/* sigaction() error */
return(2);
}
return(0);
}
/* Anhalten des Timers, restaurieren des urspruenglichen
* Handlers fuer SIGALRM
*/
void stop_timer(struct sigaction *old_handler)
{
struct itimerval timervalue;
timervalue.it_interval.tv_sec = 0;
timervalue.it_interval.tv_usec = 0;
timervalue.it_value.tv_sec = 0;
timervalue.it_value.tv_usec = 0;
setitimer(ITIMER_REAL, &timervalue, NULL);
sigaction(SIGALRM, old_handler, NULL);
}
int main(void)
{
if(start_timer(500, &timer_handler, &old_handler))
{
printf("\n timer error\n");
return(1);
}
while(var <= 10)
{
/* Mach was ganz Wichtiges */
}
stop_timer(&old_handler);
return(0);
}
Mit den POSIX-kompatiblen Timerfunktionen haben Sie weiter oben bei der Zeitmessung schon Bekannschaft geschlossen. Zur Erinnerung sind hier nochmals die Typen aufgeführt:
/* Timer-Auflösung ermitteln */ int clock_getres (clockid_t clk_id, struct timespec *res); /* Timerstand auslesen */ int clock_gettime (clockid_t clk_id, struct timespec *tp); /* Timer setzen und starten */ int clock_settime (clockid_t clk_id, const struct timespec *tp);Die Parameter aller Funktionen sind gleich: clk_id bezeichnet einen der oben aufgelisteten Timer und tp ist ein Zeiger auf eine Variable/Konstante vom Typ struct timespec:
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
clock_getres liefert, wie oben schon zu sehen war, die Auflösung des
angegebenen Timers. Das wird im folgenden Beispiel genutzt, um zu sehen, welche
Timer unterstützt werden und wie hoch deren Auflösung ist:
/* Compile with: gcc -Wall -o getres2 -lrt getres2.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
int main(void)
{
struct timespec res;
if (clock_getres(CLOCK_REALTIME, &res) == -1)
perror("clock_getres: CLOCK_REALTIME");
else
printf("CLOCK_REALTIME: %ld s, %ld ns\n", res.tv_sec, res.tv_nsec);
if (clock_getres(CLOCK_MONOTONIC, &res) == -1)
perror("clock_getres: CLOCK_MONOTONIC");
else
printf("CLOCK_MONOTONIC: %ld s, %ld ns\n", res.tv_sec, res.tv_nsec);
if (clock_getres(CLOCK_PROCESS_CPUTIME_ID, &res) == -1)
perror("clock_getres: CLOCK_PROCESS_CPUTIME_ID");
else
printf("CLOCK_PROCESS_CPUTIME_ID: %ld s, %ld ns\n", res.tv_sec, res.tv_nsec);
if (clock_getres(CLOCK_THREAD_CPUTIME_ID, &res) == -1)
perror("clock_getres: CLOCK_THREAD_CPUTIME_ID");
else
printf("CLOCK_THREAD_CPUTIME_ID: %ld s, %ld ns\n", res.tv_sec, res.tv_nsec);
return 0;
}
Beim Raspberry Pi stehen alle mit gleicher Auflösung zur Verfügung:
pi@raspberrypi ~ $ ./getres2 CLOCK_REALTIME: 0 s, 1 ns CLOCK_MONOTONIC: 0 s, 1 ns CLOCK_PROCESS_CPUTIME_ID: 0 s, 1 ns CLOCK_THREAD_CPUTIME_ID: 0 s, 1 nsDie Anwendung der Funktion clock_gettime() wurde ja schon bei der Zeitmessung demonstriert.
POSIX kennt aber noch weitere Timer-Funktionen. Die klassischen Intervall-Timer leiden ja unter einer Reihe von Einschränkungen:
int timer_create (clockid_t clockid, struct sigevent *evp,
timer_t *timerid);
int timer_delete (timer_t timerid);
int timer_settime (timer_t timerid, int flags,
const struct itimerspec *new_value,
struct itimerspec *old_value);
int timer_gettime (timer_t timerid, struct itimerspec *curr_value);
int timer_getoverrun (timer_t timerid);
Die Funktion timer_create erzeugt einen Timer.
Als clockid kann einer der Werte CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_PROCESS_CPUTIME_ID
oder CLOCK_THREAD_CPUTIME_ID angegeben werden; alternativ auch der Rückgabewert von
clock_getcpuclockid(). Der Zeiger auf die Struktur sigevent erlaubt es,
das Signal anzugeben, das ausgelöst wird, sobald der Timer abläuft. Wird hier NULL übergeben,
wird SIGALRM verwendet. Der Parameter timerId zeigt auf einen Handle vom Typ
timer_t, der es erlaubt, sich in weiteren Funktionen auf den Timer zu beziehen.
Der Parameter evp legt fest, wie der Prozess benachrichtigt werden soll, wenn der
Timer abläuft. Es verweist auf eine etwas komplexe Struktur vom Typ sigevent, die
wie folgt definiert ist (die defines am Schluss sollen das Leben dann etwas einfacher machen):
union sigval {
int sival_int; /* Integer value for accompanying data */
void *sival_ptr; /* Pointer value for accompanying data */
};
struct sigevent {
int sigev_notify; /* Notification method */
int sigev_signo; /* Timer expiration signal */
union sigval sigev_value; /* Value accompanying signal or
passed to thread function */
union {
pid_t _tid; /* ID of thread to be signaled /
struct {
void (*_function) (union sigval);
/* Thread notification function */
void *_attribute; /* Really 'pthread_attr_t *' */
} _sigev_thread;
} _sigev_un;
};
#define sigev_notify_function _sigev_un._sigev_thread._function
#define sigev_notify_attributes _sigev_un._sigev_thread._attribute
#define sigev_notify_thread_id _sigev_un._tid
Die Komponenten sigev_notify und sigev_signo steuern
das Verhalten des Timers.
Die Funktion timer_settime() startet oder stoppt einen Timer. Der ParametertimerId ist ein Timer-Handle, der von einen vorherigen Aufruf von timer_create() zurückgegeben wurde. Die Parameter new_value und old_value entsprechen jenen des Intervalltimers (setitimer()-Funktion). new_value enthält die neuen Einstellungen, old_value gibt die vorherige Timer-Einstellungen zurück. Hier kann auch NULL angegeben werden. Wird für flags 0 angegeben, dann sind die Timersettigs relativ zum Zeitpunkt des Anrufs von timer_settime() (wie bei setitimer()). Wird flags spezifiziert als TIMER_ABSTIME dann werden die Settings als Absolutzeit interpretiert (d. h. gemessen vom Nullpunkt des relevanten Clock). Wenn diese Zeit schon erreicht wurde, läuft der Timer sofort ab. Um den Timer zu stoppen, wird timer_settime() mit 0 für alle Zeitangabe aufgerufen.
Mit der Funktion timer_gettime() kann man das Intervall und die verbleibende Zeit über die timerId ermitteln. Das Intervall und die Zeit bis zum nächsten Ablauf des Timers werden vom Zeiger curr_value auf eine Struktur itimerspec zurückgegeben. curr_value.it_valueliifert dabei immer die Zeit bis zum nächsten Timerablauf, auch wenn dieser Timer mit TIMER_ABSTIME definiert wurde. Sind beide it_value-Werte 0, war der Timer nicht aktiviert. Sind beide it_interval-Werte 0, läuft der Timer nur einmal.
Jedes Timer verbraucht etwas Systemressourcen. Daher sollte man, wenn der Timer nicht mehr benötigt wird, diese Ressourcen mit Hilfe von timer_delete() freigeben. Der Parameter timerId identifiziert den Timer. War der Timer noch aktiv, wird er automatisch vor dem Entfernen beendet. Ein bereits anstehendes Signal von einem abgelaufenen Timer bleibt bestehen. Alle Timer eines Prozesses werden bei dessen Ende gelöscht.
Bleibt noch die Funktion timer_getoverrun(). Wird ein Timer aktiviert, sendet er nach Ablauf seiner Timerperiode das entsprechende Signal. Zwischen dem Ablauf des Timers und der Verarbeitung des Signals könnte aber eine gewisse Zeit vergehen und inzwischen der Timer wieder ablaufen (eventuell sogar vielfach). Für diesen Fall werden keine weiteren Signale erzeugt. Vielmehr gibt die Abfrage via timer_getoverrun() die Anzahl der Timer-Abläufe zurück, die seit dem letzten Behandeln des Signals erfolgten.
Das folgende Beispiel demonstriert den Einsatz von Posix-Timern. Die Funktion start_timer() installiert einen Timer mit Hilfe von timer_create() und timer_settime(), wobei zur Vereinfachung Start- und Wiederholungs-Delay gleich sind und die Angabe in Millisekunden erfolgen darf. Nachdem festgestellt wurde, dass die maximale Taktrate bei 4,096 ms liegt ist eine Genauigkeit von Nanosekunden sowieso nicht nötig. Falls im Programm irgendwann ein Timer gestoppt werden soll, kann dies mittels stop_timer() erfolgen.
Bei diesen Timern gibt timer_create() eine eindeutige ID zurück, die von allen anderen Funktionen verwendet wird, um den Timer zu identifizieren. Das ermögliche es, mehrere Timer laufen zu lassen. Im Programm sind dies zwei Timer, wobei der erste einen Takt von 1 Sekunde hat, der zweite ist doppelt so schnell. Da beide Timer ein SIGALRM senden, muss der Signal-Handler timer_callback() irgendwie unterscheiden können, von welchen Timer er ausgelöst wurde. Deshalb wird die Variante mit drei Parametern verwendet, bei der man über tidp = si->si_value.sival_ptr; die Timer-ID ermitteln kann. Alles weitere ist dann relativ einfach. Auch bei diesem Programm muss die rt-Library hinzugeladen werden.
/* Compile with: gcc -Wall -o timer1 -lrt timer1.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <signal.h>
timer_t Timerid1;
timer_t Timerid2;
int count1 = 0;
int count2 = 0;
/* Timer erzeugen und starten
* Timerid: die zurueckgegebene ID des Timers
* sek: Wartezeit Sekundenanteil
* msek: Wartezeit Millisekundenanteil
*/
void start_timer(timer_t *Timerid, int sek, int msek)
{
struct itimerspec timer;
timer.it_value.tv_sec = sek;
timer.it_value.tv_nsec = msek * 1000000;
timer.it_interval.tv_sec = sek;
timer.it_interval.tv_nsec = msek * 1000000;
timer_create (CLOCK_REALTIME, NULL, Timerid);
timer_settime (*Timerid, 0, &timer, NULL);
printf("Timer gestartet, ID: 0x%lx\n", (long) *Timerid);
}
/* Anhalten eines durch Timerid identifizierten Timers
* durch Setzen aller Zeiten auf 0
*/
void stop_timer(timer_t *Timerid)
{
struct itimerspec timer;
timer.it_value.tv_sec = 0;
timer.it_value.tv_nsec = 0;
timer.it_interval.tv_sec = 0;
timer.it_interval.tv_nsec = 0;
timer_settime (*Timerid, 0, &timer, NULL);
}
/* Signalhandler für alle Timer
* Unterscheidung der Timer anhand von tidp
*/
void timer_callback(int sig, siginfo_t *si, void *uc)
{
timer_t *tidp;
tidp = si->si_value.sival_ptr;
printf("Signal: %d, ID: %p ", sig, tidp);
if (tidp == Timerid1)
printf(", Count 1: %d\n",count1++);
if (tidp == Timerid2)
printf(", Count 2: %d\n",count2++);
}
int main(void)
{
struct sigaction sa;
/* cllback-Handler installieren */
memset(&sa, 0, sizeof (sa));
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = timer_callback;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGALRM, &sa, NULL) == -1)
perror("sigaction");
/* timer starten */
start_timer(&Timerid1,1,0);
start_timer(&Timerid2,0,500);
/* Programm macht irgendwas */
while(count1 <= 5);
/* Fertig, Timer stoppen */
stop_timer(&Timerid2);
stop_timer(&Timerid1);
return 0;
}
Der Output zeigt erwartungsgemäss, dass Timer 2 doppelt so oft zuschlägt und
dass die Funktion timer_callback() auch zwischen beiden Signalquellen
unterscheiden kann.
pi@raspberrypi ~ $ ./timer1 Timer gestartet, ID: 0x55d008 Timer gestartet, ID: 0x55d018 Signal: 14, ID: 0x55d018 , Count 2: 0 Signal: 14, ID: 0x55d008 , Count 1: 0 Signal: 14, ID: 0x55d018 , Count 2: 1 Signal: 14, ID: 0x55d018 , Count 2: 2 Signal: 14, ID: 0x55d008 , Count 1: 1 Signal: 14, ID: 0x55d018 , Count 2: 3 Signal: 14, ID: 0x55d018 , Count 2: 4 Signal: 14, ID: 0x55d008 , Count 1: 2 Signal: 14, ID: 0x55d018 , Count 2: 5 Signal: 14, ID: 0x55d018 , Count 2: 6 Signal: 14, ID: 0x55d008 , Count 1: 3 Signal: 14, ID: 0x55d018 , Count 2: 7 Signal: 14, ID: 0x55d018 , Count 2: 8 Signal: 14, ID: 0x55d008 , Count 1: 4 Signal: 14, ID: 0x55d018 , Count 2: 9 Signal: 14, ID: 0x55d018 , Count 2: 10 Signal: 14, ID: 0x55d008 , Count 1: 5Sie könnten in der Callback-Routine prüfen, ob ein Timerüberlauf aufgetreten ist. Dazu wird die Funktion im eine int-Variable or ergänzt und es werden vor dem Ende der Funktion die folgenden Zeilen eingefügt:
or = timer_getoverrun(tidp);
if (or > 0)
printf(" Overrun! count = %d\n", or);
Das Hauptprogramm wird um die zeitweise Blockierung der Timer ergänzt:
int main(void)
{
struct sigaction sa;
sigset_t mask;
/* callback-Handler installieren */
memset(&sa, 0, sizeof (sa));
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = timer_callback;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGALRM, &sa, NULL) == -1)
perror("sigaction");
/* timer starten */
start_timer(&Timerid1,1,0);
start_timer(&Timerid2,0,500);
/* Programm macht irgendwas */
/* NEU: Block timer signal temporarily */
printf("Blocking signal SIGALRM\n");
sigemptyset(&mask);
sigaddset(&mask, SIGALRM);
sigprocmask(SIG_SETMASK, &mask, NULL);
sleep(5);
printf("NEU: Unblocking signal SIGALRM\n");
sigprocmask(SIG_UNBLOCK, &mask, NULL);
while(count1 <= 5)
{
}
/* Fertig, Timer stoppen */
stop_timer(&Timerid2);
stop_timer(&Timerid1);
return 0;
}
Die Ausgabe zeigt die Reaktion auf die Blockade:
pi@raspberrypi ~ $ ./timer2
Timer gestartet, ID: 0x1a67008
Timer gestartet, ID: 0x1a67018
Blocking signal SIGALRM
Unblocking signal SIGALRM
Signal: 14, ID: 0x1a67018 , Count 2: 0
Overrun! count = 9
Signal: 14, ID: 0x1a67008 , Count 1: 0
Overrun! count = 4
Signal: 14, ID: 0x1a67018 , Count 2: 1
Signal: 14, ID: 0x1a67008 , Count 1: 1
Signal: 14, ID: 0x1a67018 , Count 2: 2
Signal: 14, ID: 0x1a67018 , Count 2: 3
Signal: 14, ID: 0x1a67008 , Count 1: 2
Signal: 14, ID: 0x1a67018 , Count 2: 4
Signal: 14, ID: 0x1a67018 , Count 2: 5
Signal: 14, ID: 0x1a67008 , Count 1: 3
Signal: 14, ID: 0x1a67018 , Count 2: 6
Signal: 14, ID: 0x1a67018 , Count 2: 7
Signal: 14, ID: 0x1a67008 , Count 1: 4
Signal: 14, ID: 0x1a67018 , Count 2: 8
Signal: 14, ID: 0x1a67018 , Count 2: 9
Signal: 14, ID: 0x1a67008 , Count 1: 5
Denken Sie auch daran, dass Sie den Signal-Handler schnell und effizient halten
sollten, wie bei Unterbrechungen im Kernel. Auch sind etliche C-Funktionen nicht
reentrant und gehören daher nicht in einen Signal-Handler (so sind die Beispiele
mit printf() eigentlich ungünstig. Wenn der Signal-Handler etwas
ausgeben soll, nehmen Sie z. B. write(). Auch muss nicht alles, was eine
längere Zeit in Anspruch nimmt, im Signal-Handler abgearbeitet werden. Oft
reicht das Setzen eines Flags, das dann im Hauptprogramm bearbeitet wird.