みつきんのメモ

組み込みエンジニアです。Interface誌で「Yocto Projectではじめる 組み込みLinux開発入門」連載中

kirkstone以前の環境でOverlayFSを簡単に使う方法を考える

はじめに

前回はkirkstoneでリードオンリーなファイルシステムを作成して、OverlayFSで書き込み可能な領域を追加する方法を紹介した。

kirkstoneではoverlayfs.bbclassoverlayfs-etc.bbclassを使用することができるが、 それらは以前のバージョンのYoctoProjectでは存在しないため使用できない。

OverlayFSで書き込み領域を追加する簡単な方法はないだろうか。すこし検討してみる。

環境構築

作業環境

$ mkdir -p ~/yocto/rpi-hardknott
$ cd ~/yocto/rpi-hardknott

pokyの取得

$ git clone -b hardknott git://git.yoctoproject.org/poky.git

環境変数の設定

$ source poky/oe-init-build-env

レイヤの取得

$ bitbake-layers layerindex-fetch meta-raspberrypi

local.confを編集

conf/local.confに下記の部分を追加

MACHINE = "raspberrypi4-64"
DL_DIR = "${TOPDIR}/../downloads"

# systemd
DISTRO_FEATURES:append = " systemd"
VIRTUAL-RUNTIME_init_manager = "systemd"
DISTRO_FEATURES_BACKFILL_CONSIDERED = "sysvinit"
VIRTUAL-RUNTIME_initscripts = ""

# enable uart
ENABLE_UART = "1"

# read only root filesystem.
EXTRA_IMAGE_FEATURES += "read-only-rootfs"

今回もsystemdの使用を前提とする。

書き込みパーティションの追加

wksの作成

既存のwksファイルをベースに新しくwksを作成し書き込み可能なパーティションを追加する。

$ bitbake-layers create-layer meta-work
$ bitbake-layers add-layer meta-work
$ mkdir meta-work/wic

meta-work/wic/sdimg-raspberrypi-overlay.wksを次の内容で作成する。

part /boot --source bootimg-partition --ondisk mmcblk0 --fstype=vfat --label boot --active --align 4096 --size 20
part / --source rootfs --ondisk mmcblk0 --fstype=ext4 --label root --align 4096 --size 2048
part /data --ondisk mmcblk0 --fstype=ext4 --label data --fixed-size 20

local.confに下記を追加

WKS_FILE="sdimg-raspberrypi-overlay.wks"

マウントポイントの作成

$ recipetool newappend meta-work core-image-base

作成されたmeta-work/recipes-core/images/core-image-base.bbappendに下記の内容を記述する。

create_data_dir() {
   mkdir -p ${IMAGE_ROOTFS}/data
}

ROOTFS_PREPROCESS_COMMAND += "create_data_dir;"

揮発しても良いデータ

/home/rootに対して、揮発しても良い領域を割り当てたい場合、local.confに下記のように記述する。

VOLATILE_BINDS_append = " \
    /var/volatile/root /home/root\n\
"

/home/rootは書き込み可能になるが、実際の書き込み先として/var/volatile/rootが割り当てられるため、 /home/rootに書き込んだデータはターゲットの電源がオフになると消える。

これは、下記のように/var/volatileがtmpfsの領域だから。

tmpfs on /var/volatile type tmpfs (rw,relatime)

以前にも記載したが、注意点として、local.confでVOLATILE_BINDSを書き換えただけではvolatile-bindsパッケージは再ビルドされないので、下記のように明示的にビルドし直す必要がある。

$ bitbake volatile-binds -c clean
$ bitbake core-image-base

volatile-bindsの実装

poky/meta/recipes-core/volatile-binds/volatile-binds.bbで、VOLATILE_BINDSは下記のように、定義されている。

VOLATILE_BINDS ?= "\
    /var/volatile/lib /var/lib\n\
    /var/volatile/cache /var/cache\n\
    /var/volatile/spool /var/spool\n\
    /var/volatile/srv /srv\n\
"
VOLATILE_BINDS[type] = "list"
VOLATILE_BINDS[separator] = "\n"

VOLATILE_BINDSに登録されたパスの分だけ、systemdのユニットファイルとして登録している。

def volatile_systemd_services(d):
    services = []
    for line in oe.data.typed_value("VOLATILE_BINDS", d):
        if not line:
            continue
        what, where = line.split(None, 1)
        services.append("%s.service" % what[1:].replace("/", "-"))
    return " ".join(services)

SYSTEMD_SERVICE_${PN} = "${@volatile_systemd_services(d)}"

実際にユニットファイルを作成するのは下記。

do_compile () {
    while read spec mountpoint; do
        if [ -z "$spec" ]; then
            continue
        fi

        servicefile="${spec#/}"
        servicefile="$(echo "$servicefile" | tr / -).service"
        sed -e "s#@what@#$spec#g; s#@where@#$mountpoint#g" \
            -e "s#@whatparent@#${spec%/*}#g; s#@whereparent@#${mountpoint%/*}#g" \
            volatile-binds.service.in >$servicefile
    done <<END
${@d.getVar('VOLATILE_BINDS').replace("\\n", "\n")}
END

作成されたユニットファイルは下記のようになっている。

[Unit]
Description=Bind mount volatile /home/root
DefaultDependencies=false
Before=local-fs.target
RequiresMountsFor=/var/volatile /home
ConditionPathIsReadWrite=/var/volatile
ConditionPathExists=/home/root
ConditionPathIsReadWrite=!/home/root

[Service]
Type=oneshot
RemainAfterExit=Yes
TimeoutSec=0
ExecStart=/sbin/mount-copybind /var/volatile/root /home/root
ExecStop=/bin/umount /home/root

[Install]
WantedBy=local-fs.target

ターゲット上でに実行されるのは下記の行

ExecStart=/sbin/mount-copybind /var/volatile/root /home/root

mount-copybindはシェルスクリプトで、下記のように一度OverlayFSでマウントを試みて、 失敗した場合はmount -o bindでマウントする様になっている。この時、マウントポイント内の既存データをbind先にコピーすることによって、擬似的にオーバーレイの様にしている。

... (snip) ...

    # Try to mount using overlay, which is must faster than copying files.
    # If that fails, fall back to slower copy.
    if ! mount -t overlay overlay -olowerdir="$mountpoint",upperdir="$spec",workdir="$overlay_workdir" "$mountpoint" > /dev/null 2>&1; then

        if [ "$specdir_existed" != "yes" ]; then
            cp -aPR "$mountpoint"/. "$spec/"
        fi

        mount -o "bind$options" "$spec" "$mountpoint"
    fi
elif [ -f "$mountpoint" ]; then
    if [ ! -f "$spec" ]; then
        cp -aP "$mountpoint" "$spec"
    fi

    mount -o "bind$options" "$spec" "$mountpoint"
fi

mount-copybind自体はパラメータとして渡されたディレクトリがvolatileかどうかは判断していない。

揮発してほしくないデータ

/home/rootに対して揮発しない領域を割り当てる場合にはlocal.confのVOLATILE_BINDSを下記のようにする。

VOLATILE_BINDS_append = " \
    /data/home/root /home/root\n\
"

/dataはwksで追加した、書き込み可能なパーティションのマウントポイントとなる。 volatile-bindsの本体となるmount-copybindが、割当先がvolatileかどうかを判断しないため、 VOLATILE_BINDSのリストに揮発しない領域を記述することで、揮発しない書き込み可能領域を追加することができる。

注意点

OverlayFSが使用できる場合は、揮発する場合でもしない場合でもVOLATILE_BINDは期待通りに動作するが、 OverlayFSが使用できない場合は問題となる。

mount-copybindは、OverlayFSが使用できない場合は下記のような動作を行う。

例えば/home/root/data/home/rootにbindするケースで、予め/home/rootにデータが存在する場合。例えば/home/root/helloが存在する。

  1. /home/root内の全データを/data/home/rootにコピー
  2. /data/home/rootを/home/rootにバインド

ユーザーはバインド後の/home/rootにデータを書き込む。この時/home/root/helloの内容も書き換える。 そしてターゲットを再起動する。

mount-copybindによってコピーとバインドが行われる。 この時のコピーで、再起動前に書き換えた/data/home/root/hello/home/root/helloで上書きされる。

つまり変更内容が失われてしまうのだ。

これは、名前の通りVOLATILE_BINDSが本来は揮発することを前提とした設計になっているためである。

まとめ

kirkstone以前のバージョンのYoctoProjectで、OverlayFSにより書き込み可能領域を追加するには、 VOLATILE_BINDSを言わば悪用することで実現できる。

VOLATILE_BINDSの実処理であるmount-copybindは処理対象がvolatileであるかどうかは判断しないが、 揮発することを前提して設計されている。OverlayFSが使えないケースでは割当先を不揮発な領域にに指定した場合は問題がある。

今回の目的はOverlayFSの使用が前提となっているため、基本的に問題はないのだが、 OverlayFSが意図せず使用できない場合、わかりにくい不具合となる可能性がある。

このことから、kirkstone以降ではVOLATILE_BINDSの悪用は避け、 overlayfs.bbclassoverlayfs-etc.bbclassを使用するべき。