Linux内核调度

原文地址:https://wanqbin.xyz/2019/08/08/Linux内核调度/

一、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_reschedpreempt_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()
  • 内核的任务阻塞

你可能感兴趣的:(Linux内核)