Raspbian OSのSDカードをリードオンリー化する(initrd 編)

投稿者: | 2017/10/15

Another way to run Raspbian OS on read-only file system.

リードオンリーで Raspberry Pi を動かすことにこだわるファームロジックス 横山です。

前ふり

前回の記事で、Raspbian Wheezy をリードオンリー化しましたが、今回は別の方法を試してみました。前回の方法は、Domoticz の Jannl さんが考案したアイデアで、root ファイルシステムをリードオンリーでブートした後、/var と /home だけを overlay ファイルシステムで擬似的に(RAM ディスクを使って)書き込み可能とする、というものです。シャットダウン(リブート)時には /var と/home の内容を書き戻すという、なかなか便利なスクリプトが提供されているのですが、最近試したところ以下の問題に遭遇しました。

  • 2017-04-10 版の Jessie Lite では、再起動時に固まってしまいます。(systemd の処理あたりで固まってしまうのだが、systemd が苦手なので、デバッグする気にならず断念。)
  • 2017-09-07 版の Stretch Lite では動作するのですが、上記の /home を書き戻す機能に難があり、再起動時に /home がカラッポになってしまうことがある、という致命的な問題があります。(いちおう、こちらに報告させて頂きました。どうなることなら。)

という訳で、Jannl さんのアプローチには非常に後ろ髪を引かれるものの、今回は別のアプローチを試してみることにしました。いくつか流派(参考までに以下リスト)があるようですが、いずれも Linux の initrd の機能を使って、ブート時に直接、ルートファイルシステムを overlay 化してしまう、という方法です。

私が試してみたところでは、後者が比較的簡単でうまく行ったので、御紹介してみたいと思います。今回の記事も、「人様の記事」を後追いして日本語で書き下しただけという点で、プロの端くれとしては後ろめたいのですが、参考になる方も多いのではないかと思い、紹介させて頂きます。

実際にやってみる

今回は、Raspbian Stretch Lite を使って試してみたいと思います。上記の ejolson さんの説明は掲示板で議論形式になっているので、ポイントだけを押さえて再現してみたいと思います。

なお、以下では新規にインストールする OS を対象としています。既存のインストールで試す場合は、必ず全てのデータをバックアップしてから作業してください。(場合によっては、起動しなくなる可能性があるかも知れません!)

Raspbian OS のインストール

まずは、Raspbian のインストールです。

つい最近(?)、Wheezy から Jessie となり、systemd に涙を流したばかりのように思いますが、とうとう Raspberry Pi も Stretch になってしまいました。いずれ Raspberry Pi も 「Docker でお気楽デプロイメント」の時代が来るでしょうから(妄想 80%)、ベース OS が何かとか、systemd とか、気にしなくて済むようになるでしょう…。

以下からダウンロードできます。私は今回、2017-09-07 版の Stretch Lite を用いました。

なお、このサイトで最新版の Raspbain しかダウンロードできません。私の作業と同じバージョンを使いたい方は、こちらからダウンロードできます。ただし、なぜか Lite 版は提供されていません。

次に、Raspbian OS のインストール方法はこちらです。

今回、私は Raspberry Pi 3 Model B を使用しました。なお、私は HDMI ディスプレイや USB キーボードを繋ぐのが面倒に思うので、シリアルコンソールを使用します。シリアルコンソールだと、Tera Term などで簡単にコピペができるというメリットがあります。(Mac OS や Linux の方は、screen コマンドを使うとシリアルコンソールにログインできます。)

なお、シリアルコンソールを繋ぐには、こんなケーブルが必要ですが、非常に重宝しますので持っていて損はありません。なお、eBay などで探すともっと安価なものもあります。

最近の Raspbian では、デフォルトではシリアルコンソールが無効になっているので、SD カードをパソコンから外す前に、boot パーティションのほうの config.txt の末尾行に、エディタで enable_uart=1 を追加しておきます。詳しくは、この辺のサイトを参考にしてください。修正すると、ファイルの末尾付近はこんなふうになるはずです。(Raspbian OS のバージョンによって異なるかも。)

# Additional overlays and parameters are documented /boot/overlays/README

# Enable audio (loads snd_bcm2835)
dtparam=audio=on

enable_uart=1

OS のブートと SSH の有効化

シリアルコンソールからブートすると、次のようなメッセージが表示されるので、いつものように、

  • ユーザー名: pi
  • パスワード raspberry

でログインします。こんな感じですね。

Raspbian GNU/Linux 9 raspberrypi ttyS0
raspberrypi login: pi
Password: パスワードは見えない
Linux raspberrypi 4.9.41-v7+ #1023 SMP Tue Aug 8 16:00:15 BST 2017 armv7l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.

pi@raspberrypi:~$

なお、シリアルコンソールから作業しても良いのですが、SSH でログインして作業したい方は、raspi-config で設定しておきましょう。(Tera Term では、TTSSH という機能で SSH を利用できます。)

まず、raspi-config を起動します。

pi@raspberrypi:~$ sudo raspi-config

メニューから Interfacing Options を選び、次に SSH を選びます。最後に Yes を選ぶと、SSH サーバーが有効になります。(重要な注意: SSH サーバーを有効にすると、ルーター等の設定次第では外部から Raspberry Pi にログインできるようになります。SSH の設定を変更して公開鍵認証に制限するか、せめてユーザー pi のパスワードを変更しておきましょう。

HDMI ディスプレイと USB キーボードでログインした方は、ここからは SSH ログインに移行したほうが作業しやすいです。

実際の設定作業

ここからは、ejolson さんの説明に従います。ただし、オリジナルは Raspbain Jessie を対象としているので、以下の説明と異なる部分があると思います。

まず最初に、root 権限(スーパーユーザー)に移行します。

pi@raspberrypi:~$ sudo bash
root@raspberrypi:/home/pi#

次に、ディレクトリを移動します。

root@raspberrypi:/home/pi# cd /usr/share/initramfs-tools

ファイル hook-functions を編集します。私は vi を使いますが、vi の苦手な方は nano が良いでしょう。

root@raspberrypi:/usr/share/initramfs-tools# nano hook-functions

497行目付近に

        for arg in "$@" ; do
                case "$arg" in
                base)   
                        modules="$modules btrfs ext2 ext3 ext4 ext4dev overlay"
                        modules="$modules isofs jfs reiserfs udf xfs"

という部分があると思いますので、上記の赤字の部分を追加してください。

次に、ディレクトリ scripts に移動します。

root@raspberrypi:/usr/share/initramfs-tools# cd scripts
root@raspberrypi:/usr/share/initramfs-tools/scripts#

ファイル local を、overlay というファイルにコピーします。さらに、local-premountlocal-bottom というディレクトリをコピーします。なお、後者のディレクトリ local-bottom は存在しないかも知れません。そのときは無視します。(また、以下の説明からは、プロンプトが長いので ... と略します。)

...# cp local overlay
...# cp -rp local-premount overlay-premount
...# cp -rp local-bottom overlay-bottom (存在せずにエラーになる場合は無視)

そして、ファイル overlay を編集します。

root@raspberrypi:/usr/share/initramfs-tools/scripts# nano overlay

ファイル中の local_mount_root() という関数定義の中を変更します。以下、変更箇所を赤字で示します。

        local_premount

#       if [ "${readonly}" = "y" ]; then
                roflag=-r
#       else
#               roflag=-w
#       fi

        # FIXME This has no error checking
        modprobe ${FSTYPE}

        checkfs ${ROOT} root

        # FIXME This has no error checking
        # Mount root
        mkdir /upper /lower
        if [ "${FSTYPE}" != "unknown" ]; then
            mount ${roflag} -t ${FSTYPE} ${ROOTFLAGS} ${ROOT} /lower
        else
            mount ${roflag} ${ROOTFLAGS} ${ROOT} /lower
        fi
        modprobe overlay
        mount -t tmpfs tmpfs /upper
        mkdir /upper/data /upper/work
        mount -t overlay \
            -olowerdir=/lower,upperdir=/upper/data,workdir=/upper/work \
            overlay ${rootmnt}
}

続いて、initrd(initramfs) を生成するのですが、まずはカーネルのバージョンを確認します。

...# uname -r
4.9.41-v7+

ここで、4.9.41-v7+ のように v7 が付く場合には、以下のままで結構ですが、v7 が付かない場合は、赤字で示した 7 の部分を除いてタイプします。initrd を生成しましょう。

...# update-initramfs -c -k $(uname -r)
...# cd /boot
...# mv initrd.img-$(uname -r) initrd7.img

ファイル /boot/config.txt を編集します。末尾に以下を追加します。こちらも、7 に注意してください。v7 が付かない場合は、赤字で示した 7 の部分を除いてタイプします。(編集するには、例えば sudo nano /boot/config.txt 等とします。)

kernel=kernel7.img
initramfs initrd7.img

最後に、同様にファイル /boot/cmdline.txt を修正します。赤字の部分を追加および修正します。

boot=overlay dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

OS のリブート

これで終了です。OS をリブートしてみましょう。正しくブートできたら、再度ユーザー pi でログインし、以下のコマンド(下線部)を入力してみましょう。

pi@raspberrypi:~ $ mount | fgrep ' / '

その結果、以下のようなメッセージが表示されれば、正しくリードオンリー化できています。

overlay on / type overlay (rw,noatime,lowerdir=/lower,upperdir=/upper/data,workdir=/upper/work)

一方、次のように表示された場合はリードオンリー化できていません。もう一度、作業を確認してみましょう。

/dev/mmcblk0p2 on / type ext4 (rw,noatime,data=ordered)

リードオンリーになっているかどうか確認

それでは、実際にリードオンリーになっているかどうか確認しましょう。そのためには、たとえば以下のようなコマンドを実行しファイルを作成します。その後、リブート(再起動)すると、そのファイルが跡形もなく消えていることが確認できるはずです。

pi@raspberrypi:~ $ echo Hello > ~/poi

いくつかの注意点

このリードオンリー化では、OS の動作中にはファイルを書き込めますが、リブートにより消失します。作成するファイルはどこに書き込まれているのでしょう?

実は、書き込んだファイルは RAM ディスク上に置かれています。しかし、RAM ディスクのサイズは有限なのでは?  その通りです!

ファイルは RAM ディスク上に作られるので、ファイルを作成していくと、RAM はどんどん減っていきます。試しに、大きなファイルを作って確認してみましょう。まず最初に、ファイルを作る前です。

pi@raspberrypi:~ $ vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
0  0      0 649312   8876 262240    0    0    42     0  113   39  1  1 98  1  0
pi@raspberrypi:~ $ df -k /
Filesystem     1K-blocks   Used Available Use% Mounted on
overlay           474784 113328    361456  24% /

vmstat コマンド結果の赤字の部分が、フリーメモリを示しています。また、df コマンド結果の Available は、RAM ディスクの残容量を示しています。(いずれも単位はキロバイトです。)

では、ここで 10M バイトのファイルを作って、再度確認しましょう。

pi@raspberrypi:~ $ dd if=/dev/zero of=dummy10mbytes.dat bs=1024k count=10
10+0 records in
10+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0.043471 s, 241 MB/s
pi@raspberrypi:~ $ vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
0  0      0 638976   8876 272488    0    0    24     0  103   26  0  0 99  0  0
pi@raspberrypi:~ $ df -k /
Filesystem     1K-blocks   Used Available Use% Mounted on
overlay           474784 123568    351216  27% /

フリーメモリとディスク残容量が減っているのがお分かりになりますでしょうか?  RAM ディスクとメモリは同じプールから取得されるので、双方の数字が減っています。(なお、ファイルを消すと元に戻ります。)

このように、ルートオンリー化したシステムでは書き込めるファイルサイズに制限があり、同時に、ファイルが増えるほどフリーメモリも減っていきますので、注意が必要です。

再度、書き込み可能にするには

リードオンリー化したシステムではファイルへの書き込みができません。OS やツールのアップデート(apt-get install など)をするにはどうしたら良いのでしょう?

そのためには、再度 OS を書き込み可能な状態でブートすることになります。コマンド一発の簡単な方法を用意することもできるでしょうが、基本は /boot/cmdline.txt の修正です。同ファイルの先頭にある、boot=overlay を消してリブートすると、元通り書き込み可能な状態になります。再度、boot=overlay を追加すると、またリードオンリーに戻ります。

それでは、今日はここまで。