跳转至

osal_kmalloc

动态内存分配 — kmalloc / kfree — OSAL kmalloc / kfree

前置阅读:多任务并发

学习目标

  • 理解嵌入式 RTOS 动态内存的碎片风险和适用场景
  • 掌握 osal_kmalloc(size, gfp) → NULL 检查 → osal_kfree(ptr)ptr = NULL 的标准防泄漏写法
  • 区分 OSAL_GFP_KERNEL(任务中使用,可阻塞)和 OSAL_GFP_ATOMIC(ISR 中使用,不可阻塞)
  • 能够通过 alloc/free 计数排查内存泄漏

基本概念

什么时候用动态内存

嵌入式的首选是静态分配——编译期确定大小,无碎片、无泄漏风险。但以下场景必须用动态内存:

场景 为什么静态分配不行
可变长度协议帧缓冲区 帧长不确定(几十到几千字节),静态分配只能按最大帧长预留——浪费
运行时动态设备列表 设备数量不确定(可能连接 1 个或 8 个),静态定死不灵活
OTA 固件暂存 临时大缓冲区(~128KB),用完全释放,长期占用太浪费

动态内存不是禁忌——关键是用对场景和用对方法。

碎片问题

碎片是动态内存最大的陷阱——不是总空闲够,是没有连续的块够大。

flowchart TD
    subgraph 初始状态
        A1[空闲 256B]
    end
    subgraph 分配后
        A2[已用 64B] --> F2[空闲 192B]
    end
    subgraph 再分配再释放
        A3[已用 64B] --> B3[空闲 64B] --> C3[已用 64B] --> D3[空闲 64B]
    end
    subgraph 碎片化结果
        N[需要 128B 连续块 — 虽然空闲总计 128B<br/>但都是 64B 小块,分配失败]
    end

碎片化后即使总空闲 128B,两个 64B 小块无法合并——osal_kmalloc(128, ...) 返回 NULL。

GFP 标志:何时可阻塞、何时不可

标志 使用上下文 行为 典型场景
OSAL_GFP_KERNEL 任务 可能阻塞等待内存回收,分配成功率更高 正常业务代码
OSAL_GFP_ATOMIC ISR / tasklet 绝不阻塞,分配不到立即返回 NULL 中断上下文中临时分配

ISR 中必须用 OSAL_GFP_ATOMIC——用 KERNEL 可能导致 ISR 阻塞,系统崩溃。

分配生命周期

sequenceDiagram
    participant T as 任务
    participant H as 堆

    T->>H: buf = osal_kmalloc(size, GFP_KERNEL)
    alt 分配成功
        H-->>T: 返回有效指针
        T->>T: if (buf == NULL) 错误处理
        T->>T: memset / 使用 buf
        T->>H: osal_kfree(buf)
        T->>T: buf = NULL
        Note over T: 置 NULL 防野指针重用
    else 分配失败 OOM
        H-->>T: 返回 NULL
        T->>T: 错误处理——降级或重试
    end

涉及 API

API 谁调用 用途 头文件
void *osal_kmalloc(unsigned long size, unsigned int osal_gfp_flag) 任务/ISR 动态分配内存 osal_addr.h
void *osal_kzalloc(unsigned long size, unsigned int osal_gfp_flag) 任务/ISR 分配并清零(等价于 kmalloc + memset(0)) osal_addr.h
void osal_kfree(void *addr) 任务/ISR 释放内存 osal_addr.h

案例说明

做什么

任务中循环分配随机大小的缓冲区(模拟可变长度协议帧),用完立即释放。连续运行观察可用内存是否保持稳定——验证无内存泄漏。

规格与功能

规格项 说明
分配大小 32~256B 随机(模拟可变长度帧)
GFP 标志 OSAL_GFP_KERNEL(任务中)
释放时机 每次使用后立即释放
泄漏检测 维护 alloc 计数和 free 计数,差值持续增长 = 泄漏

程序运行流程:

  1. 任务循环 100 次:随机大小 → alloc → 检查 NULL → 使用 → free → 置 NULL
  2. 循环结束后打印 alloc/free 统计,确认一致
  3. 长时间运行(10 分钟+),观察可用内存趋势

案例流程

flowchart TD
    S[开始] --> A[生成随机大小 32-256]
    A --> B[osal_kmalloc size, GFP_KERNEL]
    B --> C{buf == NULL?}
    C -->|是| E[打印 OOM 错误]
    C -->|否| D[使用 buffer]
    D --> F[osal_kfree buf]
    F --> G[buf = NULL]
    G --> H{循环结束?}
    H -->|否| A
    H -->|是| I[打印 alloc/free 统计]

案例操作指导

第一步:编译

fbb build ws63-liteos-app

第二步:烧录

第三步:验证

串口输出类似:

[alloc] size=128, ptr=0x2000A000, free_remain=45000
[free]  ptr=0x2000A000, free_remain=45128
...
[summary] alloc=100, free=100, leak=0

leak=0free_remain 变化在合理范围内(碎片导致的微小波动正常)。

关键配置

参数 说明
GFP 标志(任务中) OSAL_GFP_KERNEL 可能阻塞,分配成功率最高
GFP 标志(ISR 中) OSAL_GFP_ATOMIC 绝不阻塞,分配失败即返回 NULL
alloc 后 必须检查 NULL OOM(Out Of Memory)处理
free 后 必须置 NULL 防止野指针被误用
避免高频 alloc/free 固定大小用 membox kmalloc 高频调用加速碎片化

代码详解

标准防泄漏写法

#include "osal_addr.h"

/* 正确的 alloc/free 模式 */
void process_frame(unsigned int frame_len)
{
    uint8_t *buf = NULL;

    /* 1. 分配 */
    buf = (uint8_t *)osal_kmalloc(frame_len, OSAL_GFP_KERNEL);
    if (buf == NULL) {
        printf("OOM: cannot alloc %u bytes\n", frame_len);
        return;  // 分配失败,优雅降级
    }

    /* 2. 使用 */
    memset(buf, 0, frame_len);
    // ... 协议解析等操作 ...

    /* 3. 释放 + 置 NULL */
    osal_kfree(buf);
    buf = NULL;  // 防止后续代码误用已释放的指针
}

可变长度分配

/* 根据协议帧头决定分配大小——这是静态分配做不到的 */
uint16_t payload_len = (frame_header[2] << 8) | frame_header[3];
uint8_t *payload = osal_kmalloc(payload_len, OSAL_GFP_KERNEL);
if (payload != NULL) {
    memcpy(payload, &frame_data[4], payload_len);
    process_payload(payload, payload_len);
    osal_kfree(payload);
    payload = NULL;
}

ISR 中的原子分配

/* ISR 中临时需要一块小缓冲——必须用 GFP_ATOMIC */
static int my_isr(int irq, void *dev)
{
    uint8_t *tmp = osal_kmalloc(64, OSAL_GFP_ATOMIC);
    if (tmp == NULL) {
        /* 分配不到不阻塞,立即返回 */
        return OSAL_IRQ_HANDLED;
    }
    /* 快速使用 */
    memcpy(tmp, hw_fifo, 64);
    osal_msg_queue_write_copy(g_queue_id, tmp, 64, OSAL_MSGQ_NO_WAIT);
    osal_kfree(tmp);  // ISR 中可以 free
    return OSAL_IRQ_HANDLED;
}

ISR 中 kmallocGFP_ATOMIC、队列操作用 NO_WAIT——两条规则都指向同一条原则:ISR 绝不阻塞。

内存泄漏排查

static int alloc_count = 0;
static int free_count = 0;

void *my_alloc(unsigned long size)
{
    void *ptr = osal_kmalloc(size, OSAL_GFP_KERNEL);
    if (ptr != NULL) alloc_count++;
    return ptr;
}

void my_free(void *ptr)
{
    if (ptr != NULL) {
        osal_kfree(ptr);
        free_count++;
    }
}

/* 定期打印统计——差值持续增长 = 有泄漏 */
void print_mem_stats(void)
{
    printf("alloc=%d free=%d leak=%d\n",
           alloc_count, free_count, alloc_count - free_count);
}

如果 alloc_count - free_count 持续增长,说明有代码路径 alloc 了但没有 free。排查方法:每次 alloc 时记录调用栈(或文件名+行号),找到未配对的 alloc。