重新调整.img文件以适应较小的SD卡。如何缩小可引导的SD卡镜像。

背景

我有一张16GB的SD卡,上面安装了一个基于Linux的操作系统,用于树莓派。 大部分空间是空闲的。

我想与其他人分享这个SD卡的.img文件,但如果我使用以下命令:

dd if=/dev/sdXX of=/home/user123/SD.img

它将创建一个16GB的镜像。太大了。

问题

如何将一个16GB的SD卡镜像调整为更小的4GB?

我尝试使用GParted:它可以轻松地创建一个4GB的分区,但是整个SD卡的.img文件仍然是16GB,其中有12GB的未分配空间。

GParted table

我已经阅读了问题和答案在Ubuntu中克隆多个分区,但我仍然无法将16GB的SD卡调整为4GB的大小。
更多信息:
~$ lsblk 
...

sdc      8:32   1  14,9G  0 disk 
├─sdc1   8:33   1   100M  0 part 
└─sdc2   8:34   1     4G  0 part 

~$ sudo fdisk -l /dev/sdc
Disk /dev/sdc: 14,9 GiB, 15931539456 bytes, 31116288 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: 0xf8a631ce

Device     Boot  Start     End Sectors  Size Id Type
/dev/sdc1  *      2048  206847  204800  100M  c W95 FAT32 (LBA)
/dev/sdc2       206848 8595455 8388608    4G 83 Linux

非常感谢任何建议!

请注意:根据Melebius在评论中的观察,正确的词语是shrink(缩小):

由于SD卡是带有特定容量的硬件,无法改变,因此您不能调整SD卡的大小。显然,您想要缩小一个SD卡镜像


2看一下resize2fs命令。 - marosg
@guiverc,谢谢你指出这个问题。确实,操作系统信息对于这个问题是不必要的。 - Leos313
1@Melebius,我认为克隆多个分区可能与这个问题有关,但说实话,真正的问题是另一个:缩小SD卡(即使它只有一个分区)。这有意义吗?关键是truncat命令和GParted的使用。当然,我们可以使用GParted来处理多个分区,但这个技巧也适用于只有一个分区。此外,在其他答案中从未提到过使用truncat。 - Leos313
是的,这基本上是有道理的,除了“收缩(或调整大小)SD卡”这个术语。你_不能_调整SD卡的大小,因为它是硬件,具有固定的容量,无法更改。显然,你想要收缩一个SD卡镜像。我已经编辑了你的问题来修复这个问题,并撤回了我的关闭投票。 - Melebius
1@Melebius,谢谢。然而,在Google上使用“shrink”这个词会引导您找到许多其他解决方案,而且都能完美运行。对我来说,问题在于找到正确的词来插入到Google搜索引擎中。我当时在寻找“重新调整尺寸”,显然不是正确的词语。我建议以某种方式保留“重新调整尺寸”一词,同时搭配正确的术语。您同意吗?我在问题的末尾添加了一个请注意 - Leos313
5个回答

这篇文章提供了一个解决我的问题的方法(*). 它与其他方法非常相似,但更好地解释了如何计算以及数字和分区的含义。

关键信息是使用truncate命令。以下是完整的解决方案,以免丢失答案。

首先,在您的电脑上克隆SD卡:

  1. 使用lsblk命令查看可用设备以及它们的分区是否已挂载。

  2. 卸载您想要在计算机上复制的设备的所有分区。例如:

     umount /dev/sdc1
     umount /dev/sdc2
    
  3. 创建一个未挂载的整个SD卡的副本。

     dd if=/dev/sdc of=/path/to/file/myimage.img
    

在Linux上缩小图像

问题背景:

如果myimage.img比硬件支持的大小要大(如果它更小,就不会有问题;然而,使用相同的策略,您可以更好地适应硬件支持中的图像)。

秘诀是使用标准的Linux工具和设备:GParted、fdisktruncate

要求:

  • 一台Linux电脑
  • 要缩小的.img文件(在本例中为myimage.img

创建回环设备:

GParted是一个通常用于管理分区表和文件系统的应用程序。为了缩小图像,我们将首先使用GParted来完成这个步骤。

GParted操作的是设备,而不是像图像这样的简单文件。这就是为什么我们首先需要为图像创建一个设备。我们使用Linux的回环功能来实现这一点。

让我们启用回环功能:

sudo modprobe loop

让我们请求一个新的(免费)回环设备:
sudo losetup -f

该命令返回一个空闲环回设备的路径:
/dev/loop0

让我们创建一个图像设备:
sudo losetup /dev/loop0 myimage.img

设备/dev/loop0代表着myimage.img。我们想要访问镜像中的分区,因此我们需要请求内核也加载它们:
sudo partprobe /dev/loop0

这应该给我们提供设备/dev/loop0p1,它代表了myimage.img中的第一个分区。我们不直接需要这个设备,但是GParted需要它。
使用GParted调整分区大小:
让我们使用GParted加载新设备:
sudo gparted /dev/loop0

当GParted应用程序打开时,应该出现一个类似下面的窗口。

screenshot

现在注意几点:
- 有一个分区。 - 分区分配了整个磁盘/设备/镜像。 - 分区部分填充。
我们想要调整这个分区的大小,使其适应内容,但不超过所需。
选择该分区并点击“调整/移动”。将弹出类似以下窗口:

screenshot of dialog

拖动右边的条到尽可能左边。
请注意,有时候GParted需要额外的几MB来存放一些与文件系统相关的数据。您可以按几次New size-box上的向上箭头来实现。例如,我按了10次(=10MiB)使FAT32工作。对于NTFS,您可能根本不需要这样做。
最后按下Resize/Move。您将返回到GParted窗口。这次它看起来会类似以下内容:

unallocated space on right

请注意,磁盘中有一部分未分配的空间。这部分磁盘不会被分区使用,因此我们可以稍后将其从镜像中删除。GParted是一个用于处理磁盘的工具,它不会缩小镜像,只能缩小分区,所以我们需要自己来缩小镜像。
在GParted中点击“应用”。它将移动文件并最终缩小分区,可能需要一两分钟的时间,但大多数情况下会很快完成。完成后关闭GParted。
现在我们不再需要回环设备了,所以卸载它:
sudo losetup -d /dev/loop0

修整图像:
既然我们已经在图像的开头拥有了所有重要数据,现在是时候修整掉那部分未分配的内容了。首先,我们需要知道我们的分区在哪里结束,未分配的部分从哪里开始。我们通过使用`fdisk`来完成这个任务:
fdisk -l myimage.img

在这里我们将会看到类似下面的输出:
Disk myimage.img: 6144 MB, 6144000000 bytes, 12000000 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
Disk identifier: 0x000ea37d

      Device Boot      Start         End      Blocks   Id  System
myimage.img1            2048     9181183     4589568    b  W95 FAT32

在输出中注意两件事:

  • 分区结束于块9181183(显示为End下方)
  • 块大小为512字节(显示为1 * 512的扇区)

我们将在示例的其余部分使用这些数字。块大小(512)通常是相同的,但结束块(9181183)对您来说可能会有所不同。这些数字意味着分区以文件的第9181183*512字节结束。在该字节之后是未分配的部分。只有前9181183*512字节对我们的图像有用。

接下来,我们将缩小图像文件的大小,使其刚好包含分区。为此,我们将使用truncate命令(感谢uggla!)。使用truncate命令需要提供文件的大小(以字节为单位)。最后一个块是9181183,块编号从0开始。这意味着我们需要(9181183+1)*512字节。这一点很重要,否则分区将无法适应图像。因此,现在我们使用计算结果来执行truncate命令:

truncate --size=$[(9181183+1)*512] myimage.img

(*) 看起来FrozenCow的原始帖子已经移动在这里

SD卡没有进行碎片整理。你只是从最后写入的地方开始切割。这可能只有几MB,尽管实际上SD卡上可能有几GB的空闲空间。按照你的操作步骤,这些空间将被跳过。 - Micha93
@Micha93:我猜你是指你的镜像有两个分区,通过fdisk命名为xxx.img1和xxx.img2。如图所示,第一个镜像占据扇区8192到93802,而第二个镜像位于扇区98304到30375935。对于这个例子,你应该使用最后一个分区的结束扇区,即30375935:truncate --size=$[(30375935+1)*512] xxx.img - cootje
GPT 包括磁盘开头的主分区表和磁盘末尾的备份分区表(默认情况下占用 33 个扇区)。为了在 GPT 上使用截断命令,需要将截断命令增加 33 个扇区:truncate --size=$[(9181183+1+33)*512] myimage.img - AndreyP
如果 GPT 备份分区表在磁盘末尾已被移除,有一个修复方法。使用 dd if=/dev/zero bs=512 count=33 >> myimage.img 添加 33 个扇区。然后使用 gdisk myimage.img 进行修复,并使用 w 命令。 - AndreyP

所选答案完美适用于dos磁盘标签类型。对于GPT类型,需要考虑在磁盘末尾添加33个扇区,因为GPT也在其中存储了一个表。
因此,对于GPT用户,“截断”命令应该如下所示:
truncate --size=$[(End_of_last_partition+1+33)*512] myimage.img
这将在fdisk -l中产生一个GPT错误。要修复此问题,请运行以下命令:
gdisk myimage.img

运行命令以验证磁盘: v。您应该看到磁盘上发现了一些错误。
在gdisk中,您需要输入x进入专家菜单,然后输入e将备份的分区表数据移动到磁盘的新末尾,最后输入w将更改写入磁盘。[来源] 如何截断未使用空间的磁盘映像文件而不损坏GPT分区表(结束指针) - 谢谢!

看了很多方法,我都理解了,但是其中的步骤让我有些不太愿意去执行,因为要按顺序逐步缩小一个树莓派IMG文件。所以我继续寻找,然后发现了一个名为PiShrink的项目中的Bash脚本。
执行起来非常简单,并且在我的MacOS M1 Mac Mini上也能正常运行。
背景故事
在我这个特殊情况下,我有两张32GB的SD卡,但由于某种原因,其中一张比另一张稍微小一点(大约95MB),所以balenaEtcher拒绝将IMG从看似更大的SD卡写入到较小的SD卡中。
我知道我只使用了32GB中的大约10GB,所以我可以缩小IMG文件的大小来适应。
步骤
首先,我有以下文件。.zip文件包含了.img文件,但我在下面展示的是解压后的文件。
$ ls -lh | grep -E 'img|zip'
-rw-r--r--  1 root staff  28G Jan 29 13:16 backup.zip
--w--wx-w-  1 slm  staff  30G Jan  1  1980 disk.img

我随后下载了PiShrink的shell脚本并将其设置为可执行文件。
$ wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
$ chmod +x pishrink.sh

我然后这样运行它:

$ sudo pishrink disk.img pi.img
Copying disk.img to pi.img...
e2fsck 1.44.0 (7-Mar-2018)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
STORAGE: 69402/7684096 files (0.2% non-contiguous), 9562823/30732288 blocks
resize2fs 1.44.0 (7-Mar-2018)
resize2fs 1.44.0 (7-Mar-2018)
Resizing the filesystem on /dev/disk4s2 to 9273700 (1k) blocks.
Begin pass 2 (max = 245358)
Relocating blocks             XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Begin pass 3 (max = 3752)
Scanning inode table          XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Begin pass 4 (max = 3420)
Updating inode references     XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
The filesystem on /dev/disk4s2 is now 9273700 (1k) blocks long.

"disk4" ejected.
fdisk: could not open MBR file /usr/standalone/i386/boot0: No such file or directory
Enter 'help' for information
fdisk: 1>          Starting       Ending
 #: id  cyl  hd sec -  cyl  hd sec [     start -       size]
------------------------------------------------------------------------
 2: 83 1023   3  32 - 1023 254   2 [   1056768 -   61464576] Linux files*
Partition id ('0' to disable)  [0 - FF]: [83] (? for help) Do you wish to edit in CHS mode? [n] Partition offset [0 - 62521344]: [1056768] Partition size [1 - 61464576]: [61464576] fdisk:*1> Writing MBR at offset 0.
fdisk: 1> Shrunk pi.img from 30G to 9.3G

生成的IMG文件现在大约有9GB。
$ ls -lh | grep -E 'img|zip'
-rw-r--r--  1 root staff  28G Jan 29 13:16 backup.zip
--w--wx-w-  1 slm  staff  30G Jan  1  1980 disk.img
--w---x---  1 root staff 9.4G Jan 29 17:04 pi.img

我随后将其重新打包为ZIP文件,只是为了在处理磁盘上的文件时保持最高效的大小。
$ sudo zip pi.zip pi.img

我们现在有以下一套文件。
$ ls -lh | grep -E 'img|zip'
-rw-r--r--  1 root staff  28G Jan 29 13:16 backup.zip
--w--wx-w-  1 slm  staff  30G Jan  1  1980 disk.img
--w---x---  1 root staff 9.4G Jan 29 17:04 pi.img
-rw-r--r--  1 root staff 8.3G Jan 29 17:08 pi.zip

我可以使用balenaEtcherApplePi-Baker将ZIP文件刻录到“较小”的SD卡上。

ss01

在我的Mac上检查SD卡,我可以看到它显示大约10GB的容量。
$ diskutil list /dev/disk4
/dev/disk4 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *31.9 GB    disk4
   1:             Windows_FAT_32 LIBREELEC               536.9 MB   disk4s1
   2:                      Linux                         10.0 GB    disk4s2
                    (free space)                         21.3 GB    -

注意:我可以在Linux系统中使用lsblk等命令执行相同的操作。

后续操作

在使用上述方法后,我注意到SD卡中的文件系统不足32GB。主要显示如下:

$ df -h | grep -E "File|mmc"
Filesystem                Size      Used Available Use% Mounted on
/dev/mmcblk0p1          511.7M    141.7M    370.0M  28% /flash
/dev/mmcblk0p2            8.6G      8.2G    377.4M  96% /storage

这个问题应该很容易解决,只需要在重新启动后从内部扩展SD文件系统即可。
首先,我使用parted来扩展分区。
$ parted /dev/mmcblk0 resizepart 2 100%
Warning: Partition /dev/mmcblk0p2 is being used. Are you sure you want to
continue?
Yes/No? yes
yes
Information: You may need to update /etc/fstab.

我随后重新启动了系统。
$ reboot

在我使用resize2fs在线扩展文件系统后:
$ resize2fs /dev/mmcblk0p2
resize2fs 1.45.6 (20-Mar-2020)
Filesystem at /dev/mmcblk0p2 is mounted on /storage; on-line resizing required
old_desc_blocks = 71, new_desc_blocks = 234
The filesystem on /dev/mmcblk0p2 is now 30638592 (1k) blocks long.


现在它显示了SD卡上的所有可用空间。
$ df -h | grep -E "File|mmc"
Filesystem                Size      Used Available Use% Mounted on
/dev/mmcblk0p1          511.7M    141.7M    370.0M  28% /flash
/dev/mmcblk0p2           28.3G      8.2G     20.1G  29% /storage

你可以在dd命令中使用bscount选项来限制输出文件的大小。
示例:
dd if=sdx of=SD.img bs=1G count=4

会导致一个大小为4 GiB的输出文件。

深入研究一下man dd

您需要知道要复制多少字节,以便完全覆盖所有分区,请使用sudo fdisk -l /dev/sdx查看最后一个所需的扇区。

分区需要位于磁盘的开头(就像您提供的图片中那样)。

使用msdos分区表的磁盘可以很容易地进行克隆,但如果磁盘使用GPT并且要克隆到大小不同的磁盘,则需要在之后调整保护性MBR,并重新创建位于磁盘末尾的GPT备份,这可以使用gdisk完成。


从您的fdisk输出中,您可以看到最后一个分区的最后一个扇区是扇区8595455,这意味着您至少需要复制8595455+1个扇区(第一个扇区为0)。以512字节为扇区大小,这相当于4,400,873,472字节。bs乘以count必须大于或等于这个值。
也许对于一个4GB的USB闪存驱动器来说,这仍然太大了,您可以缩小sdc2的大小,因为它还有很多未使用的空间。
对于您提供的当前示例,
 dd if=/dev/sdc of=SD.img bs=10M count=420

将会涵盖分区表、sdc1sdc2。计算:
10*1024*1024*420 = 4,404,019,200 > 4,400,873,472

测试解决方案。一个问题:你说的“你需要知道要复制多少字节,以便所有分区都被完全覆盖,所以用sudo fdisk -l /dev/sdx查看一下最后一个扇区是哪个。分区需要位于磁盘的开头(就像你提供的图片中那样)。”另外,我正在更新问题,并附上你所说命令的输出。 - Leos313
你不想克隆空白空间,所以分区应该位于磁盘的开头。而且你不想克隆分区(文件系统)的一部分,所以你需要知道磁盘上最后一个分区结束的扇区位置。 - mook765
这是我找到的解决方案,详细说明了命令和图片,并且它有效。这个答案类似,但也使用了truncate和回环设备。它正常工作。https://softwarebakery.com//shrinking-images-on-linux一旦这个答案也更新了,我会点赞的,它是找到解决方案的关键。 - Leos313

resize2fs也可以用来调整大小。

sudo resize2fs -fp SD.img 4G

它还可以调整文件的大小!

2听起来很有希望,但对我没有用:“错误的魔数”和“坏的超级块”,也许是因为我有多个分区在那里?不过较长的方法确实有效。 - Mr.Gosh
@Mr.Gosh 当然,那个.img看起来像是一个分区/文件系统的镜像,而不是完整的SD卡镜像。 - golimar