文件读取的底层实现——generic_read_iter分析
文章目录
- 前言
- 文件系统层
- fs/ext2
- 读取——.read_iter
- 预读——.readahead
- 文件读取
- filemap_read
- filemap_get_pages
- 预读
- 梳理
- filemap_readahead
- page_cache_async_ra
- ondemand_readahead
- read_pages
- 构造bio请求
- mpage_readahead
- do_mpage_readpage
- 块设备驱动
前言
相比于linu4.x,linux6.x使用一个叫folio的结构,取代了4.x中的复合页,所以4.x和6.x的内核实现会有些差别
文件系统层
在书上我们也许会看到这样的说法:
“执行read/write系统调用之后,VFS会找到对应文件系统实现的.read/.write方法去执行”
但实际上这个说法过时了——
内核里面早就不用.read/.write了
现在用的是.read_iter/.write_iter
以ext2文件系统为例,我们看一看:
fs/ext2
读取——.read_iter
// ext2文件系统中,.read_iter挂的ext2_file_read_iter, 底层普通情况就是调用generic_file_read_iter
const struct file_operations ext2_file_operations = {.read_iter = ext2_file_read_iter,
}
// 看,主要实现就是generic_file_read_iter
static ssize_t ext2_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
#ifdef CONFIG_FS_DAXif (IS_DAX(iocb->ki_filp->f_mapping->host))return ext2_dax_read_iter(iocb, to);
#endifif (iocb->ki_flags & IOCB_DIRECT)return ext2_dio_read_iter(iocb, to);return generic_file_read_iter(iocb, to);
}
预读——.readahead
这也是很重要的一部分,从页缓存中读取,但这里先不展开
const struct address_space_operations ext2_aops = {.readahead = ext2_readahead,
}static void ext2_readahead(struct readahead_control *rac){mpage_readahead(rac, ext2_get_block);
文件读取
其实很久之前我就在看文件系统来着,但看着看着发现深入到内存管理部分,就暂且搁置了
——内核各个子系统都是交错的,想单独只看某个子系统,很难。
ssize_t generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)// 如果是IOCB_DIRECT的情况会处理的比较复杂(但这里我们不关注)if (iocb->ki_flags & IOCB_DIRECT) // 普通情况的读取,就filemap_read这一行return filemap_read(iocb, iter, retval);
filemap_read
主要内容提炼
ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter,ssize_t already_read)
{struct file *filp = iocb->ki_filp;struct file_ra_state *ra = &filp->f_ra;struct address_space *mapping = filp->f_mapping;struct inode *inode = mapping->host;struct folio_batch fbatch;int i, error = 0;bool writably_mapped;loff_t isize, end_offset;loff_t last_pos = ra->prev_pos;// 初始化(清空)fbatch结构folio_batch_init(&fbatch);do {// 把数据(一些页)读到fbatch里filemap_get_pages(iocb, iter->count, &fbatch, false);// i_size_read,读取文件大小isize = i_size_read(inode);// 计算'文件大小'和'当前位置+此次要读的大小'哪个更小,取小者end_offset = min_t(loff_t, isize, iocb->ki_pos + iter->count);// 这部分和'地址空间'有关,暂且不看writably_mapped = mapping_writably_mapped(mapping);// 处理folio_batch(前面通过filemap_get_pages读到了数据)for (i = 0; i < folio_batch_count(&fbatch); i++) {struct folio *folio = fbatch.folios[i];size_t fsize = folio_size(folio);size_t offset = iocb->ki_pos & (fsize - 1);size_t bytes = min_t(loff_t, end_offset - iocb->ki_pos, fsize - offset);size_t copied;// 把folio中的数据拷贝到iter中(准备回传给用户态的)copied = copy_folio_to_iter(folio, offset, bytes, iter);already_read += copied;iocb->ki_pos += copied;last_pos = iocb->ki_pos;}
put_folios:for (i = 0; i < folio_batch_count(&fbatch); i++)folio_put(fbatch.folios[i]);folio_batch_init(&fbatch);} while (iov_iter_count(iter) && iocb->ki_pos < isize && !error);// 更新文件的访问时间file_accessed(filp);ra->prev_pos = last_pos;return already_read ? already_read : error;
}
// 总结,比较关键的就是这两处:// get_pagesfilemap_get_pages(iocb, iter->count, &fbatch, false);// 拷进iter、准备返回copy_folio_to_iter(folio, offset, bytes, iter);
filemap_get_pages
static int filemap_get_pages(struct kiocb *iocb, size_t count, struct folio_batch *fbatch, bool need_uptodate)// 看起来像是在判断batch的数量是否为0if (!folio_batch_count(fbatch)) page_cache_sync_readahead(mapping, ra, filp, index, last_index - index);filemap_get_read_batch(mapping, index, last_index - 1, fbatch);// 如果batch count仍然是0if (!folio_batch_count(fbatch)) filemap_create_folio(filp, mapping, iocb->ki_pos >> PAGE_SHIFT, fbatch);if (folio_test_readahead(folio)) // 如果测试ok就往下走// 去执行预读取filemap_readahead(iocb, filp, mapping, folio, last_index);
这里的page_cache_sync_readahead和filemap_readahead
都是预读取的部分,我们划到下一部分分析
预读
梳理
filemap_get_pagesfilemap_readaheadpage_cache_async_raondemand_readaheadpage_cache_sync_readaheadondemand_readahead
可见最终都是调用的ondemand_readahead,(按需预读?)
filemap_readahead
static int filemap_readahead(struct kiocb *iocb, struct file *file, struct address_space *mapping, struct folio *folio, pgoff_t last_index)DEFINE_READAHEAD(ractl, file, &file->f_ra, mapping, folio->index);// 这个宏展开之后是struct readahead_control ractl = {.file = file,.mapping = mapping,.ra = &file->f_ra,._index = folio->index,}// iocb在这里只做判断if (iocb->ki_flags & IOCB_NOIO)return -EAGAIN;page_cache_async_ra(&ractl, folio, last_index - folio->index); // req_size
page_cache_async_ra
void page_cache_async_ra(struct readahead_control *ractl, struct folio *folio, unsigned long req_count)if (!ractl->ra->ra_pages)return;ondemand_readahead(ractl, folio, req_count);
ondemand_readahead
static void ondemand_readahead(struct readahead_control *ractl, struct folio *folio, unsigned long req_size)do_page_cache_ra(ractl, req_size, 0);void page_cache_ra_unbounded(struct readahead_control *ractl, unsigned long nr_to_read, unsigned long lookahead_size)static void read_pages(struct readahead_control *rac)
这里的req_size是filemap_readahead调用page_cache_async_ra时传入的last_index - folio->index
这里的last_index - folio->index是在计算什么?
read_pages
这里虽然代码不少,但其实逻辑就是调用readahead或read_folio
static void read_pages(struct readahead_control *rac) // 实现了readahead调readaheadaops->readahead(rac);// 没实现调用read_folioaops->read_folio(rac->file, folio);// 如果都没实现就要挂了
前面已经看过了,比方说ext2的.readahead实际上是mpage_readahead函数
构造bio请求
mpage_readahead
这里逻辑更少,就是封装一个mpage_readpage_args结构,然后调用do_mpage_readpage
void mpage_readahead(struct readahead_control *rac, get_block_t get_block) {struct folio *folio;struct mpage_readpage_args args = {.get_block = get_block,.is_readahead = true,};// 执行do_mpage_readpage(将多个连续页面的读取合并成一个bio)(这个readahead_folio就是)while ((folio = readahead_folio(rac))) {prefetchw(&folio->flags);args.folio = folio;args.nr_pages = readahead_count(rac);args.bio = do_mpage_readpage(&args);}// 如果do_mpage_readpage的结果非NULL(do_mpage_readpage最后会返回一个bio,为空只可能是出问题了)if (args.bio)mpage_bio_submit_read(args.bio);
}
do_mpage_readpage
核心目标:减少磁盘IO次数,将多个小的,可能连续的磁盘块读取请求合并成大的、顺序的读取请求,从而最大化磁盘吞吐量,降低IO延迟
static struct bio *do_mpage_readpage(struct mpage_readpage_args *args)
//1 这一段的作用是计算读取起点、读取终点和文件终点// 块通常是比页小的,页最小也是4k,但磁盘块通常是512字节 (根据sector_t猜测这里指的是磁盘块而非文件系统块// block_in_file是在计算 folio-index * (每页所含的块数). 比如第5个页相当于磁盘的第几个块 (folio也是一种页 复合页) 就是要读的第一个物理块的块号block_in_file = (sector_t)folio->index << (PAGE_SHIFT - blkbits);// last_block是在计算 block_in_file + 要读取的页数 * 每页所含的块数,就是要读到的最后一个物理块的块号last_block = block_in_file + args->nr_pages * blocks_per_page;// i_size_read是根据inode号读文件大小,和blkbits位运算得到的结果是:文件最后一个有效的块号(防止越界last_block_in_file = (i_size_read(inode) + blocksize - 1) >> blkbits;//2 根据先前get_block()的结果 来进行map blocks的动作blocks[page_block] = map_bh->b_blocknr + map_offset + relative_block;//3 循环调用 get_block() → 处理未映射的块├─ 映射块并填充 blocks 数组├─ 处理空洞(`first_hole`)└─ 检查块连续性,否则提交 bio//4 构建和提交 bio → 合并 I/O 请求├─ 分配 bio(`bio_alloc`)├─ 添加页到 bio(`bio_add_folio`)└─ 根据条件提交 bio(`mpage_bio_submit_read`)args->bio = bio_alloc(bdev, bio_max_segs(args->nr_pages), opf, gfp);bio_add_folio(args->bio, folio, length, 0)args->bio = mpage_bio_submit_read(args->bio);//5 返回args->bio结构return args->bio// 返回到上层调用mpage_bio_submit_read
返回之后就该准备提交bio了
如何达成do_mpage_readpage的核心目标?
在mpage_readahead中,
1)第一次执行时,mpage_readpage_args李的struct bio属性为空,这样使得do_mpage_readpage的第一次处理为新建bio
2)循环到第2次时,bio已经是存在了的,这时候do_mpage_readpage会判断,这次要读的块号和新传进来的,是否连续,能否合并。
3)在while循环中持续判断,判断能否和前一个bio合并,如果连续,就继续用(传入的/上一个)bio,如果不能合并就新建。
如果合并的话,就只是更改bio的信息,bio的地址没变,当while到下一轮的时候,args.bio = 这里赋的值仍然是上一次的bio,就达到了沿用之前的bio的效果
总结:循环处理,判断能否和上一个bio合并,不能合并才新建。
块设备驱动
在do_mpage_readpage返回之后,
mpage_readahead会调用mpage_bio_submit_read
这部分就来到了块设备驱动世界的边缘
块设备驱动我还不熟,所以暂且不看