----------------------操作系统是如何工作的----------------------
上个实验研究了Linux系统对内存的管理。其中函数的调用通过栈机制完美地执行。那么,不禁要问,操作系统又是如何工作的呢?我们都知道,其实一个程序段在运行时就是一个进程。那么,我们一起来研究一下进程是如何调度的。
该实验就是实现一个简单的单时间片轮转的微型操作系统,从而理解操作系统是如何对进程进行调度。
下面开始实验!
1使用终端,输入
cd LinuxKernel/linux-3.9.4
qemu -kernel arch/x86/boot/bzImage
启动mylinux
屏幕上不断地输出>>>>>my_timer_handlerhere <<<<<和my_start_kernel here,这实际上已经形成了一个小型的进程相互切换的过程,我们一起来对它分析一下。(其中,qemu是一款可单独运行的模拟器,它非常灵活且可移植)
转到mykernel文件夹下,查看其源代码。
主要的代码段就两个:mymain.c和myinterrupt.c。
mymain.c就是一个死循环,一直不断地打印my_start_kernel here。中间加上if判断只是因为,cpu处理一次循环的速度太快,它只每100000次循环打印一次,以便我们观察。
myinterrupt.c是一个被时间周期调用的函数,也可以看成是一个死循环的功能,一直打印>>>>>my_timer_handler here <<<<<。但与一般的死循环不同是,一般的死循环只是在做循环,而这个是周期性的调用函数。
因此,mymain.c中的my_start_kernel只调用了一次,而myinterrupt.c中的my_timer_handler一直被调用。这其实就相当于,进程的初始化与进程的调度。
2接下来,我们要完成自己的操作系统
大家都说这个实验很难,所以为了便于理解,在这里先不要看源代码,我们再仔细思考设计一下我们的系统。
要做进程的调度,即切换进程,那么首先要先知道是要从哪个进程切换到哪个进程。很显然,我们会把正在工作的进程叫为活跃进程,而要切换的进程叫为就绪进程。那么,我们就需要一个标志位来记录进程的状态,而进程的识别显然是用进程号pid来实现的。其次,调度进程就必然涉及现场的保存与恢复,这样就还需记录进程当前的运行的信息。这样对于一个进程,我们要记录这么多的信息,那么最好的方法是利用结构体。因此,我们需要一个头文件。不妨,我们就命名这个头文件叫mypcb.h。
有了这些,当然接下来要使一个进程开始运行,那么,就需要对进程进行初始化。刚才我们知道,mymain.c主要完成初始化工作,那么我们还可以继续利用这个文件,只要有所改动,使之对目前的结构体进行初始化即可。
显然接下来,被时间周期调用的myinterrupt.c也应随之改动。完成这些之后,还有一个问题,进行调度,显然要有调度算法,这该在何处实现呢?显然,就在myinterrupt.c里加入一个函数来控制打断进程的周期和顺序即可,就命名为my_schedule吧。
好的,这样思路就很清晰了:
(1) 写一个记录状态信息的结构体头文件mypcb.h
(2) 对应修改mymain.c来进行初始化
(3) 对应修改myinterrupt.c来进行打断
(4) 在中增加my_schedule函数来进行调度的管理
要做的工作清楚了,最大的技术难点在哪呢?显然是对现场的保存与恢复。这时我们要重新理解上一个实验《计算机是如何工作的》,cpu识别一个函数,就是依靠ebp、esp来确定函数的栈空间,然后利用eip来找到执行指令的位置。所以cpu对整个内存的识别,就是来靠读取寄存器的信息来确定的,而进程实质就是一个在运行的函数段,所以进程的现场也就是各个寄存器的值。这样就非常清晰了,只要将寄存器的值压栈,就可以放心调度另一个进程了,再将压栈的值出栈还给各寄存器,就又回到了原进程的运行,看起来就像什么都没发生过一样。这就是现场的保存与恢复。
明确了这一系列的问题,我们再来看源代码,这样就容易多了。
mypcb.h中结构体是typedef struct PCB,再加上其他的函数声明而已。
结构体PCB的内容是
pid:进程号
state:进程状态(就绪态为-1,运行态为0)
stack:进程的栈
thread:线程信息
task_entry:进程入口
next:下一跳,用于找到下一个PCB对象进程
mymain.c用于系统启动后对进程初始化并启动,然后创建其他进程的PCB。利用全局变量my_need_sched来标识是否要进行调度,然后交给my_schedule进行时间轮的管理。
myinterrupt.c里my_timer_handler 函数被周期性地调用,然后my_schedule函数完成管理,即进程切换工作。切换的方式我们之前已经说过,就是将当前的一些寄存器的值保存起来与读取的操作,利用汇编代码实现。这里只需要保存ebp、esp、eip即可,ebp+esp可以确定栈桢,然后eip即是返回地址,也就是指令正运行的位置,用于之后回调。
现在,所有的工作都完成了,我们来编译运行一下我们的系统!
可以看到,2进程正切换3进程。结果非常符合我们的预期。
总结
通过这个实验,我们可以清晰的看到,进程的调度的过程,也就明白了操作系统的工作机制。这样,只要时间周期足够的短,人类无法识别,我们在使用的时候就感觉若干任务进程好像在同时运行一样。而且也更为深入的理解了由ebp、esp确定的栈桢的意义。现在的操作系统都是在这个基础上实现的。
-----------------------------END--------------------------------
刘建鑫 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
------------------------------------------------------------------