查看: 1378|回复: 0
注册时间最后登录阅读权限1积分170精华0帖子
侠客, 积分 170, 距离下一级还需 130 积分
摘要:在μC/OS-II内核中,各个不同的任务使用独立的堆栈空间,堆栈的大小按每个任务所需要的最大堆栈深度来定义,这种方法可能会造成堆栈空间浪费。本文叙述如何在RTOS中多个任务共用连续存储空间作为任务栈的方法,并详细比较二者的优缺点和适用性。& &&&关键词:μC/OS-II 任务堆栈 RTOS 共用空间堆栈
关于μC/OS-II这个实时内核及其应用已经有很多文章介绍了,对于学习RTOS的人来说,这个系统是很好的学习起点。虽然文献[1]的源代码没有行号和函数名交叉索引表等,给源代码阅读造成一些困难(可使用BC31的grep查找功能,提高阅读效率),好在代码不是很长,前面又有详细的中文说明,对于有一定X86汇编和C语言基础的人来说,仍然可以在不长的时间内掌握。
μC/OS-II内核是一个抢先式内核,可以进行任务间切换,也可以让一个任务在得不到某个资源时休眠一定时间后再继续运行;提供了用于共享资源管理的信号灯,用于进程通信的消息队列和邮箱,甚至提供了存储器管理机制,一个比较全面的系统。
μC/OS-II内核有些地方仍然值得改进,比如该系统不支持时间片调度。如果有一个任务中一段死循环代码(或者条件循环代码),代码就会永远(或长时间)在此处执行,调度程序无法控制,其它任务也就是不到及时执行。这种抢先式实际上和非抢先式系统存在着同样问题。当然,如果这种代码不一个BUG,问题是可以解决的,在不提供时间片调度的抢先式系统中,一般采取信号灯,或者任务主动休眠的方法(对于μC/OS-II,很容易改造成支持时间片调度,只要在定时中断服务程序调用OSIntCtxSw()函数即可);非抢先式系统一般采取有限状态机方法,不使用这种耗时很长的循环代码。不过,无论如何,对RTOS的使用者来说,这毕竟会使得任务函数的编码不能随心所欲。
ΜC/OS-II内核的另外一个值得改进的地方就是其任务栈管理方法。在μC/OS-II内核中,各个不同的任务使用独立的堆栈空间,堆栈的大小按每个任务所需要的最大堆栈深度来定义,这种方法可能会造成堆栈空间的浪费。下面讨论如何在RTOS中多个任务共用一段连续存储空间作为傻堆栈。
1 任务切换要保存的数据
简单地说,一个任务可看作一个运行中的C函数。对于抢先式RTOS来说,在任务切换时,应保存当前任务的各种现场数据。现场数据包括局部变量、各个CPU寄存器、堆栈指针和程序被中止的任务指针。CPU寄存器是任何任务代码均会用到的;而局部变量,一般的编译器是将其它安排在堆栈空间中,堆栈指针也是各任务公用的,所以也需要保存。
对于全局变量,由于一般是在内存中的固定位置,各任务所占用的空间完全独立,所以不需要保存。
在X86环境中,要保存的CPU寄存器共14个16位寄存器;通用寄存器8个(AX、BX、CX、DX、SP、BP、SI、BI)、段寄存器4个(CS、DS、ES、SS)以及指令指针IP和标志寄存器FR各1个。
2 C编译器中变量在堆栈中的位置
对于一个存在函数调用嵌套的C程序来说,大部分编译器将传递的参数和函数本身的局部变量放在了堆栈中,编译器会自动生成压栈(push)和弹栈(pop)代码,以保存上级函数的运行寄存器。
假设函数main()调用funl(),而funl()调用fun2(),则在执行fun2()中的代码时,堆栈映像如图1所示(X86 CPU的情况)。
对于RTOS软件,堆栈中的各种数据就是一个任务的作现场。一般CPU的堆栈指针SP只有一个,在进行任务切换时,必须将挂起任务所使用的堆栈内容保存起来,以便使该任务在下次唤醒时能从原地继续运行。
3 μC/OS-II对任务栈的处理方法与缺陷
μC/OS-II为了保存任务堆栈中的数据,对每个任务定义一个数组变量作为堆栈,在任务切换时,将CPU堆栈指针SP指向该数组中的某个元素,即栈顶,如图2所示。
比如,在其ex21.c文件中定义的任务堆栈语句为:
OS_STK TaskStartStk[TASK_STK_SIZE]; /*启动任务堆栈*/
OS_STK TaskClkStk[TASK_STK_SIZE]; /*时钟任务堆栈*/
OS_STK TasklStk[TASK_STK_SIZE]; /*任务1#,任务堆栈*/
以上各任务堆栈数组变量在初始化函数OSTCBInit()中被会给了任务控制块OS_TCB的OSTCBStkPtr变量。在任务切换时,μC/OS-II调用OSCtxSw汇编过程(OS_CPU_A.ASM文件),将CPU的SP指针指向该变量,从而使每个任务使用独立的任务堆栈。
LES BX,DWORD PTR DS:_OSTCBCur
;保存挂起任务的堆栈指针SP
MOV ES:[BX+2],SS
MOV ES:[BX+0],SP
LESB X,DWORD PTR DS:_OSTCBHighRdy ;切换SP到要运行任务的堆栈空间
MOV SS,ES:[BX+2]
MOV SP,ES:[BX]
& & 在代码中,变量OSTCBHighRdy(OSTCBCur)和堆栈指针变量OSTCBStkPtr的数值是同同的,因为OSTCBStkPtr是结构OSTCBHighRdy的第一个变量。这种任务栈处理方法的缺点是可能造成空间的浪费。因为一个任务如果堆栈满了,该任务也就无法运行,即使其它任务的堆栈还有空间可用。当然,这种方法的好处是任务栈切换的时间非常短,只需要几条指令。
4 共用空间的堆栈处理方法
(1)栈共用连续存储空间
如果多个任务使用同一段连续空间作为堆栈,这样各个堆栈之间就可以互补使用。在前面说过,共用空间的问题在于一个任务运行时不能破坏其它任务的堆栈数据。为简单起见,先看图3所示两个任务的情况。
假定任务1首次运行时任务栈为空。运行一段时间后任务2运行,堆栈空间继续往上生长。这次任务切换不需要修改CPU的SP数值,但需要记下任务1的栈顶位置SP1(图3中)。
在任务2运行一段时间后,RTOS又切换到任务1运行。在切换时,不能简单地将SP指针修改回SP1的数值,因为这样堆栈向上生长时会破坏任务2堆栈中的数据。办法是将原来任1务堆栈保存的数据移动到靠栈顶的位置,而将任务2堆栈数据下移到靠栈底的位置,堆栈指针SP实际上不需要修改(图3右)。
考虑到更为一般的情况,有N个任务,当前运行的任务为k,下一个运行的任务为j,在共用任务堆栈时必须做的工作有:
*为每个任务定义栈顶和栈底2个堆栈指针;
*在任务切换时,将待运行任务j的堆栈内容移动到靠栈顶位置,同时将其堆栈上方的任务堆栈下移,修改被移动推栈的任务堆栈指针。
假设我们定义的任务栈空间和任务的栈指针变量为:
void TaskSTK[MAX_STK_LEN];/*任务堆栈空间*/
typedef struct TaskSTKPoint{
int TaskID;
int pTopSTK;
int pBottomSTK;
}TASK_STK_POINT;
TASK_STK_POINT pTaskSTK[MAX_TASK_NUM]; /*存放每个任务的栈顶和栈底指针*/
任务栈指针数组pTaskSTK的元素个数同任务个数。为了堆栈交换,需要另外一块临时存储空间,其大小可按单个任务栈最大长度定义,用于中转堆栈交换的内容。堆栈内容交换的伪C算法可写为:
StkEechange(int CurTaskID,int RunTaskID)
{ /*2个参数为当前运行任务号和下一运行任务号*/
void TempSTK[MAX_PER_STK_LEN]; /*注意该变量长度可小于TaskSTK*/
L=任务RunTaskTD的堆栈长度;
①将TaskSTK顶部的L字节移动到TempSTK中;
②将RunTaskID任务的堆栈内容移动到TaskSTK顶部;
③将RunTaskID堆栈上方(移动前位置)所有内容下移L个字节;
④修改RunTask堆栈上方(移动前位置)所有任务栈顶和栈底指针(pTaskSTK变量);
该算法的平均时间复杂度可计算如下:
O(T)=SL/2+SL/2+SL×N/2
式中,第一、二项为步骤①和步骤②时间,第三项为步骤③时间;SL表示每个任堆栈的最大长度(即MAX_PER_STK_LEN),N表示任务数。
取SL为64字节,任务数为16个,则数据项平均移动次数为576。假设每次移动指令时间为2μs,则一次任务栈移动时间长达约1ms。所以在使用该方法时,为了执行时间尽量短,编码时应仔细推敲。
从空间上说,共用任务栈比独立任务栈优越。假设独立任务栈方法中每个堆栈空间为K,任务数为N,则独立任务栈方式的堆栈总空间为N×K。在共用任务栈时,考虑各任务互补的情况,TaskSTK变量不需要定义为N×K长度,可能定义为二分之一或者更小就可以了。
另外,这种方法不需要在任务切换时修改CPU的SP指针。
(2)工作栈和任务堆栈
上节共用任务栈算法的缺点是:任务切换时的堆栈内容交换算法复杂,占用时间长。另外一个折中的方法是设计一个工作堆栈,用于给当前运行的任务使用;在任务切换时,将工作栈内容换出得另外的存储空间,该空间可以动态申请,其大小按实际需要即可。
这种方法看起来和独立任务栈的方法类似,需要N+1块存储空间,其中一块用于工作栈空间。和独立任务堆栈相比,其区别有2点:
①SP指针所指向的空间始终是同一块存储空间,即工作栈;
②每个任务栈的大小不需要按最大空间定义,可以动态按实际大小从内存中分配空间。
对于8031这种处理器结构,由于堆栈指针只能指向其内部存储器,大小十分有限。采取这种方法,可将工作栈设在内部RAM,将任务栈设在外部RAM,扩展了堆栈空间。
和上一种共用堆栈方法相比,这种方法的交换时间要短,其时间复杂度约为1.5倍最大任务栈长度。
独立任务栈的方法适合于存储器充足、任务切换频繁、对任务切换时间要求较高的场合,一般主要用在16位或者32位微处理器平台环境。值得注意的是,在某些微处理器中,虽然可使用的数据存储器可以设计得较大,但堆栈所能使用的存储器却是有限的。比如8031系列存储器,堆栈只能使用内部的128字节数据存储器,即使系统中有64K字节的外部数据存储器,任务栈的总空间也不能超过128字节。这种处理器使用共用任务栈结构的RTOS就更好一些。
由于共用任务栈系统需要较长的任务切换时间,不适于任务切换频繁的场合,在很多嵌入式系统中,长时间只有几个任务会处于运行状态,其它任务在特定的条件下才会运行。对于RTOS的使用者,也可以适当地划分任务,来减小任务切换的时间。
无论使用哪种方法,在存储空间有限时,任务栈的长度应仔细计算。计算的根据是任务中的函数嵌套数、函数局部变量长度。对于共用任务栈,还要考虑同时运行态和挂起态的最大任务数。一些编译器可以生成堆栈溢出检查代码,在调试时可将该编译开关打开,以测试需要的实际堆栈长度。
这种任务栈处理方法的缺点是可能造成空间的浪费。因为一个任务如果堆栈满了,该任务也就无法运行,即使其它任务的堆栈还有空间可用。当然,这种方法的好处是任务栈切换的时间非常短,只需要几条指令。
4 共用空间的堆栈处理方法
(1)栈共用连续存储空间
如果多个任务使用同一段连续空间作为堆栈,这样各个堆栈之间就可以互补使用。在前面说过,共用空间的问题在于一个任务运行时不能破坏其它任务的堆栈数据。为简单起见,先看图3所示两个任务的情况。
假定任务1首次运行时任务栈为空。运行一段时间后任务2运行,堆栈空间继续往上生长。这次任务切换不需要修改CPU的SP数值,但需要记下任务1的栈顶位置SP1(图3中)。
在任务2运行一段时间后,RTOS又切换到任务1运行。在切换时,不能简单地将SP指针修改回SP1的数值,因为这样堆栈向上生长时会破坏任务2堆栈中的数据。办法是将原来任1务堆栈保存的数据移动到靠栈顶的位置,而将任务2堆栈数据下移到靠栈底的位置,堆栈指针SP实际上不需要修改(图3右)。
考虑到更为一般的情况,有N个任务,当前运行的任务为k,下一个运行的任务为j,在共用任务堆栈时必须做的工作有:
*为每个任务定义栈顶和栈底2个堆栈指针;
*在任务切换时,将待运行任务j的堆栈内容移动到靠栈顶位置,同时将其堆栈上方的任务堆栈下移,修改被移动推栈的任务堆栈指针。
假设我们定义的任务栈空间和任务的栈指针变量为:
void TaskSTK[MAX_STK_LEN];/*任务堆栈空间*/
typedef struct TaskSTKPoint{
int TaskID;
int pTopSTK;
int pBottomSTK;
}TASK_STK_POINT;
TASK_STK_POINT pTaskSTK[MAX_TASK_NUM]; /*存放每个任务的栈顶和栈底指针*/
任务栈指针数组pTaskSTK的元素个数同任务个数。为了堆栈交换,需要另外一块临时存储空间,其大小可按单个任务栈最大长度定义,用于中转堆栈交换的内容。堆栈内容交换的伪C算法可写为:
StkEechange(int CurTaskID,int RunTaskID)
{ /*2个参数为当前运行任务号和下一运行任务号*/
void TempSTK[MAX_PER_STK_LEN]; /*注意该变量长度可小于TaskSTK*/
L=任务RunTaskTD的堆栈长度;
①将TaskSTK顶部的L字节移动到TempSTK中;
②将RunTaskID任务的堆栈内容移动到TaskSTK顶部;
③将RunTaskID堆栈上方(移动前位置)所有内容下移L个字节;
④修改RunTask堆栈上方(移动前位置)所有任务栈顶和栈底指针(pTaskSTK变量);
该算法的平均时间复杂度可计算如下:
O(T)=SL/2+SL/2+SL×N/2
式中,第一、二项为步骤①和步骤②时间,第三项为步骤③时间;SL表示每个任堆栈的最大长度(即MAX_PER_STK_LEN),N表示任务数。
取SL为64字节,任务数为16个,则数据项平均移动次数为576。假设每次移动指令时间为2μs,则一次任务栈移动时间长达约1ms。所以在使用该方法时,为了执行时间尽量短,编码时应仔细推敲。
从空间上说,共用任务栈比独立任务栈优越。假设独立任务栈方法中每个堆栈空间为K,任务数为N,则独立任务栈方式的堆栈总空间为N×K。在共用任务栈时,考虑各任务互补的情况,TaskSTK变量不需要定义为N×K长度,可能定义为二分之一或者更小就可以了。
另外,这种方法不需要在任务切换时修改CPU的SP指针。
(2)工作栈和任务堆栈
上节共用任务栈算法的缺点是:任务切换时的堆栈内容交换算法复杂,占用时间长。另外一个折中的方法是设计一个工作堆栈,用于给当前运行的任务使用;在任务切换时,将工作栈内容换出得另外的存储空间,该空间可以动态申请,其大小按实际需要即可。
这种方法看起来和独立任务栈的方法类似,需要N+1块存储空间,其中一块用于工作栈空间。和独立任务堆栈相比,其区别有2点:
①SP指针所指向的空间始终是同一块存储空间,即工作栈;
②每个任务栈的大小不需要按最大空间定义,可以动态按实际大小从内存中分配空间。
对于8031这种处理器结构,由于堆栈指针只能指向其内部存储器,大小十分有限。采取这种方法,可将工作栈设在内部RAM,将任务栈设在外部RAM,扩展了堆栈空间。
和上一种共用堆栈方法相比,这种方法的交换时间要短,其时间复杂度约为1.5倍最大任务栈长度。
独立任务栈的方法适合于存储器充足、任务切换频繁、对任务切换时间要求较高的场合,一般主要用在16位或者32位微处理器平台环境。值得注意的是,在某些微处理器中,虽然可使用的数据存储器可以设计得较大,但堆栈所能使用的存储器却是有限的。比如8031系列存储器,堆栈只能使用内部的128字节数据存储器,即使系统中有64K字节的外部数据存储器,任务栈的总空间也不能超过128字节。这种处理器使用共用任务栈结构的RTOS就更好一些。
由于共用任务栈系统需要较长的任务切换时间,不适于任务切换频繁的场合,在很多嵌入式系统中,长时间只有几个任务会处于运行状态,其它任务在特定的条件下才会运行。对于RTOS的使用者,也可以适当地划分任务,来减小任务切换的时间。
无论使用哪种方法,在存储空间有限时,任务栈的长度应仔细计算。计算的根据是任务中的函数嵌套数、函数局部变量长度。对于共用任务栈,还要考虑同时运行态和挂起态的最大任务数。一些编译器可以生成堆栈溢出检查代码,在调试时可将该编译开关打开,以测试需要的实际堆栈长度。
Powered byucosii任务堆栈的作用 - AI_JJ - 博客园
第一,当任务运行时,它用来保存一些局部变量;
第二,当任务挂起时,它负责保存任务的运行现场,也就是CPU寄存器的值。
堆栈作用的就是用来保存 局部变量,从本质上讲也就是将CPU寄存器的值保存到RAM中。在uCOS中,每一个任务都有一个独立的任务堆栈。为了深入理解任务堆栈的作用,不妨分析 任务从&出生&到&消亡&的整个过程,具体就是分析任务的建立,运行,挂起几种状态中任务堆栈的变化情况。
现在假设系统运行着一个 由用户创建的用以完成打印工作的任务TPrint。TPrint最初通过OSTaskCreate()函数创建,在该函数中与任务堆栈有关的第一段代码是 大家比较熟悉的函数OSTaskStkInit(),这个函数是在uCOS移植过程中必须实现的,其作用是&初始化堆栈&,其实就是预先在RAM中的一块 区域中把任务将来运行开始时CPU寄存器应处的状态(正确值)准备好,之后,任务第一次被内核调度器调度运行时,将这些准备好的数据(寄存器的值)推到 CPU的寄存器中,如果数据设计的合理,CPU便会按照我们预先设计好的思路运行。所以,&初始化堆栈&实际上是做了一个&未雨绸缪&的工作。这个过程中 有两点是必须慎重考虑的,一是PC该如何定位,二是CPU的其它寄存器(除PC之外)该怎么处理。先说第一点,因为任务是第一次运行,而任务从本质上将就 是一段代码,所以PC指针应该定位到这段代码的第一行处,即所谓的入口地址(Entry Point)处,这个地址由任务指针保存着,所以把该指针值赋给PC即可。第二,这段代码还未被执行过,所以代码中的变量与CPU的其它寄存器一点关系也 没有,因此R0-R12,R14可随便给值,或者不赋值也可,让这些寄存器保持原来的值,显然后者更为简单。最后再给CPSR赋值,用户可以根据实际需要 使系统运行于系统模式或管理模式。经过入栈和出栈,此时SP指向任务堆栈的最底端(就是已经定义好的任务堆栈数组的最后一个元素)。
之后任务代码开始正式运 行,因为CPU的寄存器是有限的,所以在运行时不可避免地要把一些临时变量暂时保存到堆栈中。具体应保存到哪个地址呢,不用担心,SP知道(任务第一次运 行时,这个地址就是任务堆栈数组的最后一个元素的地址)。任务堆栈的大小和任务代码中临时变量的数目有关,如果这段代码临变量特别多,堆栈就应设计的大一 些。
然后,TPrint任务 由于某种原因将要被挂起,所以应把任务的运行现场放到堆栈里保护起来,TPrint任务再次运行时再把这个现场还原,任务就能从上次断点处紧接着运行。那 么,这个现场是什么呢?从本质上讲,TPrint任务的运行过程就是CPU在执行一段特定的代码,所以这个现场就是CPU的现场,也就是寄存器的值。这些 寄存器的值包含了代码执行时的所有信息,包括当前运行到了这段代码的哪个位置处(由PC值指明)。因此,把CPU的寄存器的值推入堆栈,然后记住栈顶指针 的位置(SP由OSTCBCur-&OSTCBStkPtr保存),当任务再次将要运行前,从SP指向的地址处依次把先前保存的CPU寄存器的值放 到CPU的寄存器中,任务就可以从上次中断的地方准确无误地执行。这个过程就像突然把任务冻结了,与任务有关的任何东西都不能动了,一段时间之后又把任务 解冻,与它有关的东西又变得可用,于是任务又可以活蹦乱跳地跑起来了。
从以上分析可以看出,任务堆栈至始至终伴随着任务,与之生死与共,它的作用可以概括为两点:第一,当任务运行时,它用来保存一些局部变量;第二,当任务挂起时,它负责保存任务的运行现场,也就是CPU寄存器的值。有 些朋友正是忽视了第一点,产生了&任务堆栈大小应是固定值的疑问&。我感觉,这可能与对函数OSTaskStkInit()的理解有关,我们都称之为堆栈 初始化函数,但此处的&初始化&与我们理解的初始化不太一样,平时讲的(变量的)初始化似乎指的是将变量的所有成员都一一初始化。而此处的堆栈的初始化仅 仅是初始化了很大一个堆栈的一小部分,因为当前只有这部分是有用的,而剩余的大部分用不到,所以不用初始化,就像有些变量不用初始化一样(有默认值或随机 值)。更深入一点考虑,当任务挂起时,任务堆栈中保存任务挂起前CPU寄存器的这一连续的区域肯定在整个堆栈的最上面;当任务重新开始运行时,SP弹出寄 存器的值,这段区域变成空白的区域。而且,任务每次挂起前用来保存当前CPU寄存器这一连续区域在整个任务堆栈空间中是浮动的
文章 - 337UCOS II 任务堆栈里面的临时变量位置_EEWorld电子工程世界搜索中心
搜索范围:
一周以内&&&&
搜索到约