• 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
    	"     /* set task[pid].thread.sp to esp */
    3         "pushl %1
    	"             /* push ebp */
    4         "pushl %0
    	"             /* push task[pid].thread.ip */
    5         "ret
    	"                 /* 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
    	"         /* 保存当前EBP到堆栈中 */
     6                "movl %%esp,%0
    	"     /* 保存当前ESP到当前进程PCB中 */
     7                "movl %2,%%esp
    	"     /* 将next进程的堆栈栈顶的值存到ESP寄存器中 */
     8                "movl $1f,%1
    	"       /* 保存当前进程的EIP值,下次恢复进程后将在标号1开始执行 */    
     9                 "pushl %3
    	"      /* 将next进程继续执行的代码位置(标号1)压栈 */
    10                 "ret
    	"                 /* 出栈标号 1到EIP寄存器*/
    11                 "1:	"                  /* 标号1,即next进程开始执行的位置 */
    12                 "popl %%ebp
    	"     /* 恢复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<<<
    ",prev->pid,next->pid);  
    22         /* switch to new process */
    23         asm volatile(
    24                "pushl %%ebp
    	"         /* 保存当前EBP到堆栈中 */
    25                "movl %%esp,%0
    	"     /* 保存当前ESP到PCB */
    26                "movl %2,%%esp
    	"     /* 将next进程的栈顶地址到ESP寄存器中 */
    27                "movl %2,%%ebp
    	"     /* 将next进程的堆栈基地址到ESP寄存器中 */
    28                "movl $1f,%1
    	"       /* 保存当前进程的EIP寄存器值到PCB,这里$1f是指上面的标号1 */    
    29                "pushl %3
    	"      /* 把即将执行的进程的代码入口地址入栈 */
    30                "ret
    	"                 /* 出栈进程的代码入口地址到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寄存器(进程间切换)。

        

     

       

       

  • 相关阅读:
    如何使用RabbitMQ实现事件总线
    一起学Vue:UI框架(elementui)
    一起学Vue:访问API(axios)
    一起学Vue:CRUD(增删改查)
    一起学Vue:路由(vuerouter)
    如何使用IMemoryCache实现内存缓存
    手把手教你AspNetCore WebApi:Nginx(负载均衡)
    一起学Vue:状态管理(Vuex)
    自我介绍
    牛客练习赛74AB
  • 原文地址:https://www.cnblogs.com/chengzhenghua/p/11603305.html
Copyright © 2020-2023  润新知