F2FS源码分析系列文章
主目录
一、文件系统布局以及元数据结构
二、文件数据的存储以及读写
- F2FS文件数据组织方式
- 一般文件写流程
- 一般文件读流程
- 目录文件读流程(未完成)
- 目录文件写流程(未完成)
三、文件与目录的创建以及删除(未完成)
四、垃圾回收机制
五、数据恢复机制
六、重要数据结构或者函数的分析
F2FS的读流程
读流程介绍
F2FS的读流程包含了以下几个子流程:
- vfs_read函数
- generic_file_read_iter函数: 根据访问类型执行不同的处理
- generic_file_buffered_read: 根据用户传入的文件偏移,读取尺寸等信息,计算起始位置和页数,然后遍历每一个page,通过预读或者单个读取的方式从磁盘中读取出来
- f2fs_read_data_page&f2fs_read_data_pages函数: 从磁盘读取1个page或者多个page
- 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
- ind_get_page: 系统无法在cache中找到用户需要的page
- page_cache_sync_readahead: 系统执行该函数进行预读,一次性读取多个page
- find_get_page: 再重新在cache获取一次page,获取成功后跳转到page ok区域
- page_ok: 复制page的数据去用户传入的buffer中,然后判读是否为最后一个page,如果是则退出读流程
情况2: 预读(readahead)机制错误预读到用户需要接下来访问的page
- find_get_page: 系统无法在cache中找到用户需要的page
- page_cache_sync_readahead: 系统执行该函数进行预读,一次性读取多个page
- find_get_page: 再重新在cache获取一次page,获取失败,跳转到no_cached_page区域
- no_cached_page: 创建一个page cache结构,加入到LRU后,跳转到readpage区域
- readpage: 执行
mapping->a_ops->readpage
函数从磁盘读取数据,成功后跳转到page ok区域 - 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_page
和f2fs_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。
然后分析这个函数的执行流程:
- 遍历传入的page,得到每一个page的index以及inode
- 将page的inode以及index传入
f2fs_map_blocks
函数获取到该page的物理地址 - 将物理地址通过
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;
}