freeRTOS学习笔记(十四)--内存

c 复制代码
if ( xWantedSize & portBYTE_ALIGNMENT_MASK ) {
  xWantedSize += ( portBYTE_ALIGNMENT -
  ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
 }

这段代码是 FreeRTOS 操作系统中的内存对齐处理逻辑 ,核心作用是:将 xWantedSize(需要申请的内存大小)向上调整为 portBYTE_ALIGNMENT(字节对齐数)的整数倍,避免内存地址不对齐导致的硬件访问效率低、甚至崩溃问题(尤其嵌入式系统中要求严格)。

我们先拆解关键宏定义,再分析代码逻辑,最后用例子帮你理解:

一、先明确两个关键宏的含义(FreeRTOS 约定)

这段代码的核心是两个宏,FreeRTOS 中会提前定义,本质是二进制位操作的"掩码"和"对齐基数":

宏名 作用 举例(假设 portBYTE_ALIGNMENT = 8
portBYTE_ALIGNMENT 内存对齐的"基数"(必须是 2 的幂,比如 4、8、16 字节,由芯片架构决定) 8(表示按 8 字节对齐)
portBYTE_ALIGNMENT_MASK 对齐基数减 1 得到的"掩码"(用于按位与运算判断是否对齐) 8 - 1 = 7(二进制 0b00000111

👉 关键规律:因为 portBYTE_ALIGNMENT 是 2 的幂,所以 portBYTE_ALIGNMENT_MASK 的二进制一定是「低 n 位全 1,高位全 0」(比如 8→7=0b111,4→3=0b11)。

二、代码逻辑拆解(分两步:判断是否对齐 → 不对齐则向上补齐)

代码的核心是"按位与判断对齐状态"+"计算补齐差值",我们逐行分析:

1. 条件判断:if ( xWantedSize & portBYTE_ALIGNMENT_MASK )

作用:判断 xWantedSize 是否已经是 portBYTE_ALIGNMENT 的整数倍(是否对齐)。

  • 原理:利用 portBYTE_ALIGNMENT_MASK 是「低 n 位全 1」的特性:
    • 如果 xWantedSize 是对齐数的整数倍,它的二进制「低 n 位一定全是 0」(比如 8 的倍数:8=0b1000、16=0b10000,低 3 位都是 0);
    • 按位与 & 掩码后,结果为 0(比如 8 & 7 = 0、16 & 7 = 0),条件不成立,无需调整;
    • 如果 xWantedSize 未对齐,低 n 位至少有一个 1(比如 9=0b1001、13=0b1101),按位与后结果非 0,条件成立,需要调整。
2. 对齐调整:xWantedSize += (portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK))

作用:计算需要"补齐多少字节",让 xWantedSize 变成对齐数的整数倍。

  • 拆解公式:
    • xWantedSize & portBYTE_ALIGNMENT_MASK:得到 xWantedSize 相对于"上一个对齐数"的「余数」(比如 xWantedSize=9,掩码=7,9&7=1,余数是 1);
    • portBYTE_ALIGNMENT - 余数:得到需要"补齐的字节数"(比如 8-1=7,9+7=16,刚好是 8 的倍数);
    • 最终 xWantedSize 被更新为「下一个对齐数的整数倍」。

三、实际例子(让你秒懂)

假设嵌入式系统中定义:portBYTE_ALIGNMENT = 8portBYTE_ALIGNMENT_MASK = 7(二进制 0b111),分两种情况看:

情况 1:xWantedSize 未对齐(比如 xWantedSize=9)
  • 第一步判断:9 的二进制是 0b1001,与掩码 7(0b0111)按位与 → 0b0001(结果=1≠0),条件成立;
  • 第二步调整:
    补齐字节数 = 8 - (9 & 7) = 8 - 1 = 7;
    调整后 xWantedSize = 9 + 7 = 16(16 是 8 的整数倍,对齐完成)。
情况 2:xWantedSize 已对齐(比如 xWantedSize=16)
  • 第一步判断:16 的二进制是 0b10000,与掩码 7(0b00111)按位与 → 0b00000(结果=0),条件不成立;
  • 第二步:不执行调整,xWantedSize 保持 16 不变。
其他例子(快速验证)
xWantedSize(原始值) 是否对齐(8字节) 余数(&7) 补齐字节数 调整后的值
3 3 5 8
8 0 0 8
14 6 2 16
17 1 7 24

四、核心本质:为什么用按位与,而不是取余?

你可能会想:"判断是否对齐,用 xWantedSize % portBYTE_ALIGNMENT != 0 不行吗?"

答案是:可以,但按位与运算(&)比取余运算(%)快得多

嵌入式系统中内存申请是高频操作,而按位与是 CPU 直接支持的底层指令(1 个时钟周期完成),取余运算需要多步计算,效率更低。FreeRTOS 作为实时操作系统,必须优先保证效率,所以用按位与实现。

总结

这段代码的最终目的:强制将内存申请大小 xWantedSize 向上对齐到 portBYTE_ALIGNMENT 的整数倍,是嵌入式系统中解决内存对齐问题的经典写法,核心依赖"2 的幂对齐数 → 掩码是低 n 位全 1"的二进制特性,用按位与实现高效判断和计算。

二、 prvHeapInit( void )

c 复制代码
static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;

	/* Ensure the heap starts on a correctly aligned boundary. */
	pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

	/* xStart is used to hold a pointer to the first item in the list of free
	blocks.  The void cast is used to prevent compiler warnings. */
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
	xStart.xBlockSize = ( size_t ) 0;

	/* xEnd is used to mark the end of the list of free blocks. */
	xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
	xEnd.pxNextFreeBlock = NULL;

	/* To start with there is a single free block that is sized to take up the
	entire heap space. */
	pxFirstFreeBlock = ( void * ) pucAlignedHeap;
	pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
	pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}

这段代码是 FreeRTOS 内存堆初始化函数prvHeapInit),核心作用是:初始化 FreeRTOS 用于动态内存分配(pvPortMalloc/vPortFree)的堆空间,完成「堆地址对齐」「空闲块链表搭建」两大核心工作,为后续内存申请/释放打下基础。

我们逐行拆解代码逻辑,结合 FreeRTOS 内存管理的核心设计(空闲块链表管理),帮你彻底理解:

一、先明确关键背景与变量

在看代码前,需先理清 3 个核心前提,避免混淆:

1. 堆的底层存储:ucHeap 数组

FreeRTOS 的堆本质是一块预先定义的静态数组 ucHeap(用户需在配置文件中通过 configTOTAL_HEAP_SIZE 指定大小),比如:

c 复制代码
// 底层堆存储数组(FreeRTOS 内部定义或用户配置)
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

prvHeapInit 的核心是对这块数组的「地址校准」和「链表化管理」,让内存分配能通过链表快速查找空闲块。

2. 关键宏定义(延续之前内存对齐的逻辑)
宏名 作用
portBYTE_ALIGNMENT 内存对齐基数(2的幂,如4/8/16字节,由芯片架构决定)
portBYTE_ALIGNMENT_MASK 对齐掩码(portBYTE_ALIGNMENT - 1,如8→7=0b111)
portPOINTER_SIZE_TYPE 指针宽度类型(32位系统为uint32_t,64位为uint64_t,确保地址操作兼容)
configADJUSTED_HEAP_SIZE 调整后的堆总大小(= 实际可用堆大小,需扣除对齐浪费、链表节点占用等)

FreeRTOS 用「双向链表」管理空闲内存块(简化版定义如下),每个空闲块的头部都有一个 BlockLink_t 节点,记录块大小和下一个空闲块地址:

c 复制代码
typedef struct BlockLink_t {
    struct BlockLink_t *pxNextFreeBlock; // 指向下一个空闲块
    size_t xBlockSize;                   // 当前空闲块的大小(包含节点本身)
} BlockLink_t;
  • xStart:链表头节点(不占用实际堆空间,仅用于标记链表起始);
  • xEnd:链表尾节点(标记链表结束,避免越界)。

二、逐行拆解代码逻辑

代码核心分 3 步:堆地址对齐 → 初始化链表头尾 → 搭建初始空闲块链表

1. 第一步:堆地址对齐(最关键的一行代码)
c 复制代码
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

这行代码的目的是:将堆的起始地址 ucHeap 校准为 portBYTE_ALIGNMENT 的整数倍(避免内存对齐问题),拆解为 3 个关键步骤:

(1)&ucHeap[ portBYTE_ALIGNMENT ]:获取堆的「偏移起始地址」
  • ucHeap 是堆数组的起始地址,但这个地址可能不对齐(比如 ucHeap 地址是 0x20000001,按 8 字节对齐的话就不符合要求);
  • ucHeap[ portBYTE_ALIGNMENT ] 是数组第 portBYTE_ALIGNMENT 个元素(偏移量 = 对齐基数),取它的地址 &ucHeap[...]),确保后续对齐操作不会让起始地址"向前偏移"(避免越界)。
(2)( portPOINTER_SIZE_TYPE ) 强制类型转换
  • 将地址转换为 portPOINTER_SIZE_TYPE(指针宽度类型),确保地址操作(按位与)在 32/64 位系统上都兼容(避免宽度不一致导致的错误)。
(3)& ~portBYTE_ALIGNMENT_MASK:按位与对齐(核心操作)
  • ~portBYTE_ALIGNMENT_MASK:对掩码按位取反(比如掩码是 7=0b00000111,取反后是 0b11111000);
  • 按位与运算(&)的作用:将地址的「最低 n 位(n 是对齐基数的幂次)强制清 0」,最终得到对齐后的地址。

👉 示例(假设 portBYTE_ALIGNMENT=8,掩码=7=0b111):

  • &ucHeap[8] 地址是 0x2000000A(二进制 10000000000000000000000000001010);
  • 按位与 0xFFFFFFF8~7 的 32 位表示)后,结果是 0x20000008(最低 3 位清 0),刚好是 8 的整数倍,对齐完成。

最终 pucAlignedHeap 就是「对齐后的堆实际起始地址」,后续所有内存分配都从这个地址开始。

2. 第二步:初始化链表头(xStart)和链表尾(xEnd
c 复制代码
// 链表头 xStart:指向第一个空闲块(对齐后的堆起始地址),自身大小为 0(仅作为链表入口)
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
xStart.xBlockSize = ( size_t ) 0;

// 链表尾 xEnd:标记空闲块链表的结束,大小设为调整后的堆总大小,下一个节点为 NULL
xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
xEnd.pxNextFreeBlock = NULL;
  • xStart 是"哨兵节点",不占用堆空间,仅用于快速找到第一个空闲块;
  • xEnd 也是"哨兵节点",用于判断链表是否遍历到末尾(避免访问无效地址)。
3. 第三步:搭建初始空闲块链表(整个堆为一个大空闲块)
c 复制代码
// pxFirstFreeBlock 指向对齐后的堆起始地址(第一个空闲块)
pxFirstFreeBlock = ( void * ) pucAlignedHeap;
// 初始空闲块大小 = 调整后的堆总大小(整个堆未分配,是一个完整块)
pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
// 第一个空闲块的下一个节点 = 链表尾 xEnd(链表只有一个块)
pxFirstFreeBlock->pxNextFreeBlock = &xEnd;

初始化完成后,空闲块链表的结构是:

复制代码
xStart(头节点) → pxNextFreeBlock → [ 第一个空闲块(整个堆) ] → pxNextFreeBlock → xEnd(尾节点)

👉 关键:后续调用 pvPortMalloc 时,FreeRTOS 会遍历这个链表,找到大小合适的空闲块;调用 vPortFree 时,会将释放的块重新插入链表(可能合并相邻空闲块),实现内存的循环利用。

三、核心目的总结

prvHeapInit 做了两件"奠基性"的事,缺一不可:

  1. 地址对齐 :通过按位与运算,确保堆起始地址是 portBYTE_ALIGNMENT 的整数倍,避免后续内存分配因地址不对齐导致的硬件访问错误(尤其嵌入式系统);
  2. 链表初始化 :将整个堆空间初始化为一个"大空闲块",并通过 xStart(头)和 xEnd(尾)搭建空闲块链表的框架,让后续内存申请/释放能通过链表高效操作(无需遍历整个堆数组,提升效率)。

四、补充:configADJUSTED_HEAP_SIZE 的意义

configADJUSTED_HEAP_SIZE 不是简单等于 configTOTAL_HEAP_SIZE,而是 FreeRTOS 内部计算的「实际可用堆大小」,计算逻辑通常是:

c 复制代码
// 简化逻辑:总堆大小 - 地址对齐浪费的字节数 - 链表节点占用的固定空间
#define configADJUSTED_HEAP_SIZE ( configTOTAL_HEAP_SIZE - 对齐浪费 - 节点开销 )

目的是确保 pxFirstFreeBlock->xBlockSize 是"真实可用的内存大小",避免分配时超出堆的实际范围。

最终链表结构图示(初始化后)

复制代码
+----------------+       +------------------------+       +----------------+
| xStart(头节点)|       | 第一个空闲块(ucHeap) |       | xEnd(尾节点) |
+----------------+       +------------------------+       +----------------+
| xBlockSize = 0 |       | xBlockSize = 调整后堆大小 |       | xBlockSize = 调整后堆大小 |
| pxNextFreeBlock----->  | pxNextFreeBlock----->  |       | pxNextFreeBlock = NULL |
+----------------+       +------------------------+       +----------------+
                          (对齐后的堆起始地址 pucAlignedHeap)

后续调用 pvPortMalloc( xWantedSize ) 时,FreeRTOS 会从 xStart.pxNextFreeBlock 开始遍历,找到能容纳 xWantedSize(对齐后)的空闲块,分割后返回给用户;释放时再将块插回链表,完成闭环。

相关推荐
又是忙碌的一天1 小时前
mysql 学习第二天 SQL语句
sql·学习·mysql
客梦1 小时前
Java 学生管理系统
java·笔记
拼好饭和她皆失1 小时前
C#学习入门
开发语言·学习·c#
LFly_ice2 小时前
学习React-22-Zustand
前端·学习·react.js
q***3753 小时前
爬虫学习 01 Web Scraper的使用
前端·爬虫·学习
车端域控测试工程师4 小时前
Autosar网络管理测试用例 - TC003
c语言·开发语言·学习·汽车·测试用例·capl·canoe
hd51cc4 小时前
动态链接编程 学习笔记
笔记·学习
q***T5834 小时前
GitHub星标20万+的React项目,学习价值分析
前端·学习·react.js
小此方4 小时前
笔记:树。
数据结构·笔记