上下文切换

进程调度是linux系统中再常见不过的事情,对于进程的调度,这里不管什么调度策略什么的,直接来看看进程的切换做了些什么事情。

这里从内核的上下文切换函数context_switch开始分析。

static inline  

task_t * context_switch(runqueue_t * rqtask_t *prevtask_t *next 

 

struct mm_struct *mm next->mm 

struct mm_struct *oldmm prev->active_mm 

  

if (unlikely(!mm)) {  /*切换进来的进程是内核线程*/

next->active_mm = oldmm  /*借用前一个进程的进程空间*/

atomic_inc(&oldmm->mm_count);  

enter_lazy_tlb(oldmmnext);  /*惰性TLB*/

else  

switch_mm(oldmmmmnext);  /*切换运行空间*/

  

if (unlikely(!prev->mm)) { /*前一个进程是内核线程*/ 

prev->active_mm = NULL  /*断开与借用的地址空间的联系*/

WARN_ON(rq ->prev_mm);  

rq ->prev_mm = oldmm 

 

  

/* Here we just switch the register state and the stack. */  

switch_to(prevnextprev);  /*切换执行环境*/

  

return prev 

}


进程切换最主要的事情是进程执行环境的切换和运行空间的切换。

先来看看task_struct结构体的两个重要字段:mm和active_mm。mm是表示进程所拥有的内存的描述符,active_mm是表示进程运行时所使用的内存的描述符。内核线程和用户进程都是task_struct的实例,区别在于内核线程是没有进程地址空间的。

mm

active_mm

内核线程

NULL

指向上一个被调用进程的active_mm值

用户进程

两者相等


context_switch函数首先判断切换进来的进程next是内核线程还是用户进程。如果next是内核线程,那么它借用上一个进程的地址空间;由于内核线程不访问用户态地址空间,没有必要使一个用户态线性地址对应的TLB表项无效,因此调用enter_lazy_tlb通知底层体系结构不需要切换虚拟地址空间的用户空间部分。

如果next是用户进程,那么就调用switch_mm函数切换运行空间。下面分析下次函数代码实现。

static inline void switch_mm(struct mm_struct *prev struct mm_struct *next 

struct task_struct *tsk 

 

if (likely(prev != next))  

cpu_clear(cpuprev->cpu_vm_mask); /*清除cpu_vm_mask,表示prev已经弃用了当前cpu*/

#ifdef CONFIG_SMP  

per_cpu(cpu_tlbstatecpu).state TLBSTATE_OK 

per_cpu(cpu_tlbstatecpu).active_mm next 

#endif  

cpu_set(cpunext->cpu_vm_mask); /*设置nextcpu_vm_mask标志,表示next占用了当前cpu*/  

/*将进程next的页目录表首地址next->pgd转换成物理地址,并将其写入寄存器cr3*/ 

load_cr3(next->pgd); 

  

if (unlikely(prev->context.ldt != next->context.ldt))  

load_LDT_nolock(&next->contextcpu);  

 

#ifdef CONFIG_SMP  

else  

per_cpu(cpu_tlbstatecpu).state TLBSTATE_OK 

BUG_ON(per_cpu(cpu_tlbstatecpu).active_mm != next);  

        /*判断next进程在切换出去之前是否运行在当前cpu*/

if (!cpu_test_and_set(cpunext->cpu_vm_mask))    

load_cr3(next->pgd);  

load_LDT_nolock(&next->contextcpu);  

 

 

#endif  

}  


从上面代码可以看到switch_mm最主要的工作就是切换pgd,我们知道pgd是包括内核空间和用户空间的地址映射的,但是因为所有进程的内核空间的地址映射是相同的,所以只需要进行用户空间的切换。

这里切换了页表,即地址空间,不会立马引起控制流程的改变。现在处于内核空间,而所有进程的内核空间的地址映射是一样的。但是当从内核空间返回用户空间时,这里的改变就会引起本质的改变,会跳到next的用户代码中执行。

SMP系统中,即使prevnext是同一个进程,但是如果切换前后运行在不同的CPU上,这里还是需要重新加载CR3的。


接下来分析switch_to宏,这个宏比较难理解。下面是x86架构下面的代码分析。

#define switch_to(prev,next,lastdo {  \  

unsigned long esi,edi;  \  

asm volatile("pushfl\n\t"   \  /*prev内核栈上保存prev进程的efalgs*/

"pushl %%ebp\n\t"  \        /*prev内核栈上保存prev进程的ebp*/

"movl %%esp,%0\n\t" /* save ESP */ \  /*esp寄存器的值装入prev进程的thread.esp*/

"movl %5,%%esp\n\t"   /* restore ESP */  \  /*next进程的thread.esp装入esp寄存器*/

"movl $1f,%1\n\t"  /* save EIP */  \         /*prev进程恢复执行的指令设置为标号1位置处*/

"pushl %6\n\t"   /* restore EIP */ \       /*next进程恢复执行的指令位置压入next的内核栈*/

"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 

上面是x86架构下的switch_to宏的代码,是用内嵌汇编实现的。switch_to函数有5个输出参数,4个输入参数,第一个输出参数开始,参数以0开始编号,依次增加。

我们从注释就可以看到switch_to主要干的事情就是切换内核栈和寄存器。此宏开始4条命令就是切换内核堆栈。它首先保存prev进程的eflagsebp寄存器压入prev进程的内核栈,然后将esp的值装入prev进程的thread.esp字段,最后就是将next进程的thread.esp字段装入esp寄存器中,这样就开始使用next进程的内核栈,即达到了切换内核栈的目的了。

调用switch_to宏之前


切换内核栈之前

上下文切换_第1张图片

切换内核栈之后


接下来的3条指令就是切换内核的控制流程。首先将prev进程恢复执行的指令位置设置为标号1位置处,然后就是将next进程恢复执行的指令位置压入next的内核栈。接下来就是通过jmp跳转到__switch_to函数执行,此函数执行完之后不会返回到下面的标号1处,而是到next进程的thread.ip处开始执行。

上下文切换_第2张图片

__switch_to返回之前

上下文切换_第3张图片

__switch_to返回之后

calljmp指令的差别:

call指令在执行前将其后紧跟着的一条指令的地址压入堆栈,然后jmpcall调用的地址去执行,遇到ret时弹出堆栈中的指令地址,继续执行。


试想一下,如果next进程不是新创建的,那么它上一次也是通过switch_to宏调度出去的,它的恢复执行的位置必定也是在标号1处,此时next进程的内核栈上还保存有eflagsebp寄存器。紧跟的两条pop命令就会弹出寄存器。

上下文切换_第4张图片

switch_to宏完成

上面就是switch_to宏的做的事情,下面分析下switch_to为什么有三个参数,prevnext两个参数不行吗?

switch_to宏的第三个参数last是输出参数,而且是使用寄存器eax的值。__switch_to函数是有两个参数和一个返回值的,x86架构下默认的是堆栈传参的,但我们在前面的内嵌汇编代码中,没有看到在调用__switch_to之前有对参数入栈的操作?那有可能是通过寄存器传参的,查找arch/i386/Makefile文件,有下面的代码:


-mregparm=3表示使用3个寄存器传参,这证明了__switch_to是通过寄存器传参的,返回值是通过eax寄存器传递的。因此__switch_to函数的返回值就传递给了last,即作为switch_to宏的输出参数传递给力context_swicth函数。

分析__switch_to函数就知道返回值lastprey进程。此prev就需要好好分析下了。


上图为进程A的一个执行流示意图。调用switch_to宏切换到进程B,在switch函数返回之后,即进程A又切换回来执行的情况下,可能已经发生了多次进程的切换,就像图中所示的A切换到BB切换到CC切换到DD最终切换到A。这个时候,switch_to(prev,next,last)中的第三个参数last,就是指向的进程D

这样我们就能够对进程A重新回来执行之前被切换出去的进程D进行清理的工作。



你可能感兴趣的:(linux,内核)