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

1.实验要求

  • 按照https://github.com/mengning/mykernel 的说明配置mykernel 2.0,熟悉Linux内核的编译;
  • 基于mykernel 2.0编写一个操作系统内核,参照https://github.com/mengning/mykernel提供的范例代码;
  • 简要分析操作系统内核核心功能及运行工作机制。

2.实验环境

3.实验步骤

  • 在Ubuntu的终端依次执行,如下命令。
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

注意:第一条命令执行不成功,我直接从孟老师github上下载 

配置成功便能输入以下命令运行内核

qemu-system-x86_64 -kernel arch/x86/boot/bzImage

得到运行结果

4.简单分析

有两条输出语句交替打印,进入mymain.c,看到

void __init my_start_kernel(void)
{
    int i = 0;
    while(1)
    {
        i++;
        if(i%100000 == 0)
            pr_notice("my_start_kernel here  %d 
",i);
            
    }
}

可知,在循环打印中,my_start_kernel here... ...这条消息是由进程运行mymain.c时进行打印的。再打开myinterrupt.c,能够看到如下代码

void my_timer_handler(void)
{
    pr_notice("
>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<

");
}

循环打印里的另一段就是由进程执行到此处打印的

mykernel能够周期性的产生时钟中断,中断处理程序就会调用my_timer_handler函数,调用完成后再返回到原来的上下文中(mymain.c的循环处),就会产生交替打印的效果。

再看mypcb.h文件

对于一个进程所涉及的资源和控制信息,我们将其统一置于结构体PCB中。主要字段为进程id、进程状态、函数调用栈、代码入口以及ip、sp等。我们还可以把ip、sp进一步抽象为Thread结构体。每一个PCB都是链表中的一个节点,故还需要一个next字段,这样多个进程可以链接成为进程队列。在结构体PCB中可以看到进程有三种状态:unrunnable、runnable和stopped;此外每个进程都拥有自己的堆栈,并由ip、sp(对应eip寄存器和esp寄存器)进行控制。pcb块间以链表的形式串联起来。

5.修改mymain.c的my_start_kernel函数

#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>

#include "mypcb.h"

extern tPCB task[MAX_TASK_NUM];
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;

/*
 * Called by timer interrupt.
 * it runs in the name of current running process,
 * so it use kernel stack of current running process
 */
void my_timer_handler(void)
{
    if(time_count%1000 == 0 && my_need_sched != 1)
    {
        printk(KERN_NOTICE ">>>my_timer_handler here<<<
");
        my_need_sched = 1;
    } 
    time_count ++ ;  
    return;      
}

void my_schedule(void)
{
    tPCB * next;
    tPCB * prev;

    if(my_current_task == NULL 
        || my_current_task->next == NULL)
    {
        return;
    }
    printk(KERN_NOTICE ">>>my_schedule<<<
");
    /* schedule */
    next = my_current_task->next;
    prev = my_current_task;
    if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
    {        
        my_current_task = next; 
        printk(KERN_NOTICE ">>>switch %d to %d<<<
",prev->pid,next->pid);  
        /* switch to next process */
        asm volatile(    
            "pushq %%rbp
	"         /* save rbp of prev */
            "movq %%rsp,%0
	"     /* save rsp of prev */
            "movq %2,%%rsp
	"     /* restore  rsp of next */
            "movq $1f,%1
	"       /* save rip of prev */    
            "pushq %3
	" 
            "ret
	"                 /* restore  rip of next */
            "1:	"                  /* next process start here */
            "popq %%rbp
	"
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
            : "m" (next->thread.sp),"m" (next->thread.ip)
        ); 
    }  
    return;    
}

新增的my_schedule函数是处理进程调度的关键。上文已经说过,pcb块以链表的形式串联起来,my_schedule函数选择进程链表中的下一个就绪进程进行切换。它也内嵌了汇编代码,实现的功能如下:

  1. 保存进程rbp寄存器内的值

  2. 保存进程rsp寄存器内的值

  3. 更新寄存器rsp为next指向的新进程内的sp变量值,此时进行了进程间栈帧的转换

  4. 保存原进程rip寄存器内的值

  5. 更新寄存器rip为新进程的ip变量值,至此进程调度完毕,切换到了新进程运行

6.修改myinterrupt.c的my_timer_handler函数并实现具体的my_schedule调度函数

/*
 * Called by timer interrupt.
 * it runs in the name of current running process,
 * so it use kernel stack of current running process
 */
void my_timer_handler(void)
{
    if(time_count%1000 == 0 && my_need_sched != 1)
    {
        printk(KERN_NOTICE ">>>my_timer_handler here<<<
");
        my_need_sched = 1;
    } 
    time_count ++ ;  
    return;      
}

void my_schedule(void)
{
    tPCB * next;
    tPCB * prev;

    if(my_current_task == NULL 
        || my_current_task->next == NULL)
    {
        return;
    }
    printk(KERN_NOTICE ">>>my_schedule<<<
");
    /* schedule */
    next = my_current_task->next;
    prev = my_current_task;
    if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
    {        
        my_current_task = next; 
        printk(KERN_NOTICE ">>>switch %d to %d<<<
",prev->pid,next->pid);  
        /* switch to next process */
        asm volatile(    
            "pushq %%rbp
	"         /* save rbp of prev */
            "movq %%rsp,%0
	"     /* save rsp of prev */
            "movq %2,%%rsp
	"     /* restore  rsp of next */
            "movq $1f,%1
	"       /* save rip of prev */    
            "pushq %3
	" 
            "ret
	"                 /* restore  rip of next */
            "1:	"                  /* next process start here */
            "popq %%rbp
	"
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
            : "m" (next->thread.sp),"m" (next->thread.ip)
        ); 
    }  
    return;    
}

其中my_timer_handler函数的功能为周期性将my_need_sched置为1,标志进程需要进程调度。实际的调度代码为my_schedule函数。

该函数执行的具体任务为保存当前进程(prev)的上下文,并调出下一个进程(next)的上下文。核心代码为36L开始的汇编。

  1. 将当前进程的栈底指针压入栈中,保存其状态。

  2. 将rsp寄存器的值保存到prev->thread.sp中

  3. 将rsp寄存器的值更新为下一进程的栈顶地址,实现进程操作栈的切换。

  4. 将43行指令地址保存到当前进程持有线程的指令地址中。指定该进程重新被调入时,开始执行指令的位置。

  5. 将下一进程的ip地址入栈并ret,从而来更新rip寄存器的值。如果下一进程之前运行过,此时rip寄存器的值便是之前保存的43L地址。

  6. 将下一进程之前被调出时在37L保存的栈底地址出栈,赋值给rbp寄存器。 

7.重新运行,运行结果

make defconfig # Default configuration is based on 'x86_64_defconfig'
make -j$(nproc)
qemu-system-x86_64 -kernel arch/x86/boot/bzImage

8.个人收获

对Linux命令、C语言有一定的复习,了解Linux内核工作的基本原理、多任务的并发执行,了解了中断的产生以及中断在计算机中的重要作用,加深了对Linux系统学习的兴趣。

参考:https://mp.weixin.qq.com/s/SzpN1BNty5aPDZhNdCO5yA 

原文地址:https://www.cnblogs.com/cun-yu/p/12868147.html