很多人第一次在嵌入式板子上把 NAND Flash 折腾明白,基本都是从挂载根文件系统开始的。我早期也在这个问题上卡过很久:明明按网上教程一步步敲,mount -t ubifs却总是报错,要么提示找不到设备节点,要么挂载成功后空间显示异常。后来才意识到,UBIFS 这套东西和之前在开发板上用的 ext4、JFFS2 完全不是一个玩法,它多了一层 UBI(Unsorted Block Image)管理层,而且和 MTD 设备节点的对应关系很容易把人绕晕。
这篇文章不打算从“什么是 NOR Flash”“什么是 NAND Flash”这种基础概念开始讲。我假设你已经接触过 MTD 设备,对/dev/mtd0、/dev/mtdblock0这类节点有一定印象,也知道 NAND Flash 为什么会有坏块和擦除寿命问题。这篇想解决的是更实际的问题:怎么把一个 MTD 分区变成 UBIFS 文件系统并成功挂载,整个过程中有哪些环节容易出错,以及为什么每个步骤要这样做。
1. 为什么 eMMC 上的那套逻辑在 NAND 上行不通,UBIFS 到底解决了什么
先说说最容易被忽略的底层逻辑。很多人挂载 UBIFS 失败,其实是因为根本没理解 UBI 层和 MTD 层之间的分工。要搞懂这个,得先回到 NAND Flash 本身的物理特性上来。
NAND Flash 和普通块设备(比如 eMMC、SD 卡)有个本质区别:它的读写基本单位是页(Page),但擦除单位是块(Block)。一块通常包含几十甚至上百页,而且块在擦除之前不能重复写入。更麻烦的是,NAND 出厂就有坏块,使用过程中坏块还会持续产生。另外,同一个物理位置反复擦写会磨损,寿命有限。
那为什么不直接把 NAND 当成块设备,像 eMMC 那样用 ext4 就完了?问题就出在坏块管理和掉电保护上。ext4 这类文件系统虽然也有一部分坏块处理能力,但它本身是建立在“底层设备保证读写可靠”的假设之上的。它会把数据写到某个扇区,底层块设备会自行做坏块重映射,上层根本感知不到。但 NAND 没有这种机制,文件系统如果直接写页,遇到坏块就只能报错,掉电时如果正好在改写某块的数据,整个块的数据都可能错乱。
所以 MTD 子系统提供了两道工序。一层是简单的坏块管理,另一层就是 UBI 的磨损均衡和逻辑卷管理。UBI 把物理擦除块(PEB,Physical Erase Block)抽象成逻辑擦除块(LEB,Logical Erase Block),文件系统不再直接面对闪存的物理限制,而是使用一种“靠谱的虚拟块设备”——UBI volume。
UBIFS 就是跑在这种 UBI volume 之上的日志型文件系统。它本身针对 Flash 做了大量优化,支持压缩、掉电恢复、磨损均衡感知,算是目前 NAND Flash 上主流的文件系统方案。和它经常被放一起比较的 JFFS2,在文件大了以后挂载速度和资源占用会变得很难看,UBIFS 在这个层面优势很明显。
不过代价就是,链路变长了。完整的挂载链路看起来是这个样子:
NAND Flash 物理分区 → MTD 设备 → UBI 层 → UBI volume → UBIFS每一步都有对应的工具和接口。我在一开始没理清这条链,直接跳过 UBI 层想挂 UBIFS,自然是不可能的。
1.1 UBI 卷和 MTD 分区的对应关系
再往细了说,设备树或命令行里那个 mtdblock 节点和 UBI 卷不是一回事。
MTD 设备节点是整个链路的起点。比如mtd3表示内核把 NAND 的某个物理分区暴露成了 MTD 设备,既有/dev/mtd3(用于ioctl操作和读写裸数据),也有/dev/mtdblock3(把 MTD 块设备模拟成块设备,方便某些场景直接操作)。
UBI 层附着到某个 MTD 设备上之后,不会自动产生 block 设备节点,而是生成一堆新的字符设备,在/dev/ubi_ctrl、/dev/ubi0、/dev/ubi0_0这种路径下。/dev/ubi0_0就是 UBI volume 0,它才是真正能被 UBIFS 挂载的东西。
所以很多人照着教程敲mount -t ubifs /dev/mtdblock3 /mnt,必然失败。UBIFS 的挂载对象是 UBI volume 节点,不是 MTD 节点。
1.2 UBIFS 和 JFFS2 的区别,以及你该怎么选
在继续之前,顺带说一句选型问题。虽然本文主题是 UBIFS,但实际项目里偶尔还是会遇到有人在 NOR Flash 上硬套 UBIFS,把系统搞得启动很慢。原因在于 UBIFS 用在 NOR 上可以,但 NOR 容量一般很小,UBIFS 的管理开销相对较多,此时 JFFS2 反而更轻量。
这里不是要分高下,只是给一个通用建议:小容量 NOR(通常 16MB 以下)、对挂载速度要求不高,JFFS2 还够用;NAND 且容量较大、追求压缩率和挂载速度,UBIFS 是更合理的选择。
2. 挂载前必须先理清的三件事:分区、擦除和格式化
准备工作往往比挂载命令本身更容易出错。我见过不少人在这一步就埋下了隐患,后面排查半天发现居然是分区和擦除阶段的问题。
2.1 分区到底在哪里定义,内核和 bootloader 要一致
MTD 分区可以来自内核命令行参数,也可以来自设备树里的partition@节点。关键要求是 bootloader 传参里给内核指定的 NAND 分区布局,要和内核实际认为的一致,否则文件系统写到错误偏移上,挂载自然失败。
有一段比较典型的设备树分区配置长这样:
&nand_controller { nand@0 { partitions { compatible = "fixed-partitions"; #address-cells = <1>; #size-cells = <1>; partition@0 { label = "uboot"; reg = <0x0000000 0x00100000>; }; partition@1 { label = "kernel"; reg = <0x00100000 0x00800000>; }; partition@2 { label = "rootfs"; reg = <0x00900000 0x08000000>; }; }; }; };这个例子很直观:rootfs 分区从偏移 0x00900000 开始,大小 128MB。你要挂载 UBIFS,就得保证 UBI 层附着的是这个 rootfs 这个分区对应的 MTD 设备。
提示:分区名的 label 和实际的
/dev/mtdX编号之间可能会受设备树节点顺序、驱动注册顺序影响,不要想当然认为“label rootfs 就是 /dev/mtd2”。用cat /proc/mtd确认一下最稳。
实际操作里,先在板子上执行:
cat /proc/mtd你会看到类似这样的输出:
dev: size erasesize name mtd0: 00100000 00020000 "uboot" mtd1: 00800000 00020000 "kernel" mtd2: 08000000 00020000 "rootfs"这里面 erasesize 很重要。很多 NAND 的块大小是 128KB 或 256KB(上面例子是 128KB = 0x20000),后续的 UBI 参数和 mkfs.ubifs 参数都要参考这个值。
2.2 为什么不能用 dd 直接格式化,要用 ubiformat
新建 UBIFS 文件系统和在 eMMC 上格式化类似,但它不是直接对 MTD 设备运行mkfs.ubifs。原因还是 UBI 层。
mkfs.ubifs的作用是把一个目录内容制作成一个 UBIFS 镜像文件,这个镜像是给 UBI volume 用的,而不是直接烧写到裸 NAND 上。真正把镜像放进 NAND 的是ubiupdatevol,或者先把裸 MTD 设备初始化成 UBI 格式再用nandwrite类工具烧写。
打个比方:mkfs.ubifs好比在电脑上把文件打包成一个 ISO,但它不会自己刻录光盘;ubiupdatevol才是刻录软件,而ubiformat的作用则是先把空白光盘划分成可以刻录的轨道。
所以标准流程是:
- 擦除整个分区(其实
ubiformat会做擦除,但这步是为了确保干净状态) - 用
ubiformat对 MTD 设备做 UBI 格式化 ubiattach把 UBI 层附着到该 MTD 设备上ubimkvol创建 UBI volume- 挂载 UBIFS
具体命令后面给,这里先强调一个观念:UBI 的格式化会用自身的布局信息覆写整个 MTD 设备。如果你不小心对存着 bootloader 的 mtd0 执行了ubiformat,那 bootloader 就没了,所以操作之前一定要确认设备编号。
2.3 制作镜像时的页大小和擦除块大小问题
制作 UBIFS 镜像的时候,mkfs.ubifs需要指定一堆底层参数。最容易出错的就在这里:主机上生成镜像时如果参数和板子实际 NAND 页大小不一致,挂载时内核会直接拒绝。
常见参数组合如下:
mkfs.ubifs -r ./rootfs -m 2048 -e 126KB -c 1024 -o rootfs.ubifs各参数含义:
-m:NAND flash 页大小(最小 I/O 单位),常见 2048 或 4096-e:逻辑擦除块大小(LEB size),一般等于物理擦除块大小减去 2 个页的大小,因为 UBI 层需要占用一些头部空间-c:最大逻辑擦除块数量,跟分区大小强相关-r:要被打包成镜像的根目录
如果 LEB size 算错了,mkfs.ubifs会提示错误或者生成一个无法挂载的镜像。计算方式其实不复杂:
物理擦除块大小一般是 128KB 或 256KB,LEB size = 物理擦除块大小 - 2 × 页大小。比如,128KB 擦除块 + 2KB 页大小,LEB 大小就是 124KB。但实际计算建议用mtdinfo查,或者参考 Linux MTD 文档,不同制造商 NAND 可能有细微差异。
经验:我通常在制作镜像前,先在板子上跑
mtdinfo /dev/mtd2,把里面的Minimum input/output unit size、OOB size、Eraseblock size抄下来,再在主机上填参数。不要凭感觉填。
3. 从零到挂载:完整的命令流程与参数选择
现在进入实际操作部分。假设你已经有了一个已经制作好的rootfs.ubifs镜像文件,也确认了分区大小,接下来就是板上执行的完整过程。
3.1 格式化 MTD 设备:ubiformat 的完整用法
在板上打开终端,先确认当前的 MTD 分区列表:
cat /proc/mtd如果确认 rootfs 对应的是mtd2,那第一步就是擦除和初始化。可以用以下命令:
flash_erase --quiet /dev/mtd2 0 0flash_erase来自 mtd-utils 工具包,0 0表示从偏移 0 开始擦除全部分区。接下来:
ubiformat /dev/mtd2 -y-y是自动确认,适合在明确无误的情况下使用。这条命令会把 mtd2 格式化成 UBI 布局,写入 UBI 的擦除计数头和卷标识。执行完之后,可以通过:
ubiattach /dev/ubi_ctrl -m 2把 UBI 层附着到 mtd2 上。这里的-m 2是 MTD 设备编号,不是设备名。如果 UBI 正确初始化,会输出类似:
UBI device number 0, total 1024 LEBs (130 MB), 126 KiB LEB size (1024 bytes)这一行信息非常重要,它告诉你 LEB 数量和 LEB size。如果和你预期的不一致,说明分区大小或擦除块大小配置有问题,这时候应该停下来排查,而不是继续。
然后创建卷:
ubimkvol /dev/ubi0 -N rootfs -m-N rootfs指定卷名,-m表示使用最大可用空间。如果你希望留一部分空间给其他用途,也可以指定-s参数设置卷大小。
3.2 挂载环节最容易忽略的细节:从镜像文件到 UBI 卷
然后就是很多人迷的地方。UBIFS 镜像文件现在还在主机上,你得先把它放进板子的 UBI 卷里。操作方式有两种:
第一种是先通过 UBI 卷节点写入镜像:
ubiupdatevol /dev/ubi0_0 /path/to/rootfs.ubifs这种方式的本质是把 UBIFS 镜像直接写入 UBI volume。注意这里/dev/ubi0_0就是第一步ubiattach之后自动出现的卷设备节点。
第二种方式常见于量产阶段:直接用nandwrite把镜像写到 MTD 设备的偏移上。这种情况一般要求镜像文件已经按 UBI 格式制作,并且偏移对齐到物理擦除块边界,否则挂载也会失败。这里不展开,重点讲第一种开发调试时最常用的方式。
写入完成后,挂载:
mount -t ubifs /dev/ubi0_0 /mnt/rootfs到这里,UBIFS 就挂载成功了。
3.3 开箱即用的另一种方案:ubiblock 自动 attach 与挂载
上面的流程是手动流程,适合调试。实际产品里,很多方案会把 UBI 的 attach 和 volume 创建流程放进 boot 脚本或 initramfs,用ubiattach之后在/dev下面找到卷节点,然后挂载。
还有一种少见但很有用的技巧:用ubiblock把 UBI 卷模拟成块设备,然后挂载。这种方式在某些需要和块设备工具链兼容的场景下非常有用,比如你想对 UBI 卷上面的 ext4 之类文件系统操作。但对 UBIFS 来说,不需要用它,直接挂载卷节点即可。
modprobe ubiblock echo 0:0 > /sys/class/ubi/ubi0/volumes/0/block执行之后会出现/dev/ubiblock0_0,你可以像普通块设备一样对它操作。不过,这种方法绕过了 UBIFS,一般不是我们的目标,但如果你之后想在同一块 UBI 上做其他实验,会理解它的存在价值。
3.4 启动阶段自动挂载的常规做法
实际产品不可能每次开机都手动敲命令。常规做法是在 bootargs 里配置ubi.mtd=2,或者用root=ubi0:rootfs ubi.mtd=2 rootfstype=ubifs这种形式让内核自己完成 attach 和挂载。
比如:
root=ubi0:rootfs ubi.mtd=2 rootfstype=ubifs内核会先扫描 mtd2,初始化 UBI,找到名为rootfs的卷,然后挂载为根文件系统。这里有个细节:ubi.mtd=2中的数字 2 仍然是 MTD 设备编号,必须和你的分区布局一致。
调试阶段如果不想重新编译内核,可以在 U-Boot 的环境变量里临时改 bootargs,先验证能不能拉起文件系统再固化到生产配置。
4. 挂载失败排查:那些年我们一起踩过的 UBIFS 深坑
这部分是我认为整篇文章最值钱的地方。光看文档你会觉得流程挺简单,但实际操作中挂载失败的频率远比你想象的高。我总结出几类高频问题,并且按排查顺序排列。
4.1 错误一:找不到 /dev/ubi0_0 设备节点
出现这类问题,说明 UBI attach 可能没成功,或者卷没创建成功。
先查内核日志:
dmesg | tail -30如果看到类似:
UBI error: ubi_init: UBI device number 0 already exists说明重复 attach 了。可以用:
ubidetach -m 2先分离,再重新 attach。
如果 attach 正常却看不到/dev/ubi0_0,执行:
ls /dev/ubi*如果只有/dev/ubi_ctrl,说明ubi0这个设备号没有生成。原因通常是内核没有启用CONFIG_MTD_UBI或者 UBI 设备号被占用。内核配置里要确认以下选项:
CONFIG_MTD_UBI=y CONFIG_UBIFS_FS=y不少 BSP 默认配置图省事,只开了其中一个,两个都开才完整。
4.2 错误二:挂载时提示 “invalid on-disk UBI format”
这个错误基本可以断定是 UBI 层没有正确初始化,或者写镜像的方式不对。
一种典型场景是:之前分区里放了别的文件系统数据,你没有执行ubiformat,直接 attach。UBI 找不到自己的头部信息,自然报格式错误。解决办法就是老老实实回到第 3 部分,先擦除再 ubiformat。
另一种场景是:ubiformat执行了,但ubiupdatevol写入的镜像不是 UBIFS 格式,而是常见的 ext4 镜像或裸文件。UBI 卷本身可以承载多种文件系统,但你现在要挂载的是 UBIFS,不是块设备上的 ext4。如果写入的是 ext4 镜像,用mount -t ubifs挂载同样会报错。
注意:在 UBI 卷上,如果挂载成 ext4,需要先把 UBI 卷变成块设备(
ubiblock),而不是直接mount -t ubifs。这个差别是我当时绕了很久才琢磨明白的,概念一定要理清。
4.3 错误三:mount 报 “No space left on device”
“空间不足”这个报错很多人第一反应是磁盘满了,实际上在 UBIFS 里还有另一层含义。
UBIFS 的空间管理是动态的。它使用的 LEB 数量可能少于卷的总 LEB 数(因为 UBI 层可能会预留一部分用于损耗均衡、原子卷更新等操作),所以文件系统看到的可用空间和ubimkvol时候创建的大小并不完全一样。如果你把卷创建得过大,比如接近整个 MTD 分区最大值,UBIFS 在运行时可能因为“逻辑块不足”而频繁报空间不足,即使你刚格式化完。
解决方案是给 UBIFS 卷留一些缓冲空间。在ubimkvol时,不用-m占满全部可用空间,而是指定-s 120MB之类的大小,比理论最大值略小。具体留多少取决于你的 OTA 升级策略和预计的坏块数量,我给不出万能值,但常见做法是留 5% 到 10%。
4.4 错误四:掉电后文件系统挂载后变只读或异常
UBIFS 本身有掉电恢复机制,但如果硬件上 NAND 的电源滤波没做好,或者复位时序不对,掉电时可能损坏 UBI 的元数据。遇到这种问题,先看内核日志,UBI 是否识别到有未完成更新的事务:
UBI: background thread "ubi_bgt0d" started, PID = ... UBI warning: ubi_io_read: error -74 while reading 2048 bytes from PEB ...如果是恢复失败且反复出现,可能需要考虑是否是颗粒本身的问题,也可能是 UBI 的 PEB 数量设置和实际物理环境不匹配。
4.5 排查工具与信息查看命令
这里给一个我最常用的排查命令清单,比反复猜测效率高得多。
# 查看 MTD 分区 cat /proc/mtd # 查看 MTD 设备详细信息,包括擦除块大小、页大小 mtdinfo /dev/mtd2 # 查看 UBI 设备信息 ubiinfo /dev/ubi0 # 查看卷信息 ubinfo /dev/ubi0_0 # 查看内核日志 dmesg | grep -i ubi dmesg | grep -i ubifs表格里汇总一张常见的错误、可能原因、排查顺序:
| 现象 | 可能原因 | 优先排查顺序 |
|---|---|---|
| mount 找不到设备节点 | UBI attach 失败或内核配置缺失 | dmesg、CONFIG_MTD_UBI、ubidetach |
| invalid on-disk UBI format | 分区未格式化或写入镜像格式错误 | ubiformat、file 命令确认镜像 |
| No space left on device | 卷创建过大或坏块增多 | ubimkvol 卷大小、坏块数统计 |
| 掉电后文件系统异常 | 电源时序/硬件干扰/UBI元数据损坏 | dmesg、重新格式化、硬件电源检查 |
| 挂载后读写速度极慢 | 未开启压缩或未使能 UBI 的 write buffer | mount 参数、NAND controller 配置 |
5. 进阶话题:不重刷整片 NAND 也能升级 UBIFS 内容
最后这部分聊点实际的运维内容。产品量产之后,总要面临系统升级的问题。很多人一想到升级就觉得要把整个 UBI 卷重新烧一遍,其实有更轻量的办法。
5.1 按卷更新而不是按分区更新
既然 UBI 层把物理擦除块抽象成了逻辑擦除块,那么我们可以针对单个 volume 做更新,完全不碰其他卷。这在多卷布局中特别有用,比如你给 kernel 和 rootfs 分别建了两个 UBI volume,升级 rootfs 时只需要更新 rootfs 卷,kernel 卷可以保持不动。
具体操作和前面的ubiupdatevol一样:
ubiupdatevol /dev/ubi0_0 /tmp/new_rootfs.ubifs这个命令支持“原子卷更新”。也就是说,在更新过程中,旧卷在被完整覆盖之前依然可用,只有新课写入完成后,指针才切换过去。实现这个能力的底层逻辑正是 UBI 层的原子 LEB 更新机制。
这个机制对于产品远程升级非常重要,因为升级过程中如果断电,在极少数情况下会导致卷不可用,但不会破坏 UBI 全局布局,重新更新一次即可恢复。
5.2 UBI volume 的动态调整
再分享一个实用操作:UBI volume 的大小可以动态调整。
ubimkvol /dev/ubi0 -N temp_vol -s 10MB ubirmvol /dev/ubi0 -n 1也可以在卷存在时调整大小:
ubirsvol /dev/ubi0 -n 0 -s 64MB这个特性在数据分区容量规划不合理、需要临时扩展的场景下很有用。比如,原先你给数据卷只分了 16MB,跑了一段时间不够用,只要分区里还有空闲 PEB,ubirsvol就能把它扩大到 32MB。注意这个工具执行时,对应的卷不能处于挂载状态,所以最好在系统维护模式下操作。
5.3 维护 UBI:坏块与擦除计数
UBI 的一个重要能力是坏块管理和磨损均衡,但它并不是“无限免维护”。随着使用时间变长,擦除计数高的块会比计数低的块更快接近寿命上限。UBI 会尽量让所有块的擦除次数保持平均,但如果分区里预留的空闲 PEB 不足,磨损均衡的效果就会受限。
检查卷健康状态:
ubinfo -d /dev/ubi0输出里的free PEB和bad PEB数据值得关注。如果bad PEB数量增长异常,要怀疑是不是硬件环境或驱动参数有问题;如果free PEB趋近于零,说明分区规划太满,需要调整。
6. 一点个人经验:稳定的首先要务是保持链路简单
如果要总结一句我这些年做 NAND 文件系统的心得,那就是:别为了炫技把这套链路搞得太复杂。
很多厂商给出的参考设计其实非常直白:固定分区、固定卷、固定大小。你在产品初期也许觉得这样不够灵活,想搞一堆动态卷、动态扩容、多 boot 镜像,结果每次出问题都要在 UBI 层和 mount 参数之间来回排查,成本反而更高。
另外,工具链版本一致性是个容易忽略的点。制作镜像用的 mtd-utils 版本,和板子上跑的内核的 MTD/UBI 实现之间,可能存在兼容性差异。老版本生成镜像、新内核挂载,偶尔出现奇怪的 I/O 错误。我建议至少把mkfs.ubifs、ubiupdatevol的版本记录下来,和内核版本一起归档,排错的时候能省不少事。
如果现在还有人问我:挂载 UBIFS 到底难不难?我不会直接说“不难”。在理解了 MTD → UBI → UBIFS 这条路之后,它其实一点都不难;但如果你一开始就跳过了 UBI 层,直接在 mtdblock 上挂载,那后面每一步都是在跟错误较劲。先把概念理清,再动命令,稳扎稳打,这套体系其实相当好用。