本实验基于https://github.com/mengning/mykernel 实现。
一、虚拟一个x86-64的CPU硬件平台:
实验流程参见:https://github.com/mengning/mykernel
实验首先拦截linux内核的启动过程,来模拟程序执行的入口和周期性的产生时钟中断。
实验的目录结构为:
其中使用 make defconfig 生成64位x86的配置文件,再使用 make 编译共耗时约21分钟,产出为 arch/x86/boot/bzImage 文件。
通过QEMU虚拟机加载编译好的linux内核,my_start_kernel和my_timer_handler无限交替执行。
分析:linux内核初始化执行到my_start_kernel()函数的时候,由于函数里面是while无限循环,所以每隔10万次计数的时间printk输出my_start_kernel here。由于cpu定时产生时钟中断,每次产生时钟中断都要调用时钟中断处理函数,时钟中断处理函数里又调用我们写的my_timer_handler()函数,所以周期性的输出my_timer_handler here。
二、基于mykernel 2.0编写一个操作系统内核:
修改mykernel目录下的mymain.c、myinterrupt.c、mypcb.h文件,基本参考https://github.com/mengning/mykernel 。
修改好文件后重新配置编译内核,并使用QEMU加载,结果如下图:
make defconfig
make
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
三、实验结果分析:
通过实验发现进程在进程0到进程3之间循环调度。进程切换的关键代码是一段嵌入式汇编,最有技巧性的地方是通过pushq %rip和ret指令来间接修改%rip的值,从而更改代码执行流,再配合%rsp和%rbp的修改切换进程的工作栈,从而达到切换进程的目的。具体代码细节课上讲的很详细,这里不再具体分析,仅给出一些思考。
首先理顺代码执行流程,先看看对linux内核源码都修改了什么:
1. 在使用命令 patch -p1 < ../mykernel-2.0_for_linux-5.4.34.patch 时得到:
2. 查看 arch/x86/kernel/time.c 下的代码,在time_interrupt()函数内部调用自己编写的my_timer_handler()函数,在每次发生时钟中断时调用:
3. 查看 include/linux/timer.h 下的代码,这里是my_timer_handler()函数的声明:
4. 查看 include/linux/start_kernel.h 下的代码,这里是my_start_kernel()函数的声明:
5. 查看 init/main.c 下的代码,在函数 asmlinkage __visible void __init start_kernel(void){} 下,start_kernel()函数在内核初始化时被调用,可以看到先执行自己编写的my_start_kernel()函数,再调用rest_init()函数:
仔细观察QEMU的输出发现每1秒切换1次进程。这是为什么呢?因为cpu每秒产生1000次时钟中断,而my_timer_handler()函数每调用1000次输出一次,并更改调度标记,紧接着执行进程调度,同时每一个调度执行的进程都在一直运行my_process()函数。
而cpu每秒产生1000次时钟中断的结论来自于如下代码:
1. 在 linux-5.4.34/include/asm-generic/param.h 下有如下定义,其中HZ就是时钟中断的频率,此处利用了宏定义:
2. 在 linux-5.4.34/arch/x86/configs/x86_64_defconfig 中对CONFIG_HZ默认配置为1000,即一秒产生1000次时钟中断: