
                           BlocksWorld
               Ein hierarchisches Filesystem in FORTH


      Autor:
         Alexander Burger, Max-Reger-Str.18, D-8050 Freising, 08161-83264

      Zusammenfassung:
         Es wird die Implementation eines einfachen Filesystems in Forth
         beschrieben, dessen Kommandos sich an UNIX anlehnen, das aber
         unabh|angig von einem Host-Betriebssystem arbeitet und intern die
         traditionelle Block-Struktur der Forth-Files aufrechterh|alt.

      Schl|usselworte:
         SCSI, Files, Directories, Subdirectories, Pathnames


      Das Sch|one an FORTH ist, da|s man vom System her direkt dazu
      ermuntert wird, alles nach eigenen W|unschen und Vorstellungen
      zu gestalten.

      1986 bekam ich die Laxen/Perry Implemetation [1] von Forth 83 in die
      Finger. Ich hatte schon immer davon getr|aumt, mich n|aher mit der
      6809-CPU zu besch|aftigen, und beschlo|s daher, ein Stand-Alone-System
      mit dieser CPU und dem modifizierten Laxen/Perry System zu bauen.
      Das Ganze passte auf eine Europa-Karte (CPU, 32K RAM, 32K EPROM,
      ein serieller und zwei parallele Chips). Der Meta-Compiler von
      Laxen/Perry wurde umgeschrieben, auf da|s er ROM-f|ahigen 6809-
      Code erzeugte, und an einem der Parallel-Chips wurde ein SCSI-
      Interface in Software installiert.
      Auf der daran angeschlossenen Harddisk und den zwei 3.5" Floppies
      lie|sen sich nun per SCSI-Kommando bequem individuelle Blocks
      (zu je zwei 512-kByte Sektoren) lesen und schreiben. Wie aber sollte
      man nun diese gro|sen Datenmengen unter Kontrolle behalten?

      Traditionelle Forth-Systeme verwalten ihre Massenspeicher nach einem
      sehr simplen Schema: Ihre Vorstellung von der Welt ausserhalb ihres
      direkt adressierbaren Speichers beschr|ankt sich auf eine geordnete
      Folge von 1-kByte Blocks, auf die nach der Methode der sogenannten
      "Virtuellen Speicherverwaltung" |uber Blockpuffer zugegriffen wird.
      Obwohl dadurch der Filestruktur etliche Beschr|ankungen auferlegt
      werden, lassen sich auch deutliche Vorteile erkennen: Kurze Zugriffs-
      zeiten und eine einfache Implementation.
      Die eigentlichen Probleme liegen darin, da|s man sich die Blocknummern
      seiner Files auf der Disk merken und Kommandos wie "126 load" oder
      "221 edit" eingeben mu|s. Zur Erleichterung des Lebens existieren
      Utilities, die z.B. die jeweils ersten Zeilen aufeinanderfolgender
      Blocks ausgeben und so bei der Datensuche helfen.
      Daher benutzen die meisten kommerziellen und public-domain Forth-
      Systeme heutzutage das Filesystem der Hostmaschine, wobei sie
      manchmal sogar noch die alte Blockstruktur oben draufsetzen (so auch
      Laxen/Perry). In diesem Falle wird auf einzelne Blocks durch Angabe
      von Filename und Blocknummer zugegriffen. Leider handelt man sich
      damit aber die Nachteile beider Welten ein: Schlechte Performance
      und Abh|angigkeit von einem vorgegebenen Betriebssystem - und die
      Unflexibilit|at der Blockfiles.

      Bei einem Stand-Alone System f|allt diese M|oglichkeit weg. Ich
      beschlo|s, ein simples hierarchisches Filesystem zu entwickeln,
      das die Block-Philosophie beibeh|alt, aber Directories und Subdirec-
      dories als einzelne Blocks und Files als sequenziell aufeinander-
      folgende Blocks unterh|alt. Die Kommandos "pwd", "ls", "cd", "rm"
      und "mkdir" wurden von UNIX entliehen, obwohl sie nat|urlich bei
      weitem nicht die M|achtigkeit der Originale erreichen.
      Ich nannte es "BlocksWorld"; der Name hat aber nichts mit dem
      ber|uhmten KI-Programm zu tun.
      Im Herbst 1986 lief der erste Prototyp, zun|achst cross-kompiliert
      auf einem CP/M-Rechner, dann stand-alone. Seitdem war das System viel
      in Benutzung und entwickelte sich weiter.


                  Directories und Subdirectories

      Alle angeschlossenen Harddisks und Floppies sind logisch in 1024-
      Byte-Blocks unterteilt. Die Blocknummern z|ahlen von 1 bis zu einem
      Device-abh|angigen Maximalwert, der auf meinem System 719 f|ur die
      beiden Floppies und 22139 f|ur die 20-MByte Harddisk betr|agt.

      Der erste Block auf jedem Device enth|alt das Root-Directory.

      Ein Directory besteht immer aus einem einzelnen 1024-Byte-Block und
      kann maximal 64 Eintr|age enthalten. Jeder Eintrag spezifiziert
      entweder einen File oder ein Subdirectory.

      Fig.1 zeigt die Struktur eines Directory-Eintrages. Er besteht aus
      drei Feldern: Einem 2-Byte "Block Field", einem 2-Byte "Size Field"
      und einem 12-Byte "Name Field".
      Das Block-Feld enth|alt die Nummer des ersten Blocks des Files, oder
      Null wenn dieser Directory-Eintrag nicht belegt ist.
      Das Size-Feld gibt die Gr|o|se des Files in Blocks an, wobei Null
      ein Subdirectory kennzeichnet.
      Filenamen sollten nicht l|anger als 12 Bytes sein und d|urfen aus
      allen ASCII-Zeichen au|ser Blanks und Slashes ('/') bestehen. F|ur
      Namen mit weniger als 12 Bytes wird der Rest mit Blanks aufgef|ullt,
      und zu lange Namen werden einfach abgeschnitten.

      Die ersten beiden Eintr|age jedes Directories sind immer "." und "..".
      Sie dienen als Verweise auf sich selbst und auf das Parent-Directory.
      Wenn ein neues Directory mit "mkdir" kreiert wird, werden diese
      beiden Eintr|age automatisch erzeugt.
      Im Root-Directory befindet sich au|serdem immer ein spezieller File
      "heap". In ihm stecken alle freien Blocks im Filesystem, und er
      kann ge|offnet und benutzt werden wie ein normaler File (meist f|ur
      tempor|are Daten oder als Scratch). Sobald aber Blocks f|ur einen
      neuen File mit "mkdir" or "mkfile" angefordert werden, mu|s er sie
      herausr|ucken und sich dabei entsprechend verkleinern lassen.

      Fig.2 zeigt Block 1 eines frisch initialisierten Filesystems auf
      einer 720-kByte 3.5-Zoll-Floppy. Gem|a|s 6809-Convention findet
      sich das h|oherwertige Byte eines 16-bit-Wortes an erster Stelle.
      Die Block-Felder des "."-Eintrages und des ".."-Eintrages sind
      beide 1, d.h. sie zeigen auf sich selbst (Das Root-Directory ist
      als einziges sein eigenes Parent-Directory). Ihre Size-Felder sind
      Null, weil es sich um Directory-Verweise handelt.
      Der Heap-File beginnt bei Block Nummer 2 und hat die Gr|o|se 718
      (hex 0C2E), eins weniger als das Gesamtfassungsverm|ogen von 719
      Blocks. Alle |ubrigen 61 Directory-Eintr|age sind leer und enthalten
      daher die Blocknummer Null.

      Nun geben wir den Befehl "mkdir usr" ein, erzeugen also ein neues
      Directory mit dem Namen "usr". Fig.3 zeigt das ge|anderte Root-
      Directory: Der Heap-File beginnt nun bei Block Nummer 3 und hat noch
      717 (hex 02CD) blocks |ubrig, w|ahrend ein neuer Eintrag "usr" ein
      Directory in Block 2 anzeigt.
      Dieses neue Directory (in Fig.4) zeigt mit dem "."-Eintrag auf sich
      selbst (2) und mit dem ".."-Eintrag auf sein Parent-Directory, das
      Root-Directory in Block 1. Alle anderen Eintr|age sind leer.


                  Files

      Ein File ist einfach eine Anzahl zusammenh|angend aufeinanderfolgender
      Blocks. Je nach Anwendung kann er Forth Source-Screens, ASCII-Text
      oder bin|are Daten enthalten. Sobald ein File ge|offnet ist, kann
      jeder seiner Blocks mit einem einzigen Diskzugriff erreicht werden,
      da sich seine logische Adresse als Summe seiner relativen Nummer
      und des Startblocks des Files errechnen l|a|st.


                  Devices und Pathnames

      Jedes einzelne BlocksWorld Filesystem residiert auf einem eigenen
      SCSI-Device und wird durch eine Nummer ("Device Number") identifiziert.
      In meinem 6809-System ist die Harddisk das Device Nummer 0 und die
      Floppies sind 1 bzw. 2.
      Da 16-bit-Zahlen f|ur die Blocknummern verwendet werden, ist die
      maximale Gr|o|se eine Filesystems 65535 Blocks (Block Null wird nicht
      benutzt: "reserved" auf deutsch). Harddisks mit gr|o|serer Kapazit|at
      als 64 MByte m|ussen in mehrere logische Devices aufgeteilt werden.
      Ein Pathname identifiziert einen gegebenen File. Wenn er mit einer
      Nummer (d.h. der Device Number) beginnt, hei|st er "absoluter"
      Pathname. Zum Beispiel, das Kommando

         cd 1/usr/simul  ok

      macht "simul" zum aktuellen Directory. Ein "relativer" Pathname, z.B.

         cd ../languages  ok

      ist also einer, der nicht mit einer Nummer beginnt. Es spezifiziert
      den Zugriffspfad relativ zum aktuellen Directory.

      Im Gegensatz zu UNIX-|ahnlichen Systemen versteht BlocksWorld Pathnamen
      nur im Zusammenhang mit dem "cd"-Kommando. Um mit einem File zu
      arbeiten, mu|s zuerst mit "cd" sein Directory aufgesucht und er dann
      mit "open <file>" ge|offnet werden. Das ist nicht so unbequem wie es
      sich zun|achst anh|oren mag, weil "open" einen Dictionary-Eintrag
      f|ur den File kreiert und somit auf den File sp|ater einfach durch
      Exekutierten seines Namens zugegriffen werden kann, auch wenn das
      aktuelle Directory mittlerweile ge|andert wurde.


                  File Control Blocks

      Informationen |uber offene Files findet sich in "File Control Blocks"
      (FCB's). Ein FCB (siehe Fig.5) is einem Directory-Eintrag |ahnlich,
      besitzt aber zus|atzlich ein "Dev-Field" f|ur die Devicenummer. Ein
      File wird somit vollst|andig beschrieben durch Devicenummer,
      Blocknummer und Filesize. Viele Files auf verschiedenen Devices k|onnen
      gleichzeitig offen sein, ihre Anzahl ist nur durch den vorhandenen
      Speicher begrenzt.


                  L|oschen von Files

      Wie Ihnen vielleicht aufgefallen sein wird, gibt es bei dem hier
      beschriebenen Filesystem einen Problempunkt: Wie kann man einen
      nicht mehr ben|otigten File oder ein Directory l|oschen?
      Man kann zwar einfach eine Null in das Block-Feld im Directory-Eintrag
      schreiben (und somit den Eintrag als "frei" markieren), wird damit
      aber nicht die vom File belegten Blocks zur Wiederverwendung
      freigeben (Wie oben beschrieben, werden Blocks f|ur neue Files stets
      vom "heap" Systemfile abgezwackt).

      Die "richtige" L|osung l|age wohl bei der Verwendung von File-
      Management- Tabellen und komplizierteren Algorithmen (und mithin
      der Einf|uhrung von fragmentierten Files).
      Hier jedoch wird eine ziemlich brutale Methode angewandt: Alle Blocks
      im Filesystem, die sich oberhalb des gel|oschten Files befinden, werden
      verschoben und schlie|sen so die L|ucke. Danach m|ussen noch alle
      Directory-Eintr|age, die auf verschobene Files zeigen, aktualisiert
      und der "heap"-File entsprechend vergr|o|sert werden.

      Nach nunmehr mehrj|ahriger Benutzung kann ich sagen, da|s das eigentlich
      gar keine so schlechte L|osung ist. Wenn der File noch ziemlich jung
      war, mu|s nicht viel bewegt werden (oder gar nichts, wenn es der zuletzt
      kreierte File war), und die Verschiebeoperation geht recht schnell mit
      dem SCSI Copy-Kommando, das direkt von Device zu Device kopiert.
      Alte Files hingegen l|oscht man recht selten. Au|serdem wird man in
      vielen F|allen die Blocks durch einfaches Umbenennen des Files
      wiederverwenden k|onnen.


                  Die Implementation

      Das Listing zeigt die 17 Source-Screens, die hier relevant sind,
      zusammen mit ihren Shadow-Screens (Shadows enthalten ausschlie|slich
      Kommentare und werden zur Rechten der zu ihnen geh|orenden Sources
      ausgedruckt). Einige von ihnen (2, 3, 4, 5 und 17) sind noch Relikte
      der urspr|unglichen Laxen/Perry-Implementation. Sie haben sich aber
      in einigen Details ge|andert und sind darum hier aufgef|uhrt.
      Nicht aufgef|uhrt sind die (systemabh|angigen) Primitiv-Funktionen
      f|ur den SCSI-Zugriff:

         - scRead (bufHead -- )
            Liest einen 1024-Byte-Block vom SCSI-Device in den Blockpuffer
         - scWrite (bufHead -- )
            Schreibt einen 1024-Byte-Block vom BlockPuffer zum SCSI-Device
         - scCopy (srcBlk srcDev dstBlk dstDev -- )
            Kopiert einen Block direkt von Device zu Device.


                  Command Line Flags

      Screen 1 zeigt ein Werkzeug, das sich auch bei anderen Anwendungen
      als n|utzlich erwiesen hat. Es emuliert das Verhalten von Command
      Line Flags und dient zur Modifikation bestimmer Befehle.
      Das "ls"-Kommando zum Beispiel akzeptiert die Flags "-l" (: long)
      und "-t" (: tree), siehe Fig.6:
      - Ohne Flag schreibt es die Filenamen in eine Zeile, wobei es
        Directories am Ende mit einem Slash '/' markiert.
      - Mit der "-l"-Option zeigt es auch den Startblock und die Gr|o|se
        f|ur jeden File.
      - Die "-t"-Option l|a|st "ls" rekursiv den Directory-Baum absuchen
        und druckt alle Files und Directories unter dem aktuellen Directory
        aus.
      Wie Fig.6 zeigt, kann man auch die Kombination von "-t" und "-l"
      aufrufen (Das Ausgedruckte wird aber etwas unleserlich).

      Es k|onnen maximal 16 verschiedene Flags definiert werden. Nat|urlich
      k|onnen verschiedene Kommandos die selben Flags verwenden. Ich habe
      beispielsweise ein "ps"-Kommando, das alle gegenw|artig aktiven Tasks
      anzeigt und auch die "-l"-Option akzeptiert, oder eine File-Backup-
      Utility, die mit "-v" nach dem Kopieren auch verifiziert.

      Kommandos, die Command Line Flags verwenden wollen, k|onnen die
      einzelnen Flags mit "-x?" testen und sollten nach Beendigung ihrer
      Aufgabe alle Flags mit "clrFlags" l|oschen.


                  Blockzugriffe

      Screen 2 definiert ein paar oft ben|otigte Konstanten und die Block-
      Puffer-Arrays und -Variablen. Die Screens 3 bis 5 implementieren die
      virtuelle Speicherverwaltung.
      Die Worte "buffer" und "block" (Screen 4) sind die wichtigsten
      Werkzeuge f|ur den Filezugriff. Sie verhalten sich genau wie in der
      Laxen/Perry-Version, erwarten also eine Blocknummer auf dem Stack
      und geben eine Pufferadresse zur|uck.
      Anders "(buffer)" und "(block)": Sie brauchen jetzt eine Device-
      und eine Blocknummer als Argumente, um den Zugriff auf jeden beliebigen
      Block im System zu erm|oglichen.


                  Initialisierung

      Screen 6 kreiert vier FCB's f|ur den internen Gebrauch, und sechs
      kurze Worte, die den Zugriff auf die einzelnen FCB-Felder erleichtern.

      Die doppeltgenaue Variable "dir#" in Screen 7 enth|alt die Device-
      und Blocknummer des aktuellen Directorys. Ihr Initialwert ist Block
      Eins auf Device Null, in meinem Falle das Root-Directory auf der
      Harddisk.

      Das Word "directory" ist sehr n|utzlich f|ur den schnellen Directory-
      Wechsel. Es wird in der Form

         directory <dirname>  ok

      benutzt. Ein Word "<dirname>" wird kreiert, das sich das gegenw|artig
      aktuelle Directory merkt und so sp|ater aus einem beliebigen anderen
      Directory durch einfaches Executieren von "<dirname>" eine R|uckkehr
      erm|oglicht.

      Screen 8: "setName" liest einen Filenamen bis zu einem vorgegebenen
      Delimiter (das mag ein Blank oder ein Slash sein). "setName" wird
      von "setFcb" zur Initialisierung eines File Control Blocks verwendet,
      aber auch von den Worten "ren" und "from".
      "dirEnter" durchsucht das Directory in einem Blockpuffer nach einem
      unbenutzten Eintrag und installiert die Daten aus dem FCB dort. Wird
      kein freier Platz gefunden, erfolgt ein Abbruch.

      "fallot" implementiert den oben beschriebenen File Allocation
      Mechanismus, indem es den "heap"-File verkleinert und die Blocknummer
      des ersten so freigewordenen Blockes zur|uckgibt.

      "mkdir" ist das Top-Level-Kommando zur Erzeugung neuer Directories, es
      erwartet den Directory-Namen im Input-Stream, reserviert einen Block
      auf der Disk und initialisiert ihn als Directory.

      Mit "mkfile" (Screen 10) wird wird ein neuer File kreiert. Das Kommando

         40 mkfile Test  ok

      erzeugt den File "Test" mit einer Gr|o|se von 40 Blocks im aktuellen
      Directory, und f|ullt alle Blocks mit Blanks.

      "mkdir" und "mkfile" |uberpr|ufen nicht, ob ein File gleichen Namens
      bereits im aktuellen Directory existiert. Falls dem so ist, bleibt
      einer der beiden Files solange unerreichbar, bis der andere umbenannt
      wurde.

      Zur Initialisierung eines Filesystems dient "mkfs". Die beiden Kommandos

         cd 1  719 mkfs  ok

      richten ein neues Filesystem auf der Floopy in Device 1 ein.
      Das Kommando "mkfs" ist gef|ahrlich. Es |uberschreibt das Root-Directory
      und fragt vorher nicht nach einer Best|atigung. Wenn es einmal
      f|alschlich eingegeben wurde, kann aber ein sofortiger Druck auf den
      Resetknopf doch noch Rettung bedeuten, weil "mkfs" von sich aus keinen
      "flush" ausf|uhrt.


                  Suchen und Finden

      Screen 11 zeigt einige Worte zum Suchen und Ausdrucken von Filenamen.
      "pwd" in Screen 12 zeigt den absoluten "Path to Working Directory"
      - zum aktuellen Directory also - an, beginnend mit der Device-Nummer,
      gefolgt von den durch Slashes getrennten Directory-Namen:

         pwd
         0/usr/tools/math  ok

      Die eigentliche Arbeit wird dabei von "(pwd)" erledigt, einer rekursiven
      Routine die
      -  entweder die Device-Nummer ausdruckt, wenn ihr Argument gleich 1
         (Root-Directory) ist
      -  oder anderenfalls ihren eigenen Pathnamen ausdruckt, indem sie sich
         selbst mit ihrer Parent-Directory-Nummer aufruft, dann einen Slash
         und schlie|slich den aktuellen Directory-Namen anf|ugt.

      "ren" dient zum Umbenennen von Files und wird so benutzt:

         ren <oldName> <newName>  ok

      Zur |Anderung des aktuellen Directorys schreibe man

         cd <pathname>  ok

      "cd" schneidet sich die Directory-Namen aus dem Pathnamen heraus und
      ruft wiederholt "(cd)" auf. Wird irgendwo in dem String von Directory-
      Namen eine Zahl entdeckt, wird die Suche im Root-Directory des durch
      sie bestimmten Devices fortgesetzt. Siehe auch oben die Erkl|arung von
      absoluten und relativen Pathnamen.

      Screen 14 enth|alt das "ls"-Kommando. Auch hier wird die Arbeit an eine
      rekursive Funktion - "(ls)" - |ubergeben. Sie extrahiert alle Files
      im aktuellen Directory und druckt ihre Namen.
      Wenn das "-l" Flag gesetzt ist, erfolgt der Ausdruck im "langen Format",
      bei dem auch die Startadresse und Filegr|o|se mit angezeigt werden.
      Das "-t" Flag l|a|st "(ls)" rekursiv Unterdirectories abarbeiten.
      Zu beachten ist, da|s "(block)" in jedem Schleifendurchgang neu
      aufgerufen wird. Es w|are nicht genug, lediglich einen Pointer in
      das Directory im Puffer zu halten, weil der Puffer bei sehr tief
      verschachtelten Subdirectories |uberschrieben worden sein kann.

      "rm" is in Screen 16 definiert. Wenn der zu entfernende File ein
      Directory ist, sollte es leer sein, anderenfalls wird sich "rm"
      beschweren. Das aktuelle Directory und der "heap"-File im Root-
      Directory werden auf den neuen Stand gebracht, die Gr|o|se des
      Files bestimmt und alle Blocks oberhalb mit der "scCopy" Funktion
      nach unten verschoben.
      Die rekursive Hilfsfunktion "adjDir" (Screen 15) durchsucht alle
      Directories und korrigiert Directory-Eintr|age, die auf verschobene
      Files oder Directories verweisen (Sch|atzen Sie, wie oft das
      Filesystem auf meiner Harddisk ruiniert wurde, bis diese Funktion
      endlich fehlerfrei (?) war).
      "rm" entfernt Files ohne Ansehen der Person. Es ist nicht ratsam,
      es beispielsweise auf "heap" im Root-Directory anzuwenden.

      Screen 17 schlie|slich zeigt die BlocksWorld-Versionen der Laxen/Perry
      Funktionen zum Definieren und |Offnen von Files.


                  Res|umee

      Das hier beschriebene Filesystem hat sich in der Forth-Umgebung als
      durchaus n|utzlich und effektiv erwiesen.

      Es bleiben aber noch viele Punkte, in denen das System verbessert
      werden k|onnte. Es w|are zum Beispiel nett, einen Time-and-Date-Stamp
      in jedem Directory-Eintrag zu haben, oder die Block- und die Size-
      Fields auf 32 bit Breite zu vergr|o|sern, um Filesysteme gr|o|ser
      als 64 MBytes zuzulassen. Pathnamen sollten nicht auf das "cd"-
      Kommando beschr|ankt sein, und man m|u|ste Werkzeuge wie "fsck"
      (zur |Uberpr|ufung des Filesystems) oder "cp" (zum Kopieren ganzer
      Subdirectory-B|aume) entwickeln.
      F|ur Leute, die lieber mit narrensicheren Systemen arbeiten (wohl
      keine Forth-Programmierer) w|are ein bi|schen mehr Error-Checking
      vonn|oten.


      Quellenangaben:
      1. Laxen/Perry Public Domain F83-Forth
         Forth Interest Group, Orange County Chapter
