尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

文件系统静态结构详解:ext4磁盘布局、inode与VFS内核对象全解析

文件系统静态结构详解:ext4磁盘布局、inode与VFS内核对象全解析 文件系统的静态结构这个说法第一次听会以为是教科书目录里的一行字真到现场排查问题的时候它反而是最先要拿起来的东西。磁盘上到底写了什么、内核里挂了哪些对象、一个路径在还没被打开之前长什么样这些都属于静态结构要回答的问题。这份整理来自我做课堂练习 7.1 的完整过程先把概念层面的三层视图理清再拆 ext4 在盘上的物理布局接着把 VFS 的内存对象摆出来最后用一块镜像盘把所有命令跑一遍。它适合正在上操作系统课、准备存储方向面试、或者需要在嵌入式设备与 Android 端定位文件明明在却读不到这类问题的同学。不要求你会写内核代码只要能在终端里敲命令、愿意动手做实验就能完整复现。1. 静态结构到底指什么先把照片和录像分开1.1 从课堂练习的题目要求反推考点课堂练习这种题型题干通常很短比如分析文件系统的静态结构说明其组成与层次关系。短题干最容易让人慌因为你不知道老师想要哪一层的答案。我的经验是这类题几乎一定在考三件事一是元数据与数据的分工二是层次调用的关系三是从一个路径出发怎么定位到具体数据块。换句话说它要的不是你背出 ext4 的十八个字段而是你能不能画出一张谁指向谁的图。先把最核心的一组概念钉死元数据是描述文件的数据数据块是文件内容本身。静态结构关心的就是元数据怎么组织。超级块记录整个文件系统的规模与状态inode 记录单个文件的属性与数据位置目录项把人类可读的名字映射成 inode 号位图记录哪些块和哪些 inode 已被占用。这四样东西在磁盘上的固定位置与相互引用关系就是静态结构的主体。为什么强调静态因为它描述的是一种不随时间流动而改变组织方式的骨架。你写入一个文件骨架不会重排只是某些位置的值被改了一个 inode 从空闲变占用一个位图位从 0 变 1目录文件末尾多出几十字节的目录项。理解了骨架你看到的就不再是黑盒。1.2 静态结构的三层视图同一个文件系统从三个角度看会得到三份不同的结构图很多人学混就是因为把三层揉在一起了。下面这张表是我复习时用的对照表建议也照着画一份。视图层次主要组成观察手段典型问题磁盘视图引导块、超级块、组描述符、块位图、inode 位图、inode 表、数据区dumpe2fs、debugfs、hexdump我的 inode 号为什么这么大内核视图super_block、inode、dentry、file、mount/proc/mounts、/proc/filesystems、内核源码为什么 ls 很快而 stat 慢用户视图挂载点、路径、目录树、符号链接ls、find、findmnt、stat同一个文件为什么有两个路径三层之间是映射关系不是三套独立系统。磁盘上的 inode 表被内核读进来后变成内存里的 inode 对象内存里的 dentry 缓存是把磁盘上的目录项在内存中重建出来的加速结构用户看到的目录树是内核把多棵挂载树拼起来之后的呈现。搞清这个映射你就能解释很多玄学现象比如为什么删除一个正在被进程打开的文件磁盘空间不会立刻释放。注意三层视图里最容易出错的是用户视图。用户看到的路径和磁盘上的组织没有一一对应关系一个挂载点可以把任意一棵子树接到任意位置。做练习画图时务必把挂载这一层单独标出来。1.3 为什么必须先看静态结构三个真实的翻车场景第一个场景是磁盘还有空间却写不进去。我用一个 16GiB 的分区跑日志采集单条日志只有几十字节跑了两周后写入开始报 No space left on device但df -h显示只用了四成。原因就是 inode 被耗尽了df -i一敲就露馅。这件事的根源完全在静态结构inode 总数是在格式化那一刻定死的之后基本不能在线扩容。第二个场景是文件消失。有同事把数据写到了/data目录下运维为了加一块盘直接把新分区挂到了/data原来的文件全被盖住了。文件其实还在原分区的目录里躺着只是挂载点被覆盖后用户视图里的/data已经指向另一棵子树。这就是不理解挂载树导致的事故跟静态结构直接相关。第三个场景来自 Android。有同学在 ImageView 里写死了/sdcard/Download/pic.jpg去取图片在旧机型上跑得好好的升到新系统就读不到。原因是分区布局和访问规则都变了路径不再是稳定的契约应该走媒体库或内容 URI。表面上是 API 兼容问题本质还是对文件系统的静态边界没有概念。这三个坑我都踩过或者亲眼见过它们的共同点是出问题的部分都是不动的骨架而不是运行时的逻辑。2. 磁盘上的静态布局超级块、位图与 inode 表2.1 ext4 的分块组是怎么切出来的ext4 把整块设备切成若干个块组block group每个块组大小基本一致这样做的目的是让元数据和它的数据在物理上靠近减少磁头或闪存页的跨区跳转。一个典型块组内部的排布顺序是固定的以 4KiB 块、每个块组 32768 块为例顺序区域占用作用1超级块副本部分组才有1 块块组 0、1、3、5、7 等稀疏分布2组描述符表及其备份若干块记录每个块组的空闲块数、空闲 inode 数等3保留 GDT 块若干块为将来扩容组描述符预留4块位图1 块32768 位正好描述 32768 个块5inode 位图1 块描述本组 inode 的占用情况6inode 表按 inode 数决定本组所有 inode 连续存放7数据块剩余文件内容注意第 4 行那个数量巧合块位图刚好一个块是因为 32768 位等于 4096 字节。这个设计不是巧合而是刻意为之让位图读写永远是单块操作。inode 位图同理如果每个组 8192 个 inode8192 位就是 1024 字节不足一个块剩下的空间就浪费掉了。块组 0 比较特殊。设备最开始有 1024 字节的引导区保留不参与文件系统寻址所以超级块固定在字节偏移 1024 处。如果块大小是 4KiB超级块就落在块 0的后半段如果块大小是 1KiB它正好是块 1。这个偏移差异会直接影响你用 hexdump 定位超级块时该跳过多少字节。2.2 超级块里真正要看的字段超级块是文件系统的身份证字段很多但真正需要记住的就那么几个。我把它们分成三类规模类、位置类、特征类。字段含义为什么要看s_magic固定值 0xEF53判断是不是 ext 家族s_log_block_size块大小 1024 该值所有偏移计算的基础s_blocks_count总块数算容量、算组数s_inodes_count总 inode 数判断会不会 inode 耗尽s_blocks_per_group每组块数算块组数量s_inodes_per_group每组 inode 数定位某个 inode 属于哪一组s_first_data_block第一个数据块号0 或 1取决于块大小s_free_blocks_count / s_free_inodes_count空闲计数与 df 对照验证s_mnt_count / s_max_mnt_count已挂载次数与上限解释为什么开机偶尔强制自检s_state干净或脏判断上次是否正常卸载s_feature_incompat不兼容特性位决定老内核能不能挂载s_wtime / s_mtime最后写/挂载时间追踪异常时间点这些字段你不用背理解用途就够了剩下的交给 dumpe2fs 打印。我真正想强调的是s_feature_incompat它是版本兼容性的硬门槛。比如 metadata_csum、64bit、extent、flex_bg 这些特性一旦开启老内核根本挂不上报错信息还相当含糊。跨设备搬盘的时候先对比这个字段比看出错日志快得多。2.3 inode、目录项与数据块一条路径的静态落点假设要访问/home/lab/note.txt静态结构上发生了什么这个过程值得完整走一遍因为它是路径解析path resolution的核心也是这门课最容易考的地方。内核从根目录的 inode 开始ext4 里根目录固定是inode 号 21 号保留给坏块链表。根目录本身也是一个文件它的数据块里存着一串目录项。每个目录项的结构大致是4 字节 inode 号、2 字节记录长度、1 字节名字长度、1 字节文件类型、然后是变长名字。内核拿 home 这个字符串去根目录的数据块里逐个比对找到匹配项后取出 inode 号再去 inode 表里定位这个 inode。定位 inode 的位置有个计算公式练习里经常让人手推块组号 (inode 号 - 1) / s_inodes_per_group 组内偏移 (inode 号 - 1) % s_inodes_per_group inode 表中字节偏移 组内偏移 * inode 大小拿到 inode 后读它的 i_mode 判断是不是目录是就继续下一层直到 note.txt。最后读它的数据位置信息。ext4 默认用**区段extent**记录数据位置一个 extent 可以描述一段连续的物理块比传统的多级间接块高效得多。但你要知道传统方案长什么样因为练习和面试都爱问i_block 数组 15 项前 12 项直接指向数据块第 13 项是一级间接第 14 项二级间接第 15 项三级间接。4KiB 块下一个指针 4 字节一个块能装 1024 个指针所以单文件上限约为 12 块加 1024 块加 1024² 块加 1024³ 块总量在 4TiB 量级。目录项还有个细节容易被忽略删除文件时目录项并不真的被抹掉而是把 inode 号置零、把记录长度并入前一项。这就是为什么被删除文件的名字有时还能通过底层扫描找回来。理解这点你就明白删除在静态结构上到底改了什么。2.4 动手算一遍32GiB 盘的静态参数光看公式没感觉我拿一块常见的 32GB U 盘做一遍完整计算你跟着算一遍就通了。按十进制 32,000,000,000 字节、块大小 4096 字节、每块组 32768 块、每 16KiB 一个 inode 来算项目计算过程结果总块数32,000,000,000 / 40967,812,500块组数7,812,500 / 32768 向上取整239inode 总数32,000,000,000 / 16384约 1,953,125每组 inode 数1,953,125 / 239 向上取整到 8 的倍数8192实际 inode 总数239 × 81921,957,888inode 表占用1,957,888 × 256 字节约 478MiB元数据总开销占比位图 表 描述符/ 总量约 1.8%从这张表能看出两件事。第一inode 表本身要吃掉几百兆空间格式化的可用容量比标称值小一截这是正常的。第二if 你把 inode 密度调高比如-i 4096inode 总数会变成四倍inode 表膨胀到接近 2GiB小文件场景才划算。这就是格式化参数背后的权衡用空间换 inode 数量还是用 inode 数量换空间。再补一个直观例子。一个只有 100 字节的文件静态结构上占了一个 inode 加一个 4KiB 数据块stat看到的Blocks字段会是 8单位是 512 字节4096/5128。也就是说 100 字节的内容实际消耗了 4KiB 多空间放大率超过 40 倍。这就是为什么海量小文件场景要用专门的文件系统或者打包成大文件。3. 内存里的静态视图VFS 四大对象与挂载树3.1 super_block、inode、dentry、file 各管什么VFS 是内核给所有文件系统套的一层抽象。不管底层是 ext4、btrfs 还是 FAT上层看到的都是同一组对象。这四个对象的分工我建议用公司来类比内核对象类比生命周期关键内容super_block公司本身挂载到卸载文件系统类型、块大小、操作函数集inode员工档案有引用就活着权限、大小、时间戳、数据位置dentry工位门牌可回收的缓存名字到 inode 的映射、父子关系file一次业务往来打开到关闭当前读写偏移、打开模式这里最容易记错的是 inode 和 file 的区别。inode 描述文件本身file 描述某个进程对某个文件的一次打开操作。同一个文件被两个进程打开inode 只有一个file 对象有两个各自维护自己的读写偏移。这也是为什么两次open同一个文件后一个进程读到末尾另一个进程还能从头读。dentry 的地位最特殊。它纯粹是性能优化的产物磁盘上没有dentry 表这种东西。内核把最近用过的路径解析结果缓存在内存里ls一个热目录几乎不产生磁盘 IO就是因为 dentry 命中。dentry 可以被回收回收后再访问就重新走一遍磁盘解析。这解释了那个经典现象刚开机第一次 ls 大目录很慢第二次就快了。3.2 根文件系统与挂载树是怎么长出来的开机过程其实就是一步步把挂载树搭起来。内核先挂载一个临时的根通常是内存文件系统完成驱动加载和真正的根设备识别然后切换到真正的根文件系统。之后按配置把/proc、/sys、/dev、/run这些挂载上去最后才轮到用户自己定义的数据盘。以/etc/fstab为例它的六个字段是设备、挂载点、文件系统类型、挂载选项、dump 标志、fsck 顺序。第六个字段常被无视但它决定了开机自检的顺序根一般是 1其他本地盘是 2网络盘不填。填错会导致开机时多等很久甚至卡住。挂载选项里有几个跟静态结构直接相关。ro表示只读挂载这时内核在 VFS 层就拒绝写操作底层文件系统根本没机会执行。noatime表示不更新访问时间能显著减少元数据写入因为每次读文件都会改 inode 的 atime。dataordered、datawriteback、datajournal三种日志模式的区别在于数据块与元数据落盘的先后约束ordered 是默认值保证不会出现inode 指向没写下去的数据块这种不一致。只读根文件系统在嵌入式里很常见根用只读的压缩镜像需要写的目录用可写层叠加上去再挂几个 tmpfs 存放运行时数据。这样设备掉电也几乎不可能损坏根文件系统代价是固件升级要整块替换。这个思路和 Android 的分区设计是一脉相承的。3.3 读 /proc 就能看清的静态结构不想装任何工具光靠/proc就能把静态结构看得七七八八。我列几个最常用的/proc/filesystems当前内核注册了哪些文件系统类型行首带nodev的表示不需要块设备比如 tmpfs、proc、sysfs。嵌入式裁剪内核时这个文件是验证驱动有没有编进去的最快方式。/proc/mounts当前的挂载表是/proc/self/mounts的软链接。每行是设备、挂载点、类型、选项。/proc/self/mountinfo信息更全多了挂载 ID、父挂载 ID、根路径、可选字段。分析容器和命名空间时必须看这个因为/proc/mounts会漏掉一些细节。/proc/partitions内核识别到的块设备与分区表对照lsblk一起看能确认分区有没有被正确识别。/proc/self/mountstats每个挂载点的 IO 统计排查性能问题很有用。读 mountinfo 时有个坑字段之间用空格分隔但挂载选项和可选字段内部也可能出现空格需要用固定列位置去解析不能简单按空格切。我在写监控脚本时就在这里栽过选项里带空格的挂载点会导致整行错位。正确做法是按文档规定的列号取或者用findmnt的 JSON 输出。4. 不碰真盘也能做完镜像实验全流程4.1 工具清单与最小环境这个练习完全不需要额外硬件一块镜像文件就够。我用的工具都是系统自带的# Debian/Ubuntu 系 sudo apt install e2fsprogs util-linux coreutils # 验证关键工具是否就位 which dumpe2fs tune2fs debugfs findmnt state2fsprogs提供 dumpe2fs、tune2fs、debugfs、mke2fs 这一整套util-linux提供 findmnt、lsblk、mount。如果在服务器上做实验注意你需要 root 或至少能挂载文件系统的权限。没有 root 的环境可以退一步用 debugfs 直接读镜像文件它不需要挂载绝大多数分析都能完成。这一点很实用很多在线实训平台就是这么设计的。提示所有操作都在镜像文件里进行不要拿真实分区练手。一个mkfs敲错设备名整块盘的数据就没了而且几乎没有恢复可能。4.2 造盘、格式化、挂载的完整命令先造一块 100MiB 的稀疏镜像再按指定参数格式化# 造镜像用 seek 创建稀疏文件几乎不占实际空间 dd if/dev/zero oflab.img bs1M count0 seek100 # 先干跑一次只看参数不落盘 mke2fs -n -t ext4 -b 4096 -I 256 -N 4096 -L labfs lab.img # 确认无误后正式格式化 mke2fs -t ext4 -b 4096 -I 256 -N 4096 -L labfs lab.img # 挂载 mkdir -p /mnt/lab sudo mount -o loop lab.img /mnt/lab findmnt /mnt/lab参数逐个解释。-b 4096指定块大小影响所有偏移计算-I 256指定 inode 大小256 字节是现代默认值能给扩展属性留空间-N 4096直接指定 inode 总数比用-i更直观这里只给 4096 个-L labfs是卷标-n是干跑一定要养成习惯。用完记得卸载sudo umount /mnt/lab # 或者用 udisksctl桌面环境免 sudo udisksctl mount -b ./lab.img如果是稀疏镜像卸载后用ls -ls lab.img看实际占用你会看到它远小于 100MiB因为它按需分配块。这也是cp一个稀疏镜像时要用--sparsealways的原因。4.3 用 dumpe2fs 逐字段核对超级块先看概要再看全量# 只看超级块概要 dumpe2fs -h lab.img # 看全部包括每个块组的详细信息输出很长配合 head/tail 用 dumpe2fs lab.img | head -80概要输出里要重点核对这几行Filesystem volume name: labfs Filesystem features: has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg Block size: 4096 Inode size: 256 Inode count: 4096 Block count: 25600 Free blocks: 23485 Free inodes: 4085 First block: 0 Block size: 4096 Blocks per group: 32768 Inodes per group: 4096拿它跟你的计算对照。100MiB 是 104,857,600 字节除以 4096 得到 25600 块跟Block count完全一致。因为总块数小于每个块组的 32768 块所以整个文件系统只有一个块组Inodes per group就等于总 inode 数。这个例子特别好用因为只有一个组块组布局看起来一目了然。再把Block count和Free blocks相减得到已用 2115 块乘 4096 是 8.6MiB 左右这就是日志、位图、inode 表加根目录的开销。Free inodes是 4085说明已经有 11 个 inode 被占掉根目录、lostfound 是主要来源。这些数字对上了说明你对静态结构的理解已经落到地上了。4.4 用 debugfs 手工走一遍路径解析debugfs 是这个练习里最有意思的工具它能让你以上帝视角直接读元数据完全绕过 VFS 缓存。# 进入交互模式 debugfs lab.img进来之后依次敲这几条debugfs: stats debugfs: show_super_stats debugfs: stat 2 debugfs: ls -l / debugfs: mkdir /demo debugfs: cd /demo debugfs: write /etc/hostname note.txt debugfs: stat note.txt debugfs: cat note.txt debugfs: dump_unused debugfs: quitstat 2那一步会打印根目录 inode 的完整信息包括模式、链接数、大小、时间戳以及它占用的块号列表。你会看到根目录的Links: 3两个来自.和..一个来自文件系统本身。ls -l /输出的第一列就是 inode 号跟你在挂载后ls -i看到的应该完全一致这是验证实验成功的最直接证据。write命令把宿主机的文件写进镜像然后stat看新文件的 inode 分配情况。重点观察Blocks:那一行和 extent 信息。如果文件很小你会看到它只占一个块。再试试写一个几兆的文件观察 extent 是不是连续的几段。这些操作全是离线的不需要挂载、不需要 root退出后镜像仍然是一致的。debugfs 最大的价值在于安全你想看删除文件后目录项变成什么样就在里面删一个再看ls不用担心影响真实系统。4.5 sync、fsync 与静态结构讲到静态结构就绕不开一个反直觉的事实你以为写进磁盘的结构可能还只在内存里。Linux 的写回机制把文件数据先放进页缓存元数据也放在内存的 inode 和 buffer 里真正落盘要等回写或者显式同步。三个层次的同步手段要分清手段范围典型场景fsync(fd)单个文件的全部内容与元数据数据库提交事务syncfs(fd)该文件所在的整个文件系统批量写完一批文件后sync命令所有文件系统关机前、拔盘前sync命令在 coreutils 8.24 之后支持sync -f 路径走的就是 syncfs只刷一个文件系统比全量 sync 快很多。这个改进对多盘设备意义很大不然你为了保住一块盘的数据得把整机所有盘的脏页都刷一遍。为什么这件事跟静态结构有关因为文件系统的一致性是以结构完整为目标的最小落盘顺序。新建一个文件实际动作是分配 inode、写目录项、写数据块。日志模式下元数据的修改先写日志再写正式位置保证掉电后能重放。哪怕数据块还没落盘只要 inode 和目录项的组合是自洽的文件系统就不会损坏。你拔盘之前不 sync丢的可能只是最后几秒文件的内容而不是整个目录树。我在嵌入式设备上做掉电测试时的经验是不要指望 sync 命令直接在写入代码里对关键文件调 fsync 或者对整个分区调 syncfs。用户空间的sync在断电瞬间不一定跑得完而写代码时你可以把同步点放在业务允许的位置上。5. 换文件系统看差异btrfs、FAT 与 Android 的路径边界5.1 btrfs 没有 inode 表它靠 B 树理解了 ext4 的静态结构再看 btrfs 会有种世界观被刷新感觉。btrfs 是**写时复制CoW**加 B 树的设计它根本没有固定位置的 inode 表所有元数据都组织成树树节点散布在整块盘上。主要几棵树的分工是根树记录其他树的根指针区块树记录逻辑地址到物理地址的映射扩展树记录块的分配与引用计数文件系统树才是存 inode 和目录项的地方还有校验树、设备树、UUID 树。超级块在偏移 64KiB、64MiB、256GiB 各放一份副本开机时哪个校验通过就用哪个这个设计明显是为了容灾。最直观的差异有三点。第一btrfs 的 inode 号是动态分配的会不断增大你看到的 inode 号可能是几百亿这不是 bug。第二元数据默认双份存放单盘上也是 DUP 模式所以元数据占用比 ext4 高换来的是能自动修复单点损坏。第三它有子卷和快照概念子卷是独立的可挂载单元快照是 CoW 出来的瞬时副本几乎不占额外空间。做系统更新时打快照、更新、回滚这套流程就是靠这个实现的。代价也很明确CoW 文件系统天生碎片化长期高负载写入后需要靠后台平衡balance来整理而平衡是个 IO 大户。我在一块长期跑数据库的盘上做过对比ext4 直接改块btrfs 要新分配再改指针随机写场景下 ext4 稳得多。选型时想清楚是要快照和校验还是要稳定的随机写性能。5.2 FAT 与嵌入式方案里的极简静态结构把目光挪到嵌入式就完全是另一套思路了。不少音频、穿戴类的芯片方案上跑的是 FatFs 这类轻量实现它的静态结构简单到可以画在一页纸上。FAT 格式的布局是引导扇区里面是 BPB 参数块记录每簇扇区数、FAT 表数量、根目录项数等、若干份 FAT 表、根目录区、数据区。FAT 表本身就是一个大数组每个表项记录下一个簇的编号文件的簇号串成一条链表。目录项是 32 字节固定结构记录文件名、属性、起始簇号、大小。跟 ext4 相比差异是结构性的维度ext4FAT 系列元数据组织inode 表 位图 树链表 固定目录项权限与属主完整支持基本不支持硬链接支持不支持掉电安全性日志保证极易损坏 FAT 表实现复杂度高极低这就是为什么嵌入式方案偏爱 FAT代码量小、内存占用低、PC 上插上就能读。U 盘、SD 卡、固件升级包几乎清一色用 FAT32 或 exFAT图的就是这个通用性。代价是可靠性和功能都很有限写的时候掉电可能直接把 FAT 表写坏。我在做升级包校验时踩过一个坑设备写 U 盘上的文件写完就立刻复位结果下次开机文件变成 0 字节或者逻辑长度和实际数据对不上。原因就是 FAT 表和目录项还没落盘。解决办法是写完调一次f_sync()再延时断电或者在业务层做完整的校验和确认机制。在 FAT 上写完不等于写对这个认知必须从静态结构层面建立起来。5.3 Android 上从用户文件系统取图片Android 的问题更特殊因为它的静态结构是每个应用看到一份不同的挂载视图。同一个设备上你的应用看到的/storage/emulated/0和别的应用看到的可能挂在不同的命名空间里权限也不一样。先看路径层面。应用私有目录是/data/data/包名/files这类属于应用沙箱其他应用和文件管理器都看不到也不需要任何权限。共享存储是/storage/emulated/0或者软链/sdcard用户能通过文件管理器看到。这两者的静态结构完全不同前者是应用数据分区的一部分卸载即清空后者是独立的模拟存储通过一层用户态文件系统转发。再看访问方式。早期版本直接拼路径就能读现在主流做法是走媒体库或者内容 URI// 通过内容 URI 取图而不是硬编码路径 Uri uri Uri.parse(content://media/external/images/media/12345); try (InputStream in getContentResolver().openInputStream(uri)) { Bitmap bmp BitmapFactory.decodeStream(in); imageView.setImageBitmap(bmp); }如果图片是你自己的应用生成的、要分享给别的应用用 FileProvider 生成临时可授权的 URI而不是把私有路径暴露出去。核心思路就一句话路径不是契约URI 才是。路径是静态结构在某一时刻的具体呈现会因为分区布局、用户切换、系统版本而变化URI 是系统层面的稳定抽象。这里还有一个很多人不知道的细节共享存储上其实还有一个按应用隔离的目录卸载应用时会跟着清掉。往里放东西既能被用户看到又不用申请存储权限是缓存文件的好去处。但别把它当长期存储用户清一次缓存就没了。要验证这些结构/proc/self/mountinfo在设备上是最直接的证据。你可以用 adb 进去看一眼会看到一堆 bind mount 和命名空间隔离的痕迹跟你 PC 上看到的完全是两回事。理解了这一点就不会再写出路径写死然后在某个机型上挂掉的代码了。6. 常见问题与排查实录6.1 问题速查表我把这些年遇到的现象和对应结论整理成一张表遇到问题先照这张表对一遍能省掉大量瞎试的时间。现象最可能原因首选命令有空间但写不进去inode 耗尽df -i文件突然消失挂载点被覆盖findmnt -T 路径刚写的文件读出来是空的脏页未落盘sync后重试目录很大但 ls 很慢dentry 未命中观察第二次是否变快删除文件后空间没释放文件仍被进程打开lsof L1df 和 du 差很多稀疏文件、已删除句柄、保留块交叉验证挂载报错但信息含糊不兼容特性位dumpe2fs -h对比设备写 FAT 后掉电丢数据元数据未同步调 fsync 或延迟断电6.2 inode 耗尽的完整排查路径这个问题的排查流程值得完整写一遍因为它是静态结构知识最直接的变现。第一步确认现象df -h /data # 显示还有空间 df -i /data # IUse% 接近 100%第二步统计目录里的文件数找出是谁在制造海量小文件# 按目录统计文件数量取前 20 find /data -xdev -type d -printf %p\n | while read d; do echo $(find $d -maxdepth 1 -type f | wc -l) $d done | sort -rn | head -20第三步做决策。短期可以删掉无用的小文件释放 inode中期要评估把这类数据改成目录 少量大文件的打包模式或者换成支持动态 inode 的文件系统。注意 ext4 在线扩容只能扩块不能扩 inode 数量唯一的办法是备份、重新格式化、再恢复这个代价一定要提前评估。注意df -i在有些容器环境里显示的数字不可靠因为覆盖文件系统的 inode 来自底层宿主。遇到可疑数据去宿主机上验证。我在生产环境处理过一次日志目录里有上千万个几 KB 的文件df -i早就 100%。最后的方案是改采集程序把日志按小时打包再落盘inode 用量直接降了两个数量级。这类问题的根因几乎总是在应用层的写入模型上不是在文件系统本身。6.3 挂载点覆盖、只读挂载与 df/du 打架挂载点覆盖的典型表现是我明明写了文件重挂载后不见了。验证方法很简单findmnt -T /data # 看 /data 当前挂在什么设备上 sudo umount /data ls /data # 卸载后看原目录里有什么ls出来的就是被覆盖的原始内容。养成习惯往某个目录挂载之前先确认这个目录是空的或者先把它设成不可变从流程上杜绝这种事故。只读挂载的问题也很常见。系统检测到文件系统错误后会把根分区重新挂载为只读来保护数据这时所有写操作都会失败但报错信息可能出现在各种奇怪的地方。快速确认findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS /如果看到ro就要先处理底层错误检查日志找到具体的损坏位置修复后再改回读写而不是简单粗暴地mount -o remount,rw。df 和 du 对不上也是高频问题。原因主要有三类文件被删除但还被进程持有句柄这时 du 看不到但 df 认为已用稀疏文件du 按实际占用算df 按分配算文件系统保留块默认给 root 留 5%普通用户看不到但确实占了。排查顺序是先lsof L1找僵尸句柄再查稀疏文件最后看保留块设置。6.4 实验环境里的几个细节坑做这个练习最常卡住的不是知识而是环境细节。我踩过的有这几个。第一个是 loop 挂载失败。容器里如果没有权限mount -o loop会报 operation not permitted。这时候别硬刚直接用 debugfs 做全部实验它不需要挂载。或者用udisksctl这类用户态挂载工具它会自动处理权限。第二个是稀疏文件被撑大。用dd带 seek 造的镜像在cp之后可能变成实心的因为默认 cp 会逐块读写成实块。加--sparsealways就能保持稀疏省下大量磁盘空间。第三个是忘了卸载就删镜像。文件系统还挂在那里删掉文件也只是删了个目录项空间不释放。务必先umountfindmnt确认没挂载了再清理。第四个是 dump 出来数字对不上自己算的。先检查块大小假设是最常见的错误来源。4KiB 块下超级块在偏移 10241KiB 块下在块 1 的起始位置这个差别会让你的所有偏移量偏掉。最后一个提醒练习用的镜像做完别删加个卷标下节课讲日志或者目录索引的时候还能接着用。我自己留了一组不同参数的镜像从 100MiB 到 8GiB块大小从 1KiB 到 4KiB 各有几块需要验证任何猜测的时候直接 dumpe2fs 一下比查文档快。我在教新人做这个练习时最常强调的一点是别急着背字段名先把一个路径从根目录走到目标文件的完整链条在纸上画出来画到能默写为止。剩下的工具和命令都是查手册的事只有这张链条图是必须长在脑子里的。静态结构讲白了就是这张图不管是 ext4、btrfs 还是分布式系统里的一套元数据服务换的都是实现图的形状是一样的。
返回列表