为方便学习放在这里,感谢作者
声明
你可以自由地随意修改本文档的任何文字内容及图表,但是如果你在自己的文档中以任何形式直接引用了本文档的任何原有文字或图表并希望发布你的文档,那么你也得保证让所有得到你的文档的人同时享有你曾经享有过的权利。
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, apparently */
if (jffs2_cleanmarker_oob(c))
buf_size = c->sector_size;
else
buf_size = PAGE_SIZE;
D1(printk(KERN_DEBUG "Allocating readbuf of %d bytes/n", buf_size));
flashbuf = kmalloc(buf_size, GFP_KERNEL);
if (!flashbuf)
return -ENOMEM;
}
如果flashbuf为空,即尚未建立flash的直接内存映射,那么需要额外分配一个内核缓冲区用于读出flash的内容。对于NAND flash根据作者的注释,一次性读出整个擦除块更快,所以缓冲区大小为擦除块大小;对于NOR flash该缓冲区大小等于一个内存页框大小。
(由此可见,如果flash驱动支持直接内存映射那么在读操作时就无需分配额外的缓冲区了。但是,根据David Woodhouse于2005年3月的文章www.linux-mtd.infradead.org/archive/tech/mtd_info.html,函数point和unpoint的语义有待精确定义,所以目前还是不要使用的好。
另外,只有在读NOR flash时才可能会用point方法建立内存映射,而此期间会持有锁而阻塞其它写操作,所以在读操作完成后应该立即用unpoint拆除内存映射、释放锁。所以,在这里就建立内存映射是否过早,而应该推迟到真正执行读操作时?比如在执行读操作时如果可以建立内存映射则通过它读取,然后拆除之;否则由jffs2_flash_read函数通过mtd->read方法读出)
for (i=0; i<c->nr_blocks; i++) {
struct jffs2_eraseblock *jeb = &c->blocks[i];
ret = jffs2_scan_eraseblock(c, jeb, buf_size?flashbuf:(flashbuf+jeb->offset), buf_size);
if (ret < 0)
return ret;
jffs2_scan_eraseblock函数完成了jff2_scan_medium函数前4条工作,详见后文。它根据擦除块的内的数据信息返回描述擦除块状态的数值。然后,就得根据状态信息将擦除块描述符组织到jffs2_sb_info的不同的xxxx_list链表中去了。具体的工作在一个switch结构中完成:
ACCT_PARANOIA_CHECK(jeb);
/* Now decide which list to put it on */
switch(ret) {
case BLK_STATE_ALLFF:
/* Empty block. Since we can't be sure it was entirely erased, we just queue it for erase
again. It will be marked as such when the erase is complete. Meanwhile we still count it as
empty for later checks. */
empty_blocks++;
list_add(&jeb->list, &c->erase_pending_list);
c->nr_erasing_blocks++;
break;
如果该擦除块上为全1,即没有任何信息,当然也没有CLEANMARKER数据实体,则将该擦除块描述符加入erase_pending_list链表,同时增加相应的引用计数。该链表中的擦除块即将被擦除,成功擦除后要在擦除块的开始写入CLEANMARKER(在jffs2_scan_eraseblock函数中返回该值的代码位置)。
case BLK_STATE_CLEANMARKER: /* Only a CLEANMARKER node is valid */
if (!jeb->dirty_size) { /* It's actually free */
list_add(&jeb->list, &c->free_list);
c->nr_free_blocks++;
} else { /* Dirt */
D1(printk(KERN_DEBUG "Adding all-dirty block at 0x%08x to erase_pending_list/n",
jeb->offset));
list_add(&jeb->list, &c->erase_pending_list);
c->nr_erasing_blocks++;
}
break;
如果擦除块中只有一个CLEANMARKER数据实体是有效的,而且的确擦除块描述符中dirty_size也为0,即擦除块中没有任何过时数据实体,则将其描述符加入free_list中,否则加入erase_pending_list中。
case BLK_STATE_CLEAN: /* Full (or almost full) of clean data. Clean list */
list_add(&jeb->list, &c->clean_list);
break;
如果擦除块中基本上都是有效的数据,则将其加入clean_list链表。
case BLK_STATE_PARTDIRTY: /* Some data, but not full. Dirty list. */
/*Except that we want to remember the block with most free space,
and stick it in the 'nextblock' position to start writing to it. Later when we do snapshots, this
must be the most recent block, not the one with most free space. */
if (jeb->free_size > 2*sizeof(struct jffs2_raw_inode) &&
(jffs2_can_mark_obsolete(c) || jeb->free_size > c->wbuf_pagesize) &&
(!c->nextblock || c->nextblock->free_size < jeb->free_size)) {
/* Better candidate for the next writes to go to */
if (c->nextblock) {
c->nextblock->dirty_size += c->nextblock->free_size + c->nextblock->wasted_size;
c->dirty_size += c->nextblock->free_size + c->nextblock->wasted_size;
c->free_size -= c->nextblock->free_size;
c->wasted_size -= c->nextblock->wasted_size;
c->nextblock->free_size = c->nextblock->wasted_size = 0;
if (VERYDIRTY(c, c->nextblock->dirty_size)) {
list_add(&c->nextblock->list, &c->very_dirty_list);
} else {
list_add(&c->nextblock->list, &c->dirty_list);
}
}
c->nextblock = jeb;
} else {
jeb->dirty_size += jeb->free_size + jeb->wasted_size;
c->dirty_size += jeb->free_size + jeb->wasted_size;
c->free_size -= jeb->free_size;
c->wasted_size -= jeb->wasted_size;
jeb->free_size = jeb->wasted_size = 0;
if (VERYDIRTY(c, jeb->dirty_size)) {
list_add(&jeb->list, &c->very_dirty_list);
} else {
list_add(&jeb->list, &c->dirty_list);
}
}
break;
如果该擦除块含有至少一个过时的数据实体,那么就把它加入dirty_list或者very_dirty_list链表。宏VERYDIRTY定义如下:
#define VERYDIRTY(c, size) ((size) >= ((c)->sector_size / 2))
即如果过时数据实体所占空间超过擦除块的一半大小,则认为该擦除块“很脏”。
jffs2_sb_info的nextblock指向当前写入操作发生的擦除块。如果当前擦除块的剩余空间大小超过nextblock所指的擦除块,则将nextblock指向当前擦除块,而把原先的擦除块加入(very)dirty_list。
(为什么要这样做?为什么在加入(very)dirty_list前要把擦除块的free_size和wasted_size的大小都记入dirty_size?并且将二者清0。)
case BLK_STATE_ALLDIRTY:
/* Nothing valid - not even a clean marker. Needs erasing. */
/* For now we just put it on the erasing list. We'll start the erases later */
D1(printk(KERN_NOTICE "JFFS2: Erase block at 0x%08x is not formatted.
It will be erased/n", jeb->offset));
list_add(&jeb->list, &c->erase_pending_list);
c->nr_erasing_blocks++;
break;
如果这个擦除块中全都是过时的数据实体,设置没有CLEANMARKER,那么将它加入erase_pending_list等待擦除。
case BLK_STATE_BADBLOCK:
D1(printk(KERN_NOTICE "JFFS2: Block at 0x%08x is bad/n", jeb->offset));
list_add(&jeb->list, &c->bad_list);
c->bad_size += c->sector_size;
c->free_size -= c->sector_size;
bad_blocks++;
break;
最后,如果这个擦除块已经损坏,那么将其加入bad_list链表,并增加bad_size计数,减小flash分区大小计数free_size。
(如何判断擦除块已经损坏?)
default:
printk(KERN_WARNING "jffs2_scan_medium(): unknown block state/n");
BUG();
}//switch
}//for
至此,已经为所有擦除块上的所有数据实体和文件都建立了内核描述符,并且根据擦除块的使用情况将其描述符加入了合适的xxxx_list链表。结束前还得做些额外工作:
/* Nextblock dirty is always seen as wasted, because we cannot recycle it now */
if (c->nextblock && (c->nextblock->dirty_size)) {
c->nextblock->wasted_size += c->nextblock->dirty_size;
c->wasted_size += c->nextblock->dirty_size;
c->dirty_size -= c->nextblock->dirty_size;
c->nextblock->dirty_size = 0;
}
jffs2_sb_info的nextblock指向当前正在写入的擦除块(即被写入新的数据结点的擦除块。注意在jffs2上数据结点是顺序地写入flash的)。根据作者的注释,其过时的数据实体所占的空间被计算入被浪费的空间wasted_size,而这又是因为当前擦除块没办法被recycle。什么意思?
if (!jffs2_can_mark_obsolete(c) && c->nextblock && (c->nextblock->free_size & (c->wbuf_pagesize-1))) {
/* If we're going to start writing into a block which already contains data, and the end of the data isn't page-aligned, skip a little and align it. */
uint32_t skip = c->nextblock->free_size & (c->wbuf_pagesize-1);
D1(printk(KERN_DEBUG "jffs2_scan_medium(): Skipping %d bytes in nextblock to
ensure page alignment/n", skip));
c->nextblock->wasted_size += skip;
c->wasted_size += skip;
c->nextblock->free_size -= skip;
c->free_size -= skip;
}
根据作者的注释,如果当前正在被写入的擦除块的可用空间的大小不是页地址对齐的,那么跳过其开头的部分空间,到达页地址对齐处。因此,无论擦除块描述符jffs2_eraseblock还是jffs2_sb_info中的wasted_size域都要相应地增加,free_size域都要相应地减少。
另外需要说明的是,在当前开发板上使用的是NOR flash,所以CONFIG_JFFS2_FS_NAND宏未定义,所以jffs2_can_mark_obsolete(c)被定义为1,所以会跳过这段代码。
if (c->nr_erasing_blocks) {
if ( !c->used_size && ((empty_blocks+bad_blocks)!= c->nr_blocks || bad_blocks == c->nr_blocks) ) {
printk(KERN_NOTICE "Cowardly refusing to erase blocks on filesystem with
no valid JFFS2 nodes/n");
printk(KERN_NOTICE "empty_blocks %d, bad_blocks %d, c->nr_blocks %d/n",
empty_blocks,bad_blocks,c->nr_blocks);
return -EIO;
}
jffs2_erase_pending_trigger(c);
}
if (buf_size)
kfree(flashbuf);
else
c->mtd->unpoint(c->mtd, flashbuf);
return 0;
}
最后,在挂载完整个文件系统后,如果有需要立即擦除的擦除块则通过jffs2_erase_pending_trigger函数设置文件系统超级块中的s_dirt标志,并且释放缓存擦除块内容的缓冲区。
jffs2_scan_eraseblock函数
该函数解析一个擦除块:
1. 为擦除块中所有数据实体建立相应的内核描述符jffs2_raw_node_ref;
2. 为擦除块中的每个文件建立内核描述符jffs2_inode_cache,并建立相互连接关系;
3. 为擦除块中的每个目录文件的所有目录项创建相应的jffs2_full_dirent并组织到jffs2_inode_cache的scan_dents链表中去;
4. 将擦除块中所有文件内核描述符加入inocache_list哈希表。
static int jffs2_scan_eraseblock (struct jffs2_sb_info *c, struct jffs2_eraseblock *jeb,
unsigned char *buf, uint32_t buf_size) {
struct jffs2_unknown_node *node;
struct jffs2_unknown_node crcnode;
uint32_t ofs, prevofs;
uint32_t hdr_crc, buf_ofs, buf_len;
int err;
int noise = 0;
int wasempty = 0;
uint32_t empty_start = 0;
#ifdef CONFIG_JFFS2_FS_NAND
int cleanmarkerfound = 0;
#endif
ofs = jeb->offset;
prevofs = jeb->offset - 1;
D1(printk(KERN_DEBUG "jffs2_scan_eraseblock(): Scanning block at 0x%x/n", ofs));
#ifdef CONFIG_JFFS2_FS_NAND
if (jffs2_cleanmarker_oob(c)) {
int ret = jffs2_check_nand_cleanmarker(c, jeb);
D2(printk(KERN_NOTICE "jffs_check_nand_cleanmarker returned %d/n",ret));
/* Even if it's not found, we still scan to see if the block is empty. We use this information
to decide whether to erase it or not. */
switch (ret) {
case 0: cleanmarkerfound = 1; break;
case 1: break;
case 2: return BLK_STATE_BADBLOCK;
case 3: return BLK_STATE_ALLDIRTY; /* Block has failed to erase min. once */
default: return ret;
}
}
#endif
buf_ofs = jeb->offset;
设置ofs和buf_ofs为该擦除块在flash分区内的偏移。上面的代码与NAND类型的flash有关,在此略过。
if (!buf_size) {
buf_len = c->sector_size;
} else {
buf_len = EMPTY_SCAN_SIZE; //1024
err = jffs2_fill_scan_buf(c, buf, buf_ofs, buf_len);
if (err)
return err;
}
通过jffs2_fill_scan_buf函数读取flash分区上偏移为buf_ofs、长度为buf_len的数据到buf缓冲区中。这个函数就是直接调用jffs2_flash_read函数,而后者为直接调用flash驱动程序read方法的宏:
#define jffs2_flash_read(c, ofs, len, retlen, buf) ((c)->mtd->read((c)->mtd, ofs, len, retlen, buf))
对于NAND flash该缓冲区大小等于擦除块大小,所以这里就可以读出整个擦除块的内容;对于NOR flash,缓冲区大小只等于一个页框,所以这里只能读出擦除块首部一个页面大小的内容,而在后文的while循环中逐页读出整个擦除块。
/* We temporarily use 'ofs' as a pointer into the buffer/jeb */
ofs = 0;
/* Scan only 4KiB of 0xFF before declaring it's empty */
while(ofs < EMPTY_SCAN_SIZE && *(uint32_t *)(&buf[ofs]) == 0xFFFFFFFF)
ofs += 4;
前面ofs和buf_ofs都是一个擦除块在flash分区内的逻辑偏移,从此开始将ofs用作指向缓冲区buf内部的指针。如果buf中所有的数据都是0xFF(注意buf的长度就是EMPTY_SCAN_SIZE),则ofs到达EMPTY_SCAN_SIZE处,否则指向第一个非1字节。
if (ofs == EMPTY_SCAN_SIZE) {
#ifdef CONFIG_JFFS2_FS_NAND
if (jffs2_cleanmarker_oob(c)) {
/* scan oob, take care of cleanmarker */
int ret = jffs2_check_oob_empty(c, jeb, cleanmarkerfound);
D2(printk(KERN_NOTICE "jffs2_check_oob_empty returned %d/n",ret));
switch (ret) {
case 0: return cleanmarkerfound ? BLK_STATE_CLEANMARKER : BLK_STATE_ALLFF;
case 1: return BLK_STATE_ALLDIRTY;
case 2: return BLK_STATE_BADBLOCK; /* case 2/3 are paranoia checks */
case 3: return BLK_STATE_ALLDIRTY; /* Block has failed to erase min. once */
default: return ret;
}
}
#endif
D1(printk(KERN_DEBUG "Block at 0x%08x is empty (erased)/n", jeb->offset));
return BLK_STATE_ALLFF; /* OK to erase if all blocks are like this */
}
如果ofs果然到达EMPTY_SCAN_SIZE处,则认为整个擦除块都是全1(EMPTY_SCAN_SIZE只有1k,而擦除块大小比如为256k),所以返回BLK_STATE_ALLFF。(当从jffs2_scan_eraseblock函数返回到jff2_scan_medium后,在jff2_scan_medium中根据返回值BLK_STATE_ALLFF将当前擦除块加入jffs2_sb_info.erase_pending_list链表)
if (ofs) {
D1(printk(KERN_DEBUG "Free space at %08x ends at %08x/n", jeb->offset,
jeb->offset + ofs));
DIRTY_SPACE(ofs);
}
如果ofs没有到达EMPTY_SCAN_SIZE处,则指向buf中第一个非1字节。那么该擦除块开头这ofs长度的空间将无法被利用,所以用DIRTY_SIZE宏修改jffs2_eraseblock和jffs2_sb_info的free_size和dirty_size:
#define DIRTY_SPACE(x) do { typeof(x) _x = (x); /
c->free_size -= _x; c->dirty_size += _x; /
jeb->free_size -= _x ; jeb->dirty_size += _x; /
}while(0)
注意从这里开始统计擦除块的使用情况,设置jffs2_eraseblock和jffs2_sb_info中的xxxx_size域。
/* Now ofs is a complete physical flash offset as it always was... */
ofs += jeb->offset;
原来ofs指该buf内部的相对地址,而擦除块块的在flash分区的逻辑偏移为jeb->offset,所以加上后者后ofs就是在flash分区内的逻辑偏移了。下面从已经读出的buf_len个数据开始遍历整个擦除块。虽然读出的数据量buf_len可能小于擦除块大小sector_size,但是从下文可知,如果在buf的末尾含有一个数据实体的部分数据,则会接着读出flash分区中ofs开始、长度为buf_len的后继数据(ofs即指向该数据实体的起始偏移)。
noise = 10;
while(ofs < jeb->offset + c->sector_size) {
D1(ACCT_PARANOIA_CHECK(jeb));
cond_resched();
ofs为当前擦除块的某个字节在flash分区内的逻辑偏移,而“jeb->offset + c->sector_size”为当前擦除块在分区内的后继字节地址,即在一个循环中遍历整个擦除块。
新的一轮循环开始时,ofs所指字节单元可能包括各种情况。在循环的开始首先处理一些特殊状况:
if (ofs & 3) {
printk(KERN_WARNING "Eep. ofs 0x%08x not word-aligned!/n", ofs);
ofs = (ofs+3)&~3;
continue;
}
flash上的数据实体都要求是4字节地址对齐的,所以如果如果没有地址对齐,则步进ofs到地址对齐处并开始新的循环。
if (ofs == prevofs) {
printk(KERN_WARNING "ofs 0x%08x has already been seen. Skipping/n", ofs);
DIRTY_SPACE(4);
ofs += 4;
continue;
}
prevofs = ofs;
循环开始前,prevofs被设置为“jeb->offset - 1”,即当前擦除块的前驱字节地址,所以对于第一个非1数据字节的位置ofs一定不会等于prevofs。而以后prevofs就用于记录已经遍历过的地址。如果当前地址ofs已经遍历过,则跳过4个字节。
if (jeb->offset + c->sector_size < ofs + sizeof(*node)) {
D1(printk(KERN_DEBUG "Fewer than %d bytes left to end of block. (%x+%x<%x+%x)
Not reading/n", sizeof(struct jffs2_unknown_node),
jeb->offset, c->sector_size, ofs, sizeof(*node)));
DIRTY_SPACE((jeb->offset + c->sector_size)-ofs);
break;
}
再次重申,ofs为当前擦除块内非1字节在flash分区中的逻辑偏移,而sizeof(*node)为所有的数据实体的头部大小(4字节)。如果在当前擦除块的末尾无法容纳一个数据实体的头部信息,那么从ofs开始到当前擦除块末尾的空间(“jeb->offset + c->sector_size - ofs”)将不会被利用,所以应该计算为“dirty”。另外,这种情况也代表这当前擦除块遍历完毕,所以通过break跳出循环。
if (buf_ofs + buf_len < ofs + sizeof(*node)) {
buf_len = min_t(uint32_t, buf_size, jeb->offset + c->sector_size - ofs);
D1(printk(KERN_DEBUG "Fewer than %d bytes (node header) left to end of buf. Reading 0x%x
at 0x%08x/n",sizeof(struct jffs2_unknown_node), buf_len, ofs));
err = jffs2_fill_scan_buf(c, buf, ofs, buf_len);
if (err)
return err;
buf_ofs = ofs;
}
当从擦除块中读出第一个块长度为buf_len的数据时,buf_ofs为擦除块的分区偏移。如果先前读出的数据量的末尾没有包含一个完整的数据实体的头部,则从ofs开始,即该数据实体头部开始,再读出长度buf_len的数据,以便下面用node数据结构取出数据实体的头部:
node = (struct jffs2_unknown_node *)&buf[ofs-buf_ofs];
假设从ofs开始发现了一个jffs2_raw_inode/dirent,那么应该到达下面发现JFFS2_MAGIC_BITMASK的情况。我们先跳过下面这部分代码。
if (*(uint32_t *)(&buf[ofs-buf_ofs]) == 0xffffffff) {
uint32_t inbuf_ofs = ofs - buf_ofs + 4;
uint32_t scanend;
empty_start = ofs;
ofs += 4;
/* If scanning empty space after only a cleanmarker, don't bother scanning the whole block */
if (unlikely(empty_start == jeb->offset + c->cleanmarker_size &&
jeb->offset + EMPTY_SCAN_SIZE < buf_ofs + buf_len))
scanend = jeb->offset + EMPTY_SCAN_SIZE - buf_ofs;
else
scanend = buf_len;
D1(printk(KERN_DEBUG "Found empty flash at 0x%08x/n", ofs));
while (inbuf_ofs < scanend) {
if (*(uint32_t *)(&buf[inbuf_ofs]) != 0xffffffff)
goto emptyends;
inbuf_ofs+=4;
ofs += 4;
}
/* Ran off end. */
D1(printk(KERN_DEBUG "Empty flash ends normally at 0x%08x/n", ofs));
if (buf_ofs == jeb->offset && jeb->used_size == PAD(c->cleanmarker_size) &&
!jeb->first_node->next_in_ino && !jeb->dirty_size)
return BLK_STATE_CLEANMARKER;
wasempty = 1;
continue;
} else if (wasempty) {
emptyends:
//printk(KERN_WARNING "Empty flash at 0x%08x ends at 0x%08x/n", empty_start, ofs);
DIRTY_SPACE(ofs-empty_start);
wasempty = 0;
continue;
}
if (ofs == jeb->offset && je16_to_cpu(node->magic) == KSAMTIB_CIGAM_2SFFJ) {
printk(KERN_WARNING "Magic bitmask is backwards at offset 0x%08x.
Wrong endian filesystem?/n", ofs);
DIRTY_SPACE(4);
ofs += 4;
continue;
}
如果数据实体的头部node的第一个字节(魔数)为KSAMTIB_CIGAM_2SFFJ,则说明jffs2文件系统中数据实体的字节序错误。正常情况下应该等于JFFS2_MAGIC_BITMASK。它们的定义为:
/* Values we may expect to find in the 'magic' field */
#define JFFS2_OLD_MAGIC_BITMASK 0x1984
#define JFFS2_MAGIC_BITMASK 0x1985
#define KSAMTIB_CIGAM_2SFFJ 0x5981 /* For detecting wrong-endian fs */
#define JFFS2_EMPTY_BITMASK 0xffff
#define JFFS2_DIRTY_BITMASK 0x0000
if (je16_to_cpu(node->magic) == JFFS2_DIRTY_BITMASK) {
D1(printk(KERN_DEBUG "Empty bitmask at 0x%08x/n", ofs));
DIRTY_SPACE(4);
ofs += 4;
continue;
}
如果头部的魔数为0,则认为是无效的数据头部,所以跳过4字节。
if (je16_to_cpu(node->magic) == JFFS2_OLD_MAGIC_BITMASK) {
//printk(KERN_WARNING "Old JFFS2 bitmask found at 0x%08x/n", ofs);
//printk(KERN_WARNING "You cannot use older JFFS2 filesystems with newer kernels/n");
DIRTY_SPACE(4);
ofs += 4;
continue;
}
如果jffs文件系统映象是用版本1的工具生成的,那么显然不能与版本2的代码工作在一起,所以要跳过整个数据实体头部。
上面已经考虑了魔数的各种其它情况,代码执行到这里魔数就应该为JFFS2_MAGIC_BITMASK了。当然,如果还不是,就只能跳过4字节开始新的循环。
这种情况也是很有可能发生的,比如当数据实体头部的crc校验错误时,从下面可用看到只是简单的步进ofs四个字节,在新的循环中又会执行到“node = (struct jffs2_unknown_node *)&buf[ofs-buf_ofs];”
显然,从ofs开始的应该是错误crc头部的后继数据,那么就会进入这个if分支中。而且这种情况会重复发生,直到整个数据实体都被遍历完。但是,由于flash上数据实体的长度也是4字节地址对齐的,所以不会影响后继的数据实体!
(这也是我觉得当发现错误的crc头部时应该跳过整个数据实体的原因)
if (je16_to_cpu(node->magic) != JFFS2_MAGIC_BITMASK) {
/* OK. We're out of possibilities. Whinge and move on */
//noisy_printk(&noise, "jffs2_scan_eraseblock(): Magic bitmask 0x%04x not found at 0x%08x:
0x%04x instead/n", JFFS2_MAGIC_BITMASK, ofs, je16_to_cpu(node->magic));
DIRTY_SPACE(4);
ofs += 4;
continue;
}
/* We seem to have a node of sorts. Check the CRC */
crcnode.magic = node->magic;
crcnode.nodetype = cpu_to_je16( je16_to_cpu(node->nodetype) | JFFS2_NODE_ACCURATE);
crcnode.totlen = node->totlen;
hdr_crc = crc32(0, &crcnode, sizeof(crcnode)-4);
if (hdr_crc != je32_to_cpu(node->hdr_crc)) {
//noisy_printk(&noise, "jffs2_scan_eraseblock(): Node at 0x%08x {0x%04x, 0x%04x, 0x%08x}
has invalid CRC 0x%08x (calculated 0x%08x)/n",
// ofs, je16_to_cpu(node->magic),
// je16_to_cpu(node->nodetype),
// je32_to_cpu(node->totlen),
// je32_to_cpu(node->hdr_crc),
// hdr_crc);
DIRTY_SPACE(4);
ofs += 4;
continue;
}
计算数据实体头部的crc值,并与其声称的crc值向比较。正常情况下应该相同,否则就跳过4个字节并开始新的循环。
我觉得应该跳过整个数据实体!(整个数据实体的长度为node->totlen)
if (ofs + je32_to_cpu(node->totlen) > jeb->offset + c->sector_size) {
/* Eep. Node goes over the end of the erase block. */
printk(KERN_WARNING "Node at 0x%08x with length 0x%08x would run over the end of
the erase block/n", ofs, je32_to_cpu(node->totlen));
printk(KERN_WARNING "Perhaps the file system was created with the wrong erase size?/n");
DIRTY_SPACE(4);
ofs += 4;
continue;
}
jffs2要求任何数据实体不能跨越一个擦除块。如果这种情况发生了,则跳过4字节并开始新的循环。
if (!(je16_to_cpu(node->nodetype) & JFFS2_NODE_ACCURATE)) {
/* Wheee. This is an obsoleted node */
D2(printk(KERN_DEBUG "Node at 0x%08x is obsolete. Skipping/n", ofs));
DIRTY_SPACE(PAD(je32_to_cpu(node->totlen)));
ofs += PAD(je32_to_cpu(node->totlen));
continue;
}
根据linux/jffs2.h,所有四种有效的数据结点类型中JFFS2_NODE_ACCURATE都有效,否则要跳过整个数据实体,注意头部中totlen为包括了后继数据的数据实体总长度。jffs2文件系统的flash中的数据实体有如下4种类型:
#define JFFS2_NODETYPE_DIRENT (JFFS2_FEATURE_INCOMPAT |
JFFS2_NODE_ACCURATE | 1)
#define JFFS2_NODETYPE_INODE (JFFS2_FEATURE_INCOMPAT |
JFFS2_NODE_ACCURATE | 2)
#define JFFS2_NODETYPE_CLEANMARKER (JFFS2_FEATURE_RWCOMPAT_DELETE |
JFFS2_NODE_ACCURATE | 3)
#define JFFS2_NODETYPE_PADDING (JFFS2_FEATURE_RWCOMPAT_DELETE |
JFFS2_NODE_ACCURATE | 4)
下面就得根据头部中的nodetype字段判断数据实体的类型。注意,数据实体的头部是从flash分区中偏移ofs开始的:
switch(je16_to_cpu(node->nodetype)) {
case JFFS2_NODETYPE_INODE:
if (buf_ofs + buf_len < ofs + sizeof(struct jffs2_raw_inode)) {
buf_len = min_t(uint32_t, buf_size, jeb->offset + c->sector_size - ofs);
D1(printk(KERN_DEBUG "Fewer than %d bytes (inode node) left to end of buf. Reading
0x%x at 0x%08x/n", sizeof(struct jffs2_raw_inode), buf_len, ofs));
err = jffs2_fill_scan_buf(c, buf, ofs, buf_len);
if (err)
return err;
buf_ofs = ofs;
node = (void *)buf;
}
err = jffs2_scan_inode_node(c, jeb, (void *)node, ofs);
if (err) return err;
ofs += PAD(je32_to_cpu(node->totlen));
break; //跳出switch,开始新一轮大循环
首先,如果数据实体为jffs2_raw_inode,则由jffs2_scan_inode_node函数为之创建相应的内核描述符jffs2_raw_node_ref。如果该数据实体为某文件的第一个数据实体,则该文件的内核描述符jffs2_inode_cache尚未创建,则创建之并加入inocache_list哈希表中,然后建立数据实体描述符和文件描述符之间的连接关系。参见下文。
注意,先前读出的“flash分区中偏移为buf_ofs、长度为buf_len”的空间的末尾可能只含有部分数据实体,那么还得继续读出后继的数据块。注意,读出的后继数据块长度不会小于“jeb->offset + c->sector_size - ofs”,而jffs2要求一个数据实体不会跨越擦除块边界,所以一定能读出至少一个完整的数据实体。
扫描完该jffs2_raw_inode后,就要递增ofs为其长度,用break跳出switch结构、开始下一轮循环了。注意flash上的数据实体不但起始地址是4字节地址对齐的,而且长度也是4字节地址对齐的。递增ofs前还要由PAD宏将数据长度向上取整为4字节对齐的。
case JFFS2_NODETYPE_DIRENT:
if (buf_ofs + buf_len < ofs + je32_to_cpu(node->totlen)) {
buf_len = min_t(uint32_t, buf_size, jeb->offset + c->sector_size - ofs);
D1(printk(KERN_DEBUG "Fewer than %d bytes (dirent node) left to end of buf. Reading
0x%x at 0x%08x/n", je32_to_cpu(node->totlen), buf_len, ofs));
err = jffs2_fill_scan_buf(c, buf, ofs, buf_len);
if (err)
return err;
buf_ofs = ofs;
node = (void *)buf;
}
err = jffs2_scan_dirent_node(c, jeb, (void *)node, ofs);
if (err) return err;
ofs += PAD(je32_to_cpu(node->totlen));
break; //跳出switch,开始新一轮大循环
如果数据实体为jffs2_raw_dirent目录项,则由jffs2_scan_dirent_node函数分析之。详见下文。其余注意事项与前同。
case JFFS2_NODETYPE_CLEANMARKER:
D1(printk(KERN_DEBUG "CLEANMARKER node found at 0x%08x/n", ofs));
if (je32_to_cpu(node->totlen) != c->cleanmarker_size) {
printk(KERN_NOTICE "CLEANMARKER node found at 0x%08x has totlen 0x%x !=
normal 0x%x/n", ofs, je32_to_cpu(node->totlen), c->cleanmarker_size);
DIRTY_SPACE(PAD(sizeof(struct jffs2_unknown_node)));
ofs += PAD(sizeof(struct jffs2_unknown_node));
} else if (jeb->first_node) {
printk(KERN_NOTICE "CLEANMARKER node found at 0x%08x, not first node in block
(0x%08x)/n", ofs, jeb->offset);
DIRTY_SPACE(PAD(sizeof(struct jffs2_unknown_node)));
ofs += PAD(sizeof(struct jffs2_unknown_node));
} else {
struct jffs2_raw_node_ref *marker_ref = jffs2_alloc_raw_node_ref();
if (!marker_ref) {
printk(KERN_NOTICE "Failed to allocate node ref for clean marker/n");
return -ENOMEM;
}
marker_ref->next_in_ino = NULL;
marker_ref->next_phys = NULL;
marker_ref->flash_offset = ofs | REF_NORMAL;
marker_ref->totlen = c->cleanmarker_size;
jeb->first_node = jeb->last_node = marker_ref;
USED_SPACE(PAD(c->cleanmarker_size));
ofs += PAD(c->cleanmarker_size);
}
break;
如果数据实体为CLEANMARKER,即为一个jffs2_unknown_node数据结构,首先检查其长度,如果不符则跳过。另外,CLEANMARKER是该擦除块成功擦除后写入的第一个数据实体,所以当扫描出它时擦除块描述符的first_node指针应该为空,否则跳过。
如果一切正常,则为CLEANMARKER分配相应的内核描述符并插入擦除块first_node所指向的链表中去。
case JFFS2_NODETYPE_PADDING:
DIRTY_SPACE(PAD(je32_to_cpu(node->totlen)));
ofs += PAD(je32_to_cpu(node->totlen));
break;
如果数据实体为填充块,则跳过即可。
正常情况下flash上只有上述4中数据实体。对于其它特殊数据实体,则需要另外处理(参见jffs2作者描述jffs2的论文):
default://??
switch (je16_to_cpu(node->nodetype) & JFFS2_COMPAT_MASK) {
case JFFS2_FEATURE_ROCOMPAT:
printk(KERN_NOTICE "Read-only compatible feature node (0x%04x) found at offset
0x%08x/n", je16_to_cpu(node->nodetype), ofs);
c->flags |= JFFS2_SB_FLAG_RO;
if (!(jffs2_is_readonly(c)))
return -EROFS;
DIRTY_SPACE(PAD(je32_to_cpu(node->totlen)));
ofs += PAD(je32_to_cpu(node->totlen));
break;
如果发现这种类型的数据实体,那么整个jffs2文件系统都只能按照只读的方式挂载,所以设置文件系统超级块的u域即jffs2_sb_info的flags域的JFFS2_SB_FLAG_RO标志,同时还得检查挂载jffs2时的方式,如果不是按照只读方式挂载的,则返回错误EROFS(错误值将逆着函数调用链一直向上游传递,从而导致挂载失败)。
case JFFS2_FEATURE_INCOMPAT:
printk(KERN_NOTICE "Incompatible feature node (0x%04x) found at offset 0x%08x/n",
je16_to_cpu(node->nodetype), ofs);
return -EINVAL;
如果遇到这种类型的数据实体,则直接拒绝挂载jffs2。
case JFFS2_FEATURE_RWCOMPAT_DELETE:
D1(printk(KERN_NOTICE "Unknown but compatible feature node (0x%04x) found at offset
0x%08x/n", je16_to_cpu(node->nodetype), ofs));
DIRTY_SPACE(PAD(je32_to_cpu(node->totlen)));
ofs += PAD(je32_to_cpu(node->totlen));
break;
case JFFS2_FEATURE_RWCOMPAT_COPY:
D1(printk(KERN_NOTICE "Unknown but compatible feature node (0x%04x) found at offset
0x%08x/n", je16_to_cpu(node->nodetype), ofs));
USED_SPACE(PAD(je32_to_cpu(node->totlen)));
ofs += PAD(je32_to_cpu(node->totlen));
break;
如果遇到这两种数据实体,则直接跳过即可。
}//switch
}//default
}
D1(printk(KERN_DEBUG "Block at 0x%08x: free 0x%08x, dirty 0x%08x, used 0x%08x/n", jeb->offset,
jeb->free_size, jeb->dirty_size, jeb->used_size));
至此,已经遍历完整个擦除块的内容,函数的最后就是要根据擦除块描述符中的统计信息,返回反应其状态的数值了:
/* mark_node_obsolete can add to wasted !! */
if (jeb->wasted_size) {
jeb->dirty_size += jeb->wasted_size;
c->dirty_size += jeb->wasted_size;
c->wasted_size -= jeb->wasted_size;
jeb->wasted_size = 0;
}
如果当前擦除块的wasted_size域不为0,则将其算入dirty_size。同时刷新相关统计信息。为什么要这样?
if ((jeb->used_size + jeb->unchecked_size) == PAD(c->cleanmarker_size) &&
!jeb->first_node->next_in_ino && !jeb->dirty_size)
return BLK_STATE_CLEANMARKER;
第一个条件满足表示擦除块中有效数据实体空间和未检查空间等于一个CLEANMARKER大小,即只有一个CLEANMARKER,此时它的内核描述符中next_in_ino指针一定为NULL(因为CLEANMARKER不属于任何文件),所以第二个条件也满足;第三个条件再次检查的确没有任何过时数据实体,即dirty_size为0,则返回BLK_STATE_CLEANMARKER。
/* move blocks with max 4 byte dirty space to cleanlist */
else if (!ISDIRTY(c->sector_size - (jeb->used_size + jeb->unchecked_size))) {
c->dirty_size -= jeb->dirty_size;
c->wasted_size += jeb->dirty_size;
jeb->wasted_size += jeb->dirty_size;
jeb->dirty_size = 0;
return BLK_STATE_CLEAN;
}
参见下面的jffs2_scan_inode/dirent_node函数分析,它们在扫描完一个有效的jffs2_raw_inode或jffs2_raw_dirent数据实体后,分别用UNCHECKED_SPACE和USED_SPACE宏增加相应的统计信息。那么遍历完整个擦除块后,擦除块描述符的used_size + unchecked_size即为其上所有有效数据实体和未检查空间的大小。所以用擦除块大小sector_size减去这个值就是dirty和wasted的空间的大小。ISDIRTY宏用于判断为脏的区域大小size是否超过255:
/* check if dirty space is more than 255 Byte */
#define ISDIRTY(size) ((size) > sizeof (struct jffs2_raw_inode) + JFFS2_MIN_DATA_LEN)
所以,如果(c->sector_size - (jeb->used_size + jeb->unchecked_size))小于255,则还认为这个擦除块还是干净的,所以返回BLK_STATE_CLEAN。
else if (jeb->used_size || jeb->unchecked_size)
return BLK_STATE_PARTDIRTY;
else
return BLK_STATE_ALLDIRTY;
}
否则擦除块为脏。used_size为有效数据实体的总长度。如果任意不为0,即擦除块中含有至少一个有效数据实体,则返回“部分脏”数值BLK_STATE_PARTDIRTY,否则一个有效数据实体也没有,自然应该返回“全脏”数值BLK_STATE_ALLDIRTY。
jffs2_scan_inode_node函数
该函数为数据实体创建内核描述符jffs2_raw_node_ref,如果所属文件的内核描述符jffs2_inode_cache不存在,则创建之,并建立二者连接关系,再将jffs2_inode_cache记录到inocache_list哈希表中。参数ri为已经读出到内核缓冲区中的jffs2_raw_inode数据实体的首址,该数据实体在flash分区内的偏移为ofs。从函数中可用看到数据实体内核描述符jffs2_raw_node_ref的flash_offset域的值就由ofs设置。
static int jffs2_scan_inode_node(struct jffs2_sb_info *c, struct jffs2_eraseblock *jeb,
struct jffs2_raw_inode *ri, uint32_t ofs)
{
struct jffs2_raw_node_ref *raw;
struct jffs2_inode_cache *ic;
uint32_t ino = je32_to_cpu(ri->ino);
首先,从数据实体jffs2_raw_inode的ino字段获得其所属文件的索引结点号。
D1(printk(KERN_DEBUG "jffs2_scan_inode_node(): Node at 0x%08x/n", ofs));
/* We do very little here now. Just check the ino# to which we should attribute
this node; we can do all the CRC checking etc. later. There's a tradeoff here --
we used to scan the flash once only, reading everything we want from it into
memory, then building all our in-core data structures and freeing the extra
information. Now we allow the first part of the mount to complete a lot quicker,
but we have to go _back_ to the flash in order to finish the CRC checking, etc.
Which means that the _full_ amount of time to get to proper write mode with GC
operational may actually be _longer_ than before. Sucks to be me. */
raw = jffs2_alloc_raw_node_ref();
if (!raw) {
printk(KERN_NOTICE "jffs2_scan_inode_node(): allocation of node reference failed/n");
return -ENOMEM;
}
然后,通过kmem_cache_alloc函数从raw_node_ref_slab中分配一个jffs2_raw_node_ref数据结构。回想在注册jffs2文件系统时就已经为所有的内核描述符数据结构创建了相应的高速缓存。
ic = jffs2_get_ino_cache(c, ino);
if (!ic) {
/* Inocache get failed. Either we read a bogus ino# or it's just genuinely the
first node we found for this inode. Do a CRC check to protect against the former case */
uint32_t crc = crc32(0, ri, sizeof(*ri)-8);
if (crc != je32_to_cpu(ri->node_crc)) {
printk(KERN_NOTICE "jffs2_scan_inode_node(): CRC failed on node at 0x%08x: Read 0x%08x,
calculated 0x%08x/n", ofs, je32_to_cpu(ri->node_crc), crc);
/* We believe totlen because the CRC on the node _header_ was OK, just the node itself failed. */
DIRTY_SPACE(PAD(je32_to_cpu(ri->totlen)));
return 0;
}
ic = jffs2_scan_make_ino_cache(c, ino);
if (!ic) {
jffs2_free_raw_node_ref(raw);
return -ENOMEM;
}
}
函数的开始就已经得到了数据实体所属文件的索引结点号,这里通过jffs2_get_ino_cache函数返回相应文件的、组织在inocache_list哈希表中的jffs2_inode_cache数据结构。如果不存在则返回NULL,这说明我们遇到了属于该文件的第一个数据实体,所以还得通过jffs2_scan_make_ino_cache函数为该文件分配一个jffs2_inode_cache数据结构并加入哈希表。详见下文分析。
这里需要着重说明的是,对于jffs2_raw_inode数据实体,其ino即为其所属文件的索引结点号,所以在调用jffs2_scan_make_ino_cache函数返回其文件的内核描述符时自然传递ino。但是对于jffs2_raw_dirent目录项数据实体,ino表示其代表的文件的索引结点号,而该目录项所属的目录文件的索引结点号则由pino指明。目录项的内核描述符自然应该加入其所属目录文件的内核描述符的nodes链表中去。由下文jffs2_scan_dirent_node函数可见,其中调用jffs2_scan_make_ino_cache函数时传递的是pino。
另外,文件的jffs2_inode_cache数据结构不存在也有可能是由于前面得到的索引结点号ino错误引起的。所以在创建新的jffs2_inode_cache数据结构前,首先得验证crc以保证ino号是正确的。jffs2_raw_inode数据结构的最后两个域各为长度为4字节的crc校验值data_crc和node_crc,分别为其后压缩了的数据的crc校验值和jffs2_raw_inode数据结构本身的crc校验值。在计算jffs2_raw_inode数据结构本身的crc校验值时要排除这两个域共8字节,然后再与读出的node_crc相比较。
如果发生crc校验错误,则增加jffs2_sb_info和jffs2_eraseblock中的dirty_size值、减小free_size值,并直接退出。
/* Wheee. It worked */
raw->flash_offset = ofs | REF_UNCHECKED;
raw->totlen = PAD(je32_to_cpu(ri->totlen));
raw->next_phys = NULL;
如果一切顺利,则初始化jffs2_raw_node_ref,首先将flash_offset设置为数据实体在flash分区内的逻辑偏移。该函数的参数ofs正是这个值。由于flash上数据结点的总是4字节地址对齐的,所以jffs2_raw_node_ref的flash_offset的最低两个bit总是0,所以可用利用它们标记相应数据实体的状态。这两位可能的值定义如下:
#define REF_UNCHECKED 0 /* We haven't yet checked the CRC or built its inode */
#define REF_OBSOLETE 1 /* Obsolete, can be completely ignored */
#define REF_PRISTINE 2 /* Completely clean. GC without looking */
#define REF_NORMAL 3 /* Possibly overlapped. Read the page and write again on GC */
而PAD宏定义为:#define PAD(x) (((x)+3)&~3)。由此可见,flash上数据实体的长度也是4字节地址对齐的。next_phys用于把擦除块内所有数据实体描述符组织为一个链表,这里先初始化为NULL,在下面才加入链表。
尤其需要说明的是,根据作者的注释这里在挂载文件系统时并没有检查flash上数据实体的有效性,而把crc校验的工作推迟到了打开文件、为文件创建jffs2_full_dnode或者jffs2_full_dirent时进行(详见后文“打开文件时读inode的方法”一章中的jffs2_get_inode_nodes函数的相关部分)。所以需要在数据实体的内核描述符中设置REF_UNCHECKED标志。这样做的目的是加快文件系统的挂载过程,但是“跑得了和尚跑不了庙”,在打开文件时还得进行crc校验。但是以后并不是一定需要打开所有的文件,所以推迟crc校验还是有好处的。
另外在下文jffs2_scan_dirent_node函数分析中可以看到,对目录项数据实体即刻进行了crc校验,所以将其内核描述符设置为REF_PRINSTINE(参见jffs2_scan_dirent_node函数的相关部分)。
然后,就得建立数据实体内核描述符jffs2_raw_node_ref和其所属文件描述符jffs2_inode_cache之间的联系了:
raw->next_in_ino = ic->nodes;
ic->nodes = raw;
即将该jffs2_raw_node_ref加入到jffs2_inode_cache.nodes链表的首部。
if (!jeb->first_node)
jeb->first_node = raw;
if (jeb->last_node)
jeb->last_node->next_phys = raw;
jeb->last_node = raw;
擦除块内所有数据实体的内核描述符通过next_phys域组织成一个链表,链表的首尾元素由jffs2_eraseblock数据结构的last_node和first_node指向。
D1(printk(KERN_DEBUG "Node is ino #%u, version %d. Range 0x%x-0x%x/n",
je32_to_cpu(ri->ino), je32_to_cpu(ri->version), je32_to_cpu(ri->offset),
je32_to_cpu(ri->offset)+je32_to_cpu(ri->dsize)));
pseudo_random += je32_to_cpu(ri->version);
UNCHECKED_SPACE(PAD(je32_to_cpu(ri->totlen)));
return 0;
}
最后,由UNCHECKED_SPACE宏减少jffs2_sb_info和jffs2_eraseblock中的free_size域,增加unchecked_size域:
#define UNCHECKED_SPACE(x) do { typeof(x) _x = (x); /
c->free_size -= _x; c->unchecked_size += _x; /
jeb->free_size -= _x ; jeb->unchecked_size += _x; /
}while(0)
jffs2_scan_inode_node函数完成后,数据实体和文件的内核描述符之间的关系参见图2(为图2的一部分)。
jffs2_scan_make_ino_cache函数
这个函数为索引结点号为ino的文件分配一个新的jffs2_inode_cache数据结构并加入文件系统hash表:
static struct jffs2_inode_cache *jffs2_scan_make_ino_cache(struct jffs2_sb_info *c, uint32_t ino)
{
struct jffs2_inode_cache *ic;
ic = jffs2_get_ino_cache(c, ino);
if (ic)
return ic;
ic = jffs2_alloc_inode_cache();
if (!ic) {
printk(KERN_NOTICE "jffs2_scan_make_inode_cache(): allocation of inode cache failed/n");
return NULL;
}
memset(ic, 0, sizeof(*ic));
如果ino对应的jffs2_inode_cache已经在inocache_list哈希表中,则直接返回其地址即可。否则通过jffs2_alloc_inode_cache函数从内核高速缓存中分配一个,然后初始化之:
ic->ino = ino;
ic->nodes = (void *)ic;
jffs2_add_ino_cache(c, ic);
if (ino == 1)
ic->nlink=1;
return ic;
}
将jffs2_inode_cache.ino设置为ino,并通过jffs2_add_ino_cache函数加入inocache_list哈希表。最后如果是根目录,则将硬链接计数nlink设置为1。这是因为根目录没有父目录了,也就不存在代表其的jffs2_raw_dirent目录项(另外从jffs2map2的结果来看,根目录文件的硬链接计数为1,正是在这里设置的结果)。
jffs2_scan_dirent_node函数
在挂载文件系统时为已读出的jffs2_raw_dirent目录项数据实体创建内核描述符jffs2_raw_node_ref和临时的jff2_full_dirent。如果为相应目录的jffs2_inode_cache尚未创建则创建之,并建立三者之间的连接关系。
static int jffs2_scan_dirent_node(struct jffs2_sb_info *c, struct jffs2_eraseblock *jeb,
struct jffs2_raw_dirent *rd, uint32_t ofs)
{
struct jffs2_raw_node_ref *raw;
struct jffs2_full_dirent *fd;
struct jffs2_inode_cache *ic;
uint32_t crc;
D1(printk(KERN_DEBUG "jffs2_scan_dirent_node(): Node at 0x%08x/n", ofs));
/* We don't get here unless the node is still valid, so we don't have to mask in the ACCURATE bit any more. */
crc = crc32(0, rd, sizeof(*rd)-8);
if (crc != je32_to_cpu(rd->node_crc)) {
printk(KERN_NOTICE "jffs2_scan_dirent_node(): Node CRC failed on node at 0x%08x:
Read 0x%08x, calculated 0x%08x/n", ofs, je32_to_cpu(rd->node_crc), crc);
/* We believe totlen because the CRC on the node _header_ was OK, just the node itself failed. */
DIRTY_SPACE(PAD(je32_to_cpu(rd->totlen)));
return 0;
}
与jffs2_raw_inode不同,需要为jffs2_raw_dirent创建额外的内核描述符jffs2_full_dirent。首先进行crc校验。计算jffs2_raw_dirent的crc校验值时排除其后node_crc和name_crc两个域,共8字节,然后与node_crc相比较。如果错误则整个数据实体(jffs2_raw_inode及紧随其后的文件名)被认为为dirty,增加jffs2_sb_info和jffs2_eraseblock中的dirty_size值、减小free_size值,并退出。
pseudo_random += je32_to_cpu(rd->version);
fd = jffs2_alloc_full_dirent(rd->nsize+1);
if (!fd) {
return -ENOMEM;
}
memcpy(&fd->name, rd->name, rd->nsize);
fd->name[rd->nsize] = 0;
通过jffs2_alloc_full_dirent函数从内核高速缓存中分配一个jffs2_full_dirent数据结构。jffs2_raw_dirent其后跟随的文件名长度为nsize,而为jffs2_full_dirent.name分配空间时多分配一个字节,用来填充字符串结束符。然后将文件名复制到jffs2_full_dirent.name所指空间中。
crc = crc32(0, fd->name, rd->nsize);
if (crc != je32_to_cpu(rd->name_crc)) {
printk(KERN_NOTICE "jffs2_scan_dirent_node(): Name CRC failed on node at 0x%08x:
Read 0x%08x, calculated 0x%08x/n",ofs, je32_to_cpu(rd->name_crc), crc);
D1(printk(KERN_NOTICE "Name for which CRC failed is (now) '%s', ino #%d/n",
fd->name, je32_to_cpu(rd->ino)));
jffs2_free_full_dirent(fd);
/* FIXME: Why do we believe totlen? */
/* We believe totlen because the CRC on the node _header_ was OK, just the name failed. */
DIRTY_SPACE(PAD(je32_to_cpu(rd->totlen)));
return 0;
}
接着,就得验证文件名的crc校验值了。如果错误,则增加jffs2_sb_info和jffs2_eraseblock中的dirty_size值、减小free_size值,并退出。接下来就需要为jffs2_raw_dirent创建内核描述符jffs2_raw_node_ref,如果其目录文件的jffs2_inode_cache尚未创建,则创建之,并建立二者的连接关系。完全类似于jffs2_scan_inode_dnode函数,在此不再赘述。
raw = jffs2_alloc_raw_node_ref();
if (!raw) {
jffs2_free_full_dirent(fd);
printk(KERN_NOTICE "jffs2_scan_dirent_node(): allocation of node reference failed/n");
return -ENOMEM;
}
ic = jffs2_scan_make_ino_cache(c, je32_to_cpu(rd->pino));
if (!ic) {
jffs2_free_full_dirent(fd);
jffs2_free_raw_node_ref(raw);
return -ENOMEM;
}
raw->totlen = PAD(je32_to_cpu(rd->totlen));
raw->flash_offset = ofs | REF_PRISTINE;
raw->next_phys = NULL;
raw->next_in_ino = ic->nodes;
ic->nodes = raw;
if (!jeb->first_node)
jeb->first_node = raw;
if (jeb->last_node)
jeb->last_node->next_phys = raw;
jeb->last_node = raw;
需要说明的是,由于已经对目录项数据实体进行了crc校验,所以设置其内核描述符的REF_PRINSTINE标志。与此相比,在jffs2_scan_inode_node函数中没有对jffs2_raw_inode数据实体立即进行crc校验(而是推迟到打开文件时),所以才设置了REF_UNCHECKED标志(参见jffs2_scan_inode_node函数的相关部分)。
在jffs2中目录项所属的目录文件由其pino表示,所以在调用jffs2_scan_make_ino_cache函数返回其所属目录文件的内核描述符时传递pino而非ino!(类比jffs2_scan_inode_node函数中传递的是ino,参见上文)
对于目录项数据实体,还得进一步建立jffs2_full_dirent和jffs2_raw_node_ref、jffs2_inode_cache之间的连接关系:
fd->raw = raw;
fd->next = NULL;
fd->version = je32_to_cpu(rd->version);
fd->ino = je32_to_cpu(rd->ino);
fd->nhash = full_name_hash(fd->name, rd->nsize);
fd->type = rd->type;
USED_SPACE(PAD(je32_to_cpu(rd->totlen)));
jffs2_add_fd_to_list(c, fd, &ic->scan_dents);
return 0;
}
jffs2_full_dirent的nhash域为根据文件名计算的一个数值,然后在通过jffs2_add_fd_to_list函数将其插入由jffs2_inode_cache.scan_dents指向的链表时,按照nhash由小到大的顺序插入。详见下文。最后在退出前还得用USED_SPACE宏增加jffs2_sb_info和当前擦除块的jffs2_eraseblock的used_size值、减小free_size值:
#define USED_SPACE(x) do { typeof(x) _x = (x); /
c->free_size -= _x; c->used_size += _x; /
jeb->free_size -= _x ; jeb->used_size += _x; /
}while(0)
jffs2_scan_dirent_node函数完成后与目录文件相关的数据结构关系如下:
注意上图中nodes链表中含有目录文件惟一的jffs2_raw_inode的内核描述符,其相应的上层jffs2_full_dnode要等到打开目录文件时才会创建,并由目录文件inode的u域,即jffs2_inode_info的metadata指向。(另外,nodes链表中各描述符的先后顺序完全由flash上各数据实体的先后顺序决定,每扫描一个就加入链表首部。)
挂载文件系统时从jffs2_build_filesystem函数到达这个函数的调用路径为:
jffs2_build_filesystem > jffs2_scan_medium > jffs2_scan_eraseblock > jffs2_scan_dirent_node
从这个函数一直返回到jffs2_build_filesystem函数后执行流会调用jffs2_build_inode_pass1函数计算各文件的硬链接计数,随后在jffs2_build_filesystem函数的第3阶段要释放整个scan_dents链表(参见jffs2_build_filesystem函数的末尾)。与打开目录文件类比,在jffs2_do_read_inode函数中会再次为所有的目录项创建相应的jffs2_full_dirent数据结构的链表,由目录文件的jffs2_inode_info的dents域指向,参见图1。而挂载文件系统时只存在文件的内核描述符jffs2_inode_cache,所以临时链表就由其scan_dents指向。
full_name_hash函数
include/linux/dcache.h文件中定义的与full_name_hash函数有关的内联函数如下:
/* Name hashing routines. Initial hash value */
/* Hash courtesy of the R5 hash in reiserfs modulo sign bits */
#define init_name_hash() 0
/* partial hash update function. Assume roughly 4 bits per character */
static __inline__ unsigned long
partial_name_hash(unsigned long c, unsigned long prevhash)
{
return (prevhash + (c << 4) + (c >> 4)) * 11;
}
/* Finally: cut down the number of bits to a int value (and try to avoid losing bits) */
static __inline__ unsigned long
end_name_hash(unsigned long hash)
{
return (unsigned int) hash;
}
/* Compute the hash for a name string. */
static __inline__ unsigned int
full_name_hash(const unsigned char * name, unsigned int len)
{
unsigned long hash = init_name_hash();
while (len--)
hash = partial_name_hash(*name++, hash);
return end_name_hash(hash);
}
由此可见,根据文件名的每个字符计算出一个hash值,然后这个值被累积到后继字符的hash中。直到扫描到文件名的最后一个字符才返回最终的hash值。
jffs2_add_fd_to_list函数
这个函数将new指向的jffs2_full_dirent元素加入(*list)指向的链表。
void jffs2_add_fd_to_list(struct jffs2_sb_info *c, struct jffs2_full_dirent *new, struct jffs2_full_dirent **list)
{
struct jffs2_full_dirent **prev = list;
D1(printk(KERN_DEBUG "jffs2_add_fd_to_list( %p, %p (->%p))/n", new, list, *list));
while ((*prev) && (*prev)->nhash <= new->nhash) {
if ((*prev)->nhash == new->nhash && !strcmp((*prev)->name, new->name)) {
/* Duplicate. Free one */
if (new->version < (*prev)->version) {
D1(printk(KERN_DEBUG "Eep! Marking new dirent node obsolete/n"));
D1(printk(KERN_DEBUG "New dirent is /"%s/"->ino #%u. Old is /"%s/"->ino #%u/n",
new->name, new->ino, (*prev)->name, (*prev)->ino));
jffs2_mark_node_obsolete(c, new->raw);
jffs2_free_full_dirent(new);
} else {
D1(printk(KERN_DEBUG "Marking old dirent node (ino #%u) obsolete/n", (*prev)->ino));
new->next = (*prev)->next;
jffs2_mark_node_obsolete(c, ((*prev)->raw));
jffs2_free_full_dirent(*prev);
*prev = new;
}
goto out;
}
prev = &((*prev)->next);
}
new->next = *prev;
*prev = new;
如果新元素的nhash值小于链表首元素的nhash值,则步进prev指针,否则将新元素插入prev指向的位置处。如果新元素的nhash值等于(*prev)所指元素的nhash值,则进一步比较二者的文件名是否相同。如果也相同,则说明出现了关于同一文件的目录项的重复jffs2_full_dirent数据结构,则需要删除版本号较小的那一个:首先通过jffs2_mark_node_obsolete函数标记目录项的内存描述符为过时:设置其flash_offset的REF_OBSOLETE标志(这个函数还进行了许多其它相关操作:刷新所在擦除块描述符和jffs2_sb_info中的xxxx_size域,甚至要改变当前擦除块所在的jffs2_sb_info.xxxx_list链表。尚未详细研究),然后用jffs2_free_full_dirent函数释放这个jffs2_full_dirent数据结构。
out:
D2(while(*list) {
printk(KERN_DEBUG "Dirent /"%s/" (hash 0x%08x, ino #%u/n",
(*list)->name, (*list)->nhash, (*list)->ino);list = &(*list)->next;}
);
}
jffs2_build_inode_pass1函数
在挂载jffs2时为所有文件都调用这个函数,第二个参数ic指向文件的内核描述符。如果它是一个目录文件则增加其下所有目录项所对应文件的硬链接计数nlink。
int jffs2_build_inode_pass1(struct jffs2_sb_info *c, struct jffs2_inode_cache *ic)
{
struct jffs2_full_dirent *fd;
D1(printk(KERN_DEBUG "jffs2_build_inode building inode #%u/n", ic->ino));
if (ic->ino > c->highest_ino)
c->highest_ino = ic->ino;
在jffs2_build_filesystem函数中先由jffs2_scan_medium函数为目录文件的目录项创建了jffs2_full_dirent数据结构,它们的链表由jffs2_inode_cache的scan_dents域指向。在jffs2_build_filesystem函数的最后又释放掉了所有目录文件的jffs2_full_dirent数据结构的链表。临时建立这个链表就是为了jffs2_build_inode_pass1函数使用。
如果是目录文件,则其jffs2_inode_cache的scan_dents指针非空,则遍历相应链表检索目录下的所有子目录。注意,对于非目录文件,scan_dents指针为空,所以就不进入for循环而直接退出了。
/* For each child, increase nlink */
for(fd=ic->scan_dents; fd; fd = fd->next) {
struct jffs2_inode_cache *child_ic;
if (!fd->ino)
continue;
/* XXX: Can get high latency here with huge directories */
child_ic = jffs2_get_ino_cache(c, fd->ino);
if (!child_ic) {
printk(KERN_NOTICE "Eep. Child /"%s/" (ino #%u) of dir ino #%u doesn't exist!/n",
fd->name, fd->ino, ic->ino);
continue;
}
注意目录项中含有两个索引结点号:ino为其代表的文件的索引结点号,pino为其所属的目录文件的索引结点号。所以为了返回该目录项所代表的文件内核描述符,象jffs2_get_ino_cache函数传递ino。如果目录项代表的文件的jffs2_inode_cache尚未建立,则打印警告信息,并开始新的循环访问下一目录项。
if (child_ic->nlink++ && fd->type == DT_DIR) {
printk(KERN_NOTICE "Child dir /"%s/" (ino #%u) of dir ino #%u appears to be a hard link/n",
fd->name, fd->ino, ic->ino);
if (fd->ino == 1 && ic->ino == 1) {
printk(KERN_NOTICE "This is mostly harmless, and probably caused by creating
a JFFS2 image/n");
printk(KERN_NOTICE "using a buggy version of mkfs.jffs2. Use at least v1.17./n");
}
/* What do we do about it? */
}
D1(printk(KERN_DEBUG "Increased nlink for child /"%s/" (ino #%u)/n", fd->name, fd->ino));
/* Can't free them. We might need them in pass 2 */
}//for
return 0;
}
先前在jffs2_scan_medium函数中为所有的文件创建jffs2_inode_cache时,nlink域被设置为0,它代表指向文件索引结点的目录项的个数。jffs2_build_inode_pass1函数的核心操作就是“child_ic->nlink++”,即增加目录项所代表文件的硬链接个数。由于子目录、子文件的目录项属于父目录文件,所以为父目录下存在的每个目录项所代表的文件增加硬链接计数。比如,如果文件(无论是否是目录)在A目录下,在B目录中存在一个硬链接(即B目录的目录文件中含有代表该文件的目录项,即在flash上存在两个代表该文件的jffs2_raw_dirent数据实体(一个属于A目录文件,另一个属于B目录文件)),那么在遍历A目录时其nlink由0增加为1,在遍历B目录时就会将nlink进一步增加为2。
另外,如果目录项对应一个子目录,则进一步检查该子目录是否为根目录。根据作者的注释这种情况会在使用版本低于1.17的mkfs.jffs2时出现。
特别需要说明的是,上层VFS所使用的“文件硬链接计数”是其inode的nlink,而不是jffs2_inode_cache中的nlink!所以在打开文件时首先用jffs2_inode_cache中的nlink设置inode的nlink,然后要进一步增加非叶目录的inode的nlink。参见下文。
第5章 打开文件时建立inode的方法
挂载jffs2文件系统时,一旦为flash上所有文件和数据实体创建了相应的内核描述符后,就已经完成了挂载的大部分工作。剩下的就得为根目录“/”创建inode和dentry了。创建inode的工作由iget内联函数完成。在jffs2_do_fill_super函数中为根目录创建inode的代码摘录如下:
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;
}
注意传递的第二个参数为相应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。
iget和iget4函数
iget函数返回或者创建与索引结点号ino相应的inode:
static inline struct inode *iget(struct super_block *sb, unsigned long ino)
{
return iget4(sb, ino, NULL, NULL);
}
struct inode *iget4(struct super_block *sb, unsigned long ino, find_inode_t find_actor, void *opaque)
{
struct list_head * head = inode_hashtable + hash(sb,ino);
struct inode * inode;
文件索引节点号在文件系统所在的设备上是惟一的,所以在计算索引节点号的散列值时要传递文件系统超级块的地址。hash函数定义于fs/inode.c:
static inline unsigned long hash(struct super_block *sb, unsigned long i_ino)
{
unsigned long tmp = i_ino + ((unsigned long) sb / L1_CACHE_BYTES);
tmp = tmp + (tmp >> I_HASHBITS);
return tmp & I_HASHMASK;
}
由此可见它仅把文件系统超级块的地址当作无符号长整型数据来使用。由于每个文件系统超级块的地址不尽相同,所以可用保证不同文件系统内相同的索引结点号在整个操作系统中是不同的。
计算出ino对应的散列值后,就可用得到冲突项组成的链表了,链表由head指向。下面就用find_inode函数返回该链表中ino相应的inode结构的地址:
spin_lock(&inode_lock);
inode = find_inode(sb, ino, head, find_actor, opaque);
if (inode) {
__iget(inode);
spin_unlock(&inode_lock);
wait_on_inode(inode);
return inode;
}
spin_unlock(&inode_lock);
如果这个inode已经存在,则返回其地址,并通过__iget增加其引用计数(当然iget还有其它操作,这里没有分析,可参见情景分析),并且通过wait_on_inode函数确保inode没有被加锁。如果已经被加锁(inode.i_state的I_LOCK位被设置),则阻塞等待被解锁为止。这个函数很简单,仅罗列其代码如下:
static void __wait_on_inode(struct inode * inode)
{
DECLARE_WAITQUEUE(wait, current);
add_wait_queue(&inode->i_wait, &wait);
repeat:
set_current_state(TASK_UNINTERRUPTIBLE);
if (inode->i_state & I_LOCK) {
schedule();
goto repeat;
}
remove_wait_queue(&inode->i_wait, &wait);
current->state = TASK_RUNNING;
}
static inline void wait_on_inode(struct inode *inode)
{
if (inode->i_state & I_LOCK)
__wait_on_inode(inode);
}
回到iget4函数,如果相应的inode不存在,则通过get_new_inode函数分配一个新的inode:
/* get_new_inode() will do the right thing, re-trying the search in case it had to block at any point. */
return get_new_inode(sb, ino, head, find_actor, opaque);
}
get_new_inode函数
/*
* This is called without the inode lock held.. Be careful.
*
* We no longer cache the sb_flags in i_flags - see fs.h
* -- rmk@arm.uk.linux.org
*/
static struct inode * get_new_inode(struct super_block *sb, unsigned long ino, struct list_head *head,
find_inode_t find_actor, void *opaque)
{
struct inode * inode;
inode = alloc_inode();
if (inode) {
struct inode * old;
spin_lock(&inode_lock);
/* We released the lock, so.. */
old = find_inode(sb, ino, head, find_actor, opaque);
alloc_inode宏定义为:
#define alloc_inode() (struct inode *) kmem_cache_alloc(inode_cachep, SLAB_KERNEL))
首先从inode_cachep高速缓存中分配一个inode结构,然后在设置该inode之前,再次调用find_inode函数确保此前所需的inode仍然没有被创建。
if (!old) {
inodes_stat.nr_inodes++;
list_add(&inode->i_list, &inode_in_use);
list_add(&inode->i_hash, head);
如果真的需要新的inode,则将其加入内核链表inode_in_use,并加入索引结点哈希表inode_hashtable中head所指的冲突项链表,同时增加内核统计inode_stat_nr_inodes++。
inode->i_sb = sb;
inode->i_dev = sb->s_dev;
inode->i_blkbits = sb->s_blocksize_bits;
inode->i_ino = ino;
inode->i_flags = 0;
atomic_set(&inode->i_count, 1);
inode->i_state = I_LOCK;
spin_unlock(&inode_lock);
然后,根据传递的参数sb、s_dev、ino来设置inode中的相应域。由于下面要用文件系统的read_inode方法来填充这个inode,所以设置了I_LOCK标志以保证这个过程的原子性。
clean_inode(inode);
/* reiserfs specific hack right here. We don't
** want this to last, and are looking for VFS changes
** that will allow us to get rid of it.
** -- mason@suse.com
*/
if (sb->s_op->read_inode2) {
sb->s_op->read_inode2(inode, opaque) ;
} else {
sb->s_op->read_inode(inode);
}
clean_inode函数继续初始化inode其它的域:
/*
* This just initializes the inode fields
* to known values before returning the inode..
*
* i_sb, i_ino, i_count, i_state and the lists have
* been initialized elsewhere..
*/
static void clean_inode(struct inode *inode)
{
static struct address_space_operations empty_aops;
static struct inode_operations empty_iops;
static struct file_operations empty_fops;
memset(&inode->u, 0, sizeof(inode->u));
inode->i_sock = 0;
inode->i_op = &empty_iops;
inode->i_fop = &empty_fops;
inode->i_nlink = 1;
atomic_set(&inode->i_writecount, 0);
inode->i_size = 0;
inode->i_blocks = 0;
inode->i_generation = 0;
memset(&inode->i_dquot, 0, sizeof(inode->i_dquot));
inode->i_pipe = NULL;
inode->i_bdev = NULL;
inode->i_cdev = NULL;
inode->i_data.a_ops = &empty_aops;
inode->i_data.host = inode;
inode->i_data.gfp_mask = GFP_HIGHUSER;
inode->i_mapping = &inode->i_data;
}
其中指向相关方法的指针都被指向空的数据结构,然后在文件系统的read_inode方法中设置为合适的值。详见下文分析。
/*
* This is special! We do not need the spinlock
* when clearing I_LOCK, because we're guaranteed
* that nobody else tries to do anything about the
* state of the inode when it is locked, as we
* just created it (so there can be no old holders
* that haven't tested I_LOCK).
*/
inode->i_state &= ~I_LOCK;
wake_up(&inode->i_wait);
return inode;
}
设置完inode后,根据作者的注释,任何执行流在访问inode.i_state时必须首先设置I_LOCK标志。一旦这个标志已经被设置,那么执行流就得阻塞到i_wait等待队列上。而当前执行流在设置inode期间时持有I_LOCK锁的,所以可以保证此时没有其它的执行流,即不存在竞争条件。所以这里清除I_LOCK标志时无需用自旋锁保护。同时还要唤醒任何因等待I_LOCK被清除而阻塞在i_wait等待队列上的执行流。
/*
* Uhhuh, somebody else created the same inode under us. Use the old inode instead of the one we just
* allocated.
*/
__iget(old);
spin_unlock(&inode_lock);
destroy_inode(inode);
inode = old;
wait_on_inode(inode);
}
return inode;
}
另外,如果在get_new_inode函数执行时已经由其它的执行流创建了所需的inode,则释放先前获得的inode结构。增加引用计数、等待其“可用”(I_LOCK标志被清除)后直接返回其地址即可。
jffs2_read_inode函数
在jffs2文件系统源代码文件中定义了类型为file_system_type的数据结构jffs2_fs_type,其read_super方法为jffs2_read_super:
static DECLARE_FSTYPE_DEV(jffs2_fs_type, "jffs2", jffs2_read_super);
然后在jffs2文件系统的初始化函数init_jffs2_fs中用register_filesystem函数向系统注册了jffs2文件系统,即把jffs2_fs_type加入file_systmes指向的file_system_type数据结构的链表。
在挂载jffs2文件系统时,先前注册的read_super方法,即jffs2_read_super函数被调用,在函数的开始就将文件系统超级块的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
};
其中read_inode指针正指向jffs2_read_inode函数,所以在get_new_inode函数中调用文件系统的read_inode方法初始化inode数据结构时这个函数才被调用。
void jffs2_read_inode (struct inode *inode)
{
struct jffs2_inode_info *f;
struct jffs2_sb_info *c;
struct jffs2_raw_inode latest_node;
int ret;
D1(printk(KERN_DEBUG "jffs2_read_inode(): inode->i_ino == %lu/n", inode->i_ino));
f = JFFS2_INODE_INFO(inode);
c = JFFS2_SB_INFO(inode->i_sb);
jffs2_init_inode_info(f);
对于jffs2文件系统,inode的u域为jffs2_inode_info数据结构,super_block的u域为jffs2_sb_info数据结构,先前get_new_inode函数中已经设置inode.i_sb指向super_block了。用宏返回两个u域的地址,并且初始化inode的u域。
除根目录文件外,任何文件都在flash上至少有一个jffs2_raw_inode数据实体,而每个数据实体中都含有关于该文件的公共信息,比如i_mode、i_gid、i_uid、i_size、i_atime、i_mtime、i_ctime、i_nlink等等,所以这些域只需从flash中读出一个数据实体即可得到(而硬链接计数nlink在jffs2_build_filesystem函数中计算)。
如果打开目录文件,则为每个目录项创建jffs2_full_dirent并组织为链表,由jffs2_inode_info的dents域指向,并为其惟一的jffs2_raw_inode创建相应的jffs2_full_dnode,并由jffs2_inode_info的metedata指向;如果打开正规文件,为每个数据节点创建相应的jffs2_full_dnode/jffs2_node_frag,并组织为红黑树,树根为jffs2_inode_info.fragtree。参见图1,图2。上述这两个工作都是通过jffs2_do_read_inode函数完成的,读出的数据实体存放在latest_node中。详见后文分析。
ret = jffs2_do_read_inode(c, f, inode->i_ino, &latest_node);
if (ret) {
make_bad_inode(inode);
up(&f->sem);
return;
}
如果jffs2_do_read_inode函数失败,则通过make_bad_inode函数将该inode标记为“bad”:
/**
* make_bad_inode - mark an inode bad due to an I/O error
* @inode: Inode to mark bad
*
* When an inode cannot be read due to a media or remote network
* failure this function makes the inode "bad" and causes I/O operations
* on it to fail from this point on.
*/
void make_bad_inode(struct inode * inode)
{
inode->i_mode = S_IFREG;
inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;
inode->i_op = &bad_inode_ops;
inode->i_fop = &bad_file_ops;
}
其中主要操作是将索引结点方法指针i_op指向bad_inode_ops方法表,而在is_bad_inode函数中将通过检查i_op是否指向bad_inode_ops方法表来判断inode是否为bad。比如在GC操作中如果发现文件的inode为bad,则返回EIO。如果一切顺利,就可以根据读到last_node中的数据实体的信息来设置inode的相应域了。注意nlink域为硬链接个数,在挂载文件系统后已经初步计算过。
inode->i_mode = je32_to_cpu(latest_node.mode);
inode->i_uid = je16_to_cpu(latest_node.uid);
inode->i_gid = je16_to_cpu(latest_node.gid);
inode->i_size = je32_to_cpu(latest_node.isize);
inode->i_atime = je32_to_cpu(latest_node.atime);
inode->i_mtime = je32_to_cpu(latest_node.mtime);
inode->i_ctime = je32_to_cpu(latest_node.ctime);
inode->i_nlink = f->inocache->nlink;
inode->i_blksize = PAGE_SIZE;
inode->i_blocks = (inode->i_size + 511) >> 9;
下面就得根据文件的性质,设置inode中的方法i_op和i_fop:
switch (inode->i_mode & S_IFMT) {
unsigned short rdev;
case S_IFLNK:
inode->i_op = &jffs2_symlink_inode_operations;
break;
由此可见在jffs2中符号链接文件的方法为jffs2_symlink_inode_operations,参见下文。
case S_IFDIR:
{
struct jffs2_full_dirent *fd;
for (fd=f->dents; fd; fd = fd->next) {
if (fd->type == DT_DIR && fd->ino) /* 为目录文件的目录项 */
inode->i_nlink++;
}
/* and '..' */
inode->i_nlink++;
在挂载文件系统中、在jffs2_build_filesystem函数的最后初步为每个文件计算了nlink,此时硬链接计数保存在文件的内核描述符jffs2_inode_cache中,参见上文。除根目录外任何文件都至少有一个jffs2_raw_dirent目录项,所以jffs2_inode_cache中的nlink至少为1,而根目录文件的jffs2_inode_cache.nlink也为1(参见jffs2_scan_make_ino_cache函数)。
需要说明的是上层VFS所使用的硬链接计数是其inode的nlink,而不是文件内核描述符jffs2_inode_cache的nlink!(对于这一点我目前理解得还不够深入)所以这里还需要进一步增加非叶目录的inode的nlink:如果目录项对应一个目录,则增加父目录inode的nlink(但是从jffs2map2的结果来看在任何目录下都没有“.”或者“..”目录项!值得进一步讨论,参见附录),所以首先需要判断该目录项是否对应一个目录,jffs2_full_dirent中的type域从jffs2_raw_dirent的type域复制过来。
/* Root dir gets i_nlink 3 for some reason */
if (inode->i_ino == 1)
inode->i_nlink++;
inode->i_op = &jffs2_dir_inode_operations;
inode->i_fop = &jffs2_dir_operations;
break;
}
由此可见在jffs2中目录文件的方法为jffs2_dir_inode_operations,参见下文。
case S_IFREG:
inode->i_op = &jffs2_file_inode_operations;
inode->i_fop = &jffs2_file_operations;
inode->i_mapping->a_ops = &jffs2_file_address_operations;
inode->i_mapping->nrpages = 0;
break;
对于正规文件,文件方法表指针i_fop被设置为指向jffs2_file_operations方法表,而内存映射方法表指针i_mapping->a_ops被设置为指向jffs2_file_address_operations方法表。从后文就可见读写文件时必须经过这两个方法表中的相关函数。
case S_IFBLK:
case S_IFCHR:
/* Read the device numbers from the media */
D1(printk(KERN_DEBUG "Reading device numbers from flash/n"));
if (jffs2_read_dnode(c, f->metadata, (char *)&rdev, 0, sizeof(rdev)) < 0) { /* Eep */
printk(KERN_NOTICE "Read device numbers for inode %lu failed/n",
(unsigned long)inode->i_ino);
up(&f->sem);
jffs2_do_clear_inode(c, f);
make_bad_inode(inode);
return;
}
/* FALL THROUGH */
设备文件由惟一的jffs2_raw_inode数据实体表示,紧随其后的的数据为设备号rdev(类比在ext2上设备文件由一个磁盘索引结点表示,其i_data[]中第一个元素保存设备号)。另外目录文件、符号链接、设备文件的jffs2_full_dnode由metadata直接指向而没有加入fragtree红黑树中,详见jffs2_do_read_inode函数。通过jffs2_read_dnode函数读出设备号到rdev变量中,详见后文。需要注意的是如果读取设备号成功,则这个case后面没有break,所以会进入下面的代码。
case S_IFSOCK:
case S_IFIFO:
inode->i_op = &jffs2_file_inode_operations;
init_special_inode(inode, inode->i_mode, kdev_t_to_nr(mk_kdev(rdev>>8, rdev&0xff)));
break;
default:
printk(KERN_WARNING "jffs2_read_inode(): Bogus imode %o for ino %lu/n",
inode->i_mode, (unsigned long)inode->i_ino);
}
up(&f->sem);
D1(printk(KERN_DEBUG "jffs2_read_inode() returning/n"));
}
由此可见,对于特殊文件(设备文件、SOCKET、FIFO文件),它们的inode通过init_special_inode函数来进一步初始化:
void init_special_inode(struct inode *inode, umode_t mode, int rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = to_kdev_t(rdev);
inode->i_cdev = cdget(rdev);
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = to_kdev_t(rdev);
} else if (S_ISFIFO(mode))
inode->i_fop = &def_fifo_fops;
else if (S_ISSOCK(mode))
inode->i_fop = &bad_sock_fops;
else
printk(KERN_DEBUG "init_special_inode: bogus imode (%o)/n", mode);
}
其实它们的inode的mode已经在前面jffs2_do_read_inode函数读取数据实体后设置好了,这里主要是设置文件方法和设备文件的inode.i_rdev域。其中参数rdev经过to_kdev_t内联函数加以格式转换后就得到i_rdev(该函数及相关的宏定义在linux/kdev_t.h)。
需要说明的是,在jffs2文件系统中设备文件用flash上一个jffs2_raw_inode数据实体表示。而内核中的VFS的索引结点inode中设计了i_dev和i_rdev两个域,分别表示设备盘索引结点所在的设备的设备号,以及它所代表的设备的设备号。而jffs2_raw_inode中也没有任何表示设备号的域。这是因为能从flash上访问该数据实体就当然知道其所在flash分区的设备号;另外紧随该数据实体后的为其所代表的设备的设备号。
jffs2_do_read_inode函数(improved)
如前文所述,这个函数需要从flash中读出一个数据实体以得到文件索引节点的公共信息,同时,如果是目录文件,则为惟一的jffs2_raw_inode创建jffs2_full_dnode,由jffs2_inode_info的metadata指向;为每个目录项创建jffs2_full_dirent并组织为链表,由jffs2_inode_info的dents域指向;如果是普通文件,为每个jffs2_raw_inode数据节点创建相应的jffs2_full_dnode/jffs2_node_frag,并组织为红黑树,树根为jffs2_inode_info的fragtree。
/* Scan the list of all nodes present for this ino, build map of versions, etc. */
int jffs2_do_read_inode(struct jffs2_sb_info *c, struct jffs2_inode_info *f,
uint32_t ino, struct jffs2_raw_inode *latest_node)
{
struct jffs2_tmp_dnode_info *tn_list, *tn;
struct jffs2_full_dirent *fd_list;
struct jffs2_full_dnode *fn = NULL;
uint32_t crc;
uint32_t latest_mctime, mctime_ver;
uint32_t mdata_ver = 0;
size_t retlen;
int ret;
D2(printk(KERN_DEBUG "jffs2_do_read_inode(): getting inocache/n"));
f->inocache = jffs2_get_ino_cache(c, ino);
D2(printk(KERN_DEBUG "jffs2_do_read_inode(): Got inocache at %p/n", f->inocache));
由前文可知,在挂载文件系统时(在jffs2_scan_medium函数中)已经为flash上所有的文件创建了内核描述符jffs2_inode_cache并加入了文件系统inocache_list哈希表,这里直接根据索引节点号返回其地址即可。
if (!f->inocache && ino == 1) {
/* Special case - no root inode on medium */
f->inocache = jffs2_alloc_inode_cache();
if (!f->inocache) {
printk(KERN_CRIT "jffs2_do_read_inode(): Cannot allocate inocache for root inode/n");
return -ENOMEM;
}
D1(printk(KERN_DEBUG "jffs2_do_read_inode(): Creating inocache for root inode/n"));
memset(f->inocache, 0, sizeof(struct jffs2_inode_cache));
f->inocache->ino = f->inocache->nlink = 1;
f->inocache->nodes = (struct jffs2_raw_node_ref *)f->inocache;
jffs2_add_ino_cache(c, f->inocache);
}
if (!f->inocache) {
printk(KERN_WARNING "jffs2_do_read_inode() on nonexistent ino %u/n", ino);
return -ENOENT;
}
D1(printk(KERN_DEBUG "jffs2_do_read_inode(): ino #%u nlink is %d/n", ino, f->inocache->nlink));
如果文件描述符尚未创建则返回错误ENOENT。而根目录文件是惟一没有jffs2_raw_inode数据实体的目录,这段代码表明作者认为先前在挂载文件系统时并没有创建根目录文件的内核描述符,所以如果文件描述符不存在且ino又等于1,即为根目录,则在这里创建之并加入哈希表。但是我认为即使根目录不存在惟一的那个jffs2_raw_inode,但是在挂载时也已经为其创建了jffs2_inode_cache。这是因为在jffs2_scan_dirent_node函数中也会调用jffs2_scan_make_ino_cache函数创建目录项所在文件的内核描述符,此时传递的是目录项中的pino参数。因此既然根目录不为空,那么挂载文件系统时就会为其创建内核描述符。参见上文。(进一步,可以尝试注销这段代码以验证我的判断是否正确。)
先前在挂载文件系统时已经为flash上所有的数据实体创建了内核描述符jffs2_raw_node_ref,其中的flash_offset为数据实体在flash分区内的逻辑偏移,totlen为其长度。而且同一个文件的jffs2_raw_node_ref通过next_in_ino域组织成一个链表,链表由文件的jffs2_inode_cache的nodes域指向。jffs2_get_inode_nodes函数就可用利用这个链表访问文件的所有数据实体,然后:
1. 为每一个jffs2_raw_dirent创建jffs2_full_dirent,并组织为链表fd_list
2. 为每一个jffs2_raw_inode创建jffs2_tmp_dnode_info和jffs2_full_dnode,并组织为链表tn_list
/* Grab all nodes relevant to this ino */
ret = jffs2_get_inode_nodes(c, ino, f, &tn_list, &fd_list, &f->highest_version,
&latest_mctime, &mctime_ver);
if (ret) {
printk(KERN_CRIT "jffs2_get_inode_nodes() for ino %u returned %d/n", ino, ret);
return ret;
}
f->dents = fd_list;
对于目录文件,将其jffs2_full_dirent组成的链表由jffs2_inode_info的dents域指向,参见图1。对于其它文件则遍历jffs2_tmp_dnode_info组成的链表,为每一个jffs2_full_dnode创建相应的jffs2_node_frag结构并加入inode.u.fragtree所指向的红黑树。最后释放整个jffs2_tmp_dnode_info链表。
while (tn_list) {
tn = tn_list;
fn = tn->fn;
if (f->metadata && tn->version > mdata_ver) {
D1(printk(KERN_DEBUG "Obsoleting old metadata at 0x%08x/n",
ref_offset(f->metadata->raw)));
jffs2_mark_node_obsolete(c, f->metadata->raw);
jffs2_free_full_dnode(f->metadata);
f->metadata = NULL;
mdata_ver = 0;
}
if (fn->size) {
jffs2_add_full_dnode_to_inode(c, f, fn);
} else {
/* Zero-sized node at end of version list. Just a metadata update */
D1(printk(KERN_DEBUG "metadata @%08x: ver %d/n", ref_offset(fn->raw), tn->version));
f->metadata = fn;
mdata_ver = tn->version;
}
tn_list = tn->next;
jffs2_free_tmp_dnode_info(tn);
}//while
在这个循环中遍历jffs2_tmp_dnode_info的链表,为每个元素所指向的jffs2_full_dnode数据结构创建相应的jffs2_node_frag数据结构,并插入以jffs2_inode_info.fragtree为根红黑树。这个工作是通过函数jffs2_add_full_dnode_to_inode函数完成的(这个函数涉及红黑树的插入,尚未研究)。
对于正规文件、符号链接、设备文件,它们至少由一个后继带有数据的jffs2_raw_inode组成,所以这里都组织了红黑树,目录文件、SOCKET、FIFO文件的jffs2_raw_inode后没有数据(所以fn->size等于0),所以它们的jffs2_full_dnode直接由jffs2_inode_info的metadata指向。
处理完一个jffs2_full_dnode,随即释放相应的jffs2_tmp_dnode_info。由此可见该数据结构是在打开文件期间为处理jffs2_full_dnode数据结构而临时创建的。
if (!fn) {
/* No data nodes for this inode. */
if (ino != 1) {
printk(KERN_WARNING "jffs2_do_read_inode(): No data nodes found for ino #%u/n", ino);
if (!fd_list) {
return -EIO;
}
printk(KERN_WARNING "jffs2_do_read_inode(): But it has children so we fake some modes
for it/n");
}
latest_node->mode = cpu_to_je32(S_IFDIR|S_IRUGO|S_IWUSR|S_IXUGO);
latest_node->version = cpu_to_je32(0);
latest_node->atime = latest_node->ctime = latest_node->mtime = cpu_to_je32(0);
latest_node->isize = cpu_to_je32(0);
latest_node->gid = cpu_to_je16(0);
latest_node->uid = cpu_to_je16(0);
return 0;
}
jffs2中只有根目录一个文件没有jffs2_raw_inode数据实体(哪怕对于新创建、且尚未写入任何数据的正规文件,也在创建当下写入了一个临时的jffs2_raw_inode,相应的jffs2_full_dnode则由metadata指向,并在稍后真正第一次写入数据时被标记为过时),所以此时可以直接初始化lastest_node参数所指向的jffs2_raw_inode数据实体了。否则,就必须通过jffs2_flash_read函数从flash上真正读出一个来:
ret = jffs2_flash_read(c, ref_offset(fn->raw), sizeof(*latest_node), &retlen, (void *)latest_node);
if (ret || retlen != sizeof(*latest_node)) {
printk(KERN_NOTICE "MTD read in jffs2_do_read_inode() failed: Returned
%d, %ld of %d bytes read/n", ret, (long)retlen, sizeof(*latest_node));
/* FIXME: If this fails, there seems to be a memory leak. Find it. */
up(&f->sem);
jffs2_do_clear_inode(c, f);
return ret?ret:-EIO;
}
在打开文件时,在jffs2_do_read_inode函数中除了为数据实体创建相应的数据结构外,还要读取一个数据实体返回给上层的jffs2_read_inode用于设置文件的inode数据结构。
(在第1稿中这一段没有看明白,现在懂了)对于目录文件有惟一的jffs2_raw_inode数据实体(对于根目录则前面已经返回),所以上面jffs2_get_inode_nodes返回参数tn_list至少含有一个元素,由fn指向。所以这里正是读出目录文件那个惟一的jffs2_raw_inode数据实体到latest_node所指空间中!
crc = crc32(0, latest_node, sizeof(*latest_node)-8);
if (crc != je32_to_cpu(latest_node->node_crc)) {
printk(KERN_NOTICE "CRC failed for read_inode of inode %u at physical location 0x%x/n",
ino, ref_offset(fn->raw));
up(&f->sem);
jffs2_do_clear_inode(c, f);
return -EIO;
}
读出了数据实体后,还要进行CRC校验。
最后,还需要修改读出的数据实体的某些域:
switch(je32_to_cpu(latest_node->mode) & S_IFMT) {
case S_IFDIR:
if (mctime_ver > je32_to_cpu(latest_node->version)) {
/* The times in the latest_node are actually older than mctime in the latest dirent. Cheat. */
latest_node->ctime = latest_node->mtime = cpu_to_je32(latest_mctime);
}
break;
前面在调用jffs2_get_inode_nodes函数时最后两个参数返回目录文件的所有目录项中“最近”的时间,据此修正目录文件惟一的jffs2_raw_inode中的时间戳。
case S_IFREG:
/* If it was a regular file, truncate it to the latest node's isize */
jffs2_truncate_fraglist(c, &f->fragtree, je32_to_cpu(latest_node->isize));
break;
(尚未研究这个函数)
case S_IFLNK:
/* Hack to work around broken isize in old symlink code.
Remove this when dwmw2 comes to his senses and stops
symlinks from being an entirely gratuitous special case. */
if (!je32_to_cpu(latest_node->isize))
latest_node->isize = latest_node->dsize;
/* fall through... */
case S_IFBLK:
case S_IFCHR:
/* Xertain inode types should have only one data node, and it's
kept as the metadata node */
if (f->metadata) {
printk(KERN_WARNING "Argh. Special inode #%u with mode 0%o had metadata node/n",
ino, je32_to_cpu(latest_node->mode));
up(&f->sem);
jffs2_do_clear_inode(c, f);
return -EIO;
}
if (!frag_first(&f->fragtree)) {
printk(KERN_WARNING "Argh. Special inode #%u with mode 0%o has no fragments/n",
ino, je32_to_cpu(latest_node->mode));
up(&f->sem);
jffs2_do_clear_inode(c, f);
return -EIO;
}
/* ASSERT: f->fraglist != NULL */
if (frag_next(frag_first(&f->fragtree))) {
printk(KERN_WARNING "Argh. Special inode #%u with mode 0%o had more than one node/n",
ino, je32_to_cpu(latest_node->mode));
/* FIXME: Deal with it - check crc32, check for duplicate node, check times and discard the older one */
up(&f->sem);
jffs2_do_clear_inode(c, f);
return -EIO;
}
/* OK. We're happy */
f->metadata = frag_first(&f->fragtree)->node;
jffs2_free_node_frag(frag_first(&f->fragtree));
f->fragtree = RB_ROOT;
break;
}
f->inocache->state = INO_STATE_PRESENT;
return 0;
}
对于符号链接,其唯一的jffs2_raw_inode后带有数据,先前其jffs2_full_dnode已经通过jffs2_node_frag加入了红黑树;对于设备文件,其flash尚设备索引节点的后继数据为设备号,先前也已经被加入红黑树。由于这些文件都只有一个数据实体,红黑树中只有一个节点,所以这里把它们都改为由metadata直接指向。
jffs2_get_inode_nodes函数
先前在挂载文件系统时已经为flash上所有的数据实体创建了内核描述符jffs2_raw_node_ref,其中的flash_offset为数据实体在flash分区内的逻辑偏移,totlen为其长度。而且同一个文件的jffs2_raw_node_ref通过next_in_ino域组织成一个链表,链表由文件的jffs2_inode_cache的nodes域指向。
在打开文件时这个函数就可用利用这个链表访问文件的所有数据实体,然后:
1. 为每一个jffs2_raw_dirent创建jffs2_full_dirent,并组织为链表fd_list,由fdp参数返回。
2. 为每一个jffs2_raw_inode创建jffs2_tmp_dnode_info和jffs2_full_dnode,并组织为链表tn_list,由tnp参数返回。
另外由于在挂载文件系统、为jffs2_raw_inode数据实体创建内核描述符时并没有对其后继数据进行crc校验(所以才在其内核描述符中设置了REF_UNCHECKED标志),那么现在就到了真正进行crc校验的时候了。
/* Get tmp_dnode_info and full_dirent for all non-obsolete nodes associated
with this ino, returning the former in order of version */
int jffs2_get_inode_nodes(struct jffs2_sb_info *c, ino_t ino, struct jffs2_inode_info *f,
struct jffs2_tmp_dnode_info **tnp, struct jffs2_full_dirent **fdp,
uint32_t *highest_version, uint32_t *latest_mctime, uint32_t *mctime_ver)
{
struct jffs2_raw_node_ref *ref = f->inocache->nodes;
struct jffs2_tmp_dnode_info *tn, *ret_tn = NULL;
struct jffs2_full_dirent *fd, *ret_fd = NULL;
union jffs2_node_union node;
size_t retlen;
int err;
*mctime_ver = 0;
D1(printk(KERN_DEBUG "jffs2_get_inode_nodes(): ino #%lu/n", ino));
if (!f->inocache->nodes) {
printk(KERN_WARNING "Eep. no nodes for ino #%lu/n", ino);
}
文件的内核描述符jffs2_inode_cache描述了文件及其数据之间的映射关系(这里采用较为“原始”的方法,即链表来直接描述映射关系,而打开文件后会在上层inode的u域中再次描述文件及其数据的映射关系,此时就可以采用较为“高级”和多样的描述手段了,比如红黑树fragtree、链表dents、或单一指针metadata)。首先检查jffs2_inode_cache的nodes指针不为空。
spin_lock_bh(&c->erase_completion_lock);
for (ref = f->inocache->nodes; ref && ref->next_in_ino; ref = ref->next_in_ino) {
/* Work out whether it's a data node or a dirent node */
if (ref_obsolete(ref)) {
/* FIXME: On NAND flash we may need to read these */
D1(printk(KERN_DEBUG "node at 0x%08x is obsoleted. Ignoring./n", ref_offset(ref)));
continue;
}
/* We can hold a pointer to a non-obsolete node without the spinlock,
but _obsolete_ nodes may disappear at any time, if the block they're in gets erased */
spin_unlock_bh(&c->erase_completion_lock);
遍历数据实体的内核描述符链表,在访问链表期间要持有jffs2_sb_info的erase_completion_lock自旋锁,并且禁止所有的下半部分(这说明在下半部分中可以同时访问该链表。是谁的下半部分?)。开始新的循环后即可释放该自旋锁;在for循环的最后、进入新的循环前重新获得该自旋锁。
从后文对写操作的分析可用看到如果对文件进行了任何修改则直接写入新的数据实体,而原有的“过时”的数据实体不做任何改动。在内核中为新的数据实体创建新的内核描述符jffs2_raw_node_ref,同时将原有数据实体的jffs2_raw_node_ref标记为“过时”(设置其flash_offset域的REF_OBSOLETE标志)。
所以在遍历文件的数据实体内核描述符链表时,如果被标记为过时,那么说明相应的flash数据实体已经失效,则直接跳过之即可。(所以如果打开目录文件,则不会为过时的目录项jffs2_raw_dirent创建jffs2_full_dirent;如果打开正规文件,则不会为过时的jffs2_raw_inode创建jffs2_tmp_dnode_info和jffs2_full_dnode!)
cond_resched();
/* FIXME: point() */
err = jffs2_flash_read(c, (ref_offset(ref)), min(ref->totlen, sizeof(node)), &retlen, (void *)&node);
if (err) {
printk(KERN_WARNING "error %d reading node at 0x%08x in get_inode_nodes()/n",
err, ref_offset(ref));
goto free_out;
}
/* Check we've managed to read at least the common node header */
if (retlen < min(ref->totlen, sizeof(node.u))) {
printk(KERN_WARNING "short read in get_inode_nodes()/n");
err = -EIO;
goto free_out;
}
jffs2_flash_read函数最终通过调用flash驱动的read_ecc或者read方法读出flash分区上指定偏移、长度的数据段。如果支持直接内存映射,那么在读NOR flash时可以通过内存映射完成(从而节省memcpy的内存拷贝开销)。用flash驱动的point函数建立内存映射,在读操作完成后再用unpoint拆除。
jffs2_flash_read函数的最后一个域node为一个共用体:
union jffs2_node_union {
struct jffs2_raw_inode i;
struct jffs2_raw_dirent d;
struct jffs2_unknown_node u;
};
由于两种数据实体都含有同样的头部,所以node的长度应该为其中最大的jffs2_raw_inode数据结构的长度(不包括后继数据)。
分两步读出有效的数据实体:首先读出不包含后继数据的jffs2_raw_dirent或者jffs2_raw_inode数据实体本身,而其中的totlen域为整个数据实体的长度,第二次再读出后继数据。同时,根据头部信息中的nodetype字段即可得到数据实体的类型并分配相应的数据结构:为jffs2_raw_dirent分配jffs2_full_dirent,为jffs2_raw_inode分配jffs2_tmp_dnode_info和jffs2_full_dnode。
switch (je16_to_cpu(node.u.nodetype)) {
case JFFS2_NODETYPE_DIRENT:
D1(printk(KERN_DEBUG "Node at %08x (%d) is a dirent node/n", ref_offset(ref),
ref_flags(ref)));
if (ref_flags(ref) == REF_UNCHECKED) {
printk(KERN_WARNING "BUG: Dirent node at 0x%08x never got checked? How?/n",
ref_offset(ref));
BUG();
}
if (retlen < sizeof(node.d)) {
printk(KERN_WARNING "short read in get_inode_nodes()/n");
err = -EIO;
goto free_out;
}
if (je32_to_cpu(node.d.version) > *highest_version)
*highest_version = je32_to_cpu(node.d.version);
if (ref_obsolete(ref)) {
/* Obsoleted. This cannot happen, surely? dwmw2 20020308 */
printk(KERN_ERR "Dirent node at 0x%08x became obsolete while we weren't looking/n",
ref_offset(ref));
BUG();
}
读出目录项数据实体后首先进行必要的有效性检查:其内核描述符不应该是REF_UNCHECKED的(在挂载文件系统时为目录项数据实体创建相应的内核描述符,此时设置其标志为REF_PRISTINE,参见jffs2_scan_dirent_node函数),否则为BUG;如果前面jffs2_flash_read函数实际读出的数据量retlen小于jffs2_raw_dirent数据结构的长度,则表明读出失败,所以返回EIO。另外,还要根据读出的目录项的version号来更新其所在目录文件的jffs2_inode_info.highest_version。
fd = jffs2_alloc_full_dirent(node.d.nsize+1);
if (!fd) {
err = -ENOMEM;
goto free_out;
}
memset(fd,0,sizeof(struct jffs2_full_dirent) + node.d.nsize+1);
fd->raw = ref;
fd->version = je32_to_cpu(node.d.version);
fd->ino = je32_to_cpu(node.d.ino);
fd->type = node.d.type;
然后,为目录项实体jffs2_raw_dirent分配相应的jffs2_full_dirent数据结构及后继文件名的空间并初始化。而jffs2_full_dirent数据结构中的域都是从flash上目录项数据实体的相应域复制过来的。
/* Pick out the mctime of the latest dirent */
if(fd->version > *mctime_ver) {
*mctime_ver = fd->version;
*latest_mctime = je32_to_cpu(node.d.mctime);
}
/* memcpy as much of the name as possible from the raw dirent we've already read from the flash
*/
if (retlen > sizeof(struct jffs2_raw_dirent))
memcpy(&fd->name[0], &node.d.name[0], min((uint32_t)node.d.nsize,
(retlen-sizeof(struct jffs2_raw_dirent))));
先前给jffs2_flash_read函数传递的待读出的数据长度为min(ref->totlen, sizeof(node)),而node的大小为jffs2_raw_inode的长度,大于jffs2_raw_dirent数据结构的长度,所以先前的读操作至少从flash中读取了部分文件名(甚至是全部的文件名)。所以这里将已读出的部分(全部)文件名拷贝到jffs2_full_dirent的name所指的空间中。
/* Do we need to copy any more of the name directly from the flash?*/
if (node.d.nsize + sizeof(struct jffs2_raw_dirent) > retlen) {
/* FIXME: point() */
int already = retlen - sizeof(struct jffs2_raw_dirent);
err = jffs2_flash_read(c, (ref_offset(ref)) + retlen,
node.d.nsize - already, &retlen, &fd->name[already]);
if (!err && retlen != node.d.nsize - already)
err = -EIO;
if (err) {
printk(KERN_WARNING "Read remainder of name in jffs2_get_inode_nodes():
error %d/n", err);
jffs2_free_full_dirent(fd);
goto free_out;
}
}
正是由于第一次可能只读出了部分文件名,所以这里可能需要读出剩余的文件名。注意第一次实际读出的数据长度为retlen,那么剩余文件名的起始地址在flash分区上的逻辑偏移为(ref_offset(ref) + retlen),而已经读出的部分文件名长度为already,而nsize为完整文件名的长度,所以二者之差为剩余文件名的长度。第二次读操作之间将剩余文件名读出到jffs2_full_dirent.name[already]所指的地方。
fd->nhash = full_name_hash(fd->name, node.d.nsize);
fd->next = NULL;
/* Wheee. We now have a complete jffs2_full_dirent structure, with
the name in it and everything. Link it into the list */
D1(printk(KERN_DEBUG "Adding fd /"%s/", ino #%u/n", fd->name, fd->ino));
jffs2_add_fd_to_list(c, fd, &ret_fd);
break;
最后,需要根据文件名计算一个“散列值”,记录到nhash域中,然后,通过jffs2_add_fd_to_list函数根据nhash值将目录文件的所有目录项的jffs2_full_dirent数据结构组织在ret_fd所指向的链表中(这个指针最终由参出返回,然后被设置到jffs2_inode_info.dents域)。
最后由break跳出switch结构,并开始新的for循环访问当前文件的下一个flash数据实体。
case JFFS2_NODETYPE_INODE:
D1(printk(KERN_DEBUG "Node at %08x (%d) is a data node/n", ref_offset(ref), ref_flags(ref)));
if (retlen < sizeof(node.i)) {
printk(KERN_WARNING "read too short for dnode/n");
err = -EIO;
goto free_out;
}
if (je32_to_cpu(node.i.version) > *highest_version)
*highest_version = je32_to_cpu(node.i.version);
D1(printk(KERN_DEBUG "version %d, highest_version now %d/n", je32_to_cpu(node.i.version),
*highest_version));
if (ref_obsolete(ref)) {
/* Obsoleted. This cannot happen, surely? dwmw2 20020308 */
printk(KERN_ERR "Inode node at 0x%08x became obsolete while we weren't looking/n",
ref_offset(ref));
BUG();
}
如果该数据实体为jffs2_raw_inode,则于上面处理目录项数据实体类似首先进行有效性检查。由于在挂载文件系统时并没对jffs2_raw_inode数据实体进行crc校验而是推迟到了真正打开文件时,所以在其内核描述符中设置了REF_UNCHECKED标志(参见jffs2_scan_inode_node函数的相关部分)。那么现在打开文件时就到了真正进行crc校验的时候了:对jffs2_raw_inode本身和后继数据进行crc校验。
/* If we've never checked the CRCs on this node, check them now. */
if (ref_flags(ref) == REF_UNCHECKED) {
uint32_t crc;
struct jffs2_eraseblock *jeb;
crc = crc32(0, &node, sizeof(node.i)-8);
if (crc != je32_to_cpu(node.i.node_crc)) {
printk(KERN_NOTICE "jffs2_get_inode_nodes(): CRC failed on node at 0x%08x: Read
0x%08x, calculated 0x%08x/n",
ref_offset(ref), je32_to_cpu(node.i.node_crc), crc);
jffs2_mark_node_obsolete(c, ref);
spin_lock_bh(&c->erase_completion_lock);
continue;
}
jffs2_raw_inode的最后两个域为其本身及其后数据的crc校验值,在计算本身的crc值时要去掉这个两个域所占的8个字节。如果crc校验失败,则将这个数据实体的内核描述符标记为“过时”,然后获得erase_completion_lock自旋锁后开始新的循环。如果jffs2_raw_inode数据实体本身的crc校验正确,下面接着对后继数据进行crc校验:(在挂载文件系统、为数据实体建立内核描述符时已经对数据实体本身进行了crc校验,这里再次检查就重复了)
if (node.i.compr != JFFS2_COMPR_ZERO && je32_to_cpu(node.i.csize)) {
/* FIXME: point() */
char *buf = kmalloc(je32_to_cpu(node.i.csize), GFP_KERNEL);
if (!buf)
return -ENOMEM;
err = jffs2_flash_read(c, ref_offset(ref) + sizeof(node.i), je32_to_cpu(node.i.csize),
&retlen, buf);
if (!err && retlen != je32_to_cpu(node.i.csize))
err = -EIO;
if (err) {
kfree(buf);
return err;
}
crc = crc32(0, buf, je32_to_cpu(node.i.csize));
kfree(buf);
if (crc != je32_to_cpu(node.i.data_crc)) {
printk(KERN_NOTICE "jffs2_get_inode_nodes(): Data CRC failed on node at
0x%08x: Read 0x%08x, calculated 0x%08x/n",
ref_offset(ref), je32_to_cpu(node.i.data_crc), crc);
jffs2_mark_node_obsolete(c, ref);
spin_lock_bh(&c->erase_completion_lock);
continue;
}
}
如果node.i.compr等于JFFS2_COMPR_ZERO,那么表示该数据实体对应的是一个洞。如果不是洞而且的确存在后继压缩过了数据,则需要进行crc校验(否则无需校验,则不进入这个if分支)。csize为压缩后数据的长度。首先从flash上读出压缩数据,计算出crc值后即可释放相应缓冲区。如果crc校验失败,则将这个数据实体的内核描述符标记为“过时”并然后获得erase_completion_lock自旋锁后开始新的循环。
/* Mark the node as having been checked and fix the accounting accordingly */
jeb = &c->blocks[ref->flash_offset / c->sector_size];
jeb->used_size += ref->totlen;
jeb->unchecked_size -= ref->totlen;
c->used_size += ref->totlen;
c->unchecked_size -= ref->totlen;
mark_ref_normal(ref);
}//if (ref_flags(ref) == REF_UNCHECKED)
对数据实体进行了crc校验后,就要通过mark_ref_normal改变其内核描述符的标志为REF_NORMAL,并且刷新数据实体所在擦除块描述符和文件系统超级块的u域中的used_size和unchecked_size统计信息。
tn = jffs2_alloc_tmp_dnode_info();
if (!tn) {
D1(printk(KERN_DEBUG "alloc tn failed/n"));
err = -ENOMEM;
goto free_out;
}
tn->fn = jffs2_alloc_full_dnode();
if (!tn->fn) {
D1(printk(KERN_DEBUG "alloc fn failed/n"));
err = -ENOMEM;
jffs2_free_tmp_dnode_info(tn);
goto free_out;
}
tn->version = je32_to_cpu(node.i.version);
tn->fn->ofs = je32_to_cpu(node.i.offset);
/* There was a bug where we wrote hole nodes out with csize/dsize swapped. Deal with it */
if (node.i.compr == JFFS2_COMPR_ZERO && !je32_to_cpu(node.i.dsize) &&
je32_to_cpu(node.i.csize))
tn->fn->size = je32_to_cpu(node.i.csize);
else // normal case...
tn->fn->size = je32_to_cpu(node.i.dsize);
tn->fn->raw = ref;
D1(printk(KERN_DEBUG "dnode @%08x: ver %u, offset %04x, dsize %04x/n",
ref_offset(ref), je32_to_cpu(node.i.version),
je32_to_cpu(node.i.offset), je32_to_cpu(node.i.dsize)));
jffs2_add_tn_to_list(tn, &ret_tn);
break;
接下来为jffs2_raw_inode分配相应的jffs2_tmp_dnode_info和jffs2_full_dnode数据结构并初始化,然后将同文件的jffs2_tmp_dnode_info组织到ret_tn所指向的链表中。最后由break退出switch,开始新的循环。
注意,jffs2_tmp_dnode_info数据结构组成了链表,其fn域指向相应的jffs2_full_dnode,而后者的raw域指向数据实体的内核描述符。此时尚需jffs2_node_frag数据结构才能组织红黑树,这个操作在返回到上层jffs2_do_read_inode函数中才完成。(而且一旦红黑树组织完毕,jffs2_tmp_dnode_info数据结构的链表即被释放)
一般情况下,文件由jffs2_raw_inode或者jffs2_raw_dirent数据实体组成。default分支中处理其它特殊类型的数据节点:(这些特殊的数据节点的作用是什么?由谁?何时写入?)
default:
if (ref_flags(ref) == REF_UNCHECKED) {
struct jffs2_eraseblock *jeb;
printk(KERN_ERR "Eep. Unknown node type %04x at %08x was marked
REF_UNCHECKED/n", je16_to_cpu(node.u.nodetype), ref_offset(ref));
/* Mark the node as having been checked and fix the accounting accordingly */
jeb = &c->blocks[ref->flash_offset / c->sector_size];
jeb->used_size += ref->totlen;
jeb->unchecked_size -= ref->totlen;
c->used_size += ref->totlen;
c->unchecked_size -= ref->totlen;
mark_ref_normal(ref);
}
(如果搞明白了为什么会出现特殊类型的数据实体,)如果特殊类型的数据实体尚未检查crc,则硬性改为已检查过的。为什么?
node.u.nodetype = cpu_to_je16(JFFS2_NODE_ACCURATE | je16_to_cpu(node.u.nodetype));
if (crc32(0, &node, sizeof(struct jffs2_unknown_node)-4) != je32_to_cpu(node.u.hdr_crc)) {
/* Hmmm. This should have been caught at scan time. */
printk(KERN_ERR "Node header CRC failed at %08x. But it must have been OK earlier./n",
ref_offset(ref));
printk(KERN_ERR "Node was: { %04x, %04x, %08x, %08x }/n",
je16_to_cpu(node.u.magic), je16_to_cpu(node.u.nodetype),
je32_to_cpu(node.u.totlen), je32_to_cpu(node.u.hdr_crc));
jffs2_mark_node_obsolete(c, ref);
}
对特殊类型的数据实体进行头部的crc校验。如果失败,则开始新的循环。否则进一步分析其类型:
else switch(je16_to_cpu(node.u.nodetype) & JFFS2_COMPAT_MASK) {
case JFFS2_FEATURE_INCOMPAT:
printk(KERN_NOTICE "Unknown INCOMPAT nodetype %04X at %08x/n",
je16_to_cpu(node.u.nodetype), ref_offset(ref));
/* EEP */
BUG();
break;
当初在挂载文件系统时如果发现了这种类型的数据实体,则拒绝挂载文件系统。所以现在在打开文件时就不会检查出这种类型的数据实体,否则一定是BUG。
case JFFS2_FEATURE_ROCOMPAT:
printk(KERN_NOTICE "Unknown ROCOMPAT nodetype %04X at %08x/n",
je16_to_cpu(node.u.nodetype), ref_offset(ref));
if (!(c->flags & JFFS2_SB_FLAG_RO))
BUG();
break;
当初在挂载文件系统时如果发现了这种类型的数据实体,则把文件系统挂载为RO的。现在在打开文件时如果发现文件含有这种类型的数据实体,则检查文件系统是否按照RO方式挂载的。
case JFFS2_FEATURE_RWCOMPAT_COPY:
printk(KERN_NOTICE "Unknown RWCOMPAT_COPY nodetype %04X at %08x/n",
je16_to_cpu(node.u.nodetype), ref_offset(ref));
break;
如果是这种类型的数据实体,则不做任何额外操作。
case JFFS2_FEATURE_RWCOMPAT_DELETE:
printk(KERN_NOTICE "Unknown RWCOMPAT_DELETE nodetype %04X at %08x/n",
je16_to_cpu(node.u.nodetype), ref_offset(ref));
jffs2_mark_node_obsolete(c, ref);
break;
如果是这种类型的数据实体,则标记其内核描述符为过时即可。在函数的最后,通过返回参数返回ret_tn和tet_fd链表的指针。
}
}//switch
spin_lock_bh(&c->erase_completion_lock);
}//for
spin_unlock_bh(&c->erase_completion_lock);
*tnp = ret_tn;
*fdp = ret_fd;
return 0;
free_out:
jffs2_free_tmp_dnode_info_list(ret_tn);
jffs2_free_full_dirent_list(ret_fd);
return err;
}
第6章 jffs2中写正规文件的方法
在打开文件、创建文件的inode数据结构时,在jffs2_read_inode函数中将正规文件的文件方法设置为:
case S_IFREG:
inode->i_op = &jffs2_file_inode_operations;
inode->i_fop = &jffs2_file_operations;
inode->i_mapping->a_ops = &jffs2_file_address_operations;
inode->i_mapping->nrpages = 0;
break;
访问文件的方法由jffs2_file_operation方法表提供,而文件的内存映射方法由jffs2_file_address_operation方法表提供,它们的定义如下:
struct file_operations jffs2_file_operations =
{
.llseek = generic_file_llseek,
.open = generic_file_open,
.read = generic_file_read,
.write = generic_file_write,
.ioctl = jffs2_ioctl,
.mmap = generic_file_mmap,
.fsync = jffs2_fsync,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,5,29)
.sendfile = generic_file_sendfile
#endif
};
struct address_space_operations jffs2_file_address_operations =
{
.readpage = jffs2_readpage,
.prepare_write = jffs2_prepare_write,
.commit_write = jffs2_commit_write
};
为了提高读写效率在设备驱动程序层次上设计了缓冲机制,以页面为单位缓存文件的内容。这样做的好处是可用很容易地通过mmap系统调用将文件的缓冲页面直接映射到用户进程空间中去,从而实现文件的内存映射。也就是说文件的内存映射是建立在文件缓冲的基础上的。
在inode中设计了一个域i_mapping,它指向一个address_space数据结构:
struct address_space {
struct list_head clean_pages; /* list of clean pages */
struct list_head dirty_pages; /* list of dirty pages */
struct list_head locked_pages; /* list of locked pages */
unsigned long nrpages; /* number of total pages */
struct address_space_operations *a_ops; /* methods */
struct inode *host; /* owner: inode, block_device */
struct vm_area_struct *i_mmap; /* list of private mappings */
struct vm_area_struct *i_mmap_shared; /* list of shared mappings */
spinlock_t i_shared_lock; /* and spinlock protecting it */
int gfp_mask; /* how to allocate the pages */
};
其中的clean_pages、dirty_pages、locked_pages分别指向页高速缓存中的相关页面,i_mmap、i_mmap_shared指向映射该文件的用户进程的线性区描述符的链表,host指向该数据结构所属的inode,a_ops指向的address_space_operations方法表提供了文件缓冲机制和设备驱动程序之间的接口(由这个方法表中的函数最终调用设备驱动程序)。
从下文可见,当用户进程访问文件时方法表jffs2_file_operation中的相应函数会被调用,而它又会进一步调用address_space_operations方法表中的相关函数来完成与flash交换数据实体的操作。
sys_write函数
sys_write函数为write系统调用的处理方法:
asmlinkage ssize_t sys_write(unsigned int fd, const char * buf, size_t count)
{
ssize_t ret;
struct file * file;
ret = -EBADF;
file = fget(fd);
进程描述符PCB中有一个file_struct数据结构,其中的fd[]数组为file数据结构的指针数组,用进程已经打开的文件号索引。首先通过fget函数返回与打开文件号fd相对应的file结构的地址,同时增加其引用计数。
if (file) {
if (file->f_mode & FMODE_WRITE) {
struct inode *inode = file->f_dentry->d_inode;
ret = locks_verify_area(FLOCK_VERIFY_WRITE, inode, file, file->f_pos, count);
if (!ret) {
ssize_t (*write)(struct file *, const char *, size_t, loff_t *);
ret = -EINVAL;
if (file->f_op && (write = file->f_op->write) != NULL)
ret = write(file, buf, count, &file->f_pos);
}
}
然后通过file数据结构得到文件索引节点inode的指针。在进行写操作前首先要由locks_verify_area检查在待写入的区域上没有已存在的写强制锁。(这也就是“强制”锁名称的来历了:任何写入操作都会执行检查)如果通过了检查,则调用file->f_op所指方法表中的write方法。
我们没有分析sys_open函数,这里仅指出当创建file对象时会用inode.i_fop指针设置file.f_op。所以这里调用的就是inode.i_fop指向的jffs2_file_operation方法表中的generic_file_wirte函数。
if (ret > 0)
dnotify_parent(file->f_dentry, DN_MODIFY);
fput(file);
}
return ret;
}
当写操作完成后,要通过fput函数减少文件的file对象的引用计数。
generic_file_write函数
/*
* Write to a file through the page cache.
*
* We currently put everything into the page cache prior to writing it.
* This is not a problem when writing full pages. With partial pages,
* however, we first have to read the data into the cache, then
* dirty the page, and finally schedule it for writing. Alternatively, we
* could write-through just the portion of data that would go into that
* page, but that would kill performance for applications that write data
* line by line, and it's prone to race conditions.
*
* Note that this routine doesn't try to keep track of dirty pages. Each
* file system has to do this all by itself, unfortunately.
* okir@monad.swb.de
*/
ssize_t
generic_file_write(struct file *file,const char *buf,size_t count, loff_t *ppos)
{
struct address_space *mapping = file->f_dentry->d_inode->i_mapping;
struct inode *inode = mapping->host;
unsigned long limit = current->rlim[RLIMIT_FSIZE].rlim_cur;
通过file、dentry、inode结构之间的链接关系,就可以由file找到文件的inode了。同时得到当前进程在文件大小方面的“资源限制”。
loff_t pos;
struct page *page, *cached_page;
ssize_t written;
long status = 0;
int err;
unsigned bytes;
if ((ssize_t) count < 0)
return -EINVAL;
if (!access_ok(VERIFY_READ, buf, count))
return -EFAULT;
首先进行必要的参数检查:待写入的数据量不能小于0,而且写入数据所在的用户空间必须是可读的。
cached_page = NULL;
down(&inode->i_sem);
在写操作开始前要获得信号量i_sem。根据待写入的数据量一次写入可能要分成若干次操作才能完成,但是在整个写入操作期间当前进程一直持有这个信号量,直到在这个函数退出前(即写入操作完成后)才释放,从而实现了写操作的原子性。
pos = *ppos;
err = -EINVAL;
if (pos < 0)
goto out;
任何针对文件的操作都是相对于进程在文件中的上下文,即文件指针ppos进行的。
err = file->f_error;
if (err) {
file->f_error = 0;
goto out;
}
执行失败的系统调用在返回用户态(即先前发出该系统调用的用户进程)前可能自动重新执行。比如在执行系统调用时发生阻塞,后因为收到信号而恢复执行,此时从结束阻塞处返回ERESTARTSYS。在从ret_from_sys_call返回后在do_signal中处理非阻塞挂起信号,信号处理方法决定了是否自动重新执行先前阻塞过程被打断的系统调用:在由iret返回用户态的系统调用封装函数时可以选择修改保存在内核栈中的返回地址,使从“int 0x80”处恢复执行(在x86体系结构上),从而在系统调用封装例程中再次发出系统调用、而不是返回用户进程(如果系统调用正常结束,或者相应信号的处理方法不自动重新执行失败的系统调用,则应该返回到封装例程中“int 0x80”之后的指令)。
file的f_error域用于记录错误。如果它不为0,则自动重新执行的系统调用就没有必要重新执行了,直接从out退出,向上层返回该错误值。
written = 0;
由下文可见写入操作可能要分多次完成。written记录了当前已经完成的写入量,这里首先清0。
/* FIXME: this is for backwards compatibility with 2.4 */
if (!S_ISBLK(inode->i_mode) && file->f_flags & O_APPEND)
pos = inode->i_size;
如果文件标志O_APPEND有效(表示只能向文件末尾追加数据),则调整写入位置为文件末尾(即文件的大小)。
/*
* Check whether we've reached the file size limit.
*/
err = -EFBIG;
if (!S_ISBLK(inode->i_mode) && limit != RLIM_INFINITY) {
if (pos >= limit) {
send_sig(SIGXFSZ, current, 0);
goto out;
}
if (pos > 0xFFFFFFFFULL || count > limit - (u32)pos) {
/* send_sig(SIGXFSZ, current, 0); */
count = limit - (u32)pos;
}
}
接着检查文件的大小是否超过系统的设定值。如果不是允许无限地写入(限制为RLIM_INFINITY,即没有限制),则如果待写入的位置大于文件大小的限制值,则给当前进程发送SIGXFSZ信号后直接退出,向上层返回的错误码为EFBIG;如果待写入的数据量超过了剩余可写入的数据量,则调整待写入量为允许写入的数据量。
下面的代码与“LFS rule”有关。(它是什么?暂时没有研究。)
/*
* LFS rule
*/
if ( pos + count > MAX_NON_LFS && !(file->f_flags&O_LARGEFILE)) {
if (pos >= MAX_NON_LFS) {
send_sig(SIGXFSZ, current, 0);
goto out;
}
if (count > MAX_NON_LFS - (u32)pos) {
/* send_sig(SIGXFSZ, current, 0); */
count = MAX_NON_LFS - (u32)pos;
}
}
/*
* Are we about to exceed the fs block limit ?
*
* If we have written data it becomes a short write
* If we have exceeded without writing data we send
* a signal and give them an EFBIG.
*
* Linus frestrict idea will clean these up nicely..
*/
if (!S_ISBLK(inode->i_mode)) {
if (pos >= inode->i_sb->s_maxbytes)
{
if (count || pos > inode->i_sb->s_maxbytes) {
send_sig(SIGXFSZ, current, 0);
err = -EFBIG;
goto out;
}
/* zero-length writes at ->s_maxbytes are OK */
}
if (pos + count > inode->i_sb->s_maxbytes)
count = inode->i_sb->s_maxbytes - pos;
} else {
if (is_read_only(inode->i_rdev)) {
err = -EPERM;
goto out;
}
if (pos >= inode->i_size) {
if (count || pos > inode->i_size) {
err = -ENOSPC;
goto out;
}
}
if (pos + count > inode->i_size)
count = inode->i_size - pos;
}
err = 0;
if (count == 0)
goto out;
remove_suid(inode);
inode->i_ctime = inode->i_mtime = CURRENT_TIME;
mark_inode_dirty_sync(inode);
只要待写入的数据量不为0下面就要开始真正的写操作了。刷新VFS的inode中的时间戳,并用mark_inode_dirty_sync函数将其标记为“脏”,以后它就会被写回到设备索引节点了。
(没有研究remove_suid函数即相关的机制,根据情景分析,如果当前进程没有setuid权利而且目标文件具有setuid和setgid属性,则它剥夺目标文件的这些属性,详见上册P588)
if (file->f_flags & O_DIRECT)
goto o_direct;
文件的O_DIRECT标志的作用如何?是否代表IO设备?尚未研究相关的generic_file_direct_IO函数。
do {
unsigned long index, offset;
long page_fault;
char *kaddr;
/*
* Try to find the page in the cache. If it isn't there, allocate a free page.
*/
offset = (pos & (PAGE_CACHE_SIZE -1)); /* Within page */
index = pos >> PAGE_CACHE_SHIFT;
bytes = PAGE_CACHE_SIZE - offset;
if (bytes > count)
bytes = count;
文件在逻辑上被认为是一个连续的线性空间,可用看作由若干连续页面组成。根据待写入的数据量及初始写入位置可能要在一个循环中分多次完成写入操作,而每次循环都只能针对一个页面写入。
在每次循环的开始都首先计算本次循环涉及的页面、写入位置在页面内的偏移和写入的数据量。pos为初始写入位置在文件内的偏移,它右移页面大小即得到文件内的页面号index,对页面大小取整即得到本次写操作在该页面内的偏移offset,而bytes为写入这个页面的数据量,参见下图。如果bytes大于剩余待写入的数据量,则调整bytes的值。
/*
* Bring in the user page that we will copy from _first_.
* Otherwise there's a nasty deadlock on copying from the
* same page as we're writing to, without it being marked
* up-to-date.
*/
{ volatile unsigned char dummy;
__get_user(dummy, buf);
__get_user(dummy, buf+bytes-1);
}
__get_user函数用于访问用户空间,这里通过两个__get_usr操作读取用户空间写缓冲区的首尾字节。这样可用保证一定为用户空间缓冲区分配了相应的物理页框。作者的注释是什么意思??
status = -ENOMEM; /* we'll assign it later anyway */
page = __grab_cache_page(mapping, index, &cached_page);
if (!page)
break;
/* We have exclusive IO access to the page.. */
if (!PageLocked(page)) {
PAGE_BUG(page);
}
前面计算出了本次循环要写入的页面,这里通过__grab_cache_page函数返回该页面在内核页高速缓存中对应的物理页框。内核页高速缓存中的所有物理页框的指针被组织在哈希表page_hash_table中,由于“页面在文件内的偏移”在系统内显然不唯一,所以在计算散列值时要使用文件的address_space结构的指针mapping(将该指针值当作无符号长整型来使用)。另外,在返回页框描述符时递增了页框的引用计数,等到本次循环结束时再递减。
得到了该页面对应的物理页框的内核描述符page数据结构的指针后,就要对该页面加锁。在本次循环结束前再解锁。
值得说明的是,一次循环只操作一个页框,所以加锁的粒度为页框。而写操作由多次循环组成,针对的是整个文件,所以加锁的粒度为整个文件。回想前面在进入循环前就获得了inode.i_sem信号量,在generic_file_write函数退出前才释放这个信号量,从而保证整个写文件操作的原子性。而在一次循环中加锁相应的页框,从而保证在循环期间对页框操作的原子性。
kaddr = kmap(page);
status = mapping->a_ops->prepare_write(file, page, offset, offset+bytes);
if (status)
goto sync_failure;
获得了页面对应的页框后,在开始真正的写操作前还需要进行一些必须的准备操作,比如如果页框不是“Uptodate”的话就得首先从设备上读出相应的页面(因为本次写操作不一定涵盖整个页面)。对于ext2文件系统,如果该页框时刚才才分配、并加入页高速缓存的,那么还需要为页框内的磁盘块缓冲区建立相应的描述符buffer_head,所有这些操作都由文件的address_space_operation方法表中prepare_write指针指向的函数完成。对于jffs2文件系统,由于底层flash驱动并不使用“磁盘块缓冲区”,所以只需要在相应的页框过时、且写入操作没有包括整个页框时读入它。另外如果page页框在文件内的起始大于文件大小,则本次循环将在文件中造成一个洞(hole),而且后面的写操作并不会描述这个洞,所以得向flash写入一个jffs2_raw_node数据实体来描述它。详见后文。(那么在ext2文件系统中是如何处理洞的?)
另外,kmap函数返回相应页框的内核虚拟地址kaddr。
page_fault = __copy_from_user(kaddr+offset, buf, bytes);
flush_dcache_page(page);
然后,用__copy_from_user函数从用户进程空间中读取buf缓冲区的bytes个字节到该页框内offset偏移处。注意offset和bytes在循环开始已经被设置为针对当前页框的偏移和写入量。
status = mapping->a_ops->commit_write(file, page, offset, offset+bytes);
if (page_fault)
goto fail_write;
if (!status)
status = bytes;
if (status >= 0) {
written += status;
count -= status;
pos += status;
buf += status;
}
将待写入的数据从用户空间复制到页高速缓存中的相应页框后,就可以进行真正的写入操作了,它由address_space_operation方法表的commit_write所指向的函数完成,即jffs2_commit_write函数,详见后文分析。在写入操作完成后根据实际写入的量刷新已写入数据量written、剩余写入数据量count、下次写入位置pos和用户空间缓冲区指针buf。
在ext2文件系统上写入是异步的,在generic_file_write中只需要将待写入数据复制到页高速缓存中的相应页框即可,而commit_write函数只是将脏页框提交给kflushd,然后由kflushd内核线程异步地将脏页框刷新回磁盘。在jffs2文件系统上写入是同步的,在这里立即执行写入操作(由flash驱动程序提供写入时的阻塞唤醒机制)。
unlock:
kunmap(page);
/* Mark it unlocked again and drop the page.. */
SetPageReferenced(page);
UnlockPage(page);
page_cache_release(page);
if (status < 0)
break;
} while (count);
在本次循环结束前还要递减页框的引用计数并解锁。
done:
*ppos = pos;
if (cached_page)
page_cache_release(cached_page);
/* For now, when the user asks for O_SYNC, we'll actually provide O_DSYNC. */
if (status >= 0) {
if ((file->f_flags & O_SYNC) || IS_SYNC(inode))
status = generic_osync_inode(inode, OSYNC_METADATA|OSYNC_DATA);
}
out_status:
err = written ? written : status;
out:
up(&inode->i_sem);
return err;
在generic_file_write函数结束前还要刷新文件指针ppos为最后一次循环后pos的值,返回总的写入的数据量或者错误码,并释放加在整个文件上的信号量inode.i_sem。
另外,在exit2文件系统上,如果文件标志O_SYNC有效,那么表示应该立即把相应文件页高速还从中的的脏页框刷新回磁盘。这个工作由generic_osync_inode函数完成(尚未研究该函数在jffs2文件系统中执行的具体操作)。
fail_write:
status = -EFAULT;
goto unlock;
sync_failure:
/*
* If blocksize < pagesize, prepare_write() may have instantiated a
* few blocks outside i_size. Trim these off again.
*/
kunmap(page);
UnlockPage(page);
page_cache_release(page);
if (pos + bytes > inode->i_size)
vmtruncate(inode, inode->i_size);
goto done;
o_direct:
written = generic_file_direct_IO(WRITE, file, (char *) buf, count, pos);
if (written > 0) {
loff_t end = pos + written;
if (end > inode->i_size && !S_ISBLK(inode->i_mode)) {
inode->i_size = end;
mark_inode_dirty(inode);
}
*ppos = end;
invalidate_inode_pages2(mapping);
}
/*
* Sync the fs metadata but not the minor inode changes and
* of course not the data as we did direct DMA for the IO.
*/
if (written >= 0 && file->f_flags & O_SYNC)
status = generic_osync_inode(inode, OSYNC_METADATA);
goto out_status;
}
jffs2_prepare_write函数
在generic_file_write函数的一次循环中要把[start,end]区间的数据写入pg页框,而在写入前必须完成如下准备工作:如果pg页框的在文件内的起始大于文件大小,则本次循环将在文件中造成一个洞(hole),所以得向flash写入一个jffs2_raw_node数据实体来描述这个洞。另外,如果pg页框的内容不是最新的,而且写入操作没有包括整个页框,则首先得从flash上读出该页框的内容。由jffs2_prepare_write函数完成这两个工作。
int jffs2_prepare_write (struct file *filp, struct page *pg, unsigned start, unsigned end)
{
struct inode *inode = pg->mapping->host;
struct jffs2_inode_info *f = JFFS2_INODE_INFO(inode);
uint32_t pageofs = pg->index << PAGE_CACHE_SHIFT;
int ret = 0;
如果页框位于页高速缓存,则其描述符page的mapping指向其所属文件的address_space数据结构,而其中的host即指向其所属文件的inode。页框描述符page的index指明页框在相应文件内的页面号,所以pageofs为页框在文件内的逻辑偏移。
down(&f->sem);
D1(printk(KERN_DEBUG "jffs2_prepare_write()/n"));
由于inode.i_sem在generic_file_write/read期间一直被当前执行流持有,用以实现原子地读写文件,用于实现上层用户进程之间的同步,而jffs2_inode_info.sem用于实现底层读写执行流与GC之间的同步:
在写文件时jffs2_prepare_write可能写入代表空洞的数据实体、在jffs2_commit_write中要写入新的数据实体,而GC内核线程每次执行时都将一个有效的数据实体的副本写入新的擦除块,即GC操作也是通过写入数据实体完成的。伴随着新数据实体的写入还需要:
1. 创建新的内核描述符jffs2_raw_node_ref,并加入文件描述符jffs2_inode_cache的nodes链表
2. 创建新的jffs2_full_dnode数据结构,并修改红黑树中的相应结点jffs2_node_frag的node域指向这个新的数据结构
所以jffs2_inode_cache.nodes链表及红黑树结点是写操作和GC操作的临界资源,故必须采用同步机制避免竞争条件,这也就是设计jffs2_inode_info.sem的初衷了。(另外更底层访问flash芯片时的同步问题由flash驱动程序处理。由此可见关键是要分析清楚操作系统各个层次上的竞争条件,比如竞争条件的双方是谁,何时触发竞争条件等,然后使用合适的同步机制加以解决。)
if (pageofs > inode->i_size) {
/* Make new hole frag from old EOF to new page */
struct jffs2_sb_info *c = JFFS2_SB_INFO(inode->i_sb);
struct jffs2_raw_inode ri;
struct jffs2_full_dnode *fn;
uint32_t phys_ofs, alloc_len;
D1(printk(KERN_DEBUG "Writing new hole frag 0x%x-0x%x between current EOF and new page/n",
(unsigned int)inode->i_size, pageofs));
如果新写入的页面在文件内的起始位置超过了文件的大小,则此次写入将在文件原有结尾处到该页面起始之间造成一个洞(hole),所以得向flash写入一个jffs2_raw_node数据实体来描述这个洞。
ret = jffs2_reserve_space(c, sizeof(ri), &phys_ofs, &alloc_len, ALLOC_NORMAL);
if (ret) {
up(&f->sem);
return ret;
}
在向flash写入jffs2_raw_inode数据实体之前得通过jffs2_reserve_space函数返回flash上一个合适的区间,由phys_ofs和alloc_len参数返回其位置及长度。(从flash上分配空间的操作可能因剩余空间不足而触发GC,同时在选择擦除块时必须考虑Wear Levelling策略。这个函数尚未详细研究)
memset(&ri, 0, sizeof(ri));
ri.magic = cpu_to_je16(JFFS2_MAGIC_BITMASK);
ri.nodetype = cpu_to_je16(JFFS2_NODETYPE_INODE);
ri.totlen = cpu_to_je32(sizeof(ri));
ri.hdr_crc = cpu_to_je32(crc32(0, &ri, sizeof(struct jffs2_unknown_node)-4));
ri.ino = cpu_to_je32(f->inocache->ino);
ri.version = cpu_to_je32(++f->highest_version);
ri.mode = cpu_to_je32(inode->i_mode);
ri.uid = cpu_to_je16(inode->i_uid);
ri.gid = cpu_to_je16(inode->i_gid);
ri.isize = cpu_to_je32(max((uint32_t)inode->i_size, pageofs));
ri.atime = ri.ctime = ri.mtime = cpu_to_je32(CURRENT_TIME);
ri.offset = cpu_to_je32(inode->i_size);
ri.dsize = cpu_to_je32(pageofs - inode->i_size);
ri.csize = cpu_to_je32(0);
ri.compr = JFFS2_COMPR_ZERO;
ri.node_crc = cpu_to_je32(crc32(0, &ri, sizeof(ri)-8));
ri.data_crc = cpu_to_je32(0);
由代码可以看出,洞仅由一个jffs2_raw_inode数据实体表示而不需要后继数据,所以其头部中的totlen就等于该数据结构本身的长度。而offset和dsize分别为洞在文件内的起始位置和长度,分别为i_size和pageofs – i_size。注意,如果数据实体对应一个洞,则设置其compr为JFFS2_COMPR_ZERO。然后通过jffs2_write_dnode函数将该数据实体写入flash,并创建相应的内核描述符jffs2_raw_node_ref,然后组织到链表中去。详见后文。
(其实,从原有文件结尾到该页面的start前都是一个洞,即文件内部[i_size, pageofs + start]区间都是洞。但是这里的数据实体只描述了洞的前部分[i_size, pageofs],而没有包括后部分[pageofs, pageofs + start]。所以我觉得洞的长度应该是pageofs - inode->i_size + start)
fn = jffs2_write_dnode(c, f, &ri, NULL, 0, phys_ofs, NULL);
if (IS_ERR(fn)) {
ret = PTR_ERR(fn);
jffs2_complete_reservation(c);
up(&f->sem);
return ret;
}
ret = jffs2_add_full_dnode_to_inode(c, f, fn);
同时,还必须为数据实体创建相应的jffs2_full_dnode、jffs2_node_frag数据结构并刷新红黑树。(jffs2_add_full_dnode_to_inode函数在前文打开文件、创建inode时就碰到过,涉及红黑树结点的插入,或者修改已有结点。尚未深入研究)
if (f->metadata) {
jffs2_mark_node_obsolete(c, f->metadata->raw);
jffs2_free_full_dnode(f->metadata);
f->metadata = NULL;
}
由目录文件的创建正规文件的create方法可见,在创建一个正规文件时、写入任何有意义的数据前,就首先向flash中写入了一个jffs2_raw_inode数据实体,其上层的jffs2_full_dnode则直接由jffs2_inode_info的metadata指向。而等到第一次真正写入有效数据时再将其标记为“过时”,而且以后的jffs2_full_dnode都组织在fragtree红黑树中。
if (ret) {
D1(printk(KERN_DEBUG "Eep. add_full_dnode_to_inode() failed in prepare_write,
returned %d/n", ret));
jffs2_mark_node_obsolete(c, fn->raw);
jffs2_free_full_dnode(fn);
jffs2_complete_reservation(c);
up(&f->sem);
return ret;
}
如果加入红黑树失败,则释放jffs2_full_dnode。由于相应的数据实体已经写入flash,所以没有删除其内核描述符,而是将其标记为“过时”。jffs2_complete_reservation函数用于向GC内核线程发送SIGHUP信号,使其唤醒。
jffs2_complete_reservation(c);
inode->i_size = pageofs;
}//if (pageofs > inode->i_size)
如果一切顺利,将文件大小增加洞的长度。注意,洞本身并不属于待写入的数据,所以没有刷新剩余数据量count和已写入数据量written,而只是调整了文件的大小。(为什么要在这里唤醒GC内核线程?调用jffs2_complete_reservation函数的时机是什么?)
/* Read in the page if it wasn't already present, unless it's a whole page */
if (!PageUptodate(pg) && (start || end < PAGE_CACHE_SIZE))
ret = jffs2_do_readpage_nolock(inode, pg);
D1(printk(KERN_DEBUG "end prepare_write(). pg->flags %lx/n", pg->flags));
up(&f->sem);
return ret;
}
最后,如果页框的内容不是“Uptodate”的,而且待写入的区域又不包括整个页框,则必须首先从设备中读出整个页框的内容,然后再写入相应的区间,最后再把整个页框写回。否则页框内不在本次写入范围内的其它数据就会丢失。jffs2_do_readpage_nolock函数的分析详见后文。
jffs2_commit_write函数
本函数将pg页框中[start, end]区间的数据写入flash。首先应该写入一个jffs2_raw_inode数据实体,然后再写入数据。同时创建相应的内核描述符jffs2_raw_node_ref以及jffs2_full_dnode和jffs2_node_frag,并刷新红黑树:要么插入新结点,要么刷新过时结点(从而使得红黑树只涉及有效的数据实体)。
int jffs2_commit_write (struct file *filp, struct page *pg, unsigned start, unsigned end)
{
/* Actually commit the write from the page cache page we're looking at.
* For now, we write the full page out each time. It sucks, but it's simple */
struct inode *inode = pg->mapping->host;
struct jffs2_inode_info *f = JFFS2_INODE_INFO(inode);
struct jffs2_sb_info *c = JFFS2_SB_INFO(inode->i_sb);
struct jffs2_raw_inode *ri;
int ret = 0;
uint32_t writtenlen = 0;
D1(printk(KERN_DEBUG "jffs2_commit_write(): ino #%lu, page at 0x%lx, range %d-%d, flags %lx/n",
inode->i_ino, pg->index << PAGE_CACHE_SHIFT, start, end, pg->flags));
if (!start && end == PAGE_CACHE_SIZE) {
/* We need to avoid deadlock with page_cache_read() in
jffs2_garbage_collect_pass(). So we have to mark the
page up to date, to prevent page_cache_read() from trying to re-lock it. */
SetPageUptodate(pg);
}
如果写入的范围包括整个页框,那么在写入前就设置页框的“Uptodate”标志。根据作者的注释,其实这样做的目的是为了避免和GC执行的page_cache_read发生死锁。为什么当前写入整个页框时就会发生死锁?死锁是怎么产生的?写入部分页框时会发生死锁么?需要研究GC。
ri = jffs2_alloc_raw_inode();
if (!ri) {
D1(printk(KERN_DEBUG "jffs2_commit_write(): Allocation of raw inode failed/n"));
return -ENOMEM;
}
/* Set the fields that the generic jffs2_write_inode_range() code can't find */
ri->ino = cpu_to_je32(inode->i_ino);
ri->mode = cpu_to_je32(inode->i_mode);
ri->uid = cpu_to_je16(inode->i_uid);
ri->gid = cpu_to_je16(inode->i_gid);
ri->isize = cpu_to_je32((uint32_t)inode->i_size); //文件大小
ri->atime = ri->ctime = ri->mtime = cpu_to_je32(CURRENT_TIME);
在将数据实体写入flash前首先得准备好一个jffs2_raw_inode数据实体,并根据文件的索引节点inode来设置它。这里只设定了部分域,剩下的与后继数据长度相关的域在下面写入flash的函数中再接着设置。这是因为根据数据量可能需要写入多个jffs2_raw_inode数据实体,而这又是因为数据实体的后继数据有最大长度限制。但是这些数据实体的jffs2_raw_inode中含有关于该文件的相同的信息,而这些相同的信息在这里就可以设置了,剩下与数据长度相关的域及版本号在写入flash的函数中才能确定。
下面将jffs2_raw_inode及相应的数据一起写入flash。注意先前在generic_file_write函数中用kmap函数得到了pg页框的内核虚拟地址,所以传递的第四个参数为该页框内写入位置的内核虚拟地址,而第五个参数为写入位置在文件内的逻辑偏移,写入长度为end – start,实际写入的数据量由writtenlen参数返回。另外,在这个函数中还要创建数据实体的内核描述符jffs2_raw_node_ref和jffs2_full_dnode、jffs2_node_frag,并刷新红黑树。详见下文。
/* We rely on the fact that generic_file_write() currently kmaps the page for us. */
//传递数据的起始虚拟地址和在文件内的逻辑偏移(jffs2_raw_inode.offset记录数据在文件内的偏移)
ret = jffs2_write_inode_range(c, f, ri, page_address(pg) + start,
(pg->index << PAGE_CACHE_SHIFT) + start, end - start, &writtenlen);
if (ret) {
/* There was an error writing. */
SetPageError(pg);
}
if (writtenlen) {
if (inode->i_size < (pg->index << PAGE_CACHE_SHIFT) + start + writtenlen) {
inode->i_size = (pg->index << PAGE_CACHE_SHIFT) + start + writtenlen;
inode->i_blocks = (inode->i_size + 511) >> 9;
inode->i_ctime = inode->i_mtime = je32_to_cpu(ri->ctime);
}
}
jffs2_free_raw_inode(ri);
(pg->index << PAGE_CACHE_SHIFT + start)为写入位置在文件内的逻辑偏移,而本次写入的数据量为writtenlen,所以它们的和为新的文件末尾位置。写操作完成后设置文件大小i_size为这个值,并释放jffs2_raw_inode数据实体。
if (start+writtenlen < end) {
/* generic_file_write has written more to the page cache than we've
actually written to the medium. Mark the page !Uptodate so that it gets reread */
D1(printk(KERN_DEBUG "jffs2_commit_write(): Not all bytes written. Marking page !uptodate/n"));
SetPageError(pg);
ClearPageUptodate(pg);
}
D1(printk(KERN_DEBUG "jffs2_commit_write() returning %d/n",writtenlen?writtenlen:ret));
return writtenlen?writtenlen:ret;
}
由上文可见,在函数的开始如果发现写入范围包括了整个页框则设置了其“Uptodate”标志。如果写入操作完成后发现没有写入额定的数据,即没有写完整个页框,则必须清除页框的“Uptodate”标志。
另外,在generic_file_write的一个循环中写入一个页框。在一个页框写入后怎么没看到设置其“Uptodate”标志??
jffs2_write_inode_range函数
该函数将jffs2_raw_inode及相应的数据一起写入flash。第四个参数为该页框内写入位置的内核虚拟地址,而第五个参数为写入位置在文件内的逻辑偏移,写入长度为end – start,实际写入的数据量由writtenlen参数返回。另外,在这个函数中还要创建数据实体的内核描述符jffs2_raw_node_ref和jffs2_full_dnode、jffs2_node_frag,并刷新红黑树。
/* The OS-specific code fills in the metadata in the jffs2_raw_inode for us, so that
we don't have to go digging in struct inode or its equivalent. It should set:
mode, uid, gid, (starting)isize, atime, ctime, mtime */
int jffs2_write_inode_range(struct jffs2_sb_info *c, struct jffs2_inode_info *f,
struct jffs2_raw_inode *ri, unsigned char *buf,
uint32_t offset, uint32_t writelen, uint32_t *retlen)
{
int ret = 0;
uint32_t writtenlen = 0;
D1(printk(KERN_DEBUG "jffs2_write_inode_range(): Ino #%u, ofs 0x%x, len 0x%x/n",
f->inocache->ino, offset, writelen));
因为每个数据实体所携带的数据有长度限制,所以根据待写入数据量可能需要写入多个jffs2_raw_inode数据实体。但是这些数据实体的jffs2_raw_inode中含有关于该文件的相同的信息,而这些相同的信息在jffs2_commit_write函数中就已经设置好了,而剩下的与数据长度相关的域及版本号在这里才能确定。
下面就在一个循环中写入若干数据实体,以便把所有的数据都写入flash。在写入一个数据实体前可能要首先压缩数据,并设置jffs2_raw_inode中与后继数据长度相关的域。在写入操作完成后,还要为数据实体创建内核描述符jffs2_raw_node_ref及相应的jffs2_full_dnode和jffs2_node_frag,并刷新红黑树:要么插入新结点,要么刷新结点指向新的jffs2_node_frag(从而使得红黑树只与有效数据实体有关),并标记相关区域的原有数据实体为“过时”。
while(writelen) {
struct jffs2_full_dnode *fn;
unsigned char *comprbuf = NULL;
unsigned char comprtype = JFFS2_COMPR_NONE;
uint32_t phys_ofs, alloclen;
uint32_t datalen, cdatalen;
D2(printk(KERN_DEBUG "jffs2_commit_write() loop: 0x%x to write to 0x%x/n", writelen, offset));
ret = jffs2_reserve_space(c, sizeof(*ri) + JFFS2_MIN_DATA_LEN, &phys_ofs, &alloclen,
ALLOC_NORMAL);
if (ret) {
D1(printk(KERN_DEBUG "jffs2_reserve_space returned %d/n", ret));
break;
}
down(&f->sem);
首先通过jffs2_reserve_space函数在flash上找到一个合适大小的空间,参数phys_ofs和alloclen返回该空间的位置和大小。注意传递的第二个参数为sizeof(*ri) + JFFS2_MIN_DATA_LEN,可见数据实体后继数据是有最大长度限制的。在写操作期间要持有信号量jffs2_inode_info.sem,它用于实现写操作和GC操作之间的同步,参见上文。
datalen = writelen;
cdatalen = min(alloclen - sizeof(*ri), writelen);
comprbuf = kmalloc(cdatalen, GFP_KERNEL);
writelen为剩余待写入的数据量,而给本次写操作分配的空间长度为alloclen,所以本次写操作实际写入的原始数据量为cdatalen。这里按照本次操作的原始写入量分配一个缓冲区用于存放压缩后的待写入数据,然后通过jffs2_compress函数压缩原始数据,其参数为:原始数据在buf中,长度为datalen。函数返回后datalen为实际被压缩了数据量;压缩后的数据存放在comprbuf中,缓冲区长度为cdatalen。函数返回后为实际被压缩了的数据长度(可能小于comprbuf的长度)(尚未研究jffs2所采用的压缩算法):
if (comprbuf) {
comprtype = jffs2_compress(buf, comprbuf, &datalen, &cdatalen);
}
if (comprtype == JFFS2_COMPR_NONE) {
/* Either compression failed, or the allocation of comprbuf failed */
if (comprbuf)
kfree(comprbuf);
comprbuf = buf;
datalen = cdatalen;
}
如果压缩失败,则数据实体的后继数据没有被压缩,所以实际参与压缩的数据量datalen就等于先前参与压缩的数据量cdatalen,而comprbuf也指向原始数据缓冲区buf;如果压缩成功,则根据jffs2_compress函数语义,datalen为实际参与压缩的数据量,cdatalen为压缩后的数据量,下面就可以根据这两个返回参数确定数据实体中与后继数据长度相关的域了:
/* Now comprbuf points to the data to be written, be it compressed or not.
comprtype holds the compression type, and comprtype == JFFS2_COMPR_NONE means
that the comprbuf doesn't need to be kfree()d. */
ri->magic = cpu_to_je16(JFFS2_MAGIC_BITMASK);
ri->nodetype = cpu_to_je16(JFFS2_NODETYPE_INODE);
ri->totlen = cpu_to_je32(sizeof(*ri) + cdatalen);
ri->hdr_crc = cpu_to_je32(crc32(0, ri, sizeof(struct jffs2_unknown_node)-4));
ri->ino = cpu_to_je32(f->inocache->ino);
ri->version = cpu_to_je32(++f->highest_version);
ri->isize = cpu_to_je32(max(je32_to_cpu(ri->isize), offset + datalen));
ri->offset = cpu_to_je32(offset);
ri->csize = cpu_to_je32(cdatalen);
ri->dsize = cpu_to_je32(datalen);
ri->compr = comprtype;
ri->node_crc = cpu_to_je32(crc32(0, ri, sizeof(*ri)-8));
ri->data_crc = cpu_to_je32(crc32(0, comprbuf, cdatalen));
由此可见,数据实体头部中的totlen为数据实体本身及后继数据的长度;同一个文件的所有数据实体的version号递增(一个文件的最高version号记录在jffs2_inode_info.highest_verison);数据实体中的csize和dsize分别为压缩了的和解压缩后的数据长度;compr为对压缩算法的描述;node_crc为数据实体本身的crc校验值,data_crc为后继数据本身的校验值。另外,offset为该数据实体的后继数据在文件内的逻辑偏移。
fn = jffs2_write_dnode(c, f, ri, comprbuf, cdatalen, phys_ofs, NULL);
准备好数据实体和后继数据后,就可以将它们顺序地写入flash了。jffs2_write_dnode函数将jffs2_raw_inode和压缩过的数据写入flash上phys_ofs处,同时分配内核描述符jffs2_raw_node_ref以及相应的jffs2_full_dnode,将前者加入文件的链表并返回后者的地址。
if (comprtype != JFFS2_COMPR_NONE)
kfree(comprbuf);
if (IS_ERR(fn)) {
ret = PTR_ERR(fn);
up(&f->sem);
jffs2_complete_reservation(c);
break;
}
ret = jffs2_add_full_dnode_to_inode(c, f, fn);
写入flash完成后即可释放缓存压缩数据的缓冲区并释放jffs2_inode_info.sem信号量。在jffs2_write_dnode函数中创建、注册了数据实体的内核描述符,还要用jffs2_add_full_dnode_to_inode函数在文件的红黑树中查找相应数据实体的jffs2_node_frag数据结构,如果没找到,则创建新的并插入红黑树;如果找到,则将其改为指向新的jffs2_full_node,并递减原有jffs2_full_node的移用计数,并标记原有数据结点的内核描述为过时。
if (f->metadata) {
jffs2_mark_node_obsolete(c, f->metadata->raw);
jffs2_free_full_dnode(f->metadata);
f->metadata = NULL;
}
这里还有一个需要考虑的问题。当调用目录文件的create方法创建正规文件时,除了向父目录文件写入其目录项jffs2_raw_dirent数据实体外,还要向flash中写入“代表文件存在”的第一个jffs2_raw_inode数据实体。注意此时并没有需要写入的数据,所以这个jffs2_raw_inode数据实体的dsize域为0,而且其jffs2_full_dnode由jffs2_inode_info的metadata直接指向而没有组织到fragtree红黑树中,参见jffs2_do_create函数的相关部分。直到真正进行第一次写操作时将这个数据结点标记为过时的,这也就是上段代码的作用了。另外,由jffs2map2可以观察到当用“echo 1 > 1.txt”创建正规文件1.txt时,该文件存在两个数据实体,相信被标记为过时的那个就是在创建时写入的,而第二个数据实体含有该文件的数据。参见附录。
if (ret) { /* Eep */
D1(printk(KERN_DEBUG "Eep. add_full_dnode_to_inode() failed in commit_write, returned
%d/n", ret));
jffs2_mark_node_obsolete(c, fn->raw);
jffs2_free_full_dnode(fn);
up(&f->sem);
jffs2_complete_reservation(c);
break;
}
up(&f->sem);
jffs2_complete_reservation(c);
if (!datalen) {
printk(KERN_WARNING "Eep. We didn't actually write any data in ffs2_write_inode_range()/n");
ret = -EIO;
break;
}
D1(printk(KERN_DEBUG "increasing writtenlen by %d/n", datalen));
writtenlen += datalen;
offset += datalen;
writelen -= datalen;
buf += datalen;
}//while
*retlen = writtenlen;
return ret;
}
在函数的最后刷新writtenlen和offset:datalen在jffs2_compress函数返回后为实际参与压缩的数据量,用它递增已写入数据量writtenlen、待写入数据在文件内的逻辑偏移offset、待写入数据在缓冲区内的偏移buf并递减待写入数据量writenlen。
jffs2_write_dnode函数
jffs2_write_dnode函数将ri所指jffs2_raw_inode和data缓冲区内长度为datalen的数据写入flash上phys_ofs处,同时分配内核描述符jffs2_raw_node_ref以及相应的jffs2_full_dnode,将前者加入文件的链表并返回后者的地址。
/* jffs2_write_dnode - given a raw_inode, allocate a full_dnode for it,
write it to the flash, link it into the existing inode/fragment list */
struct jffs2_full_dnode *jffs2_write_dnode(struct jffs2_sb_info *c, struct jffs2_inode_info *f,
struct jffs2_raw_inode *ri, const unsigned char *data,
uint32_t datalen, uint32_t flash_ofs, uint32_t *writelen)
{
struct jffs2_raw_node_ref *raw;
struct jffs2_full_dnode *fn;
size_t retlen;
struct iovec vecs[2];
int ret;
unsigned long cnt = 2;
D1(if(je32_to_cpu(ri->hdr_crc) != crc32(0, ri, sizeof(struct jffs2_unknown_node)-4)) {
printk(KERN_CRIT "Eep. CRC not correct in jffs2_write_dnode()/n");
BUG();});
vecs[0].iov_base = ri;
vecs[0].iov_len = sizeof(*ri);
vecs[1].iov_base = (unsigned char *)data;
vecs[1].iov_len = datalen;
显然写入操作分两步完成,依次写入数据实体及后继数据,于是用两个iovec类型的变量分别指向它们的基地址和长度。
writecheck(c, flash_ofs);
if (je32_to_cpu(ri->totlen) != sizeof(*ri) + datalen) {
printk(KERN_WARNING "jffs2_write_dnode: ri->totlen (0x%08x) != sizeof(*ri) (0x%08x) + datalen
(0x%08x)/n", je32_to_cpu(ri->totlen), sizeof(*ri), datalen);
}
raw = jffs2_alloc_raw_node_ref();
if (!raw)
return ERR_PTR(-ENOMEM);
fn = jffs2_alloc_full_dnode();
if (!fn) {
jffs2_free_raw_node_ref(raw);
return ERR_PTR(-ENOMEM);
}
raw->flash_offset = flash_ofs;
raw->totlen = PAD(sizeof(*ri)+datalen);
raw->next_phys = NULL;
fn->ofs = je32_to_cpu(ri->offset);
fn->size = je32_to_cpu(ri->dsize);
fn->frags = 0;
fn->raw = raw;
在写入前还要为新的数据实体创建相应的内核描述符jff2_raw_node_ref和jffs2_full_dnode数据结构,内描述符的flash_offset和totlen为数据实体在flash分区内的逻辑偏移和整个数据实体的长度。由于此时尚未加入相应文件的链表,所以next_phys域暂时设置为NULL;后者用于描述后继数据在文件内的位置和长度,所以ofs和size域分别为相应数据在文件内的逻辑偏移和长度。由于此时尚未创建相应的jffs2_node_frag数据结构,所以其frags域设置为0。
/* check number of valid vecs */
if (!datalen || !data)
cnt = 1;
ret = jffs2_flash_writev(c, vecs, cnt, flash_ofs, &retlen);
前面已经用两个iovec类型的数据结构描述好了数据实体及后继数据的基地址及长度,这里就可以通过jffs2_flash_writev函数将它们写入flash了(这个函数最终调用flash驱动的mtd->writev方法)。retlen返回实际写入的数据量。如果实际写入的数据量小于整个数据实体的长度、或者函数返回错误,则需要立即处理:
if (ret || (retlen != sizeof(*ri) + datalen)) {
printk(KERN_NOTICE "Write of %d bytes at 0x%08x failed. returned %d, retlen %d/n",
sizeof(*ri)+datalen, flash_ofs, ret, retlen);
/* Mark the space as dirtied */
if (retlen) {
/* Doesn't belong to any inode */
raw->next_in_ino = NULL;
/* Don't change raw->totlen to match retlen. We may have
written the node header already, and only the data will
seem corrupted, in which case the scan would skip over
any node we write before the original intended end of this node */
raw->flash_offset |= REF_OBSOLETE;
jffs2_add_physical_node_ref(c, raw);
jffs2_mark_node_obsolete(c, raw);
}
如果实际写入的数据量小于整个数据实体的长度,即认为只写入了部分数据,而数据实体本身认为已完整写入。根据作者的注释此时并不改变头部中的totlen域,而是将内核描述标记为过时,并照样加入文件的链表。
else {
printk(KERN_NOTICE "Not marking the space at 0x%08x as dirty because the flash driver
returned retlen zero/n", raw->flash_offset);
jffs2_free_raw_node_ref(raw);
}
/* Release the full_dnode which is now useless, and return */
jffs2_free_full_dnode(fn);
if (writelen)
*writelen = retlen;
return ERR_PTR(ret?ret:-EIO);
}
/* Mark the space used */
if (datalen == PAGE_CACHE_SIZE)
raw->flash_offset |= REF_PRISTINE;
else
raw->flash_offset |= REF_NORMAL;
如果成功写入,则需要设置数据实体的内核描述符的相关标志:如果后继数据为整个页框大小,则设置PREF_PRISTINE标志,至少也是REF_NORMAL标志。然后,将其内核描述符加入文件的jffs2_inode_cache的nodes域指向的链表的首部,并通过jffs2_add_physical_node_ref函数更新相应flash擦除块和文件系统内的相关统计信息:
jffs2_add_physical_node_ref(c, raw);
/* Link into per-inode list */
raw->next_in_ino = f->inocache->nodes;
f->inocache->nodes = raw;
D1(printk(KERN_DEBUG "jffs2_write_dnode wrote node at 0x%08x with dsize 0x%x, csize 0x%x,
node_crc 0x%08x, data_crc 0x%08x, totlen 0x%08x/n",
flash_ofs, je32_to_cpu(ri->dsize), je32_to_cpu(ri->csize),
je32_to_cpu(ri->node_crc), je32_to_cpu(ri->data_crc), je32_to_cpu(ri->totlen)));
if (writelen)
*writelen = retlen;
f->inocache->nodes = raw;
return fn;
}
第7章 jffs2中读正规文件的方法
与写正规文件类似,读正规文件时函数调用路径如下:
sys_read > do_generic_file_read > jffs2_readpage > jffs2_do_readpage_unlock > jffs2_do_readpage_nolock
在do_generic_file_read函数中需要处理预读,并且在一个循环中通过inode.i_mapping->a_ops->readpage方法依次读出文件的各个页面到页高速缓存的相应页框中,而这个方法即为jffs2_readpage函数。
jffs2_readpage函数
int jffs2_readpage (struct file *filp, struct page *pg)
{
struct jffs2_inode_info *f = JFFS2_INODE_INFO(pg->mapping->host);
int ret;
down(&f->sem);
ret = jffs2_do_readpage_unlock(pg->mapping->host, pg);
up(&f->sem);
return ret;
}
int jffs2_do_readpage_unlock(struct inode *inode, struct page *pg)
{
int ret = jffs2_do_readpage_nolock(inode, pg);
unlock_page(pg);
return ret;
}
在do_generic_file_read函数中启动一个页面的读操作前,已经通过lock_page获得了页高速缓存中页框的锁,所以在读操作完成后还要释放锁。
jffs2_do_readpage_nolock函数
这个函数读出文件中指定的一页内容到其页高速缓存中的相应页框中(页框描述符由pg参数指向)。
int jffs2_do_readpage_nolock (struct inode *inode, struct page *pg)
{
struct jffs2_inode_info *f = JFFS2_INODE_INFO(inode);
struct jffs2_sb_info *c = JFFS2_SB_INFO(inode->i_sb);
unsigned char *pg_buf;
int ret;
D1(printk(KERN_DEBUG "jffs2_do_readpage_nolock(): ino #%lu, page at offset 0x%lx/n", inode->i_ino,
pg->index << PAGE_CACHE_SHIFT));
if (!PageLocked(pg))
PAGE_BUG(pg);
在操作函数调用链的上游、在do_generic_file_read函数中就已经获得了该页框的锁,否则为BUG。
pg_buf = kmap(pg);
/* FIXME: Can kmap fail? */
ret = jffs2_read_inode_range(c, f, pg_buf, pg->index << PAGE_CACHE_SHIFT,
PAGE_CACHE_SIZE);
然后,由kmap函数返回相应页框的内核虚拟地址,并由jffs2_read_inode_range函数读入整个页面的内容到该页框中。详见下文。
if (ret) {
ClearPageUptodate(pg);
SetPageError(pg);
} else {
SetPageUptodate(pg); //如果读成功,则设置uptodate标志
ClearPageError(pg);
}
flush_dcache_page(pg);
kunmap(pg);
D1(printk(KERN_DEBUG "readpage finished/n"));
return 0;
}
最后,根据读操作完成的情况设置页框的“Uptodate”标志、清除错误标志,或者反之。并由kunmap函数解除内核页表中对相应页框的映射。
jffs2_read_inode_range函数
jffs2_read_inode_range函数用于从flash上读取文件的一个页框中[offset, offset + len]区域的内容到页高速缓存中。在打开文件、读inode时已经为所有有效的jffs2_raw_inode数据实体创建了相应的jffs2_full_dnode,并由jffs2_node_frag加入了红黑树,这个函数仅是通过红黑树访问相应的数据实体即可。
(早在打开文件、创建inode时就会跳过所有过时的数据实体,另外写入新的数据实体时会修改红黑树中的相应结点的node指向新数据实体的jffs2_node_frag,那么红黑树中的结点始终指向有效数据实体。flash上的数据实体一旦过时就再也不会被访问到,最终它所在的擦除块会被GC加入待擦除链表。)
第三个参数为缓冲区的内核虚拟地址,第四、五个参数描述文件的一个页中的一个区域。为了帮助理解这个函数的逻辑,可以参考下图所示的具有普遍意义的一种情形:
假设文件的一个页面内容由三部分数据组成:第一块数据的起始即为页面起始,第三块数据结束在页面结尾处,两部分数据之间存在一个空洞。三部分数据都由相应的jffs2_raw_inode数据实体描述(回想在写操作时address_space_operation的prepare_write方法向flash写入一个数据实体来描述空洞)。而需要读出的区域始于第一块数据中间、终于第三块数据中间。下面我们就以这种情形为例来分析这个函数的逻辑:
int jffs2_read_inode_range(struct jffs2_sb_info *c, struct jffs2_inode_info *f, unsigned char *buf,
uint32_t offset, uint32_t len)
{
uint32_t end = offset + len;
struct jffs2_node_frag *frag;
int ret;
D1(printk(KERN_DEBUG "jffs2_read_inode_range: ino #%u, range 0x%08x-0x%08x/n",
f->inocache->ino, offset, offset+len));
frag = jffs2_lookup_node_frag(&f->fragtree, offset);
由于在打开文件、创建inode时已经为为jffs2_raw_inode数据实体创建了相应的jffs2_full_dnode,并由jffs2_node_frag加入了红黑树,所以在读文件时就可以通过红黑树来定位相应的数据实体了。这里首先找到包含offset的数据实体,或者起始位置ofs大于offset、但ofs又是最小的数据实体(jffs2_lookup_node_frag涉及红黑树的查找,尚未详细分析,所以该函数的行为有待确认)。
/* XXX FIXME: Where a single physical node actually shows up in two
frags, we read it twice. Don't do that. */
/* Now we're pointing at the first frag which overlaps our page */
由于待读出的区域可能涉及多个数据实体,所以在一个循环中可能需要分多次读出,其中每次循环只涉及一个数据实体中的数据(尽管一次循环待读出的数据可能只是一个数据实体的一部分,但是也要首先读出整个数据实体的内容。)每次循环后递增offset指针,并从红黑树中返回指向“后继”数据实体的frag。
while(offset < end) {
D2(printk(KERN_DEBUG "jffs2_read_inode_range: offset %d, end %d/n", offset, end));
if (!frag || frag->ofs > offset) {
uint32_t holesize = end - offset;
if (frag) {
D1(printk(KERN_NOTICE "Eep. Hole in ino #%u fraglist. frag->ofs = 0x%08x,
offset = 0x%08x/n", f->inocache->ino, frag->ofs, offset));
holesize = min(holesize, frag->ofs - offset);
D1(jffs2_print_frag_list(f));
}
D1(printk(KERN_DEBUG "Filling non-frag hole from %d-%d/n", offset, offset+holesize));
memset(buf, 0, holesize);
buf += holesize;
offset += holesize;
continue;
在上一个数据实体读出后,frag即指向其“后继”的数据实体。或者在第一次循环中frag指向第一个数据实体。如果该数据实体的数据在文件的相关页面内的逻辑偏移大于offset,则说明在两次循环的两个数据实体之间存在一个空洞、或者在第一次循环、第一个数据实体前存在一个空洞。此时洞的大小为ofs – offset。洞内的数据都为0,所以直接将buf中相应长度的区域清0即可,同时步进offset和buf指针为洞的大小,并开始下一轮循环。
} else if (frag->ofs < offset && (offset & (PAGE_CACHE_SIZE-1)) != 0) {
D1(printk(KERN_NOTICE "Eep. Overlap in ino #%u fraglist. frag->ofs = 0x%08x,
offset = 0x%08x/n", f->inocache->ino, frag->ofs, offset));
D1(jffs2_print_frag_list(f));
memset(buf, 0, end - offset);
return -EIO;
}
这不正是我假设的第一种情况吗,为什么是非法的呢?!
else if (!frag->node) {
uint32_t holeend = min(end, frag->ofs + frag->size);
D1(printk(KERN_DEBUG "Filling frag hole from %d-%d (frag 0x%x 0x%x)/n", offset, holeend,
frag->ofs, frag->ofs + frag->size));
memset(buf, 0, holeend - offset);
buf += holeend - offset;
offset = holeend;
frag = frag_next(frag);
continue;
}
jffs2_node_frag的node域指向数据实体的jffs2_full_dnode数据结构。在什么情况下会为NULL??这种情况也是洞,将读缓冲区中buf开始相应长度的区域清0(另外在jffs2_prepare_write中写入洞的数据实体,创建相应内核描述符并加入红黑树)。
else {
uint32_t readlen;
uint32_t fragofs; /* offset within the frag to start reading */
fragofs = offset - frag->ofs;
readlen = min(frag->size - fragofs, end - offset);
D1(printk(KERN_DEBUG "Reading %d-%d from node at 0x%x/n", frag->ofs+fragofs,
frag->ofs+fragofs+readlen, ref_offset(frag->node->raw)));
fragofs为数据起始位置在数据实体内的偏移。在我们假设的情形中,本次循环需要读取数据实体中除fragofs之外的数据,所以读出的数据量readlen等于数据实体大小frag->size减去fragofs。
第四个参数为什么是这样?? frag.ofs和jffs2_full_dnode.ofs都是数据在文件内的偏移,相减为0。那么第四个参数不就是数据在数据实体内部的偏移fragofs吗??从jffs2_read_dnode函数分析看,第四个参数的确也是指起始读位置在数据实体内部的偏移。
ret = jffs2_read_dnode(c, frag->node, buf, fragofs + frag->ofs - frag->node->ofs, readlen);
D2(printk(KERN_DEBUG "node read done/n"));
if (ret) {
D1(printk(KERN_DEBUG"jffs2_read_inode_range error %d/n",ret));
memset(buf, 0, readlen);
return ret;
}
buf += readlen;
offset += readlen;
frag = frag_next(frag);
D2(printk(KERN_DEBUG "node read was OK. Looping/n"));
continue;
}
printk(KERN_CRIT "dwmw2 is stupid. Reason #5325/n");
BUG();
}//while
return 0;
}
读出操作完成后步进buf和offset指针,frag_next宏用于返回红黑树中“后继”数据实体的指针。然后开始新的循环。
jffs2_read_dnode函数
该函数读出fd所指数据实体内部[ofs, ofs + len]区域的数据到缓冲区buf中。即使可能只需要读出一个数据实体的一部分,但还是首先读出整个数据实体。而从flash上读取一个数据实体时分两步进行:首先读出jffs2_raw_inode,从而获得csize和dsize信息,然后再分配合适的大小,再读出紧随其后的压缩了的数据,最后再解压缩。最后再将指定区域内的数据复制到buf中。
int jffs2_read_dnode(struct jffs2_sb_info *c, struct jffs2_full_dnode *fd, unsigned char *buf, int ofs, int len)
{
struct jffs2_raw_inode *ri;
size_t readlen;
uint32_t crc;
unsigned char *decomprbuf = NULL;
unsigned char *readbuf = NULL;
int ret = 0;
ri = jffs2_alloc_raw_inode();
if (!ri)
return -ENOMEM;
ret = jffs2_flash_read(c, ref_offset(fd->raw), sizeof(*ri), &readlen, (char *)ri);
if (ret) {
jffs2_free_raw_inode(ri);
printk(KERN_WARNING "Error reading node from 0x%08x: %d/n", ref_offset(fd->raw), ret);
return ret;
}
if (readlen != sizeof(*ri)) {
jffs2_free_raw_inode(ri);
printk(KERN_WARNING "Short read from 0x%08x: wanted 0x%x bytes, got 0x%x/n",
ref_offset(fd->raw), sizeof(*ri), readlen);
return -EIO;
}
crc = crc32(0, ri, sizeof(*ri)-8);
D1(printk(KERN_DEBUG "Node read from %08x: node_crc %08x, calculated CRC %08x. dsize %x, csize
%x, offset %x, buf %p/n", ref_offset(fd->raw), je32_to_cpu(ri->node_crc),
crc, je32_to_cpu(ri->dsize), je32_to_cpu(ri->csize), je32_to_cpu(ri->offset), buf));
if (crc != je32_to_cpu(ri->node_crc)) {
printk(KERN_WARNING "Node CRC %08x != calculated CRC %08x for node at %08x/n",
je32_to_cpu(ri->node_crc), crc, ref_offset(fd->raw));
ret = -EIO;
goto out_ri;
}
/* There was a bug where we wrote hole nodes out with csize/dsize swapped. Deal with it */
if (ri->compr == JFFS2_COMPR_ZERO && !je32_to_cpu(ri->dsize) && je32_to_cpu(ri->csize)) {
ri->dsize = ri->csize;
ri->csize = cpu_to_je32(0);
}
如前所述,读取数据实体时首先要读出其jffs2_raw_inode结构本身的内容,以得到后继数据长度的信息csize和dsize。首先用jffs2_alloc_raw_inode函数分配一个jffs2_raw_inode数据结构,然后由jffs2_flash_read函数填充之。如果实际读出的数据长度小于jffs2_raw_inode数据结构本身的长度、或者发生crc校验错误,则返回错误码EIO。
D1(if(ofs + len > je32_to_cpu(ri->dsize)) {
printk(KERN_WARNING "jffs2_read_dnode() asked for %d bytes at %d from %d-byte node/n",
len, ofs, je32_to_cpu(ri->dsize));
ret = -EINVAL;
goto out_ri;
});
if (ri->compr == JFFS2_COMPR_ZERO) {
memset(buf, 0, len);
goto out_ri;
}
如果compr等于JFFS2_COMPR_ZERO,则表示为一个洞,所以只需直接将读缓冲区中buf偏移开始、长度为len的空间清0即可。处理完洞,下面就根据读出的范围是否涵盖数据实体后继的所有数据、数据是否压缩,分为4种情况进行处理:
/* Cases:
Reading whole node and it's uncompressed - read directly to buffer provided, check CRC.
Reading whole node and it's compressed - read into comprbuf, check CRC and decompress
to buffer provided
Reading partial node and it's uncompressed - read into readbuf, check CRC, and copy
Reading partial node and it's compressed - read into readbuf, check checksum,
decompress to decomprbuf and copy */
if (ri->compr == JFFS2_COMPR_NONE && len == je32_to_cpu(ri->dsize)) {
readbuf = buf;
第一种情况,如果需要读出后继所有数据,并且数据没有经过压缩,则直接将数据读入到接收缓冲区buf种即可(而无需通过解压缩缓冲区中转)。
只要不是读出所有的数据,就需要经过中间的缓冲区中转,即使数据也没有被压缩,所以下面根据数据实体的长度csize分配中间缓冲区:(如果没有压缩,则csize等于dsize)
} else {
readbuf = kmalloc(je32_to_cpu(ri->csize), GFP_KERNEL);
if (!readbuf) {
ret = -ENOMEM;
goto out_ri;
}
}
if (ri->compr != JFFS2_COMPR_NONE) {
if (len < je32_to_cpu(ri->dsize)) { //读出部分压缩数据,需要额外解压缩缓冲区
decomprbuf = kmalloc(je32_to_cpu(ri->dsize), GFP_KERNEL);
if (!decomprbuf) {
ret = -ENOMEM;
goto out_readbuf;
}
} else {
decomprbuf = buf; //读出全部压缩数据,直接将接收缓冲区当作解压缩缓冲区即可
}
} else { //读出部分、未压缩的数据
decomprbuf = readbuf;
}
compr不等于JFFS2_COMPR_NONE,则说明数据被压缩。如果需要读出部分被压缩的数据,那么还需要另一个容纳解压缩了的数据的缓冲区,然后再截取其中的数据到接收缓冲区buf中;如果要读出全部压缩数据,则无需截取操作,所以可以直接将数据解压缩到接收缓冲区buf中即可;如果数据没有被压缩,则首先将全部数据读出到中间缓冲区readbuf中,随后在截取其中相应范围的数据到接收缓冲区buf中。下面通过jffs2_flash_read函数将所有后继数据读到readbuf中,并进行crc校验。
D2(printk(KERN_DEBUG "Read %d bytes to %p/n", je32_to_cpu(ri->csize),
readbuf));
ret = jffs2_flash_read(c, (ref_offset(fd->raw)) + sizeof(*ri), je32_to_cpu(ri->csize), &readlen, readbuf);
if (!ret && readlen != je32_to_cpu(ri->csize))
ret = -EIO;
if (ret)
goto out_decomprbuf;
crc = crc32(0, readbuf, je32_to_cpu(ri->csize));
if (crc != je32_to_cpu(ri->data_crc)) {
printk(KERN_WARNING "Data CRC %08x != calculated CRC %08x for node at %08x/n",
je32_to_cpu(ri->data_crc), crc, ref_offset(fd->raw));
ret = -EIO;
goto out_decomprbuf;
}
数据读出后,如果是压缩了的数据,则进行解压缩到decomprbuf中;如果需要读出的仅是其中的部分数据,那么还要截取这部分数据:
D2(printk(KERN_DEBUG "Data CRC matches calculated CRC %08x/n", crc));
if (ri->compr != JFFS2_COMPR_NONE) {
D2(printk(KERN_DEBUG "Decompress %d bytes from %p to %d bytes at %p/n", ri->csize, readbuf,
je32_to_cpu(ri->dsize), decomprbuf));
ret = jffs2_decompress(ri->compr, readbuf, decomprbuf, je32_to_cpu(ri->csize),
je32_to_cpu(ri->dsize));
if (ret) {
printk(KERN_WARNING "Error: jffs2_decompress returned %d/n", ret);
goto out_decomprbuf;
}
}
if (len < je32_to_cpu(ri->dsize)) {
memcpy(buf, decomprbuf+ofs, len);
}
out_decomprbuf:
if(decomprbuf != buf && decomprbuf != readbuf)
kfree(decomprbuf);
out_readbuf:
if(readbuf != buf)
kfree(readbuf);
out_ri:
jffs2_free_raw_inode(ri);
return ret;
}
第8章 jffs2中符号链接文件的方法表(new)
在jffs2_read_inode函数中创建inode的最后,会根据文件的类型将i_op等指针设置为具体类型文件的方法表。符号链接的方法表为jffs2_symlink_inode_operations,定义于fs/jffs2/symlink.c:
struct inode_operations jffs2_symlink_inode_operations =
{
.readlink = jffs2_readlink,
.follow_link = jffs2_follow_link,
.setattr = jffs2_setattr
};
在path_walk函数中逐层解析路径名时,如果当前路径名分量对应的方法表中的follow_link指针不为空,则通过do_follow_link函数调用符号链接文件的follow_link方法“跳转”到真正被链接的目标文件。
由于索引结点号只在当前文件系统内惟一,所以硬链接的目标只能在同文件系统内。与硬链接不同,符号链接的对象可以在其它文件系统中。这是因为符号链接文件的本质类似正规文件,其数据即为被链接的文件名,那么从根文件系统的根目录出发总是可以到达被链接文件的。
由于符号链接文件的目标可能在另外一个文件系统中,所以可想而知在调用具体文件系统的follow_link方法跟踪符号链接文件时是一定会首先回到高层VFS的框架代码的,然后从那里再通过path_walk进入其它的文件系统。在ext2中符号链接文件的数据,即被链接的文件名保存在ext2_inode的i_block[]数组中(和ext2_inode_info的i_data[]数组中),而jffs2中被链接文件名保存在其惟一的jffs2_raw_inode数据结点后。不同文件系统只需得到被链接文件名,然后就可以借助vfs_follow_link函数来实现各自的follow_link方法了。
有关path_walk和do_follow_link以及vfs_follow_link函数的细节可以参见情景分析。
jffs2_follow_link函数
在do_follow_link函数中调用jffs2_follow_link函数时传递的第一个参数为指向符号链接本身的dentry,第二个参数nd用于保存后继路径名解析的结果。
int jffs2_follow_link(struct dentry *dentry, struct nameidata *nd)
{
unsigned char *buf;
int ret;
buf = jffs2_getlink(JFFS2_SB_INFO(dentry->d_inode->i_sb), JFFS2_INODE_INFO(dentry->d_inode));
if (IS_ERR(buf))
return PTR_ERR(buf);
ret = vfs_follow_link(nd, buf);
kfree(buf);
return ret;
}
如前所述,在jffs2中只需首先获得被链接文件名,然后就可以直接调用vfs_follow_link方法了。被链接文件名在其惟一的jffs2_raw_inode数据实体后,而与之相关的上层jffs2_full_dnode数据结构由符号链接文件的jffs2_inode_info的metadata域直接指向。所以,只需从符号链接文件的inode出发就可以得到其惟一jffs2_raw_inode数据实体的内核描述符了,进而读出被链接文件名。这些操作由jffs2_getlink函数完成。
jffs2_getlink函数
/* Core function to read symlink target. */
char *jffs2_getlink(struct jffs2_sb_info *c, struct jffs2_inode_info *f)
{
char *buf;
int ret;
down(&f->sem);
if (!f->metadata) {
printk(KERN_NOTICE "No metadata for symlink inode #%u/n", f->inocache->ino);
up(&f->sem);
return ERR_PTR(-EINVAL);
}
buf = kmalloc(f->metadata->size+1, GFP_USER);
if (!buf) {
up(&f->sem);
return ERR_PTR(-ENOMEM);
}
buf[f->metadata->size]=0;
符号链接、目录文件和设备文件的惟一的jffs2_raw_inode的上层数据结构jffs2_full_dnode由其inode的u域,即jffs2_full_dnode的metadata域直接指向(参见图1)。如果该域为空则出错。根据jffs2_full_dnode中记录的jffs2_raw_inode的后继数据的大小分配合适的空间,然后就可以通过jffs2_read_dnode函数读出被链接文件名了。注意倒数第二个参数指定待读出数据在相关文件内的逻辑偏移,最后一个参数指明长度:
ret = jffs2_read_dnode(c, f->metadata, buf, 0, f->metadata->size);
up(&f->sem);
if (ret) {
kfree(buf);
return ERR_PTR(ret);
}
return buf;
}
第9章 jffs2中目录文件的方法表(new)
在jffs2_read_inode函数中创建inode的最后,会根据文件的类型将i_op等指针设置为具体类型文件的方法表。目录文件的方法表为jffs2_dir_inode_operations,定义于fs/jffs2/dir.c:
struct inode_operations jffs2_dir_inode_operations =
{
.create = jffs2_create,
.lookup = jffs2_lookup,
.link = jffs2_link,
.unlink = jffs2_unlink,
.symlink = jffs2_symlink,
.mkdir = jffs2_mkdir,
.rmdir = jffs2_rmdir,
.mknod = jffs2_mknod,
.rename = jffs2_rename,
.setattr = jffs2_setattr,
};
这个方法表提供了在一个目录下创建、删除各种类型文件的方法,下面我们分析用于创建正规文件的jffs2_create方法。
jffs2_create函数
在open操作中用户可以通过O_CREATE标志指定当相关文件不存在时创建它。注意open函数只能创建正规文件。此时函数调用关系为:
sys_open > filp_open > open_namei > vfs_create > i_op->create(即jffs2_create函数)
各层函数的细节可参见情景分析。在新建正规文件时需要创建其inode、向父目录文件中写入其目录项,既然是打开文件则还要创建其file、dentry。创建dentry的工作在open_namei函数中调用vfs_create之前,由lookup_hash函数完成。该函数从dentry_cache中分配一个新的dentry数据结构,其d_name.name指向文件名,并用d_parent、d_child加入系统目录树。此时的dentry为“负”的,因为d_inode指针为NULL。然后调用jffs2_lookup函数尝试从父目录的jffs2_inode_info.dents队列中查找指定子文件的jffs2_full_dirent结构(此时当然失败)。
创建inode、向其父目录文件写入相应目录项的工作是由vfs_create函数完成的,另外就jffs2而言创建文件时还必须创建相应的内核描述符,并初始化好“代表文件存在”的第一个jffs2_raw_inode数据实体(该数据实体为“metadata”,仅表文件的存在,由jffs2_inode_info的metadata直接指向,在第一次写操作时会被设置为过时的,参见正规文件的写操作jffs2_write_inode_range函数的相关部分)。vfs_create函数在持有父目录的i_zombie信号量的情况下调用父目录文件的create方法,即jffs2_create函数完成所有具体操作。其第一个参数指向父目录inode,第二个参数指向已经为新文件创建的dentry(注意文件名已设置好,并已加入文件系统目录树),第三个参数为用户指定的文件访问权限。
static int jffs2_create(struct inode *dir_i, struct dentry *dentry, int mode)
{
struct jffs2_raw_inode *ri;
struct jffs2_inode_info *f, *dir_f;
struct jffs2_sb_info *c;
struct inode *inode;
int ret;
ri = jffs2_alloc_raw_inode();
if (!ri)
return -ENOMEM;
c = JFFS2_SB_INFO(dir_i->i_sb);
D1(printk(KERN_DEBUG "jffs2_create()/n"));
inode = jffs2_new_inode(dir_i, mode, ri);
if (IS_ERR(inode)) {
D1(printk(KERN_DEBUG "jffs2_new_inode() failed/n"));
jffs2_free_raw_inode(ri);
return PTR_ERR(inode);
}
首先通过jffs2_alloc_raw_inode函数从raw_inode_slab高速缓存中分配一个空白的jffs2_raw_inode数据实体,然后调用jffs2_new_inode函数完成如下工作:
1, 分配、初始化文件的内核描述符jffs2_inode_cache数据结构;
2, 设置jffs2_raw_inode数据实体;
3, 分配、初始化inode;
4, 建立jffs2_inode_cache和inode的联系,注册inode。
文件的内核描述符实现了文件及其数据实体之间的映射机制,所以早在挂载jffs2时就已经为设备上的所有文件创建了内核描述符,在新建文件时也必须首先建立其内核描述符。详见下文。
inode->i_op = &jffs2_file_inode_operations;
inode->i_fop = &jffs2_file_operations;
inode->i_mapping->a_ops = &jffs2_file_address_operations;
inode->i_mapping->nrpages = 0;
接下来就要设置文件的方法表指针了。由于这里创建的是正规文件,所以指针都指向正规文件的相关方法表(这里与jffs2_read_inode函数的相关代码相同)。
至此,新建正规文件时剩余的工作就是向其父目录中写入相应的目录项jffs2_raw_dirent数据实体、向flash写入“代表其存在”的第一个jffs2_raw_inode数据实体,以及创建上层的jffs2_full_dirent和jffs2_full_dnode。这些工作都由jffs2_do_create函数完成,参见下文。
f = JFFS2_INODE_INFO(inode);
dir_f = JFFS2_INODE_INFO(dir_i);
ret = jffs2_do_create(c, dir_f, f, ri, dentry->d_name.name, dentry->d_name.len);
if (ret) {
jffs2_clear_inode(inode);
make_bad_inode(inode);
iput(inode);
jffs2_free_raw_inode(ri);
return ret;
}
最后,更新父目录inode的相应时间戳为数据实体中的创建时间。最后释放数据实体,并调用d_instantiate函数用d_alias和d_inode域建立dentry和inode之间的联系。
dir_i->i_mtime = dir_i->i_ctime = je32_to_cpu(ri->ctime);
jffs2_free_raw_inode(ri);
d_instantiate(dentry, inode);
D1(printk(KERN_DEBUG "jffs2_create: Created ino #%lu with mode %o, nlink %d(%d). nrpages %ld/n",
inode->i_ino, inode->i_mode, inode->i_nlink, f->inocache->nlink, inode->i_mapping->nrpages));
return 0;
}
jffs2_new_inode函数
该函数创建、初始化新文件的内核描述符jffs2_inode_cache,初始化文件的jffs2_raw_inode数据实体,并创建、设置inode,最后建立jffs2_inode_cache和inode的联系,并将inode注册到内核相关数据结构中。
/* jffs2_new_inode: allocate a new inode and inocache, add it to the hash,
fill in the raw_inode while you're at it. */
struct inode *jffs2_new_inode (struct inode *dir_i, int mode, struct jffs2_raw_inode *ri)
{
struct inode *inode;
struct super_block *sb = dir_i->i_sb;
struct jffs2_sb_info *c;
struct jffs2_inode_info *f;
int ret;
D1(printk(KERN_DEBUG "jffs2_new_inode(): dir_i %ld, mode 0x%x/n", dir_i->i_ino, mode));
c = JFFS2_SB_INFO(sb);
inode = new_inode(sb);
if (!inode)
return ERR_PTR(-ENOMEM);
首先通过定义于linux/fs.h中的内联函数new_inode分配一个空白的inode,这个操作又是通过get_empty_inode函数完成的,它将空白的inode加入内核的inode_in_use队列:
static inline struct inode * new_inode(struct super_block *sb)
{
struct inode *inode = get_empty_inode();
if (inode) {
inode->i_sb = sb;
inode->i_dev = sb->s_dev;
inode->i_blkbits = sb->s_blocksize_bits;
}
return inode;
}
f = JFFS2_INODE_INFO(inode);
jffs2_init_inode_info(f);
memset(ri, 0, sizeof(*ri));
jffs2中将inode的u域解释为jffs2_inode_info数据结构,然后其和由参数传递的jffs2_raw_inode清空。
/* Set OS-specific defaults for new inodes */
ri->uid = cpu_to_je16(current->fsuid);
if (dir_i->i_mode & S_ISGID) {
ri->gid = cpu_to_je16(dir_i->i_gid);
if (S_ISDIR(mode))
mode |= S_ISGID;
} else {
ri->gid = cpu_to_je16(current->fsgid);
}
设置新文件的uid为当前进程实际使用的uid,即fsuid。如果父目录的S_ISGID标志有效,则新文件继承父目录的gid,否则设置为当前进程实际使用的gid,即fsgid。
ri->mode = cpu_to_je32(mode);
ret = jffs2_do_new_inode (c, f, mode, ri);
if (ret) {
make_bad_inode(inode);
iput(inode);
return ERR_PTR(ret);
}
文件的内核描述符jffs2_inode_cache建立了文件及其数据之间的索引机制,在向新建文件写入任何数据实体前必须首先创建好其内核描述符。这个工作通过jffs2_do_new_inode函数完成,包括设置索引结点号、初始化数据实体描述符的nodes链表,建立文件描述符与文件inode的联系并加入超级块的inocache_list[]哈希表,并进一步设置jffs2_raw_inode中的相应域,参见下文。
inode->i_nlink = 1;
inode->i_ino = je32_to_cpu(ri->ino);
inode->i_mode = je32_to_cpu(ri->mode);
inode->i_gid = je16_to_cpu(ri->gid);
inode->i_uid = je16_to_cpu(ri->uid);
inode->i_atime = inode->i_ctime = inode->i_mtime = CURRENT_TIME;
ri->atime = ri->mtime = ri->ctime = cpu_to_je32(inode->i_mtime);
inode->i_blksize = PAGE_SIZE;
inode->i_blocks = 0;
inode->i_size = 0;
insert_inode_hash(inode);
return inode;
}
设置好新建文件的jffs2_raw_inode数据实体后,就可以根据其中的信息来设置inode的相应域了,最后通过insert_inode_hash函数利用inode.i_hash域将其加入索引节点哈希表inode_hashtable的某个队列中。
jffs2_do_create函数
在创建正规文件时需要向其父目录写入相应目录项jffs2_raw_dirent数据实体,同时写入“代表其存在”的第一个jffs2_raw_inode数据结构,创建其内核描述符jffs2_raw_node_ref以及上层的jffs2_full_dirent和jffs2_full_dnode数据结构。注意此时写入的第一个jffs2_raw_inode数据结构没有携带任何有效数据,其jffs2_full_dnode也被jffs2_inode_info的metadata域直接指向而没有通过jffs2_node_frag组织到fragtree红黑树中。详见下文。
该函数的第2、3个参数指向父目录文件、其自身的inode,第4个参数指向在jffs2_create函数中已经设置好的jffs2_raw_inode数据实体,最后两个参数描述新文件名字符串。
int jffs2_do_create(struct jffs2_sb_info *c, struct jffs2_inode_info *dir_f, struct jffs2_inode_info *f,
struct jffs2_raw_inode *ri, const char *name, int namelen)
{
struct jffs2_raw_dirent *rd;
struct jffs2_full_dnode *fn;
struct jffs2_full_dirent *fd;
uint32_t alloclen, phys_ofs;
uint32_t writtenlen;
int ret;
/* Try to reserve enough space for both node and dirent. Just the node will do for now, though */
ret = jffs2_reserve_space(c, sizeof(*ri), &phys_ofs, &alloclen, ALLOC_NORMAL);
D1(printk(KERN_DEBUG "jffs2_do_create(): reserved 0x%x bytes/n", alloclen));
if (ret) {
up(&f->sem);
return ret;
}
首先通过jffs2_reserve_space函数在flash上分配一个合适大小的空间,其物理地址由phys_ofs参数返回,长度由alloclen参数返回。然后就可以通过jffs2_write_dnode函数向flash写入“代表文件存在”的jffs2_raw_inode数据实体,并创建相应内核描述符jffs2_raw_node_ref和上层jffs2_full_dnode数据结构了。
ri->data_crc = cpu_to_je32(0);
ri->node_crc = cpu_to_je32(crc32(0, ri, sizeof(*ri)-8));
fn = jffs2_write_dnode(c, f, ri, NULL, 0, phys_ofs, &writtenlen);
D1(printk(KERN_DEBUG "jffs2_do_create created file with mode 0x%x/n",
je32_to_cpu(ri->mode)));
if (IS_ERR(fn)) {
D1(printk(KERN_DEBUG "jffs2_write_dnode() failed/n"));
/* Eeek. Wave bye bye */
up(&f->sem);
jffs2_complete_reservation(c);
return PTR_ERR(fn);
}
/* No data here. Only a metadata node, which will be obsoleted by the first data write */
f->metadata = fn;
注意调用jffs2_write_dnode函数时传递的倒数第3个参数为0,因为该数据实体并没有携带任何数据。另外其上层jffs2_full_dnode数据结构由jffs2_inode_info的metadata域直接指向,并且在第一次真正写入文件时被标记为过时的。可参见正规文件的写操作jffs2_write_inode_rage函数的相关部分,以及jffs2map2观察到的结果。
/* Work out where to put the dirent node now. */
writtenlen = PAD(writtenlen);
phys_ofs += writtenlen;
alloclen -= writtenlen;
up(&f->sem);
写完了“代表文件存在”的jffs2_raw_inode数据实体后,接下来就要向父目录文件写入其目录项了。由此可见目录项jffs2_raw_dirent数据实体是“紧接着”刚才的jffs2_raw_inode数据实体写入的。如果先前从flash上分配的空间不足,则再次分配连续的空间:
if (alloclen < sizeof(*rd)+namelen) {
/* Not enough space left in this chunk. Get some more */
jffs2_complete_reservation(c);
ret = jffs2_reserve_space(c, sizeof(*rd)+namelen, &phys_ofs, &alloclen, ALLOC_NORMAL);
if (ret) {
/* Eep. */
D1(printk(KERN_DEBUG "jffs2_reserve_space() for dirent failed/n"));
return ret;
}
}
接下来,通过jffs2_alloc_raw_dirent函数从raw_dirent_slab高速缓存中分配一个空白的jffs2_raw_dirent,并初始化:
rd = jffs2_alloc_raw_dirent();
if (!rd) {
/* Argh. Now we treat it like a normal delete */
jffs2_complete_reservation(c);
return -ENOMEM;
}
down(&dir_f->sem);
rd->magic = cpu_to_je16(JFFS2_MAGIC_BITMASK);
rd->nodetype = cpu_to_je16(JFFS2_NODETYPE_DIRENT);
rd->totlen = cpu_to_je32(sizeof(*rd) + namelen);
rd->hdr_crc = cpu_to_je32(crc32(0, rd, sizeof(struct jffs2_unknown_node)-4));
注意jffs2_raw_dirent数据结点的类型当然为JFFS2_NODETYPE_DIRENT,紧随其后的为新建文件的文件名。
rd->pino = cpu_to_je32(dir_f->inocache->ino);
rd->version = cpu_to_je32(++dir_f->highest_version);
rd->ino = ri->ino;
rd->mctime = ri->ctime;
rd->nsize = namelen;
rd->type = DT_REG;
rd->node_crc = cpu_to_je32(crc32(0, rd, sizeof(*rd)-8));
rd->name_crc = cpu_to_je32(crc32(0, name, namelen));
参数dir_f指向父目录文件的jffs2_inode_info。由于jffs2中组成目录文件的每个目录项不连续,缺乏类似ext2中通过目录文件的索引结点就可以索引到所有目录项的机制,所以每个目录项除记录自己所代表的文件的索引结点号外,还得用额外的pino域记录自己所属的目录文件的索引节点号。另外type域表明目录项所对应的文件的类型,这里为正规文件类型DT_REG。
fd = jffs2_write_dirent(c, dir_f, rd, name, namelen, phys_ofs, &writtenlen);
jffs2_free_raw_dirent(rd);
if (IS_ERR(fd)) {
/* dirent failed to write. Delete the inode normally
as if it were the final unlink() */
jffs2_complete_reservation(c);
up(&dir_f->sem);
return PTR_ERR(fd);
}
/* Link the fd into the inode's list, obsoleting an old one if necessary. */
jffs2_add_fd_to_list(c, fd, &dir_f->dents);
jffs2_complete_reservation(c);
up(&dir_f->sem);
return 0;
}
设置好目录项jffs2_raw_dirent数据实体后,就可以通过jffs2_write_dirent函数将其写入flash了,同时创建相应的内核描述符jffs2_raw_node_ref及上层的jffs2_full_dnode数据结构,最后通过jffs2_add_fd_to_list函数将其加入父目录文件jffs2_inode_info的dents队列。jffs2_write_dirent函数与jffs2_write_dnode函数很类似,请读者自行阅读。
jffs2_do_new_inode函数
该函数分配、初始化文件的内核描述符jffs2_inode_cache(包括设置索引结点号、初始化数据实体描述符链表),建立其与文件inode的联系并加入超级块的inocache_list[]哈希表,并进一步设置jffs2_raw_inode的相应域。
int jffs2_do_new_inode(struct jffs2_sb_info *c, struct jffs2_inode_info *f,
uint32_t mode, struct jffs2_raw_inode *ri)
{
struct jffs2_inode_cache *ic;
ic = jffs2_alloc_inode_cache();
if (!ic) {
return -ENOMEM;
}
memset(ic, 0, sizeof(*ic));
首先通过jffs2_alloc_inode_cache函数从inode_cache_slab高速缓存中分配一个文件的内核描述符jffs2_inode_cache数据结构,并清空。
init_MUTEX_LOCKED(&f->sem);
f->inocache = ic;
f->inocache->nlink = 1;
f->inocache->nodes = (struct jffs2_raw_node_ref *)f->inocache;
f->inocache->ino = ++c->highest_ino;
f->inocache->state = INO_STATE_PRESENT;
然后将文件inode的u域,即jffs2_inode_info的inocache指向其内核描述符,并完成文件内核描述符的初始化:硬链接计数初始值为1,索引节点号等于文件系统超级块的u域即jffs2_sb_info的hisgest_ino,文件的状态为PRESENT。文件内核描述符的重要作用就是建立文件及其数据实体之间的索引,由于新增文件此时尚未写入任何jffs2_raw_inode数据实体,所以描述符的nodes域指向其自身。
在ext2中文件数据所在的磁盘块由其ext2_inode进行索引,而ext2_inode在块组的索引节点表里,因此索引结点号就由其所处物理位置决定。为了提高访问效率,一般从其父目录所在块组中分配新建文件的ext2_inode。如果新建目录文件,则还要考虑均衡每个块组内所含目录的数目。详情可以参考情景分析上册P568-P571。
但是对jffs2而言,在flash中并不存在“索引”文件数据实体的“索引节点”,而且flash上也没有块组的概念,所以索引节点号的分配策略就简单得多:由于索引节点号在这个文件系统内惟一,所以就用jffs2_sb_info的highest_ino来保存下一个新建文件的索引节点号,并逐一递增。
ri->ino = cpu_to_je32(f->inocache->ino);
D1(printk(KERN_DEBUG "jffs2_do_new_inode(): Assigned ino# %d/n", f->inocache->ino));
jffs2_add_ino_cache(c, f->inocache);
ri->magic = cpu_to_je16(JFFS2_MAGIC_BITMASK);
ri->nodetype = cpu_to_je16(JFFS2_NODETYPE_INODE);
ri->totlen = cpu_to_je32(PAD(sizeof(*ri)));
ri->hdr_crc = cpu_to_je32(crc32(0, ri, sizeof(struct jffs2_unknown_node)-4));
ri->mode = cpu_to_je32(mode);
f->highest_version = 1;
ri->version = cpu_to_je32(f->highest_version);
return 0;
}
最后,还要通过jffs2_add_ino_cache函数将该文件描述符加入jffs2_sb_info.inocache_list[]哈希表,并继续设置jffs2_raw_inode中的相应域,注意设置jffs2_raw_inode数据实体的类型为JFFS2_NODETYPE_INODE。
第10章 jffs2的Garbage Collection
在挂载jffs2文件系统时、在jffs2_do_fill_super函数的最后创建并启动GC内核线程,相关代码如下:
if (!(sb->s_flags & MS_RDONLY))
jffs2_start_garbage_collect_thread(c);
return 0;
如果jffs2文件系统不是以只读方式挂载的,就会有新的数据实体写入flash。而且jffs2文件系统的特点是在写入新的数据实体时并不修改flash上原有数据实体,而只是将其内核描述符标记为“过时”。系统运行一段时间后若空白flash擦除块的数量小于一定阈值,则GC被唤醒用于释放所有过时的数据实体。
为了尽量均衡地使用flash分区上的所有擦除块,在选择有效数据实体的副本所写入的擦除块时需要考虑Wear Levelling算法。
jffs2_start_garbage_collect_thread函数
该函数用于创建GC内核线程。
/* This must only ever be called when no GC thread is currently running */
int jffs2_start_garbage_collect_thread(struct jffs2_sb_info *c)
{
pid_t pid;
int ret = 0;
if (c->gc_task)
BUG();
init_MUTEX_LOCKED(&c->gc_thread_start);
init_completion(&c->gc_thread_exit);
pid = kernel_thread(jffs2_garbage_collect_thread, c, CLONE_FS|CLONE_FILES);
if (pid < 0) {
printk(KERN_WARNING "fork failed for JFFS2 garbage collect thread: %d/n", -pid);
complete(&c->gc_thread_exit);
ret = pid;
} else {
/* Wait for it... */
D1(printk(KERN_DEBUG "JFFS2: Garbage collect thread is pid %d/n", pid));
down(&c->gc_thread_start);
}
return ret;
}
信号量gc_thread_start用于保证在当前执行流在创建了GC内核线程后、在返回到当前执行流时GC内核线程已经运行了。kernel_thread函数创建GC内核线程,此时GC内核线程与当前执行流谁获得cpu还不一定,于是当前执行流在获得gc_thread_start信号量时会阻塞(先前通过init_MUTEX_LOCKED宏已将信号量初始化为“不可用”状态),直到GC内核线程第一次获得运行后释放该信号量,参见下文。
传递给kernel_thread函数的第一个参数为新的内核线程所执行的代码,所以GC内核线程的行为由函数jffs2_garbage_collect_thread确定:
jffs2_garbage_collect_thread函数
static int jffs2_garbage_collect_thread(void *_c)
{
struct jffs2_sb_info *c = _c;
daemonize();
c->gc_task = current;
up(&c->gc_thread_start);
sprintf(current->comm, "jffs2_gcd_mtd%d", c->mtd->index);
既然是运行于后台的内核线程,首先就得通过daemonize函数释放从其父进程获得的一些资源,比如关闭已经打开的控制台设备文件描述符、释放所有的属于用户空间的页面(如果存在的话)、并改换门庭投靠到init门下等等,其详细分析可参见情景分析。
用jffs2_sb_info.gc_task来指向GC内核线程的PCB,这样就不用为它创建额外的等待队列了:当GC内核线程无事可作时不用加入等待队列,只需从运行队列中删除即可;而在需要唤醒GC内核线程时可以通过gc_task指向通过send_sig函数向它发送SIGHUP信号。(类似的还有在schedule_timeout函数中也是用定时器数据结构timer_list的data域指向当前进程的PCB,而没有使用额外的等到队列,当定时器到时时通过wake_up_process直接唤醒data所指向的进程即可)
释放gc_thread_start信号量,这将唤醒先前创建GC内核线程的执行流。然后给GC内核线程起个名字“jffs2_gcd_mtd#”,其中最后的数字为jffs2文件系统所在的flash分区的编号(从0开始),比如在我的系统中这个编号为5(整个flash上有6个分区:uboot映象、uboot参数、分区表、内核映象、work分区、root分区)。
通过set_user_nice函数设置GC内核线程的静态优先级为10后,就进入它的主体循环了:
set_user_nice(current, 10);
for (;;) {
spin_lock_irq(¤t->sigmask_lock);
siginitsetinv (¤t->blocked, sigmask(SIGHUP) | sigmask(SIGKILL) | sigmask(SIGSTOP) |
sigmask(SIGCONT));
recalc_sigpending();
spin_unlock_irq(¤t->sigmask_lock);
信号的编号与体系结构相关,定义于asm/signal.h。宏sigmask返回信号的位索引:
#define sigmask(sig) (1UL << ((sig) - 1))
这是因为在Linux上没有编号为0的信号。
GC内核线程的状态由SIGHUP、SIGKILL、SIGSTOP和SIGCONT四种信号控制,所以每次循环的开始都要做相应的准备工作,以便在进入TASK_INTERRUPTIBLE状态后接收:由内联函数siginitsetinv清除其信号屏蔽字blocked中这四种信号的相应位,使得GC内核线程只接收这些信号。recalc_sigpending函数检查当前进程是否有非阻塞的挂起信号,如果有,则设置PCB中的sigpending标志。这个标志由下面的signal_pending函数检查。
(比较:用户进程的信号处理函数在用户态执行,用户进程不必关心接收信号的细节,只注册信号处理函数。而内核线程运行于内核态,要由内核线程自己来完成信号的接收工作。)
if (!thread_should_wake(c)) {
set_current_state (TASK_INTERRUPTIBLE);
D1(printk(KERN_DEBUG "jffs2_garbage_collect_thread sleeping.../n"));
/* Yes, there's a race here; we checked thread_should_wake() before
setting current->state to TASK_INTERRUPTIBLE. But it doesn't
matter - We don't care if we miss a wakeup, because the GC thread
is only an optimisation anyway. */
schedule();
}
每次循环的开始都要判断GC内核线程是否真的需要运行。若不需要,那么主动调用调度程序。由于jffs2_sb_info.gc_task保存了GC内核线程PCB的地址,所以就不需要创建一个额外的等待队列并将其加入其中了,只需将其状态改为TASK_INTERRUPTIBLE即可,而在调度程序中会把它从运行队列中删除。
需要说明的是这样进入睡眠的方法存在竞争条件。要想无竞争地进入睡眠就必须在判断条件是否为真前改变进程的状态,这样在判断条件之后、进入调度程序之前,用于唤醒GC内核线程的中断执行流能够撤销进入睡眠。但是根据作者的注释即使发生竞争条件也无大碍。
GC内核线程无事可作时进入TASK_INTERRUPTIBLE状态,收到SIGHUP、SIGKILL、SIGSTOP和SIGCONT中任何一种信号时恢复到TASK_RUNNING状态。那么每次恢复运行后都得首先处理所有非阻塞挂起信号,判断唤醒的原因。dequeue_signal取出非阻塞挂起信号中编号最小的那一个:
cond_resched();
/* Put_super will send a SIGKILL and then wait on the sem. */
while (signal_pending(current)) {
siginfo_t info;
unsigned long signr;
spin_lock_irq(¤t->sigmask_lock);
signr = dequeue_signal(¤t->blocked, &info);
spin_unlock_irq(¤t->sigmask_lock);
switch(signr) {
case SIGSTOP:
D1(printk(KERN_DEBUG "jffs2_garbage_collect_thread(): SIGSTOP received./n"));
set_current_state(TASK_STOPPED);
schedule();
break;
如果因收到SIGSTOP信号而唤醒,那么GC内核线程应该进入TASK_STOPPED状态并立刻引发调度。注意当再次恢复执行时,通过break语句直接跳出信号处理while循环。
case SIGKILL:
D1(printk(KERN_DEBUG "jffs2_garbage_collect_thread(): SIGKILL received./n"));
spin_lock_bh(&c->erase_completion_lock);
c->gc_task = NULL;
spin_unlock_bh(&c->erase_completion_lock);
complete_and_exit(&c->gc_thread_exit, 0);
如果因收到SIGKILL信号而唤醒,那么GC内核线程应该立即结束。当需要结束GC内核线程时(比如卸载jffs2文件系统时、或者重新按照只读方式挂载时),当前进程通过jffs2_stop_garbage_collect_thread函数给它发送SIGKILL信号,然后阻塞在gc_thread_exit.wait等待队列上;而由GC内核线程在处理SIGKILL信号时再将这个进程唤醒并完成退出。
case SIGHUP:
D1(printk(KERN_DEBUG "jffs2_garbage_collect_thread(): SIGHUP received./n"));
break;
如果因收到SIGHUP信号而唤醒,那么说明GC内核线程有事可作了,所以通过break语句直接跳出信号处理while循环。在jffs2_garbage_collect_trigger函数中向它发送SIGHUP信号。
default:
D1(printk(KERN_DEBUG "jffs2_garbage_collect_thread(): signal %ld received/n", signr));
}
} //while
最后的default分支用于处理SIGCONT信号。此时没有直接跳出while循环,而是接着处理剩余的非阻塞挂起信号(如果存在的话),直到所有的信号都处理完才结束while循环。
(需要说明的是,虽然代码中允许GC内核线程接收四种信号,但现有的代码中只使用到了SIGKILL和SIGHUP两种信号。)
/* We don't want SIGHUP to interrupt us. STOP and KILL are OK though. */
spin_lock_irq(¤t->sigmask_lock);
siginitsetinv (¤t->blocked, sigmask(SIGKILL) | sigmask(SIGSTOP) | sigmask(SIGCONT));
recalc_sigpending();
spin_unlock_irq(¤t->sigmask_lock);
D1(printk(KERN_DEBUG "jffs2_garbage_collect_thread(): pass/n"));
jffs2_garbage_collect_pass(c);
} //for
}
最后,如果GC内核线程恢复执行的原因不是希望其退出的话,就说明它有事可做了。GC操作由jffs2_garbage_cllect_pass函数完成。另外根据作者的注释,在执行GC操作期间不希望收到SIGHUP信号,这是为什么?另外在GC操作后如何进入TASK_INTERRUPIBLE状态?
jffs2_garbage_collect_pass函数
在一个擦除块中既有有效数据,又有过时数据。那么GC的思路就是:挑选一个擦除块,每次GC只针对一个有效的数据实体:将它的副本到另外一个擦除块中,从而使得原来的数据实体变成过时的。直到某次GC后使得整个擦除块只含有过时的数据实体,就可以启动擦除操作了。
/* jffs2_garbage_collect_pass
Make a single attempt to progress GC. Move one node, and possibly start erasing one eraseblock.
*/
int jffs2_garbage_collect_pass(struct jffs2_sb_info *c)
{
struct jffs2_eraseblock *jeb;
struct jffs2_inode_info *f;
struct jffs2_raw_node_ref *raw;
struct jffs2_node_frag *frag;
struct jffs2_full_dnode *fn = NULL;
struct jffs2_full_dirent *fd;
uint32_t start = 0, end = 0, nrfrags = 0;
uint32_t inum;
struct inode *inode;
int ret = 0;
if (down_interruptible(&c->alloc_sem))
return -EINTR;
spin_lock_bh(&c->erase_completion_lock);
在GC期间要持有信号量alloc_sem。如果unchecked_size不为0,则不能开始GC操作。为什么?
while (c->unchecked_size) {
/* We can't start doing GC yet. We haven't finished checking
the node CRCs etc. Do it now and wait for it. */
struct jffs2_inode_cache *ic;
if (c->checked_ino > c->highest_ino) {
printk(KERN_CRIT "Checked all inodes but still 0x%x bytes of unchecked space?/n",
c->unchecked_size);
D1(jffs2_dump_block_lists(c));
BUG();
}
ic = jffs2_get_ino_cache(c, c->checked_ino++);
if (!ic)
continue;
if (!ic->nlink) {
D1(printk(KERN_DEBUG "Skipping check of ino #%d with nlink zero/n", ic->ino));
continue;
}
if (ic->state != INO_STATE_UNCHECKED) {
D1(printk(KERN_DEBUG "Skipping check of ino #%d already in state %d/n", ic->ino, ic->state));
continue;
}
spin_unlock_bh(&c->erase_completion_lock);
D1(printk(KERN_DEBUG "jffs2_garbage_collect_pass() triggering inode scan of ino#%d/n", ic->ino));
{
/* XXX: This wants doing more sensibly -- split the core of jffs2_do_read_inode up */
struct inode *i = iget(OFNI_BS_2SFFJ(c), ic->ino);
if (is_bad_inode(i)) {
printk(KERN_NOTICE "Eep. read_inode() failed for ino #%u/n", ic->ino);
ret = -EIO;
}
iput(i);
}
up(&c->alloc_sem);
return ret;
}
/* First, work out which block we're garbage-collecting */
jeb = c->gcblock;
if (!jeb)
jeb = jffs2_find_gc_block(c);
if (!jeb) {
printk(KERN_NOTICE "jffs2: Couldn't find erase block to garbage collect!/n");
spin_unlock_bh(&c->erase_completion_lock);
up(&c->alloc_sem);
return -EIO;
}
jffs2_sb_info的gcblock指向当前应该被GC的擦除块。如果它为空,则由jffs2_find_gc_block函数从含有过时数据实体的擦除块中选择一个(该函数涉及Wear Levelling算法,有待深究)。(在第一次运行GC前、或完成一个擦除块的GC操作后gcblock为NULL,还有其它情况么?)
D1(printk(KERN_DEBUG "GC from block %08x, used_size %08x, dirty_size %08x, free_size %08x/n",
jeb->offset, jeb->used_size, jeb->dirty_size, jeb->free_size));
D1(if (c->nextblock)
printk(KERN_DEBUG "Nextblock at %08x, used_size %08x, dirty_size %08x, wasted_size %08x,
free_size %08x/n", c->nextblock->offset, c->nextblock->used_size, c->nextblock->dirty_size,
c->nextblock->wasted_size, c->nextblock->free_size)
);
if (!jeb->used_size) {
up(&c->alloc_sem);
goto eraseit;
}
used_size为擦除块内所有有效数据实体所占的空间。如果它为0,则说明整个擦除块都过时了,因此可以直接跳到eraseit处将其描述符加入erase_pending_list链表。
一个flash擦除块内所有数据实体的内核描述符由next_phys域组织成一个链表,其首尾元素分别由擦除块描述符jffs2_eraseblock的first_node和last_node域指向,而gc_node指向当前被GC的数据实体的描述符。
raw = jeb->gc_node;
while(ref_obsolete(raw)) {
D1(printk(KERN_DEBUG "Node at 0x%08x is obsolete... skipping/n", ref_offset(raw)));
jeb->gc_node = raw = raw->next_phys;
if (!raw) {
printk(KERN_WARNING "eep. End of raw list while still supposedly nodes to GC/n");
printk(KERN_WARNING "erase block at 0x%08x. free_size 0x%08x, dirty_size 0x%08x,
used_size 0x%08x/n", jeb->offset, jeb->free_size, jeb->dirty_size, jeb->used_size);
spin_unlock_bh(&c->erase_completion_lock);
up(&c->alloc_sem);
BUG();
}
}
当重新选择一个新的擦除块进行GC时,gc_node指向该擦除块内第一个数据实体的描述符(见jffs2_find_gc_block函数)。如果数据实体过时,那么一直步进到第一个不过时的数据实体。这是因为GC操作的对象是有效数据实体(就是要将该擦除块内有效的数据实体写一个副本到另外的擦除块中,从而使有效的数据实体变成过时的,最终使整个擦除块上的数据实体都过时,然后就可以擦除整个擦除块了)。
D1(printk(KERN_DEBUG "Going to garbage collect node at 0x%08x/n", ref_offset(raw)));
if (!raw->next_in_ino) {
/* Inode-less node. Clean marker, snapshot or something like that */
/* FIXME: If it's something that needs to be copied, including something
we don't grok that has JFFS2_NODETYPE_RWCOMPAT_COPY, we should do so */
spin_unlock_bh(&c->erase_completion_lock);
jffs2_mark_node_obsolete(c, raw);
up(&c->alloc_sem);
goto eraseit_lock;
}
任何文件的数据实体的内核描述符的next_in_ino用于组织循环链表。所以如果它为空,则说明该数据实体不从属于任何文件。根据注释可能为cleanmarker或者snapshot,此时标记描述符为过时即可。
inum = jffs2_raw_ref_to_inum(raw);
D1(printk(KERN_DEBUG "Inode number is #%u/n", inum));
spin_unlock_bh(&c->erase_completion_lock);
D1(printk(KERN_DEBUG "jffs2_garbage_collect_pass collecting from block @0x%08x. Node @0x%08x,
ino #%u/n", jeb->offset, ref_offset(raw), inum));
inode = iget(OFNI_BS_2SFFJ(c), inum);
然后通过jffs2_raw_ref_to_inum函数返回数据实体所在文件的索引节点号,并由iget函数返回文件的索引结点指针(同时增加其引用计数)。(这岂不是GC只操作有inode的文件了?!而inode只有在打开文件期间才存在啊?!)
if (is_bad_inode(inode)) {
printk(KERN_NOTICE "Eep. read_inode() failed for ino #%u/n", inum);
/* NB. This will happen again. We need to do something appropriate here. */
up(&c->alloc_sem);
iput(inode);
return -EIO;
}
先前在打开文件、创建inode对象时(在jffs2_read_inode函数中),如果读取flash上的数据实体失败,则通过make_bad_inode函数标记该inode为bad,其代码如下:
ret = jffs2_do_read_inode(c, f, inode->i_ino, &latest_node);
if (ret) {
make_bad_inode(inode);
up(&f->sem);
return;
}
读取数据实体失败是由于访问介质错误引起的,所以在GC操作中一旦发现这种情况就直接以EIO退出了。
f = JFFS2_INODE_INFO(inode);
down(&f->sem);
/* Now we have the lock for this inode. Check that it's still the one at the head of the list. */
在GC操作期间必须持有jffs2_inode_info.sem信号量,它用于同步写操作和GC操作。参见上文。
if (ref_obsolete(raw)) {
D1(printk(KERN_DEBUG "node to be GC'd was obsoleted in the meantime./n"));
/* They'll call again */
goto upnout;
}
在真正开始GC操作前还要再次检查该数据实体是否变成过时的了,这是因为前面获得jffs2_inode_info.sem信号量时可能阻塞,所以需要再次检查。
(下面的代码有待进一步分析,针对数据实体的类型通过相应的jffs2_garbage_collect_xxxx函数完成GC操作。等到把一个擦除块中所有有效的数据实体移走之后,即不含有任何有效数据实体(擦除块描述符中used_size域等于0)就可以擦除这个擦除块了:将其描述符加入erase_pending_list链表并激活擦除过程,并将gcblock设置为NULL)。
/* OK. Looks safe. And nobody can get us now because we have the semaphore. Move the block */
if (f->metadata && f->metadata->raw == raw) {
fn = f->metadata;
ret = jffs2_garbage_collect_metadata(c, jeb, f, fn);
goto upnout;
}
/* FIXME. Read node and do lookup? */
for (frag = frag_first(&f->fragtree); frag; frag = frag_next(frag)) {
if (frag->node && frag->node->raw == raw) {
fn = frag->node;
end = frag->ofs + frag->size;
#if 1 /* Temporary debugging sanity checks, till we're ready to _trust_ the REF_PRISTINE flag stuff */
if (!nrfrags && ref_flags(fn->raw) == REF_PRISTINE) {
if (fn->frags > 1)
printk(KERN_WARNING "REF_PRISTINE node at 0x%08x had %d frags. Tell
dwmw2/n", ref_offset(raw), fn->frags);
if (frag->ofs & (PAGE_CACHE_SIZE-1) && frag_prev(frag) && frag_prev(frag)->node)
printk(KERN_WARNING "REF_PRISTINE node at 0x%08x had a previous non-hole
frag in the same page. Tell dwmw2/n", ref_offset(raw));
if ((frag->ofs+frag->size) & (PAGE_CACHE_SIZE-1) && frag_next(frag) &&
frag_next(frag)->node)
printk(KERN_WARNING "REF_PRISTINE node at 0x%08x (%08x-%08x) had a
following non-hole frag in the same page. Tell dwmw2/n",
ref_offset(raw), frag->ofs, frag->ofs+frag->size);
}
#endif
if (!nrfrags++)
start = frag->ofs;
if (nrfrags == frag->node->frags)
break; /* We've found them all */
}
} // for
if (fn) {
/* We found a datanode. Do the GC */
if((start >> PAGE_CACHE_SHIFT) < ((end-1) >> PAGE_CACHE_SHIFT)) {
/* It crosses a page boundary. Therefore, it must be a hole. */
ret = jffs2_garbage_collect_hole(c, jeb, f, fn, start, end);
} else {
/* It could still be a hole. But we GC the page this way anyway */
ret = jffs2_garbage_collect_dnode(c, jeb, f, fn, start, end);
}
goto upnout;
}
/* Wasn't a dnode. Try dirent */
for (fd = f->dents; fd; fd=fd->next) {
if (fd->raw == raw)
break;
}
if (fd && fd->ino) {
ret = jffs2_garbage_collect_dirent(c, jeb, f, fd);
} else if (fd) {
ret = jffs2_garbage_collect_deletion_dirent(c, jeb, f, fd);
} else {
printk(KERN_WARNING "Raw node at 0x%08x wasn't in node lists for ino #%u/n",
ref_offset(raw), f->inocache->ino);
if (ref_obsolete(raw)) {
printk(KERN_WARNING "But it's obsolete so we don't mind too much/n");
} else {
ret = -EIO;
}
}
upnout:
up(&f->sem);
up(&c->alloc_sem);
iput(inode);
eraseit_lock:
/* If we've finished this block, start it erasing */
spin_lock_bh(&c->erase_completion_lock);
eraseit:
if (c->gcblock && !c->gcblock->used_size) {
D1(printk(KERN_DEBUG "Block at 0x%08x completely obsoleted by GC. Moving to
erase_pending_list/n", c->gcblock->offset));
/* We're GC'ing an empty block? */
list_add_tail(&c->gcblock->list, &c->erase_pending_list);
c->gcblock = NULL;
c->nr_erasing_blocks++;
jffs2_erase_pending_trigger(c);
}
spin_unlock_bh(&c->erase_completion_lock);
return ret;
}
jffs2_erase_pending_trigger函数
这个函数设置jffs2的超级块中的s_dirt标志:
void jffs2_erase_pending_trigger(struct jffs2_sb_info *c)
{
OFNI_BS_2SFFJ(c)->s_dirt = 1;
}
这样当卸载jffs2时,在sync_super函数中会遍历super_blocks队列调用所有s_dirt标志有效的超级块的write_super方法,对于jffs2而言即为jffs2_write_super函数:
void jffs2_write_super (struct super_block *sb)
{
struct jffs2_sb_info *c = JFFS2_SB_INFO(sb);
sb->s_dirt = 0;
if (sb->s_flags & MS_RDONLY)
return;
D1(printk(KERN_DEBUG "jffs2_write_super()/n"));
jffs2_garbage_collect_trigger(c);
jffs2_erase_pending_blocks(c);
}
在jffs2_garbage_collect_trigger函数中向GC内核线程发送SIGHUP信号。总之在卸载jffs2时唤醒GC内核线程的步骤如下:
sys_umount > do_umount > fsync_dev > sync_supers > s_op->write_super (即jffs2_write_super) >
jffs2_garbage_collect_trigger > send_sig(SIGHUP, c->gc_task, 1);
第11章 讨论和体会
什么是日志文件系统,为什么要使用jffs2
上层VFS框架通过函数指针接口实现与底层具体文件系统实现相隔离。底层具体文件系统提供在相应设备上按照特定格式访问各种类型文件的方法,并分配函数指针接口的空间(而VFS数据结构中只为指向接口的指针分配了空间)。VFS框架实现了访问、管理文件系统的上层策略,具体文件系统则提供在具体设备上的底层机制。
必须针对具体设备的特点设计具体文件系统的数据存储格式。jffs2是专门针对flash的特点设计的文件系统。flash为EEPROM,可以随机读出,但是在写之前必须执行擦除操作,这是因为flash的写操作只能将1写为0,所以必须首先将待写区域全部“擦除”为1。而且擦除操作必须以擦除块为单位进行,即使只改动一个字节的数据,也不得不擦除其所在的整个擦除块。
flash芯片由若干擦除块组成,单个擦除块的寿命(大约100,000次)决定了整个flash芯片的寿命。所以基于flash的文件系统应该使得所有的擦除块都被几乎同等频繁地使用,避免过度使用一部分擦除块而导致整个芯片过早报废。这种算法称为“Wear levelling”。
在ext2文件系统中如果修改了文件的某一个区域,那么在写文件前,必须首先找出相应区域所对应的磁盘块,然后视需要读出这个块,再修改,最后异步地写回这个块。而在jffs2文件系统中如果修改了文件的某一个区域,那么不会立即删除flash上含有该区域数据的原有数据实体,是直接向flash分区中写入一个崭新的数据实体,同时将原有数据实体在内核中的描述符标记为“过时”。那么在打开这个文件、为其数据实体创建相应的数据结构时就会跳过所有过时的数据实体,参见jffs2_get_inode_nodes函数。另外,当标记原有数据实体“过时”时,在jffs2_mark_node_obsolete函数(尚未详细分析)中还会同时修改原有数据实体头部的nodetype区域。这个操作可以从jffs2map2的结果看出来。
flash上干净擦除块数量低于某一阈值时或者卸载jffs2时GC就会启用:回收“过时”的数据实体所在的空间。其实每次GC内核线程运行时只把被GC的擦除块中一个有效数据实体变成过时,当整个擦除块中只含有过时数据实体时进行擦除操作。
jffs2尽量减少擦除操作:如果每擦除一个擦除块只是修改其中的一个单元,那么效率是最低的。如果需要修改整个擦除块的所有单元,那么执行一次擦除操作的效率就是最高的。在GC时会挑选一个含有过时数据实体的擦除块,把其中有效的数据实体写一个副本到另外的擦除块上,从而使有效的数据实体也变成过时的。当整个擦除块变成全脏的时候就可以进行擦除操作了(挑选“被GC的”擦除块和“另外的”擦除块都与Wear Levelling算法有关)。
为什么需要红黑树
jffs2文件系统中需要维护数据实体的内核描述符jffs2_raw_node_ref的链表。假设文件初始数据结点为N个,那么多次修改后实际的数据实体个数和链表长度将远远大于N。
VFS的读写函数接口提供文件某个区域的起始位置offset及长度len。当然可以遍历整个链表,跳过落在这个区域内的过时数据实体、找到有效的数据实体,然后再访问flash上的有效实体。但是如果N较大,修改次数较多,那么每次读写时都要遍历整个链表就十分缺乏效率了。
所以需要一种合适的用于快速查找的数据结构及算法,将链表元素以另外的方式组织起来,于是就采用了红黑树。文件个数据实体的内核描述符除了组织在链表中,还通过相应的jffs2_full_dnode和jffs2_node_frag组织在红黑树中,树根为jffs2_inode_info的fragtree。
对文件进行修改时要写入新的数据实体,同时在内核中创建相应的描述符和jffs2_full_dnode数据结构,在将新实体的描述符加入jffs2_inode_cache.nodes链表的首部时还要标记原有实体的描述符为过时,同时,还要修改红黑树中相应结点的node指针指向新的jffs2_full_dnode结构,同时递减过时的jffs2_full_dnode的frags引用计数(原来为1,现在就为0了)。(注意,红黑树中的结点始终指向有效数据实体的相关数据结构)
何时、如何判断数据实体是过时的
在修改文件时写入新的数据实体,此时显然知道原有数据实体是过时的。但这只是整个事情的一小部分。
在挂载文件系统时将扫描整个flash分区,为所有的数据实体创建内核描述符,此时只对正规文件的数据实体本身进行crc校验,而把对后继数据的crc校验延迟到了打开文件时。如果发现数据实体本身的crc校验错误,则标记其内核描述符为过时的,参见jffs2_get_inode_nodes函数的相关部分。
需要强调的是,在挂载文件系统时要为所有的数据实体创建内核描述符,无论其过时与否!在jffs2_mark_node_obsolete函数中标记数据实体过时时,还会同时修改flash中数据实体本身头部的nodetype,使得头部的crc校验失败。所以在挂载文件系统时是知道数据实体是否为过时的。
对于目录文件,在挂载的后期创建目录项的jffs2_full_dirent数据结构的链表。此时就可以发现目录项数据实体是否过时了:如果它们的名字相同,那么版本号较高的那个为有效的,其它的为过时的。此时只将有效数据实体的jffs2_full_dirent加入链表,而释放过时的数据结构。详见jffs2_add_fd_to_list函数。
对于正规文件,在打开文件、创建inode时根据数据实体的内核描述符创建其它数据结构,并组织红黑树。在组织红黑树时就可以发现是否有多个数据实体涉及相同区域的数据了,版本号最高的数据实体为有效的,而其它的都是过时的。此时将红黑树中的结点指向版本号最高的数据实体的jffs2_full_dnode数据结构,并标记其它数据实体的内核描述符为过时。具体操作在jffs2_add_full_dnode_to_inode函数中,有待详细分析核实。
后记
本文从jffs2文件系统的注册、挂载、文件的打开、正规文件的读写这几个情景出发分析了其源代码的主体框架,描述了jffs2文件系统的核心数据结构及之间的相互引用关系。
由于作者时间有限,jffs2文件系统所特有的垃圾回收(GC)算法、Wear Levelling算法及红黑树的插入删除操作等方面需要进一步深究,在此罗列如下:
1. 红黑树的插入、删除、查找、平衡,比如jffs2_add_full_dnode_to_inode函数、jffs2_lookup_node_frag函数。
2. 读写除正规文件之外其它特殊文件的方法,比如目录文件、符号链接文件等等。
3. 关闭文件时释放相应数据结构的过程。
4. 使用zlib库压缩、解压缩数据的方法。
5. Wear Levelling算法有关的函数,比如jffs2_rotate_list和jffs2_resereve_space函数,以及GC时选择一个擦除块的方法。
6. NAND flash的异步写入机制。
由于作者能力有限,现有文档中存在许多疑问(采用红色字体),也一定有许多错误,欢迎大家补充、批评指正,谢谢!
(从www.infradead.org上可以获得与jffs2相关的各种资源,包括正在开发中的jffs3)
附录 用jffs2map2模块导出文件的数据实体(new)
这个模块使得jffs2前所未有地真实、清晰,可以很好地促进对相关源代码的理解,发现先前犯下的许多错误认识,呵呵。
jffs2map2模块可以用于导出指定的jffs2文件系统上指定ino的文件的所有数据结点的信息,这些信息来自于挂载jffs2时创建的文件描述符jffs2_inode_cache的nodes链表。注意用jffs2map2观察指定文件时其可能尚未打开,所以上层的dentry、inode尚不存在,因此只能得到比较有限的记载于jffs2_inode_cache和各jffs2_raw_node_ref中的信息。比如根文件系统映像中根目录下proc和dev目录下都为空,它们的内容在内核启动时挂载procfs和devfs后才在内存中动态创建,所以用jffs2map2只能看到描述挂载点本身的那个jffs2_raw_inode数据实体,而没有任何其它目录项。
注意为了使用jffs2map2模块内核必需导出super_blocks和sb_lock变量。在安装jffs2map2模块时可以在命令行通过参数“jffs2map_sdev”和“jffs2map_ino”分别指定jffs2所在的设备,以及指定的文件ino。如果缺省则将来会输出作为根文件系统的jffs2上的根目录的信息。安装jffs2map2后,就可以通过“cat /proc/jffs2map”来读出指定文件的数据结点信息了。这样从根目录开始我们可以逐层得到任何文件的ino,进而在安装jffs2map2时指定其ino,从而观察挂载文件系统后相关文件的信息。
目前jffs2map2实现得还有些不便于使用,欢迎改进!
1. 每次安装后只能导出一个文件的信息,如果要观察其它文件,则必须首先rmmod,然后再次insmod并同时用“jffs2map_ino”指定其它文件的ino(庆幸开发板上的bash支持history功能,逃避了重复键入的麻烦:-P)。
2. 由于从proc导出数据时一次不能超过一个页框,否则就应该使用proc读回调函数接口中的start、pages参数来生成多余一个页框的数据了。
观察根目录文件的数据实体
1. 执行操作
insmod jffs2map2.o
cat /proc/jffs2map
2. 输出结果
Display jffs2 with s_dev = (31,4).
The highest ino is 1174, displaying file information with ino 1
ino=1, nlink=0x1, state: unchecked
<pristine,0x1341238,0x30>,jffs2_raw_dirent,ino=0,pino=1,type=UNKNOWN,name: shrek2
<pristine,0x1341208,0x30>,jffs2_raw_dirent,ino=1174,pino=1,type=DIR,name: shrek3
<pristine,0x5906c4,0x2c>,jffs2_raw_dirent,ino=823,pino=1,type=DIR,name: work
<pristine,0x588d50,0x2c>,jffs2_raw_dirent,ino=817,pino=1,type=LNK,name: tmp
<pristine,0x588cd0,0x2c>,jffs2_raw_dirent,ino=816,pino=1,type=LNK,name: var
<pristine,0x565320,0x38>,jffs2_raw_dirent,ino=815,pino=1,type=REG,name: .ramfs.tar.gz
<pristine,0x3af4,0x30>,jffs2_raw_dirent,ino=133,pino=1,type=DIR,name: yanshou
<pristine,0x3a84,0x2c>,jffs2_raw_dirent,ino=132,pino=1,type=DIR,name: root
<pristine,0x3a14,0x2c>,jffs2_raw_dirent,ino=131,pino=1,type=DIR,name: proc
<pristine,0x38c4,0x2c>,jffs2_raw_dirent,ino=128,pino=1,type=DIR,name: home
<pristine,0x3854,0x2c>,jffs2_raw_dirent,ino=127,pino=1,type=DIR,name: dev
<pristine,0x26b4,0x2c>,jffs2_raw_dirent,ino=88,pino=1,type=DIR,name: etc
<pristine,0x2644,0x2c>,jffs2_raw_dirent,ino=87,pino=1,type=DIR,name: sbin
<pristine,0x1d40,0x2c>,jffs2_raw_dirent,ino=67,pino=1,type=DIR,name: lib
<pristine,0x1cd0,0x2c>,jffs2_raw_dirent,ino=66,pino=1,type=DIR,name: bin
<pristine,0x16a4,0x2c>,jffs2_raw_dirent,ino=52,pino=1,type=DIR,name: mnt
<pristine,0x0,0x2c>,jffs2_raw_dirent,ino=2,pino=1,type=DIR,name: usr
3. 结果分析
由于在安装模块时没有指定jffs2所在的设备号,则缺省值为根设备(主号31,次号4);由于没有指定文件的ino,则缺省值为1,即输出根目录的信息。
注意打开文件后上层VFS所使用的文件硬链接计数为inode的nlink,而这里输出的nlink为jffs2_inode_cache的nlink,二者不同(在打开文件时会进一步根据非叶目录下子目录的个数递增父目录的硬链接计数)。
接下来的每一行输出包括如下内容:数据实体的状态、在flash上的物理地址、长度、数据实体的类型、目录项所代表的文件的ino、目录项所属的目录文件的ino、目录项所对应文件的类型、目录项所对应文件的名字。
由此可见,根目录文件没有那个惟一的jffs2_raw_inode数据实体,只包含其下子目录、文件的目录项。另外目录项的pino都等于1,它们的内核描述符都链接在根目录文件的jffs2_inode_cache的nodes链表中。
另外在控制台执行“ls –l /”的结果如下:
total 0
drwxr-xr-x 2 root root 0 Dec 24 2004 bin
drwxr-xr-x 1 root root 0 Jan 1 00:00 dev
drwxr-xr-x 26 root root 0 Jan 1 1970 etc
drwxr-xr-x 4 root root 0 Jan 1 1970 home
drwxr-xr-x 4 root root 0 Jul 9 2002 lib
drwxr-xr-x 6 root root 0 Jan 1 1970 mnt
dr-xr-xr-x 26 root root 0 Jan 1 00:00 proc
drwxr-xr-x 3 root root 0 Jan 1 1970 root
drwxr-xr-x 2 root root 0 Jan 1 1970 sbin
drwxr-xr-x 2 root root 0 Jan 1 1970 shrek3
lrwxrwxrwx 1 root root 13 Aug 11 2005 tmp -> mnt/ramfs/tmp
drwxr-xr-x 7 root root 0 Jan 1 1970 usr
lrwxrwxrwx 1 root root 13 Aug 11 2005 var -> mnt/ramfs/var
drwxr-xr-x 2 root root 0 Jan 1 1970 work
drwxr-xr-x 2 500 root 0 Jan 1 1970 yanshou
由此可见在“/”下除了有两个符号链接tmp和var外,其余的都是目录文件。注意有一个名为“shrek3”的目录,原先其名字叫做“shrek2”,后来由“mv”改名过来。但是此时仍然可以看到由一个关于shrek2的目录项,而且其ino等于0。
下面我们看一个符号链接tmp的信息注意其ino等于817,它的目标文件为“mnt/ramfs/tmp”。
观察符号链接的信息
1. 执行操作
rmmod jffs2map2
insmod jffs2map2.o jffs2map_ino=817
cat /proc/jffs2map
2. 输出结果
Display jffs2 with s_dev = (31,4).
The highest ino is 1174, displaying file information with ino 817
ino=817, nlink=0x1, state: present
<normal,0x588d7c,0x54>,jffs2_raw_inode,ino=817,dsize=0xd,csize=0xd
A symbolic link, the linked file is mnt/ramfs/tmp
3. 结果分析
从源代码分析中我们已经知道目录文件、符号链接、设备文件都有惟一的jffs2_raw_inode数据实体。对于符号链接,其后继数据即为被链接文件名。
这里验证了这个结果。
可以看到tmp文件只有一个jffs2_raw_inode数据实体,其后继数据长度为0xd,即等于其后继文件名“mnt/ramfs/tmp”的长度。
观察正规文件创建后的数据实体
1. 执行操作
rmmod jffs2map2
mkdir /test
echo 1 > /test/1.txt
insmod jffs2map2.o jffs2map_ino=1175(注:事先从根目录下得到新建的test目录的ino为1175)
cat /proc/jffs2map
rmmod jffs2map2
insmod jffs2map2.o jffs2map_ino=1176(注:从上一步得到1.txt文件的ino为1176)
cat /proc/jffs2map
2. 输出结果
Display jffs2 with s_dev = (31,4).
The highest ino is 1176, displaying file information with ino 1175
ino=1175, nlink=0x1, state: present
<pristine,0x13452e8,0x30>,jffs2_raw_dirent,ino=1176,pino=1175,type=REG,name: 1.txt
<normal,0x1345234,0x44>,jffs2_raw_inode,ino=1175,dsize=0x0,csize=0x0
A directory.
Display jffs2 with s_dev = (31,4).
The highest ino is 1176, displaying file information with ino 1176
ino=1176, nlink=0x1, state: present
<normal,0x1345318,0x48>,jffs2_raw_inode,ino=1176,dsize=0x2,csize=0x2
A regular file.
<obsolete,0x13452a4,0x44>,Header CRC 98f7fb1d != calculated CRC 5936d419 for node at 13452a4
Unknown node type
3. 结果分析
从源代码分析中我们已经知道除根目录外任何目录都由惟一的jffs2_raw_inode数据实体,其后继没有数据。从这里可以看到这一点。在目录“/test”下有一个正规文件“1.txt”,所以观察test目录时可以看到其目录项,其ino等于1176,即1.txt文件的ino;pino等于test目录的ino;类型为REG,文件名为“1.txt”。
进一步观察1.txt文件的数据实体,由于在创建时只写入了一个字符1,所以包括字符串结束符在内文件的长度应该为2,即dsize等于2。
从目录文件的create方法可见在创建正规文件之初就创建了一个临时的jffs2_raw_inode,其上层的jffs2_full_dnode由其jffs2_inode_info的metadata直接指向。等到第一次真正写入时将其过时,然后再写入真正带有有效数据的jffs2_raw_inode数据实体。由于在jffs2_mark_node_obsolete函数中标记数据实体的内核描述符为过时时,还会同时修改flash上的数据实体的头部的nodetype域,清除其JFFS2_NODE_ACCURATE标志,所以头部的crc校验会失败。
这里看到的那个被标记为obsolete的数据实体相信就是那个在创建之初写入的临时的jffs2_raw_inode。
另外可以进一步修改这个文件,比如通过“echo 22 > 1.txt”或者“echo 22 >> 1.txt”,再观察该文件数据实体的信息。再进一步,重启系统,然后再次观察。
观察jffs2_raw_inode数据实体的大小上限
在使用mkfs.jffs2将ext2目录树转换为jffs2映像时,可以使用其“-s”选项指定正规文件数据结点的大小上限,默认值为一个页框。当数据结点大小上限增加时可以减少jffs2_raw_inode数据实体本身的个数,但是这仅是一个方面。
1. 执行操作
mount /dev/mtdblock/3 /work (挂载/dev/mtdblock/3上的jffs2到/work目录)
rmmod jffs2map2
insmod /home/cqt/jffs2map2.o jffs2map_sdev=7939
cat /proc/jffs2map (输出新挂载的jffs2的根目录的信息)
rmmod jffs2map2
insmod /home/cqt/jffs2map2.o jffs2map_ino=3
cat /proc/jffs2map (输出新挂载的jffs2中mkfs.jffs22文件的信息)
cp /work/mkfs.jffs22 / (将这个文件拷贝到根jffs2中。注意这两种jffs2有不同的数据实体大小上限!)
rmmod jffs2map2
insmod /home/cqt/jffs2map2.o jffs2map_ino=1177
cat /proc/jffs2map (输出拷贝到根jffs2下mkfs.jffs22的信息)
2. 输出结果
Display jffs2 with s_dev = (31,3).
The highest ino is 3, displaying file information with ino 1
ino=1, nlink=0x1, state: unchecked
<pristine,0x2c0040,0x3c>,jffs2_raw_dirent,ino=2,pino=1,type=DIR,name: telnetd_on_pcm7210
<pristine,0x2c000c,0x34>,jffs2_raw_dirent,ino=0,pino=1,type=UNKNOWN,name: mkfs.jffs2
<pristine,0x4cb0,0x34>,jffs2_raw_dirent,ino=3,pino=1,type=REG,name: mkfs.jffs22
<obsolete,0x0,0x34>,jffs2_raw_dirent,ino=2,pino=1,type=REG,name: mkfs.jffs2
Display jffs2 with s_dev = (31,3).
The highest ino is 3, displaying file information with ino 3
ino=3, nlink=0x1, state: checkedabsent
<normal,0x9014,0x94c>,jffs2_raw_inode,ino=3,dsize=0x13db,csize=0x907
A regular file.
<normal,0x7dc4,0x1250>,jffs2_raw_inode,ino=3,dsize=0x3000,csize=0x120b
<normal,0x65fc,0x17c8>,jffs2_raw_inode,ino=3,dsize=0x3000,csize=0x1784
<normal,0x4ce4,0x1918>,jffs2_raw_inode,ino=3,dsize=0x3000,csize=0x18d2
Display jffs2 with s_dev = (31,4).
The highest ino is 1177, displaying file information with ino 1177
ino=1177, nlink=0x1, state: present
<normal,0x134ab88,0x1d4>,jffs2_raw_inode,ino=1177,dsize=0x3db,csize=0x18d
A regular file.
<normal,0x134a390,0x7f8>,jffs2_raw_inode,ino=1177,dsize=0x1000,csize=0x7b1
<normal,0x1349da0,0x5f0>,jffs2_raw_inode,ino=1177,dsize=0x1000,csize=0x5aa
<normal,0x134973c,0x664>,jffs2_raw_inode,ino=1177,dsize=0x1000,csize=0x620
<normal,0x1349060,0x6dc>,jffs2_raw_inode,ino=1177,dsize=0x1000,csize=0x698
<normal,0x134859c,0xac4>,jffs2_raw_inode,ino=1177,dsize=0x1000,csize=0xa7f
<normal,0x1347c2c,0x970>,jffs2_raw_inode,ino=1177,dsize=0x1000,csize=0x92c
<normal,0x134773c,0x4f0>,jffs2_raw_inode,ino=1177,dsize=0x1000,csize=0x4ac
<normal,0x1347068,0x6d4>,jffs2_raw_inode,ino=1177,dsize=0x1000,csize=0x690
<normal,0x13465dc,0xa8c>,jffs2_raw_inode,ino=1177,dsize=0x1000,csize=0xa46
<normal,0x1345d2c,0x8b0>,jffs2_raw_inode,ino=1177,dsize=0x1000,csize=0x86b
<obsolete,0x1345cb4,0x44>,Header CRC 98f7fb1d C 5936d419 for node at 1345cb4
Unknown node type
3. 结果分析
根文件系统为位于/dev/mtdblock/4上的jffs2,在使用mkfs.jffs2制作其映像时采用默认的jffs2_raw_inode大小上限,即0x1000。再使用mkfs.jffs2制作一个指定大小上限为0x3000的jffs2映像,并把它拷贝到flash上次号为3的那个分区上,并挂载到根jffs2的/work目录上。
由其根目录文件的信息知,一个名为mkfs.jffs22的正规文件的ino等于3。进一步观察其数据结点大小,结果看到dsize等于0x3000,而csize为压缩后的大小。
最后,将这个文件拷贝到根jffs2中。注意这两个文件系统所支持的数据实体的大小上限不同!然后再次观察这个文件的信息,结果发现数据实体的大小上限由0x3000变成了0x1000!(最后那个过时的数据结点相信还是目录文件create的结果,呵呵。)
(全文完)