F2FS之write_checkpoint

F2FS需要在特定的时刻将Checkpoint的数据写入到磁盘中。

Checkpoint的时机

CP是一个开销很大的操作,因此合理选取CP时机,能够很好地提高性能。CP的触发时机有:
前台GC(FG_GC) FASTBOOT UMOUNT DISCARD RECOVERY TRIM 周期进行
因此F2FS有几个宏表示CP的触发原因:

#define	CP_UMOUNT	0x00000001
#define	CP_FASTBOOT	0x00000002
#define	CP_SYNC		0x00000004
#define	CP_RECOVERY	0x00000008
#define	CP_DISCARD	0x00000010
#define CP_TRIMMED	0x00000020

大部分情况下,都是触发 CP_SYNC 这个宏的CP。

Checkpoint的核心流程

最重要的函数就是write_checkpoint:

void write_checkpoint(struct f2fs_sb_info *sbi, bool blocked, bool is_umount)
{
	struct f2fs_checkpoint *ckpt = F2FS_CKPT(sbi);//从sbi读取当前CP的数据结构
	unsigned long long ckpt_ver;

	if (!blocked) {
		mutex_lock(&sbi->cp_mutex);
		block_operations(sbi);//将文件系统的所有操作都停止
	}
 	//将暂存的所有BIO刷写到磁盘,按照按照 DATA -> NODE -> META 的顺序将数据从 page 同步写入到磁盘(META 包括了
	//SIT/NAT/checkpoint):
	f2fs_submit_bio(sbi, DATA, true);
	f2fs_submit_bio(sbi, NODE, true);
	f2fs_submit_bio(sbi, META, true);

	/*
	 * update checkpoint pack index
	 * Increase the version number so that
	 * SIT entries and seg summaries are written at correct place
	 *///完成数据的写入后更新 checkpoint_ver
	ckpt_ver = le64_to_cpu(ckpt->checkpoint_ver);
	ckpt->checkpoint_ver = cpu_to_le64(++ckpt_ver);//检查点版本号加一
	//再将 nat/sit 从 cache 写入 page(还未落盘) 1.如果journal没满,则写入journal中。2.如果journal满了,则将journal中的nat表项,然后写入磁盘nat的page cache
	/* write cached NAT/SIT entries to NAT/SIT area */
	flush_nat_entries(sbi);// \
	flush_sit_entries(sbi);

	reset_victim_segmap(sbi);
	//更新元数据的Checkpoint区域以及Summary区域
	/* unlock all the fs_lock[] in do_checkpoint() */
	do_checkpoint(sbi, is_umount);// checkpoint核心流程

	unblock_operations(sbi);//恢复文件系统的操作
	mutex_unlock(&sbi->cp_mutex);
}

该函数完成如下工作:
1.按照 DATA -> NODE -> META 的顺序将数据从 page 同步写入到磁盘(META 包括了
SIT/NAT/checkpoint):
f2fs_submit_bio(sbi, DATA, true) ;
f2fs_submit_bio(sbi, NODE, true) ;
f2fs_submit_bio(sbi, META, true) ;
需要注意的是, 此时写入磁盘的 meta 并非最新的 meta(FIX_ME:在多数情况下, 此处并无 meta 可写,个
人觉得仅仅有可能是在此之前使用 fsync/fdatasync时触发了 checkpoint事件, meta 尚未完全写入磁盘时
起到阻塞的作用), 最新的 meta 还只保存在 cache 中.
2.完成数据的写入后更新 checkpoint_ver:
ckpt_ver=cur_cp_version(ckpt) ;
ckpt->checkpoint_ver= cpu_to_le64(++ckpt_ver) ;
3.再将 nat/sit 从 cache 写入 page
flush_nat_entries(sbi) ;
flush_sit_entries(sbi) ;
4.完成上述工作后调用 do_checkpoint()进行实际的 checkpoint, 即将最新的元数据写入
磁盘.
voiddo_checkpoint(struct f2fs_sb_info *sbi, boolis_umount) (fs/f2fs/checkpoint.c) 完成如
下工作:
1.将 nat/sitpage 写入 disk
while(get_pages(sbi, F2FS_DIRTY_META) )
sync_meta_pages(sbi, META, LONG_MAX) ;
2.更新 checkpoint 中的元数据
3.将 checkpoint block 写入 page
4.将 orphan blocks 写入 page
5.将 SAT 写入 page
6.将上述操作写入 disk
下面的示意图概述了在进行 checkpoint 时所发生的内容
F2FS Checkpoint Mechanisim

这个函数的逻辑还是比较清晰的,所以我们开始详细介绍其中调用的子函数。

1.block_operations(sbi)

这个函数 为检查点冻结所有FS操作

void block_operations(struct f2fs_sb_info *sbi)
{
	int t;
	struct writeback_control wbc = {
		.sync_mode = WB_SYNC_ALL,
		.nr_to_write = LONG_MAX,
		.for_reclaim = 0,
	};

	/* Stop renaming operation */
	mutex_lock_op(sbi, RENAME);
	mutex_lock_op(sbi, DENTRY_OPS);

retry_dents:
	/* write all the dirty dentry pages */
	sync_dirty_dir_inodes(sbi);

	mutex_lock_op(sbi, DATA_WRITE);
	if (get_pages(sbi, F2FS_DIRTY_DENTS)) {//如果脏dentry blocks数目不为0
		mutex_unlock_op(sbi, DATA_WRITE);
		goto retry_dents;
	}

	/* block all the operations */
	for (t = DATA_NEW; t <= NODE_TRUNC; t++)
		mutex_lock_op(sbi, t);

	mutex_lock(&sbi->write_inode);

	/*
	 * POR: we should ensure that there is no dirty node pages
	 * until finishing nat/sit flush.
	 POR:在完成nat / sit刷新之前,我们应确保没有脏node页面
	 */
retry:
	sync_node_pages(sbi, 0, &wbc);

	mutex_lock_op(sbi, NODE_WRITE);

	if (get_pages(sbi, F2FS_DIRTY_NODES)) {
		mutex_unlock_op(sbi, NODE_WRITE);
		goto retry;
	}
	mutex_unlock(&sbi->write_inode);
}

首先将所有dentry相关的ditry pages同步写回,这个写回过程要先进行f2fs_lock_all(sbi)操作,我们发现,此过程结束的条件是无F2FS_DIRTY_DENTS, 但是结束时并没有释放锁,即没有f2fs_unlock_all(sbi).

2.f2fs_submit_bio(sbi, DATA, true)

然后就是暂存BIO的回写,一般情况下,文件系统与设备的交互的开销是比较大的,因此一些文件系统为了减少交互的开销,都会尽可能将更多的page合并在一个bio中,再提交到设备,进而减少交互的次数。F2FS中,在sbi中使用了struct f2fs_bio_info结构用于减少交互次数,它的核心是缓存一个bio,将即将回写的page都保存到这个bio中,等到bio尽可能满再回写进入磁盘。它在sbi的声明如下:

struct f2fs_sb_info {
	...
	struct f2fs_bio_info *write_io[NR_PAGE_TYPE]; // NR_PAGE_TYPE表示HOW/WARM/COLD不同类型的数据
	...
}

未完待续

3.flush_nat_entries(sbi)

将暂存在dirty_nat_entries的脏nat entry都回写到Journal或磁盘当中:
修改node的信息会对对应的nat_entry进行修改,同时nat_entry会被设置为脏, 加入到nm_i->nat_set_root的radix tree和nm_i->dirty_nat_entries中。Checkpoint会对脏的nat_entry进行回写,完成元数据的更新。该函数的工作:1.如果journal没满,则写入journal中。2.如果journal满了,则将journal中的nat表项,然后写入磁盘nat的page cache。
注意:nat journal存在HOT DATA的summary block中,而sit journal存在COLD DATA的summary block中。
下面下列出这个函数,然后再解释。

void flush_nat_entries(struct f2fs_sb_info *sbi)
{
	struct f2fs_nm_info *nm_i = NM_I(sbi);
	struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_HOT_DATA);//正在写的热数据段
	struct f2fs_summary_block *sum = curseg->sum_blk;//活动segment的summary block
	struct list_head *cur, *n;
	struct page *page = NULL;
	struct f2fs_nat_block *nat_blk = NULL;
	nid_t start_nid = 0, end_nid = 0;
	bool flushed;

	flushed = flush_nats_in_journal(sbi);//flushed=true代表journal已满(刷到脏链表),否则代表没满(无操作)

	if (!flushed)
		mutex_lock(&curseg->curseg_mutex);

	/* 1) flush dirty nat caches */
	list_for_each_safe(cur, n, &nm_i->dirty_nat_entries) {//对于dirty_nat_entries中的每一项
		struct nat_entry *ne;
		nid_t nid;
		struct f2fs_nat_entry raw_ne;
		int offset = -1;
		block_t new_blkaddr;

		ne = list_entry(cur, struct nat_entry, list);//脏nat_entry
		nid = nat_get_nid(ne);//获得脏nat_entry的nid

		if (nat_get_blkaddr(ne) == NEW_ADDR)
			continue;
		if (flushed)//如果journal中已经满了,并且已经由flush_nats_in_journal刷到脏缓存里
			goto to_nat_page;
		//如果还有空
		/* if there is room for nat enries in curseg->sumpage */
		offset = lookup_journal_in_cursum(sum, NAT_JOURNAL, nid, 1);//在journal中查找nid,返回数组下标
		if (offset >= 0) {//如果找到了,则将其赋值给raw_ne
			raw_ne = nat_in_journal(sum, offset);
			goto flush_now;
		}
to_nat_page://如果journal中已经满了,并且已经由flush_nats_in_journal刷到脏缓存里
		if (!page || (start_nid > nid || nid > end_nid)) {
			if (page) {
				f2fs_put_page(page, 1);
				page = NULL;
			}
			start_nid = START_NID(nid);//nid所在的nat block的第一个nid号
			end_nid = start_nid + NAT_ENTRY_PER_BLOCK - 1;//nid所在的nat block的最后一个nid号

			/*
			 * get nat block with dirty flag, increased reference
			 * count, mapped and lock
			 */
			page = get_next_nat_page(sbi, start_nid);
			nat_blk = page_address(page);//从磁盘中读取出nid所属的nid block
		}

		BUG_ON(!nat_blk);
		raw_ne = nat_blk->entries[nid - start_nid];//获得nid所属的f2fs_nat_entry
flush_now:
		new_blkaddr = nat_get_blkaddr(ne);//读取脏nat_entry的新地址(修改过后)
		//将脏nat_entry中的数据刷到磁盘f2fs_nat_entry
		raw_ne.ino = cpu_to_le32(nat_get_ino(ne));
		raw_ne.block_addr = cpu_to_le32(new_blkaddr);
		raw_ne.version = nat_get_version(ne);

		if (offset < 0) {//offset =-1说明journal已经满了,raw_ne是从磁盘上读出来的
			nat_blk->entries[nid - start_nid] = raw_ne;
		} else {//offset>=0则说明journal没满,raw_ne是从journal中读出来的
			nat_in_journal(sum, offset) = raw_ne;//给journal表项offset处赋值
			nid_in_journal(sum, offset) = cpu_to_le32(nid);
		}
		/*缓存处理,如果nat地址不为null则将表项从脏链表移除*/
		if (nat_get_blkaddr(ne) == NULL_ADDR) {//如果地址为null,则将表项从缓存中删除
			write_lock(&nm_i->nat_tree_lock);
			__del_from_nat_cache(nm_i, ne);
			write_unlock(&nm_i->nat_tree_lock);

			/* We can reuse this freed nid at this point */
			add_free_nid(NM_I(sbi), nid);
		} else {
			write_lock(&nm_i->nat_tree_lock);
			__clear_nat_cache_dirty(nm_i, ne);
			ne->checkpointed = true;//持久化的表项要进行checkpoint标签
			write_unlock(&nm_i->nat_tree_lock);
		}
	}
	if (!flushed)
		mutex_unlock(&curseg->curseg_mutex);
	f2fs_put_page(page, 1);

	/* 2) shrink nat caches if necessary *///如果缓存的nat表项超过2 * NM_WOUT_THRESHOLD,则从表头删除nm_i->nat_cnt - NM_WOUT_THRESHOLD个
	try_to_free_nats(sbi, nm_i->nat_cnt - NM_WOUT_THRESHOLD);
}

这里讨论两种情况:
① 调用之前journal没满,则flushed=false,对于dirty_nat_entries中的每一项,这时offset>=0,从journal中找每一个脏项的nid的地方,将其写到journal中。如果在循环的过程中journal都没有被写满,则循环结束后即可。假设在循环的过程中journal被写满了,则offset=-1,则从磁盘中读取出nid所属的nid block,将剩余的脏项都循环写入对应的磁盘nat block中,直至循环结束。
②在调用之前journal就满了,则调用flush_nats_in_journal(sbi)将journal中的nat表项统统刷到和nm_i->dirty_nat_entries中,则flushed=trueoffset=-1,则从磁盘中读取出nid所属的nid block,将剩余的脏项都循环写入对应的磁盘nat block中,直至循环结束。
其中关于具体读写的函数如get_next_nat_page,由于涉及磁盘布局,在此不便详述,请参考本人另外一篇博客https://blog.csdn.net/wenj12/article/details/115414147
下面介绍一下被调用的flush_nats_in_journal(sbi)

static bool flush_nats_in_journal(struct f2fs_sb_info *sbi)
{
	struct f2fs_nm_info *nm_i = NM_I(sbi);
	struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_HOT_DATA);
	struct f2fs_summary_block *sum = curseg->sum_blk;
	int i;

	mutex_lock(&curseg->curseg_mutex);

	if (nats_in_cursum(sum) < NAT_JOURNAL_ENTRIES) {//如果活跃segment的journal的nat表项没满,直接返回
		mutex_unlock(&curseg->curseg_mutex);
		return false;
	}
	/*对于nat journal中的每一项f2fs_nat_entry,将其从journal中刷到nat缓存的脏链表中*/
	for (i = 0; i < nats_in_cursum(sum); i++) {
		struct nat_entry *ne;
		struct f2fs_nat_entry raw_ne;
		nid_t nid = le32_to_cpu(nid_in_journal(sum, i));//nat journal中第i项的nid

		raw_ne = nat_in_journal(sum, i);//nat journal 中第i项的f2fs_nat_entry
retry:
		write_lock(&nm_i->nat_tree_lock);
		ne = __lookup_nat_cache(nm_i, nid);//在nat缓存中查找上述nat表项的nat_entry
		if (ne) {//如果找到了
			__set_nat_cache_dirty(nm_i, ne);//将该表项添加到f2fs_nm_info的dirty_nat_entires
			write_unlock(&nm_i->nat_tree_lock);
			continue;
		}//如果没找到
		ne = grab_nat_entry(nm_i, nid);//创建一个新的nat_entry
		if (!ne) {
			write_unlock(&nm_i->nat_tree_lock);
			goto retry;
		}/*使用journal中的f2fs_nat_entry给新创建的nat_entry赋值*/
		nat_set_blkaddr(ne, le32_to_cpu(raw_ne.block_addr));
		nat_set_ino(ne, le32_to_cpu(raw_ne.ino));
		nat_set_version(ne, raw_ne.version);
		__set_nat_cache_dirty(nm_i, ne);//将nat_entry插到脏链表
		write_unlock(&nm_i->nat_tree_lock);
	}
	update_nats_in_cursum(sum, -i);//刷走多少nat就修改多少
	mutex_unlock(&curseg->curseg_mutex);
	return true;
}

如果curseg的nat journal满了,则将journal中所有表项刷到nat 脏链表中(在nat tree中找不到的需创建),如果没满则返回false

4.flush_sit_entries(sbi)

主要过程跟 f2fs_flush_nat_entries 类似,将dirty的seg_entry刷写到journal或sit block中,该函数的作用:1.如果journal没满,则将脏seg_entry写到COLD_DATA的journal中。2.如果journal满了,则先调用flush_sits_in_journal将根据journal中的每一项将sbi中dirty_sentries_bitmap置位,然后将超级块的脏sit entry位图中的每一条seg_entry写入磁盘f2fs_sit_blockf2fs_sit_entry

void flush_sit_entries(struct f2fs_sb_info *sbi)
{
	struct sit_info *sit_i = SIT_I(sbi);
	unsigned long *bitmap = sit_i->dirty_sentries_bitmap;//超级块的脏sit entry位图
	struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_COLD_DATA);//sit journal存在COLD DATA
	struct f2fs_summary_block *sum = curseg->sum_blk;//COLD_DATA的summary block
	unsigned long nsegs = TOTAL_SEGS(sbi);
	struct page *page = NULL;
	struct f2fs_sit_block *raw_sit = NULL;
	unsigned int start = 0, end = 0;
	unsigned int segno = -1;
	bool flushed;

	mutex_lock(&curseg->curseg_mutex);
	mutex_lock(&sit_i->sentry_lock);

	/*
	 * "flushed" indicates whether sit entries in journal are flushed
	 * to the SIT area or not.
	 */
	flushed = flush_sits_in_journal(sbi);//如果journal满了,则返回1,没满返回0

	while ((segno = find_next_bit(bitmap, nsegs, segno + 1)) < nsegs) {//对于超级块的脏sit entry位图中的每一条seg_entry
		struct seg_entry *se = get_seg_entry(sbi, segno);//从sit条目缓存中读取段号为segno的sit条目的seg_entry
		int sit_offset, offset;

		sit_offset = SIT_ENTRY_OFFSET(sit_i, segno);

		if (flushed)
			goto to_sit_page;
		//flushed=false
		offset = lookup_journal_in_cursum(sum, SIT_JOURNAL, segno, 1);//在journal中找到segno所在的位置
		if (offset >= 0) {
			segno_in_journal(sum, offset) = cpu_to_le32(segno);//将seg_entry写入到journal中
			seg_info_to_raw_sit(se, &sit_in_journal(sum, offset));
			goto flush_done;
		}
to_sit_page:
		if (!page || (start > segno) || (segno > end)) {
			if (page) {
				f2fs_put_page(page, 1);
				page = NULL;
			}

			start = START_SEGNO(sit_i, segno);//我估计这个函数是获得segno的f2fs_sit_entry所在的f2fs_sit_block的起始f2fs_sit_entry的段号
			end = start + SIT_ENTRY_PER_BLOCK - 1;

			/* read sit block that will be updated */
			page = get_next_sit_page(sbi, start);//将segno所在的f2fs_sit_block从磁盘里读出来
			raw_sit = page_address(page);//segno所在的f2fs_sit_block
		}

		/* udpate entry in SIT block */
		seg_info_to_raw_sit(se, &raw_sit->entries[sit_offset]);//向磁盘f2fs_sit_block赋值segno的f2fs_sit_entry
flush_done:
		__clear_bit(segno, bitmap);//清除segno的脏位
		sit_i->dirty_sentries--;//脏seg_entry数-1
	}
	mutex_unlock(&sit_i->sentry_lock);
	mutex_unlock(&curseg->curseg_mutex);

	/* writeout last modified SIT block */
	f2fs_put_page(page, 1);

	set_prefree_as_free_segments(sbi);//将prefree位图中的segment设置为free segment
}

这里讨论两种情况:
① 调用之前journal没满,则flushed=0,对于sit_i->dirty_sentries_bitmap中的每一脏seg_entry,这时offset>=0,从journal中找每一个脏项的segno的地方,将其写到journal中。如果在循环的过程中journal都没有被写满,则循环结束后即可。假设在循环的过程中journal被写满了,则offset=-1,则从磁盘中读取出segno所属的f2fs_sit_block,将剩余的脏项都循环写入对应的磁盘f2fs_sit_block中,直至循环结束。
②在调用之前journal就满了,则调用flush_sits_in_journal(sbi)根据journal中的sit表项sit_i->dirty_sentries_bitmap置位,则flushed=1且,则从磁盘中读取出segno所属的f2fs_sit_block,将剩余的脏项都循环写入对应的磁盘f2fs_sit_block中,直至循环结束。
其中关于具体读写的函数如get_next_sit_page,由于涉及磁盘布局,在此不便详述,请参考本人另外一篇博客https://blog.csdn.net/wenj12/article/details/115414147
下面介绍一下被调用的flush_sit_entries
如果当前summary中的journal区域中充满了sit entries,根据journal中的每一项将sbi中dirty_sentries_bitmap置位。

static bool flush_sits_in_journal(struct f2fs_sb_info *sbi)
{
	struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_COLD_DATA);
	struct f2fs_summary_block *sum = curseg->sum_blk;//SIT journal存在活动COLD_DATA段的summary block
	int i;

	/*
	 * If the journal area in the current summary is full of sit entries,
	 * all the sit entries will be flushed. Otherwise the sit entries
	 * are not able to replace with newly hot sit entries.
	 * 如果当前summary中的journal区域中充满了sit entries,则将刷新所有sit entries。 
	 * 否则,sit entries将无法替换为新的热sit entries。
	 */
	if (sits_in_cursum(sum) >= SIT_JOURNAL_ENTRIES) {//如果journal满了
		for (i = sits_in_cursum(sum) - 1; i >= 0; i--) {//根据journal中的每一项将sbi中dirty_sentries_bitmap置位
			unsigned int segno;
			segno = le32_to_cpu(segno_in_journal(sum, i));
			__mark_sit_entry_dirty(sbi, segno);
		}
		update_sits_in_cursum(sum, -sits_in_cursum(sum));//将journal的计数清零
		return 1;
	}
	return 0;//没满,则无操作
}

5.do_checkpoint(sbi, is_umount)

do_checkpoint是针对Checkpoint区域的更新。Checkpoint主要涉及两部分,第一部分f2fs_checkpoint结构的更新,第二部分是curseg的summary数据的回写。在分析这个函数之前,需要知道元数据的Checkpoint区域在磁盘中是如何保存的,磁盘的保存结构如下:

             +---------------------------------------------------------------------------------------------------+
             | f2fs_checkpoint | data summaries | hot node summaries | warm node summaries | cold node summaries |
             +---------------------------------------------------------------------------------------------------+
                              .                 .             
                       .                                   .               
                 .                 compacted summaries                 .        
                 +----------------+-------------------+----------------+
                 |  nat journal   |    sit journal    | data summaries |
                 +----------------+-------------------+----------------+

                 .                  normal summaries                   .        
                 +----------------+-------------------+----------------+
                 |                    data summaries                   |
                 +----------------+-------------------+----------------+

其中f2fs_checkpoint、hot/warm/cold node summaries都分别占用一个block的空间。f2fs为了减少Checkpoint的写入开销,将data summaries被设计为可变的。它包含两种写入方式,一种是compacted summaries写入,另一种是normal summaries写入。compacted summaries可以在一次Checkpoint中,减少1~2个page的写入。

do_checkpoint函数

do_checkpoint()进行实际的 checkpoint, 即将最新的元数据写入磁盘
1.将 nat/sitpage 写入 disk

while(get_pages(sbi, F2FS_DIRTY_META) )
	sync_meta_pages(sbi, META, LONG_MAX) ;

2.更新 checkpoint 中的元数据
3.将 checkpoint block 写入 page
4.将 orphan blocks 写入 page
5.将 SAT 写入 page
6.将上述操作写入 disk

static void do_checkpoint(struct f2fs_sb_info *sbi, bool is_umount)
{
	struct f2fs_checkpoint *ckpt = F2FS_CKPT(sbi);
	nid_t last_nid = 0;
	block_t start_blk;
	struct page *cp_page;
	unsigned int data_sum_blocks, orphan_blocks;
	unsigned int crc32 = 0;
	void *kaddr;
	int i;

	/* Flush all the NAT/SIT pages */
	while (get_pages(sbi, F2FS_DIRTY_META))//将 nat/sitpage 写入 disk
		sync_meta_pages(sbi, META, LONG_MAX);

	next_free_nid(sbi, &last_nid);//获取一个空的nid号

	/*
	 * modify checkpoint
	 * version number is already updated
	 *	更新 checkpoint 中的元数据
	 */
	ckpt->elapsed_time = cpu_to_le64(get_mtime(sbi));
	ckpt->valid_block_count = cpu_to_le64(valid_user_blocks(sbi));
	ckpt->free_segment_count = cpu_to_le32(free_segments(sbi));
	for (i = 0; i < 3; i++) {
		ckpt->cur_node_segno[i] =
			cpu_to_le32(curseg_segno(sbi, i + CURSEG_HOT_NODE));
		ckpt->cur_node_blkoff[i] =
			cpu_to_le16(curseg_blkoff(sbi, i + CURSEG_HOT_NODE));
		ckpt->alloc_type[i + CURSEG_HOT_NODE] =
				curseg_alloc_type(sbi, i + CURSEG_HOT_NODE);
	}
	for (i = 0; i < 3; i++) {
		ckpt->cur_data_segno[i] =
			cpu_to_le32(curseg_segno(sbi, i + CURSEG_HOT_DATA));
		ckpt->cur_data_blkoff[i] =
			cpu_to_le16(curseg_blkoff(sbi, i + CURSEG_HOT_DATA));
		ckpt->alloc_type[i + CURSEG_HOT_DATA] =
				curseg_alloc_type(sbi, i + CURSEG_HOT_DATA);
	}

	ckpt->valid_node_count = cpu_to_le32(valid_node_count(sbi));
	ckpt->valid_inode_count = cpu_to_le32(valid_inode_count(sbi));
	ckpt->next_free_nid = cpu_to_le32(last_nid);
	
	/*开始刷 2 cp  + n data seg summary + orphan inode blocks  */
	// 根据需要回写的summary的数目,返回需要写回的block的数目,返回值有1、2、3
	data_sum_blocks = npages_for_summary_flush(sbi);
	// 如果data_sum_blocks = 1 或者 2,则表示回写1个或者2个block,则设置CP_COMPACT_SUM_FLAG标志
	if (data_sum_blocks < 3)
		set_ckpt_flags(ckpt, CP_COMPACT_SUM_FLAG);
	else
		clear_ckpt_flags(ckpt, CP_COMPACT_SUM_FLAG);

	orphan_blocks = (sbi->n_orphans + F2FS_ORPHANS_PER_BLOCK - 1)
					/ F2FS_ORPHANS_PER_BLOCK;
	ckpt->cp_pack_start_sum = cpu_to_le32(1 + orphan_blocks);

	if (is_umount) {
		set_ckpt_flags(ckpt, CP_UMOUNT_FLAG);
		ckpt->cp_pack_total_block_count = cpu_to_le32(2 +
			data_sum_blocks + orphan_blocks + NR_CURSEG_NODE_TYPE);
	} else {
		clear_ckpt_flags(ckpt, CP_UMOUNT_FLAG);
		ckpt->cp_pack_total_block_count = cpu_to_le32(2 +
			data_sum_blocks + orphan_blocks);
	}

	if (sbi->n_orphans)
		set_ckpt_flags(ckpt, CP_ORPHAN_PRESENT_FLAG);
	else
		clear_ckpt_flags(ckpt, CP_ORPHAN_PRESENT_FLAG);

	/* update SIT/NAT bitmap */
	get_sit_bitmap(sbi, __bitmap_ptr(sbi, SIT_BITMAP));
	get_nat_bitmap(sbi, __bitmap_ptr(sbi, NAT_BITMAP));

	crc32 = f2fs_crc32(ckpt, le32_to_cpu(ckpt->checksum_offset));
	*(__le32 *)((unsigned char *)ckpt +
				le32_to_cpu(ckpt->checksum_offset))
				= cpu_to_le32(crc32);

	start_blk = __start_cp_addr(sbi);//获取磁盘检查点的起始块地址

	/* write out checkpoint buffer at block 0 */
	cp_page = grab_meta_page(sbi, start_blk++);//注意,start_blk使用后才+1
	kaddr = page_address(cp_page);
	memcpy(kaddr, ckpt, (1 << sbi->log_blocksize));//将内存ckpt复制到磁盘checkpoint缓存页中
	set_page_dirty(cp_page);
	f2fs_put_page(cp_page, 1);

	if (sbi->n_orphans) {//此时的start_blk已经+1,为orphan BLK区域起始块
		write_orphan_inodes(sbi, start_blk);//写入orphan BLK
		start_blk += orphan_blocks;
	}
	//此时到了写summary的块地址start_blk
	write_data_summaries(sbi, start_blk); // 将data summary以及里面的journal写入磁盘
	/* 
	 * node summaries的写回只有在启动和关闭F2FS的时候才会执行,
	 * 如果出现的宕机的情况下,就会失去了UMOUNT的标志,也会失去了所有的NODE SUMMARY
	 * F2FS会进行根据上次checkpoint的情况进行恢复
	 */
	start_blk += data_sum_blocks;
	if (is_umount) {
		write_node_summaries(sbi, start_blk);// 将node summary以及里面的journal写入磁盘
		start_blk += NR_CURSEG_NODE_TYPE;
	}

	/* writeout checkpoint block *///写第二个f2fs_checkpoint
	cp_page = grab_meta_page(sbi, start_blk);
	kaddr = page_address(cp_page);
	memcpy(kaddr, ckpt, (1 << sbi->log_blocksize));
	set_page_dirty(cp_page);
	f2fs_put_page(cp_page, 1);

	/* wait for previous submitted node/meta pages writeback */
	while (get_pages(sbi, F2FS_WRITEBACK))
		congestion_wait(BLK_RW_ASYNC, HZ / 50);

	filemap_fdatawait_range(sbi->node_inode->i_mapping, 0, LONG_MAX);
	filemap_fdatawait_range(sbi->meta_inode->i_mapping, 0, LONG_MAX);

	/* update user_block_counts */
	sbi->last_valid_block_count = sbi->total_valid_block_count;
	sbi->alloc_valid_block_count = 0;

	/* Here, we only have one bio having CP pack */
	if (is_set_ckpt_flags(ckpt, CP_ERROR_FLAG))
		sbi->sb->s_flags |= MS_RDONLY;
	else
		sync_meta_pages(sbi, META_FLUSH, LONG_MAX);

	clear_prefree_segments(sbi);//对prefree位图中的每个置位的segment,向设备发送discard命令
	F2FS_RESET_SB_DIRT(sbi);
}

该函数调用函数比较多,我们来一一分析一下:
首先调用sync_meta_pages(sbi, META, LONG_MAX)来把第3第4步中刷到NAT和SIT磁盘缓存的数据同步到磁盘对应的块中。
然后使用sbi中的数据更新 f2fs_checkpoint 中的元数据,cur_node_segno[MAX_ACTIVE_NODE_LOGS]cur_data_segno[MAX_ACTIVE_DATA_LOGS]表示node和data当前的Active Segment的编号(segment number, segno),系统可以通过这个编号找到对应的segment。MAX_ACTIVE_NODE_LOGS以及MAX_ACTIVE_NODE_LOGS分别表示data和node有多少种类型,F2FS默认情况下都等于3,表示、、即HOT、WARM、COLD类型数据。cur_node_blkoff[MAX_ACTIVE_NODE_LOGS]以及cur_data_blkoff[MAX_ACTIVE_DATA_LOGS]则分别表示当前active segment分配到哪一个block(一个segment包含了512个block)。

接下来重点讨论,Checkpoint区域的summary的回写,在分析流程之前,需要分析compacted summaries和normal summaries的差别。

compacted summaries和normal summaries 通过查看curseg的结构可以知道,curseg管理了(NODE,DATA) X (HOT,WARM,COLD)总共6个的segment,因此也需要管理这6个segment对应的f2fs_summary_block。

因此一般情况下,每一次checkpoint时候,应该需要回写6种类型的f2fs_summary_block,即6个block到磁盘。

为了减少这部分回写的开销,f2fs针对DATA类型f2fs_summary_block设计了一种compacted summary block。一般情况下,DATA需要回写3个f2fs_summary_block到磁盘(HOT,WARM,COLD),但是如果使用了compacted summary block,大部分情况下只需要回写1~2个block。

compacted summary block被设计为通过1~2个block保存当前curseg所有的元信息,它的核心设计是将HOW WARM COLD DATA的元信息混合保存:

混合类型Journal保存 compacted summary block分别维护了一个公用的nat journal,以及sit journal,HOT WARM COLD类型的Journal都会混合保存进入两个journal结构中。
在满足COMPACTED的条件下,系统启动时,F2FS会从磁盘中读取这两个Journal到内存中,分别保存在HOT以及COLD所在的curseg->journal中。
不同类型的journal会在CP时刻,通过f2fs_flush_sit_entries函数写入到HOT或者COLD对应的curseg->journal区域中。如果HOT或者COLD对应的curseg->journal区域的空间不够了,就将不同类型的journal保存的segment的信息,直接写入到对应的sit entry block中。
接下来将HOT或者COLD对应的curseg->journal包装为compacted block回写到cp区域中。

混合类型Summary保存 也将HOT,WARM,COLD三种类型的summary和journal保存在同一个data summary block中。
它们的差别如下:

compacted summary block (4KB)
+------------------+
|nat journal       |存在活动HOT DATA的summary block的journal中
|sit journal       |存在活动COLD DATA的summary block的journal中
|data sum[439]     | data summaries数组大小是439
+------------------+
|                  | 如果需要,会接上一个纯summary数组的block
| data sum[584]    | data summaries数组大小是584
|                  |
+------------------+


normal summary block,表示三种类型的DATA的summary
+--------------------+
|hot data journal    |
|hot data summaries  | data summaries数组大小是512
|                    |
+--------------------+
|warm data journal   |
|warm data summaries | data summaries数组大小是512
|                    |
+--------------------+
|cold data journal   |
|cold data summaries | data summaries数组大小是512
|                    |
+--------------------+

也就是说对于类型,根据不同类型的summary block的可以保存的summary的大小,可以得到 HOT,WARM,COLD DATA这三种类型,如果目前加起来仅使用了:
1少于439的block(只修改了439个f2fs_summary),那么可以通过compacted回写方式进行回写,即通过一个compacted summary block完成回写,需要回写1个block。
2大于439,少于439+584=1023个block,那么可以通过compacted回写方式进行回写,即可以通过compacted summary block加一个纯summary block的方式保存所有信息,需要回写2个block。
3大于1023的情况下,即和normal summary block同样的回写情况,那么就会使用normal summary block的回写方式完成回写,即回写3个block。(因为大于1023情况下,如果继续使用compacted回写,最差的情况下要回写4个block)

然后根据需要回写的summary的数目,返回需要写回的block的数目,返回值有1、2、3,即调用data_sum_blocks = npages_for_summary_flush(sbi)

int npages_for_summary_flush(struct f2fs_sb_info *sbi)
{
	int total_size_bytes = 0;
	int valid_sum_count = 0;
	int i, sum_space;
	//计算需要刷多少个f2fs_summary条目
	for (i = CURSEG_HOT_DATA; i <= CURSEG_COLD_DATA; i++) {
		if (sbi->ckpt->alloc_type[i] == SSR)
			valid_sum_count += sbi->blocks_per_seg;//如果是就地更新,则整个段的块数都要算
		else
			valid_sum_count += curseg_blkoff(sbi, i);//如果是异地更新,则需要按照分配的块个数
	}

	total_size_bytes = valid_sum_count * (SUMMARY_SIZE + 1)//f2fs_summary所占的空间
			+ sizeof(struct nat_journal) + 2//nat journal 站的空间
			+ sizeof(struct sit_journal) + 2;
	sum_space = PAGE_CACHE_SIZE - SUM_FOOTER_SIZE;
	if (total_size_bytes < sum_space)
		//1.少于439的block(只修改了439个f2fs_summary),那么可以通过compacted回写方式进行回写,即通过一个compacted summary block完成回写,需要回写1个block。
		return 1;
	else if (total_size_bytes < 2 * sum_space)
		//2.大于439,少于439+584=1023个block,那么可以通过compacted回写方式进行回写,即可以通过compacted summary block加一个纯summary block的方式保存所有信息,需要回写2个block。
		return 2;
	//3.大于1023的情况下,即和normal summary block同样的回写情况,那么就会使用normal summary block的回写方式完成回写,即回写3个block。(因为大于1023情况下,如果继续使用compacted回写,最差的情况下要回写4个block)
	return 3;
}

接下来设置标志:

	if (data_sum_blocks < 3)
		set_ckpt_flags(ckpt, CP_COMPACT_SUM_FLAG);
	else
		clear_ckpt_flags(ckpt, CP_COMPACT_SUM_FLAG);

如果data_sum_blocks = 1 或者 2,则表示回写1个或者2个block,则设置CP_COMPACT_SUM_FLAG标志
接下来是写orphan节点信息(未完待续
然后是将上面填充的f2fs_checkpoint刷入磁盘对应块。首先start_blk = __start_cp_addr(sbi)获取磁盘检查点的起始块地址,cp_page = grab_meta_page(sbi, start_blk++)从磁盘中读取f2fs_checkpoint块,然后通过memcpy(kaddr, ckpt, (1 << sbi->log_blocksize))将内存ckpt复制到磁盘checkpoint缓存页中,将page标志为脏。
然后如果写入了orphan block则,将start_blk加上写了多少orphan block。

	if (sbi->n_orphans) {//此时的start_blk已经+1,为orphan BLK区域起始块
		write_orphan_inodes(sbi, start_blk);//写入orphan BLK
		start_blk += orphan_blocks;
	}

随后到了写summary的块地址start_blk,write_data_summaries函数会判断一下是否设置了CP_COMPACT_SUM_FLAG标志,采取不同的方法写入磁盘。write_node_summaries也是只有在umount和fastboot的情况下才会调用。

void write_data_summaries(struct f2fs_sb_info *sbi, block_t start_blk)
{
	if (is_set_ckpt_flags(F2FS_CKPT(sbi), CP_COMPACT_SUM_FLAG))
		write_compacted_summaries(sbi, start_blk);//会根据上述的compacted block的数据分布,将数据写入到磁盘中
	else
		write_normal_summaries(sbi, start_blk, CURSEG_HOT_DATA);//简单地将按照HOT/WARM/COLD的顺序写入到checkpoint区域中
}

如果设置了CP_COMPACT_SUM_FLAGwrite_compacted_summaries函数会根据上述的compacted block的数据分布,将数据写入到磁盘中的检查点中

static void write_compacted_summaries(struct f2fs_sb_info *sbi, block_t blkaddr)
{
	struct page *page;
	unsigned char *kaddr;
	struct f2fs_summary *summary;
	struct curseg_info *seg_i;
	int written_size = 0;
	int i, j;

	page = grab_meta_page(sbi, blkaddr++);//获取磁盘上blkaddr相应的页
	kaddr = (unsigned char *)page_address(page);

	/* Step 1: write nat cache */
	seg_i = CURSEG_I(sbi, CURSEG_HOT_DATA);//第一步写nat的journal,在hot中
	memcpy(kaddr, &seg_i->sum_blk->n_nats, SUM_JOURNAL_SIZE);
	written_size += SUM_JOURNAL_SIZE;

	/* Step 2: write sit cache */
	seg_i = CURSEG_I(sbi, CURSEG_COLD_DATA);// 第二步写sit的journal,在cold中
	memcpy(kaddr + written_size, &seg_i->sum_blk->n_sits,
						SUM_JOURNAL_SIZE);
	written_size += SUM_JOURNAL_SIZE;

	set_page_dirty(page);

	/* Step 3: write summary entries */
	for (i = CURSEG_HOT_DATA; i <= CURSEG_COLD_DATA; i++) {// 开始写summary
		unsigned short blkoff;
		seg_i = CURSEG_I(sbi, i);
		if (sbi->ckpt->alloc_type[i] == SSR)
			blkoff = sbi->blocks_per_seg;
		else
			blkoff = curseg_blkoff(sbi, i);

		for (j = 0; j < blkoff; j++) {//这个循环是将一个活动segment中的summary项写入
			if (!page) {
				page = grab_meta_page(sbi, blkaddr++);
				kaddr = (unsigned char *)page_address(page);
				written_size = 0;
			}
			summary = (struct f2fs_summary *)(kaddr + written_size);
			*summary = seg_i->sum_blk->entries[j];//一项一项的写入,将其写入到磁盘位置
			written_size += SUMMARY_SIZE;
			set_page_dirty(page);

			if (written_size + SUMMARY_SIZE <= PAGE_CACHE_SIZE -//如果一个块装的下,跳入下一个循环
							SUM_FOOTER_SIZE)
				continue;

			f2fs_put_page(page, 1);// 如果超过了compaced sum block可以承载的极限,就设置这个block是脏,等待回写
			page = NULL;
		}
	}
	if (page)
		f2fs_put_page(page, 1);
}

该函数将HOT_DATA中的NAT journal和COLD_DATA中的SIT journal顺序写入磁盘检查点的blkaddr处,然后顺序把活动DATA segment中的summary项写入。
write_normal_summaries函数则是简单地将按照HOT/WARM/COLD的顺序把活动DATA segment的f2fs_summary_block写入到checkpoint区域中,其中f2fs_summary_block中包括journal和f2fs_summary条目(即struct f2fs_summary_block):

static void write_normal_summaries(struct f2fs_sb_info *sbi,
					block_t blkaddr, int type)
{
	int i, end;
	if (IS_DATASEG(type))
		end = type + NR_CURSEG_DATA_TYPE;
	else
		end = type + NR_CURSEG_NODE_TYPE;

	for (i = type; i < end; i++) {//循环的将hot,warm,cold 的f2fs_summary_block刷到磁盘的blkaddr + (i - type)处
		struct curseg_info *sum = CURSEG_I(sbi, i);
		mutex_lock(&sum->curseg_mutex);
		write_sum_page(sbi, sum->sum_blk, blkaddr + (i - type));
		mutex_unlock(&sum->curseg_mutex);
	}
}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`load_from_checkpoint` 并非 Python 内置函数,我无法直接提供具体的解释。但是,`load_from_checkpoint` 通常是在机器学习或深度学习领域中使用的一个函数或方法,用于从检查点文件中加载模型的参数或状态。 通常情况下,`load_from_checkpoint` 函数会接收一个检查点文件的路径作为参数,并从该文件中读取保存的模型参数或状态,然后将其加载到一个模型对象中。这样可以方便地在训练过程中保存模型的中间状态,并在需要的时候重新加载模型继续训练或进行推理。 具体的 `load_from_checkpoint` 实现可能会依赖于所使用的深度学习框架或库。例如,在 PyTorch 中,可以使用 `torch.load()` 函数来加载检查点文件,并将保存的参数或状态加载到模型中。 以下是一个示例代码片段,展示了如何使用 PyTorch 中的 `load_from_checkpoint` 函数来加载模型的参数: ```python import torch # 定义模型类 class MyModel(torch.nn.Module): def __init__(self): super(MyModel, self).__init__() self.fc = torch.nn.Linear(10, 1) # 创建模型对象 model = MyModel() # 从检查点文件中加载模型参数 checkpoint_path = 'model_checkpoint.pt' checkpoint = torch.load(checkpoint_path) model.load_state_dict(checkpoint['model_state_dict']) ``` 请注意,以上示例仅为演示目的,并不代表 `load_from_checkpoint` 的具体实现方式,实际使用时可能需要根据具体情况进行调整。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值