Operating Systems Development Series
FileSystems and the VFS
by Mike, 2010

はじめに

オペレーティングシステム開発のための終わりのないシリーズの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(プリンタ)などがそうです。以下、デバイスファイルの一覧です。

  • CON
  • PRN
  • AUX
  • CLOCK$
  • NUL
  • COM0,COM1,...COM9
  • LPT0, LPT1, ...LPT9
これらの名前はDOSやWindowsで特別な意味を持つため、ファイルやフォルダーに上記のような名前を付けることはできません。

.および.

.および...は、一部のファイルシステムで実装されている特殊なファイルです。は、カレントディレクトリを参照するファイル情報を含むファイルのファイル名です。...」は、そのファイルの親ディレクトリを参照する情報を含むファイルのファイル名である。例えば、カレントディレクトリが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とWindows

DOSと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;
簡単だったでしょう?idは、お望みであれば、識別の目的で使用できます。deviceは、ファイルが存在するデバイスを表します。

ファイルの種類

これまで、ファイル、ディレクトリ、シンボリックリンクなど、さまざまな種類のファイルについて説明してきました。簡単のために、ここではファイルとディレクトリにのみ焦点を当てます。ファイルの種類を表すために、上記のFILE構造体のフラグ・メンバにこれらのフラグを使用します。

#define FS_FILE       0
#define FS_DIRECTORY  1
#define FS_INVALID    2

操作について

ファイルに対して実行できる典型的な操作がいくつかあります。

  • 開く
  • 閉じる
  • 読む
  • 書き込む
  • マウント
  • アンマウント
OpenとCloseはファイルオブジェクト(ファイルやディレクトリ、ファイルタイプは問わない)を開いたり閉じたりする操作で、ReadとWriteはファイルタイプの読み込みと書き込みを行う操作です。これらはすべて、Cの標準ファイルI/O関数を通じてプログラマに公開されています。

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とルートディレクトリの位置を確認しましょう。

Boot Sector Extra Reserved Sectors File Allocation Table 1 File Allocation Table 2 Root Directory (FAT12/FAT16 Only) Data Region containng files and directories.

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は、ファイルまたはディレクトリの属性を含んでいます。参考までに以下の値を持つ。

  • Read only: 1
  • Hidden: 2
  • System: 4
  • Volume Lable: 8
  • Subdirectory: 0x10
  • Archive: 0x20
  • Device: 0x60

なお、本シリーズでは不要なので使用しませんが、お好きな方はご自身のシステムでファイル属性の作業や設定のサポートを提供することができます。

この構造体におけるすべての日付メンバは、特定のビットフォーマットに従う。

  • Bits 0-4: Day (0-31)
  • Bits 5-8: Month (0-12)
  • Bits 9-15: Year
この構造体のすべてのタイムメンバーは、特定のビットフォーマットに従います:

  • Bits 0-4: Second
  • Bits 5-10: Minute
  • Bits 11-15: Hour

ファイルやディレクトリの日付や時刻を変更したり取得したりする必要がないため、このシリーズでは使用していません。しかし、読者の皆様には、お好きなように機能を追加していただければと思います。

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->currentClusterDIRECTORY->FirstCluster と同じになります。

このクラスタは、ディスク上のデータ領域へのオフセットです。FAT12でフォーマットされたディスクのフォーマットを思い出して、FATとデータ領域の位置を確認しましょう。

Boot Sector Extra Reserved Sectors File Allocation Table 1 File Allocation Table 2 Root Directory (FAT12/FAT16 Only) Data Region containng files and directories.

各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ルーチンが類似していることに注意してください。

まとめ

これは楽しい章でしたね。これでディスクからファイルを読み込むことができるようになりました。そうですね、「そろそろ時間だ!」という感じでしょうか。マルチタスクとプログラム実行に大きく飛躍する準備はほぼ整いました。しかし、マルチタスクに入る前に、ローダーについて説明する必要があります。ローダーはプログラムをロードして実行し、アドレス空間にマッピングする役割を果たします。また、アドレス空間におけるヒープ管理、スタック管理もカバーする必要があります。

メモリ管理の章を大幅に更新する予定なので、ヒープ管理とスタック管理はメモリ管理の章の次の章に移すかもしれません。いずれにせよ、変更点については随時お知らせしていくつもりです。

とはいえ、これでそろそろマルチタスクに飛び込む時期が来たということです。その次は?ユーザーモード