1183|14

4224

帖子

233

资源

管理员

【FreeRTOS打卡第二站开启】堆栈—任务切换的关键,关门时间8月17日

 

活动总览:点此查看(含活动鼓励和活动学习总内容)

 

本站打卡开始和截止时间:8月15日-8月17日(3天)

打卡任务:

1、阅读cruelfox干货笔记第二篇:FreeRTOS学习笔记 (2)堆栈——任务切换的关键

2、跟本帖回复思考题: 请自选一个熟悉的8位MCU体系结构(比如C51),和一个32位MCU体系结构(比如ARM Cortex-m0),写出它们在从RTOS任务A切换到任务B时,对任务A和B的堆栈进行了哪些操作。请对比两个体系结构下RTOS堆栈切换的开销。

 

 

FreeRTOS学习笔记 (2)堆栈——任务切换的关键

为了方便大家阅读,将cruelfox“FreeRTOS学习笔记 (2)堆栈——任务切换的关键”复制过来了~

 


  本篇中“堆栈”术语(stack)是指计算机(包括MCU)处理器通过堆栈寄存器(stack pointer register)来存取数据的内存区域。常用的访问方式包括 Push/Pop, 以及根据堆栈指针寄存器的间接寻址访问。

  先复习一下C语言中局部变量是怎么存放的。
  举例,main() 函数调用了 func1(),然后 func1() 又调用了 sub2(),如下图
2.jpg
  当CPU执行到 sub2() 函数里面的时候,main() 以及 func1() 中的局部变量不在作用域内,但是它们的存储都保留着。所以通过参数传递指针的办法,sub2() 是可以访问到 main() 的局部变量的数据的。但是反过来,当 sub2() 函数返回以后,sub2() 的局部变量占用的空间就被撤消了,哪怕是通过用指针返回值传递给 func1(),func1() 获得的地址也是无效的内容。因为一个C语言函数被执行的时候,它会在堆栈上保留出一块空间用来存放自己的局部变量,以及保存重要的寄存器。一般在函数入口就会把当前堆栈指针保存,在返回之前则恢复。
  上面这个调用关系,堆栈的使用情况一般是这样的:
3.jpg
从左到右,是函数调用(嵌套)的过程;从右到左是函数一级级返回的过程。

  此外,中断服务程序(ISR)也是一种函数,稍微特殊一点。当中断发生的时候,CPU会从中断向量表里面去取出中断向量对应的中断服务程序入口地址,然后自动保存关键的寄存器到堆栈上,再跳转到中断服务程序的入口地址去执行。中断服务程序也会在堆栈上保留出一块空间用来存放自己的局部变量,以及保存一些没有被自动保存的寄存器。而主程序以及它调用的子程序是无从知道中断服务程序在什么时候被调用的(仅有使用休眠指令暂停执行是例外)。

  怎样来实现多任务呢?

  如上面,假设一个多任务的要求是,sub2() 想暂时等待一下,但不返回,而让 func1() 继续执行。这又意味着 func1() 认为是 sub2() 已经返回了?这显然不对,因为 sub2() 一旦返回,堆栈指针就改回去 func1() 执行时的状态,sub2() 所保留的堆栈空间被划归成空余的,随时可能被改写(比如 func1() 调用任何子函数,比如发生中断调用),它不可能再继续执行了。

  可以分析出来,要实现多任务,必须让每个任务函数自己占有的堆栈空间在任务期间都是有效的,不能被别的任务覆盖。然后,我们需要一个调度程序来协调堆栈的使用。还有,任务都是平级的,不存在相互之间的调用关系,那就只能是被调度程序调用。改进一下,如果堆栈像下面这样使用:
4.jpg
  我们由一个调度器(Scheduler)来创建了 task1() 并运行,然后又创建了 task2() 并切换到 task2() 执行。任何时候可以切换到 task1() 执行也能再随意切换回 task2().  当一个任务结束时它占据的堆栈空间被划归为空余的。

  先不管理调度器的实现细节,上面这样使用堆栈似乎多任务就可以了。直观上有不完美的地方,就是前面的任务结束后会造成“内存碎片”。如果后面新建的任务申请的堆栈……不是刚刚好……
  且慢,调度器怎样预先知道一个任务函数要使用多少堆栈空间?C语言函数调用可不需要知道子函数使用多少堆栈——随你用,整个都是你的。但是对于多任务,不允许某一个任务使用全部的堆栈呀。
  上面还有问题:左起第三个图,task1() 如果要想调用一个子程序,是不可以的。因为它再修改堆栈指针的话,就破坏了 task2() 的私有数据。若不能调子程序,多任务系统的任务间通信、同步都难以实现了……
  所以,为一个任务所保留的堆栈空间,不能只是任务的函数自身占用的堆栈空间的大小。

  现在来看看 FreeRTOS 是怎样管理任务的堆栈的吧。

  借用Tutorial Guide中的图
6.jpg
  FreeRTOS 在内存中申请了一块空间,用来存放任务的堆栈和存放任务的配置(Task Control Block)。这个空间的使用是 FreeRTOS 自己管理的,任务的创建和销毁对应着这块内存里面的分配和释放(注意是独立于C函数 malloc(), free()管理的内存)。
这就是为什么创建任务的时候一定需要指定堆栈大小的原因——申请多少给多少,不够了创建不了任务;给多少不允许用超了,不管任务里面是怎么嵌套调用函数的。此外 FreeRTOS 还有动态内存分配功能,让任务可以使用堆栈之外的剩余内存。但是预先指定的堆栈大小是重要的,因为单片机资源有限,分配过多了就影响其它任务了。好在单片机运行的任务一般不会太复杂,可以分析或者开发阶段通过测试决定一个堆栈用量。

  我写的第一个 FreeRTOS 的程序,创建了两个任务分别点两个LED。在 main() 中,以及在任务的函数中分别输出各自一个局部变量的地址,可以判断出堆栈分配的位置:
7.jpg
  从串口输出的这个结果可以看到 main() 的局部变量在总的堆栈上,也就是总的程序初始化后堆栈指针所在位置(通常是SRAM末尾)附近。而全局变量在 .data 或者 .bss 段里面,是内存中按顺序安排的。两个任务的堆栈分配在内存的低端,看来是使用 .bss 的位置固定分配的一块内存里面取的。

  再用 arm-none-eabi-objdump 工具对编译生成的 ELF 文件进行查看,可以获知内存的静态分配情况:

 

  1. 20040000 g       .data        00000000 _sdata
  2. 20040000 l     O .data        00000004 uxCriticalNesting
  3. 20040000 l    d  .data        00000000 .data
  4. 20040004 l     O .data        00000004 xFreeBytesRemaining
  5. 20040008 g       .bss        00000000 __bss_start__
  6. 20040008 g       .bss        00000000 _sbss
  7. 20040008 g       .data        00000000 _edata
  8. 20040008 l     O .bss        00000014 xSuspendedTaskList
  9. 20040008 l    d  .bss        00000000 .bss
  10. 2004001c l     O .bss        00000014 xPendingReadyList
  11. 20040030 l     O .bss        00000004 pxDelayedTaskList
  12. 20040034 l     O .bss        00000004 xNextTaskUnblockTime
  13. 20040038 l     O .bss        00000004 xTickCount
  14. 2004003c g     O .bss        00000004 pxCurrentTCB
  15. 20040040 l     O .bss        00000004 uxTopReadyPriority
  16. 20040044 l     O .bss        00000004 pxOverflowDelayedTaskList
  17. 20040048 l     O .bss        00000004 uxCurrentNumberOfTasks
  18. 2004004c l     O .bss        00000064 pxReadyTasksLists
  19. 200400b0 l     O .bss        00000014 xDelayedTaskList1
  20. 200400c4 l     O .bss        00000014 xDelayedTaskList2
  21. 200400d8 l     O .bss        00000014 xTasksWaitingTermination
  22. 200400ec l     O .bss        00000004 xSchedulerRunning
  23. 200400f0 l     O .bss        00000004 uxTaskNumber
  24. 200400f4 l     O .bss        00000004 uxDeletedTasksWaitingCleanUp
  25. 200400f8 l     O .bss        00000004 uxSchedulerSuspended
  26. 200400fc l     O .bss        00000004 xIdleTaskHandle
  27. 20040100 l     O .bss        00000004 xNumOfOverflows
  28. 20040104 l     O .bss        00000004 uxPendedTicks
  29. 20040108 l     O .bss        00000004 xYieldPending
  30. 2004010c l     O .bss        00000001 ucMaxSysCallPriority
  31. 20040110 l     O .bss        00000004 ulMaxPRIGROUPValue
  32. 20040114 l     O .bss        00000008 xStart
  33. 2004011c l     O .bss        00000004 xHeapHasBeenInitialised.5018
  34. 20040120 l     O .bss        00012c00 ucHeap
  35. 20052d20 l     O .bss        00000008 xEnd
  36. 20052d28 l     O .bss        00000004 xTimerQueue
  37. 20052d2c l     O .bss        00000014 xActiveTimerList1
  38. 20052d40 l     O .bss        00000014 xActiveTimerList2
  39. 20052d54 l     O .bss        00000004 pxCurrentTimerList
  40. 20052d58 l     O .bss        00000004 pxOverflowTimerList
  41. 20052d5c l     O .bss        00000004 xTimerTaskHandle
  42. 20052d60 l     O .bss        00000004 xLastTime.5299
  43. 20052d64 g     O .bss        00000010 dummy
  44. 20052d74 g     O .bss        00000040 xQueueRegistry
  45. 20052db4 g       .bss        00000000 __bss_end__
  46. 20052db4 g       .bss        00000000 _ebss
复制代码


  可看到固定分配了 0x12c00 字节给 ucHeap,两个任务的堆栈都在这块空间里面。在我使用的 demo 例子的FreeRTOSConfig.h 头文件中有这么一句
#define configTOTAL_HEAP_SIZE                        ( ( size_t ) ( 75 * 1024 ) )
这个值刚好就是 ucHeap 这个变量占用的内存大小。

  研究一下 FreeRTOS 实现的细节。


  我用的 port 部分是 CORTEX_M3 目录下的(虽然运行在Cortex-m4 CPU上,没用到浮点处理器),在 FreeRTOS 代码 tasks.c 中定义了一个结构来描述 TCB 数据
8.jpg
  根据配置需要,TCB的许多数据域是可选的,这样不需要用的就被条件编译去掉,节省内存。全局变量 pxCurrentTCB 总是指向当前任务的 TCB, 因此可以在调试器中随时查看,例如
9.jpg
  根据任务名的字符串,断定这是 FreeRTOS 系统任务 Timer task 的TCB.
  TCB 的第一个数据域 pxTopOfStack 是一个指针,指向任务的堆栈顶;pxStack 则应该是给任务分配的堆栈的最低地址。

  我用 GDB 跟踪了一下任务的创建过程。在调用 xTaskCreate() 创建任务时,调用了两次 pvPortMalloc() 函数来动态申请内存,一次用于任务的堆栈,另一次用于 TCB. 然后是调用 prvInitializeNewTask() 初始化任务,调用 prvAddNewTaskToReadyList() 将任务添加到 Ready 状态的任务列表当中待执行。
  其中,prvInitialiseNewTask() 调用了与平台有关的 port.c 中的 pxPortInitialiseStack() 来初始化堆栈,看一下:
10.jpg
  比较有意思,这是把一系列寄存器的初始值,包括执行代码的地址,压入任务的堆栈中。当这个任务被执行时,想必将从堆栈中恢复现场。

  继续跟踪代码,在主函数中创建好任务之后,调用 vTaskStartScheduler() 来启动调度器,主函数的使命就完成了。vTaskStartScheduler() 还创建了 Idle task 和 Timer task 两个任务,然后是调用 port.c 中的 xPortStartScheduler() 实现调度。这里就访问了 ARM Cortex 的系统寄存器(跟优先级有关),最后来到 prvPortStartFirstTask() 函数,启动第一个任务。
11.jpg
  这里做了几件简单的事情:
(1) 设置系统的堆栈寄存器 MSP, 是从中断向量表中重新装入了 SP 的初值。这就意味着连同 main() 的局部变量都被销毁了,因为这个函数是不会返回的。
(2) 允许中断
(3) 执行系统调用指令 svc

  svc 指令将触发一个软件中断,使 SVC_Handler 被执行,看一下实际ISR程序是这个
13.jpg


  这里先是从全局变量 pxCurrentTCB 获得当前 TCB 的地址,然后读取TCB第一项数据,也就是任务堆栈的指针。接着从堆栈中弹出 r4 到 r11 这8个通用寄存器的值(和堆栈初始化代码是对应的),再把 PSP 寄存器设成任务堆栈指针(现在 vPortSVCHandler 用的是 MSP 堆栈)。根据堆栈初始化代码,任务堆栈里面的内容依次 pop 的话应该是这些寄存器值:r0, r1, r2, r3, r12, LR, PC, xPSR.

  然后,将 BASEPRI 寄存器写成0 (不限制异常处理的优先级),最后用 bx lr 指令从 vPortSVCHandler 返回。疑问来了:返回不就回到 prvPortStartFirstTask() 函数中 svc 指令的下一条指令那里了吗?
  不,如果对ARM cortex-m的异常处理机制比较了解的话可以明白,LR寄存器在 Exception handler (包括ISR) 状态下其内容并不是存放的返回地址。这也是为什么可以用普通C函数来书写ARM cortex-m的中断服务程序的原因,像其它平台往往要用 interrupt 关键字之类的,告诉编译器使用中断返回指令。在 vPortSVCHandler 的代码中,bx r14 ( LR 就是 R14 的别名)指令前还有一条 orr r14, #0xd 指令,将 LR 寄存器的低 4 位改写成 0xd,就是表示返回到 Thread mode 执行,并且使用 PSP 堆栈寄存器,于是就切换到任务的堆栈了。

 

 

懂否?懂了?

真的懂了吗?来看看能跟帖回复这个问题吧:

请自选一个熟悉的8位MCU体系结构(比如C51),和一个32位MCU体系结构(比如ARM Cortex-m0),写出它们在从RTOS任务A切换到任务B时,对任务A和B的堆栈进行了哪些操作。请对比两个体系结构下RTOS堆栈切换的开销。

此帖出自单片机论坛

扫一扫,关注 EEWORLD 微信订阅号

行业资讯、电子趣闻、技术干货、精彩活动……尽可掌握~


回复

37

帖子

0

资源

一粒金砂(中级)

看了一遍。感觉有点懵。越到源码分析越看不懂了。这期难度高了点

点评

不要着急,看不懂的话找相关单片机CPU部分的文档或书籍来复习一下。  详情 回复 发表于 2020-8-15 12:48

回复

1310

帖子

1

资源

五彩晶圆(初级)

symic 发表于 2020-8-15 12:01 看了一遍。感觉有点懵。越到源码分析越看不懂了。这期难度高了点

不要着急,看不懂的话找相关单片机CPU部分的文档或书籍来复习一下。


回复

5

帖子

0

资源

一粒金砂(初级)

堆栈空间分配,和创建任务需要分配空间大小,这个的学问也有点深啊


回复

1万

帖子

15

资源

版主

本帖最后由 ddllxxrr 于 2020-8-16 08:00 编辑

这块是关键,也就是说操作系统比不用操作系统麻烦就麻烦在这,每个任务给个堆栈TCB,还得先说好数量。

CORTEX-m3每个任务要保存如下:

堆栈在内存的的排列
;xPSR        状态寄存器                        栈顶
;R15        PC
;R14        LR
;R12
;R3
;R2
;R1
;R0              
;R14
;R11
;R10
;R9
;R8
;R7
;R6
;R5
;R4

 

C51要保存的

用户堆栈长度(15),PCL,PCH,PSW,

ACC,B,DPL,DPH,R0,R1,R2,R3,R4,R5,R6,R7

 

可见Cortex-m3要大一些,可是人家空间也大,小51要保存的东东小,可是空间也小。

 

 

感觉这么切来切去地,能安全吗???一但有个中断啥地打断了是不是全军覆没了。奥切换时禁止中断。我还是觉得裸奔的实在。也许我只是个码农。

 

个人签名http://shop34182318.taobao.com/
https://shop436095304.taobao.com/?spm=a230r.7195193.1997079397.37.69fe60dfT705yr

回复

100

帖子

0

资源

一粒金砂(中级)

个人觉得这章应该是RTOS的核心

正常裸奔基本不考虑堆栈问题,只有调用中断的时候会调用到栈的功能,正常使用比较多的也是定义变量,动态内存使用的也比较少

当使用了RTOS就无时不刻的在调用堆栈,TCB基本就是动态获取堆,任务切换就要一直压栈出栈,同时还要考虑到系统中断的堆栈调用

这里RTOS针对各任务的整个堆栈操作,就要是由任务调度器来完成,整个过程大概就是

保存任务1现场,恢复任务2现场,保存任务2现场,恢复任务1现场不断的切换

 

什么是现场呢,就是CPU运行状态寄存器

51与contex-m核其实比较大的区别就是现场的不同,51的资源相较于contex-m的资源要少,cpu调用的寄存器也少,可以理解为现场小,所以在保存现场的数据上,contex-m是会比51要大一些的

 

以上为个人理解,如果理解有误,请大家指正,其实看这章也是挺懵的,查了很多资料,希望大家相互交流共同进步


回复

92

帖子

0

资源

一粒金砂(中级)

堆栈这部分我还要深入了解。 以c51为例, 1. 我们需要知道两个任务占用内存大小,这样才能给任务分配内存。 2. 创建任务堆栈和tcb,初始化。 3. 任务调度,全局句柄获取tcb地址,读取任务1堆栈指针,同时保存任务2堆栈值; 4. 由于任务堆栈是连续的,堆栈先进后出,任务调度过程中,内存任务逐渐释放。


回复

505

帖子

1

资源

一粒金砂(高级)

还是理解不上去。通过阅读文章,个人认为,OS已经在初始化的时候给每个Task分好了栈空间。所以在进行TaskA到TaskB的切换时,由于TaskA并不是运行完了,结束任务,所以TaskA的局部变量及函数调用关系应该还在,因此认为,这里的栈开销应该只是保存CPU的寄存器用。 


回复

27

帖子

0

资源

一粒金砂(初级)

51的程序切换时,程序指针pc,程序状态字psw,通用寄存器组(acc,b,r0-r7,dptr),对arm cortex而言是r0-r16,xpsr 如果任务间有对共同外设的寄存器操作,可能还得保存相关外设寄存器,这个得自己在任务中手动添加了 另外,在中断过程中,51是pc和psw自动压栈,cortex是xpsr和r0-r16自动压栈。 任务切换就是把任务中用到的各种寄存器压栈,用到的局部变量单独保存,不要冲突,不要被覆盖。 好一点的编译器会查出潜在的覆盖,把局部变量定义成static,也可让编译器查出是否覆盖。


回复

27

帖子

0

资源

一粒金砂(初级)

如果task1和task2都调用了同一个函数fun(),为保证task1执行fun()期间,被切换到task2,为保证在task2中执行fun(),时对task1的fun()不产生影响,需要将fun()定义为重入/再入函数,即fun()reentrant


回复

6

帖子

0

资源

一粒金砂(初级)

任务的切换主要是对栈的开销,保存任务时将此任务的寄存器压入这个任务的独立栈中,切换到下一个任务,将此任务从栈中弹出,将任务的寄存器弹入到CPU中


回复

403

帖子

0

资源

一粒金砂(中级)

本帖最后由 我的学号 于 2020-8-18 00:04 编辑

感觉这期难度稍微有些高,ARM 的指令集不常用也就忘得差不多了

直觉上的概念是:无论8 bit  或32 bit 的单片机,放在RTOS 进行任务的切换,实际上做的都是同样的工作:A任务切换到B任务时,把当前A任务相关的变量的值,函数的执行位置等信息压入到A 相关的堆栈中;然后开启B任务生成变量和执行程序;切换到A任务时,同样需要将当前B任务相关的变量的值,函数的执行位置等信息压入到B 相关的堆栈中,然后从A 的堆栈调出之前的信息使用,如此反复;

至于C51 和STM32 对这个操作的差异,由于架构不同记载信息的寄存器会不同,此外可能用不同的机制进行实现。具体有多不同,还需要再查看多些资料。。。。

个人签名君应有语,渺万里层云,千山暮雪,知向谁边?

回复

2642

帖子

1

资源

五彩晶圆(初级)

混一贴:堆栈是每个task的堆栈,独立的。每个task的堆栈地址记在tcb里面,tcb是每个task的tcb,独立的,tcb的链条是rtos管理的,所以切换的时候rtos找tcb找tcb下面的堆栈就找到了压入的所有上下文。

不用深究堆栈在sram里存在哪里,没有意义,只要知道rtos可以找到tcb,tcb可以找到堆栈,堆栈有task的所有上下文,该压该弹切换就好了。

就是这样。

点评

nmg
大虾给小虾发福利了,多谢多谢  详情 回复 发表于 2020-8-18 17:35
个人签名人已离开,无事别找,找也找不到。

回复

4224

帖子

233

资源

管理员

freebsder 发表于 2020-8-18 15:24 混一贴:堆栈是每个task的堆栈,独立的。每个task的堆栈地址记在tcb里面,tcb是每个task的tcb,独立的,tcb ...

大虾给小虾发福利了,多谢多谢


回复

1

帖子

0

资源

一粒金砂(初级)

谢谢分享

回复

4224

帖子

233

资源

管理员

作者cruelfox相关解读:http://bbs.eeworld.com.cn/thread-1141050-1-1.html


回复
您需要登录后才可以回帖 登录 | 注册

关闭
站长推荐上一条 1/5 下一条

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区知春路23号集成电路设计园量子银座1305 电话:(010)82350740 邮编:100191

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号 电信业务审批[2006]字第258号函 京公海网安备110108001534 Copyright © 2005-2020 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表