FilesLast commitLast update
15 days ago
15 days ago
15 days ago
README

为什么 1KB 的文件占 4KB 磁盘?10 万个文件簇对齐浪费实测

图 1:首页——参数区、四个结果数字、单文件簇布局可视化,底部是 8 条自检

右键一个文件看属性,会看到两个对不上的数字。

「大小」写 1,024 字节,「占用空间」写 4,096 字节。

同一份文件,多出来的 3,072 字节并没有丢。

它只是被磁盘的最小分配单位吃掉了:文件不满一个簇,也要占满一个簇。

我把这条规则做成一个单文件网页,自己跑了一遍。

  • 10 万个 1024 字节文件配 4KB 簇:逻辑 102,400,000 字节,实际占用 409,600,000 字节
  • 同样的文件换成 512B 簇:实际占用 102,400,000 字节,浪费 0
  • 同样的文件换成 64KB 簇:实际占用 6,553,600,000 字节,浪费 6,451,200,000 字节

同一批内容,换个簇大小,占用从 97.66 MiB 一路涨到 6.10 GiB。

在线案例与源码:https://atomgit.com/deli007/demo_park/tree/main/codearts-disk-allocation-lab 成品只有一个 index.html,双击就能打开。下面每个数字都是我在页面里读回来的。

一、为什么一个 1KB 文件要占 4KB 磁盘

磁盘不按字节分配空间,按簇分配。

簇是文件系统一次分配的最小格子,Windows 里叫「分配单元大小」。

一个文件占多少个簇是向上取整:占用 = ceil(文件大小 / 簇大小) * 簇大小。

1,024 字节的文件配 4,096 字节的簇,向上取整到 1 个簇,占 4,096 字节。

最后那个簇里没用完的部分就是浪费,英文里叫 slack。

1 个字节的文件在 4KB 簇下同样占 4,096 字节,浪费 4,095 字节。

我让页面把这个数字算了一遍,浪费率给到 409500%。

这个百分比看着离谱,因为它的分母是 1 个字节,不是磁盘容量。

常见场景 典型簇大小 备注
NTFS 默认 4 KB 现代 Windows 系统盘最常见
exFAT 大容量盘 32 ~ 128 KB 大簇换来更少的分配记录
老式 FAT32 / 软盘 512 B ~ 4 KB 小簇,元数据开销大
Linux ext4 4 KB 默认 4K 块,也支持 1K / 2K

小文件多的机器最吃亏,因为每个文件都要单独占满一个簇。

二、准备环境:进入码道 Web

浏览器打开码道 Web 版:https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwsf&sourcead=dmzntgwwbwz

登录后直接在对话窗口写需求,不用装软件。

码道有三种使用方式:WebUI 浏览器对话、TUI 终端命令行和桌面 IDE 插件。本文用 WebUI 版演示。

三、需求原文:把要验的数字先写死

我这次没让模型自己回忆公式,把簇对齐的边界值直接写进了需求。

8 条自检的期望值、公式写法、失败路径的提示文案,一条条列在里面。

需求原文如下,可以直接复制。

做一个单文件网页应用「磁盘簇对齐实验室」(磁盘分配单元浪费模拟器),只需要一个 index.html,纯 HTML/CSS/JS,不引入任何外部库和网络资源,双击即可打开运行。

需求要点:
1. 顶部输入区:文件大小输入(数字 + 滑动条,1 字节 ~ 4MiB)、文件数量输入(1 ~ 200000)、簇大小下拉(512B / 4KB / 16KB / 64KB / 1MB,默认 4KB)。
2. 主结果区并排显示四个数字:逻辑大小(文件真实字节数合计)、实际占用(按簇向上取整后的合计)、浪费字节(slack)、浪费比例(浪费/逻辑)。
3. 每个文件的占用公式必须写成 ceil(文件大小 / 簇大小) * 簇大小;最后一个簇里没用完的部分就是浪费。
4. 可视化:把一个文件的簇布局画出来(格子数随簇数自适应,最多画 64 格),最后一个簇里"已用"和"浪费"用两种颜色区分,标注每个格子等于多少字节。
5. 对照表:固定 5 个场景并排对比逻辑大小、实际占用、浪费、浪费率——(a)1 个 1 字节文件;(b)1 个 1MiB 文件;(c)100000 个 1024 字节文件;(d)100000 个 1024 字节文件换成 512B 簇;(e)100000 个 1024 字节文件换成 64KB 簇。表格里同时给出 MiB/GiB 换算。
6. 反直觉提示:同一批文件逻辑大小一样,簇越大占用越多;簇越小大文件读取开销越大——给一句结论文案。
7. 页面内自检面板:加载时自动跑下面 8 条断言,把每条的名称、期望值、实际值、通过与否显示出来,最后显示"自检 X/8 通过"。
   断言清单(必须逐条实现,期望值不要改):
   A. ceil(1/4096)*4096 = 4096
   B. ceil(4097/4096)*4096 = 8192
   C. 1 个 1 字节文件在 4KB 簇下:实际占用 4096、浪费 4095
   D. 100000 个 1024 字节文件在 512B 簇下:实际占用 102400000、浪费 0
   E. 100000 个 1024 字节文件在 4KB 簇下:实际占用 409600000、浪费 307200000、浪费率 300%
   F. 100000 个 1024 字节文件在 64KB 簇下:实际占用 6553600000
   G. 1 个 1048576 字节文件在 4KB 簇下:实际占用 1048576、浪费 0
   H. 文件大小 1..4096 各取一次(4096 个文件,4KB 簇):浪费合计 8386560、平均 2047.5
8. 输入校验与失败路径:文件数量填 0、文件大小填 0 或负数或非数字时,不计算,输入框标红并显示中文提示原文(例如"文件大小需要是大于 0 的数字"),同时保留上一次的有效结果。
9. 界面中文,深色科技风,1920x1200 下排版完整不溢出。

完成后打开预览,让我直接看到运行效果。

第 7 条是整个需求里最值钱的一条:断言写死在页面上,验收就不用靠嘴说。

四、页面上有哪几块

  • 参数区:文件大小带对数滑动条,1 字节到 4MiB;文件数量 1 到 200000;簇大小 512B 到 1MB
  • 结果区:逻辑大小、实际占用、浪费字节、浪费比例四个数字并排
  • 可视化:按簇画格子,末簇用两种颜色区分已用和浪费
  • 对照表:5 个固定场景的占用和浪费率并排
  • 自检面板:8 条断言在加载时自跑,逐条给出期望值和实际值

我把文件大小设成 20000 字节、簇 4KB,页面上出现 5 个格子。

前 4 格是满的,第 5 格左边一小块绿色,右边一大块红色。

页面标注:共 5 个簇,末簇已用 3,616 B / 浪费 480 B。

图 2:20000 字节的文件配 4KB 簇,画出 5 个格子,末簇只用了 3616 字节

五、验证:五场景对照表与 8 条自检

页面里的对照表是固定算的,不随上面的输入变化。

场景 文件数 单文件 簇大小 逻辑大小 实际占用 浪费 浪费率
(a) 1 个 1 字节文件 1 1 B 4 KB 1 4,096 4,095 409500.00%
(b) 1 个 1MiB 文件 1 1,048,576 B 4 KB 1,048,576 1,048,576 0 0.00%
(c) 10 万 × 1024B 100,000 1,024 B 4 KB 102,400,000 409,600,000 307,200,000 300.00%
(d) 10 万 × 1024B 100,000 1,024 B 512 B 102,400,000 102,400,000 0 0.00%
(e) 10 万 × 1024B 100,000 1,024 B 64 KB 102,400,000 6,553,600,000 6,451,200,000 6300.00%

(a) 和 (b) 摆在一起最说明问题。

同样是 1 个文件、同样是 4KB 簇,1 字节的浪费率 409500%,1MiB 的浪费率是 0。

因为 1MiB 正好是 4KB 的整数倍,末簇刚好填满。

(c)(d)(e) 三行的逻辑大小一模一样,都是 102,400,000 字节。

换了簇大小,实际占用就从 97.66 MiB 变成 390.63 MiB,再变成 6.10 GiB。

图 3:100000 个 1024 字节文件配 512B 簇,实际占用正好等于逻辑大小,浪费 0

图 4:同一批文件换成 64KB 簇,实际占用 6.10 GiB,浪费率 6300%

页面底部的自检面板跑出 自检 8/8 通过,8 条断言是这一批。

编号 断言 期望值 实际值
A ceil(1/4096)*4096 4,096 4,096
B ceil(4097/4096)*4096 8,192 8,192
C 1 字节文件 / 4KB 簇 占用 4,096、浪费 4,095 占用 4,096、浪费 4,095
D 10 万 × 1024B / 512B 簇 占用 102,400,000、浪费 0 占用 102,400,000、浪费 0
E 10 万 × 1024B / 4KB 簇 占用 409,600,000、浪费 307,200,000 占用 409,600,000、浪费 307,200,000
F 10 万 × 1024B / 64KB 簇 占用 6,553,600,000 占用 6,553,600,000
G 1 个 1048576B 文件 / 4KB 簇 占用 1,048,576、浪费 0 占用 1,048,576、浪费 0
H 文件大小 1..4096 各一个 / 4KB 簇 浪费合计 8,386,560、平均 2,047.5 浪费合计 8,386,560、平均 2,047.5

我没有只看页面自己报的绿勾,又用 Python 复算了一遍。

import math
alloc = lambda size, cluster: math.ceil(size / cluster) * cluster
assert alloc(1, 4096) == 4096
assert alloc(4097, 4096) == 8192
assert alloc(1024, 512) * 100000 == 102400000
assert alloc(1024, 4096) * 100000 == 409600000
assert alloc(1024, 65536) * 100000 == 6553600000
assert alloc(1048576, 4096) == 1048576
assert sum(alloc(i, 4096) - i for i in range(1, 4097)) == 8386560

8 条断言两边一位不差,Python 侧平均值也算出 2047.5。

这个 2047.5 就是 4KB 簇下小文件的平均浪费,等于 簇大小减一再除以 2。

复现只要两行命令,不需要任何依赖。

python -m http.server 8080
# 浏览器打开 http://127.0.0.1:8080/index.html

六、失败路径与三处真实边界

页面算不出结果的时候,它会把话说清楚。

我把文件大小填成 0,输入框变红,下面的小字写「文件大小需要是大于 0 的数字」。

结果区没有被清空,还留着上一次的 102,400,000 和 6,553,600,000。

图 5:文件大小填 0 后的失败路径——红框、中文提示原文,结果保留上一次有效值

我把几处边界都点了一遍,提示原文如下。

  • 文件大小 4194305:文件大小不能超过 4MiB(4194304 字节)
  • 文件数量 200001:文件数量不能超过 200000
  • 文件数量 2.5:文件数量需要是整数
  • 文件数量 0:文件数量至少为 1

这里有三件事我原样记下来,没有改代码。

第一,文件大小输入 1024.5,页面不报错,直接按 1024 算。

数量那一栏遇到小数会提示「文件数量需要是整数」,大小这一栏是静默向下取整,两边口径不一致。

第二,簇数超过 64 时可视化是压缩画法。

文件 4194304 字节配 512B 簇,实际有 8,192 个簇,页面只画 64 格。

它在下面标注了「仅显示前 63 个满簇 + 最后 1 个簇」,不是 1:1 的真实比例图。

第三,页面只算簇对齐这一项,没有算文件系统自己的账。

NTFS 的 MFT 记录、目录项、日志都会吃掉额外空间。

所以页面给出的实际占用是一个下界,真实磁盘上只会更多,不会更少。

浪费率那一列也一样,分母是逻辑大小不是磁盘容量,可以远超 100%。

七、用码道的体会

这次一次就拿到了能跑的页面,关键在于需求写得像验收单。

我把 8 条断言的期望值直接写死在需求里,模型没法用「大概对」蒙过去。

对照表也做成固定的 5 个场景,不跟着输入乱跳,截图和文章里的数字才能对得上。

反过来看,需求里没提的地方它也不会自己补。

小数取整的口径不一致、可视化在簇数超限时是压缩画法,这两处都是我点出来才发现的。

下次写这类工具,我会在需求里多加一句:所有数字输入统一按整数处理,小数要提示。

八、直接下载试玩

这个 Demo 已经完整开源(源代码、本文档、全部截图),托管在公开仓库,可直接下载来玩:

打开上面的链接,进入 codearts-disk-allocation-lab 文件夹,把 index.html 下载到本地,用浏览器打开即可运行,无需安装任何依赖。

九、下一步

这份页面还能往下走。

把 NTFS 的 MFT 单条记录大小、目录项这些一起算进去,得到的就是「为什么 128G 的手机只有 119G」的完整账。

再加一个文件数分布输入,就能模拟截图文件夹、node_modules 这类小文件重灾区的真实占用。

你也可以把 index.html 存下来,本地起个静态服务,自己拖几个数字看看占多少。