第4章进程调度(五)
4.6 抢占和上下文切换
上下文切换,是从一个可执行进程切换到另一个可执行进程,定义在kernel/sched.c中context_switch()函数负责处理。
/*
* context_switch - switch to the new MM and the new
* thread's register state.
*/
static inline void
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
trace_sched_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev);
if (likely(!mm)) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next);
} else
switch_mm(oldmm, mm, next);
if (likely(!prev->mm)) {
prev->active_mm = NULL;
rq->prev_mm = oldmm;
}
/*
* Since the runqueue lock will be released by the next
* task (which is an invalid locking op but in the case
* of the scheduler it's an obvious special-case), so we
* do an early lockdep release here:
*/
#ifndef __ARCH_WANT_UNLOCKED_CTXSW
spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
#endif
/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);
barrier();
/*
* this_rq must be evaluated again because prev may have moved
* CPUs since it called schedule(), thus the 'rq' on its stack
* frame will be invalid.
*/
finish_task_switch(this_rq(), prev);
}
当一个新的进程被选出来准备投入运行时,schedule()就会调用该函数。主要完成两项基本工作:
调用声明在asm/mmu_context.h中的switch_mm(),该函数负责把虚拟内存从上一个进程映射切换到新进程中。
调用声明在asm/system.h中的switch_to,该函数负责从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息,还有其他任何与体系结构相关的状态信息,都必须以每个进程为对象进行管理和保存。
内核知道何时调用schedule()。如果仅靠用户程序代码显式地调用schedule(),它们可能会永远地执行下去。相反,内核提供一个need_resched标志来表明是否需要重新执行一次调度。当某个进程应该被抢占时,scheduler_tick()就会设置这个标志;当一个优先级高的进程进入可执行状态时,try_to_wake_up()也会设置这个标志,内核检查该标志,确认其被设置,调用schedule()来切换到一个新的进程。该标志对于内核来讲是一个信息,它表示有其他进程应当被运行了,要尽快调用调度程序。
再返回用户空间以及从中断返回时,内核也会检查need_resched标志。如果已被设置,内核会在继续执行之前调用调度程序。
每个进程都包含一个need_resched标志,这是因为访问进程描述符内的数值要比访问一个全局变量快。
1、用户抢占
内核即将返回用户空间时,如果need_resched标志被设置,会导致schedule()函数被调用,此时会发生用户抢占。在内核返回用户空间时,它知道自己是安全的,因为既然它可以继续去执行当前进程,那么它当然可以再去选择一个新的进程去执行。所以,内核无论是在中断处理程序还是在系统调用后返回,都会检查need_resched标志。如果它被设置了,那么,内核会选择一个其他进程投入运行。从中断处理程序或系统调用返回的返回路径都是跟体系结构相关的,在entry.S 文件中通过汇编语言来实现的。
用户抢占在以下情况是发生:
从系统调用返回用户空间时。
从中断处理程序返回用户空间时。
2、内核抢占
Linux支持内核抢占。在不支持内核抢占的内核中,内核代码可以一直执行,到完成为止。调度程序没有办法在一个内核级的任务正在执行时重新调度——内核中的各任务是以协作方式调度的,不具备抢占性。内核代码一直要执行到完成(返回用户空间)或明显的阻塞为止。在2.6的内核中,内核引入抢占能力;现在,只要重新调度是安全的,内核就可以在任何时间抢占正在执行的任务。
那么,何时重新调度是安全的?只要没有持有锁,内核就可以进行抢占。锁是非抢占区域的标志。由于内核支持SMP的,所以,如果没有持有锁,正在执行的代码就是可以抢占的。
为了支持内核抢占所做的第一处变动,就是为每个进程的thread_info引入preempt_count计数器。
arch/arm/include/asm/thread_info.h
/*
* low level task data that entry.S needs immediate access to.
* __switch_to() assumes cpu_context follows immediately after cpu_domain.
*/
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
struct cpu_context_save cpu_context; /* cpu context */
__u32 syscall; /* syscall number */
__u8 used_cp[16]; /* thread used copro */
unsigned long tp_value;
struct crunch_state crunchstate;
union fp_state fpstate __attribute__((aligned(8)));
union vfp_state vfpstate;
#ifdef CONFIG_ARM_THUMBEE
unsigned long thumbee_state; /* ThumbEE Handler Base register */
#endif
struct restart_block restart_block;
};
preempt_count的初始值为0,每当使用锁时数值加1,释放锁时数值减1。当数值为0时,内核就可以抢占。从中断返回用户空间时,内核会检查need_resched和preempt_count的值。如果need_resched被设置,并且preempt_count为0,这说明有一个更为重要的任务需要执行并且可以安全地抢占,此时,调度程序就会被调用。如果preempt_count不为0,说明当前任务持有锁,所以抢占是不安全的。这时,内核直接从中断返回当前执行进程。如果当前进程持有的所有锁都被释放了,preempt_count会重新为0。此时,释放锁的代码会检查need_resched是否被设置。如果是,会调用调度程序。
如果内核中的进程被阻塞了,或它显式地调用了schedule(),内核抢占也会显式地发生。这种形式的内核抢占从来都是受支持的,因为根本无须额外的逻辑来保证内核可以安全地被抢占。如果代码显式地调用了schedule(),那么可以安全地被抢占的。
内核抢占会发生在:
中断处理程序正在执行,且返回内核空间之前。
内核代码再一次具有可抢占性的时候。
如果内核中的任务显式地调用schedule()。
如果内核中的任务阻塞,这同样也会导致调用schedule()。