进程调度和切换---linux内核学习笔记(八)

时间:2022-06-27 06:11:51

内容一:实验报告相关说明

 

所学课程:《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\n\t"        /* save    flags */    \
43             "pushl %%ebp\n\t"        /* save    EBP   */    \
44             "movl %%esp,%[prev_sp]\n\t"    /* save    ESP   */ \
45             "movl %[next_sp],%%esp\n\t"    /* restore ESP   */ \
46             "movl $1f,%[prev_ip]\n\t"    /* save    EIP   */    \
47             "pushl %[next_ip]\n\t"    /* restore EIP   */    \
48             __switch_canary                    \
49             "jmp __switch_to\n"    /* regparm call  */    \
50             "1:\t"                        \
51             "popl %%ebp\n\t"        /* restore EBP   */    \
52             "popfl\n"            /* 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\n\t"        /* save    flags */    \
"pushl %%ebp\n\t"        /* save    EBP   */    \

  step2:保存当前esp到A进程内核描述符中(即进程A的PCB中)

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

  step3:从next(进程B)的描述符中取出之前B在切换出去时保存的内核堆栈栈顶

"movl %[next_sp],%%esp\n\t"    \
 

  step4:把标号为1的指令地址保存到A进程描述符的ip域,当A进程下次被switch_to回来时,会从这条指令开始执行。

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

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

"pushl %[next_ip]\n\t"    /* restore EIP   */     \
 "jmp __switch_to\n"    /* 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:\t"                        \
"popl %%ebp\n\t"     /* restore EBP   */    \
"popfl\n

 

内容六:小结 

  1:通过本次课的学习,把握了进程调度的时机,掌握了进程A切换到进程B发生的事情和基本流程,以及一些特殊情况。

  2:区分了进程上下文切换和中断上下文的切换。

  3:通过查阅资料和听课,对switch_to的功能有了较好的把握,理解的关键就在于:next进程如果之前也被switch_to出去过,那么[next_ip]里存的就是下面这个1f的标号,

但如果next进程刚刚被创建,之前没有被switch_to出去过,那么[next_ip]里存的将是ret_ftom_fork,这样又回到了进程创建的知识点上了。这样前后的知识就连贯起来了。