基于mykernel 2.0编写一个操作系统内核
Posted 公元1864
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于mykernel 2.0编写一个操作系统内核相关的知识,希望对你有一定的参考价值。
题目:基于mykernel 2.0编写一个操作系统内核
- 按照https://github.com/mengning/mykernel 的说明配置mykernel 2.0,熟悉Linux内核的编译;
- 基于mykernel 2.0编写一个操作系统内核,参照https://github.com/mengning/mykernel 提供的范例代码
- 简要分析操作系统内核核心功能及运行工作机制
一、实验环境配置
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编写一个操作系统内核的主要内容,如果未能解决你的问题,请参考以下文章