• 基于mykernel 2.0编写一个操作系统内核--课程实验1


    一、配置mykernel 2.0  

    按照 https://github.com/mengning/mykernel 的说明配置mykernel 2.0,熟悉Linux内核的编译;
    环境:VMware Workstation 15 pro ,Ubuntu 18.04.4 LTS;

    1. 配置前先拍快照,保存当前状态备用。

    2. 按照 https://github.com/mengning/mykernel 的说明配置mykernel 2.0,执行以下命令。

    wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch
    sudo apt install axel
    axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
    xz -d linux-5.4.34.tar.xz
    tar -xvf linux-5.4.34.tar
    cd linux-5.4.34
    patch -p1 < ../mykernel-2.0_for_linux-5.4.34.patch
    sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev
    make defconfig # Default configuration is based on 'x86_64_defconfig'
    make -j$(nproc)
    sudo apt install qemu # install QEMU
    qemu-system-x86_64 -kernel arch/x86/boot/bzImage

           执行完上述代码后,可以看到QEMU窗口中my_start_kernel进程周期性的调用my_handler_here方法,如下图所示。

            在Linux-5.3.34 内核源代码根目录下进入mykernel目录,可以看到QEMU窗口输出的内容的代码 mymain.c 和 myinterrupt.c ,当前有一个虚拟的CPU执行C代码的上下文环境,可以看到 mymain.c 中的代码在不停地执行。同时有一个中断处理程序的上下文环境,周期性地产生的时钟中断信号,能够触发myinterrupt.c中的代码。这样就通过Linux内核代码模拟了一个具有时钟中断和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);
        }
    }

            观察上述代码可知:每当 i 能够被 100000 整除 就输出 “my_start_kernel here” ,即相当于一个时钟信号。下面的代码就是进行处理时钟中断的。

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

    二、编写一个操作系统内核并简要分析

      参照 https://github.com/mengning/mykernel 提供的范例代码,基于mykernel 2.0编写一个操作系统内核。
      在 https://github.com/mengning/mykernel 中 下载 mypcb.h,myinterrupt.c和mymain.c文件,并拷贝到本机的 mykernel 目录下,并新增头文件mypcb.h,修改好文件后重新配置编译内核,并使用QEMU加载,结果如下图:

    make allnoconfig
    make 
    qemu -kernel arch/x86/boot/bzImage

      观察可知上图中正在进行进程的切换:进程2 切换到 进程3,进程切换的关键代码是一段嵌入式汇编,最有技巧性的地方是通过pushq %rip和ret指令来间接修改%rip的值,从而更改代码执行流,再配合%rsp和%rbp的修改切换进程的工作栈,从而达到切换进程的目的。

      简单分析:

      首先在mykernel目录下增加一个mypcb.h 头文件,用来定义进程控制块PCB(Process Control Block),也就是进程结构体的定义,在Linux内核中是struct tast_struct结构体。

     1 /*
     2  *  linux/mykernel/mypcb.h
     3  */
     4  
     5 #define MAX_TASK_NUM        4
     6 #define KERNEL_STACK_SIZE   1024*8
     7 
     8 /* CPU-specific state of this task */
     9 struct Thread {
    10     unsigned long       ip;
    11     unsigned long       sp;
    12 };
    13 
    14 typedef struct PCB{
    15     int pid;
    16     volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
    17     char stack[KERNEL_STACK_SIZE];
    18     /* CPU-specific state of this task */
    19     struct Thread thread;
    20     unsigned long   task_entry;
    21     struct PCB *next; 
    22 }tPCB;
    23 
    24 void my_schedule(void);

      其中:pid表示进程号;state表示进程状态,在模拟系统中,所有进程控制块信息都会被创建出来,其初始化值就是-1,如果被调度运行起来,其值就会变成0;stack是进程使用的堆栈,栈大小为1024*8;task_entry为进程入口函数;*next:指向下一个PCB,此模拟系统中的PCB是以链表的形式组织起来的;函数的声明 my_schedule,它的实现在my_interrupt.c中,在mymain.c中的各个进程函数会根据一个全局变量的状态来决定是否调用它,从而实现主动调度。

      对mymain.c进行修改,初始化各个进程并启动0号进程,这里是mykernel内核代码的入口,负责初始化内核的各个组成部分。在Linux内核源代码中,实际的内核入口是init/main.c中的start_kernel(void)函数。

     1 /*
     2  *  linux/mykernel/mymain.c
     3  */
     4  
     5 #include "mypcb.h"
     6 
     7 tPCB task[MAX_TASK_NUM];
     8 tPCB * my_current_task = NULL;
     9 volatile int my_need_sched = 0;
    10 
    11 void my_process(void);
    12 
    13 void __init my_start_kernel(void)
    14 {
    15     int pid = 0;
    16     int i;
    17     /* Initialize process 0*/
    18     task[pid].pid = pid;
    19     task[pid].state = 0; /* -1 unrunnable, 0 runnable, >0 stopped */
    20     task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
    21     task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
    22     task[pid].next = &task[pid];
    23     /*fork more process */
    24     for(i=1;i<MAX_TASK_NUM;i++)
    25     {
    26         memcpy(&task[i],&task[0],sizeof(tPCB));
    27         task[i].pid = i;
    28         task[i].state = -1;
    29         task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
    30         task[i].next = task[i-1].next;
    31         task[i-1].next = &task[i];
    32     }
    33     /* start process 0 by task[0] */
    34     pid = 0;
    35     my_current_task = &task[pid];
    36     asm volatile(
    37         "movq %1,%%rsp
    	"  /* set task[pid].thread.sp to rsp */
    38         "pushq %1
    	"          /* push rbp */
    39         "pushq %0
    	"          /* push task[pid].thread.ip */
    40         "ret
    	"              /* pop task[pid].thread.ip to rip */
    41         :
    42         : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)   /* input c or d mean %ecx/%edx*/
    43     );
    44 }

      进程调度为一个环形队列,每次创建新的进程时,都将该进程插入到队尾,然后将该进程指向第一个进程。

      在mymain.c中添加了my_process函数,用来作为进程的代码模拟一个个进程,只是我们这里采用的是进程运行完一个时间片后主动让出CPU的方式(简单的时间片轮转方式的进程切换),没有采用中断的时机完成进程切换,因为中断机制实现起来较为复杂,等后续部分再逐渐深入。

     1 void my_process(void)
     2 {
     3     int i = 0;
     4     while(1)
     5     {
     6         i++;
     7         if(i%10000000 == 0)
     8         {
     9             printk(KERN_NOTICE "this is process %d -
    ",my_current_task->pid);
    10             if(my_need_sched == 1)
    11             {
    12                 my_need_sched = 0;
    13                 my_schedule();
    14             }
    15             printk(KERN_NOTICE "this is process %d +
    ",my_current_task->pid);
    16         }
    17     }
    18 }

      进程运行过程中是怎么知道时间片消耗完了呢?这就需要时钟中断处理过程中记录时间片。对myinterrupt.c中修改my_timer_handler用来记录时间片。

     1 /*
     2  *  linux/mykernel/myinterrupt.c
     3  */
     4 #include "mypcb.h"
     5 
     6 extern tPCB task[MAX_TASK_NUM];
     7 extern tPCB * my_current_task;
     8 extern volatile int my_need_sched;
     9 volatile int time_count = 0;
    10 
    11 /*
    12  * Called by timer interrupt.
    13  */
    14 void my_timer_handler(void)
    15 {
    16     if(time_count%1000 == 0 && my_need_sched != 1)
    17     {
    18         printk(KERN_NOTICE ">>>my_timer_handler here<<<
    ");
    19         my_need_sched = 1;
    20     }
    21     time_count ++ ;
    22     return;
    23 }

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

     1 void my_schedule(void)
     2 {
     3     tPCB * next;
     4     tPCB * prev;
     5 
     6 
     7     if(my_current_task == NULL
     8         || my_current_task->next == NULL)
     9     {
    10       return;
    11     }
    12     printk(KERN_NOTICE ">>>my_schedule<<<
    ");
    13     /* schedule */
    14     next = my_current_task->next;
    15     prev = my_current_task;
    16     if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
    17     {
    18       my_current_task = next;
    19       printk(KERN_NOTICE ">>>switch %d to %d<<<
    ",prev->pid,next->pid);
    20       /* switch to next process */
    21       asm volatile(
    22          "pushq %%rbp
    	"       /* save rbp of prev */
    23          "movq %%rsp,%0
    	"     /* save rsp of prev */
    24          "movq %2,%%rsp
    	"     /* restore  rsp of next */
    25          "movq $1f,%1
    	"       /* save rip of prev */
    26          "pushq %3
    	"
    27          "ret
    	"               /* restore  rip of next */
    28          "1:	"                  /* next process start here */
    29          "popq %%rbp
    	"
    30         : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
    31         : "m" (next->thread.sp),"m" (next->thread.ip)
    32       );
    33     }
    34     return;
    35 }

      my_time_handler中断处理程序,该函数每隔1000 判断 my_need_sched 是否不等于1,如果是则将其置为1,使 myprocess 执行 my_schedule() 。my_schedule 函数在进程队列中选择下一个要执行的进程;对于处于不同状态的进程,调度方式也不同,如果即将上CPU的进程之前已经运行过(即state为0),我们需要保存当前进程的上下文信息,然后把下一个进程的信息写入到寄存器中,执行 ret 使下一个进程开始执行。之前没有在运行态的(state不为0),我们先将其设置为运行态,我们这里需要初始化其ebp,因为该进程的堆栈是空栈 。

    参考资料:

    1. https://github.com/mengning/mykernel/blob/master/README.md

    2. 计算机系统的基本工作原理

    3. 自己动手写一个操作系统内核

  • 相关阅读:
    Angular JS 中的内置方法之$watch
    Angular JS 中 指令详解
    Angular JS 中 ng-controller 值复制和引用复制
    git 使用技巧
    itextpdf 解析带中文的html问题
    详解Java 8中Stream类型的“懒”加载
    JSP网页处理过程
    [Java 8] (10) 使用Lambda完成函数组合,Map-Reduce以及并行化
    深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
    Android:联系人Contacts之ContentResolver query 参数详解
  • 原文地址:https://www.cnblogs.com/LiScott/p/12871507.html
Copyright © 2020-2023  润新知