文件系统基础:文件、目录与元数据
你写一行 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") 时,系统怎么从这串路径找到磁盘上的内容?这个过程叫路径解析,它是从根目录开始、一级一级查映射表的过程。
逐步拆解这条链:
- 从根开始:绝对路径以
/开头,解析从根目录的 inode 出发(根 inode 号是约定好的固定值); - 逐级查表:读根目录的内容(那张映射表),在里面找名字
var,得到var目录的 inode 号;再读var的 inode、读它的目录内容,找log,得到log的 inode 号;再如法炮制找到app.log的 inode 号; - 拿到目标 inode:读出
app.log的 inode,这里做两件事——先按 inode 里的权限位检查你有没有资格访问(权限检查),再取出"内容在哪几个块"的数据块指针; - 读内容:按数据块指针去磁盘对应的块里把字节读出来。
从这条链能看出几个深刻的点:
- 路径解析是逐级的、有成本的:路径越深,要读的目录和 inode 越多。这就是为什么超深的目录层级会拖慢文件访问,也是为什么文件系统要缓存目录项(dentry cache)来加速;
- 权限检查嵌在解析里:不光目标文件,路径上每一级目录都要有执行(x)权限你才能"穿过"它。这解释了为什么有时文件本身可读、却因为某层父目录没权限而打不开;
- 名字解析和内容读取是分离的两个阶段:前面全程在查目录映射表(找 inode 号),最后才真正碰文件内容。
五、删除文件的真相:动的是元数据,不是内容
理解了 inode、目录、链接计数,一个常被误解的操作就豁然开朗了——删除文件(rm)到底删了什么?
直觉上你以为 rm 会把磁盘上那些数据块的字节擦掉。但实际上,删除一个文件名时,文件系统做的是:
- 从所属目录的映射表里删掉那条「文件名 → inode 号」的条目(名字没了);
- 把该 inode 的链接计数减 1;
- 只有当链接计数减到 0(再没有任何文件名指向它)时,才真正回收这个 inode 和它的数据块。而且通常也只是标记为"可用",并不立刻擦除数据内容。
这串机制解释了一连串现象:
- 为什么删大文件那么快:删的只是一条目录条目 + 改个计数,根本不动 GB 级的内容数据,当然快;
- 为什么删了还能恢复(有时):数据块没被擦,只是被标记可用,被覆盖前用恢复工具还能捞回来;
- 为什么一个文件还开着、
rm了却没真正释放空间:进程打开文件会占一个引用,链接计数(含打开引用)不为 0,空间不会回收——直到进程关闭它。这正是线上"明明rm了日志,磁盘空间却没回来"的经典坑; - 硬链接为什么删一个名字不影响另一个:因为还有别的名字指向同一 inode,计数没到 0。
删除文件删的是"名字到 inode 的链接",不是内容本身。只有当指向某 inode 的链接计数归零,它的空间才被回收——而且往往只是标记可用、不立刻擦除。
六、看清机制后,回答:为什么非要这么设计
现在机制都清楚了,回头看那个核心问题:文件系统为什么不只是"把数据写进磁盘",非要搞出 inode、目录、元数据这一整套?因为它要在裸块设备之上同时建立五种能力,每一种都对应了上面机制的一部分:
- 命名:让人能用
/var/log/app.log这种名字访问数据,而不是记一堆块号——靠目录的「名字→inode」映射; - 组织:把海量文件组织成可浏览、可管理的层级结构——靠目录可以嵌套目录(目录也是文件);
- 定位:从名字快速找到内容到底在磁盘哪几个块——靠 inode 里的数据块指针 + 路径解析;
- 保护:控制谁能读、谁能写、谁能执行——靠 inode 里的权限位 + 路径上每级目录的权限检查;
- 一致性与持久:保证断电、崩溃后元数据和数据不至于错乱——靠日志(journaling)等机制(这是更进阶的话题,但根基也在元数据管理)。
把这五件事串起来你会发现:它们几乎全都围绕元数据展开。 命名靠目录(元数据),定位靠 inode 指针(元数据),保护靠权限位(元数据),一致性首先要保的也是元数据。这就是为什么我们一开始强调"文件 = 内容 + 元数据",并说文件系统的大量工作其实是在管理元数据——因为正是元数据,把笨拙的块设备变成了智能的、可用的文件抽象。
文件系统的价值不在"存字节"(磁盘自己就能干),而在围绕字节建立起命名、组织、定位、保护、一致性这五种能力——而这五种能力的载体,几乎全是元数据。
七、和工程实践 / SRE 的联系
文件系统的机制,直接对应后端和 SRE 的一类高频故障。理解元数据视角,很多"灵异现象"立刻就有了解释。
- “磁盘没满,却报 No space left on device”:磁盘空间分两种——数据块和 inode。inode 数量是格式化时定死的。如果你创建了海量小文件(如缓存碎片、大量小日志),可能数据块还很空,但 inode 用尽了,照样写不进新文件。用
df -i查 inode 使用率,是排查这类问题的关键一招。 - “
rm了大日志,磁盘空间却没释放”:第五节讲过——只要还有进程打开着那个被删的文件,链接计数(含打开引用)不为 0,空间就不回收。解决办法是重启/通知那个进程关闭文件(或truncate),而不是反复rm。lsof | grep deleted能揪出这类被删但仍占用空间的文件。 - 深路径 / 海量小文件拖慢访问:路径解析是逐级查 inode 和目录的,目录里条目过多(一个目录放几十万文件)会让查找变慢。这就是为什么很多系统按
xx/yy/文件的方式做散列分层目录,避免单目录文件爆炸。 - 权限问题排查:文件本身可读却打不开,常是某层父目录缺执行权限——因为路径解析要"穿过"每一级目录。理解权限检查嵌在路径解析里,排查就有方向。
- 数据库/对象存储复用了这套思想:很多存储系统在自己层面重建了类似结构——用"元数据 + 数据"分离、用索引(类比 inode)定位数据块。理解文件系统,能帮你看懂这些系统的底层组织。
八、动手做一个小实验:观察内容与元数据的分离
理解"内容 vs 元数据"最直观的方式,是亲手观察它们如何独立变化。
- 创建一个文件
echo hello > a.txt,用stat a.txt查看它的 inode 号、大小、权限、各种时间戳——这些全是元数据; - 只改元数据、不改内容:执行
chmod 600 a.txt(改权限)或touch a.txt(改时间戳),再stat一次。你会看到权限/时间变了,但文件大小和内容没变——元数据和内容是分开管理的; - 验证目录是「名字→inode」映射:用
ls -i a.txt看它的 inode 号,然后ln a.txt b.txt建一个硬链接,再ls -i a.txt b.txt——两个名字的 inode 号完全相同!再stat看链接计数从 1 变成了 2。这直接印证了第三、四节; - 验证删除的真相:
rm a.txt后stat b.txt,文件内容还在(因为 inode 链接计数还有 1)。删名字 ≠ 删内容; - 进阶:
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 -i 和 lsof 去排查,这一篇就真正落地了——文件系统不再是抽象概念,而是你排障工具箱里的一套利器。
参考资源:
- 《现代操作系统》(Andrew S. Tanenbaum)文件系统章节
- 《操作系统导论》(Operating Systems: Three Easy Pieces)File System Implementation
- OSTEP - File System Implementation
- 《深入理解计算机系统》系统级 I/O 章节