Entwicklung eines Automotive Embedded Java - fbi.h

Werbung
Masterthesis
Entwicklung eines Automotive
Embedded Java Frameworks
Enrico Bedau
Matrikel-Nr.: 700724
27. Januar 2005
Referent:
Prof. Dr. Joachim Wietzke (FH-Darmstadt)
Korreferent: Prof. Dr. Thomas Horsch (FH-Darmstadt)
Inhaltsverzeichnis
1 Einführung
6
1.1
Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
1.2
Zielsetzung
6
1.3
Entwicklung im Automotive Embedded Bereich
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . .
2 Analyse eines C++ Frameworks
7
8
2.1
Aufgaben eines Frameworks
. . . . . . . . . . . . . . . . . . . . . . .
8
2.2
Architektur und Funktionsweise . . . . . . . . . . . . . . . . . . . . .
9
2.3
Verwendete Möglichkeiten der Interprocesscommunication (IPC) . . .
11
2.4
Ablauf der Initialisierungsphase
. . . . . . . . . . . . . . . . . . . . .
12
2.5
Verwaltung von Speicher und Objekten . . . . . . . . . . . . . . . . .
12
2.6
Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
3 Das Framework Design
3.1
3.2
14
Vergleich möglicher Design Patterns . . . . . . . . . . . . . . . . . . .
14
3.1.1
Task Management
. . . . . . . . . . . . . . . . . . . . . . . .
14
3.1.2
Speicherverwaltung . . . . . . . . . . . . . . . . . . . . . . . .
15
. . . . . . . . . . . . . . . . . . . . .
16
3.2.1
Java spezische Besonderheiten
Java-IPC
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
17
3.2.2
Garbage Collection . . . . . . . . . . . . . . . . . . . . . . . .
18
3.2.3
Weitere Einschränkungen gegenüber C++
20
. . . . . . . . . . .
4 Grundlagen für die Entwicklung des Frameworks
4.1
Die Entwicklungsumgebung
4.2
Zugri auf native Bibliotheken über JNI
26
. . . . . . . . . . . . . . . . . . . . . . .
27
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
4.2.1
Warum JNI
4.2.2
Ausführen von C++ Code aus Java-Programmen
. . . . . . .
28
4.2.3
Entwickeln einer Shared-Library . . . . . . . . . . . . . . . . .
28
5 Entwicklung des Frameworks
5.1
26
. . . . . . . . . . . . . . . .
31
HMI Prototyp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
31
5.1.1
Aufteilung des Frameworks . . . . . . . . . . . . . . . . . . . .
31
5.1.2
Schnittstelle zwischen C++ und Java Framework
. . . . . . .
32
5.1.3
Implementierung der HMI-Komponente . . . . . . . . . . . . .
37
2
5.2
Das Java Framework
. . . . . . . . . . . . . . . . . . . . . . . . . . .
37
5.2.1
Ausführen der Komponenten . . . . . . . . . . . . . . . . . . .
38
5.2.2
Verwalten des Speichers
. . . . . . . . . . . . . . . . . . . . .
40
. . . . . . . . . . . . . . . . . . . . . . . . . .
41
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
5.3
Die HMI-Komponente
5.4
JNI Module
5.5
5.4.1
Input Schnittstelle
. . . . . . . . . . . . . . . . . . . . . . . .
41
5.4.2
Mostgateway
5.4.3
. . . . . . . . . . . . . . . . . . . . . . . . . . .
42
CD-Player . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
43
Grasche Oberächen unter Java
. . . . . . . . . . . . . . . . . . . .
43
5.5.1
Auswahl eines Toolkits . . . . . . . . . . . . . . . . . . . . . .
43
5.5.2
SWT-Programmierung . . . . . . . . . . . . . . . . . . . . . .
44
6 Vergleiche der Frameworks
6.1
46
Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
6.1.1
Leistungsfähigkeit der JNI-Schnittstelle . . . . . . . . . . . . .
46
6.1.2
Startverhalten . . . . . . . . . . . . . . . . . . . . . . . . . . .
49
6.1.3
Reaktionszeiten & Antwortverhalten
50
7 Zusammenfassung & Ausblick
. . . . . . . . . . . . . .
51
Abbildungsverzeichnis
2.1
Aufbau des Frameworks
. . . . . . . . . . . . . . . . . . . . . . . . .
2.2
Aufbau einer Komponente
. . . . . . . . . . . . . . . . . . . . . . . .
4
10
11
Listingverzeichnis
3.1
struct-Anweisung aus CMessage.cpp . . . . . . . . . . . . . . . . . . .
21
3.2
Struct-Anweisung in Javaklasse überführt . . . . . . . . . . . . . . . .
21
3.3
Union: AppMessage und MostMessage
. . . . . . . . . . . . . . . . .
22
3.4
Union: Raw Daten und beschriebene Daten . . . . . . . . . . . . . . .
22
3.5
Ausschnitt aus der Klasse CMessage
. . . . . . . . . . . . . . . . . .
22
3.6
Objektübergabe per Kopie der Referenz . . . . . . . . . . . . . . . . .
24
3.7
Objektübergabe per Referenz
. . . . . . . . . . . . . . . . . . . . . .
25
4.1
Java-JNI-Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
5.1
JNI-Klasse CMessagehandler . . . . . . . . . . . . . . . . . . . . . . .
33
5.2
Aufrufe wie diese wurden entfernt . . . . . . . . . . . . . . . . . . . .
34
5.3
Die init-Funktion der JNI-Library . . . . . . . . . . . . . . . . . . . .
34
5.4
Die getMessage-Funktion der JNI-Library
. . . . . . . . . . . . . . .
35
5.5
Beispiel Implementierung des Runnable Interfaces . . . . . . . . . . .
39
5.6
Anlegen und Starten einer Komponente . . . . . . . . . . . . . . . . .
39
5.7
Wiederverwenden von Speicher in C++ . . . . . . . . . . . . . . . . .
40
5.8
Auszug aus der Most-JNI-Library . . . . . . . . . . . . . . . . . . . .
42
5.9
Ausführen von Code im UI-Thread
. . . . . . . . . . . . . . . . . . .
45
6.1
Testfall 1: Aufrufen einer JNI-Funktion . . . . . . . . . . . . . . . . .
47
6.2
Testfall 1: Aufrufen einer Java-Funktion
. . . . . . . . . . . . . . . .
47
6.3
Testfall 1: Rückgabe einer Long-Variablen an Java . . . . . . . . . . .
48
5
1 Einführung
1.1 Motivation
Im Automotive Bereich ist der Druck auf die Software Entwickler sehr hoch. Den immer kürzer werdenden Releasezyklen bei den Automobilen, stehen ständig wachsende Softwareprojekte gegenüber. Um bei dieser Entwicklung schritt halten zu können,
müssen die Entwicklungszyklen kürzer werden.
Für viele erfolgreich abgeschlossenen Projekte im Automotive Bereich wurde hauptsächlich die Programmiersprache C eingesetzt und neue Projekte werden zu einem
groÿen Teil in C++ entwickelt. Diese Sprachen sind dafür bekannt, ein sehr schnelles
Programm zu ermöglichen. Jedoch begünstigen sie auch Programmierfehler, welche
vor allem auf Embedded Systemen nur schwer zu entdecken sind.
Hier soll Java Abhilfe schaen. Eine der groÿen Stärken von Java liegt darin, viele der
von C und C++ bekannten fehleranfälligen Programmiertechniken nicht zu zulassen.
Schon beim Kompilieren werden weitaus mehr Fehler gefunden, als dies bei einem
C/C++-Compiler der Fall wäre.
Im Rahmen dieser Arbeit galt es zu untersuchen, in wie weit eine Java Entwicklung
in der Lage ist, C und C++ Software auf einem Embedded System zu ersetzen.
1.2 Zielsetzung
Das Ziel dieser Arbeit bestand darin, Probleme und Risiken zu identizieren und
einen Vergleich zwischen einer C++- und einer Java-Software zu ermöglichen.
Um einen solchen Vergleich anstellen zu können, wurde auf der Grundlage eines bekannten C++-Frameworks, ein Java Framework entwickelt, welches in der Funktionalität vergleichbar ist. Dabei galt es die Probleme bei einem Umstieg von C++, im
Hinblick auf die beschränkten Ressourcen eines Embedded Systems, zu identizieren
und wenn möglich, Lösungswege aufzuzeigen.
6
Das Java Framework sollte am Ende der Entwicklung in der Lage sein, eine Audio
CD auf einem Embedded System abzuspielen. Ein Vergleich zwischen dem C++- und
dem Java-Framework soll zeigen, ob Java den strengen Anforderungen im Automotive
Bereich genügen kann.
1.3 Entwicklung im Automotive Embedded Bereich
An Software im Automotive Bereich werden sehr hohe Anfordungen gestellt. Da es
sich hier zum Teil um Unterhaltungselektronik handelt, erwartet der Kunde von einem Gerät mindestens genau so viel, wie einer Hi-Stereo-Anlage. Kleinste Verzögerungen in der Bedienung oder bei der Anzeige hinterlassen einen schlechten Eindruck
von dem Produkt. Es wirkt unfertig und unprofessionell.
In der Automobilbrange steht die Sicherheit an erster Stelle. Ein zu spät reagierender Abstandswarner oder eine fehlerhafte Software, welche durch unerwartetes
Verhalten auf sich aufmerksam macht, könnte reale Folgen nach sich ziehen. Daher
ist auch Elektronik, welche das Fahrzeug nicht direkt beeinusst Sicherheitsrelevant.
Die Funktionen eines solchen Gerätes müssen also schnell verfügbar sein und immer
gleich reagieren.
Eine weitere Besonderheit in dieser Umgebung liegt darin, dass ein spät entdeckter
Fehler nur sehr schwer korrigierbar ist. Es ist unter Umständen nötig, tausende Fahrzeuge zurück ins Werk zu rufen. Aber auch in der Testphase ist es schwierig, eine
Fehler zurück zu verfolgen.
7
2 Analyse eines C++ Frameworks
Im Automotive Bereich wurden schon verschiedene Frameworks entwickelt, welche
hauptsächlich in C oder C++ programmiert wurden. Die Erfahrungen aus einem
solchen Projekt sollte in die Entwicklung des Java-Frameworks mit einieÿen. Aus
diesem Grund wurde erst ein bestehendes Framework analysiert, bevor mit der Entwicklung des Java-Frameworks begonnen wurde.
Dabei handelt es sich um ein C++-Framework, da C++ genau wie Java eine objektorientierte Sprache ist und dadurch leichter mit Java verglichen werden kann. Das
Projekt begann im Jahre 2003 und wird in an der FH-Darmstadt durchgeführt. Da
dieses Framework noch in der Entwicklung ist, basiert diese Analyse hauptsächlich
auf dem Stand von November 2004.
2.1 Aufgaben eines Frameworks
Die Entwicklung eines Frameworks verfolgt mehrere Ziele, welche alle darauf abzielen,
Software kostengünstiger erstellen zu können. Dabei ieÿen verschiedene Konzepte
in die Entwicklung ein, welche im Folgenden kurz vorgestellt werden.
ˆ
Wiederverwendbarkeit
Ein Framework basiert auf einem abstrakten Design, das allgemein genug ist,
um in verschiedenen Softwareentwicklungen zum Einsatz zu kommen. Jedoch
muss es auch speziell genug sein, um die Entwicklung wirklich zu vereinfachen,
denn ein rein abstraktes Framework würde keine konkreten Methoden bereitstellen, welche die Entwicklung beschleunigen könnte. Das Abwägen zwischen
allgemeineren oder konkreten Ansätzen ist eine Hauptaufgabe beim Design eines Frameworks.
ˆ
Richtlinien für die Entwickler
Wie der Name Framework schon andeutet, sollte es den Entwicklern einen
Rahmen für ihre Arbeit vorgeben. Das bedeutet, ein Framework stellt Richtlinien und Anweisungen zur Verfügung und forciert ihre Einhaltung wenn immer
8
dies sinnvoll möglich ist. Dadurch wird ein einheitlicher Programmierstil festgelegt, welcher die Software später leichter lesbar macht. Auch wird hierdurch
die Einarbeitung neuer Mitarbeiter erheblich beschleunigt.
ˆ
Modularer Aufbau
Wie es in der Softwareentwicklung schon seit langem Standard ist und auch von
objektorientierten Sprachen gefördert wird, so ist auch ein Framework modular
aufgebaut. Die Vorteile in Bezug auf Design, Implementierung, Testen und
Wartung sind zum Beispiel die hohe Parallelisierbarkeit der Entwicklungsarbeit
und die gröÿere Übersichtlichkeit der Software. Die Vorteile sind im Einzelnen
jedoch sehr vielfältig und können zum Beispiel in [Bal00] nachgelesen werden.
Das Framework deniert, wie die Module aufgebaut sind und wie diese untereinander kommunizieren, in dem es Schnittstellen und abstrakte Klassen
bereitstellt.
ˆ
Bereitstellen von Standardfunktionen
Immer wiederkehrende Funktionen werden von einem Framework in Form von
Bibliotheken bereitgestellt.
2.2 Architektur und Funktionsweise
Das Framework teilt die Module in vier Haupttypen auf. Dies sind die HMI (Human
Machine Interface), der Main Dispatcher, die Adminkomponente und Devices. Die
HMI, die Adminkomponente und der Main Dispatcher kommen im gesamten System
nur jeweils einmal vor, wogegen es von den Devices normalerweise mehrere gibt.
Main Dispatcher
Der
ist das Herzstück des Frameworks und für die Koordination der
Kommunikation zwischen den einzelnen Modulen verantwortlich. Jegliche Kommunikation zwischen den Komponenten sollte über den Maindispatcher erfolgen. Jedoch
wurde diese Designprinzip im Framework teilweise nicht beachtet, da durch die Art
der Implementierung Nachrichten auch direkt beim Empfänger abgelegt werden können.
Die
HMI
zer
und der Maschine. Sie ist somit für das Darstellen der Inhalte, sowie für das
1
ist wie der Name es bereits sagt, die Schnittstelle zwischen dem Benut-
Verarbeiten der Benutzereingaben zuständig und hat auch die Funktion eines Main
Controllers.
Die
1 In
Adminkomponente
ist für das Verwalten der einzelnen Komponenten zuständig.
dieser Arbeit ist von Benutzer die Rede, wenn der Endbenutzer der fertigen Software gemeint
ist
9
Human
Machine
Interface
Gerät 1
Komponente
Gerät 2
Main−
Dispatcher
Gerät X
Administrator Komponente
Abbildung 2.1: Aufbau des Frameworks
Dies ist beim aktuellen Stand des Frameworks nur das Erstellen, Initialisieren, Starten
und Stoppen. Geplant, aber zum Zeitpunkt dieser Arbeit noch nicht implementiert
ist auch die Möglichkeit des Pausierens und Wiedererwecken, sowie das Überwachen
der Komponenten.
Bei den
Devices
kann es sich um alle Arten von Geräten handeln. Dies können zum
Beispiel Controller für reale Geräte wie zum Beispiel DVD-Player oder Radio sein,
aber auch logische Geräte, welche anderen Geräten höhere Funktionen zur Verfügung
stellen.
Beschreibung der Basis-Komponenten
2
Eine jede Komponente, auÿer der Adminkomponente stellt dem User
verschiedene
Eigenschaften, Funktionen und Objekte zur Verfügung.
Für das Eventhandling stehen jeder Komponente zwei Queues zur Verfügung, in
denen eingehende Nachrichten für die Bearbeitung zwischengespeichert werden. Die
erste Queue ist die Systemqueue und für Nachrichten mit hoher Priorität reserviert.
Diese Queue wird immer als erstes abgefragt. Die normalen Nachrichten werden in
der Normalqueue gespeichert.
2 In
dieser Arbeit ist von User die Rede, wenn der Benutzer des Frameworks, bzw. der Entwickler
der eigentlichen Software gemeint ist
10
Nachrichten
Controller
Systemqueue
Dispatcher
Normalqueue
Logische Gerät
(LogDev)
Abbildung 2.2: Aufbau einer Komponente
Im Normalfall ist eine Komponente aus drei Teilen aufgebaut. Dies ist zum einen
das logische Gerät (LogDev), welches die logischen Funktionen eines Gerätes für die
anderen Komponenten bereitstellt. Es kann also eine standardisiere Schnittstelle zu
gleichartigen Geräten, wie zum Beispiel verschiedenen CD-Playern darstellen oder
als Proxy für entfernte Geräte dienen.
Der Controller (ComponentController) ist für die interne Logik, der eigentlichen Programmlogik verantwortlich.
Das Zustellen der Nachrichten zu diesen beiden Komponenten ist die Aufgabe des
Dispatchers, welcher die Nachricht aus den beiden Queues entgegen nimmt und in
Abhängigkeit vom Inhalt der Nachricht zustellt.
2.3 Verwendete Möglichkeiten der
Interprocesscommunication (IPC)
Im C++-Framework wird jede Komponente als eigener Prozess durch Fork-Aufrufe
erzeugt. Dadurch wird der Vater-Prozess kopiert und ein neuer Sohn-Prozess gestartet. Dadurch erbt der Sohnprozess alle Daten des Vaterprozesses, wodurch bereits
initialisierte Variablen und Objekte weiterverwendet werden können. Im Gegensatz
dazu muss der eigentliche Programmcode nicht kopiert werden, wodurch Speicherplatz gespart wird und dennoch der Code als gesamtes zur Verfügung steht (siehe
[QNX04]).
11
Durch das Aufteilen der Komponenten in Prozesse erhält jeder Prozess einen eigenen
zugrisgeschützten Datenbereich. Dies hat den Vorteil, dass Speicherzugrisfehler
sich nicht auf andere Speicherbereiche auswirken können und schnell erkannt werden.
Jedoch verhindert dies auch den Zugri auf gemeinsame Daten.
Für die Kommunikation zwischen den Prozessen verwendet das Framework Shared
Memory (siehe auch Literatursec:SharedMemory), da sich diese Form des Datenaustausches in Tests als die Schnellste herausgestellt hat. Mithilfe von Mutexen aus der
Posix Thread Implementierung werden die gemeinsamen Daten gegen gleichzeitigen
Zugri geschützt.
2.4 Ablauf der Initialisierungsphase
Die Initialisierungs- bzw. Startphase des Frameworks läuft in drei Stufen ab.
In der ersten Stufe werden die Kontexte für alle Komponenten angelegt und initialisiert. Dies beinhaltet in diesem Fall auch das Anlegen der Nachrichtenqueues aller
Komponenten.
In der zweiten Stufe werden nacheinander alle Komponenten angelegt und initialisiert. Dabei wird der Komponente ihr jeweiliger Kontext übergeben, der Controller
und das LogDev angelegt und die Standardinitialisierung über die Konstruktoren
durchgeführt.
In der dritten Stufe werden die Prozesse nacheinander über ihre Init-Funktion initialisiert, geforked und anschlieÿend über ihre Run-Funktion gestartet.
2.5 Verwaltung von Speicher und Objekten
Das Framework implementiert keine Technik zum globalen Verwalten von Speicher.
Der Speicher wird immer dann Allociert, wenn er benötigt wird. Jedoch werden
langsame Funktionen, wie das Kopieren von Objekten nur dann eingesetzt, wenn es
sich nicht vermeiden lässt.
Die Nachrichtenqueues bilden dabei eine Ausnahme. Sie werden beim Starten des
Frameworks angelegt und bleiben während der gesamten Laufzeit erhalten. Da sie
im Shared Memory abgelegt werden, hat jeder Prozess Zugri auf alle Nachrichtenqueues.
12
2.6 Zusammenfassung
Das Framework vermeidet wenn immer es möglich ist, dass Daten kopiert werden, in
dem hauptsächlich Zeiger und Referenzen verwendet werden. Lässt sich das Umkopieren von Daten nicht vermeiden, so werden eektive Funktionen, wie zum Beispiel
memcopy
verwendet.
Bei der Analyse des Frameworks ist auch aufgefallen, das die Informationen zu einer
Komponente relativ Verstreut sind und dadurch etwas an Übersichtlichkeit verloren
geht. Möchte man zum Beispiel eine Komponente zum Framework hinzufügen, so
muss man dazu an ca. 10 Stellen in 4 verschiedenen Dateien Änderungen vornehmen.
Auch sind einige der Daten redundant.
13
3 Das Framework Design
Wie der Name Framework schon andeutet, stellt es einen Rahmen zur Verfügung
in dem ein System abläuft. Die Aufgabe eines Framework besteht darin, denierte
und stabile Schnittstellen, zu oft benötigten Funktionen, zur Verfügung zu stellen. Im
folgenden wird dargestellt, welche Richtlinien bei der Erstellung des Java Frameworks
eine Rolle gespielt haben.
3.1 Vergleich möglicher Design Patterns
Die wichtigsten Standardaufgaben eines Frameworks bestehen darin den Speicher zu
verwalten, die Kommunikation zwischen den einzelnen Teilsystemen sicher zu stellen
und den Lebenszyklus der Teilsysteme zu kontrollieren. In diesem Kapitel werden
verschieden Design Patterns vorgestellt, welche für diese Aufgaben in Frage kommen.
3.1.1 Task Management
Um das Management von Tasks zu beschreiben, sollte zu erst der Begri Task'nnäher erläutert werden. Unter Task verstehen wir im Hinblick auf ein Framework eine
Aufgabe oder ein Teilsystem, welches parallel zu anderen Tasks ausgeführt wird. Bei
einem Task kann es sich um einen Thread oder einen Prozess handeln, wobei sich
ein Framework typischerweise auf eines von beiden beschränkt. Um Verwirrungen
zu vermeiden, wird in dieser Arbeit von Tasks gesprochen, wenn es sich um einen
Prozess oder ein Thread handeln kann.
Die Aufgabe des Frameworks ist es unter anderem, diese Tasks zu erzeugen und zu
starten. Dies kann beinhalten, dass weiter für jeden Prozess notwendige Aufgaben,
wie z.B. das Anlegen eines Kontextes, durchgeführt werden.
Weitere Aufgaben die ein Framework durchführen kann, sind das Überwachen der
Tasks und der kontrollierte Neustart im Falle eines Fehlers. Komplexere Frameworks
erlauben auch, Tasks zu pausieren und zu einem späteren Zeitpunkt fortzusetzen.
14
3.1.2 Speicherverwaltung
Eine wichtige und aufwendige Aufgabe eines Framework besteht darin den notwendigen Speicher für Objekte und Daten zur Verfügung zu stellen. Es gibt für diesen
Zweck verschieden Ansätze, welche sich zum Beispiel hinsichtlich Geschwindigkeit
und Speicherplatzbedarf unterscheiden. Im Folgenden werden einige dieser Verfahren
vorgestellt, welche für den Einsatz auf einem embedded System in Frage kommen
könnten.
Dynamisches Allocieren und Deallocieren
Beim dynamischen Allocieren wird der Speicher immer dann angelegt, wenn er gebraucht wird. Dies hat zur Folge, dass nicht benötigte Speicherbereiche wieder freigegeben werden müssen (deallocieren), wenn sie nicht weiter gebraucht werden.
Die Vorteile dieses Verfahrens liegen zum Einen im geringen Speicherbedarf. Es wird
immer nur soviel Speicher reserviert, wie auch benötigt wird. Zum anderen kann ein
Programm das Reservieren des Speichers über die gesamte Laufzeit verteilen. Der
Programmstart verzögert sich nicht, durch zusätzliches initialisieren von Speicher.
Ein weiterer Vorteil ist der geringe Overhead, denn es sind keine globalen Strukturen
zur Verwaltung des Speichers notwendig.
Die dynamische Speicherverwaltung hat aber auch einige gravierende Nachteile. Dadurch, dass zu jeder Zeit Speicher allokiert werden kann, lässt sich oft nur schwer
nachvollziehen, wann wieviel Speicher reserviert wurde. Dies kann die Fehlersuche erheblich erschweren. Viele Fehler beim Programmieren lassen sich auf vergessenes oder
zum falschen Zeitpunkt durchgeführtes reservieren oder freigeben von Speicher zurückführen. Nicht durchgeführtes reservieren von Speicher fällt normalerweise schnell
durch NullPointer-Exceptions auf. Jedoch fallen nicht wieder freigegeben Bereiche oft
nicht auf und führen zu Memory Leaks. Dieser Fehler kann sich erst nach längerer
Laufzeit bemerkbar machen und ist somit schwer zu nden.
Jeder Vorgang, bei dem Speicher reserviert oder freigegeben wird, zwingt das Betriebssystem dazu, in den geschützten Kernel-Mode zu gehen. Da das Umschalten
in den Kernel mode relativ zeitaufwendig ist, können viele dieser Vorgänge ein Programm stark ausbremsen. Dies trit besonders auf langsame Systeme zu.
Einige der oben vorgestellten Nachteile der dynamischen Speicherverwaltung erschweren gröÿere Softwareprojekte erheblich. Aus diesem Grund wurden verschieden Verfahren entwickelt um diese zu umgehen. Dies ist zum Beispiel Objectpooling, welches
nachfolgend näher erläutert wird.
15
Object Pooling
Mit Hilfe des Object Pooling versucht man möglichst selten Speicher zu reservieren
bzw. frei zugeben, um die oben genannten Nachteile zu minimieren. Die Idee dabei
ist, nicht mehr benötigten Speicher nicht freizugeben, sondern wiederzuverwenden.
Beim Object Pooling werden nicht benötigte Objekte in einem Pool vorgehalten.
Wird ein neues Objekt benötigt, so kann es aus dem Pool entnommen werden und es
muss nicht erst ein Speicherbereich allokiert werden. Sobald ein Objekt nicht mehr
benötigt wird, kann es wieder in den Pool für freie Objekte übergeben werden.
Ein Nachteil des Object Pooling ist, dass nicht jede Objektgröÿe vorgehalten kann.
Man muss sich auf eine oder wenige Gröÿen beschränken. Dies kann bei Programmen, welche viele stark unterschiedliche Objektgröÿen benötigen, zu einem hohem
Overhead führen. Viele Objekte werden nicht den gesamten ihnen zur Verfügung gestellten Speicher benötigen. Andererseits kann kein Objekt gröÿer sein als das gröÿte
Speicherobjekt.
Es gibt verschiedene Ansätze, welche man für das Anlegen der Objekte verfolgen
kann. Dies kann zum Beispiel direkt für alle Objekte beim Programmstart erfolgen. Dadurch würde der Start länger dauern, jedoch das Programm danach schneller
ablaufen. Es ist aber auch möglich nur bei einem leeren Objektpool neue Objekte
anzulegen, wobei man meist mehrere Objekte anlegt, um diesen Vorgang zu optimieren. Dies würde die Zeit für das Speicherallokieren auf einen gröÿeren Zeitraum
verteilen und den Programmstart beschleunigen.
Das Anlegen von neuen Objekten lässt sich aber auch im ersten Fall oft nicht vermeiden, da nicht immer bekannt ist, wie hoch der maximale Speicherbedarf ist.
Da das Object Pooling an einer zentralen Stelle, über eine fest denierte Schnittstelle
durchgeführt werden muss, ist das Debugging oft einfacher, als bei der dynamischen
Speicherverwaltung.
3.2 Java spezische Besonderheiten
Da Java im Embedded Bereich immernoch eine besondere Stellung einnimmt, wird
in diesem Abschnitt auf die Besonderheiten von Java eingegangen. Das wohl bedeutenste Merkmal von Java ist, dass das kompilierte Programm nicht direkt auf dem
Prozessor ausgeführt werden kann, sondern unter einer virtuellen Maschine, der Java
Virtual Machine (JVM) läuft. Dies führt zu einigen grundsätzlichen Unterschieden
im Vergleich zu direkt ausführbaren Programmen.
16
3.2.1 Java-IPC
Da Java eine plattformunabhängige Programmiersprache ist, kann man keine betriebssystem spezischen Mechanismen für die Kommunikation mit anderen Prozessen nutzen. Im folgenden werden die von Java unterstützten Methoden zur Inter
Prozess Communication vorgestellt.
Sockets
Die eigentliche, von Sun für Java vorgesehene Methode zur IPC ist die Kommunikation über Network Sockets. Diese Methode funktioniert wie bei anderen Programmiersprachen auch und soll deshalb in dieser Arbeit nicht im Detail erläutert werden.
Es gibt verschieden APIs, welche das Entwickeln von verteilten Anwendungen unter
Java vereinfachen und auf Sockets aufsetzen. Dies sind zum Beispiel Remote Method
Invocation (RMI) oder Soap und werden von Sun für IPC vorgeschlagen.
Die Kommunikation über Network Sockets hat jedoch einen entscheidenden Nachteil.
Sie ist verhältnismäÿig langsam.
Pipes
Eine weitere Methode, welche weniger Overhead produziert als Sockets, ist die Kommunikation über Pipes.
Unter Java gibt es verschieden Möglichkeiten mit Hilfe von Pipes Daten zwischen
Prozessen auszutauschen.
1. Wird das Java Programm innerhalb einer Pipe gestartet, so kann es mit Hilfe
der System.in Funktion die Ausgaben des in der Pipe vorausgehenden Programmes einlesen, bzw. mit System.out dem nachfolgenden Programmdaten
übergeben. Jedoch funktioniert diese Pipe nur in eine Richtung und ist somit
als Interprozesskommunikation ungeeignet.
2. Wird ein Prozess durch die Funktion Runtime.getRuntime().exec() gestartet,
so kann die Ausgabe dieses Prozesses mit Hilfe der Funktion getInputStream()
gelesen werden.
3. Auf Posix kompatiblen Betriebssystemen ist es möglich Named Pipes zu nutzen. Dies Funktioniert, da solche Pipes gegenüber der Anwendung als normale
17
Dateien erscheinen und somit die Standard-Dateiklassen FileInputStream und
FileOutputStream genutzt werden können. Das Anlegen einer Pipe ist unter
Java leider nicht möglich, wodurch das Ausführen von externen Programme
oder nativen Code über Java Native Interface (JNI) notwendig ist.
Shared Memory
Der direkte Zugri auf gemeinsamen Speicher ist unter Java aus verschiedenen Gründen unmöglich. Zum einen ist es unter Java weder möglich Shared Memory anzulegen,
noch auf bereits angelegten Speicher zu zugreifen. Genau genommen gibt es in Java
keine Funktion um auf Speicher zuzugreifen. Weder auf den eigenen, den der JVM
oder anderer Bereiche. Da dies dem Grundgedanken einer Virtuellen Maschine auch
zzuwiderlaufen würde, wird es solche Funktionen auch in Zukunft nicht geben.
Verschieden Versuche, direkt Speicher aus dem Prozess der JVM auszulesen sind
fehlgeschlagen. Es war zwar möglich, innerhalb eines Debuggers die Pattern eines in
einer Javavariable gespeicherten Wertes wiederzunden, jedoch nur bei primitiven
Typen wie einem Integer und an nicht vorhersagbarer AAdresse Auch ist es nicht
ohne weiteres möglich den Speicher der JVM zu lesen, da es sich nicht um Shared
Memory handelt und dieser Speicher somit geschützt ist.
Man kann Shared Memory jedoch indirekt nutzen, in dem man das Java Native
Interface (JNI) nutzt (siehe Literatursec:JNI).
3.2.2 Garbage Collection
Ein Eigenschaft von Java, welche das Programmieren erleichtern soll, ist das Fehlen
einer delete-Funktion. Es ist nicht möglich ein einmal angelegtes Objekt explizit zu
löschen. Für das Löschen eines Objekts ist der Garbage Collector (GC) zuständig. Er
wird jedoch nur ausgeführt, wenn ein neues Objekt angelegt werden soll, aber kein
Platz auf dem Heap kein freier Platz mehr vorhanden ist.
Der Garbage Collector (GC) gibt den Speicher eines Objekts wieder frei, wenn keine
Referenz auf das Objekt mehr vorhanden ist. Um dies tun zu können, muss er alle
Objekte auf noch vorhandene Referenzen untersuchen. Dies kann die Leistungsfähigkeit eines Systems beeinträchtigen und wird deshalb auch gern als Argument gegen
Java aufgeführt.
Um diesen Engpass zu beheben wurde in den letzten Jahren viel Entwicklungsarbeit
in die GCs gesteckt. Moderne JVMs besitzen verschiedene Verfahren um zu verhin-
18
dern, das der GC die Performance eines Programmes negativ bebeeinusst
Die Phasen der Garbage Collection
Jede JVM implementiert den Garbage Collector etwas unterschiedlich. Das Grundkonzept ist aber bei nahezu allen gleich. Ist für ein neues Objekt nicht genügend
Speicher auf dem Heap frei, übernimmt der Thread, welcher den Speicher angefordert hat die Kontrolle und führt den GC aus. Dazu werden alle Threads gestoppt
und wird deshalb auch Stop-The-World Collection (STW) genannt. Die GC wird in
drei Phasen ausgeführt, welche Mark, Sweep und Compaction heiÿen.
Die Erste Phase ist die
Mark Phase, in der alle erreichbaren Objekte markiert. Dazu
wird auf dem Stack nach Objektreferenzen gesucht. Die gefundenen Objektreferenzen werden rekursiv nach weiteren Objekten durchsucht. Jeder so auf dem Heap
gefundene Speicherbereich wird in einem sogenannten Markbitvektor als referenziert
markiert. Objekte, welche nach dieser Phase nicht markiert sind, werden auch nicht
mehr referenziert und können gelöscht werden.
In der
Sweep Phase
werden wird nach Speicherbereichen gesucht, welche allociert
sind, aber nicht markiert wurden. Diese Speicherbereiche werden dann als frei markiert.
Nach der Sweep Phase ist der Speicher fragmentiert und wird in der
Phase
Compaction
wieder defragmentiert. Diese Phase kostet sehr viel Zeit, da ein Objekt ver-
schoben und danach alle Referenzen auf das neue Objekt geändert werden müssen.
Aus diesem Grund wird die Compaction Phase nur ausgeführt, wenn nach der Sweep
Phase immer noch nicht ausreichend Platz auf dem Heap ist.
Ist nach der Compaction Phase der Platz auf dem Heap immer noch nicht ausreichend, so wird der Heap vergröÿert.
Optimieren der Garbage Collection
Ein Garbage Collection Lauf kann unter Umständen sehr viel Zeit in Anspruch nehmen. Man versucht also einen solchen GC-Lauf möglichst zu verhindern oder zu
beschleunigen.
Der erste Gedanke ist oft, die Garbage Collection auszuschalten und nur bei Bedarf
selbst auszuführen. Es ist aber bei keiner der bekannten JVMs möglich, die GC auszuschalten. Auch die GC vorzeitig laufen zu lassen, wenn keine kritischen Operationen
19
anstehen, ist nicht empfehlenswert. Das Ausführen der Funktion System.gc() ist für
solche Zwecke gedacht. Jedoch erzwingt diese Funktion einen GC-Lauf nicht, sondern
empelt ihn nur. Auch wird beim Ausführen der GC nach diesem Funktionsaufruf
immer die Compaction Phase ausgeführt, die sehr zeitaufwendig ist.
Die Mark- und Sweep-Phasen sind sehr eektiv und stören den Ablauf eines Programmes normalerweise nicht. Die Compaction Phase sollte jedoch auf jeden Fall
vermieden werden. Diese Phase kann auftreten, wenn sehr groÿe Objekte oder Arrays erstellt werden. Es kann also von Vorteil sein diese aufzusplitten.
Besitzt ein Objekt, welches bei einem GC-Lauf gelöscht werden soll eine FinalizeMethode, so wird diese ausgeführt. Dies verzögert die GC und sollte deshalb vermieden werden (siehe [Bor04]).
Neuere JVMs wie IBMs VM ab Version 1.3.1 oder Suns VM ab Version 1.4 besitzen
zwei neue Garbage Collector Methoden, welche die Verzögerung bei der GC verringern können. Der
Parallel Collector
führt pro vorhandenen Prozessor einen GC
Thread aus. Dadurch kann die GC auf Mehrprozessorsystemen erheblich beschleunigt werden. Da ein Embedded System im Normalfall nur einen Prozessor hat, ist
dieser Collector dort nicht von nutzen.
Der
Concurrent Collector von Suns VM kann einen Groÿteil der Mark und Sweep
Phasen parallel zu den anderen Threads ausführen und muss nur am Beginn und
am Ende der Mark-Phase eine Stop-The-World Phase ausführen. Dadurch werden
die Phasen, in denen das System steht, sehr klein und behindern das System nicht.
IBMs VM kennt einen
Concurrent Mark,
welcher während das System läuft, nach
unreferenzierten Objekten sucht und dadurch eine Mark Phase bei einem GC überüssig macht.
Werden beim Starten eines Java Programmes viele Objekte angelegt, kann die Startzeit verringert werden, in dem die minimale Heapgröÿe erhöht wird. Die minimale
Heapgröÿe der für diese Frameworkentwicklung benutzten JVM ist zum Beispiel 1MB
und kann mit dem Parameter -Xms4mäuf 4 MB vergröÿert werden.
3.2.3 Weitere Einschränkungen gegenüber C++
Union & Stucts
Eine in C und C++ sehr beliebte Möglichkeit, unterschiedliche Sichten auf ein und
dieselben Daten zu erhalten, sind Union- und Struct-Anweisungen. Diese Anweisungen gibt es in Java jedoch nicht. Am Beispiel von CMessage aus dem C++-Framework
werden in diesem Kapitel einige mögliche Wege beschrieben.
20
Um eine Struct-Anweisung zu ersetzen, gibt zwei Möglichkeiten. Möchte man sehr
nahe an der Funktionalität einer Struct-Anweisung bleiben, müÿte man für jeden
Struct eine Klasse erstellen. Eine Klasse ohne Memberfunktionen verhält sich genau
wie ein Struct (siehe 3.2). Dies hat jedoch den Nachteil, das sehr viele neue Klassen
entstehen würden. Da sich in Java auch Klassen in andere Klassen einbetten lassen,
würde es an der Lesbarkeit des Source-Codes nichts ändern.
struct AppMessage
{
unsigned char senderType ;
Int32 senderID ;
Int32 receiverID ;
Int32 opcode ;
Int32 parameter1 ;
Int32 parameter2 ;
Int32 parameter3 ;
Int8 parameter4 [39];
};
Listing 3.1: struct-Anweisung aus CMessage.cpp
class AppMessage {
char senderType ;
int senderID ;
int receiverID ;
int opcode ;
int parameter1 ;
int parameter2 ;
int parameter3 ;
byte parameter4 [] = new byte [39];
};
Listing 3.2: Struct-Anweisung in Javaklasse überführt
Eine andere Möglichkeit besteht in diesem Fall darin, die einzelnen Variablen des
Structs direkt als MemberVariable in die Klasse CMessage aufzunehmen. Durch den
Zugri über Memberfunktionen kann dann die Zuordnung zu den einzelnen denierten Typen hergestellt werden (siehe 3.5).
Auf dem zweiten Wege lassen sich auch relativ einfach Union-Anweisungen ersetzen. In der originalen Nachrichtenklasse CMessage gab es insgesamt zwei UnionAnweisungen. Die erste beschreibt eine Nachricht als Mostnachricht (MostMessage)
oder interne Nachricht (AppMessage) (siehe 3.3). Die zweite Union Anweisung beschreibt eine Mostnachricht mit benannten Variablen oder als Array (siehe 3.4). Im
Java-Framework ndet man die Variablen der AppMessage als Membervariablen der
21
Klasse CMessage wieder. Möchte man eine Nachricht wie im C++-Framework als
Mostnachricht behandeln, so ist dies über die Memberfunktionen möglich (siehe 3.5).
union msg {
AppMessage appMsg ;
MostMessage mostMsg ;
} theMessage ;
Listing 3.3: Union: AppMessage und MostMessage
struct MostMessage
{
unsigned char senderType ;
union {
unsigned char bytes [19];
struct {
UInt8 FBlockID ;
UInt8 Instance ;
UInt16 FkID ;
UInt8 OPType ;
UInt16 Length ;
UInt8 data [12];
} rx_msg ;
} data ;
};
Listing 3.4: Union: Raw Daten und beschriebene Daten
class CMessage {
private char senderType ;
private int senderID ;
private int receiverID ;
private int opcode ;
private int parameter1 ;
private int parameter2 ;
private int parameter3 ;
private byte parameter4 [] = new byte [39];
CMessage () {
init ();
}
void init () {
senderType = 0;
22
senderID = 0;
receiverID =
opcode = 0;
parameter1 =
parameter2 =
parameter3 =
}
0;
0;
0;
0;
for ( byte counter = 0; counter < 39; counter ++)
{
parameter4 [ counter ] = 0;
}
void setSenderID ( int sender ) {
this . senderID = sender ;
}
void setReceiverID ( int receiver ) {
this . receiverID = receiver ;
}
...
void setMost_FBlockID ( byte FBlockID ) {
this . senderID = ( FBlockID << 24) +
( this . senderID & 0 x00FFFFFF );
}
void setMost_Instance ( byte Instance ) {
this . senderID = ( Instance << 16) +
( this . senderID & 0 xFF00FFFF );
}
...
}
Listing 3.5: Ausschnitt aus der Klasse CMessage
Kopieren von Speicherbereichen
Unter C und C++ ist es sehr beliebt, groÿe Speicherbereiche wie Arrays oder Objekte
mit Hilfe der Funktion
memcopy
zu kopieren oder mit
23
memset
zu initialisieren. Diese
Funktionen sind auf nahezu allen Plattformen auf Geschwindigkeit optimiert und
deshalb sehr schnell.
In Java gibt es solche Funktionen nicht. Das liegt haupsächlich an den der Philosophie
von Java, keine direkten Speicherzugrie zu zulassen und an der strengen Typprüfung. Die Felder eines Arrays, bzw. die Membervariablen eines Objektes, müssen
immer nacheinander kopiert werden. Aus diesem Grund ist das Kopieren von groÿen
Objekten sehr viel langsamer als in C/C++.
Eine Möglichkeit für Objekte, eine ähnliche Funktionalität zu erreichen, ist das Implementieren des Interfaces
Cloneable (siehe [Mic04a, Online im Internet unter] [Stand:
28.12.2004]).
Um Arrays zu kopieren steht die Funktion System.arraycopy zur Verfügung, welche
alle Arten von Arrays kopieren kann.
Für beide Methoden gilt jedoch, dass sie nicht so eektiv kopieren, wie es unter
C/C++ möglich ist.
Übergeben von Objektreferenzen
Wird in Java eine Funktion aufgerufen, welcher als Parameter ein Objekt übergeben
wird, so wird eine neue Referenz erzeugt, welche auf das übergebene Objekt zeigt.
Das direkte Übergeben einer Referenz, wie es in C++ möglich ist, funktioniert unter
Java nicht.
Das folgende Beispiel würde beim Aufruf der Funktion teste() Ist nicht null zurückgeben.
void test () {
}
CMessage msg = new CMessage ();
getMessage ( msg );
if ( msg == null ) {
System . out . println (" Ist null " );
} else {
System . out . println (" Ist nicht null " );
}
void getMessage ( CMessage mMessage ) {
mMessage = null ;
24
}
Listing 3.6: Objektübergabe per Kopie der Referenz
Möchte man eine Referenz übergeben, so geht dies nur über den Umweg über ein
Objekt oder ein Array, welches die Referenz enthält. Das folgende Beispiel gibt für
die Objektrefernz nun null zurück.
void test () {
CMessage msgRefs [] = new CMessage [1];
msgRefs [0] = new CMessage ();
getMessage ( msgRefs );
}
if ( msgRefs [0] == null ) {
System . out . println (" Ist null " );
} else {
System . out . println (" Ist nicht null " );
}
void getMessage ( CMessage mMessage []) {
mMessage [0]= null ;
}
Listing 3.7: Objektübergabe per Referenz
25
4 Grundlagen für die Entwicklung
des Frameworks
Dieses Kapitel gibt eine kleine Einführung in die Softwareentwicklung unter QNX.
Eine Anforderung des zu entwickelnden Frameworks war es, eine möglichst Plattformunabhängige Software zu erstellen. Aus diesem Grund wurden alle Teile des JavaFrameworks unter den Betriebsystemen QNX 6.3 (X86 und SH4), Linux (Debian
Sid) und Microsoft Windows XP getestet. Dazu waren einige Anpassungen notwendig, welche im folgenden näher erläutert werden.
4.1 Die Entwicklungsumgebung
Mit der QNX Momentics Development Suite liefert QNX die QNX Momentics Integrated Development Environment. Diese IDE basiert auf der bekannten Open
Source IDE Eclipse, welche ursprünglich von IBM entwickelt wurde. Sie ist für das
QNX eigene Betriebssystem Neutrino, sowie Linux und Windows verfügbar. Durch
verschieden Änderungen an der IDE hat QNX Eclipse an die eigenen Bedürfnisse
angepaÿt. Jedoch ist Momentics dadurch teilweise inkompatibel zum original Eclipse. Plug-Ins funktionieren in vielen Fällen nicht und Updates eingebauter Plug-Ins
können zu verschiedenen unerwünschten Eekten führen. Die eingebaute UpdateFunktion sollte nicht genutzt werden, da sie die Momentics IDE beschädigen kann
und damit zu einer Neuinstallation zwingt.
Das Build-System von Momentics basiert auf der bekannten GNU Compiler Collection und den GNU Build Tools, welche zum Teil an QNX angepaÿt wurden. Dadurch
ist es möglich, ein unter QNX entwickeltes Programm nahezu unverändert auch auf
anderen Systemen zu kompilieren, auf denen die GNU Compiler Collection vorhanden
ist. Dies funktioniert jedoch nicht, wenn hardwarenahe Funktionen genutzt werden
(siehe auch 5.4.3
CD-Player ).
Eine weitere Einschränkung besteht bei der Nutzung
von Photon, dem QNX eigenen Windows System. Photon ist nur für QNX Neutrino
verfügbar.
Die Momentics IDE erstellt beim Anlegen neuer QNX Projekte ein Makele, welches
26
nur unter Momentics funktioniert, da es verschieden Dateien benötigt, welche nur in
der Momentics Development Suite verfügbar sind. Aus diesem Grund wurden alle
Makeles selbst erstellt. Dadurch war es auch möglich Eclipse für die Entwicklung
zu nutzen, welches schneller reagiert und in neueren Versionen verfügbar ist.
Für die Java Entwicklung wird auf Neutrino Systemen der j9 Compiler und die j9
VM von IBM genutzt. Diese lagen auf der Entwicklungsumgebung in Version 2.1 und
auf dem Target in Version 2.0 vor. Dies entspricht Java Version 1.3.0. Auf anderen
Systemen kam die Sun Java SDK 1.4.1 zum Einsatz.
Auf dem Target war es notwendig die j9 VM anzupassen. Da das Framework SWT
(siehe Literatursec:SWT) nutzt, benötigt die j9 VM eine Photon SWT Bibliothek,
welche für die laufende Neutrino-Version (in diesem Fall 6.3) kompiliert wurde. QNX
liefert diese nur für die Entwicklungsumgebung mit. Versuche, ältere Version der
Bibliothek unter Verwendung der dazugehörigen Bibliotheken aus Neutrino 6.2 zu
nutzen schlugen fehl. Auch führten neuere SWT-Versionen immer zum Absturz der
j9 VM. Die einzige Möglichkeit bestand darin, diese selbst zu kompilieren. Da die
von der JVM benötigte Version oziell eine Entwicklerversion ist, ndet man diese
nicht als Packet. Sie kann aber über das Concurrent Versions System vom EclipseEntwicklungsserver bezogen werden, welcher alle Versionen kennt.
4.2 Zugri auf native Bibliotheken über JNI
Um C oder C++ Code aus einer Java-Anwendung heraus aufzurufen, kann die JavaAnwendung eine spezielle Shared-Library laden und darin enthaltene Funktionen
ausführen, als wären es statische Java Funktionen. Im folgenden wird kurz erklärt,
wie dabei vorgegangen werden kann.
4.2.1 Warum JNI
ˆ
Performanzkritische Bereiche optimieren.
ˆ
Auf Targetspezische Libaries zugreifen (meist nicht in Java verfügbar). Einige
plattformabhängige Merkmale stehen unter Java nicht zur Verfügung. Darunter
vielen bei der Entwicklung des Java-Frameworks zum Beispiel der Zugri auf
die serielle Schnittstelle oder auf CDs nach dem Red Book Standard (Audio
CDs).
27
4.2.2 Ausführen von C++ Code aus Java-Programmen
Eine Klasse, welche Funktionen aus einer Shared Library importieren möchte, muss
diese zu importierenden Funktionen prototypisch mit dem Schlüsselwort
native
be-
kannt machen.
public class CMessageHandler
public static native int
public static native int
public static native int
}
{
initSharedMem ();
getInt ();
sendInt ( int intTest );
Listing 4.1: Java-JNI-Klasse
Das Laden der Shared-Libary sollte in einer statischen Funktion erfolgen, da eine
Library nur einmal geladen wird.
static {
System . loadLibrary (" MessageHandler " );
}
Es ist zu beachten, dass der Name der Library nicht mit dem Dateinamen der Library übereinstimmt. Der Dateiname ist abhängig vom Betriebssystem und ist auf
Posix-Systemen lib{Library-Name}.so und auf Win32-Systemen {Library-Name}.dll.
In diesem Zusammenhang kann die folgende Funktion nützlich sein:
System.mapLibraryName("MessageHandler")
Sie gibt den erwarteten Dateinamen der Library zurück.
4.2.3 Entwickeln einer Shared-Library
Im Folgenden wird erklärt, wie die Shared-Library erstellt wird.
Compilieren des Java Codes
Für das Erstellen der Shared-Library wird eine Headerdatei benötigt, welche aus dem
Javacode generiert werden kann.
Wird unter Momentics entwickelt, so wird der Code automatisch nach jeder Änderung
kompiliert. Der Befehl
28
javah classname
erzeugt aus der Javaklasse eine C/C++-Headerdatei, welche Prototypen für alle JNIFunktionen der Klasse enthält.
Dies funktionierte in der vorliegenden Win32 Version der Momentics-Umge-bung
(6.2.1A) nicht, da mehrere Dateien (javah und verschiedene Header) fehlten. Unter
QNX sind diese Dateien aber wie erwartet vorhanden.
Erstellen des C++ Codes
Am einfachsten ist es, Momentics das Compilieren und Linken des Codes zu überlassen. Dazu erstellt man ein neues Projekt mit Hilfe des Templates QNX C++
Library Project. Darin kann man nun die Headerdatei importieren und den Code
implementieren.
Das Compilieren von Hand funktioniert genau wie bei Programmen, mit dem zusätzlichen Kommandozeilenparameter -shared.
Ein Beispiel mit dem GNU-Compiler:
g++ -shared -o libMessageHandler.so CMessageHandler.cpp
Das folgende Beispiel zeigt, wie eine Bibliothek für QNX Neutrino auf einem SH4System kompiliert wird:
qcc -Vgcc\_ntoshle -lang-c++ -shared
-o libMessageHandler.so CMessageHandler.cpp
Sollte beim Laden der Library eine Fehlermeldung erscheinen, welche auf Unresolved
Symbols verweist, kann durch Entfernen des Kommandozeilenparameter -shared
überprüft werden, welche Bibliotheken fehlen. Werden alle Bibliotheken gefunden,
so sollte nur eine Fehlermeldung erscheinen, welche auf die fehlende MainProzedur
verweist.
Deployen der Library
Die Library wird wie gewohnt unter dem Projektpfad im jeweiligen Targetverzeichnis erstellt. Für unsere Zwecke (JNI) ist nur die Sharedvariante mit der Endung
29
.so wichtig. Die Library muss auf dem jeweiligen Target in ein Verzeichnis kopiert
werden, welches nach Libraries durchsucht wird. Dies ist auf Posixsystemen jeder
Pfad, welcher in der Umgebungsvariablen LD_LIBRARY_PATH vorkommt sowie
auf Win32-Systemen die Ordner %SYSTEM% \system32 und der Startordner des
ladenen Programms.
Sollte die Bibliothek nicht gefunden werden, so kann der Pfad zur Bibliothek beim
Starten von Java wie folgt angegeben werden.
java -Djava.library.path=<Pfad zur Bibliothek> <Zu startende Klasse>
Debugging
Zum Debugging muss die Shared-Library mit Debugging-Symbolen compiliert werden. Zu erkennen ist sie an der Endung
_g. Das Debuggen von optimiertem Code ist
nahezu unmöglich. Aus diesem Grund sollte der Optimization Level auf no optimize eingestellt werden. Dies funktionierte bei der vorliegenden Momenticsversion
jedoch nicht, wodurch ein manuelles Ändern des Makeles (common.mk) notwendig
ist. Die Variable CCFLAGS sollte wie folgend gesetzt sein:
CCFLAGS+=-Y _ecpp_ne -O0
Die Debubugversion der Library wird automatisch geladen, wenn die JVM im Debugmodus (-Xdebug) gestartet wird.
30
5 Entwicklung des Frameworks
Bei der Entwicklung des Java-Frameworks wurde die Architektur des C++-Frameworks soweit wie möglich übernommen, um das Verhalten der beiden Frameworks
später vergleichen zu können. Dies spiegelt sich auch in den Bezeichnern von Klassen, Objekten und Variablen wieder, soweit diese in beiden Frameworks die gleiche
Funktion haben.
Unterschiede zwischen den Frameworks und die Besonderheiten, welche unter Java
zu beachteten sind, werden in diesem Kapitel näher erläutert.
5.1 HMI Prototyp
Um erste Erfahrungen zu sammeln, wurde ein Prototyp erstellt, welcher nur einen
kleinen Teil des Frameworks in Java abbildet. Um dennoch ein funktionsfähiges
Framework untersuchen zu können und erste Vergleiche mit dem originalen C++Framework zu ermöglichen, nutzt der Prototyp das C++-Framework für nicht implementierte Funktionen.
5.1.1 Aufteilung des Frameworks
Für die erste Implementierung in Java wurde die HMI ausgewählt, da hierbei viele der
für die Entwicklung des Frameworks notwendigen Verfahren zum Einsatz kommen.
Der gröÿte Teil des Prototypes besteht aus dem originalen C++-Framework, aus
dem der HMI-Teil entfernt wurde und weitere notwendige Änderungen vorgenommen
wurden.
31
5.1.2 Schnittstelle zwischen C++ und Java Framework
Die Kommunikation zwischen der HMI-Komponente in Java und dem C++Framework erfolgt über Shared-Memory. Die Wahl viel auf diese IPC Form, da das C++
Framework dieses Verfahren nutzt und somit weniger Anpassungen notwendig sind.
Auch in Bezug auf Geschwindigkeit ist dies die erste Wahl (siehe 3.2.1). Die Festlegung auf Shared-Memory bedeutet die zusätzliche Entwicklung einer nativen Bibliothek, welche über JNI angesprochen wird, sowie weitere Anpassungen in beiden
Frameworks.
Ein Problem bei der Entwicklung einer Shared Memory-Schnittstelle zwischen dem
C++-Framework und einem externen Programm ist, dass der Kontext, also alle Informationen, wie zum Beispiel welche Daten, an welcher Stelle im Shared Memory
liegen, dem externen Programm nicht bekannt ist. Im C++-Framework selbst ist
dies jeder Komponente bekannt, da der gemeinsam genutzte Speicher vor dem ersten
Fork-Aufruf initialisiert wird.
Für das Javaprogramm ist es unmöglich über einen Fork-Aufruf Daten zu erhalten.
Dies liegt an der Art, wie Javaprogramme ausgeführt werden. Da diese in einer Virtuellen Maschiene (VM) ausgeführt werden, muss ein ausführbares Programm (die
JVM) gestartet werden, welches im Gegensatz zum laufenden Programm oder dynamisch ladbaren Bibliotheken immer einen eigenen Datenbereich anlegt. Auch aus
den in 3.2.1
Shared Memory
genannten Gründen ist dies nicht möglich.
Es gäbe verschieden Möglichkeiten, die notwendigen Daten auszutauschen. Dies könnte zum Beispiel über einen zusätzlichen, am Beginn des gemeinsamen Speichers gelegen Bereichs geschehen, welcher fest deniert ist. Aber auch der Datenaustausch über
Pipes oder Sockets wäre denkbar. In diesem Fall wäre der Aufwand für ein solchen
Datenaustausch aber übermäÿig hoch, da es auch umfangreichere Änderungen am
original Framework bedeuten würde und eine dynamisches Shared Memory für den
Prototypen nicht notwendig ist.
Da das C++-Framework den gemeinsamen Speicher nicht dynamisch anlegt, sondern die Gröÿe und Lage der Objekte statisch im Sourcecode festgelegt ist, wird in
der JNI-Library immer von der gleichen Lage und Gröÿe der Objekte ausgegangen.
Dies wird gewährleistet, in dem verschieden Teile des C++-Frameworks für die JNILibrary wiederverwendet werden (siehe 5.1.2
JNI-Library ).
Dadurch ist es möglich,
die Objekte wie eine Schablone über die bereits im Shared Memory vorhandenen
Objekte zu legen. Es ist jedoch zu beachten, dass die Objekte nur einmal initialisiert
werden dürfen. Aus diesem Grund initialisiert die JNI-Library keine Objekte und
muss immer nach dem C++-Framework gestartet werden. Ein einfaches Start-Skript
wurde aus diesem Grund für das Starten der Frameworks verwendet.
32
Im Folgenden wird auf die wichtigsten Bereiche des Frameworks nocheinmal genauer
eingegangen.
Aufbau der Java JNI-Klasse
Für den Zugri auf die native Bibliothek wurde die Klasse CMessageHandler implementiert. Sie stellt die Schnittstellen für die Kommunikation mit dem C++Framework zur Verfügung.
public class CMessageHandler {
public static native int initSharedMem ();
public static native int
getMessage ( CMessage objMessage );
public static native int
sendMessage ( CMessage objMessage );
static {
}
System . out . println (" Lade " +
System . mapLibraryName (" MessageHandler " ));
System . loadLibrary (" MessageHandler " );
}
Listing 5.1: JNI-Klasse CMessagehandler
Diese Klasse beinhaltet nur die Schnittstellen zur nativen Bibliothek und enthält
keinen eigenen Code. Da es sich bei diesen Schnittstellen um statische Funktionen
handelt, ist es nicht notwendig eine Instanz dieser Klasse anzulegen.
JNI-Library
Die Aufgabe der JNI-Library ist es, die Daten, welche in einer Queue der HMIComponente ankommen, entgegenzunehmen und an die Java-HMI weiterzuleiten.
Eine weiter Aufgabe ist das Entgegennehmen einer Nachricht vom Java-Framework
und das weiterleiten dieser Nachricht an den Main Dispatcher des C++-Frameworks.
Zu diesem Zweck wurden einige der Basiselemente des C++Frameworks wiederverwendet. Wenn immer möglich, wurde dazu ein Link benutzt um die Redundanz
33
gering zu halten. Dies war jedoch teilweise nicht möglich, da einige Klassen geändert
werdenmusstenn.
Die JNI-Library übernimmt im Grunde die Aufgaben der Adminkomponente des
C++Frameworks für das Java-Frameworks. Im Gegensatz zur Adminkomponente
braucht es jedoch nur die Kontexte anlegen. Die einzelnen Komponenten werden nicht
benötigt. Beim Anlegen der Kontexte, muss jedoch darauf geachtet werden, dass
Objekte, welche sich im Shared Memory benden, nicht wirklich angelegt werden,
sondern nur die richtigenAdressenn erhalten.
Um einen Kontext anlegen zu können, werden zusätzlich zur Kontextklasse (CContext) die Klassen CComponentContext, CCommQueue, CBinarySemaphore und CMessage, sowie die Headerdatei Global.h benötigt. Dabei werden jedoch nur von
den Klassen CContext, CComponentContext und CCommQueue mit Hilfe der newFunktion neue Objekte im gemeinsamen Speicher angelegt. Diese Funktionsaufrufe
müssen entfernt werden.
context . mNormalQueuePtr
= new ( memPtr ) CCommQueue ( normalQueueSize ,
* context . mTriggerSemaphorePtr );
Listing 5.2: Aufrufe wie diese wurden entfernt
Eine weiter Aufgabe bei der Initialisierung der JNI-Library ist das Abfragen und
Speichern der Java-Klasse CMessage. Da dies ansonsten bei jedem Empfangen oder
Senden einer Nachricht durchgeführt werdenmüsstee und nach Aussagen von Sun sehr
zeitaufwendig ist (siehe [Mic04b] Online im Internet verfügbar [Stand: 28.12.2004]).
JNIEXPORT jint JNICALL Java_CMessageHandler_initSharedMem
( JNIEnv * env , jclass mCMessageHandler )
{
CContext :: createContexts ();
// buffer often used Objects
// jclass clsMessage
clsMessage = env -> GetObjectClass ( Message );
mHMISystemQueue =
& CContext :: getHMIContext ()
. getSystemQueue ();
mHMINormalQueue =
& CContext :: getHMIContext ()
. getNormalQueue ();
34
mHMITriggerSemaphore =
& CContext :: getHMIContext ()
. getTriggerSemaphore ();
mMainDispatcherQueue =
& CContext :: getMainDispatcherContext ()
. getNormalQueue ();
}
return 0;
Listing 5.3: Die init-Funktion der JNI-Library
Durch das nicht Anlegen der Komponenten, wird auch der Dispatcher der HMIKomponente nicht angelegt. Diese Aufgabe muss die JNI-Library übernehmen. In
der Funktion
getMessage
ist die Funktionalität des Dispatchers, sowie die Konver-
tierung von der C++-Nachricht in die Java-Nachricht implementiert. Im Gegensatz
zur Dispatcher-Funktion im C++-Framework, blockiert der Aufruf von getMessage
immer.
JNIEXPORT jint JNICALL Java_CMessageHandler_getMessage
(
JNIEnv * env ,
jclass mCMessageHandler ,
jobject Message ) {
bool hasMessage = false ;
// as long as the dispatcher is not interrupted
// it will block if no message is available
while ( false == hasMessage )
{
// try to get an system command
hasMessage =
mHMISystemQueue -> getMessage ( mMessage );
if ( false == hasMessage )
{
hasMessage =
mHMINormalQueue -> getMessage ( mMessage );
}
if ( true == hasMessage )
{
// a command has been found
break ;
35
}
}
mHMITriggerSemaphore -> take ();
if ( true == hasMessage )
{
jfieldID senderID =
env -> GetFieldID (
jfieldID receiverID =
env -> GetFieldID (
jfieldID opCode =
env -> GetFieldID (
clsMessage ,
" sender " ,
"I" );
clsMessage ,
" receiver " ,
"I" );
clsMessage ,
" opCode " ,
"I" );
...
env -> SetIntField (
Message ,
senderID ,
mMessage . getSenderID ());
env -> SetIntField (
Message ,
receiverID ,
mMessage . getReceiverID ());
env -> SetIntField (
Message ,
opCode ,
mMessage . getOpcode ());
...
}
return 0;
36
}
return -1;
Listing 5.4: Die getMessage-Funktion der JNI-Library
Beim Senden einer Nachricht, wird diese in eine C++-Nachricht kopiert und wie im
C++-Framework über die add-Funktion der Main-Dispatcher-Queue verschickt.
C++Framework
Am C++-Framework waren nur sehr wenige Änderungen notwendig, da eigentlich
nur der Start der HMI-Komponente unterbunden werden muss, um zu verhindern,
dass die Photonoberäche gestartet wird und das die Nachrichten aus der HMI-Queue
abgeholt werden. Dazu reicht es, die HMI aus der Admin-Komponente zu entfernen.
Da jedoch ein Groÿteil der Source- und Header-Dateien nur für die HMI nötig sind
wurden diese entfernt. Dies führte zu einem übersichtlicherem Projekt und einem
schnelleren kompilieren.
5.1.3 Implementierung der HMI-Komponente
Die übergeordnete Klasse der HMI ist, wie im C++-Framework, die Klasse CHMIComponent. Sie ist zuständig für das Initialisieren, Starten, Stoppen und Pausieren
der HMI und enthält einen Dispatcher für das Verteilen von Nachrichten der eigenen
Queue.
Die Klasse CHMIController ist für den logischen Ablauf zuständig und nutzt die
Klasse CCDPlayerGUI zum Darstellen einer CDPlayer-Komponente. Da diese Klassen später wiederverwendet werden sollen, gibt es eine extra Klasse CHMIManager,
welche für das Starten und Initialisieren der HMI zuständig ist.
Die Klassen CContext und CMessage sind auch im C++-Framework vorhanden und
haben auch die gleiche Funktion. Es waren jedoch in beiden Fällen einige Änderungen
notwendig.
5.2 Das Java Framework
Wie bereits unter 1.2
Zielsetzung
erwähnt wurde, sollen verschiedene Vergleiche zwi-
schen einem C++-Framework und einem Java-Framework angestellt werden. Um
37
solche Vergleiche durchführen zu können, sollten sich die Frameworks hinsichtlich
Funktionsweise und Aufbau möglichst wenig unterscheiden. Da eine Software Architektur unabhängig von einer Programmiersprache ist, konnte die C++-FrameworkArchitektur aus 2.2
Architektur und Funktionsweise
vollständig übernommen werden.
5.2.1 Ausführen der Komponenten
Vergleich von Prozessen und Threads
Aus der Architektur des Frameworks ergibt sich, dass die einzelnen Komponenten
parallel ausgeführt werden müssen. Dazu kann eine Komponente entweder als eigenständiger Prozess oder als ein Thread gestartet werden. Das C++-Framework nutzt,
Prozesse für die Ausführung der Komponenten (siehe 2.3
der Interprocesscommunication (IPC) ).
Verwendete Möglichkeiten
Die Wahl viel auf Prozesse, da diese in ei-
nem eigenen Speicherbereich ausgeführt werden. Dadurch fallen zum Beispiel Fehler
durch ungültige Pointer sehr schnell auf. Sollte eine Komponente unerwartet reagieren, blockiert sein oder durch einen Fehler beendet werden, so laufen die anderen
Prozesse weiter Bei der Verwendung von Threads wäre die Wahrscheinlichkeit, das
durch einen Fehler auch andere Threads in Mitleidenschaft gezogen werden, sehr viel
gröÿer.
Unter Java kommen diese Vorteile von Prozessen jedoch kaum zum Tragen. Auch
innerhalb einer JVM ist weder möglich auf Speicher direkt zu zugreifen, noch über
die Grenzen einer Variablen hinweg zuschreiben. Ein Thread kann auf ein Objekt
also nur zugreifen, wenn es eine Referenz auf dieses besitzt. Lediglich auf statische
Funktionen und Variablen kann immer zugegrien werden. Führt ein Fehler in einem
Thread zum Abbruch, können die anderen Threads ungehindert weiter laufen.
Prozesse haben also unter Java kaum Vorteile, jedoch einige Nachteile. Da ein Javaprogramm nicht alleine als eigenständiger Prozess laufen kann, muss für jeden neuen
Prozess auch eine neue JVM gestartet werden. Diese benötigt Zeit zum Starten und
Speicher. Bei Tests auf dem Target, mit einem einfachen Hello World Beispiel, ergab sich eine Speicherbelegung von 3MB pro JVM + einmalig 1,5MB für gemeinsame
Bibliotheken (siehe auch 6.1
Performance ). Dies ist eine nicht unerhebliche Belastung
für ein Embedded System.
Weiterhin ist es Java Prozessen nicht möglich, über gemeinsamen Speicher, Daten
auszutauschen (siehe 3.2.1
Shared Memory ). Dies könnte zum Beispiel über JNI oder
auch über Sockets geschehen. Beide Verfahren sind aber wesentlich langsamer, als
ein Zugri innerhalb einer JVM.
38
Aus diesen Gründen ist es sinnvoller Java-Threads zu nutzen, wenn es sich nicht um
über Rechnergrenzen verteilte Anwendungen handelt.
Implementierung der Admin Komponente
Das Starten der einzelnen Komponenten übernimmt die Admin Komponente. Um
eine Komponente als Thread auszuführen gibt es zwei Möglichkeiten. Man kann die
Komponentenklasse von Thread erben lassen und die run-Methode überschreiben,
oder das Runnable Interface implementieren. Für das Java Framework wurde die
zweite Variante gewählt, da eine Klasse unter Java nur von einer Klasse erben kann,
jedoch mehrere Interfaces implentieren kann. Durch das Wählen der Interface Methode bleibt es weiterhin möglich, von einer anderen Klasse zu erben.
public class CMiniCommander implements Runnable {
public void run () {
// Hier beginnt das Leben des Threads
...
}
}
Listing 5.5: Beispiel Implementierung des Runnable Interfaces
Um eine Komponente zu starten, wird in der Adminkomponente ein ComponentContext angelegt. Dieser Kontext benötigt drei Argumente - eine eindeutige ID, die Gröÿe
der Systemqueue und die Gröÿe der normalen Queue. Mit Hilfe der add-Methode wird
der ComponentContext der Auistung aller Kontexte hinzugefügt. Nun kann die eigentliche Komponente erzeugt werden. Dabei bekommt sie ihren Kontext übergeben,
um auf ihr Queues zugreifen zu können. Im letzten Schritt wird die Komponente
gestartet.
CComponentContext hmiContext =
new CComponentContext ( CContext . HMIID , 10 , 100);
CContext . add ( hmiContext );
CHmiComponent hmi = new CHmiComponent ( hmiContext );
new Thread ( hmi ). start ();
Listing 5.6: Anlegen und Starten einer Komponente
Im Gegensatz zum C++-Framework müssen im Java-Framework nur zwei Dateien
geändert werden, wenn eine neue Komponente hinzugefügt wird. Zusätzlich zu den
39
Änderungen in der Admin Komponente muss in die Klasse CContext noch eine Komponenten ID hinzugefügt werden. Dies könnte vermieden werden, wenn für die Komponenten ID ein Identier in Form eines String benutzt werden würde. Dies würde
das Abfragen von CContext nach einem konkreten Kontext jedoch etwas langsamer
machen.
5.2.2 Verwalten des Speichers
Objekt Pooling bringt unter Java weitaus weniger Performanzezuwachs als weithin
angenommen wird. Wie Dr. Click unter [Cli03] beschreibt, gibt es bei kleinen Objekten sogar einen Performanzeverlust. Nur bei sehr groÿen Objekten von mehreren
Kilobytes kann es einen Geschwindigkeitszuwachs geben.
Es ist in Java nicht möglich eine Objekt anzulegen und den reservierten Speicher für
ein anderes Objekt zu nutzen, wie es zum Beispiel in C++ möglich ist (siehe 5.7).
In Java würde dieses Vorgehen zu einer Exception führen, da nur eine Verallgemeinerung, also der Cast von einer Unterklasse zu ihrer Superklasse möglich ist. Die
Objekte im Pool müssen also vom gleichen Typ sein, wie die benötigten Objekte.
CBigClass * bigObject = new CBigClass ();
CSmallerClass * smallerObject = ( CSmallerClass *) bigObject
Listing 5.7: Wiederverwenden von Speicher in C++
Aus diesem Grund wurde die Strategie verfolgt, alle Objekte die benötigt werden
möglichst beim Starten anzulegen und diese wieder zu verwenden. Um keine neuen
Nachrichten anzulegen, wenn man eine Nachricht aus einer Queue abholt, gibt es
dann zwei Möglichkeiten. Man könnte die Nachricht nicht wirklich aus der Queue
entfernen, sondern warten bis die Bearbeitung der Nachricht abgeschlossen ist und
erst dann mit der nächsten Nachricht fortfahren. Dies hat jedoch den Nachteil, das bei
einem Fehler, dieser nur schwerer aundenbar ist. Ein weitaus gröÿeres Problem tritt
aber auf, sollte innerhalb einer Komponente ein Thread gestartet werden, welcher für
die Verarbeitung der Nachricht benötigt wird. Dieser Thread würde den Thread der
Komponente blockieren und könnte zu einem Deadlock führen.
Eine andere Möglichkeit ist das Übergeben eines Nachrichtenobjektes an die Queue,
wenn eine Nachricht abgeholt wird. Bei dieser Variante muss kein Objekt kopiert werden, da nur Referenzen auszutauschen sind. Auch wird der Dispatcher nicht blockiert,
da er sofort mit der nächsten Nachricht weitermachen kann. Da im Framework alle
Objekte wiederverwendet werden sollen ist dies daeektivstete Verfahren, welches
ohne Aufruf eines Pooling Managers auskommt.
40
5.3 Die HMI-Komponente
Die HMI-Komponente besitzt einen Controller, welcher für das Starten der SWTUmgebung, sowie das Verwalten und das Umschalten auf die verschiedenen GUIKomponenten verantwortlich ist.
Eine GUI-Komponente ist in diesem Fall eine grasche Darstellung eine Gerätes, wie
zum Beispiel dem CD-Player. Der HMI-Controller ist dafür verantwortlich, Benutzereingaben an die GUI-Komponente weiterzureichen, welche darauf mit dem Darstellen neuer Inhalte oder auch dem Verschicken von Nachrichten an andere Komponenten reagieren kann.
5.4 JNI Module
Für ein funktionales Framework mit den unter 1.2
Zielsetzung
genannten minimalen
Anforderungen war es nötig, verschiedene JNI-Module zu erstellen.
5.4.1 Input Schnittstelle
Das Target besitzt für Benutzereingaben ein Gerät, welches über 6 Knöpfe und einen
drehbaren Knopf verfügt. Dieses wird über die serielle Schnittstelle angeschlossen
und nennt sich Mini-Commander. Unter QNX Neutrino ist jedoch die Java Standarderweiterung javax.comm nicht verfügbar. Aus diesem Grund wurde eine JNI
Bibliothek entwickelt, welche die Daten vom Mini-Commander entgegen nimmt und
dem Framework die gedrückte Taste als Zahlencode zurückgibt.
Auf der Java-Seite wurde die Klasse CMiniCommander als Komponente implementiert, welche jedoch keinen Kontext besitzt, da sie keine Nachrichten empfangen muss.
Die Schnittstelle zur JNI-Bibliothek wird über die Funktionen initSharedMem() und
getPressedKey() hergestellt. In diesem Fall sind die Funktionen als private deklariert,
da sie sonst von jedem Objekt auch ohne Referenz ausgeführt werden könnten.
Die Funktion getPressedKey() blockiert den Aufruf solange, bis eine Taste gedrückt
wurde. Dadurch verbraucht die Mini-Commander Komponente keine Rechenzeit,
wenn auf eine Taste gewartet wird.
41
5.4.2 Mostgateway
Das Mostgateway wurde in Java etwas anders implementiert, als im C++-Framework.
In der C++-Implentierung wird im Maindispatcher ein weiterer Thread gestartet,
welcher für das Verschicken und Empfangen der Mostnachrichten verantwortlich ist.
Dies stellt einen Bruch mit der Frameworkarchitektur dar, da für jedes Gerät eine
Komponente vorgesehen ist. Im Java-Framework wurde das Mostgateway als eigenständige Komponente (CMostComponent) implementiert.
Ein weiterer Unterschied gegenüber dem C++-Framework besteht in der Art wie das
Gateway Nachrichten abfragt. Es wird Polling verwendet, um abwechselnd nachzusehen, ob eingehende Nachrichten für das Framework oder ausgehende Nachrichten
vom Framework vorliegen. Da dies wertvolle Rechenzeit verbraucht, wurden hierfür
blockierende Aufrufe implementiert.
Wie in 5.8 zu sehen ist, werden zwei Instanzen der Klasse CMostIO erstellt. Ein
Aufruf der getMessage-Funktion blockiert nun den Thread. Um eine Nachricht senden
zu können muss dies durch einen anderen Thread ausgeführt werden. Würden jedoch
ein zweier Thread auf die Most Bibliothek zugreifen, könnte das zu unvorhersehbaren
Resultaten führen, da sie nicht Thread sicher ist. Jede Instanz der Klasse CMostIO
önet einen eigenen Stream zur Most Bibliothek, wodurch sie sich nicht gegenseitig
stören können.
CMostIO mostSender ;
CMostIO mostReceiver ;
CMessage rMessage ;
CMessage sMessage ;
JNIEXPORT jint JNICALL Java_CMostDispatcher_initMost
( JNIEnv * env , jclass , jobject ) {
int resultSender =
mostSender . init ();
int resultReceiver = mostReceiver . init ();
}
if ( resultSender || resultReceiver )
return 1;
else
return 0;
/*
* Class :
CMostDispatcher
* Method :
runMost
* Signature : () I
42
*/
JNIEXPORT jint JNICALL Java_CMostDispatcher_getMostMessage
( JNIEnv * env , jclass , jobject Message ) {
mostReceiver . getMessage ( rMessage );
// Copy Most to JavaMessage
....
}
JNIEXPORT jint JNICALL Java_CMostDispatcher_sendMostMessage
( JNIEnv * env , jclass , jobject Message ) {
// Copy Java to Mostmessage
....
}
mostSender . sendMessage ( sMessage );
Listing 5.8: Auszug aus der Most-JNI-Library
Die Klasse CMostIO wurde zu kleinen Teilen aus der Klasse CMostIO (in der Datei
most_thread.cpp) des C++-Frameworks übernommen.
Da nur das Target einen Mostbus besitzt, wird das Fehlen der Most-Bibliothek nur
gemeldet, führt aber nicht zu einem Fehler. Alle Pakete, welche an die Mostkomponente geschickt werden, werden in diesem Fall verworfen.
5.4.3 CD-Player
Der CD-Player wurde auch als Framework-Komponente implementiert. Es handelt
sich ihr um eine Hardwarenahe-Komponente. Aus diesem Grund wurden für Momentics und Linux/Windows unterschiedliche Versionen erstellt. Für das Framework
selbst, ergibt sich jedoch kein Unterschied.
5.5 Grasche Oberächen unter Java
5.5.1 Auswahl eines Toolkits
Beim SWT handelt es sich um ein Toolkit zum Erstellen von graschen Benutzeroberächen. Aus den folgenden Gründen wurde SWT anderen vorhanden Lösungen
43
den Vortritt gegeben:
AWT Das Abstract Windowing Toolkit (AWT) benutzt native Widgets des unterliegenden Betriebssystem zur Darstellung von Oberächen. Dadurch ist es sehr
schnell. Jedoch ist es in seinen Fähigkeiten sehr beschränkt, da es aus Portablilitätsgründen nur einen relativ kleinen Befehlsumfang hat.
Swing Das Toolkit Swing baut auf AWT auf und ist sehr viel exibler. Da es aber die
Widgets selbst zeichnet, ist es auch sehr langsam.
SWT Das Standard Widget Toolkit (SWT) ist ursprünglich eine Eigenentwicklung
von IBM, welche nun unter einer Open-Source-Lizenz steht. Mit Hilfe von JNI
nutzt SWT die Betriebssystemfunktionen, um Widgets darzustellen. Es ist dadurch exibler als AWT und schneller als Swing.
J9 Es gibt für die Targetplattform keine JVM von Sun. Aus diesem Grund wurde
die JVM von IBM (j9) ausgewählt, welche als einzige ohne weitere Anpassungen
nutzbar ist und auch von QNX selbst eingesetzt wird. Unter IBMs JVM kommt
hauptsächlich das auch von IBM entwickelte Standard Widgets Toolkit zum
Einsatz. Andere Benutzeroberächen, wie Swing und AWT laufen hingegend
nur mit Einschränkungen.
5.5.2 SWT-Programmierung
Die Programmierung mit dem Standard Widget Toolkit (SWT) hat einige Besonderheiten, welche eine Anpassung des Frameworks notwendig gemacht haben. Aus
diesem Grund wird in diesem Kapitel näher auf SWT eingegangen.
Das Standard Widget Toolkit basiert auf einem single-threaded UI Modell. Das bedeutet, dass der Thread, welcher das Display startet, der UI-Thread wird. Nur dieser
Thread hat Zugri auf die grasche Oberäche. Für die HMI-Komponente bedeutet
dies, dass sie entweder alle Aktivitäten in der Dispatcherschleife der SWT-Oberäche
durchführt, oder einen Zugriüberber die syncExec()-Funktion von SWT zu realisieren. Da die HMI der Controller des Frameworks ist und ein langer Update der
GUI-Oberäche das ganze System ausbremsen könnte, wurde die zweite Variante
ausgewählt.
Um eine Funktion im UI-Thread auszuführen, kann eine Klasse angelegt werden, welche von der Runnable-Klasse erbt, oder das Runnable Interface implementiert. Eine
weitere Methode besteht darin, beim Anlegen einer Instanz von der Klasse Runnable
die virtuelle Funktion run() zu implementieren (siehe 5.9). In der Run-Funktion wird
44
der Code eingefügt, welcher auf den UI-Thread zugreifen soll. In der Methode handleMessage wird dieser Code asynchron ausgeführt. Die Funktion blockert dadurch
den Thread der HMI-Komponente nicht.
Runnable mGUIMessageHandler = new Runnable (){
public void run () {
switch ( mMessage . getParam1 () & 0 xff )
{
case 0 xc3 :
mGUIHandler . ButtonA ();
break ;
};
}
}
...
public void handleMessage ( CMessage msg ) {
mMessage = msg ;
mGUIHandler . mDisplay . asyncExec ( mGUIMessageHandler );
}
Listing 5.9: Ausführen von Code im UI-Thread
45
6 Vergleiche der Frameworks
6.1 Performance
Die Zeit wurde unter Java mit Hilfe der Funktion System.currentTimeMillis() gemessen. Die Auösung der Zeitmessung lag somit im Millisekundenbereich. Auf neueren
Java VMs gibt es eine Funktion, welche auch im Nanosekundenbereich messen kann.
Diese stand auf dem Target jedoch nicht zur Verfügung. Da die benötigte Zeit für
einen Funktionsaufruf im ns-Bereich liegt, wurde die Aufrufe zwischen 1 bis 1 Millionen mal wiederholt und daraus die Dauer eines Aufrufs errechnet.
Jeder Test wurde mindestens 5 mal durchgeführt, um zu verhindern, dass Ausreiÿer
in die Testergebnisse mit einieÿen. Der erste Aufruf einer Funktion aus einer JavaKlasse wurde nicht gewertet, da dieser immer 18ms Zeit kostete. Dies muss an dem
Just In Time Comiler von Java liegen, welche die Klasse vorher optimiert.
6.1.1 Leistungsfähigkeit der JNI-Schnittstelle
Das Testverfahren
Die JNI-Schnittstelle von Java ist für die korrekte Übergabe von Daten verantwortlich. Dabei muss sie zur Laufzeit, je nach Datentyp und Zugrisart verschiedene Tests
ausführen und die Datenintegrität sicherstellen. Es ist also zu erwarten, das jeder Zugri von Java auf die JNI-Bibliothek und vor allem von der JNI-Bibliothek auf Java
mit einem Performanzeverlust behaftet ist.
Um zu ermitteln, wie viel Zeit an der JNI-Schnittstelle verloren geht, wurde ein
Testprogramm entwickelt, welches einen Vergleich anstellt zwischen den Zugrien
auf interne Java- und externe JNI-Konstrukte.
Das folgende Testbespiel zeigt, wie die Zeit für eine bestimmte Anzahl an JNI Aufrufen gemessen wird. In diesem Testfall soll untersucht werden, wie viel länger der
Aufruf einer JNI-Funktion mit einer Long-Variablen (ein 64-Bit-Integer) gegenüber
46
einer gleichartigen Java-Funktion benötigt. Dabei muss berücksichtigt werden, das
der Aufruf der JNI-Funktion vom Java-Compiler nicht optimiert werden kann, da
diese dem Compiler nicht vorliegt. Jedoch kann der Java-Compiler, den Aufruf der
Java-Funktion optimieren. Es wurde jedoch bei jedem Test anhand der Ergebnisse
sicher gestellt, dass der Test Aussagekraft besitzt und nicht etwa durch Compileroptimierungen ganze Schleifen wegoptimiert wurden sind.
public long JNITest_long ( int intNumOfTests ) {
int counter ;
Date
Date
long
long
dtStartTime = new Date ();
dtEndTime = new Date ();
lngDiffTime ;
lngBuffer ;
dtStartTime . setTime ( System . currentTimeMillis ());
for ( counter = 0;
counter < intNumOfTests ;
counter ++) {
mDiffTime = CJNIHandler . getLong ();
}
dtEndTime . setTime ( System . currentTimeMillis ());
lngDiffTime = dtEndTime . getTime ()
- dtStartTime . getTime ();
}
return lngDiffTime ;
Listing 6.1: Testfall 1: Aufrufen einer JNI-Funktion
public long JavaTest_long ( int intNumOfTests ) {
int counter ;
Date
Date
long
long
dtStartTime = new Date ();
dtEndTime = new Date ();
lngDiffTime ;
lngBuffer ;
dtStartTime . setTime ( System . currentTimeMillis ());
for ( counter = 0; counter < intNumOfTests ; counter ++) {
lngBuffer = CJavaHandler . getLong ();
}
47
dtEndTime . setTime ( System . currentTimeMillis ());
lngDiffTime = dtEndTime . getTime ()
- dtStartTime . getTime ();
return lngDiffTime ;
}
Listing 6.2: Testfall 1: Aufrufen einer Java-Funktion
JNIEXPORT jlong JNICALL
Java_CJNIHandler_getLong ( JNIEnv * env , jclass cls ) {
return lngTest ++;
}
Listing 6.3: Testfall 1: Rückgabe einer Long-Variablen an Java
Diese Tests wurden für primitive Typen wie Integer, Long und Char durchgeführt und
auch mit Objekten, wie String und einem Nachrichtenobjekt der Framework-Klasse
CMessage.
Die Testergebnisse
Die folgenden Ergebnisse zeigen, wie lange es dauert, einen Wert von einer Funktion
zu erhalten. Bei primitiven Datentypen benötigt die JNI-Funktion etwas mehr als
doppelt solange wie die interne Java-Funktion. Die Gröÿe der Variablen spielt dabei
eine untergeordnete Rolle.
Anders sieht das bei Objekten, wie einem String aus. Hier benötigt die JNI-Funktion
mehr als 12 mal länger, als die interne Java Funktion. Dies kann damit erklärt werden,
das die Prüfung eines ungeschützten Puers, welcher ein String in C++ darstellt, sehr
aufwendig ist. Da die Länge des Strings in diesen Dimensionen vernachlässigbar.
Char
Int
Long
String(10)
String (10000)
JNI Zeit in ns
3,36
3,42
3,31
12,0
12,0
Java Zeit in ns
1,46
1,46
1,45
0,96
0,96
Verhältnis JNI/Java
2,30
2,34
2,28
12,5
12,5
Die Übergabe von Werten an die JNI-Bibliothek ist für primitive Typen in etwa
genauso schnell wie das Erhalten von Werten (siehe Tabelle 6.1.1). Das Übergeben
von Objekten, wie einem String ist hier jedoch doppelt so schnell. Im Vergleich mit
dem Aufruf einer Java-Funktion ist das Übergeben eines Strings aber immernoch
etwa 5 mal langsamer.
48
Char
Int
Long
String(10)
String (10000)
JNI Zeit in ns
3,63
3,60
3,69
6,08
6,09
Java Zeit in ns
1,40
1,40
1,41
1,19
1,19
Verhältnis JNI/Java
2,59
2,57
2,61
5,11
5,12
Alle nicht im obigen Beispiel aufgeführten Objekte verhalten sich genau so, wie man
es nach den Tabellen 6.1.1 und 6.1.1 erwartet. So ist das Verhalten eines Arrays von
einem primitiven Datentyp, mit dem eines Strings vergleichbar. Greift man auf die
Variablen eines Objektes zu, so muss man für jede Variable, auf die zugegrien wird,
die oben angegebene Zeit veranschlagen.
Aus der obigen Tabelle lassen sich verschiedene Schlüsse für das Java-Framework
ziehen. Da der Aufruf einer JNI-Funktion relativ teuer ist, aber die übergebene Gröÿe
der Variablen kaum eine Rolle spielt, sollten die folgenden Regeln beachtet werden.
Daten immer in einem Aufruf übergeben werden. es vermieden werden, Daten durch
1. Wenige Variablen von einem kleinen Datentyp, wie Integer oder Byte können
zu einem Gröÿerezusammengefasstÿt werden.
2. Gröÿere Datenmengen per Array übergeben
3. Keine Objekte übergeben, aus denen mehrere Membervariablen benötigt werden. Diese besser in einem Array zusammen fassen.
6.1.2 Startverhalten
Das Startverhalten des Java-Frameworks ist Erwartungsgemäÿ etwas anders, als das
vom C++-Framework. Bei Messungen mit einem einfachen Hello World Programm
hat sich gezeigt, das auf dem Target 8,9 s vergehen, bis die JVM gestartet ist.
Um alle Komponenten des Frameworks zu starten, benötigt das Java-Framework 4,6
s. Das C++-Framework braucht dazu nur 0,96 s. Dies liegt vor allem an der SWTOberäche, welche alleine 3,5 s benötigt um zu starten. Mit dem Start der JVM
benötigt das Java-Framework somit 13,5 s zum starten.
Da die Komponenten in verschiedenen Threads laufen, können diese jedoch schon
mit ihrer Arbeit beginnen, bevor die SWT-Oberäche gestartet wurde.
49
6.1.3 Reaktionszeiten & Antwortverhalten
Um die Reaktionszeiten der Frameworks vergleichen zu können, wurden versucht die
Laufzeiten von Nachrichten unter Last zu messen. Da es sich beim Verschicken von
Nachrichten jedoch nur um das Umkopieren von Referenzen handelt, ergaben sich
hierbei keine verwertbaren Ergebnisse.
Das Verhalten des Java-Frameworkes wurde überprüft, in dem das System durch dauerhaftes Nachrichten generieren stark belastet wurde. Dabei generierte die CDPlayerKomponente ein einer Endlosschleife Nachricht zum Update der CD-Spielzeit. Das
Framework reagierte aber auch hier ohne gröÿere Verzögerung. Lediglich die grasche
Oberäche wurde etwas träger.
50
7 Zusammenfassung & Ausblick
Die Entwicklung des Java-Frameworks hat gezeigt, dass Java auf Embedded Systemen
möglich ist. In vielen Bereichen ist Java genauso schnell, wie ein in C++ geschriebenes
Programm. So kann man Subjektiv keinen Unterschied zwischen dem C++- und dem
Java-Framework ausmachen, sobald das Framework gestartet ist.
Für den Automotive Bereich dürfte die lange Startphase der Virtuellen Java Maschine
das gröÿte Problem darstellen. Da dies für die Entwickler eine feste Gröÿe darstellt,
können nur noch schnellere Prozessoren oder in Hardware gegossene VMs daran etwas
ändern.
Während der Entwicklung hat sich auch gezeigt, dass Java auf einem Embedded
System nicht alle seine Stärken voll ausspielen kann. Durch die Notwendigkeit, die
JNI-Schnittstelle zu benutzen, kann man sehr schnell die Vorteile der Javaprogrammierung verlieren. Schon bei den relativ wenigen implementieren Funktionen des
Frameworks war es unumgänglich, drei JNI-Bibliotheken zu erstellen. Dabei hat sich
gezeigt, das ein Fehler in einer JNI-Bibliothek das gesamte Framework zum Absturz
bringen kann.
Auf Embedded Systemen verliert man also teilweise die leite Rückverfolgung von Fehlern, die Robustheit der JVM gegeAbstürzeze und die klaren und fehlervermeidenden
Programmiertechniken.
Es stellt sich die Frage, ob Java in diesem Umfeld jetzt schon eingesetzt werden
sollte. Aus der Sicht eines Entwicklers muss ich dies bejahen, da die Programmierung
dennoch schneller und strukturierter voran geht. Aus der Sicht eines Autofahrers sehe
ich die lange Startphase des Java-Frameworks jedoch ein Problem, welches vorher
gelöst werden muss
51
Literaturverzeichnis
[Bal98]
Lehrbuch der SoftwareTechnik - Software
SoftwareQualitätssicherung, Unternehmensmodellierung.
Helmut:
Balzert,
Managment,
Nummer Band 2. Spektrum Akademischer Verlag, Heidelberg - Berlin,
1998.
[Bal00]
Helmut:
Balzert,
Entwicklung.
Lehrbuch der SoftwareTechnik - Software
Nummer Band 1. Spektrum Akademischer Verlag, Heidelberg
- Berlin, 2000.
[Bor04]
Borman,
Garbage.
Sam:
Sensible Sanitation Understanding the IBM Java
IBM, http://www-106.ibm.com/developerworks/ibm/library/i-
garbage1/, 12 2004.
[Cli03]
Click, Dr. Cliff:
Java One: Performance Myths Exposed.
Azul Sys-
tems, http://servlet.java.sun.com/javaone/resources/content/sf2003/conf/sessions/pdfs/1522.pdf, 2003.
[Mic04a] Microsystems,
Sun:
The Java Tutorial: Java Native Interface.
http://java.sun.com/j2se/1.4.2/docs/api/index.html, 12 2004.
[Mic04b] Microsystems,
Sun:
The Java Tutorial: Java Native Interface.
http://java.sun.com/docs/books/tutorial/native1.1/index.html, 12 2004.
[QNX04] QNX Software Systems Ltd:
cumentation,
12 2004.
52
QNX Momentics Development Suite Do-
Herunterladen