Aus Linux-Magazin 08/2016

Kernel- und Treiberprogrammierung mit dem Linux-Kernel – Folge 87

© psdesign1, Fotolia

Zeit exakt zu messen und definierte Pausen zu verordnen, das gehört zum Wesen von Realtime-Betriebssystemen. Aber auch universelle Betriebssysteme brauchen gute Timer- und Delay-Prozeduren. Welche Funktionen mit welcher Auflösung und Genauigkeit Linux bietet, ist Thema dieser Kern-Technik.

“Die Zeit ist Amm’ und Mutter alles Guten”, weiß Proteus in “Die beiden Veroneser” zu berichten; für moderne Software gilt die Shakespearische Weisheit nicht minder. Linux nennt auf Anfrage die aktuelle Zeit und vermittelt der Software so etwas wie ein Zeitgefühl. Obwohl konzeptionell anfangs nicht vorgesehen, besitzt der Kernel zudem Realzeit-Fähigkeiten.

In dieser Kern-Technik soll es darum gehen, dass Linux Software anbietet, um einen Prozess im Rahmen der Zeitsteuerung für ein gewünschtes Intervall zu pausieren. In vielen Fällen bilden für Programmierer die gebotene Auflösung und die erzielte Genauigkeit die wesentlichen Kriterien. Prinzipiell löst Linux im Nanosekunden-Bereich auf und verspricht eine Genauigkeit im Bereich von Mikrosekunden. Prinzipiell. Das Ganze ist abhängig von der Hardware, aber auch davon, wie man’s macht.

In puncto Zeitmessung und -steuerung hat sich auf dem Weg von Unix zu Linux in den Jahren einiges getan, insbesondere ist der Umgang mit Zeit professioneller geworden. Statt der alten Funktion »time()« verwenden Entwickler von Applikationen heute »clock_gettime()« und »clock_nanosleep()« anstelle von »sleep()« (siehe Kasten “Schlafkur für Applikationen”). Die modernen Funktionen berücksichtigen über so genannte Zeitgeber beispielsweise das Drehen an der Uhr, etwa bei der Sommer-Winterzeit-Umstellung.

Im Kernel tickt die Welt etwas komplizierter [1]. Hier gibt es mehrere Ebenen und damit unterschiedliche Kontexte, die den Code abarbeiten (Abbildung 1). Abhängig vom Kontext stehen einige Funktionen nicht zur Verfügung. Die Funktion »clock_nanosleep()« beispielsweise legt – wie der Name schon sagt – eine Funktion schlafen. Im Interrupt-Kontext ist so etwas weder möglich noch erlaubt. Seit der letzten Kern-Technik, die sich mit dem Clockwork des Kernels befasst hat [2] im Jahr 2006, haben die Entwickler viel erneuert und verfeinert.

Abbildung 1: Je nach Kontext sind Programmierer in der Funktionsauswahl eingeschränkt.

Abbildung 1: Je nach Kontext sind Programmierer in der Funktionsauswahl eingeschränkt.

Schlafkur für Applikationen

Haben die Anwendungsprogramme-Entwickler in den Unix-Anfangstagen mit der Funktion »time()« die Zeit in Sekundenauflösung erfasst, greift der Programmierer heute zu »clock_gettime()« . Neben der deutlich besseren Auflösung von Nanosekunden berücksichtigt diese Funktion unterschiedliche Zeitgeber: Standardisiert sind »CLOCK_REALTIIME« und »CLOCK_MONOTONIC« . Während »CLOCK_REALTIME« die vergangene Zeit seit dem 1.1.1970 (Unix-Epoche) zurückgibt, liefert »CLOCK_MONOTONIC« eine Zeit, die zumeist mit dem Systemstart angefangen hat zu ticken. »CLOCK_REALTIME« berücksichtigt Korrekturen an der Uhr, »CLOCK_MONOTONIC« dagegen zählt bei Sommer-Winterzeit-Umstellungen unbeeindruckt weiter.

Wie die Funktion »time()« veraltet ist, sollten Programmierer auch »sleep()« und »nanosleep()« nicht mehr verwenden. Die Funktion »clock_nanosleep()« berücksichtigt nicht nur den Zeitgeber und arbeitet mit »struct timespec« , sie lässt den Programmierer auch zwischen relativem und absolutem Schlafen wählen – also ob sein Prozess für eine Zeitdauer x verharrt oder bis zum Zeitpunkt y.

Unterbricht ein Ereignis unvorhergesehen einen zeitgesteuert schlafenden Rechenprozess vor Ablauf der Wunschzeit – etwa weil der Rechenprozess ein Signal geschickt bekommt –, kann der Programmierer das Schlafen leicht neu aufsetzen: Im vierten Parameter übergibt er die Adresse eines Speicherbereichs, in dem beim Abbruch die Restzeit abgelegt ist.

Wer hat an der Uhr gedreht?

Das Zeitmanagement des Linux-Kernels basiert auf High-Resolution-Timern (Hrtimer, [1]). Diese verarbeiten und speichern genaue Zeiten in der Datenstruktur »ktime_t« . Hinter diesem Typ steckt eine 64 Bit breite Variable, die die Zeit in einer Auflösung von Nanosekunden speichert. Zum Lesen der Zeit mit der passenden Auflösung besitzt der Kernel die Funktionen »ktime_get_ts()« und »ktime_get_real_ts()« . Sie präsentieren die Zeit in der aus dem Userland bekannten Form »struct timespec« , also in Sekunden und die Restzeit in Nanosekunden.

Die Funktion »ktime_get_real_ts()« liefert – in Abgrenzung zu »ktime_get_ts()« – die Zeit seit dem 1.1.1970, also seit dem Beginn der Unix-Epoche, zurück. Dabei berücksichtigt sie auch ein Drehen an der Uhr, wie es bei der Sommer-Winterzeit-Umstellung stattfindet (Zeitgeber »CLOCK_REALTIME« ). »ktime_get_ts()« berechnet die Zeit relativ zu einer internen Basis – zumeist dem Systemstart – (Zeitgeber »CLOCK_MONOTONIC« ).

Der Startschuss fällt beim Laden des Moduls

Für einfache Zeitverzögerungen stehen die in Tabelle 1 gelisteten Funktionen bereit. Bestimmungsgemäß sollten sie die aufrufende Codesequenz für die angegebene Zeit Nano-, Mikro- oder Millisekunden-genau pausieren lassen. Wegen des Multitasking und der Interruptverarbeitung der CPU gelingt ihnen dies in der Praxis aber mehr schlecht als recht. Um es klarer zu sagen: In Linux’ Pausen schleichen sich oft solche Ungenauigkeiten ein, dass man bei Ausreißern geneigt ist, von einem fehlerhaften Verhalten auszugehen.

Der Beweis dafür ist mit ein wenig Aufwand zu erbringen: Ein für diesen Artikel entwickeltes einfaches Kernelmodul soll die Genauigkeit dieser Zeitverzögerungsfunktionen messen. Das Modul ist so gebaut, dass es die Messung beim Laden durchführt. Allerdings meldet es nach der Messung absichtlich einen Fehler, um das eigene Einnisten in den Kernel zu verhindern. Der Trick spart das ansonsten notwendige Entladen.

Der Quellcode lässt sich auf einem PC und – falls der Linux-Quellcode auf dem Gerät vorhanden ist – auch auf einem Raspberry Pi übersetzen. Abbildung 2 zeigt das Makefile (alle Sourcen gibt es auf dem FTP-Server des Linux-Magazins [3]), den Übersetzungsvorgang und das Laden des binären Moduls per »sudo insmod messung.ko« .

Die Messung der einzelnen Zeitverzögerungsfunktionen – unterschiedlich parametrisiert – dauert knapp 10 Minuten. Anschließend erscheint die erwähnte Fehlermeldung. Die Ausgabe des Kernelmoduls landet im Syslog des Systems, das man am besten in einem separaten Terminalfenster mit Hilfe des Kommandos »tail -f /var/log/kern.log« verfolgt.

Tabelle 1

Kernelfunktionen zur Zeitverzögerung

Atomic-Kontext (ISR, Tasklet, Soft-IRQ)
ndelay(unsigned long nsecs) Verzögerungszeit in Nanosekunden
udelay(unsigned long usecs) Verzögerungszeit in Mikrosekunden
mdelay(unsigned long msecs) Verzögerungszeit in Millisekunden
Non-atomic-Kontext (Kernelthread, normale Treiberfunktionen)
udelay(unsigned long usecs) Verzögerungszeit in Mikrosekunden
usleep_range(unsigned long min, unsigned long max) Schlafenszeit in Mikrosekunden
msleep(unsigned long msecs) Schlafenszeit in Mikrosekunden
msleep_interruptible(unsigned long msecs) Schlafenszeit in Mikrosekunden
schedule_timeout(long timeout) Schlafenszeit in Jiffies
schedule_timeout_interruptible(long timeout) Schlafenszeit in Jiffies
Abbildung 2: Eine Messung der Verzögerungszeiten kann jeder Kern-Technik-Leser auf seinen Systemen mit wenig Aufwand selbst durchführen.

Abbildung 2: Eine Messung der Verzögerungszeiten kann jeder Kern-Technik-Leser auf seinen Systemen mit wenig Aufwand selbst durchführen.

Manchmal das Aufwachen total verpennt

Zum Messen nimmt der Tester unmittelbar vor der geplanten Zeitverzögerung einen Zeitstempel und einen danach. Die Verzögerungsdauer errechnet sich dann aus der Differenz der beiden Zeitstempel (Differenzzeit-Messung). Die Messung – ein paar Tausend Mal ausgeführt – liefert dann einen ersten Eindruck bezüglich der Genauigkeit.

Listing 1 zeigt den Quellcode, der die Abweichung unterschiedlicher Funktionen bestimmt. In Zeile 42 beginnt die generische Messfunktion »loop()« , welche die Adresse der auszumessenden Funktion als Parameter übergeben bekommt und damit jeweils 1000 Messungen durchführt. Das »messung.c« -Kernelmodul bestimmt den niedrigsten, den mittleren und den höchsten Wert.

Die Messungen beginnen mit »udelay()« . Da die Funktion im Kontext der Kernelmodul-Initialisierungsfunktion »init_ module()« startet, arbeitet »udelay()« im User-Kontext (»NON-ATOMIC« ). Die Funktion »udelay()« selbst realisiert übrigens die Zeitverzögerung über eine – eigentlich unschöne – Busy-Loop, also durch ständiges Überprüfen, ob die zu wartende Zeit abgelaufen ist. Bei kurzen Verzögerungszeiten ist das jedoch ein sinnvolles Vorgehen.

Listing 1

Messen von Zeitverzögerungen: messung.c

01 #include <linux/module.h>
02 #include <linux/fs.h>
03 #include <linux/delay.h>
04 #include <linux/sched.h>
05
06 static void m_udelay(int us,struct timespec *start,
07     struct timespec *end)
08 {
09     ktime_get_ts( start );
10     udelay( us );
11     ktime_get_ts( end );
12 }
13
14 static void m_udelay_ir(int us,struct timespec *start,
15     struct timespec *end)
16 {
17     static spinlock_t mylock;
18
19     spin_lock_irq( &mylock );
20     ktime_get_ts( start );
21     udelay( us );
22     ktime_get_ts( end );
23     spin_unlock_irq( &mylock );
24 }
25
26 static void m_usleep(int us,struct timespec *start,
27     struct timespec *end)
28 {
29     ktime_get_ts( start );
30     usleep_range( us, us );
31     ktime_get_ts( end );
32 }
33
34 static void m_msleep(int ms,struct timespec *start,
35     struct timespec *end)
36 {
37     ktime_get_ts( start );
38     msleep( ms );
39     ktime_get_ts( end );
40 }
41
42 static void loop( void (*messure)(int,struct timespec*,
43     struct timespec*),int tm)
44 {
45     unsigned long i, diff, mean, shortest=0x7fffffff, highest=0;
46     struct timespec start;
47     struct timespec end;
48     unsigned long before_us, after_us;
49     for (i=0, mean=0; i<1000; i++) {
50         messure( tm, &start, &end );
51         before_us = (start.tv_sec*1000000)+(start.tv_nsec/1000);
52         after_us = (end.tv_sec*1000000)+(end.tv_nsec/1000);
53         diff = after_us - before_us;
54
55         mean += diff;
56         if (diff > highest)
57             highest = diff;
58         if (diff < shortest)
59             shortest = diff;
60     }
61     mean = mean / i;
62     printk("shortest: %ld mean: %ld highest: %ld\n",
63         shortest, mean, highest);
64     return;
65 }
66
67 static int __user mod_init( void )
68 {
69     struct sched_param schedpar;
70
71     loop( m_udelay, 15 );
72     loop( m_udelay_ir, 15 );
73     loop( m_udelay_ir, 100 );
74     loop( m_usleep, 15 );
75     schedpar.sched_priority = 99;
76     sched_setscheduler( current, SCHED_FIFO, &schedpar );
77     loop( m_usleep, 15 );
78     loop( m_usleep, 100 );
79     sched_setscheduler( current, SCHED_NORMAL, &schedpar );
80     loop( m_usleep, 110000 );
81     loop( m_msleep, 10 );
82     loop( m_msleep, 15 );
83     loop( m_msleep, 18 );
84     loop( m_msleep, 24 );
85     loop( m_msleep, 50 );
86     loop( m_msleep, 100 );
87     loop( m_msleep, 110 );
88     return -EIO;
89 }
90
91 static void __exit mod_exit( void )
92 {
93     return;
94 }
95
96 module_init( mod_init );
97 module_exit( mod_exit );
98
99 MODULE_LICENSE("GPL");

Erste Ergebnisse mit der Funktion »udelay()« auf einem Raspberry Pi 2 mit Kernel 4.1.0 (Tabelle 2) sind ernüchternd. Bei einer Verzögerung von 15 Mikrosekunden dauert die tatsächliche Wartezeit zwar meist die gewünschten 15 Mikrosekunden (siehe Durchschnittswert), manchmal aber eben auch mehr als 60 Mikrosekunden. Auf einem PC misst man – je nach Hardware – ähnlich schlechte Werte. Die Abweichung ist einfach signifikant, Realzeit-Fähigkeit sollte anders aussehen.

Die Ursache für die Abweichung bei »udelay()« sind Interrupt-Service-Routinen, die auf derselben CPU abgearbeitet werden. Wer die Interrupts sperrt, was zwar recht tricky ist, aber über eine Spinlock-Funktion gelingt, wird schnell feststellen, dass danach auf dem Raspberry Pi die höchsten Differenzen zur erwarteten Zeitverzögerung sichtlich niedriger sind.

Noch deutlicher fällt der positive Effekt aus, wenn man »udelay()« 100 Mikrosekunden schlafen schickt. Mit lokalem Interrupt gesperrt, liegt der Fehler dann bei unter 10 Prozent und damit im akzeptablen Bereich. Doch Vorsicht: Was für die eigene Applikation von so großem Vorteil ist, erweist sich für die anderen Komponenten des Systems als im gleichen Maße ungünstig. So ist es prinzipbedingt, dass die Interruptsperre für das übrige System eine zusätzliche Latenz von der Länge der gewählten Verzögerungszeit einbaut.

Tabelle 2

Gemessene Zeitverzögerungen im Kernel

Funktion Kürzeste Zeit Mittlere Zeit Längste Zeit Besonderheit
Raspberry Pi 2
udelay(15) 15 µs 15 µs 68 µs
udelay(15) 15 µs 15 µs 17 µs mit IR-Sperre
udelay(100) 100 µs 100 µs 101 µs
usleep(15) 29 µs 32 µs 155 µs
usleep(15) 25 µs 29 µs 70 µs mit RT-Priorität
usleep(100) 111 µs 114 µs 160 µs mit RT-Priorität
usleep(110000) 110 015 µs 110 027 µs 110 081 µs
msleep(10) 10 715 µs 19 990 µs 20 017 µs
msleep(15) 29 944 µs 29 999 µs 30 064 µs
msleep(18) 29 981 µs 29 999 µs 30 019 µs
msleep(24) 39 983 µs 39 999 µs 40 017 µs
msleep(50) 59 969 µs 59 999 µs 60 017 µs
msleep(100) 109 945 µs 109 999 µs 110 067 µs
msleep(110) 119 962 µs 119 999 µs 120 018 µs
PC (AMD Phenom II X6 1055T)
udelay(15) 15 µs 15 µs 31 µs
udelay(15) 15 µs 15 µs 16 µs mit IR-Sperre
udelay(100) 99 µs 99 µs 100 µs
usleep(15) 20 µs 21 µs 61 µs
usleep(15) 20 µs 20 µs 39 µs mit RT-Priorität
usleep(100) 104 µs 105 µs 109 µs mit RT-Priorität
usleep(110000) 110 003 µs 110 056 µs 110 093 µs
msleep(10) 12 942 µs 15 996 µs 16 044 µs
msleep(15) 19 983 µs 19 999 µs 20 017 µs
msleep(18) 27 972 µs 27 999 µs 28 032 µs
msleep(50) 55 975 µs 55 999 µs 56 024 µs
msleep(100) 103 948 µs 10 3999 µs 104 049 µs
msleep(110) 115 958 µs 115 999 µs 116 038 µs

Schlafe nur ein Weilchen

Die Funktion »usleep_range()« realisiert die Verzögerung nicht mehr über aktives Warten, sondern über Schlafen. Die Auszeit können deshalb andere Rechenprozesse nutzen. Der Namenszusatz »_range« bedeutet übrigens, dass der moderne Programmierer statt eines genauen Zeitpunkts einen Zeitbereich angibt. Das ermöglicht dem System die Optimierung; unter Umständen spart es sich das mehrfache Aufwachen bei zeitlich nahe beieinanderliegenden Aufweckpunkten und damit auch Energie.

Doch auch bei »usleep_range()« sorgt die erste Messung für Ernüchterung. Bereits die durchschnittlichen Schlafenszeiten weichen erheblich vom Soll ab; statt 15 Mikrosekunden schläft der Rechenprozess im gemessenen Worst Case auf dem Raspberry Pi 155 Mikrosekunden. Interrupts kommen als Ursache natürlich in Frage, lassen sich aber beim Schlafenlegen nicht abschalten – sonst würde das Scheduling steckenbleiben. Allerdings lässt sich das Messmodul mit Realzeit-Priorität versehen. Doch auch das bringt keinen durchschlagenden Erfolg.

Ein ähnliches Bild vermitteln die Messungen von »msleep()« . Krass fällt der Fehler bei einer Schlafenszeit von 24 Millisekunden aus. Im besten Fall schlief das Testmodul knapp 40 Millisekunden. Hintergrund: Der Kernel stellt sicher, nie kürzer als veranschlagt zu schlafen. Da »msleep()« zudem im klassischen (alten) 10-Millisekunden-Raster arbeitet, ergeben sich Fehler von bis zu 20 Millisekunden.

Geht es besser? Ja! Mit der Funktion »usleep_range()« für lange Schlafenszeiten, also beispielsweise 110 Millisekunden, zeigte »usleep_range(110000,110000)« in den getätigten Messungen einen maximalen Fehler von 81 Mikrosekunden.

Quintessenz

Zeit verzögern will gelernt sein. Abhängig von der Verzögerungszeit wählt der Programmierer die passende Funktion, eventuell muss er Interrupts sperren oder Prioritäten anpassen. Aber Achtung: Das Sperren von Interrupts hat Rückwirkungen auf das gesamte System. Die Kerneldokumentation [4] empfiehlt das Folgende: Wer für wenige Mikrosekunden (weniger als 10) verzögern möchte, nimmt die Funktion »udelay()« . Wer zwischen 10 Mikrosekunden und 20 Millisekunden lange Nickerchen einlegen will, wählt »usleep_range()« , und wer noch mehr Schlaf verordnen möchte, greift zu »msleep()« beziehungsweise »msleep_interruptible()« . < Davon abweichend zeigen die Messungen, dass an Stelle von »msleep()« für höhere Genauigkeiten »usleep_range()« grundsätzlich empfehlenswert ist (Abbildung 3).

Abbildung 3: Die besten Genauigkeiten erzielt der Programmierer mit »udelay()« und »usleep_range()«.

Abbildung 3: Die besten Genauigkeiten erzielt der Programmierer mit »udelay()« und »usleep_range()«.

Valide Messungen

Die Messungen hier können nur einen groben Eindruck von den zeitlichen Größenordnungen vermitteln. Um eine valide Latenzzeit-Messung handelt es sich nicht. Die findet man aber beim OSADL [5], das in seinen Racks diverse Linux-Systeme Tag und Nacht automatisiert ausmisst. Die Messungen laufen unter ständig veränderten Lastsituationen. Auch zwei Raspberry Pis gehören zu den Testobjekten.

Der Raspberry Pi 2 weist bei diesen Userland-Messungen typische 100 Mikrosekunden Latenzzeit auf, im Extremfall aber auch mit einem Standardkernel 32,4 Millisekunden. Das ist zwar um Größenordnungen schlechter als hier dokumentiert, aber erstens finden die Messungen im Userland und zweitens unter wirklichen Worst-Case-Bedingungen statt. Für die preiswerte Hardware sind diese Latenzzeiten nicht schlecht und für die Handhabung der meisten Realzeit-Probleme ausreichend. (jk)

Infos

  1. Quade, Kunst, “Linux-Treiber entwickeln”, 4. Auflage: Dpunkt-Verlag, 2016, S. 197
  2. Quade, Kunst, “Kern-Technik – Folge 27”: Linux-Magazin 04/06, S. 118, https://www.linux-magazin.de/Ausgaben/2006/04/Kern-Technik
  3. Listings zu diesem Artikel:ftp://www.linux-magazin.de/pub/listings/magazin/2016/08/Kern-Technik
  4. Kerneldokumentation, “Delays – Information on the various kernel delay / sleep mechanisms”: http://www.kernel.org/doc/Documentation/timers/timers-howto.txt
  5. Open Source Automation Development Lab, “Latency Plot ARMv7 Processor rev 4 (v7l), Linux 4.4.10-v7+”: https://www.osadl.org/Latency-plot-of-system-in-rack-b-slot.qa-latencyplot-rbs3.0.html?shadow=0

Der Autor

Eva-Katharina Kunst ist seit den Anfängen von Linux Fan von Open Source. Jürgen Quade, Professor an der Hochschule Niederrhein, hat mit “Embedded Linux lernen mit dem Raspberry Pi” letztes Jahr sein drittes Linux-Buch veröffentlicht. Das gemeinsame Buch “Linux-Treiber entwickeln” ist jüngst in vierter Auflage erschienen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben