Linux 进程管理之进程调度与切换( 三 )

进程上下文切换包括进程的地址空间的切换和执行环境的切换 。

  • switch_mm 完成了进程的地址空间的切换:如果新进程有自己的用户空间,也就是说,如果 next->mm 与 next->active_mm 相同,那么,switch_mm() 函数就把该进程从内核空间切换到用户空间,也就是加载next 的页目录 。如果新进程无用户空间(next->mm 为空),也就是说,如果它是一个内核线程,那它就要在内核空间运行,因此,需要借用前一个进程(prev)的地址空间,因为所有进程的内核空间都是共享的,因此,这种借用是有效的 。
  • switch_to 完成了执行环境的切换,该宏实现了进程之间的真正切换 。
  •  
//进程切换包括进程的执行环境切换和运行空间的切换 。运行空间的切换是有switch_mm完成的static inline void switch_mm(struct mm_struct *prev,struct mm_struct *next,struct task_struct tsk){//得到当前进程运行的cpuint cpu = smp_processor_id();//若要切换的prev != next,执行切换过程if (likely(prev != next)) {/ stop flush ipis for the previous mm *///清除prev的cpu_vm_mask,标志prev已经弃用了当前cpucpu_clear(cpu, prev->cpu_vm_mask);#ifdef CONFIG_SMP//在smp系统中,更新cpu_tlbstateper_cpu(cpu_tlbstate, cpu).state = TLBSTATE_OK;per_cpu(cpu_tlbstate, cpu).active_mm = next;#endif//设置cpu_vm_mask,表示next占用的当前的cpucpu_set(cpu, next->cpu_vm_mask);/* Re-load page tables *///加载CR3load_cr3(next->pgd);/*• load the LDT, if the LDT is different:*///若ldt不相同,还要加载ldtif (unlikely(prev->context.ldt != next->context.ldt))load_LDT_nolock(&next->context);}#ifdef CONFIG_SMPelse {per_cpu(cpu_tlbstate, cpu).state = TLBSTATE_OK;//prev == next 那当前cpu中的active_mm就是prev,也即是nextBUG_ON(per_cpu(cpu_tlbstate, cpu).active_mm != next);/*在smp系统中,虽然mm时一样的,但需要加载CR3执行cpu_test_and_set来判断next是否正运行在此cpu上,这里是判断在切换next是否运行在当前的cpu中,假设cpu为1,一个进程在1上执行时候,被调度出来,再次调度的时候,又发生在cpu1上*/if (!cpu_test_and_set(cpu, next->cpu_vm_mask)) {/* We were in lazy tlb mode and leave_mm disabled• tlb flush IPI delivery. We must reload %cr3.*/load_cr3(next->pgd);load_LDT_nolock(&next->context);}}#endif}对于 switch_mm 处理,关键的一步就是它将新进程页面目录的起始物理地址装入到寄存器 CR3 中 。CR3 寄存器总是指向当前进程的页面目录 。
#define switch_to(prev,next,last) do { unsigned long esi,edi; /*分别保存了eflags、ebp、esp,flags和 ebp他们是被保存在prev进程(其实就是要被切换出去的进程)的内核堆栈中的*/asm volatile("pushflnt" /* Save flags */ "pushl %%ebpnt" //%0为 prev->thread.esp,%1 为 prev->thread.eip"movl %%esp,%0nt" /* save ESP */ //%5为 next->thread.esp,%6 为 next->thread.eip"movl %5,%%espnt" /* restore ESP */ /*将标号为1所在的地址,也即是"popl %%ebpnt" 指令所在的地址保存在prev->thread.eip中,作为prev进程下一次被调度运行而切入时的返回地址,因此可以知道,每个进程调离时都要执行"movl $1f,%1nt",所以这就决定了每个进程在受到调度恢复运行时都会从标号1处也即是"popl %%ebpnt"开始执行 。但是有一个例外,那就是新创建的进程,新创建的进程并没有在上一次调离时执行上面的指令,所以一来要将其task_struct中的thread.eip事先设置好,二来所设置的返回地址也未必是这里的标号1所在的地址,这取决于系统空间堆栈的设置 。事实上可以下fork()中可以看到,这个地址在copy_thread()中设置为ret_from_fork,其代码在entry.S中,也就是对于新创建的进程,在调用schedule_tail()后直接转到了ret_from_sys_call, 也即是返回到了用户空间去了*/"movl $1f,%1nt" /* save EIP */ /*将next->thread.eip压入栈中,这里的next->thread.eip正是next进程上一次被调离在"movl $1f,%1nt"保存的,也即是指向标号为1的地方,也即是"popl %%ebpnt"指令所在的地址*/"pushl %6nt" /* restore EIP */ /*需要注意的是 __switch_to 是经过regparm(3)来修饰的,这个是gcc的一个扩展语法 。即从eax,ebx,ecx寄存器取函数的参数 。这样,__switch_to函数的参数就是从寄存器中取的 。并是不向普通函数那样从堆栈中取的 。在__switch_to之前,将next->thread.eip压栈了,这样从函数返回后,它的下一条运行指令就是 next->thread.eip了 。对于新创建的进程 。我们设置了p->thread.eip = ret_from_fork.这样子进程被切换进来之后,就会通过ret_from_fork返回到用户空间了 。对于已经运行的进程,我们这里可以看到,在进程被切换出去的时候,prev->thread.eip被设置了标号1的地址,即是从标号1的地址开始运行的 。标号1的操作:恢复ebp(popl %%ebp)恢复flags(popf1)这样就恢复了进程的执行环境 。从代码可以看到,在进程切换时,只保留了flags esp和ebp寄存器,显示的用到了eax和edx,那其他寄存器怎么保存的呢?实际上过程切换只是发生在内核态,对于内核态的寄存器来说,它的段寄存器都是一样的,所以不需要保存*//*通过jmp指令转入到函数__switch_to中,由于上一行"pushl %6nt"把next->thread.eip,也即是标号为1的地址压到栈中,所以跳入__switch_to执行完后,执行标记为1的地方,也即是"popl %%ebpnt" 指令*/"jmp __switch_ton" "1:t" "popl %%ebpnt" "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)


推荐阅读