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

1、配置实验环境

本机的环境是Ubuntu20.04 LTS物理机,具体如下

新建文件夹mykernel2.0,右键打开终端,按照老师给的代码配置环境

 1 wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch
 2 sudo apt install axel
 3 axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
 4 xz -d linux-5.4.34.tar.xz
 5 tar -xvf linux-5.4.34.tar
 6 cd linux-5.4.34
 7 patch -p1 < ../mykernel-2.0_for_linux-5.4.34.patch
 8 sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev
 9 make defconfig
10 make -j$(nproc)
11 sudo apt install qemu12 qemu-system-x86_64 -kernel arch/x86/boot/bzImage

这里配置使用的是defconfig,有关allnoconfig的问题放在后面讨论。

配置成功并使用qemu运行后的结果如下图所示,可以看到时钟中断my_timer_handler在周期性工作。

2、编写进程控制块和中断调度模块

打开mykernel文件夹下的mymain.c以及myinterrupt.c文件,可以看到里面的代码有如下几段:

 1 # mymain.c
 2 void __init my_start_kernel(void)
 3 {
 4     int i = 0;
 5     while(1)
 6     {
 7         i++;
 8         if(i%100000 == 0)
 9             pr_notice("my_start_kernel here  %d \n",i);
10             
11     }
12 }
1 # myinterrupt.c
2 void my_timer_handler(void)
3 {
4     pr_notice("\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n");
5 }

mymain.c中是一个死循环,不断输出"my_start_kernel here %d \n",myinterrupt.c中则不断输出时钟中断"my_timer_handler here"。而根据之前qemu的运行结果,可以看出进程和时钟中断不断的交替运行。而我们要做的事情就是写一个自己的进程控制块以及进程调度算法,从而实现模拟多个进程调度运行。

在mykernel目录下建立mypcb.h头文件,该头文件定义了进程控制块的数据结构,代码如下:

 1 #define MAX_TASK_NUM        4
 2 #define KERNEL_STACK_SIZE   1024*8                 //进程堆栈大小
 3 
 4 /* CPU-specific state of this task */
 5 struct Thread {                        //存储ip,sp
 6     unsigned long ip;
 7     unsigned long sp;
 8 };
 9 
10 typedef struct PCB{
11     int pid;                //进程的id
12     volatile long state;    //表示进程的状态,-1表示就绪状态,0表示运行状态,1表示阻塞状态
13     char stack[KERNEL_STACK_SIZE];    //内核堆栈
14     /* CPU-specific state of this task */
15     struct Thread thread;
16     unsigned long task_entry;        //指定的进程入口,平时入口为main函数
17     struct PCB *next;                //指向下一个进程控制块的指针,进程控制块间用链表连接
18 }tPCB;
19 
20 void my_schedule(void);         //函数调度

 mypcb.h头文件规定了进程控制块的数据结构,例如进程的pid,进程的状态,进程的指定入口等。

接着打开mymain.c文件,修改其内容如下:

 1 #include "mypcb.h"
 2 
 3 tPCB task[MAX_TASK_NUM];               //声明tPCB类型的数组
 4 tPCB * my_current_task = NULL;           //声明当前task的指针
 5 volatile int my_need_sched = 0;        //是否需要调度
 6 
 7 void my_process(void);
 8 
 9 
10 void __init my_start_kernel(void)
11 {
12     int pid = 0;
13     int i;
14     /* Initialize process 0*/
15     task[pid].pid = pid;         //初始化0号进程
16     task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped ,状态正在运行*/
17     task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;    //入口
18     task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];//
19     task[pid].next = &task[pid];    //指向自己,系统启动只有0号进程
20     /*fork more process */
21     22     for(i=1;i<MAX_TASK_NUM;i++)
23     {24         memcpy(&task[i],&task[0],sizeof(tPCB));25         task[i].pid = i;26         task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
27         task[i].next = task[i-1].next;  //新进程加到进程链表尾部
28         task[i-1].next = &task[i];
29     }
30     /* start process 0 by task[0] */
31     pid = 0;
32     my_current_task = &task[pid];
33     asm volatile(
34         "movq %1,%%rsp\n\t"  /* set task[pid].thread.sp to rsp */
35         "pushq %1\n\t"          /* push rbp */
36         "pushq %0\n\t"          /* push task[pid].thread.ip */
37         "ret\n\t"              /* pop task[pid].thread.ip to rip */
38         :
39         : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)   /* input c or d mean %ecx/%edx*/
40     );
41 }   
42 int i = 0;43 void my_process(void)
44 {  45     while(1)
46     {
47         i++;
48         if(i%10000000 == 0)        //循环1000万次判断是否需要调度
49         {
50             printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
51             if(my_need_sched == 1)
52             {
53                 my_need_sched = 0;
54                 my_schedule();
55             }
56             printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
57         }     
58     }
59 }

上述函数完成了对零号进程的初始化,包括设置零号进程的pid=0,进程当前的状态为阻塞,进程的入口等参数。接着将零号进程添加到进程链表的尾部,并且以0号进程为模板复制了MAX_TASK_NUM-1个进程构成链表。接着是一段汇编代码,该段汇编第一行是将task[0].thread.sp拿去修改rsp的值,这时候内核堆栈的栈顶被修改到了task[0]的sp位置,接着在task[0]的sp位置处压入rbp的值,来保护原来的内核堆栈,然后把task[0].thread.ip的值给rip,这样就能够保证cpu下一步能够执行0号进程,完成了进入my_process()的过程。注意此时rip的值已经被ret修改,因此cpu开始执行my_process。

最后修改myinterrupt.c里的代码,如下:

 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 }
29 
30 void my_schedule(void)   //进程切换
31 {
32     tPCB * next;
33     tPCB * prev;
34 
35     if(my_current_task == NULL 
36         || my_current_task->next == NULL)
37     {
38         return;
39     }
40     printk(KERN_NOTICE ">>>my_schedule<<<\n");
41     /* schedule */
42     next = my_current_task->next;
43     prev = my_current_task;
44     if(next->state == 0)/* -1 unrunnable,0 runnable,1 stopped 根据下一个进程的状态来判断是否切换*/
45     {        
46         my_current_task = next; 
47         printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);  
48         /* switch to next process */
49         asm volatile(    
50             "pushq %%rbp\n\t"         /* save rbp of prev */
51             "movq %%rsp,%0\n\t"     /* save rsp of prev */
52             "movq %2,%%rsp\n\t"     /* restore  rsp of next */
53             "movq $1f,%1\n\t"       /* save rip of prev ,%1f指接下来的标号为1的位置*/    
54             "pushq %3\n\t" 
55             "ret\n\t"                 /* restore  rip of next */
56             "1:\t"                  /* next process start here */
57             "popq %%rbp\n\t"
58             : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
59             : "m" (next->thread.sp),"m" (next->thread.ip)
60         ); 
61     }  
62     return;    
63 }

 

 

进程切换的时间片在my_timer_handler中给出。在my_schedule函数中,首先会判断当前任务或当前任务的下一个任务是否为空,若为空则直接返回。接着对next和prev变量进行赋值,next为当前进程的下一个而prev为当前进程。接着判断next进程的状态是否为运行态,若已经是运行态则保存现场并切换至next进程。该函数中最重要的部分即是内联汇编的部分,这里对内联汇编的代码作详细的解释:

pushq %%rbp\n\t:保存prev进程的rbp到堆栈

movq %%rsp,%0\n\t:保存prev进程的rsp,此时rsp指向prev的栈顶,因此相当于保存了prev进程的栈顶。

movq %2,%%rsp\n\t:把next进程的栈顶地址放入rsp,相当于完成了进程栈顶的切换。

movq $1f,%1\n\t:保存prev进程的rip。

pushq %3\n\t:入栈next进程的指令起始地址next->thread.ip,如果是第一次执行,则是my_process函数的入口地址,否则是$1f地址,即上次的执行位置。

ret\n\t:将上一步中的next->thread.ip赋值给rip寄存器。

1:\t:代表next进程的起始地址。

popq %%rbp\n\t:将next进程基地址出栈并放入rbp中。

3、运行结果演示

重新make后运行,结果如下所示:

可以看到从进程1切换到了进程2。

4、关于make allnoconfig

做完以上实验后我试着使用make allnoconfig编译,关于allnoconfig,网上的说明是该配置是linux内核的最小配置,即除了必要的选项外其余一律不选,因此此种配置方式编译最快。

打开defconfig和allnoconfig配置结果进行对比,

defconfig部分配置结果:

allnoconfig部分配置结果:

可以看到allnoconfig是以32位i386为基础进行编译的,而我们的汇编代码都是64位的,因此这里可能产生冲突导致无法运行。

allnoconfig配置下继续实验:

编译时出现了以下提醒:

可以看到编译器提示了60位寄存器以及pushq等操作的错误,这里无视之,等待编译完成后继续执行:

神奇的事情发生了,我这里居然执行成功了:

(这里截屏技术不好没截到切换进程的瞬间)

我初步思考了一下,怀疑可能是我之前make defconfig后没有清理编译结果直接make allnoconfig,于是allnoconfig遇到错误后停止该块的编译,因此含有64位汇编代码的片段依旧是defconfig的编译结果。于是我make clean后重新编译,结果如下:

这次同样遇到了64位汇编代码不兼容的问题:

继续无视之。

发现无法编译成功:

遂将mymain和myinterrupt中的汇编代码换为32位汇编并重新make之:

这次编译成功:

运行内核,发现会停在这一步没有反应:

由于config选项过于复杂,目前并没有找到错误在哪。

5、总结

通过这次实验,我深刻的理解了linux内核的进程调度过程及其上下文切换的原理,同时也对手工编译linux内核有了一些粗浅的体验,同时也了解了编译内核的各种选项的含义。总而言之收获颇多。

 

posted @ 2020-05-10 11:11  darz233  阅读(215)  评论(0编辑  收藏  举报