[转]JFFS2源代码情景分析Beta2

声明

你可以自由地随意修改本文档的任何文字内容及图表,但是如果你在自己的文档中以任何形式直接引用了本文档的任何原有文字或图表并希望发布你的文档,那么你也得保证让所有得到你的文档的人同时享有你曾经享有过的权利。

JFFS2源代码情景分析(Beta2)

作者在www.linuxforum.net上的ID为shrek2
欢迎补充,欢迎批评指正!

前言(new) 4
第1章 jffs2的数据实体及其内核描述符(improved) 5
数据实体的内核描述符jffs2_raw_node_ref 6
文件的内核描述符jffs2_inode_cache 6
jffs2_raw_dirent数据实体及其上层数据结构 7
jffs2_raw_inode数据实体及其上层数据结构 10
第2章 描述jffs2特性的数据结构(improved) 14
文件系统超级块的u域:jffs2_sb_info数据结构 14
文件索引结点的u域:jffs2_inode_info数据结构 18
打开正规文件后相关数据结构之间的引用关系 19
第3章 注册文件系统 21
init_jffs2_fs函数 21
register_filesystem函数 23
第4章 挂载文件系统(improved) 25
jffs2_read_super函数 25
jffs2_do_fill_super函数 27
jffs2_do_mount_fs函数 30
jffs2_build_filesystem函数 31
jffs2_scan_medium函数 34
jffs2_scan_eraseblock函数 40
jffs2_scan_inode_node函数 52
jffs2_scan_make_ino_cache函数 55
jffs2_scan_dirent_node函数 56
full_name_hash函数 59
jffs2_add_fd_to_list函数 60
jffs2_build_inode_pass1函数 61
第5章 打开文件时建立inode的方法 63
iget和iget4函数 63
get_new_inode函数 65
jffs2_read_inode函数 68
jffs2_do_read_inode函数(improved) 73
jffs2_get_inode_nodes函数 78
第6章 jffs2中写正规文件的方法 88
sys_write函数 89
generic_file_write函数 90
jffs2_prepare_write函数 98
jffs2_commit_write函数 102
jffs2_write_inode_range函数 104
jffs2_write_dnode函数 107
第7章 jffs2中读正规文件的方法 111
jffs2_readpage函数 111
jffs2_do_readpage_nolock函数 111
jffs2_read_inode_range函数 112
jffs2_read_dnode函数 115
第8章 jffs2中符号链接文件的方法表(new) 120
jffs2_follow_link函数 120
jffs2_getlink函数 121
第9章 jffs2中目录文件的方法表(new) 122
jffs2_create函数 122
jffs2_new_inode函数 124
jffs2_do_create函数 126
jffs2_do_new_inode函数 129
第10章 jffs2的Garbage Collection 131
jffs2_start_garbage_collect_thread函数 131
jffs2_garbage_collect_thread函数 132
jffs2_garbage_collect_pass函数 135
jffs2_erase_pending_trigger函数 141
第11章 讨论和体会 142
什么是日志文件系统,为什么要使用jffs2 142
为什么需要红黑树 142
何时、如何判断数据实体是过时的 143
后记 144
附录 用jffs2map2模块导出文件的数据实体(new) 145
观察根目录文件的数据实体 145
观察符号链接的信息 147
观察正规文件创建后的数据实体 147
观察jffs2_raw_inode数据实体的大小上限 148
 前言(new)
第1稿后拜读了情景分析中文件系统的相关章节,将ext2与jffs2相类比,显著地加深了对上层文件系统相关概念的理解,尤其是VFS框架的数据结构的设计思想,比如为了实现良好的可移植性和重用性,上层VFS框架代码就必须与具体的应用(底层具体文件系统)无关,而这一点恰恰是通过设计中间层的函数指针接口实现的。依靠接口实现的封装性是可移植性的基础。还有VFS各数据结构的设计目的和设计方法等等,比如只有尽可能地概括各种不同文件系统的共性才能使VFS具有良好的通用性,同时通过各种数据结构中的union,让具体文件系统的实现来定义、解释、使用其特有数据结构、描述在具体设备上具体文件系统的数据组织格式。“union域反映了各种不同文件系统在上层数据结构上的差异”。

第2稿对第1稿的改进也主要集中在对jffs2相关数据结构的理解和ext2与jffs2的类比上,这样可以加深对jffs2数据结构的理解。对于已经看过第1稿的朋友,再翻翻第1、2章就差不多了。另外有兴趣的话也可以再看看其它新增的章节。

第1稿中只涉及了正规文件的访问方法,第2稿中补充了符号链接文件和目录文件的相关方法。这些补充可以验证、加深对各种类型文件在jffs2中的实现方法的理解,比如可以通过目录文件的create方法看到在创建正规文件时是如何设置inode的u域、向父目录文件增加jffs2_raw_dirent目录项的(具体操作不止这些)。由于我目前的兴趣主要集中在内核中那些与具体应用无关的上层框架上面,而不是与具体应用相关的最底层代码,所以第1稿中有关jffs2某些实现细节的遗留问题暂时还没有继续研究下去,请大家谅解。

感谢论坛上所有鼓励支持我、尤其是那些向我提出问题的朋友,你们的提问促使我研究得更深入一些。完成第1稿后我就曾打算通过继续阅读mkfs.jffs2的源代码、或者编写一个能导出指定的jffs2文件系统上所有数据结点的模块来加深对jffs2的理解。在和网友们讨论jffs2对jffs2_raw_inode数据结点的最大长度限制时我完成了这个模块,目前它可以导出指定jffs2文件系统上指定文件的所有数据结点的信息,这样每次从根目录开始就可以逐层得到文件系统目录树中任何一个文件的数据结点信息了。

人们对Linux的喜爱很大程度上源自于它的可实践性,从而极大地调动研究和使用的积极性。通过这个可以导出一个文件在flash上的物理实体的模块,jffs2的概念前所未有地清晰、真实,也进一步改正、完善了对目录文件和jffs2_raw_dirent的理解,有兴趣的朋友可以参见附录及附件源代码,或者进一步改进这个模块。(在第1稿中曾错误地认为目录文件是没有jffs2_raw_inode数据实体的,很抱歉,而实际情况是除了根目录外所有目录都由惟一的、用于描述其类型和其它管理信息的jffs2_raw_inode,与此相关的jffs2_full_dnode则由jffs2_inode_info的metadata域直接指向。)


Let’s DIY Linux!


2006年1月18日星期三
 第1章 jffs2的数据实体及其内核描述符(improved)
存储于辅存的任何文件都至少包含三种类型的信息:文件的数据本身、描述文件属性的管理信息、以及描述文件在文件系统内部的位置信息。文件的位置信息用于实现“从路径名找到文件”的机制。jffs2在flash上只有两种类型的数据实体:jffs2_raw_inode和jffs2_raw_dirent,前者包含文件的管理信息,类似于ext2中的磁盘索引结点ext2_inode,后者用于描述文件在文件系统中的位置,类似于ext2中的ext2_dir_entry_2目录项实体。

与ext2_inode可以定位磁盘文件的磁盘块相比,jffs2_raw_inode没有这种“索引”功能,flash上文件的数据是由若干离散的jffs2_raw_inode数据结点进行描述的。与ext2_dir_entry_2类似,jffs2_raw_dirent也描述了文件名及其索引结点编号之间的映射关系,是文件硬链接的物理实体。

在ext2中目录文件所占的磁盘块由其ext2_inode进行索引,在一个磁盘块内部为描述其下子目录、子文件的ext2_dir_entry_2实体。由于ext2_dir_entry_2可指明自身的长度,而且它们在磁盘块内部是连续存放的,所以并不需要描述其所在目录文件的索引结点号。而jffs2中目录文件由一个jffs2_raw_inode数据实体和若干jffs2_raw_dirent数据实体组成,由于目录文件的数据实体之间都是离散存放的,所以每个jffs2_raw_dirent中还得描述其所属目录文件的索引结点号,参见下文。

正规文件、符号链接文件、SOCKET/FIFO文件、设备文件都由一个或多个jffs2_raw_inode来表示,而紧随jffs2_raw_inode数据结构后的为相关数据块,不同文件所需要的jffs2_raw_inode个数及其后数据的内容如下表所示:

文件类型 所需jffs2_raw_inode
结点的个数 后继数据的内容
目录文件 1(根目录除外) 无
正规文件 >= 1 文件的数据
符号链接文件 1 被链接的文件名
SOCKET/FIFO文件 1 无
设备文件 1 设备号

另外,所有文件都至少存在一个jffs2_raw_dirent数据实体(具体个数由其硬链接个数决定),它们组成其父目录文件的内容。区分jffs2_raw_dirent和jffs2_raw_inode是为了实现硬链接:在jffs版本1中就只有类似jffs2_raw_inode的一种数据实体,只能实现符号链接。(注:根目录没有父目录了,自然不需要jffs2_raw_dirent。另外它也没有那个惟一的jffs2_raw_inode。)

jffs2_raw_dirent和jffs2_raw_inode数据实体都以相同的“头”开始:

struct jffs2_unknown_node
{
 /* All start like this */
 jint16_t magic;
 jint16_t nodetype;
 jint32_t totlen; /* So we can skip over nodes we don't grok */
 jint32_t hdr_crc;
} __attribute__((packed));

其中nodetype指明数据结点的具体类型JFFS_NODETYPE_DIRENT或者JFFS2_NODETYPE_INODE;totlen为包括后继数据的整个数据实体的总长度;hdr_crc为头部中其它域的CRC校验值。另外整个数据结构在内存中以“紧凑”方式进行存储,这样当从flash上复制数据实体的头部到该数据结构后,其各个域就能够“各得其所”了。
数据实体的内核描述符jffs2_raw_node_ref
flash上每个数据实体的位置、长度都由jffs2_raw_node_ref数据结构描述:

struct jffs2_raw_node_ref
{
 struct jffs2_raw_node_ref *next_in_ino;
 struct jffs2_raw_node_ref *next_phys;
 uint32_t flash_offset;
 uint32_t totlen;
};

其中flash_offset表示相应数据实体在flash分区上的物理地址,totlen为包括后继数据的总长度。同一个文件的多个jffs2_raw_node_ref由next_in_ino组成一个循环链表,链表首为文件内核描述符jffs2_inode_cache数据结构的nodes域,链表末尾元素的next_in_ino则指向jffs2_inode_cache,这样任何一个jffs2_raw_node_ref元素就都知道自己所在的文件了。

一个flash擦除块内所有数据实体的内核描述符由next_phys域组织成一个链表,其首尾元素分别由擦除块描述符jffs2_eraseblock数据结构的first_node和last_node域指向。
文件的内核描述符jffs2_inode_cache
每一个文件在内核中都由惟一的jffs2_inode_cache数据结构表示,其最主要的作用就是提供了“文件及其数据之间的映射机制”:

struct jffs2_inode_cache {
 struct jffs2_full_dirent *scan_dents;
 struct jffs2_inode_cache *next;
 struct jffs2_raw_node_ref *nodes;
 uint32_t ino;
 int nlink;
 int state;
};

ino为文件的在文件系统中唯一的索引结点号;所有文件内核描述符被组织在一个inocache_list哈希表中,next用于组织冲突项的链表。

nlink为文件的硬链接个数,在挂载文件系统时会计算指向每个文件的目录项个数,然后赋值给nlink。注意上层VFS所使用的nlink与此不同:在打开文件时会首先将jffs2_inode_cache的nlink复制到inode的nlink,然后对于非叶目录,会根据其下子目录的目录项个数增加inode的nlink。详见后文。

上层VFS的inode的主要作用之一就是描述文件及其数据之间的映射关系。由于不同文件系统中数据的组织格式不同,所以这种映射关系的描述符自然放到inode的u域中,由具体文件系统的方法实现。就ext2而言,ext2_inode_info的i_data[]索引文件的磁盘块,而其“物质基础”为文件磁盘索引结点ext2_inode的i_block[]数组;就jffs2而言,根据文件类型的不同由jffs2_inode_info的fragtree/dents/metadata描述flash数据实体相关上层数据结构的组织,其“物质基础”就是jffs2_inode_cache的nodes链表,它组织了文件所有数据实体的内核描述符jffs2_raw_node_ref数据结构,在挂载文件系统时建立。

在挂载jffs2文件系统时将遍历整个文件系统(扫描jffs2文件系统映象所在的整个flash分区),为flash上每个jffs2_raw_dirent和jffs2_raw_inode数据实体创建相应的内核描述符jffs2_raw_node_ref、为每个文件创建内核描述符jffs2_inode_cache,具体过程详见下文。另外在打开文件时,如果是目录文件,则还要为每个jffs2_raw_dirent创建相应的jffs2_full_dirent数据结构并组织为链表;如果是正规文件等,则为每个jffs2_raw_inode创建相应的jffs2_full_dnode、jffs2_tmp_dnode_info、jffs2_node_frag数据结构,并组织到红黑树中,详见下文。
jffs2_raw_dirent数据实体及其上层数据结构
jffs2_raw_dirent数据实体为jffs2中目录项的表示形式,即硬链接的物理实体。文件的目录项组成了其父目录文件的“数据”。定义如下:

struct jffs2_raw_dirent
{
 jint16_t magic;
 jint16_t nodetype;  /* == JFFS_NODETYPE_DIRENT */
 jint32_t totlen;
 jint32_t hdr_crc;
 jint32_t pino;
 jint32_t version;
 jint32_t ino;   /* == zero for unlink */
 jint32_t mctime;
 uint8_t nsize;
 uint8_t type;
 uint8_t unused[2];
 jint32_t node_crc;
 jint32_t name_crc;
 uint8_t name[0];
} __attribute__((packed));

紧随jffs2_raw_dirent的是相应文件的文件名,长度由nsize域表示,而name域预留了文件名字符串结束符的空间,ino表示相应文件的索引结点号。如前所述flash上目录文件的若干jffs2_raw_dirent数据实体是离散的,而且也没有类似ext2_inode的“索引”机制,所以就必须由每个jffs2_raw_dirent数据实体表明自己所属的目录文件。pino即是为这个目的设计的,表示该目录项所属的目录文件的索引节点号。另外在挂载文件系统时,会将jffs2_raw_dirent数据实体的描述符加入pino所指文件,即该目录项所属目录文件的jffs2_inode_cache的nodes链表,参见jffs2_scan_dirent_node函数。

版本号version是相对于某一文件内部的概念。任何文件都由若干jffs2_raw_dirent或者jffs2_raw_inode数据实体组成,修改文件的“某一个区域”时将向flash写入新的数据实体,它的version总是不断递增的。一个文件的所有数据实体的最高version号由其inode的u域,即jffs2_inode_info数据结构中的highest_version记录。文件内同一“区域”可能由若干数据实体表示,它们的version互不相同,而且除了最新的一个数据结点外,其余的都被标记为“过时”。(另外,按照jffs2作者的论文,如果flash上数据实体含有相同的数据则允许它们的version号相同)

打开目录文件时要创建其VFS的dentry 、inode、file对象,在创建inode时要调用super_operations函数指针接口中的read_inode方法,根据相应的内核描述符jffs2_raw_node_ref为每个目录项创建一个上层的jffs2_full_dirent数据结构,并读出jffs2_raw_dirent数据实体后的文件名到jffs2_full_dirent数据结构后面。jffs2_full_dirent组成的链表则由目录文件的索引结点inode.u.dents(即jffs2_inode_info.dents)指向,参见图1。

jffs2_full_dirent数据结构在打开目录文件时才创建,用于保存读出的jffs2_raw_dirent数据实体的结果,其定义如下:

struct jffs2_full_dirent
{
 struct jffs2_raw_node_ref *raw;
 struct jffs2_full_dirent *next;
 uint32_t version;
 uint32_t ino;     /* == zero for unlink */
 unsigned int nhash;
 unsigned char type;
 unsigned char name[0];
};

其中raw指向相应的jffs2_raw_node_ref结构,紧随其后的为从flash上读出的文件名。


总之,在打开一个目录文件后其相关数据结构的关系如下图所示(假设该目录有2个目录项,没有画出file、dentry)。其中对inode的u域即jffs2_inode_info数据结构的解释参见下文。

(注:很抱歉,在第1稿中我曾错误地认为目录文件只包含jffs2_raw_dirent数据实体,而实际上目录文件、设备文件、符号链接文件都有惟一的jffs2_raw_inode数据实体,在打开文件时为其创建的jffs2_full_dnode由其inode的u域jffs2_inode_info的metedata指向。一个目录文件的数据实体信息可以使用jffs2map2模块导出,参见附录)
 

jffs2_raw_inode数据实体及其上层数据结构
jffs2_raw_inode数据实体用于描述文件的类型、访问权限、其它管理信息和文件的数据(如果存在的话)。由于正规文件、符号链接、设备文件的jffs2_raw_inode后都有相应的数据,共同组成一个flash上的数据实体,所以在下文中若无特别说明“jffs2_raw_inode”均指该数据结构本身及其后的数据。

struct jffs2_raw_inode
{
 jint16_t magic;      /* A constant magic number.  */
 jint16_t nodetype;   /* == JFFS_NODETYPE_INODE */
 jint32_t totlen;     /* Total length of this node (inc data, etc.) */
 jint32_t hdr_crc;
 jint32_t ino;        /* Inode number.  */
 jint32_t version;    /* Version number.  */
 jint32_t mode;       /* The file's type or mode.  */
 jint16_t uid;        /* The file's owner.  */
 jint16_t gid;        /* The file's group.  */
 jint32_t isize;      /* Total resultant size of this inode (used for truncations)  */
 jint32_t atime;      /* Last access time.  */
 jint32_t mtime;      /* Last modification time.  */
 jint32_t ctime;      /* Change time.  */
 jint32_t offset;     /* Where to begin to write.  */
 jint32_t csize;      /* (Compressed) data size */
 jint32_t dsize;      /* Size of the node's data. (after decompression) */
 uint8_t compr;       /* Compression algorithm used */
 uint8_t usercompr;   /* Compression algorithm requested by the user */
 jint16_t flags;      /* See JFFS2_INO_FLAG_* */
 jint32_t data_crc;   /* CRC for the (compressed) data.  */
 jint32_t node_crc;   /* CRC for the raw inode (excluding data)  */
// uint8_t data[dsize];
} __attribute__((packed));

一个正规文件可能由若干jffs2_raw_inode数据实体组成,每个数据实体含有文件一个区域的数据。即使文件的同一个区域也可能因为修改而在flash上存在多个数据实体,它们都含有相同的ino,即文件的索引结点编号。

文件在逻辑上被当作一个连续的线性空间,每个jffs2_raw_inode所含数据在该线性空间内的偏移由offset域表示。注意offset为文件内的偏移,而该jffs2_raw_inode在flash分区中的偏移则由其内核描述符jffs2_raw_node_ref的flash_offset域表示。

jffs2支持数据压缩。如果后继数据没有被压缩,则compr被设置JFFS2_COMPR_NONE。压缩前(或解压缩后)的数据长度由dsize表示,而压缩后的数据长度由csize表示。从后文的相关函数分析中可以看到,在修改文件的已有内容或者写入新内容时,首先要将数据压缩,然后在内存中组装合适的jffs2_raw_inode结构,最后再将二者连续地写入flash。而在读flash上的设备结点时首先读出jffs2_raw_inode结构,然后根据其中的csize域的值,分配合适大小的缓冲区,第二次再读出紧随其后的(压缩了的)数据。在解压缩时则根据dsize大小分配合适的缓冲区。另外,如果jffs2_raw_node没有后继数据而是代表一个洞,那么compr被设置为JFFS2_COMPR_ZERO。

除了文件头jffs2_unknown_node中有crc校验值外,在jffs2_raw_inode中还有该数据结构本身及其后数据的crc校验值。这些校验值在创建jffs2_raw_inode时计算,在读出该数据实体时进行验证。

在ext2中,磁盘正规文件所占用的所有磁盘块都通过其ext2_inode的i_block[]进行直接或间接的索引,而jffs2中一个正规文件的所有数据实体可能分布在flash的任何位置上,每个jffs2_raw_inode都“独善其身”地描述自己的后继数据。正因为缺少类似ext2_inode的“索引”机制,所以在挂载jffs2时才不得不遍历整个flash分区,从而找到每个文件的所有数据实体,并建立它们的内核描述符jffs2_raw_node_ref并组织到文件的jffs2_inode_cache的nodes链表中去。


在打开正规文件时要为其jffs2_raw_inode数据实体创建相应的内核映象jffs2_full_dnode(以及临时数据结构jffs2_tmp_dnode_info)、jffs2_node_frag,并通过后者组织到红黑树中。

struct jffs2_full_dnode
{
 struct jffs2_raw_node_ref *raw;
 uint32_t ofs;    /* Don't really need this, but optimisation */
 uint32_t size;
 uint32_t frags;   /* Number of fragments which currently refer
        to this node. When this reaches zero, the node is obsolete. */
};

该数据结构的ofs和size域用于描述数据实体的后继数据在文件内的逻辑偏移及长度,它们的值来自数据实体jffs2_raw_inode的offset和dsize域。而raw指向数据实体的内核描述符jffs2_raw_node_ref数据结构。

尤其需要说明的是frags域。当打开一个文件时为每个jffs2_raw_node_ref创建jffs2_full_node和jffs2_node_frag,并由后者插入文件的红黑树中。如果对文件的相同区域进行修改,则将新的数据实体写入flash的同时,还要创建相应的jff2_raw_node_ref和jffs2_full_dnode,并将原有jffs2_node_frag数据结构的node域指向新的jffs2_full_dnode,使其frags引用计数为1,而原有的frags则递减为0。

struct jffs2_node_frag
{
 rb_node_t rb;
 struct jffs2_full_dnode *node;  /* NULL for holes */
 uint32_t size;
 uint32_t ofs;    /* Don't really need this, but optimisation */
};

其中rb域用于组织红黑树,node指针指向相应的jffs2_full_dnode,size和ofs也是从相应的jffs2_full_dnode复制而来,表示数据结点所代表的区域在文件内的偏移和长度。

总之,在打开一个正规文件时内核中创建的数据结构之间的关系如下图所示(假设文件由3个数据结点组成、没有画出file、dentry、临时创建后又被删除的jffs2_tmp_dnode_info)。

需要说明的是下图仅仅针对正规文件。对于目录文件、符号链接、设备文件都只有惟一的jffs2_raw_inode数据实体,目录文件没有“数据”,后二者的数据分别是被链接文件名和设备结点所代表的设备号。显然这一个数据实体对应的jffs2_full_dnode就没有必要用红黑树来组织了,而是由jffs2_inode_info中的metedata直接指向(在jffs2_do_read_inode函数中首先将它们的jffs2_full_dnode也向正规文件的那样加入红黑树,然后又改为由metadata直接指向)。

(另外,文件的目录项jffs2_raw_dirent数据结点则属于其父目录,所以没有出现在下图中。)
 

 第2章 描述jffs2特性的数据结构(improved)
如果在配置内核时选择对jffs2文件系统的支持,则在内核启动时在init内核线程上下文中执行jffs2的注册工作:将其源代码中定义的file_system_type类型的变量jffs2_fs_type注册到由内核全局变量file_systems指向的链表中去,即用jffs2提供的jffs2_read_super方法来设置file_system_type类型变量的read_super函数指针,详见后文“注册文件系统”。如果内核引导命令行“root=”指定的设备上含有jffs2文件系统映象,则在内核启动时还将jffs2文件系统挂载为根文件系统。

在挂载文件系统时内核将为其创建相应的VFS对象super_block,进而用具体文件系统注册的方法read_super读取设备,以填充、设置super_block数据结构,并建立根目录的inode、dentry等基本VFS设施。有些文件系统,比如ext2,在磁盘上就有文件系统的超级块ext2_super_block,因此这个方法主要是读取磁盘上的超级块,将其内容复制到内存中的super_block。而jffs2在flash上没有超级块实体,所以在这个方法执行其它挂载所必须的操作,比如遍历flash为所有的数据实体和文件创建内核描述符、建立文件及其数据的映射关系等等,详见后文“挂载文件系统”。
文件系统超级块的u域:jffs2_sb_info数据结构
如上文所述,上层VFS框架的数据结构的设计应该尽可能多地概括各种不同文件系统之间的共性,从而使VFS框架具有良好的通用性。但对于各种不同文件系统的个性则用相应的union域来描述,由具体文件系统的实现来定义、解释、使用这些union,从而实现具体文件系统的操作。在挂载具体文件系统时super_block的union域即被实例化为相应文件系统的私有数据结构,对于jffs2这个域实例化为jffs2_sb_info:

struct jffs2_sb_info {
 struct mtd_info *mtd;
 uint32_t highest_ino;
 uint32_t checked_ino;
 unsigned int flags;
 struct task_struct *gc_task;   /* GC task struct */
 struct semaphore gc_thread_start;   /* GC thread start mutex */
 struct completion gc_thread_exit;   /* GC thread exit completion port */
 struct semaphore alloc_sem;   /* Used to protect all the following fields, and also to protect against
       out-of-order writing of nodes. And GC.*/
 uint32_t cleanmarker_size;   /* Size of an _inline_ CLEANMARKER
(i.e. zero for OOB CLEANMARKER */
 uint32_t flash_size;   
 uint32_t used_size;
 uint32_t dirty_size;
 uint32_t wasted_size;
 uint32_t free_size;
 uint32_t erasing_size;
 uint32_t bad_size;
 uint32_t sector_size; 
 uint32_t unchecked_size;
 uint32_t nr_free_blocks;
 uint32_t nr_erasing_blocks;
 uint32_t nr_blocks;
 struct jffs2_eraseblock *blocks;  /* The whole array of blocks. Used for getting blocks
          * from the offset (blocks[ofs / sector_size]) */
 struct jffs2_eraseblock *nextblock; /* The block we're currently filling */
 struct jffs2_eraseblock *gcblock;  /* The block we're currently garbage-collecting */

 struct list_head  clean_list;  /* Blocks 100% full of clean data */
 struct list_head  very_dirty_list; /* Blocks with lots of dirty space */
 struct list_head  dirty_list;   /* Blocks with some dirty space */
 struct list_head  erasable_list;  /* Blocks which are completely dirty, and need erasing */
struct list_head  erasable_pending_wbuf_list;  /* Blocks which need erasing but only after
the current wbuf is flushed */
 struct list_head  erasing_list;   /* Blocks which are currently erasing */
 struct list_head  erase_pending_list;  /* Blocks which need erasing now */
 struct list_head  erase_complete_list; /* Blocks which are erased and need the clean marker
written to them */
 struct list_head  free_list;   /* Blocks which are free and ready to be used */
 struct list_head  bad_list;   /* Bad blocks. */
 struct list_head  bad_used_list;  /* Bad blocks with valid data in. */

 spinlock_t erase_completion_lock;  /* Protect free_list and erasing_list against erase
completion handler */
 wait_queue_head_t erase_wait;  /* For waiting for erases to complete */

 struct jffs2_inode_cache **inocache_list;
 spinlock_t inocache_lock;
 
/* Sem to allow jffs2_garbage_collect_deletion_dirent to drop the erase_completion_lock while it's holding a pointer to an obsoleted node. I don't like this. Alternatives welcomed. */
 struct semaphore erase_free_sem;

 /* Write-behind buffer for NAND flash */
 unsigned char *wbuf;
 uint32_t  wbuf_ofs;
 uint32_t  wbuf_len;
 uint32_t  wbuf_pagesize;
 struct tq_struct  wbuf_task;  /* task for timed wbuf flush */
 struct timer_list  wbuf_timer;  /* timer for flushing wbuf */

 /* OS-private pointer for getting back to master superblock info */
 void *os_priv;
};

其中部分域的说明如下,其它域的作用放到代码中说明:
文件系统是在具体设备上的特定数据组织形式。在向设备写入数据前,需要通过文件系统的相应方法将数据组织为特定的格式;在从设备读出数据后,需要通过文件系统的相应方法解释数据,而真正访问设备的工作是由设备驱动成完成的。jffs2是建立在flash上的文件系统,所以向flash写入、读出数据实体的操作最终通过flash驱动程序完成。jffs2_sb_info的mtd域指向整个flash(注意是包含若干分区的整个flash)的mtd_info数据结构,该数据结构在安装、初始化flash设备驱动程序时创建,提供了访问flash的方法。从后文的读写操作分析可以看到,在向flash写入数据实体jffs2_raw_inode或者jffs2_raw_dirent及其后的数据时,最终要调用mtd_info中的相应方法。

在文件系统所在的设备上索引节点号是惟一的,highest_ino记录了文件系统内最高的索引结点号。在ext2中索引结点ext2_inode的编号由其物理位置决定,而且在分配其物理位置时出于效率因素要综合考虑多种因素。而jffs2中事情就简单多了:每当新建文件时为之分配的索引结点号即为highest_ino,并逐一递增该域,参见下文。

flags为挂载文件系统时指定的各种标志,比如是否以只读方式挂载等等。

再次强调,一种文件系统是具体设备上的一种数据组织格式,因此必须针对具体设备的特点来设计文件系统的数据结构。而文件系统的超级块从宏观上描述了整个文件系统,所以针对具体设备的特点所设计的数据结构正体现在其超级块中。比如ext2为用于磁盘的文件系统,所以在其超级块ext2_sb_info中包含了磁盘分区上所有块组描述符、块组内索引结点的数量等与磁盘操作相关的数据结构。而jffs2是应用于flash的文件系统(有关flash的使用特点可参见讨论),所以其超级块包含了针对flash使用特点的数据结构,主要可分为三个部分:为了使各flash擦除块都被均衡使用的各种xxxx_list链表和GC内核线程,以及用于组织所有文件的内核描述符的哈希表inocache_list,而文件描述符的主要作用就是描述一个文件的所有离散的数据结点的位置等信息。下面我们逐一介绍这三个方面的数据结构。

为了尽量推迟写flash的时机(也可以提高写一个擦除块的效率),jffs2使用一个内核线程来执行垃圾回收( GC,即Garbage Collecting),在需要时回收所有过时的数据结点,比如剩余干净的擦除块数量过低时、卸载jffs2时。创建这个内核线程后使用gc_task指向其PCB,这样就可以直接给它发送各种信号从而控制其状态了。相关的数据结构还包括gc_thread_start和gc_thread_exit。信号量gc_thread_start用于保证在当前进程(或init内核线程)在创建了GC内核线程后,在调用kernel_thread的函数返回前,GC内核线程已经运行了;gc_thread_exit是一个completion数据结构,定义如下:

struct completion {
 unsigned int done;
 wait_queue_head_t wait;
};

其核心是一个等待队列wait,整个数据结构由wait.lock保护。在jffs2_stop_garbage_collect_thread函数中通过给GC内核线程发送SIGKILL信号来结束它,当前执行流在发送完信号后就阻塞在gc_thread_exit.wait等待队列上;而GC内核线程处理SIGKILL信号时将唤醒受阻的执行流并退出,详见后文分析。

为了实现均衡地使用所有擦除块,jffs2的方法必须记录各擦除块的使用情况和状态。这也就是各xxxx_list域和擦除块描述符数组blocks[]的目的了。与此相关还包括若干xxxx_size的域,重新罗列如下:

 uint32_t flash_size;   
 uint32_t used_size;
 uint32_t dirty_size;
 uint32_t wasted_size;
 uint32_t free_size;
 uint32_t erasing_size;
 uint32_t bad_size;
 uint32_t sector_size; 
 uint32_t unchecked_size;

其中flash_size和sector_size的值在挂载文件系统时由jffs2_sb_info.mtd所指向的flash板块描述符mtd_info中相应的域复制过来,分别代表jffs2文件系统所在flash分区的大小和擦除块的大小。

在flash擦除块描述符jffs2_eraseblock中就设计了used_size、dirty_size、wasted_size、free_size域,它们分别表示当前擦除块内有效数据实体的空间大小、过时数据实体的空间大小、无法利用的空间大小和剩余空间大小。其中“无法利用的空间”是由于flash上数据结点之间必须是4字节地址对齐的,因此在数据结点之间可能存在间隙;或者由于填充;或者擦除块尾部的空间无法利用。那么jffs2_sb_info中这些域就是分区上所有擦除块相应域的求和。

jffs2_eraseblock数据结构为擦除块描述符,所有擦除块的描述符都存放在blocks[]数组中。另外,根据擦除块的状态(即是否有数据、数据过时情况等信息)还将擦除块描述符组织在不同的xxxx_list链表中,以供文件系统的写方法和GC使用,从而实现对所有擦除块的均衡使用。根据作者的注释,各个xxxx_list域所指向链表的含义如下:

链表 链表中擦除块的性质
clean_list 只包含有效数据结点
very_dirty_list 所含数据结点大部分都已过时
dirty_list 至少含有一个过时数据结点
erasable_list 所有的数据结点都过时需要擦除。但尚未“调度”到erase_pending_list
erasable_pending_wbuf_list 同erase_pending_list,但擦除必须等待wbuf冲刷后
erasing_list 当前正在擦除
erase_pending_list 当前正等待擦除
erase_complete_list 擦除已完成,但尚未写入CLEANMARKER
free_list 擦除完成,且已经写入CLEANMARKER
bad_list 含有损坏单元
bad_used_list 含有损坏单元,但含有数据

最后,由于jffs2中不存在类似ext_inode的可以提供“索引”文件数据的机制,所以才在挂载文件系统时不得不遍历整个flash分区,建立每个文件所包含数据实体的位置和长度信息。数据实体通过其所属文件的内核描述符jffs2_inode_cache进行“索引”。所有文件的内核描述符被组织在一张哈希表中,即为inocache_list所指向的指针数组。

目录文件的内容由各目录项jffs2_raw_dirent数据实体组成,实现了“从文件名找到文件”的机制。在打开文件、逐层解析文件的路径名时通过访问父目录文件即可得到当前文件的目录项实体jffs2_raw_dirent,进而得到文件的索引结点号ino(path_walk的细节可参见情景分析),然后通过inocache_list哈希表就可以得到其jffs2_inode_cache的指针,然后通过其nodes域得到文件所有数据实体的内核描述符jffs2_raw_node_ref组成的链表,从而最终得到所有数据实体在flash上的位置、长度信息。
文件索引结点的u域:jffs2_inode_info数据结构
在打开文件创建其inode时由具体文件系统提供的read_inode方法来初始化inode的u域。无论底层具体文件系统中“索引结点”的形式如何,上层VFS的inode都完整、惟一描述了文件的所有管理信息(其在文件系统内的组织信息由dentry描述),其中就必然包括索引文件数据的机制。由于在不同文件系统中数据的组织、存储格式不同,索引文件数据的机制显然应该放到inode的u域中。比如ext2中通过ext2_inode_info的i_data[]来索引文件数据所在的磁盘块,该数组内容从ext2_inode的i_block[]数组复制过来;而jffs2中通过jffs2_inode_info的fragtree/metedata,或者dents来组织文件的所有数据实体的内核描述符。

struct jffs2_inode_info {
 struct semaphore sem;
 uint32_t highest_version; /* The highest (datanode) version number used for this ino */
 rb_root_t fragtree;   /* List of data fragments which make up the file */

/* There may be one datanode which isn't referenced by any of the above fragments, if it contains a metadata update but no actual data - or if this is a directory inode. This also holds the _only_ dnode for symlinks/device nodes, etc. */
 struct jffs2_full_dnode *metadata;
 struct jffs2_full_dirent *dents;   /* Directory entries */

 /* Some stuff we just have to keep in-core at all times, for each inode. */
 struct jffs2_inode_cache *inocache;
 uint16_t flags;
 uint8_t usercompr;
#if LINUX_VERSION_CODE > KERNEL_VERSION(2,5,2)
 struct inode vfs_inode;
#endif
};

由于inode的i_sem在generic_file_write/read期间一直被当前执行流持有,用于实现上层用户进程之间的同步。所以在读写操作期间还要使用信号量的话,就必须设计额外的信号量。在jffs2_inode_info中设计的sem信号量用于实现底层读写执行流与GC之间的同步。(这是因为GC的本质就是将有效数据实体的副本写到其它的擦除块中去,即还是通过写入操作完成的,所以需要与其它写入操作同步,详见后文。)

一个文件的所有数据实体(无论过时与否)都有唯一的version号。当前所使用的最高version号由highest_version域记录。

正规文件包含若干jffs2_raw_inode数据实体,它们的内核描述符jffs2_raw_node_ref组成的链表由jffs2_inode_cache的nodes指向。在打开文件时还创建相应的jffs2_full_dnode和jffs2_node_frag数据结构,并由后者组织在由fragtree指向的红黑树中。详见图2。

由于目录文件、符号链接和设备文件只有一个jffs2_raw_inode数据实体所以没有必要使用红黑树。所以在jffs2_do_read_inode函数中先象对待正规文件那样先将它们的jffs2_full_dnode加入红黑树,然后又改为由metadata直接指向。如果是目录文件,则在打开文件时为数据实体的内核描述符jffs2_raw_node_ref创建相应的jffs2_full_dirent,并组织为由dents指向链表,详见图1。

最后,inocache指向该文件的内核描述符jffs2_inode_cache数据结构。
打开正规文件后相关数据结构之间的引用关系
如前所述,在挂载文件系统后即创建了super_block数据结构,为根目录创建了inode、dentry,为所有的文件创建了jffs2_inode_cache及每一个数据实体的描述符jffs2_raw_node_ref;在打开文件时创建file、dentry、inode,并为数据实体描述符创建相应的jffs2_full_dnode或者jffs2_full_dirent等数据结构。打开正规文件后相关数据结构的关系如下图所示:(没有画出根目录的inode)

(注意,虚线框中的数据结构在打开文件时才创建,其它数据结构在挂载文件系统时就已经创建好了)
 

 第3章 注册文件系统
在linux上使用一个文件系统之前必须完成安装和注册。如果在配置内核时选择对文件系统的支持,那么其代码被静态链接到内核中,在内核初始化期间init内核线程将完成文件系统的注册。或者在安装文件系统模块时在模块初始化函数中完成注册。

一旦完成注册,这种文件系统的方法对内核就是可用的了,以后就可以用mount命令将其挂载到根文件系统的某个目录结点上。而在内核初始化期间注册、挂载根文件系统。在设备上根目录文件由其子目录、子文件的目录项jffs2_raw_dirent数据实体组成,根目录在内核中的挂载点为“/”,相应的dentry和inode在内核初始化时由mount_root函数创建。
init_jffs2_fs函数
在配置内核时选择对jffs2的支持,那么jffs2的源代码编译后被静态链接入内核映象,在初始化期间init内核线程执行init_jffs2_fs函数完成jffs2的注册:

static int __init init_jffs2_fs(void)
{
 int ret;
 printk(KERN_NOTICE "JFFS2 version 2.1. (C) 2001, 2002 Red Hat, Inc., designed by Axis
Communications AB./n");
#ifdef JFFS2_OUT_OF_KERNEL
 /* sanity checks. Could we do these at compile time? */
 if (sizeof(struct jffs2_sb_info) > sizeof (((struct super_block *)NULL)->u)) {
  printk(KERN_ERR "JFFS2 error: struct jffs2_sb_info (%d bytes) doesn't fit in the super_block union
(%d bytes)/n", sizeof(struct jffs2_sb_info), sizeof (((struct super_block *)NULL)->u));
  return -EIO;
 }
 if (sizeof(struct jffs2_inode_info) > sizeof (((struct inode *)NULL)->u)) {
  printk(KERN_ERR "JFFS2 error: struct jffs2_inode_info (%d bytes) doesn't fit in the inode union (%d
bytes)/n", sizeof(struct jffs2_inode_info), sizeof (((struct inode *)NULL)->u));
  return -EIO;
 }
#endif

VFS的super_block以及inode的最后一个域都为一个共用体,将在挂载文件系统时实例化为具体文件系统的私有数据结构。这里首先检查super_block和inode的u域是否能够容纳jffs2的相关私有数据结构。

 ret = jffs2_zlib_init();
 if (ret) {
  printk(KERN_ERR "JFFS2 error: Failed to initialise zlib workspaces/n");
  goto out;
 }

jffs2文件系统支持压缩和解压缩,在将数据实体写入flash前可以使用zlib库的压缩算法进行压缩,从flash读出后进行解压缩。在jffs2_zlib_init函数中为压缩、解压缩分配空间deflate_workspace和inflate_workspace。

 ret = jffs2_create_slab_caches();
 if (ret) {
  printk(KERN_ERR "JFFS2 error: Failed to initialise slab caches/n");
  goto out_zlib;
 }

为数据实体jffs2_raw_dirent和jffs2_raw_inode、数据实体内核描述符jffs2_raw_node_ref、文件的内核描述符jffs2_inode_cache、jffs2_full_dnode和jffs2_node_frag等数据结构通过kmem_cache_create函数创建相应的内存高速缓存(这些数据结构都是频繁分配、回收的对象,因此使用内存高速缓存再合适不过了。另外通过slab的着色能够使不同slab内对象的偏移地址不尽相同,从而映射到不同的处理器高速缓存行上)。

 ret = register_filesystem(&jffs2_fs_type);
 if (ret) {
  printk(KERN_ERR "JFFS2 error: Failed to register filesystem/n");
  goto out_slab;
 }
 return 0;
 out_slab:
 jffs2_destroy_slab_caches();
 out_zlib:
 jffs2_zlib_exit();
 out:
 return ret;
}

最后,就是通过register_filesystem函数向系统注册jffs2文件系统了。使用mount命令挂载某一文件系统前,它必须事先已经向系统注册过了。每一个已注册过的文件系统都由数据结构file_system_type描述:

struct file_system_type {
 const char *name;
 int fs_flags;
 struct super_block *(*read_super) (struct super_block *, void *, int);
 struct module *owner;
 struct file_system_type * next;
 struct list_head fs_supers;
};

所有已注册的文件系统的file_system_type通过next域组织成一个链表,链表由内核全局变量file_systems指向。name域用于描述文件系统的名称,由find_filesystem函数在链表中查找指定名称的文件系统时使用。fs_flags指明了文件系统的一些特性,比如文件系统是否只支持一个超级块结构、是否允许用户使用mount命令挂载等等,详见linux/fs.h文件。

file_system_type中最重要的域就是函数指针read_super了。这个函数指针即为上层VFS框架与具体文件系统的特定挂载方法之间的接口,VFS框架中挂载文件系统的代码向下只调用到该函数指针,从而实现与具体文件系统方法的无关性。在注册具体文件系统时实例化该函数指针为相应的方法,从而按照特定格式来创建、初始化文件系统的超级块。

另外根据注释,任何文件系统的file_syste_type自注册之时其就必须一直存在在内核中,直到其被注销。无论文件系统是否被挂载都不应该释放file_system_type数据结构。

在jffs2源代码中实现了所有jffs2的方法,并通过如下的宏定义了file_system_type数据结构:

static DECLARE_FSTYPE_DEV(jffs2_fs_type, "jffs2", jffs2_read_super);

这个宏定义在linux/fs.h中:

#define DECLARE_FSTYPE(var,type,read,flags) /
struct file_system_type var = { /
 name:  type, /
 read_super: read, /
 fs_flags:  flags, /
 owner:  THIS_MODULE, /
}

由此可见,在jffs2源代码文件中定义了file_system_type类型的变量jffs2_fs_type,其名字为“jffs2”,而“read_super”方法为jffs2_read_super,它是具体文件系统所提供的各种方法的“总入口”,将在后文挂载文件系统时详细分析。
register_filesystem函数
int register_filesystem(struct file_system_type * fs)
{
 int res = 0;
 struct file_system_type ** p;

 if (!fs)
  return -EINVAL;
 if (fs->next)
  return -EBUSY;

 INIT_LIST_HEAD(&fs->fs_supers);
 write_lock(&file_systems_lock);
 p = find_filesystem(fs->name);
 if (*p) //若已注册过
  res = -EBUSY;
 else  //否则,将新的file_system_type结构加入到file_systems链表的末尾
  *p = fs;
 write_unlock(&file_systems_lock);
 return res;
}

如前所述,所有已注册文件系统的file_system_type组成一个链表,由内核全局变量file_systems指向。注册文件系统就是将其file_system_type加入到这个链表中。当然,在访问链表期间必须首先获得锁file_system_locks。

如果name命名文件系统已经注册过了,则find_filesystems函数返回其file_system_type结构的地址,否则返回内核file_systems链表的末尾元素next域的地址:

static struct file_system_type **find_filesystem(const char *name)
{
 struct file_system_type **p;
 for (p=&file_systems; *p; p=&(*p)->next)
  if (strcmp((*p)->name, name) == 0)
   break;
 return p;
}

由此可见,register_filesystem函数就是将新的文件系统的file_system_type加入到file_systems链表的末尾。
 第4章 挂载文件系统(improved)
如前所述,在jffs2源代码中定义了file_system_type类型的全局变量jffs2_fs_type,并将其注册到内核的file_systems链表中去。需要说明的是file_system_type中的read_super函数指针所指向的方法是具体文件系统所提供的各种方法的“总入口”,从时间上看各种方法的“引入时机”如下:
1. 在注册时实例化read_super方法;
2. 在挂载文件系统、初始化超级块时实例化super_operations方法表;
3. 在打开文件、创建inode时调用super_operations方法表的read_inode方法,从而根据文件的类型将inode.i_op实例化为具体的file_operations方法表,比如正规文件的jffs2_file_operations方法表;
4. 在读写文件时根据file_operations接口中的函数指针调用jffs2的具体方法。

在挂载文件系统时内核为之创建VFS的super_block数据结构,以及根目录的inode、dentry等数据结构。挂载根文件系统时函数调用链如下:(“>”表示调用)

mount_root > mount_block_root > sys_mount > do_mount > get_sb_bdev > read_super > jffs2_read_super

在read_super函数中将由get_empty_super函数分配一个super_block数据结构,稍后调用相应文件系统注册的read_super方法初始化super_block数据结构(这个调用链中各个函数的细节可参见情景分析,在此不再赘述)。
jffs2_read_super函数
这个函数在初始化VFS超级块对象时为flash上所有的数据实体和文件建立内核描述符。内核描述符是数据实体和文件的“地图”,由于在flash中缺少对文件数据的索引机制,所以早在挂载文件系统时就必须建立文件及其数据实体的映射关系。而文件的其它jffs2数据结构,比如jffs2_full_dnode或jffs2_full_dirent等要在打开文件时才被创建。

static struct super_block *jffs2_read_super(struct super_block *sb, void *data, int silent)
{
 struct jffs2_sb_info *c;
 int ret;
 unsigned long j, k;
 D1(printk(KERN_DEBUG "jffs2: read_super for device %s/n", kdevname(sb->s_dev)));

 if (major(sb->s_dev) != MTD_BLOCK_MAJOR) {
  if (!silent)
   printk(KERN_DEBUG "jffs2: attempt to mount non-MTD device %s/n", kdevname(sb->s_dev));
  return NULL;
 }

在read_super函数中已经将super_block.bdev设置为jffs2文件系统所在flash分区的设备号了,再次检查设备号是否正确。

 c = JFFS2_SB_INFO(sb);
 memset(c, 0, sizeof(*c));
 sb->s_op = &jffs2_super_operations;
 c->mtd = get_mtd_device(NULL, minor(sb->s_dev));
 if (!c->mtd) {
  D1(printk(KERN_DEBUG "jffs2: MTD device #%u doesn't appear to exist/n", minor(sb->s_dev)));
  return NULL;
 }

JFFS2_SB_INFO宏返回super_block的u域(即jffs2_sb_info数据结构)的地址。首先将整个jffs2_sb_info数据结构清空,然后设置文件系统方法表的指针s_op指向jffs2_super_operations方法表,它提供了访问整个文件系统的基本方法。

static struct super_operations jffs2_super_operations =
{
 read_inode: jffs2_read_inode,
 put_super: jffs2_put_super,
 write_super: jffs2_write_super,
 statfs:  jffs2_statfs,
 remount_fs: jffs2_remount_fs,
 clear_inode: jffs2_clear_inode
};

作为策略的上层VFS框架通过各种数据结构中的函数指针接口实现与提供机制的底层具体文件系统的隔离。在VFS数据结构中只定义了一个指向具体方法表的指针变量,而由具体文件系统实现方法表中定义的各个函数(当然如果具体文件系统不支持某些方法也可以不用实现,因此上层VFS框架在调用函数指针指向的底层方法之前必须首先检查函数指针是否有效,即底层是否支持具体的方法。题外话。),并定义函数指针接口变量,最后将VFS数据结构中的方法表指针指向这个函数指针接口即可。

另外一个关键设置就是将jffs2_sb_info.mtd指向在初始化flash设备驱动程序时创建的mtd_info数据结构,它物理上描述了整个flash板块并提供了访问flash的底层驱动程序。从后文可见,jffs2方法最终通过调用flash驱动程序中将数据实体jffs2_raw_dirent或jffs2_raw_inode及后继数据块写入flash(或从中读出)。

 j = jiffies;
 ret = jffs2_do_fill_super(sb, data, silent);
 k = jiffies;
 if (ret) {
  put_mtd_device(c->mtd);
  return NULL;
 }
 printk("JFFS2 mount took %ld jiffies/n", k-j);
 return sb;
}

真正初始化VFS超级块super_block数据结构、为flash上所有数据实体建立内核描述符jffs2_raw_node_ref、为所有文件创建内核描述符jffs2_inode_cache的任务交给jffs2_do_fill_super函数完成。
jffs2_do_fill_super函数
int jffs2_do_fill_super(struct super_block *sb, void *data, int silent)
{
 struct jffs2_sb_info *c;
 struct inode *root_i;
 int ret;

 c = JFFS2_SB_INFO(sb);
 c->sector_size = c->mtd->erasesize;
 c->flash_size = c->mtd->size;
 if (c->flash_size < 4*c->sector_size) {
  printk(KERN_ERR "jffs2: Too few erase blocks (%d)/n", c->flash_size / c->sector_size);
  return -EINVAL;
 }
 c->cleanmarker_size = sizeof(struct jffs2_unknown_node);
 if (jffs2_cleanmarker_oob(c)) {  /* Cleanmarker is out-of-band, so inline size zero */
  c->cleanmarker_size = 0;
 }

首先根据mtd_info数据结构的相应域来设置jffs2_sb_info中与flash参数有关的域:擦除块大小和分区大小(mtd_info数据结构在flash驱动程序初始化中已创建好)。jffs2驱动在成功擦除了一个擦除块后,要写入类型为CLEANMARKER的数据实体来标记擦除成功完成。如果为NOR flash,则CLEANMARKER写在擦除块内部,cleanmarker_size即为该数据实体的大小;如果为NAND flash,则它写在oob(Out_Of_Band)区间内而不占用擦除块空间,所以将cleanmarker_size清0。(NAND flash可以看作是一组“page”,每个page都有一个oob空间。在oob空间内可以存放ECC(Error CorreCtion)代码、或标识含有错误的擦除块的信息、或者与文件系统相关的信息。jffs2就利用了oob来存放CLEANMARKER)

 if (c->mtd->type == MTD_NANDFLASH) {
  /* Initialise write buffer */
  c->wbuf_pagesize = c->mtd->oobblock;
  c->wbuf_ofs = 0xFFFFFFFF;
  c->wbuf = kmalloc(c->wbuf_pagesize, GFP_KERNEL);
  if (!c->wbuf)
   return -ENOMEM;
  /* Initialize process for timed wbuf flush */
  INIT_TQUEUE(&c->wbuf_task,(void*) jffs2_wbuf_process, (void *)c);
  /* Initialize timer for timed wbuf flush */
  init_timer(&c->wbuf_timer);
  c->wbuf_timer.function = jffs2_wbuf_timeout;
  c->wbuf_timer.data = (unsigned long) c;
 }

NAND flash由一组“page”组成,若干page组成一个擦除块。读写操作的最小单元是page,擦除操作的最小单元是擦除块。flash描述符mtd_info的oobblock域即page的大小,所以这里分配oobblock大小的写缓冲区,以及周期地将该写缓冲区刷新(或同步)到flash的内核定时器及一个任务队列元素。由内核定时器周期性地把jffs2_sb_info.wbuf_task通过schedule_task函数调度给keventd执行,相应的回调函数为jffs2_wbuf_process,它将jffs2_sb_info.wbuf写缓冲区的内容写回flash。(注意,flash的写操作可能阻塞,因此必须放到进程上下文中进行,所以交给keventd来完成)

 c->inocache_list = kmalloc(INOCACHE_HASHSIZE * sizeof(struct jffs2_inode_cache *),
GFP_KERNEL);
 if (!c->inocache_list) {
  ret = -ENOMEM;
  goto out_wbuf;
 }
 memset(c->inocache_list, 0, INOCACHE_HASHSIZE * sizeof(struct jffs2_inode_cache *));

如前所述flash上的任何文件都有唯一的内核描述符jffs2_inode_cache数据结构,用于建立文件及其数据实体之间的映射关系,在挂载文件系统创建。在打开文件、创建inode时,用inode的u域即jffs2_inode_info数据结构的inocache域指向它,参见图3。所有文件的jffs2_inode_cache数据结构又被组织到一张哈希表里,由jffs2_sb_info.inocache_list指向。

 if ((ret = jffs2_do_mount_fs(c)))
  goto out_inohash;
 ret = -EINVAL;

这个函数完成挂载jffs2文件系统的绝大部分工作,详见下文分析,这里仅罗列之:
1. 创建擦除块描述符数组jffs2_sb_info.blocks[]数组,初始化jffs2_sb_info的相应域;
2. 扫描整个flash分区,为所有的数据实体建立内核描述符jffs2_raw_node_ref、为所有的文件创建内核描述符jffs2_inode_cache;
3. 将所有文件的jffs2_inode_cache加入hash表,检查flash上所有数据实体的有效性(注意,只检查了数据实体jffs2_raw_dirent或jffs2_raw_inode自身的crc校验值,而把后继数据的crc校验工作延迟到了真正打开文件时,参见jffs2_scan_inode_node函数);
4. 根据擦除块的内容,将其描述符加入jffs2_sb_info中相应的xxxx_list链表。

 D1(printk(KERN_DEBUG "jffs2_do_fill_super(): Getting root inode/n"));
 root_i = iget(sb, 1);
 if (is_bad_inode(root_i)) {
  D1(printk(KERN_WARNING "get root inode failed/n"));
  goto out_nodes;
 }
 D1(printk(KERN_DEBUG "jffs2_do_fill_super(): d_alloc_root()/n"));
 sb->s_root = d_alloc_root(root_i);
 if (!sb->s_root)
  goto out_root_i;

为flash上所有文件、所有数据实体创建相应的内核描述符后,就已经完成了挂载jffs2文件系统的大部分工作,下面就得为根目录“/”创建VFS的inode和dentry了。除根目录外任何文件的inode和dentry等数据结构都是等到打开文件时才创建。由于根目录文件是path_walk解析路径名的出发点,所以早在挂载文件系统时就打开了根目录文件。文件系统超级块super_block的s_root指针指向根目录的dentry,而dentry的d_inode指向其inode,而inode的i_sb又指向文件系统超级块super_block。

创建inode的工作由iget内联函数完成,注意传递的第二个参数为相应inode的索引节点编号,而根目录的索引节点编号为1。iget函数的函数调用路径为:

 iget > iget4 > get_new_inode > super_operations.read_inode(指向jffs2_read_inode)

为文件创建inode时,首先根据其索引节点编号ino在索引节点哈希表inode_hashtable中查找,如果尚未创建,则调用get_new_inode函数分配一个inode数据结构,并用相应文件系统已注册的read_super方法初始化。对于ext2文件系统,相应的ext2_read_inode函数将读出磁盘索引结点,而对于jffs2文件系统,若为目录文件,则为目录文件的所有jffs2_raw_dirent目录项创建相应的jffs2_full_dirent数据结构并组织为链表,并为其惟一的jffs2_raw_inode创建jffs2_full_dnode数据结构,并由jffs2_inode_info的metedata直接指向(对符号链接和设备文件的惟一的jffs2_raw_inode的处理与此相同);若为正规文件,则为数据结点创建相应的jffs2_full_dnode和jffs2_node_frag数据结构,并由后者组织到红黑树中,最后根据文件的类型设置索引结点方法表指针inode.i_op/i_fop/i_mapping,详见后文。

#if LINUX_VERSION_CODE >= 0x20403
 sb->s_maxbytes = 0xFFFFFFFF;
#endif
 sb->s_blocksize = PAGE_CACHE_SIZE;
 sb->s_blocksize_bits = PAGE_CACHE_SHIFT;
 sb->s_magic = JFFS2_SUPER_MAGIC;
 if (!(sb->s_flags & MS_RDONLY))
  jffs2_start_garbage_collect_thread(c);
 return 0;
 out_root_i:
 iput(root_i);
 out_nodes:
 jffs2_free_ino_caches(c);
 jffs2_free_raw_node_refs(c);
 kfree(c->blocks);
 out_inohash:
 kfree(c->inocache_list);
 out_wbuf:
 if (c->wbuf)
  kfree(c->wbuf);
 return ret;
}

挂载文件系统的最后还要设置jffs2_sb_info中的几个域,比如页缓冲区中的页面大小s_blocksize,标识文件系统的“魔数”s_magic。另外,就是要启动GC(Garbage Collecting,垃圾回收)内核线程了。jffs2日志文件系统的特点就是任何修改都会向flash中写入新的数据结点,而不该动原有的数据结点。当flash可用擦除块数量低于一定的阈值后,就得唤醒GC内核线程回收所有“过时的”数据结点所占的空间了。有关GC机制详见第8章。

如果成功挂载则返回0,否则释放所有的描述符及各种获得的空间并返回ret中保存的错误码。
jffs2_do_mount_fs函数
在这个函数中仅为flash分区上所有的擦除块分配描述符并初始化各种xxxx_list链表首部,然后调用jffs2_build_filesystem函数完成挂载文件系统的绝大部分操作。

int jffs2_do_mount_fs(struct jffs2_sb_info *c)
{
 int i;

 c->free_size = c->flash_size;
 c->nr_blocks = c->flash_size / c->sector_size;
 c->blocks = kmalloc(sizeof(struct jffs2_eraseblock) * c->nr_blocks, GFP_KERNEL);
 if (!c->blocks)
  return -ENOMEM;

在挂载文件系统之前,认为整个flash都是可用的,所以设置空闲空间大小为整个flash分区的大小,并计算擦除块总数。jffs2_eraseblock数据结构是擦除块描述符,这里为分配所有擦除块描述符的空间并初始化:

 for (i=0; i<c->nr_blocks; i++) {
  INIT_LIST_HEAD(&c->blocks[i].list);
  c->blocks[i].offset = i * c->sector_size;
  c->blocks[i].free_size = c->sector_size;
  c->blocks[i].dirty_size = 0;
  c->blocks[i].wasted_size = 0;
  c->blocks[i].unchecked_size = 0;
  c->blocks[i].used_size = 0;
  c->blocks[i].first_node = NULL;
  c->blocks[i].last_node = NULL;
 }

其中offset域为擦除块在flash分区内的逻辑偏移,free_size为其大小。此时所有记录擦除块使用状况的xxxx_size域都为0,它们分别表示(按照代码中的出现顺序)擦除块中过时数据实体所占空间、由于填充和对齐浪费的空间、尚未进行crc校验的数据实体所占的空间、有效的数据实体所占的空间。一个擦除块内所有数据实体的内核描述符jffs2_raw_node_ref由其next_phys域组织成一个链表,其首尾元素分别由first_node和last_node指向。

 init_MUTEX(&c->alloc_sem);
 init_MUTEX(&c->erase_free_sem);
 init_waitqueue_head(&c->erase_wait);
 spin_lock_init(&c->erase_completion_lock);
 spin_lock_init(&c->inocache_lock);

 INIT_LIST_HEAD(&c->clean_list);
 INIT_LIST_HEAD(&c->very_dirty_list);
 INIT_LIST_HEAD(&c->dirty_list);
 INIT_LIST_HEAD(&c->erasable_list);
 INIT_LIST_HEAD(&c->erasing_list);
 INIT_LIST_HEAD(&c->erase_pending_list);
 INIT_LIST_HEAD(&c->erasable_pending_wbuf_list);
 INIT_LIST_HEAD(&c->erase_complete_list);
 INIT_LIST_HEAD(&c->free_list);
 INIT_LIST_HEAD(&c->bad_list);
 INIT_LIST_HEAD(&c->bad_used_list);
 c->highest_ino = 1;

在下面的jffs2_build_filesystem函数将根据所有擦除块的使用情况将各个擦除块的描述符插入不同的链表,这里首先初始化这些链表指针xxxx_list。文件的索引结点号在某个设备上的文件系统内部才唯一,当前flash分区的jffs2文件系统中最高的索引结点号由jffs2_sb_info的highest_ino域记录,而1是根目录“/”的索引结点号。由于jffs2中不需要象ext2那样考虑优先将文件数据放到其父目录所在块组中、以及平衡各磁盘块组中目录的数目,所以jffs2中新文件的索引结点号分配策略非常简单,直接设置为highest_ino即可,并同时递增之。参见下文。

 if (jffs2_build_filesystem(c)) {
  D1(printk(KERN_DEBUG "build_fs failed/n"));
  jffs2_free_ino_caches(c);
  jffs2_free_raw_node_refs(c);
  kfree(c->blocks);
  return -EIO;
 }
 return 0;
}

下面就是由jffs2_build_filesystem函数真正完成jffs2文件系统的挂载工作了。
jffs2_build_filesystem函数
上文罗列了jffs2_do_mount_fs函数完成挂载jffs2文件系统的绝大部分工作:
1. 创建擦除块描述符数组jffs2_sb_info.blocks[]数组,初始化jffs2_sb_info的相应域;
2. 扫描整个flash分区,为所有的数据实体建立内核描述符jffs2_raw_node_ref、为所有的文件创建内核描述符jffs2_inode_cache;
3. 将所有文件的jffs2_inode_cache加入inocache_list哈希表,检查flash上所有数据结点的有效性;
4. 根据擦除块的内容,将其描述符加入jffs2_sb_info中相应的xxxx_list链表
除了第一条外其余的工作都是由jffs2_build_filesystem函数完成的,我们分段详细分析这个函数:

static int jffs2_build_filesystem(struct jffs2_sb_info *c)
{
 int ret;
 int i;
 struct jffs2_inode_cache *ic;

 /* First, scan the medium and build all the inode caches with lists of physical nodes */
 c->flags |= JFFS2_SB_FLAG_MOUNTING;
 ret = jffs2_scan_medium(c);
 c->flags &= ~JFFS2_SB_FLAG_MOUNTING;
 if (ret)
  return ret;

由jffs2_scan_medium函数遍历flash分区上的所有的擦除块,读取每一个擦除块上的所有数据实体,建立相应的内核描述符jffs2_raw_node_ref,为每个文件建立内核描述符jffs2_inode_cache,并建立相互连接关系;如果是目录文件,则为其所有目录项创建相应的jffs2_full_dirent并组织为链表,由jffs2_inode_cache的scan_dents域指向;并将jffs2_inode_cache加入inocache_list哈希表;最后,根据擦除块的使用情况将其描述符jffs2_eraseblock加入jffs2_sb_info中的xxxx_list链表。详见下文分析。注意在挂载文件系统期间要设置超级块中的JFFS2_SB_FLAG_MOUNTING标志。

剩下的工作分为三个阶段完成:为每个文件计算硬链接计数、删除硬链接为0的文件、最后释放每个目录项jffs2_raw_dirent所对应的上层jffs2_full_dirent数据结构。

 D1(printk(KERN_DEBUG "Scanned flash completely/n"));
 D1(jffs2_dump_block_lists(c));
 /* Now scan the directory tree, increasing nlink according to every dirent found. */
 for_each_inode(i, c, ic) {
  D1(printk(KERN_DEBUG "Pass 1: ino #%u/n", ic->ino));
  ret = jffs2_build_inode_pass1(c, ic);
  if (ret) {
   D1(printk(KERN_WARNING "Eep. jffs2_build_inode_pass1 for ino %d returned %d/n",
 ic->ino, ret));
   return ret;
  }
  cond_resched();
 }

上面jffs2_scan_medium已经为flash上所有的数据实体和文件创建了内核描述符,并且进一步为所有的目录项数据实体jffs2_raw_dirent创建了临时的jffs2_full_dirent数据结构(它们将在jffs2_build_filesystem函数的最后删除,目的只是计算所有文件的硬链接计数),目录文件的作用就是实现“从路径名找到文件”的机制,其物质基础就是组成目录文件的各个目录项。在path_walk逐层解析路径名时将逐一打开各级目录文件,建立其dentry以及彼此之间的联系,此时就在内核中建立起相应的系统目录树分支。

for_each_inode宏用于访问所有文件的内核描述符,定义如下:

#define for_each_inode(i, c, ic) /
for (i=0; i<INOCACHE_HASHSIZE; i++) /
for (ic=c->inocache_list[i]; ic; ic=ic->next)

所以这里使用这个宏对每一个文件都调用了jffs2_build_inode_pass1函数,它为目录文件下的所有目录项增加其所指文件的硬链接计数。下面要再次遍历所有文件的内核描述符删除那些nlink为0的文件。如果它是目录,那么还要减小其下的所有子目录、文件的硬链接计数。这就是第2个阶段的工作:
 
上面的操作可能比较耗时,因此cond_resched宏用于让出cpu,定义如下:
#define cond_resched()   do { if need_resched() schedule(); } while(0)

 D1(printk(KERN_DEBUG "Pass 1 complete/n"));
 D1(jffs2_dump_block_lists(c));
/* Next, scan for inodes with nlink == 0 and remove them. If they were directories, then decrement the nlink of their children too, and repeat the scan. As that's going to be a fairly uncommon occurrence, it's not so evil to do it this way. Recursion bad. */
 do {
  D1(printk(KERN_DEBUG "Pass 2 (re)starting/n"));
  ret = 0;
  for_each_inode(i, c, ic) {
   D1(printk(KERN_DEBUG "Pass 2: ino #%u, nlink %d, ic %p, nodes %p/n", ic->ino, ic->nlink, ic,
ic->nodes));
   if (ic->nlink)
    continue;
   /* XXX: Can get high latency here. Move the cond_resched() from the end of the loop? */
   ret = jffs2_build_remove_unlinked_inode(c, ic);
   if (ret)
    break;
/* -EAGAIN means the inode's nlink was zero, so we deleted it, and furthermore that it had children and their nlink has now gone to zero too. So we have to restart the scan. */
  }
  D1(jffs2_dump_block_lists(c));
  cond_resched();
 } while(ret == -EAGAIN);
 D1(printk(KERN_DEBUG "Pass 2 complete/n"));
 
最后第3阶段要释放jffs2_full_dirent数据结构了,它们在挂载文件系统时就建立就是为了统计各文件的硬链接计数,此时其使命已经完成。

 /* Finally, we can scan again and free the dirent nodes and scan_info structs */
 for_each_inode(i, c, ic) {
  struct jffs2_full_dirent *fd;
  D1(printk(KERN_DEBUG "Pass 3: ino #%u, ic %p, nodes %p/n", ic->ino, ic, ic->nodes));

  while(ic->scan_dents) {
   fd = ic->scan_dents;
   ic->scan_dents = fd->next;
   jffs2_free_full_dirent(fd);
  }
  ic->scan_dents = NULL;
  cond_resched();
 }
 D1(printk(KERN_DEBUG "Pass 3 complete/n"));
 D1(jffs2_dump_block_lists(c));

 /* Rotate the lists by some number to ensure wear levelling */
 jffs2_rotate_lists(c);
 return ret;
}

先前在jffs2_scan_medium函数中为每个jffs2_raw_dirent目录项建立了临时的jffs2_full_dirent,这里逐一删除,同时把每个目录文件的jffs2_inode_cache.scan_dents域设置为NULL(其它文件的这个域本来就是NULL),以标记数据实体内核描述符jffs2_raw_node_ref的next_in_ino域组成的链表的末尾。
(jffs2_rotate_list的作用?及与wear leveling算法的关系如何?)
jffs2_scan_medium函数
这个函数遍历flash分区上的所有的擦除块,执行操作:
1. 读取每一个擦除块上的所有数据实体建立相应的内核描述符jffs2_raw_node_ref;
2. 为每个文件建立内核描述符jffs2_inode_cache,并建立相互连接关系;
3. 为目录文件的所有目录项创建相应的jffs2_full_dirent并组织为链表,由jffs2_inode_cache的scan_dents域指向;(注:这个目录树仅在jffs2_build_filesystem函数内部使用,在后面通过jffs2_build_inode_pass1函数计算完所有文件的硬链接个数nlink后,在jffs2_build_filesystem函数退出前就被删除了。)
4. 将所有文件的jffs2_inode_cache加入inocache_list哈希表;
5. 根据擦除块的使用情况将其描述符jffs2_eraseblock加入jffs2_sb_info中的xxxx_list链表。

int jffs2_scan_medium(struct jffs2_sb_info *c)
{
 int i, ret;
 uint32_t empty_blocks = 0, bad_blocks = 0;
 unsigned char *flashbuf = NULL;
 uint32_t buf_size = 0;
 size_t pointlen;

 if (!c->blocks) {
  printk(KERN_WARNING "EEEK! c->blocks is NULL!/n");
  return -EINVAL;
 }
 if (c->mtd->point) {
  ret = c->mtd->point (c->mtd, 0, c->mtd->size, &pointlen, &flashbuf);
  if (!ret && pointlen < c->mtd->size) {
   /* Don't muck about if it won't let us point to the whole flash */
   D1(printk(KERN_DEBUG "MTD point returned len too short: 0x%x/n", pointlen));
   c->mtd->unpoint(c->mtd, flashbuf);
   flashbuf = NULL;
  }
  if (ret)
   D1(printk(KERN_DEBUG "MTD point failed %d/n", ret));
 }

NOR flash允许“就地运行”(XIP,即eXecute_In_Place),比如在系统加电时引导程序的前端就是在flash上就地运行的。读NOR flash的操作与读sdram类似,而flash驱动中的读方法(read或者read_ecc)的本质操作为memcpy,所以通过内存映射读取flash比通过其读方法要节约一次内存拷贝。

如果NOR flash驱动程序实现了point和unpoint方法,则允许建立内存映射。point函数的第2、3个参数指定了被内存映射的区间,而实际被内存映射的区间长度由pointlen返回,起始虚拟地址存放在mtdbuf所指变量中。这里试图内存映射整个flash(传递的第2、3个参数为0和mtd->size),如果实际被映射的长度pointlen小于flash大小,则用unpoint拆除内存映射。

 if (!flashbuf) {
  /* For NAND it's quicker to read a whole eraseblock at a time, apparent

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值