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

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

F2FS的读流程

读流程介绍

F2FS的读流程包含了以下几个子流程:

  1. vfs_read函数
  2. generic_file_read_iter函数: 根据访问类型执行不同的处理
  3. generic_file_buffered_read: 根据用户传入的文件偏移,读取尺寸等信息,计算起始位置和页数,然后遍历每一个page,通过预读或者单个读取的方式从磁盘中读取出来
  4. f2fs_read_data_page&f2fs_read_data_pages函数: 从磁盘读取1个page或者多个page
  5. f2fs_mpage_readpages函数: f2fs读取数据的主流程

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

generic_file_read_iter函数

这个函数的作用是处理普通方式访问以及direct方式访问的读行为,这里仅针对普通方式的读访问进行分析:

ssize_t generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
	size_t count = iov_iter_count(iter); // 获取需要读取的字节数
	ssize_t retval = 0;

	if (!count)
		goto out;

	if (iocb->ki_flags & IOCB_DIRECT) { // 处理direct方式的访问,这里不做介绍
		...
	}

	retval = generic_file_buffered_read(iocb, iter, retval); // 进行普通的读访问
out:
	return retval;
}

generic_file_buffered_read函数

在介绍这两个之前,需要先介绍一种VFS提高读取速度的机制: 预读(readahead)机制。它的核心原理是,当用户访问page 1,系统就会将page 1后续的page 2,page 3,page 4一起读取到page cache(减少与磁盘这种速度慢设备的交互次数,提高读性能)。之后用户再连续读取page 2,page 3,page 4时,由于已经读取到内存中,因此可以快速地返回给用户。

generic_file_buffered_read函数的主要作用是循环地从磁盘或者内存读取用户需要的page,同时也会在某些情况调用page_cache_sync_readahead函数进行预读,由于函数比较复杂,且很多goto语句,简化后的步骤如下:

情况1: 预读(readahead)机制成功预读到用户需要接下来访问的page

  1. ind_get_page: 系统无法在cache中找到用户需要的page
  2. page_cache_sync_readahead: 系统执行该函数进行预读,一次性读取多个page
  3. find_get_page: 再重新在cache获取一次page,获取成功后跳转到page ok区域
  4. page_ok: 复制page的数据去用户传入的buffer中,然后判读是否为最后一个page,如果是则退出读流程

情况2: 预读(readahead)机制错误预读到用户需要接下来访问的page

  1. find_get_page: 系统无法在cache中找到用户需要的page
  2. page_cache_sync_readahead: 系统执行该函数进行预读,一次性读取多个page
  3. find_get_page: 再重新在cache获取一次page,获取失败,跳转到no_cached_page区域
  4. no_cached_page: 创建一个page cache结构,加入到LRU后,跳转到readpage区域
  5. readpage: 执行mapping->a_ops->readpage函数从磁盘读取数据,成功后跳转到page ok区域
  6. page_ok: 复制page的数据去用户传入的buffer中,然后判读是否为最后一个page,如果是则退出读流程。
static ssize_t generic_file_buffered_read(struct kiocb *iocb,
		struct iov_iter *iter, ssize_t written)
{

	index = *ppos >> PAGE_SHIFT; // 文件指针偏移*ppos除以page的大小就是页偏移index
	prev_index = ra->prev_pos >> PAGE_SHIFT;
	prev_offset = ra->prev_pos & (PAGE_SIZE-1);
	last_index = (*ppos + iter->count + PAGE_SIZE-1) >> PAGE_SHIFT;
	offset = *ppos & ~PAGE_MASK;

	for (;;) {
find_page:
		page = find_get_page(mapping, index); // 根据页偏移index从cache获取page
		if (!page) { // 获取失败进行一次预读
			page_cache_sync_readahead(mapping, ra, filp,
					index, last_index - index);
			page = find_get_page(mapping, index); // 预读后再从cache获取page
			if (unlikely(page == NULL)) // 如果仍然失败则跳转到no_cached_page,成功则直接去page ok区域
				goto no_cached_page;
		}
page_ok: 
		// page数据读取成功后都进入这个区域,用于将数据复制到用户传入的buffer中
		isize = i_size_read(inode);
		end_index = (isize - 1) >> PAGE_SHIFT;

		nr = PAGE_SIZE;
		if (index == end_index) { // 如果到了最后一个index就退出循环
			nr = ((isize - 1) & ~PAGE_MASK) + 1;
			if (nr <= offset) {
				put_page(page);
				goto out;
			}
		}
		nr = nr - offset;
		ret = copy_page_to_iter(page, offset, nr, iter); // 复制用户数据到buffer中
		offset += ret;
		index += offset >> PAGE_SHIFT;
		offset &= ~PAGE_MASK;
		prev_offset = offset;

		put_page(page);
		written += ret;
		if (!iov_iter_count(iter))  // 如果将所有数据读取完毕后退出循环
			goto out;
		if (ret < nr) {
			error = -EFAULT;
			goto out;
		}
		continue;
readpage:
		ClearPageError(page);
		error = mapping->a_ops->readpage(filp, page); // 去磁盘进行读取
		goto page_ok;
no_cached_page:
		page = page_cache_alloc(mapping); // 创建page cache
		error = add_to_page_cache_lru(page, mapping, index,
				mapping_gfp_constraint(mapping, GFP_KERNEL)); // 加入lru
		goto readpage;
	}
out:
	ra->prev_pos = prev_index;
	ra->prev_pos <<= PAGE_SHIFT;
	ra->prev_pos |= prev_offset;

	*ppos = ((loff_t)index << PAGE_SHIFT) + offset;
	file_accessed(filp);
	return written ? written : error;
}

预读函数page_cache_sync_readahead的分析由于篇幅有限无法全部展示,因此这里仅分析它的核心调用函数__do_page_cache_readahead:

unsigned int __do_page_cache_readahead(struct address_space *mapping,
		struct file *filp, pgoff_t offset, unsigned long nr_to_read,
		unsigned long lookahead_size)
{
	end_index = ((isize - 1) >> PAGE_SHIFT); // 得到文件的最后一个页的页偏移index

	for (page_idx = 0; page_idx < nr_to_read; page_idx++) { // nr_to_read是需要预读的page的数目
		pgoff_t page_offset = offset + page_idx; // offset表示从第几个page开始预读

		if (page_offset > end_index) // 预读超过了文件大小就退出
			break;
			
		page = __page_cache_alloc(gfp_mask); // 创建page cache
		page->index = page_offset; // 设置page index
		list_add(&page->lru, &page_pool); // 将所有预读的page加入到一个list中
		nr_pages++;
	}

	if (nr_pages)
		read_pages(mapping, filp, &page_pool, nr_pages, gfp_mask); // 执行预读
	BUG_ON(!list_empty(&page_pool));
out:
	return nr_pages;
}

static int read_pages(struct address_space *mapping, struct file *filp,
		struct list_head *pages, unsigned int nr_pages, gfp_t gfp)
{
	struct blk_plug plug;
	unsigned page_idx;
	int ret;

	blk_start_plug(&plug);

	if (mapping->a_ops->readpages) {
		ret = mapping->a_ops->readpages(filp, mapping, pages, nr_pages); // 执行readpages函数进行预读
		put_pages_list(pages);
		goto out;
	}
	ret = 0;

out:
	blk_finish_plug(&plug);

	return ret;
}

f2fs_read_data_page&f2fs_read_data_pages函数

从上一节可以知道,当预读机制会调用mapping->a_ops->readpages函数一次性读取多个page。而当预读失败时,也会调用mapping->a_ops->readpage读取单个page。这两个函数在f2fs中对应的就是f2fs_read_pagef2fs_read_pages,如下所示:

static int f2fs_read_data_page(struct file *file, struct page *page)
{
	struct inode *inode = page->mapping->host;
	int ret = -EAGAIN;

	trace_f2fs_readpage(page, DATA);

	if (f2fs_has_inline_data(inode)) // inline文件使用特定的读取方法,这里暂不分析
		ret = f2fs_read_inline_data(inode, page);
	ret = f2fs_mpage_readpages(page->mapping, NULL, page, 1); // 读取1个page
	return ret;
}

static int f2fs_read_data_pages(struct file *file,
			struct address_space *mapping,
			struct list_head *pages, unsigned nr_pages)
{
	struct inode *inode = mapping->host;
	struct page *page = list_last_entry(pages, struct page, lru);

	trace_f2fs_readpages(inode, page, nr_pages);

	if (f2fs_has_inline_data(inode)) // inline文件是size小于1个page的文件,因此不需要进行预读,直接return 0
		return 0;

	return f2fs_mpage_readpages(mapping, pages, NULL, nr_pages); // 读取nr_pages个page
}

f2fs_mpage_readpages函数

无论是f2fs_read_page函数还是f2fs_read_pages函数,都是调用f2fs_mpage_readpages函数进行读取,区别仅在于传入参数。f2fs_mpage_readpages的定义为:

static int f2fs_mpage_readpages(struct address_space *mapping,
			struct list_head *pages, struct page *page, unsigned nr_pages);

第二个参数表示一个链表头,这个链表保存了多个page,因此需要写入多个page的时候,就要传入一个List。
第三个参数表示单个page,在写入单个page的时候,通过这个函数写入。
第四个参数表示需要写入page的数目。

因此
在写入多个page的时候,需要设定第二个参数,和第四个参数,然后设定第三个参数为NULL。
在写入单个page的时候,需要设定第三个参数,和第四个参数,然后设定第二个参数为NULL。

然后分析这个函数的执行流程:

  1. 遍历传入的page,得到每一个page的index以及inode
  2. 将page的inode以及index传入 f2fs_map_blocks 函数获取到该page的物理地址
  3. 将物理地址通过 submit_bio 读取该page在磁盘中的数据
static int f2fs_mpage_readpages(struct address_space *mapping,
			struct list_head *pages, struct page *page,
			unsigned nr_pages)
{
	// 主流程第一步 初始化map结构,这个步骤非常重要,用于获取page在磁盘的物理地址
	struct f2fs_map_blocks map;
	map.m_pblk = 0;
	map.m_lblk = 0;
	map.m_len = 0;
	map.m_flags = 0;
	map.m_next_pgofs = NULL;

	// 主流程第二步 开始进行遍历,结束条件为 nr_pages 不为空
	for (page_idx = 0; nr_pages; page_idx++, nr_pages--) {

		// 循环第一步,如果是读取多个page,则pages不为空,从list里面读取每一次的page结构
		if (pages) {
			page = list_entry(pages->prev, struct page, lru);
			list_del(&page->lru);
			if (add_to_page_cache_lru(page, mapping,
						  page->index, GFP_KERNEL))
				goto next_page;
		}

		/**
 		 * map.m_lblk是上一个block_in_file
 		 * map.m_lblk + map.m_len是需要读取长度的最后一个blokaddr
 		 * 因此这里的意思是,如果是在这个 map.m_lblk < block_in_file < map.m_lblk + map.m_len 
 		 * 这个范围里面,不需要map,直接将上次的blkaddr+1就是需要的地址
 		 * 
		 */
		// 循环第二步,如果上一次找到了page,则跳到 got_it 通过bio获取page的具体数据
		if ((map.m_flags & F2FS_MAP_MAPPED) && block_in_file > map.m_lblk &&
			block_in_file < (map.m_lblk + map.m_len))
			goto got_it;
	
		// 循环第三步,使用page offset和length,通过f2fs_map_blocks获得物理地址
		map.m_flags = 0;
		if (block_in_file < last_block) {
			map.m_lblk = block_in_file; // 文件的第几个block
			map.m_len = last_block - block_in_file; // 读取的block的长度

			if (f2fs_map_blocks(inode, &map, 0,
						F2FS_GET_BLOCK_READ))
				goto set_error_page;
		}

got_it:
		// 循环第四步,通过map的结果执行不一样的处理方式
		if ((map.m_flags & F2FS_MAP_MAPPED)) { // 如果找到了地址,则计算block_nr得到磁盘的地址
			block_nr = map.m_pblk + block_in_file - map.m_lblk;
			SetPageMappedToDisk(page);

			if (!PageUptodate(page) && !cleancache_get_page(page)) {
				SetPageUptodate(page);
				goto confused;
			}
		} else { // 获取失败了,则跳过这个page
			zero_user_segment(page, 0, PAGE_SIZE);
			SetPageUptodate(page);
			unlock_page(page);
			goto next_page;
		}

		/**
		 * 这部分开始用于将物理地址通过submit_bio提交到磁盘读取数据
		 * 由于从磁盘读取数据是一个相对耗时的操作,
		 * 因此显然每读取一个页就访问一次磁盘一次的方式是低效的且影响读性能的,
		 * 所以F2FS会尽量一次性提交多个页到磁盘读取数据,以提高性能。
		 * 
		 * 这部分开始就是具体实现:
		 * 1. 创建一个bio(最大一次性提交256个页)
		 * 2. 将需要读取的页添加到这个bio中,
		 *     ------如果bio未满则将page添加到bio中
		 *     ------如果bio满了立即访问磁盘读取
		 *     ------如果循环结束以后,bio还是未满,则通过本函数末尾的操作提交未满的bio。
		 *     
         */

		// 循环第五步,判断bio装的page是否到了设定的最大数量,如果到了最大值则先发送到磁盘
		if (bio && (last_block_in_bio != block_nr - 1)) {
submit_and_realloc:
			submit_bio(READ, bio);
			bio = NULL;
		}

		// 循环第六步,如果bio是空,则创建一个bio,然后指定的f2fs_read_end_io进行读取
		if (bio == NULL) {
			struct fscrypt_ctx *ctx = NULL;

			if (f2fs_encrypted_inode(inode) &&
					S_ISREG(inode->i_mode)) {

				ctx = fscrypt_get_ctx(inode, GFP_NOFS);
				if (IS_ERR(ctx))
					goto set_error_page;

				/* wait the page to be moved by cleaning */
				f2fs_wait_on_encrypted_page_writeback(
						F2FS_I_SB(inode), block_nr);
			}

			bio = bio_alloc(GFP_KERNEL,
				min_t(int, nr_pages, BIO_MAX_PAGES)); // 创建bio
			if (!bio) {
				if (ctx)
					fscrypt_release_ctx(ctx);
				goto set_error_page;
			}
			bio->bi_bdev = bdev;
			bio->bi_iter.bi_sector = SECTOR_FROM_BLOCK(block_nr); // 设定bio的sector地址
			bio->bi_end_io = f2fs_read_end_io;
			bio->bi_private = ctx;
		}

		// 循环第七步,将page加入到bio中,等待第五步满了之后发送到磁盘
		if (bio_add_page(bio, page, blocksize, 0) < blocksize)
			goto submit_and_realloc;

set_error_page:
		SetPageError(page);
		zero_user_segment(page, 0, PAGE_SIZE);
		unlock_page(page);
		goto next_page;
confused: // 特殊情况进行submit bio
		if (bio) {
			submit_bio(READ, bio);
			bio = NULL;
		}
		unlock_page(page);
next_page:
		if (pages)
			put_page(page);
		
	}

	
	BUG_ON(pages && !list_empty(pages));

	// 如果还有bio没有处理,例如读取的页遍历完以后,还没有达到第五步要求的bio的最大保存页数,就会在这里提交bio到磁盘读取
	if (bio)
		submit_bio(READ, bio);
	return 0;
}
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
F2FS是专为闪存设备设计的文件系统,具有出色的性能和可靠性。下面对F2FS的代码进行简要的分析: 1. 超级块(superblock):超级块是F2FS中存储文件系统元数据的结构体,包含文件系统的基本信息,如版本号、块大小、节点大小、块位图、节点位图、inode表、日志区域等。 2. inode(index node):inode是F2FS中存储文件和目录元数据的结构体,每个文件和目录都会对应一个inode节点。inode包含文件类型、权限、大小、数据块指针等信息。 3. 数据块(data block):数据块是F2FS中存储文件数据的结构体,每个数据块大小为4KB,可以存储文件数据、索引节点数据、日志数据等。 4. 日志(journal):F2FS中的写操作都会写入日志,然后再同步到数据块中。日志大小为1MB,用于记录文件系统的变化情况,以便在系统重启后恢复数据的一致性。 5. 垃圾回收(garbage collection):由于闪存设备的写入操作是有限制的,因此需要定期进行垃圾回收以释放已经不再使用的空间。F2FS中的垃圾回收机制采用了段式管理的思路,即将整个闪存设备分成多个段,每个段独立进行垃圾回收。 6. 压缩(compression):F2FS中的压缩机制采用了zlib压缩算法,可以将文件数据进行压缩以节省存储空间和提高读写性能。 7. 加密(encryption):F2FS中的加密机制采用了AES加密算法,可以对文件数据进行加密和解密以保护用户数据的安全性。 总之,F2FS是一种高效、可靠的文件系统,它的代码实现非常精细和模块化,各个模块之间相互独立,并且有很好的扩展性和灵活性。F2FS的设计思路和数据结构也非常有特色,可以更好地充分利用闪存设备的性能和寿命。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值