文件数据的组织方式一般时被设计为inode-data模式,即 每一个文件都具有一个inode,这个inode记录data的组织关系,这个关系称为文件结构。例如用户需要访问A文件的第1000个字节,系统就会先根据A文件的路径找到的A的inode,然后从inode找到第1000个字节所在的物理地址,然后从磁盘读取出来。那么F2FS的文件结构是怎么样的呢?
如上图,F2FS中的一个inode,包含两个主要部分: metadata部分,和数据块寻址部分。我们重点观察数据块寻址部分,分析inode是如何将数据块索引出来。在图中,数据块寻址部分包含direct pointers,single-indirect,double-indirect,以及triple-indirect。它们的含义分别是:
direct pointer: inode内直接指向数据块(图右上角Data)的地址数组,即inode->data模式。
single-indirect pointer: inode记录了两个single-indirect pointer(图右上角Direct node),每一个single-indirect pointer存储了多个数据块的地址,即inode->direct_node->data模式。
double-indirect: inode记录了两个double-indirect pointer(图右上角indirect node),每一个double-indirect pointer记录了许多single-indirect pointer,每一个single-indirect pointer指向了数据块,即inode->indirect_node->direct_node->data模式。
triple-indirect: inode记录了一个triple-indirect pointer(图右上角indirect node),每一个triple-indirect pointer记录了许多double-indirect pointer,每一个double-indirect pointer记录了许多single-indirect pointer,最后每一个single-indirect pointer指向了数据块。即inode->indirect_node->indirect_node->direct_node->data模式。
因此,我们可以发现,F2FS的inode结构采取indirect_node,首先在inode内部寻找物理地址,如果找不到再去direct_node找,层层深入。
f2fs_node的结构以及作用
根据上面的分析,我们可以发现一个对于一个较大的文件,它可能包含inode以外的node,去保存一些间接寻址的信息。single-indirect pointer记录的是数据块的地址,而double-indirect pointer记录的是single-indirect pointer的地址,triple-indirect pointer记录的double-indirect pointer地址。在F2FS中,
inode对应的是f2fs_inode
结构,包含了多个direct pointer指向数据块物理地址;
single-indirect pointer对应的是direct_node
结构,包含了多个direct pointer指向物理地址;
double-indirect pointer对应的是indirect_node
结构,包含了多个指向direct_node
的地址;
triple-indirect pointer对应的也是indirect_node
结构,包含了多个指向indirect_node
的地址
接下来我们逐个分析F2FS每一个node的具体数据结构。
基本node结构
为了方便F2FS的对node的区分和管理,f2fs_inode
和direct_node
以及indirect_node
都使用了同一个数据结构f2fs_node
进行描述,并通过union的方式,将f2fs_node
初始化成不同的node形式,它的结构如下:
arduino
// f2fs_node size:4096
struct f2fs_node {
union {
struct f2fs_inode i; //size:4072
struct direct_node dn; //size:4072
struct indirect_node in; //size:4072
};
struct node_footer footer; // footer用于记录node的类型
} __packed;
struct node_footer {
__le32 nid; /* node id */
__le32 ino; /* inode nunmber */
__le32 flag; /* include cold/fsync/dentry marks and offset */
__le64 cp_ver; /* checkpoint version */
__le32 next_blkaddr; /* next node page block address */
} __packed;
其中起到区分是哪一种node的关键数据结构是node_footer
。
footer->nid和footer->ino
每一个node都有一个独特的nid
,它被记录在footer
中,如果是direct_node
或者indirect_node
,它们都有一个对应的f2fs_inode
,因此为了记录从属关系,还需要footer
记录它所属于的f2fs_inode
的nid
,即ino
。因此,如果footer->nid == footer->ino
,那么这个node就是inode,反正这个node
是direct_node
或者indirect_node
。
footer->flag
footer->flag
的作用是标记当前的node的属性。目前F2FS给node定义了三种属性:
arduino
enum {
COLD_BIT_SHIFT = 0,
FSYNC_BIT_SHIFT,
DENT_BIT_SHIFT,
OFFSET_BIT_SHIFT
};
#define OFFSET_BIT_MASK (0x07) /* (0x01 << OFFSET_BIT_SHIFT) - 1 */
其中footer->flag
的
第0位表示这个node是否是cold node。
第1位表示这个node是否执行了完整的fsync。F2FS为了fsync
的效率做了一些改进,F2FS不会在fsync
刷写所有脏的node page进去磁盘,只会刷写一些根据data直接相关的node page进入磁盘,例如f2fs_inode
和direct_node
。因此这个标志位是用来记录这个node是否执行了完整的fsync,以便系统在crash中恢复。
第3位表示这个node是是用来保存文件数据,还是目录数据的,也是用于数据恢复
footer->cp_ver和footer->next_blkaddr
footer->cp_ver
分别用来记录当前的checkpoint的version,恢复的时候比较version版本确定如何进行数据恢复。
footer->next_blkaddr
则是用来记录这个node对应下一个node page的地址,也是用来恢复数据
f2fs_inode结构
我们先看f2fs_inode
的结构,省略其他元数据的信息,重点关注文件如何索引的,结构如下:
ini
struct f2fs_inode {
...
union {
struct {
__le16 i_extra_isize; /* extra inode attribute size */
__le16 i_inline_xattr_size; /* inline xattr size, unit: 4 bytes */
__le32 i_projid; /* project id */
__le32 i_inode_checksum;/* inode meta checksum */
__le64 i_crtime; /* creation time */
__le32 i_crtime_nsec; /* creation time in nano scale */
__le32 i_extra_end[0]; /* for attribute size calculation */
} __packed;
__le32 i_addr[DEF_ADDRS_PER_INODE]; /* DEF_ADDRS_PER_INODE=923, Pointers to data blocks */
};
__le32 i_nid[DEF_NIDS_PER_INODE]; /*DEF_NIDS_PER_INODE=5, direct(2), indirect(2),
double_indirect(1) node id */
...
} __packed;
i_addr
数组就是前面提及的direct pointer,数组的下标是文件的逻辑位置,数组的值就是flash设备的物理地址。例如文件的第一个页就对应i_addr[0]
,第二个页就对应i_addr[1]
,而i_addr[0]
和i_addr[1]
所记录的物理地址,就是文件第一个页(page)和第二个页的数据的物理地址,系统可以将两个物理地址提交到flash设备,将数据读取出来。
我们可以发现i_addr
的数组长度只有923,即一个f2fs_inode
只能直接索引到923个页/块的地址(约3.6MB),对于大于3.6MB的文件,就需要使用间接寻址 。f2fs_inode
的i_nid
数组就是为了间接寻址而设计,i_nid
数组是一个长度为5的数组,可以记录5个node的地址。其中
i_nid[0]
和i_nid[1]
记录的是direct_node
的地址,即对应前述的single-indirect pointer。
i_nid[2]
和i_nid[3]
记录的是indirect_node
的地址,这两个indirect_node
记录的是direct_node
的地址,即对应前述的double-indirect pointer。
i_nid[4]
记录的是indirect_node
的地址,但是这个indirect_node
记录的是indirect_node
的地址,即前述的triple-indirect pointer。
direct_node和indirect_node结构
direct_inode
以及indirect_inode
的结构如下所示,direct_node
记录的是数据块的地址,indirect_inode
记录的是node的id,系统可以通过nid找到对应的node的地址。
ini
struct direct_node {
__le32 addr[ADDRS_PER_BLOCK]; // ADDRS_PER_BLOCK=1018
} __packed;
struct indirect_node {
__le32 nid[NIDS_PER_BLOCK]; // NIDS_PER_BLOCK=1018
} __packed;
Wandering Tree问题
F2FS的设计是为了解决wandering tree的问题,那么现在的设计是如何解决这个问题的呢。假设一个文件发生更改,修改了direct_node
里面的某一个block的数据,根据LFS的异地更新特性,我们需要给更改后的数据一个新的block。传统的LFS需要将这个新的block的地址一层层往上传递,直到inode结构。而F2FS的设计是只需要将direct_node
对应位置的addr
的值更新为新block的地址,从而没必要往上传递,因此解决了wandering tree的问题。
普通文件数据的保存
从上节描述可以知道,一个文件由一个f2fs_inode
和多个direct_inode
或者indirect_inode
所组成。当系统创建一个文件的时候,它会首先创建一个f2fs_inode
写入到flash设备,然后用户往该文件写入第一个page的时候,会将数据写入到main area的一个block中,然后将该block的物理地址赋值到f2fs_inode->i_addr[0]
中,这样就完成了Node-Data的管理关系。随着对同一文件写入的数据的增多,会逐渐使用到其他类型的node去保存文件的数据。
经过上面的分析,我们可以计算F2FS单个文件的最大尺寸:
f2fs_inode
直接保存了923个block的数据的物理地址f2fs_inode->i_nid[0~1]
保存了两个direct_node
的地址,这里可以保存 2 x 1018个block的数据f2fs_inode->i_nid[2~3]
保存了两个indirect_node
的地址,这两个其中2个indirect_node
保存的是direct_node
的nid,因此可以保存 2 x 1018 x 1018个block的数据;f2fs_inode->i_nid[4]
保存了一个indirect_node
的地址,这个indirect_node
保存的是indirect_node
的nid,因此可以保存 1018 x 1018 x 1018个页的数据
可以得到如下计算公式: 4KB x (923 + 2 x 1018 + 2 x 1018 x 1018 + 1 x 1018 x 1018 x 1018) = 3.93 TB 因此F2FS单个文件最多了保存3.93TB数据。
内联文件数据的保存
从上节可以知道,文件的实际数据是保存在f2fs_inode->i_addr
对应的物理块当中,因此即使一个很小的文件,如1个字节的小文件,也需要一个node和data block才能实现正常的保存和读写,也就是需要8KB的磁盘空间去保存一个尺寸为1字节的小文件。而且f2fs_inode->i_addr[923]
里面除了f2fs_inode->i_addr[0]
保存了一个物理地址,其余的922个i_addr都被闲置,造成了空间的浪费。
因此F2FS为了减少空间的使用量,使用内联(inline)文件减少这些空间的浪费。它的核心思想是当文件足够小的时候,使用f2fs_inode->i_addr
数组直接保存数据本身,而不单独写入一个block中,再进行寻址。因此,如上面的例子,只有1个字节大小的文件,只需要一个f2fs_inode
结构,即4KB,就可以同时将node信息和data信息同时保存,减少了一半的空间使用量。
根据上述定义,可以计算得到多大的文件可以使用内联的方式进行保存,f2fs_inode
有尺寸为923的用于保存数据物理地址的数组i_addr,它的数据类型是__le32,即4个字节。保留一个数组成员另做它用,因此内联文件最大尺寸为: 922 * 4 = 3688字节。
inline_data的挂载标志设置到了sbi中的struct f2fs_mount_info,因此可以通过文件大小就可以确定出,inode中保存的是文件数据还是Direct pointer。
文件读写与 物理地址 的映射的例子
Linux的文件是通过page进行组织起来的,默认page的size是4KB,使用index作为编号。
一个小文件访问例子
例如一个size=10KB的文件,需要3个page去保存数据,这3个page的编号是0,1,2。当用户访问这个文件的第2~6kb的数据的时候,系统就会计算出数据保存在page index = 0和1的page中,然后根据文件的路径找到对应的f2fs_inode
结构,page index = 0和1即对应f2fs_inode
的i_addr[0]
和i_addr[1]
。系统进而从这两个i_addr
读取物理地址,提交到flash设备将数据读取出来。
一个大文件访问例子
假设用户需要读取文件第4000个页(page index = 3999)的数据, 第一步: 那么首先系统会根据文件路径找到对应的f2fs_inode结构 第二步: 由于4000 >(923 + 1018 + 1018),f2fs_inode->i_addr
和f2fs_inode->nid[0]和nid[1]
都无法满足需求,因此系统根据f2fs_inode->nid[2]
找到对应的 indirect_node
的地址 第三步: indirect_node
保存的是direct_node
的nid数组,由于 4000 - 923 - 1018 - 1018 = 1041,而一个direct_node
只能保存1018个block,因此可以知道数据位于indirect_node->nid[1]
对应的direct_node
中 第四步: 计算剩下的的偏移(4000-923-1018-1018-1018=23)找到数据的物理地址位于该direct_node
的direct_node->addr[23]
中。
F2FS 物理地址 寻址的实现
VFS的读写都依赖于物理地址的寻址。经典的读流程,VFS会传入inode以及page index信息给文件系统,然后文件系统需要根据以上信息,找到物理地址,然后访问磁盘将其读取出来。F2FS的物理地址寻址,是通过f2fs_get_dnode_of_data
函数实现。
在执行这个f2fs_get_dnode_of_data
函数之前,需要通过set_new_dnode
函数进行对数据结构struct dnode_of_data
进行初始化:
arduino
/*
* this structure is used as one of function parameters.
* all the information are dedicated to a given direct node block determined
* by the data offset in a file.
*/
struct dnode_of_data {
struct inode *inode; /* VFS inode结构 */
struct page *inode_page; /* f2fs_inode对应的node page */
struct page *node_page; /* 用户需要访问的物理地址所在的node page,有可能跟inode_page一样*/
nid_t nid; /* 用户需要访问的物理地址所在的node的nid,与上面的node_page对应*/
unsigned int ofs_in_node; /* 用户需要访问的物理地址位于上面的node_page对应的addr数组第几个位置 */
bool inode_page_locked; /* inode page is locked or not */
bool node_changed; /* is node block changed */
char cur_level; /* 当前node_page的层次,按直接访问或者简介访问的深度区分 */
char max_level; /* level of current page located */
block_t data_blkaddr; /* 用户需要访问的物理地址 */
};
static inline void set_new_dnode(struct dnode_of_data *dn, struct inode *inode,
struct page *ipage, struct page *npage, nid_t nid)
{
memset(dn, 0, sizeof(*dn));
dn->inode = inode;
dn->inode_page = ipage;
dn->node_page = npage;
dn->nid = nid;
}
以f2fs_do_write_data_page为例
ini
int f2fs_do_write_data_page(struct f2fs_io_info *fio)
{
struct page *page = fio->page;
struct inode *inode = page->mapping->host;
struct dnode_of_data dn;
struct extent_info ei = {0,0,0};
struct node_info ni;
bool ipu_force = false;
int err = 0;
set_new_dnode(&dn, inode, NULL, NULL, 0); // 0表示不清楚nid
...
//然后根据需要访问的page index(文件的page offset),执行f2fs_get_dnode_of_data函数寻找
err = f2fs_get_dnode_of_data(&dn, page->index, LOOKUP_NODE);
if (err)
goto out;
fio->old_blkaddr = dn.data_blkaddr; //对应要访问的物理地址
}
f2fs_get_dnode_of_data
ini
int f2fs_get_dnode_of_data(struct dnode_of_data *dn, pgoff_t index, int mode)
{
struct f2fs_sb_info *sbi = F2FS_I_SB(dn->inode);
struct page *npage[4];
struct page *parent = NULL;
int offset[4];
unsigned int noffset[4];
nid_t nids[4];
int level, i = 0;
int err = 0;
//根据文件offset,找到对应的物理地址所保存的位置,noffset和offset,以及Level
level = get_node_path(dn->inode, index, offset, noffset);
if (level < 0)
return level;
//下面是根据inode的信息,依次索引读出noffset和offset对应的数据,获取offset对应的block物理地址。
nids[0] = dn->inode->i_ino;
npage[0] = dn->inode_page;
if (!npage[0]) {
npage[0] = f2fs_get_node_page(sbi, nids[0]); // 获取inode对应的f2fs_inode的node page
if (IS_ERR(npage[0]))
return PTR_ERR(npage[0]);
}
/* if inline_data is set, should not report any block indices */
if (f2fs_has_inline_data(dn->inode) && index) {
err = -ENOENT;
f2fs_put_page(npage[0], 1);
goto release_out;
}
parent = npage[0];
if (level != 0)
nids[1] = get_nid(parent, offset[0], true);// 获取f2fs_inode->i_nid
dn->inode_page = npage[0];
dn->inode_page_locked = true;
/* get indirect or direct nodes */
for (i = 1; i <= level; i++) {
bool done = false;
if (!nids[i] && mode == ALLOC_NODE) {
// 创建模式,常用,写入文件时,需要node page再写入数据,因此对于较大文件,在这里创建node page
if (!f2fs_alloc_nid(sbi, &(nids[i]))) {
err = -ENOSPC;
goto release_pages;
}
dn->nid = nids[i];
// 分配node page
npage[i] = f2fs_new_node_page(dn, noffset[i]);
if (IS_ERR(npage[i])) {
f2fs_alloc_nid_failed(sbi, nids[i]);
err = PTR_ERR(npage[i]);
goto release_pages;
}
// 如果i == 1,表示f2fs_inode->nid[0~1],即direct node,直接赋值到f2fs_inode->i_nid中
// 如果i != 1,表示parent是indirect node类型的,要赋值到indirect_node->nid中
set_nid(parent, offset[i - 1], nids[i], i == 1);
f2fs_alloc_nid_done(sbi, nids[i]);
done = true;
} else if (mode == LOOKUP_NODE_RA && i == level && level > 1) {
npage[i] = f2fs_get_node_page_ra(parent, offset[i - 1]);
if (IS_ERR(npage[i])) {
err = PTR_ERR(npage[i]);
goto release_pages;
}
done = true;
}
if (i == 1) {
dn->inode_page_locked = false;
unlock_page(parent);
} else {
f2fs_put_page(parent, 1);
}
if (!done) {
npage[i] = f2fs_get_node_page(sbi, nids[i]);
if (IS_ERR(npage[i])) {
err = PTR_ERR(npage[i]);
f2fs_put_page(npage[0], 0);
goto release_out;
}
}
if (i < level) {
parent = npage[i]; // 注意这里parent被递归地赋值,目的是处理direct node和indrect node的赋值问题
nids[i + 1] = get_nid(parent, offset[i], false);
}
}
// 全部完成后,将结果赋值到dn,然后退出函数
dn->nid = nids[level];
dn->ofs_in_node = offset[level];
dn->node_page = npage[level];
dn->data_blkaddr = datablock_addr(dn->inode,
dn->node_page, dn->ofs_in_node);
return 0;
release_pages:
f2fs_put_page(parent, 1);
if (i > 1)
f2fs_put_page(npage[0], 0);
release_out:
dn->inode_page = NULL;
dn->node_page = NULL;
if (err == -ENOENT) {
dn->cur_level = i;
dn->max_level = level;
dn->ofs_in_node = offset[level];
}
return err;
}
get_node_path
根据在文件中的offset,确定出要读取的Page,是属于几级索引的哪个偏移地址。这个函数输出的是一个Level和两个数组。
这里offset和noffset分别表示block offset和node offset,返回的level表示寻址的深度,一共有4个深度,使用0~3表示: level=0: 表示可以直接在f2fs_inode
找到物理地址 level=1: 表示可以在f2fs_inode->i_nid[0~1]
对应的direct_node
能够找到物理地址 level=2: 表示可以在f2fs_inode->i_nid[2~3]
对应的indirect_node
下的nid对应的direct_node
能够找到物理地址 level=3: 表示只能在f2fs_inode->i_nid[4]
对应indirect_node
的nid对应的indirect_node
的nid对应的direct_node
才能找到地址
由于offset和noffset,表示的是物理地址寻址信息,分别表示block偏移和direct node偏移来表示,它们是长度为4的数组,代表不同level 0~3 的寻址信息。之后的函数可以通过offset和noffset将数据块计算出来。
例子1: 物理地址 位于f2fs_inode 例如我们要寻找page->index = 665的数据块所在的位置,显然655是位于f2fs_inode
内,因此level=0,因此我们只需要看offset[0]以及noffset[0]的信息,如下图。offset[0] = 665表示这个数据块在当前direct node(注意: f2fs_inode也是direct node的一种)的位置;noffset[0]表示当前direct node是属于这个文件的第几个node,由于f2fs_inode是第一个node,所以noffset[0] = 0。
ini
level = 0 // 可以直接在f2fs_inode找到物理地址
offset[0] = 665 // 由于level=0,因此我们只需要看offset[level]=offset[0]的信息,这里offset[0] = 665表示地址位于f2fs_inode->i_addr[665]
noffset[0] = 0 // 对于level=0的情况,即看noffset[0],因为level=0表示数据在唯一一个的f2fs_inode中,因此这里表示inode。
例子2: 物理地址 位于direct_node 例如我们要寻找page->index = 2113的数据块所在的位置,它位于第二个direct_node,所以level=1。我们只需要看offset[1]以及noffset[1]的信息,如下图。offset[1] = 172表示这个数据块在当前direct node的位置,即direct_node->addr[172]
;noffset[1]表示当前direct node是属于这个文件的第几个node,由于它位于第二个direct_node,前面还有一个f2fs_inode以及一个direct node,所以这是第三个node,因此noffset[1] = 2。
ini
level = 1 // 表示可以在f2fs_inode->i_nid[0~1]对应的direct_node能够找到物理地址
offset[1] = 172 // 表示物理地址位于对应的node page的i_addr的第172个位置中,即direct_node->addr[172]
noffset[1] = 2 // 数据保存在总共第三个node中 (1个f2fs_inode,2个direct_node)
例子3: 物理地址 位于indirect_node 例如我们要寻找page->index = 4000的数据块所在的位置,它位于第1个indirect_node的第2个direct_node中,所以level=2。我们只需要看offset[2]以及noffset[2]的信息,如下图。offset[2] = 23表示这个数据块在当前direct node的位置;noffset[2]表示当前direct node是属于这个文件的第几个direct node,即这是第6个node。(1 * f2fs_inode + 2 * direct_node + 1 * indirect_node + 2 * direct node)。
ini
level = 2
offset[2] = 23
noffset[2] = 5
例子4: 物理地址 位于indirect_node再indiret_node中 (double indirect node) 例如我们要寻找page->index = 2075624的数据块所在的位置,它位于第一个double indirect_node的第一个indirect_node的第一个direct_node中,所以level=3。同理我们只需要看offset[3]以及noffset[3]的信息,如下,可以自己计算一下:
ini
level = 3
offset[3] = 17
noffset[3] = 2043
ini
/*
* The maximum depth is four.
* Offset[0] will have raw inode offset.
*/
static int get_node_path(struct inode *inode, long block,
int offset[4], unsigned int noffset[4])
{
const long direct_index = ADDRS_PER_INODE(inode);
const long direct_blks = ADDRS_PER_BLOCK(inode);
const long dptrs_per_blk = NIDS_PER_BLOCK;
const long indirect_blks = ADDRS_PER_BLOCK(inode) * NIDS_PER_BLOCK;
const long dindirect_blks = indirect_blks * NIDS_PER_BLOCK;
int n = 0;
int level = 0;
noffset[0] = 0;
if (block < direct_index) {
offset[n] = block;
goto got;
}
block -= direct_index;
if (block < direct_blks) {
offset[n++] = NODE_DIR1_BLOCK;
noffset[n] = 1;
offset[n] = block;
level = 1;
goto got;
}
block -= direct_blks;
if (block < direct_blks) {
offset[n++] = NODE_DIR2_BLOCK;
noffset[n] = 2;
offset[n] = block;
level = 1;
goto got;
}
block -= direct_blks;
if (block < indirect_blks) {
offset[n++] = NODE_IND1_BLOCK;
noffset[n] = 3;
offset[n++] = block / direct_blks;
noffset[n] = 4 + offset[n - 1];
offset[n] = block % direct_blks;
level = 2;
goto got;
}
block -= indirect_blks;
if (block < indirect_blks) {
offset[n++] = NODE_IND2_BLOCK;
noffset[n] = 4 + dptrs_per_blk;
offset[n++] = block / direct_blks;
noffset[n] = 5 + dptrs_per_blk + offset[n - 1];
offset[n] = block % direct_blks;
level = 2;
goto got;
}
block -= indirect_blks;
if (block < dindirect_blks) {
offset[n++] = NODE_DIND_BLOCK;
noffset[n] = 5 + (dptrs_per_blk * 2);
offset[n++] = block / indirect_blks;
noffset[n] = 6 + (dptrs_per_blk * 2) +
offset[n - 1] * (dptrs_per_blk + 1);
offset[n++] = (block / direct_blks) % dptrs_per_blk;
noffset[n] = 7 + (dptrs_per_blk * 2) +
offset[n - 2] * (dptrs_per_blk + 1) +
offset[n - 1];
offset[n] = block % direct_blks;
level = 3;
goto got;
} else {
return -E2BIG;
}
got:
return level;
}