基于mykernel 2.0编写一个操作系统内核
一、实验要求
- 按照https://github.com/mengning/mykernel 的说明配置mykernel 2.0,熟悉Linux内核的编译;
- 基于mykernel 2.0编写一个操作系统内核,参照https://github.com/mengning/mykernel提供的范例代码;
- 简要分析操作系统内核核心功能及运行工作机制。
二、实验目的
- 理解Linux操作系统内核工作原理;
- 理解进程调度和中断机制
三、实验环境
ubuntu-16.04.6
四、实验步骤
1、根据PPT / GitHub上的教程下载并安装mykernel
下载mykernel并编译,在终端依次执行下列命令即可。
1 wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch 2 sudo apt install axel 3 axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz 4 xz -d linux-5.4.34.tar.xz 5 tar -xvf linux-5.4.34.tar 6 cd linux-5.4.34 7 patch -p1 < ../mykernel-2.0_for_linux-5.4.34.patch 8 sudo apt install build-essential gcc-multilib libncurses5-dev bison flex libssl-dev libelf-dev 9 sudo apt install qemu # install QEMU 10 make defconfig # Default configuration is based on 'x86_64_defconfig' 11 make -j$(nproc)
2、启动mykernel
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
从运行结果中可以看出,系统内核代码已经启动完成。my_start_kernel进程周期性的调用my_handler_here方法,输出一些消息。
3、代码分析
观察一下mymain.c和myinterrupt.c两个文件中的核心代码,cd mykernel 您可以看到qemu窗口输出的内容的代码mymain.c和myinterrupt.c。
1 void __init my_start_kernel(void) 2 { 3 int i = 0; 4 while(1) 5 { 6 i++; 7 if(i%100000 == 0) 8 printk(KERN_NOTICE "my_start_kernel here %d \n",i); 9 10 } 11 }
其中有一个while循环,周期性的输出 my_start_kernel here,相当于一个时钟信号,判断条件是每循环到 i % 100000 == 0,操作系统从这里开始执行。
void my_timer_handler(void) { printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n"); }
这段代码是用来处理时钟中断的,时钟中断是由linux内核完成,这段代码的主要功能就是将时钟中断进行处理。
4、基于mykernel 2.0编写一个操作系统内核,参照https://github.com/mengning/mykernel提供的范例代码
在Linux-5.3.34内核源代码根⽬录下进⼊mykernel⽬录,可以看到QEMU窗⼝输出的内容的代码mymain.c和myinterrupt.c ,当前有⼀个虚拟的CPU执⾏C代码的上下⽂环境,可以看到mymain.c中的代码在不停地执⾏。同时 有一个中断处理程序的上下⽂环境,周期性地产⽣的时钟中断信号,能够触发myinterrupt.c中的代码。这样就通过Linux内核代码模拟了⼀个具有时钟中断和C代码执⾏环境的硬件平台。您只要在mymain.c的基础上继续写进 程描述PCB和进程链表管理等代码,在myinterrupt.c的基础上完成进程切换代码,就可以完成⼀个可运⾏的⼩OS kernel。
- 在下载孟宁老师的源码后,将mypcb.h,myinterrupt.c和mymain.c这三个文件拷贝到mykernel目录下,即要覆盖之前的mykernel文件夹下mymain.c和myinterrupt.c,并新增头文件mypcb.h
- mypcb.h:进程控制块PCB结构体定义;
- mymain.c:初始化各个进程并启动0号进程;
- myinterrupt.c:时钟中断处理和进程调度算法。
1 #define MAX_TASK_NUM 4 2 #define KERNEL_STACK_SIZE 1024*2 # unsigned long 3 /* CPU-specific state of this task */ 4 struct Thread { 5 unsigned long ip; 6 unsigned long sp; 7 }; 8 9 typedef struct PCB{ 10 int pid; 11 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 12 unsigned long stack[KERNEL_STACK_SIZE]; 13 /* CPU-specific state of this task */ 14 struct Thread thread; 15 unsigned long task_entry; 16 struct PCB *next; 17 }tPCB; 18 19 void my_schedule(void);
- pid:进程号
- state:进程状态,在模拟系统中,所有进程控制块信息都会被创建出来,其初始化值就是-1,如果被调度运行起来,其值就会变成0
- stack:进程使用的堆栈
- thread:当前正在执行的线程信息
- task_entry:进程入口函数
- next:指向下一个PCB,模拟系统中所有的PCB是以链表的形式组织起来的。
- 函数的声明 my_schedule,它的实现在my_interrupt.c中,在mymain.c中的各个进程函数会根据一个全局变量的状态来决定是否调用它,从而实现主动调度。
1 void __init my_start_kernel(void) 2 { 3 int pid = 0; 4 int i; 5 /* Initialize process 0*/ 6 task[pid].pid = pid; 7 task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ 8 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; 9 task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; 10 task[pid].next = &task[pid]; 11 /*fork more process */ 12 for(i=1;i<MAX_TASK_NUM;i++) 13 { 14 memcpy(&task[i],&task[0],sizeof(tPCB)); 15 task[i].pid = i; 16 //*(&task[i].stack[KERNEL_STACK_SIZE-1] - 1) = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1]; 17 task[i].thread.sp = (unsigned long)(&task[i].stack[KERNEL_STACK_SIZE-1]); 18 task[i].next = task[i-1].next; 19 task[i-1].next = &task[i]; 20 } 21 /* start process 0 by task[0] */ 22 pid = 0; 23 my_current_task = &task[pid]; 24 asm volatile( 25 "movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */ 26 "pushl %1\n\t" /* push ebp */ 27 "pushl %0\n\t" /* push task[pid].thread.ip */ 28 "ret\n\t" /* pop task[pid].thread.ip to eip */ 29 : 30 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ 31 ); 32 } 33 34 int i = 0; 35 36 void my_process(void) 37 { 38 while(1) 39 { 40 i++; 41 if(i%10000000 == 0) 42 { 43 printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); 44 if(my_need_sched == 1) 45 { 46 my_need_sched = 0; 47 my_schedule(); 48 } 49 printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid); 50 } 51 } 52 }
PCB实现成了一个环形队列,每次创建新的进程时,都将该进程插入到队列的最后一个位置,然后将该进程指向第一个进程。
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; }
mykernel提供了时钟中断机制,周期性执行my_time_handler中断处理程序,该函数定时观察my_need_sched是否不等于1,如果是则将其置为1,使myprocess执行my_schedule()。my_schedule函数在PCB环形队中选 择下一个进程进行执行,但对于处于不同状态的进程,调度方式略有不同,如果即将上CPU的进程之前已经运行过(即state为0),我们保存当前进程的信息,然后把下一个进程的信息写入到寄存器中,执行ret使下一个进 程开始执行。之前没有在运行态的(state不为0),我们先将其设置为运行态,我们这里需要初始化其ebp,因为该进程的堆栈是空栈esp=ebp。
- 再次编译、运行
make allnoconfig make qemu -kernel arch/x86/boot/bzImage
五、实验总结
通过本次实验,我更加深入理解了操作系统的工作原理以及操作系统的进程调度和中断机制。