UBIFS文件系统(三)
ubifs文件系统为异地更新,数据总是写到日志区中,只有当page cache中的数据达到一定条件时,才会将日志区的文件数据同步到flash。将日志区的内容同步到flash的操作称之为commit,本文主要对ubifs的commit过程进行描述。commit过程由函数do_commit实现:将处于内存空间中的wbug、log、TNC以及LPT等数据结构依此写回flash,并释放脏数据占据的flash空间,具体参见do_commit的如下函数:
ubifs_wbuf_sync
依次将各个journal head(DATA、BASE、GC)对应的wbuf写回到flash,对于还未填满的wbuf(即wbuf->avail > 0),将其剩余的空间全部填充为0再写回flash。
ubifs_gc_start_commit
与ubifs_gc_end_commit成对使用,完成脏LEB的回收。对于分属不同类别的LEB执行不同的回收策略:
- freeable_list:unmap LEB并更新LPT,同时修改LEB的属性并重新对其进行归类
- frdi_idx_list:将链表中的元素挂接到c -> idx_gc链表,并将其标记为可unmap(并未执行实际的unmap操作),将LEB以unmap之后的状态对LPT进行更新并且重新归类。
属于frdi_idx_list中的LEB不能够直接进行unmap而只是将其标记为可unmap的原因为:frdi_idx_list中的LEB存储的是TNC中被新的索引节点所替换而被标记为脏的索引节点,此时在新的索引节点还未写入到flash之前,不能够将存储脏索引节点的LEB进行unmap操作,否则当在新索引节点写入flash之前,脏索引节点所属LEB被unmap之后发生异常掉电,存储于flash中的索引节点将会缺失导致无法在内存中建立TNC。然而,对于freeable中的LEB,由于在ubifs_wbuf_sync函数之后所有新的数据节点都已经写回到了flash,此时便可以直接对其unmap。
ubifs_log_start_commit
与ubifs_log_end_commit成对使用,用于将log区的内容写回到flash。由前文可知,当一个空闲区域被用作日志区时,表示该区域的lnum和offs将会被组成一个ubifs_ref_node,并被实时的写入到flash,所有的介于两次commit之间的ubifs_ref_node共同组成一次commit需要同步到flash里的日志区。ubifs的log区域并不是只记录一次commit需要同步的ubifs_ref_node,而是包含了多次commit的ubifs_ref_node,并且在LEB中顺序存储。为了区分属于不同commit的ubifs_ref_node,ubifs_log_start_commit将会在log区域的写入一个ubifs_cs_node,由于每一个node中包含一个squm域,该值随着写入LEB的时间增大,可以据此来区分哪些ubifs_ref_node是有效的(commit未完成,数据不可清除),哪些ubifs_ref_node是无效的(commit已完成,数据可被清除)。log区域的数据写过程如下图所示: 为保证空间的可重复利用,即使当前活动LEB并未填满,ubifs仍然索引一个新的LEB写入ubifs_cs_node。此外,由于commit时,write buffer对应的LEB并未填满,所以在commit之后其LEB的剩余空间仍然被作为日志区使用,所以其对应的LEB将在此阶段直接封装成为ubifs_ref_node,并随ubifs_cs_node一起写入flash中。ubifs按顺序依此在log区域中查找空闲的LEB,当查找到最后一个LEB之后,将折回头部开始以环形查找。上图中,假定当前ubifs_ref_node写入的LEB(c -> lhead_lnum)为LEB4,则ubifs_cs_node和三个ubifs_ref_node(由wbuf对应的LEB封装而成)组成的packet将被写入到LEB5,如果此时LEB5为脏LEB(其中的ubifs_ref_node为无效的),将先unmap LEB5再被索引,并同时更新c -> lhead_lnum为LEB5。同理,当本次commit完成由于journal区域的增大,导致LEB5被ubifs_ref_node填满之后,将从LEB3开始查询空闲LEB,此时如果LEB3为脏LEB,则会先unmap再被索引。
commit完成之后,ubifs将本次commit的ubifs_cs_node所在的LEB跟新到c -> ltal_lnum,并同步写回到master LEB中。此举是以防在下次commit之前发生异常掉电时,可以从c -> ltal_lnum(上次commit完成的ubifs_cs_node所在LEB)开始扫描所有的node,根据node的squm来识别ubifs_ref_node的有效性,从而找出还未同步到flash的journal区,进而扫描journal区中的数据node重构TNC树,此过程即为日志的回放(replay)。
此外,ubifs_log_star_commit将bud红黑树中的元素(除当前journal head对应的LEB)标记为可删除,并依此链接到old_bud链表当中。不立即删除这些元素的目的是为了保证在post_log_commit之前这些LEB不能够被GC,可保证在异常掉电时,这些LEB由于没有被GC还可以通过log区进行replay。
ubifs_tnc_start_commit
与ubifs_tnc_end_commit成对使用,用于将内存中新的索引节点同步到flash。实际上,TNC树中保存数据的叶子节点在数据更新的过程中已然实时的写入到了flash(日志区)当中,只是此时由于叶子节点变化而变化的索引节点还仍未写到flash,如果发生异常掉电,从flash中读取到的索引节点仍然是旧的索引节点,因此索引到的叶子节点也是旧的,这也是为什么叶子节点虽然已经写入了flash,但是仍然将其称之为日志区的原因。TNC索引节点回写到flash的过程如下图: 首先,在TNC中查找含脏标志位的索引节点;然后置节点的COW_ZNODE标志位;最后将所有的脏节点通过c -> next串联起来。其中,COW_ZNODE标志位为标识该节点正处于commit过程中,此后其他进程如果有对该节点进行修改的需求,则首先将该节点复制,然后将复制的节点进行修改,最后将原节点替换为该修改后的节点,此时由于原节点已经加入到了c -> next链表中,不会对索引节点的回写产生影响。
回写索引节点时存在两种空间索引方式:layout_in_empty和layout_in_gaps,分别表示从全空的LEB中索引和从已经包含数据的LEB中索引。ubifs总是试图从全空的LEB中索引空间回写索引节点,当没有全空的LEB时才会从已经包含数据的LEB中索引其剩余的空间用于回写索引节点。
在layout_in_emty方式中,全空的LEB的查找顺序为:
empty_list -> freeable_list -> flash
在layout_in_gaps方式中,空间的查找顺序为:
(c -> dirty_idx_arr[]) -> dirty_idx_heap -> frdi_idx_list -> uncat_list -> flash -> (c -> idx_gc)
在ubifs_tnc_start_commit中,只是将需要回写的索引节点置相应的标志位,并未其索引到足够的flash存储空间,并没有实际将其写入到flash当中,该过程将在ubifs_tnc_end_commit中最终完成。
ubifs_lpt_start_commit
与ubifs_pt_end_commit成对使用,用于将LPT写回到flash,该过程与TNC的回写过程类似,只是在回写时查找空闲空间时有所区别。LPT的存储存在big model、small model两种模式,采用何种模式取决于lpt_sz,当lpt_sz < leb_size时,采用small model,反之则采用big model。实际上,由于ubifs本身便是为小容量flash设计的文件系统,一般而言都符合small model的条件,因此下文将主要以small model对LPT的回写进行介绍。lpt区域占用的LEB数量根据mkfs中的定义如下:
lpt_lebs = 4 * ( pnode_sz + nnode_sz + ltab_sz + lsave_sz ) = 4 * lpt_sz
与log区相同,lpt区的LEB同样按照循环顺序的原则进行查找,不同点在于处于log区域中的LEB在一次commit完成之后,除ltail_lnum所指的LEB之外,其他的LEB均可以被unmap 擦除,而lpt中的LEB则需要视情况而定。ubifs只对LPT中含脏标志位的node进行回写,以下图进行说明,此时处于写位置的LEB(nhead)为LEB7,LPT在进行回写的时候需要分两种情况进行处理:LEB6中全部为脏node,则将该LEB标记为可unmap(置tgc标志位);LEB6中仍然包含部分有效node,此时若nhead(LEB7)包含最够的空间(> 2 * lpt_sz),则不做处理,否则将整个LPT中的所有node均置脏标志位,即将LEB6变为可以unmap的LEB,使得有足够的空间写回LPT数据。
ubifs_tnc_end_commit
将所有在ubifs_tnc_start_commit过程中链入c -> next链表的node依此写回到flash。对于前面提到的,在提交过程中由于需要对node进行修改而进行复制的node,此时被链入c -> next链表中的那个node将被置obsolete标志,代表该node的空间在commit完成之后可以被释放。
ubifs_lpt_end_commit
回写脏node,并将置obsolete的node空间释放。
ubifs_log_end_commit
重新初始化c -> min_log_bytes和 c -> bud_bytes两个阈值,当内存空间中的相关数据达到任一一个阈值,将触发下一次commit操作的执行。将ltail_lnum等其他内存数据回写到master区域,用于系统重启时对ubifs系统的内存数据进行恢复和重建。master node的回写过程如下图所示:
ubifs在master区域维护两个LEB,用于数据的相互备份。master区域同样是按顺序对master LEB进行填充,当一个LEB的空间不足以容纳一个master node时,首先将其中一个master LEB进行unmap,然后从该LEB的起始位置写入master LEB,此后将另一个LEB同样unmap,并写入master LEB。ubifs以此种方式保证同一时刻,两个LEB中一定有一份有效的master node存在,防止异常掉电时的数据丢失。正是由于master node采用顺序写的方式,当分区被挂载时只需按顺序对master区域进行扫描,则最后一个master node即为有效node。
ubifs_log_post_commit
在LPT中将所有处于old_bud链表中的LEB的taken标志位清除,并重新归类(所有涉及LPT node的改动都将使其重新归类)。当node被置为taken标志位,则该node所对应的LEB将不会被查找和索引,知道commit完成,使得同一个LEB不会再一个commit周期内被索引两次。将处于old_ltail_lnum和ltail_lnum之间所有的LEB进行unmap清除,以腾出空间供下次commit周期使用(实际上,从之前的分析来看,此时这些LEB已然全部被unmap了)。
ubifs_gc_end_commit
到达此步时,处于TNC中的所有索引节点已然全部被写回到了flash,所以此时将在ubifs_gc_start_commit中链入到c -> idx_gc链表中的LEB全部unmap,并从链表中删除。
ubifs_lpt_post_commit
将此前置tgc标志的LEB unmap。
**注:**log区是用于记录作为journal(日志区)使用的空间的区域,由于两个英文单词都可以被翻译为日志,可能会有所混淆,需要根据上下文去理解其所表达的意思。
|