FreeRTOS学习(5)——内存映射
文章目录前言.map文件标准库工程的 SRAM 内存布局全局和静态变量区域.bss段.data段一个小思考/扩展/建议堆区域HEAP/C库堆栈区域STACK/主栈引入 FreeRTOS 后的 SRAM 内存布局.data段C库堆和主栈空间.bss段包含FreeRTOS内核堆FreeRTOS内核堆的大小FreeRTOS内核堆的作用FreeRTOS内核堆细节杂谈为什么叫这个名字SP、PSP、MSP任务栈的栈基地址和栈顶地址前言在前面的内容中我们已经明确了一点CPU 在进行上下文切换时需要将寄存器中的执行状态保存到 SRAM 中。而在 FreeRTOS 系统中系统往往同时存在多个任务。为了保证每个任务在被切换回来时都能够继续从原来的位置正确执行这些上下文信息显然不能混在一起保存。因此FreeRTOS 为每一个任务在 SRAM 中划分出一块独立且专属的内存区域用于保存该任务的运行状态以及相关的工作数据。那么这块属于“任务”的内存区域具体包含哪些内容又是如何组织和管理的这正是本文将要重点讲解的内容。.map文件在Keil5的STM32开发工程中所有代码和数据在编译链接完成之后都会被精确地放置到具体的内存地址上。而这些“最终的内存分布结果”都被完整的记录在一个文件当中链接映射文件.map 文件在该文件中详细列出了 SRAM 的实际分段情况包括 .bss段、堆Heap、栈Stack。如果系统中使用了FreeRTOS那么也会列出任务相关结构在内存中的占用位置和大小。在Keil5工程中编译链接成功后你可以在工程根目录\Listings目录下找到该.map文件。通过阅读该文件我们可以直观地看到STM32 的 SRAM 是如何划分和使用的。当然FreeRTOS 中任务的专属内存布局同样可以借助 .map 文件进行分析和验证。标准库工程的 SRAM 内存布局找一个标准库模板工程然后将以下代码直接复制替换到mian.c文件中#includestm32f10x.hinta10;staticintb;intmain(void){// 使用一下a和b避免不使用被链接器优化丢弃b100;ab100;while(1){}}编译链接完成后找到.map文件。打开此文件后一直往下面翻就可以找到在不集成FreeRTOS的情况下一个经典的STM32裸机应用的SRAM内存布局。文字内容如下所示根据这张段文字描述我们可以画出以下SRAM内存分段布局其中.bss段和.data段共同组成了SRAM中的全局和静态变量区域。下面来逐一讲解这些区域。全局和静态变量区域.bss段和.data段共同组成了SRAM中的全局和静态变量区域。这两个段在命名时比较显著的特点就是名字的前面都存在一个.点这个点有用吗当然是有用的这个点表示这两个段都属于链接器管理的内存区域。链接器会在链接阶段自动根据工程中使用全局和静态变量的情况自动分配这两个段的大小。如果使用的全局和静态变量多就会分配大一些反之则会分配小一些。在之前讲解进程的虚拟内存空间时我们讲过数据段用于存放全局和静态数据。实际上数据段也可以进一步分段也存在.bss段和.data段。那么.bss段和.data段的区别是什么呢它们是有明显区别的。.bss段.bss段用于存放未手动初始化的全局和静态变量。比如上述代码中的staticintb;它的内存空间就分配在.bss段内。.bss段会在系统启动时自动清零这也是未手动初始化的全局和静态变量仍然具有初始化零值的原因。.data段.data 段用于存放 已手动初始化的全局变量和静态变量。比如上述代码中的int a 10;变量 a 是一个已初始化的全局变量因此它的运行时内存空间会被分配在 .data 段中。手动初始化的全局变量和静态变量具有一个显著特点是在程序启动时它们就应当被初始化为初始值。比如上面的a变量它的初始化值是10这就意味着在系统启动时必须将初始值赋值给存储在.data段中的全局和静态变量。那么问题来了系统启动的时候去哪里找这个初始值呢如果这个初始值不存储起来找得到它吗显然手动初始化的全局和静态变量其初始值需要找一个位置存储起来。存在哪里呢实际上存在了单片机的Flash闪存中。因为Flash闪存主要的作用就两个存放程序的代码指令存放只读常量比如手动初始化的全局和静态变量的初始值。因此对于存放在 .data 段中的变量系统在启动阶段会执行如下操作将 Flash 中保存的初始值拷贝到 SRAM 中对应的 .data 段内存区域。这一步操作通常由启动代码在进入 main() 函数之前自动完成。讲到这里你就会发现一个手动初始化的全局或静态变量它其实占用了两片存储空间常量初始值存放在 Flash 中自身内存空间在SRAM的.data段中一个小思考/扩展/建议请查看下面的代码// num和num2都是全局变量intnums[1000];intnums2[1000]{0};这两句代码从执行的效果来看没有任何区别。都得到了一个长度为1000全部都是默认零值的int数组。在传统的通用计算机系统编程中这两种写法也不会带来明显的差异。但在 STM32 等嵌入式系统中这两种写法在内存和存储资源的消耗上却存在本质区别。其中下面这种写法显然是不推荐的。nums全局变量会存放在.bss段中且只占用SRAM内存空间不会占用Flash空间。但nums2全局变量会存放在.data段中同时占用SRAM和Flash存储空间导致Flash存储空间被浪费。启动阶段需要进行无意义的0值拷贝增加了不必要的时间开销。基于这一点在嵌入式工程中有一个非常实用的经验性建议对于体积较大、且初始值为 0 或不关心初始值的全局或静态变量应尽量避免手动初始化最好让其进入 .bss 段自动完成初始化。总之能进.bss段就不要进.data段。但是在上面的例子中很有可能都会存入.bss段因为编译器会进行优化尽量不占用flash的空间如果想要尝试实验结果可以在数组靠后的位置赋值手动初始化几个非0值堆区域HEAP/C库堆SRAM中的堆和我们以往认知中的堆作用都是一致的。堆HEAP是一块 用于手动动态内存分配的 内存区域。堆通常在.bss之后高地址随着使用堆空间会从低地址向高地址“生长”。默认情况下堆的大小是512字节。堆空间的大小是可以手动调节的打开启动文件在汇编代码的最上面。如下图所示对于STM32中堆的使用我们还是和之前保持一样的态度堆尽量不要使用即便集成FreeRTOS此观点依然保持不变在 STM32 中一旦使用堆如果出现问题将会非常隐蔽STM32中没人会告诉你到底怎么回事甚至很多关于堆的问题一旦出现难以复现更不用说去进行调试解决。总之使用堆收益不明显风险却很高。并且在绝大多数 STM32 应用场景中也很少会遇到非用堆不可的情况。因此在实际编程中如果遇到需要申请内存并在多个地方共享使用的场景。应优先考虑使用全局变量或静态变量而不是依赖堆内存。这里讲到的堆其作用非常明确它主要用于配合 malloc、calloc、free 等 C 语言标准库函数完成动态内存的申请、使用与释放。为了避免与后续将要讲到的FreeRTOS 内核堆产生混淆同时也为了更明确的体现它的作用我们将这个堆称之为“C库堆”。栈区域STACK/主栈在裸机开发的前提下SRAM中的栈主要用于函数调用时保存局部变量函数嵌套调用时保存返回地址以便函数调用结束后返回调用点。在裸机开发的前提下栈还用于在中断响应过程中保存上下文信息以及保存中断处理函数执行过程中产生的相关函数调用数据。除此之外STM32中的栈和以往我们使用过的通用计算机编程中的栈没有什么区别先进后出后进先出生命周期自动管理自动创建自动销毁性能高效率快从高地址向低地址生长正因为如此栈是嵌入式系统中最安全、也是最可靠的一种内存使用方式。当然在通用计算机中编程时栈也具备这种特点。注意在裸机开发的前提下整个系统只有一个CPU只能一条执行路径永远执行下去。在裸机开发的前提下时整个系统只有一个栈也就是我们前面讲过的主栈。在 STM32 中主栈由 MSPMain Stack Pointer 寄存器进行管理该寄存器中存储的是当前主栈的栈顶地址。注意事项主栈的大小也是可以手动调整的如下图所示裸机开发的前提下可以根据实际需求适当将栈设置得大一些。毕竟在不使用 RTOS 的情况下SRAM 的使用相对宽松栈空间过小反而更容易埋下隐患。需要特别强调的是栈是我们日常开发中使用频率最高的一块内存区域同时也是最容易被忽视的一块内存区域。在使用栈时必须时刻警惕栈溢出问题。例如在main函数中写下如下代码char buf[1024 * 30];这句代码写下后编译链接烧录一气呵成没有任何问题。真的没有问题吗显然是有的buf数组占的空间太大了别说栈溢出了整个SRAM都装不下它。但是我们没有看到任何警告更没有报错也没有任何提示。甚至原本工程的点灯代码依然执行成功了LED依然被点亮了。这说明什么呢这说明在 STM32 编程中一旦发生内存空间使用越界或溢出系统不会告诉你哪里错了程序的行为也将变得完全不可预测可能“现在看起来没事”但地雷已经埋下什么时候爆炸全凭运气。因此在使用任何一类内存空间时都必须对其空间占用情况有清晰认知合理规划资源。尤其是栈因为栈用的是最多的也是最容易产生栈溢出的。一旦发生栈溢出程序的行为是完全未知的测试阶段可能毫无异常上线后却给你来一次“大的”。如果出现这种情况那就只能自求多福了~引入 FreeRTOS 后的 SRAM 内存布局下面我们以前面已经创建并使用过的一个 FreeRTOS 工程为例进行说明。该工程中包含两个任务用于演示多任务运行场景。代码如下#includestm32f10x.h// STM32F10x 标准外设库头文件#includeFreeRTOS.h// FreeRTOS 核心头文件#includetask.h// FreeRTOS 任务相关 API// 任务1voidtask1(void*arg){while(1){vTaskDelay(1);}}// 任务2voidtask2(void*arg){while(1){vTaskDelay(1);}}intmain(void){// 1. 创建任务1xTaskCreate(task1,Task1,configMINIMAL_STACK_SIZE,NULL,tskIDLE_PRIORITY1,NULL);// 2. 创建任务2与任务1优先级相同xTaskCreate(task2,Task2,configMINIMAL_STACK_SIZE,NULL,tskIDLE_PRIORITY1,NULL);// 3. 启动 FreeRTOS 调度器vTaskStartScheduler();// 理论上不会运行到这里while(1){}}注意最好保持代码一致否则查看map文件的内存布局可能会不同。在完成工程的编译与链接之后进入工程目录下的 Listing 文件夹找到链接器生成的 .map 文件。往最下面翻就可以看到如下图所示的SRAM文字内容布局根据这段文字描述我们可以画出如下图所示的SRAM内存分段布局集成FreeRTOS后内存布局就要复杂一些我们逐一分段进行讲解。仍然从低地址向高地址进行讲解。备注某个任务的TCB与任务栈在内存中实际上并不是“连续的一整块”。或者说FreeRTOS不会保证某一个任务的TCB和任务栈在内存空间使用上必然连续。你可以这么理解FreeRTOS在创建任务时一次malloc用于分配任务栈的内存区域另一次malloc用于分配TCB的内存区域。既然是两次不同的malloc内存分配那么大概率来说这两片空间是不连续的。但是从逻辑上而言某个任务的TCB和任务栈同属于一个任务将它们画在一起便于理解也并没有错误之处。另外对于任务栈要清楚引入FreeROTS之后是没有任务栈的任务栈是创建任务时创建的仅供创建它的任务使用。但是任务栈并不在TCB内。可以概括为任务栈时该任务的私有栈有多少任务就有多少私有栈。.data段.data段用于存储已手动初始化的全局变量或静态变量。需要注意的是我们自己的代码中是没有这种变量的说明这些已初始化的全局或静态变量来自 FreeRTOS 的源码。.data段中的数据在编译阶段会将“常量初始化值”存放在 Flash 闪存中。当系统上电启动时启动代码会负责将这些初始化值从 Flash 拷贝到 SRAM 中对应的 .data 段区域。从而保证程序一开始运行时这些变量就已经具备正确的初始值。下面我们先看一下图中的堆和栈.bss段我们放到最后再讲。C库堆和主栈空间关于 C 语言库堆这里不再赘述。总的来说在嵌入式系统中并不推荐使用C库堆而且在大多数场景下也并不存在“非用不可”的理由。相比之下主栈空间MSP仍然是一个非常重要且高频使用的内存区域。在系统启动阶段以及在进入 FreeRTOS 调度之前程序仍然使用 主栈MSP 来完成函数调用。例如在main函数执行之前单片机启动阶段的一系列函数调用如系统初始化等都是在主栈中完成的。在调用vTaskStartScheduler()启动调度器之前main函数本身的执行过程也都是在主栈上完成的。因此在 FreeRTOS 调度器启动之前主程序的执行流程仍然完全依赖主栈完成。当 FreeRTOS 任务调度器启动之后主程序通常不会再继续执行此时 主栈的主要作用就转变为处理中断相关的工作。在FreeRTOS运行过程中只要产生中断其ISR的执行过程 一定是在主栈MSP上完成的这是 Cortex-M3 内核架构所规定的行为。但是当任务运行过程中发生中断时任务的上下文信息是保存在任务栈中的而不是主栈中。举一个例子假设 任务1正在执行此时发生了中断。Cortex-M3 内核会首先将任务1的上下文信息 压入当前使用的栈即任务1的任务栈。随后处理器切换到 主栈MSP并在主栈上执行中断服务函数。当中断处理完成后再根据任务栈中保存的上下文信息恢复任务1的执行现场继续执行任务1。总的来说即使系统引入了 FreeRTOS主栈仍然具有非常重要的作用。像 中断处理这样的内核级功能仍然必须依赖主栈才能完成。因此在工程实践中仍然需要 为主栈预留足够的空间以避免在中断嵌套或复杂中断处理场景下出现 栈溢出问题。当然在大多数工程中默认约 1KB 左右的主栈空间通常已经足够用于中断处理。如果系统中为主栈预留了 1KB 的空间但在运行过程中仍然出现主栈不足的情况那么往往说明 中断服务程序中执行了过于复杂或重量级的操作。这就是设计问题应该优化设计问题而不是继续扩大主栈空间。.bss段包含FreeRTOS内核堆可以看到在这张图中整个 .bss 段所占的空间非常大。可以看到在这张图中整个 .bss 段所占的空间非常大。.bss 段用于存储未初始化的全局变量或静态变量。在 FreeRTOS 以及 SPL 库的源码中都会存在一些未初始化的全局或静态变量这些变量我们就不关注了。真正需要我们关注的是在整个 .bss 段中占据绝大部分空间的是FreeRTOS内核堆。接下来我们就要把注意力从 .bss 段整体聚焦到其中这一块最核心的区域 —— FreeRTOS 内核堆。FreeRTOS内核堆的大小首先我们来看一下整个 FreeRTOS 内核堆的大小。从图中可以看到整个FreeRTOS内核堆当前的大小是17KB。那么问题就来了这个 17KB 是怎么来的为什么是 17KB 呢整个 FreeRTOS 内核堆又到底是“从哪儿冒出来的”FreeRTOS 内核堆本质上其实就是一个 未初始化的全局或静态变量。所以我们可以尝试去FreeRTOS源码中找到这个变量。还记得在最开始进行 FreeRTOS 移植时我们曾经选择过一个文件叫做 heap_4.c。它的作用就是决定 FreeRTOS 使用哪一种内存管理策略。在这个文件中可以找到下面的一段代码其中宏configAPPLICATION_ALLOCATED_HEAP的默认值就是0于是这一段代码就是定义了一个静态全局变量数组ucHeap这个全局变量数据就是FreeRTOS内核堆这一点在.map文件中也有所体现而这个数组的长度configTOTAL_HEAP_SIZE它定义在FreeRTOS的配置文件中。如下所示也就是说FreeRTOS的内核堆其实就是FreeRTOS源代码中申请创建的一个大小为17KB的静态全局变量字节数组。FreeRTOS内核堆的作用前面我们已经明确了一点FreeRTOS 的内核堆本质上就是一块大小固定的、由 FreeRTOS 源码主动申请创建的静态全局字节数组。那么接下来一个非常关键的问题就来了这块 FreeRTOS 内核堆到底是干什么用的FreeRTOS 的内核堆并不是给用户代码随意 malloc 使用的而是专门为 FreeRTOS 内核自身服务的一块内存区域。在当前这个学习阶段我们可以这么看待FreeRTOS内核堆的作用第一FreeRTOS中每创建一个任务都会在FreeRTOS内核堆上分配分配一块专属于该任务的内存区域。第二每个任务的内存区域内部又可以再细分为两个部分任务栈是任务在运行过程中使用的栈其作用与主栈类似在任务执行过程中如果出现函数嵌套调用那么任务栈中会存储返回地址以便于函数结束后返回调用点。在任务执行过程中任务相关函数的调用栈帧以及函数中的局部变量均存储在该任务自己的任务栈中。当任务发生切换时包括切到任务或中断当前任务的运行上下文会被保存到任务栈中以便后续恢复上下文使用。任务栈的生长方向与主栈一致均为从高地址向低地址生长。任务控制块TCBTask Control Block任务控制块用于描述和管理一个任务其中保存了该任务的各种关键信息。比如任务名任务的状态、任务的优先级、任务栈的栈基地址、任务栈的栈顶地址等信息。FreeRTOS 的调度器正是通过访问和维护这些任务控制块才能够完成任务的切换与调度。总得来说当我们创建一个任务时FreeRTOS 实际上是在内核堆中一次性为这个任务“划出”了一整块专属空间。这块空间内部同时包含了任务栈和TCB任务控制块。除此之外我们在前面讲过CPU的内核寄存器如下图所示其中关于栈顶指针SP寄存器其数据来源有两个MSPMain Stack Pointer主栈栈顶指针MSP 用于始终保存主栈的栈顶地址。主栈始终是存在的即便引入FreeRTOSmain函数也仍然是要执行的。裸机开发时SP就等价于MSP因为系统中只存在主栈没有其余栈。PSPProcess Stack Pointer实际上的任务栈栈顶指针PSP 用于记录当前正在执行任务的任务栈栈顶地址。当系统引入 FreeRTOS 后每个任务都会拥有自己独立的任务栈。如果FreeRTOS在多个任务之间切换你会发现PSP存储的栈顶地址就会在多个任务栈栈顶切换。在任务的执行过程中SP等价于PSP因为此时的栈顶是任务栈的栈顶。FreeRTOS内核堆细节杂谈关于FreeRTOS内核堆空间的划分和使用我们把它从整个内存空间中单独拎出来。得到如下图所示的结构下面我们针对这张模型图讲解一些细节上的问题。为什么叫这个名字第一个问题FreeRTOS内核堆为什么叫这个名字首先从实现和存储位置上来说FreeRTOS内核堆其实是SRAM中.bss段也就是存放全局和静态变量的区域。但从使用上来说它更具有堆的特点FreeRTOS内核会在运行过程中按需动态分配内存给任务使用不同任务fFreeRTOS分配彼此独立的内存空间除此之外FreeRTOS内核堆中还分配了每个任务的任务栈所以在使用时它还具备栈的一些特点。所以有些人还会把FreeRTOS内核堆称之为**“FreeRTOS堆栈**”也是有道理的。总之FreeRTOS内核在存储实现上是“全局静态内存池”在使用和行为上它具备堆的特点兼有栈的使用特点。那么把它称之为“FreeRTOS内核堆”甚至叫做“FreeRTOS堆栈”都是可以的。SP、PSP、MSP第二个问题SP、PSP、MSP这三个栈顶指针它们的之间的关系是什么样的呢要理解这三个栈顶指针之间的关系首先需要明确一个前提从硬件层面来说CPU 内部真实存在的栈顶指针寄存器只有两个MSP 和 PSP。SP 并不是一个独立存在的寄存器可以将其理解为“当前 CPU 正在使用的那个栈顶指针”的别名。也就是说当 CPU 使用 主栈 MSP 时SP 实际上等同于 MSP当 CPU 使用 任务栈 PSP 时SP 实际上等同于 PSP。什么时候 CPU 使用主栈MSP主栈会在以下场景中被使用系统复位启动阶段。系统上电复位后CPU 默认使用主栈例如 main 函数的执行过程。所有中断处理过程包括中断函数的调用、以及中断发生时的上下文保存与恢复都必须在主栈MSP中完成。因此可以得出一个结论MSP 是系统级、最基础的栈指针一定会被使用且无法被替代。什么时候 CPU 使用任务栈PSP在引入 FreeRTOS 的场景下系统中存在多个任务每一个任务都拥有自己独立的任务栈。当 CPU 正在运行某个任务时所有任务函数的执行与相关函数调用均在该任务对应的 任务栈 PSP 上完成这整个过程我们可以画一幅图来描述对于 Cortex-M3 架构的 CPU 来说在硬件层面同时提供了 MSP 和 PSP用于支持两种不同用途的栈结构在软件层面FreeRTOS 在 C 语言库堆之外又实现了一套属于自身的 FreeRTOS 内核堆用于管理系统中的任务。这种“双堆栈式“的设计将系统内核相关的操作与任务级别的执行逻辑进行清晰的划分。这也是 FreeRTOS 能够在资源受限的 MCU 上依然实现稳定多任务调度的关键原因之一。‘任务栈的栈基地址和栈顶地址在 FreeRTOS 中每个任务的 TCB 内部都会保存两个与任务栈相关的地址一个指向任务栈的栈基地址一个指向任务栈的栈顶地址那么如下图所示哪里是任务栈的栈基哪里是栈顶呢首先我们要知道的一点是任务栈本质上是一段连续的内存空间。从程序员角度看可以把这段空间视为一个数组。所谓栈基地址是指这段连续内存空间的起始地址如果把任务栈视为一个数组那么栈帧地址也就是数组的首地址。那么栈基地址在哪里就一目了然了栈基地址必然是整个任务栈区域的最低地址处并且在整个任务生命周期内保持不变。那栈顶地址在哪呢栈顶地址也可以叫当前栈顶地址用于指向当前已使用栈区域的最低地址位置。也就是上面讲的PSP、SP所指的位置。任务栈和主栈在使用上是没有区别的都遵循从高地址向低地址生长的规则。因此在任务刚创建、尚未运行时栈顶地址位于任务栈空间的高地址端边界附近。随着函数调用和数据压栈栈顶地址会不断向低地址方向移动。对于上图中的任务栈而言栈基和栈顶的位置如下图所示如果任务当前未上CPU执行任务刚刚创建还未执行时的栈顶地址和栈基地址。如下图所示如果任务已经上CPU执行任务已经开始执行已有函数栈帧入栈后的栈顶地址和栈基地址。如下图所示那么任务TCB存储这两个任务栈指针有什么用呢任务 TCB 中同时保存 栈基地址 和 栈顶指针主要是为了管理任务栈空间以及实现任务切换。其中栈基地址 表示任务栈空间的低地址边界在整个任务生命周期内保持不变用于确定任务栈的范围并用于栈溢出检测**需要开启**。栈顶指针 表示当前任务运行时的栈顶位置在任务切换时用于保存和恢复任务的执行现场。因此可以理解为栈基地址决定任务栈空间的边界而栈顶指针记录当前任务执行到栈中的哪个位置。