目录
0.前言
内核版本:4.19
文档目的: 主要以null_dev为例来研究多队列的工作机制。本文主要以ext4文件系统为例,介绍null block dev读流程。注意本文所述流程是在开启blk-mq调度器的情况下进行的,开启方法为null blk驱动
中g_no_sched设为0。
1.通过文件系统读取nullb上的文件
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)
如上调用流程同 nullb driver分析2-读文件过程(禁用调度器)
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)
如上调用流程同 nullb driver分析2-读文件过程(禁用调度器)
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;
如上调用流程同 nullb driver分析2-读文件过程(禁用调度器)
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)
如上调用流程同 nullb driver分析2-读文件过程(禁用调度器)
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_del_init(&rq->queuelist)
| 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_mq_flush_plug_list: 将plug->mq_list合并进list
list_splice_init:将plug->mq_list合并进list
list_sort:对list进行排序
list_del_init: 将req从list移除
list_add_tail:通过while循环,将从list下移除的request移入ctx_list链表
blk_mq_sched_insert_requests:对于开启调度器的情况下,将ctx_list链表中的 request尝试与调度队列进行合并
blk_mq_sched_insert_requests
blk_mq_sched_insert_requests
|--dd_insert_requests (hctx, list, false)
| \--while (!list_empty(list))
| rq = list_first_entry(list, struct request, queuelist)
| list_del_init(&rq->queuelist);
| dd_insert_request(hctx, rq, at_head);
| |--if (blk_mq_sched_try_insert_merge(q, rq))
| | return;
| \--list_add(&rq->queuelist, &dd->dispatch) or deadline_add_rq_rb(dd, rq)
\--blk_mq_run_hw_queue(hctx, run_queue_async)
blk_mq_sched_insert_requests:把plug队里的请求下发给调度器
dd_insert_requests: 当使能调度器的情况下,从初始化函数可以知道,对于null_dev使用的是deadline调度器(默认均使用此调度器),因此e->type->ops.mq.insert_requests为dd_insert_requests,因此会调用dd_insert_requests尝试将plug list中的request与调度器队列进行合并
blk_mq_sched_try_insert_merge: 通过循环调用elv_rqhash_find(q, blk_rq_pos(rq))快速查找是否有可以与req合并的请求,如果有就执行合并
如果无法与调度队列合并则通过list_add(&rq->queuelist, &dd->dispatch) or deadline_add_rq_rb(dd, rq) 暂存到调度队列
其中:
.list_add(&rq->queuelist, &dd->dispatch)会将request暂存到dd->dispatch链表
.deadline_add_rq_rb(dd, rq)会将request暂存到dd->sort_list[rq_data_dir(rq)]红黑树,同时如果request支持merger,也会通过elv_rqhash_add(q, rq)加入到hash表,下次可以快速查询新的rq是否可与暂存的requests执行merge
blk_mq_run_hw_queue:处理请求
blk_mq_run_hw_queue
blk_mq_run_hw_queue
\--__blk_mq_delay_run_hw_queue(hctx, async, 0)
\--blk_mq_run_work_fn
\--__blk_mq_run_hw_queue(hctx)
\--blk_mq_sched_dispatch_requests(hctx)
|--if (!list_empty_careful(&hctx->dispatch))
| list_splice_init(&hctx->dispatch, &rq_list)
\--if (!list_empty(&rq_list))
blk_mq_dispatch_rq_list(q, &rq_list, false)
else if (has_sched_dispatch)
blk_mq_do_dispatch_sched(hctx)
\--__dd_dispatch_request
\--do {
rq = e->type->ops.mq.dispatch_request(hctx)
list_add(&rq->queuelist, &rq_list);
} while (blk_mq_dispatch_rq_list(q, &rq_list, true))
else if (hctx->dispatch_busy)
blk_mq_do_dispatch_ctx(hctx)
\--do {
rq = blk_mq_dequeue_from_ctx(hctx, ctx);
list_add(&rq->queuelist, &rq_list);
ctx = blk_mq_next_ctx(hctx, rq->mq_ctx)
} while (blk_mq_dispatch_rq_list(q, &rq_list, true))
else
blk_mq_flush_busy_ctxs(hctx, &rq_list)
blk_mq_dispatch_rq_list(q, &rq_list, false)
blk_mq_run_hw_queue真正将请求下发给驱动
blk_mq_sched_dispatch_requests:派发请求
list_splice_init(&hctx->dispatch, &rq_list):优先处理之前在硬队列中没有派发的请求, 将其移入到rq_list
分几种情况:
(1) blk_mq_dispatch_rq_list:优先派发之前在硬件度列中没有派发的请求到驱动;
(2) blk_mq_do_dispatch_sched:将调度队列中的请求移入rq_list,然后调用blk_mq_dispatch_rq_list,派发到驱动
(3)blk_mq_do_dispatch_ctx: 在硬队列busy的情况下,直接将软队列的rq移入到rq_list,然后派发给驱动, 公平起见,会考虑到轮流对多个软队列执行派发
(4) blk_mq_dispatch_rq_list: 其它情况将rq_list中请求派发给驱动处理
blk_mq_dispatch_rq_list
blk_mq_dispatch_rq_list(q, &rq_list, false)
\--do {
null_queue_rq(hctx, &bd)
} while (!list_empty(list))
blk_mq_dispatch_rq_list将rq_list中请求派发给驱动处理, 对于null blk dev, 主要是调用了null_queue_rq进行请求的处理
2. 总结
当在null block dev中使能调度时,流程变得复杂好多。总体来说,进程的plug队列首先会下发到调度队列,尝试与调度队列的请求进行合并,调度队列会直接下发到驱动