前言
在前面的介绍中,f2fs保证一致性有两种方法:前滚恢复和后滚恢复。前滚恢复需要配合fsync流程使用,先不进行介绍。而后滚恢复就是本章节需要介绍的CheckPoint相关内容。所谓的文件系统一致性,可以简单类比交易操作,即交易双方中一方资金减少,另一方资金增加,任何时候(即使系统崩溃)都不应该出现一方资金减少,另一方资金没有增加,或者反过来,一方资金没有减少,另一方资金却增加了。这些都是违背了一致性。交易操作中常使用原子事务原理,只有最终提交交易完成操作,才算最终完成交易。如果没有提交交易完成操作,那么这笔交易可以被撤销。
CP介绍
CP是Checkpoint的缩写。上面将CP原理和交易流程做了类比,如果具体来说CP功能就是:将当前内存缓存的管理数据,写入到存储介质,一旦发生掉电或者宕机等事件,还可以恢复到上一次CP的状态。当然,还没有来得及做CP的修改,最终会丢失,所以掌握CP的时机显得尤为重要。因为CP可能需要做大量IO操作,非常耗时,但是不做CP的话,刚修改的数据可能会丢失,所以需要平衡好两者。
CP数据结构
盘上布局
struct f2fs_checkpoint {
__le64 checkpoint_ver; /* checkpoint block version number */
__le64 user_block_count; /* # of user blocks */
__le64 valid_block_count; /* # of valid blocks in main area */
__le32 rsvd_segment_count; /* # of reserved segments for gc */
__le32 free_segment_count; /* # of free segments in main area */
/* information of current node segments */
__le32 cur_node_segno[MAX_ACTIVE_NODE_LOGS];
__le16 cur_node_blkoff[MAX_ACTIVE_NODE_LOGS];
/* information of current data segments */
__le32 cur_data_segno[MAX_ACTIVE_DATA_LOGS];
__le16 cur_data_blkoff[MAX_ACTIVE_DATA_LOGS];
__le32 ckpt_flags; /* Flags : umount and journal_present */
__le32 cp_pack_total_block_count; /* total # of one cp pack */
__le32 cp_pack_start_sum; /* start block number of data summary */
__le32 valid_node_count; /* Total number of valid nodes */
__le32 valid_inode_count; /* Total number of valid inodes */
__le32 next_free_nid; /* Next free node number */
__le32 checksum_offset; /* checksum offset inside cp block */
__le64 elapsed_time; /* mounted time */
/* allocation type of current segment */
unsigned char alloc_type[MAX_ACTIVE_LOGS];
/* SIT and NAT version bitmap */
unsigned char sit_nat_version_bitmap[];
} __packed;
一个f2fs_checkpoint代表CP pack,其作用是记录本次CP的基本信息,包括版本号、sit和nat的bitmap使用情况、block的使用情况等等。
- checkpoint_ver:本次CP的版本号。每次写CP时,都会将版本号递增,版本号越大,表示数据越新。在恢复时,优先选择版本号大的CP。
- user_block_count:当前用户已经使用的block数量。
- valid_block_count:Main Area区域已经使用的block数量。
- rsvd_segment_count:为GC流程保留的segment数量。
- free_segment_count:Main Area区域空闲的segment数量。
- cur_node_segno:当前正在使用的node segment id号,对应curseg的数据。
- cur_node_blkoff:当前正在使用node segment的下一个待使用的node id。
- cur_data_segno:当前正在使用的data segment id号,对应curseg的数据。
- cur_data_blkoff:当前正在使用data segment的下一个待使用的node id。
- ckpt_flags:本次CP的标记,例如umount、resize、fsck等。
- cp_pack_total_block_count:本CP pack所占用的block数,每次CP的长度不固定。
- cp_pack_start_sum:本CP中,summary信息的开始位置。
- valid_node_count:本次CP中,已经使用的node数量。
- valid_inode_count:本次CP中,已经使用的inode数量。
- next_free_nid:下一个待使用的free node,对应NAT的管理。
- checksum_offset:本CP的checksum偏移位置。
- elapsed_time:系统挂载的时间戳。
- alloc_type:记录各个curseg对应的分配器类型,最多能记录16个。
- sit_nat_version_bitmap:记录SIT和NAT的bitmap,用于判断本次使用的时主区还是备区。
以上就是一个CP pack的全部内容,这只是记录了内存中的一些基础信息,curseg中的内容则附加在CP pack后。
CP的管理
写CP全流程
如下是CP的全流程图,下面将对照着代码逐一展开。
f2fs写CP全流程
f2fs_write_checkpoint
int f2fs_write_checkpoint(struct f2fs_sb_info *sbi, struct cp_control *cpc)
{
...
// 阻塞文件系统写操作
err = block_operations(sbi);
// 提交sbi中缓存的bio(可能经过合并了的)
f2fs_flush_merged_writes(sbi);
// 获取当前的CP版本号
ckpt_ver = cur_cp_version(ckpt);
// CP版本号递增
ckpt->checkpoint_ver = cpu_to_le64(++ckpt_ver);
/* write cached NAT/SIT entries to NAT/SIT area */
// 下刷journal中的nat entry cache
err = f2fs_flush_nat_entries(sbi, cpc);
// 下刷journal中的sit entry cache
f2fs_flush_sit_entries(sbi, cpc);
/* save inmem log status */
// 将pinned文件sum信息下刷
f2fs_save_inmem_curseg(sbi);
// 写CP
err = do_checkpoint(sbi, cpc);
// TODO:恢复pinned文件的sum信息?
f2fs_restore_inmem_curseg(sbi);
}
这是写CP全流程的操作,对照流程图可以看出有如下步骤:
- 阻塞系统写操作,将脏数据落盘,包括脏inline数据、脏dentry、脏node数据等等,这些数据会影响系统的一致性。
- 将sbi中缓存的bio下发。
- 将当前CP版本号递增,作为本次CP的版本号。版本号可以检验本次CP是否可用,以及哪一份数据是最新的。
- 将对NAT和SIT的修改下刷。
- 完成前面的脏数据处理后,开始真正写CP区域,也就是说提交一次“交易”完成的记录。
block_operations
/*
* Freeze all the FS-operations for checkpoint.
*/
static int block_operations(struct f2fs_sb_info *sbi)
{
/*
* Let's flush inline_data in dirty node pages.
*/
// 将inline data回写到inode page中
f2fs_flush_inline_data(sbi);
/* write all the dirty dentry pages */
// 下刷所有脏dentry页,fsync并没有处理dir的data数据,只下刷了文件的data
// 数据,在此处将dir的data数据下刷
if (get_pages(sbi, F2FS_DIRTY_DENTS)) {
f2fs_unlock_all(sbi);
err = f2fs_sync_dirty_inodes(sbi, DIR_INODE, true);
}
/*
* POR: we should ensure that there are no dirty node pages
* until finishing nat/sit flush. inode->i_blocks can be updated.
*/
f2fs_down_write(&sbi->node_change);
// 将管理元数据下刷(不属于文件、目录、链接的inode,应该就是元数据inode,例如:
// acl、attr等等)
if (get_pages(sbi, F2FS_DIRTY_IMETA)) {
f2fs_up_write(&sbi->node_change);
f2fs_unlock_all(sbi);
err = f2fs_sync_inode_meta(sbi);
}
f2fs_down_write(&sbi->node_write);
// fsync流程只下刷了普通文件的node page,配合前滚恢复可以保证一致性;
// 在CP流程中,将其他类型的noage page下刷,配合后滚恢复保证一致性
if (get_pages(sbi, F2FS_DIRTY_NODES)) {
f2fs_up_write(&sbi->node_write);
atomic_inc(&sbi->wb_sync_req[NODE]);
err = f2fs_sync_node_pages(sbi, &wbc, false, FS_CP_NODE_IO);
atomic_dec(&sbi->wb_sync_req[NODE]);
}
...
}
在写CP的时候,会冻结所有文件系统操作,这也是为啥CP流程开销很大的原因,需要尽可能减少CP次数。在这个流程有如下步骤:
- 遍历所有脏direct node(主要是为了找到inode page),如果该node存在inline data,则将page cache(已经被修改的inline data)数据回写到inode page,等待后续inode page落盘。
注:f2fs使用inline data来优化空间,一个inode page占4KB大小,但是元数据只占用一小部分空间。如果data数据小于一定范围,则存放在inline区域,省去分配一个data block的开销。同时由于inline区域的存在,所以在写数据之前,需要将inline data转为0号page cache,等待需要回写时,在将其写入inode page中,如果超过inline data范围,则不再回写。 - 通过遍历inode_list链表,将脏dirty dentry页下刷。
- 将脏元数据页下刷。
- 将脏node页下刷。
以上流程是将内存中缓存的脏页下刷,这是在写CP区域之前需要完成的动作。
f2fs_flush_nat_entries
int f2fs_flush_nat_entries(struct f2fs_sb_info *sbi, struct cp_control *cpc)
{
...
/*
* if there are no enough space in journal to store dirty nat
* entries, remove all entries from journal and merge them
* into nat entry set.
*/
// 如果是umount流程,就将journal下刷(但前面已经下刷过了?)
// 如果当前journal没有空间,也会释放journal的空间。
if (cpc->reason & CP_UMOUNT ||
!__has_cursum_space(journal,
nm_i->nat_cnt[DIRTY_NAT], NAT_JOURNAL))
remove_nats_in_journal(sbi);
// 遍历nat_set_root,将set中所有nat_entry下刷,这里需要考虑net_entry的合并
while ((found = __gang_lookup_nat_set(nm_i,
set_idx, SETVEC_SIZE, setvec))) {
unsigned idx;
set_idx = setvec[found - 1]->set + 1;
for (idx = 0; idx < found; idx++)
__adjust_nat_entry_set(setvec[idx], &sets,
MAX_NAT_JENTRIES(journal));
}
/* flush dirty nats in nat entry set */
// 将nat_entry_set放到journal或者下刷到存储介质。
list_for_each_entry_safe(set, tmp, &sets, set_list) {
err = __flush_nat_entry_set(sbi, set, cpc);
if (err)
break;
}
...
}
将内存中缓存的NAT表项修改下刷,而该信息是缓存在CURSEG_HOT_DATA类型的journal中。首先判断journal剩余空间是否满足缓存脏NAT表项,如果不足,则将当前journal中的表项放入到nat_entry_set中,等待下刷。如果本次是umount或者journal空间不足,则直接下刷到NAT区域中,否则存放在journal中即可。
f2fs_flush_sit_entries
void f2fs_flush_sit_entries(struct f2fs_sb_info *sbi, struct cp_control *cpc)
{
...
// 如果没有dirty sentry entry,则直接跳过,不处理
if (!sit_i->dirty_sentries)
goto out;
/*
* add and account sit entries of dirty bitmap in sit entry
* set temporarily
*/
// 将dirty sentry entry放入到set中
// 修改过的sit entry都放入到dirty_sentries_bitmap中
add_sits_in_set(sbi);
/*
* if there are no enough space in journal to store dirty sit
* entries, remove all entries from journal and add and account
* them in sit entry set.
*/
// 如果journal没有足够的空间,则将journal放到set中,并清空journal的内容
if (!__has_cursum_space(journal, sit_i->dirty_sentries, SIT_JOURNAL) ||
!to_journal)
remove_sits_in_journal(sbi);
/*
* there are two steps to flush sit entries:
* #1, flush sit entries to journal in current cold data summary block.
* #2, flush sit entries to sit page.
*/
list_for_each_entry_safe(ses, tmp, head, set_list) {
...
// 如果本次要写journal,但是空间不足,则直接落盘
if (to_journal &&
!__has_cursum_space(journal, ses->entry_cnt, SIT_JOURNAL))
to_journal = false;
/* flush dirty sit entries in region of current sit set */
// 遍历bitmap,获取脏sit entry,并写入journal或者存储介质
for_each_set_bit_from(segno, bitmap, end) {
int offset, sit_offset;
se = get_seg_entry(sbi, segno);
/* add discard candidates */
if (!(cpc->reason & CP_DISCARD)) {
cpc->trim_start = segno;
add_discard_addrs(sbi, cpc, false);
}
if (to_journal) {
// 找到可以写本次journal的偏移,如果journal中有相应的记录,
// 修改对应的位置
offset = f2fs_lookup_journal_in_cursum(journal,
SIT_JOURNAL, segno, 1);
segno_in_journal(journal, offset) =
cpu_to_le32(segno);
seg_info_to_raw_sit(se,
&sit_in_journal(journal, offset));
} else {
// 直接写到sit表中(前面获取sit页的时候已经写过了???)
sit_offset = SIT_ENTRY_OFFSET(sit_i, segno);
seg_info_to_raw_sit(se,
&raw_sit->entries[sit_offset]);
}
}
}
...
}
SIT缓存的下刷与NAT类似,将脏的SIT表项放入到sit_entry_set集合中,遍历该集合,如果journal中有空间,则存放在journal中,否则直接下刷到SIT区域。不同的是,SIT对应的是CURSEG_COLD_DATA类型的journal。
do_checkpoint
static int do_checkpoint(struct f2fs_sb_info *sbi, struct cp_control *cpc)
{
...
/* Flush all the NAT/SIT pages */
f2fs_sync_meta_pages(sbi, META, LONG_MAX, FS_CP_META_IO);
...
start_blk = __start_cp_next_addr(sbi);
/* write nat bits */
if ((cpc->reason & CP_UMOUNT) &&
is_set_ckpt_flags(sbi, CP_NAT_BITS_FLAG)) {
__u64 cp_ver = cur_cp_version(ckpt);
block_t blk;
cp_ver |= ((__u64)crc32 << 32);
*(__le64 *)nm_i->nat_bits = cpu_to_le64(cp_ver);
blk = start_blk + sbi->blocks_per_seg - nm_i->nat_bits_blocks;
for (i = 0; i < nm_i->nat_bits_blocks; i++)
f2fs_update_meta_page(sbi, nm_i->nat_bits +
(i << F2FS_BLKSIZE_BITS), blk + i);
}
/* write out checkpoint buffer at block 0 */
f2fs_update_meta_page(sbi, ckpt, start_blk++);
for (i = 1; i < 1 + cp_payload_blks; i++)
f2fs_update_meta_page(sbi, (char *)ckpt + i * F2FS_BLKSIZE,
start_blk++);
if (orphan_num) {
write_orphan_inodes(sbi, start_blk);
start_blk += orphan_blocks;
}
f2fs_write_data_summaries(sbi, start_blk);
start_blk += data_sum_blocks;
...
/* flush all device cache */
err = f2fs_flush_device_cache(sbi);
if (err)
return err;
/* barrier and flush checkpoint cp pack 2 page if it can */
commit_checkpoint(sbi, ckpt, start_blk);
...
}
该接口是才是最终写CP区域的,其可以总结为如下步骤:
- 等待NAT/SIT的修改落盘。
- 将文件系统状态信息填入CP pack #1中,包括各个curseg的segno号、next_blkoff、分配策略、SIT/NAT bitmap等等,最终计算CP pack #1的checksum。
- 计算CP的起始地址,由于CP区域存在主备区,采用“乒乓”策略写入。
- 如果支持NAT_BITS,将nat_bits写入CP区域的结尾处。
- 在CP区域开头处写入CP pack #1的block,然后写入填充内容(cp_payload)。
- 如果存在“孤儿”节点,需要在其后写入孤儿节点的内容。
- 写入data的summary,如果允许压缩(只写有实际内容的SSA表项),还需要将压缩后的数据写入。
- 写入node的summary,不支持压缩。
- 等待上述的数据真正落盘后,才最终写入CP pack #2。(注:如果不等待,可能出现CP pack #1和#2都已经落盘,但其他元数据还没有落盘,这是后发生宕机,那么其他数据是受损的。在写入CP pack #2前等待,可以保证所有元数据已经落盘,即使宕机,由于CP pack #2还没写入,最终该CP会被丢弃。)、
以上是一次写CP的完整流程,CP区域的具体内容参考如下:
f2fs的CP布局
如上就是一个CP的具体布局,分为主备两个segment。每次轮流使用主备区域,确保总有一个区域的内容是可用的。
总结
本章节介绍了f2fs一致性中的后滚恢复部分,虽然只是介绍了写CP的流程,但是读操作只是写操作的反向流程,就不过多赘述。