第二章 操作系统是如何工作的
一、虚拟一个x86的CPU硬件平台
-
在实验楼环境下输入如下命令:
> cd ~/LinuxKernel/linux-3.9.4 > qemu -kernel arch/x86/boot/bzImage
得到如图结果:
-
QEMU窗口不显示输出结果,为方便理解内核启动效果,重新输入如下命令:
> cd ~/LinuxKernel/linux-3.9.4 > rm -rf mykernel > patch -p1 < ../mykernel_for_linux3.9.4sc.patch > make allnoconfig > qemu -kernel arch/x86/boot/bzImage
经过make后,可在QEMU窗口清楚看到内核启动效果:
-
进入mykernel目录,可以看到QEMU输出内容的代码mymain.c和myinterrupt.c,如图:
mymain.c的执行功能是每100000次输出一次“my_start_kernel here i”,同时有一个中断处理程序的上下文环境,周期性地产生时钟中断信号,能够触发myinterrupt.c中的代码(如下图),输出“>>>my_timer_hender here<<<<”。
二、在mykernel基础上构造一个简单的操作系统内核
-
增加一个 mypcb.h 头文件,用来定义进程控制块,即进程结构体的定义,具体代码请点击“mypcb.h”;
-
对 mymain.c 进行修改,这里是mykernel内核代码的入口,负责初始化内核的各个组成部分。现对部分关键代码进行分析:
- 启动第一个进程
/* start process 0 by task[0] */ pid = 0; my_current_task = &task[pid]; asm volatile( "movl %1,%%esp\n\t" /* 将进程原堆栈栈顶的地址(这里是初始化的值)存入ESP寄存器 */ "pushl %1\n\t" /* 将当前EBP寄存器值入栈 */ "pushl %0\n\t" /* 将当前进程的EIP(这里是初始化的值)入栈 */ "ret\n\t" /* ret命令正好可以让入栈的进程EIP保存到EIP寄存器中 */ : : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ ); }
具体分析如下图:
附:这里用到vi编辑器的全删除,可输入可输入
:1,$d
从第一行删到末尾,其他删除操作点击这里访问。 -
对 interrupt.c 进行修改,主要是增加了进程切换的代码,现对关键代码进行分析:
- 进程0启动,开始执行my_process(void)函数的代码。
if(next->state==0) /*next->state==0对应进程next对应进程曾经执行过。*/ { //进行进程调度关键代码。 asm volatile( "pushl %%ebp\n\t" /*保存当前EBP到堆栈中。*/ "movl %%esp,%0\n\t" /*保存当前ESP到当前PCB中。*/ "movl %2,%%esp\n\t" /*将next进程的堆栈栈顶的值存到ESP寄存器。*/ "movl $1f,%1\n\t" /*保存当前进程的EIP值,下次恢复进程后将在标号1开始执行。*/ "pushl %3\n\t" /*将next进程继续执行的代码位置(标号1)压栈。*/ "ret\n\t" /*出栈标号1到EIP寄存器。*/ "1:\t" /*标号1,即next进程开始执行的位置。*/ "pop1 %%ebp\n\t" /*恢复EBP寄存器的值。*/ : "=m" (prev->thread.sp),"=m" (prev->thread.ip) : "m" (next->thread.sp),"m" (next->thread.ip) ); my_current_task=next; printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); } else { next-state=0; my_current_task=next; printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); /*转换到新的进程中*/ asm volatile( "pushl %%ebp\n\t" /*保存当前EBP到堆栈中。*/ "movl %%esp,%0\n\t" /*保存当前ESP到当前PCB中。*/ "movl %2,%%esp\n\t" /*载入next进程的栈顶地址到ESP寄存器。*/ "movl %2,%%ebp\n\t" /*载入next进程的堆栈基地址到EBP寄存器*/ "movl $1f,%1\n\t" /*保存当前EIP寄存器值到PCB,这里$1f是指上面的标号1。*/ "push %3\n\t" /*把即将执行的进程的代码入口地址入栈。*/ "ret\n\t" /*出栈进程的代码入口地址到EIP寄存器。*/ : "=m" (prev->thread.sp),"=m" (prev->thread.ip) : "m" (next->thread.sp),"m" (next->thread.ip) ); }
三、总结
本章主要借助Linux内核部分源代码模拟了计算机3法宝(存储程序计算机、函数调用堆栈、中断),通过认真对源代码中汇编部分的分析,基本了解了进程的切换,进程在执行过程中,如果有需要调度其他进程时,需先保存当前进程的上下文环境,当该进程被调度时需先恢复上下文环境,以此实现了进程的并发执行,大大提高了计算机运行效率。