12. Feb 2024
Lesedauer 8 Min.
FAT12-Dateisystem
Das eigene Betriebssystem, Teil 15
Ein vollständiger FAT12-Treiber wird implementiert.

[Link auf :]Die zurückliegende Folge dieser Serie [1] behandelte den Aufbau der Grundstrukturen einer Command-Shell. Die Command-Shell bietet nicht nur eigene Befehle an, sondern ist auch in der Lage, Anwendungsprogramme von der Festplatte zu laden und auszuführen.Diese letzte Folge der Serie zeigt nun, wie man ein Dateisystem implementiert. Dafür wird ein FAT12-Treiber entwickelt, der sowohl lesend als auch schreibend auf das FAT12-Dateisystem des Selbstbau-Betriebssystems zugreifen kann. Tabelle 1 gibt einen Überblick über die Funktionen, die dafür entwickelt und eingebaut werden.
Tabelle 1: Dateisystem-Funktionen
|
Das FAT12 Root Directory
Wie bereits aus den vorangegangenen Folgen dieser Artikelserie bekannt ist, nutzt das Selbstbau-Betriebssystem eine FAT12-Partition, die einerseits einen Boot-Sektor enthält und andererseits auch alle notwendigen Betriebssystem-Komponenten, wie beispielsweise den Kernel.Die Abkürzung FAT steht für File Allocation Table. Diese Datenstruktur beschreibt, aus welchen Teilen (Clustern) eine Datei oder ein Ordner im Dateisystem besteht. Ein Cluster hat hier eine Größe von 512 Bytes und ist damit genauso groß wie ein physischer Sektor auf der Festplatte. In unserer File Allocation Table werden zwölf Bits verwendet, um diese Cluster zu adressieren – davon leitet sich der Name FAT12 ab.Eine Datei kann maximal 4096 Cluster (212) zu jeweils 512 Bytes umfassen und hat somit eine maximale Größe von 2 MByte (4096 × 512 Bytes). Das FAT12-Dateisystem besteht aus den folgenden logischen Abschnitten (siehe auch Bild 1):
Die vier logischen Abschnitte des FAT12-Dateisystems (Bild 1)
Autor
- Boot-Sektor,
- FAT Tables,
- Root Directory,
- Data Area.
Listing 1: Die Struktur RootDirectoryEntry
struct RootDirectoryEntry
{
unsigned char FileName[8];
unsigned char Extension[3];
unsigned char Attributes[1];
unsigned char Reserved[2];
unsigned CreationSecond: 5;
unsigned CreationMinute: 6;
unsigned CreationHour: 5;
unsigned CreationDay: 5;
unsigned CreationMonth: 4;
unsigned CreationYear: 7;
unsigned LastAccessDay: 5;
unsigned LastAccessMonth: 4;
unsigned LastAccessYear: 7;
unsigned char Ignore[2];
unsigned LastWriteSecond: 5;
unsigned LastWriteMinute: 6;
unsigned LastWriteHour: 5;
unsigned LastWriteDay: 5;
unsigned LastWriteMonth: 4;
unsigned LastWriteYear: 7;
unsigned short FirstCluster;
unsigned int FileSize;
} __attribute__ ((packed));
typedef struct RootDirectoryEntry
RootDirectoryEntry;
Um das Root Directory ausgeben zu können, wird es beim Initialisieren des FAT12-Treibers von der Festplatte mithilfe der Funktion LoadRootDirectory() geladen, siehe Listing 2. Gespeichert wird der Inhalt des Root Directorys an der in der Konstanten ROOT_DIRECTORY_BUFFER definierten Hauptspeicheradresse. Zugleich wird auch die komplette FAT Table in den Hauptspeicher geladen, die Zieladresse dafür ist in der Konstanten FAT_BUFFER hinterlegt.
Listing 2: Das Root Directory laden
static void LoadRootDirectory()
{
// Calculate the Root Directory Size:
// 14 sectors: => 32 * 224 / 512
short rootDirectorySectors =
32 * ROOT_DIRECTORY_ENTRIES /
BYTES_PER_SECTOR;
// Calculate the LBA address of the Root
// Directory: 19: => 2 * 9 + 1
short lbaAddressRootDirectory =
FAT_COUNT * SECTORS_PER_FAT +
RESERVED_SECTORS;
// Load the whole Root Directory (14 sectors)
// into memory
ROOT_DIRECTORY_BUFFER =
malloc(rootDirectorySectors *
BYTES_PER_SECTOR);
ReadSectors(
(unsigned char *)
ROOT_DIRECTORY_BUFFER,
lbaAddressRootDirectory,
rootDirectorySectors);
// Load the whole FAT (18 sectors)
// into memory
FAT_BUFFER = malloc(FAT_COUNT *
SECTORS_PER_FAT * BYTES_PER_SECTOR);
ReadSectors((unsigned char *)FAT_BUFFER,
FAT1_CLUSTER, FAT_COUNT *
SECTORS_PER_FAT);
}
Ist das Root Directory geladen, kann man über die 224 Einträge iterieren und sie auf dem Bildschirm ausgeben. Das erledigt die Funktion PrintRootDirectory().Wie Sie in Listing 3 erkennen können, wird ein Root-Directory-Eintrag nur dann ausgegeben, wenn er einen Dateinamen aufweist. Fehlt der Name, ignoriert die Funktion Print-RootDirectory() den Eintrag und setzt die Arbeit mit dem nächsten Eintrag fort.
Listing 3: Das Root Directory anzeigen
void PrintRootDirectory()
{
char str[32] = "";
int fileCount = 0;
int fileSize = 0;
int i;
RootDirectoryEntry *entry = (RootDirectoryEntry *)
ROOT_DIRECTORY_BUFFER;
for (i = 0; i < ROOT_DIRECTORY_ENTRIES; i++)
{
if (entry->FileName[0] != 0x00)
{
// Print out the file size
itoa(entry->FileSize, 10, str);
printf(str);
printf(" bytes");
printf("\t");
// Extract the name and the extension
char name[9] = "";
char extension[4] = "";
substring(entry->FileName, 0, 8, name);
substring(entry->FileName, 8, 3,
extension);
// Convert everything to lower case
tolower(name);
tolower(extension);
// Print out the file name
int pos = find(name, ' ');
char trimmedName[9] = "";
substring(name, 0, pos, trimmedName);
printf(trimmedName);
printf(".");
printf(extension);
printf("\n");
// Calculate the file count and the file size
fileCount++;
fileSize += entry->FileSize;
}
// Move to the next Root Directory Entry
entry = entry + 1;
}
// Print out the file count and the file size
printf("\t\t");
itoa(fileCount, 10, str);
printf(str);
printf(" File(s)");
printf("\t");
itoa(fileSize, 10, str);
printf(str);
printf(" bytes");
printf("\n");
}
Neue Dateien erzeugen
Nachdem man sich den Inhalt des Dateisystems anzeigen lassen kann, geht es nun darum, eine neue Datei mithilfe der Funktion CreateFile() zu erzeugen. Listing 4 zeigt die Implementierung dieser Funktion.Listing 4: CreateFile()
static void CreateFile(
unsigned char *FileName,
unsigned char *Extension)
{
// Find the next free RootDirectoryEntry
RootDirectoryEntry *freeEntry =
FindNextFreeRootDirectoryEntry();
if (freeEntry != 0x0)
{
// Getting a reference to the
// BIOS Information Block
BiosInformationBlock *bib =
(BiosInformationBlock *)BIB_OFFSET;
// Allocate the first cluster for the new file
unsigned short startCluster =
FindNextFreeFATEntry();
FATWrite(startCluster, 0xFFF);
strcpy(freeEntry->FileName, FileName);
strcpy(freeEntry->Extension, Extension);
freeEntry->FirstCluster = startCluster;
freeEntry->FileSize = 0;
// Set the Date/Time information of the
// new file
freeEntry->LastWriteYear =
freeEntry->LastAccessYear =
freeEntry->CreationYear =
bib->Year - FAT12_YEAROFFSET;
freeEntry->LastWriteMonth =
freeEntry->LastAccessMonth =
freeEntry->CreationMonth =
bib->Month;
freeEntry->LastWriteDay =
freeEntry->LastAccessDay =
freeEntry->CreationDay = bib->Day;
freeEntry->LastWriteHour =
freeEntry->CreationHour = bib->Hour;
freeEntry->LastWriteMinute =
freeEntry->CreationMinute = bib->Minute;
freeEntry->LastWriteSecond =
freeEntry->CreationSecond =
bib->Second / 2;
// Write the changed Root Directory and the
// FAT tables back to disk
WriteRootDirectoryAndFAT();
// Allocate a new cluster of 512 bytes in
// memory, and copy the initial content
// into it.
// Therefore, we can make sure that the
// remaining bytes are all zeroed out.
unsigned char *content =
(unsigned char *)
malloc(BYTES_PER_SECTOR);
memset(content, 0x00,
BYTES_PER_SECTOR);
// Write the intial file content to disk
WriteSectors((unsigned int *)content,
startCluster + DATA_AREA_BEGINNING, 1);
// Release the block of memory
free(content);
}
}
Um eine neue Datei im FAT12-Dateisystem anzulegen, gilt es zunächst einen Eintrag im Root Directory zu finden, der noch nicht verwendet wird. Dies erledigt die Funktion FindNextFreeRootDirectoryEntry(). Ist ein freier Eintrag ermittelt, muss die Funktion FindNextFreeFATEntry() noch einen freien Start-Cluster in der FAT Table finden. Listing 5 zeigt die Implementierung beider Funktionen.
Listing 5: Hilfsfunktionen für das Erstellen einer neuen Datei
static RootDirectoryEntry
*FindNextFreeRootDirectoryEntry()
{
RootDirectoryEntry *entry =
(RootDirectoryEntry *)
ROOT_DIRECTORY_BUFFER;
for (int i = 0; i < ROOT_DIRECTORY_ENTRIES;
i++)
{
if (entry->FileName[0] == 0x00)
return entry;
// Move to the next Root Directory Entry
entry = entry + 1;
}
// A free Root Directory Entry was not found
return 0x0;
}
static unsigned short FindNextFreeFATEntry()
{
unsigned short Cluster = 1;
unsigned short result = 1;
while (result > 0)
{
Cluster++;
// Calculate the offset into the FAT table
unsigned int fatOffset =
(Cluster / 2) + Cluster;
unsigned long *offset =
FAT_BUFFER + fatOffset;
// Read the entry from the FAT
unsigned short val = *offset;
if (Cluster & 0x0001)
{
// Odd Cluster
result = val >> 4; // Highest 12 Bits
}
else
{
// Even Cluster
result = val & 0x0FFF; // Lowest 12 Bits
}
}
return Cluster;
}
Anschließend werden die Datums- und Uhrzeitinformationen der neuen Datei gesetzt und das Root Directory sowie die FAT Tables wieder zurück auf die Festplatte geschrieben. Zum Abschluss wird auch noch der mit Null-Werten initialisierte erste Cluster der neuen Datei auf die Festplatte geschrieben.
Dateien öffnen und schließen
Bei der Funktion CreateFile(), die Sie oben bereits kennengelernt haben, handelt es sich um eine statische C-Funktion, das heißt, dass sie sich außerhalb des kompilierten Moduls nicht aufrufen lässt, da ihr Gültigkeitsbereich beschränkt ist.Das Erstellen einer neuen Datei erfolgt mit der Funktion OpenFile(), nachdem die Datei im Write- oder Append-Modus geöffnet wurde. Im Dateisystem ist sie zu diesem Zeitpunkt noch nicht vorhanden. Der FAT12-Treiber implementiert aktuell die folgenden Datei-Modi:- r (read): Die Datei wird für einen Lesevorgang geöffnet.
- w (write): Die Datei wird für einen Schreibvorgang geöffnet. Ist die Datei noch nicht vorhanden, wird sie erzeugt, ansonsten wird sie überschrieben.
- a (append): Die Datei zum Anhängen von Daten öffnen.
struct FileDescriptor
{
unsigned char FileName[11];
unsigned char Extension[3];
unsigned long FileSize;
unsigned long
CurrentFileOffset;
char FileMode[2];
};
typedef struct FileDescriptor
FileDescriptor;
In der Variablen CurrentFileOffset wird hier das aktuelle File Offset innerhalb der geöffneten Datei gespeichert. Dieser Wert ist sehr wichtig, wenn eine Datei später gelesen oder geschrieben werden soll. Die Funktion SeekFile() dient dazu den aktuellen File Offset zu ändern.Um nun eine geöffnete Datei in eine Kernel-Liste eintragen zu können, ist noch ein eindeutiger Schlüssel erforderlich. Dieser wird mit der Funktion HashFileName() erzeugt, die auf Basis des Dateinamens (und der angehängten Prozess-ID) einen eindeutigen Hashwert erzeugt. Die Hashfunktion basiert auf einem Algorithmus, der unter [2] näher beschrieben ist. Die Implementierung sieht wie folgt aus:
static unsigned long HashFileName(
unsigned char *FileName)
{
int hash = 0;
int length = strlen(FileName);
// To store 'P'.
int p = 1;
// For taking modulo.
int m = 1000000007;
for (int i = 0; i < length; i++)
{
hash += (FileName[i] - 'a') * p;
hash = hash % m;
p *= 41;
}
return hash;
}
In Listing 6 sehen Sie die Funktion OpenFile(), die auch die Logik hinsichtlich der unterschiedlichen Datei-Modi implementiert. OpenFile() liefert den erzeugten Hash-Wert des hinzugefügten File Descriptors zurück. Dieser Hash-Wert ist das sogenannte File Handle, das die anderen I/O-Funktionen als Input-Parameter benötigen.
Listing 6: Die Funktion OpenFile()
unsigned long OpenFile(unsigned char *FileName,
unsigned char *Extension, char *FileMode)
{
char pid[10] = "";
// Construct the full file name
char fullFileName[15];
strcpy(fullFileName, FileName);
strcat(fullFileName, Extension);
// Find the Root Directory Entry for the
// given program name
RootDirectoryEntry *entry =
FindRootDirectoryEntry(fullFileName);
// Check, if the requested file was found in the
// FAT12 file system partition
if ((entry == 0x0) &&
(strcmp(FileMode, "w") == 0))
{
// If the requested file was not found in the
// "write" mode, we just create a new empty
// file
CreateFile(FileName, Extension);
}
else if ((entry == 0x0) &&
(strcmp(FileMode, "r") == 0))
{
// If the requested file was not found in the
// "write" mode, we return a NULL value for
// the file handle
return 0;
}
else if ((entry == 0x0) &&
(strcmp(FileMode, "a") == 0))
{
// If the requested file was not found in the
// "append" mode, we just create a new
// empty file
CreateFile(FileName, Extension);
}
else if ((entry != 0x0) &&
(strcmp(FileMode, "w") == 0))
{
// If the requested file exists in the "write"
// mode, its content must be truncated.
// Therefore, we delete and recreate the file
DeleteFile(FileName, Extension);
CreateFile(FileName, Extension);
}
// The PID of the current running task is
// concatenated to the file name
// to make it unique across multiple running
// tasks.
// Otherwise we would have a hash collision if
// the same file is opened across
// multiple running tasks.
tolower(fullFileName);
ltoa(GetTaskState()->PID, 10, pid);
strcat(fullFileName, pid);
// Calculate a hash value for the given
// file name
unsigned long hashValue =
HashFileName(fullFileName);
// Create a new FileDescriptor and store it in
// a system-wide Kernel list
FileDescriptor *descriptor = (FileDescriptor *)
malloc(sizeof(FileDescriptor));
strcpy(descriptor->FileName, FileName);
strcpy(descriptor->Extension, Extension);
descriptor->FileSize = entry->FileSize;
descriptor->CurrentFileOffset = 0;
strcpy((char *)&descriptor->FileMode,
FileMode);
AddEntryToList(FileDescriptorList,
descriptor, hashValue);
// If the requested file exists in the "append"
// mode, we set the FileOffset to the end of
// the file
if (strcmp(FileMode, "a") == 0)
descriptor->CurrentFileOffset =
descriptor->FileSize;
// Return the key of the newly added
// FileDescriptor
return hashValue;
}
Eine dieser Funktionen ist CloseFile(), die eine geöffnete Datei wieder schließt. Dazu wird einfach der entsprechende File Descriptor aus der Kernel-Liste entfernt:
int CloseFile(unsigned long FileHandle)
{
// Find the file which needs to be closed
ListEntry *entry =
GetEntryFromList(
FileDescriptorList, FileHandle);
if (entry != 0x0)
{
// Close the file by removing it from the list
RemoveEntryFromList(
FileDescriptorList, entry);
return 0;
}
else
return -1;
}
Dateien schreiben
Legt man eine neue Datei mit OpenFile() an, so hat diese zunächst einmal noch keinen Inhalt. Dieser wird erst durch die Funktion WriteFile() auf die Festplatte geschrieben. Wie Sie bereits aus der ersten Folge dieser Serie [3] wissen, besteht eine Datei im FAT12-Dateisystem aus einer einfach verketteten Liste von Clustern zu 512 Bytes, die in der FAT Table abgelegt sind. Der Eintrag der Datei im Root Directory zeigt auf den ersten Cluster der Datei (Bild 2).
Aufbau des FAT12-Dateisystems (Bild 2)
Autor
Die Funktion WriteFile() erwartet im ersten Parameter das File Handle der bereits geöffneten Datei, im zweiten Parameter werden die Nutzdaten übergeben und im dritten Parameter die Länge der Nutzdaten.Im ersten Schritt muss nun basierend auf dem aktuellen File Offset der Cluster, in den die Nutzdaten geschrieben werden sollen, von der Festplatte in den Hauptspeicher geladen werden. Wie Sie in Listing 7 erkennen können, muss die Funktion WriteFile() auch dafür sorgen, dass die Datei im FAT12-Dateisystem passend vergrößert wird, wenn sich das aktuelle File Offset außerhalb der aktuellen Dateigröße befindet. Das Vergrößern der Datei erfolgt dynamisch über die Funktion AllocateNewClusterToFile().
Listing 7: Laden des richtigen Clusters
// Allocate a file buffer
unsigned char *file_buffer =
(unsigned char *)malloc(BYTES_PER_SECTOR);
// Calculate from the current file position the
// cluster and the offset within that cluster
unsigned long cluster =
descriptor->CurrentFileOffset /
BYTES_PER_SECTOR;
unsigned long offsetWithinCluster =
descriptor->CurrentFileOffset –
(cluster * BYTES_PER_SECTOR);
unsigned short currentFatSector =
entry->FirstCluster;
// Loop until we reach the cluster that we want
// to write to. If necessary, new clusters will be
// created and added for the file.
for (int i = 0; i < cluster; i++)
{
// Read the next Cluster from the FAT table
unsigned short nextFatSector =
FATRead(currentFatSector);
// The next cluster is the last one in the chain
if (nextFatSector >= EOF)
{
// Allocate a new cluster for the file
unsigned short newFatSector =
AllocateNewClusterToFile(
currentFatSector);
// Set the current sector
currentFatSector = newFatSector;
}
else
{
// Set the current sector
currentFatSector = nextFatSector;
}
}
// When the data is stored across the last
// boundary of the current sector, we have
// allocate an additional cluster to the file
if ((offsetWithinCluster + Length >=
BYTES_PER_SECTOR) && (descriptor->FileSize
< descriptor->CurrentFileOffset + Length))
{
// Allocate a new cluster for the file
AllocateNewClusterToFile(currentFatSector);
}
// Calculate the following disk sector
unsigned short fatSectorFollowing =
FATRead(currentFatSector);
// Read the specific sector from disk
ReadSectors((unsigned char *)file_buffer,
currentFatSector + DATA_AREA_BEGINNING, 1);
// Read the following logical sector, when the data is stored across 2 disk sectors
if (offsetWithinCluster + Length >=
BYTES_PER_SECTOR)
{
ReadSectors((unsigned char *)(file_buffer +
BYTES_PER_SECTOR),
fatSectorFollowing +
DATA_AREA_BEGINNING, 1);
}
Wie Sie in Listing 8 erkennen können, wird dafür der nächste freie FAT-Cluster über die Funktion FindNextFreeFATEntry() ermittelt. Über FATWrite() wird dieser neue FAT-Cluster dann in den aktuell letzten FAT-Cluster eingetragen und die Datei somit erweitert. In den letzten FAT-Cluster wird schlussendlich noch der Wert 0xFFF eingetragen, der das Ende der Datei (EOF) signalisiert.
Listing 8: Vergrößern einer Datei
static unsigned short AllocateNewClusterToFile(
unsigned short CurrentFATSector)
{
// Allocate a new cluster for the file
unsigned short newFatSector =
FindNextFreeFATEntry();
FATWrite(CurrentFATSector, newFatSector);
FATWrite(newFatSector, 0xFFF);
// Zero-initialize the new cluster and write it
// to disk
unsigned char *emptyContent =
(unsigned char *)
malloc(BYTES_PER_SECTOR);
memset(emptyContent, 0x00,
BYTES_PER_SECTOR);
WriteSectors((unsigned int *)emptyContent,
newFatSector + DATA_AREA_BEGINNING, 1);
// Release the block of memory
free(emptyContent);
// Return the new allocated FAT sector
return newFatSector;
In Listing 9 sehen Sie die Implementierung der Funktion FATWrite(), die auf den ersten Blick ein wenig kompliziert erscheint, da sich ein 12-Bit langer FAT-Eintrag über zwei Bytes erstreckt und somit ein wenig Bit-Arithmetik betrieben werden muss.
Listing 9: Einen FAT-Eintrag schreiben
static void FATWrite(
unsigned short Cluster,
unsigned short Value)
{
// Calculate the offset into the FAT table
unsigned int fatOffset = (Cluster / 2) + Cluster;
if (Cluster % 2 == 0)
{
// Even Cluster
FAT_BUFFER[fatOffset + 0] = (0xFF & Value);
FAT_BUFFER[fatOffset + 1] =
((0xF0 & (FAT_BUFFER[fatOffset + 1])) |
(0x0F & (Value >> 8)));
}
else
{
// Odd Cluster
FAT_BUFFER[fatOffset + 0] =
((0x0F & (FAT_BUFFER[fatOffset + 0])) |
((0x0F & Value) << 4));
FAT_BUFFER[fatOffset + 1] =
((0xFF) & (Value >> 4));
}
}
Nachdem WriteFile() den gesuchten Cluster von der Festplatte in den Hauptspeicher geladen hat, kopiert die Funktion memcpy() die übergebenen Nutzdaten an die entsprechende Stelle im Hauptspeicher. Schlussendlich schreibt die Funktion WriteSectors() die Nutzdaten wieder auf die Festplatte zurück.Außerdem werden noch das aktuelle Zugriffsdatum der Datei und die Dateigröße im Root Directory passend geändert. Listing 10 zeigt den für diesen Zweck innerhalb der Funktion WriteFile() zusätzlich erforderlichen Code.
Listing 10: Dateiänderungen durchführen
// Copy the requested data into the destination
// disk sector
memcpy(file_buffer + offsetWithinCluster,
Buffer, Length);
// Write the specific sector to disk
WriteSectors((unsigned int *)file_buffer,
currentFatSector + DATA_AREA_BEGINNING, 1);
// Write the following logical sector, when the
// data is stored across 2 disk sectors
if (offsetWithinCluster + Length >=
BYTES_PER_SECTOR)
{
WriteSectors((unsigned int *)
(file_buffer + BYTES_PER_SECTOR),
fatSectorFollowing +
DATA_AREA_BEGINNING, 1);
}
// Release the file buffer
free(file_buffer);
// Set the last Access and Write Date
SetLastAccessDate(entry);
// Set the current file position within
// the FileDescriptor
descriptor->CurrentFileOffset += Length;
// Check if the file size has changed
if (descriptor->CurrentFileOffset >
entry->FileSize)
{
// Change the data in the RootDirectory
entry->FileSize =
descriptor->CurrentFileOffset;
descriptor->FileSize =
descriptor->CurrentFileOffset;
}
// Write the RootDirectory and the FAT
// tables back to disk
WriteRootDirectoryAndFAT();
Dateien lesen
Was noch fehlt, ist eine Funktion ReadFile(), mit der man Daten aus einer Datei lesen kann. Auch diese Funktion erwartet als Parameter das File Handle der geöffneten Datei, den reservierten Hauptspeicherbereich und die gewünschte Länge der zu lesenden Daten.ReadFile() liest die Daten immer basierend auf dem aktuellen File Offset, das im File Descriptor hinterlegt ist. Wie Sie in Listing 11 erkennen können, wird am Schluss das aktuelle File Offset im File Descriptor entsprechend den gelesenen Bytes erhöht, wodurch der nächste Aufruf von ReadFile() die Daten am aktuellen File Offset innerhalb der Datei zurückliefert. Dadurch lässt sich die Datei durch mehrmalige Aufrufe von ReadFile() komplett auslesen. Dass das Ende der Datei erreicht ist, signalisiert die Funktion EndOfFile(), die 1 zurückliefert, sobald keine Nutzdaten mehr vorhanden sind:Listing 11: Lesen einer Datei
// Allocate a file buffer
unsigned char *file_buffer =
(unsigned char *)malloc(BYTES_PER_SECTOR);
// Calculate from the current file position the
// cluster and the offset within that cluster
unsigned long cluster =
descriptor->CurrentFileOffset / BYTES_PER_SECTOR;
unsigned long offsetWithinCluster =
descriptor->CurrentFileOffset -
(cluster * BYTES_PER_SECTOR);
unsigned short fatSector = entry->FirstCluster;
// Loop until we reach the cluster that
// we want to read
for (int i = 0; i < cluster; i++)
{
// Read the next Cluster from the FAT table
fatSector = FATRead(fatSector);
}
// Calculate the following disk sector
unsigned short fatSectorFollowing =
FATRead(fatSector);
// Check for the EndOfFile condition
if (descriptor->CurrentFileOffset + Length >
descriptor->FileSize)
{
Length = descriptor->
FileSize – descriptor->CurrentFileOffset;
}
// Read the specific sector from disk
ReadSectors((unsigned char *)file_buffer,
fatSector + DATA_AREA_BEGINNING, 1);
// We also read the following sector, when the
// requested data is stored across 2 disk sectors
if (offsetWithinCluster + Length > BYTES_PER_SECTOR)
{
ReadSectors((unsigned char *)file_buffer +
BYTES_PER_SECTOR,
fatSectorFollowing +
DATA_AREA_BEGINNING, 1);
}
// Copy the requested data into the
// destination buffer
memcpy(Buffer, file_buffer +
offsetWithinCluster, Length);
// Set the current file position within
// the FileDescriptor
descriptor->CurrentFileOffset += Length;
// Release the file buffer
int EndOfFile(unsigned long FileHandle)
{
// Find the file from which we want to check
// the EndOfFile condition
ListEntry *entry =
GetEntryFromList(FileDescriptorList,
FileHandle);
FileDescriptor *descriptor =
(FileDescriptor *)entry->Payload;
if (descriptor->CurrentFileOffset ==
descriptor->FileSize)
return 1;
else
return 0;
}
Auf Basis der vorgestellten Funktionen können Sie nun bereits sehr mächtige Funktionalitäten in unserem Selbstbau-Betriebssystem implementieren, wie zum Beispiel das Kopieren einer Datei. Den dafür erforderlichen Code finden Sie in Listing 12.
Listing 12: Kopieren einer Datei
unsigned char buffer[512] = "";
// Open both files
unsigned long fileHandleSource =
OpenFile("BIGFILE ", "TXT", "r");
unsigned long fileHandleTarget =
OpenFile("TARGET ", "TXT", "w");
// Check if the source file was opened
if (fileHandleSource == 0)
printf("The source file could not be
opened.\n");
// Check if the target file was opened
if (fileHandleSource == 0)
printf("The target file could not be opened or
created.\n");
if ((fileHandleSource != 0) &&
(fileHandleTarget != 0))
{
// Copy the source file to the target file
while (!EndOfFile(fileHandleSource))
{
ReadFile(fileHandleSource,
(unsigned char *)&buffer, 512);
WriteFile(fileHandleTarget,
(unsigned char *)&buffer, 512);
}
// Close both file handles
CloseFile(fileHandleSource);
CloseFile(fileHandleTarget);
printf("File copied.\n");
}
Die bereits erwähnte Funktion SeekFile() erlaubt es, den aktuellen File Offset der geöffneten Datei zu verändern, um an eine gewünschte Position innerhalb der Datei zu springen:
int SeekFile(unsigned long FileHandle,
unsigned long NewFileOffset)
{
// Find the file from which we want to read
ListEntry *entry =
GetEntryFromList(FileDescriptorList,
FileHandle);
FileDescriptor *descriptor =
(FileDescriptor *)entry->Payload;
if (descriptor != 0x0)
{
descriptor->CurrentFileOffset =
NewFileOffset;
return 0;
}
else
return -1;
}
Mithilfe von SeekFile() sind Sie in der Lage, jeden beliebigen Teil einer Datei auszulesen und weiterzuverarbeiten. Damit verfügen Sie nun über einen vollständigen FAT-Treiber, mit dessen Hilfe Sie in der Lage sind, mit dem FAT12-Dateisystem zu interagieren, um persistente Daten zu verwalten.
Fazit
Im Rahmen dieses Artikels wurde ein FAT12-Treiber entwickelt, der in der Lage ist Dateien im FAT12-Dateisystem zu lesen und zu schreiben. Damit ist die inzwischen auf 15 Teile angewachsene Artikelserie zur Betriebssystem-Entwicklung an ihrem Ende angelangt. Das Selbstbau-Betriebssystem hat inzwischen ein Stadium erreicht, in dem man damit bereits etliche sinnvolle Aufgaben realisieren kann. Wenn auch nicht in dieser Serie, geht der Ausbau des Betriebssystems dennoch weiter. Wer möchte kann den Fortgang im GitHub-Repository des Autors [4] verfolgen.Fussnoten
- Klaus Aschenbrenner, Programme ausführen, Das eigene Betriebssystem, Teil 14, dotnetpro 2/2024, Seite 91 ff.,
- Ujjawal Gupta, String Hashing,
- Klaus Aschenbrenner, Kaos64: Die ersten Schritte, dotnetpro 1/2023, Seite 76 ff.,
- GitHub-Repository des Autors, https://github.com/sqlpassion/osdev