Raspberry Pi: Sleep- und Timerfunktionen in C

Uncategorized

Raspberry Pi: Sleep- und Timerfunktionen in C

Prof. 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.

Zeitmessung

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
#include
#include
#include

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 usn”, 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:
CLOCK_REALTIME
System-wide realtime clock. Setting this clock requires appropriate privileges.
CLOCK_MONOTONIC
Clock that cannot be set and represents monotonic time since some unspecified starting point.
CLOCK_PROCESS_CPUTIME_ID
High-resolution per-process timer from the CPU.
CLOCK_THREAD_CPUTIME_ID
Thread-specific CPU-time clock.

Für die Zeitmessung nimmt man am Besten CLOCK_REALTIME_ID. Das Testprogramm gleicht
nahezu dem vorhergehenden, nur dass eben in Nanosekunden gerechnet wird. Beim Compilieren muss die
Realtime-Bibliothek hinzugebunden werden (Parameter -lrt).

/* Compile with: gcc -Wall -o timediff2 timediff2.c -lrt */
#include
#include
#include
#include
#include

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 Nanosekundenn”,
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 nsn”, 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
#include
#include
#include
#include
#include

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 Sekundenn”,
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: %ldn", tms0.tms_utime, tms0.tms_stime, tms0.tms_cutime, tms0.tms_cstime); printf("times 1 - usr: %ld sys: %ld ch-usr: %ld ch-sys: %ldn", tms1.tms_utime, tms1.tms_stime, tms1.tms_cutime, tms1.tms_cstime); /* Ausgabe in Sekunden */ printf("user: %lf system: %lfn", tbase*(tms1.tms_utime - tms0.tms_utime), tbase*(tms1.tms_stime - tms0.tms_stime)); return 0; } Die Programmausgabe auf dem Raspberry Pi lautet: [email protected] ~ $ ./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

Sleep-Funktionen

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/cyclen", 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
#include
#include
#include
#include
#include

/* 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 usn”, 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 usn”, 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 usn”, 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 usn”, 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.

[email protected] ~ $ ./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

[email protected] ~ $ ./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

[email protected] ~ $ ./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

[email protected] ~ $ ./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/cycle

Programmlä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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#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: %ldn", 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: [email protected] ~ $ sudo ./tone 1000 1000 Delay: 500, Cycles: 1000 Aufruf dauerte 1189760 us (1189 us pro Cycle) [email protected] ~ $ sudo ./tone 1000 1000 Delay: 500, Cycles: 1000 Aufruf dauerte 1185369 us (1185 us pro Cycle) [email protected] ~ $ sudo ./tone 1000 1000 Delay: 500, Cycles: 1000 Aufruf dauerte 1186828 us (1186 us pro Cycle) [email protected] ~ $ 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:

  • Die Sleep-Funktionen suspendieren den Prozess immer etwas länger, als es
    der Aufrufparameter vorgibt. Das steht übrigens auch in der Dokumentation,
    in der immer von einer Mindest-Wartezeit gesprochen wird. In der Regel
    dauert es etwa 135 bis 145 Mikrosekunden länger.
  • “Busy Wait” liefert etwas exaktere Werte, hier sind es ca. 70 Mikrosekunden.
  • Aufeinanderfolgende Aufrufe der Sleep-Funktionen haben immer eine leicht
    unterschiedliche Wartezeit, die im Bereich von 2 – 3 Prozent schwankt.

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
#include
#include
#include

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.%03ldn”, 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.

Timerfunktionen in C

Der Vorteil von timergesteuerten Funktionen liegt darin, dass sie per Interrupt aufgerufen
werden und damit unabhängig vom Ablauf des Codes in main() sind. Gerade bei der
Erfassung von Messwerten und anderen Aktionen, die in Quasi-Echtzeit ablaufen sollen,
kann man mit den Timern unter Linux viel erreichen, auch wenn es sich bei Linux nicht
um ein Echtzeitsystem handelt.

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”.

Der Intervall-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:

ITIMER_REAL
wird in Realzeit (Uhr, “wall clock”) dekrementiert. Erreicht er 0, wird das
Signal SIGALRM zum Prozess gesendet.
ITIMER_VIRTUAL
dekrementiert in der Laufzeit des Prozesses. Das heißt, er dekrementiert,
wenn der Prozess im Usermode ausgeführt wird. Die Zeit, in der der Prozess nicht
ausgeführt wird (wenn der Kernel oder ein anderer Prozess laufen) wird nicht gezählt.
Erreicht er 0, wird das Signal SIGVTALRM an den Prozess gesendet.
ITIMER_PROF
dekrementiert, wenn der Prozess im Usermode ausgeführt wird, aber auch
während der Ausführung von System-Calls des Prozesses. Erreicht er 0, wird das Signal
SIGPROF zum Prozess gesendet. ITIMER_PROF ist für die Verwendung in Profiler-Programmen
gedacht.

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
#include
#include
#include

void timer_handler (int signum)
{
/* Ganz einfacher Signalhandler */
static int count = 0;
printf(“Timer abgelaufen, Zähler: %dn”, ++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.

[email protected] ~ $ ./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
^C

Die 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
#include
#include
#include
#include
#include
#include

#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 hoeher

ACHTUNG: 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
#include
#include
#include

/* 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 %in”, 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 errorn”);
return(1);
}

while(var <= 10) { /* Mach was ganz Wichtiges */ } stop_timer(&old_handler); return(0); }

POSIX-Timerfunktionen

Mit den POSIX-kompatiblen Timerfunktionen haben Sie weiter oben bei der Zeitmessung
schon Bekannschaft geschlossen. Zur Erinnerung sind hier nochmals die Typen aufgeführt:

CLOCK_REALTIME
Dies ist die systemweite Realtime Clock, die vom Superuser gesetzt werden kann.
CLOCK_MONOTONIC
Dies ist auch ein systemweiter Timer. Es kann nicht gesetzt werden und zählt monoton
ab einem unspezifizierten Startpunkt hoch.
CLOCK_PROCESS_CPUTIME_ID
Prozessspezifischer Timer der CPU.
CLOCK_THREAD_CPUTIME_ID
Threadspecifischer Timer der CPU.

CLOCK_REALTIME existiert in in allen UNIX- und Linux-Implementierungen. Die Verfügbarkeit der
anderen Timer ist von der Umsetzung im jeweiligen Kernel abhängig. Die Timer-Systemaufrufe
lauten (Headerfile time.h):

/* 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
#include
#include
#include
#include

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 nsn”, 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 nsn”, 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 nsn”, 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 nsn”, res.tv_sec, res.tv_nsec);

return 0;
}

Beim Raspberry Pi stehen alle mit gleicher Auflösung zur Verfügung:

[email protected] ~ $ ./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 ns

Die 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:

  • Es kann nur ein Timer von jedem der drei o. g. Typen verwendet werden.
  • Die einzige Möglichkeit, die über den Timer-Ablauf informiert, ist ein einziges
    Signal, SIGALRM.
  • Auch wenn ein Intervall-Timer mehrmals abläuft, während das entsprechende Signal
    blockiert ist, dann wird der Signal-Handler nur einmal aufgerufen.
  • Der Timer ist auf Mikrosekunden-Auflösung beschränkt.

Die POSIX-Funktionen erweitern die Timer-Anwendung.

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.
SIGEV_NONE
Der Ablauf des Timers wird nicht gemeldet (kein Signal). Der Prozess kann
weiterhin den Fortschritt des Timers mittels timer_gettime() verfolgen.
SIGEV_SIGNAL
Wenn der Timer abgelaufen ist, wird das in sigev_signo angegebene Signal
an den Prozess gesendet. Handelt es sich um ein Echtzeit-Signal, dann begleiten die
in sigev_value stehenden Daten (ein Integer oder ein Zeiger) das Signal.
Diese Daten können über si_value der siginfo_t-Struktur abgerufen
werden.
SIGEV_THREAD
Wenn der Timer abläuft, wird die Funktion aufgerufen, auf die sigev_notify_function
zeigt. Diese Funktion wird aufgerufen, als wäre sie die Start-Funktion in einem neuen Thread.
Das Signal könen entweder nach jedem Interrupt an einen neuen Thread gesendet werden
oder es wird nur ein neuer Thread erzeugt, der die periodischen Mitteilungen empfängt.
sigev_notify_attributes kann entweder mit NULL besetzt werden oder als Zeiger auf
eine Struktur pthread_attr_t, welche die Attribute für den Thread enthält.
sigev_value wird als einziger Parameter an die Funktion übergeben.
SIGEV_THREAD_ID
Dies ist ähnlich zu SIGEV_SIGNAL, aber das Signal wird an den Thread gesendet, dessen
Thread-ID übereinstimmt mit sigev_notify_thread_id. Dieser Thread muss zum gleichen
Prozess gehören.

Im einfachsten Fall kann der Parameter evp Argument als NULL angegeben werden, was den
folgenden Angaben entspricht: sigev_notify = SIGEV_SIGNAL, sigev_signo = SIGALRM
und sigev_value.sival_int ist gleich der Timer-ID (die Signalnummer kann auf anderen
Systemen auch etwas Anderes als SIGALRM sein).

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
#include
#include
#include
#include
#include

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%lxn”, (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: %dn”,count1++);
if (tidp == Timerid2)
printf(“, Count 2: %dn”,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. [email protected] ~ $ ./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: 5 Sie 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 = %dn”, 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 SIGALRMn”);
sigemptyset(&mask);
sigaddset(&mask, SIGALRM);
sigprocmask(SIG_SETMASK, &mask, NULL);

sleep(5);

printf(“NEU: Unblocking signal SIGALRMn”);
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: [email protected] ~ $ ./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.

Links

Copyright © Hochschule München, FK 04, Prof. Jürgen Plate und die Autoren
Letzte Aktualisierung:

Source

Sharing is caring!

Leave a Reply