4. UBIFS实战:UBI卷管理、wear-leveling机制、LEB/PEB映射关系、ubinfo/ubiformat工具链

好,咱们今天来聊聊UBIFS。说实话,我在嵌入式存储领域摸爬滚打这么多年,UBIFS一直是我最偏爱的文件系统之一。为什么?因为它解决了NAND Flash上很多让人头疼的问题。你想想看,传统的JFFS2在容量稍大的Flash上,挂载慢得像蜗牛,内存占用也高得离谱。UBIFS的出现,算是把这些问题都收拾得服服帖帖了。

4.1 从MTD到UBI:为什么要多这一层?

很多刚接触UBIFS的朋友会问:为什么不能直接在MTD设备上跑文件系统?嗯,这个问题问得好。

MTD(Memory Technology Device)层直接操作Flash芯片,但它有个致命弱点——它不处理坏块,也不做磨损均衡。你想想,NAND Flash的寿命是按擦除次数算的,一般MLC也就3000-10000次。如果某个块被频繁擦写,它很快就挂了。

UBI(Unsorted Block Images)层就是来解决这个问题的。它在MTD之上加了一层抽象,核心功能就两个:

  • 坏块管理:自动跳过坏块,把数据映射到好块上
  • 磨损均衡:让所有块的擦除次数尽量平均

说白了,UBI层就像一个智能的物业管理员。它知道哪块地皮(PEB)快不行了,就少用点;哪块地皮还很新,就多安排点活。这样整个Flash的寿命就能大大延长。

核心概念:PEB vs LEB

PEB(Physical Erase Block)是物理擦除块,就是Flash芯片上实实在在的存储单元。

LEB(Logical Erase Block)是逻辑擦除块,是UBI层向上层(UBIFS)暴露的虚拟块。

UBI负责维护PEB到LEB的映射关系。一个LEB可能对应不同的PEB,这取决于磨损均衡策略。

4.2 LEB/PEB映射关系:UBI的"小本本"

UBI内部维护着一张映射表,记录着每个LEB对应哪个PEB。这张表存储在Flash的特定区域,叫UBI卷表(volume table)。

我在项目中遇到过一个问题:某次系统异常掉电后,UBI卷表损坏了,导致整个文件系统无法挂载。后来我学乖了,每次做固件升级前都会备份卷表。

映射关系的变化过程是这样的:

  1. UBIFS要写数据到LEB 5
  2. UBI查表,发现LEB 5当前对应PEB 100
  3. UBI把PEB 100的数据读出来,合并新数据,写入一个新的PEB(比如PEB 200)
  4. 更新映射表:LEB 5 → PEB 200
  5. 把旧的PEB 100标记为"待擦除"
  6. 后台线程择机擦除PEB 100,加入空闲池

你看,每次写操作都伴随着一次"搬移"。这就是UBI实现磨损均衡的方式——通过不断迁移数据,让所有块都有机会被擦除。

个人经验:UBI的磨损均衡不是实时的,而是通过后台线程(wear-leveling thread)定期触发。如果你的系统对实时性要求很高,建议把UBI的磨损均衡线程优先级调低,避免影响关键任务。

4.3 UBI卷管理:创建、调整、删除

UBI卷类似于硬盘上的分区。你可以创建多个卷,每个卷可以格式化成不同的文件系统(UBIFS、squashfs等)。

常用的卷管理命令:

# 创建一个UBI卷,大小100MiB,卷名"rootfs"
ubimkvol /dev/ubi0 -N rootfs -s 100MiB

# 查看UBI设备信息
ubiinfo /dev/ubi0

# 查看UBI卷信息
ubiinfo /dev/ubi0_0

# 调整卷大小(只能增大,不能缩小)
ubirsvol /dev/ubi0_0 -s 200MiB

# 删除卷
ubirmvol /dev/ubi0 -n 0

我个人习惯在创建卷时预留10%-20%的空闲空间。为什么?因为UBI需要空闲块来做垃圾回收和磨损均衡。如果卷占得太满,系统性能会急剧下降。

注意:UBI卷的大小必须是LEB大小的整数倍。LEB大小通常是PEB大小减去UBI头部开销(一般是2个PEB)。比如PEB大小为128KiB,LEB大小就是126KiB(128KiB - 2KiB头部)。

4.4 ubiformat:格式化Flash的正确姿势

很多人直接用flash_erase擦除NAND Flash,然后写UBI镜像。这种做法其实有隐患。flash_erase不会处理坏块,也不会写入UBI的卷表信息。

正确的做法是用ubiformat

# 格式化整个MTD设备,自动扫描坏块
ubiformat /dev/mtd0

# 格式化并写入UBI镜像
ubiformat /dev/mtd0 -f rootfs.ubi

# 只格式化,不写入镜像
ubiformat /dev/mtd0 -y

ubiformat会做以下几件事:

  • 扫描所有块,标记坏块
  • 擦除所有好块
  • 写入UBI头部信息
  • 如果指定了镜像文件,还会写入卷数据

我曾经踩过一个坑:用flash_erase擦除后直接写UBI镜像,结果某些块因为擦除次数过多变成了坏块,但系统不知道,导致后续读写错误。从那以后,我再也不敢偷懒了,一律用ubiformat

4.5 ubinfo:诊断UBI系统的利器

当系统出问题时,ubinfo是你最好的朋友。它能告诉你UBI设备的详细状态:

# 查看UBI设备概要
ubinfo -a /dev/ubi0

# 输出示例:
UBI device number 0
total PEBS: 1024
available PEBS: 98
total LEBs: 1016
bad PEBs: 8
max allowed bad PEBs: 20
mean erase counter: 127
max erase counter: 156
min erase counter: 98

这些信息非常关键:

  • bad PEBs:坏块数量,如果接近最大值,说明Flash快不行了
  • mean erase counter:平均擦除次数,反映磨损均衡效果
  • max/min erase counter:最大和最小擦除次数,差值越小说明磨损均衡越好

我一般会在产品出厂前跑一轮压力测试,用ubinfo监控这些指标。如果发现最大和最小擦除次数差值超过100,说明磨损均衡策略可能需要调整。

4.6 实战:构建UBIFS根文件系统

好了,理论讲完了,咱们来点实际的。下面是我在Buildroot中配置UBIFS的完整流程:

# 1. 在Buildroot中启用UBIFS
make menuconfig
# 进入 Filesystem images → ubi image generation
# 勾选 "ubifs" 和 "ubinize"

# 2. 配置ubinize参数
# 在 Buildroot 目录下创建 ubinize.cfg 文件
[ubifs]
mode=ubi
image=rootfs.ubifs
vol_id=0
vol_size=100MiB
vol_type=dynamic
vol_name=rootfs
vol_flags=autoresize

# 3. 生成UBI镜像
make

# 4. 烧录到Flash
ubiformat /dev/mtd0 -f output/images/rootfs.ubi

小技巧:使用vol_flags=autoresize可以让UBI卷自动扩展到Flash的剩余空间。这样你就不用手动计算卷大小了,特别适合不同容量的Flash共用同一套固件。

4.7 性能调优:让UBIFS跑得更快

UBIFS的性能受很多因素影响,我总结几个关键点:

参数 默认值 建议值 说明
LEB大小 自动 126KiB(128KiB PEB) 越大顺序读写性能越好
写缓冲区大小 64KiB 128KiB-256KiB 增大可减少写放大
垃圾回收阈值 5% 10%-15% 提高可减少碎片
磨损均衡阈值 256 128-512 越小磨损均衡越积极

我在一个视频监控项目中,把写缓冲区从64KiB调到256KiB后,录像写入性能提升了约30%。当然,代价是内存占用多了些。这需要根据你的硬件资源来权衡。

4.8 避坑指南:我踩过的那些坑

最后,分享几个实战中容易遇到的问题:

  • 挂载失败:检查MTD分区大小是否和UBI卷匹配。我曾经因为分区表写错,导致UBI卷越界,系统死活起不来。
  • 空间不足:UBI需要至少5%的空闲空间做垃圾回收。如果卷使用率超过95%,性能会急剧下降。
  • 掉电保护:UBIFS有日志机制,能应对意外掉电。但如果在写日志时掉电,可能会丢失少量数据。关键数据建议用fsync强制刷盘。
  • 坏块增长:定期用ubinfo监控坏块数量。如果增长过快,说明Flash寿命快到了,该考虑换硬件了。

嗯,UBIFS的内容差不多就这些了。说实话,它是我用过的最靠谱的NAND Flash文件系统之一。只要配置得当,跑个三五年不成问题。下一章咱们聊聊SquashFS,那又是另一番天地了。