深入理解文件系统:inode、块目录与存储链路实战笔记 这份笔记我拖了两周才真正动手整理。前10期把进程、线程、内存管理这些主线啃完之后第11期我原本打算随便聊聊就过去了结果一翻开书才发现存储这块内容远比自己印象中复杂得多。磁盘、文件系统、inode、挂载、页缓存……单独看每个概念都似曾相识串在一起却经常对不上号。尤其是后来真的动手做排查、写脚本模拟实验之后才意识到前面学的那些知识凡是涉及到“落盘”的操作其实都得靠这一期来托底。这篇算是我对“计算机基础笔记-11”的完整记录内容围绕文件系统的底层原理、核心数据结构、排查方法和实操验证展开。无论你是正在复习计算机基础、准备面试还是工作中遇到磁盘写满、空间未释放这类问题这篇文章都值得多看两遍。我会尽量把底层原理解释得接地气一些也会把我自己在实验里踩过的坑、试出来的经验一并写出来。1. 为什么第11期要啃文件系统这个硬骨头1.1 从整个课程地图看前面的积累如何在这里交汇我把前10期的内容粗略分成了三条线CPU线进程、线程、调度、内存线虚拟内存、页表、缺页中断、并发线锁、信号量、原子操作。到了第11期这三条线开始明显往一个方向收拢那就是I/O。任何一个进程要运行总得读取指令、加载数据任何一次系统调用产生的结果要么留在内存要么最终写到磁盘。如果不懂文件系统前面学的进程模型其实都是悬空的。举个例子进程被换出内存的时候它的整个地址空间可能被交换到磁盘的交换分区而交换分区本身就是一个文件系统。线程在跑日志的时候每一行输出都要经过文件描述符找到inode、找到数据块再经过页缓存刷盘。这些链路里每一步都可能成为瓶颈。所以在看这部分内容时我给自己定的目标不是“记住概念”而是能达到这样一个程度看到任何一个I/O现象能顺着整条链路把原因推导出来。1.2 文件系统到底在解决什么问题很多人会把“文件系统”和“磁盘”当成一回事实际上它们是两个层次。磁盘提供的是一块可以随机读写的连续存储空间类比成一块完全没有分隔的空白地皮而文件系统则是地皮上的设计图纸和物业管理制度负责划分地块、登记产权、管理公共通道。没有文件系统你只能面对一堆扇区发愁想存一个10MB的文档得自己记录它放在哪些扇区、怎么拼接这不现实。文件系统要解决的核心问题有三个一是命名和定位给定一个路径名能高效找到对应的数据二是空间管理哪些块被用了、哪些块空闲不能被写乱三是可靠性和效率断电、崩溃后数据不能烂成一堆频繁读写时性能不能崩。整个存储技术栈基本都是围绕这三个问题展开的。1.3 先建立一个核心认知文件是“假的”磁盘是“真的”这个观点是我的一位同事提醒我的当时觉得挺震撼。我们每天看到的目录结构、文件名、路径本质上都是操作系统用文件系统元数据搭建出来的虚拟视图。真实磁盘上只有扇区、块、偏移量没有所谓的“目录”也没有“文件名”。目录在磁盘上其实也是一个文件只是它里面存放的是文件名到inode的映射关系。理解了这一点之后后面很多难题就豁然开朗了。比如为什么移动文件到同一个分区几乎瞬时完成因为只是改了目录项里的名字映射数据块根本没动。为什么复制文件到另一个分区速度差异很大因为要真正创建新的inode并拷贝数据块。这些现象如果只看表面永远只是“经验”理解了映射关系才是“原理”。2. inode、块和目录结构把文件系统的骨架拆开看2.1 inode是理解文件系统的一把钥匙inode索引节点是整个文件系统里最重要的数据结构可以把inode理解成文件的“户口本”上面记录着文件是谁的、多大、什么时候改的、存在哪里。更关键的是inode里保存了文件的块地址列表即这个文件占据的磁盘块位置。在做实验的时候我发现很多新人拿到一本数据结构书看完链表、树、哈希表之后总觉得这些结构缺少实际应用场景。其实文件系统的块寻址就是典型的混合索引结构里面同时用了直接指针、一级间接指针、二级间接指针。直接指针指向实际数据块适合小文件一级间接指针指向一个存了地址的块那块里的每条地址再指向真实数据块二级间接指针再往外套一层。这种方式的好处是大多数文件都很小用直接指针就够了不需要额外开销而超大文件比如动辄几十GB的镜像又能通过间接指针向上扩展。我在笔记里画了个丑丑的示意图但越看越觉得设计得巧妙这是典型的以少量复杂度换空间利用率和访问效率的做法。inode本身是有限的格式化文件系统时就会确定inode总数。inode个数决定了整个文件系统最多能容纳多少个文件哪怕磁盘还在空闲inode用完了也写不进新文件。这个场景很容易被忽略等到了第4章排查部分我会专门展开。2.2 目录项和数据块读取一个文件的全过程打开一个文件通常是先经过路径解析。以/home/user/notes.txt为例文件系统会先从根目录的inode出发根目录的内容里有一个目录项“home”它指向home目录的inode再在home目录的内容里找到“user”目录项进入user目录最终在user目录里找到“notes.txt”这个目录项拿到目标文件的inode号。拿到inode之后通过inode里的块地址列表按偏移读取对应的数据块组装成你真正需要的内容。这里“目录项”和“目录文件的内容”很容易混淆。目录项是一个内存态的结构主要用于路径查找的缓存和快速匹配而目录内容在磁盘上会持久化地记录一组“名字-inode号”的条目。理解了这层关系你会明白为什么路径遍历会随着路径深度变长而变慢为什么目录的缓存dentry cache能大幅提升重复访问的速度。另外读取文件时还有一个容易被忽视的中间层就是页缓存page cache。每次读取不是直接访问磁盘而是先从页缓存里找找不到才发起真正的磁盘I/O。写入也是如此先写进页缓存再按策略刷盘。这样设计是为了弥合CPU速度和磁盘速度的巨大鸿沟。理解这一点之后才能明白为什么有时候程序write完数据进程退出数据却“丢”了因为还没刷盘。这不是玄学而是缓存策略的固有行为。2.3 硬链接与软链接两个最容易混淆的概念链接问题是面试常客也是实际运维的高频场景。硬链接的本质是多个目录项共享同一个inode号也就是说同一个文件在目录树上呈现出多个入口。硬链接不能跨文件系统因为inode号只有在同一个文件系统内才唯一。软链接则不同它本身就是一个独立的小文件这个文件的inode里存放的内容是另一个路径的字符串指向时可以跨文件系统。我在笔记里自己试过一组命令echo hello /tmp/ori.txt ln /tmp/ori.txt /tmp/hard.txt ln -s /tmp/ori.txt /tmp/soft.txt rm /tmp/ori.txt cat /tmp/hard.txt cat /tmp/soft.txt结果很有画面感hard.txt还能读因为inode引用计数减到1但还指向数据块soft.txt则报出“文件或目录不存在”因为软链接存的是路径字符串路径被删除后链接就断了。这一组实验做完硬链接和软链接的区别就再也忘不掉了。顺带一提很多日志轮转工具用软链接指向当前日志文件就是为了在切换文件时不打断写入方的路径引用。3. 实操用几种方式把文件系统彻底看透3.1 准备一个趁手的实验环境我建议准备一台Linux虚拟机或者用空闲的移动硬盘、U盘都行反正要在上面做一些格式化操作必须确保数据有备份。我自己是在一台旧笔记本上装了Linux发行版然后在上面挂了一块空硬盘来做实验。如果你手头没有备用设备也可以用dd命令生成一个固定大小的镜像文件再在镜像文件上建文件系统这样最安全也好清理。dd if/dev/zero of/tmp/testfs.img bs1M count512 mkfs.ext4 /tmp/testfs.img mkdir -p /mnt/testfs mount -o loop /tmp/testfs.img /mnt/testfs把镜像文件当作块设备挂载就能在普通用户的机器上体验完整的格式化、挂载流程。我第一次用这种方式实验的时候觉得特别方便不用反复找物理设备出错后直接删掉镜像文件重来零风险。3.2 用stat命令观察inode信息stat是非常直观的查看inode信息的工具。在实验目录里创建一个文件然后执行echo hello world /mnt/testfs/note.txt stat /mnt/testfs/note.txt输出会包括文件大小、块数、inode号、文件权限、时间戳等。值得留意的是里面的Blocks字段单位是512字节块。比如文件内容只有11个字节但因为文件系统按块分配它依然占用了固定数量的块。在ext4上一般块大小是4096字节所以就算写一个字节的文件也得占4KB空间这就是“磁盘占用”和“文件大小”两个概念出现偏差的根源。还可以用df -i查看inode使用情况df -i /mnt/testfs这一步可以看到文件系统的inode总数、已使用、剩余和挂载点。很多人在df -h还剩很多空间但写不进文件的时候才想起检查inode不如养成定期看的习惯。3.3 du和df的差异一场测试搞懂工作中经常遇到“明明du统计出来的文件总大小不大但df显示磁盘快满了”的问题。这个现象背后的原因不搞清楚排查起来就会走弯路。du统计的是文件层视角下每个文件实际占用的块数df统计的是文件系统视角下所有已分配块的总数。如果一个文件被删除了但某个进程还持有它的文件描述符数据块并没释放du已经算不到它了df却依然记着它。我做了这样一个小实验来复现dd if/dev/zero of/mnt/testfs/bigfile bs1M count200 cat /mnt/testfs/bigfile /dev/null 或者更直接一点用脚本打开文件后睡眠几十秒期间把文件删掉。删除后立刻看du和df差异非常大。这个实验做完之后我对“删除文件不等于释放空间”这个问题的理解才真正从“背过答案”变成“亲眼所见”。后面在工作里遇到磁盘没释放的情况我可以第一反应就去查有没有进程还在占用被删除的文件。3.4 把一个完整的挂载流程走通挂载的概念来自Unix的设计理念一切访问都要先有一个统一的入口。不挂载磁盘分区上的数据对系统并不可见。常见的误区是认为挂载和Windows里“插入U盘自动分配盘符”差不多实际上挂载可以发生在任意目录甚至可以把一个已存在数据的目录当作挂载点挂载后原目录内容会被暂时隐藏卸载后又会显现出来。实际实验时我建议把挂载和卸载反复做几遍同时用mount、findmnt、blkid这几个命令交叉验证。比如umount /mnt/testfs blkid /tmp/testfs.imgblkid能显示镜像文件里的文件系统类型和UUID。文件系统在格式化时会生成UUID这就解释了为什么/etc/fstab里可以用UUID指定分区而不是用设备名。其实这也是一个很稳定的定位方式设备名在系统重启后可能变化UUID却相对稳定。4. 常见问题与排查技巧实录4.1 磁盘明明显示还有空间却写不进文件这类问题的排查路径很有套路。先df -h看空间然后df -i看inode。如果inode使用率接近100%那基本可以确认是inode耗尽。我当时实验时故意创建了海量小文件几万个空文件就能把一个几百MB的镜像文件系统的inode占满而数据块还剩下很多。解决办法是找出这些占用inode的文件集中归档或删除或者在未来规划文件系统时把inode数量调大比如mkfs.ext4 -i 4096等参数调小inode字节比。另一个隐藏原因可能是预留块。ext系文件系统默认会留5%的块给root用户单独使用普通用户写满到这一阈值之后就会被拒绝但df并不会把这部分标成特殊状态。遇到这个情况可以tune2fs -l /dev/xxx查看预留比例必要时用tune2fs -m 1调低但生产环境建议保留默认值这是为了防文件系统碎片化和给紧急维护留余地。4.2 删除了大文件df显示空间依然没释放这一点在上文的结构层面已经解释了但排查时还需要一个能直接定位进程的命令组合。我常用的手法是lsof | grep deleted或者通过ls -l /proc/*/fd/ 2/dev/null | grep deleted找到持有已删除文件描述符的进程。找到以后重启对应的服务进程空间就会立刻释放。需要注意某些常驻服务的日志文件被轮转后依然被进程持有导致磁盘空间持续上涨这类问题在线上环境特别常见。一个更稳妥的做法是在服务启动脚本里对日志文件做“复制-截断”操作而不是简单删除文件这样既保证进程持续可写又释放了已轮转文件的占位。4.3 页缓存导致的假空间占用另一个让df看起来“不正常”的因素是页缓存。往磁盘写入大量数据后如果内核尚未把页缓存刷盘df统计的还是文件系统元数据层面的情况通常不会出现“已用空间”降低的假象。但如果你在清理空间时误删了大文件紧接着sync了一下看到空间立即释放了一部分这里是正常流程不用紧张。真正容易被迷惑的是读大文件时页缓存占用大量内存这时候用free -h看内存剩余很少似乎系统卡顿但实际这只是缓存机制在工作。清理缓存的直接手段是echo 3 /proc/sys/vm/drop_caches但我不建议在业务运行期随便执行。更好的做法是理解页缓存的自动淘汰机制让内核自己管理除非是单次实验需要干净的缓存环境。4.4 各种链接引发的路径坑软链接在定位问题上会带来一个经典困惑你cat /var/log/current.log发现文件显示的内容是旧的排查半天才意识到current.log是个软链接目标位置已经切换了而检测工具或监控脚本还在轮询旧路径。这类问题在日志系统里最容易出现。解决办法是在排查脚本里加上readlink -f把真实路径解析出来再判断文件是否存在。硬链接引发的坑也有意思。如果误删了一个文件以为数据没了结果另一个目录里还保存着硬链接文件内容其实还在。这在备份场景中可能成为残影数据即多处副本指向同一块数据而导致空间统计虚高。所以在备份工具选型时要明确是“复制数据”还是“创建硬链接”否则可能造成意想不到的连锁删除。5. 把整条存储链路重新走一遍我看到了什么5.1 一次read()系统调用的幕后链路学到这里我尝试把一次平凡的read()调用从头串到尾。应用程序调用read(fd, buf, len)进入内核后先通过文件描述符找到对应的文件对象文件对象里有指向dentry和inode的指针在dentry缓存里查路径命中则跳过磁盘的元数据读取接着计算目标数据在文件内的偏移映射到inode的数据块地址再在页缓存里查找对应的数据页如果页缓存未命中才会启动真正的磁盘I/O把数据块读出来之后放入页缓存最后拷贝到用户缓冲区。这条链路里的每个环节都有命中和未命中两种情况也都有各自的缓存策略和淘汰算法。以前学习的时候每部分单独拎出来都能说出个大概但真正串起来时才发现之前的理解是孤立的、单点的。现在再看这个问题脑子里会自动浮现出缓存命中率、块分配方式、I/O调度、磁盘寻道时间等多个维度的交互这种系统性眼光比记住某个具体结论重要得多。5.2 这次笔记留给我的三个实战意识第一看到“磁盘满”不要只盯空间而要有inode和进程占用两个备选方向。第二任何删除操作都要多想一步有没有进程持有fd有没有链接指向它确认清楚了再执行。第三对文件系统的理解不能停留在命令层面文件是虚拟的磁盘是真实的中间靠inode、目录项、页缓存这些结构搭桥。想通这一点很多问题的排查方向就自然浮出来了。我还是很推荐每一个准备系统学习计算机基础的人在第11期这个位置确实停下来认真做一轮文件系统实验。这一块内容不像并发编程那样轰轰烈烈也不像网络那样有大量协议细节可以讨论但它绝不是一个可以跳过的小节。我自己做完这些实验再回头读进程管理与内存管理章节很多之前没看懂的设计点突然就通了比如为什么线程的栈要按页对齐、为什么进程切换时要伴随地址空间操作这些都是为了和文件系统、页缓存体系顺畅协作。后面几期我准备顺着这一期继续往I/O调度和存储性能调优的方向推进争取把整条存储链路完全吃透。