UBIFS设计简介

原文:

A Brief Introduction to the Design of UBIFS
Document version 0.1
by Adrian Hunter 27.3.2008

链接:http://www.linux-mtd.infradead.org/doc/ubifs_whitepaper.pdf

        为Flash而开发的文件系统必须具有异地更新(out-of-place updates)的特性。这是因为Flash在写之前必须擦除,是典型的写完之后再写就必须重新擦除。如果擦除的块较小就能够快速擦除,然而并非如此他们必须被化作磁盘扇区来对待。读取一个完整的擦除块(eraseblocks),然后擦除该块,最后将更新的数据写回到Flash;这样的时间大约是是简单的将数据写回到已经擦除的块的100倍。换句话说,对于一小块数据的更新,就地更新(in-place updates)的时间是异地更新(out-of-place updates)的百倍多。

        注解:就地更新(in-place updates):是指将数据更新回Flash的操作;异地更新(out-of-place updates)是指在文件系统内,内存中所做的数据更新

        异地更新要求具有垃圾收集(garbage collection)机制,作为异地更新的擦除块数据包含有有效数据或者是在其他地方更新过的废弃数据(obsolete data)。最后文件系统将耗尽所有的空擦除块,以至于每个擦除块都包含有效数据和废弃数据。为了写入新的数据,擦除块必须保持是能被擦除和重新使用的空状态。识别擦除块包含废弃数据的过程并将有效数据移动到另一个擦除块的过程就叫做垃圾收集(garbage collection)。

        垃圾收集借助于node-structure数据结构,为了垃圾收集一个擦除块,文件系统必须能够识别存在那里的数据,这与文件系统通常面对的索引问题是相反的。文件系统通常开始于文件名并去找到属于那个文件的数据。垃圾收集开始于属于任何文件的数据或者不属于任何文件的废弃数据,最好的解决方法是为一个文件数据存储一行元数据(metadata),关联元数据和数据的结构体叫做node。每个node记录着哪些node属于这个文件(更多特别的inode号)和哪些数据(比如文件偏移和数据长度)在这个node内。JFFS2和UBIFS遵循node-structured设计,允许垃圾收集直接读取擦除块并决定哪些数据需要被移动和丢弃,最后相应地更新他们的索引。

        JFFS2和UBIFS最大的不同在于UBIFS存储索引(index)在Flash上,而JFFS2存放在主存中并在mount文件系统的时候重建。对于存放很大的JFFS2文件系统来说会受到最大size的限制,这是因为mount的时间和使用的内存与Flash的大小成线性关系。UBIFS正是克服了这个限制。

        不幸的是在Flash上存储index非常复杂,这是因为index必须异地更新。当一部分index异地更新了,那么其他引用该部分的index也要被更新,如此更新下去。解决这种看似无限制的级联更新的数据结构叫做游离树(wandering tree)。

        wandering tree(一种B+树)仅仅叶子存放数据,他们是文件系统有效的节点,而树的中间元素是为了引用他们儿子的索引节点。因此索引节点(index node)记录子节点在Flash上的位置。UBIFS的wandering tree可以视为两个部分:上面部分是构建树结构的索引节点,下面部分是包含实际文件数据的叶子节点。文件系统的更新包含建立并添加一个新的叶子节点,以及替换进wandering tree。为此父索引节点也必须更新,一直替换到树的根节点。替换的节点数量等于树的高度。剩下的问题是如何知道哪个节点是树的根节点?UBIFS的做法是将根节点存放在主节点(master node)上。

        主节点存放Flash上没有特定逻辑的位置,它被重复的写入逻辑擦除块(logical eraseblocks,简称LEBs)的1和2中。LEBs是被UBI抽象出来的,也被UBI映射到物理擦除块(physical eraseblocks,简称PEBs),因此LEBs1和2可以在Flash媒体上(UBI device)的任何位置,但是UBI会去记录他们存放在哪里。这两个擦除块是主节点的两个拷贝,互为备份关系,这样做的目的是为了能够在下面所说的两种导致主节点的损坏和丢失的情况下恢复。第一种情况是在写主节点的同一瞬间掉电,第二种情况是Flash本身的退化和损坏。第一种情况是尽可能被恢复的,因为主节点的前一版本是能够使用的,第二种就难说了,因为不能确定哪个版本是有效可靠的。在后一种情况下,用户空间程序可以分析媒体上的所有节点,并试图修复和重建损坏和丢失的节点,具有两个副本的主节点能够尽可能的判断是哪种情况的发生,并作出正确的响应。

        (2012/12/2  23:32翻译前一段,未完待续)

        第一个LEB是LEB0,存放超级块节点(superblock node),包含改变很小的文件系统参数。比如Flash的擦除块大小,擦除块数目等存在超级块节点。目前只有自动大小调整的一种情况导致超级块节点重写,UBIFS调整自动变大的能力很有限,只能在建立文件系统的时候指定最大的大小。这样做的原因是因为Flash的分区实际大小受坏块的影响。mkfs.ubifs用来创建文件系统image,image会记录指定的最大擦除块数,在超级块节点记录实际使用的擦除块的数目。当UBIFS挂在UBI volume上的时候,如果需要的超级块的数目介于超级块节点记录的数目和超级块节点记录的最大擦除块数目之间,UBIFS文件系统会自动调整大小以适应UBI Volume。

        实际上在UBIFS创建的时候会确定6个区域,前两个区域已经被描述,超级块区域(superblock area)在LEB0,超级块节点通常在偏移0,超级块的LEB是用UBI原子的change函数来写的,这样可以保证LEB更新成功或者一点不改变。接下来是主节点区域(master node area.),存放在LEB1和LEB2,通常这两个LEB包含相同的数据,主节点在这两个LEB上顺序写入知道没有空的区域,某一处没有映射到一个擦过的LEB,UBI会自动映射一个擦过的LEB并将主节点会写在偏移0的位置。注意主节点的LEB不会同时未映射,否则会导致文件系统没有有效的主节点。剩下的UBIFS区域分别是:log area,LEB properties tree (LPT) area,orphan area,main area。下面将分别介绍这些区域:

         Log日志是UBIFS的一部分,UBIFS日志是为了减少更新Flash索引的次数,回顾一下,这个索引包含只有索引节点组成的wandering tree的上部分和包含实际数据的叶子节点,更新和替换叶子节点的数据必须相应的更新所有父索引节点。这样每次一个叶子节点的数据写入会导致因flash上的索引更新而效率很低,由于很多相同的索引节点要被重复写入,尤其是朝向树顶的索引节点。相反UBIFS定义的日志会使叶子节点的写入不会立即更新到Flash上的index。注意内存中的index是立即更新的,当日志适当的满的时候,系统会周期性的提交日志,该提交过程将新的索引版本和相应的主节点写入。

         存在的日志是指UBIFS已经mount了,Flash上的index已经异地更新了。为了更新到最新,日志中的叶子节点必须被读出来重新索引。这个过程叫做重现(replay),重现日志,注意日志越大,重现的时间越长,UBIFS mount的时间也更长。另一方面,日志越大,说明提交的次数越少,也使文件系统更具效率。mkfs.ubifs会设置日志文件的最大size参数,可以选择性的满足系统的要求,然而默认的UBIFS不采用快速卸载(fast unmount)的选项,相反在卸载前执行提交日志操作。这样会使文件系统再次mount的时候日志文件为空,使得mount的速度很快,提交操作本身也很快,只需要数秒,这将是一个很好的平衡(trade-off)。

        注意提交过程不能从日志里移动叶子节点,相反是移动日志的记录位置。日志包含两种节点:提交开始节点(commit start node),和引用节点(reference nodes),它记录着主区域LEBS的数目,弥补日志的其余部分。这些LEBs叫(buds),所以日志文件包括LOG和BUDS。有限的Log的大小可能被考虑为通知缓存(circular buffer)。提交之后,日志中记录先前节点位置的引用节点不再需要,所以Log头延长到尾被以相同的速率擦除。提交开始节点记录着提交的开始,而主节点被写入的时候定义提交的结束,这是由于主节点指向一个log结尾的新位置。如果提交没有完成(比如由于掉电导致的文件系统未彻底卸载),重演过程将重演新旧日志。

        重演过程(replay process)在一些问题上是复杂的,首先是叶子节点必须顺序的重演,由于UBIFS使用多头日志(multiheaded journal),叶节点的顺序并不是log中bud擦除块引用的顺序。为了排序叶子节点,每个节点包含64位的序列号,随着文件系统的生命周期增加。重演会首先读取日志中的所有叶子节点,然后以排序的序列号的方式放在RB树中,然后顺序的处理RB树,在内存中的索引也会相应的更新。

         另一个复杂的是必须处理删除和截断,有两种删除:Inode删除(文件和目录),与断开链接和重命名相关的目录项删除。UBIFS的inodes与记录目录项链接数的inode节点相关,即链接数。一个节点的删除导致inode节点的链接数为0则会写入日志。这种情况会删除所有节点索引项中哪儿inode号。删除目录项的情况会是将目录项节点写入日志并将目录项先前的inode号置为0。注意目录项相关的有两个inode号,父目录inode号和文件inode号或者子目录的,之后删除的目录项节点inode号都被设为0 。当重演一个inode号为0的目录项时候,会从索引上删除目录项。

         截断是改变文件的长度,既可以增加也可以减少,在UBIFS扩展文件长度不需要特殊的处理,只需要建立设为0字节的未写文件部分的“空洞”即可,UBIFS不为这些洞索引,也不为其存储节点。相反洞是一个不存在的索引目录,当UBIFS查找索引节并找到没有索引目录时候,会定义一个洞并相应创建0数据,另一方面截断会减少一个文件的长度,在即将成为新文件之外的数据都会被移除索引,为此会写入日志记录新旧的文件长度,可以重演删除的索引项。

         再一个复杂的地方是重演必须更新LEB属性树(LPT),它有三个值需要被主区域的所有LEBs知道:剩余空间,脏空间和擦除块是否是一个索引擦除块。注意同一个擦除块不会掺和索引和非索引节点,因此一个索引擦除块仅仅包含索引节点,非索引擦除块仅仅包含非索引节点。剩余空间是擦除块末尾未写入的字节数,还可以填充更多的节点。脏空间是被丢弃和填充的字节数,能被垃圾收集机制重新使用。LEB属性本质上是找空间增加日志,索引,和找到脏块给垃圾收集机制处理。每当一个节点被写入,擦除块的剩余空间就会减少,每当一个节点被写入丢弃或者填充,被写入截断或者删除,擦除块的脏空间就会增加。当一个擦除块申请给索引,就必须被记录,比如一个索引的擦除块的剩余空间未被申请给日志,会导致索引和非索引混合的状态。接下来讲进一步描述索引和非索引不能混合的原因。

        通常来讲索引子系统只关心LEB属性子系统的改变,其复杂在于重现时垃圾收集擦除块增加到到日志导致LEB属性的增加。如索引一样,LPT区域的更新也在提交的时候,flash上的LPT在mount的时候就已经过期,在重演过程中才被更新。所以在flash上的垃圾收集LEB属性反应的是最后提交的状态。重演将更行LEB属性,有些改变是在垃圾收集之前,而有些是在之后。依赖于垃圾收集的发生点,最终的LEB属性的值也是不同的,为了处理这种不同,重演会在RB树上插入一个引用来表示这个点,这个LEB将加入日志(使用顺序的log引用节点号),重演RB树应用索引会正确调整LEB属性值。

        另一个重演的复杂性在于恢复的效果,UBIFS在主节点上记录着文件系统卸载的是否彻底,如果不彻底会有一个错误条件触发恢复去解决这个问题,重演会影响两个方面:(1)破坏bud擦除块,它在不彻底卸载的时候被写入。(2)同样的原因导致log擦除块被写。如果文件系统以RW方式mount,恢复要在flash上做必要的修复,这样整合恢复的文件系统就像没有遭遇不彻底卸载的一样。如果文件系统是只读方式mount的,恢复会推迟到读写方式moount。

        最后的复杂地方在于叶子节点的引用不再在flash上索引,发生在节点被删除和包含它们的擦除块随后被垃圾收集处理了。通常删除叶子节点不会影响重演过程,因为他们不是索引的一部分。然后索引结构一方面有时候需要读叶子节点来更新索引。主要发生在目录项节点和外部属性项节点。UBIFS的目录包含inode节点和每个目录项的目录项节点。访问索引使用节点key(64-bit值表示节点)。大部分情况节点key唯一表示节点,所以index可以使用key来更新。不幸的目录项和外部属性项仅仅表示名字信息(最多255个字节)。却被压缩到64 bit内,名字被hash成29 bit,并非指向同一个名字了(hash冲突)。这样存在叶子节点的名字就必须被读用来解决冲突,如上面所说会发生叶子节点消失,但是证明了没关系。目录项节点和外部属性项节点从来不会被增加和删除,这是因为key值不变。所以名字比较的结果是肯定的,即使其中一个名字消失了。当增加hash值的节点,将会没有匹配。当删除一个hash值节点,将会一直被匹配,可能是存在的节点,也可能是有正确键值的丢失节点。为了重演的时候提供正确的索引更新,函数分割集被使用(在code上带有fallible前缀)。

        Log区之后是LPT区,Log区的大小是在文件系统的创建和开始于LPT区。目前LPT区域的大小是在文件系统创建的时候在LEB大小和最大LEB数目基础上自动计算出来的,就像Log区域,LPT没有无限的空间,与Log区不同的是更新LPT不是顺序的而是随机的。此外LEB属性数据量潜在是非常大的且访问必须是可扩展的。解决方案是wandering tree存储LEB属性。实际上LPT区域就像一个自主的微型文件系统,包括LEB属性(ltab),有自己的垃圾收集格式,有自己的节点结构,打包节点的位域。然而如index一样,LPT区域只在提交的时候更新,flash上index和LPT表现的是最后的提交状态内容,与实际文件系统状态不同的是日志中的几点表示不同。

        LPT根据两个些许的不同划分为小模型(small model)大模型(big model)。小模型用在全部LEB属性表写入单个擦除块中,LPT垃圾收集仅仅包含写入全部表,从而使LPT区域其他擦除块重新使用。大模型,LPT垃圾收集机制选择脏LPT擦除块,使得那个LEB节点为脏,然后如提交的一部分将脏节点写出,此时LEB号的表被保存,因此整个LPT在UBIFS首次mount的时候不再扫描查找空擦除块。而在小模型中被认为扫描整个表会很快。

        UBIFS的一个主要任务是访问wandering tree的索引,为了更具效率,索引节点被缓存到内存中,这个结构体叫树节点缓存(tree node cache ,TNC),这个树是一颗B+树,node-for-node如同flash上的索引一样,任何增加和改变都会在最后一次提交生效。TNC的节点叫做znodes.它在flash上叫做索引节点,在内存中就叫znode。初始化没有znode,当查找完index,将需要读的索引节点增加到TNC作为znodes。当znode需要改变的时候,它将标记为脏,直到下一个提交为干净的时候。UBIFS的内存收缩将释放TNC中干净的znode,因此需要表示部分索引在内存中的大小。此外挂在TNC底层的叶子节点缓存(leafnode cache ,LNC)仅仅对目录项和外部属性项有效。LNC在冲突解决或readdir操作才需要缓存节点读,这样做是因为LNC附在TNC上,能够在TNC收缩的时候具有效率。

         TNC复杂之处在于期望做提交的时候对UBIFS的其他操作影响甚微,因此将提交划分两部分:(1)提交开始(commit start),为写down一个commit semaphore,将会阻止日志的进一步更新,与此同时TNC子系统列表出脏znodes并列出他们将要写的flash上的位置,然后release commit semaphore,如果提交正在进行,新的日志将开始写;(2)提交结束(commit end),TNC写一个没有lock的新节点,同时会向flash上写入一个新的索引,在znodes提交时结束(写时复制copy-on-write),如果被提交的znode需要改变,它的备份会在未改变的znode上见到,此外提交过程大都运行在UBIFS后台线程上,用户操作尽可能小的等待commit。

          注意LPT采用TNC同样的提交策略,他们都是wandering tree(B+ tree),导致LPT和TNC的code具有相似之处。

          UBIFS和JFFS2的三个重要不同:(1)UBIFS有flash上的索引,具有可扩展性,JFFS却没有。(2)UBIFS运行在UBI层和MTD子系统上,而JFFS2直接运行在MTD上,UBIFS得益于UBI使用的flash空间、内存和其他资源的上负载平衡和UBI错误处理。(3)UBIFS允许回写(writeback)。

          回写是VFS的属性,它允许写的data被缓存在内存中,并不直接写入存储介质,这样使得系统对相同文件统一更新的响应更具效率。较难的地方是要求文件系统知道有多少可用的空间,因此缓存从不会比介质空间大。这个是UBIFS很难决定的,因此整个子系统被称为预算(budgeting)来处理。其困难有以下几个原因:

           (1)UBIFS支持透明压缩(transparent compression),先前并不知压缩容量,所需空间先前也未知。预算机制必须假设最坏的情况,并假设没有压缩,然而很多情况下这是最弱的假设。为了克服这个缺点,预算机制会在侦测到空间不足的时候强制回写。

           (2)预算机制的困难在于垃圾收集机制不能保证回收利用脏空间。UBIFS的垃圾收集机制一次操作一个擦除块,对于nand flash仅仅在Nand的pages被一次写入时,Nand擦除块是一些固定数目的页,UBIFS把每个页的大小叫做最小的I/O单元。这正是因为UBIFS的垃圾收集一次处理一个擦除块的原因,如果脏空间小于最小I/O的大小,就不能被回收利用,它将最终作为nand最后一页的填充。当脏空间在一个擦除块中小于最小I/O大小,这个不能被回收利用的空间就叫做死空间(dead space)。

           与dead space相似的是暗空间(dark space),它是脏空间在一个擦除块中小于最大节点大小的空间。最坏的情况是文件系统可能充满节点的最大大小,垃圾回收机制不会导致能够足够大到另一个最大的节点大小的空闲空间。因此在最坏的情况下,dark space是不可回收利用的,最好的情况下是可回收利用的。UBIFS的预算机制必须假设这种最坏情况和不可能同时出现dead space和dark space。然而如果有很多dark space却空间不足,预算机制将自己运行垃圾回收机制显示回收的到更多空闲空间。

          (3)预算困难在于缓存数据可能会丢弃存在flash上的数据,无论这种情况是否可知,压缩的不同可能确实不知。这是预算机制计算空间不足强制写回的另一个原因。仅仅在尝试写回之后,垃圾收集机制和提交日志将预算放弃和返回ENOSPC(the no space error code)。

            当然这意味着UBIFS变得如同文件系统接近满时不富有效率。事实上,所有的flash文件系统在flash满的时候都不富有效率。这是因为这不可能是后台已经擦过的空擦除块,更可能运行垃圾收集机制。

          (4)预算对于删除和截断是困难的,因为他们需要写入一个新的节点。因此如果文件系统真的缺乏空间,它将不可能删除任何东西,因为没有空间再写入删除的inode节点或截断的节点。为了防止这种情况的发生,UBIFS通常为删除和截断保留一些空间。

            下一个UBIFS区域是孤区(orphan area),它是一个链接数为0且已被提交到索引的inode号。它发生在打开一个已经删除的文件并执行的时候,自然情况下inode会在关闭文件的时候被删除。然而在未彻底卸载的情况下,orphans需要被计算,其节点也必须被删除,这就意味着要么扫描整个index重新查找,要么在flash上某个地方保留列表,UBIFS实现后者。

            孤区是固定数的LEBs,位于LPT区域和主区域之间,具体LEBs数目在文件系统创建时候指定,最小的数目为1。孤区大小需 能够保持期望从未一次存在的最大数目的orphans。一个LEB满足的orphans数为:(leb_size-32)/8。比如15872个字节的LEB满足1980 orphans,因此1LEB就够了。

            orphans在一颗RB树上累计计算,当一个inode的链接数为0的时候,inode号被添加到RB树中,在inode节点删除的时候删除。任何在orphan树的新orphans在提交的时候运行,并被写入orphan区域中1个或多个orphans节点。如果orphan区域已满,它将合并区域。验证阻止用户建立大于最大允许orphans数使其通常能有足够的空间。

            最后一个UBIFS区域是主区域(main area),它包含组成文件系统数据和索引的节点。一个主区域LEB可能是一个索引擦除块或未索引的擦除块。一个未索引擦除块可能是个bud(部分的日志)或者已经提交了,bud可能是当前一个日志头。如果有空闲空间,包含提交的节点的LEB也可成为bud。因此bud LEB在日志节点开始处偏移,尽管这个偏移为0。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值