【嵌入式八股6】内存管理

内存模型:data、bss、heap、stack

在嵌入式系统中,内存的管理和组织对于系统的性能和稳定性至关重要。内存主要分为 Flash 和 RAM 两大部分,它们各自存储着不同类型的数据,并且系统中还存在着代码区、全局区、堆区和栈区这四个重要的内存区域。

1、Flash 和 RAM 的数据存储

  1. Flash:Flash 存储器用于存储程序代码和一些只读数据,其存储内容可以表示为:Flash = Code + RO-data + RW-data。
    • Code:即编译后的机器码,是程序执行的指令集合。
    • RO-data:Read Only data,只读数据,例如常量等。
    • RW-data:Read Write data,可读写数据,这些数据在程序启动时需要从 Flash 搬运到 RAM 中。
  2. RAM:随机存取存储器,用于程序运行时的数据存储,其存储内容为:RAM = RW-data + ZI-data。
    • RW-data:已初始化的可读写数据,如静态变量和全局变量中已初始化的部分。
    • ZI-data:Zero Initialized data,未初始化的全局变量和静态变量,在系统启动时会自动初始化为 0。

2、内存四区

内存被划分为四个主要区域,分别是代码区、全局区、堆区和栈区,以下是各区域的详细介绍:

地址范围 区域名称 区域内容 存放位置 举例说明
低地址
(示例:0x0000 开始)
.text 代码段 编译后的机器码,包含程序的指令和操作逻辑 Flash #define ro_def 0x11111111UL(虽然宏定义在预处理阶段处理,但类似常量相关的逻辑与代码段相关),这里的代码段主要是实际执行的机器指令,宏定义参与指令的生成等
.ROdata 只读数据段 只读常量,如 const 修饰的常量,这些数据在程序运行过程中不会被修改 Flash const uint32_t ro_var = 0x22222222;,该变量的值在程序运行期间保持不变
.RWdata 已初始化数据段 静态变量、全局变量中已初始化的部分,在系统启动时,这些数据会从 Flash 读取并搬运到 RAM 中 RAM int global_var = 123;,这是一个已初始化的全局变量;static int c = 0;,这是一个已初始化的静态变量
.bss 未初始化数据段 全局变量和静态变量中未初始化的部分,在系统启动时,这些变量会被自动初始化为 0 RAM int global_var;,该全局变量未进行初始化,会在启动时被置为 0
.heap 堆区 用于动态内存分配,由程序员手动调用内存分配函数(如 malloc 等)来开辟内存空间,并在使用完毕后手动调用释放函数(如 free 等)来释放内存。堆区的内存增长方向是从低地址向高地址(向下增长) RAM 例如使用 int* ptr = (int*)malloc(sizeof(int)); 动态分配内存,使用完后需 free(ptr); 释放内存
高地址
(示例:0xFFFF 结束)
.stack 栈区 用于存储函数的局部变量、函数参数、返回地址等,由编译器自动进行内存的开辟和释放。栈区的内存增长方向是从高地址向低地址(向上增长) RAM 在函数内部定义的变量,如 void func() { int local_var = 10; }local_var 存储在栈区

3、初始化过程

在嵌入式系统启动时,内存的初始化过程如下:

  1. 数据最初存储在 ROM(通常是 Flash)中,其中包括 RO DATA(只读常量数据)、text(代码段)、RW DATA(已初始化的可读写数据,先存储在 Flash 中)。
  2. 系统上电后,RAM 会加载来自于 ROM 的 RW DATA,将这些已初始化的数据搬运到 RAM 中对应的位置。
  3. 随后,依据启动文件的配置,系统会自动将 ZI DATA(未初始化的全局变量和静态变量)初始化为 0,使得这些变量在程序开始执行前处于确定的初始状态。

数组下标越界问题

在 C 语言中,数组下标越界是一个常见且需要注意的问题。以下面的代码为例:

int arr[5];

arr[-1];  // 可能可以正常执行
arr[5];  // 一定报错

对于上述代码,数组 arr 被定义为可以容纳 5 个 int 类型元素的数组,其合法的下标范围是从 04

当访问 arr[-1] 时,之所以说可能可以正常执行,是因为函数栈的增长方向为从高地址到低地址。在栈的内存布局中,高地址处存放着函数的返回信息以及比数组先存入的其他信息,并且数组的存储顺序是下标小的元素在低地址。因此,当往低地址越界(如 arr[-1])时,修改的可能是空的未使用的栈空间,所以在某些情况下可能不会立即引发错误或异常。

而访问 arr[5] 时一定会报错,这是因为 arr[5] 已经超出了数组 arr 所分配的内存空间,它会访问到不属于该数组的内存区域,这很可能会导致程序崩溃或产生不可预期的结果。

为了避免数组下标越界问题,可以利用 assert 断言和迭代器来进行检查和处理。assert 可以在程序运行时检查条件是否满足,如果不满足则会触发程序中断并输出错误信息,帮助开发者快速定位问题;而迭代器则可以在遍历数组时,更安全地控制访问的范围,避免越界情况的发生。

MCU 采用 XIP(eXecute In Place)方式运行程序

MCU(微控制单元)常常采用 XIP(eXecute In Place)的方式在 Flash 中直接运行程序,而不是将程序搬运到 RAM 中。这种方式具有以下显著优势:

  1. 节省内存空间:MCU 通常内存容量有限,尤其是 RAM 的容量相对较小。采用 XIP 方式运行程序,可以避免将程序完整地复制到 RAM 中,从而节省了宝贵的 RAM 空间。这样,就可以将 RAM 资源用于存储那些需要快速存取的数据,比如程序运行过程中的临时变量、缓存数据等,提高了系统的整体性能。
  2. 成本优势:从成本角度来看,RAM 的价格往往比 Flash 更高。在 MCU 系统中,Flash 通常是固化在芯片内部的,而 RAM 则可能需要额外的外部芯片或部件来支持,这不仅增加了系统的成本,还增加了系统的复杂性。因此,将程序直接运行在 Flash 中,可以有效降低系统成本,使产品在市场上更具竞争力。
  3. 提高读取速度:Flash 存储器一般具有较快的访问速度,对于微控制器而言,其访问速度在执行程序时往往已经足够快。在 XIP 模式下,程序无需从 Flash 复制到 RAM,避免了复制过程中所耗费的时间,能够直接在 Flash 中运行,从而加快了程序的启动时间和响应速度,提升了用户体验。
  4. 适用于嵌入式系统:MCU 常常被嵌入在一些资源受限、对功耗要求较低的嵌入式系统中。使用 XIP 方式可以减少对外部 RAM 的需求,降低系统的整体功耗。同时,由于减少了数据搬运等操作,也提高了系统整体的稳定性和可靠性,更适合嵌入式系统的应用场景。

然而,尽管 XIP 具有诸多优势,但它也存在一些限制和需要考虑的因素。例如,Flash 的访问延迟相对较高,在一些对实时性要求极高的场景中可能不太适用;而且 Flash 不适用于频繁写操作的场景,因为 Flash 的擦写次数是有限的,频繁的写操作可能会缩短其使用寿命。因此,在设计 MCU 系统时,需要综合考虑具体的应用场景和需求,选择合适的存储方案。

Linux 栈的大小相关问题

在 Linux 系统中,栈的大小是一个重要的参数,它可以在编译内核时进行配置,并且能够根据系统的实际需求进行灵活调整。栈的大小决定了每个线程所拥有的可用栈空间大小。

在大多数常见的 Linux 系统上,默认的栈大小为 8MB。但需要注意的是,这个默认值并不是固定不变的。用户可以通过修改内核参数或使用特定的命令来改变栈的大小。例如,在某些情况下,如果程序需要更大的栈空间来处理复杂的递归调用或存储大量的局部变量,就可以适当增大栈的大小;反之,如果系统资源有限,也可以减小栈的大小以节省内存。

栈的生长方向问题

栈的生长方向,具体指的是入栈操作时栈空间的扩展方向。当栈从高地址向低地址生长时,我们称之为向下生长,也叫逆向生长。以 STM32 为例,其栈就是向下生长的。

当程序执行过程中需要分配新的栈帧时,栈指针(通常是 SP 寄存器)会向较低的内存地址方向移动,从而为新的栈帧分配足够的空间来存储局部变量、函数参数等信息。而当某个栈帧不再被需要时,比如函数执行完毕,栈指针会向较高的内存地址方向移动,释放该栈帧所占用的内存空间,以便后续的栈操作可以使用这些空间。这种从高地址向低地址生长的方式,是由处理器的架构和操作系统的设计共同决定的,并且在大多数常见的计算机系统中被广泛采用。

操作系统对内存管理的作用

  • 内存分配与回收
  • 采用虚拟内存进行扩容
  • 负责逻辑地址到物理地址的转换
  • 实现内存保护与隔离(应用间、内核隔离)

分页管理

定义:将内存分为大小相等的页框、进程也分为页框,OS将进程的页框一一对应放入内存

在进程控制块PCB中存放页表,记录了进程页号和内存块号之间的对应关系 alt

在进程控制块PCB中存放页表,记录了进程页号和内存块号之间的对应关系

alt

逻辑地址到物理地址的转换

  • 依据逻辑地址,整除页面大小得到页号,余数为页内偏移量
  • 判断越界
  • 通过PCB中保存的页表查询该页存放在哪一块内存(逻辑内存地址)
  • 通过逻辑内存地址计算实际物理内存地址

alt

缺页中断

为了使得页表不用常驻内存,将页表分为2级管理,1级页表存储页表索引,2级页表存储内存逻辑地址

当某些页面不在内存中但被访问到时发生缺页中断

alt

虚拟内存

虚拟内存是现代操作系统中一项重要的内存管理技术,它允许进程使用比实际物理内存更大的内存空间。其核心原理是将即将使用的数据装入内存,当内存空间已满时,将暂时不用的数据换出到磁盘存储(如硬盘上的 swap 区域)。

  1. 突破物理内存限制:虚拟内存使得进程能够运行内存需求超过物理内存大小的程序。这是因为程序运行具有局部性原理,即 CPU 访问内存时存在明显的重复访问倾向。对于那些不常被使用的内存区域,操作系统可以将其换出到物理内存之外,如硬盘上的 swap 区域。当后续需要使用这些数据时,再将其从磁盘换回内存,从而在逻辑上实现了比物理内存更大的内存空间。
  2. 进程内存空间隔离:每个进程都拥有自己独立的页表,页表用于实现虚拟地址到物理地址的映射。由于页表的私有性,进程无法访问其他进程的页表,这就有效地解决了多进程之间的地址冲突问题。每个进程都感觉自己拥有独立的、连续的内存空间,增强了系统的稳定性和安全性。
  3. 内存访问控制与安全:页表中的页表项除了包含物理地址外,还包含一些标记属性的比特位。例如,这些比特位可以用于控制一个页的读写权限,标记该页是否存在于内存中等等。通过这些控制位,操作系统能够对内存访问进行更精细的管理,提供了更好的安全性,防止非法的内存访问操作。

Nor F

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

嵌入式八股/模拟面试拷打 文章被收录于专栏

一些八股模拟拷打Point,万一有点用呢

全部评论
mark一下内存模型
点赞 回复 分享
发布于 03-06 21:43 山东
mark一下内存模型
点赞 回复 分享
发布于 03-06 20:01 山东
mark一下内存模型
点赞 回复 分享
发布于 03-03 11:24 陕西

相关推荐

评论
5
15
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务