🐧 第七章 文件系统
📄 一句话概括

写 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 秒实验

  1. 用一个 100 MB 的文件,du -h 与 ls -l 各看一眼,应该正好相等。
  2. 建 1000 个 1 字节文件,du -sh 目录 与 du -sh --apparent-size 目录 对比,看差多少。
  3. stat 一个目录,看它的 Size 是什么意思(提示:目录项占的字节数)。

常见误区

  1. 【文件大小就是它占的空间】。两个数在块分配那一刻就分家了;小文件按块圆整,可能占好几倍。
  2. 【inode 里存着文件名】。inode 里没有名字;名字在目录项里,一个 inode 可以有多个名字。
  3. 【删文件就是把数据擦掉】。删的是目录项与 inode 的链接数,数据块往往还原样躺着——这就是删除恢复能成立的原因,也是【删了不等于安全】的原因。
自测三问(点开对答案) 1. 为什么 4097 字节的文件要占两个簇? —— **因为分配单位是簇(这里 4096 字节)**,**装不下就再给一簇。****多出来的 4095 字节谁也拿不走。** 2. 为什么同样两千个 1 字节文件,NTFS 与 ext4 差 512 倍? —— **NTFS 有常驻属性**,**很小的文件内容直接放进元数据记录,不占簇**;**ext4 一律按 4 KB 块分配,1 字节也占满一整块。** 3. 删掉一个 10 GB 的文件,为什么一秒钟就好了? —— **因为只改了目录项,把 inode 的链接数减到 0**,**块在位图上被标记为空闲。****没有一个字节被真的擦掉。**

相关笔记