ARTICLE DETAIL

资讯详情

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

一文读懂Linux文件系统:VFS、inode、挂载与掉电安全

一文读懂Linux文件系统:VFS、inode、挂载与掉电安全 刚接触Linux的时候我对“Linux文件系统”最朴素的理解就是一堆文件夹和文件直到有一次把一块U盘直接拔掉再插上就看到一串ext4恢复日志才意识到这玩意儿的水深得很。后来做嵌入式项目被掉电写坏过分区也在服务器上查过“磁盘满但空间却找不回来”的诡异问题才慢慢把VFS、inode、挂载点这些概念串成了一条线。这篇文章算是我对自己Linux文件系统知识的一个整理既包含新手必须懂的底层逻辑也包含运维和嵌入式开发真正会用到的排查命令、挂载参数和掉电安全方案。1. 从“文件”到“文件系统”先弄清楚你在操作什么1.1 文件不是一个“东西”而是三个概念的组合很多Linux教程会告诉你“一切皆文件”但这个文件在文件系统内部并不是一个连续的对象。在ext4这类传统文件系统里一个文件至少由三部分组成inode、数据块和目录项。inode存的是元数据文件大小、权限、属主、时间戳以及指向数据块的指针数据块存真实内容目录项则把文件名映射到inode编号。你可以用ls -i看到文件的inode号用stat看到完整的元数据。打个比方整个文件系统像一个大型小区。inode是每户的户口本数据块是房子本身目录项就是门牌号。你在命令行里写下的路径内核会一级一级查目录项找到对应的inode再根据inode里的地址去读数据块。很多初学者把“文件名”当成文件的唯一身份但文件系统底层真正关心的是inode。硬链接就是两个目录项指向同一个inode软链接则是另一个小文件里面存着目标路径。这里有个我年轻时踩过的坑以为删除文件就是把数据擦掉。实际上rm只是把目录项和inode之间的关联断掉数据块仍是原样躺在磁盘上的。在没有TRIM的机械盘或传统U盘上只要没被覆盖还能通过debugfs等工具找回一部分。到了SSD上则要拼运气因为FTL和TRIM会在后台清理无效块。1.2 VFS层为什么各种文件系统能在一台机器里和平共处Linux支持兼容几十种文件系统却不要求每个程序都专门写一套代码靠的就是内核里的VFS虚拟文件系统抽象层。无论是ext4、XFS、Btrfs还是NFS这种网络文件系统又或者内存里的tmpfs对用户态进程来说都长一个样接受open、read、write、close这些系统调用返回文件描述符。VFS相当于一个总调度台把通用请求翻译成具体文件系统的私有操作。VFS里有几个核心对象super_block、inode、dentry、file。super_block描述整个文件系统实例inode对应元数据dentry是目录项缓存file才是进程打开文件时产生的动态对象。这套设计看起来很学术但它直接影响你每天看到的/proc和/sys。这两个目录根本不是磁盘上的文件而是内核动态生成的“虚拟文件系统”。所以你能像读普通文件一样cat /proc/cpuinfo却永远找不到它在硬盘上的真实位置。理解这层抽象还有一个实际价值排查问题时可别再用“我格式化成了ext4”来一刀切。同样一个分区挂载选项不同、内核特性不同表现可能完全不同。文件系统出问题时我们要判断是具体驱动的问题还是通用VFS逻辑的问题排查范围会完全不一样。1.3 常见文件系统定位差异选型不是越多越好文件系统定位典型场景注意点ext4通用日志文件系统Linux服务器、桌面、嵌入式eMMC/SD兼容性好inode数量创建时不方便动态改XFS高扩展性、大文件友好大数据、视频存储、高性能服务器无法缩小分区删除大量小文件有时反而慢Btrfs快照、压缩、校验和新特性优先的存储服务器、容器成熟度不如ext4/XFS坏块场景要小心tmpfs内存文件系统/tmp、共享内存、容器overlay重启即丢受内存大小约束NFS网络文件系统嵌入式开发、共享存储网络延迟敏感重IO场景要调参数littlefs掉电安全的嵌入式文件系统MCU、裸Flash、IoT设备文件数、单文件大小限制明显表格里每一类背后都有一堆故事。比如ext4拔U盘容易遇到recovery而XFS在小文件密集场景表现可能不如ext4。选型第一条原则是匹配硬件环境第二条才是性能。这些差异我会在后面几节展开讲。2. 日常操作里那些“看不见”的文件系统行为2.1 df和du永远对不上你先要搞清楚它们统计的口径运维最常遇到的“假警报”就是df -h显示磁盘100%可用空间为0但du -sh /把整个目录全加起来都到不了这个数字。面对这种情况第一步不是删文件而是理解df和du的口径完全不同。df直接读取文件系统的块统计基于super_block里记录的“已用/剩余块数”du则是遍历目录树把每个文件块数累加起来。目录项本身占用的块、inode表、日志文件以及已经删除但还被进程打开的文件df会统计du却看不到。我前阵子排查一台测试服务器根分区在df里已经100%du却只统计出60%左右。用lsof -nP | grep deleted一看果然有个进程正在写一个已删除的旧日志文件占了几十GB。解决方法是重启进程或 /proc/PID/fd/路径清掉它。这个操作确实粗暴但在磁盘满到没法正常启动服务时先保命再找根因才是正理。另外reserved block也容易让人误会。ext4默认会保留5%空间给root用避免磁盘满后系统无法写关键日志。普通用户dd到95%就报no spaceroot还能继续写。在mkfs时可以用-m 0取消保留但我不建议在大分区上这么干真要满盘时你会感谢那5%空间。2.2 mv、ln、rm背后的目录项和inode操作日常命令一组合起来就很神奇同样一个mv在同一个文件系统内只是修改目录项瞬间完成跨文件系统则是“复制到新位置删除旧记录”慢得让人怀疑电脑坏了。因为硬链接不能跨文件系统跨区的cp、mv、rsync会创建一份完整的新inode。如果目标目录所在分区空间不够mv还会失败留下一个断掉的路径。ln -s创建软链接时新文件是一个独立inode里面的内容是路径字符串。所以软链接可以指向任何地方甚至可以指向不存在的目标而硬链接直接共享原文件的inode。我常用stat去验证自己写脚本时到底是链接还是副本避免误删。上面说过rm只是断开目录项与inode的关联当引用计数降到0后文件的block才会被标记为可回收。如果一个文件被多个硬链接引用删掉一个链接数据仍然在只有所有链接都被删掉文件才算真正“消失”。这里有个不错的测试对一个大文件做多个硬链接然后删掉其中一个再查磁盘占用。你会发现df里的剩余空间并没有恢复。这个现象对理解inode引用计数非常有帮助。2.3 sync、fsync和脏页回写数据到底什么时候落盘我见过不少人以为write一返回数据就稳稳落在硬盘上了。Linux默认的策略其实是“先写内存后落盘”。每个普通write只是把用户缓冲区复制到内核的page cache并把对应的页标记为脏页。真正写回磁盘要等两个条件一个是时间到了默认30秒另一个是脏页比例超过/proc/sys/vm/dirty_ratio。这个过程由内核线程flush/kworker完成。所以拔U盘或者关机前最稳妥的方法是执行sync。它会把整个缓存里的脏页刷一遍。但对数据库这类要求严格的应用来说单个sync太粗暴了通常用fsync或fdatasync保证某一个文件的落盘。fsync把数据和元数据一起同步fdatasync只同步数据从性能角度大量写日志的进程用fdatasync收益明显。另一个极端是嵌入式设备如果掉电时数据损坏可以在挂载时用sync选项让每次write返回前都真正写完代价是慢上几倍。调节回写频率也是运维常用手段。比如宿主机上有一堆临时目录频繁写缓存又不希望影响磁盘IO可以调整dirty_expire_centisecs和dirty_writeback_centisecs。但别乱调回写太保守会让断电丢数据窗口变大太长又会让IO一下子突发。3. 挂载与根文件系统从内核启动到应用运行的必经之路3.1 mount不只是“挂上”而是把两棵树接成一颗Linux的目录结构是一棵以/为根的大树。一个新分区刚格式化完只是一个孤零零的文件系统实例必须通过mount命令把它的根目录“嫁接”到某个目录上。挂载点可以是任意目录mount /dev/sdb1 /mnt/tmp之后访问/mnt/tmp就等于访问那个分区的根目录。挂载时内核会读取新文件系统的super_block并和挂载点的dentry建立关联。可以用findmnt或mount命令查看当前挂载树。umount则是从树里“断开”一段但前提是没有任何进程正在使用这个挂载点下的文件。常见报错“target is busy”多半是某个进程的工作目录在里面或者还有文件被打开用lsof f -- 挂载点能查出来。mount还有两类容易混的问题。一类是挂载了但看不到内容很可能目标目录本来就是非空的——原目录里的文件会被挂载点盖住直到卸载才重新出现。另一类是挂载选项写错比如noexec会导致程序明明有执行权限却运行失败nouser会限制普通用户挂载。排查这类问题最快的方式是mount -t type -o ...后立刻看findmnt输出的options列。3.2 嵌入式Linux根文件系统挂载initramfs、MMC块设备与NFS v3嵌入式根文件系统的挂载比PC上要来得“硬核”因为它直接关系到内核启动链路bootloader把内核和设备树加载到内存内核初始化完成后根据启动参数里的root找到根设备并挂载根文件系统。常见的生产方案是root/dev/mmcblk0p2 rootwait也就是从eMMC或SD卡的第2个分区挂载根文件系统。rootwait是为了等块设备注册完成否则内核可能找不到设备而panic。开发调试阶段我一般用NFS挂载根文件系统这样编译完内核模块或应用后直接放到开发主机的rootfs目录里目标板重启即生效大幅缩短烧录周期。启动参数大致是这样root/dev/nfs nfsroot192.168.1.10:/srv/rootfs,prototcp,nfsvers3 ipdhcp rw主机侧在/etc/exports里写一行/srv/rootfs *(rw,sync,no_root_squash,no_subtree_check)并执行exportfs -ra。这里我特意用了nfsvers3。NFS v3在现代内核里支持度很成熟比v2支持更大的文件比v4的权限协商简单嵌入式交叉编译环境里踩的坑最少。需要注意一点内核必须开启Network File System支持以及NFS root支持CONFIG_ROOT_NFS和CONFIG_NFS_V3都要打开。目标板跑起来后如果根文件系统挂载失败最优先看串口打印。常见症状是VFS: Unable to mount root fs十有八九是内核没有对应驱动或者nfsroot路径、IP地址写错。先ping通主机再说别的。3.3 挂载选项里的坑noatime、sync、discard怎么选挂载选项对实际磁盘行为影响很大。最典型的是atime。早期内核每次读取文件都会更新最后访问时间导致大量写盘操作后来默认改成relatime只有访问时间早于修改时间才更新。如果要彻底减少写放大可以挂noatime。对嵌入式设备来说哪怕只是日志分区这个选项都能明显延长Flash寿命。sync选项则是另一个极端每次文件写入都直接写到磁盘不再依赖回写机制。它的好处是掉电丢数据的窗口极短坏处是性能断崖式下跌。我一般只在U盘根文件系统、boot分区这种数据量小但对一致性要求高的地方用。绝大多数分区用默认的async即可靠ext4的日志机制保证崩溃恢复。discard和fstrim也值得注意。SSD上开启discard删除文件时自动发TRIM命令但频率高的时候可能会影响性能。我更倾向于让系统定期用fstrim清理而不是在挂载时开discard。机械硬盘无脑挂discard没有任何意义。对于嵌入式eMMC同理建议用fstrim或干脆不做因为eMMC会自己回收块。4. 嵌入式文件系统的特殊选择littlefs与掉电安全4.1 为什么裸Flash上不能直接上ext4嵌入式项目里面临一个现实不是所有存储介质都可以当成普通磁盘用。NOR Flash和NAND Flash的物理特性与机械硬盘完全不同擦除以块为单位、写入需要先擦除、每块擦除次数有限而且掉电时如果正巧在更新元数据很容易导致整个文件系统损坏。ext4这类为块设备设计的日志文件系统依赖底层把它模拟成“可以随机读写的块阵列”。eMMC、SD卡因为有FTL和控制器天然做了这层转换所以能直接用ext4。但很多MCU外扩的SPI NOR Flash没有成熟的FTL这时候就需要UBIFS或littlefs这类专门给裸Flash设计的文件系统。littlefs在嵌入式圈子里流行起来一个重要原因是它面向掉电安全无论什么时候断电挂载后都能回到一个可一致状态。它不是靠日志回放而是基于“提交点写时复制”的策略每次写操作先写新数据块再更新元数据并通过全局状态定位最新的提交点。4.2 littlefs的原理要点和PlatformIO集成littlefs的核心思想可以用一句话概括写新读旧选最新。它内部有一组“可搬移的元数据对”每次元数据更新都写到另一块区域并记录一个序号挂载时根据序号确定最新版本。坏块处理和磨损均衡也都在库内部完成。用起来其实很普通lfs_format格式化成empty文件系统lfs_mount挂载之后就是lfs_file_open、lfs_file_write、lfs_file_close。没接触过的人可以把它当成一个“掌上版文件系统”底层细节全被封装掉了。在PlatformIO里使用littlefs很方便尤其搭配Arduino框架。只需要在platformio.ini里加上board_build.filesystem littlefs然后在项目里引入LittleFS库#include LittleFS.h void setup() { LittleFS.begin(); }上传文件系统镜像时执行pio run -t uploadfs它会根据分区表把data目录里的文件打包成littlefs镜像烧入Flash。需要注意的坑是platformio默认的文件系统分区大小往往很小改大分区时一定要同步调整partition表否则烧进去的rootfs或用户文件会越界失败。我第一次跑uploadfs烧进去几十个网页文件结果设备重启后发现目录全是乱码后来查代码发现分区大小填错Loader覆盖了后面的配置区。4.3 实际项目里的容量规划和磨损均衡littlefs虽然好用但不是万能神药。它适合“小文件、频繁覆盖”的场景比如配置参数、日志、OTA状态记录。它不太适合存储超大文件块分配策略和元数据约束会让大文件读写比FAT慢很多。所以实际项目里我通常会同时规划两层存储一层eMMC或SD卡上的fatfs放图片、视频等大文件一层SPI NOR上的littlefs放关键配置和日志。容量规划上我建议保留至少10%到20%的空余空间给littlefs做磨损均衡和元数据搬移。不要把它用到100%否则文件系统会频繁搬移数据块掉电风险也会上升。日志建议做成循环文件限制数量并定期轮转比如每次开机写一个新的log.N保留最近10个。反复覆盖同一个文件虽然littlefs也能处理但会加速特定块的磨损最终还是影响整颗Flash寿命。用PlatformIO上传完littlefs镜像后如果反复出现挂载失败先检查Flash芯片的SPI时钟频率很多型号跑太高会偶发读写错误降低频率通常能稳定很多。这是我在好几个ESP32项目里测出来的共性经验。5. 三次“磁盘满”的排查链路从表象到根因5.1 第一次df显示100%du却只统计到一半前面提到过根分区显示满但目录占用量对不上最经典的场景就是“已删除文件仍被进程占用”。碰到这种情况用lsof | grep deleted列出删掉但仍打开的文件。输出里会有PID、文件描述符和路径。看到日志文件后可以用ls -l /proc/PID/fd/FD确认实际占用的空间然后决定是重启进程、截断文件还是先保留证据。如果lsof没装还有一个传统手段是du -h --max-depth1 /一层一层往下找但这种方法对“神秘空间”完全无效因为它根本不会统计已删除的文件。所以排查满盘问题我建议顺序是先df确认挂载点再lsof查deleted最后才考虑目录级分析。另外别忘了还有一层是tmpfs/dev/shm、/run这些看起来不占磁盘的空间也可能间接影响可用内存和“df满”不是一回事。5.2 第二次明明有空间却说No space left on device有一次rm掉了一堆临时文件df显示还有20GB可用可应用仍然报“No space left on device”。我盯着输出看了半天猛然想起来应该看inode。df -i /一看inode已经100%了。原因是那个分区上正好跑了一个容器日志目录生成了几十万个小文件inode表被吃满。磁盘空间还有但新文件已经没有“身份证”可以发了。ext4格式化时inode数量取决于块大小和bytes-per-inode参数默认每4096字节就创建一个inode。对小文件密集场景可以mkfs时指定更大的-i值或直接-N指定数量。但已创建的分区不能动态增加inode所以善后只有删文件或迁移。后来我改用XFS承载容器目录就是看中它inode可以动态增长不用提前精算。如果是老服务器又不能迁移最快的缓解手段是找到小文件密集区用find按size或按数量批量合并压缩。5.3 第三次根文件系统突然变成只读挂载为只读是Linux文件系统自我保护的一种表现。内核在发现文件系统出现严重I/O错误或元数据异常时会把分区从rw降级成ro避免进一步损害数据。遇到这种情况不要急着mount -o remount,rw /。先看dmesg | tail -50找到底是磁盘I/O error、网络文件系统超时还是ext4校验失败。我遇到过一盘SATA机械盘出现大量坏道dmesg里连续爆ata exceptions根分区自动变ro了。当时第一反应是先将磁盘数据备份出来然后smartctl检查SMART状态确认硬件确实有问题再更换。如果是ext4的超级块或元数据被破坏会提示EXT4-fs error (device sda1): ext4_find_entry这类信息可以在单用户模式下跑e2fsck -y修复。修复之前无论如何要备份盲目fsck把可用数据搞丢的情况我见过不止一次。运维里还有一个容易被忽略的NFS根文件系统。嵌入式目标板跑得好好的突然整个系统变成只读不用先怀疑Flash坏了有可能是主机侧NFS服务方没响应或网络断了。目标板的NFS客户端通常有软/硬挂载选项硬挂载在服务不可用时候会反复重试表现就是“卡死”或“只读”。写开发脚本时对NFS根文件系统做重启测试前最好先确认主机侧systemctl status nfs-server是正常的。6. 面试与进阶思考文件系统最容易被问透的几个点6.1 硬链接、软链接和inode最基础的“送分题”也是“送命题”面试官爱问硬链接和软链接的区别。背答案谁都会但追问到“硬链接能不能跨文件系统”“软链接的目标被删除后会怎样”就露馅了。硬链接不能跨文件系统因为它绑定的是同一个inodeinode不能随目录项跑到别的文件系统里。软链接可以跨因为它是独立文件内容只是一段路径字符串目标失效也只会留下“broken link”。我自己被追问“如果删掉原文件软链接和硬链接分别会怎样”时曾经凭直觉答错过一次。实测下来硬链接的原始目标文件被rm后另一个硬链接仍能正常打开inode和内容都还在软链接指向的文件被rm后ln -s本身还在但cat会报No such file or directory。如果你在面试里再把“rm是把链接计数减一计数归零才释放inode和数据块”这层机制讲出来基本就是把送分题变成了加分题。6.2 页面缓存、脏页和回写答出这层就能聊出深度另一个高频点是write返回后数据在哪里。很多人只知道“还有缓存”但真正有深度的回答应该包含page cache、dirty page、flusher线程、dirty_ratio触发条件以及fsync/fdatasync/sync的区别。面试时被问到“如果突然断电数据可能丢多少”答案不是一个固定阈值而是看脏页有没有被刷出去。默认dirty_ratio大约是20%在内存很大的机器上意味着最多可能有几个G的数据停留在缓存里断电就全部丢失。实际调优时要分场景。数据库服务器通常希望脏页尽快刷盘可以降低dirty_expire_centisecs和dirty_writeback_centisecs但这也意味着磁盘IO请求增多。而在一台只做缓存代理的服务器上可能更希望保留较多脏页来吸收写入峰值这时可以适当调高dirty_ratio。我自己的原则是动这些参数前先用vmstat或者iostat连续观察几天基线没有性能问题就不动。6.3 OverlayFS、容器镜像与文件系统现代云原生绕不开的一层容器镜像的分层本质也是文件系统的组合。OverlayFS把多个目录层叠加成一个合并视图上层文件覆盖下层同名文件。跑容器、构建镜像时大量的层和解压操作都在/var/lib/docker/overlay2里进行所以这个目录所在分区如果inode耗尽就会出现No space的经典故障。解决思路和前面排查一样先df -i确认再考虑删除不用的镜像和容器层、合并小文件。另一个和容器相关的容易忽略点是容器内临时文件默认写在可写层如果可写层是一块容量很小的分区即便宿主机还有几十G空闲容器照样报磁盘满。理解VFS之后再回看OverlayFS会发现它只是挂载在VFS之上的又一类具体实现。该理解挂载、理解super_block、理解inode的地方一样都不少。这也是为什么很多云原生工程师回过头来补文件系统基础补得越透排障越快。我对文件系统最大的体会是它离应用这么近却很少有人真正把它当“系统”去学习。建议有空的同学自己做一个实验准备一个小分区格式化后看df -i创建几万个文件观察inode耗尽的现象再拔U盘不断电观察ext4 recovery日志。这些亲手操作花费不了半天却比看文档有效得多。我自己就是这样一点点把文件系统从“玄学”变回科学的。
返回列表