Linux内核分析(八)Linux中的进程调度与进程切换
本文将包括以下内容:
1. Linux中进程调度的时机
2. Linux的进程调度函数schedule()处理过程分析
3. 进程上下文切换过程分析
一、Linux中进程调度的时机
进程调度函数schedule在Linux的源代码文件中有非常多的地方会调用,包括各种设备驱动程序(网络设备,文件系统,声卡等等)中,用cscope可以找到500+处调用。而我们今天将只关注内核部分,也就是kernel目录下的代码中调用schedule的地方。一共找到53处,如下面两截图所示:
至于在这些地方进行进程调度的原因,我用了一个取巧的办法就是去查看schedule函数的注释,发现注释写的还真是非常详细,对理解进程调度非常有帮助。
/*
* __schedule() is the main scheduler function.
* __schedule()函数是主要的进程调度函数
* The main means of driving the scheduler and thus entering this function are:
* 主要的意思是进程调度的驱动器,所以,在下面几种情况下会调用该函数
* 1. Explicit blocking: mutex, semaphore, waitqueue, etc.
* 1. 显式的阻塞,如被同步锁,信号量,等待队列等所阻塞的时候
* 2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return
* paths. For example, see arch/x86/entry_64.S.
* To drive preemption between tasks, the scheduler sets the flag in timer
* interrupt handler scheduler_tick().
* 2. TIF_NEED_RESCHED标记被中断处理程序和用户态返回处理的过程中被设置
* 为了在进程之间实现抢占优先调度,调度器在定时器中断处理函数scheduler_tick()函数中设置该标志
*
* 3. Wakeups don't really cause entry into schedule(). They add a
* task to the run-queue and that's it.
* 3. 唤醒一个进程的时候并不实际调用schedule()函数,而只是在运行队列中添加一条任务。
* Now, if the new task added to the run-queue preempts the current
* task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets
* called on the nearest possible occasion:
* 现在,如果新添加到运行队列中的任务要抢占当前的任务,唤醒函数会设置TIF_NEED_RESCHED标志,所以,调度器会在下一次被调用时运行这个进程。调度时机包括:
* - If the kernel is preemptible (CONFIG_PREEMPT=y):
* - 内核被配置成抢占式的
* - in syscall or exception context, at the next outmost
* preempt_enable(). (this might be as soon as the wake_up()'s
* spin_unlock()!)
* -
* - in IRQ context, return from interrupt-handler to
* preemptible context
*
* - If the kernel is not preemptible (CONFIG_PREEMPT is not set)
* then at the next:
* - 如果内核没有被配置成可抢占式的,则在下列情况下也会执行进程调度
* - cond_resched() call // cond_resched()被调用
* - explicit schedule() call // schedule函数被显式调用
* - return from syscall or exception to user-space // 从系统调用或异常处理中返回用户态
* - return from interrupt-handler to user-space // 从终端处理程序中返回用户态
*/
二、进程调度函数schedule()处理过程分析
schedule()函数的实现在core.c文件中,如下:
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk); // 提交IO请求用于防止死锁
__schedule(); // 主要的调度处理
}
__schedule()函数的实现和解释如下,关键处理的注释做了加粗并标注成了蓝色:
static void __sched __schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
need_resched:
preempt_disable();
cpu = smp_processor_id();
rq = cpu_rq(cpu); // 获取当前正在CPU上运行的进程信息
rcu_note_context_switch(cpu);
prev = rq->curr; // 将当前的进程保存为新的prev进程
schedule_debug(prev); // 调试进程调度函数的额外信息
if (sched_feat(HRTICK))
hrtick_clear(rq);
smp_mb__before_spinlock(); // 一些精细的特殊处理,防止死锁的
raw_spin_lock_irq(&rq->lock); // 在要操作的rq结构上加锁
switch_count = &prev->nivcsw;
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
if (unlikely(signal_pending_state(prev->state, prev))) {
prev->state = TASK_RUNNING;
} else {
deactivate_task(rq, prev, DEQUEUE_SLEEP);
prev->on_rq = 0; // 将当前进程挂起
if (prev->flags & PF_WQ_WORKER) {
struct task_struct *to_wakeup;
to_wakeup = wq_worker_sleeping(prev, cpu);
if (to_wakeup)
try_to_wake_up_local(to_wakeup);
}
}
switch_count = &prev->nvcsw;
}
if (task_on_rq_queued(prev) || rq->skip_clock_update < 0)
update_rq_clock(rq);
next = pick_next_task(rq, prev); // 调用具体的调度算法,从进程队列中取出下一个要运行的进程
clear_tsk_need_resched(prev);
clear_preempt_need_resched(); // 清除一些调度标志
rq->skip_clock_update = 0;
if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
++*switch_count;
context_switch(rq, prev, next); /* 执行进程切换 */
cpu = smp_processor_id();
rq = cpu_rq(cpu); /*重新获得当前正在运行的进程信息,因为我们已经切换到新进程上了*/
} else
raw_spin_unlock_irq(&rq->lock);
post_schedule(rq);
sched_preempt_enable_no_resched();
if (need_resched())
goto need_resched;
}
context_switch函数的实现如下,为了能更清楚的看到整体的结构,删掉了一些大段的注释,并对关键步骤做了加粗标注:
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);
mm = next->mm;
oldmm = prev->active_mm;
arch_start_context_switch(prev);
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;
rq->prev_mm = oldmm;
}
spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
context_tracking_task_switch(prev, next);
switch_to(prev, next, prev); // 具体处理过程见第三部分
barrier();
finish_task_switch(this_rq(), prev);
}
跟踪schedule函数执行过程的方法也非常简单,因为我们有那么多地方都会调用schedule函数,所以用之前的方法启动内核之后,只需要在函数schedule处设置一个断点,内核就会在下一次调用schedule函数的时候停在断点的位置:
三、上下文切换宏switch_to解析
上面进程切换的最关键部分swtich_to实现了不同进程的CPU寄存器内容的切换,是硬件相关的,我们找到32位X86平台的实现代码来分析。(整洁期间,删掉了源文件中的大段注释)
#define switch_to(prev, next, last) \
do { \
unsigned long ebx, ecx, edx, esi, edi; \
asm volatile("pushfl\n\t" /* save flags */ \ // 保存状态寄存器
"pushl %%ebp\n\t" /* save EBP */ \ // 保存栈底指针EBP到栈上
"movl %%esp,%[prev_sp]\n\t" /* save ESP */ \ // 把ESP保存到进程结构的sp字段中
"movl %[next_sp],%%esp\n\t" /* restore ESP */ \ // 将要调入的进程的ESP值设置给ESP寄存器
"movl $1f,%[prev_ip]\n\t" /* save EIP */ \ // 将标号1的代码地址保存到换出的进程结构的IP字段
"pushl %[next_ip]\n\t" /* restore EIP */ \ // 将要调入的进程曾经保存的IP值设置给EIP
__switch_canary \ // 64位X86上有些额外的事情做,32位X86该宏是空
"jmp __switch_to\n" /* regparm call */ \ // 跳到__switch_to函数,将正式跳入新进程去执行
"1:\t" \ // 这是某进程被换入时将开始执行的地方
"popl %%ebp\n\t" /* restore EBP */ \ // 恢复EBP
"popfl\n" /* restore flags */ \ // 恢复状态寄存器,随后CPU将继续执行上次调用
// schedule函数的下面的代码,也就是上次被挂起的进程继续执行
/* output parameters */ \
: [prev_sp] "=m" (prev->thread.sp), \
[prev_ip] "=m" (prev->thread.ip), \
"=a" (last), \
\
/* clobbered output registers: */ \
"=b" (ebx), "=c" (ecx), "=d" (edx), \
"=S" (esi), "=D" (edi) \
\
__switch_canary_oparam \ // , [stack_canary] "=m" (stack_canary.canary)
\
/* input parameters: */ \
: [next_sp] "m" (next->thread.sp), \
[next_ip] "m" (next->thread.ip),
/* regparm parameters for __switch_to(): */ \
[prev] "a" (prev), \
[next] "d" (next) \
\
__switch_canary_iparam \ //, [task_canary] "i" (offsetof(struct task_struct, stack_canary))
\
: /* reloaded segment registers */ \
"memory"); \ // 上面都是嵌入式汇编用到的变量
} while (0)