编写实模式多任务操作系统模型之二.docx
《编写实模式多任务操作系统模型之二.docx》由会员分享,可在线阅读,更多相关《编写实模式多任务操作系统模型之二.docx(17页珍藏版)》请在冰豆网上搜索。
![编写实模式多任务操作系统模型之二.docx](https://file1.bdocx.com/fileroot1/2022-10/28/cd0272ef-3cdb-4009-94a4-66dbea085a49/cd0272ef-3cdb-4009-94a4-66dbea085a491.gif)
编写实模式多任务操作系统模型之二
在前面,我们了解了X86模式的功能特点,运行机制并对程序模块进行了分析。
下面将继续分析程序模块的进程调度子程序和其他辅助程序,并完成这个程序的编译及安装运行。
进程调度子程序scheduler
scheduler是系统的进程调度程序,是实现多任务关键的部分。
它实质上是嵌入到系统时钟中断内来完成进程调度功能的。
由于系统时钟以18.2次/秒的频率发生中断,这样每当时钟中断一发生,便可带动进程调度程序执行,所以这个OS模型是以时钟中断来驱动的。
为了不影响原来时钟中断程序的例行工作,它开始运行时首先要调用位于ROMBIOS中的0xF000:
0xFEA5处的时钟中断服务程序。
返回后,它从current_task单元中找到当前被中断进程的进程号,根据该进程号在SPtable中找到其进程栈指针的存放地址,将其栈指针存入到该地址中,为下一次运行做好准备。
而后将current_task加1,得到下一个将被调度运行的进程号。
由该进程号在SPtable中找到上一次该进程被中断时的进程栈指针,将CPU的SP寄存器设为该值,这样堆栈指针便指向进程栈的栈顶。
由于每次进程被中断运行时在进程栈顶都保存有断点处的CPU内部通用寄存器及ES和DS的值,此时将这些值恢复到相应的寄存器中,最后通过一条IRET指令便可将CPU的控制权转移到这一进程上,这个进程便重新从断点处重新运行。
整个系统便在调度程序的控制下循环往复地运行,依次将CPU分时地切换到各个进程上。
由于切换的速度较快,所以宏观上我们便看到几个进程在同时向前并行推进,互不影响。
在微观上,几个进程轮流占用CPU,分时运行。
Scheduler初次被激活是Kernel全部初始化工作完成后。
Kernel初始化工作完成后,各个进程堆栈空间布局均如图7所示的形式,其中“Task1偏移地址”单元中为相应进程入口地址的偏移地址。
在Kernel程序中:
jmp$;kernel在此等待时钟中断的到来
该指令相当一条无穷循环指令while
(1);,当时钟中断到来时,它将从无穷循环中转到scheduler去执行,而后每一次时钟中断都引起scheduler的运行,这时的各个进程堆栈空间布局仍有如图7所示的形式,只不过此时堆栈的底端由于进程的运行已经有进程所压入的数据,留下了进程运行过的痕迹,如图8所示。
调度程序运行流程如图9所示。
具体工作步骤如下(假设调度程序运行前,CPU正处于task1的进程空间,SP指向task1的进程堆栈的栈顶,此时发生时钟中断,scheduler投入运行):
1.调度程序将task1进程断点处的CPU内部通用寄存器及ES和DS的值压入到task1进程堆栈中,代码如下:
pusha
pushes
pushds
2.向中断控制器8259发送EOI中断结束指令:
moval,0x20
out0x20,al
3.将标志寄存器Flag压入堆栈,调用位于ROMBIOS中的0xF000:
0xFEA5处的原时钟中断服务程序,使原时钟中断服务程序完成其原有的工作,代码如下:
pushf
call0xF000:
0xFEA5
由于原时钟中断为硬件中断,所以中断服务程序返回指令为iret。
iret指令依次将栈顶字单元的内容弹出到IP、CS及Flag中,这样为了保证上述call指令的正确返回及堆栈不被破坏,所以要在call指令的前面加入pushf指令,以和iret的动作相配合。
4.保存将被剥夺执行权的task1进程堆栈指针。
由于进程堆栈中保存有进程被中断时的断点信息,而其它进程运行时也要使用CPU内部的SP寄存器,所以在进行任务切换前要做进程的堆栈指针的保存工作。
如下指令所示:
movax,[current_task]
movbx,ax
shlbx,1
leasi,[SPtable]
mov[ds:
si+bx],SP
在current_task内存中存放当前运行进程的进程号,以为索引可以在SPTABLE数组中找到该进程堆栈指针的存放地址,即进程堆栈指针的存放地址=SPtable+2*进程号,找到该地址将进程断点处的SP存入其中。
5.切换到下一将被调度运行task2的进程堆栈空间。
由于在进程堆栈保存有进程的断点信息,所以找到进程堆栈的顶位置便可转入到该进程运行。
scheduler对于各进程的调度采用轮转式的方式,即按照task1-->task2-->task3的顺序,而后再从task1开始依次循环,使各个进程得以运行。
所以将current_task的值增1,再判断current_task是否超过最大进程数MAXTASKS,若超过,则使其复位,从0开始重新计数即可。
取得合适的current_task值后,按上面步骤的公式便可找到将被调度进程的堆栈指针的存放地址,从中取出值送到SP寄存器即可。
如下段程序代码所示:
incax;取得下一将被调度进行的进程号
cmpax,MAXTASKS;该进程号是否超过最大进程数
jbmove_on;若没超过,跳到move_on继续
xorax,ax;超过最大进程数,清0,从第一个进程开始调度
move_on:
mov[current_task],ax
;将更改后进程号送入current_task单元保存,按公式进程堆栈指针的存放地址=SPtable+2*进程号
取得进程堆栈指针
movbx,ax
shlbx,1
leasi,[SPtable]
movSP,[ds:
si+bx];将进程堆栈指针送到SP,切换到进程堆栈空间
6.恢复断点信息,使下一被调度进程从断点处运行。
popds
popes
popa;恢复所有被保存的CPU内寄存器值
iret;激活被调度进程
其它辅助程序
系统中存在3个被调度的程序,分别为task1、task2、task3。
由于这是一个实验程序,所以它们的功能较为单一,主要是将自身的一个16位计数器进行循环计数,而后通过直接写屏方式在显示器上显示出来。
由于现行机器的运行速度较快,为使用户能看清计数器的变化过程,在各个进程的执行过程中加入了人为的延时控制。
每一个进程都是一个无限的循环过程,只要调度程序在运行,各个进程就会无限运行。
此外系统中还有几个辅助程序:
◆16进制数显示子程序printhex
这个子程序主要是为系统内的3个演示进程服务,它可以将从堆栈传递过来的一个16位的数以16进制形式显示到屏幕的指定位置,通过直接写屏方式在显示器上显示出来。
在VGA的80X25彩色文本显示模式下,一个字符在显示缓冲区上占2个字节的空间,其中所显示字符的ASCⅡ码存于低地址单元中,字符的显示属性(前景、背景、高亮和闪烁等)存于高地址单元中,如果将字符及其显示属性存储位置弄错,将会产生花屏的现象。
Printhex根据显示位置参数计算出所需显示的准确位置,避免产生花屏的现象。
调用者只需提供合法的显示位置即可,行号应在1~25之间,列号在1~80之间。
◆键盘中断子程序keybd
它是一个基本的内核级键盘中断处理程序,显示键盘的ASCⅡ码到屏幕上。
它不受调度进程的控制,只要有键盘中断发生,该中断服务程序就会运行。
在该程序被触发运行时,从60H端口读入键盘的扫描码。
扫描码最高位的状态标志着其是键入码还是释放码,如为释放码则不进行处理,直接返回;若为按键的压入码,则以键入码作为索引在键盘扫描码/ASCⅡ码对照表查得对应的ASCⅡ码,通过int10h功能调用将其输出到显示器上。
在键盘中断返回前,要向键盘内的单片机发复位信号,先将61h端口的最高位置1,而后再置0,这样键盘才不会死锁。
kernel.asm完整代码及注释如下:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;内核程序kernel.asm
;在系统时钟中断的驱动下,依次使系统内的3个进程轮流占用CPU完成各
;自的工作:
在屏幕上指定位置循环显示自己的计数器。
具有内核级键盘中
;断,显示出键盘ASCII码
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[BITS16]
[ORG0x00]
STACKSIZEequ0x400;各进程的堆栈空间大小为1k个字节
MAXTASKSequ3;最大进程数
VIDEORAMequ0xb800;显示缓存段地址
KERNELSEGequ0x8000;kernel程序段地址
;------------------------------------------------------------------
start:
movax,KERNELSEG
movds,ax;数据段,堆栈段寄存器的
movss,ax;值均设为KERNELSEG
movsp,0xFFFF;kernel堆栈指针=0xFFFF
movdx,0x3f2
moval,0x0c
outdx,al;关闭软驱马达
movax,0x0003
int0x10;清屏
movdx,0x0600;进程的堆栈基地址
adddx,STACKSIZE;进程1的堆栈指针=0xA00
movax,task1;进程1入口地址
calltaskinit;初始化进程1
adddx,STACKSIZE;进程2的堆栈指针=0xD00
movax,task2;进程2入口地址
calltaskinit;初始化进程2
adddx,STACKSIZE;进程3的堆栈指针=0x1000
movax,task3;进程3入口地址
calltaskinit;初始化进程3
;重新设置系统定时器的中断向量
cli;关中断,以免在设置中断向量发生意外
movax,KERNELSEG
pushds;保存ds值
movbx,0
movds,bx;ds=0
movword[ds:
0x08*4],scheduler;定时器中断向量偏移送到0:
20~0:
21
movword[ds:
0x08*4+2],ax;定时器中断向量段地址送到0:
22~0:
23
;重新设置键盘的中断向量
movword[ds:
0x09*4],keybd;键盘的中断向量偏移送到0:
24~0:
25
movword[ds:
0x09*4+2],ax;键盘的中断向量段地址0:
26~0:
27
popds;恢复ds值
sti;开中断,允许中断进入
jmp$;kernel在此等待时钟中断的到来
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;被调度进程task1
;功能:
对于自身的计数器进行计数,而后在屏幕的3行1显示输出
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
task1:
.l1decword[task1ctra];产生短时间延迟,以免计数显示过快