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

一、实验环境安装

使用的虚拟机创建软件:VMware Fusion Pro 11.1.0

使用的系统:Ubuntu-18.04.4-desktop-amd64

 

二、实验开始前准备工作

1. 下载孟老师的内核代码库(取出其中的mykernel-2.0_for_linux-5.4.34.patch)

 1 git clone https://github.com/mengning/mykernel 

也可以使用(尝试时一直连接失败)

1 wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch

 

2. 安装axel(下载软件)和Linux内核源码并解压

1 sudo apt install axel
2 axel -n -20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
3 xz -d linux-5.4.34.tar.xz
4 tar -xvf linux-5.4.34.tar

3. 进入内核文件夹,将mykernel-2.0_for_linux-5.4.34.patch的修改同步到代码中

1 patch -p1 < ../mykernel-2.0_for_linux-5.4.34.patch
2 sudo apt install build-essential gcc-multilib

4. 在ubuntu中安装必要的依赖

1 sudo apt install build-essential gcc-multilib
2 sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
3 sudo apt install qemu # install QEMU

5. 编译内核,并使用qemu启动

1 make defconfig
2 make -j$(nproc) #根据对应的cpu核心数调整
3 qemu-system-x86_64 -kernel arch/x86/boot/bzImage

可以看到mymain.c的函数在不断的执行,同时不断产生时钟中断信号,触发myinterrupt.c中的代码。一个具有时钟中断的功能就完成了。 

 

基础准备工作完毕,让我们开始编写一个操作系统内核吧!

 

三、基于mykernel2.0编写一个操作系统内核

1. 进程PCB(进程控制块)是进程重要的数据结构。我们首先实现它。

在mykernel目录下增加一个mypcb.h头文件。

 1 /*
 2  *  linux/mykernel/mypcb.h
 3  *  Kernel internal PCB type
 4  */
 5 
 6 #define MAX_TASK_NUM        4
 7 #define KERNEL_STACK_SIZE   1024*2
 8 /* CPU-specific state of this task */
 9 
10 //存储ip和sp
11 struct Thread {
12     unsigned long        ip; //函数入口指针
13     unsigned long        sp; //栈顶指针
14 };
15 
16 //PCB结构
17 typedef struct PCB{
18     int pid;//进程id
19     volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
20     unsigned long stack[KERNEL_STACK_SIZE];//进程堆栈
21     /* CPU-specific state of this task */
22     struct Thread thread;//线程
23     unsigned long    task_entry;//进程入口地址
24     struct PCB *next;//下一个进程控制块地址
25 }tPCB;
26 
27 void my_schedule(void);//调度函数

2.mymain.c是mykernel代码的入口,负责将各个模块进行组合。

对my_start_kernel进行修改。

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

初始化进程0

3. mymain.c在这里采用了时间片轮转的方式进行进程调度,进程调度的也是根据序号先进先出。

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

4.myinterupt.c 修改 使用my_timer_handler模拟一个时间片的操作

 1 #include <linux/types.h>
 2 #include <linux/string.h>
 3 #include <linux/ctype.h>
 4 #include <linux/tty.h>
 5 #include <linux/vmalloc.h>
 6 
 7 #include "mypcb.h"
 8 
 9 extern tPCB task[MAX_TASK_NUM];
10 extern tPCB * my_current_task;
11 extern volatile int my_need_sched;
12 volatile int time_count = 0;
13 
14 /*
15  * Called by timer interrupt.
16  * it runs in the name of current running process,
17  * so it use kernel stack of current running process
18  */
19 void my_timer_handler(void)
20 {
21     if(time_count%1000 == 0 && my_need_sched != 1)
22     {
23         printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
24         my_need_sched = 1;
25     } 
26     time_count ++ ;  
27     return;      
28 }

5.myinterupt.c进程切换部分(难点&重点)

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

 

四、关键代码分析

前置汇编知识

%1 %2代表末尾:符号后面的按照顺序排列的对应变量。

64系统为8个字节。

1.第一个进程0的启动

1     asm volatile(
2         "movq %1,%%rsp\n\t"     /* set task[pid].thread.sp to rsp */
3         "pushq %1\n\t"             /* push rbp */
4         "pushq %0\n\t"             /* push task[pid].thread.ip */
5         "ret\n\t"                 /* pop task[pid].thread.ip to rip */
6         : 
7         : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)    /* input c or d mean %ecx/%edx*/
8     );

 

movq %1,%%rsp  将rsp寄存器指向进程0的堆栈栈底,task[pid].thread.sp的初始值就是堆栈栈底;

pushq %1            将当前rsp寄存器的值压栈,进程的堆栈栈顶的值task[pid].thread.sp,rsp=rsp-8;

pushq %0            将当前进程的rip入栈,相应的rsp寄存器指向的位置也发生了变化。rsp=rsp-8;

1 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;

ret                      将栈顶位置task[0]. thread.ip,即my_process(void)函数的地址放入rip寄存器,rsp=rsp+8;

 

2.进程的切换

 1         asm volatile(    
 2             "pushq %%rbp\n\t"         /* save rbp of prev */
 3             "movq %%rsp,%0\n\t"     /* save rsp of prev */
 4             "movq %2,%%rsp\n\t"     /* restore  rsp of next */
 5             "movq $1f,%1\n\t"       /* save rip of prev */    
 6             "pushq %3\n\t" 
 7             "ret\n\t"                 /* restore  rip of next */
 8             "1:\t"                  /* next process start here */
 9             "popq %%rbp\n\t"
10             : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
11             : "m" (next->thread.sp),"m" (next->thread.ip)
12         ); 

 

pushq %%rbp   保存prev进程的rbp的值,入栈

movq %%rsp,%0  当前rsp寄存器的值到prev->thread.sp,这时rsp寄存器指向进程的栈顶地址,也就是保存prev进程栈顶地址

movq %2,%%rsp  将next进程的栈顶地址放入rsp寄存器,完成进程0和进程1的堆栈切换

movq $1f,%1      保存prev进程当前rip寄存器的值到prev->thread.ip,$1f指标号1

pushq %3            把即将执行的next进程的指令地址next->thread.ip入栈

ret                    将压入栈的next->thread.ip放入rip寄存器(此时执行1:)

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

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

 

五、总体流程分析

系统启动后,运行mymain.c中的my_start_kernal, 进程0首先初始化,再初始化其他的进程。

my_process中一直查询my_need_sched是否为1,为1则在函数中调用my_schedule,切换到下一个进程

my_timer_handler当count到1000时,把my_need_sched置为1

 

六、总结

通过实现mykernel的操作系统内核,对Linux的启动和调度切换有了更深的理解。尤其是切换时堆栈的变化,加深了对汇编语言的理解。

 

posted @ 2020-05-12 13:15  USTC老虞  阅读(296)  评论(0)    收藏  举报