堆与栈的关系以及在裸机与RTOS中的应用

注:本文会讲述堆与栈(非数据结构概念)在存储空间的作用,并会分析在裸机与RTOS中的例子来介绍和区分

(1).基本概念

一、 栈 (Stack):为了"办事"而存在的临时工位

1. 定义 栈是一块连续的自动管理的内存区域。

  • 特点 :它的操作非常快(仅次于 CPU 寄存器),遵循 "后进先出" (LIFO) 原则。

  • 生命周期自动生灭。函数调用时自动分配,函数返回时自动释放。你不需要操心。

2. 保存什么内容? 栈主要保存 "为了执行函数而产生的临时数据",具体包括:

  1. 局部变量 :函数内部定义的变量(如 int i, char buf[10])。

  2. 函数参数 :调用函数时传进去的值(如 func(int a, int b) 中的 a 和 b)。

  3. 返回地址:函数执行完后,CPU 应该回到哪一行代码继续执行。

  4. 现场保护:发生中断或子函数调用时,CPU 寄存器里的值。

  5. 例子:栈是如何工作的

形象比喻 : 栈就像是 "便签纸"

  • main 函数在便签上写了 x 和 y。

  • 调用 add 时,撕下一张新便签贴在上面,写上 a, b 和 result。

  • add 算完后,把这张便签撕掉扔进垃圾桶(自动释放)。

  • 回到 main 的便签继续工作。

二、 堆 (Heap):为了"存货"而存在的仓库

1. 定义 堆是一块巨大的散乱的手动管理的内存区域。

  • 特点:空间大,但分配和释放速度慢。它不像栈那样有条理,数据可以在里面随便放。

  • 生命周期手动生灭 。从你 malloc 开始存在,直到你 free 结束。如果你忘了 free,它就一直占着(内存泄漏)。

2. 保存什么内容? 堆主要保存 "生命周期需要由程序员控制的数据""特别大的数据"

  1. 大数组/缓冲区:比如处理一张图片(100KB),栈太小放不下,必须放堆里。

  2. 动态数据结构:链表节点、二叉树节点(因为你不知道会有多少个节点,无法在编译时确定)。

  3. 跨函数存活的数据:你希望一个数据在函数 A 里创建,但函数 A 结束后数据还要留给函数 B 用。

  4. 例子:堆是如何工作的

形象比喻 : 堆就像是 "公共仓库"

  • 你想存一大箱货物,便签纸(栈)肯定贴不住。

  • 你去找管理员(malloc)申请了一个货架位置。

  • 管理员给了你一张 提货单(指针)

  • 你把 提货单(指针) 夹在手里的便签本(栈)里。

  • 当你办事时,通过便签本上的提货单,去仓库找到货物。

  • 办完事,你必须告诉管理员"货架空出来了"(free),否则那个货架永远被占用。

Gemini said

好的,我们回归最基础的概念。为了让你彻底理清,我们可以把 RAM(随机存储器) 想象成一个 "办事大厅"

在这个大厅里,主要划分为两个核心区域:栈(Stack)堆(Heap)


一、 栈 (Stack):为了"办事"而存在的临时工位

1. 定义 栈是一块连续的自动管理的内存区域。

  • 特点 :它的操作非常快(仅次于 CPU 寄存器),遵循 "后进先出" (LIFO) 原则。

  • 生命周期自动生灭。函数调用时自动分配,函数返回时自动释放。你不需要操心。

2. 保存什么内容? 栈主要保存 "为了执行函数而产生的临时数据",具体包括:

  1. 局部变量 :函数内部定义的变量(如 int i, char buf[10])。

  2. 函数参数 :调用函数时传进去的值(如 func(int a, int b) 中的 a 和 b)。

  3. 返回地址:函数执行完后,CPU 应该回到哪一行代码继续执行。

  4. 现场保护:发生中断或子函数调用时,CPU 寄存器里的值。

3. 例子:栈是如何工作的

C

复制代码
// 这是一个子函数
int add(int a, int b) {  // 1. 参数 a, b 存放在栈中
    int result;          // 2. 局部变量 result 存放在栈中
    result = a + b;
    return result;       // 3. 返回后,a, b, result 占用的空间自动失效
}

void main() {
    int x = 10;          // x 存放在栈中
    int y = 20;          // y 存放在栈中
    int z = add(x, y);   // 调用 add 时,栈会"长"出一截给 add 函数用
}

形象比喻 : 栈就像是 "便签纸"

  • main 函数在便签上写了 x 和 y。

  • 调用 add 时,撕下一张新便签贴在上面,写上 a, b 和 result。

  • add 算完后,把这张便签撕掉扔进垃圾桶(自动释放)。

  • 回到 main 的便签继续工作。


二、 堆 (Heap):为了"存货"而存在的仓库

1. 定义 堆是一块巨大的散乱的手动管理的内存区域。

  • 特点:空间大,但分配和释放速度慢。它不像栈那样有条理,数据可以在里面随便放。

  • 生命周期手动生灭 。从你 malloc 开始存在,直到你 free 结束。如果你忘了 free,它就一直占着(内存泄漏)。

2. 保存什么内容? 堆主要保存 "生命周期需要由程序员控制的数据""特别大的数据"

  1. 大数组/缓冲区:比如处理一张图片(100KB),栈太小放不下,必须放堆里。

  2. 动态数据结构:链表节点、二叉树节点(因为你不知道会有多少个节点,无法在编译时确定)。

  3. 跨函数存活的数据:你希望一个数据在函数 A 里创建,但函数 A 结束后数据还要留给函数 B 用。

3. 例子:堆是如何工作的

C

复制代码
#include <stdlib.h>

void process_image() {
    // 1. 指针 p 是局部变量,它本身存放在【栈】中!
    // 2. malloc(1024) 申请的 1024 字节的大空间,是在【堆】中!
    // 3. p 的值是一个地址,指向堆里的这块空间。
    char *p = (char *)malloc(1024); 

    if (p != NULL) {
        // 在堆里写入数据
        p[0] = 0xFF; 
        p[1] = 0xAA;
        
        // 使用完毕,必须手动释放!
        free(p); 
    }
    // 函数返回,栈里的指针变量 p 消失了。
    // 如果上面没写 free(p),堆里的 1024 字节就成了没人能找到的孤魂野鬼(内存泄漏)。
}

形象比喻 : 堆就像是 "公共仓库"

  • 你想存一大箱货物,便签纸(栈)肯定贴不住。

  • 你去找管理员(malloc)申请了一个货架位置。

  • 管理员给了你一张 提货单(指针)

  • 你把 提货单(指针) 夹在手里的便签本(栈)里。

  • 当你办事时,通过便签本上的提货单,去仓库找到货物。

  • 办完事,你必须告诉管理员"货架空出来了"(free),否则那个货架永远被占用。


三、 总结对比表

特性 栈 (Stack) 堆 (Heap)
管理者 编译器自动生成指令(自动挡) 程序员手动写代码(手动挡)
内容 局部变量、参数、返回地址 大数据、链表、图片缓存
大小 小(通常几 KB) 大(取决于剩余 RAM,通常几十 KB)
速度 极快 (CPU 指令直接操作) 较慢 (需要算法去查找空闲块)
生长方向 向下生长 (高地址 -> 低地址) 向上生长 (低地址 -> 高地址)
碎片问题 (严格排队,没有空隙) 严重 (容易产生零碎空间)

四、 它们是如何互动的?(关键点)

指针是连接栈和堆的桥梁。

通常我们不会直接"在堆里写代码",而是:

  1. 上定义一个指针变量。

  2. 让这个指针指向 里的数据。

(2).在裸机上的堆和栈

一、 裸机中栈和堆的大小是如何设置的?

在 STM32 等单片机开发中(如 Keil MDK 或 STM32CubeIDE),这两个空间的大小通常是在**启动文件(Startup File)链接脚本(Linker Script)**中静态定义的。

1. 设置位置:启动文件 (.s 汇编文件)

打开你的工程,找到类似 startup_stm32f103xb.s 的文件,开头通常就是:

  • 栈 (Stack):这里预留了 1KB 的连续内存。这就是你整个裸机程序所有函数嵌套能用的"总存款"。

  • 堆 (Heap) :这里预留了 512 字节。这就是你调用 malloc 能用的最大上限。


二、 栈 (Stack):核心误区纠正与实例

1. 核心误区:Main 函数与子函数的栈是独立的吗?

答案:绝对不是。这是一个极其重要的概念修正。

真相 :在裸机中,整个程序只有一个巨大的栈空间(就是上面定义的那 1KB)。

  • main 函数和它调用的子函数,大家共用这同一块长条形的内存。

  • 并没有"独立的栈",只有**"独立的栈帧 (Stack Frame)"**。

2. 栈的运作:像在笔记本上写字

想象这 1KB 的栈就是一个 笔记本

  1. Main 函数进场

    • 它在笔记本的第 1 页 写下了它的局部变量 int a

    • MSP 指针 指向第 1 页的末尾。

  2. Main 调用子函数 Func_A

    • CPU 不会换一本新笔记本,而是直接翻到第 2 页(紧接着 Main 的后面)。

    • Func_A 在第 2 页写下它的局部变量 char b

    • MSP 指针 移到了第 2 页的末尾。

  3. Func_A 返回

    • CPU 把第 2 页撕掉(MSP 指针回退到第 1 页末尾)。

    • 第 2 页的数据变成了"垃圾数据",等待被下次覆盖。

3. 栈里保存什么?
  • 局部变量 :函数内部定义的 int i, char buf[] 等。

  • 返回地址 (LR):当子函数执行完,CPU 知道回哪里继续执行。

  • 现场寄存器 (R0-R12):如果寄存器不够用了,或者发生中断,CPU 会把寄存器的值临时存在栈里。

4. 代码实例

结论 :栈是连续的、线性的 。所有函数(包括主函数)像"叠罗汉"一样挤在这预设的 1KB 空间里。如果叠得太高(递归太深),超出了 1KB,就叫 栈溢出 (Stack Overflow),程序崩溃。

三、 堆 (Heap):裸机下的管理与碎片化

裸机下的堆,主要服务于标准 C 库的 mallocfree

1. 裸机堆里保存什么?
  • 动态申请的数据 :你明确调用 malloc 想要存储的大块数据,或者不知道长度的数据(比如变长数组、链表节点)。

  • 注意:指针变量本身是在栈里 ,指针指向的实体内容在堆里

2. 裸机堆的回收机制 (最容易出问题的地方)

在没有 RTOS 的情况下,编译器(如 ARMCC 或 GCC)提供的 malloc/free 库函数通常实现得比较简单(通常是基于链表的管理算法)。

碎片化 (Fragmentation) 是如何产生的?

想象你的堆空间是 一排只有 10 个座位的电影院 (总大小 10 字节)。

  1. 申请 AptrA = malloc(3); -> 占用座位 [1, 2, 3]。剩余 7 个。

  2. 申请 BptrB = malloc(3); -> 占用座位 [4, 5, 6]。剩余 4 个。

  3. 申请 CptrC = malloc(3); -> 占用座位 [7, 8, 9]。剩余 1 个。

    • 此时内存布局:[AAA] [BBB] [CCC] [空]
  4. 释放 Bfree(ptrB);

    • 此时内存布局:[AAA] [空空空] [CCC] [空]

    • 虽然中间空出了 3 个位置,但它们被夹在 A 和 C 之间。

  5. 灾难发生 - 申请 DptrD = malloc(4);

    • 失败!

    • 虽然总共有 4 个空位(中间 3 个 + 最后 1 个),但没有连续的 4 个位置。

    • 这就是内存碎片

3. 裸机下堆设置多了会怎样?
  • 浪费 RAM :裸机 RAM 本来就小(如 STM32F103C8T6 只有 20KB)。如果你给堆分了 10KB,但实际上只用了 malloc 几次,剩下的空间就白白闲置了,栈想用也用不了。

  • 更严重的碎片化 :如果没有高效的算法(像 FreeRTOS 的 heap_4 那样的合并算法),裸机的 free 函数往往不具备高效合并相邻空闲块的能力 ,或者合并效率很低。随着程序运行时间变长,堆里会充满"小孔洞",最终导致明明有内存却 malloc 失败。

注:**能不用堆就不用堆 :在裸机开发中,99% 的场景推荐使用全局数组** 或栈变量 来替代 malloc

  • 坏习惯char *buf = malloc(100); ... free(buf);
  • 好习惯static char buf[100]; (虽然占用 RAM,但绝对安全,无碎片)。

**注:**栈要给够 :如果你要在局部变量里定义大数组(如 char buffer[512]),一定要去启动文件里把 Stack_Size 改大,否则程序会莫名其妙进入 HardFault 死机。

(3).裸机和RTOS上的堆和栈对比

一、 栈和堆的大小设置:RTOS vs 裸机

在裸机中,内存布局是"一眼望穿"的;而在 RTOS 中,内存布局变成了"两层套娃"。

1. 裸机 (Bare Metal)
  • 设置位置 :全部在 启动文件 (.s) 中设置。

  • 栈 (Stack) :只有一个 Stack_Size。这是所有函数(Main + 子函数)和中断共用的唯一空间。

  • 堆 (Heap) :只有一个 Heap_Size。这是给标准库 malloc 用的。

2. RTOS (Real-Time OS)

RTOS 引入了"系统级"和"任务级"的概念,设置变复杂了:

  • A. 启动文件中的 Stack (MSP)

    • 设置 :依然在启动文件 .s 中设置,但通常可以设得比较小(比如 1KB 或 512B)。

    • 用途只给中断服务函数 (ISR) 和 RTOS 内核启动前使用 。普通任务用这里!

  • B. RTOS 总堆 (Total Heap)

    • 设置 :在 FreeRTOSConfig.h 中通过 #define configTOTAL_HEAP_SIZE 设置(比如 20KB)。

    • 用途:这是 RTOS 的"大金库"。所有的任务栈、信号量、队列都从这里扣除。

  • C. 任务栈 (Task Stack)

    • 设置 :在创建任务 xTaskCreate 时指定(如 usStackDepth 参数)。

    • 来源注意!任务栈是从"RTOS 总堆"里 malloc 出来的。

一句话区别

  • 裸机:栈是栈,堆是堆,井水不犯河水。

  • RTOS :任务栈 寄生 在 RTOS 的堆里面。

结论:RTOS中有基本的栈还有系统申请的堆空间中的任务栈

二、 MSP 与 PSP 指针:双栈机制

"是否是同一个 SP? " 答案是:物理上是两个独立的寄存器,但逻辑上 CPU 同一时间只能用其中一个。

1. 定义
  • MSP (Main Stack Pointer):主栈指针。

  • PSP (Process Stack Pointer):进程栈指针。

2. 裸机 vs RTOS 的区别
  • 裸机

    • 只用 MSP。无论在运行 Main 函数还是在跑中断,SP 寄存器永远指向 MSP 的地址。

    • PSP 在裸机里通常是闲置的。

  • RTOS

    • 双栈切换

    • 运行内核/中断时 :CPU 自动切换使用 MSP

    • 运行用户任务时 :CPU 自动切换使用 PSP

3. 为什么要这样设计?(为了安全!)

如果某个任务写的不好,导致 栈溢出 (Stack Overflow)

  • 在裸机中:栈溢出会直接破坏全局变量甚至中断向量表,系统直接死机或乱跑。

  • 在 RTOS 中 :因为它用的是 PSP,溢出只会破坏堆里的数据(因为任务栈在堆里),通常不会破坏 MSP。这意味着中断和内核还能响应,你还有机会在 HardFault 中查出是哪个任务出了问题,甚至重启该任务,而不是整个系统挂掉。

三、 优先级:反直觉的颠倒

这也是初学者最容易晕的地方。

1. 硬件优先级 (NVIC / 裸机)
  • 规则数字越小,优先级越高

  • 例子:0 是最高优先级(皇帝),15 是最低(平民)。

  • 适用对象:所有的中断(定时器中断、串口中断、外部中断)。

2. 软件优先级 (RTOS 任务)
  • 规则数字越大,优先级越高

  • 例子configMAX_PRIORITIES - 1 是最高优先级,0 是空闲任务(垫底)。

  • 适用对象 :你创建的各种 Task

3. 谁是大王?

永远记住:硬件中断 > 软件任务 。 不管你的 RTOS 任务优先级设得有多高(比如 999),只要一个中断(优先级 15,最低的中断)来了,CPU 立刻 暂停任务去跑中断。

  • MSP 此时接管现场。

  • PSP 暂停工作。

四、 RTOS 的堆内存管理 (heap_x.c)

FreeRTOS 提供了 5 种堆管理文件,用来替代裸机那个简陋的 malloc/free

1. 为什么裸机容易碎片化?

裸机通常直接调用编译器库(C Library)的 malloc

  • 它的算法通常很基础(比如简单的链表)。

  • 致命弱点 :当你 free 了一块内存,它可能只是简单标记"这里空了",但不会主动去检查这块空地能不能和旁边的空地合并。

  • 结果:内存变成了"瑞士奶酪",全是小洞,存不下大数据。

2. RTOS 的 Heap_4.c 是如何"回收"的?

heap_4.c 之所以强,是因为它实现了 内存合并 (Coalescence) 算法

工作流程例子 : 假设堆里有 3 块连续的内存:[A] [B] [C]

  1. free(B)。现在 B 是空的。

  2. Heap_4 不会就此罢休,它会向左看:A 是空的吗?如果是,把 A 和 B 捏在一起。

  3. 它会向右看:C 是空的吗?如果是,把 B 和 C 捏在一起。

  4. 最终结果:原本三个小碎片,被合并成了一个巨大的连续空闲块。

3. 五种模式速览
  • Heap_1 :只分配,不回收。最安全,适合由于不删除任务的系统。(无碎片,因为根本不释放)

  • Heap_2 :能回收,但不合并。会产生碎片。(不推荐)

  • Heap_3 :也就是封装了标准 C 的 malloc/free,只是加了线程保护。碎片化问题和裸机一样。

  • Heap_4能回收 + 能合并。最常用,碎片化极低。

  • Heap_5 :和 Heap_4 算法一样,但支持不连续的物理内存(比如内部 SRAM + 外部 SDRAM)。

相关推荐
寻寻觅觅☆8 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
YJlio8 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
l1t9 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
赶路人儿9 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar12310 小时前
C++使用format
开发语言·c++·算法
码说AI10 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS10 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
星空下的月光影子10 小时前
易语言开发从入门到精通:补充篇·网络爬虫与自动化采集分析系统深度实战·HTTP/HTTPS请求·HTML/JSON解析·反爬策略·电商价格监控·新闻资讯采集
开发语言
老约家的可汗10 小时前
初识C++
开发语言·c++
wait_luky10 小时前
python作业3
开发语言·python