oberon index <- ^ -> mail ?
Weiter: Auf Names aufbauende Module, davor: Konzeption, darüber: Namensräume.

Realisierung


Objekt- und Parametertypen

Entsprechend der im ersten Kapitel genannten Prinzipien wird eine neue Abstraktion in Form eines Basismoduls in die Oberon-Bibliothek integriert, das die für die Abstraktion charakteristischen Typen und Methoden definiert. Das Basismodul für Namensräume in Oberon heißt schlicht Names. Es dient als gemeinsamer Bezugspunkt für Anwendungen, Erweiterungen und spezielle Implementierungen des vorgestellten Konzeptes. Eine einfache Implementierung ist direkt eingebaut.

Namensräume sind gerichtete Graphen, und Graphen bestehen aus Knoten und Kanten (vertices und edges). Da die Kanten durch einfache Zeichenfolgen repräsentiert werden sollen, ergibt sich als alleiniger zentraler Objekttyp dieser Abstraktion der Knoten (Names.Node).

Da für Knoten auch typenunabhängige Abstraktionen in Frage kommen, etwa um Dienste wie RemoteNames separat implementieren zu können, ist Names.Node eine Erweiterung von Services.Object.

Ein Name, über den von einem Knoten auf einen nachgeordneten Knoten verwiesen wird, soll eine beliebig lange Folge beliebiger Zeichen sein können. Zwar ist es sicherlich sinnvoll, in konkreten Fällen gewisse Namenskonventionen einzuhalten, jedoch spielen dergleichen Einschränkungen für das abstrakte Modell keine Rolle. Im Gegenteil kann nur durch größtmögliche Allgemeinheit erreicht werden, daß eine Abstraktion mit jeder erdenklichen Konkretisierung harmoniert.

Für den effizienten Umgang mit solchen Zeichenketten stellt die Oberon-Bibliothek einen eigenen Datentyp zur Verfügung -- ConstStrings.String --, der somit auch als Names.Name zum Einsatz kommt. Diese Art von Zeichenketten verursacht einen kleinen Zusatzaufwand beim Einlesen, wird aber platzsparend gespeichert und kann sogar günstiger als gewöhnliche (Null-Byte-terminierte) Zeichenketten verglichen oder vervielfältigt werden.

Einzelheiten des Autorisierungsschemas wurden im vorhergehenden Kapitel behandelt. In der Schnittstelle von Names schlägt sich die Autorisierungsproblematik in erster Linie durch die Anwesenheit von Parametern des Typs Shards.Lid bei einzelnen Methoden nieder.

Wichtig ist, daß Zugriffe auf Knoten genau nach Zugriffsarten (access modes) unterschieden werden, für die ggf. getrennte Berechtigungen verwaltet werden. Names.Permissions ist ein Vektor von Autorisierungsagenten, der nach Zugriffsarten indiziert wird.

In Analogie zum Dateisystem werden "administrative" Attribute eines Knotens als sein Status bezeichnet. In der Standard-Implementierung umfaßt der Status genau die Berechtigungen.

Ein Ereignis, das eine Veränderung der Kantenmenge eines Knotens signalisiert, trägt als Information neben der Art der Veränderung auch diesen Knoten und den hinzugekommenen bzw. nicht mehr gültigen Namen. Ein zusätzlicher Verweis auf den über den Namen referenzierten Unterknoten ist, wie es sich zeigen wird, wohlweislich nicht enthalten.

In diesem Codefragment sind die entscheidenden Typdefinitionen der Abstraktion zusammengestellt.

^^^

Methoden

In welcher Weise bei einem Knoten die zu ihm gehörenden Kanten abgespeichert werden, wird von der Abstraktion nicht festgelegt. Die Kantenmenge soll allein über die zum Knoten-Objekt gehörenden Methoden verfügbar sein.

Methoden werden von dem die Abstraktion definierenden Modul als gewöhnliche Prozeduren vereinbart, deren erstes Argument das betroffene Objekt ist. Aufrufe von Methoden werden in der Regel auf Aufrufe von Prozeduren aus einer zum Objekt gehörenden Schnittstelle (interface) abgebildet.

Das Modul Names gibt Implementierungen die Freiheit, gewisse Gruppen von Methoden zu implementieren und andere nicht, wobei die genaue Auswahl durch die "Fähigkeiten" (capabilities) des Knotens bestimmt wird. Je nach vorliegenden Fähigkeiten kann die Methode dann, anstatt eine Interfaceprozedur aufzurufen, auch eine Standard-Aktion durchführen oder nur eine Fehlerbedingung weitergeben.

In dieser Tabelle sind alle Methoden zusammengestellt, die im Folgenden näher erläutert werden:

InitNode ist die übliche Methode, neue Instanzen eines von Names.Node abgeleiteten Objekts zu initialisieren, d.h., mit dem zur Abstraktion gehörenden Interface zu verbinden.

CreateNode erzeugt einen Knoten mit der eingebauten einfachen Implementierung, wobei die gewünschten Fähigkeiten angegeben werden können.

Access erteilt Auskunft über die Berechtigung zu einer bestimmten Zugriffsart, ohne den Zugriff tatsächlich auszuführen. Falls für den betreffenden Knoten Access nicht implementiert ist, wird lediglich geprüft, ob er die vorausgesetzte Fähigkeit besitzt.

GetStatus und SetStatus stellen den Status eines Knotens zur Verfügung, falls dieser darauf eingerichtet ist.

GetMembers liefert die Menge der von einem Knoten wegführenden Kanten (d.h. die Namen seiner "Unterknoten") in Form eines Iterators. Iteratoren abstrahieren eine Art Pipeline für Objekte. Sie eignen sich besonders gut für sequentielle Abfragen großer Datenstrukturen, weil sie Container überflüssig machen und weil durch die implizite Synchronisierung des Produzenten mit dem Konsumenten die Länge der Traverse einfach und direkt von der Menge der tatsächlich abgeholten Datensätze bestimmt wird.

TakeInterest veranlaßt, daß bei jeder Änderung der Menge der nachfolgenden Knoten eines Knotens ein Ereignis mit einer entsprechenden Nachricht ausgelöst wird. Dies erlaubt interessierten Parteien, stets auf dem Laufenden zu bleiben, ohne ständig aktiv nach dem neuesten Stand fragen zu müssen.

Wie GetMembers gilt auch TakeInterest als ein Lesezugriff auf den Knoten (Zugriffsart read), der dieselbe Art von Informationen liefert und an dieselbe Berechtigung geknüpft ist.

GetNode ist wohl die wichtigste Methode, denn sie realisiert den Schritt entlang einer Kante von einem Knoten zum nächsten. Konkret heißt das, der Knoten wird nach einem Unterknoten mit einem bestimmten Namen gefragt und liefert als Resultat diesen Knoten, falls er ihn tatsächlich kennt und die Abfrage aufgrund ihrer Autorisierung als berechtigt ansieht.

In diesem Zusammenhang sollte die Bedeutung der Differenzierungsmöglichkeiten zwischen Berechtigungen für verschiedene Zugriffsarten nicht unterschätzt werden. Beispielsweise kann über die Autorisierung bei GetNode kontrolliert werden, welche konkreten Objekte für eine bestimmte Partei erreichbar sind, während GetMembers unabhängig davon die Sichtbarkeit der dazugehörenden Namen regelt.

Da die Namen selbst bereits einen wesentlichen Teil der Information bilden, die durch Namensräume dargestellt wird, kann es sinnvoll sein, diese einem weiteren Kreis möglicher Interessenten zugänglich zu machen als die dahinter verborgenen Objekte. Umgekehrt könnte man auch bewußt die Abfrage kompletter Verzeichnisse einschränken (etwa um Übertragungskosten zu sparen), während die einzelnen Objekte durchaus allgemein verfügbar bleiben.

Bemerkung: Ein anderes Detail in der Systematik der Berechtigungen erwies sich nach ersten praktischen Erfahrungen allerdings als diskussionswürdig: die Differenzierung von GetStatus und SetStatus als unterschiedliche Zugriffsarten. Weil Oberon bei Zeigern nicht unterscheidet, ob die referenzierten Daten nur gelesen oder auch verändert werden dürfen, muß die Weitergabe eines nicht zum Verändern bestimmten Verbunds zwangsläufig in Form einer Kopie erfolgen, was die entsprechende Methode leider verkompliziert. Andererseits werden diese Informationen in der Regel genau deswegen angefordert, weil sie gleich darauf verändert werden sollen -- Berechtigungen lediglich zu testen erlaubt ja schon Access, wenn man sich an die Konvention hält, statusCap nur zusammen mit accessCap zu implementieren. All dies legt nahe, in einer überarbeiteten Version von Names Statusinformationen doch als kanonische Objekte -- mit allen Vorteilen für die Handhabung und Erweiterbarkeit -- zu behandeln und entweder die Zusammenfassung der Zugriffsarten examine und change zu einer einzigen in Betracht zu ziehen oder zumindest innerhalb eines Prozesses auf die durch das Kopieren gewonnene Schutzwirkung zu verzichten.

Insert und Delete vergrößern bzw. verkleinern die Menge der von einem Knoten wegführenden Kanten. Diese Methoden betreffen also Namen, unter denen Knoten einander bekannt sind; die Knoten selbst werden so nicht erzeugt oder zerstört.

Destroy dient hingegen genau zum Löschen eines Knotens und implizit aller Kanten, die von ihm ausgehen oder zu ihm führen. Die Fähigkeit destroyCap regelt nicht, ob ein Knoten gelöscht werden kann, sondern ob eine spezifische Methode der Implementierung dabei eingeschaltet wird.

Anders als viele andere Abstraktionen offeriert das Modul Names also eine explizite Destruktor-Methode für seine Objekte. Das hat verschiedene Gründe. Die stärkste Motivation zu dieser Methode kommt aus der nützlichen Rolle, die sie im Autorisierungssystem einnimmt. Mit ihr kann für jedes Objekt die Erlaubnis, seine Termination einzuleiten, unabhängig von den Namen, unter denen es erreichbar ist, verwaltet werden. Das Löschen eines Knotens unter der Kontrolle dieser Methode erspart überdies den Aufwand, auch die zugehörigen Namen einzeln zu löschen, da dies implizit erledigt werden kann. Dies wiederum ist sinnvoll, weil veraltete Namen in jedem Fall und ohne weitere Autorisierungshindernisse verschwinden sollen. Diese für die Benutzung angenehme Semantik muß freilich mit etwas mehr Aufwand bei der Implementierung bezahlt werden.

Die genauen Definitionen der Methoden sind aus diesem Codefragment ersichtlich.

^^^

Variable

Eine besondere Funktion hat die globale Variable Names.root, deren Definition in diesem Codefragment wiedergegeben ist. Sie ist ein Knoten, der immer existiert und unter dem vom Laufzeitsystem Referenzen auf externe Objekte angelegt werden können. Auf diese Weise können sich viele Prozesse einen Namensraum teilen, ohne daß für eine Kontaktaufnahme zu Namensservern eigens gesorgt werden müßte. Names kann somit als eine Abstraktion verwendet werden, die den Import und Export von Objekten generell transparent macht.

^^^

Interne Implementierung

Standard- versus Triviallösung

Basismodule von Abstraktionen enthalten häufig minimale Implementierungen, die nicht viel mehr tun, als die formalen Eigenschaften auf eine triviale Art zu erfüllen. So sind etwa in Streams die immerzu leeren Kanäle, in Clocks die stehende Uhr, in Timezones die Zone der Weltzeit und in Shards die stereotypen Antworten eingebaut. Trotz ihrer Trivialität kann kaum bezweifelt werden, daß diese Implementierungen nützlich sind, besonders, wo sie auch zur Initialisierung globaler Variabler dienen.

Bei Names liegen die Verhältnisse ein wenig anders. Eine denkbare Trivialform von Knoten ohne Unterknoten, also auch ohne Namen, wäre offensichtlich absurd, zumal der Startknoten Names.root ja Verweise zu weiteren Objekten aufnehmen können soll. Sobald aber Knoten mit der Fähigkeit domainCap und sämtlichen dazugehörenden Methoden realisiert sind, fehlt nur noch so wenig zu einer vollwertigen Implementierung, daß es wohl vernünftig ist, dieses wenige -- die Unterstützung von accessCap, statusCap und destroyCap -- hinzuzunehmen und so bereits intern eine substantielle, alle Möglichkeiten des Protokolls realisierende Lösung anzubieten.

Eine solche Standardlösung ist also im Basismodul Names enthalten. Sie beruht auf einer privaten Erweiterung des Knoten-Objekttyps um einen Status, von dem nichts als der obligate Vektor für Berechtigungen benutzt wird, und einer Datenstruktur zur Aufnahme von Kanten. Objekte dieses Typs können mit Names.Create erzeugt werden. Obwohl sie im Grunde alle Fähigkeiten hätten, können diese selektiv festgelegt werden. Alle zur Standardlösung gehörenden Definitionen sind privat.

Datenrepräsentation

Standardknoten sind -- wie Knoten im allgemeinen -- nicht persistent. Ihre Kantenmengen werden in der vorläufigen Fassung als gewöhnliche, im dynamischen Speicher angelegte binäre Bäume verwaltet. Eine spätere Fassung könnte ausgefeiltere Algorithmen verwenden, die dann aber wohl in einer separaten Abstraktion für Datenstrukturen anzusiedeln wären. Derartige Abstraktionen gibt es bisher in der Ulmer Oberon-Bibliothek noch nicht (es existieren vielversprechende Ansätze, vgl. etwa [Borchert95b, Keys(3)]), weil die Bedürfnisse der vorhandenen Module entweder zu unterschiedlich oder hinreichend gut mit elementaren Mitteln erfüllbar waren. Namensräume selbst könnten sogar in Konkurrenz zu manchen dieser künftigen Abstraktionen treten, da sie ein relativ allgemeines Konzept ohne allzuviel störenden Ballast verwirklichen (es sei daran erinnert, daß z.B. die Autorisierung nur sehr wenig kostet, solange nur einfache oder gar keine Autorisierungsagenten benutzt werden).

Kooperation bei Aufräumarbeiten

Nicht zuletzt wegen der Anforderungen an das Verhalten von Namensräumen beim "Zerstören" von Knoten ist es nützlich, in einem kleinen Ausflug die Mechanismen näher zu betrachten, die in der Bibliothek für Koordinationsaufgaben im Zusammenhang mit der Termination von Objekten vorgesehen sind. Eine geeignete Abstraktion offeriert das Modul Resources. Sie gibt eine Vorgehensweise vor, um Aufräumarbeiten für nicht mehr benötigte Ressourcen zum richtigen Zeitpunkt zu veranlassen. Soll ein System skalierbar sein, also prinzipiell keinerlei festen Begrenzungen unterliegen, ist es unabdingbar, daß alle beschränkt vorhandenen Mittel nach Gebrauch der Wiederverwendung zugänglich gemacht werden. Dazu bedarf es der Kooperation unter den Nutznießern dieser Mittel -- was leider nicht in allen Bereichen so einfach zu erreichen ist wie bei Softwarekomponenten.

Die Kanten, die zu einem ungültig gewordenen Knoten führen, sollen automatisch entfernt werden. Dazu muß jede Implementierung, die Kanten verwaltet, erfahren können, wann der Zeitpunkt zum Bereinigen einer solchen Situation gegeben ist. An der Stelle, wo über die Termination eines Objektes entschieden wird, dient Resources.Notify dazu, potentielle Interessenten von dessen "tragischem Ende" zu benachrichtigen. Umgekehrt sorgt in einem Kontext, in dem auf die Termination reagiert werden soll, Resources.TakeInterest dafür, daß ein entsprechendes Ereignis ausgelöst wird, sobald dieser Fall eingetreten ist.

Dieses Ereignis gilt pauschal für das terminierte Objekt, ohne daß für die Interessenten ein Zusammenhang zu einzelnen davon betroffenen anderen Objekten hergestellt würde. Daher müssen z.B. Module, die Namensräume implementieren, sofern sie tatsächlich Kantenbeziehungen selbst verwalten (in einem der folgenden Abschnitte lernen wir mit RemoteNames eine Implementierung kennen, die keine Kantenverwaltung nötig hat), Informationen über die eigenen zu einem Knoten führenden Kanten bei diesem Knoten aufbewahren. Woher ein Knoten sonst noch referenziert werden mag, ist für die einzelne Implementierung unerheblich; sie kennt ja auch nur die Kanten, für die sie selbst zuständig ist.

Speicherplatz wird in Oberon nicht explizit freigegeben, sondern mit automatischer Speicherbereinigung (garbage collection) zurückgewonnen. Diese kann freilich nicht erkennen, daß ein Objekt nicht mehr gebraucht wird, solange noch irgendwelche Referenzen darauf existieren, selbst wenn diese nur zu Verwaltungsstrukturen gehören, die nicht als eigentlicher Zugang zu dem Objekt gedacht sind. Mit Hilfe von Resources besteht eine Möglichkeit, diese nicht entscheidenden Referenzen (lightweight references) und damit letztlich das Objekt unter geeigneten Vorkehrungen dennoch loszuwerden. Dazu gehören zwei Dinge: Erstens sollte überall dort, wo Verweise aus anderen Datenstrukturen auf ein Objekt gehalten werden, auf das Terminationsereignis reagiert werden, indem diese Verweise entfernt werden, sodaß auch die "leichtgewichtigen", von außen nicht zugänglichen Referenzen anschließend der automatischen Speicherfreigabe nicht mehr im Wege stehen. Zweitens muß das Einrichten sowie die Aufhebung jeder (ge-)wichtigen Referenz mit Resources.Attach bzw. Resources.Detach angekündigt werden. Den Umstand, daß die letzte Referenz dieser Art auf ein Objekt aufgehoben worden ist, können Interessenten dann über ein Ereignis erfahren, das dem Terminationsereignis ähnelt, aber statt Resources.terminated die Information Resources.unreferenced trägt.

Solche Ereignisse sind grundsätzlich als Signal dafür zu verstehen, daß ein Objekt nicht mehr benötigt wird und die Beendigung seiner Existenz unmittelbar bevorsteht. Gleichwohl gilt das betroffene Objekt in diesem Moment noch nicht als terminiert, sondern bleibt voll funktionsfähig und offen für Interaktionen, was für Parteien interessant ist, die bei einer angekündigten Termination mehr zu tun bereit sind als bei schon vollendeten Tatsachen (in einen Datenkanal etwa können noch gepufferte Daten ausgegeben werden, was keinen Sinn mehr hat, sobald er tatsächlich unbrauchbar geworden ist). Letztendlich sollte dieses Zwischenstadium jedoch zur endgültigen Termination führen, damit auch die dazu gehörenden Reaktionen folgerichtig ausgelöst werden. Wenn die Basisabstraktion dies veranlaßt, spart das nicht nur Arbeit bei allen Implementierungen, sondern garantiert auch -- weil Ereignisbearbeiter in umgekehrter Reihenfolge ihrer Anmeldung aktiviert werden -- dafür, daß alle Interessenten zum Zuge gekommen sind, bevor das Objekt in den nächsten, letzten Zustand wechselt.

Dementsprechend wird in Names für jeden neu erzeugten Knoten sofort ein Ereignisbearbeiter eingesetzt, der unter anderem bei "Unreferenziertheit" unverzüglich dessen Termination veranlaßt. Die Rückwärtsverweisliste auf von Standardknoten ausgehende Kanten, die Bestandteil der privaten Sicht der Standardimplementierung auf jeden Knoten ist, stellt ein typisches Beispiel für "leichtgewichtige" Referenzen dar. Ob diese Liste leer ist oder nicht, ist ein eindeutiger Hinweis, ob Standardkanten zu einem Knoten führen (Kanten selbst sind selbstverständlich gewöhnliche wichtige Referenzen). Daher braucht nicht über jede einzelne Standardkante mit Resources.Attach und Resources.Detach Buch geführt zu werden, sondern nur darüber, ob diese Liste ein erstes Element erhält bzw. leer wird.

Da Knoten somit grundsätzlich empfindlich auf den Verlust wichtiger Referenzen reagieren, muß jeder Knoten, dessen Lebensdauer nicht an seine Einbindung in einen Namensraum gebunden sein soll -- etwa weil er selbst die Wurzel eines Namensraumes bildet --, mit Attach gegen diesen Verlust geschützt werden. Etwas allgemeiner gesprochen sollten sich Anwendungen also dessen bewußt sein, daß ein Objekt den Konventionen von Resources gemäß zu behandeln ist. Speziell bei Implementierungen von Namensräumen verursacht dies erfreulicherweise überhaupt keinen nennenswerten zusätzlichen Aufwand, da "leichte" und wichtige Referenzen hier stets paarweise auftreten und gemeinsam verwaltet werden und Aufräumarbeiten ohnehin auf Veranlassung der Methode Destroy durchzuführen sind.

gif (Grafik, 6.9 KB)
Koordination von Aufräumarbeiten für Knoten (Beispiel)

Diese Abbildung illustriert, wie die Abstraktion Resources innerhalb des Moduls Names benutzt wird. Die schraffiert dargestellten Knoten mögen zu einer anderen Implementierung gehören. Wenn hier z.B. die Kante b gelöscht wird, führt dies zu Resources.Detach für den Knoten B, da ihn anschließend keine Standardkante mehr erreicht. Dies wiederum könnte ein Resources.unreferenced-Ereignis auslösen, das mit Resources.Notify sofort zur Termination von B umgemünzt wird. Infolge dieser Termination werden dann die Kanten c und d beseitigt, was eine Detach-Operation für den Knoten D, nicht jedoch für C bewirkt.

Anmerkungen

Mit skurrilen Autorisierungen oder zyklischen Graphen kann man erreichen, daß Teile von Namensräumen unerreichbar werden, ohne daß Resources dessen gewahr wird. In der Praxis spielen diese konstruierten Beispiele allerdings deswegen kaum eine Rolle, weil Namensräume typischerweise dergestalt über verschiedene Prozesse verteilt werden, daß Knoten genau dort angesiedelt sind, wo auch die zu den Namen gehörenden Inhalte existieren. Unabhängig von ihrer Erreichbarkeit bleiben diese Knoten natürlich in ihrer Lebensdauer an die sie beherbergenden Prozesse gebunden und können daher immer noch eliminiert werden, ohne daß andere Parteien davon beeinträchtigt würden.

Im Gegensatz zu Names.Destroy ist für Resources.Notify keinerlei Autorisierung nötig oder Plausibilitätskriterium zu erfüllen. Allerdings werden Informationen über Termination immer nur von Objekten zu Stellvertreter-Objekten und nicht in der umgekehrten Richtung über Prozeßgrenzen weitergegeben, schon allein um die Möglichkeit zu haben, gezielt Stellvertreter-Objekte loszuwerden. Daher öffnen die verschiedenen Terminationsmechanismen keine Sicherheitslücke bei Objekten, die einem potentiellen Angreifer nicht selbst gehören, und deren Schutz allein als relevant zu betrachten ist (vgl. Autorisierungsprotokolle / Zusätzliche Anforderungen).

^^^


oberon index <- ^ -> mail ?
Weiter: Auf Names aufbauende Module, davor: Konzeption, darüber: Namensräume.
Martin Hasch, Oct 1996