您的位置:首页 > 其它

实时多任务系统内核分析

2006-04-28 22:16 197 查看

实时多任务系统内核分析

初次接触实时多任务操作系统的人,往往对实时程序的运行机制感到很困惑:任务在什么时候投入运行?操作系统以什么机制决定目前应该运行哪一个任务?本任务什么时候放弃了对CPU的控制?为了解答以上问题,我们从分析一个很简单的实时调度程序入手,来说明一下多任务程序的运行过程。
从结构上来说,实时多任务操作系统包括两部分,一部分为操作系统内核(kernel),即实时执行程序(Real Time Executive:RTX),另一部分是输入输出部分(I/O)(注意开发系统不属于操作系统的范畴);嵌入式系统对I/O的需求通常比较小(无文件系统需求),因此很多实时多任务操作系统本质上就是一个实时执行程序,如AMX(Kadak),VRTX(Microtec),iRMX(Intel)等(这里的X即:eXecutive),如果纯粹从kernel的角度来考察目前流行的各种实时多任务操作系统的性能,它们的效率差别都不大。
在市面上可以得到一些RTX的源代码(有用C实现的,有用汇编实现的,还有用PL/M语言实现的),从internet上也可以荡一些下来(我介绍一个站点www.eg3.com,堪称世界电子工程师资源宝库),下面我要介绍的一个RTX版本(我命名为SRTX:short RTX),可以说是RTX中的元老级产品了,来自某研究所,九十年代初他们到美国考察,从美国某公司购得。五年以前,SRTX在国内有售,许多搞工控的研究所利用SRTX开发了一些大型或小型的产品,这里介绍的SRTX我作了一些简化和改动.
在功能上,SRTX的确无法和目前市售的实时多任务操作系统相比,不支持任务的调试,不支持优先级反转,甚至不支持相同优先级任务的分时间片运行;SRTX的功能单一,程序代码非常短,效率高(毕竟是80年代末的产品)。从内核的角度看,SRTX实现了一个较基本的任务调度版本;因此通过对SRTX的介绍,可以了解其他实时多任务产品内核的结构及实现方法。

一. 任务的管理及程序实现
1. 任务及程序结构:
从程序实现上说,任务就是一段能完成既定功能的程序代码。与一般的程序代码(或子程序)不一样的地方就在于任务是死循环的程序结构。
任务的通用程序结构如下:
#define void TASK
TASK Common_Task( void )
{
Task_ Init(); // initialize data structure for this task
while( 1 ) // loop forever
{
Suspend_Task_for_Msg(); //wait for message
Process_This_Msg(); //process this message
Post_Msg_to_Task(); //send message to other task
}
}
举例说明:
应用户的要求,需要在屏幕的右上角显示当前时钟,我们可以把此功能当作一个专用的任务来设计,该任务的功能是每隔一秒取出系统当前的时钟,转换成规定格式的字符串,将其指针传送给显示任务,接着继续等下一秒的到来。任务的程序实现结构为:
TASK Alert_Clock( void )
{
Set_String_NULL(); // initialize string
while( 1 )
{
Get_Current_Clock( ); //get clock
Format_Clock_Str( ); //format digit to string
Send_Msg_To_Task( DISPLAY,str ); //post message pointer to
//DISPLAY task
Suspend_Task( 100 ); //suspend 1 second
}
}

2. 数据区及堆栈区的组织方法
以下讲一下SRTX的系统数据区及堆栈区的组织方法。
任务在运行的时候可能被更高优先级的任务中断,这时候任务需要将现场信息放到其堆栈中,以便今后能从该堆栈中取出被中断的现场信息(包括断点位置,任务状态等)恢复任务的断点运行,任务需要有自己独立的堆栈区以及描述任务运行状态措施,在实时多任务系统中,采用一种任务控制块(Task Control Block:TCB)的数据结构描述任务的运行状态,每个任务有一个TCB。
在SRTX中,采用如下方法定义任务的堆栈:
max_task_no equ 32 ;support maxinum tasks
;defined by myself
;--------------------------------
;stacks for all tasks
;--------------------------------
stack segment para stack 'stack'
db max_task_no*1024 dup(?)
stack ends
每个任务的堆栈大小为1K,用户可以根据需要修改(同时需要改动程序)。
在SRTX中,TCB的结构为:(共16个字节:10h)
表1 任务的TCB的结构
偏移(字节) 说 明
0 任务号:Task_ID
1 任务状态:Task_Status
2-3 初始代码偏移:Init_Offset
4-5 初始CS段值:Init_CS
6-7 初始堆栈指针SP:Init_SP
8-9 初始堆栈段SS:Init_SS
10-11 上下文堆栈指针SP:Last_SP
12-13 上下文堆栈段SS:Last_SS
14-15 系统时钟援用:Slice

以下是系统数据区的组织方法:
;---------------------------------------
;buffer for task and queue manage
;---------------------------------------
dseg segment
db 10h dup(?) ;00h-> RUNning task message
; public for all tasks
db "taskdescripitors" ;10h-> TCB starting flag
db max_task_no*10h dup(?) ;20h-> task TCBs message
db "queue tables " ;220h->QUEUE starting flag
db max_task_no*108h dup(?) ;230h->all queues message
db "SRTX version 1.0" ; SRTX version flag
dseg ends
其中:"taskdescripitors"为16字节的标志串,以标明TCB的开始;
"queue tables "为16字节的标志串,标明任务队列的开始;
"SRTX version 1.0"为16字节的版本标志,
关于TCB各字段的含义以及SRTX的队列组织方法见相应章节。
3. 任务状态及其表示
在TCB的Task_Status字段用于描述本任务当前的状态,TCB状态及其它信息也在本字段中描述。
表2 任务状态及其描述
位 状 态 描 述
10000000 INHIBIT 任务被强制禁止执行
00100000 AVAILABLE 本TCB为空,可以使用
00010000 INTERRUPT 任务运行被中断
00001000 ASLEEP 任务处于睡眠状态(调用SYSSLP)
00000010 AWAKENED 任务睡眠超时,被唤醒
00000001 RUNNING 任务处于可运行状态(ready to run)
以上描述了系统数据区的组织方法,尤其描述了其中的任务状态字节,以下程序代码用于系统运行初期对系统数据区的初始化,它是最先需要运行的代码.
;-------------------------------------------
mov ax,dseg
mov ds,ax
cld
push ds
pop es
xor di,di ; initialize public area
mov cx,10h
xor ax,ax
repz stosb ; set 0 for all 10h bytes

mov bx,20h ; bx->first tcb
mov cx,max_task_no
task_dec:
mov byte ptr[bx+01],20h ; initialize tcb
add bx,10h ; set TCBs are all AVAILABLE
loop task_dec

mov bx,0230h
mov cx,max_task_no
task_queue:
mov byte ptr[bx+0100h],80h ; initialize queue is empty
add bx,0108h
loop task_queue

4. 任务创建及启动
为了详细说明TCB中一些字段的使用,我们介绍在SRTX中的创建任务例程。
在SRTX中,syscre为任务创建例程,其使用方法是:
输入:寄存器DX:AX包含初始任务代码段的CS:offset
输出:若创建成功,CARRY清0;此时,AX包含了任务的ID号,它是唯一的任务标识符。若CARRY置位,AX寄存器将包含下列错误码:03eah……任务创建错误。
以下是SYSCRE的典型使用方法:
;---------------------------------------------------------
mov dx, seg NEW_TASK ;put seg to dx
mov ax, offset NEW_TASK ;offset to ax
call SYSCRE ;creat NEW_TASK
jc creat_error ;creat error process
mov NEW_TASKID,ax ;put task id to NEW_TASKID
call SYSSTR ;startup this task
;--------------------------------------------------------

以下是具体的实现代码:
;--------------------------------
;syscre: create a task
;--------------------------------
syscre_sub:
push bx
push cx
push si
push ds ;reserve used regs
mov bx,dseg
mov ds,bx

mov si,0 ;first task ID=0
mov bx,20h ;bx->first TCB
mov cx,max_task_no ;max loop count

find_empty_tcb:
test byte ptr[bx+01],20h ; is this tcb empty?
jnz fill_task_msg ; yes,TCB is AVAILABLE
; please see table 2
add bx,10h ; next tcb
inc si ; task ID++
loop find_empty_tcb

mov ax,03eah ; no tcb AVAILABLE
jmp syscre_error

fill_task_msg:
mov cl,0 ; initial task status
mov [bx+01],cl ; save initial task status
mov [bx+02],ax ; offset of task code
mov [bx+04],dx ; segment of task code
mov ax,si ; si:task ID
mov cx,0400h ; stack length
mul cx
add ax,0400h ; initial task stack pointer
; stack bottom =SP
mov [bx+06],ax ; set initial sp
mov word ptr [bx+08],stack ; set initial stack segment
mov ax,si ; task's ID
mov [bx+0],al ; reserve task ID
clc
jmp syscre_exit

syscre_error:
stc

syscre_exit:
pop ds ;restore used regs
pop si
pop cx
pop bx
ret
由此可见, 在TCB中,Task_ID表示任务创建的顺序号(系统创建的第一个任务, Task_ID=0,创建的第二个任务, Task_ID=1……),任务创建后,Task_Status=0;所有的状态清除。Init_Offset=任务代码的offset,Init_CS=任务代码的CS, Init_SP=该任务创建时系统分配给该任务的SP,Init_SS=该任务创建时系统分配给该任务的SS,在任务创建阶段,TCB的其它域未涉及到。

在SRTX中,sysstr为任务启动例程,其使用方法是:
输入:寄存器AX包含待启动任务的ID号;
输出:若启动成功,CARRY清0;否则, CARRY置位,AX寄存器将包含下列错误码:03e9h……无效任务ID号。
关于SYSSTR的使用示例见上.
以下是SYSSTR的实现代码.
;------------------------------
;sysstr start(or restart task)
;------------------------------
sysstr_sub:
push ax
push bx
push cx
push ds ; reserve used regs

mov bx,dseg
mov ds,bx
cmp ax,max_task_no
jnb sysstr_error ;task ID is too large

mov bx,20h
mov cl,04 ; size of TCB=16,by <<4
shl ax,cl ; get TCB addr for this task
add bx,ax ; bx->this task's TCB
test byte ptr[bx+01],20h ; task created ?
jnz sysstr_error ; task is not created still
or byte ptr[bx+01],01 ; set runnable flag
clc
jmp sysstr_exit

sysstr_error:
mov ax,03e9h
stc

sysstr_exit:
pop ds ; restore used regs
pop cx
pop bx
pop ax
ret
在SRTX中,启动一个任务实质上就是设置该任务的状态为可运行状态,等待调度程序reschedul运行时根据优先级启动该任务的运行.

5. 任务删除及其实现方法
SYSDEL用于删除一个已经创建的任务,释放该任务占用的TCB.
SYSDEL的使用方法:
输入:AX包含将要删除任务的标识符.
输出:如果删除成功,CARRY清零,否则,CARRY置位,AX寄存器包含如下的错误码…… 03e9h,无效任务ID号。
以下是SYSDEL的实现代码。
;---------------------------------------------
; sysdel delete a task
;---------------------------------------------
sysdel_sub:
push bx
push ds
mov bx,dseg
mov ds,bx
cmp ax,max_task_no
jnb sysdel_sub_error ;task id is too large
mov bx,10h
mul bx
mov bx,20h
add bx,ax ;bx->tcb for this task ID
mov al,[bx+01]
or al,20h ;tcb is now AVAILABLE
mov [bx+01],al
clc
jmp sysdel_sub_exit
sysdel_sub_error:
mov ax,03e9h
stc
sysdel_sub_exit:
pop ds
pop bx
ret
任务在创建之后,该任务的TCB的AVAILABLE 字段被设置为0,表明该任务对应的TCB不可用(或已经被占用);SYSDEL实质上就是简单地将该任务的TCB的AVAILABLE 字段被设置为1,重新置为可用(未被占用),这样,今后在创建一个新任务时,可以重新利用该TCB。

6. 禁止任务切换、允许任务切换的实现
在需要对临界区数据进行访问的时候,需要禁止任务的切换,以保护共享的资源.
举例如下:
任务A需要任务B的数据以便继续操作,任务A和任务B通过设置公共变量READY(信号量)来表示数据是否准备好,任务B设置READY的初值为false,一旦B将数据放入公共的缓冲区之后,设置READY为true;任务A挂在信号量READY上,若READY为true,任务A从公共的缓冲区取出B存放的数据.对公共数据的访问就成了临界区的操作.这主要是由于,在A测试READY为true之后,马上取公共缓冲区的数据,如果这时候发生任务的切换,任务B开始运行,它可能将最新的数据放入公共缓冲区中,这样任务A恢复运行后,将继续读取公共缓冲区的数据,使得任务A读取的数据不完整,达不到预期的效果.
对临界区资源的访问时,一定要禁止任务的切换,以保护资源的完整性.
SRTX采用了最简单的关门算法实现对任务切换的管理,即:
需要对临界资源进行访问时,任务A首先将某全局变量M减1,然后访问临界资源;操作系统的调度程序reschedul首先判断M是否等于0,如果M不等于0,则不能进行任务的切换(即不能让高优先级的任务打断),继续运行当前的任务,直到完成; 等于0,表明任务已经不需要该资源,可以进行任务的切换.任务A对临界资源访问完毕后,将M加1,实现配对操作.
以下是任务禁止切换和任务允许切换的实现代码:
;--------------------------------------------
;sysdti-disable task interrpt(task switching)
;--------------------------------------------
sysdti_sub:
pushf
push ds
push ax
mov ax,dseg
mov ds,ax
dec ds:byte ptr[07] ; disable flag
pop ax
pop ds
popf
ret
;--------------------------------------------
;syseti:enable task interrpt( task switching)
;--------------------------------------------
syseti_sub:
pushf
push ds
push ax
mov ax,dseg
mov ds,ax
inc ds:byte ptr[07] ; enable flag
pop ax
pop ds
popf
ret
这里可以看出系统数据区公共字段中的第7字节的用途,当第7字节不等于0时,不允许发生任务的切换( 系统数据区公共字段一共有16个字节,保存目前系统运行的一些信息)

二. 队列的实现及其管理
1. 通路的概念及其实现
限制对不可重入进程的访问,通路(gateway)是一种常用的程序结构。
不可重入过程最明显的例子是DOS的不可重入,最初在设计DOS的磁盘文件系统时,编程人员用了较多的全局变量,以标识目前文件操作的位置和状态;可是,在多任务环境下,一个低优先级任务调用int 21h用于写磁盘文件的操作的时候被中断,另一个优先级高的任务被激活,它也需要调用int 21h用于磁盘文件的操作,这样,新的状态值便代替了旧的状态值,当高优先级任务运行结束之后,低优先级任务取得控制权,这时由于相关的变量已经无法恢复,造成低优先级任务无法继续正常运转.
一个很容易想到的方法是让一个任务彻底完成使用该不可重入进程的调用之后,才允许另一个任务调用这个不可重入的进程.
gateway能实现对不可重入进程的保护作用,在某一时刻它只允许一个任务使用这个进程。
gateway实现对不可重入进程受控访问的简单模拟是:到大使馆签证,你必须清早去排队领取一个顺序号码,当叫到你的号时,你才能进去,否则,你只能耐心地等待。
一个有序的gateway对各调用任务提供了类似的裁决方法。gateway为调用者分配一个号,若这个号与当前正在服务的号相符合,则允许调用者进入不可重入的过程。
在gateway初始化时,局部变量"当前标签号"和当前正在服务的号都应置为0,这里列举了有序gateway的典型算法:
(1)调用SYSDTI禁止任务中断。
(2)将"我的标签号"置成等于"当前标签号"。(即申请一个属于自己的号码)
(3)"当前标签号"加1。(以便下一次申请到下一个号码)
(4)调用SYSETI开中断。
(5)若"我的标签号"不等于"当前正在服务的号",则调用SYSSLP睡眠一会,再进入(5);
(6)调用这个不可重入的过程。
(7)"当前正在服务的号"加1。
(8)返回到调用者。

下例是用汇编语言编写的对gateway算法的实现。
;-------------------------------------------
gateway proc far
push ax
push ds
mov ax,dseg
mov ds,ax
g_10:
call SYSDTI ; prevent task switch
mov ax,current_ticket ; apply my ticket
inc current_ticket
call SYSETI ; restore
g_20 :
cmp ax,current_service ; is my number up?
jc g_30 ; if so jump…
push ax ; save my ticket
mov ax,1000 ;
call SYSSLP ; sleep 1 second
pop ax ; restore my ticket
jmp g_20 ; and try again…
g_30 :
pop ds ; restore caller's DS
pop ax ; restore caller's AX
call non_reentrant ; invoke the procedure
push ax ; save resulting AX
push ds ; save resulting DS

mov ax,dseg ; reclaim local DS
mov ds,ax ;
inc current_service ; who's next?
pop ds
pop ax
ret
这里用到SYSSLP调用,它的作用是让本任务挂起给定时间长度(释放对CPU的控制权),然后又继续往下执行;因为它涉及到定时管理和系统调度,将在后面讲述。

2. SRTX中队列的表示及存取算法
从SRTX系统数据区的230H开始的部分是系统队列区,可以看出,系统可以有MAX_TASK_NO个队列(可以自己定义),每个队列有108H字节,其中,后8个字节(100h-107h)是队列的控制信息,前100h个字节存放消息指针(offset+segment=4bytes,即双字指针,偏移量在前2字节,数据段值在后2 字节),因此每个队列最多可以同时存放100h/4=64条指针消息.
后8字节的详细安排如下表3:
表3 队列控制字段的位置和作用
偏移(字节) 作 用
100h 本队列的最大记录数(由队列创建时指定)
101h 本队列中现有记录数(现存消息数目)
102h 访问队列的当前标签id(见通路的描述)
103h 提供服务的server_id(见通路的描述)
104h-105h 队列的输入指针(输入消息的存放位置:put)
106h-107h 队列的输出指针(输出消息的存放位置:out)
应该注意的是,第100h是一个字节,可以记录最大255条记录,因为本系统最大才允许有64条记录,因此100h的高两位(bit 7-6)其实没有用上,在SRTX中,设置100h的最高位为1,表明本队列没有被使用.在初始化时,设置100h的内容为80H,就表明本队列尚未被使用(程序详见" 任务状态及其表示"的[系统数据区的初始化]).
SRTX采用循环队列的算法实现对队列的管理,循环队列实现方法如下:
input→┌───────┐
│ │ │
├───────┤ │
│ │ │
├───────┤ │
output→│ . │ 64
│ . │ │
│ . │ │
├───────┤ │
│ │ │
└───────┘
其中:
DWORD buffq[64]; //64为队列的深度,DWORD表示为2个字,第一字为offset,第二字为seg
ulong input: 为输入指针,消息存放在输入指针的位置
ulong output: 为输出指针;消息从输出指针位置取出
队列空的条件: input = output
队列满的条件: input = [output+1] mod 64;
实现队列管理的汇编代码见如下的介绍.

3. 队列的创建及其实现
在SRTX中,syqcre用于创建队列,其使用方法是:
输入:寄存器CX包含欲创建队列的最大深度(注意必须小于最大队列深度64).
输出:若创建成功,CARRY清0;此时,AX包含了队列的ID号.若CARRY置位,AX寄存器将包含下列错误码:
0451h……无效队列长度。
044eh……队列建立错误
以下syqcre是的典型使用方法:
;---------------------------------------
mov cx,40
call syqcre
jc creat_error
mov queue_id,AX

以下是syqcre的实现代码:
;-----------------------------------------
;syqcre create a queue
;-----------------------------------------
syqcre_sub:
push bx
push cx
push dx
push si
push ds ;reserve used regs

mov bx,dseg
mov ds,bx
or cx,cx ; is queue's size zero?
jnz syqcre_sub_check1 ; no jump
mov ax,0451h ; queue depth=0 error
jmp syqcre_sub_error

syqcre_sub_check1:
cmp cx,40h ; larger than 64 ?
jbe syqcre_sub1 ; no jump
mov ax,0451h ; error
jmp syqcre_sub_error

syqcre_sub1:
mov dl,cl ; depth in cl
xor ax,ax ; first queue ID=0->AX
mov bx,0230h
mov cx,max_task_no

syqcre_sub2:
test byte ptr[bx+0100h],80h ; is this queue AVAILABLE?
jnz fill_queue_message ; yes, jmp
add bx,0108h ; find next queue
inc ax ; queue id increased
loop syqcre_sub2
mov ax,044eh ; no empty queue for use
jmp syqcre_sub_error

fill_queue_message:
mov si,bx ; bx->queue buffer head
mov [si+0100h],dl ; record size for this queue
mov byte ptr[si+0101h],0 ; initial message count
mov byte ptr[si+0102h],0 ; initial current label number
mov byte ptr[si+0103h],0 ; initial server number
mov ds:[si+0104h],bx ; initial input pointer
; point to first element
mov ds:[si+0106h],bx ; initial output pointer
; point to first element,too
clc ;
jmp syqcre_sub_exit

syqcre_sub_error:
stc

syqcre_sub_exit:
pop ds
pop si
pop dx
pop cx
pop bx
ret
由上可见, queue_ID与Task_ID的定义方法差不多,表示队列创建的顺序号(创建的第一个队列,queue_ID=0,创建的第二个队列, queue_ID=1……),队列创建后,初始队列消息数为0(队列中没有消息),input和output指针都指向队列缓冲区的头部,初始当前标签号和服务号均为0.

4. 插入一条消息到指定队列
在SRTX中,syqpst用于发送消息到队列,其使用方法是:
输入:寄存器BX包含队列的标识符,DX:AX包含记录在队列里的双字信息.
输出:若成功,CARRY清0;若CARRY置位,AX寄存器将包含下列错误码:
044dh……队列标识符错误。
044fh……队列满错误
以下syqpst是的典型使用方法:
;--------------------------------------
mov ax,offset block
mov dx,seg block
mov bx,queue_ID
call SYQPST
jc error

以下是syqcre的实现代码,注意其中对临界区和队列的操作方法:
;---------------------------------
;syqpst: post a message to a queue
;---------------------------------
syqpst_sub:
push bx
push dx
push si
push di
push ds

push ax
push dx
mov ax,bx
cmp ax,max_task_no
jb syqpst_sub1
mov ax,044dh ;queue_id is too large
jmp syqpst_sub_error

syqpst_sub1:
mov bx,dseg
mov ds,bx
mov bx,0108h
mul bx
mov bx,ax
add bx,0230h ; bx->this queue addr

syqpst_sub2:
test byte ptr[bx+0100h],80h ; is queue empty?
jz syqpst_sub3 ; no jmp
mov ax,044dh ; queue is still not created
jmp syqpst_sub_error

syqpst_sub3:
call sysdti_sub ; disable task switch
mov al,[bx+0101h] ; get current message count
cmp al,[bx+0100h] ; greater than max length?
jb syqpst_sub4 ; no,jmp
call syseti_sub ; enable task switch
mov ax,044fh ; queue is full
jmp syqpst_sub_error ; exit

syqpst_sub4:
mov si,[bx+0104h] ; take input pointer
pop dx ; dx=seg
pop ax ; ax=offset
mov [si],ax ; insert a double word
mov [si+02],dx ; message in current location
mov di,bx
add di,0100h ; di->queue bottom
add si,04 ; next input pointer
cmp si,di ; input pointer arrive at bottom?
jb syqpst_sub5 ; No, jump
mov si,bx ; input pointer -> queue head
;1996.1.29 by yqh
syqpst_sub5:
mov [bx+0104h],si ; reserve input pointer
inc ds:byte ptr [bx+0101h] ; message count++
call syseti_sub ; enable task switch again
clc
jmp syqpst_sub_exit

syqpst_sub_error:
pop bx
pop bx
stc

syqpst_sub_exit:
pop ds
pop di
pop si
pop dx
pop bx
ret
注意其中对队列输入指针的处理,每加入一条消息,输入指针input加4,若输入指针加4后已经到达队列的底部,这时需要将输入指针调整到队列的头部,实现循环队列的操作.

5. 从队列中取出一条消息(1)
在SRTX中,syqacc用于从队列中取出一条消息,其使用方法是:
输入:寄存器BX包含队列的标识符.
输出:若成功,CARRY清0;DX:AX中包含双字信息,若CARRY置位,AX寄存器将包含下列错误码:
044dh……队列标识符ID错误。
0450h……队列空错误.
以下syqacc是的典型使用方法:
;--------------------------------------
mov bx, queue_ID
call SYQACC
jc error
mov ds, dx
mov si, ax

以下是syqacc的实现代码, 注意其中对临界区和队列的操作方法:
;--------------------------------------
;syqacc accept a message from a queue
;--------------------------------------
syqacc_sub:
push bx
push cx
push si
push di
push ds

mov ax,bx
cmp ax,max_task_no
jb syqacc_sub1
mov ax,044dh ;queue_ID is to large
jmp syqacc_sub_error

syqacc_sub1:
mov bx,dseg
mov ds,bx
mov bx,0108h
mul bx
mov bx,ax
add bx,0230h ; bx-> this queue buffer
test byte ptr [bx+0100h],80h ; is this queue created ?
jz syqacc_sub2 ; yes, jump
mov ax,044dh ; queue is still not created
jmp syqacc_sub_error

syqacc_sub2:
call sysdti_sub
mov al,[bx+0101h] ; get msg count
or al,al ; is there message in queue
jnz syqacc_sub3 ; yes jump
call syseti_sub ; no msg in queue
mov ax,0450h ;
jmp syqacc_sub_exit

syqacc_sub3:
mov si,[bx+0106h] ; remove a message from queue
mov ax,[si] ; get offset first
mov dx,[si+02] ; get seg
add si,04 ; next output pointer
mov di,bx
add di,0100h ; queue bottom
cmp si,di ; if next point to bottom?
jb syqacc_sub4 ; no, jump
mov si,bx ; next point to queue head

syqacc_sub4:
mov [bx+0106h],si ; set next output pointer
dec byte ptr [bx+0101h] ; dec message count
call syseti_sub
clc
jmp syqacc_sub_exit

syqacc_sub_error:
stc

syqacc_sub_exit:
pop ds
pop di
pop si
pop cx
pop bx
ret
注意其中对队列输出指针的处理,每取出一条消息,输出指针output加4,若输出指针加4后已经到达队列的底部,这时需要将输出指针调整到队列的头部,实现循环队列的操作.
注意,SYQACC实现的是从队列中直接取出一条记录,若队列中没有消息,则返回错误代码0450H,以下要介绍的SYQPND也是从队列中取出一条记录,但若队列中没有消息时,本任务需要挂在此队列上等待,直到超时为止.(由于SYQPND涉及到任务调度和定时管理的内容,因此留到后面的章节来介绍).

6. 队列状态查询
在SRTX中,syqinq用于查询指定队列的状态,其使用方法是:
输入:寄存器BX包含队列的标识符.
输出:若CARRY为0;CX包含本队列中的元素数目(可能为0), DX:AX中包含双字信息(当CX=0时,DX:AX无效);若CARRY置位,查询失败,CX=0,AX寄存器将包含下列错误码:
044dh……队列标识符ID错误。
以下是syqinq的典型使用方法:
;--------------------------------------
mov bx, queue_ID
call SYQINQ
jc error
or cx, cx
jz no_mail
mov ds, dx
mov si, ax

以下是syqinq的实现代码,注意其中对临界区的操作:
;--------------------------------
;syqinq perform a queue inquiry
;--------------------------------
syqinq_sub:
push bx
push si
push di
push ds

xor cx,cx
mov ax,bx
cmp ax,max_task_no
jb syqinq_sub1
mov ax,044dh ;queue_ID is too large
jmp syqinq_sub_error

syqinq_sub1:
mov bx,dseg
mov ds,bx
mov bx,0108h
mul bx
mov bx,ax
add bx,0230h ; bx-> queue head
test byte ptr[bx+0100h],80h ; is queue created ?
jz syqinq_sub2 ; yes, jump
mov ax,044dh ; queue is not still created
jmp syqinq_sub_error

syqinq_sub2:
call sysdti_sub
mov si,[bx+0106h] ; find out addr of message
mov ax,[si] ; dx:ax contain a copy of
mov dx,[si+02] ; first_out queue data
mov cl,[bx+0101h] ; take message count
call syseti_sub
clc
jmp syqinq_sub_exit

syqinq_sub_error:
stc

syqinq_sub_exit:
pop ds
pop di
pop si
pop bx
ret
注意SYQINQ完成的操作只是将双字指针的拷贝取出,并不象SYQACC那样将输出指针移位,在SYQINQ之后,再用SYQACC,得到的DX:AX与SYQINQ完全一样.

三. 定时管理及其实现
多任务程序的超时机制和状态的切换都与定时管理有关,本节介绍以下SRTX的定时管理。
1. 定时器及定时中断
多任务程序的运行一定需要有定时机制的硬件支持,通过对硬件的编程,可以使系统实现既定时间长度产生中断,在中断服务程序中,完成对相应需要定时的进程进行控制。
SRTX是针对PC机而设计的,DOS系统每隔55ms产生一次INT8 的中断,在INT8的中断服务程序中,调用了INT 1C(INT 1C的函数体是空),因此可以编一个自己的函数替换INT1C,就可以让系统定时调用我们自己的函数,在此函数中,可以让它实现定时管理功能,在SRTX中,SYSRTI就是这样一个函数.
2. 定时器的实现
在SRTX中,系统定时调用sysrti,实现定时管理功能.以下是SYSRTI的实现代码:
;-------------------------------
;colck interrupt handler
;-------------------------------
sysrti proc far
push ds
push ax
mov ax,dseg
mov ds,ax
add ds:word ptr[04],55 ; inc system clock

test ds:byte ptr[07],0ffh ; task can switch ?
jnz sysrti_sub2 ; not,exit
test ds:byte ptr[0bh],0ffh ; task has been suspended?
jz sysrti_sub2 ; yes,exit
jmp sysrti_sub3 ;

sysrti_sub2:
pop ax ; direct exit
pop ds
sti
iret

sysrti_sub3:
push bx
mov bx,ds:[0eh] ; take out TCB for running task
or byte ptr[bx+01],10h ; set interrupted flag
pop bx
pop ax ; reserve all state

push es ; for recover running
push di ; ( see module of
push si ; run_interrupt_task )
push bp ;
push bx
push dx
push cx
push ax ; reserve environment
mov bx,ds:[0eh] ; take out TCB pointer
mov [bx+0ah],sp ; save current task status
mov [bx+0ch],ss
mov ds:byte ptr[0bh],0 ; set suspended flag
jmp reschedul ; enter next schedul
sysrti endp
在本程序中,用到几个系统变量:
[04-05]:字变量,用于表示从本程序运行以来经过的ms数,因为每隔55ms调用一次,因此[04-05]每次加55.
[07]:当前运行的任务是否可以被切换.因为硬件中断是随时可以发生的,在SYSRTI中需要根据任务的状态随时将最高优先级的任务取出运行,若当前运行的任务不允许被切换,设置[07]为0xff.(见上面相应章节的说明)
[0bh]:当前任务被挂起的标志,若任务被挂起,设置[0bh]=0,不允许进行任务的切换。(这标志是否有必要?)
[0eh-0fh]:当前运行任务的TCB指针.
可以看出,SYSRTI的最后,取出当前任务的TCB,将断点及现场都保存在任务的堆栈中,把堆栈指针保存在本任务TCB的[0a-0d]字节中(这里可以看出TCB中的[10-11,12-13]字节的作用),然后进入调度程序(reschedul).

3. 任务睡眠(SYSSLP)及其实现
在SRTX中,sysslp用于让任务挂起定长时间,其使用方法是:
输入:寄存器AX包含挂起时间长度(ms).
输出:无.
以下sysslp的典型使用方法:
;---------------------------------
MOV DX,PARALLEL_PORT
P1:
IN AL, DX
TEST AL, READY_BIT
JNZ DO_OUTPUT
MOV AX, 55
CALL SYSSLP
JMP P1
以下是SYSSLP的实现代码:
;----------------------------------
;sysslp sleep for specfied duration
;----------------------------------
sysslp_sub:
cli ; forbit clock interrupt
call current_time_agument ; time calculate
call suspend_itself ; set task suspended
ret ; enter next schedul
;-------------------------------
current_time_agument:
push ax
push bx
push cx
push ds
mov bx,dseg
mov ds,bx
mov bx,ds:[0eh] ; take out TCB
mov cx,ds:[04] ; get current clock
add ax,cx ; current time add agument
mov [bx+0eh],ax ; task will be awakened
mov al,[bx+01] ; at 'ax' moment
and al,0fdh ; set not awakened status
or al,04 ; set sleep status
mov [bx+01],al ; save
pop ds
pop cx
pop bx
pop ax
ret
;---------------------------------
;suspend current task
;---------------------------------
suspend_itself proc far
pushf ; called only by sysslp_sub
push ds
push es ; reserve current status
push di ; for awakening to run
push si
push bp
push bx
push dx
push cx
push ax ; reserve enviroment
mov ax,dseg
mov ds,ax
mov ds:byte ptr[0bh],0 ; set suspended flag
mov bx,ds:[0eh] ; take TCB addr of current task
mov [bx+0ah],sp ; reserve stack address
mov [bx+0ch],ss ; (all status is in stack)
jmp reschedul ; enter next schedul
suspend_itself endp
这里终于可以看出在TCB中的14-15字节(slice)的作用了,SYSSLP的AX值与当前时钟值相加,放入slice中.从今后的相关代码中可以看出,slice时刻一到,任务就被唤醒.
suspend_itself函数将当前任务的现场保存在其TCB中(10-11,12-13字节:SP 和SS),然后进入调度程序.

4. 从队列中取消息SYQPND的实现.
以前我们讲了从队列中取消息SYQACC的实现,以下我们讲SYQOND的实现.
输入:寄存器BX包含队列的标识符,AX包含了若没有消息时最多等待的ms数.
输出:若CARRY为0; DX:AX中包含从该队列中取得的双字信息;若CARRY置位,AX寄存器将包含下列错误码:
044dh……队列标识符ID错误。
0450h……超时错误。
以下是syqpnd的典型使用方法:
;----------------------------------
mov bx, queue_ID
check:
mov ax, 1000
call SYQPND
jnc OKEY
jmp check

以下是syqpnd的实现代码:
;---------------------------------------
;syqpnd: receive a message from queue
;---------------------------------------
syqpnd_sub:
push bx
push cx
push si
push di
push ds
push ax
mov ax,bx
cmp ax,max_task_no
jb syqpnd_sub01 ;queue_ID is too large
mov ax,044dh
jmp syqpnd_sub_error

syqpnd_sub01:
mov bx,dseg
mov ds,bx
mov bx,0108h

syqpnd_sub1:
mul bx
mov bx,ax
add bx,0230h ; bx->this queue head
test ds:byte ptr[bx+0100h],80h ; is queue created?
jz syqpnd_sub2 ; yes, jump
mov ax,044dh ; queue is not created
jmp syqpnd_sub_error

syqpnd_sub2:
call sysdti_sub
mov ch,[bx+0102h] ; get current pass id
inc ds: byte ptr[bx+0102h] ; next pass id
call syseti_sub
mov cl,01

syqpnd_sub22: ; my pass id is equal to
cmp ch,[bx+0103h] ; current server id ?
je syqpnd_sub3 ; yes jump
or cl,cl ; time out ?
jz syqpnd_sub6 ; yes, exit
jmp syqpnd_sub4 ; no, delay ax ms

syqpnd_sub3:
test byte ptr [bx+0101h],0ffh; is there message?
jnz syqpnd_sub7 ; yes,jump
or cl,cl ; time out ?
jz syqpnd_sub5 ; yes,exit

syqpnd_sub4:
pop ax
push ax
call sysslp_sub ; delay AX ms
dec cl ; set time out flag
jmp syqpnd_sub22 ; continue fetch message

syqpnd_sub5:
inc byte ptr [bx+0103h] ; set next server id

syqpnd_sub6:
mov ax,0450h ; time out error
jmp syqpnd_sub_error

syqpnd_sub7:
pop ax
call sysdti_sub
mov si,[bx+0106h] ; take out a message
mov ax,[si]
mov dx,[si+02]
add si,04 ; form next output pointer
mov di,bx
add di,0100h ; queue bottom
cmp si,di ; is point to queue bottom
jb syqpnd_sub8 ; no, jump
mov si,bx ; yes,next output pointer
; is queue head
syqpnd_sub8:
mov [bx+0106h],si ; save output pointer
dec byte ptr [bx+0101h] ; dec msg number
inc byte ptr [bx+0103h] ; next server id
call syseti_sub ; this is gateway alg
clc
jmp syqpnd_sub_exit

syqpnd_sub_error:
pop bx
stc

syqpnd_sub_exit:
pop ds
pop di
pop si
pop cx
pop bx
ret
在syqpnd中,若队列中没有消息,调用了sysslp,这期间,本任务放弃了对CPU的控制权,其它任务被激活运行,它们可能往本队列中syqpst消息,这样,当本任务超时恢复运行时,就可以取出该消息了.

四. 任务调度及其实现
任务调度是实时多任务程序的核心,它的主要功能是从当前众多可运行的任务中提取最高优先级的可运行任务,找到该任务的TCB地址,从该TCB中取出上次被中断运行的现场,从而将该任务投入运行.

首先介绍几个函数:
take_ptr_pair:功能是提取指定位置的双字指针.
输入:AX=欲提取的双字指针的偏移位置.
输出:AX:BX=双字指针
如:
mov ax,0ah ;should be 06h
call take_ptr_pair ; take out current ss:sp
mov ss,ax ; of this awakened task
mov sp,bx
以上语句的功能是提取一个任务上次被中断是的SS:SP,这样可以恢复该任务上次运行的断点. 因为:TCB的0ah位置存放的是上次被中断时的SS:SP.若AX=04H,表示提取任务初始的CS:IP.
take_ptr_pair的实现代码如下:
;------------------------------
take_ptr_pair:
pushf
mov bx,ds:[0eh]
add bx,ax ; get TCB
mov ax,[bx+02] ; take out a pointer pair
mov bx,[bx] ;
popf
ret
task_not_run:功能是挂起本任务,设置本任务为不可运行状态.
task_not_run的实现代码如下:
;------------------------------------------
;stop a task,this task will not run forever
;------------------------------------------
task_not_run:
mov ax,dseg
mov ds,ax
mov ds:byte ptr[0bh],0 ; set suspend flag
mov bx,ds:[0eh] ; take out TCB pointer
mov byte ptr[bx+01],0 ; stop task running
jmp reschedul ; task is excluded from reschedul

set_run_flag:功能是设置任务未运行结束,未被挂起状态.
set_run_flag的实现代码如下:
;-------------------------------
set_run_flag:
push ax
mov al,0ffh ; set current task
mov ds:[0bh],al ; not be suspended
mov ds:[0ch],al ; current task is
pop ax ; not ended
ret

run_ready_task:功能是将一个处于READY状态的任务投入运行.
一般来说,任务在创建之后,才处于READY状态,这时候,任务还没有运行过,因此任务必须从头运行.在任务运行中间,它可能被中断(即定时中断SYSRTI将此任务置为被中断的状态),也可能由于等待资源而处于挂起(睡眠)状态,但是任务只要投入运行,它就不会处于READY状态.
run_ready_task的实现代码如下:
;----------------------------------------------
;pass cpu control to task that is ready status
;----------------------------------------------
run_ready_task proc far
1 mov ax,06
2 call take_ptr_pair ; take out initial ss:sp
3 mov ss,ax ; form task stack
4 mov sp,bx
5 call set_run_flag ; set task is ready
6 push cs ; cs of task_not_run
7 mov ax,offset task_not_run ; ip of task_not_run
8 push ax ; (after running this
9 mov ax,02 ; task completely,
10 call take_ptr_pair ; run task_not_run)
11 push ax ; take out initial cs:ip
12 push bx
13 ret ; start to run this task
run_ready_task endp
此程序有点技巧,1-4行,提取任务创建时的SS:SP作为目前的SS:SP,6-8行,将函数task_not_run的CS:OFFSET压入堆栈,9-12行,将任务创建时的CS:OFFSET提取出来并压入堆栈,13行的ret指令实现函数的返回,因为ret指令要作出栈操作,因此将刚压入堆栈的CS:OFFSET弹出,作为ret指令之后的CS:IP,因此任务投入运行.本任务在彻底运行完成以后,与以上相同的机制弹出task_not_run的CS:IP,于是task_not_run开始运行.
该编程技巧经常用于程序的反跟踪,需要仔细领会.

run_awakened_task: 功能是将一个处于睡眠超时状态的任务投入运行.
程序首先取出上次挂起时的SS:SP,然后从该堆栈中取出上次压入的各寄存器的值,恢复现场并返回.注意在reschedul中采用的是jmp run_awakened_task指令而不是采用CALL run_awakened_task指令是为了与suspend_itself(在SYSSLP_SUB中调用)中jmp reschedul相呼应, run_awakened_task的末尾用ret指令就返回到调用SYSSLP的下一条指令.
run_awakened_task的实现代码如下:
;-------------------------------------------------
;pass cpu control to task that is awakened status
;-------------------------------------------------
run_awakened_task proc far
mov ax,0ah
call take_ptr_pair ; take out current ss:sp
mov ss,ax ; of this awakened task
mov sp,bx
call set_run_flag ; set task is not suspended
pop ax ; restore all status
pop cx ; and flags,
pop dx ; continue running
pop bx ; task from awakend
pop bp ; address,usually,
pop si ; this address is after
pop di ; sysslp call.
pop es ; for example:
pop ds ; mov ax,1000
popf ; call sysslp
sti ; -> ;here is awakened addr
ret ; mov si,bx
run_awakened_task endp ; (pushed by suspend_itself)

run_interrupt_task: 功能是将一个处于被中断状态的任务投入运行.
程序首先取出上次被中断时的SS:SP,然后从该堆栈中取出上次压入的各寄存器的值,恢复现场并返回.注意在reschedul中采用的是jmp run_interrupt_task指令而不是采用CALL run_interrupt_task指令是为了与sysrti(在SYSSLP_SUB中调用)中jmp reschedul相呼应, run_interrupt_task的末尾用iret指令表明中断服务程序的返回.
run_interrupt_task的实现代码如下:
;---------------------------------------------------
;pass cpu control to task that is interrupted
;---------------------------------------------------
run_interrupt_task proc far
mov ax,0ah
call take_ptr_pair ; take out current ss:sp of
mov ss,ax ; this interrupted task
mov sp,bx ; set new ss:sp of this task
call set_run_flag ; set task is not suspended
pop ax ; restore break point
pop cx ; of interrupted task
pop dx
pop bx
pop bp ; interrupt break point is
pop si ; pushed by sysrti -real
pop di ; time manage program
pop es
pop ds ; restore to run from
iret ; interrupted point
run_interrupt_task endp

读懂以上几个函数以后, reschedul程序就不难理解了.
以下是任务调度的实现代码,从reschedul开始:
;-------------------------------------------------------------
schedul:
mov bx,ds:[0eh] ; form currnt tcb address
add bx,10h ; point to next TCB
cmp bx,0220h ; has finished ?
jb reschedul1 ; no, check status
test ds:byte ptr[0ch],0ffh ; task execute end during
mov ds:byte ptr[0ch],0 ; this tick?
jnz reschedul ; no, jump
sti ; important,by yqh
hlt ; wait for next interrupt

reschedul: ; entry for reschedul procedure
cli ; important,by yqh
mov bx,20h ; bx->first TCB

reschedul1:
mov ds:[0eh],bx ; reserve tcb pointer
mov al,[bx+01] ; get status
test al,20h ; tcb is empty?
jnz schedul ; yes, jump
and al,81h ; is inhibited to execute?
xor al,01 ; is not runnable?
jnz schedul ; yes, jump
mov al,[bx+01] ; get status
and al,10h ; task is interrupted ?
jnz int_process ; yes,jump
mov al,[bx+01] ; get status
and al,04 ; task is seleeping?
jnz sleep_process ; yes, jump
mov byte ptr [bx+01],01 ; set runnable status
sti ; important, by yqh
jmp run_ready_task

int_process:
and byte ptr [bx+01],0efh ; cancel interrupted status
sti ; important,by yqh
jmp run_interrupt_task ; entery task interrupted

sleep_process:
mov ax,ds:[04] ; take out current clock
cmp ax,[bx+0eh] ; cmp with awakening moment
js schedul ; not time out, jump
mov byte ptr [bx+01],03 ; set awakened status
sti ; important,by yqh
jmp run_awakened_task ; entry awakened task

从如上代码可以看出, 任务调度总是从reschedul开始, 而reschedul又总是从第一个TCB开始考察各任务的状态,选出第一个可以运行的任务投入运行,由此可见,最先建立的任务的优先级最高,TCB处于较前面的位置,由此实现基于优先级的调度.

任务调度的时机:
任务什么时候进行调度?从以上程序可以看出,任务调度发生在两个地方,一个是在函数suspend_itself中,另一个是sysrti的定时器管理程序.
suspend_itself由sysslp来调用,sysslp置任务于挂起状态.通常,任务在访问资源时,若条件不满足,需要等待资源的到来,这时通过调用sysslp,置本任务于睡眠状态,让调度程序激活其它任务运行,当调用sysslp的任务超时之后,本任务恢复运行;经过这段时间,本任务需要的资源可能已经到来,因此,可以取出资源,任务继续执行,最典型的情况请见syqpnd程序.
sysrti无疑是最重要的调度程序源,定时器由硬件管理,每个tick(通常10ms,DOS系统是55ms)产生一个定时中断,在中断服务程序中把最高优先级的可运行的任务提取出来,投入运行,正是因为有了这个定时器中断,才使得实时调度成为可能,同时也使得系统定时管理变得简单和容易.

五. 小结
本文提供的SRTX的实现只是用以说明实时多任务系统的运行原理和内部机制,实际上,一个商用的实时多任务系统比SRTX复杂得多,功能也强大得多.
本文附带的SRTX的所有原码及注释(srtx.arj),读者可以用masm及link处理,得到一个最简单的SRTX的演示程序,另附原版代码及演示程序(pax.arj,注意到所有ASM都是1986年的文件,确实古老).
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: