Wi-Fi 驱动内存占用分析

Linux 与 RTOS 驱动的静态段、动态分配与回收检查

Posted by Yvain Zhang on February 8, 2026 主题:技术

评估 Wi-Fi 驱动内存时,一个总数难以说明哪些配置可以调整,也难以判断是否存在泄漏。可以把问题拆成几项:

  • 静态成本是多少:代码段(.text)、常驻数据(.data、.bss)各占多少?
  • 动态开销在哪里:驱动基础运行需要多少?业务流量(TX/RX Buffer)撑起时峰值是多少?
  • 连接数的影响:作为 SoftAP 时,每增加一个 STA 客户端会增加多少内存?
  • 稳定性与趋势:高负载打流结束后内存能否完全回落?反复连断是否存在隐式泄漏? 下面分别整理 Linux 和 RTOS 环境中的统计方法、占用来源与回收检查。

注:本文使用经过归一化处理的示例区间说明分析方法,不对应任何具体芯片、客户项目或驱动版本。实际结果应以目标固件的链接 Map、模块信息和运行时统计为准。


1. 统计方法与维测工具

先确认统计覆盖哪些分配,再比较不同运行状态。操作系统的全局剩余内存还会受到其他模块和缓存影响,不能直接视为驱动占用。

1.1 静态段分析

静态内存反映了驱动固化的基础成本:

  • Linux 模块:
    • 通过 size <driver>.ko 查看 ELF 文件的 .text、.data、.bss 段大小。
    • 通过 lsmod 或 /sys/module/<driver_name>/sections/ 查看加载进内核后的实际内存映射。
  • RTOS 静态库 / 固件:
    • 通过交叉编译链的 arm-none-eabi-size <driver_library>.a,可以细化到目标文件的段大小分布。

1.2 动态内存与维测节点

在 Linux 内核驱动中,kmalloc/vmalloc 散落在各个协议与数据流分支中,单靠内核全局 Slab 统计很难剥离出单一驱动的真实开销。

推荐的做法是在驱动内部维护轻量级的内存监控结构(例如通过 Procfs 或 Debugfs 的通用统计节点导出),记录运行时的关键指标:

  • Total allocated size 与 Total freed size(历史累计申请/释放字节数)
  • Allocation calls 与 Free calls(调用次数)
  • Current dynamic usage(当前净动态占用 = 累计申请 - 累计释放)
# 示例:节点名称由具体驱动决定
$ cat /proc/<driver>/mem_stats
Total allocated size: <accumulated_allocated_bytes>
Total freed size:     <accumulated_freed_bytes>
Allocation calls:     <allocation_count>
Free calls:           <free_count>
Current tracked usage:<allocated_minus_freed>

在空闲、打流和断开重连时分别记录这些指标,再结合分配位置分析波动来源。未经过监控接口的分配不会出现在这里,需要说明统计范围。


2. Linux 平台 Wi-Fi 驱动内存剖析

在 Linux 系统下,Wi-Fi 驱动通常以内核模块(.ko)形式存在,依赖内核的内存管理子系统(Slab/Slub、Buddy 分配器)以及网络子系统的 sk_buff。

2.1 静态内存分布

以经过归一化的嵌入式 Linux Wi-Fi USB 驱动样例为例:

内存段 大小(ELF 解析) 说明
.text 数百 KB 量级 驱动核心逻辑、802.11 状态机、MAC/HIF 控制指令等代码
.data 数十 KB 以内 已初始化的全局/静态变量、配置表
.bss 与预分配策略相关 未初始化的全局变量与静态缓冲区
实际加载占用 (lsmod) 通常高于 ELF 段简单求和 包含页对齐、模块元数据及其他运行时段

为什么 lsmod 显示的大小通常大于段之和? 内核在 insmod 加载模块时,需要按页对齐分配只读段(代码)与可读写段,同时会保留 struct module 结构、导出符号表、异常表以及模块初始化段(.init.text 等在初始化完成后会释放但加载时仍占空间)。

2.2 动态内存开销与拆解

Linux 下 Wi-Fi 驱动的动态内存主要由 协议常驻控制块、STA 节点上下文 和 收发数据包队列(skb) 组成:

内存类型 典型开销 说明
基础常驻对象 (Base Alloc) 数百 KB 量级 驱动核心管理结构、网卡上下文、HIF 队列描述符、认证状态机等
USB TX/RX skb 缓冲池 数十至数百 KB 预分配或飞行中的 USB URB 与网络数据帧缓冲
AP 模式单 STA 上下文 数十 KB / STA 包含连接上下文、重排序队列、密钥与速率统计

汇总对比:

  • STA 工作模式:基础常驻对象 + 接口缓冲池;
  • AP 工作模式:STA 模式基础开销 + $N \times$ 单 STA 上下文。

在这类实现中,动态内存可能成为运行期的大头,因此需要同时观察基础常驻、业务峰值与连接数增长曲线。


3. RTOS 平台 Wi-Fi 驱动内存剖析

微控制器上的 RTOS 驱动常使用静态缓冲区或预分配内存池,以控制运行时分配开销和碎片。RTOS 并不只对应静态分配,仍要查看目标实现。

3.1 静态段分布

下面列出 RTOS 驱动静态库中常见的段占用来源:

内存段 大小 核心构成与特点
.text 数百 KB 量级 协议栈、MLME、认证、安全算法与 HIF 通信代码
.data 数十 KB 量级 可能包含内嵌固件数组和初始化数据
.bss 数百 KB 量级 常由预分配内存池和静态包缓冲主导
总计 与功能裁剪和池配置相关 以最终链接 ELF 与 Map 文件为准

3.2 各模块段大小透视

通过对静态库内部目标文件进行分类,可以进一步看出空间分布:

  1. 内存池模块:
    • 大块 .bss 通常来自静态包池和对象池。这样可以减少系统堆碎片,但会提高固件的常驻 RAM 基线。
  2. 固件内嵌模块:
    • 没有文件系统时,芯片固件可能以 C 数组形式编入镜像,并在启动阶段通过 SDIO、SPI 或 USB 下发。
  3. 加密与安全模块:
    • 认证、哈希与对称加密算法通常占据较明显的代码空间,应结合产品安全要求决定是否裁剪旧协议。
  4. 802.11 核心与收发处理:
    • MLME、扫描、AP、TX/RX 与 HIF 等模块构成主要协议和数据面代码。

4. 内存泄漏与稳定性排查方法

正常收发也会让内存波动,因此单次快照不足以判定泄漏。可以重复相同操作,观察业务结束后的占用是否回到相近水平。

内存使用量
  ^
  |        /--\  /--\  [高吞吐打流峰值]
  |       /    \/    \
  |      /            \
  |     /              \______________ [业务停止后正常回落至基线]
  |    /
  |   /               ---------------- [异常:基线被抬高,未回收]
  |  /               /
  | /               /
  +-------------------------------------> 时间

4.1 压测前后的基线比对

  • 测试步骤:
    1. 启动驱动并建立连接,记录基线动态内存 $M_0$。
    2. 使用 iperf 进行持续 1~2 小时的大流量双向打流,观察动态内存峰值 $M_{\text{peak}}$ 是否收敛(不应无节制上涨)。
    3. 停止打流,等待 1~2 分钟让缓存与重传队列超时回收,记录当前内存 $M_1$。
  • 判定标准:
    • 若 $M_1 \approx M_0$:本次测试中,已统计的分配大致回到基线,仍不能据此证明所有路径都没有泄漏。
    • 若 $M_1 > M_0$,且重复测试后持续上涨:检查缓存保留策略、skb 或描述符回收,以及引用计数是否完整。

4.2 客户端频繁上下线测试(AP 模式)

  • 测试步骤:
    • 编写脚本让 STA 客户端以 1 秒一次的频率反复关联(Associate)、认证(Auth)、断开(Disassociate)。
  • 常见隐患:
    • AP 侧为每个 STA 分配的连接上下文(如重排序 BA 窗口队列、密钥上下文、DHCP Lease 结构)在异常离线时未走完清理流程,表现为每次重连后基线持续抬高。

5. 驱动内存优化与裁剪策略

确定主要占用来源后,可以分别检查功能裁剪和缓冲池配置。

5.1 静态内存裁剪

  1. 废弃算法与协议精简:
    • 在不需要兼容旧安全协议的产品中,可按安全要求裁剪 WEP、TKIP、ARC4 及过时的 Ad-hoc 模式,具体收益以链接 Map 为准。
  2. 固件加载解耦(RTOS 场景):
    • 若系统具备外部 Flash 或存储分区,可将固件二进制从常驻 RAM 数组移出,改为启动时从存储分区读取并通过主机接口下发。
  3. 剥离调试与 Proc 节点:
    • Release 编译选项中关闭冗余字符串、详细统计结构与调试节点,可减少代码和常驻数据空间。

5.2 动态缓冲与数据面优化

  1. 按需配置 STA 容量与队列深度:
    • 通用驱动通常默认支持 8~16 个 STA 连接,并在初始化阶段为每个槽位预分配连接控制块与 BA(Block Ack)接收重排序队列;
    • 在单品 IPC 等轻量场景下,将 MAX_STA_NUM(如从 16 缩减至 4)对于预分配架构可直接减少常驻内存;对于按需动态分配架构,则能有效限制连接风暴时的内存峰值。
  2. 数据面环形队列复用(零拷贝):
    • 高频收发路径上的频繁分配和释放可能增加开销,可以先统计调用量和耗时,再判断是否需要对象池。
    • 采用预分配固定长度的 Ring Buffer 与描述符环,数据帧通过共享内存直接上送网络协议栈(如 lwIP 的 pbuf 直接包裹接收数据,避免额外 memcpy)。
  3. 动态收缩的自适应 Buffer Pool:
    • 可考虑在低负载时缩小池容量、负载增加时扩容;收缩时机要结合流量变化和分配失败风险,避免反复扩缩。

6. 内存报告应记录什么

报告中需要说明统计口径、运行状态和占用来源。可以按下面的式子整理项目,但预分配在静态段中的缓冲池不能再计入动态占用:

\[\text{总内存占用} = \underbrace{\text{代码段 (.text)} + \text{数据段 (.data/.bss)}}_{\text{静态基础成本}} + \underbrace{\text{常驻控制结构} + \text{缓冲池 (TX/RX Ring)} + N \times \text{STA 节点}}_{\text{动态业务成本}}\]

同时记录驱动版本、功能开关、连接数、队列深度和测试流量,保留空闲、峰值以及业务结束后的数据。这样才能比较两次构建,判断节省来自功能裁剪、缓冲配置还是资源回收变化。