使用df命令看到的分区大小是正确的吗?

在使用Linux时,通常会用到df命令来查看磁盘挂载情况,那么df的输出结果可信吗?一般用户似乎不应怀疑,作者以前也从没怀疑过,直到发生了下面的这些事情。

df报告的分区大小比分区表定义的大时

在使用某芯片厂商的SDK开发产品时,检查设备上的eMMC使用情况看到有下面的情况:

1
2
3
4
5
6
7
8
9
~# df -Th -x tmpfs -x devtmpfs
Filesystem Type Size Used Avail Use% Mounted on
/dev/root ext4 1.9G 622M 1.1G 36% /
/dev/mmcblk0p36 vfat 1.0G 0 1.0G 0% /bluetooth
/dev/mmcblk0p25 vfat 95M 19M 77M 20% /firmware
/dev/mmcblk0p26 ext4 12M 4.2M 7.4M 36% /dsp
/dev/mmcblk0p42 ext4 28M 108K 27M 1% /persist
/dev/mmcblk0p41 ext4 58M 3.1M 54M 6% /cache
/dev/mmcblk0p50 ext4 3.4G 1.4G 2.0G 41% /data

一个名为bluetooth的分区竟然分配了1G的空间,那么,这里面究竟包含了什么呢?使用ls -a命令查看/bluetooth目录没有任何内容。回看厂商烧录工具使用的配置文件时,发现有这样的配置:

<program SECTOR_SIZE_IN_BYTES="512" file_sector_offset="0" filename="BTFM.bin" label="bluetooth" num_partition_sectors="2047" partofsingleimage="false" physical_partition_number="0" readbackverify="false" size_in_KB="1023.5" sparse="false" start_byte_hex="0xd8468000" start_sector="7086912" />

从描述上看这是个存放Bluetooth Firmware文件的分区,但定义的分区大小只有1023.5K,占用2047个扇区,这显然与df的输出不匹配,那这个分区的信息被Linux Kernel识别为怎样的?为此,做了下面的一系列试验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
~# cat /sys/class/block/mmcblk0p36/size         # 查看mmcblk0p36的扇区数
2048
~# cat /sys/class/block/mmcblk0p36/start # 查看mmcblk0p36的起始扇区
7086912
~# ls -Slh /usr/lib64/ | head -n 2 # 寻找一个稍大的文件
total 326M
-rw-r--r-- 1 root root 56M Jul 31 2020 libQt5Bootstrap.a
~# cp /usr/lib64/libQt5Bootstrap.a /bluetooth/ # 将文件复制到/bluetooth目录
~# ls -lh /bluetooth/
total 56M
-rwxr-xr-x 1 root root 56M Aug 20 2019 libQt5Bootstrap.a
~# diff /usr/lib64/libQt5Bootstrap.a /bluetooth/libQt5Bootst>
~# sync
~# diff /usr/lib64/libQt5Bootstrap.a /bluetooth/libQt5Bootstrap.a
~# sync /bluetooth/libQt5Bootstrap.a
sync: error syncing '/bluetooth/libQt5Bootstrap.a': Input/output error
~# diff /usr/lib64/libQt5Bootstrap.a /bluetooth/libQt5Bootstrap.a
~# echo $?
0
~# reboot
~# diff -q /usr/lib64/libQt5Bootstrap.a /bluetooth/libQt5Bootstrap.a
Files /usr/lib64/libQt5Bootstrap.a and /bluetooth/libQt5Bootstrap.a differ

从上面的测试中可以看到,cat命令读到的起始扇区能与配置文件中对应,但读到的扇区数多了1个扇区,猜测是扇区对齐导致。接下来通过复制大于分区表大小的文件进行试验,cp命令的输出表明复制成功,将原文件与目标文件比较,diff命令的输出表明文件内容一致,考虑到Linux的缓冲机制,文件并不一定写入了eMMC,于是又执行了一遍sync命令后再次比较,diff的输出仍旧表明文件内容一致,但给sync命令指定目标文件时,sync给出了一条错误消息,这表明不能把文件刷写到eMMC。继续比较,diff的输出仍旧表明文件内容一致,可见,文件始终是保留在缓冲区里,并没有写入到eMMC。为验证这一观点,将设备重启后再度比较,这次diff报了两个文件是不同的。

看来,mmcblk0p36分区确实只有2048个扇区,无法写入这么大的文件,那么,为什么df命令报出来是1G的,想来只有配置文件中的BTFM.bin导致的了,在Ubuntu上使用file命令查看BTFM.bin:

1
2
$ file BTFM.bin
BTFM.bin: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "MSDOS5.0", sectors/cluster 32, root entries 512, Media descriptor 0xf8, sectors/FAT 257, sectors/track 63, heads 255, sectors 2097152 (volumes > 32 MB) , reserved 0x1, serial number 0xbc614e, unlabeled, FAT (16 bit)

file命令报告BTFM.bin的总扇区数有2097152个,以512字节为1扇区,2097152个扇区数恰巧是1G的空间,到此基本上可以确定是这个BTFM.bin导致的问题,当它写入设备的eMMC后,Linux从相应的分区读取了这些数据作为文件系统信息,并据此挂载,于是df等使用了/proc/self/mountinfo数据的程序都认为此分区有1G空间,但在实际写入时,Kernel却知道没有足够的扇区来存放数据,于是报了错误。

修复此问题也很简单,只需要提供一个大小匹配实际的BTFM.bin就可以了,以Kernel报的2048个扇区为准,在Ubuntu上重新生成一个用于烧录的vfat.bin文件的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ dd if=/dev/zero of=vfat.bin bs=512 count=2048 # 产生一个2048个扇区大小的文件
$ mkfs.vfat -s 32 -r 512 vfat.bin # 将此文件格式化为vfat格式
mkfs.fat 4.1 (2017-01-24)
$ file vfat.bin
vfat.bin: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", sectors/cluster 32, root entries 512, sectors 2048 (volumes <=32 MB) , Media descriptor 0xf8, sectors/FAT 1, sectors/track 32, heads 64, serial number 0x51a31e42, unlabeled, FAT (12 bit)
$ mkdir vfat/
$ sudo mount -t vfat -o loop vfat.bin vfat
$ mkdir btfm/
$ sudo mount -t vfat -o loop BTFM.bin btfm/
$ #sudo cp btfm/* vfat/ # 若原bin文件包含了内容,应当复制到新的bin文件中
$ sync vfat/
$ sudo umount vfat # 此后vfat.bin将包含BTFM.bin的内容,并具有适当大小
$ sudo umount btfm

为什么BTFM.bin是16位的FAT表,而vfat.bin是12位的FAT表,因为2048个扇区的空间太小,已经不足以使用16位的FAT表。将新生成的vfat.bin替换为原BTFM.bin并刷写到设备验证,可以看到df命令将输出除去文件系统自身信息后的正确大小,cp命令复制超出分区表空间大小的文件到分区时也将报错:

1
2
3
4
5
6
7
8
9
10
11
12
~# df -Th -x tmpfs -x devtmpfs
Filesystem Type Size Used Avail Use% Mounted on
/dev/root ext4 1.9G 622M 1.1G 36% /
/dev/mmcblk0p36 vfat 992K 16K 976K 2% /bluetooth
/dev/mmcblk0p25 vfat 95M 19M 77M 20% /firmware
/dev/mmcblk0p26 ext4 12M 4.2M 7.4M 36% /dsp
/dev/mmcblk0p50 ext4 3.4G 1.4G 2.0G 41% /data
/dev/mmcblk0p41 ext4 58M 3.1M 54M 6% /cache
/dev/mmcblk0p42 ext4 28M 108K 27M 1% /persist

~# cp /usr/lib64/libQt5Bootstrap.a /bluetooth/
cp: error writing '/bluetooth/libQt5Bootstrap.a': No space left on device

df报告的分区大小比分区表定义的小时

在上面,已经看到了df报告的分区大小比分区表定义的大时是怎样的情况,当df报告的分区大小比分区表中小时,又将如何?正巧的是,/分区就是这样的一种情况,查看烧录工具的配置文件可以看到这样的配置:

<program SECTOR_SIZE_IN_BYTES="512" file_sector_offset="0" filename="rootfs.ext4" label="system" num_partition_sectors="6291455" partofsingleimage="false" physical_partition_number="0" readbackverify="false" size_in_KB="3145727.5" sparse="false" start_byte_hex="0xcf3a000" start_sector="424400" />

配置文件里定义的大小是6291455个扇区,3145727.5KB,也就是2.99G,查看sys文件系统中的信息验证:

1
2
3
4
# mount | grep "/ "
/dev/mmcblk0p18 on / type ext4 (rw,relatime,data=ordered)
# cat /sys/class/block/mmcblk0p18/size
6291456

同样是比配置文件中定义的多了1个扇区,但df命令报告的总大小是1.9G,用掉了622M,若以2.99G为限,剩余应有(2.99G-622M)大小,那此时能复制超过1.9G的文件进去吗?试验一下就知道了:

1
2
3
4
~# ls -lh /media/usb1/rootfs.ext4
-rwxr-xr-x 1 root root 2.1G Aug 1 2020 /media/usb1/rootfs.ext4
~# cp /media/usb1/rootfs.ext4 /
cp: error writing '/rootfs.ext4': No space left on device

cp命令报告没有足够的空间来复制,有1G多的空间丢失了。

要修复此问题,仍旧是生成正确大小的文件系统镜像,对于ext4文件系统镜像来讲,可以用Android SDK中的make_ext4fs命令来生成的,此例中,如果要利用上分区表定义的大小,在Ubuntu上这样操作就可以了:

1
2
3
$ mkdir rootfs
$ sudo mount -t ext4 -o loop rootfs.ext4 rootfs
$ sudo make_ext4fs -l 3145728K rootfs-raw.ext4 rootfs

上述命令使用原文件系统镜像再生成rootfs-raw.ext4文件,并指定对应到6291456个扇区的大小,产生的rootfs-raw.ext4会有3G大,若烧写工具支持Android Sparse格式,则可以使用sudo make_ext4fs -s -l 3145728K rootfs-sparse.ext4 rootfs这样的命令产生一个较小的文件。将这个文件刷回设备上验证,可以看到df命令将输出除去文件系统自身信息后的正确大小,cp命令复制2.1G大文件到/分区时正确完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
~# df -Th -x tmpfs -x devtmpfs
Filesystem Type Size Used Avail Use% Mounted on
/dev/root ext4 3.0G 646M 2.3G 22% /
/dev/mmcblk0p36 vfat 1.0G 56M 969M 6% /bluetooth
/dev/mmcblk0p26 ext4 12M 4.2M 7.4M 36% /dsp
/dev/mmcblk0p42 ext4 28M 108K 27M 1% /persist
/dev/mmcblk0p25 vfat 95M 19M 77M 20% /firmware
/dev/mmcblk0p41 ext4 58M 3.1M 54M 6% /cache
/dev/mmcblk0p50 ext4 3.4G 1.4G 2.0G 41% /data
/dev/sda1 vfat 15G 15G 199M 99% /media/usb1
~# ls -lh /media/usb1/rootfs.ext4
-rwxr-xr-x 1 root root 2.1G Aug 1 2020 /media/usb1/rootfs.ext4
~# cp /media/usb1/rootfs.ext4 /
~# sync /rootfs.ext4
~# diff -q /media/usb1/rootfs.ext4 /rootfs.ext4
~# reboot
~# diff -q /media/usb1/rootfs.ext4 /rootfs.ext4
~# echo $?
0

总结

  1. df、cp等命令对于产品开发者来说并不是那么可靠的,开发者正是要通过设计正确的系统来保证这些命令能正常工作

  2. 不带参数的sync命令是不可靠的,若要确认某个文件写入到存储设备上,最好带上具体的路径。在《UNIX环境高级编程》第三版的3.13节中,对sync类函数有这样的描述:sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。不带参数的sync命令调用这个函数。fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fdatasync函数类似于fsync,但它只影响文件的数据部分。

  3. 若使用mkfs.XXX去格式化分区,这些命令都能正确识别分区大小,并格式化为恰当的文件系统

参考

  • 《UNIX环境高级编程》第三版