基于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. 配置前先拍快照,保存当前状态备用。

基于mykernel 2.0编写一个操作系统内核--课程实验1_第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方法,如下图所示。

基于mykernel 2.0编写一个操作系统内核--课程实验1_第2张图片

        在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 \n",i);
    }
}

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

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

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

  参照 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

基于mykernel 2.0编写一个操作系统内核--课程实验1_第3张图片

  观察可知上图中正在进行进程的切换:进程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)
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\n\t"  /* set task[pid].thread.sp to rsp */
38         "pushq %1\n\t"          /* push rbp */
39         "pushq %0\n\t"          /* push task[pid].thread.ip */
40         "ret\n\t"              /* 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 -\n",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 +\n",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<<<\n");
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<<<\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 }

  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. 自己动手写一个操作系统内核

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