树莓派RaspberryPi系统备份Image的制作

在树莓派上做开发,难免会弄出各种版本的系统,加上有时候还需要拿给客户自己烧录或者demo,总是要clone一下TF卡,做系统做到烦躁。于是想想有没有什么办法能做跟官方release一样的烧录img出来,基本要求就是全系统克隆,但做出的img跟卡的容量无关,只跟系统占用的存储大小有关。

简单全卡备份

其实如果只是简单备份,可以直接将TF卡插入Linux电脑,用dd命令来备份和恢复(设备号不固定这里只是例子,还是用fdisk -l先查看一下比较保险):

# Backup the system to img file in Linux
dd if=/dev/mmcblk0 of=raspberrypi-bak.img bs=1M
# A little difference if Mac
dd if=/dev/rdisk2 of=raspberrypi-bak.img bs=1M
#
# Restore system from img
dd if=raspberrypi-bak.img bs=1M of=/dev/mmcblk0
# Or in Mac
dd if=raspberrypi-bak.img bs=1M of=/dev/rdisk2

不过这样做,16G的卡哪怕你只用了1G,整个备份文件也有16G,占用空间耗时间是小事,想想拿这么大文件给客户不方便,也显得太不专业了。
在网上搜了一下,已经有朋友先行做过类似的事情:树莓派 Raspberry Pi SD卡系统备份与还原,看了一下按照他的步骤实践了一遍,但出现了一些问题,折腾了很久最终才解决。

RaspberryPi的文件系统

首先介绍一下RaspberryPi的文件系统。树莓派的官方系统是基于Debian的,主要是两个分区:启动分区boot和根分区。boot分区为fat32格式,挂载在/boot,存放一些系统启动需要的基本文件,包括内核、驱动、firmware、启动脚本等;根分区文件系统是ext4格式,挂载于/,存放一些安装的软件和库文件、系统配置、用户数据等等;另外当系统启动时会自动生成和挂载一些必要的其他文件夹,包括temfs、sysfs、proc、debugfs、configfs等(使用mount可以看到他们),这些都是虚拟文件系统,由操作系统自动管理,备份时不需要关注。日常使用时,修改的文件包括安装的软件都是在根分区中,而如果自行编译内核,需要更新的文件都在/boot中。
所以备份一个系统,实际上是要备份这两个分区,官方发布的烧录镜像,也是包含了这样的两个分区,并保证通过dd的操作,能将其完整写入目标TF卡。首次烧录完毕后,不论你的TF卡容量为多少,启动后的boot和/分区大小都是固定的,然后可以使用raspi-config来扩展根分区的大小,boot分区不变,来达到使用所有卡内容量的目的。
相对应的备份步骤,大致为:创建img,把img当作一个磁盘分区和格式化,mount各个分区,将文件备份至对应的分区中,umount分区结束备份。

目标备份文件的创建和分区

既然是备份到文件,那么首先需要创建一个备份文件,并且把这个文件看作一个虚拟设备,对其进行分区。

你看到的是非授权版本!爬虫凶猛,请尊重知识产权!

转载请注明出处:http://conanwhf.github.io/2016/08/25/rpi-cloneimg/

访问原文「树莓派RaspberryPi系统备份Image的制作」获取最佳阅读体验并参与讨论

需要用到的工具有_parted, kpartx, dosfstools, rsync_,使用apt-get安装:

sudo apt-get -y install rsync dosfstools parted kpartx

创建新的文件

我们用dd命令来创建一个新的img,img的大小和当前RaspberryPi已使用的存储空间有关。使用df -P来查看磁盘空间状态(以1K为单位),会看到如下的信息:

其中红框的部分就是我们需要关注的,将它们对应的Used数目取出,计算得到需要创建的img大小:
img=rpibackup.img

#sudo rm $img
bootsz=df -P | grep /boot | awk '{print $2}'
rootsz=df -P | grep /dev/root | awk '{print $3}'
totalsz=echo $bootsz $rootsz | awk '{print int(($1+$2)*1.3)}'
sudo dd if=/dev/zero of=$img bs=1K count=$totalsz

这里的totalsz是基于两个分区使用总和的1.3倍,一方面是分区表、格式化等操作造成的空间损失,另一方面是系统对剩余空间的要求。我曾经尝试过使用1.1,然而系统启动后在终端大部分命令都报错,说空间不足。

将文件分区

工具parted可以用来把一个img文件当作一个磁盘来分区。使用fdisk -l能够看到系统中各个磁盘分区的情况,在我的树莓派上系统TF卡的分区情况如下:

我们把boot分区设计为跟原分区大小一样,root分区则是扩展到img文件的末尾。对应的脚本:

bootstart=sudo fdisk -l /dev/mmcblk0 | grep mmcblk0p1 | awk '{print $2}'
bootend=sudo fdisk -l /dev/mmcblk0 | grep mmcblk0p1 | awk '{print $3}'
rootstart=sudo fdisk -l /dev/mmcblk0 | grep mmcblk0p2 | awk '{print $2}'
echo "boot: $bootstart >>> $bootend, root: $rootstart >>> end"
rootend=sudo fdisk -l /dev/mmcblk0 | grep mmcblk0p2 | awk '{print $3}'
sudo parted $img --script -- mklabel msdos
sudo parted $img --script -- mkpart primary fat32 ${bootstart}s ${bootend}s
sudo parted $img --script -- mkpart primary ext4 ${rootstart}s -1

分别格式化fat32和ext4分区

然后对分区分别进行格式化,我们可以通过loop来建立虚拟的磁盘挂载点,进行后续的操作:

loopdevice=sudo losetup -f --show $img
device=/dev/mapper/sudo kpartx -va $loopdevice | sed -E 's/.*(loop[0-9])p.*/\1/g' | head -1
sleep 5
sudo mkfs.vfat ${device}p1 -n boot
sudo mkfs.ext4 ${device}p2

在中间的这个sleep是我在实践中发现,创建loop的设备节点需要一些时间,如果手动在终端贴命令则没有问题,而若是用脚本执行,则在format时节点还没有创建好,从而造成格式化失败。

备份Boot分区

Boot分区是Fat32格式且数据不多,直接mount然后copy数据即可,注意权限问题。

mountb=$usbmount/backup_boot/
mkdir -p $mountb
sudo mount -t vfat ${device}p1 $mountb
sudo cp -rfp /boot/* $mountb
sync 
sudo umount $mountb

备份根(root)分区

根分区的备份是我折腾了很久的部分。因为它文件众多,不止一个文件夹,而且在系统运行时有一些文件、文件夹是临时生成或者mount的,不能全部直接copy。这就需要有一种方法能区分真正写在磁盘上的文件并将其备份。

失败的经验:dump&restore

在开头提到的道友那篇文章里,是采用了针对ext4备份和恢复的工具dump和restore。

sudo mount -t ext4 $partRoot /media/
cd /media
sudo dump -0uaf - / | sudo restore -rf -
cd; sudo umount /media

但在关键的步骤出现了 Broken pipe错误,经过重复测试多次,虽然每次报错的文件和inode不一样,但每次都无法顺利完成。这个问题在原博的评论中也有人遇到了:

restore: ./lost+found: File exists
./tmp/rstdir1445584846: (inode 159534) not found on tape
./tmp/rstmode1445584846: (inode 161527) not found on tape
DUMP: Broken pipe
DUMP: The ENTIRE dump is aborted.
………………

针对这个问题我做了一些测试。首先是把sudo dump -0uaf - / | sudo restore -rf -分开,取消管道,先dump到一个文件再restore。发现dump总是成功的,而问题出在restore上,所以就broken pipe了。开始怀疑是restore的时候权限有问题,后来把dump出来的文件拿到PC上做restore,也同样是失败。这样就很有可能是dump的文件有问题,仔细看看每次出错的文件都是隐藏或者临时文件,怀疑跟这个有关,但找了一大圈dump相关的参数尝试,依然是没有解决。

使用rsync备份

由于每次测试dump和restore花的时间很长,项目又比较急,我实在是不能再耽搁下去了,于是决定放弃这种方式。想了一下,曾经用过rpi-clone来做卡与卡之间的备份,那么备份成文件也应该是一样的。我于是去看了下它的源码,把根文件系统备份相关的部分提出来就能直接用起来了。这里用的是备份工具rsync,其本质上是基于文件系统之上的的直接拷贝,因为没有用到增量备份,所以跟cp其实是一回事。只是rsync能很好的保留各种权限、时间戳、软链接和文件信息,避免了一些用cp的问题。保险起见,我依然沿用了它。
首先还是mount一下:

mountr=$usbmount/backup_root/
mkdir -p $mountr
sudo mount -t ext4 ${device}p2 $mountr

然后要对存在swap分区的情况进行特殊处理:

if [ -f /etc/dphys-swapfile ]; then
        SWAPFILE=`cat /etc/dphys-swapfile | grep ^CONF_SWAPFILE | cut -f 2 -d=`
    if [ "$SWAPFILE" = "" ]; then
        SWAPFILE=/var/swap
    fi
    EXCLUDE_SWAPFILE="--exclude $SWAPFILE"
fi

接着就是所有文件的备份,跳过某些临时、系统自动生成的文件和文件夹:

sudo rsync --force -rltWDEgopt --delete --stats --progress\
    $EXCLUDE_SWAPFILE \
    --exclude '.gvfs' \
    --exclude '/dev' \
        --exclude '/media' \
    --exclude '/mnt' \
    --exclude '/proc' \
        --exclude '/run' \
    --exclude '/sys' \
    --exclude '/tmp' \
        --exclude 'lost\+found' \
    --exclude '$usbmount' \
    // $mountr

完成后新建一些特殊文件夹,用来做系统启动时某些文件的自动挂载点,例如proc和mnt等等:

for i in dev media mnt proc run sys boot; do
    if [ ! -d $mountr/$i ]; then
        sudo mkdir $mountr/$i
    fi
done
if [ ! -d $mountr/tmp ]; then
    sudo mkdir $mountr/tmp
    sudo chmod a+w $mountr/tmp
fi

到这里基本上就完成了,有一点小小的修改就是把网络配置文件删掉,以免换了新的网络环境启动系统无法自动配置:

sudo rm -f $mountr/etc/udev/rules.d/70-persistent-net.rules
sync

最后umount,清理loop设备,结束备份:

sudo umount $mountr
# umount loop device
sudo kpartx -d $loopdevice
sudo losetup -d $loopdevice
sudo umount $usbmount
rm -rf $mountb $mountr

Mount USB device

使用dump/restore的时候,有一个跳过特定文件的参数,在前文提到的教程中,作者是将img放到用户目录,并且跳过这个文件的。虽然使用rsync备份的时候,依然可以这么做,但考虑到通用性和调试的方便,我把文件直接备份在了外接的U盘上,以避免考虑各种“自己备份自己”可能带来的问题。顺便把相关的USB mount脚本和判断也贴一下供参考:

usbmount=/mnt
mkdir -p $usbmount
if [ -z $1 ]; then
    echo "no argument, assume the mount device is /dev/sda1 ? Y/N"
    read key
    if [ "$key" = "y" -o "$key" = "Y" ]; then
        sudo mount -t vfat -o uid=1000 /dev/sda1 $usbmount
    else
        echo "$0 [backup dest device name], e.g. $0 /dev/sda1"
        exit 0
    fi
else
    sudo mount -t vfat -o uid=1000 $1 $usbmount
fi
if [ -z "`grep $usbmount /etc/mtab`" ]; then
    echo "mount fail, exit now"
    exit 0
fi 

当然,如果你有兴趣,大可以自己改一改尝试直接备份在用户目录,只是我感觉依然是需要copy出来,意义不大。😏

恢复备份

备份img做好了,恢复备份就无需多谈了。既然是用树莓派的小伙伴,自然有一百种方法将镜像烧录到卡上。实在不知道的,放官方链接:Installing images,或者看这篇:树莓派初始配置指南(2代B型)

小结

整理后完整的script我已经更新到GitHub:https://github.com/conanwhf/RaspberryPi-script/blob/master/rpi-backup.sh,如果对你有所帮助,欢迎Star!😊有什么疑问和质疑,也欢迎砸过来!🤗
顺便提一下,写这篇文章的时候,我发现官方已经release了一个备份工具,piclone,看上去是在两个TF卡之间备份,跟rpi-clone功能一样。我还没有测过,有兴趣的小伙伴可以试一下,看看源码,也许/的备份能有新的思路和方法。