基于mykernel 2.0编写一个操作系统内核——Linux操作系统分析第一次作业
实验环境:
Ubuntu 16.0.4
1. 搭建基于mykernel虚拟⼀个x86-64的CPU硬件平台
包括下载mykernel补丁、linux5.4.34内核代码、依赖库文件和qemu模拟器,给linux内核打上mykernel的补丁等等,ppt上已经有详细的命令,逐条运行即可,这里不再赘述。值得一提的是,从github上clone速度还是不太稳定,因此我将这一项目搬运到了码云上,可以:
git clone https://gitee.com/dexttter/mykernel.git
获取整个仓库。总而言之,经过以上的步骤后,我们可以使用qemu加载我们的mykernel的启动镜像:
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
运行结果:
从qemu的窗口我们可以看到:my_start_kernel在持续打印,同时my_timer_handler也在周期性打印。
2. 分析源码
接着上面,我们来关注下my_timer_handler和my_timer_handler这两条打印信息的源头。
进入mykernel文件中,我们看到主要就是两个C代码文件:mymain.c、myinterrupt.c。
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); } }
这是我们mykernel的系统入口,一个死循环,每当计数器等于整100000时打印一条信息。
myinterrupt.c
void my_timer_handler(void) { pr_notice("\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n"); }
更为简洁,只要它被调用就打印my_timer_handler的函数。获取时钟中断、进入时钟中断linux的内核已经帮我们完成,相当于屏蔽了这些细节。因此我们只需要在这里完成我们在中断时想处理的工作即可——也就是我们要完成的进程切换。
3. 完成进程切换代码
参照老师的范本,我们完成了以下工作:
3.1 完成进程控制块这一数据结构的定义和基本操作——mypcb.h
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]; struct Thread thread; unsigned long task_entry; struct PCB *next; }tPCB;
void my_schedule(void);
ip和sp就是eip寄存器和esp寄存器,我们要完成进程的切换,也即进程栈帧的切换,实际上就是把rsp指向即将运行进程栈帧的栈顶.rbp指向栈底。
此外,在pcb这一结构体中,我们还有pid——进程id,state——进程的三状态、task_entry——入口以及next——指向进程链表中的下一进程的指针。
3.2 完成mymain.c
my_start_kermel函数: 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*/ );
它完成了:初始化0号进程:包括初始化pid,状态,入口,ip和sp等等。然后,我们fork了更多的进程,并加入进程管理链表,这是实现进程切换的前提。然后我们运行0号进程,它主要是由这段嵌入式的汇编代码实现的,我们重点关注下:
首先明确:%0指代task[pid].thread.ip,%1指代task[pid].thread.sp。
首先将sp的值赋给rsp,然后将rbp压栈(这里0号进程的栈帧为空,rsp和rbp相等),然后将0号程的ip压栈,return操作完成对eip的pop操作,使得rip = ip=myprocess,开始执行myprocess。
my_process函数:
当计数器my_need_sched=1时,我们运行my_schedule函数(在myinterrupt.c中,下面会提到)完成进程的切换
3.3 完成myinterrupt.c
my_timer_handler: 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; }
当tim_count这个变量为1000,且my_need_sched!=1时我们把my_need_sched置为1,然后会调用my_schedule,这是进程切换的核心,它也是由一段汇编代码实现的:
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) );
它完成的工作可以用上面这张图来解释:
- 保存push rbp,将prev进程的rbp的值压栈
- 把rsp的值保存到它自己的sp变量中
- 之后将next进程的sp变量中的值赋给rsp寄存器(完成栈帧的转换)
- 并保存prev进程的rip
- 随后压栈next进程的ip,也即$1(这里已经是在next的栈帧中操作了)
- 这之后return, 也就是pop next进程的rip,执行next进程也就是$1后面的语句
- pop rbp,将rbp指向next进程的栈底
至此,进程切换彻底完成,rbp和rsp都分别指向了next进程的栈底和栈顶
4. make后重新运行mykernel
完成了进程的切换