暗色模式

Linux 稀疏文件:从 truncate 预分配到 du 与 ls 大小不一致

技术教程
2026-09-20
8
0
本文要点
  • 同一个文件,ls -lh 说 512M、du -h 说 0,两个数都是对的:前者读的是 inode 里的逻辑长度,后者数的是真正分配出去的块。差距来自空洞(hole)——稀疏文件的核心概念
  • 两种「造大文件」的手法是反的:truncate -s 512M 只改长度、一个块都不占(实测 stat 的 blocks=0、filefrag0 extents found);fallocate -l 512M真预分配,占用立刻变 513M
  • 空洞会不会被填实,取决于你用什么工具搬运——实测同一台机器(coreutils 8.32 / tar 1.34 / rsync 3.2.7):cp 默认保留空洞、cp --sparse=never 填实、tar -cf 默认填实(513M)、tar -cSf 保留(12K)、rsync -a 填实rsync -aS 保留。备份和迁移场景下这组差异直接决定磁盘会不会被撑爆
  • fallocate -d 能反向打洞回收空间:实测把一个 513M 的实心文件前 256M 打掉后,占用降到 256M
  • ⚠️ 稀疏不是压缩:读出来的仍是 512M 个零字节,只是磁盘上没真的存。别用它糊弄磁盘配额检查
  • 判断口径要统一:du 看真实占用、du --apparent-size 看逻辑长度、ls -ls 第一列是块数(coreutils 默认按 1024 字节/块)、stat -c %b 是 512 字节扇区数——本机实测同一个 100M 实心文件这两者分别是 102400 和 204800

Linux 稀疏文件:从 truncate 预分配到 du 与 ls 大小不一致

在一个 1.9G 内存、7.6G 磁盘的小机器上,最怕的就是空间莫名其妙没了。而 Linux 上有一个反直觉的现象会让人彻底怀疑人生:ls -lh 显示某个文件 512M,du -h 却说它占用 0 字节——两个数都是对的

这就是稀疏文件(sparse file):文件有一副「512M 长」的躯壳,里面却全是空气。它既是最省空间的技巧,也是备份、同步、迁移时最容易踩的坑。本文在一台 Ubuntu 22.04 服务器(ext4、coreutils 8.32、GNU tar 1.34、rsync 3.2.7、util-linux 2.37.2)上把稀疏文件的造、看、搬、收四个环节全跑了一遍,下面所有数字都来自这次实测。

长度和占用,本来就是两回事

一个文件在 inode 里有两个互不相干的账本:逻辑长度stat 里的 size)和块分配表(记录数据实际落在哪些磁盘块上)。往文件里写数据,两个账本一起涨;而如果只是把长度「撑大」,比如 truncate,内核只改前者,一块磁盘都不用动——中间那段没有数据的区间就叫空洞

cd /tmp
truncate -s 512M sparse.img
ls -lh sparse.img
du -h sparse.img
stat -c 'size=%s bytes / blocks=%b x %B' sparse.img

truncate 造出的 512M 稀疏文件:ls -lh 显示 512M,du 显示 0,stat 的 blocks 为 0

三个数字连起来读:ls -lh 的 512M 是逻辑长度;du 的 0 是真实占用;stat 直接给出了两个原始字段——size=536870912 bytes(长度)配 blocks=0 x 512(分配出去的 512 字节扇区数为 0)。文件确实「有 512M 那么长」,也确实「一个字节都没占」。

再补一个工具,filefrag 从文件系统的角度看得更直白:

filefrag -v sparse.img | head -6
Filesystem type is: ef53
File size of sparse.img is 536870912 (131072 blocks of 4096 bytes)
sparse.img: 0 extents found

0 extents found——整个文件在 ext4 的 extent 树上一个区间都没有,百分百是空洞。读取它的时候,内核遇到没有对应块的区间就直接返回 0 字节,应用程序完全无感;这也解释了为什么用 headcatdd 读一个稀疏文件,读到的全是 \0

这里有个单位陷阱。 同样是「看占用」,三个命令的默认口径并不一样:

命令单位100M 实心文件实测值
ls -ls块(coreutils 默认按 1024 字节计)102400
stat -c %b512 字节扇区204800
du -h人类可读的真实占用100M

ls -lsstat %b 的量纲差一倍,混着用会得出「占用凭空翻倍」的错觉。要判断一个文件是不是稀疏文件,最省事的组合是 du -hdu -h --apparent-size——实测同一个 sparse.img,前者 0、后者 512M,两个数不一样就说明有空洞。

造稀疏文件,和它的反面:预分配

truncate 是造稀疏文件最快的方式。另一种常见写法是用 dd 跳过前面一段再写一个块,效果一样——长度 256M,占用只有 1M:

dd if=/dev/zero of=dd.img bs=1M count=1 seek=255 status=none
du -h dd.img
ls -ls dd.img
1.0M    dd.img
1024 -rw-r--r-- 1 root root 268435456 dd.img

而它的反义词是 fallocate——真金白银地把块分配出来,但文件内容仍然是空的(ext4 上分配的是「未写入的 extent」,读到的是 0,但块已经算这个文件的了):

cd /tmp
fallocate -l 512M alloc.img
cp sparse.img copy-auto.img
cp --sparse=never sparse.img copy-full.img
du -h alloc.img copy-auto.img copy-full.img

fallocate 预分配的 512M 占用 513M,默认 cp 拷贝稀疏文件仍然是 0,加 --sparse=never 后变成 513M

这一屏其实是三件事挤在一起:

  • alloc.img 占用 513Mfallocate 不给空洞,长度多少就占多少(比 512M 略大是块对齐和元数据的正常损耗);
  • copy-auto.img 占用 0cp 的默认行为(--sparse=auto)会保留空洞——注意这条依赖文件系统支持 SEEK_HOLE 探测,老版本 coreutils 或不支持的文件系统上会退化成实心拷贝;
  • copy-full.img 占用 513M--sparse=never 老老实实把每个空洞都写成真实的零块。

为什么要有 fallocate 这种「浪费」的操作? 因为在真实业务里,「写到一半发现磁盘满了」比「多占点空间」可怕得多:数据库的预写日志、虚拟机的磁盘镜像、消息队列的持久化文件,通常都会开场就按预期上限预分配,换来的是写入过程不会因为 ENOSPC 中断,以及连续块带来的顺序 IO 性能。反过来,临时造测试文件、做「一块临时硬盘」时,稀疏才是首选——同样是 512M 的长度,稀疏版瞬间完成、零占用,fallocate 版要真的写盘。

顺带说一句,truncate 出来的「空气文件」并不是永远不占空间:一旦你真的往里写数据,写到哪里、哪里的块才会被真正分配。所以「用 truncate 造一个大文件」不能用来骗过磁盘空间检查——df 看得清清楚楚(本次实验期间,fallocate 一处就让 df 的已用空间从 2.6G 涨到 3.1G,cp --sparse=never 再把它推到 3.6G)。

搬动稀疏文件:默认行为各不相同

这是最容易出事的一节。稀疏文件在搬运(复制、打包、同步、上传)时的表现,完全取决于工具,而且这几个工具的默认值方向相反

cd /tmp
tar -cf plain.tar sparse.img
tar -cSf sparse.tar sparse.img
du -h plain.tar sparse.tar

tar 默认打包把 512M 空洞全部写实(513M),加 -S 后只有 12K

同一个 512M 的稀疏文件,tar -cf 打出来的包占用 513M,加上 -S--sparse)之后只有 12K——差了四万多倍。GNU tar 的默认行为是不保留空洞,-S 才会把它写成稀疏归档。

把实测结果整理成一张表,按「是否保留空洞」排序:

命令是否保留空洞512M 稀疏文件的真实占用
cp(默认 --sparse=auto保留0
cp --sparse=never填实513M
cp --sparse=always反向造洞:64M 全零文件 → 00
rsync -a填实64M(64M 全零文件实测)
rsync -aS保留0
tar -cf填实513M
tar -cSf保留12K
gzip(对填实后的 tar)压缩,不是稀疏521130 字节

两个高频踩坑场景:

一是备份。 rsync -a 是绝大多数人的肌肉记忆,但它在默认参数下会把稀疏文件读成 512M 个零字节再写出去。本地盘对本地盘还只是浪费空间,一旦目标是网络存储或远端服务器,等于白传 512M 流量。习惯写法是 rsync -aHS-S 保留稀疏,-H 保留硬链接)。

二是打包上传。 tar -cf 打出来的实心包,压缩后体积会回落(本次 513M 的包 gzip 后 521130 字节),但中间过程要有地方放那 513M 的实心包——在磁盘本来就紧张的小机器上,这一步就足以撑爆 /tmp。用 tar -cSf 直接从源头避免。

反向操作:给文件打洞,把空间收回来

fallocate -d--dig-holes)能扫描文件里的零块,把它们释放成空洞,空间立刻还给你:

fallocate -d -o 0 -l 256M copy-full.img
du -h copy-full.img
filefrag copy-full.img | tail -2
256M    copy-full.img
copy-full.img: 3 extents found

513M 的实心文件,把前 256M 打洞之后占用降到 256M。这在真实场景里对应两类需求:一是日志/数据文件里大段大段的零(比如预分配后没用完的数据库文件)可以回收;二是从模板镜像裁剪出「真正有数据的部分」。

打洞是文件系统能力,不是通用操作。 ext4、XFS、Btrfs 支持 punch hole(本机 ext4 实测有效),NFS、部分老文件系统和某些网络存储不支持,会直接报错或者静默退化成空操作。另外虚拟机镜像(qcow2)自带一套稀疏/回收机制,对镜像文件手动 fallocate -d 通常没有意义,那类场景应该用 qemu-img convertfstrim 这类对口工具。

什么时候会用到它

  • 虚拟机与云镜像truncateqemu-img create 造出来的 raw 镜像天生稀疏,写多少占多少;反过来把镜像复制来复制去时,cp --sparse=never 或者不认空洞的传输工具会让它「胖」成完整大小;
  • 容器镜像层:镜像层是本地的目录树,层与层之间的文件系统打包/解包默认就要处理稀疏,才能让一个 1G 的镜像在磁盘上只占几百兆;
  • 「临时硬盘」:需要一块 ext4 做实验时,truncate -s 1G 造镜像瞬间完成,写满才占 1G(dd 命令:从磁盘克隆到数据备份的完整指南conv=sparse 那类参数也是同一个道理);
  • 测试数据:造一个「看起来 10G」的大文件测试程序对超大文件的处理,稀疏文件是唯一不心疼磁盘的做法;
  • 系统瘦身:给已经填实的零块文件打洞回收空间。

判断与避坑清单

先确认口径,再下结论:

  • 真实占用:du -h <文件>
  • 逻辑长度:du -h --apparent-size <文件>ls -l
  • 有没有空洞:filefrag -v <文件>(看 extents 数量)、stat -c '%s %b'(size 与 blocks 是否匹配);
  • 批量找稀疏文件:find /path -type f -size +100M -exec du -h --apparent-size {} \; -exec du -h {} \; 对比两列。

四个必须记住的坑:

  1. 搬运工具的默认值方向不一致cptarrsync 三家的默认行为在本机实测里就分成了两派,跨机器、跨版本还可能变化(cp --sparse=auto 依赖 SEEK_HOLE 支持),备份脚本里显式写参数比赌默认值靠谱;
  2. 稀疏不等于压缩:读出来永远是 512M 个零字节,网络传输前该压缩还是要压缩;
  3. truncate 造的文件看着大,cp --sparse=never 造的文件是实打实的大:本次实验里后者一次性吃掉 513M 磁盘,小机器上要养成「搬完就看一眼 dudf」的习惯;
  4. dfdu 对不上是另一码事:文件系统元数据、journal、已删除但被进程占用的文件都会造成差额,处理思路见 Linux 磁盘空间排查与清理:从 df 到 journald 的完整救火流程

小结

三句话记住稀疏文件:

  1. 长度 ≠ 占用ls -l 是账面上的长度,du 是真实开销,中间差的是空洞(ls -ls 的块数、stat 的 blocks、filefrag 的 extents 都能验证);
  2. 造洞用 truncate/dd seek,实心预分配用 fallocate,二者的用途正好相反——前者为了省,后者为了稳;
  3. 搬运用 -Star -cSfrsync -aS,忘了它就可能把一个 512M 的空壳变成 513M 的真包。

想把大文件看清楚,filestat 这类基础工具是第一步(Linux 二进制分析:从 file 类型识别到 ELF 结构与 objdump 反汇编);如果关心的是「这个文件是不是被别人改过」,那要换校验和那一套(Linux 文件完整性校验:从 sha256sum 到 dpkg -V 篡改排查);而把一个稀疏文件搬进备份盘时该留意什么,见 Linux tar 打包与压缩:从归档原理到增量备份

发表评论

暂无评论,快来抢沙发吧!