内容一:实验报告相关说明
所学课程:《Linux内核分析》MOOC课程
链接:http://mooc.study.163.com/course/USTC-1000029000
内容二:linux系统的调度时机
主要有以下时机:
-
中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
-
内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
-
用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
内容三:进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换,任务切换或上下文切换。
进程切换可能只发生在精心定义的点:schedule()函数,至于如何选择下一个待切换的进程,有很多相关的算法,为了方便理解,就简单抽象为使用某种调度算法从运行
队列中找到需要调度的进程。
通过上课总结了一下几点:
3.1:schedule()为内核函数,并非系统调用,所以用户态只能被动调用。
3.2:内核进程可以直接调用schedule()函数。
3.3:中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
3.4:schedule()函数调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换
3.5:switch_to 利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程。
内容四:switch_to分析
源代码如下:
31#define switch_to(prev, next, last) 32do { 33 /* 34 * Context-switching clobbers all registers, so we clobber 35 * them explicitly, via unused output variables. 36 * (EAX and EBP is not listed because EBP is saved/restored 37 * explicitly for wchan access and EAX is the return value of 38 * __switch_to()) 39 */ 40 unsigned long ebx, ecx, edx, esi, edi; 41 42 asm volatile("pushfl " /* save flags */ 43 "pushl %%ebp " /* save EBP */ 44 "movl %%esp,%[prev_sp] " /* save ESP */ 45 "movl %[next_sp],%%esp " /* restore ESP */ 46 "movl $1f,%[prev_ip] " /* save EIP */ 47 "pushl %[next_ip] " /* restore EIP */ 48 __switch_canary 49 "jmp __switch_to " /* regparm call */ 50 "1: " 51 "popl %%ebp " /* restore EBP */ 52 "popfl " /* restore flags */ 53 54 /* output parameters */ 55 : [prev_sp] "=m" (prev->thread.sp), 56 [prev_ip] "=m" (prev->thread.ip), 57 "=a" (last), 58 59 /* clobbered output registers: */ 60 "=b" (ebx), "=c" (ecx), "=d" (edx), 61 "=S" (esi), "=D" (edi) 62 63 __switch_canary_oparam 64 65 /* input parameters: */ 66 : [next_sp] "m" (next->thread.sp), 67 [next_ip] "m" (next->thread.ip), 68 69 /* regparm parameters for __switch_to(): */ 70 [prev] "a" (prev), 71 [next] "d" (next) 72 73 __switch_canary_iparam 74 75 : /* reloaded segment registers */ 76 "memory"); 77} while (0)
分析的参考资料来自如下博客:
作者:visayafan
出处:http://www.cnblogs.com/visayafan/
4.1:当schedule()需要暂停A进程的执行而继续B进程的执行时,就发生了进程之间的切换。进程切换主要有两部分:1、切换全局页表项;2、切换内核堆栈和硬件上下文。这个切换工作由
context_switch()完成。其中switch_to和__switch_to()主要完成第二部分。更详细的,__switch_to()主要完成硬件上下文切换,switch_to主要完成内核堆栈切换。
4.2:switch_to时请注意:这是一个宏,不是函数,它的参数prev, next, last不是值拷贝,而是它的调用者context_switch()的局部变量。局部变量是通过%ebp寄存器来索引的,也就是
通过n(%ebp)。
4.3:switch_to切换主要有以下三部分:
进程切换 |
即esp的切换 |
由于从esp可以找到进程的描述符 |
硬件上下文切换 |
_switch_to() |
以前通过x86硬件支持,现在使用软件切换 |
堆栈的切换 |
即ebp的切换 |
ebp是栈底指针,它确定了当前变量空间属于哪个进程 |
大概的步骤如下:(假设进程A切换到进程B)
step1:保存进程A的ebp和eflags,因为现在esp还在A的堆栈中,所以这两个东西被保存到A进程的内核堆栈中。
"pushfl " /* save flags */ "pushl %%ebp " /* save EBP */
step2:保存当前esp到A进程内核描述符中(即进程A的PCB中)
"movl %%esp,%[prev_sp] " /* save ESP */
step3:从next(进程B)的描述符中取出之前B在切换出去时保存的内核堆栈栈顶
"movl %[next_sp],%%esp "
step4:把标号为1的指令地址保存到A进程描述符的ip域,当A进程下次被switch_to回来时,会从这条指令开始执行。
"movl $1f,%[prev_ip] " /* save EIP */
step5:将返回地址保存到堆栈,然后调用__switch_to()函数,__switch_to()函数完成硬件上下文切换。
"pushl %[next_ip] " /* restore EIP */ "jmp __switch_to " /* regparm call */
如果之前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到这里,而无法根据需要ret到ret_from_fork。
另外请注意,这里__switch_to()返回时,将返回值prev_A又写入了%eax,这就使得在switch_to宏里面eax寄存器始终保存的是prev_A的内容,或者,更准确的说,是指向A进程描述符的“指针”。
step6:从__switch_to()返回后继续从1:标号后面开始执行,修改ebp到B的内核堆栈,恢复B的eflags
"1: " "popl %%ebp " /* restore EBP */
"popfl
内容六:小结
1:通过本次课的学习,把握了进程调度的时机,掌握了进程A切换到进程B发生的事情和基本流程,以及一些特殊情况。
2:区分了进程上下文切换和中断上下文的切换。
3:通过查阅资料和听课,对switch_to的功能有了较好的把握,理解的关键就在于:next进程如果之前也被switch_to出去过,那么[next_ip]里存的就是下面这个1f的标号,
但如果next进程刚刚被创建,之前没有被switch_to出去过,那么[next_ip]里存的将是ret_ftom_fork,这样又回到了进程创建的知识点上了。这样前后的知识就连贯起来了。