← 返回文章
技术笔记

同一个文件,为什么有两个大小

2026-09-12 · M 写

一个文件问「多大」,会得到两个答案,而且两个都是对的。

先造一个 64 MiB 的文件,什么都不写,只给它一个长度:

$ truncate -s 64M sparse
$ stat -c %s sparse
67108864
$ du --block-size=1 sparse
0

一个说 67108864 字节,一个说 0。这不是谁算错了,它们量的是两件事:前一个是文件的内容有多长,后一个是它实际占掉了多少空间。

两个数各自的含义

文件在文件系统里有两个独立的量。一个是逻辑长度,也就是从头到尾有多少字节,ls -lstat -c %s 报的是它。另一个是实际分配出去的空间,du 报的是它。GNU 手册对 du 的定义很直白:报告「表示这些文件所需的空间」。手册还给了一个可操作的等价说法:文件的表观大小就是 wc -c 数出来的字节数,而它和实际占用可以差得很远 —— 手册举的例子是一个 2 GiB 的文件「在多数现代文件系统上几乎不占空间」。

想换个数看也有开关:du --apparent-size sparse 报的是 67108864,而不是 0。同一个 du,加一个参数就改报长度。

空间是按块给的

两个数为什么会分开?因为空间不是按字节发的,是按块。这台机器上文件系统报的块大小是 4096 字节,于是同样写满的两个文件:

4096 字节的文件  ->  占用 4096
4097 字节的文件  ->  占用 8192

多出一个字节,就多占一整个块。占用以块为单位往上走,而逻辑长度可以是任何数。小文件吃亏最明显:几十字节的内容也要占掉完整的一块。

空洞:长度是真的,占用 0 也是真的

truncate -s 64M 造出来的文件没有内容,只有长度。文件系统在元数据里记下「这里长 64 MiB,全是空的」,然后一个块也不分配。去读它,会读到一串零 —— 补零发生在读取的时候,不在磁盘上。所以长度是真的,占用 0 也是真的,两者并不矛盾。

内容一模一样,占用差 64 MiB

还有一处对比更直接。我造了两个文件:一个是刚才的空洞文件,另一个是真写满了 64 MiB 零字节的文件。用 cmp 逐字节比较,它们完全相同 —— 读出来的字节没有任何差别。可是:

sparse     长度 67108864   占用 0
realzeros  长度 67108864   占用 67108864

读起来一模一样,占的空间差了 64 MiB。这就是「占用」必须作为一个独立的量存在的原因:它记的是存法,不是内容。

为什么值得分清

因为磁盘报警看的从来不是 ls。空间够不够,是分配的事,要看 dudf;而「这个文件有多大」通常问的是内容,要看长度。混着用就会得出看着矛盾的结论:一个用 ls -l 加出来只有几 MB 的目录,可能占掉几个 GB;反过来,一个 64 MiB 的文件也可能完全不占空间。两种「不可能」都是真的,只是问的不是同一件事。

这个区别在搬运文件时会突然变得很实在。同一个 64 MiB 的空洞文件,我用三种方式各复制一份:

cp                 ->  占用 0
cp --sparse=never  ->  占用 67108864
cat sparse > copy  ->  占用 67108864

cp 默认会认出空洞并保留它,副本占用仍是 0;--sparse=never 关掉这个识别,空洞就被写成真的零;用 cat 重定向更直接,它只把字节流出来,出来的就是 64 MiB 实打实的零。所以往网络上传、或者被一个不认识空洞的工具搬一趟,文件的占用会从 0 变成 64 MiB —— 传出去的是长度,不是占用。

参考:GNU Coreutils 手册 · du。文中两组数字都是我在本机实测的输出。

—— M-D