1Multitasking, Kontext, Real-Time & Co.
Das Zielsystem in diesem Buch sind Mikrocontroller. Dennoch soll der Einstieg in das Multitasking an einem weitaus mĂ€chtigeren Rechnersystem betrachtet werden. Jeder PC-Benutzer kennt bereits mindestens ein Multitasking-System, denn das PC-Betriebssystem erlaubt die AusfĂŒhrung mehrerer Programme nebeneinander.
Wenn ein Programm gestartet wird, entsteht als Laufzeitumgebung fĂŒr dieses Programm ein sogenannter Prozess. Der physikalische Speicher wird vom Betriebssystem an die laufenden Prozesse vergeben, jedoch hat jeder Prozess einen eigenen virtuellen Adressraum, in dem das Programm lĂ€uft. Die verfĂŒgbare Rechenzeit verteilt das Betriebssystem mithilfe des Schedulers an die vorhandenen Prozesse.
Der separate virtuelle Adressraum sorgt dafĂŒr, dass jedes Programm isoliert von den anderen laufen kann, ohne dass sich Programme gegenseitig beeinflussen. Innerhalb dieses virtuellen Adressraums werden der Programmcode und alle Variablen des Programms gespeichert.
1.1Kontext
Wenn man sich von einem Programm eine Momentaufnahme vorstellt, erhĂ€lt man dessen augenblicklichen Kontext mit den Werten aller Variablen, den Inhalten der CPU-Register, dem aktuell bearbeiteten Programmbefehl und der Liste der RĂŒcksprungadressen aus den aufgerufenen Unterprogrammen.
Der Scheduler verteilt die CPU-Zeit auf die vorhandenen Programme. Ein »Kontextwechsel« wird eingeleitet, indem der Scheduler den aktuellen Programmkontext speichert, aus der Liste der lauffĂ€higen Programme eines auswĂ€hlt und dessen frĂŒher gespeicherten Kontext wiederherstellt. Dieser Wechsel verlĂ€uft schnell genug, sodass der Nutzer den Eindruck gewinnt, mehrere Programme liefen gleichzeitig, obwohl tatsĂ€chlich immer nur eines die CPU belegt. Nur wenn es mehrere CPUs oder mehrere CPU-Kerne gibt, kann es echte Parallelverarbeitung geben.
Das Umschalten des kompletten virtuellen Adressraums ist eine aufwendige Angelegenheit. Daher bietet das Betriebssystem mit den Threads oder Tasks (hier synonym verwendet) kleinere Einheiten an, deren Kontext sich deutlich schneller umschalten lÀsst. Ein Programm kann dabei mehrere Tasks besitzen, die alle im virtuellen Adressraum des Programms laufen. Dabei ist in einem C-Programm eine Task nichts anderes als eine Funktion, die auf besondere Weise gestartet wird. Da alle Tasks eines Programms im selben virtuellen Adressraum existieren, sind die globalen Vereinbarungen allen Tasks gemeinsam zugÀnglich. Beispielsweise können die Tasks auf die globalen Variablen des Programms zugreifen. Dadurch lÀsst sich eine Kommunikation zwischen Tasks verwirklichen.
Den Kontext einer Task bilden die Inhalte der benutzten CPU-Register, die aktuelle Position im Programmcode, die eigenen lokalen Variablen und die Liste der RĂŒcksprungadressen aus den Unterprogrammen, die die Task aufgerufen hat.
Die RĂŒcksprungadressen und die lokalen Variablen liegen auf dem Stack-Speicher, daher braucht jede Task einen eigenen Stack. Wenn eine Task eine C-Funktion aufruft, lĂ€uft diese im Kontext der Task, d. h., die lokalen Variablen der aufgerufenen Funktion liegen auf dem Stack der Task, die die Funktion aufgerufen hat. Eine Funktion kann ohne Weiteres von mehreren Tasks gleichzeitig aufgerufen werden, alle lokalen Variablen existieren dann mehrfach auf verschiedenen Stacks, ohne sich zu stören. (Ausnahme: static-Variablen einer Funktion existieren nur einmal, also greifen alle Tasks auf denselben Speicherplatz zu. Das kann spezielle SchutzmaĂnahmen erforderlich machen.)
Vor dem Umschalten auf eine andere Task muss der Scheduler den Task-Kontext abspeichern: Alle CPU-Registerinhalte und die Adresse des nĂ€chsten auszufĂŒhrenden Befehls sowie der private Stack der Task werden gerettet, damit spĂ€ter an derselben Stelle nahtlos weitergemacht werden kann. Der Kontextwechsel bei Tasks ist viel schneller durchfĂŒhrbar als bei Prozessen, da viel weniger Daten gesichert werden mĂŒssen.
Auf einem Mikrocontroller kann nur ein Programm zur gleichen Zeit laufen, das aber mehrere Tasks starten kann. Auch wird man ohne komplettes Betriebssystem auskommen. Die Verwaltung der Tasks und das Umschalten zwischen ihnen ĂŒbernimmt ein Scheduler, der in das eigene Programm integriert wird.
Beispiele fĂŒr verschiedene Arten solcher Scheduler finden sich in den Kapiteln »Multitasking, die Erste: Die Minimalversion« bis »Multitasking, die Vierte: PrĂ€emptives Tasking«.
1.2Zustand einer Task
Jede Task befindet sich in einem von mehreren möglichen ZustÀnden, die in der nachfolgenden Abbildung dargestellt sind. Die Task, deren Kontext gerade geladen ist, befindet sich im Zustand laufend. Existiert lediglich eine CPU mit nur einem Kern, kann auch nur eine Task gleichzeitig in diesem Zustand sein. Andere Tasks sind bereit, d. h., sie könnten zwar laufen, aber der Scheduler hat sie zurzeit nicht aktiviert. Soll ein Task-Wechsel stattfinden, muss der Scheduler die laufende Task deaktivieren, indem er ihren Kontext speichert und ihren Zustand auf bereit setzt. Aus der Menge der bereiten Tasks wÀhlt er eine andere Task aus und aktiviert sie, indem er ihren Kontext lÀdt und ihren Zustand auf laufend setzt. Im folgenden Abschnitt »Scheduler« wird auf mögliche Kriterien eingegangen, nach denen der Scheduler seine Auswahl trifft.
Bild 1.1: Zustandsdiagramm einer Task.
Eine laufende Task kann blockieren, wenn sie auf ein Ereignis wartet, z. B. dass eine Wartezeit verstreicht. Einer blockierten Task wird der Scheduler sofort die CPU-Zeit entziehen. Ihr Kontext wird gespeichert, und sie geht in den Zustand blockiert ĂŒber. Erst wenn das erwartete Ereignis eintrifft, z.B. die Wartezeit vorĂŒber ist, wird die Task wieder lauffĂ€hig und darf sich in die Liste der bereiten Tasks einreihen. Zu welchem Zeitpunkt sie dann erneut lĂ€uft, entscheidet der Scheduler.
Der letzte Zustand, suspendiert, ist nicht zwingend nötig. Viele Systeme verfĂŒgen ĂŒber ein spezielles Kommando suspend(Task X), um Task x vorĂŒbergehend vom Scheduling auszuschlieĂen. Erst mit der AusfĂŒhrung des Kommandos resume(Task X) wird Task x wieder als bereit markiert.
1.3Scheduler
Die Verteilung der verfĂŒgbaren Rechenzeit auf die verschiedenen Tasks kann nach ganz unterschiedlichen Kriterien erfolgen. Stellvertretend fĂŒr alle anderen sollen hier drei einfache Scheduling-Prinzipien dargestellt werden.
1.3.1Kooperatives Scheduling
Bei diesem Verfahren bleibt die gerade laufende Task ohne ZeitbeschrĂ€nkung aktiv. Ein Task-Wechsel findet nur statt, wenn die laufende Task blockiert, z.B. indem sie sich selbst fĂŒr eine gewisse Zeit schlafen legt ('sleep(..)'). Damit alle Tasks Rechenzeit erhalten, mĂŒssen sich alle kooperativ verhalten und den anderen freiwillig CPU-Zeit einrĂ€umen. Der Scheduler entscheidet bei einem Kontextwechsel, welche Task als NĂ€chstes lĂ€uft. Er hat aber keinen Einfluss darauf, wie viel Rechenzeit sie bekommen wird. Auf einen Scheduler nach diesem Prinzip wird im Kapitel 6 »Multitasking, die Dritte: Kooperation ist gefragt« im Detail eingegangen.
1.3.2Round Robin Scheduling
Hier wird die verfĂŒgbare Rechenzeit gleichmĂ€Ăig auf alle Tasks verteilt, wie in der nĂ€chsten Abbildung dargestellt. Jede der existierenden Tasks (Te, Ta etc.) erhĂ€lt eine gleich groĂe Zeitscheibe, die Reihenfolge der Aktivierung ist von Anfang an festgelegt. Es gibt keine unterschiedlichen PrioritĂ€ten, alle werden gleichbehandelt.
Bild 1.2: »Round Robin Scheduling« â gleiche Zeitscheiben fĂŒr alle.
Man kann einerseits eine Systemperiode TPer vorgeben, dann bekommt jede der n Tasks eine Zeitscheibe der GröĂe
Bei wachsendem n wird die Zeitscheibe T immer kĂŒrzer, und das System beschĂ€ftigt sich zunehmend mit den Kontextwechseln und immer weniger mit den eigentlichen Aufgaben der Tasks. Andererseits kann man die GröĂe der Zeitscheibe T vorgeben. Bei wachsender Task-Anzahl n dauert es dann immer lĂ€nger, bis die einzelne Task wieder zum Zuge kommt.
1.3.3PrÀemptives Scheduling
Bei dieser Art Scheduling kann eine Task jederzeit unterbrochen werden, sobald der gerade laufende Maschinenbefehl abgearbeitet ist. Es gibt keine feste Task-Reihenfolge und keine konstanten Zeitscheiben. Stattdessen erhĂ€lt jede Task eine PrioritĂ€t zugeordnet. Aus der Menge der bereiten Tasks wird immer die mit der höchsten PrioritĂ€t aktiviert. Sobald eine Task lauffĂ€hig wird, deren PrioritĂ€t die der gerade laufenden ĂŒbersteigt, findet sofort ein Kontextwechsel statt â die laufende Task wird verdrĂ€ngt. Die Abbildung stellt das Prinzip fĂŒr drei Tasks Ta, Tb, Tc dar.
Bild 1.3: PrÀemptives Scheduling: Aus den bereiten Tasks lÀuft die mit der höchsten PrioritÀt.
Zum Zeitpunkt 0 sind Ta und Tc bereit, Tb ist blockiert. Die PrioritÀt von Ta ist höher als die von Tc, daher aktiviert der Scheduler Ta. Zum Zeitpunkt 1 blockiert Ta, und aus der Menge der bereiten Tasks wird die mit der höchsten PrioritÀt aktiviert, hier also Tc. Zum Zeitpunkt 2 wird Tb bereit und verdrÀngt aufgrund ihrer PrioritÀt sofort Tc. Sobald Ta zum Zeitpunkt 3 lauffÀhig ist, verdrÀngt sie Tb.
1.4Multitasking und EchtzeitfÀhigkeit
Die bisher angesprochenen Scheduling-Verfahren sagen noch nichts ĂŒber die EchtzeitfĂ€higkeit eines Systems aus. Ein Multitasking-System verwaltet Tasks und teilt ihnen Rechenzeit zu, aber es wird zunĂ€chst keine Angabe ĂŒber die Verzögerungszeit bei der Aktivierung von Tasks gemacht. Ein Echtzeitsystem muss in der Lage sein, bestimmte Anforderungen innerhalb einer maximalen Zeit zu bearbeiten. Es ist z.B. zu garantieren, dass eine hochpriorisierte Task spĂ€testens 10 ms, nachdem sie lauffĂ€hig wurde, auch tatsĂ€chlich aktiviert wird.
Das Eintreten eines Ereignisses, z.B. der Tritt auf das Bremspedal eines Fahrzeugs, soll eine Task aktivieren, z.B. einen Teil des Bremsassistenten im Fahrzeug. Die folgende Abbildung zeigt die Latenzzeit zwischen dem Tritt auf das Pedal und der Aktivierung der Task, also dem Beginn des Bremsvorgangs. Ein echtzeitfÀhiges System kann garantieren, dass die Latenzzeit immer kleiner sein wird als eine bestimmte Maximalzeit. Wie groà diese Maximalzeit sein darf, hÀngt dabei allein von der Anwendung ab. Ein nicht echtzeitfÀhiges Multitasking-System wird die Task auch aktivieren, sich aber nicht festlegen, wann die Bremse spÀtestens aktiv wird.
Neben der Latenzzeit ist auch die Antwortzeit bis zur vollstĂ€ndigen Bearbeitung der Task von groĂer Bedeutung. Die »Zeitschranke« einer Task ist die maximal tolerierbare Antwortzeit, bestehend aus Latenzzeit plus Bearbeitungszeit. Wenn eine Task ihre Zeitschranke immer einhalten kann, spricht man von Rechtzeitigkeit, halten alle Tasks ihre Zeitschranken ein, erreicht man Gleichzeitigkeit.
Bild 1.4: Ein Echtzeitsystem hÀlt vorgegebene Zeitschranken ein.
Die in diesem Buch in den Kapiteln 4 »Multitasking, die Erste: Die Minimalversion« bis 6 »Multitasking, die Dritte: Kooperation ist gefragt« vorgestellten Scheduler sind nicht echtzeitfĂ€hig. Ihre maximalen Latenzzeiten hĂ€ngen vom Anwenderprogramm ab. Mit diesen Schedulern lassen sich Echtzeitanforderungen nur begrenzt ĂŒber Hardware-Interrupts realisieren. Im Kapitel 7 »Multitasking, die Vierte: PrĂ€...