2019-2020-3 20199317《Linux内核原理与分析》第三周作业

第2章  操作系统是如何工作的

1  计算机的三大法宝

     存储程序计算机:冯诺依曼结构

        函数调用堆栈机制:记录调用的路径和参数的空间

        中断机制:由CPU和内核代码共同实现了保存现场和恢复现场,把ebp,esp,eip寄存器的数据push到内核堆栈中。再把eip指向中断程序的入口,保存现场。

        EBP(基址指针)寄存器在C语言中用作记录当前函数调用的基址,如果当前函数调用比较深,每一个函数的EBP是不一样的。函数调用堆栈就是由多个逻辑上的堆栈叠起来的框架,利用这样的堆栈框架实现函数的调用和返回。

        对于x86体系结构来讲,堆栈空间是从高地址向低地址增长的,如下图所示:

        

 2  在mykernel基础上构造一个简单的操作系统内核

     C代码中内嵌汇编的语法如下:

        _asm_ _volatile_ (

                                  汇编语句模板:

                                  输出部分:

                                  输入部分:

                                 破坏描述部分

                       );

        这里可以把内嵌汇编当作一个函数,则第二部分输出和第三部分输入相当于函数的参数和返回值,而第一部分的汇编代码则相当于函数内部的具体代码。

        构造一个简单的操作系统内核为:

        第一步:增加一个mypcb.h头文件,用来定义进程控制块(Process Control Block),也就是进程结构体的定义,在LInux内核中是struct tast_struct结构体,如下图所示:

       

       第二步:对mymain.c进行修改,这里是mykernel内核代码的入口,负责初始化内核的各个组成部分。在Linux内核源代码中,实际的内核入口是init/main.c中的start_kernel(void)函数。如下图所示:

       

       第三步:对myinterrupt.c进行修改,主要是增加了进程切换的代码my_schedule(void)函数,在Linux内核源代码中对应的是schedule(void)函数。如下图所示:

       

        最后,用cd ..命令返回上一层目录linux-3.9.4下,输入make命令重新编译,再输入qemu -kernel arch/x86/boot/bzImage,就可以看到重新编译后的内核启动效果如下图所示:

       

      从图中可看到进程在切换。

3  代码分析

        mypcb.h:此文件主要定义了一个PCB结构体,也就是所谓的进程管理块,用来记录进程的有关信息。

        mymain.c:该文件就是完成了内核的初始化工作,并且创建了4个进程,进程从0号开始执行,并且根据标志位判断进程是否需要调度,此时会执行my_schedule()方法来完成相应的调度。

        myinterrupt:此文件主要是产生时钟中断,用一个时间计数器周期性的检查循环条件,当条件满足时便会产生中断,并将进程调度标志位置1,当标志位为1时,进程便会执行my_schedule()方法,首先将当前进程的信息通过嵌入式汇编语句保存到堆栈当中,然后将下一个进程的地址赋给当前运行程序的指针,完成调度。

        启动第一个进程即0号进程的关键汇编代码为:

1 asm volatile(
2         "movl %1,%%esp\n\t"     /* set task[pid].thread.sp to esp */
3         "pushl %1\n\t"             /* push ebp */
4         "pushl %0\n\t"             /* push task[pid].thread.ip */
5         "ret\n\t"                 /* pop task[pid].thread.ip to eip */
6         : 
7         : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)    /* input c or d mean %ecx/%edx*/
8     );

       这里需要注意的是%1是指后面的“ “d”(task[pid].thread.sp)”,%0是指后面的“ “c”(task[pid].thread.ip)”,进程0被初始化时,进程0的堆栈和相关寄存器的变化过程如下图所示:

       

        进程调度代码如下:

 1 if(next->state == 0)/* next->state == 0对应进程next对应进程曾经执行过 */
 2 {         
 3         /* 进行进程调度关键代码 */
 4         asm volatile(    
 5                "pushl %%ebp\n\t"         /* 保存当前EBP到堆栈中 */
 6                "movl %%esp,%0\n\t"     /* 保存当前ESP到当前进程PCB中 */
 7                "movl %2,%%esp\n\t"     /* 将next进程的堆栈栈顶的值存到ESP寄存器中 */
 8                "movl $1f,%1\n\t"       /* 保存当前进程的EIP值,下次恢复进程后将在标号1开始执行 */    
 9                 "pushl %3\n\t"      /* 将next进程继续执行的代码位置(标号1)压栈 */
10                 "ret\n\t"                 /* 出栈标号 1到EIP寄存器*/
11                 "1:\t"                  /* 标号1,即next进程开始执行的位置 */
12                 "popl %%ebp\n\t"     /* 恢复EBP寄存器的值 */
13                 : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
14                 : "m" (next->thread.sp),"m" (next->thread.ip)
15        ); 
16 }  
17 else     /* next该进程第一次被执行 */
18 {
19         next->state = 0;
20         my_current_task = next; 
21         printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);  
22         /* switch to new process */
23         asm volatile(
24                "pushl %%ebp\n\t"         /* 保存当前EBP到堆栈中 */
25                "movl %%esp,%0\n\t"     /* 保存当前ESP到PCB */
26                "movl %2,%%esp\n\t"     /* 将next进程的栈顶地址到ESP寄存器中 */
27                "movl %2,%%ebp\n\t"     /* 将next进程的堆栈基地址到ESP寄存器中 */
28                "movl $1f,%1\n\t"       /* 保存当前进程的EIP寄存器值到PCB,这里$1f是指上面的标号1 */    
29                "pushl %3\n\t"      /* 把即将执行的进程的代码入口地址入栈 */
30                "ret\n\t"                 /* 出栈进程的代码入口地址到EIP寄存器*/   
31                : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
32                : "m" (next->thread.sp),"m" (next->thread.ip)
33        ); 
34 }

         为了简便,假设系统只有两个进程,分别是进程0和进程1。进程0由内核启动时初始化执行,然后需要进程调度,开始执行进程1。下面从进程1被调度开始分析堆栈变化,因为进程1从来没有被执行过,此时执行else中的代码。堆栈和相关寄存器的变化过程如下图所示:                 

                                                

         如果进程1执行的过程中发生了进程调度,进程0重新被调度执行了,这个时候应该执行if中的代码,if中的内嵌汇编代码执行过程中堆栈的变化分析如下图所示:

          

        然后这里有一个问题:$1f为前方的标号1,if中有标号1,else中没有标号1。这是因为else中是进程第一次被执行的代码,只用将其存入prev->thread.ip,并不需要使用$lf来获取程序入口地址,但是if中的代码代表的是进程再次被重新调度执行,prev->thread.ip变成了next->thread.ip,此时进入了if代码块中会将next->thread.ip压栈,并由ret出栈到EIP寄存器中,这时就需要使用上次被调度出去时保存的$1f。

4   总结

        操作系统首先初始化内核相关的进程,然后开始循环运行这些进程,当进程间进行切换时,利用内核堆栈所保存的每个进程的sp,ip即所对应的%esp,%eip寄存器中的值,对当前的进程的sp,ip即对应%esp,%eip寄存器的值进行保存(中断上下文),并用下一个进程的sp,ip的值赋值给%esp,%eip寄存器(进程间切换)。

    

 

   

   

猜你喜欢

转载自www.cnblogs.com/chengzhenghua/p/11603305.html
今日推荐