基于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 \n",i); } }
可知,在循环打印中,my_start_kernel here... ...这条消息是由进程运行mymain.c时进行打印的。再打开myinterrupt.c,能够看到如下代码
void my_timer_handler(void) { pr_notice("\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n"); }
循环打印里的另一段就是由进程执行到此处打印的
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<<<\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_schedule函数是处理进程调度的关键。上文已经说过,pcb块以链表的形式串联起来,my_schedule函数选择进程链表中的下一个就绪进程进行切换。它也内嵌了汇编代码,实现的功能如下:
-
保存进程rbp寄存器内的值
-
保存进程rsp寄存器内的值
-
更新寄存器rsp为next指向的新进程内的sp变量值,此时进行了进程间栈帧的转换
-
保存原进程rip寄存器内的值
-
更新寄存器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<<<\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函数的功能为周期性将my_need_sched置为1,标志进程需要进程调度。实际的调度代码为my_schedule函数。
该函数执行的具体任务为保存当前进程(prev)的上下文,并调出下一个进程(next)的上下文。核心代码为36L开始的汇编。
-
将当前进程的栈底指针压入栈中,保存其状态。
-
将rsp寄存器的值保存到prev->thread.sp中
-
将rsp寄存器的值更新为下一进程的栈顶地址,实现进程操作栈的切换。
-
将43行指令地址保存到当前进程持有线程的指令地址中。指定该进程重新被调入时,开始执行指令的位置。
-
将下一进程的ip地址入栈并ret,从而来更新rip寄存器的值。如果下一进程之前运行过,此时rip寄存器的值便是之前保存的43L地址。
- 将下一进程之前被调出时在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