Real Java für Real-Zeit-Applikationen
plus Java public-domain experimentelle Software

Sergio Montenegro (.)
GMD-FIRST (http://www.first.fhg.de)

 

Bei der Entwicklung sicherheitsrelevanter real-Zeit-eingebetteter Systeme, deren Versagen tödliche Folgen haben kann, ist ADA mit Abstand die vorgezogene (geeignetste) Programmiersprache. Dennoch beharren vielen Entwickler auf C und C++, obwohl diese Sprachen allgemein als unsicherer eingestuft werden. Für die C-Rebellen gibt es jetzt eine neue Hoffnung: "Java". Java vereinigt Sicherheit mit der geliebten C (und C++) Syntax. Aber noch sind einige Steine aus dem Weg zu räumen, bevor aus Java die ideale Programmiersprache für sicherheitsrelevante eingebettete Real-Zeit-Systeme wird. Sachlich gesehen, wäre ADA immer noch die erste Wahl, aber die persönliche Neigung des Programmierers spielt auch eine wichtige Rolle (für die Sicherheit). Deswegen wird so viel in Java investiert. Z.Z. ist Java für diese Applikationen kaum geeignet, aber das Interesse und Potential daran sind enorm. Java ist noch eine sehr junge Sprache und ihre Mankos sind bekannt. Es wird weltweit sehr intensiv daran gearbeitet, um aus Java die ideale Programmiersprache für solche Systeme zu machen. Folgende Tabelle vergleicht diese Sprachen.

Sprache

C/C++

ADA

Java

Zukünftige Java

Sicherheit

- -

+ +

+ +

+ +

I/O-Anbindung

+ +

+ +

- -

- -

Real-Zeit

-

+ +

- -

+ +

Effizienz

+ +

+

- -

0

Parallelität

- -

+ +

+ +

+ +

Lesbarkeit

- -

+

+

+

Flexibilität

+ +

0

0

0

Portabilität

-

+

+ +

+ +

Die Kriterien sind nach Wichtigkeit für die Gebiete von sicherheitsrelevanten eingebetteten Real-Zeit-Systemen aufgelistet. Sicherheit ist für diese Anwendungen das wichtigste und gerade hier liegen die Schwächen von C/C++. Die I/O-Anbindung und Real-Zeit-Unterstützung sind wichtig, um die Kontrollsoftware in ihre Umgebung (HW + Zeit) einzubetten. Genau hier und bei der Effizienz liegen die Schwächen von Java. Die Defizite von Java bei der I/O-Anbindung sind:

 

Die größten Defizite bei der Real-Zeit-Unterstützung zeigt Java in der Implementierung des Garbage Collectors, der extrem dynamischen Speicherverwaltung und der schwierigen Ausführungszeit-Vorhersage. An diesen Punkten wird weltweit sehr intensiv gearbeitet (wir auch).

Die Java-Effizienz hat zwei Defizite: Die langsamere Ausführung auf Grund der Interpretation und/oder "on the fly"-Übersetzung des Bytecodes und der relativ große Speicherverbrauch für den Interpeter oder für den Just-In-Time-Compiler. Um diesen Flaschenhals zu beseitigen, werden spezielle Java-Prozessoren, die den Bytecode in Hardware interpretieren, entwickelt. Für die, die nicht schon wieder einen neuen Prozessor einsetzen können, entwickeln wir einen Übersetzer von Bytecode auf den Assembler verschiedener Standard-Prozessoren. Damit werden diese zwei Effizienz-Defizite von Java in eingebetteten Systemen ausgeglichen. Die Übersetzer werden wir in einigen Monaten als Public-domain-Software unter der am Ende genannten URL (im WWW) anbieten.

Die Unterstützung der Parallelität ist wichtig, weil Real-Zeit-, Steuerungs- und Regelungs-Systeme meistens als parallel laufende Tasks implementiert werden. Die Lesbarkeit ist wichtig, weil sie das Überprüfen (Audits) von Code vereinfacht, was die Sicherheit steigern kann. Die Flexibilität ist wünschenswert, um das Programmieren zu vereinfachen. Dies kann wiederum die Sicherheit steigern, denn komplexe Programme sind eine Gefahrenquelle.

Portabilität ist für viele Applikationen sehr wichtig. Für eingebettete Real-Time-Systeme ist sie nicht so relevant. Denn meistens wird die Software speziell für eine gegebene Hardware (I/O, Interrupts, Redundanz,...) entwickelt, abgestimmt und optimiert. Um die Kontrollsoftware in einer anderen Anlage laufen zu lassen, reicht es meistens nicht aus, sie einfach erneut zu kompilieren.

Java bietet auch folgenden Vorteile gegenüber C/C++ an:

 

Allerdings, Programmieren in Java hat einen kleinen Nachteil. Man muß sich in einem Dschungel von mehr 510 Klassen mit Tausenden von Beziehungen zurechtfinden. Wenn man schließlich denkt: "Jetzt kann ich es!", dann kommt das neue Java-Release.

Java in Real-Zeit

Dennoch, wie gesagt, ist Java noch nicht die ideale Programmiersprache für Real-Zeit-Anwendungen, aber ihre Probleme sind bekannt, und es wird weltweit daran gearbeitet. Besonders wird Java für diese Applikationen interessant werden, wenn die ersten Java-Prozessoren verfügbar sind. Diese Prozessoren sind schon seit langer Zeit versprochen, aber sie werden immer wieder verschoben.

Die größten Schwierigkeiten von Java für Real-Zeit-Applikationen sind:

Speicherverwaltung

Für die Freigabe von alloziierten Speicherbereichen benutzt Java keine delete- oder free-Funktion, sondern wenn mehr Speicher benötigt wird (new-Operator) als z.Z. im Heap vorhanden ist, werden alle laufenden Tasks für eine Weile suspendiert, während der Garbage Collector den gesamten Speicher untersucht und nach Blöcken sucht, zu denen es keine Referenz mehr gibt. Diese Blöcke werden als frei markiert und können wieder alloziiert werden. Man kann nicht vorhersagen wann und für wie lange der Garbage Collector aktiviert wird, und der Gargabe Collector kann nicht unterbrochen werden, bis er fertig ist. Dies kann das gesamte Timing der Applikation über Bord werfen und kritische Deadlines können verpaßt werden. Dies kann wiederum zu einem Systemversagen führen.

Eine Alternative ist hier ein inkrementeller Garbage Collector, der seine Aufgabe in kurze Phasen aufteilen kann. Ein inkrementeller Garbage Collector kann immer dann, wenn der Prozessor keine zeitkritischen Aufgaben ausführt, für eine kleine überschaubare Zeit aktiviert werden. Solch ein Garbage Collector gehört noch nicht zur Standard-Java-Umgebung, aber es sind bereits einige mit experimentellem Charakter fertiggestellt worden. Andere sind dabei, eine Java-Virtuelle-Maschine ohne Garbage Collector zu implementieren. An der Stelle wird eine delete-Funktion, wie man sie von C gewohnt ist, angeboten. Dies ist eine gute Lösung für kompakte eingebettete Systeme.

Das Sicherste für zeitkritische Aufgaben ist, alle benötigten Speicherbereiche im voraus, bei der Initialisierungsphase, zu alloziieren und während der Regelungs- oder Steuerungsaufgabe überhaupt keinen weiteren Speicher mehr zu alloziieren (keinen new-Operator und keine delete-Funktion mehr zu benutzen).

Ein ähnliches Problem, aber nicht Java-spezifisch, kommt vom virtuellen Speicher. Wenn der benötigte Speicherbereich ausgeswapt (ausgelagert) wurde, muß der betroffene Task warten, bis der Speicherbereich von der Festplatte zurückgeholt wurde. Dies kann wiederum das gesamte Timing des Programms ruinieren. Deswegen wird bei zeitkritischen Systemen kein virtueller Speicher benutzt, oder die benötigten Bereichen müssen gelockt (unswapbar) sein.

I/O

Die Einbettung von Java in eine spezielle Hardware (mit Sensoren und Aktuatoren) ist nicht vorgesehen. Es ist nicht möglich, auf bestimmte Register oder Adressen zuzugreifen. Man muß eine abstrakte Schnittstelle benutzen, die mittels einer anderen Programmiersprache (z.B. C) implementiert wurde. Bei der Erstellung von Java-Prozessoren soll sich das ändern. Diese Prozessoren sollen einen erweiterten Bytecode haben, der es erlaubt, auf bestimmte Adressen zuzugreifen. Es ist aber noch nicht definiert, wie man aus Java diese Funktionalität benutzen kann.

Das zweite Problem ist, daß Java nicht für asynchronen I/O (interruptsgesteuert) konzipiert ist. Eine einfache Lösung ist ein Thread (Task) pro Interrupt zu haben. Beim Eintreten eines Interrupts wird der passende Thread vom Laufzeitsystem aktiviert. Dies gehört nicht zur Standard-Java-Umgebung, kann aber einfach implementiert werden, wenn das System eingebettet wird.

Auf der anderen Seite sind asynchrone Interrupts ein Risiko wegen ihrer Unvorhersehbarkeit (Undeterminismus) bei der Ausführung der Tasks. Der Programmierer muß sehr aufpassen und atomare Blöcke definieren, weil der Scheduler sonst jeden Task an beliebigen Stellen unterbrechen kann, um andere Tasks fortzusetzen. Das kann unerwartete Nebenwirkungen haben. Bei sicherheitsrelevanten Systemen wird empfohlen, periodisch die Interruptquellen zu checken, und wenn nötigt, die passenden Server zu aktivieren. Ein asynchroner Interrupt soll nur zugelassen werden, wenn er eine gefährliche Ausnahme signalisiert. In diesem Fall muß eine Ausnahmebehandlung eingeleitet werden und die laufenden Tasks werden nicht unterbrochen, sondern abgebrochen. Nach der Ausnahme-Behandlung wird ein Recovery durchgeführt, und die normal laufenden Tasks werden wieder gestartet.

Scheduler

Der Standard-Java-Scheduler ist nicht für Real-Zeit-Applikationen konzipiert. Deadlines und Zeitfenster werden nicht unterstützt. In der Regel hat man einen nicht preemptiven Scheduler, der nicht in der Lage ist, Tasks ohne ihre aktive Beteiligung zu unterbrechen. Dadurch sind Timeouts nicht möglich.

Wir haben einen experimentellen Real-Zeit-Scheduler geschrieben, bei dem die Tasks ihre Zeitanforderungen anmelden können und er versucht, (ohne Garantie) diese Anforderungen zu erfüllen. Dieser Scheduler wird als Public-domain-Software unter der am Ende genannten URL angeboten.

Die Framework-Technologie

Die Framework-Technologie ist eine Weiterentwicklung der objekt-orientierten Technologie. In der OO-Methodik werden Funktionen als Klassen zur Verfügung gestellt. Der Anwender kann sie, so wie sie sind, benutzen oder er kann die Klassen mittels Vererbung und Überladen von Methoden/Operatoren auf seine Bedürfnisse spezialisieren (anpassen).

Dagegen bietet die Framework-Technologie komplette anpaßbare Strukturen von Klassen an. Ein Framework besteht aus mehreren Klassen mit einer Beziehungsstruktur von Vererbungen und Referenzen. Die Struktur hat eine spezifische komplexe Funktionalität, die der Benutzer an seine Aufgabe anpassen kann.

 

Einige Klassen in der Struktur stellen den Anpassungsanschluß zum Anwender dar. Andere Klassen stellen einen Funktionsanschluß oder unterstützen dessen Funktion, und alle Klassen zusammen erfüllen die Funktionalität des Frameworks. Um die Funktionalität des Frameworks an seine Bedürfnisse anzupassen, schreibt der Anwender neue Klassen, die von den Anpassungsanschluß-Klassen erben (Subklassen). Die Subklassen sollen die Anpassungsanschluß-Methoden überladen, um ihre Funktionalität im Framework zu integrieren. Die neuen Subklassen werden automatisch (durch Vererbung) in die Struktur integriert. Dadurch wird die Funktionalität des Frameworks um die gewünschte Funktionalität erweitert und an die gestellte Aufgabe angepaßt. Die Funktionalität des gesamten Frameworks wird durch Methoden der Funktionsanschluß-Klassen aufgerufen.

Ein Framework-Beispiel mit einem Real-Zeit-Kern

Dieses Beispiel ist ein sehr kleines Framework. Große Frameworks können Tausende von Klassen haben.

 

 

Die Klasse TimeInterval gehört nicht zum Framework, sie ist nur eine Hilfe für die Behandlung von Zeitintervallen. Sie stellt Methoden zum Messen, Markieren und Vergleichen von Zeitpunkten und Zeitintervallen zur Verfügung .

Das Framework bietet einen Interrupt-Mechanismus und einen zeitgesteuerten Scheduler an. Diese Mechanismen aktivieren die passenden TimedEvents (Tasks) zur passenden Zeit oder als Reaktion auf Interrupts. Die im Framework definierten TimedEvents sind nur Platzhalter. Der Anwender soll seine gewünschte Funktion in eine Subklasse (vererben) von TimedEvent schreiben und die entsprechende Methode handle() und/oder interruptHandle() überladen. Ist dies geschehen, aktiviert das Framework automatisch die vom Anwender definierten Methoden zum passenden Zeitpunkt.

Der andere Anpassungsanschluß ist die OnGoing-Klasse. Damit kann ein ongoing zeitgebundener Task implementiert werden. Dieser Task läuft unabhängig von Scheduler und Interrupts, aber er kann sich mit Interrupts und/oder Zeit synchronisieren, indem er auf solche Ereignisse wartet. Dafür stellt die OnGoing-Klasse Methoden zur Verfügung. Der Benutzer soll seine gewünschte Funktionalität in einer Subklasse vom OnGoing schreiben und die Methode run() überladen.

Interruptbehandlung

Die Klasse InterruptTable verwaltet die Interrupts und ihre Server. Die Interrupts werden nicht durch Nummern sondern durch Namen (String) identifiziert. TimedEvents können sich als Server mittels der Methode InterruptTable.register() anmelden und mittels der Methode InterruptTable.cancel() abmelden. Das Feld TimedEvent.interruptIds enthält eine Liste von Interruptnamen, getrennt durch einen Blank " " (z.B. "hot cold low"), die der Server (eine Subklasse von TimedEvent) behandeln soll. Ein Interrupt wird signalisiert, indem der Interrupterzeuger die Methode InterruptTable.raise(id) mit dem Name des Interrupts als Parameter aufruft. Die InterruptTable aktiviert dann alle angemeldeten Server, die diesen Name in ihrer Liste halten, d.h er ruft ihre interruptHandle()-Methode auf.

Beispiel von einem Interrupt-Server:

 

Zeitgesteuertes Scheduling (Prozessorzuteilung)

Die Klasse RTScheduler ist ein zeitgesteuerter Scheduler, bei der sich TimedEvents mit ihren Zeitanforderungen anmelden können. Der Scheduler versucht, diese Zeitanforderungen zu erfüllen und die TimedEvents werden zum bestmöglichen Zeitpunkt aktiviert, d.h. ihre handle()-Methode wird aufgerufen. Das Feld TimedEvent.programedTime beinhaltet den Zeitpunkt (in Millisekunden), wann dieser Task zum ersten Mal bzw. zum nächsten Mal aktiviert werden soll. Das Feld TimedEvent.cycleTime bestimmt die Periode in Millisekunden für zyklische Ausführungen des Tasks. Wenn die cycleTime gleich null ist, dann wird der Task nur einmal aktiviert bzw. wenn er bereits läuft, dann wird er nicht wieder aktiviert.

Für die Registrierung von Tasks bietet der RTScheduler mehrere Methoden an. Beispiele davon sind:

  • at(time,...) : für einmalige Aktivierung, zum gegebenen Zeitpunkt.
  • every(cycle,..): für wiederholte Aktivierung mit der gegebenen Periode.
  • after(time,..): für einmalige Aktivierung nach der gegebenen Zeit.
  • schedule(...): für Aktivierung, wie es in den Feldern programedTime und cycleTime angegeben ist.

 

Ein Task kann durch die RTScheduler.cancel()-Methode abgemeldet werden.

Beispiel von einem zeitgebundenen Task

 

OnGoing

Eine OnGoing-Subklasse hat keine handle()-Methode, sondern sie führt die run()-Methode, so lange es nötig ist, aus. Damit andere Tasks arbeiten können, muß ein Ongoing-Task den Prozessor abgeben (yield()-Methode) oder wait()-Methoden aufrufen. OnGoing stellt einige Methoden zur Zeit- und Interrupt-Synchronisierung zur Verfügung. Z.B:

  • waitInterrupt(timeout): wartet auf einen der in interruptIds genannten Interrupts, aber nicht länger als das angegebene Timeout.
  • waitTime(time): Wartet so lange wie angegeben.
  • waitUntil(time): Wartet bis zum angegebenen Zeitpunkt.

 

Beispiel von einem Ongoing-Task mit Interrupt-Synchronisierung:

 

 

Java Public-domain-Software zum Experimentieren

Zum Experimentieren und für die Entwicklung von Real-Zeit-Steuerungs-, Regelungs- und Simulationsumgebungen haben wir u.a. den oben dargestellten Real-Zeit-Kern Framework implementiert. Den Real-Zeit-Kern stellen wir jetzt als Public-domain-software unter die URL

./public_domain.html

zur Verfügung.

Es handelt sich um einen dynamischen, nicht preemptiven Scheduler. Er unterstützt periodische, sporadische und spontane Tasks (ohne Reaktionszeitfenster). Ongoing-Tasks können als separate Java-Threads unabhängig vom Scheduler laufen, aber es wird eine Unterstützung für ihre Synchronisierung angeboten.

Die Anwendung des Frameworks wird mit einem kleinen graphischen Beispiel erläutert. Die Struktur des Beispieles ist:

 

 

Das Demo erzeugt vier gleiche Tasks (TimedEvents), die mit verschiedenen Perioden laufen und auf verschiedene Interrupts reagieren. Die Interrupts werden simuliert und werden als Reaktion zum Tastendruck der Tastatur generiert. Der periodische Task DemoPainter malt ein wachsendes Quadrat, das bei jedem Interrupt seine Farbe ändert. Der DemoOnGoing-Task mißt die Zeit zwischen Ihren Interrupts, aber ab10 Sekunden Warten wird er nervös. Der DemoKeyListener generiert die Interrupts als Reaktion zum Tastendruck. DemoDrawArea ist das "main" und erzeugt alle anderen Objekte.

Weitere Referenzen

Mein Buch: Sichere und Fehlertolerante Steuerungen

Real Time Seminar an der TU-Berlin
./seminar/index.html
Entwicklung sicherheitsrelevanter eingebetteter Systeme, Real Time, Posix, Real Time java.

The IEEE-CS TC-RTS
http://cs-www.bu.edu/pub/ieee-rts
"The Real-Time Research repository" Software, Research Groups, Conferences, Courses, Tools, etc.

CHORUS/JaZZ
http://www.chorus.com/Products/Datasheets/jazz.html
CHORUS/JaZZ is a Java real-time operating system. It provides a natural platform for Java to interface to the embedded world.

Portos von Oberon microsystems
http://www.oberon.ch/portos/index.html
Portos is a light-weight deadline-driven real-time operating system (RTOS) for hard real-time applications. It is optimized for embedded high-performance applications.

PERCs from NewMonics Inc.
http://www.newmonics.com
PERC is a clean-room implementation of the Java programming language and run-time system. It's design has been tailored to meet the specialized needs of the embedded real-time and consumer electronics industries.