
Linuxをハードディスクへインストールせずに使用するLiveCD/DVDも広く利用されるようになり、多くのディストリビューションが従来のインストール媒体/方法に加え、LiveCDをリリースしています。ほとんどのLiveCDではsquashfs、tmpfs、更にAUFSを用い、ハードディスクへインストールしない形態を実現していますが、本記事ではその他の方法も取り上げ、考察します。 なお、本記事はhttp://sourceforge.net/mailarchive/forum.php?thread_name=20986.1293537718%40jrobl&forum_name=aufs-usersを元にしています。
古代の構成
一般的にLinux LiveCDではシステムをインストールした状態のファイルシステムを圧縮し、読み取り専用ファイルシステムイメージとして収納してあります。この際に用いられるのがsquashfsなどのファイルシステムですが、ここからそのままシステムを起動しても読み取り専用ですから、使用できない部分が生まれます。
圧縮の反対動作を指す言葉は伸長とも表現されますが、本記事では展開と表現します。
例えば、システムを起動すれば各種ログファイルや/var/wtmpファイルに対する書き込みが発生しますし、ファイルシステムをマウントすれば/etc/mtabというファイルも更新されます。デーモンが起動されればサービスに必要な各種ファイルなども作成されます。それぞれを一つづつ調査し、書き込みが発生しないように対処して行くことも不可能ではないかもしれませんが、もちろん手間がかかります。そもそもLinux(UNIX)システムは書き込み可能ファイルシステム上で動作することを前提としているのですから、この努力は有意義とは言えないでしょう。
かつてはシャドウディレクトリ方式で対応しようという時代もありました。シャドウディレクトリとはやや古い呼び方かもしれませんが、X.Org/XFree86以前のX11 Window Systemのビルドなどにも用いられていた(と思うけど、記憶に自信がなくなってきた )形態です。例えば、ソースファイルが置かれたsrc/下でそのままビルドすると、他のアーキテクチャ用にビルドする際には全オブジェクトファイルを削除しなければなりません。そうすると、先のアーキテクチャ用のリビルド時にはまた初めからすべてをコンパイルしなければならず、この手間暇を節約するための方策として、obj/を別に用意し、全てのソースファイルのシンボリックリンクをobj/下に作成する方法が採られました。この形態がシャドウディレクトリで、ビルドされるオブジェクトファイルはobj/下に作成され、ソースファイルを書く場合にもディレクトリの差異を気にする必要もありません。lndir(1)を用いると、子ディレクトリ、孫ディレクトリも含んだシャドウディレクトリを作成できます。
シャドウディレクトリの例
$ cd obj
$ lndir -silent ../src
$ cd ..
$ find src obj -ls
drwxr-xr-x ... src
-rw-r--r-- ... src/a.c
drwxr-xr-x ... src/sub
-rw-r--r-- ... src/sub/b.c
drwxr-xr-x ... obj
lrwxrwxrwx ... obj/a.c --> ../src/a.c
drwxr-xr-x ... obj/sub
lrwxrwxrwx ... obj/sub/b.c --> ../../src/sub/b.c
以前のLiveCDでは読み取り専用ファイルシステムイメージの作成時に、例えば/readonlyというディレクトリ下にシステム全体を格納し、/etc、/binなどを作成し、LiveCD起動時にはtmpfsをルートディレクトリとし、/readonly/etc、/readonly/bin下の全ファイルへのシンボリックリンクを作成するというシャドウディレクトリ方式が採られたことがあります。この状態ではファイルの新規作成はtmpfsに対して行われ、読み取りにだけCD-ROM内のファイルシステムイメージが用いられ、当初の目的は達成できます。しかし、シンボリックリンク数は実用的ではない程膨大になり、システム起動にも時間がかかります。更に、既存のファイルを変更する場合には、ファイルを一度tmpfsへコピーする手間もかかります。
squashfsとAUFS
その後に用いられるようになったのがUnion機能を備えたファイルシステムである、AUFS、Unionfsです。この機能については以前の記事「UnionMountとUnion-type Filesystem」で取り上げました。簡単に振り返ると、CD-ROM内の読み取り専用ファイルシステムイメージをループバックマウントし、その上に書き込み可能なファイルシステムを透過的に重ねてマウントし、上述の読み取り/書き込みを実現します。既存ファイルを変更する際のコピーもUnion機能が備えるcopy-upにより自動的に行われます。レイヤは複数指定できるので、アプリケーション/パッケージの取捨選択にも応用できます。例えばSLAX LiveCDではコンパイラパッケージ、オフィススートなどを個別の読み取り専用ファイルシステムイメージに分割しており、ユーザが必要に応じレイヤとして追加します。
現在のLiveCDでは、上位の書き込み可能なレイヤにはtmpfsやHDD上のファイルシステムを指定し、下位の読み取り専用ファイルシステムには圧縮機能を備えたsquashfsを、Union機能にはAUFSをそれぞれ用いるのが一般的です。
squashfs作成例
$ mksquashfs /bin /dev /etc /lib /sbin /usr /tmp/squashfs.img \
-no-progress -noappend -keep-as-directory -comp gzip
squashfsと aufsのマウント例 (LiveCD)
(initramfs内の起動スクリプト)
# mount -o ro /dev/cdrom /cdrom
# mount -o ro,loop /cdrom/squashfs.img /squashfs
# mount -t tmpfs none /tmpfs
# cd /tmpfs; mkdir -p tmp var/log; chmod 1777 tmp; cd /
# mount -t aufs -o br:/tmpfs:/squashfs none /aufs
(以降、/aufsをルートディレクトリしてシステム起動)
上の例を実行した結果構築されるファイルシステムとブロックデバイスのレイヤ関係を図示します(図1)。比較、参考のため、通常マウントのデバイス/レイヤ関係と、ループバックマウントの場合も図示します(図2、図3)。通常、ファイルシステムはブロックデバイスと一対一に対応しますが、ループバックマウントの場合は/dev/loopNという特殊なブロックデバイスを用い、このデバイスが別途マウントされたファイルシステム内に存在する1ファイルに対応します(図3では/dev/sda1内に存在するsquashfs.img)。
図1:squashfsとaufsのマウント例(LiveCD)

図2:通常のファイルシステムマウント例

図3:ループバックマウント例

しかし、上記のようなファイルシステム環境はこの組み合わせ以外でも実現可能です。本章では二つの方法を紹介します。
cloop:ループバックブロックデバイスの圧縮機能
一つ目は圧縮機能をファイルシステムではなく下位に位置するブロックデバイスに実装したcloop(compressed loopback block device)というモジュールです。標準ではLinuxカーネルには含まれていません。
http://debian-knoppix.alioth.debian.org/packages/cloop/cloop_2.636-1.tar.gz
ブロックデバイスレベルでの機能なのでファイルシステム種類を問いません。一度ファイルシステムイメージを作成し、必要ファイルをコピーした後に、create_compressed_fsコマンド(別名advfs)でファイルシステムイメージを圧縮し、cloopイメージを作成します。
cloopイメージの作成/マウント
$ dd if=/dev/zero of=/tmp/ext2.img bs=10M count=1 ---ファイル領域確保
$ mkfs -t ext2 -F -m0 -q -O dir_index /tmp/ext2.img ---ext2イメージ作成
$ sudo mount -o loop /tmp/ext2.img /mnt ---/mntへマウント
$ tar -C /bin -cf - . | sudo tar -C /mnt -xpf - ----/binを/mntへコピー
$ sudo umount /mnt
$ create_compressed_fs -q -L9 /tmp/ext2.img /tmp/ext2.cloop.img ---cloopイメージ作成
$ sudo losetup -r /dev/cloop0 /tmp/ext2.cloop.img ---cloopデバイスと関連付け
$ mount -o ro /dev/cloop0 /mnt ---cloopデバイスをマウント(以降/mntをext2として使用可能)
レイヤ関係は基本的に図3と変らず、squashfs以外の任意のファイルシステムを圧縮、使用できます。ループバックブロックデバイスは/dev/loopNから/dev/cloopNへ変更します。上の例では/dev/cloop0というデバイスを用いていますが、これはcloopモジュールがロード時に作成するものです。
また、cloopは書き込みには対応していませんので、cloopでループバックマウントしたファイルシステムは読み取り専用になるため、(疑似的に)書き込みを実現するには、AUFSなどスタッカブルファイルシステムを用いる必要があります。
cloopモジュールを採用しているLiveCDにはKnoppix(とその派生物)があります。
dm-snapshot:デバイスマッパのスナップショット機能
もう一つの方法はスタッカブルファイルシステムの代わりにデバイスマッパのスナップショット機能を用いるものです。デバイスマッパは、やはりブロックデバイスレベルの機能で、論理ボリューム管理(LVM)などの上位ツールから利用されるのが一般的ですが、dmsetup(8)コマンドを用い、デバイスマッパを直接操作することも可能です。
デバイスマッパはファイルシステムの下位に位置するブロックデバイスを操作するもので、スナップショット以外にも暗号化、リニア、ストライプ、ミラーなどの機能があります。暗号化機能はデバイスに対する書き込み/読み取りをデバイスマッパが符号化/複合化するもので、デバイス上には符号化された結果が書き込まれます。リニア/ストライプ/ミラーはRAID一般の機能なので、本記事では割愛します。
スナップショット機能は、デバイスマッパに二つのブロックデバイス、それぞれオリジナルデバイス、COWデバイスと呼びます、を指定し、書き込みはすべてCOWデバイスへ向け、読み取りは対象がすでにCOWデバイスに存在すればCOWデバイスから、存在しなければオリジナルデバイスから読み取ります。まさに、スタッカブルファイルシステム内の読み書き動作が上位/下位レイヤへ振り分けられる動作です(図4)。COWデバイスには変更差分が蓄積され、オリジナルデバイスは変化しません。
デバイスマッパには zeroという機能(dm-zero)もあり、/dev/zeroに似た動作をするブロックデバイスを作成できます(/dev/zeroはキャラクタデバイスです)。すなわち、書き込みは何もせずに単に成功を、読み取りには常に0を返すデバイスです。筆者は有効性を確認していませんが、dm-zeroとスナップショットを組み合わせ、ブロックデバイスやファイルシステムのテストに応用する方法があります。実デバイスよりもサイズが大きな zeroデバイスを作成し、これをCOWデバイスとし、実デバイスのスナップショットを作成します。もし、実デバイスの範囲を越えた位置に I/Oが発生すると、そのI/OはCOWデバイスへ向けられ、結果的に正常に処理されず、エラーを検知できるという方法です。
図4:dm-snapshotマウント例

デバイスマッパによるブロック単位の Copy-on-Write(COW)
(16MBのext2イメージを作成)
$ dd if=/dev/zero of=/tmp/ext2.img bs=1M count=16
$ mkfs -t ext2 -F -q /tmp/ext2.img
$ sudo mount -o loop /tmp/ext2.img /mnt
$ > /mnt/fileA
$ sudo umount /mnt
(サイズが1MBのCOWイメージを作成、sparseファイル)
$ dd if=/dev/zero of=/tmp/cow.img bs=1 count=1 seek=$((1024 * 1024 -1))
(ファイルとループバックデバイスを結合)
$ sudo losetup -r /dev/loop0 /tmp/ext2.img
$ sudo losetup /dev/loop1 /tmp/cow.img
(二つのデバイスを結合)
$ echo 0 $(du /tmp/ext2.img | cut -f1) snapshot /dev/loop0 /dev/loop1 p 0 | sudo dmsetup create dm-cow
(結合したデバイスをマウント)
$ sudo mount /dev/mapper/dm-cow /mnt
(以降/mntへの書き込みは/tmp/cow.imgへ蓄えられ、/tmp/ext2.imgは変更されない)
動作がスタッカブルファイルシステムとほぼ同じだとしても、操作対象が異なります。すなわち、デバイスマッパではブロックデバイス/セクタを対象とするのに対し、スタッカブルファイルシステムではファイルシステム/ファイルを対象とします。例えば、ブロックサイズを512バイトとし、サイズが2KBのファイルがオリジナルデバイスにあったとします。つまりファイルシステム上の4ブロックを消費しているファイルです。このファイルの先頭から1KB目の1バイトだけを書き換えると(3ブロック目の先頭バイト)、デバイスマッパのスナップショット機能は、3ブロック目だけをCOWデバイスへ書き込みます。変更されない他のブロックは書き込まれません(厳密に言えば、ファイル更新時刻などinodeが持つ情報も更新されるため、inodeブロックも書き込まれます)。言い換えるとオリジナルデバイスはファイルシステムが構築されマウント可能な状態であっても、COWデバイスにはファイルシステムが構築されずファイルシステム内の一部のブロックだけが保存されており、それだけをマウントすることができません。一方、スタッカブルファイルシステムではファイル単位で操作をするので、下位レイヤからファイル全体(2KB)を上位へコピーし、その後上位レイヤで本来要求された書き換えを処理します。
実行効率を考えれば、ファイル全体をコピーしないデバイスマッパの方が有利ですが、変更差分だけを確認したいといった場合の利便性では不利となる場合も考えられます。スタッカブルファイルシステムでは上位レイヤに対し、ls(1)すれば変更されたファイル一覧が得られますし、下位レイヤとdiff(1)することもできます。この機能は差分バックアップにも応用できます。デバイスマッパではCOWデバイスだけをマウントすることはできず、当然ls(1)もできません。COWデバイスは常にオリジナルデバイスと組み合わせて使用する必要があります。また、COWデバイスと結合していない時などに、オリジナルデバイスに直接変更を加えてしまうと、整合が取れなくなり、それまでのCOWデバイスが使えなくなる恐れもあります。
COWデバイスの内容をオリジナルデバイスへ書き戻す、スナップショットマージ機能もありますが、本記事では割愛します。
デバイスマッパは既存のデバイスから新たにデバイスを作成しますが、この新デバイスに対してもデバイスマッパは動作します。この特徴を活かし、スナップショットを複数重ねることも可能です。スタッカブルファイルシステム風に言うと、レイヤ(ブランチとも呼ぶ)を複数使用できます。
下位レイヤは読み取り専用デバイスですが、デバイス内に構築するファイルシステムには書き込み機能が必要となる点には注意が必要です。ファイルを書き込むのはファイルシステムの機能なので、オリジナルデバイスに構築するのがsquashfsのような読み取り専用ファイルシステムだと、COWデバイスを追加してもマウントするのはsquashfsとなり、書き込むことができません。しかし、圧縮機能は是非とも欲しいところです。Linuxシステムを一般的にインストールしたファイルシステムを圧縮せずにLiveCDに収めるのは随分窮屈になってしまいます。デバイスマッパを採用しているLiveCDにはFedoraがありますが、Fedora(少なくともバージョン12)ではインストールした状態をext3(名前と異なり中身はext4です)イメージとして作成し、このファイル一つだけを含むsquashfsを作成しています。この場合、マウント時に二重にループバックマウントします。若干簡略化した例をレイヤ図(図5)と共に示します。
デバイスマッパとsquashfsを併用した例
$ sudo mount -o ro /dev/cdrom /cdrom
$ sudo mount -o ro,loop /cdrom/squashfs.img /squashfs
$ sudo losetup -r /dev/loop1 /squashfs/ext3.img
$ dd if=/dev/zero of=/tmp/cow.img bs=1 count=1 seek=$((1024 * 1024 -1))
$ sudo losetup /dev/loop2 /tmp/cow.img
$ echo 0 $(du /tmp/ext3.img | cut -f1) snapshot /dev/loop1 /dev/loop2 p 0 | sudo dmsetup create dm-cow
$ sudo mount /dev/mapper/dm-cow /mnt
少し面倒ですが、この例は文章でも補足しておきます。図5を下から上へ追ってみます。 cd-romをマウントすると、squashfs.imgというファイルがあり、これをループバックマウントします。するとそこにはext3.imgというファイルだけがあり、これを/dev/loop1と結合し、ブロックデバイスとして扱えるようにします。別途作成した空ファイルも/dev/loop2と連結し、loop1をオリジナルデバイス、loop2をCOWデバイスとして、新デバイスdm-cowを作成し、これをマウントすると(やっと)読み書き可能なファイルシステムとして利用可能になります。
図5:デバイスマッパと squashfsを併用した例

二重ループバックマウントとsquashfsのキャッシュ
二重ループバックマウントのメリット、デメリットを考えてみましょう。
まず資源消費が思い当たります。ブロックデバイスが増えると言うことは、そのデバイス用にキャッシュすることになり、これは不必要な二重キャッシュの恐れがあります。システム内部ではマウント毎にオブジェクトを作成し、内部のマウントツリーに追加します。すなわち、メモリを消費し、アクセス時にはツリーを辿る処理が増えます。また、ループバックブロックデバイスも一つ余計に消費します。一般的にデバイスにはその種類毎に0から番号が振られ、この番号には上限があります。ループバックブロックデバイスの場合、デフォルトでは0-7まで八つのデバイスが作成され、モジュールパラメータを明示的に指定すれば255まで拡張可能ですが、二重ループバックマウントの場合は、大雑把に言って二倍の消費量となります。更にloopNという名前のカーネルスレッドがループバックマウント毎に起動され、常駐します。これもメモリ消費、プロセス ID空間消費、スケジューラの処理増加につながります。
デメリットを先に挙げましたが、悪いことばかりではありません。もっとも大きな効果は、圧縮されたsquashfsを展開した結果をキャッシュする点です。mksquashfsは圧縮効率を高める(結果サイズをより小さくする)ため、指定されたサイズごと(squashfsのブロックサイズ。デフォルトでは128KB)に圧縮しますが、圧縮対象ファイルサイズがこのブロックサイズよりも小さい場合は、他のサイズが小さいファイルとまとめて圧縮します。これをフラグメントブロックと言います。このため、サイズが1KBのファイルfileAと同じく1KBのfileBが存在する場合、fileAにアクセスしても、squashfs内部動作としてはfileBも同時に読み取り/展開することになります。展開結果はキャッシュされますが、フラグメントブロックについては一般に使用されるキャッシュ機構(ページキャッシュ)を用いず、squashfsが独自に実装したキャッシュ機構(フラグメントキャッシュ)を用います。この点についてはページキャッシュを利用した方が良いと言う指摘が度々あるようですが、現在までsquashfsは独自路線を維持しています。
このフラグメントキャッシュ量はconfig時に変更可能です。CONFIG_SQUASHFS_FRAGMENT_CACHE_SIZEがそれで、デフォルト値は3です。
展開されたfileAについては一般的なファイル読み取り処理の一環としてページキャッシュに蓄えられますが、同じフラグメントブロックに存在する他のファイルについてはページキャッシュにはキャッシュされず、squashfs独自のフラグメントキャッシュにのみ残されます。次にfileBへアクセスし、ファイルデータがfileAの場合と同じフラグメントブロックに存在する場合、フラグメントキャッシュに残っていれば良いのですが、残っていなければ再び読み取り、展開します。フラグメントキャッシュのデフォルト値は大きくありませんから、アクセスパターンがランダムの場合にはキャッシュから追い出されている場合が多くなり、結果的に実行速度が犠牲になってしまいます。フラグメントキャッシュサイズを大きくすれば良いと言えばその通りですが、このキャッシュは squashfs内で固定的に確保され、ページキャッシュの様に動的に増減はしないので、メモリ圧迫の恐れがあります。
しかし、二重ループバックマウントすると、二番目のループバックブロックデバイスがフラグメント部分を含め展開結果を全てキャッシュ対象(ページキャッシュ)とするため、同じブロックを展開する機会が減り、アクセス速度は犠牲になりにくくなります。もちろん、不必要に二重、三重にキャッシュする恐れはあります。しかし、キャッシュによる速度改善は大きなものですし、またページキャッシュは参照頻度の低いものから順に破棄されるため、二重ループバック環境では下位キャッシュ(squashfs)から先に破棄され、上位キャッシュ(上例のext3)は生き残りやすい傾向が予想できます。このため前述のデメリットを補って余りある速度性能向上が期待できます。また、squashfs内に含まれるファイルがサイズの大きなファイルシステムイメージ一つとなるため、フラグメントブロックは実質的に発生しません。
mksquashfsにはフラグメントブロックを使用しない-no-fragmentsオプションが用意されており、圧縮サイズは犠牲になりますが、アクセスパターンがランダムの場合には効果を発揮します。
ブロックデバイス読み取り速度と CPU展開速度
一般にCPU動作はブロックデバイス動作よりも高速で、また展開処理はCPUを多く消費します。圧縮されたファイルまたはファイルシステムを読み取る際にはこの点が重要となる場合があります。
一般的なデスクトップPC環境では、圧縮しないファイルを読み取る場合の方が、展開処理に時間がかかるため、圧縮した場合よりも高速となる場合が多いのですが、低速なブロックデバイスを使用する環境ではCPU/ブロックデバイスの速度差が開き、圧縮した場合の方が動作が高速になるという逆転が発生する場合があります。例えば、サイズが4KBのファイルを圧縮して1KBになった場合、低速なブロックデバイスから4KBを読み取るよりも、1KBを読み取り、展開した方が短時間で済む場合があります。つまり、展開処理に時間をかけることになっても、ブロックデバイスからの読み取り時間短縮の方が効果が大きいということです。
(初出H22/12)
Copyright © 2010-2011 SENJU Jiro
本記事のサンプルコードは、以下のリンクよりダウンロードすることができます。記事と合わせてご参照ください。
[サンプルコード]
また、本稿で取り上げた内容は、記事の執筆時の情報に基づいており、その後の実装の進展、変更等により、現在の情報とは異なる場合があります。
本連載「プログラマーズ・ハイ」では読者からのご意見、ご感想を切望しています。今後取り上げるテーマなどについてもご意見をお寄せください。