实验环境
- 虚拟机版本:VMware 12
- 操作系统:Ubuntu 16.04 LTS
实验内容
基于mykernel 2.0编写一个操作系统内核
- 按照https://github.com/mengning/mykernel 的说明配置mykernel 2.0,熟悉Linux内核的编译;
- 基于mykernel 2.0编写一个操作系统内核,参照https://github.com/mengning/mykernel 提供的范例代码
- 简要分析操作系统内核核心功能及运行工作机制
实验过程
按照老师的说明配置mykernel2.0
1 wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch
多次尝试下载patch失败,于是直接宿主机下好了再复制到虚拟机里面...
下载原始内核文件,解压
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 5 cd linux-5.4.34
用 mykernel-2.0_for_linux-5.4.34.patch 对 内核文件进行更新
安装编译工具,配置编译文件,并进行编译
1 sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev 2 make defconfig 3 make -j$(nproc)
小霸王学习机编译耗时40min
安装qemu虚拟机,运行 arch/x86/boot 目录下的 bzImage,这是刚才编译好的内核镜像
1 sudo apt install qemu 2 qemu-system-x86_64 -kernel arch/x86/boot/bzImage
从qemu窗口中可以看到my_start_kernel在重复执行,中间穿插着 my_timer_handler 时钟中断处理程序。
查看源码
1 // mymain.c 2 void __init my_start_kernel(void) 3 { 4 int i = 0; 5 while(1) 6 { 7 i++; 8 if(i%100000 == 0) 9 pr_notice("my_start_kernel here %d \n",i); 10 11 } 12 }
1 // myinterrupt.c 2 void my_timer_handler(void) 3 { 4 pr_notice("\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n"); 5 }
由代码可分析得,my_start_kernel 每循环十万次就会产生一次中断,转入中断处理函数,处理完毕又回到 my_start_kernel 继续循环。
基于以上原理,对内核加入更多的功能:在mymain.c基础上继续写进程描述PCB和进程链表管理等代码,在myinterrupt.c的基础上完成进程切换代码。
以下参考老师给出的示例代码:
首先为了标识各个进程,需要定义一个进程控制块PCB结构体文件 mypcb.h
1 #define MAX_TASK_NUM 4 2 #define KERNEL_STACK_SIZE 1024*8 3 4 5 /* CPU-specific state of this task */ 6 struct Thread { 7 unsigned long ip; 8 unsigned long sp; 9 }; 10 11 12 typedef struct PCB{ 13 int pid; 14 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 15 char stack[KERNEL_STACK_SIZE]; 16 /* CPU-specific state of this task */ 17 struct Thread thread; 18 unsigned long task_entry; 19 struct PCB *next; 20 }tPCB; 21 22 23 void my_schedule(void);
其中 pid 标识进程;state 表示进程当前是处于何种状态,状态关系到进程能否调度CPU执行;stack[KERNEL_STACK_SIZE] 堆栈;thread 对应的线程;task_entry 入口地址;PCB *next 进程链表中的下一个进程。 对于每一个thread有一个ip指针存储指令地址和sp指针存储堆栈地址。
重写main.c内核入口代码
1 #include "mypcb.h" 2 3 tPCB task[MAX_TASK_NUM]; 4 tPCB * my_current_task = NULL; 5 volatile int my_need_sched = 0; 6 7 void my_process(void); 8 9 10 void __init my_start_kernel(void) 11 { 12 int pid = 0; 13 int i; 14 /* Initialize process 0*/ 15 task[pid].pid = pid; 16 task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ 17 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; 18 task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; 19 task[pid].next = &task[pid]; 20 /*fork more process */ 21 for(i=1;i) 22 { 23 memcpy(&task[i],&task[0],sizeof(tPCB)); 24 task[i].pid = i; 25 task[i].thread.sp = (unsigned long)(&task[i].stack[KERNEL_STACK_SIZE-1]); 26 task[i].next = task[i-1].next; 27 task[i-1].next = &task[i]; 28 } 29 /* start process 0 by task[0] */ 30 pid = 0; 31 my_current_task = &task[pid]; 32 asm volatile( 33 "movq %1,%%rsp\n\t" /* set task[pid].thread.sp to rsp */ 34 "pushq %1\n\t" /* push rbp */ 35 "pushq %0\n\t" /* push task[pid].thread.ip */ 36 "ret\n\t" /* pop task[pid].thread.ip to rip */ 37 : 38 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ 39 ); 40 } 41 42 int i = 0; 43 44 void my_process(void) 45 { 46 while(1) 47 { 48 i++; 49 if(i%10000000 == 0) 50 { 51 printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); 52 if(my_need_sched == 1) 53 { 54 my_need_sched = 0; 55 my_schedule(); 56 } 57 printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid); 58 } 59 } 60 }
可见此时支持的最大并发进程数为4。 将0号进程初始化为可运行状态,其他3个初始化为不可运行状态,sp指针均指向各自堆栈栈底。
嵌入式汇编代码将0号进程的sp和ip更新到cpu的rsp和rip寄存器,即开始执行该进程。
在my_process中,每一千万次循环产生一次中断,显示当前进程pid, 然后进行一次调度,显示调度后进程pid。
进程切换 myinterrupt.c
1 #include "mypcb.h" 2 3 extern tPCB task[MAX_TASK_NUM]; 4 extern tPCB * my_current_task; 5 extern volatile int my_need_sched; 6 volatile int time_count = 0; 7 8 /* 9 * Called by timer interrupt. 10 * it runs in the name of current running process, 11 * so it use kernel stack of current running process 12 */ 13 void my_timer_handler(void) 14 { 15 if(time_count%1000 == 0 && my_need_sched != 1) 16 { 17 printk(KERN_NOTICE ">>>my_timer_handler here<<<\n"); 18 my_need_sched = 1; 19 } 20 time_count ++ ; 21 return; 22 } 23 24 void my_schedule(void) 25 { 26 tPCB * next; 27 tPCB * prev; 28 29 if(my_current_task == NULL 30 || my_current_task->next == NULL) 31 { 32 return; 33 } 34 printk(KERN_NOTICE ">>>my_schedule<<<\n"); 35 /* schedule */ 36 next = my_current_task->next; 37 prev = my_current_task; 38 if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ 39 { 40 my_current_task = next; 41 printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); 42 /* switch to next process */ 43 asm volatile( 44 "pushq %%rbp\n\t" /* save rbp of prev */ 45 "movq %%rsp,%0\n\t" /* save rsp of prev */ 46 "movq %2,%%rsp\n\t" /* restore rsp of next */ 47 "movq $1f,%1\n\t" /* save rip of prev */ 48 "pushq %3\n\t" 49 "ret\n\t" /* restore rip of next */ 50 "1:\t" /* next process start here */ 51 "popq %%rbp\n\t" 52 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 53 : "m" (next->thread.sp),"m" (next->thread.ip) 54 ); 55 } 56 return; 57 }
在中断处理程序中time_count每增加1000,且调度标识位my_need_sched 为0时将标志位改为1。
my_schedule用于完成进程切换,无进程运行或只有一个进程时无需切换,否则需进行切换。将下一个进程的状态改为可运行,并将其更新为当前运行进程,同时在汇编代码中,将之前进程的运行环境即rbp,rsp,rip的值压入堆栈,然后将rsp,rip的值更新为切换后进程的sp和ip, 并从堆栈中弹出bp,由此完成了进程的切换。
用图能更好的理解这个过程:
重新编译并运行内核:
1 make clean 2 make defconfig 3 make -j$(nproc) 4 qemu-system-x86_64 -kernel arch/x86/boot/bzImage
实验总结
在本次的实验中,通过对源码特别是嵌入式汇编代码的理解,让我从更底层的层面学习了进程的调度和切换过程,操作系统不再只是一个抽象的概念,加深了理解。