Source Code









Was vorher an Dateisystem war

Der Fortschritt

Jetzt ein funktionierendes mkdir

Letzter Beitrag filesystem8.c
Die Sache funktioniert jetzt so: Ich stelle mir das Dateisystem als ein Array im Arbeitsspeicher vor. Das Dateisystem ist nun in den Arbeitsspeicher gespiegelt. Die Festplatte ist in Blöcke je 512 Byte unterteilt. Ich stelle mir jetzt die Festplatte gespiegelt in den Arbeitsspeicher - in einen Bereich des Arbeitsspeichers, einem Array - vor. Ich arbeite in diesem Bereich des Arbeitsspeichers mit 512 Byte Blöcken, auch wenn der Speicherbereich ein einziges Array ist. Jetzt, und auch während das Betriebssystem arbeiten wird, werde ich immer in diesem Arbeitsspeicherbereich arbeiten. Dieser Arbeitsspeicherbereich wird immer 1:1 auf die Festplatte kopiert.
Was jetzt zu realisieren ist, ist das Dateisystem. Dazu gibt es normalerweise folgende Aufrufe: MKDIR, READ, WRITE, OPEN, ... Diese realisiere ich gerade. Ich bin jetzt bei MKDIR.
Das Dateisystem ist ein einfaches Dateisystem: Es verwendet eine Freispeicherliste und jeder Block einer Datei zeigt am Ende auf die Anfangsadresse des nächsten Blocks.
Ein Verzeichnis ist eine Datei, mit der Dateigröße eines Blocks. Ein Verzeichnis kann nicht beliebig viele Dateinamen enthalten. Ein Dateiname ist 12 Byte groß. Die Datei eines Verzeichnis ist so aufgebaut, dass die Dateinamen hintereinander mit einer Größe von 16 Byte stehen, nach den 12 Byte des Dateinamens folgen 4 Byte für die Adresse der Daten der Datei. Jetzt zu unserem Betriebssystem. Für das Anwenderprogramm ist es nachher egal, wie das Dateisystem aufgebaut ist. Das Betriebssystem verheimlicht den Aufbau des Dateisystems vor dem Benutzer. Also ist es egal, dass ich ein so einfaches Dateisystem implementiere. Man muss also kein FAT12, FAT16, FAT32, NTFS, EXT, EXT2, EXT3, EXT4, ReiserFS, ... implementieren. Das Dateisystem muss von den Funktionsaufrufen stimmen.
Dann kommt der Schritt, bei dem man, diese Funktionsrufe, die man jetzt für das Dateisystem geschrieben hat, so in den Interruptvektor einbaut, so dass sie nachher als Systemaufrufe verwendet werden können. Die Grundlage für die allgemeine Verwendung von Systemaufrufen ist ja schon vorhanden. Man schaue auf meine Website. (Hier geht es um das erste starten des PC's, mit Textausgabe, ohne das ein Betriebssystem wie Linux oder Windows verwendet wird).
Bei diesem Schritt verwendet man dann einfach eine Interrupt-Nummer (wir würden 0x21 verwenden) und baut die Systemaufrufe für das Dateisystem entsprechend ein.
Es ließen sich relativ viele Funktion des MS-DOS-Systemaufrufs implementieren.
Nachdem das Dateisystem geschrieben wurde, folgt ein Systemaufruf zum laden einer ausführbaren Datei (zum EXE-Datei). Dieser Systemaufruf ist EXEC oder FORK. EXEC würde dabei EXE-Dateien aufrufen. Hier ginge es wie bei JPEG oder GIF um die genauen Bytes in einer EXE-Datei. Damit hätte man schon beinahe den gesamten MS-DOS-Systemaufruf 0x21 implementiert: Also, erst das Dateisystem - vollkommen frei - schreiben, die Funktionen werden nachher in die Systemaufrufe eingebaut, anhand ihrer Einsprungsadressen, dann folgt der Systemaufruf EXEC.


Die letzte Version ist in der Datei filesystem9.c enthalten. Sie enthält bereits mkdir. Bei der Überprüfung von mkdir ist mir allerdings aufgefallen, dass die Funktion get_sub_dir_names noch ein paar Fehler enthält. mkdir selber funktioniert im Wurzelverzeichnis, es ist allerdings noch ein Bug da, ich vermute, dass liegt an get_sub_dir_names
Korrektur des Fehlers im letzten Beitrag. Ich habe den Fehler korrigiert. Der vorletzte Beitrag mit dem Code enthält jetzt die Version, bei der, der Fehler gefunden und korrigiert wurde. MKDIR funktioniert jetzt einwandfrei.
Jetzt werde ich READ und WRITE realisieren.
Ich habe den vor-vor-letzten Beitrag mit Code noch mal geupdated, um die Sache ein klein bisschen klarer herauskommen zu lassen ... Ach ja! Ich werde noch ein open_path schreiben.

open_path und weiter Überlegungen...

So, jetzt habe ich den Code schon so weit geupdated, dass ich ein openpath habe. Das openpath gibt den Sektor an, an dem das letzte Verzeichnis steht. Die Ausgaben sind vom Sinn richtig. Somit tut openpath - aber eben nicht nur openpath, sondern auch mkdir.

Im nächsten Schritt folgt ein READ and WRITE.

Natürlich brauchen wir dazu auch ein Open. Die Sache wird dann wie folgt aussehen. Die letzten vier Byte eines Sektors einer Datei enthalten die Adresse der nächsten vier Byte des nächsten Sektors. Nun hätten wir ein Problem: Denn, wir müssen unbedingt die Dateilänge speichern. Denn, ein Sektor ist 512 Byte groß. Wenn eine Datei allerdings 412 Byte ist, kann man bei den übrigen nicht daher gehen und sagen, dann erscheinen in der Datei halt lauter 0en die nicht zur Datei gehören. Es gibt eine Menge Dateien, die einen Haufen 0en enthält. Also müssen wir auch die Dateilänge speichern. Das könnte ein Problem sein, ist es aber nicht. Denn wir speichern so oder so die Adresse des nächsten Sektors der Datei in einem Sektor, warum also nicht auch die Anzahl der Byte speichern, die, in diesem Sektor in Anspruch genommen werden. Diese Länge könnte man Ende des Sektors speichern, oder gleich am Anfang. Wir könnten überhaupt die gesamten Informationen, nämlich auch den nächsten Sektor, gleich am Anfang eines Sektors speichern. Eine andere Möglichkeit wäre, einen eigenen Sektor für jede Datei zu reservieren, die nur Informationen enthält, Informationen, wie 1.) Der erste Sektor 2.) Besitzer 3.) Rechte 4.) Datum usw. Wenn man so einen Sektor verwendet, der Dateiinformationen enthält, könnte man diesen auch gleich umbauen, und in diesen Sektor, der Informationen enthält, einfach alle Sektoren schreiben, die Nutzerdaten enthalten.

Für das Open muss man sich jetzt folgendes vorstellen: Wir brauchen einen Datei-Handler (File Handler). Denn wir haben nicht nur eine Datei geöffnet, sondern gleich mehrere. Und zu einer Datei, die geöffnet ist, müssen wir Daten speichern, die im geschlossenen Zustand nicht wichtig sind. Wie, zum Beispiel, die aktuelle Position in der Datei. Lesen wir eine Datei aus, dann lesen wir Byte für Byte. Aber wir können diese Aufgabe stoppen und uns mit etwas anderem beschäftigen. Wollen wir die Datei wieder weiter auslesen, dann beginnen wir nicht wieder von Anfang, sondern von der letzten Position, also müssen wir zumindest die letzte Position in einer Datei speichern. Wir müssen auch zurückgehen können. Dazu müssen wir die Datei nicht neu öffnen. Ist eine Datei geöffnet muss sie identifiziert sein und das geschieht mit dem File Handler. Der File Handler ist schlicht und ergreifend eine Zahl. Diese Zahl muss sich das Benutzerprogramm intern selber merken (Natürlich nicht für den Benutzer sichtbar, aber intern, wenn ein Programm in C programmiert wurde, existieren solche File Handler). Jedes Benutzerprogramm verfügt über Dateihandler. Auf der einfachsten Ebene von C sind das Integer-Zahlen. Jetzt brauchen wir einen Array. Für jede geöffnete Datei geben einfach eine Zahl wieder, nämlich die nächste freie. Wir brauchen einen Array, um für jede Datei bestimmte Informationen zu speichern, eben die Position usw. Und durch wen sie geöffnet wurde. Die File Handler sind also aus Sicht dieses Codes nicht einzigartig, sondern es gibt global eine Menge Datei-Handler.

Wo stehen wir?

Ein File-Handler - wie hat man sich diesen vor zu stellen? Zunächst ein Mal ein anderes Thema, zur weiteren Vorgehensweise. Jetzt hätten wir die Funktionen: Dazu werden kommen: Sind wir damit wirklich bedient? Man denke nach.
Zunächst ein Mal, zum interessanteren Teil: Wie kann man diese Funktionen überhaupt nutzen? Wir haben bisher Funktionen in C geschrieben. Diese scheinen im Raum zu hängen und mit einem Betriebssystem im wahrsten Sinne nichts zu tun zu haben.
Wir erfüllen aber folgende drei Wahrheiten:


Jetzt aber zu dem nächsten Teil, der steht im nächsten Beitrag.

Haben wir wirklich mit ein paar Routinen, wie oben genannt, alles bedient? Ist das unser Betriebssystem?
Zunächst ein Mal, wir werden den Interrupt 0x21 verwenden. Wie unter DOS.
Unter DOS sind wir es gewohnt, dass wir den Interrupt 0x21 ausführen und Parameter in dem Register AH übergeben, sowie in AX, BX, CX, DX. Wir können - zum Beispiel in wikipedia - jede Funktion implementieren. 0x09 in AH bedeutet etwas. Wir gehen alles Funktionen durch und implementieren alle.
Zur Implementation der Systemaufrufe - und hier trifft eben das eine das andere - ist zu sagen: Ich habe ja bereits auf meiner Website eine Menge Code, wo es um das Starten des Betriebssystems geht. Und da geht es auch um die Installation der Interruptroutinen. Man entsprechende Routinen eintragen. In DOS sind diese Routinen, auf eine gewisse Art und Weise implementiert. Bei diesem Betriebssystem anders. Aber es gibt dieselben: Datei öffnen und so weiter. Dass nachher, im Betriebssystem, die Parameter für die Funktionsaufrufe, in AX, BX, CX, DX übergeben werden, so, dass die Systemaufrufe wirklich im Betriebssystem verwendbar sind, stellt keinen Widerspruch zu der Realiserung mit C-Funktionen, die in einem User-Space-Prorgramm verwendet werden. Die Routinen sind dieselben.
So oder so haben wir schon in dem Quelltext, der beim Booten einen Text ausgibt, C-Funktionen verwendet. Das spielt keine Rolle, dass diese nicht Hardware-nah in Assembler realisiert sind. Wenn wir den Quelltext übersetzen, stehen dort nachher in Assembler oder Maschinencode umgewandelte Routinen. Dahinter ist keine Hexerei. Der Compiler erfüllt seinen Zweck. Und er würde nie anders übersetzen. Und ein Compiler wandelt jeden Programm-Code in C u.a. in Maschinencode um. Ist also der Compiler ein Hexer? Nein, die Übersetzung von C-Code in Maschinencode ist keine Hexerei. Wenn wir allerdings Routinen in C verwenden, müssen wir uns nur vorstellen: Eine Variable hat 1.) einen Namen 2.) einen Wert 3.) eine Adresse 4.) einen Typ. Eine Funktion hat allerdings auch eine Adresse. Eine Variable ist eine benannte Speicherstelle, eine Funktion allerdings auch. Und diese Adresse führen wir nachher geschickt zu den Interrupts. So, dass bei jedem Interrupt eine entsprechende Routine, die wir bisher einfach in C programmier haben, aufgerufen wird.
Eine ganz andere Frage ist: Haben diese Routinen jetzt überhaupt das geleistet, was sie sollen Und die Antwort lautet, man ist ganz nahe dran. Nun gut, da fehlt noch eine Routine, näh (eigentlich sind es zwei), die für den Terminal. Ein und Ausgabe auf dem Bildschirm. Das ist ein typisches Programm in Assembler einen Text auf dem Bildschirm ausgeben. Mit MOV AH, 09h INT 21h. Aber, das kann der bestehende Code-Teil ja bereits vollständig leisten.
Man kann den Computer mit dem bisherigen Code bereits so starten, dass er einen Text ausgibt. Natürlich lassen sich diese Routinen wieder in den Interrupt 21h einbauen. Damit hätte man bedient: Tastatur, Bildschirm, Festplatte. Damit ist viel bedient. Nun kann man Dateien speichern und Text auf dem ausgeben und von der Tastatur einlesen. Für die Benutzerprogramme ist der Terminal nicht automatisch die erst wichtige Bedeutung, sondern die Routinen von gerade eben. Die Eingabe per Tastatur und die Ausgabe am Bildschirm lässt sich aber ebenso integrieren, nachdem sie nun mal da sind, wie die Funktionen READ, WRITE, ...
Zur Textausgabe ist natürlich eine Frage interessant: Wie kommen denn die Buchstaben überhaupt dahin. Nun gut, die Buchstaben sind generell bereits in der Hardware enthalten. Schreibt man in einen bestimmten Teil vom Arbeitsspeicher erscheinen automatisch diese Buchstaben. Sind wir von Graphik weit entfernt? Nicht automatisch. Denn ebenso, wie sich die Buchstaben dadurch von selbst darstellen, nämlich am Bildschirm, wenn man in einen bestimmten Teil des Arbeitsspeichers schreibt, so lassen sich auch Punkte erzeugen, in dem man in einen bestimmten Teil des Arbeitsspeicher schreibt. Schreibt man einen Wert an eine Stelle, hat man eben einen Pixel beschrieben.
Die komplexen Graphiken, die darauf folgen, sind eine Frage des Graphischen Benutzerfrondends, wie X oder KDE oder GNOME. Dann muss man entweder sein eigenes komplexes Graphikfrontend anbieten, was ein Haufen Arbeit ist, oder aber, man bietet Schnittstellen an, die X erlauben zu funktionieren. ... Das sind andere Themen.
Jetzt zu den Funktionen.
Hat man Zugriff Dateien, bei denen man Ordner hat und das Dateisystem auf der Festplatte liegt, hat man desweiteren Zugriff auf Tastatur und Bildschirm hat man, mit denen folgenden Funktionen EXEC und EXIT viel erreicht.
Dazu mehr im nächsten Beitrag.


Zunächst: Hat man Dateisystem, Bildschirm und Tastur, sowie EXEC und EXIT, hat man so gut wie alles. Ein Programm muss Dateien speichern können. Aber es kommt noch besser. Ein Programm liegt als Datei auf der Festplatte in dem Dateisystem. EXEC heißt, wir rufen aus einem Programm ein anderes Programm auf und das ist nicht nebensächlich. Man stelle sich die COMMAND.COM unter DOS vor, mit dem Pendant bash und Linux oder UNIX. Ruft man hier ein Programm auf, so ruft die Bash oder die COMMAND.COM dieses Programm über EXEC auf.
Generell wird hier das Programm in den Arbeitsspeicher geladen, dass als Datei auf der Festplatte ist, es wird 1:1 in den Arbeitsspeicher kopiert, mit ein paar Änderungen, an dem Dateikopf oder was auch immer - vielleicht (!) - aber der Programmcode wird 1:1 in den Arbeitsspeicher kopiert. Und hier heißt MOV im Programm auf der Festplatte, eben MOV in dem Programm im Arbeitsspeicher und da gibt es kein Wenn-und-Aber. Der Code ist derselbe und nichts anderes. Bei EXEC wird dieser Code in den Arbeitsspeicher geladen und der Instruction Point IP, sowie das Code Segment CS, werden auf die Adresse in den Arbeitsspeicher initialisiert, in der der entsprechende Code liegt. Das geschieht über einen JMP Befehl. Denn bei Intel können wir IP nicht direkt neu laden, wie mit MOV. Nun wird der Code des Programms ausgeführt. Am Ende - und das ist entscheidend! - muss im User-Programm ein EXIT stehen. Tut es das nicht, bedeutet, der IP läuft weiter und weiter und über die Grenze des Benutzerprogramms hinweg. Stattdessen müssen wir am Ende aber ins Betriebssystem zurückspringen. Dies geschieht, wie immer mit einem INT-Befehl. Einem Systemaufruf. Und den gibt es auch unter DOS. Es gibt einen Assemblerbefehl mit INT 0x21 und dem entsprechenden Code in AH. Damit springt man ins Betriebssystem und wenn dieser Befehl, mit allen Parametern, EXIT enspricht, dann wird eben im Betriebssystem weitergearbeitet, so entscheidet dann, das Betriebssystem. Somit ist EXIT nicht unnötig.
Dem aber nicht gut.
Zu unserer EXE-Datei. Unter Windows oder DOS ist eine ausführbare Datei eine EXE-Datei. Dabei enthält diese den Code, der 1:1 ausgeführt wird. Diese EXE-Datei ist in gewisser Hinsicht so, wie ein JPEG, was die Verwendung von Strukturen betrifft. Oder wie eine BMP. Eine BMP enthält an einer bestimmte Stelle ein gewisses Byte oder Wort, dass enthält, wie breit und wie hoch das Bild ist. Und so weiter. Dementsprechend tut das auch eine EXE-Datei. Auch hier stehen Informationen, wie, die Größe des Codes-Segments, oder die Magik Number. Für EXEC ist es nicht nur entscheidend, dass der Code im Arbeitsspeicher gelagert wird, die Register entsprechend belegt werden, nämlich die Segmentregister, und, dass der Instruction Pointer auf den Code zeigen wird, der ausgeführt werden soll, sondern, dass die Datei geöffnet wird, damit sie überhaupt in den Arbeitsspeicher kopiert werden kann. Und dazu braucht ein EXEC eben automatisch die Funktionen, die beim Dateisystemzugriff nötig sind. Nämlich: Datei öffnen, Datei lesen und so weiter.

Dazu mehr im nächsten Beitrag.


Jetzt. Für eine Datei ist ein File-Handler nötig. Denn, sollten wir uns in die weiten eines Multitasking-Betriebssystem vorwagen, dann kommen wir schnell dahin, dass mehrere Dateien geöffnet sind, oder: Ein Programm hat mehrere Dateien, die gleichzeitig geöffnet sind. Dann braucht man einen Filehandler. Man muss die Datei identifizieren können, die zum Lesen oder Schreiben geöffnet wurde. Man muss Informationen ablegen, wie, ob sie zum LESEN, SCHREIBEN oder was auch immer geöffnet ist. Am einfachsten ist es, eine Datei zu identifizieren, indem man eine Zahl verwendet. Diese Zahl ist global im gesamten Betriebssystem vorhanden. Auch das ist quasie ein File-Hander, einfache eine Ganz-Zahl. Die erste Datei, die geöffnet wurde, erhält die Zahl 0, die zweite die Zahl 1, die dritte, die Zahl 2 usw. Jetzt haben wir aber die Informationen über die geöffnete Datei. Dazu gehört zumindest die Position, innerhalb der Datei. Und jetztt bietet es sich an, eine Struktur zu definieren. Eine Struktur, die enthält, ATTRIBUTE, POSITION, ERSTER SEKTOR, ... . Aber eine Struktur ist lästig. Man würde ein Array von diesen Strukturen anfertigen. Ein Array mit Strukturen als Elementen. Wir wir aber ein Array haben, können wir genauso gut, viele Arrays mit anderen Namen aber dem gleichen Index verwenden. Der Index ist der File-Handler. Eine Zahl. Dann gibt es ein Array für die ATTRIBUTE, eines für die POSITION usw.
Jetzt stellt sich gleich mal eine Frage. Also, wir verwenden eine Menge von Ganzzahlen für die Filehandler. Zunächst Mal ist eine Datei offen, also ist der nächste Filehandler 1 oder 2. Bei der nächsten Datei 2 oder 3, bei der nächsten 3 oder 4. Also nehmen wir einen Index, der zählt mit, was der höchste Filehandler war. Wenn man jetzt eine Datei schließt, nämlich mit CLOSE, muss man dann den nächsten Filehandler für die nächste von der geschlossenen Datei verwenden? Auf gar keinen Fall. Dann bräuchte man wieder ein Array. Der Filehanlder und der Index wäre gar kein Index als Filehandler, sondern würde etwas in diesem Array anpeilen, was eine Menge Filehandler enthält. Die nächste Datei kriegt den nächste Filehandler. Egal, ob eine Datei inzwischen geschlossen wurde. Die geschlossene Datei bekommt auch Attribute, nämlich, dass sie geschlossen wurde, als so mit diesem Filehandler nicht weiter verwendbar ist. Die Frage ist, können zwei Programme, denselben Filerhandler verwenden, und dabei an verschiedenen Stellen lesen und schreiben? Die Antwort lautet, nein, das geht nicht. Beim selben Filehandler kann nicht an unterschiedlichen Stellen zwei Mal geschrieben oder gelesen werden, denn ein Filehandler ist ein Index, zum Beispiel für ein Array, dass die aktuellen Positionen enthält. Man müsste beim selben Filehandler zur Verfügung stellen, das widerspricht dem Prinzip. Umgekehrt kann dieselbe Datei mit unterschiedlichen Filehandlern zwei Mal geöffnet sein.

So, READ und WRITE sind nun auch fertig

Ich habe jetzt eine neue Version des Quelltext des Quelltextes erstellt. Ich habe es getestet. READ zumindest funktioniert jetzt fabelhaft und bestens. Ich habe in dem Quelltext mit der Funktion test5() getestet, also das READ.


unsigned int calc_end_of_sector(unsigned int adrs);
unsigned get_sectornumber_of_next_sector(unsigned int adrs):
unsigned int set_sector_number(unsigned int actual_sector, unsigned int next_sector);
unsigned get_sectornumber_of_actual_sector(unsigned int adrs);


Diese Funktionen sind sehr nützlich und hilfreich und mach einen ansonsten unlesbaren Quelltext lesbar.
In diesem folgenden Quelltext finden Sie ein funktionierendes READ, vielleicht auch WRITE, das habe ich noch nicht getestet, mit einem funktionierenden Test für READ. Hier noch die vorherigen Quelltexte: Und noch ein Mal zur Sicherheit:

Jetzt endlich: WRITE, OPEN, CLOSE

So, es sieht jetzt so aus, als hätte ich eine funktionierende Version von WRITE geschrieben. Das gleich im nächsten Beitrag, auch auf meiner Website. Ich habe des weiteren OPEN und CLOSE implementiert. Desweiteren TOUCH. TOUCH ist wie MKDIR von einem Unterverzeichnis in einem Pfad, bloß legt es eine Datei an.
RMDIR und RM - dürfen übrigens nicht vergessen werden.