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

1.首先是配置mykernel 2.0,并熟悉Linux内核的编译

  本机环境是VirtualBoxVM6.0.20+Ubuntu 16.04.2,首先是按照孟老师在README.md上面提供的指导,依次配置好linux5.4.34内核代码、mykernel补丁、相对应的依赖库文件和qemu仿真系统,以下是课程PPT里面的命令,逐条执行即可:

 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 gcc-multilib 
 9 sudo apt install qemu # install QEMU 
10 sudo apt install libncurses5-dev bison flflex libssl-dev libelf-dev 
11 make defconfifig # Default confifiguration is based on 'x86_64_defconfifig' 
12 make -j$(nproc) 
13 qemu-system-x86_64 -kernel arch/x86/boot/bzImage

在本机环境下,linux-5.4.34内核源代码位于目录/home/hesetone/mylinux/linux-5.4.34位置。

  在上面的命令中,axel是一个多线程高速下载命令,支持多种文件传输协议,可以从多个地址或者与一个地址建立多个连接来下载同一个文件。第10行的install目录中,README.md里面的libncurses-dev似乎是不起作用的,执行sudo apt install libncurses-dev之后,qemu启动的时候,会莫名其妙卡住崩溃,无法正常模拟出结果,但是换成libncurses5-dev之后就可以正常复现。

  其次,make defconfig命令的作用是按照默认的配置文件.arch/arm64/configs/defconfig对内核进行配置,在当前目录下生成.config用作初始化配置。用ls -a命令可以在编译完成配置文件之后查看到该文件。最后,make -j$(nproc)命令作用是编译内核,这里nproc是一个内核参数,指系统上的最大进程数。$(nproc)是获取安装系统的该内核参数,指定该参数,完成内核编译。

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

执行qemu-system-x86_64 -kernel arch/x86/boot/bzImage正常复现的结果如下:

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

2.基于mykernel 2.0编写一个操作系统内核,实现进程的切换。

  第一步是在目录./mylinux/linux-5.4.34/mykernel/目录下增加一个头文件mypcb.h,并且在mymain.c和myinterrupt.c中都include进去,它用于定义进程控制块的结构,后面会有各个进程控制块内容的初始化步骤,在Linux内核中,实际上是struct task_struct结构体。在struct PCB中,包含进程标识符pid、进程状态state(-1表示阻塞状态,0表示正在运行中,大于0则表示停止执行)、栈空间stack、指示CPU特征状态(sp和ip)的线程结构体thread、进程入口task_entry以及指向下一个进程的指针*next,最后的指针,说明进程是以链表的形式组织起来的,mypcb.h内容如下。

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

  第二步是对mymain.c进⾏修改,mymain.c是mykernel内核代码的⼊⼝,我们在这里对内核的各个组成部分进行初始化操作,在函数my_start_kernel中,首先是初始化第0个进程,然后开始对1~MAX_TASK_NUM中的进程依次进行初始化,其中进程状态初始化为0,表示所有进程都处在就绪状态等待调用,且所有的进程初始化之后成为一个循环链表。在mymain.c中添加了my_process函数,用于模拟进程执行的过程。此外,my_need_sched变量初始化为0,但是因为my_time_handler函数会定期更新其为1,表示需要调度新的进程,调度完毕之后,重新切换为0,执行1个时间片,依次循环,mymain.c内容如下:

 1 #include "mypcb.h"
 2 
 3 tPCB task[MAX_TASK_NUM];       /*task数组*/
 4 tPCB *my_current_task = NULL;    /*指向当前进程的指针8*/
 5 volatile int my_need_sched = 0;   /*是否需要调度,1表示需要,0表示不需要*/
 6 
 7 void __init my_start_kernel(void)
 8 {
 9     int pid = 0;
10     int i;
11     /* Initialize process 0*/
12     task[pid].pid = pid;
13     task[pid].state = 0; /* -1 unrunnable, 0 runnable, >0 stopped */
14     task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
15     task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE - 1];
16     task[pid].next = &task[pid];
17     /*fork more process */
18     for (i = 1; i < MAX_TASK_NUM; i++)
19     {
20         memcpy(&task[i], &task[0], sizeof(tPCB));
21         task[i].pid = i;
22         task[i].state = 0;
23         task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE - 1];
24         task[i].next = task[i - 1].next;
25         task[i - 1].next = &task[i];
26     }
27     /* start process 0 by task[0] */
28     pid = 0;
29     my_current_task = &task[pid];
30     asm volatile(
31         "movq %1,%%rsp\n\t" /* set task[pid].thread.sp to rsp */
32         "pushq %1\n\t"      /* push rbp */
33         "pushq %0\n\t"      /* push task[pid].thread.ip */
34         "ret\n\t"           /* pop task[pid].thread.ip to rip */
35         :
36         : "c"(task[pid].thread.ip), "d"(task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
37     );
38 }
39 
40 void my_process(void)
41 {
42     int i = 0;
43     while (1)
44     {
45         i++;
46         if (i % 10000000 == 0)
47         {
48             printk(KERN_NOTICE "this is process %d -\n", my_current_task->pid);
49             if (my_need_sched == 1)  /*表示需要从当前进程切换到下一个进程*/
50             {
51                 my_need_sched = 0;   /*重新置0,执行一个时间片*/
52                 my_schedule();     /*切换进程*/
53             }
54             printk(KERN_NOTICE "this is process %d +\n", my_current_task->pid);
55         }
56     }
57 }

  第三步是myinterrupt.c的内容,进程运⾏过程使用时钟中断处理过程记录时间⽚,每次time_count % 1000 == 0的时候,就置my_need_sched为1,表示当前进程时间片用完,需要调度下一个进程。当next->state为0的时候,表示下一个进程可以被调度,只要先将指向当前进程的指针指向下一个进程,然后保存上一个进程的rsp和rip,重新设置rsp和rip指向当前进程的栈空间,就完成了进程的切换。对myinterrupt.c中修改如下:

 1 extern tPCB task[MAX_TASK_NUM];
 2 extern tPCB *my_current_task;
 3 extern volatile int my_need_sched;
 4 volatile int time_count = 0;
 5 
 6 /*
 7  * Called by timer interrupt.
 8  */
 9 void my_timer_handler(void)
10 {
11     if (time_count % 1000 == 0 && my_need_sched != 1)
12     {
13         printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
14         my_need_sched = 1;      /*执行一个时间片之后,重新置my_need_sched为1,因为当前进程时间片用完,需要调度下一个进程*/
15     }
16     time_count++;
17     return;
18 }
19 
20 void my_schedule(void)
21 {
22     tPCB *next;
23     tPCB *prev;
24     printk(KERN_NOTICE ">>>my_schedule<<<\n");
25     /* schedule */
26     next = my_current_task->next;
27     prev = my_current_task;
28     if (next->state == 0) /* -1 unrunnable, 0 runnable, >0 stopped */
29     {
30         my_current_task = next;    /*将当前进程指针指向下一个进程,表示调度下一个进程*/
31         printk(KERN_NOTICE ">>>switch %d to %d<<<\n", prev->pid, next->pid);
32         /* switch to next process */
33         asm volatile(
34             "pushq %%rbp\n\t"   /* save rbp of prev */
35             "movq %%rsp,%0\n\t" /* save rsp of prev */
36             "movq %2,%%rsp\n\t" /* restore  rsp of next */
37             "movq $1f,%1\n\t"   /* save rip of prev */
38             "pushq %3\n\t"
39             "ret\n\t" /* restore  rip of next */
40             "1:\t"    /* next process start here */
41             "popq %%rbp\n\t"
42             : "=m"(prev->thread.sp), "=m"(prev->thread.ip)
43             : "m"(next->thread.sp), "m"(next->thread.ip)
44         );
45     }
46     return;
47 }

  最后,修改完毕,重新执行make命令,编译内核,得到进程切换成功的复现结果如下:

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

  操作系统内核一个核心的功能是进程管理,包括进程的创建与销毁、处理与外部设备的联系、进程间通信等等,最主要的进程管理操作主要有实现进程间的切换执行、对各种设备的调度使用以及CPU的共享,以下以进程调度为例阐述进程切换机制:

  首先是第一个进程启动执行的步骤,右侧是该过程的栈空间以及对应寄存器值变化图:

1 asm volatile(
2     "movq %1,%%rsp\n\t" /* 将进程原堆栈栈顶的地址存⼊RSP寄存器 */
3     "pushq %1\n\t"      /* 将当前RBP寄存器值压栈 */
4     "pushq %0\n\t"      /* 将当前进程的RIP压栈 */
5     "ret\n\t"           /* ret命令正好可以让压栈的进程RIP保存到RIP寄存器中 */
6     :
7     : "c"(task[pid].thread.ip), "d"(task[pid].thread.sp)
8 );

 

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

  其次是进程的切换,核心代码如下:

 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 );
  第一部分,是保存prev进程的RSP和RBP值。执行pushq %%rbp 保存prev进程当前RBP寄存器的值到堆栈,与此同时,RSP=RSP-8,执行movq %%rsp,%0 保存prev进程当前RSP寄存器的值到prev->thread.sp,这时RSP寄存器指向进程的栈顶地址,所以thread.sp=(rsp)。
  第二部分,movq %2,%%rsp 将next进程的栈顶地址next->thread.sp放⼊RSP寄存器,等于是现在RSP指向next进程的栈顶,完成了进程prev和进程next的堆栈空间切换。
  第三部分,movq $1f,%1 保存prev进程当前RIP寄存器的值到prev->thread.ip,这⾥$1f是指标号1。 
  第四部分,pushq %3 把即将执⾏的next进程的指令地址next->thread.ip⼊栈,说明下一步执行的将是next进程的指令内容,但是这时的next->thread.ip可能是进程next的起点my_process(void)函数,也可能是$1f(标号1)。事实上,因为my_process(void)函数使用了一个while(1)无限循环,只有第⼀次被执⾏从头开始执行第1个进程的指令时,起点会是my_process(void)函数的地址,其余的情况均为$1f(标号1)。
  第五部分,ret 就是将压⼊栈中的next->thread.ip放⼊RIP寄存器,因为程序不能直接使⽤RIP寄存器,只能通过call、ret等指令间接改变RIP寄存器,所以必须要先pushq %3,经过压栈之后,再通过ret指令间接取出。 
  第六部分,popq %%rbp 将next进程堆栈基地址从堆栈中恢复到RBP寄存器中,此时RBP寄存器存储next进程栈底的地址,RSP存储next->thread.ip的值,也就是下一条要执行的指令地址,至此,prev进程的执行现场保存完毕,RBP和RSP切换到next进程。

 

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