Technische Universität Darmstadt

Fachbereich Theoretische Informatik

Prof. Dr. Johannes Buchmann

 

 

Diplomarbeit

 

JLiPSD – eine Portierung des
lipsd nach Java

 

Vorgelegt von

Andreas Müller

 

Angefertigt nach einem Thema von

Prof. Dr. Johannes Buchmann

 

Betreut von Dr. Ing. Thomas Setz

 

Darmstadt, Oktober 2002

 


Hiermit versichere ich, die vorliegende Diplomarbeit ohne Hilfe Dritter und nur mit den angegebenen Quellen und Hilfsmitteln angefertigt zu haben. Alle Stellen, die aus den Quellen entnommen wurden, sind als solche kenntlich gemacht worden. Diese Arbeit hat in gleicher oder ähnlicher Form noch keiner Prüfungsbehörde vorgelegen.

 

Darmstadt, Oktober 2002

 


A distributed System is one that stops

you from getting any work done,

when a machine you’ve never

heard of crashes

Lesley Lamport[1]

 

Danksagung

 

Hiermit bedanke ich mich bei Herrn Prof. Dr. Johannes Buchmann für das interessante Thema dieser Arbeit.

 

Dr. Thomas Setz danke ich für die überaus gute und zwanglose Betreuung während aller Phasen dieser Arbeit.

 

Dem Fachbereich Theoretische Informatik danke ich für die Bereitstellung und Wartung der Hardware, ohne die das Testen der Implementierung nicht möglich gewesen wäre. Insbesondere Danke ich den Administratoren für ihre zuverlässige Unterstützung bei der Lösung technischer Probleme.

 

Auch wenn die Hardware des Fachbereichs zum Testen dringend benötigt wurde, muss hier erwähnt werden, dass es ohne die Diplomarbeit von Herrn Jochen Hähnle keine Testfälle gegeben hätte. Nur die von ihm entwickelten und durchgeführten Tests haben die Qualität des JLiPSD ermöglicht. Vielen Dank Jochen!

 

Des weiteren bedanke ich mich bei Johannes Fischer, Jochen Hähnle, Jörn Huxhorn und Marcus Willhelm für die fruchtbaren Diskussionen und Gespräche, die so manchen gordischen Knoten lösten.

 

Zu guter Letzt bedanke ich mich bei meinen Eltern und der Familie meiner Schwester für ihre Unterstützung während des gesamten Studiums. Ohne sie wäre das Informatikstudium weiterhin ein Traum geblieben.


Inhaltsverzeichnis

 

1          Einleitung. 1

1.1       Überblick. 2

2          Grundlagen. 4

2.1       Die Komponenten des LiPS-Laufzeitsystems. 6

2.1.1       Die fehlertolerante Tupelraummaschine. 6

2.1.2       Das Anwendungs-Laufzeitsystem.. 12

2.1.3       Das System-Laufzeitsystem.. 12

2.2       Ablauf einer verteilten Anwendung mit LiPS.. 13

2.2.1       Design einer LiPS Anwendung. 13

2.2.2       Ablauf 14

2.3       Zusammenfassung. 15

3          Analyse. 17

3.1       C vs. Java. 19

3.1.1       Datentypen. 19

3.1.2       Funktionen / Methoden. 25

3.1.3       Static. 27

3.1.4       Pointer 28

3.1.5       Funktionspointer 29

3.1.6       Arrays. 30

3.1.7       Operatoren. 31

3.1.8       Variable Argumentliste. 34

3.1.9       Präprozessor 35

3.1.10     Thread. 35

3.1.11     Netzwerkprogrammierung. 36

3.2       Komponenten des lipsd. 37

3.2.1       Technische Voraussetzungen. 37

3.2.2       Tupelraumbibliothek. 39

3.2.3       System-Laufzeitsystem.. 46

3.2.4       Anwendungs-Laufzeitsystem.. 56

3.3       Zusammenfassung. 58

4          Modellierung. 60

4.1       Tupelraumbibliothek. 61

4.1.1       Tupel 63

4.1.2       Tupelraumnachricht 66

4.1.3       Verbindungsaufbau. 67

4.1.4       TupleSpace. 68

4.1.5       MessageHandler 70

4.2       System-Laufzeitsystem.. 75

4.2.1       StatusUpdater und DaemonWatcher 75

4.2.2       Befehlsnachrichten. 77

4.3       Anwendungs-Laufzeitsystem.. 80

4.3.1       Design einer verteilten Anwendung in Java. 82

4.3.2       EvalServer 84

4.3.3       MasterStarter 86

4.3.4       ClientStarter 87

4.4       JLiPSDController 87

4.4.1       Start des JLiPSDController 88

4.4.2       Initialisierung des System-Laufzeitsystems. 88

4.4.3       Befehlsverarbeitungskomponente. 90

4.5       Zusammenfassung. 91

5          Implementierung. 93

5.1       Ant 94

5.1.1       build.properties. 95

5.1.2       Ant Targets. 95

5.2       Logging. 97

5.2.1       Erzeugen von Lognachrichten. 97

5.2.2       Konfiguration. 99

5.2.3       Auswertung von Lognachrichten mit Chainsaw.. 101

5.3       Testen. 101

5.4       Installation der Entwicklungsumgebung. 102

5.5       Zusammenfassung. 103

6          JLiPSD.. 104

6.1       Installation. 105

6.2       Start 106

6.3       Ende. 107

6.4       Zusammenfassung. 108

7          Zusammenfassung und Ausblick. 109

7.1       Messageserver 110

7.2       LogicTime. 110

7.3       Sicherheit 110

7.4       SSH.. 111

7.5       Load balancing. 111

8          Lieraturverzeichnis. 112

 


Abbildungsverzeichnis

 

Abbildung 1 Format der Tupelraumnachricht 41

Abbildung 2 Format der Befehlsnachricht 49

Abbildung 3 Format der Befehlsnachricht REGISTER.. 49

Abbildung 4 Format der Befehlsnachricht RSTART. 50

Abbildung 5 Format der Befehlsnachricht STARTEN.. 51

Abbildung 6 Format der GET-USERE-PROCS Nachricht 52

Abbildung 7 Format der USER-PROC Nachricht 52

Abbildung 8 Format der SHUTDOWN-USER Nachricht 52

Abbildung 9 Format der CONF-CHANGE Nachricht 53

Abbildung 10 Modell der Tupelraumbibliothek. 63

Abbildung 11 Modell der Klasse Tuple und ihrer Erben. 65

Abbildung 12 Modell der Klasse AbstractMessage. 67

Abbildung 13 Modell der Vererbungshierarchie der Tupelraumnachrichten. 67

Abbildung 14 Die Schnittstelle TupleSpace. 69

Abbildung 15 Modell der Signalbehandlung. 73

Abbildung 16 Die Klasse LogicTime. 75

Abbildung 17 Modell der regelmäßig ablaufenden Threads. 76

Abbildung 18 Modell der Befehlsnachrichten. 77

Abbildung 19 Modell der Sendekomponente für UdpMessages. 79

Abbildung 20 Modell der Empfangskomponente für UdpMessages. 80

Abbildung 21 Modell der Befehlsverarbeitungskomponente, JLiPSDController 91

Abbildung 22 Methoden der Klasse Log. 98

Abbildung 23 Die Datei resources/log4j.properties. 99


Beispielverzeichnis

Beispiel 1 Funktionsaufruf mit eval() 12

Beispiel 2 Die Struktur Punkt 22

Beispiel 3 Die Typdefinition Punkt 22

Beispiel 4 Die Klasse Punkt 23

Beispiel 5 Gute Portierung der Struktur Punkt 24

Beispiel 6 Verwendung der Klasse Punkt 24

Beispiel 7 Portierung der Funktion move. 26

Beispiel 8 Portierung einfacher Pointerarithmetik. 31

Beispiel 9 Erzeugung eines Tuple. 64

Beispiel 10 Verwendung eines IntegerTuple. 65

Beispiel 11 Verwendung des TupleSpace. 70

Beispiel 12 Registrierung einer Klasse mit dem EvalServer 85

Beispiel 13 Eine typische Loganweisung. 98

Beispiel 14 Angabe der IP Adresse des Log-Empfängers in log4j.properties. 100

 

 


1         Einleitung

 

LiPS, Library for Parallel Systems, ist ein System, welches Anwendern ermöglicht ein für LiPS geschriebenes Programm in einem heterogenen Netzwerk von Arbeitsplatzrechnern verteilt ablaufen zu lassen. Aus der Sicht des Anwenders abstrahiert LiPS von den einzelnen Computern des Netzwerks und der darauf ablaufenden Menge von Prozessen und präsentiert dem Entwickler einer verteilten Anwendung das Netzwerk als einen einzigen fehlerfrei arbeitenden Computer. Dieser virtuelle Computer besitzt einen Speicher, der Tupelraum genannt wird und eine Architektur, welche die Erteilung und automatische Abarbeitung von Aufträgen ermöglicht. Alle Probleme die sich aus der Verwaltung dieses Systems ergeben bleiben dem Entwickler verborgen, sodass er sich ganz auf die Entwicklung der Anwendung konzentrieren kann.

 

Dies ist allerdings nur die Sicht des Entwicklers der verteilten Anwendung. Tatsächlich ist LiPS ein komplexes System, welches das Bild des virtuellen Computers erst entstehen lässt. Es besteht aus einer Menge vernetzter Einzelcomputer, im folgenden LiPS-Rechner genannt, einer Reihe eigenständig ablaufender Programme zur Realisierung des Tupelraums und zur Systemkontrolle sowie einer Software-Bibliothek, welche Anwendungen den Zugang zum Tupelraum ermöglicht. Der auf jedem LiPS-Rechner residierende Kontrollprozess erfüllt zwei verschiedene Aufgaben. Zum einen überwacht er alle anderen Kontrollprozesse und leitet bei deren Ausfall ihren Neustart ein, wodurch die hohe Verfügbarkeit des LiPS Systems gewährleistet wird. Zum anderen verfügt er über Mechanismen Prozesse zu manipulieren, wodurch die Abarbeitung von Aufträgen ermöglicht wird. Zur Speicherung sämtlicher Verwaltungsdaten verwenden die Kontrollprozesse einen Tupelraum, wodurch die Zusammenarbeit sämtlicher Kontrollprozesse koordiniert werden kann. Der Kontrollprozess wird lipsd genannt.

 

Er ist, genauso wie die Tupelraumbibliothek[2], in der Programmiersprache C so geschrieben, dass er für alle Unix-Systemen übersetzt werden kann. Hierdurch ist das Einsatzgebiet von LiPS sowohl auf die Program­miersprache C, als auch auf Unix-Systeme beschränkt. Die in dieser Arbeit vorgestellte Portierung des lipsd nach Java ermöglicht die Teilnahme aller vernetzten javafähigen Rechner an einer mit LiPS verteilten und ebenfalls in Java implementierten Berechnung.

 

Der folgende Abschnitt gibt einen Überblick über die Bearbeitung dieses Problems.

 

1.1      Überblick

 

Diese Arbeit beschreibt, wie der in C implementierte lipsd nach Java portiert wurde. Bevor solch eine Portierung durchgeführt werden kann, ist es notwendig zu verstehen was überhaupt portiert werden soll. Daher werden in Kapitel 2 zunächst die Komponenten des LiPS-Laufzeitsystems vorgestellt, wobei der Schwerpunkt hierbei auf der Darstellung der fehlertoleranten Tupelraummaschine, welche die zentrale Kommunikationskomponente bildet, liegt. Anschließend wird der Ablauf einer verteilten Anwendung geschildert, um die Aufgaben des Kontrollprozesses lipsd zu verdeutlichen.

 

Das folgende Kapitel analysiert im ersten Teil, wie Anweisungen der Programmiersprache C nach Java übersetzt werden können. Im zweiten Teil werden die Komponenten des lipsd analysiert, indem zunächst festgestellt wird, welche technischen Voraussetzungen erfüllt sein müssen, um den Kontrollprozess lipsd laufen lassen zu können. Anschließend wird untersucht, wie eine Verbindung zum Tupelraum aufgebaut wird und wie die Kommunikation abläuft. Im weiteren Teil dieses Kapitels wird dargestellt, welche Aufgaben das System-Laufzeitsystem[3] erbringt, und welche Mittel hierzu eingesetzt werden. Der letzte Abschnitt analysiert die Komponente, welche die Erteilung und automatische Abarbeitung von Aufträgen implementiert, sie wird im folgenden Evalserver genannt.

 

Auf den Ergebnissen der dritten Kapitels aufbauend werden in Kapitel 4 die Komponenten des JLiPSD modelliert. Auch hier wird zunächst auf die zentrale Tupelraumkommunikation eingegangen, bevor die Komponenten des System-Laufzeitsystems auf Javaklassen abgebildet werden. Anschließend wird der Evalserver modelliert und es wird vorgestellt, wie ein Javaprogramm aufgebaut sein muss, damit es mit LiPS verteilt ablaufen kann. Das Kapitel endet mit der Modellierung eines Programms, welches die Initialisierung des LiPS Systems vornimmt und weiterhin zur Überwachung der verteilten Anwendung eingesetzt werden kann - dem JLiPSDController.

 

Die zur Portierung eingesetzte Entwicklungsumgebung besteht aus dem Entwicklungswerkzeug Ant [Ant02], der Commons Logging Bibliothek [Del01], die so konfiguriert ist, dass log4j [Gül02] verwendet wird und dem von Jochen Hähnle entwickelten Testframework [Häh02]. Sie wird in Kapitel 5 vorgestellt.

 

Kapitel 6 stellt den Kontrollprozess JLiPSD vor, indem beschrieben wird, wie Installation, Start und Beendigung vorgenommen werden.

 

Das letzte Kapitel fasst die vorliegende Arbeit zusammen und gibt einen Ausblick auf Probleme und Themen, deren Relevanz während der Entstehung dieser Arbeit auffiel.

 


2         Grundlagen

 

Es ist wohl offensichtlich, dass nicht jedes Programm mit LiPS verteilt ausge­führt werden kann. Es muss speziell für LiPS entwickelt sein und sich zusätz­lich an gewisse Richtlinien halten. Des weiteren kann ein Programm nicht ohne weiteres auf einem entfernten Rechner gestartet werden. Abgesehen von den technischen Voraussetzungen, wie Netzwerk oder Benutzerkennung auf dem entfernten Rechner, müssen auch gewisse Werkzeuge, wie bei­spielsweise SSH vorhanden sein. Sollten alle technischen Bedingungen erfüllt sein, ist es möglich Programme auf entfernten Rechnern auszuführen und Daten auszutauschen. Hierbei unterstützt LiPS den Anwender, indem das System-Laufzeitsystem zur Verfügung gestellt wird. Es setzt sich aus den Kon­trollprozessen (lipsd), die auf jedem LiPS-Rechner ablaufen und den Fixser­vern, die den Tupelraum des Systems verwalten, zusammen. Seine Aufgabe besteht darin, ausgefallene oder nicht mehr benutzbare Maschinen zu erken­nen und der verteilten Anwendung Mechanismen zur Verfügung zu stellen, sich daran anzupassen. Des weiteren stellt LiPS das Anwendungs-Laufzeitsystem zur Verfügung. Es besteht aus einem Verbund von Messageserverprozessen, die den Tupelraum der Anwendung verwalten, und der Bibliothek, die Anwen­dungen den Zugriff auf diesen Tupelraum ermöglicht.

 

System-Laufzeitsystem und Anwendungs-Laufzeitsystem gewährleisten gemein­sam die fehlertolerante Durchführung einer verteilten Anwendung, indem ei­nerseits die Fix- und Messageserverprozesse hochverfügbar implementiert sind und andererseits die auf jedem LiPS-Rechner laufenden Kontrollpro­zesse sowohl sich selbst, als auch die Anwendung überwachen.

 

Bevor im weiteren Teil dieses Kapitels der Ablauf einer mit LiPS verteilten Anwendung beschrieben wird, müssen die Komponenten des LiPS-Lauf­zeit­sys­tems vorgestellt werden. Hierzu werden zunächst die fehlertolerante Tupel­raummaschine und die damit verknüpfte generative Tupelraumkommu­nikation erläutert. Anschließend wird die Funktionsweise der Laufzeitsysteme dargestellt. 

 

Die abschließende Beschreibung des Ablaufs einer verteilten Anwendung beginnt mit einer Erläuterung, wie die Anwendung gestaltet sein muss, damit sie mit dem LiPS-System verteilt ausgeführt werden kann.

 

 


2.1      Die Komponenten des LiPS-Laufzeitsystems

 

Die im folgenden vorgestellten Komponenten des LiPS-Laufzeitsystems wer­den zur Ausführung einer verteilten Anwendung benötigt. Die zuerst erläuterte fehlertolerante Tupelraummaschine, kurz Tupelraum, ist wesentlicher Be­standteil beider Laufzeitsysteme und realisiert jeweils den globalen Speicher, der zum Austausch von Daten genutzt wird. Das System-Laufzeitsystem implementiert Mechanismen, um Prozesse auf entfernten Maschinen zu ma­nipulieren und zu überwachen. Das Anwendungs-Laufzeitsystem vermittelt dem Anwender Zugang zum Tupelraum der Anwendung und ermöglicht ihm so zusammen mit dem System-Laufzeitsystem die Durchführung einer ver­teilten Anwendung

 

2.1.1      Die fehlertolerante Tupelraummaschine

Sie ist die zentrale Komponente des LiPS Systems und dient beiden Laufzeit­systemen als globaler Speicher, in welchem Tuple verwaltet werden. Dieser Speicher arbeitet allerdings nicht adressbezogen, sondern assoziativ, was bedeutet, dass Zugriffe auf diesen Speicher über Art und Inhalt der Daten ge­steuert werden. Alle fünf verschiedenen Zugriffsarten werden im Abschnitt 2.1.1.2 erklärt.

 

Die Implementierung der fehlertoleranten Tupelraummaschine basiert auf ei­nem Verbund gleichberechtigter Messageserverprozesse, die alle eine eigene Kopie des Tupelraums verwalten. Fällt einer dieser Messageserverprozesse aus, kann die Anwendung mit den Daten eines anderen Messageserverpro­zesses weiterarbeiten, wobei LiPS alle hierzu nötigen Schritte, wie den er­neuten Verbindungsaufbau, einleitet. Aus der Sicht der Anwendung wird hier lediglich eine „System-Pause“ wahrgenommen. Zusätzlich bietet der Messa­geserver weitere Dienstleistungen, welche zur Gewährleistung der fehlertole­ranten Durchführung einer Anwendung benötigt werden. Im Kapitel 3 wird hierauf näher eingegangen.

 

Im weiteren Verlauf dieses Abschnitts werden die Zugriffseinheit des Tupel­raums, das Tuple, sowie die generative Tupelraumkommunikation vorgestellt.

 

2.1.1.1            Tuple

Nach [ACG86] ist ein Tuple eine Liste von Datenelementen beliebiger Typen, vergleichbar mit den Parametern eines Funktionsaufrufs. Es besteht aus drei Teilen, nämlich einem Namen, der allerdings nur zur Beschleunigung der Zugriffe dient, einem Typdeskriptor, der die folgenden Daten beschreibt und den Daten, die mit diesem Tuple transportiert werden. Tuple können in den Tupelraum gelegt und aus ihm gelesen bzw. entfernt werden. Im Abschnitt 2.1.1.2 wird beschrieben welche Möglichkeiten hier gegeben sind.

 

Neben der Fähigkeit Daten zu transportieren, haben Tuple eine weitere wich­tige Eigenschaft: Ihre Lebensdauer im Tupelraum ist unabhängig vom Erzeu­ger des Tupels. Hierdurch hat man die Möglichkeit einen unterbrochenen Pro­zess neu zu starten und an der Stelle weiterarbeiten zu lassen, an der das letzte mal alle hierzu benötigten Daten in den Tupelraum gelegt wurden. Es muss allerdings darauf geachtet werden, dass die Berechnung eines Ergeb­nisses wirklich nur von den Daten im Tupelraum abhängt. Sollten lokale Ge­gebenheiten oder zeitbehaftete Daten Teil des Algorithmus sein, kann diese Eigenschaft nicht mehr benutzt werden, um abgebrochene Prozesse weiter­arbeiten zu lassen.

 

Name

Der Name eines Tupels besteht aus einer beliebigen Zeichenkette ohne Leer­zeichen und kann vom Anwender frei gewählt werden. Er dient bei Suchope­rationen im Tupelraum zur Beschleunigung und kann darüber hinaus dem Entwickler helfen Daten zu klassifizieren und so die Übersichtlichkeit steigern.

 

Typdeskriptor

Der Typdeskriptor beschreibt die Daten, die mit dem Tuple transportiert wer­den. Er besteht aus einer Kette klein geschriebener Buchstaben, wobei der Buchstabe die Art des Datums beschreibt. Die Position eines Buchstaben im Typdeskriptor gibt Auskunft über die Position des entsprechenden Datums im folgenden Datenstrom. Manche Datentypen können durch voranstellen des kleinen Buchstaben ‚u’ vorzeichenlos interpretiert werden. Des weiteren ist es möglich eindimensionale Felder (Array) bestimmter Datentypen zu beschrei­ben. Hierzu wird der kleingeschriebene Buchstabe ‚a’ vor den Bezeichner des entsprechenden Datums gestellt. Außer dem Datentyp „Datei“ können Felder aus allen in Tabelle 1 dargestellten Datentypen verwendet werden.

 

Tabelle 1 gibt Auskunft über die Datentypen, ihre Bezeichner und die Möglich­keit sie vorzeichenlos oder als Feld zu verwenden.

 

Typ

Bezeichner

Array

Unsigned

int

i

P

P

string

s

P

 

long (32 Bit integer)

l

P

P

float

f

P

P

character

c

P

 

double

d

P

P

Datei

t

 

 

Tabelle 1 Datentypen der Generativen Tupelraumkommunikation

 

Daten

Alle Daten eines Tupels werden entsprechend der Reihenfolge im Typ­deskriptor hintereinander geschrieben. Hierbei ist besonders hervorzuheben, dass der Anwender die unterschiedliche Repräsentation verschiedener Da­tentypen auf verschiedenen Betriebssystemen nicht beachten muss. LiPS verwaltet alle Tuple und somit auch alle evtl. unterschiedlich repräsentierten Daten in Network byte order (Netzwerk Byte Ordnung), wobei die Daten vor bzw. nach dem Transport in die jeweils benötigte Darstellung transformiert werden.

 

Beispiele

Name

Typdeskriptor

Daten

„Punkt“

ii

0, 0

„Telefonbucheintrag“

sss

„Andreas“, „Müller“, „nicht bekannt“

„Produkt“

sd

„Hose“, 69.99

„Matrixreihe“

ai

[1, 3, 4, -1, 2]

Tabelle 2 Beispieltupel

 

2.1.1.2            Generative Tupelraumkommunikation

Tuple können mit der Operation out() in den Tupelraum gelegt werden. Es mag nun verwirrend erscheinen, dass etwas mit einer Funktion out() (dt. her­aus) irgendwo hineingelegt werden soll. Dies liegt allerdings daran, dass der Prozess, der ein Tuple in den Tupelraum transportieren möchte, es aus sich heraus legt. Die Bezeichnung der Tupelraumoperationen ist also auf den Ak­teur bezogen, und nicht etwa auf den Tupelraum. Ein Tuple, das mit out() er­zeugt wurde, existiert nun unabhängig vom erzeugenden Prozess im Tupel­raum. Es kann von jedem anderen an den selben Tupelraum angeschlosse­nem Prozess bearbeitet werden. Aus diesem Grund wird die Kommunikation generativ genannt - sie generiert Tupel.

 

Um mit out() erzeugte Tuple zu lesen, gibt es zwei unterschiedliche Operatio­nen. Mit rd() (engl. read – dt. lesen) können Tuple lediglich gelesen werden. Nach dem Lesezugriff existiert das Tuple unverändert im Tupelraum. Die Operation in() liest und extrahiert ein Tuple aus dem Tupelraum. Nach diesem Zugriff existiert das Tuple nicht mehr im Tupelraum. Des weiteren gibt es jede dieser lesenden Operationen in zwei Ausführungen. Eine blockierende, welche den lesenden Prozess solange anhält, bis das geforderte Tupel auch wirklich gelesen werden konnte und eine nicht blockierende, die entweder das geforderte Tuple oder einen Fehler liefert, falls das Tuple nicht gefunden werden konnte. Die nicht blockierenden Leseoperationen heißen inp() und rdp(), die blockierenden in() und rd(). Tabelle 3 gibt einen Überblick über die Tupelraumoperationen.

 

Operation

Semantik

out()

Legt Tupel in den Tupelraum

in()

Liest und extrahiert blockierend, liefert Tupel

rd()

Liest blockierend, liefert Tupel

inp()

Liest und extrahiert nicht blockierend, liefert Tupel oder Fehler

rdp()

Liest nicht blockierend, liefert Tupel oder Fehler

Tabelle 3 Übersicht über die Tupelraumoperationen

 

Bestandteil aller Leseanfragen ist das Template, welches das zu lesende Tuple beschreibt und ähnlich wie ein Tuple aus den drei Teilen Name, Typdeskriptor und Daten aufgebaut ist. Der Tupelraum verfügt über eine Operation, die überprüft, ob ein Tuple zu einem Template passt. Findet der Tupelraum ein passendes Tuple, so ist die Leseoperation erfolgreich und das Tuple wird zurückgeliefert. Wird kein passendes Tuple gefunden, hängt das Verhalten von der Art der lesenden Operation ab, entweder wird gewartet, bis solch ein Tuple erzeugt wird[4], oder es wird ein Fehler geliefert, der anzeigt, dass das Tuple nicht vorhanden ist. Die Auswahl eines Tupels erfolgt nicht deterministisch, falls mehrere passende Tuple vorhanden sind; genauer: Das erste passende Tuple wird zurückgeliefert.

 

Die folgenden Abschnitte erklären den Aufbau eines Template, bevor die Regeln der Template - Tupel Prüfung erläutert werden.

 

Name

Der Name eines Template besteht aus einer beliebigen Zeichenkette ohne Leerzeichen und kann vom Anwender frei gewählt werden.

 

Typdeskriptor

Auch hier dient der Typdeskriptor dazu, die folgenden Daten zu beschreiben. Er ist genauso aufgebaut, wie beim Tuple, allerdings werden hier kleine und große Buchstaben mit unterschiedlicher Bedeutung verwendet. Kleine Buchstaben bezeichnen aktuelle Parameter, die bei der Suche als Schlüssel verwendet werden. Große Buchstaben bezeichnen formale Parameter und damit die Daten, die unbekannt sind. Der Typdeskriptor, mit dem aus der in Tabelle 2 dargestellten Menge von Tupeln der Preis des Produktes Hose ermittelt werden kann, ist demnach „sD“. Das gesamte Template wäre: „Produkt“, „sD“, „Hose“; ?. (Das ‚?’ stellt hier einen Platzhalter dar, der tatsächliche Wert wird automatisch eingefügt)

 

Daten

Auch beim Template werden die Daten, wie im Typdeskriptor angegeben, hintereinander geschrieben. Beschreibt der Typdeskriptor einen aktuellen Parameter, so werden die tatsächlichen Daten geschrieben, handelt es sich um einen formalen Parameter wird ein Defaultwert als Platzhalter geschrieben. Falls die Leseanfrage erfolgreich bearbeitet werden kann, wird dieser Defaultwert von dem tatsächlichen Wert überschrieben. Da die Defaultwerte von LiPS eingefügt werden, müssen sie hier nicht dargestellt werden. Der Anwender muss lediglich Platz für die Antwort zur Verfügung stellen. Einzelheiten hierzu hängen stark von der verwendeten Programmiersprache ab. Der Umgang mit Tupeln in C wird in [Set98] erläutert, 4.1.1 beschreibt wie Java Tupel verwendet werden.

 

Regeln der Template - Tuple Prüfung

Die im folgenden aufgeführten Regeln beschreiben, wann ein Template zu einem Tuple passt:

 

  1. Name von Template und Tuple ist identisch.
  2. Die Anzahl der Einträge im Typdeskriptor ist gleich.
  3. Korrespondierende Felder beschreiben Daten des selben Typs.
  4. Korrespondierende aktuelle Parameter haben den selben Wert.

 

Nur wenn alle Regeln erfüllt sind, passen Template und Tuple zusammen.

 

Die folgenden Beispiele verdeutlichen die Anwendung dieser Regeln.

 

Template

Tuple

Passt

Name

Typdesk.

Daten

Name

Typdesk.

Daten

 

„job“

„s“

„a“

„Arbeit“

„s“

„a“

Nein – wegen 1

„job“

„s“

„a“

„job“

„ss“

„a“; „b“

Nein – wegen 2

„job“

„i“

1

„job“

„s“

„a“

Nein – wegen 3

„job“

„s“

„a“

„job“

„s“

„b“

Nein – wegen 4

„job“

„iSS“

1; ?; ?

„job“

„iss“

1; „a“; „b“

Ja

„sem“

 

 

„sem“

 

 

Ja

Tabelle 4 Beispiele Template - Tuple

 

2.1.2      Das Anwendungs-Laufzeitsystem

Die Bibliothek mit den fünf im vorigen Abschnitt erläuterten Tupelraumoperationen bildet zusammen mit den Messageservern, die den Tupelraum der Anwendung verwalten, das Anwendungs-Laufzeitsystem.

 

Dies allein genügt aber noch nicht, um eine verteilte Anwendung ablaufen zu lassen. Es fehlt noch ein Mechanismus mit dem, wie in der Einleitung erwähnt, Aufträge erteilt werden können. Hierzu wird die ohnehin benötigte Bibliothek mit den Tupelraumoperationen um eine weitere Funktion namens eval() erweitert. Eine ausführliche Erläuterung dieses Themas folgt im Kapitel 3. Momentan genügt die Darstellung, dass anstelle eines lokalen Funktionsaufrufs die selbe Funktion auf einem entfernten Rechner ausgeführt wird, wenn sie mit eval() aufgerufen wurde. Folgendes Beispiel verdeutlicht den Unterschied zwischen einem lokalen Funktionsaufruf und der Erteilung eines Auftrags an das Anwendungs-Laufzeitsystem. Der hier dargestellte ungültige C Code wird vom LiPS Präprozessor vor der Übersetzung der Anwendung umgewandelt.

 


Lokaler Funktionsaufruf:

...

foo(17);

...

 

Beispiel 1 Funktionsaufruf mit eval()


Funktionsaufruf mit eval:

...

eval foo(17); /* Dies ist kein gültiges C */

...


Die Messageserver kontrollieren aber auch die Prozesse der verteilten Anwendung und leiten bei Ausfall eines Prozesses dessen Neustart ein. Hierzu wird allerdings das System-Laufzeitsystem benötigt, da das Anwendungs-Laufzeitsystem nicht über Methoden verfügt, mit denen Prozesse auf entfernten Maschinen gestartet werden können.

 

2.1.3      Das System-Laufzeitsystem

Die Kontrollprozesse lipsd bilden zusammen mit den Messageservern, die den Tupelraum des Systems verwalten das System-Laufzeitsystem. Bei den hier erwähnten Messageservern handelt es sich um Instanzen des selben Programms, wie im Anwendungs-Laufzeitsystem. Um die Zugehörigkeit eines Messageserverprozesses zu einem Laufzeitsystem besser unterscheiden zu können, werden die Messageserver des Systems Fixserver genannt.

 

Der lipsd hat hierbei zwei Aufgaben zu bewältigen. Zum Einen muss er gewährleisten, dass auf allen beteiligten Rechnern ein lipsd läuft. Hierzu legt er regelmäßig Statusmeldungen in den Systemtupelraum und überwacht gleichzeitig Statusmeldungen anderer lipsd’s, um nicht mehr arbeitende lipsd’s zu erkennen und entsprechend darauf zu reagieren. Zum Anderen muss er die Anwendungsprozesse starten, überwachen und beenden. Hierbei wird er von den Messageservern unterstützt, indem einerseits Verbindungsabbrüche gemeldet werden, sodass der lipsd lediglich den Neustart eines lipsd auf einem entfernten Rechner einzuleiten hat und andererseits konkrete Anweisungen erteilt werden, die dann beispielsweise den Start eines Anwenderprozesses initiieren.

 

Weitere Einzelheiten den lipsd betreffend werden im Kapitel 3 beschrieben.

 

2.2      Ablauf einer verteilten Anwendung mit LiPS

 

Dieser Abschnitt beschreibt wie eine verteilte Anwendung mit LiPS ausgeführt wird. Es wird dabei davon ausgegangen, dass bereits beide Laufzeitsysteme etabliert sind und dass die Anwendung in ausführbarer Form für alle beteiligten Plattformen vorhanden ist.

 

Damit die Anwendung mit LiPS verteilt ausgeführt werden kann, muss sie aber speziellen Designrichtlinien folgen. Daher wird zunächst beschrieben, wie eine Anwendung aufgebaut sein muss, bevor ihr Ablauf geschildert wird.

 

2.2.1      Design einer LiPS Anwendung

Eine LiPS Anwendung besteht aus einem Masterprozess und vielen Clientprozessen. Zur Implementierung werden die Programmiersprache C und die LiPS Bibliothek, welche Tupelraumoperationen zur Verfügung stellt, verwendet. Der gesamte Quelltext muss sich in einer Datei befinden und muss eine main(char** argc, int argv) Methode definieren. Diese stellt zum Einen den Anfang des Masterprozesses und damit den Start der verteilten Anwendung dar und ermöglicht es zum Anderen, ihn wie ein normales Programm zu starten.

 

Bei den Clientprozessen ist es etwas aufwendiger. Der LiPS-Präprozessor erzeugt aus dem Quelltext der Anwendung ein neues Programm, welches alle Funktionen enthält, die mit eval() aufgerufen werden. Damit der Präprozessor dies leisten kann, müssen diese Funktionen von speziellen Symbolen, START_EVAL_FUNCTION und END_EVAL_FUNCTION, vor bzw. nach dem Quelltext der Funktion geklammert sein. Näheres hierzu kann in [Set98] gefunden werden.

 

2.2.2      Ablauf

Die nun folgende Schilderung des Ablaufs einer verteilten Anwendung geht von keiner konkreten Zahl beteiligter Rechner oder Betriebssysteme aus. Es werden vielmehr die stattfindenden Aktionen in der richtigen Reihenfolge und mit Zuordnung des jeweiligen Akteurs geschildert. Hierbei wird manchmal von „seinem lipsd“ gesprochen; gemeint ist dann der lipsd, der auf dem selben Rechner abläuft, wie „er“, wobei „er“ wiederum ein Prozess ist.

 

Zuerst startet der Anwender den Masterprozess der verteilten Anwendung. Hierbei wird bereits angegeben, wie viele Clientprozesse gestartet werden sollen und für welche Betriebssysteme ausführbare Dateien vorhanden sind.

 

Der Master meldet als erstes die verteilte Anwendung bei seinem lipsd an und teilt dem System so mit welche Betriebssysteme unterstützt werden.

 

Anschließend veranlasst der Master, dass die ausführbaren Dateien der verteilten Anwendung von seinem lipsd in das LiPS-Repository kopiert werden. Das Repository dient als zentrale Dateiverwaltungsstelle. Hier werden alle verwendeten Dateien aufbewahrt und verteilt. Die Funktionsweise wird in 3.2.3.5 genauer dargestellt.

 

Nun baut der Master eine Verbindung zu einem der Messageserver, die den Tupelraum der Anwendung realisieren auf und erteilt den Auftrag, die gewünschte Anzahl an Clientprozessen zu starten. Der Messageserver leitet diesen Auftrag an seinen lipsd weiter, welcher einen geeigneten Rechner auswählt und dem dort laufenden lipsd den Befehl erteilt, den Clientprozess zu starten.

 

Dieser lipsd kopiert nun die für seine Architektur benötigte ausführbare Datei aus dem Repository und legt sie im lokalen Dateisystem ab. Anschließend startet er den Clientprozess und überwacht seinen Ablauf.

 

Nachdem diese Schritte abgelaufen sind, ist der Start der verteilten Anwendung abgeschlossen. Wichtig ist zu erwähnen, dass alle hier aufgezählten Schritte ohne direkte Anweisung des Entwicklers der verteilten Anwendung ablaufen. Auch wenn hier des öfteren der Master als Akteur dargestellt wurde, so handelt es sich in Wirklichkeit um Funktionalitäten, die von der verwendeten Bibliothek erbracht werden. Der Entwickler stößt diese Aktionen durch Verwendung der Bibliothek lediglich an.

 

Der weitere Ablauf wird von den Anweisungen des Entwicklers der verteilten Anwendung bestimmt. Üblicherweise zerlegt der Master das Problem nun in Teilprobleme, welche mit eval() als Auftrag erteilt und von den Clientprozessen abgearbeitet werden. Eine Anwendung, die nicht verteilt ablaufen soll, würde statt der Erteilung des Auftrags mit eval() die entsprechende Funktion direkt aufrufen. Nachdem alle Aufträge erteilt wurden, kann der Master mit dem Sammeln der Ergebnisse beginnen. Sind alle Ergebnisse eingesammelt, wird die verteilte Anwendung beendet.

 

2.3      Zusammenfassung

 

In diesem Kapitel wurden die Komponenten des LiPS-Laufzeitsystems vorgestellt. Der Tupelraum ist ein globaler assoziativer Speicher für Tupel und stellt die zentrale Komponente beider Laufzeitsysteme dar. Er wird durch einen Verbund von Messageserverprozessen realisiert. Aufbau eines Tupels und Ablauf der Kommunikation mit dem Tupelraum wurden im ersten Teil dieses Kapitels geschildert. Außerdem wurde erläutert, dass das Anwendungs-Laufzeitsystem neben Zugriff auf den Tupelraum der Anwendung eine Möglichkeit zur Auftragserteilung benötigt, welche durch den eval() Mechanismus und den LiPS Präprozessor gegeben ist. Die Aufgaben des Kontrollprozesses lipsd, welcher Bestandteil des System-Laufzeit­sys­tems ist, bestehen einerseits aus der Überwachung und Sicherstellung der eigenen Verfügbarkeit und andererseits aus dem Angebot Prozesse für das Anwendungs-Laufzeitsystem kontrolliert ablaufen zu lassen. Den Abschluss dieses Kapitels bildet die Beschreibung des Ablaufs einer verteilten Anwendung. Nachdem ihr Aufbau einführend erläutert wurde, wurden alle während des Ablaufs stattfindenden Aktionen beschrieben.

 


3         Analyse

 

C ist eine Programmiersprache, welche den imperativen Programmierstil unterstützt, wohingegen Java einen objektorientierten Programmierstil fordert. [Brü01]

 

Während im imperativen Programmierstil Funktionen dazu verwendet werden Daten zu manipulieren, erweitert der objektorientierte Ansatz den imperativen, indem Daten und Funktionen zu deren Modifikation in einer Einheit, der Klasse, zusammengefasst werden. Hierbei kann der Zugriff auf Daten, die Attribute der Klasse, so geschützt werden, dass er nur über Methoden der Klasse möglich ist - man nennt dies Kapselung. Objekte sind Instanzen einer Klasse und haben den durch die Klasse festgelegten Typ.

 

Klassen können außerdem voneinander erben, was bedeutet, dass die erbende Klasse das Verhalten des Vorfahren erhält, zusätzlich aber in der Lage ist weitere Dienste anzubieten oder das geerbte Verhalten zu verändern, man spricht hier auch von Spezialisierung. Da Klassen Datentypen definieren, wird durch die Vererbung eine Hierarchie zwischen diesen Datentypen aufgebaut, in welcher Erben unter ihren Vorfahren angeordnet sind. Durch das Konzept des dynamischen Bindens wird erreicht, dass Objekte in Variablen gespeichert werden können, die einen übergeordneten Typ haben, sich aber trotzdem wie das Spezialisierte verhalten. Beispielsweise könnte es eine Klasse Katze geben, die über die Methode gibLaut() verfügt und bei Aufruf „Miau!“ auf dem Bildschirm ausgibt. Da ein Löwe eine spezielle Katze ist, könnte die Klasse Löwe von Katze erben und so alle Eigenschaften einer Katze erhalten. Die Methode gibLaut() würde allerdings mit einer neuen Implementierung, welche „Brüll !!!“ auf dem Bildschirm ausgibt, überladen werden. Wenn nun ein Objekt vom Typ Löwe in einer Variablen vom Typ Katze gespeichert ist und die Methode gibLaut() aufgerufen wird, wird zur Laufzeit des Programms festgestellt, dass es sich in Wirklichkeit um einen Löwen handelt und „Brüll !!!“ wird auf dem Bildschirm ausgegeben.

 

Diese Erweiterungen des imperativen Programmierstils erlauben es die Struktur des Programms an der Struktur des Problems auszurichten, wohingegen imperative Programme häufig an der Struktur des Computers ausgerichtet sind. Bei der Portierung eines imperativen Programms in ein objektorientiertes geht es also nicht nur darum die Anweisungen, aus denen das Programm besteht, zu übersetzen, sondern vielmehr zu analysieren, wie das Verhalten des Programms durch Modellierung von Klassen und Beziehungen zwischen ihnen abgebildet werden kann.

 

Nachdem das Verhalten in ein Modell von Klassen überführt wurde muss es implementiert werden, wozu der Quellcode des Originals als Vorlage dienen kann. Allerdings können nur wenige Konstrukte direkt übersetzt werden, da in C verschiedene Konzepte, wie beispielsweise Pointer, existieren, die in Java nur durch den Einsatz von Objekten umzusetzen sind. Aus diesem Grund wird zunächst dargestellt wie einzelne Anweisungen eines C Programms in semantisch äquivalente Java-Konstrukte umgewandelt werden können.

 

Anschließend werden die Komponenten des lipsd analysiert, indem zunächst untersucht wird, welche technischen Voraussetzungen erfüllt sein müssen, um den Kontrollprozess lipsd ablaufen zu lassen. Im folgenden Abschnitt wird die Kommunikation mit dem Tupelraum analysiert, indem Verbindungsaufbau und Format der Tupelraumnachricht dargestellt werden. Einen weiteren Schwerpunkt dieses Kapitels stellt die Analyse des System-Laufzeitsystems dar. Zunächst wird beschrieben, wie es sich selbst überwacht und somit gewährleistet, dass es permanent zur Verfügung steht. Anschließend wird dargestellt, wie Befehlsnachrichten zur Steuerung des Ablaufs einer verteilten Anwendung eingesetzt werden. Abschließend wird das Repository zur zentralen Dateiverwaltung vorgestellt. Den Abschluss dieses Kapitels bildet die Analyse des Anwendungs-Laufzeitsystems, indem der Mechanismus der Auftragserteilung und -abarbeitung analysiert wird.

 


3.1      C vs. Java

 

Ziel dieses Abschnitts ist es aufzuzeigen, dass jedes in C mögliche Konstrukt in ein semantisch identisches Konstrukt in Java überführt werden kann.

 

Einzige Ausnahme ist der in C mögliche Befehl chdir(newWorkingDir), welcher bewirkt, dass sich das Arbeitsverzeichnis der Anwendung ändert. Dateien, die relativ adressiert sind, werden in diesem Arbeitsverzeichnis gesucht oder angelegt. Soll sich dies zur Laufzeit des Programms ändern, so darf die entsprechende Java-Implementierung nur absolut adressierte Dateien ansprechen, wobei die absolute Adressierung erreicht wird, indem der Dateiname aus einem Arbeitsverzeichnis und einem relativ adressierten Namen gebildet wird. Anstelle des chdir-Befehls muss dann lediglich die Variable, die den Namen des Arbeitsverzeichnis speichert, verändert werden.

 

3.1.1      Datentypen

Programme verwalten Daten in Variablen. Damit der Compiler weis, wie er diese Variablen, bzw. die Daten in ihnen behandeln soll, hat jede Variable einen Typ. Hierbei wird zwischen eingebauten Basisdatentypen und zusammengesetzten Datentypen unterschieden. Eingebaute Basisdatentypen kennt der Compiler bereits, sie sind „in ihn eingebaut“ und er weis wie er sie zu verwalten hat. Zusammengesetzte Datentypen werden vom Programmierer entwickelt und sind dem Compiler unbekannt. Durch den Quelltext erhält er Informationen, wie diese Typen zu behandeln sind. Weitere besonders zu erwähnende Typen sind Union, Boolean und String. Sie werden in den Abschnitten 3.1.1.3 bis 3.1.1.5 erläutert, die Abschnitte 3.1.1.1 und 3.1.1.2 stellen eingebaute Basisdatentypen beider Sprachen gegenüber und erklären wie zusammengesetzte Datentypen portiert werden können.

 

3.1.1.1            Eingebaute Basisdatentypen

In der Standard C Spezifikation wird nicht vorgeschrieben, wie viele Bits zur Speicherung eines Basisdatentypen verwendet werden müssen [Eck00]. Statt dessen definieren die beiden Dateien limits.h und float.h Minimal- und Maximalwerte für die verschiedene Datentypen, woraus für Computer, die auf dem binären System aufbauen, die Anzahl der minimal benötigten Bits abgeleitet wird. Die tatsächlich verwendete Anzahl ist ebenso wie das Format, plattformabhängig und somit unbestimmt. In Java sind sowohl Größe als auch Format fest definiert, was bedeutet, dass gleiche Basisdatentypen auf allen Plattformen gleich behandelt werden.

 

Des weiteren gibt es in C vier Spezifikationsbezeichner (engl. specifier), welche den Wertebereich des Basisdatentyps genauer festlegen: long, short, signed und unsigned[5]. Long und short wirken sich auf die definierten Minimal- und Maximalwerte aus, wohingegen signed und unsigned dem Compiler Informationen über die Verwendung des ersten Bits geben. Falls das erste Bit nicht für das Vorzeichen verwendet wird, steht es zur Darstellung der Zahl zur Verfügung und kann somit zur Speicherung doppelt so großer positiver Zahlen mitverwendet werden. Flieskommazahlen bilden eine Ausnahme: Sie sind immer vorzeichenbehaftet und niemals short. Zusätzlich ist die Kombination aus long und float nicht erlaubt. In Java sind alle eingebauten Basisdatentypen vorzeichenbehaftet und es gibt keine den Wertebereich verändernden Spezifikationsbezeichner.

 

Für die Portierung gilt, dass der Wertebereich des in Java verwendeten Datentyps den des Originaldatentyps enthalten muss. Vor allem bei vorzeichenlos interpretierten Datentypen muss hierauf geachtet werden.

 

Beispielsweise kann der Wert 2^15+1 sowohl in C, als auch in Java syntaktisch korrekt in einer Variablen vom Typ short gespeichert werden. Der vorzeichenlos interpretierte Wert in C unterscheidet sich allerdings von dem negativen Wert in Java - in Java müsste also int verwendet werden, um die selbe Semantik zu erreichen.

 

Tabelle 5 stellt alle möglichen Kombinationen aus Spezifikationsbezeichner und Basisdatentyp dem entsprechenden Java Basisdatentyp gegenüber. Eckige Klammern zeigen hier an, dass die Angabe dieses Schlüsselwortes optional ist. Da die C Standard Spezifikation sich lediglich auf Mindestangaben beschränkt, kann hier auch nur der minimal zu verwendende Javatyp angegeben werden. Im Zweifelsfall müssen sowohl die Dateien limits.h und float.h, als auch die Verwendung der so deklarierten Variablen untersucht werden.

 

C

Java

Typ

Beschreibung

Typ

Beschreibung

signed/unsigned[6] char

>= 8 bit

byte

8 Bit

unsigned short [int]

>= 16 bit

int

32 Bit

unsigned int

>= 16 bit

long

64 Bit

unsigned long [int]

>= 16 bit

BigInteger[7]

unterschiedlich

[signed] short [int]

>= 16 bit

short

16 Bit

[signed] int

>= 16 bit

int

32 Bit

[signed] long [int]

>= 16 bit

long

64 Bit

float

32 Bit - IEEE[8]

float

32 Bit - IEEE 754

double

64 Bit - IEEE[9]

double

64 Bit - IEEE 754

long double

>= double

BigDezimal[10]

unterschiedlich

Tabelle 5 Gegenüberstellung der Basisdatentypen

 

3.1.1.2            Zusammengesetzte Datentypen

Im Gegensatz zu den Basisdatentypen werden zusammengesetzte Datentypen nicht durch die Spezifikation der Sprache definiert und vom dazugehörenden Compiler erkannt. Sie werden vielmehr vom Programmierer entwickelt und im Quelltext beschrieben. Wenn auch beide Programmiersprachen über Mittel verfügen zusammengesetzte Datentypen zu definieren, so unterscheiden sich diese Mittel doch sehr. In C können sogenannte Strukturen (engl. struct) definiert werden. Eine Struktur hat einen Namen und fasst verschiedene Variablen verschiedenen Typs zu einem Datensatz zusammen. Ein Beispiel wäre die Struktur Punkt_2D, welche zwei Zahlenvariablen zur Speicherung der Koordinaten zusammenfasst. Mit dieser Struktur ist es möglich Variablen vom Typ „struct Punkt“ zu deklarieren und so zwei Werte in einer Variablen zu speichern. Durch ein weiteres Element der Sprache C, nämlich der Typdefinition (engl. typedef), kann diese Struktur als echter Typ definiert werden. Anstelle von Variablen vom Typ „struct Punkt“ können dann Variablen vom Typ „Punkt“ verwendet werden. Im wesentlichen stellt dies eine Erleichterung für den Programmierer dar. Beispiel 2 stellt Definition und Verwendung der Struktur Punkt dar, Beispiel 3 zeigt das selbe für die Typdefinition Punkt.

 


Beispiel: Struktur Punkt:
struct Punkt
{

int x;

int Y;
};

Beispiel 2 Die Struktur Punkt


Verwendung der Struktur Punkt:
struct Punkt p;

p.x = 12;

p.y = 7;



 


Beispiel Typdefinition Punkt
typedef struct
{

int x;

int y;
}
Punkt;

Beispiel 3 Die Typdefinition Punkt

Verwendung des Typs Punkt
Punkt p;

p.x = 12;
p.y = 7;




 

In Java wird ein zusammengesetzter Datentyp in einer Klasse definiert. Eine Klasse besteht aus Attributen und Methoden und hat den Zweck, Aufbau (Attribute) und Verhalten (Methoden) eines neuen Datentyps zu beschreiben. Durch eine spezielle Methode, den Konstruktor, kann man Objekte einer Klasse erzeugen und gleichzeitig initialisieren. Ein Objekt ist dann eine gewisse Menge belegter Speicher, auf den nur durch die Methoden des Objekts zugegriffen werden kann. Der Compiler verhindert, dass auf uninitialisierte Objekte zugegriffen wird. Er verlangt, dass ein Objekt entweder durch einen Konstruktoraufruf erzeugt wurde oder ihm explizit der Wert null zugewiesen wurde. Der Speicher wird von Java verwaltet, d. h. der Programmierer muss selbst keinen Speicher für das Objekt allokieren oder freigeben.

 

Die Portierung von Strukturen und Typdefinitionen zu Klassen kann durch einfaches Abbilden der Elemente der Struktur auf Attribute der Klasse erfolgen. Beispiel 4 stellt die Klasse Punkt dar.

 


Beispiel: Klasse Punkt

public class Punkt
{

public int x;

public int y;

 

/* Konstruktor */

public Punkt(int paX, int paY)

{
         x = paX;
         y = paY;

}
}

Beispiel 4 Die Klasse Punkt


Verwendung der Klasse Punkt
Punkt p=new Punkt(12, 7);

int loX = p.x;

p.y=17;

 

 

 

 

 

 

 

 

 



Die hier dargestellte Klasse Punkt ist allerdings noch keine gute Klasse im objektorientierten Sinne, da sie den Zugriff auf ihre Attribute nicht kapselt. Besser wäre jedes Attribut vor Zugriff von außen zu schützen, indem es privat (engl. private) deklariert wird und die Klasse um zwei weitere Methoden pro Attribut zu erweitern, welche den lesenden beziehungsweise schreibenden Zugriff implementieren. Üblicherweise werden diese Methoden getAttributname() und setAttributname(wert) genannt.

 

Nun hat sich auch die Verwendung eines Objekts vom Typ Punkt verändert: Statt direkt auf die Attribute zuzugreifen, müssen jetzt die neuen Methoden aufgerufen werden. Beispiel 5 und Beispiel 6 stellen eine gute Portierung der Struktur Punkt und ihre Verwendung dar.

 

 

Beispiel: Klasse Punkt

public class Punkt

{

private int x;

private int y;

 

/* Konstruktor */

public Punkt(int paX, int paY)

{
         x = paX;
         y = paY;

}

public int getX()

{

return x;

}

public int getY()

{

return y;

}

 

public void setX(int paX)

{

x = paX;

)

public void setY(int paY)

{

y = paY;

)

}

Beispiel 5 Gute Portierung der Struktur Punkt

 

Verwendung der Klasse Punkt

Punkt p = new Punkt(12, 7);

int loX = p.getX();

p.setY(17);

Beispiel 6 Verwendung der Klasse Punkt

 

3.1.1.3            Union

Mit dem Union Konstrukt stellt C eine weitere Möglichkeit zur Verfügung einen eigenen Typ zu definieren. Eine Union dient dazu eine Variable zur Speicherung verschiedener Datentypen zu benutzen. Hierbei wird Speicherplatz der Größe des größten Typs reserviert und je nach Verwendung der Union interpretiert.

 

In Java kann dies nur durch eine Klasse, die verschiedene Zugriffsmethoden anbietet realisiert werden. Der Datentyp, der den meisten Speicherplatz benötigt, sollte als Attribut zur Speicherung des Werts dienen und verschiedene Methoden können die dort gespeicherten Daten unterschiedlich interpretieren.

 

3.1.1.4            Boolean

Boolean ist ein Datentyp, den es in C nicht gibt, der aber durch die Verwendung eines Integerdatentyps simuliert wird. Mit ihm werden die beiden Werte wahr (engl. true) und falsch (engl. false) kodiert. Der Wert 0 wird als false interpretiert, jeder von 0 verschiedene Wert als true.

 

3.1.1.5            String

Der Datentyp String beschreibt Zeichenfolgen. In C werden Strings durch einen Pointer auf ein Bytearray (Typ char) realisiert, wobei das Ende des Strings durch ein Byte mit dem Wert 0 gekennzeichnet ist. In Java werden Strings durch Objekte der Klasse String realisiert. Dies erfordert keine spezielle Kennzeichnung des Endes der Zeichenkette.

 

Bei der Portierung von Anweisungen, die Strings verwenden, muss beachtet werden, dass in Java kein Nullbyte das Ende der Zeichenkette kennzeichnet. Bei Bedarf, beispielsweise bei Versand eines Strings über das Netzwerk, muss also manuell ein Nullbyte angehängt werden, wenn der Empfänger einen String im C-Format erwartet.

 

3.1.2      Funktionen / Methoden

Funktionen bezeichnen abgeschlossene Programmteile einer imperativen Programmiersprache wie C, die dazu dienen, häufig benötigte Anweisungsgruppen zusammenzufassen. Durch den Aufruf einer Funktion, wobei auch Argumente übergeben werden können, werden diese Anweisungen dann ausgeführt. Innerhalb der Funktion kann auf die Argumente, auf globale Variablen und auf andere Funktionen zugegriffen werden. Dies kann dazu verwendet werden ein Ergebnis zu berechnen und zurückzuliefern, es ist aber auch möglich globale Variablen und, wie später gezeigt wird, sogar die Argumente der Funktion zu verändern. Hierdurch kann der Zustand des Programms beeinflusst werden.

 

In objektorientierten Programmiersprachen spricht man von Methoden. Der wesentliche Unterschied zu Funktionen ist, dass Methoden nicht global verfügbar, sondern an ein Objekt gebunden sind. Sie können lediglich mit den Daten dieses Objekts und den Argumenten der Methode arbeiten.

 

Beim Portieren von Funktionen muss daher untersucht werden, in welcher Klasse sie implementiert werden sollten. Häufig erhält eine Funktion als Parameter eine Struktur, mit deren Daten sie dann arbeitet. Die Klasse, die diese Struktur darstellt, sollte dann auch die entsprechende Methode anbieten. Folgendes Beispiel stellt die Portierung der Funktion void move(Punkt* p, int x, int y) vor, welche den übergebenen Punkt um die übergebenen Richtungsanteile verschieben soll.

 

 


C:

void move(Punkt* p , int x, int y)
{

*p.x = *p.x + x;

*p.y = *p.y + y;

}


Beispiel 7 Portierung der Funktion move

 


Java:
public class Punkt
{

void move(int paX, int paY)

{

  x = x + paX;
     y = y + paY;

}
}


Funktionen, die nicht mit den Daten einer Struktur arbeiten, können meist aufgrund der Datei, in welcher sie gespeichert sind, einem Objekt zugeordnet werden. In C werden häufig verschiedene Funktionen in einer Datei zu einem sogenannten Modul zusammengefasst. Ähnlich wie bei einer Klasse bildet die Menge der in einem Modul zusammengefassten Funktionen oft eine logische Einheit. Es bietet sich also an C-Module auf Java-Klassen abzubilden, indem die Funktionen eines Moduls zu Methoden einer Klasse werden.

 

3.1.3      Static

Das Schlüsselwort static hat in C zwei Bedeutungen:

 

  1. Innerhalb einer Funktion kann eine Variable static deklariert werden, wodurch erreicht wird, dass die Variable nur beim ersten Aufruf der Funktion initialisiert wird. Bei jedem weiteren Aufruf wird dieselbe Variable wiederverwendet und man kann auf den Wert zugreifen, den sie bei einem früheren Funktionsaufruf erhalten hat. Beispielsweise könnte so ein Zähler implementiert werden, der bei jedem Funktionsaufruf eine lokale statische Variable inkrementiert. In Java ist dies so nicht möglich, jedoch könnte die Klasse, welche die entsprechende Methode implementiert, um ein Attribut erweitert werden, welches der Methode als statische Variable dienen kann.
  2. Außerhalb einer Funktion können Variablen und Funktionen static deklariert werden, wodurch ihre Verwendung auf das Modul beschränkt wird. In Java gibt es die Möglichkeit Methoden und Attribute private zu deklarieren, wodurch sie außerhalb der Klasse nicht verwendet werden können. Aufgrund von Vererbung kann es allerdings nötig sein die Zugriffsbeschränkung zu erweitern und die entsprechende Methode / Variable protected zu deklarieren, wodurch auch erbende Klassen Zugriff auf die Methode erhalten.

 

In Java gibt es ebenfalls das Schlüsselwort static und es dient auch dazu Methoden und Variablen zu spezifizieren, jedoch hat es eine andere Bedeutung. Wie bereits erwähnt arbeiten Methoden mit den Daten eines Objekts. Statische Methoden hingegen sind nicht an ein Objekt gebunden, sondern an eine Klasse. Sie werden oft verwendet um Dienstleistungen zu implementieren, die entweder von allen Objekten der Klasse benötigt werden oder als Werkzeug zum Umgang mit Objekten der Klasse dienen. Ein Beispiel für solch eine „Werkzeugmethode“ wäre eine Methode, die aus einer Menge von Punkten diejenigen mit einer besonderen Eigenschaft heraussucht. Es wäre nicht sinnvoll, diese Methode an ein Objekt vom Typ Punkt zu binden, da die Daten dieses einen Punktes nur wenig zur Lösung des Problems beitragen.

 

3.1.4      Pointer

Zeiger (engl. Pointer) sind ein sehr maschinennahes Element der Sprache C, welches benutzt wird, um „auf Speicheradressen zu zeigen“. Der Speicherplatz für einen Pointer richtet sich nach der Architektur des Computers. Es werden genau so viele Bytes zur Speicherung eines Pointers benötigt, wie zur Darstellung einer Speicheradresse benötigt werden, zur Zeit üblicherweise 32 bit. Da die Übergabe der Argumente an Funktionen durch Kopieren der Argumente realisiert wird, ist es ohne Pointer nicht möglich effiziente Algorithmen zu implementieren. Mit Pointern allerdings kann die Übergabe einer großen Datenmenge an eine Funktion erfolgen, indem der Funktion anstelle der Daten ein Pointer auf die Daten übergeben wird[11]. Die Funktion kann dann mit den Daten des Aufrufenden arbeiten, statt mit einer Kopie, die nach Beendigung der Funktion wieder zurückkopiert werden müsste.

 

Es genügt allerdings noch nicht, wenn ein Pointer eine Speicheradresse speichern kann. Eine Typangabe wird auch benötigt, um den Speicherbereich, auf den ein Pointer zeigt, richtig interpretieren zu können. Aus diesem Grund ist der Pointer kein neuer Datentyp, sondern ein weiterer Spezifikationsbezeichner, welcher allerdings orthogonal zu den übrigen angeordnet ist. Die Menge der möglichen Basisdatentypen verdoppelt sich also. Des weiteren können auch Pointer auf Strukturen oder selbst definierte Typen sowie Funktionen deklariert werden.

 

Mit Hilfe von Pointern ist es nun möglich einer Funktion Argumente zu übergeben, die von dieser Funktion verändert werden. Dies kann in Java identisch abgebildet werden, indem statt eines Pointers vom Typ X ein Objekt der Klasse X übergeben wird. Falls der Pointer auf einen Basisdatentypen verweist, muss allerdings eine Klasse zur Kapselung des Basisdatentypen geschrieben werden, da Basisdatentypen in Java immer als Kopie übergeben[12] werden. Die Semantik ist die selbe, doch statt eines Basisdatentyps muss ein Objekt eingesetzt werden. In Beispiel 7 wurde bereits ein Pointer zur Manipulation eines Punktes verwendet.

 

3.1.5      Funktionspointer

In C können nicht nur Daten über einen Pointer referenziert werden, sondern auch Funktionen, wodurch es möglich ist einen Funktionsaufruf zu implementieren, ohne zur Kompilierzeit zu wissen, welche Funktion zur Laufzeit aufgerufen wird.

 

Mit diesem Werkzeug kann beispielsweise eine Sortierfunktion für Daten eines beliebigen aber festen Typs entwickelt werden, indem die Sortierfunktion einen Pointer auf eine Vergleichsfunktion erhält. Solange diese Funktion zwei Elemente des zu sortierenden Typs vergleichen kann, kann die Sortierfunktion ihre Aufgabe erfüllen.

 

In Java gibt es keine Funktionspointer, jedoch ist es möglich, das Konzept zu portieren, indem eine Schnittstelle (engl. Interface) entwickelt wird, die eine zu implementierende Funktion deklariert, aber nicht implementiert. Eine Schnittstelle ist keine Klasse, somit kann weder ein Objekt dieser Schnittstelle erzeugt werden, noch kann von ihr geerbt werden. Allerdings kann sie von einer Klasse implementiert werden, was bedeutet, dass die in der Schnittstelle deklarierten Funktionen von der Klasse bereitgestellt werden müssen. Das Interface schreibt vor WAS möglich sein soll, die Implementierende Klasse verfügt über das Wissen WIE es geht. Objekte vom Typ der Klasse können dann auch als Objekte vom Typ der Schnittstelle behandelt werden. Der Mechanismus ist dem des Erbens sehr ähnlich, er verhindert aber Probleme, die sich aus der in C++ erlaubten Mehrfachvererbung ergeben. Falls verschiedene der Klassen, die beerbt werden, eine Methode mit gleicher Signatur anbieten, muss entschieden werden, welche Implementierung verwendet wird. Dies ist in Java nicht nötig, da Schnittstellen lediglich die Signatur einer zu implementierenden Methode vorgeben, selbst aber über keine Implementierung verfügen dürfen.

 

Das Beispiel des Sortieralgorithmus kann in Java realisiert werden, indem eine Schnittstelle entwickelt wird, welche die Signatur der Vergleichsmethode vorgibt, nennen wir sie Comparator. Sollen nun Objekte eines speziellen Typs sortiert werden, muss der Sortieralgorithmus ein Objekt vom Typ Comparator erhalten, es muss also eine implementierende Klasse entwickelt werden. Diese Forderung kann nun auf verschieden Arten erfüllt werden. Zum einen könnte die zu sortierende Klasse selbst die Schnittstelle implementieren, was den Vorteil bietet, dass Daten und Funktionalität zentral verwaltet würden. Falls es allerdings verschiedene Möglichkeiten geben soll eine Schnittstelle zu implementieren, beispielsweise Vergleiche nach unterschiedlichen Kriterien, könnten verschiedene spezielle Klassen besser geeignet sein, weil dann je nach Bedarf die geeignete Implementierung gewählt werden kann.

 

3.1.6      Arrays

Ein Array wird benutzt um eine Menge von Elementen eines Typs mit einer Variablen zu verwalten. Der Zugriff auf Elemente eines Arrays erfolgt üblicherweise über einen Index in der Form Variablenname[index] und kann so in beiden Sprachen verwendet werden. Index ist hierbei vom Typ int. Es ist ebenfalls in beiden Sprachen möglich mehrdimensionale Arrays zu verwenden.

 

In C kann aber nicht nur über einen Index auf Arrayelemente zugegriffen werden, sondern auch mit einem Pointer. Da Arrays statisch verwaltet werden, also einmal ausreichend Speicher für alle Elemente des Arrays reserviert wird, liegen sie linear im Speicher. Hierdurch ist es möglich ausgehend von der Adresse eines beliebigen Elements auch die Adresse anderer Elemente zu  errechnen. Da der Compiler die Größe des referenzierten Elements kennt, ist es nicht nötig explizit anzugeben, um wie viele Bytes die Adresse verändert werden soll. Statt dessen wird in Einheiten des Datentyps gerechnet, d.h. der Wert eins steht für die Menge an Bytes, die zur Speicherung von einem Element dieses Typs benötigt werden. Wird ein Pointer auf ein Arrayelement also um eins erhöht, zeigt er auf das nächste Element. Diese Form des Rechnens mit Pointern wird Pointerarithmetik genannt und kann nach Java portiert werden, indem mit Arrayindizes gerechnet wird. Da der Wert eins in beiden Fällen für ein Element steht, kann die Berechnung selbst identisch portiert werden.

 

Folgendes Beispiel zeigt, wie eine einfache Pointerarithmetik nach Java portiert wird.

 


C:
int a[100];
int* arrayAnfang = a;
int* pointerAufElementDrei =

arrayAnfang + 3;
int element_drei =

*pointerAufElementDrei;

Beispiel 8 Portierung einfacher Pointerarithmetik


Java:
int a[100];

int arrayAnfang = 0;
int pointerAufElementDrei = arrayAnfang + 3;
int element_drei = a[pointerAufElementDrei];


 

3.1.7      Operatoren

Ein Ausdruck (engl. expression) wird aus einem Wert, einem Wert und einem Operator oder einem Wert, einem Operator und einem Ausdruck gebildet. Durch Auswertung eines Ausdrucks wird ein Ergebnis berechnet, welches, wie alle anderen Werte eines Ausdrucks auch, einen Basisdatentyp hat.

 

Bei der Portierung von Ausdrücken muss darauf geachtet werden, dass sich nicht alle Operatoren in beiden Sprachen gleich verhalten. Aus diesem Grund werden die Unterschiede in den folgenden Abschnitten dargestellt.

 

Operatoren, die sich in beiden Sprachen gleich verhalten, werden hier nicht aufgeführt.

 

3.1.7.1            Shiftoperator

Mit den Shiftoperatoren kann die binäre Darstellung eines Basisdatentyps manipuliert werden, indem alle Bits nach rechts oder links verschoben werden. Hierzu wird in beiden Sprachen der Operator >> oder << verwendet. Je nach Richtung der Shiftoperation müssen am jeweils anderen Ende Bits eingefügt werden.

 

Wenn nach links geschoben wird, werden am rechten Ende ungesetzte Bits eingefügt. Beim Schieben nach rechts werden in Java Kopien des Vorzeichenbits eingefügt. In C hängt es davon ab ob der Datentyp vorzeichenbehaftet ist oder nicht. Bei vorzeichenlosen Datentypen wird immer ein ungesetztes Bit eingefügt, bei vorzeichenbehafteten ist das Verhalten nicht vorgeschrieben.

 

Um in Java zu verhindern, dass am linken Ende gesetzte Bits eingefügt werden, gibt es einen weiteren Operator, nämlich >>>, welcher immer ungesetzte Bits einfügt.

 

3.1.7.2            Fragezeichenoperator

Der Fragezeichenoperator ist der einzige Operator, der drei Operanden erhält. Hierbei entscheidet der Wert des ersten Operanden, welcher einen Wahrheitswert darstellt, darüber, ob der zweite oder dritte Operand das Ergebnis des Ausdrucks darstellt - nur dieser wird dann auch ausgewertet. Der zweite Operand wird gewählt, falls das Ergebnis des ersten Operanden wahr ist, sonst der dritte. Insgesamt ähnelt dieser Operator stark der if-else Anweisung, da aber ein Ergebnis berechnet wird, ist es ein „richtiger“ Operator.

 

Das Ergebnis dieses Ausdrucks kann in C allerdings auch leer (engl. void) sein, beispielsweise beim Aufruf einer Funktion ohne Rückgabewert. Dies ist in Java nicht möglich und wird vom Compiler als Fehler gemeldet. In solchen Fällen kann der gesamte Ausdruck in ein äquivalentes if-else Konstrukt umgewandelt werden, was auch die Lesbarkeit des Quelltextes erhöht.

 

 

3.1.7.3            Relationale Operatoren

Mit relationalen Operatoren kann die Beziehung (Relation) zwischen zwei Werten ermittelt werden. Ein relationaler Ausdruck kann hierbei als Behauptung angesehen werden, deren Wahrheitswert durch die Auswertung des Ausdrucks ermittelt wird. Beispielsweise ist der Ausdruck 5 > 10 ein korrekter Ausdruck, dessen Ergebnis aber den Wahrheitswert falsch hat.

 

Beide Sprachen stellen die gleichen relationalen Operatoren zur Verfügung (<, <=, ==, >=, >, !=), da es in C aber keine Wahrheitswerte gibt, werden sie mit Zahlen emuliert. (siehe 3.1.1) In Java gibt es hierfür den Datentyp boolean.

 

Bei der Portierung von Ausdrücken, die relationale Operatoren verwenden, muss daher darauf geachtet werden, in welchem Kontext sie verwendet werden. Innerhalb der Bedingung von Kontrollanweisungen kann der Ausdruck identisch portiert werden, da Bedingungen in C und Java jeweils den Datentyp verlangen, der von relationalen Ausdrücken geliefert wird.

 

In C ist es aber auch möglich das Ergebnis eines relationalen Ausdrucks in einer Integervariablen zu speichern. Mit dieser Variablen kann anschließend weitergerechnet werden, was in Java nicht möglich ist, da Wahrheitswerte und Zahlen nicht kompatibel sind. Meist wird der Wert aber nur aus dem Grund in einer Variablen gespeichert, um ihn später in der Bedingung einer Kontrollanweisung zu verwenden. In diesem Fall kann die Portierung durch Anpassung des Datentyps der Variablen erreicht werden. Konstrukte der Art int a = 5 > 10; int b = a + 1; sind in C zwar theoretisch möglich, haben aber wenig Sinn.

 

3.1.7.4            Adressoperator

Mit dem Adressoperator & kann ein Zeiger auf die Speicheradresse einer Variablen oder einer Funktion ermittelt werden. Das Ergebnis eines solchen Ausdrucks ist immer ein Pointer mit dem Datentyp des Ausdrucks rechts vom Operator.

 

Die Portierung von Pointer bzw. Funktionspointer wurde bereits in 3.1.4 und 3.1.5 beschrieben.

 

3.1.7.5            Dereferenzierungsoperator

Der Dereferenzierungsoperator * dient dazu den Speicherbereich, auf den ein Zeiger verweist, dem Typ des Zeigers entsprechend zu interpretieren. Wenn beispielsweise eine Variable vom Typ Zeiger auf int dereferenziert wird, ist das Ergebnis vom Typ int. Wird ein Funktionspointer dereferenziert, entspricht das dem Aufruf der entsprechenden Funktion.

 

Die Portierung von Pointer bzw. Funktionspointer wurde bereits in 3.1.4 und 3.1.5 beschrieben.

 

3.1.8      Variable Argumentliste 

Im Gegensatz zu Java können in C Funktionen definiert werden, die eine unbestimmte Zahl an Argumenten erhalten. Über spezielle Zugriffsfunktionen können diese Argumente erreicht werden, sodass die Funktion sie lesen oder modifizieren kann. Hierbei muss die Funktion entscheiden wie sie die Argumente interpretiert, da im Gegensatz zu Funktionen mit fester Argumentmenge, keine Typinformationen über die Argumente existieren.

 

In Java kann dies nur mit einem Objekt realisiert werden, welches eine beliebige Menge anderer Objekte verwaltet, einem sogenannten Behälter (engl. Container). Dieser nimmt die variable Menge an Argumenten auf und wird als Argument an die Funktion gegeben, wodurch die Funktion über Zugriffsmethoden des Behälters die Argumente erreichen kann.

 

Das Konzept „variable Argumentliste“ kann also mit identischer Semantik portiert werden, allerdings müssen einige Anpassungen vorgenommen werden. Die Signatur der Methode muss den Behälter als Argument erhalten, die Zugriffe des Originalquelltextes auf die variable Argumentliste müssen in Behältermethodenaufrufe umgewandelt werden und alle Aufrufe dieser Funktion müssen modifiziert werden, da die Argumente zuerst in dem Behälter abgelegt werden müssen. Basisdatentypen können hier nur dann verwendet werden, wenn es sich um eine variable Argumentlist eines festen Basisdatentyps handelt - dann kann ein Array dieses Basisdatentyps verwendet werden, sonst müssen Basisdatentypen in Objekten gekapselt werden.

 

3.1.9      Präprozessor

Bevor der Compiler den C Quelltext übersetzt, ersetzt der Präprozessor alle Vorkommen gewisser Symbole durch ihre Definition. Hierbei kann der Programmierer eigene Symbole definieren und somit den Quelltext übersichtlich und effizient gestalten. Ähnlich, wie bei Funktionen ist es auch möglich Argumente an solche Präprozessormakros zu übergeben.

 

Zur Portierung von Präprozessormakros nach Java gibt es zwei Möglichkeiten. Entweder ersetzt man selbst vorkommende Symbole, genauso wie es der Präprozessor macht und portiert anschließend das Ergebnis, oder man entwickelt eine Methode, mit der Funktionalität des Makros und ruft sie auf. Vor allem bei komplexen Makros bietet es sich an eine Methode oder sogar eine eigene Klasse zu entwickeln.

 

3.1.10 Thread

Thread ist das englische Wort für Faden und bezeichnet einen Ausführungsstrang durch das Programm. Das besondere an Threads ist, dass ein Programm mehrere Threads gleichzeitig ablaufen lassen kann. Langwierige Berechnungen können beispielsweise in einem Thread implementiert werden, sodass das Programm auch während der Berechnung noch in der Lage ist auf Eingaben o.ä. zu reagieren.

 

Inzwischen stellen beide Sprachen Bibliotheken zur Verfügung, welche der Kontrolle und Verwaltung von Threads dienen. Zur Zeit als LiPS entwickelt wurde waren diese Bibliotheken allerdings noch nicht verfügbar, sodass das LiPS Team eine eigene Threadbibliothek implementierte. Mit Hilfe von Betriebssystemwerkzeugen konnte ein Mechanismus entwickelt werden, der den weiteren Programmablauf nach einer gewissen Zeit unterbricht. So wurde dann erreicht, dass verschiedene Funktionen quasi gleichzeitig ablaufen, indem jede Funktion nur eine gewisse Zeit zur Verfügung bekam. Konnte in dieser Zeit die Aufgabe nicht erledigt werden, wurde sie unterbrochen und später wieder gestartet. Der Programmablauf bleibt auch beim Einsatz von Threads linear, das heißt es wird nur eine Operation zu einem Zeitpunkt ausgeführt, aber durch den Wechsel zwischen den verschiedenen Threads kann erreicht werden, dass mehrere Aufgaben wie gleichzeitig bearbeitet erscheinen.

 

Aufgrund der Verwendung von Betriebssystemwerkzeugen ist der gesamte Mechanismus aber stark von dem verwendeten Betriebssystem abhängig, was dem plattformunabhängigen Gedanken von Java widerspricht. Da Java allerdings Klassen zur Verfügung stellt, mit denen Threads verwaltet werden können, muss dieser Mechanismus nicht selbst nachgebildet werden. Es ist lediglich nötig, zu analysieren, wofür Threads eingesetzt werden und dann die entsprechende Funktionalität in eine die Schnittstelle Runnable implementierende Klasse zu übertragen. Dann kann ein Thread erzeugt werden, der eine Instanz dieser Klasse erhält und somit nach seinem Start die run() Methode aufruft.

 

Um Daten vor konkurrierendem Zugriff zweier verschiedener Threads zu schützen, müssen entsprechend kritische Stellen synchronisiert werden.

 

3.1.11 Netzwerkprogrammierung

Netzwerkprogrammierung umfasst die Nutzung der Netzwerkdienste TCP und UDP mit dem Zweck Nachrichten zwischen verschiedenen Prozessen auszutauschen.

 

In beiden Sprachen können diese Netzwerkdienste genutzt werden, indem zum Sprachumfang gehörende oder vom Betriebssystem gelieferte Bibliotheken verwendet werden. Eine detaillierte Darstellung der Portierungsmöglichkeiten würde den Rahmen dieser Arbeit sprengen - hier sei auf die zu den Bibliotheken gehörende Dokumentation verwiesen.

 

3.2      Komponenten des lipsd

 

In Kapitel 2 wurde bereits dargestellt wie eine Anwendung mit LiPS verteilt ablaufen kann. Die hierfür nötigen Schritte wurden nur sehr abstrakt dargestellt, um ein Bild des ganzen Systems zu liefern, außerdem wurde davon ausgegangen, dass das System-Laufzeitsystem bereits etabliert ist. Hier wird nun dargestellt, welche Komponenten benötigt werden, um einerseits ein fehlertolerant arbeitendes System-Laufzeitsystem bereitstellen zu können und andererseits Anwendungen mit dem Anwendungs-Laufzeitsystem die fehlertolerante Ausführung zu ermöglichen.

 

Zunächst werden die technischen Voraussetzungen dargestellt, die erfüllt sein müssen, damit ein Rechner Teil des LiPS Systems werden kann.

 

Anschließend wird analysiert, wie die zentrale Komponente, die Tupelraumbibliothek, aufgebaut ist und welche Komponenten ihrerseits benötigt werden.

 

Im nächsten Abschnitt wird dargestellt wie das System-Laufzeitsystem sich selbst überwacht und somit permanent zur Verfügung steht.

 

Der folgende Abschnitt zeigt, wie Befehlsnachrichten ausgetauscht und interpretiert werden, wodurch das Anwendungs-Laufzeitsystem die Möglichkeit hat den Start von Prozessen zu veranlassen.

 

Im letzten Abschnitt dieses Kapitels wird der vorgestellt, wie der Evalserver die Abarbeitung der Aufträge steuert.

 

3.2.1      Technische Voraussetzungen

Programme, die Dienstleistungen zur Prozesskommunikation oder Manipulation von Prozessen auf entfernten Computern anbieten, können auf jedem zeitgemäßen Computer unter Verwendung verschiedener Softwarebibliotheken implementiert werden. Um sie aber ablaufen zu lassen, müssen verschiedene technische Voraussetzungen erfüllt sein.

 

Der Computer muss Teil eines IP-Netzwerks sein und das auf ihm ablaufende Betriebssystem muss die Netzwerkdienste TCP und UDP anbieten, damit Kommunikation überhaupt möglich ist. SSH muss vorhanden sein und für automatische Authentifizierung konfiguriert sein, da sonst keine Prozessmanipulation auf entfernten Computern möglich ist. SCP wird benötigt um Dateien mit SSH auf entfernte Rechner zu kopieren und somit auch Zugriff auf das Dateisystem.

 

3.2.1.1            Prozesskommunikation

Damit auf verschiedenen Computern residierende Prozesse Nachrichten unter einander austauschen können, muss eine Verbindung zwischen diesen Computern existieren. Entweder haben beide Computer eine Verbindung zum Internet, oder sie sind Teil des selben lokalen IP-Netzwerks.

 

Außerdem müssen die auf den Computern laufenden Betriebssysteme die Netzwerkdienste TCP und UDP bereitstellen.

 

3.2.1.2            Prozessmanipulation

Prozessmanipulation auf entfernten Computern wird erreicht, indem Prozesskommunikation verwendet wird. Hierzu ist es nötig, dass auf beiden Computern jeweils ein Prozess läuft, der in der Lage ist, Nachrichten eines anderen Prozesses zu interpretieren. Wichtig ist, dass die Programme „sich verstehen“, also das selbe Protokoll implementieren. Das Protokoll legt Aufbau, Semantik und Reihenfolge der Nachrichten fest, schreibt aber nicht vor, wie es umgesetzt werden muss. Des weiteren regelt es welcher Netzwerkdienst zum Transport der Nachrichten benutzt wird.

 

Es gibt verschiedene Programme, wie beispielsweise ssh oder rsh, die den Dienst anbieten Befehle zu empfangen und auszuführen, die also benutzt werden können um Programme auf entfernten Computern zu starten. In LiPS wird ssh verwendet, da es aufgrund des verschlüsselten Nachrichtenaustauschs mehr Sicherheit bietet.

 

Ebenfalls aus sicherheitsrelevanten Gründen ist eine erfolgreiche Authentifizierung[13] des Anwenders nötig, der mit ssh einen Befehl auf einem entfernten Rechner ausführen möchte. Auch dies ist Bestandteil von ssh, allerdings muss eine weitere Voraussetzung erfüllt sein, damit ein Prozess, im Gegensatz zu einem Anwender, den ssh - Dienst nutzen kann. Ssh muss so konfiguriert sein, dass die Authentifizierung automatisch abläuft. Da es verschiedene Anbieter von ssh - Programmen gibt und daher auch Unterschiede bei der Konfiguration bestehen, kann hier nur auf die Dokumentation des jeweils verwendeten ssh – Programms verwiesen werden.

 

3.2.1.3            Dateisystem

Zugriff auf das Dateisystem wird aus verschiedenen Gründen zum lesen, schreiben und erzeugen von Dateien benötigt. Einerseits muss das System-Laufzeitsystem verschiedene Konfigurationsdateien lesen und schreiben, andererseits werden für ablaufende Anwendungsprozesse Arbeitsverzeichnisse angelegt, in welchen der Anwendungsprozess selbst ebenfalls das Recht haben muss Dateien zu lesen, zu schreiben oder zu erzeugen.

 

3.2.2      Tupelraumbibliothek

In Kapitel 2 wurde bereits dargestellt über welche Funktionen die Tupelraumbibliothek verfügt und wie Tupel erzeugt und im Tupelraum abgelegt oder mittels eines Template von dort gelesen werden können. Es ist aber auch nötig andere Daten an einen Messageserver zu schicken, weshalb ein allgemeines Format für Tupelraumnachrichten entwickelt wurde, welches außerdem Platz für die Daten bereitstellt, die zur Gewährleistung der Fehlertoleranz benötigt werden. Dies sind zum Einen die „logische Zeit“ (engl. logic time), eine Nummerierung der Nachrichten, und zum Anderen ein Signal, welches zum Transport dringender Nachrichten benutzt wird. Bevor aber Nachrichten ausgetauscht werden können, muss eine Verbindung zu einem Messageserverprozess aufgebaut werden. Der folgende Abschnitt beschreibt wie dieser Verbindungsaufbau abläuft, bevor das Format der Nachricht dargestellt wird. Hierbei werden auch Logic Time und Signalmechanismus erläutert.

 

3.2.2.1            Verbindungsaufbau

Der Verbindungsaufbau läuft immer in drei Phasen ab, unabhängig davon, wer die Verbindung wünscht. Nach der initialen Phase wird die aktive Phase solange wiederholt, bis sie erfolgreich durchgeführt werden konnte. Abschließend wird in der passiven Phase die Verbindung etabliert.

 

Initiale Phase

In der initialen Phase muss eine Datei gelesen werden, die Serveradressen bereitstellt. Für den lipsd ist dies die Datei .lips.conf mit einer Liste von Fixserveradressen,  für Anwenderprozesse ist dies die Datei .MSGSERVER, die im Arbeitsverzeichnis der Anwendung gefunden wird und eine Liste von Messageserveradressen beinhaltet.

 

Außerdem muss ein Port geöffnet werden, zu welchem sich in der dritten Phase ein Messageserver verbindet.

 

Aktive Phase

In der aktiven Phase wird der TCP Netzwerkdienst genutzt, um eine Nachricht an einen der Server aus der Datei zu schicken, die den Wunsch eine Verbindung aufzubauen ausdrückt. Teil der Nachricht ist eine ID, die den Kommunikationspartner identifiziert. Anwendungsprozesse bekommen die ID zugewiesen, der lipsd berechnet sie selbst, indem die Datei lipsd.pids gelesen wird, welche die IP-Adressen aller LiPS-Rechner enthält. Da jede Zeile dieser Datei nur eine Adresse enthält, kann die Nummer der Zeile, welche die eigene IP-Adresse enthält als systemweit eindeutige ID verwendet werden. Es muss allerdings sichergestellt werden, dass auf jedem Rechner dieselbe Datei verwendet wird. Hierfür sorgt das System-Laufzeitsystem durch einen Mechanismus, der in Abschnitt 6.2[o1]  beschrieben wird. Außer der ID enthält die Nachricht den Port, mit welchem der Messageserver sich verbinden soll und den intialen Wert der logischen Zeit.

 

Wenn das Versenden der Anfrage nicht erfolgreich durchgeführt werden kann, wird versucht, einen anderen Server zu erreichen. Sollte kein Server erreichbar sein, beendet sich das Programm.

 

Passive Phase

In der passiven Phase wird gewartet, bis der Messageserver eine Verbindung zu dem angegebenen Port aufbaut.

 

Die erste Nachricht des Messageservers beinhaltet den Wert der Systemuhr des Messageservers, welcher benutzt wird, um die lokale Systemuhr zu synchronisieren. Von Ungenauigkeiten, welche durch unterschiedliche Netzwerklaufzeiten entstehen abgesehen, kann so eine systemweit synchrone Uhr realisiert werden.

 

3.2.2.2            Format der Tupelraumnachricht

Tupelraumnachrichten bestehen immer aus einem Kopf (engl. header) und einem Datenteil. Der Kopf hat immer die selbe Größe und enthält neben Signal, Typ und logischer Zeit auch eine Angabe über die Größe der folgenden Daten. Abbildung 1 stellt das Nachrichtenformat grafisch dar.

 

Kopf 16 Byte

Daten

Signal

Typ

LogicTime

Länge d. Daten

Daten

4 Byte int

4 Byte int

4 Byte int

4 Byte int

n Bytes

Abbildung 1 Format der Tupelraumnachricht

 

Signal

Mit dem Signalmechanismus wird allen Kommunikationspartnern eines Tupelraums angezeigt, dass ein wichtiger Systemauftrag abgearbeitet werden muss. Das Signal gibt hierbei lediglich Auskunft über die Art des Systemauftrags, wohingegen alle benötigten Daten zur Erledigung dieses Auftrags im Tupelraum abgelegt sind. Wichtig ist in diesem Zusammenhang, dass ein Signal nur als Teil einer anderen Tupelraumnachricht übertragen wird. Allein das Vorhandensein eines wichtigen Systemauftrags führt noch nicht zur Verbreitung des zugehörigen Signals.

 

Die folgenden Abschnitte erläutern die verschiedenen Signale.

 

SIG_CLIENT

SIG_CLIENT hat den Wert 1 und signalisiert, dass die Verbindung eines Fixservers zu einem lipsd abgebrochen ist. Im Tupelraum liegt mindestens ein ClientCrashedTupel, welches die IP-Adresse des Rechners enthält, auf welchem ein neuer lipsd gestartet werden muss. Jeder lipsd versucht nach Erhalt dieses Signals ein ClientCrashedTupel zu extrahieren, die erfolgreichen lipsd aktualisieren das entsprechende Daemon-Status-Tupel um zu verhindern, dass der DaemonWatcher ebenfalls einen Neustart durchführt und starten auf dem entsprechenden Rechner einen neuen lipsd.

 

Tests haben gezeigt, dass die IP Adresse im ClientCrashedTupel fehlerhaft angegeben wird. Der Messageserver gibt nur drei der benötigten vier Adressbestandteile an. Fehlerhafte IP Adressen werden erkannt und ignoriert. Der DaemonWatcher sorgt, wie später gezeigt wird, dafür, dass das System-Laufzeitsystem trotzdem verfügbar bleibt.

 

SIG_REFRESH

SIG_REFRESH hat den Wert 2 und signalisiert, dass die Konfiguration mindestens eines lipsd verändert wurde. Nach Erhalt dieses Signals liest jeder lipsd sein DaemonConfigTupel neu ein und aktualisiert gegebenenfalls verschiedene globale Variablen. Hauptsächlich wird dieses Signal genutzt um die Restriktionen, die den Start eines Anwenderprozesses verhindern, zu ändern.

 

SIG_CONFIG

SIG_CONFIG hat den Wert 4 und zeigt an, dass für eine spezielle Anwendung eines speziellen Anwenders ein neuer Messageserver zur Verfügung steht. Die neue Serverliste ist Teil eines UserConfigTupels, welches jeder lipsd nach Erhalt dieses Signals zu extrahieren versucht. Hierbei wird die eigene IP-Adresse als Schlüssel verwendet. Kann ein solches Tupel extrahiert werden, aktualisiert der lipsd die Serverliste der Prozesse, die zu dem angegebenen Anwendung - Anwender Paar passen und von ihm gestartet wurden.

 

SIG_SHUTDOWN

SIG_SHUTDOWN hat den Wert 8 und zeigt an, dass alle Prozesse, die zu einer speziellen Anwendung eines speziellen Anwenders gehören, beendet werden sollen. Nach Erhalt dieses Signals versucht jeder lipsd ein UserShutdownTupel zu extrahieren, indem die eigene IP-Adresse als Schlüssel verwendet wird. Gelingt dies, werden alle zu dem aus dem Tupel gelesenen Anwendung - Anwender Paar passenden Prozesse beendet, die von dem jeweiligen lipsd selbst gestartet wurden.

 

Typ

Der Typ gibt an, um welche Art von Daten  es sich im Datenteil  handelt. Insgesamt gibt es 31 verschiedene Typen, wobei die meisten lediglich für den Austausch von Nachrichten zwischen Messageservern benötigt werden. Für die Implementierung der Tupelraumbibliothek für lipsd und verteilte Anwendung werden nur 14 Typen benötigt, wobei beachtet werden muss, dass die meisten Typen lediglich empfangen oder gesendet werden müssen. Beispielsweise werden weder an den lipsd noch an die Anwendungen Tupel lesende Nachrichten geschickt. Beide müssen aber in der Lage sein Tupel lesende Anfragen zu versenden. Aus diesem Grund ist in den folgenden Abschnitten nicht nur dargestellt, welchen Zweck die verschiedenen Typen haben, sondern auch ob sie aus der Sicht der Tupelraumbibliothek gesendet oder empfangen werden müssen.

 

INIT_TRANSMISSION_LINE

Eine Nachricht dieses Typs wird automatisch immer dann an einen Messageserver geschickt, wenn eine Tupelraumoperation aufgerufen wurde, die Verbindung zum Tupelraum aber nicht existiert, also aufgebaut werden muss. Die Adresse eines Tupelraums wird aus der Datei, die bei der Initialisierung der Tupelraumbibliothek angegeben wurde, gelesen, wodurch auch festgelegt ist, ob die Verbindung zu einem Fix- oder Messageserver aufgebaut wird. Im Datenteil dieser Nachricht wird die ID des Absenders und der Port angegeben, an welchem die Verbindung des Servers erwartet wird.

 

CLIENT_OUT

Dieser Typ wird sowohl gesendet als auch empfangen. Nachrichten, die ein Tupel in den Tupelraum transportieren und Antworten, die ein Tupel aus dem Tupelraum zu lipsd oder Anwendung transportieren sind vom Typ CLIENT_OUT.

 

CLIENT_IN, CLIENT_INP, CLIENT_RD, CLIENT_RDP

Nachrichten mit einem dieser Typen transportieren ein Template und werden zu einem Messageserver geschickt. Als Antwort wird entweder ein Nachricht vom Typ CLIENT_OUT oder eine negative Quittungsnachricht mit einem der Typen CLIENT_NACK, CLIENT_INP_NACK oder CLIENT_RDP_NACK erwartet.

CLIENT_OUT_ACK

Diese Nachricht wird als Quittung für das erfolgreiche Ablegen eines Tupels im Tupelraum empfangen.

 

CLIENT_NACK

Wenn auf eine blockierende Leseanfrage kein passendes Tupel geliefert werden kann, wird eine Nachricht vom Typ CLIENT_NACK empfangen. Die Tupelraumbibliothek wartet dann eine Weile, bevor sie die Anfrage wiederholt.

 

CLIENT_INP_NACK / CLIENT_RDP_NACK

Wenn auf eine nicht blockierende Leseoperation kein passendes Tupel geliefert werden kann, wird eine Nachricht des entsprechenden Typs empfangen.

 

CLIENT_START

Mit einer Nachricht dieses Typs wird einem Messageserver der Anwendung der Befehl erteilt, einen Clientprozess zu starten. Im Datenteil werden Name des Anwenders, Programmname, eine Liste der unterstützten Architekturen und der Name einer Logdatei angegeben. Durch die Angabe der Anzahl der gewünschten Clientprozesse beim Start des Masterprozesses wird diese Nachricht automatisch an den Messageserver geschickt. Als Antwort wird ebenfalls eine Nachricht mit Typ CLIENT_START erwartet, welche im Datenteil dann den Text „STARTED „ gefolgt von der Prozess ID des neuen Clientprozesses enthält.

 

CLIENT_SETSIG

Nachrichten dieses Typs werden an einen Fixserver geschickt, um ein Signal zu setzen. Hierdurch erhält jeder lipsd, der mit diesem Tupelraum verbunden ist, den mit dem Signal verknüpften Auftrag. Nachdem das Signal abgearbeitet ist, muss es von jedem lipsd mit einer Nachricht vom Typ CLIENT_RESETSIG gelöscht werden.

 

CLIENT_RESETSIG

Nachrichten dieses Typs werden an einen Fixserver geschickt, nachdem ein durch ein Signal ausgelöster Auftrag abgearbeitet wurde. Der Messageserver löscht das im Datenteil spezifizierte Signal dann für den entsprechenden lipsd.

 

CLIENT_CKPT

Das Versenden einer Nachricht mit Typ CLIENT_CHKP veranlasst einen Checkpoint. Siehe [Set98]

 

LogicTime

Teil jeder Nachricht, die an einen Messageserver geschickt wird. ist die logische Zeit, welche eine aufsteigende Nummerierung der Nachricht darstellt. Messageserver verwalten nicht nur den Tupelraum, sondern  protokollieren gleichzeitig alle Nachrichten im sogenannten Nachrichtenlogbuch (engl. message log). Hierdurch kann jederzeit auf ältere Nachrichten zugegriffen werden, was verwendet wird, um Anwenderprozesse nach einem Ausfall neu zu starten. Näheres hierzu wird in 4.1.5.3 erläutert.

 

Datenlänge

Die Angabe der Größe der folgenden Daten wird benötigt, um die Nachricht komplett lesen zu können und aufeinanderfolgende Nachrichten zu differenzieren.

 

3.2.3      System-Laufzeitsystem

Das System-Laufzeitsystem besteht, wie in Kapitel zwei geschildert, aus einem Verbund von Fixservern und den auf allen LiPS-Rechnern ablaufenden Kontrollprozessen lipsd. Dieser Abschnitt untersucht nun, wie die Aufgabe, die Verfügbarkeit des System-Laufzeitsystems zu gewährleisten, erbracht wird und mit welchen Mitteln Prozesse kontrolliert werden.

 

Zur Gewährleistung der Verfügbarkeit des System-Laufzeitsystems startet der lipsd zwei verschiedene Threads, einen, der regelmäßig den Status des lokalen lipsd im Tupelraum aktualisiert, den StatusUpdater und einen, der regelmäßig Statusmeldungen anderer lipsd’s überprüft und bei Bedarf einen Neustart einleitet, den DaemonWatcher. Der Ablauf des Starts eines lipsd auf einem entfernten Rechner wird in 6.2 beschrieben.

 

Außerdem stellt das System-Laufzeitsystem dem Anwendungs-Lauf­zeit­sys­tem Methoden zur Prozesskontrolle zur Verfügung, indem Befehlsnachrichten (eng. Commandmessage) eines Masterprozesses oder Messageservers empfangen und interpretiert werden können. Hierbei fungiert der lipsd, der auf dem selben Rechner abläuft, wie der Master bzw. Messageserver, als Verteiler der Befehlsnachricht, was erfordert, dass die lipsd’s auch untereinander Befehlsnachrichten austauschen können. Unbedingt zu beachten ist hierbei, dass die Kommunikation mit dem Master bzw. Messageserver identisch nachprogrammiert werden muss, wohingegen die Kommunikation zwischen den lipsd’s bei Bedarf neu gestaltet werden kann.

 

Die folgenden Abschnitte erläutern zunächst, das Format des Daemon-Status-Tupel, bevor die Arbeitsweise des StatusUpdater und DaemonWatcher dargestellt wird. Anschließend wird die Befehlsnachricht analysiert, indem sowohl Format als auch Auswirkung der verschiedenen Befehle dargestellt werden.

 

3.2.3.1            Daemon-Status-Tupel

Das Daemon-Status-Tupel gibt Auskunft über den Status eines lipsd. Es enthält neben der Adresse und einigen statistischen Angaben über die bisher verursachte Netzlast vor allem zwei wichtige Angaben: Erstens die Zeit der letzten Aktualisierung und zweitens den Status der Verfügbarkeit. Letzte Angabe wird benötigt, um zu verhindern, dass ein lipsd oder ein Anwenderprozess auf einem speziellen Rechner gestartet wird. So kann gewährleistet werden, dass ein Rechner zwar erreichbar ist, trotzdem aber kein lipsd oder Anwenderprozess dort gestartet wird, da beispielsweise administrative Arbeiten an diesem Rechner ausgeführt werden. Die Zeit der letzten Aktualisierung basiert auf der beim Verbindungsaufbau synchronisierten Systemzeit, sodass diese Angabe im gesamten System gleich interpretiert werden kann.

 

3.2.3.2            StatusUpdater

Der StatusUpdater hat die Aufgabe das Daemon-Status-Tupel in regelmäßigen Abständen zu aktualisieren. Die Zeit zwischen zwei Aktualisierungen wird aus der Datei home/lips/newJLiPSD.properties als STATUS_UPDATE_INTERVAL­_IN_MILLIS gelesen.

 

3.2.3.3            DaemonWatcher

Der DaemonWatcher prüft in regelmäßigen Abständen das Alter genau eines Daemon-Status-Tupel. Liegt der Zeitpunkt der letzten Aktualisierung länger als zwei Aktualisierungsintervalle zurück, wird davon ausgegangen, dass der entsprechende lipsd neu gestartet werden muss. Da der Vorgang des Neustarts der selbe ist, wie nach erhalt des Signals Sig_Client, wird er zusammenfassend in 6.2 beschrieben. 

 

3.2.3.4            Befehlsnachrichten

Eine Befehlsnachricht ist ebenso wie die eventuell erwartete Antwort eine Textnachricht, die mit dem UDP Netzwerkdienst verschickt wird. Das Format der Befehlsnachricht wird im folgenden Abschnitt erläutert, woraufhin ihre Interpretierung im Abschnitt Semantik dargestellt wird. Abschließend wird erläutert, wie Textnachrichten mit dem UDP Netzwerkdienst so verschickt werden können, dass eine zuverlässige Kommunikation gewährleistet werden kann.

 

Format

Eine Befehlsnachricht besteht aus einem String, der aus einem Nachrichtenkopf und einem Datenteil zusammengesetzt ist. Der Nachrichtenkopf enthält den Namen des Befehls und die Antwortadresse, der Datenteil die Argumente des Befehls. Antworten sind nach dem selben Format aufgebaut, wodurch auch sie wie Befehlsnachrichten behandelt werden können.

 

Abschnitt 4.2.2 beschreibt die Komponente, welche Versand und Empfang von UDP-Nachrichten implementiert. Abbildung 2 stellt das Format grafisch dar.


 








Befehlsnachricht

Nachrichten­kopf

Name

Befehlsname

@

Trennzeichen

„ „

Leerzeichen

Port

Portnummer, an welcher Antwort erwartet wird

„ „

Leerzeichen

IP

IP-Adresse des Absenders

„ „

Leerzeichen

Argumente

Daten

Argumente des Befehls, durch Leerzeichen getrennt

Abbildung 2 Format der Befehlsnachricht

 

Semantik

Die folgenden Abschnitte beschreiben sowohl das Format der Argumente als auch die Interpretierung der Befehlsnachricht. Außerdem wird der Aufbau der eventuell geforderten Antwort dargestellt.

 

REGISTER

Mit dem REGISTER Befehl registriert der Masterprozess die verteilte Anwendung bei dem lokal ablaufenden lipsd. Dies führt dazu, dass das Kopieren der ausführbaren Programmdateien in das Repository vorbereitet wird und zusätzlich ein den Masterprozess beschreibendes UserProcTupel in den Systemtupelraum gelegt wird, wodurch die Überwachung des Masterprozesses gewährleistet werden kann. Abbildung 3 stellt das Format der REGISTER Nachricht dar.

 

REGISTER

 

 

Unix Pid

Prozess ID des Masterprozesses

 

Anwendername

Name des Anwenders, in dessen Namen die Anwendung abläuft.

 

Repository

Pfad zum Repository

 

Programmname

Name der verteilten Anwendung

 

Architekturliste

Liste der Betriebssysteme, für die ausführbare Clientprogramme zur Verfügung stehen

 

ExecTimeStamp

Ausführungszeitpunkt

 

Abbildung 3 Format der Befehlsnachricht REGISTER

RSTART

RSTART wird von einem Messageserver an den lokal ablaufenden lipsd geschickt, wenn ein neuer Evalserver für eine verteilte Anwendung gestartet werden soll. Abbildung 4 stellt das Format der Nachricht dar. Die Nachricht enthält alle Informationen, die für den Start des geforderten Prozesses benötigt werden. Der Empfänger der RSTART-Nachricht startet den Prozess aber nicht selbst, schließlich läuft auf dem Rechner bereits ein Messageserver, sondern er sucht einen anderen lipsd, den er mit dem Start beauftragt. Hierzu wählt er aus der Liste der Daemon-Status-Tupel einen geeigneten lipsd aus und sendet diesem eine STARTEN-Nachricht. Anschließend sendet er eine positive Bestätigung (engl. Acknowledgement) an den Messageserver. Konnte kein geeigneter lipsd gefunden werden, wird eine negative Bestätigung an den Messageserver geschickt, woraufhin dieser einige Zeit wartet und dann erneut den Start des Prozesses anstößt. Die Bestätigungsnachrichten bestehen lediglich aus dem Text „NACK RSTART“ bzw. „RSTART ACK“, weshalb auf eine grafische Darstellung an dieser Stelle verzichtet wurde. Abbildung 4 stellt das Format der RSSTART Nachricht dar.

 

RSTART

 

 

Programmname

Name der verteilten Anwendung

 

LiPS-PID

Interne LiPS ID, welche den zu startenden Prozess eindeutig identifiziert.

 

startwert LogicTime

Ermöglicht den Start eines Anwenderprozesses ab seinem letzten Checkpoint

 

Architekturliste

Liste der Betriebssysteme, für die ausführbare Clientprogramme zur Verfügung stehen

 

Repository

Pfad zum Repository

 

Serverliste

Liste der Messageserver, die von der Anwendung benutzt werden können.

 

ExecTimeStamp

Ausführungszeitpunkt

 

Anwendername

Name des Anwenders, in dessen Namen die verteilte Anwendung abläuft.

 

Abbildung 4 Format der Befehlsnachricht RSTART

 

STARTEN

STARTEN wird von einem lipsd an einen anderen lipsd geschickt, um einen Evalserver für eine verteilte Anwendung zu starten. Hierzu wird zunächst sichergestellt, dass das Clientprogramm im lokalen Dateisystem verfügbar ist. Falls sich mehrere Arbeitsplatzrechner ein Dateisystem teilen, wird die Datei für jedes Betriebssystem nur einmal kopiert. Anschließend wird ein eindeutiges Arbeitsverzeichnis für die Anwendung erzeugt. Der Startwert für die logische Zeit ermöglicht es hier die Anwendung von ihrem letzten Checkpoint aus zu starten. Außerdem wird ein den Prozess beschreibendes UserProcTupel im Systemtupelraum abgelegt.

 

An den Absender wird keine Bestätigung geschickt. Sollte der Start nicht erfolgreich durchgeführt werden können, stellt der Messageserver das Ausbleiben der erwarteten Anmeldung fest, woraufhin er einen erneuten Start veranlasst. Abbildung 5 stellt das Format der Befehlsnachricht STARTEN dar.

 

STARTEN

 

 

Anwendername

Name des Anwenders, in dessen Namen die Anwendung abläuft.

 

AppID

ID der Anwendung - immer 42

 

LiPS-PID

Interne LiPS ID

 

startwert LogicTime

Ermöglicht den Start eines Anwenderprozesses ab seinem letzten Checkpoint

 

Repository

Pfad zum Repository

 

Serverliste

Liste der Messageserver, die von der Anwendung benutzt werden können.

 

ExecTimeStamp

Ausführungszeitpunkt

 

Programmname

Name des Clientprogramms

 

Abbildung 5 Format der Befehlsnachricht STARTEN

 

GET_USER_PROCS

Mit dieser Befehlsnachricht fordert der Messageserver eine Liste aller Anwenderprozesse an, die von den lipsd’s kontrolliert werden. Diese gleicht er mit den Anwenderprozessen ab, die eine aktive Verbindung zu einem Messageserver haben. Sollten hier Prozesse fehlen, wird der Neustart eingeleitet.

 

Der Name der Antwort ist USER-PROCS und im Datenteil ist eine Liste der internen LiPS PID’s durch Leerzeichen getrennt. Abbildung 6 und Abbildung 7 stellen die Nachrichten dar.

 

GET-USER-PROCS

 

 

Anwendername

Bezeichnet den Anwender, dessen Prozesse aufgelistet werden sollen. (Wird nicht verwendet!)

 

Abbildung 6 Format der GET-USERE-PROCS Nachricht

 

USER-PROCS

 

 

Liste der Prozess ID’s

Liste der internen LiPS PID’s

 

Abbildung 7 Format der USER-PROC Nachricht

 

SHUTDOWN_USER

SHUTDOWN_USER wird gesendet, wenn eine Anwendung eines speziellen Anwenders beendet werden soll. Die Argumente enthalten den Namen des Anwenders und die ID der in seinem Namen ablaufenden Anwendung. Der Empfänger der Nachricht durchsucht alle UserProcTupel und legt für jedes zu den Argumenten passende Tupel ein SHUTDOWN-Tupel im Tupelraum ab. Anschließend setzt er das SIG_SHUTDOWN Signal, sodass jeder lipsd bei der nächsten Tupelraumoperation den Auftrag erhält nach SHUTDOWN-Tupeln zu suchen, die von ihm gestartete Prozesse betreffen. Diese beendet er dann.

 

SHUTDOWN-USER

 

 

Anwendername

Bezeichnet den Anwender, dessen Prozesse beendet werden sollen.

 

AppID

ID der Anwendung - immer 42

 

Abbildung 8 Format der SHUTDOWN-USER Nachricht

 

CONF_CHANGE

CONF_CHANGE wird verschickt, wenn ein neuer Messageserver für eine Anwendung eines Anwenders zur Verfügung steht. Ähnlich wie bei der SHUTDOWN_USER Nachricht wird hier für jedes USER_PROC Tupel, das einen Prozess des Anwenders beschreibt ein UserConfigTupel im Tupelraum abgelegt und anschließend das SIG_CONF Signal gesetzt. Jeder lipsd wird nach Erhalt dieses Signals nach UserConfigTupeln suchen, und gegebenenfalls die Serverliste der Prozesse des Anwenders aktualisieren.

 

CONF-CHANGE

 

 

Anwendername

Bezeichnet den Anwender, dessen Anwendungen eine aktualisierte Serverliste erhalten

 

AppID

ID der Anwendung - immer 42

 

Serverliste

Liste von IP-Adressen und Portnummern, von Messageservern für die Anwendung des Anwenders

 

Abbildung 9 Format der CONF-CHANGE Nachricht

 

Zuverlässige Kommunikation mit UDP

Der Transport von Daten mit dem UDP Netzwerkdienst ist nicht zuverlässig, was es erfordert eigene Mechanismen zu implementieren, mit denen zuverlässig kommuniziert werden kann. Hierzu wurde ein eigenes Protokoll entwickelt, welches durch die Übertragung zusätzlicher Daten die vollständige Ankunft einer Nachricht garantieren kann. Da dieses Protokoll auch im Messageserver implementiert ist, muss es identisch nachprogrammiert werden. Eine detaillierte Darstellung des Protokolls ist in [Fis96] ab Seite 64 zu finden. Hier sei lediglich darauf hingewiesen, dass ein Thread Nachrichten solange verschickt, bis ihr vollständiger Empfang durch den Erhalt einer Quittungsnachricht bestätigt wurde. Nachricht und Quittung können durch die Vergabe einer Nachrichtenidentifikationsnummer eindeutig zugeordnet werden.

 

Falls eine Befehlsnachricht eine Antwort erfordert, muss ein Funktionspointer an den Sendethread übergeben werden. Nach erfolgreichem Versand wird auf eine Antwort gewartet und die durch den Funktionspointer spezifizierte Funktion wird mit der empfangenen Antwort als Argument aufgerufen.

 

Ein weiterer Thread hat die Aufgabe Befehlsnachrichten zu empfangen, die erwartete Quittungsnachricht zu senden und die Ausführung einzuleiten.

 

3.2.3.5            Repository

Mit Repository wird das Verzeichnis bezeichnet, in welchem alle von einer verteilten Anwendung benötigten Dateien gespeichert werden. Dies betrifft sowohl die ausführbaren Clientprogramme für die verschiedensten Plattformen, als auch von der verteilten Anwendung benötigte oder erzeugte Dateien. Gerade letztere erfordern eine spezielle Behandlung, da sie als Teil eines Tupels replizierbar verwaltet werden müssen. Wenn ein Client nach einem Absturz neu gestartet wird, wird seine Kommunikation mit dem Tupelraum identisch zu seinem ersten Ablauf wiederholt, bis alle gespeicherten Tupelraumoperationen verbraucht sind. Falls während diesen Tupelraumoperationen auch eine Datei übertragen wurde, muss sie in identischer Form - und nicht etwa in aktueller Version - erneut übertragen werden. Dies wird einerseits durch die im folgenden Abschnitt dargestellte und in der Tupelraumbibliothek implementierte Dateibehandlung und andererseits durch den Repositoryserver gewährleistet.

 

Dateibehandlung

Während Tupel im Speicher eines Computers liegen, auf dem ein Messageserverprozess abläuft, werden Dateien im Dateisystem des selben Computers abgelegt. Um Dateien replizierbar speichern zu können, muss zur Verwaltung der verschiedenen Versionen zusätzlicher Aufwand betrieben werden. Bei LiPS besteht dieser Aufwand aus der speziellen Art, den Namen der physikalischen Datei im Repository zu erzeugen, für den Anwender ändert sich der Name nicht.

 

Der physikalische Name der Datei setzt sich aus ihrem eigentlichen Namen und einer Versionsnummer zusammen. Die Versionsnummer der jeweils als nächstes zu schreibenden Datei wird in einem Tupel mit Namen LIPS_FILE_VERSION gespeichert, welches als Schlüssel den eigentlichen Namen der Datei speichert. Zur Übertragung einer Datei in das Repository wird dieses Tupel extrahiert, wodurch exklusiver Zugriff auf die Versionsnummer der nächsten zu schreibenden Datei besteht. Der Name der Datei wird nun mit dieser Versionsnummer versehen und so an den Repositoryserver übertragen. Die um eins erhöhte Versionsnummer wird mit dem LIPS_FILE_VERSION Tupel wieder im Tupelraum abgelegt, und der nächste Client schreibt nach dieser Methode die nächste Version der selben Datei. Falls das LIPS_FILE_VERSION Tupel nicht vorhanden ist, wird es erzeugt. Hieraus könnten Probleme entstehen, wenn mehrere Clients gleichzeitig versuchen eine Datei mit selben Namen zu übertragen. Einer ermittelt gerade die Versionsnummer wie oben beschrieben, hat also das LIPS_FILE_VERSION Tupel extrahiert und ein anderer erzeugt das Tupel neu, da er es nicht extrahieren kann. Um dies zu verhindern, kann nur der Client ein LIPS_FILE_VERSION Tupel manipulieren, der zuvor das ein Tupel mit Namen LIPS_FILE_LOCK extrahiert hat. Dieses Tupel sorgt dafür, dass immer nur ein Client gleichzeitig eine Manipulation an LIPS_FILE_VERSION Tupeln durchführen kann. Es wird bei der Initialisierung des Systems erzeugt, vor einer Manipulation extrahiert und danach wieder im Tupelraum abgelegt.

 

Das lesen einer Datei erfolgt, indem zuerst die Versionsnummer der nächsten zu schreibenden Version ermittelt wird, hierfür genügt das Lesen des Tupels LIPS_FILE_VERSION. Der um eins niedrigere Wert beschreibt die aktuelle Version.

 

Im Falle eines nach Absturz neu gestarteten Clientprogramms werden auch die Tupel zur Dateiverwaltung repliziert, was bewirkt, dass die selbe Versionsnummer wie beim ersten Ablauf ermittelt wird. Dies wiederum bewirkt die Übertragung der selben Datei. Die Übertragung der Datei in das Repository überschreibt in diesem Fall die beim ersten Ablauf erzeugte Datei, was aber keinen Schaden anrichtet, da die Dateien identisch sein sollten.

 

Repositoryserver

Der Repositoryserver verwaltet das Repository. Er nimmt Dateien an und legt sie im Repository ab oder er schickt Dateien aus dem Repository an entfernte Rechner. Allerdings bietet er diese Dienstleistungen nur an, er ist unabhängig von der Richtung des Dateitransfers immer der passive Kommunikationspartner. Clientprogramme senden oder empfangen aktiv Dateien, nachdem sie den Namen der Datei nach oben geschildertem Algorithmus bestimmt und die entsprechende Anfrage gesendet haben.

 

3.2.4      Anwendungs-Laufzeitsystem

Neben dem durch die Tupelraumbibliothek ermöglichten Zugriff auf einen globalen Speicher stellt das Anwendungslaufzeitsystem mit dem Evalserver einen Mechanismus zur Verfügung, der es erlaubt eine Funktion auf einem entfernten Rechner aufzurufen.

 

Ein entfernter Funktionsaufruf wird mit Hilfe des LiPS Präprozessors ermöglicht. Hier soll nun nicht jede Aktion des Präprozessors dargestellt werden. Wesentlich ist erstens, dass Anweisungen der Art eval foo(17); in Tupelraumoperationen transformiert werden, die den Auftrag die Funktion foo() mit dem Argument 17 aufzurufen im Tupelraum ablegen und zweitens, dass aus dem Quelltext der verteilten Anwendung der sogenannte Evalserver erzeugt wird, welcher die Aufgabe hat Aufträge zu finden und auszuführen. Diese beiden Aspekte sind Bestandteil der nächsten Abschnitte, zuvor werden aber noch für die Auftragsverwaltung benötigte Tupel vorgestellt.

 

3.2.4.1            Tupel zur Auftragsverwaltung

Dieser Abschnitt stellt die verschiedenen Auftragsverwaltungtupel vor.

 

·        _system_jobs Tupel speichert im Datenteil wie viele Aufträge bereits erteilt wurden.

·        _system_jobs_done Tupel speichert im Datenteil wie viele Aufträge bereits bearbeitet wurden und gibt gleichzeitig die Sequenznummer des nächsten zu bearbeitenden Auftrags vor.

·        _system_job_x Tupel speichert im Datenteil die Sequenznummer der Funktion, die dem Auftrag mit der Sequenznummer x zugeordnet ist.

·        _lips_job_y Tupel speichert im Datenteil die Argumente der Funktion mit der Sequenznummer y

 

Die hier verwendete Sequenznummer der Funktion wird vom Präprozessor vergeben. Ihr ist auch der Typdeskriptor zugeordnet, welcher die Argumente eines solchen Funktionsaufrufs beschreibt.

 

Die Sequenznummer des Auftrags ergibt sich zur Laufzeit der verteilten Anwendung.

 

3.2.4.2            Auftragserteilung mit eval()

Die Anweisung eval foo(17); steht für die Erteilung des Auftrags die Funktion foo() mit dem Argument 17 aufzurufen. Der Präprozessor ersetzt diese ungültigen Anweisungen durch Tupelraumoperationen, die zwei neue Tupel erzeugen. Zuvor wird aber die Sequenznummer des neuen Auftrags ermittelt. Hierzu wird das _system_jobs Tupel extrahiert. Der darin gespeicherte Wert legt die Sequenznummer sa des neuen Auftrags fest. Anschließend wird das Tupel mit einem um eins erhöhten Wert erneut im Tupelraum abgelegt. Der Name des ersten neu erzeugten Tupels wird mit Hilfe der eben ermittelten Sequenznummer zu _system_job_sa gebildet und speichert im Datenteil die Sequenznummer der Funktion foo(). Das zweite Tupel hat den Namen _lips_job_ gefolgt von der Sequenznummer der Funktion foo(), also beispielsweise _lips_job_1 und speichert die Argumente des Funktionsaufrufs, in diesem Beispiel den Wert 17.

 

Nachdem sich die genannten Tupel im Tupelraum befinden gilt der Auftrag als erteilt.

 

3.2.4.3            Auftragsbearbeitung mit dem Evalserver

Der Evalserver ist ein Programm, welches aus dem Quelltext der verteilten Anwendung erzeugt wurde und mit dem Tupelraum der Anwendung verbunden ist. Seine Aufgabe ist es Aufträge im Tupelraum zu finden und auszuführen. Hierzu ermittelt er die Sequenznummer des nächsten zu erledigenden Auftrags, indem das _system_jobs_done Tupel extrahiert wird. Der darin gespeicherte Wert ist die Sequenznummer. Er wird gespeichert und das Tupel wird mit einem um eins erhöhten Wert erneut in den Tupelraum gelegt. Anschließend wird das Tupel mit dem Namen _system_job_ gefolgt von der eben ermittelten Sequenznummer des Auftrags extrahiert. Hierdurch ergibt sich die Sequenznummer der Funktion seq, wodurch auch das Tupel, welches die Argumente der Funktion speichert, _lips_job_seq und der zugehörige Typdeskriptor spezifiziert werden. Nachdem dann die Argumente aus dem Tupelraum gelesen wurden, wird die durch seq spezifizierte Funktion aufgerufen, wobei die Argumente als variable Argumentliste übergeben werden. Anschließend wird eine Checkpointnachricht geschickt und der eben beschriebene Prozess beginnt erneut, solange, bis der Wert des _system_jobs_done Tupel dem Wert des _system_jobs Tupel gleicht.

 

Der LiPS Präprozessor generiert alle hier beschriebenen Tupelraumoperationen und Funktionsaufrufe, sodass der Programmierer von der gesamten Verwaltung entlastet ist.

 

3.3      Zusammenfassung

In diesem Kapitel wurde zunächst untersucht, ob und wie Anweisungen, die in der Programmiersprache C geschrieben sind, nach Java portiert werden können. Hierbei wurden die wichtigsten Unterschiede sowohl auf syntaktischer, als auch auf semantischer Ebene dargestellt und es wurden Hinweise zur Umsetzung sprachspezifischer Konstrukte gegeben. Durch den richtigen Einsatz von Objekten kann jede Funktionalität übertragen werden. Es kommt nicht darauf an den Quelltext zu übersetzen, was begrenzt möglich wäre, sondern das Verhalten des Programms durch die Modellierung von Klassen und Beziehungen abzubilden.

 

Daher wurde im zweiten Teil untersucht, welche Dienstleistungen von welcher Komponente zu welchem Zweck und auf welche Art erbracht werden und welche technischen Voraussetzungen erfüllt sein müssen, um den aus diesen Komponenten bestehenden lipsd überhaupt ablaufen zu lassen. Neben Zugang zu einem IP-Netzwerk werden die Programme ssh und scp benötigt sowie eine Benutzerkennung auf jedem LiPS-Rechner. Die Funktionalitäten der Prozesskommunikation und Prozessmanipulation werden durch Versand, Empfang und Interpretierung verschiedenster Nachrichten erbracht, wobei die IP-Netzwerkdienste TCP und UDP zum Einsatz kommen. Das System-Laufzeitsystem speichert alle Daten, die es zur Erfüllung seiner Aufgaben benötigt, im Tupelraum, sodass jeder Kontrollprozess in der Lage ist, auf alle Daten des Systems zuzugreifen. Hierdurch können die lipsd’s sich gegenseitig überwachen und bei Bedarf entsprechend reagieren.

 

Außerdem wurde die auf speziellen Tupeln basierende Methode der Auftragsbearbeitung vorgestellt, wodurch der verteilte Ablauf eines Programms mit LiPS gesteuert wird.

 

 


4         Modellierung

Dr. Lecter: „First principles, Clarice. Simplicity. Read Marcus Aurelius. Of each particular thing, ask: What is it, in itself, what is its nature...? What does he do, this man you seek?“[14]

 

Modellierung ist die Abbildung der Funktionalität eines Programms in eine Menge von Klassen, die über Schnittstellen miteinander agieren. Das oberste Prinzip sollte hierbei Einfachheit sein! Bei jeder einzelnen Funktionalität frage: Was ist es, in sich selbst...? Wann immer hierbei eine Antwort der Art: „Ein X ist ein...“ gegeben wird, spricht dies für die Modellierung einer Klasse, die X beschreibt, oft sogar für eine Vererbungsbeziehung, dann nämlich wenn X eine Spezialisierung einer bereits bekannten Klasse ist. Einfach bedeutet in diesem Zusammenhang, dass eine Klasse klein und somit überschaubar sein sollte. Wenn X in Wirklichkeit aus Y und Z besteht, so wie ein Auto, welches aus Motor und Karosserie zusammengesetzt ist, sollten auch die Klassen Y und Z modelliert werden und die Klasse X sollte sie verwenden. Dies wird auch Aggregation genannt und beschreibt „hat ein“ - Beziehungen, wie beispielsweise das Auto einen Motor hat. So entstehen immer kleinere Klassen mit geringerer Komplexität, womit die Grundlage für eine stabile Implementierung gelegt ist.

 

Die folgenden Abschnitte beschreiben die Modellierung der Komponenten des JLiPSD. Hierbei orientiert sich der Aufbau an der Reihenfolge der Darstellung in Kapitel 3. Der letzte Abschnitt beschreibt die Modellierung des JLiPSDController, eines Programms, welches die Initialisierung des LiPS Laufzeitsystems übernimmt.

 


4.1      Tupelraumbibliothek

Die Dienstleistungen der Tupelraumbibliothek wurden bereits ausführlich in 2.1.1.2 und 3.2.2 dargestellt. Hier wird nun gezeigt, wie diese Anforderungen umgesetzt wurden.

 

Zuerst wurde die Klasse AbstractMessage zur Beschreibung der Tupelraumnachricht entwickelt. Sie muss mit dem TCP-Netzwerkdienst gesendet und empfangen werden können, das heißt es muss Methoden geben, welche die Umwandlung der Nachricht in ein Bytearray und umgekehrt liefern. Außerdem müssen verschiedene Typen von Tupelraumnachricht modelliert werden, da verschieden Typen von Daten zu transportieren sind.

 

Zur Beschreibung von Tupeln und Templates wurde die Klasse Tuple modelliert. Tuple verfügen genauso wie Tupelraumnachrichten über Methoden, die das Senden und Empfangen unterstützen. Hierdurch können sie problemlos im Datenteil einer Tupelraumnachricht gesendet und empfangen werden.

 

Die Sende- und Empfangskomponente der Tupelraumbibliothek setzt sich aus zwei Klassen zusammen. Die Klasse TupleSpace stellt dem Anwender die Tupelraumoperationen aus Tabelle 3 zur Verfügung. Hierzu benötigt sie unter anderem Methoden, um die Übertragung einer Datei, die Teil eines Tupels sein kann, anzustoßen. Für den Transport von Tupelraumnachrichten wird eine Instanz der Klasse MessageHandler verwendet, welche auch die Liste der Serveradressen verwaltet. Zu einem der in dieser Liste angegebenen Server ist eine Instanz der Klasse MessageHandler immer verbunden, sodass - vorausgesetzt ein Server ist erreichbar - die Kommunikation mit dem Tupelraum bzw. den entsprechenden Messageservern immer möglich ist. Für den Nachrichtentransport werden zwei Methoden zur Verfügung gestellt, die Instanzen der Klasse AbstractMessage senden bzw. empfangen können. Außerdem verwaltet ein MessageHandler die logische Zeit und reagiert auf empfangene Signale.

 

Mit den eben beschriebenen Klassen kann die Tupelraumbibliothek zwar modelliert werden, allerdings birgt dies noch einen wesentlichen Nachteil in sich. Das Testen der Klassen, die einen TupleSpace zur Erbringung ihrer Dienstleistung benötigen ist sehr schwierig. Jede Verwendung der Tupelraumoperationen führt zu einem Verbindungsaufbau, was auch genau so gewünscht ist, allerdings kann dieser nur erfolgreich durchgeführt werden, wenn auch ein Messageserverprozess erreicht werden kann. Ist dies nicht der Fall, wird das Programm beendet und der Testfall wird nicht erreicht. Zum Testen einer Klasse, die einem TupleSpace verwendet müsste also ein Messageserverprozess gestartet werden, was das Testen allein wegen des Zeitaufwands erheblich erschwert. Eine andere Möglichkeit ist, die Implementierung des verwendeten TupleSpace Objekts auszutauschen. Um dies zu ermöglichen wurde die Schnittstelle TupleSpace eingeführt. Diese wird von der tatsächlich eingesetzten Klasse TupleSpaceImpl (vorher TupleSpace) und zusätzlich von verschiedenen anderen Testklassen implementiert. Eine solche Testklasse wird auch Mockklasse oder Stellvertreterklasse (engl. stub) genannt. Sie wird eingesetzt um das Verhalten der jeweiligen Originalklasse zu simulieren und so das Testen einer dritten Klasse zu ermöglichen. Soll beispielsweise eine Methode getestet werden, die im als Parameter übergebenen Tupelraum ein spezielles Tupel sucht und bei Erfolg eine spezielle Aktion anstößt, so ist dies bei Verwendung einer Instanz der „echten“ Klasse TupleSpaceImpl nicht möglich, ohne einen Messageserver zu starten. Durch die Implementierung einer MockTupleSpace Klasse können alle möglichen Verhaltensweisen simuliert werden und die Methode kann vollständig getestet werden. Die Methode stellt hierbei nicht einmal fest, dass eine andere Klasse übergeben wurde, da sowohl Originalimplementierung als auch Mockobjekt zur Schnittstelle TupleSpace passen.

 

Aus dem selben Grund wurde später noch eine zweite Schnittstelle namens TupleSpacePrivateIfc zwischen erster Schnittstelle und Implementierung eingefügt. In dieser Schnittstelle werden Methoden modelliert, die nicht öffentlich erreichbar sein sollen, zu Testzwecken aber mit einer anderen Implementierung versehen werden müssen.

 

Abbildung 10 gibt einen Überblick über die bisher beschriebenen Klassen.

Abbildung 10 Modell der Tupelraumbibliothek

4.1.1      Tupel

Wie in 2.1.1.1 beschrieben besteht ein Tupel aus einem Namen, einem Typdeskriptor und einer im Typdeskriptor beschriebenen Menge von Daten. In C wurden diese als variable Argumentliste an die verschiedenen Tupelraumfunktionen übergeben. Senden und Empfangen eines Tupels als Teil einer Tupelraumnachricht wurde von anderen Funktionen angeboten. In Java wurden die Daten eines Tupels und die Funktionen, die mit diesen Daten arbeiten, zu der Klasse Tuple zusammengefasst. Die variable Menge an Daten wurde hierbei auf ein Objektarray abgebildet. Hieraus resultiert, dass keine eingebauten Basisdatentypen als Teil eines Tuples verwendet werden können, da diese kein Objekt sind. Statt dessen müssen die von Java angebotenen Hüllklassen (engl. wrapper class) verwendet werden. Arrays aus Basisdatentypen allerdings sind Objekte, wodurch hier keine Hüllklassen eingesetzt werden. Beispiel 9 stellt dar, wie ein Tuple erzeugt wird.

 

Tuple t = new Tuple(“Punkt“, “ii“, new Object[]
{new Integer(1); new Integer(17)});

int[] anArray = new int[]{1, 17, 42};
Tuple t2 = new Tuple(“ArrayTuple“,“ai“, anArray);

Beispiel 9 Erzeugung eines Tuple

 

Eine Klasse Template wurde nicht entwickelt, da keine besondere Funktionalität benötigt wurde. Statt dessen kann ein Objekt vom Typ Tuple auch als Template verwendet werden. Die Methoden, die nur ein Tupel, aber kein Template als Parameter erlauben, wie z.B. out(), können mit der Methode boolean isTemplate() erfragen, ob es sich um ein Template handelt und dann entsprechend reagieren.

 

Da der Umgang mit den Hüllklassen für Basisdatentypen aus verschiedenen Gründen umständlich ist, wurden diverse Spezialtupel als Erbe der Klasse Tupel modelliert. Ein einfaches Beispiel ist die Klasse IntegerTuple. Ihre Aufgabe ist es den Wert einer Variablen vom Typ int als Tupel zu verwalten. Hierdurch wurde erreicht, dass der Programmierer kein Objektarray mit einem Integerobjekt und dazu passendem Typdeskriptor erzeugen muss. Dies nimmt ihm die Spezialklasse ab. Er muss zum Schreiben lediglich einen Namen und den Wert angeben, zum Lesen nur den Namen. Beispiel 10 zeigt die Verwendung eines IntegerTuple.

 

import de.tu_darmstadt.lips.com.IntegerTuple;

...
IntegerTuple iTuple = new IntegerTuple( „name“, 17 );

tupleSpace.out( iTuple );

 

iTuple = new IntegerTuple( „name“ );

tupleSpace.in( iTuple );

int vlaue = iTuple.getValue();

Beispiel 10 Verwendung eines IntegerTuple

 

Für die verschiedenen Dienstleistungen des JLiPSD wurden weitere Spezialtupel modelliert, die in Abbildung 11 dargestellt sind. Ihre Verwendung wird im weiteren Teil der Arbeit erläutert.

 

Abbildung 11 Modell der Klasse Tuple und ihrer Erben

 

4.1.2      Tupelraumnachricht

Wie bereits erwähnt muss die Tupelraumnachricht für den Transport mit dem TCP-Netzwerkdienst in ein Bytearray umgewandelt werden. Beim Empfang muss aus einem Bytearray eine Tupelraumnachricht erzeugt werden. Da es zudem Tupelraumnachrichten mit unterschiedlichen Typen von Daten gibt, wurde eine abstrakte Klasse entwickelt, welche die Gemeinsamkeiten aller Tupelraumnachrichten vereint und die Umwandlung jeweils zweistufig implementiert. In der ersten Stufe wird der Kopf der Nachricht umgewandelt - dies kann in der abstrakten Klasse implementiert werden, in der zweiten Stufe, die als abstrakte Methode modelliert wird, wird die Umwandlung des variablen Datenteils der Nachricht verlangt. Abstrakt bedeutet, dass die Signatur einer Methode definiert ist, die definierende Klasse aber keine Implementierung anbietet. Die erbende Klasse wird so gezwungen sie zu implementieren da kein Objekt einer abstrakten Klasse erzeugt werden kann. Ohne Implementierung einer geerbten abstrakt definierten Methode kann nur eine weitere abstrakte Klasse definiert werden. Durch diesen zweistufigen Aufbau ist es möglich den gesamten Sende- und Empfangsvorgang in der abstrakten Klasse zu modellieren. Alle Tupelraumnachrichten werden auf die selbe Art und Weise gesendet und empfangen, die unterschiedliche Behandlung der verschiedenen Datenteile muss in den abstrakten Methoden entsprechend implementiert werden. Die Klasse AbstractTupleMessage wandelt dort beispielsweise ein Tupel in ein Bytearray, eine ClientSetSigMessage wandelt lediglich die vier Byte des Signals um.

 

Die verschiedenen in Abschnitt 3.2.2.2 beschriebenen Typen von Tupelraumnachricht werden jeweils auf eine eigene Klasse abgebildet, wobei weitere abstrakte Klassen in die Vererbungshierarchie einbezogen wurden, sofern Gemeinsamkeiten vereint werden konnten. Beispielsweise haben alle Nachrichten, die ein Tupel oder Temp­late transportieren den Typ AbstractTupleMessage. Abbildung 12 stellt die Klasse AbstractMessage dar, Abbildung 13 zeigt die Vererbungshierarchie der Tupelraumnachrichten.

Abbildung 12 Modell der Klasse AbstractMessage

 

Abbildung 13 Modell der Vererbungshierarchie der Tupelraumnachrichten

 

4.1.3      Verbindungsaufbau

Wie in 3.2.2.1 beschrieben löst eine spezielle Nachricht den Verbindungsaufbau aus. Sie überträgt eine ID und die Adresse, zu welcher sich der Server verbinden soll. Instanzen der Klasse ConnectionRequestMessage, welche von AbstractMessage erbt, verwalten diese Daten und können bei Bedarf gesendet werden. Die Methode connect() in der Klasse MessageHandler hat die Aufgabe die ConnectionRequestMessage zu versenden, die Verbindung des Servers entgegen zu nehmen und die Antwort zu interpretieren. Sollte die Verbindung abbrechen, wählt der MessageHandler eine andere Serveradresse und baut eine neue Verbindung auf.

 

 

4.1.4      TupleSpace

Eine Instanz diese Klasse stellt für den Anwender den Tupelraum dar. Sie bietet ihm die in Tabelle 3 aufgeführten Methoden, um Tupel im Tupelraum zu speichern bzw. aus ihm zu lesen oder zu entfernen. Hierbei wurde jede der Methoden in zwei Versionen modelliert, wobei sich lediglich die Parameterlisten unterscheiden. In der einen Version müssen Name, Typdeskriptor und Objektarray als Parameter übergeben werden, in der anderen Version ein Objekt vom Typ Tuple, wodurch die Verwendung von Spezialtupeln unterstützt wird.

 

Aus dem Tupelraum lesende Methoden müssen dem Anwender die geforderte Antwort liefern und teilweise gleichzeitig  eine Aussage über den Erfolg der Operation treffen. Dies wurde umgesetzt, indem das Liefern der Antwort durch Modifikation der Argumente modelliert wurde. Für die nicht blockierenden Lesemethoden inp() und rdp() kann nun der Rückgabewert für die Erfolgsaussage verwendet werden. Die Modifikation der Argumente bezieht sich auf das Objektarray, welches entweder direkt oder als Attribut eines Tuples bei der Anfrage übergeben wurde. Während des Lesens der Antwort müssen die Arrayfelder, die mit formalen Parametern besetzt sind mit den entsprechenden aktuellen Parametern der Antwort gefüllt werden.

 

Des weiteren bietet der TupleSpace die Möglichkeit SignalListener hinzuzufügen, wobei die tatsächliche Implementierung dieser Methode bei der Klasse MessageHandler vorgesehen ist.

 

Da jede Instanz der Klasse TupleSpace ihren eigenen MessageHandler erzeugt, müssen die hierfür benötigten Daten bereits an den Konstruktor des TupleSpace übergeben werden. Hierzu gehören die Liste der Serveradressen und die ID des Prozesses, der die Verbindung wünscht.

 

Abbildung 14 stellt die Schnittstelle TupleSpace dar, Beispiel 11 zeigt die Verwendung eines TupleSpace.

 

Abbildung 14 Die Schnittstelle TupleSpace

 

import de.tu_darmstadt.lips.com.TupleSpace;
import de.tu_darmstadt.lips.com.Tuple;
public class Foo
{

   public static void foo( TupleSpace paTupleSpace )
   throws IOException

   {

      Object[] loObjects = new Object[2];
      tupleSpace.in( „otto“, „II“, loObjects );
      int loValue1 = ((Integer)loObjects[0]).intValue();
      int loValue2 = ((Integer)loObjects[1]).intValue();
      int x = 0;
      int y = 0;
      // einige Berechnungen mit x und y

      loObjects[0] = new Integer(x);
      loObjects[1] = new Integer(y);
      Tuple t = new Tuple(„anna“, „ii“, loObjects);
      tupleSpace.out( t );

   }
}

Beispiel 11 Verwendung des TupleSpace

 

4.1.5      MessageHandler

Ein Objekt vom Typ MessageHandler verbindet sich mit einem Messageserverprozess und bietet Methoden an, um Nachrichten, Objekte vom Typ AbstractMessage, zu senden bzw. zu empfangen. Des weiteren verwaltet es die logische Zeit und steuert die Signalbehandlung.

 

In den folgenden Abschnitten wird zunächst beschrieben wie der Aufbau der AbstractMessage genutzt wird, um beliebige Erben dieses Typs senden, vor allem aber empfangen zu können. Anschließend wird beschrieben, wie Signalbehandlung und Verwaltung der logischen Zeit modelliert wurden.

 

4.1.5.1            Nachrichtentransport

In diesem Abschnitt wird beschrieben, wie Objekte vom Typ AbstractMessage gesendet und empfangen werden.

 

Senden

Das Senden einer AbstractMessage wird in der Methode void write(AbstractMessage paMessage) modelliert. Um das Bytearray der Nachricht zu erhalten, muss hier die abstrakte Methode getMessageData() der AbstractMessage verwendet werden, wodurch der MessageHandler jede beliebige AbstractMessage senden kann. Zuvor ermittelt der MessageHandler aber noch die zu dieser Nachricht gehörende logische Zeit und speichert diese bei der Nachricht ab. Dies muss hier und genau zu diesem Zeitpunkt geschehen ,da zum Zeitpunkt der Erzeugung der Nachricht noch nicht festgestellt werden kann, wann die Nachricht tatsächlich gesendet wird.

 

Empfangen

Im Gegensatz zum Sendevorgang, wo beliebige Objekte des Typs AbstractMessage gesendet werden können, muss beim Empfangen der Typ der Nachricht beachtet werden. Aus diesem Grund liest und interpretiert der MessageHandler in der Methode AbstractMessage read() den im Kopf der Nachricht übertragenen Typ und ruft dann den dazu passenden Konstruktor auf - die entsprechende Klasse muss also bekannt sein. Der Konstruktor dieser Nachricht ruft zunächst den Konstruktor der AbstractMessage auf. Hier wird die Nachricht in ein Bytearray gelesen und an die abstrakt modellierte Methode zur Initialisierung der Nachricht übergeben.

 

Bevor die empfangene Nachricht zurückgegeben wird, wird das eventuell empfangene Signal vom SignalHandler behandelt.

 

4.1.5.2            SignalHandler

In dieser Klasse wird die Signalbehandlung modelliert, die im Original durch ein Array von Funktionspointern realisiert wurde.

 

Ein Signal ist eine Folge von 32 Bit, in welcher gesetzte Bits das Vorhandensein einer Systemnachricht anzeigen. Zur Speicherung eines Signals wird eine Variable vom Typ int verwendet. Der Index gesetzter Bits gibt hierbei an, welche Systemnachrichten vorliegen, weitere Informationen sind dann im Tupelraum zu finden. Dies macht es erforderlich, jedem möglichen Index das Äquivalent eines Funktionspointers zuordnen zu können, was durch die Modellierung der Schnittstelle SignalListener erreicht wurde. Klassen, die diese Schnittstelle implementieren, müssen die Methode void handleSignal(int paSignal) implementieren. Die Klasse AbstractSignalListener implementiert dieses Interface abstrakt, d.h. die tatsächliche Implementierung wird den Erben vorgeschrieben, und speichert zusätzlich ein TupleSpace Objekt, sodass Erben dieser Klasse Zugang zum Tupelraum haben.

 

Eine Instanz der Klasse SignalHandler verwaltet alle SignalListener und bietet hierzu die Methoden void addSignalListener(Signal­Li­stener paSL) zum hinzufügen und entsprechend void removeSignalLi­stener(SignalListener paSL) zum entfernen von SignalListener Objekten an. Außerdem ist die Methode void handleSignal(int paSignal) als Einstiegspunkt in die Signalbehandlung modelliert. Falls der MessageHandler beim Empfang einer Nachricht ein Signal erhält, kann die Signalbehandlung durch Aufruf dieser Methode angestoßen werden.

 

Abbildung 15 stell den Aufbau der an der Signalbehandlung beteiligten Klassen dar. Für jedes der in 3.2.2.2 beschriebenen Signale wurde eine von AbstractSignalListener erbende Klasse modelliert, welche die dort beschrieben Funktionalität bietet.

Abbildung 15 Modell der Signalbehandlung

 

4.1.5.3            LogicTime

Die logische Zeit wurde mit der Klasse LogicTime modelliert. Sie dient der aufsteigenden Nummerierung der Nachrichten, sodass der Messageserver sie im Nachrichtenlogbuch chronologisch speichern kann. Wenn eine Nachricht mit einer niedrigeren logischen Zeit als der aktuellen an einen Messageserver gesendet wird, liefert er die dazu passende Antwort aus dem Nachrichtenlogbuch. Ausgefallene Prozesse erhalten so nach ihrem Neustart die selben Nachrichten wie zuvor, was dazu führt, dass sie sich identisch verhalten[15].

 

Zur Speicherung der logischen Zeit wird ein 32 Bit großer Datentyp verwendet, in welchem zusätzlich die ID des Absenders kodiert wird. Hierzu werden die Bits 0-9 verwendet, woraus sich ergibt, dass LiPS nicht mehr als 1024 verschiedene ID’s vergeben kann. Für die tatsächliche Nummerierung der Nachrichten stehen die Bits 10-31 zur Verfügung, womit über 4 Millionen Nachrichten nummeriert werden können[16].

 

Der Startwert der logischen Zeit wird entweder im Konstruktor angegeben, wobei es sich dann um einen Neustart eines ausgefallenen Prozesses handelt, oder ist eins. Nachdem eine Nachricht empfangen wurde, wird ihr Wert um eins inkrementiert. Allerdings führen nicht alle Nachrichtentypen zu einer Erhöhung der logischen Zeit. Die erfolglosen Anfragen blockierender Lesemethoden beispielsweise sollen nicht repliziert werden, was dazu führt, dass die logische Zeit erst nach erfolgreicher Anfrage inkrementiert wird. Der empfangene Nachrichtentyp legt also das Verhalten der logischen Zeit fest.

 

Ein weiteres Problem besteht in der Verwaltung der logischen Zeit vor, während und nach der Signalbehandlung, da Nachrichten, die Bestandteil der Signalbehandlung sind, nicht repliziert werden sollen. Signale werden nicht im Nachrichtenlogbuch abgespeichert, da sie jederzeit von einem anderen Prozess gesetzt werden können und sich auf den aktuellen Zustand des Systems beziehen. Während der Signalbehandlung wird die logische Zeit trotzdem erhöht, da dem Fix- oder Messageserver nicht mitgeteilt wird, dass die Signalbehandlung beginnt und dieser deshalb eine aufsteigende logische Zeit erwartet. Wenn die Behandlung eines Signals abgeschlossen ist, wird die logische Zeit durch Versand einer ClientResetSigMessage zurückgesetzt, sodass die zur Signalbehandlung benötigten Nachrichten im Logbuch von folgenden Tupelraumnachrichten überschrieben werden können. Die logische Zeit dieser Nachricht selbst ergibt sich aus der logischen Zeit vor Eintritt in die Signalbehandlung und dem Typ der Nachricht, welche die Signalbehandlung auslöste. Das Verhalten der logischen Zeit wird also nicht nur vom Typ der empfangenen Nachricht bestimmt, sondern auch vom Zustand des MessageHandlers (keine Signalbehandlung, Eintritt in Signalbehandlung, in Signalbehandlung und Ende der Signalbehandlung).

 

Um das Verhalten der logischen Zeit abzubilden wurden drei Methoden modelliert, die alle den Typ der empfangenen Nachricht als Parameter erhalten. Die verschiedenen Methoden sind den Zuständen des MessageHandlers zugeordnet und manipulieren den Wert der logischen Zeit entsprechend. Abbildung 16 stellt die Klasse LogicTime dar.

 

Abbildung 16 Die Klasse LogicTime

 

4.2      System-Laufzeitsystem

In diesem Abschnitt wird zunächst dargestellt wie die Threads StatusUpdater und  DaemonWatcher modelliert wurden, wodurch, wie in 3.2.3 beschrieben, die hohe Verfügbarkeit des System-Laufzeitsystems gewährleistet wird. Hierbei wird auch erklärt, wie der Start eines JLiPSD auf einem entfernten Rechner abläuft. Anschließend wird die Modellierung der Befehlsnachrichtenkomponente vorgestellt.

4.2.1      StatusUpdater und DaemonWatcher

Wie in Abschnitt 3.2.3 dargestellt, werden zwei Threads benötigt, um zu gewährleisten, dass auf jedem LiPS-Rechner ein Kontrollprozess JLiPSD läuft. Das hierbei verwendete DaemonStatusTupel wurde bereits in 3.2.3.1 vorgestellt. In diesem Abschnitt wird nun gezeigt, wie die regelmäßige Ausführung der Threads modelliert wurde.

 

Java stellt zu diesem Zweck die Klasse Timer zur Verfügung. Einer Instanz dieser Klasse können TimerTasks zur regelmäßigen Ausführung übergeben werden, wobei TimerTask eine abstrakte Klasse bezeichnet. Erben von TimerTask müssen die Methode run(), welche vom Timer aufgerufen wird, implementieren. Da beide Threads einen TupleSpace benötigen, wurde eine weitere abstrakte Klasse modelliert, AbstractTupleSpaceTimerTask, welche das Attribut TupleSpace verwaltet. Als konkrete Erben dieser Klasse wurden die Klassen StatusUpdater und DaemonWatcher modelliert. die in den Abschnitten 3.2.3.2 und 3.2.3.3 beschriebenen Algorithmen werden jeweils in der Methode run() implementiert. Abbildung 17 stellt das Modell dar.

Abbildung 17 Modell der regelmäßig ablaufenden Threads

4.2.2      Befehlsnachrichten

In Abschnitt 3.2.3.4 wurde zunächst das einheitliche Format einer Befehlsnachricht vorgestellt, bevor die verwendeten konkreten Nachrichten und ihre Argumente erklärt wurden. Diesem Aufbau folgend wurde die abstrakte Klasse AbstractCommandMessage modelliert, welche die in Abbildung 2 dargestellten Informationen verwaltet und zusätzlich über ein Attribut vom Typ TupleSpace verfügt. Ähnlich wie bei der Klasse AbstractMessage wurde die Initialisierung einer Befehlsnachricht sowohl auf die abstrakte Klasse, als auch auf konkrete Erben verteilt. Zusätzlich wurde die abstrakte Methode String runCmd() modelliert, in welcher Erben den Befehl implementieren müssen. Die erwartete Antwort wird als String zurückgegeben. Für jede der in 3.2.3.4 beschriebenen Befehlsnachrichten wurde ein konkreter Erbe der AbstractCommandMessage modelliert.

 

Bevor im weiteren Verlauf des Abschnitts die Modellierung der Sende- und Empfangskomponenten vorgestellt wird, muss darauf hingewiesen werden, dass lediglich Messageserver eine Antwort auf eine Befehlsnachricht erwarten. Aus diesem Grund wurde der Empfang einer Antwort nicht modelliert. Das vorhandene Modell kann aber leicht so erweitert werden, dass auch der Empfang und die damit verbundene Interpretation einer Antwort möglich ist. Abbildung 18 stellt das Modell der Befehlsnachrichten dar.

 

Abbildung 18 Modell der Befehlsnachrichten

 

4.2.2.1            Senden einer Befehlsnachricht

Die Übertragung einer Befehlsnachricht bzw. einer Antwort kann auf die Übertragung von Text reduziert werden. Da zum Versand allerdings der unsichere Netzwerkdienst UDP verwendet wird, muss ein Protokoll, welches den sicheren Transport der Nachricht garantiert, implementiert werden. Der Beschreibung in [Fis96] folgend wurden daher die in Abbildung 19 abgebildeten Klassen entwickelt.

 

Besonders hervorgehoben werden muss hier die Klasse ReceiveAckRunnable. Sie implementiert die Schnittstelle Runnable, wodurch sie in einem Thread ausgeführt werden kann. Dieser Thread empfängt und verwaltet Quittungsnachrichten. Hierzu greift er gemeinsam mit dem UdpMessageSender auf eine Liste bisher gesendeter, aber noch nicht bestätigter Nachrichten zu. Der Empfang einer Quittungsnachricht führt zur Markierung der zugehörigen Nachricht, wobei lediglich eine gewisse Zeit auf den Empfang einer Bestätigung gewartet wird. Nach Ablauf dieser Zeit wird die Liste der unbestätigten Nachrichten überprüft und der erneute Versand von Nachrichten eingeleitet, falls ihre Bestätigung bereits vor zwei Zyklen hätte eintreffen müssen. Anschließend beginnt der Zyklus wieder, indem auf eine Bestätigung gewartet wird.

 

Abbildung 19 Modell der Sendekomponente für UdpMessages

 

4.2.2.2            Empfangen einer Befehlsnachricht

Der Empfang einer Befehlsnachricht wird zunächst auf den Empfang einer Textnachricht reduziert, was zu dem in Abbildung 20 dargestellten Modell führt.

 

Nachdem eine Nachricht vollständig empfangen wurde, muss der in ihr enthaltene Befehl ausgeführt werden. Hierzu wurde die Klasse UdpMessageInterpreter modelliert, welche die UdpMessage analysiert, die entsprechende AbstractCommandMessage erzeugt und anschließend die Methode runCmd() aufruft. Die evtl. zurückgelieferte Antwort wird dem UdpMessageSender übergeben, welcher den erfolgreichen Versand der Antwort garantiert.

 

Abbildung 20 Modell der Empfangskomponente für UdpMessages

 

4.3      Anwendungs-Laufzeitsystem

Das Anwendungs-Laufzeitsystem hat die Aufgabe die Prozessmanipulation für die verteilte Anwendung zu übernehmen. Da Java eine plattformunabhängige Programmiersprache ist, gewährt sie aber nur rudimentären Zugriff auf Prozesse. Es ist möglich einen schwergewichtigen Prozess zu starten, auf seine Beendigung zu warten oder ihn selbst zu beenden. Ferner kann über die drei Datenströme stdin, stdout u. stderr mit dem Prozess kommuniziert werden. Kommunikation über Signale, wie dies beispielsweise in Unix möglich ist, wird nicht unterstützt. Sicherlich könnte man eine Bibliothek entwickeln, die auch dies für die verschiedenen Betriebssysteme zur Verfügung stellt, doch wiederspricht dies dem Ziel, auf jedem javafähigen Betriebsystem ablaufen zu können. Aus diesem Grund wurde entschieden, Clientprozesse nicht in eigenständigen Prozessen zu realisieren, sondern sie in einem Thread ablaufen zu lassen, welcher vom JLiPSD verwaltet wird. Dieser Thread wird von der Klasse ClientStarter realisiert, siehe hierzu Abschnitt 4.3.4.

 

Um eine in C geschriebene verteilte Anwendung zu starten wird, wie in 2.2.2 beschrieben ein Masterprozess gestartet, welcher sich dann bei dem lokal ablaufenden lipsd anmeldet und den Start der Clientprozesse initiiert. Sowohl Masterprozess als auch Clientprozess verwenden zur Kommunikation mit dem Tupelraum die LiPS - Bibliothek, welche zur Kompilierzeit mit dem Quellcode der verteilten Anwendung zusammen gebunden wird. Damit diese Programme nun mit Java Objekten zusammen arbeiten, muss eine ebenfalls in C geschriebene Bibliothek entwickelt werden, welche die gleichen Funktionen anbietet, diese hinter der Fassade allerdings durch Verwendung des Java Native Interface JNI auf Java Objekte abbildet. Der Quelltext der verteilten Anwendung muss nun lediglich mit dieser neuen JLiPSD Bibliothek zusammen gebunden werden. Außer den bereits bekannten Tupelraumoperationen muss die Bibliothek lediglich die Funktion lips_eval(int direction, int jobID, String typDeskriptor, ...) anbieten, welche Aufträge im Tupelraum ablegt, bzw. aus dem Tupelraum entfernt. Auch diese Funktion wird mit JNI auf Java Objekte abgebildet. Um die Komplexität der Entwicklung aber nicht weiter zu steigern wurde zunächst eine Lösung entwickelt, die es erlaubt Java Programme verteilt ablaufen zu lassen. Auf diese Weise gibt es weniger verschiedene Fehlerquellen, die sich gegenseitig beeinflussen. Erst nachdem dieser Schritt implementiert und getestet ist, kann mit der Entwicklung der Bibliothek begonnen werden.

 

Zur Initialisierung des Masterprozesses gehört nicht nur den Zugang zum Tupelraum herzustellen, sondern auch, die benötigten Programmdateien in das Repository zu kopieren und den Start der Clientprozesse anzustoßen. Für diese Aufgaben wurde die Klasse MasterStarter modelliert, welche nach der Initialisierung den eigentlichen Masterprozess startet.

 

Doch auch ein Javaprogramm kann nicht ohne weiteres verteilt ablaufen. Der folgende Abschnitt beschreibt daher wie eine in Java geschriebene verteilte Anwendung aufgebaut sein muss, damit sie mit LiPS ablaufen kann. Anschließend wird dargestellt wie der Evalserver modelliert wurde, worauf die Darstellung der beiden Starterklassen folgt.

 

4.3.1      Design einer verteilten Anwendung in Java

Eine in Java geschriebene verteilte Anwendung für LiPS kann aus beliebig vielen Klassen bestehen, die, wie in Java üblich, alle in eine Archivdatei gepackt werden müssen. Der Name der Archivdatei ergibt sich aus dem Namen der verteilten Anwendung. Damit ein Programm aus einem Java Archiv heraus ausgeführt werden kann, muss der Name der Hauptklasse (engl. main class) bekannt sein. Aus diesem Grund ist in einem Java Archiv eine sogenannte Manifest Datei enthalten, welche den Namen der Hauptklasse enthält, hier ist die Hauptklasse diejenige, welche den Masterpart implementiert. Der folgende Abschnitt beschreibt, welche Anforderungen sowohl an den Aufbau als auch an das Verhalten der Masterklasse gestellt werden. Anschließend wird der Aufbau der Clientklassen dargestellt.

 

4.3.1.1            Masterklasse

Eine Javaklasse wird dann zu einer Masterklasse, wenn sie die Methode static void masterMain(TupleSpace paTupleSpace, String[] paArgs) implementiert und in der Manifestdatei der JAR Datei als Hauptklasse angegeben ist. Diese Methode wird vom MasterStarter aufgerufen, nachdem er u.a. den Zugang zum Tupelraum initialisiert hat. Durch Übergabe des TupleSpace Objekts hat die Masterklasse Zugang zum Tupelraum, das Stringarray paArgs enthält die Kommandozeilenparameter der verteilten Anwendung.

 

Um einen Auftrag in den Tupelraum zu legen, ist es nötig eine Verbindung zwischen dem Auftrag und einer Methode herzustellen. Im Original wurde dies von dem LiPS Präprozessor erledigt, indem er den Quelltext analysiert und entsprechend verändert. In Java gibt es die sogenannte Reflection API, welche es erlaubt unbekannte Klassen zu untersuchen und zu verwenden. So ist es beispielsweise möglich, Zugang zu den Methoden einer Klasse zu erhalten, die eine spezielle Signatur haben. Genau dies ist in der statischen Methode register(Class paClass) der Klasse EvalServer implementiert. Hier werden aus der übergebenen Klasse alle statischen Methoden herausgesucht und analysiert, die als erstes Argument ein TupleSpace Objekt bekommen - näheres hierzu in 4.3.2. Die sich hieraus ergebende Anforderung an die Masterklasse ist also, dass sie vor dem erteilen eines Auftrags die Klasse registriert, die für die Abarbeitung verantwortlich ist. Es ist auch möglich, mehrere Clientklassen zu registrieren.

4.3.1.2            Client

Um eine Klasse als LiPS Client verwenden zu können, muss sie nur eine Anforderung erfüllen: Es muss mindestens eine Methode geben, die der folgenden Signatur entspricht: static void foo(TupleSpace paTupleSpace, args) throws IOException. Hierbei können beliebig viele weitere Argumente zur Signatur gehören, solange ihre Typen im Typdeskriptor eines Tupels dargestellt werden können. Siehe hierzu Tabelle 6. Der Name der Methode kann frei gewählt werden.

 

Eine Clientklasse kann genauso wie die Masterklasse Aufträge erteilen. Siehe hierzu 4.3.2.1.

 

Zusätzlich muss die Klasse bei einem EvalServer registriert werden, dies liegt allerdings in der Verantwortung der Klasse, die den Auftrag erteilt hat.

 

4.3.1.3            Übersetzung einer verteilten Anwendung

Zur Übersetzung eines Javaprogramms muss der Compiler alle verwendeten Klassen finden. Hierzu verwendet er die Umgebungsvariable CLASSPATH, welche angibt in welchen Verzeichnissen oder Java Archivdateien verwendete Klassen gefunden werden können.

 

Um eine Klasse zu übersetzen, welche die Tupelraumbibliothek verwendet, muss die absolute Pfadangabe zur Datei JLiPSD.jar im CLASSPATH enthalten sein. Falls die verteilte Anwendung an einem Rechner entwickelt wird, auf welchem der Kontrollprozess JLiPSD installiert ist, befindet sich die Datei im Verzeichnis home/JLiPSD/jar/. Ansonsten kann sie aus dem Installationsarchiv JLiPSD_install.jar entpackt und in ein beliebiges Verzeichnis kopiert werden.

 

Nachdem alle Klassen der verteilten Anwendung übersetzt wurden, müssen sie in ein Java Archiv gepackt werden. Der Name der Archivdatei kann frei gewählt werden. Die Manifestdatei muss wie in 4.3.1.1 beschrieben den Namen der Masterklasse enthalten.

Die Archivdatei, welche alle Klassen der verteilten Anwendung enthält muss keine der LiPS-Klassen enthalten, wodurch ein relativ kleines Archiv erzeugt wird, was schnelle Übertragungszeiten mit sich bringt.

 

Die virtuelle Java Maschine verwendet ebenfalls die Umgebungsvariable CLASSPATH um Klassen zu finden. Da ein Masterprozess nur auf einem LiPS-Rechner gestartet werden kann, muss der CLASSPATH hierzu lediglich um home/JLiPSD/jar/JLiPSD.jar erweitert werden. Der Start eines Clientprozesses erfolgt in einem Thread, welcher vom Kontrollprozess JLiPSD gestartet wird, wodurch bereits sichergestellt ist, dass die verwendeten Klassen gefunden werden.

 

4.3.2      EvalServer

Der in 3.2.4.2 und 3.2.4.3 beschriebene Algorithmus der Auftragsverwaltung wird von der Klasse EvalServerPublic implementiert. Bevor allerdings Aufträge erteilt werden können, müssen Informationen über die einzelnen Auftragsarten gesammelt werden. Hierzu dient die Methode static void register(Class paClass). Die übergebene Klasse wird nach statischen Methoden ohne Rückgabewert durchsucht, deren erstes Argument vom Typ TupleSpace ist und deren weitere Argumente in einem Tupel gespeichert werden können. Solchen Methoden wird eine ID zugeordnet und es wird ein Tupel erzeugt, welches ID, Klasse und Name der Methode sowie den für die Speicherung der Argumente benötigten Typdeskriptor verwaltet. Folgendes Beispiel veranschaulicht dies.

 

Beispiel:                                                    

Angenommen es gibt in der Klasse Bar eine Methode static void foo( TupleSpace t, int i) und es ist noch keine Klasse registriert. Bei der Registrierung dieser Klasse wird der Methode die ID 1 zugeordnet und es wird ein JavaJobDescriptionTuple mit dem Typdeskriptor „isss“ im Tupelraum abgelegt, welches die Werte (1, „Bar“, „foo“, „i“) enthält.

Beispiel 12 Registrierung einer Klasse mit dem EvalServer

 

Das JavaJobDescriptionTuple wird sowohl bei der Erteilung als auch bei der Abarbeitung eines Auftrags benötigt. Der original Algorithmus kam ohne diese Tupel aus, da er die hier abgelegten Informationen direkt in den Quellcode generiert.

 

Um die in den folgenden Abschnitten erläuterten Methoden verwenden zu können muss die Klasse de.tu_darmstadt.lips.com.EvalServerPublic importiert werden.

 

 

4.3.2.1            Auftragserteilung

Ein Auftrag wird erteilt, indem die statische Methode eval(TupleSpace t, String paClassName, String paMethodName, String paTypDeskriptor, Object[] paValues) aufgerufen wird. Das TupleSpace Argument ermöglicht dem EvalServer Zugang zum Tupelraum der Anwendung, die drei folgenden String Argumente werden zur Ermittlung der Methoden ID benötigt und das Objektarray enthält die Argumente für den Methodenaufruf. Auf den Typdeskriptor kann hier leider nicht verzichtet werden, da es in Java möglich ist Methoden mit gleichem Namen aber unterschiedlicher Signatur innerhalb einer Klasse zu verwenden. Hieraus ergibt sich das Problem, dass der vom Programmierer angegebene Typdeskriptor zu dem vom EvalServer erzeugten passen muss. Tabelle 6 stellt daher dar, welche Typdeskriptoren den verschiedenen Datentypen zugeordnet sind. Auf vorzeichenlose Datentypen wird hier nicht eingegangen, da in Java alle eingebauten Datentypen vorzeichenbehaftet sind.

 

Typ

Typdeskriptor

byte

c

byte[]

ac

int

i

int[]

ai

long

l

long[]

al

float

f

float[]

af

double

d

double[]

ad

String

s

String[]

as

Tabelle 6 Typdeskriptoren für erlaubte Datentypen bei EvalServerPublic.eval(...)

 

Das JavaJobDescriptionTuple wurde hier zur Ermittlung der Methoden ID benötigt.

 

4.3.2.2            Bearbeitung eines Auftrags

Bei der Abarbeitung eines Auftrags ist zunächst lediglich die Auftragsnummer bekannt. Mit dieser kann die zugeordnete Methoden ID ermittelt werden, welche wiederum benutzt wird, um das passende JavaJobDescriptionTuple zu erhalten. Neben den Informationen, welche Klasse und Methode für diesen Auftrag zuständig sind, erhält der EvalServer so auch den Typdeskriptor, mit welchem die Argumente des Methodenaufrufs aus dem Tupelraum geholt werden können. Mit all diesen Informationen und der Reflection API ist es dann möglich die Klasse zu laden und die Methode mit den Argumenten aufzurufen.

 

4.3.3      MasterStarter

Die Klasse MasterStarter ist ein Programm, welches die Aufgabe hat, den Zugang zum Tupelraum der Anwendung zu initialisieren, den Masterprozess zu starten und den Start der Clientprozesse anzustoßen. Außerdem kopiert es die Programmdateien in das Repository und meldet den Masterprozess bei dem lokalen JLiPSD an. Der MasterStarter wird nur für verteilte Anwendungen benötigt, die in Java geschrieben sind. In C geschriebene Anwendungen werden mit Hilfe des LiPS Präprozessors übersetzt, welcher das Masterprogramm vollständig generiert. Dieses muss lediglich mit der JLiPSD Bibliothek zusammen gebunden werden und kann dann als eigenständiger Prozess ablaufen.

 

Zur Steuerung des MasterStarter müssen folgende Argumente beim Start übergeben werden:

 

·        Name der Jar Datei, welche das Masterprogramm enthält. Die Manifest Datei des Jar - Archivs muss die Masterklasse benennen.

·        Name des Betriebssystems, für den Masterprozess

·        Liste von Betriebssystemen für Clientprozesse - durch Doppelpunkt getrennt und mit Doppelpunkt endend. Beispiel: OS_1:OS_2:..:OS_3:

·        Anzahl zu Startender Evalserver

·        Argumente des Masterprozess

 

4.3.4      ClientStarter

Ein Objekt dieser Klasse wird erzeugt, nachdem eine Befehlsnachricht STARTEN empfangen wurde. Zu diesem Zeitpunkt ist die Umgebung des zu startenden Prozesses bereits initialisiert d.h. alle benötigten Dateien sind vorhanden und ein Arbeitsverzeichnis wurde erzeugt. Der ClientStarter initialisiert nun den Zugang zum Tupelraum der Anwendung und startet einen EvalServer zur Abarbeitung der Aufträge.

 

4.4      JLiPSDController

Im Gegensatz zu den bisher beschriebenen Komponenten des JLiPSD wird der JLiPSDController nicht vom JLiPSD verwendet, sondern stellt ein eigenständiges Programm dar. Neben der Initialisierung des System-Laufzeitsystems gibt er dem Anwender die Möglichkeit, Informationen über den Zustand des Systems zu erlangen. Hierzu wurde eine Befehlsverarbeitungskomponente modelliert, die ähnlich aufgebaut ist, wie die Befehlsnachrichten Komponente. Der Unterschied ist, das Befehlsnachrichten über das Netzwerk empfangen werden, wohingegen Befehle von einem Anwender eingegeben werden.

 

Der JLiPSDController wird aber auch nur zur Initialisierung des System-Laufzeitsystems verwendet. Der nächste Abschnitt zeigt, welche Optionen der Anwender beim Start des JLiPSDController hat. Anschließend wird die zur Initialisierung des System-Laufzeitsystems benötigte CLUSTER Datei vorgestellt, welche alle Informationen zur Initialisierung des System-Laufzeitsystems enthält. Im dritten Abschnitt wird das Modell der Befehlsverarbeitungskomponente erläutert.

 

4.4.1      Start des JLiPSDController

Mit dem Befehl „java de.tu_darmstadt.lips.com.JLiPSDController [-f Dateiname [-c]]“ wird der JLiPSDController gestartet. Die Option -f gefolgt vom Namen einer Datei gibt an, dass die Initialisierung des System-Laufzeitsystems basierend auf dem Inhalt der Datei vorgenommen werden soll. Durch die Option -c wird der JLiPSDController (nach der Initialisierung) in den interaktiven Modus versetzt, wodurch der Anwender die Möglichkeit hat Informationen über den Zustand des Systems zu erlangen. Wird die Option -c  nicht angegeben, beendet sich der JLiPSD­Controller nach der Initialisierung. Wird keine der Optionen angegeben, startet der JLiPSDController im interaktiven Modus.

 

4.4.2      Initialisierung des System-Laufzeitsystems

Die Initialisierung des System-Laufzeitsystems basiert auf dem Inhalt der sogenannten Clusterdatei, deren Name beim Start des JLiPSDController angegeben wird. Diese Datei beschreibt alle zum LiPS System gehörenden Maschinen. Jede Zeile enthält, durch Leerzeichen getrennt, folgende Angaben zu einer bestimmten Maschine:

·        DNS (Domain Name System) der Maschine Beispiel: cdc-ultra3.cdc.informatik.tu-darmstadt.de

·        Internet-Adresse im dotted-quad[17] Format

·        Architekturkürzel

·        Beschreibungstext (darf keine Leerzeichen enthalten)

·        absoluter Pfad des Arbeitsverzeichnisses, welches vom JLiPSD angelegt wird, falls es noch nicht existiert.

·        Login des LiPS-Users@ (absoluter) Pfad zum JLiPSD Programmtext (JAR Datei) für die betreffende Architektur. Beispiel: andreasm@JLiPSD/jar/JLiPSD.jar

·        Name des NFS-Clusters, in dem die Maschine liegt; Hosts, bei denen das Arbeitsverzeichnis auf einem lokalen Dateisystem liegt, bilden jeweils einen eigenen NFS-Cluster

·        Ausdruck zur Ermittlung der Laufzeitberechtigung

 

Kommentarzeilen ( mit # als erstes Zeichen einer Zeile) sind zulässig.

 

Aus der Beschreibung einer Maschine erstellt der JLiPSDController ein DaemonConfigTupel, welches im Tupelraum abgelegt wird. Außerdem wird das DaemonStatusTupel für jede Maschine vorbereitet, wobei der Eintrag für den letzten Aktualisierungszeitpunkt auf den Wert 0 gesetzt wird.. Hierdurch wird erreicht, dass der Start des ersten JLiPSD zum Start aller weiteren führt, da der zugehörige DaemonWatcher erkennt, dass die Frist für die Aktualisierung überschritten ist und einen Neustart einleitet. Außerdem wird die Datei lipsd.pids aktualisiert, indem die Internetadressen neuer LiPS-Rechner an das Ende der Datei angehängt werden. Zusätzlich wird der Inhalt der Konfigurationsdateien lipsd.pids und lips.conf im Tupelraum abgelegt, wodurch jeder Kontrollprozess in der Lage ist, seine Konfigurationsdateien zu aktualisieren. Hierdurch ist es möglich, das System-Laufzeitsystem während der Laufzeit zu erweitern, näheres hierzu findet sich in 6.2.

 

Nachdem die Initialisierung des System-Laufzeitsystems abgeschlossen ist, wird das InitOKTupel im Tupelraum abgelegt. Möglicherweise bereits laufende JLiPSD Prozesse beginnen erst dann mit ihrer Arbeit, wenn dieses Tupel gelesen werden kann.

 

4.4.3      Befehlsverarbeitungskomponente

Abbildung 21 stellt das Modell des Befehlsverarbeitungskomponente des JLiPSDController vor. Sein Aufbau ähnelt dem der Befehlsnachrichtenverarbeitungskomponente aus 4.2.2, allerdings werden Befehle von einem Anwender eingegeben und nicht über das Netzwerk empfangen. Außerdem produziert jeder Befehl eine Antwort, die auf dem Bildschirm ausgegeben wird. Dies macht es erforderlich eine Ausgabeformatierungskomponente zu entwickeln, die es erlaubt einzelne Felder der Ausgabe ein- bzw. auszublenden oder die Länge der Ausgabe einzustellen.

 

Folgende Befehle stehen zur Verfügung:

·        ping IP         Verwendet den Systembefehl ping IP.

·        ps                  Listet alle Anwenderprozesse auf.

·        status           Listet alle im Tupelraum befindlichen DaemonStatusTupel auf.

·        reload          Liest die Clusterdatei erneut und aktualisiert die
                      DaemonStatus- u. DaemonConfigTupel.

·        cfg                Listet alle im Tupelraum befindlichen DaemonConfigTupel auf.

·        set length Feldname Länge       Setzt die Breite der Ausgabe für Feldname                                                     auf Länge Zeichen

·        set field Feldname on/off           Schaltet die Ausgabe für Feldname an
                                                          bzw. aus.

·        set list          Listet alle verfügbaren Feldnamen auf.

·        set print       Listet alle Feldnamen auf, deren Ausgabe aktiviert ist.

·        help              Gibt eine Hilfe zu allen Befehlen aus.

 

Abbildung 21 Modell der Befehlsverarbeitungskomponente, JLiPSDController

 

4.5      Zusammenfassung

In diesem Kapitel wurde das in Kapitel 3 analysierte Verhalten der Komponenten des lipsd auf Javaklassen übertragen. Es wurde eine Tupelraumbibliothek entwickelt, welche sowohl vom Kontrollprozess JLiPSD als auch von der verteilten Anwendung genutzt wird. Teil dieser Bibliothek ist auch die Signalbehandlung und die Verwaltung der logischen Zeit. Die für den Anwender transparente Handhabung verlorener Verbindungen zu Messageserverprozessen ist ebenfalls in der Tupelraumbibliothek modelliert.

 

Um die hohe Verfügbarkeit des System-Laufzeitsystems zu gewährleisten, wurden die Threads StatusUpdater und DaemonWatcher modelliert. Sie nutzen den Tupelraum des Systems zur Speicherung der Verwaltungsdaten und sind so in der Lage von jedem LiPS-Rechner aus das gesamte System zu überwachen. Außerdem wurde eine Komponente zur Verarbeitung von Befehlsnachrichten entwickelt, welche dazu dient Prozesse für das Anwendungs-Laufzeitsystem zu manipulieren.

 

Das Anwendungs-Laufzeitsystem benötigt neben dem Zugriff auf den Tupelraum der Anwendung eine Möglichkeit Aufträge zu erteilen bzw. erteilte Aufträge zu bearbeiten. Hierzu wurden die in 3.2.4 beschriebenen Algorithmen auf die Klasse EvalServer übertragen. Statt eines Präprozessors zur Erzeugung von ausführbaren Master- und Clientprogrammen wurde die Reflection API von Java verwendet um Klassen der verteilten Anwendung zu analysieren und unbekannterweise zu verwenden. Dies führte dazu, dass spezielle Gestaltungsrichtlinien für Javaprogramme, die mit LiPS verteilt ablaufen sollen, aufgestellt wurden. Außerdem wurde beschrieben wie eine verteilte Anwendung übersetzt wird.

 

Das Kapitel endet mit der Modellierung des JLiPSDController, eines Interaktiven Programms zur Initialisierung des System-Laufzeitsystems.

 


5         Implementierung

Für die bisher dargestellten Schritte der Portierung wurde außer einem Texteditor und einem Modellierungswerkzeug kein weiteres Werkzeug benötigt. Dies ändert sich in der Implementierungsphase, die in diesem Kapitel erläutert wird.

 

Zunächst wird das Entwicklungswerkzeug Ant [Ant02] vorgestellt, welches nicht nur zur Übersetzung der Quelltexte dient, sondern auch andere Aufgaben, wie die Installation des JLiPSD, übernimmt. Die hierzu entwickelten sogenannten Ant-Targets (Ant-Ziele) sind in XML formuliert und in der Datei build.xml gespeichert. Sie verwenden Variablen, deren Definition der Datei build.properties entnommen wird. Das Verzeichnis, in welchem sich die erwähnten Dateien befinden wird Projektverzeichnis genannt und enthält auch alle weiteren Dateien des Projekts. Alle folgenden relativen Pfadangaben beziehen sich auf das Projektverzeichnis.

 

Ein weiterer sehr wichtiger Aspekt der Implementierung ist das Erzeugen von Lognachrichten. Gerade bei einem auf mehreren Rechnern eines Netzwerks ablaufendem Programm wird ein Werkzeug benötigt, welches dem Entwickler ermöglicht den Ablauf des Programms zu protokollieren und die dabei anfallenden Informationen zentral zu verwalten. Hierzu wird die Bibliothek Commons [Del01] eingesetzt, die wiederum auf log4j [Gül02] aufsetzt. Neben der Möglichkeit Lognachrichten unterschiedlicher Priorität zu erzeugen kann der Anwender durch Manipulation der Datei redist/log4j.properties einstellen welche Lognachrichten ausgefiltert werden sollen.

 

Der dritte Abschnitt dieses Kapitels erläutert kurz wie getestet wurde und geht auf die sich daraus ergebende Abhängigkeit zur benötigten Bibliothek JUnit ein.

 

Die Installation der Entwicklungsumgebung wird im letzten Abschnitt beschrieben.

 


 

5.1      Ant

Ant ist ein Kommandozeilen basiertes Entwicklungswerkzeug für Java, welches seinen Ursprung in dem Jakarta Projekt von Apache hat und einen Ersatz für das bekannte make [AT91] Werkzeug darstellt. Es steuert die Erzeugung ausführbarer, aber auch anderer Dateien aus den Quelltexten eines Javaprogramms. Inzwischen ist es sowohl bei Apache von Jakarta losgelöst, als auch Quasi-Standard Entwicklungswerkzeug, da es sogar von SUN in vielen Beispielen eingesetzt wird.

 

Der große Vorteil von Ant ist, dass es komplett in Java realisiert ist und somit plattformunabhängig ist.

 

In der Datei build.xml kann der Entwickler verschiedene Ziele (engl. targets) in der Sprache XML definieren, um beispielsweise Quelltext zu übersetzen oder Dokumentation zu erzeugen. Hierzu werden sogenannte Tasks verwendet, deren Implementierung hinter einer Javaklasse verborgen ist. Ant liefert bereits viele solcher Tasks mit, es ist aber auch möglich eigene Tasks zu entwickeln, was in diesem Projekt allerdings nicht benötigt wurde. Hier sei auf die Dokumentation von Ant verwiesen, die sowohl mitgelieferte Tasks vorstellt als auch die Entwicklung eigener Tasks erklärt.

 

Außer den in der Datei build.xml definierten Zielen können auch Variablen verwendet werden, deren Wert aus der Datei build.properties gelesen wird. Hierdurch ist es möglich, Ziele allgemein zu formulieren und in verschiedenen Projekten wieder zu verwenden. Fast die gesamte Datei build.xml kann auf diese Weise in verschiedenen Projekten eingesetzt werden. Lediglich die Angabe des Projektnamens muss angepasst werden, da sie benötigt wird, bevor die Datei mit den Variablendefinitionen gelesen wird.

 

Der folgende Abschnitt stellt zunächst die Datei build.properties vor und erläutert die dort definierten Variablen. Anschließend werden die wichtigsten Ant Ziele erläutert. Eine komplette Auflistung der Ziele wird durch Eingabe von ant -projecthelp ausgegeben.

 

5.1.1      build.properties

In der Datei build.properties, welche sich im selben Verzeichnis befinden muss, wie build.xml, werden Variablen definiert, die von den in build.xml formulierten Zielen verwendet werden. Folgende Liste erläutert alle verwendeten Variablen.

 

Legt die Hauptklasse des Projekts fest (JLiPSD).

In dieses Verzeichnis kann der JLiPSD mit dem Ziel install installiert werden.

Name der verteilten Anwendung

Verzeichnis, in welches die JAR Dateien der verteilten Anwendung kopiert werden. In diesem Verzeichnis wird später der MasterStarter gestartet.

Name der Mainfestdatei, welche in die JAR Datei eingefügt wird und die Hauptklasse des Masterprozesses der verteilten Anwendung spezifiziert.

 

5.1.2      Ant Targets

Ein Ant Ziel legt fest, was aus dem Quelltext erzeugt werden soll, wobei Ziele von anderen Zielen abhängen können. So gibt es beispielsweise das Ziel init, welches u.a. die vom Compiler benötigte Umgebungsvariable CLASSPATH setzt und das Ziel build, welches von init abhängt und den Quelltext übersetzt.

 

Ein Ziel wird ausgeführt, indem ant im Verzeichnis java gestartet wird und der Name des Ziels als Argument übergeben wird. Beispielsweise kann mit ant build die Übersetzung des JLiPSD angestoßen werden.

 

Folgende Liste stellt die wichtigsten Ziele vor, eine komplette Liste kann durch Eingabe von ant -projecthelp im Projektverzeichnis angezeigt werden.

 

Löscht alle durch zuvor ausgeführte Ziele erzeugte Dateien.

Übersetzt alle Java Quelltexte aus dem Verzeichnis src.

Hängt von build ab und erzeugt eine JAR Datei, welche außerdem alle Bibliotheken aus dem Verzeichnis redist enthält. Zur Übersetzung benötigte Bibliotheken die jedoch zur Laufzeit nicht benötigt werden befinden sich im Verzeichnis libs und werden nicht mit eingepackt..

Startet das Programm, welches durch manifest.main.class spezifiziert ist.

Installiert den JLiPSD in dem durch install.directory festgelegten Verzeichnis.

Übersetzt die verteilte Anwendung, erzeugt eine JAR Datei, welche alle benötigten Klassen enthält, und kopiert diese in das durch die Variable sharedApplication.destDir bezeichnete Verzeichnis, wobei sowohl Master- als auch Clientdateien erzeugt werden. Hierbei werden die Variablen sharedApplication.name und sharedApplication.manifest.name verwendet.

 

5.2      Logging

Ein weiterer sehr wichtiger Aspekt der Implementierung ist das Erzeugen von Lognachrichten zur Protokollierung des Programmablaufs. Hier wurde die Bibliothek commons [Del01], ebenfalls aus dem Jakarta Projekt von Apache, eingesetzt. Sie bietet eine einheitliche Schnittstelle zur Erzeugung von Lognachrichten und verwendet selbst entweder die Logging Bibliothek aus dem JDK1.4 oder log4j (Jakarta, Apache). Durch Einsatz dieser Logging Bibliothek ist es auf einfache Art und Weise möglich, die tatsächlich verwendete Bibliothek auszutauschen, die aktuelle Version ist so konfiguriert, dass log4j [Gül02] verwendet wird.

 

Der folgende Abschnitt stellt zunächst die Verwendung der Logging Bibliothek vor. Anschließend wird die Konfiguration erläutert und abschließend wird das Auswertungswerkzeug Chainsaw vorgestellt.

 

5.2.1      Erzeugen von Lognachrichten

Zur Erzeugung einer Lognachricht wird ein Objekt der Klasse org.apache.commons.logging.Log benötigt. Dieses wird von einer Fabrikklasse (engl. factory class) erzeugt, indem die Methode Log org.apache.commons.logging.LogFactory.getLog( String/Class paKategorie ) aufgerufen wird. Das hierbei übergebene Argument ordnet dem Log Objekt eine Kategorie zu, auf die in der Konfigurationsdatei Bezug genommen werden kann. Siehe hierzu Abbildung 23. In diesem Projekt wurde für jede Klasse eine neue Kategorie angelegt.

 

Das Log Objekt stellt die in Abbildung 22 dargestellten Methoden zur Verfügung. Die Methoden der Art boolean is_X_Enabled() liefern true, wenn die mit _X_ dargestellte Log Priorität für diese Kategorie aktiviert ist, sonst false. Die Methoden trace(), debug(), info(), warn(), error() und fatal() erzeugen Lognachrichten der entsprechenden Priorität aus dem übergebenen Objekt, indem dessen toString() Methode aufgerufen wird. Die Methode trace() erzeugt Lognachrichten der niedrigsten Priorität, Ausgaben, die mit fatal() erzeugt werden haben die höchste Priorität. Falls zusätzlich ein Argument vom Typ Throwable übergeben wird, enthält die Lognachricht auch den Aufrufstapel (engl. call stack), wodurch protokolliert wird, wie der Prozess zu der entsprechenden Stelle gekommen ist.

 

In den allermeisten Fällen ist das an die Logmethoden übergebene Objekt ein String, der durch zusammenfügen mehrerer Teilstrings und anderer Werte erzeugt wird. Diese Stringoperationen sind sehr aufwendig und verbrauchen daher viel Rechenzeit. Ihre Ausführung kann im Fall einer deaktivierten Log Priorität verhindert werden, indem zuvor geprüft wird, ob die entsprechende Priorität aktiviert ist. Beispiel 13 stellt eine typische Loganweisung dar.

 

if(log.isInfoEnabled())

log.info(“eine Lognachricht: “ + foo.getSomething()

+ “, “ + bar.getAnotherThing());

Beispiel 13 Eine typische Loganweisung

 

Auf diese Weise werden die Stringoperationen nur dann ausgeführt, wenn sie auch benötigt werden.

 

boolean isTraceEnabled()

boolean isDebugEnabled()

boolean isInfoEnabled()

boolean isWarnEnabled()

boolean isErrorEnabled()

boolean isFatalEnabled()

void trace(Object o)

void trace(Object o, Throwable t)

void debug(Object o)

void debug(Object o, Throwable t)

void info(Object o)

void info(Object o, Throwable t)

void warn(Object o)

void warn(Object o, Throwable t)

void error(Object o)

void error(Object o, Throwable t)

void fatal(Object o)

void fatal(Object o, Throwable t)

Abbildung 22 Methoden der Klasse Log

5.2.2      Konfiguration

Das Verhalten der Logging Bibliothek richtet sich nach dem Inhalt der Datei resources/log4j.properties. Eine komplette Darstellung der Konfigurationsmöglichkeiten würde den Rahmen dieser Arbeit sprengen, hier werden lediglich die wichtigsten Aspekte kurz beleuchtet. In [Gül02] wird ausführlich erklärt, wie die Logging Bibliothek verwendet wird.

 

Die in Abbildung 23 dargestellte Datei resources/log4j.properties besteht aus zwei Teilen: Im ersten Teil wird spezifiziert, wie Lognachrichten mit sogenannten Appendern weiterverarbeitet werden, im zweiten Teil werden Filter für Lognachrichten definiert.

 

log4j.rootLogger=debug, CH_CLIENT, FileApp, stdout

 

log4j.appender.CH_CLIENT=org.apache.log4j.net.SocketAppender

log4j.appender.CH_CLIENT.RemoteHost=130.83.242.199

log4j.appender.CH_CLIENT.Port=4445

log4j.appender.CH_CLIENT.LocationInfo=true

 

log4j.appender.stdout=org.apache.log4j.ConsoleAppender

log4j.appender.stdout.layout=org.apache.log4j.PatternLayout

 

log4j.appender.FileApp=org.apache.log4j.RollingFileAppender

log4j.appender.FileApp.File=index.html

log4j.appender.FileApp.MaxFileSize=1000KB

log4j.appender.FileApp.MaxBackupIndex=3

log4j.appender.FileApp.append=false

log4j.appender.FileApp.layout=org.apache.log4j.HTMLLayout

 

# -----------------------------------------------------------

 

log4j.logger.de=ERROR

log4j.logger.de.tu_darmstadt.lips.com=DEBUG

log4j.logger.de.tu_darmstadt.lips.com.EvalServer=DEBUG

log4j.logger.de.tu_darmstadt.lips.com.AbstractStarter=DEBUG

log4j.logger.de.tu_darmstadt.lips.com.MasterStarter=DEBUG

log4j.logger.de.tu_darmstadt.lips.com.ClientStarter=DEBUG

log4j.logger.de.tu_darmstadt.lips.com.DaemonWatcher=WARN

Abbildung 23 Die Datei resources/log4j.properties

 

5.2.2.1            Verarbeitung von Lognachrichten

Zur weiteren Verarbeitung von Lognachrichten werden sogenannte Appender verwendet. Appender legen fest, wie Lognachrichten verarbeitet werden. Es gibt Appender, die Lognachrichten in eine Datei schreiben (RollingFileAppender), wobei man dies so konfigurieren kann, dass ältere Logdateien umbenannt werden und somit auch nach einem erneuten Programmlauf zu Vergleichen herangezogen werden können. Des weiteren kann die maximale Größe der Datei angegeben werden. Ein anderer Appender ist der sogenannte SocketAppender. Mit ihm ist es möglich Lognachrichten an eine spezifizierte Internetadresse zu versenden. Dieser Appender ermöglicht es alle Lognachrichten einer verteilten Anwendung an einen Rechner zu senden und dort zentral auszuwerten. Die Angabe der empfangenden Internetadresse erfolgt durch folgende Anweisungen, wobei die Angabe des Ports nur dann wichtig ist, wenn der Empfänger Lognachrichten an einem anderen, als dem Standardport erwartet.

 

log4j.appender.CH_CLIENT.RemoteHost=127.0.0.1

log4j.appender.CH_CLIENT.Port=4445

Beispiel 14 Angabe der IP Adresse des Log-Empfängers in log4j.properties

 

Neben der Spezifikation der Appender muss auch ein Layout spezifiziert werden, welches die Lognachricht formatiert. Im Falle des RollingFileAppenders wurde beispielsweise ein Html Layout verwendet, wodurch erreicht wurde, dass die Logdatei in einem Browser dargestellt werden kann.

 

Ausführliche Informationen zur Konfiguration der log4j Bibliothek können bei [Gül02] bezogen werden.

 

5.2.2.2            Filterung von Lognachrichten 

Im zweiten Teil der Konfigurationsdatei können Lognachrichten herausgefiltert werden, deren Priorität eine der Kategorie zugeordnete Grenze unterschreitet. Die Anweisung log4j.logger.de.tu_darmstadt.lips.com.DaemonWatcher = WARN beispielsweise filtert alle Lognachrichten der Kategorie DaemonWatcher, deren Priorität kleiner als WARN ist, heraus. Diese Prüfung wird lokal vorgenommen, sodass bei Einsatz des SocketAppenders auf diese Weise auch die Netzlast reduziert wird.

 

5.2.3      Auswertung von Lognachrichten mit Chainsaw

Chainsaw [Bur01] ist ein Werkzeug, welches ebenfalls aus dem Jakarta Projekt von Apache stammt und dazu dient Lognachrichten zu empfangen und darzustellen. Es verfügt über eine grafische Benutzungsoberfläche, welche Lognachrichten tabellarisch darstellt und außerdem verschiedene Filterfunktionen anbietet. So kann man beispielsweise alle Lognachrichten ausfiltern, die nicht zu einer speziellen Kategorie gehören. Die Bedienung diese Werkzeugs erfolgt intuitiv und gerade die Filterfunktionen helfen dabei, ein spezielles Thema zu verfolgen.

 

Mit Chainsaw können auch Lognachrichten der Messageserverprozesse dargestellt werden. Hierzu muss das Programm SysLogDaemon auf jedem Rechner gestartet werden, auf dem ein Messageserver abläuft. Der Start erfolgt mit dem Befehl folgend dargestellten Befehl:

java -classpath home/JLiPSD/jar/JLiPSD.jar de.tu_darmstadt.li­ps.SysLogDaemon

 

Der Start von Chainsaw erfolgt durch den Befehl

java -classpath projectDir/java/redist/log4j-1.2.7.jar org.apache.log4j.chainsaw.Main, falls die Entwicklungsumgebung installiert ist, oder durch java -classpath log4jDir/dist/lib/log4j-1.2.7.jar org.apache.log4j.chainsaw.Main, falls log4j in log4jDir in der Version 1.2.7 installiert ist. Gegebenenfalls ist die Versionsnummer in dem dargestellten Befehl zu aktualisieren.

 

5.3      Testen

Um Software zu entwickeln, die keine bzw. möglichst wenig Fehler enthält, ist es unbedingt notwendig sie zu testen. Aus diesem Grund wurde in der Diplomarbeit von Herrn Jochen Hähnle ein Testframework entwickelt und erfolgreich angewendet, siehe hierzu [Häh02].

 

Da die Testfälle als interne Klassen modelliert sind, kann der JLiPSD nur übersetzt werden, wenn auch die (nur für Testfälle benötigte) Bibliothek JUnit [Bec02] zur Verfügung steht. Sie ist im Verzeichnis libs zufinden.

 

5.4      Installation der Entwicklungsumgebung

Die Entwicklungsumgebung des JLiPSD kann durch entpacken der Datei JLiPSD_develop.jar installiert werden. Der hierzu benötigte Befehl lautet jar -xf JLiPSD_develop.jar und führt zur Erzeugung des Projektverzeichnis inklusive aller benötigter Dateien und Quelltexte. Außerdem muss das Entwicklungswerkzeug ant installiert werden. Es kann bei http://ant.apache.org heruntergeladen werden, wo auch die Installation beschrieben wird. Bevor ant dann, wie in 5.1.2 vorgestellt, verwendet werden kann, muss die Datei java/build.properties, wie in 5.1.1 erläutert, angepasst werden. Alle erwähnten Bibliotheken befinden sich im Verzeichnis java/libs oder java/redist. In der Datei java/build.xml wird der Classpath so gesetzt, dass bei Einsatz von ant alle referenzierten Klassen gefunden werden. Die zur Laufzeit benötigten Bibliotheken aus dem Verzeichnis java/redist werden in die JLiPSD-Archivdatei einbezogen, sodass die Installation weiterer Bibliotheken nicht nötig ist, um den JLiPSD aus der Archivdatei heraus zu starten. Um die verwendeten Bibliotheken zu aktualisieren, genügt es, die in den genannten Verzeichnissen befindlichen Archivdateien auszutauschen. Aktuelle Versionen der Bibliotheken können bei folgend aufgezählten Internetadressseen bezogen werden.

 

 

5.5      Zusammenfassung

In diesem Kapitel wurde die Entwicklungsumgebung des JLiPSD vorgestellt. Sie besteht aus dem Entwicklungswerkzeug Ant, welches zur Übersetzung des Quelltextes dient und der Logging Bibliothek commons, welche so konfiguriert ist, dass log4j verwendet wird. Die Konfiguration von log4j, die in der Datei resources/log4j.properties editiert werden kann wurde ebenfalls erläutert. Bei dem überaus wichtigen Thema des Software Testens wurde auf die zur Kompilierzeit benötigte Bibliothek JUnit hingewiesen. Das von Jochen Hähnle im Rahmen seiner Diplomarbeit entwickelte Testframework für den JLiPSD wurde erfolgreich angewandt und gewährleistet so die Qualität des Kontrollprozesses. Abschließend wurde beschrieben, wie die Entwicklungsumgebung installiert wird.

 

 


6         JLiPSD

Nachdem in den vorigen Kapiteln dargestellt wurde, wie der lipsd nach Java portiert wurde, geht dieses Kapitel auf die Verwendung des JLiPSD ein.

 

Im ersten Abschnitt wird die Installation und Konfiguration des JLiPSD beschrieben, der zweite Abschnitt geht auf den Start des JLiPSD ein. Hierbei wird dargestellt, unter welchen Bedingungen der erfolgreiche Start möglich ist und welche Aktionen vom Anwender durchgeführt werden müssen. Außerdem wird beschrieben, wie der automatische Start des JLiPSD abläuft. Der letzte Abschnitt beschreibt, wie der JLiPSD beendet wird.

 


6.1      Installation

Die Installation des JLiPSD erfolgt, indem die Datei JLiPSD_install.jar in das Homeverzeichnis des Anwenders kopiert und mit dem Befehl jar -xf JLiPSD_install.jar ausgepackt wird. Der Befehl jar ist Teil des Java Development Kit, welches in der Version 1.4 oder höher installiert sein muss. Außerdem wird, wie in 3.2.1 beschrieben der Netzwerkdienst ssh benötigt, welcher so konfiguriert sein muss, dass keine Passworteingabe benötigt wird, um eine Verbindung zu einem entfernten Rechner aufzubauen.

 

Nach dem Auspacken der Datei JLiPSD_install.jar befinden sich die Verzeichnisse lips und JLiPSD im Homeverzeichnis des Anwenders. Das Verzeichnis lips/etc enthält die Konfigurationsdateien CLUSTER, lipsd.pids, lips.conf, und .MSGSERVER, welche vom Anwender teilweise editiert werden müssen. Folgende Liste erläutert den Inhalt der Dateien:

 

 

Nach erfolgter Installation muss die Datei JLiPSD_install.jar im Homeverzeichnis verbleiben, da sie für die automatische Installation auf einem entfernten Rechner benötigt wird. Die enthaltenen Konfigurationsdateien werden vor der Übertragung auf einen anderen Rechner aktualisiert, sodass kein weiterer Schritt nötig ist, um den JLiPSD auf allen LiPS-Rechnern zu installieren. Nachdem das System-Laufzeitsystem gestartet und mit dem JLiPSDController initialisiert ist, führt der Start des eben installierten JLiPSD automatisch zur Verteilung des Kontrollprozesses auf alle konfigurierten LiPS Rechner. Der Fortschritt kann mit dem JLiPSDController und dem Kommando status beobachtet werden.

 

6.2      Start

Der Start des JLiPSD erfolgt durch Eingabe des Befehls java -jar <home>/JLiPSD/jar/JLiPSD.jar. Zuvor muss allerdings mindestens ein Fixserver gestartet sein, da sich der JLiPSD beendet, falls kein Verbindung zu einem Fixserver aufgebaut werden kann. Wie in 4.4.2 beschrieben, wartet der JLiPSD nach erfolgreicher Verbindung zu einem Fixserver auf ein Tupel, welches anzeigt, dass die Initialisierung des LiPS System-Laufzeitsystems abgeschlossen ist. Um dieses zu erreichen muss der JLiPSDController gestartet werden. Hierbei spielt es keine Rolle, in welcher Reihenfolge die beiden Programme gestartet werden. Der Start des JLiPSDControllers erfolgt wie in 4.4.1 beschrieben.

 

Nachdem der JLiPSDController das System-Laufzeitsystem initialisiert hat und der erste Kontrollprozess gestartet wurde, erkennt der DaemonWatcher, dass der Start weiterer JLiPSD Prozesse benötigt wird. Nun werden zunächst die Konfigurationsdateien lipsd.pids, lips.conf und CLUSTER aktualisiert, indem deren Inhalte aus dem Tupelraum gelesen werden. Der JLiPSDController hat sie bei der Initialisierung des System-Laufzeitsystems dort abgelegt. Anschließend werden die Konfigurationsdateien mittels scp zu dem entfernten Rechner kopiert, bevor mittels ssh versucht wird, den Kontrollprozess zu starten. Ist er bereits installiert, verfügt er nun über aktuelle Konfigurationsdateien, ist er noch nicht installiert, wird eine automatische Installation auf dem entfernten Rechner vorgenommen, indem das Kopieren und Auspacken der Datei JLiPSD_install.jar mittels scp und ssh vorgenommen werden. Anschließend wird der Start ein zweites mal initiiert, wobei die aktuellen Konfigurationsdateien zuvor erneut übertragen werden, da sie durch die Installation überschrieben wurden. Der Fortschritt des Startvorgangs kann beobachtet werden, indem der Befehl status im JLiPSDController ausgeführt wird.

 

Durch diese Art der automatischen Installation kann das System-Laufzeitsystem während der Laufzeit erweitert werden, indem lediglich die Clusterdatei editiert und mittels des JLiPSDControllers neu eingelesen wird.

 

Sobald auf allen Rechnern, die als MessageServer in der Datei .MSGSERVER eingetragen sind, ein JLiPSD läuft, können die MessageServer Prozesse gestartet werden. Diese Reihenfolge ist nötig, da der MessageServer selbständig den Start eines lipsd Prozesses einleitet, falls er keine Verbindung zu dem Kontrollprozess aufbauen kann. Durch Anpassung des Programms MessageServer kann erreicht werden, dass diese Bedingung entfällt.

 

Nach dem Start der MessageServer Prozesse sind die Vorbereitungen für den Start einer verteilten Anwendung abgeschlossen.

 

6.3      Ende

Die Beendigung eines JLiPSD Prozesses ist komplizierter als der Start, da das System-Laufzeitsystem einen fehlenden JLiPSD erkennt und seinen erneuten Start automatisch veranlasst. Falls es trotzdem gewünscht ist einzelne Kontrollprozesse abzuschalten, kann dies erreicht werden, indem die entsprechende Zeile in der Datei CLUSTER auskommentiert und der JLiPSDController erneut gestartet wird. Dies führt dazu, dass der Status des entsprechenden JLiPSD auf unavailable gesetzt wird, was dieser bei seinem nächsten Statusupdate feststellt und sich daraufhin beendet.

 

Zum beenden des gesamten LiPS Systems, müssen lediglich die Fix- und MessageServer Prozesse beendet werden. Die Kontrollprozesse beenden sich, wenn die Verbindung zu einem Fixserver abbricht und nicht wieder aufgebaut werden kann. Eventuell laufende Clientprozesse werden automatisch mit beendet. Ein eventuell laufender Masterprozess beendet sich ebenfalls, falls die Verbindung zu einem Messageserver abbricht.

 

Eine letzte Möglichkeit einzelne JLiPSD zu beenden besteht darin, die ihnen zugeordnete Lockdatei zu löschen. Sie hat den Zweck sicherzustellen, dass auf einem Rechner lediglich ein Kontrollprozess abläuft, indem beim Start des JLiPSD überprüft wird, ob sie bereits existiert. Falls ja, führt die zum Abbruch des Starts, falls nein, wird sie erzeugt und enthält die IP-Adresse des Rechners im Namen, also beispielsweise .jlipsd_IP_ADRESSE.lock. Während des Ablauf überprüft der JLiPSD in regelmäßigen Abständen die Existenz dieser Datei. Wird sie gelöscht, führt dies zur Abbruch des JLiPSD. Dieser Abbruch wird allerdings erkannt und das System-Laufzeitsystem leitet den erneuten Start ein.

 

6.4      Zusammenfassung

Dieses Kapitel stellte das Ergebnis der Portierung des lipsd, den JLiPSD vor. Es wurde beschrieben, wie er installiert und konfiguriert wird. Außerdem wurde beschrieben, wie mit dem Start eines einzelnen JLiPSD die automatische Installation auf allen konfigurierten LiPS-Rechnern vorgenommen wird. Der Start der so installierten Kontrollprozesse folgt ebenfalls automatisch. Abschließend wurde beschrieben, wie einzelne Kontrollprozesse oder das gesamte LiPS Laufzeitsystem beendet werden können.

 


7         Zusammenfassung und Ausblick

Die vorliegende Portierung des Kontrollprozesses lipsd nach Java ermöglicht die Beteiligung aller javafähigen Computer an einer mit LiPS verteilten Berechnung. Hierdurch wird die Menge der zur Verfügung stehenden LiPS Rechner erheblich erhöht. Außerdem ist nun die Entwicklung einer verteilten Anwendung in Java durch Bereitstellung der Tupelraumbibliothek und Vorgabe spezieller Gestaltungsrichtlinien möglich. Aufgrund der Plattformunabhängigkeit von Java Programmen muss kein weiterer Aufwand betrieben werden, um die verteilte Anwendung an das jeweilige Betriebssystem anzupassen, wodurch die Entwicklung einer verteilten Anwendung stark vereinfacht wurde.  Dem Anwender wird ein fehlerfrei arbeitendes Netzwerk präsentiert, da auftretende Fehler vom LiPS Laufzeitsystem erkannt und für den Nutzer nicht bemerkbar behoben werden.

 

Mit dem Programm JLiPSDController wurde das Werkzeug zur Initialisierung und Beobachtung des LiPS Laufzeitsystems portiert, wodurch es möglich ist, die Administration des LiPS Laufzeitsystems von jedem LiPS Rechner aus vorzunehmen.

 

Durch die Entwicklung des Programms SysLogDaemon ist es möglich Lognachrichten der Messageserverprozesse so zu transformieren, dass sie genauso wie die Lognachrichten des JLiPSD, von Chainsaw dargestellt werden können. Hierdurch ist eine zentrale Verarbeitung von Lognachrichten möglich.

 

An der Entwicklung einer Bibliothek, welche den Ablauf von in C geschriebenen LiPS  Programmen mit Unterstützung des JLiPSD ermöglicht wird zur Zeit noch gearbeitet. Doch auch wenn diese Bibliothek fertig gestellt ist, sind noch nicht alle Probleme gelöst. Die folgenden Abschnitte stellen die Probleme kurz vor und modellieren einen Ansatz zu deren Lösung.


7.1      Messageserver

Die Fix- und MessageServer Prozesse sind zur Zeit nur auf Unix Betriebssystemen lauffähig und erfordern eine umfangreiche Konfiguration. Eine Portierung des Serverprogramms würde, genau wie die Portierung des lipsd, eine große Zahl weiterer Rechner dazu befähigen, verteilte Berechnungen zu unterstützen. Hierbei könnten viele Klassen des JLiPSD wiederverwendet werden, wodurch die Portierung vereinfacht wird. Das Argument die Performanz von Java sei nicht ausreichend, um einen MessageServer Prozess zu realisieren, sollte die Portierung nicht verhindern. Einerseits wird die Performanz von Java sehr stark von der Weiterentwicklung der Hardware beeinflusst, andererseits wird auch Java selbst immer weiter entwickelt. Der Einsatz von Just in Time Compilern ist beispielsweise heute schon Realität, an der  Entwicklung von Prozessoren, welche die virtuelle Java Maschine implementieren wird noch gearbeitet. Außerdem überwiegen die Vorteile der besseren Wartbarkeit objektorientierter Software.

 

7.2      LogicTime

Da 10 Bit der in 32 Bit gespeicherten logischen Zeit für die Übermittlung der ID des Prozesses verwendet werden, kann LiPS nicht mehr als 1024 Prozesse verwalten. Bei der eventuell stattfindenden Portierung des Messageserverprozesses könnte das Protokoll zum Austausch von Tupelraumnachrichten so geändert werden, dass mehr Platz für die Übertragung der ID reserviert wird und somit eine größere Zahl von Rechnern an der verteilten Berechnung teilnehmen kann.

 

7.3      Sicherheit

In der vorliegenden Version ist die gesamte Kommunikation zwischen den verschiedenen Prozessen unverschlüsselt, wodurch Angreifer die Möglichkeit haben das System abzuhören und so beispielsweise unberechtigt an das Ergebnis einer verteilten Berechnung zu gelangen. Dies könnte verhindert werden, indem die gesamte Kommunikation verschlüsselt wird, wobei zu beachten ist, dass beide Kommunikationspartner das selbe Protokoll implementieren müssen. In Java wäre ein einfacher Weg, spezialisierte Ein- und Ausgabeströme zu entwickeln und diese für die Kommunikation zu verwenden. Die erforderlichen Änderungen am Quelltext wären minimal, da lediglich einige Konstruktoraufrufe geändert werden müssten. Der Änderungsaufwand für den Messageserver kann hier nicht abgeschätzt werden, sollte er aber nach Java portiert werden, genügt es die bereits erwähnten spezialisierten Ein- und Ausgabeströme auch hier zu verwenden.

 

7.4      SSH

In der aktuellen Version des JLiPSD wird ssh zu Installation und Start des Kontrollprozesses eingesetzt. Die hierfür benötigte Nutzerkennung birgt allerdings Sicherheitsrisiken, da sie auch zum Start anderer Programme missbraucht werden kann. Ohne ssh ist es allerdings nicht möglich den Kontrollprozess auf einem entfernten Rechner zu starten bzw. zu installieren. Hieraus folgt, dass die Installation von einem Anwender vorgenommen werden müsste. Außerdem müsste sicher gestellt werden, dass der Kontrollprozess bei Start des Betriebssystems gestartet wird und auch dann läuft, wenn kein Fixserver erreicht werden kann. Durch Vorgabe eines Prüfintervalls kann dann erreicht werden, dass in regelmäßigen Abständen versucht wird einen Fixserver zu erreichen. Hierfür würden nur geringe Ressourcen benötigt und der Anwender wäre bei seiner eigentlichen Tätigkeit nicht beeinflusst.

 

7.5      Load balancing

In der aktuellen Version des Maserstarters spezifiziert der Anwender die Anzahl der gewünschten EvalServer. Wenn nicht genug LiPS Rechner zur Verfügung stehen, ist das System-Laufzeitsystem während des gesamten Ablaufs der verteilten Anwendung damit beschäftigt den Start eines Evalservers zu versuchen, da der MessageServer dies immer wieder veranlasst. Stehen mehr LiPS Rechner zur Verfügung, wird die vorhandene Rechenleistung nicht adäquat ausgenutzt. Hier wäre es denkbar, einen Algorithmus zu entwickeln, der sowohl die Anzahl noch nicht bearbeiteter Aufträge als auch die Anzahl zur Verfügung stehender LiPS Rechner mit einbezieht und hieraus ermittelt, wann der Start eines weiteren Evalservers vorteilhaft wäre. Der Quotient aus beiden genannten Größen liefert die Anzahl von Aufträgen pro EvalServer. Wird dieser Wert größer, als ein vom Anwender vorgegebener Schwellenwert, führt dies zum Start eines weiteren Evalservers.


8         Lieraturverzeichnis

 


[Fis96]     J. Fischer: Integration von Ebene-1-Softwarefehlertoleranz in LIPS, Diplomarbeit, TU-Darmstadt, Lehrstuhl Prof. Buchmann, 1996.

 

[ACG86] Ahuja S., Carriero N. und Gelernter D., Linda and Friends. IEEE Computer, 1986.

 

[Set98]    Dr. Setz. T., LiPS Manual Version 2.5, TU Darmstadt, Lehrstuhl Prof. Buchmann, 1998.

 

[Brü01]    Prof. B. Brügge Ph. D.: Einführung in die Informatik II Vergleich von Programmierstilen und abschließende Bemerkungen, Institut für Informatik TU München, 2001.

 

[Eck00]   B. Eckel, Thinking in C++ Second Edition, Prentice Hall 2000.

 

[Ant02]    S. Bailliez et al., Apache Ant 1.5.1 Manual, Apache Software Foundation, 2002.

[Del01]    M. Delagrange et al., Jakarta Commons, Apache Software Foundation, 2001.

 

[Gül02]    C. Gülci, Short introduction to log4j, Apache Software Foundation, 2002.

 

[AT91]     A. Oram und S. Talbott, Managing projects with make, O’Reilly Associates, Inc., 1998.

 

[Bur01]    O. Burn, Chainsaw Home Page, http://logui.sourceforge.net/, 2002.

 

[Häh02]   J. Hähnle, Design und Entwicklung eines Testframeworks für JLiPSD, Diplomarbeit, TU-Darmstadt, Lehrstuhl Prof. Buchmann, 2002.

 

[Bec02]          K. Beck und E. Gamma, JUnit - Unit testing, http://www.junit.org, 2002


 



[1] Ein verteiltes System ist eines, dass dich davon abhält irgend eine Arbeit zu beenden, wenn eine Maschine, von der du noch nie gehört hast, zusammenbricht. Lesley Lamport

[2] Die Tupelraumbibliothek ist die Bibliothek, welche Anwendungen Zugang zu einem Tupelraum ermöglicht.

[3] Die Kontrollprozesse lipsd bilden zusammen mit den Prozessen, die den Tupelraum des Systems verwalten das System-Laufzeitsystem.

[4] Ein anderer Prozess müsste es mit out() erzeugen.

[5] lang, kurz, vorzeichenbehaftet und vorzeichenlos

[6] Die Angabe eines dieser beiden Bezeichner ist zwar optional, allerdings schreibt die Spezifikation nicht vor, wie das erste Bit behandelt werden soll, falls kein Bezeichner angegeben wird.

[7] Ein Objekt, welches zur Verarbeitung großer Zahlen dient. Anstelle von Operatoren stehen Funktionen zur Verfügung.

[8] laut [3] üblicherweise Flieskommazahlen einfacher Genauigkeit nach IEEE.

[9] laut [3] üblicherweise Flieskommazahlen doppelter Genauigkeit nach IEEE.

[10] Ein Objekt, welches zur Verarbeitung großer Flieskommazahlen dient. Anstelle von Operatoren stehen Funktionen zur Verfügung.

[11] Um genau zu sein erhält die Funktion anstelle einer Kopie der Daten eine Kopie des Pointers auf die Daten. Kopiert wird jedes Argument, die Frage ist nur wie aufwendig dies ist.

[12] das soll nicht andeuten, dass Objekte anders als Basisdatentypen an Methoden übergeben werden. In Wirklichkeit wird auch bei der Übergabe eines Objekts eine Kopie erzeugt, allerdings nur von der Referenz auf das Objekt. Eine Referenz ist fast das selbe wie ein Pointer, mit der kleinen Ausnahme, dass sie immer auf ein initialisiertes Objekt verweist, der Compiler erlaubt die Verwendung uninitialisierter Objekte nicht.

[13] Bei der Authentifizierung beweist der Anwender dem entfernten Rechner seine Identität. Dies kann nur erfolgreich sein, wenn der entfernte Rechner den Anwender auch kennt, also eine Benutzerkennung für den Anwender auf dem entfernten Rechner existiert.

[14] Dr. Lecter: „Oberste Prinzipien, Clarice. Simplifikation. Lies Marc Aurel, für jedes einzelne Ding frage: Was ist es in sich selbst, was ist seine Natur...?  Was tut er, der Mann den Du suchst?“ aus „The silence of the lambs“ von Thomas Harris 1989, Drehbuch von Ted Tally.

[15] sofern der Algorithmus deterministisch ist.

[16] Mit 22 Bit können 222 = 4.194.304 verschiedene Werte dargestellt werden.

[17] dotted quad - Die allgemein übliche Form, eine Internetadresse anzugeben, indem die vier Adressteile durch Punkte getrennt sind. Beispiel: 127.0.0.1


 [o1]gemeint ist hier das Signal, welches angibt, dass eine neue Serverliste kommt, beziehungsweise die CommandMessage? muss ich wohl nachsehen...