本文要点
- 同一个文件,
ls -lh说 512M、du -h说 0,两个数都是对的:前者读的是 inode 里的逻辑长度,后者数的是真正分配出去的块。差距来自空洞(hole)——稀疏文件的核心概念 - 两种「造大文件」的手法是反的:
truncate -s 512M只改长度、一个块都不占(实测stat的 blocks=0、filefrag报0 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
三个数字连起来读:ls -lh 的 512M 是逻辑长度;du 的 0 是真实占用;stat 直接给出了两个原始字段——size=536870912 bytes(长度)配 blocks=0 x 512(分配出去的 512 字节扇区数为 0)。文件确实「有 512M 那么长」,也确实「一个字节都没占」。
再补一个工具,filefrag 从文件系统的角度看得更直白:
filefrag -v sparse.img | head -6Filesystem type is: ef53
File size of sparse.img is 536870912 (131072 blocks of 4096 bytes)
sparse.img: 0 extents found0 extents found——整个文件在 ext4 的 extent 树上一个区间都没有,百分百是空洞。读取它的时候,内核遇到没有对应块的区间就直接返回 0 字节,应用程序完全无感;这也解释了为什么用 head、cat、dd 读一个稀疏文件,读到的全是 \0。
这里有个单位陷阱。 同样是「看占用」,三个命令的默认口径并不一样:
| 命令 | 单位 | 100M 实心文件实测值 |
|---|---|---|
ls -ls | 块(coreutils 默认按 1024 字节计) | 102400 |
stat -c %b | 512 字节扇区 | 204800 |
du -h | 人类可读的真实占用 | 100M |
ls -ls 和 stat %b 的量纲差一倍,混着用会得出「占用凭空翻倍」的错觉。要判断一个文件是不是稀疏文件,最省事的组合是 du -h 配 du -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.img1.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
这一屏其实是三件事挤在一起:
alloc.img占用 513M:fallocate不给空洞,长度多少就占多少(比 512M 略大是块对齐和元数据的正常损耗);copy-auto.img占用 0:cp的默认行为(--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
同一个 512M 的稀疏文件,tar -cf 打出来的包占用 513M,加上 -S(--sparse)之后只有 12K——差了四万多倍。GNU tar 的默认行为是不保留空洞,-S 才会把它写成稀疏归档。
把实测结果整理成一张表,按「是否保留空洞」排序:
| 命令 | 是否保留空洞 | 512M 稀疏文件的真实占用 |
|---|---|---|
cp(默认 --sparse=auto) | 保留 | 0 |
cp --sparse=never | 填实 | 513M |
cp --sparse=always | 反向造洞:64M 全零文件 → 0 | 0 |
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 -2256M copy-full.img
copy-full.img: 3 extents found513M 的实心文件,把前 256M 打洞之后占用降到 256M。这在真实场景里对应两类需求:一是日志/数据文件里大段大段的零(比如预分配后没用完的数据库文件)可以回收;二是从模板镜像裁剪出「真正有数据的部分」。
打洞是文件系统能力,不是通用操作。 ext4、XFS、Btrfs 支持 punch hole(本机 ext4 实测有效),NFS、部分老文件系统和某些网络存储不支持,会直接报错或者静默退化成空操作。另外虚拟机镜像(qcow2)自带一套稀疏/回收机制,对镜像文件手动 fallocate -d 通常没有意义,那类场景应该用 qemu-img convert、fstrim 这类对口工具。
什么时候会用到它
- 虚拟机与云镜像:
truncate或qemu-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 {} \;对比两列。
四个必须记住的坑:
- 搬运工具的默认值方向不一致:
cp、tar、rsync三家的默认行为在本机实测里就分成了两派,跨机器、跨版本还可能变化(cp --sparse=auto依赖SEEK_HOLE支持),备份脚本里显式写参数比赌默认值靠谱; - 稀疏不等于压缩:读出来永远是 512M 个零字节,网络传输前该压缩还是要压缩;
truncate造的文件看着大,cp --sparse=never造的文件是实打实的大:本次实验里后者一次性吃掉 513M 磁盘,小机器上要养成「搬完就看一眼du和df」的习惯;df和du对不上是另一码事:文件系统元数据、journal、已删除但被进程占用的文件都会造成差额,处理思路见 Linux 磁盘空间排查与清理:从 df 到 journald 的完整救火流程。
小结
三句话记住稀疏文件:
- 长度 ≠ 占用:
ls -l是账面上的长度,du是真实开销,中间差的是空洞(ls -ls的块数、stat的 blocks、filefrag的 extents 都能验证); - 造洞用
truncate/dd seek,实心预分配用fallocate,二者的用途正好相反——前者为了省,后者为了稳; - 搬运用
-S:tar -cSf、rsync -aS,忘了它就可能把一个 512M 的空壳变成 513M 的真包。
想把大文件看清楚,file、stat 这类基础工具是第一步(Linux 二进制分析:从 file 类型识别到 ELF 结构与 objdump 反汇编);如果关心的是「这个文件是不是被别人改过」,那要换校验和那一套(Linux 文件完整性校验:从 sha256sum 到 dpkg -V 篡改排查);而把一个稀疏文件搬进备份盘时该留意什么,见 Linux tar 打包与压缩:从归档原理到增量备份。
评论 (0)
暂无评论,快来抢沙发吧!