ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Squashfs只读文件系统:嵌入式Linux根文件系统的稳定基石

Squashfs只读文件系统:嵌入式Linux根文件系统的稳定基石 做嵌入式Linux快十年了我越来越觉得一个容易被低估的东西不是某个调度算法而是文件系统。很多项目一开始都把根文件系统直接丢进ext4分区然后就开始跟掉电损坏、日志刷爆、OTA升级写坏rootfs的问题搏斗。后来我切换到Squashfs这种只读文件系统很多问题突然就消失了。Squashfs在Linux世界里不算新东西但它的价值在今天反而比早期更突出只读、压缩、按需解压这三个特性正好踩中嵌入式根文件系统、LiveUSB、容器镜像和固件升级包的刚需。这篇文章不打算堆理论我会从为什么需要只读文件系统、镜像内部的布局原理、mksquashfs的实操参数、挂载和overlay的组合再到嵌入式场景的选型边界最后把常见故障排查路径列出来。不管你是做智能网关、工业设备、车载系统还是只想把一个Linux发行版做成可启动U盘这篇都适合你。1. 只读文件系统不是落伍先从几个日常场景看Squashfs的不可替代性1.1 三个最容易遇到Squashfs的场景第一个是嵌入式设备。路由器、智能网关、DTU、工业控制器的根文件系统本质上是一堆固定的二进制、库、配置模板和内核模块。这些内容在产品生命周期里基本不变把整个rootfs做成Squashfs镜像能省下大量Flash空间。我做过一个网关项目原始rootfs大概96MB用Squashfs默认参数压完只有23MB左右直接让eMMC分区从2GB砍到512MB都够用。第二个是LiveCD/LiveUSB和系统恢复环境。你拿一个发行版ISO启动桌面环境、驱动、应用全在那个只读镜像里但系统跑起来之后依旧能改配置、装软件靠的就是overlayfs把可写层叠加在只读层之上。底层那个只读层绝大多数情况下就是Squashfs。第三个是OTA和容器镜像。固件升级包经常直接打包成一个Squashfs文件升级时只有校验通过才写入目标分区。容器镜像分层虽然默认不是Squashfs但很多OCI兼容实现为了提高分发效率和缩小镜像体积也会导出/导入Squashfs格式。我甚至见过有人把整个Docker镜像目录直接压成Squashfs做成一个离线可挂载容器包省掉了registry这一步。1.2 只读带来的不只是不能写Squashfs挂在系统上之后内核会强制以只读方式使用它用户态没有合法的写路径。这意味着三件事。第一运行期系统文件不可能被误改写。哪怕是root权限下误执行了rm -rf操作范围也只会停留在可写层底层镜像纹丝不动。第二意外掉电不会损坏文件系统。ext4这类可写文件系统掉电之后可能出现日志不一致、inode分配表错乱必须在下次启动时fsckSquashfs根本没有脏页回写这回事断电就像把一本书合上下次打开永远还是那本书。第三系统被入侵后的破坏面被限制住了。攻击者即使拿到shell改不了镜像里的二进制重启之后就恢复原样。这几点对批量生产设备特别重要。售后不需要技术员去现场修系统恢复出厂设置很多时候就是重新挂载一下干净镜像甚至什么都不用做重启即可。1.3 和ext4挂成只读的区别在哪有人会说我也可以把ext4挂成ro效果不是一样吗不一样。ext4是一个可写文件系统ro只是挂载时的一个约束条件。它内部该有的日志、块位图、inode分配逻辑全都在一旦你误以rw方式挂载它马上开始读写。而且ext4只读挂载后如果镜像分区本身在掉电前就有脏数据或日志需要回放内核通常会拒绝只读挂载要求你先修复。Squashfs从磁盘结构上就没有为写准备任何接口。没有日志没有块分配位图没有写指针。文件数据以一种紧凑的压缩布局静态存放。这个区别就像大门上贴了一张暂停营业的公告和这栋楼根本没有门。公告随时可能被撕掉而没有门则从根本上杜绝了误入。1.4 从romfs、cramfs到Squashfs的历史选择早期内核里做只读文件系统的有romfs和cramfs。romfs极其简单但不支持压缩适合做一个极小的启动引导环境。cramfs支持压缩可它年代太久文件名长度、文件大小、inode数量方面限制很多而且维护不活跃。Squashfs从2009年进入内核主线后逐步成为只读文件系统的标准答案它支持gzip、lzo、lz4、xz、zstd多种压缩算法文件系统和单文件大小上限大幅提升还能导出NFS、支持稀疏文件、硬链接、大文件系统。可以说现在凡是说Linux只读压缩文件系统默认就是在说Squashfs。2. 镜像里到底藏了什么superblock、block与按需解压2.1 一个核心设计把文件切成独立压缩块Squashfs能压缩又能随机访问关键就在于它把每个文件按固定大小切成逻辑块每个块单独压缩。这个块大小默认是128KB可以用-b参数改。如果一个文件很大比如100MB那它会被切成大概800个128KB的逻辑块。inode里记录的不是文件内容本身而是这800个压缩块在镜像文件中的偏移位置和压缩后长度。你读取文件偏移10MB的位置时内核先算出这属于哪几个逻辑块找到对应的压缩数据解压出原始块返回给用户态。其余799个块根本不会动。这跟zip形成了鲜明对比。zip虽然也逐个压缩文件但你要读取尾部数据必须处理前面的中央目录和压缩流顺序读取成本高。而Squashfs把随机读取和压缩绑定在一起所以它在运行时表现更像一个普通块设备文件系统读哪里就解压那里。打个比方Squashfs像一墙独立的压缩饼干包装袋想吃第37块就拿第37包拆开。tar.gz则像一个被压成一条的压缩饼干想吃到中间那段得把前面一路啃过去。2.2 简化的镜像布局我可以给一个便于理解的布局示意实际细节会随版本略有差异但整体思路不变[superblock] [inode/directory metadata 区压缩存储] [data block 区文件主体数据逐块压缩] [fragment block 区多个小文件尾部打包] [export table / 其他扩展表]superblock在镜像开头记录了整个文件系统的大小、块大小、inode表位置、碎片表位置、压缩算法等关键信息。内核挂载时第一步就是读它。接着是inode和目录项。Squashfs的元数据本身也是压缩的这意味着哪怕一个目录里有几千个小文件目录项的开销也被压得很小。为什么很多设备敢把整个usr目录放进去就是因为这个。文件的数据主体放在data block区。每个block独立压缩所以前面说的随机访问才成立。2.3 fragment机制小文件的尾段不浪费大多数文件系统里文件占用的最后一个块往往会浪费一部分空间。比如块大小128KB一个文件130KB第二个块只用了2KB剩下126KB就空着。Squashfs用fragment机制来解决这个问题把很多小文件不足一个block的尾部数据统一收集起来打包塞进公共的fragment block里。配合它自己的inode设计一堆十几KB的配置文件、shell脚本、日志模板并不会像你想象的那样浪费空间。这也是Squashfs对大量小文件场景依然友好的原因。不过fragment block意味着随机访问一个小文件尾部时可能要把整个fragment block解压出来。这是设计取舍实际影响很小因为碎片块通常只有专门的尾部数据体积可控。2.4 内核中如何接入VFSSquashfs是作为一个标准Linux文件系统类型注册进内核的。它的核心路径是这样的register_filesystem(squashfs_fs_type)让VFS知道mount -t squashfs该找谁squashfs_mount负责读取superblock做基础校验squashfs_fill_super解析inode表、目录结构建立根目录文件系统挂载后所有打开、读取、目录遍历操作都走VFS的通用路径真正读数据时VFS调用Squashfs自己的地址空间操作比如老内核里的squashfs_readpage新内核里则是read_folio它负责读取对应的压缩block并解压在内核编译时你要确保CONFIG_SQUASHFS打开同时打开你实际使用的压缩算法对应的配置项比如CONFIG_SQUASHFS_ZSTD、CONFIG_SQUASHFS_XZ。如果镜像用了xz压缩而内核没有开启xz解压支持挂载可能显示成功或者部分文件读不了这个问题我后面专门讲。理解了这些你就会明白Squashfs的压缩和解压不是在用户态一次性完成的而是由内核在每次I/O路径上按需完成的。块大小、压缩算法、解压内存峰值这些参数因此直接决定了运行时表现。3. 亲手做镜像mksquashfs参数、压缩算法和我的一轮实测3.1 工具安装制作Squashfs镜像的工具叫squashfs-tools里面最常用的是mksquashfs和unsquashfs。Debian/Ubuntu系直接用sudo apt install squashfs-toolsRHEL/CentOS系用sudo dnf install squashfs-tools如果你要交叉编译给其他平台用也可以从官方仓库拉源码自己编但绝大多数发行版都有现成包没必要折腾。3.2 最基础的一条命令假设你有一个目录rootfs/里面是完整的根文件系统内容我要把它做成rootfs.sfsmksquashfs rootfs/ rootfs.sfs \ -comp zstd \ -b 128K \ -processors 4 \ -noappend \ -all-root \ -mkfs-time 1700000000 \ -all-time 1700000000说下为什么加这些参数。-comp zstd选压缩算法。zstd这几年的综合表现最均衡后面我会给实测数据。-b 128K设置块大小默认就是128K但我习惯写出来团队里其他人一看就知道这个镜像的构建基准。-processors 4指定用4个CPU核心并行压缩构建快很多。-noappend很关键如果rootfs.sfs已经存在mksquashfs默认会把新的文件系统追加到旧镜像里产生一个包含旧数据和新文件的怪物镜像。加了这个参数目标文件存在就直接覆盖。-all-root把所有文件的属主统一设置成root嵌入式量产镜像里一般不需要保留开发机上的uid/gid统一root能减少部署环境差异。-mkfs-time和-all-time把镜像本身和所有文件的时间戳固定下来实现可复现构建。这个对CI和版本对比非常重要我后面还会提。3.3 更实用的参数速查我平时用到频率比较高的参数整理成了这个表参数作用我的习惯-comp指定压缩算法zstd或xz看平台-b设置块大小默认128K低内存用64K-processors并行压缩线程数同构建机核数-noappend不追加到已有镜像每次都加-all-root所有文件属主设为root量产镜像必加-mkfs-time固定镜像创建时间CI构建时固定-all-time固定所有文件时间戳同上-sort按列表顺序排列文件启动关键文件优先-mem限制构建内存内存紧张时用-wildcards支持通配符排除/包含需要排除目录时用3.4 压缩算法怎么选一轮实测参照我拿一个实际项目的rootfs做过对比。内容以BusyBox、glibc、一些Python库和Qt运行库为主原始大小约96MB构建机是四核A53的嵌入式构建服务器性能不算强。结果大致如下算法镜像大小构建耗时解压速度体感gzip26.8MB14秒正常中等CPU开销xz21.3MB230秒明显偏慢高CPU开销zstd22.1MB10秒快明显比gzip省CPUlz431.2MB6秒最快但镜像明显更大这个数据只代表那一次项目的负载特征纯文本、二进制库、已压缩图片的占比会对结果造成很大影响但方向是稳定的xz压缩率最好但构建和解压的CPU开销都大zstd在压缩率和速度之间拿到了最优平衡lz4适合存储不紧张、追求极致解压速度的场景。如果设备CPU很弱比如几百MHz的单核处理器我建议用gzip。它兼容性最好解压开销可控。如果设备内存很小比如只有32MB可用内存可以把块大小降到64K牺牲一点压缩率换取更低的单次解压缓冲。-b 256K会带来略好的压缩率但随机访问粒度变大、解压缓冲翻倍不是所有场景都划算。3.5 制作完怎么验证一个常见误区是做完镜像直接上传量产我不建议这么做。最轻量的验证是把镜像的关键信息读出来确认superblock和文件列表没有异常unsquashfs -s rootfs.sfs这个命令会显示压缩算法、块大小、inode数量、文件系统大小。再看一下文件列表unsquashfs -ll rootfs.sfs | head -20如果需要完整解包到本地目录unsquashfs -d rootfs-extracted rootfs.sfs我在CI里会把构建解包回读对比关键目录结构计算镜像sha256串成一条流水线镜像产物如果解不出来构建直接失败不会流到发布环节。4. 从mount到overlay挂在只读根文件系统上的写层方案4.1 基本挂载loop设备与ro把一个普通的squashfs镜像文件挂上系统标准命令是sudo modprobe squashfs sudo mkdir -p /mnt/ro sudo mount -t squashfs -o loop,ro rootfs.sfs /mnt/ro-o loop是让内核把镜像文件当作块设备来使用分配一个loop设备。ro明确只读。虽然Squashfs本身没有写支持写上这个参数既符合习惯也能让一些上层工具误判时有所警觉。如果Squashfs直接写在块设备分区上就不需要loop了sudo mount -t squashfs -o ro /dev/mmcblk0p2 /mnt/ro4.2 作为根文件系统的启动参数嵌入式设备里Squashfs最常见的角色就是根文件系统。假如镜像放在eMMC的/dev/mmcblk0p2内核启动参数可以这样写root/dev/mmcblk0p2 rootfstypesquashfs rorootfstypesquashfs让内核不要一个个文件系统去试直接按Squashfs解析。配合initramfs时要确保initramfs里有squashfs模块和解压器模块否则根文件系统挂不上系统会掉进initramfs的shell。4.3 叠加可写层overlayfs组合拳Squashfs只读那用户要改配置、写日志怎么办标准做法是叠加一个可写层。这就是overlayfs的经典用法。我给出一个实际可跑的流程。先把Squashfs底层挂到一个只读目录sudo mount -t squashfs -o loop,ro rootfs.sfs /mnt/ro然后在内存或数据分区上建一个可写upper层sudo mkdir -p /mnt/upper /mnt/merged /mnt/work sudo mount -t tmpfs tmpfs /mnt/upper注意work和upper必须在同一个文件系统上所以我把它们都放在tmpfs内部。接着挂overlaysudo mount -t overlay overlay \ -o lowerdir/mnt/ro,upperdir/mnt/upper,workdir/mnt/work \ /mnt/merged挂载之后/mnt/merged看起来是一个完整的可读写目录。读操作优先找upper层找不到再下探到只读层写操作全部落在upper层。系统重启tmpfs清空配置又回到镜像里的初始状态。这就是干净重启的底层原理。如果upper需要持久化可以把upper层放在ext4或UBIFS分区上而不是tmpfs。区别只是重启后用户配置是否保留。4.4 A/B双分区升级里的Squashfs现在很多设备做A/B双系统升级系统有两个slot每个slot里有一个Squashfs根文件系统镜像。升级时把新镜像写入不活动的那个slot校验通过后切换启动标志下一次启动进入新系统。如果新系统引导失败bootloader还能回退到旧slot。这里Squashfs的优势非常明显整个系统镜像就是一个单文件你可以对整文件计算sha256升级前校验签名升级中断电也最多只破坏目标slot已运行的系统不受影响。我之前用ext4做A/B分区时最怕的就是升级过程中把文件系统元数据写坏而Squashfs镜像是一次性整体写入的结构和内容都是静态的风险小很多。4.5 聊聊sync这个热词网上经常有人问Squashfs挂载要不要加sync。我的回答是运行时不需要构建和烧录阶段才需要。Squashfs挂载后根本没有脏页回写路径意外掉电不会出现文件系统层面不一致。真正的sync发生在mksquashfs执行完之后以及你往Flash/eMMC写入镜像之前。养成习惯制作完镜像先sync确认镜像文件数据完整落盘再执行dd或烧录工具。烧录完成后再sync一次然后拔卡或断电。这个动作多花一秒能帮你排掉大量镜像看起来写进去了重启又起不来的问题。5. 别乱选Squashfs、UBIFS、LittleFS和NFS调试的边界5.1 不同文件系统各有主场老有人问Squashfs能不能替代UBIFS或者LittleFS是不是Squashfs的对手。这俩根本不是一回事放在一起比较毫无意义。我做了个表方便你对照文件系统可写压缩磨损均衡典型场景Squashfs否是无依赖底层MTD/UBILinux根文件系统、OTA镜像、LiveUSBUBIFS是是是NAND Flash上的可写数据分区ext4是否无依赖FTL/MMCeMMC、SD卡上的数据分区LittleFS是否有MCU、SPI NOR Flash资源极小的环境5.2 Squashfs UBIFS的组合很常见NAND Flash的嵌入式设备里最成熟的方案通常不是二选一而是组合Squashfs承载只读的rootfs和应用程序UBIFS承载用户数据、日志、配置。UBIFS本身带磨损均衡、掉电保护适合Flash的物理特性Squashfs则保证系统镜像本身的紧凑和不可篡改。这样两个文件系统各干各的整个系统既稳定又灵活。我曾经在一个工业设备上试过用UBIFS直接做整个根文件系统结果固件升级时一旦遇上意外断电UBI volume就需要长时间扫描恢复用户那边很难接受。后来改成Squashfs放rootfsUBIFS只放数据类似问题再没出现过。5.3 LittleFS什么时候用LittleFS是给MCU级别设备准备的典型场景是PlatformIO写ESP32、STM32外挂SPI NOR Flash做配置保存、数据日志。它掉电安全、有磨损均衡、资源占用极小一个单片机裸机环境就能跑。它确实跟Squashfs一样追求数据在断电后依然可靠但Squashfs需要Linux内核和一定内存才能运行LittleFS只需要几个KB的RAM。所以如果你在做的项目是MCU加SPI NOR直接选LittleFS就好。如果你在嵌入式Linux平台上看到有人说用LittleFS做根文件系统除非是那种极小内核配合极简环境的特殊场景否则还是优先考虑Squashfs加可写数据分区。5.4 开发期用NFS量产期切Squashfs这个是我个人很推荐的工作流。开发调试阶段根文件系统用NFS v3挂载代码改了立刻生效不需要反复烧录Flashroot/dev/nfs nfsroot192.168.1.10:/srv/nfs/arm-rootfs,v3 ipdhcpNFS挂载根文件系统的好处是方便但绝不适合量产。它依赖网络、对存储介质没有保护、掉电场景下行为不可控。等代码稳定后把整个rootfs目录用mksquashfs打成镜像再切换到本地只读启动。开发期和量产期的分工很清晰NFS管效率Squashfs管稳定。5.5 分区规划的一个参考一个4GB eMMC的简单设备我会大致这样分分区大小文件系统内容boot128MBvfat或raw内核、设备树、bootloaderrootfs1GBsquashfs只读根文件系统单个镜像约200-300MBdata剩余ext4或UBIFS用户数据、日志、overlay upper层rootfs分区预留1GB不是因为它需要那么大而是为了容纳未来几次OTA升级里可能变大的新版镜像。只读镜像可以整体校验、替换分区分大一点升级时就不用动分区表了。6. 挂载失败、读取报错、压缩率不对一份浓缩版排查链路6.1 内核根本不认识squashfs最常见的报错是mount: unknown filesystem type squashfs这一步先不要怀疑镜像先检查内核cat /proc/filesystems | grep squashfs如果没有任何输出说明内核没编译Squashfs支持或者模块没加载。试一下sudo modprobe squashfs如果模块存在加载后/proc/filesystems里就会出现squashfs。如果没有模块只能重新编译内核把CONFIG_SQUASHFS和相关解压器配置打开。很多裁剪过的内核会为了省空间把Squashfs整个去掉我遇到过不止一次。6.2 superblock坏掉先别急着重新制作挂载时报错mount: /mnt: wrong fs type, bad option, bad superblock先排除最基础的问题镜像文件是否完整。用unsquashfs -s读一下unsquashfs -s rootfs.sfs如果这个命令能正确打印superblock信息说明镜像结构基本完好问题多半出在挂载方式上。比如镜像前面被加了一个头部常见于路由器固件固件整体是squashfs加上一个引导头或者配置头。这时候要用offset参数从偏移位置挂载sudo mount -t squashfs -o loop,ro,offset512 rootfs.bin /mnt/ro这个offset值可以从binwalk或者unsquashfs -s找到的偏移信息里确认。dmesg | tail经常会直接告诉你内核在哪里读到了异常别忽略它。6.3 挂载成功但读文件报I/O error这种情况有个典型规律用gzip压缩的镜像一切正常换用xz压缩后某个文件读着读着就报错。多半是内核只编译了CONFIG_SQUASHFS_GZIP没开CONFIG_SQUASHFS_XZ。镜像里算法字段和解压器不匹配内核在解压阶段直接失败。处理办法按需打开对应解压器配置项重新编译内核。验证也很简单先看superblock里记录的压缩算法再看/boot/config-$(uname -r)或者/proc/config.gz里有没有对应的CONFIG_SQUASHFS_XXX。还有一种可能性是镜像本身损坏或者被部分写入。用unsquashfs -ll能列出目录但读到一个坏块时报错这时应该重新制作镜像或者重新烧录而不是继续挂载。6.4 Windows下打不开Squashfs镜像Windows不是Squashfs的天然主场但也不是完全没法处理。7-Zip 21.0以上的版本可以解压大多数Squashfs镜像。如果你是做固件分析binwalk在Linux下搭配unsquashfs最顺手。我自己的建议Windows只用来应急查看制作和修改一律在Linux或者WSL里做。不要试图在Windows上改镜像再吐回给设备用工具链不完整很容易搞出兼容性问题。6.5 压缩率为什么和预期差很多第一你的源文件可能已经在压缩状态。JPEG、PNG、压缩包、视频素材这些内容再做Squashfs压缩也不会小多少属于正常现象。第二Squashfs默认会识别稀疏文件如果你打包了一个1GB的稀疏文件镜像实际占用的空间可能很小但看起来文件列表里它还是1GB这会让压缩率显得低。可以用du --apparent-size和du对比看出稀疏空洞。第三块大小越小压缩率通常越差。-b 64K比-b 256K压出来的镜像大这是代价。6.6 构建时间很长或者每次产物hash都变构建慢的常见原因是没有用-processorsmksquashfs默认只在单线程下工作。我见过有人压一个几百MB的rootfs跑了几十分钟加-processors 8之后直接缩到几分钟。产物hash每次都不同通常是时间戳在捣乱。镜像里的每个文件在构建时会被写入当前时间如果你不固定-mkfs-time和-all-time同一个rootfs目录打出来的镜像每次都不一样。这不影响运行但会影响固件版本追溯和CI缓存。解决方案就是把这两个参数固定成CI环境变量里的一个常量比如版本号对应的epoch时间。6.7 排查命令速查表现象第一个动作关键命令unknown filesystem type查内核支持cat /proc/filesystemsmodprobe squashfsbad superblock验证镜像本身unsquashfs -sbinwalk读取I/O error查dmesg和解压器dmesg构建慢看并行度-processors-mem每次hash都变看时间戳-mkfs-time-all-time我现在做固件习惯是把mksquashfs打包成一个固定脚本所有参数写死时间戳用版本号生成构建完自动跑一遍unsquashfs回读和sha256校验全部通过才允许发布。Squashfs这个老文件系统看着不起眼但它真的能帮你把系统镜像做得又小又稳。如果你正在被rootfs膨胀、升级断电变砖这些问题困扰我建议周末花半天时间把mksquashfs和overlay这套流程完整走一遍后面会省很多事。
返回列表