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

Posted 公元1864

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于mykernel 2.0编写一个操作系统内核相关的知识,希望对你有一定的参考价值。

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

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

一、实验环境配置

1.操作系统环境

腾讯云Ubuntu 

2.内核环境配置

按照https://github.com/mengning/mykernel上的内容输入如下命令:

复制代码
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
复制代码

运行结果如下:

 

二.内核编写

这一部分使用孟宁老师给出的范例代码https://github.com/mengning/mykernel 进行分析。

1.添加PCB结构体

复制代码
#define MAX_TASK_NUM        4
#define KERNEL_STACK_SIZE   1024*2
/* CPU-specific state of this task */
struct Thread {
    unsigned long        ip;
    unsigned long        sp;
};

typedef struct PCB{
    int pid;
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    unsigned long stack[KERNEL_STACK_SIZE];
    /* CPU-specific state of this task */
    struct Thread thread;
    unsigned long    task_entry;
    struct PCB *next;
}tPCB;

void my_schedule(void);
复制代码

Thread结构体中,ip和sp变量用来在进程切换时,保存或者恢复当前指令指针寄存器和栈指针寄存器的值。也就是需要用这个结构来保存部分的上下文信息。

PCB结构体中,pid是进程的id进程号,state表示进程状态,-1、0、1分别表示阻塞、就绪、终止三种状态,stack数组用来模拟进程的栈。task_entry是进程的运行入口,

而从next指针可以看到,进程PCB之间用链表结构连接。

 

2.修改mymain.c

复制代码
#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>


#include "mypcb.h"

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

void my_process(void);


void __init my_start_kernel(void)
{
    int pid = 0;
    int i;
    /* Initialize process 0*/
    task[pid].pid = pid;
    task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
    task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
    task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
    task[pid].next = &task[pid];
    /*fork more process */
    for(i=1;i<MAX_TASK_NUM;i++)
    {
        memcpy(&task[i],&task[0],sizeof(tPCB));
        task[i].pid = i;
        task[i].thread.sp = (unsigned long)(&task[i].stack[KERNEL_STACK_SIZE-1]);
        task[i].next = task[i-1].next;
        task[i-1].next = &task[i];
    }
    /* start process 0 by task[0] */
    pid = 0;
    my_current_task = &task[pid];
    asm volatile(
        "movq %1,%%rsp\\n\\t"     /* set task[pid].thread.sp to rsp */
        "pushq %1\\n\\t"             /* push rbp */
        "pushq %0\\n\\t"             /* push task[pid].thread.ip */
        "ret\\n\\t"                 /* pop task[pid].thread.ip to rip */
        : 
        : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)    /* input c or d mean %ecx/%edx*/
    );
} 

int i = 0;

void my_process(void)
{    
    while(1)
    {
        i++;
        if(i%10000000 == 0)
        {
            printk(KERN_NOTICE "this is process %d -\\n",my_current_task->pid);
            if(my_need_sched == 1)
            {
                my_need_sched = 0;
                my_schedule();
            }
            printk(KERN_NOTICE "this is process %d +\\n",my_current_task->pid);
        }     
    }
}
复制代码
这里,my_start_kernel函数是mykernel内核代码的入口,主要的功能就是初始化内核的各个组成部分。一开始pid=0,意味着0号进程,接下来就是对0号进程的一些初始化工作,包括将其state置为runnable。
task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
task[pid].next = &task[pid];

    第一句代码在程序中的意义是:将pid号(就是0号)进程的入口位置设置为my_process函数。ip保存的应当是进程下一条要执行的指令的地址,这里就相当于进程第一次运行时的入口。进程的sp变量保存堆栈的栈顶位置,这里栈为空(栈的大小有上限,通过KERNEL_STACK_SIZE限定),所以指向数组最后一个位置。第三行代码产生的效果是:0号进程的next域指向0号进程自己。

 my_start_kernel函数中的for循环用来创建更多的进程。

 在my_start_kernel中,还要负责进程的启动,通过嵌入汇编代码完成:

复制代码
    pid = 0;
    my_current_task = &task[pid];
    asm volatile(
        "movq %1,%%rsp\\n\\t"     /* set task[pid].thread.sp to rsp */
        "pushq %1\\n\\t"             /* push rbp */
        "pushq %0\\n\\t"             /* push task[pid].thread.ip */
        "ret\\n\\t"                 /* pop task[pid].thread.ip to rip */
        : 
        : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)    /* input c or d mean %ecx/%edx*/
    );
复制代码

    启动的进程为0号进程。每条汇编指令的作用:1)用sp变量赋值rsp寄存器,切换堆栈。2)将rbp(这里是sp,但空栈时sp=rbp)压入。3)将ip压入堆栈。4)ret将pop出压入的ip,将rip寄存器置为ip,完成进程指令的切换(进程将得到将要执行的代码的位置,这里为my_process函数)。这样,就完成了进程的切换,下一个动作将启动进程0。

    my_process函数是一个无限循环,每循环一千万次,就检查my_need_sched变量,看是否需要调度。等于1为需要调度,将切换到下一进程。

 

3.修改myinterrupt.c

复制代码
#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<<<\\n");
        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<<<\\n");
    /* 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<<<\\n",prev->pid,next->pid);  
        /* switch to next process */
        asm volatile(    
            "pushq %%rbp\\n\\t"         /* save rbp of prev */
            "movq %%rsp,%0\\n\\t"     /* save rsp of prev */
            "movq %2,%%rsp\\n\\t"     /* restore  rsp of next */
            "movq $1f,%1\\n\\t"       /* save rip of prev */    
            "pushq %3\\n\\t" 
            "ret\\n\\t"                 /* restore  rip of next */
            "1:\\t"                  /* next process start here */
            "popq %%rbp\\n\\t"
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
            : "m" (next->thread.sp),"m" (next->thread.ip)
        ); 
    }  
    return;    
}
复制代码

   my_timer_handler函数调用于CPU发生中断,中断当前进程时。主要看my_schedule函数。my_schedule中,my_current_task 变量为当前进程,在.c文件中使用extern声明,可以从其他的模块中寻找其定义。my_schedule中的嵌入式汇编为进程切换的核心。

 4.实验结果

 

三、分析内核核心功能

 本次实验,内核的核心功能围绕两个问题:1)何时发生进程切换。2)如何进行进程切换

1.何时切换?

my_timer_handler函数在中断发生时调用,每1000次中断,会触发一次my_need_sched变量置为1。在每一个进程运行的都是my_process,其中每循环一千万次会检查一次my_need_sched的值是否为1,若是就清零,并调用my_schedule切换进程。本质上相当于每个进程都拥有一个时间片,时间片用完就会调度。

 

2.如何进程切换?

复制代码
        asm volatile(    
            "pushq %%rbp\\n\\t"         /* save rbp of prev */
            "movq %%rsp,%0\\n\\t"     /* save rsp of prev */
            "movq %2,%%rsp\\n\\t"     /* restore  rsp of next */
            "movq $1f,%1\\n\\t"       /* save rip of prev */    
            "pushq %3\\n\\t" 
            "ret\\n\\t"                 /* restore  rip of next */
            "1:\\t"                  /* next process start here */
            "popq %%rbp\\n\\t"
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
            : "m" (next->thread.sp),"m" (next->thread.ip)
        ); 
复制代码

这段汇编代码就是进程切换的主要实现方法。分析如下:

1)pushq %%rbp:将rbp寄存器值保存在切换前进程(prev)的堆顶。

2)movq %%rsp,%0:将rsp寄存器值保存在切换前进程的sp变量中。(将prev进程栈顶位置保存)

3)movq %2,%%rsp:将切换后进程(next)的栈顶指针sp放入rsp寄存器。(此时已发生堆栈切换,之后的堆栈操作都是在next进程中的)

4)movq $1f,%1:保存切换前进程的下一条指令地址到ip变量中。这里prev进程的下一条指令就在标号1后面。

5)pushq %3:这里将切换后进程的下一条指令地址ip压栈(rip寄存器程序员没有权限进行写入,需要多一个步骤)

6)ret :将栈顶的ip,pop出来到rip寄存器(此时进程执行指令也被切换成功)

7)popq %%rbp:将切换后进程的栈顶的堆栈基地址pop出来,放入rbp寄存器(注意:如果next进程第一次被调度运行,将不会执行到这,因为rip在上一步被置为my_process的函数入口,紧接着会去运行my_process函数。若不是第一次运行,则发生过切换,next进程的ip变量为$1f,且next进程栈顶为上次切换时压入的rbp寄存器的值,所以这步可以获取到next进程的堆栈基地址

四.总结

  本实验作为Linux学习过程中的第一个实验,通过编写一个简单的计算机操作系统内核,完成了基于时间片的进程轮换。进程在执⾏过程中,当时间⽚⽤完需要进⾏进程切换时,需要先保存当前的进程上下⽂环境,下次进程被调度执⾏时,需要恢复进程上下⽂环境,就这样通过虚拟化的进程概念实现了多道程序在同⼀个物理CPU上并发执⾏。同时也进一步加深了对汇编代码的理解,也为后续中断的学习打下了基础。

以上是关于基于mykernel 2.0编写一个操作系统内核的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

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

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