一看二做三总结
分类: 嵌入式
2018-06-07 22:34:16
本文乃fireaxe原创,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,并注明原作者及原链接。内容可任意使用,但对因使用该内容引起的后果不做任何保证。
作者:fireaxe.hq@outlook.com
博客:fireaxe.blog.chinaunix.net
1. 基本原理
block用户态调用read函数时,会陷入内核的system call 函数sys_read。然后调用vfs层的读入口vfs_read。我们的read解析之旅也由此开始。
block解析的终点是调用块设备的接口为止,也就是request解析函数。(这里假设块设备提供的函数为wiccablk_request。该函数的入参是reqeust queue。而read的入参是文件描述符。本文除了理出中间的函数调用关系外,更重要的是,整理出参数传递过程中的转换,从而理解block layer的工作原理。
block layer是用linux下文件处理的核心。其对上是vfs接口层,对下是块设备驱动的request解析函数。中间对文件系统做了抽象,方便不同文件系统挂入其中。
块设备驱动:
提供块设备的读写接口,简单说就是读取一个或多个连续sector。以xxxblk_reqeust()的形式对上提供服务。上层模块会把读写请求包装成bio,放到驱动对应的request queue中,然后启动驱动去处理对应的request。驱动开发者就是要根据设备的特性,实现这种处理。
文件系统:
根据硬盘上的文件系统结构,构造superblock,构造inode节点,连接block层文件与目录处理函数(类似于透传,当然经常会加入一些特殊的处理)。
superblock会在mount时进行构造,只构造一次。文件与目录的inode节点则需要每次构造(当然如果上次构造的还在cache中,则不用再次构造了)。
2. read函数调用关系
2.1 第一条链:从read到磁盘读取
(1) ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
(2) ssize_t __vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
(3) ssize_t new_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
iocb->ki_filp = *pposfilp;
iocb->ki_flags = iocb_flags(filp);
iocb->ki_pos = *ppos;
iov_iter_init(&iter, READ, &{ .iov_base = buf, .iov_len = len }, 1, len);
(4) filp->f_op->read_iter = ext4_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
(5) generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
filp = iocb->ki_filp;
ppos = &iocb->ki_pos;
written = read;
(6) ssize_t do_generic_file_read(struct file *filp, loff_t *ppos, struct iov_iter *iter, ssize_t written)
mapping = file->f_mapping; (就是file system中的inode->i_mapping)
ra = & filp->f_ra;
offset = *ppos & ~PAGE_MASK;
req_size = (iter->count + PAGE_SIZE - 1) >> PAGE_SHIFT;
= (len + PAGE_SIZE - 1) >> PAGE_SHIFT
(7) void page_cache_sync_readahead(struct address_space *mapping,
struct file_ra_state *ra, struct file *filp, pgoff_t offset, unsigned long req_size)
hit_readahead_marker = false;
(8) unsigned long ondemand_readahead(struct address_space *mapping,
struct file_ra_state *ra, struct file *filp, bool hit_readahead_marker,
pgoff_t offset, unsigned long req_size)
lookahead_size = 0;
nr_to_read = req_size;
(9) int __do_page_cache_readahead(struct address_space *mapping, struct file *filp,
pgoff_t offset, unsigned long nr_to_read, unsigned long lookahead_size)
gfp = mapping->gfp_mask | __GFP_COLD | __GFP_NORETRY | __GFP_NOWARN
(10) int read_pages(struct address_space *mapping, struct file *filp,
struct list_head *pages, unsigned int nr_pages, gfp_t gfp)
blk_start_plug会初始化一个blk_plug节点,并挂入进程的task->plug。也即是说,block io请求与进程相关,只有同一个进程的请求才会merge到一起,这就避免了全局锁的使用。
blk_finish_plug会最终处理前面挂进来的plug。可以看到,plug只是用于通知进程有一个新的读写请求,但并不会立即执行,因为进程要尽量合并读写请求,以减少对磁盘的操作。
plug只是个请求,数据不会通过它传递。数据的传递通过inode->i_mapping->a_ops中的readpage实现。readpage会把读请求转换的为bio,并挂入blk_start_plug中的队列中去。等待blk_finish_plug时处理这些bio。(关于readpage的调用关系,在后面分析)
(11) void blk_start_plug(struct blk_plug *plug)
mapping->a_ops->readpage = ext4_readpage(struct file *filp, struct page *page);
void blk_finish_plug(struct blk_plug *plug)
from_schedule = false;
(12) void blk_flush_plug_list(struct blk_plug *plug, bool from_schedule)
q来自于plug中存储的list;
depth用于限制递归层数;
后续的处理就比较简单了,q最终传到块设备驱动自定义的wiccablk_reqeust中,进行磁盘的数据的实际操作。
(13) void queue_unplugged(struct request_queue *q, unsigned int depth,
bool from_schedule)
(14) void __blk_run_queue(struct request_queue *q)
(15) void __blk_run_queue_uncond(struct request_queue *q)
(16) q->request_fn = void wiccablk_request(struct request_queue *q)
2.2 第二条链:mapping->a_ops->readpage生成bio
下面回过头来分析readpage,这个函数很重要,因为他关系到disk cache的实现与io调度器的使用。
cache的实现这里不做分析。bio的生成则完全由该函数实现,并会吧bio加入到对应的reqeust queue中去。
(1) int ext4_readpage(struct file *file, struct page *page)
mapping = page->mapping;
pages = NULL;
nr_pages = 1;
另一个入口是ext4_readpages,其原型如下:
ext4_readpages(struct file *file, struct address_space *mapping, struct list_head *pages, unsigned nr_pages)
往下传入时,参数如下:
mapping = page->mapping;
page = NULL;
(2) int ext4_mpage_readpages(struct address_space *mapping,
struct list_head *pages, struct page *page, unsigned nr_pages)
这一步将来成bio。此后的操作都将以bio作为基本调度单元。
下一步是把bio加入到reqeust queue。
(3) blk_qc_t submit_bio(struct bio *bio)
(4) blk_qc_t generic_make_request(struct bio *bio)
(5) q->make_request_fn = blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)
此处要解释下make_request_fn的赋值,通常的块设备都会通过blk_init_queue()来注册q->request_fn,而blk_init_queue()中会调用如下语句给make_request_fn赋值:
blk_queue_make_request(q, blk_queue_bio);
也就是说,通常的块设备都默认使用blk_queue_bio作为make_request_fn函数。当然也有例外,很多模块都实现了自己的make_request_fn函数。
blk_queue_bio用于把bio加入到request中,然后把request家进入到plug的list中,并等待blk_flush_plug_list来处理。
3.3 IO scheduler
在blk_queue_bio中的处理,使用电梯算法进行IO调度,讲几种电梯算法的文章很多,这里不展开讨论。需要说的是,blk_queue_bio中调用了elv_merge,从而进入了IO
调度算法的范围。
(1) int elv_merge(struct request_queue *q, struct request **req, struct bio *bio)
(2) e->type->ops.elevator_merge_fn = int cfq_merge(struct request_queue *q, struct request **req, struct bio *bio)
注:用dump_stack打印出的调用链
[ 176.584162] [] wiccablk_request+0x4c/0x410 [test_wiccablk]
[ 176.584563] [] __blk_run_queue+0x40/0x58
[ 176.584923] [] queue_unplugged+0x38/0x140
[ 176.585293] [] blk_flush_plug_list+0x1d4/0x248
[ 176.585642] [] blk_finish_plug+0x40/0x50
[ 176.585986] [] __do_page_cache_readahead+0x1b8/0x290
[ 176.586379] [] ondemand_readahead+0x108/0x2b0
[ 176.586668] [] page_cache_sync_readahead+0x60/0xa0
[ 176.586970] [] generic_file_read_iter+0x5c4/0x740
[ 176.587271] [] ext4_file_read_iter+0x44/0x50
[ 176.587567] [] __vfs_read+0xd0/0x120
[ 176.587832] [] vfs_read+0x8c/0x148
[ 176.588327] [] SyS_read+0x54/0xb0
[ 219.581052] [] vfs_write+0xa8/0x1b8
[ 219.581321] [] SyS_write+0x54/0xb0
本文乃fireaxe原创,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,并注明原作者及原链接。内容可任意使用,但对因使用该内容引起的后果不做任何保证。
作者:fireaxe.hq@outlook.com
博客:fireaxe.blog.chinaunix.net