在μc/os-ii内核中,各个不同的任务使用独立的堆栈空间,堆栈的大小按每个任务所需要的最大堆栈深度来定义,这种方法可能会造成堆栈空间浪费。本文叙述如何在rtos中多个任务共用连续存储空间作为任务栈的方法,并详细比较二者的优缺点和适用性。
关于μc/os-ii这个实时内核及其应用已经有很多文章介绍了,对于学习rtos的人来说,这个系统是很好的学习起点。虽然文献的源代码没有行号和函数名交叉索引表等,给源代码阅读造成一些困难(可使用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%26;#215;n/2
式中,第一、二项为步骤①和步骤②时间,第三项为步骤③时间;sl表示每个任堆栈的最大长度(即max_per_stk_len),n表示任务数。
取sl为64字节,任务数为16个,则数据项平均移动次数为576。假设每次移动指令时间为2μs,则一次任务栈移动时间长达约1ms。所以在使用该方法时,为了执行时间尽量短,编码时应仔细推敲。
从空间上说,共用任务栈比独立任务栈优越。假设独立任务栈方法中每个堆栈空间为k,任务数为n,则独立任务栈方式的堆栈总空间为n%26;#215;k。在共用任务栈时,考虑各任务互补的情况,taskstk变量不需要定义为n%26;#215;k长度,可能定义为二分之一或者更小就可以了。
另外,这种方法不需要在任务切换时修改cpu的sp指针。
(2)工作栈和任务堆栈
上节共用任务栈算法的缺点是:任务切换时的堆栈内容交换算法复杂,占用时间长。另外一个折中的方法是设计一个工作堆栈,用于给当前运行的任务使用;在任务切换时,将工作栈内容换出得另外的存储空间,该空间可以动态申请,其大小按实际需要即可。
这种方法看起来和独立任务栈的方法类似,需要n+1块存储空间,其中一块用于工作栈空间。和独立任务堆栈相比,其区别有2点:
①sp指针所指向的空间始终是同一块存储空间,即工作栈;
②每个任务栈的大小不需要按最大空间定义,可以动态按实际大小从内存中分配空间。
对于8031这种处理器结构,由于堆栈指针只能指向其内部存储器,大小十分有限。采取这种方法,可将工作栈设在内部ram,将任务栈设在外部ram,扩展了堆栈空间。
和上一种共用堆栈方法相比,这种方法的交换时间要短,其时间复杂度约为1.5倍最大任务栈长度。
5 总结
独立任务栈的方法适合于存储器充足、任务切换频繁、对任务切换时间要求较高的场合,一般主要用在16位或者32位微处理器平台环境。值得注意的是,在某些微处理器中,虽然可使用的数据存储器可以设计得较大,但堆栈所能使用的存储器却是有限的。比如8031系列存储器,堆栈只能使用内部的128字节数据存储器,即使系统中有64k字节的外部数据存储器,任务栈的总空间也不能超过128字节。这种处理器使用共用任务栈结构的rtos就更好一些。
由于共用任务栈系统需要较长的任务切换时间,不适于任务切换频繁的场合,在很多嵌入式系统中,长时间只有几个任务会处于运行状态,其它任务在特定的条件下才会运行。对于rtos的使用者,也可以适当地划分任务,来减小任务切换的时间。
无论使用哪种方法,在存储空间有限时,任务栈的长度应仔细计算。计算的根据是任务中的函数嵌套数、函数局部变量长度。对于共用任务栈,还要考虑同时运行态和挂起态的最大任务数。一些编译器可以生成堆栈溢出检查代码,在调试时可将该编译开关打开,以测试需要的实际堆栈长度。
虹科智能AR眼镜如何保护眼睛健康?
英创信息技术WINCE工控主板软键盘使用注意事项
pwm调光和dc调光的优缺点 pwm调光真的伤眼吗
艾灸仪语音芯片方案!
华耀电子入选2023年度中国智能电动汽车核心零部件100强榜单
μC/OS-II内核如何在RTOS多个任务中作为任务栈的方法研究
选择合适的电源连接器
C语言的概述
一加6和小米8哪个拍照最好
AI如何让广告变得有创意
USM超声波马达是什么,它的优点及应用的介绍
奇格半导体简介
纳多德正式发布400G QSFP-DD ER4光模块
比特币的未来价值预测
蔡力行亮相2018台北国际电脑展 联发科下一步聚焦AI和5G
高速PCB 设计中终端匹配电阻的放置
多功能集成电路启用化学独立电池充电-Versatile IC
新西兰政府就此信公开警告华为称“不要威胁新西兰政府
2021中国广州国际投资年会南沙专场活动拉开帷幕
为消除安全隐患 江淮汽车召回4248辆iEV5纯电动汽车