一、上下文切换
- 上下文切换:也就是从一个可执行进程切换到另一个可执行进程
context_switch()函数
- 由context_switch()函数负责处理。每当一个新的进程被选出来准备投入运行的时候,schedule()就会调用该函数
- 它完成了两项基本的工作:
- 1.调用声明在<asm/mmu_context.h>中的switch_mm()。该函数负责把虚拟内存从上一个进程映射切换到新进程中
- 2.调用声明在<asm/system.h>中的switch_to()。该函数负责从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息,还有其他任何与体系结构相关的状态信息,都必须以每个进程为对象进行管理和保存
- 以下代码来自于 Linux 2.6.22/kernel/schedule.c中
/*
* context_switch - switch to the new MM and the new
* thread's register state.
*/
static inline struct task_struct *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm = next->mm;
struct mm_struct *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_enter_lazy_cpu_mode();
if (!mm) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next);
} else
switch_mm(oldmm, mm, next);
if (!prev->mm) {
prev->active_mm = NULL;
WARN_ON(rq->prev_mm);
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);
return prev;
}
need_resched标志
- 内核必须知道在什么时候调用schedule()。如果仅靠用户程序代码显式地调用schedule(),它们可能就会永远地执行下去。相反,内核提供了一个need_resched标标来表明是否需要重新执行—次调度
- 何时被设置:
- 内核检査该标志,确认其被设置,调用schedule()来切换到一个新的进程。该标志对于内核来讲是一个信息,它表示有其他进程应当被运行了,要尽快调用调度程序
- 用于访问和操作need_resched的函数:
- 再返回用户空间以及从中断返回的时候,内核也会检査need_resched标志。如果已被设置, 内核会在继续执行之前调用调度程序
- need_resched标志的保存:
- 每个进程都包含一个need_resched标志,这是因为访问进程描述符内的数值要比访问一个全局变量快(因为current宏速度很快并且描述符通常都在高速缓存中)
- 在2.2以前的内核版本中,该标志是一个全局变量
- 2.2到2.4版内核中它在task_struct中
- 而在2.6版本中,它被移到thread_info结构体中,用一个特别的标志变量中的一位来表示
二、用户抢占
- 内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。在内核返回用户空间的时候,它知道自己是安全的,因为既然它可以继续去执行当前进程,那么它当然可以再去选择一个新的进程去执行
- 所以,内核无论是在中断处理程序还是在系统调用后返回,都会检査need_resched标志。如果它被设置了,那么,内核会选择一个其他(更合适的)进程投入运行
- 从中断处理程序或系统调用返回的返回路径都是跟体系结构相关的,在entry.S(此文件不仅包含内核入口部分的程序,内核退出部分的相关代码也在其中)文件中通过汇编语言来实现
- 简而言之,用户抢占在以下情况时产生:
- 从系统调用返回用户空间时
- 从中断处理程序返回用户空间时
三、内核抢占
Linux支持内核抢占
- 与其他大部分的Unix变体和其他大部分的操作系统不同,Linux完整地支持内核抢占
- 在不支持内核抢占的内核中,内核代码可以一直执行,到它完成为止。也就是说,调度程序没有办法在一个内核级的任务正在执行的时候重新调度——内核中的各任务是以协作方式调度的,不具备抢占性。内核代码一直要执行到完成(返回用户空间)或明显的阻塞为止
- 在2.6版的内核 中,内核引入了抢占能力;现在,只要重新调度是安全的,内核就可以在任何时候抢占正在执行的任务
- 有些内核代码需要允许或禁止内核抢占,相关内容会在后面内核同步的文章中介绍
什么时候重新调度是安全的?
- 只要没有持有锁,内核就可以进行抢占。锁是非抢占区域的标志。由于内核是支持SMP的,所以,如果没有持有锁,正在执行的代码就是可重新导入的,也就是可以抢占的
preempt_count计数器
- 为了支持内核抢占所做的第一处变动,就是为每个进程的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是否被设置。如果是的话,就会调用调度程序
显式地内核抢占
- 如果内核中的进程被阻塞了,或它显式地调用了schedule(),内核抢占也会显式地发生。这 种形式的内核抢占从来都是受支持的,因为根本无须额外的逻辑来保证内核可以安全地被抢占。 如果代码显式地调用了schedule(),那么它应该清楚自己是可以安全地被抢占的
- 内核抢占会发生在:
- 中断处理程序正在执行,且返回内核空间之前
- 内核代码再一次具有可抢占性的时候
- 如果内核中的任务显式地调用schedule()
- 如果内核中的任务阻塞(这同样也会导致调用schedule())