![[Programmer's High]](/community/blog/images/union/pg_high_logo.png)
UnionMount
AUFS/UnionFSがunion内のメンバをディレクトリとするのに対し、UnionMountでのメンバはブロックデバイス(すなわちマウントとしてunion機能実装)です。もっとも大きな違いはその実装方針にあり、UnionMountはより上位に位置するVFS内でunion機能を実現しようとしています( 図6 )。
ファイルシステムとしてのunion実装と比較すると、VFS内でメンバ内から処理対象を選択決定する点が大きく異なります。このため、 vfs_mkdir() などVFSヘルパ関数を再度コールする必要がなくなります。
図6 :VFS とファイルシステムのレイヤ関係―UnionMount

fallthruと readdir(3)
UnionMountでは readdir(3) の実装に大変苦労していましたが、平成21年春に投稿されたパッチシリーズではfallthruというファイルタイプを導入し、新たな実装が試みられました。
UnionMountに限らずファイルシステムとしてのunion機能でも、 readdir(3) の実装が難しい点は seekdir(3) 対応にあります。通常ファイル(regular file)内でI/Oを行う位置ファイルポジションを移動できるように、ディレクトリに対しても seekdir(3) により移動が可能です。nfsやsmbfsではこれを積極的に利用しています。union機能ではメンバに対し順次内部で readdir(3) した結果を結合し、それをunionの readdir(3) 結果として返しますが、 seekdir(3) に対応するためには結合結果を保持する必要が出て来ます。AUFSでは、通常のファイルシステムがエントリを維持したディレクトリブロックをディスク上に保存するように、メモリ上に一時的な結果をキャッシュします。その実装には、キャッシュのライフタイム管理、上位/下位での重複を判断するためのハッシュテーブル、エントリ数が増加してもフラグメンテーションを発生させないメモリ管理など随所に工夫が見られますが、UnionMountではこの複雑さを嫌い、lookup時にディレクトリをcopy-upし、 readdir(3) 時にエントリをfallthruという特殊なファイルとして上位メンバに作成します。
fallthruはファイルシステム内に「実体を持たない名前だけのエントリ」として存在するファイルで、unionの readdir(3) 処理を簡略化するものです。VFS内のlookupには変更が必要ですが、 readdir(3) の変更は不要とすることを狙っています。最初の readdir(3) 実行時に、下位メンバ内に存在するエントリを上位メンバ内にfallthruとして作成し、 readdir(3) が返す結果として、上位メンバの readdir(3) 結果をそのまま使用できます。VFSの readdir(3) の変更は不要とする目的は達成されていますが、いくつか問題があります。
資源の消費
lookupしただけでディレクトリがcopy-upされ、空のディレクトリが上位メンバに作成されます。また readdir(3) すると、そのディレクトリ下のファイルがfallthruとして作成されます。これはいずれも上位メンバのディスク容量を消費することを意味します。空ディレクトリもfallthruも1つ1つの消費量は多くありませんが、 find コマンドなどでファイルシステム全体をlookup/ readdir(3) すると、その消費量は無視できなくなるでしょう。もちろんunion内に存在するファイル数に応じて消費量は変化します。
この消費に大きく影響を受けるのは上位メンバにtmpfs/shmfsを使用した場合です。tmpfsなどメモリをバックエンドとするファイルシステムでは、作成したファイルは削除されるまでメモリ内に常駐します。通常システムのメモリ容量はディスクに比較すればずっと小さなものなので、常駐させるとメモリを圧迫する恐れがあります。最近のLiveCDでは収録アプリケーション数の増加に伴い、CDからDVDへとメディア容量も増大する傾向がありますが、これはfindコマンドなどを使用すると、tmpfsに作成されるfallthruエントリ数が増加することを意味します。 AUFSでも一時的にはメモリを圧迫しますが、必要に応じメモリを解放しますので、システムがメモリ不足に陥る可能性は下がります。実質的にファイル名だけの存在といえども、すべてをtmpfsに作成するアプローチはLiveCD環境、特に多数のフォルダを操作するユーザや readdir(3) をよく使用するアプリケーションにとってはデメリットが大きい恐れもあります。
ファイルシステム側の変更
UnionMountはVFSレイヤでのunion機能の実装ですが、VFSの変更に伴い、ファイルシステムにも追加実装が必要となります。前述のfallthruはファイルシステム内に作成される特殊なファイルタイプですので、これを作成する機能を新たに実装する必要があります。UnionMountのパッチシリーズではext2とtmpfsに限り、fallthruを作成する関数を追加しています。他のファイルシステムはこれからおいおい実装するということかもしれません。
fallthru対応はカーネル/ファイルシステム内に留まりません。ファイルシステムには、ほとんどの場合、専用のユーティリティが付属します。初めにファイルシステムを構築する mkfs ヘルパ、整合性を検査し異常時には修復する fsck ヘルパなどです。fallthruは新たなファイルタイプですので、ユーティリティにも変更が必要となります。特に fsck では、ファイルを新規作成した途中でシステムが異常終了した場合と、正常に作成されたfallthruを区別するなどを考慮する必要も出て来るかもしれません。
fallthruだけでなく、whiteoutについてもファイルシステム側の対応が必要になります。UnionMountはwhiteout用にも新たなファイルタイプを追加しており、fallthru同様に実体のない、名前だけのファイルが作成されます。簡単に言うと、fallthruが readdir(3) に対し「同名のエントリが下位に存在するので、この名前を存在するファイル名として扱え」と意味するのに対し、whiteoutはlookup全般に「同名のエントリが下位に存在するかもしれないが、すでにunion内では論理的に削除されているので、存在しないファイル名として扱え」という意味になります。
AUFS/UnionFSでのwhiteoutの実装はファイルシステム側の対応は不要です。特殊な名前の通常ファイルがwhiteoutとして機能します。具体的には、下位に存在するファイル名の先頭に" .wh. "を付加したもの、例えば .wh.fileA となります。このファイルが上位メンバに存在すると、下位メンバをlookupせずに fileA は存在しないファイル名として扱われます。
read/writeの上位メンバが必須
上位メンバにreadonlyなものを指定できなくなる点についても問題が指摘されています。例えば、readonlyで圧縮ファイルシステムのsquashfsなどを2つunionする場合に問題となります。前述のようにユーザがファイルを作成しなくとも、UnionMountが上位メンバにディレクトリや特殊なファイルを書き込むため、上位メンバは必ずread/write可能なものでなければいけません。そもそもUnionMountの開発は、メンバは2つだけ、下位がreadonly、上位がread/writeに固定という前提で進められたので、ある意味当然の結果かもしれません。しかし、現実にはreadonlyなメンバを複数使用することも多いので、あくまでも現時点での話ですが、UnionMountの使用は手間がかかると思われます。
この問題に対しては、tmpfsなどread/write可能なものを常に指定する方法以外に、readonlyなファイルシステムを改造し、実際にはディスクに書き込まなくとも write(2) などがエラーを返さず無条件に成功を返すfake writeという方法も提案されました(現実にはその方向には進んでいません)。readonlyのマウントが可能なAUFS/UnionFSに比較すると、この点もデメリットと言えるでしょう。
ハードリンクの扱いが不十分
AUFSと比較するとUnionMount内ではハードリンクが正しく機能しない場合があります。これはファイルシステムとしての実装であるかないかという違いに起因するものです。
例えば、下位のメンバにのみ存在する fileA が同一ファイルシステム内で fileL にハードリンクされていたとします。この状態では、 fileL はどの実装のunion機能でも正しく表示され、 fileA とまったく同じ内容となります。その後 fileA の内容を書き換えると、union内部でcopy-upが発生し、上位メンバ内に fileA が作成されます。上位に存在するようになった fileA の内容は期待通り最新になりますが、ハードリンクである fileL は下位にのみ存在する状態のままで、 fileA とは違う内容になります。この問題に対応し、 fileL でも最新の内容を正しく表示できるのは、現時点ではAUFSだけです。
しかし、この問題は現時点ではあまり重要視されていません。
ファイルオープン後のcopy-up
UnionMountではファイルが最新の情報を反映しない問題は、ハードリンク以外でも発生します。次のように下位メンバにのみ存在する fileA に対し、2つのプロセスが同時にI/Oする場合を考えます。
ProcessA()
fd1 = open("fileA", O_RDONLY); read(fd1);
ProcessB()
fd2 = open("fileA", O_WRONLY); write(fd2);
どちらも単純なコード例であり、通常はどの環境でも問題無く動作します。ProcessAの read(2) は期待通り、その時点での最新の内容を読み取ります。ここで言う「最新」とは、同時に実行されているProcessBが先にスケジューリングされ、ProcessAの read(2) よりも先に write(2) が実行されれば write(2) 後の内容を返し、そうでなければ write(2) 前の内容を返すと言う意味です(仮に read(2) にバグがあり、最新ではない内容が返されても、書き込んでいるプロセスが別になっている限り、気がつかない場合もあるかもしれません)。
では、1プロセスから1ファイルに書き込んでから読み取るとどうでしょうか。
fd1 = open("fileA", O_RDONLY); fd2 = open("fileA", O_WRONLY); write(fd2); read(fd1);
この場合では read(2) は最新の内容、すなわち常に write(2) 後の内容を返すことが期待されます。しかし、UnionMountでは read(2) で最新の内容が得られない場合があります。
UnionMountの現時点での実装では、copy-upが発生するのは書込み用にファイルをオープンした時点です(AUFS/UnionFSではオープンしただけではcopy-upせず、書込み発生時にcopy-upする)。内部でcopy-upし、上位メンバ内に作成したファイルをオープンし、ファイルディスクリプタを返します。上例のコードでは読み取り用のファイルオープンが先に行われており、この時点ではfileAはまだ上位メンバ内には存在していません( 図7 )。その後の書込みはすべて、copy-upにより上位メンバに存在するようになったfileAに対して行われ、下位のfileAは変更されません。つまり先に読み取り用にオープンされたファイルディスクリプタからは古い情報しか得られません。
図7 :リフレッシュ動作―初期状態

この問題はcopy-upのタイミングが原因というよりも、やはりファイルシステムとしての実装であるかないかという点に由来すると考えられます。ファイルシステムには、前述のハードリンクもそうですが、 read(2) は最新の内容を返すことが明に必須とされますが、マウントとしての実装では必ずしもそうではないのかもしれません。また、ファイルシステムとして実装すると、カーネル内部のデータ構造に自ファイルシステム専用の情報を埋め込める利点があります。具体的にはオープンしたファイルを表す構造体 struct file 内に用意されている、 void \*private_data に自ファイルシステム専用データへのポインタを代入します。 struct file はオープンする度にカーネル内で作成されるデータで、上例の2つのオープンでは同じファイルを指すstruct fileが2つ作成され、ファイルディスクリプタに対応します。
図7 にその動作の過程を図示してみました。 図6 の状態から始まり、前掲のコード例を1行づつ実行した際に、オープンしたファイルに対応するカーネル内のデータがどのように作成されるを示しています。 open(2) の度に struct file が作成されるのは、どのファイルシステムでも共通の動作ですが、UnionMountではunion内の実ファイルに対応する struct file が作成されます。すなわち fstat(2) するとunionの情報ではなく、union内のメンバの情報が得られます。copy-up後は、2つの struct file を切り替えず、それぞれのファイルを操作します。
図8:UnionMountはファイルをリフレッシュしない

AUFS/UnionFSでは struct file は抽象ファイルである自ファイルシステムとしての fileA を表し、メンバ内に存在する実ファイルの fileA を表すデータは void *private_data へ代入しています。ファイル書込みによりcopy-upが発生すると、対象の struct file は上位の fileA を参照するように更新されますが、もう1つの struct file は更新されない点は同様です。しかし、copy-up後に読み取りが発生した時点で、AUFS/UnionFSが対象ファイルがunion内の最上位に存在するものを参照しているかを調べ、そうでなければ最上位を指すように void *private_data を操作し、仮想ファイルの struct file をリフレッシュすることで、上位の fileA から最新の内容を読み取ります。
同様に 図9 にAUFS/UnionFSでの動作を示しました。UnionMountとは異なり、 open(2) の際に作成される struct file はAUFS/UnionFS内の仮想的なファイルに対応し、AUFS/UnionFSが内部でunionメンバ内の実ファイルをオープンします。また、copy-upはオープンしただけでは行われず、書込みの際に行われます。 struct file を1つ余分に作成することで、union内の変化(copy-up)に追随できるようになります。
図9:AUFSはファイルをリフレッシュする

UnionMountの実装では抽象 struct file を使用しておらず、ファイルがcopy-upされ内容が古くなってしまったことを検知する術がありません。もちろん今後の開発により、UnionMount用に struct file に新規メンバが追加されることは考えられますが、単純さを優先する方向性がうかがえるため、そのようには実装されないかもしれません。
copy-upによるinodeの変化
UnionMountのcopy-upでは stat(2) も予想外の値を返します。これもファイルシステムとしての実装ではない点に由来しますが、通常 write(2) 前後の stat(2) はファイルサイズ、日付などが異なりますが、UnionMountではデバイス番号やinode番号も変化します。別ファイルシステム上に存在するようになり、メンバファイルシステムからの情報をそのまま返すためです。この動作はデバイス番号、inode番号を意識しながら処理を進める chmod/chown -r などが影響を受ける恐れがあります。AUFSではファイルシステムとして抽象化されたファイルの情報が返されるため、またinode番号を自身で管理しているためデバイス番号やinode番号は維持されます(UnionFSではinode番号が変化する場合がある)。
メリットとデメリット
UnionMountのメリットは、なんと言ってもパッチ規模の小ささでしょう。VFS内でunion機能を実装するため、AUFS/UnionFSのようなVFSヘルパ関数の再呼び出しを排除でき、より簡素になっています。lookupなどはVFS内に新たな複雑さを導入するデメリットはありますが、この点についてはそのメリットの方が大きいと思われます。
VFS内でunion機能を実装する試みはAUFSのソースツリーにある開発ブランチ'rum'(Readonly UnionMount)でも行われています。Readonlyとされているため、AUFSが持つ機能の大半が削除され、VDIR(virtual or vertical directory)と呼ばれる readdir(3) に対応する機能のみがVFSへ移植されたものです。UnionMountの readdir(3) が上位メンバへ書き込むのに対し、rumでは一切書き込まず、squashfsやCD-ROMだけでもunionを構成できる点が大きな特長でしょう。しかしrumは開発ブランチのままで特にアナウンスもされていません。
デメリットの大きな点は、前述の上位メンバの容量圧迫が予想されます。lookupしただけでもディレクトリをcopy-upし、 readdir(3) しただけでもfallthruを作成するアプローチは、VFS内の処理簡素化よりも潜在的に大きなデメリットです。AUFSの readdir(3) でもメモリは消費しますが、一時的なもので、システムの他の部分でメモリを必要とされれば、通常は解放/再利用されます。UnionMountでのtmpfsに恒久的にfallthruを作成するのと比べればずっと少ないメモリ量です。これはメモリ消費対速度性能もしくは複雑さという一般的な問題とも考えられます。UnionMountでは一度 readdir(3) してしまえば、以降は再利用するので高速に動作することが期待できますが、メモリを大量に消費する恐れがあります。
copy-up
更にcopy-upの実装も上位メンバの容量圧迫の一因となりえます。copy-upはカーネル内で自動的に処理されるためユーザが意識することはありませんが、 /var/log/wtmp などサイズの大きなsparseファイル(ファイルの一部にディスクブロックが割り当てられていない、「穴空きファイル」)の処理には注意が必要です。sparseファイルを単純にcopy-upしてしまうと、本来はそれほどディスクブロックを消費しないはずのに、「穴」の部分にも延々とゼロを書込み、ディスクブロックを無駄に消費してしまいます。現在のcopy-up実装でsparseファイルに対応しているのはAUFSのみです。
その他
そのほかの機能上のデメリットとしては、union内のメンバ管理があります。AUFS/UnionFSではメンバ(ディレクトリ)を動的に追加/削除/属性変更する機能を備えていますが、UnionMountではメンバ(ディスク)を動的に変更する機能はなく、単に上に積み重ねる、または上から取り除くのみです。
デメリットとして強いて挙げるならば、まだ実装が充分進んでいない点でしょうか。例えば link(2) / rename(2) は既存のファイルを、同一ファイルシステム内に限り処理するものですが、現在のUnionMountではリンク元/リネーム元をcopy-upしていないため、エラーとなります。 rename(2) については、多くの場合 mv から使用されるため、通常は mv 内でエラーに対応し rename(2) の代わりにコピーを行うのでまだ影響は小さいのですが、 link(2) についてはなんらかの対応が必要になると思われます。しかし、この点についても「 link(2) が異なるファイルシステムを跨げないのは当然だ。UnionMountは複数のファイルシステムを操作するのだから、 link(2) がエラーを返すのも当然だ」とする考え方もあり得るでしょう。
おわりに
現時点でのLinuxにはunion機能は含まれていませんが、今後UnionMountが取り込まれる見込みです。すでにカーネルクラッシュなども報告されていますが、バグ修正なども今後進むものと思われます。構造に由来する機能的な不足についてはどのように対応するかが注目です。UnionMountが使用可能になるまでは、AUFSが選択の第一候補となるでしょう。
Copyright © 2009 SENJU, Jiro
本稿で取り上げた内容は、記事の執筆時(2009年6月)の情報に基づいており、その後の実装の進展、変更等により、現在の情報とは異なる場合があります。
本連載「プログラマーズ・ハイ」では読者からのご意見、ご感想を切望しています。今後取り上げるテーマなどについてもご意見をお寄せください。