第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()。

猜你喜欢

转载自blog.csdn.net/xiezhi123456/article/details/81128213