前文
きっかけは公式フォーラムの以下のトピ。
dd vs cp for flashing sd cards on the Linux command line. - Raspberry Pi Forums
Linux
環境でイメージファイルの書き込みといえばdd
コマンドが定番だけど、馴染みが薄いしスイッチ類が分かりにくいしで使い勝手が悪い。cp
コマンドでも代用できるので、そっちの方が良くね? という議論が交わされている。
実はcp
コマンドの方が(僅かではあるが)速い、というのも興味深い。
そんな訳でdd
コマンド以外のイメージファイルの書き込み(操作)方法を考察検証してみた。
動作確認環境
Pi4B 4G、64 bit Raspberry Pi OS with desktop(bullseye)、USB-SSDブート。
イメージファイル書込み先は/dev/sdb
。USB接続 microSDカードリーダ/ライタ。
microSDカードは Transcend の 32G。
検証に使用したイメージファイルは以下(LibreELEC-RPi4.arm-10.0.2.img.gz)、又はそれを解凍したファイル(LibreELEC-RPi4.arm-10.0.2.img)。
Raspberry Pi4用LibreELEC 10.0.2
イメージファイル書込み
まずdd
コマンドなら以下。
$ sudo dd if=./LibreELEC-RPi4.arm-10.0.2.img of=/dev/sdb bs=1M
次はcp
コマンドで。確かに直観としてこちらの方が分かりやすい。
$ sudo cp ./LibreELEC-RPi4.arm-10.0.2.img /dev/sdb
冒頭で挙げたトピでは、cp
コマンドでのイメージ書き込み例に対し、cp
コマンドとは通常のファイルコピーで使用する物、イメージ書き込みなんて出来ないですよ、と脊髄反射している人が居た。
気持ちは分かるけど、これはコピー先がデバイスファイルという特殊ファイルの成せる技。
デバイスファイルは連動するストレージ、今回のケースなら/dev/sdb
は microSDカードのイメージにダイレクトに紐づいている。dd
コマンドを使用しなくても、このデバイスファイルを利用すれば、ストレージに対しハードウェアに近い低レベルな直接操作ができる、というのが基本的な考え方。
cat
コマンドなら以下。
$ cat ./LibreELEC-RPi4.arm-10.0.2.img | sudo tee /dev/sdb > /dev/null
うーん、一般ユーザのsudo
なので微妙に複雑。sudo cat ~
としても、リダイレクト先の/dev/sdb/
にはsudo
が作用しないので、sudo tee
で橋渡ししている。しかしtee
はファイルへの出力だけではなく標準出力(画面出力)も行ってしまい邪魔なのでそれは> /dev/null
で捨てている、という内容。回りくどいなぁ。
root
ユーザなら単純に以下で良い。
# cat ./LibreELEC-RPi4.arm-10.0.2.img > /dev/sdb
圧縮ファイルをそのまま使用したい場合は以下。
$ gunzip -c ./LibreELEC-RPi4.arm-10.0.2.img.gz | sudo tee /dev/sdb > /dev/null
gunzip -c
は解凍結果を標準出力に吐くのでそれをパイプで繋げて書き込み。
てか、ならzcat
でも良くね、ということで以下でも同じ。
$ zcat ./LibreELEC-RPi4.arm-10.0.2.img.gz | sudo tee /dev/sdb > /dev/null
こちらも前述同様、root
ユーザならストレートに以下。
# gunzip -c ./LibreELEC-RPi4.arm-10.0.2.img.gz > /dev/sdb
# zcat ./LibreELEC-RPi4.arm-10.0.2.img.gz > /dev/sdb
ベリファイ
イメージの書き込みでbalenaEtcher
を使用している方は多いと思うけど、新品または新品に近い状態の microSDカードでもイメージ書き込み後のベリファイでエラーを吐くことがある。
(自分もそうだけど)英語で書かれた画面なんてよく分からないしでろくに確認もせずに閉じてしまい、見落としている人が多い予感。
因みにベリファイでエラーが報告されたら即ダメって話でもなくて、再度書き込めば次は正常終了だったりするので OK。つまりそれだけ不安定、信頼できないメディアってこと。
前項の、要領よくやればこんなに簡単なコマンドでイメージファイルの書き込みができますよ、で完結する話ではなく。本当に正しく書き込まれたのかこれ、という不安が必ずつきまとう。
という訳で次はベリファイを行うコマンドの考察。
diff
コマンドはバイナリも比較できるので以下。しかし上手くいかない。
$ sudo diff --brief --report-identical-files ./LibreELEC-RPi4.arm-10.0.2.img /dev/sdb
ファイル ./LibreELEC-RPi4.arm-10.0.2.img と /dev/sdb は異なります
これはイメージファイルと microSDカード全体と比較してしまうから。そこでcmp
コマンドで以下。
$ sudo cmp -b ./LibreELEC-RPi4.arm-10.0.2.img /dev/sdb
cmp: EOF on ./LibreELEC-RPi4.arm-10.0.2.img after byte 575668224, in line 541104
-b
スイッチを指定しているので差異があればその位置と双方の値が表示される。それが表示されず且つcmp: EOF on ~
でファイルの終端に達したことを示しているということは、イメージファイルの範囲内で相違点はなかったということ。
因みに異なる箇所がある場合の出力例は以下。
- /dev/sdb 異なります: バイト 4458241、行 655 0 ^@ 345 M-e
圧縮ファイルでの比較は以下。
$ gunzip -c ./LibreELEC-RPi4.arm-10.0.2.img.gz | sudo cmp -b - /dev/sdb
$ sudo zcmp -b ./LibreELEC-RPi4.arm-10.0.2.img.gz /dev/sdb
書き戻し(バックアップ)
ついでにバックアップコマンドの考察。
冒頭のイメージファイルから microSDカードへの書き込みの逆なので以下。以降出力ファイル名は<デバイスファイル名>.img
とする。
$ sudo cp /dev/sdb ./sdb.img
$ sudo cat /dev/sdb > ./sdb.img
但しこの場合、microSDカード全体のバックアップとなる。従って、前述のdiff
コマンドでの比較はこのバックアップファイルの場合なら同一と判定される。
$ sudo diff --brief --report-identical-files ./sdb.img /dev/sdb
ファイル ./sdb.img と /dev/sdb は同一です
全体バックアップならこれで良いけど、バックアップ時の容量節約を考慮しパーティションサイズを敢えて縮小している場合はそのサイズでファイルに書き出したい。
今回、LibreELEC
イメージファイルを書き込んだ直後のsdb
のパーティション構成は以下。
$ sudo fdisk -l /dev/sdb
Disk /dev/sdb: 28.33 GiB, 30416044032 bytes, 59406336 sectors
Disk model: STORAGE DEVICE
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: 0x8d4aa938
Device Boot Start End Sectors Size Id Type
/dev/sdb1 * 8192 1056767 1048576 512M c W95 FAT32 (LBA)
/dev/sdb2 1056768 1122303 65536 32M 83 Linux
簡単な図に起こすと以下。
4M + 512M + 32M = 548M しか使用していない。極端な例だけど、この範囲で取得してみる。
上記の通り、sdb
の最終セクタは 1122303 なので、先頭から1122304セクタコピーすれば良いことになる。
しかし手動でサイズを求めバックアップ時のパラメータとして設定するのは間違いの元なのでまずコマンドで求める方法を確定する。上記の通り、欲しい値はsudo fdisk -l /dev/sdb
の出力結果の最後の行の 3列目で更にそれを +1 した値。
更にデバイス名sdb
も変数化すると以下。
$ src='sdb'
$ sudo fdisk /dev/$src -l | tail -n 1 | awk '{$3=($3+1);print $3}'
1122304
期待した結果が得られた。これをdd
コマンドにパラメータとして組み込むと以下。
$ src='sdb'
$ sudo dd if=/dev/$src of=./$src.img bs=512 count=`sudo fdisk /dev/$src -l | tail -n 1 | awk '{$3=$3+1;print $3}'`
1122304+0 レコード入力
1122304+0 レコード出力
574619648 bytes (575 MB, 548 MiB) copied, 12.8095 s, 44.9 MB/s
一応ベリファイ。大丈夫そう。
$ sudo cmp -b ./$src.img /dev/$src
cmp: EOF on ./sdb.img after byte 574619648, in line 541104
しかし今回のテーマはdd
コマンド以外で行う、なので以下。
$ src='sdb'
$ sudo head -c `sudo fdisk /dev/$src -l | tail -n 1 | awk '{$3=($3+1)*512;print $3}'` /dev/$src > ./$src.img
head
コマンドは-c
スイッチで取得したい先頭バイト数を指定できる。つまりここでは単位がバイト数なので*512
としている。(1セクタ = 512バイト)
そしてベリファイ。先ほどと同じ結果。
$ sudo cmp -b ./$src.img /dev/$src
cmp: EOF on ./sdb.img after byte 574619648, in line 541104
書き戻し(バックアップ)其ノ二
パーティション単位でのバックアップなら以下。
$ sudo cp /dev/sdb1 ./sdb1.img
$ sudo cp /dev/sdb2 ./sdb2.img
これならサイズを意識する必要がない。しかし MBR を含む先頭の領域となると、こういった方法では対応できない。サイズ指定が必要。うーむ。サイズを意識しないで抽象的にバックアップできることが究極の理想だけど無理っぽい。
この先頭の領域はsdb0.img
とし、パーティション単位で取得する方向でまとめるなら以下。
$ src='sdb'
$ sudo head -c `sudo fdisk -l /dev/$src | grep "${src}1" | awk '{print $3*512}'` /dev/$src > ./$src0.img
$ sudo cp /dev/${src}1 ./$src1.img
$ sudo cp /dev/${src}2 ./$src2.img
ベリファイは以下。sdb0.img、sdb1.img、sdb2.img をcat
で連結し/dev/sdb
と比較。
$ src='sdb'
$ sudo cat ./${src}0.img ./${src}1.img ./${src}2.img | sudo cmp -b /dev/$src -
cmp: EOF on - after byte 574619648, in line 541104
コマンドまとめ
イメージファイル書込みはベリファイを含めて以下。
$ src='./LibreELEC-RPi4.arm-10.0.2.img.gz'
$ dst='sdb'
$ zcat $src | sudo tee /dev/$dst > /dev/null
$ sudo zcmp -b $src /dev/$dst
cmp: EOF on - after byte 575668224, in line 541104
バックアップは以下。
$ src='sdb'
$ sudo head -c `sudo fdisk /dev/$src -l | tail -n 1 | awk '{$3=($3+1)*512;print $3}'` /dev/$src > ./$src.img
$ sudo cmp -b ./$src.img /dev/$src
cmp: EOF on ./sdb.img after byte 574619648, in line 541104
最終確認
最初のイメージファイルと最終的に microSDカードから吸い出したイメージファイルを比較してみる。結果は以下。
$ src='sdb'
$ zcmp -b ./LibreELEC-RPi4.arm-10.0.2.img.gz ./$src.img
cmp: EOF on ./sdb.img after byte 574619648, in line 541104
sdb.img
はパーティションサイズできっちり切り出しているので最初のイメージファイルより微妙にサイズが小さい。
一応、最初のイメージファイルの 574619648(22400000h) 以降を確認すると以下。
$ zcat ./LibreELEC-RPi4.arm-10.0.2.img.gz | od -j 574619648 -Ax -tx1z -
22400000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
*
22500000
00
で埋まっているだけの余白なので問題ないでしょう。sdb上も以下の内容。(100000h = 1048576)
$ sudo od -j 574619648 -Ax -tx1z -N 1048576 /dev/sdb
22400000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
*
22500000
注意点
実はベリファイで少々躓いた。結果が異なる場合があり当初は原因が分からず悩まされた。全体的には同じだが、一部(1バイト)だけ異なるようなケース。
原因はマウント状態やdirty bit
だった。
まずdirty bit
。これはストレージがmount
(マウント)されると設定され、umount
(アンマウント)されるとクリアされるという仕様らしい。
もし新規でmount
したストレージのdirty bit
が既に設定済だった場合は、前回明示的にumount
されずに取り外されたか(リムーバブルストレージの場合)、電源が落ちたか、といった正規の手続きを踏んでいなかったことを示す。
その場合、本来ならストレージに書き込まれる筈だったメモリ上のデータがフラッシュされていない(書き込まれていない)、不整合が発生している可能性がある、ということになる。という仕様のもの。
問題はこのdirty bit
が microSDカードに設定されている場合、イメージファイルを書き込んでもリセットされず残るという。
結果ベリファイをかけるとdirty bit
で差異が発生してしまう。
異なる値が、イメージファイル上では0
、microSDカード上では1
だった場合はまずこれが原因。
その場合はfsck
コマンドで状態を確認。n
スイッチは検査だけで修復は行わない、v
は詳細表示スイッチ。実行例は以下。(正常時)
$ sudo fsck -nv /dev/sdb1; echo $?
fsck from util-linux 2.36.1
fsck.fat 4.2 (2021-01-31)
Checking we can access the last sector of the filesystem
Boot sector contents:
System ID "MTOO4026"
Media byte 0xf0 (5.25" or 3.5" HD floppy)
512 bytes per logical sector
8192 bytes per cluster
1 reserved sector
First FAT starts at byte 512 (sector 1)
2 FATs, 16 bit entries
131072 bytes per FAT (= 256 sectors)
Root directory starts at byte 262656 (sector 513)
512 root directory entries
Data area starts at byte 279040 (sector 545)
65501 data clusters (536584192 bytes)
8192 sectors/track, 4 heads
0 hidden sectors
1048576 sectors total
Checking for unused clusters.
/dev/sdb1: 268 files, 18214/65501 clusters
0
$ sudo fsck -nv /dev/sdb2; echo $?
fsck from util-linux 2.36.1
e2fsck 1.46.2 (28-Feb-2021)
STORAGE: clean, 12/8192 files, 6938/32768 blocks
0
echo $?
で明示的に戻り値も表示。0
が返ってきているので正常終了。
もしdirty bit
が設定されている場合はこれで表示され確認できる。その場合は改めてfsck
をかけ、dirty bit
をクリアすること。
また、mount
されている状態でバックアップを取るとベリファイで異なる結果となり無駄に悩むことになるので、umount
してから行うこと。
$ sudo umount /dev/sdb1
$ sudo umount /dev/sdb2
余談
dataset definitionの略であるが、IBMのメインフレームのJob Control Language(ジョブ制御言語、JCL)の「DD文」(DD statement)に由来するため、
引数の構文が、Unixの一般的なコマンドの引数のそれとは激しく異なっている
(datasetというのはメインフレーム用語)。
なるほどねぇ。dd
コマンドの引数構文の異質さ気持ち悪さはこれが原因なのか。