注意事项
UBI不同于FTL(Flash Translation Layer)
UBI只支持裸Flash,不支持管理例如TF/SD卡,U盘等封装的Flash设备
UBI支持Raw Flash
概述
UBI全程为"Unsorted Block Image",是一种用于Raw Flash的卷管理系统,主要功能是在同一个Flash芯片上管理多个逻辑卷,并且平衡整个Flash读写操作(不让一个页或者块过于频繁的写擦除)
不同于LVM,LVM只提供逻辑扇区到物理扇区的映射,而UBI提供的是逻辑块到擦除块的映射,同时提供对于用户透明的错误处理
一个UBI卷代表这一个连续的逻辑擦除块(LEB),每个逻辑擦除块映射成任何位置物理擦除块。映射对于用户是完全透明的,映射是Wear-Leveling的基础(需要支持擦除次数和支持对用户透明的数据搬移能力)
UBI卷容量在建立时被设定,并且能够在使用时被修改。UBI提供了用户使用的工具
UBI卷有两种:静态(不可修改,CRC校验)和动态(可修改,上层例如文件系统保证数据完整性)
UBI支持坏块管理功能,这样上层软件不需要复杂逻辑进行坏块处理。实际上,UBI通过预留了一些擦除块,在擦除块发生坏块时可以使用预留的擦除块进行替换并将原有的数据拷贝到新的替换的擦除块中,从而实现对坏块的处理(所有的这些操作对于上层都是透明的)
读写Flash时可能会发生比特翻转,一般情况下可以使用ECC进行修复。发生比特翻转时,UBI会将擦除块中的数据拷贝到新的擦除块中(称为Scrubbing)。这个过程对用户是透明的。
UBI的特性:
UBI卷可以被动态删除或者改变大小
支持整个Flash的负载均衡,用户可能对同一个逻辑擦除块进行反复写/擦,实际上UBI会将这些操作分配到不同的物理擦除块上。
提供透明的坏块管理
最大程度保证数据不被损坏(Scrubbing)
UBI卷和MTD分区的比较:
都提供三种操作:读、写、擦除
都提供擦除块概念,MTD操作的是物理擦除块,UBI操作的是逻辑擦除块
和MTD分区的优势:
用户不需要关心负载均衡,上层软件实现简单
用户不需要关心坏块(UBI会将坏擦除块进行替换),上层软件实现简单
UBI卷可以动态管理,MTD只能静态。
处理比特翻转使得上层软件实现简单
UBI的卷更新操作能够很容易的检测到中断的软件更新,并且能够修复
支持原子块数据更新,即使突然出现重启,数据也不会丢失。简化了像文件系统这样上层软件实现。
支持解除映射操作(实际上将LEB和PEB解除映射,同时安排擦除PEB),这个过程很快,且对于上层软件来说不需要实现和关心擦除操作。
UBI处理坏块的操作对用户是透明的,这样它就允许通用的、块的文件系统装载在UBI的卷上。
源码
UBI在2.6.22被引入建议使用最新的版本。
用户工具
git://git.infradead.org/mtd-utils.git
UBI和其他MTD工具在上面的链接中可以找到,下载并编译后可以找到如下工具:
ubinfo
- 打印UBI设备和卷信息ubiattach
- 将MTD设备关联到UBI上并建立相应的UBI设备ubidetach
- 反关联ubimkvol
- 为UBI设备建立UBI卷ubirmvol
- 从UBI设备上删除UBI卷ubiblock
- UBI卷的块管理ubiupdatevol
- 更新卷ubicrc32
- 计算CRC32ubinize
- 生成UBI镜像ubiformat
- 格式化空FLASH,擦除FLASH并且保留擦除次数然后将UBI镜像写入MTD设备中mtdinfo
- 打印MTD信息
UBI头
UBI定义每个正常擦除块前面有两个64字节的信息头,分别是:
EC头:64字节,包含擦除次数(和一些其他不重要的数据)
VID头:64字节,包含卷ID、物理擦除块(PEB)映射的逻辑擦除块(LEB)(以及一些其他不重要的数据)
这些头信息占用了一部分的PEB,可使用的LEB比PEB要少一些空间(EC头和VID头对于LEB是透明的)。
另外需要对这两个UBI头数据进行CRC校验
当UBI关联到MTD设备操作步骤:
扫描整个MTD设备
读取所有PEB的头,并计算校验值,在内存中保存擦除次数和LEB-PEB的映射信息。
当UBI擦除PEB时,首先从PEB中读取EC,然后增加EC后写入到EC头中。这意味着大多数时候,PEB中存储着EC信息(除了刚擦完的瞬间)。不过若是PEB在刚擦完的时候,设备发生重启,EC没有被写入。启动后,PEB就处于Corrupted状态,UBI需要在扫描完设备后对被认为这个损坏PEB写入平均的擦除次数。
PEB和LEB发生映射/解除映射,会更新VID,考虑下面的情况:
1、LEB解除映射到PEB,并且准备马上进行擦除操作。EC头和VID头信息都被丢失
2、LEB映射到PEB或者对没有映射的LEB进行写操作时,UBI会查找相应的PEB,并且写入相应的VID信息(注意,对已经映射的LEB写操作时,并不会改变VID,只会直接写入数据)
为什么需要EC和VID两个头,何不将这两个头合为一个信息头?原因是不同的时刻写入不同的数据到不同的头
1、当PEB被擦除,EC信息可以直接写入。即使重启也只会丢失EC信息。
2、当LEB关联到PEB时,VID信息被更新。
另外,当EC被写入PEB时,还并不需要知道关联的LEB信息和卷信息。
UBI卷表
UBI卷表是一个保存在FLash中的数据结构表,它描述这个UBI设备上每个卷的信息。这个表的每项是一个卷记录,每个记录包含:
- 卷大小
- 卷名
- 卷类型(静态或者动态)
- 卷对齐方式
- 更新标记(标记是否Interrupt)
- 自动调整大小标志
- CRC校验值
每个记录描述一个UBI卷,记录的索引值代表卷ID,记录的数目受限于LEB的大小,不过最多不能超过128个。
一旦UBI卷建立、删除、改变大小、改名、更新时,对应的卷记录改变。UBI维护两份表,这是为了可靠性以及防止突然掉电。
实现
在内部,卷表保存在一个卷名为layout的卷中。这个卷包含2个LEB,分别对应2个数据备份。layout卷是一个内部卷,用户不能看到它也无法进行操作。在内部对它的操作方式和其他普通卷没有差别。
对UBI卷表操作时的步骤:
1、在内存中准备好数据
2、解除LEB0的映射
3、将内存中的数据写入LEB0中
4、解除LEB1的映射
5、将内存中的数据写入LEB1中
6、Flush the UBI work queue to make sure the PEBs are corresponding to the un-mapped LEBs are erased.
疑问:不是说写没有映射的LEB时,会发生映射操作吗?
当MTD关联到UBI时,会检查这两个备份是否一致,不一致则代表突然掉电的发生,然后就需要从一个备份恢复另外一个备份的操作了。
最小读写单元
UBI使用抽象模型描述Flash,从UBI的角度来看,flash包含擦除块,每个擦除块可以进行读写和擦除操作,每个擦除块可以被标记为坏块。
Flash的类型不同,读写的最小单元不同:
Nor Flash的一般最小读写单元位1个字节(有些甚至可以操作一个比特位)
有些Nor Flash的最小读写单元为16或者32个字节(ECC's Flash)
Nand Flash最小读写单元是512/2048/4096字节(被称为页),每个页拥有一个ECC校验值,被保存在OOB区。因此,每次读写的最小单元必须是一个页,这样才能和ECC校验一致。
最小读写单元对UBI结构影响如下:
VID头的物理位置和最小读写单元相关,最小读写单元越大,LEB就越小
所有的写操作必须最小读写单元对齐,读操作对于用户来说好像没有这样的限制,不过在驱动层每次读操作也是最小单元对齐的。
Nand Flash子页
一些SLC结构的Nand Flash还拥有子页这样的最小读写单元
若是支持子页,ECC校验值是基于子页而不是页。这样最小读写单元就可以细化到子页。
不过,虽然有些FLash芯片支持子页,但若是Flash控制器不能支持子页,也不能进行子页操作。
子页实际上是MTD的概念,也被称作NOP(分区数目),NOP1代表1个页中只有1个子页,NOP2/NOP4代表1个页中有2/4个子页
UBI能够支持子页功能,这样可以减少LEB的浪费空间(对于PEB为128K,pagesize为2k,sub-page size为512的flash来说,不使用子页是LEB的大小128k-2k*2,使用子页时大小为128K-2K,EC头和VID头保存在一个页中)
UBI只在内部使用子页概念保存EC头和VID头(只有EC/VID使用了子页的特性,其他数据并没有子页的概念),对外提供的API函数中没有子页的概念(原因是子页会影响读写性能)
UBI头位置
EC头总是存储在PEB的第0个页的第0个字节,并且占用64个字节。VID头在:
支持subpage的话,保存在第0个页的第1个子页的第0个字节,占用64字节
不支持subpage的话,保存在第1个页的第0个字节,占用64字节
NorFlash的话,保存在第64个字节,占用64字节。
Flash的OverHead
从上面可以看出,UBI额外占用了一些空间,包括:
2个PEB,每个保存卷表备份(防止掉电)
1个PEB,用于Wire-leveling(防止掉电)
1个PEB,用于原子改变LEB(防止掉电)
多个PEB用于处理坏块管理,一般来说每1024个PEB需要保留20个PEB。(坏块处理)
每个PEB中保存EC头和VID头
上面可以看出,整块的额外占用区实际上是为了在相应操作时发生突然重启,保证数据能够恢复到之前。
保存EC
UBI Flasher如何工作
UBI Flasher烧录镜像时步骤:
首先,扫描整个Flash,读取每个PEB的EC头并校验CRC,忽略所有坏块和VID头,在内存中保存擦除次数
计算平均擦除次数,用于恢复在突然重启时丢失的EC值
若是进行擦除操作,需要更新擦除次数,若在擦除或写时发生I/O错误,需要标记块为坏块。
若是进行烧录操作时:
将当前PEB需要的镜像数据读取到内存中
- 从后往前,若是最小单元中全部位0xFF则从内存中去除这个最小单元大小的数据
- 擦除PEB
- 在内存中修改更新EC头,并重新计算CRC
- 将内存中数据烧写到PEB中
一般来说,UBI的镜像比FLash要小,需要烧录所有使用的PEB,擦除其他未使用PEB
注意,当写PEB时,并不关心操作的是哪个PEB(关心的是LEB)
对于生产线上烧录时,不需要改变EC值,这可以简化烧录步骤
当UBI镜像包行UBI文件系统时并且使用Nand Flash时,需要丢掉后面的所有oxFF的数据。这很重要,虽然并不是所有的Flash都需要这样,不过还是强烈建议这样。
原因在于UBI文件系统对于全部值为oxFF的页作是空闲区。对于某些Flash来说,页不能够写入两次(虽然第一次写入的全部时0xFF)
另外,不需要丢弃所有的0xFF页,只需要从后往前的所有0xFF页。中间的不需要管。
去除0xff页的操作可以自己实现,也可以在生成文件系统的时候使用选项"free space fixup"
标记坏的擦除块
坏块标记发生在:
1、擦除时发生IO错误,擦除块被直接标记为坏块
2、写操作失败时,数据被搬移到其他保留块中,并安排检查当前的块
对于写操作时标记的坏块需要作检查,因为有时候并不一定是块真正坏了,检查过程:
1、擦除
2、读取,检查是否为全部0xFF
3、写入特殊数据
4、读取数据并检查
5、其他特殊数据
没有通过了上述检查的块会被标记为坏块,注意在测试过程中发生的比特翻转也可以认为是没有通过测试
扩展性问题
UBI和Flash的大小相关,容量越大,初始化的时间越长。(这个问题在3.7有了解决方案叫"firstmap" 它能够使得关联操作花费的时间为常数)UBI初始化操作所花费的时间就只与I/O速率和CPU的计算能力相关了。
当发生关联时,UBI需要扫描MTD设备,它需要从每个PEB中读取EC头和VID头,读取的数据量为128字节(Nor Flash) 2K(支持子页)4K(不支持子页)。虽然看起来不少,不过这比其他文件系统要读取的数据量小得多
CPU需要计算EC头和VID头的CRC校验值,时间和CPU的计算能力相关,虽然这个时间和I/O的读取时间相关比很小
实现细节
总体上,需要操作3个表
- 卷表,这个表中包行所有的卷信息(包括卷类型,大小)
- EBA表,包含逻辑-物理映射关系信息,每次读写LEB时都会查询这个表得到对应的PEB,然后操作这个PEB
- EC表,它包含每个PEB的擦除次数信息,这个表的用处在于wear-out leveling。
卷表在Flash中被维护(意味着不需要在内存中维护一份数据结构),只有当建立卷、删除卷、改变卷大小时才会修改这个表。这种情况非常少见,而且操作时间慢点也无所谓
EBA表和EC表在每次LEB和PEB发生映射和解映射时都会改变,映射和解映射操作非常常见,因此对于EBA表和EC表的管理操作时间必须很快且高效率。
EBA表和EC表可以保存在Flash中,不过这样会变得很复杂(涉及日志)。这是以后扩展的问题
UBI要求Flash上的数据结构简单,因为boot-loader需要读取UBI卷,Boot-loader不支持复杂的日志扫描和恢复操作
因此,UBI不会将EBA和EC表保存在Flash上。它会在内存中建立这两个表(每次关联MTD设备时)。这意味着UBI需要扫描整个Flash并且从每个PEB中读取EC头和VID头
这样作的缺点是:扩展性差且Overhead大(例如,对于页代大小位2K擦除块大小位128K的NandFlash,大约有1.5%-3%的flash空间Overhead)。优点在于简单的二进制格式和容错性、实现简单
但是,也有可能的办法区建立UBI2将表保存在其他FLash区域。UBI2和UBI不兼容(原因是Flash上的保存数据格式不同),但用户层可以保持不变,这就保证了原有支持UBI的用户软件也兼容UBI2
保留块(只有NandFlash支持)
一般地,Nand都有一些物理擦除块被厂家标记为坏块。在使用的过程中,可能会出现其他的坏块。虽然,厂商通常会保证最前面几个的擦除块不是坏块且整个芯片的坏块比例不会超过一定的数字(一般地20/1024)。上面的20/1024也是UBI保留块的默认数目,这看起来像是对Flash空间的浪费,但是,坏块总是会出现,总不至于一出现坏块就丢掉整个芯片吧。 另外,在一个Nand Flash上,应该使用1个UBI设备且多个UBI卷,而不是使用多个UBI设备。这样作的空间利用率会更大。
默认的20/1024保留是内核的默认配置选项,不同的UBI设备可以使用不同的配置参数或者ubiattach使用不能的选项(内核3.7后才支持)
卷的自动调整大小
当需要建立UBI镜像在生产线上被烧录到用户的终端设备上时,需要定义卷的准确大小(这个大小被保存在UBI卷表中)。但一般地在嵌入式中,对于根文件系统只需要一个只读卷,其他的空间用作可读写的卷。假如根文件系统确定,第二个文件系统的占用大小可以根据不同的产品设置成不同的大小,我们只需要主文件系统剩下的所有空间。
这就是自动调整大小的功能。假如卷拥有自动调整大小标记,在UBI第一次使用时会自动调整卷的大小。当卷的大小调整后,UBI会删除卷的自动调整标记(以后就不会再调整了)。自动调整标记保存在卷表中,且只能有一个卷可以被标记为自动调整大小。
UBI操作
LEB un-map
LEB解映射操作在ubi_leb_unmap函数中实现,从内核2.6.29开始,在用户空间,可以通过UBI_IOCEBUNMAP的ioctrl命令进行解映射操作。注意,只能通过UBI卷字符设备调用这个ioctrl。LEB解映射操作包括:
解除LEB和对应的PEB映射
安排PEB进行擦除操作,擦除操作实际在后台运行,因此解映射不会的带PEB的擦除完成。
当读取一个没有映射的LEB时,UBI得到是全部0xFF,所以解映射必须时一个很快的擦除操作。当有一个问题需要UBI程序员注意:
假设解除LEB L和PEB P之间的映射。由于P并不是同步擦除的而是被安排在未来某个时间擦除,而此时可能发生突然的重启。假如重启发生在P真正被擦除之前,重启后MTD关联时L会被再次映射到P。事实上,UBI扫描MTD设备并且发现P和L关联,然后它会将这个映射信息添加到EBA表中
问题是,一旦你向L写入任何数据或者映射这个LEB,L会映射到一个另外一个PEB上,原来的PEB中的内容被永久丢弃,原因是突然重启会使得UBI为L选择一个新的映射
实现细节
这节描述了UBI如何在突然重启时区分老的和新的LEB。假设LEB L映射到PEB P1,现在我们解除映射,这意味着P1将要被擦除。然后我们向L写入数据,这意味着UBI选择另外一个PEB P2、将L映射到PEB然后向P2写入数据。假若在P1被擦除之前但是写操作完成后发生突然重启,这时有2个PEB(P1和P2)映射到同一个LEB L上
为了处理这种情况,UBI维护这一个全局的64位的序列数字变量。这个数字变量在每次发生PEB映射到LEB时加1,并且这个值被存入当前PEB的VID头中。这样每个VID头都有一个特有的序列数字,并且数字越大说明VID越新。当UBI关联到MTD设备时,UBI设置全局序列数字变量的值位所有VID头中最大值加1.
在上面的情况中,UBI会选择数字大的PEB(P2)并且丢弃数字小的PEB(P1)
注意,有些情况下可能会更加复杂。假如突然重启发生在UBI将数据从一个PEB搬移到另外一个PEB时,或者发生在LEB原子改变操作时。在这种情况下,选择最新的PEB是不够的,还需要保证数据到达新的PEB中。
LEB映射
LEB映射操作是将之前解除映射的LEB映射到一个PEB上。例如,为了给LEB A映射,UBI需要查找合适PEB,给这个PEB写入VID,并且在内存中EBA表中作相应的修改。 以后通过VID头就能找到LEB A。操作过后,所有对于LEB A的I/O操作都会操作到被映射的PEB上。LEB映射操作在内核函数ubi_leb_map中实现。也可以通过卷字符设备发生UBI_IOCEBMAP的IOCTL命令实现。IOCTL命令在2.6.29后才支持。
映射时需要保证原来的LEB数据被丢弃。LEB解映射时,对应的PEB并没有立刻直接擦除。因此,若是突然重启,LEB很可能会被再次映射到原来的PEB(当UBI关联到MTD设备时)。所以,假若映射了刚刚被解除映射的PEB,程序员需要保证原来的LEB数据不会重新出现。简单说,映射操作的结果是LEB中的数据应该是全为0xFF。
LEB映射操作需要小心,只要必要时才使用。原因是已经映射的LEB比没有映射的LEB对于wear-leveling处理起来更复杂。原因在于,当LEB未映射时,wear-leveling不需要搬移数据。
卷更新
卷更新在设备软件更新时很有用。这个操作使用新的内容更新了整个UBI卷内容。当时,假如在更新中途突然终止,卷的状态就会变为"corrupted",以后对于这个卷的I/O操作就会导致EBADF错误。修复的唯一方法是启动一个新的卷更新操作并且正常完成它。卷更新操作可以检测被中断的更新并且重启启动更新。例如,一个名字"mirror"的卷会提示一个对话框告诉用户问题并且要求flashing。而raw MTD分区则不能够检测到中断的更新。
卷更新操作只能在用户层进行(内核层没有对应的API),更新时,先向对应的UBI卷字符设备调用UBIIOCVLOLUP的ioctl命令,并且传入一个一个指针(指向一个64-bit数据,保存中新卷内容的字节长度)。然后,卷数据被写入到卷字符设备中,一旦最后一个字节被写入,更新操作才算完成。代码如下:
fd = open("/dev/my_volume");
ioctl(fd, UBI_IOCVOLUP, &image_size);
write(fd, buf, image_size);
close(fd);
若是image_size为0,则卷被擦除,所有的LEB成为未映射。
注意/sys/class/ubi/ubiX-X/corrupted 文件中反映了卷的状态,0代表卷OK,1代表卷损坏。
卷更新操作不会保存原来的卷内容,不过会支持在卷改名时的卷更新原子操作。
卷更新操作使用update marker。一旦用户发起UBI_IOCVOLUP的ioctl命令,UBI在UBI卷表中对应的卷记录中设置update marker标志。然后卷被擦除,然后等待用户输入数据。一旦所有的数据到达,并且被写入到Flash中,Upadate Marker标志位被清除。为了防止突然的中断,update marker标志位没有被清除,卷的状态被当作损坏。只有成功的更新操作才能清除这个标志位。
原子改变LEB
LEB原子改表是原子操作改变LEB的内容,一旦终端原来的内容可以保留。简单说,这个操作的结果要么是新的内容要么不成功还是原来的内容。这个操作在内核API函数ubi_leb_change中实现。用户层调用接口在2.6.25后支持。
用户层原子改变LEB的操作使用UBI_IOCEBCH的IOCTL命令。必须传入一个指向struct ubi_leb_change_req类型数据的指针。然后向卷字符设备写入指定字节大小的数据。程序如下:
struct ubi_leb_change_req req;
req.lnum = lnum_to_change;
req.len = data_len;
fd = open("/dev/my_volume");
ioctl(fd, UBI_IOCEBCH, &req);
write(fd, data_buf, data_len);
close(fd);
假若因为某些原因没有写入指定字节大小的数据却关闭的设备文件,本次操作就被当作取消。原来的LEB内容还是保留的。
和卷更新操作一样,并不关心写入的次数和每次写入的字节数,重要的总共写入的字节数。
需要记住原子更新LEB操作会计算新数据的CRC校验值,所以和LEB擦除写操作比需要多一些时间。 卷更新操作并不计算CRC,所以比原子更新所有的擦除块要快。只有在需要的情况下才进行这样操作。
实现细节
假若UBI需要改变LEB L(它映射到PEB P1)。首先,UBI通常会为这个原子操作保留一个空闲的PEB(假设它位P2)。在原子操作之前,P1保存着LEB L的数据并且P2空闲(P2只有EC头和0xFF数据)。新数据被写入到P2而不是P1,这样一旦任何问题发生,原来的LEB的数据还是在那。
当原子操作完成,UBI将L解除和P1的映射,然后映射到P2并且安排P1擦除。假若这些操作被中断,L保持和P1的映射关系,而P2被安排进行擦除。
假若原子操作过程中突然重启,显而易见需要保留原来的L和P1的映射,并且擦除P2。但假若重启发生在P1擦除之前但原子改变完成后,则需要保留L和P2的映射然后擦除P1。
为了解决上面的问题。UBI对LEB的新数据在写入Flash之前计算CRC校验值,然后将CRC值保存到VID头(和数据长度)。当UBI发现2个PEB(P1和P2)映射到同一个LEB L时,若P2中的CRC校验成功,它选择拥有大序列值的PEB(P2,CRC正确意味着数据成功被写入),否则选择P1。当然,UBI必须读取LEB的数据然后检查CRC的校验值。