Operating Systems Development Series | |
FileSystems and the VFS
はじめにオペレーティングシステム開発のための終わりのないシリーズの22番目の章へようこそ!これは22番目の章というより、OS開発シリーズの2年目です。 これはファイルシステムに関連したチュートリアルです (心配しないでください、これが最後です ;) 。 最初のものは、ブートコードからメインブートローダープログラムをロードするために必要なものでした。そしてもうひとつは、カーネルがプログラムをロードして実行できるようにするためのものです。しかし、この章と他の2つの章の間には違いがあります。 しかし、この章のスパイスとして、また新しいものを紹介するために、Virtual FileSystems (VFS) にも注目します。 これにより、あらゆるファイルシステムドライバや異なるディスクデバイスと同じようにインターフェースすることができるようになります。これはローカルディスクドライブにも使えますし、ネットワークファイルシステムとのインターフェースにも使えます。 準備はいいですか? ファイルシステム概要ファイルシステムファイルシステムは、情報の読み取りと書き込みの論理的な方法を定義します。このように、ファイルシステムは仕様と考えることができます。PCのファイルシステムは、デスクトップのファイルとフォルダの概念に基づいています。 ファイルシステムには、さまざまな種類があります。FAT12、FAT16、FAT32、NTFS、ext(Linux)、HFS(古いMACで使用)、特定の企業が社内で使用するファイルシステム(GFS - Google File System)、ネットワークだけで使用するファイルシステム(NFS)などがあります。また、独自のファイルシステム実装を開発・設計することも可能です。 ファイルシステムは、データの保存と整理のために使用されます。ファイルシステムは、取り外し可能なメディア(フロッピー、フラッシュドライブ、CD、DVD)、ローカルドライブ(ハードディスクドライブ)、ネットワーククライアント上のファイルやディレクトリにアクセスする簡単な方法を提供します。 ファイルシステムは、インメモリイメージとして存在することも可能です。例えば、特殊なタイプのファイルシステムの「足跡」をその中に含むファイルを読み込むことができます。 ファイルとフォルダーファイルとは、プログラムまたはユーザーに対して何かを示すデータのグループです。このデータは、私たちが望むものであれば何でもあり得ます。例えば、テキストファイルはテキスト情報を含んでいます。フォルダは、ファイルの論理的なグループです。ディレクトリとも呼ばれます。 ディレクトリは、大量のファイルを管理するための手段です。 ディレクトリは通常、ツリー構造を形成します。これは、ディレクトリツリーと呼ばれています。すべてのディレクトリとファイルの親となるディレクトリは1つだけです。ファイルパスとは、ディレクトリツリー内のファイルの位置のことです。例えば、a:↵myfile.txtというファイルは、myfile.txtがファイル名です。a:\mydirmyfile.txtは、デバイス "a: "のルートディレクトリにあるサブディレクトリ mydir にあるファイル myfile.txt のことです。 ファイルおよびフォルダーの命名 フォルダやファイルの名前は、そのファイルやフォルダの内容を表す文字列です。 ファイルシステムは、ファイル名とフォルダ名をそれぞれ異なる方法で実装し、それぞれに制約があります。 たとえば、FAT12では、ディレクトリエントリにファイル名とフォルダ名を11バイトの配列(ファイル名が8、拡張子が3)で格納しています。これは8.3命名規則とも呼ばれる)これにより、ファイル名とフォルダ名は11文字に制限されます。一方、NTFSはLFN(Long File Name)をサポートし255文字に制限されています。NTFSは、別の例として、ファイル名をファイル属性とともにマスターファイルテーブルに格納しています。 ほとんどのファイルシステムのファイル名は、大文字と小文字を区別しません。しかし、いくつかのファイルシステムは、内部的にファイル名を異なる方法で保存することがあります。たとえば、フロッピーディスク上のファイル名を8.3小文字にしていても、OSからそのファイルを読み込むときにはすべて大文字のファイル名を使うことができることをご存じでしょうか。Windowsではファイル名のLFNが表示されますが、FAT12の8.3ファイルエントリには、その8.3オール大文字ファイル名だけが入ります。これが可能になるのです。 ファイルの種類シンボリックリンクのシンボリックリンクは、短いパスを提供する方法です。例えば、a:/folder/link.lnkはa:/otherfolder/subsubfolder/yet another folder/link.txtを指しています。 これで、テキストファイルに簡単にアクセスできるようになりました。シンボリックリンクは、フォルダーを整理するためにも非常によく使われます。Windowsのスタートメニューのようなものです。プログラムへのシンボリックリンクが含まれています。 シンボリックリンクの実装はそれほど難しくはない。与えられたノード(これがリンクになる)を見つけるのです。それはリンクであるように見えるので、あなたは本当のパスを取得し、代わりにそのファイルを読み取る。 Windowsショートカットは、シンボリックリンクの一種です。 パイププロセス間通信(IPC)の一種をパイプと呼びます。パイプは、通常2つ以上のプロセス間の仮想ファイルです。Unix の stdout, stdin, stderror が最も良い例でしょう。 これらは通常のファイルとして扱われますが、stdoutに書き込まれたデータは画面上に表示されます(またはstdout.txtに表示されます)。 特殊なファイルタイプメタファイルファイルシステムの中には、ファイルシステム用に特別なファイルやフォルダを実装しているものがあります。通常、同じディレクトリに同じ名前のファイルやフォルダを2つ置くことはできません(フォルダと同じ名前を共有するファイル名も同様)。このため、これらの隠しファイルを使ってファイルやフォルダーに名前を付けることも、実装によってはできない場合があります。 例えば、NTFSでは、ファイルシステム用にいくつかのメタファイルを用意しています。これらのファイルは、システム・ドライブのルート・ディレクトリ(通常はC:)に配置されます。隠しファイルやシステムファイルの表示にチェックを入れても表示されませんが、そこに上記のような名前のファイルを作成するとどうなるかを見てみましょう。これらのファイルは他の場所でも作成できますが、メタファイルの関係でルート・ディレクトリに作成すると「file already exists」エラーが発生します。 デバイスファイルUnix系システム、DOS(ひいてはWindows)には、デバイスを表す特別な「ファイル」であるデバイスファイルがあります。 例えば、NUL(ヌルデバイス)、CLOCK$、PRN(プリンタ)などがそうです。以下、デバイスファイルの一覧です。
.および..および...は、一部のファイルシステムで実装されている特殊なファイルです。は、カレントディレクトリを参照するファイル情報を含むファイルのファイル名です。...」は、そのファイルの親ディレクトリを参照する情報を含むファイルのファイル名である。例えば、カレントディレクトリがc: ⇄mydirの場合、パス名...はC:を、パス名...はc:⇄mydirを参照することになります。 ファイルシステムの種類フラットファイルシステムフラットファイルシステムとは、サブディレクトリを持たないファイルシステムのことで、すべてのファイルが同じ(ルート)ディレクトリに格納されています。初期のコンピューターシステムの多くは、フラットファイルシステムを使用していました。最近のオペレーティングシステムは、より高度な階層型ファイルシステムを実装しているのが一般的です。フラットファイルシステムは、小型で導入が簡単な反面、初期化が困難です。 階層型ファイルシステムサブディレクトリをサポートするファイルシステムです。最近のファイルシステム(FAT12、FAT16、FAT32、etx、NTFSなど)のほとんどは、このタイプに分類されます。(FAT12の最初のバージョンはフラットファイルシステムでした。ただし、それ以降のバージョンではサブディレクトリをサポートしています)。 ジャーナリングファイルシステムこのタイプのファイルシステムは、ファイルシステムの変更に関する「ジャーナル」を使用します。これは、システムがファイルやディレクトリに対して行おうとしている変更を、手順を完了する前に記録したものです。これにより、ファイルシステムの操作中にクラッシュが発生した場合(ファイルの書き込みなど)、ジャーナルを読んで変更を元に戻し、ファイルシステムを修復できることが保証されます。 ファイルシステムドライバファイルシステムが「ファイル」と「ディレクトリ」の読み書きの仕様を定義するのに対して、ファイルシステムドライバは特定のタイプのファイルシステムの実装を含んでいます。ファイルシステムドライバの例としては、マイクロソフト社のNTFSファイルシステムを実装したntfs.sysがあります。ファイルシステムドライバはまた、より大きなソフトウェアの中のミニドライバとして実装されることもあります。 ブートローダはその良い例です。ブートローダは、別のドライバプログラムなしでディスクからファイルをロードできなければならないので、ブートローダ自体の中に、異なるタイプのファイルシステムのためのいくつかのファイルシステムミニドライバを含んでいます。このシリーズでブートローダを開発した人は、すでに FAT12 ファイルシステムを経験し、私たちのブートローダ用に FAT12 のミニドライバを開発したはずです。 仮想ファイルシステム(VFS)概要仮想ファイルシステム(VFS)は、特定のファイルシステム実装の上にある抽象化レイヤです。 ソフトウェアは、VFSを通してストレージデバイスにアクセスします。これにより、ソフトウェアは、使用されているデバイスやファイルシステムの知識がなくても、異なるストレージデバイスへの読み取りや書き込みを行うことができます。また、インストールされたファイルシステムやデバイスの数だけ、同じコードを動作させることができます。 基本的な考え方は、単一のシステムインターフェイスで、あらゆるファイルシステムを統一的に扱えるようにすることです。Windows、Linux、Mac OSはすべて、異なる方法でVFSをサポートしています。 実装VFSを実装するには、さまざまな方法があります。 マウントポイントリストマウントポイントリストとは、マウントされたファイルシステムと、それらがマウントされている場所のリストです。 例えば、ファイルを読み込む必要がある場合、OSは通常VFS ReadFile()関数を呼び出し、マウントされたファイルシステムのリストを検索して、ファイルがあるデバイスとファイルシステムを探します。そして、読み取り要求をそのファイルシステムのReadFile()関数に渡します。 ノードグラフノード グラフには、ファイル、フォルダー、マウント ポイントなど、さまざまな種類のファイルを表すノードのグラフが含まれます。各ファイルノード構造には、通常、ファイルの読み取りと書き込みを行うファイルシステム固有のルーチンへの関数ポインタが含まれている。 例えば、次のようなFILE構造を作ることができます。typedef struct _FILE { char name[32]; //filename uint32_t flags; //flags uint32_t fileLength; //length of file read_funct read; //function pointers to read,write,open,close file write_funct write; open_funct open; close_funct close; }FILE, *PFILE; 関数ポインタは、このFILE構造体に格納されていることに注意してください。例えば、ファイルを読み込むためにfopen()を呼び出すと、最終的にVFSのOpenFile()関数が呼び出されます。VFSのファイル操作ルーチンが行う必要があるのは、特定のFILEの関数ポインタに制御を移すことだけです。 void VfsOpenFile (PFILE file, const char* filename) { if (file) file->open (filename); } これにより、ファイルシステムで定義されたルーチンを呼び出すことができます。 DOSとWindowsDOSとWindowsでは、マウントされたファイルシステムを表すためにaからzまでの文字が割り当てられます。 Windowsでは、ドライブレターとそのObject Manager名の間にシンボリックリンクが保持されます。 例えば、ドライブレターc: (シンボリックリンク名 \GLOBAL??\C:) はObject名 \DeviceHardDiskVolume1 device objectにマップされることがあります。ファイルシステムはデバイスオブジェクトを所有するために自身を登録することができます。ファイルシステムがそのオブジェクトを所有することが判明した場合、残りのファイルパス名 (この例では "myfile.txt") がそのファイルシステムの FileOpen() 関数に渡されます。 ドライブレターの割り当て Windows では、マウントされたファイルシステムを表すデバイスやパーティションに、ドライブレターを割り当てることができます。(ブート時に、ファイルシステムドライバがデバイスオブジェクトの所有者として登録されていない場合、WindowsはデバイスにRAWミニドライバを使用します)。ドライブレターは、ネットワーク共有ドライブ、仮想ディスクイメージ、ローカルまたはネットワーククライアント内の別の場所へのシンボリックリンクを参照することもできます。ただし、「a」から「z」まで26文字しか使えないため、26個のデバイスに限定される。 インターフェース簡単のために、VFSの実装では、ドライブレターの割り当てとマウントポイントリストを使用することにします。このシリーズで紹介するOSでは、デバイス管理やI/O管理は行っていませんので、シンプルな実装にする必要があります。 個人的には、ファイルシステムドライバの前にVFSを先に開発することをお勧めします。そうすれば、VFSのインターフェースやフレームワークは既に完成しているはずです。 ファイルC言語を使ったことがある人なら、悪名高いFILE*データ型はすでによく知られています。FILE*は、ファイル・オブジェクトへのポインタを表す抽象データ型(ADT)です。ISO Cでは、C言語の実装はFILE型を定義しなければならないと定義していますが、構造体の中身は定義していません。つまり、FILE*はISO Cですが、構造体の中身はインプリメンテーションで定義されたものです。 ファイルの現在の状態を表すファイル構造体は、どのようにでも定義できます。 つまり、ファイルには名前とサイズがあり、これはすでに2つのメンバーです。ファイル終了(EOF)かどうかをフラグする方法と、ファイル固有のフラグが必要で、これがさらに2つのメンバとなる。さらに、ファイルの現在位置(クラスタとクラスタオフセット)を追跡する方法も必要で、次のようなものがある。 typedef struct _FILE { char name[32]; uint32_t flags; uint32_t fileLength; uint32_t id; uint32_t eof; uint32_t position; uint32_t currentCluster; uint32_t device; }FILE, *PFILE; ファイルの種類これまで、ファイル、ディレクトリ、シンボリックリンクなど、さまざまな種類のファイルについて説明してきました。簡単のために、ここではファイルとディレクトリにのみ焦点を当てます。ファイルの種類を表すために、上記のFILE構造体のフラグ・メンバにこれらのフラグを使用します。 #define FS_FILE 0 #define FS_DIRECTORY 1 #define FS_INVALID 2 操作についてファイルに対して実行できる典型的な操作がいくつかあります。
VFSの場合は、fsys.hにあるボリューム・マネージャを通じて公開されます。 extern FILE volOpenFile (const char* fname); extern void volReadFile (PFILE file, unsigned char* Buffer, unsigned int Length); extern void volCloseFile (PFILE file); extern void volRegisterFileSystem (PFILESYSTEM, unsigned int deviceID); extern void volUnregisterFileSystem (PFILESYSTEM); extern void volUnregisterFileSystemByID (unsigned int deviceID); 例えば、C言語のfopen()ルーチンを呼び出すとします。このルーチンは、FILEオブジェクトを返すvolOpenFile()ルーチンを呼び出します。ファイルへのパスは、"a: \myfile.txt" のように渡します。ボリューム・マネージャはマウント・ポイント・リストを検索し、ファイル・システムが「a」を表すデバイスIDに登録されているかどうかを確認します。もし登録されていれば、ファイルシステムドライバのFileOpen()メソッドを呼び出し、「myfile.txt」を渡します。 複雑に聞こえるかもしれませんが、心配しないでください。でも、このデモの実装はとても簡単です。 ボリュームマネージャの実装ファイルシステムの抽象化最初に必要なのは、ファイルシステム固有の情報を抽象化する方法です。 これには、ファイルシステムの名前やファイルに対して実行可能な操作が含まれます。これは関数ポインターを用いて行われる. typedef struct _FILE_SYSTEM { char Name [8]; FILE (*Directory) (const char* DirectoryName); void (*Mount) (); void (*Read) (PFILE file, unsigned char* Buffer, unsigned int Length); void (*Close) (PFILE); FILE (*Open) (const char* FileName); }FILESYSTEM, *PFILESYSTEM; 実装ボリューム・マネージャは、デモのVFSをインプリメントします。fsys.hとfsys.cppに含まれています。デバイスは26個あるので、DEVICE_MAXという定数を作ると便利です。各デバイスはマウント可能なファイルシステムを1つだけ持つことができるので、(マウントポイントリストのように)リストに格納します。 #define DEVICE_MAX 26 //! File system list PFILESYSTEM _FileSystems[DEVICE_MAX]; 以下は、その仕組みです。ファイルシステムはポインタのリストとして格納しているので、ポインタが有効であれば、そこにファイルシステムが登録されていることになります。配列の各要素は、それが参照するドライブ文字を表します。つまり、'a' は _FileSystems[0] に、'b' は _FileSystems[1] に、といった具合です。ファイルシステムが書き込むディスクを管理するのはファイルシステムの責任です。 このメソッドを使用すると、非常に基本的ですが、デバイスに簡単にアクセスすることができます。例えば、volOpenFile() はパスの最初の文字(ドライブ文字)をチェックし、そのデバイスにファイルシステムが登録されているかどうか、リストを検索するだけでよいのです。もし登録されていれば、そのファイルシステムの open() メソッドを呼び出し、ファイル名をドライバに渡します。デフォルトでは 'a' を使用しますが、入力パスに ':' が含まれている場合は、代わりにデバイスの最初の文字を使用します。 これにより、volOpenFile は"myfile.txt"と"a:myfile.txt" という2通りの方法で呼び出すことができます(ここで "a" はファイルが存在するデバイスを表します)。かっこいいでしょう? FILE volOpenFile (const char* fname) { if (fname) { //! default to device 'a' unsigned char device = 'a'; //! filename char* filename = (char*) fname; //! in all cases, if fname[1]==':' then the first character must be device letter if (fname[1]==':') { device = fname[0]; filename += 2; //strip it from pathname } //! call filesystem if (_FileSystems [device - 'a']) { //! set volume specific information and return file FILE file = _FileSystems[device - 'a']->Open (filename); file.deviceID = device; return file; } } FILE file; file.flags = FS_INVALID; return file; } 他のファイル操作ルーチンは基本的にすべて同じです。 VFS がどのようにファイルシステムを保存しているかを知れば、volRegisterFileSystem() ルーチン・ファミリーがどのように動作するか想像がつくはずです。基本的には、ファイルシステムへのポインタをリストに格納したり、クリアしたりするだけです。 void volRegisterFileSystem (PFILESYSTEM fsys, unsigned int deviceID) { if (deviceID < DEVICE_MAX) if (fsys) _FileSystems[ deviceID ] = fsys; } さて、それではファイルシステム・ドライバを初期化し、VolRegisterFileSystem()を呼び出して自分自身を登録します。 fopen()を呼び出し、VolOpenFile()を呼び出して、ファイルシステムのopen()メソッドを呼び出しました。これで準備は整いましたが、何かが足りません...とても重要な何かが...ファイルシステム・ドライバそのものが! そうですね、もう一回やってみましょうか......。 FAT12 - テイク3はじめにこのシリーズでは、過去に2回、FAT12を見て実装しました。そのため、今回もFAT12について詳しく説明する予定はありません。しかし、今回はFAT12をCドライバのコードとその動作とともに復習することにします。 必要であれば、第11章を参照しながら読んでください。 ブートセクター重要なファイルシステムの情報の多くは、ブートストラッププログラムとともにブートセクタに格納されていることを思い出してください。具体的には、ブートセクターにあるBios Paramater Block (PBP)に格納されているのです。 ファイルシステムをマウントするときに、後で使用するために BPB からこの情報を読み出して保存する必要があります。これを行うには、ブートセクターに一致する構造体を作成します。 typedef struct _BOOT_SECTOR { uint8_t Ignore[3]; //first 3 bytes are ignored (our jmp instruction) BIOSPARAMATERBLOCK Bpb; //BPB structure BIOSPARAMATERBLOCKEXT BpbExt; //extended BPB info uint8_t Filler[448]; //needed to make struct 512 bytes }BOOTSECTOR, *PBOOTSECTOR; ブートセクタがどのように見えるかの良い例は、Stage1 ブートローダプログラムがメモリ上でどのように見えるかを考えてみることです。Stage1 の一番最初の命令(4 章のデモ、Stage1.asm を参照)はjmp loader です。これは3バイト命令なので、上の構造の最初の3バイトは、jmp 命令のオペレーションコード(OPCode)です。 また、第4章でOEMパラメータブロック(別名、Biosパラメータブロック(BPB))を取り上げましたので、覚えておいてください。このため、BIOSPARAMATERBLOCKは、この構造体の次にあります。また、FAT32などの他のファイルシステム用にBPBを拡張したBIOSPARAMATERBLOCKEXT構造体も提供しています。 ブートセクタの最後の448バイトは、ブートセクタのプログラムコードの残りを含んでいます。今すぐには重要ではないので、Fillerメンバのパディングとして扱います。これにより、BOOTSECTOR構造体がディスク上のブートセクター(512バイト)と正確に同じサイズになることが保証されます。 BIOSPARAMATERBLOCKは、BPBのフォーマットを定義する構造体です。これはブートセクターと同じ構造体で、第5章で詳しく説明しました。 typedef struct _BIOS_PARAMATER_BLOCK { uint8_t OEMName[8]; uint16_t BytesPerSector; uint8_t SectorsPerCluster; uint16_t ReservedSectors; uint8_t NumberOfFats; uint16_t NumDirEntries; uint16_t NumSectors; uint8_t Media; uint16_t SectorsPerFat; uint16_t SectorsPerTrack; uint16_t HeadsPerCyl; uint32_t HiddenSectors; uint32_t LongSectors; }BIOSPARAMATERBLOCK, *PBIOSPARAMATERBLOCK; 上記の構造体は見慣れたものでしょう :)そうでなければ、第5章の説明を読んでください。 しかし、BIOSPARAMATERBLOCKEXTは新しいかもしれません。BPBについてはすでに詳しく説明し、過去にFAT12の解析に使用しましたが、FAT12のブートセクタはBPB拡張メンバに依存しないのです。しかし、FAT32はそうです。 typedef struct _BIOS_PARAMATER_BLOCK_EXT { uint32_t SectorsPerFat32; //sectors per FAT uint16_t Flags; //flags uint16_t Version; //version uint32_t RootCluster; //starting root directory uint16_t InfoCluster; uint16_t BackupBoot; //location of bootsector copy uint16_t Reserved[6]; }BIOSPARAMATERBLOCKEXT, *PBIOSPARAMATERBLOCKEXT; これで全部です :)すべて以前の章で詳しく説明されています。これらの構造体は、ファイルシステムドライバにBPBのデータを参照する簡単な方法を提供し、後でファイルシステムを使用できるようにします。必要なのはブートセクタを読み込むことと、PBOOTSECTOR を通してデータにアクセスすることです。 前の章で開発したフロッピーディスクドライバを使って、セクタを読み取ります。 //! Boot sector info PBOOTSECTOR bootsector; //! read boot sector bootsector = (PBOOTSECTOR) flpydsk_read_sector (0); 必要なのはこれだけです :)重要な情報はすべてbootsector.bpb にあります。あとは、ファイルシステムをマウントするだけです... ファイルシステムのマウントさて、BPB の情報がメモリに入ったので、ファイルシステムを使う準備をする必要があり ます。まず、必要な情報を決定することから始めます。 さて、ディスク上の総セクタ数が必要です。また、ディレクトリエントリの総数も必要です。その他に、ファイルアロケーションテーブル(FAT)とルートディレクトリを使用する際に役立つ情報があります。 typedef struct _MOUNT_INFO { uint32_t numSectors; uint32_t fatOffset; uint32_t numRootEntries; uint32_t rootOffset; uint32_t rootSize; uint32_t fatSize; uint32_t fatEntrySize; }MOUNT_INFO, *PMOUNT_INFO; さて...ブートセクタはすでに BOOTSECTOR 構造体に格納されていますね?これを知っていれば、BPB から MOUNT_INFO 構造体に情報をコピーすることができます。 さてと...FAT12でフォーマットされたディスクの最初のFATとルートディレクトリの位置を確認しましょう。
FATが2つあることに注意してください。最初のFATは、ディスクのブートセクタの直後にあります。このため、MOUNT_INFOのfatOffsetを1に設定しています。また、Root Directoryは両方のFATの直後にあることに注意してください。これを知っていれば、ルート・ディレクトリの開始セクタを見つけるための簡単な計算を思いつくことができます。(NumberOfFATs * sectorsPerFAT) + 1. ブートセクタのために 1 を加える必要があります。 これで、最初のFATとルート・ディレクトリの位置がわかりました。 ルート・ディレクトリのサイズを求めるために必要なのは、ルート・ディレクトリのエントリ数と各エントリのサイズです。 FAT12の各ディレクトリ・エントリは、サイズが32バイトの特定の構造形式になっています。つまり、bootsector->Bpb.NumDirEntries * 32で、ディレクトリが占めるバイト数ということになります。これをセクタあたりのバイト数で割ってセクタ数に変換しています。 //! store mount info _MountInfo.numSectors = bootsector->Bpb.NumSectors; _MountInfo.fatOffset = 1; _MountInfo.fatSize = bootsector->Bpb.SectorsPerFat; _MountInfo.fatEntrySize = 8; _MountInfo.numRootEntries = bootsector->Bpb.NumDirEntries; _MountInfo.rootOffset = (bootsector->Bpb.NumberOfFats * bootsector->Bpb.SectorsPerFat) + 1; _MountInfo.rootSize = ( bootsector->Bpb.NumDirEntries * 32 ) / bootsector->Bpb.BytesPerSector; 以上で完了です。これでFAT12ドライバは初期化できました。簡単でしょう?MOUNT_INFO に重要なファイルシステム情報があるので、あとはディレクトリを解析してファイルを読み込むだけです :) ディレクトリのパースフォーマットFAT12のディレクトリは32バイトの構造体で構成され、ファイルやサブディレクトリの情報を提供します。 各ディレクトリのエントリは次のようなフォーマットになっています。 typedef struct _DIRECTORY { uint8_t Filename[8]; //filename uint8_t Ext[3]; //extension (8.3 filename format) uint8_t Attrib; //file attributes uint8_t Reserved; uint8_t TimeCreatedMs; //creation time uint16_t TimeCreated; uint16_t DateCreated; //creation date uint16_t DateLastAccessed; uint16_t FirstClusterHiBytes; uint16_t LastModTime; //last modification date/time uint16_t LastModDate; uint16_t FirstCluster; //first cluster of file data uint32_t FileSize; //size in bytes }DIRECTORY, *PDIRECTORY; これだけです :)これはディレクトリエントリです。DIRECTORY構造に格納される情報は、サブディレクトリでもファイルでもかまいません。Filenameと Extには、ファイルやディレクトリの8.3形式の名前が含まれています。 Attribは、ファイルまたはディレクトリの属性を含んでいます。参考までに以下の値を持つ。
なお、本シリーズでは不要なので使用しませんが、お好きな方はご自身のシステムでファイル属性の作業や設定のサポートを提供することができます。 この構造体におけるすべての日付メンバは、特定のビットフォーマットに従う。
ファイルやディレクトリの日付や時刻を変更したり取得したりする必要がないため、このシリーズでは使用していません。しかし、読者の皆様には、お好きなように機能を追加していただければと思います。 FAT12でフォーマットされたフロッピーディスクでは、クラスタは1セクタ(512バイト)と同じ大きさであることを思い出してください。このため、DIRECTORYのFirstClusterフィールドは、ファイルの最初のセクタも指しています。したがって、このセクタを読むことで、ファイルの最初の512バイトを効果的に読み取ることができます。 それでは、ディレクトリを解析して、ファイルを探してみましょう。 パースディレクトリはディレクトリエントリ構造のリストを含んでいることを思い出してください。 これを知っていれば、ディレクトリを解析してファイルやディレクトリを見つけるのは非常に簡単になります。 まず、ルートディレクトリを読み込むことから始めます。ファイルシステムをマウントしたときにBPBからルート・ディレクトリ・セクタを取得し、_MountInfo.rootOffsetに格納したことを思い出してください。したがって、必要なことはセクタをロードし、DIRECTORY*を使用してディレクトリ・エントリにアクセスすることだけです。 次に、ループしてファイル名を比較し、一致するものを探します。ToDosFileName()を使用して、入力ファイル名をDOS 8.3ファイル名フォーマットに変換します。例えば、入力ファイル名「Myfile.txt」をFAT12内部フォーマット「MYFILE TXT」に変換する。 セクタを読み込んで、セクタ内の各エントリを比較します。また、ファイル名をC文字列に変換し、単純なstrcmp()呼び出しでファイル名が一致するかどうかをテストできるようにしていることにお気づきでしょう。一致するものが見つかったら、FILE構造体を埋めてそれを返します。 それでは、見てみましょう。 FILE fsysFatDirectory (const char* DirectoryName) { FILE file; unsigned char* buf; PDIRECTORY directory; //! get 8.3 directory name char DosFileName[11]; ToDosFileName (DirectoryName, DosFileName, 11); DosFileName[11]=0; DirectoryNameには、探したいディレクトリ名やファイル名が入ります。 myfile.txt」のような入力ファイル名をDOS 8.3ファイルシステムのフォーマット「MYFILE TXT」に変換してDosFileNameに格納します。 for (int sector=0; sector<14; sector++) { //! read in sector buf = (unsigned char*) flpydsk_read_sector ( _MountInfo.rootOffset + sector ); //! get directory info directory = (PDIRECTORY) buf; ルートディレクトリから読み込んでいます。ルートクラスタは_MountInfoに格納されており、ファイルシステムをマウントしたときにBios Paramater Block(BPB)から取得した情報が格納されています。_MountInfo.rootOffsetにはルートディレクトリの最初のクラスタが格納されています。ルートディレクトリには、最大で224のDIRECTORYエントリーが含まれます。1つのDIRECTORYエントリーは32バイトで、224*32=7168バイト、7168バイト/512バイト(1クラスタは512バイト)=14となります。つまり、ルートディレクトリは14のクラスタから構成されていることになります。 これを知っていれば、ディレクトリ全体を一度に読み込むのではなく、セクタごとに読み込んで、ディレクトリの各部分を解析することができます。 //! 16 entries per sector for (int i=0; i<16; i++) { //! get current filename char name[11]; memcpy (name, directory->Filename, 11); name[11]=0; //! find a match? if (strcmp (DosFileName, name) == 0) { DIRECTORYのエントリが32バイトであることを知ると、1クラスタ512バイト÷32バイト=16となります。つまり、1つのセクタに16のDIRECTORYエントリがあることになります。そこで、各エントリーをループしてファイル名を比較し、探しているファイルまたはディレクトリを見つけます。file .currentClusterには、後で読み込むファイルの最初のクラスタが格納され、file.fileLengthには、ファイルのサイズがバイトで格納されます。DIRECTORYのエントリ属性に基づいて設定します。 //! found it, set up file info strcpy (file.name, DirectoryName); file.id = 0; file.currentCluster = directory->FirstCluster; file.eof = 0; file.fileLength = directory->FileSize; //! set file type if (directory->Attrib == 0x10) file.flags = FS_DIRECTORY; else file.flags = FS_FILE; //! return file return file; } あと少し...ファイルやディレクトリがまだ見つかっていない場合は、次のDIRECTORYエントリに移動するだけです。もしファイルが見つからなかったら、FS_INVALIDをセットして戻ります。 //! go to next directory directory++; } } //! unable to find file file.flags = FS_INVALID; return file; } 以上です。上記のルーチンはFAT12内のディレクトリとファイルに対して動作します。このルーチンを呼び出すと、ルートディレクトリから任意のフォルダやファイル名を検索し、その情報を返します。 サブディレクトリFAT12 の古いバージョンはフラットでしたが、このファイルシステムの新しいバージョンはサブディレクトリをサポートしています。これにより、ディレクトリを使用することができ、多くのファイルをより簡単に管理することができるようになりました。例えば、大規模なOSではOS固有のファイルをシステムディレクトリで分けたり、ユーザープロファイルを含むユーザーディレクトリで分けたりするとよいでしょう。 サブディレクトリは、DIRECTORYフラグが設定されているだけの普通のファイルです。このため、まずファイルの読み方について知っておく必要があります。 ファイルの読み方フォーマットさて、ディレクトリを解析してファイルを探すことができるようになりました。次に、ファイルの内容を読み取る方法が必要です。技術的には、ファイルのディレクトリエントリ構造のFirstClusterフィールドだけで、どのファイルでも最初の512バイトを読むことができることを思い出してください。それ以上のクラスタを読み込むには、File Allocation Table (FAT)を解析する必要があります。 FATはクラスタ番号を含むいくつかのエントリから構成されていることを思い出してください。 これらのエントリのサイズはファイルシステムに依存します。FAT12は1エントリあたり12ビット、FAT16は16ビット、FAT32は32ビットです。 FATはリンクリストではなく、物理ディスク全体を表すエントリーの表だと考えてください。ディスクの第1クラスタは、FATの最初のエントリで表されます。2番目のクラスターは2番目のエントリで表され、以下同様です。つまり、クラスタとFATのエントリは1対1の関係になっています。このため、FAT12でのファイルの読み書きが容易になります。 ファイルの読み込みファイルを読み込むには、そのファイルの現在のクラスタを読み込みます。 FATテーブルを解析して、ディスク上の次のクラスタを見つけようとします。次のクラスタが見つかったら、次に読み込むファイルのために「現在のクラスタ」を更新します。 読み込むクラスタは、ファイルがオープンされたときに設定されました。このルーチンの最初の呼び出しでは、file->currentClusterはDIRECTORY->FirstCluster と同じになります。 このクラスタは、ディスク上のデータ領域へのオフセットです。FAT12でフォーマットされたディスクのフォーマットを思い出して、FATとデータ領域の位置を確認しましょう。
各FATは9セクタを使用することを忘れないでください。FATは2つあるので、9+9=18です。また、前節でルートディレクトリは14セクタであると結論づけました。18+14=32.これは、両方のFATとルート・ディレクトリが占有するセクタの量です。ここまでの式は32 + file->currentCluster ですが、ここから1を引くと、32 + (file->currentCluster - 1)となります。これが読み込むべきセクタで、ファイル・データが格納されています。 void fsysFatRead(PFILE file, unsigned char* Buffer, unsigned int Length) { if (file) { //! starting physical sector unsigned int physSector = 32 + (file->currentCluster - 1); //! read in sector unsigned char* sector = (unsigned char*) flpydsk_read_sector ( physSector ); //! copy block of memory memcpy (Buffer, sector, 512); 次のクラスタを読み込むには、FATテーブルをパースする必要があります。FAT テーブルは 9 セクタなので、9 セクタすべてを読むのではなく、どのセクタを読む必要があるのかを判断します。 まず、次のクラスターがどこにあるか、バイト・オフセットを取得します。これを行うには、クラスタの値にクラスタのサイズを掛けます。FAT32のクラスタのサイズは4バイトなので、FAT32を使用している場合は4倍します。FAT16を使用している場合は、クラスタエントリごとに2バイトを使用するため、2倍します。もちろん、これで問題ありませんが、FAT12ではどうでしょうか。FAT12では1クラスタ・エントリあたり12ビットを使用します。1バイト目が8ビット、2バイト目が4ビットで、4ビットは8ビットの半分なので、0.5ビットとなります。 あとはこのバイトオフセットをセクタサイズで割って、読み込むFATのセクタを求めればよい。リマンダーはこのセクタ内のオフセットで、これがFATから読み込むクラスタとなります。 これがentryOffsetになります。 FATは、uint8_t FAT [SECTOR_SIZE*2]と定義されています。FATのセクタを1つではなく、2つメモリに読み込んでいることに注目してください。なぜそうするのでしょうか。セクタのサイズが512バイトだとすると、512バイト*8=4096ビット/セクタです。4096ビット÷12ビット(FATエントリ)で、341.3333...となります。つまり、エントリーは第1セクタと第2セクタの間に位置することになります。これでは、ファイルを読み込むときに問題が発生する。このため、1番目のセクタの最後のクラスタ値が破損しないように、追加のセクタをロードする必要があります。 unsigned int FAT_Offset = file->currentCluster + (file->currentCluster / 2); //multiply by 1.5 unsigned int FAT_Sector = 1 + (FAT_Offset / SECTOR_SIZE); unsigned int entryOffset = FAT_Offset % SECTOR_SIZE; //! read 1st FAT sector sector = (unsigned char*) flpydsk_read_sector ( FAT_Sector ); memcpy (FAT, sector, 512); //! read 2nd FAT sector sector = (unsigned char*) flpydsk_read_sector ( FAT_Sector + 1 ); memcpy (FAT + SECTOR_SIZE, sector, 512); FATセクタが読み込まれた後、クラスタ番号を読み込んでいます。 ここで問題にぶつかります。8ビットの値を読み込むと、クラスタ値の12ビット全部を読み込むことができないのです。そこで、uint16_tを使って16ビットを読み込むことにします。もちろん、12ビットの値のビット数が多すぎるという問題があります。 もう少し詳しく見てみましょう。これがFATだとします。FATをバイトに分割し、12ビットのエントリーをマークします。 (これは第6章から引用しています) Note: Binary numbers seperated in bytes. Each 12 bit FAT cluster entry is displayed. | | 01011101 0111010 01110101 00111101 0011101 0111010 0011110 0011110 | | | | | | | |1st cluster | |3rd cluster-| | |-0 cluster ----| |2nd cluster---| |4th cluster----| すべての偶数クラスタが最初のバイトのすべてをコピーし、2番目のバイトの一部をコピーしていることに注意してください。また、すべての奇数クラスターは最初のバイトの一部をコピーしますが、2番目のバイトのすべてをコピーすることに注意してください このことから、クラスタが偶数の場合は、次のクラスタに属するので、上位4ビットをマスクします。クラスタが奇数の場合は、4ビット下にシフトします(最初のクラスタで使用したビットを破棄するため)。 さて、これでこの関数を終了させましょう。 //! read entry for next cluster uint16_t nextCluster = *( uint16_t*) &FAT [entryOffset]; //! test if entry is odd or even if( file->currentCluster & 0x0001 ) nextCluster >>= 4; //grab high 12 bits else nextCluster &= 0x0FFF; //grab low 12 bits //! test for end of file if ( nextCluster >= 0xff8) { file->eof = 1; return; } //! test for file corruption if ( nextCluster == 0 ) { file->eof = 1; return; } //! set next cluster file->currentCluster = nextCluster; } } ファイルの書き込み[章更新で完結予定!】。]サブディレクトリサブディレクトリは、DIRECTORY 属性が設定されたファイルです。サブディレクトリから読み込むには、そのディレクトリ名を持つディスク上のFAT12ファイルを探し出し、FATを使用する他のファイルと同じ方法で読み込めばよいのです。 ファイルが読み込まれた後、最初のバイトから最後のバイトまでは、単なるDIRECTORYエントリの配列です。このディレクトリを読み込むには、ルートディレクトリと同じようにDIRECTORYエントリーを解析します :-)これらは、ディレクトリ内のファイルやフォルダになります。 それでは、見てみましょう。 FILE fsysFatOpenSubDir (FILE kFile, const char* filename) { FILE file; //! get 8.3 directory name char DosFileName[11]; ToDosFileName (filename, DosFileName, 11); DosFileName[11]=0; filenameには、探したいファイルやディレクトリが含まれています。入力ファイル名「myfile.txt」をDOS 8.3ファイルシステム形式「MYFILE TXT」に変換し、DosFileNameに格納します。 //! read directory while (! kFile.eof ) { //! read directory unsigned char buf[512]; fsysFatRead (&file, buf, 512); //! set directort PDIRECTORY pkDir = (PDIRECTORY) buf; fileは解析したいサブディレクトリです。FAT12では普通のファイルなので、ファイルのセクタを読み込むことを覚えておいてください。ファイルはDIRECTORYエントリの配列で構成されています。DIRECTORYのメンバーにアクセスしやすくするために、pkDirを使用してセクタの内容をポイントしています。さて、このディレクトリを検索してみましょう... //! 16 entries in buffer for (unsigned int i = 0; i < 16; i++) { //! get current filename char name[11]; memcpy (name, pkDir->Filename, 11); name[11]=0; //! match? if (strcmp (name, DosFileName) == 0) { DIRECTORYの各エントリは32バイトです。1つのセクタ(FAT12ではクラスタ)は512バイトなので、512バイト÷32バイト=16のDIRECTORYエントリが1つのセクタに存在します。そこで、16個のエントリすべてをループして名前を比較します。検索しているファイル名と一致するものが見つかれば、そのファイルを見つけたことになります。 //! found it, set up file info strcpy (file.name, filename); file.id = 0; file.currentCluster = pkDir->FirstCluster; file.fileLength = pkDir->FileSize; file.eof = 0; //! set file type if (pkDir->Attrib == 0x10) file.flags = FS_DIRECTORY; else file.flags = FS_FILE; //! return file return file; } ファイルが見つかったら、FILE構造体に最初のファイルクラスタ(後で読めるように)、ファイルサイズ(EOFのタイミングが分かるように)、属性(ファイルかディレクトリか)を記入します。 ファイルが見つからなかった場合は、次のエントリに移動します。このループは、ファイルの終端まで続けられます。ファイルが見つからなかった場合は、FS_INVALIDをセットして、リターンする。 //! go to next entry pkDir++; } } //! unable to find file file.flags = FS_INVALID; return file; } このルーチンとFsysFatDirectoryルーチンが類似していることに注意してください。 まとめこれは楽しい章でしたね。これでディスクからファイルを読み込むことができるようになりました。そうですね、「そろそろ時間だ!」という感じでしょうか。マルチタスクとプログラム実行に大きく飛躍する準備はほぼ整いました。しかし、マルチタスクに入る前に、ローダーについて説明する必要があります。ローダーはプログラムをロードして実行し、アドレス空間にマッピングする役割を果たします。また、アドレス空間におけるヒープ管理、スタック管理もカバーする必要があります。 メモリ管理の章を大幅に更新する予定なので、ヒープ管理とスタック管理はメモリ管理の章の次の章に移すかもしれません。いずれにせよ、変更点については随時お知らせしていくつもりです。 とはいえ、これでそろそろマルチタスクに飛び込む時期が来たということです。その次は?ユーザーモード |