Linux切换并没有使用X86CPU的切换方法,Linux切换的实质就是cr3切换(内存空间切换,在switch_mm函数中)+ 寄存器切换(包括EIP,ESP等,均在switch_to函数中)。这里我们讲述下switch_to主流程:
- 在switch_mm函数中将new_task->pgd设置到cr3寄存器中,实现页表切换,由于每个进程
3-4G的页表映射机制完全一样(从内核页表中直接复制过来的),故这里虽然切换了pgd,但是并无影响,只是在任务回到用户空
间中时,才会发生变化,因为每个任务在0-3G中的页表映射都是各自独立的;[注意]因为cr3指向的是"1st level"页表, 次级页表(2dn level)本身的address是固定不变的(内容可变),所以,内核空间所对应的一级页表内容是不变的. 在copy_mm()->dup_mm()时进行了copy.
- 压入esi edi ebp到cur_task堆栈中;
- 将esp寄存器中的值保存到cur_task.task_struct.thread.esp中,也就是将cur_task切换时的堆栈指针保存起来;
- 将
new_task.task_struct.thread.esp中的值设置到esp寄存器中,这里的
new_task.task_struct.thread.esp中的值就是new_task上一次被换出时的堆栈指针,现在被恢复了,2和3结合实现了
从cur_task到new_task的堆栈切换;
- 将1f地址设置到cur_task.task_struct.thread.eip中,当下次cur_task恢复运行时,将会从1f处开始运行,下面阐述了这种原理;
- 将
new_task.task_struct.thread.eip压入到new_task的堆栈中,这里
new_task.task_struct.thread.eip的值就是1f,因为从4中可知,new_task上一次被换出时,其也是和现在的
cur_task类似,1f地址被设置到new_task.task_struct.thread.eip中;
- 随后CPU跳转到
__switch_to函数(注意,在__switch_to中,做了一件非常重要的事情就是让
init_tss.esp0=new_task.task_struct.esp0,原因见【C.Linux进程:Linux进程管理和X86进程管理的
结合】一文)中开始执行,注意这里使用的是jmp,不是call,call会pusheip,而jmp不会,由于__switch_to是函数,当CPU
执行完该函数后,最后一条指令必然为iret,该指令会popeip,从5中可以知道,此时new_task堆栈中的镜像为[.......,
esi,edi,ebp,eip(&1f)],故popeip将值eip(&1f)设置到eip寄存器中,这样当iret执行完毕
后,CPU将从eip处继续执行,也就是从1f处继续执行;;
- 此时已经在new_task的执行环境中了,pop ebp, pop
edi,
popesi,回到schedule函数中,当返回用户空间中时,由于new_task用户空间的eip,ss,esp等均被从new_task的堆栈中
弹出到对应寄存器中,从而new_task得以顺利执行;
switch_to中cur_task寄存器保存、new_task寄存器恢复的几个问题:
- GCC在编译该段代码时,会注意到EAX,EBX,EDX在这里被使用,因此此处无需要显示的用汇编语句来保存这三个寄存器,GCC在编译时会自动考虑添加对这些寄存器的保护;
- 由于在内核中所有的内核段寄存器均为统一的,因此这里无需保存ES,CS,SS,DS,FS,GS;
- CR3(Linux中没有使用LDT)已经在前面的switch_mm处理了;
- 由于Linux没使用TSS-previous task link field,其切换完全采用软件处理切换,故这里无需考虑TSS-previous task link field;
- 指令EIP会保存在task.thread.eip中,ESP会保存在task.thread.esp中,EBP,ESI,EDI会用显示指令入栈保存;
- 对于Linux而言,其仅仅使用到了CPU的4级机制中的0和3两级,使用方法如下:
当进程正运行在用户空间时,如果此时来了个中断,CPU将会执行:从TR->GDTR[i
]->TSS中取出当前的SS0:ESP0,从IDTR->IDT[i
]中取出执行代码CS:IP,将当前所有寄存器压到SS0:ESP0堆栈中,包括进程的SS3:ESP3,随后从CS:IP处开始执行代码。当中断代码执
行完毕后,内核将会从进程堆栈中,将SS3:ESP3、CS:IP弹出,从而回到用户空间重新开始执行,此时并不需要CPU主动来切换级别了;从这里可
知,CPU是需要TSS中的SS0和ESP0来进行高->低级别切换的,因此进程在切换时,必须要将自己的SS0和ESP0保存到
TR->GDTR[i
]->TSS的SS0和ESP0字段中去,其实,在Linux中,对于同一个CPU,所有的进程都使用一个TSS,只是在进程切换时,
被切换到的进程将会把自己的ESP0保存到TSS.ESP0中去(在函数__switch_to中),那为什么不把自己的SS0也保存到TSS.SS0中
呢,这是因为所有进程的SS0都是统一的,为内核的SS,而内核在初始化的时候,已经将该TSS.SS0设置为自己的SS,因此无需继续设置SS0; - 至于EFLAGS为什么没有保存,这点在2.6中已经纠正,即执行了pushf和popf;
- ECX为什么没有保存,则涉及到了如下的理由:
Allregisters on the Intel386 are global and thus visible to both a
callingand a called function. Registers %ebp, %ebx, %edi,%esi, and
%esp'belong' to the calling function. In other words, a called
functionmust preserve these registers' values for its caller.
Remainingregisters 'belong' to the called function. If a calling
function wantsto preserve such a register value across a function call,
it must savethe value in its local stack frame.Some registers have
assigned rolesin the standard calling sequence:
- %esp:The
stack pointerholds the limit of the current stack frame, which is the
address of thestack’s bottom-most, valid word. At all times, the stack
pointer shouldpoint to a word-aligned area.
- ......
- %ecx
and%edx:Scratch registers have no specified role in the standard
callingsequence. Functions do not have to preserve their values for the
caller.
- ......
Linux进程管理和X86进程管理的结合
- 从上面描述中,我们知道X86要求每个进程都必须有自己的TSS,在每次进程切换的时候,通过对应的TR[i ]+GDTRbase找到该进程的TSS,然后保存前一个任务的所有寄存器,同时将找到的TSS中所有的寄存器值恢复到系统对应的寄存器中,从而实现进程切换。
- 由于Linux实现进程切换的
时候,并不采用该机制,但是却避不过X86这个机制,因此Linux内核设置了init_tss全局变量,在
start_kernel->trap_init->cpu_init初始化时,设置了对应的GDT[i
]指向该init_tss以及TR.index=i;此后,在Linux系统运行过程中,就再也不会改变TR以及该GDT[i
],对应X86来讲,就好像永远运行着1个进程;Linux使用自身的切换机制来实现了进程的切换,这些在上面的文章中已经说明,这里不在多说。
- 另
一重要问题Linux必须面对:当从高级别切换到低级别时,会引起CPU运行级别的变化,例如从级别3到级别0,此时CPU需要获取0级别的SS0以及
ESP0(例如前面描述的当进程正运行在用户空间时来了个中断范例)来恢复SS:ESP,其设计方法就是从当前的TSS->SS0:ESP0中获
取。为了适应CPU的这种设计,Linux内核在每次switch_to切换进程时,都将被切换来的进程的ESP0保存到init_tss.esp0中;
另外由于Linux内核的SS0始终为KERNEL_SS保持不变,故无需每次切换都将其保存到init_tss.ss0中,只需要在Linux内核初始
化时将init_tss.ss0设置为KERNEL_SS就可以了;
阅读(1250) | 评论(0) | 转发(0) |