写 1 个字节的文件,实际占 8 字节;写 740 字节,一下子占满 4096 字节;写 4097 字节,占 8192。同样两千个 1 字节文件,NTFS 上 16000 字节,ext4 上要 8192000 字节——差 512 倍。
7.1 讲了【名字怎么找到文件】,这一节往里看一层:一个文件除了内容,还带着一大堆属性,而属性有时比内容还占地方。
一、两样东西:数据与元数据
数据(data):你要存的那串字节。文档正文、图片像素、程序代码。 元数据(metadata):描述这串字节的东西。大小、时间、权限、属主、内容散在哪些块上。 两者存在不同的地方:数据在数据块里,元数据在 inode 里。 这个区分不是学术上的洁癖,它决定了很多实际行为。 比如【为什么 df 显示还有很多空间,却写不进去】(7.3);比如【为什么断电之后文件内容可能还在,但文件名没了】(7.4)。 一句话:元数据是【关于数据的账】,而账本本身也占地方、也会坏。
二、inode 里到底存什么
inode(索引节点)是文件的身份证,每个文件一个,编号在文件系统内唯一。 它存这些:① 大小(多少字节);② 权限与类型(rwx、是普通文件还是目录);③ 属主与属组;④ 三个时间戳(最后访问、最后修改、inode 最后变更);⑤ 硬链接数(有几个名字指向它);⑥ 块指针(内容存在哪些块)。 它不存文件名。文件名在目录项里——这就是【一个文件可以有多个名字】(硬链接)能成立的原因:几个目录项指向同一个 inode,inode 里的链接数记着有几个。 ext4 一个 inode 固定 256 字节,块指针只有 15 个——12 个直接指、1 个一级间接、1 个二级间接、1 个三级间接。小文件用直接指针就够,大文件才一层层往外指。
三、逻辑大小不等于占的字节
文件有两个【大小】:逻辑大小(ls -l 看到的那个数);实际分配(盘上真占了多少)。
两者在【块分配】的那一刻就分家了。因为盘是按块(簇)分配的,你只用到一块的一部分,剩下的也归你,别人用不了。
实测(NTFS,簇 4096 字节):写 1 字节占 8 字节;写 100 字节占 104 字节;写 740 字节直接跳到 4096;写 4097 字节占 8192。
两个台阶很清楚。第一个台阶来自【常驻属性】:NTFS 会把很小的文件内容直接塞进元数据记录里,连一个簇都不用。这个门槛大约在 730 字节(MFT 记录 1 KB 扣掉头与属性开销)。
第二个台阶就是普通的簇圆整:超过门槛之后,按 4096 往上取整。所以 4097 字节要两个簇、5000 字节也是两个簇。
ext4 没有【常驻】这回事,最小分配单位就是 4 KB 块,1 字节也占满一整块。所以【两千个 1 字节文件】在两个系统上差 512 倍:16000 字节对 8192000 字节。
四、为什么改个文件名是瞬间的
因为改名只动目录项,不动 inode,更不动数据块。目录那张表里那一行的【名字】部分换掉就行。
这也解释了另外几件常见的事。
① 删除文件很快:删的是目录项,把 inode 的链接数减一;减到 0 才真的释放块。所以删一个 10 GB 的文件比拷它快得多。
② 删除之后空间可能没回来:只要有别的进程还开着这个文件,块就不能回收(lsof | grep deleted 能看到一批)。要等那个进程关闭或退出。
③ 硬链接不能指向目录:因为目录项的结构要是允许成环,遍历就永远走不完。软链接可以指目录,因为它是另一回事(7.9)。
五、动手 Demo:数据与元数据——1 字节的文件占多大地方
这一节写一串大小不同的文件,用系统接口取每个文件【真正占了多少字节】,再和逻辑大小对比。
Windows 上这个数是 AllocationSize,Linux 上对应 stat 的 Blocks。
import ctypes
import os
import tempfile
from ctypes import wintypes
class INFO(ctypes.Structure):
_fields_ = [("AllocationSize", ctypes.c_longlong),
("EndOfFile", ctypes.c_longlong),
("NumberOfLinks", wintypes.DWORD),
("DeletePending", wintypes.BOOLEAN),
("Directory", wintypes.BOOLEAN)]
def alloc_size(p):
h = ctypes.windll.kernel32.CreateFileW(
ctypes.c_wchar_p(p), 0x80000000, 7, None, 3, 0, None)
info = INFO()
ctypes.windll.kernel32.GetFileInformationByHandleEx(
wintypes.HANDLE(h), 1, ctypes.byref(info), ctypes.sizeof(info))
ctypes.windll.kernel32.CloseHandle(wintypes.HANDLE(h))
return info.AllocationSize
d = tempfile.mkdtemp(prefix="ch29c7_")
for s in (1, 100, 720, 740, 4096, 4097, 5000):
p = os.path.join(d, "s%d.bin" % s)
open(p, "wb").write(b"\0" * s)
print(s, os.path.getsize(p), alloc_size(p))
实跑输出:
--------------------------------------------------------------------
数据与元数据——1 字节的文件占多大地方
--------------------------------------------------------------------
① 同一个文件,逻辑大小与实际分配是两个数
写多少字节 逻辑大小 实际占多少 多出来
-------------------------------------------------------------
1 1 字节 8 字节 7 字节
100 100 字节 104 字节 4 字节
600 600 字节 600 字节 0 字节
720 720 字节 720 字节 0 字节
740 740 字节 4096 字节 3356 字节
4096 4096 字节 4096 字节 0 字节
4097 4097 字节 8192 字节 4095 字节
5000 5000 字节 8192 字节 3192 字节
1048576 1048576 字节 1048576 字节 0 字节
**看这几行就懂了**:
**1 字节只占 8 字节;写 740 字节一下子跳到 4096;写 4097 变成 8192。**
**两个台阶**:
**① 很小的文件(这里约 730 字节以内)内容直接塞进元数据记录里,不占簇。**
**② 一旦超过这个门槛,就按簇(4096 字节)圆整,**
**所以 4097 字节要占两个簇 = 8192 字节。**
**注意最后一列【多出来】——**
**那是【为了能存下这些字节,多花掉的物理空间】。**
**文件越小,这一列相对越大。**
② 同样两千个 1 字节文件,两个文件系统差多少
**NTFS 上 1 字节文件实占 8 字节**(上面量出来的),**所以两千个是 16000 字节。**
**ext4 上没有【常驻】这回事,1 字节也占满一个 4 KB 块**——
**两千个就是 8192000 字节。**
**差 512 倍。**
**这就是【小文件仓库】最容易被忽略的地方**:
**在 ext4 上你以为存了 2 KB 数据,实际吃掉了 8 MB 空间。**
📋 **Linux 上验证这件事**:`stat 文件` 看 `Size` 与 `Blocks`——
**注意 Blocks 的单位是 512 字节**;
`du -h` 与 `ls -l` 差出来的就是这件事。
③ 元数据本身有哪些字段
字段 值 意思
-----------------------------------------------------
大小 1 字节 内容有多少字节
inode 号 5066549581039545 文件的身份号
硬链接数 1 有几个名字指向它
权限位 33206 rwx 与类型
修改时间 2026-10-20 18:09 三个时间戳之一
**这五行都不是文件内容,却必须存着**——**它们全都存在 inode 里。**
**ext4 一个 inode 固定 256 字节,只存这些属性与【块指针】**,**不存文件名。**
**文件名存在【目录】里**:**目录本身也是一个文件,**
**内容是【名字 → inode 号】的对照表。**
**所以【改文件名】只是改目录里的一行**,**文件内容一个字节都不用动。**
**这就是 mv 在同一分区内瞬间完成的原因。**
逻辑大小、实际占用、多出来多少,三列一摆就明白了。 1 字节只占 8 字节(塞在元数据记录里,按 8 字节对齐)。720 字节以内都是这个模式,写多少占多少。 740 字节一下子跳到 4096——门槛过了,必须给一整簇。 4097 字节是 8192——跨簇了。 最后一列【多出来】才是真正付的冤枉钱。文件越小,这一列相对越亏。
📋 Linux 上验证:stat 文件 看 Size 与 Blocks(注意 Blocks 单位是 512 字节);du -h 文件 与 ls -l 文件 各看一眼,差出来的就是这件事。
30 秒实验
- 用一个 100 MB 的文件,
du -h与ls -l各看一眼,应该正好相等。 - 建 1000 个 1 字节文件,
du -sh 目录与du -sh --apparent-size 目录对比,看差多少。 stat一个目录,看它的Size是什么意思(提示:目录项占的字节数)。
常见误区
- 【文件大小就是它占的空间】。两个数在块分配那一刻就分家了;小文件按块圆整,可能占好几倍。
- 【inode 里存着文件名】。inode 里没有名字;名字在目录项里,一个 inode 可以有多个名字。
- 【删文件就是把数据擦掉】。删的是目录项与 inode 的链接数,数据块往往还原样躺着——这就是删除恢复能成立的原因,也是【删了不等于安全】的原因。
自测三问(点开对答案)
1. 为什么 4097 字节的文件要占两个簇? —— **因为分配单位是簇(这里 4096 字节)**,**装不下就再给一簇。****多出来的 4095 字节谁也拿不走。** 2. 为什么同样两千个 1 字节文件,NTFS 与 ext4 差 512 倍? —— **NTFS 有常驻属性**,**很小的文件内容直接放进元数据记录,不占簇**;**ext4 一律按 4 KB 块分配,1 字节也占满一整块。** 3. 删掉一个 10 GB 的文件,为什么一秒钟就好了? —— **因为只改了目录项,把 inode 的链接数减到 0**,**块在位图上被标记为空闲。****没有一个字节被真的擦掉。**- ☐ 7.3 容量限制
相关笔记
- 高速缓存 — 为什么小文件多反而更吃亏
- Linux的文件系统 — 名字是怎么查到的
- 容量限制 — 元数据也会用完
- 🐧 Linux是怎样工作的 · 索引