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.