提到Linux文件系统用不了多久你就会被superblock这个词拦住去路。我第一次在源码里遇到它时想当然地以为这只是磁盘上那块存着“文件系统总信息”的元数据区域直到后来排查一个挂载异常问题才发现它远比想象的复杂同样的名字既指磁盘上格式化的静态数据又指内存里每次挂载动态生成的结构体既承担VFS通用层的入口职责又通过一组回调函数把几十种文件系统全都串在一起。这篇文章是我系统性啃完superblock源码后整理的学习笔记适合正在读VFS层代码、或者想搞明白挂载/卸载/sync到底发生了什么的人。核心关键词就一个Linux内核的superblock超级块。我会先把它到底是什么讲清楚再顺着数据结构、生命周期、落盘一致性、调试手段这条线把我踩过的坑一并写出来。1. 我为什么盯上 superblock文件系统生态的核心枢纽1.1 从一个调用栈倒查superblock 总会出现如果你从任意一个文件操作出发比如最普通的open(/mnt/test/foo, O_CREAT)顺着内核调用栈往下追用不了多久就会撞见super_block。以路径查找为例open() - do_sys_open() - do_dentry_open() - path_openat() - walk_component() - lookup_fast() - lookup_slow() - inode iget_locked(sb, ino); - inode-i_op-lookup(inode, dentry, ...)这里的sb就是struct super_block *。每个 inode 都必须隶属于一个 superblock文件系统类型、设备号、最大文件长度、挂载选项、根目录 dentry 全部挂在它身上。换句话说只要你操作文件就离不开它。这种“无处不在”的感觉让我决定不再见到一个字段查一个字段而是系统性地从头读一遍。我后来发现几乎所有疑难问题都能归结到 superblock 的某个环节为什么某块设备被重复挂载时报device busy为什么umount后内核崩溃为什么sync看起来执行了但数据还是丢了答案都藏在这个结构体以及它对应的操作函数里。1.2 先分清楚磁盘超级块和 VFS 超级块这是第一道坎我见过不少同学把这两个概念混为一谈其实它们是两层东西。磁盘超级块是在执行mkfs.ext4时写进块设备固定位置的元数据描述的是“这个文件系统怎么布局”而 VFS 超级块是每次挂载时在内存里分配的通用结构描述的是“当前这个挂载实例运行得怎么样”。一份磁盘数据可以被多次挂载也就对应多个不同的 VFS superblock 实例但共用同一份磁盘超级块信息。用我的生活类比磁盘超级块像房产证上写明的房屋结构、面积、产权归属VFS 超级块则像你住进去后自己贴的便利贴——水电抄表数、门禁卡状态、昨天买了什么菜。前者是登记数据后者是运行时状态。对比项磁盘超级块VFS 超级块struct super_block存储位置块设备的固定偏移如 ext4 在偏移 1024 字节处RAM 中每次挂载动态分配作用描述文件系统静态布局和特性描述挂载实例状态并为 VFS 提供统一入口生命周期格式化时写入与文件系统共存挂载时创建卸载后销毁格式每种文件系统各自定义VFS 统一结构通过 s_fs_info 指向各文件系统私有数据搞懂这一层后再读源码就不会被“superblock”这一个词绕晕了。本文后面提到的superblock/sb默认指 VFS 层的struct super_block需要对比时我会明确写出“磁盘超级块”。2. struct super_block读懂这个结构体才算摸到门struct super_block是内核里出了名的大结构体字段多的时候接近上百个。我的经验是不要从头到尾死记而是按功能分成四组身份标识、挂载关系、锁与状态、缓存关联。下面按这条线逐一拆开。2.1 身份标识它代表“哪个设备上的哪种文件系统”第一组字段回答的是“我是谁”。日常调试最常用的有这几个s_id一个可读字符串通常是设备名比如/dev/sda1。内核日志、trace 输出里频繁出现排查问题时第一眼就靠它。s_dev与设备关联的设备号。注意有些内存文件系统没有块设备内核会为其分配一个虚拟伪设备号。s_magic文件系统魔数比如 ext4 是0xEF53XFS 是0x58465342。区分文件系统类型时靠这个字段。s_type指向struct file_system_type表示这是 ext4、xfs 还是别的什么。s_bdev指向struct block_device只有基于块设备的文件系统才有。s_fs_info一个void *指向当前文件系统的私有超级块结构。比如 ext4 里它通常指向struct ext4_sb_info里面保存了从磁盘读出的原始超级块副本以及大量运行时数据。s_maxbytes这个文件系统支持的最大文件大小。写文件时VFS 的generic_write_checks会在这一层做边界校验。有一点值得专门记一下s_fs_info是把 VFS 和具体文件系统解耦的关键。VFS 不需要知道 ext4 内部怎么组织只要s_fs_info能拿到私有数据、s_op能回调到具体实现就够了。2.2 挂载关系s_root、s_mounts 与全局链表第二组字段描述的是“我挂在哪儿、我怎么被找到”。s_root是文件系统根目录对应的 dentrys_mounts是该超级块上的所有挂载点列表。一个文件系统可以被挂载到多个位置每个位置是一个struct mount它们通过s_mounts串起来。struct super_block自身还会挂到全局的super_blocks链表中由s_list连接。内核遍历所有文件系统时比如执行sync()时就是从super_blocks链表逐个遍历再通过s_mounts处理每个挂载点。我写调试脚本时经常利用这个全局链表在 crash 工具里list super_blocks就能看到当前系统全部挂载实例。这也是理解“同一块设备重复挂载”的关键内核在分配新超级块之前会先通过sget系列函数在已有的超级块里找“同类型、同设备”的实例找到了就直接复用找不到才新建。所以同一个分区不需要真的在内存里存在多份 VFS superblock除非挂载时使用了某些特殊选项或经过网络文件系统的特殊处理。2.3 锁与状态标志为什么结构体里全是锁第三组字段是我最开始读源码时最不适应的地方一个 superblock 居然要维护一把读信号量、若干原子计数和一堆标志位。原因很简单superblock 是全局共享资源几十个 CPU 可能同时在读写同一个文件系统不加锁就全乱了。几个重要的s_umount读写信号量保护超级块的挂载状态。读文件时持有读锁umount/remount需要写锁。这也是为什么在挂载目录里执行cd /mnt/test后立刻umount会说不忙——因为那个进程还在持有s_umount读锁。s_active一个原子计数表示“这个超级块当前是否被使用”。当它从 1 降到 0说明没有挂载实例引用了符合销毁条件。s_count超级块本身的基础引用计数脱钩回收时用。s_flags、s_iflags一堆标志位比如SB_RDONLY表示只读挂载内部还有一些表示“正在销毁”“已有活跃挂载”等状态的标志。这里有个常见误区s_umount名字叫 umount但普通文件操作也依赖它。我之前有一次在调试回调函数里试图直接拿写锁结果在一个已经持有读锁的路径上死锁。记住处理优先级普通文件路径先拿s_umount读锁再调具体文件系统回调和umount则要拿写锁然后等所有读者退出。2.4 与 inode/file 的关联整个缓存体系的根第四组字段是缓存关联。s_inodes是当前文件系统所有 inode 的链表s_files是所有打开文件struct file的链表s_dentry_lru和s_inode_lru是 dentry 和 inode 的 LRU 回收队列。这套结构决定了什么比如你执行echo 2 /proc/sys/vm/drop_caches回收机制要从每个 superblock 的 LRU 链表开始逐个把这些缓存对象无效化又比如排查“某个文件删了磁盘空间还不释放”时本质上就是在 superblock 的s_inodes链表里找那个 still open 的 inode。可以这样理解superblock 是文件系统一切内存对象的“起点”每个对象都要通过它反向找到文件系统类型和私有数据。我阅读这一章的技巧是先只抓每个字段一句话的作用知道它属于哪一类等后面读具体调用路径时再回头记忆细节。这样比从头一行行啃效率高得多。3. super_operations 回调集VFS 怎么调用具体文件系统的实现3.1 一组明确分工的回调函数struct super_block里最关键的成员其实是s_op它指向struct super_operations。这是 VFS 与具体文件系统之间的“接口约定”文件系统通过实现这些回调把自己的能力暴露给 VFSVFS 则尽量不感知具体实现。我按调用时机整理了一份简表方便对照源码看回调函数调用时机典型作用s_alloc_inode分配 inode 时分配包含文件系统私有数据的 inode 对象s_dirty_inodeinode 被标记为脏时让文件系统知道某个 inode 需要回写s_write_inode写 inode 元数据时将内存 inode 的元数据落盘s_evict_inodeinode 从内存删除时回收 inode 相关资源截断文件等s_sync_fssync 时同步文件系统私有状态到磁盘s_statfsstatfs 系统调用时返回文件系统统计信息df 的结果就在这里产生s_put_super超级块销毁时释放文件系统私有数据s_remount_fsremount 时处理挂载选项变化s_shutdown文件系统进入停机状态时如检测到损坏后将文件系统标记为只读理解这套回调有个大方向文件操作每走一步VFS 都在调用这些函数。比如ext4_file_open、xfs_fs_sync_fs本质上都是s_op某个具体实现。3.2 不同文件系统实现风格差异极大一个让我印象很深的点是不同文件系统的super_operations差别巨大。ext4 这类需要日志、配额、错误恢复的本地文件系统s_op密密麻麻实现了一堆而像proc、sysfs这类内存文件系统很多回调是NULL或者极简实现。VFS 在调用前会做if (sb-s_op-xxx)判空所以少实现几个也没关系。也就是说s_op本质上是一份“能力声明”。你实现得越完整VFS 就能替你做得越多你实现得越少VFS 只能用最通用的逻辑兜底。我曾经为了给一个教学用的模拟文件系统写s_op只实现了s_alloc_inode、s_evict_inode和s_statfs其他全靠 VFS 的通用路径兜底照样能跑通基本功能。这也反向印证了读文件系统驱动代码时第一件事就是去它的super_operations表里看它到底承诺了哪些能力。4. 超级块生命周期挂载到卸载全链路4.1 分配前的 sget 与缓存复用挂载一个文件系统远不止“读取磁盘超级块数据”那么简单。以常见的块设备文件系统为例挂载路径大致是mount() - path_mount() - do_new_mount() - fs_context_for_mount() - vfs_get_tree() - fc-ops-get_tree(fc) - get_tree_bdev(fc) - sget_fc(fc, set_bdev_super, compare_bdev_super)get_tree_bdev做的就是先从现有超级块里找“同类型、同设备”的实例找到就直接复用找不到才调用sget_fc分配一个新的。之所以要先找是为了避免同一块设备反复挂载导致内存里有多个互相打架的 superblock。你可以在/proc/mounts里把同一个/dev/sda1挂到两个目录但内核里通常只有一个 VFS superblock只是有两个mount实例挂在s_mounts上。实际调试时sget的比较函数是区分不同文件系统行为的重要一环。有的文件系统如基于网络的可能不按块设备比较而是按服务器地址、导出路径比较这时候同一个远程目录挂两次就可能出现两个 superblock。记住这一点排查“为什么这里有两个 superblock”时非常有用。4.2 fill_super 与超级块状态转换分配完空的 superblock 后真正干活的函数是fill_super。以 ext4 为例ext4_fill_super会从块设备偏移 1024 字节处读出磁盘超级块做大量校验然后把解析结果填进s_fs_info指向的私有结构最后设置sb-s_op、sb-s_maxbytes、sb-s_root等字段。这个过程如果出错文件系统会返回一个负数错误码VFS 需要在错误路径里做清理。我在这里踩过一个大坑自己写学习用的模拟文件系统时fill_super中途失败我直接返回错误码结果发现之前分配的s_fs_info泄漏了。正确做法是任何在fill_super之前的分配都要在错误路径里显式释放或者依赖s_op-s_put_super在kill_sb时兜底。内核日志里常见的EXT4-fs (loop0): ...报错就是这一阶段校验失败的输出。fill_super成功之后内核才把 superblock 状态从“正在初始化”切到“已激活”然后挂载点才真正可用。所以你在dmesg里看到的文件系统错误大半都发生在这个窗口内。4.3 卸载、冻结与销毁最容易泄漏的地方卸载路径的关键函数是deactivate_super和kill_super。当最后一个挂载点被移除s_active计数变成 0内核会先调用文件系统的回写逻辑确保脏数据落盘再调用s_op-put_super释放私有数据最后在destroy_super里释放结构体本身。这个流程里最典型的坑有两个。第一个是异步任务没有取消。比如文件系统内部起了一个定期清理脏 inode 的 workqueue如果put_super里没有cancel_work_sync等 superblock 已经释放那个任务再访问s_fs_info就会触发 use-after-free表现是卸载后随机崩溃且栈回溯很难看。第二个是引用计数不平衡。任何人在自己的代码里调用了sb_get、get_super等增加s_count/s_active的操作就必须在对应路径里调用deactivate_super或put_super。漏了会让 superblock 永远无法销毁表现是umount后设备仍然 busy/proc/mounts里看不到挂载但内核上报device busy。遇到这种情况我一般先去 grep 谁多拿了引用。5. 脏超级块和落盘一致性一次 sync 的完整旅程5.1 “超级块脏”到底脏在哪这是很多人最迷惑的一点。读了很多文档总觉得 superblock 应该有个“脏标志”定期回写磁盘。但现代内核里VFS 超级块本身已经不太强调“单块超级块脏”的概念早期内核里的s_dirt字段已经被删除。现在真正管落盘的是 writeback 子系统和具体文件系统自己的事务机制。所以我的理解是VFS 超级块更多是一个“调度入口”具体哪些元数据要落盘、以什么顺序落盘都由文件系统自己把握。ext4 有日志jbd2XFS 有日志btrfs 有自己的事务它们并不会因为 VFS 层说“同步一下”就把磁盘超级块整个重写一遍。大多数情况下磁盘超级块只在挂载、卸载、或关键特性变更时才更新其余时间保持只读。这解释了为什么你会看到类似EXT4-fs: superblock last mount time is in the future这样的告警——它读到了磁盘超级块上的字段发现时间戳不合理。磁盘超级块确实存在但它不是每次文件操作都在变的“热数据”。5.2 sync 系统调用触发路径看sync(2)的完整旅程最能理解 superblock 在一致性中扮演的角色。简化后的内核路径大致是ksys_sync() - iterate_supers(sync_inodes_one_sb, NULL) // 第一遍回写所有 inode - iterate_supers(sync_fs_one_sb, NULL) // 第二遍回写文件系统私有状态 - sync_filesystem(sb) - writeback_inodes_sb(sb, ...) - if (sb-s_op-sync_fs) sb-s_op-sync_fs(sb, 0) - if (sb-s_bdev) blkdev_issue_flush(sb-s_bdev)注意这里有两遍遍历第一遍先回写 inode 数据第二遍才调用文件系统的s_sync_fs。为什么要两遍因为文件系统私有的元数据比如块位图、inode 位图可能依赖第一遍刚回写的 inode 状态必须先确保 inode 落盘再更新文件级元数据。另外blkdev_issue_flush很重要它向块设备下发 flush 命令确保先前的数据已经被物理设备真正写入非易失介质而不仅仅是到达设备缓存。很多人跑完sync后立刻拔电还是丢数据就是因为依赖了设备缓存没有下刷。5.3 实测观察用 ftrace 看 sync 过程我在测试环境里验证过完整的 sync 流程。步骤很简单mkdir -p /mnt/test mount -t ext4 /dev/loop0 /mnt/test # 打开 writeback 相关 tracepoint echo 0 /sys/kernel/debug/tracing/tracing_on echo 1 /sys/kernel/debug/tracing/events/writeback/writeback_start/enable echo 1 /sys/kernel/debug/tracing/events/writeback/writeback_sb_inodes/enable # 制造脏数据 dd if/dev/zero of/mnt/test/foo bs1M count64 convfsync echo 0 /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace | grep /mnt/test | head我看到的 trace 输出大致是writeback_start: bdiloop0:0 sb_dev0:0 inodes... writeback_sb_inodes: sb_dev0:0 ino...注意sb_dev字段是一个(dev_t)的组合值不是我们平时看到的8:1这种成熟格式初看时容易懵。从这里能直观看到一次fsync调用最终会汇聚到超级块的回写逻辑再由超级块决定回写哪些 inode。这个实验也适合做教学改挂载参数、调整 dirty 比例、模拟断电前只执行sync不执行设备 flush观察数据丢失概率。当然实验要在测试环境做别拿生产设备折腾。5.4 崩溃一致性视角为什么可以不完全依赖超级块脏回写现代文件系统大量使用日志或写时复制CoW机制目的就是不让磁盘超级块成为单点瓶颈。ext4 在格式化时会写入多个超级块副本主超级块损坏时可以利用备份恢复XFS 也有主超级块和备用超级块的冗余设计。这些设计意味着即使断电发生在超级块更新过程中文件系统重启时也能通过备份、日志重放或校验和检测把文件系统恢复到一致状态。这也是为什么“VFS 超级块不需要每次变动都写回磁盘”在工程上完全可行。理解到这一层再去看sync的路径就不会觉得“它怎么只调了几个函数就完事”因为真正的一致性保障是文件系统和块设备层共同承担的。6. 排查问题时的三板斧观察状态、复现崩溃、查找泄漏6.1 用户态能看到的“影子”用户态看不到 superblock 的完整结构但能看到它的若干投影。/proc/mounts、/proc/self/mountinfo里能看到当前挂载类型和选项df -T会调用statfs实际上触发s_op-s_statfsmount -o remount,ro会改变内存超级块的状态标志这就是为什么 remount 后/proc/mounts里多了ro。我遇到的一个实用技巧是通过/proc/self/mountinfo的 mount ID 可以反查一个挂载点对应的超级块编号关系内核提供/proc/self/mountstats之类的信息虽然它不直接显示 sb 地址但配合 ftrace 能定位到具体实例。如果你有 root 权限crash工具在读内核内存时可以super_blocks链表遍历所有实例再打印s_id、s_fs_info、s_count、s_active等字段这是排查泄漏的利器。6.2 内核态观察工具箱首选 ftrace 的几个 tracepoint。在挂载和卸载文件系统时events/fs下有关联的跟踪点可以这样观察echo 1 /sys/kernel/debug/tracing/events/fs/sb_alloc/enable echo 1 /sys/kernel/debug/tracing/events/fs/sb_free/enable mount -t ext4 /dev/loop0 /mnt/test umount /mnt/test cat /sys/kernel/debug/tracing/trace如果内核编译了CONFIG_TRACEPOINTS这一套就能用。配合trace-cmd record -e fs:* -e writeback:*可以更舒服地记录全过程。另一条路是用 bpftrace 挂在alloc_super、kill_super这类函数上。比如bpftrace -e kprobe:alloc_super { printf(alloc sb: %p\n, arg0); }注意这要求内核带调试符号不同内核版本字段布局可能不同打印字段前先pahole看一眼再写。6.3 一个延迟任务导致超级块释放后崩溃的复盘最后分享一个我在内部测试里遇到的典型案例文件系统用一个虚拟代号“某网络文件系统”代替。现象是卸载挂载点后过了几十秒系统日志里出现一次内核崩溃栈回溯落在某网络文件系统_writeback_worker这个函数里指向一个非法地址。从栈看worker 在访问sb-s_fs_info里的一个链表但那个地址已经被释放。排查过程分三步。第一步我确认umount时kill_super确实执行了并且没有报错说明s_op-put_super正常释放了私有数据第二步我通过 ftrace 确认sb_free发生的时间点第三步我在s_op-put_super和 worker 函数入口加上自定义打印发现 worker 在sb_free之后仍然被调度起来。根因很清晰文件系统内部在挂载时启动了一个延迟回写任务但put_super里只做了flush_work没有处理delayed_work任务在 countdown 结束后又跑了一遍。修复也很简单在put_super和kill_sb里显式调用cancel_delayed_work_sync并且在 worker 入口检查 superblock 状态标志如果已进入销毁流程就直接返回。这个案例提醒我一件事凡是文件系统内部起了独立线程或延迟任务必须把生命周期终点严格对齐到 superblock 释放之前。这个坑和具体文件系统无关任何基于 superblock 的模块都可能碰到。我在实际排查中还有一个小习惯遇到 superblock 相关问题先不看代码先在 ftrace 里把sb_alloc、sb_free、writeback事件抓一遍把时间线画出来。大多数问题在这个阶段就能看出端倪。读完整个机制之后你会发现superblock 并没有那么神秘它就是一个把所有文件系统对象统一挂起来的“总目录”。顺着生命周期、回调和状态转换三条主线走一遍比单纯背字段有效得多。希望这篇笔记能帮你少走一点弯路。
阅读完成 · 觉得有帮助?