每一个调度器类sched_class都必须提供一个pick_next_task函数用以在就绪队列中选择一个最优的进程来等待调度, 而咱们的CFS调度器类中, 选择下一个将要运行的进程由pick_next_task_fair函数来完成linux
以前咱们在将主调度器的时候, 主调度器schedule函数在进程调度抢占时, 会经过__schedule函数调用全局pick_next_task选择一个最优的进程, 在pick_next_task
中咱们就按照优先级依次调用不一样调度器类提供的pick_next_task
方法c#
今天就让咱们窥探一下彻底公平调度器类CFS的pick_next_task方法pick_next_fair缓存
pick_next_task_fairapp
选择下一个将要运行的进程pick_next_task_fair
执行. 其代码执行流程以下负载均衡
对于pick_next_task_fair函数的讲解, 咱们从simple标签开始, 这个是常规状态下pick_next的思路, 简单的来讲pick_next_task_fair的函数框架以下框架
again: 控制循环来读取最优进程 #ifdef CONFIG_FAIR_GROUP_SCHED 完成组调度下的pick_next选择 返回被选择的调度时实体的指针 #endif simple: 最基础的pick_next函数 返回被选择的调度时实体的指针 idle : 若是系统中没有可运行的进行, 则须要调度idle进程
可见咱们会发现,less
在不支持组调度状况下(选项CONFIG_FAIR_GROUP_SCHED), CFS的pick_next_task_fair函数会直接执行simple标签, 优选下一个函数, 这个流程清晰并且简单, 可是已经足够咱们理解cfs的pick_next了electron
pick_next_task_fair函数的simple标签订义在kernel/sched/fair.c, line 5526), 代码以下所示ide
simple: cfs_rq = &rq->cfs; #endif /* 若是nr_running计数器为0, * 当前队列上没有可运行进程, * 则须要调度idle进程 */ if (!cfs_rq->nr_running) goto idle; /* 将当前进程放入运行队列的合适位置 */ put_prev_task(rq, prev); do { /* 选出下一个可执行调度实体(进程) */ se = pick_next_entity(cfs_rq, NULL); /* 把选中的进程从红黑树移除,更新红黑树 * set_next_entity会调用__dequeue_entity完成此工做 */ set_next_entity(cfs_rq, se); /* group_cfs_rq return NULL when !CONFIG_FAIR_GROUP_SCHED * 在非组调度状况下, group_cfs_rq返回了NULL */ cfs_rq = group_cfs_rq(se); } while (cfs_rq); /* 在没有配置组调度选项(CONFIG_FAIR_GROUP_SCHED)的状况下.group_cfs_rq()返回NULL.所以,上函数中的循环只会循环一次 */ /* 获取到调度实体指代的进程信息 */ p = task_of(se); if (hrtick_enabled(rq)) hrtick_start_fair(rq, p); return p;
其基本流程以下函数
流程 | 描述 |
---|---|
!cfs_rq->nr_running -=> goto idle; | 若是nr_running计数器为0, 当前队列上没有可运行进程, 则须要调度idle进程 |
put_prev_task(rq, prev); | 将当前进程放入运行队列的合适位置, 每次当进程被调度后都会使用set_next_entity从红黑树中移除, 所以被抢占时须要从新加如红黑树中等待被调度 |
se = pick_next_entity(cfs_rq, NULL); | 选出下一个可执行调度实体 |
set_next_entity(cfs_rq, se); | set_next_entity会调用__dequeue_entity把选中的进程从红黑树移除,并更新红黑树 |
put_prev_task
用来将前一个进程prev放回到就绪队列中, 这是一个全局的函数, 而每一个调度器类也必须实现一个本身的put_prev_task函数(好比CFS的put_prev_task_fair),
因为CFS调度的时候, prev进程不必定是一个CFS调度的进程, 所以必须调用全局的put_prev_task来调用prev进程所属调度器类sched_class的对应put_prev_task方法, 完成将进程放回到就绪队列中
全局的put_prev_task函数定义在kernel/sched/sched.h, line 1245, 代码以下所示
static inline void put_prev_task(struct rq *rq, struct task_struct *prev) { prev->sched_class->put_prev_task(rq, prev); }
而后咱们来分析一下CFS的put_prev_task_fair函数, 其定义在kernel/sched/fair.c, line 5572
在选中了下一个将被调度执行的进程以后,回到pick_next_task_fair
中,执行set_next_entity
/* * Account for a descheduled task: */ static void put_prev_task_fair(struct rq *rq, struct task_struct *prev) { struct sched_entity *se = &prev->se; struct cfs_rq *cfs_rq; for_each_sched_entity(se) { cfs_rq = cfs_rq_of(se); put_prev_entity(cfs_rq, se); } }
前面咱们说到过函数在组策略状况下, 调度实体之间存在父子的层次, for_each_sched_entity会从当前调度实体开始, 而后循环向其父调度实体进行更新, 非组调度状况下则只执行一次
而put_prev_task_fair
函数最终会调用put_prev_entity函数将prev的调度时提se放回到就绪队列中等待下次调度
put_prev_entity函数定义在kernel/sched/fair.c, line 3443, 他在更新了虚拟运行时间等信息后, 最终经过__enqueue_entity函数将prev进程(即current进程)放回就绪队列rq上
/* * Pick the next process, keeping these things in mind, in this order: * 1) keep things fair between processes/task groups * 2) pick the "next" process, since someone really wants that to run * 3) pick the "last" process, for cache locality * 4) do not run the "skip" process, if something else is available * * 1. 首先要确保任务组之间的公平, 这也是设置组的缘由之一 * 2. 其次, 挑选下一个合适的(优先级比较高的)进程 * 由于它确实须要立刻运行 * 3. 若是没有找到条件2中的进程 * 那么为了保持良好的局部性 * 则选中上一次执行的进程 * 4. 只要有任务存在, 就不要让CPU空转, * 只有在没有进程的状况下才会让CPU运行idle进程 */ static struct sched_entity * pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr) { /* 摘取红黑树最左边的进程 */ struct sched_entity *left = __pick_first_entity(cfs_rq); struct sched_entity *se; /* * If curr is set we have to see if its left of the leftmost entity * still in the tree, provided there was anything in the tree at all. * * 若是 * left == NULL 或者 * curr != NULL curr进程比left进程更优(即curr的虚拟运行时间更小) * 说明curr进程是自动放弃运行权利, 且其比最左进程更优 * 所以将left指向了curr, 即curr是最优的进程 */ if (!left || (curr && entity_before(curr, left))) { left = curr; } /* se = left存储了cfs_rq队列中最优的那个进程 * 若是进程curr是一个自愿放弃CPU的进程(其比最左进程更优), 则取se = curr * 不然进程se就取红黑树中最左的进程left, 它必然是当前就绪队列上最优的 */ se = left; /* ideally we run the leftmost entity */ /* * Avoid running the skip buddy, if running something else can * be done without getting too unfair. * * cfs_rq->skip存储了须要调过不参与调度的进程调度实体 * 若是咱们挑选出来的最优调度实体se正好是skip * 那么咱们须要选择次优的调度实体se来进行调度 * 因为以前的se = left = (curr before left) curr left * 则若是 se == curr == skip, 则选择left = __pick_first_entity进行便可 * 不然则se == left == skip, 则选择次优的那个调度实体second */ if (cfs_rq->skip == se) { struct sched_entity *second; if (se == curr) /* se == curr == skip选择最左的那个调度实体left */ { second = __pick_first_entity(cfs_rq); } else /* 不然se == left == skip, 选择次优的调度实体second */ { /* 摘取红黑树上第二左的进程节点 */ second = __pick_next_entity(se); /* 同时与left进程同样, * 若是 * second == NULL 没有次优的进程 或者 * curr != NULL curr进程比left进程更优(即curr的虚拟运行时间更小) * 说明curr进程比最second进程更优 * 所以将second指向了curr, 即curr是最优的进程*/ if (!second || (curr && entity_before(curr, second))) second = curr; } /* 判断left和second的vruntime的差距是否小于sysctl_sched_wakeup_granularity * 即若是second能抢占left */ if (second && wakeup_preempt_entity(second, left) < 1) se = second; } /* * Prefer last buddy, try to return the CPU to a preempted task. * * */ if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1) se = cfs_rq->last; /* * Someone really wants this to run. If it's not unfair, run it. */ if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1) se = cfs_rq->next; /* 用过一次任何一个next或者last * 都须要清除掉这个指针 * 以避免影响到下次pick next sched_entity */ clear_buddies(cfs_rq, se); return se; }
pick_next_entity则从CFS的红黑树中摘取一个最优的进程, 这个进程每每在红黑树的最左端, 即vruntime最小, 可是也有例外, 可是不外乎这几个进程
调度实体 | 描述 |
---|---|
left = __pick_first_entity(cfs_rq) | 红黑树的最左节点, 这个节点拥有当前队列中vruntime最小的特性, 即应该优先被调度 |
second = __pick_first_entity(left) | 红黑树的次左节点, 为何这个节点也可能呢, 由于内核支持skip跳过某个进程的抢占权力的, 若是left被标记为skip(由cfs_rq->skip域指定), 那么可能就须要找到次优的那个进程 |
curr结点 | curr节点的vruntime可能比left和second更小, 可是因为它正在运行, 所以它不在红黑树中(进程抢占物理机的时候对应节点同时会从红黑树中删除), 可是若是其vruntime足够小, 意味着cfs调度器应该尽量的补偿curr进程, 让它再次被调度 |
其中__pick_first_entity会返回cfs_rq红黑树中的最左节点rb_leftmost所属的调度实体信息, 该函数定义在kernel/sched/fair.c, line 543
而__pick_next_entity(se)函数则返回se在红黑树中中序遍历的下一个节点信息, 该函数定义在kernel/sched/fair.c, line 544, 获取下一个节点的工做能够经过内核红黑树的标准操做rb_next完成
在pick_next_entity的最后, 要把红黑树最左下角的进程和另外两个进程(即next和last)作比较, next是抢占失败的进程, 而last则是抢占成功后被抢占的进程, 这三个进程到底哪个是最优的next进程呢?
Linux CFS实现的判决条件是:
cfs_rq的last和next指针,last表示最后一个执行wakeup的sched_entity,next表示最后一个被wakeup的sched_entity。他们在进程wakeup的时候会赋值,在pick新sched_entity的时候,会优先选择这些last或者next指针的sched_entity,有利于提升缓存的命中率
所以咱们优选出来的进程必须同last和next指针域进行对比, 其实就是检查就绪队列中的最优进程, 即红黑树中最左节点last是否能够抢占last和next指针域, 检查是否能够抢占是经过wakeup_preempt_entity
函数来完成的.
// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5317 /* * Should 'se' preempt 'curr'. * * |s1 * |s2 * |s3 * g * |<--->|c * * w(c, s1) = -1 * w(c, s2) = 0 * w(c, s3) = 1 * */ static int wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se) { /* vdiff为curr和se vruntime的差值*/ s64 gran, vdiff = curr->vruntime - se->vruntime; /* cfs_rq的vruntime是单调递增的,也就是一个基准 * 各个进程的vruntime追赶竞争cfsq的vruntime * 若是curr的vruntime比较小, 说明curr更加须要补偿, * 即se没法抢占curr */ if (vdiff <= 0) return -1; /* 计算curr的最小抢占期限粒度 */ gran = wakeup_gran(curr, se); /* 当差值大于这个最小粒度的时候才抢占,这能够避免频繁抢占 */ if (vdiff > gran) return 1; return 0; } // http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5282 static unsigned long wakeup_gran(struct sched_entity *curr, struct sched_entity *se) { /* NICE_0_LOAD的基准最小运行期限 */ unsigned long gran = sysctl_sched_wakeup_granularity; /* * Since its curr running now, convert the gran from real-time * to virtual-time in his units. * * By using 'se' instead of 'curr' we penalize light tasks, so * they get preempted easier. That is, if 'se' < 'curr' then * the resulting gran will be larger, therefore penalizing the * lighter, if otoh 'se' > 'curr' then the resulting gran will * be smaller, again penalizing the lighter task. * * This is especially important for buddies when the leftmost * task is higher priority than the buddy. * * 计算进程运行的期限,即抢占的粒度 */ return calc_delta_fair(gran, se); }
到底能不能选择last和next两个进程, 则是wakeup_preempt_entity函数来决定的, 看下面的图解便可:
若是S3是left,curr是next或者last,left的vruntime值小于curr和next, 函数wakeup_preempt_entity确定返回1,那么就说明next和last指针的vruntime和left差距过大,这个时候没有必要选择这个last或者next指针,而是应该优先补偿left
若是next或者last是S2,S1,那么vruntime和left差距并不大,并无超过sysctl_sched_wakeup_granularity ,那么这个next或者last就能够被优先选择,而代替了left
而清除last和next这两个指针的时机有这么几个:
如今咱们已经经过pick_next_task_fair选择了进程, 可是还须要完成一些工做, 才能将其标记为运行进程. 这是经过set_next_entity来处理的. 该函数定义在kernel/sched/fair.c, line 3348
当前执行进程(咱们选择出来的进程立刻要抢占处理器开始执行)不该该再保存在就绪队列上, 所以set_next_entity()函数会调用__dequeue_entity(cfs_rq, se)把选中的下一个进程移出红黑树. 若是当前进程是最左节点, __dequeue_entity会将leftmost指针设置到次左进程
/* 'current' is not kept within the tree. */ if (se->on_rq) /* 若是se尚在rq队列上 */ { /* ...... */ /* 将se从cfs_rq的红黑树中删除 */ __dequeue_entity(cfs_rq, se); /* ...... */ }
尽管该进程再也不包含在红黑树中, 可是进程和就绪队列之间的关联并无丢失, 由于curr标记了当前进程cfs_rq->curr = se;
cfs_rq->curr = se;
而后接下来是一些统计信息的处理, 若是内核开启了调度统计CONFIG_SCHEDSTATS标识, 则会完成调度统计的计算和更新
#ifdef CONFIG_SCHEDSTATS /* * Track our maximum slice length, if the CPU's load is at * least twice that of our own weight (i.e. dont track it * when there are only lesser-weight tasks around): */ if (schedstat_enabled() && rq_of(cfs_rq)->load.weight >= 2*se->load.weight) { se->statistics.slice_max = max(se->statistics.slice_max, se->sum_exec_runtime - se->prev_sum_exec_runtime); } #endif
在set_next_entity的最后, 将选择出的调度实体se的sum_exec_runtime保存在了prev_sum_exec_runtime中, 由于该调度实体指向的进程, 立刻将抢占处理器成为当前活动进程, 在CPU上花费的实际时间将记入sum_exec_runtime, 所以内核会在prev_sum_exec_runtime保存此前的设置. 要注意进程中的sum_exec_runtime没有重置. 所以差值sum_exec_runtime - prev_sum_runtime确实标识了在CPU上执行花费的实际时间.
最后咱们附上set_next_entity函数的完整注释信息
static void set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) { /* 'current' is not kept within the tree. */ if (se->on_rq) /* 若是se尚在rq队列上 */ { /* * Any task has to be enqueued before it get to execute on * a CPU. So account for the time it spent waiting on the * runqueue. */ if (schedstat_enabled()) update_stats_wait_end(cfs_rq, se); /* 将se从cfs_rq的红黑树中删除 */ __dequeue_entity(cfs_rq, se); update_load_avg(se, 1); } /* 新sched_entity中的exec_start字段为当前clock_task */ update_stats_curr_start(cfs_rq, se); /* 将se设置为curr进程 */ cfs_rq->curr = se; #ifdef CONFIG_SCHEDSTATS /* * Track our maximum slice length, if the CPU's load is at * least twice that of our own weight (i.e. dont track it * when there are only lesser-weight tasks around): */ if (schedstat_enabled() && rq_of(cfs_rq)->load.weight >= 2*se->load.weight) { se->statistics.slice_max = max(se->statistics.slice_max, se->sum_exec_runtime - se->prev_sum_exec_runtime); } #endif /* 更新task上一次投入运行的从时间 */ se->prev_sum_exec_runtime = se->sum_exec_runtime; }
/* 若是nr_running计数器为0, * 当前队列上没有可运行进程, * 则须要调度idle进程 */ if (!cfs_rq->nr_running) goto idle;
若是系统中当前运行队列上没有可调度的进程, 那么会调到idle标签去调度idle进程.
idle标签以下所示
idle: /* * This is OK, because current is on_cpu, which avoids it being picked * for load-balance and preemption/IRQs are still disabled avoiding * further scheduler activity on it and we're being very careful to * re-start the picking loop. */ lockdep_unpin_lock(&rq->lock); new_tasks = idle_balance(rq); lockdep_pin_lock(&rq->lock); /* * Because idle_balance() releases (and re-acquires) rq->lock, it is * possible for any higher priority task to appear. In that case we * must re-start the pick_next_entity() loop. */ if (new_tasks < 0) return RETRY_TASK; if (new_tasks > 0) goto again; return NULL;
其关键就是调用idle_balance进行任务的迁移
每一个cpu都有本身的运行队列, 若是当前cpu上运行的任务都已经dequeue出运行队列,并且idle_balance也没有移动到当前运行队列的任务,那么schedule函数中,按照stop > idle > rt > cfs > idle这三种调度方式顺序,寻找各自的运行任务,那么若是rt和cfs都未找到运行任务,那么最后会调用idle schedule的idle进程,做为schedule函数调度的下一个任务
若是某个cpu空闲, 而其余CPU不空闲, 即当前CPU运行队列为NULL, 而其余CPU运行队列有进程等待调度的时候, 则内核会对CPU尝试负载平衡, CPU负载均衡有两种方式: pull和push, 即空闲CPU从其余忙的CPU队列中pull拉一个进程复制到当前空闲CPU上, 或者忙的CPU队列将一个进程push推送到空闲的CPU队列中.
idle_balance其实就是pull的工做.
组调度的情形下, 调度实体之间存在明显的层次关系, 所以在跟新子调度实体的时候, 须要更新父调度实体的信息, 同时咱们为了保证同一组内的进程不能长时间占用处理机, 必须补偿其余组内的进程, 保证公平性
#ifdef CONFIG_FAIR_GROUP_SCHED /* 若是nr_running计数器为0, 即当前队列上没有可运行进程, * 则须要调度idle进程 */ if (!cfs_rq->nr_running) goto idle; /* 若是当前运行进程prev不是被fair调度的普通非实时进程 */ if (prev->sched_class != &fair_sched_class) goto simple; /* * Because of the set_next_buddy() in dequeue_task_fair() it is rather * likely that a next task is from the same cgroup as the current. * * Therefore attempt to avoid putting and setting the entire cgroup * hierarchy, only change the part that actually changes. */ do { struct sched_entity *curr = cfs_rq->curr; /* * Since we got here without doing put_prev_entity() we also * have to consider cfs_rq->curr. If it is still a runnable * entity, update_curr() will update its vruntime, otherwise * forget we've ever seen it. */ if (curr) { /* 若是当前进程curr在队列上, * 则须要更新起统计量和虚拟运行时间 * 不然设置curr为空 */ if (curr->on_rq) update_curr(cfs_rq); else curr = NULL; /* * This call to check_cfs_rq_runtime() will do the * throttle and dequeue its entity in the parent(s). * Therefore the 'simple' nr_running test will indeed * be correct. */ if (unlikely(check_cfs_rq_runtime(cfs_rq))) goto simple; } /* 选择一个最优的调度实体 */ se = pick_next_entity(cfs_rq, curr); cfs_rq = group_cfs_rq(se); } while (cfs_rq); /* 若是被调度的进程仍属于当前组,那么选取下一个可能被调度的任务,以保证组间调度的公平性 */ /* 获取调度实体se的进程实体信息 */ p = task_of(se); /* * Since we haven't yet done put_prev_entity and if the selected task * is a different task than we started out with, try and touch the * least amount of cfs_rqs. */ if (prev != p) { struct sched_entity *pse = &prev->se; while (!(cfs_rq = is_same_group(se, pse))) { int se_depth = se->depth; int pse_depth = pse->depth; if (se_depth <= pse_depth) { put_prev_entity(cfs_rq_of(pse), pse); pse = parent_entity(pse); } if (se_depth >= pse_depth) { set_next_entity(cfs_rq_of(se), se); se = parent_entity(se); } } put_prev_entity(cfs_rq, pse); set_next_entity(cfs_rq, se); } if (hrtick_enabled(rq)) hrtick_start_fair(rq, p); return p;
咱们在以前讲解主调度器的时候就提到过, 主调度器函数schedule会调用__schedule来完成抢占, 而主调度器的主要功能就是选择一个新的进程来抢占到当前的处理器. 所以其中必然不能缺乏pick_next_task工做
参见主调度器schedule)中调用全局的pick_next_task选择抢占的进程一节的内容
__schedule调用全局的pick_next_task函数选择一个最优的进程, 内核代码参见kernel/sched/core.c, line 3142
static void __sched notrace __schedule(bool preempt) { /* ...... */ next = pick_next_task(rq); /* ...... */ }
全局的pick_next_task函数会从按照优先级遍历全部调度器类的pick_next_task函数, 去查找最优的那个进程, 固然由于大多数状况下, 系统中全是CFS调度的非实时进程, 于是linux内核也有一些优化的策略
其执行流程以下