文件系统基础:文件、目录与元数据

你写一行 open("/var/log/app.log"),理所当然地以为系统会"找到那个文件"。但磁盘其实是个非常笨的设备——它只认编号的块(block):你给它一个块号,它把那一块的字节还给你;你给它一块数据和一个块号,它把数据写到那一块去。它完全不知道什么叫"文件",更不懂什么叫"/var/log/app.log"。

那么,从"一串路径名"到"磁盘上某几个块里的字节",这中间巨大的鸿沟是谁、怎么填上的?

磁盘只会按块存数据,它天然不理解"这是某个用户的日志文件"。把裸的块设备组织成"可命名、可定位、可保护、可长期维护"的文件,正是文件系统的全部工作。

这一篇我们先把文件系统的运转机制讲透——文件的内容和属性分别存在哪、inode 是什么、目录到底是不是"文件夹"、一个路径名是怎么一步步被解析成磁盘块的——看清这套机制后,再回答它为什么不只存数据、还非要维护目录和元数据。这是操作系统系列从内存转向持久化存储的第一篇。

一、先把"文件"拆成两半:内容与元数据

理解文件系统的第一刀,是把"文件"这个看似单一的东西拆成两部分

  • 文件内容(data):你真正关心的那串字节——日志的文本、图片的像素、视频的帧。它存在磁盘的数据块里;
  • 文件元数据(metadata):描述这个文件的"档案信息"——它有多大、谁创建的、权限是什么、什么时候改过、内容到底存在哪几个块里。

这个拆分至关重要。因为磁盘只存字节,它分不清"哪几个块是一个文件、这个文件叫什么、谁能读它"。这些"关于文件的信息"必须被单独记录、单独管理。元数据就是文件系统给每个文件建立的"户口本"。

文件 = 内容 + 元数据。内容是你要存的字节,元数据是"关于这些字节的一切信息"。文件系统的大量工作,其实是在管理元数据。

很多初学者只盯着"文件内容存哪了",却忽略了元数据。但你后面会看到,文件系统的绝大多数操作(查找、权限检查、定位、删除)动的都是元数据,而不是内容本身

二、inode:每个文件的"户口本"

在类 Unix 文件系统里,承载元数据的核心数据结构叫 inode(索引节点)。每个文件有且仅有一个 inode,它就是这个文件的身份档案。一个 inode 里装着:

  • 文件类型(普通文件 / 目录 / 符号链接……);
  • 文件大小(字节数);
  • 权限位(读/写/执行,对应 owner/group/other);
  • 属主(user id、group id);
  • 时间戳(创建、最后修改 mtime、最后访问 atime 等);
  • 链接计数(有多少个文件名指向这个 inode,后面讲);
  • 最关键的:指向文件内容所在数据块的指针(这个文件的字节到底存在磁盘哪几个块)。

请特别注意一件反直觉的事:inode 里不包含文件名! 文件的名字不在它的"户口本"里。这听起来很怪,但恰恰是 Unix 文件系统设计的精妙之处——名字和文件本体是分开存的。那名字存在哪?这就引出了目录。

inode 是文件的本体档案,装着文件的全部元数据和"内容在哪几个块"的指针,唯独不装文件名。每个文件对应一个唯一的 inode,系统内部其实是用 inode 号(而非文件名)来识别文件的。

三、目录的真相:它本质上也是一个文件

我们平时把目录想象成"装文件的文件夹",但在文件系统里,目录其实是一种特殊的文件——它的内容不是用户数据,而是一张映射表

目录的内容 = 一组「文件名 → inode 号」的对应条目。

也就是说,一个目录文件里存的,是它直接包含的每一项的名字、以及那个名字对应的 inode 号。比如 /home 这个目录的内容可能是:

文件名 inode 号
.(自己) 128
..(父目录) 2
alice 256
bob 257

这下第二节那个"inode 不存文件名"的谜题就解开了:文件名不在 inode 里,而在它所属目录的这张表里。inode 管"文件是什么、内容在哪",目录管"什么名字对应哪个 inode"。两者分工,各管一摊。

这个设计带来一个优雅的结果——硬链接(hard link):同一个 inode 可以被多个目录条目(多个文件名)指向。比如 /home/alice/a.txt/home/bob/b.txt 可以是两个不同的名字,却指向同一个 inode(同一份文件本体)。inode 里的"链接计数"就是记录有多少个名字指向它。这也解释了删除的机制(第五节细说)。

目录不是"文件夹"这种容器,而是一个内容为「名字→inode 号」映射表的特殊文件。理解这一点,路径解析和链接机制就全通了。

四、路径解析:一次 open 到底走了多少步

现在把 inode 和目录拼起来,回答开篇的问题:open("/var/log/app.log") 时,系统怎么从这串路径找到磁盘上的内容?这个过程叫路径解析,它是从根目录开始、一级一级查映射表的过程。

flowchart TD A["open('/var/log/app.log')"] --> B["从根目录 / 开始<br/>(根 inode 号固定, 通常是 2)"] B --> C["读根目录内容<br/>找 'var' → 得到 var 的 inode 号"] C --> D["读 var 的 inode → 读其内容(目录表)<br/>找 'log' → 得到 log 的 inode 号"] D --> E["读 log 的 inode → 读其内容(目录表)<br/>找 'app.log' → 得到文件 inode 号"] E --> F["读 app.log 的 inode<br/>① 权限检查 ② 取出数据块指针"] F --> G["按数据块指针<br/>读出文件内容"]

逐步拆解这条链:

  1. 从根开始:绝对路径以 / 开头,解析从根目录的 inode 出发(根 inode 号是约定好的固定值);
  2. 逐级查表:读根目录的内容(那张映射表),在里面找名字 var,得到 var 目录的 inode 号;再读 var 的 inode、读它的目录内容,找 log,得到 log 的 inode 号;再如法炮制找到 app.log 的 inode 号;
  3. 拿到目标 inode:读出 app.log 的 inode,这里做两件事——先按 inode 里的权限位检查你有没有资格访问(权限检查),再取出"内容在哪几个块"的数据块指针;
  4. 读内容:按数据块指针去磁盘对应的块里把字节读出来。

从这条链能看出几个深刻的点:

  • 路径解析是逐级的、有成本的:路径越深,要读的目录和 inode 越多。这就是为什么超深的目录层级会拖慢文件访问,也是为什么文件系统要缓存目录项(dentry cache)来加速;
  • 权限检查嵌在解析里:不光目标文件,路径上每一级目录都要有执行(x)权限你才能"穿过"它。这解释了为什么有时文件本身可读、却因为某层父目录没权限而打不开;
  • 名字解析和内容读取是分离的两个阶段:前面全程在查目录映射表(找 inode 号),最后才真正碰文件内容。

五、删除文件的真相:动的是元数据,不是内容

理解了 inode、目录、链接计数,一个常被误解的操作就豁然开朗了——删除文件(rm)到底删了什么?

直觉上你以为 rm 会把磁盘上那些数据块的字节擦掉。但实际上,删除一个文件名时,文件系统做的是:

  1. 从所属目录的映射表里删掉那条「文件名 → inode 号」的条目(名字没了);
  2. 把该 inode 的链接计数减 1
  3. 只有当链接计数减到 0(再没有任何文件名指向它)时,才真正回收这个 inode 和它的数据块。而且通常也只是标记为"可用",并不立刻擦除数据内容

这串机制解释了一连串现象:

  • 为什么删大文件那么快:删的只是一条目录条目 + 改个计数,根本不动 GB 级的内容数据,当然快;
  • 为什么删了还能恢复(有时):数据块没被擦,只是被标记可用,被覆盖前用恢复工具还能捞回来;
  • 为什么一个文件还开着、rm 了却没真正释放空间:进程打开文件会占一个引用,链接计数(含打开引用)不为 0,空间不会回收——直到进程关闭它。这正是线上"明明 rm 了日志,磁盘空间却没回来"的经典坑;
  • 硬链接为什么删一个名字不影响另一个:因为还有别的名字指向同一 inode,计数没到 0。

删除文件删的是"名字到 inode 的链接",不是内容本身。只有当指向某 inode 的链接计数归零,它的空间才被回收——而且往往只是标记可用、不立刻擦除。

六、看清机制后,回答:为什么非要这么设计

现在机制都清楚了,回头看那个核心问题:文件系统为什么不只是"把数据写进磁盘",非要搞出 inode、目录、元数据这一整套?因为它要在裸块设备之上同时建立五种能力,每一种都对应了上面机制的一部分:

  1. 命名:让人能用 /var/log/app.log 这种名字访问数据,而不是记一堆块号——靠目录的「名字→inode」映射;
  2. 组织:把海量文件组织成可浏览、可管理的层级结构——靠目录可以嵌套目录(目录也是文件);
  3. 定位:从名字快速找到内容到底在磁盘哪几个块——靠 inode 里的数据块指针 + 路径解析;
  4. 保护:控制谁能读、谁能写、谁能执行——靠 inode 里的权限位 + 路径上每级目录的权限检查;
  5. 一致性与持久:保证断电、崩溃后元数据和数据不至于错乱——靠日志(journaling)等机制(这是更进阶的话题,但根基也在元数据管理)。

把这五件事串起来你会发现:它们几乎全都围绕元数据展开。 命名靠目录(元数据),定位靠 inode 指针(元数据),保护靠权限位(元数据),一致性首先要保的也是元数据。这就是为什么我们一开始强调"文件 = 内容 + 元数据",并说文件系统的大量工作其实是在管理元数据——因为正是元数据,把笨拙的块设备变成了智能的、可用的文件抽象。

文件系统的价值不在"存字节"(磁盘自己就能干),而在围绕字节建立起命名、组织、定位、保护、一致性这五种能力——而这五种能力的载体,几乎全是元数据。

七、和工程实践 / SRE 的联系

文件系统的机制,直接对应后端和 SRE 的一类高频故障。理解元数据视角,很多"灵异现象"立刻就有了解释。

  • “磁盘没满,却报 No space left on device”:磁盘空间分两种——数据块和 inode。inode 数量是格式化时定死的。如果你创建了海量小文件(如缓存碎片、大量小日志),可能数据块还很空,但 inode 用尽了,照样写不进新文件。用 df -i 查 inode 使用率,是排查这类问题的关键一招。
  • rm 了大日志,磁盘空间却没释放”:第五节讲过——只要还有进程打开着那个被删的文件,链接计数(含打开引用)不为 0,空间就不回收。解决办法是重启/通知那个进程关闭文件(或 truncate),而不是反复 rmlsof | grep deleted 能揪出这类被删但仍占用空间的文件。
  • 深路径 / 海量小文件拖慢访问:路径解析是逐级查 inode 和目录的,目录里条目过多(一个目录放几十万文件)会让查找变慢。这就是为什么很多系统按 xx/yy/文件 的方式做散列分层目录,避免单目录文件爆炸。
  • 权限问题排查:文件本身可读却打不开,常是某层父目录缺执行权限——因为路径解析要"穿过"每一级目录。理解权限检查嵌在路径解析里,排查就有方向。
  • 数据库/对象存储复用了这套思想:很多存储系统在自己层面重建了类似结构——用"元数据 + 数据"分离、用索引(类比 inode)定位数据块。理解文件系统,能帮你看懂这些系统的底层组织。

八、动手做一个小实验:观察内容与元数据的分离

理解"内容 vs 元数据"最直观的方式,是亲手观察它们如何独立变化。

  1. 创建一个文件 echo hello > a.txt,用 stat a.txt 查看它的 inode 号、大小、权限、各种时间戳——这些全是元数据;
  2. 只改元数据、不改内容:执行 chmod 600 a.txt(改权限)或 touch a.txt(改时间戳),再 stat 一次。你会看到权限/时间变了,但文件大小和内容没变——元数据和内容是分开管理的;
  3. 验证目录是「名字→inode」映射:用 ls -i a.txt 看它的 inode 号,然后 ln a.txt b.txt 建一个硬链接,再 ls -i a.txt b.txt——两个名字的 inode 号完全相同!再 stat 看链接计数从 1 变成了 2。这直接印证了第三、四节;
  4. 验证删除的真相rm a.txtstat b.txt,文件内容还在(因为 inode 链接计数还有 1)。删名字 ≠ 删内容;
  5. 进阶:df -i 看看你系统的 inode 使用情况,体会"空间"其实有两种。

做完这一圈,inode、目录映射、硬链接、删除机制就从概念变成了你亲手验证过的事实。

学习这一部分最容易踩的坑

1. 以为文件名存在文件(inode)里

文件名不在 inode 里,而在所属目录的「名字→inode 号」映射表里。inode 只管文件本体和元数据。这是 Unix 文件系统最关键、也最反直觉的设计,硬链接和路径解析全靠它。

2. 把目录当成"容器"而非"文件"

目录本质是一种特殊文件,内容是一张「名字→inode 号」的映射表。它不"装"文件,只是记录"什么名字对应哪个 inode"。想通这点,路径解析就是"逐级查表"。

3. 以为删除文件就是擦除数据

rm 删的是目录里的名字条目、并把 inode 链接计数减 1,只有计数归零才回收空间,且通常不立刻擦内容。这解释了"删得快"“能恢复”"删了空间没释放"等一系列现象。

4. 忽视 inode 也是会耗尽的资源

磁盘空间 = 数据块 + inode 两种资源。海量小文件可能耗尽 inode 却没用完数据块,导致"磁盘没满却写不进"。排查要用 df -i,光看 df 会被骗。

总结

文件系统的价值,不只是"把数据写进磁盘",而是把杂乱的块设备组织成可命名、可保护、可长期维护的结构化空间。把机制和结论一起带走:

  • 文件 = 内容 + 元数据;文件系统的大量工作其实是在管理元数据;
  • inode 是每个文件唯一的元数据档案,含权限、大小、时间戳、数据块指针,唯独不含文件名
  • 目录是一种特殊文件,内容是「文件名 → inode 号」的映射表,文件名存在这里;
  • 路径解析从根开始逐级查目录映射表找 inode,沿途每级目录都要权限检查,名字解析与内容读取是分离的两阶段;
  • 删除删的是名字条目 + 链接计数减 1,计数归零才回收空间且不立刻擦内容——解释了快删、可恢复、空间不释放等现象;
  • 文件系统在裸块设备上建立了命名、组织、定位、保护、一致性五种能力,而它们的载体几乎都是元数据;
  • 工程上:No space 可能是 inode 耗尽(df -i)、rm 后空间不释放多因文件仍被打开(lsof | grep deleted)。

当你能解释"删了文件磁盘空间为什么没回来"“磁盘没满为什么写不进去”,并知道用 df -ilsof 去排查,这一篇就真正落地了——文件系统不再是抽象概念,而是你排障工具箱里的一套利器。

参考资源

  • 《现代操作系统》(Andrew S. Tanenbaum)文件系统章节
  • 《操作系统导论》(Operating Systems: Three Easy Pieces)File System Implementation
  • OSTEP - File System Implementation
  • 《深入理解计算机系统》系统级 I/O 章节