先用自己话非常通俗的概括一下整个切换的流程。故事开始的时候一个进程(or线程,在这里不加以区分,简单的认为二者的区别只是在是否产生内存映射表的切换,其他方面相同)A正在愉快的运行着,CPU通过取指执行的方式一点点的去执行它的代码,突然遇到一个中断(系统调用、外部中断、异常),然后就要进入到A进程的内核态去执行相应的中断服务例程,在这期间突然不小心发生一个意外,程序的执行由于缺乏某中资源而不得不中止,而调用了schedule()函数,即产生了进程的切换。而这块知识的重点并非是schedule()函数如何去选择下一个要被执行的进程,我们跳过这个黑箱,假设现在已经取得下一个进程的PCB和PID,那么剩下的重点就落在了switch_to()这个函数的身上。在切换到了新的进程B的内核栈之后,再利用B的内核栈中保存的用户栈的信息跳转到用户态去执行程序,从而完成了进程的切换。
课程介绍了两种切换(switch_to)的方法,概括的说一种是基于TSS的切换,一种是基于内核栈的切换。
(一)基于TSS的进(线)程切换
首先介绍TSS的概念,Task State Segment,即任务状态段。它记录了cpu执行某个线程的上下文,就是比如这个线程开始执行了5秒钟后cpu中各个寄存器的内容,概括的说是记录从cpu来看整个进程执行到了一个什么样的地步,可以形象的理解为TSS就是个相机,将cpu某一时刻的状态给“咔嚓”地拍了下来。然后由于TSS的本质是一个段,所以关于他的寻找遵从保护模式下的段机制(详情出门左转http://www.cnblogs.com/immortal-worm/p/5867418.html),由cpu中的tr寄存器记录当前进程tss所在的位置,然后统一去GDT里面找。
Intel架构不仅提供了TSS来实现任务切换,而且只要一条指令就能完成这样的切换,即图中的ljmp指令。具体的工作过程是:
(1)通过tr寄存器在GDT中找到当前进程的TSS表,然后将当前cpu的状态记录到找到的TSS表中。
(2)通过ljmp指令的参数找到下一个要切换进程的TSS表的位置,并将其内容“扣”到cpu中。
(3)将cpu中的tr寄存器置为当前的TSS位置
(4)由于当前cpu中已经存放了新进程的cs和ip寄存器,所以直接开始执行就OK
而基于TSS的进程切换要耗费约200个时钟周期,时间较长, 因此又有了基于内核栈的进程切换。
(二)基于内核栈的进(线)程切换
首先是schedule()函数里传给switch_to()参数的变动
if ((*p)->state == TASK\_RUNNING && (*p)->counter > c) c = (*p)->counter, next = i, pnext = *p; ....... switch_to(pnext, LDT(next));
不难看出ebp+8也就是pnext参数
switch_to: pushl %ebp movl %esp,%ebp pushl %ecx pushl %ebx pushl %eax movl 8(%ebp),%ebx cmpl %ebx,current je 1f 切换PCB TSS中的内核栈指针的重写 切换内核栈 切换LDT movl $0x17,%ecx mov %cx,%fs cmpl %eax,last_task_used_math //和后面的clts配合来处理协处理器,由于和主题关系不大,此处不做论述 jne 1f clts 1: popl %eax popl %ebx popl %ecx popl %ebp ret
五个步骤:PCB的切换->TSS内核栈指针的重写->内核栈的切换->LDT的切换->PC指针的切换
(1)PCB指针的切换
movl %ebx,%eax
xchgl %eax,current
其中ebx中存的是switch_to的第一个参数,即下一个进程的PCB指针,然后和当前指针的值进行交换,从这里我们能看出——所谓的PCB切换实质就是让current指针存入被切换来进程的PCB指针。
(2)TSS内核指针的改写
movl tss,%ecx addl $4096,%ebx movl %ebx,ESP0(%ecx)
之前提过ebx存放的是新进程PCB指针的地址,之所以要给他加上4096即4K空间是指向了新进程的内存栈指针,由于每个进程的内核栈是由tss中的ss0和esp0共同标记的,所以每次进入内核栈时,任务的内核栈总是空的,而它初始的地址就是PCB首地址+4096。
(3)内核栈的切换
这步要做的也很简单,就是将cpu中的esp寄存器的内容置成新进程的内核栈首地址即可。
(4)LDT的切换
指令movl 12(%ebp),%ecx负责取出对应LDT(next)的那个参数,指令lldt %cx负责修改LDTR寄存器,一旦完成了修改,下一个进程在执行用户态程序时使用的映射表就是自己的LDT表了,地址空间实现了分离。
(5)PC指针的切换
最后一个切换是关于PC的切换,和前面论述的一致,依靠的就是switch_to的最后一句指令ret,虽然简单,但背后发生的事却很多:schedule()函数的最后调用了这个switch_to函数,所以这句指令ret就返回到下一个进程(目标进程)的schedule()函数的末尾,遇到的是},继续ret回到调用的schedule()地方,是在中断处理中调用的,所以回到了中断处理中,就到了中断返回的地址,再调用iret就到了目标进程的用户态程序去执行,和书中论述的内核态线程切换的五段论是完全一致的。