Raspbianの初期起動でのrootfsパーティション自動拡張の仕組みを調べた
環境
試したRaspbianのイメージは2018-11-13-raspbian-stretch-lite.img
。母艦はLinux PCで調べた。
きっかけ
Raspbian Jessieのいつ頃からか忘れたけど、イメージをSDカードに焼いてRaspberry PIに入れて起動すると、 自動でrootfsのパーティションがSDカードの容量全体まで拡張されるようになった。
RaspbianのOSイメージをSDカードに焼いた直後のパーティションはfdiskで確認すると、次のようになっている。
$ sudo fdisk -l /dev/mmcblk0 Disk /dev/mmcblk0: 3.7 GiB, 3965190144 bytes, 7744512 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x7ee80803 デバイス 起動 Start 最後から セクタ Size Id タイプ /dev/mmcblk0p1 8192 98045 89854 43.9M c W95 FAT32 (LBA) /dev/mmcblk0p2 98304 3645439 3547136 1.7G 83 Linux
これをRaspberry PIに入れて起動すると、自動拡張のメッセージが表示されて、再起動後に次のようになる。
$ sudo fdisk -l /dev/mmcblk0 Disk /dev/mmcblk0: 3.7 GiB, 3965190144 bytes, 7744512 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x7273b820 デバイス 起動 Start 最後から セクタ Size Id タイプ /dev/mmcblk0p1 8192 98045 89854 43.9M c W95 FAT32 (LBA) /dev/mmcblk0p2 98304 7744511 7646208 3.7G 83 Linux
/dev/mmcblk0p2
の容量が1.7Gから3.7Gに広がっているのがわかる。
これはこれで便利なのだけれど、rootfsとは別のパーティション作ったり色々やりたいことがあるので、 そもそもこれはどうやって実現しているのだろうと思って調べてみた。
/boot/cmdline.txt
を調べる
ブート時になにかやっているのだから/boot/cmdline.txt
をとりあえず見てみようと思い、
イメージをSDカードに焼いてLinux PCからマウントしてファイルを覗いてみた。
/boot/cmdline.txt
の中身
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=7ee80803-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet init=/usr/lib/raspi-config/init_resize.sh
末尾に、なんかそのものズバリになりそうなinit=/usr/lib/raspi-config/init_resize.sh
を発見。
/usr/lib/raspi-config/init_resize.sh
を調べる
今度はrootfsをマウントして/usr/lib/raspi-config/init_resize.sh
の中身を覗いてみた。
init_resize.shの中身は次の通り。
#!/bin/sh reboot_pi () { umount /boot mount / -o remount,ro sync if [ "$NOOBS" = "1" ]; then if [ "$NEW_KERNEL" = "1" ]; then reboot -f "$BOOT_PART_NUM" else echo "$BOOT_PART_NUM" > "/sys/module/${BCM_MODULE}/parameters/reboot_part" fi fi echo b > /proc/sysrq-trigger sleep 5 exit 0 } check_commands () { if ! command -v whiptail > /dev/null; then echo "whiptail not found" sleep 5 return 1 fi for COMMAND in grep cut sed parted fdisk findmnt partprobe; do if ! command -v $COMMAND > /dev/null; then FAIL_REASON="$COMMAND not found" return 1 fi done return 0 } check_noobs () { if [ "$BOOT_PART_NUM" = "1" ]; then NOOBS=0 else NOOBS=1 fi } get_variables () { ROOT_PART_DEV=$(findmnt / -o source -n) ROOT_PART_NAME=$(echo "$ROOT_PART_DEV" | cut -d "/" -f 3) ROOT_DEV_NAME=$(echo /sys/block/*/"${ROOT_PART_NAME}" | cut -d "/" -f 4) ROOT_DEV="/dev/${ROOT_DEV_NAME}" ROOT_PART_NUM=$(cat "/sys/block/${ROOT_DEV_NAME}/${ROOT_PART_NAME}/partition") BOOT_PART_DEV=$(findmnt /boot -o source -n) BOOT_PART_NAME=$(echo "$BOOT_PART_DEV" | cut -d "/" -f 3) BOOT_DEV_NAME=$(echo /sys/block/*/"${BOOT_PART_NAME}" | cut -d "/" -f 4) BOOT_PART_NUM=$(cat "/sys/block/${BOOT_DEV_NAME}/${BOOT_PART_NAME}/partition") OLD_DISKID=$(fdisk -l "$ROOT_DEV" | sed -n 's/Disk identifier: 0x\([^ ]*\)/\1/p') check_noobs ROOT_DEV_SIZE=$(cat "/sys/block/${ROOT_DEV_NAME}/size") TARGET_END=$((ROOT_DEV_SIZE - 1)) PARTITION_TABLE=$(parted -m "$ROOT_DEV" unit s print | tr -d 's') LAST_PART_NUM=$(echo "$PARTITION_TABLE" | tail -n 1 | cut -d ":" -f 1) ROOT_PART_LINE=$(echo "$PARTITION_TABLE" | grep -e "^${ROOT_PART_NUM}:") ROOT_PART_START=$(echo "$ROOT_PART_LINE" | cut -d ":" -f 2) ROOT_PART_END=$(echo "$ROOT_PART_LINE" | cut -d ":" -f 3) if [ "$NOOBS" = "1" ]; then EXT_PART_LINE=$(echo "$PARTITION_TABLE" | grep ":::;" | head -n 1) EXT_PART_NUM=$(echo "$EXT_PART_LINE" | cut -d ":" -f 1) EXT_PART_START=$(echo "$EXT_PART_LINE" | cut -d ":" -f 2) EXT_PART_END=$(echo "$EXT_PART_LINE" | cut -d ":" -f 3) fi } fix_partuuid() { DISKID="$(fdisk -l "$ROOT_DEV" | sed -n 's/Disk identifier: 0x\([^ ]*\)/\1/p')" sed -i "s/${OLD_DISKID}/${DISKID}/g" /etc/fstab sed -i "s/${OLD_DISKID}/${DISKID}/" /boot/cmdline.txt } check_variables () { if [ "$NOOBS" = "1" ]; then if [ "$EXT_PART_NUM" -gt 4 ] || \ [ "$EXT_PART_START" -gt "$ROOT_PART_START" ] || \ [ "$EXT_PART_END" -lt "$ROOT_PART_END" ]; then FAIL_REASON="Unsupported extended partition" return 1 fi fi if [ "$BOOT_DEV_NAME" != "$ROOT_DEV_NAME" ]; then FAIL_REASON="Boot and root partitions are on different devices" return 1 fi if [ "$ROOT_PART_NUM" -ne "$LAST_PART_NUM" ]; then FAIL_REASON="Root partition should be last partition" return 1 fi if [ "$ROOT_PART_END" -gt "$TARGET_END" ]; then FAIL_REASON="Root partition runs past the end of device" return 1 fi if [ ! -b "$ROOT_DEV" ] || [ ! -b "$ROOT_PART_DEV" ] || [ ! -b "$BOOT_PART_DEV" ] ; then FAIL_REASON="Could not determine partitions" return 1 fi } check_kernel () { local MAJOR=$(uname -r | cut -f1 -d.) local MINOR=$(uname -r | cut -f2 -d.) if [ "$MAJOR" -eq "4" ] && [ "$MINOR" -lt "9" ]; then return 0 fi if [ "$MAJOR" -lt "4" ]; then return 0 fi NEW_KERNEL=1 } main () { get_variables if ! check_variables; then return 1 fi check_kernel if [ "$NOOBS" = "1" ] && [ "$NEW_KERNEL" != "1" ]; then BCM_MODULE=$(grep -e "^Hardware" /proc/cpuinfo | cut -d ":" -f 2 | tr -d " " | tr '[:upper:]' '[:lower:]') if ! modprobe "$BCM_MODULE"; then FAIL_REASON="Couldn't load BCM module $BCM_MODULE" return 1 fi fi if [ "$ROOT_PART_END" -eq "$TARGET_END" ]; then reboot_pi fi if [ "$NOOBS" = "1" ]; then if ! parted -m "$ROOT_DEV" u s resizepart "$EXT_PART_NUM" yes "$TARGET_END"; then FAIL_REASON="Extended partition resize failed" return 1 fi fi if ! parted -m "$ROOT_DEV" u s resizepart "$ROOT_PART_NUM" "$TARGET_END"; then FAIL_REASON="Root partition resize failed" return 1 fi partprobe "$ROOT_DEV" fix_partuuid return 0 } mount -t proc proc /proc mount -t sysfs sys /sys mount -t tmpfs tmp /run mkdir -p /run/systemd mount /boot mount / -o remount,rw sed -i 's| init=/usr/lib/raspi-config/init_resize.sh||' /boot/cmdline.txt if ! grep -q splash /boot/cmdline.txt; then sed -i "s/ quiet//g" /boot/cmdline.txt fi sync echo 1 > /proc/sys/kernel/sysrq if ! check_commands; then reboot_pi fi if main; then whiptail --infobox "Resized root filesystem. Rebooting in 5 seconds..." 20 60 sleep 5 else sleep 5 whiptail --msgbox "Could not expand filesystem, please try raspi-config or rc_gui.\n${FAIL_REASON}" 20 60 fi reboot_pi
partedを使って$ROOT_DEV
をゴニョゴニョしている部分があるのでビンゴっぽい。
whiptail --infobox "Resized root filesystem. Rebooting in 5 seconds..." 20 60
ってのがリサイズ完了メッセージを出すやつみたい。
面白いのは174行目から178行目あたり。
sed -i 's| init=/usr/lib/raspi-config/init_resize.sh||' /boot/cmdline.txt if ! grep -q splash /boot/cmdline.txt; then sed -i "s/ quiet//g" /boot/cmdline.txt fi sync
/boot/cmdline.txt
からinit=/usr/lib/raspi-config/init_resize.sh
を消して次から実行されないようにしている。
自動拡張を無効にしてみる
とりあえず/boot/cmdline.txt
からinit=/usr/lib/raspi-config/init_resize.sh
を削除して、
Raspberry PIに入れて起動してみた。
初回起動を終えた後に改めてfdiskで確認すると、自動拡張されていないことが確認できる。
$ sudo fdisk -l /dev/mmcblk0 Disk /dev/mmcblk0: 3.7 GiB, 3965190144 bytes, 7744512 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x7ee80803 デバイス 起動 Start 最後から セクタ Size Id タイプ /dev/mmcblk0p1 8192 98045 89854 43.9M c W95 FAT32 (LBA) /dev/mmcblk0p2 98304 3645439 3547136 1.7G 83 Linux
この状態で、raspi-configの7 Advanced Options -> A1 Expand Filesystem
すれば、昔のRaspbianのように手動で拡張できる。
resize2fsの実行はどうなっているか調べる
通常、パーティションをリサイズしたあとはOSを再起動してからresize2fsをしなければいけないはずなんだけど、 それはどこでやっているのだろうと思ってraspi-configのExpand Filesystem機能を調べてみた。
raspi-config自体はgithubにリポジトリがあったのでそこのraspi-configのソースを読んだ。
Expand Filesystem機能はdo_expand_rootfs
で実装されていて、その中のコードを読んでみたら、fdiskでパーティションを拡張した後に、/etc/init.d/resize2fs_once
を作成してupdate-rc.dでサービスに登録し、再起動後に実行させていることがわかった。
resize2fs_once
はサービスが起動されたら、resize2fsをrootfsパーティションに対して行い、自分自身のサービス登録を解除して、ファイル削除するようになっている。
RaspbianのOSイメージを焼いたばかりのSDカードでrootfsをマウントして/etc/init.d
の中を調べてみると、確かにresize2fs_once
が入っていた。
OSイメージに入っていた/etc/init.d/resize2fs_once
はこんな感じになっていた。
#!/bin/sh ### BEGIN INIT INFO # Provides: resize2fs_once # Required-Start: # Required-Stop: # Default-Start: 3 # Default-Stop: # Short-Description: Resize the root filesystem to fill partition # Description: ### END INIT INFO . /lib/lsb/init-functions case "$1" in start) log_daemon_msg "Starting resize2fs_once" ROOT_DEV=$(findmnt / -o source -n) && resize2fs $ROOT_DEV && update-rc.d resize2fs_once remove && rm /etc/init.d/resize2fs_once && log_end_msg $? ;; *) echo "Usage: $0 start" >&2 exit 3 ;; esac
Raspbianの初回起動時には必ずこのサービスが動くので、rootfsのパーティションに対してresize2fsが行われる。
/etc/init.d/resize2fs_once
もスパイ映画の「なお、このテープは自動的に消滅する」みたいな作りになっていて面白い。
まとめ
Raspbianのrootfsの初回起動時自動拡張は/usr/lib/raspi-config/init_resize.sh
と/etc/init.d/resize2fs_once
の合わせ技で実現されていることがわかった。