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