内存堆管理

Copyright © Quectel Wireless Solutions Co., Ltd. 2026. All rights reserved.


本文档旨在介绍UniRTOS堆内存管理的功能概述、相关API、示例代码以及常见问题,帮助用户正确理解并使用UniRTOS的内存管理功能。

功能概述

内存是程序运行时用于保存指令和数据的空间,通常由RAM提供。它支持读写操作,且掉电后数据会丢失。在嵌入式系统中,由于RAM容量有限且内容动态变化,内存管理的核心目标如下:

  1. 在有限空间内提升利用率。

  2. 确保内存分配与释放行为可控。

  3. 降低内存碎片与耗尽风险。

UniRTOS的内存管理设计思想:将内核与具体的堆分配算法解耦。

内核仅定义统一接口,不强制具体的底层实现。这使得开发者能够根据实际产品场景灵活选择分配策略,在可靠性、实时性与内存利用率之间取得最佳平衡。

备注

不同项目模块的物理内存大小存在差异。

静态内存

概述

静态存储空间(即静态存储期)是指在程序整个运行期间持续存在的数据区域。此类对象的生命周期贯穿程序的启动与结束,不会因函数的退出而释放。在嵌入式系统中,这类数据通常由链接脚本分配至固定的内存段(如 .data.bss.rodata),其物理地址一般在链接阶段确定。

全局变量

全局变量是定义在函数外部、可被多个函数访问的变量。根据其初始化情况,可分为以下两类:

  1. 已初始化全局变量

    此类变量通常存放于 .data 段。在常见的MCU/SoC架构中,其初始值保存在非易失性存储(Flash/ROM)中。系统上电启动时,启动代码会将这些初值拷贝至RAM,以供运行时读写。

  2. 未初始化全局变量(或显式初始化为0)

    此类变量通常存放于 .bss 段。由于它们无需在Flash中保存完整的初值镜像,启动时由启动代码统一进行清零操作。

静态变量

静态变量是使用 static 修饰的变量,生命周期同样贯穿程序整个运行期。根据定义位置不同,含义略有区别:

  1. 文件作用域 static 变量
    定义在函数外部,仅在当前源文件内可见,常用于实现模块级的私有数据。

  2. 函数内部 static 变量
    定义在函数内部,仅在该函数的作用域内可见。此类变量不会随函数的返回而销毁,在多次调用该函数时,能够保留上一次调用结束时的值。

动态内存

概述

动态存储空间是指在程序运行期间按需分配与释放的内存区域,其生命周期并不固定。常见的动态内存来源包括栈分配和堆分配。需要强调的是,“地址是否变化”并非动态内存的本质特征;其核心本质在于,内存的分配时机与生命周期由程序的运行状态决定,而非在编译阶段静态确定。

栈主要用于保存函数调用的现场信息与自动对象,典型内容包括函数参数、返回地址、部分寄存器上下文以及普通局部变量等。其主要特点如下:

  1. 自动管理
    内存在进入作用域时自动分配,在退出作用域时自动回收,该过程通常由编译器和运行时环境自动完成。

  2. 线程私有
    在UniRTOS及多线程系统中,每个线程均拥有独立的栈空间。当线程结束时,其占用的栈资源会被系统自动回收。

  3. 高效但容量受限
    栈内存通常按顺序推进与回退,分配开销小且速度快。但由于其空间有限,定义过大的局部对象极易导致栈溢出。

  4. 可配置而非“完全不可控”
    虽然开发者通常无法手动释放单个栈变量,但可以灵活配置线程栈的大小、栈布局以及溢出检测策略。

  5. 线程栈来源
    在许多系统中,线程栈通常从堆或内存池中动态申请;但在部分平台上,也可以进行静态预留,因此线程栈并非总是来源于堆内存。

堆是用于在运行期间进行动态分配的大块内存区域,通常由内存分配器统一管理(如 malloc/freeUniRTOS heap_x、内存池等)。其主要特点如下:

  1. 手动或半自动管理
    其典型模式是内存的申请与释放必须成对出现。若管理不当,极易引发内存泄漏、悬垂指针(野指针)以及重复释放等问题。

  2. 生命周期灵活
    堆上分配的对象可以跨函数、跨模块甚至跨任务存在,非常适合用于存储大小和存活时间在编译期难以确定的数据。

  3. 分配开销高于栈
    内存分配器需要维护额外的元数据并遍历查找可用内存块,因此其分配速度通常慢于栈内存分配。

  4. 碎片风险
    长期的随机分配与释放操作容易产生外部碎片,导致“总空闲内存充足,但缺乏足够大的连续内存块”的现象。

  5. 管理结构不唯一
    “基于链表”仅仅是堆分配的一种常见实现方式。在实际的底层设计中,还可能采用分离链表、位图、伙伴系统、TLSF算法或固定块内存池等多种数据结构。

RTOS内存管理算法

概述

本节所指的内存管理,仅针对堆内存空间。模块在开机运行后,会预留一块较大的内存区域作为堆内存。该堆的实际可用空间会随着应用程序的启动与停止而动态变化。鉴于不同RTOS的内存管理机制存在差异,UniRTOS作为多RTOS抽象层进行了统一适配。目前,该抽象层底层主要支持ThreadX和FreeRTOS两种内核。

常用算法

  1. 首次适应算法(First Fit)

该算法要求空闲分区链表按照地址从小到大的顺序进行链接。在分配内存时,系统从链表的第一个空闲分区开始顺序查找,并将第一个大小能够满足要求的空闲分区分配给进程。

  1. 循环首次适应算法(Next Fit)

Next Fit由First Fit算法演变而来。分配内存时,从上一次刚分配过的空闲分区的下一个开始查找,直至找到能满足要求的空闲分区。

  1. 最佳适应算法(Best Fit)

该算法旨在从所有空闲分区中,找出能满足要求且大小最小的空闲分区。为了加快查找速度,Best Fit算法会将所有空闲分区按其容量从小到大的顺序链接成链表。这样,系统第一次找到的满足大小要求的空闲分区必然是最优的。

  1. 最坏适应算法(Worst Fit)

该算法旨在从所有空闲分区中,找出能满足要求且大小最大的空闲分区。为了加快查找速度,Worst Fit算法会将所有空闲分区按其容量从大到小的顺序链接成链表。

  1. 两级隔离适应算法(TLSF)

该算法使用两层链表结构来管理空闲内存。第一级链表按大小范围对空闲分区进行粗略分类,每一类对应一个空闲链表,其中的分区大小均在特定范围内。由于存在多个空闲链表,系统引入了第二级索引链表来统一管理这些链表,索引表的每一项都对应一种特定的空闲链表,并记录其表头指针。

  1. 伙伴算法(Buddy systems)

该算法是Segregated Fit算法的一种变种,具有更高效的内存拆分与回收合并机制。伙伴算法包含多种实现形式,如Binary Buddies和Fibonacci Buddies等。其中,Binary Buddies是最简单且最流行的一种。它将所有空闲分区按大小进行分类,每一类包含大小完全相同的空闲分区集合,并使用双向链表进行管理。在Binary Buddies算法中,所有分区的大小均严格为2的幂次方。

内存算法

优点

缺点

First Fit

高地址空间的大空闲块得以保留

低地址空间被不断拆分,产生碎片;每次都从第一个空闲分区开始查找,系统开销增加

Next Fit

空闲分区分布比较均匀,算法开销小

缺乏大内存空闲块

Best Fit

用最小内存满足要求,保留大内存空闲块

每次分配后拆分的剩余空闲内存最小,易产生大量小碎片,算法开销大

Worst Fit

每次分配后拆分的剩余空闲内存较大,可减小碎片产生

缺乏大内存空闲块,算法开销大

TLSF

查找效率高,时间复杂度低,碎片问题表现良好

内存回收时算法复杂,系统开销大

Buddy systems

分配和回收速度快,合并高效

内部碎片比较严重

ThreadX

分配

ThreadX的内存分配采用首次适应算法。系统在分配内存时,会从第一个空闲内存块开始顺序查找,一旦找到大小满足要求的空闲块,便立即完成分配并返回成功。

空闲内存块采用链表方式进行管理,且各内存块在物理地址上按从低到高的顺序排列。每个内存块的前8个字节被用作控制字段(Header),其具体结构如下:

  • 前4字节:存储下一块相邻内存的起始地址。

  • 后4字节:用于标识当前内存块的状态。若为空闲内存,则存储特定标志值 TX_BYTE_BLOCK_FREE;若为已分配内存,则存储所属内存池管理结构体的起始地址。

在内存池初始化时,整个内存空间首先被划分为两个特殊的内存块:位于起始位置的第一块和位于末尾的最后一块。

  • 第一块(初始空闲区):其前4字节存储最后一块的起始地址,后4字节被设置为 TX_BYTE_BLOCK_FREE,标记为尚未使用的空闲块。

  • 最后一块(边界哨兵):其前4字节存放内存池管理结构体的起始地址,后4字节被标记为 TX_BYTE_BLOCK_ALLOC。该块作为内存池的边界哨兵,在逻辑上始终处于已分配状态,用于防止内存操作越界。

../../_images/image_EqcObVPSsohRZqx9Ho3cUnWcnnf.webp

下图展示了首次内存分配后的状态。最初的第一块内存被拆分为两块:新分配的第一块和剩余的第二块。其中,第一块被返回给应用程序。由于每个内存块的前8个字节为控制字段(Header),因此返回给应用程序的内存起始指针(memory_ptr)会向后偏移8个字节,直接指向实际可用的数据区域。对于新分配的第一块内存,其控制字段被更新为已分配状态:前4字节存储第二块内存的起始地址;后4字节则指向内存池管理结构体。这一状态标识表明该块内存已被占用,不再是空闲块。对于剩余的第二块内存,其前4字节更新为指向第三块内存(即初始化时的最后一块边界内存),从而继续保持单向链表的连贯性;其后4字节则存储 TX_BYTE_BLOCK_FREE 标志值,明确标识该块仍为空闲内存块。

../../_images/image_CMjRbs5Y3oJ1zjxv7Bocr1XGnue.webp

释放

内存释放时,将释放内存的首地址减8,得到内存块控制字段的首地址,再通过前4字节找到内存池管理结构体的起始地址,即可执行内存释放操作。释放后,控制字段的后4字节(偏移4~7)存储 TX_BYTE_BLOCK_FREE,标记该块为空闲内存块。

释放后,即使相邻的两个内存块地址连续,系统也不会立即进行合并或内存整理。等到申请内存时,若请求大小大于当前块的大小,系统会继续查找下一块。如果发现当前块与下一块地址连续,则将两者合并为一块。

释放内存后,系统会检查挂起链表中是否有线程等待。若有,则尝试分配内存并恢复线程执行。线程按FIFO顺序恢复,而非按照线程优先级高低。如需改变此行为,可在释放内存前调用 tx_byte_pool_prioritize(),将最高优先级线程移动到挂起链表最前面,使其优先被恢复。

碎片处理

内存在多次分配和释放后,可能会出现大量小的内存块,这种现象称为内存碎片化。

当需要分配较大的内存块时,每次可能需要先遍历大量的小内存块,这会导致查找开销增加,算法性能下降。由于每个内存块都占用8个字节的控制字段,大量小内存会导致内存的浪费。在查找过程中,若发现两个相邻内存块的地址连续,则将其合并为一个内存块,这个过程称为内存整理。

下图为多次内存分配和释放后的结构。其中,第一块已被占用,第二块、第三块、第四块为空闲内存块。第二块大小为64字节,第三块大小为64字节,第四块内存大小为256字节。假设现在申请分配的内存大小为128字节。在分配查找过程中,发现第二块太小无法满足,但第二块与第三块地址连续,于是将两者合并为一块。合并后的块大小正好满足请求(64+64=128),系统将其返回给应用程序,如下面第二个图所示。

../../_images/image_InMRbDFouoqFQlxaFDJc2QSTnAg.webp

FreeRTOS

FreeRTOS提供5种内存分配实现,开发者可按需求选择其一,或采用自定义方案。这5种实现分别对应源码中的heap_1.c、heap_2.c、heap_3.c、heap_4.c和heap_5.c。各实现的适用场景见下文。为便于理解其核心机制,文中给出的伪代码仅用于说明实现思路,不等同于完整工程代码且不可运行。

heap_1.c:

// 全局静态堆指针,大小由 configTOTAL_HEAP_SIZE 决定
static uint8_t ucHeap[HEAP_SIZE];

// 下一个可分配位置(偏移)
static size_t xNextFreeByte = 0;

// 已分配总量(可选统计)
static size_t xFreeBytesRemaining = HEAP_SIZE;

void *pvPortMalloc(size_t xWantedSize)
{
    void *pvReturn = NULL;

    // 1) 对齐到字节边界
    xWantedSize = align_up(xWantedSize, PORT_BYTE_ALIGNMENT);

    // 2) 进入临界区(线程安全)
    suspend_scheduler_or_enter_critical();

    // 3) 检查是否还能分配
    if ((xWantedSize > 0) &&
        (xNextFreeByte + xWantedSize <= HEAP_SIZE))
    {
        pvReturn = &ucHeap[xNextFreeByte];
        xNextFreeByte += xWantedSize;
        xFreeBytesRemaining -= xWantedSize;
    }
    else
    {
        // 分配失败,返回 NULL
        pvReturn = NULL;
    }

    // 4) 退出临界区
    resume_scheduler_or_exit_critical();

    // 5) 可选:malloc failed hook
    if (pvReturn == NULL) {
        call_malloc_failed_hook_if_enabled();
    }

    return pvReturn;
}

void vPortFree(void *pv)
{
    // heap_1 不支持释放
    (void)pv;
}
../../_images/image_Kla9bIp1eoLmSbxiJOZcrow1nM5.webp

heap_2

// 空闲块链表节点
typedef struct BlockLink_t {
    struct BlockLink_t *pxNextFreeBlock;  // 指向下一个空闲块
    size_t xBlockSize;                     // 当前块大小(包括节点头)
} BlockLink_t;

// 全局变量
static BlockLink_t *pxEnd = NULL;
static BlockLink_t xStart;  // 哨兵节点
static uint8_t ucHeap[HEAP_SIZE];

function prvHeapInit():
    // 将整个堆组织为一个大空闲块
    pxFirstFreeBlock = &ucHeap[0]
    pxFirstFreeBlock->xBlockSize = HEAP_SIZE
    pxFirstFreeBlock->pxNextFreeBlock = NULL
    
    // xStart 和 xEnd 作为哨兵
    xStart.pxNextFreeBlock = pxFirstFreeBlock
    xStart.xBlockSize = 0
    pxEnd = NULL  // 标记堆末尾
    
 
 function pvPortMalloc(xWantedSize):
    pxBlockToAllocate = NULL
    pxPreviousBlock = &xStart
    pxBlock = xStart.pxNextFreeBlock
    xWantedSize = xWantedSize + sizeof(BlockLink_t)  // 加上节点头
    
    enter_critical()
    
    // 遍历找最小满足块(Best Fit)
    while pxBlock != NULL:
        if pxBlock->xBlockSize >= xWantedSize:
            // 如果这是第一个满足的块 或 比前一个找到的块更小
            if (pxBlockToAllocate == NULL) or (pxBlock->xBlockSize < pxBlockToAllocate->xBlockSize):
                pxBlockToAllocate = pxBlock
                pxPreviousBlockForAlloc = pxPreviousBlock
      
        pxPreviousBlock = pxBlock
        pxBlock = pxBlock->pxNextFreeBlock
    
    // 如果找到了合适的块
    if pxBlockToAllocate != NULL:
        // 检查是否需要拆分
        if pxBlockToAllocate->xBlockSize > xWantedSize + heapMINIMUM_BLOCK_SIZE:
            // 拆分:剩余部分作为新空闲块
            pxNewBlockLink = (char *)pxBlockToAllocate + xWantedSize
            pxNewBlockLink->xBlockSize = pxBlockToAllocate->xBlockSize - xWantedSize
            pxNewBlockLink->pxNextFreeBlock = pxBlockToAllocate->pxNextFreeBlock
            
             // 原块变小
            pxBlockToAllocate->xBlockSize = xWantedSize
            pxBlockToAllocate->pxNextFreeBlock = pxNewBlockLink
        
        // 从链表移除已分配块
        pxPreviousBlockForAlloc->pxNextFreeBlock = pxBlockToAllocate->pxNextFreeBlock
        
        pvReturn = (char *)pxBlockToAllocate + sizeof(BlockLink_t)
    else:
        pvReturn = NULL
    
    exit_critical()
    
    if pvReturn == NULL:
        call_malloc_failed_hook_if_enabled()
    
    return pvReturn
              
              
 function vPortFree(pv):
    if pv == NULL:
        return
    
    enter_critical()
    
    // 倒退找到块头
    pxBlockToFree = (BlockLink_t *)((char *)pv - sizeof(BlockLink_t))
    
    // 重新插入空闲链表(按地址有序)
    pxBlock = &xStart
    while pxBlock->pxNextFreeBlock != NULL:
        if (char *)pxBlock->pxNextFreeBlock > (char *)pxBlockToFree:
            break
        pxBlock = pxBlock->pxNextFreeBlock
    
    // 注意:heap_2 不合并相邻块!
    pxBlockToFree->pxNextFreeBlock = pxBlock->pxNextFreeBlock
    pxBlock->pxNextFreeBlock = pxBlockToFree
    
    exit_critical()
../../_images/image_HaBpbtkA2opHh1xr15Wca7s5nfP.webp

heap_3:其实就pvPortMalloc定向标准库已经做好的malloc和free函数,底层就不用像heap_1和heap_2一样进行实现。

function pvPortMalloc(xWantedSize):
    pvReturn = NULL
    
    // 挂起调度器(保证线程安全)
    vTaskSuspendAll()
    {
        // 直接调用标准库 malloc
        pvReturn = malloc(xWantedSize)
        
        // 可选:记录 trace 信息
        traceMALLOC(pvReturn, xWantedSize)
    }
    // 恢复调度器
    xTaskResumeAll()
    
    // 分配失败处理
    if pvReturn == NULL:
        if CONFIG_USE_MALLOC_FAILED_HOOK == 1:
            // 回调失败处理钩子
            vApplicationMallocFailedHook()
    
    return pvReturn
    
    
  function vPortFree(pv):
    if pv == NULL:
        return
    
    // 挂起调度器
    vTaskSuspendAll()
    {
        // 直接调用标准库 free
        free(pv)
        
        // 可选:记录 trace 信息
        traceFREE(pv, 0)
    }
    // 恢复调度器
    xTaskResumeAll()  

heap_4: 直接在heap_2的基础上增加了合并相邻节点的功能,可以减少碎片化,下面伪代码只展示与heap_2差异之处,vPortFree函数。

function vPortFree(pv):
    pxBlockToFree = NULL
    pxIterator = NULL
    
    if pv == NULL:
        return
    
    enter_critical()
    
    // 倒退找到块头
    pxBlockToFree = (BlockLink_t *)((char *)pv - sizeof(BlockLink_t))
        // 更新统计
    xFreeBytesRemaining = xFreeBytesRemaining + pxBlockToFree->xBlockSize
    
    // 找到要插入的位置(按地址有序)
    pxIterator = &xStart
    while (pxIterator->pxNextFreeBlock != NULL) and 
          ((char *)pxIterator->pxNextFreeBlock < (char *)pxBlockToFree):
        pxIterator = pxIterator->pxNextFreeBlock
    
    // ===== 关键:合并逻辑开始 =====
    
    // 1) 检查与后面的块合并
    pxBlockToFree->pxNextFreeBlock = pxIterator->pxNextFreeBlock
    
    if pxBlockToFree->pxNextFreeBlock != NULL:
        if ((char *)pxBlockToFree + pxBlockToFree->xBlockSize == 
            (char *)pxBlockToFree->pxNextFreeBlock:
            // 与后面的块地址连续,合并
            pxBlockToFree->xBlockSize = pxBlockToFree->xBlockSize + 
                                        pxBlockToFree->pxNextFreeBlock->xBlockSize
            pxBlockToFree->pxNextFreeBlock = pxBlockToFree->pxNextFreeBlock->pxNextFreeBlock
    // 2) 检查与前面的块合并
    if pxIterator != &xStart:
        if ((char *)pxIterator + pxIterator->xBlockSize == 
            (char *)pxBlockToFree:
            // 与前面的块地址连续,合并
            pxIterator->xBlockSize = pxIterator->xBlockSize + pxBlockToFree->xBlockSize
            pxIterator->pxNextFreeBlock = pxBlockToFree->pxNextFreeBlock
        else:
            // 不连续,正常插入
            pxIterator->pxNextFreeBlock = pxBlockToFree
    else:
        // 插在链表最前面
        pxIterator->pxNextFreeBlock = pxBlockToFree
    
    // ===== 合并逻辑结束 =====
    
    exit_critical()

heap_5:

在保留heap_4主要优势(支持分配、释放和碎片合并)的基础上进一步增强了堆来源管理能力。与前几种实现通常只使用单一堆数组不同,heap_5可将多块彼此不连续的内存区域统一组织为一个逻辑堆,并通过同一个pvPortMalloc接口对外提供服务。对上层业务而言,无需感知底层内存来自几块区域,只需按统一接口申请和释放内存;对底层实现而言,可同时管理两块、三块甚至更多内存来源。这些内存来源既可以是同一RAM中相邻或不相邻的区域,也可以来自不同RAM/存储体。

方法

适用场景

heap_1

支持内存分配但不支持回收。适用于资源受限的小型嵌入式设备,系统启动时申请内存,在整个程序生命周期内通常无需释放

heap_2

支持内存回收,但不支持碎片合并。使用Best Fit算法。适用于每次申请内存大小相对固定的场景

heap_3

在标准库的malloc和free接口基础上增加了线程安全保护

heap_4

支持内存分配、回收及碎片合并。适用于频繁分配和释放大小不确定的内存的场景

heap_5

支持将多个不连续的内存区域组成堆,适用于内存分布不连续的嵌入式设备

当前模块使用的是heap_4方法,本小节主要介绍heap_4的算法实现。

分配

内存分配使用首次适应(First Fit)算法,每次从头开始查找空闲内存块,找到大小合适的内存块后,返回成功。

通过一个链表维护未分配的内存。链表节点定义如下:

typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK *pxNextFreeBlock;/*<< The next free block in the list. */
    size_t xBlockSize;/*<< The size of the free block. */
} BlockLink_t;

两个变量分别为:指向下一块内存的地址指针 pxNextFreeBlock,以及当前块的内存大小 xBlockSize。申请内存时,系统会在申请大小 xWantedSize 的基础上加上 heapSTRUCT_SIZE(链表节点大小),作为最终申请的内存大小。然后从链表头开始遍历未分配内存链表,查找符合大小的内存块。同时判断当前内存块是否有剩余(即大于所申请的大小)。若有剩余,则将剩余部分新建为一个未分配内存块节点,插入到未分配链表中,供下次分配使用。整体算法与前述ThreadX类似。

释放

内存释放时,根据释放的内存地址向前定位到对应的链表节点,获取该内存块的信息,然后调用链表插入函数将节点归还。

碎片处理

由上述分配过程可知,内存在多次分配和释放后同样会产生内存碎片。因此需要将内存碎片合并成大块内存,合并算法与前述ThreadX类似。为实现该合并算法,空闲内存链表按内存地址顺序(从小到大)进行存储。例如,对于待插入的内存块P,系统找到其地址前面的相邻内存块A,判断A与P之间是否存在已分配的内存块。若不存在,则直接将两者合并。接着,再检查P与其后面的相邻内存块C的位置关系。若P与C之间没有其他已分配的内存块,则同样进行合并。

应用场景 - 动态内存

底层动态内存申请和释放

底层程序在运行过程中,当需要内存空间存储数据时,会调用malloc接口从堆内存中申请一定大小的内存。当业务执行结束、不再需要该内存块时,会调用free接口释放内存并将其归还到堆内存中。

创建线程

程序运行过程中创建新线程时,需为该线程分配栈空间,该栈空间同样从堆内存中申请。当线程执行结束或被删除时,会释放其栈空间并将其归还到堆内存中。底层和应用层的程序都有可能创建线程。

内存管理API

头文件

qosa_def.h

函数概览

函数

说明

qosa_malloc()

从系统heap中申请指定大小的内存

qosa_free()

释放获得的内存

qosa_realloc()

调整一块已申请heap内存的大小

qosa_calloc()

申请heap内存

qosa_strdup()

复制一个字符串,并在heap中生成新的字符串副本

函数详解

qosa_malloc

  • 功能描述
    从系统heap中申请指定大小的内存。当前工程中的适配实现会对成功申请到的内存做清零初始化。

  • 函数原型

void* qosa_malloc(qosa_size_t nbytes)
  • 参数说明

参数名

输入/输出

类型

说明

nbytes

输入

qosa_size_t

需要申请的内存大小。单位:字节

  • 返回值说明
    非空指针:申请成功,返回可用内存首地址
    QOSA_NULL:申请失败,通常表示heap空间不足

备注

  1. 当前工程里的 qosa_malloc() 行为更接近“malloc + memset(0)”。

  2. 申请成功后,必须在不再使用时调用 qosa_free() 释放。

  3. 不应使用 qosa_free() 释放静态内存、栈内存或其他模块内部管理的内存。

qosa_free

  • 功能描述
    释放通过 qosa_malloc()qosa_calloc()qosa_realloc()qosa_strdup() 等接口获得的heap内存。

  • 函数原型

void qosa_free(void* ptr)
  • 参数说明

参数名

输入/输出

类型

说明

ptr

输入

void*

待释放的内存指针

  • 返回值说明

qosa_realloc

  • 功能描述
    调整一块已申请heap内存的大小,可用于扩容或缩容。

  • 函数原型

void* qosa_realloc(void* ptr, qosa_size_t nbytes)
  • 参数说明

参数名

输入/输出

类型

说明

ptr

输入

void*

原内存指针。通常应来自 qosa_malloc()qosa_calloc()qosa_realloc()

nbytes

输入

qosa_size_t

调整后的目标大小。单位:字节

  • 返回值说明
    非空指针:调整成功,返回新的内存首地址
    QOSA_NULL:调整失败,原指针通常仍然有效,调用方不应直接丢失原指针

qosa_calloc

  • 功能描述
    按“元素个数 × 单个元素大小”的方式申请heap内存,并将申请到的内存全部清零。

  • 函数原型

void* qosa_calloc(qosa_size_t count, qosa_size_t size)
  • 参数说明

参数名

输入/输出

类型

说明

count

输入

qosa_size_t

元素个数

size

输入

qosa_size_t

单个元素大小。单位:字节

  • 返回值说明
    非空指针:申请成功
    QOSA_NULL:申请失败,通常表示heap空间不足

qosa_strdup

  • 功能描述
    复制一个字符串,并在heap中生成新的字符串副本。

  • 函数原型

char* qosa_strdup(const char* s)
  • 参数说明

参数名

输入/输出

类型

说明

s

输入

const char*

源字符串指针

  • 返回值说明
    非空指针:复制成功,返回新字符串地址
    QOSA_NULL:输入为空或内存申请失败

示例代码

完整示例代码请查看https://github.com/UniRTOS/UniRTOS-Doc-Examples/blob/main/system/memory/memory.c

常见问题

如何避免内存碎片?

内存管理算法可在一定程度上减少内存碎片,但无法完全避免。建议采取以下措施:

  • 尽量使用栈空间,减少动态内存分配函数的调用。

  • 分配内存和释放内存尽量在同一个函数中完成。

  • 尽量一次性申请较大的内存块,避免反复申请小内存,以减少内存分割。

  • 自行设计内存池来管理内存。

堆安全剩余量

虽然堆空间是动态分配和释放的,但仍需保证有足够的剩余空间,以满足复杂业务场景下的内存申请需求。堆空间的总量由底层决定,固件生成后该值即确定。

内存泄露

常见内存泄漏的场景有:

  • 分配和释放内存的操作不成对。例如,业务开始时申请了内存,但在业务结束时未释放,下次业务运行时又重新申请。如此反复,导致可用内存不断减少,最终可能耗尽整个内存空间,使程序无法申请到内存而崩溃。

  • 重复创建线程。底层或应用层创建线程和删除线程的操作不成对,导致相同业务的线程被重复创建。每创建一个线程都需要从堆中分配栈空间,最终将耗尽整个堆空间。

内存越界

常见内存越界的场景有:

  • 数组越界:操作数组时,索引超过了实际分配的数组大小,导致访问越界到其他内存空间,可能破坏这些空间中的数据,从而引发系统异常。

  • 栈溢出:创建线程时传入的栈空间大小小于业务实际运行所需空间,导致运行过程中数据操作越界到其他内存空间,可能破坏这些空间中的数据,从而引发系统异常。

内存申请失败异常信息

底层内存申请失败通常会触发系统崩溃异常。