Linux内核分析实验八

来源:互联网 发布:广东红松网络怎么样 编辑:程序博客网 时间:2024/06/08 08:57

*************************************************************************************************

刘旸 + 原创作品转载请注明出处 

《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 

*************************************************************************************************


知识储备:

Linux任务切换

Linux任务切换是通过switch_to这个宏实现的,它利用长跳指令,当长跳指令的操作数是TSS描述符的时候,就会引起CPU的任务的切换,此时,cpu将所有寄存器的状态保存到当前任务寄存器TR所指向的TSS段(当前任务的任务状态段)中,然后利用长跳指令的操作数(TSS描述符)找到新任务的TSS段,并将其中的内容填写到各个寄存器中,最后,将新任务的TSS选择符更新到TR中。这样系统就正式开始运行新切换的任务了。

 

Linux系统的一般执行过程

即从正在运行的用户态进程X切换到运行用户态进程Y的过程

1.正在运行的用户态进程X

2.发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).

3.SAVE_ALL //保存现场

4.中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换

5.标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)

6.restore_all //恢复现场

7.iret - pop cs:eip/ss:esp/eflags from kernel stack

8.继续运行用户态进程Y

 

几种特殊情况

•通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;

•内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;

•创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork

•加载一个新的可执行程序后返回到用户态的情况,如execve

 

下面我们通过跟踪schedule函数来仔细分析系统进行进程调度和进程切换的过程:

1. 定位到LinuxKernel文件夹下,使用qemu命令编译运行MenuOS

 

 

2. 分割出新的终端窗口,开始进行gdb跟踪调试。断点设置如下:

 

 

3. 使用continue命令让MenuOS运行到breakpoint1,即schedule函数处停止,使用list命令查看其代码,s命令逐条分析。

 

 

4. 使用continue命令让MenuOS运行到breakpoint2,即context_switch处停下,使用list命令查看其代码,s命令逐条分析。

 

 

5. 注意到context_switch中调用了switch_to函数,进入其内部查看分析。

 

 

分析与总结:

switch_toA进程切换到B进程的步骤如下:

step1:复制两个变量到寄存器:

[prev]"a" (prev)

[next]"d" (next)

       即:

eax <== prev_Aeax<==%p(%ebp_A)

edx <== next_Aedx<==%n(%ebp_A)

 

step2:保存进程Aebpeflags

pushfl

pushl %ebp

       注意,因为现在esp还在A的堆栈中,所以它们是被保存到A进程的内核堆栈中。

 

step3:保存当前espA进程内核描述符中:

"movl%%esp,%[prev_sp]\n\t"    /*save    ESP   */

       它可以表示成:prev_A->thread.sp<== esp_A

       在调用switch_to时,prev是指向A进程自己的进程描述符的。

 

step4:next(进程B)的描述符中取出之前从B切换出去时保存的esp_B

"movl%[next_sp],%%esp\n\t" /* restore ESP */

       它可以表示成:esp_B <==next_A->thread.sp

       注意,在A进程中的next是指向B的进程描述符的。从这个时候开始,CPU当前执行的进程已经是B进程了,因为esp已经指向B的内核堆栈。但是,现在的ebp仍然指向A进程的内核堆栈,所以所有局部变量仍然是A中的局部变量,比如next实质上是%n(%ebp_A),也就是next_A,即指向B的进程描述符。

 

step5:把标号为1的指令地址保存到A进程描述符的ip域:

"movl$1f,%[prev_ip]\n\t"    /* save    EIP  */

       它可以表示成:prev_A->thread.ip<== %1f,当A进程下次被switch_to回来时,会从这条指令开始执行。具体方法看后面被切换回来的B的下一条指令。

 

step6:将返回地址保存到堆栈,然后调用__switch_to()函数,__switch_to()函数完成硬件上下文切换。

"pushl%[next_ip]\n\t"    /* restoreEIP   */

"jmp__switch_to\n"    /* regparmcall  */

       这里,如果之前B也被switch_to出去过,那么[next_ip]里存的就是下面这个1f的标号,但如果进程B刚刚被创建,之前没有被switch_to出去过,那么[next_ip]里存的将是ret_ftom_fork(参看copy_thread()函数)。这就是这里为什么不用call__switch_to而用jmp,因为call会导致自动把下面这句话的地址(也就是1:)压栈,然后__switch_to()就必然只能ret到这里,而无法根据需要retret_from_fork

       另外,这里__switch_to()返回时,将返回值prev_A又写入了%eax,这就使得在switch_to宏里面eax寄存器始终保存的是prev_A的内容,或者,更准确的说,是指向A进程描述符的“指针”。这是有用的,下面step8中将会看到。

 

step7:__switch_to()返回后继续从1:标号后面开始执行,修改ebpB的内核堆栈,恢复Beflags

"popl%%ebp\n\t"        /* restoreEBP   */    

"popfl\n"            /*restore flags */

       如果从__switch_to()返回后从这里继续运行,那么说明在此之前B肯定被switch_to调出过,因此此前肯定备份了ebp_Bflags_B,这里执行恢复操作。

      这时候ebp已经指向了B的内核堆栈,所以上面的prev,next等局部变量已经不是A进程堆栈中的了,而是B进程堆栈中的(B上次被切换出去之前也有这两个变量,所以代表着B堆栈中prevnext的值了),因为prev== %p(%ebp_B),而在B上次被切换出去之前,该位置保存的是B进程的描述符地址。如果这个时候就结束switch_to的话,在后面的代码中(即context_switch()函数中switch_to之后的代码)的prev变量是指向B进程的,因此,进程B就不知道是从哪个进程切换回来。而从context_switch()switch_to之后的代码中,我们看到finish_task_switch(this_rq(), prev)中需要知道之前是从哪个进程切换过来的,因此,我们必须想办法保存A进程的描述符到B的堆栈中,这就是last的作用。

 

step8:eax写入last,以在B的堆栈中保存正确的prev信息。

"=a"(last)  即  last_B <== %eax

       而从context_switch()中看到的调用switch_to的方法是:

switch_to(prev,next, prev);

       所以,这里面的last实质上就是prev,因此在switch_to宏执行完之后,prev_B就是正确的A的进程描述符了。这里,last的作用相当于把进程A堆栈中的A进程描述符地址复制到了进程B的堆栈中。


    至此,switch_to已经执行完成A停止运行,而开始了B。在以后,可能在某一次调度中,进程A得到调度,就会出现switch_to(C,A)这样的调用,这时,A再次得到调度,得到调度后,A进程从context_switch()switch_to后面的代码开始执行,这时候,它看到的prev_A将指向C的进程描述符。


0 0