现在的位置: 首页 > 综合 > 正文

UBIFS设计简介 – A Brief Introduce to the Design of UBIFS

2012年12月13日 ⁄ 综合 ⁄ 共 12053字 ⁄ 字号 评论关闭

项目闲暇,想了解下UBIFS,就先从UBIFS的设计文档翻译开始吧,以后有机会有时间能分析下UBIFS源码

 

flash memory文件系统需要异地更新(out-of-place updates). 这是因为flash存储在写之前必须擦除, 并且每次擦除后只能写一次。如果擦除块很小并且擦除速度很快,那么擦除块可以看作是磁盘扇区,然而事实并非如此。读取整个擦除快,擦除它然后回写更新的数据, 与写更新的数据到一个已经擦除好的擦除块相比可能需要花费100倍甚至更多的时间。换句话说,原地更新(in-place-updates)要比异地更新慢很多。这主要是flash memory的擦除过程非常消耗时间。

 

异地更新引入了垃圾收集技术。数据在异地更新,那么原始数据所在的擦除块就包含了废弃的数据。最终,文件系统必然会消耗掉空的擦除块,同时一些擦除块包含了废弃的数据。为了能够写入新的数据,就要回收这些包含废弃数据的擦除块。定位包含废弃数据擦除块,移动有效数据到其他擦除块的过程称为garbage collection

 

垃圾收集需要借助于node-structure. 为了能够garbage colleciton一个擦除块, 一个文件系统必须能够标识存储在擦除块上的数据。 这和文件系统常用的索引问题正好相反。文件系统是通过文件名字找到属于文件的数据, 而garbage collection则是通过data找到所属的文件(或者不属于文件)。解决这个问题的办法就是把文件metadata和data保存在一起。 metadata和data的复合体我们称之为node. 每一个node记录了那个文件拥有这个node,  以及什么数据(比如data所在文件的offset,
data 长度等)保存在这个node中。 JFFS2和UBIFS都利用了基于node的设计。这使得他们能够直接读取eraseblock来决定哪些数据需要搬运到其他eraseblock,哪些数据可以抛弃。

 

JFFS2和UBIFS的最大不同是: UBIFS存储文件index在flash的某处;而JFFS2则是存放这些索引到内存中(内存index的建立是在文件系统mount阶段进行的),这导致了JFFS2文件系统支持的最大尺寸限制,因为mount时间和内存消耗是和flash memory的大小成正比的。而UBIFS正是为克服这个限制设计的

 

不幸的是,储存index到flash memory中是非常复杂的,因为index本身也不得不out-of-place update. 当一部分index需要异地update, 那么引用update部分的的index也必须update并且也是异地update. 解决这种传导updates的办法是使用wandering tree

 

UBIFS的wandering tree是使用B+树, 仅仅树的叶子节点保存文件信息,他们是文件系统的有效数据, 而树的内部元素则是index nodes。 因此一棵wandering tree包含了两个部分,树的内部节点是文件树的结构,而树的叶子节点则保存了文件的有效数据。对文件系统的更新包含了创建新的叶子节点并加到树中,或者替换wandering 树中的叶子。 对叶子的更新,必然要替换parent index节点,并且一直传递到树的root node. 要更新的index nodes数目等于树的高度。
这也引出了另外一个问题,root node的位置,在UBIFS中, root index node被存储在master node中

 

(继续)

master node用来记录flash结构的位置,而这些结构的逻辑位置又不是固定的,这样就可以通过master找到这些结构。mast node本身是保存在逻辑擦除块(LEB) one 和two中的。 LEB由UBI提供到物理擦除块的映射,因此理论上LEB one LEB two可能在flash介质的任意位置(严格的讲,是UBI device)。使用两个擦除块是为了保持master node的备份,以便能够进行recovery。有两种情况可能导致master node的损坏。第一种是在写master node
的瞬间出现了掉电;第二种情况是flash介质本身会出现退化损坏。第一种情况recovery过程可以使用前一个版本的master node; 而第二种情况recovery 无法确定哪一个是有效的master node 版本。对于后一种情况,可以通过一个用户空间的工具来分析介质上的所有nodes来修正或者重新创建损坏或丢失的node. master node的两个copies可以帮助确定发生了什么情况。

 

第一个LEB不是LEB1, 而是LEB0。LEB0保存着superblock node. 超级块节点保存着文件系统基本不变的参数。比如,flash的布局(删除块尺寸,删除块数目等)。当前仅有一种情况,超级块node可能需要重写:自动resize。UBIFS当前可以有条件的resize, 能够resize的最大尺寸是在文件系统创建的时候就标识的。之所以需要resize机制,是因为flash分区的确切尺寸随着坏块的存在可能发生变化。所以当使用mkfs.ubifs创建文件系统时,擦除块的最大数目和已使用的擦除块数目记录在superblock
node中,当UBIFS被mount到一个分区上,如果分区的擦除块数目大于记录在superblock node中的已使用数目并且小于擦除块的最大数目,那么UBIFS文件系统自动resize到分区的实际尺寸

 

实际上在UBIFS文件系统创建时有6个区域位置是固定的:

1. Superblock节点 LEB0

2. Master节点 LEB1 LEB2, 正常情况下,这里个擦除块保存相同的数据

3.  log area

4. LEB properties tree area

5. orphan area

6. main area

前两个区,已经在上面描述过了。超级块是LEB0,超级块节点一直在offset0,超级块LEB使用UBI's 原子LEB change 函数,确保这个LEB成功更新或者不变。下一个区域是master node区。它占据LEB1 LEB2。一般来说,这两个LEBs保存着相同的数据。对master node的写操作是LEB顺序的位置直到没有空闲空间,此时LEBs被重新unmapped然后从offset 0开始继续写(这个过程UBI会重新map一个干净的erased LEB)。注意master node LEBs不能同时unmapped因为这将使文件系统暂时没有有效master
node。

Log 是UBIFS日志的一部分。UBIFS日志是用来降低flash index的更新频率。回忆一下, wandering树的上面部分,也就是index nodes存放这些index. 更新文件系统的leaf node必然要增加或者替换wandering树,同时还要更新所有的父节点。每次写leaf node都立刻更新flash上的index node, 这种更新的效率是非常低的。 因为相同的index nodes可能会被重复的写,特别是树的上层节点。UBIFS通过日志,仅仅写leaf nodes但并不立刻更新on-flash的index.
注意memory中的index是立刻更新的。 周期性的,当日志系统认为日志足够满时,进行提交。提交过程包括写入memory index以及相应的master node

 

日志的存在意味着当UBIFS被mounted后,flash上的index是过期的,为了得到最新的index, 日志中的leaf node必须被读出来然后reindexed. 这个过程称之为replay。注意日志越大,replay需要的时间越长,mount所花费的时间越长。另一方面,日志越大需要提交的频率越低,文件系统也更高效。日志的尺寸可以通过mkfs.ubifs参数确定,因此可以根据需求调整文件系统。缺省情况下UBIFS不使用fast unmount选项,而是在unmounting前执行一次提交。这就促使日志几乎为空,这样下一次文件系统重新mount时,mount速度非常的块。commit的过程本身很快,大概需要几分之一秒,因此unmount
时commit是一个很好的权衡。

 

注意commit过程本身不会从日志中移动leaf节点。 而是修改日志本身,也就是修改日志的记录位置。log包含两种类型的节点,commit start node 记录一个commit已经开始;reference nodes 记录组成Journal的LEBs的序号。这些LEBs叫做buds, 所以说日志包含log 和 buds; log 的尺寸是固定的可以认为是一个circular buffer。提交后,前一个reference nodes不再需要

After a commit, the reference nodes that recorded the previous position of the journal are no longer needed so the tail of the log is erased at the same rate that the head of the log is extended. While the commit-start node records the start of commit, the
end of commit is defined to be when the master node is written, because the master node points to the new position of the log tail.

如果由于系统掉电等导致的系统unmounted uncleanly导致的提交没完成,那么replay过程replay新旧日志

Replay process很复杂,表现在以下几个方面。

第一, 叶子节点必须按照顺序replay。因为UBIFS使用了multiheaded joural。叶节点的顺序并不是log中bud擦除块引用的顺序。 为了对叶子节点排序, 每个节点包含了一个64bit的序列号。replay首先读日志中所有的叶节点 按照这个序列号插入一个RB树,然后按照顺序处理这个RB树对in-memory index进行更新。

 

另外一个复杂是replay必须考虑删除和truncation的情况。有两种类型的删除:1. inode删除(对应文件和目录的删除);2. 目录项的删除对应着unlinking和重命名。UBIFS inode记录在inode node中,记录着目录项的links number或者叫links count。当删除一个inode时, links count为0的inode node写入到日志中。 对于1,instead of adding that leaf node to the index, it is
removed from the index along with all index entries for nodes with that inode number.  对于2,一个directory entry node被写到日志中,但是目录项内的inode number 被设置为0, 注意一个目录项有两个inode number,一个是父目录的inode number, 另外一个是目录项对应文件或目录本身的inode number。当replay 过程碰到一个inode number为零的目录项时,删除目录项的index而不是加入它

 

Truncate会改变文件的长度。事实上,truncate除了能减少文件长度也能够扩展文件的长度。 对于UBIFS文件系统,扩展文件长度不需要额外的处理。对于文件系统而言,通过truncate来扩展文件长度就是在文件中创建hole, 这些hole被假定内容都是0。UBIFS不index这些holes也不为这些holes保存任何nodes。当UBIFS查找index时发现没有index,那么就意味着这是一个hole。另一方面,如果truncate减少文件长度,那么所有落在新文件长度外的data nodes都要从index中移除。为了处理这种truncate,truncate
nodes被写到日志中记录新旧文件长度,replay处理把已删除的data nodes从相应的index entries中移除。

 

下一个复杂是replay必须保证LEB properities tree(LPT)更新。LEB properties 记录main area所有的LEBs的三个属性:free space, dirty space,是否为index eraseblock or not。注意不要把index nodes, non-index nodes和index eraseblock混淆了,index eraseblock的意思是eraseblock仅仅包含index nodes, non-index eraseblock仅仅包含non-index
nodes。Free space是一个eraseblock结尾尚未被写的字节数,这些空间还可以写入更多的nodes.Dirty space是指被废弃的nodes和padding占用的空间,是潜在的可被garbage colleciton回收的部分。LEB属性可以用来发现空闲空间,加到日志中,或者index中,或者找到最脏的擦除块给garbage collect。每次一个node被写时,node所在的eraseblock的空闲空间就要减少。每次一个node被废弃或者一个padding node被写,或者truncate
deletion node被写,所在eraseblock的dirty space必须增加。当一个eraseblock被分配给index,那么必须记录这个信息,一个index eraseblock即便有空闲空间也不应该分配给journal,否则会导致index nodes和no-index nodes混在同一个eraseblock中。原因是budgeting有这个需求,budgeting 将在后面讨论

 

一般来说,index子系统会注意到LEB properties子系统对LEB properties的修改。在gargage collected 把擦除块加到日志过程中,LEB properties会引起的replay复杂性。如同index, LPT area仅仅当commit发生时才会更新;如同index, on-flash LPT从mount时刻开始就是out-of-date,必须要通过replay过程更新。所以garbage collected LEB在on-flash的LEB properties仅仅反映的是最后一次commit后的状态。replay开始更新LEB
properties,然而这些改变有的发生在garbage collected之前,有的发生在之后。依赖于garbage发生的时间,最终的LEB property会不相同。为了处理这种情况,replay插入reference到RB树中,代表LEB被加到日志中的时间点。That enables the replay to correctly adjust the LEB property values when the replay RB-tree is applied to the index.

 

replay的另外一个复杂性是recovery对replay的影响。UBIFS文件系统是否干净的unmounted被记录在master node上。 如果在mount时没有在master node看到这个标记,那么特定的条件触发recovery来修复文件系统。replay受到两方面的影响。第一,一个bud eraseblock可能被损坏 比如eraseblock正在被写时unclean unmount发生了。第二, 一个log eraseblock可能基于同样的原因被损坏。replay把eraseblock传送给recovery来修复这些eraseblock上的node。如果文件系统以可读写方式mount,那么recovery需要做必要的fix,recovered
UBIFS文件系统的完整性如同unclean unmount没有发生一样完美。 如果文件系统是以只读方式mount的,那么recovery被推迟到下一个读写mount

 

最后的复杂性是 on-flash index引用的叶节点可能已经不再存在。 当节点被删除并且包含节点的eraseblock被garbage collected。一般来说,删除的叶子节点不影响replay因为他们不是index。然而有些时候index的确需要读取叶子节点以便更新index. 比如directory entry nodes和extended attribute entry nodes。在UBIFS中,一个目录包含一个inode node和一个directory entry node。访问index是通过使用node
key, node key是64-bit值来标识这个node。在大部分情况下,node key唯一的标识node。所以对index的访问只要借助这个key就可以了, 不幸的是,目录项和扩展属性项惟一的标识信息是名字,可能非常的长(在UBIFS中最多255个字符),为了缩减到64-bit,名字就需要hashed为29-bit的值,当两个不同的名字对应相同hash值时,被称作hash collision。在这种情况下,必须读取leaf node比较保存在leaf node中的名字来解决hash collision。所以当被删除的leaf节点已经由于GC不存在了。It
turns out that it does not matter. Directory entry nodes (and extended attribute entry nodes) are only ever added or removed - they are never replaced because the information they contain never changes. So the outcome of the name comparison is known even though
the node contained one of the names is gone. When adding a hashed-key node, there will be no match. When removing a hashed-key node, there will always be a match, either to an existing node, or to a missing node that has the correct key.  为了提供这种特殊的index updating,
使用另外一套函数

 

log area之后是LPT area. log area的尺寸在文件系统创建的时候就已经固定下来,因此log area起始位置加log area的尺寸就是LPT area的起始位置。当前LPT area的尺寸是根据LEB size以及文件系统创建时的最大LEB数计算的。和log area一样, LPT area永远不会耗尽空间。和log area不同的是LPT area不是顺序的,而是随机的。此外,LEB properties数据可能非常的大,因此要考虑可扩展性。解决办法是储存LEB properties到wandering
tree中。 事实上LPT区更像是一个小规模的文件系统。它有自己的LEB properties - 也就是说LEB properties area的LEB properties 称为ltab。它有自己的garbage collection。它有自己的节点结构,包装nodes尽可能的紧密。然而,和index一样,LPT区仅仅在commit时更新。因此on-flash index和on-flash LPT代表的是文件系统最后一次更新前的状态。和当前文件系统状态的差异,体现在日志内的nodes上。

 

LPT有两种稍微不同的形式: small model和big model。

small model是整个LEB properties table可以写入一个eraseblock。在这种情况下,LPT GC写整个表,因此使得所有其他的LPT区的eraseblocks可重用;big model, LPT GC选择脏的LPT eraseblocks, 这个eraseblock标记那些包含脏nodes的LEB,然后把这些脏节电写出(这也是提交的一部分)。此外,在big model情况下,一个LEB numbers的表被保存起来(保存在什么地方),以便在UBIFS第一次mount后,这个LPT表不需要被扫描。small
model,因为只有表很小,假定扫描整个表是很快的。

UBIFS的一个主要工作是访问wandering tree中的index。为了达到高效,index nodes被cached到内存结构中称为tree node cache(TNC). TNC是B+树就如同on-flash上的index一样,除了包含上次提交以来的所有变化。TNC节点被称作znodes。从另外一个角度看,znode在on-flash称作index node,  index node在memory中称为znode。最初没有znodes,当一个index被lookup,index nodes被读取并且加到TNC中作为znodes.
当一个znode需要修改,他被mark为dirty直到下次提交后再被mark为clean。在任何时候UBIFS内存回收器可以决定释放TNC中的clean znodes,因此所用内存的数目是和当前正在使用的index成比例而不是整个index的尺寸。此外,挂在TNC上的leaf node cache(LNC) 仅仅被目录项和扩展属性项使用。LNC仅仅在需要冲突解决或者readdir操作时需要cache nodes。因为LNC是挂在TNC上,因此在TNC做回收时也能很有效的回收LNC

 

要求TNC在提交时尽量不影响到UBIFS其他的操作 使得TNC有点复杂。提交被分成了两个部分,第一部分是commit start,在commit start时用down获取semaphore获取commit semaphore来防止其他人对日志的更新。同时,TNC子系统生成一个dirty znodes的链表,计算好这些znodes要写到flash上的位置。然后释放commit semaphore, 并启用一个新的日志,此时提交仍在进行中。提交的第二部分是commit end. 在commit end期间,TNC
写新的index nodes而无须任何TNC lock。那是因为TNC可以在新的index被写入flash的同时被更新。在更新期间通过标记这个TNC为copy-on-write。如果一个正在被提交的znode需要被修改,那么copy这个znode,使commit看到是未修改的znode。此外,大部分commit是UBIFS后台线程执行的所以用户线程在commit执行时几乎不需要等待。

 

注意LPT和TNC有相同的提交策略,也是用B+树实现的wandering trees, 因此LPT和TNC有相似的代码

 

UBIFS和JFFS2有三个主要区别:

1. UBIFS有on-flash index,而JFFS2没有,因此UBIFS在这点上是可扩展的

2. UBIFS运行在UBI上面,而UBI又运行在MTD子系统上,而JFFS直接运行在MTD上,UBIFS可以受益于UBI提供的wear-leveling和error handling, 当然也要承担因此带来的flash space,memory和其他被UBI占用的资源代价

3. UBIFS允许writeback写回操作

 

writeback是一个VFS提供的功能,允许数据缓存在cache中,而不是立刻写入存贮介质。这提高了系统更有效因为对一个文件update操作的局部性原理。支持writeback的困难是需要预知文件系统的空闲空间使得cache不会超过这个空闲空间。预知空闲空间对UBIFS是困难的,因此引入了另外一个子系统叫做budgeting。预知的困难主要在以下几个方面

 

第一UBIFS支持transparent compression。 因为压缩导致无法提前预知需要的空间,budgeting 必须按最坏的情况考虑,假定没有压缩存在,然而在很多情况下这个假设很不好。为了克服这种情况,budgeting在检测到空间不足的情况下,将强迫writeback。实际上译者觉得随着flash磁盘空间的增大 这真的不是个问题,只要用没有压缩假设就好了,在这时候去强迫writeback不知道会不会增加复杂性(以后我会研究的),如果答案是yes 那么就假设没有压缩好了。

 

budgeting的第二个难点是garbage collection不能保证可以回收所有的dirty space。UBIFS garbage collection每次处理一个eraseblock。对于NAND flash写的最小单位是NAND pages。当dirty space小于NAND pages时,那么这个空间是不能回收的。当一个eraseblock的dirty space小于最小I/O尺寸时,那么这个空间叫做dead space。 dead space是不可回收的

类色的还有dark space. Dark space 是eraseblock上的dirty space小于最大的node尺寸。在最坏的情况下,文件系统充满了最大尺寸的nodes,GC无法释放出一片空间来存在一个对大尺寸的node。所以在最坏的情况下,dark space是不被回收的,在最好的情况又是可以回收的。UBIFS budgeting 必须假定最坏的的情况,所以dead space和dark space都是认为不可用的。然而,如果空间不够但是有很多dark space, budgeting将运行garbage
collection来看看是否可以回收空闲空间。

 

第三个原因 cached数据可能是flash上已经废弃的数据。Whether or not that is the case is not always known, and what the difference in compression may be is certainly not known. 这也是budgeting 在空间不足时强制writeback的另外一个原因。仅仅在尝试writeback, garbage collection以及提交日志后,budgeting才能决定放弃,并返回ENOSPC

 

当然这就意味着UBIFS在文件系统空间接近满时效率变低。事实上,所有的flash文件系统在flash变满时都会变得低效。可能是因为一个empty eraseblock可能正在后台擦除,当然更大的可能是garbage collection正在工作。

 

第四个原因是deletions和truncations需要写新nodes,因此如果文件系统耗尽空间时,删除变得不可能,因为没有空间写一个deletion inode node或者truncation node. 为了防止这种情况, UBIFS一直保留一部分空间以允许deletions和truncations

 

下面一个UBIFS area是orphan area. orphan是一个inode number,对应的inode node已经提交到index并且link count为0。这种情况发生在删除一个已经打开的文件然后运行commit。在正常情况下inode会在文件关闭时删除。然而unclean unmount的情况下,需要考虑orphans。在unclean unmount后,orphans' inodes必须要被删除,这意味着要么扫描整个index找到他们,或者把orphans保存在flash的某处,UBIFS正是使用后一种方式。

 

orphan区域是介于LPT area和main area之间,由固定数目LEBs组成。orphan area的LEBs数目是在文件系统创建过程标定的。最小数目是1. (保存下,吃饭去) orphan area的尺寸应该保证它能够容纳下系统可能存在orphans的数量。一个LEB可以存放的orphan的数量是:

(leb_size - 32) / 8

比如:一个15872 byte的LEB可以容纳下1980个orphans, 所以一个LEB足够了

 

Orpans存放在RB-tree。当一个inode's link计数变为0时, inode number 被加到RB-tree。It is removed from the tree when the inode is deleted.  Any new orphans that are in the orphan tree when the commit is run, are written to the orphan area in 1 or more orphan nodes. If the orphan
area is full, it is consolidated to make space. Orphan一直有足够的空间,因为代码会检验以确保user创建多于允许数目的orphans

 

 UBIFS的最后一个区是main area. main area包含节点组成文件系统的data和index。一个main area LEB可以是一个index eraseblock或者non-index eraseblock。一个non-index eraseblock可以是一个bud(journal部分)或者已经被提交了。一个bud当前可能是journal heads。一个LEB包含提交的nodes 如果包含空闲空间仍然可以成为bud。因此一个bud LED有一个偏移标识日志nodes的开始位置,尽管大多数情况下offset是0

 

 

 

 

 

抱歉!评论已关闭.