实验基于https://github.com/mengning/mykernel完成
一.配置虚拟机QEMU
安装过程不再阐述,参考上方链接即可
为了使得qemu能够正常进行debug,需要设置相关内核选项
# 关闭KASLR,否则会导致打断点失败 Processor type and features ---->
之后终端输入make,编译。
接下来配置内存根文件系统,需要用到busybox。
首先,取消busybox的动态链接,然后编译,将编译后_install下的文件,以及dev目录下的文件打包制作根文件系统。
由于默认的内核命令行上有 init=/linuxrc, 因此,在文件系统被挂载后,运行的第一个程序是根目录下的 linuxrc。 这是一个指向/bin/busybox 的链接,也就是说,系统起来后运行的
第一个程序也就是 busybox 本身。
二,代码具体分析
首先定义进程控制块PCB
PCB主要包含下面几部分的内容:
1. 进程的描述信息,比如进程的名称,pid,
2. 处理机的状态信息,当程序中断是保留此时的信息,以便CPU返回时能从断点执行
3. 进程调度信息,比如进程状态,优先级等等
4. 进程控制和资源占用,同步通信机制,链接指针(指向队列中下一个进程的PCB地址)
1 typedef struct PCB{ 2 int pid;//进程id 3 //进程状态 4 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 5 char stack[KERNEL_STACK_SIZE];//每个进程都有自己独立的栈空间 6 /* CPU-specific state of this task */ 7 struct Thread thread;//线程 8 unsigned long task_entry;//函数入口地址 9 struct PCB *next;//下一个进程控制块 10 }tPCB;
接下来定义线程
struct Thread { unsigned long ip;//指向的是函数地址 unsigned long sp;//指向栈底 };
由线程的定义可见,线程自己不拥有自己的地址空间,它使用的是进程的栈,也就是线程和进程共享数据。
接下来初始化所有的pcb,每个进程的pcb的入口地址,以及线程的ip,其值都是my_process函数的地址。
int pid = 0;//0号进程 int i; /* Initialize process 0*/ task[pid].pid = pid; task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ //任务入口,即my_process函数的地址 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;//把my_process函数的地址赋给了ip 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) { memcpy(&task[i],&task[0],sizeof(tPCB));// task[i].pid = i; task[i].state = -1; task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];//sp指向栈底 task[i].next = task[i-1].next;//当前的next指向上一个,上一个的next指向当前。最后的pcb的next指向第0个pcb,形成环形 task[i-1].next = &task[i]; }
接下来,准备进程切换/调用
1.将调用者的ebp压栈处理,保存指向栈底的ebp的地址(方便函数返回之后的现场恢复),此时esp指向新的栈顶位置;
push ebp
2.将当前栈帧切换到新栈帧(将ebp值装入esp,更新栈帧底部), 这时ebp指向栈顶,而此时栈顶就是old esp ,
mov esp, ebp
3.之后将my_process的地址入栈,ret执行后rip保存my_process的地址,之后就会进入这个函数
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*/ );
进程切换的过程与上面的过程类似,都是先保存当前栈底rbp,这是为了之后的返回,
接着调整把当前线程thread的ip,和sp保存到pcb中
然后下一个pcb的ip,即函数地址,压栈,因为rip无法直接操作
然后rsp指向了新的堆栈的栈顶
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) );
为了清楚知道rsp,rbp的变化情况,根据vscode的调试,方便起见,把stack的size调整到8*8
一开始,4个pcb的stack全都是空,那么在运行上述代码的时候,当rsp指向next的stack栈顶时,stack全空,如图
那么当执行popq %%rbp的时候, rsp-8的位置,也就是本该存放rbp的位置,全0,那么在弹栈的时候,rbp的值应该也为全0
为了验证rbp的值到底是多少,修改popq %2,也就是弹栈到next->thread.sp
不知道什么原因,therad.sp的值在前后并未发生改变。。。
本来以为popq可能没有执行,但是将popq %%rbp删除后,第一次循环正常运行,当再次循环到0号pcb的时候就发生错误了
但是可以看到新堆栈的值已经改变
后来查询,在vscode调试控制台输入-exec info registers可以直接查询寄存器的值
在进入进程切换前
rbp=0xffffffff82b57b00,rsp=0xffffffff82b5bb3f
切成切换后:
rsp的值确确实实被改变了,但是,rbp的值并没有变化(疑惑),并不知道为什么会这样。
在一开始,四个pcb中的stack全都是空的,当准备切换到下一个进程时,rsp指向了下一个进程stack的栈顶,rsp+8的位置理应保存的是rbp的值,但是此时栈是空的,如果rbp值变化了,那么他的值应该是全0,然而rbp并没有变。
接下来:
中断在cpu中扮演者重要的角色,cpu每隔一定的时间就会自动检查是否又中断信号,例如每次敲击键盘都会产生一次中断
在qemu初始化时,就产生了一个中断,每次中断就会调用中断处理函数
通过调试,可以看到time.c中调用了自定义的处理函数
在这个中断处理函数中,会把my_need_sched改为1
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; }
接着在init函数中的my_process检测到my_need_sched=1时,就会进行进程切换,也就是上面的那段汇编代码
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); } } }
四个pcb会循环执行,中断处理定期改变my_need_sched的值为1,进程切换时再改变为0
三,总结
通过断点调试kernel,了解了中断的产生以及中断在计算机中扮演的重要角色,最主要的是明白如何通过栈指针的调整(rbp,rip,调整rsp),完成进程之前的切换。