项目概述:从零开始理解 UBIFS 挂载嵌入式开发的朋友应该都有过这种经历:手里一块 NAND Flash 板子,交叉编译了个内核,烧完系统却发现文件系统挂不上,卡在“UBI device number 0 already exists”或者“ubiattach: error: no UBI device number for mtdX”这种报错上一整天。这篇文章就围绕在 Linux MTD 设备上完成 UBIFS 的挂载这件事情,把内核配置、分区规划、UBI 卷创建、最终 mount 的完整链路讲透,顺便把我在实际项目中踩过的坑一并列出来。UBIFS 全称是 Unsorted Block Image File System,专注于裸闪存(NAND 与 NOR)场景。传统在嵌入式上常用的文件系统无非是 JFFS2、YAFFS2,但 JFFS2 内存开销随着分区增大直线上升,YAFFS2 又没有严格的写平衡和事务保证。UBIFS 构建在 UBI 卷之上,能有效解决磨损均衡、坏块管理和掉电安全这些痛点。适合需要长时间运行、频繁写日志、掉电不丢数据的嵌入式产品,比如工业控制器、物联网网关、车载终端。对新手来说,这篇文章可以帮你把挂载过程背后的原理一步步理顺。1. 设计思路:为什么需要 UBI 这层“翻译官”1.1 从裸 MTD 到块设备的层次关系MTD(Memory Technology Device)是 Linux 内核里抽象裸闪存设备的一层,把 NAND/NOR 芯片的读写擦操作归一为对分区设备的操作,在/dev/下体现为/dev/mtdX和字符设备/dev/mtdXro。MTD 分区就像一块整盘被切成几段,常见做法是 bootloader 一个分区、内核一个分区、设备树分区一个、根文件系统一个。这些分区在逻辑上彼此独立,但底层对应同一个 Flash 芯片。问题在于 MTD 分区面向的是“裸设备”,它不管理坏块、不做擦写均衡,也没有逻辑卷的概念。如果应用直接在上面写数据,某块擦写次数过于集中,Flash 寿命就会很快耗光。UBI(Unsorted Block Images)就是在 MTD 之上引入的逻辑层,它对上提供连续的逻辑卷,对下管理物理擦除块(PEB),复杂点全被封装掉。UBI 与 MTD 之间的映射关系,一定程度上可以类比操作系统里的虚拟内存:MTD 的物理分区是物理内存,UBI 逻辑卷是进程看到的虚拟地址空间。不用每次读取都去操心底层物理擦除块是否坏掉、有没有偏移,只需要面向逻辑卷读写即可。1.2 UBIFS 的适用场景与选型对比既然有 JFFS2 和 YAFFS2,为什么还要折腾 UBIFS?直接对比三者的差异最清晰:对比项UBIFSJFFS2YAFFS2挂载速度快,不需要全盘扫描慢,挂在顺序遍历节点较慢,仍要遍历页写放大低,有日志顺序写高,垃圾回收比较重低,但缺少事务保护支持写平衡强,UBI 层统一管理弱弱掉电恢复好,有日志和事务一般一般内存开销约几 MB,与分区大小弱相关随分区大小线性增长,吃紧中等文件压缩内嵌 zlib/lzo 压缩无无一般在以下场景我会优先选用 UBIFS:NAND 容量偏大(超过 512MB)、设备需要频繁追加日志或数据库文件、系统异常掉电后不希望文件系统被劫持。如果 Flash 是 NOR 或容量很小,JFFS2 的简单性反而更合适,这个要按项目实际情况来定,没必要盲目追赶新东西。从数据流层面看,UBIFS 挂载的主链路是:Flash 芯片 → MTD 字符设备 → UBI 逻辑卷 → UBIFS 文件系统 →/dev/ubiX_Y。每一层解决的问题都不同,少了任何一环,挂载都无法完成。2. 内核配置与工具链准备2.1 内核必须打开的开关要在设备上启用 UBIFS,内核编译时需确保打开以下选项:CONFIG_MTD、CONFIG_MTD_UBI、CONFIG_UBIFS_FS,以及与 NAND 控制驱动相关的 CONFIG_MTD_NAND。如果板子的 Flash 是 SPI NOR,则需要 CONFIG_MTD_SPI_NOR 等相应驱动。很多朋友在配置阶段就把 CONFIG_UBIFS_FS 打成模块,但文件系统模块反复加载卸载其实非常容易踩坑,尤其内核自动设备节点编号错乱时,模块加载顺序会带来额外不确定性。我建议在商品化设备的内核中把这三项全部编译进内核而不是模块。另外要注意 CONFIG_MTD_CMDLINE_PARTS 或 CONFIG_MTD_OF_PARTS。前者支持通过内核 cmdline 里的mtdparts=参数切分分区,后者是从设备树里的partitions节点读分区表。两者至少打开一个,否则内核无法知道 Flash 分区怎么划,MTD 设备列表是空的,UBI 也就无从谈起。2.2 mtd-utils 工具集的准备宿主机上需要准备 mtd-utils 工具包,里面包含了我们后面要用的ubiformat、ubiattach、ubimkvol、ubinfo、mount.ubifs等命令。在 Ubuntu/Debian 环境可以直接用 apt 安装 mtd-utils,但嵌入式根文件系统要使用交叉编译版本,需要根据目标系统的 libc 版本去编译。我自己常用交叉编译命令大致是:./configure --host=arm-linux-gnueabihf --prefix=$PWD/_install make -j8 make install编译时可以顺手加入 LZO 压缩支持,因为内核配置了 CONFIG_UBIFS_FS_LZO 之后,UBIFS 挂载默认会尝试 lzo 压缩,根文件系统里缺少相应库会导致压缩策略不达预期,虽然不至于挂载失败,但可能影响后续写入性能。2.3 mtdparts 分区规则分区规则决定了 UBIFS 挂在哪一片物理区域。以 512MB NAND 为例,常见的分区划分方式如下(在 U-Boot 环境变量或内核 cmdline 中体现):mtdparts=nand0:1m(u-boot),8m(kernel),1m(dtb),500M(rootfs),-(data)分区之后,/dev/mtd0是 u-boot,/dev/mtd1是 kernel,/dev/mtd2是 dtb,/dev/mtd3是 rootfs,/dev/mtd4是 data。挂载 UBIFS 前,需要确定要规划哪个分区。如果 rootfs 用的就是 UBIFS,UBI 卷名就是rootfs;如果 data 分区也做成 UBI 卷,那卷名就是data。分区大小要注意对齐。NAND 的擦除块(PEB)大小通常为 128KB 或 256KB,分区结尾最好对齐到擦除块边界,避免出现无法完整映射的残块。有一部分板子在分区规划时为了图省事,直接把 GB 级剩余空间全划给 data 分区,结果 UBI 初始化时报size is not multiple of min I/O unit size——这就是没对齐的后果。3. 实操过程:从 UBIFS 到裸盘一步到位3.1 流程总览:四步完成挂载在一个还没有任何数据的空 MTD 分区上挂载 UBIFS,完整流程按顺序是:格式化 MTD 分区为 UBI 格式(ubiformat)、将 MTD 分区绑定到 UBI(ubiattach)、创建 UBI 逻辑卷(ubimkvol)、挂载卷(mount)。这四个步骤缺一不可,且顺序严格。这个流程我第一次跑的时候有点绕,关键点是理解了ubiformat和ubiattach的区别:ubiformat是在裸分区上写入 UBI 的元数据,相当于把磁盘分区做成某个文件系统前的“格式化”,它直接操作/dev/mtdX;ubiattach则是让内核 UBI 驱动接管这个分区,初始化和扫描其中已有的卷结构,在/dev/下生成ubiX设备。如果分区已经格式化过,可以省略ubiformat,但通常建议重做一遍保证干净。3.2 分步操作与参数计算以/dev/mtd4作为 data 分区为例,目标大小约 256MB。第一步是擦除并格式化:ubiformat /dev/mtd4 -s 2048 -e 128KiB -y-s指定页大小(page size),NAND 常见为 2048 字节或 4096 字节,具体需要看芯片 datasheet,错误填写的后果是 UBI 初始化报错或者格式化成功后读写校验失败。-e指定擦除块大小,若 NAND 的块大小为 128KB,就填128KiB,注意去掉空格否则工具可能识别不了。-y是跳过确认提示,脚本里必须加,否则自动化流程会卡住。第二步是 attach:ubiattach /dev/ubi_ctrl -m 4-m 4表示把/dev/mtd4绑定到 UBI。执行完成后,内核会创建/dev/ubi0,如果 attach 多个分区,设备号依次递增。可以通过ubinfo /dev/ubi0查看卷信息、空闲空间、PEB 数量等。第三步是创建逻辑卷:ubimkvol /dev/ubi0 -N data -s 248MiB-N data是卷名,挂载时会用到。-s指定逻辑卷大小。有一个常见误区是我检查后发现很多人以为逻辑卷大小必须等于整个分区,实际不是——UBI 可以在一个 MTD 分区里划多个卷,每个卷对应不同用途。比如把同一个 500MB 分区划分为rootfs和tmp两个卷,rootfs250MB,tmp250MB。卷大小不一定和分区一致,建议留一些余量给 UBI 自身管理,比如 256MB 的实际可用,卷设到 248MiB 左右比较稳妥,后面解释原因。第四步是挂载:mount -t ubifs ubi0:data /mnt/dataubifs 挂载语法是ubiX:卷名,不是设备节点。系统会自动从/dev/ubi0里找名为data的卷。如果你在只想挂载某一个卷的前提下,网络上有教程用/dev/ubi0_0这种节点匹配也很常见,但多卷环境下用名字最直观、最不易错。3.3 校验挂载结果挂载完成后,可以用df -h /mnt/data查看空间占用,也可以用ubinfo再次确认卷状态。我在现场调试时还习惯用cat /proc/mtd确认分区列表,结合dmesg | grep ubi看内核打印。如果 mount 命令成功,日志中通常会出现类似UBI: attached mtd4 (name "data", size ...)、UBIFS: mounted UBI device 0, volume 0的内容。看到这些信息,说明链路已经通了一半。一个容易忽略的点是mount命令依赖/sbin/mount.ubifs或者内核的 mount 工具支持,部分精简 rootfs 没有 mount.ubifs 帮助程序,就需要在根文件系统里补上 mtd-utils 的挂载助手,否则会报unknown filesystem type 'ubifs'。4. U-Boot 环境与根文件系统自启动配置4.1 U-Boot 下的 UBI 与 UBIFS 操作对于需要从 NAND 启动的系统,U-Boot 里也需要支持 UBI/UBIFS,才能去读取内核镜像和 dtb。U-Boot 相关配置项是 CONFIG_CMD_UBI、CONFIG_CMD_UBIFS、CONFIG_CMD_MTDPARTS,选上后编译进 U-Boot。启动阶段的操作顺序一般是:setenv bootcmd 'ubi part rootfs; ubifsmount ubi0:rootfs; ubifsload 0x82000000 /boot/uImage; ubifsload 0x83000000 /boot/dtbs.dtb; bootm 0x82000000 - 0x83000000' saveenv这里ubi part rootfs相当于在内核接管之前先让 U-Boot 把rootfs分区扫描成 UBI 设备,ubifsmount ubi0:rootfs挂载 UBIFS 卷,ubifsload则把内核文件读到内存指定位置。这个流程与用户态 mtd-utils 的基本逻辑一致,只是运行环境在 U-Boot,没有 Linux 的进程和文件系统缓存机制。实际使用中我遇到过 U-Boot 的 UBI 版本落后于内核版本,导致 U-Boot 里创建好的卷在内核侧识别异常。处理方法很简单:优先保持 U-Boot 和内核都采用 Linux 主线同一代 UBI 实现,或者不要跨太多个小版本,尤其是那些旧版 UBI 不支持fastmap而新版开启CONFIG_UBI_FASTMAP的情况。建议内核和 U-Boot 都开启 fastmap,且保持配置一致,能显著减少启动时 UBI 扫描时间。4.2 内核 cmdline 与根文件系统挂载策略内核正式接管 MTD 分区后,如何让根文件系统自动挂载 UBIFS?传统做法是用root=参数指定挂载类型和设备。对于 UBIFS,一种写法是:ubi.mtd=3 ubi.attach_mode=ubi root=ubi0:rootfs rootfstype=ubifsubi.mtd=3表示内核在启动早期自动 attach/dev/mtd3(即 rootfs 分区),root=ubi0:rootfs指定根卷,rootfstype=ubifs明确文件系统类型。ubi.attach_mode=ubi是告诉内核以 UBI 模式 attach,而不是把它当普通块设备。某些平台还可以用rootfstype=ubifs ubi.block=0,rootfs,其含义是让 UBI 在启动时创建逻辑卷,两种方式在主线内核上都能用,按平台习惯选。如果根文件系统不是 UBIFS,而是普通 ext4 或 squashfs,那么 data 分区作为 UBIFS 挂载就不需要通过 cmdline 特殊配置,直接在 rootfs 里的启动脚本执行 mount 命令即可。实际工业产品上常见组合是 squashfs 根文件系统 + 一个独立的 UBIFS 数据卷,把系统只读保护和数据读写分开,这种架构容错率更高。4.3 initramfs 与 switch_root 场景使用 initramfs 启动时,挂载 UBIFS 的时机往往在 init 脚本里。initramfs 里没有 mtd-utils,需要通过nandwrite、ubiattach的方式由 kernel 内建命令完成。比较常见的流程是在 init 脚本里调用:ubiattach /dev/ubi_ctrl -m 3 mkdir -p /newroot mount -t ubifs ubi0:rootfs /newroot exec switch_root /newroot /sbin/init这里要注意ubiattach在 initramfs 里的路径以及静态编译问题。initramfs 本身是一个小的 busybox 环境,很多命令是外部链接的。建议将 ubiattach、mount 等命令静态编译进入 busybox 或作为独立二进制放入 initramfs,否则在 switch_root 之后才发现命令找不到,只能干瞪眼。5. 常见问题与排查技巧实录5.1 挂载报错汇编与根因分析我把实际项目里遇到的典型问题按表现整理成表格,方便按图索骥:报错信息核心原因排查方向ubiattach: error: no UBI device number for mtdXMTD 分区不存在或 attach 顺序错乱/proc/mtd检查分区号,dmesg看 UBI 打印UBI error: ubi_attach_mtd_dev: UBI device 0 already exists同一分区被重复 attachubinfo -d查看现有 UBI 设备,先 detachmount: unknown filesystem type 'ubifs'内核未打开 UBIFS 支持,或缺少 mount.ubifs查看/proc/filesystems,补内核配置UBIFS error: ubifs_recover_leb: LEB ... failed之前掉电导致卷不一致,或坏块处理未完成尝试ubifs --force重新挂载,必要时 backup 后重新格式化NAND read from ... failed物理坏块或 ECC 校验失败确认 NAND 驱动配置,检查擦除块是否标记坏块size is not multiple of min I/O unit size分区未对齐到页/块大小检查 mtdparts 分区边界,做出调整后重建分区报错already exists是我在开发阶段遇到最多的,原因是开发板反复烧写后 UBI 设备残留,需要先 detach 再 attach。每次重烧后养成良好习惯,先执行一下 detach:ubidetach -m 4除非系统是从干净状态启动的,否则这个操作很必要。5.2 挂载后读写异常与掉电恢复有的板子挂载成功但出现写入断电后文件仍为 0 字节的怪问题。此类问题多半不是 UBIFS 本身,而是 NAND 驱动里 ECC 强度配置不够,写入数据校验失败导致 FS 层认为落盘数据异常。排查时可以先用nandtest工具对裸分区做读写压力测试,确认底层是否稳定,再回到 FS 层看。不要一上来就格式化,容易把现场数据搞没。UBIFS 有较强的掉电恢复能力,但除非配置了ubifs.stat或sync及时刷盘,否则文件在内存缓存中是常态。应用层要求高可靠时,写文件后必须调用fsync或syncfs。部分项目用日志型数据库,数据库自带刷盘,但普通配置下只靠fwrite后在顺序写期间断电,文件内容和目录项都可能不一致,我在项目中吃过这个亏,后来统一在写入完成后加fsync。5.3 坏块管理与卷空间规划经验UBI 在处理坏块时自动标记并跳过,因此坏块不太容易导致挂载失败,但会在容量计算上体现。假如你的 MTD 分区有 1024 个 PEB,其中有 8 个物理坏块,UBI 自己还要保留几个 PEB 用于原子更新和磨损均衡,那么实际可用卷空间会少于理论值。这就是为什么前面建议卷大小不要顶满分区。像 256M 的分区实际建 248MiB 的卷是相当稳的,如果顶满,坏块再多一点,创建卷时可能直接报not enough PEBs。另外 UBI 卷支持动态增长,创建时用小容量,后期使用ubimkvol的--maxavail功能扩展。还可以用ubirmvol删除卷后重建,但里面的数据当然就没了。生产环境扩容前先评估后再动手,命令行没有后悔药。5.4 fastmap 开启与启动时间优化老版本 UBI 在 detach 后重新 attach 时需要全盘扫描所有 PEB,大容量 NAND 在启动时可能多花数秒。主线 UBI 引入了 fastmap,会在 AVP 空间保存一份快速索引,减少扫描范围。内核启动参数或设备树里可以开启CONFIG_UBI_FASTMAP,缺点是每次更新 UBI 卷结构时损坏 fastmap 会导致重建,不过这在普通产品上摊销后的收益还是正的。如果发现 U-Boot 使能了 fastmap 而内核没使能,或者相反,尽量统一。因为两边使用的 UBI 版本和配置不一致,容易导致 attach 时出现 “fastmap not found” 的告警或者卷内部数据结构不一致的奇怪问题。这个细节不处理,上线后偶发启动失败,排查起来非常痛苦。6. 挂载之外的扩展经验6.1 镜像制作与批量产线烧录UBIFS 卷制作必须使用mkfs.ubifs而不是mkfs.ext4之类的工具。制作命令大致是:mkfs.ubifs -r rootfs_dir -m 2048 -e 129024 -c 2047 -o rootfs.ubifs-m是页大小,-e是逻辑擦除块大小(LEB size),通常为 PEB 大小减去 UBI 头开销,-c是卷包含的最大 LEB 数量。这三个参数必须与内核实际 attach 时读到的参数一致,否则会在挂载时报UBIFS error: bad superblock之类的严重错误。-e和-c的计算方式是很多项目新人比较容易出错的地方,其实通过ubinfo可以看到当前 LEB size,直接对齐那个数字就好。产线烧录时,一般先ubiformat,再ubiattach,然后ubimkvol,最后把rootfs.ubifs用ubifsload或nandwrite写入卷。更快的办法是直接使用ubinize工具一次性把 UBI 镜像写进 MTD 分区,然后烧写时就省去多次交互。适合批量生产,效率高很多。6.2 性能调优参数UBIFS 挂载时可以加noatime避免每次访问都更新 atime,这能减少很多不必要的写操作。对大多数嵌入式应用,我建议默认加:mount -t ubifs ubi0:data /mnt/data -o noatime如果日志写入频繁,可以设置commit=5让 UBIFS 每 5 秒自动提交一次事务,默认是 5 秒,过短的 commit 周期会导致写放大。对于数据库类应用,还要合理使用bulk_read和chk_data_crc参数,前者顺序读大文件有效,后者能校验数据完整性但会降低性能,按需求取舍。压缩策略方面,UBIFS 默认选用 lzo,如果你的内核使能了 zlib,也可以改成 zlib 获得更高压缩率,但 CPU 占用相应上升。实际项目中,我用 lzo 在大多数场合都够了,嵌入式 CPU 性能有限,压缩不是主要收益点时就不必折腾。6.3 生产环境监控与预防维修运行过程中,建议周期性检查 UBI 设备的坏块数量变化。命令是:ubinfo -d /dev/ubi0关注Bad PEBs这一项。当然产品在生命周期内坏块会逐渐出现,这是 NAND Flash 的正常特性,但如果坏块增长速度过快,说明底层 NAND 可能过热、供电不稳或者 ECC 配置不合规。早发现可以早更换硬件,避免恶性掉电导致系统崩溃后才排查。设备上电后还可以通过/sys/class/ubi/ubi0/bad_peb_count快速读取坏块数,适合写监控脚本。在产品运行期间,坏块数超过 Flash 总量 2% 就应列入预警,超过 5% 就要准备返修计划了。这个数字不是绝对阈值,只是一个参考,不同厂商不同制程差别很大。6.4 与设备树和分区机制的配合很多平台现在用设备树来定分区表。设备树里写:partition@4 { label = "data"; reg = <0x4000000 0x10000000>; };UBI 侧通过label匹配分区名。要注意设备树里的reg起始地址和长度单位通常是字节,需要对擦除块大小对齐。写错地址或者长度不规范,轻则分区无法创建,重则覆盖到其他分区,直接把内核搞挂。我在一个项目里见过 device tree 的 reg 长度少写一个 0,导致 UBI attach 成功后卷大小只剩十分之一,应用写日志没一会儿分区就满了,排查了两天才发现是这种低级错误。对于同时存在 SPI NOR 和 NAND 的板子,要区分 MTD 设备号谁是mtd0谁是mtd1,设备树里 SPI NOR 和 NAND 的子节点顺序不同会直接影响编号。最好的办法是启动后立即查看/proc/mtd,确认名字和分区归属,再让 U-Boot 的 bootargs 和 rootfs 的镜像烧录脚本都基于这个编号来写。结语:我的实操体会玩了几年 MTD 和 UBIFS,最大的体会是:这东西本身不复杂,复杂的是底层 NAND 的行为差异和层层封装带来的排查链路。从 Flash 的页大小、擦除块大小,到 UBI 的 LEB 开销、fastmap 配置,再到 UBIFS 的压缩、挂载参数,每一层如果没对齐,都会在前面提到的那一串报错里露出一角。建议新手朋友第一次做时,别急着追求快速跑通,要学会用dmesg把从NAND device到UBI: attached再到UBIFS: mounted每一行输出读一遍,这个过程积累起来的能力,比直接照抄命令要值钱得多。最后再分享一个小技巧:调试阶段可以把ubiformat、ubiattach、ubimkvol和mount写成一个带set -e的 shell 脚本,每一步失败即退出,再配合日志重定向,定位问题会快很多。等量产阶段,再把脚本简化成一次ubinize生成镜像、一次烧写。照这个思路走,UBIFS 挂载就不再是什么玄学,而是一个纯粹的工程细节。
项目概述:从零开始理解 UBIFS 挂载嵌入式开发的朋友应该都有过这种经历:手里一块 NAND Flash 板子,交叉编译了个内核,烧完系统却发现文件系统挂不上,卡在“UBI device number 0 already exists”或者“ubiattach: error: no UBI device number for mtdX”这种报错上一整天。这篇文章就围绕在 Linux MTD 设备上完成 UBIFS 的挂载这件事情,把内核配置、分区规划、UBI 卷创建、最终 mount 的完整链路讲透,顺便把我在实际项目中踩过的坑一并列出来。UBIFS 全称是 Unsorted Block Image File System,专注于裸闪存(NAND 与 NOR)场景。传统在嵌入式上常用的文件系统无非是 JFFS2、YAFFS2,但 JFFS2 内存开销随着分区增大直线上升,YAFFS2 又没有严格的写平衡和事务保证。UBIFS 构建在 UBI 卷之上,能有效解决磨损均衡、坏块管理和掉电安全这些痛点。适合需要长时间运行、频繁写日志、掉电不丢数据的嵌入式产品,比如工业控制器、物联网网关、车载终端。对新手来说,这篇文章可以帮你把挂载过程背后的原理一步步理顺。1. 设计思路:为什么需要 UBI 这层“翻译官”1.1 从裸 MTD 到块设备的层次关系MTD(Memory Technology Device)是 Linux 内核里抽象裸闪存设备的一层,把 NAND/NOR 芯片的读写擦操作归一为对分区设备的操作,在/dev/下体现为/dev/mtdX和字符设备/dev/mtdXro。MTD 分区就像一块整盘被切成几段,常见做法是 bootloader 一个分区、内核一个分区、设备树分区一个、根文件系统一个。这些分区在逻辑上彼此独立,但底层对应同一个 Flash 芯片。问题在于 MTD 分区面向的是“裸设备”,它不管理坏块、不做擦写均衡,也没有逻辑卷的概念。如果应用直接在上面写数据,某块擦写次数过于集中,Flash 寿命就会很快耗光。UBI(Unsorted Block Images)就是在 MTD 之上引入的逻辑层,它对上提供连续的逻辑卷,对下管理物理擦除块(PEB),复杂点全被封装掉。UBI 与 MTD 之间的映射关系,一定程度上可以类比操作系统里的虚拟内存:MTD 的物理分区是物理内存,UBI 逻辑卷是进程看到的虚拟地址空间。不用每次读取都去操心底层物理擦除块是否坏掉、有没有偏移,只需要面向逻辑卷读写即可。1.2 UBIFS 的适用场景与选型对比既然有 JFFS2 和 YAFFS2,为什么还要折腾 UBIFS?直接对比三者的差异最清晰:对比项UBIFSJFFS2YAFFS2挂载速度快,不需要全盘扫描慢,挂在顺序遍历节点较慢,仍要遍历页写放大低,有日志顺序写高,垃圾回收比较重低,但缺少事务保护支持写平衡强,UBI 层统一管理弱弱掉电恢复好,有日志和事务一般一般内存开销约几 MB,与分区大小弱相关随分区大小线性增长,吃紧中等文件压缩内嵌 zlib/lzo 压缩无无一般在以下场景我会优先选用 UBIFS:NAND 容量偏大(超过 512MB)、设备需要频繁追加日志或数据库文件、系统异常掉电后不希望文件系统被劫持。如果 Flash 是 NOR 或容量很小,JFFS2 的简单性反而更合适,这个要按项目实际情况来定,没必要盲目追赶新东西。从数据流层面看,UBIFS 挂载的主链路是:Flash 芯片 → MTD 字符设备 → UBI 逻辑卷 → UBIFS 文件系统 →/dev/ubiX_Y。每一层解决的问题都不同,少了任何一环,挂载都无法完成。2. 内核配置与工具链准备2.1 内核必须打开的开关要在设备上启用 UBIFS,内核编译时需确保打开以下选项:CONFIG_MTD、CONFIG_MTD_UBI、CONFIG_UBIFS_FS,以及与 NAND 控制驱动相关的 CONFIG_MTD_NAND。如果板子的 Flash 是 SPI NOR,则需要 CONFIG_MTD_SPI_NOR 等相应驱动。很多朋友在配置阶段就把 CONFIG_UBIFS_FS 打成模块,但文件系统模块反复加载卸载其实非常容易踩坑,尤其内核自动设备节点编号错乱时,模块加载顺序会带来额外不确定性。我建议在商品化设备的内核中把这三项全部编译进内核而不是模块。另外要注意 CONFIG_MTD_CMDLINE_PARTS 或 CONFIG_MTD_OF_PARTS。前者支持通过内核 cmdline 里的mtdparts=参数切分分区,后者是从设备树里的partitions节点读分区表。两者至少打开一个,否则内核无法知道 Flash 分区怎么划,MTD 设备列表是空的,UBI 也就无从谈起。2.2 mtd-utils 工具集的准备宿主机上需要准备 mtd-utils 工具包,里面包含了我们后面要用的ubiformat、ubiattach、ubimkvol、ubinfo、mount.ubifs等命令。在 Ubuntu/Debian 环境可以直接用 apt 安装 mtd-utils,但嵌入式根文件系统要使用交叉编译版本,需要根据目标系统的 libc 版本去编译。我自己常用交叉编译命令大致是:./configure --host=arm-linux-gnueabihf --prefix=$PWD/_install make -j8 make install编译时可以顺手加入 LZO 压缩支持,因为内核配置了 CONFIG_UBIFS_FS_LZO 之后,UBIFS 挂载默认会尝试 lzo 压缩,根文件系统里缺少相应库会导致压缩策略不达预期,虽然不至于挂载失败,但可能影响后续写入性能。2.3 mtdparts 分区规则分区规则决定了 UBIFS 挂在哪一片物理区域。以 512MB NAND 为例,常见的分区划分方式如下(在 U-Boot 环境变量或内核 cmdline 中体现):mtdparts=nand0:1m(u-boot),8m(kernel),1m(dtb),500M(rootfs),-(data)分区之后,/dev/mtd0是 u-boot,/dev/mtd1是 kernel,/dev/mtd2是 dtb,/dev/mtd3是 rootfs,/dev/mtd4是 data。挂载 UBIFS 前,需要确定要规划哪个分区。如果 rootfs 用的就是 UBIFS,UBI 卷名就是rootfs;如果 data 分区也做成 UBI 卷,那卷名就是data。分区大小要注意对齐。NAND 的擦除块(PEB)大小通常为 128KB 或 256KB,分区结尾最好对齐到擦除块边界,避免出现无法完整映射的残块。有一部分板子在分区规划时为了图省事,直接把 GB 级剩余空间全划给 data 分区,结果 UBI 初始化时报size is not multiple of min I/O unit size——这就是没对齐的后果。3. 实操过程:从 UBIFS 到裸盘一步到位3.1 流程总览:四步完成挂载在一个还没有任何数据的空 MTD 分区上挂载 UBIFS,完整流程按顺序是:格式化 MTD 分区为 UBI 格式(ubiformat)、将 MTD 分区绑定到 UBI(ubiattach)、创建 UBI 逻辑卷(ubimkvol)、挂载卷(mount)。这四个步骤缺一不可,且顺序严格。这个流程我第一次跑的时候有点绕,关键点是理解了ubiformat和ubiattach的区别:ubiformat是在裸分区上写入 UBI 的元数据,相当于把磁盘分区做成某个文件系统前的“格式化”,它直接操作/dev/mtdX;ubiattach则是让内核 UBI 驱动接管这个分区,初始化和扫描其中已有的卷结构,在/dev/下生成ubiX设备。如果分区已经格式化过,可以省略ubiformat,但通常建议重做一遍保证干净。3.2 分步操作与参数计算以/dev/mtd4作为 data 分区为例,目标大小约 256MB。第一步是擦除并格式化:ubiformat /dev/mtd4 -s 2048 -e 128KiB -y-s指定页大小(page size),NAND 常见为 2048 字节或 4096 字节,具体需要看芯片 datasheet,错误填写的后果是 UBI 初始化报错或者格式化成功后读写校验失败。-e指定擦除块大小,若 NAND 的块大小为 128KB,就填128KiB,注意去掉空格否则工具可能识别不了。-y是跳过确认提示,脚本里必须加,否则自动化流程会卡住。第二步是 attach:ubiattach /dev/ubi_ctrl -m 4-m 4表示把/dev/mtd4绑定到 UBI。执行完成后,内核会创建/dev/ubi0,如果 attach 多个分区,设备号依次递增。可以通过ubinfo /dev/ubi0查看卷信息、空闲空间、PEB 数量等。第三步是创建逻辑卷:ubimkvol /dev/ubi0 -N data -s 248MiB-N data是卷名,挂载时会用到。-s指定逻辑卷大小。有一个常见误区是我检查后发现很多人以为逻辑卷大小必须等于整个分区,实际不是——UBI 可以在一个 MTD 分区里划多个卷,每个卷对应不同用途。比如把同一个 500MB 分区划分为rootfs和tmp两个卷,rootfs250MB,tmp250MB。卷大小不一定和分区一致,建议留一些余量给 UBI 自身管理,比如 256MB 的实际可用,卷设到 248MiB 左右比较稳妥,后面解释原因。第四步是挂载:mount -t ubifs ubi0:data /mnt/dataubifs 挂载语法是ubiX:卷名,不是设备节点。系统会自动从/dev/ubi0里找名为data的卷。如果你在只想挂载某一个卷的前提下,网络上有教程用/dev/ubi0_0这种节点匹配也很常见,但多卷环境下用名字最直观、最不易错。3.3 校验挂载结果挂载完成后,可以用df -h /mnt/data查看空间占用,也可以用ubinfo再次确认卷状态。我在现场调试时还习惯用cat /proc/mtd确认分区列表,结合dmesg | grep ubi看内核打印。如果 mount 命令成功,日志中通常会出现类似UBI: attached mtd4 (name "data", size ...)、UBIFS: mounted UBI device 0, volume 0的内容。看到这些信息,说明链路已经通了一半。一个容易忽略的点是mount命令依赖/sbin/mount.ubifs或者内核的 mount 工具支持,部分精简 rootfs 没有 mount.ubifs 帮助程序,就需要在根文件系统里补上 mtd-utils 的挂载助手,否则会报unknown filesystem type 'ubifs'。4. U-Boot 环境与根文件系统自启动配置4.1 U-Boot 下的 UBI 与 UBIFS 操作对于需要从 NAND 启动的系统,U-Boot 里也需要支持 UBI/UBIFS,才能去读取内核镜像和 dtb。U-Boot 相关配置项是 CONFIG_CMD_UBI、CONFIG_CMD_UBIFS、CONFIG_CMD_MTDPARTS,选上后编译进 U-Boot。启动阶段的操作顺序一般是:setenv bootcmd 'ubi part rootfs; ubifsmount ubi0:rootfs; ubifsload 0x82000000 /boot/uImage; ubifsload 0x83000000 /boot/dtbs.dtb; bootm 0x82000000 - 0x83000000' saveenv这里ubi part rootfs相当于在内核接管之前先让 U-Boot 把rootfs分区扫描成 UBI 设备,ubifsmount ubi0:rootfs挂载 UBIFS 卷,ubifsload则把内核文件读到内存指定位置。这个流程与用户态 mtd-utils 的基本逻辑一致,只是运行环境在 U-Boot,没有 Linux 的进程和文件系统缓存机制。实际使用中我遇到过 U-Boot 的 UBI 版本落后于内核版本,导致 U-Boot 里创建好的卷在内核侧识别异常。处理方法很简单:优先保持 U-Boot 和内核都采用 Linux 主线同一代 UBI 实现,或者不要跨太多个小版本,尤其是那些旧版 UBI 不支持fastmap而新版开启CONFIG_UBI_FASTMAP的情况。建议内核和 U-Boot 都开启 fastmap,且保持配置一致,能显著减少启动时 UBI 扫描时间。4.2 内核 cmdline 与根文件系统挂载策略内核正式接管 MTD 分区后,如何让根文件系统自动挂载 UBIFS?传统做法是用root=参数指定挂载类型和设备。对于 UBIFS,一种写法是:ubi.mtd=3 ubi.attach_mode=ubi root=ubi0:rootfs rootfstype=ubifsubi.mtd=3表示内核在启动早期自动 attach/dev/mtd3(即 rootfs 分区),root=ubi0:rootfs指定根卷,rootfstype=ubifs明确文件系统类型。ubi.attach_mode=ubi是告诉内核以 UBI 模式 attach,而不是把它当普通块设备。某些平台还可以用rootfstype=ubifs ubi.block=0,rootfs,其含义是让 UBI 在启动时创建逻辑卷,两种方式在主线内核上都能用,按平台习惯选。如果根文件系统不是 UBIFS,而是普通 ext4 或 squashfs,那么 data 分区作为 UBIFS 挂载就不需要通过 cmdline 特殊配置,直接在 rootfs 里的启动脚本执行 mount 命令即可。实际工业产品上常见组合是 squashfs 根文件系统 + 一个独立的 UBIFS 数据卷,把系统只读保护和数据读写分开,这种架构容错率更高。4.3 initramfs 与 switch_root 场景使用 initramfs 启动时,挂载 UBIFS 的时机往往在 init 脚本里。initramfs 里没有 mtd-utils,需要通过nandwrite、ubiattach的方式由 kernel 内建命令完成。比较常见的流程是在 init 脚本里调用:ubiattach /dev/ubi_ctrl -m 3 mkdir -p /newroot mount -t ubifs ubi0:rootfs /newroot exec switch_root /newroot /sbin/init这里要注意ubiattach在 initramfs 里的路径以及静态编译问题。initramfs 本身是一个小的 busybox 环境,很多命令是外部链接的。建议将 ubiattach、mount 等命令静态编译进入 busybox 或作为独立二进制放入 initramfs,否则在 switch_root 之后才发现命令找不到,只能干瞪眼。5. 常见问题与排查技巧实录5.1 挂载报错汇编与根因分析我把实际项目里遇到的典型问题按表现整理成表格,方便按图索骥:报错信息核心原因排查方向ubiattach: error: no UBI device number for mtdXMTD 分区不存在或 attach 顺序错乱/proc/mtd检查分区号,dmesg看 UBI 打印UBI error: ubi_attach_mtd_dev: UBI device 0 already exists同一分区被重复 attachubinfo -d查看现有 UBI 设备,先 detachmount: unknown filesystem type 'ubifs'内核未打开 UBIFS 支持,或缺少 mount.ubifs查看/proc/filesystems,补内核配置UBIFS error: ubifs_recover_leb: LEB ... failed之前掉电导致卷不一致,或坏块处理未完成尝试ubifs --force重新挂载,必要时 backup 后重新格式化NAND read from ... failed物理坏块或 ECC 校验失败确认 NAND 驱动配置,检查擦除块是否标记坏块size is not multiple of min I/O unit size分区未对齐到页/块大小检查 mtdparts 分区边界,做出调整后重建分区报错already exists是我在开发阶段遇到最多的,原因是开发板反复烧写后 UBI 设备残留,需要先 detach 再 attach。每次重烧后养成良好习惯,先执行一下 detach:ubidetach -m 4除非系统是从干净状态启动的,否则这个操作很必要。5.2 挂载后读写异常与掉电恢复有的板子挂载成功但出现写入断电后文件仍为 0 字节的怪问题。此类问题多半不是 UBIFS 本身,而是 NAND 驱动里 ECC 强度配置不够,写入数据校验失败导致 FS 层认为落盘数据异常。排查时可以先用nandtest工具对裸分区做读写压力测试,确认底层是否稳定,再回到 FS 层看。不要一上来就格式化,容易把现场数据搞没。UBIFS 有较强的掉电恢复能力,但除非配置了ubifs.stat或sync及时刷盘,否则文件在内存缓存中是常态。应用层要求高可靠时,写文件后必须调用fsync或syncfs。部分项目用日志型数据库,数据库自带刷盘,但普通配置下只靠fwrite后在顺序写期间断电,文件内容和目录项都可能不一致,我在项目中吃过这个亏,后来统一在写入完成后加fsync。5.3 坏块管理与卷空间规划经验UBI 在处理坏块时自动标记并跳过,因此坏块不太容易导致挂载失败,但会在容量计算上体现。假如你的 MTD 分区有 1024 个 PEB,其中有 8 个物理坏块,UBI 自己还要保留几个 PEB 用于原子更新和磨损均衡,那么实际可用卷空间会少于理论值。这就是为什么前面建议卷大小不要顶满分区。像 256M 的分区实际建 248MiB 的卷是相当稳的,如果顶满,坏块再多一点,创建卷时可能直接报not enough PEBs。另外 UBI 卷支持动态增长,创建时用小容量,后期使用ubimkvol的--maxavail功能扩展。还可以用ubirmvol删除卷后重建,但里面的数据当然就没了。生产环境扩容前先评估后再动手,命令行没有后悔药。5.4 fastmap 开启与启动时间优化老版本 UBI 在 detach 后重新 attach 时需要全盘扫描所有 PEB,大容量 NAND 在启动时可能多花数秒。主线 UBI 引入了 fastmap,会在 AVP 空间保存一份快速索引,减少扫描范围。内核启动参数或设备树里可以开启CONFIG_UBI_FASTMAP,缺点是每次更新 UBI 卷结构时损坏 fastmap 会导致重建,不过这在普通产品上摊销后的收益还是正的。如果发现 U-Boot 使能了 fastmap 而内核没使能,或者相反,尽量统一。因为两边使用的 UBI 版本和配置不一致,容易导致 attach 时出现 “fastmap not found” 的告警或者卷内部数据结构不一致的奇怪问题。这个细节不处理,上线后偶发启动失败,排查起来非常痛苦。6. 挂载之外的扩展经验6.1 镜像制作与批量产线烧录UBIFS 卷制作必须使用mkfs.ubifs而不是mkfs.ext4之类的工具。制作命令大致是:mkfs.ubifs -r rootfs_dir -m 2048 -e 129024 -c 2047 -o rootfs.ubifs-m是页大小,-e是逻辑擦除块大小(LEB size),通常为 PEB 大小减去 UBI 头开销,-c是卷包含的最大 LEB 数量。这三个参数必须与内核实际 attach 时读到的参数一致,否则会在挂载时报UBIFS error: bad superblock之类的严重错误。-e和-c的计算方式是很多项目新人比较容易出错的地方,其实通过ubinfo可以看到当前 LEB size,直接对齐那个数字就好。产线烧录时,一般先ubiformat,再ubiattach,然后ubimkvol,最后把rootfs.ubifs用ubifsload或nandwrite写入卷。更快的办法是直接使用ubinize工具一次性把 UBI 镜像写进 MTD 分区,然后烧写时就省去多次交互。适合批量生产,效率高很多。6.2 性能调优参数UBIFS 挂载时可以加noatime避免每次访问都更新 atime,这能减少很多不必要的写操作。对大多数嵌入式应用,我建议默认加:mount -t ubifs ubi0:data /mnt/data -o noatime如果日志写入频繁,可以设置commit=5让 UBIFS 每 5 秒自动提交一次事务,默认是 5 秒,过短的 commit 周期会导致写放大。对于数据库类应用,还要合理使用bulk_read和chk_data_crc参数,前者顺序读大文件有效,后者能校验数据完整性但会降低性能,按需求取舍。压缩策略方面,UBIFS 默认选用 lzo,如果你的内核使能了 zlib,也可以改成 zlib 获得更高压缩率,但 CPU 占用相应上升。实际项目中,我用 lzo 在大多数场合都够了,嵌入式 CPU 性能有限,压缩不是主要收益点时就不必折腾。6.3 生产环境监控与预防维修运行过程中,建议周期性检查 UBI 设备的坏块数量变化。命令是:ubinfo -d /dev/ubi0关注Bad PEBs这一项。当然产品在生命周期内坏块会逐渐出现,这是 NAND Flash 的正常特性,但如果坏块增长速度过快,说明底层 NAND 可能过热、供电不稳或者 ECC 配置不合规。早发现可以早更换硬件,避免恶性掉电导致系统崩溃后才排查。设备上电后还可以通过/sys/class/ubi/ubi0/bad_peb_count快速读取坏块数,适合写监控脚本。在产品运行期间,坏块数超过 Flash 总量 2% 就应列入预警,超过 5% 就要准备返修计划了。这个数字不是绝对阈值,只是一个参考,不同厂商不同制程差别很大。6.4 与设备树和分区机制的配合很多平台现在用设备树来定分区表。设备树里写:partition@4 { label = "data"; reg = <0x4000000 0x10000000>; };UBI 侧通过label匹配分区名。要注意设备树里的reg起始地址和长度单位通常是字节,需要对擦除块大小对齐。写错地址或者长度不规范,轻则分区无法创建,重则覆盖到其他分区,直接把内核搞挂。我在一个项目里见过 device tree 的 reg 长度少写一个 0,导致 UBI attach 成功后卷大小只剩十分之一,应用写日志没一会儿分区就满了,排查了两天才发现是这种低级错误。对于同时存在 SPI NOR 和 NAND 的板子,要区分 MTD 设备号谁是mtd0谁是mtd1,设备树里 SPI NOR 和 NAND 的子节点顺序不同会直接影响编号。最好的办法是启动后立即查看/proc/mtd,确认名字和分区归属,再让 U-Boot 的 bootargs 和 rootfs 的镜像烧录脚本都基于这个编号来写。结语:我的实操体会玩了几年 MTD 和 UBIFS,最大的体会是:这东西本身不复杂,复杂的是底层 NAND 的行为差异和层层封装带来的排查链路。从 Flash 的页大小、擦除块大小,到 UBI 的 LEB 开销、fastmap 配置,再到 UBIFS 的压缩、挂载参数,每一层如果没对齐,都会在前面提到的那一串报错里露出一角。建议新手朋友第一次做时,别急着追求快速跑通,要学会用dmesg把从NAND device到UBI: attached再到UBIFS: mounted每一行输出读一遍,这个过程积累起来的能力,比直接照抄命令要值钱得多。最后再分享一个小技巧:调试阶段可以把ubiformat、ubiattach、ubimkvol和mount写成一个带set -e的 shell 脚本,每一步失败即退出,再配合日志重定向,定位问题会快很多。等量产阶段,再把脚本简化成一次ubinize生成镜像、一次烧写。照这个思路走,UBIFS 挂载就不再是什么玄学,而是一个纯粹的工程细节。