Linux中的进程调度(二)
近期主要研究linux kernel中的进程调度算法。
在2.4及2.4以前的版本中,基本上都是根据优先级来选择下一个被调入的进程。算法时间复杂度为O(n)(因为要遍历所有进程,来决定哪个被选中)。当进程数比较多时,些操作费时较大。
在2.6版本刚推出时,采用著名的O(1)调度算法,每个优先级有一个单独的可运行队列,此队列又分为active和expired两个,通过指针的交换等巧妙操作,使选择下一个进程操作的时间复杂度降到了常数时间,O(1)调度算法因此得名。
在2.6.23版本的内核中,又推出了CFS(complete fair scheduler),抛弃了以前优先级和时间片是固定的简单映射方式,采用虚拟运行时间,同时用红黑树维护可运行队列。
下面来具体分析其实现(代码以2.6.24为准)。
先来看新创建一个进程时的动作。
在do_fork()函数后,根据clone_flags复制完父进程相关资源后,将会执行wake_up_new_task(p,clone_flags),将新创建的进程加入可执行队列(这里是红黑树),然后重新进行一次调度。相关代码如下
if (!(clone_flags & CLONE_STOPPED)) wake_up_new_task(p, clone_flags);我们顺着调用路线往下走,进入到wake_up_new_task中去。
/* * wake_up_new_task - wake up a newly created task for the first time. * * This function will do some initial scheduler statistics housekeeping * that must be done for every newly created context, then puts the task * on the runqueue and wakes it. */ void fastcall wake_up_new_task(struct task_struct *p, unsigned long clone_flags) { unsigned long flags; struct rq *rq; rq = task_rq_lock(p, &flags); BUG_ON(p->state != TASK_RUNNING); update_rq_clock(rq); p->prio = effective_prio(p); if (!p->sched_class->task_new || !current->se.on_rq) { activate_task(rq, p, 0); } else { /* * Let the scheduling class do new task startup * management (if any): */ p->sched_class->task_new(rq, p); inc_nr_running(p, rq); } check_preempt_curr(rq, p); task_rq_unlock(rq, &flags); }首先,将进程p所在的CPU的可执行队列加锁(自旋锁),然后调用update_rq_clock(rq),更改一些此队列的统计信息(这些信息不是针对某个进程,而是针对这整个队列),然后,通过effective_prio(p)算出进程的优先级(虽然在CFS调度器中优先级不直接与时间片进行映射,但是还是会作为权重来区分进程的重要性),此函数如下
/* * Calculate the current priority, i.e. the priority * taken into account by the scheduler. This value might * be boosted by RT tasks, or might be boosted by * interactivity modifiers. Will be RT if the task got * RT-boosted. If not then it returns p->normal_prio. */ static int effective_prio(struct task_struct *p) { p->normal_prio = normal_prio(p); /* * If we are RT tasks or we were boosted to RT priority, * keep the priority unchanged. Otherwise, update priority * to the normal priority: */ if (!rt_prio(p->prio)) return p->normal_prio; return p->prio; }可见,先通过normal_prio(p)得到p的normal_prio,进入此函数
/* * Calculate the expected normal priority: i.e. priority * without taking RT-inheritance into account. Might be * boosted by interactivity modifiers. Changes upon fork, * setprio syscalls, and whenever the interactivity * estimator recalculates. */ static inline int normal_prio(struct task_struct *p) { int prio; if (task_has_rt_policy(p)) prio = MAX_RT_PRIO-1 - p->rt_priority; else prio = __normal_prio(p); return prio; }这里需要区分实时进程与非实时进程,task_has_rt_policy()代码如下
static inline int rt_policy(int policy) { if (unlikely(policy == SCHED_FIFO) || unlikely(policy == SCHED_RR)) return 1; return 0; }
static inline int task_has_rt_policy(struct task_struct *p) { return rt_policy(p->policy); }如果进程所属的调度策略是SCHED_FIFO或者SCHED_RR(这两种实时进程的区别最大之处就是有没有时间片的概念,RR=round robin),这里用了unlikely宏,将此宏展开后,是gcc编译器专门的一个优化,也就是说,如果if条件很大概率下为真,那么这部分代码在编译时会放到较前的位置。可见,kernel的设计者考虑的何等细致。 回到刚才的normal_prio, 如果通过task_has_rt_policy()发现进程是一个实时进程,那么,返回MAX_RT_PRIO-1-p->rt_priority,(对于实时进程,rt_priority值越大,优先级越高) 此值将会赋给p->normal_prio(明明是实时优先级,为什么还要赋给normal_prio呢?先把网上找到的一段话放到这里,以后慢慢研究) prio和normal_prio为动态优先级,static_prio为静态优先级。static_prio是进程创建时分配的优先级,如果不人为的更 改,那么在这个进程运行期间不会发生变化。 normal_prio是基于static_prio和调度策略计算出的优先级。prio是调度器类考虑的优先级,某些情况下需要暂时提高进程的优先级 (实时互斥量),因此有此变量,对于优先级未动态提高的进程来说这三个值是相等的。以上三个优先级值越小,代表进程的优先级有高。一般情况下子进程的静态 优先级继承自父进程,子进程的prio继承自父进程的normal_prio。 如果发现进程是一个非实时进程,那么,返回__normal_prio(),这个函数的代码如下
/* * __normal_prio - return the priority that is based on the static prio */ static inline int __normal_prio(struct task_struct *p) { return p->static_prio; }也就是说,对于非实时进程,返回的就是它的静态优先级 好了,绕了这么远分析返回值是什么,再回过头去看返回值给了谁
p->prio = effective_prio(p);看来,进程调度时,是以prio为准进行调度的。 今天先分析到这,将优先级确定后的动作下回再分析~