ソースコード・リテラシーのススメ

第23回ソースコード・リテラシー実践編 2】

前回は本連載で紹介してきたソースコードリテラシーの実践編として、pmountコマンドを組み込む際に遭遇したトラブルとその調査、解決方法を紹介しました。今回は、もうひとつの実例として、最近、P-Plamoの開発時に遭遇したpivot_rootがらみのトラブルの調査、解決の例を紹介しましょう。

P-Plamo とは?

P-Plamoとは、筆者が現在開発を始めているDVDメディアから直接起動するPlamo Linuxの名称です。HDDにインストールする必要がなく、CDやDVD、USBメモリといったメディアから直接起動して利用できるLinuxシステムは、一般にLiveLinuxと呼ばれ、DebianベースのKnoppixやSlackwareベースのSlaxなどが有名です。

Plamo Linuxでも過去にCD-Plamoという名称で CDメディアから起動して使えるシステムを開発したことがありましたが、昨今の多機能、大規模化が進む各種ソフトウェアを700MバイトというCDメディアのサイズに合わせるために取捨選択するのが面倒で、長く放置したままになっていました。

一方、最近では、4.7GBまで使えるDVDメディアの価格も数年前のCDメディア程度にまで下ってきましたし、数Gバイト規模のUSBメモリも気軽に買える値段になってきました。4GバイトくらいあればKDEやGNOMEといったデスクトップ環境まで含めたPlamo Linuxがそのまま入りますし、LiveLinuxで広く使われている圧縮ファイルシステム機能を使えば、pTeXやOpenOfficeまで収めることも可能でしょう。

そこで、Plamo-4.6の開発が一段落したのに合わせて、久しぶりにPlamo LinuxベースのLiveLinuxに取り組んでみたのがP-Plamoです。P-Plamoでは、かってのように700MバイトというCDメディアのサイズには拘泥せず、4.7GバイトのDVDメディアに収まればいい、ただし、あまり節操無くパッケージを追加するのもどうかと思うので、2Gバイト程度のUSBメモリに、自分のファイルと共に書き込める規模を目指す、という方針で開発を始めました。

トラブル発生

一般的なLiveLinuxではCDやDVDメディア上に置いた実際のルートファイルシステムを使う前に、メモリに読み込んだファイルシステム上で必要な初期化処理を行います。この仕組みはPlamo Linuxのインストーラと同じなので、P-Plamoの起動処理の部分はインストーラ同様のinitramfs機能を用いて実装しました。

限られた容量に可能な限りファイルを詰め込むためのファイルシステムの圧縮にはSquashFSを、リードオンリーになるルートファイルシステムに設定ファイルやログファイルを書き込むためにはaufsを用いたunion mountを使うことにしました。いずれの技術もまだ公式のカーネルソースには含まれていませんが、Slax等のLiveLinuxで広く使われており、各種パッチも公開されています。

これらLiveLinuxに必要な機能を提供するモジュールとCD-ROM関連のモジュールを、メモリに読み込んだ初期化用ファイルシステム上でBusyBoxを使ってカーネルに組み込み、DVDのありそうなデバイスを総当り的に探してDVDメディアをマウント、見つかったDVDメディア上のSquashFS化した正式なルートファイルシステムをloopback形式でマウントして、switch_rootでルートファイルシステムを切り替えるという形で、とりあえず動くバージョンをまとめることができました。

とりあえず動くものができたので、P-Plamo 0.1としてメーリングリストで紹介したところ、⁠終了時にDVDをejectして欲しい」というリクエストがありました。確かに現状ではDVDメディアをマウントしたまま電源断してしまうので、DVDを取り出すためには再度電源を入れてejectしないといけないので不便です。

当初は、終了処理の最後にでもejectコマンドを実行すればいいだろう、と簡単に考えていましたが、調べ始めたところ、かなり深刻な問題に気づきました。

P-Plamo 0.1ではswitch_rootでルートファイルシステムを切り替えて、DVDメディア上のSquashFSにある/sbin/initに処理を委ねてしまうため、ランレベルを切り替えて終了処理を実行している際も(DVDメディア上にある)initが動き続けるのでDVDメディアをアンマウントできません。

さてどうしたものか…、と、BusyBoxのejectコマンドをメディアのロックを無視するように改造する方法や、initコマンドの最後でejectを呼ぶように改造する方法なども検討しましたが、ヘンにローカルに改造したBusyBoxを使うのも気持ちよくないし、使用中のメディアを強制排出するのも行儀よくないでしょう。

そのため処理的には多少面倒になりますが、ルートファイルシステムを切り替える際、古いルートファイルシステムを捨ててしまうswitch_rootではなく、別のディレクトリ以下に残しておけるpivot_rootを使い、終了時には古いルートファイルシステムに戻った上でDVDをアンマウトしようとしました。

ところが、switch_rootの代りにpivot_rootを使おうとすると⁠Invalid argument⁠のエラーになり、ルートファイルシステムが切り替わってくれません。

....
CD_DEV:/dev/hda
pivot_root to /loop
pivot_root: Invalid argument
chroot: cannot execute /loop/sbin/init: No such file or directory
Kernel panic - not syncing: Attemted to kill init!

よく似た機能なのに switch_root と pivot_root って何が違うのかな、と調べてみることにしました。

pivot_root のソースコード

今回使っているswitch_rootとpivot_rootは共にBusyboxに含まれているコマンドです。そこでまずBusyBoxのマニュアルを調べてみました。

% less docs/BusyBox.1
...
    pivot_root
        pivot_root NEW_ROOT PUT_OLD
        
        Move the current root file system to PUT_OLD and make
        NEW_ROOT the new root file system
...
    switch_root
        switch_root [-c /dev/console] NEW_ROOT NEW_INIT [ARGUMENTS_TO_INIT]
        
        Use from PID 1 under initramfs to free initramfs,
        chroot to NEW_ROOT, and exec NEW_INIT
        
        Options:

               -c      Redirect console to device on new root
...

BusyBox のマニュアルには上記のような使い方の簡単な記述しかなく、これだけでは手掛りになりません。

仕方ないのでBusyBoxのソースコードを眺めてみました。

% cat -n busybox-1.13.2/util-linux/pivot_root.c
...
 11  #include "libbb.h"
 12  
 13  extern int pivot_root(const char * new_root,const char * put_old);
 14  
 15  int pivot_root_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 16  int pivot_root_main(int argc, char **argv)
 17  {
 18   if (argc != 3)
 19    bb_show_usage();
 20  
 21   if (pivot_root(argv[1], argv[2]) < 0) {
 22    /* prints "pivot_root: " */
 23    bb_perror_nomsg_and_die();
 24   }
 25  
 26   return EXIT_SUCCESS;
 27  }

BusyBoxのpivot_root.cのコードはこれだけで、パラメータ数をチェックしてpivot_rootシステムコールを呼び出しているだけです。

紹介はしませんでしたが、pivot_rootはutil-linuxパッケージにも含まれているLinuxの標準コマンドなので、そちらのマニュアルページも調べたところ「pivot_rootはシステムコールのpivot_rootを呼び出しているだけなので、詳細はシステムコールのpivot_rootを参照せよ」とありました。このソースコードを見る限り、BusyBoxのpivot_rootもシステムコールのpivot_rootを呼び出しているだけのようです。

カーネルソースの調査

システムコールとなるとカーネルソースの中の話になるので簡単には手が出せそうにありませんが、とりあえずpivot_rootがどこで処理されているかを調べるためにソースコードをgrepしてみました。以下に示すfind コマンド中の-followはシンボリックリンクも辿る指示、cat -nは行番号の表示用です。

% find /usr/src/linux -follow  | xargs grep -i pivot_root | cat -n
  1  /usr/src/linux/arch/s390/include/asm/unistd.h:#define __NR_pivot_root         217
  2  /usr/src/linux/arch/s390/kernel/syscalls.S:SYSCALL(sys_pivot_root,sys_pivot_root,sys32_pivot_root_wrapper)
    ...
 88  /usr/src/linux/Documentation/early-userspace/README:   filesystem via linuxrc and use the pivot_root syscall.  The initrd is
 89  /usr/src/linux/Documentation/arm/IXP4xx:   a pivot_root to NFS.
 90  /usr/src/linux/usr/include/asm/unistd_64.h:#define __NR_pivot_root				155
 91  /usr/src/linux/usr/include/asm/unistd_64.h:__SYSCALL(__NR_pivot_root, sys_pivot_root)
 92  /usr/src/linux/usr/include/asm/unistd_32.h:#define __NR_pivot_root		217

各種アークテクチャ(CPU)用のアセンブラコードからドキュメントファイルまで92箇所ほどでpivot_rootへの言及があるようです。これらのファイルのうちx86用のファイルから眺めてみましたが、ヘッダファイルやアセンブラコードはシステムコール番号の定義のみで、実際の処理はありません。

実際の処理はどこだろうな…、としばし悩みましたが、fs/namespace.cに見つかりました。

% cat -n /usr/src/linux/fs/namespace.c
 ...
2157  /*
2158   * pivot_root Semantics:
2159   * Moves the root file system of the current process to the directory put_old,
2160   * makes new_root as the new root file system of the current process, and sets
2161   * root/cwd of all processes which had them on the current root to new_root.
2162   *
2163   * Restrictions:
2164   * The new_root and put_old must be directories, and  must not be on the
2165   * same file  system as the current process root. The put_old  must  be
2166   * underneath new_root,  i.e. adding a non-zero number of /.. to the string
2167   * pointed to by put_old must yield the same directory as new_root. No other
2168   * file system may be mounted on put_old. After all, new_root is a mountpoint.
2169   *
2170   * Also, the current root cannot be on the 'rootfs' (initial ramfs) filesystem.
2171   * See Documentation/filesystems/ramfs-rootfs-initramfs.txt for alternatives
2172   * in this situation.
2173   *
2174   * Notes:
2175   *  - we don't move root/cwd if they are not at the root (reason: if something
2176   *    cared enough to change them, it's probably wrong to force them elsewhere)
2177   *  - it's okay to pick a root that isn't the root of a file system, e.g.
2178   *    /nfs/my_root where /nfs is the mount point. It must be a mountpoint,
2179   *    though, so you may need to say mount --bind /nfs/my_root /nfs/my_root
2180   *    first.
2181   */
2182  SYSCALL_DEFINE2(pivot_root, const char __user *, new_root,
2183    const char __user *, put_old)
2184  {
2185   struct vfsmount *tmp;
2186   struct path new, old, parent_path, root_parent, root;
2187   int error;
2188  
2189   if (!capable(CAP_SYS_ADMIN))
2190    return -EPERM;
2191  

どういう処理をしているのかを見る前に、ざっとコメントに目を通しておこうと眺めたところ、2170行目に⁠the current root cannot be on the 'rootfs' (initial ramfs) filesystem.⁠という記載がありました。

そもそもinitial ramfs(initramfs)をルートファイルシステムにしている状態ではpivot_rootは使えないようです。

Documentation/filesystems/ramfs-rootfs-initramfs.txtを見よ、とあるので、さっそくそのファイルを調べてみました。

% cat -n /usr/src/linux/Documentation/filesystems/ramfs-rootfs-initramfs.txt 
 1  ramfs, rootfs and initramfs
 2  October 17, 2005
 3  Rob Landley 
 4  =============================
 5  
 6  What is ramfs?
 7  --------------
 8  
 9  Ramfs is a very simple filesystem that exports Linux's disk caching
     ...

このファイルは以前、Plamoのインストーラをinitrd形式からinitramfs形式に変更する際に調べたことがありました。その際はinitramfs形式の作り方を中心に読んだのですが、今回は改めて頭から読み直しました。

このファイルでは、まずカーネルのディスクキャッシュの仕組みを用いたramfsというファイルシステムについて紹介し、従来使われていたram diskとの違いを解説しています。新しく実装されたramfsは、カーネルのキャッシュ機能を利用して動的に大きさが変更できるのに対して、従来のram diskではあらかじめメモリ上に決まった容量を割り当てなければならないので無駄が多いこと、ram diskの機能の多くはloopbackデバイスを使えば実現できるので時代遅れになっていることなどが説明されています。

そして、ramfsの特別な例としてのrootfsについて紹介されていました。

 70  What is rootfs?
 71  ---------------
 72  
 73  Rootfs is a special instance of ramfs (or tmpfs, if that's enabled), which is
 74  always present in 2.6 systems.  You can't unmount rootfs for approximately the
 75  same reason you can't kill the init process; rather than having special code
 76  to check for and handle an empty list, it's smaller and simpler for the kernel
 77  to just make sure certain lists can't become empty.
 78  
 79  Most systems just mount another filesystem over rootfs and ignore it.  The
 80  amount of space an empty instance of ramfs takes up is tiny.
 81  

rootfsはramfsで実現されたカーネル内蔵のファイルシステムで、カーネルがファイルシステムを探す際の起点として使うのでアンマウントはできず、カーネルの起動時パラメータで指定するroot=/dev/hda2等のHDD上の実際のルートパーティションは、このrootfsの上にマウントされることでルートファイルシステムと見なされるようです。

もう少し読み進めていくと、initramfsinitrdの違いについても紹介してありました。

114    - When switching another root device, initrd would pivot_root and then
115      umount the ramdisk.  But initramfs is rootfs: you can neither pivot_root
116      rootfs, nor unmount it.  Instead delete everything out of rootfs to
117      free up the space (find -xdev / -exec rm '{}' ';'), overmount rootfs
118      with the new root (cd /newmount; mount --move . /; chroot .), attach
119      stdin/stdout/stderr to the new /dev/console, and exec the new init.

以前、initramfsの作り方を中心に読んだ時にはこのあたりの話は読み飛していたのですが、改めて読み直してみるとinitrdとinitramfsの重要な違いが説明されています。

initrdの場合はメモリ上に割りあてたram diskをrootfs上にマウントしているので、pivot_rootすることで別のファイルシステムをrootfsにマウントし直すことができるのに対し、initramfsの場合はrootfsそのものにcpio+gzipで固めたファイルを書き込んでルートファイルシステムにしているため、データを消去することで使っていた領域を解放することはできるものの、pivot_rootでルートパーティションを切り替えることはできない、ということです。

実のところ、P-Plamoの参考にするためにSlaxの起動の仕組みなどを調べたことがあります。その際、Slaxでは従来のinitrd形式を使っているので、なぜ作りやすいinitramfs形式を使わないのだろう?と疑問を感じたのですが、終了時にCD/DVDメディアを排出するためには、pivot_rootで元のルートファイルシステムに戻る必要があり、そのためには従来のinitrd形式が必須だったようです。

実際にinitrd形式ならpivot_rootできるかどうかを試してみるために、initramfs形式で作っていた起動用ファイルシステムをinitrd形式に変更して試してみました。

# dd if=/dev/zero of=miniroot bs=1M count=4
4+0 records in
4+0 records out
4194304 bytes (4.2 MB) copied, 0.0188857 s, 222 MB/s
# mke2fs -i1024 -m0 miniroot
mke2fs 1.41.4 (27-Jan-2009)
initrd is not a block special device.
Proceed anyway? (y,n) y
...
# mount miniroot /loop -o loop
# ( cd ./initramfs ; tar cvf - * ) | ( cd /loop ; tar xf -)
bin/
bin/busybox
bin/sh
...

# umount /loop 
# gzip -c miniroot > initrd.gz

まずddコマンドでinitrd用に固定サイズのファイルをあらかじめ確保しておきます。この例では4Mバイト分、/dev/zeroから0を書きこんだファイル(miniroot)を作りました。

そのファイルをmke2fsでフォーマットし、/loopにloopback形式でマウントそこにinitramfs用に作っていたファイル一式をtarコマンドを使ってコピーし、念のためumountしてからgzipで固めてinitrd.gzを作りました。

このinitrd.gzに入れ替えて作成したDVDイメージで起動し、pivot_rootの動作を試しました。

図1 pivot_rootを実行して/loopを新しいルートファイルシステムに切り替え
図1 pivot_rootを実行して/loopを新しいルートファイルシステムに切り替え

起動直後はinitrdが/dev/rootになり、DVDメディア上のLiveLinux用のルートファイルシステムは/loop以下に配置されているのに対し、/loop ディレクトリでpivot_root . put_oldした後は、/loop/tmp/tmpになる等、LiveLinux 用のルートファイルシステムが本来の位置に配置され、元のinitrd上のファイルシステムはput_oldディレクトリ以下に移動しています。

この状態で、再度put_old以下のディレクトリに移動してpivot_rootを実行すれば、無事、元のinitrd上のファイルシステムがルートファイルシステムに戻りました。

/ # cd /put_old
/put_old # /sbin/pivot_root . put_old
/ # ls /
bin         etc         loop         proc         shutdown
cdrom       init        lost_found   put_old      sys
dev         lib         new_root     sbin

pivot_rootでルートファイルシステムを行き来できるメドがついたので、最終的な終了処理はinitrd上のshutdownコマンドで行い、そこからejectコマンドを実行するようにして、何とか終了時にDVDメディアを排出できるようになりました。

今回はinitramfsでpivot_rootできない原因をカーネルのソースコードまで辿って調べてみましたが、実際の処理そのものを調べるまでもなく、コメントと付属のドキュメントで原因は判明しました。

前回のpmountもそうでしたが、ソースコードを読む、といっても、アルゴリズムや処理のステップを丁寧に追うまでもなく、デバッグメッセージやコメント行を調べる程度で解決する問題は多数あります。ソースコードや付属ドキュメントを「難しそうだから」⁠英語だから」と敬遠せず、本連載で紹介してきたように、機会を見つけてはチェックするようにすることが、問題解決の勘を養い、初心者から一歩を踏み出す近道になるでしょう。


さて、春は出会いと別れの季節です。約2年ほど続いたこの連載も、カーネルソースまで辿りついた今回で一段落させてもらうことになりました。ご愛読を感謝すると共に、これからも皆さんがソースコードやドキュメントに親しんでいかれることを願って、終了の挨拶とさせていただきます。長い間、ありがとうございました。

おすすめ記事

記事・ニュース一覧