在《内核block层IO调度器—bfq算法之2源码详解》文章的基础上,本文主要总结一个bfq算法的源码要点,bfq算法真的复杂!本文基于centos 8.3,内核版本4.18.0-240.el8,详细源码注释见 https://github.com/dongzhiyan-stack/linux-4.18.0-240.el8。

要想了解bfq调度算法的工作原理,我觉得主要还得看派发IO请求的__bfq_dispatch_request()函数。该函数涉及怎么选择IO调度器使用的bfqq和entity、派发bfqq上的IO请求、bfqq因配额不够或者没有要派发的IO请求而过期失效、bfqq的entity过期失效而从st->active红黑树剔除而移动到st->active红黑树或者计算新的entity->finish而重新加入st->active红黑树、分配一个新的bfqq后把它的entity加入st->active红黑树、激活一个在st->active红黑树的entity而把它加入到st->active红黑树。下边先把涉及的流程框图贴下:

下文围绕这个图较为深入的讲解一下bfq调度算法的工作原理。首先还是把涉及的核心源码简明扼要贴下:

1:bfq 重要函数源码总结

先从派发IO请求的__bfq_dispatch_request()函数开始。

1.1派发IO请求入口函数

  1. static struct request *__bfq_dispatch_request(struct blk_mq_hw_ctx *hctx)
  2. {
  3. //选择派发IO请求的bfqq
  4. bfqq = bfq_select_queue(bfqd);
  5. //从bfqq选择一个派发的IO请求req
  6. rq = bfq_dispatch_rq_from_bfqq(bfqd, bfqq);
  7. }

入口函数__bfq_dispatch_request(),主要调用bfq_select_queue()和bfq_dispatch_rq_from_bfqq()两个函数。

  1. static struct bfq_queue *bfq_select_queue(struct bfq_data *bfqd)
  2. {
  3. bfqq = bfqd->in_service_queue;
  4. if (!bfqq)
  5. goto new_queue;
  6. ........
  7. check_queue:
  8. next_rq = bfqq->next_rq;
  9. if (next_rq) {
  10. if (bfq_serv_to_charge(next_rq, bfqq) >bfq_bfqq_budget_left(bfqq)) {
  11. reason = BFQQE_BUDGET_EXHAUSTED;//bfqq 的配额消耗光了
  12. goto expire;//bfqq 的配额消耗光而令bfqq到期失效
  13. }else{
  14. goto keep_queue;//继续使用bfqd->in_service_queue这个bfqq,保持
  15. }
  16. }
  17. ...........
  18. reason = BFQQE_NO_MORE_REQUESTS;//bfqq没有req要传输了
  19. expire:
  20. //bfqq过期失效
  21. bfq_bfqq_expire(bfqd, bfqq, false, reason);
  22. new_queue:
  23. //选择一个新的bfqq赋于bfqd->in_service_queue,作为bfq当前使用的bfqq
  24. bfqq = bfq_set_in_service_queue(bfqd);
  25. if (bfqq) {
  26. goto check_queue;//goto check_queue 分支,派发新选中的bfqq的IO请求
  27. }
  28. keep_queue:
  29. return bfqq;
  30. }

bfq_select_queue()中先判断bfqq=bfqd->in_service_queue是否有正在使用的bfqq,即判断bfqd->in_service_queue是否是NULL。如果bfqd->in_service_queue是NULL,表示没有正在使用的bfqq,则goto new_queue分支执行bfq_set_in_service_queue()选择一个bfqq赋于bfqd->in_service_queue,然后goto check_queue分支。如果bfqd->in_service_queue非NULL,则从bfqq->next_rq取出该bfqq要派发的IO请求。如果bfqq->next_rq是NULL,表示该bfqq没有要派发的IO请求了,则reason = BFQQE_NO_MORE_REQUESTS,然后执行bfq_bfqq_expire()令bfqq因没有派发的IO请求而过期失效。如果bfqq->next_rq非NULL,则判断该bfqq是否有足够的配额派发bfqq->next_rq这个IO请求,有足够的配额的话则goto keep_queue分支直接返回bfqd->in_service_queue这个bfqq。否则goto expire分支执行bfq_bfqq_expire()令bfqq因配额不够而过期失效。bfq_select_queue()函数有点复杂,在本文开头的示意图也有重点标出。下边把bfq_set_in_service_queue ()和bfq_bfqq_expire()函数源码简单列下:

  1. static struct bfq_queue *bfq_set_in_service_queue(struct bfq_data *bfqd)
  2. {
  3. //从sd->next_in_service取出entity赋于sd->in_service_entity,作为本次要使用的entity。然后查找一个新的entity赋于sd->next_in_service,作为下一次使用的entity
  4. struct bfq_queue *bfqq = bfq_get_next_queue(bfqd);
  5. //本质是 bfqd->in_service_queue = bfqq
  6. __bfq_set_in_service_queue(bfqd, bfqq);
  7. return bfqq;
  8. }

下边看下bfq_bfqq_expire()函数相关源码

  1. void bfq_bfqq_expire(struct bfq_data *bfqd,struct bfq_queue *bfqq,bool compensate,enum bfqq_expiration reason)
  2. {
  3. ..............
  4. //计算更新bfqq->max_budget,如果bfqq上还有req(bfqq->next_rq)要派发,则计算根据next_rq传输的字节数计算bfqq->entity的配额,保证bfqq的配额还能传输完next_rq
  5. __bfq_bfqq_recalc_budget(bfqd, bfqq, reason);
  6. ..............
  7. //令bfqq过期失效
  8. if (__bfq_bfqq_expire(bfqd, bfqq, reason))
  9. ..............
  10. }
  11. static bool __bfq_bfqq_expire(struct bfq_data *bfqd, struct bfq_queue *bfqq,
  12. enum bfqq_expiration reason)
  13. {
  14. if (RB_EMPTY_ROOT(&bfqq->sort_list) &&//bfqq上没有派发的req
  15. !(reason == BFQQE_PREEMPTED &&
  16. idling_needed_for_service_guarantees(bfqd, bfqq)))
  17. {   //bfqq上没有IO请求要派发了而过期失效,把bfqq的entity从st->active红黑树剔除而移入st->active红黑树
  18. bfq_del_bfqq_busy(bfqd, bfqq, true);
  19. }else{
  20. //bfqq配额消耗光了而过期失效,把bfqq的entity重新加入到st->active红黑树。因为bfqq上还有IO请求要派发
  21. bfq_requeue_bfqq(bfqd, bfqq, true);
  22. }
  23. //bfqd->in_service_queue = NULL 清空
  24. return __bfq_bfqd_reset_in_service(bfqd);
  25. }
  26. void bfq_del_bfqq_busy(struct bfq_data *bfqd, struct bfq_queue *bfqq,
  27. bool expiration)
  28. {
  29. //清理bfqq busy状态
  30. bfq_clear_bfqq_busy(bfqq);
  31. //从st->active 红黑树剔除bfqq的entity
  32. bfq_deactivate_bfqq(bfqd, bfqq, true, expiration);
  33. }
  34. void bfq_deactivate_bfqq(struct bfq_data *bfqd, struct bfq_queue *bfqq,
  35. bool ins_into_idle_tree, bool expiration)
  36. {
  37. struct bfq_entity *entity = &bfqq->entity;
  38. bfq_deactivate_entity(entity, ins_into_idle_tree, expiration);
  39. }
  40. //bfqd->in_service_queue指向的正在使用的bfqq,配额消耗光但bfqq上还有要派发IO请求,于是把bfqq重新插入到st->active红黑树,并且选择一个新的bfqq作为bfqd->in_service_queue。
  41. void bfq_requeue_bfqq(struct bfq_data *bfqd, struct bfq_queue *bfqq,
  42. bool expiration)
  43. {
  44. struct bfq_entity *entity = &bfqq->entity;
  45. bfq_activate_requeue_entity(entity, false,bfqq == bfqd->in_service_queue, expiration);
  46. }
  47. //把entity加入到st->active红黑树
  48. static void bfq_activate_requeue_entity(struct bfq_entity *entity,
  49. bool non_blocking_wait_rq,
  50. bool requeue, bool expiration)
  51. {
  52. //把entity加入到st->active红黑树
  53. __bfq_activate_requeue_entity(entity, sd, non_blocking_wait_rq);
  54. ///查找新的 next_in_service entity赋于sd->next_in_service
  55. if (!bfq_update_next_in_service(sd, entity, expiration) &&!requeue)
  56. break;
  57. }

可以发现,bfqq因没有要派发的IO请求而过期失效, bfq_bfqq_expire()函数的调用流程是bfq_bfqq_expire()->__bfq_bfqq_expire()->bfq_del_bfqq_busy()->bfq_deactivate_bfqq()->bfq_deactivate_entity()。bfqq因没有足够的配额派发IO请求而过期失效, bfq_bfqq_expire()函数的调用流程是bfq_bfqq_expire()->__bfq_bfqq_expire()->bfq_requeue_bfqq()->bfq_activate_requeue_entity()。

1.2 bfq_deactivate_entity()从st->active红黑树剔除entity

  1. //把entity从st->active红黑树剔除
  2. static void bfq_deactivate_entity(struct bfq_entity *entity,
  3. bool ins_into_idle_tree,
  4. bool expiration)
  5. {
  6. //把entity从st->active 红黑树剔除,计算新的entity->finish把entity按照entity->>finish加入到st->active红黑树
  7. if (!__bfq_deactivate_entity(entity, ins_into_idle_tree))
  8. ...........
  9. //如果要从st->active红黑树剔除的entity是sd->next_in_service 指向的entity
  10. if (sd->next_in_service == entity)
  11. ///查找新的 next_in_service entity赋于sd->next_in_service
  12. bfq_update_next_in_service(sd, NULL, expiration);
  13. }

bfq_deactivate_entity()中主要调用__bfq_deactivate_entity()和bfq_update_next_in_service()函数。__bfq_deactivate_entity()函数把entity从st->active红黑树剔除,bfq_update_next_in_service()函数选择下一次运行的entity赋于sd->next_in_service。先看下__bfq_deactivate_entity()源码,bfq_update_next_in_service()放最后讲解。

  1. bool __bfq_deactivate_entity(struct bfq_entity *entity, bool ins_into_idle_tree)
  2. {
  3. struct bfq_sched_data *sd = entity->sched_data;
  4. struct bfq_service_tree *st;
  5. bool is_in_service;
  6. //找到entiry所属st
  7. st = bfq_entity_service_tree(entity);
  8. //entity是sd正在使用的entity则is_in_service为true
  9. is_in_service = entity == sd->in_service_entity;
  10. //service是entity已经消耗的配额,除以weight得到entity已经消耗的虚拟运行时间,再加上entity->start得到entity->finish
  11. bfq_calc_finish(entity, entity->service);
  12. if (is_in_service)//把sd->in_service_entity指向的entity从红黑树剔除时,要把sd->in_service_entity清NULL
  13. sd->in_service_entity = NULL;
  14. else
  15. entity->service = 0;
  16. if (entity->tree == &st->active)//如果entity处于st->active树,从st->acitve tree剔除entity
  17. bfq_active_extract(st, entity);
  18. else if (!is_in_service && entity->tree == &st->idle)//如果entity处于st->idle树,并且entity不是st正在使用的entity
  19. bfq_idle_extract(st, entity);
  20. //ins_into_idle_tree为false则不会把entity再插入到st->idle,而是释放掉entity。或者st->vtime>=entity->finish也是释放掉entity
  21. if (!ins_into_idle_tree || !bfq_gt(entity->finish, st->vtime))
  22. bfq_forget_entity(st, entity, is_in_service);//entity不再被用到,释放bfqq,把entity剔除掉
  23. else
  24. bfq_idle_insert(st, entity);//把entity再插入st->idle树,并可能更新st->first_idle或st->last_idle
  25. return true;
  26. }

__bfq_deactivate_entity()函数主要作用是:把entity从st->active或st->idle红黑树踢掉,ins_into_idle_tree为true则把entity再插入st->idle树,否则直接释放掉entity及bfqq。

1.3 bfq_activate_requeue_entity()把entity加入st->active红黑树

  1. //把entity加入到st->active红黑树
  2. static void bfq_activate_requeue_entity(struct bfq_entity *entity,
  3. bool non_blocking_wait_rq,
  4. bool requeue, bool expiration)
  5. {
  6. //把entity加入到st->active红黑树
  7. __bfq_activate_requeue_entity(entity, sd, non_blocking_wait_rq);
  8. ///查找新的 next_in_service entity赋于sd->next_in_service
  9. if (!bfq_update_next_in_service(sd, entity, expiration) &&!requeue)
  10. break;
  11. }

__bfq_activate_requeue_entity()函数主要调用__bfq_activate_requeue_entity()把entity加入st->active红黑树,然后也调用bfq_update_next_in_service()选择下一次运行的entity赋于sd->next_in_service。先看下__bfq_activate_requeue_entity()函数源码:

  1. static void __bfq_activate_requeue_entity(struct bfq_entity *entity,
  2. struct bfq_sched_data *sd,
  3. bool non_blocking_wait_rq)
  4. {
  5. //如果entity是调度器正在使用的entiry或entity处于st->active红黑树
  6. if (sd->in_service_entity == entity || entity->tree == &st->active)
  7. //根据entity->service计算entity的虚拟运行时间并累加到entity->finish,最后把entity按照新的entity->finish插入st->active红黑树
  8. __bfq_requeue_entity(entity);
  9. else//否则到这里说明entity 是新创建的
  10. //计算entity->start,按照entity->budget计算entity的虚拟运行时间并累加到entity->finish,把entity按照entity->finish插入st->active红黑树
  11. __bfq_activate_entity(entity, non_blocking_wait_rq);
  12. }

该函数两个分支:如果entity是调度器正在使用的entiry或entity处于st->active树,然后根据entity->service计算entity的虚拟运行时间并累加到entity->finish,最后把entity按照新的entity->finish插入st->active红黑树。否则,entity及其bfqq是新创建的,计算entity->start,直接把entity插入st->active红黑树。这两个函数源码是:

  1. //bfqq的entity之前就在st->active红黑树,这里根据entity->service计算entity的虚拟运行时间并累加到entity->finish,最后把entity按照新的entity->finish插入st->active红黑树
  2. static void __bfq_requeue_entity(struct bfq_entity *entity)
  3. {
  4. if (entity == sd->in_service_entity) {//entity是IO调度器正在使用的entity
  5. //根据entity->service/entity->weight计算entity已经消耗的虚拟运行时间,entity->start加上这段虚拟运行时间就是entity->finish
  6. bfq_calc_finish(entity, entity->service);
  7. //用entity->finish更新entity->start,重置了
  8. entity->start = entity->finish;
  9. if (entity->tree)//如果entity在st->active树,则从st->acitve tree剔除entity,下边再把entity加入st->active树
  10. bfq_active_extract(st, entity);
  11. }else{
  12. bfq_active_extract(st, entity);
  13. }
  14. //按照entity->budget计算entity的虚拟运行时间并累加到entity->finish,最后把entity按照新的entity->finish插入st->active红黑树
  15. bfq_update_fin_time_enqueue(entity, st, false);
  16. }
  17. //bfqq的entity是新创建的或者bfqq的entity之前在st->active红黑树,根据entity->budget计算entity的虚拟运行时间并累加到entity->finish,最后把entity按照新的entity->finish插入st->active红黑树
  18. static void __bfq_activate_entity(struct bfq_entity *entity,
  19. bool non_blocking_wait_rq)
  20. {
  21. if (non_blocking_wait_rq && bfq_gt(st->vtime, entity->finish)) {
  22. backshifted = true;
  23. min_vstart = entity->finish;
  24. } else
  25. min_vstart = st->vtime;
  26. //entity现在处于st->active红黑树
  27. if (entity->tree == &st->idle) {
  28. //entity从st->active红黑树剔除掉,可能更新st->first_idle或st->last_idle
  29. bfq_idle_extract(st, entity);
  30. entity->start = bfq_gt(min_vstart, entity->finish) ?min_vstart : entity->finish;
  31. } else {//到这里说明entity之前不在st->idle或st->active红黑树,entity是新分配的也走这里
  32. entity->start = min_vstart;//更新entity->start
  33. //st->wsum调度类st的active和idle tree上所有entity的权重之和,累加每一个entity的entity->weight
  34. st->wsum += entity->weight;
  35. }
  36. //按照entity->budget计算entity的虚拟运行时间并累加到entity->finish,最后把entity按照新的entity->finish插入st->active红黑树
  37. bfq_update_fin_time_enqueue(entity, st, backshifted);
  38. }

最后都是调用bfq_update_fin_time_enqueue()把entity插入st->active红黑树,看下它的源码:

  1. //按照entity->budget计算entity的虚拟运行时间并累加到entity->finish,最后把entity按照新的entity->finish插入st->active红黑树
  2. static void bfq_update_fin_time_enqueue(struct bfq_entity *entity,
  3. struct bfq_service_tree *st,
  4. bool backshifted)
  5. {
  6. //这里简化成entity->finish=entity->start+entity->budget/entity->weight,用entity的配额得到entity->finish,注意是entity的配额
  7. bfq_calc_finish(entity, entity->budget);
  8. //把entity按照新的entity->finish插入st->active红黑树
  9. bfq_active_insert(st, entity);
  10. }
  11. //把entity按照新的entity->finish插入st->active红黑树
  12. static void bfq_active_insert(struct bfq_service_tree *st,
  13. struct bfq_entity *entity)
  14. {
  15. //把entity按照entity->finish插入st->active红黑树,entity->finish越小entity在红黑树越靠左
  16. bfq_insert(&st->active, entity);
  17. if (node->rb_left)
  18. node = node->rb_left;
  19. else if (node->rb_right)
  20. node = node->rb_right;
  21. /*从entity的node所在节点,一直在红黑树向上得到它的parent节点,用entity->start更新这些红黑树节点的entity->min_start,同时也更新这些node在红黑树的左右子节点对应的entity的min_start。这个更新操作一直到根节点才结束*/
  22. bfq_update_active_tree(node);
  23. }

1.4 bfq_set_in_service_queue()和bfq_get_next_queue()

  1. static struct bfq_queue *bfq_set_in_service_queue(struct bfq_data *bfqd)
  2. {
  3. //从sd->next_in_service取出entity赋于sd->in_service_entity,作为本次要使用的entity。然后查找一个新的entity赋于sd->next_in_service,作为下一次使用的entity
  4. struct bfq_queue *bfqq = bfq_get_next_queue(bfqd);
  5. //本质是 bfqd->in_service_queue = bfqq
  6. __bfq_set_in_service_queue(bfqd, bfqq);
  7. return bfqq;
  8. }

bfq_set_in_service_queue()主要调用bfq_get_next_queue()函数:把sd->next_in_service赋值给sd->in_service_entity,sd->in_service_entity正是bfq调度器使用的entity,并且返回entity对应的bfqq。__bfq_set_in_service_queue()函数主要是把bfqq赋值给bfqd->in_service_queue,bfqd->in_service_queue正是bfq正在使用的bfqq。看下bfq_get_next_queue()函数源码:

  1. //从sd->next_in_service取出entity赋于sd->in_service_entity,作为本次要使用的entity。然后查找一个新的entity赋于sd->next_in_service,作为下一次使用的entity
  2. struct bfq_queue *bfq_get_next_queue(struct bfq_data *bfqd)
  3. {
  4. for (; sd ; sd = entity->my_sched_data) {
  5. ///取出sd->next_in_service作为当前要使用的entity,赋值给sd->in_service_entity
  6. entity = sd->next_in_service;
  7. sd->in_service_entity = entity;
  8. //sd->in_service_entity这个entity作为bfq当前正在使用的entity,就要把entity从st->actvie tree剔除
  9. if (bfq_no_longer_next_in_service(entity))
  10. bfq_active_extract(bfq_entity_service_tree(entity),entity);
  11. }
  12. for_each_entity(entity) {
  13. struct bfq_sched_data *sd = entity->sched_data;
  14. ///查找新的 next_in_service entity赋于sd->next_in_service
  15. if (!bfq_update_next_in_service(sd, NULL, false))
  16. break;
  17. }
  18. }

按照前文所说,该函数是从sd->next_in_service取出entity赋于sd->in_service_entity,作为本次要使用的entity。然后执行bfq_update_next_in_service()查找一个新的entity赋于sd->next_in_service,作为下一次使用的entity。bfq_update_next_in_service()前文已经提到了多次,下一小节讲解。

1.5 bfq_update_next_in_service()选择一个新的sd->next_in_service

看下bfq_update_next_in_service()函数重点代码,主要是调用bfq_lookup_next_entity()。

  1. static bool bfq_update_next_in_service(struct bfq_sched_data *sd,
  2. struct bfq_entity *new_entity,//new_entity有时是NULL,有时不是
  3. bool expiration)//expiration为true说明是bfqq到期了,为false说明是其他情况,比如在bfq_update_next_in_service()
  4. {
  5. struct bfq_entity *next_in_service = sd->next_in_service;
  6. bool parent_sched_may_change = false;
  7. bool change_without_lookup = false;
  8. ………………
  9. //如果change_without_lookup为false则执行bfq_lookup_next_entity()选择下一个使用的bfqq的entity。可能找不到,则返回NULL
  10. if (!change_without_lookup)
  11. next_in_service = bfq_lookup_next_entity(sd, expiration);
  12. ........................
  13. //为sd->next_in_servic赋值下一次使用的bfqq的entity。如果前边没找到则sd->next_in_service被赋值NULL
  14. sd->next_in_service = next_in_service;
  15. return parent_sched_may_change;
  16. }
  17. static struct bfq_entity *bfq_lookup_next_entity(struct bfq_sched_data *sd,
  18. bool expiration)
  19. {
  20. //在sd调度器每个调度策略st里都搜索符合条件的 entity,都找不到返回NULL
  21. for (; class_idx < BFQ_IOPRIO_CLASSES; class_idx++) {
  22. entity = __bfq_lookup_next_entity(st + class_idx,sd->in_service_entity &&!expiration);
  23. if (entity)
  24. break;
  25. }
  26. return entity;
  27. }
  28. static struct bfq_entity *__bfq_lookup_next_entity(struct bfq_service_tree *st, bool in_service)
  29. {
  30. //从st->active红黑树红黑树最左边查找entity->start最小并且entity->start小于vtime的entity
  31. entity = bfq_first_active_entity(st, new_vtime);
  32. return entity;
  33. }

bfq_first_active_entity()函数主要是在st->active红黑树最左边查找一个entity->start最小并且entity->start小于st->vtime的entity,这就是要最终找到的合适的entity,接着就会赋值给sd->next_in_service。bfq_update_next_in_service()的调用主要有3处:

1:bfq_activate_bfqq() / bfq_requeue_bfqq() ->bfq_activate_requeue_entity()把entity加入st->active红黑树后,执行bfq_update_next_in_service()选择一个新的entity赋于sd->next_in_service,作为下次使用的entity。

2:bfq_del_bfqq_busy ()->bfq_deactivate_bfqq ()->bfq_deactivate_entity()从st->active红黑树剔除entity后,执行bfq_update_next_in_service()选择一个新的entity赋于sd->next_in_service,作为下次使用的entity。

:

3:派发IO请求过程执行到__bfq_dispatch_request ()->bfq_select_queue->bfq_set_in_service_queue()->bfq_get_next_queue()->bfq_update_next_in_service()函数,把sd->next_in_service赋于sd->in_service_entity后,执行bfq_update_next_in_service()选择一个新的entity赋于sd->next_in_service,作为下次使用的entity。

1.6 bfq_dispatch_rq_from_bfqq()派发IO请求的后半部分

最后看下bfq_dispatch_rq_from_bfqq()函数。

  1. static struct request *bfq_dispatch_rq_from_bfqq(struct bfq_data *bfqd,
  2. struct bfq_queue *bfqq)
  3. {
  4. struct request *rq = bfqq->next_rq;
  5. unsigned long service_to_charge;
  6. //计算并返回本次传输req需要配额,配额以req要传输字节数为基准,同步和异步IO计算方法不一样。
  7. service_to_charge = bfq_serv_to_charge(rq, bfqq);
  8. //根据本次传输req的配额served,增加entity配额entity->service和调度器虚拟运行时间st->vtime
  9. bfq_bfqq_served(bfqq, service_to_charge);
  10. //计算新的bfqd->peak_rate,并选择一个新的req作为bfqq->next_rq,计算更新新的entity->budget,把req从bfqq->sort_list链表剔除
  11. bfq_dispatch_remove(bfqd->queue, rq);
  12. if (!(bfq_tot_busy_queues(bfqd) > 1 && bfq_class_idle(bfqq)))
  13. goto return_rq;
  14. return_rq:
  15. return rq;//bfqq->next_rq作为本次派发的IO请求
  16. }

bfq_dispatch_rq_from_bfqq()函数作用:从bfqq->next_rq选择为要派发IO请求,根据派发IO请求的数据量计算消耗的配额,累加到bfqq的entity->service和bfq调度器消耗的虚拟运行时间st->vtime。最后看下bfq_bfqq_served()函数:

  1. //根据待派发req的配额served,增加entity配额entity->service和调度器虚拟运行时间st->vtime
  2. void bfq_bfqq_served(struct bfq_queue *bfqq, int served)//served是当前要派发req消耗的配额
  3. {
  4. …………
  5. bfqq->service_from_backlogged += served;
  6. for_each_entity(entity) {
  7. //取出entity指向的调度器实体st
  8. st = bfq_entity_service_tree(entity);
  9. //entity->service累加待派发req的配额served
  10. entity->service += served;
  11. //st->vtime累加待派发req的配额served除以st->wsum算出的虚拟运行时间
  12. st->vtime += bfq_delta(served, st->wsum);
  13. /*处理st->active红黑树的first_idle和last_idle,可能会把first_idle指向的entity从st->active红黑树剔除掉,释放掉它的bfqq和entity*/
  14. bfq_forget_idle(st);
  15. }
  16. }

2:sd->next_in_service、sd->in_service_entity、bfqd->in_service_queue的使用与更新

第一节多次提到sd->next_in_service、sd->in_service_entity、bfqd->in_service_queue这3个变量,这一节主要介绍这3个变量的工作过程。sd->in_service_entity是bfq调度器当前正在使用的entity,bfqd->in_service_queue是bfq调度器正在使用的entity的bfqq,sd->next_in_service是bfq调度器下一次使用的entity。

派发IO请求执行到__bfq_dispatch_request->bfq_select_queue()函数,首先判断bfqd->in_service_queue是否为NULL,即 bfq是否有正在使用的bfqq。如果bfq没有正在使用的bfqq(bfqd->in_service_queue是NULL),则要执行bfq_set_in_service_queue()把sd->next_in_service这个下一次使用的entity赋值给sd->in_service_entity。sd->in_service_entity正是bfq调度器正在使用的entity,bfqd->in_service_queue是该entity对应的bfqq。如果bfq_select_queue()函数中,bfqd->in_service_queue非NULL,即bfq有正在使用的bfqq,则从bfqq的IO队列取出要派发的IO请求(即bfqq->next_rq指向的IO请求)。

最后说下sd->next_in_service,sd->next_in_service的更新是在bfq_update_next_in_service()函数,主要有三处。bfq_deactivate_entity()函数中把entity从st->active红黑树剔除后,执行bfq_update_next_in_service();bfq_activate_requeue_entity()函数中把entity加入st->active红黑树后,执行bfq_update_next_in_service();派发IO请求__bfq_dispatch_request()->bfq_select_queue()函数中,bfqd->in_service_queue是NULL即bfq没有正在使用的bfqq,则执行bfq_set_in_service_queue()->bfq_get_next_queue(),把sd->next_in_service赋值给sd->in_service_entity,然后执行bfq_update_next_in_service()选择一个新的entity赋值给sd->next_in_service。

bfq_update_next_in_service()第1节介绍过,函数主要是从st->active红黑树最左边查找一个entity->start最小并且entity->start小于vtime的entity,这就是要最终找到的合适的entity,后边就会赋值给sd->next_in_service。

3:entity->start、entity->finish、entity->min_start的更新

3.1 entity->start的更新时机

entity->start是entity的起始虚拟运行时间,entity->finish是entity结束的虚拟运行时间。entity->start在什么时间更新呢?

1:在一个新的进程派发IO请求,分配创建新的bfqq后把bfqq的entity加入st->active红黑树函数流程:bfq_add_request()->bfq_bfqq_handle_idle_busy_switch()->bfq_add_bfqq_busy()->bfq_activate_bfqq()->bfq_activate_requeue_entity()->__bfq_activate_requeue_entity()->__bfq_activate_entity(),在__bfq_activate_entity()函数中,走了if (entity->tree == &st->idle)的else分支,用调度器的虚拟运行时间st->vtime赋值给entity->start。

2:在派发IO请求时,因为bfqq的配额消耗光而令bfqq过期失效过程__bfq_dispatch_request()->bfq_select_queue()->bfq_bfqq_expire()->__bfq_bfqq_expire()->bfq_requeue_bfqq()->bfq_activate_requeue_entity()->__bfq_activate_requeue_entity-> __bfq_requeue_entity()。在该函数中,执行bfq_calc_finish(entity, entity->service)用entity已经消耗的配额更新entity->finish,即entity->finish = entity->start + bfq_delta(service, entity-> service),然后entity->start = entity->finish用entity->finish更新entity->start。

3:entity之前因为没有派发的IO请求过期失效而从st->active红黑树移动到st->active红黑树,然后又要派发派entity的bfqq上的IO请求,而把entity激活,函数流程是bfq_add_request()->bfq_bfqq_handle_idle_busy_switch()->bfq_add_bfqq_busy()->bfq_activate_bfqq()->bfq_activate_requeue_entity()->__bfq_activate_requeue_entity()->__bfq_activate_entity(),在__bfq_activate_entity()函数中,走了if (entity->tree == &st->idle)分支,用entity->finish和st->vtime更新entity->start。

3.2 entity->finish的更新时机

首先,entity加入st->active红黑树或者st->active红黑树时,都是按照entity->finish由小到大从左到右而排列。就是说,entity->finish越小,entity在st->active红黑树或者st->active红黑树越靠左。entity->finish的更新是在bfq_calc_finish()函数,如下:

  1. static void bfq_calc_finish(struct bfq_entity *entity, unsigned long service)
  2. //service是entity->budget或entity->service
  3. struct bfq_queue *bfqq = bfq_entity_to_bfqq(entity);
  4. entity->finish = entity->start + bfq_delta(service, entity->weight);
  5. }

函数形参service是entity已经消耗的配额或者entity的权重,这个计算可以简化成entity->finish = entity->start + service*N/entity->weight(N是个常数)。显然,entity->finish可以等同于entity->start+entity消耗的配额。

什么时机会调用到bfq_calc_finish()函数呢?

1:在派发IO请求时,因为bfqq的配额消耗光而令bfqq过期失效过程__bfq_dispatch_request()->bfq_select_queue()->bfq_bfqq_expire()->__bfq_bfqq_expire()->bfq_requeue_bfqq()->bfq_activate_requeue_entity()->__bfq_activate_requeue_entity-> __bfq_requeue_entity()。在该函数中,执行bfq_calc_finish(entity, entity->service)。用entity已经消耗的配额,乘以N再除以entity权重weight得到entity已经消耗的虚拟运行时间(N是个常数),再加上entity->start得到entity->finish。

2:在派发IO请求时,因为bfqq上没有要派发的IO请求而令bfqq过期失效过程__bfq_dispatch_request()->bfq_select_queue()->bfq_bfqq_expire()->__bfq_bfqq_expire()->bfq_del_bfqq_busy()->bfq_deactivate_bfqq()->bfq_deactivate_entity()->__bfq_deactivate_entity()。在__bfq_deactivate_entity()函数中,执行bfq_calc_finish(entity, entity->service),用 entity已经消耗的配额,乘以N再除以entity权重weight得到entity已经消耗的虚拟运行时间(N是个常数),再加上entity->start得到entity->finish。

3:bfq_update_fin_time_enqueue()函数中,执行bfq_calc_finish(entity, entity->budget)。用entity的权重,乘以N再除以entity权重weight得到entity已经消耗的虚拟运行时间(N是个常数),再加上entity->start得到entity->finish。然后执行bfq_active_insert(st, entity)把entity按照最新的entity->finish加入st->active红黑树。

需要说明一点,__bfq_requeue_entity()和__bfq_requeue_entity()都是执行bfq_calc_finish(entity, entity->service),用 entity已经消耗的配额,乘以N再除以entity权重weight得到entity已经消耗的虚拟运行时间(N是个常数),再加上entity->start得到entity->finish。而bfq_update_fin_time_enqueue()函数中,是用entity的权重,乘以N再除以entity权重weight得到entity已经消耗的虚拟运行时间(N是个常数),再加上entity->start得到entity->finish。这是个很明显的区别。

还要说明一点,bfq_update_fin_time_enqueue()函数的调用有两个时机,一个是执行__bfq_dispatch_request()->bfq_select_queue()->bfq_bfqq_expire()->__bfq_bfqq_expire()->bfq_requeue_bfqq()->bfq_activate_requeue_entity()->__bfq_activate_requeue_entity-> __bfq_requeue_entity()->bfq_update_fin_time_enqueue(),把entity重新插入st->active红黑树最后,执行bfq_update_fin_time_enqueue()。

另一个时机是把一个新创建的bfqq的entity加入st->active红黑树或者把一个处于st->active红黑树的entity激活加入到st->active红黑树,函数流程是bfq_add_request()->bfq_bfqq_handle_idle_busy_switch()->bfq_add_bfqq_busy()->bfq_activate_bfqq()->bfq_activate_requeue_entity()->__bfq_activate_requeue_entity()->__bfq_activate_entity()->bfq_update_fin_time_enqueue(),__bfq_activate_entity()函数最后执行bfq_update_fin_time_enqueue()把entity插入st->active红黑树。

3.3 entity->min_start的更新时机

entity->min_start的更新在bfq_update_active_tree()函数,有两个时机:在entity从st->active 剔除最后执行的bfq_active_extract()函数和entity加入st->active红黑树最后执行的bfq_active_insert()函数。看下源码

  1. //把entity按照新的entity->finish插入st->active红黑树
  2. static void bfq_active_insert(struct bfq_service_tree *st,
  3. struct bfq_entity *entity)
  4. {
  5.     ………………..
  6. //把entity按照entity->finish插入st->active红黑树,entity->finish越小entity在红黑树越靠左
  7. bfq_insert(&st->active, entity);
  8. /*从entity的node所在节点,一直在红黑树向上得到它的parent节点,用entity->start更新这些红黑树节点的entity->min_start,同时也更新这些node在红黑树的左右子节点对应的entity的min_start。这个更新操作一直到根节点才结束*/
  9. bfq_update_active_tree(node);
  10. }
  1. //从st->acitve tree剔除entity
  2. static void bfq_active_extract(struct bfq_service_tree *st,
  3. struct bfq_entity *entity)
  4. {
  5. //返回entity所属红黑树下边的entiry对应节点
  6. node = bfq_find_deepest(&entity->rb_node);
  7. //把entiry从st active tree剔除
  8. bfq_extract(&st->active, entity);
  9. /*从entity的node所在节点,一直在红黑树向上得到它的parent节点,更新这些红黑树节点对应的bfq_entity的成员min_start,同时也更新这些node在红黑树的左右子节点对应的entity的min_start。这个更新操作一直到根节点才结束*/
  10. if (node)
  11. bfq_update_active_tree(node);
  12. }

bfq_update_active_tree()函数源码如下:

  1. /*从entity的node所在节点,一直在红黑树向上得到它的parent节点,更新这些红黑树节点对应的bfq_entity的成员min_start,同时也更新这些node在红黑树的左右子节点对应的entity的min_start。这个更新操作一直到根节点才结束*/
  2. static void bfq_update_active_tree(struct rb_node *node)
  3. {
  4. struct rb_node *parent;
  5. up:
  6. //先更新当前node对应的entity的min_start
  7. bfq_update_active_node(node);
  8. parent = rb_parent(node);
  9. if (!parent)//更新entity的min_start,直到entity红黑树根节点才return返回
  10. return;
  11. //如果node这个entity是父节点parent的左节点,则更新parent节点的右节点对应的entity的min_start
  12. if (node == parent->rb_left && parent->rb_right)
  13. bfq_update_active_node(parent->rb_right);
  14. else if (parent->rb_left)//否则说明node这个entity是父节点parent的右节点,则更新parent节点的左节点对应的entity的min_start
  15. bfq_update_active_node(parent->rb_left);//更新entity->min_start
  16. node = parent;
  17. goto up;
  18. }
  19. //先把entity->start赋值给entity->min_start,如果entity在红黑树下边的左右子节点的entity的min_start更小,再重新赋值给entity->min_start
  20. static void bfq_update_active_node(struct rb_node *node)
  21. {
  22. struct bfq_entity *entity = rb_entry(node, struct bfq_entity, rb_node);
  23. //用entity->start更新entity->min_start
  24. entity->min_start = entity->start;
  25. //如果当前entity在红黑树下边的左子节点的entity的min_start更小,则赋值给当前entity的min_start
  26. bfq_update_min(entity, node->rb_right);
  27. //如果当前entity在红黑树下边的右子节点的entity的min_start更小,则赋值给当前entity的min_start
  28. bfq_update_min(entity, node->rb_left);
  29. }

从entity的node所在节点,一直在红黑树向上得到它的parent节点,更新这些红黑树节点对应的bfq_entity的成员min_start,同时也更新这些node在红黑树的左右子节点对应的entity的min_start。这个更新操作一直到根节点才结束。

4:entity->service、st->vtime的更新和使用

entity->service是entity已经消耗的配额,st->vtime是bfq调度器的虚拟运行时间。在派发IO请求时,__bfq_dispatch_request()->bfq_dispatch_rq_from_bfqq(),先执行bfq_serv_to_charge()根据本次派发的IO请求bfqq->next_rq需要传输的字节数,计算需要消耗的配额served。然后执行到bfq_bfqq_served()函数,执行里边的entity->service += served把served累加到entity->service,同时执行st->vtime += bfq_delta(served, st->wsum),等同于st->vtime += served*N/ st->wsum(N是个常数),其实就是根据本次派发的IO请求消耗的配额累加到st->vtime。

显然,每派发一个IO请求,都要根据派发的IO请求消耗的配额,累加到entity->service,累计到st->vtime,entity->service和st->vtime随着派发IO请求个数增加而累加。

每派发调度器一个entity的一个IO请求,都要计算派发的IO请求的消耗的配额 (使用served*N/ st->wsum计算,N是一个常数),然后累计到调度器的虚拟运行时间st->vtime。简单说st->vtime= (bfqq1派发的IO请求消耗的配额之和+ bfqq2的派发的IO请求消耗的配额之和+……….+bfqqN的派发的IO请求消耗的配额之和) /(bfqq1的entity的权重+ bfqq1的entity的权重+………+bfqqN的entity的权重)。

5:为什么bfq有利交互式进程IO快速响应

首先,一个新的进程派发IO请求前,会分配一个新的bfqq,然后把这个新创建的bfqq的entity加入st->active红黑树,函数流程是bfq_add_request()->bfq_bfqq_handle_idle_busy_switch()->bfq_add_bfqq_busy()->bfq_activate_bfqq()->bfq_activate_requeue_entity()->__bfq_activate_requeue_entity()->__bfq_activate_entity(),把__bfq_activate_entity()源码简化下:

  1. static void __bfq_activate_entity(struct bfq_entity *entity,
  2. bool non_blocking_wait_rq)
  3. {
  4. if (non_blocking_wait_rq && bfq_gt(st->vtime, entity->finish)) {
  5. backshifted = true;
  6. min_vstart = entity->finish;
  7. } else
  8. min_vstart = st->vtime;
  9.     ……………………..
  10. entity->start = min_vstart;//更新entity->start
  11. //st->wsum调度类st的active和idle tree上所有entity的权重之和,累加每一个entity的entity->weight
  12. st->wsum += entity->weight;
  13. //按照entity->budget计算entity的虚拟运行时间并累加到entity->finish,最后把entity按照新的entity->finish插入st->active红黑树
  14. bfq_update_fin_time_enqueue(entity, st, backshifted);
  15. }

重点是用st->vtime 更新entity->start,然后执行bfq_update_fin_time_enqueue(),按照entity的初始权重entity->budget计算entity的虚拟运行时间并累加到entity->finish,最后把entity按照新的entity->finish插入st->active红黑树。这样可以保证entity的entity->start和entity->finish尽可能小,entity尽可能靠左插入到st->active红黑树。

然后在bfq_update_next_in_service()->bfq_lookup_next_entity()->__bfq_lookup_next_entity->bfq_first_active_entity()函数流程,在st->active红黑树选择一个最符合条件的entity即sd->next_in_service时。哪个entity最符合条件呢?是st->active红黑树最左边并且entity->start最小并且entity->start小于st->vtime的entity。一个entity的entity->finish越小在st->active红黑树 越靠左,entity->start越小越容易符合小于st->vtime的条件。因此新派发IO请求的进程创建的bfqq的entity插入st->active红黑树后,更容易在bfq_update_next_in_service()函数中选中被赋于sd->next_in_service。

bfq_update_next_in_service()的执行时机主要有3个,第1节有解过。

然后,等__bfq_dispatch_request()->bfq_select_queue()派发IO请求时,bfqd->in_service_queue这个正在的使用的bfqq过期失效,执行bfq_bfqq_expire()令bfqd->in_service_queue这个bfqq失效,然后执行bfq_set_in_service_queue()->bfq_get_next_queue(),把sd->next_in_service赋于sd->in_service_entity,sd->in_service_entity是bfq调度器当前使用的entity。然后返回sd->in_service_entity这个entity对应的bfqq。接着回到__bfq_set_in_service_queue()函数,执行bfqd->in_service_queue = bfqq,bfqd->in_service_queue是bfq当前使用的bfqq。好了,刚才那个新进程派发IO请求,创建的bfqq终于被bfq调度器使用到了,接着就可以派发这个进程的IO请求了。

6:st->first_idle 和 st->last_idle的更新

先把前文使用的贴图发下(为了演示简单,没有演示entity在红黑树中的树形排列形式,只是演示entity按照entity->finish由小到大从左向右排列)

一个新bfqq的entity是先加入的st->active 红黑树,后续如果该bfqq没有要派发的IO请求了,则把该bfqq的entity移动到st->idle红黑树。st->idle红黑树上entity是按照entity->finish由小到大从左向右排列。st->first_idle指向st->idle红黑树entity->finish最小的entity, st-> last_idle指向st->idle红黑树entity->finish最大的entity。下边把用到st->first_idle和st-> last_idle的函数源码列下:

把entity从st->active红黑树剔除的函数流程bfq_deactivate_entity()->__bfq_deactivate_entity()->bfq_idle_extract()

  1. //entity从st->active红黑树剔除掉,可能更新st->first_idle或st->last_idle
  2. static void bfq_idle_extract(struct bfq_service_tree *st,
  3. struct bfq_entity *entity)
  4. {
  5. struct bfq_queue *bfqq = bfq_entity_to_bfqq(entity);
  6. struct rb_node *next;
  7. /*entity是st->first_idle指向的entity,即st->idle红黑树最左边的entity。因为entiry要从st->active红黑树剔除,则选择entity右边的entity赋于st->first_idle*/
  8. if (entity == st->first_idle) {
  9. next = rb_next(&entity->rb_node);
  10. st->first_idle = bfq_entity_of(next);
  11. }
  12. /*entity是st->last_idle指向的entity,即st->idle红黑树最右边的entity。因为entiry要从st->active红黑树剔除,则选择entity左边的entity赋于st->last_idle*/
  13. if (entity == st->last_idle) {
  14. next = rb_prev(&entity->rb_node);
  15. st->last_idle = bfq_entity_of(next);
  16. }
  17. //entity从st->active红黑树剔除掉
  18. bfq_extract(&st->idle, entity);
  19. if (bfqq)
  20. list_del(&bfqq->bfqq_list);
  21. }

把entity从st->active红黑树剔除,然后加入st->idle红黑树的函数流程bfq_deactivate_entity()->__bfq_deactivate_entity()->bfq_idle_insert()。

  1. //把entity插入st->idle树,并可能更新st->first_idle或st->last_idle
  2. static void bfq_idle_insert(struct bfq_service_tree *st,
  3. struct bfq_entity *entity)
  4. {
  5. struct bfq_queue *bfqq = bfq_entity_to_bfqq(entity);
  6. struct bfq_entity *first_idle = st->first_idle;
  7. struct bfq_entity *last_idle = st->last_idle;
  8. //st->idle红黑树的最左边的entiry的finish最小,最右边的entity的finish最大。
  9. //st->first_idle记录entity->finish最小的entity,就是st->idle红黑树最靠左的entity
  10. if (!first_idle || bfq_gt(first_idle->finish, entity->finish))
  11. st->first_idle = entity;
  12. //st->last_idle记录entity->finish最大的entity,就是st->idle红黑树最靠右的entity
  13. if (!last_idle || bfq_gt(entity->finish, last_idle->finish))
  14. st->last_idle = entity;
  15. //把entity按照entity->finish插入st->idle红黑树,entity->finish越小entity在红黑树越靠左
  16. bfq_insert(&st->idle, entity);
  17. if (bfqq)//加入st->idle 的红黑树都加入到bfqd->idle_list链表
  18. list_add(&bfqq->bfqq_list, &bfqq->bfqd->idle_list);
  19. }

派发IO请求时执行__bfq_dispatch_request ()->bfq_dispatch_rq_from_bfqq ()->bfq_bfqq_served()->bfq_forget_idle(),释放掉st->first_idle指向的entity。

  1. //处理st->active红黑树的first_idle和last_idle,可能会把first_idle指向的entity从st->active红黑树剔除掉,释放掉它的bfqq和entity
  2. static void bfq_forget_idle(struct bfq_service_tree *st)
  3. {
  4. struct bfq_entity *first_idle = st->first_idle;
  5. struct bfq_entity *last_idle = st->last_idle;
  6. //st->active红黑树空并且last_idle->finish小于st->vtime则if成立,
  7. if (RB_EMPTY_ROOT(&st->active) && last_idle &&
  8. !bfq_gt(last_idle->finish, st->vtime)) {
  9. st->vtime = last_idle->finish;
  10. }
  11. //first_idle非空,并且st->vtime大于first_idle->finish则if成立
  12. if (first_idle && !bfq_gt(first_idle->finish, st->vtime))
  13. //first_idle指向的entity从st->active红黑树剔除掉,如果不再被用到,释放掉它的bfqq和entity
  14. bfq_put_idle_entity(st, first_idle);
  15. }

bfq_forget_idle()函数需要重点说下,当st->first_idle->finish小于st->vtime,该函数会执行bfq_put_idle_entity()释放掉st->first_idle指向的entity。st->vtime随着派发一个又一个IO请求而不断增加,st->vtime大于st->first_idle->finish是迟早的事。讲到这里,感觉bfq还是有很多知识点没有讲通,后续再慢慢更新吧……..水平有限,如有错误请指正。

最后说下测试经验:为了能复现多个进程的bfqq同时存在st->active红黑树,然后在st->active红黑树中查找st->start最小的entity,fio测试时的参数iodepth要很大,numjobs也要尽可能多,如下:

fio -filename=/mnt/ext4/fio_test -direct=1 -iodepth 80 -thread -rw=randrw -rwmixread=50 -ioengine=libaio -bs=64k -size=10G -numjobs=6 -runtime=180 -group_reporting -name=test。同时,也要有脏页回写进程在频繁刷IO。

内核block层IO调度器—bfq算法之3源码要点总结相关推荐

  1. Linux IO调度器相关算法介绍

    IO调度器(IO Scheduler)是操作系统用来决定块设备上IO操作提交顺序的方法.存在的目的有两个,一是提高IO吞吐量,二是降低IO响应时间.然而IO吞吐量和IO响应时间往往是矛盾的,为了尽量平 ...

  2. Go调度器系列(4)源码阅读与探索

    各位朋友,这次想跟大家分享一下Go调度器源码阅读相关的知识和经验,网络上已经有很多剖析源码的好文章,所以这篇文章不是又一篇源码剖析文章,注重的不是源码分析分享,而是带给大家一些学习经验,希望大家能更好 ...

  3. 一文深入了解Linux IO 调度器

    [推荐阅读] 浅谈linux 内核网络 sk_buff 之克隆与复制 深入linux内核架构--进程&线程 了解Docker 依赖的linux内核技术 每个块设备或者块设备的分区,都对应有自身 ...

  4. Linux的IO调度器-CFQ

    最近由于一些控制IO带宽的需求,开始研究CFQ以及对应的IO cgroup,今天baidu了一下,竟然发现没有多少中文的介绍,所以准备写一个系列,介绍一下这个调度器,粗粗想了一下,大概可以灌四篇水,包 ...

  5. 木其工作室(专业程序代写服务)[转]学写块设备驱动(三)----踢开IO调度器,自己处理bio(下)...

    优质代写程序 代写Assignment程序 定制软件 设计程序 专业代写IT 大学生程序代写 踢开IO调度器很容易,即不使用__make_request 这个系统指定的强力函数,如何不使用?其实我们从 ...

  6. 挑战360无死角讲解Linux内核 进程管理,调度器的5种实现丨C++后端开发丨C/C++Linux服务器开发丨内核开发丨网络编程

    挑战360无死角讲解 进程管理,调度器的5种实现 1. 8500行 CFS是什么 2. RT调度器使用场景 3. IDLE/Dealine调度器 视频讲解如下,点击观看: 挑战360无死角讲解Linu ...

  7. 【微电网优化】基于matlab粒子群算法求解微网经济调度和环境友好调度优化问题【含Matlab源码 2283期】

    ⛄一.获取代码方式 获取代码方式1: 完整代码已上传我的资源:[微电网优化]基于matlab粒子群算法求解微网经济调度和环境友好调度优化问题[含Matlab源码 2283期] 点击上面蓝色字体,直接付 ...

  8. JAVA毕业设计Vue.js音乐播放器设计与实现计算机源码+lw文档+系统+调试部署+数据库

    JAVA毕业设计Vue.js音乐播放器设计与实现计算机源码+lw文档+系统+调试部署+数据库 JAVA毕业设计Vue.js音乐播放器设计与实现计算机源码+lw文档+系统+调试部署+数据库 本源码技术栈 ...

  9. C++bidirectional dijkstra双向最短路径算法(附完整源码)

    C++bidirectional dijkstra双向最短路径算法 C++bidirectional dijkstra双向最短路径算法的完整源码(定义,实现,main函数测试) C++bidirect ...

  10. C++ hamming distance汉明距离算法(附完整源码)

    hamming distance汉明距离的算法 hamming distance汉明距离的算法的完整源码(定义,实现,main函数测试) hamming distance汉明距离的算法的完整源码(定义 ...

最新文章

  1. LeetCode-字符串-反转字符串中的单词 II
  2. NLTK完成简单的情感分析
  3. 这样去写你的 HTML
  4. windows server 2012 R2 standard 评估版过期重启
  5. cocos2d-x JS 重力感应监听事件
  6. java 的单态模式(只可以创建一个对象)
  7. 倒计时 1 天 | 年前不学习,年后无加薪!区块链开发者们不要纠结了!(内含赠票福利)...
  8. mysql为用户部分授权,MYSQL为用户授权
  9. mysql 性能调优 参数随写
  10. 如何进入交换机配置命令窗口
  11. 电信光猫の破解使用路由
  12. H3C交换机配置命令大全
  13. 什么叫计算机硬件特征码,获取计算机硬件特征码【上】
  14. china-pub第3波免费赠书正式推出,精品赠书20册,抢楼得书!
  15. c语言投票系统程序,C语言智能投票系统.doc
  16. jeecg-boot 官方文档v2.0快速开始之hello world 前端教程
  17. 《原力计划-打卡挑战》总榜名单揭晓!!
  18. 《磨菇书三四章整理》
  19. 使用DiskGenius(原DiskMan)修复损坏的硬盘分区
  20. 智慧消防 物联网网关用于防火监测系统

热门文章

  1. 新一代服务器性能测试工具Gatling
  2. [管理]案例剖析企业ERP失利原因
  3. 自带电池的Linux开发板,支持动态调频,AXP228电源管理,预留锂电池接口,内置充放电电路及电量计iTOP-4418开发板...
  4. OWL教程6 OWL五大组成部分之五响应式系统
  5. 归国谣 双脸 温庭筠
  6. 自制Edge,Chrome标签页插件
  7. 中国聚光光伏市场投资规划建议与未来发展方向预测报告2022年版
  8. 美国计算机一流学校有哪些,美国数据分析专业排名TOP6的一流大学 哪所院校最能打动你的心呢?...
  9. (CVE-2018-2628)Weblogic WLS Core Components 反序列化命令执行漏洞
  10. html静态页面作业~旅游开发景点网站设计——紫色的旅游开发景点网站(4页) HTML+CSS+JavaScript 主题酒店景点