Datenräume bilden die oberste und allgemeinste Klasse für Objekte, die gespeichert werden. Der Typ DATASPACE bezeichnet Objekte vom Typ Datenraum, ein elementarer Datentyp des L3-Systemkerns. Datenräume können Daten beliebigen Typs aufnehmen und gewähren direkten Zugriff auf ihren Inhalt. Ein Datenraum ist als Container für Daten aufzufassen, es sind zunächst keine Annahmen über den Inhalt (Texte, ausführbarer Code oder vielleicht eine Datenbank?) nötig. Ein Datenraum ist einfach ein Stück linearer Speicher, das bis zu einem Gigabyte groß werden kann.
Datenräume sind lokale Objekte in den Tasks eines L3 Systems, oder einfacher gesagt, jeder Datenraum hat genau eine Besitzertask. Eine Task kann bis zu 16380 Datenräume besitzen. Da der dafür theoretisch erforderliche Adreßraum 16 Terabyte beträgt, der Prozessor jedoch nur 4 Gigabyte pro Task adressieren kann, muß noch eine Abbildung der aktuell angesprochenen Datenräume in den virtuellen Adreßraum der Task erfolgen. Diese Abbildung heißt Mapping. Das Mapping erfolgt in Segmenten zu 16 MB. Bis zu 234 dieser 16 MB Segmente können gleichzeitig »gemappt« sein, die restlichen 22 Segmente werden intern beansprucht. Da Datenobjekte meistens kleiner als 16 MB sind, bedeutet das, daß gleichzeitig 234 »Dateien geöffnet« sein können. Falls Datenräume größer als 16 MB sind, verringert sich die Anzahl der möglichen Mappings entsprechend, da ein solch großer Datenraum dann zwei oder mehr nebeneinanderliegende Einträge in der Mappingliste beansprucht. Falls sehr große Datenräume gemappt werden sollen, sollten sie sofort nach Erzeugung der Task gemappt werden, da andernfalls die Gefahr besteht, daß sich in der Mappingliste keine hinreichend große »Lücke« mehr findet. Mapping- Operationen sind implizit in den üblichen Datenraumoperationen und darauf aufbauenden enthalten. Beispielsweise ist nach einem edit auf eine neue Datei der Datenraum anschließend gemappt. Explizites Mapping per open und close ist nur dann erforderlich, wenn sehr viele (große) Datenräume gleichzeitig direkt adressiert werden müssen.
Die Datenräume einer Task sind in der Reihenfolge ihrer Erschaffung, bzw. ihres Empfangs, durchnumeriert. Dieser Datenraumindex kann zum Durchlaufen aller Datenräume einer Task benutzt werden. Datenraumindices, die nach Löschen eines Datenraums frei sind, können wiederbenutzt werden. Der Index kann also nicht zur Identifikation benutzt werden.
Bei der Beschreibung von Operationen auf Datenräumen sind drei Zugriffsebenen zu unterscheiden. Zunächst gibt es Operationen, die für den Datentyp DATASPACE Initialisierung, Zuweisungsprozeduren und dergleichen anbieten. Auf dieser Ebene ist ein Datenraum ein atomares Objekt. Er wird in seiner Ganzheit erzeugt und gehandhabt. Diese Ebene wird z.B. von der Intertask Kommunikation benutzt. Wie in Kapitel 8 beschrieben, ist es möglich einen oder mehrere Datenräume in einem atomaren Botschaftstransfer zwischen Sender- und Empfängertask zu übertragen.
Die nächste Zugriffsebene betrifft die Organisation eines Datenraums. Er ist ein Objekt, das aus einer Anzahl von »Pages« im virtuellen Speicher besteht. Diese technische Struktur eines Datenraums kann Angaben über benötigten Speicherplatz liefern und wird insbesondere für blockorientierte Schreib- und Leseoperationen, z.B. auf Diskette, benutzt.
Die dritte Zugriffsebene betrifft den korrekten Zugriff auf den Datenrauminhalt. Diese innere Struktur wird einem Datenraum von der konkreten Anwendung »aufgeprägt«.
Durch das Schlüsselwort BOUND bei der Deklaration einer Variablen wird in einem ELAN-Programm erreicht, daß diese Variable nicht im Standarddatenraum, sondern im jeweils angekoppelten Datenraum abgelegt wird.
Da die innere Struktur eines Datenraums anwendungsabhängig ist, können an dieser Stelle keine Aussagen über Möglichkeiten des Zugriffs auf diese Struktur gemacht werden. Ein Datenraum kann eine Typnummer erhalten. Durch Test dieser Typnummer kann dann festgestellt werden, ob die passenden Zugriffswerkzeuge zur Verfügung stehen.
Eine gewisse Sonderbehandlung erfahren TEXTE in Datenräumen. Texte werden als dynamische Strukturen in einem Textheap gespeichert. Die Mindestgröße dieses Textheaps kann mit der Prozedur set min heap size vorgegeben werden. Siehe dazu auch Kap 2.3: Aufbau des Textheap in anderen Datenräumen.
Eine ständig innerhalb des L3 Systems verwendete Technik ist die Koppelung von Datenräumen durch faules Kopieren. Die Überlegung dabei ist, daß die virtuelle Speicherverwaltung ermöglicht, Kopieroperationen zunächst in Bezug auf das eigentliche Datenobjekt (den Datenraum) zu unterdrücken.
Dazu zunächst ein Blick auf die in dieser Erörterung aus Gründen der Verständlichkeit vereinfacht dargestellte, technische Organisation eines Datenraums. Sie läßt sich vorstellen als ein Zeiger auf eine Seitenblocktabelle, die Einträge dieser Tabelle verweisen auf die Blöcke auf dem Hintergrundspeicher (Festplatte). Durch eine von der MMU (Memory Management Unit) des 80386 unterstützte Adreßumsetzung erhält man so ein wahlfrei adressierbares Datenobjekt.
Seite p1 -----> Block a (4096 Byte) DATASPACEA Ptr ---> Seite p2 -----> Block b " Seite p3 -----> Block c " ...
Um eine Kopie dieses Datenraums zu erhalten, reicht es nun zunächst aus, einen zweiten Verweis auf die Seitenblocktabelle zu erzeugen und zu vermerken, daß der Datenraum gekoppelt ist.
Seite p1 -----> Block a (4096 Byte) DATASPACEA Ptr ----> Seite p2 -----> Block b " DATASPACEB Ptr ----> Seite p3 -----> Block c " ...
Da die (internen) Datenraumnamen innerhalb des Gesamtsystems eindeutig sind, wird diese Kopiertechnik sowohl tasklokal, als auch systemweit benutzt.
Erst wenn ein Schreibzugriff stattfindet, müssen die betroffenen Datenraumseiten tatsächlich kopiert werden.
Seite p1 ----> |--> Block a (4096 Byte) DATASPACEA Ptr ---> Seite p2 ----> ||-> Block b " Seite p3 ----> || Block c " || Seite p1 ------|| DSCOPY Ptr ---> Seite p2 -------| Seite p3 ---------> Block d " (neu)
Dieses Verfahren erlaubt das Kopieren großer Datenmengen, ohne daß nennenswerte Datenmengen bewegt werden. Insbesondere im L3 Systemmantel ist diese Technik gut erkennbar. Das Tasksystem besteht von Beginn an aus mehreren Tasks, die jede logisch über 3MB Speicherplatz einnehmen. Da die Standarddatenräume der Tasks, die diesen Platz beanspruchen, sich jedoch nicht oder nur geringfügig unterscheiden, beträgt der Gesamtplatzbedarf eines frischen Systems nur etwa das Doppelte der Standarddatenraumgröße, plus ca. 1,5 MB für den L3- Systemkern und Treiberprozesse.
OP := (DATASPACE VAR dest, DATASPACE CONST source)
PROC copy (DATASPACE CONST source, DATASPACE VAR dest)
PROC copy (DATASPACE CONST source, dest, INT CONST from page, to page, pages)
PROC copy (DATASPACE CONST ds, INT CONST from page, to page, pages)
PROC close (DATASPACE CONST ds)
DATASPACE PROC dataspace (TEXT CONST name)
create ("matrix4D", ds) ; TYPE DA = STRUCT (ROW 25 INT a), DB = STRUCT (ROW 25 DA b), DC = STRUCT (ROW 25 DB c), DD = ROW 25 DC; BOUND DD VAR sp := dataspace ("matrix4D") ; INT VAR i,j,k,l; FOR i FROM 1 UPTO 05 REP FOR j FROM 1 UPTO 05 REP FOR k FROM 1 UPTO 05 REP FOR l FROM 1 UPTO 05 REP sp[i].c[j].b[k].a[l] := i * j * k * l; PER; PER; PER; PER;
INT PROC dataspaces(TASK CONST task)
PROC delete (DATASPACE VAR ds)
PROC delete (DATASPACE CONST ds, INT CONST from page, pages)
BOOL PROC exists (DATASPACE CONST ds)
PROC move (DATASPACE VAR source, dest)
DATASPACE PROC next (DATASPACE CONST ds)
INT PROC next (DATASPACE CONST ds, INT CONST pageno)
INT PROC index (DATASPACE CONST ds)
INT PROC pages (DATASPACE CONST ds)
PROC set min heapsize (DATASPACE CONST ds, INT CONST new limit)
create ("hugo", ds); set min heap size (dataspace ("hugo"), 0); TYPE MI = ROW 3 000 000 INT; BOUND MI VAR mio := dataspace("hugo") ; INT VAR i; FOR i FROM 1 UPTO 3 000 000 REP mio [i] := i PER ;
create ("texte satt", ds); set min heap size (dataspace ("texte satt"), 45000000); TYPE TH = ROW 40 000 000 TEXT; BOUND TH VAR thd := dataspace("texte satt") ; ...
INT PROC storage (DATASPACE CONST ds)
PROC type (DATASPACE CONST ds, INT CONST typ)
INT PROC type (DATASPACE CONST ds)
type ('ds') < 0 falls 'ds' nie an ein BOUND Objekt
angekoppelt war,
type ('ds') = 0 falls 'ds' schon an ein BOUND Objekt
angekoppelt war.
Durch das Schlüsselwort BOUND bei der Deklaration einer Variablen teilt man dem ELAN-Compiler mit, daß die Werte dieser Variablen in einem Datenraum gespeichert werden. Das Ablegen von Datenobjekten in Datenräumen kann aus verschiedenen Gründen notwendig bzw. sinnvoll sein:
BOUND ROW 10000 REAL VAR liste; (* 'liste' soll in einem Datenraum abgelegt werden *) DATASPACE VAR ds :: nilspace; (* erzeugen des Datenraums 'ds' als Kopie des nilspace *) liste := ds; (* 'liste' wird an den Datenraum 'ds' angekoppelt *) bearbeite liste; (* Das Datenobjekt 'liste' wird bearbeitet *) copy (ds, public, "Liste"); (* Der Datenraum 'ds' wird unter dem Namen "Liste" in der Task "PUBLIC" gesichert *) delete (ds); (* nicht vergessen, sonst wird der belegte Platz nicht wieder freigegeben, bevor die Task gelöscht wird *) . bearbeite liste: liste [314] := pi;
Beispiel für die Verwendung eines benannten, von der Dateiverwaltung der Task verwalteten, Datenraums:
BOUND ROW 10000 STRUCT (REAL wert, INT position) VAR liste; (* 'liste' soll in einem Datenraum abgelegt werden *) create ("liste2") ; liste := dataspace ("liste 2"); (* 'liste' wird an einen neuen Datenraum angekoppelt. Dieser Datenraum wird unter dem Namen "liste 2" von der Dateiver- waltung der Task verwaltet *) bearbeite liste; (* Das Datenobjekt 'liste' wird irgendwie bearbeitet *) programmende;
In den bislang erörterten Prozeduren wurde zumindestens implizit vorausgesetzt, daß ein Datenraum ein Datencontainer ist. Er wird als Speicherobjekt für eine Datenstruktur aufgefaßt. Der Zugriff auf den Inhalt des Datenraums setzt Kenntnis der inneren Struktur voraus. Es kann jedoch auch notwendig sein, ohne Beachtung der inneren Struktur auf den Inhalt eines Datenraums zuzugreifen. Beispielsweise wird diese Möglichkeit benutzt, um in der Task hardware configurator in eine (Binär-)datei, die zum Standarddatenraum eines Treibers wird, die vom Benutzer eingestellte Interruptnummer, Portadresse und dergleichen einzutragen. Der in dieser Datei enthaltene ausführbare Code kann schwerlich als Datenstruktur betrachtet werden, nichtdestotrotz müssen Schreib/Leseoperationen in solchen Datenräumen möglich sein.
Für derartige Zwecke gibt es den Datentyp BOUNDPTR. Variablen dieses Typs liefern für einen gemappten Datenraum eine Basisadresse, die, durch ein Offset ergänzt, Zugriff auf den Inhalt des Datenraums ermöglicht.
REF INT PROC address (BOUNDPTR CONST boundptr)
PROC close (BOUNDPTR VAR bound ptr)
DATASPACE PROC dataspace (BOUNDPTR CONST boundptr)
PROC open (DATASPACE CONST space, INT CONST map size BOUNDPTR VAR bound ptr)
LET mb16 = 0x 0100 0000 , config area = 0x80 ; BOUNDPTR VAR std space ; .... open (dataspace("stdds"), mb16, std space); ... drivers ip := VAL (REF INT [address (std space) + config area]) ;
Soll sagen: die Startadresse für die Aktivierung des Treibers, also der Anfangswert seines Befehlszählers (drivers ip) wird aus dem Konfigurationsbereich des Programms gelesen. Dieses Programmsegment ist aus dem "install" Programm zu einem Treiber abgeleitet. Es zeigt ganz nebenbei, daß auch die Benutzung von BOUNDPTRn Kenntnisse über die innere Struktur des Datenraums erfordert, da ohne nähere Kenntnisse über die config area auch ein BOUNDPTR nur zum Herumstochern taugte.
Prozesse und damit insbesondere auch Tasks sind die einzig globalen Objekte in einem L3 System. Es muß für jeden Prozeß eine eindeutige Benennung im System geben. Insbesondere müssen Prozesse auch über ihre Lebensdauer hinaus eindeutig identifizierbar sein. Wäre diese Sicherheit nicht gegeben, bestünde z.B. die Gefahr, daß nach Löschen einer Task durch ihren Besitzer ein anderer Benutzer im System eine gleichnamige Task einrichtet und zum Empfänger von Daten wird, die nicht für ihn bestimmt sind.
Die Sicherheit der Benennung wird bei L3 durch vom Systemkern vergebene interne Taskbezeichner erreicht. Ein solcher Bezeichner besteht aus einem Taskindex und einer Generationsnummer. Der Index ist der zeitinvariante Teil des internen Taskbezeichners. Durch Kombination des Index mit einer Generationsnummer ist sichergestellt, daß Tasks auch über die Zeit eindeutig identifizierbar sind. Bei erneuter Vergabe des Index wird die Generationsnummer hochgezählt, so daß auch bei »alten« Systemen die Eindeutigkeit der Taskbezeichner sicher ist.
Diese Taskbezeichner können mit Hilfe von Taskvariablen benutzt werden. Bei benannten Tasks kann der Bezeichner durch den Operator / bzw. die Prozedur task erhalten werden. Unbenannte Tasks, die im Taskbaum mit ihrem »Pseudonamen '-'« ausgewiesen sind, können über Verwandschaftsbeziehungen erreicht werden.
Für einige standardmäßig in einem L3 System vorhandene Tasks und einige der an sich unbenannten Realprozesse gibt es vereinfachende Schreibweisen bzw. Pseudonamen, die die Identifizierung vereinfachen.
Das gesamte Tasksystem wird von der Task SUPERVISOR verwaltet. Der SUPERVISOR kann als einzige Task im System spezielle Verwaltungsoperationen auf Objekten vom Typ Task ausführen. Erzeugen, Anhalten und Löschen einer Task geschehen also stets durch Aufträge an die Task SUPERVISOR. Informationen über den Aufbau des Systems, also Namen und Vater/Sohn Beziehungen der Tasks verwaltet der SUPERVISOR im Systemkatalog. Falls eine Task Informationen aus dem Systemkatalog benötigt, kann sie durch access catalogue eine Kopie dieses Katalogs vom SUPERVISOR anfordern.
Neben den Operationen, die allgemein den Typ TASK und die Verwandschaftsbeziehungen zwischen Objekten dieses Typs betreffen, gibt es weitere, die die Interna einzelner Tasks behandeln. Darunter fallen storage (belegter Speicherplatz), Tasknummer (index) oder Statusangaben. Alle diese Angaben stammen aus dem Prozeßkontrollblock (PCB) der jeweiligen Task im Systemkern. Die Prozeduren am Ende dieses Abschnitts stellen die ELAN Schnittstelle zum Systemkern dar.
Die Tasks sind in drei Privilegklassen eingeteilt. Das höchste Privileg hat die Task SUPERVISOR. Nur diese Task darf Objekte des Typs TASK anlegen und löschen. Die mittlere Stufe, das Systemprivileg, haben die Tasks des Systemzweigs. Diese Tasks können insbesondere für jede Task, außer ihren eigenen Vorfahren, einen Ende Auftrag absetzen, der von SUPERVISOR akzeptiert wird. Man kann also z.B. von der Task OPERATOR aus jede Task außer SUPERVISOR und SYSUR löschen (VORSICHT!).
Das niedrigste Privileg (das Benutzerprivileg) haben die Task PUBLIC und ihre Nachkommen. In diesem Zweig des Systems dürfen bzw. können Tasks nur sich selbst oder ihre Nachkommen löschen.
Privilegien werden vererbt, Tasks unter SYSUR haben also stets das Systemprivileg, Tasks unter PUBLIC das Benutzerprivileg.
Bei einem Betriebssystem mit virtueller Speicherverwaltung und Datenraumsharing (»faules Kopieren«) kann für eine Task nicht angegeben werden, daß sie z.B. x Prozent des Plattenplatzes einnehmen darf, da das Erreichen eines solcher Wertes nicht effizient bestimmt werden kann. Um andererseits zu verhindern, daß Tasks das System unkontrolliert bis zur Aktionsunfähigkeit vollschreiben, kann für alle Tasks eine Schranke vorgegeben werden, bei deren Erreichen sie von der Arbeit zeitweise suspendiert werden. So wird erreicht, daß diese Tasks keine neuen Datenräume anlegen etc. Zumindestens einigen Tasks mit Systemprivileg muß zugestanden werden, bis 100% agieren zu dürfen, um gegebenfalls von hier aus andere Tasks zu löschen. Für Tasks mit Benutzerprivileg sollte dagegen eine Schranke angegeben werden, die mindestens 10 MB Freiraum läßt. Die Einstellung dieser Schranke kann im privilegierten Systemzweig mit der Prozedur limit vorgenommen werden.
L3 ist ein Timesharing Betriebssystem. Die Zuteilung von Rechenzeit an Tasks erfolgt durch den Scheduler im Systemkern, der allen Prozessen reihum eine Zeitscheibe von 10 Millisekunden anbietet. Dieses Verfahren wird trotz der großen Anzahl von Prozessen in einem L3 System durch einige Randbedingungen sehr effizient:
Grundsätzlich erhalten nur Systemprozesse (Treiber, Kern) jede ihrer mögliche Zeitscheiben. Einige wichtige Tasks (Supervisor, CONSOLE) werden nur einmal (prio = 1), normale Tasks werden generell zwei- (prio = 2) bis fünfmal (prio = 5) bei der Vergabe übergangen. Desweiteren warten die meisten Prozesse im System auf die Übertragung einer Botschaft. Falls die Botschaft noch nicht da ist, verbraucht der Prozeß nicht seine zustehenden 10 Millisekunden, sondern teilt dem Kern per Systemcall mit, daß der Scheduler den nächsten Prozeß bedienen kann. Durch dieses Verfahren bleiben im Endeffekt nur wenige Prozesse, die ganze Zeitscheiben verbrauchen. Die Tasks haben, wie oben erwähnt eine Priorität zwischen 2 und 5. Diese Priorität wird dynamisch vergeben: Tasks, die viel Rechenzeit verbrauchen, also selten oder garnicht warten, sondern »rechnen«, sinken in ihrer Priorität, Tasks, die viel von ihren Zeitscheiben »weitergeben«, weil sie z.B. im wesentlichen auf Eingaben warten, werden belohnt und behalten ihre Priorität 2.
OP := (TASK VAR left, TASK CONST right)
BOOL OP = (TASK CONST left, right)
BOOL OP <> (TASK CONST left, right)
BOOL OP < (TASK CONST left, right)
BOOL OP <= (TASK CONST left, right)
TASK OP / (TEXT CONST task name)
TASK PROC brother (TASK CONST task)
TASK PROC father (TASK CONST task)
TASK PROC son (TASK CONST task)
BOOL PROC exists (TASK CONST task)
BOOL PROC exists task (TEXT CONST taskname)
BOOL PROC is niltask (TASK CONST task)
TEXT PROC name (TASK CONST task)
TASK PROC systemtask (TEXT CONST systaskname)
TASK PROC task (TEXT CONST task name)
TASK PROC xbrother (TASK CONST task)
BOOL PROC xexists task (TEXT CONST task)
TASK PROC xfather (TASK CONST task)
TEXT PROC xname (TASK CONST task)
TASK PROC xson (TASK CONST task)
INT VAR i; TASK VAR t; taskinfo; (* Im Systemkatalog sind die Tasks noch nicht vorhanden *) page; FOR i FROM 5 DOWNTO 1 REP begin("Task " + text(i),PROC unlink,t); PER; taskinfo; (* Im Systemkatalog sind alle fünf Tasks vorhanden *) FOR i FROM 1 UPTO 5 REP end (son (myself), quiet); access catalogue PER; taskinfo; (* Hiernach sind alle fünf Tasks im Systemkatalog wieder gelöscht *) (* Wäre access catalogue nicht aufgerufen worden, *) (* hätte es beim Löschen des zweiten Sohnes *) (* die Fehlermeldung "'end' unzulaessig" gegeben, *) (* da der taskeigene Katalog nicht aktuell war. *)
PROC begin (TEXT CONST new taskname)
PROC begin (TEXT CONST new taskname, father)
wait for SYSIO => 'fathertask' hat sich nicht selbst vom Terminal abgekoppelt
PROC begin (TEXT CONST new taskname, PROC startproc, TASK VAR t)
PROC begin (PROC startproc, TASK VAR t)
PROC end (TASK CONST victim, QUIET CONST quiet)
Der Auftrag wird nur akzeptiert, wenn die auftraggebende Task eine höhere Privilegstufe als 'victim' hat oder 'victim' ein Nachkomme des Auftraggebers ist.
PROC rename myself (TEXT CONST new name)
TASK PROC callee (TASK CONST task)
DINT PROC cpu time (TASK CONST task)
INT PROC dataspaces (TASK CONST task)
INT PROC index (TASK CONST task)
PROC limit (TASK CONST task, INT CONST limit)
INT PROC limit (TASK CONST task)
PROC prio (TASK CONST task, INT CONST prio)
INT PROC prio (TASK CONST task)
INT PROC privilege (TASK CONST task)
INT PROC status (TASK CONST task)
INT PROC storage (TASK CONST task)
DINT PROC wakeup time (TASK CONST task)