文章目录UBI介绍Flash存储内容代码实现Readflashdataintomemoryorganizationdatastructurevolume&EBAsubsysteminitializationwear-levelingsubsysteminitializationanexampleEraseandwriteequalizationEraseandwritetimingEraseandwriteconditionaltextUBI简介UBI的全称是UnsortedBlockImages。上图是UBI在系统中的层级结构,最下面是flash层(包括flash控制器、各个flash驱动代码、spi-mem层等);MTD层是flash层的抽象。一个flash可以分为不同的分区,每个分区对应一个MTD设备;UBI层是基于MTD层之上的更高一层,UBI层抽象出每一个逻辑擦除块,每一个逻辑擦除块都有一个物理擦除块对应于前一个,有了这个映射,我们可以添加一些软件算法来达到擦写平衡的目的,从而提高flash的使用寿命;以上是基于UBI层实现和各种文件系统,比如UBIFS。flash存储的内容首先介绍几个概念:PEB:physicaleraseblocks,对应flash上??的一个eraseblockLEB:logicaleraseblocks,软件上的概念Volume:卷apartitionoftheflash)结构:ubi层以eraseblocks为单位管理flash,LEB对应软件的概念,PEB对应flash上??真实的eraseblock,每个LEB对应一个PEB。向上看,多个LEB可以组成一个volume,即LEB可以根据不同的功能分成不同的volume;其中,value-layout是ubi内部使用的一个volume,用于存放MTD设备上的所有数据。每个分区卷的信息包含两个LEB,它们存储相同的内容并且互为备份。往下看,每个PEB的内容包含3部分:ech(erasecounterheader)、vidh(volumeidentifierheader)、data。下面介绍具体含义。代码实现Linux对于UBI层的代码实现大致可以归纳为三个方面:首先,数据是存放在flash中的,因此需要将flash中的相关信息读入内存中,同时进行checkoutflash中的badblock数据读取内存后,需要按照内部逻辑关系进行整理(比如把正在使用的PEB放在红黑树上进行管理,空闲的PEB也放在用于管理的红黑树)。与内存中的这些数据建立关系后,就可以进行操作(如读写操作、增容、删除、扩容等,擦除和写平衡操作)将闪存数据读入内存。UBI初始化时的代码调用流程如上图所示,最后会调用scan_all()函数,scan_all()函数会遍历MTD设备中的每一个PEB,并从中读出ech和vidh,以及它们的定义如下。ech的定义如上,其中:ec:表示PEB被擦写的次数。借助这个字段,我们可以找出擦写次数最少的PEB,从而达到擦写平衡的目的。vid_hdr_offset:表示vidh在PEB中的Offset位置data_offset:表示实际数据在PEB中的偏移位置vidh定义如上,其中:vol_id:表示PEB属于哪个volumevolume,该字段与MTD设备中PEB的编号形成映射关系。通过遍历MTD设备的每一个PEB,就可以知道每一个PEB的情况,是被使用、空闲还是损坏。这些信息会暂时记录在structubi_attach_info结构中。遍历过程详见scan_all()函数。组织数据结构遍历PEB后,flash信息会保存在临时结构体structubi_attach_info中,然后structubi_attach_info中的临时信息会保存在全局结构体structubi_device*ubi_devices中。代码如下:分为三步,分别是volume的初始化,wear-leveling子系统的初始化,eba(EraseblockAssociation)子系统的初始化;让我们分别看看它们。Volume&EBA子系统初始化前面介绍过,volume-layout是UBI内部使用的一个volume,包含两个LEB(mutualbackups),对应上图PEB中的数据内容,数据(灰色)部分是一个structubi_vtbl_record结构体数组,记录了当前UBI设备所有卷的信息。ubi_read_volume_table()函数首先遍历临时结构体structubi_attach_info找出volumelayout所在的PEB,然后读出structubi_vtbl_record的结构体数组保存在内存中,也就是structubi_device*中的structubi_volumevolumes[]字段,初始化后的数组结构如下图所示,其中structubi_volume*volumes[]是一个指针数组,数组中的每个元素都是一个structubi_volume结构体(参见ubi_read_volume_table()函数详细过程)。在structubi_volume结构体中,有一个比较重要的字段structubi_eba_table*eba_tbl,它记录了当前volume中所有LEB和PEB的映射关系,其中structubi_eba_entry*entries是一个数组结构,每个元素对应一个structubi_eba_table结构体,structubi_eba_entry*entries数组的下标对应LEB的序号,数组元素的内容对应EB的序号,从而实现LEB和PEB的关联(见ubi_eba_init()函数详细过程)。磨损均衡子系统初始化将PEB在UBI中分为四种情况,分别是正在使用、空闲、需要擦除和损坏。每个状态的PEB在不同的红黑树中进行管理。在ubi_eba_init()函数中,首先会分配一个structubi_wl_entry指针数组,存储在sructubi_wl_entry**lookuptbl字段中。数组的下标是PEB的编号。数组内容记录了PEB的擦除次数和次数信息。每个PEB都有这样一个结构,对应下图。此外,每个PEB也根据状态放入不同的红黑树中进行管理。上图展示了used、free、scrub三种状态下的红黑树。红黑树按照擦除次数的顺序排列,擦除次数最少的排列在最左边。如果擦除次数相同,比较PEB的个数。较小的数排列在树的左侧,对应的值为structubi_wl_entry的指针数组中的一个元素。调用ubi_eba_init()函数后,磨损均衡子系统被初始化,内存中会形成上图中的数组关系。经过前面UBI层操作的初始化,各个数据的结构关系已经存储在内存中,所以UBI层的操作实际上就是对这些数据在内存中的操作。从用户空间来看,UBI初始化后会对应三种字符设备,分别是/dev/ubi_ctrl、/dev/ubix(x=0,1,2...)、/dev/ubix_y(x=0,1,2...,y=0,1,2),它们对应的运算函数如下。ubi_vol_cdev_operations:针对某个卷(/dev/ubi1_0等)进行操作。从volume来看,只能看到它包含的PEB,所以它的操作也是围绕PEB进行的。ubi_cdev_operations:它在UBI设备(/deb/ubi0等)上运行。从UBI设备的角度,可以看到不同的卷,因此可以创建、删除和扩展卷。ubi_ctrl_cdev_operations:它是针对UBI层(/dev/ubi_ctrl)的操作。从这个角度可以看到UBI设备,所以可以创建和删除UBI设备。举个例子需求:如果我们想对/dev/ubi1_0进行扩容,应该怎么操作呢?用户空间将volume_id和size参数传递给内核空间。在内核空间,我们使用structubi_volume*volumes[]数组中找到的volume_id的handler需要扩展(分配更多的LEB),所以需要重新分配structubi_eba_table*eba_tbl数组,并且旧数组中的数据被复制到新数组中。在树上申请,建立LEB到PEB的映射关系,保存到structubi_eba_table*eba_tbl数组中。此外,还需要更新PEB中的ech和vidh,指明PEB属于哪个卷。以上一系列操作是我自己的想法,并不是内核实现的。代码(具体实现可以参数ubi_cdev_ioctl()函数)。这里想表达的是,UBI初始化完成后,内存中已经存在各个volume和各个LEB/PEB的关系。因此,理论上我们可以完成UBI的运行。唯一的区别是代码实现;程序=算法+数组结构。这里的数组结构已经存在,算法就是UBI层的各种操作。这里的代码其实大家都可以实现,只是有好有坏。好在内核已经帮助我们实现了,我们可以参考学习。其实别人写的文章只能提供一个大概的思路,真正的细节只能在源码中获取。擦除和写入平衡闪存块的使用寿命有限。如果flash的某个PEB被频繁擦除,这个PEB很快就会损坏。擦写平衡的目的是将擦除操作平均分配到整个闪存,从而提高闪光灯的使用寿命。那么如何将擦除操作均匀分布到整个flash上??呢?这个条件还是比较难实现的,所以我们退一步修改条件,最大擦除次数和PEB最小次数之差小于一定值。例如flash中有20个PEB,数字表示PEB被擦写的次数。我们约定擦写次数最多相差15次。阈值,所以我们需要想办法增加擦除次数为10的PEB被擦除的几率,减少擦除次数为39的PEB被擦除的几率,使整个flash的擦除次数趋于要平均。具体实现后面会介绍。擦写时序linux内核会在以下两个地方调用擦写均衡:wear-leveling子系统初始化时会检查是否需要擦写均衡。这是一个初始状态,也是一个检查的机会。当要擦除某个PEB时,此时擦写次数会增加,可以满足擦写平衡的要求。这也是一个检查的机会。擦写条件除了上面的调用时序外,还有一些擦写平衡的条件。在遍历flash的每一个PEB时,如果发现从flash读取的数据中有bitflip,就会添加一个scrubflag,放到scrub红黑树上进行维护,说明PEB需要待擦除;擦写均衡时,先取出scrub树最左边的节点e1,然后从空闲树中找一个合适的节点e2,然后读取e1的PEB对应的数据,如果还有读取数据有问题,程序结束,没有问题,将e1数据复制到e2位置,擦除e1数据,完成本次擦写均衡操作。当scrub树上没有节点时,从已用树中取出最左边的节点e1,从空闲树中寻找合适的节点e2,然后检查e2和e1的PEB擦除次数是否不同大于阈值,如果大于,则将e1数据复制到e2位置,擦除e1数据,完成本次擦除。为什么要这样做,原因是usedtree中的节点已经初始化(先整体擦除,然后写入ech和vidh,后面再写入data,不擦除)所以不会有erase操作,freetree中的节点above在使用前需要擦除一次,所以把擦除次数多的PEB放在usedtree上,减少被擦除的机会,把擦除次数少的node放在freetree上,增加次数的擦除机会,从而达到擦写均衡的目的。此外,在空闲树上选择一个合适的节点。什么是合适的节点?最简单的方法是从freetree的最右边取出一个节点(擦除次数最多的节点),然后将其与usedtree中移除最多的节点进行比较。左侧的节点进行比较以查看差异是否超过阈值。但实际情况可能更复杂。下面的代码第29行是内核中在空闲树上选择节点的方法。它将最大擦除次数限制为空闲树的最左边节点+WL_FREE_MAX_DIFF。看到上面的评论说在某种情况下,一个或几个PEB会被不断的擦除和写入,所以做了这样的限制。(没想到是什么情况??)如果你觉得现在走路很吃力,那证明你是在上坡。尹仲凯,Linux内核爱好者,2017年6月毕业于杭州电子科技大学。现就职于北京地平线信息技术有限公司系统软件工程师,主要负责SPI、I2C、OSPI、DMA等模块代码注意。转载本文请联系Linux代码阅读领域公众号。
