Harte und weiche Echtzeitsysteme

Werbung
Harte und weiche Echtzeitsysteme
Material zur Vorlesung Echtzeitsysteme an
der Hochschule Niederrhein.
Jürgen Quade
Harte und weiche Echtzeitsysteme Material zur Vorlesung Echtzeitsysteme an der Hochschule
Niederrhein.
von Jürgen Quade
V4.1, Mo 19. Sep 16:00:56 CEST 2011
Dieses Buch darf unter der Bedingung, dass die eingetragenen Copyright-Vermerke nicht modifiziert werden, kopiert und weiterverteilt
werden. Die kommerzielle Nutzung (Weiterverkauf) ist ohne Zustimmung des Autors bislang nicht erlaubt. Da auch die Quellen des Buches
zur Verfügung stehen, ist die Modifikation, Erweiterung und Verbesserung durch andere Autoren möglich und sogar erwünscht.
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
$Id:
ezsI.sgml,v 1.77 2011/09/19 13:58:30 quade Exp $
legalnotice.sgml,v 1.3 2001/01/04 21:44:42 quade Exp $
literatur.sgml,v 1.13 2009/02/08 13:38:08 quade Exp $
architecture.sgml,v 1.16 2011/09/18 16:42:43 quade Exp $
definitionen.sgml,v 1.7 2009/02/08 14:49:23 quade Exp $
prozess.sgml,v 1.13 2006/08/10 08:30:43 quade Exp $
realtime.sgml,v 1.77 2011/09/13 19:23:44 quade Exp $
os.sgml,v 1.116 2011/09/19 12:55:49 quade Exp $
feldbus.sgml,v 1.11 2006/08/10 08:30:43 quade Exp $
Multiplex.sgml,v 1.3 2001/01/04 21:44:42 quade Exp $
sicherheit.sgml,v 1.18 2011/09/19 13:16:11 quade Exp $
verfuegbar.sgml,v 1.7 2011/02/27 19:14:48 quade Exp $
algorithm.sgml,v 1.9 2006/08/10 08:30:43 quade Exp $
entwicklung.sgml,v 1.5 2006/08/10 08:30:43 quade Exp $
distributed.sgml,v 1.5 2006/08/10 08:30:43 quade Exp $
petri.sgml,v 1.11 2011/09/19 13:18:31 quade Exp $
socket.sgml,v 1.7 2006/08/10 08:30:43 quade Exp $
IoSubSystem.sgml,v 1.11 2008/01/31 14:58:56 quade Exp $
ir.sgml,v 1.7 2006/08/10 08:30:43 quade Exp $
Versionsgeschichte
Version 4.1 $Date: 2011/09/19 13:58:30 $ Geändert durch: $Author: quade $
Inhaltsverzeichnis
1. Überblick................................................................................................................................................... 1
1.1. Definitionen ................................................................................................................................... 1
1.1.1. Prozesse............................................................................................................................. 1
1.1.1.1. Technischer Prozess.............................................................................................. 1
1.1.1.2. Rechenprozess ...................................................................................................... 2
1.1.1.3. Steuerung.............................................................................................................. 2
1.1.1.4. Kognitiver Prozess................................................................................................ 3
1.2. Kennzeichen von Realzeitsystemen............................................................................................... 3
1.2.1. Ausprägungen von Realzeitsystemen ............................................................................... 5
1.3. Realzeitsystem-Komponenten ....................................................................................................... 6
2. Realzeitbetrieb.......................................................................................................................................... 9
2.1. Echtzeitbedingungen...................................................................................................................... 9
2.1.1. Auslastung....................................................................................................................... 10
2.1.2. Pünktlichkeit ................................................................................................................... 11
2.1.3. Harte und weiche Echtzeit .............................................................................................. 14
2.2. Latenzzeiten................................................................................................................................. 15
2.3. Unterbrechbarkeit und Prioritäten ............................................................................................... 15
3. Realzeitbetriebssysteme ......................................................................................................................... 20
3.1. Aufbau und Struktur .................................................................................................................... 21
3.2. Betriebssystemkern...................................................................................................................... 22
3.2.1. Prozess-Management ...................................................................................................... 22
3.2.1.1. Scheduling auf Einprozessormaschinen............................................................. 28
3.2.1.1.1. Scheduling Points .................................................................................. 30
3.2.1.1.2. Bewertungskritierien.............................................................................. 30
3.2.1.1.3. First Come First Serve (FCFS) .............................................................. 31
3.2.1.1.4. Round Robin Scheduling ....................................................................... 32
3.2.1.1.5. Prioritätengesteuertes Scheduling.......................................................... 33
3.2.1.1.6. Deadline-Scheduling.............................................................................. 34
3.2.1.1.7. POSIX 1003.1b...................................................................................... 35
3.2.1.1.8. Scheduling in VxWorks ......................................................................... 36
3.2.1.1.9. Der O(1) Scheduler im Linux-Kernel 2.6. ............................................. 37
3.2.1.2. Scheduling auf Mehrprozessormaschinen.......................................................... 38
3.2.2. Memory-Management..................................................................................................... 41
3.2.2.1. Segmentierung.................................................................................................... 42
3.2.2.2. Seitenorganisation .............................................................................................. 44
3.2.3. I/O Subsystem ................................................................................................................. 46
3.2.3.1. Gerätezugriff auf Applikationsebene.................................................................. 46
3.2.3.1.1. Schnittstellenfunktionen ........................................................................ 46
3.2.3.1.2. Zugriffsarten .......................................................................................... 47
3.2.3.2. Gerätezugriff innerhalb des Betriebssystemkerns .............................................. 50
3.2.3.3. Filesystem........................................................................................................... 54
3.2.4. Systemcall-Interface........................................................................................................ 55
3.2.5. Services ........................................................................................................................... 56
3.2.6. Bibliotheken .................................................................................................................... 57
3.3. Zeitaspekte................................................................................................................................... 58
3.3.1. Interrupt- und Task-Latenzzeiten .................................................................................... 58
3.3.2. Unterbrechungsmodell.................................................................................................... 60
3.3.3. Timing (Zeitverwaltung) ................................................................................................. 61
iii
4. Aspekte der nebenläufigen Echtzeit-Programmierung....................................................................... 64
4.1. Taskmanagement ......................................................................................................................... 64
4.1.1. Tasks erzeugen ................................................................................................................ 64
4.1.2. Tasks beenden ................................................................................................................. 66
4.1.3. Tasks parametrieren ........................................................................................................ 66
4.2. Schutz kritischer Abschnitte........................................................................................................ 68
4.2.1. Semaphor und Mutex ...................................................................................................... 68
4.2.2. Prioritätsinversion ........................................................................................................... 71
4.2.3. Deadlock ......................................................................................................................... 72
4.2.4. Schreib-/Lese-Locks ....................................................................................................... 73
4.2.5. Weitere Schutzmaßnahmen für kritische Abschnitte ...................................................... 74
4.3. Programmtechnischer Umgang mit Zeiten.................................................................................. 75
4.3.1. Zeit lesen ......................................................................................................................... 77
4.3.2. Der Zeitvergleich............................................................................................................. 78
4.3.3. Differenzzeitmessung...................................................................................................... 79
4.3.4. Schlafen........................................................................................................................... 80
4.3.5. Weckrufe per Timer......................................................................................................... 81
4.4. Inter-Prozess-Kommunikation..................................................................................................... 82
4.4.1. Pipes ................................................................................................................................ 83
4.4.2. Shared-Memory .............................................................................................................. 84
4.4.3. Sockets ............................................................................................................................ 84
4.5. Condition-Variable (Event).......................................................................................................... 88
4.6. Signals.......................................................................................................................................... 89
4.7. Peripheriezugriff .......................................................................................................................... 91
5. Echtzeitarchitekturen ............................................................................................................................ 96
5.1. Applikationen im Kernel ............................................................................................................. 96
5.2. Mehrkernmaschine ...................................................................................................................... 96
5.3. Mehrkernelansatz....................................................................................................................... 100
6. Echtzeitsysteme in sicherheitskritischen Anwendungen .................................................................. 103
6.1. Begriffsbestimmung................................................................................................................... 103
6.2. Mathematische Grundlagen....................................................................................................... 103
6.3. Redundante Systeme.................................................................................................................. 108
6.4. Weitere Maßnahmen zur Zuverlässigkeitssteigerung ................................................................ 109
7. Formale Beschreibungsmethoden ....................................................................................................... 113
7.1. Daten- und Kontrollflussdiagramme.......................................................................................... 113
7.2. Darstellung von Programmabläufen mit Hilfe von Struktogrammen........................................ 115
7.3. Petrinetze ................................................................................................................................... 117
8. Echtzeitnachweis .................................................................................................................................. 124
8.1. Worst Case Betrachtungen......................................................................................................... 124
8.2. Abschätzung der Worst Case Execution Time (WCET)............................................................ 125
8.3. Abschätzung der Best Case Execution Time (BCET) ............................................................... 127
8.4. Echtzeitnachweis bei prioritätengesteuertem Scheduling ......................................................... 127
8.5. Echtzeitnachweis bei Deadline-Scheduling............................................................................... 133
8.5.1. Ereignisstrom-Modell ................................................................................................... 133
8.5.2. Behandlung abhängiger Ereignisse............................................................................... 137
Literatur.................................................................................................................................................... 141
Stichwortverzeichnis ................................................................................................................................ 143
iv
Tabellenverzeichnis
3-1. Vergleich Task und Thread.................................................................................................................... 28
4-1. Unterschiede zwischen einem Signal und einer Condition Variablen .................................................. 90
v
Kapitel 1. Überblick
Das Feld der Echtzeitsysteme erfährt gegenwärtig große Aufmerksamkeit. Das liegt nicht zuletzt daran,
dass die durch die fortschreitende Minituarisierung und Leistungssteigerung der Hardware auf der einen
Seite und dem damit verbundenen Eizug von Standardsoftware – allen voran Linux – auf der anderen Seite
ganz neue Möglichkeiten und or allem auch verkürzte Entwicklungszeiten ergeben. Entsprechend gibt es zur
Zeit auh tatsächlich ständig Neuentwicklungen auf dem Gebiet der Echtzeitsytseme, die dem Ingenieur oder
Informatiker neue Möglichkeiten zur Realisierung von Systemen mit harter Realzeitanforderungen geben.
Als Beispiel seien hier die Mehrkernsysteme (SMP) erwähnt, bei denen der Systemarchitekt durch eine
geschickte Verteilung von Realzeitaufgaben und nicht Realzeitaufgaben auf die unterscheidlichen CPUKerne für die Einhaltung der Echtzeitbedingungen osrgen kann.
Bei dem vorliegenden Skript handelt es sich um das Begleitmaterial zur Vorlesung Echtzeitsysteme an der
Hochschule Niederrhein.
Ziel der Vorlesung ist dabei die Vermittlung notwendigen Wissens und der notwendigen Technologie, um
Realzeitsysteme konzipieren, realisieren und bewerten zu können.
Die Vorlesung wurde seit dem Wintersemester 1999/2000 im Diplomstudiengang Informatik, seit dem Wintersemester 2008/2009 in den Bachelorstudiengängen Informatik und Elektrotechnik in gekürzter Form
abgehalten. Eine Überarbeitung fand zum Wintersemester 2011/2012 statt.
1.1. Definitionen
Realzeit- oder Echtzeitsysteme sind Systeme, bei denen neben den funktionalen Anforderungen noch Anforderungen zeitlicher Natur gestellt werden. Die durch den Rechner durchgeführten Berechnungen müssen
damit nicht nur korrekt, sondern zudem auch zum richtigen Zeitpunkt erfolgen (logische und temporale Korrektheit).
Realzeit-Steuerungssysteme bestehen aus dem technischen Prozess, den Rechenprozessen und kognitiven
Prozessen. Im folgenden sollen diese Begriffe genauer erläutert werden.
1.1.1. Prozesse
Ein Prozess ist nach [DIN66201] eine "Gesamtheit von aufeinander einwirkenden Vorgängen in einem
System, durch die Materie, Energie oder Information umgeformt, transportiert oder gespeichert wird". Man
spricht von technischen Prozessen, wenn Material oder Energie umgeformt, transportiert beziehungsweise
gespeichert wird, und von Rechenprozessen bei der Umformung, beim Transport beziehungsweise bei der
Speicherung von Information.
1.1.1.1. Technischer Prozess
Ein technischer Prozess ist gekennzeichnet durch (physikalische) Zustandsgrößen. Viele dieser Zustandsgrößen können durch Sensoren (Meßwertaufnehmer) erfaßt und durch Aktoren (Stellglieder) beeinflußt
werden. Typische Zustandsgrößen sind Temperatur, Druck, Feuchtigkeit oder Position.
Beispiele für Sensoren:
•
•
•
•
Endlagenschalter
Temperaturmesser
Winkelgeber
Füllstandsmesser
1
Kapitel 1. Überblick
Beispiele für Aktoren:
•
•
•
Motoren
Ventile
Relais
Technische Prozesse lassen sich weiter nach der Art der vorwiegend verarbeiteten Variablen klassifizieren
[Lauber 76]. Diese Variablen können sein:
1. Physikalische Größen mit kontinuierlichem oder wenigstens stückweise kontinuierlichem Wertebereich.
2. Binäre Informationselemente, die Ereignissen zugeordnet werden können.
3. Informationselemente, die einzeln identifizierbaren Objekten zugeordnet werden können.
1.1.1.2. Rechenprozess
Ein Rechenprozess ist der Umformung, Verarbeitung und des Transportes von Informationen gewidmet.
Im Regelfall berechnet er aus Eingabewerten Ausgabewerte. Ein Rechenprozess wird durch ein Programm
beschrieben. Ein Programm beschreibt die Abfolge der Befehle und ist damit zunächst statisch. Die Instanz
eines Programmes (die dynamische Abarbeitung des Programms also) ist dann der Rechenprozess. Anstelle
des Begriffes Rechenprozess wird oft auch der Begriff Task verwendet, insbesondere um eine Verwechselung mit dem technischen Prozess begrifflich auszuschließen.
Moderne Rechnersysteme und vor allem auch Realzeitsysteme sind in der Lage, quasi parallel (in der Realität jedoch nur sequentiell) mehrere Rechenprozesse gleichzeitig zu bearbeiten (Multitasking).
1.1.1.3. Steuerung
Die Summe der Rechenprozesse und deren Ablaufumgebung (Hardware und Software) wird als Steuerung
bezeichnet.
Die Aufgabe der Steuerung ist:
•
Erfassung der Zustandsgrößen des technischen Prozesses.
•
Beeinflußung des technischen Prozesses.
•
Koordination der Prozessabläufe.
•
Überwachung der Prozessabläufe.
Nach [DIN 19226] unterscheidet man zwischen einer Steuerung und einer Regelung je nachdem, ob ein
geschlossener Regelkreis vorliegt oder nicht. Vielleicht weil im Englischen nur das Wort control existiert,
wird im deutschen Sprachgebrauch auch dann oftmals der Begriff Steuerung verwendet, wenn es sich im
eigentlichen Sinn um eine Regelung handelt (z.B. Speicherprogrammierbare Steuerung).
2
Kapitel 1. Überblick
Rechen−
prozeß
technischer
Prozeß
Aktor(−en)
Rechen−
prozeß
technischer
Prozeß
Aktor(−en)
Sensor(−en)
1
0
gesteuerter Prozeß
1
0
geregelter Prozeß
Abbildung 1-1. Steuerung versus Regelung
Man spricht also von einem geregelten - im Gegensatz zum gesteuertem - Prozess, wenn dieser jeweils mindestens einen Wandler für ein Ein- als auch ein Ausgangssignal (Aktor und Sensor) besitzt (Bild
Steuerung versus Regelung). Dabei wird der Eingangswert (die Stellgröße) durch den über einen Sensor
gewonnenen Zustandswert des Prozesses beeinflußt.
1.1.1.4. Kognitiver Prozess
Als kognitive Prozesse schließlich bezeichnet man die Verarbeitung, Umformung und den Transport von Informationen innerhalb des menschlichen Bedieners. Durch entsprechende Mensch-Maschine-Schnittstellen
(HMI, Human-Machine-Interface) nimmt der Bediener Einfluß auf die Rechenprozesse und auch direkt auf
die technischen Prozesse.
Für die Betrachtung der Rechenprozesse ist es jedoch nicht entscheidend, aus welcher Quelle die zu verarbeitenden Eingaben kommen. Kognitive Prozesse spielen daher bei den weiteren Betrachtungen eine
untergeordnete Rolle (und werden wie Eingaben aus dem technischen Prozess behandelt).
Ein Beispiel für einen derartigen kognitiven Prozess ist die Steuerungssoftware des Rangierbahnhofs
Nord in München. Am Rangierbahnhof werden Güterzüge zusammengestellt. Da Güterzüge eine
maximale Länge haben, kann es schon einmal passieren, dass nicht alle Waggons mitkommen können. Die
Steuerungssoftware ist bewußt so geschrieben worden, dass in diesem Falle ein Mitarbeiter der Deutschen
Bahn AG entscheidet, welche Waggons bevorzugt behandelt werden.
Technischer
Prozeß
Kognitiver
Prozeß
Sensoren
Userinput
Aktoren
Visualisierung
Rechen
Prozeß
Abbildung 1-2. Zusammenhang zwischen unterschiedlichen Prozessformen
3
Kapitel 1. Überblick
1.2. Kennzeichen von Realzeitsystemen
Bei einem Echtzeitsystem handelt es sich um ein System, das in der Lage ist, eine Aufgabe innerhalb eines
spezifizierten Zeitfensters abzuarbeiten. Kennzeichen eines Echtzeitsystems ist daher die sogenannte Pünktlichkeit oder Rechtzeitigkeit und damit insbesondere verbunden die Eigenschaft, zeitlich deterministisch
(also berechenbar oder zumindest vorhersehbar und vorhersagbar) zu sein. Eine oftmals notwendige Eigenschaft, aber auf keinen Fall ein Kennzeichen eines Echtzeitsystems ist die Schnelligkeit.
Umwelt
(z.B. technischer Prozeß)
1 0
Echtzeitsteuerung
Wandler
(z.B. Aktoren)
(z.B. Sensoren)
Abbildung 1-3. Struktur eines Echtzeitsystem
Echtzeitsysteme (Bild Struktur eines Echtzeitsystem) kann man in allen Bereichen und beinahe überall
finden. Prinzipiell verarbeiten sie Eingangswerte (z.B. Zustandsgrößen technischer Prozesse) und berechnen
- zeitgerecht - Ausgangsgrößen (Werte für Stellglieder). Ein- und Ausgangsgrößen werden dem Echtzeitsystem über Wandler (z.B. Aktoren und Sensoren eines technischen Prozesses) zugeführt.
Steuert bzw. regelt das Echtzeitsystem (Realzeitsystem) einen technischen Prozess, ohne dass der Benutzer
direkt damit in Berührung kommt (eingebettete mikroelektronische Steuerung), spricht man von einem
eingebetteten Realzeitsystem. Im Automobil beispielsweise finden sich eingebettete Realzeitsysteme, die
zur Steuerung des Motors (Motormanagement), zur Steuerung des Anti-Blockier-Systems (ABS) oder zur
Überwachung der Airbags zuständig sind.
Weitere Beispiele für Echtzeitsysteme sind die vielfältigen Multimediaanwendungen bis hin zu den
schnellen Computerspielen, die durch diverse Tricks versuchen, auf nicht echtzeitfähigen Plattformen, wie
dem Windows-PC, Realzeitverhalten zu erreichen.
In vielen Fällen werden Realzeitsysteme im sicherheitskritischen Umfeld (z.B. ABS) eingesetzt. Zu der Anforderung nach Rechtzeitigkeit gesellen sich damit noch die Forderungen nach Sicherheit (das System muss
auch wirklich das richtige Ergebnis liefern) und Verfügbarkeit (das System darf unter keinen Umständen
ausfallen).
Elektronik
1 ns
Programm
+ ISR
1 us
RTOS
1 ms
Standard−OS
1s
10 s
Mechanik
100 s
Abbildung 1-4. Realzeitsysteme und ihre Zeitanforderungen [TimMon97]
4
Handbetrieb
1000 s
Kapitel 1. Überblick
Gemäß der obigen Definition sind also alle Systeme mit zeitlichen Anforderungen Echtzeitsysteme. Bild
Realzeitsysteme und ihre Zeitanforderungen [TimMon97] spiegelt entsprechend der Zeitanforderungen sinnvolle Realisierungstechnologien wider. Dabei erkennt man, dass Echtzeitsysteme sogar von Hand betrieben werden können, wenn Reaktionszeiten im Stundenbereich gefordert werden. muss auf eine zeitliche
Anforderung im Minutenbereich reagiert werden, kann man mechanische Steuerelemente einsetzen. Mit
Standard-Betriebssystemen ist es bereits möglich, Realzeitanforderungen im Sekundenbereich zu bedienen.
Sind die Zeitanforderungen jedoch noch strenger, wird man ein Realzeitbetriebssystem einsetzen, zumindest, solange die Anforderungen nicht im Mikrosekunden-Bereich (usec) liegen. In diesem Zeitbereich
lassen sich die Probleme zwar noch mit Software lösen, entsprechende Reaktionen müssen aber innerhalb
der sogenannten Interrupt-Service-Routine erfolgen. Um noch stärkere Anforderungen zeitlicher Natur zu
befriedigen, muss man schließlich eine Hardware-Realisierung wählen.
Im folgenden wird der Schwerpunkt auf Realzeitsysteme liegen, deren Zeitanforderungen im
Millisekunden-Bereich liegen, zu deren Realisierung im Normalfall ein Echtzeitbetriebssystem eingesetzt
wird. Während aber auch das Spektrum der Echtzeitsysteme mit noch höheren Anforderungen betrachtet
wird, werden Systeme auf mechanischer Basis bzw. Systeme mit Handsteuerung außer acht gelassen.
•
Zusammenfassend lauten die wichtigsten Anforderungen an Echtzeitsysteme:
•
•
•
•
•
Pünktlichkeit
Rechtzeitigkeit
Deterministisch
Sicherheit
Verfügbarkeit
1.2.1. Ausprägungen von Realzeitsystemen
Wie bereits im vorhergehenden Abschnitt deutlich wurde, gibt es unterschiedliche Ausprägungen von
Echtzeitsystemen. Diese Ausprägungen lassen sich vor allem bezüglich ihrer Hardware klassifizieren.
•
•
•
•
(Speicherprogrammierbare-) Steuerungen
Eingebettete Systeme (embedded systems)
RZ-Applikationen auf Standardarchitekturen
Realzeitkommunikationssysteme
Speicherprogrammierbare Steuerungen (SPS)
Speicherprogrammierbare Steuerungen sind klassische Realzeitsysteme, die insbesondere auch als
harte Realzeitsysteme eingesetzt werden. SPS’n werden im Regelfall über spezielle Programmiersprachen und -Werkzeuge programmiert und arbeiten in einem festen Zeitraster (Zykluszeit).
Eingebettete Systeme
Als ein eingebettetes System bezeichnet man die Steuerung eines technischen Prozesses, mit der der
Benutzer nur indirekt in Verbindung kommt:
•
Bei einem eingebetteten System handelt es sich um eine integrierte, mikroelektronische Steuerung.
•
Die Steuerung ist Teil eines Gesamtsystems (in das Gesamtsystem eingebettet).
•
Das System besteht aus Hard- und aus Software.
•
Das System wurde für eine spezifische Aufgabe entwickelt.
•
Das Mensch-Maschine-Interface ist im Regelfall eingeschränkt.
5
Kapitel 1. Überblick
•
Eingebettete Systeme weisen keine (kaum) bewegten Teile auf (diskless, fanless).
•
Sie decken ein breites Einsatzspektrum ab.
So stellt ein Handy ebenso ein eingebettetes System dar, wie das Modem, das ABS, die Waschmaschinensteuerung und der Gameboy.
Echtzeitsysteme - oder Realzeitsysteme - werden im amerikanischen vielfach gleichgesetzt mit den
eingebetteten Systemen (embedded systems). Diese Sichtweise ist jedoch zu einfach. Wenn auch die
meisten eingebetteten Systeme Realzeitsysteme sind, so doch nicht ausschließlich alle. Nicht jedes
eingebettete System ist ein Realzeitsystem, und nicht jedes Realzeitsystem ist ein eingebettetes System.
Eingebettete Systeme - mit ihren zusätzlichen Anforderungen - spielen auch in dem vorliegenden Buch
eine besondere Rolle.
RZ-Applikationen auf Standardarchitekturen (auch Soft-SPS)
Zwar für harte Realzeitsysteme nicht geeignet, werden Standardarchitekturen, wie beispielsweise eine
PC-Architektur, zur Lösung von weichen Echtzeitproblemen eingesetzt. Typische Beispiele dafür sind
Multimediaanwendungen und "Telefonieren über das Internet" (Voice over IP).
Vorteile:
•
Preiswerte Hardware
•
Verfügbarkeit von Zusatz-Hardware
•
Verfügbarkeit von Standard-Software zur Weiterverarbeitung aufgenommener Zustandsgrößen
Nachteile:
•
Preiswerte Hardware ist im rauhen Umfeld, in dem Echtzeitsysteme betrieben werden, nur
eingeschränkt einsatzfähig.
•
Standard-Betriebssysteme bieten kein kalkulierbares Realzeit-Verhalten.
Realzeitkommunikationssysteme
Realzeitkommunikationssysteme stellen eine Komponente eines verteilten Realzeitsystems dar.
Einzelne Realzeitprozesse tauschen über diese Komponente mit deterministischem Verhalten
Informationen aus.
1.3. Realzeitsystem-Komponenten
Realzeitsysteme bestehen aus unterschiedlichen Komponenten. Je nach Ausprägung des Realzeitsystems
sind dabei nicht alle Komponenten notwendig. So wird beispielsweise bei einem Computerspiel in den
seltensten Fällen eine Prozessperipherie benötigt.
6
Kapitel 1. Überblick
Applikation
Regel− und
Steuerprogramm
Visualisierung
Realtime Operating System
Hardware
Prozeßankopplung
direkte Kopplung
oder Feldbus
Prozeßperipherie
(Sensoren, Aktoren)
Abbildung 1-5. Architektur einer Echtzeitsteuerung
Die Komponenten eines Realzeitsystems sind:
•
Applikation
• Verarbeitungs- bzw. Steuerungsalgorithmen
• Visualisierung (HMI=Human-Man-Interface)
•
Echtzeitbetriebssystem
Rechnerhardware
Prozessankopplung (entweder direkt oder über Realzeit-Kommunikationssysteme/Feldbusse)
Prozessperipherie
•
•
•
Ein Realzeitsystem verarbeitet fristgerecht Eingangssignale zu Ausgangssignalen. Dazu werden auf logischer Ebene externe Prozesse 1 (z.B. ein technischer Prozess) auf sogenannte Rechenprozesse (siehe Abschnitt Rechenprozess) abgebildet, die auf einer Hardware (Rechner, Mikrokontroller) ablaufen. Die Eingangswerte der Rechenprozesse korrespondieren mit Signalen des externen Prozesses. Wandler (Meßwertaufnehmer, Sensoren) transformieren dabei diese (physikalischen) Signale und Werte in maschinell verarbeitbare Digitalwerte. Entsprechend werden die von den Rechenprozessen erzeugten Ausgangs- bzw.
Ausgabewerte über Wandler (Stellglieder, Aktoren) in (physikalische) Größen transformiert, mit denen sich
der externe Prozess beeinflussßen (regeln, steuern) läßt.
Die Wandlung der Werte, sowohl der Eingangs- als auch der Ausgangswerte, kann entweder am externen Prozess vollzogen werden oder aber direkt in der Realzeitrechnerhardware. Im ersten Fall werden die
externen Prozesssignale direkt an ihrer Entstehungsquelle gewandelt und als digitale Information an den
Realzeitrechner übertragen, bzw. vom Realzeitrechner als digitale Information an den externen Prozess gebracht. Für die Übertragung dieser digitalen Information wird oft ein (realzeitfähiges) Kommunikationssystem (z.B. Feldbus) eingesetzt. Werden nur wenige Prozesssignale ausgetauscht, kann der externe Prozess
auch direkt angekoppelt werden. In diesem Fall ist jedes Ein- und Ausgangssignal des externen Prozesses
direkt mit der Hardware des Realzeitsystems verbunden.
7
Kapitel 1. Überblick
Eine direkte Kopplung verwendet man aber vor allem auch beim zweiten Fall, bei dem die Ein- und
Ausgangswerte im Realzeitrechner selbst gewandelt werden. Hier müssen die Prozesssignale in ihrer ursprünglichen Form bis zu den entsprechenden Wandlern transportiert werden.
Die Rechnerhardware selbst besteht aus einem oder mehreren Prozessoren, dem Speicher und der sogenannten Glue-Logik (Bausteinen, die das Zusammenspiel der übrigen Komponenten ermöglichen). Darüber
hinaus gehören die bereits erwähnten Wandler dazu. Bei Systemen, die hohe zeitliche Anforderungen erfüllen, wird man auch oft spezielle Hardwarebausteine finden, die die Funktionalität, die normalerweise
in Software realisiert worden wäre, in Hardware realisiert (Asics, FPGA’s, LCA’s usw.). Daneben gehören
auch Sekundärspeicher (z.B. Festplatten, Flashkarten) zur Rechnerhardware.
Die Rechnerhardware wird über ein Realzeitbetriebssystem oder ein Laufzeitsystem (Executive, einfaches
Betriebssystem) angesteuert. Dieses Betriebssystem stellt die Ablaufumgebung für die Rechenprozesse zur
Verfügung. Die Hardware wird dabei über Gerätetreiber abgebildet.
In Realzeitsystemen laufen mehrere Rechenprozesse scheinbar gleichzeitig ab (quasi parallel). Neben den
Rechenprozessen, die applikationsspezifisch sind, gibt es in einem Realzeitsystem oftmals auch Rechenprozesse, die zum Betriebssystem gehören (Dienstprogramme). Bei solch einem Dienstprogramm kann es
sich beispielsweise um einen Rechenprozess handeln, der Netzwerkdienste (z.B. HTTP-Server) bedient.
Eine andere Gruppe von Rechenprozessen könnte sich bei einem Realzeitsystem um die Visualisierung des
Zustandes des externen Prozesses kümmern.
Fußnoten
1. Mit externer Prozess wird hier der Prozess bezeichnet, der durch das Realzeitsystem gesteuert wird,
ohne dass er direkt Teil des Realzeitsystems ist. Nach dieser Definition besteht das Gesamtsystem aus
dem externen (technischen) Prozess und dem Realzeitsystem.
8
Kapitel 2. Realzeitbetrieb
Unter schritthaltender Verarbeitung bzw. Echtzeitbetrieb versteht man die fristgerechte Bearbeitung von
Anforderungen aus einem technischen Prozess. Was aus Sicht des technischen Prozesses „Anforderungen“
sind, sind aus Sicht der Steuerung Ereignisse. Die Steuerung muss also Ereignisse fristgerecht bearbeiten.
Können die Ereignisse durch die Steuerung nicht innerhalb der durch den technischen Prozess gestellten
Zeitschranken bearbeitet werden, sind die Folgen möglicherweise katastrophal. Daher ist es notwendig, die
zeitlichen Anforderungen und die zeitlichen Kenndaten bezüglich der Steuerung (der Rechenprozesse und
deren Ablaufumgebung) zu kennen.
Daher werden in diesem Kapitel die Zeitschranken des technischen Prozesses formal eingeführt. Außerdem
werden Methoden vorgestellt, den sogenannten Echtzeitnachweis durchzuführen.
Dabei sind folgende Aspekte von Bedeutung:
•
Zur Bearbeitung von Aufgaben wird Zeit (Verarbeitungszeit) benötigt.
•
Beim Anliegen mehrerer Aufgaben muss die Reihenfolge, in der die Arbeiten erledigt werden, geplant
werden.
•
Die richtige Reihenfolge der Aufgaben ist entscheidend, um die zeitgerechte Bearbeitung der Aufgaben
gewährleisten zu können.
•
Als Planungsgrundlage bekommen die einzelnen Aufgaben Prioritäten gemäß ihrer Wichtigkeit.
•
Die Bearbeitung einer Aufgabe (durch einen Prozess) muss unterbrechbar sein, damit kurzfristige höherpriore Aufgaben erledigt werden können.
2.1. Echtzeitbedingungen
Der technische Prozess stellt Anforderungen unterschiedlicher Art an die Echtzeitsteuerung. Alle Anforderungen müssen aber von der Steuerung bearbeitet werden können, auch wenn sämtliche Ereignisse
zu einem Zeitpunkt auftreten (erste Echtzeitforderung).
Der zeitliche Abstand zwischen zwei Anforderungen gleichen Typs wird Prozesszeit (tP) genannt. Ist der
zeitliche Abstand zweier Ereignisse konstant, spricht man von einem periodischen Ereignis. Vielfach
schwankt jedoch der zeitliche Abstand, so dass es einen minimalen zeitlichen Abstand tPmin und einen
maximalen zeitlichen Abstand tPmax gibt.
Der Auftrittszeitpunkt eines zu einem Rechenprozess gehörenden Ereignisses wird mit tA (im englischen
auch mit Releasetime) bezeichnet. Wird der Auftrittszeitpunkt nicht absolut, sondern relativ zur Prozesszeit
angegeben, spricht man von der sogenannten Phase tph. So tritt für uns bei periodischen Zeitpunkten das
erste Ereignis vielfach zum Zeitpunkt »tA=0« auf, die Phase für dieses Ereignis beträgt ebenfalls tph=0.
Nimmt man ein Ereignis mit der Periode tP=5ms und der Phase tph=4ms, tritt das Ereignis zu den folgenden
Zeitpunkten tA auf: 4ms, 9ms, 14ms usw.
Das zeitliche Auftreten der Ereignisse wird in einer Anforderungsfunktion dargestellt. Dazu werden die
Anforderungen (sichtweise des Steuerungs- bzw. Betriebssystems) bzw. Ereignisse (Sichtweise des technischen Prozess) in Form von Pfeilen über der Zeit aufgetragen (siehe Abbildung [Anforderungsfunktion]).
9
Kapitel 2. Realzeitbetrieb
Eintritt des Ereignisses E
E
E
E
E
E
E
t[T]
tPmin
tPmax
tPmin = 2T
tPmax= 6T
tp
=
2T+6T
= 4T
2
ts = 3T
Abbildung 2-1. Anforderungsfunktion
Für die eigentliche Bearbeitung des Ereignisses werden vom Rechner Anweisungen abgearbeitet. Die zum
Ereignis gehörende Folge von Anweisungen wird hier als Codesequenz oder auch als Job bezeichnet. Jobs
sind innerhalb des Rechners als Rechenprozesse realisiert, die sich selbst wiederum in Tasks und Threads
unterscheiden lassen (siehe Kapitel [Prozess-Management]). Manchmal bilden auch mehrere Codesequenzen zusammen einen solchen Rechenprozess. Für die Abarbeitung des Jobs »i« benötigt der Rechner die
sogenannte Verarbeitungszeit tE,i. Die Verarbeitungszeit – im Englischen auch Execution-Time – ist insbesondere bei modernen Prozessoren nicht konstant. Es gibt eine minimale Verarbeitungszeit tEmin und eine
maximale Verarbeitungszeit tEmax. Die Execution-Time ist das Verhältnis aus aus der zu leistenden Rechenarbeit RA und der Leistung P des eingesetzten Prozessors (Rechnerkern).
tE=RA/P
Die Angabe der Verarbeitungszeit ist in der Praxis problematisch, da zum einen – abhängig von der Aufgabenstellung oder/und bedingt durch die Eingabewerten – die Rechenarbeit RA oft schwankt und zum
anderen auch die Leistung des Prozessor bedingt durch Caches und z.B. DMA nicht konstant ist. Zur
Berechnung bei Echtzeitsystemen muss tEmax,i und damit RAmax und Pmin angesetzt werden.
Die für den Worst-Case anzunehmende maximale Verarbeitungszeit
der
Literatur
auch
Worst-Case-Execution
Time
(WCET)
genannt
[Abschätzung der Worst Case Execution Time (WCET)]).
tEmax wird in
(siehe
Kapitel
2.1.1. Auslastung
Abhängig von der Auftrittshäufigkeit eines Ereignisses und der damit anfallenden Verarbeitungszeit ist der
Rechnerkern (Prozessor) ausgelastet. Die Auslastung (ρ, englisch utilization) durch eine Anforderung ergibt
sich damit als Quotient aus notwendiger Verarbeitungszeit und Prozesszeit:
ρ=
tE
tP
Im Worst Case ist die Auslastung
ρmax =
10
tEmax
tP min
Kapitel 2. Realzeitbetrieb
Ein Realzeitsystem muss in der Lage sein, die auftretenden Anforderungen in der Summe bearbeiten zu
können (oft auch als Forderung nach Gleichzeitigkeit bezeichnet). Mathematisch bedeutet dieses, dass die
Gesamtauslastung ρges kleiner als 100% sein muss.
Die Gesamtauslastung ergibt sich aus der Summe der Auslastungen der einzelnen Jobs.
Gleichung 2-1. Gesamtauslastung
ρges =
n
X
tEmax,i
i=1
tP min,i
≤1
te A
Prozessor−
belegung
ρ =
Rechenprozess A
A
te B
Rechenprozess B
ρ =
Rechenprozess A + B
ρ =
B
A+B
t
tPB
te A
tp
A
te B
tp
B
te A
tP
A
+
te B
tP
B
tpA
Abbildung 2-2. Auslastung
Abbildung [Auslastung] verdeutlicht die Auslastung grafisch. Ein Echtzeitrechner bearbeitet zwei Rechenprozesse (Jobs): Rechenprozess A und Rechenprozess B. Sei die Verarbeitungszeit des Rechenprozesses A
tE,A=0.8ms und die Prozesszeit tP,A=2ms ergibt sich eine Auslastung des Rechners durch den Rechenprozess
A von 40%.
Ist die Verarbeitungszeit des Rechenprozesses B ebenfalls tE,B=0.8ms und hat dieser auch eine Prozesszeit
von tP,B=2ms ergibt sich die gleiche Auslastung wie die des Rechenprozesses A von ρB=40%.
Die Gesamtauslastung des Rechners - er bearbeitet ja beide Rechenprozesse - beträgt nun ρGes.=ρA +
ρB=80%.
Erste Echtzeitbedingung: Die Auslastung ρges eines Rechensystems muss kleiner oder gleich 100%
sein.
Die Auslastungsbedingung ist eine notwendige, aber keine hinreichende Bedingung.
2.1.2. Pünktlichkeit
Aus dem technischen Prozess kommen immer wieder Anforderungen an die Echtzeitsteuerung. Aufgabe
der Steuerung ist es, alle Anforderungen zeitgerecht zu bearbeiten. Alle bedeutet in diesem Falle, dass der
Rechner leistungsfähig sein muss, um mit der vorgegebenen Last fertig zu werden. Zeitgerecht bedeutet,
dass der Rechner die Anforderungen rechtzeitig bzw. pünktlich bearbeitet.
Das wesentliche Kriterium für ein Realzeitsystem ist Pünktlichkeit in Abgrenzung zu Schnelligkeit.
11
Kapitel 2. Realzeitbetrieb
Allerdings lassen sich Anforderungen an Pünktlichkeit mit schnellen Systemen leichter erfüllen als mit
langsamen. Prinzipiell muss ein Realzeitsystem eine Aufgabe in einem vorgegebenen Zeitfenster erfüllt
haben, wie es das tut, ist dabei unwesentlich.
tatsächlich
tDmin
t0
Ereignis
tritt ein
erlaubt
Reaktions−
Bereich
des
Rechners
tRmax tDmax t
Abbildung 2-3. Reaktionsbereich
Unter Pünktlichkeit wird hier also verstanden, dass die Aufgabe
1. nicht vor einem weiteren spezifizierten Zeitpunkt tDmin erledigt ist und
2. bis zu einem spezifizierten Zeitpunkt tDmax erledigt wird (Rechtzeitigkeit).
Die Forderung, dass eine Ausgabe nicht vor einem bestimmten Zeitpunkt durchgeführt wird, fehlt oft
(tDmin=t0) und ist in den übrigen Fällen vielfach trivial zu erfüllen. Die Forderung nach Rechtzeitigkeit stellt
dagegen hohe Anforderungen an das System und damit letzlich auch an den Ingenieur bzw. Informatiker,
der das System konzipiert und realisiert.
Formal wird die Forderung nach Rechtzeitigkeit als sogenannte maximal zulässige Reaktionszeit tDmax ausgedrückt. Bei der maximal zulässigen Reaktionszeit handelt es sich um eine Relativzeit beginnend mit dem
Eintreten eines Ereignisses. Die maximal zulässige Reaktionszeit, wie sie hier gebraucht wird, beinhaltet
nur die Zeiten, die für die Rechenprozesse relevant sind. Unter maximaler zulässiger Reaktionszeit wird
also der Zeitpunkt verstanden, bis zu dem der Rechenprozess seine Ausgabe gemacht haben muss. Es wird
nicht der Zeitpunkt verstanden, bis zu dem der technische Prozess einen definierten Zustand eingenommen haben muss. Sind daher die Randbedingungen des technischen Prozesses bekannt, muss die maximal
zulässige Reaktionszeit des Rechenprozesses daraus abgeleitet werden.
Mit maximaler Reaktionszeit tRmax wird der Zeitpunkt bezeichnet, bis zu dem durch den Rechner eine
Reaktion in jedem Fall erfolgt ist. Es handelt sich um eine Worst Case Zeit. Die maximale Reaktionszeit
muss also immer kleiner sein als die maximal zulässige Reaktionszeit.
Weiche
v=1
To You
m
s
Barcodeleser
300 mm
Umschaltzeit=200ms
m
t300= sv = 0.3
1 m s = 300 ms
tDmax= t300
tU = 100 ms
Abbildung 2-4. Maximal zulässige Reaktionszeit bei einer Rohrpost
12
Kapitel 2. Realzeitbetrieb
Beispiel 2-1. Rohrpostsystem
In einem Rohrpostsystem bewegen sich Postbehälter, die durch einen Strichcode gekennzeichnet sind. Jeweils 30 cm vor einer Rohrpostweiche identifiziert ein Realzeitrechner mittels Barcodeleser den Postbehälter
und stellt daraufhin die Weiche. Die Postbehälter haben eine Geschwindigkeit von v=1m/s. Die Weiche hat
eine Stellzeit von 200 ms.
Aus diesen Daten ergibt sich, dass der Postbehälter ab dem Barcodeleser bis zur Weiche 300 ms benötigt.
Da die Weiche jedoch noch eine Stellzeit von 200 ms hat, muss der Realzeitrechner spätestens nach 100ms
der Weiche die Anweisung übermitteln, in welches Rohr der Postbehälter weitergeleitet werden muss. Die
maximal zulässige Reaktionszeit beträgt damit tDmax=100ms. Die maximale Reaktionszeit tRmax kann aufgrund fehlender Informationen nicht angegeben werden. Diese Zeit basiert nämlich auf der Leistung des
verwendeten Realzeitsystems und der dort ablaufenden Applikation.
Das Ereignis, das den Bezugspunkt für die Reaktionszeit darstellt, kann beispielweise durch eine Zustandsänderung im technischen Prozess charakterisiert sein (Postbehälter angekommen, Temperaturwert überschritten).
Trägt man das Auftreten der Ereignisse über der Zeit auf, erhält man die sogenannte Anforderungsfunktion
(Abb. Anforderungsfunktion).
Der Zeitabstand zwischen zwei Ereignissen gleichen Typs wird als Prozesszeit tP bezeichnet. Oftmals ist die
maximal zulässige Reaktionszeit tDmax durch die Anforderung definiert, dass ein Ereignis vor dem Eintreffen
eines nachfolgenden Ereignisses gleichen Typs bearbeitet sein muss (tDmax≤tPmin).
Die Prozesszeit tPist nicht immer konstant. Daher wird man im Regelfall bei Berechnungen, ob die Realzeitbedingungen erfüllt sind oder nicht, die minimale Prozesszeit tPmin einsetzen.
Zur Berechnung der durchschnittlichen Auslastung ist eine Mittelwertbildung (bei einer Gleichverteilung
zum Beispiel der statistische Mittelwert) durchzuführen.
Nachdem ein Ereignis eingetreten ist, kann in den meisten Fällen der zugehörige Rechenprozess nicht direkt
die Bearbeitung des Ereignisses beginnen, da beispielsweise die Bearbeitung eines anderen Ereignisses
noch nicht abgeschlossen ist. Der Rechenprozess muss warten. Diese Zeit wird als Wartezeit tw oder auch
Verzögerungszeit beschrieben.1
Damit ergibt sich die Reaktionszeit tR aus der Summe der Wartezeit tW und der Verarbeitungszeit tE.
Gleichung 2-2. Reaktionszeit
tR = tE + tW
RK
111
000
000
111
10
20
30
40
11
00
00
11
50
60
70
80
11
00
00
11
90
100
110
120
t
Abbildung 2-5. Rechnerkernbelegung durch 4 Rechenprozesse
Trägt man die Verarbeitungszeit tE über die Zeit auf, erhält man die sogenannte Rechnerkernbelegung (Abb.
Rechnerkernbelegung durch 4 Rechenprozesse). Sie gibt Auskunft darüber, zu welchen Zeiten die Verarbeitungseinheit mit welcher Aufgabe betraut ist. Oftmals trägt man die Anforderungsfunktion zusammen
mit der Rechnerkernbelegung in ein Diagramm ein.
13
Kapitel 2. Realzeitbetrieb
Merke: Um eine Anforderung i pünktlich (in Echtzeit) zu erledigen, muss die Reaktionszeit größer oder
gleich der minimal zulässige Reaktionszeit, aber kleiner oder gleich der maximal zulässigen Reaktionszeit sein (für alle tR):
tDmin,i≤tR ≤tDmax,i
Gleichung 2-3. Pünktlichkeitsbedingung
tDmin ≤ tRmin ≤ tRmax ≤ tDmax
2.1.3. Harte und weiche Echtzeit
Kosten
Deadline
harte Echtzeit
weiche Echtzeit
t
Dmin
t
Dmax
Zeit
Abbildung 2-6. Harte und weiche Echtzeit als Kostenfunktion
Die Forderung nach Pünktlichkeit ist nicht bei jedem System gleich stark. Während das nicht rechtzeitige
Absprengen der Zusatztanks bei einem Raumschiff zum Verlust desselbigen führen kann, entstehen durch
Verletzung der Pünktlichkeitsbedingung bei einem Multimediasystem allenfalls Komfortverluste. Vielfach
findet man den Begriff harte Echtzeit, wenn die Verletzung der Realzeitanforderungen katastrophale Folgen hat und daher nicht toleriert werden kann. Man findet den Begriff der weichen Echtzeit, wenn die
Verletzung der Realzeitanforderungen verkraftet werden kann. Allerdings kann man nicht ausschließlich
zwischen „harter“ und „weicher“ Echtzeit unterscheiden, sondern jegliche Abstufung zwischen den beiden
Extremen (weich und hart, soft und hard) ist möglich. Die Graduation hängt von der Aufgabenstellung ab.
Die Unterschiede bei der Forderung nach Pünktlichkeit werden oft anhand einer Kostenfunktion verdeutlicht. Bei den sogenannten weichen Echtzeitsystemen bedeutet das Verletzen der Realzeitbedingung einen
leichten Anstieg der Kosten. Bei den sogenannten harten Echtzeitsystemen steigen jedoch die Kosten durch
die Verletzung der Realzeitbedingung massiv an.
14
Kapitel 2. Realzeitbetrieb
Benefit
no realtime requirements
100 %
soft realtime requirements
hard realtime requirements
t0
t
Dmin
t
t
Dmax
t
Dmin minimal zulässige Reaktionszeit
t
Dmax maximal zulässige Reaktionszeit
Abbildung 2-7. Benefit-Function
Eine andere Art der Darstellung ist die sogenannte Benefitfunction (Nutzenfunktion). Hier wird der Nutzen
der Reaktion des Realzeitrechners auf das Ereignis über die Zeit aufgetragen. Bei Systemen ohne Echtzeitanforderungen ist der Nutzen unabhängig von dem Zeitpunkt der erfolgten Reaktion, bei einem absolut
harten Echtzeitsystem ist nur dann ein Nutzen vorhanden, wenn die Reaktion innerhalb des durch tDmin
und tDmax aufgestellten Zeitfensters erfolgt. Dazwischen ist jegliche Abstufung an Realzeitanforderungen
möglich. Hier kann man (je nach Graduation) von weichen Realzeitanforderungen sprechen.
2.2. Latenzzeiten
Mit der Latenzzeit gibt es noch weitere Zeiten, die für die Analyse eines Echtzeitsystems von Bedeutung
sind.
Die Zeit, die zwischen Anforderung und Start der zugehörigen Codesequenz (Bearbeitung) vergeht, nennt
man Latenzzeit tL. Man unterscheidet die Interrupt-Latenzzeit von der Prozess-Latenzzeit. Die Zeit zwischen Auslösen eines Interrupts und Start der zugehörigen Interrupt-Service-Routine wird als InterruptLatenzzeit bezeichnet, die Zeit zwischen einer Anforderung (das kann ebenfalls ein Interrupt sein) und
dem Start des zugehörigen Rechenprozesses bezeichnet man als Prozess-Latenzzeit.
Vergleich Reaktions- und Latenzzeit:
Reaktionszeit: Zeit zwischen einer Anforderung und dem Ende der Bearbeitung.
Latenzzeit: Zeit zwischen einer Anforderung und dem Start der Bearbeitung.
Soll die Reaktionszeit tR in Abhängigkeit von der Latenzzeit ausgedrückt werden, muss noch die Unterbrechungszeit eingeführt werden. Die Unterbrechungszeit tU ist die Zeit, die ein Rechenprozess nach dem
Start nicht arbeitet, entweder, weil der Rechenprozess durch einen anderen verdrängt ist oder weil er auf
Betriebsmittel wartet.
tR=tL+ tU+tE
tW=tL+ tU
15
Kapitel 2. Realzeitbetrieb
2.3. Unterbrechbarkeit und Prioritäten
Ist die Auslastungsbedingung (siehe [Auslastung]) erfüllt, die Pünktlichkeitsbedingung aber nicht, kann
unter Umständen eine schritthaltende Verarbeitung dennoch erreicht werden. Hierzu sind allerdings weitere Maßnahmen, insbesondere bezüglich der Systemsoftware und der Architektur der Realzeitapplikation
notwendig:
1. Die gestellte Aufgabe muss ich in (unabhängige, parallel ablaufende) Teilaufgaben zerlegen lassen und
2. die einzelnen Teilaufgaben (Jobs) müssen unterbrechbar sein und müssen priorisiert werden.
Hierzu ein simples Beispiel:
100 Meßwerte
erfaßt
Meßwerte
Aufnehmen
Meßwerte
Sichern
Abbildung 2-8. Datenflußdiagramm Meßwertaufnahme
Beispiel 2-2. Unterbrechbarkeit bei der Meßwerterfassung
Ein Meßwerterfassungssystem soll im Abstand von 1 ms (tP,1=1ms) kontinuierlich Meßwerte aufnehmen.
Dazu benötigt der zugehörige Rechenprozess eine Rechenzeit von tE,1=500 µs. Jeweils 100 Meßwerte
(tP,2=100tP,1=100ms) ergeben einen Datensatz, der vorverarbeitet und zur Archivierung weitergeleitet wird.
Dazu ist eine Rechenzeit von tE,2=40ms notwendig.
Überprüft man an diesem Beispiel die 1. Echtzeitbedingung, so ist diese bei einer Auslastung von
ρ=tE,1/tP,1 + tE,2/tP,2 = 0.5 + 0.4 = 0.9 = 90%
erfüllt. Dennoch läßt sich die Aufgabe ohne weitere Maßnahmen nicht in Echtzeit erfüllen.
16
Kapitel 2. Realzeitbetrieb
while( 1 )
for( i=0; i<100; i++ )
Sende Timer−Event in 1 ms
Nimm Meßwert auf
Warte auf Timer−Event
Verarbeite Datensatz
Archiviere Ergebnis
Abbildung 2-9. Struktogramm Meßwerterfassung sequentiell
Anforderung
97 98
99 100 101 102 103
Meßwertaufnahme
140
t[ms]
Verarbeitung und Archivierung
Abbildung 2-10. Zeitdiagramm Meßwert sequentiell
Abbildung [Struktogramm Meßwerterfassung sequentiell] visualisiert eine Lösung der Aufgabenstellung,
bei der die Aufgabenstellung in einem einzigen Rechenprozess abgearbeitet wird. Zunächst werden die
100 Meßwerte aufgenommen, dann verarbeitet. Wie in Abbildung [Zeitdiagramm Meßwert sequentiell] ersichtlich, startet gemäß Struktogramm nach 100ms die Codesequenz Verarbeitung, welche den Prozessor
für 40ms belegt. Allerdings müssen natürlich auch während dieser Zeit weiterhin Meßwerte aufgenommen
werden, das ist aber bei der gewählten Struktur nicht möglich.
Um das Problem dennoch zu lösen, wird die Aufgabe auf zwei unabhängige Rechenprozesse
aufgeteilt. Rechenprozess »Erfassung« ist für die Meßwertaufnahme verantwortlich, Rechenprozess
»Verarbeitung« für die Verarbeitung der Werte und Sicherung der Ergebnisse. Über sogenannte
Inter-Prozess-Kommunikation (IPC) informiert Rechenprozess »Erfassung« den Rechenprozess
»Verarbeitung«, dass 100 Meßwerte erfaßt wurden. Aus Systemsicht stellt »Erfassung« eine Anforderung
respektive bekommt »Verarbeitung« ein Ereignis zugestellt. Dadurch wird Rechenprozess »Verarbeitung«
aktiv. Er verarbeitet die Daten und sichert das Ergebnis. Damit keine Meßwerte verloren gehen, muss
jetzt noch Rechenprozess »Verarbeitung« unterbrechbar gemacht werden, das bedeutet, dass jedesmal,
wenn der Timer-Event auftritt, Rechenprozess »Verarbeitung« kurz unterbrochen wird (man spricht von
Preemption) und der Rechenprozess »Erfassung« den neuen Meßwert erfassen kann.
Dass Aufgrund der Ereignisse ein (zugehöriger) Rechenprozess aktiv wird, besorgt die Systemsoftware (Betriebssystem). Den Wechsel zwischen den beiden Rechenprozessen bezeichnet man als Kontextswitch2. Ein
Kontextswitch benötigt Zeit, die jetzt auch noch in die Berechnung der Auslastung (1. Echtzeitbedingung)
mit einfließen muss. Wir werden jedoch in unseren Betrachtungen davon ausgehen, dass die Kontextwechselzeit so gering ist, dass sie vernachlässigt werden kann.
17
Kapitel 2. Realzeitbetrieb
Anmerkung: Die im Beispiel angesprochenen Dienste wie IPC und Unterbrechbarkeit von Rechenprozessen stellt das Echtzeitbetriebssystem zur Verfügung.
Rechenprozess "Verarbeitung"
Rechenprozess "Erfassung"
while( 1 )
while( 1 )
RcvMsg von "Erfassung"
for( i=0; i<100; i++ )
Verarbeitung der Daten
Sende Timer−Event in 1 ms
Archiviere Ergebnis
Nimm Meßwert auf
Warte auf Timer−Event
SendMsg an "Verarbeitung"
Abbildung 2-11. Struktogramm Meßwerterfassung parallel
Anforderung
97 98
99 100 101 102 103
Meßwertaufnahme
140
t[ms]
Verarbeitung
Abbildung 2-12. Zeitdiagramm Meßwert parallel
Die dargestellte Lösung, basierend auf der Parallelisierbarkeit der Aufgaben und der Möglichkeit, einzelne
Rechenprozesse während der Abarbeitung kurz zu unterbrechen, funktioniert nur dann, wenn festlegbar ist,
welcher Rechenprozess unter welchen Umständen andere Rechenprozesse unterbrechen darf. Im einfachsten Fall geschieht dies über die Vergabe von Prioritäten.
Damit im angegebenen Beispiel die Echtzeitanforderungen erfüllt werden, muss daher der Rechenprozess
»Erfassung« höhere Priorität haben als der Rechenprozess »Verarbeitung«.
Es gibt keine einheitliche Definition darüber, wie eine hohe Priorität repräsentiert wird. In manchen Systemen stehen niedrige Zahlen für eine hohe Priorität, in anderen dagegen ist es genau umgekehrt. Bei uns
werden die hohen Prioritäten normalerweise durch niedrige Zahlenwerte gekennzeichnet.
Nicht immer ist die Prioritätenvergabe so einfach und eindeutig wie im Beispiel der Meßwerterfassung.
Oft sind im System eine Vielzahl unterschiedlicher Rechenprozesse aktiv. Um hier Prioritäten vergeben zu
können, kann man initial nach folgender Faustformel verfahren:
Merke: Prozesse mit kurzen Rechenzeiten tE haben in der Regel auch kurze Prozesszeiten tP und
bekommen hohe Prioritäten.
18
Kapitel 2. Realzeitbetrieb
Prozesse mit langen Rechenzeiten tE haben in der Regel auch lange Prozesszeiten tP und bekommen
niedrige Prioritäten.
Besteht die in der Faustformel angesprochene Korrespondenz zwischen tE und tP nicht, wird man bei der
Vergabe meist die kürzere Prozesszeit oder auch kürzere maximal zulässige Reaktionszeit tDmax als Indikator
für eine höhere Priorität ansetzen. Allerdings ist in einem solchen Fall immer noch der gesunde Menschenverstand (und später der noch folgende Echtzeitnachweis) erforderlich.
Variable Prozesszeiten. Das Problem der Prioritätenvergabe und Auslastungsberechnung wird auch
dadurch erschwert, dass in der Realität nicht alle Prozesssignale – wie in den Beispielen angenommen
– mit konstanter Periode anliegen. Vielmehr variieren Prozesszeiten abhängig von der zu lösenden
Aufgabenstellung (zum Beispiel vom physikalischen Prozess).
Sind sichere Informationen, über die Auftrittshäufigkeit der Ereignisse des technischen Prozesses verfügbar,
läßt sich zwar die Auslastung berechnen (und damit die Anforderungen bezüglich der 1. Echtzeitbedingung
überprüfen), damit läßt sich aber nur sehr schwer das Einhalten der 2. Echtzeitbedingung überwachen (zumindest solange keine Informationen über die Mindestabstände zwischen den Ereignissen vorliegen).
Fußnoten
1. Später werden wir noch eine Sleeptime (Schlafenszeit) kennenlernen. Die Schlafenszeit unterscheidet
sich von der Verzögerungszeit dadurch, dass der Job selbst die Schlafensdauer festgelegt hat – er möchte
für die gewählte Zeit suspendiert werden. Die Wartezeit dagegen wird ihm vom System aufgezwungen,
weil das System auch andere Jobs zu bedienen hat und nicht einem Job exklusiv zur Verfügung steht.
2. Die Einheit, die den Kontextswitch durchführt, wird Scheduler genannt.
19
Kapitel 3. Realzeitbetriebssysteme
Lernziele:
•
•
•
•
•
•
•
•
•
Aufbau und Anforderungen des Betriebssystems.
Philosophien und Konzepte in Betriebssystemen.
Prinzipielles Verständnis für die Funktionsweise eines Betriebssystems bekommen.
Wichtige Komponenten des Betriebssystems beschreiben und erklären können (Aufgabe und
Arbeitsweise).
Datenfluß durch das Betriebssystem.
Was ist ein Systemcall?
Unterschied Systemcall - Funktionsaufruf.
Schedulingverfahren.
Aufgaben und Aufbau einer Memory-Management-Unit.
Definition. Aus Systemsicht ist ein Betriebssystem die Bezeichnung für alle Softwarekomponenten, die
•
•
die Ausführung der Benutzerprogramme,
die Verteilung der Betriebsmittel (z.B. Speicher, Prozessor, Dateien),
ermöglichen, steuern und überwachen.
Das Betriebssystem stellt dem Benutzer die Sicht eines virtuellen Prozessors zur Verfügung, der einfacher
zu benutzen ist als die reale Hardware, z.B.:
•
•
Aus Sicht eines Benutzers steht der Rechnerkern (CPU) ihm allein zur Verfügung.
Einfacher Zugriff auf Ressourcen wie Speicher, Geräte, Dateien (das wird erreicht durch Speichermanagement, Gerätetreiber und das Dateisystem).
Das Betriebssystem besteht aus einem Betriebssystemkern und aus sonstigen Systemkomponenten wie
beispielsweise den Dienstprogrammen (z.B. die Shell).
An das Betriebssystem werden die folgenden Anforderungen gestellt:
•
Zeitverhalten
•
•
•
Schnelligkeit
Bei einem RTOS insbesondere die Realisierung kurzer Antwortzeiten.
Zeitlicher Determinismus
•
Geringer Ressourcenverbrauch
• Hauptspeicher
• Prozessorzeit
•
Zuverlässigkeit und Stabilität
• Programmfehler dürfen das Betriebssystem und andere Programme nicht beeinflussen.
•
Sicherheit
• Dateischutz, Zugangsschutz
•
Flexibilität und Kompatiblität
• Erweiterbarkeit
• Einhalten von Standards (z.B. POSIX)
• Möglichkeit, für andere BS geschriebene Programme auszuführen
20
Kapitel 3. Realzeitbetriebssysteme
•
•
Portabilität
Skalierbarkeit
Ein Realzeitbetriebssystem hat insbesondere die Aufgabe:
•
•
•
die Ressourcenverteilung sicherzustellen,
den deterministischen Ablauf (insbesondere Scheduling) zu garantieren und
die Angabe und das Einhalten von Zeitbedingungen zu ermöglichen.
Bild Realzeitsysteme und ihre Zeitanforderungen [TimMon97] spiegelt die Größenordnung wider, unter denen Realzeitbetriebssysteme Verwendung finden. Hat man Zeitanforderungen im Minutenbereich, läßt sich
ein System durchaus noch von Hand steuern, unterhalb dieser Marke (etwa 1 Minute) reicht eine Automatisierung auf Basis von Mechanik aus. Zeitanforderungen im Sekunden-Bereich lassen sich durchaus mit
einem Standardbetriebssystem erfüllen, im Millisekunden-Bereich jedoch benötigt man ein Realzeitbetriebssystem. Gilt es Anforderungen im Mikrosekunden-Bereich zu erfüllen, müssen die notwendigen Aktionen
innerhalb einer ISR durchgeführt werden. Unterhalb dieser Grenze hilft nur noch eine Realisierung in Hardware.
3.1. Aufbau und Struktur
Applications
Services
Libraries
Sonstige BS−Komponenten
Syscall Interface
IO
Management
Process
Management
Memory
Management
Kernel
Device−Driver−Layer
Hardware
Abbildung 3-1. Betriebssystem-Architektur
Bild Betriebssystem-Architektur stellt den prinzipiellen Aufbau eines Betriebssystems dar. Die
Treiberschicht abstrahiert Zugriffe auf die Hardware und stellt eine betriebssysteminterne, standardisierte
Schnittstelle zur Verfügung, um in das System neue Hardwarekomponenten systemkonform zu
integrieren. Gerade in Realzeitsystemen ist die Treiberschicht von zentraler Bedeutung. Denn nur wenn
zusätzliche (proprietäre) Hardware systemkonform in das System integriert wird, kann der versprochene
Determinismus des Betriebssystems auch gewährleistet werden.
Die Integration von Hardware in das System über die Treiberschicht ermöglicht darüber hinaus auch den
systemkonformen und standardisierten Zugriff auf Hardware aus der Applikation heraus. Hierfür sorgt das
I/O Subsystem eines Betriebssystems. Bestimmte Gruppen von Hardware (z.B. Netzwerkkarten, SCSI,
21
Kapitel 3. Realzeitbetriebssysteme
Filesysteme, PCI) benötigen ähnliche Funktionalitäten. Diese zur Verfügung zu stellen ist ebenfalls Aufgabe des I/O Subsystems.
Eine wichtige Aufgabe für Betriebssysteme ist die Verteilung der Ressource CPU (Rechnerkern) auf
mehrere Tasks, das sogenannte Scheduling. Diese Aufgabe wird durch den Block Process-Management
dargestellt.
Speichermanagement (Adressumsetzung und Speicherschutz) ist eine weitere wesentliche Aufgabe. Es
darf nicht möglich sein, dass eine normale Applikation ein komplettes Rechnersystem zum Absturz bringt.
Dazu müssen aber die Speicherräume der unterschiedlichen Applikationen gegeneinander abgeschottet sein,
ein Speicherschutzmanagement ist notwendig. Die Komponente dazu heißt Memory-Management-Unit
(MMU) . Speicherschutz wird heutzutage massiv durch Hardware unterstützt.
Über sogenannte Softwareinterrupts können Applikationen Dienste des Betriebssystems in Anspruch
nehmen. Man spricht hier von sogenannten Systemcalls. Die Schnittstelle innerhalb des Betriebssytems,
die gemäß des ausgelösten Systemcalls den richtigen Dienst ausführt, wird als Systemcall-Interface
bezeichnet.
Die bisher genannten Blöcke gehören alle zum sogenannten Betriebssystemkern. Auf der dem Kern
gegenüberstehenden User-Ebene befinden sich die Applikationen, Dienstprogramme und Libraries.
Dienstprogramme sind zum Betrieb des Rechnersystems notwendig. Beispielsweise werden für
die Konfiguration des Systems (Zuteilung von Netzwerkadressen u.ä.) und auch zum Betrieb der
Netzwerkdienste Dienstprogramme benötigt. Die Programme selbst greifen in den seltensten Fällen direkt
über die Systemcall-Schnittstelle auf die Dienste des Betriebssystems zu. Im Regelfall sind diese in einer
Bibliothek gekapselt, die Standardfunktionen (beispielsweise open, close, read und write) zur Verfügung
stellt. Innerhalb der Library ist dann der Systemcall selbst auscodiert.
Natürlich sind die Dienstprogramme des Betriebssystems im strengen Sinne auch Applikationen. Es werden hier nur deshalb zwei Begriffe verwendet, um die zum Betriebssystem gehörigen Applikationen - eben
die Daemonen, Services oder einfach Dienstprogramme - von den selbstgeschriebenen Applikationen unterscheiden zu können.
Beim Aufbau eines Realzeitsystems erweitert der Ingenieur/Informatiker eventuell an zwei Stellen das Betriebssystem:
1. bei der Integration neuer Hardware (Treiber),
2. bei der Integration zusätzlicher Kernelmodule.
3.2. Betriebssystemkern
Im Folgenden werden die einzelnen Komponenten, die ein Betriebssystem bilden, vorgestellt.
3.2.1. Prozess-Management
Die primäre Aufgabe des Prozess-Management ist die Verteilung der Ressource »CPU«.
Die Fähigkeit der CPU, den normalen Programmablauf zu unterbrechen, wenn ein Interrupt auftritt, wird
genutzt, um die quasi parallele Bearbeitung mehrere Programme (Tasks) zu ermöglichen.
Tritt nämlich ein Interrupt auf, legt die CPU den Programmcounter auf den Stack (automatische Rettung
des Befehlszählers) und eventuell den Inhalt des (internen) Flagregisters, sperrt weitere Interrupts und ruft
die sogenannte Interrupt-Service-Routine (ISR) auf. Die Adressen der ISR’s befinden sich dabei in einer
22
Kapitel 3. Realzeitbetriebssysteme
Tabelle, wobei es für jede mögliche Unterbrechungsursache (z.B. externern Hardwareinterrupt auf Level 0,
Softwareinterrupt oder unerlaubter Speicherzugriff) einen Tabelleneintrag (Vektortabelle) gibt.
Am Ende der ISR steht der Befehl "Return Interrupt" (iret, reti). Dieser Befehl restauriert wieder das
Prozessor-Flag-Register und lädt den zuvor auf den Stack befindlichen Wert des Programmcounters in den
Programmcounter, wodurch die Abarbeitung der ursprünglichen Aufgabe fortgesetzt wird.
Address
0x1000
0x1002
Normal
0x1004
0x1006 Program−Execution
0x1008
0x100a
PC
ISR
0xa000
0xa002
0xa004
Stack
1
2
registers
7
0x1006
...
save registers
3
interrupt
treatment
4
restore registers
5
return interrupt
6
Code Memory
PC = Program Counter
Abbildung 3-2. Interruptverarbeitung
Bild Interruptverarbeitung verdeutlicht die Vorgänge noch einmal:
1. Die CPU arbeitet ein Programm ab.
2. Während der Programmabarbeitung wird (z.B. von der Systemuhr) ein Interrupt ausgelöst. Die
Abarbeitung des gerade begonnenen Befehls wird zuende geführt (moderne Prozessoren können bei
Auftreten des Interrupts sogar die Abarbeitung des gerade aktuellen Befehls unterbrechen und müssen
nicht erst auf das Ende der Befehlsabarbeitung warten). Dann wird der Inhalt des Befehlszählers
(0x1006) auf den Stack abgelegt, abhängig vom Prozessor ebenfalls Registerinhalte (beispielsweise
das Flagregister). Aufgrund der Interruptursache wird von der CPU der Befehlszähler (PC) auf die
Startadresse der Interrupt-Bearbeitungsroutine (hier 0xa000) gesetzt, und die CPU arbeitet die Befehle
ab dieser Adresse ab.
3. Die ISR wird zunächst die sonstigen CPU-Register retten.
4. Daran schließt sich die eigentliche Interrupt-Behandlung an.
5. Am Ende der ISR werden die vorher gesicherten CPU-Register wieder zurückgeholt
6. und der Befehl “Return Interrupt” ausgeführt. Der Befehl bewirkt, dass vom Stack die dort eventuell
abgelegten Flagregister zurückgespeichert werden und der PC wieder mit der Rücksprungadresse
(0x1006) geladen wird.
7. Der normale Programmablauf wird damit fortgesetzt.
23
Kapitel 3. Realzeitbetriebssysteme
Interrupt
zum
Kontextwechsel
Retten des Kontextes des
unterbrochenen Rechenprozesses j
eventuelle Auftragsbearbeitung
Scheduler:
Auswahl des nächsten
Rechenprozesses i
Lade Kontext
des Rechenprozesses i
Return zu PCi
Abbildung 3-3. Interrupt-Service-Routine in einem Realzeitbetriebssystem
Address
0x1000
0x1002
Normal
0x1004
0x1006 Program−Execution
0x1008
0x100a
PC
RETI
Stack
1
2
0x1006 0x2000
...
0x2000
Task 3
7
0xa000
0xa002
0xa004
save registers
3
interrupt
treatment
4
Tasklist
schedule
5
complete
Task
Information
current
ISR
contextswitch
return interrupt
0xf020
6
complete
Task
Information
0xf000
0xf020
Code Memory
complete
0x2000
Task
Information
0xf040
Abbildung 3-4. ISR mit Scheduler
In einem Multitasking Betriebssystem wird dieser Mechanismus der Programmunterbrechung
genutzt, um die quasi parallele Abarbeitung mehrerer Aufgaben zu ermöglichen. Dazu wird
bei jedem Interrupt zunächst eine vom Betriebssystem zur Verfügung gestellte ISR (Bild
24
Kapitel 3. Realzeitbetriebssysteme
Interrupt-Service-Routine in einem Realzeitbetriebssystem) aufgerufen. In dieser ISR werden die
Prozessorregister und die auf dem Stack abgelegten Werte (PC und Flags) in eine zum gerade aktiven
Programm gehörigen Datenstruktur (Process Control Block, Bild Process-Kontrollblock (PCB)) abgelegt.
Danach kann die eigentliche Ursache des Interrupts bearbeitet werden. Am Ende der Interruptbearbeitung
wird - das ist im einfachen Fall noch während der ISR - ein nächstes Programm ausgewählt, das die CPU,
den Rechnerkern, zugeteilt bekommt. Dazu werden die Prozessorregister mit den Kopien der Register zum
Zeitpunkt der letzten Unterbrechung der Task geladen, Flags und insbesondere der Programmcounter
werden so auf dem Stack abgelegt, dass die vom Prozessor bei Eintritt des Interrupts abgelegten Werte
überschrieben werden, und es wird schließlich der Befehl “Return Interrupt” ausgeführt. Dadurch wird der
Befehlszähler mit dem “neuen” Wert geladen, und die Bearbeitung der “neuen” Task wird fortgesetzt.
Unterschied zu einer gewöhnlichen ISR ist damit (Bild ISR mit Scheduler), dass
1. die Register nicht auf den Stack gerettet werden, sondern in die Speicherzellen des Process Control
Blocks (PCB) der gerade aktiven Task. Die Adresse des PCB des gerade aktiven Rechenprozesses ist
im Regelfall in einer globalen Variable (“current”) abgespeichert.
2. beim Contextswitch die Register des Prozessors nicht zwangsläufig mit den Werten geladen werden,
die vor der Unterbrechung geladen waren. Stattdessen werden die Register aus dem PCB genommen,
den der Scheduler ausgewählt hat.
3. die Rücksprungadresse auf dem Stack manipuliert wird, d.h. der durch den Prozessor dort bei der
Unterbrechung abgelegte Wert wird mit dem Wert für den PC überschrieben, der sich in dem PCB der
neuen aktiven Task befindet.
Damit ist ersichtlich, dass insbesondere das sogenannte Prozessmanagement, aber eigentlich auch alle anderen Dienste des Betriebssystemkerns über Interrupts angefordert werden.
Zwei Arten von Interrupts werden unterschieden: Softwareinterrupts von Hardwareinterrupts.
•
•
Über Softwareinterrupts (Systemcalls) fordern Benutzerprogramme Dienste des Betriebssystems an.
Hierbei handelt es sich um eine synchrone Unterbrechung, da die Unterbrechung synchron zum
Programmablauf im Programmcode verankert erfolgt.
Über Hardwareinterrupts fordern Hardwarekomponenten (Systemuhr, Platten, Modem usw.) Dienste des
Betriebssystems an.
Mit jedem Interrupt findet damit ein Übergang in den Kernel-Mode (Supervisor-Mode) statt. In der ISR
(Interrupt-Service-Routine) wird zunächst die dem Interrupt zugehörige Aufgabe durchgeführt und danach
die Kontrolle dem Scheduler übergeben, der den nächsten RechenProzess auswählt.
25
Kapitel 3. Realzeitbetriebssysteme
Prozess−Kontrollblock/
Virtuelle CPU
Prozess−Priorität
Quantum
Prozess−Zustand
Exitcode
CPU−Zustand
(Register, Stack usw.)
Abbildung 3-5. Process-Kontrollblock (PCB)
Merke: Dienste des Betriebssystems werden immer über Interrupts angestoßen.
Die den Zustand einer Task (also des im System instanziierten Programms) genau beschreibende Datenstruktur Prozess-Control-Block speichert die folgenden Informationen:
•
•
•
Speicherplätze, in denen jeweils alle Prozessorzustände (Registerinhalte) gespeichert werden, mit
welchen dieser Rechenprozess zum letzten Mal unterbrochen wurde.
Angaben über den Zustand des Rechenprozesses, gegebenenfalls über die Bedingungen, auf die der
Rechenprozess gerade wartet.
Angaben über die Priorität des Rechenprozesses.
Der Prozess-Control-Block spezifiziert damit einen Rechenprozess eindeutig.
Anhand der Informationen im PCB entscheidet der Scheduler, ob eine Task die CPU zugeteilt bekommt
oder nicht.
Start Task
ruhend
Ende der
Wartebedingung
lauffähig
"nicht mehr
höchste
Priorität" schlafend
"höchste
Priorität"
Stopp Task
aktiv
Warten, z.B. auf
− Betriebsmittel
− E/A−Aufruf−Ende
− Zeit
Abbildung 3-6. Prozess-Zustände
26
Kapitel 3. Realzeitbetriebssysteme
Die wichtigste Information im PCB zur Auswahl des nächsten Rechenprozesses ist der Zustand der Task.
Jede Task hat dabei im wesentlichen 4 mögliche Zustände, wie im Bild Prozess-Zustände ersichtlich.
Ruhend
Bei »ruhend« handelt es sich im eigentlichen Sinn nicht um einen Zustand, vielmehr um einen
»Metazustand«. Bevor ein Rechenprozess überhaupt bearbeitet wird, befindet er sich im Metazustand
»ruhend«. Aus diesem Zustand kommt der Prozess nur, wenn er durch das Betriebssystem (welches
im Regelfall durch einen anderen Rechenprozess dazu aufgefordert wurde) gestartet wird. Im
Metazustand »ruhend« existiert im Kernel noch kein PCB für die Task. Der Metazustand »ruhend«
wird oft auch als Zustand terminiert bezeichnet.
Lauffähig
Der Scheduler wählt aus der Liste der Prozesse denjenigen lauffähigen Prozess als nächstes zur Bearbeitung aus, der die höchste Priorität hat. Mehrere Jobs im System können sich im Zustand lauffähig
befinden.
Aktiv
Immer nur ein Rechenprozess im System kann sich im Zustand aktiv befinden. Der aktive Job bekommt
die CPU zugeteilt und wird ausgeführt, bis er entweder
• sich selbst beendet (in den Zustand ruhend) versetzt, oder
• auf ein Betriebsmittel warten muss (z.B. auf das Ende eine I/O-Aufrufes), oder
• nicht mehr die höchste Priorität hat, da beispielsweise die Wartebedingung eines höherprioren
Prozesses erfüllt wurde.
Schlafend
Ein Prozess wird in den Zustand schlafend versetzt, wenn nicht mehr alle Bedingungen zur direkten
Ausführung erfüllt sind. Ein Job kann dabei auf unterschiedliche Bedingungen warten, beispielsweise
auf das Ende von I/O-Aufrufen, auf den Ablauf einer definierten Zeitspanne oder auf das Freiwerden
sonstiger Betriebsmittel.
TCB
TCB
Task
Code Segment
Thread
Data Segment
Stack Segment
Stack Segment
Abbildung 3-7. Speicherbereiche einer Task
Rechenprozesse oder Jobs gibt es in zwei Ausprägungen: den Tasks und den Threads.
27
Kapitel 3. Realzeitbetriebssysteme
Bei Threads handelt es sich um sogenannte leichtgewichtige Prozesse. Sowohl Tasks als auch
Threads bestehen prinzipiell aus einem Speichersegment für Code, Daten und für den Stack (Bild
Speicherbereiche einer Task). Der Unterschied zur Task ist, dass zu einer Task gehörige Threads alle ein
und denselben Adressraum teilen. Das bedeutet also, dass Threads auf dasselbe Daten und Codesegment
zugreifen, nur ein eigenes Stacksegment besitzen.
Tabelle 3-1. Vergleich Task und Thread
Task
Thread
Eigener PCB
Eigener PCB
Eigenes Codesegment
Codesegment der zugehörigen Task
Eigenes Datensegment
Datensegment der zugehörigen Task
Eigenes Stacksegment
Eigenes Stacksegment
Aufgrund dieses Unterschiedes lassen sich Threads
•
•
schneller erzeugen (beim Erzeugen von Tasks werden im Regelfall Datensegmente kopiert, das fällt bei
der Erzeugung von Threads weg).
Darüber hinaus kann - abhängig von der Applikation - eine vereinfachte Inter-Prozess-Kommunikation
(beispielsweise wird kein Shared-Memory benötigt) verwendet werden.
Für das Erzeugen von Tasks und das Erzeugen von Prozessen stellt das Betriebssystem Systemcalls zur
Verfügung (siehe Taskmanagement). Diese Systemcalls legen einen neuen PCB an und kopieren - falls
notwendig - die zugehörigen Speicherbereiche (bei Tasks wird das Datensegment kopiert und nur ein neues
Stacksegment angelegt, bei Threads wird nur ein neues Stacksegment angelegt).
3.2.1.1. Scheduling auf Einprozessormaschinen
Der Ingenieur/Informatiker ist mit Scheduling konfrontiert bei:
•
•
•
•
•
Projektierung eines Realzeitsystems (Auswahl des Verfahrens)
Zerlegung einer Aufgabe (Applikation) in Rechenprozesse/Threads
Festlegung von Prioritäten
Evaluierung von Scheduling-Parameters (z.B. Deadlines)
Implementierung der Rechenprozesse
Unter Scheduling versteht man die Strategie zur Festlegung des nächsten zu bearbeitenden
Rechenprozesses. Das Scheduling ermöglicht die parallele Bearbietung mehrerer Tasks.
Anhand eines konstruierten Beispiels soll die Aufgabe des Schedulers und die Wirkung bzw. Auswirkung
unterschiedlicher Schedulingvarianten vorgestellt werden.
Ein Realzeitsystem wird eingesetzt, um ein Experiment an Bord eines Satelliten zu steuern. Dazu sind
folgende Anforderungen spezifiziert:
•
•
•
28
Alle 1500ms (Zeitpunkt t0) Meßdaten aufnehmen (tP=1500 ms).
Die Meßwertaufnahme muss nach 400 ms abgeschlossen sein (tDmax=400 ms).
Alle 200 ms nach Start der Meßdatenaufnahme t0: Experiment anstossen.
Kapitel 3. Realzeitbetriebssysteme
•
•
•
•
Die Vorbereitungen müssen 500 ms später abgeschlossen sein (also 700 ms nach dem Start der Meßwertaufnahme t0, tP=1500 ms, tDmax=500 ms).
Meßergebnisse müssen innerhalb von 1100 ms (tDmax=1100 ms) zur Erde weitergeleitet werden (alle 60s,
tP=60000 ms).
Zur Steuerung des Experiments steht ein Mikroprozessor zur Verfügung.
Die Verarbeitungszeiten für die drei Aufgaben betragen:
• Task A - Meßdatenaufnahme: t =250 ms
E
• Task B - Datenübertragung: t =500 ms
E
• Task C - Experiment: t =300 ms
E
Job
tEmax
tPmin
tDmin
tDmax
tA
A
250ms
1500ms
0ms
400ms
0ms
B
300ms
1500ms
0ms
500ms
200ms
C
500ms
60000ms
0ms
1100ms
0ms
Daten müssen
aufgenommen
sein
Experiment muß
vorbereitet sein
Übertragung muß
abgeschlossen sein
Experiment
300ms t V
Meßdatenübertragung
500ms t V
Parameter aufnehmen 250ms t V
100 200 300 400 500 600 700 800 900 1000 1100
t[ms]
Abbildung 3-8. Zeitanforderungen (Beispiel)
Aufgrund des Beispieles ergeben sich für die Konzeption des Realzeitsystems folgende Folgerungen:
•
•
•
•
•
Der Prozessor muss mehrere Aufgaben (Tasks) bearbeiten.
Dazu muss jeder Prozess unterbrechbar (preemptiv) sein.
Der Prozessor bearbeitet abwechselnd jeweils einen Teil einer der zu bearbeitenden Tasks.
Der Rechner muss von seiner Leistung her in der Lage sein, die Aufgabenstellung zeitgerecht zu bearbeiten (Auslastung, 1. Echtzeitbedingung).
Das Betriebssystem muss sicherstellen, dass die maximal zulässigen Reaktionszeiten eingehalten werden
(2. Echtzeitbedingung).
Die Einheit im Betriebssystem (oder Laufzeitsystem), die dieses sicherstellt, ist der Scheduler.
Unter Scheduling versteht man die Strategien in einem Betriebssystem zur Festlegung desjenigen Rechenprozesses, der als nächstes bearbeitet werden soll. Das Scheduling ermöglicht damit die quasi-parallele
Bearbeitung mehrerer Aufgaben (Tasks).
Sowohl für die Auswahl des nächsten Rechenprozesses als auch für den eigentlichen Wechsel
(Kontexswitch) wird ebenfalls Rechenzeit benötigt.
29
Kapitel 3. Realzeitbetriebssysteme
T3
T2
T1
11
00
00
11
00
11
00
11
00
11
00
11
000
111
000
111
0011
11
0011
00
000
111
000
111
100
0
10
0
1000
1000
0
1 0
0
111
100
0
10
0
1
11
111
000
111
11100
11
11
00
111
000
100 200 300 400 500 600 700 800 900 1000 1100
CPU
t[ms]
Zeit, die der Scheduler (incl. Kontextswitch) benötigt.
Abbildung 3-9. Prinzip des Schedulings
Die Zeit, in denen eine Task den Rechnerkern (CPU) zur Verfügung hat (also rechnen darf), muss in einem
vernünftigen Verhältnis zu der Zeit stehen, die für die Auswahl des nächsten Rechenprozesses (Scheduling)
und das eigentliche Wechseln zu diesem Rechenprozess (Kontextswitch) verwendet wird. Die Zeiten für
das Scheduling bewegen sich im µ-Sekundenbereich.
Man unterscheidet statisches und dynamisches Scheduling.
Statisches Scheduling
Festlegung eines „Fahrplans“ (im vorhinein), nach dem die einzelnen Rechenprozesse in einem festen
Schema abzuarbeiten sind. Einsatz: sicherheitskritische Anwendungen (z.B. Flugzeug-Steuerungen),
da die Einhaltung von Realzeitbedingungen formal nachgewiesen werden kann; außerdem in SpeicherProgrammierbaren-Steuerungen.
Dynamisches Scheduling
Zuteilung des Prozessors an Rechenprozesse durch den im Betriebssystem enthaltenen Scheduler aufgrund der jeweils aktuellen Bedarfssituation. Rechenprozesse müssen damit preemptiv (unterbrechbar)
sein.
Als Scheduler bezeichnet man die Einheit, die für die Zuteilung von Rechenzeit (CPU-Zeit) an
Prozesse/Threads zuständig ist. Bei Multi-Prozessor-Systemen verteilt der Scheduler die Prozesse/Threads
zusätzlich auch auf Prozessoren.
3.2.1.1.1. Scheduling Points
In folgenden Situationen muss der Scheduler überprüfen, ob ein anderer Prozess die CPU erhalten sollte
und gegebenenfalls einen Kontextwechsel veranlassen:
•
Ende einer Systemfunktion (Übergang Kernel/User Mode):
•
•
•
•
30
Die Systemfunktion hat den aktiven Prozess blockiert (z.B. Warten auf Ende von I/O)
In der Systemfunktion sind die Scheduling-Prioritäten geändert worden.
Der aktive Rechenprozess terminiert.
Interrupts
• Timer-Interrupt: der aktive Prozess hat sein Quantum verbraucht (Round Robin).
• I/O signalisiert das Ende einer Wartebedingung (höher priorer Prozess wird „Bereit“).
Kapitel 3. Realzeitbetriebssysteme
3.2.1.1.2. Bewertungskritierien
Gerechtigkeit
Jeder Prozess soll einen fairen Anteil der CPU-Zeit erhalten.
Effizienz
Die CPU soll möglichst gut ausgelastet werden.
Durchlaufzeit
Ein Prozess soll so schnell wir möglich abgeschlossen sein.
Durchsatz
Es sollen so viele Jobs wie möglich pro Zeiteinheit ausgeführt werden.
Antwortzeit
Die Reaktion auf Ereignisse soll möglichst schnell erfolgen.
Determinismus
Das Scheduling als solches soll berechenbar sein.
Anmerkung: Während für Standard-Betriebssysteme Gerechtigkeit, Effizienz, Durchlaufzeit und
Durchsatz eine wesentliche Rolle spielen, sind für Realzeitbetriebssysteme insbesondere die Kriterien
Antwortzeit und Determinismus entscheidend.
3.2.1.1.3. First Come First Serve (FCFS)
D
C
D
B
C
D
Prozeß A ist aktiv
A
B
C
Prozeß A blockiert,
Prozeß B ist aktiv
A
A
B
Prozeß A ist wieder bereit,
wird eingereiht
Zeit
Abbildung 3-10. Prinzip des FCFS Schedulings
Prinzip
•
•
•
Die bereiten Prozesse sind in einer Warteschlange nach ihrem Erzeugungszeitpunkt geordnet.
Jeder Prozess darf bis zu seinem Ende laufen, außer er geht in den Zustand „Blockiert“ über.
Geht ein Prozess vom Zustand „Blockiert“ in den Zustand „Bereit“ über, wird er entsprechend seinem
Erzeugungszeitpunkt wieder in die Warteschlange eingereiht, unterbricht aber den laufenden Prozess
nicht.
31
Kapitel 3. Realzeitbetriebssysteme
Deadline T1
Deadline T2
Deadline T3
B
C
A
100
200
300
400
500
600
700
800
900
1000
1100
bereit
aktiv
Abbildung 3-11. First Come First Serve Scheduling
Anwendungen
•
•
Batch-Systeme
um gleiche mittlere Wartezeiten für alle Prozesse zu erreichen.
Realzeiteigenschaften: nicht für Realzeitsysteme geeignet, da ein Prozess alle anderen blockieren
kann!
3.2.1.1.4. Round Robin Scheduling
Prinzip
•
•
•
•
Alle Prozesse werden in eine Warteschlange eingereiht.
Jedem Prozess wird eine Zeitscheibe (time slice, quantum) zugeteilt
Ist ein Prozess nach Ablauf seines Quantums noch im Zustand „Aktiv“,
• wird der Prozess verdrängt (preempted), d.h. in den Zustand „lauffähig“ versetzt;
• wird der Prozess am Ende der Warteschlange eingereiht;
• wird dem ersten Prozess in der Warteschlange die CPU zugeteilt.
Geht ein Prozess vom Zustand „schlafend“ in den Zustand „lauffähig“ über, so wird er am Ende der
Warteschlange eingereiht.
Kriterien für die Wahl des Quantums
•
•
•
32
Das Verhältnis zwischen Quantum und Kontextwechselzeit muss vernünftig sein.
Großes Quantum: effizient, aber lange Verzögerungszeiten und Wartezeiten möglich.
Kleines Quantum: kurze Antwortzeiten, aber großer Overhead durch häufige Prozessumschaltung.
Kapitel 3. Realzeitbetriebssysteme
Deadline A
Deadline B
Deadline C
Round Robin
B
C
A
100 200 300 400 500 600 700 800 900 1000 1100
t[ms]
Rechenprozess aktiv
Rechenprozess bereit
Abbildung 3-12. Round Robin Schedulings
Realzeiteigenschaften:
Dynamisches Scheduling
Da die Anzahl der bereiten Rechenprozesse nicht bekannt ist, dann der Abarbeitungszeitpunkt
eines Prozesses nicht vorhergesagt werden (nicht deterministisch). Asynchrone Anforderungen
werden nicht direkt bedient. Daher ist das Verfahren für Realzeitsysteme nicht geeignet.
Statisches Scheduling
In einer Abwandlung des Verfahrens (TDMA=Time Division Multiple Access) sind die Zeitscheiben
starr und fest. Sind die Prozesse im System im vornherein bekannt, kann sich das Verfahren,
abhängig von der Aufgabenstellung (z.B. SPS) für Realzeitsysteme eignen.
3.2.1.1.5. Prioritätengesteuertes Scheduling
Deadline T1 Deadline T2
Deadline T3
B
C
A
100 200 300 400 500 600 700 800 900 1000 1100
Task aktiv
Task bereit
t[ms]
Prioritäten:
A = 5 (höchste)
C = 2 (niedrigste)
B = 4 (mittlere)
Abbildung 3-13. Prioritätengesteuertes Schedulings
Prinzip
•
Für jeden Prozess wird eine Priorität vergeben.
33
Kapitel 3. Realzeitbetriebssysteme
•
Der Prozess mit der höchsten Priorität bekommt die CPU.
Realzeiteigenschaften: Für Realzeitsysteme geeignet, insbesondere wenn keinerlei Informationen
bezüglich der maximal zulässigen Reaktionszeiten zur Verfügung stehen.
Allerdings gibt es die Schwierigkeit, die Prioritäten richtig zu verteilen (siehe dazu auch Schritthaltende
Verarbeitung).
3.2.1.1.6. Deadline-Scheduling
Prinzip
•
Der Rechenprozess mit der dem Momentanzeitpunkt am nächsten gelegenen Deadline (maximal zulässige Reaktionszeit) bekommt die CPU zugeteilt.
tDmax =Deadline
Ist−Zeit
t
tE
ts
tDmax
tE
ts
zu diesem Zeitpunkt muß die Task bearbeitet sein
Verarbeitungszeit (ohne Wartezeit)
Zeit−Spielraum bis zum spätestmöglichen Start der Verarbeitung (laxity)
Abbildung 3-14. Prinzip des Deadline Scheduling
0110 1010 1010 1010
000
111
000
111
111
000
11110 10
10 10000
D1 D2 D3 D4
T1
T2
T3
T4
t
Abbildung 3-15. Beispiel für Deadline Scheduling
34
Kapitel 3. Realzeitbetriebssysteme
Deadline A Deadline B
Deadline C
B
C
A
100 200 300 400 500 600 700 800 900 1000 1100
t[ms]
Task aktiv
Task bereit
Abbildung 3-16. Realzeiteigenschaften beim Deadline Scheduling
Realzeiteigenschaften: Das Verfahren führt zur Einhaltung der maximalen Reaktionszeiten, wenn dies
überhaupt möglich ist (optimales Verfahren)!
Nachteil: Deadlines (sprich die maximal zulässigen Reaktionszeiten) sind nicht immer bekannt.
3.2.1.1.7. POSIX 1003.1b
Prinzip
•
Prioritätengesteuertes Scheduling
Auf jeder Prioritätsebene können sich mehrere Prozesse befinden.
Innerhalb einer Prioritätsebene werden Prozesse gescheduled nach:
• First In First Out (First Come First Serve)
• Round Robin
•
Prioritätsebene 0 besitzt die niedrigste Priorität.
•
•
Scheduling Strategie (Policy)
Queue Prioritätsebene
SCHED_RR
103 41
SCHED_FIFO
SCHED_RR
24 111 42 53
11
2
1
SCHED_RR
22 13 12 10
0
Max Prio
42 Prozeß mit der PID 42
Abbildung 3-17. Posix Scheduling
Zur Parametrierung des Schedulers stehen die folgenden Funktionen zur Verfügung:
35
Kapitel 3. Realzeitbetriebssysteme
sched_setparam
Setzt die Task-Priorität
sched_getparam
Liest die Schedulingparameter einer Task
sched_setscheduler
Setzt die Schedulingparameter für eine Task
sched_yield
Gibt die CPU frei (erzwingt Scheduling)
sched_getscheduler
Liest die aktuelle Schedulingstrategie
sched_get_priority_max
Liest die maximal mögliche Priorität
sched_get_priority_min
Liest die minimale Priorität
sched_rr_get_interval
Ergibt die Dauer des Zeitintervalls bei Round Robin Scheduling
Informationen zur Programmierung des Schedulings und zum Setzen der Priorität finden Sie im Abschnitt
Taskmanagement.
Unter Linux gibt es das Programm chrt, mit dem dir Priorität eines anderen Programms während der
Laufzeit modifiziert werden kann.
3.2.1.1.8. Scheduling in VxWorks
VxWorks bietet prinzipiell zwei Verfahren zur Auswahl:
•
•
Wind-Task Scheduling
POSIX-Scheduling
Meist wird das Wind Scheduling eingesetzt. Dieses ist durch folgende Kenndaten geprägt:
•
Preemptives Prioritäten Scheduling
•
•
36
256 Prioritätsebenen (0=höchste, 255=niedrigste Priorität)
Round Robin Scheduling
• Innerhalb einer Prioritätsebene wird Round Robin Scheduling verwendet.
Kapitel 3. Realzeitbetriebssysteme
Priorität
hoch
niedrig
time slice
T4
T1
T2
T3
T1
T2
T2
t1
T3
t
Abbildung 3-18. Beispiel für Wind Scheduling
Die Unterschiede zum Posix-Scheduling sind die folgenden:
•
•
•
Der POSIX-Scheduler in VxWorks scheduled Prozesse, der Wind-Scheduler Threads.
Die Prioritätenbezeichnungen (hoch, niedrig) sind einander invers. In POSIX bedeutet eine hohe Zahl
(z.B. 255) eine hohe Priorität, in Wind-Scheduling eine niedrige.
Unter Wind-Scheduling ist das Schedulingverfahren innerhalb einer Priorität immer gleich.
Das Betriebssystem bietet zur Parametrierung des Schedulers die folgenden Funktionen an:
kernelTimeSlice
Einstellung der RR-Parameter
taskPrioritySet
Modifikation der Taskpriorität
taskLock
Scheduling ausschalten
taskUnlock
Scheduling einschalten
3.2.1.1.9. Der O(1) Scheduler im Linux-Kernel 2.6.
Weiche Echtzeitsysteme, wie sie beispielsweise Multimediasysteme darstellen, erfordern als zusätzliche
Eigenschaft von einem Scheduler Interaktivität. Interaktivität bedeutet, dass man die Zeitscheibe, die man
einzelnen Tasks zur Verfügung stellt, gemäß der Anforderungen an Interaktivität variiert. Sehr interaktive
Tasks bekommen eine kurze Zeitscheibe und kommen damit häufiger dran. Rechenintensiven Tasks wird
dagegen eine lange Zeitscheibe zugeteilt, falls diese gescheduled werden, dürfen diese auch länger rechnen.
Bei den immer größer werdenden Caches kommt dieses insbesondere der Verarbeitungsgeschwindigkeit zu
gute, schließlich müssen die Caches nicht so häufig gefüllt werden.
Da man im Fall des dynamischen Schedulings im vorhinein nicht die Verarbeitungszeiten und die Anforderungzeitpunkte der einzelnen Rechenprozesse kennt, muss man auf statistische Informationen aus der
Vergangenheit zurückgreifen.
Der O(1) Scheduler von Ingo Molnar wird folgendermassen beschrieben:
hybrid priority-list and round-robin design with
an array-switch method of distributing timeslices
and per-CPU runqueues.
37
Kapitel 3. Realzeitbetriebssysteme
Der Scheduler basiert prinzipiell auf einem Round Robin Verfahren. Alle rechenbereiten Prozesse sind in
einer Warteschlange (hier als array implementiert) gemäß ihrer Priorität eingehängt. Im System existieren
pro CPU jeweils zwei Warteschlangen: eine für die aktiven und eine für die Tasks, deren Zeitscheibe abgelaufen ist (expired).
Prozesslisten-Bitmap
Normale Priorität
139
100
1
0
139
0
Es existiert kein rechenbereiter Prozess.
Mindestens 1 rechenbereiter Prozess existiert.
Berücksichtigung von Interaktivität
Realzeit Priorität
0
active
expired
PCB
PCB
PCB
Listen rechenbereiter Prozesse
Abbildung 3-19. O(1) Scheduler im Linux-Kernel
Sobald die Zeitscheibe des aktiven Rechenprozesses abgelaufen ist, wird dieser in das zweite Array (expired) gemäß seiner Priorität eingetragen. Ist das erste Array abgearbeitet (sprich leer), werden die beiden
Arrays umgeschaltet. Das ehemals expired-Array wird zum active-Array und das ehemals active-Array zum
expired-Array.
Die Priorität innerhalb des Arrays ergibt sich aus dem Nice-Wert, den eine Task besitzt und einem
Interaktivitäts-Bonus bzw. -Malus. Tasks die viel Rechenzeit verbrauchen bekommen eine größere
Zeitscheibe, allerdings bei geringerer Priorität. Die verbrauchte Rechenzeit wird über den Load-Wert
innerhalb der letzten 4 Sekunden bestimmt.
Auch wenn das vorgestellte Verfahren sehr effizient ist, happert es doch mit der Bewertung von rechenintensiven, interaktiven Tasks, wie Tasks zum Video- oder Audiostreaming bzw. Tasks zur Oberflächendarstellung. Diese Tasks (X-Windows beispielsweise) mussten zunächst durch den Administrator als interaktiv
gekennzeichnet werden.
Eine erweiterte Heuristik hat dieses überflüssig gemacht. So bekommen ebenfalls die Tasks einen Interaktivitätsbonus, die sehr interaktive Tasks aufwecken. Damit ist nach bisherigen Erfahrungen eine ausgesprochen
gute Erkennungsrate für interaktive Rechenprozesse zu erreichen.
Die typische Zeitscheibe des Schedulers beträgt 200ms. Alle 250ms ist ein Loadbalancer aktiv, der dafür
sorgt, dass die Last im Fall eines Mehrprozessorsystems gleichmässig auf die Prozessoren verteilt wird.
3.2.1.2. Scheduling auf Mehrprozessormaschinen
Insgesamt sind drei unterschiedliche Mehrprozessorarchitekturen zu unterscheiden:
1. Simultaneous Multithreading (SMT)
38
Kapitel 3. Realzeitbetriebssysteme
Diese von Intel eingeführte Architektur hat meist zwei Registersätze und Pipelines, so dass sehr leicht
zwischen zwei Threads umgeschaltet werden kann. Allerdings gibt es nur eine Verarbeitungseinheit.
Der Performancegewinn wird allgemein mit bis zu 10 Prozent angegeben. Im strengen Sinn ist das
natürlich kein wirkliches Multiprozessor-System.
2. Symmetric Multi-Processing (SMP)
Mehrere Prozessorkerne besitzen einen Adressraum.
3. Non-uniform Memory Architecture (NUMA)
Einzelne Prozessoren können auf unterschiedliche Speicherbereiche unterschiedlich schnell zugreifen.
busy
Auswahl über O(1)
Thread
CPU 0
Migration gemäß
Multi−CPU−Scheduling
Auswahl über O(1)
CPU 1
idle
Abbildung 3-20. Der übergeordnete Mehrprozessorscheduler verteilt die Jobs auf die einzelnen
Prozessoren.
Ziel des Mehrprozessorschedulings ist es, die Last auf allen Prozessoren im System gleichmässig zu
verteilen. Grundsätzlich ist das Scheduling zweistufig aufgebaut: Auf jedem Prozessor arbeitet der
Einprozessor-Scheduler. Der Mehrprozessorscheduler verteilt die Rechenprozesse auf die einzelnen
Prozessoren, so dass der Einprozessorscheduler sich aus der Liste der ihm zugeteilten Jobs den als nächstes
von ihm zu bearbeitenden heraussucht.
Der Mehrprozessorscheduler ist häufig als eigener Prozess realisiert.
Das Verschieben eines Rechenprozesses von einer auf die andere CPU wird als Prozessmigration bezeichnet.
An folgenden Stellen respektive zu den folgenden Zeitpunkten wird der Mehrprozessorscheduler aktiv:
1. Bei Aufruf der Systemcalls fork(), clone(), exec() und exit().
2. Wenn eine CPU idle wird, wenn also die Liste der zu bearbeitenden Rechenprozesse leer geworden ist.
3. Zeitgesteuert (periodisch).
Auch bei einer Ungleichverteilung der Last ist eine Prozessmigration nicht unbedingt wünschenswert, da
diese zunächst mit Verlusten bezahlt werden muss. Bei einem SMP-System wird beispielsweise bei einer
Prozessmigration der Inhalt sämtlicher Caches verloren.
Am preiswertesten ist die Prozessmigration auf einem SMT-System; hier ist allerdings der Gewinn auch am
geringsten. Am teuersten ist die Verschiebung auf einem NUMA-System; hier sind aber auch die Gewinne
möglicherweise am größten.
39
Kapitel 3. Realzeitbetriebssysteme
CPU
0, 1
flags = SD_LOAD_BALANCE|...
parent
struct sched_group
CPU
1
CPU
0
CPU
2
CPU
3
Base Level Domain
SMT
parent
SMP
};
Level 1 Domain
CPU
2, 3
struct sched_domain {
...
Abbildung 3-21. Die Modellierung eines SMP-Systems im Linux-Kernel.
Da in der Praxis meist gemischte Hardware-Architekturen vorkommen, führte der Linux-Kernel so genannte Scheduling-Domains und Scheduling-Gruppen ein. Eine Scheduling-Domain ist der Container für (untergeordnete) Scheduling-Gruppen. Eine Scheduling-Gruppe wiederum steht für eine CPU oder für eine
andere (untergeordnete) Scheduling-Domain. Ein Zweiprozessorsystem beispielsweise modelliert Linux in
einer Scheduling-Domain mit zwei Gruppen; für jede CPU eine. Ein Rechner mit Hyperthreading-Prozessor
wird übrigens ebenfalls auf eine Domain und zwei Gruppen abgebildet. Ein heterogenes System dagegen
besteht aus mehreren Domains, wobei die Scheduling Gruppen der übergeordneten Container (Domains)
nicht Prozessoren, sondern eben weitere Domains repräsentieren. Ein einfaches Beispiel für eine derartig baumartige Topologie finden Sie in Abbildung Die Modellierung eines SMP-Systems im Linux-Kernel..
Hier ist eine Architektur mit zwei Hyperthreading-Prozessoren mit Hilfe dreier Scheduling-Domains abgebildet: Zwei Basis-Domains und die übergeordnete Level 1 Domain. Die Level 1 Domain umspannt alle vier
Prozessoren, die Basis-Domains jeweils einen Hyperthreading Prozessor mit seinen zwei logischen Kernen.
Der Mehrprozessor-Scheduler sorgt jetzt immer für die Lastverteilung innerhalb einer Scheduling-Domain.
Die Entscheidung über eine Migration wird dabei abhängig von der Last innerhalb der Scheduling-Gruppen,
den Kosten für die Migration und des erwarteten Gewinns gefällt.
In Unix-Systemen kann über die Funktionen sched_get_affinity() und sched_set_affinity() die
Affinität, also die Zugehörigkeit eines Threads zu einer CPU ausgelesen und festgelegt werden. Damit
ist also die Zuordnung eines Threads auf eine spezifische CPU möglich. Durch diesen Mechanismus
können Echtzeitprozesse von Prozessen ohne strenge zeitliche Anforderungen separiert und damit
deterministischer abgearbeitet werden.
Das folgende Programm zeigt, wie unter Linux die Affinität ausgelesen und schließlich auch gesetzt wird.
Durch den Code wird eine Prozessmigration erzwungen.
#include <stdio.h>
#include <stdlib.h>
#define __USE_GNU
#include <sched.h>
#include <errno.h>
int main( int argc, char **argv )
{
cpu_set_t mask;
unsigned int cpuid, pid;
if( argc != 3 ) {
fprintf(stderr,"usage: %s pid cpuid\n", argv[0]);
return -1;
}
40
Kapitel 3. Realzeitbetriebssysteme
pid = strtoul( argv[1], NULL, 0 );
cpuid = strtoul( argv[2], NULL, 0 );
CPU_CLR( cpuid, &mask ); // Aktuelle CPU verbieten.
if( sched_setaffinity( pid, sizeof(mask), &mask ) ) {
perror("sched_setaffinity"); // UP-System???
return -2;
}
return 0;
}
// vim: aw ic ts=4 sw=4:
3.2.2. Memory-Management
Aufgaben der Memory-Management-Unit ist
•
•
•
•
der Speicherschutz,
die Adressumsetzung,
virtuellen Speicher zur Verfügung stellen und
Zugriff auf erweiterte Speicherbereiche (Highmem).
Speicherschutz
•
•
Applikationen (Prozesse, aber nicht Threads) werden voreinander geschützt, indem jeder Prozess
seinen eigenen Adressraum bekommt. Zugriff ist damit nur möglich auf eigene Daten-, Stack- und
Codesegmente.
Daten bzw. Teile der Applikation werden vor Fehlzugriffen geschützt. Greift eine Applikation auf
Speicherbereiche zu, die nicht zur Applikation gehören, oder versucht die Applikation aus einem
Codesegment Daten zu lesen, führt dies - dank MMU - zu einer Ausnahmebehandlung.
Adressumsetzung
•
•
Programme sollen einen einheitlichen Adressraum bekommen, um das Laden von Programmen zu
beschleunigen und Shared-Libraries zu ermöglichen. War in früheren Zeiten der Loader des Betriebssystems, der Applikationen in den Speicher geladen und danach gestartet hat, dafür verantwortlich,
dem Programm die richtigen (freien) Adressen zuzuweisen, kann mit MMU der Linker bereits die
Adressen vergeben. Aus Sicht jeder Applikation beginnt der eigene Adressraum ab der Adresse 0.
Mehrere Tasks können sich ein (Code-) Segment teilen. Durch die Trennung von Code- und
Datensegmenten und mit Hilfe der MMU können mehrere Tasks, die auf dem gleichen Programm
beruhen, ein oder mehrere Codesegmente teilen. Dadurch wird der Hauptspeicherbedarf reduziert.
Virtuellen Speicher zur Verfügung stellen
Durch die MMU kann virtueller Speicher zur Verfügung gestellt werden. Damit können
Applikationen auf mehr Speicher zugreifen, als physikalisch vorhanden ist. Als Speicherersatz wird
Hintergrundspeicher (Festplatte) genommen. Der verwendete Hintergrundspeicher wird Swap-Space
genannt. Der vorhandene Hauptspeicher wird durch die Speicherverwaltung in Seiten (oder so
41
Kapitel 3. Realzeitbetriebssysteme
genannte Kacheln) eingeteilt, und – wenn keine freien Kacheln mehr zur Verfügung stehen – werden
die Inhalte belegter Kacheln auf den Hintergrundspeicher ausgelagert. Dieser Vorgang wird Paging
oder Swapping genannt. Die freigeräumte Kachel kann nach dem Freiräumen genutzt werden. In
Realzeitsystemen wird Swapping nur bedingt eingesetzt, da es zu Undeterminismen führt.
Zugriff auf erweiterte Speicherbereiche
Für einige Applikationen reicht der Adressraum von 16- und 32-Bit Prozessoren mit 64kByte und
4GByte nicht aus. Die Prozessorhersteller haben Techniken eingebaut, um physikalisch mehr Speicher anzuschließen (bei x86-Prozessoren heisst die Technik Process-Adress-Extension, PAE). Die
Speicherverwaltung muss Techniken zur Verfügung stellen, diesen erweiterten Speicherbereich – oft
auch als Highmem bezeichnet – ansprechen zu können.
MMU’s bestehen aus Hardware, die durch entsprechende Software initialisiert werden muss. Heutige
Mikroprozessoren haben im Regelfall eine MMU integriert. Prinzipiell funktioniert der Vorgang so, dass
der Prozessorkern eine logische Adresse erzeugt. Diese logische Adresse wird durch die MMU in eine
physikalische Adresse umgesetzt. Die Umsetzungsregeln selbst werden dazu in die MMU geladen. Eine
MMU muss also durch das Betriebssystem initialisiert werden.
Prinzipiell kann eine MMU den physikalischen Speicher auf zwei Arten aufteilen: in Segmente und in
Seiten. Während Speicher-Seiten feste (physikalische) Größen haben und einander auch nicht überlappen,
ist die Größe der Segmente ebenso dynamisch, wie ihre wirkliche Lage im Hauptspeicher.
Physik.
CPU
Speicher
CB
CB
MMU
(Prozessorkern)
AB
Logische
Adresse
AB
Physikalische
Adresse
MMU = Memory Management Unit
AB=Adreßbus
CB=Controlbus
Abbildung 3-22. Adressumsetzung mittels MMU
42
Kapitel 3. Realzeitbetriebssysteme
3.2.2.1. Segmentierung
16bit
Logische
Adresse
20bit
Physikalische
Adresse
+
Basis
Adresse
15 . . .
20bit
0000
0000
0000
0000
...0
Code Segment
Stack Segment
Data Segment
Extra Data Segment
4 Basis−Adreß−Register
Abbildung 3-23. Segmentierung beim 8086
Insbesondere mit den Intel 8086 Prozessoren sind Speicherverwaltungseinheiten, die auf Segmenten
basieren, eingeführt worden. Die Segmentierung diente hierbei vor allem der Erweiterung des logischen
(man merke, nicht des physikalischen!) Adressraums (der physikalische Speicher war/ist größer als der
logische Adressraum). Man findet diese Methode auch heute noch häufig bei einfachen (8 oder 16 bit)
Mikroprozessoren vor und - in Kombination mit Paging - auch bei den modernen Prozessoren. Damit
spielt die Segmentierung immer noch - insbesondere bei der Realisierung eingebetteter Systeme - eine
große Rolle.
Ein Segmentregister kennzeichnet die Anfangsadresse (Basisadresse) eines Segmentes. Diese
Anfangsadresse wird zu jeder vom Prozessor erzeugten logischen Adresse hinzuaddiert. Um eine
Erweiterung des Adressraumes zu erhalten, ist es natürlich notwendig, dass die Basisadresse breiter ist
als die vom Prozessor erzeugte logische Adresse. Beim 8086 beispielsweise werden 16bit logische
Adressen erzeugt. Die Segmentregister sind jedoch 20bit breit, wobei die letzten 4bit grundsätzlich auf 0
gesetzt sind. Damit können Segmente immer nur auf Adressen beginnen, die durch 16 teilbar sind. Da
von den 20bit der Basisadresse nur 16bit variabel sind, können diese 16bit durch einen entsprechenden
Maschinenbefehl belegt werden.
Im System werden meistens mehrere Segmentregister vorgehalten. Beim 8086 sind dies beispielsweise:
•
•
•
ein Segmentregister für Code,
zwei für Daten und
eines für den Stack.
Zu Beginn einer Applikation werden die Segmentregister belegt, danach kann die Applikation mit einem
Maschinenbefehl auf Speicherzellen zugreifen, solange sie innerhalb des logischen Adressraums (beim
8086 64kByte) bleibt. Benötigt die Applikation jedoch mehr als 64KByte Hauptspeicher, muss vor dem
Zugriff das Segmentregister wieder umgeladen werden; ein zeitaufwendiger Vorgang. Übrigens ist das Umladen der Segmentregister die Ursache für die Einführung der Memory-Modelle bei Prozessoren, die ihren
physikalischen Speicherbereich durch Segmentierung erweitern. Die Memory-Modelle (Small, Medium,
Large und Huge) bestimmen, welche Segmente größer als 64 KByte sein dürfen und welche nicht. Die
Modelle Large und Huge allerdings unterscheiden sich nur darin, ob im C-Programm verwendete Pointer
normalisiert und damit vergleichbar gemacht werden oder nicht.
43
Kapitel 3. Realzeitbetriebssysteme
Segment
Offset
0200 : 0004
0200
0004
Phys.Adresse 0x02004
Segment Offset
0000 : 2004
Segment Offset
0100 : 1004
0100
1004
0x02004
0000
2004
0x02004
Normalisieren:
SegmentRegister = SegmentRegister + (Offset >> 4);
Offset = Offset & 0x000F;
Abbildung 3-24. Beispiel zum Normalisieren
Problem: eine physikalische Adresse kann über mehrere logische Adressen ausgedrückt werden. Das erschwert einen Pointer-Vergleich. Pointer müssen erst normalisiert werden. Dieser Vorgang ist zeitaufwendig
und fehleranfällig.
Die Segmentierung bietet keinen besonderen Speicherschutz, abgesehen davon, dass zunächst einmal eine richtige Belegung der Segmentregister vorausgesetzt - ein Zugriff auf das Codesegment mit normalen
Datenlesebefehlen nicht möglich ist.
3.2.2.2. Seitenorganisation
Bei der Seitenorganisation wird der Hauptspeicher in gleich große (beispielsweise 4KByte) Seiten bzw.
Kacheln eingeteilt. Die vom Prozessor erzeugte logische Adresse selbst besteht aus zwei semantischen
Einheiten, einem Teil, der innerhalb der MMU den sogenannten Seitendeskriptor (Kachelbeschreibung)
auswählt, und einem Teil, der eine Speicherzelle innerhalb der Kachel auswählt.
Seitendeskriptoradresse
31
28 27
23
Seitenoffset
15
12 11
Abbildung 3-25. Strukturierung der logischen Adresse
44
9 8
7 6
5 4
3
2 1 0
Kapitel 3. Realzeitbetriebssysteme
Zugriffsrechte Kachel−Nummer
15
12 11
0
Schreib−Zugriff erlaubt
Daten−Zugriff erlaubt
Code−Zugriff erlaubt
Valid (Kachel im Speicher vorhanden)
Abbildung 3-26. Aufbau eines Seitendeskriptors
Der Seitendeskriptor besteht ebenfalls aus zwei semantischen Teilen. Der erste Teil enthält Flags, die die
Zugriffsrechte kontrollieren, der zweite Teil enthält die eigentliche physikalische Seitenadresse.
Im Regelfall existieren vier Flags:
Schreibflag
Ist dieses Flag gesetzt, darf auf die Seite schreibend zugegriffen werden. Versucht der Prozessor auf
die Seite zu schreiben, obwohl das Flag zurückgesetzt ist, wird ein Fehler (Bus-Error) ausgelöst.
Daten-Zugriff
Dieses Flag gibt an, ob die Seite Daten enthält oder nicht. Ist das Flag gültig gesetzt, darf der Prozessor
aus dem Segment Daten lesen. Ist das Flag ungültig gesetzt, löst ein Lesezugriff einen Fehler (BusError) aus.
Code-Zugriff
Dieses Flag gibt an, ob die Seite Code enthält oder nicht. Ist das Flag gültig gesetzt, darf der Prozessor
die Daten in seinen Programmspeicher laden. Ist das Flag ungültig gesetzt und der Prozessor versucht
aus der Kachel Code zu lesen, wird ein Fehler (Bus-Error) ausgelöst.
Kachel im Speicher
Dieses Flag gibt an, ob sich die Seite im Speicher befindet oder (auf den Hintergrundspeicher) ausgelagert wurde. Befindet sich die Seite nicht im Hauptspeicher, muss ein Fehler-Signal (Bus-Error)
ausgelöst werden, so dass das Betriebssystem für das Nachladen der Seite sorgen kann.
Die MMU ist entweder im Prozessor integriert und über eigene Befehle ansprechbar, oder sie befindet sich
als eigener Baustein im Adressraum des Prozessors. In diesem Fall ist sichergestellt, dass beim Startup des
Systems die MMU einen definierten Zustand hat, so dass auch ohne Vorbelegung das Betriebssystem die
Möglichkeit hat, die MMU zu initialisieren.
Wie aus der Zeichnung ersichtlich, wird der Zugriff auf den Hauptspeicher durch die MMU verzögert, da die
MMU selbst auch eine Zugriffszeit besitzt. Moderne Mikroprozessoren verwenden als MMU-Speicher den
Hauptspeicher, so dass im ungünstigen Fall mehrere Hauptspeicherzugriffe notwendig sind, um schließlich
das gewünschte Datum zu bekommen.
Eine MMU, die auf Basis der Aufteilung des Speichers in Seiten funktioniert, kann leicht mit Hilfe von
Speicherbausteinen aufgebaut werden. Der Speicher beinhaltet die Seitendeskriptoren. Mit den oberen Bits
der logischen Adresse wird der Speicher adressiert. Die Daten, die daraufhin am Datenausgang (Datenbus)
des Speicherbausteins sichtbar werden, werden teils als Adresse der Kachel, teils als Kontrollsignale zur
45
Kapitel 3. Realzeitbetriebssysteme
Verifikation der Zugriffsrechte, verwendet. Konkret wird also ein Teil des Datenbusses auf den Adressbus
gemappt. Der Datenbus selbst muss gemultiplext sein, d.h. damit der MMU-Speicher auch belegt werden
kann, muss der Datenbus auch mit dem Datenbus des Prozessorkerns verbunden werden.
Ein Teil der Datenbits eines Seitendeskriptors wird mit den Zugriffs-Kontrollsignalen des Prozessorkerns (R/W, C/D) verknüpft. Diese Verknüpfung erzeugt dann entweder die Zugriffskontrollsignale für den
physikalischen Teil des Adressbusses oder aber das Bus-Error-Signal.
Adressumsetzspeicher
Zugriffsrechte Kacheladresse
31
Auswahl/
Adressierung
des
Seiten−Deskriptors
111
000
000
111
000
111
000
111
000
111
000
111
000
111
000
111
1111
0000
0000
1111
11
00
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
00
11
23
Auswahl/
Adressierung
der
Kachel (Seite)
12
11
12
11
Adresse/Offset innerhalb der Kachel (Seite)
0
logische Adresse (32bit)
0
physikalische Adresse (24bit)
Abbildung 3-27. MMU auf Basis von Seitenadressierung
3.2.3. I/O Subsystem
Das I/O Subsystem hat vielfältige Aufgaben. Aus Applikationssicht schafft es eine Schnittstelle für den
einheitlichen Zugriff auf unterschiedlichste Hardware, aus Hardwaresicht stellt es eine Umgebung zur Verfügung, um mit relativ wenig Aufwand Hardware systemkonform in den Kernel zu integrieren. Daneben ist
das I/O Subsystem aber auch noch für die Organisationsstrukturen auf Hintergrundspeicher, den sogenannten Filesystemen, zuständig.
3.2.3.1. Gerätezugriff auf Applikationsebene
Das Betriebssystem stellt auf Applikationsebene ein Systemcallintferface zur Verfügung, um auf Geräte
zuzugreifen. Dazu gehören im wesentlichen Funktionen um Geräte zu initialisieren, um Daten zu den
Geräten zu transferieren und Daten von den Geräten zu lesen. Die Applikationsschnittstelle zum I/OSubsystem ist so gestaltet, dass sie für beinahe jegliche Art von Geräten nutzbar ist.
3.2.3.1.1. Schnittstellenfunktionen
Ähnlich wie in der Hardware werden für die Applikation Zugriffe auf Peripherie auf ein einfaches Lesen und
Schreiben (read und write) abgebildet. Bevor jedoch auf ein Gerät geschrieben bzw. von einem Gerät gelesen werden kann, muss dazu die Ressource beim Betriebssystem angefordert werden. Bei der Anforderung
46
Kapitel 3. Realzeitbetriebssysteme
entscheidet das System, ob die Task den Zugriff bekommt oder ob der Zugriff abgelehnt wird. Gründe für
eine Ablehnung können sein:
•
fehlende Zugriffsrechte
•
die Ressource ist bereits belegt.
#define ZUMACHEN 0x00
#define AUFMACHEN 0x01
...
char Tuere;
...
fd=open( “Aufzugtuer”, O_RDWR );
Tuere = AUFMACHEN;
write( fd, &Tuere, sizeof(Tuere) ); // Tuere auf
...
close( fd );
Abbildung 3-28. Geräteschnittstelle
Die klassischen Zugriffsfunktionen (ANSI-C) für den Zugriff auf Geräte sind die gleichen wie für den
Zugriff auf Dateien (Geräte werden also an dieser Schnittstelle wie Dateien gehandhabt!):
open
Mit open wird der spätere Zugriffswunsch auf ein Gerät beim Betriebssystem angemeldet. Das Gerät
selbst wird in symbolischer Form spezifiziert. Außerdem muss angegeben werden, in welcher Form
(lesend/schreibend) auf das Gerät zugegriffen werden soll, und in welcher Art (z.B. blockierend oder
nicht blockierend). open liefert als Ergebnis einen sogenannten Descriptor zurück, wenn das Betriebssystem die Ressource der Task zuteilt. Dieser Descriptor wird von den folgenden Diensten als Kennung verwendet, so dass die produktiven Dienste (read/write) effizient bearbeitet werden können (Bild
Geräteschnittstelle).
close
Dieser Systemcall gibt die angeforderte Ressource wieder frei.
read
Zum lesenden Zugriff wird der Systemcall read verwendet.
write
Mit dieser Funktion können Daten an die Peripherie übermittelt werden.
ioctl
Läßt sich eine bestimmte Funktionalität, die ein Peripheriegerät besitzt, nicht auf Schreib-/Leseaufrufe
abbilden, steht der IO-Control-Aufruf zur Verfügung. Dieser Aufruf ist gerätespezifisch. Über IOControls werden beispielsweise die verschiedenen Betriebsparameter und Betriebsarten einer seriellen
Schnittstelle konfiguriert oder auch ein atomares Lesen und Schreiben ermöglicht.
47
Kapitel 3. Realzeitbetriebssysteme
3.2.3.1.2. Zugriffsarten
Da man bei Echtzeitbetriebssystemen darauf achten muss, dass das Warten auf Ereignisse das System selbst
nicht blockiert, lassen sich Aufrufe, die zum Warten führen können, unterschiedlich parametrieren. Insgesamt lassen sich drei Aufrufarten unterscheiden:
1. Aufruf mit implizitem Warten (blockierender Aufruf)
2. nicht blockierender Aufruf
3. Aufruf mit explizitem Warten
Normalerweise wird die Task, die auf ein Gerät zugreift, solange “schlafen” gelegt, bis das Gerät geantwortet hat, oder eine Fehlersituation (das Gerät antwortet innerhalb einer definierten Zeitspanne nicht)
eingetreten ist. Man spricht von einem Warteaufruf mit implizitem Warten oder auch von einem synchronen
Zugriff.
Demgegenüber spricht man von einem Warteaufruf mit explizitem Warten, wenn der Warteaufruf (Zugriff
auf das Gerät) direkt zurückkehrt. Die Task, die den Aufruf durchgeführt hat, kann - unabhängig von der
Bearbeitung des Warteaufrufs innerhalb des Betriebssystems - zurückkehren und weiterrechnen. Ist der Zugriff auf das Gerät durch das I/O-Subsystem abgeschlossen, wird die Task darüber entweder per Ereignis
(auf das an anderer Stelle entweder explizit gewartet wird, oder welches durch die Task gepollt wird) informiert, oder das Betriebssystem ruft eine vorher übergebene Callback-Funktion auf. Bei Warteaufrufen
mit explizitem Warten spricht man auch von einem asynchronen Zugriff.
Ein Beispiel für eine derartige Funktion mit explizitem Warten ist unter Win32 die Funktion ReadFile mit
sogenannter overlap-structure (Bild Expliziter Warteaufruf mit Win32 Interface).
Aus Sicht des Betriebssystems sind Warteaufrufe mit explizitem Warten nicht trivial zu realisieren. Das liegt
an der Schwierigkeit, dass von der Applikation mehrere explizite Warteaufrufe gestartet werden können.
Das Betriebssystem muss sich darüber die Informationen innerhalb des Betriebssystems aber merken (daher
auch die overlap-structure, die die notwendigen Informationen bezüglich des Aufrufes enthält und deren
Inhalt im Betriebssystem abgelegt ist). Darüberhinaus muss das I/O-Susbsystem darauf eingerichtet sein,
dass zwischen Start des Zugriffs und Ende des Zugriffs die zugehörige Applikation beendet wird.
Einige Betriebssysteme (z.B. Unix) stellen Warteaufrufe mit explizitem Warten nicht direkt (native) zur Verfügung (auch wenn sich diese leicht implementieren lassen). Hier kann man sich aber an der Applikationsschnittstelle anderer Möglichkeiten bedienen, um die gleiche Funktionalität zu bekommen. Dazu wird der
eigentliche Zugriff auf das Gerät auf einen zweiten Thread verteilt (Bild Asynchroner Zugriff über Threads).
Der zweite Thread schläft im eigentliche Leseaufruf (Funktion read) solange, bis die Daten vorhanden sind.
Der Hauptthread wartet schließlich, bis sich der zweite Thread beendet.
...
char *OverlappedAccess()
{
char buf[100];
DWORD dwBytesRead;
DWORD dwCurBytesRead;
OVERLAPPED ol;
ol.Offset = 10;
ol.OffsetHigh = 0;
ol.hEvent = NULL;
HANDLE hFile = CreateFile("overlap.test", GENERIC_READ,
FILE_SHARE_WRITE, 0, OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, 0);
48
Kapitel 3. Realzeitbetriebssysteme
ReadFile(hFile, buf, 100, &dwBytesRead, &ol);
// perform other tasks in this thread
/ ...
// Synchronise with file I/O
WaitForSingleObject(hFile, INFINITE);
GetOverlappedResult(hFile, &ol, &dwCurBytesRead, TRUE);
CloseHandle(hFile);
return(result_buffer);
}
...
Abbildung 3-29. Expliziter Warteaufruf mit Win32 Interface
#include <stdio.h>
#include <pthread.h>
static char buf[100]; // global or as argument to pthread_create
void *AsyncThread( void *fp )
{
fread( buf, sizeof(buf), 1, (FILE *)fp );
pthread_exit( NULL );
}
main( int argc, char **argv )
{
pthread_t MyThread;
FILE *fp;
fp = fopen( "overlap.test", "r" );
if( fp==NULL ) {
perror( "overlap.test" );
return;
}
if( pthread_create( &MyThread, NULL, AsyncThread, fp )!= 0 ) {
fprintf(stderr,"creation of thread failed\n");
exit( -1 );
}
// perform other tasks
// ...
// Synchronise with file I/O
pthread_join( AsyncThread, NULL );
}
Abbildung 3-30. Asynchroner Zugriff über Threads
Neben diesen beiden prinzipiellen Zugriffsmöglichkeiten gibt es die Variation des nicht blockierenden Zugriffs (non blocking mode).
Beim nicht blockierenden Zugriff wartet die aufrufende Task (der Thread) nicht, wenn die angeforderten
Daten nicht zur Verfügung stehen. Stattdessen kehrt der Funktionsaufruf (Systemcall) direkt mit einer
entsprechenden Benachrichtigung zurück. Blockierenden oder nicht blockierenden Zugriff kann man bei
Unix-Betriebssystemvarianten entweder beim Öffnen/Initialisieren (Geräte werden über die Funktion open
geöffnet bzw. initialisiert) einstellen, oder nachträglich über die Funktion fcntl.
49
Kapitel 3. Realzeitbetriebssysteme
int fd;
fd = open( "/dev/modem", O_RDWR | O_NONBLOCK );
if( fd < 0 ) {
perror( "/dev/modem" );
return( -1 );
}
Abbildung 3-31. Öffnen eines Gerätes im Non-Blocking Mode
Weitere Möglichkeiten warten zu vermeiden bietet das I/O-Subsystem an der Applikationsschnittstelle mit
dem sogenannten select Aufruf respektive poll Funktion (Beispiel Programmbeispiels select). Ein weiteres wichtiges Merkmal ist, dass diese Funktionen (Systemcalls) gleich mehrere Ein- bzw. Ausgabequellen
(z.B. Geräte, Netzverbindungen u.ä.) darauf überprüfen, ob von diesen ohne zu warten Daten gelesen werden können oder ob auf diese ohne warten zu müssen Daten geschrieben werden können.
Select und auch poll können zeitüberwacht aufgerufen werden. Das bedeutet, dass entweder nur nachgesehen werden kann, ob eine Veränderung stattgefunden hat, oder es kann gleichzeitig implizit eine zu
definierende Zeit gewartet werden. Sind innerhalb dieser angegebenen Zeitspanne keine Daten auf den
spezfizierten Ein-/Ausgabequellen angekommen oder lassen sich keine Daten darauf schreiben, bricht die
Funktion mit einer entsprechenden Nachricht ab.
Die Funktionen select und poll werden auch genutzt, um eine Task/Thread (ähnlich der Funktion sleep)
im Millisekundenbereich warten (schlafen) zu lassen. Der Unterschied zwischen select und poll besteht
darin, dass man mittels select nur den Zustand von Ein-/Ausgabequellen überprüfen kann, bei poll jedoch
noch zusätzlich den Zustand von Ereignissen (events).
3.2.3.2. Gerätezugriff innerhalb des Betriebssystemkerns
Um an der Applikationsschnittstelle ein einheitliches Interface anbieten zu können, muss ein
Peripheriemodul gemäß bestimmter Konventionen (Schnittstellen) in den Betriebssystemkern eingebunden
werden. Dazu existiert innerhalb des Betriebssystemkerns die Gerätetreiberschnittstelle. Diese Schnittstelle
verwendend, erstellt der Informatiker oder Ingenieur einen sogenannten Gerätetreiber (Device-Driver), der
– oftmals dynamisch während der Laufzeit – in den Betriebssystemkern integriert wird. Verwendet dann
eine Applikation eine der Zugriffsfunktionen open, close, read, write oder ioctl, wird im Gerätetreiber
eine entsprechende Funktion aufgerufen, die das Peripheriemodul so ansteuert, dass dieses die spezifizierte
Funktionalität erbringt.
Ein Gerätetreiber ist also:
•
•
ein Satz von Funktionen, die den Zugriff auf eine spezifische Hardware (Gerät) steuern.
die gerätespezifische Implementierung der allgemeinen Schnittstellenfunktionen.
Die Aufgabe des Betriebssystems (I/O Subsystem) im Kontext der Gerätetreiber ist:
•
•
•
•
50
eine eindeutige Schnittstelle zur Verfügung zu stellen,
die Verwaltung der Ressourcen,
Mechanismen für die Realisierung von Zugriffsarten und Zugriffsschutz bereit zu stellen,
die Zuordnung der allgemeinen Schnittstellenfunktionen (Systemcalls) zu den gerätespezifischen Funktionen durchzuführen.
• Der Treiber registriert sich beim I/O-Subsystem mit seinen Funktionen und der Majornumber.
• Über die Zuordnung symbolischer Name und Majornumber oder Filedeskriptor wird das Mapping
zwischen I/O-Systemcall und Treiberfunktion durchgeführt.
Kapitel 3. Realzeitbetriebssysteme
•
Das Betriebssystem ruft die mit der Majornumber korrespondierende Treiberfunktion auf.
Auch heute findet man noch sehr häufig Ingenieure, die Peripheriemodule nicht über einen Treiber, sondern
direkt aus der Applikation heraus ansteuern. Jedoch ist ein Treiber notwendig:
•
•
•
Um benötigte Ressourcen vom Betriebssystem zugeteilt zu bekommen. Schließlich ist das Betriebssystem für die Verwaltung der Ressourcen zuständig. Wird ein Gerät nicht systemkonform eingebunden,
kann das Betriebssystem die zugesicherten Eigenschaften nicht mehr garantieren. Es hat beispielsweise
keinerlei Kontrolle darüber, ob eine Ressource (ein Interrupt, ein I/O-Bereich oder ein sonstiger Speicherbereich) bereits vergeben ist oder nicht. Das Sperren von Interrupts in ungeeigneten Systemzuständen
kann außerdem zu erhöhten Latenz-Zeiten (siehe Abschnitt Interrupt- und Task-Latenzzeiten) führen.
Um systemkritische Teile zu kapseln. Zugriffe auf Hardware sind sicherheitskritische Aktionen, die nur
innerhalb eines Treibers durchgeführt werden dürfen. Wird aber beispielsweise die Hardware aus der Applikation heraus angesteuert (indem die Register bzw. Speicherbereiche der Hardware in den Adressraum
der Applikation gemapped werden), muss die Applikation erweiterte Zugriffsrechte bekommen. Damit
wiederum gefährdet sie die Sicherheit des gesamten Systems. Programmierfehler können nicht nur zum
Absturz der Task, sondern sogar zum Absturz des gesamten Systems führen.
Um bei Applikationsfehlern das Gerät in den sicheren Zustand überführen zu können. Durch die eindeutige Trennung zwischen Applikation und Gerätetreiber ist es für das Betriebssystem möglich, bei
Applikationsfehlern das Gerät in den sicheren Zustand zu überführen. Stürzt die Applikation durch einen
Fehler ab, erkennt dies das Betriebssystem. Da es außerdem weiß, dass diese (bzw. welche) Applikation auf das Gerät zugegriffen hat, kann es die im Gerätetreiber befindliche Funktionalität aktivieren,
einen sicheren Gerätezustand herzustellen. Insbesondere dieser dritte Grund ist für sicherheitskritische
Echtzeitsysteme wesentlich!
Die meisten Betriebssysteme haben das durch Unix eingeführte Konzept übernommen, Geräte auf Applikationsebene auf Dateien abzubilden und mit den bereits vorgestellten Systemcalls (open, close, read, write,
ioctl, select und poll) anzusprechen.
Diese Aufrufe werden innerhalb des Betriebssystems an den Gerätetreiber weitergereicht. Verwendet also
eine Applikation den open-Aufruf, um eine Ressource (in diesem Falle also ein Gerät) zu reservieren, stellt
das Betriebssystem fest, um welches Gerät es sich handelt und ruft dann die zugehörige open-Funktion
innerhalb des Treibers auf (Bild Treiberauswahl). Jeder Treiber muss also eine treiberspezifische openFunktion zur Verfügung stellen. Es gibt damit im Betriebssystem so viele open-Funktionen wie es Treiber
gibt, während an der Applikationsschnittstelle genau eine open-Funktion existiert.
Damit die richtige Treiberfunktion ausgewählt werden kann, benötigt das Betriebssystem natürlich einen
Anhaltspunkt. Dieser Anhaltspunkt ist beim Systemcall (also an der Applikationsschnittstelle) open ein
symbolischer Name, über den die Zuordnung durchgeführt wird, bei den anderen Funktionen ein Deskriptor,
den das Betriebssystem der Applikation als Ergebnis des open-Systemcalls zurückgibt.
Die Zuordnung eines symbolischen Gerätenamens zum zugehörigen Gerätetreiber wird in Unix beispielsweise über die sogenannten Majornumber durchgeführt. Im Dateisystem wird eine Datei erzeugt, die einen
symbolischen Namen wie zum Beispiel mydevice besitzt. Diese Datei bekommt als Attribute eine Majornumber, zum Beispiel 122 zugeteilt (der Aufruf dazu lautet mknod mydevice c 122 0). Diese Majornumber
ist im Regelfall eindeutig festgelegt und im Treiber eincodiert. Wird der Treiber schließlich vom Betriebssystem geladen (unter Linux dynamisch mit dem insmod Kommando), meldet sich dieser beim Betriebssystemkern mit der eincompilierten Majornumber an. Bei diesem Vorgang wird außerdem eine Liste der
Treiberfunktionen mit übergeben, die aufgerufen werden sollen, wenn die Applikation die entsprechenden Zugriffsfunktionen (open, close, read, write, ioctl, select oder poll) aufruft. Damit sind im System
51
Kapitel 3. Realzeitbetriebssysteme
alle notwendigen Informationen vorhanden, um bei Zugriffen durch die Applikation die entsprechenden
Treiberfunktionen zu starten.
Bild Einbindung des Gerätetreibers in das System zeigt dieses noch einmal anhand eines
Linux-Gerätetreibers. Wird der Treiber per insmod geladen, so wird beim Laden die Funktion init_module
aufgerufen. Diese Funktion registriert den Treiber, wobei die Majornumber und die Tabelle mit
den Treiberfunktionen dem Kernel übergeben werden (register_chrdev(MY_MAJOR_NUMBER, ...,
&mydevice_table);, wobei MY_MAJOR_NUMBER zum Beispiel 122 sein kann).
Open−
Systemcall
Applikation
Kernel
Open−Funktion des
Gerätetreibers wird aufgrund
der Systemcallparameter ausgewählt
serielle
parallele
Schnittstelle Schnittstelle Festplatte CD−ROM
Analog
Input
Open−Funktionen
der Gerätetreiber
Hardware
Abbildung 3-32. Treiberauswahl
Ist der Treiber geladen, kann die Applikation diesen nutzen. Dazu muss im Filesystem eine Spezialdatei
existieren, die die Zuordnung des symbolischen Gerätenamens zur Majornumber MY_MAJOR_NUMBER (also
z.B. 122) durchführt. Führt die Applikation dann den Systemcall open mit dem Gerätetreibernamen als
Parameter durch, ruft der Kernel die gerätetreiberspezifische Funktion mydevice_open auf. Das gleiche gilt
für die übrigen Schnittstellenfunktionen wie read oder write.
Bei der Treibererstellung sind damit im wesentlichen diese internen Funktionen zu kodieren:
init_device/init_module
Die Funktion, die beim Laden des Treibers aufgerufen wird, hat die folgenden Aufgaben:
• Anmelden des Treibers beim Betriebssystem.
• Eventuell automatisches Suchen und Erkennen der Hardware und des Hardwaretyps (z.B. serielle
Schnittstelle und diese in der Ausprägung mit 16550 Chip).
• Initialisierung der Hardware.
• Anforderung von System-Ressourcen (Interrupt, I/O-Bereich, Speicherbereich).
exit_treiber/cleanup_module
Diese Funktion führt folgende Aktivitäten durch:
• Freigabe der vom Treiber reservierten Systemressourcen.
• Abmelden des Treibers beim Betriebssystem.
52
Kapitel 3. Realzeitbetriebssysteme
open
(treiberspezifisch)
Diese Funktion hat die folgenden Aufgaben:
• Überprüfen der Schreib-/Leserechte auf das Gerät.
• Überprüfen, ob der Zugriff zu einem Zeitpunkt für eine oder für mehrere Applikationen erlaubt ist
und ob bereits eine Applikation die Ressource nutzt.
• Zuteilung von Hardware-Ressourcen (z.B. einem Prozess wird einer von vier DMA-Kanälen
zugeteilt).
close
(treiberspezifisch)
Die Funktion close hat die Aufgaben:
• Freigabe der (durch open) belegten Ressourcen.
• Überführen des Gerätes (der Hardware) in einen definierten und sicheren Zustand (deinitialisieren).
Hat eine Applikation das Device (Gerät) geöffnet, bricht dann aber ab, ohne das Gerät wieder zu
schließen (z.B. weil die Applikation abgestürzt ist), ruft das Betriebssystem anstelle der Applikation
die close-Funktion auf und gewährleistet damit einen sicheren Betrieb.
read
(treiberspezifisch)
Aufgabe von read ist:
• Realisierung der Zugriffsarten (blockierend, nicht blockierend, siehe Abschnitt Zugriffsarten)
• Anpassung des Taskzustandes (Versetzen der Task im blocking-Mode in den Zustand “schlafend”
bzw. in den Zustand “lauffähig”).
• Kopieren der Daten aus dem Kernelspace in den Userspace (Speicherbereich der aufrufenden Applikation). Das Kopieren der Daten wird über Systemfunktionen durchgeführt, da aus Sicherheitsgründen ein direkter Zugriff auf die Speicherbereiche nicht möglich ist.
write
(treiberspezifisch)
Für diese Funktion gilt das gleiche wie bei der read-Funktion:
• Realisierung der Zugriffsarten (blockierend, nicht blockierend, siehe Abschnitt Zugriffsarten)
• Anpassung des Taskzustandes (Versetzen der Task im blocking-Mode in den Zustand “schlafend”
bzw. in den Zustand “lauffähig”).
• Kopieren der Daten aus dem Userspace in den Kernelspace.
ioctl
(treiberspezifisch)
Mittels ioctl (I/O-Control) werden die folgenden Aufgaben durchgeführt:
• Realisierung gerätespezifischer Funktionalität, die sich nicht sinnvoll auf das read/write Interface
abbilden lassen.
• Einstellung von Treiberparametern (z.B. Baudrate bei einer seriellen Schnittstelle).
IO-Controlls sollten so sparsam wie möglich verwendet werden! Diese Funktion stellt ein über Plattformen hinweg nur sehr schwer zu portierende Eigenschaft des Treibers dar. Die ioctl-Funktion bekommt
als Parameter ein Kommando und einen Zeiger auf eine Datenstruktur mit. Entsprechend dem Kommando beinhaltet die Datenstruktur Datenbereiche im Userspace, aus denen Daten abgeholt (kopiert)
werden können bzw. in die Daten geschrieben (kopiert) werden können.
select
Die treiberspezifische select-Funktion hat nur die Aufgabe zu überprüfen:
• ob Daten, ohne warten zu müssen (also direkt), vom Gerät gelesen werden können,
• ob Daten, ohne warten zu müssen (also direkt), auf das Gerät geschrieben werden können.
53
Kapitel 3. Realzeitbetriebssysteme
Die an der Applikationsschnittstelle übrigen Funktionalitäten (insbesondere die Überwachung
mehrerer Ein-/Ausgabekanäle und die Zeitüberwachung) sind innerhalb des I/O-Subsystems
realisiert.
poll
Die Funktion poll wird im Treiber wie die Funktion select behandelt. Auch hier wird die eigentliche
Funktionalität innerhalb des Kernels realisiert.
Kernelmodul: treiber.c
struct file_operations fops {
.open
=driver_open;
.release=driver_close;
.write =driver_write;
.read
=driver_read;
.ioctl =driver_ioctl;
...
};
int mod_init()
{
register_chrdev(240, ...,
&Fops);
...
}
void mod_exit()
{
unregister_chrdev(...);
...
}
insmod treiber.ko
mknod geraete_datei c 240 0
geraete_datei
240 | 0
Über die Major−Nummer der
Gerätedatei findet der Kernel
die zugehörige driver_open−
Funktion.
Applikation:
main()
{
int fd=open("geraete_datei", O_RDWR);
write( fd, "Hallo", 6 );
close( fd );
}
rmmod treiber
int driver_open(...)
{
...
}
int driver_close(...)
{
...
}
int driver_write(...)
{
...
// copy data from user space to I/O
...
}
...
KERNEL
USER
Abbildung 3-33. Einbindung des Gerätetreibers in das System
3.2.3.3. Filesystem
Filesysteme dienen zum Abspeichern bzw. Sichern von:
•
•
•
Programmen (Betriebssystemkern, Dienstprogrammen und Applikationen)
Konfigurationsinformationen
Daten (z.B. HTML-Seiten)
auf sogenanntem Hintergrundspeicher.
Werden in der Desktop- und Serverwelt klassischerweise Festplatten als Hintergrundspeicher verwendet,
benutzt man im Bereich eingebetteter Systeme EEPROMs oder Flashspeicher.
54
Kapitel 3. Realzeitbetriebssysteme
Damit auf einen Hintergrundspeicher mehrere Dateien/Daten abgelegt werden können, ist eine Organisationsstruktur (Filesystem, z.B. FAT, VFAT, NTFS, Linux Ext2 Filesystem) notwendig. Diese Organisationsstruktur sollte:
•
•
einen schnellen Zugriff ermöglichen und
wenig Overhead (bezüglich Speicherplatz) für die Verwaltungsinformation benötigen.
...
read( fd, buf, 128 )
...
Applikation
Buffercache
Hintergrundspeicher
Abbildung 3-34. Lesen über den Buffercache
Um schnellen Zugriff zu ermöglichen, arbeiten einige Systeme mit einem dazwischengeschalteten Cache,
dem sogenannten Buffercache (in der Windows-Welt auch unter dem Produktnamen “Smartdrive” bekannt).
Möchte eine Applikation eine Datei, die auf dem Filesystem abgelegt ist, lesen, schaut der Betriebssystemkern im Buffercache nach, ob die gewünschte Information dort vorhanden ist oder nicht. Ist dies nicht der
Fall, wird die Information in den Cache geladen und dann weiter an die Applikation gereicht. Wird ein
Schreibauftrag erteilt, gehen die Daten zunächst an den Buffercache. Erst einige Zeit später werden die
Daten auf dem Hintergrundspeicher mit den Daten aus dem Cache abgeglichen.
Dieses Verfahren ist oftmals nicht tolerabel:
•
•
Das Filesystem kann dann Inkonsistenzen enthalten, wenn das System abstürzt, der Hintergrundspeicher
aber nicht mit dem Cache rechtzeitig abgeglichen worden ist.
Zugriffe auf das Filesystem sind nicht deterministisch, da nicht vorhergesagt werden kann, ob angeforderte Daten im Cache gefunden werden oder zu einem Leseauftrag an den Hintergrundspeicher führen.
Um dennoch das Verfahren im Umfeld von Echtzeitsystemen einsetzen zu können, wird der sync-Mode
verwendet. Beim sync-Mode werden Dateien direkt (also unter Umgehung des Buffercaches) geschrieben,
ein Lesezugriff kann aber über den deutlich schnelleren Buffercache stattfinden.
3.2.4. Systemcall-Interface
Über die Systemcall-Schnittstelle lassen sich aus der Applikation heraus die Dienste des Betriebssystems
nutzen. Diese Schnittstelle ist absolut unabhängig von jeglicher Programmiersprache. In den seltensten
Fällen greift eine Applikation direkt auf das Systemcall-Interface zu, sondern nutzt zu diesem Zwecke
Bibliotheksfunktionen. So lautet beispielsweise der Systemcall für die Ausgabe von Daten in eine Datei
oder in ein Gerät write, wobei sich hinter dem Namen eine Nummer (bei Linux beispielsweise der Code 4)
verbirgt.
55
Kapitel 3. Realzeitbetriebssysteme
Systemcalls erwarten ihre Argumente entweder in Registern oder auf dem Stack. Ein Systemcall wird dann
über den Assemblerbefehl INT bzw. TRAP mit einer Exceptionnummer (bei Linux beispielsweise 0x80)
aufgerufen (Softwareinterrupt). Dieser Befehl führt zu einer Exception, wobei die Exceptionnummer innerhalb des Betriebssystems die Dienstaufrufsschnittstelle aktiviert. Hier wird anhand der Registerinhalte
(Systemcallcode) in die entsprechenden Funktionen verzweigt.
Bild Codebeispiel Systemcall macht dieses deutlich. Der Code für den Systemcall (hier 4) wird in das Register eax geschrieben. Die Register ebx, ecx und edx werden mit den Parametern des Funktionsaufrufes belegt
(in diesem Fall der Dateideskriptor 1 für stdout, die Adresse der auszugebenden Daten "Hello World"
und die Länge 12 des auszugebenden Strings). Danach wird der Softwareinterrupt für die SystemcallSchnittstelle (0x80) aufgerufen.
.text
.globl write_hello_world
write_hello_world:
movl $4,%eax
; //code fuer "write" systemcall
movl $1,%ebx
; //file descriptor fd (1=stdout)
movl $message,%ecx ; //Adresse des Textes (buffer)
movl $12,%edx
; //Laenge des auszugebenden Textes
int $0x80
; //SW-Interrupt, Auftrag an das BS
ret
.data
message:
.ascii "Hello World\n"
Abbildung 3-35. Codebeispiel Systemcall
Der angegebene Assemblercode realisiert einen Unterprogrammaufruf (ohne Rückgabewert, also vom Typ
void), der in eine Bibliothek abgelegt werden könnte. Danach würde eine Applikation nur noch diese Bibliotheksfunktion (Library-Call) aufrufen, um den String auszugeben (Bild Codebeispiel Library-Call).
main( int argc, char **argv )
{
write_hello_world();
}
Abbildung 3-36. Codebeispiel Library-Call
Im Regelfall erweitern Bibliotheksfunktionen aber noch den Funktionsumfang von Systemcalls. Die wenigsten Applikationen verwenden beispielsweise den write-Aufruf zur einfachen Ausgabe von Informationen,
stattdessen verwenden sie eine Funktion wie printf, um die Ausgabeinformation noch zu formatieren. Die
Funktion printf wiederum führt die Formatierung und den anschließenden Systemaufruf write durch.
3.2.5. Services
Ein Betriebssystem besteht aus dem Betriebssystemkern, ebenso aber auch aus Diensten, die auf der UserEbene ablaufen (Services oder Daemonen genannt).
Das Vorhandensein und die Aufgaben dieser Dienste hängen stark vom Einatz des Realzeitbetriebsystems
ab. Es lassen sich die folgenden Gruppen unterscheiden:
56
Kapitel 3. Realzeitbetriebssysteme
Dienste zur Konfiguration
Insbesondere in der Hochlaufphase des Systems müssen diverse Konfigurationen durchgeführt werden.
Hier sind also Dienste aktiv, die die Hardware initialisieren, die das Netzwerk konfigurieren (z.B. IPAdresse) oder auch Treiber laden.
Protokolldienste
Entgegen der sehr verbreiteten Art und Weise, dass die einzelnen (Realzeit-) Tasks Informationen auf
proprietäre Weise protokollieren und sichern, sollten die vom Betriebssystem zu diesem Zweck eingerichteten Dienste in Anspruch genommen werden. Protokolliert wird dann nicht nur auf einheitliche
Weise der Zustand des Betriebssystems, sondern auch der Zustand der Applikation.
Netzwerkdienste
Ist das Realzeitsystem vernetzt, müssen verschiedene Dienste aktiv sein, um Anfragen, die vom Netz
kommen, zu beantworten. Typische Dienste hier sind der HTTP-Server (WWW) oder der FTP-Server.
Hierzu zählen aber auch Dienste zur Zeitsynchronisation in einem verteilten Echtzeitsystem.
Dienste der Zeitsteuerung
Betriebssysteme bieten die Möglichkeit, Tasks zyklisch oder zu vorgegebenen Zeiten zu starten. Würden die Tasks diese Zeitsteuerung selber dadurch realisieren, dass sie sich für einen entsprechenden
Zeitraum “schlafen” legten, würden im System mehr Ressourcen benötigt, da die Tasks Speicher im
System (und in den Systemtabellen des Betriebssystemkerns) belegten.
3.2.6. Bibliotheken
dynamic
statisch
Platte
Hauptspeicher
Prgm. 1
Prgm. 2
printf
printf
RP 1
RP 2
printf
printf
Prgm. 1
Prgm. 2
Prgm.
printf
RP 1
RP 2
printf
shared
printf
RP
printf
dynamic loaded
Abbildung 3-37. Verschiedene Bibliotheksarten
Wie bereits in Abschnitt Systemcall-Interface beschrieben, abstrahieren zum Betriebssystem gehörige Bibliotheken (Libraries) den Zugriff auf die Systemcalls/Dienste (nicht zu verwechseln mit den Dienstprogrammen, Services oder Daemonen). Libraries werden sowohl zu den eigenen Applikationen als auch zu den
Dienstprogrammen hinzugebunden. Man unterscheidet statische von dynamischen Bibliotheken. Während
statische Libraries zu dem eigentlichen Programm beim Linken hinzugebunden werden, werden dynamische Bibliotheken (auch shared libs genannt) erst dann an das Programm gebunden, wenn dieses ausgeführt
werden soll. Das ergibt folgende Vorteile:
57
Kapitel 3. Realzeitbetriebssysteme
•
Das eigentliche Programm wird kleiner (der Code für die Libraries muss ja nicht abgelegt werden).
•
Programme können sich eine “shared lib” teilen, wodurch Hauptspeicherplatz gespart wird. dass mehrere
Programme Code verwenden, der nur einmal im Speicher ist, wird durch die Trennung von Code- und
Datensegment ermöglicht (Bild Verschiedene Bibliotheksarten).
Nachteilig bei diesem Verfahren ist es, dass zum Ausführen einer Applikation nicht nur selbige,
sondern zusätzlich auch alle zugehörigen Bibliotheken in der richtigen Version notwendig sind. Gerade
bei komplexen Applikationen können diese Wechselwirkungen derart groß werden, dass Im übrigen
unterstützen moderne Betriebssysteme das Laden dynamische Libraries unter Programmkontrolle (Bild
Dynamisches Laden von Funktionen während der Laufzeit).
Dynamisch ladbare Bibliotheken werden im Regelfall vom System zur Applikation dazugebunden (gelinkt),
wenn die Applikation gestartet wird. Daneben gibt es auch die Variante, dass eine Applikation selbst eine
Bibliothek lädt (siehe Bild Dynamisches Laden von Funktionen während der Laufzeit). Dieses bietet für
einige Anwendungen den Vorteil der möglichen Modularisierung.
#include <stdio.h>
#include <dlfcn.h>
int main(int argc, char **argv)
{
void *handle;
int (*function)(int);
char *error;
handle = dlopen ("myshlib.so", RTLD_LAZY);
if( (error=dlerror()) ) {
fprintf(stderr,"%s\n", error );
return( -1 );
}
function = dlsym(handle, "DoubleMinusOne");
printf ("%d\n", function(5) );
dlclose(handle);
}
Abbildung 3-38. Dynamisches Laden von Funktionen während der Laufzeit
Während früher beim Linken statischer Libraries die komplette Bibliothek hinzugebunden wurde, werden
heute aus einer Bibliothek nur die Funktionen extrahiert, die ein Programm auch wirklich einsetzt. Damit
wird ebenfalls verhindert, dass der Programmcode auf der einen Seite zu stark anwächst oder auf der anderen Seite Funktionen auf mehrere Libraries verteilt werden. (Die Verwendung vieler kleiner Libraries
anstelle einer großen Library spart Speicherplatz.)
Insbesondere in eingebetteten Systemen bzw. wenn einfache Laufzeitsysteme verwendet werden, entfällt
die Notwendigkeit von Bibliotheksfunktionen.
3.3. Zeitaspekte
3.3.1. Interrupt- und Task-Latenzzeiten
Echtzeitbetriebssysteme werden durch die Hersteller meist über zwei Werte charakterisiert: Der Interruptund der Task-Latenzzeit.
58
Kapitel 3. Realzeitbetriebssysteme
Die Interruptlatenzzeit tL ist die Zeit, die zwischen dem Auftreten des Interrupts (tA) und dem Starten der
zugehörigen Interruptserviceroutine maximal vergeht.
Ereignis tritt ein
Task Latency
Interrupt Latency
Hardware
Latency
1111
0000
0000
1111
Preemption
Latency
ISR
Higher prior
IR−Latency
ISR2
IR−Execution
Latency
Scheduler
Context Switch
(IR disabled)
IR ausgelöst
000
111
000
111
11
00
000
00111
11
System Call Delay
Abbildung 3-39. Verzögerungszeiten
Die Interruptlatenzzeit setzt sich aus den folgenden Zeiten zusammen [TimMon97]:
1. Hardware-Latency: Zeit, die die Hardware benötigt, um das Ereignis in Form eines Interrupts dem
Prozessor zu melden (einige wenige Gatterlaufzeiten).
2. Preemption-Delay: Verzögerungszeit, die entsteht, wenn sich das Betriebssystem in einem kritischen
Abschnitt befindet, der nicht unterbrochen werden darf (Interrupts sind gesperrt).
3. Verarbeitungszeit einer höherprioren ISR.
Die Tasklatenzzeit tList die Zeit, die zwischen dem Auftreten des Ereignisses (Interrupt, tA) un dem Start
des zugehörigen Rechenprozesses maximal vergeht. Die Tasklatenzzeit setzt sich aus den folgenden Zeiten
zusammen:
1. Interruptlatenzzeit.
2. Verarbeitungszeit der zum Ereignis zugehörigen ISR.
3. System Call: Tritt der Interrupt gerade zu einem Zeitpunkt auf, an dem ein Systemcall bearbeitet wurde,
muss zunächst dieser Systemcall zuende bearbeitet (oder abgebrochen) werden.
4. Scheduling: Zeit, die vergeht, um den nächsten lauffähigen Prozess auszuwählen (der Scheduler wird
im Regelfall mit jedem Interrupt aufgerufen).
5. Context-Switch: Zeit, die benötigt wird, um die (neue) Task starten zu können (also die Zeit, um die
Register der CPU mit den Daten der zu bearbeitenden Task zu laden).
6. Hat die Task, für die das Ereignis gilt, nicht höchste Priorität, werden erst die höher prioren Tasks
bearbeitet.
Die Zeiten treten weder in der angegebenen Reihenfolge auf, noch müssen sie grundsätzlich alle berücksichtigt werden. Hinzu kommt, dass die Zeiten auch mehrfach auftreten, wenn beispielsweise mehr als zwei
unterschiedliche Interrupts auftreten.
Die Hardwarelatenzzeit muss in jedem Fall berücksichtigt werden, fällt aber bei ihrer Größenordnung (ns)
im Vergleich zu den anderen Zeiten kaum ins Gewicht. Diese Zeit fällt maximal einmal an.
Da Interrupts die höchste Priorität haben, fallen – abgesehen von dem Fall, dass Interrupts gesperrt sind –
die Interrupt-Latenzzeiten an. Interrupt-Latenzzeiten treten für jeden Interrupt genau einmal auf, bei unterschiedlicher Priorisierung der Interrupts möglicherweise auch mehrfach. Die Zeiten, die durch Abarbeitung
von Interrupt Service Threads anfallen, werden hier als den Interrupts zugehörig betrachtet.
59
Kapitel 3. Realzeitbetriebssysteme
Nach den Interrupts bearbeitet der Prozessor zunächst einen aktiven Systemcall zuende. Danach folgt der
Scheduler und schließlich der Kontextswitch. Diese Zeiten fallen durchaus mehrfach an, insbesondere dann,
wenn es noch höherpriore Tasks im System gibt.
Diese Verzögerungszeiten kennzeichnen die Realzeiteigenschaften eines Betriebssystems. Echtzeitbetriebssysteme weisen auf PC-Plattformen (Pentium) beispielsweise Wartezeiten in der Größenordnung von 15
bis 300 µs auf.
Bei der Wahl eines Echtzeitbetriebssystems werden häufig die Interrupt-Latenzzeiten verglichen. Die
Interrupt-Latenzzeit wird aber im wesentlichen durch die ISR’s der Treiber bestimmt. Der Hersteller eines
Betriebssystem kennt aber nur die Treiber, die integrativer Bestandteil sind. Beim Vergleich ist daher auf
die eigentliche Systemkonfiguration zu achten. Sperrt ein einzelner
Untersuchungen haben gezeigt, dass Latenzzeiten nicht unbedingt konstant sein müssen. Vielmehr gibt es
auch den Fall, dass die Zeiten von der sogenannten Uptime des Systems abhängen (z.B. dadurch, dass intern
Listen verarbeitet werden, die it längerer Laufzeit des Systems ebenfalls länger werden) [Mächtel].
Merke: Um Verzögerungszeiten (Latency-Times) so gering wie möglich zu halten, ist es beim Systemdesign wichtig, Interrupts nicht länger als notwendig zu sperren und critical sections kurz zu halten.
3.3.2. Unterbrechungsmodell
Das Unterbrechungsmodell ist verantwortlich für den Schutz kritischer Abschnitte und damit auch für
Latenzzeiten im System.
Der Systemarchitekt muss es kennen, da die Ebenen unterschiedlich priorisiert sind und er durch Zuordnung
von Funktionen auf Ebenen das Echtzeitverhalten generell beeinflusst. Der Programmierer muss es kennen,
da er nur so kritische Abschnitte identifizieren und effizient schützen kann.
Grundsätzlich muss er hierfür die quasi-parallelen von der real-parallelen Verarbeitung unterschieden. Erstere entsteht durch die Unterbrechbarkeit: Auf einer Einkernmaschine wird ein einzelner Job A unterbrochen (preempted), ein anderer Job B wird lauffähig und abgearbeitet und nach einiger Zeit wird auch
dieser wieder preempted und Job A fortgesetzt. Damit laufen, wenn auch ineinander geschachtelt, Job A
und Job B (quasi-) parallel.
Auf einer Mehrkernmaschine dagegen kann Job A auf einem Prozessorkern und Job B auf einem anderen
Prozessorkern zeitgleich, real-parallel ablaufen. Diese Parallelverarbeitung ist im Unterbrechungsmodell
nicht direkt sichtbar.
Im Linux-Kernel gibt es das in Abbildung Unterbrechungsmodell dargestellte Unterbrechungsmodell, das
aus vier Ebenen besteht.
1. Die Applikations-Ebene.
2. Die Kernel-Ebene.
3. Die Soft-IRQ-Ebene.
4. Die Interrupt-Ebene.
Auf der Applikations-Ebene laufen die Userprogramme, die beispielsweise über Prioritäten gescheduled
werden. Kritische Abschnitte, beispielsweise die Zugriffe auf globale Variablen, werden über Semaphore
oder Spinlocks geschützt.
60
Kapitel 3. Realzeitbetriebssysteme
Rufen Applikationen Systemcalls auf (beispielsweise open(), read() oder nanosleep()) werden diese auf
der Kernel-Ebene im User-Kontext abgearbeitet. Der Systemcall hat dabei die Priorität des aufrufenden
Jobs. Auf gleicher Ebene laufen Kernel-Threads ab, die sich dadurch von User-Threads unterscheiden,
indem sie keine Ressourcen (z.B. Speicher) im Userland belegen und damit im Kernel-Kontext arbeiten.
Kernel-Threads haben eine Priorität und konkurrieren mit User-Threads. Anders als User-Threads werden
die Kernel-Threads jedoch nicht unterbrochen (preempted).
Höchste Priorität in der Abarbeitungsreihenfolge haben Interrupt-Service-Routinen. Sobald ein Interrupt
auftritt und Interrupts freigegeben sind, werden sie auf dem lokalen Kern gesperrt und die zugehörige ISR
wird abgearbeitet. Eine quasi-parallele Verarbeitung findet damit nicht statt. Auf einer Mehrkernmaschine
können real jedoch mehrere ISRs parallel verarbeitet werden, ein und dieselbe typischerweise jedoch nicht.
Eine Datenstruktur, die von nur einer ISR verwendet wird, muss daher nicht gesondert geschützt werden.
Sind anstehende ISRs abgearbeitet, gibt der Kernel Interrupts frei und startet Soft-IRQs, die ebenfalls im
Interruptkontext abgearbeitet werden. Soft-IRQs sind nicht unterbrechbar, können aber auf einer Mehrkernmaschine real parallel abgearbeitet werden. Doch auch hier gilt die Einschränkung, dass ein und derselbe
Soft-IRQ nicht mehrfach gleichzeitig abläuft.
Kernel−Level
Soft−IRQ−Level
KERNEL
vertical
preemptible
USER
Application−Level
horizontal preemption
Interrupt−Level
preemptible
not preemptible
Abbildung 3-40. Unterbrechungsmodell
Da Funktionen, die im Interrupt-Kontext ablaufen, nicht schlafen gelegt werden können, kann ein kritischer
Abschnitt auf dieser Ebene niemals per Semaphor geschützt werden. Hier wird auf dem lokalen Kern eine
Interruptsperre eingesetzt und für die reale Parallelität ein Spinlock.
Der Linux-Kernel 2.6.xx bietet die Möglichkeit von so genannten Threaded-Interrupts. Hierbei laufen Interrupts als Kernel-Threads, also im Kernel-Kontext ab. Vorteil: Threads können durch den Systemarchitekten
untereinander und sogar bezüglich sonstiger User-Tasks priorisiert werden.
3.3.3. Timing (Zeitverwaltung)
Zeiten spielen in einem Echtzeitsytem eine wesentliche Rolle. Ein Zeitgefühl wird für unterschiedliche
Aufgaben benötigt:
Zeitmessung. Das Messen von Zeiten ist eine oft vorkommende Aufgabe in der Automatisierungstechnik.
Geschwindigkeiten lassen sich beispielsweise über eine Differenzzeitmessung berechnen.
61
Kapitel 3. Realzeitbetriebssysteme
Zeitsteuerung für Dienste. Spezifische Aufgaben in einem System müssen in regelmäßigen Abständen
durchgeführt werden. Zu diesen Aufgaben gehören Backups ebenso, wie Aufgaben, die der User dem System überträgt (z.B. zu bestimmten Zeitpunkten Meßwerte erfassen).
Watchdog (Zeitüberwachung). Neben der Zeitmessung spielt die Zeitüberwachung eine sicherheitsrelevante Rolle in Echtzeitsystemen. Zum einen werden einfache Dienste (z.B. die Ausgabe von Daten an eine
Prozessperipherie) zeitüberwacht, zum anderen aber auch das gesamte System (Watchdog). Dazu muss das
System im regelmäßigen Abstand einen Rückwärtszähler (Timer) zurücksetzen. Ist das System in einem
undefinierten Zustand und kommt nicht mehr dazu, den Zähler zurückzusetzen, zählt dieser bis Null und
bringt das System in den sicheren Zustand (löst beispielsweise an der CPU ein Reset aus).
Das für Betriebssystem und Anwendung benötigte Zeitgefühl wird über Hardware- oder Software-Zähler
realisiert, die periodisch getaktet incrementieren oder decrementieren. Die Auflösung der Zähler ergibt sich
aus der Taktfrequenz, die Genauigkeit aus der Stabilität des Taktgebers. Grundsätzlich gilt: je höher die Taktfrequenz, desto genauer die Zeitmessung. Die Bitbreite des Zählers entscheidet über den (Zeit-) Meßbereich.
Die Software-Zähler werden typischerweise im Rahmen einer Interrupt-Service-Routine incrementiert
beziehungsweise decrementiert. Klassischerweise wird dafür ein periodischer Interrupt eingesetzt
(Timer-Interrupt), der beispielsweise alle 10ms, also mit einer Rate von 100Hz auftritt. Innerhalb der durch
den Interrupt aktivierten Interrupt-Service-Routine wird nicht nur der Zähler incrementiert, sondern auch
überprüft, ob Rechenprozesse in den Zustand lauffähig zu versetzen sind, die auf das Ablaufen einer Zeit
geschlafen haben. Danach läuft der Scheduling-Algorithmus und schließlich der Kontext-Switch ab.
Gemäß dieser Architektur sind Zeitaufträge mit einer Auflösung von maximal dem so genannten TimerTick (Zeitabstand zwischen zwei Timer-Interrupts) möglich. Möchte beispielsweise ein Job für wenige
Mikrosekunden schlafen wird er trotzdem erst mit dem nächsten Timertick geweckt. Im ungünstigsten Fall
ist das bei 100 Hz nach knapp 10ms. Um die Genauigkeit von Zeitoperationen zu verbessern, muss die Rate
der Timerinterrupts erhöht werden. Das geht allerdings zu Lasten der Effizienz, was sich insbesondere bei
leistungsschwachen Systemen bemerkbar machen kann.
Moderne Echtzeitsysteme sind tickless. Tickless bedeutet jedoch mitnichten, dass es gar keinen TimerInterrupt gibt, sondern nur, dass der Timer-Interrupt nicht periodisch, sondern bedrafsgemäß auftritt. Dazu
wird mit jedem Timer-Interrupt der nächste Zeitpunkt berechnet, an dem der Interrupt erneut ausgelöst
werden soll. Entsprechend wird der Interrupt-Controller programmiert. Das hat gleich mehrere Vorteile:
•
•
•
•
Die Systemlast wird reduziert, da nur dann Interrupts auftreten, wenn diese auch gebraucht werden.
Die Effizienz wird gesteigert.
Energie wird eingespart, da während der inaktiven Zeiten das System schlafen gelegt werden kann.
Die Genauigkeit von Zeitaufträgen wird erhäht, da nicht mehr auf den nächsten Interrupt gewartet werden
muss. Vielmehr wird der Interrupt exakt zum benötigten Zeitpunkt ausgelöst.
Nachteilig ist, dass mit jedem Interrupt eine Berechnung des nächsten Auslösezeitpunkts und die Umprogrammierung des Timerbausteins erfolgen muss. Außerdem müssen die Softwaretimer entsprechend der
real vergangenen Zeit aktualisiert werden.
Zwei Ausprägungen von Zeitgebern lassen sich unterscheiden:
Absolutzeitgeber. Zeiten werden grundsätzlich relativ zu einem Ereignis (beispielsweise die Geburt
Christi) angegeben. Falls dieses Ereignis aus Sicht eines Rechnersystems außerhalb und vor allem vor der
Aktivierung des Rechnersystems liegt, spricht man von einer absoulten Zeit (Uhren).
Relativzeitgeber. Steht das Ereignis in direkter Beziehung zum Rechnersystem, beispielsweise das Ereignis
Start des Rechners spricht man von einer relativen Zeit. Der zugehörige Zeitgeber ist ein Relativzeitgeber.
Die Relativ-Zeitgeber gibt es als Vorwärts- und als Rückwärtszähler (Timer).
Bieten Prozessoren einen so genannten Timestamp Counter (TSC) an, einen Zähler, der mit der Taktfrequenz der CPU getaktet wird, nutzt das Betriebssystem diesen häufig, um damit das Timekeeping auf eine
62
Kapitel 3. Realzeitbetriebssysteme
sehr genaue Basis (Nanosekunden) zu stellen. Bei einem Interrupt wird die real vergangene Zeit durch
Auslesen des TSC bestimmt und die internen (Software-) Zeitzähler entsprechend angepasst. Dabei gibt
es allerdings das Problem der variablen Taktfrequenzen. Um Energie zu sparen werden moderne Prozessoren mit möglichst niedriger Frequenz getaktet. Erst wenn mehr Leistung benötigt wird, wird auch die
Taktfrequenz erhöht. Der Taktfrequenzwechsel muss vom Timekeeping natürlich berücksichtigt werden.
Da Applikationen im Userland ebenfalls Zugang zum Timestamp Counter haben, müssen sich diese über
Taktfrequenzänderungen informieren lassen und einen Wechsel entsprechend berücksichtigen.
Beim Umgang mit Zeiten gibt es im Wesentlichen zwei Problemfelder:
Zählerüberläufe. Die verwendeten Hard- und Softwarezähler haben eine endliche Breite und können damit
abhängig von der gewählten Auflösung auch nur einen endlichen Zeitraum messen. Danach gibt es einen
Zählerüberlauf. Der Zählerüberlauf ist dann kritisch, wenn Komponenten Zeitvergleiche durchführen. Bei
einem Zählerüberlauf ist der jüngere Zeitstempel kleiner als der ältere und führt damit zu einem falschen
Vergleichsergebnis infolge dessen es zu einem unvorhersehbarem Verhalten kommen kann. Der LinuxKernel 2.4 beispielsweise kann mit seinem 32-Bit Zähler und einer Rate von 100Hz einen Zählerüberlauf
nach knapp 497 Tage. Die Zähler, die die so genannte Unixzeit repräsentieren, laufen am 19. Januar 2038
um 03:14:08 über.
Zur Lösung dieses Problems sind zum einen ausreichend große Zähler zu verwenden. Linux beispielsweise
setzt ab Kernel 2.6 auf 64-Bit breite Zähler. Das reicht aus, um in Milliarden von Jahren keinen Überlauf zu
bekommen. Zum anderen sind Zeitvergleiche mit Vorsicht zu programmieren. Falls die zu vergleichenden
Zeitstempel nicht weiter auseinanderliegen, als die Hälfte des Meßbereiches, können die in Abschnitt 4.3.2>
vorgestellten Makros eingesetzt werden.
Zeitsynchronisation. Rechner werden nur noch in seltenen Fällen als vollständig autarke Systeme eingesetzt. Vielmehr sind sie Teil eines Gesamtsystems, bei dem mehrere Komponenten Informationen austauschen. Hierbei spielt sehr häufig die Zeit eine wichtige Rolle, so dass die Systeme zeitlich synchronisiert
werden. Ist dabei eine Zeitkorrektur notwendig, darf diese niemals sprunghaft vorgenommen werden! Wird
die Zeit sprunghaft vorgestellt (zum Beispiel bei der Umstellung von Winterzeit auf Sommerzeit), kommen manche Zeitpunkte nicht vor (im Beispiel 2:30 Uhr). Steht zu einem ausgelassenen Zeitpunkt aber
ein Zeitauftrag (Backup) an, wird dieser nicht durchgeführt und geht verloren. Wird die Zeit sprunghaft
zurückgestellt, kommen demgegenüber Zeitpunkte gleich zweifach vor. Zeitaufträge werden dabei unter
Umständen doppelt ausgeführt.
Für eine funktionierende Zeitsynchronisation ist es zunächst notwendig, dass die Systemzeit auf der Zeitzone UTC (universal time zone) basiert. Damit wird das Rechnersystem unabhängig vom Ort und unabhängig von Sommer- und Winterzeiten. Die Anwendungssoftware kann auf Basis der Systemzeit und des
aktuellen Rechnerstandpunktes die lokale Zeit ableiten. Desweiteren darf niemals eine sprunghafte Zeitkorrektur vorgenommen werden. Die Zeit muss vielmehr der Zielzeit angepasst werden. Dazu wird die Zeitbasis (das Auftreten des periodischen Interrupts) gefühlvoll verlangsamt oder beschleunigt. Gefühlvoll bedeutet in diesem Zusammenhang, dass bei großen zeitlichen Unterschieden zunächst eine stärkere Korrektur
und je näher man der Zielzeit ist eine behutsamere Korrektur vorgenommen wird. Das Network Time Protocol (NTP) ist übrigens für diese Art der Zeitsynchronisation entwickelt worden. Der Systemcall adjtime
parametriert die zeitliche Anpassung im Kernel.
Beim Netowrk Time Protocol stellen Zeitserver die aktuelle Zeit zur Verfügung, wobei die Zeitserver klassifiziert werden. Der Zeitserver, der die Uhrzeit selbst bestimmt (z.B. durch eine DCF77 Funkuhr), ist ein
sogenannter Stratus 1 Server. Der Server, der die Uhrzeit von einem Stratus 1 Server bezieht ist der Stratus
2 Server usw. Bei der Verteilung der Uhrzeit werden Berechnungen über die Laufzeit der Pakete zwischen
den Rechnern durchgeführt, und die Uhrzeit wird entsprechend korrigiert.
63
Kapitel 4. Aspekte der nebenläufigen
Echtzeit-Programmierung
4.1. Taskmanagement
Das Taskmanagement ist für die Erzeugung, das Beenden und die Parametrierung von Tasks zuständig.
Zwischen den erzeugenden und den erzeugten Tasks gibt es ein Eltern-Kindverhältnis. Die Kindtasks erben dabei sämtliche Eigenschaften - man sagt das komplette Environment - des Elternjobs. Dazu gehören
beispielsweise die Besitzverhältnisse, die Priorität oder auch offene Datei- oder Kommunikationshandels.
4.1.1. Tasks erzeugen
Zur Erzeugung von Prozessen steht der Systemcall pid_t fork(void) zur Verfügung. Nach dem Aufruf gibt
es eine Kopie des Prozesses, der fork() aufgerufen hat. Der neue Prozess unterscheidet sich nur in der
neuen Prozessidentifikationsnummer (PID) und im Rückgabewert der Funktion. Der Prozess, der fork()
aufgerufen habe bekommt die PID der Prozesskopie zurück, die Kopie aber bekommt 0 zurück. Anhand des
Rückgabewertes kann der Programmierer also erkennen, ob es sich um den Eltern- oder um den Kindprozess
handelt.
if (fork()) {
/* Parentprocess */
...
} else {
/* Childprocess */
...
}
Sehr häufig starten Elternprozesse Kindprozesse, damit diese selbständig eine Verarbeitung durchführen
und ein Ergebnis liefern. Make beispielsweise startet per fork() den Compiler und wartet darauf, dass der
Compiler mitteilt, ob er den Compilierungsvorgang erfolgreich beenden konnte oder beispielsweise wegen
eines Syntaxfehlers abbrechen musste. Ist letzteres der Fall, wird auch Make seine Arbeit beendetn. Erfolg
oder Mißerfolg teilt ein Progamm über seinen Exitcode mit. Der Exitcode entspricht entweder dem Rückgabewert der Funktion main oder dem Parameter der Funktion exit() selbst. Der Exitcode wird über die
Funktion pid_t wait(int *status) oder pid_t waitpid(pid_t pid, int *status, int options) abgeholt.
Erstere legt an der übergebenen Adresse status den Exitcode eines Kindprozesses ab. Welcher Kindprozess
das ist - falls mehrere gestartet wurden - ist am Rückgabewert erkennbar. Um den Rückgabewert eines
bestimmten Kindprozesses abzuholen dient waitpid(). Zur Auswertung des von wait() zurückgelieferten
Werts dient ein Satz von Makros (siehe Manpage).
childpid = fork();
if (childpid==0) { /* child */
....
return result;
}
waitpid( childpid, &status, 0 );
printf("child returned 0x%x\n", status);
if (WIFEXITED(status)) {
printf("child status = %d\n", WEXITSTATUS(status));
}
Durch Aufruf der Funktion int execve(const char *filename, char *const argv[], char *const
überschreibt ein per fork() erzeugter Prozess sein Codesegment. Über diesen Mechanismus kann
ein komplett anderer Code abgearbeitet werden. Beispiel Execve zeigt, wie parallel zum gerade laufenden
envp[])
64
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
Process das Programm cat /etc/issue aufgerufen werden kann. Soll im übrigen ein normales Kommando
aufgerufen werden, kann auch alternativ die Funktion int system(const char *command) verwendet
werden.
Beispiel 4-1. Execve
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main( int argc, char **argv, char **envp )
{
int childpid, status;
char *childargv[] = { "/bin/cat", "/etc/issue", NULL, NULL };
char *childenvp[] = { NULL };
childpid = fork();
if (childpid==(-1) ) {
perror( "fork" );
return -1;
}
if (childpid==0) { /* child */
execve("/bin/cat", childargv, childenvp);
}
/* parent only */
printf("waiting for my child %d to die...\n", childpid);
wait(&status);
printf("child died...\n");
return 0;
}
Threads werden über die Funktion
int pthread_create(pthread_t *thread, const pthread_attr_t
erzeugt. Intern verwendet pthread_create() den
Systemcall clone(). Um die Funktion nutzen zu können, muss die Bibliothek libpthread zum Programm
gebunden werden (Option -lpthread). Anders als bei fork() startet der neue Thread die Verarbeitung
in einer separaten Funktion, deren Adresse über den Parameter start_routine übergeben wird. Dieser
Funktion wird beim Aufruf das Argument arg übergeben. Der Parameter attr steuert die Erzeugung,
thread enthält nach dem Aufruf die Kennung des neuen Threads. pthread_create() gibt im Erfolgsfall 0
zurück, ansonsten einen Fehlercode.
*attr, void *(*start_routine) (void *), void *arg)
Ein Thread beendet sich typischerweise, in dem er void
thread verwendet int pthread_join(pthread_t thread,
thread zu warten.
aufruft. Der Elternum auf den Exitcode des Threads
pthread_exit(void *retval)
void **retval)
Beispiel 4-2. Programmbeispiel Threads
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<unistd.h>
<pthread.h>
void *child( void *args )
{
printf("The child thread has ID %d\n", getpid() );
// You can either call pthread_exit or "return".
// pthread_exit( NULL );
return NULL;
}
int main( int argc, char **argv, char **envp )
{
pthread_t MyThread;
65
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
if( pthread_create( &MyThread, NULL, child, NULL )!= 0 ) {
fprintf(stderr,"creation of thread failed\n");
return -1;
}
/* Now the thread is running concurrent to the main task
*/
/* But it won’t return to this point (aulthough it might
*/
/* execute a return statement at the end of the Child routine).*/
/* waiting for the termination of MyThread */
pthread_join( MyThread, NULL );
printf("end of main thread\n");
return 0;
}
4.1.2. Tasks beenden
Per int kill(pid_t pid, int sig) wird einem anderen Prozess oder einem anderen Thread mit der PID pid
das Signal sig geschickt. Typischerweise führt dieses dazu, dass sich der Job beendet. Allerdings können die
Signale durch die Programme - mit Ausnahme von SIGKILL (Signalnummer 9) - ignoriert oder abgefangen
werden und das Beenden damit verhindert oder verzögert werden.
Die eigene Prozess-Identifikationsnummer (PID) wird durch
Identifikationsnummer (TID) per pid_t gettid(void).
pid_t getpid(void)
abgefragt, die Thread-
4.1.3. Tasks parametrieren
Neu erzeugte Tasks erben die Echtzeiteigenschaften der Elterntasks. Um diese anzupassen stehen eine Reihe
von Funktionen zur Verfügung.
Per int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param) wird für den
Job mit der Priorität pid das Schedulingverfahren policy für die in der Datenstruktur param definierte
Prioritätsebene eingestellt. Ist der Parameter pid 0, gelten die Einstellungen für den aufrufenden Prozess.
Als Schedulingverfahren stehen die folgenden zur Verfügung:
•
•
•
SCHED_RR: Round Robin
SCHED_FIFO: First Come First Serve
SCHED_OTHER: Default Verfahren für Jobs ohne (Echtzeit-) Priorität
Die gerade aktiven Schedulingparameter werden durch Aufruf von int sched_getparam(pid_t pid, struct
sched_param *param) ausgelesen. pid referenziert wieder den Job, an der mit param übergebenen Speicheradresse legt die Funktion später die Parameter (Priorität, Prioritätsbereich etc.) ab.
Beispiel 4-3. Threads parametrieren
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<sched.h>
<time.h>
#define PRIORITY_OF_THIS_TASK
char *Policies[] = {
"SCHED_OTHER",
"SCHED_FIFO",
"SCHED_RR"
};
66
15
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
static void print_scheduling_parameter()
{
struct timespec rr_time;
printf("Priority-Range SCHED_FF: %d - %d\n",
sched_get_priority_min(SCHED_FIFO),
sched_get_priority_max( SCHED_FIFO ) );
printf("Priority-Range SCHED_RR: %d - %d\n",
sched_get_priority_min(SCHED_RR),
sched_get_priority_max( SCHED_RR));
printf("Current Scheduling Policy: %s\n",
Policies[sched_getscheduler(0)] );
sched_rr_get_interval( 0, &rr_time );
printf("Intervall for Policy RR: %ld [s] %ld [nanosec]\n",
rr_time.tv_sec, rr_time.tv_nsec );
}
int main( int argc, char **argv )
{
struct sched_param scheduling_parameter;
struct timespec t;
int i,j,k,p;
print_scheduling_parameter();
sched_getparam( 0, &scheduling_parameter );
printf("Priority: %d\n", scheduling_parameter.sched_priority );
// only superuser:
// change scheduling policy and priority to realtime priority
scheduling_parameter.sched_priority = PRIORITY_OF_THIS_TASK;
if( sched_setscheduler( 0, SCHED_RR, &scheduling_parameter )!= 0 ) {
perror( "Set Scheduling Priority" );
exit( -1 );
}
sched_getparam( 0, &scheduling_parameter );
printf("Priority: %d\n", scheduling_parameter.sched_priority );
t.tv_sec = 10;
// sleep
t.tv_nsec = 1000000;
nanosleep( &t, NULL );
print_scheduling_parameter();
return 0;
}
Multicore-Architekturen bieten durch die CPU-Isolation und -Affinität interessante Möglichkeiten,
Anforderungen an das Zeitverhalten zu erfüllen. Dazu werden einzelne Prozessor-Kerne für
Echtzeitaufgaben reserviert und kritische Jobs auf diese Kerne fixiert. Zentral hierfür ist die Datenstruktur
cpu_set_t, die die Prozessorkerne repräsentiert. Jerder Kern cpu ist dabei durch ein einzelnes Bit
dargestellt. Auf diese Datenstruktur sind eine Reihe von Funktionen (Methoden) anwendbar, die das
Bitfeld set löschen (void CPU_ZERO(cpu_set_t *set)) und einzelne Bits setzen (void CPU_SET(int cpu,
cpu_set_t *set)) beziehungsweise rücksetzen (void CPU_CLR(int cpu, cpu_set_t *set)). Per
int
CPU_ISSET(int cpu, cpu_set_t *set) wird überprüft, ob die CPU cpu im Set set gesetzt ist oder nicht.
Um einen Job auf spezifische CPU-Kerne zu fixieren verwenden Sie die Funktion int
Beispiel
Erzwungene Prozessmigration zeigt die programmtechnische Anwendung. Die Vorbereitung des Systems
finden Sie in Abschnitt Mehrkernmaschine beschrieben.
sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask).
67
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
Beispiel 4-4. Erzwungene Prozessmigration
#include <stdio.h>
#include <stdlib.h>
#define __USE_GNU
#include <sched.h>
#include <errno.h>
int main( int argc, char **argv )
{
cpu_set_t mask;
unsigned int cpuid, pid;
if( argc != 3 ) {
fprintf(stderr,"usage: %s pid cpuid\n", argv[0]);
return -1;
}
pid = strtoul( argv[1], NULL, 0 );
cpuid = strtoul( argv[2], NULL, 0 );
CPU_CLR( cpuid, &mask ); // Aktuelle CPU verbieten.
if( sched_setaffinity( pid, sizeof(mask), &mask ) ) {
perror("sched_setaffinity"); // UP-System???
return -2;
}
return 0;
}
// vim: aw ic ts=4 sw=4:
4.2. Schutz kritischer Abschnitte
Zur Lösung einer Echtzeitaufgabe werden im Normalfall mehrere Threads oder Prozesse verwendet, die
(quasi-)parallel arbeiten. Man unterscheidet dabei
•
•
diskunkte Prozesse, bei denen der Ablauf eines Prozesses (respektive Threads) unabhängig von den anderen Prozessen, zu denen er disjunkt ist,
nicht disjunkte Prozesse, die auf gemeinsamen Variablen arbeiten. Diese lassen sich wiederum unterscheiden in:
• Konkurrierende Prozesse, die sich um den Zugriff auf Daten konkurrieren und
• Kooperierende Prozesse (meist verkettet), bei denen der eine Prozess Daten für den anderen Prozess
liefert (Hersteller/Verbrauchermodell).
Die Wirkung der gegenseitigen Beeinflussung nicht disjunkter paralleler Prozesse ist ohne Synchronisation
nicht vorhersagbar und im Regelfall nicht reproduzierbar.
Der Programmteil, in dem auf gemeinsame Betriebsmittel (z.B. Daten) zugegriffen wird, heißt kritischer
Abschnitt (critical section). Greifen zwei oder mehr Codesequenzen (Threads, Prozesse oder auch
Interrupt-Service-Routinen) auf dieselben Daten zu, kann es zu einer sogenannten race condition kommen.
Bei einer race condition hängt das Ergebnis des Zugriffs vom Prozessfortschritt ab (siehe hierzu auch
http://forum.mikrokopter.de/topic-2416.html).
68
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
4.2.1. Semaphor und Mutex
Race conditions lassen sich vermeiden, indem nie mehr als ein Prozess in einen kritischen Abschnitt eintritt
(mutual exclusion, gegenseitiger Ausschluß). Dieses läßt sich mithilfe von Semaphoren sicherstellen.
Ein Semaphor wird zur Synchronisation, inbesondere bei gegenseitigem Ausschluß verwendet. Es handelt
sich dabei um ein Integer Variable, die wie folgt verwendet wird:
•
•
•
Der Wert des Semaphors wird auf einen Maximalwert N initialisiert.
Bei einem Zugriff auf das Semaphor (P-Operation, das P stammt aus dem holländischen und steht für
passeeren) wird
• dessen Wert um 1 erniedrigt und
• der Prozess schlafend gelegt, wenn der neue Semaphor-Wert negativ ist.
Bei der Freigabe eines Semaphors (V-Operation, V leitet sich vom holländischen vrijgeven ab) wird
• dessen Wert um 1 erhöht und
• falls der neue Semaphor-Wert negativ oder gleich Null ist, ein auf das Semaphor wartender
(schlafender) Prozess aufgeweckt.
P-Operation
s = s - 1;
if (s < 0) {
sleep_until_semaphore_is_free();
}
V-Operation
s = s + 1;
if (s <= 0) {
wake_up_sleeping_process_with_highest_priority();
}
P- und V-Operationen sind selbst kritische Abschnitte, deren Ausführung nicht unterbrochen werden darf.
Daher sind Semaphor-Operationen im Betriebssystem als System-Calls verankert, und während ihrer Ausführung (im Kernel) sind Interrupts gesperrt.
Bei einfachen Mikroprozessoren bzw. Laufzeitsystemen werden Semaphoroperationen ansonsten durch
Test-And-Set-Befehle des Mikroprozessors realisiert.
Ein Mutex ist ein Semaphor mit Maximalwert N=1 (binärer Semaphor).
POSIX-Funktionen für Mutexe:
int pthread_mutex_destroy pthread_mutex_t * mutex
Über diese Funktion wird ein Mutex (Semaphor, das genau einem Thread den Zugriff auf den kritischen Abschnitt ermöglicht) gelöscht und die damit zusammenhängenden Ressourcen freigegeben. Ein
Mutex wird nur entfernt, wenn es „frei“ (unlocked) ist.
69
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
int pthread_mutex_lock
pthread_mutex_t * mutex
Über diese Funktion wird ein kritischer Abschnitt betreten. Sollte der kritische Abschnitt frei sein, wird
das Mutex gesperrt (locked) und dem aufrufenden Thread zugehörig erklärt. In diesem Falle returniert
die Funktion direkt. Sollte der kritische Abschnitt gesperrt sein, wird der aufrufende Thread solange
in den Zustand „schlafend“ versetzt, bis das Mutex wieder freigegeben wird.
Ist das Mutex durch denselben Thread gesperrt, der den Aufruf durchführt, kann es zu einem Deadlock
kommen. Um dies zu verhindern läßt sich ein Mutex so konfigurieren, dass es entweder in diesem Fall
den Fehlercode EDEADLK returniert (ein „error checking mutex“ oder den Mehrfachzugriff erlaubt (ein
„recursive mutex“).
int pthread_mutex_trylock pthread_mutex_t * mutex
Diese Funktion verhält sich genauso wie die Funktion pthread_mutex_lock mit der Ausnahme, dass sie
nicht blockiert, falls der kritische Abschnitt nicht betreten werden kann. In diesem Fall returniert die
Funktion EBUSY.
int pthread_mutex_init pthread_mutex_t * mutex
const pthread_mutexattr_t * mutexattr
Diese Funktion initalisiert ein Mutex mutex gemäß der Angaben im Parameter mutexattr. Ist mutexattr
NULL werden default Werte zur Initialisierung verwendet.
int pthread_mutex_unlock pthread_mutex_t * mutex
Über diese Funktion wird ein kritischer Abschnitt wieder verlassen.
Beispiel 4-5. Programmbeispiel Mutex
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
static pthread_mutex_t mutex;
int main( int argc, char **argv, char **envp )
{
pthread_mutex_init( &mutex, NULL );
pthread_mutex_lock( &mutex ); // enter critical section
printf("%d in critical section...\n", getpid());
pthread_mutex_unlock( &mutex ); // leave critical section
pthread_mutex_destroy( &mutex );
return 0;
}
Ein Semaphor kann zur kooperativen Synchronisation zwischen Rechenprozessen verwendet werden,
indem die zusammengehörigen P- und V-Operationen aufgesplittet werden (siehe auch den Abschnitt
Condition-Variable (Event)).
70
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
Task
C
B P(X)
A
t1
t1
t2
t3
t4
t5
P(X)
t2
t3
t4
t5
Task A belegt die Ressource X
Task B (mit höherer Priorität)wird rechenbereit
Task C (mit der höchsten Priorität) wird rechenbereit
Task C versucht die Ressource X zu belegen und wird schlafen gelegt
Task B hat von den rechenbereiten Prozessen die höchste Priorität
Abbildung 4-1. Prioritäten und Synchronisation
4.2.2. Prioritätsinversion
Unter Prioritätsinversion versteht man die Situation, dass eine niedrigpriore Task eine Ressource alloziert
hat (über ein Semaphor), die von einer hochprioren Task benötigt wird. Dadurch wird die Bearbeitung
der hochprioren Task solange verzögert, bis die niedrigpriore Task die Ressource freigibt. Das kann aber
unzumutbar lang dauern, wenn im System eine Reihe Tasks lauffähig sind, die mittlere Priorität haben
(siehe Abbildung Prioritäten und Synchronisation).
Task
C
B P(X)
A
t1
t1
t2
t3
t4
t4
t5
P(X)
V(X)
V(X)
t2
t3
t4
t5
Task A belegt die Ressource X
Task B (mit höherer Priorität)wird rechenbereit
Task C (mit der höchsten Priorität) wird rechenbereit
Task C versucht die Ressource X zu belegen und wird schlafen gelegt
Task A erbt die Priorität von Task C und wird rechenbereit
Task A gibt X wieder frei und bekommt die ursprüngliche Priorität
Abbildung 4-2. Prioritätsinversion
Das angesprochene Problem wird über die Methode der Prioritätsvererbung (Priority Inheritance) gelöst.
Es gibt unterschiedliche Protokolle zur Prioritätsvererbung, zum Beispiel das
1. Priority Inheritance Protocol (PIP) oder das
2. Priority Ceiling Protocol (PCP).
71
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
Priority Inheritance Protocol. Falls ein Job JR ein Mutex (oder auch ein Semaphor) anfordert, das gegenwärtig von einem Job JO mit niedrigerer Priorität gehalten wird, dann wird die Priorität von JO auf die
Priorität von Job JR angehoben. Sobald der Job JO das Mutex wieder freigibt, bekommt er seine initiale
Priorität zurück.
Die Prioritätsvererbung ist transitiv. Falls der Job JO bereits die Priorität von JR geerbt hat und ein weiterer
Job JS, dessen Priorität größer als die von JR ist, ankommt, erbt JO dir Priorität von JS.
Gibt JO die Ressource wieder frei, erhält JO seine ursprüngliche Priorität zurück. Die Ressource selbst wird
dem anfordernden Prozess mit der höchsten Priorität zugeteilt (im Beispiel also JS).
Mit Hilfe dieses Protokolls lassen sich sehr viele Probleme lösen, insbesondere wenn die Jobs nur um eine
Ressource konkurrieren. Es gilt allerdings zu beachten, dass die Transitivität des Protokolls nicht bei allen
Implementierungen gesichert ist, die Implementierungen können sich also bezüglich ihres Verhaltens im
Detail unterscheiden!
Priority Ceiling Protocol. Das Priority Ceiling Protocol ähnelt dem Priority Inheritance Protocol insofern,
dass auch hier der blockierte Job dem Job, der die Ressource belegt hat, die Priorität vererbt. Unterschiede
aber gibt es bei der Freigabe des Mutex. In diesem Fall bekommt der freigebende Job nicht seine ursprüngliche Priorität zurück, sondern erhält die höchste Priorität, die ein anderer Job besitzt, mit dem er
ebenfalls ein anderes Mutex teilt.
Damit das Priority Ceiling Protocol angewendet werden kann, muss also bereits vor dem Start bekannt sein,
welche Ressourcen die Jobs benötigen.
Das Priority Ceiling Protocol ist unter Umständen in der Lage, Deadlock-Situation zu verhindern. Gesichert
ist dies allerdings nicht.
Beispiel Prioritätsinversion aktivieren zeigt, wie bei der Initialisierung des Mutex Prioritätsinversion
eingeschaltet wird. Allerdings ist diese nur dann wirksam, wenn der Rechenprozess eine Echtzeitpriorität
besitzt, also der Schedulingklasse rt_sched_class zugeordnet ist (siehe Abschnitt Tasks parametrieren).
Beispiel 4-6. Prioritätsinversion aktivieren
#include <stdio.h>
#include <unistd.h>
#define __USE_UNIX98 /* Needed for PTHREAD_PRIO_INHERIT */
#include <pthread.h>
static pthread_mutex_t mutex;
static pthread_mutexattr_t attr;
int main( int argc, char **argv, char **envp )
{
// don’t forget: rt-priority is needed for priority inheritance
pthread_mutexattr_init( &attr );
pthread_mutexattr_setprotocol( &attr, PTHREAD_PRIO_INHERIT );
pthread_mutex_init( &mutex, &attr );
pthread_mutex_lock( &mutex ); // enter critical section
printf("%d in critical section...\n", getpid());
pthread_mutex_unlock( &mutex ); // leave critical section
pthread_mutex_destroy( &mutex );
return 0;
}
72
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
4.2.3. Deadlock
Task A
Task B
P(S1)
P(S2)
P(S2)
P(S1)
Deadlock
S1 ist durch Task A
blockiert, S2 durch
Task B
V(S2)
V(S1)
V(S1)
V(S2)
Abbildung 4-3. Deadlock
Durch critical sections (gegenseitigem Ausschluß) kann es leicht zu Verklemmungen, den sogenannten
Deadlocks kommen. Muss eine Task beispielsweise zwei Datenstrukturen manipulieren, die durch zwei unabhängige Semaphore geschützt sind, und eine zweite Task muss das gleiche tun, nur dass sie die Semaphore
in umgekehrter Reihenfolge allokiert, gibt es eine Verklemmung (Deadlock).
In der Praxis kommen derartige Konstellationen häufiger vor. Dabei sind sie nur sehr schwer zu entdecken,
da das Nehmen und Freigeben des Semaphors nicht selten in Funktionen gekapselt sind.
Deadlocks lassen sich durch zwei Maßnahmen vermeiden:
1. entweder durch geeignete Systemauslegung und Programmierung oder dadurch, dass
2. nur dann Anforderungen an Betriebsmittel befriedigt werden, wenn sichergestellt ist, dass durch die
Anforderung keine Deadlock-Situation entstehen kann.
Zur Überprüfung, ob durch eine spezifische Anforderung eine Deadlock-Situation entstehen kann oder
nicht, haben die Informatiker den Bankers-Algorithmus entwickelt. Der Einsatz desselben führt allerdings zu einem völlig verzehrten Zeitverhalten und soll hier daher nicht weiter betrachtet werden. Beim
Software-Entwurf können Deadlocks unter Umständen durch die Modellierung und Analyse des zugehörigen Petrinetzes entdeckt werden.
Die Erkennung, Vermeidung und Behandlung von Deadlocks gehört zu den schwierigsten Aufgaben bei der
Programmierung moderner Anwendungen!
4.2.4. Schreib-/Lese-Locks
Wie hier vorgestellt wird ein Job blockiert, sobald er auf einen kritischen Abschnitt zugreifen möchte, der
bereits durch einen anderen Job belegt ist.
Falls der kritische Abschnitt aus dem Zugriff auf globale Daten besteht, kommt es nur dann zu einer RaceCondition, falls die zugreifenden Rechenprozesse die Daten modifizieren. Das parallele Lesen ist jedoch
73
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
unkritisch. Aus dieser Erkenntnis heraus bieten viele Systeme sogenannte Schreib-/Lese-Locks an. Das
sind Semaphore, die parallele Lesezugriffe erlauben, aber einem schreibenden Job einen exklusiven Zugriff
ermöglichen.
Bei der Anforderung des Mutex muss der Rechenprozess mitteilen, ob er den kritischen Abschnitt nur zum
Lesen, oder auch zum Schreiben betreten möchte. Folgende Fälle werden unterschieden:
1. Der kritische Abschnitt ist frei:
Der Zugriff wird gewährt.
2. Der kritische Abschnitt ist von mindestens einem Rechenprozess belegt, der lesend zugreift und es gibt
zur Zeit keinen Rechenprozess, der schreibend zugreifen möchte:
Ein lesend wollender Prozess bekommt Zugriff.
Ein Prozess, der schreiben möchte, wird blockiert.
3. Der kritische Abschnitt ist von mindestens einem Rechenprozess belegt, der lesend zugreift und es gibt
zur Zeit mindestens einen Rechenprozess, der schreibend zugreifen möchte:
Der anfragende Rechenprozess wird blockiert (unabhängig davon, ob er lesen oder schreiben möchte).
4. Der kritische Abschnitt ist von einem Prozess belegt, der schreibend zugreift:
Der anfragende Rechenprozess wird blockiert (unabhängig davon, ob er lesen oder schreiben möchte).
Auch einen lesen wollenden Prozess zu blockieren, sobald ein Rechenprozess zum Schreiben den kritischen Abschnitt betreten möchte ist notwendig, damit es nicht zum „sterben“ des schreiben wollenden Jobs
kommt.
4.2.5. Weitere Schutzmaßnahmen für kritische Abschnitte
Ein Semaphor läßt sich dann zum Schutz kritischer Abschnitte einsetzen, wenn der Abschnitt durch zwei
oder mehr Applikationen (User-Prozesse) betreten werden soll. Kritische Abschnitte aber, die beispielsweise innerhalb des Betriebssystemkerns, eines Treibers oder in Interrupt-Service-Routinen vorkommen,
können damit nicht gesichert werden. Hier werden andere Techniken benötigt: Unterbrechungssperren und
Spinlocks.
Unterbrechungssperre. Hierbei werden die Interrupts auf einem System für die Zeit des Zugriffs auf den
kritischen Abschnitt gesperrt. Um die Latenzzeiten kurz zu halten, darf der Zugriff selbst nur kurz dauern.
Bei modernen Betriebssystemen ist diese Methode nur für Zugriffe innerhalb des Betriebssystemkerns, also
beispielsweise bei der Realisierung von Treibern, interessant, denn nur hier lassen sich Interrupts sperren.
Spinlocks. Da bei Multiprozessorsysteme (SMP=Symmetric Multiprocessing) zwei oder mehr Interruptserviceroutinen real parallel bearbeitet werden können, hilft eine lokale Unterbrechungssperre nicht weiter.
Hier arbeitet man im Regelfall mit sogenannten Spinlocks.
Bei Spinlocks entscheidet - wie schon beim Semaphor - eine Variable darüber, ob ein kritischer Abschnitt
betreten werden darf oder nicht. Ist der kritische Abschnitt bereits besetzt, wartet die zugreifende Einheit
aktiv solange, bis der kritische Abschnitt wieder freigegeben worden ist.
Damit ergeben sich für die vorgestellten drei Methoden zum gegenseitigen Ausschluß bei einem kritischen
Abschnitt unterschiedliche Einsatzfelder. Die Unterbrechungssperre steht normalen Applikationen nicht
zur Verfügung. Bei Einprozessorsystemen (UP=Uni-Processor) wird selbige im Betriebssystemkerns eingesetzt. Bei SMP lassen sich zwar die Interrupts auf allen Prozessoren sperren, bevor aber auf den kritischen
Abschnitt zugegriffen werden kann ist sicherzustellen, dass nicht zufällig zwei Prozessoren die ISR bearbeiten.
74
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
Spinlocks, die ein aktives Warten realisieren, lassen
Mehrprozessormaschienen auch auf Kernelebene einsetzen.
sich
auf
Applikations-
und
auf
Das Semaphor schließlich ist für den gegenseitigen Ausschluß auf Applikationsebene gedacht. Es ist sowohl
für UP- als auch für SMP-Umgebungen verwendbar. Der Einsatz im Betriebssystemkern ist möglich, aber
nur dann, wenn der kritische Abschnitt nicht von einer ISR betreten werden soll. In diesem Fall müsste man
nämlich die ISR schlafen legen, was nicht möglich ist.
4.3. Programmtechnischer Umgang mit Zeiten
year = ORIGINYEAR; /* = 1980 */
while (days > 365) {
if (IsLeapYear(year)) {
if (days > 366) {
days -= 366;
year += 1;
}
} else {
days -= 365;
year += 1;
}
}
Abbildung 4-4. Weil der Programmierer des Treibers für die Echtzeituhr das Schaltjahr nicht richtig
programmierte, blieben am 31. Dezember 2008 viele MP3-Player der Firma Microsoft (Zune-Player)
hängen.
Betriebssysteme stellen Echtzeitapplikationen Zeitgeber mit unterschiedlichen Eigenschaften zur Verfügung, über die das Zeitverhalten kontrolliert wird. Diese sind gekennzeichnet durch ihre
•
•
•
•
•
Genauigkeit,
die Zuverlässigkeit,
den Bezugspunkt,
die Darstellung und
den maximalen Zeitbereich.
Das Betriebssystem repräsentiert Zeiten unter anderem mit den folgenden Datentypen (Darstellung):
• clock_t:
Timerticks.
Zeit in Mikrosekunden-Auflösung.
timespec: Zeit in Nanosekunden-Auflösung.
tm: absolute Zeitangabe.
• struct timeval:
• struct
• struct
struct timeval {
time_t
tv_sec;
suseconds_t tv_usec;
};
/* seconds */
/* microseconds */
struct timespec {
time_t
tv_sec;
long
tv_nsec;
};
/* seconds */
/* nanoseconds */
struct tm {
int tm_sec;
/* seconds */
75
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
int
int
int
int
int
int
int
int
tm_min;
tm_hour;
tm_mday;
tm_mon;
tm_year;
tm_wday;
tm_yday;
tm_isdst;
/*
/*
/*
/*
/*
/*
/*
/*
minutes */
hours */
day of the month */
month */
year */
day of the week */
day in the year */
daylight saving time */
};
Die Strukturen struct timeval und struct timespec bestehen aus jeweils zwei Variablen, die einmal
den Sekundenanteil und einmal den Mikro- beziehungsweise den Nanosekundenanteil repräsentieren.
Die Darstellung erfolgt jeweils normiert. Das bedeutet, dass der Mikro- oder Nanosekundenanteil
immer kleiner als eine volle Sekunde bleibt. Ein Zeitstempel von beispielsweise 1 Sekunde, 123456
Mikrosekunden ist gültig, 1 Sekunde, 1234567 Mikrosekunden ist ungültig. In normierter Darstellung
ergäbe sich 2 Sekunden, 234567 Mikrosekunden.
Die Darstellungs- beziehungsweise Repräsentationsform reflektiert auch den darstellbaren Wertebereich.
Da bei den dargestellten Datenstrukturen für den Typ time_t ein long eingesetzt wird, lassen sich auf einer
32-Bit Maschine rund 4 Milliarden Sekunden zählen, auf einer 64-Bit Maschine 264 (mehr als 500 Milliarden
Jahre).
Als Bezugspunkte haben sich die folgenden eingebürgert:
•
•
•
Start des Systems,
Start eines Jobs und
Start einer Epoche, beispielsweise "Christi Geburt" oder der 1.1.1970 (Unix-Epoche). Dieser
Bezugspunkt weist zudem noch eine örtliche Komponente auf: Der Zeitpunkt 19:00 Uhr in Europa
entspricht beispielsweise einem anderen Zeitpunkt in den USA (minus sechs Stunden zur Ostküste).
Die Genauigkeit wird beeinflußt durch, die Taktung des Zeitgebers, deren Schwankungen und durch Zeitsprünge.
Das Attribut Zuverlässigkeit eines Zeitgebers beschreibt dessen Verhalten bei (bewußten) Schwankungen
der Taktung und bei Zeitsprüngen: Ein Zeitgeber kann beispielsweise der Systemuhr folgen
(CLOCK_REALTIME) oder unabhängig von jeglicher Modifikation an der Systemzeit einfach
weiterzählen (CLOCK_MONOTONIC). Die Posix-Realzeiterweiterung beziehungsweise Linux-Systeme
definieren hierzu folgende Clocks [Manpage: clock_gettime]:
CLOCK_REALTIME. Dieser Zeitgeber repräsentiert die systemweite, aktuelle Zeit. Er reagiert auf Zeitsprünge, sowohl vorwärts als auch rückwärts, die beispielsweise beim Aufwachen (Resume) nach einem
Suspend (Schlafzustand des gesamten Systems) ausgelöst werden. Er reagiert ebenfalls auf unterschiedliche
Taktungen, die beispielsweise durch NTP erfolgen. Dieser Zeitgeber liefert die Sekunden und Nanosekunden seit dem 1.1. 1970 UTC (Unixzeit) zurück.
CLOCK_MONOTONIC. Dieser Zeitgeber läuft entsprechend seiner Auflösung stets vorwärts, ohne dabei
Zeitsprünge zu vollziehen. Er ist also unabhängig von der mit Superuserprivilegien zu verändernden Systemuhr. Allerdings reagiert dieser Zeitgeber auf Modifikationen der Taktung, die beispielsweise durch NTP
erfolgen.
CLOCK_MONOTONIC_RAW. Dieser Zeitgeber ist linuxspezifisch. Er reagiert weder auf Zeitsprünge
noch auf in Betrieb geänderte Taktungen (NTP).
CLOCK_PROCESS_CPUTIME_ID. Dieser Zeitgeber erfasst die Verarbeitungszeit (Execution Time) des
zugehörigen Prozesses. Das funktioniert aber nur zuverlässig auf Single-Core-Systemen beziehungsweise
wenn sichergestellt werden kann, dass keine Prozessmigration stattfindet.
76
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
CLOCK_THREAD_COMPUTE_ID. Dieser Zeitgeber erfasst die Verarbeitungszeit (Execution Time)
des zugehörigen Threads. Das funktioniert aber nur zuverlässig auf Single-Core-Systemen beziehungsweise
wenn sichergestellt werden kann, dass keine Prozessmigration stattfindet.
Der maximale Zeitbereich schließlich ergibt sich durch die Auflösung des Zeitgebers und die Bitbreite der
Variablen:
zeitbereich = auflösung * 2bitbreite
4.3.1. Zeit lesen
Es gibt unterschiedliche Systemfunktionen, mit denen die aktuelle Zeit gelesen werden kann. Favorisiert ist
die Funktion int clock_gettime(clockid_t clk_id, struct timespec *tp), die die Zeit seit dem 1.1.1970
(Unixzeit) als Universal Time (Zeitzone UTC) zurückliefert (struct timespec). Konnte die aktuelle Zeit
gelesen werden, gibt die Funktion Null, ansonsten einen Fehlercode zurück. Allerdings ist das Auslesen auf
32-Bit Systemen in so fern problematisch, da der 32-Bit Zähler am 19. Januar 2038 überläuft. Vor allem
eingebettete Systeme, bei denen mit einer jahrzentelangen Standzeit geplant wird, könnten von diesem
Zählerüberlauf betroffen sein. Wichtig ist, dass Sie in diesem Fall entsprechend programmiert sind (siehe
Der Zeitvergleich).
struct timepspec timestamp;
...
if (clock_gettime(CLOCK_MONOTONIC,&timestamp)) {
perror("timestamp");
return -1;
}
printf("seconds: %ld, nanoseconds: %ld\n",
timestamp.tv_sec, timestamp.tv_nsec);
Durch die Wahl der Clock CLOCK_PROCESS_CPUTIME_ID beziehungsweise
auch die Verarbeitungszeit ausgemessen werden (Profiling).
CLOCK_THREAD_CPUTIME_ID
Die Genauigkeit der zurückgelieferten Zeit kann mit Hilfe der Funktion clock_getres(clockid_t
ausgelesen werden.
kann
clk_id,
struct timespec *res)
Die Funktion clock_gettime() ist nicht in der Standard-C-Bibliothek zu finden, sondern in der
Realzeit-Bibliothek librt. Daher ist bei der Programmgenerierung diese Bibliothek hinzuzulinken
(Parameter -lrt). Steht nur die Standard-C-Bibliothek zur Verfügung, kann time_t time(time_t *t) oder
auch int gettimeofday(struct timeval *tv, struct timezone *tz) eingesetzt werden.
time()
gibt die Sekunden zurück, die seit dem 1.1.1970 (UTC) vergangen sind.
time_t now;
...
now = time(NULL);
schreibt an die per tv übergebene Speicheradresse die Sekunden und Mikrosekunden seit
dem 1.1.1970. Das Argument tz wird typischerweise mit NULL angegeben.
gettimeofday()
Liegen die Sekunden seit dem 1.1.1970 vor (timestamp.tv_sec), können diese mit Hilfe der Funktionen
oder struct tm *gmtime_r(const
time_t *timep, struct tm *result); in die Struktur struct tm konvertiert werden.
struct tm *localtime_r(const time_t *timep, struct tm *result);
struct tm absolute_time;
if (localtime_r( timestamp.tv_sec, &absolute_time )==NULL) {
perror( "localtime_r" );
return -1;
}
printf("year: %d\n", absolute_time.tm_year);
77
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
Die Funktion time_t mktime(struct tm
Sekunden seit dem 1.1.1970 (time_t).
*tm)
konvertiert eine über die Struktur struct
tm
gegebene Zeit in
Mit Hilfe der Funktion clock_t times(struct tms *buf) lässt sich sowohl die aktuelle Zeit zu einem
letztlich nicht genau definierten Bezugspunkt, als auch die Verarbeitungszeit (Execution-Time) des
aufrufenden Prozesses bestimmen. Die Zeiten werden als Timerticks (clock_t) zurückgeliefert. Die
zurückgelieferte Verarbeitungszeit ist aufgeschlüsselt in die Anteile, die im Userland und die Anteile, die
im Kernel verbraucht wurden. Außerdem werden diese Anteile auch für Kindprozesse gelistet.
#include
#include
#include
#include
<stdio.h>
<sys/times.h>
<unistd.h>
<time.h>
int main( int argc, char **argv, char **envp )
{
struct tms exec_time;
clock_t act_time;
long ticks_per_second;
long tickduration_in_ms;
ticks_per_second = sysconf(_SC_CLK_TCK);
tickduration_in_ms = 1000/ticks_per_second;
act_time = times( &exec_time );
printf("actual time (in ms): %ld\n", act_time*tickduration_in_ms);
printf("execution time (in ms): %ld\n",
(exec_time.tms_utime+exec_time.tms_stime)*tickduration_in_ms);
return 0;
}
Sehr genaue Zeiten lassen sich erfassen, falls der eingesetzte Prozessor einen Zähler besitzt, der mit der
Taktfrequenz des Systems getaktet wird. Bei einer x86-Architektur (PC) heißt dieser Zähler Time Stamp
Counter (TSC). Der TSC kann auch von einer normalen Applikation ausgelesen werden, allerdings muss
sichergestellt sein, dass sich die Taktfrequenz zwischen zwei Messungen ändert. Alternativ kann man sich
vom Betriebssystem über die Taktänderung informieren lassen.
4.3.2. Der Zeitvergleich
Zwei Absolutzeiten (struct tm) werden am einfachsten über deren Repräsentation in Sekunden
verglichen. Die Umwandlung erfolgt über die Funktion (time_t mktime(struct tm *tm)). Allerdings ist
dabei zu beachten, dass es auf einem 32-Bit System am 19. Januar 2038 zu einem Überlauf kommt. Wird
einer der beiden Zeitstempel vor dem 19. Januar 2038 genommen, der andere danach, kommt es zu einem
falschen Ergebnis, wenn nur die beiden Werte per "<" beziehungsweise ">" verglichen werden.
Das ist ein generelles Problem und kann dann gelöst werden, wenn sichergestellt ist, dass die zu vergleichenden Zeiten nicht weiter als die Hälfte des gesamten Zeitbereiches auseinanderliegen. In diesem Fall
lassen sich die Makros einsetzen, die im Linux-Kernel für den Vergleich zweier Zeiten eingesetzt werden.
Das Makro time_after(a,b) liefert true zurück, falls es sich bei a um eine spätere Zeit als b handelt. Das
Makro time_after_eq(a,b) liefert true zurück, falls es sich bei a um eine spätere Zeit oder um die gleiche
Zeit handelt, wie b handelt. Die Zeitstempel a und b müssen beide vom Typ unsigned long sein. Natürlich
können die Makros auch auf andere Datentypen angepasst werden [HEADERDATEI linux/jiffies.h].
#define time_after(a,b)
\
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(b) - (long)(a) < 0))
#define time_before(a,b)
time_after(b,a)
78
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
#define time_after_eq(a,b)
\
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(a) - (long)(b) >= 0))
#define time_before_eq(a,b)
time_after_eq(b,a)
4.3.3. Differenzzeitmessung
In Echtzeitapplikationen ist es häufig notwendig eine Zeitdauer zu messen, Zeitpunkte zu erfassen oder eine
definierte Zeit verstreichen zu lassen. Dabei sind folgende Aspekte zu beachten:
•
•
•
•
•
Die Genauigkeit der eingesetzten Zeitgeber,
die maximalen Zeitdifferenzen,
Schwankungen der Zeitbasis, beispielsweise durch Schlafzustände,
Modifikationen an der Zeitbasis des eingesetzten Rechnersystems (Zeitsprünge) und
die Ortsabhängigkeit absoluter Zeitpunkte.
Zur Bestimmung einer Zeitdauer verwendet man häufig eine Differenzzeitmessung. Dabei wird vor und
nach der auszumessenden Aktion jeweils ein Zeitstempel genommen. Die Zeitdauer ergibt sich aus der
Differenz dieser beiden Zeitstempel.
Dies ist allerdings nicht immer ganz einfach. Liegt der Zeitstempel beispielsweise in Form der Datenstruktur struct timeval (als Ergebnis der Funktion gettimeofday()) vor, müssen die Sekunden zunächst
getrennt von den Mikrosekunden subtrahiert werden. Ist der Mikrosekundenanteil negativ, muss der Sekundenanteil um eins erniedrigt und der Mikrosekundenanteil korrigiert werden. Dazu wird auf die Anzahl der
Mikrosekunden pro Sekunde (also eine Million) der negative Mikrosekundenanteil addiert.
#define MICROSECONDS_PER_SECOND 1000000
struct timespec * diff_time( struct timeval before, struct timeval after,
struct timeval *result )
{
if (result==NULL)
return NULL;
result->tv_sec = after.tv_sec - before.tv_sec;
result->tv_usec= after.tv_usec- before.tv_usec;
if (result->tv_usec<0) {
result->tv_sec--;
/* result->tv_usec is negative, therefore we use "+" */
result->tv_usec = MICROSECONDS_PER_SECOND+result->tv_usec;
}
return result;
}
Für Zeitstempel vom Typ struct timeval gilt entsprechendes. Anstelle der MICROSECONDS_PER_SECOND sind
einzusetzen. Sind die Zeitstempel vorzeichenlos, sieht die Rechnung für den
Sekundenanteil etwas komplizierter aus. Das soll hier aber nicht weiter vertieft werden.
NANOSECONDS_PER_SECOND
Etwas einfacher ist die Differenzbildung, wenn aus der Datenstruktur eine einzelne Variable mit der gewünschten Auflösung, beispielsweise Mikrosekunden, generiert wird. Im Fall von struct timeval wird dazu
der Sekundenanteil mit einer Million multipliziert und der Mikrosekundenanteil aufaddiert. Bei der Multiplikation können natürlich Informationen verloren gehen, allerdings geht der gleiche Informationsgehalt
auch beim zweiten Zeitstempel verloren: Für die Differenzbildung ist das damit nicht relevant, solange der
zu messende zeitliche Abstand kleiner als 1000 Sekunden ist und es während der Messung keinen Überlauf
beim Sekundenanteil gibt.
time_in_usec=((nachher.tv_sec*1000000)+nachher.tv_usec)-
79
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
((vorher.tv_sec*1000000)+vorher.tv_usec);
4.3.4. Schlafen
Threads legen sich für eine Zeitspanne oder aber bis zu einem Punkt, an dem sie aufgeweckt werden,
schlafen (absolut oder relativ schlafen). Innerhalb von Echtzeitapplikationen kann zusätzlich die Zeitquelle
(CLOCK_MONOTONIC oder CLOCK_REALTIME) eingestellt werden.
Ob relativ oder absolut geschlafen wird, hat durchaus Relevanz: Wird das Schlafen unterbrochen und danach
mit der Restzeit neu aufgesetzt, kommt es durch den zusätzlichen Aufruf zu einer Verzögerung. Bei der
Verwendung einer absoulten Weckzeit fallen zusätzliche Aufrufe nicht ins Gewicht.
Ein Thread kann sich durch Aufruf der Funktion int nanosleep(const struct timespec *req, struct
timespec *rem); für die über req definierte Zeitspanne schlafen legen. Hierbei wird zwar offiziell die
Zeitquelle CLOCK_REALTIME verwendet, eine Änderung der Schlafenszeit wird aber nachgeführt. Defacto
basiert nanosleep() daher auf CLOCK_MONOTONIC. Der schlafende Thread wird aufgeweckt, wenn entweder
die angegebene Relativzeit abgelaufen ist, oder der Thread ein Signal gesendet bekommen hat. Ist letzteres
der Fall gewesen und hat der Aufrufer den Parameter rem mit einer gültigen Hauptspeicheradresse versehen,
legt das Betriebssystem in diesem Speicher die noch übrig gebliebene Schlafenszeit ab.
Genauer kann das Verhalten beim Schlafenlegen über die Funktion
int clock_nanosleep(clockid_t
eingestellt
werden. Über den Parameter clock_id wird die Zeitquelle (CLOCK_REALTIME, CLOCK_MONOTONIC) eingestellt.
Der Parameter flag erlaubt die Angabe, ob die Zeitangabe request relativ (flag==0) oder absolut
(flag==TIMER_ABSTIME) zu interpretieren ist. Um die Restzeit bei einem durch ein Signal provozierten
vorzeitigen Abbruch aufzunehmen, kann per remain eine Speicheradresse dafür übergeben werden.
clock_id, int flags, const struct timespec *request, struct timespec *remain);
Beispiel 4-7. Schlafenlegen einer Task
#include
#include
#include
#include
#include
<stdio.h>
<time.h>
<unistd.h>
<errno.h>
<signal.h>
void sigint_handler(int signum)
{
printf("SIGINT (%d)\n", signum);
}
int main( int argc, char **argv, char **envp )
{
struct timespec sleeptime;
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = sigint_handler;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror( "sigaction" );
return -1;
}
printf("%d going to sleep for 10 seconds...\n", getpid());
clock_gettime( CLOCK_MONOTONIC, &sleeptime );
sleeptime.tv_sec += 10;
sleeptime.tv_nsec += 0;
while (clock_nanosleep(CLOCK_MONOTONIC,TIMER_ABSTIME,
&sleeptime,NULL)==EINTR) {
printf("interrupted...\n");
}
80
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
printf("woke up...\n");
return 0;
}
Die Funktion clock_nanosleep() steht nur über die Realzeitbibliothek librt zur Verfügung. Beim Linken
ist daher die Option -lrt mit anzugeben.
Neben nanosleep() finden sich in der Standard-C-Bibliothek noch weitere Funktionen, mit denen Threads
schlafen gelegt werden können: int usleep(useconds_t usec), unsigned int sleep(unsigned int
seconds) und int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct
timeval *timeout).
Die Funktion sleep() bekommt die Schlafenszeit als eine Anzahl von Sekunden übergeben, bei usleep()
sind es Mikrosekunden. select() schließlich ist eigentlich nicht primär dazu gedacht, Jobs schlafen zu
legen. Diese Eigenschaft wurde insbesondere früher genutzt, um eine portierbare Funktion zum Schlafenlegen bei einer Auflösung im Mikrosekundenbereich zu haben.
4.3.5. Weckrufe per Timer
Das Betriebssystem kann periodisch Funktionen, so genannte Timer aufrufen. Diese Funktionen werden
typischerweise als Signal Handler oder im Rahmen eines Threads aktiviert.
Dazu müssen zwei Datenstrukturen vorbereitet werden. struct sigevent speichert die Daten, die mit dem
Timer selbst Funktion und der aufzurufenden Funktion zusammenhängen:
struct sigevent {
int sigev_notify;
int sigev_signo;
union sigval sigev_value;
void (*sigev_notify_function) (union sigval);
void *sigev_notify_attributes;
pid_t sigev_notify_thread_id;
};
Das Element sigev_notify legt fest, ob die Timerfunktion als Signal Handler oder im Rahmen eines
Threads aktiviert wird. Interessant sind die Werte SIGEV_SIGNAL und SIGEV_THREAD. Im Fall von
SIGEV_SIGNAL legt sigev_signo die Nummer des Signals fest, welches nach Ablauf des Auslöseintervalls
dem Job gesendet wird. Das Feld sigev_value nimmt einen Parameter auf, der entweder dem Signal
Handler oder der im Thread abgearbeiteten Funktion übergeben wird. Der Signal Handler selbst wird mit
der Funktion sigaction() (siehe Signals) etabliert. Unter Linux kann auch noch der Thread spezifiziert
werden, dem das Signal zuzustellen ist. Dazu ist der Parameter sigev_notify_thread_id mit der Thread-ID
zu besetzen und anstelle von SIGEV_SIGNAL ist SIGEV_THREAD_ID für sigev_notify auszuwählen.
Ist für sigev_notify SIGEV_THREAD ausgewählt, nimmt sigev_notify_function die Adresse der Funktion
auf, die im Kontext eines Threads abgearbeitet wird. Per sigev_notify_attributes lässt sich die Threaderzeugung konfigurieren. Meistens reicht es aus, hier NULL zu übergeben.
Die Datenstruktur
wird per int timer_create(clockid_t clockid, struct sigevent
dem Kernel übergeben. clockid spezifiziert den Zeitgeber (CLOCK_REALTIME,
CLOCK_MONOTONIC), evp die initialisierte struct sigevent und in timerid findet sich nach dem Aufruf die
Kennung des erzeugten, aber deaktivierten Timers.
struct sigevent
*evp, timer_t *timerid)
Der beziehungsweise die Zeitpunkte, zu denen die Timerfunktion aufgerufen werden soll, wird über struct
itimerspec konfiguriert:
struct itimerspec {
struct timespec it_interval;
struct timespec it_value;
/* Timer interval */
/* Initial expiration */
81
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
};
gibt in Sekunden und Nanosekunden die Zeit an, zu der erstmalig die Funktion oder der Signal
Handler aufgerufen werden soll, it_interval spezifiziert in Sekunden und Nanosekunden die Periode. Ob
die Zeiten absolut oder relativ gesehen werden, wird beim Aufruf der Funktion int timer_settime(timer_t
timerid, int flags, const struct itimerspec *new_value, struct itimerspec * old_value) über den
Parameter flags (0 oder TIMER_ABSTIME festgelegt. new_value übernimmt die zeitliche Parametrierung. Falls
old_value ungleich NULL ist, findet sich in dieser Datenstruktur nach dem Aufruf das vorhergehende Intervall.
it_value
Die Funktionen sind nur nutzbar, wenn die Realzeitbibliothek librt zur Applikation gebunden wird. Dazu
ist beim Aufruf des Compilers die Option -lrt mit anzugeben.
Beispiel 4-8. Periodische Taskaktivierung
#include
#include
#include
#include
<stdio.h>
<time.h>
<signal.h>
<unistd.h>
void timer_function( union sigval parameter )
{
printf("timer with id %d active\n", getpid());
return;
}
int main( int argc, char **argv, char **envp )
{
timer_t itimer;
struct sigevent sev;
struct itimerspec interval;
printf("start process.\n");
sev.sigev_notify
= SIGEV_THREAD;
sev.sigev_notify_function
= timer_function;
sev.sigev_value.sival_int
= 99;
sev.sigev_notify_attributes = NULL;
if (timer_create(CLOCK_MONOTONIC, &sev, &itimer ) == -1) {
perror("timer_create");
return -1;
}
interval.it_interval.tv_sec = 1;
interval.it_interval.tv_nsec= 0;
interval.it_value.tv_sec
= 1;
interval.it_value.tv_nsec
= 0;
timer_settime( itimer, 0, &interval, NULL ); /* activate timer */
sleep( 5 );
printf("end process.\n");
return 0;
}
4.4. Inter-Prozess-Kommunikation
Die klassische Inter-Prozess-Kommunikation (Datenaustausch) bietet unter anderem die folgenden Methoden an:
•
•
82
Pipes/Mailboxes/Messages
Shared-Memory
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
•
Sockets
4.4.1. Pipes
Zum Datenaustausch bieten Betriebssysteme einen Mailbox-Mechanismus (send/receive Interface) an.
Dabei werden - im Regelfall unidirektional - Daten von Task 1 zu Task 2 transportiert und gequeued (im
Gegensatz zu gepuffert, Nachrichten gehen also nicht verloren, da sie nicht überschrieben werden).
In den meisten Realzeitbetriebssystemen und in Unix-Systemen ist ein Mailbox-Mechanismus über Pipes
(FIFO) und über die sogenannten Messages implementiert. Messages gehören zur Gruppe der klassischen System-V-IPC. Da allerdings die Verwaltung der benötigten Message-Queues, also das Anlegen, die
Verteilung von Zugriffsberechtigungen und das spätere Freigeben sehr kompliziert und vor allem fehleranfällig ist, sollten Messages in Realzeitapplikationen nicht verwendet werden und werden daher auch nicht
weiter beschrieben.
Werden demgegenüber bei der Kommunikation zwischen Tasks (unnamed) Pipes eingesetzt, ist ein solches
Management nicht notwendig. Über den Systemcall int pipe(int piped[2]) werden zwei Deskriptoren
reserviert. Über piped[1] können per write() Daten geschrieben, über piped[0] per read() gelesen werden.
Typischerweise reserviert eine Task zunächst per pipe() die Pipe-Deskriptoren um danach per fork() oder
pthread_create() eine neue Task zu starten, die die beiden Deskriptoren erbt. Da die Kommunikation über
Pipes unidirektional ist, gibt jede der Task den Deskriptor wieder frei, der nicht benötigt wird.
Beispiel 4-9. Interprozess-Kommunikation über Pipes
#include
#include
#include
#include
#include
<sys/wait.h>
<stdio.h>
<stdlib.h>
<unistd.h>
<string.h>
int main(int argc, char **argv, char **envp)
{
int pd[2]; /* pipe descriptor */
pid_t cpid;
char c;
if (pipe(pd) == -1) {
perror("pipe");
return -1;
}
if ((cpid=fork()) == -1) {
perror("fork");
return -1;
}
if (cpid == 0) {
close(pd[1]);
/* child reads from pipe */
/* close unused write end */
while (read(pd[0], &c, 1) > 0)
write(STDOUT_FILENO, &c, 1);
write(STDOUT_FILENO, "\n", 1);
close(pd[0]);
return 0;
} else {
/* Parent writes argv[1] to pipe */
close(pd[0]);
/* Close unused read end */
write(pd[1], "hello world", strlen("hello world"));
close(pd[1]);
/* Reader will see EOF */
wait(NULL);
/* Wait for child */
return 0;
}
83
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
}
4.4.2. Shared-Memory
Bei einem Shared-Memory handelt es sich um einen gemeinsamen Speicherbereich einer oder mehrerer
Tasks innerhalb eines Rechners. Die Realisierung eines gemeinsamen Speicherbereichs im Falle
von Threads auf Basis globaler Variablen ist trivial. Wollen mehrere Rechenprozesse jedoch einen
gemeinsamen Speicher nutzen, müssen sie diesen vom Betriebssystem anfordern. Ähnlich wie beim
Dual-Port-RAM, bei dem allerdings unterschiedliche Prozessoren zugreifen, kann die Speicheradresse des
gemeinsamen Speicherbereichs von Task zu Task unterschiedlich sein. Deshalb ist es auch hier wichtig,
Pointer innerhalb von Datenstrukturen relativ zum Beginn (oder Ende) des Speicherbereichs anzugeben.
Aus den gleichen Gründen wie sie für die Messages gelten, ist auch das Shared-Memory zwischen Rechenprozessen zu vermeiden. Die Verwaltung mit Anlegen der Ressource, Identifikation der Ressource, Zugriffsberechtigungen verwalten und schließlich der Freigabe ist im Detail ausgesprochen komplex. Außerdem ist
unter normalen Umständen nur eine Kommunikation lokal (innerhalb eines Rechnersystems) möglich.
Ein Shared-Memory auf Basis von globalen Variablen zwischen Threads demgegenüber ist jedoch eine sehr
einfache Möglichkeit, Daten zwischen zwei Threads auszutauschen. Natürlich stellt jede globale Variable
einen potenziellen kritischen Abschnitt dar und ist gegebenenfalls zu schützen!
4.4.3. Sockets
sd = socket(PF_INET, SOCK_STREAM, 0 );
bind( sd, ... );
listen( sd, Anzahl möglicher paralleler Verbindungen );
while( 1 )
Newsd = accept( sd, ... );
Zugriff auf die Verbindung über read und write
read( Newsd, buffer, sizeof(buffer) );
Abbildung 4-5. Basisstruktur einer Socket-Serverapplikation
Die wichtigste Schnittstelle für Inter-Prozess-Kommunikation stellt die Socket-Schnittstelle dar. Mittels
Sockets können Daten zwischen Prozessen ausgetauscht werden, die auf unterschiedlichen Rechnern
lokalisiert sind (verteiltes System).
Die Socketschnittstelle bietet Zugriff zur tcp/ip- und zur udp-Kommunikation. Ist dabei einmal eine
Verbindung zwischen zwei Prozessen hergestellt worden, können die Daten mit den bekannten Systemcalls
(read(), write() ...) ausgetauscht werden.
Auf tcp/ip-Ebene handelt es sich um eine Server/Client-Kommunikation. Ein Server alloziert per socketFunktion einen beliebigen Socket (Ressource) im System, wobei der Socket über eine Nummer identifiziert
wird. Damit ein Client auf einen Socket zugreifen kann, muss er die Socketnummer (Socketadresse) kennen.
Für Standarddienste, wie beispielsweise http, sind diese Nummern reserviert, man spricht von sogenannten
well known sockets.
Hat der Server einen Socket alloziert, muss er ihm eine
zuweisen (z.B. 80 bei http). Dies geschieht mit der Funktion
84
bekannte Socketnummer
bind() (siehe Abbildung
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
Basisstruktur einer Socket-Serverapplikation). Über einen Socket kann der Server gleichzeitig mehrere
Clients bedienen. Über die Funktion listen() parametriert er dabei, wieviele derartige Clients er maximal
bedienen möchte (Beispiel Socket-Serverprogramm). Ein einzelner Port auf einem Rechner kann aus dem
Grund mehrere Verbindungen (Clients) bedienen (und unterscheiden), weil eine Verbindung durch
•
•
•
•
Ziel-IP-Adresse,
Ziel-Port,
Remote-IP-Adresse und
Remote-Port
charakterisiert ist.
Ist der Socket geöffnet und ist ihm eine bekannte Socketadresse zugewiesen worden, kann der Server auf
Verbindungswünsche warten. Dieses Warten wird über den accept() Aufruf realisiert. Accept liefert, wenn
auf dem bekannten Socket ein Verbindungswunsch kommt, einen neuen Socket zurück. Der ursprüngliche,
gebundene Socket (bind socket) ist damit wieder frei, um auf weitere Verbindungswünsche zu warten.
Klassische Serverprogramme, wie beispielsweise der http-Server, erzeugen für jeden Verbindungswunsch
(also nach dem accept()) mittels fork() einen eigenen Prozess. Der zurückgegebene Socket (im Bild
ConnectionSocket) wird vom Server wie ein Filedeskriptor verwendet. Er kann also auf den Filedeskriptor/Socketdeskriptor schreiben und von ihm lesen (bidirektionale Verbindung).
Beispiel 4-10. Socket-Serverprogramm
#include
#include
#include
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<unistd.h>
<signal.h>
<string.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
static int bind_socket, connection_socket;
static void emergency_close( int Signal )
{
close( bind_socket );
close( connection_socket );
exit( -1 );
}
int main( int argc, char **argv, char **envp )
{
unsigned int count;
struct sockaddr_in my_port, remote_socket;
char buffer[512];
signal( SIGINT, emergency_close );
bind_socket = socket( AF_INET, SOCK_STREAM, 0 );
if (bind_socket <= 0) {
perror( "socket" );
return -1;
}
bzero( &my_port, sizeof(my_port) );
my_port.sin_port = htons( 12345 );
if (bind(bind_socket,(struct sockaddr *)&my_port,sizeof(my_port))<0) {
perror( "bind" );
return -1;
}
listen( bind_socket, 3 );
while (1) {
count = sizeof( remote_socket );
connection_socket = accept( bind_socket,
85
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
(struct sockaddr *)&remote_socket, &count );
if (connection_socket >= 0) {
printf("connection established ...\n");
while ((count=read(connection_socket,buffer,
sizeof(buffer)))) {
if (strncmp(buffer,"end",strlen("end")) == 0) {
break;
} else {
write( connection_socket,buffer,count );
}
}
close( connection_socket );
printf( "connection released ...\n");
}
}
close( bind_socket );
return 0;
}
sd=socket(PF_INET, SOCK_STREAM, 0 );
inet_aton( "192.168.12.12", &destination.sin_addr ); //Zieladresse spez.
destination.sin_port = htons( Portnummer ); //Zielport spezifizieren
destination.sin_family = PF_INET;
connect( sd, &destination, sizeof(destination) );
Zugriff auf die Verbindung über read und write
write( sd, Nachricht, strlen(Nachricht)+1 );
...
Abbildung 4-6. Basisstruktur einer Socket-Clientapplikation
Der Verbindungsaufbau über Sockets auf der Client-Seite ist nicht so kompliziert (siehe Bild
Basisstruktur einer Socket-Clientapplikation). Der Client alloziert sich - wie der Server auch - per
socket() Funktion die Socket-Ressource. Da der Client ja keine Verbindung entgegennehmen möchte,
muss dieser Socket auch nicht an eine bestimmte Nummer gebunden werden. Der Client tätigt nur
noch einen connect() Aufruf, um sich mit dem Remote-Rechner zu verbinden. Er verwendet den vom
Socket-Aufruf returnierten Socketdeskriptor/Filedeskriptor direkt (Beispiel Socket-Clientprogramm).
Beispiel 4-11. Socket-Clientprogramm
#include
#include
#include
#include
#include
#include
#include
#include
#include
<stdio.h>
<signal.h>
<stdlib.h>
<string.h>
<unistd.h>
<sys/types.h>
<sys/socket.h>
<arpa/inet.h>
<netinet/in.h>
#define TEXT "Hello, here is a client ;-)"
static int sd;
static void emergency_close( int signal )
{
close( sd );
}
int main( int argc, char **argv, char **envp )
{
86
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
int count;
struct sockaddr_in destination;
char buffer[512];
signal( SIGINT, emergency_close );
sd = socket( PF_INET, SOCK_STREAM, 0 );
if (sd <= 0) {
perror( "socket" );
return -1;
}
bzero( &destination, sizeof(destination) );
//inet_aton( "194.94.121.156", &destination.sin_addr );
//inet_aton( "127.0.0.1", &destination.sin_addr );
inet_aton( "192.168.69.39", &destination.sin_addr );
destination.sin_port = htons( 12345 );
destination.sin_family = PF_INET;
if (connect(sd,(struct sockaddr *)&destination,sizeof(destination))<0) {
perror( "connect" );
return -1;
}
write( sd, TEXT, strlen(TEXT)+1 );
if ((count=read( sd, buffer, sizeof(buffer))) <=0) {
perror( "read" );
close( sd );
return -1;
}
printf( "%d: %s\n", count, buffer );
write( sd, "end", 4 );
close( sd );
return 0;
}
Die Kommunikation über Sockets ist - wie in den Abbildungen ersichtlich - relativ unproblematisch.
Reale Serverprogramme werden aber aufgrund notwendiger Flexibilität deutlich komplexer. So
programmiert man im Regelfall weder die IP-Adresse noch die Portadresse fest in die Applikation ein.
Stattdessen werden symbolische Namen spezifiziert, die in entsprechenden Konfigurationsdateien
abgelegt sind (z.B. /etc/services) oder über andere Serverdienste (z.B. DNS für die Auflösung von
Hostnamen zu IP-Adressen) geholt werden. Für diese Aktionen existieren eine ganze Reihe weiterer
Bibliotheksfunktionen.
Die Schnittstelle unterstützt nicht nur die Kommunikation über tcp/ip, sondern auch über udp. Bei udp
handelt es sich um einen verbindungslosen Dienst. Der Server muss also kein accept() aufrufen, sondern
kann direkt vom Socket (Bind-Socket) lesen. Um hier Informationen über den Absender zu erhalten, gibt
es eigene Systemaufrufe, bei denen IP- und Portadresse des Absenders übernommen werden.
Auch die über IP angebotenen Multicast- und Broadcast-Dienste werden über die Socket-Schnittstelle bedient und sind letztlich Attribute (Parameter) des Sockets.
Die im IP-Protokoll festgelegten Datenstrukturen gehen von einem „big endian“-Ablageformat der Variablen (z.B. Integer) aus. Das bedeutet, dass eine Applikation, die auf einem Rechner abläuft, der ein „little
endian“-Datenablageformat verwendet, die Inhalte der Datenstrukturen erst konvertieren muss. Dieser Vorgang wird durch die Funktioen ntohX (net to host) und htonX (host to net) unterstützt (X steht hier für
short oder long). Auf einem Rechner mit „big endian“-Ablageformat sind die Funktionen (Makros) leer,
auf einem Rechner mit „little endian“-Ablageformat wird dagegen eine Konvertierung durchgeführt.
Schreibt man also eine verteilte Applikation, die unabhängig vom Datenablageformat ist, müssen alle
Datenstrukturen in ein einheitliches Format gewandelt werden. Die Daten müssen zum Verschicken konvertiert und beim Empfang wieder rückkonvertiert werden. Folgende Technologien unterstützen den Programmierer bei dieser Arbeit:
•
•
Remote Procedure Call (RPC)
Remote Message Invocation (RMI, für Java)
87
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
•
•
OLE (Microsoft)
Corba (Unix)
RPC, RMI, OLE und Corba kümmern sich dabei nicht nur um die Konvertierung, sondern auch um den
eigentlichen Datentransport.
4.5. Condition-Variable (Event)
Während über ein Semaphor der Zugriff auf ein gemeinsam benutztes Betriebsmittel synchronisiert wurde
(Synchronisation konkurrierender Tasks), werden zwei oder mehrere kooperierende Tasks über so genannte
Condition Variablen oder Events bezüglich ihres Programmablaufs synchronisiert. Dabei schläft eine Task
auf eine Condition Variable (Ereignis), welche durch eine andere Task gesetzt wird.
Eine Condition Variable ist ein Synchronisationselement, welches Rechenprozessen erlaubt solange den
Prozessor frei zu geben, bis eine bestimmte Bedingung erfüllt ist. Die Basisoperationen auf Condition Variablen sind:
•
•
schlafen auf die Condition Variable und
setzen der Condition Variable.
Das Setzen der Condition Variable, wenn kein anderer Job darauf schläft, bleibt wirkungslos; das Ereignis
wird nicht zwischengespeichert. Um die sich dadurch ergebende Deadlocksituation zu vermeiden, kombiniert man die Condition Variable mit einem Mutex.
Um innerhalb von Echtzeitapplikationen, die auf ein Posix-Interface aufsetzen, Condition Variable nutzen
zu können, gibt es einen Satz von Funktionen.
Die Condition Variable selbst wird innerhalb der Applikation definiert. Die Initialisierung kann statisch
(Compiler) durch Zuweisung mit dem Makro PTHREAD_COND_INITIALIZER erfolgen, oder dynamisch durch
Aufruf der Funktion int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);.
Wird für attr Null übergeben, werden die Default-Attribute eingesetzt.
Um
auf
die
Signalisierung
der
Condition
Variable
zu
warten,
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
dienen
die
und
Funktionen
int
pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec
*abstime). Vor Aufruf einer der beiden Funktionen muss das zugehörige Mutex mutex zwingend reserviert
(gelockt) sein. Die Funktionen geben atomar (ohne dass sie dabei unterbrochen werden) das Mutex frei
und legen den aufrufenden Thread schlafen, bis die Signalisierung erfolgt und gleichzeitig das Mutex
wieder gelockt werden kann. Nach dem Aufruf einer der beiden Funktionen ist das Mutex mutex also
wieder gesperrt.
Zur Signalisierung dienen die Funktionen
pthread_cond_broadcast(pthread_cond_t
und int
*cond). Erstere weckt genau einen Job auf, letztere alle auf die
int pthread_cond_signal(pthread_cond_t *cond)
Condition Variable cond schlafende Jobs.
Um
die
Condition
Variable
wieder zu deinitialisieren, dient die Funktion
int
pthread_cond_destroy(pthread_cond_t *cond). Allerdings ist diese Funktion nicht zwingend erforderlich,
da die Variable ja ohnehin durch die Applikation selbst definiert wird..
cond
Condition Variablen lassen sich gut zur Lösung so genannter Producer-/Consumer-Probleme einsetzen.
Bei derartigen Problemen erzeugt eine Task Producer eine Information und eine Task Consumer verwertet
diese. Condition-Variable zur Lösung des Producer-/Consumer-Problems zeigt den Einsatz zweier Condition Variablen zur Lösung eines solchen Problems.
88
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
Beispiel 4-12. Condition-Variable zur Lösung des Producer-/Consumer-Problems
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
static pthread_mutex_t mutex;
static pthread_cond_t cond_consumed = PTHREAD_COND_INITIALIZER;
static pthread_cond_t cond_produced = PTHREAD_COND_INITIALIZER;
static void *producer( void *arg )
{
int count=0;
pthread_mutex_lock( &mutex );
while( 1 ) {
printf("producer: PRODUCE %d...\n", count++);
pthread_cond_signal( &cond_produced );
pthread_cond_wait( &cond_consumed, &mutex );
}
return NULL;
}
static void *consumer( void *arg )
{
int count=0;
sleep( 1 );
pthread_mutex_lock( &mutex );
while( 1 ) {
printf("consumer: CONSUME %d...\n", count++);
pthread_cond_signal( &cond_consumed );
pthread_cond_wait( &cond_produced, &mutex );
}
return NULL;
}
int main( int argc, char **argv, char **envp )
{
pthread_t p1, p2;
if (pthread_mutex_init( &mutex, NULL )) {
perror("pthread_mutex_init");
return -1;
}
pthread_create( &p2, NULL, consumer, NULL );
pthread_create( &p1, NULL, producer, NULL );
pthread_join( p1, NULL );
pthread_join( p2, NULL );
pthread_mutex_destroy( &mutex );
return 0;
}
4.6. Signals
Bei Signals handelt es sich um Software-Interrupts auf Applikationsebene, das heißt: Ein Signal führt zu
einer Unterbrechung des Programmablaufs innerhalb der Applikation. Daraufhin wird das Programm entweder abgebrochen oder reagiert mit einem vom Programm zur Verfügung gestellten Signal-Handler (ähnlich einer Interrupt-Service-Routine).
89
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
Realisiert werden Signals als Systemcalls des Betriebssystems. Signals können zum einen durch eine Applikation ausgelöst werden, zum anderen aber auch durch Ereignisse innerhalb des Betriebssystem selbst.
So führt zum Beispiel der Zugriff auf einen nicht vorhanden Speicherbereich (z.B. der Zugriff auf Adresse
Null) innerhalb des Betriebssystems dazu, dem Prozess ein Segmentation-Fault-Signal zu schicken, welches
der Prozess abfangen könnte. In der zugehörigen Segmentation-Fault-ISR würden alle notwendigen Daten
noch gespeichert. Erst danach würde die Applikation beendet werden.
Die Unterschiede zwischen einer Condition Variable (Event) und Signals sind in Tabelle
Unterschiede zwischen einem Signal und einer Condition Variablen dargestellt.
Tabelle 4-1. Unterschiede zwischen einem Signal und einer Condition Variablen
Signals
Condition Variable
Die Signalisierung kommt asynchron zum
Programmablauf und wird asynchron verarbeitet.
Die Signalisierung kommt synchron zum
Programmablauf und wird synchron verarbeitet.
Charakter einer Interrupt-Service-Routine
(Software-Interrupt).
Rendezvous-Charakter
Warnung
Ein Signal führt - wenn nicht anders konfiguriert - zum sofortigen Abbruch eines gerade
aktiven Systemcalls. Werden im Rahmen einer Applikation Signals verwendet bzw. abgefangen, muss jeder Systemcall daraufhin überprüft werden, ob selbiger durch ein Signal
unterbrochen wurde und, falls dieses zutrifft, muss der Systemcall neu aufgesetzt werden!
Unixartige Betriebssysteme unterstützen Signals über verschiedene Interfaces. Die größte Kontrolle gibt
dabei die Funktion int sigaction(int signum, const struct sigaction *act, struct sigaction
*oldact). Mit dieser Funktion kann über den Parameter oldact die aktuelle Konfiguration ausgelesen und
alternativ oder gleichzeitig mit act eine neue Konfiguration für das Signal signum gesetzt werden.
Die Konfiguration selbst findet sich in der Datenstruktur struct sigaction. Der Programmierer hat die
Möglichkeit, die Adresse einer Signalhandler-Funktion (Typ sa_sighandler anzugeben, oder alternativ
die Adresse eines Signalhandlers (sa_sigaction), der drei Parameter übergeben bekommt und dem damit
vielfältige Information auch über den Prozess zur Verfügung stehen, der das Signal geschickt hat. In das
Bitfeld sa_mask können die Signalnummern aktiviert werden, die während der Abarbeitung des Signalhandlers geblockt werden sollen. Über das Datenstrukturelement sa_flags wird unter anderem ausgewählt, ob
der einfache oder der alternative Signalhandler verwendet werden soll.
Beispiel 4-13. Programmierbeispiel Signal
#include
#include
#include
#include
<stdio.h>
<unistd.h>
<signal.h>
<string.h>
#define message "SIGINT caught\n"
void signal_handler(int value)
{
write( 1, message, strlen(message) ); // printf is not signalsafe
}
int main(int argc, char **argv, char **envp )
{
struct sigaction new_action;
int time_to_sleep;
90
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
new_action.sa_handler = signal_handler;
sigemptyset( &new_action.sa_mask );
new_action.sa_flags = 0;
sigaction( SIGINT, &new_action, NULL );
printf("pid: %d\n", getpid() );
time_to_sleep = 20;
while( time_to_sleep )
time_to_sleep = sleep( time_to_sleep );
return 0;
}
Applikationen schicken Signals durch Aufruf der Funktion int
dabei den Job, dem das Signal num zugestellt werden soll.
kill(pid_t pid, int sig):. pid spezifiziert
4.7. Peripheriezugriff
Der applikationsseitige Zugriff auf Peripherie erfolt in mehreren Schritten. Im ersten Schritt teilt die Applikation dem Betriebssystem mit, auf welche Peripherie sie in welcher Art (zum Beispiel lesend oder
schreibend) zugreifen möchte. Das Betriebssystem prüft, ob der Zugriff möglich und erlaubt ist. Ist dies der
Fall, bekommt die Applikation die Zugriffsberechtigung in Form eines Handles beziehungsweise Deskriptors mitgeteilt. Im zweiten Schritt greift die Applikation mit Hilfe des Deskriptors auf die Peripherie so
oft und so lange wie notwendig zu. Erst wenn keine Zugriffe mehr notwendig sind, wird das Handle
beziehungsweise der Deskriptor wieder freigegeben (Schritt 3).
Für den ersten Schritt steht in unxibasierten Systemen die von normalen Dateizugriffen her bekannte Funktion int open(const char *pathname, int flags) zur Verfügung. Da hier das Konzept alles ist eine Datei
praktiziert wird, wird die Peripherie über einen Dateinamen (pathname), den Gerätedateinamen identifiziert.
Die Art des Zugriffs (lesend, schreibend, nicht blockierend) wird über die in der Headerdatei <fcntl.h>
definierten flags spezifiziert:
• O_RDONLY:
Lesender Zugriff
Schreibender Zugriff
O_RDWR: Lesender und Schreibender Zugriff
O_NONBLOCK: Nichtblockierender Zugriff
• O_WRONLY:
•
•
Ein negativer Rückgabewert der Funktion open bedeutet, dass der Zugriff nicht möglich ist. Anhand der
(thread-) globalen Variablen errno kann die Applikation die Ursache abfragen. Ein positiver Rückgabewert
repräsentiert das Handle beziehungsweise den Deskriptor.
ssize_t und size_t
Viele Standardfunktionen verwenden den Datentyp size_t (size type, vorzeichenlos)
oder ssize_t (signed size type, vorzeichenbehaftet). Typischerweise repräsentiert er den
orginären Typ unsigned long beziehungsweise long.
Applikationen greifen auf die Peripherie dann über die Funktionen ssize_t read(int fd, void *buf,
size_t count) und ssize_t write(int fd, const void *buf, size_t count) zu. Der Parameter fd ist der
Filedeskriptor (Handle), der von open zurückgegeben wird. Die Adresse buf enthält den Speicherbereich,
in den read die Daten ablegt und count gibt an, welche Größe dieser Speicherbereich hat.
read kopiert von der Peripherie mindestens ein Byte und maximal count Bytes an die Speicheradresse buf
und gibt - solange kein Fehler aufgetreten ist - die Anzahl der kopierten Bytes zurück. Gibt es zum Zeitpunkt
91
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
des Aufrufes der Funktion read keine Daten, die gelesen werden können, legt die Funktion im so genannten
blockierenden Mode den aufrufenden Thread schlafen und weckt ihn wieder auf, wenn mindestens ein
Byte in den Speicherbereich buf abgelegt wurde. Im nicht blockierenden Mode (O_NONBLOCK) quittiert die
Funktion den Umstand, dass keine Daten zur Verfügung stehen, mit einem negativen Rückgabewert. Die
(thread-) globale Variable errno hat dann den in der Headerdatei <errno.h> definierten Wert EAGAIN.
Auch im Fehlerfall, wenn beispielsweise der Aufruf durch ein Signal unterbrochen wurde, gibt read einen
negativen Wert zurück und errno enthält den zugehörigen Fehlercode.
Die Funktion write arbeitet identisch. Die Funktion schreibt mindestens ein Byte maximal aber count Bytes
auf das über den Filedeskriptor fd spezifizierte Gerät. Falls nicht geschrieben werden kann, weil beispielsweise das Sendefifo einer seriellen Schnittstelle bereits voll ist, wird der zugreifende Thread in den Zustand
schlafend versetzt, bis der Zugriff möglich ist. Beim nicht blockierenden Zugriff gibt write in diesem Fall
- schreiben ist zur Zeit nicht möglich - einen negativen Wert zurück und errno enthält wieder den Wert
EAGAIN.
Direct-IO versus Buffered-IO
Wenn Ein- und Ausgabebefehle wie die Systemcalls read() und
Verzögerung umgesetzt werden, spricht man von Direct-IO.
write()
direkt, ohne
Funktionen wie beispielsweise fprintf(), fread(), fwrite() oder fscanf() gehören zur so
genannten Buffered-IO. Bei dieser puffert das Betriebssystem (genauer die Bibliothek)
die Daten aus Gründen der Effizienz zwischen. Erst wenn es sinnvoll erscheint, werden
die zwischengespeicherten Daten per Systemcall read() oder write() transferiert. Dies ist
beispielsweise der Fall, wenn ein \n ausgegeben wird oder wenn 512 Byte Daten gepuffert
sind.
Da nur die Direct-IO Funktionen die volle Kontrolle über die Ein- und Ausgabe geben, werden in Echtzeitapplikationen nur diese für den Datentransfer mit der Peripherie eingesetzt.
Beispiel 4-14. Programmbeispiel Zugriff auf Peripherie
#include
#include
#include
#include
#include
<stdio.h>
<fcntl.h>
<string.h>
<errno.h>
<unistd.h>
static char *hello = "Hello World";
int main( int argc, char **argv, char **envp )
{
int dd; /* device descriptor */
ssize_t bytes_written, bytes_to_write;
char *ptr;
dd = open( "/dev/ttyS0", O_RDWR ); /* blocking mode */
if (dd<0) {
perror( "/dev/ttyS0" );
return -1;
}
bytes_to_write = strlen( hello );
ptr = hello;
while (bytes_to_write) {
bytes_written = write( dd, hello, bytes_to_write );
if (bytes_written<0) {
perror( "write" );
close( dd );
return -1;
}
92
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
bytes_to_write -= bytes_written;
ptr += bytes_written;
}
close( dd );
return 0;
}
Der Zugriffsmode (blockierend oder nicht blockierend) kann im übrigen per int fcntl(int fd, int cmd,
... /* arg */) auch nach dem Öffnen noch umgeschaltet werden. Dazu werden zunächst mit dem Kommando (cmd) F_GETFL die aktuellen Flags gelesen und dann wird entweder per Oder-Verknüpfung der nicht
blockierende Modus gesetzt beziehungsweise per XOR-Verknüpfung der blockierende Modus wieder aktiviert.
int fd, fd_flags, ret;
...
fd_flags = fcntl( fd, F_GETFL );
if (fd_flags<0) {
return -1; /* Fehler */
}
fd_flags|= O_NONBLOCK; /* nicht blockierenden Modus einschalten */
if (fcntl( fd, F_SETFL, (long)fd_flags )<0 ) {
return -1; /* Fehler */
}
...
fd_flags = fcntl( fd, F_GETFL );
if (fd_flags<0) {
return -1; /* Fehler */
}
fd_flags~= O_NONBLOCK; /* nicht blockierenden Modus einschalten */
if (fcntl( fd, F_SETFL, (long)fd_flags )<0 ) {
return -1; /* Fehler */
}
...
Werden regelmäßig (im nicht blockierenden Modus) Peripheriegeräte daraufhin abgeprüft, ob Daten zum
Lesen vorliegen oder ob Daten geschrieben werden können, spricht man von Polling. Polling ist insofern
ungünstig, da ein Rechner auch dann aktiv wird, wenn eigentlich nichts zu tun ist. Besser ist der ereignisgesteuerte Zugriff (blockierender Modus), bei dem die Applikation nur dann aktiv wird, wenn Daten gelesen oder geschrieben werden können. Allerdings hat der blockierende Modus in unixartigen Systemen den
Nachteil, dass per read oder write immer nur eine Quelle abgefragt werden kann. Oftmals sind aber mehrere
Datenquellen oder Datensenken abzufragen. Dieses Problem kann auf zwei Arten gelöst werden.
Die klassische Methode basiert auf der Funktion
int select(int nfds, fd_set *readfds, fd_set
*writefds, fd_set *exceptfds, struct timeval *timeout),
die auch eine portable Möglichkeit ist, einen
Thread mit einer Genauigkeit von Mikrosekunden schlafen zu legen. Select werden Sets von Deskriptoren
übergeben, die auf Aktivitäten überwacht werden. Sollten an einem der in readfds eingetragenen
Deskriptoren Daten gelesen werden können, ohne dass ein (blockierend) zugreifender Thread schlafen
gelegt wird, returniert die Funktion. Ebenso ist das mit writefds: select returniert, falls an einem
der überwachten Deskriptoren Daten ohne zu blockieren ausgegeben werden können. Das dritte Set,
exceptfds, wird nur im Kontext von Netzwerkverbindungen verwendet. Sollten an einem der im Set
spezifizierten Socketdeskriptoren so genannte Out of Band Daten vorliegen, kehrt die Funktion zurück.
Der letzte Parameter der Funktion (timeout) dient der Zeitüberwachung. Sollten an keinem der in den Sets
definierten Deskriptoren Daten ohne zu blockieren transferiert werden können, schläft select maximal für
die in timeout angegebene Zeitspanne. Ist timeout Null, schläft die Funktion ohne Zeitüberwachung. Aus
Gründen der Effizienz muss im Parameter nfds beim Aufruf noch der höchste in den Sets spezifizierte
Deskriptor plus eins übergeben werden.
Die übergebenen Sets von Deskriptoren (Datentyp fd_set) sind implementierungstechnisch betrachtet
meistens als Bitfelder realisiert. Jedes Bit steht für einen Deskriptor. Ist das Bit gesetzt, soll der
entsprechende Deskriptor überwacht werden. Um ein Set von Deskriptoren zu initialisieren, steht das
93
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
Makro void FD_ZERO(fd_set *set) zur Verfügung. Es stellt sicher, dass alle Bits zu Null gesetzt sind. Mit
der Funktion void FD_SET(int fd, fd_set *set) wird dem Set ein Deskriptor hinzugefügt (Bit setzen).
Ein Deskriptor wird über void FD_CLR(int fd, fd_set *set) wieder aus dem Set entfernt (ein einzelnes
Bit wird gelöscht). Und um nach dem Aufruf von select festzustellen, über welchen Deskriptor Daten
gelesen oder geschrieben werden können, verwenden Sie das Makro int FD_ISSET(int fd, fd_set *set)
(siehe Programmbeispiels select).
Beispiel 4-15. Programmbeispiels select
#include
#include
#include
#include
<stdio.h>
<sys/time.h>
<sys/types.h>
<unistd.h>
#define max(x,y) ((x) > (y) ? (x) : (y))
int main( int argc, char **argv, char **envp )
{
fd_set rds;
struct timeval tv;
int retval, nds=0;
int dd = 0; /* watch stdin to see when it has input */
FD_ZERO(&rds); /* initialize the deskriptor set */
/* for every deskriptor you want to add... */
FD_SET(dd, &rds); /* add the deskriptor you want to watch to the set */
nds = max(nds, dd);
tv.tv_sec = 5; /* wait up to five seconds. */
tv.tv_usec = 0;
retval = select(nds, &rds, NULL, NULL, &tv);
if (retval) {
if ( FD_ISSET(dd, &rds) )
printf("Data is available now.\n");
} else {
printf("No data within five seconds.\n");
}
return 0;
}
Die Funktion select ist problematisch. Per Definitionem soll ein dem select folgender read oder write
Aufruf nicht blockieren. Dieses Verhalten müsste letztlich ein Treiber sicherstellen, was aber nur in Ausnahmefällen gewährleistet ist. Falls also mehrere Threads auf die gleiche Peripherie zugreifen, könnte select
dem einen Thread signalisieren, dass das Lesen der Daten möglich ist, der andere Thread aber in dem Moment diese Daten bereits abholen, so dass der erste Thread, wenn er das read aufruft doch schlafen gelegt
wird.
Die modernere Variante, mehrere Ein- oder Ausgabekanäle zu überwachen, basiert auf der Threadprogrammierung. Für jeden Kanal (Filedeskriptor, Handle) wird ein eigener Thread aufgezogen, der dann blockierend per read oder write zugreift. Neben der einfacheren und übersichtlicheren Programmierung hat das
auch den Vorteil, dass damit direkt das nebenläufige Programmieren (Stichwort Multicore-Architektur) unterstützt wird.
Sind keine Zugriffe auf die Peripherie über den Deskriptor mehr notwendig, kann dieser per int close(int
fd) wieder freigegeben werden. In vielen Anwendungen fehlt jedoch zu einem open das korrespondierende
close. Das ist insofern nicht dramatisch, da das Betriebssystem beim Ende einer Applikation alle noch
offenen Deskriptoren von sich aus wieder freigibt.
94
Kapitel 4. Aspekte der nebenläufigen Echtzeit-Programmierung
Manche Peripheriegeräte, wie beispielsweise eine Grafikkarte, transferieren nicht nur einzelne Bits oder
Bytes, sondern umfangreiche Speicherbereiche. Diese Daten über einzelne read und write Aufrufe zu verschieben, wäre sehr ineffizient: Mit jedem Zugriff ist ein Kontextwechsel notwendig und außerdem wird
mindestens in der Applikation ein Speicherbereich benötigt, in den beziehungsweise von dem die Daten
zwischen Applikation und Hardware transferiert werden. Daher bieten Betriebssysteme und die zu den
Geräten gehörenden Treiber oft die Möglichkeit, Speicherbereiche der Hardware (oder aber auch des Kernels) in den Adressraum einer Applikation einzublenden. Die Applikation greift dann direkt auf diese Speicherbereiche zu.
Dazu öffnet die Applikation als erstes die Gerätedatei (Funktion open), die den Zugriff auf die gewünschte
Peripherie ermöglicht. Der zurückgelieferte Gerätedeskriptor dd (Device Deskriptor) wird dann der Funktion void *mmap(void *addr, size_t length, int prot, int flags, int dd, off_t offset) übergeben.
Ist der Parameter addr mit einer Adresse vorbelegt, versucht das Betriebssystem den Speicher an diese
Adresse einzublenden. Typischerweise ist addr aber auf Null gesetzt und das System selbst sucht eine günstige, freie Adresse aus. Der Parameter length gibt die Länge des Speicherbereiches an, der aus Sicht der
Peripherie ab dem Offset offset eingeblendet werden soll, prot spezifiziert den gewünschten Speicherschutz und ist entweder PROT_NONE, PROT_EXEC, PROT_READ oder PROT_WRITE (bitweise verknüpft). Das Argument flags legt fest, ob Änderungen an dem eingeblendeten Speicherbereich für andere Threads sichtbar
sind, die den Bereich ebenfalls eingeblendet haben. Hierzu stehen die vordefinierten Werte MAP_SHARED und
MAP_PRIVATE zur Verfügung.
Beispiel 4-16. Speicherbereiche in den Adressraum einblenden
#include
#include
#include
#include
<stdio.h>
<fcntl.h>
<unistd.h>
<sys/mman.h>
int main( int argc, char **argv, char **envp )
{
int dd;
void *pageaddr;
int *ptr;
dd = open( "/dev/mmap_dev", O_RDWR );
if (dd<0) {
perror( "/dev/mmap_dev" );
return -1;
}
pageaddr = mmap( NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, dd, 0 );
printf("pageaddr mmap: %p\n", pageaddr );
if (pageaddr) {
ptr = (int *)pageaddr;
printf("*pageaddr: %d\n", *ptr );
*ptr = 55; /* access to the mapped area */
}
munmap( pageaddr, 4096 );
return 0;
}
Um einen eingeblendeten Speicherbereich wieder freizugeben, ruft die Applikation
*addr, size_t length) auf.
int munmap(void
95
Kapitel 5. Echtzeitarchitekturen
Um ein Standardbetriebssystem für eine Aufgabe einzusetzen, ei der harte Zeitanforderungen eingehalten
werden müssen, gibt es drei Basistechnologien:
1. Teile der Applikation in den Kernel verlagern.
2. Mehrkernansatz: Einsatz einer Mehrkernmaschine.
3. Mehrkernelansatz: Neben dem Standardbetriebssystem wird ein Echtzeitbetriebssystem eingesetzt.
5.1. Applikationen im Kernel
Wenn die zeitlichen Anforderung auf Applikationsebene nicht eingehalten werden können, kann der Systemarchitekt überlegen, die zeitkritischen Teile der Applikation in den Kernel zu verlagern. Das hat die
folgende Vorteile:
•
•
Im Kernel gibt es generell kürzere Latenzzeiten.
Der Overhead ist geringer, da weniger Kontextwechsel stattfinden müssen.
Um kürzere Latenzzeiten zu erhalten, verlagert der Architekt die Teile, die Anforderungen von wenigen
Mikrosekunden haben, direkt in die ISR. Hierbei ist natürlich zu beachten, dass die Abarbeitung der ISR
dadruch nicht zu lang wird. Das würde sich generell negativ auf das Zeitverhalten des Gesamtsystems
auswirken, da ja für alle anderen Prozesse eine zusätzliche Latenzzeit auftritt.
Ansonsten sollten die Applikationsteile in Treiberfunktionen ausgelagert werden, die entweder auf der
Kernel-Ebene oder auf der Soft-IRQ-Ebene (siehe Unterbrechungsmodell) abgearbeitet werden.
Der Vorteil dieses Ansatzes: Der Anwender hat trotz der Verlagerung weiterhin sein Standardbetriebssystem
mit all seinen Vorteilen. Nachteilig ist zweifelsohne, dass sich die Lösung der Echtzeitaufgabe auf unterschiedliche Komponenten verteilt und auf das Betriebssystem angepasst werden muss. Es besteht damit
auch keine saubere Trennung zwischen Applikation und Betriebssystemkern mehr, die Gefahr von Systemabstürzen durch Programmierfehler, die sich im Kernel ungeschützt bemerkbar machen können, besteht.
Außerdem gibt es bei der Programmierung von Funktionen im Kernel einige Einschränkungen. So können
im Kernel beispielsweise keine Floating-Point-Operationen durchgeführt werden.
Trotz der manigfaltigen Nachteile ist diese Lösung zumindest dem Mehrkernel-Ansatz vorzuziehen.
5.2. Mehrkernmaschine
Eine weitere Möglichkeit, Echtzeitanforderungen einzuhalten, bieten Mehrprozessor- beziehungsweise
Mehrkernmaschinen. Auf diesen Rechnern wird eine CPU allein für die Echtzeitaufgaben reserviert.
Bereits ein moderner Linux-Kernel (zum Beispiel Kernel 2.6.21) bietet diese Möglichkeit an. Kommerziell
gibt es solche Systeme, die Realzeitprozesse auf einer abgeriegelten (shielded) CPU abarbeiten lassen
(zum Beispiel Redhawk-Linux der Firma Concurrent).
Aufgabe des Systemarchitekten ist es zunächst, die abzuarbeitenden Rechenporzesse in Realzeit- und
Nichtrealzeitprozesse zu klassifizieren. Dabei müssen die zeitlichen Parameter der Prozesse und die sich
96
Kapitel 5. Echtzeitarchitekturen
ergebende Gesamtauslastung bestimmt werden. Die Gesamtauslastung muss unterhalb von »Anzahl-CPU
* 100 Prozent« liegen. Typischerweise wird die Boot-CPU (die CPU mit der Bezeichnung »0« als die CPU
eingeplant, die nicht Realzeitprozesse abarbeitet. Das hat oft hardwaretechnische Gründe, denn nicht bei
jeder Hardware lassen sich Interrupts beliebig auf unterschiedliche Prozessorkerne verteilen.
n
0
0
0
0
7
6
5
4
3
2
1
0
Bit
0
0
0
0
1
1
1
1
cpumask_t
Globale CPU−Sets:
cpu_possible_map
cpu_present_map
cpu_online_map
1 = CPU könnte es geben
1 = CPU ist eingebaut
1 = CPU ist verfügbar
pro Thread 1x vorhanden:
cpus_allowed
1 = CPU darf genutzt werden
Abbildung 5-1. Datenstruktur zur Verwaltung von mehreren Prozessorkernen.
Jedem Rechenpozress kann übrigens typischerweise eine Affinität mitgegeben werden. HIerbei handelt es
sich um die Kennung, auf welchen Prozessoren ein Prozess abgearbeitet werden dasrf (siehe Abbildung
Datenstruktur zur Verwaltung von mehreren Prozessorkernen.). Die Affinität lässt sich über die Funktion
sched_setaffinity() programmtechnisch realisieren.
Beim weiteren Vorgehen gibt es zwei Möglichkeiten:
Alle Nicht-Realzeitprozesse werden auf CPU 0 verlagert. Die Gesamtauslastung ρges für diese CPU muss
dabei natürlich unterhalb von 100 Prozent liegen. Die Realzeitprozesse werden auf den übrigen Prozessoren
abgearbeitet, wobei der Scheduler selbst die Verteilung auf die einzelnen Prozessoren vornimmt.
Alternativ dazu kann der Systemarchitekt die Rechenprozesse direkt den einzelnen Rechnerkernen zuordnen.
In jedem Fall muss nach der Verteilung auf die Prozessoren für jeden Prozessor getrennt ein Echtzeitnachweis geführt werden.
In Linux gibt es außerdem für die Zuordnung von Rechenprozesse auf Prozessoren das
»cgroup«-Framework (Container- oder Control-Group) [KuQu08]. Control Croups stellen ein neues
Framework zur hierarchischen Organisation von Rechenprozessen (die spezifische Eigenschaften besitzen)
und deren zukünftigen Kindprozessen dar. Mit diesem Mittel gruppiert der Admin über ein virtuelles
Filesystem (type cpuset) sowohl Prozessoren als auch Speicherressourcen (»memory nodes«) und verteilt
auf die eingerichteten Gruppen die Rechenprozesse. In Abbildung Hierarchische Prozessor-Organisation.
ist zu sehen, dass diese Verteilung hierarchisch aufgebaut ist. Die oberste Gruppe enthält sämtliche
Prozessoren und Memory-Nodes. In einer Ebene darunter lassen sich die Ressourcen auf mehrere
Untergruppen vertreilen. Per Flag »cpu-exclusive« und »memory-exclusive« lässt sich noch festlegen, ob
eine Ressource von mehreren Gruppen aus verwendet werden darf (überlappend), oder eben exklusiv der
einen Gruppe zur Verfügung steht.
Die Verlagerung eines Rechenprozesses in eine Gruppe führt dazu, dass das Attribut cpus_allowed des
verschobenen Prozesses mit den gruppenspezifischen Einstellungen (Affinity-Maske der Gruppe) überschrieben wird. Auf diese Weise lassen sich recht einfach (und dauerhaft) die zeitkritischen Prozesse von
den weniger zeitkritischen trennen. Im einfachsten Fall legen Sie zwei Gruppen (»rt« und »non-rt«) an,
weisen diesen die Speicherressourcen und die Prozessorkerne zu und verschieben sämtliche Prozesse in die
97
Kapitel 5. Echtzeitarchitekturen
»non-rt«-Gruppe. Anschließend picken Sie sich die zeitkritischen Rechenprozesse heraus und migrieren
diese in die »rt«-Gruppe:
root@lab01:~# mkdir /dev/cpuset
root@lab01:~# mount -t cpuset -ocpuset cpuset /dev/cpuset
root@lab01:~# mkdir /dev/cpuset/rt
root@lab01:~# mkdir /dev/cpuset/non-rt
root@lab01:~# echo 0 > /dev/cpuset/rt/mems
root@lab01:~# echo 0 > /dev/cpuset/non-rt/mems
root@lab01:~# echo 2 >/dev/cpuset/rt/cpus
root@lab01:~# echo 1 >/dev/cpuset/rt/cpu_exclusive
root@lab01:~# echo 0-1 >/dev/cpuset/non-rt/cpus
root@lab01:~# echo 1 >/dev/cpuset/rt/cpu_exclusive
root@lab01:~# for pid in $(cat /dev/cpuset/tasks); \
> do /bin/echo $pid > /dev/cpuset/non-rt/tasks;\
> done
root@lab01:~# cd /dev/cpuset/rt
root@lab01:~# /bin/echo $$ >tasks
root@lab01:~# starte_rt_task
...
cgroup
Jobs, die
auf allen Kernen der
cgroup abgearbeitet
werden.
/dev/cpuset
0
Lastverteilung
zwischen den
Kernen
non−rt
0
1
1
2
3
rt−h
rt
2
3
normales
Scheduling
Rechenprozesse
Ohne RT−
Anforderungen
Mit RT−
Anforderungen
Abbildung 5-2. Hierarchische Prozessor-Organisation.
Wenn Sie die Verteilung einmal vorgenommen haben, sollten Sie davon absehen, nachträglich die AffinityMaske einer Gruppe zu ändern. Das hat nämlich keine Auswirkung mehr auf den innerhalb der Gruppe
befindlichen Prozess. Um eine veränderte Parametrierung wirksam werden zu lassen, müsste der Rechenprozess der entsprechenden Gruppe neu zugeordnet werden. Das Aus- oder Einbrechen aus dem ContainerGefängnis (cgroup) über die Funktion sched_setaffinity() ist aber dennoch nicht möglich. Nur wenn ein
Prozessorkern zur Gruppe gehört, kann der Prozess dorthin migrieren.
Durch das Verschieben der Prozesse in die non-rt-Gruppe wird der für Realzeitaufgaben vorgesehene
Prozessor freigeschaufelt. Das lässt sich einfacher per CPU-Hotplugging bewerkstelligen. Eigentlich
heißt Hotplugging, dass während des Betriebs Hardware zu oder abgesteckt wird; in diesem Fall
also einer von mehreren Prozessoren. Das sollten Sie – falls Sie zu den glücklichen Besitzern einer
Mehrprozessormaschine gehören – jetzt nicht wörtlich nehmen und während des Betriebs den Rechner
aufschrauben. Hardware-CPU-Hotplugging unterstützen nämlich nur wenige Maschinen. Unabhängig
davon können Sie aber softwaretechnisch (logisch) einzelne Prozessoren (und damit auch Prozessorkerne)
deaktivieren und wieder aktivieren. Beim Deaktivieren werden dann die auf dem Prozessor ablaufenden
Rechenprozesse auf die anderen Prozessoren migriert. Das Feature wird auch sinnvoll eingesetzt, wenn
der Verdacht besteht, dass ein einzelner CPU-Kern defekt ist. Die CPU-0 spielt als Boot-Prozessor eine
98
Kapitel 5. Echtzeitarchitekturen
Sonderrolle und lässt sich auf vielen Systemen nicht deaktivieren. Das hängt damit zusammen, dass bei
diesen Architekturen einige Interrupts fest an den ersten Prozessor-Kern gekoppelt sind. Abbildung
Deaktivieren von Rechnerkernen. zeigt, wie Sie durch Zugriffe auf das Sys-Filesystem eine CPU aktivieren
und deaktivieren. Sichtbar am Inhalt der Datei /proc/interrupts sind zunächst vier Kerne online. An der
Prozesstabelle lassen sich die auf dieser CPU lokalisierten Kernel-Threads entdecken. Wird die CPU jetzt
deaktiviert (Kommando echo "0" >online), taucht die CPU in der Datei /proc/interrupts nicht mehr auf.
Auch die cpu-spezifischen Kernel-Threads sind verschwunden. Wenn Sie bei einer CPU die Datei online
nicht vorfinden bedeutet das übrigens nur, dass sich diese CPU auch nicht deaktivieren lässt.
Abbildung 5-3. Deaktivieren von Rechnerkernen.
In Abgrenzung zu den CPU-Sets setzt die CPU-Isolation bereits beim Booten ein. Per Boot-Parameter
isolcpus= lassen sich einzelne Rechnerkerne (mit Ausnahme von CPU 0) direkt deaktivieren und vom
Load-Balancing (Verteilen der Last zwischen den vorhandenen Prozessoren) ausnehmen. Hierbei handelt
es sich um eine sehr rudimentäre Methode, die als wesentlichen Vorteil die kürzere Bootzeit mit sich bringt.
Ansonsten spricht wohl noch die einfache Handhabung für den Bootparameter. Sollen auf einer Mehrkernmaschine mehrere Prozessoren für die Abarbeitung zeitkritischer Rechenprozesse eingesetzt werden, kann
– ander als bei den cgroups – auf Basis von isolcpus keine Lastverteilung innerhalb der isolierten Kerne
aktiviert werden. Außerdem werden auch auf den auf diese Weise isolierten Rechnerkernen ISRs, Tasklets
und Timer abgearbeitet; etwas, das sich allenfalls durch Verwendung des Bootparameters max_cpus= verhindern lässt. Verständlich, dass bei den wenigen Alleinstellungsmerkmalen die Entwickler diskutieren, dieses
Feature aus dem Kernel zu entfernen.
Ähnlich wie Rechenprozesse besitzen auch Interrupts im Kernel ein ihnen zugewiesenes Attribut, welches
die Prozessoren in Form der Bitmaske auf die Rechnerkerne festlegt, auf die die zugehörige InterruptService-Routine abgearbeitet werden darf. Dieses lautet im Kernel relativ einfach nur mask. Wenn es um
Hardware-Interrupts geht, kann der Kernelprogrammierer zum einen beim Einhängen der Interrupt-ServiceRoutine festlegen, dass die ISR nicht "balanced", also auf unterschiedlichen Rechnerkernen abgearbeitet
wird. Vom Userland aus kann das Attribut mask wieder einmal über das Proc-Filesystem manipuliert werden. Im folgenden Beispiel wird auf einer Vierkern-Maschine (smp_affinity=0x0f) die CPU-0 von der Verarbeitung des Interrupts 1 ausgenommen (smp_affinity=0x0e):
99
Kapitel 5. Echtzeitarchitekturen
root@lab01:# cat irq/1/smp_affinity
0f
root@lab01:# echo "0x0e"> irq/1/smp_affinity
root@lab01:# cat irq/1/smp_affinity
0e
Die Abarbeitung von Threads und Interrupts durch die vorhandenen Prozessoren lässt sich mit den
beschriebenen Methoden sehr gut kontrollieren. Softirqs, Tasklets und Timer, aber auch Workqueues sind
hingegen anders gelagert. Typischerweise werden Softirqs und Tasklets auf der CPU abgearbeitet, auf
der die Interrupt-Service-Routine angestoßen wurde. Hier muss der Systemarchitekt also sorgfältig die
IRQ-Affinität für einen mit harten Echtzeitanforderungen versehenen Prozess auf die für Realzeitaufgaben
reservierte CPU legen. Da Interrupts heutzutage allerdings gemeinsam (unterschiedliche Hardware löst
den gleichen Interrupt aus) genutzt werden, muss der Konstrukteur dafür sorgen, dass die zugehörige
Interruptleitung exklusiv nur von dem einen Kern genutzt wird. In solch einem Fall ist ein Eingriff in das
PCI-Handling erforderlich, was die Portabilität erschwert.
HINTERGRUND: Redhawk-Linux: Concurrent Computer Systems bietet eine Linux-Variante an, bei
der auf einem Mehrprozessorsystem Echtzeittasks ein eigener Prozessor exklusiv zur Verfügung steht.
Dies wird durch zwei Technologien ermöglicht:
1. Der Möglichkeit, eine CPU abzuriegeln (schielding). Das Abriegeln verhindert, dass auf dieser CPU
andere als spezifisch zugewiesene Interrupts und Rechenprozesse abgearbeitet werden.
2. Der Möglichkeit, Interrupts und Rechenprozesse einer CPU zuzuordnen (setzen einer
CPU-Affinität).
Concurrent garantiert bei einer abgeriegelten CPU eine Task-Latenzzeit von weniger als 30 Mikrosekunden. Die Systeme werden nur als Komplettsysteme bestehend aus Hard- und Software verkauft. Preis:
zwischen 5.000 und 100.000 Euro.
Vorteile gegenüber RT-Linux und RTAI: Es wird kein zusätzlicher Echtzeitkernel benötigt, dem Programmierer stehen die bekannten APIs zur Verfügung. Nachteil: Das Prinzip funktioniert nur auf einer
Mehrprozessormaschine, nur eine Auswahl der Linux-Systemfunktionen sind echtzeitfähig.
5.3. Mehrkernelansatz
Weite Verbreitung zur Realisierung hoher Anforderungen an das Zeitverhalten im Kntext mit einem Standardbetriebssystem hat auch die Möglichkeit gefunden, das Standardbetriebssystem als einen Rechenprozess eines Echtzeitkernels ablaufen zu lasen. Hierbei handelt es sich um eine Form der Virtualisierung,
wobei der Echtzeitkernel den Hypervisor darstellt. Der Echtzeitkernel kontrolliert insbesondere das SPerren
und Freigeben von Interrupts. Das Verfahren ist in der Implementierung zwar vergleichsweise aufwandsarm,
nachteilig dabei ist aber, dass sämtliche Tasks, die unter dem Standardbetriebssystem laufen, nicht echtzeitfähig sind. Insbesondere ist es also nicht möglich, dass eine Task des Echtzeitkerns eine Komponente des
Standardbetriebssystems nutzt (z.B. tcp/ip), ohne dass diese Echtzeittask ihre Echtzeitfähigkeit (ihren Determinismus) verliert. Implemenetierungen finden sich übrigens für Linux (RT-Linux, RTAI), als auch für
Windows-NT.
HINTERGRUND: RT-Linux.: RT-Linux ist am Department of Computer Science der Universität New
Mexico entwickelt worden. RT-Linux selbst ist zunächst ein kleiner Echtzeitkern, der Linux in einer eige-
100
Kapitel 5. Echtzeitarchitekturen
nen Task ablaufen läßt. Linux ist dabei die Idletask , die nur dann aufgerufen wird, wenn keine andere
Echtzeittask lauffähig ist.
Jeder Versuch der Linux-Task Interrupts zu sperren (um selbst nicht unterbrochen zu werden) wird vom
Echtzeitkern abgefangen. Dazu emuliert der Echtzeitkern die komplette Interrupt-Hardware. Versucht
Linux einen Interrupt zu sperren, so merkt sich der Echtzeitkern selbiges, ohne den Interrupt wirklich zu
sperren. Tritt nun ein realer Interrupt auf, wird vom Realzeitkern entweder ein Realzeit-Interrupt-Handler
(realtime handler) aufgerufen. Andernfalls wird der Interrupt als pending markiert. Gibt die Linux-Task
Interrupts wieder frei, emuliert RT-Linux den als pending markierten Interrupt für die Linux-Task.
Linux Processes
Real−Time
RT−
Fifos
Linux
Tasks
Real−Time Kernel
Interrupt control hardware
Abbildung 5-4. RT-Linux Systemarchitektur [Yod99]
Damit können Interrupts die Linux-Task in jedem Zustand unterbrechen und Linux als solches ist nicht
in der Lage, irgendwelche Latency-Times zu verursachen.
LINUX ALS IDLE−TASK EINES RT−KERNELS
REDHAWK LINUX
b)
Linux Tasks
CPU−Shielding
a)
Linux Tasks
Real−Time
Tasks
RT− Fifos
Linux−
Kernel
Real−Time
Linux−Tasks
Linux−Kernel
CPU−Affinity
Real−Time Kernel
CPU
Interrupts und RT−Interrupts
CPU 1
Interrupts
CPU 2
RT−Interrupt
Abbildung 5-5. Unterschied RT-Linux/RedHawk-Linux
Interessant ist das Konzept Standard- und Echtzeitbetriebssystem parallel zu fahren, nur dann, wenn
zwischen beiden auch ein Informationsaustausch möglich ist. RT-Linux bietet dazu zwei Möglichkeiten
an:
1. Real-Time-Fifos und
2. Shared-Memory
Die Wartezeit unter RT-Linux beträgt weniger als 15µs und periodische Tasks werden nicht mehr als
35µs verzögert.
101
Kapitel 5. Echtzeitarchitekturen
102
Kapitel 6. Echtzeitsysteme in
sicherheitskritischen Anwendungen
Dem Betreiber eines Webschops, bei dem über das Internet Kunden ihre Waren elektronisch aussuchen und
bestellen (eCommerce) ist es wichtig, dass der Webshop ständig verfügbar ist. Welche Maßnahmen kann
er aber ergreifen, um eine möglichst hohe (durchgehende) Verfügbarkeit zu gewährleisten? Im einfachsten
Fall kauft er sich eine zweite Webserver-Hardware. Fällt der erste Webserver - und damit der Webshop aus, schaltet er die redundante Hardware ein und der Shop ist wieder verfügbar.
Ähnlich wie beim Webshop existieren auch an Bord moderner Flugzeuge mehrere redundante Rechner.
Dadurch wird verhindert, dass das Flugzeug abstürzt, wenn ein Steuerungsrechner ausfällt. Wie aber handelt
ein Pilot, der gerade in Frankfurt am Main zu einem Atlantikflug mit Ziel JFK in New York gestartet ist,
wenn ein Rechner ausfällt? Da der Pilot für die Sicherheit von Passagieren und Maschine verantwortlich
ist, wird er sofort den nächstgelegenen Flughafen anfliegen. Trotz Ausfall eines Steuerungssystems ist das
Flugzeug zwar noch voll flugfähig, der Ausfall allerdings des zweiten Rechners könnte katastrophale Folgen
haben.
Diese beiden Beispiele zeigen, dass man redundante Systeme sowohl zur Steigerung der Verfügbarkeit, als
auch zur Steigerung der Sicherheit verwenden kann. In vielen Einsatzgebieten, z.B. beim Space-Shuttle,
benötigt man die hohe Verfügbarkeit ebenso wie eine hohe Sicherheit.
Bevor im folgenden auf Verfügbarkeits- und Sicherheitskonzepte eingegangen wird, müssen die grundlegenden Begriffe geklärt werden.
6.1. Begriffsbestimmung
Zuverlässigkeit
“Fähigkeit einer Betrachtungseinheit, innerhalb der vorgegebenen Grenzen denjenigen durch den
Verwendungszweck bedingten Anforderungen zu genügen, die an das Verhalten ihrer Eigenschaften
während der gegebenen Zeitdauer gestellt sind.” [DIN 40041]
Sicherheit
“Sicherheit ist eine Sachlage, bei der das Risiko nicht größer als das Grenzrisiko ist.” [DIN/VDE 31
000 Teil2] Als Grenzrisiko ist dabei das größte, noch vertretbare Risiko zu verstehen [Hal99].
Verfügbarkeit
Die Wahrscheinlichkeit, dass ein System innerhalb eines spezifizierten Zeitraums funktionstüchtig
(verfügbar) ist.
Unverfügbarkeit
Die Wahrscheinlichkeit, dass ein System innerhalb eines spezfizierten Betrachtungszeitraums funktionsuntüchtig (unverfügbar) ist.
6.2. Mathematische Grundlagen
103
Kapitel 6. Echtzeitsysteme in sicherheitskritischen Anwendungen
Ein junger Student erwirbt einen Rechner. Leider zeigen sich gleich nach etwa 4 Betriebsstunden erste
Mängel. Für die Fehlersuche, Ersatzteilbeschaffung und Reparatur benötigt der Student 1 Stunde. Danach
läuft der Rechner weitere 96 Stunden problemlos, um abermals auszufallen. Diesmal dauert die Reparatur
1.5 Stunden. Weitere 96 Stunden später fällt der Rechner erneut aus, ist aber nach bereits einer halben
Stunden wieder einsatzbereit. Die Zeit bis zum nächsten Ausfall (TBF, Time Between Failure) beträgt dann
72 Stunden, die Zeit zur Reparatur (TTR, Time To Repair) beträgt eine halbe Stunde. Insgesamt ergeben
sich über einen Beobachtungszeitraum von 376 Stunden die folgenden Kennzahlen:
TBF
TTR
96h
1.5h
96h
0.5h
72h
0.5h
48h
0.5h
44h
1h
16h
1h
4h
1h
Aus diesen Werten berechnet der Student die durchschnittliche Zeit bis zum nächsten Ausfall (MTBF, Mean
Time Between Failure):
MTBF=376h/7=53.7h
Ebenso berechnet er die durchschnittliche Reparaturzeit (MTTR, Mean Time To Repair):
MTTR = 6/7 = 0.857h = 51 Minuten
Der Student berechnet weiterhin die Wahrscheinlichkeit dafür, dass System verfügbar anzutreffen1. Dazu
setzt er die MTBF in Beziehung zur Gesamtbetriebszeit:
p = MTBF/(MTBF+MTTR) = 376/382 = 98.43%
beziehungsweise die Wahrscheinlichkeit, dass System im unverfügbaren Zustand anzutreffen:
q = MTTR/(MTBF+MTTR) = 6/382 = 1.57%
Eine Komponente kann sich in den Zuständen “verfügbar” (funktionstüchtig) oder “ausgefallen” befinden.
Eine Komponente wird im ausgefallenen Zustand im Regelfall repariert, damit das System wieder verfügbar
wird. Die Zeit, während der das System ausgefallen ist, wird daher auch als “Reparaturzeit” (TTR, Time
To Repair) bezeichnet. Die Zeit, in der das System verfügbar ist, wird als die Zeit zwischen den Ausfällen
(TBF, Time Between Failure) bezeichnet.
Summiert man die Zeiten, in denen die Komponente verfügbar ist, über einen genügend langen Zeitraum auf
und bildet hiervon das arithmetische Mittel, kann man die durchschnittliche Verfügbarkeitsdauer (MTBF,
Mean Time Between Failure) angeben. Das gleiche gilt für die mittlere Dauer der Unverfügbarkeit:
Gleichung 6-1. MTBF und MTTR
n
MTBF = lim
n
1
oo
n
Σ TBF
i=1
i
n
MTTR = lim
n oo
104
1
n
Σ TTR
i=1
i
Kapitel 6. Echtzeitsysteme in sicherheitskritischen Anwendungen
Zyklus 1
v
Zyklus 2
a
TBF
v
a
Zyklus 3
v
a
TBF
TTR
v = verfügbar
a = ausgefallen
TBF: Time Between Failure
TTR: Time To Repair
Abbildung 6-1. Zeitverlauf des Systemzustands [Färber94]
Möchte man die Wahrscheinlichkeit angeben, mit der sich die Komponente im Zustand “verfügbar” (p,
Dauerverfügbarkeit) oder “ausgefallen” (q, Dauerunverfügbarkeit) befindet, muss man die Zeiten der Verfügbarkeit (die Summe der Betriebszeiten) in Beziehung zur Gesamtzeit (Beobachtungszeitraum) setzen,
bzw. für die Dauerunverfügbarkeit die Ausfallzeiten in Beziehung zum Beobachtungszeitraum setzen. p
und q lassen sich mathematisch auch über die MTBF und die MTTR darstellen.
Gleichung 6-2. Dauerverfügbarkeit und Dauerunverfügbarkeit
p=
MTBF
MTTR + MTBF
(Verfügbarkeit)
q=
MTTR
MTTR + MTBF
(Unverfügbarkeit)
mit p + q = 1
Die (im Mittel) erwartete Zahl von Ausfällen wird mit λ (Ausfallrate) beschrieben:
Gleichung 6-3. Ausfallrate
λ=
1
MTBF
Zeitlich betrachtet sind die Ausfälle nicht gleichverteilt. Nach einer Reparatur ist die Wahrscheinlichkeit
sehr groß (nahezu 100%), dass das System wieder funktionstüchtig, sprich verfügbar ist; nach einer genügend langen Zeit ist diese Wahrscheinlichkeit sehr gering (nahe 0%). Die Relation zwischen Verfügbarkeit
und Zeit gibt die Gleichung Verfügbarkeit in Abhängigkeit von der Zeit an:
Gleichung 6-4. Verfügbarkeit in Abhängigkeit von der Zeit
p(t) =
ρ
λ
−(λ+ρ)t
+
e
ρ+λ
ρ+λ
105
Kapitel 6. Echtzeitsysteme in sicherheitskritischen Anwendungen
Bild Verfügbarkeitsfunktion [Färber 1994] stellt die Funktion p(t) für ausgewählte Reparatur- bzw. Ausfallraten dar.
p(t)
ρ=
ρ=kλ
Dauerverfügbarkeit
ρ
1
=
1
λ +ρ
1+
k
ρ=0
t
Abbildung 6-2. Verfügbarkeitsfunktion [Färber 1994]
Für eine gegebene Reparatur- und Ausfallrate ungleich Null ergibt sich die Dauerverfügbarkeit nach einem
entsprechend langen Zeitraum. Wird das System nicht reparariert (Reparaturrate=0), sinkt die Verfügbarkeit
auf 0, ist die Reparaturzeit beliebig klein (0), ist das System ständig verfügbar.
q
Frühausfälle
statistische Ausfälle
Verschleiß
t
Abbildung 6-3. Badewannenkurve [Färber94]
Die Gleichung stellt die Verfügbarkeitswahrscheinlichkeit nur in erster Näherung dar. In der Realität gilt
vielmehr die sogenannte Badewannenkurve (Bild Badewannenkurve [Färber94]). Ist ein System neu, gibt
es eine hohe Wahrscheinlichkeit dafür, dass das System ausfällt. Man spricht von den sogenannten Frühausfällen. Danach kommt die Phase der statistischen Ausfälle, die durch die angegebene Gleichung beschrieben
wird. Bei einem hohen Alter des Systems kommt es durch Alterung und Verschleiß erneut vermehrt zu Ausfällen.
106
Kapitel 6. Echtzeitsysteme in sicherheitskritischen Anwendungen
λ
funktions−
p(t)
tüchtig
ausgefallen q(t)
ρ
Abbildung 6-4. Systemzustand bezüglich der Funktionstüchtigkeit [Färber94]
Die bisherigen Betrachtungen gelten für eine Komponente. Möchte man die Verfügbarkeit eines Systems
angeben, kennt aber nur die Verfügbarkeit der Einzelkomponenten, läßt sich die Gesamtverfügbarkeit nach
folgenden Regeln berechnen.
K
K
K
Abbildung 6-5. Serienbetrieb
Gleichung 6-5. Verfügbarkeit bei Serienbetrieb
n
p
gesamt
=
Πp
i
i=1
für q << 1:
n
n
p
gesamt
= 1−q
gesamt
=
Π(1−q ) ≈1−Σq
i=1
i
i=1
i
n
q
gesamt
≈
Σq
i=1
i
mit p = Verfügbarkeit
q = Unverfügbarkeit
n
λgesamt =
Σλ
i=1
i
λ = Ausfallrate
Serienbetrieb (Abhängige Komponenten). Ist das System nur dann verfügbar, wenn alle Einzelkomponenten verfügbar sind (beispielsweise sei ein Rechnersystem nur dann verfügbar, wenn die CPU, der
Speicher und das Peripheriemodul verfügbar sind), spricht man von einem Serienbetrieb. Bei einem Serienbetrieb erhält man die Gesamtverfügbarkeit durch Multiplikation der Einzelverfügbarkeiten. Das Ersatzschaltbild für einen Serienbetrieb ist in Bild Serienbetrieb dargestellt. Sind die Unverfügbarkeiten qi
der Komponenten darüber hinaus sehr klein, läßt sich die Gesamtunverfügbarkeit auch als Summe der
Einzelunverfügbarkeiten angeben. Die Ausfallrate des Gesamtsystems ist die Summe aus den Ausfallraten
der Einzelkomponenten.
Bei einem Serienbetrieb ist die Gesamtverfügbarkeit kleiner als die kleinste Einzelverfügbarkeit.
Parallelbetrieb (redundante Komponenten). Ist das System verfügbar, sobald eine von mehreren Komponenten verfügbar ist (redundantes System), spricht man von einem Parallelbetrieb. Bei dem Parallelbetrieb ergibt sich die Gesamtunverfügbarkeit aus dem Produkt der Einzelunverfügbarkeiten. Damit wird die
Gesamtunverfügbarkeit deutlich kleiner als die Einzelverfügbarkeiten.
107
Kapitel 6. Echtzeitsysteme in sicherheitskritischen Anwendungen
K
K
K
Abbildung 6-6. Parallelbetrieb
Gleichung 6-6. Unverfügbarkeit bei Parallelbetrieb
n
q
gesamt
p
gesamt
=
Πq
i
i=1
= 1−q
gesamt
6.3. Redundante Systeme
Im Regelfall besteht ein System nicht nur aus abhängigen oder redundaten Komponenten, sondern
aus einer Mixtur von beiden. Redundante Systeme benötigen beispielsweise eine Koppeleinrichtung,
über die die Ergebnisse der beiden Komponenten weitergeführt werden. Damit ergibt sich das in Bild
Parallelbetrieb mit Koppelkomponente dargestellte Ersatzschaltbild.
K
K
K
Abbildung 6-7. Parallelbetrieb mit Koppelkomponente
Merke: Zur Berechnung von Verfügbarkeiten erstellt man ein Verfügbarkeitsersatzschaltbild nach dem
vorgestellten Schema. Dabei ist zu beachten, welche Anforderungen an die Betriebsart (sichere Betriebsart gegenüber einem möglichst verfügbaren System) das System zu erfüllen hat.
Ein redundantes System kann entweder mit hoher Verfügbarkeit betrieben werden, oder als besonders
sicheres System. Stehen beispielsweise 2 redundante Rechner zur Verfügung, lassen sich diese in einem
sogenannten 1:2 (sprich 1 aus 2) System oder einem 2:2 (2 aus 2) System betreiben. Bei einem 1:2 System
muss ein Rechner von zweien verfügbar sein, damit das Gesamtsystem verfügbar ist, bei einem zwei aus
zwei System (sicherheitsgerichtet) müssen beide Rechner verfügbar sein, damit das Gesamtsystem verfügbar ist.
108
Kapitel 6. Echtzeitsysteme in sicherheitskritischen Anwendungen
Bei einem sicherheitsgerichteten System muss man daher zur Bestimmung der Gesamtverfügbarkeit das Ersatzschaltbild als Serienbetrieb angeben (Rechner 1 und Rechner 2 und Koppelelektronik müssen verfügbar
sein).
Wird das 2:2 System nach dem Schema der möglichst hohen Verfügbarkeit betrieben, setzt man einen
Parallelbetrieb der beiden Rechner in Serie mit der Koppelelektronik an (Rechner 1 oder Rechner 2 muss
verfügbar sein und die Koppelelektronik muss verfügbar sein).
Im Verfügbarkeitsbetrieb werden redundante Rechner nach unterschiedlichen Strategien betrieben:
cold standby (statische Redundanz)
Fällt eine Komponente aus, wird die redundante Komponente aktiviert. Da die bis zum Zeitpunkt
des Ausfalls deaktivierte Komponente nicht den aktuellen Betriebszustand (z.B. des technischen
Prozesses) kennen kann, wird im Regelfall ein kompletter Neuanlauf durchgeführt.
hot standby (dynamische Redundanz)
Die redundante Komponente ist aktiviert und kennt den aktuellen Systemzustand des Gesamtsystems.
Ausgaben bzw. Ergebnisse stammen jedoch nur von der Hauptkomponente. Sobald ein Fehler in der
Hauptkomponente identifiziert wird, wird auf die redundante Komponente umgeschaltet.
Doppelsystem
Die redundanten Komponenten bearbeiten jeweils Teilaufgaben (damit gibt es keine Hauptkomponente). Wird bei einem System ein Fehler festgestellt, übernimmt die redundante Komponente die
Aufgaben der defekten Komponente.
6.4. Weitere Maßnahmen zur
Zuverlässigkeitssteigerung
Um insbesondere die Verfügbarkeit eines Systems zu erhöhen (man spricht in diesem Kontext von High
Availability) müssen in unterschiedlichen Bereichen Maßnahmen getroffen werden:
•
•
•
•
Infrastruktur
Hardware
Software
Management
109
Kapitel 6. Echtzeitsysteme in sicherheitskritischen Anwendungen
Wechselspannung
Gleichspannung
Interrupt
wird
ausgelöst
geglättete Spannung
Betriebsspannung
Power−fail−Spannung
Minimal−Spannung
z.B. 10 ms
Abbildung 6-8. Power-fail-Interrupt
Infrastruktur
Zu den Maßnahmen bezüglich der Infrastruktur gehören:
• Sicherung der Stromversorgung. Damit ein Ausfall der Stromversorgung nicht zum Ausfall des
Rechnersystems führt, wird dieses entweder durch eine unterbrechungsfreie Stromversorgung
(USV), durch ein Notstromaggregat oder durch beides gesichert.
•
•
muss der Betrieb bei Stromausfall nicht zwangsläufig aufrecht erhalten werden, reicht es oft auch
schon aus, den Stromausfall zu dedektieren und am Rechner einen Interrupt auszulösen (Powerfail-Interrupt). Der Interrupt selbst wird vom Netzteil ausgelöst. Das Netzteil speichert (meist in
Kondensatoren) Energie. Damit werden die Spannungslücken geglättet, die bei der Gleichrichtung der Wechselspannung entstehen (siehe Bild Power-fail-Interrupt). Fällt jedoch die Spannung
unter eine kritische Betriebsspannung (Power-fail-Spannung), wird ein Interrupt ausgelöst. Dem
Steuerungssystem bleibt jetzt noch eine kurze Zeitspanne, innerhalb derer der technische Prozess in
einen sicheren Zustand und die Steuerung in einen konsistenten Zustand gebracht werden müssen.
Klimatisierung. Eine der wichtigsten Ursachen für Rechnerausfälle stellt die Überhitzung dar. Sie
führt zunächst zu schwer zu dedektierende sporadische Fehler, später schließlich zum Totalausfall.
Ein optimales Betriebsklima wird durch Klimatisierung erreicht. Auch gegen den Ausfall der dazu
notwendigen Klimaanlage sollten Maßnahmen (z.B. mobile Ersatz-Klimageräte) getroffen werden.
Schutz vor Feuer, Nässe, Blitzschlag und Einbruch. Zum Schutz vor Feuer, Blitzschlag, Nässe,
Einbruch oder anderen mechanischen Beschädigungen werden Rechner meist in Rechnerräume untergebracht. Zutritt zu diesen Räumen ist nur in den seltensten Fällen (z.B. Austausch von Rechnerkomponenten, Austausch von Datenbändern) notwendig.
Hardware
Die möglichen Hardeware-Maßnahmen finden einmal auf der Systemebene und ein zweites Mal auf
der Komponentenebene statt:
• Redundanz der Systemkomponenten. Durch Vervielfachung (meist Verdoppelung) kann die Verfügbarkeit erhöht werden. Problematisch bei diesem Ansatz ist jedoch, den Ausfall zu dedektieren,
den Systemzustand des ausgefallenen Systems auf die redundante Ersatzukomponente zu übertragen
und diese schließlich zu aktivieren.
Die Überwachung einer redundant ausgelegten Komponente muss auf Systemebene erfolgen. Dazu
kann beispielsweise eine Überwachungs- bzw. Koppeleinrichtung dienen. Diese kann
• aktiv oder
• passiv
die Komponente überwachen. Wird die Komponente passiv überwacht, muss die redundante
Komponente in periodischen Abständen ein Lifesign (Lebenszeichen) übermitteln. Dieses Lifesign
110
Kapitel 6. Echtzeitsysteme in sicherheitskritischen Anwendungen
setzt in der Überwachungseinrichtung einen Watchdog zurück. Wird der Watchdog nicht rechtzeitig
zurückgesetzt, schaltet die Überwachungseinrichtung auf die redundante Komponente um.
Bei der aktiven Überwachung wird die zu überwachende Komponente periodisch mit einer Anforderung beschickt, die das System im funktionstüchtigen Zustand beantwortet. Beispiel: Eine Netzkomponente kann dadurch überwacht werden, indem man ihr regelmässig ein “echo request” Paket
sendet.
Da die Komponenten meistens interne Zustände besitzen, müssen diese beim Ausfall bekannt sein
und auf die redundante Ersatzkomponente übertragen werden (Vermeidung von Informationsverlust). Dieses stellt die größte Herausforderung dar:
• Bei Systemen im Hot Standby wird der interne Zustand regelmässig mit dem Ersatzsystem
abgeglichen. Dieser Abgleich muss so häufig erfolgen, dass ein Umschalten ohne
Informationsverlust zu jedem beliebigen Zeitpunkt möglich ist.
• Die wohl verbreitetste Technologie ein redundantes System im Hot Standby zu betreiben ist
die, dass das Ersatzsystem parallel mit der aktiven Komponente arbeitet. Es bekommt die gleichen Eingangsinformationen und berechnet mit den gleichen Applikationen die Ausgabewerte.
Die Überwachungs- bzw. Koppeleinrichtung leitet aber diese Ausgabewerte solange nicht an den
technischen Prozess (bzw. die nachfolgende Komponente) weiter, solange die aktive Komponente
funktionstüchtig ist (Beispiel Space Shuttle).
• Im Cold Standby muss die Komponente ihren Systemzustand ständig (oder zumindest an den
relevanten Punkten) auf ein Medium sichern, auf das das Ersatzsystem auch beim Ausfall der
Komponente Zugriff hat. Hierbei ist auch der Fall zu berücksichtigen, dass sowohl die Komponente als auch das Medium, auf das der Zustand gesichert wird, ausfallen kann.
•
•
Redundanz der Einzelkomponenten. Auf Komponentenebene werden:
• CPU
• Speicher
• Festplatten (RAID) und
• Netzteile
redundant ausgelegt.
Weitere Maßnahmen die nicht auf Redundanz beruhen. Während sich durch Redundanz die
Verfügbarkeit eines Gesamtsystems erhöhen kann, kann die Verfügbarkeit der Einzelkomponenten
durch folgende Maßnahmen gesteigert werden:
• Auswahl zuverlässiger Bauelemente (z.B. Mil-Varianten).
• Steigerung der mechanischen Festigkeit (Vibrationsschutz).
• Fixierung löslicher Elemente (Lackierung von Schrauben).
• Vermeidung von Steckkontakten.
• Wärmeschutz.
• Betriebsdatenüberwachung der Komponente. Bei dieser Maßnahme werden Temperaturen und
Spannungen innerhalb der Komponente überwacht und geregelt. Eine zu hohe Temperatur (Überhitzung) bei einem Rechnerkern kann beispielsweise dazu führen, dem Prozessor durch Runtertakten weniger Leistung abzuverlangen. Eine Betriebsdatenerfassung wird bereits bei handelsüblichen PC-Motherboards angeboten.
• Vorbeugende Wartung.
Software
Um eine hohe Verfügbarkeit gewährleisten zu können sind auch softwareseitig eine Reihe von Maßnahmen notwendig:
• Software ermöglicht die Überwachung der Systemkomponenten bzw. auch der Diagnose der
Fehlerursache.
111
Kapitel 6. Echtzeitsysteme in sicherheitskritischen Anwendungen
•
•
Die Umschaltung und der Abgleich zwischen dem aktiven System und dem Ersatzsystem ist/muss
im Regelfall durch Software unterstützt sein.
Um Informationsverlust bei Ausfall eines Systems vorzubeugen muss eine Backupstrategie für
Daten und Systemzustände existieren.
Management
Managementseitig sind folgende Dinge zu regeln:
• Wartung: Es ist dafür zu sorgen, dass die kritischen Systeme ständig überwacht und gewartet werden. Auch der regelmässige Austausch wichtiger (alternder) Komponenten kann sinnvoll sein. Ein
großes und oft wenig beachtetes Problem beim Austausch von Komponenten besteht - gerade in der
Automatisierungstechnik mit ihren langen Systemlaufzeiten - darin, dass Ersatzkomponenten nicht
mehr verfügbar sind. Gibt es Nachfolgeprodukte muss darauf geachtet werden, dass diese den gleichen Hardware- und Softwarestand, wie die im System (in der Anlage) verwendeten Komponenten,
haben.
•
•
Da ältere Komponenten oftmals nicht mehr zu kaufen sind, legen sich viele Firmen Ersatzteile ins
Lager. Fällt eine Komponente aus, wird das Ersatzteil aus dem Lager eingesetzt. Aber auch bei
diesem Verfahren muss das Lager gewartet werden. Sind beispielsweise die Softwarestände einer
Komponente im System und der Ersatzkomponente im Lager nicht auf dem gleichen Stand, kann es
Probleme geben, wenn die aktive Komponente ausfällt und das Ersatzteil aus dem Lager eingebaut
wird. Um also Lager und System auf dem gleichen Stand zu halten, sind organisatorische (management) Maßnahmen notwendig.
Staffing: Fällt das System oder auch nur eine Komponente des Systems aus, sind Eingriffe durch
den Menschen notwendig, um das betrachtete System wieder in den funktionstüchtigen Zustand zu
bringen: Die Fehlerursache muss festgestellt und analysiert werden, redundante Systeme müssen
aktiviert werden und Ersatzteile eingebaut werden. Aus diesem Grunde ist dafür zu sorgen, dass
genügend kompetente Mitarbeiter zumindest erreichbar und in kürzester Zeit vor Ort sind.
Notfallpläne: Um Stillstands- bzw. Ausfallzeiten zu verkürzen sind insbesondere Notfallpläne
unumgänglich. Diese beinhalten strukturiert und Punkt für Punkt (Listen) alle Maßnahmen die
notwendig sind, um in der kürzest möglichen Zeit die Systeme wieder in den Zustand verfügbar zu
überführen. Notfallpläne beinhalten:
• Eine Fehlerklassifizierung: Nicht alle auftretenden Fehler sind gleich kritisch. Insbesondere lassen
sich nicht tolerierbare Fehler von den tolerierbaren Fehlern unterscheiden. Unter tolerierbare
Fehler oder Fehlertoleranz versteht man den Umstand, dass trotz des Fehlers das System dennoch
die geforderte Funktion erbringen kann (weil beispielsweise automatisch auf ein Ersatzsystem
umgeschaltet wird).
• Ausgehend vom Fehlerbild enthalten die Notfallpläne Listen, die zur Identifikation der defekten
Komponente führen (Fehlerlokalisierung, Diagnose).
• Notfallmaßnahmen
• Die Fehlerkritikalität bestimmt auch die notwendige Informationspolitik: Wer (Techniker, Manager, Kunden) ist bei welchem Fehler zu welchem Zeitpunkt zu informieren.
Fußnoten
1. Natürlich reicht der kurze Beobachtungszeitraum nicht aus, um eine gesicherte statistische Aussage
über die Wahrscheinlichkeiten durchzuführen.
112
Kapitel 7. Formale Beschreibungsmethoden
7.1. Daten- und Kontrollflussdiagramme
Temp.
Regelung
Druck
Regeleung
Technischer
Prozeß
Realzeit−Steuerung
Rechenprozesse
Abbildung 7-1. Abbildung technischer Prozesse auf Rechenprozesse
Bei
der
Abbildung
technischer
Prozesse
auf
Rechenprozesse
(Bild
Abbildung technischer Prozesse auf Rechenprozesse)
werden
zusammengehörige
Zustandsgrößen und Stellwerte der technischen Prozesse (z.B. der Temperaturwert und die Stellgröße für
die Heizung) auf logische Einheiten, eben den Rechenprozess, abgebildet.
Rechenprozesse
Datenspeicher
Datenfluß
Datenquelle oder Datensenke
Kontrollfluß zwischen R−Prozessen
Abbildung 7-2. Elemente der Datenflussdiagramme
Zur
Darstellung
der
Rechenprozess-Architektur
werden
Datenflussdiagramme
(Bild
Elemente der Datenflussdiagramme) eingesetzt. Folgende grafische Elemente können verwendet werden:
Rechenprozesse
Rechenprozesse (Jobs, Tasks, Threads) werden als Kreise dargestellt. Sie verarbeiten eingehende Daten
(ankommende Kanten) zu Ausgaben (abgehende Kanten). Für ihre Aufgabe benötigen sie auch lokale
Daten, die aber für die Umgebung nicht sichtbar sind.
113
Kapitel 7. Formale Beschreibungsmethoden
Rechenprozesse können schrittweise verfeinert werden. In diesem Fall erhalten Sie zur Identifikation eine Kennung (meist ein einfacher Buchstabe). Unter dieser Kennung wird dann ein eigenes
Datenflussdiagramm angefertigt. Auf der untersten Verfeinerungsebene werden zur Beschreibung des
Rechenprozesses (und seiner Algorithmen) sogenannte Prozessspezifikationen angefertigt.
Datenspeicher
Für mehrere Prozesse sichtbare (also globale) Daten werden durch zwei parallele Striche dargestellt.
Sie stellen einen temporären Aufenthaltsort für Daten dar. Dieser wird benötigt, wenn der Entstehungszeitpunkt der Daten verschieden ist vom Nutzungszeitpunkt. Daten werden geschrieben und
gelesen.
Datenflüsse
Die eigentlichen Datenflüsse werden durch gerichtete Kanten (Pfeile) dargestellt. Als Beschriftung
dient die Bezeichnung der über die Kante fließenden Daten, nicht die Operation auf die Daten.
Datenquellen und Datenspeicher
Als Rechtecke oder ohne jegliche Umrahmung werden Datenquellen und Datensenken dargestellt. In
technischen Datenflussdiagrammen sind dies oft Sensoren und Aktoren.
Die ursprünglichen Datenflussdiagramme sind für die Darstellung von Echtzeitsystemen noch um weitere
Elemente, die den Kontrollfluss definieren, erweitert worden. So deutet ein gestrichelter Pfeil an, dass zwischen zwei Tasks eine Kontroll-Information (Events) ausgetauscht wird. Eine derartige Information kann
beispielsweise das Starten oder Beenden eines Prozesses, das Senden eines Events oder eines Signals, das
Schlafenlegen und das Aufwecken eines Rechenprozesses oder schließlich das Betreten und Verlassen eines
kritischen Abschnittes sein.
Zustandsinfo
Rennbahn
/dev/Carrera
Auto
Streckentyp,
Länge
Speed
Position
Strecke
Streckentyp,
Rennbahn
/dev/Carrera.other
Position
Fremd−
Auto
Abbildung 7-3. Einfaches DFD einer Carrerabahnsteuerung
Abbildung
Einfaches DFD einer Carrerabahnsteuerung
stellt
das
Datenflussdiagramm
einer Carrerabahnsteuerung dar. Die Verarbeitungseinheit Auto bekommt von der Rennbahn
Zustandsinformationen und berechnet daraus Geschwindigkeitswerte (Speed). Aufgrund der
Zustandsinformationen werden Streckentyp und Länge eines Streckensegments berechnet und in einem
Datenspeicher abgelegt.
Die Position des gegnerischen Fahrzeugs erhält der Rechenprozess Auto über den Rechenprozess Fremdauto. Dieser bekommt Positionsangaben in Form von Segmenttypen ebenfalls von der Rennbahn, allerdings über das Interface /dev/Carrera.other. Zur genauen Bestimmung des gegnerischen Fahrzeugs auf der
Strecke wird diese Information mit der Streckeninformation abgeglichen.
114
Kapitel 7. Formale Beschreibungsmethoden
digitalisiertes
Audio Audiosignal
Hardware
Erfas−
sung
Ko−
dierung
Rohdaten
Audiodaten
Pakete
Versand
Pakete
Netz
Sendepuffer
Konfiguration
Abbildung 7-4. DFD Audiostreaming
Abbildung DFD Audiostreaming stellt das vereinfachte DFD eines Audiostreaming-Systems dar. Hier ist
der Rechenprozess Erfassung für die zeitgenaue Aufnahme der digitalisierten Audiosignale zuständig.
Für die zeitgenaue Aufnahme der Daten ist eine Entkopplung dieser Aufgabe von der Weiterverarbeitung
notwendig. Aus diesem Grunde werden die Daten in einem Datenspeicher abgelegt und nicht direkt dem
Rechenprozess Kodierung übergeben. Diese Verarbeitungseinheit liest zu Beginn eine Konfigurationsdatei,
in der das zu verwendende Kodierungsverfahren samt seiner Parameter beschrieben ist. Entsprechend dieser
Informationen werden die Daten, die sich im Datenspeicher Audiodaten befinden, kodiert, paketiert und in
den Sendepuffer abgelegt. Auch das Senden muss aus Gründen der Echtzeitfähigkeit über einen Datenspeicher entkoppelt werden.
Anhand eines reinen Datenflussdiagramms sind keinerlei dynamischen Abläufe sichtbar. Das DFD gibt
eine statische Sicht der Dinge wieder. Dynamik kann partiell über die Erweiterung der Kontrollflüsse ausgedrückt werden. Kontrollflüsse drücken aus, welche Verarbeitungseinheit welche Verarbeitung auslöst.
Folgende Regeln sind bei der Erstellung eines Datenflussdiagramms zu beachten [kramer04]:
1. Jeder Datenfluss muss mit mindestens einem Rechenprozess verbunden sein.
2. Direkte Datenflüsse zwischen Datenquellen und Datensenken (auch untereinander) sind nicht erlaubt.
3. Direkte Datenflüsse zwischen Datenspeicher sind nicht möglich.
4. Datenspeicher müssen immer eine abgehende Kante besitzen.
5. Datenflüsse müssen immer gerichtet sein (Pfeilspitzen).
6. Unterschiedliche Datenflüsse zwischen zwei Rechenprozessen werden als separate Pfeile dargestellt.
7. Jeder Rechenprozess, jeder Datenfluss und jeder Datenspeicher muss bezeichnet sein (Ausnahme:
Datenflüsse zwischen Rechenprozess und Datenspeicher).
8. Beim Datenflussnamen muss es sich um ein Substantiv handeln. Kein Verb, darf inhaltlich keine Verarbeitung und keinen Vorgang beschreiben!
9. Rechenprozessbeschreibungen der Art „Daten verarbeiten“, „Datensatz erzeugen“ oder „Daten aufbereiten“ sind zu vermeiden.
7.2. Darstellung von Programmabläufen mit Hilfe von
Struktogrammen
Struktogramme dienen zur strukturierten Darstellung von Software. Sie ersetzen die früher verwendeten
Programmablaufpläne beziehungsweise Flussdiagramme.
115
Kapitel 7. Formale Beschreibungsmethoden
Programme setzen sich aus Anweisungen (Aktionen) zusammen und aus Bedingungen. Als besondere Anweisung sind
•
ein Funktionsaufruf (entspricht dem Sprung in ein Unterpgrogramm) und
•
der Rücksprung aus einer Funktion (Return) zu nennen.
Zähler inkrementieren
Init()
Anweisung
Fehlercode
Funktionsaufruf
Return
Bedingungen kommen in Programmiersprachen in verschiedenen Konstrukten vor:
•
als einfach Bedingung (IF-Abfrage),
•
als FOR-Schleife (der Schleifenkörper wird unter Umständen keinmal durchlaufen),
•
als DO-WHILE-Schleife (der Schleifenkörper wird mindestens einmal durchlaufen, auch
REPEAT-UNTIL genannt),
•
als WHILE-Schleife (der Schleifenkörper wird unter Umständen keinmal durchlaufen) und
•
als SWITCH-CASE Anweisung.
Solange AktEtage < Ziel
Schleifenbedingung
Aufwaerts()
Aufwaerts()
Schleifenkörper
Solange AktEtage < Ziel
For− oder While−Schleife
Do−Until−Schleife
Keine weiteres Ziel in
Vorzugsrichtung?
Ja
Richtung =
NachOben
Falls I gleich
Nein
Richtung =
NachUnten
Normale Bedingung
2
1
Etage[1]=1;
Etage[3]=1;
3
Etage[6]=1;
Switch−Case−Anweisung
Um eine Funktion mit Hilfe eines Struktogramms zu beschreiben, werden die einzelnen Blöcke aneinandergesetzt. Hierzu ein Beispiel:
i = 1;
1
Etage == 5 ?
Ja
Nein
2
VorzugsRichtung = NachUnten; 3
Solange Etage < 4
5
Led[Etage] = 1;
6
4
Etage++;
7
Zustand ausgeben
8
Die im Struktogramm angegebenen Aktionen (Anweisungen) werden von oben nach unten abgearbeitet.
Falls beispielsweise »etage=2« wäre, ergäbe sich der folgende Ablauf (1, 2, 5, 6, 7, 5, 6, 7, 8).
116
Kapitel 7. Formale Beschreibungsmethoden
i = 1;
1
Etage == 5 ?
Ja
Nein
2
VorzugsRichtung = NachUnten; 3
Solange Etage < 4
5
Led[Etage] = 1;
6
4
Etage++;
7
Zustand ausgeben
8
7.3. Petrinetze
Ein Petrinetz ist ein Modell zur Beschreibung nebenläufiger (paralleler) Prozessen. Eng verwandt mit den
Zustandsautomaten ermöglichen sie die Analyse der modellierten Systeme auf:
•
•
•
•
Erreichbarkeit von Systemzuständen (wie kann bei einem gegebenem Zustand ein zweiter Systemzustand
erreicht werden?).
Lebendigkeit bzw. Terminierung (werden die im System vorhandenen Ressourcen aufgebraucht?).
Totale Verklemmung (Stillstand des Systems).
Partielle Verklemmung (Stillstand von Systemteilen).
Vorbereich
Stelle
Übergang
Transition
Übergang
Nachbereich
Ein Petrinetz ist ein gerichteter Graph, der aus Knoten und Übergängen besteht. Bei Petrinetzen unterscheidet man zwei Knotenarten: die Stellen und die Transitionen. Stellen - üblicherweise als Kreise dargestellt
- entsprechen Zuständen, Transitionen, die als Balken oder Rechtecke visualisiert werden, entsprechen den
Aktionen bzw. Ereignissen. Die Knoten (Stellen und Transitionen) des Netzes werden durch Übergänge
(Kanten) miteinander verbunden, wobei eine Kante jeweils nur von einer Stelle zu einer Transition führen
darf, bzw. von einer Transition zu einer Stelle. Kanten zwischen gleichartigen Knoten (z.B. von Stelle zu
Stelle) sind nicht erlaubt (siehe Bild Zwei Transtionen direkt hintereinander sind nicht erlaubt.).
Stelle
Transition Transition
Abbildung 7-5. Zwei Transtionen direkt hintereinander sind nicht erlaubt.
117
Kapitel 7. Formale Beschreibungsmethoden
Formal betrachtet ist ein Petrinetz ein Tripel PN=(S,T,F), wobei S die nicht leere Menge der Stellen, T die
nicht leere Menge der Transitionen und F die Flußrelation (Übergänge) ist.
Markenfluß. Anders als reine Zustandsautomaten läßt sich bei Petrinetzen das dynamische Verhalten modellieren. Dazu existiert eine oder mehrere Marken (Markierung) M, die gemäß der Schaltbedingungen über
die Transitionen von Stelle zu Stelle in Nullzeit “fließen”. Die Anzahl der Marken im Netz ist nicht konstant, so fließen aus einer Transition soviele Marken, wie es Übergänge zu den nachfolenden Stellen gibt.
Werden die Marken in einem System so lange reduziert, bis keine schaltfähige Transition mehr existiert, ist
das Petrinetz terminiert.
Schaltbedingungen. Wechselt eine oder mehrere Markierungen die Stelle, spricht man davon, dass die
zwischen den Stellen befindliche Transition schaltet. Eine Transition schaltet dann, wenn alle sogenannten
Vorbedingungen erfüllt sind. Unter Vorbedingung wird dabei das Vorhandensein einer Markierung in der
Stelle bezeichnet, die vor der Transition plaziert ist (Vorbereich). Damit eine Transition schaltet müssen alle
zur Transition führenden Stellen (Vorbedingungen) mit mindestens einer Marke versehen sein.
a)
Stelle
Transition
b)
c)
Transition
Stelle
vorher
nachher
Stelle
Transition
Transition
Stelle
Abbildung 7-6. Unterschiedliche Übergänge bei Petrinetzen
In Bild Unterschiedliche Übergänge bei Petrinetzen sind die unterschiedlichen Übergangsmöglichkeiten
verdeutlicht. Im Teilbild a) wird ein einfacher Übergang dargestellt. Über die Transition fließt von den drei
im Vorbereich existenten Marken eine einzelne. Da die Transition jedoch zwei Nachbedingungen (zwei
nachfolgende Stellen) besitzt, wird die Marke verdoppelt. Im Teilbild b) kann die Transition schalten, weil
sich in den Vorbedingungen jeweils eine Marke befindet. Da es nur eine Nachbedingung gibt, besitzt das
Netz am Ende des Schaltvorganges auch nur noch eine Marke. In Teilbild c) schließlich ist ein nicht deterministischer Übergang dargestellt. Für die beiden angegebenen Stellen ist die Vorbedingung erfüllt. Es ist
aber nicht geklärt, welche der beiden Transitionen schalten wird.
118
Kapitel 7. Formale Beschreibungsmethoden
a)
b)
2
2
t1
t1
3
3
vorher nachher
Abbildung 7-7. Stellen/Transitionsnetz
Petrinetze, bei denen immer genau eine Marke über einen Übergang fließt, heißen Bedingungs/Ereignis
Netz. Demgegenüber bieten die sogenannten Stellen/Transitionsnetze die Möglichkeit, Kanten zu
gewichten. Dazu wird jedem Übergang eine Zahl zugeordnet, der die Anzahl der Marken angibt, die über
diesen Übergang bei Schalten der Transtion (bzw. zum Schalten der Transition) transferiert werden. Wird
in einem Stellen/Transitionsnetz für eine Kante keine Gewichtung angegeben, hat die Kante das Gewicht
1. Im Bild Stellen/Transitionsnetz schaltet die Transition t1 mit der Gewichtung zwei nur dann, wenn im
Vorbereich mindestens zwei Markierungen vorhanden sind. In diesem Fall werden dem Vorbereich zwei
Marken entnommen und im Nachbereich bekommt die linke Stelle eine Marke, die rechte Stelle jedoch
drei Marken, da die Gewichtung der rechten Kante mit drei angegeben ist.
Modellbildung. Eine Modellbildung wird vereinfacht, wenn man zunächst die folgenden Schritte durchführt:
1. Die verwendeten Betriebsmittel (Ressourcen) sind zu identifizieren. Betriebsmittel sind beispielsweise
Werkstücke, Werkstückträger oder auch Semaphore oder gemeinsame Speicherbereiche.
2. Betriebsmittel werden durch jeweils eine Stelle modelliert, die Anzahl der Betriebsmittel oft über die
Anzahl der Marken in dieser Stelle (Anfangszustand).
3. Weitere Stellen ergeben sich, in dem die Bedingungen respektive berücksichtigt werden respektive in
dem die dynamischen Abläufe »durchgespielt« werden.
119
Kapitel 7. Formale Beschreibungsmethoden
A
B
4
1
I
III
Y
2
Z
7
8
5
II
IV
3
6
Abbildung 7-8. Petrinetz einer Fertigungsstraße [Abel1990]
Im folgenden soll beispielhaft ein technischer Prozess in Form eines Petrinetzes modelliert werden.
Auf den beiden Fertigungsstraßen A und B kommen Werkstücke an, die durch zwei Handhabungssysteme (Y und
Roboter) bearbeitet werden. Zur Bearbeitung eines Werkstücks auf der Fertigungsstraße A sind beide Handhabungssysteme notwendig, zur Bearbeitung eines Werkstückes auf der Fertigungsstraße B jedoch nur der Roboter
Z.
Z,
Aus der Beschreibung lassen sich die folgenden Betriebsmittel (Ressourcen) ableiten:
•
•
•
•
Werkstück auf Fertigungsstraße A
Werkstück auf Fertigungsstraße B
Handhabungssystem Y
Handhabungssystem Z
Die Fertigungsstraßen für sich allein werden hier nicht als Betriebsmittel modelliert, da sie anscheinend
ständig zur Verfügung stehen.
Aus dieser Aufstellung läßt sich die Ausgangssituation (hier aus 4 Stellen bestehend)
modellieren. Werden jetzt die beschriebenen Bedingungen berücksichtigt, ergibt sich das in Bild
Petrinetz einer Fertigungsstraße [Abel1990] dargestellte Petrinetz.
Netzwerk Analyse
Mechanismen der Netzwerk Analyse ermöglichen die Identifikation von Verklemmungen. Dazu wird aufgrund des Petrinetzes ein sogenannter Erreichbarkeitsgraph aufgestellt.
Ein Erreichbarkeitsgraph ist ein gerichteter Graph, dessen Knoten erreichbare Markierungen M des zugeordneten Petrinetzes PN sind.
Konstruktion des Erreichbarkeitsgraphen. Ausgehend von der Anfangsmarkierung werden alle
möglichen schaltfähigen Transitionen betrachtet. Die beim Schalten der Transition möglichen erreichbaren
120
Kapitel 7. Formale Beschreibungsmethoden
Markierungen werden als Knoten des Graphen übernommen. Dieses Vorgehen wird ebenfalls bei den
neu entstandenen Knoten durchgeführt, und zwar solange, bis keine Transtion mehr gefunden wird,
die noch nicht durch entsprechende Kanten im Graphen abgebildet wurde. Ein Kennzeichnen des
Erreichbarkeitsgraphen besteht darin, dass die Knoten des Graphen unterschiedliche Markierungen
besitzen.
Die Knoten im Erreichbarkeitsgraphen, die nur eingehende aber keine ausgehenden Kanten haben, stellen
mögliche Verklemmungen dar.
1
3
1010
t1
t2
t3
t1 t2
0110
t2
2
4
1001
t1
0101
t3
Petrinetz
Erreichbarkeitsgraph
Abbildung 7-9. Synchronisation durch ein Petrinetz modelliert [Abel1990]
Im folgenden soll die Erstellung des Erreichbarkeitsgraphen exemplarisch vorgestellt werden (Bild
Synchronisation durch ein Petrinetz modelliert [Abel1990]).
Das Petrinetz besteht aus vier Stellen, die initial folgendermassen markiert sind:
•
•
•
•
Stelle 1 besitzt eine Markierung
Stelle 2 besitzt keine Markierung
Stelle 3 besitzt eine Markierung
Stelle 4 besitzt keine Markierung
Damit ergibt sich der erste Knoten zu M0={1,0,1,0}. Von M0 sind sowohl die Transition t1 als auch t2 möglich.
Schaltet die Transition t1, wäre der nächste Knoten im Erreichbarkeitsgraph gegeben durch M1={0,1,1,0}.
Das Schalten der Transition t2 führt zum Knoten M2={1,0,0,1}.
Die Transition t3 kann erst schalten, wenn die Stelle 2 und die Stelle 4 besetzt ist. Je nach Knoten im
Erreichbarkeitsgraph muss daher die Transition t2 oder t1 schalten. Beide Transitionen führen dann aber
in denselben Knoten M3={0,1,0,1}. Hier kann die Transition t3 schalten und damit wird der Initialzustand
wieder eingenommen.
Der resultierende Erreichbarkeitsgraph ist in Bild Synchronisation durch ein Petrinetz modelliert [Abel1990]
dargestellt. Ein weiteres Beispiel für ein Petrinetz mit zugehörigem Erreichbarkeitsgraph ist in Bild
Zugriff auf ein gemeinsames Betriebsmittel [Abel1990] dargestellt.
121
Kapitel 7. Formale Beschreibungsmethoden
1
t1
t2
t3
010
100
t4
t1 t3
001
3
2
t4
t2
Petrinetz
Erreichbarkeitsgraph
Abbildung 7-10. Zugriff auf ein gemeinsames Betriebsmittel [Abel1990]
Bei der Verklemmung werden die
•
totale Verklemmung von der
•
partiellen Verklemmung
unterschieden.
Im folgenden noch ein Beispiel zur Verklemmung.
Kritische Abschnitte sind dann mehrfach betretbar, wenn während des Zugriffes innerhalb des kritischen
Abschnittes keine Veränderungen an den Daten vorgenommen werden. Solange also kein Rechenprozess
schreibend auf Daten innerhalb eines solchen Abschnittes zugreift, können mehrere Rechenprozesse gefahrlos lesend zugreifen.
Dieses Verhalten eines Lese-/Schreiblocks soll mit Hilfe normaler Semaphore nachgebildet werden, so dass
zwei Rechenprozessen der lesende Zugriff oder einem Rechenprozess der schreibende Zugriff erlaubt ist.
Dazu wird ein Semaphor mit 2 vorinitialisiert. Ein Prozess, der nur lesend auf den kritischen Abschnitt
zugreift, alloziert das Semaphor wie gewohnt einmal, ein Rechenprozess, der schreibend zugreifen möchte,
alloziert dagegen das Semaphor zweimal. Die folgende Codesequenzen verdeutlichen den Vorschlag:
Sequenz zum Lesen
Sequenz zum Schreiben
P(S1)
P(S1)
P(S1)
... // kritischer Abschnitt
... // kritischer Abschnitt
V(S1)
V(S1)
V(S1)
Mit Hilfe eines Petrinetzes soll geklärt werden, ob dieser Vorschlag praktikabel ist. Dazu werden die
beschriebenen Vorgänge in einem Bedingungs-/Ereignisnetz modelliert. Betriebsmittel sind hier die zugreifenden Prozesse (von denen zur Vereinfachung maximal 3 aktiv sein sollen) und das Semaphor.
122
Kapitel 7. Formale Beschreibungsmethoden
Prozesse
1
Semaphor
2
t1
P(S1)
t2
P(S1)
schreiben
4
lesen
3
t3
V(S1)
t4
P(S1)
5
schreiben
t5
V(S1)
6
schreiben
t6
V(S1)
Auf Basis des Petrinetzes läßt sich der Erreichbarkeitsgraph aufstellen:
320000
t2 P(S1)
t3
211000
V(S1)
t3
t1
t1
P(S1)
t2
210100
P(S1)
102000
t3
t1
P(S1)
V(S1)
101100
t2
t4 P(S1)
P(S1)
t6
200010
t5 V(S1)
V(S1)
t2
t6
210001
t1
100200
100101
V(S1)
t6
P(S1)
101001
V(S1) t3
Da es im Erreichbarkeitsgraphen einen Knoten ohne abgehende Kanten gibt (100200), kann es zu einer
Verklemmung kommen.
123
Kapitel 8. Echtzeitnachweis
Der Nachweis, dass alle zeitlichen Anforderungen der Aufgabenstellung unter allen Umständen eingehalten
werden, ist sehr schwierig. Insbesondere sind für unterschiedliche Schedulingalgorithmen (siehe [Kapitel
Scheduling auf Einprozessormaschinen]) auch unterschiedliche Nachweisverfahren zu verwenden. Prinzipiell beruht der Echtzeitnachweis darauf,
1. die relevanten Kenndaten des technischen Prozesses zu evaluieren. Für jede Anforderung (Ereignis)
sind dies:
a. Minimale Prozesszeit tPmin,i
b. Minimal zulässige Reaktionszeit tDmin,i
c. Maximal zulässige Reaktionszeit tDmax,i
d. Auftrittszeitpunkt für jede Anforderung tA (mögliche Abhängigkeiten der Ereignisse untereinander)
e. Maximale Verarbeitungszeit tEmax (WCET, Schätzwert),
f. Die minimale Verarbeitungszeit tEmin (BCET, Schätzwert),
2. Die Auslastungsbedingung zu überprüfen und
3. die Pünktlichkeitsbedingung zu verifizieren (Bestimmung von tRmin,i und tRmax,i) und für jede
Anforderung ist zu überprüfen, ob die Pünktlichkeitsbedingung eingehalten wird.
Die Prozesszeiten und die maximal zulässigen Reaktionszeiten (tDmin,i und tDmax,i) sind der Beschreibung des
technischen Prozesses zu entnehmen respektive aus dieser abzuleiten.
Zur Überprüfung insbesondere der Pünktlichkeitsbedingung ist die Kenntnis der minimalen und der maximalen Reaktionszeit erforderlich. Während die Bestimmung der minimalen Reaktionszeit oft kein Problem
darstellt (meist ist diese identisch mit der minimalen Verarbeitungszeit tEmin), ist die Bestimmung der maximalen Reaktionszeit nicht trivial.
8.1. Worst Case Betrachtungen
Für die Worst Case Betrachtung ist eine möglichst
große Belastungssituation für das Rechensystem
anzusetzen:
− tPmin
− tEmax
− alle (sonstigen) Ereignisse treten zum Zeitpunkt t 0
ein (alle Ereignisse kommen gleichzeitig).
tA = 0
Abbildung 8-1. Parameterauswahl für den Worst Case Fall
Für die Berechnung, ob alle Echtzeitanforderungen erfüllt sind, ist immer der sogenannte Worst Case
(schlimmste Fall) heranzuziehen. Der Worst Case ist der Fall, an dem die Prozessereignisse am dichtesten
aufeinander folgen (oder besser, die höchste Belastung mit sich bringen). Da meistens mehrere Prozesssig-
124
Kapitel 8. Echtzeitnachweis
nale unterschiedlichen Typs (und damit mit unterschiedlichen Ankunftszeiten) vorhanden sind, ist das bei
der Bestimmung des Worst Case mit zu berücksichtigen.
Der Worst Case bei der 2. Echtzeitbedingung (Pünklichkeit, tDmin,i ≤ tRmin,i ≤ tRmax,i ≤ tDmax,i) ist gegeben,
wenn die Prozesszeiten möglichst kurz sind und im Gegenzug die Verarbeitungszeiten möglichst lang.
Außerdem ist in diesem Fall zu berücksichtigen, dass alle sonstigen Ereignisse im System – soweit sie
im gegenwärtigen Prozesszustand möglich sind – auch auftreten.
E
tDmax
tRmin
1
2
t
E
tDmax
tRmin
1 2
t
1=Berechnung
2=Ausgabe
Abbildung 8-2. Verteilung der Rechenzeit bei minimalen Reaktionszeiten
Schwierig wird die Worst Case Betrachtung auch dann, wenn die minimale Reaktionszeit tDmin 6=
t0 ist. Hier ist die Einhaltung der Realzeitbedingung stark abhängig von der gewählten Architektur
des Systems. Löst beispielsweise das Ereignis aus dem technischen Prozess zum Zeitpunkt »tA=0«
einen Interrupt aus und die zugehörige Interrupt Service Routine (ISR) wiederum erteilt dem
Betriebssystem den Auftrag, einen Rechenprozess zum Zeitpunkt tDmin zu starten (Abbildung
[Verteilung der Rechenzeit bei minimalen Reaktionszeiten], unten), so führt dies zu einem anderen
Echtzeitverhalten, als wenn der Rechenprozess direkt nach Eintritt des Ereignisses gestartet wird,
diese aber nur die Ausgabe des Ergebnisses an den technischen Prozess zum Zeitpunkt tDmin beauftragt
(Abbildung [Verteilung der Rechenzeit bei minimalen Reaktionszeiten], oben).
Bezüglich der 1. Echtzeitbedingung (Auslastungsbedingung) muss die Summe aller Auslastungen, zu dem
Zeitpunkt, an dem sich das Maximum dieser Summe ergibt, gebildet werden.
Es ist also zu beachten:
1. Bei von einander unabhängigen Ereignissen ist anzusetzen, dass die unabhängigen Ereignisse gleichzeitig (zum Zeitpunkt tA=0) auftreten.
2. Bei voneinander abhängigen Ereignissen darf die zeitliche Abhängigkeit der Ereignisse mit berücksichtigt werden. Das führt zu einer geringeren Auslastung als bei der Berechnung mit unabhängigen
Ereignissen.
Die Abhängigkeiten der Ereignisse untereinander machen die Worst Case Bestimmung in der Praxis
schwierig. Oftmals wird man daher davon ausgehen, dass Abhängigkeiten nicht bestehen. Damit gelten
dann – eigentlich abhängige – Ereignisse als unabhängig voneinander und man muss zur Berechnung
der Auslastung (1. Echtzeitbedingung) ansetzen, dass die Ereignisse gleichzeitig auftreten. Das führt im
Ergebnis zu einer höheren Auslastung, als sie in der Realität gegeben sein wird.
125
Kapitel 8. Echtzeitnachweis
8.2. Abschätzung der Worst Case Execution Time
(WCET)
Die maximale Verarbeitungszeit tEmax - auch Worst Case Execution Time (WCET) genannt - einer Anforderung ist sehr schwer zu bestimmen. Die WCET hängt insbesondere vom Algorithmus, von der Implementierung des Algorithmus, von der verwendeten Hardware und von den sonstigen Aktivitäten des
Systems (andere Rechenprozesse) ab. Daher kann die WCET allenfalls abgeschätzt werden.
Prinzipiell werden zwei Methoden zur Bestimmung der WCET unterschieden:
1. das Ausmessen der WCET und
2. die statische Analyse inklusive der daraus erfolgenden Ableitung der WCET.
Ausmessen der WCET. Die WCET wird dadurch bestimmt, dass die zugehörige Codesequenz (im
Regelfall auf der Zielplattform) abgearbeitet wird. Dabei wird die Zeit zwischen Eintreten des Ereignisses
und Ausgabe bestimmt.
while( TRUE )
Warte auf Ereignis
Für die Messung
der WCET
modifiziert.
t 1 = Zeitstempel
Eigentliche Verarbeitung
t 2 = Zeitstempel
Abbildung 8-3. Messprinzip zur Bestimmung der WCET
Zur Messung dieser Zeit lassen sich prinzipiell zwei Verfahren unterscheiden:
1. Messung
durch
das
auszumessende
[Messprinzip zur Bestimmung der WCET]).
Codestück
selbst
(siehe
Das Codstück wird so ergänzt, dass jedesmal, wenn ein Ereignis ankommt und wenn die Reaktion
erfolgt ist, ein Zeitstempel abgelegt wird. Die Differenz ergibt die aktuelle Ausführungszeit tEmax.
Beim ersten Durchlauf wird diese als WCET abgelegt, bei den nachfolgenden Durchläufen wird die
neu berechnete Zeit mit der abgelegten Zeit verglichen. Ist die neu berechnete Zeit größer als die bisher
gemessene WCET überschreibt der neue Wert die bisherige WCET.
Eventuell kann anstelle der Modifikation des Codestücks auch ein Profiler eingesetzt werden.
2. Externe Messung von Ereignis und Reaktion (Ausgabe) zum Beispiel mit Hilfe eines Oszilloskops.
Damit eine WCET gemessen werden kann, muss die Codesequenz mit entsprechenden Input-Werten versorgt werden. Außerdem muss das gesamte System unter Last gesetzt werden.
Vorteile dieser Methode:
•
•
Unabhängig von einer Programmiersprache.
Eventuell einfach realisierbar.
Nachteile der Meßmethode:
126
Kapitel 8. Echtzeitnachweis
•
•
•
•
•
•
Die WCET einer Codesequenz kann nicht garantiert werden, schließlich hängt sie von zu vielen Einflußfaktoren (Vorgeschichte, Schleifendurchläufe, Caches, Verzweigungen usw.) ab.
Das Messen ist WCET ist zeitaufwendig und damit letztlich teuer. Das auszumessende Codestück muss
theoretisch mit sämtlichen Inputdaten (in jeglicher Kombination) beschickt werden.
Diese Methode kann korrekt nur mit dem produktivem Code auf der Zielplattform durchgeführt werden.
Damit ist eine Abschätzung zu einem frühen Zeitpunkt nur bedingt möglich.
Für die Durchführung der Messung muss eine Messumgebung (die die Inputdaten zur Verfügung stellt)
geschaffen werden.
Die auszumessende Codesequenz ist unter Umständen zu modifizieren (zum Ablegen von Zeitstempeln).
Es ist oftmals schwierig, das System unter die notwendige Last zu setzen.
Statische Analyse. Hierbei wird der Code selbst analysiert. Dazu ist ein Analyseprogramm notwendig,
welches meist mit dem C-Quellcode, manchmal aber auch mit dem Objektcode gefüttert wird. Außerdem
ist noch eine Hardwarebeschreibung notwendig. Die Analysewerkzeuge arbeiten so, dass aus dem Code
zunächst ein Kontroll-Graph erstellt wird. Die Laufzeit der einzelnen Codesequenzen des Kontroll-Graphen
können abgeschätzt werden. Danach muss noch die Anzahl der Schleifendurchläufe berücksichtigt werden.
Komplex wird die Analyse durch die notwendige Abschätzung von Cache-Hits und Cache-Misses und des
Prozessor-Pipelinings.
Unabhängig vom Verfahren gilt prinzipiell, den ausgemessenen Code nicht nachträglich zu verändern und
außerdem noch mit einem Sicherheitsaufschlag zu versehen. Als Sicherheitsaufschlag arbeitet der Ingenieur
gern mit der Zahl π.
tEmax=tWCET·π
8.3. Abschätzung der Best Case Execution Time
(BCET)
Die Best Case Execution Time eines Jobs muss bestimmt werden, falls eine Codesequenz nicht vor einer
minimal zulässigen Reaktionszeit tDmin abgearbeit worden sein darf.
Die Bestimmung der BCET ist jedoch einfacher als die Bestimmung der WCET. Hier kann der Code
recht einfach analysiert werden (kürzester Pfad durch den Code, Prozessor arbeitet mit optimaler
Geschwindigkeit). Ansonsten kann die Bestimmung der BCET erfolgen, wie bei der WCET, wobei
allerdings möglichst wenig Last auf dem System vorhanden sein soll.
8.4. Echtzeitnachweis bei prioritätengesteuertem
Scheduling
Der Echtzeitnachweis bei prioritätengesteuertem Scheduling beruht im Wesentlichen darauf, für alle Anforderungen die beiden Echtzeitbedingungen (Gesamtauslastung und Pünktlichkeit) zu überprüfen.
Folglich müssen folgende Kenndaten bekannt sein bzw. bestimmt werden:
1. Die minimalen und die maximalen Verarbeitungszeiten (tE,i, also die BCET und die WCET) der einzelnen Jobs.
2. Die minimalen und die maximalen Reaktionszeiten (tRmin,i und tRmax,i).
127
Kapitel 8. Echtzeitnachweis
3. Die minimal und die maximal zulässigen Reaktionszeiten für die den Jobs zugeordneten, einzelnen
Ereignisse (tDmin,i und tDmax,i).
Anf.
Prio
tPmin
tPmax
tDmin
tDmax
tEmin
tEmax
tRmin
tRmax
tA
tDmin,i und tDmax,i sind aus dem technischen Prozess abzuleiten. BCET und WCET werden nach dem oben
beschriebenen Schema abgeschätzt. Mit der BCET ist zumeist auch tRmin gegeben, so dass insbesondere die
Bestimmung von tRmax als schwierige Aufgabe übrigbleibt. Unter der Voraussetzung, dass die Ereignisse,
die die Bearbeitung auslösen, alle unabhängig voneinander sind, hilft hierbei die Time-Demand Analyse.
Bei diesem Verfahren wird die benötigte Rechenzeit (Verarbeitungszeit, computation time) der zur Verfügung stehenden Rechenzeit gegenüber gestellt. Die benötigte Rechenzeit erhält man dabei durch einfaches
Aufsummieren der für das Rechnersystem bestimmten WCET. Die benötigte respektive vom Rechnersystem zu erbringede Rechenzeit tc(t) ist abhängig von der Zeit und eine stetig wachsende Funktion.
In Abbildung Benötigte Rechenzeit (Computation Time) tc(t) ist die zu erbringende Rechenzeit tc(t) für zwei
Jobs dargestellt. Job 1 stellt alle 30ms eine Anforderung von 10ms und Job 2 alle 45ms von 15ms.
E(t)
2
70
1
1
60
50
Rechenprozess 1+2
40
30
2
20
10
Rechenprozess 1
1
10
20
30
40
50
60
70
t
Abbildung 8-4. Benötigte Rechenzeit (Computation Time) tc(t)
Der zu erbringenden Rechenzeit kann man die zur Verfügung gestellte Rechenzeit (ta(t), A=available)
gegenüber stellen. Gemäss der Berechnung der WCET respektive von tc(t) ist die Darstellung so normiert,
dass das Rechensystem mit jeder Millisekunde eine Millisekunde Rechenzeit zur Verfügung stellt (also
ta(t)=t). Die Summe der zur Verfügung gestellte Rechenzeit entspricht damit der Winkelhalbierenden (siehe
Abbildung Available Computation Time ta(t)).
Es versteht sich, dass der Rechner dann unterhalb seiner Auslastungsgrenze arbeitet, wenn die zu erbringende Rechenzeit tc(t) kleiner ist als die zur Verfügung stehende Rechenzeit ta(t). Der Schnittpunkt zwischen tc(t) und ta(t) entspricht damit der maximalen Reaktionszeit des niedrigpriorsten Rechenprozesses. Zu
diesem Zeitpunkt hat der Rechner erstmalig alle Anforderungen erfüllt.
128
Kapitel 8. Echtzeitnachweis
Beispiel 8-1. Grafische Herleitung zur Bestimmung der maximalen Reaktionszeit
Gegeben seien drei Rechenprozesse mit den folgenden Kenndaten:
Anf.
tEmax
tPmin
tDmin
tDmax
tA
A
10 ms
30 ms
0 ms
30 ms
0 ms
B
15 ms
45 ms
0 ms
45 ms
0 ms
C
15 ms
60 ms
0 ms
60 ms
0 ms
Abbildung Bestimmung von tRmax (zu Beispiel []) veranschaulicht dieses Prozessgebilde. Der
Schnittpunkt von tc(t) und ta(t) befindet sich bei t=90ms. Damit ist tRmax des niedrigpriorsten
Rechenprozesses ebenfalls 90ms.
A(t)
70
60
50
40
30
20
10
10
20
30
40
50
60
70
t
Abbildung 8-5. Available Computation Time ta(t)
t c(t)
90
t a(t)
Rechenprozess A+B+C
niedrigste Priorität
80
70
3
2
60
Rechenprozess A+B
mittlere Priorität
1
50
40
3
2
1
30
Rechenprozess A
höchste Priorität
2
1
20
10
1
verbrauchte Rechenzeit
10
20
30
40
50
60
70
80
90
100
t
Abbildung 8-6. Bestimmung von tRmax (zu Beispiel [Grafische Herleitung zur Bestimmung der maximalen Reaktionszeit])
129
Kapitel 8. Echtzeitnachweis
Mit Hilfe der Grafik läßt sich aber nicht nur die maximale Reaktionszeit des niedrigpriorsten Rechenprozesses ablesen. Falls man – wie in Abbildung Bestimmung von tRmax (zu Beispiel []) angedeutet – nur
die Anforderungen der Prozesse aufaddiert, für deren niederpriorsten man die maximale Reaktionszeit bestimmen möchte, ist jeweils der Schnittpunkt mit der Winkelhalbierenden die gesuchte Größe.
90
t c(t)
t a(t)
3
80
70
60
50
40
3
1
2
t E,2
30
2
20
10
t E,3
1
10
20
30
40
50
60
70
80
90
100
t
Abbildung 8-7. Bestimmung von tRmax höher priorisierter
[Grafische Herleitung zur Bestimmung der maximalen Reaktionszeit])
Prozesse
(Zahlen
von
Noch einfacher geht es, wenn man die Winkelhalbierende auf der Y-Achse verschiebt. Wenn man für einen
Rechenprozess Ji die maximale Reaktionszeit bestimmen möchte, verschiebt man die Winkelhalbierende
um die Summe der Verarbeitungszeiten der Prozesse nach oben, die niedriger priorisiert sind als Ji (siehe
Abbildung Bestimmung von tRmax höher priorisierter Prozesse (Zahlen von [])).
(remaining computation time)
t c(t) − ta(t)
40
1
2
3
Zu erbrigende
Rechenzeit
1
1
1
2
2
3
ohne
Job 2
30
20
ohne
Job 3
10
5 10
20
30
40
50
60
70
80
90
100
t
Abbildung 8-8. Bestimmung der maximalen Reaktionszeit
Oft wird auch eine andere Variante zur grafischen Bestimmung der maximalen Reaktionszeit tRmax angewendet: Anstelle tc(t) trägt man hier direkt tc(t)-ta(t) über der Zeit auf. Der erste Schnittpunkt dieser Funktion
130
Kapitel 8. Echtzeitnachweis
mit der Zeitachse entspricht der maximalen Reaktionszeit des niedrigpriorsten Prozesses (siehe Abbildung
Bestimmung der maximalen Reaktionszeit).
Mit ta(t)=t ergibt sich für die »remaining computation time«: trc(t)=tc(t)-t. trc(t) korrespondiert semantisch
mit dem Load eines Rechners.1
Verschiebt man die Zeitachse um die Rechenzeit des niedrigpriorsten Rechenprozesses nach oben, erhält man mit dem neuen Schnittpunkt der Zeitachse die maximale Reaktionszeit des nächstniederprioren
Rechenprozesses, im Beispiel der Rechenprozess 2. Ein nochmaliges Verschieben um die Verarbeitungszeit,
in diesem Fall des Rechenprozess 2, erhält man die maximale Reaktionszeit des Rechenprozess 1 (siehe
ebenfalls Abbildung Bestimmung der maximalen Reaktionszeit).
Mathematischer Ansatz
Die maximale Reaktionszeit des niederpriorsten Jobs ergibt sich nach folgender Formel:
t=tRmax, falls tc(t) = t;
tc(t) für ein periodisches Ereignis kann durch folgende Formel dargestellt werden:
Gleichung 8-1. Berechnung der Rechenzeitanforderung eines periodisch aufgerufenen Jobs.
tc,i (t) =
i
X
j=1
⌈
t
⌉ · tEmax,j
tP min,,j
Hierbei ist »i« der Index des Rechenprozesses, für den tc(t) berechnet werden soll. Die Indizes sind
dabei gemäss der Priorität verteilt. Der Rechenprozess mit der höchsten Priorität bekommt den Index
1, der Rechenprozess mit der nächst niedrigeren Priorität den Index 2. Mit der Summe werden die
Verarbeitungszeiten (tE,j) aller Rechenprozesse gleicher oder höherer Priorität aufsummiert.
tP,j ist die zum jeweiligen Prozess gehörige Prozesszeit. Die obere Gaußklammer ergibt immer die nächst
größere Ganzzahl des Ausdrucks innerhalb der Gaußklammer.2 Sie entspricht damit der Funktion ceil, die
im Fall von Nachkommastellen ebenfalls den nächst höheren Integerwert zurückliefert (ceil(2,235)=3).
Für das Beispiel Grafische Herleitung zur Bestimmung der maximalen Reaktionszeit ergibt sich damit für
den höchstprioren Rechenprozess A (Index 1):
tc (t) = ⌈
t
⌉ · 10ms
30ms
Für den nächst priorisierten Rechenprozess B (Index 2) ergbit sich:
tc,2 (t) = ⌈
t
t
⌉ · 15ms + ⌈
⌉ · 10ms
45ms
30ms
Und für den niedrigprioren Rechenprozess C (Index 3) schließlich:
tc,3 (t) = ⌈
t
t
t
⌉ · 15ms + ⌈
⌉ · 10ms + ⌈
⌉ · 15ms
60ms
30ms
45ms
Zur Bestimmung von tRmax ist es notwendig, ein t zu finden, welches die Gleichung tc(t)=t erfüllt. Da die
Gleichung tc(t)=t nicht trivial aufzulösen ist, wird ein iterativer Ansatz gewählt:
131
Kapitel 8. Echtzeitnachweis
Gleichung 8-2. Iterative Bestimmung von tRmax
t(l+1) =
i
X
j=1
⌈
t(l)
tP min,,j
⌉ · tEmax,j
Unter der Voraussetzung, dass alle Anforderungen zum Zeitpunkt tA=0 eintreffen und die maximal zulässige
Reaktionszeit tDmax,i kleiner als tP,i ist, muss t nur im Bereich von 0≤t≤tP,i untersucht werden. Wird bis zu
diesem Zeitpunkt kein passendes t gefunden, ist eine schritthaltende Verarbeitung nicht möglich.
Als Initialwert für »t« wählt man die Summe der Verarbeitungszeiten höher- und gleichpriorer Rechenprozesse:
(1)
ti
=
i
X
tEmax,j
j=1
Für
Beispiel
Grafische Herleitung zur Bestimmung der maximalen Reaktionszeit
ergeben sich demnach die folgenden maximalen Reaktionszeiten (die auch aus den
Bestimmung von tRmax höher priorisierter Prozesse (Zahlen von [])
und
Abbildungen
Bestimmung der maximalen Reaktionszeit abzulesen sind):
Job A:
tc,1 (t) = ⌈
Initialwert für t:
t(1) = tEmax,1 = 10ms
t(2) = ⌈
Die Gleihung
t
t
⌉ · tEmax,1 = ⌈
⌉ · 10ms
tP min,1
30ms
10ms
⌉ · 10ms = 1 · 10ms = 10ms
30ms
tc (t) = t ist bereits erfüllt, damit ergibt sih: tRmax,1 = t(1) =
10ms
Job B:
tc,2 (t) = ⌈
Initialwert für t:
t(2) = ⌈
t(1) = tEmax,1 + tEmax,2 = 10ms + 15ms = 25ms
25ms
25ms
⌉ · 10ms + ⌈
⌉ · 15ms = 1 · 10ms + 1 · 15ms = 25ms
30ms
45ms
tc (t) = t
tRmax,2 = 25ms
Die Gleihung
Job C:
132
t
t
⌉ · 10ms + ⌈
⌉ · 15ms
30ms
45ms
ist für
t = 25ms
erfüllt, damit ergibt sih:
Kapitel 8. Echtzeitnachweis
tC,3 (t) = ⌈
Initialwert für t:
t
t
t
⌉ · 10ms + ⌈
⌉ · 15ms + ⌈
⌉ · 15ms
30ms
45ms
60ms
t(1) = tEmax,1 + tEmax,2 + tEmax,3 = 40ms
t(2) = ⌈
40ms
40ms
40ms
⌉·10ms+⌈
⌉·15ms+⌈
⌉·15ms = 2·10ms+1·15ms+1·15ms = 50ms
30ms
45ms
60ms
t(3) = ⌈
50ms
50ms
50ms
⌉·10ms+⌈
⌉·15ms+⌈
⌉·15ms = 2·10ms+2·15ms+1·15ms = 65ms
30ms
45ms
60ms
t(4) = ⌈
65ms
65ms
65ms
⌉·10ms+⌈
⌉·15ms+⌈
⌉·15ms = 3·10ms+2·15ms+2·15ms = 90ms
30ms
45ms
60ms
t(5) = ⌈
90ms
90ms
90ms
⌉·10ms+⌈
⌉·15ms+⌈
⌉·15ms = 3·10ms+2·15ms+2·15ms = 90ms
30ms
45ms
60ms
tc (t) = t
tRmax,2 = 90ms
Die Gleihung
ist für
t = 90ms
erfüllt, damit ergibt sih:
Der eigentliche Echtzeitnachweis
Sind alle Kenndaten der Rechenprozesse bekannt, können die Echtzeitbedingungen überprüft werden. Für
das Beispiel Grafische Herleitung zur Bestimmung der maximalen Reaktionszeit ergibt sich:
Auslastung:
tDmin ≤ tRmin ≤ tRmax ≤ tDmax
für Job A ergibt sih:
0 ≤ 10ms ≤ 10ms ≤ 30ms
für Job B ergibt sih:
0 ≤ 15ms ≤ 25ms ≤ 45ms
für Job C ergibt sih:
0 ≤ 15ms ≤ 90ms ≤ 60ms
Für Job C ist die Echtzeitbedingung nicht erfüllt! Eine schritthaltende Verarbeitung ist damit nicht möglich.
8.5. Echtzeitnachweis bei Deadline-Scheduling
8.5.1. Ereignisstrom-Modell
Beim Ereignisstrom-Modell handelt es sich um eine formale Methode zur Beschreibung des Auftretens
von Ereignissen (Zeitpunkt, Häufigkeit und Abhängigkeiten zwischen Ereignissen). Mit Hilfe des Modells
können Worst-Case Situationen ermittelt und ein Echtzeitnachweis durchgeführt werden [Gresser93].
Das Verfahren setzt als Schedulingverfahren ein Deadlinescheduling voraus, welches im Abschnitt
Scheduling auf Einprozessormaschinen vorgestellt wird.
Das Modell besteht aus den Komponenten:
133
Kapitel 8. Echtzeitnachweis
1. Ereignisstrom
2. Ereignisdichtefunktion und
3. Rechenzeitanforderungsfunktion.
I=1
I=2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
t
E(I)
5
4
3
2
1
Im Intervall der Länge 1
treten max. 2 Ereignisse
auf.
Im Intervall der Länge 8
treten max. 4 Ereignisse
auf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
I
Abbildung 8-9. Ereignisstrom-Modell
Um die Ereignisdichtefunktion (Ereignisfunktion) zu bestimmen, muss bekannt sein, zu welchen Zeitpunkten die Ereignisse im Prozess auftreten. Für jede Intervallgröße I von 0 bis ∞ bestimmt man die im Intervall I maximale auftretende Anzahl Ereignisse. In Abbildung [Ereignisstrom-Modell] beispielsweise würde
man bei einer Intervallgröße nahe 0 genau ein Ereignis erfassen. Bei einem Intervall der Größe 1 könnte
man bereits zwei Ereignisse (wenn das Intervall zum Zeitpunkt t=7 beginnt) erfassen. Man schiebt also
gedanklich die Intervalle unterschiedlicher Größe über die Ereignisse und erfaßt die im Intervall maximal
sichtbare Anzahl. Dieser Wert wird an die entsprechende Stelle der Ereignisdichtefunktion E(I) eingetragen.
Die auf die anschauliche Art gewonnene Ereignisdichtefunktion wird mathematisch als Ereignisstrom
dargestellt. Ein Ereignisstrom ist eine Anzahl Ereignistupel. Jedes Ereignistupel besteht aus der Angabe
eines Zyklus tP,i und eines Intervalls tph,i:
Ereignis Tupel:
( )
t Pmin,i
t A,i
Da jedes Ereignis mehrfach auftreten kann, wird jedes Ereignistupel i noch mit einem Faktor ki multipliziert. Für einmalig (also nicht periodisch) auftretende Ereignisse findet der Wert unendlich “∞” als Zyklus Anwendung.
ES:
{ ( )}
ki
t Pmin,i
t A,i
Ein Ereignisstrom enthält also zu jeder Änderung in der Ereignisdichtefunktion ein Ereignistupel. In Abbildung [Ereignisstrom] ist der Ereignisstrom der Ereignisdichtefunktion aus Abbildung Ereignisstrom-Modell
dargestellt. Bei einer Periode von 16 findet sich im Intervall der Größe (nahe) 0 genau 1 Ereignis. Bei einem
Intervall der Länge 4 finden sich 2 Ereignisse, bei einem Intervall der Länge 6 finden sich 3 und bei einem
Intervall der Länge 9 finden sich 4 Ereignisse.
134
Kapitel 8. Echtzeitnachweis
ES:
{( )( )( )( ) ( )}
16 16 16
16
16
0 , 1 , 4 , 6 , 9
1
2
3
Ereignisse
4
5
Abbildung 8-10. Ereignisstrom
Die bisher nur grafisch vorgestellte Ereignisdichtefunktion läßt sich in mathematische Gleichungen umsetzen. Gleichung Ereignisdichtefunktion eines periodischen Ereignisses stellt die allgemeine Form eines
periodischen Ereignisses dar.
Gleichung 8-3. Ereignisdichtefunktion eines periodischen Ereignisses
E(I) = k · ⌊
I + tP min,i − tA,i
⌋
tP min,i
Die soeben gegebene Interpretation entspricht der eines sogenannten homogenen Ereignisstromes. Beim
homogenen Ereignisstrom sind die endlichen (unendliche Zyklen dürfen damit weiterhin noch vorkommen) Zykluswerte einheitlich, beim inhomogenen Ereignisstrom sind die Zykluswerte unheitlich (siehe
Abbildung [Inhomogener Ereignisstrom]).
ES:
{( )( )( )}
∞
2
0 , 0
3
, 0
Abbildung 8-11. Inhomogener Ereignisstrom
Inhomogene Ereignisströme lassen sich durch Vervielfachung von Ereignistupeln auf das kleinste gemeinsame Vielfache vereinheitlichen (Abbildung [Homogener Ereignisstrom]).
ES:
{( ) ( )( )( )( )}
∞ 2
0 ,
6
0
6
, 2
6
, 3
6
, 4
Abbildung 8-12. Homogener Ereignisstrom
E(I)
9
8
7
6
5
4
3
2
1
()
∞
0
()
2
0
()
3
0
0 1 2 3 4 5 6 7 8 9 10 I
Abbildung 8-13. Ereignisdichtefunktion des inhomogenen Ereignisstroms
135
Kapitel 8. Echtzeitnachweis
Für den Realzeitnachweis ist neben dem Ereignisstrom noch die Kenntnis über Rechenzeit tE und Deadline
tDmax notwendig.
tDmax
E
tE
Abbildung 8-14. Ereignisstrom-Modell
Im Ereignisstrom-Modell wird ein einfacher Rechenprozess durch seine Deadline tDmax und seine Rechenzeit
tE beschrieben (siehe Abbildung [Ereignisstrom-Modell]).
Aus der Ereignisdichtefunktion, der Deadline und der Rechenzeit läßt sich die Rechenzeitanforderungsfunktion tC(I) bestimmen, vorausgesetzt, der nächste aktive Rechenprozess wird über das Deadlinescheduling
bestimmt:
Gleichung 8-4. Rechenzeitanforderungsfunktion
tC (I) =
X
Ei (I − tDmax,i ) · tEmax,i
i
tc,i (I) =
i
X
kj · ⌊
j=1
I − tDmax,j + tP min,j − tA,j
⌋ · tEmax,j
tP min,j
Die Rechenzeitanforderungsfunktion des Rechenprozesses wird bestimmt, indem für jedes Ereignistupel
i die Ereignisdichtefunktion mit der Rechenzeit tE,i multipliziert und um die zum Ereignistupel gehörige
Deadline tDmax,i nach rechts verschoben wird (siehe Gleichung Rechenzeitanforderungsfunktion).
Die Echtzeitbedingung des Rechenprozess ist erfüllt, wenn jetzt: tC(I)≤I ist. Graphisch veranschaulicht
bedeutet dies, dass die Rechenzeitanforderungsfunktion die Winkelhalbierende nicht überschreiten darf.
Umgekehrt gibt der Abstand von der Winkelhalbierenden an, welche Auslastung der Rechner durch Bearbeitung des Rechenprozesses erfährt.
Bearbeitet ein Rechner mehrere Rechenprozesse, so sind die Rechenzeitanforderungsfunktionen der einzelnen Rechenprozesse aufzusummieren. Auch hier gilt wieder, dass
tC(I)ges≤I
ist.
Diese Gleichung ist für alle I mit
0 < I < (⌈
max(tA,i )
⌉ + 1) · kgV (tP min,1 ..tP min,i )
kgV (tP min,1 ..tP min,i )
zu untersuchen.
Beispiel 8-2. Echtzeitnachweis bei Deadline-Scheduling
Es soll noch einmal das Beispiel Grafische Herleitung zur Bestimmung der maximalen Reaktionszeit
aufgegriffen und die Frage geklärt werden, ob die fristgerechte Bearbeitung der Aufgabe mit einem
Deadlinescheduling-Verfahren möglich ist.
136
Kapitel 8. Echtzeitnachweis
ES=
{( 300) ( 450) ( 600)}
C(I)
60
50
40
30
20
10
5 10
20
30
40
50
60
70
80
90
100
I
Abbildung 8-15. Echtzeitnachweis mit Hilfe des Ereignisstrom-Modells
Als erstes wird der Ereignisstrom erstellt. Dieser wird zur Rechenzeitanforderungsfunktion, wenn man den
Ereignisstrom mit der Verarbeitungszeit multipliziert und um die Deadline nach rechts verschiebt. Wie aus
Abbildung [Echtzeitnachweis mit Hilfe des Ereignisstrom-Modells] ersichtlich, wird die Winkelhalbierende
nicht übertroffen, die Aufgabe ist also bei Verwendung eines Deadline-Schedulings fristgerecht zu bearbeiten.
Rechnerisch ergibt sich für tC(I):
tC (I) = ⌊
I − 30ms + 30ms
I − 45ms + 45ms
I − 60ms + 60ms
⌋·10ms+⌊
⌋·15ms+⌊
⌋·15ms
30ms
45ms
60ms
z.B. für
I = 60ms:
tC (60ms) = ⌊
60ms
60ms
60ms
⌋ · 10ms + ⌊
⌋ · 15ms + ⌊
⌋ · 15ms
30ms
45ms
60ms
= 20ms + 15ms + 15ms = 50ms
8.5.2. Behandlung abhängiger Ereignisse
Das Ereignisstrom-Modell ermöglicht den Nachweis über die Einhaltung der Zeitanforderungen auch von
abhängigen Ereignissen. Die Kenntnis über Abhängigkeiten zwischen Ereignissen entschärft die zeitlichen
Anforderungen, der bisherige Ansatz, zeitlich abhängige Ereignisse wie zeitlich unabhängige Ereignisse zu
betrachten, führt in den meisten Fällen zu ungünstigeren Ergebnissen.
Der einfachste Weg, zeitlich abhängige Ereignisse zu verarbeiten besteht darin, die abhängigen Ereignisse in
einem einzigen Ereignisstrom zusammenzufassen. Bei dem auf diese Art neu gewonnenen Ereignisstrom ist
für die Berechnung der Ereignisdichtefunktion darauf zu achten, dass für die Berechnung die entsprechenden Werte für tE,i und tDmax,i eingesetzt werden.
137
Kapitel 8. Echtzeitnachweis
2T
3T
6T
6T
tP =6T
tP =6T
{ ( )}
ES: 2
6
0
1 2 3 4 5 6 7 8 9 10 11 t
E(I)
4
3
2
1
RP1
RP2
I
1 2 3 4 5 6
Abbildung 8-16. Prozessbeschreibung und Ereignisdichtefunktion
Betrachten wir zunächst zwei unabhängige Prozesse, die beide eine Prozesszeit von tP=6T
besitzen. Rechenprozess RP1 besitze eine Verarbeitungszeit von tE,1=2T und Prozess RP2
besitze eine Verarbeitungszeit von tE,2=3T. Falls die maximal zulässige Reaktionszeit tDmax
beider Rechenprozesse identisch mit der Prozesszeit tP ist, ergeben sich die in Abbildung
[Prozessbeschreibung und Ereignisdichtefunktion] dargestellten Ereignisdichtefunktionen.
C(I)
6
5
4
3
2
1
RP2
RP1
I
1 2 3 4 5 6
Abbildung 8-17. Rechenzeitanforderungsfunktion tc(I)
Mit Kenntnis der Prozessparameter läßt sich daraus die Rechenzeitanforderungsfunktion tC(I) erstellen
(Abbildung [Rechenzeitanforderungsfunktion tc(I)]). Beide Rechenprozesse sind - so das Ergebnis - innerhalb der gestellten Zeitanforderungen bearbeitbar.
tC (I) = ⌊
I − tDmax,2 + 6T − 0T
I − tDmax,1 + 6T − 0T
⌋ · 2T + ⌊
⌋ · 3T
6T
6T
tC (I) = ⌊
I
I
⌋ · 2T + ⌊ ⌋ · 3T
6T
6T
tC (0T ) = ⌊
0T
0T
⌋ · 2T + ⌊ ⌋ · 3T = 0T
6T
6T
tC (6T ) = ⌊
6T
6T
⌋ · 2T + ⌊ ⌋ · 3T = 5T
6T
6T
tC (7T ) = ⌊
7T
7T
⌋ · 2T + ⌊ ⌋ · 3T = 5T
6T
6T
Werden allerdings die Anforderungen derart vergrößert, dass bei gleichgebliebener Prozesszeit tP=6T die
maximal zulässige Reaktionszeit (Deadline) von RP1 auf tDmax,1 =3T und die maximal zulässige Reaktionszeit
von RP2 auf tDmax,2=4T festgelegt wird,
138
Kapitel 8. Echtzeitnachweis
2T
3T
3T
4T
tP =6T
tP =6T
ist bei unabhängigen Rechenprozessen eine fristgerechte Bearbeitung nicht mehr möglich (Abbildung
[Rechenzeitanforderungsfunktion tC(I) bei verschärften Bedingungen]).
tC (I) = ⌊
I − 4T + 6T − 0T
I − 3T + 6T − 0T
⌋ · 2T + ⌊
⌋ · 3T
6T
6T
tC (I) = ⌊
I + 3T
I + 2T
⌋ · 2T + ⌊
⌋ · 3T
6T
6T
tC (0T ) = ⌊
3T
2T
⌋ · 2T + ⌊ ⌋ · 3T = 0T
6T
6T
tC (3T ) = ⌊
5T
6T
⌋ · 2T + ⌊ ⌋ · 3T = 2T
6T
6T
tC (4T ) = ⌊
7T
6T
⌋ · 2T + ⌊ ⌋ · 3T = 5T
6T
6T
C(I)
7
6
5
4
3
2
1
1 2 3 4 5 6
I
Abbildung 8-18. Rechenzeitanforderungsfunktion tC(I) bei verschärften Bedingungen
Würde aber eine Abhängigkeit zwischen dem Auftreten der beiden Rechenprozesse bestehen, beispielsweise derart, dass das Ereignis 2 grundsätzlich zwei Zeiteinheiten (2T) nach Ereignis 1 auftritt, könnte die
Bearbeitung zeitgerecht erfolgen.
{( )( )}
ES:
6
0
6
, 2
1 2 3 4 5 6 7 8 9 10 11 t
Abbildung 8-19. Auflösung der Abhängigkeiten
Um dieses nachzuweisen wird aus den beiden Ereignisfolgen (Ereignisströmen) ein einzelner Ereignisstrom
gebildet und die Ereignisdichtefunktion erstellt (Abbildung [Auflösung der Abhängigkeiten]).
Die Rechenzeitanforderungsfunktion wird gemäß Gleichung Rechenzeitanforderungsfunktion berechnet.
Die Ereignisdichtefunktionen für die beiden Ereignistupel sind im negativen Bereich 0. Die Ereignisdichtefunktionen sind desweiteren im Bereich 0 bis 6 respektive 2 bis 8 stetig und haben dort den Wert 1.
139
Kapitel 8. Echtzeitnachweis
tC (I) = E1 (I − tDmax,1 ) · te,1 + E2 (I − tDmax,2 ) · te,2 = 0
tC (I) = ⌊
I − tDmax,2 + 6T − 2T
I − tDmax,1 + 6T − 0T
⌋ · 2T + ⌊
⌋ · 3T
6T
6T
tC (I) = ⌊
I + 3T
I − 4T + 6T − 2T
⌋ · 2T + ⌊
⌋ · 3T
6T
6T
tC (I) = ⌊
I
I + 3T
⌋ · 2T + ⌊ ⌋ · 3T
6T
6T
tC (0T ) = ⌊
3T
0T
⌋ · 2T + ⌊ ⌋ · 3T = 0T
6T
6T
tC (3T ) = ⌊
3T
6T
⌋ · 2T + ⌊ ⌋ · 3T = 2T
6T
6T
4T
7T
⌋ · 2T + ⌊ ⌋ · 3T = 2T
6T
6T
8T
5T
tC (5T ) = ⌊ ⌋ · 2T + ⌊ ⌋ · 3T = 2T
6T
6T
6T
9T
tC (6T ) = ⌊ ⌋ · 2T + ⌊ ⌋ · 3T = 5T
6T
6T
10T
7T
tC (7T ) = ⌊
⌋ · 2T + ⌊ ⌋ · 3T = 5T
6T
6T
11T
8T
tC (8T ) = ⌊
⌋ · 2T + ⌊ ⌋ · 3T = 5T
6T
6T
12T
9T
tC (9T ) = ⌊
⌋ · 2T + ⌊ ⌋ · 3T = 7T
6T
6T
13T
10T
tC (10T ) = ⌊
⌋ · 2T + ⌊
⌋ · 3T = 7T
6T
6T
tC (4T ) = ⌊
C(I)
7
6
5
4
3
2
1
1 2 3 4 5 6
I
Abbildung 8-20. Rechenzeitanforderungsfunktion tC(I)
Fußnoten
1. Da das Betriebssystem nicht im vorhinein weiß, wieviel Rechenzeit ein Job anfordert, wird zur Bestimmung der Last (Load) die Zahl der Anforderungen herangezogen.
2. Die untere Gaußklammer – die später noch kommen wird – ergibt die nächst kleinere Ganzzahl des
Ausdrucks innerhalb der Gaußklammer.
140
Literatur
[Abel90] Petrinetze für Ingenieure, Dirk Abel, Springer Verlag, Berlin 1990.
[Bengel2002] Verteilte Systeme, Günther Bengel, 2. Auflage, Braunschweig 2002, Vieweg Verlag.
[GeCa84] Architecture of the Space Shuttle Primary Avionics Software System , Gene D. Carlow, Volume
27, Number 9, 926-935, September 1984, Communications of the ACM .
[DIN66201] DIN 66201: Prozeßrechensysteme, Anonym, Beuth Verlag GmbH, Berlin 1981.
[EiMüSch2000] Formale Beschreibungsverfahren der Informatik, Helmut Eirund, Bernd Müller und Gerlinde Schreiber, 1. Auflage, Stuttgart 2000, Teubner Verlag.
[Färber2000] Prozeßrechentechnik, Manuskript zur Vorlesung Prozeßrechentechnik, Georg Färber, Stand
Wintersemester 1999/2000, Lehrstuhl für Realzeit-Computersyteme, TU-München.
[Gresser93] Echtzeitnachweis ereignisgesteuerter Realzeitsysteme, Klaus Gresser, Düsseldorf 1993, VDIVerlag GmbH.
[HalKona99] Sicherheitsgerichtete Echtzeitsysteme, Wolfgang A. Halang und Rudolf Konakovsky, 1. Auflage, München 1999, Oldenbourg Verlag.
[HerrHomm89] Kooperation und Konkurrenz: Nebenläufige, verteilte und echtzeitabhängige Programmsysteme, R.G. Herrtwich und G. Hommel, Berlin 1989, Springer Verlag.
kramer04 Elemente der Automatisierungstechnik, Teil 4: Systematischer Entwurf von Steuerungen Kramer
Ulrich http://autolab.fh-bielefeld.de/auto/WS03/AUTO4.pdf
[Kuhn99] Ein- und Ausgänge im Griff , Bernhard Kuhn, 10/1999, München 1999, Linux-Magazin Verlag.
[KuQu08] Kern-Technik Folge 41, Kunst Eva-Katharina und Quade Jürgen, 9/2008, München 2008, 88-91,
Linux-Magazin Verlag.
[Lauber76] Prozeßautomatisierung I, R. Lauber, Berlin 1976, Springer Verlag.
[LanSch94] Verteilte Systeme, Horst Langendörfer und Bettina Schnor, München 1994, Hanser Verlag.
[liu2000] Real-Time Systems, Jane W. S. Liu, Upper Saddle River 2000, Prentice Hall.
[Pressman92] Software Engineering: A Parctitioner’s Approach, Roger S. Pressman, 1992, 3. Edition,
McGraw-Hill.
[Quade2006] Linux-Treiber entwickeln, Jürgen Quade und Eva-Katharina Kunst, Heidelberg 2006, 2. Edition, dpunkt.Verlag.
[SchCaFrJa96] RTP: A Transport Protocol for Real-Time Applications RFC-1889 H. Schulzrinne, Casner,
Frederick und Jacobson 1. Februar 1996 http://www.ietf.org/rfc/rfc1889.txt
[Schlz94] Issues in designing a transport protocol for audio and video conferences
and
other
multiparticipant
realt-time
applications
H.
Schulzrinne
1994
http://www-users.cs.umn.edu/~zhzhang/cs8299/Readings/draft-ietf-avt-issues-02.ps
[SpeGif84] The Space Shuttle Primary Computer System , Alfred Spector und David Gifford, Volume 27,
Number 9, 874-900, September 1984, Communications of the ACM .
141
Literatur
[TimMon97] Windows NT Real-Time Extensions: an Overview, Martin Timmermann und Jean-Christophe
Monfret, 2/1997, Real-Time Magazine.
[Yodaiken] The RTLinux Manifesto, Victor Yodaiken, Department of Computer Science New Mexico Institute of Technology.
[ZöbAlb95] Echtzeitsysteme: Grundlagen und Techniken, Dieter Zöbel und Wolfgang Albrecht, 1. Auflage,
ISBN: 3-8266-0150-5, 1995, Thomson Publishing.
142
Stichwortverzeichnis
Betriebsmittel, 20
accept, 85
Aktor, 2
Anforderungsfunktion, 13
Asynchronous-IO, 48
Auftrittszeitpunkt, 9
Ausgangssignal, 2
Auslastung, 9, 10
durchschnittliche, 13
BCET, 127, 127
Benefitfunction, 15
Best Case Execution Time, 127
Betriebssystem, 8
bind, 85
Broadcast, 85
close, 46
Codesequenz, 10
cold standby, 109
connect, 85
Context Switch, 58
critical section, 68
Daemon, 56
Datenflussdiagramm, 113
Datenspeicher, 113
Dauerunverfügbarkeit, 105
Dauerverfügbarkeit, 105
Deadlock, 73
Determinismus, 4
Diagnose, 110
Diagramm
Datenfluss-, 113
Doppelsystem, 109
Echtzeit
hart, 14
weich, 14
Echtzeitbedingung
erste, 9, 14
Echtzeitnachweis, 127, 133
Ereignis
-funktion, 133
-Strom, 133
-Tupel, 133
abhängiges, 133
Erreichbarkeitsgraph, 120
Event
Software-, 88
Execution-Time, 10
Executive, 8
FIFO
Mailbox, 83
Filesystem, 54
gegenseitiger Ausschluß, 69
Gerätetreiber, 50
Gleichzeitigkeit, 9
Hardware-Latenzzeit, 58
Hardwareinterrupt, 22
High Availability, 109
Hintergrundspeicher, 54
hot standby, 109
Infrastruktur, 110
insmod, 51
Inter-Prozess-Kommunikation, 82
Interrupt
-Latency, 15, 58
-Service-Routine-, 22
Applikations-, 88
Hardware-, 22
Software-, 22
Vektortabelle-, 22
Interrupt Service Routine, 22
ioctl, 46
IPC, 82
ISR, 22
Job, 10
Kontextswitch, 17
Kontextwechsel, 17
Kontroll-Fluss, 113
Kostenfunktion, 14
kritischer Abschnitt, 68
Latenzzeiten, 15, 58
Laufzeitsystem, 8
Leistung
Prozessor-, 10
listen, 85
Load, 128
Mailbox, 83
Majornumber, 51
Management, 110
maximale Reaktionszeit, 128
Memory-Management-Unit, 22
Messages, 83
MMU, 22
MTBF, 104
MTTR, 104
Multicast, 85
Multitasking, 22
mutual exclusion, 69
Notfallpläne, 110
Notstromaggregat, 109
open, 46
overlap-structure, 48
143
p (Verfügbarkeit), 105
Parallelbetrieb, 107
PCB, 30
Petrinetz, 117
Pipe, 83
poll, 50
Power-fail-Interrupt, 109
Preemption, 17
Preemption-Delay, 58
Priority
Ceiling, 72
Inheritance, 72
Prioritäten, 16
-Vergaberegeln, 18
Prioritäts
-inversion, 72
-vererbung, 72
Process
Management, 22
Prozess
disjunkter-, 82
externer, 7
konkurrierender-, 82
kooperierender-, 82
Management, 22
Rechen-, 2
technischer, 1
Prozesse
nebenläufige-, 117
Prozesszeit, 13
minimale, 13
Pünktlichkeit, 4, 11
q (Unverfügbarkeit), 105
race condition, 68
RAID, 109
read, 46
non blocking-, 49
ReadFile, 48
Reaktionszeit, 128
maximal zulässige, 12
maximale, 12
Realtime
hard, 14
soft, 14
Realzeitkommunikationssystem, 5
Rechenarbeit, 10
Rechenprozess, 2
Rechenzeitanforderung, 128, 133
Rechnerkern, 22
Rechnerkernbelegung, 13
Rechtzeitigkeit, 4
Regelung, 2
144
Releasetime, 9
Rendevous, 88
Reparaturzeit, 104
repetitiver timer, 61
Scheduler, 30
Scheduling
-Deadline, 133
Points, 30
prioritätengesteuertes-, 127
Schnelligkeit, 4
Schnittstelle
Gerätetreiber-, 50
Segmentation Fault, 89
select, 50
Semaphore, 69
Sensor, 2
Serienbetrieb, 107
Services, 56
Shared-Memory, 84
Sicherheit, 4, 103
Signal, 89
Handler, 89
Signals, 88
single shot timer, 61
Skalierbarkeit, 20
sleep, 88
Socket, 85
Sockets, 84
Softwareinterrupt, 22
Speichermanagement, 22
standby
cold, 109
hot, 109
Stellglied, 2
Steuerung, 2
Synchronisation, 120
Systemcall, 58
Systemfunktion, 30
Task-Kontrollblock, 30
Task-Latency, 15, 58
TBF, 104
Technischer Prozess, 1
Time
Execution-, 10
Time-Demand Analysis, 128
Timer, 61
Timing, 61
TTR, 104
Unterbrechbarkeit, 16
Unterbrechungsfreie Stromversorgung, 109
Unverfügbarkeit, 103
USV, 109
Variable
Prozess-, 2
Vektortabelle, 22
Verarbeitungszeit, 10
Verfügbarkeit, 4, 103
Verfügbarkeits
-berechnung, 107
-ersatzschaltbild, 107
Verklemmung, 73, 117
Verzögerungszeit, 13
Vorhersagbarkeit, 4
Vorhersehbarkeit, 4
wakeup, 88
Wandler, 2
Warteaufruf, 48
Warten
explizites-, 48
implizites-, 48
Wartezeit, 13
WCET, 127
Worst Case, 124
write, 46
Zeit
Prozess-, 13
Verarbeitungs-, 10
Verzögerungs-, 13
Warte-, 13
Zeitgeber, 61
Zeitverwaltung, 61
Zuverlässigkeit, 103, 109
145
Herunterladen