JFFS2文件系统简介

JFFS2 是一个日志结构(log-structured)的文件系统,包含数据 和 原数据(meta-data)的节点在闪存上顺序的存储。

JFFS2 之所以选择日志结构的存储方式,是因为对闪存的更新应该是 out-of-place 的更新方式,而不是对磁盘的 in-place 的更新方式。在闪存上 in-place 更新方式的问题我们已经在闪存转换层一节描述过了。

JFFS2介绍

节点头部定义和兼容性

JFFS2 将文件系统的数据和原数据以节点的形式存储在闪存上,具体来说节点头部的定义如下:

http://www.ibm.com/developerworks/cn/linux/l-jffs2/images/image002.gif
  • 幻数屏蔽位:0x1985 用来标识 JFFS2 文件系统。

  • 节点类型:JFFS2 自身定义了三种节点类型,但是考虑到文件系统可扩展性和兼容性,JFFS2从 ext2 借鉴了经验,节点类型的最高两位被用来定义节点的兼容属性,具体来说有下面几种兼容属性:

    JFFS2_FEATURE_INCOMPAT:当 JFFS2 发现了一个不能识别的节点类型,并且它的兼容属性是 JFFS2_FEATURE_INCOMPAT,那么 JFFS2 必须拒绝挂载(mount)文件系统。
    JFFS2_FEATURE_ROCOMPAT:当 JFFS2 发现了一个不能识别的节点类型,并且它的兼容属性是 JFFS2_FEATURE_ROCOMPAT,那么 JFFS2 必须以只读的方式挂载文件系统。
    JFFS2_FEATURE_RWCOMPAT_DELETE:当 JFFS2 发现了一个不能识别的节点类型,并且它的兼容属性是 JFFS2_FEATURE_RWCOMPAT_DELETE,那么在垃圾回收的时候,这个节点可以被删除。

    JFFS2_FEATURE_RWCOMPAT_COPY:当 JFFS2 发现了一个不能识别的节点类型,并且它的兼容属性是 JFFS2_FEATURE_RWCOMPAT_COPY,那么在垃圾回收的时候,这个节点要被拷贝到新的位置。

  • 节点总长度:包括节点头和数据的长度。

  • 节点头部 CRC 校验:包含节点头部的校验码,为文件系统的可靠性提供了支持。

节点类型

JFFS2 定义了三种节点类型:

JFFS2_NODETYPE_INODE: INODE 节点包含了i-节点的原数据(i节点号,文件的组 ID, 属主 id, 访问时间,偏移,长度等),文件数据被附在 INODE 节点之后。除此之外,每个 INODE 节点还有一个版本号,它被用来维护属于一个i-节点的所有 INODE 节点的全序关系。下面举例来说明这个全序关系在 JFFS2 的使用:

http://www.ibm.com/developerworks/cn/linux/l-jffs2/images/image003.gif

因此,当文件系统从闪存上读节点信息后,会生成下面的映射信息:

http://www.ibm.com/developerworks/cn/linux/l-jffs2/images/image004.gif

根据这个映射信息表,文件系统就知道到相应的 INODE 节点去读取相应的文件内容。最后要说明的是,JFFS2 支持文件数据的压缩存储,因此在 INODE 节点中还包含了所使用的压缩算法,在读取数据的时候选择相应的压缩算法来解压缩。 JFFS2_NODETYPE_DIRENT:DIRENT 节点就是把文件名与 i 节点对应起来。在 DIRENT节点中也有一个版本号,这个版本号的作用主要是用来删除一个 dentry。具体来说,当我们要从一个目录中删除一个 dentry 时,我们要写一个 DIRENT 节点,节点中的文件名与被删除的 dentry 中的文件名相同,i 节点号置为 0,同时设置一个更高的版本号。 JFFS2_NODETYPE_CLEANMARKER:当一个擦写块被擦写完毕后,CLEANMARKER 节点会被写在 NOR flash 的开头,或 NAND flash 的 OOB(Out-Of-Band) 区域来表明这是一个干净,可写的擦写块。在 JFFS v1 中,如果扫描到开头的 1K 都是 0xFF 就认为这个擦写块是干净的。但是在实际的测试中发现,如果在擦写的过程中突然掉电,擦写块上也可能会有大块连续 0xFF,但是这并不表明这个擦写块是干净的。于是我们需要 CLEANMARKER 节点来确切的标识一个干净的擦写块。

JFFS2节点,擦写块在内存中的表示和操作

JFFS2 维护了几个链表来管理擦写块,根据擦写块上的内容,一个擦写块会在不同的链表上。具体来说,当一个擦写块上都是合法(valid)的节点时,它会在 clean_list 上;当一个擦写块包含至少一个过时(obsolete)的节点时,它会在 dirty_list 上;当一个擦写块被擦写完毕,并被写入 CLEANMARKER 节点后,它会在 free_list 上。

通常情况下,JFFS2 顺序的在擦写块上写入不同的节点,直到一个擦写块被写满。此时 JFFS2 从 free_list 上取下一个擦写块,继续从擦写块的开头开始写入节点。当 free_list 上擦写块的数量逐渐减少到一个预先设定的阀值的时候,垃圾回收就被触发了,为文件系统清理出更多的可用擦写块。为了减少对内存的占用,JFFS2 并没有把 i 节点所有的信息都保留在内存中,而只是把那些在请求到来时不能很快获得的信息保留在内存中。具体来说,对于在闪存上的每个 i 节点,在内存里都有一个 struct jffs2_inode_cache 与之对应,这个结构里保存了 i 节点号,指向 i 节点的连接数,以及一个指向属于这个 i 节点的物理节点链表的指针。所有的 struct jffs2_inode_cache 存储在一个哈希表中。闪存上的每个节点在内存中由一个 struct jffs2_raw_node_ref 表示,这个结构里保存了此节点的物理偏移,总长度,以及两个指向 struct jffs2_raw_node_ref 的指针。一个指针指向此节点在物理擦写块上的下一个节点,另一个指针指向属于同一个 i-节点的物理节点链表的下一个节点。

http://www.ibm.com/developerworks/cn/linux/l-jffs2/images/image006.gif

在闪存上的节点的起始偏移都是 4 字节对齐的,所以 struct jffs2_inode_cache 中flash_offset 的最低两位没有被用到。JFFS2 正好利用最低位作为此节点是否过时的标记。

下面举一例来说明 JFFS2 是如何使用这些数据结构的。VFS 调用 iget() 来得到一个 i 节点的信息,当这个 i 节点不在缓存中的时候,VFS 就会调用 JFFS2 的 read_inode() 回调函数来得到 i 节点信息。传给 read_inode() 的参数是 i 节点号,JFFS2 用这个 i 节点号从哈希表中查找相应的 struct jffs2_inode_cache,然后利用属于这个 i 节点的节点链表从闪存上读入节点信息,建立类似于表三的映射信息。

JFFS2 挂载过程

JFFS2 的挂载过程分为四个阶段:

  1. JFFS2 扫描闪存介质,检查每个节点 CRC 校验码的合法性,同时分配了 struct jffs2_inode_cache 和 struct jffs2_raw_node_ref

  2. 扫描每个 i 节点的物理节点链表,标识出过时的物理节点;对每一个合法的 dentry 节点,将相应的 jffs2_inode_cache 中的 nlink 加一。

  3. 找出 nlink 为 0 的 jffs2_inode_cache,释放相应的节点。

  4. 释放在扫描过程中使用的临时信息。

JFFS2 垃圾回收机制

当 free_list 上的擦写块数太少了,垃圾回收就会被触发。垃圾回收主要的任务就是回收那些已经过时的节点,但是除此之外它还要考虑磨损平衡的问题。因为如果一味的从 dirty_list上选取擦写块进行垃圾回收,那么 dirty_list 上的擦写块将先于 clean_list 上的擦写块被磨损坏。JFFS2 的处理方式是以 99% 的概率从 dirty_list,1% 的概率从 clean_list 上取一个擦写块下来。由此可以看出 JFFS2 的设计思想是偏向于性能,同时兼顾磨损平衡。对这个块上每一个没有过时的节点执行相同的操作:

  1. 找出这个节点所属的 i 节点号(见图五)。

  2. 调用 iget(),建立这个 i 节点的文件映射表。

  3. 找出这个节点上没有过时的数据内容,并且如果合法的数据太少,JFFS2 还会合并相邻的节点。

  4. 将数据读入倒缓存里,然后将它拷贝到新的擦写块上。

  5. 将回收的节点置为过时。

当擦写块上所有的节点都被置为过时,就可以擦写这个擦写块,回收使用它。

JFFS2 的不足之处

挂载时间过长

JFFS2 的挂载过程需要对闪存从头到尾的扫描,这个过程是很慢的,我们在测试中发现,挂载一个 16M 的闪存有时需要半分钟以上的时间。

磨损平衡的随意性(random nature)

JFFS2 对磨损平衡是用概率的方法来解决的,这很难保证磨损平衡的确定性。在某些情况下,可能造成对擦写块不必要的擦写操作;在某些情况下,又会引起对磨损平衡调整的不及时。

很差的扩展性

JFFS2 中有两个地方的处理是 O(N) 的,这使得它的扩展性很差。

首先,挂载时间同闪存的大小,闪存上节点数目成正比。

其次,虽然 JFFS2 尽可能的减少内存的占用,但通过上面对 JFFS2 的介绍我们可以知道实际上它对内存的占用量是同 i 节点数和闪存上的节点数成正比的。

因此在实际应用中,JFFS2 最大能用在 128M 的闪存上。

JFFS2 的新特性

最近加入到 JFFS2 中的两个补丁程序分别解决了上面提到的挂载时间过长和磨损平衡随意性的问题。

磨损块小结补丁程序(erase block summary patch)

这个补丁程序最基本的思想就是用空间来换时间。具体来说,就是将每个擦写块每个节点的原数据信息写在这个擦写块的最后,当 JFFS2 挂载的时候,对每个擦写块只需要读一次来读取这个小结节点,因此大大减少了挂载时间。使用了磨损块小结补丁程序,一个擦写块的结构就像下面这样:

http://www.ibm.com/developerworks/cn/linux/l-jffs2/images/image007.gif

根据我们的测试,使用磨损块小结补丁程序,挂载一个 12M 的闪存需要 2~3 秒,挂载一个 16M 的闪存需要 3~4 秒。

改进的磨损平衡补丁程序

这个补丁程序的基本思想是,记录每个擦写块的擦写次数,当闪存上各个擦写块的擦写次数的差距超过某个预定的阀值,开始进行磨损平衡的调整。调整的策略是,在垃圾回收时将擦写次数小的擦写块上的数据迁移到擦写次数大的擦写块上。这样一来我们提高了磨损平衡的确定性,我们可以知道什么时候开始磨损平衡的调整,也可以知道选取哪些擦写块进行磨损平衡的调整。

擦写块头部补丁程序

在写改进的磨损平衡补丁程序的过程之中,我们需要记录每个擦写块的擦写次数,这个信息需要记录在各自的擦写块上。可是我们发现 JFFS2 中缺少一种灵活的对每个擦写块的信息进行扩展的机制。于是我们为每个擦写块引入了擦写块头部(header),这个头部负责纪录每个擦写块的信息(比如说擦写次数),并且它提供了灵活的扩展机制,将来如果有新的信息需要记录,可以很容易的加入到头部之中。

JFFS3 简介

虽然不断有新的补丁程序来提高 JFFS2 的性能,但是不可扩展性是它最大的问题,但是这是它自身设计的先天缺陷,是没有办法靠后天来弥补的。因此我们需要一个全新的文件系统,而 JFFS3 就是这样的一个文件系统,JFFS3 的设计目标是支持大容量闪存(>1TB)的文件系统。JFFS3 与 JFFS2 在设计上根本的区别在于,JFFS3 将索引信息存放在闪存上,而 JFFS2将索引信息保存在内存中。比如说,由给定的文件内的偏移定位到存储介质上的物理偏移地址所需的信息,查找某个目录下所有的目录项所需的信息都是索引信息的一种。 JFFS3 现在还处于设计阶段,文件系统的基本结构借鉴了 Reiser4 的设计思想,整个文件系统就是一个 B+ 树。JFFS3 的发起者正工作于垃圾回收机制的设计,这是 JFFS3 中最复杂,也是最富有挑战性的部分。JFFS3 的设计文档可以在http://www.linux-mtd.infradead.org/doc/jffs3.html 得到,有兴趣的读者可以积极参与到 JFFS3 的设计中,发表自己的见解,参与讨论。