schedule()
选定下一个需要调度的进程并切换到该进程上面去执行是通过schedule()
函数来实现的。当内核代码想要休眠时,将会直接调用该函数;另外,在系统返回到用户态以及从中断返回时,内核都会调用need_resched()
函数检查时候设置了need_resched
标志,来决定是否调用schedule()
函数。need_resched
存放在thread_info
结构体中,用一个标志变量中的一位来表示,用于表示当前是否需要重新调度。
schedule()
函数独立于每个处理器运行,每个CPU都有自己的标号id和运行队列,因此每个CPU可以独立地对下一次该运行那个进程做出判断。通过smp_processor_id()
可以获取当前的CPU标号,其类型为unsigned int
。通过cpu_rq(cpu)
可以获取CPU的运行队列。
schedule()
函数中的部分代码如下:
need_resched:
preempt_disable();
cpu=smp_processor_id();
rq=cpu_rq(cpu);
rcu_qsctr_inc(cpu);
prev=rq->curr;
switch_count=&prev->nivcsw;
release_kernel_lock(prev);
调用preempt_disable()
函数禁止内核抢占,然后获取当前CPU的运行队列。调用release_kernel_lock()
函数,如果内核被锁定,该函数释放大内核锁。
local_irq_disable();
_update_rq_clock(rq);
sppin_lock(&rq->lock);
clear_tsk_need_resched(prev);
代码中local_irq_disable()
函数用于禁止当前处理器中断,_update_rq_clock()
函数用于更新当前运行队列rq
的时钟,clear_tsk_need_resched()
函数用于清除当前进程的need_resched()
标志。
if(prev->state&&!(preempt_count()&&PREEMPT_ACCTIVE))
{
if(unlikely((prev->state&TASK_INTERRUPTIBLE)&&unlikely(signal_pending(prev))))
{
prev->state=TASK_RUNNING;
}
else
{
deactivate_task(rq,prev,1);
}
switch_count=&prev->nvcsw;
}
if(prev->state&&!(preempt_count()&&PREEMPT_ACCTIVE))
,表示判断当前进程的状态是否为TASK_RUNNING
状态,并且当前的内核是否为非抢占式。
if(unlikely((prev->state&TASK_INTERRUPTIBLE)&&unlikely(signal_pending(prev))))
{
prev->state=TASK_RUNNING;
}
else
{
deactivate_task(rq,prev,1);
}
其中,以上代码查看当前进程的状态是否为可中断睡眠状态TASK_INTERRUPTIBLE
,并且等待的信号已经发生。如果是,则将当前进程的状态重新设置为可执行状态TASK_RUNNING
,否则将当前进程从运行队列中删除。
if(unlikely(!rq->nr_running))
{
idle_balance(cpu,rq);
}
以上代码检查当前CPU的运行队列是否有可执行的进程,如果没有可执行的进程且定义了SMP,调用idle_balance()
函数进行负载平衡。
prev->sched_class->put_prev_task(rq,prev);
以上代码调用当前进程调度类的put_prev_task()
函数,根据进程的状态将其放入到相应的队列中。
next=pick_next_task(rq,prev);
以上代码调用pick_next_task()
函数从当前CPU的运行队列中选取下一个需要执行的任务。pick_next_task()
函数在选择下一个需要调度的进程时,充分体现了模块化调度的好处。它首先检查是否有实时进程,如果没有,就调用CFS调度类的pick_next_task()
函数选择下一个需要调度的进程;如果存在实时进程,则调用实时进程调度类的pick_next_task()
函数。
if(likely(prev!=next))
{
rq->nr_switchs++;
rq->curr=next;
++*switch_count;
context_switch(rq,prev,next);
}
else
{
spin_unlock_irq(&rq->lock);
}
以上代码检查下一个需要调度的进程和当前进程是否为同一个进程。如果不是,则调用context_switch()
函数进行上下文切换。context_switch()
函数负责上下文的切换,即从一个进程切换到另一个可执行的进程。它主要完成以下两个工作:
switch_mm()
来实现的。switch_to()
来实现的。为了提高系统的响应能力、实时能力以及用户的满意度,在Linux2.6内核中引入了抢占机制;即只要重新调度是安全的,那么内核就可以在任何时间抢占正在执行的任务。内核抢占与用户抢占一样,包含显式内核抢占和隐式内核抢占。
(1)显式内核抢占:这种抢占很简单,它发生在内核代码调用schedule()
函数时的内核空间中,内核代码可以用两种方式调用schedule()
函数,即主动调用schedule()
函数让出CPU控制权或内核代码被阻塞时调用schedule()
函数。显式内核抢占从来都是受支持的,因为它无需额外的逻辑来保证内核可以安全地被抢占,如果内核代码调用schedule()
,那么它应该很清楚自己是可以被安全地抢占的。
(2)隐式内核抢占:这是Linux2.6新增的功能,当一个内核任务拥有对CPU控制权时,只要当前进程没有持有任何锁(即重新调度是安全的),这个内核任务就可以被另一个更高优先级的内核任务抢占。为了支持隐式内核抢占,在每个进程的thread_info
结构中引入了preempt_count
计数器,该计数器的初始值为0,每当使用锁的时候数值增加1,释放锁的时候数值减1。当数值为0时,内核就可以执行抢占。从中断返回内核空间的时候,内核会检查need_resched
和preempt_count
的值,如果need_resched
被设置,并且preempt_count
为0的话,这说明有一个更为重要的任务需要执行且可以安全地被抢占,此时,调度程序就会被调用。如果preempt_count
不为0,说明当前的任务持有锁,不能被抢占,这时就会直接从中断返回到当前执行的任务中。如果当前的任务持有的锁被释放,preempt_count
等于0,释放锁的代码就会检查need_resched
时候被设置,如果是,就会调用调度程序。与内核相关的几个函数如下:
preempt_enable_no_resched()
函数激活内核抢占,该函数会调用dec_preempt_count()
函数,使preempt_count
的值减1。
preempt_disable()
函数禁止内核抢占,该函数会调用inc_preempt_count()
,使preempt_count
的值加1。
preempt_enable()
函数激活内核抢占且检查是否需要内核抢占,该函数会调用preempt_enable_no_resched()
函数,同时会调用preempt_check_resched()
判断当前进程是否被标记为重新调度,如果是,它调用preempt_schedule()
函数进行内核抢占。
根据以上描述,可知内核抢占的时机是:
schedule()