一、基本原理
Linux块设备层使用了plug/unplug(蓄流/泄流)的机制来提升IO吞吐量。基本原理为:当IO请求提交时,不知直接提交给底层驱动,而是先将其放入一个队列中(相当于水池),待一定时机或周期后再将该队列中的请求统一下发。将请求放入队列的过程即plug(蓄流)过程,统一下发请求的过程即为unplug(泄流)过程。每个请求在队列中等待的时间不会太长,通常在ms级别。
如此设计,可以增加IO合并和排序的机会,便于提升磁盘访问效率。

二、plug
1、基本流程
从mapping层提交到块设备层的io请求为bio,bio会在块设备进行合并,并生成新的request,并经过IO调度(排序和合并)之后下发到底层。下发request时,通过请求队列的make_request_fn接口,其中实质为将请求放入per task的plug队列,当队列满或在进行调度时(schedule函数中)会根据当前进程的状态将该队列中的请求flush到派发队列中,并触发unplug(具体流程后面介绍)。

per task的plug队列:新内核版本中实现的机制。IO请求提交时先链入此队列,当该队列满时(>BLK_MAX_REQUEST_COUNT),会flush到相应设备的请求队列中(request_queue)。
优点:per task维护plug队列,可以避免频繁对设备的请求队列操作导致的锁竞争,能提升效率。

2、plug基本代码流程如下:
submit_bio->
    generic_make_request->
        make_request->
            blk_queue_bio->
                list_add_tail(&req->queuelist, &plug->list);//将请求加入plug队列

三、unplug
unplug分同步unplug和异步unplug两种方式。
同步unplug即当即通过调用blk_run_queue对下发请求队列中的情况。
异步unplug,通过唤醒kblockd工作队列来对请求队列中的请求进行下发。

1、kblockd工作队列的初始化:
1) 分配工作队列
主要代码流程:
blk_dev_init ->
    alloc_workqueue //分配工作队列
2)  初始化工作队列
blk_alloc_queue_node():

点击(此处)折叠或打开

  1. /*在指定node上分配请求队列*/
  2. struct request_queue *blk_alloc_queue_node(gfp_t gfp_mask, int node_id)
  3. {
  4. struct request_queue *q;
  5. int err;
  6. /*分配请求队列需要的内存,从slab中分配,并初始化为0*/
  7. q = kmem_cache_alloc_node(blk_requestq_cachep,
  8. gfp_mask | __GFP_ZERO, node_id);
  9. if (!q)
  10. return NULL;
  11. if (percpu_counter_init(&q->mq_usage_counter, 0))
  12. goto fail_q;
  13. q->id = ida_simple_get(&blk_queue_ida, 0, 0, gfp_mask);
  14. if (q->id < 0)
  15. goto fail_c;
  16. q->backing_dev_info.ra_pages =
  17. (VM_MAX_READAHEAD * 1024) / PAGE_CACHE_SIZE;
  18. q->backing_dev_info.state = 0;
  19. q->backing_dev_info.capabilities = BDI_CAP_MAP_COPY;
  20. q->backing_dev_info.name = "block";
  21. q->node = node_id;
  22. err = bdi_init(&q->backing_dev_info);
  23. if (err)
  24. goto fail_id;
  25. /*设置laptop模式下的定时器*/
  26. setup_timer(&q->backing_dev_info.laptop_mode_wb_timer,
  27. laptop_mode_timer_fn, (unsigned long) q);
  28. /*
  29. * 关键点:设置请求队列的超时定时器,默认超时时间为30s,当30s内IO请求未完成时,定时器到期,
  30. * 进行重试或错误处理。这是IO 错误处理架构中的关键点之一,在内核老版本中(2.6.38?),该定时器
  31. * 是在scsi中间层定义的,新版本中将其上移至块设备层。Fixme:为何要这样?*/
  32. setup_timer(&q->timeout, blk_rq_timed_out_timer, (unsigned long) q);
  33. /*初始化各个队列*/
  34. INIT_LIST_HEAD(&q->queue_head);
  35. INIT_LIST_HEAD(&q->timeout_list);
  36. INIT_LIST_HEAD(&q->icq_list);
  37. #ifdef CONFIG_BLK_CGROUP
  38. INIT_LIST_HEAD(&q->blkg_list);
  39. #endif
  40. INIT_LIST_HEAD(&q->flush_queue[0]);
  41. INIT_LIST_HEAD(&q->flush_queue[1]);
  42. INIT_LIST_HEAD(&q->flush_data_in_flight);
  43. /*初始化delay_work,用于在kblockd中异步unplug请求队列*/
  44. INIT_DELAYED_WORK(&q->delay_work, blk_delay_work);
  45. kobject_init(&q->kobj, &blk_queue_ktype);
  46. mutex_init(&q->sysfs_lock);
  47. spin_lock_init(&q->__queue_lock);
  48. /*
  49. * By default initialize queue_lock to internal lock and driver can
  50. * override it later if need be.
  51. */
  52. q->queue_lock = &q->__queue_lock;
  53. /*
  54. * A queue starts its life with bypass turned on to avoid
  55. * unnecessary bypass on/off overhead and nasty surprises during
  56. * init. The initial bypass will be finished when the queue is
  57. * registered by blk_register_queue().
  58. */
  59. q->bypass_depth = 1;
  60. __set_bit(QUEUE_FLAG_BYPASS, &q->queue_flags);
  61. init_waitqueue_head(&q->mq_freeze_wq);
  62. if (blkcg_init_queue(q))
  63. goto fail_id;
  64. return q;
  65. fail_id:
  66. ida_simple_remove(&blk_queue_ida, q->id);
  67. fail_c:
  68. percpu_counter_destroy(&q->mq_usage_counter);
  69. fail_q:
  70. kmem_cache_free(blk_requestq_cachep, q);
  71. return NULL;
  72. }

2) kblockd工作队列的工作内容
kblockd工作队列的工作内容有由blk_delay_work()函数实现,主要就是调 用__blk_run_queue进行unplug请求队列。

点击(此处)折叠或打开

  1. /*IO请求队列的delay_work,用于在kblockd中异步unplug请求队列*/
  2. static void blk_delay_work(struct work_struct *work)
  3. {
  4. struct request_queue *q;
  5. /*获取delay_work所在的请求队列*/
  6. q = container_of(work, struct request_queue, delay_work.work);
  7. spin_lock_irq(q->queue_lock);
  8. /*直接run queue,最终调用request_fn对队列中的请求逐一处理*/
  9. __blk_run_queue(q);
  10. spin_unlock_irq(q->queue_lock);
  11. }

2、unplug机制
内核中设计了两种unplug机制:
1)调度时进行unplug(异步方式)
当发生内核调度时,当前进程sleep前,先将当前task的plug列表中的请求flush到派发队列中,并进行unplug。
主要代码流程如下:
schedule->
    sched_submit_work ->
        blk_schedule_flush_plug()->
            blk_flush_plug_list(plug, true) ->注意:这里传入的from_schedule参数为true,表示将触发异步unplug,即唤醒kblockd工作队列来进行unplug操作。后续的kblockd的唤醒周期在块设备驱动中设置,比如scsi中设置为3ms。
                queue_unplugged->
                    blk_run_queue_async

queue_unplugged():

点击(此处)折叠或打开

  1. /*unplug请求队列,plug相当于蓄水,将请求放入池子(请求队列)中,unplug相当于放水,即开始调用请求队列的request_fn(scsi_request_fn)来处理请求队列中的请求,将请求提交到scsi层(块设备驱动层)*/
  2. static void queue_unplugged(struct request_queue *q, unsigned int depth,
  3. bool from_schedule)
  4. __releases(q->queue_lock)
  5. {
  6. trace_block_unplug(q, depth, !from_schedule);
  7. /*调用块设备驱动层提供的request_fn接口处理请求队列中的请求,分异步和同步两种情况。*/
  8. if (from_schedule)
  9. /*异步unplug,即通过kblockd工作队列来处理,该工作队列定期唤醒(5s),通过这种方式可以控制流量,提高吞吐量*/
  10. blk_run_queue_async(q);
  11. else
  12. /*同步unplug,即直接调用设备驱动层提供的request_fn接口处理请求队列中的请求*/
  13. __blk_run_queue(q);
  14. spin_unlock(q->queue_lock);
  15. }

blk_run_queue_async():

点击(此处)折叠或打开

  1. /*异步unplug,即通过kblockd工作队列来处理,该工作队列定期唤醒(5s),通过这种方式可以控制流量,提高吞吐量*/
  2. void blk_run_queue_async(struct request_queue *q)
  3. {
  4. if (likely(!blk_queue_stopped(q) && !blk_queue_dead(q)))
  5. /*唤醒kblockd相关的工作队列,进行unplug处理,注意:这里的delay传入0表示立刻唤醒,kblockd对应的处理接口为:blk_delay_work*/
  6. mod_delayed_work(kblockd_workqueue, &q->delay_work, 0);
  7. }

scsi_request_fn()://scsi块设备驱动的request_fn()接口,其中当scsi命令下发失败时,会重设kblockd,延迟unplug请求队列。

点击(此处)折叠或打开

  1. static void scsi_request_fn(struct request_queue *q)
  2. {
  3. ...
  4. /*
  5. * Dispatch the command to the low-level driver.
  6. */
  7. /*将scsi命令下发到底层驱动,当返回非0时,表示命令下发失败,则当前的请求队列需要被plug*/
  8. rtn = scsi_dispatch_cmd(cmd);
  9. spin_lock_irq(q->queue_lock);
  10. /*命令下发失败,需要plug请求队列*/
  11. if (rtn)
  12. goto out_delay
  13. ...
  14. out_delay:
  15. if (sdev->device_busy == 0)
  16. /*命令下发失败,需要延迟处理,需plug请求队列,设置3ms定时启动kblockd工作队列,进行请求队列的unplug*/
  17. blk_delay_queue(q, SCSI_QUEUE_DELAY);
  18. blk_delay_queue
  19. /*在指定msecs时间后启动kblockd工作队列*/
  20. void blk_delay_queue(struct request_queue *q, unsigned long msecs)
  21. {
  22. if (likely(!blk_queue_dead(q)))
  23. queue_delayed_work(kblockd_workqueue, &q->delay_work,
  24. msecs_to_jiffies(msecs));
  25. }

2)提交IO请求时(make_request)进行unplug
提交IO请求时(make_request),先将请求提交时先链入此队列,当该队列满时(>BLK_MAX_REQUEST_COUNT),会flush到相应设备的请求队列中(request_queue)。
主要代码流程为:
submit_bio->
    generic_make_request->
        make_request->
            blk_queue_bio->
                blk_flush_plug_list(plug, false) ->注意:这里传入的from_schedule参数为false,表示将触发同步unplug,即当即下发请求。
                    queue_unplugged->
                        blk_run_queue_async ->
                            __blk_run_queue

普通块设备的 make_request接口在3.10内核版本中被设置为blk_queue_bio,相应代码分析如下:

点击(此处)折叠或打开

  1. /*在submit_bio中被调用,用于合并bio,并提交请求(request),请求提交到per task的plug list中*/
  2. void blk_queue_bio(struct request_queue *q, struct bio *bio)
  3. {
  4. const bool sync = !!(bio->bi_rw & REQ_SYNC);
  5. struct blk_plug *plug;
  6. int el_ret, rw_flags, where = ELEVATOR_INSERT_SORT;
  7. struct request *req;
  8. unsigned int request_count = 0;
  9. /*
  10. * low level driver can indicate that it wants pages above a
  11. * certain limit bounced to low memory (ie for highmem, or even
  12. * ISA dma in theory)
  13. */
  14. /*bounce buffer(回弹缓冲区)使用*/
  15. blk_queue_bounce(q, &bio);
  16. if (bio_integrity_enabled(bio) && bio_integrity_prep(bio)) {
  17. bio_endio(bio, -EIO);
  18. return;
  19. }
  20. if (bio->bi_rw & (REQ_FLUSH | REQ_FUA)) {
  21. spin_lock_irq(q->queue_lock);
  22. where = ELEVATOR_INSERT_FLUSH;
  23. goto get_rq;
  24. }
  25. /*
  26. * Check if we can merge with the plugged list before grabbing
  27. * any locks.
  28. */
  29. /*尝试将bio合并到request中*/
  30. if (blk_attempt_plug_merge(q, bio, &request_count))
  31. return;
  32. spin_lock_irq(q->queue_lock);
  33. el_ret = elv_merge(q, &req, bio);
  34. /*向后合并*/
  35. if (el_ret == ELEVATOR_BACK_MERGE) {
  36. if (bio_attempt_back_merge(q, req, bio)) {
  37. elv_bio_merged(q, req, bio);
  38. if (!attempt_back_merge(q, req))
  39. elv_merged_request(q, req, el_ret);
  40. goto out_unlock;
  41. }
  42. /*向前合并*/
  43. } else if (el_ret == ELEVATOR_FRONT_MERGE) {
  44. if (bio_attempt_front_merge(q, req, bio)) {
  45. elv_bio_merged(q, req, bio);
  46. if (!attempt_front_merge(q, req))
  47. elv_merged_request(q, req, el_ret);
  48. goto out_unlock;
  49. }
  50. }
  51. /*不能合并,需要新建request来处理bio*/
  52. get_rq:
  53. /*
  54. * This sync check and mask will be re-done in init_request_from_bio(),
  55. * but we need to set it earlier to expose the sync flag to the
  56. * rq allocator and io schedulers.
  57. */
  58. rw_flags = bio_data_dir(bio);
  59. /*判断是否需要sync,即直接将IO请求unplug(提交到块设备驱动层),不用等待kblockd来定期plug*/
  60. if (sync)
  61. rw_flags |= REQ_SYNC;
  62. /*
  63. * Grab a free request. This is might sleep but can not fail.
  64. * Returns with the queue unlocked.
  65. */
  66. /*从请求队列中取一个request*/
  67. req = get_request(q, rw_flags, bio, GFP_NOIO);
  68. if (unlikely(!req)) {
  69. bio_endio(bio, -ENODEV);    /* @q is dead */
  70. goto out_unlock;
  71. }
  72. /*
  73. * After dropping the lock and possibly sleeping here, our request
  74. * may now be mergeable after it had proven unmergeable (above).
  75. * We don't worry about that case for efficiency. It won't happen
  76. * often, and the elevators are able to handle it.
  77. */
  78. /*将bio加入新的request中*/
  79. init_request_from_bio(req, bio);
  80. if (test_bit(QUEUE_FLAG_SAME_COMP, &q->queue_flags))
  81. req->cpu = raw_smp_processor_id();
  82. plug = current->plug;
  83. /*如果有plug,则将请求加入到plug的list中,如果没有则直接调用__blk_run_queue提交请求*/
  84. if (plug) {
  85. /*
  86. * If this is the first request added after a plug, fire
  87. * of a plug trace. If others have been added before, check
  88. * if we have multiple devices in this plug. If so, make a
  89. * note to sort the list before dispatch.
  90. */
  91. if (list_empty(&plug->list))
  92. trace_block_plug(q);
  93. else {/*如果请求队列中的请求数超过了限值,则先unplug?*/
  94. if (request_count >= BLK_MAX_REQUEST_COUNT) {
  95. blk_flush_plug_list(plug, false);
  96. trace_block_plug(q);
  97. }
  98. }
  99. /*把请求加入到plug的list中,当plug的list满了后(>BLK_MAX_REQUEST_COUNT),会flush到相应设备的请求队列中(request_queue)*/
  100. list_add_tail(&req->queuelist, &plug->list);
  101. blk_account_io_start(req, true);
  102. } else {
  103. spin_lock_irq(q->queue_lock);
  104. add_acct_request(q, req, where);
  105. /*如果没有plug控制,最终调用此接口处理队列中的请求,最终会调用请求队列的request_fn接口处理请求*/
  106. __blk_run_queue(q);
  107. out_unlock:
  108. spin_unlock_irq(q->queue_lock);
  109. }
  110. }

原文地址: http://blog.chinaunix.net/uid-14528823-id-4778396.html

Linux Kernel 3.10内核源码分析--块设备层request plug/unplug机制相关推荐

  1. Linux kernel 3.10内核源码分析--进程上下文切换

    一.疑问 进程调度时,当被选中的next进程不是current进程时,需要进行上下文切换. 进行上下文切换时,有一些问题不太容易理解,比如: 1.进程上下文切换必然发生在内核态吗? 2.上下文切换后原 ...

  2. Linux kernel 3.10内核源码分析--TLB相关--TLB概念、flush、TLB lazy模式

    一.概念及基本原理 TLB即Translation Lookaside Buffer,是MMU中的一种硬件cache,用于缓存页表,即缓存线性地址(虚拟地址)到物理地址的映射关系. 如果没有TLB,那 ...

  3. Linux kernel 3.10内核源码分析--slab原理及相关代码

    1.基本原理 我们知道,Linux保护模式下,采用分页机制,内核中物理内存使用buddy system(伙伴系统)进行管理,管理的内存单元大小为一页,也就是说使用buddy system分配内存最少需 ...

  4. Linux kernel 3.10内核源码分析--进程退出exit_code

    进程退出时,有相应的exit_code,可用于判断进程退出的原因. 比如,waitpid()接口用于等待进程退出,此时被等待退出的进程的返回值比较重要,需要用其来判断进程退出的相应状态,而这就是通过进 ...

  5. kernel 3.10内核源码分析--中断--中断和异常返回流程

    一.问题 1.内核调度与中断/异常/系统调用的关系如何? 2.信号处理与中断/异常/系统调用的关系如何? 3.内核抢占与中断/异常/系统调用的关系如何? 4.内核线程的调度有何特别之处?中断/异常/系 ...

  6. kernel 3.10内核源码分析--内核栈及堆栈切换

    1.概念 Linux中有3种栈: 1)用户栈.当进程处于用户态时使用,位于进程地址空间(用户态部分(如:0-0xc0000000))底部,用户态分配局部变量和函数调用时时,使用该栈,跟平时我们见到和理 ...

  7. Linux Kernel 2.6.9源码分析 -- send/recieve 报文

    Linux Kernel 2.6.9源码分析 – send/recieve 报文 可用户socket报文读写的函数有以下几对: ssize_t read(int fd, void *buf, size ...

  8. ernel 3.10内核源码分析--KVM相关--虚拟机运行

    1.基本原理 KVM虚拟机通过字符设备/dev/kvm的ioctl接口创建和运行,相关原理见之前的文章说明. 虚拟机的运行通过/dev/kvm设备ioctl VCPU接口的KVM_RUN指令实现, 在 ...

  9. Linux内核源码分析之内存管理

    本文站的角度更底层,基本都是从Linux内核出发,会更深入.所以当你都读完,然后再次审视这些功能的实现和设计时,我相信你会有种豁然开朗的感觉. 1.页 内核把物理页作为内存管理的基本单元. 尽管处理器 ...

最新文章

  1. Opencv模块功能介绍
  2. 使用Adobe Lifecycle ES将若干个word合并成一个PDF
  3. 服务器无法继续该事务 3400000006 错误原因--JAVA调用SQLSERVER存储过程时过程发生异常内部事务未
  4. 基础Git操作与GitHub协作吐血整理,收好!| 原力计划
  5. PHP+MySQL实现新闻管理系统
  6. 免疫算法在物流配送中心选址中的应用
  7. 明天省赛,我都还不太熟悉Dev - C++,怎么切换成C++11了?它的报错看不懂呀,那花八分钟看看这篇文章吧~解决你的困惑。
  8. 弹珠css3,使用CSS3实现的弹球小动画
  9. 对vue的理解 什么是渐进式框架
  10. hosts文件及域名解析过程
  11. 西北农林科技大学计算机类到底咋样,为了去读985,西北农林科技大学值得选择吗?...
  12. 基于Pytorch的手写汉字识别
  13. 关于SLAM的系列很有价值的网文_拔剑-浆糊的传说_新浪博客
  14. 关闭 WinRAR 广告弹出窗口
  15. hash冲突原理及解决方法
  16. esp32和MPU6500 I2C通信
  17. BetaFlight模块设计之二十九:滤波模块分析
  18. 阮一峰老师的JavaScript标准参考教程:函数和ES6函数的拓展
  19. Android平台Airplay的实现方法
  20. 四款常用浏览器比较、简介

热门文章

  1. Android面试题(五) --重要
  2. php单文件短链接,php实现的短网址算法分享
  3. 【流量】一觉醒来发现CSDN博客访问量增加十倍!原来是这个原因
  4. 科大星云诗社动态20210417
  5. 科大星云诗社动态20210428
  6. 近三月浏览器网页访问量统计
  7. [C++调试笔记]推动粒子move.cpp
  8. IP插件:批量替换论文图片
  9. 十一、“由专入分易,由分入专难。”(2020.12.18)
  10. 云炬Android开发笔记 2-1项目初始化