Linux内核源码学习之进程切换细节整理

来源:互联网 发布:烧it 编辑:程序博客网 时间:2024/04/29 21:26
linux中的进程是个最基本的概念,进程从运行队列到开始运行有两个开始的地方,
一个就是switch_to宏中的标号1:"1:/t",//只要不是新创建的进程,几乎都是从上面的那个标号1开始的,而switch_to宏则是除了内核本身,所有的进程要 想运行都要经过的地方
另 一个就是ret_form_fork
这样看来,虽然linux的进程体系以及进程调度非常复杂,但是总体看来就是一个沙漏状,
对于系统中的每个新进程它首次被执行的过程必然是:
sys_fork---->do_fork---->copy_process ---->copy_thread 其中copy_process 设置的新进程的eip ret_from_fork --->放入就绪队列---->被调度---->执行switch_to
然后就到了jmp __switch_to了,这是个函数,这个函数的返回地址就区分了被调度的进程是新创建的进程还是已经执行过的进程了。
新创建(没有被执行过)的进程的返回地址是ret_from_fork
已经执行过的进程的返回地址是:switch_to中的标号1:

switch_to宏就是沙漏中间那个 最细的地方,想从一端到另一端,必然要经过那个地方,在非新创建的进程的情况下,所有进程都是从标号1开始,
看一下switch_to宏:
#define switch_to(prev,next,last) do {                            / 
         unsigned long esi,edi;                                          / 
         asm volatile("pushfl/n/t"                                      / 
                      "pushl %%ebp/n/t"                                 / 
                      "movl %%esp,%0/n/t"        /* save ESP */          / 
                      "movl %5,%%esp/n/t"        /* restore ESP */       /   //注意这里已经切换到了新的内核栈,故原来的栈中的局部变量全部失效,因而想得到其值就必须想办法保存它们,为了效率,这里将prev保存在寄存器中, 以便善后使用 
                      "movl $1f,%1/n/t"          /* save EIP */          /   //这里,只要是曾经在这里被切换出去的进程都会将标号1作为再回来时的eip 
                      "pushl %6/n/t"             /* restore EIP */       /   //将新进程的eip压入栈中,因为下面是个jmp,而且jmp到的函数最后有一个return,那么按照return的语义,就可以从栈取出eip载入 eip寄存器了,实际上这个对__switch_to的jmp调用就是一个手动的call调用(需要注意的是,这里return的是下一个进程的eip保存的是本进程的eip留给下次被切换进来的时候像现在这样使用),很巧妙 
                      "jmp __switch_to/n"                                /   //__switch_to是个FASTCALL的函数,eax/ebx寄存器传参数 
                      "1:/t"                                             /   //标号1的指令,很简单,但是就是这个简单成全了整体架构的简单 这里为什么不用call
                      "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) 
switch_to宏里面的第三个参数是什么?
实际上这里的三个参数分别是原先A的 prev和next 而最后一个参数是 A被切换进来的前一个进程为什么要这第三个(被调度前的一个进程)参数呢?留待后续 
linux 之所以实现上述的单点切换就是为了降低复杂度,其实很多操作系统内核都是这么做的,这里的单点并不是指switch_to这个单点,而是保存/恢复eip 这个寄存器从而保证所有切换回来的进程都从一个地方开始,但是有点美中不足的就是linux并没有将所有的进程从就绪到开始执行都从标号1开始,这也就引出了下面一个问题:
为什么不用call 代替 push+jmp?
如果用call 的话,call执行的时候首先将标号1处的地址压栈,然后jmp到__switch_to开始执行, __switch_to的函数都要返回到标号1:开始处来执行
而事实上并不是这样的,因为新创建的进程并不是从标号1开始执行的,而是从ret_from_fork开始执行的。
所以新进程在执行宏  switch_to到 "jmp __switch_to/n"的时候,它的返回地址将不会是标号1:而应该是eip,即在copy_thread中设置的eip中的值:ret_from_fork
所以关键点是这里的__switch_to函数
所有进程被调度的时候应该执行的是:

新创建进程返回的地址是:ret_from_fork
已经被调度过的进程的返回地址是:1:
看看新创建进程的返回地址处干了什么
到这里说明,新的线程已产生了. 
ENTRY(ret_from_fork) 
    pushl %eax 
    call schedule_tail 
    GET_THREAD_INFO(%ebp) 
    popl %eax 
    jmp syscall_exit 
syscall_exit: 
... 
work_resched: 
    call schedule 
... 
当他从ret_from_fork退出时,会从堆栈中弹出原来保存的eip,而ip指向kernel_thread_helper, 
至此kernel_thread_helper被调用,他就能够运行我们的指定的函数了do_exit(). 
从内核空间返回到用户空间。
http://my.oschina.net/linuxred/blog/9477
这样费事做的原因是什么?
看看 do_fork的实现就知道,其实新创建的进程是不这么做的,新创建的进程的eip是ret_from_fork而不是标号1,这个原因是什么?新创建进 程的时候要手工指定一个开始的地址,毕竟它要开始就要有个起点,那么起点在哪里好呢
最好是模拟该进程和别的已有进程一样是重新开始运行的,这样比较统一又便于管理, 然后将这个开始地址也指为标号1,但是这时标号1在哪里,是标号1在嵌入式汇编宏中导致标号1的地址不好取到吗,如果真的因为这的话,完全可以将标号1分 离出来放到一个地方,然后不管是已经有的进程还是新创建的进程都从这个固定的分离出来标号1的地址处取指令不就可以了吗?内核的设计者不可能还没有我聪明,那样的话会浪费取指令的时间和空间的,
来个间接引用肯定没有嵌入式汇编标号直接,降低时间开销,而且还有一个原因,用ret_from_fork完全可以做到和既有 进程的标号1一样的好

我们看看进程切换函数的设计,既有进程的切换都是在schedule里面进入switch_to从而找到标号1的,而在 switch_to之后就剩下一个finish_task_switch和判断重新调度标志了,
我们看看ret_from_fork: 
ENTRY(ret_from_fork) 
         pushl %eax      //注意刚从switch_to调用的__switch_to中ret回来,正好ret到了ret_from_fork(注意switch_to中jmp 指令前的push),而那个函数返回的就是prev,将其放到了eax中,故这里schedule_tail的参数就是prev,也就是切换出去的进程。 
         call schedule_tail 
         GET_THREAD_INFO(%ebp) 
         popl %eax 
         jmp syscall_exit 
上 面看到ret_from_fork调用的schedule_tail参数是切换出去的进程,而后者马上调用finish_task_switch,这样就 和schedule中的switch_to之后的逻辑对上了,而且参数也没有什么问题,

那么finish_task_switch之后的逻辑呢,比如判断 重新调度标志怎么办?
那就看看ret_from_fork中的syscall_exit吧,那里面做了判断,如果需要调度,那就会进入正常的 schedule流程,十分正确。
其实就是这个finish_task_switch善后惹的祸,不过它的设计也是一个很巧妙的看点,它主要判断原先的进 程是否还有存在的必要,如果已经dead了,那么就是在这里彻底释放其task_struct的,因此必须保存prev的值(回答了为甚么要三个参数这个问题引出的“要了这第三个参数有什么问题”),因为prev是 schedule的局部变量在prev的内核栈中,在切换到新的内核栈后(schedule函数用到了两个内核栈),prev失效,因此才要保存(这里可以理解为从prev传递到后续可见的地方)的。
http://blog.csdn.net/dog250/article/details/5303541

0 0