当前位置: 首页 > 工具软件 > fs-lock > 使用案例 >

F2FS源码分析-2.2 [F2FS 读写部分] F2FS的一般文件写流程分析

闻人和泽
2023-12-01

F2FS源码分析系列文章

主目录
一、文件系统布局以及元数据结构
二、文件数据的存储以及读写
  1. F2FS文件数据组织方式
  2. 一般文件写流程
  3. 一般文件读流程
  4. 目录文件读流程(未完成)
  5. 目录文件写流程(未完成)
三、文件与目录的创建以及删除(未完成)
四、垃圾回收机制
五、数据恢复机制
六、重要数据结构或者函数的分析

F2FS的写流程

写流程介绍

F2FS的写流程主要包含了以下几个子流程:

  1. 调用vfs_write函数
  2. 调用f2fs_file_write_iter函数: 初始化f2fs_node的信息
  3. 调用f2fs_write_begin函数: 创建page cache,并填充数据
  4. 写入到page cache: 等待系统触发writeback回写到磁盘
  5. 调用f2fs_write_end函数: 将page设置为最新状态
  6. 调用f2fs_write_data_pages函数: 系统writeback或者fsync触发的时候执行这个函数写入到磁盘

第一步的vfs_write函数是VFS层面的流程,下面仅针对涉及F2FS的写流程,且经过简化的主要流程进行分析。

f2fs_file_write_iter函数

这个函数的主要作用是在数据写入文件之前进行预处理,核心流程就是将该文件对应f2fs_inode或者direct_node对应写入位置的i_addr或者addr的值进行初始化。例如用户需要在第4个page的位置写入数据,那么f2fs_file_write_iter函数会首先找到该文件对应的f2fs_inode,然后找到第4个page对应的数据块地址记录,即f2fs_inode->i_addr[3]。如果该位置的值是NULL_ADDR则表示当前是添加写(Append Write),因此将值初始化为NEW_ADDR;如果是该位置的值是一个具体的block号,那么表示为覆盖写(Overwrite),不需要做处理。

static ssize_t f2fs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
	struct file *file = iocb->ki_filp;
	struct inode *inode = file_inode(file);
	ssize_t ret;

	...
	err = f2fs_preallocate_blocks(iocb, from); // 进行预处理
	...
	ret = __generic_file_write_iter(iocb, from); // 预处理完成后继续执行下一步写流程
	...

	return ret;
}

下面继续分析f2fs_preallocate_blocks:

int f2fs_preallocate_blocks(struct kiocb *iocb, struct iov_iter *from)
{
	struct inode *inode = file_inode(iocb->ki_filp); // 获取inode
	struct f2fs_map_blocks map;

	map.m_lblk = F2FS_BLK_ALIGN(iocb->ki_pos); // 根据文件指针偏移计算需要从第几个block开始写入
	map.m_len = F2FS_BYTES_TO_BLK(iocb->ki_pos + iov_iter_count(from)); // 计算要写入block的个数

	// 初始化一些信息
	map.m_next_pgofs = NULL;
	map.m_next_extent = NULL;
	map.m_seg_type = NO_CHECK_TYPE;

	flag = F2FS_GET_BLOCK_PRE_AIO;

map_blocks:
	err = f2fs_map_blocks(inode, &map, 1, flag); // 进行初始化
	return err;
}

f2fs_map_blocks函数的作用非常广泛,主要作用是通过逻辑地址(文件偏移指针)找到对应的物理地址(block号)。因此在读写流程中都有作用。在写流程中,该函数的主要作用是初始化地址信息:

int f2fs_map_blocks(struct inode *inode, struct f2fs_map_blocks *map,
						int create, int flag)
{
	unsigned int maxblocks = map->m_len;

	struct f2fs_sb_info *sbi = F2FS_I_SB(inode);
	int mode = create ? ALLOC_NODE : LOOKUP_NODE;

	map->m_len = 0;
	map->m_flags = 0;

	pgofs =	(pgoff_t)map->m_lblk; // 获得文件访问偏移量
	end = pgofs + maxblocks; // 获得需要读取的block的长度

next_dnode:

	set_new_dnode(&dn, inode, NULL, NULL, 0); // 初始化dnode,dnode的作用是根据逻辑地址找到物理地址
	
	// 根据inode找到对应的f2fs_inode或者direct_node结构,然后通过pgofs(文件页偏移)获得物理地址,记录在dn中
	err = f2fs_get_dnode_of_data(&dn, pgofs, mode); 

	start_pgofs = pgofs;
	prealloc = 0;
	last_ofs_in_node = ofs_in_node = dn.ofs_in_node;
	end_offset = ADDRS_PER_PAGE(dn.node_page, inode);

next_block:
	// 根据dn获得物理地址,ofs_in_node表示这个物理地址位于当前node的第几个数据块
	// 如 f2fs_inode->i_addr[3],那么dn.ofs_in_node=3
	blkaddr = datablock_addr(dn.inode, dn.node_page, dn.ofs_in_node); 
	...
	if (!is_valid_blkaddr(blkaddr)) { // is_valid_blkaddr函数用于判断是否存在旧数据
		// 如果不存在旧数据
		if (create) {
			if (flag == F2FS_GET_BLOCK_PRE_AIO) {
				if (blkaddr == NULL_ADDR) {
					prealloc++; // 记录有多少个添加写的block
					last_ofs_in_node = dn.ofs_in_node;
				}
			}
			map->m_flags |= F2FS_MAP_NEW; // F2FS_MAP_NEW表示正在处理一个从未使用的数据
			blkaddr = dn.data_blkaddr; // 记录当前的物理地址
		}
	}
	...
	// 记录处理了多少个block
	dn.ofs_in_node++; 
	pgofs++;
	...
	// 这里表示已经处理到最后一个block了
	if (flag == F2FS_GET_BLOCK_PRE_AIO &&
			(pgofs == end || dn.ofs_in_node == end_offset)) {

		dn.ofs_in_node = ofs_in_node; // 回到第一个block
		err = f2fs_reserve_new_blocks(&dn, prealloc); // 通过这个函数将其地址设置为NEW_ADDR
		map->m_len += dn.ofs_in_node - ofs_in_node;
		dn.ofs_in_node = end_offset;
	}
	...
	if (pgofs >= end)
		goto sync_out; // 表示已经全部处理完,可以退出这个函数了
	else if (dn.ofs_in_node < end_offset)
		goto next_block; // 每执行上面的流程就处理一个block,如果没有处理所有用户写入的block,那么回去继续处理
	...
sync_out:
	...
out:
	return err;
}

然后分析f2fs_reserve_new_blocks:

int f2fs_reserve_new_blocks(struct dnode_of_data *dn, blkcnt_t count)
{
	struct f2fs_sb_info *sbi = F2FS_I_SB(dn->inode);
	int err;

	...
	for (; count > 0; dn->ofs_in_node++) {
		block_t blkaddr = datablock_addr(dn->inode,
					dn->node_page, dn->ofs_in_node);
		if (blkaddr == NULL_ADDR) { // 首先判断是不是NULL_ADDR,如果是则初始化为NEW_ADDR
			dn->data_blkaddr = NEW_ADDR;
			__set_data_blkaddr(dn);
			count--;
		}
	}
	...

	return 0;
}

f2fs_write_begin和f2fs_write_end函数

VFS中write_beginwrite_end函数分别是数据写入page cache前以及写入后的处理。写入page cache后,系统会维护一段时间,直到满足一定条件后(如fsync和writeback会写),VFS会调用writepages函数,将这些缓存在内存中的page一次性写入到磁盘中。write_beginwrite_end函数的调用可以参考VFS的generic_perform_write函数,

ssize_t generic_perform_write(struct file *file,
				struct iov_iter *i, loff_t pos)
{
	struct address_space *mapping = file->f_mapping;
	const struct address_space_operations *a_ops = mapping->a_ops;
	long status = 0;
	ssize_t written = 0;
	unsigned int flags = 0;

	do {
		struct page *page;
		unsigned long offset;
		unsigned long bytes;
		size_t copied;
		void *fsdata;

		offset = (pos & (PAGE_SIZE - 1)); // 计算文件偏移,按page计算
		bytes = min_t(unsigned long, PAGE_SIZE - offset, iov_iter_count(i)); // 计算需要写多少个字节
again:
		status = a_ops->write_begin(file, mapping, pos, bytes, flags, &page, &fsdata); // 调用write_begin,对page进行初始化

		copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes); //  将处理后的数据拷贝到page当中
		flush_dcache_page(page); // 将包含用户数据的page加入到page cache中,等待系统触发writeback的时候回写

		status = a_ops->write_end(file, mapping, pos, bytes, copied, page, fsdata); // 调用write_end函数进行后续处理
		
		copied = status;

		iov_iter_advance(i, copied);

		pos += copied;
		written += copied;

		balance_dirty_pages_ratelimited(mapping);
	} while (iov_iter_count(i)); // 直到处理完所有的数据

	return written ? written : status;
}

然后分析VFS的write_beginwrite_end对应的功能,write_begin在F2FS中对应的是f2fs_write_begin,它的作用是将根据用户需要写入的数据类型,对page进行初始化,如下所示:

static int f2fs_write_begin(struct file *file, struct address_space *mapping,
		loff_t pos, unsigned len, unsigned flags,
		struct page **pagep, void **fsdata)
{
	struct inode *inode = mapping->host;
	struct f2fs_sb_info *sbi = F2FS_I_SB(inode);
	struct page *page = NULL;
	pgoff_t index = ((unsigned long long) pos) >> PAGE_SHIFT;
	bool need_balance = false, drop_atomic = false;
	block_t blkaddr = NULL_ADDR;
	int err = 0;

repeat:
	page = f2fs_pagecache_get_page(mapping, index,
				FGP_LOCK | FGP_WRITE | FGP_CREAT, GFP_NOFS); // 第一步创建或者获取page cache

	*pagep = page;

	err = prepare_write_begin(sbi, page, pos, len,
					&blkaddr, &need_balance); // 第二步根据页偏移信息获取到对应的物理地址blkaddr

	// 第三步,根据写类型对新创建的page进行初始化处理
	if (blkaddr == NEW_ADDR) { //如果是添加写,则将该page直接使用0填充
		zero_user_segment(page, 0, PAGE_SIZE);
		SetPageUptodate(page);
	} else {  //如果是覆盖写,则将该page直接使用0填充
		err = f2fs_submit_page_read(inode, page, blkaddr); // 从磁盘中将旧数据读取出来

		lock_page(page);
		if (unlikely(page->mapping != mapping)) {
			f2fs_put_page(page, 1);
			goto repeat;
		}
		if (unlikely(!PageUptodate(page))) {
			err = -EIO;
			goto fail;
		}
	}
	return 0;
}

通过flush_dcache_page函数将用户数据写入到page cache之后,进行write_end处理,在F2FS中它对应的是f2fs_write_end函数,它的作用是,如下所述:

static int f2fs_write_end(struct file *file,
			struct address_space *mapping,
			loff_t pos, unsigned len, unsigned copied,
			struct page *page, void *fsdata)
{
	struct inode *inode = page->mapping->host;

	if (!PageUptodate(page)) { // 判断是否已经将page cache在写入是否到达了最新的状态
		if (unlikely(copied != len))
			copied = 0;
		else
			SetPageUptodate(page); // 如果不是就处理后设置为最新
	}
	if (!copied)
		goto unlock_out;

	set_page_dirty(page); // 将page设置为dirty,就会加入到inode->mapping的radix tree中,等待系统回写

	if (pos + copied > i_size_read(inode))
		f2fs_i_size_write(inode, pos + copied); // 更新文件尺寸
unlock_out:
	f2fs_put_page(page, 1);
	f2fs_update_time(F2FS_I_SB(inode), REQ_TIME); // 更新文件修改日期
	return copied;
}

f2fs_write_data_pages函数

如上一节所述,系统会将用户写入的数据先写入到page cache,然后等待时机回写到磁盘中。page cache的回写是通过f2fs_write_data_pages函数进行。系统会将page cache中dirty的pages加入到一个list当中,然后传入到` f2fs_write_data_pages进行处理。针对F2FS文件系统,它包含如下步骤:

  1. f2fs_write_data_pages&__f2fs_write_data_pages函数: 做一些不那么重要的预处理
  2. f2fs_write_cache_pages函数: 从inode->mapping的radix tree中取出page
  3. __write_data_page函数: 判断文件类型(内联文件,目录文件,普通文件)进行不同的写入
  4. f2fs_do_write_data_page: 根据F2FS的状态选择进行就地回写(在原物理地址更新)还是异地回写(在其他物理地址更新)
  5. f2fs_outplace_write_data: 执行回写,更新f2fs_inode的状态
  6. do_write_page: 从CURSEG分配物理地址,然后写入到磁盘
    下面各自进行分析。

f2fs_write_data_pages&__f2fs_write_data_pages函数

这两个函数只是包含了一些不太重要的预处理

static int f2fs_write_data_pages(struct address_space *mapping,
			    struct writeback_control *wbc)
{
	struct inode *inode = mapping->host;

	return __f2fs_write_data_pages(mapping, wbc,
			F2FS_I(inode)->cp_task == current ?
			FS_CP_DATA_IO : FS_DATA_IO); // 这个函数可以知道当前是普通的写入,还是Checkpoint数据的写入
}

static int __f2fs_write_data_pages(struct address_space *mapping,
						struct writeback_control *wbc,
						enum iostat_type io_type)
{
	struct inode *inode = mapping->host;
	struct f2fs_sb_info *sbi = F2FS_I_SB(inode);
	struct blk_plug plug;
	int ret;


	blk_start_plug(&plug);

	ret = f2fs_write_cache_pages(mapping, wbc, io_type); // 取出需要回写的page,然后写入

	blk_finish_plug(&plug);

	f2fs_remove_dirty_inode(inode); // 写入后将inode从dirty标志清除,即不需要再回写
	return ret;
skip_write:
	wbc->pages_skipped += get_dirty_pages(inode);
	trace_f2fs_writepages(mapping->host, wbc, DATA);
	return 0;
}

f2fs_write_cache_pages函数

这个函数的主要作用是从inode对应的mapping(radix tree的root)中,取出所有需要回写的page,然后通过一个循环,逐个写入到磁盘。

static int f2fs_write_cache_pages(struct address_space *mapping,
					struct writeback_control *wbc,
					enum iostat_type io_type)
{
	struct pagevec pvec;
	
	pagevec_init(&pvec); // 这是一个用于装载page的数组,数组大小是15个page

	if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages)
		tag = PAGECACHE_TAG_TOWRITE; // tag是mapping给每一个pae的标志,用于标志这些page的属性
	else
		tag = PAGECACHE_TAG_DIRTY;
		
retry:
	if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages)
		tag_pages_for_writeback(mapping, index, end); // SYNC模式下,将所有的tag=PAGECACHE_TAG_DIRTY的page重新标志为PAGECACHE_TAG_TOWRITE,作用是SYNC模式下必须全部回写到磁盘
	done_index = index;
	
	while (!done && (index <= end)) {
		int i;
		
		// 从mapping中取出tag类型的15个page,装载到pvec中
		nr_pages = pagevec_lookup_range_tag(&pvec, mapping, &index, end, tag); 
		
		// 循环将pvec中的page取出,回写到磁盘
		for (i = 0; i < nr_pages; i++) {
			struct page *page = pvec.pages[i];
			bool submitted = false;
			
			ret = __write_data_page(page, &submitted, wbc, io_type); // 写入磁盘的核心函数

			if (--wbc->nr_to_write <= 0 &&
					wbc->sync_mode == WB_SYNC_NONE) {
				done = 1; // 如果本次writeback的所有page写完就退出
				break;
			}
		}
		pagevec_release(&pvec); // 释放掉pvec
		cond_resched();
	}

	if (wbc->range_cyclic || (range_whole && wbc->nr_to_write > 0))
		mapping->writeback_index = done_index;

	if (last_idx != ULONG_MAX)
		// page通过一些函数后,会放入到bio中,然后提交到磁盘。
		// f2fs的机制是不会马上提交bio,需要等到bio包含了一定数目的page之后才会提交
		// 因此这个函数作用是,即使数目不够,但是仍要强制提交bio,需要与磁盘同步
		f2fs_submit_merged_write_cond(F2FS_M_SB(mapping), mapping->host,
						0, last_idx, DATA);

	return ret;
}

__write_data_page函数

这个函数的作用是判断文件类型(目录文件,内联文件,普通文件)进行不同的写入。F2FS针对普通文件,有两种保存方式,分别是内联方式(inline)和普通方式。内联方式在数据的保存以及逻辑地址和物理地址的映射 这一节已做介绍。这里主要介绍普通文件的写流程,内联文件以后再更新。

static int __write_data_page(struct page *page, bool *submitted,
				struct writeback_control *wbc,
				enum iostat_type io_type)
{
	struct inode *inode = page->mapping->host;
	struct f2fs_sb_info *sbi = F2FS_I_SB(inode);
	loff_t i_size = i_size_read(inode);
	const pgoff_t end_index = ((unsigned long long) i_size) >> PAGE_SHIFT;
	// 这个数据结构在整个写流程非常重要,记录了写入的信息
	// 关键变量是 fio->old_blkaddr 以及 fio->new_blkaddr记录旧地址和新地址
	struct f2fs_io_info fio = { 
		.sbi = sbi,
		.ino = inode->i_ino,
		.type = DATA,
		.op = REQ_OP_WRITE,
		.op_flags = wbc_to_write_flags(wbc),
		.old_blkaddr = NULL_ADDR,
		.page = page, // 即将写入的page
		.encrypted_page = NULL,
		.submitted = false,
		.need_lock = LOCK_RETRY,
		.io_type = io_type,
		.io_wbc = wbc,
	};

	if (page->index < end_index)
		goto write;
		
write:
	if (S_ISDIR(inode->i_mode)) { // 如果是目录文件,直接写入不需要修改
		err = f2fs_do_write_data_page(&fio);
		goto done;
	}

	err = -EAGAIN;
	if (f2fs_has_inline_data(inode)) { // 内联文件使用内联的写入方式
		err = f2fs_write_inline_data(inode, page);
		if (!err)
			goto out;
	}

	if (err == -EAGAIN) { // 普通文件则使用普通的方式
		err = f2fs_do_write_data_page(&fio);
	}

done:
	if (err && err != -ENOENT)
		goto redirty_out;

out:
	inode_dec_dirty_pages(inode); // 每写入一个page,就清除了inode一个dirty pages,因此数目减去1
	if (err)
		ClearPageUptodate(page);

	unlock_page(page);

	if (submitted)
		*submitted = fio.submitted;

	return 0;

redirty_out:
	redirty_page_for_writepage(wbc, page);
	if (!err || wbc->for_reclaim)
		return AOP_WRITEPAGE_ACTIVATE;
	unlock_page(page);
	return err;
}

f2fs_do_write_data_page函数

这个函数的作用是根据系统的状态选择就地更新数据(inplace update)还是异地更新数据(outplace update)。一般情况下,系统只会在磁盘空间比较满的时候选择就地更新策略,避免触发过多的gc影响性能。因此,这里主要介绍异地更新的写流程:

int f2fs_do_write_data_page(struct f2fs_io_info *fio) // 前面提到fio是写流程最重要的数据结构
{
	struct page *page = fio->page;
	struct inode *inode = page->mapping->host;
	struct dnode_of_data dn;
	struct extent_info ei = {0,0,0};
	bool ipu_force = false;
	int err = 0;

	set_new_dnode(&dn, inode, NULL, NULL, 0); // 初始化dnode
	err = f2fs_get_dnode_of_data(&dn, page->index, LOOKUP_NODE); // 根据文件偏移page->index获取物理地址

	fio->old_blkaddr = dn.data_blkaddr; // 将旧的物理地址赋值给fio->old_blkaddr

	if (fio->old_blkaddr == NULL_ADDR) { // 前面提及到f2fs_file_write_iter已经将物理地址设置为NEW_ADDR或者具体的block号,因此这里表示在写入磁盘之前,用户又将这部分数据删除了,所以没必要写入了
		ClearPageUptodate(page);
		goto out_writepage;
	}
got_it:
	if (ipu_force || (is_valid_blkaddr(fio->old_blkaddr) &&
					need_inplace_update(fio))) { // 判断是否需要就地更新
		err = encrypt_one_page(fio);
		if (err)
			goto out_writepage;

		set_page_writeback(page);
		ClearPageError(page);
		f2fs_put_dnode(&dn);
		if (fio->need_lock == LOCK_REQ)
			f2fs_unlock_op(fio->sbi);
		err = f2fs_inplace_write_data(fio); // 使用就地更新的方式写入
		trace_f2fs_do_write_data_page(fio->page, IPU);
		set_inode_flag(inode, FI_UPDATE_WRITE);
		return err;
	}

	err = encrypt_one_page(fio); // 如果开启系统加密,会将这个fio->page先加密

	set_page_writeback(page);
	ClearPageError(page);

	f2fs_outplace_write_data(&dn, fio); // 执行异地更新函数

	set_inode_flag(inode, FI_APPEND_WRITE);
	if (page->index == 0)
		set_inode_flag(inode, FI_FIRST_BLOCK_WRITTEN);
out_writepage:
	f2fs_put_dnode(&dn);
out:
	if (fio->need_lock == LOCK_REQ)
		f2fs_unlock_op(fio->sbi);
	return err;
}

f2fs_outplace_write_data函数

这个函数主要用作异地更新,所谓异地更新即不在原先的物理地址更新数据,因此包含了如下四个步骤:

  1. 分配一个新的物理地址
  2. 将数据写入新的物理地址
  3. 将旧的物理地址无效掉,然后等GC回收
  4. 更新逻辑地址和物理地址的映射关系

本函数即完成以上四个步骤:

void f2fs_outplace_write_data(struct dnode_of_data *dn,
					struct f2fs_io_info *fio)
{
	struct f2fs_sb_info *sbi = fio->sbi;
	struct f2fs_summary sum;
	struct node_info ni;

	f2fs_get_node_info(sbi, dn->nid, &ni);
	set_summary(&sum, dn->nid, dn->ofs_in_node, ni.version);
	
	do_write_page(&sum, fio); // 这里完成第1,2,3步骤
	f2fs_update_data_blkaddr(dn, fio->new_blkaddr); // 这里完成第四个步骤,重新建立映射
}

上面多次提及到struct dnode_of_data dn的作用是根据文件inode,找到f2fs_inode或者direct_node,然后再通过文件偏移得到物理地址,因此f2fs_update_data_blkaddr也是通过dnode_of_data将新的物理地址更新到f2fs_inode或者direct_node对应的位置中。

void f2fs_update_data_blkaddr(struct dnode_of_data *dn, block_t blkaddr)
{
	dn->data_blkaddr = blkaddr; // 获得新的物理地址
	f2fs_set_data_blkaddr(dn); // 更新地址到f2fs_inode或者direct_node
	f2fs_update_extent_cache(dn); // 更新cache
}

void f2fs_set_data_blkaddr(struct dnode_of_data *dn)
{
	f2fs_wait_on_page_writeback(dn->node_page, NODE, true); // 因为要更新node,所以要保证当前的node是最新状态
	__set_data_blkaddr(dn);
	if (set_page_dirty(dn->node_page)) // 设置dirty,因为更新后的地址要回写到磁盘记录
		dn->node_changed = true;
}

static void __set_data_blkaddr(struct dnode_of_data *dn)
{
	struct f2fs_node *rn = F2FS_NODE(dn->node_page); // 根据node page转换到对应的f2fs_node
	__le32 *addr_array;
	int base = 0;

	addr_array = blkaddr_in_node(rn); // 这个用于获得f2fs_inode->i_addr地址或者direct_node->addr地址
	addr_array[base + dn->ofs_in_node] = cpu_to_le32(dn->data_blkaddr); // 根据偏移赋值更新
}

static inline __le32 *blkaddr_in_node(struct f2fs_node *node)
{
	// RAW_IS_INODE判断当前node是属于f2fs_inode还是f2fs_node,然后返回物理地址数组指针
	return RAW_IS_INODE(node) ? node->i.i_addr : node->dn.addr;
}

do_write_page函数

上一节提及到异地更新的1,2,3步骤都是在这里完成,分别是f2fs_allocate_data_block函数完成新物理地址的分配,以及旧物理地址的回收; f2fs_submit_page_write函数完成最后一步,将数据提交到磁盘。下面进行分析:

static void do_write_page(struct f2fs_summary *sum, struct f2fs_io_info *fio)
{
	int type = __get_segment_type(fio); // 获取数据类型,这个类型指HOT/WARM/COLD X NODE/DATA的六种类型

	f2fs_allocate_data_block(fio->sbi, fio->page, fio->old_blkaddr,
			&fio->new_blkaddr, sum, type, fio, true); // 完成异地更新的1,2步

	f2fs_submit_page_write(fio); //完成异地更新的第3步

}

f2fs_allocate_data_block函数首先会根据type获得CURSEG(定义可以参考Active Segment)。然后在CURSEG分配一个新的物理块,然后将旧的物理块无效掉。

void f2fs_allocate_data_block(struct f2fs_sb_info *sbi, struct page *page,
		block_t old_blkaddr, block_t *new_blkaddr,
		struct f2fs_summary *sum, int type,
		struct f2fs_io_info *fio, bool add_list)
{
	struct sit_info *sit_i = SIT_I(sbi);
	struct curseg_info *curseg = CURSEG_I(sbi, type);

	*new_blkaddr = NEXT_FREE_BLKADDR(sbi, curseg); // 获取新的物理地址

	__add_sum_entry(sbi, type, sum); // 将当前summary更新到CURSEG中

	__refresh_next_blkoff(sbi, curseg); // 更新下一次可以用的物理地址

	// 下面更新主要是更新SIT区域的segment信息
	
	// 根据new_blkaddr找到对应的sit_entry,然后更新状态为valid(值为1),表示被用户使用,不可被其他人所使用
	update_sit_entry(sbi, *new_blkaddr, 1);
	
	// 根据old_blkaddr找到对应的sit_entry,然后更新状态为invalid(值为-1),表示被覆盖了,等待GC回收后重新投入使用
	if (GET_SEGNO(sbi, old_blkaddr) != NULL_SEGNO)
		update_sit_entry(sbi, old_blkaddr, -1);

	// 如果当前segment没有空间进行下一次分配了,就分配一个新的segment给CURSEG
	if (!__has_curseg_space(sbi, type))
		sit_i->s_ops->allocate_segment(sbi, type, false);

	// 将segment设置为脏,等CP写回磁盘
	locate_dirty_segment(sbi, GET_SEGNO(sbi, old_blkaddr));
	locate_dirty_segment(sbi, GET_SEGNO(sbi, *new_blkaddr));

}

f2fs_submit_page_write完成最后的提交到磁盘的任务,具体步骤是先创建一个bio,然后将page加入到bio中,如果bio满了就提交到磁盘。

void f2fs_submit_page_write(struct f2fs_io_info *fio)
{
	struct f2fs_sb_info *sbi = fio->sbi;
	enum page_type btype = PAGE_TYPE_OF_BIO(fio->type);
	struct f2fs_bio_info *io = sbi->write_io[btype] + fio->temp; // 这个是F2FS用于临时存放bio的变量
	struct page *bio_page;

	down_write(&io->io_rwsem);
next:
	// 第一步根据是否有加密,将bio_page设置为对应的page
	if (fio->encrypted_page)
		bio_page = fio->encrypted_page;
	else
		bio_page = fio->page;

	fio->submitted = true;

alloc_new:
	// 如果bio是null,就创建一个新的bio
	if (io->bio == NULL) {
		io->bio = __bio_alloc(sbi, fio->new_blkaddr, fio->io_wbc,
						BIO_MAX_PAGES, false,
						fio->type, fio->temp); // BIO_MAX_PAGES一般等于256
		io->fio = *fio;
	}

	// 将page加入到bio中,如果  < PAGE_SIZE 表示bio已经满了,因此就先将这个bio提交,然后重新分配一个新的bio
	if (bio_add_page(io->bio, bio_page, PAGE_SIZE, 0) < PAGE_SIZE) {
		__submit_merged_bio(io); // 提交bio,最终会执行submit_bio函数
		goto alloc_new;
	}
out:
	up_write(&io->io_rwsem);
}

需要注意的是,在这个函数,当bio还没有填满page的时候是不会被提交到磁盘的,这是因为F2FS通过增大bio的size提高了写性能。因此,在用户fsync或者系统writeback的时候,为了保证这些page都可以刷写到磁盘,会如f2fs_write_cache_pages函数所介绍一样,通过f2fs_submit_merged_write_cond函数或者其他函数强行提交这个page未满的bio。

 类似资料: