基于mykernel2.0 编写一个操作系统内核
-
按照https://github.com/mengning/mykernel 的说明配置mykernel 2.0,熟悉Linux内核的编译;
-
基于mykernel 2.0编写一个操作系统内核,参照https://github.com/mengning/mykernel 提供的范例代码
-
简要分析操作系统内核核心功能及运行工作机制
二、实验环境
发行版本:Deepin 15.11
内核版本:Linux 4.15.0-30deepin-generic x86_64
三、实验步骤
1. 运行mykernel2.0
首先下载Linux源码及mykernel patch文件,并通过patch命令在Linux源码之上修改生成mykernel。
接下来使用默认选项完成内核编译构建,并在qemu虚拟环境中启动内核。
可以看到内核成功启动,并在终端上周期性打印信息。下面从源码角度对该行为进行分析。
从前述执行patch命令的输出信息不难看出,核心逻辑代码位于mymain.c和myinterrupt.c两个源文件中。
mymain.c中定义了一个内核启动函数,该函数会循环打印"my_start_kernel here"
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何时被调用的呢?在Linux内核的启动代码源文件main.c中,可以看到它在内核启动函数start_kernel被调用。
asmlinkage __visible void __init start_kernel(void) { // ... acpi_subsystem_init(); arch_post_acpi_subsys_init(); sfi_init_late(); my_start_kernel(); /* Do the rest non-__init'ed, we're now alive */ arch_call_rest_init(); }
myinterrupt.c中定义了一个定时器句柄函数,其在定时器中断时执行,并打印信息。
/* * called by timer interrupt. */ void my_timer_handler(void) { pr_notice("\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n"); }
该句柄函数是如何被注册的呢?在time.c源文件中,可以看到my_timer_handler是在Linux定时器中断函数timer_interrupt中被调用的。
/* * Default timer interrupt handler for PIT/HPET */ static irqreturn_t timer_interrupt(int irq, void *dev_id) { global_clock_event->event_handler(global_clock_event); my_timer_handler(); return IRQ_HANDLED; }
至此,我们明白了mykernel在启动后打印的两种控制台信息的由来。
2. 实现进程调度
在上述mykernel框架的基础上,我们来为其添加进程调度的功能。首先,我们需要定义表示进程的数据结构,用于存储进程的信息和资源;其次,我们需要实现调度函数,这是进程调度的核心逻辑;除此之外我们还需要在内核启动时完成进程调度的初始化。这分别涉及到三个源文件:mypcb.h,myinterrupt.c以及mymain.c。下面依次分析。
首先是数据结构的定义,对于一个进程所涉及的资源和控制信息,我们将其统一置于结构体PCB中。主要字段为进程id、进程状态、函数调用栈、代码入口以及ip、sp等。我们还可以把ip、sp进一步抽象为Thread结构体。每一个PCB都是链表中的一个节点,故还需要一个next字段,这样多个进程可以链接成为进程队列。
/* 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;
my_schedule函数是实现进程调度及上下文切换的核心。
对于进程的调度,我们可以简单地选择进程链表中的下一个就绪进程。
我们重点关注如何实现两个进程间上下文的切换。上下文切换的核心是代码执行流的切换,二与代码执行流的控制相关的主要有三个寄存器:ip、sp以及bp,它们分别指示执行流的下一条指令、栈顶以及栈底的位置,后两者构成指令执行时的栈帧上下文。当然在切换过程中,不仅要载入新进程的执行流,还需要保存原进程执行流的快照。
以进程A切换到进程B为例,大致流程如下:
-
保存进程A当前的bp值
-
保存进程A当前的sp值
-
更新寄存器sp为进程B的sp值
-
保存进程A的ip值
-
更新寄存器ip为进程B的ip值
至此控制流完成跳变,指令继续向下执行。有几处关键点需要稍加说明:
-
进程A的bp保存在自身的栈中;而其sp和ip都保存在PCB中
-
保存进程A的ip值时,保存的是下一条指令的位置,即进程A下次被调度时执行的第一条指令。而这条指令所做的就是从自身的栈中还原bp值。对于进程B来说亦是如此。
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_start_kernel内添加相关代码:
对于每个进程,分别设置pid、状态、各自的栈空间,将代码入口置为函数my_process地址。各进程PCB之间构成环形链表。从pid为0的进程开始执行,设置ip、sp,bp,其中bp = sp表示当前执行栈为空。
而函数my_process则在一个循环中周期性地调用my_schedule。
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); } } }
将上述三个源文件移动到Linux源码下的mykernel目录下替换原来的文件。再次编译并在虚拟机中启动,可以看到进程调度成功运行。