ARTICLE DETAIL

资讯详情

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

文件系统静态结构:ext4、inode、超级块与磁盘布局

文件系统静态结构:ext4、inode、超级块与磁盘布局 1. 从这道课堂练习说起静态结构到底在讲什么提到文件系统很多人的第一反应是能存文件的地方这个理解不算错但太浅了。如果把文件系统比作一栋楼我们平时用的ls、cp、open看到的都是住户进进出出的动态场景而静态结构问的是另一件事这栋楼在图纸上是怎么画的承重墙在哪水电井在哪每个房间的门牌号怎么编。这道课堂练习之所以把它单独拎出来就是因为后面所有的读写性能、崩溃恢复、空间碎片问题根子都埋在这张图纸里。换句更直白的话静态结构指的是文件系统在被创建之后落在存储介质上的持久化布局——包括哪些区域、各自多大、彼此什么关系、元信息放在哪、怎么寻址。它不随进程的读写而随意变形只会在分配和释放时按既定规则改动。理解了它你再看df、dumpe2fs、debugfs这些工具的输出就不会只看见一堆数字而是能对应到盘上的真实位置。提示静态结构是设计期的产物动态行为是运行期的表现。面试或考试里问文件系统由哪些部分组成问的基本都是静态结构。这篇文章适合三类人正在做操作系统或存储课程实验的同学、刚接触 Linux 运维想搞明白df -h背后原理的工程师、以及准备面试存储方向岗位的开发者。我会用 ext4 作为主线穿插 VFS、btrfs、根文件系统挂载、sync落盘这些高频关键词既讲清楚图纸怎么画也告诉你怎么亲手把它看出来。2. 把一张盘切开核心组成速览与设计取舍2.1 动态行为与静态布局的分界线一个初学者最容易混淆的问题是我在文件里写 1KB 数据算是改了静态结构吗答案是——看情况。写数据本身是往数据块里填内容属于动态 I/O但如果这是文件第一次分配数据块那么 inode 里的块指针、块位图里的标记、甚至超级块里的空闲块计数都会变这些就是静态结构的改动。区分的方法很简单问自己一个问题这个信息在断电重启后还需要存在吗需要它就是静态结构不需要它就是内存里的运行时状态。比如 dentry 缓存目录项加速查找这是动态的而磁盘上的目录项表是静态的。再比如 VFS 层的struct file是每个进程打开文件时临时创建的进程退出就销毁属于动态struct inode的内存副本虽然也是临时的但它是磁盘 inode 的缓存代理背后有静态结构兜底。这个分界线非常重要因为它决定了崩溃一致性的设计目标文件系统必须保证静态结构要么完整更新要么完全不更新否则一次断电就会让你的盘变成一锅粥。后面讲日志和sync时我会回到这一点。2.2 一块盘被切成几块ext4 的六大件以最经典的 ext4 为例一个格式化好的分区大致会被切成下面几块。注意这里的块block是文件系统的分配单位默认 4KB跟硬件的扇区通常 512B 或 4KB不是一回事。区域典型位置作用是否可被用户直接访问引导块偏移 0占 1024 字节历史遗留给引导代码留位否超级块偏移 1024占 1024 字节全局元信息块大小、inode 总数等通过工具读取块组描述符表紧随超级块描述每个块组的位图、inode 表位置通过工具读取块位图每个块组一份标记哪些数据块已用否inode 位图每个块组一份标记哪些 inode 已用否inode 表每个块组一份存放 inode 结构体数组通过 debugfs 读取数据块块组剩余部分存放文件和目录的实际内容是超级块为什么放在 1024 偏移而不是 0因为最早的 x86 引导约定把第一个 1024 字节留给引导扇区文件系统格式设计时尊重了这个惯例一直沿用到现在。这个细节看起来无聊但你在用dd做备份时必须知道直接从分区头部拷 512 字节是拿不到超级块的要skip1或者直接从 1024 偏移读。块组的意义在于局部性。如果整块 1TB 的盘只有一份位图和 inode 表那么每次分配 inode 都要跨越半个磁盘去读位图寻道时间会爆炸。ext4 的做法是把盘切成若干块组每个块组自带位图和 inode 表尽量把同一目录的文件和它的 inode 放在同一个块组里。这就是为什么你ls -i看到同一目录下文件的 inode 号往往是连号的——不是巧合是块组分配的功劳。2.3 关键参数的来龙去脉格式化时那几个参数不是随便定的背后都有关联。块大小默认 4KB因为这是页大小的常见值能减少页缓存和磁盘块之间的映射开销同时 4KB 块下单个文件的理论最大尺寸和元数据开销比较平衡。inode 数量是另一个容易踩坑的点。mkfs.ext4默认按每 16KB 一个 inode来估算一块 100GB 的盘大约给 650 万个 inode。如果你的业务是存海量小文件比如邮件队列、缓存切片这个数字很快就见底表现出来就是df -h显示还有空间但新建文件报No space left on device。这不是磁盘满了是 inode 满了。注意inode 数量在格式化时就固定了事后只能用tune2fs扩容较新版本支持缩容基本无解。所以做小文件业务之前务必预估 inode 需求。至于块组的最大容量可以简单算一下块位图本身占一个块4KB 块大小下它有 32768 位每位对应一个数据块所以一个块组最多管 32768 × 4KB 128MB 数据。这是默认上限ext4 引入的meta_bg特性允许块组描述符分散存放从而支持更大的分区。3. 超级块与块组磁盘布局的第一层骨架3.1 超级块里到底存了什么超级块是文件系统的身份证大小固定 1024 字节但里面的字段密度极高。用dumpe2fs -h /dev/sda1能看到完整清单我挑几个实际工作中最常打交道的字段说说它们为什么重要。字段含义实际用途s_inodes_countinode 总数判断小文件容量上限s_blocks_count_lo/hi数据块总数配合块大小算总容量s_free_blocks_count空闲块数df的数据来源之一s_log_block_size块大小指数实际块大小 1024 该值s_magic魔数ext 系列为 0xEF53文件系统识别s_state卷状态clean/errors判断是否需要 fscks_mnt_count挂载次数超过s_max_mnt_count会触发自检s_feature_*特性标志位决定内核能否挂载块大小的换算值得单独提一句s_log_block_size存的是指数实际值等于1024 n。所以当 n0 时块大小是 1024 字节n2 时是 4096 字节。为什么这么设计因为早期块大小只有 1KB/2KB/4KB 几种可能用指数存省空间现在虽然块大小选择更多了这个编码方式还是留着。s_state字段是排查问题的好帮手。如果显示not clean说明文件系统被标记为脏可能是上次非正常关机导致的。现代发行版大多开启了日志重启后内核会回放日志自动恢复一般不需要手工 fsck但如果日志回放失败你就会在启动时看到那个熟悉的UNEXPECTED INCONSISTENCY提示。3.2 备份超级块容灾设计的老智慧ext4 不会只存一份超级块。它在 0 号块组的起始处放主超级块然后在一系列特定的块组里放备份典型位置是 1、3、5、7、9、25、27、49、81……这些数字是 3、5、7 的幂次具体能通过dumpe2fs输出里的 Backup superblock at 看到。为什么要备份因为超级块一旦损坏整个文件系统就失忆了——不知道块大小、不知道 inode 表在哪等于地图没了。有了备份你可以用fsck -b 32768 /dev/sda1指定备份超级块来重建。这是救盘的最后手段之一。我手上有个真实案例某台测试机被误操作写了盘头mount报 Bad magic number in super-block。当时用dumpe2fs也读不出信息最后是找到备份超级块位置用fsck带-b参数重建才救回来数据基本无损。所以第一条经验就是动盘之前先记录下备份超级块的位置dumpe2fs /dev/sdX | grep -i backup一条命令的事关键时刻能省几小时。提示备份超级块不会随主超级块实时同步它只在fsck或特定时机更新所以它反映的是较旧的状态用于恢复时才有效。3.3 块组描述符表每个块组的档案超级块告诉你整体有多少资源块组描述符表告诉你资源分布在哪。每个块组对应一个 64 字节的描述符开启64bit特性后是 64 字节否则 32 字节记录该块组的块位图位置、inode 位图位置、inode 表起始块、空闲块数、空闲 inode 数、已用目录数等。这里有个设计细节值得玩味描述符里存了已用目录数。为什么单独统计目录因为 ext4 在分配新目录时会优先选择目录数少的块组目的是让目录结构在磁盘上尽量均匀分布避免所有目录挤在一个块组里导致元数据热点。这是一个典型的用一点额外元数据换取分配均衡的设计。块组描述符表本身也需要备份。默认情况下它跟着备份超级块一起存在那些 3、5、7 幂次的块组里如果开启了meta_bg特性则会分散在多个块组中每块组描述符单独备份更适合超大分区。4. inode 与目录项文件名如何变成数据块4.1 inode 固定大小的取舍inode 是文件系统的灵魂。每个文件包括目录、符号链接、设备文件都有一个 inode里面记录文件类型和权限、所有者 UID/GID、大小、三个时间戳atime/ctime/mtime、链接计数、数据块指针数组以及一些扩展属性。ext4 的 inode 默认大小是 256 字节老 ext3 是 128 字节。为什么固定因为 inode 表是个数组只有固定大小才能用inode 号 × 大小 起始偏移的方式 O(1) 定位某个 inode。如果变长就必须顺序扫描性能会崩溃。代价是空间浪费和扩展性受限。一个只有几字节的小文件也要占满一个 inode反过来inode 里能放的数据块指针数量有限大文件就不得不借助多级间接寻址。经典的三级间接块寻址是这样算的以 4KB 块、4 字节指针为例12 个直接指针12 × 4KB 48KB一级间接1024 个指针 × 4KB 4MB二级间接1024 × 1024 × 4KB 4GB三级间接1024³ × 4KB 4TB加起来最大文件约 4TB 出头。这个数字在 90 年代是天文数字但到了今天显然不够看而且访问文件尾部要多次读盘。所以 ext4 引入了extent 树不再记录单个块而是记录从逻辑块 X 开始的连续 N 个块映射到物理块 Y用一棵 B 树组织。一个 extent 就能表示成百上千个连续块元数据开销大幅下降也顺带解决了碎片问题。提示判断一个文件用的是旧式块映射还是 extent可以看debugfs -R stat inode号 /dev/sdX输出里是否出现 EXTENTS: 标记。4.2 目录也是文件这点常被忽略目录在磁盘上也是一种文件也有 inode也有数据块只不过它的数据块里存的是目录项数组每条记录形如inode 号 记录长度 名字长度 文件类型 文件名。早期的目录就是一张线性表删文件时把记录长度合并给前一项这叫墓碑复用所以删文件不会立刻缩小目录大小新文件可以复用被删除项留下的空洞。当目录里文件很多时线性扫描越来越慢于是 ext4 引入了dir_index特性把目录项组织成一颗哈希树HTree按文件名哈希查找即使目录里有几十万个文件查找也能保持近似对数复杂度。实测一下这个差异很直观在一台机器上分别建两个目录一个开启 dir_index一个用tune2fs -O ^dir_index关掉各放 20 万个空文件然后time ls -f | wc -l你会发现数量级不同的差距。这里就牵扯到一个实战经验大目录是运维的隐形炸弹。像/var/spool/postfix这种目录如果不做哈希队列清理脚本会越跑越慢。现在主流发行版默认开 dir_index但如果你接手的是老系统务必确认一下。4.3 硬链接、软链接在结构上的差别理解了目录项硬链接就很好解释了硬链接本质是给同一个 inode 再加一条目录项记录两条记录指向同一个 inode 号inode 的链接计数加一。删除其中一个只是删掉一条目录项并把计数减一只有计数归零、且没有进程打开时inode 和数据块才真正释放。这解释了几个常见迷惑现象硬链接不能跨文件系统因为它依赖 inode 号的唯一性不同文件系统的 inode 号会冲突。目录默认不能建硬链接因为会形成环让fsck和目录树遍历陷入死循环早期的.和..是例外它们用特殊处理。删除一个仍被进程打开的文件df看空间没减少——因为 inode 还在被引用直到进程关闭 fd 才释放。软链接则简单得多它就是一个独立的 inode内容就是目标路径字符串访问时由内核解析替换。所以软链接可以跨文件系统、可以指向不存在的路径悬空链接删掉它也不影响目标。排查删了文件空间不释放的问题标准动作是lsof L1或者遍历/proc/*/fd找那些 deleted 状态的 fd。这个技巧我在生产环境用过不止一次尤其是日志文件被日志切割工具重命名后老进程还持有旧 fd 的场景。5. VFS 与根文件系统把多种实现统一起来5.1 四个核心对象撑起抽象层Linux 支持 ext4、xfs、btrfs、f2fs、ntfs 等一堆文件系统应用层却只用open/read/write就够了这中间的功臣是VFS虚拟文件系统。VFS 用四个核心对象做抽象对象代表生命周期是否持久化super_block一个已挂载的文件系统实例挂载到卸载对应磁盘超级块inode一个文件/目录的元数据被引用期间缓存对应磁盘 inodedentry路径中的一个分量缓存可回收对应目录项file一个打开的文件句柄进程 open 到 close无这四个对象构成了 VFS 的骨架。要支持一个新文件系统本质上就是实现一套操作函数集super_operations、inode_operations、dentry_operations、file_operations让 VFS 在需要时回调。理解这层抽象有个实用价值你能解释为什么stat和lstat结果不同。stat会跟随符号链接一路解析到底层的 inodelstat只返回链接本身那个 inode 的信息。也可以用readlink单独读取链接内容。这些差异全部源于 VFS 对软链接的特殊处理。5.2 根文件系统是怎么挂上的开机时那个/从哪来这是一条容易被忽略但很有意思的链路。内核启动后先挂一个临时的initramfs内存文件系统里面带着必要的驱动模块和挂载工具然后 udev 枚举设备找到真正的根分区把它挂到某个临时目录再通过switch_root或pivot_root切换过去卸载临时的 initramfs最终/才是你熟悉的那个根。/etc/fstab里那些条目就是告诉系统哪个设备挂到哪个目录、用什么选项。看一眼典型的配置# 设备 挂载点 类型 选项 备份 自检 UUIDxxxx / ext4 defaults,noatime 0 1 UUIDyyyy /boot ext4 defaults 0 2 UUIDzzzz /data xfs defaults,noatime 0 2有两个细节新手常踩坑。第一fstab里用 UUID 而不是/dev/sda1因为设备名在插拔硬盘后可能变化UUID 稳定。第二最后一列的 0 表示不检查非 0 表示开机时做自检数字越小优先级越高。根分区通常是 1。如果你把数据盘也设成 1每次重启都要等它fsck扫描硬盘大的话会等很久用户体验极差。注意修改/etc/fstab后不要直接重启验证先用mount -a测试。参数写错会导致系统进不了图形界面甚至单用户模式这是我见过最多的自伤操作。5.3 btrfs 的静态结构完全是另一个思路前面讲的都是 ext4 这种位图 固定 inode 表的传统布局。btrfs走的是完全不同的路整个文件系统由多棵写时复制COW的 B 树构成包括根树、chunk 树、设备树、extent 树、文件系统树、校验树等。没有固定位置的 inode 表也没有块位图所有元数据都是树节点。它有几个结构性特点值得记住。超级块在 64KB、64MB、256GB 等固定偏移处放多份副本容忍整块损坏。逻辑地址到物理地址的映射由 chunk 树维护所以 btrfs 可以很方便地跨多块盘做扩展这也是头歌分布式文件系统这类实验里经常拿它做对比的原因。数据块带 checksum读出来能自动校验发现静默损坏。代价是结构更复杂写放大更明显而且在 RAID5/6 场景下有众所周知的坑。选型时我的经验是单盘或简单镜像、需要快照和子卷的场景btrfs 很舒服高并发随机写、追求可预期延迟的场景还是 xfs 更稳。有个很实用的对比命令看 ext4 用dumpe2fs -h看 btrfs 用btrfs filesystem usage /data和btrfs filesystem df /data你会发现后者的输出里全是 Data、Metadata、System 这些逻辑块组而不是设备偏移正好体现了 chunk 映射这层抽象。6. 实操演练把静态结构真正看出来6.1 从 df 到 debugfs 的完整观察链理论讲完动手才踏实。下面这套命令建议在一个虚拟机或实验盘上跑别在生产机上乱来。第一步看整体布局sudo tune2fs -l /dev/vdb1 sudo dumpe2fs -h /dev/vdb1 | head -40tune2fs -l和dumpe2fs -h输出的是超级块信息重点关注 Block count、Free blocks、Inode count、Block size、Filesystem features 五行。用 Block count × Block size 算出来的就是分区总容量和df -h对不上是正常的——因为还有约 5% 的空间预留给 root 使用目的是防止普通用户把盘写满导致系统无法正常运行。第二步看块组分布sudo dumpe2fs /dev/vdb1 | grep -E Group|Backup superblock | head -30你会看到形如Group 0: block bitmap at 1025, inode bitmap at 1026, inode table at ...的行。这就是块组描述符的直观呈现块位图、inode 位图、inode 表的物理位置一目了然。第三步看具体 inodesudo debugfs -R stat 2 /dev/vdb1inode 2 在 ext 系列里固定是根目录。输出里你会看到Links: n子目录数加自身、Blockcount、以及EXTENTS:段这就是 extent 树的实际形态。想验证目录项结构可以用sudo debugfs -R ls -l /home /dev/vdb1 sudo debugfs -R htree /home /dev/vdb1htree命令能看到目录哈希树的深度和节点数如果输出显示 not a htree directory说明这个目录还停留在线性模式文件一多就慢。6.2 sync 与落盘静态结构什么时候真正改变很多人以为write()返回就代表数据落到盘上了这是个危险的误解。实际的链路是write()把数据写进页缓存内存然后返回内核后台线程在较新内核里是 per-bdi 的 writeback 线程定期把脏页刷到磁盘。如果你希望立刻持久化得调用fsync()或fdatasync()前者连元数据一起刷后者只刷数据不刷不影响读取的元数据比如 mtime所以更快。sync命令则是一次性把所有文件系统的脏页刷回。它的用途是确保拔盘或关机前数据完整。注意sync是全局的在写压力大的服务器上执行sync可能触发长时间的 I/O 抖动所以生产环境的脚本里我更倾向用针对性的fsync而不是无脑sync。从静态结构的角度看一次数据写入可能引发这些持久化改动数据块被写入新分配的话块位图相应位置 1inode 的i_size、i_mtime、extent 树更新目录项可能新增新建文件时超级块的s_free_blocks_count、s_wtime更新这四步如果中途断电就会造成结构不一致——比如位图标记了块已用但 inode 里没记录这些块就变成泄漏的孤儿块。ext4 用JBD2 日志解决这个问题先把元数据变更写进日志区提交后再写回原位。崩溃后回放日志要么全做要么全不做。这就是为什么日志必须和数据分开看待——它保护的是静态结构的一致性不是你的文件内容。想同时保证内容也完整就要靠fsync加日志的dataordered模式配合。6.3 一个动手小实验观察 inode 耗尽光看不过瘾做个小实验加深印象。准备一块小盘比如 200MB 的 loop 设备故意把 inode 数量设得很小dd if/dev/zero of/tmp/fs.img bs1M count200 mkfs.ext4 -N 1000 /tmp/fs.img # 只给 1000 个 inode mkdir -p /tmp/mnt sudo mount -o loop /tmp/fs.img /tmp/mnt cd /tmp/mnt for i in $(seq 1 1200); do touch file_$i 2/dev/null; done df -i /tmp/mnt df -h /tmp/mnt你会看到df -h显示还有几十 MB 空闲但df -i显示 IUse% 已经 100%第 1001 个文件开始创建失败。这个小实验一次就能把inode 是稀缺资源刻进脑子里比看十遍文档管用。7. 常见问题与排查技巧实录7.1 结构相关问题速查表下面这张表是我这些年处理盘问题时最常翻的按现象分类整理你可以直接收藏。现象大概率原因排查命令处理思路有空间但写不进去inode 耗尽df -i清理小文件或重做文件系统删文件后空间不释放进程持有已删文件 fdlsof L1重启或 kill 相关进程挂载报 bad magic超级块损坏dumpe2fs报错fsck -b用备份超级块开机卡在 fsck分区大且自检开启查看/etc/fstab最后一列数据盘设为 0I/O 持续高但无进程后台 writeback 刷脏页cat /proc/meminfo | grep Dirty调整 dirty_ratio 或错峰目录 ls 极慢未启用 dir_indexdebugfs -R htree /pathtune2fs -O dir_index后重建目录时间戳异常乱跳atime 频繁更新mount | grep atime挂载加noatime或relatime容量突然变小保留块被占用dumpe2fs -h看 Reservedtune2fs -m调整保留比例7.2 三条血泪换来的经验第一条永远先备份超级块位置。前面提过接到新盘或新虚拟机第一件事就是dumpe2fs /dev/sdX | grep -i backup superblock把结果记下来。我见过太多人盘一坏才开始找备份位置结果因为主超级块读不出来工具连备份位置都报不出来只能靠经验值猜 32768。第二条别在挂载状态下对文件系统结构动手。tune2fs有些参数可以热改比如-m、-L但改块大小、改特性集这类操作必须在卸载状态下进行。在线改特性集是 fsck 报错的头号来源特别是一些老教程里教的tune2fs -O has_journal在已挂载的盘上执行可能直接让文件系统进入不可恢复状态。第三条预估容量要同时算空间和 inode。我做容量规划时的习惯是先用du统计现有数据估算未来增长然后按平均文件大小 × 文件数反推需要的 inode 总量再留 30% 余量。一个 1TB 的盘如果规划成只放 50KB 以上的文件默认 inode 数完全够用但如果要放 5KB 的日志切片就得手动调mkfs.ext4 -i 4096把每 4KB 分配一个 inode。7.3 应用层怎么感知到静态结构回到热搜词里出现的那个场景移动端从用户文件系统取图片。Android 应用里常见的做法是通过BitmapFactory.decodeFile()或者ContentResolver读取路径底层走的仍然是 VFS 那条链路Java 层FileInputStream到 native 层open/read再到 VFS 的 dentry/inode 缓存最后落到具体文件系统的块地址解析。静态结构在这里的影响体现在两个地方。一是小文件多的目录如果应用图片缓存目录里堆了几万个缩略图开启 dir_index 的 ext4 查找依然很快没开启就会明显卡顿。二是随机读性能图片文件的块在盘上是否连续直接决定一次解码要触发多少次寻道这在机械盘时代是致命的在闪存上虽然没那么严重但也会影响吞吐。所以即使你做的是应用层开发理解静态结构也不是浪费时间。你至少应该知道为什么图片要按日期分目录存放避免单目录过大、为什么缓存清理要做容量和数量双限制防止 inode 耗尽、为什么大文件写完后调用flush()有意义触发 fsync 语义保证断电不丢。8. 我个人在做这类实验时的体会这门课的核心其实不是让你背下超级块有哪些字段而是建立一种从盘往上想的思维方式。我刚开始学的时候也觉得dumpe2fs的输出又长又枯燥直到有一次在实验室里把一块盘的超级块用dd覆盖掉然后花了一个下午用备份超级块把它救回来那之后所有字段在我眼里都变成了有用的地图坐标。如果你要动手复现这篇文章里的实验我的建议是准备一块独立的 loop 镜像或虚拟机数据盘别拿系统盘练手。命令都可以先在mkfs出来的空盘上跑通再去看真实分区上的输出对比起来体会更深。另外一个小技巧debugfs支持交互模式直接sudo debugfs /dev/vdb1进去用stat、ls、dump、ncheck一条条敲比每次带-R参数舒服得多也更容易发现 inode 号和目录层级之间的对应关系。跑到你随手就能说出根目录是 2、lostfound 是 11的时候这个练习的目标就基本达成了。
返回列表