本文是笔者在之前写过的一篇 《iostat IO统计原理linux内核源码分析----基于单通道SATA基础上,对IO传输过程涉及的IO请求的合并、加入IO算法队列、从IO算法队列派发IO请求、deadline调度算法涉及的linux内核源码,做更深层次的探讨,内核版本3.10.96。更详细的源码注释见https://github.com/dongzhiyan-stack/kernel-code-comment

跟上篇一样,开头先来个IO传输的入口函数submit_bio->generic_make_request->blk_queue_bio流程图。

本文正式开讲前,先说几点,rq和req都是同一个意思,都代表IO请求struct request结构。流程图中的虚线代表进入一个新的函数。

1  blk_queue_bio函数IO请求的合并

1.1  bio的合并

1  blk_queue_bio()函数中,首先尝试能否将bio合并到进程plug->list链表上的req,合并成功直接返回。

2  接着执行elv_merge()函数尝试看能否将bio合并到IO算法队列里的req,如果可以合并则执行bio_attempt_front_merge()/bio_attempt_back_merge()将bio前项/后项到匹配的req。这个req合并bio后,req扇区起始/结束地址增大。接着需再尝试将req合并到其他req,就是req的二次合并,具体是执行attempt_front_merge()/attempt_back_merge()函数进行前项/后项二次req合并。如果二次合并失败,则执行elv_merged_request():因为req发生了前项或者后项合并,req的扇区起始或者结束地址增大,需要把req从hash队列或者调度算法deadline红黑树队列中剔除,再按照req新的扇区起始或者结束地址插入队列

3 如果bio没找到能合并的req,就需要get_request()分配新的req,然后调用__elv_add_request()函数把新分配的req添加到IO算法队列。

1.1.1  elv_merge函数讲解

 先看下流程图

 /*尝试3次合并:1 bio能否前项或者后项合并到q->last_merge;2 bio能否后项合并到hash队列的req;3:bio能否前项合并到deadline调度算法红黑树队列的req,返回值ELEVATOR_BACK_MERGEELEVATOR_FRONT_MERGE。如果三者都不能合并只有返回ELEVATOR_NO_MERGE*/

  1. int elv_merge(struct request_queue *q, struct request **req, struct bio *bio)
  2. {
  3.     struct elevator_queue *e = q->elevator;
  4.     struct request *__rq;
  5.     int ret;
  6.     //是否可以把bio合并到q->last_merge,上次rq队列合并过的rqelv_rq_merge_ok是做一些权限检查啥的
  7.     if (q->last_merge && elv_rq_merge_ok(q->last_merge, bio)) {
  8.         //检查bioq->last_merge代表的req磁盘范围是否挨着,挨着则可以合并bioq->last_merge,分为前项合并和后项合并
  9.         ret = blk_try_merge(q->last_merge, bio);
  10.         if (ret != ELEVATOR_NO_MERGE) {
  11.             *req = q->last_merge;
  12.             return ret;
  13.         }
  14.     }
  15.      /*新加入IO调度队列的req会做hash索引,这是根据bio的扇区起始地址在hash表找匹配的req:遍历hash队列req,如果该req的扇区结束地址等于bio的扇区起始地址,bio可以后项合并到req*/
  16.     __rq = elv_rqhash_find(q, bio->bi_sector);
  17.     if (__rq && elv_rq_merge_ok(__rq, bio)) {
  18.         *req = __rq;
  19.         return ELEVATOR_BACK_MERGE;//找到可以合并的req,这里返回ELEVATOR_BACK_MERGE,表示后项合并
  20.     }
  21.     //具体IO调度算法函数cfq_merge或者deadline_merge,找到可以合并的bioreq,这里是把bio前项合并到req
  22.     if (e->type->ops.elevator_merge_fn)
  23.         /*deadline是在红黑树队列里遍历req,如果该req起始扇区地址等于bio的扇区结束地址,返回前项合并(bio合并到req的前边)req是个双重指针,保存这个红黑树队列里匹配到的req*/
  24.         return e->type->ops.elevator_merge_fn(q, req, bio);//返回ELEVATOR_FRONT_MERGE,前项合并
  25.     return ELEVATOR_NO_MERGE;
  26. }

1.1.2  bio_attempt_front_merge、bio_attempt_back_merge函数讲解

    bio_attempt_front_merge()/bio_attempt_back_merge()函数的源码比较简单,就是bio前项/后项合并到req,简单看下二者的源码。

//reqbio二者磁盘范围挨着,req向前合并本次的bio,合并成功返回真

  1. static bool bio_attempt_front_merge(struct request_queue *q,
  2.                     struct request *req, struct bio *bio)
  3. {
  4.     const int ff = bio->bi_rw & REQ_FAILFAST_MASK;
  5.     bio->bi_next = req->bio;
  6.     req->bio = bio;
  7.     req->buffer = bio_data(bio);//bio对应的bh的内存page地址
  8.     //req->__sector代表的磁盘空间起始地址=bio->bi_sector.显然req代表的磁盘空间范围向前扩张
  9.     req->__sector = bio->bi_sector;
  10.     req->__data_len += bio->bi_size; //req扇区范围向前增大
  11.     req->ioprio = ioprio_best(req->ioprio, bio_prio(bio));
  12.     drive_stat_acct(req, 0);
  13.     return true;
  14. }
  15. //reqbio二者磁盘范围挨着,req向后合并本次的bio,合并成功返回真
  16. static bool bio_attempt_back_merge(struct request_queue *q, struct request *req,
  17.                    struct bio *bio)
  18. {
  19.     const int ff = bio->bi_rw & REQ_FAILFAST_MASK;
  20.     req->biotail->bi_next = bio;
  21. req->biotail = bio;
  22. //req->__sector没变,但是req->__data_len累加本次的bio磁盘范围bio->bi_size
  23.     req->__data_len += bio->bi_size;
  24.     req->ioprio = ioprio_best(req->ioprio, bio_prio(bio));
  25.     //IO合并后,更改IO使用率等数据
  26.     drive_stat_acct(req, 0);
  27.     return true;
  28. }

1.1.3 attempt_front_merge()、attempt_back_merge()、attempt_merge()源码讲解

   attempt_front_merge()/attempt_back_merge()函数都是调用attempt_merge()函数,如下:

//之前req发生了前项合并,req的磁盘空间向前增大,从算法队列(比如deadline的红黑树队列)取出req的上一个reqprev,再次尝试把req合并到prev后边

  1. int attempt_front_merge(struct request_queue *q, struct request *rq)
  2. {
  3.     //红黑树中取出req原来的前一个req,prev
  4.     struct request *prev = elv_former_request(q, rq);
  5.     if (prev)//req合并到prev,然后把req从算法队列剔除掉,做一些剔除req的收尾处理,并更新IO使用率数据
  6.         return attempt_merge(q, prev, rq);
  7.     return 0;
  8. }
  9. //之前req发生了后项合并,req的磁盘空间向后增大,从算法队列(比如deadline的红黑树队列)取出req的下一个reqnext,再次尝试把next合并到req后边
  10. int attempt_back_merge(struct request_queue *q, struct request *rq)
  11. {
  12.     //只是从IO调度算法队列里取出req的下一个reqnext,调用的函数elv_rb_latter_request(deadline算法)noop_latter_request(noop算法)
  13.     struct request *next = elv_latter_request(q, rq);
  14.     if (next)//next合并到req,然后把next从算法队列剔除掉,做一些剔除next的收尾处理,并更新IO使用率数据
  15.         return attempt_merge(q, rq, next);
  16.     //如果req没有next req,只能返回0
  17.     return 0;
  18. }

下边现在重点看下attempt_merge()的流程图和源码实现。

  1. static int attempt_merge(struct request_queue *q, struct request *req,
  2.               struct request *next)//next合并到req后边,req来自比如q->last_mergehash队列的req
  3. {
  4.     //检查req扇区范围后边紧挨着next,没有紧挨着返回0
  5.     if (blk_rq_pos(req) + blk_rq_sectors(req) != blk_rq_pos(next))
  6.         return 0;
  7.     //在这里更新req->nr_phys_segments,扇区总数,因为要把next合并到req后边吧
  8.     if (!ll_merge_requests_fn(q, req, next))
  9.         return 0;
  10.     if (time_after(req->start_time, next->start_time))//如果next->start_time更小则赋值于req->start_time
  11.         req->start_time = next->start_time;
  12.     //一个req对应了多个bioreq->biotail应该是指向next上的第一个bio
  13.     req->biotail->bi_next = next->bio;
  14.     //biotail貌似指向了next的最后一个bio
  15.     req->biotail = next->biotail;
  16.     //req吞并了next的磁盘空间范围
  17.     req->__data_len += blk_rq_bytes(next);
  18.     /*调用IO调度算法的elevator_merge_req_fn回调函数。在这里,next已经合并到了rq,fifo队列里,把req移动到next节点的位置,更新req的超时时间。从fifo队列和红黑树剔除next,还更新dd->next_rq[]赋值next的下一个req。因为rq合并了next,扇区结束地址变大了,则rqhash队列中删除掉再重新再hash中排序*/
  19.     elv_merge_requests(q, req, next);
  20.     //next合并打了req,没用了,这个nextin flight队列剔除掉,顺便执行part_round_stats更新io_ticks IO使用率计数
  21.     blk_account_io_merge(next);
  22.     //req优先级,cfq调度算法的概念
  23.     req->ioprio = ioprio_best(req->ioprio, next->ioprio);
  24.     if (blk_rq_cpu_valid(next))
  25.         req->cpu = next->cpu;
  26.     return 1;
  27. }

1.1.4  elv_merged_request()函数讲解

该函数流程图和源码如下:

/*req发生了前项或者后项合并,req的扇区起始或者结束地址增大,需要把req从调度算法deadline红黑树队列或者hash队列中剔除,再按照req新的扇区起始或者结束地址插入队列*/

  1. void elv_merged_request(struct request_queue *q, struct request *rq, int type)
  2. {
  3.     struct elevator_queue *e = q->elevator;
  4.     //如果刚req发生了前项合并,req扇区起始地址增大,把reqdeadline的红黑树队列删除再按照新的扇区起始地址插入红黑树队列,具体调用deadline算法的deadline_merged_request函数
  5.     if (e->type->ops.elevator_merged_fn)
  6.         e->type->ops.elevator_merged_fn(q, rq, type);
  7.     if (type == ELEVATOR_BACK_MERGE)
  8.         //如果刚req发生了后项合并,req扇区结束地址增大,把reqhash队列删除再按照新的扇区结束地址插入hash队列
  9.         elv_rqhash_reposition(q, rq);
  10.     //q->last_merge保存刚发生合并的req
  11.     q->last_merge = rq;
  12. }

1.1.5 __elv_add_request()函数讲解

该函数主要负责将req添加IO算法队列里,流程图与源码如下:

 deadline算法elevator_add_req_fn接口函数是deadline_add_request(),目的是将req插入到红黑树队列和fifo队列__elv_add_request源码如下:

  1. //新分配的req插入IO算法队列,或者是把当前进程plug链表上req全部插入到IO调度算法队列
  2. void __elv_add_request(struct request_queue *q, struct request *rq, int where)
  3. {
  4.     rq->q = q;
  5.     switch (where) {
  6.     case ELEVATOR_INSERT_REQUEUE:
  7.     case ELEVATOR_INSERT_FRONT://前向合并
  8.         rq->cmd_flags |= REQ_SOFTBARRIER;
  9.         list_add(&rq->queuelist, &q->queue_head);//req直接插入q->queue_head链表头而已,并没有进行req合并
  10.         break;
  11.     case ELEVATOR_INSERT_BACK://后向合并
  12.         rq->cmd_flags |= REQ_SOFTBARRIER;
  13.         //循环调用deadline算法的elevator_dispatch_fn接口一直选择派发的reqq->queue_head链表
  14.         elv_drain_elevator(q);
  15.         list_add_tail(&rq->queuelist, &q->queue_head);
  16.         //这里调用底层驱动数据传输函数,就会从rq->queue_head链表取出req发送给磁盘驱动去传输
  17.         __blk_run_queue(q);
  18.         break;
  19.     case ELEVATOR_INSERT_SORT_MERGE://把进程独有的plug链表上的req插入IO调度算法队列里走这里
  20.         if (elv_attempt_insert_merge(q, rq))
  21.             break;
  22.     case ELEVATOR_INSERT_SORT://新分配的req插入的IO调度算法队列
  23.         BUG_ON(rq->cmd_type != REQ_TYPE_FS);
  24.         rq->cmd_flags |= REQ_SORTED;
  25.         //队列插入新的一个req
  26.         q->nr_sorted++;
  27.         if (rq_mergeable(rq)) {
  28.             //新的reqreq->hash添加到IO调度算法的hash链表里
  29.             elv_rqhash_add(q, rq);
  30.             if (!q->last_merge)
  31.                 q->last_merge = rq;
  32.         }
  33.         //req插入到IO调度算法队列里,deadline是插入到红黑树队列和fifo队列
  34.         q->elevator->type->ops.elevator_add_req_fn(q, rq);//deadline算法函数是deadline_add_request()
  35.         break;
  36.     case ELEVATOR_INSERT_FLUSH:
  37.         rq->cmd_flags |= REQ_SOFTBARRIER;
  38.         blk_insert_flush(rq);
  39.         break;
  40.     }
  41. }

好的,前文主要讲解了:submit_bio->generic_make_request->blk_queue_bio发起的IO请求,bio怎么合并到IO算法队列,或者新分配的req怎么插入到IO算法队列。IO算法队列需要特别说明一下,一共有这几个:IO算法默认的hash队列deadline调度算法特有的红黑树rb队列fifo队列

1  IO算法默认的hash队列:每一个新分配的req必然以“ req扇区结束地址”为key插入到hash队列。具体见elv_rqhash_add函数,里边执行hash_add(e->hash, &rq->hash, rq_hash_key(rq))把req添加到hash队列。rq_hash_key(rq)就是hash key,即req扇区结束地址。对hash队列的操作有这几处:

  1. 前文介绍过的blk_queue_bio–>elv_merged_request函数,里边执行elv_rqhash_reposition对req在hash队列重新排序。原因是前边blk_queue_bio–> bio_attempt_back_merge/ bio_attempt_front_merge把bio前项/后项合并到了req,req扇区结束地址可能变大了,就需要执行elv_merged_request函数对这个req按照新的扇区结束地址在hash链表中重新排序。
  2. blk_queue_bio->__elv_add_request->elv_rqhash_add流程是把新分配的req按照它的扇区结束地址添加到hash队列
  3. blk_queue_bio ->elv_merge->elv_rqhash_find 遍历hash队列的req,看哪个req的扇区结束地址等于bio的扇区起始地址,等于则可以把bio后项合并到req。
  4. 当一个req合并到其他req时,要从hash队列剔除掉它,函数流程是attempt_front_merge/attempt_back_merge->attempt_merge->elv_merge_requests->elv_rqhash_del
  5. 当派发req给磁盘驱动时,先执行blk_peek_request->__elv_next_request->deadline_dispatch_requests-> elv_dispatch_add_tail->elv_rqhash_del 从deadline 红黑树和fifo队列取出req到q->queue_head链表,然后把req从hash队列剔除掉,最后再从q->queue_head链表取出req派发个磁盘驱动。

2  deadline调度算法的红黑树rb队列和fifo队列:deadline_add_request()函数负责将新的req添加到红黑树队列和fifo队列。把新的req插入红黑树队列的规则是req的“扇区起始地址”从小到大依次排列。新的req 插入fifo队列比较简单,直接插入fifo 队列dd->fifo_list[data_dir]链表尾部即可。fifo队列存在的意义是,每个req都有一个超时时间dd->fifo_expire[data_dir] ,新的req都是插入fifo队列的尾部。

fifo队列尾部的req都是最晚插入fifo队列的,fifo队列头的req都是最早插入req的。fifo队列头的req最先被检查是否超时了,超时到了则选择该req派发。fifo队列存在的意义是为了保证队列头超时的req尽快得到传输,红黑树队列存在的意义感觉只是让req按照扇区起始地址在上边排列,方便req遍历查找、插入删除、合并等的。

deadline_add_request和deadline_add_rq_rb源码如下:

  1. static void   deadline_add_request(struct request_queue *q, struct request *rq)
  2. {
  3.     struct deadline_data *dd = q->elevator->elevator_data;
  4.     const int data_dir = rq_data_dir(rq);
  5.     //req添加到红黑树队列里
  6.     deadline_add_rq_rb(dd, rq);
  7.     //设置req调度超时时间,超时时间到,则会把fifo队列头的req派发给驱动
  8.     rq_set_fifo_time(rq, jiffies + dd->fifo_expire[data_dir]);
  9.     //req插入到fifo队列尾部
  10.     list_add_tail(&rq->queuelist, &dd->fifo_list[data_dir]);
  11. }
  12. //req添加到红黑树队列里
  13. static void   deadline_add_rq_rb(struct deadline_data *dd, struct request *rq)
  14. {
  15.      struct rb_root *root = deadline_rb_root(dd, rq);
  16.      //rq添加到红黑树里,就是按照每个req的起始扇区排序的
  17.      elv_rb_add(root, rq);
  18. }

2  进程plug链表req的插入IO算法队列

内核很多地方派发req实际是执行blk_flush_plug_list()函数把req插入IO算法队列,比如blk_queue_bio()->blk_flush_plug_list(),blk_flush_plug()->blk_flush_plug_list()。blk_flush_plug_list函数源码和流程图如下:

 //把进程plug链表上的req依次插入IO调度算法队列上

  1. void blk_flush_plug_list(struct blk_plug *plug, bool from_schedule)
  2. {
  3.     struct request_queue *q;
  4.     unsigned long flags;
  5.     struct request *rq;
  6.     LIST_HEAD(list);
  7.     unsigned int depth;
  8.     list_splice_init(&plug->list, &list);
  9.     //plug链表上的req排序,应该是按照每个req的起始扇区地址排序,起始扇区小的排在前
  10.     list_sort(NULL, &list, plug_rq_cmp);
  11.     q = NULL;
  12.     depth = 0;
  13.     //依次取出进程plug链表上的req依次插入IO调度算法队列上
  14.     while (!list_empty(&list)) {
  15.         //取出req
  16.         rq = list_entry_rq(list.next);
  17.         //plug链表删除req
  18.         list_del_init(&rq->queuelist);
  19.         //在这里把req插入到IO调度算法队列里
  20.          __elv_add_request(q, rq, ELEVATOR_INSERT_SORT_MERGE);
  21.         //深度depth1
  22.         depth++;
  23.     }
  24.     if (q)//里边执行__blk_run_queue派发req给磁盘驱动
  25.         queue_unplugged(q, depth, from_schedule);
  26. }

     这个函数就是就是依次取出进程plug链表上的req依次执行__elv_add_request()插入IO算法队列。__elv_add_request()函数源码上一节已经详细解释过。还有一点需要注意,blk_flush_plug_list()函数最后执行queue_unplugged()才会把刚才插入IO算法队列的req派发给磁盘驱动,才能完成最终的磁盘数据传输。queue_unplugged()里实际是执行__blk_run_queue()->__blk_run_queue_uncond()->scsi_request_fn()把req派发给磁盘驱动。下文重点讲解。

3  __blk_run_queue()派发req到磁盘驱动

3.1  req整体派发流程

先看下整体流程图

//从IO算法队列选择req派发给磁盘驱动

  1. static void scsi_request_fn(struct request_queue *q)
  2. {
  3.     struct scsi_device *sdev = q->queuedata;
  4.     struct Scsi_Host *shost;
  5.     struct scsi_cmnd *cmd;
  6.     struct request *req;
  7.     shost = sdev->host;
  8.     for (;;) {
  9.         int rtn;
  10.          //把IO算法队列req先添加到q->queue_head链表头(默认是链表尾,IO高优先级进程是链表头),然后从q->queue_head链表头取出待派发的req,针对req的信息分配SCSI命令结构体cmd并赋值
  11.         req = blk_peek_request(q);
  12.          //如果req为NULL或者向磁盘驱动队列派发的req太多break跳出,不再派发
  13.         if (!req || !scsi_dev_queue_ready(q, sdev))
  14.             break;
  15.         //req 传输前的一些操作主要是把req从q->queue_head链表尾剔除掉
  16.         blk_start_request(req);
  17.         cmd = req->special;
  18.         //发送SCSI命令,真正开始传输数据
  19.         rtn = scsi_dispatch_cmd(cmd);
  20.     }
  21. }

重点是执行blk_peek_request()选择派发的req,分配SCSI命令cmd并赋值,源码如下:

  1. struct request *blk_peek_request(struct request_queue *q)
  2. {
  3.     /* 循环执行__elv_next_request(),从q->queue_head队列取出待进行IO数据传输的req。如果q->queue_head没有req,则执行deadline_dispatch_requestsfifo队列选择派发的reqq->queue_head链表*/
  4.      while ((rq = __elv_next_request(q)) != NULL) {
  5.    /*1 分配一个struct scsi_cmnd *cmd,使用reqcmd进行部分初始化cmd->request=reqreq->special = cmd,还有cmd->transfersize传输字节数、cmd->sc_data_direction DMA传输方向
  6.      2 先遍历req上的每一个bio,再得到每个biobio_vec,把bio对应的文件数据在内存中的首地址bvec->bv_pag+bvec->bv_offset写入scatterlistscatterlist是磁盘数据DMA传输有关的数据结构,scatterlist保存到bidi_sdb->table.sglbidi_sdbreqstruct scsi_data_buffer成员。
  7. */ 
  8.         ret = q->prep_rq_fn(q, rq);//scsi_prep_fn
  9.         if (ret == BLKPREP_OK) {
  10.             break;
  11.     }
  12. }

  blk_peek_request()函数整体总结如下:

1 从q->queue_head队列头取出待进行IO数据传输的req.如果q->queue_head没有req,则执行deadline_dispatch_requests从fifo队列选择派发的req

2 分配一个struct scsi_cmnd *cmd,使用req对cmd进行部分初始化cmd->request=req,req->special = cmd,还有cmd->transfersize传输字节数、cmd->sc_data_direction DMA传输方向

3 先遍历req上的每一个bio,再得到每个bio的bio_vec,把bio对应的文件数据在内存中的首地址bvec->bv_pag+bvec->bv_offset写入scatterlist。scatterlist是磁盘数据DMA传输有关的数据结构,scatterlist保存到bidi_sdb->table.sgl,bidi_sdb是req的struct scsi_data_buffer成员。

blk_peek_request()函数里执行__elv_next_request(),目的是:从q->queue_head链表取出待传输的req,如果q->queue_head链表没有req,则执行deadline_dispatch_requests()从fifo队列选择派发的req到q->queue_head。

  1. static inline struct request *__elv_next_request(struct request_queue *q)
  2. {
  3.     while (1) {
  4.         //q->queue_head取出待传输的req,如果q->queue_head没有req,则执行deadline_dispatch_requestsfifo队列选择派发的req
  5.         if (!list_empty(&q->queue_head)) {
  6.             rq = list_entry_rq(q->queue_head.next);
  7.             return rq;
  8.     }
  9.     if (unlikely(blk_queue_bypass(q)) ||
  10.             !q->elevator->type->ops.elevator_dispatch_fn(q, 0))//deadline_dispatch_requests()选择派发的req
  11.         return NULL;
  12. }

deadline_dispatch_requests()函数是deadline  IO调度算法的核心,重点讲解。

3.2  deadline_dispatch_requests()IO调度算法派发req

deadline_dispatch_requests()函数流程,源码和流程图如下:

static int deadline_dispatch_requests(struct request_queue *q, int force) 

  1. {
  2.     struct deadline_data *dd = q->elevator->elevator_data;
  3.     //如果fifo队列有read req,list_empty返回0reads1
  4.     const int reads = !list_empty(&dd->fifo_list[READ]);
  5.     //如果fifo队列有write req,list_empty返回0writes1
  6.     const int writes = !list_empty(&dd->fifo_list[WRITE]);
  7.     struct request *rq;
  8.     int data_dir;
  9.     //每次从红黑树选取一个req发给驱动传输,这个req的下一个req保存在next_rq[],现在又向驱动发送req传输,优先先从next_rq取出req
  10.     if (dd->next_rq[WRITE])
  11.         rq = dd->next_rq[WRITE];
  12.     else
  13.         rq = dd->next_rq[READ];
  14.     /*如果dd->batching大于等于dd->fifo_batch,不再使用next_rq,否则会一直只向后使用红黑树队列的req向驱动发送传输,队列前边的req得不到发送*/
  15.     if (rq && dd->batching < dd->fifo_batch)
  16.         goto dispatch_request;
  17.     /*选择readwrite req,因为一直选择read req给驱动传输,那write req就饿死了*/
  18.     if (reads) {
  19.         BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[READ]));
  20.         /*write req要传送给驱动,并且write req被饥饿次数达到上限,就强制选择跳转选择write req,防止一直选择read req给驱动传输,write req得不到选择而starve饥饿,每次write req得不到选择而饥饿则starved++writes_starved是饥饿的次数上限,starved大于writes_starved,就强制选择write req*/
  21.         if (writes && (dd->starved++ >= dd->writes_starved))
  22.             goto dispatch_writes;
  23.         //否则下面选择read req
  24.         data_dir = READ;
  25.         goto dispatch_find_request;
  26.     }
  27.     if (writes) {
  28. dispatch_writes:
  29.         BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[WRITE]));
  30.         //dd->starved0
  31.         dd->starved = 0;
  32.         //下面选择write req,就一个赋值操作
  33.         data_dir = WRITE;
  34.         goto dispatch_find_request;
  35.     }
  36.     return 0;
  37. dispatch_find_request:
  38.     //deadline_check_fifo:如果deadline fifo队列有超时的req要传输返回1,或者next_rq没有暂存reqif都成立。则从fifo队列头取出req
  39.     if (deadline_check_fifo(dd, data_dir) || !dd->next_rq[data_dir]) {
  40.         //取出fifo队列头的req,最早入fifo队列的req,最早入队的req当然更容易超时
  41.         rq = rq_entry_fifo(dd->fifo_list[data_dir].next);
  42.     } else {
  43.         //否则直接取出next_rq暂存的req
  44.         rq = dd->next_rq[data_dir];
  45.     }
  46.     //batching0
  47.     dd->batching = 0;
  48. dispatch_request://到这里,req直接来自next_rq或者fifo队列,这个req就要被发给驱动传输了
  49.     //batching1
  50.     dd->batching++;
  51.     /*req添加到rqqueue_head队列,设置新的next_rq,并把reqfifo队列和红黑树队列剔除,将来磁盘驱动程序就是从queue_head链表取出req传输的*/
  52.     deadline_move_request(dd, rq);
  53.     return 1;
  54. }

deadline_dispatch_requests()函数简单来说是:选择合适待派发给驱动传输的req,然后把req添加到q->queue_head链表,然后设置新的next_rq,并把req从fifo队列和红黑树队列剔除。将来向磁盘驱动程序派发的req就是从queue_head链表取出的。req来源有:上次派发设置的next_rq;read req派发过多而选择的write req;fifo 队列上超时要传输的req,统筹兼顾,有固定策略。

1 首先呢,从dd->next_rq[WRITE/ READ]获取上次派发req后设置的next  req,if (rq && dd->batching < dd->fifo_batch)这个判断是为了防止一直派发dd->next_rq[WRITE/ READ],每派发一个next req,dd->batching就会加1,如果dd->batching < dd->fifo_batch成立,就goto dispatch_request直接使用dd->next_rq[WRITE/ READ]指定的next  req。

2 如果if (rq && dd->batching < dd->fifo_batch) 不成立,说明派发的dd->next_rq[WRITE/ READ]指定的next  req太多了,该派发fifo队列的req了,这个req更紧急。此时就会进入if (reads) 或者if (writes) 分支,最后执行goto dispatch_find_requestdd->fifo_list[data_dir] fifo队列选择派发的req,具体流程是:先执行if (deadline_check_fifo(dd, data_dir) || !dd->next_rq[data_dir])deadline_check_fifo(dd, data_dir)函数是判断fifo队列有没有超时的req,有则执行rq = rq_entry_fifo(dd->fifo_list[data_dir].next) 取出fifo队列头的req(这是最早加入fifo队列的req,最早入队的req当然更容易超时)

回到第二步,还有一点没讲,就是if (reads)分支里的if (writes && (dd->starved++ >= dd->writes_starved)) ,每派发一个read  req(data_dir = READ)dd->starved++1,等到dd->starved++ >= dd->writes_starvedgoto dispatch_writes执行data_dir = WRITE,这样就会派发write reqdd->starved的作用是派发dd->writes_starvedread req后,就该派发write req了,防止write req饿着。

 deadline_dispatch_requests()最后执行的deadline_move_request()函数,作用是把req添加到q->queue_head链表,设置新的next_rq,并把reqfifo队列和红黑树队列剔除,将来磁盘驱动程序就是从q->queue_head链表取出req派发的。源码如下:

//req添加到q->queue_head链表,设置新的next_rq,并把reqfifo队列和红黑树队列剔除,将来磁盘驱动程序就是从q->queue_head链表取出req传输的

  1. static void   deadline_move_request(struct deadline_data *dd, struct request *rq)
  2. {
  3.     //reqread还是write
  4.     const int data_dir = rq_data_dir(rq);
  5.     dd->next_rq[READ] = NULL;
  6.     dd->next_rq[WRITE] = NULL;
  7.     //从红黑树队列中取出req的下一个req作为next_rq,下次deadline_dispatch_requests()选择派发给的req时就可能是它了
  8.     dd->next_rq[data_dir] = deadline_latter_request(rq);
  9.     //req的磁盘空间end地址
  10.     dd->last_sector = rq_end_sector(rq);
  11.     //req添加到q->queue_head链表,并把reqfifo队列和红黑树队列剔除,将来磁盘驱动程序就是从queue_head链表取出req派发的
  12.     deadline_move_to_dispatch(dd, rq);
  13. }

deadline_move_to_dispatch()函数源码如下:

//req添加到q->queue_head链表,并把reqfifo队列和红黑树队列剔除,将来磁盘驱动程序就是从queue_head链表取出req派发的

  1. static inline void  deadline_move_to_dispatch(struct deadline_data *dd, struct request *rq)
  2. {
  3.     struct request_queue *q = rq->q;
  4.     //fifo队列和红黑树队列剔除req
  5.     deadline_remove_request(q, rq);
  6.     //req添加到q->queue_head链表,将来磁盘驱动程序就是从queue_head链表取出req派发的
  7.     elv_dispatch_add_tail(q, rq);
  8. }

deadline_remove_request()源码如下:

//deadline算法从fifo队列和红黑树剔除req。剔除前如果req原本是dd->next_rq[]保存req,还要找到req在红黑树的下一个req赋值给dd->next_rq[]

  1. static void deadline_remove_request(struct request_queue *q, struct request *rq)
  2. {
  3.     struct deadline_data *dd = q->elevator->elevator_data;
  4.     //fifo队列剔除rq
  5.     rq_fifo_clear(rq);
  6.     //如果req原本是dd->next_rq[]保存req,则要找到req在红黑树的下一个req赋值给dd->next_rq[],然后把req从红黑树中剔除
  7.     deadline_del_rq_rb(dd, rq);
  8. }

deadline_del_rq_rb()函数源码如下:

//如果req原本是dd->next_rq[]保存req,则要找到req在红黑树的下一个req赋值给dd->next_rq[],然后把req从红黑树中剔除

  1. static inline void  deadline_del_rq_rb(struct deadline_data *dd, struct request *rq)
  2. {
  3.     const int data_dir = rq_data_dir(rq);
  4.     /*这个if判断是说rq原本是dd->next_rq[]保存req,现在rq马上要从红黑树中剔除,则要找到rq在红黑树的下一个req赋值给dd->next_rq[]deadline算法选择派发的req时会优先选择dd->next_rq[]保存的req*/
  5.     if (dd->next_rq[data_dir] == rq)
  6.         dd->next_rq[data_dir] = deadline_latter_request(rq);
  7.     //deadline_rb_root(dd, rq)是取出调度算法的读或者写红黑树队列头rb_root,然后把req从这个红黑树队列剔除掉
  8.     elv_rb_del(deadline_rb_root(dd, rq), rq);
  9. }

//req添加到rq->queue_head链表,将来磁盘驱动程序就是从queue_head链表取出req派发的

  1. void elv_dispatch_add_tail(struct request_queue *q, struct request *rq)
  2. {
  3.     if (q->last_merge == rq)
  4.         q->last_merge = NULL;
  5.     //reqhash队列剔除
  6.     elv_rqhash_del(q, rq);
  7.     q->nr_sorted--;
  8.     //结束扇区
  9.     q->end_sector = rq_end_sector(rq);
  10.     q->boundary_rq = rq;
  11.     //req添加到rq->queue_head链表,将来磁盘驱动程序就是从queue_head链表取出req派发的
  12.     list_add_tail(&rq->queuelist, &q->queue_head);
  13. } 
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐