F2FS源码分析系列文章
主目录
一、文件系统布局以及元数据结构
二、文件数据的存储以及读写
三、文件与目录的创建以及删除(未完成)
四、垃圾回收机制
五、数据恢复机制
六、重要数据结构或者函数的分析
- f2fs_summary的作用
- f2fs_journal的作用
- f2fs_map_block的作用
- get_dnode_of_data的作用
- get_node_page的作用(未完成)
F2FS Journal机制
Journal机制的介绍
当F2FS进行文件读写的时候,根据 f2fs_node
的设计以及闪存设备异地更新的特性,每修改一个数据块,都需要改动 f2fs_node
的地址映射,以及NAT,SIT等信息。但是如果仅仅因为一个小改动,例如修改一个块,就需要改动这么多数据,然后再写入磁盘,这样既会导致性能下降,也会导致闪存寿命的下降。
因此F2FS设计了journal机制,用于将这些对SIT和NAT区域的数据的修改暂存在Checkpoint区域的 f2fs_journal
中,这种设计可以减少NAT和SIT的更新产生的大量的I/O。
产生大量I/O的原因是,sit和nat的值会随机更新,由于flash的机制,即使更新SIT/NAT其中的一个entry,都要将这个entry所在的整个NAT/SIT的block进行重写。如用户更新了多个node(segment),每一个node/segment的更改都要更新了其中一个entry,即f2fs_nat_entry
(f2fs_sit_entry
)。假设这些需要更新的entry都不在通过一个f2fs_nat_block
(f2fs_sit_block
),那么系统就要将所有的block更新,从而产生大量的4KB的I/O,即使只改动其中一个entry。
F2FS则是通过journal机制,将这些随机更新的f2fs_nat_entry
(f2fs_sit_entry
)都会尝试集中写到checkpoint区域的f2fs_journal
当中,即把随机更新的NAT/SIT的block需要更新的entry都集中写到同一个block,从而减少I/O的写入。如果f2fs_journal
的空间还没用尽,系统就会将更新的NAT/SIT entry写入journal中,如果这些journal用尽了,才回写到NAT/SIT区域当中。
Journal涉及到的数据结构
struct f2fs_journal {
union {
__le16 n_nats; /* 这个journal里面包含多少个nat_journal对象 */
__le16 n_sits; /* 这个journal里面包含多少个sit_journal对象 */
};
/* spare area is used by NAT or SIT journals or extra info */
union {
struct nat_journal nat_j;
struct sit_journal sit_j;
struct f2fs_extra_info info;
};
} __packed;
f2fs_journal
可以保存NAT的journal也可以保存SIT的journal,以下分别分析:
NAT Journal
每一个nat_journal
都对应了一个node,记录了这个node是属于哪一个inode,以及这个node的在flash设备的物理地址。
struct nat_journal {
struct nat_journal_entry entries[NAT_JOURNAL_ENTRIES];
__u8 reserved[NAT_JOURNAL_RESERVED];
} __packed;
struct nat_journal_entry {
__le32 nid; /* 当前node的nid信息 */
struct f2fs_nat_entry ne;
} __packed;
struct f2fs_nat_entry {
__u8 version; /* latest version of cached nat entry */
__le32 ino; /* 属于哪一个inode */
__le32 block_addr; /* flash设备的物理地址 */
} __packed;
SIT Journal
每一个sit_journal
都对应了一个segment,记录了该segment的有效block的数目 vblocks
以及 它的分配状态bitmap valid_map
。
struct sit_journal {
struct sit_journal_entry entries[SIT_JOURNAL_ENTRIES];
__u8 reserved[SIT_JOURNAL_RESERVED];
} __packed;
struct sit_journal_entry {
__le32 segno; /* 当前的segment number */
struct f2fs_sit_entry se;
} __packed;
struct f2fs_sit_entry {
__le16 vblocks; /* 有效block的数目 */
__u8 valid_map[SIT_VBLOCK_MAP_SIZE]; /* SIT_VBLOCK_MAP_SIZE=64,64*8=512可以表示每一个块的valid状态 */
__le64 mtime; /* 用于GC的victim segment selection */
} __packed;
Journal机制的实现
前面提及Journal机制主要用于将频繁的NAT和SIT的更新集中写到checkpoint区域的f2fs_journal
中,从而提高性能,延长闪存颗粒的寿命。去分析Journal机制如何实现时,首先我们需要分析,系统什么时候会读写NAT、SIT区域的数据:
NAT: 更新一个node的物理地址,获取一个node的物理地址
SIT: 分配segment中的block,回收segment中的block
对于node的物理地址的更新,以及segment中的block分配状态的更新,可以参考Checkpoint机制这一节的f2fs_flush_nat_entries
函数以及f2fs_flush_sit_entries
函数。
因此这里关注如何获取一个node的物理地址,以及一个segment的block分配状态。
通过Journal获取一个node的物理地址
F2FS一般是通过f2fs_get_node_info
函数,根据传入的nid值,找到对应的物理地址。首先从内存中找一下nid有没有cache,如果没有则使用journal寻址,如果还是没有采取NAT找,最后找到之后将这个entry的信息,加入到cache中。
void f2fs_get_node_info(struct f2fs_sb_info *sbi, nid_t nid,
struct node_info *ni)
{
struct f2fs_nm_info *nm_i = NM_I(sbi);
struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_HOT_DATA);
struct f2fs_journal *journal = curseg->journal;
nid_t start_nid = START_NID(nid); // 计算这个nid所在的f2fs_nat_block的第一个nid
struct f2fs_nat_block *nat_blk;
struct page *page = NULL;
struct f2fs_nat_entry ne;
struct nat_entry *e;
pgoff_t index;
int i;
ni->nid = nid;
/* 根据nid从cache寻找物理地址信息 */
down_read(&nm_i->nat_tree_lock);
e = __lookup_nat_cache(nm_i, nid);
if (e) { // 如果有就返回
ni->ino = nat_get_ino(e);
ni->blk_addr = nat_get_blkaddr(e);
ni->version = nat_get_version(e);
up_read(&nm_i->nat_tree_lock);
return;
}
memset(&ne, 0, sizeof(struct f2fs_nat_entry));
/* 再去journal找 */
down_read(&curseg->journal_rwsem);
/*
* 根据nid从journal找nat_entry的信息,如果 i>=0 ,
* 则表示journal有这个信息,否则表示journal不存在这个nid的信息
* */
i = f2fs_lookup_journal_in_cursum(journal, NAT_JOURNAL, nid, 0);
if (i >= 0) {
ne = nat_in_journal(journal, i); // 将journal中的nat_entry返回出来
node_info_from_raw_nat(ni, &ne); // 读到node_info中
}
up_read(&curseg->journal_rwsem);
if (i >= 0) {
up_read(&nm_i->nat_tree_lock);
goto cache; // 在journal找到了,直接就返回
}
/*
* 如果journal都没有,就要从NAT读取
*
* 先根据这个nid计算一下所属的f2fs_nat_block的偏移,即物理地址
* */
index = current_nat_addr(sbi, nid);
up_read(&nm_i->nat_tree_lock);
/* 根据f2fs_nat_block偏移,将从磁盘读取出来 */
page = f2fs_get_meta_page(sbi, index);
/* 将数据转换为f2fs_nat_block的形式 */
nat_blk = (struct f2fs_nat_block *)page_address(page);
/* start_nid是这个nat_block的第一个nid,
* 减去它就可以找出当前nid在nat_block内的偏移,然后都取出来
* */
ne = nat_blk->entries[nid - start_nid];
/* 根据读取出来的entry转为为ne的值,返回给调用函数 */
node_info_from_raw_nat(ni, &ne);
f2fs_put_page(page, 1);
cache:
/* 如果cache不存在自然要缓存一下到内存中 */
cache_nat_entry(sbi, nid, &ne); // 缓存这个node_entry
}
通过Journal获取一个segment的block分配状态
F2FS一般是通过get_seg_entry
函数根据segment number(segno)获取对应的entry。由于segment的entry的数目比node entry少很多,所以F2FS将所有的segment的entry都读入了内存,参考Segment结构这一节。因此系统中读取segmeng entry的状态是简单的数组访问:
/* seg_entry是f2fs_sit_entry的内存结构,同样记录了vblock valid_bitmap等信息 */
static inline struct seg_entry *get_seg_entry(struct f2fs_sb_info *sbi,
unsigned int segno)
{
struct sit_info *sit_i = SIT_I(sbi); // 获取segment的内存管理结构
return &sit_i->sentries[segno]; // 根据segno返回entry
}