第4章第2节 定时器触发的实时抢占…
来源:互联网 发布:中国程序员联合开发网 编辑:程序博客网 时间:2024/06/04 07:16
目前更新到5.3节,请在http://dl.dbank.com/c02ackpwp6下载5.3节的全部文档
本节源代码请在http://dl.dbank.com/c0o17w4jcg下载
第2节 定时器触发的实时抢占调度
在第3章,我们依靠用户代码主动调用任务切换函数WLX_TaskSwitch实现了任务切换功能,这种任务调度方式的调度时机是固定死的,只有代码运行到WLX_TaskSwitch函数时才会发生任务切换,因此实时性不强。
实时操作系统采用2种调度方式确保它的实时性,一种是依靠硬件定时器触发的定时调度,另一种是依靠实时事件触发的随机调度,本节将讲述第一种调度方式。
实时操作系统会使用一个硬件定时器,利用硬件定时器定时产生中断,这个中断叫做tick中断,tick中断的中断服务函数就是操作系统的任务调度函数,因此当设定好tick定时器后,实时操作系统就可以不依赖代码的运行情况,周期性的产生任务调度。操作系统的调度周期与tick周期有关,tick周期越小操作系统的实时性越好,tick周期越大操作系统的实时性越差,但tick周期也不是越小越好,因为产生tick中断时需要进行中断上下文切换和任务上下文切换,这个过程也是浪费时间的,如果tick周期过小,会使这一过程所占的比重过大,反而会降低芯片的效率,一般这个tick周期可以设置为10ms,具体数值视系统整体情况而定。
从上面的介绍来看,实时操作系统也并非完全是实时的,那么它对比分时操作系统有什么优点呢?我们来看看PC机使用的Windows操作系统,它类似一种分时操作系统,Windows在每个tick会调度一个任务,各个任务排队,在2个tick之间完成一个任务调度后便将这个任务放到队列的后面,下个tick将从队列前面的任务开始调度,就这样,每个任务都按照一定的顺序周而复始的运行。当在分时操作系统上同时运行很多任务时,这时候就可以明显的感觉到每个任务的执行速度变慢了,我们在Windows上同时运行很多大程序时就会体会到这种感觉。
而实时操作系统是不会出现这种情况的,实时操作系统中的任务是有优先级这个属性的,优先级越高的任务就越会被优先执行,优先级低的任务会等到优先级高的任务执行完毕再执行。依照优先级的调度方式,即使操作系统上同时运行了很多任务,但它会确保优先级高的任务先运行,当下个tick到来时,它还是运行优先级最高的任务,这样就确保了任务的实时性。
从上面的介绍可以看出,实时操作系统尽管保证了实时性,但它也是有代价的,它是以牺牲低优先级任务的实时性为代价来保证高优先级任务实时性的,不过,这也是合理的,物竞天择,重要的事情总是要优先处理的。当我们在实时操作系统上设计任务时,就需要根据任务功能的轻重缓急为任务分配适合的优先级,以保证整个系统的功能得以实现。
实时操作系统除了会优先执行高优先级的任务,还会发生高优先级任务抢占低优先级任务的情况。实时操作系统的任务可以分为running、ready、delay、pend、suspend等几种状态,任务在运行时可能会在这几种状态之间切换,在这个切换过程中就可能会发生任务抢占。
running:任务获取到CPU资源,任务正在运行。任何时刻,系统中只能有一个任务处于running状态。
ready:任务准备就绪,已经获取除CPU资源之外的所有资源。处于ready状态的任务如果具有最高优先级,那么下个任务调度就会执行它。
delay:任务主动延迟,放弃CPU资源。处于running状态的任务若暂时不需要执行,则可以调用相关函数进入delay状态,主动将CPU控制权交给其它任务。
pend:任务由于获取不到非CPU资源被阻塞。处于running状态的任务若获取不到非CPU资源,可能会被阻塞进入pend状态,被动将CPU控制权交给其它任务。
suspend:任务被挂起,不参与任务调度。这种状态一般只存在于调试过程中,在本手册中没有实现该状态,若读者感兴趣,可以自行编写代码实现。
这几种状态是可以相互转换的,如图29所示:
图
①ready—>running:任务获取到所有资源,包括CPU资源,正在运行。
②running—>ready:任务被其它任务抢占,丧失CPU资源。
③running—>delay:任务主动调用MDS_TaskDelay函数,任务被延迟,丧失CPU资源。
④running—>pend:任务使用MDS_SemTake等函数获取不到资源,任务被阻塞,丧失CPU资源。
⑤delay—>ready:任务延迟时间耗尽或者被MDS_TaskWake函数唤醒,等待CPU资源。
⑥pend—>ready:任务阻塞时间耗尽或者任务获取到了被阻塞的资源,等待CPU资源。
从图29中可以看出,任务如果要转换成running态去运行,必须得先转换成ready态,操作系统在任务调度时只会从处于ready态的任务中查找最高优先级的任务去执行,至于那些即便是拥有了最高优先级的任务,只要它不是处于ready态,任务调度时也不会考虑,现在我们就来设计这个ready态。
图
任务1和任务2的优先级都是7,与ready表的“优先级7”相关联,任务3的优先级是5,与ready表的“优先级5”相关联,任务4的优先级是2,与ready表的“优先级2”相关联。与ready表关联的任务是可以改变的,可以增加也可以减少,我们可以采用链表来关联ready表与各个任务,ready表中的每个“优先级”就是这个优先级链表的根节点,与之相关的任务就是这个链表上的一个节点。比如“优先级7”就是ready表中优先级为7的链表的根节点,任务1和任务2分别是优先级为7的链表中的一个节点。这样当我们需要查找ready表中优先级为7的任务时,只需要先找到优先级为7的链表根节点,再从根节点顺着链表就可以找到优先级为7的任务有任务1和任务2。
我们来看看Mindows里使用的链表结构体:
typedef
{
}M_CHAIN;
pstrHead是链表的头指针,pstrTail是链表的尾指针。当链表为空,即链表只有根节点时,根节点的头尾指针都指向空指针。当链表不为空时,根节点的头指针指向链表中的第一个子节点,根节点的尾指针指向链表中的最后一个子节点,每个子节点的头指针指向它前面的节点(包括根节点),每个子节点的尾指针指向它后面的节点(包括根节点)。这样所有的子节点与根节点就组成了一个环状的双向链表结构,通过根节点可以快速找到链表的第一个和最后一个子节点,通过任何一个节点都可以找到它前后的节点。我们来看看链表为空,有1个节点,2个节点和多个节点的情况:
图
图
图
图
Mindows提供了几个链表操作函数,包括:初始化链表的函数MDS_ChainInit,向链表尾部插入一个节点的函数MDS_ChainNodeAdd,从链表头部删除一个节点的函数MDS_ChainNodeDelete,向链表指定的节点前插入一个节点的函数MDS_ChainCurNodeInsert,删除链表中指定的节点的函数MDS_ChainCurNodeDelete,查询链表是否为空的函数MDS_ChainEmpInq,查询链表中指定节点的下一个节点是否为空的函数MDS_ChainNextNodeEmpInq。这些链表操作函数将会在Mindows中用到,链表操作函数的细节这里就不具体介绍了,请读者自己参考代码。
拥有了链表结构,ready表就可以用链表数组来实现,如下定义了具有8个元素的链表数组astrChain[8],每个数组元素分别对应ready表里每个“优先级”链表的根节点,astrChain[0]~astrChain[7]分别对应优先级0~优先级7链表的根节点。
M_CHAIN
任务若要挂到ready表上,那么任务里也需要有一个M_CHAIN的链表节点结构,任务的这个链表节点结构就被放到了TCB里面,后面我们在介绍TCB的时候再详细说。
图
8个标志位对应8个优先级的根节点,我们正好可以定义1个字节的变量ucPrioFlag,使用这个变量里面的每个bit作为一个标志来表示各个优先级的链表是否为空,bit0~bit7分别对应astrChain[0]~astrChain[7],bit为0代表链表为空,bit为1代表链表不为空,这个优先级的链表上有任务处于ready状态。
到这里,我们已经完成了具有8个优先级ready表的设计,我们再来看看对ready表的操作过程。
u
将ready表中的每个根节点的头尾指针都指向NULL,使8个优先级的链表都为空,同时,将标志置为0,这时候ready表上没有挂接任何任务。
u
当有任务变为ready态时,我们将任务TCB中的链表节点添加到ready中相同优先级的链表上,并将对应的标志bit置为1。
u
当有任务需要从ready表拆离时,我们就将这个任务的节点从ready表中相同优先级的链表中拆除,同时将对应的标志bit置为0。
u
这个过程稍微复杂一些,听我慢慢道来。
我们来看看ready表标志为下面几种情况的例子:
bit7
Bit6
Bit5
Bit4
Bit3
Bit2
Bit1
bit0
最高优先级
1
0
0
0
0
0
0
0
7
1
1
0
0
0
0
0
0
6
1
1
1
0
0
0
0
0
5
1
0
1
0
0
0
0
0
5
0
1
1
0
1
0
1
1
0
1
0
1
0
0
0
0
1
0
表
通过这几组数据我们可以看出来,ready表中的最高优先级是由标志中为1的最低bit决定的,也就是说,从bit0向bit7找,当发现第一个为1的bit时,这个bit对应的就是任务的最高优先级,与后面的高位bit无关。8bits共有256种组合,我们可以将这256种组合一一列出:
当标志为0b000000001的时候,也就是1的时候,优先级为0
当标志为0b000000010的时候,也就是2的时候,优先级为1
当标志为0b000000011的时候,也就是3的时候,优先级为0
当标志为0b000000100的时候,也就是4的时候,优先级为2
当标志为0b000000101的时候,也就是5的时候,优先级为0
……
当标志为0b11111110的时候,也就是254的时候,优先级为1
当标志为0b11111111的时候,也就是255的时候,优先级为0
如果我们将“标志的值”作为数组下标,将“优先级”作为数组元素值的话,我们就可以构造出下面的这个数组:
const
{
};
当我们需要查找ready表中的最高优先级时,就可以把标志ucPrioFlag当做数组下标带入到数组caucTaskPrioUnmapTab中,数组元素的值caucTaskPrioUnmapTab
当标志值为0时,这种情况是不存在的,ready表中至少会有一个任务,至少处于running态的任务就位于ready表中,所以caucTaskPrioUnmapTab[0]的值是无效的,只是用来占个坑填满组数。
8个优先级的ready表相关内容已经全部讲述完毕。
8个优先级对于小型嵌入式设备来说应该是够用了,但对稍大一些的嵌入式设备来说就显得太少了。我们可以通过扩充astrChain数组的数组元素数量来扩充Mindows所支持的优先级数量,同时,也需要相应的扩充ucPrioFlag标志的长度好与之相对应。通过这种方法,我们可以将Mindows支持的优先级数量扩展到支持8、16、32、64、128、256这6种不同的级别上,用下面的宏分别区分这些级别:
#define
#define
#define
#define
#define
#define
用户在确定使用哪个数量的优先级时,需要在mds_userdef.h文件里将PRIORITYNUM宏定义为上面这6种优先级数量中的一个,例如,用户使用下面的宏定义,选择Mindows支持32个优先级数量:
#define
操作系统通过PRIORITYNUM宏就可以知道用户所希望使用的优先级数量了,那么,现在我们把ready表的结构重新整理一下:
typedef
{
}M_TASKSCHEDTAB;
astrChain仍是链表数组,每个数组元素对应一个优先级的链表根节点,优先级的数量由PRIORITYNUM宏确定。strFlag是每个优先级链表对应的标志,M_PRIOFLAG结构体如下:
typedef
{
#if
#elif
#else
#endif
}M_PRIOFLAG;
M_PRIOFLAG结构体根据不同优先级数量将标志位分为3种情况:只定义8个优先级数量的时候标志位只使用1个字节就可以了,定义16、32、64个优先级数量的时候标志位分为了2级,定义128、256个优先级数量的时候标志位分为了3级,下面我们来看看为什么要这么做。
在优先级只有8个的时候,标志有28共256种组合,我们可以使用一个256Bytes的数组caucTaskPrioUnmapTab来构建优先级反向查找表。当优先级为16个的时候,标志有216共65536种组合,若构建64KBytes的反向查找表未免显得太浪费了,而且对于某些小型嵌入式系统来说也无法实现。当优先级为256个时,2256是一个非常巨大的数,任何硬件都无法支持这么大的数组。
为了解决这个问题,我们可以将ready表的标志分级,以256个优先级为例,如图36所示:
图
优先级反向查找表只支持8个优先级,256可以分解为4×8×8,第三级有4个bits,每个bit分别对应第二级的一个Byte,第二级每个Byte中的每个bit分别对应第一级的一个Byte,这样到第一级就有4×8×8共256个bits,每个bit对应一个优先级链表,用来指示该优先级链表是否为空。第一级标志对应着每个优先级链表,第二级和第三级是为了查找第一级而设立的,我们来看看操作ready表时这个3级标志是怎么处理的:
u
将3级标志全部置为0。
u
将每一级标志中对应的bit置为1。比如说添加的任务优先级是143,使用整型数计算舍弃小数,143÷8÷8=2,对应第三级的bit2,将第三级的bit2置为1。143÷8=17,对应第二级的bit17,17÷8=2,对应第二级的Byte2,17-8×2=1,对应Byte2的bit1,将第二级Byte2中的bit1置为1。143÷8=17,对应第一级的Byte17,143-8×17=7,对应Byte17的bit7,将第一级Byte17中的bit7置为1。
u
将第一级标志中对应的bit置为0,若该bit所属的第二级和第三级中没有其它任务的标志,则将第二级、第三级对应的bit也置为0,否则维持原状不变。比如ready表中有143和144这两个优先级的任务,如果要拆除143时,将第一级143的标志置为0,而第二级Byte2中的bit1和第三级的bit2是不能置为0的,因为144优先级也位于第二级Byte2中的bit1和第三级的bit2。
u
查询3级标志的优先级时仍使用caucTaskPrioUnmapTab这个优先级反向查询表,只不过是分了4步。第1步,从第三级4bits里查询出第二级中拥有最高优先级的Byte,第2步,从第二级的这个Byte里查询出第一级中拥有最高优先级的Byte,第3步,从第一级的这个Byte里查询出拥有最高优先级的bit,第4步,通过前3步找出的最高优先级所在第一级、第二级和第三级中所在的位置,算出最高优先级。
我们仍以143优先级为例,通过上面的讲述,我们知道143优先级在第三级的bit2,在第二级Byte2中的bit1,在第一级Byte17中的bit7。第1步,第三级标志为0b0100,查询优先级反向表caucTaskPrioUnmapTab[4]的值为2,说明最高优先级在第二级的Byte2中。第2步,Byte2的值为0b00000010,查询优先级反向表caucTaskPrioUnmapTab[2]的值为1,说明最高优先级在第二级Byte2中所拥有的8个第一级Bytes中的Byte1中,8×2+1=17,也就是第一级中的Byte17。第3步,Byte17的值为0b10000000,查询优先级反向表caucTaskPrioUnmapTab[128]的值为7,说明最高优先级在第一级Byte17中的bit7。第4步,(8×2+1)×8+7=143,这样我们就查找到了ready表中的最高优先级。
查找到最高优先级后,astrChain[最高优先级]就是最高优先级链表的根节点,通过此链表就可以找到最高优先级任务的节点,而最高优先级任务的节点与最高优先级任务的TCB相关联,因此就可以找到最高优先级任务的信息了。关于本节的任务TCB结构,我们在后面再讲述。
本节主要是增加了ready调度表,原理讲解到这里,下面来看操作ready表的代码。
MDS_TaskAddToSchedTab函数的作用是将任务添加到ready表中:
00194
00195
00196
00197
00198
00199
00200
00201
00202
194行,入口参数pstrChain是ready表中优先级链表的根节点指针,也就是astrChain数组里面的一个数组元素的指针。入口参数pstrNode是需要挂接到链表上的节点。入口参数pstrPrioFlag是优先级标志的指针。入口参数ucTaskPrio是挂入的任务的优先级。
这个函数的作用就是将优先级为ucTaskPrio的任务的节点pstrNode,挂入到pstrChain链表上,同时将pstrPrioFlag标志中对应的位置置为1。
198行,将子节点添加到链表中。
201行,设置对应位置的标志。
MDS_TaskPrioFlagSet函数的原理前面已经介绍过,代码如下,就不一一讲解了。
00230
00231
00232
00233
00234
00235
00236
00237
00238
00239
00240
00241
00242
00243
00244
00245
00246
00247
00248
00249
00250
00251
00252
00253
00254
00255
00256
00257
00258
00259
00260
00261
00262
00263
00264
00265
00266
00267
00268
00269
00270
00271
00272
00273
00274
00275
00276
MDS_TaskHighestPrioGet函数是查询ready表中最高优先级的函数,入口参数pstrPrioFlag是优先级标志的指针,原理前面已经介绍过,代码如下,不再做介绍了。
00283
00284
00285
00286
00287
00288
00289
00290
00291
00292
00293
00294
00295
00296
00297
00298
00299
00300
00301
00302
00303
00304
00305
00306
00307
00308
00309
00310
00311
00312
00313
00314
00315
00316
00317
00318
00319
00320
00321
操作系统支持的优先级数量越多,占用的系统资源也就越多,不但对ready表操作的时间会增加,而且还会占用内存空间,如表7所示:
优先级数量
根节点字节数
一级标志字节数
二级标志字节数
三级标志字节数
总字节数
8
64
1
0
0
65
16
128
2
1
0
131
32
256
4
1
0
261
64
512
8
1
0
521
128
1024
16
2
1
1043
256
2048
32
4
1
2085
表
从表7可以看到,8个优先级的ready表只需要使用65个字节就可以了,这对资源少的嵌入式系统来说是完全可以接受的,而256个优先级的ready则需要使用2085个字节,单单一个ready表就已经超过了2KBytes的内存,这对只有几KBytes或者更少内存的嵌入式设备来说已经不适合了。但2KBytes的内存对于拥有几百MBytes甚至几GBytes的嵌入式设备来说却又不算什么,这样的大系统也会很复杂,8个优先级远远不能满足需求,也需要操作系统能支持256个优先级。我们在设计软件系统时,需要根据硬件资源的限制,合理选择Mindows所支持的优先级数量。
ready表就介绍到这里,接下来我们来看一下TCB结构。
typedef
{
}M_TCB;
TCB里面有寄存器组、一个队列、还有任务的优先级。
在Wanlix中,寄存器组是在任务切换的时候被保存在栈指针当前的位置的,在Mindows中我把它放到了TCB里面,这样做的好处是寄存器组被固定在TCB中固定的位置,使用的时候可以很方便的通过TCB找到,而且,在Mindows中也不可能像Wanlix那样把寄存器组放在任务切换时的栈指针处,因为Mindows的任务切换是由中断引起的,中断产生的时机是不可预知的,如果产生中断时SP栈指针没有指向当时栈的位置,那么将寄存器组存储在SP栈指针指向的内存则会破坏内存中的数据,可能会导致系统崩溃。
在中断发生时,芯片内核会从USR模式切换到IRQ模式(这里使用的tick中断是IRQ模式的),中断返回的下条指令地址被保存到了IRQ模式下的LR寄存器中,这个值相当于是PC值,而USR模式下的LR寄存器值没有变,它保存的是USR模式下当前函数返回上级父函数的PC值。因此,Mindows在任务上下文切换时要比Wanlix多备份、恢复一个PC寄存器,在Mindows中,寄存器组就由CPSR、R0~R15组成了:
typedef
{
}STACKREG;
我们再来看看TCB中的M_TCBQUE结构体,
typedef
{
}M_TCBQUE;
M_TCBQUE队列结构相比M_CHAIN链表结构来说,只是多了一个M_TCB型的指针pstrTcb。我们从ready表中获取最高优先级任务时,先获取到最高优先级,再通过最高优先级获取到链表根节点,再通过根节点获取到挂在上面的任务的子节点,然后应该就是再通过子节点获取到任务的TCB。pstrTcb指针在任务初始化时就被初始化为任务的TCB指针,因此在获取到子节点strQueHead的地址后,我们就可以通过它下面的pstrTcb获取到TCB的地址,进而可以获取整个TCB的信息。
- 第4章第2节 定时器触发的实时抢占…
- 第4章第2节 定时器触发的实时抢占…
- 第4章第3节 实时事件触发的实时抢…
- 第4章第3节 实时事件触发的实时抢…
- 第4章第3节 实时事件触发的实时抢…
- 第1章第2节 操作系统的分类
- Dive into python 第4章 自省的威…
- 第2章第3节 ARM7芯片的函数调用标…
- 第2章第4节 Wanlix的文件组织结构
- 第2章第2节 ARM7汇编语言简介
- 第4章第6节 任务自结束
- 第3章第1节 两个固定任务之间的切…
- 第4章第1节 Mindows的文件组织结构
- 第2章第5节 Wanlix的开发环境
- 第3章第2节 任意任务间的切换
- 第3章第4节 使用Wanlix编写交通红…
- 第4章第5节 任务创建和任务删除钩…
- 定时器,第1次就是在间隔的秒数之后才触发
- 第3章第4节 使用Wanlix编写交通红…
- Makefile编写及一个简单的Makefile架构实现
- 第3章第5节 发布Wanlix操作系统
- 第4章 Mindows操作系统
- 第4章第1节 Mindows的文件组织结构
- 第4章第2节 定时器触发的实时抢占…
- 第4章第2节 定时器触发的实时抢占…
- 第4章第3节 实时事件触发的实时抢…
- 第4章第3节 实时事件触发的实时抢…
- MXNet的数据读取:data.py源码详解
- 第4章第3节 实时事件触发的实时抢…
- Vmware 将kalilinux装进 U盘
- cookie 和session 的区别详解
- JavaScript(jquery)写的像素游戏贪吃蛇