B. prev
在schedule中,当前进程(也就是可能被调度走的进程)用prev指针访问。
对于SCHED_RR的实时进程,仅当该进程时间片结束(counter==0)后才会切换到别的进程,此时将根据nice值重置counter,并将该进程置于就绪队列的末尾。当然,如果当前就绪队列中不存在其他实时进程,则根据前面提到的goodness()算法,调度器仍将选择到该进程。
如果处于TASK_INTERRUPTIBLE状态的进程有信号需要处理(这可能发生在进程因等待信号而准备主动放弃CPU,在放弃CPU之前,信号已经发生了的情况),调度器并不立即执行该进程,而是将该进程置为就绪态(该进程还未来得及从就绪队列中删除),参与紧接着的goodness选择。
如果prev不处于就绪态,也不处于上面这种有信号等待处理的挂起态(prev为等待资源而主动调用schedule()放弃CPU),那么它将从就绪队列中删除,此后,除非有唤醒操作将进程重新放回到就绪队列,否则它将不参与调度。
被动方式启动调度器工作时,当前进程的need_resched属性会置位(见下"调度器工作时机")。在schedule()中,该位会被清掉,表示该进程已经在调度器中得到了处理(当然,这一处理并不意味着该进程就一定获得了CPU)。
C. goodness
调度器遍历就绪队列中的所有进程,只要它当前可被调度(cpus_runnable & cpus_allowed & (1 << cpu),表示该进程可在当前运行调度器的CPU上运行,且不在其他CPU上运行),就调用goodness()计算其权值。next指针用来指向权值最大的进程,缺省指向idle_task,如果就绪队列为空,就使用缺省的idle_task作为next。
正如前面所提到的,如果遍历结束后的最大权值为0,则表示当前所有可被调度的就绪进程的时间片都用完了,这时调度器将需要重新设置所有进程(包括就绪的和挂起的)的counter值,未完成时间片的进程(例如当前被挂起的进程或者当前正在其他CPU上运行的进程),其剩下的时间片的一半将叠加到新的时间片中。
将选中的进程设置为在本CPU上运行(task_set_cpu())之后,runqueue_lock就可以解开了,接下来就将对next进行配置。
D. next
选取的新进程可能刚好就是需要替换出去的老进程,此时因为实际上不需要进行进程切换,所以可以跳过配置next以及下面的"switch"和"schedule_tail"两个阶段。
新进程的运行环境实际上主要就是指内存。在task_struct中有两个与调度器相关的内存属性:mm和active_mm,前者指向进程所拥有的内存区域,后者则指向进程所实际使用的内存。对于大多数进程,mm和active_mm是相同的,但核心线程没有自主的内存,它们的mm指针永远为NULL。我们知道,任何进程的虚页表中,核心空间永远映射到了虚存的高端固定位置,所以,核心线程所使用的内存无论对于哪个进程空间都是一样的,所以也就没有必要切换进程的内存。在调度器中,只要判断一下next->mm是否为空就能知道该进程是不是核心线程,如果是,则继续使用prev的active_mm(next->active_mm = prev->active_mm),并通过设置cpu_tlbstate[cpu].state为TLBSTATE_LAZY,告诉内存管理部件不要刷新TLB;否则就调用switch_mm()函数进行内存的切换(具体过程牵涉到内存管理模块的知识,这里就从略了)。实际上,在switch_mm()中还会对prev->active_mm和next->mm判断一次,如果两值相等,说明两个进程是同属于一个"进程"的两个"线程"(实际上是轻量进程),此时也不需要执行内存的切换,但这种情况TLB还是需要刷新的。
设置好next的内存环境以后,就可以调用mmdrop()释放掉prev的内存结构了。所有不在运行中的进程,其active_mm属性都应该为空。
E. switch
进程切换的过程在上文中已经描述得比较详细了。
F. schedule_tail
完成切换后,调度器将调用__schedule_tail()。这一函数对于UP系统基本没什么影响,对于SMP系统,如果被切换下来的进程(用p表示)仍然处于就绪态且未被任何CPU调度到,__schedule_tail()将调用reschedule_idle(),为p挑选一个空闲的(或者是所运行的进程优先级比p低的)CPU,并强迫该CPU重新调度,以便将p重新投入运行。进程从休眠状态中醒来时也同样需要挑选一个合适的CPU运行,这一操作是通过在wake_up_process()函数中调用reschedule_idle()实现的。挑选CPU的原则如下:
p上次运行的CPU目前空闲。很显然,这是最佳选择,因为不需要抢占CPU,CPU Cache也最有可能和p吻合。不过,既然p可运行,调度器就不可能调度到idle_task,所以这种情况只会发生在wake_up_process()的时候。
所有空闲的CPU中最近最少活跃(last_schedule(cpu)最小)的一个。该CPU中的Cache信息最有可能是无用的,因此这种选择方式可以尽最大可能减少抢占CPU的开销,同时也尽可能避免频繁抢占。值得注意的是,在使用支持超线程技术的CPU的SMP平台上,一旦发现一个物理CPU的两个逻辑CPU均空闲,则该CPU的其中一个逻辑CPU立即成为p候选的调度CPU,而不需要继续寻找最近最少活跃的CPU。
CPU不空闲,但所运行的进程优先级比p的优先级低,且差值最大。计算优先级时使用的是goodness()函数,因为它所包含的信息最多。
找到合适的CPU后,reschedule_idle()就会将目标进程(正在该CPU上运行的进程,可能是idle_task)的need_resched置为1,以便调度器能够工作(见"调度器工作时机")。同时,因为idle_task很多情况下都使cpu处于停机(halt)状态以节电,所以有必要调用smp_send_reschedule(cpu)向cpu发RESCHEDULE_VECTOR中断(通过IPI接口),以唤醒该cpu。
注:对于目标进程是idle_task的情况,还要判断它的need_resched标志位,仅当它为0的时候才会启动调度,因为非0状态的idle_task本身一直都在检查need_resched值,它自己会启动schedule()(见下"IDLE进程")。
G. clear
调度器工作的结果有两种:发生了切换、没有发生切换,但调度器退出前的清理工作是一样的,就是恢复新进程的状态。主要包含两个动作:
清被切换走的进程的SCHED_YIELD位(不管它是否置位);
如果新进程(p)的lock_depth大于等于0,则重新为核心锁kernel_flag加锁(见上"相关锁")。