Java Real-Time

Werbung
Institut für Informatik
Arbeitsgruppe Softwaretechnik
Warburger Str. 100
33098 Paderborn
Java Real-Time
Seminarausarbeitung
im Rahmen des Seminars
Trends in der Softwaretechnik
für Echtzeitsysteme
Sommersemester 2003
von
Stefan Scharberth
Bodenfelder Str. 15
37170 Uslar
betreut von
Dr. Holger Giese
Paderborn, September 2003
Inhaltsverzeichnis
1
2
3
4
5
6
Einleitung .......................................................................................................................... 3
1.1
Echtzeitsysteme .......................................................................................................... 3
1.2
Vorteile von Java........................................................................................................ 3
1.3
Echtzeitfähigkeit von Java ......................................................................................... 4
1.4
Realtime Specification for Java.................................................................................. 4
1.5
Aufbau eines Java RT Systems .................................................................................. 5
Speichermanagement ....................................................................................................... 6
2.1
HeapMemory.............................................................................................................. 6
2.2
ImmortalMemory ....................................................................................................... 6
2.3
Scoped Memory ......................................................................................................... 7
2.4
Mögliche Referenzen ................................................................................................. 8
Threads.............................................................................................................................. 9
3.1
Java Thread ................................................................................................................ 9
3.2
RealtimeThread ........................................................................................................ 10
3.3
NoHeapRealtimeThread........................................................................................... 11
3.4
Periodisch / Aperiodisch .......................................................................................... 12
3.5
AysncEventHandler ................................................................................................. 12
3.6
BoundAsyncEventHandler....................................................................................... 12
Scheduling ....................................................................................................................... 13
4.1
PriorityScheduler...................................................................................................... 13
4.2
ReleaseParameter ..................................................................................................... 14
Thread Synchronisation ................................................................................................ 16
5.1
Priority Inversion...................................................................................................... 16
5.2
Wait-Free-Queues .................................................................................................... 17
5.2.1
WaitFreeWriteQueue ....................................................................................... 17
5.2.2
WaitFreeReadQueue ........................................................................................ 17
5.2.3
WaitFreeDeQueue ............................................................................................ 18
5.2.4
Allgemeine Methoden ...................................................................................... 18
Beispiel SteamBoiler ...................................................................................................... 18
6.1
7
Umsetzung mit Java RT ........................................................................................... 19
Zusammenfassung und Fazit......................................................................................... 21
1 Einleitung
3
1 Einleitung
Der Markt für Echtzeitsysteme (engl. realtime system) ist in den letzten Jahren stark
gewachsen. Echtzeitsysteme werden in vielen Bereichen eingesetzt, von der Raumfahrt über
Telekommunikation bis hin zu Haushaltsgeräten sind sie nicht mehr wegzudenken. Durch den
ständig wachsenden Kostendruck, werden Firmen dazu gezwungen, Echtzeitsysteme immer
effizienter zu entwickelt und schneller auf den Markt zu bringen. Aus diesen Gründen ist die
Sprache Java sehr interessant für den Echtzeitbereich geworden, da es mit ihr möglich ist,
schnell komplexe Software zu entwickeln. Bevor diese Arbeit jedoch näher auf Java und ihre
Vorteile eingeht, werden erst einmal Echtzeitsysteme eingeführt.
1.1 Echtzeitsysteme
Echtzeitsysteme werden dadurch charakterisiert, dass sie neben der Forderung ein korrektes
Ergebnis zu liefern, Zeitrestriktionen einhalten müssen. Dies bedeutet, dass alle Aufgaben, die
ein Echtzeitsystem erfüllen muss, Zeitvorgaben (engl. Deadline) haben. Es werden zwei Arten
von Echtzeitsystemen unterschieden, harte und weiche. Unter harten Echtzeitsystemen
werden solche Systeme verstanden, bei denen die Zeitvorgaben zu 100 % einzuhalten sind.
Bei nicht einhalten ist das Ergebnis als falsch zu interpretieren, selbst wenn es korrekt ist.
Harte Echtzeitsysteme werden dort eingesetzt, wo das Verfehlen von Zeitanforderungen
katastrophale Folgen haben kann, z.B. Kernkraftwerk, Robotersteuerung oder Airbagsystem
bei einem Auto. Bei weichen Echtzeitsystemen hingegen werden einzelne Verstöße gegen die
Zeitvorgaben toleriert. Dies bedeutet, dass die Güte des Ergebnisses abgestuft wird, wenn es
die Zeitanforderungen überschreitet. Weiche Echtzeitsysteme werden oft in
Multimediabereich eingesetzt, z.B. bei Video- oder Audioübertragungen, wo eine kurze
Verzögerung akzeptabel ist.
Schaden
Deadline
Schaden
Deadline
Zeit
harte Echtzeit
Zeit
weiche Echtzeit
Abbildung 1-1: harte und weiche Echtzeit
1.2 Vorteile von Java
Es existieren bereits viele Möglichkeiten Echtzeitsysteme zu entwickeln. Warum ist es also
interessant Java Real-Time als Programmiersprache einzusetzen und nicht eine andere,
bewährtere Sprache zu nutzen? In der Einleitung wurde bereits erwähnt, dass in den letzten
Jahren ein immer größerer Kostendruck, bei der Entwicklung von Echtzeitsystemen,
entstanden ist. Dies führt dazu, dass neben dem Kriterium sicher funktionierende Software
herzustellen, immer mehr eine günstige und schnelle Entwicklung in den Vordergrund rückt.
Genau hier hat Java seine Vorteile. Jeder, der schon einmal mit Java gearbeitet hat, kennt den
Slogan „write once – run everywhere“, der das bekannteste Feature von Java wiedergibt, die
Portabilität. Portabilität bedeutet, dass der Java Bytecode ohne Veränderungen auf jeder
Plattform ausführbar ist, falls eine Java Virtual Machine für diese Plattform existiert. Dies ist
ein entscheidender Vorteil bei der Entwicklung, da jetzt nur noch eine Anwendung für alle
1 Einleitung
4
Plattformen programmiert werden muss. Auch die Wiederverwendung wird durch die
Portabilität entscheidend verbessert, da man auch Klassen aus Projekten wieder verwenden
kann, die für andere Plattformen entwickelt wurden. Weitere Vorteile von Java sind unter
anderem Objektorientierung, Robustheit, Sicherheit, Dynamik und Verteilung. Um diese
Vorteile zu vertiefen sei auf weiterführende Literatur [JavaNut97] verwiesen.
1.3 Echtzeitfähigkeit von Java
Java hat sich in den letzten Jahren auf Grund der oben genannten Vorteile weit verbreitet und
auch im Umfeld von Echtzeitanwendungen wurden Entwickler darauf aufmerksam. In der
ursprünglichen Form ist Java für Echtzeitsysteme wenig geeignet. Das hat den Grund, dass es
nie für den Einsatz in zeitkritische Anwendungen konzipiert war. Die folgenden Punkte
stellen die Probleme von Java in Echtzeitsystemen dar.
•
•
•
Speicherverwaltung: In Java gibt es nur einen Speicherbereich, den Heap Memmory
in dem alle Objekte abgespeichert werden. Es gibt keine Möglichkeit die
Lebensdauer von Objekten zu bestimmen, da der Garbarge Collector (GC) für das
Löschen von nicht mehr referenzierten Objekten zuständig ist.
Nichtdeterministisches Verhalten: Der Garbage Collector schaltet sich in
undefinierten Zeitabständen ein und löscht nicht mehr referenzierte Objekte. Da der
GC die höchste Ausführungspriorität in Java besitzt, verdrängt er den gerade
ausführenden Thread auf unvorhersagbare Zeit. Dadurch ist die genaue Laufzeit der
einzelnen Thread nicht bestimmbar.
Scheduling: Der Java Scheduler genügt den Echtzeitanforderungen nicht. In
Echtzeitsystemen müssen Startzeitpunkt, Deadline und Periode definiert werden
können. In Java existiert nur die Möglichkeit einem Thread eine Priorität zu
zuweisen oder ihn mit Thread.sleep(millSec) zu unterbrechen.
Um Java trotz dieser Probleme in Echtzeit Systemen einzusetzen zu können, musste die
Sprache erweitert werden. Diese Erweiterungen wurden in der Realtime Specification for Java
[RTSJ00] zusammengefasst.
1.4 Realtime Specification for Java
Bevor die Realtime Specification for Java entwickelt wurde, wurden Richtlinien für diese
Spezifikation festgelegt. Zu diesen Richtlinien gehören:
• Abwärtskompatibel, d.h. es sollen Standard Java Programme uneingeschränkt
lauffähig sein.
• Portabilität soll weiterhin unterstützt werden.
• Eine deterministische Ausführung von Java Code muss möglich sein.
• Es soll keine syntaktische Erweiterung geben, wie z.B. neue Schlüsselwörter.
• RTSJ soll den aktuellen Stand der Echtzeitentwicklung abdecken und für spätere
Erweiterungen offen sein.
In Realtime Specification for Java werden die die Erweiterungen beschrieben, die
vorgenommen wurden, um Java echtzeitfähig zu machen. Für diese Echtzeiterweiterungen
musste eine Ergänzungen der bestehenden API (Application Programming Interface)
vorgenommen werden. Die neue API wird dem Programmierer im dem Package
javax.realtime.* zur Verfügung gestellt und beinhaltet unter anderen echtzeitfähige Threads
und neue Speicherbereiche. Die wichtigsten Erweiterungen, die das Package zur Verfügung
stellt, werden in den Kapiteln Speichermanagement, Threads, Scheduling und Thread
1 Einleitung
5
Synchronisation vorgestellt. Um diese Erweiterungen nutzen zu können, wird ein
echtzeitfähiges System benötigt. Der Aufbau eines solchen Systems wird im folgenden
Kapitel, Aufbau eines Java RT Systems, vorgestellt.
1.5 Aufbau eines Java RT Systems
Der Aufbau eines Java Echtzeitsystems besteht aus den drei Schichten Echtzeitbetriebsystem
(Realtime Operation System, RTOS), Echtzeit Java Virtual Machine (TJVM1) und Java
Sourecode bzw. Java Libraries. Die unterste Schicht bildet das Echtzeitbetriebsystem. Es
dient als Schnittstelle zwischen der Hardware und der TJVM. Weiterhin stellt das RTOS
einen Scheduler zur Verfügung welches den entscheidenden Unterschied zu einen nicht
echtzeitfähigen Betriebsystem (OS) ausmacht. In der mittleren Schicht ist die TJVM, die als
Laufzeitumgebung für das Java Echtzeitprogramm dient. Die Unterschiede zu einer normalen
JVM bestehen darin, dass die TJVM die Echtzeiterweiterungen, die im Package
javax.realtime.* zusammengefasst sind, unterstützt. In der obersten Schicht befindet sich der
Java Sourcecode und die zur Verfügung gestellten Java Libraries. Bei einem Echtzeitsystem
enthalten die Libraries zusätzlich das Package javax.realtime.*, welches bei Java RT einfach
importiert werden kann. Damit stehen dann alle Echtzeit spezifischen Erweiterungen für Java
zur Verfügung.
Java
Libraries
Java
Application
Java
Application
Java +
JavaRealTime
Libraries
JVM
TJVM
OS
RTOS
Abbildung 1-2: Java und Java RT Schichten
In den folgenden Kapitel dieser Arbeit wird eine Übersicht über die wichtigsten Echtzeit
Erweiterungen von Java RT gegeben. Das Kapitel Speichermanagement zeigt die Erweiterung
des Speicherbereiches. Es wird vorgestellt wie die Lebensdauer von Objekten vom Garbage
Collector entkoppelt werden kann. Im Kapitel Threads wird gezeigt, wie Threads eine höhere
Priorität wie der Garbage Collector erhalten können, damit sie ein deterministisches Verhalten
bekommen. Außerdem geht dieses Kapitel darauf ein, wie den Threads Zeitvorgaben zugeteilt
werden können. Das Kapitel Scheduling werden die Scheduling Parameter der Threads
erläutert und der Standard Java RT Scheduler, der PriorityScheduler, wird vorgestellt. Da in
Echtzeitsystemen mehrere Threads existierten können, müssen diese untereinander
synchronisiert werden. Das Kapitel Thread Synchronisation beschriebt wie Echtzeit Threads
untereinander und Echtzeit Threads mit normalen Java Threads synchronisiert werden
können. Zum Schluss dieser Arbeit wird im Kapitel SteamBoiler Beispiel eine Beschreibung
1
Timesys Java Virtual Machine. Timesys ist das Unternehmen welches die erste Referenz Implementation von
RTSJ auf den Markt gebracht hat. [TS]
1 Einleitung
6
eines Echtzeitsystems und dessen Umsetzung in Java RT gegeben. Zu diesem Beispiel
existiert eine Implementation, die im Anhang zu finden ist.
2 Speichermanagement in Java RT
In Java werden alle Objekte im Heap Speicher angelegt, der vom GC verwaltet wird. Da der
Heap deswegen nicht echtzeitfähig ist, wurden in der RTSJ zusätzlich neben dem Heap
Memory zwei weitere Speicherbereiche definiert, Immortal Memory und Scope Memory.
2.1 Heap Memory
Jede echtzeit JVM und jede normale JVM hat genau eine HeapMemory Instanz. Der Vorteil
des Heapspeichers besteht darin, dass der Programmierer nicht selbst den Speicher verwalten
muss, da diese Arbeit der GC übernimmt. Objekte können mit dem Aufruf new Class()
angelegt werden. Auf alles was darüber hinaus geht, hat der Programmierer keinen Einfluss
mehr. Da der GC den Heap Memory verwaltet, hat er von allen Threads, die diesen
Speicherbereich benutzen, die höchste Priorität. Dies führt dazu, dass er alle Thread für
unbestimmte Zeit verdrängen kann, die auf dem Heap arbeiten. Dadurch ist eine Ausführung
von harten Echtzeitanwendungen auf dem Heap Memory nicht möglich. Lediglich für weiche
Echtzeitanwendungen, wo eine Verzögerung durch den GC toleriert werden kann, ist dieser
Speicherbereich geeignet.
Wenn ein Java RT Anwendung gestartet wird, befindet sie sich zunächst im Heap Memory.
Das bedeutet, wird ein neues Objekt angelegt, so liegt dieses im Heap. Wurde der Heap
Memory durch deinen Speicherwechsel verlassen ist es weiterhin möglich darin Objekte
anzulegen oder java.lang.Runable auszuführen, wie das folgende Beispiel zeigt.
HeapMemory hm = HeapMemory.instance(); //Referenz auf Heap Memory holen
hm.enter(Runable); // ein Runable im Heap ausführen, Threads
Object obj = hm.newInstance(Class); //ein Objekt im Heap anlegen
2.2 Immortal Memory
Wie auch beim Heap Memory existiert in einer echtzeit JVM nur eine ImmortalMemory
Instanz. Der Unterschied zu dem Heap Memory besteht darin, dass der GC keinen Zugriff auf
diesen Speicherbereich hat. Hier angelegt Objekte existieren bis zum Ende der Anwendung.
Es gibt keine Möglichkeit vor Ende der Applikation das Objekt aus dem Speicher zu
entfernen. Aus diesem Grund sollten man hier nur Objekte angelegen, die während der
gesamten Anwendung gebraucht werden. Da der GC auf diesen Speicherbereich keinen
Zugriff hat, ist er für harte Echtzeitanwendungen geeignet. Hier angelegte Objekte dürfen
auch auf Objekte im Heap Memory oder auf andere Objekte im Immortal Memory
referenzieren. Im Heap angelegte Objekte ist es ebenfalls gestattet auf Immortal Objekte zu
verweisen. Die Ausführung von java.lang.Runable Klasse und das Anlegen eines Objektes im
Immortal Memory, falls sich der aktuelle Thread in einem anderen Speicherberich befindet,
ist analog zum Heap Memory.
//Referenz auf Immortal Memory holen
ImmortalMemory im = ImmortalMemory.instance();
im.enter(Runable); // ein Runable im Immortal ausführen
Object obj = im.newInstance(Class); // ein Objekt im Immortal anlegen
3 Threads
7
2.3 Scoped Memory
Anderes als bei Immortal und Heap können vom Scoped Memory mehrere Instanzen in einer
echtzeit JVM existieren. Der GC bereinigt den Scope nicht automatisch, daher ist er für harte
Echtzeitanwendungen verwendbar. Ein Objekt im Scoped Memory hat aber dennoch eine
beschränke Lebenszeit. Jeder Scoped Memory hat einen Referenzzähler, der angibt wie viele
Threads Zugriff auf ihn haben. Hat der Zähler den Wert Null, so werden alle Objekte, die sich
in diesem Scoped Memory befinden gelöscht. Es besteht keine Möglichkeit einzelne Objekte
zulöschen, nur der komplette Scope kann gelöscht werden. In einem Scope können weitere
Scopes angelegt werden, diese werden dann als Children und der Erzeuger als Parent
bezeichnet. Es ist möglich, dass ein Scope beliebig viele Children hat, aber er besitzt einen
eindeutigen „Parent“ Scope. Diese Verschachtelung von Scope Bereichen wird auf einen
Stack abgebildet wie die Abbildung 2-1 zeigt.
T1
T2
A
A
B
D
Parent
C
A
B
D
C
E
E
Child
Abbildung 2-1: Scope Stack
Thread T1 befindet sich im Scope C, der B als Parent hat. Thread T2 befindet sich im Scope
E, der D als Parent hat. Die beiden Scope Bereiche B und D haben einen gemeinsamen Parent
Scoped A. Auch A hat einen Parent Scope, den so genannten „Primordial-Scope“, der den
Heap und Immortal repräsentiert und gleichzeitig das unterste Element im Stack darstellt.
Referenzen auf Objekte eines Scope Bereichs sind nur aus dem gleichen Scope oder aus einen
seiner Child Scopes möglich. Das bedeutet das Thread T1 Referenzen auf Objekte im Bereich
C, B, A, Heap und Immortal haben kann, aber nicht auf Objekte aus D und E. Analog für
Thread T2, er darf Referenzen auf die Scopes E, D, A, Heap und Immortal haben, aber nicht
auf B und C. Falls ein Thread T3 im Scope B existiert, kann er auf Objekte in B, A, Heap und
Immortal referenzieren aber nicht auf C, da C ein Child Scope von B ist somit nicht
referenziert werden darf.
Diese Einschränkungen haben den Grund, dass eine Referenz nie länger existieren darf als das
Objekt auf welches referenziert wird. Da im Scope Stack immer nur der oberste Scope
gelöscht werden kann, dürfen aus einem Scope alle Objekte referenziert werden die in Scopes
darunter liegen, da diese mindestens genau so lange existieren wie die Referenz. Würde Java
RT das Referenzieren des darüber liegenden Scopes erlauben, so kann es passieren, dass eine
Referenz auf ein Objekt zeigt welches nicht mehr existiert. Dies würde zu einem Fehler
führen und ist deswegen verboten. Zugriff auf einen Scopebereich haben damit alle Thread
die in dem selben Scope oder in einem darüber liegenden Scope laufen. Existieren keine
Thread mehr in diesen Scopes, so ist der Referenzzähler auf Null und alle Objekte im Scope
werden gelöscht.
Den Zugriff auf einen darunter liegenden Scope zeigt das folgende Beispiel:
3 Threads
8
// die aktuelle Stack tiefe
int depth = RealtimeThread.getMemoryAreaStackDepth();
// hole den eine Referenz auf den Parent Scoped
MemoryArea mem = RealtimeThread.getOuterMemoryArea(depth - 1);
Object obj = mem.newInstance(Class); // ein Objekt im ParentScope anlegen
Vor der Benutzung eines Scoped Memories, muss er erst einmal angelegt werden, dass
geschieht durch eine Objekt Erzeugung der Klasse LTMemory oder VTMemory.
LTMemory mem = new LTMemory( 1024 * 16, 1024 * 16);
Dieser Aufruf legt einen neuen Scoped Memory mit der Größe von 16 Kilobyte an wobei der
erste Parameter die initiale Größe und der zweite Parameter die maximale Größe angibt. Wie
schon erwähnt existieren zwei Arten von Scope Memories LTMemory (linear time memory)
und VTMemory (variable time memory). Der Unterschied zwischen den beiden Speicherarten
ist der Algorithmus der den freien Speicher verwaltet. Der LTMemory garantiert eine lineare
Zeit bei der Zuordnung eines neuen Objektes zu dem noch freien Speicher. Die Zeit ist dabei
abhängig von der Größe des neuen Objektes. Beim VTMemory kann der Programmierer
einen eigenen Algorithmus verwenden, um den Speicher zu verwalten, näheres dazu [Dib02].
Da alle Objekte in einem Scope Memory gelöscht werden, wenn sich kein Thread mehr in
ihm befindet, bietet er somit eine Möglichkeit dynamische Datenstrukturen in harter Echtzeit
zu verwenden. Das folgende Beispiel ist aus dem [Dib02] entnommen und zeigt die Run
Methode eines Threads, die bei jedem Schleifendurchlauf eine Runnable Klasse in einem
Scoped Memory ausführt. Nach jeder Ausführung wird der Scope Bereich wieder verlassen
und somit werden alle Objekte in ihm gelöscht. Beim nächsten Schleifedurchlauf steht somit
wieder ein leerer Scope zur Verfügung.
public void run() {
LTMemory mem=new LTMemory(1024*16, 1024*16) //Scoped Memory anlegen
while (true) {
mem.enter (java.lang.Runnable); // Runnable im Scope ausführen
}
}
2.4 Mögliche Referenzierungen zwischen den Speicherbereichen
Wie schon bei den einzelnen Speicherbereichen kurz erwähnt, besteht die Möglichkeit, dass
Objekte aus anderen Speicherbereichen referenziert werden können. Ein Objekt das in einem
Scope Memory liegt kann beispielsweise eine Referenz auf ein Objekt im Immortal Memory
besitzen, aber ein Objekt im Immortal Memory darf keine Referenz auf ein Objekt in einem
Scope Memory halten. Der Grund für diese eingeschränkten Referenzierungen ist, dass so
keine Referenzen länger existieren können als die Objekte auf die sie referenzieren. Die
nachstehende Tabelle gibt einen Überblick über alle Referenzen die in RTSJ erlaubt sind.
3 Threads
9
Referenz Referenz zumReferenz zum
zum Heap Immortal
Scope
Heap
Immortal
Ja
Ja
Ja
Ja
Nein
Nein
Scope
Ja
Ja
eingeschränkt2
Abbildung 2-2: mögliche Speicherreferenzen
3 Threads
Um das Problem zulösen, dass Threads vom Garbage Collector verdrängt werden können,
wurde die Java RT API um zwei neue Threads erweitert. Neben dem normalen Java Thread
gibt es in Java RT noch zwei weitere Threads, die für Echtzeitanwendungen geeignet sind.
Die beiden neuen Threads, RealtimeThread und NoHeapRealtimeThread, erben vom
normalen Java Thread und haben spezifische Echtzeit Erweiterungen. In folgenden Kapitel
werden alle drei Threads mit ihren Eigenschaften und ihren Anwendungsbereichen
vorgestellt.
Thread
RealtimeThread
NoHeapRealtimeThread
Abbildung 3-1: Thread Hierarchie
3.1 Java Thread
Für nicht zeitkritische Aufgaben ist es weiterhin möglich einen normalen Java Thread zu
verwenden. Dieser hat immer eine niedrigere Priorität als ein Echtzeit Thread und bekommt
daher nur Ausführungszeit zugewiesen, wenn alle Echtzeit Threads abgearbeitet sind.
2
Eingeschränkt bedeutet, Referenzen auf den gleichen oder im Stack darunter befindliche Scopes sind möglich.
3 Threads
10
Priority
NoHeap
Realtime
Thread
Garbarge
Collector
Realtime
Thread
Realtime
No Realtime
Thread
Abbildung 3-2: Thread Prioritäten
3.2 RealtimeThread
Ein RealtimeThread eignet sich nur für weiche Echtzeitanwendungen, da er eine niedrigere
Priorität als der GC hat und jederzeit durch ihn unterbrochen werden kann. Der Vorteil von
der niedrigeren Priorität ist, dass er den Heap Speicher weiterhin benutzen kann, was nicht
möglich wäre wenn er eine höhere Priorität als der GC hätte. Wie schon im Kapitel
Speichermanagement erwähnt, hat der Heap Memory den Vorteil, dass der GC die gesamte
Verwaltung dieses Speichers übernimmt und dem Entwickler somit die Arbeit erleichtert. Die
wichtigsten Unterschiede zu einem normalen Java Thread sind, dass er einen anderer
Prioritätsbereich hat und das ihm Zeitrestriktionen vorgegeben werden können. Während ein
Thread eine Priorität von 1 bis 10 besitzt, hat ein Realtime Thread eine Priorität über 11.
Diese wird beim Erzeugen des Threads mit dem Scheduling Parameter gesetzt.
Die Zeitrestriktionen werden durch den Release Parameter dem Thread übergeben. Der
Parameter beinhaltet eine Startzeit, eine Periode, eine Deadline, eine maximale
Ausführungszeit, ein OverRunHandler und ein MissHandler. Auf die Bedeutung der
einzelnen Release Parameter wird im Kapitel Scheduling genau eingegangen.
Zusätzlich zu einem normalen Thread bietet ein RealtimeThread die Möglichkeit an, dem
Konstruktor den Speicherbereich (engl. memoryArea) zu übergeben in dem der Thread
gestartet werden soll. Es können Instanzen von allen drei Speicherbereichen, Heap, Immortal
und Scope, übergeben werden. So kann der neue Thread in einen anderen Speicherbereich wie
der aktuelle Thread erzeugt werden.
Ein weiterer Parameter, der Speicherparameter (engl. memoryParameter), spezifiziert die
Speicherbeschränkungen. Über diesen Parameter kann eine maximale Grösse des
ImmortalMemory beziehungsweise des HeapMemory angegeben werden, die der Thread
benutzen darf. Wird dieser Parameter nicht gesetzt, so darf der Thread beliebig viel Speicher
verwenden.
Bei der Thread Erzeugung kann der Thread einer Gruppe zugeteilt werden. Dies geschieht mit
dem ProcessingGroupParameter. Der ProcessingGroupParameter besteht, wie auch der
ReleaseParameter, aus einer Startzeit, einer Periode, einer Deadline, einer maximalen
Ausführungszeit, einem OverRunHandler und einem MissHandler. Soll ein Thread einer
Gruppe zugeteilt werden, so wird dem Konstruktor des Thread, die Referenz auf das
ProcessingGroupParameter Objekt übergeben. Alle Threads, die das gleiche
ProcessingGroupParameter Objekt bekommenhaben, befinden sich in der selben Gruppe. Ein
Vorteil der Gruppeneinteilung ist beispielsweise, dass mehrere Threads eine gemeinsame
3 Threads
11
maximale Ausführungszeit haben. Dies bedeutete, steht der Gruppe 200 Millisekunden
Ausführungszeit zur Verfügung und verbraucht der erste Thread davon 80 Millisekunden, so
haben die restlichen Threads dieser Gruppe noch 120 Millisekunden zur Verfügung.
Im folgenden Beispiel wird die Erzeugung eines Realtime Thread dargestellt. Die Priorität
und die Release Parameter wurden zur Vollständigkeit in diesem Beispiel mit aufgenommen.
Eine ausführliche Beschreibung dieser Parameter folgt im Kapitel Scheduling.
//Priorität setzen
PriorityParameters priority = new PriorityParameters (50);
//Release Parameter
RelativeTime start = new RelativeTime (200, 0);
RelativeTime period = new RelativeTime (500, 0);
RelativeTime deadline = new RelativeTime (300, 0);
RelativeTime cost = new RelativeTime (100, 0); //maximale Ausführungszeit
MyMissHandler mmh = new MyMissHandler();
MyOverrunHandler moh = new MyOverrunHandler();
PeriodicParameters periodic = new PeriodicParameters (start, period, cost,
deadline, mmh, moh);
//Einen neuen Scope Speicherbereich mit der Größe 1 MB anlegen.
LTMemory memoryArea = new LTMemory (1024 * 1024, 1024 * 1024);
//Ein ProcessingGroupParameter Objekt erzeugen
RelativeTime groupStart = new RelativeTime (200, 0);
RelativeTime groupPeriod = new RelativeTime (500, 0);
RelativeTime groupDeadline = new RelativeTime (300, 0);
RelativeTime groupCost = new RelativeTime (100, 0);
MyMissHandler groupMMH = new MyMissHandler();
MyOverrunHandler groupMOH = new MyOverrunHandler();
ProcessingGroupParameter processingGroupParameter
new ProcessingGroupParameter (
groupStart,
groupPeriod,
groupCost,
groupDeadline,
groupMMH,
groupMOH);
=
//Speicherbeschränkungen festlegen
// kein Immortal Speicher, keine Speicherbeschränkung bei Heap
MemoryParamters memoryParamters = new MemoryParamters(
MemoryParamters.NO_MAX, //Heap
0);
//Immortal
//Thread erzeugen
Realtimethread rt = new RealtimeThread(
priority,
// Priorität setzen
periodic,
// Zeitrestriktionen
memoryParameters,
// Speicherbeschränkungen
memoryArea,
// Speicherbereich
processingGroupParameter, //Gruppenzuteilung
java.lang.Runnable);
3.3 NoHeapRealtimeThread
Im Gegensatz zum RealtimeThread ist der NoHeapRealtimeThread für harte
Echtzeitanwendungen geeignet. Er hat eine höhere Priorität als der GC und kann daher nicht
von ihm verdrängt werden. Wegen der höheren Priorität kann er aber keine Objekte mehr auf
dem Heap referenzieren. Genau wie der RealtimeThread hat er eine höhere Priorität als 11,
aber der Entwickler ist selbst dafür verantwortlich dass alle NoHeapRealtimeThreads eine
höhere Priorität haben als RealtimeThreads. Es darf
nicht vorkommen, dass ein
3 Threads
12
NoHeapRealtimeThread eine Priorität von 50 hat und ein RealtimeThread eine Priorität von
60, da sonst der NoHeapRealtimeThread auch von dem GC verdrängt werden könnte. Außer
der verschiedenen Priorität und dem Zugriff auf den Heap Memory sind die beiden Echtzeit
Threads identisch. Aus diesem Grund wurde auf ein Beispiel für die Erzeugung eines
NoHeapRealtimeThreads verzichtet.
3.4 Periodisch / Aperiodisch
Eine Ausführung der beiden Echtzeit Threads, RealtimeThread und NoHeapRealtimeThread,
ist periodisch und aperiodisch möglich. Aperiodisch bedeutet, der Thread wird in
unregelmäßigen und unbekannten Zeitabständen gestartet. Bei einer periodischen Ausführung
hingegen wird der Thread in gleichen Zeitabständen regelmäßig neu gestartet. Diese Abstände
heißen Periode und können im Release Parameter angegeben werden. Ein periodischer Thread
muss innerhalb seiner Periode beendet sein, da das System ihn sonst in der nächsten Periode
nicht neu starten kann. Jeder periodische Echtzeit Thread sollte eine Schleife mit dem
Methodenaufruf waitForNextPeriod() enhalten. Der Thread legt sich mit diesem Befehl
schlafen und wartet auf seine nächste Periode. Sobald die nächste Periode des Thread beginnt,
wird dieser automatisch vom System geweckt und arbeitet einmal seine Schleife ab, bis er
wieder zu waitForNextPeriod() kommt.
public void run() {
while (true) {
....
waitForNextPeriod();
}
}
3.5 AysncEventHandler
Zusätzlich zu den Threads gibt es in Java RT AsyncEventHandler (AEH) und AsyncEvent
(AE). Diese beiden Klassen dienen dazu, um auf externe Ereignisse zu reagieren. Ein
Plattform abhängiger Mechanismus informiert die echtzeit JVM über ein externes Ereignis,
beispielsweise das Auslösen eines Interrupt. Jedes Ereignis bekommt einen Namen zugeteilt
und kann über asyncEvent.bindTo(„Ereignisname“) an ein Java AsyncEvent Objekt
gebunden werden. Zwischen den AysncEvents und den AsyncEventHandlern besteht eine N
zu N Beziehung, das heißt ein AE kann mehrere AEH starten und ein AHE kann auf mehrere
AE warten. Mit dem Befehl asyncEvent.addHandler(handler) kann ein AEH an ein AE
gebunden werden. Bei der Erzeugung des Handlers wird per Parameter festgelegt ob ein
RealtimeThread oder ein NoHeapRealtimeThread erzeugt wird, wenn der Handler aufgerufen
wurde.
AsyncEventHandler(boolean nonheap, java.lang.Runnable logic);
3.6 BoundAsyncEventHandler
Außer dem AEH gibt es noch den BoundAsyncEventHandler (BAEH). Der Unterschied
besteht darin, dass beim BAEH in der Initialisierung ein Thread angelegt wird, der sobald ein
Aufruf des Handlers erfolgt nur noch starten muss. Beim AEH wird bei jedem Aufruf ein
neuer Thread erzeugt und anschließend gestartet. Nach der Ausführung wird der Thread
wieder gelöscht. Der Vorteil eines BAEH ist die Reaktionszeit auf ein Ereignis, er kann
schneller darauf reagieren, da alle Threads bereits existieren. Dem entgegen steht der
Nachteil, dass dadurch mehr Ressourcen belegt sind.
4 Scheduling
13
4 Scheduling
In einer Java Virtual Machine besteht die Möglichkeit beliebig viele Threads zu starten. Alle
Thread brauchen für ihre Ausführung Systemressourcen, die sie vom Scheduler zugeteilt
bekommen. Ziel ist es, eine Zuteilung der Ressourcen zu erreichen, so dass jeder Thread
rechtzeitig abgearbeitet werden kann und das eine faire Verteilung der Systemressourcen
stattfindet. In Java RT existiert eine abstrakte Klasse Scheduler von der alle konkreten
Scheduler Implementationen erben. Neben dem Standard Scheduler, dem PriorityScheduler,
bietet Java RT die Möglichkeit weitere eigene Scheduler zu implementieren. Dabei muss man
selber darauf achten das eine Interaktion zwischen allen verwendeten Scheduler gewährleistet
ist. Wie eine konkrete Interaktion aussehen könnte wurde nicht näher spezifiziert. Durch diese
Architektur wurde aber erreicht, dass Erweiterungen einfach möglich sind. Alle konkreten
Scheduler sind als Singelton zu implementieren, dass bedeutet, es kann nur eine Instanz von
der Klasse existieren. Um eine Referenz auf das eine Objekt zubekommen bietet jeder
Scheduler eine Static Methode instance() an.
Jeder Thread hat genau einen Scheduler, der ihm die Ressourcen zuteilt. Mit dem
Methodenaufruf realtimeThread.setScheduler(scheduler) kann der Scheduler für einen Thread
geändert werden. Wird keiner gesetzt, verwendet Java den Standard Scheduler, den
PriorityScheduler. Außerdem bietet die Klasse Scheduler eine static Funktion
Scheduler.setDefaultScheduler(scheduler) an, womit ein anderer Scheduler als Standard
definiert wird. Bei neu erzeugten Threads verwendet Java RT jetzt den neue Standard
Scheduler, wenn kein Anderer explizit gesetzt wurde. Auf bereits existierenden Threads hat
dieser Wechsel keinen Einfluss, das bedeutet, falls ein existierender Thread den Standard
Scheduler A bisher verwendet hat und der Standard Scheduler nun auf B wechselt, so
bekommt er weiterhin den Prozessor von A zugeteilt.
4.1 PriorityScheduler
Wie schon erwähnt ist der PriorityScheduler der Standard Scheduler in Java RT. Es ist der
Einzige, den die RTSJ mit bestimmten Eigenschaften vorschreibt. Er muss prioritätsbasiert
sein, das bedeutet, dass jeder Thread eine Priorität zugewiesen bekommt. Ein Thread mit
einer höheren Priorität wird gegenüber einem Thread mit niedrigerer Priorität bevorzugt. Die
Priorität kann beim Erzeugen festgelegt werden und ist während der Laufzeit veränderbar.
Wird keine festlegt so übernimmt der neue Thread die Priorität vom Erzeugenden, sofern dies
möglich ist. So kann z. B. ein NoHeapRealtimeThread die Priorität von einem anderen
NoHeapRealtimeThread erben, aber nicht von einem RealtimeThread, da diese in
unterschiedlichen Prioritätsbereichen liegen. Kann die Priorität nicht übernommen werden so
gibt es für jeden Threadtyp eine Standardpriorität, die er zugeteilt bekommt. In der RTSJ wird
vorgeschrieben, dass der PriorityScheduler neben den 10 Prioritäten für normale Java
Threads, mindestens 32 Prioritäten für Echtzeit Threads zur Verfügung stellen muss. Der
PriorityScheduler von der Firma Timesys [TS] bietet z. B. 255 Prioritäten an. Falls mehrere
Thread die gleiche Priorität besitzen bekommt der Thread Vorrang, der zuerst die
Systemressource angefordert hat, also First Come First Serve (FCFS). Zusätzlich muss der
PriorityScheduler auch preemptives Scheduling unterstützen, das bedeutet, dass jederzeit ein
höher priorisierter Thread den laufenden unterbrechen kann. Er muss also nicht warten bis der
niedriger priorisierte Thread die Ressourcen selbst wieder frei gibt. Das folgende Beispiel
zeigt wie die Priorität bei einem RealtimeThread gesetzt werden kann.
//setze die Priorität auf 31
// MIN_PRIRORITY ist eine Konstante mit dem Wert 11
SchedulingParameter sched = new PriorityParameters(
PriorityScheduler.MIN_PRIORTITY+20);
4 Scheduling
14
RealtimeThread rt = new RealtimeThread(sched);
//verändern der Priorität auf 20
SchedulingParameter sched = new PriorityParameters(20);
rt.setschedulingParameter(sched);
Nach welchem Schedulingverfahren die Prioritäten den Threads zugewiesen werden ist dem
Entwickler selbst überlassen. Es wird lediglich darauf verwiesen, dass das Rate Monotonic
Verfahren bei periodischen Thread zu guten Ergebnissen führt. Das Rate Monotonic ordnet
den Threads mit den niedrigsten Perioden die höchsten Prioritäten zu, näheres dazu kann
[Dib02] entnommen werden.
4.2 ReleaseParameter
Jeder Echtzeit Thread kann neben seiner Priorität auch ReleaseParameter besitzen. Im Release
Parameter sind alle Zeitvorgaben spezifiziert, die der Thread einhalten muss. Da es
periodische und aperiodische Echtzeit Threads gibt, müssen auch die ReleaseParameter in
PeriodicParameter und AperiodicParameter unterteilt werden. Die PeriodicParameter setzen
sich Zusammen aus der Startzeit, der Periode, der Deadline und der Ausführungszeit
(starttime, period, deadline, cost). AperiodicParameter beinhalten dagegen nur eine Deadline
und die Aufführungszeit. Alle vier Parameter sind Zeitrestriktionen, die mit Hilfe der Klasse
RelativeTime gesetzt werden.
RelativeTime
RelativeTime
RelativeTime
RelativeTime
starttime = new RelativeTime( 1000, 0); // Millisek., Nanosek.
period = new RelativeTime( 100, 0);
deadline = new RelativeTime( 50, 0);
cost = new RelativeTime( 30, 0);
Der Konstruktor von RelativeTime hat zwei Parameter, der erste ist Millesekunden und der
zweite Nanosekunden. Im oberen Beispiel wurde der Parmameter „starttime“ auf eine
Sekunde, der Parameter „period“ auf 100 Millisekunden usw. gesetzt. Zusätzlich zu den
Zeitrestriktionen können auch zwei AsyncEventHandler im ReleaseParameter angegeben
werden, ein MissHandler und ein OverrunHandler.
MyMissHandler mmh = new MyMissHandler();
MyOverrunHandler moh = new MyOverrunHandler();
// für periodische Threads
ReleaseParameter release = PeriodicParameter
(starttime, period, deadline, cost, mmh, moh);
// für aperiodische Threads
ReleaseParameter release = AeriodicParameter ( deadline, cost, mmh, moh);
// Thread erzeugen und starten
RealtimeThread rt = new RealtimeThread(
new PriorityParameters(15), // Priorität 15
release); // ReleaseParameter
rt.start();
Die Startzeit (starttime) gibt die Verzögerung an zwischen dem Aufruf rt.start() und dem
eigentlichen Starten des Threads an. Das heißt, wenn die Startzeit auf eine Sekunde gesetzt ist
startet der Thread genau eine Sekunde nachdem rt.start() aufgerufen wurde. Falls keine
Startzeit angegeben ist, startet der Thread sofort. Wie im Kapitel Thread schon erwähnt,
haben alle periodischen Threads eine Schleife, die sie immer wieder durchlaufen. Am Ende
dieser Schleife steht der Befehl waitForNextPeriod() mit dem der Thread schlafen gelegt
wird.
4 Scheduling
15
Der Parameter Periode gibt an, in welchem Zeitabstand die Periode des Threads neu beginnt.
Der Scheduler weckt ihn auf und er kann einmal seine Schleife durcharbeiten bis er wieder zu
waitForNextPeriod() kommt.
Die Deadline spezifiziert einen Zeitpunkt, an dem er Thread spätestens abgearbeitet sein
muss. Ist die Deadline z. B. auf 50 Millisekunden gesetzt, so muss der Thread spätestens 50
Millisekunden nach seinem Startzeitpunkt abgearbeitet sein, sonst kommt es zu einem Fehler
im Echtzeitsystem. Bei einem periodischen Thread muss die Deadline immer kleiner oder
gleich der Periode sein, da sonst der Thread noch ausgeführt werden könnte, obwohl schon
eine neue Periode für ihn begonnen hat, was nicht vorkommen darf. Ist keine Deadline
angegeben, so wird sie bei periodischen Threads gleich der Periode.
Der vierte Parameter gibt die maximale Zeit an, die ein Thread den Prozessor maximal
benutzen darf, dieser wird als Kosten (engl. cost) bezeichnet. Zum Beispiel, wenn die Kosten
50 Millisekunden sind, darf der Thread zwischen seinem Startzeitpunkt und seiner Deadline
maximal 50 Millisekunden ausgeführt werden. Daher müssen die Kosten immer kleiner oder
gleich der Deadline sein. Die Ausführung kann dabei in mehrere Blöcke unterteilt werden,
falls ihn ein höher priorisierter Thread unterbricht. Alle Blöcke müssen aber vor erreichen der
Deadline abgearbeitet sein und dürfen zusammen nicht die maximale Ausführungszeit
überschreiten. Falls ein Thread doch seine maximale Ausführungszeit überschreitet, führt das
erstmal zu keinem Fehler, falls er seine Deadline einhält. Es besteht aber die Möglichkeit das
andere Threads dadurch ihre Deadline verpassen, weil ihnen weniger Ausführungszeit zur
Verfügung stand. Werden keine Kosten angegeben so werden sie automatisch auf die
Deadline gesetzt.
Hält ein Thread seine Zeitvorgaben nicht ein, so wird dies durch einen AsyncEventHandler
behandelt. Der MissHandler startet, falls die Deadline verfehlt wurde und der
OverrunHandler, wenn der Thread seine maximale Ausführungszeit überschreitet. Bei
periodischen Threads wird die Periode während der Ausführung eines Handlers unterbrochen,
kann aber durch den Aufruf realtimeThread.schedulePeriodic() im Handler wieder gestartet
werden. Wie ein Handler auf das Verfehlen der Vorgaben reagieren soll, muss der Entwickler
selbst festlegen, da es von der jeweiligen Anwendung abhängig ist. Sind keine Handler
angegeben so reagiert der Scheduler nicht auf das Verfehlen der Zeitvorgaben und arbeitet die
Threads weiter ab.
Thread1
Ausführungszeit > cost −> overrunHandler
Periode
deadline −> missHandler
Thread2
Start
Periode
Abbildung 4-1: ReleaseParameter
5 Thread Synchronisation
16
5 Thread Synchronisation
In einer Echtzeit Java Virtual Machine können Threads parallel laufen. Wenn mehrere
Threads auf die gleichen Objekte oder Methoden zugreifen wollen, müssen diese Zugriffe
synchronisiert werden, da diese sonst zu Problemen führen können, z. B. ein Thread verändert
gerade ein Objekt während ein Anderer dieses gerade liest. Java RT benutzt wie auch Java,
Monitore die einen kritischen Bereich überwachen. Ein kritischer Bereich wird mit
synchronized { …} definiert und darf nicht von zwei Thread gleichzeitig betreten werden. Tritt
ein Thread in einen kritischen Bereich ein, sperrt der Monitor diesen für alle Anderen. Will
ein Thread einen gesperrten Abschnitt betreten, so wird dieser blockiert und in die
Warteschlange eingereiht. Ist der kritische Bereich wieder freigegeben, darf als nächstes der
Thread mit der höchsten Priorität eintreten, der sich in der Warteschlage befindet. Habe
Threads die gleiche Priorität so wird der Bevorzug, der schon länger wartet (First Come First
Serve).
5.1 Priority Inversion
Durch Monitore kann es vorkommen, dass höher priorisierter Thread von einem mit
niedrigerer Priorität blockiert wird. Im ungünstigsten Fall kann es sogar passieren dass der
höchst priorisierte Thread sogar auf alle Anderen warten muss. Dies kann vorkommen, wenn
er in einen kritischen Bereich eintreten will, der aber von dem Thread mit der niedrigsten
Priorität blockiert wird. Es werden erst alle Anderen ausgeführt bevor der Thread mit der
niedrigsten Priorität den Prozessor bekommt und den blockierten Bereichs wieder frei gibt.
Erst jetzt kann der höchst priorisierte Thread in den kritischen Abschnitt eintreten und sein
Programm weiter abarbeiten. Dieser Effekt wird als Priority Inversion bezeichnet und muss in
einem Echtzeitsystem verhindert werden. Um dieses Problem zu lösen existieren 2 Protokolle
in Java RT, Priority Inheritance (PI) und Priority Ceiling Emulation (PCE). Beim PI Protokoll
wird das Problem gelöst indem Prioritäten kurzfristig vererbt werden. Befindet sich ein
Thread in einem kritischen Bereich, kann er die Priorität der Threads aus der Warteschlange
erben, falls diese höher ist. Sind mehrere in der Warteschlange so wird die höchste Priorität
verwendet. Nach Freigabe des Bereichs wird die Priorität wieder zurückgesetzt. Beim PCE
Protokoll hingegen wird den einzelnen Ressourcen selbst eine Priorität zugeteilt. Sie sollte
gleich der Priorität sein, die der höchst priorisierte Thread hat, der in diesen Abschnitt
eintreten kann. Für ausführlichere Informationen über PI und PCE sei auf [Dib02] verwiesen.
Im Gegensatz zu Java muss ein Java RT Monitor immer eins der beiden oberen Protokolle PI
oder PCE verwenden. Dabei ist PI der Standard und wird von RTSJ verlangt. Falls PCE auch
angeboten wird, kann der Entwickler entscheiden, für welches Protokoll für die jeweilige
Ressource verwendet werden soll.
MyClass mc = new MyClass();
// für PI
PriorityInheritance pi = PriorityInheritance.instance();
MonitorControl.setMonitorControl(mc,pi);
// für PCE - Priorität 27
PriorityCeilingEmulation pce = new PriorityCeilingEmulation(27);
MonitorControl.setMonitorControl(mc,pce);
Beide Protokolle lösen das Problem der Priority Inversion indem sie den Threads kurzfristig
neue Prioritäten zuweisen. Das funktioniert nur unter der Voraussetzung, dass allen Threads
die gleiche Priorität zugewiesen werden kann. In Java RT gibt es aber drei verschiedene
Threads, die alle in unterschiedliche Prioritätsbereichen liegen. Ein normaler Java Thread
kann nicht die gleiche Priorität besitzen, wie ein RealtimeThread. Deswegen können die
beiden Protokolle nur zur Synchronisation zwischen gleichen Threads verwendet werden.
5 Thread Synchronisation
17
5.2 Wait-Free-Queues
Um zwei unterschiedliche Threads zu synchronisieren, bietet Java RT die so genannten WaitFree-Queues an, die WaitFreeWriteQueue (WFWQ), die WaitFreeReadQueue (WFRQ) und
die WaitFreeDeQueue (WFDQ). Alle drei Queues verbinden jeweils genau zwei Threads
miteinander, wobei sich an jedem Ende der Queue genau ein Thread befindet. Beim Anlegen
der Queue müssen dem Konstruktor beide Threads übergeben werden. Nur diese Threads
können später über die Queue kommunizieren. Falls sich ein Kommunikationspartner ändert,
muss eine neue Queue angelegt werden. Außerdem wird beim Erzeugen der Queue genau
festgelegt welches Ende für welchen Thread bestimmt ist, dadurch wird verhindert, dass ein
Thread auf das falsche Ende zugreifen kann. Alle Queues haben alle ein blockierendes und
ein nicht blockierendes Ende. Werden zwei Threads aus unterschiedlichen Prioritätsbereichen
damit synchronisiert, so sollte der Thread mit der höheren Priorität das nicht blockierende
Ende bekommen, damit der niedrigere Thread ihn nicht bei seiner Ausführung behindern
kann.
5.2.1 WaitFreeWriteQueue
Die WaitFreeWriteQueue erlaubt ein nicht blockierendes Schreiben und ein blockierendes
Lesen. Sie biete nur die Möglichkeit einer unidirektionalen Kommunikation, wobei der höher
priorisierte Thread am nicht blockierenden Ende schreibt (Writer-Thread) und der niedriger
priorisierte Thread am blockierenden Ende liest (Reader-Thread). Es können so nur Daten von
einem höher priorisierten zu einem niedriger priorisierten Thread geschickt werden. Ein
Anwendungsfall für diese Queue ist beispielsweise eine Visualisierung eines Echtzeitsystems.
Die Visualisierung selbst muss dazu nicht unbedingt in Echtzeit laufen, sie muss aber Daten
von dem Echtzeit Threads erhalten, um diese visualisieren zu können. Weiterhin ist es nicht
nötig, dass die Visualisierung Daten zum Echtzeit Thread schicken muss, weswegen eine
unidirektionale Verbindung ausreicht.
Der Writer-Thread kann mit dem Methodeaufruf wfwq.write(myObject) Objekte in Queue
legen. Wurde ein Objekt erfolgreich hineingelegt so wird der Booleanwert True
zurückgegeben. Lautet der Rückgabewert False, so deutet das darauf hin, dass die Queue voll
ist und keine weiteren Objekte aufnehmen kann. In diesem Fall bleib dem Writer Thread die
Möglichkeit statt write die Methode force zu benutzen, die das letzte Element in der Queue
überschreibt. Auf der anderen Seite der Queue kann der Reader Thread die hineingelegten
Objekte mit dem Aufruf wfwq.read() wieder herausholen. Ist sie leer, so wird der Thread
blockiert bis sich wieder ein Element in der Queue befindet.
WaitFreeWriteQueue wfwq = new WaitFreeWriteQueue(
myNoheapRealtimeThread, // Threads der in die Queue schreibt
myRealtimeThread,
// Threads der aus der Queue liest
elements,
// Größe der Queue
memmoryArea);
// Speicherbereich wo die Queue angelegt wird
5.2.2 WaitFreeReadQueue
Die WaitFreeReadQueue ist genau wie die WaitFreeWriteQueue für eine unidirektionale
Kommunikation. Der Unterschied ist das sie blockierendes Schreiben und ein nicht
blockierendes Lesen erlaubt. Dadurch bildet sie genau die entgegengesetzte
Kommunikationsrichtung ab. Das bedeutet Daten können nur von einem niedriger
priorisierten Thread zu einem höher priorisierten Thread geschickt werden. Mit dem Aufruf
wfrq.read() kann der Reader ein Element aus der Queue holen. Ist diese leer, so wird Null
zurückgegeben. Zusätzlich können auch Elemente mit dem Befehl wfrq.waitForData() aus der
Queue gelesen werden. Dabei wird der Thread allerdings solange blockiert bis mindesten ein
5 Thread Synchronisation
18
Element wieder in der Queue ist. WaitForData sollte also nur in Fällen benutzt werden, wenn
der Thread ohne das Element aus der Queue ist weiterarbeiten kann. Um diese Methode
benutzen zu können, muss im Konstruktor der WFRQ eine Boolean Variable gesetzt werden.
Um in die Queue zu schreiben wird wfrq.write(myObject) verwendet. Ist die maximale
Anzahl von Elementen bereits erreicht so wird der Writer Thread solange blockiert bis wieder
mindestens ein Speicherplatz frei ist.
5.2.3 WaitFreeDeQueue
Soll eine bidirektionale Kommunikation zwischen zwei Threads realisiert werden, muss man
entweder eine WFWQ und eine WFRQ verwenden oder eine WaitFreeDeQueue. Die WFDQ
hat die Methoden blockingRead, blockingWrite, force, nonBlockingRead und
nonBlockingWrite. Sie legt im Hintergrund eine WFWQ und eine WFRQ an und leite die
Methodenaufrufe einfach an die dementsprechende Queue weiter.
5.2.4 Allgemeine Methoden
Alle drei Queues haben zusätzlich noch allgemeine Methoden. Dazu gehören size, clear,
isEmty und isFull. Size liefert die Anzahl der Elemente zurück, die sich gerade in der Queue
befinden, während clear diese löscht. IsEmpty und isFull liefern jeweils einen Boolean Wert,
ob die Queue leer bzw. voll ist. Mit IsEmpty kann man z. B. vor dem Lesen am blockierenden
Ende überprüfen, ob die Operation möglich ist ohne zu warten, weil die Queue nicht leer ist.
IsFull kann analog vor dem Schreiben verwendet werden.
if
(!(wfwq.isEmpty()))
Object myobject = wfwq.read();
// falls WFWQ nicht leer ist
// lese Element
6 Beispiel SteamBoiler
Im folgenden Beispiel soll der Wasserstand eines SteamBoilers (Durchlauferhitzer) geregelt
werden. Dazu werden Sensoren und Aktoren verwendet. Sie dienen als Schnittstelle zwischen
dem Echtzeitcomputersystem und der physikalischen Welt. Über Sensoren werden Daten für
das Echtzeitcomputersystem erfasst während über Aktoren das System die physikalische Welt
steuern kann. In diesem Beispiel gibt es zwei Sensoren und sechs Aktoren. Der erste Sensor
gibt den Wasserstand (engl. waterlevel) des SteamBoilers an und der zweite den austretenden
Dampf (engl. exiting Steam). Die sechs Aktoren bestehen aus vier Pumpen (engl. pump)
einem Ventil (engl. valve) und einer Heizung. Die Aufgabe des Echtzeitcomputersystems ist
es den Wasserstand in dem Steamboiler zwischen dem maximalen (Wmax) und dem
minimalen (Wmin) Wasserstand zuhalten. Wird die Heizung des SteamBoilers eingeschaltet
so wird das Wasser erwärmt bis des verdunstet und als Dampf den Steamboiler verlässt.
Dadurch wird der Wasserstand im Boiler immer niedriger und das Echtzeitsystem muss eine
oder mehrere Pumpen aktiveren um Wasser hineinzupumpen. Ist wieder genug Wasser im
Boiler können die Pumpen wieder abgeschaltet werden. Erreicht der Wasserstand die
maximale Grenze, so öffnet das Echtzeitsystem das Ventil und lässt solange Wasser ab bis er
sich wieder im normalen Bereich befindet.
Im normalen Betrieb hat das Echtzeitcomputersystem also die Aufgaben die Sensoren
auszulesen, mit Hilfe der Sensordaten neue Aktorwerte zu berechnen und diese in die Aktoren
zu schreiben. Dabei müssen bestimmte Zeitvorgaben eingehalten werden, sonst könnte es
beispielsweise passieren das der Steamboiler ganz leer ist, bevor sich die Pumpen einschalten.
6 Beispiel SteamBoiler
19
Bei einem Steamboiler darf das nicht vorkommen, da er sonst beschädigt wird. Deswegen
muss für diesen Anwendungsfall ein Echtzeitcomputersystem eingesetzt werden.
Exiting Steam
Wmax
4 Pumps
Waterlevel
Wmin
Valve
Abbildung 6-1: SteamBoiler
6.1 Umsetzung mit Java RT
Dieses Beispiele wurde in Java RT mit drei periodischen Threads umgesetzt. Der erste
Thread, SteamBoilerSimulation, simuliert die Hardware, also den Steamboiler. Er liest die
Aktoren, berechtet die neuen Werte für die Sensordaten und schreibt diese in die Sensoren.
Da Hardware in Echtzeit läuft, wurde hierfür ein NoHeapRealtimeThread verwendet, der im
Immortal Speicher liegt. Der zweite Thread, SteamBoilerControl, ist für die Steuerung und
Überwachung des Steamboilers zuständig. Dieser Thread kann sich in den drei Zuständen
Stop, Init und Run befinden. Im Stop Zustand werden einfach alle Aktorwerte auf null. Von
Stop kann der Thread in Init wechseln, hier wird überprüft, ob der SteamBoiler betriebsbereit
ist. Dazu wird beispielsweide der Wasserstand kontrolliert, ist dieser nicht im vorgeschrieben
Bereich werden die Pumpen aktiviert bzw. das Ventil geöffnet. Ist er danach betriebsbereit so
wird die Heizung aktiviert und der Zustand des Threads wechselt nach Run. Dort werden
periodisch die Sensoren ausgelesen, neue Aktorwerte berechnet und in die Aktoren
geschrieben. Tritt ein Fehler auf oder wird der SteamBoiler abgeschaltet, wechselt der
Zustand wieder nach Stop und alle Aktorwerte werden auf null gesetzt. Da dieser Thread ein
Echtzeitsystem darstellt ist der ebenfalls ein NoheapRealtimeThread. Aber er läuft im
Gegensatz zu dem ersten Thread im Scope Memory und nicht im Immortal. Das hat den
Vorteil, dass es möglich ist während der Laufzeit neue Objekte und Variablen anzulegen. Der
dritte Thread, SteamBoilerVisualization, ist für die Visualisierung des Steamboilers auf dem
Bildschirm zuständig. Da die Visualisierung keinen Echtzeitanforderungen unterliegt, reicht
ein normaler Java Thread aus, der im Heap Memory läuft.
6 Beispiel SteamBoiler
20
Init
if (waterLevel < wmin) pumpsOn();
if (waterLevel > wmax) vavleOpen();
if (waterLevel <= wmax && waterLevel >= wmin) init = true;
initCycles−−;
[initCycles == 0]
/ pumpsOff()
& valveClose()
initialization
[init == true] / pumpsOff() & valveClose() & steamBoilerOn()
/ init = false
& initCycles = initCyclesConst
Stop
Run
stopActors();
readSensors();
decide();
writeActors();
stop
Abbildung 6-2: Zustandsdiagramm von SteamBoilerControl
Damit das System lauffähig ist, muss der Thread SteamBoilerSimulation mit den beiden
Anderen kommunizieren können. Die Kommunikation zu SteamBoilerControl ist über
Monitore realisiert, da beide NoHeapRealtimeTreads sind. Für jeden Actor bzw. Sensor
existiert ein Monitor, der im Immortal Memmory liegen muss. Würde die Monitore im Scope
Memmory liegen so könnte SteamBoilerSimulation nicht darauf zugreifen, weil keine
Referenzen vom Immortal auf den Scope erlaubt sind, siehe dazu Kapitel Scoped Memory.
Die Kommunikation zwischen den beiden Threads SteamBoilerSimulation und
SteamBoilerVisualization dagegen kann nicht über einen Monitor realisiert werden, da der
eine ein NoHeapRealtimeThread und der andere ein normaler Java Thread ist. Hierzu muss
eine WaitFreeQueue benutzt werden. Da lediglich Daten von der Simulation zur
Visualisierung geschickt werden, also nur von einem höher priorisierten zu einem niedriger
priorisierten Thread, ist hierfür die WaitFreeWriteQueue geeignet. Die Queue muss im
Immortal Memmory liegen, da die Simulation ein NoHeapRealtimeThread ist und deshalb
nicht auf den HeapMemmory zugreifen darf. Die folgende Abbildung zeigt die verwendeten
Klassen und in welchem Speicherbereich sie sich befinden.
6 Beispiel SteamBoiler
21
ImmortalMemory
<<Monitor>>
1
SteamBoilerControl
*
Actor
*
1
1
SteamBoilerSimulation
1
*
<<Monitor>>
Sensor
1
*
ScopeMemory
HeapMemory
1
SteamBoilerVisualization
1
1
WaitFreeWriteQueue
Abbildung 6-3: SteamBoiler Klassendiagramm
7 Zusammenfassung und Fazit
In dieser Ausarbeitung wurden die wichtigsten Erweiterungen von Java RT vorgestellt. Es
wurde gezeigt, dass es mit Java RT möglich ist, harte Echtzeit-, weiche Echtzeit- und normale
Anwendungen gleichzeitig in einer Virtual Machine auszuführen. Dabei wird durch den
Thread Typ festgelegt um welche Anwendung es sich handelt. Bei weichen Echtzeit- und
normalen Anwendungen ist es weiterhin möglich den Heap Speicher zu verwenden wodurch
dem Entwickler die Speicherverwaltung durch den Garbage Collector abgenommen wird.
Neben dem Heap Speicher gibt es in Java RT noch den ImmortalMemmory und den
ScopeMemmory. Während im Immortal keine Objekte gelöscht werden können, ist es
möglich einen kompletten Scope zulöschen. Damit können im Scope selbst unter harter
Echtzeit dynamische Datenstrukturen verwendet werden. Java RT hat einen prioritätsbasierten
Standardscheduler, es ist aber durch die spezielle Architektur möglich, diesen durch einen
anderen zu ersetzten oder mehrere parallel zu verwenden, wenn eine Interaktion gewährleistet
wird. Zur Thread Synchronisation von zwei gleichen Thread werden Monitore genau wie in
Java verwendet werden, mit dem Unterschied, dass hinter dem Monitor ein Protokoll stehen
muss um die Priority Inversion zu verhindern. Für unterschiedliche Thread gibt es die
WaitFreeQueue mit deren Hilfe sie miteinander kommunizieren können.
Trotz aller Vorteile die Java RT bietet, stellt sich beim Programmieren schnell heraus, dass es
auch einige Nachteile gibt. Die meisten Echtzeitcomputersysteme werden in Bereichen
eingesetzt, wo es nötig ist mit der Hardware zu interagieren. Hier liegt eine große Schwäche
von Java und Java RT. Um auf Hardware zuzugreifen, muss über das Java Native Interface
eine andere Programmiersprache angebunden werden, wie z: B. C, mit deren Hilfe der
Hardwarezugriff erfolgt. Java RT soll abwärts kompatibel sein, das bedeutet, normale Java
Programme sollen 100% ausführbar sein unter der Echtzeit Java Virtual Machine. Leider ist
das bei der benutzten Referenzimplemtation von der Firma Timesys nicht möglich. Sobald
das Pakage AWT benutzt wird stürzt die Virtual Machine mit der Fehlermeldung „Receiving
Signal 11“ ab. Die Fehlermeldungen stellen ein weiter Problem dar, denn die meisten
Fehlermeldungen, die man zur Zeit erhält, entsprechen der oben genannten und helfen dem
Entwickler nicht bei der Fehlersuche. Selbst kleine Fehler nehmen deswegen sehr viel Zeit in
7 Zusammenfassung und Fazit
22
Anspruch bis diese gefunden werden. Ein weiterer Nachteil von Java RT ist die Performance.
Da Java RT eine Erweiterung von Java darstellt, ist es noch langsamer als Java und damit
auch deutlich langsamer als ein C oder C++ Programm. Dadurch wird der
Anwendungsbereich von Java RT sehr eingeschränkt. Bei Massenprodukten wie z.B. einer
Waschmaschinensteuerung ist es nicht geeignet, da jede Waschmaschine sonst schneller
Prozessoren bräuchte, was die Herstellungskosten nach oben treibt. Java RT eignet sich eher
für große und komplexe Projekte, wie z. B. einer Robotersteuerung oder in der Raumfahrt.
Dort wo die Vorteile von Java, wie Robustheit oder Portabilität ausgenutzt werden können.
Die NASA entwickelt beispielsweise einen Mars Roboter, von dem einige Teile mit Java RT
umgesetzt werden sollen. Um nähere Informationen darüber zu bekommen sei auf [TS]
verwiesen. Auch die Firma Timesys entwickelt Java RT ständig weiter, so hat sie Mitte 2003
die erste kommerzielle Echtzeit Java Virtual Machine auf den Markt gebracht.
23
Literatur
[Dib02]
Peter C. Dibble. Real-Time Java – Platform Programming. Sun
Microsystem Press, 2002.
[JavaNut97]
David Flanagan. Java in a Nutshell. A Desktop Quick Reference.
O’Reilly, 1997.
[RTSJ00]
Greg Bollella, Ben Brosgol, Steve Furr, Savid Hardin, Peter Dibble,
James Gosling and Mark Turnbull. The Real-Time Specification for
JavaTM. Addison-Wesley, 2000.
[RTSPL97]
Alan Burns and Andy Wellings. Real-Time Systems and Programming
Languages. Addison-Wesley, 1997.
[GJRTC02]
Sven Burmester. Generierung von Java Real-Time Code für
zeitbehaftete UML Modelle. Diplomarbeit, 2002.
[TS]
Timesys Home Page: http://timesys.com
Anhang
SteamBoilerBsp.zip Diese Datei enthält den Java RT Sourcecode von dem vorgestellten
SteamBoiler Beispiel.
Roboter.html
Enthält Artikel von der Homepage Timesys über den in der
Zusammenfassung angesprochenen Java RT Roboter.
Herunterladen