目录
blk_mq_try_issue_list_directly
0.前言
内核版本:4.19
文档目的: 主要以null_dev为例来研究多队列的工作机制。本文主要以ext4文件系统为例,介绍null block dev读流程。
注意本文所述流程是在关闭blk-mq调度器的情况下进行的。
1.通过文件系统读取nullb上的文件(禁用调度器)
通过在null blk drv的copy_from_nullb中加dump_stack可以读调用流程
通过perf也可以看到类似的流程
new_sync_read
vfs_read
|--rw_verify_area(READ, file, pos, count)
\--__vfs_read(file, buf, count, pos)
\--if (file->f_op->read)
file->f_op->read(file, buf, count, pos)
else if (file->f_op->read_iter)
new_sync_read(file, buf, count, pos)
|--init_sync_kiocb(&kiocb, filp)
|--iov_iter_init(&iter, READ, &iov, 1, len)
\--call_read_iter(filp, &kiocb, &iter)
\--ext4_file_read_iter(kio, iter)
#new_sync_read:对于ext4的文件系统,由于没有定义file->f_op->read,而是定义了file->f_op->read_iter回调,因此会调用new_sync_read函数。
这里查看了Documentation/filesystems/vfs.txt文件中关于read_iter的说明。这里说read_iter有可能是一种异步的读,并且会读取到iov_iter
read_iter: possibly asynchronous read with iov_iter as destination
##init_sync_kiocb:用于初始化kiocb,包括文件描述符,标记,文件位置等。kiocb保存了文件描述符file的位置等信息,相当于由kiocb控制对IO的读写操作,所以称为内核IO控制块
##iov_iter_init:iov_iter是一个向量迭代器,指向一个iovec向量数组,用于推进io的读写, iov_iter_init用于初始化此向量迭代器;
##call_read_iter: 将执行file->f_op->read_iter(kio, iter),最终调用了ext4_file_read_iter(kio, iter), 执行文件读操作
ext4_file_read_iter
ext4_file_read_iter(kio, iter)
\--generic_file_read_iter(iocb, to)
\--if (iocb->ki_flags & IOCB_DIRECT)
对DIRECT IO的处理(TODO)
else
generic_file_buffered_read(iocb, iter, retval)
ext4_file_read_iter实际会分为direct io的处理和非direct io的处理,这里只分析非direct io的处理,主要调用generic_file_buffered_read
generic_file_buffered_read
generic_file_buffered_read
|--if (PageReadahead(page))
| page_cache_async_readahead//异步读
|--wait_on_page_locked_killable(page)//如果页面锁定则说明正在读,block等待end bio唤醒即可
|--if (!trylock_page(page)) //如果页面没有被锁定,则跳转到page_not_up_to_date,锁定页面
| goto page_not_up_to_date
|--page_not_up_to_date:lock_page_killable(page)
|--mapping->a_ops->readpage(filp, page)//从磁盘读入缓存,end bio唤醒
\--if (PageUptodate(page))
unlock_page(page);
goto page_ok;
先来看一下generic_file_buffered_read的总体情况,根据前面的读取调用栈可知道实际调用了page_cache_async_readahead,执行异步预读操作。
#page_cache_async_readahead:执行异步预读操作。
#wait_on_page_locked_killable:重新检查页面是否已经被锁定,如果被锁定,则将被阻塞,直到读取完毕唤醒进程(通过end_io回调?)
#readpage:对于页面没有被锁定的情况,说明并未启动页面读取,则通过readpage回调执行磁盘读取
page_cache_async_readahead
根据调用栈流程,下面重点介绍page_cache_async_readahead的流程:
page_cache_async_readahead
\--ondemand_readahead(mapping, ra, filp, true, offset, req_size)
\--__do_page_cache_readahead(mapping, filp, offset, req_size, 0)
\--read_pages(mapping, filp, &page_pool, nr_pages,gfp_mask)
|--blk_start_plug(&plug)
|--ext4_readpages(filp, mapping, pages, nr_pages)
| \--ext4_mpage_readpages()
\--blk_finish_plug(&plug)
#ondemand_readahead:执行预读算法,其中offset为page在mapping所处的index,req_size为请求预读的大小
##__do_page_cache_readahead: 真正执行读取一个disk chunk。它会先通过radix_tree_lookup查找page在mapping->i_pages中是否存在,如果不存在它会预先分配page, 然后通过read_pages来执行页面读取,如果存在则直接通过read_pages读取页面;
###blk_start_plug: 初始化进程blk_plug
###ext4_readpages: 通过看ftrace, 调用ext4_mpage_readpages,最终从块设备中读取多个page, 主要通过submit_bio下发到了进程的plug队列
###blk_finish_plug: flush plug队列的请求(flush plug有哪些时机?TODO)
blk_finish_plug
blk_finish_plug(&plug)
|--blk_flush_plug_list(plug, false)
|--if (!list_empty(&plug->mq_list))
blk_mq_flush_plug_list(plug, from_schedule)
|--list_splice_init(&plug->mq_list, &list)
|--list_sort(NULL, &list, plug_ctx_cmp)
|--while (!list_empty(&list))
| list_add_tail(&rq->queuelist, &ctx_list)
\-- blk_mq_sched_insert_requests(this_q, this_ctx, &ctx_list,from_schedule)
blk_finish_plug: 将进程plug队列向下派发
#blk_flush_plug_list:此处没有使用调度器,因此from_schedule参数为false,由于plug->mq_list非空,因此会调用blk_mq_flush_plug_list
##blk_mq_flush_plug_list: 将plug->mq_list合并进list,下发到硬件队列直接处理或暂存到软队列
###list_splice_init:将plug->mq_list合并进list
###list_sort:对list进行排序
###list_add_tail:通过while循环,将list下所有的request移入ctx_list链表
###blk_mq_sched_insert_requests:根据是否有调度队列,及当前硬队列是否busy来将ctx_list中的request派发给硬件队列进行直接处理 或 插入到软队列暂存,此处没有采用调度处理,且当前硬队列处于非busy状态,因此直接将ctx_list中的request派发给了硬队列进行直接处理
blk_mq_sched_insert_requests
blk_mq_sched_insert_requests(this_q, this_ctx, &ctx_list,from_schedule)
|--hctx = blk_mq_map_queue(q, ctx->cpu)
\--if (e && e->type->ops.mq.insert_requests)
e->type->ops.mq.insert_requests(hctx, list, false)
else
if (!hctx->dispatch_busy && !e && !run_queue_async)
blk_mq_try_issue_list_directly(hctx, list)
blk_mq_insert_requests(hctx, ctx, list)
blk_mq_run_hw_queue(hctx, run_queue_async)
blk_mq_sched_insert_requests:如果没有使用调度器且当前硬队列非busy,则将request直接派发给硬件队列进行直接处理,剩余的request将插入到软队列暂存;此处没有采用调度处理,且当前硬队列处于非busy状态,因此直接将ctx_list中的request派发给了硬队列进行直接处理
#blk_mq_map_queue:获取软队列映射的硬队列
#如果e && e->type->ops.mq.insert_requests,则将请求插入到调度队列
#blk_mq_try_issue_list_directly:如果满足!hctx->dispatch_busy && !e && !run_queue_async,将通过blk_mq_try_issue_list_directly直接将request发送给硬件队列处理,对于null block dev,当前就是处于这样的状态;
#blk_mq_insert_requests:如果还有剩余的request,将通过blk_mq_insert_requests将其插入到软队列ctx
#blk_mq_run_hw_queue: 处理软对列中的request(TODO)
blk_mq_try_issue_list_directly
blk_mq_try_issue_list_directly(hctx, list)
\--while (!list_empty(list))
blk_mq_request_issue_directly(rq)
|--blk_mq_get_dispatch_budget(hctx)
|--hctx = blk_mq_map_queue(rq->q, ctx->cpu)
\--__blk_mq_try_issue_directly(hctx, rq, &unused_cookie, true)
|--blk_mq_get_driver_tag(rq)
\--__blk_mq_issue_directly(hctx, rq, cookie)
\--q->mq_ops->queue_rq(hctx, &bd)
\--null_queue_rq(hctx, &bd)
blk_mq_try_issue_list_directly通过while循环将list链表中的所有request,直接发送到硬队列进行处理
null_queue_rq
null_queue_rq(hctx, &bd)
\--blk_mq_start_request(bd->rq)
\--null_handle_cmd(cmd)
|--null_handle_rq(cmd)
| req_for_each_segment(bvec, rq, iter)
| null_transfer(nullb, bvec.bv_page, len, bvec.bv_offset...)
| |--copy_from_nullb(nullb, page, off, sector, len)
| \--flush_dcache_page(page)
\--blk_mq_complete_request(cmd->rq)
null_queue_rq对请求进行处理,主要是从内存模拟的磁盘中拷贝到bio对应的bio_vec描述的page segment,处理完成后通过bio end回调通知完成
#blk_mq_complete_request:负责上报request及bio完成,并执行对应的回调
blk_mq_complete_request
blk_mq_complete_request(cmd->rq)
\--__blk_mq_complete_request(rq)
\--rq->q->softirq_done_fn(rq)
\--null_softirq_done_fn(rq)
\--end_cmd(blk_mq_rq_to_pdu(rq))
\--blk_mq_end_request(cmd->rq, cmd->error)
|--blk_update_request(rq, error, blk_rq_bytes(rq))
| |--while(req->bio)
| req_bio_endio(req, bio, bio_bytes, error)
\--__blk_mq_end_request(rq, error)
\--if (rq->end_io)
rq->end_io(rq, error)
else
blk_mq_free_request(rq)
blk_mq_complete_request负责上报request及bio完成,并执行对应的回调
#null_softirq_done_fn: softirq_done_fn回调为null_softirq_done_fn
#此处的rq->end_io为空, 因此会执行blk_mq_free_request
#blk_update_request:遍历req的每个bio,执行bio endio回调,unlock_page解锁bio所关联的所有page
这样generic_file_buffered_read->wait_on_page_locked_killable的进程就会被唤醒了
#__blk_mq_end_request:由于rq->end_io为空,会执行blk_mq_free_request
2.数据结构
struct iovec {
const struct iovec *iov;// 指向iovec向量数组
unsigned long nr_segs;// iovec向量数组剩余的io向量个数
size_t iov_offset;//当前io向量偏移
size_t count;//剩余的字节数
}
/*内核IO控制块*/
struct kiocb {
struct file *ki_filp;//读写文件描述符
loff_t ki_pos;//读写文件的位置
void (*ki_complete)(struct kiocb *iocb, long ret, long ret2);
void *private;
int ki_flags;
u16 ki_hint;
u16 ki_ioprio; /* See linux/ioprio.h */
} __randomize_layout;
3.参考文档
1.Documentation/filesystems/vfs.txt