/*
* 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);
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_enter_lazy_cpu_mode();
/*
* 新进程没有mm结构(存储当前进程的虚拟内存),即内核态进程。
* 此时借用prev进程的mm结构,内核态进程只访问内核态地址空间,
* 而内核态空间是所有进程共享的,不会有问题。
*/
if (unlikely(!mm)) {
next->active_mm = oldmm; //借用mm结构
atomic_inc(&oldmm->mm_count); //增加引用计数
enter_lazy_tlb(oldmm, next);
} else
switch_mm(oldmm, mm, next); //新进程有mm结构,切换到新进程的虚拟内存空间。
if (unlikely(!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);
}
具体的切换动作由switch_to()完成,然而在此之前,已经完成了虚拟内存的切换。但是在进程切换过程中,处于内核态,访问的都是内核地址空间,而内核地址空间是由所有进程共享的,因此不会出现问题。
switch_to()位于include/asm-x86/system_32.h中,是一段汇编代码,如下:
#define switch_to(prev,next,last) do { \
unsigned long esi,edi; \
asm volatile("pushfl\n\t" /* Save flags */ \
"pushl %%ebp\n\t" \
"movl %%esp,%0\n\t" /* save ESP */ \
"movl %5,%%esp\n\t" /* restore ESP */ \
"movl $1f,%1\n\t" /* save EIP */ \
"pushl %6\n\t" /* restore EIP */ \
"jmp __switch_to\n" \
"1:\t" \
"popl %%ebp\n\t" \
"popfl" \
:"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
"=a" (last),"=S" (esi),"=D" (edi) \
:"m" (next->thread.esp),"m" (next->thread.eip), \
"2" (prev), "d" (next)); \
} while (0)
它实际上是一个宏定义,主要完成保存prev进程状态,切换到next进程的操作。
首先将prev进程的EFLAGS和EBP寄存器压入栈中,然后把ESP保存在prev->thread.esp中,以保存prev进程的状态。
随后执行movl next.thread->esp, %esp,切换内核栈。此时,内核已经在next进程的内核栈中执行。
接下来,movl $1f, prev->thread.eip,将标号1的地址存储在prev进程的eip中,到下一次进程切换到prev时,就会从标号1处开始执行,可以看到标号1处就是将之前压入栈中的EFLAGS和EBP弹出栈,恢复环境。
之后pushl next->thread.eip,将next进程的运行地址压入栈中。注意接下来调用__switch_to函数时,使用jmp指令直接跳转,这样就不会将返回地址压栈。当__switch_to函数执行ret指令时,就会跳到之前压入的eip出执行,也就是说,当__switch_to函数返回时,cpu就正式执行next进程的代码了。
另外,切换进程只需要两个进程的信息,为什么switch_to需要3个参数呢?假设进程A切到B,则此刻A的内核栈上,调用context_switch()的两个参数prev和next分别指向A和B。如果B又切换到了C,最后由C切换到A。这时候切换到A时,prev应该指向C,但在A的内核栈上的prev却依然是A。在context_switch()函数的最后,还要使用prev这个参数,这样就导致了错误。于是就需要第三个参数last,在调用__switch_to返回时,eax指向prev,这时候就将其赋给last,那么在新进程的栈上就可以保存正确的值了。
switch_to的Intel风格汇编如下。注意switch_to是一个宏,不是函数。被调用时,prev和next是context_switch()函数的局部变量,利用ebp相对寻址,即ebp+prev_offset的形式。
mov eax, [ebp+prev_offset] #AT&T汇编风格中输入部分的"2" (prev),和"=a"(last)使用同一个寄存器eax
mov edx, [ebp+next_offset] #"d" (next)
push eflags #EFLAGS入栈
push ebp #EBP入栈
mov [ebp+prev_offset]->thread.esp, esp #保存esp
mov esp, [ebp+next_offset]->thread.esp #切换ESP,注意此时已经切换到了next进程的内核栈
mov [ebp+prev_offset]->thread.eip, $1 #将标号1位置的地址保存到prev进程中的eip,再次切换到prev进程时,会从标号1处开始继续执行
push [ebp+next_offset]->thread.eip #将next进程中保存的eip压入栈。如果next是之前被换出的,则此时的eip也是下面的标号1
jmp __switch_to #跳到__switch_to函数执行硬件上下文切换。
#__switch_to是一个fastcall类型的函数,两个参数分别保存在eax和edx中,即prev和next。
#使用jmp而非call进行跳转,ret时,会跳到当前栈顶内容指向的位置,也即刚才压入的next进程的eip
#__switch_to的返回值是prev,即函数ret后,eax指向prev
1:
pop ebp #__switch_to函数返回后,会跳到这里执行,此时将上次被切换出时保存的ebp恢复
pop eflags #恢复EFLAGS
mov [ebp+prev_offset], eax #"=a" (last),注意此时的ebp已经是next进程的ebp,此时eax保存的是切换之前的prev
__switch_to()主要完成硬件上下文的切换,在这里就不过多讨论了。__switch_to函数返回时,cpu就开始执行下一个进程的代码了。
整个切换过程中的函数调用关系如下:
context_switch() ---> switch_to() ---> __switch_to()
其中,context_switch()完成进程页表项的切换,switch_to()完成寄存器的切换,__switch_to()完成硬件上下文的切换。
参考资料:http://blog.csdn.net/xiaoxiaomuyu2010/article/details/11935393