linux调度器(四)——主调度器与CFS

         当内核从系统调用返回,或者从中断处理程序返回,内核都会检查当前进程是否设置了TIF_NEED_RESCHED标志;或者进程主动放弃CPU时(sched_yield,sleep或者收到SIGSTOP,SIGTTOP信号)都会进入主调度器。同样的我们先看一下主调度的框架部分,该部分就是sched.c:schedule(void):
关闭内核抢占
如果进程之前是不可运行并且被内核抢占了,那么如果它现在有非阻塞信号,则将它的状态改为TASK_RUNNING而且不移出就绪队列,否则该进程(不可运行)从就绪队列中取出deactivate_task
判断是否要进行负载均衡(当前运行队列为空)
通知调度器类将当前(活动)进程将被其它进程替换掉(put_prev_task)
选择下一个将要被执行的进程(pick_next_task),并清除前一个进程的TIF_NEED_RESCHED
进行上下文切换(context_switch)
重新计算新进程所在的cpu及rq,即为当前cpu(因为新的进程之前可能在不同的cpu上运行了,同样老的进程唤醒时也是所这里开始)
如果新的进程也被设置了TIF_NEED_RESCHED,则再次调度
大体过程如下图:


图 schedule与CFS的交互

         下面我们主要来分析下CFS的相关的三个操作:
deactivate_task:该函数最终调用CFS的dequeue_task_fair,并且将进程的p->se.on_rq置0,表示该进程不在运行队列里。 dequeue_task_fair对于非组调度的话就是调用dequeue_entity更新执行进程的信息update_curr,把该se从buddies中去掉(clear_buddies,见后面的分析),如果这个se不是正在运行的进程则把该se从运行队列的红黑树上删除掉(运行的进程已经不存在红黑树里),置se->on_rq = 0,并且减少运行列队的相应load(update_cfs_load:这里更新的是统计值的load,account_entity_dequeue这个才是真正更新跟进程调度相关的cfs_rq->load)及se的weight(update_cfs_shares)其它统计信息(注意:当se出队时如果它不是DEQUEUE_SLEEP必须把vruntime标准化se->vruntime -= cfs_rq->min_vruntime,否则就不需要标准化, 这里不是很明白?)。对于组调度,它从当前进程开始dequeue_entity,如果它的父group load为0,那么说明这个父group也应该被 dequeue_entity,直到不为0(该group有其它进程就绪)的祖先group为止,到这里就把从叶子(当前进程)到该进程向上递归load为空的父group都出队了;然后再更新从这个非空的父group到根的其余group se的load(这里只是更新统计的load update_cfs_load,而cfs_rq的load因为只记录它本层的se的load之和不递归,所以不需要再更新该load),shares及h_nr_running统计,因为它们下层的se已经被出队列了。另外,所有被dequeue的se的on_rq被置为0

put_prev_task_fair:该过程是与上一个函数不一样的,上一个把 不可运行的进程从运行队列中删除掉,而 put_prev_task_fair主要是通知CFS当前进程将会被调度出去了,如果当前进程已经不是可运行进程(on_rq=0), 那么这个函数只会把当前 cfs_rq->curr 置为 NULL,表示当前cfs_rq没有进程正在运行,否则如果当前进程还是可运行的那么还需要对它的状态进行更新:update_curr更新它的实质物理运行时间,虚拟时间及它从现在开始就是进入等待的时间,并且再次将该进程重新入队列(__enqueue_entity当前进程还是可运行状态)。对于组调度同样的需要更新从该进程到它的根group的所有se,包括每个se的执行时间(这里的执行时间并不是代表它在cpu的执行时间,而是由它的下级执行时间的一个反映),至于每个层次的se都把它的cfs_rq->curr置为NULL是因为:在一个CPU上某一时刻只有一个进程在运行,当当前运行要被调度出去的时候,也代表了它的所有上层group在这个CPU上将被调度出去(对于group这只是一个理论概论,它并不会真正在CPU上运行,只是为了与真正task统一起来才有这个标志,表示当它的叶子task在CPU上运行;同样的,当某group的叶子被调度[运行]时,它的所有上层group在它所在的运行队列里也被表示为运行的)。

pick_next_task:挑选一个最需要运行的进程来运行。如果当前队列的等待运行的进程总数等于cfs等待的数目,那么就直接从cfs中挑选一个,否则从高优先策略的调度类中挑选一个进程来运行。这里我们直接看CFS的pick_next_task_fair(这里从根的cgroup开始一层一层往左边找):通过 pick_next_entity从当前层的cfs_rq判断哪个se将被取出,它采用这样的优先级(从高到低)——已经被要求运行的se(cfs_rq->next,即next要求抢占),上一个运行的se(cfs_rq->last),不是被skip的se,而且这三个优先级都还需要满足——它们比起cfs_rq最左边的se更需要先被运行(wakeup_preempt_entity,它们的虚拟运行时间小于最左边的虚拟运行时间,或者比最左边再运行最小运行时间后的新的虚拟时间还小,减少不必要的切换);这样就能选出一个合适的se;然后调用set_next_entity将该se设置为当前cfs_rq上正在运行的进程:如果这个se还在运行队列里则更新它的等待结束时间,及出队列(运行进程不应该放在就绪队列里,注这里调用的是__dequeue_entity而不是 dequeue_entity,后者是把这可运行的se出队列并需要更新nr_running--,on_rq=0等,而前者是不需要的,即运行的进程虽然不在红黑树里,但是se->nr_rq还是等于1,cfs_rq->nr_running还是包括这个运行的进程);更新开始执行的时钟,将cfs_rq->curr置为该se。对于非组调度这样就能把该se的task返回;而对于组调度其实也很简单,如果pick_next_entity取得的是一个group的话,那么再从它的运行队列里se->my_q里选出一个合适的se出来,直到该se是非group,而且这些group的se所在的cfs_rq也会把curr置为当前递归的group se(这也是我们上面说的put_prev_entity的反操作)。
    总之,schedule是为了完成从prev进程切换到next进程的过程,如果prev是不可运行的并且没收到信号那么应该先把它从运行队列里去掉(deactivate_task),注意此时还是它占用的CPU所以还需要更新它的执行时间(update_curr);然后告诉CFS该prev将要被调度出去了,此时也是需要考虑它是否是可运行的状态,还是不可运行状态,如果是不可运行状态,那么上面它已经被从运行队列中去掉,并且on_rq的标志也被清0,所以只需要把cfs_rq->curr置为NULL就可以了,否则就是它是可运行的,那么首先也是先更新它的执行时间update_curr,然后把它重新放到运行队列里(当前运行的进程是不在运行队列里的),最后同样把cfs_rq->curr置为NULL;接着从CFS里挑选一个合适的进程来执行,一些比较优先考虑的进程被保存在buddies(next,last),所以它先从这些里及最左筛选,筛选后把该se从运行队列中出队,相应的最后需要把cfs_rq->curr置为当前被筛选出来的se,表示该se是当前cfs_rq上运行的se。
         这样我们就把调度器两个主要部件介绍完了,下面介绍到task创建时的调度器对新任务的初始化过程。我们估且把该过程称为进程调度初始化,下面我们就来分析该过程。

你可能感兴趣的:(linux基础)