else
return(-1); /* InitDos() hasn't been called */ }
2.4.2 调度程序的实现
1.调度算法设计
本次课程设计题目要求实现基于优先权的时间片轮转调度算法,即当前线程时间片到时,应暂停当前线程的运行,重新选择一个优先级最高的就绪线程参加运行。为了简单,在我们给出的实例中没有使用优先权,也没有建立就绪队列,而只是用TCB 的数组下标隐含地把所有的线程排成一个循环队列,并使用简单的时间片轮转调度算法,即当前正在运行的线程时间片到时,就暂停它的执行,把它的状态从执行态变为就绪态,把它的现场信息压入它的私有堆栈,并从隐含队列中找出下一个就绪线程,恢复它的现场,让它执行。
只要稍作改动,我们就可使用优先权高者优先的时间片轮转调度算法。为此,我们需在TCB中增加一优先数字段,并将优先数与优先权联系起来(如:优先数大者优先权较低,优先数小者优先权较高,也可反之),我们也可使用某种原则修改优先数(如:一个线程在就绪队列中等待一个时间片,则将它的优先数减去某个值a, 以提高它的优先权而避免长期等待;一个线程执行完一个时间片,则将它的优先数加上某个值b, 以降低它的优先权)。 另外,我们也可在TCB中加一链接字段, 把所有就绪的线程按某种方式排成一显式就绪队列,如优先权从高到低的队列,这样寻找新线程参加运行时只要取出就绪队列的队首线程即可。
2.调度程序的实现思路
引起CPU调度的主要原因有:时间片到时、 线程执行完毕或正在执行的线程因等待某事件发生而不能继续执行。
根据上述调度原因,在实例中的调度功能是通过两个函数来完成的:
(1)void interrupt my_swtch(void):该函数主要解决两种原因引起的调度:线程执行完毕或正在执行的线程因等待某事件发生而不能继续执行。
(2)void interrupt new_int8(void):该函数主要解决因时间片到时引起的调度,这可通过截取时钟中断(int 08)来完成。
在有的系统中,调度程序只有一个。如在UNIX系统中,时钟中断服务程序内并不直接进行CPU调度, 而只是设置需重新调度的标志; 每当中断(包括时钟中断)或捕俘(类似于PC 机的软中断)结束时,中断总控程序将检查重新调度标志,若重新调度标志已置上且被中断的是用户态程序,则系统的中断总控程序将控制转向调度程序来切换CPU。
3.调度程序的具体设计
(1)void interrupt my_swtch(void) 该函数要完成的主要工作包括:首先保存当前线程的运行环境,包括私有堆栈的段址和偏移量;然后在系统的TCB数组中查找状态为就绪态的线程,恢复其运行现场,使其在cpu上参加运行。具体的程序设计流程图如图2-9所示。图中的current和timecount是实例中定义的两个全局变量,其中current记录当前正在运行的现场的内部标识符,timecount记录当前线程从上次调度至今已经运行了多少时间,注意timecount的时间单位是一个时钟中断间隔1/18.2 秒,约等于55ms,比如timecount的值为1,则表示当前线程已经运行了约55ms。
- 21 -
关中断保护正在执行的线程current的现场,暂停它的执行:tcb[current].ss=_SStcb[current].sp=_SPif(tcb[current].state=RUNNING) tcb[current].state=READY 找到一个新的就绪线程i恢复线程i的现场,把CPU分派给它:_SS=tcb[i].ss_SP=tcb[i].sptcb[i].state=RUNNING置线程i为当前线程:current=i重新开始计时:timecount=0开中断返回 图2-9 my_swtch()的程序设计流程图
(2)void interrupt new_int8(void) 该函数要完成的主要工作包括:首先执行老的时钟中断处理程序的功能;然后判断当前线程的时间片是否到了,如果到了,且DOS不忙,则保存当前线程的运行环境,重新选择一个新的就绪线程,恢复其运行现场,使其在CPU上参加运行。
由于该函数必须是自动地定期地运行,所以我们通过截取时钟中断(int 08)来完成。 在PC机上,有一个定时器,每当它发一个时钟信号时(一般情况下,它每秒发出18.2个信号,除非是对产生该中断的定时器芯片重新编程),系统就进入时钟中断处理程序(INT 8)来完成系统计时、调用INT 1CH、关闭磁盘马达等工作。
我们可设计新的时钟中断服务程序,在其中来完成因时间片到时引起的CPU 调度。设计中要注意:新的时钟中断服务程序不能太长,否则系统效率将大大下降甚至使系统无法正常工作;在新的时钟中断服务程序里必须调用系统原来的INT 08H的功能,否则将影响磁盘马达的关闭和系统的计时,另外,我们还要依赖原来的INT 08H 向中断控制器发中断结束指令(EOI)。
在为INT 08H设置新的时钟中断服务程序前,必须提前用Turbo C提供的getvect()函数获取系统原来的INT 08H 的中断服务程序的入口地址并将它保存起来: void interrupt (*old_int8)(void); /*定义一个函数指针old_int8*/ old_int8=getvect(8);
new_int8()具体的程序设计流程图如图2-10所示:
- 22 -
调用原来的时钟中断服务程序(*old_int8) ();进行计时:timecount++;当前线程的时间片到否?是DOS忙吗?否调用my_swtch()进行重新调度否是
返回
图2-10 new_int8()的程序设计流程图
为了观察不同的时间片对调度所起的影响,实例中用一符号常量TL来表示时间片的大小,单位与timecount变量一样为时钟中断的间隔1/18.2 秒;对于当前线程来说,在每次时钟中断发生时,timecount加1。将timecount的值与TL 比较可判断当前线程的时间片是否到时,从而决定是否要进行CPU调度。
current 是实例中的另一全程变量,始终等于正在执行线程的内部标识符。 最后有几个问题留待同学们思考:
(1)在用my_swtch()进行CPU调度时,要不要判断DOS的状态?为什么? (2)在时间片调度时,时间片TL为什么不能太大或太小?
2.5 基本实例程序的实现
根据前面对设计思路的分析,我们可以编写一个较完整的实例:首先由main()函数进行一些初始化工作,然后直接创建0#线程对应于main()函数,再由0#线程调用create()创建1#、2#线程分别对应于函数f1()、f2(),最后,将系统的中断服务程序设置成new_int8(),并把控制交给1#线程,启动多个线程的并发执行。不过在这个基本的实例程序中,我们没有实现线程的阻塞与唤醒、线程的同步与互斥、线程间的通信等内容,请同学们在学习了后面2.6-2.8节的内容后自行补充完成。
0#线程是一个比较特别的线程,它在创建时没使用create(),而是在系统初始化后直接创建的,因它所对应的程序段为main函数中的一段,所以也直接使用整个系统的堆栈,而不在创建时为私有堆栈分配额外的空间;同样,撤消时也不需释放私有堆栈的空间,所以也没使用destroy(),而是直接撤消的,从这方面看,我们可将它看成是一个系统线程。另一方面,在启动多个线程的并发执行后,0#线程就将系统控制权转交出去,直至系统中其它线程都不具备执行条件时,它才可能重新得到CPU,从这一点看,它相当于是一个空转线程。最后,0# 线程还担负一个特别的使命——等待系统中所有其它线程的完成,此时,它将直
- 23 -
接撤消自己并恢复原来的时钟中断服务程序,从而终止整个多任务系统。
实例中f1()、f2()、main()函数的具体实现代码如后面所示,其中tcb_state() 的功能为输出所有线程的状态信息;finished()函数的功能为检查系统中除0#线程以外的所有其它线程是否都已运行完成,是则返回1,否则返回0。
void f1(void) { int i,j,k;
for(i=0;i<40;i++){ putchar('a'); /*延时*/
for(j=0;j<10000;j++) for(k=0;k<10000;k++); } }
void f2(void) { long i,j,k;
for(i=0;i<30;i++){ putchar('b');
for(j=0;j<10000;j++) for(k=0;k<10000;k++); } }
main() {
InitInDos(); InitTcb();
old_int8=getvect(8); /* 获取系统原来的时钟中断服务程序的入口地址 */
/* 创建0#线程 */
strcpy(tcb[0].name, \ tcb[0].state=RUNNING; current=0;
create(\ create(\ tcb_state();
/* 启动多个线程的并发执行 */
setvect(8,new_int8); /*为时钟中断设置新的中断服务程序*/ my_swtch();
while(!finished());
/* 终止多任务系统 */
- 24 -
tcb[0].name[0]='\\0'; tcb[0].state=FINISHED; setvect(8,old_int8);
tcb_state();
printf(\ }
实例的总流程如图2-11所示。
多任务系统开始系统初始化创建0#线程创建1#、2#线程截取时钟中断,启动线程的并发执行,使控制转向1#线程1#重复输出字符“a”撤销自己2#重复输出字符“b”撤销自己否0#其他线程已完成CPU调度程序是终止整个多任务系统输出结束信息 图2-11 实例总流程图
最后,补充说明一点,在程序运行的过程中,若DOS发现输入流中有Ctrl-C,DOS将执行INT 23H的中断服务程序。通常,该中断服务程序将终止当前程序的运行,并将控制转给当前程序的父进程(在这里是DOS的COMMAND程序),但它不会将时钟中断程序恢复成原先的(*old_int8)(), 这种不正常的结束将导致整个系统死机。要避免这个问题有两种方法:一种是不使用Ctrl-C;另一种更好的办法是修改INT 23H的中断服务程序,使它或是不起作用,或是撤消当前正在执行的线程, 或是正常地终止整个多任务系统。我们把这个问题留给同学们自己去解决。
- 25 -