LinuxLab1---基于mykernel 2.0编写一个操作系统内核
一、实验环境安装
使用的虚拟机创建软件:VMware Fusion Pro 11.1.0
使用的系统:Ubuntu-18.04.4-desktop-amd64
二、实验开始前准备工作
1. 下载孟老师的内核代码库(取出其中的mykernel-2.0_for_linux-5.4.34.patch)
1 git clone https://github.com/mengning/mykernel
也可以使用(尝试时一直连接失败)
1 wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch
2. 安装axel(下载软件)和Linux内核源码并解压
1 sudo apt install axel 2 axel -n -20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz 3 xz -d linux-5.4.34.tar.xz 4 tar -xvf linux-5.4.34.tar
3. 进入内核文件夹,将mykernel-2.0_for_linux-5.4.34.patch的修改同步到代码中
1 patch -p1 < ../mykernel-2.0_for_linux-5.4.34.patch 2 sudo apt install build-essential gcc-multilib
4. 在ubuntu中安装必要的依赖
1 sudo apt install build-essential gcc-multilib 2 sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev 3 sudo apt install qemu # install QEMU
5. 编译内核,并使用qemu启动
1 make defconfig 2 make -j$(nproc) #根据对应的cpu核心数调整 3 qemu-system-x86_64 -kernel arch/x86/boot/bzImage

可以看到mymain.c的函数在不断的执行,同时不断产生时钟中断信号,触发myinterrupt.c中的代码。一个具有时钟中断的功能就完成了。
基础准备工作完毕,让我们开始编写一个操作系统内核吧!
三、基于mykernel2.0编写一个操作系统内核
1. 进程PCB(进程控制块)是进程重要的数据结构。我们首先实现它。
在mykernel目录下增加一个mypcb.h头文件。
1 /* 2 * linux/mykernel/mypcb.h 3 * Kernel internal PCB type 4 */ 5 6 #define MAX_TASK_NUM 4 7 #define KERNEL_STACK_SIZE 1024*2 8 /* CPU-specific state of this task */ 9 10 //存储ip和sp 11 struct Thread { 12 unsigned long ip; //函数入口指针 13 unsigned long sp; //栈顶指针 14 }; 15 16 //PCB结构 17 typedef struct PCB{ 18 int pid;//进程id 19 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 20 unsigned long stack[KERNEL_STACK_SIZE];//进程堆栈 21 /* CPU-specific state of this task */ 22 struct Thread thread;//线程 23 unsigned long task_entry;//进程入口地址 24 struct PCB *next;//下一个进程控制块地址 25 }tPCB; 26 27 void my_schedule(void);//调度函数
2.mymain.c是mykernel代码的入口,负责将各个模块进行组合。
对my_start_kernel进行修改。
1 #include <linux/types.h> 2 #include <linux/string.h> 3 #include <linux/ctype.h> 4 #include <linux/tty.h> 5 #include <linux/vmalloc.h> 6 7 8 #include "mypcb.h" 9 10 tPCB task[MAX_TASK_NUM]; 11 tPCB * my_current_task = NULL; 12 volatile int my_need_sched = 0; 13 14 void my_process(void); 15 16 17 void __init my_start_kernel(void) 18 { 19 int pid = 0; 20 int i; 21 /* Initialize process 0*/ 22 task[pid].pid = pid; 23 task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ 24 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; 25 task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; 26 task[pid].next = &task[pid]; 27 /*fork more process */ 28 for(i=1;i<MAX_TASK_NUM;i++) 29 { 30 memcpy(&task[i],&task[0],sizeof(tPCB)); 31 task[i].pid = i; 32 task[i].thread.sp = (unsigned long)(&task[i].stack[KERNEL_STACK_SIZE-1]); 33 task[i].next = task[i-1].next; 34 task[i-1].next = &task[i]; 35 } 36 /* start process 0 by task[0] */ 37 pid = 0; 38 my_current_task = &task[pid]; 39 asm volatile( 40 "movq %1,%%rsp\n\t" /* set task[pid].thread.sp to rsp */ 41 "pushq %1\n\t" /* push rbp */ 42 "pushq %0\n\t" /* push task[pid].thread.ip */ 43 "ret\n\t" /* pop task[pid].thread.ip to rip */ 44 : 45 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ 46 ); 47 }
初始化进程0
3. mymain.c在这里采用了时间片轮转的方式进行进程调度,进程调度的也是根据序号先进先出。
1 int i = 0; 2 3 void my_process(void) 4 { 5 while(1) 6 { 7 i++; 8 if(i%10000000 == 0) 9 { 10 printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); 11 if(my_need_sched == 1) 12 { 13 my_need_sched = 0; 14 my_schedule(); 15 } 16 printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid); 17 } 18 } 19 }
4.myinterupt.c 修改 使用my_timer_handler模拟一个时间片的操作
1 #include <linux/types.h> 2 #include <linux/string.h> 3 #include <linux/ctype.h> 4 #include <linux/tty.h> 5 #include <linux/vmalloc.h> 6 7 #include "mypcb.h" 8 9 extern tPCB task[MAX_TASK_NUM]; 10 extern tPCB * my_current_task; 11 extern volatile int my_need_sched; 12 volatile int time_count = 0; 13 14 /* 15 * Called by timer interrupt. 16 * it runs in the name of current running process, 17 * so it use kernel stack of current running process 18 */ 19 void my_timer_handler(void) 20 { 21 if(time_count%1000 == 0 && my_need_sched != 1) 22 { 23 printk(KERN_NOTICE ">>>my_timer_handler here<<<\n"); 24 my_need_sched = 1; 25 } 26 time_count ++ ; 27 return; 28 }
5.myinterupt.c进程切换部分(难点&重点)
1 void my_schedule(void) 2 { 3 tPCB * next; 4 tPCB * prev; 5 6 if(my_current_task == NULL 7 || my_current_task->next == NULL) 8 { 9 return; 10 } 11 printk(KERN_NOTICE ">>>my_schedule<<<\n"); 12 /* schedule */ 13 next = my_current_task->next; 14 prev = my_current_task; 15 if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ 16 { 17 my_current_task = next; 18 printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); 19 /* switch to next process */ 20 asm volatile( 21 "pushq %%rbp\n\t" /* save rbp of prev */ 22 "movq %%rsp,%0\n\t" /* save rsp of prev */ 23 "movq %2,%%rsp\n\t" /* restore rsp of next */ 24 "movq $1f,%1\n\t" /* save rip of prev */ 25 "pushq %3\n\t" 26 "ret\n\t" /* restore rip of next */ 27 "1:\t" /* next process start here */ 28 "popq %%rbp\n\t" 29 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 30 : "m" (next->thread.sp),"m" (next->thread.ip) 31 ); 32 } 33 return; 34 }
四、关键代码分析
前置汇编知识
%1 %2代表末尾:符号后面的按照顺序排列的对应变量。
64系统为8个字节。
1.第一个进程0的启动
1 asm volatile( 2 "movq %1,%%rsp\n\t" /* set task[pid].thread.sp to rsp */ 3 "pushq %1\n\t" /* push rbp */ 4 "pushq %0\n\t" /* push task[pid].thread.ip */ 5 "ret\n\t" /* pop task[pid].thread.ip to rip */ 6 : 7 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ 8 );
movq %1,%%rsp 将rsp寄存器指向进程0的堆栈栈底,task[pid].thread.sp的初始值就是堆栈栈底;
pushq %1 将当前rsp寄存器的值压栈,进程的堆栈栈顶的值task[pid].thread.sp,rsp=rsp-8;
pushq %0 将当前进程的rip入栈,相应的rsp寄存器指向的位置也发生了变化。rsp=rsp-8;
1 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
ret 将栈顶位置task[0]. thread.ip,即my_process(void)函数的地址放入rip寄存器,rsp=rsp+8;
2.进程的切换
1 asm volatile( 2 "pushq %%rbp\n\t" /* save rbp of prev */ 3 "movq %%rsp,%0\n\t" /* save rsp of prev */ 4 "movq %2,%%rsp\n\t" /* restore rsp of next */ 5 "movq $1f,%1\n\t" /* save rip of prev */ 6 "pushq %3\n\t" 7 "ret\n\t" /* restore rip of next */ 8 "1:\t" /* next process start here */ 9 "popq %%rbp\n\t" 10 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 11 : "m" (next->thread.sp),"m" (next->thread.ip) 12 );
pushq %%rbp 保存prev进程的rbp的值,入栈
movq %%rsp,%0 当前rsp寄存器的值到prev->thread.sp,这时rsp寄存器指向进程的栈顶地址,也就是保存prev进程栈顶地址
movq %2,%%rsp 将next进程的栈顶地址放入rsp寄存器,完成进程0和进程1的堆栈切换
movq $1f,%1 保存prev进程当前rip寄存器的值到prev->thread.ip,$1f指标号1
pushq %3 把即将执行的next进程的指令地址next->thread.ip入栈
ret 将压入栈的next->thread.ip放入rip寄存器(此时执行1:)
1: 标号1是一个特殊的地址位置,地址是$1f
popq %%rbp 将next进程堆栈基地址从堆栈恢复到rbp寄存器中
五、总体流程分析
系统启动后,运行mymain.c中的my_start_kernal, 进程0首先初始化,再初始化其他的进程。
my_process中一直查询my_need_sched是否为1,为1则在函数中调用my_schedule,切换到下一个进程

六、总结
通过实现mykernel的操作系统内核,对Linux的启动和调度切换有了更深的理解。尤其是切换时堆栈的变化,加深了对汇编语言的理解。

浙公网安备 33010602011771号