Raspberry Pi: Sleep- und Timerfunktionen in C

Prof. Jürgen Plate

Raspberry Pi: Sleep- und Timerfunktionen in C

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

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

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 <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
^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 <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 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 <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);
  }

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

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 <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: 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 = %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.

Links


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