ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux文件系统底层机制详解:VFS、inode、页缓存与日志

Linux文件系统底层机制详解:VFS、inode、页缓存与日志 写这篇文章完全是接着上一篇的思路来的。上一篇聊了 Linux 文件系统的基础概念今天这篇想再往里钻一钻把那些面试经常问、日常排查问题绕不开的底层机制讲清楚。文件系统看起来只是把数据存进磁盘可一旦深入就会发现它其实是一整套缓存、索引、日志、层次结构配合出来的结果。理解了这些之后再去看df、du、fsck、mount这些命令很多莫名其妙的输出马上就说得通了。这篇文章适合刚接触 Linux 运维或后端开发的读者也适合准备 Linux 面试的人。我会从 VFS 开始把 inode、dentry、page cache、挂载机制、日志文件系统这些核心概念讲透再用实际工作中经常踩的坑来验证这些知识点。1. 文件系统到底是什么1.1 从用户的一个小困惑说起有一次同事发现服务器上的某个目录删不掉报错说空间不足但用df -h一看根分区明明还有几十 GB 可用。于是他怀疑磁盘坏了直接把整台机器重启了一遍。重启后问题依旧最后才发现是目录里有一个已经被进程删除但仍占着句柄的日志文件空间被“占而不放”而且碰巧另一个分区被 inode 耗尽。这个场景很典型如果不了解文件系统的工作方式完全不可能靠试错解决。文件系统对普通用户来说就是一串目录和文件。但对内核来说它是一个非常复杂的软件层。它要负责把块设备的扇区组织成我们看到的目录树要处理文件名的检索要记录每个文件占用了哪些磁盘块还要保证系统崩溃之后不至于整个文件全丢。你发出的每一次read、write、open、stat最终都会经过文件系统这层转化变成对设备的具体操作。1.2 文件系统分层模型Linux 的文件系统大致分成三层最上层是系统调用接口中间是虚拟文件系统层最下面是具体的文件系统实现和块设备驱动。系统调用接口不用多说就是我们在用户态调的open、read、write、close这些。中间这一层才是今天的主角名字叫 VFS虚拟文件系统。它存在的意义是让所有文件系统用同一套方式对上提供接口对上层的应用而言ext4、XFS、Btrfs、FAT、NFS 都只是一个“文件系统”不需要关心底层差异。第三层是各种具体实现比如 ext4 处理 extents 和日志XFS 处理自己的 B 树元数据NFS 走网络 RPC。每家的存储布局可以完全不同但在 VFS 眼里大家都是同一套操作的实现者。这个分层设计最大的好处是扩展性和统一性。系统管理员可以随意在 /、/home、/data 上挂不同的文件系统应用程序不用重新编译。配合 mount 机制甚至可以直接把网络存储和本地目录无缝融合。2. VFS 里的四个核心对象2.1 为什么必须有一个虚拟层如果你同时用过 Windows 和 Linux就会发现一个有意思的现象Windows 的盘符 C 盘、D 盘是固定的而 Linux 只有一个根目录U 盘、移动硬盘、光驱挂上来之后都变成根目录下面的某个目录。这个设计不是随意的它正是 VFS 存在的理由之一。VFS 提供了一套标准的数据结构和接口让内核不需要关心设备是谁家的。比如你插入一个 U 盘内核识别出设备后会按设备上的文件系统类型调用对应的驱动去读取超级块然后在根目录的某个挂载点建立起这个文件系统与 VFS 的连接。之后你访问/mnt/usb/a.txtVFS 会先找到/mnt/usb对应的是哪个具体文件系统然后把路径解析任务交给那个文件系统。2.2 inode、dentry、file、super_block真正理解 VFS必须把这四个结构分清楚。第一个是 super_block超级块。它描述的是整个文件系统的全局信息比如块大小、总块数、空闲块数、文件系统状态、挂载选项。一个文件系统被挂载时内核就要读它的超级块。ext4 的超级块还有多个副本分别放在不同的块组里主副本损坏时可以从备份恢复。第二个是 inode索引节点。它保存一个文件的元数据包括文件类型、权限、属主、属组、大小、时间戳、数据块位置以及硬链接数。目录本身也是一个 inode只不过里面的数据是一张从文件名到 inode 编号的映射表。inode 不保存文件名文件名是放在上级目录的数据块里的这个设计导致了一个非常重要的结果硬链接本质上就是多个目录项指向同一个 inode。第三个是 dentry目录项。它主要是缓存路径解析结果的。比如你访问/var/log/messages内核需要从根目录开始逐级查先查var的目录项再查log最后查messages。如果每次都重新读磁盘会很慢。dentry 缓存就是用来加速这个过程的内存层。第四个是 file文件对象。它不是持久存在的而是每次打开文件时创建的。它记录了当前读写位置、打开模式、对应的 dentry、以及对 inode 的引用。多个 file 可以指向同一个 inode比如同一个文件被打开两次或者被多个进程打开。这几个对象之间的关系是进程打开文件时拿到 file 对象通过 file 找到对应的 dentrydentry 指向 inodeinode 负责管理真实的磁盘数据。理解了这个链条你就能解释“为什么删除一个正在被占用的文件空间不释放”“为什么硬链接的文件修改会影响所有名字”“为什么 stat 和 ls 经常显示完全一样的 inode 信息”。还有一个不算常用的对象是 address_space它把文件页缓存和具体文件的 inode 关联起来。每个 inode 都有一个 address_space里面是页面树。读文件时如果页面在缓存里直接返回内容不需要访问磁盘。写文件时先写缓存后面再异步刷盘。2.3 页缓存与回写机制page cache 是 Linux 性能的基石。你读一个文件内核会按页把内容读进内存再次读同一页时直接内存返回速度提升几个数量级。写也是一样write系统调用默认先写缓存标记脏页然后由内核在合适的时间真正写到磁盘。这个机制在绝大多数场景下是对的但它也带来一个副作用断电或系统崩溃时缓存里的数据可能还没落盘。这里就涉及/proc/sys/vm/dirty_ratio、dirty_background_ratio这些参数。它们控制脏页的高水位和后台回写时机。dirty_ratio 默认大约是 20也就是当脏页占内存比例达到 20% 时用户进程自己的写操作会被阻塞强制刷盘。后台回写的阈值则低一些大概 10到了这个比例内核会启动后台线程慢慢写。生产环境里如果大量小文件频繁写可能会出现一写就卡一下的情况这往往就是 dirty_ratio 触发了同步限制。sync 命令的作用就是强制把所有脏数据刷到磁盘。它不只是刷 page cache还会把文件系统元数据也刷下去。真正常用的做法是在停运维、拔移动硬盘前执行一个 sync尽量减少数据丢失窗口。3. 磁盘上的文件系统怎么选3.1 ext4 的关键机制ext4 是目前最通用的默认文件系统之一它把 ext3 的很多功能做了升级比较关键的是 extents、多块分配和日志校验。先看 extents。ext2、ext3 时代一个文件的数据块位置是记录在 inode 的 block 数组里的每个块都要记录块号文件一碎片化就非常慢。ext4 用 extent 树来表示连续的数据块范围一次记录起始块号和长度大文件的元数据开销大幅度下降连续文件的读写性能也更快了。再看日志。ext4 默认是 journaled 模式也就是把元数据变更先写入一个日志区域然后再修改实际位置。这样做是为了保证崩溃恢复时一致性。默认的 data 模式是 ordered意思是对元数据记日志在元数据提交前保证相应的数据块已经写到磁盘。这样即使断电最多丢失刚写入的数据但不会出现目录项指向尚未写入的 inode 这种“半成品”状态。用 mkfs.ext4 时可以通过-O参数关闭或开启某些特性比如64bit、metadata_csum这些。生产上新格式化磁盘时我一般会加上-E lazy_itable_init1来避免格式化阶段全盘扫描 inode加快初始化速度。3.2 XFS 与大文件场景XFS 是老牌的高性能文件系统特别适合大文件、大分区。它设计之初就考虑了并行 IO元数据用 B 树管理支持大量并发操作。RHEL 7、8 默认就是 XFSCentOS 7 之后也是。XFS 没有传统意义的“块组”而是把空间切割成多个分配组每个分配组有独立的 inode 分配和管理结构。这就意味着并发的文件创建和删除可以在不同的分配组并行全局锁冲突少。对高并发小文件写入XFS 的表现也很稳定。XFS 的日志只记录元数据不支持文件数据日志但这不意味着不安全。它内部有一套复杂的恢复机制崩溃后扫描日志和 AGI 结构把文件系统恢复到一致状态。日常运维中如果遇到 XFS 分区只读挂载dmesg往往已经提醒了 metadata 有问题需要xfs_repair处理。一个值得注意的点XFS 暂时无法收缩文件系统大小也就是不能用resize2fs那种方式缩小分区。所以给 XFS 分区做 LVM 时最好预留一些调整空间否则要缩分区只能迁移数据重建。3.3 Btrfs 的 COW 与快照Btrfs 是一个具有很多“现代”特性的文件系统比如写入时复制、快照、校验和、子卷、在线压缩。它把磁盘组织成树状结构所有元数据和数据都可以按树来管理子卷就是独立的树根因此给一个子卷做快照成本非常低几乎是零拷贝因为快照只是复制树的根部记录。COW 的机制是写文件时不直接覆盖旧数据块而是新分配一个块写入然后更新元数据指向新块旧块在确认没有引用之后才被回收。这意味着任何时候磁盘上都保留了一个历史上某个时刻的一致状态快照和回滚才有意义。Btrfs 的缺点一直也很明显早期稳定性差修复工具不如 ext4 和 XFS 成熟但在很多 NAS、个人存储场景下越来越流行。如果要用 Btrfs我建议至少用内核 4.19 以上的版本而且要开启 scrub定期扫描磁盘错误作为习惯千万别把生产关键数据库硬塞给 Btrfs 当小白鼠。选型上简单说默认无脑用发行版默认就好CentOS/RHEL 默认 XFSUbuntu 默认 ext4数据安全要求高、需要快照可以用 Btrfs如果做海量小文件存储也可以考虑 ext4 或者 XFS配合高内存和高 IOPS。4. 实操中的文件系统管理4.1 查看和确认文件系统类型新接手一台服务器第一件事就是知道每个分区是什么文件系统、挂载参数是什么。用findmnt或lsblk -f最直观。findmnt /data # 或者 lsblk -f /dev/sdb1但有一点容易忽略df -T显示的是挂载点所在文件系统的类型blkid看的是块设备上的标识信息。如果某个目录只是 bind mount 或者覆盖挂载df -T结果可能和你预想的不一样。排查挂载关系时用findmnt -a会看到完整结构而且能识别出哪些是子挂载。/proc/mounts是内核视角的真实挂载状态有些基于 FUSE 的文件系统只在挂载点出现了进程不一定在这里看到明显差异。管理时多几个命令交叉验证总没坏处。4.2 挂载与挂载参数挂载命令是 mount但生产环境的挂载参数远比默认值重要。常见优化是打开 noatime 或 relatime。默认情况下每次读文件都会更新 atime这会带来额外的写 IO。relatime 是折中方案只有在 atime 早于 mtime/ctime 时更新。而 noatime 则完全不更新适合大量读的场景能明显降低写压力。mount -o rw,noatime,dataordered /dev/sdb1 /data还有一个参数是discard它对应在线 TRIM 的功能。传统机械盘不需要SSD 则需要定期或即时回收空闲块。但生产环境建议用 fstrim 定时任务跑而不是一直开着 discard因为在线 discard 在高负载下可能与 SSD 垃圾回收产生冲突导致性能抖动。具体做法是 crontab 里每周跑一次fstrim -av。nofail 参数也得记一下。写 /etc/fstab 时如果某块设备在启动时不存在默认可能导致系统进入维护模式。加 nofail 之后启动可以跳过这个挂载点适合移动硬盘和备份盘。/dev/sdb1 /data ext4 defaults,noatime,nofail,x-systemd.device-timeout5 0 24.3 常见维护操作格式化文件系统最常用的三类是 ext4、XFS、Btrfs。mkfs.ext4 /dev/sdb1 mkfs.xfs /dev/sdb1 mkfs.btrfs /dev/sdb1格式化之前务必确认没选错盘命令不会二次确认。生产上我习惯先用lsblk和wipefs -n确认设备干净再用blkid看旧标识。resize 操作也要谨慎。ext4 可以resize2fs扩大缩小XFS 扩可以xfs_growfs缩不行。LVM 与文件系统配合时先扩 LV 再扩文件系统顺序不能反。lvextend -L 100G /dev/vg/data resize2fs /dev/vg/data # ext4 xfs_growfs /data # xfs这些操作在生产上一定要先看快照或备份尤其是缩容。用 ext4 缩分区时resize2fs要求卸载文件系统或者只读挂载否则拒绝执行。缩小到多少先用df看已用空间留足余量不然数据处理会中途失败。5. 深入问题排查5.1 磁盘满但 df 显示还有空间的秘密这是一个很典型的场景我用一个小步骤就能定位。df -h /data df -i /data du -sh /data/* lsof L1 /data先看空间是否满再看 inode 是否满。如果 df -h 显示 30G 用了 29G但 du 加起来只有 5G大概率是有已删除文件仍被进程占用。lsof L1会显示删除但仍打开的文件找到 PID 后重启进程或者等它释放空间就回来了。还有一种情况是目录下隐藏文件特别多du默认统计所有内容一般不会漏。但如果文件系统上存在大量未连接的孤儿文件比如 ext4 的 lostfound 目录塞满了东西du能统计到只是需要排查是不是之前发生过 fsck。inode 耗尽更隐蔽。df -h显示一切正常但touch新文件时报No space left on device。解决办法是清理海量小文件或者把目录迁移到支持更多 inode 的文件系统。大多数文件系统在 mkfs 时已经按照每多少字节一个 inode 的比例分配好了比如 ext4 默认 16KB 一个 inode。如果只存几百万个 4KB 小文件就只能调大 inode 数重新格式化。5.2 缓存、sync 与掉电保护有同事问我sync 到底有没有用。直接回答sync 确实能强制把页缓存里的脏数据写回磁盘但它不是万能保险。Linux 的写盘有很高的延迟容忍度默认策略追求性能如果在一个不稳定的环境比如树莓派拔电里不 sync文件丢失的概率相当高。从内核角度拆解一下你执行 sync 时会发生什么。首先它唤醒回写线程把当前所有文件的脏页变成写 IO 队列然后等待设备层把数据真正送到存储介质。对于机械盘意味着磁盘必须完成寻道和写入对于 SSD意味着 FTL 必须完成映射。内核通过submit_bio层知道写的完成sync 后关机相对更安全。应对掉电更稳妥的方式是使用带掉电保护电容的 RAID 卡或者 NVMe 盘它们能在断电瞬间把内容冲掉。如果没有这个条件至少要保持文件系统的日志特性完整比如 ext4 不要用datawriteback因为 less protective。它们的差别在于数据块和元数据块的一致性程度前面的 ordered 模式已经能保证元数据不引用还未落盘的数据块。5.3 文件系统损坏的应急处理Linux 通常不会无缘无故崩溃成无法挂载一旦出现就需要 fsck 或 xfs_repair。常见诱因是异常断电、磁盘坏道、强制关机。我实践中的步骤是# 先确认设备当前状态 dmesg | tail blkid /dev/sdb1 # 卸载目标分区避免在线检查 umount /dev/sdb1 fsck.ext4 -f -y /dev/sdb1如果是 XFSxfs_repair -n /dev/sdb1 # 先检查 xfs_repair /dev/sdb1 # 实际修复这里有一个坑在文件系统挂载状态下运行 fsck可能造成更大的破坏。即使忙也不能直接对个设备 fsck。很多云主机默认挂了根分区而你无法卸载它这时可以用系统维护模式启动或进入 rescue 环境。fsck 完成后丢失的文件可能被放到lostfound需要人工去看是否有可恢复数据。对于损坏程度大的盘先做磁盘镜像再修复更安全例如用dd或ddrescue把整块盘克隆到另一块盘。这块盘就变成只读备份然后再对副本进行 xfs_repair避免在坏盘上越修越坏。6. 一些我踩过的坑和经验做文件系统方向这么久有几个观念特别想分享。第一别把文件系统当成“存东西的地方”就完事了。文件系统的行为会影响数据库性能、备份可靠性、容器运行稳定性。很多疑难问题最终定位在文件系统层比如 ext4 的 delayed allocation 导致某目录出现空文件XFS 的 AG 冲突导致某进程卡在 D 状态。第二布局很重要。根分区和业务分区分开日志、数据库、临时目录分开。系统盘如果被大量日志写满会直接影响核心服务。备份盘、快照盘也不要和业务盘混在同一块物理盘上RAID 卡缓存策略也要考虑写回还是写透。第三监控要持续。日常关注磁盘空间、inode 数、IO 负载还要看文件系统是否转成只读。只是等到监控告警再去处理往往已经晚了。第四搞懂 sync 和文件系统刷盘策略再谈“性能优化”。很多人上来就调 dirty_ratio 或者关闭日志确实能提升一些吞吐但也有可能引入数据丢失风险。没有取舍决策的前提不建议盲调。最后说一个小技巧每次在 fstab 里改挂载项、格式化新盘之前先sync再执行。虽然这只是个习惯但它曾多次阻止我在错误操作前丢失重要数据。文件系统是 Linux 基础里最枯燥也最值得投入时间的一部分把这些机制吃透你再去看mount报错、IO 性能问题、数据恢复工具整个思路会清晰很多。
返回列表