2007年09月
これまでLinuxのハードウェア自動認識と言えば、 /sys/bus/pci/devices 以下と、 /lib/modules/`uname -r`/modules.pcimap を照らし合わせて 解析していくのが定石でした。 USBにも対応しようとすると、もう一つ大変です。
しかしこれからの常識は、 /sys/bus/*/devices/*/modalias と
/lib/modules/`uname -r`/modules.alias です。古橋貞之の日記「20行できる高精度ハードウェア自動認識」から引用
すばらしい。 確かに modules.alias を使う方が、 簡単かつ確実に必要なモジュールを読み込むことができそう。 さっそくこの方法を使って initramfs の init スクリプトを書き直してみた。
tmp=/tmp/dev2mod
echo 'dev2mod(){ while read dev; do case $dev in' > $tmp
sort -r /lib/modules/`uname -r`/modules.alias \
| sed -n 's/^alias *\([^ ]*\) *\(.*\)/\1)modprobe \2;;/p' >> $tmp
echo 'esac; done; }' >> $tmp
. $tmp
rm $tmp
cat /sys/bus/*/devices/*/modalias | dev2mod
わずかに 8行 (^^)
(9/30追記: modules.alias を逆順ソートしておく必要があることが判明、sort -r を追加)。
シェルスクリプト版はRuby版と比べて40倍くらい遅いので注意。同ページ(古橋貞之の日記)から続けて引用
sh スクリプトの名誉のために言っておくと、 私が書いた上記 sh スクリプトだと、 古橋さんの Ruby 版と比べて 4倍くらいの遅さで済んでいる。
% time ./dev2mod ide_cd intel_agp intelfb uhci_hcd ...(中略)... libusual usbcore 0.252u 0.012s 0:00.77 33.7% 0+0k 0+0io 0pf+0w % time ./detect_kmod.rb ["ivtv", "snd_intel8x0", "intelfb", "libusual", "ftdi_sio", "usbhid", "uhci_hcd", "ehci_hcd", "usbcore", "via_velocity", "eepro100", "e100", "3c59x", "psmouse", "ide_cd", "i2c_i801", "hw_random", "intel_agp"] 0.072u 0.008s 0:00.18 38.8% 0+0k 0+0io 0pf+0w
ちなみに古橋さんのスクリプトは、
modules.alias の各行それぞれに対し、
マッチするデバイスが /sys/bus/*/devices/*/modalias に存在すれば、
そのモジュールを読み込む処理になっている。
しかしながら、これだと一つのデバイスに対し、
複数のモジュールが読み込まれてしまうことになるのではないだろうか?
古橋さんが同日追記されているように、 複数のモジュールが読み込まれること自体は簡単に修正可能で、 むしろモジュールの読み込み順が modules.alias に載っている順になることのほうが問題。 この問題点を解決するため、 /sys/bus/*/devices/*/modalias の各行それぞれに対し、 マッチするモジュールを modules.alias から見つける修正版が追記された。 さすが古橋さん、すばやい。9/30追記
例えば古橋さんのスクリプトだと、 私の手元のマシンでは e100 と eepro100 の両方のモジュールが読み込まれてしまう。 つまり、
% cat /sys/bus/pci/devices/0000:01:08.0/modalias pci:v00008086d00001050sv0000107Bsd00004043bc02sc00i00
が、modules.alias の次の二つの行にマッチするため、 このようなことが起こる。
alias pci:v00008086d00001050sv*sd*bc*sc*i* eepro100 alias pci:v00008086d00001050sv*sd*bc02sc00i* e100
modules.alias を検索する際は、 マッチする行が見つかった時点で以降の行はスキップしないと、 この例のように複数のモジュール読み込みが起きる恐れがある。 マッチした以降の行を読み飛ばすには、 私が書いた上記 sh スクリプトのように、 /sys/bus/*/devices/*/modalias の各行それぞれに対し、 マッチするモジュールを一つだけ modules.alias から見つけて読み込む処理のほうが、 簡単に書けるのではないかと思うがどうだろうか。
とはいえ、実際の NIC は Intel Pro 10/100 だったりする (^^;) ので、 読み込むべきモジュールは e100 であるような気もする。 もし e100 が正しいモジュールであるのなら、 modules.alias における eepro100 のパターンが適切ではないということになるのかも。9/30追記「*」を多く含むパターンは「後で」マッチさせたほうが、 より適切なモジュールを選択できると考えられるため、 modules.alias を逆順ソートしておくことにした。 これにより、eepro100 ではなく、e100 を読み込むようになった。9/30さらに追記
参考までに initramfs の /init スクリプト全体を添付しておく:
続きを読む要約すれば、 「chrootなんて簡単に抜けられるからセキュリティ目的で使っても意味ないよ。」 ってことね。そうだったのか。「 セキュリティ目的のchrootは意味がない」から引用
そうだったのか orz
Note that this call does not change the current working directory, so that `.' can be outside the tree rooted at `/'. In particular, the super-user can escape from a `chroot jail' by doing `mkdir foo; chroot foo; cd ..'.chroot(2) から引用
chroot するときは、そのディレクトリへ chdir しておくのが常識と 思っていたので気づいていなかった。 つまり、 故意にカレントディレクトリを chroot 外へもっていけば、 chroot されたディレクトリから抜け出せてしまう、ということ。
より正確に言えば、 chroot されたディレクトリの中で、 さらに chroot すれば、 その「親」chroot ディレクトリを抜け出せてしまう。 chroot がネストしないことを利用したテクニック、ということか。 逆に言えば、 chroot(2) 実行時にカレントディレクトリを chroot ディレクトリ下へ 強制的に移動させるか、 あるいは chroot がネストするようにすれば回避可能?
mkdir foo; chroot foo; cd ..
確かに本質はこの短いコードで言い尽くされているが、 こーいうのを見ると実地に試さずにはおれないので、コードを書いてみた。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#define BUFMAX 256
int main(int argc, char *argv[]) {
char buf[BUFMAX+1];
sprintf(buf, "escape.%d", getpid());
if (chdir("/") < 0) {
printf("Can't chdir \"/\" errno=%d\n", errno);
return 1;
}
if (mkdir(buf, 0755) < 0) {
printf("Can't mkdir \"%s\" errno=%d\n", buf, errno);
return 1;
}
if (chroot(buf) < 0) {
printf("Can't chroot \"%s\" errno=%d\n", buf, errno);
return 1;
}
if (rmdir(buf) < 0) {
printf("Can't rmdir \"%s\" errno=%d\n", buf, errno);
return 1;
}
if (!getcwd(buf, BUFMAX)) {
printf("Can't getcwd errno=%d\n", errno);
return 1;
}
printf("Now escaping from chrooted %s\n", buf);
do {
if (chdir("..") < 0) {
printf("Can't chdir \"..\" cwd=%s errno=%d\n", buf, errno);
return 1;
}
if (!getcwd(buf, BUFMAX)) {
printf("Can't getcwd errno=%d\n", errno);
return 1;
}
} while (buf[1] != '\0' && buf[0] == '/');
if (chroot(".") < 0) {
printf("Can't chroot \".\" errno=%d\n", errno);
return 1;
}
argv++;
execv(argv[0], argv);
printf("Can't exec %s err=%d\n", argv[0], errno);
return 0;
}
chdir / して、mkdir foo して、chroot foo して (rmdir foo して)、 その後に chdir .. でディレクトリ階層を上がれば抜け出せる。 言葉で書けば簡単だが、実際のコードを書こうとすると、 もう少し考えるべきことがあった。
すなわち、 抜け出した後、プログラムを終了してしまっては元の木阿弥であるので、 /bin/sh などを exec すべきであるし、 「本物」の / 下の /bin/sh をちゃんと実行するには、 「本物」の / へ chroot しなおす必要もある。 ここで注意すべきなのは、 「/」ディレクトリは元の chrooted なディレクトリのままという点だろう。 つまり chroot / してしまうと、 元の chrooted なディレクトリへ chroot してしまう (つまり何も変わらない)。
だから「/」を使わずに、 「chdir ..」で一段ずつディレクトリ階層を上っていって 「本物」の / にたどり着かねばならない。 上記コード中の while ループが「一段ずつ上っていく」処理である。 「本物」の / にたどりついたら chroot . する (くどいようだがここで chroot / してはいけない)。
試しに脱出してみる:
ikeda:/ # chroot /tmp/chroot /bin/sh # ls -laR / /: drwxr-xr-x 3 0 0 29 Sep 28 17:05 . drwxr-xr-x 3 0 0 29 Sep 28 17:05 .. drwxr-xr-x 2 0 0 38 Sep 28 16:21 bin -rwxr-xr-x 1 0 0 2111689 Sep 28 17:02 escape /bin: drwxr-xr-x 2 0 0 38 Sep 28 16:21 . drwxr-xr-x 3 0 0 29 Sep 28 17:05 .. -rwxr-xr-x 1 0 0 1392832 Sep 1 11:24 busybox lrwxrwxrwx 1 0 0 7 Sep 28 16:21 ls -> busybox lrwxrwxrwx 1 0 0 7 Sep 28 16:21 sh -> busybox # ./escape /bin/sh Now escaping from chrooted /tmp/chroot sh-3.00# ls -la /tmp/chroot total 2068 drwxr-xr-x 3 root root 29 Sep 28 17:05 . drwxrwxrwt 8 root root 4096 Sep 28 17:05 .. drwxr-xr-x 2 root root 38 Sep 28 16:21 bin -rwxr-xr-x 1 root root 2111689 Sep 28 17:02 escape sh-3.00#
Linux 2.6.22 以前は、 シンボリックリンク (symbolic link) のタイムスタンプ (time stamp) を 変更することが出来なかった。 Linux (に限らず unix のほとんど全て) では、 シンボリックリンクも普通のファイルと同様、 アクセス日時と更新日時のタイムスタンプを保持している。 BSD な unix などでは、 lutimes システムコールを使ってシンボリックリンクのタイムスタンプを変更できる。
ところが、Linux には lutimes システムコールが存在しない。 したがってシンボリックリンクのタイムスタンプは、 そのシンボリックリンクを作成した時刻のままである。 tar などでアーカイブからリストアする場合や、 rsync などでディレクトリをコピーする場合などでも、 シンボリックリンクだけは元のタイムスタンプが復元されず、 復元した時刻のシンボリックリンクが作成されるので、 不便なことこのうえない。
Linux 2.6.22 になって、 utimensat システムコールが新設された。
Ulrich Drepper (glibc のメンテナ) は、 futimesat インターフェイスでは機能が足りていないということを理由に、 utimensat システムコールを提案しました。
futimesat は、inode のアクセス・変更時間を設定するためのシステムコールです。 struct timeval をパラメータとして受け取るため、 マイクロ秒単位で設定します。 一方、inode の各種情報を取得する stat というシステムコールでは、 ナノ秒単位で取得できるようになっています。 つまり、そもそも設定できない精度で情報を取得できるような仕組みに なっているわけです。
この問題を解決するために、 パラメータとして struct timespec (ナノ秒単位) を利用できる utimensat というシステムコールを用意することになりました。 このシステムコールは Linux カーネル 2.6.22-rc1 でマージされました。
この記事では、 ナノ秒単位で設定できることばかり強調しているが (私の感覚だとマイクロ秒がナノ秒になっても、あまり嬉しくない ;-)、 utimensat で機能拡張されたのはそれだけではない。
int utimensat(unsigned int dfd, char *filename, struct timespec *t, int flags);
引数が「struct timespec」になってナノ秒単位で設定できるようになったわけだが、 「utimensat」という名称から推測される通り、 他の *at (末尾に「at」がつく) システムコールと同様、 引数として与えたパス名 (char *filename) の扱いを指定できる。 /usr/include/fcntl.h には次のように書いてある。
#ifdef __USE_ATFILE # define AT_FDCWD -100 /* Special value used to indicate the *at functions should use the current working directory. */ # define AT_SYMLINK_NOFOLLOW 0x100 /* Do not follow symbolic links. */ # define AT_REMOVEDIR 0x200 /* Remove directory instead of unlinking file. */ # define AT_SYMLINK_FOLLOW 0x400 /* Follow symbolic links. */ # define AT_EACCESS 0x200 /* Test access permitted for effective IDs, not real IDs. */ #endif
utimensat の第四引数 int flags に「AT_SYMLINK_NOFOLLOW」を与えれば、 シンボリックリンクを「たどらず」に、 シンボリックリンクそのものに対して、 タイムスタンプの変更を行なうことができそうである (ちなみに futimesat は第三引数までしかなく int flags を指定できない)。
さっそく実験してみる:
int utimensat(int dfd, char *filename,
struct timespec *utimes, int flags) {
register unsigned int ret;
asm volatile (
"movl %1, %%eax\n\t"
"call *%%gs:%P2\n\t"
: "=a" (ret)
: "i" (320), "i" (16),
"b" (dfd), "c" (filename), "d" (utimes), "S" (flags)
: "memory", "cc");
return (long)ret;
}
手元の Linux マシンの glibc では、 utimensat を呼び出せるようにはなっていなかったので、 glibc のソースを参考にしながら utimensat システムコールを呼び出す関数を書いてみた。 コード中「320」などとハードコーディング (^^;) している数値は、 linux/include/asm-i386/unistd.h 中の、
#define __NR_utimensat 320
を意味している。 これで utimensat システムコールをユーザ空間から呼び出せるようになった。 もちろん、utimensat をサポートしている glibc であれば、 このような関数をデッチあげるまでもなく、 そのまま普通に utimensat を呼び出せばよい。
さっそく使ってみる:
#include <stdio.h>
#include <stdlib.h>
#define __USE_ATFILE
#include <fcntl.h>
#undef __USE_MISC
#include <sys/stat.h>
int main(int argc, char *argv[]) {
int i;
for (i=1; i < argc; i++) {
struct stat lst, st;
if (lstat(argv[i], &lst)) {
printf("Can't find: %s\nUsage: %s <file>...\n", argv[i], argv[0]);
exit(1);
}
if (S_ISLNK(lst.st_mode) && !stat(argv[i], &st)) {
struct timespec ts[2];
ts[0].tv_sec = st.st_atime;
ts[0].tv_nsec = st.st_atimensec;
ts[1].tv_sec = st.st_mtime;
ts[1].tv_nsec = st.st_mtimensec;
if (utimensat(AT_FDCWD, argv[i], ts, AT_SYMLINK_NOFOLLOW) < 0) {
printf("Failed to utimensat %s %ld.%09ld\n",
argv[i], ts[1].tv_sec, ts[1].tv_nsec);
}
}
}
return 0;
}
シンボリックリンクのタイムスタンプを、
シンボリックリンク先のファイル (or ディレクトリ、その他) のタイムスタンプと
一致させるプログラムである。
上記ソースプログラム (utimensat 関数と main 関数) を
ltouch.c という名前で保存し、
「gcc -o ltouch ltouch.c」などとコンパイルした後、
「ltouch シンボリックリンクのパス名」などと実行する。
senri # ls -lt /usr/i486-linuxaout/lib/ total 20000 lrwxrwxrwx 1 root root 16 Dec 22 2005 libPEX5.so.1 -> libPEX5.so.1.1.0* lrwxrwxrwx 1 root root 14 Dec 22 2005 libPEX5.so.6 -> libPEX5.so.6.0* lrwxrwxrwx 1 root root 15 Dec 22 2005 libX11.so.3 -> libX11.so.3.1.0* lrwxrwxrwx 1 root root 13 Dec 22 2005 libX11.so.6 -> libX11.so.6.0* lrwxrwxrwx 1 root root 13 Dec 22 2005 libXIE.so.6 -> libXIE.so.6.0* lrwxrwxrwx 1 root root 15 Dec 22 2005 libXaw.so.3 -> libXaw.so.3.1.0* lrwxrwxrwx 1 root root 13 Dec 22 2005 libXaw.so.6 -> libXaw.so.6.0* lrwxrwxrwx 1 root root 15 Dec 22 2005 libXpm.so.3 -> libXpm.so.3.3.0* lrwxrwxrwx 1 root root 13 Dec 22 2005 libXpm.so.4 -> libXpm.so.4.3* lrwxrwxrwx 1 root root 14 Dec 22 2005 libXt.so.3 -> libXt.so.3.1.0* lrwxrwxrwx 1 root root 12 Dec 22 2005 libXt.so.6 -> libXt.so.6.0* lrwxrwxrwx 1 root root 18 Dec 22 2005 libcurses.so.0 -> libcurses.so.0.1.2* lrwxrwxrwx 1 root root 15 Dec 22 2005 libdb.so.1 -> libdb.so.1.85.1* lrwxrwxrwx 1 root root 10 Dec 22 2005 libdbm.sa -> libgdbm.sa lrwxrwxrwx 1 root root 16 Dec 22 2005 libdosemu -> libdosemu-0.60.1 lrwxrwxrwx 1 root root 14 Dec 22 2005 libe2fs.so.1 -> libe2fs.so.1.0* lrwxrwxrwx 1 root root 13 Dec 22 2005 libe2p.so.1 -> libe2p.so.1.0* lrwxrwxrwx 1 root root 12 Dec 22 2005 libet.so.1 -> libet.so.1.0* lrwxrwxrwx 1 root root 12 Dec 22 2005 libgr.so.1 -> libgr.so.1.3* lrwxrwxrwx 1 root root 12 Dec 22 2005 libss.so.1 -> libss.so.1.0* lrwxrwxrwx 1 root root 14 Dec 22 2005 libtcl.so.3 -> libtcl.so.3.1j* lrwxrwxrwx 1 root root 13 Dec 22 2005 libtk.so.3 -> libtk.so.3.1j* lrwxrwxrwx 1 root root 16 Dec 22 2005 libvga.so.1 -> libvga.so.1.0.11* ...(後略)...
古いディレクトリだと、 このように同じタイムスタンプのシンボリックリンクばかり並んでしまって、 見にくいことこの上ない (おそらく 2005年12月22日に、ハードディスクを入れ替えたのだろう) のであるが、 「ltouch *」を実行すると、
senri # ltouch /usr/i486-linuxaout/lib/* senri # ls -lt /usr/i486-linuxaout/lib/ total 20000 lrwxrwxrwx 1 root root 14 Sep 11 1995 libPEX5.so.6 -> libPEX5.so.6.0* -r-xr-xr-x 1 root root 234500 Sep 11 1995 libPEX5.so.6.0* lrwxrwxrwx 1 root root 13 Sep 11 1995 libXIE.so.6 -> libXIE.so.6.0* -r-xr-xr-x 1 root root 58372 Sep 11 1995 libXIE.so.6.0* lrwxrwxrwx 1 root root 13 Sep 11 1995 libXaw.so.6 -> libXaw.so.6.0* -r-xr-xr-x 1 root root 209924 Sep 11 1995 libXaw.so.6.0* lrwxrwxrwx 1 root root 12 Sep 11 1995 libXt.so.6 -> libXt.so.6.0* -r-xr-xr-x 1 root root 320516 Sep 11 1995 libXt.so.6.0* lrwxrwxrwx 1 root root 13 Sep 11 1995 libX11.so.6 -> libX11.so.6.0* -r-xr-xr-x 1 root root 529412 Sep 11 1995 libX11.so.6.0* lrwxrwxrwx 1 root root 16 Jul 29 1995 libdosemu -> libdosemu-0.60.1 -rw-r--r-- 1 root root 630973 Jul 29 1995 libdosemu-0.60.1 -rw-r--r-- 1 root root 28912 Mar 19 1995 libdes.a lrwxrwxrwx 1 root root 14 Feb 28 1995 libe2fs.so.1 -> libe2fs.so.1.0* -rwxr-xr-x 1 root root 84035 Feb 28 1995 libe2fs.so.1.0* lrwxrwxrwx 1 root root 13 Feb 28 1995 libe2p.so.1 -> libe2p.so.1.0* -rwxr-xr-x 1 root root 51633 Feb 28 1995 libe2p.so.1.0* lrwxrwxrwx 1 root root 12 Feb 28 1995 libet.so.1 -> libet.so.1.0* -rwxr-xr-x 1 root root 56437 Feb 28 1995 libet.so.1.0* lrwxrwxrwx 1 root root 12 Feb 28 1995 libss.so.1 -> libss.so.1.0* -rwxr-xr-x 1 root root 63306 Feb 28 1995 libss.so.1.0* lrwxrwxrwx 1 root root 18 Feb 19 1995 libcurses.so.0 -> libcurses.so.0.1.2* -rwxr-xr-x 1 root root 49152 Feb 19 1995 libcurses.so.0.1.2* ...(後略)...
このように、 シンボリックリンクとリンク先ファイルが同じタイムスタンプになるので、 「ls -lt」などと更新日時でソートすれば、 リンクとリンク先が隣り合わせになって見やすくなる。
ディスクレス (diskless) サーバを多数運用しようとしたときネックとなるのが、 NAS (Network Attached Storage) サーバの性能。 多数のディスクレスサーバを賄え、かつ高信頼な NAS サーバとなると、 どうしても高価なものになりがちであり、 NAS サーバ本体の価格もさることながら、 ディスクが壊れたときの交換体制などの保守運用費用も高くつく。
それでも、多数のハードディスク内蔵サーバ (つまり一般的なサーバ) を 運用して各サーバのディスクを日々交換し続ける (運用台数が多くなると、 毎週のようにどこかのディスクが壊れると言っても過言ではない) よりは、 ディスクを一ヶ所の NAS にまとめたほうがまだ安い、 というわけで NAS/SAN へのシフトは今後も進むだろう。 そもそも CPU やメモリなどとハードディスクとでは、 故障率のケタが違うのだから、 両者の台数を同じように増やせば破綻するのは当たり前。 要は、滅多に故障しないものは増やしてもいいが、 普通に故障するものは増やしてはいけない。 サーバの部品で故障する確率が桁違いに高いのはハードディスクだから、 大規模負荷分散環境においてディスクレス化は論理的必然だろう。
ハードディスクの故障率が高いのは可動部品が多いから、 というわけでハードディスクをフラッシュメモリで 置き換えようとする傾向もあるようだ。 確かに高価な NAS サーバを導入するよりは、 各サーバにフラッシュメモリを搭載する方が安上がりである可能性もある (比較的低容量であれば)。 しかしながら、 以下に述べるように NAS サーバを普通の PC サーバで実現できてしまえば、 ディスクレス化のほうが安いのは当たり前である。 サーバの台数が多くなればなるほど、 各サーバにディスク/フラッシュメモリを必要としない ディスクレス方式の方が有利になる。
とはいえ、 PC サーバの価格に慣れてしまうと、 超高価な専用サーバの世界にはもう戻れない。 そこで、 どうすればサーバ群のディスクレス化を、 低コストで行なうことができるか考えてみる。 そもそもなぜ NAS サーバが高価かと言えば、 高パフォーマンス性と高信頼性を兼ね備えようとするから。 多数のディスクレスサーバにストレージサービスを提供するのだから 高パフォーマンス性は譲れない。 となると犠牲にしてよいのは高信頼性ということになる。
信頼できなくてもよい、 つまり時々ディスクが壊れて、 書込んだはずのデータが失われても良いなら、 そこそこ高性能な PC サーバで用が足りてしまうだろう。 壊れた場合に備えて冗長化しておけば、 高信頼ではないものの、無停止性は達成できる。 もちろんマスターサーバが壊れてスレーブサーバに切り替われば、 マスターサーバにしか書込めなかったデータは失われる。
そんな NAS サーバは使えない、と言われてしまいそうであるが (^^;)、 ディスクに書込むデータで消えては困るデータとは何だろうか? 例えば Web サーバなどでは、 永続化が必要なデータは DB サーバへ書込むのが普通で、 それ以外のデータは消えては困るとは必ずしも言えないのではないか? というか消えては困るデータは DB サーバへ書けばいいのである。
さらに考えを一歩進めて、 ディスクに書込むデータは消えてもよい、 と割りきってしまうことができれば、 NAS サーバにデータを書く必要性すらなくなってしまう。 つまり NAS サーバのディスクを読み込み専用 (Read Only 以下 RO と略記) でマウントし、 書き込みはローカルな読み書き可能な (Read Write 以下 RW と略記) RAM ディスクに対して行なう。 NAS サーバは RO だから 内容が同じ NAS サーバを複数台用意して負荷分散させれば、 高パフォーマンスと故障時のフェールオーバを同時に達成できてしまう。 NAS サーバのクラスタリングが難しいのはデータを書込もうとするからであって、 書込む必要がなければ話は一気に単純になる。
もちろん全く何も書込めない NAS サーバというのはナンセンスだろう。 ここで「書く必要がない」と言っているのは、 アプリケーション実行中にアプリケーションの動作に同期して (つまり動作結果を) 「書く」必要性である。 アプリケーションとは非同期な書込み、 例えば何らかのコンテンツを配信する Web サーバを考えたとき、 あらかじめ大量の「コンテンツ」を NAS サーバへ事前に保存しておく場合や、 あるいは「コンテンツ」を定期的に更新する場合は、 (アプリケーションの動作結果とは無関係な書き込みなので) Web アプリケーションが NAS へ書込む必要はない。
むしろ、 大量の「コンテンツ」を NAS サーバに集中することは、 コンテンツ更新が素早く行なえるというメリットとなる。 多数の Web サーバそれぞれにハードディスクを内蔵して 同じコンテンツをコピーしていては、 コンテンツの更新頻度が上がってくると 全 Web サーバの内容を同期させるのが難しくなってくるからだ。
ここで重要なのは、 上記「RO NAS サーバ + RW ローカル RAM ディスク」が、 ディスクレスサーバ上で動くソフトウェア (例えば Web アプリケーションやミドルウェア) から見ると、 普通の RW ローカルディスク (つまり普通に書込み可能なハードディスク) に見えなければならないという点である。 もしソフトウェア側で特別な対応が必要だと、 ソフトウェアの改修コストがかかってしまう。 ハードウェアのコストを下げようとして ソフトウェアのコストが上がってしまっては本末転倒である。 ディスク上のデータが RO な NAS サーバから読み込まれたものであり、 ディスクへ書込んだデータが、実は RAM ディスクに書込んだだけで、 再起動によって消えてしまうものであったとしても、 ソフトウェアから見れば、 普通の RW ハードディスクディスクのように 振る舞わなければならないのである。
このように、
複数のディスク (RO NAS と RW RAM ディスク) を重ねて
一つのディスクとして見せる仕掛けを、
重ね合わせ可能な統合ファイルシステム (Stackable Unification File System)
と呼ぶ。
Linux 2.6.20 以降の場合、
二種類の統合ファイルシステムが利用可能である。
後発の Aufs (Another Unionfs) を利用して、
ディスクレスサーバを作ってみた。
(あいかわらず) 前フリが長いが (^^;)、ここからが本題である。
linux をブートさせる際、 さまざまな PC に対応させようとすると、 多くのデバイスドライバをカーネルに組み込んでおかねばならない。 つまりルートファイルシステム (root file system, 以下 rootfs と略記) をマウントするまでは、 その rootfs 上にインストールしたモジュール群 (/lib/modules/ の下に置いた *.ko ファイル群) を読めないからだ。 rootfs さえマウントできてしまえば、 あとはいくらでもモジュールを必要に応じて読み込むことができるようになる。 だから、さまざまなハードウェアへの対応といっても、 重要なのは rootfs をマウントするまで、である。
しかしながら、 rootfs をマウントするまでの辛抱といっても、 rootfs をマウントするにはハードディスクを認識しなければならないし、 それには ATA ドライバやら SCSI ドライバやら、 果ては AHCI ドライバなどが、 ハードウェアに応じて必要になる。
個人で管理する PC の全てのハードウェアに対応させるだけなら、 全てのドライバをあらかじめカーネルに読み込んでおくのもアリだろう。 しかし汎用的なディストリビューションなど、 (カーネル再構築を行なわずに) 多くのハードウェアに対応する必要がある場合は、 必要になるかも知れない全てのドライバを、 あらかじめカーネルに読み込んでおくなどということは非現実的である。
ちなみに私は、1993年頃から linux を使っているが、 いまだに当時インストールした slackware を使用し続けている。 もちろん kernel や libc をはじめとして、 ほぼ全てのソフトウェアをアップデートしてしまっているし、 しかも起動スクリプトをはじめとして、 あらゆる設定を好き勝手に書き換えてしまっているので、 インストールしてから 10年以上たった今となっては、 元の slackware の痕跡は全くといっていいほど残っていない。 もはや、私独自のディストリビューションと呼んでしまっても差し支えないだろう。 私が個人的に管理しているマシンには、 全てこの「my distribution」をインストールしている。 そんなわけで、私は「普通の」ディストリビューションを使ったことがない。 initrd がディストリビューションの「常識」となってからも、 私は initrd は使わずに、 自分が管理する PC のハードウェアに合わせてカーネルを再構築して使ってきた。
そこで linux では、 initrd (Initial RAM Disk) という仕掛けが使われてきた。 すなわちハードディスクを rootfs としてマウントする前に、 一時的にマウントする「ミニ」ルート (mini root) である。 このミニルートには、 ハードディスク (あるいは 1CD Linux の場合であれば CD だし、 ネットワークブートする場合であれば NFS サーバ) を rootfs としてマウントするのに必要となる可能性がある モジュール群一式を置いておき、 ハードウェアに応じて必要なモジュールをミニルートから読む。 そして、ハードディスクをマウントして、 / (ルート) をミニルートからハードディスクへ切り替える。
ただ、この initrd は少々扱いが面倒くさい。 initrd は RAM ディスクという「本物の」ブロックデバイスなので、 「本物の」ファイルシステム (例えば ext2) で mkfs しなければならない。 initrd にモジュールを追加しようとすれば、 initrd イメージを (losetup コマンドを使って) ループバックデバイス経由でマウントして内容を書き換えなければならないし、 たくさんのモジュールを追加した結果、 もしファイルシステムが一杯にでもなったりしたら、 initrd イメージのサイズを大きくして mkfs からやり直しである。
メンドクサイだけでなく、 RAM ディスク自体が非効率なものであるようで、 ファイルからブロックデバイスを作る方法としては、 すでに「semi-obsolete」とまで言われてしまっているようである:
Another reason ramdisks are semi-obsolete is that the introduction of loopback devices offered a more flexible and convenient way to create synthetic block devices, now from files instead of from chunks of memory. See losetup (8) for details.linux/Documentation/filesystems/ramfs-rootfs-initramfs.txt から引用
というわけで、initrd に代わる仕掛けとして、 linux kernel 2.6 からは initramfs と呼ばれる仕掛けが導入された。 すなわち RAM ディスクというブロックデバイスを用いるのではなく、 RAM 上に直接ファイルシステムを作る ramfs を用いた「ミニルート」である。 私自身は今まで initrd を使っていなかったのであるが、 cpio アーカイブを作るだけでいいというのは、 とても手軽であるように思えたし、 カーネルにどんどんドライバを組み込んで肥大化させるよりは、 initramfs を使う方がヨサゲである (もちろん、どんどんドライバを組み込めば、 initramfs が肥大化するのだが、 カーネルが肥大化するデメリットとは比較にならない) ように感じてきたので、 宗旨替えすることにした。
| initrd | initramfs | |
|---|---|---|
| イメージ | ファイルシステム (ext2など + gzip) | アーカイブ (cpio + gzip) |
| 実装 | ブロックデバイス (RAM ディスク) | ファイルシステム |
| 実行 | /linuxrc | /init |
| rootfs マウント |
適当なディレクトリへマウントして pivot_root |
/ へマウント (switch_root) |
| init 起動 | /linuxrc 終了後、カーネルが起動 | /init が exec /sbin/init する |
ブートパラメータとして「initrd=」を与えると、 ブートローダがイメージをメモリ上に読み込んでカーネルに渡す。 するとカーネルはそのイメージがファイルシステムなのか、 cpio アーカイブなのか調べる。 もしファイルの magic number が cpio であれば、 ramfs としてマウントする。 そして /init が実行可能ならば、 initramfs として扱い、 /init を起動する。
以上の条件が一つでも成立しない場合、
すなわち cpio アーカイブでない場合や、
/init が実行できない (/init が存在しない) 場合は、
initrd 扱いになるので注意が必要である。
すなわち RAM ディスクとしてマウントしようとするので、
カーネルに RAM ディスクドライバが組み込まれていなかったり、
「root=/dev/ram0」カーネルパラメータを指定していなかったりすると、
kernel panic を起こす。
実は、initramfs として起動できるようになるまで、 かなりハマってしまった。 まず、cpio アーカイブを作るところで、いきなりハマった。
(cd /usr/src/initramfs/; find . | cpio -o -H newc ) | gzip > initrd.gz
などとしてアーカイブを作ればいいだけの話なのであるが、 このコマンドラインを /bin/csh 上で実行したために、 アーカイブの先頭にゴミが入ってしまった。 つまり、
senri:/ % (cd /usr/src/initramfs/; find . | cpio -o -H newc ) | cpio -tv | head cpio: Malformed number 0000000. cpio: Malformed number 000000. cpio: Malformed number 00000. cpio: Malformed number 0000. cpio: Malformed number 000. cpio: Malformed number 00. cpio: Malformed number 0. cpio: Malformed number . cpio: warning: skipped 56 bytes of junk drwxr-xr-x 13 root root 0 Aug 27 17:56 . drwxr-sr-x 2 root root 0 Aug 25 10:00 bin -rwxr-xr-x 1 root root 1392832 Aug 25 18:36 bin/busybox lrwxrwxrwx 1 root root 7 Aug 25 10:09 bin/addgroup -> busybox lrwxrwxrwx 1 root root 7 Aug 25 10:09 bin/adduser -> busybox lrwxrwxrwx 1 root root 7 Aug 25 10:09 bin/ash -> busybox lrwxrwxrwx 1 root root 7 Aug 25 10:09 bin/cat -> busybox lrwxrwxrwx 1 root root 7 Aug 25 10:09 bin/catv -> busybox lrwxrwxrwx 1 root root 7 Aug 25 10:09 bin/chattr -> busybox lrwxrwxrwx 1 root root 7 Aug 25 10:09 bin/chgrp -> busybox
何が起きているかお分かりだろうか? お恥ずかしながら、アーカイブにゴミが混入していると気づくまで、 何度も kernel を panic させてしまった。 シェルのカスタマイズをやりすぎるとロクなことにならない、 という典型例なのかも (^^;)。
senri:/ % (cd /usr/src/initramfs/; echo test) | od -t a
0000000 esc E m A c S c d sp / u s r / s r
0000020 c / i n i t r a m f s nl esc E m A
0000040 c S c d sp / u s r / s r c / i n
0000060 i t r a m f s nl t e s t nl
0000075
senri:/ % alias cd
set back="$cwd";chdir !*;if(!* =~ "..")set cwd="$back:h";chdir "$cwd";setProm
senri:/ % alias setProm
set prompt="${HOST}:${cwd} $prompt_tail_char "
この alias 設定は、 もうかれこれ 10年以上使い続けてきた設定。 こんな形で悪さをするとは... orz
ようやくマトモなアーカイブを作れたと思ったら、 今度は以下のような Kernel panic が起きた:
Unpacking initramfs... done Freeing initrd memory: 1412k freed ...(中略)... No filesystem could mount root, tried: Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(8,1)
「Unpacking initramfs... done」と出ているのだから、 cpio アーカイブはちゃんと展開できて ramfs としてマウントできているはず。 なぜに「Unable to mount root fs」なのか、 と思っていたのだが、 これは initramfs に「/init」が無いためだった (エラーメッセージが不親切杉!)。 initrd みたいなものだろうと思って、 起動スクリプトを「/linuxrc」というファイル名で作っていたのが敗因。
「/init」がないと initrd 扱いになってしまい、 RAM ディスクを mount しようとしていたが、 RAM ディスクドライバが組み込まれていなかったのでマウントできない、 というのが、このエラーメッセージの主旨だったようだ (素直に /init が見つからない、って言ってくれればいいのに...)。 「/linuxrc」を「/init」へファイル名変更してみると、 あっさり initramfs 上での起動に成功した。
% ls -lt /boot/*-2.6.20.* -rw-r--r-- 1 root root 1643881 Sep 3 07:55 /boot/initz-2.6.20.18 -rw-r--r-- 1 root root 1193392 Sep 1 23:10 /boot/linuz-2.6.20.18 -rw-r--r-- 1 root root 2017936 May 12 19:31 /boot/linuz-2.6.20.11
2.6.20.11 を make したときは、 rootfs のマウントに必要なドライバを全てカーネルに詰め込んでいたのに対し、 2.6.20.18 では、 マウントに必要なドライバは極力 initramfs に入れた。 これにより linuz 単体のサイズは半分近くに減っている。 linuz (カーネル) + initz (initramfs) の合計サイズは 2.6.20.11 に比べ増えてしまっているが、 これは busybox だけで 1.3MB ほどあるため。 とはいえ、 initramfs 内では busybox よりも lib/modules 以下のサイズの方が倍ほど大きいので、 非効率というほどでもない。
senri:/usr/src/initramfs % ls -l bin/busybox -rwxr-xr-x 1 root root 1392832 Sep 1 11:24 bin/busybox senri:/usr/src/initramfs % du --max-depth=2 --byte 1397502 ./bin 4740 ./sbin 6204 ./usr/bin 4334 ./usr/sbin 8226 ./usr/share 22860 ./usr 4096 ./dev/pts 4096 ./dev/shm 12338 ./dev 4108 ./etc 4096 ./mnt 2422679 ./lib/modules 2426775 ./lib 4096 ./proc 4096 ./tmp 4096 ./var 4096 ./sys 3895272 .
じゃ、いよいよハードウェアの自動認識をして、 適切なモジュールのみを組み込むようにしてみようかと思って、 いろいろ探してみたのだが、 どうしたことか適当なスクリプトが見当たらない。 1CD Linux の /linuxrc をいろいろ読んでみたのだが、 いまいちパッとするものがない。 デバイスの ID などがゴリゴリ書いてあるものが大半で、 どれもアドホックすぎるように思えたのである。
かといって、ハードウェアの認識を udev などに行なわせる、 というのは牛刀過ぎるように思えた。 なんたって init を起動する前のブートストラップなのである。 目的は rootfs をマウントするだけなのであるから、 あまりに汎用的な仕掛けは、いかがなものかと思うのである。
というわけで、 busybox だけでハードウェアの自動認識 & モジュール読み込みを実現することを 目標にしてみた。 前フリが長くなった (長すぎ!) が、ようやくここからが本題である。
続きを読む