oberon index <- ^ -> mail ?
Weiter: Autorisierungsprotokolle, davor: Systemumgebung, darüber: Das Ulmer Oberon-System.

Aufbau der Bibliothek


Bestandteile

Die Basis der Bibliothek wird von Modulen gebildet, die allgemeine Abstraktionen darstellen. Eine Abstraktion wird modelliert als eine Sammlung von einem oder mehreren Datentypen (Objektklassen) sowie Operationen auf denselben (Methoden). Um abgeleitete Objektklassen mit jeweils eigenen Implementierungen der zugehörigen Methoden zu unterstützen, enthalten die Basismethoden in der Regel einen indirekten Aufruf der implementierenden Methode, die für jedes Objekt erst mit seiner Initialisierung festgelegt wird. Diese auf upcalls, d.h. Prozeduraufrufe entgegen der Importhierarchie fußende Technik unterscheidet sich von Ansätzen in einigen klassischen objektorientierten Sprachen, ist aber sehr zweckmäßig. Insbesondere schafft sie die Voraussetzung für die durchgehende Trennung von Implementierungen und Abstraktionen, sodaß z.B. systemnahe Bausteine den entsprechenden Abstraktionen nicht über- sondern untergeordnet werden können, was der Portabilität aller anderen Programmteile ungemein zugute kommt. Eine ausführliche Diskussion dazu bietet [Borchert94, Kapitel 2].

Zu den allgemeinsten Abstraktionen gesellen sich systemunabhängige Implementierungen und Erweiterungen; auf darüberliegenden Ebenen sind unter anderem Schnittstellen zum Betriebssystem und schließlich die systemspezifischen, auf diese Schnittstellen aufbauenden Implementierungen zu finden.

^^^

Importbeziehungen

Die unterschiedlichen Beziehungen zwischen Modulen der Bibliothek sollen im folgenden durch ein Beispiel veranschaulicht werden. Betrachtet wird ein (fiktives) Modul MyModule, das eine Prozedur ShowTime zur Ausgabe der momentanen Uhrzeit auf die Standardausgabe zur Verfügung stellen soll. Wie diese Prozedur realisiert werden könnte, zeigt dieses Codefragment.

gif (Grafik, 5.1 KB)
Vier verschiedene Arten von Importbeziehungen

Diese Abbildung zeigt die wichtigsten daran beteiligten Module und ihre Importbeziehungen:

gif (Grafik, 6.8 KB)
Prozeduraufrufkette mit upcall

Das anwendende Modul MyModule benützt nur systemunabhängige Komponenten und bleibt somit portabel. Den Zugriff auf die eigentlich benötigte systemabhängig implementierte Funktion erhält es in diesem Fall über die Variable Clocks.system. Zur Verdeutlichung des Instruktionenflusses zeichnet diese Abbildung einen Teil der Aufrufkette nach, die für die Bestimmung der Uhrzeit durchlaufen wird.

^^^

Typensystem

Eine wichtige Funktion einer Programmiersprache und im gleichen Sinne auch der sie ergänzenden Bibliotheken ist es, Softwarekomponenten so leicht wie möglich auf Korrektheit und Vorgabentreue überprüfbar zu machen. Dazu leistet ein gut durchdachtes System differenzierter Datentypen einen wertvollen Beitrag. Strenge Kompatibilitätsregeln, die mittels statischer und dynamischer Kontrollen durchgesetzt werden, erzwingen die Einhaltung bestimmter Bezüge und helfen zugleich, Fehler frühzeitig zu erkennen oder gar nicht erst zu machen. Die richtige Wahl von Verallgemeinerungen und Vereinheitlichungen erleichtert andererseits in vielen Fällen die Wiederverwendbarkeit und universelle Einsetzbarkeit einmal entwickelter Teile. Beide Gesichtspunkte kommen in Oberon sehr stark zum Tragen.

gif (Grafik, 3.6 KB)
Grundlegende Objekttypen in Oberon

In der hier beschriebenen Bibliothek bilden alle Verbundtypen eine gemeinsame Hierarchie. Einige elementare Eigenschaften eines Objekts können bereits aus seiner statischen Typinformation entnommen werden. Diese Abbildung zeigt die wichtigsten dieser Typen. Im einzelnen repräsentieren sie die folgenden Abstraktionen:

^^^

Kontrollfluß

In diesem Abschnitt sollen wichtige Abstraktionen, die den Kontrollfluß betreffen, zusammengefaßt werden, namentlich die schon erwähnten Ereignisse, Tasks und Synchronisationsmechanismen.

Ereignisse

Unter Ereignissen versteht man Träger von Informationen, die jederzeit anfallen können und ohne direkte Rückantwort an verschiedene Interessenten weiterzuleiten sind. Das Modul Events definiert die entsprechende Basisabstraktion. Ein Ereignis erreicht nicht unbedingt sofort nach seiner Entstehung die an dieser Sorte von Ereignissen interessierten Parteien, sondern kann, abhängig von seiner Priorität, zunächst auch in eine Warteschlange geraten, wo es so lange verzögert wird, bis die entsprechende Priorität wieder akzeptiert wird und es (ggf. nach Bearbeitung seiner länger wartenden Vorgänger) schließlich an die Reihe kommt; ferner kann es, wie schon angedeutet, zur späteren Untersuchung bei einem Objekt abgespeichert werden. Als Priorität kann ein beliebiger Wert aus einer eindimensionalen, aber konfigurierbaren Skala vergeben werden.

Anstatt zwischengespeichert oder an einen oder mehrere Bearbeiter (event handlers) weitergegeben zu werden, können Ereignisse auch zum Programmabbruch führen oder schlicht ignoriert werden. Als Informationsträger für die Ausnahmenbehandlung eingesetzt, erlauben sie somit sehr differenzierte Reaktionsmöglichkeiten auf die verschiedensten Fehlerbedingungen. Auch Laufzeitfehler wie Verletzungen von Indexgrenzen, Dereferenzieren von Nullzeigern, Division durch Null usw. und asynchrone externe Ereignisse (etwa UNIX-Signale) werden im System durch Ereignisse repräsentiert.

Koroutinen

Im Ulmer Oberon-System bilden Koroutinen die Grundlage für Mechanismen, mit denen die Eingleisigkeit des Kontrollflusses aufgebrochen werden kann. Koroutinen haben jeweils ihren eigenen Anweisungszeiger (program counter) und Keller (stack); sie sind nicht wirklich gleichzeitig, sondern immer abwechselnd aktiv, und zwar jeweils solange, bis sie die Kontrolle explizit an eine andere Koroutine weitergeben. Dieses noch relativ einfache Sprachmittel genügt allerdings, um auch nichtdeterministische Formen der Nebenläufigkeit zu simulieren, also genau das Verhalten, das bei echter paralleler Ausführung gegeben wäre. Die dafür entwickelten Kooperationsmechanismen eignen sich dementsprechend für jede Art der Parallelität, auch derjenigen, die auf natürliche Weise bei Beteiligung mehrerer Rechner an einem verteilten System gegeben ist.

Tasks

Die konkurrierend lauffähigen Ablaufeinheiten innerhalb eines Oberon-Programms werden als Tasks bezeichnet, deren Eigenschaften von der gleichnamigen Abstraktion definiert werden. Technisch ist eine Task eine Koroutine (oder ein Verband einander auf herkömmliche Weise explizit abwechselnder Koroutinen), die nicht direkt sondern unter Kontrolle eines Ablaufkoordinators (scheduler) aktiviert wird. Sie kann vier verschiedene Zustände annehmen: bereit, beschäftigt, auf etwas wartend, beendet. Die einem Ablaufkoordinator unterstehenden Tasks bilden eine Taskgruppe; von diesen können auch mehrere existieren, deren Ablaufkoordinatoren wiederum Mitglieder einer weiteren Taskgruppe sind usw., bis hinauf zur immer vorhandenen ersten Koroutine, die implizit eine Taskgruppe für sich allein bildet.

Ist eine Task einmal angestoßen worden, d.h. "beschäftigt", bleibt sie das solange, bis sie selbst ihrem Ablaufkoordinator wieder eine Gelegenheit, die Kontrolle zu übernehmen, einräumt (sei es direkt, sei es durch einen Ereignisbearbeiter). Normalerweise ergeben sich solche Gelegenheiten ganz von selbst, sobald gewartet werden muß; sie können aber auch aus Fairneßgründen zusätzlich geschaffen werden.

Bedingungen

Die kooperative Weise, auf etwas zu warten, ergibt sich aus der Verwendung der Abstraktion für Bedingungen (conditions). Solche Bedingungen können in beliebiger Weise miteinander verknüpft werden und sich auf alle denkbaren Gegebenheiten beziehen, vom Freiwerden einer nur beschränkt verfügbaren Ressource bis zum Eintreten eines bestimmten Ereignisses. Wenn eine Task auf etwas warten will, teilt sie über Tasks.WaitFor ihrem Ablaufkoordinator in Form einer Bedingung mit, worauf sie wartet. Dieser wird, wenn er noch rechenbereite Tasks zur Verfügung hat, die Kontrolle an eine von ihnen weitergeben, oder andernfalls selbst unter der Vereinigung aller Bedingungen in den Wartezustand gehen. Schließlich kommt es irgendwann dazu, daß auch auf der höchsten Ebene (in der Task der Ur-Koroutine) keine Bedingung erfüllt ist; dann wird auf die gewöhnliche Art gewartet, d.h. den Implementierungen der Bedingungen entsprechend eine effiziente Warteoperation ausgeführt. Diese kostet in der Regel selbst bei komplizierten Kombinationen praktisch keine Rechenzeit, sondern läuft auf eine blockierende Ein-/"Ausgabeoperation oder das Suspendieren des Prozesses hinaus; eine Diskussion der Algorithmen, die diese Optimierung bewerkstelligen, würde hier allerdings zu weit führen ([Borchert94, Kapitel 7] geht darauf ein).

^^^

Aufträge und entfernte Objekte

Verteiltheit mit Transparenz

Der letzte Abschnitt dieser Einführung stellt einige der Elemente vor, die die Bibliothek zur Unterstützung verteilter Systeme anbietet.

Als entscheidendes Merkmal, das ein verteiltes System charakterisiert, gilt hier die Annahme, daß die Objekte, auf denen operiert wird, nicht nur im eigenen, sondern auch in fremden Prozessen lokalisiert sein können. Dabei wird erwartet, daß der Umgang mit diesen Objekten -- der Aufruf ihrer Methoden -- völlig unabhängig von deren Lokalisierung sein sollte, sodaß die vorgesehenen Anwendungen funktionieren, ohne daß auch nur das geringste Wissen über die Herkunft der betreffenden Objekte eingebracht werden müßte.

Die Technik, mit der diese Einheitlichkeit erreicht wird, beruht auf Stellvertreter-Objekten und Delegation. Das bedeutet, für jedes bekannt gewordene fremde Objekt existiert ein lokales Stellvertreter-Objekt, das zu nichts anderem in der Lage ist, als die Aufträge, die es erhält, zum Originalobjekt weiterzuleiten und dessen Reaktion anschließend für die eigene auszugeben.

Netzwerke

Verbindungen zu anderen Prozessen abstrahiert das Modul Networks. Selbstverständlich existieren Implementierungen für reale vom Betriebssystem bereitgestellte Netzwerkschnittstellen, unter UNIX etwa Internet und UnixDomainSockets. Wenn vom Betriebssystem gemeinsame Speicherbereiche (shared memory) unterstützt werden, können Prozesse auf derselben Maschine auch über schnelle SMStreams miteinander kommunizieren. All diesen Verbindungen ist gemeinsam, daß sie für den eigentlichen Datentransfer die bekannten byte-orientierten Kanäle (streams) verwenden. Für die Delegation von Aufträgen über ein Netzwerk müssen diese also in Byte-Folgen und wieder zurück verwandelt werden, kurz: persistent sein. Einen Basistyp für persistente Aufträge definiert das Modul Messages.

Typenspezifische Vorbereitungen

In der Regel reagieren Objekte nicht von vornherein nur auf Aufträge in einem einheitlichen Nachrichtenformat, sondern realisieren ihre wichtigen Methoden als direkte Prozeduraufrufe. Damit ein Objekt "verteilfähig" wird, bedarf es also der Umsetzung dieser Prozeduraufrufe in persistente Nachrichten (auf der Seite mit dem Stellvertreter-Objekt) und dazu passender Nachrichtenbearbeiter (auf der Seite mit dem Originalobjekt). Dies zusammen mit den für die Persistenz der Nachrichten notwendigen Schreib- und Leseoperationen zu implementieren, ist nicht schwierig und kann mit einem Hilfsprogramm (genrem) sogar weitgehend automatisiert werden. Sollen zusätzliche Sichten oder Erweiterungen aus der Ferne erreichbar sein, können dafür weitere Transporteinrichtungen angekoppelt werden. Wohlgemerkt sind alle diese Vermittlerdienste mit keinerlei Veränderungen der original mit dem Objekt befaßten Module verbunden. Sie werden üblicherweise zu einem Modul RemoteXXX, wobei XXX für den Namen der ursprünglichen Abstraktion steht, zusammengefaßt und unter diesem Namen auch als Dienst registriert.

Das Modul RemoteObjects

Die Schaltzentrale, die diese Elemente so koordiniert, daß der Prozeßgrenzen überschreitende Zugang zu Objekten so einfach zu benutzen ist wie z.B. gewöhnliche Schreib- und Leseoperationen, ist in der Basisabstraktion RemoteObjects verwirklicht. RemoteObjects verwertet allgemeine, von Networks definierte Adressen (addresses) und Einschaltpunkte (sockets), um Verbindungen mit anderen Prozessen herzustellen. Über die so erhaltenen Verbindungskanäle wickelt es die Nachrichtenübermittlung zwischen Objekten und Stellvertreter-Objekten ab.

Kanäle bleiben in der Implementierung von RemoteObjects verborgen. Sie werden bei Bedarf eröffnet und automatisch den betreffenden Objekten zugeordnet. Über einen Kanal können Verbindungen zu mehreren Objekten gleichzeitig aufrecht erhalten werden (multiplexing), und die Aufträge für ein bestimmtes Objekt können -- je nach Maßgabe der das Objekt unterstützenden Dienste -- sequentiell oder auch quasi-parallel, d.h. ohne Warten auf die Erledigung des jeweils vorhergehenden Auftrags, empfangen werden. Die letztere Unterscheidung dient zur Feinabstimmung der Auftragsentgegennahme auf das zu erwartende Antwortverhalten, also der Verbesserung der Effizienz, ändert aber nichts an der Tatsache, daß ein von außen sichtbares Objekt jederzeit mit neuen Aufträgen rechnen muß, auch während es z.B. einen lokalen Aufruf ausführt. Kritische Regionen, die nur sequentiell durchlaufen werden dürfen, sollten sich daher immer selbst schützen, etwa mit Semaphoren und, falls auch Ereignisbearbeiter eine Bedrohung darstellen, erhöhter Priorität.

Das Modul RemoteObjects hält intern für jedes exportierte und für die Dauer der Verbindung auch für jedes importierte Objekt eine Identität aufrecht, was die erfreulichen Konsequenzen hat, daß Zeiger auf eigene und fremde Objekte gleichermaßen eindeutig sind, Mehrfachimporte auch bei verschiedenen Verbreitungswegen nicht zur Vervielfältigung der Datenstrukturen führen und re-importierte eigene Objekte ohne zusätzlichen Verwaltungsaufwand als das, was sie sind, behandelt werden.

Eine weitere Aufgabe dieser Abstraktion ist die Unterstützung persistenter Disziplinen, für die sie ja ein regelrechtes Paradebeispiel darstellt. Eigene Sichten, die auf der Basis von PersistentDisciplines realisiert werden, gelten also ohne irgendein weiteres Zutun, wenn sie sich zufällig auf ein Stellvertreter-Objekt beziehen sollten, dem Originalobjekt.

Das Exportieren (für die Außenwelt Sichtbarmachen) eines Objekts hat analog zum Versenden eines persistenten Objekts die Gestalt einer Schreiboperation. Sie führt implizit, falls noch nicht vorhanden, zum Aufbau der nötigen Infrastruktur, damit das Objekt Nachrichten empfangen kann, und zur Vergabe einer Adresse. Geschrieben wird dann diese Adresse, oder anders ausgedrückt: eine persistente Form der Information darüber, wie das Objekt zu erreichen ist. Das Importieren eines Objekts besteht dementsprechend aus dem Lesen dieser Information und, wenn nötig, der Erzeugung eines Stellvertreter-Objekts sowie dem Versuch, einen Kanal zum Originalobjekt zu eröffnen. Die durch den Export hergestellte Sichtbarkeit eines Objekts kann explizit widerrufen werden mit RemoteObjects.Withdraw oder implizit, indem pauschal alle Verbindungen gekappt werden, etwa im Zuge der Aufräumarbeiten vor der Beendigung des Prozesses.

Illustration

gif (Grafik, 15.2 KB)
Kontrollfluß beim Zugriff auf ein entferntes Objekt (schematisch)

Diese Abbildung zeigt schematisch, wie ein Auftrag an ein entferntes Objekt weitergeleitet wird. Mit RemoteXXX wurde das Modul bezeichnet, das den Dienst RemoteObjects für die Abstraktion realisiert. Für die Seite des Klienten (Importeurs) definiert es eine Implementierung, die Methoden in Aufträge verwandelt. Für den Server (Exporteur) hingegen installiert es einen Auftragsbearbeiter, der diese Aufträge ausführt, indem er die entsprechenden Methoden aufruft. Jede in RemoteXXX implementierte Methode erzeugt einen Auftragsverbund, der die Prozedurparameter, Rückgabewerte und eine Komponente errors zum Sammeln von Fehlerereignissen enthält. Dieser Verbund ist insgesamt zweimal im Netz unterwegs (zum Server und zurück), wobei er natürlich erst auf dem Rückweg die Information der Resultate und/oder Fehlerereignisse trägt.

Dienste für spezielle Abstraktionen

Für eine Reihe von Abstraktionen der Oberon-Bibliothek gibt es Module, die die für entfernte Zugriffe nötigen Dienste implementieren. Teilweise sind dazu auch weitere Techniken notwendig, auf die hier nicht näher eingegangen werden kann. Erwähnenswert ist vor allem RemoteEvents, das sogar Ereignisse über Prozeßgrenzen hinweg verteilbar macht. Im dritten Kapitel kommen wir auf entfernte Objekte zurück.

^^^


oberon index <- ^ -> mail ?
Weiter: Autorisierungsprotokolle, davor: Systemumgebung, darüber: Das Ulmer Oberon-System.
Martin Hasch, Oct 1996