2019-2020-3 20199317 "Linux kernel principle and Analysis" in the third week of work

Chapter 2 how the operating system works

1 computer three magic weapons

     Stored program computer: the von Neumann architecture

        Call recording space and path parameters: function call stack mechanism

        Interrupt mechanism: by the CPU and kernel code to achieve a common site preservation and restoration of the site, the data ebp, esp, eip register push into the kernel stack. Then eip entry point to interrupt the program, save the scene.

        EBP (base pointer) register in the C language is used as the base address of the current record in the function call, if the current depth comparison function call, a function of each EBP are not the same. Function call stack is a stack frame stacked on a plurality of logical up, with such a stack frame to realize the function calls and returns.

        For the x86 architecture is concerned, the stack address space from the high to the low address growth, as shown below:

        

 2 a simple configuration of the operating system kernel on the basis of mykernel

     C inline assembly code syntax is as follows:

        _asm_ _volatile_ (

                                  Assembly statement template:

                                  Output section:

                                  Input section:

                                 Destruction description section

                       );

        Here can be embedded assembly as a function of the output of the second portion and a third portion corresponds to the function of the input parameters and return values, and the first portion of the assembly code is specific code corresponding to internal functions.

        Construct a simple operating system kernel:

        Step: Add a mypcb.h header, used to define the process control, i.e. a definition block (Process Control Block) structure of the process, is struct tast_struct structure, as shown below in LInux kernel:

       

       第二步:对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寄存器(进程间切换)。

 

 

    

 

 

 

 

 

 

 

 

   

 

 

 

   

Guess you like

Origin www.cnblogs.com/chengzhenghua/p/11603305.html