yunomuのブログ

酒とゲームと上から目線

ディスク拡張したのにディスク容量が増えないんだけど(ファイルシステムの話)

 例えばAmazon EC2のEBSのように、ディスクが仮想化されるようになり、そうするとディスクの容量を拡張するというよくわかんない事がお手軽にできるようになり、「ディスク容量増やしたんだけどOSから見ると増えてない。なんで」みたいな事が起こり、「ディスク容量を増やしてもファイルシステムの容量は増えないんだよ」というのを説明するのも面倒くさい。
 良い時代になりました。

 まだ二次記憶装置はハードディスクが多いだろうし、単に私が知ってるというのと、私の習慣でディスクって言葉を使います。が、たぶんCDもDVDもSSDもだいたい同じことです。

 ディスクにはメモリと同じようにデータ格納位置にアドレスが振ってあって、メモリは1バイトごとにアドレスが振られていますが、ディスクの場合は512バイトごとに振らてています。なぜ512バイトなのかは知りません。この512バイトをセクタとかディスクブロックと呼びます。

 ディスクは大抵アクセスは遅いけど大容量で不揮発なので、消えると困るデータはだいたいディスクに保存する事になるんですが、どうやって保存するのか。
 どうやってもなにも単にディスク上のアドレスを指定して書き込めばいいんです。

 例えばディスクに書き込むdk_write(addr, data)みたいな関数があったとしたら

    dk_write(0, "Yahho-");

みたいにやればディスクの0番地にデータを書き込んで、めでたくあなたのコンピュータは起動しなくなります。まあ起動しなくなるかどうかは構成しだいではありますし、ここでは全然関係ないんですけど、単に0番地にはだいたいOSの起動に必要な情報が書かれている事が多いので。ブートセクタとかMBRってやつ。
 dk_writeの中では、out命令とかでハードディスクに対してアドレスとデータが格納されたメモリ上の位置を送信しているだけです。暇だったらATAデバイスドライバのコードとか見てみるとわかるかもしれませんが、DMAとか書き込みスケジューリングとか色々あってわかりづらいかもしれません。私は面倒なのでATAの仕様を調べました。

 それで、この関数があれば一応データは保存できるようになりますが、0番地にはこのデータ、1番地にはこのデータ、みたいに覚えておくのは大変だし、512バイトを超えるデータはどうするんだという話になります。まあ、512バイトを超える場合は連続した領域に書き込めばいいんですが、それにしてもじゃあどこからどこまでが一つのデータなのかを覚えないといけなくなるし、外部断片化が起きるようになります。

 なんにしてもディスク上のデータを管理するなんらかの機構が必要です。
 そのなんらかの機構というやつがファイルシステムです。

 つまり、ファイルシステムというのは、ディスク上のデータの『どこからどこまでがどのデータか』という情報を管理しておく機構で、そのデータの管理単位のことをファイルといいます。
 そして、ここまででOSの話がほとんど出てきていないことからわかるように、ファイルシステムというものはディスク上のデータ管理の話であって、本来OSとはあまり関係ありません。厳密にはというか、狭義では。
 そんなわけで、LinuxでもNTFS用のドライバがあればWindowsのファイルを読むことはできますし、逆にWindowsでもext4とかのドライバがあればLinuxのファイルを読むことができます。

 ファイルシステムがやっている管理というのは、本でいうところの目次を作っているようなものです。本にはページ1つ1つに番号がついていて、目次には章節のタイトルとページ番号の対応が書いてある。基本的にはそれと同じです。この目次に書いてあるタイトルが、ファイルシステムではファイル名になります。

 具体的に、UFS/FFSやext2のようなやつの話をします。以降はFFSの事だと思ってください。FAT系やNTFSは全然違うみたいですし。
 で、FFSでは、ファイルシステムではディスクをデータ用の領域と管理用の領域とに分けています。

 データ用の領域には、データが格納されています。そのまんまですが、データ、つまりファイルの中身です。中身は中身だ。それ以上の情報は無い。
 あ、一応『ここでデータは終わりです』みたいな情報は入っています。
 ファイルシステム上のデータは、ディスク上の複数のセクタをまとめたブロックで管理されています。なぜまとめるかというと、ディスクは物理的な特性上、連続したセクタを読むのは速いけども飛び飛びの領域を読むのは非常に遅いので、できるだけデータはまとめておきたい。512バイトでは小さすぎる。それで4KBなり8KBなりのサイズでまとめて管理することで、読み書きを高速化しようとしています。
 ただあまり大きいと内部断片化か起きるので、適当に小さい必要もあって、その速さと空間効率のバランスを取った結果が8KBくらいだったらしいです。
 ちなみに2004年頃に発表された資料によると、初期のGoogle File Systemのブロックサイズは4MBだったらしいです。当時はデカいと思ったけど、今はそうでもないような気もするけども、アクセス速度を考えるとやっぱりデカい気もする。どうせストライピングしてるだろうから問題ないのかなぁ。おそらく今も変わらないか、小さくなるか、逆にめちゃくちゃデカくなってるか、いやよくわかんない。
 4KBやその倍数や4MBなのはメモリ管理の都合じゃないかな。メモリ管理的に、4KBより小さいのや4MBより大きいのは、現状ではあまり意味が無い気がします。

 管理用の領域には、ファイルシステムの大きさや種類などの基本的な情報や、inodeが格納されています。
 inodeというのは、目次の1行みたいなものです。ファイルのサイズ、中身のデータの格納場所(ブロック番号のリスト)、ファイルの種類(通常ファイル、ディレクトリ、シンボリックリンクなど)、パーミッションなどが記録されたテーブルです。基本的に1エントリが1ファイルを表しています。ただここにはファイル名は入っていません。
 inodeのエントリは固定長なので、番号でアクセスすることができます。この時の番号をinode番号と呼びます。inode番号は"ls -i"などのコマンドで見ることができます。
 ちなみにinodeはIndex-Nodeの略だという人もいますが、実のところは何の略だかよくわかってないらしいです。どうでもいいですけども。

 で、ディレクトリというのがあります。
 ディレクトリというのは、ファイルの一種です。
 ディレクトリ・ファイルは、簡単に言うとファイル名とinode番号が格納されたテーブルです。ファイル名はディレクトリに保存してあります。
 つまり、"/bin/ls"というファイルを読もうとした時には、以下の様な手順を踏みます。
(1) "/"のディレクトリを指すinodeを読む("/"のinode番号だけはファイルシステムの管理領域に書いてある)
(2) (1)で読んだinodeから"/"ディレクトリの内容が格納されたブロックを特定し、読む
(3) (2)の内容から、"bin"のエントリを見て、"bin"のinode番号を特定する
(4) "bin"のinodeを読む
(5) "bin"の内容が格納されたブロックを読む
(6) (5)から"ls"のエントリを見て、"ls"のinode番号を特定する
(7) "ls"のinodeを読む
(8) "ls"の内容が格納されたブロックを読む(終わりだよ〜)

 まあ結構面倒くさいわけです。DNSとだいたい同じです。
 ただ、これでファイル名があればファイルの中身にたどり着くことができるようになりました。
 おおむね、ファイルシステムはこんな感じの仕組みになっています。

 このような仕組みになっているので、以下の様な現象が起きます。

 lnコマンドで作れるハードリンクというものがあります。これはファイルの実体は同じものなんだけど、パスや名前が違うファイルを作ることができる機能です。これは違うファイル名に対応するinode番号を同じものにすることで実現することができます。
 ハードリンクはどっちがリンク先リンク元という区別はなくなり、どちらもまったく同じ立場のファイルになります。
 ただし、inode番号というのは定義上ファイルシステムに固有のものなので、違うファイルシステム上のファイルにハードリンクを張ることはできません。

 mvコマンドによるファイルの移動は、同じファイルシステム内であればinode番号は変わらずに、移動元と移動先のディレクトリの情報を書き換えるだけです。ディスク上のデータの格納位置も変わりません。

 ファイルの削除というのは、ディレクトリ内のファイル名とinode番号のエントリを削除したり、inodeからブロック群への参照を削除したりする処理です。『ファイルを削除してもデータは残る』と言われる所以はこれです。それと、ファイル削除のことをunlinkと言ったりするのもこれが理由です。というか、ファイルシステムのインタフェース的にも削除じゃなくてunlinkです。複数のハードリンクがあると消えませんし。

 ところでパーティション(スライス)というのはディスクを複数に分割する機能で、それぞれに異なるファイルシステムを構築することができます。
 なのでパーティションが違うと違うファイルシステムとみなされて、ハードリンクを張ることはできません。
 また、mvコマンドでディスクやパーティションをまたいでファイルを移動させようとすると、移動ではなくてコピーになります。いや最近はコピーした後に移動元を削除してくれますけども。

 ちなみにシンボリックリンクというのはリンク先のファイル名が書かれたファイルなので、リンク先が他のパーティションだろうが他のホストだろうが一時的にしか存在してなかろうがファイル名でアクセスできる限りなんでもアクセスできますが、ファイル名が変わったりするとアクセスできなくなります。

 で、えー、なんだ。話を戻すと、
 ファイルシステムは管理領域とデータ領域に分かれていて、管理領域の中のinodeは固定長で、番号でアクセスできるようになっている。データ領域も8KBだかのブロックに分かれていて、これも番号が振られている。
 この構造自体がディスクに書き込まれている情報なので、一度作ってしまうと、例えばデータ領域に一切変更を加えずに管理領域の容量を増やしたり減らしたりするのは難しい。管理領域のサイズやデータ領域の構造に少しでも変更を加えると全データが使えなくなる可能性がありますし、普通は管理に必要な領域以外は全部データ領域にするので、変更の余地が無いわけです。
 この、ファイルシステムの構造をディスクに書き込む処理の事を『フォーマット』といいます。ファイルシステムの構造を作るのでフォーマットといいます。フォーマットでファイルシステムの内容が全部消えるのはこのせいです。

 そして、inodeは中盤でちょっと触れたように、1エントリが1ファイルに対応しています。そして、ファイルシステム作成時に管理領域の大きさも決めてしまわないといけないので、inodeのエントリ数もファイルシステム作成時に決めてしまわないといけなくなります。
 つまり、ファイルシステム作成時に、そのファイルシステムで管理できるファイル(ディレクトリ等含む)の最大数を決める必要があります。inode数を多くするとファイル数を増やせますが、管理領域が増えるのでデータ領域が減ります。小さいファイルを多数作るならそれもアリでしょう。

 また、データ領域のブロックサイズも作成時に決める必要があります。
 これもまた、大きいと読み書きは速くなりやすくなりますが、小さいファイルが多いと内部断片化しやすくなります。小さいと効率は良くなりますが、大きいファイルを扱う時に外部断片化が起きやすくなって、アクセスは遅くなりやすくなります。デフラグすりゃいいんですが、大変ですし。
 デフラグというのは、この場合は飛び飛びになったデータを並び替えてできるだけ一箇所にまとめることです。一箇所にまとめた方がディスク上の効率が良い。SSDだと関係ないんですけど。

 そんなわけで、実はファイルシステムを作る時というのは結構色々な事を考えないといけないのです。どれくらいの大きさのファイルをどれくらいの数作るか。
 最近はディスクが安いからかなんなのか、そこら辺はあまり神経質に考える必要がなくなっています。が、mkfsコマンドのオプションを見るとまあ一応色々細かく指定することができるようにはなっています。
 昔はOSをインストールするといったらこんな風にまず用途を考えてファイルシステムのパラメータを色々と調整するところから始めないといけなかったそうです。時代は変わって、特に何も考えなくてもOSをインストールできるようになりました。素敵ですね。

 そう、で、だから、ディスク容量を増やしてもinodeの数も増えないしデータ領域のブロック数も増えないからファイルシステム上の容量は増えないわけです。
 ただ、これらの管理情報を後から拡張する方法がファイルシステムに用意されていれば容量を増やすことはできますし、単に新しく作ったファイルシステムに情報をコピーしても増やすことはできます。どちらが簡単かはそれこそファイルシステムによりますね。
 減らすのが難しいのもこういう構造だからです。

 それと途中で「狭義のファイルシステム」みたいな事を書いたと思いますが、じゃあ広義ってなんだというと、ディスク上の構造ではなく、インタフェースとしてのファイルシステムという概念があります。
 この辺から、OSとかユーザ寄りの話になってきて、これはこれでかなり面白いんですが、別の話なので別な時に。
 キーワードは仮想ファイルシステムとか、まあ単にファイルシステムでもいいです。あとはreadカーネルコールの処理を追いかけてみるとか。