你的位置:首页 > 操作系统

[操作系统]RTOS Thread stack and CPU stack registers in Cortex


Background

使用Keil RTX RTOS的项目开发过程中,在加入一些新的代码之后,发现在线程们被创建并被启动之后,程序就跑飞了。

借助Keil的RTOS debug窗口,发现有其中2个线程有stack overflow的现象。

于是开始思考RTOS thread stack size的设置问题。

以前一直就对有了RTOS之后,线程栈和内核栈是个什么情况。Cortex-M3的MSP和PSP该如何使用,这些都不是很清楚。

正好借此机会,好好研究一番。

 

本文基于以下开发环境:

Cortex-M3,Keil MDK 5。

C memory Modle

先上一张图,看看一般意义上的无OS情况下,ARM C语言执行环境下,RAM的布局。明显这张图是基于“向下增长”的栈结构来设计的。

从上图可以看到:

RW空间,位于RAM的最低地址区域。用来存放,已经被初始化了的,可读写的,全局变量的值;(提个问题,到底什么叫全局变量,什么叫全局变量的内容?)

ZI空间,位于RW的上方。用来存放,未被初始化的,可读写的全局变量的值;

Heap空间,位于ZI的上方。c函数malloc和calloc等内存分配函数,会从这块区域里面取一块内存区域,给函数的调用者;

Stack空间,位于RAM的最上方。C函数的自动变量(也叫局部变量),以及函数返回地址,会被保存在这里。

Stack空间从高位向低位增长,Heap从低位向高位增长。所以如果控制不好,会出现Heap和Stack overlap的情况。

可以参考:http://stackoverflow.com/questions/39113658/when-does-malloc-return-null-in-a-bare-metal-environment

那么这个c memory model是怎么形成的呢?

当我们写好了一个c程序,开始build这个程序。

这个程序会被预处理、编译、链接,形成一份image。编译器会分辨出:代码指令,常量,已初始化的可读写全局变量,未初始化的可读写全局变量,自动变量,内存分配函数。

链接脚本(link script)会告诉链接器,在链接的时候这些东西在flash里面放哪里,在RAM里面放哪里。

那么我们关心的栈,是在哪里设置栈顶是在RAM的最高位,而heap在ZI的上面呢?

CPU里面有个SP寄存器,又叫栈指针寄存器,指向程序运行时栈的实时位置。

往栈里面PUSH东西啦,SP寄存器的数值就减小;从栈里面POP东西啦,SP寄存器的数值就增加。

在CPU刚开始复位,开始从复位中断向量执行第一条指令的时候,都是汇编,但我们也得先把CPU初始化、栈指针初始化啊,这样后面的c程序才能开始跑。

一般在这里,我们都是通过汇编指令设置SP寄存器为RAM的最高地址,也就是指定了栈顶在哪里。

而Heap底部,则是链接器自动根据RW/ZI空间大小计算出来的,等于ZI空间的顶部。

 

Keil MDK下Cortex-M3的C Memory Model, without RTOS

那么在Keil MDK下,Cortex-M3的C memory model又是个什么样子呢。

为了说明,先上图。

在不跑RTOS的情况下,这个时候,整个C程序都只会用到main stack,为什么?请去翻翻cortex-m3 技术手册。

当然高级一点,可以自己去设置process stack,然后让用户程序在process stack里面跑,让中断函数再main stack里面跑。

先看没有RTOS,并且代码里面调用了malloc或者calloc这类内存分配函数的情况:

   +--------+  Last Address of RAM    | not  |
| used | +--------+ MSPRAM | main |
| stck | | | +--------+ Heal_limit | ^ | | | | | Heap | +--------+ | ZI | +--------+ | RW | +========+ First Address of RAM

从图上可以看到RAM的顶部有一段空间,是没有被c程序用到的,相当于浪费掉了。

在这段空间的下面才是Main stack,MSP指向了这段空间的最上面。

Heap 在main stack的下面。

startup.s里面设置了heap和main stack的size,Keil toolchain会计算heap的起始地址,main stack的起始地址,防止出现heap和stack overlap的情况。

设置见下图的“Stack Configuration”和“Heap Configuration”,默认都是0x400 = 1k bytes。

 

 

以后c程序,就从heap里面去取memory,往main stack里面存自动变量和函数返回地址。

注意,如果你的c代码里面没有调用malloc这类函数,那么是不会最后生成的image,去看map file,是不会有heap这段空间的。

我们来看看没有RTOS的时候,并且没有调用malloc这类函数,最后image的map file是什么样子的。没错,没有heap,只有stack。

  Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x00000418, Max: 0x00002000, ABSOLUTE)  Base Addr  Size     Type  Attr   Idx  E Section Name    Object  0x20000000  0x00000004  Data  RW      4  .data        main.o  0x20000004  0x00000014  Data  RW     236  .data        system_gd32f1x0.o  0x20000018  0x00000400  Zero  RW     3328  STACK        startup_gd32f1x0.o

那我们再来看看没有RTOS的时候,有调用malloc这类函数,最后image的map file是什么样子的。看到没有,有heap,也有stack。并且size都是在之前的startup.s中设置的。

  Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x00000820, Max: 0x00002000, ABSOLUTE)  Base Addr  Size     Type  Attr   Idx  E Section Name    Object  0x20000000  0x00000004  Data  RW      4  .data        main.o  0x20000004  0x00000014  Data  RW     239  .data        system_gd32f1x0.o  0x20000018  0x00000004  Data  RW     3387  .data        mc_w.l(mvars.o)  0x2000001c  0x00000004  Data  RW     3388  .data        mc_w.l(mvars.o)  0x20000020  0x00000400  Zero  RW     3332  HEAP        startup_gd32f1x0.o  0x20000420  0x00000400  Zero  RW     3331  STACK        startup_gd32f1x0.o

再来看这段调用malloc代码经过Keil编译之后,统计的code大小,data大小。

看到没有RW size = 2080 bytes。而上面显示的所有RAM中被用到的空间大小为0x00000820 = 2080 bytes。

是的,ZI Data包含了stack和heap空间的。所以keil编译出来的结果,就是最后ram占用的空间大小。而这个和c memory model里面讲的是不一样的。

==============================================================================  Total RO Size (Code + RO Data)         1400 (  1.37kB)  Total RW Size (RW Data + ZI Data)       2080 (  2.03kB)  Total ROM Size (Code + RO Data + RW Data)    1432 (  1.40kB)==============================================================================

Keil MDK下Cortex-M3的C Memory Model, with Keil RTX RTOS

在有了RTOS之后,就多了RTOS内核和线程。RTOS内核代码,中断函数,以及启动代码,使用main stack。线程使用process stack。

整个C程序就有2个栈指针寄存器,MSP/PSP。MSP指向main stack的top address,PSP指向线程stack的top address。

于是就有了下面的图。

可以看到heap在main stack的下面,process stack在heap的下面。

process stack其实是RTOS代码里面的一个全局数组。

每创建一个线程,就从数组里面拿一块空间作为这个线程的栈空间。

这个数组的大小,由RTX_Conf_CM.C里面的stack number,以及每个stack的栈大小这两者的乘积决定。

  1. 当从线程陷入到RTOS内核时,就使用MSP;
  2. 当在RTOS内核中,需要从线程1调度掉线程2时,则修改PSP,将其值从线程1的栈顶,修改到线程2的栈顶。
   +--------+  Last Address of RAM    | not  |
| used | +--------+ MSPRAM | main |
| stck | | | +--------+ Heal_limit | ^ | | | | | Heap | +--------+ PSP
| Process|
| Stack |
| |
+--------+ | ZI | +--------+ | RW | +========+ First Address of RAM
我们再来看看有RTOS的时候,map file是什么样子的吧。因为内容比较多,只截取了其中一部分:
Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x000018a8, Max: 0x00002000, ABSOLUTE, COMPRESSED[0x00000084])

    Base Addr Size Type Attr Idx E Section Name Object
  0x2000016c  0x00000001  Data  RW     7640  .data        RTX_CM3.lib(hal_cm.o)  0x2000016d  0x00000003  PAD  0x20000170  0x00000008  Data  RW     7684  .data        RTX_CM3.lib(rt_robin.o)  0x20000178  0x00000004  Data  RW     8029  .data        mc_w.l(stdout.o)  0x2000017c  0x0000003c  Zero  RW     250  .bss        pse_hostcomm.o  0x200001b8  0x000001b0  Zero  RW     484  .bss        pse_pwrmgmt.o  0x20000368  0x00000010  Zero  RW     6324  .bss        thread.o  0x20000378  0x00000010  Zero  RW     6325  .bss        thread.o  0x20000388  0x00000010  Zero  RW     6326  .bss        thread.o  0x20000398  0x00000020  Zero  RW     6401  .bss        rtx_conf_cm.o  0x200003b8  0x00000110  Zero  RW     6402  .bss        rtx_conf_cm.o  0x200004c8  0x00000c90  Zero  RW     6403  .bss        rtx_conf_cm.o  0x20001158  0x00000250  Zero  RW     6404  .bss        rtx_conf_cm.o  0x200013a8  0x00000084  Zero  RW     6405  .bss        rtx_conf_cm.o  0x2000142c  0x00000014  Zero  RW     6406  .bss        rtx_conf_cm.o  0x20001440  0x00000034  Zero  RW     7141  .bss        RTX_CM3.lib(rt_task.o)  0x20001474  0x00000030  Zero  RW     7361  .bss        RTX_CM3.lib(rt_list.o)  0x200014a4  0x00000004  PAD  0x200014a8  0x00000400  Zero  RW     6457  STACK        startup_gd32f1x0.o==============================================================================

这个里面标STACK的,其实是main stack,采用默认值为0x400. 而下面这段则是所有线程栈空间的总和:

 0x200004c8  0x00000c90  Zero  RW     6403  .bss        rtx_conf_cm.o

 

Keil的统计数据如下,其中RW size刚好等于上面所占的RAM空间大小:Size = 0x000018a8 = 6312 bytes。也就是所Keil给出的结果,RW data + ZI data就是最后代码运行时占用的RAM大小,包含了所有的stack以及RW/ZI data空间。
==============================================================================  Total RO Size (Code + RO Data)        38196 ( 37.30kB)  Total RW Size (RW Data + ZI Data)       6312 (  6.16kB)  Total ROM Size (Code + RO Data + RW Data)   38328 ( 37.43kB)==============================================================================