ミルク色の記録

やったこと、やってみたこと

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のソースを読んだ。

github.com

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の合わせ技で実現されていることがわかった。