1GB 的文件只占 8KB:稀疏文件的洞,以及拷贝时怎么不把它填满

举报
yd_251540181 发表于 2026/11/06 06:37:19 2026/11/06
【摘要】 用内核稀疏文件特性造 1GiB 文件只占 0 块,写两个 4K 后 8KB;实测 cp、rsync -S、tar --sparse、dd conv=sparse 与 shutil.copyfile 的差别,附 SEEK_DATA 探洞方法。

1GB 的文件只占 8KB:稀疏文件的洞,以及拷贝时怎么不把它填满

做镜像、日志预分配或者虚机磁盘时,会碰到一个让 ls 和 df 对不上的现象:文件明明显示 1.0G,占用却只有几 KB。这不是统计错误,而是稀疏文件(sparse file)。本文把实测过程写清楚,并给出拷贝、打包、写中间数据时保持或放弃稀疏的具体手段。以下所有数字来自本地隔离临时目录的真实执行,未连接任何生产存储;本文为原创整理,使用 AI辅助进行实验设计、命令执行与文字检查。

先分清三个"大小"

同一个文件在不同工具里会看到三个不同的量,混用就会误判:

  • 表观大小(stat 的 st_size、ls -l):文件的逻辑长度,稀疏文件也按这个算。
  • 实际分配(stat 的 st_blocks × 512):真正落盘的块数。注意单位是 512 字节,不是 4K,直接用会算错。
  • 目录遍历占用(du 默认):按分配块累计;加 --apparent-size 才变成表观大小。

实测:truncate -s 1073741824 s.img 之后,stat 给出 size=1073741824 blocks=0,即表观 1GiB、实际分配 0。此时 du -h s.img 是 0,而 du -h --apparent-size s.img 是 1.0G;ls -l 只显示前者那个逻辑长度。

为什么 0 块?因为 truncate 只改元数据,不写入数据块,文件里全是"洞"。读取这些洞会返回零字节,但内核不需要真的去读盘。

写一点数据后,占用只增加那一小块

在偏移 100MB 和 700MB 各写 4096 字节,再查:

after truncate: size=1073741824 blocks=0   alloc=0
after 2 writes: size=1073741824 blocks=16  alloc=8192

两次 4K 写入一共 8192 字节,st_blocks 正好是 16(16 × 512 = 8192)。文件仍然是 1GiB 逻辑长度,但只占 8KB。这说明写入是按页分配的,未触碰的区域继续保持为洞。

用 SEEK_DATA / SEEK_HOLE 直接看洞的分布

不想猜的话,可以用 lseek 的两个扩展 whence 遍历数据段与洞段。Python 里 os.SEEK_DATA 和 os.SEEK_HOLE 直接可用:

import os
size=os.stat('disk.img').st_size
fd=os.open('disk.img',os.O_RDONLY); off=0; segs=[]
while off<size:
    try: d=os.lseek(fd,off,os.SEEK_DATA)
    except OSError: break
    h=os.lseek(fd,d,os.SEEK_HOLE); segs.append((d,h-d)); off=h
os.close(fd)
# segments == [(104857600, 4096), (734003200, 4096)]

输出与写入位置一一对应:100MiB 处 4096 字节数据,700MiB 处 4096 字节数据,其余全部是洞。这个方法比解析 filefrag 的文本输出更稳,也不依赖文件系统工具版本;代价是必须用能返回 SEEK_DATA 的文件系统(ext4、xfs、btrfs、tmpfs 都支持)。

真正危险的是"普通拷贝"

洞不会自动跟着文件走。多数按字节流写的拷贝方式会把整段零也读出来再写下去,于是 1GiB 全部落盘。实测同一份文件(2 个 4K 数据段,其余为洞)用不同方式复制,哈希全部一致,但占用差了两个数量级:

disk.img           apparent=1073741824 blocks=16      alloc=8192
cp (默认)          apparent=1073741824 blocks=16      alloc=8192
cp --sparse=never  apparent=1073741824 blocks=2097232 alloc=1073782784
rsync -aS          apparent=1073741824 blocks=16      alloc=8192
rsync (无 -S)      apparent=1073741824 blocks=2097208 alloc=1073770496
dd conv=sparse     apparent=1073741824 blocks=4096    alloc=2097152
python shutil.copyfile  apparent=1073741824 blocks=2097224 alloc=1073778688

几个可以直接抄走的结论:

  • GNU cp 默认会检测全零块并保持稀疏,所以 cp 通常是安全的;加 --sparse=never 才会强制写满,这一条在把镜像交付给不支持稀疏的目标(比如某些网络存储)时才需要。
  • rsync 的 -S(--sparse)是关键开关。本地同机 rsync 默认也是整份数据过一遍,不加 -S 就把洞填实了。
  • dd 必须显式写 conv=sparse;不加就是整份写。
  • Python 的 shutil.copyfile、以及手写的 read(1MB)/write 循环,都属于逐字节流式复制,会把洞填成实块。这一点在写备份脚本时最容易踩。

打包:tar 的默认行为差别很大

打包是第二个高频误伤点。普通 tar -cf 会把零数据真的写进归档,--sparse 则按 GNU 稀疏格式记录洞:

tar -cf plain.tar disk.img    # 归档大小 1073745920(约 1GiB)
tar --sparse -cf sparse.tar disk.img   # 归档大小 10240(10KiB)
tar --sparse -xf sparse.tar -C out     # 解出后 blocks=16,稀疏被保留

解包时这里有个容易忽略的点:tar --sparse 解出来的文件仍然是稀疏的(blocks=16,占用 8KB),内容哈希与原件一致。归档体积从 1GiB 降到 10KiB,传输和存储成本完全不同。

什么时候该主动放弃稀疏

稀疏不是越稀疏越好,有三类场景要反着做:

  • 目标不支持稀疏:部分网络文件系统、某些对象存储网关、以及经由 POSIX 层封装的同步工具,会把洞读成零再上传,流量和费用按表观大小算。这时提前 --sparse=never 反而能避免中途爆量。
  • 随机写密集的数据库文件:预分配的价值是避免运行期碎片和扩容抖动,fallocate 一次真实占位比 truncate 更合适。实测 fallocate -l 64M alloc.img 得到 blocks=131088(约 64MiB 实分配),与 truncate 的 0 块形成对照。
  • 需要按表观大小计费或校验容量的流程:备份系统如果按 st_size 估算用量,稀疏文件会让估算严重偏高,需要改成读 st_blocks。

一个可直接用的判断函数

把上面的规则收成一个只读检查,用来在拷贝前后判断是否被填实:

def footprint(path):
    st = os.stat(path)
    return {'apparent': st.st_size, 'allocated': st.st_blocks * 512,
            'ratio': round(st.st_blocks * 512 / st.st_size, 6) if st.st_size else None}

对一个 1GiB 的稀疏镜像,正常结果应该是 ratio 极小(本例 8192/1073741824 ≈ 0.0000076)。如果拷贝后发现 ratio 接近 1,说明这一份已经是实心文件,要回头检查是用 cp、rsync -S、tar --sparse 还是流式写入。

边界说明

以上只在本地 ext4 上做过实测,没有覆盖 btrfs 的 reflink 共享计费、压缩文件系统的特殊语义,也没有覆盖跨网络传输时中间层的处理。不同实现(GNU coreutils、busybox、BSD tar、Python 各版本)对稀疏的默认策略并不一致,不能据本文外推。另外,稀疏只减少零字节的物理占用,不减少逻辑长度,也不提供任何加密或访问控制;需要保护内容仍然要靠权限与加密,不要指望"洞"能隐蔽数据。

本文所有命令与输出都可以在隔离临时目录复现,未做结果修饰。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。