zoukankan      html  css  js  c++  java
  • 2018-2019-1 20189229《Linux内核原理与分析》第三周作业

    一. mykernel实验指导(操作系统是如何工作的)

    运行并分析一个精简的操作系统内核,理解操作系统是如何工作的
    使用实验楼的虚拟机打开shell

    • 1.cd LinuxKernel/linux-3.9.4
    • 2.qemu -kernel arch/x86/boot/bzImage
      然后cd mykernel 您可以看到qemu窗口输出的内容的代码mymain.c和myinterrupt.c
      使用自己的Linux系统环境搭建过程参见mykernel,其中也可以找到一个简单的时间片轮转多道程序内核代码。

    二. 开始第一部分实验:

    进入实验楼虚拟机后,打开mymain.c文件,可以看到其中只有如下这一个函数。它作为内核启动的起始位置,从这个函数开始运行,并无限循环。

    void __init my_start_kernel(void)
    {
        int i = 0;
        while(1)
        {
            i++;
            if(i%100000 == 0)
                printk(KERN_NOTICE "my_start_kernel here  %d 
    ",i);
    
        }
    }
    

    打开myinterrupt.c文件,里面也只有一个my_timer_handler(),它被Linux内核周期性调用,从而产生了一个周期性的中断机制。

    void my_timer_handler(void)
    {
        printk(KERN_NOTICE "
    >>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<
    
    ");
    }
    

    在终端中输入如下命令:

    可以看到初始的内核运行情况如下:

    内核不停的执行my_start_kernel(),每隔一段时间被my_timer_handler()中断,然后执行一条打印语句:printk(KERN_NOTICE “ >>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<< ”);后,又回到my_start_kernel()继续执行。

    三.第二部分实验:修改内核代码,使之成为一个简单的时间片轮转多道程序内核,然后重新编译运行。

    从https://github.com/mengning/mykernel上下载mypcb.h;mymain.c;myinterrupt.c;然后替换位于home/shiyanlou/LinuxKernel/linux-3.9.4/mykernel/中的mymain.c;myinterrupt.c;将mypcb.h也放在这里。 接下来在shell中将当前工作目录退回到home/shiyanlou/LinuxKernel/linux-3.9.4/ 然后执行make,重新编译内核。效果如下:


    然后再次输入:qemu -kernel arch/x86/boot/bzImage 启动内核。

    可以看到新的内核运行效果如下:

    从上图中可以清晰看到进程的切换过程:在0号进程运行过程中,先是my_timer_handler()被执行,然后是myschedule()被执行,myschedule()在运行过程中会打印switch 0(被切换出去的进程号) to 1(切换到的进程号)。然后就跳到新的进程1继续执行。

    四.mykernel内核源代码分析:在关键地方都加了注释。

    4.1 mypcb.h

    /*
     *  linux/mykernel/mypcb.h
     *
     *  Kernel internal PCB types
     *
     *  Copyright (C) 2013  Mengning
     *
     */
    #define MAX_TASK_NUM        4    //最大进程数,这里设置为了4个。
    #define KERNEL_STACK_SIZE   1024*8  //每个进程的内核栈的大小。
    
    /* CPU-specific state of this task */
    struct Thread {
        unsigned long       ip;//用于保存进程的eip
        unsigned long       sp;//用户保存进程的esp
    };
    
    typedef struct PCB{
        int pid;//进程的id号
        volatile long state;    /* 进程的状态:-1 unrunnable, 0 runnable, >0 stopped */
        char stack[KERNEL_STACK_SIZE];//进程的栈,只有一个核心栈。
        /* CPU-specific state of this task */
        struct Thread thread;//每个进程只有一个线程。
        unsigned long   task_entry;//进程的起始入口地址。
        struct PCB *next;//指向下一个进程的指针。
    }tPCB;
    
    void my_schedule(void);
    

    4.2 mymain.c

    /*
     *  linux/mykernel/mymain.c
     *
     *  Kernel internal my_start_kernel
     *
     *  Copyright (C) 2013  Mengning
     *
     */
    #include <linux/types.h>
    #include <linux/string.h>
    #include <linux/ctype.h>
    #include <linux/tty.h>
    #include <linux/vmalloc.h>
    
    
    #include "mypcb.h"
    
    tPCB task[MAX_TASK_NUM];
    tPCB * my_current_task = NULL;
    volatile int my_need_sched = 0;
    
    void my_process(void);
    
    
    void __init my_start_kernel(void)
    {
        int pid = 0;
        int i;
        /* Initialize process 0*/
        task[pid].pid = pid;//task[0].pid=0;
        task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
        task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;//令0号进程的入口地址为my_process();
        task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];//0号进程的栈顶为stack[]数组的最后一个元素
        task[pid].next = &task[pid];//next指针指向自己
        /*fork more process */
        for(i=1;i<MAX_TASK_NUM;i++)//根据0号进程,复制出几个只是编号不同的进程
        {
            memcpy(&task[i],&task[0],sizeof(tPCB));//void *memcpy(void *dest, const void *src, size_t n);从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。
            task[i].pid = i;
            task[i].state = -1;//这些进程的状态都设置为未运行。
            task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
            task[i].next = task[i-1].next;//新创建的进程的next指向0号进程的首地址
            task[i-1].next = &task[i];//前一个进程的next指向最新创建的进程的首地址,从而成为一个循环链表。
        }
        /* start process 0 by task[0] */
        pid = 0;
        my_current_task = &task[pid];//当前运行的进程设置为0号进程。
        asm volatile(
            "movl %1,%%esp
    	"     /* set task[pid].thread.sp to esp */
            "pushl %1
    	"          /* push ebp */
            "pushl %0
    	"          /* push task[pid].thread.ip */
            "ret
    	"               /* pop task[pid].thread.ip to eip */
            "popl %%ebp
    	"
            : 
            : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)   /* input c or d mean %ecx/%edx*/
        );
    }   
    void my_process(void)
    {
        int i = 0;
        while(1)
        {
            i++;
            if(i%10000000 == 0)
            {
                printk(KERN_NOTICE "this is process %d -
    ",my_current_task->pid);//打印-号
                if(my_need_sched == 1)//如果需要调度
                {
                    my_need_sched = 0;
                    my_schedule();
                }
                printk(KERN_NOTICE "this is process %d +
    ",my_current_task->pid);//打印+号
            }     
        }
    }
    

    4.3 myinterrupt.c

    /*
     *  linux/mykernel/myinterrupt.c
     *
     *  Kernel internal my_timer_handler
     *
     *  Copyright (C) 2013  Mengning
     *
     */
    #include <linux/types.h>
    #include <linux/string.h>
    #include <linux/ctype.h>
    #include <linux/tty.h>
    #include <linux/vmalloc.h>
    
    #include "mypcb.h"
    
    extern tPCB task[MAX_TASK_NUM];
    extern tPCB * my_current_task;
    extern volatile int my_need_sched;
    volatile int time_count = 0;
    
    /*
     * Called by timer interrupt.
     * it runs in the name of current running process,
     * so it use kernel stack of current running process
     */
    void my_timer_handler(void)
    {
    #if 1
        if(time_count%1000 == 0 && my_need_sched != 1)
        {
            printk(KERN_NOTICE ">>>my_timer_handler here<<<
    ");
            my_need_sched = 1;
        } 
        time_count ++ ;  
    #endif
        return;     
    }
    
    void my_schedule(void)
    {
        tPCB * next;
        tPCB * prev;
    
        if(my_current_task == NULL 
            || my_current_task->next == NULL)
        {
            return;
        }
        printk(KERN_NOTICE ">>>my_schedule<<<
    ");
        /* schedule */
        next = my_current_task->next;//将下一个将要运行的进程设置为my_current_task->next指向的下一个进程。
        prev = my_current_task;//将当前进程设置为prev进程。
        if(next->state == 0)/*如果下一个将要运行的进程已经处于运行状态 -1 unrunnable, 0 runnable, >0 stopped */
        {
            /* switch to next process */
            asm volatile(   
                "pushl %%ebp
    	"       /* 保存当前进程的ebp到自己的栈中。    save ebp */
                "movl %%esp,%0
    	"     /* 保存当前进程的esp到自己的栈中。    save esp */
                "movl %2,%%esp
    	"     /* 从next->thread.sp中弹出下一个进程的esp。与第二句相对应。   restore  esp */
                "movl $1f,%1
    	"       /* 将下一个进程的eip设置为1f。$1f就是指标号1:的代码在内存中存储的地址  save eip */   
                "pushl %3
    	"          /* 将next->thread.ip压入当前进程的栈中。*/
                "ret
    	"               /* 从当前进程的栈中弹出刚刚压入的next->thread.ip。完成进程切换。  restore  eip */
                "1:	"                  /* 即$1f指向的位置。next process start here */
                "popl %%ebp
    	"        /* 切换到的进程把ebp从栈中弹出至ebp寄存器。与第一句相对应。*/
                : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
                : "m" (next->thread.sp),"m" (next->thread.ip)
            ); 
            my_current_task = next; //当前进程切换为next
            printk(KERN_NOTICE ">>>switch %d to %d<<<
    ",prev->pid,next->pid); //打印切换信息     
        }
        else//如果下一个将要运行的进程还从未运行过。
        {
            next->state = 0;//将其设置为运行状态。
            my_current_task = next;////当前进程切换为next
            printk(KERN_NOTICE ">>>switch %d to %d<<<
    ",prev->pid,next->pid);//打印切换信息
            /* switch to new process */
            asm volatile(   
                "pushl %%ebp
    	"       /* save ebp */
                "movl %%esp,%0
    	"     /* save esp */
                "movl %2,%%esp
    	"     /* restore  esp */
                "movl %2,%%ebp
    	"     /* restore  ebp */
                "movl $1f,%1
    	"       /* 将要被切换出去的进程的ip设置为$1f。这样等一下它被切换回来时(一定是运行状态)肯定会进入if判断分支,可以从if中的标号1处继续执行。  save eip */    
                "pushl %3
    	"          /* 将next->thread.ip(因为它还没有被运行过,所以next->thread.ip现在仍处于初始状态,即指向my_process(),压入将要被切换出去的进程的堆栈。*/
                "ret
    	"               /* 将刚刚压入的next->thread.ip出栈至eip,完成进程切换。   restore  eip */
                : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
                : "m" (next->thread.sp),"m" (next->thread.ip)
            );          
        }   
        return; 
    }
    

    五.分析进程的启动和进程的切换机制。

    首先,内核启动__init my_start_kernel(void),创建了4个进程,分别是0,1,2,3号,设置0号为运行态,其它3个进程为未运行态。0号进程的入口都被初始化为 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;即指向my_process()
    0号进程的栈顶被初始化为 task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1]; 之后的进程也都是根据0号进程复制得到,所以它们的起始入口也是my_process(),初始栈顶也是指向了自己的stack[KERNEL_STACK_SIZE-1];
    my_current_task = &task[pid];将当前进程设置为0号进程。然后从0号进程开始运行。
    “movl %1,%%esp ”
    将0号进程的栈顶放入esp寄存器。
    “pushl %1 ” /* push ebp */
    当前esp指向了stack数组的末尾,这里也是栈顶,因为栈是空的,所以esp==ebp
    “pushl %0 " /* push task[pid].thread.ip */
    “ret ” /* pop task[pid].thread.ip to eip */
    切换到0号进程的入口地址开始执行。
    “popl %%ebp ”
    这句多余,在ret之后,不会被执行。

    :
    “c” (task[pid].thread.ip),”d” (task[pid].thread.sp)

    之后0号进程不断执行my_process()。一段时间后,my_timer_handler()被内核调用,触发中断, my_need_sched = 1;将全局变量my_need_sched 设置为了1。此后,当0号进程执行到了if(my_need_sched == 1)时就会进入这个if条件分支中,执行 my_schedule();执行进程调度。

    0号进程的next指针指向的是1号进程,所以在my_schedule()中的next指针指向了1号进程,prev指针指向了0号进程。
    因为1号进程当前还未被运行过,所以会执行else条件分支:next->state = 0;//将1号进程设置为运行状态
    my_current_task = next;//当前进程切换为1号进程printk(KERN_NOTICE “>>>switch %d to %d<<< ”,prev->pid,next->pid);//打印switch 0 to 1

    “pushl %%ebp
    	”       /* save ebp */ 
    “movl %%esp,%0
    	”     /* save esp */ 
    

    将0号进程的ebp和esp都保存到0号进程的栈上。

    “movl %2,%%esp
    	”     /* restore  esp */ 
    “movl %2,%%ebp
    	”     /* restore  ebp */ 
    

    将1号进程的存在1号进程结构体中next->thread.sp保存的esp的值存入esp寄存器和ebp寄存器,因为1号进程还未被运行过,所以esp仍指向了1号栈的stack KERNEL_STACK_SIZE-1]
    “movl $1f, %1 ” 将0号进程的eip设置为if。
    “pushl %3 ”
    “ret ”
    将1号进程的eip加入0号进程的栈中,然后通过ret指令,将这个eip从0号进程的栈中弹出,存入eip寄存器,完成从0号进程到1号进程的切换。接下来1到2,2到3号进程的切换也是这样。 然后是3号进程切换到0号进程,因为0号进程最开始已经运行过,所以已经处于运行态,在执行到my_schedule()中的if(next->state == 0)时,会进入此if条件分支。本次具体分析见代码后的注释。

    asm volatile(   
                "pushl %%ebp
    	"    /* 保存3号进程的ebp到3号的栈中。*/
                "movl %%esp,%0
    	"     /* 保存3号进程的esp到3号的栈中。*/
                "movl %2,%%esp
    	"     /* 从next->thread.sp中弹出0号进程的esp。与第二句相对应。*/
                "movl $1f,%1
    	"     /* 将0号进程的eip设置为1f。$1f就是指标号1:的代码在内存中存储的地址。*/ 
                "pushl %3
    	"     /* 将next->thread.ip压入3号进程的栈中。*/
                "ret
    	"               /* 从3号进程的栈中弹出刚刚压入的next->thread.ip。完成进程切换。*/
                "1:	"     /* 即$1f指向的位置。next process start here */
                "popl %%ebp
    	"        /* 切换到的进程(这一次是0号进程)把ebp从栈中弹出至ebp寄存器。与第一句相对应。*/
                : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
                : "m" (next->thread.sp),"m" (next->thread.ip)
            ); 
            my_current_task = next; //当前进程切换为0号进程。      
            printk(KERN_NOTICE ">>>switch %d to %d<<<
    ",prev->pid,next->pid); //打印切换信息 switch 3 to 0 
    

    接下来从0到1,1到2,2到3······都是执行这个if条件分支。

    第二周课程的难点分析(来自《Linux内核分析》课程团队):
    理解和运行mykernel,它是提供初始化好的CPU从my_start_kernel开始执行,并提供了时钟中断机制周期性性执行my_time_handler中断处理程序,执行完后中断返回总是可以回到my_start_kernel中断的位置继续执行。当然中断保存现场恢复现场的细节都处理好了,mykernel就是一个逻辑上的硬件平台,具体怎么做到的一般不必深究。

    能运行mykernel后就可以写一个自己的时间片轮转调度内核了,自己写还是很难的,只需到mykernel的github版本库找到代码复制过来重新编译Linux3.9.4的源代码,能按视频的效果跑起来,这都不难。

    难点是理解基于mykernel实现的时间片轮转调度代码。

    往往系统都有很多进程比较复杂,我们假定当前系统只有两个进程0和1,第一次调度是从0切换到1,也就是prev=0,next=1,第二次调度正好相反。

    这时再看https://github.com/mengning/mykernel/blob/master/myinterrupt.c 中的汇编代码,保存prev的进程(0)上下文,下次调度是next进程就是0了,反之进程1是next那它肯定之前作为prev被调度出去过。理解进程上下文的保存和恢复极为关键。

    $1f就是指标号1:的代码在内存中存储的地址

    再来看特殊一点代码切换到一个新的进程,也就是next没有被保存过进程上下文,它从没有被执行过,这时稍特殊一点即else部分的汇编代码。

    六.自己对“操作系统是如何工作的”理解。

    操作系统的内核有一个起始位置,从这个起始位置开始执行。在执行了一些初始化操作,比如进程的状态设置,各个进程的栈的空间的分配后,将CPU分配给第一个进程,开始执行第一个进程,然后通过一定的调度算法,比如时间片轮转,在一个时间片后,发生中断,第一个进程被阻塞,在完成保存现场后将CPU分配给下一个进程,执行下一个进程。这样,操作系统就完成了基本的进程调度的功能。

  • 相关阅读:
    Steal 偷天换日 题解(From luoguBlog)
    Hibernat之关系的处理多对多
    Hibernat之关系的处理一对一处理
    Hibernat之关系的处理一对多/多对一
    Hibernate 一对多注解 mappedby 作用
    hibernate之使用Annotation注解搭建项目
    暑假学习第八周
    暑假学习第七周
    java学习第六周
    暑假学习第五周
  • 原文地址:https://www.cnblogs.com/zisong/p/9867880.html
Copyright © 2011-2022 走看看