第4章第2节 定时器触发的实时抢占…

SysTick 原理简介以及中断进不去的原因 SysTick 阅读详情

源码请在https://github.com/ifreecoding/MbedRtos.git下载

第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所示:


 

图 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态。

    同一时刻会有很多任务处于ready态,这些处于ready态的任务的优先级是一个非常重要的属性,操作系统就是依靠优先级这个属性来调度任务的。我们可以做一个ready表,使用这个ready表将各个不同优先级的任务关联起来,任务调度时只需要查找ready表就可以找到各个任务了。我们先以8级(0~7)优先级为例,如图30所示:

图 30  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 struct m_chain              

{

    struct m_chain* pstrHead;       

    struct m_chain* pstrTail;       

}M_CHAIN;

pstrHead是链表的头指针,pstrTail是链表的尾指针。当链表为空,即链表只有根节点时,根节点的头尾指针都指向空指针。当链表不为空时,根节点的头指针指向链表中的第一个子节点,根节点的尾指针指向链表中的最后一个子节点,每个子节点的头指针指向它前面的节点(包括根节点),每个子节点的尾指针指向它后面的节点(包括根节点)。这样所有的子节点与根节点就组成了一个环状的双向链表结构,通过根节点可以快速找到链表的第一个和最后一个子节点,通过任何一个节点都可以找到它前后的节点。我们来看看链表为空,有1个节点,2个节点和多个节点的情况:

图 31  空链表

图 32  拥有1个子节点的链表

图 33  拥有2个子节点的链表

图 34  拥有多个子节点的链表

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 astrChain[8];

任务若要挂到ready表上,那么任务里也需要有一个M_CHAIN的链表节点结构,任务的这个链表节点结构就被放到了TCB里面,后面我们在介绍TCB的时候再详细说。

    根据前面链表的定义我们知道,当链表为空时,链表根节点的头尾指针指向NULL,我们可以根据这一点来查找ready表中所挂接的最高优先级任务。但这样做有一个问题,当ready表中有最高优先级为0的任务时(在Mindows中,我们定义优先级0为最高优先级),我们只需要查找astrChain[0]的链表,发现astrChain[0]的链表不为空就不需要再继续查找了,而当ready表中只有优先级为7的任务时,我们则需要依次查询strChain[0]~strChain[7]的链表,这样做不但效率不高,而且查询ready表所花费的时间也不固定。实时操作系统不但需要有实时性,而且调度时间也需要有确定性,这样系统才能运行平稳,调度时间不会有大的波动。为了弥补这个缺陷,我们为ready表的每个根节点配备一个标志,用这些标志来指明这些根节点是否为空,这样,我们就可以通过这些标志快速的查找到挂有任务的最高优先级的链表根节点,而不需要直接去查询每个根节点里的头尾指针是否为NULL,如图35所示:

图 35  ready表链表根节点与标志的对应关系图

8个标志位对应8个优先级的根节点,我们正好可以定义1个字节的变量ucPrioFlag,使用这个变量里面的每个bit作为一个标志来表示各个优先级的链表是否为空,bit0~bit7分别对应astrChain[0]~astrChain[7],bit为0代表链表为空,bit为1代表链表不为空,这个优先级的链表上有任务处于ready状态。

到这里,我们已经完成了具有8个优先级ready表的设计,我们再来看看对ready表的操作过程。

u 初始化ready表

将ready表中的每个根节点的头尾指针都指向NULL,使8个优先级的链表都为空,同时,将标志置为0,这时候ready表上没有挂接任何任务。

u 将节点添加到ready表

当有任务变为ready态时,我们将任务TCB中的链表节点添加到ready中相同优先级的链表上,并将对应的标志bit置为1。

u 从ready表拆离一个节点

当有任务需要从ready表拆离时,我们就将这个任务的节点从ready表中相同优先级的链表中拆除,同时将对应的标志bit置为0。

u 查询ready表中的最高优先级

这个过程稍微复杂一些,听我慢慢道来。

我们来看看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

表 6  ready表标志与优先级关系

通过这几组数据我们可以看出来,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 U8 caucTaskPrioUnmapTab[256] =    

{

    0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,

    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0

};

当我们需要查找ready表中的最高优先级时,就可以把标志ucPrioFlag当做数组下标带入到数组caucTaskPrioUnmapTab中,数组元素的值caucTaskPrioUnmapTab [ucPrioFlag]就是ready表中的最高任务优先级。这样查找最高优先级的过程非常简单,速度快且花费时间相同,而且系统开销也不大。

当标志值为0时,这种情况是不存在的,ready表中至少会有一个任务,至少处于running态的任务就位于ready表中,所以caucTaskPrioUnmapTab[0]的值是无效的,只是用来占个坑填满组数。

8个优先级的ready表相关内容已经全部讲述完毕。

8个优先级对于小型嵌入式设备来说应该是够用了,但对稍大一些的嵌入式设备来说就显得太少了。我们可以通过扩充astrChain数组的数组元素数量来扩充Mindows所支持的优先级数量,同时,也需要相应的扩充ucPrioFlag标志的长度好与之相对应。通过这种方法,我们可以将Mindows支持的优先级数量扩展到支持8、16、32、64、128、256这6种不同的级别上,用下面的宏分别区分这些级别:

#define PRIORITY256         256

#define PRIORITY128         128

#define PRIORITY64          64

#define PRIORITY32          32

#define PRIORITY16          16

#define PRIORITY8           8

用户在确定使用哪个数量的优先级时,需要在mds_userdef.h文件里将PRIORITYNUM宏定义为上面这6种优先级数量中的一个,例如,用户使用下面的宏定义,选择Mindows支持32个优先级数量:

#define PRIORITYNUM         PRIORITY32

操作系统通过PRIORITYNUM宏就可以知道用户所希望使用的优先级数量了,那么,现在我们把ready表的结构重新整理一下:

typedef struct m_taskschedtab           

{

    M_CHAIN astrChain[PRIORITYNUM];     

    M_PRIOFLAG strFlag;                 

}M_TASKSCHEDTAB;

astrChain仍是链表数组,每个数组元素对应一个优先级的链表根节点,优先级的数量由PRIORITYNUM宏确定。strFlag是每个优先级链表对应的标志,M_PRIOFLAG结构体如下:

typedef struct m_prioflag               

{

#if PRIORITYNUM >= PRIORITY128

    U8 aucPrioFlagGrp1[PRIOFLAGGRP1];

    U8 aucPrioFlagGrp2[PRIOFLAGGRP2];

    U8 ucPrioFlagGrp3;

#elif PRIORITYNUM >= PRIORITY16

    U8 aucPrioFlagGrp1[PRIOFLAGGRP1];

    U8 ucPrioFlagGrp2;

#else

    U8 ucPrioFlagGrp1;

#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所示:

图 36  ready表256级标志分级方法

优先级反向查找表只支持8个优先级,256可以分解为4×8×8,第三级有4个bits,每个bit分别对应第二级的一个Byte,第二级每个Byte中的每个bit分别对应第一级的一个Byte,这样到第一级就有4×8×8共256个bits,每个bit对应一个优先级链表,用来指示该优先级链表是否为空。第一级标志对应着每个优先级链表,第二级和第三级是为了查找第一级而设立的,我们来看看操作ready表时这个3级标志是怎么处理的:

u 初始化ready表

将3级标志全部置为0。

u 将节点添加到ready表

将每一级标志中对应的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 从ready表拆离一个节点

将第一级标志中对应的bit置为0,若该bit所属的第二级和第三级中没有其它任务的标志,则将第二级、第三级对应的bit也置为0,否则维持原状不变。比如ready表中有143和144这两个优先级的任务,如果要拆除143时,将第一级143的标志置为0,而第二级Byte2中的bit1和第三级的bit2是不能置为0的,因为144优先级也位于第二级Byte2中的bit1和第三级的bit2。

u 查询ready表中的最高优先级

查询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  void MDS_TaskAddToSchedTab(M_CHAIN* pstrChain, M_CHAIN* pstrNode,

00195                             M_PRIOFLAG* pstrPrioFlag, U8 ucTaskPrio)

00196  {

00197      

00198      MDS_ChainNodeAdd(pstrChain, pstrNode);

00199  

00200      

00201      MDS_TaskPrioFlagSet(pstrPrioFlag, ucTaskPrio);

00202  }

194行,入口参数pstrChain是ready表中优先级链表的根节点指针,也就是astrChain数组里面的一个数组元素的指针。入口参数pstrNode是需要挂接到链表上的节点。入口参数pstrPrioFlag是优先级标志的指针。入口参数ucTaskPrio是挂入的任务的优先级。

这个函数的作用就是将优先级为ucTaskPrio的任务的节点pstrNode,挂入到pstrChain链表上,同时将pstrPrioFlag标志中对应的位置置为1。

198行,将子节点添加到链表中。

201行,设置对应位置的标志。

MDS_TaskPrioFlagSet函数的原理前面已经介绍过,代码如下,就不一一讲解了。

00230  void MDS_TaskPrioFlagSet(M_PRIOFLAG* pstrPrioFlag, U8 ucTaskPrio)

00231  {

00232  #if PRIORITYNUM >= PRIORITY128

00233      U8 ucPrioFlagGrp1;

00234      U8 ucPrioFlagGrp2;

00235      U8 ucPosInGrp1;

00236      U8 ucPosInGrp2;

00237      U8 ucPosInGrp3;

00238  #elif PRIORITYNUM >= PRIORITY16

00239      U8 ucPrioFlagGrp1;

00240      U8 ucPosInGrp1;

00241      U8 ucPosInGrp2;

00242  #endif

00243  

00244      

00245  #if PRIORITYNUM >= PRIORITY128

00246  

00247      

00248      ucPrioFlagGrp1 = ucTaskPrio / 8;

00249      ucPrioFlagGrp2 = ucPrioFlagGrp1 / 8;

00250  

00251      

00252      ucPosInGrp1 = ucTaskPrio % 8;

00253      ucPosInGrp2 = ucPrioFlagGrp1 % 8;

00254      ucPosInGrp3 = ucPrioFlagGrp2;

00255  

00256      

00257      pstrPrioFlag->aucPrioFlagGrp1[ucPrioFlagGrp1] |= (U8)(1 << ucPosInGrp1);

00258      pstrPrioFlag->aucPrioFlagGrp2[ucPrioFlagGrp2] |= (U8)(1 << ucPosInGrp2);

00259      pstrPrioFlag->ucPrioFlagGrp3 |= (U8)(1 << ucPosInGrp3);

00260  

00261  #elif PRIORITYNUM >= PRIORITY16

00262  

00263      ucPrioFlagGrp1 = ucTaskPrio / 8;

00264  

00265      ucPosInGrp1 = ucTaskPrio % 8;

00266      ucPosInGrp2 = ucPrioFlagGrp1;

00267  

00268      pstrPrioFlag->aucPrioFlagGrp1[ucPrioFlagGrp1] |= (U8)(1 << ucPosInGrp1);

00269      pstrPrioFlag->ucPrioFlagGrp2 |= (U8)(1 << ucPosInGrp2);

00270  

00271  #else

00272  

00273      pstrPrioFlag->ucPrioFlagGrp1 |= (U8)(1 << ucTaskPrio);

00274  

00275  #endif

00276  }

MDS_TaskHighestPrioGet函数是查询ready表中最高优先级的函数,入口参数pstrPrioFlag是优先级标志的指针,原理前面已经介绍过,代码如下,不再做介绍了。

00283  U8 MDS_TaskHighestPrioGet(M_PRIOFLAG* pstrPrioFlag)

00284  {

00285  #if PRIORITYNUM >= PRIORITY128

00286      U8 ucPrioFlagGrp1;

00287      U8 ucPrioFlagGrp2;

00288      U8 ucHighestFlagInGrp1;

00289  #elif PRIORITYNUM >= PRIORITY16

00290      U8 ucPrioFlagGrp1;

00291      U8 ucHighestFlagInGrp1;

00292  #endif

00293  

00294      

00295  #if PRIORITYNUM >= PRIORITY128

00296  

00297      ucPrioFlagGrp2 = caucTaskPrioUnmapTab[pstrPrioFlag->ucPrioFlagGrp3];

00298  

00299      ucPrioFlagGrp1 =

00300                  caucTaskPrioUnmapTab[pstrPrioFlag->aucPrioFlagGrp2[ucPrioFlagGrp2]];

00301  

00302      ucHighestFlagInGrp1 = caucTaskPrioUnmapTab[pstrPrioFlag->aucPrioFlagGrp1

00303                                               [ucPrioFlagGrp2 * 8 + ucPrioFlagGrp1]];

00304  

00305      return (U8)((ucPrioFlagGrp2 * 8 + ucPrioFlagGrp1) * 8 + ucHighestFlagInGrp1);

00306  

00307  #elif PRIORITYNUM >= PRIORITY16

00308  

00309      ucPrioFlagGrp1 = caucTaskPrioUnmapTab[pstrPrioFlag->ucPrioFlagGrp2];

00310  

00311      ucHighestFlagInGrp1 =

00312                  caucTaskPrioUnmapTab[pstrPrioFlag->aucPrioFlagGrp1[ucPrioFlagGrp1]];

00313  

00314      return (U8)(ucPrioFlagGrp1 * 8 + ucHighestFlagInGrp1);

00315  

00316  #else

00317  

00318      return caucTaskPrioUnmapTab[pstrPrioFlag->ucPrioFlagGrp1];

00319  

00320  #endif

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  优先级数量与需要使用的内存数量

从表7可以看到,8个优先级的ready表只需要使用65个字节就可以了,这对资源少的嵌入式系统来说是完全可以接受的,而256个优先级的ready则需要使用2085个字节,单单一个ready表就已经超过了2KBytes的内存,这对只有几KBytes或者更少内存的嵌入式设备来说已经不适合了。但2KBytes的内存对于拥有几百MBytes甚至几GBytes的嵌入式设备来说却又不算什么,这样的大系统也会很复杂,8个优先级远远不能满足需求,也需要操作系统能支持256个优先级。我们在设计软件系统时,需要根据硬件资源的限制,合理选择Mindows所支持的优先级数量。

ready表就介绍到这里,接下来我们来看一下TCB结构。

typedef struct m_tcb

{

    STACKREG strStackReg;           

    M_TCBQUE strTcbQue;             

    U8 ucTaskPrio;                  

}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 struct stackreg

{

    U32 uiCpsr;

    U32 uiR0;

    U32 uiR1;

    U32 uiR2;

    U32 uiR3;

    U32 uiR4;

    U32 uiR5;

    U32 uiR6;

    U32 uiR7;

    U32 uiR8;

    U32 uiR9;

    U32 uiR10;

    U32 uiR11;

    U32 uiR12;

    U32 uiR13;

    U32 uiR14;

    U32 uiR15;

}STACKREG;

我们再来看看TCB中的M_TCBQUE结构体,

typedef struct m_tcbque             

{

    M_CHAIN strQueHead;             

    struct m_tcb* pstrTcb;          

}M_TCBQUE;

M_TCBQUE队列结构相比M_CHAIN链表结构来说,只是多了一个M_TCB型的指针pstrTcb。我们从ready表中获取最高优先级任务时,先获取到最高优先级,再通过最高优先级获取到链表根节点,再通过根节点获取到挂在上面的任务的子节点,然后应该就是再通过子节点获取到任务的TCB。pstrTcb指针在任务初始化时就被初始化为任务的TCB指针,因此在获取到子节点strQueHead的地址后,我们就可以通过它下面的pstrTcb获取到TCB的地址,进而可以获取整个TCB的信息。

Frida三层次钩子快速逆向VMP+OLLVM加固APK VMP(虚拟机保护)和OLLVM(控制流平坦化)是Android应用常见的双重混淆技术,其核心原理是将Java逻辑虚拟化、Native代码控制流打散,从而阻断传统静态分析与简单Hook。但任何混淆都必须在CPU上执行,因此必然暴露JNI注册行为、ABI参数传递规律及系统调用痕迹。基于此,Frida可通过跨层联动——从JNI_OnLoad捕获Native库加载、在VMP解释器入口截获字节码上下文、于关键系统调用(如read/gettimeofday)反向追踪数据流——构建可观测、可复现的动态分析链。该方法无需 阅读详情

相关推荐

RoboMaster机甲大师赛备赛:如何用STM32+CAN总线精准控制四个大疆3508电机?

本文详细介绍了在RoboMaster机甲大师赛中,如何利用STM32和CAN总线技术精准控制四台大疆3508电机。从硬件架构设计、多电机ID分配、数据解析到同步控制算法,全面解析了构建高响应、低延迟底盘驱动系统的关键技术,助力参赛队伍提升机器人运动性能。

weixin_28684825的博客 133

RT-Thread学习笔记(13):软件定时器

硬件定时器和软件定时器 硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。 软件定时器,软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的。 使用硬件定时器时,每次在定时时间到达之后就会自动触发一个中断,用户在中断中处理信息;而使

BLBQ962464的博客 3097

【NLP】使用 PyTorch 从头构建自己的大型语言模型 (LLM)

如果我们在 n 维空间中绘制一个向量,看起来相似的物体(如狗和猫)将彼此靠近,而看起来不相似的物体(如学校和家庭)的嵌入向量将位于更远的地方。为此,我们将使用一种流行的标记器,称为 BPE 标记器,这是一种子词标记器,正在 GPT3 等模型中使用。:前馈网络使用深度神经网络来学习两个线性层(第一层有 d_model 节点,第二层有 d_ff 节点,根据注意力论文分配值)中嵌入向量的所有特征,并且将 ReLU 激活函数应用于第一线性层的输出,为嵌入值提供非线性,并应用 dropout 以进一步避免过度拟合。

sikh_0529的博客 1950

STM32CubeMX外部中断定时器嵌套问题及实验现象

现在没有对下降沿触发这个动作进行消抖的判断,并且这么多天实验没有发现抖动现象,消抖的话我打算最后解决了嵌套问题后加上。 目前TIM2定时器的抢占优先级和响应优先级是(1,1),GPIO抢占优先级和响应优先级是(2,2) 这个部分的内容是写在HAL库的GPIO外部中断 EXTI15_10_IRQHandler10中的回调函数HAL_GPIO_EXTI_Callback中,HAL库的逻辑是执行完回调函数然后配置好了关闭中断的函数,所以一般不需要在回调函数里用完之后手动关闭。 我现在的实现过程是: 按照这个思

“逛丢一只鞋”的博客 3327

Windows 11 RDP Wrapper 配置教程与常见问题解决方法

解锁家庭版RDP主机功能(Windows 11家庭版原生不支持作为RDP服务器)突破单用户连接限制,实现多用户同时远程登录(类似服务器版功能)它不修改系统核心文件,通过注入方式工作,相对安全可靠。RDP Wrapper是Windows 11家庭版用户启用远程桌面、突破单用户限制的高效工具。遵循本文步骤可快速完成配置,遇到问题时按常见问题排查方案逐一解决,多数问题可通过更新配置文件或重启服务解决。若长期使用,建议关注官方GitHub仓库的更新,确保兼容性和安全性。

weixin_41521681的博客 1363

金蝶云ERP远程办公指南:零公网IP实现安全内网穿透访问

本文详细介绍了如何利用内网穿透技术,在无需公网IP和复杂路由器设置的情况下,安全远程访问内网部署的金蝶云ERP系统。通过使用cpolar工具,逐步指导用户创建加密隧道、配置固定域名,并实现管理中心与数据中心的远程登录,有效解决了企业员工在外出或居家办公时的核心业务系统访问难题。

weixin_29229261的博客 269

关于STM32定时器使用的一个注意事项(以此为前车之鉴,重要!)

转自:https://blog.youkuaiyun.com/ludaoyi88/article/details/51934122 我们平时使用定时器的时候多数都是处于开启状态,平时的定时中断书写格式一般是: void TIM3_IRQHandler(void){ if(TIM_GetITStatus(TIM3, TIM_IT_Update) == SET){ TIM_ClearITPend...

724

FOC-滑膜控制器SMC/滑膜观测器SMO

三相PMSM是一个非线性、强耦合的多变量控制系统,对于外界的扰动或者电机自身的参数发生变化时,传统的PI控制并不能满足实际的要求。因此可以引入滑模控制SMC,它对扰动和参数不敏感,响应速度也够快。简单来说就是,滑膜观测器就是通过采样与坐标变换得到的Ualpha,Ubeta,Ialpha,Ibeta四个参数值,获取。SMO叫滑膜观测器(Sliding Mode Observer),用于获取转子位置信息与转速信息。2、作为滑膜观测器SMO,获取电机角度与转速信息。之间的误差来设计滑膜观测器,并通过该误差。

小胡同学的工程师学习之路 1万+

linux内核时钟节拍tick

linux时钟节拍tick的原理和实现,基于linux-5.4.18内核源码分析。 从内核启动开始,一直到tick正常切换到高精度模式后的实现和工作

kkqier的博客 5645

海洋捕食者算法(MPA)详解:从海洋生态智慧到高性能优化

摘要: 海洋捕食者算法(MPA)是一种基于海洋生态觅食行为的元启发式优化算法,模拟捕食者(如鲨鱼)的布朗运动和莱维飞行策略进行全局与局部搜索。算法分为三阶段:高速比阶段(全局探索)、单位速度比阶段(平衡搜索)和低速比阶段(局部开发),通过精英矩阵和FADs效应(概率扰动)避免早熟收敛。MPA具有参数少、收敛快、寻优能力强等特点,适用于复杂优化问题,其数学模型将生物行为转化为随机游走与自适应策略,并通过收敛因子动态调整探索-开发平衡。实验表明,MPA在多个基准问题上表现优异。

qq_42212808的博客 273

毕设-MobileNetV4:给YOLOv8装上轻量化“引擎”——通用瓶颈与C2f融合的实战教程

本文介绍了一种将MobileNetV4与YOLOv8结合的目标检测模型轻量化方案。MobileNetV4通过通用瓶颈(UB)结构实现多任务适配,其核心改进包括动态卷积核、注意力机制和多路径特征聚合,显著提升特征提取能力。作者详细阐述了如何在YOLOv8框架中实现MobileNetV4的通用瓶颈模块,包括"膨胀-卷积-注意力-压缩"的流程设计,并构建完整的backbone网络。该方案通过融合MobileNetV4的轻量化优势与YOLOv8的高效检测架构,在移动设备上实现了精度与速度的良好平

FJN110的博客 505

cpu是如何实现抢占的

在计算机系统中,抢占是指操作系统能够中断正在执行的任务,将控制权转移到其他任务,从而实现多任务(multitasking)的一种机制。在 CPU 中,抢占是通过中断(interrupt)和时钟中断(timer interrupt)来实现的。这个过程使得操作系统能够在不同任务之间进行快速切换,实现多任务的同时,确保每个任务都能够得到执行。时钟中断是最常见的触发机制,但还有其他类型的中断,如硬件中断、软件中断等,都可以导致 CPU 执行相应的中断处理程序。

weixin_43892979的博客 1075

【云原生进阶之容器】第六章容器网络6.6.2--Cilium部署

部署Cilium非常简单,可以通过单独的yaml文件部署全部组件(目前我使用了这个方式部署了1.7.1版本),也可以通过Helm Chart一键完成。重要的是部署环境和时机:官方建议所有部署节点都使用Linux最新稳定内核版本,这样所有的功能都能启用。作为一个Kubernetes网络组件,它应该在部署Kubernetes其他基础组件之后,才进行部署。

junbaozi的专栏 792

从零手写操作系统之RVOS抢占式多任务实现-06

实现抢占式多任务

m0_53157173的博客 1112

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

FreeRTOS 实时操作系统第五讲 - 任务调度 (抢占式,时间片与合作式),任务栈大小确定与溢出检测

如下图:Test1-3 使用的栈空间:(128-106)=22 words, 乘以安全系数,分配 33 words 即可。

qq_37391577的博客 2763

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

Java定时器的使用方法与调度策略

在软件开发中,定时任务的执行是一种常见的需求,特别是需要在特定时间间隔内执行重复任务或者在未来某个时间点执行任务。Java提供了多种方式来实现定时任务,本文将介绍Java定时器的使用方法及其调度策略,帮助开发者根据需求选择合适的定时任务解决方案。Java定时器是处理定时任务的一种简单且有效的方式,通过灵活的调度策略可以满足各种业务场景的需求。在使用Java定时器时,需要考虑任务的调度策略,以便满足不同的业务需求和性能要求。方法可以确保任务按照固定的速率执行,即使前一个任务的执行时间超过了周期。

微赚开发者技术分享博客 571

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

挂起进程和进程的阻塞和挂起的区别

挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态.. 转自http://blog.sina.com.cn/s/blog_9fa53b5f0102vyuy.html 理解一:挂起是一种主动行为,因此

qingkongyeyue的博客 5226

vxworks基础知识--更新

timex,我们可以在shell下调用timex myFunc,myArg1,myArg2,...(最多8个参数),就可以测量myFunc的执行时间,但如果myFunc的执行时间过短(意味着误差比较大),timex会提示我们使用timexN重测以求精确结果。      我们可以把任务的状态分为两类,READY、DELAY和PEND称为任务的基本状态,它们是互斥的,也就是一个任务同一时刻有且只能有

学海无涯,回头是岸........ 6988

杨老师课堂之JavaScript定时器_农夫山泉限时秒杀案例 ...

预览效果图: 使用到的知识点:   定时器 setInterval(函数,毫秒):在指定的毫秒数后调用函数或执行一段代码   取消定时器 clearInterval:取消由setInterval设置的定时器   函数(日期函数、parseInt函数)     parseInt()函数 :可解析一个字符串,并返回一个整数 示例代码: <!DOC...

weixin_34143774的博客 828

tasklet 工作队列 内核定时器 内核线程

1. tasklet只可以在一个CPU上同步地执行,不同的tasklet可以在不同地CPU上同步地执行. 2. tasklet的实现是建立在两个软件中断的基础之上的,即HI_SOFTIRQ和TASKLET_SOFTIRQ,本质上没有什么区别,只不过HI_SOFTIRQ的优先级更高一些. 3. 由于tasklet是在软中断上实现的,所以像软中断一样不能睡眠、不能阻塞,处理函数内不能含有导致睡眠的

cl11010的专栏 2528

第2章第2节&nbsp;ARM7汇编语言简介

源码请在https://github.com/ifreecoding/MbedRtos.git下载 第2节ARM7汇编语言简介 ARM7芯片有2种汇编语言指令集,一种叫做ARM指令集,字长为32bits,另一种叫THUMB指令集,字长为16bits。这两种指令集各有优缺点,它们可以单独使用也可以混合在一起使用,在ARM7芯片上,我们将只使用ARM指令集,在后续的Cortex芯片上

ifreecoding的博客 1834

底层工作者手册之嵌入式操作系统内…

源码请在https://github.com/ifreecoding/MbedRtos.git下载 我在写本手册前没有任何有关操作系统内核的知识,有的仅仅是简单的使用过2个操作系统的经验,也仅限于对操作系统应用层一些功能的简单了解。我在写操作系统内核时也只能从这些应用经验出发,参考一些资料,更多的是自己想办法用最顺其自然的代码实现操作系统的这些内核功能。因此,你要相信,既然我能在

ifreecoding的博客 1707

第2章第1节&nbsp;ARM7芯片基本结构

源码请在https://github.com/ifreecoding/MbedRtos.git下载 第1节ARM7芯片基本结构 ARM7芯片构架比较简单,32bits线性地址空间统一排列,任何地址都是唯一的,不同的片上资源及外设被分配到不同的地址空间,不同数据结构的指针固定为4字节长度,这相对51芯片来说方便很多也清晰很多,从用户编程的角度来看入手比较简单,因此本手册首先选用

ifreecoding的博客 900

第1章第2节&nbsp;操作系统的分类

源码请在https://github.com/ifreecoding/MbedRtos.git下载 第2节操作系统的分类 操作系统是管理整个软硬件系统的软件系统,从不同的角度操作系统可以有很多种划分,比如按与用户对话的界面分类可分为命令行界面操作系统和图形用户界面操作系统,按支持用户数的多少可以分为单用户和多用户操作系统,按功能可以分为嵌入式操作系统和PC机通用操作系统,按调度的

ifreecoding的博客 837
上一篇: 第4章第1节&nbsp;Mindows的文件组织结构
下一篇: 第4章第2节&nbsp;定时器触发的实时抢占…
ifreecoding
博客等级 码龄15年 0粉丝 33原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值