基于mykernel 2.0编写一个操作系统内核

实验要求:

1.按照https://github.com/mengning/mykernel 的说明配置mykernel 2.0,熟悉Linux内核的编译;

2.基于mykernel 2.0编写一个操作系统内核,参照https://github.com/mengning/mykernel 提供的范例代码

3.简要分析操作系统内核核心功能及运行工作机制

 

实验环境:

本次实验使用虚拟机Vmware15,Ubuntu18.01操作系统。

 

实验原理:

本次实验主要是通过mykernel虚拟一个x86-64的CPU硬件平台,并在其中触发时钟中断,通过时钟中断计数实现时间片调度,完成模拟一个能进行简单进程调度的内核编写。

从而了解Linux系统内核核心功能和运行工作机制。下面介绍Linux系统中中断的概念。

中断:

中断概述
中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。

中断类型

同步中断由CPU本身产生,又称为内部中断。这里同步是指中断请求信号与代码指令之间的同步执行,在一条指令执行完毕后,CPU才能进行中断,不能在执行期间。所以也称为异常(exception)。

异步中断是由外部硬件设备产生,又称为外部中断,与同步中断相反,异步中断可在任何时间产生,包括指令执行期间,所以也被称为中断(interrupt)。

异常又可分为可屏蔽中断(Maskable interrupt)和非屏蔽中断(Nomaskable interrupt)。而中断可分为故障(fault)、陷阱(trap)、终止(abort)三类。

本次实验中实现的时钟中断属于异步中断,当然这里的中断是由软件模拟的,并不是真的使用硬件信号产生。

 

实验流程:

1.实验环境的搭建和内核的下载:
按照顺序依次输入下列命令:

wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.3.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.3.34.patch
sudo apt install build-essential gcc-multilib
sudo apt install qemu # install QEMU
sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
make defconfig # Default configuration is based on 'x86_64_defconfig'
make -j$(nproc)
qemu-system-x86_64 -kernel arch/x86/boot/bzImage

按照实验流程,将会在qemu虚拟机中产生一个x86-64的CPU硬件平台,它将定时产生时钟中断,实验效果如下:

基于mykernel 2.0编写一个操作系统内核_第1张图片

 

 在这个基础之上,我们将继续进行内核的编写工作。

2.在mykernel目录下进行程序的编写:

2. 1 第一步,我们必须定义进程控制块(Process Control Block),也就是进程结构体的定义,在Linux内核中是struct tast_struct结构体。将其定义在mypcb.h头文件中,头文件的代码如下:

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

 

2.2 第二步,修改该目录下的mymain.c文件,声明多个进程,并启动0号进程,以及编写my_process()函数模拟主程序,在该主程序中,每次打印出进程编号和配合时钟中断进行进程的切换。

张贴出具体代码:

  1 /*
  2  *  linux/mykernel/mymain.c
  3  *
  4  *  Kernel internal my_start_kernel
  5  *
  6  *  Copyright (C) 2013  Mengning
  7  *
  8  */
  9 #include 
 10 #include 
 11 #include 
 12 #include 
 13 #include 
 14 #include 
 15 #include string.h>
 16 #include 
 17 #include 
 18 #include 
 19 #include 
 20 #include 
 21 #include 
 22 #include 
 23 #include 
 24 #include 
 25 #include 
 26 #include 
 27 #include 
 28 #include 
 29 #include 
 30 #include 
 31 #include 
 32 #include 
 33 #include 
 34 #include 
 35 #include 
 36 #include 
 37 #include 
 38 #include 
 39 #include 
 40 #include 
 41 #include 
 42 #include 
 43 #include 
 44 #include 
 45 #include 
 46 #include 
 47 #include 
 48 #include 
 49 #include 
 50 #include 
 51 #include 
 52 #include 
 53 #include 
 54 #include 
 55 #include 
 56 #include 
 57 #include 
 58 #include 
 59 #include 
 60 #include 
 61 #include 
 62 #include 
 63 #include 
 64 #include 
 65 #include 
 66 #include 
 67 #include 
 68 #include 
 69 
 70 #include 
 71 #include 
 72 #include 
 73 #include 
 74 #include 
 75 
 76 #ifdef CONFIG_X86_LOCAL_APIC
 77 #include 
 78 #endif
 79 
 80 
 81 /*
 82  *  linux/mykernel/mymain.c
 83  */
 84  
 85 #include "mypcb.h"
 86 
 87 
 88 tPCB task[MAX_TASK_NUM];
 89 tPCB * my_current_task = NULL;
 90 volatile int my_need_sched = 0;
 91 
 92 
 93 void my_process(void);
 94 
 95 
 96 void __init my_start_kernel(void)
 97 {
 98     int pid = 0;
 99     int i;
100     /* Initialize process 0*/
101     task[pid].pid = pid;
102     task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
103     task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
104     task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
105     task[pid].next = &task[pid];
106     /*fork more process */
107     for(i=1;i)
108     {
109         memcpy(&task[i],&task[0],sizeof(tPCB));
110         task[i].pid = i;
111         task[i].state = 0;
112         task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
113         task[i].next = task[i-1].next;
114         task[i-1].next = &task[i];
115     }
116     /* start process 0 by task[0] */
117     pid = 0;
118     my_current_task = &task[pid];
119     asm volatile(
120         "movq %1,%%rsp\n\t"  /* set task[pid].thread.sp to rsp */
121         "pushq %1\n\t"          /* push rbp */
122         "pushq %0\n\t"          /* push task[pid].thread.ip */
123         "ret\n\t"              /* pop task[pid].thread.ip to rip */
124         :
125         : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)   /* input c or d mean %ecx/%edx*/
126     );
127 }
128 
129 
130 void my_process(void)
131 {
132     int i = 0;
133     while(1)
134     {
135         i++;
136         if(i%10000000 == 0)
137         {
138             printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
139             if(my_need_sched == 1)
140             {
141                 my_need_sched = 0;
142                 my_schedule();
143             }
144             printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
145         }
146     }
147 }

可以看到在my_process中有一个进程切换标志位:my_need_sched,在该标志位为1时进行进程切换,那么这个全局变量又是如何改变的呢?上文已经交代过,这是通过时钟中断来改变的,即通过改写中断服务程序来定时改变标志位,起到时间片调度的效果。下面将分析中断服务程序的更改:

2.3 更改中断服务程序myinterrupt.c

张贴出相应代码:

 1 void my_timer_handler(void)
 2 {
 3     if(time_count%1000 == 0 && my_need_sched != 1)
 4     {
 5         printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
 6         my_need_sched = 1;
 7     }
 8     time_count ++ ;
 9     return;
10 }

实现了时间片,下面就要编写最重要的进程切换代码了,在头文件中,该部分代码已经被定义成void my_schedule(void)函数,下面具体介绍该函数:

 

2.4 进程切换函数:

首先张贴出该函数的代码;

 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<<<\n");
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<<<\n",prev->pid,next->pid);
20       /* switch to next process */
21       asm volatile(
22          "pushq %%rbp\n\t"       /* save rbp of prev */
23          "movq %%rsp,%0\n\t"     /* save rsp of prev */
24          "movq %2,%%rsp\n\t"     /* restore  rsp of next */
25          "movq $1f,%1\n\t"       /* save rip of prev */
26          "pushq %3\n\t"
27          "ret\n\t"               /* restore  rip of next */
28          "1:\t"                  /* next process start here */
29          "popq %%rbp\n\t"
30         : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
31         : "m" (next->thread.sp),"m" (next->thread.ip)
32       );
33     }
34     return;
35 }

其中最最关键的就是中间的那段汇编代码

为了简便,这里假设由进程0切换到进程1,进程切换过程中进程0和进程1的堆栈和相关寄存器的变化过程大致如下:

  • pushq %%rbp  保存prev进程(本例中指进程0)当前RBP寄存器的值到prev进程的堆栈;

  • movq %%rsp,%0 保存prev进程(本例中指进程0)当前RSP寄存器的值到prev->thread.sp,这时RSP寄存器指向进程的栈顶地址,实际上就是将prev进程的栈顶地址保存;%0、%1...指这段汇编代码下面输入输出部分的编号。

  • movq %2,%%rsp 将next进程的栈顶地址next->thread.sp放入RSP寄存器,完成了进程0和进程1的堆栈切换。

  • movq $1f,%1 保存prev进程当前RIP寄存器值到prev->thread.ip,这里$1f是指标号1。

  • pushq %3 把即将执行的next进程的指令地址next->thread.ip入栈,这时的next->thread.ip可能是进程1的起点my_process(void)函数,也可能是$1f(标号1)。第一次被执行从头开始为进程1的起点my_process(void)函数,其余的情况均为$1f(标号1),因为next进程如果之前运行过那么它就一定曾经也作为prev进程被进程切换过。

  • ret 就是将压入栈中的next->thread.ip放入RIP寄存器,为什么不直接放入RIP寄存器呢?因为程序不能直接使用RIP寄存器,只能通过call、ret等指令间接改变RIP寄存器。

  • 1: 标号1是一个特殊的地址位置,该位置的地址是$1f。

  • popq %%rbp 将next进程堆栈基地址从堆栈中恢复到RBP寄存器中。

到这里开始执行进程1了,如果进程1执行的过程中发生了进程调度和进程切换,进程0重新被调度执行了,就是从进程1再切换到进程0,prev进程变成了进程1,而next进程变成进程0。

 

3.重新编译并通过虚拟机演示实验结果:

基于mykernel 2.0编写一个操作系统内核_第2张图片

 

 可以看到编写的内核成功完成了从进程2像进程3的调度,实验至此结束。

 

 

 

 

 

你可能感兴趣的:(基于mykernel 2.0编写一个操作系统内核)