4. LwIP 内存管理:内存池与内存堆的配置与调优
内存管理,说白了就是 LwIP 的命根子。你想想看,一个嵌入式设备总共就那么点 RAM,协议栈收发包、建连接、处理数据,全得在这巴掌大的地方折腾。搞不好就内存泄漏,搞不好就碎片爆炸,设备跑几天就挂了。
我个人习惯,拿到一个新平台,第一件事不是调网络参数,而是先把内存管理摸透。这章咱们就聊聊 LwIP 的两种内存分配方式——内存池和内存堆,以及怎么根据实际场景去配、去调。
4.1 内存池(PBUF_POOL)
内存池,说白了就是预先切好的一堆固定大小的内存块。LwIP 里最常用的就是 PBUF_POOL 类型的 pbuf。每个 pbuf 大小固定,分配和释放都是 O(1) 的,速度极快,而且不会产生碎片。
我在项目中遇到过一个问题:某个设备跑 MQTT,平时好好的,一旦上报数据频率高了,就频繁丢包。查了半天,发现是内存池太小,申请不到 pbuf 了。嗯,这就是典型的池子不够用。
4.1.1 配置参数
内存池的大小和数量,由 lwipopts.h 里的两个宏控制:
// 每个内存池 pbuf 的大小(字节)
#define PBUF_POOL_SIZE 16
// 内存池中 pbuf 的数量
#define PBUF_POOL_BUFSIZE 256
这里有个容易踩的坑:PBUF_POOL_BUFSIZE 是每个 pbuf 的有效载荷大小,但 LwIP 内部还会加上协议头开销。比如你配了 256 字节,实际每个 pbuf 占用的内存可能是 256 + 几十字节的头部。
4.1.2 如何估算池子大小
我一般按这个公式估算:
| 场景 | 推荐 PBUF_POOL_SIZE | 说明 |
|---|---|---|
| 低负载 MQTT(几秒发一次) | 8 ~ 16 | 够用,省 RAM |
| 中等负载(每秒几十个包) | 32 ~ 64 | 留点余量 |
| 高负载(视频流、大量并发) | 128 ~ 256 | 根据实际压测调整 |
lwip_stats.pbuf.used 的最大值。跑几天看看峰值用了多少池子,然后按峰值 + 20% 余量配置。我习惯在调试阶段把这个统计打开,上线前再关掉。
4.2 内存堆(PBUF_RAM)
内存堆就不一样了。它是一大块连续内存,按需分配,大小灵活。像 TCP 的发送窗口、大块数据缓存,通常都用 PBUF_RAM 类型的 pbuf。
但堆有个致命问题——碎片。频繁分配释放,内存会碎成一片一片的。你明明还有 10KB 空闲,但最大连续块只有 1KB,申请 2KB 就失败。
4.2.1 堆的大小配置
// 内存堆的总大小(字节)
#define MEM_SIZE 4096
这个值怎么定?我一般这么算:
- 先算协议栈本身的开销:TCP 控制块、路由表、ARP 表等,大概几百字节到 1KB。
- 再加应用层需求:比如 MQTT 的发送缓冲区、接收缓冲区。
- 最后留 20%~30% 余量,应对突发流量。
举个例子,一个 MQTT 客户端,最大发 2KB 的消息,收 2KB 的消息,再加上协议栈开销:
MEM_SIZE = 1024(协议栈) + 2048(发送) + 2048(接收) + 1024(余量) = 6144 字节
4.2.2 堆的分配策略
LwIP 默认用 首次适应(First Fit)算法。说白了就是从头扫描,找到第一个够大的空闲块就分配。速度快,但容易产生碎片。
如果你对实时性要求高,可以换成 最佳适应(Best Fit)或 最差适应(Worst Fit)。不过我个人建议,除非你特别清楚自己在做什么,否则别动分配算法。默认的 First Fit 在大多数场景下够用了。
4.3 内存池 vs 内存堆:怎么选?
很多新手会问:到底用池还是用堆?我一般这么区分:
| 维度 | 内存池 | 内存堆 |
|---|---|---|
| 分配速度 | 极快(O(1)) | 较慢(O(n)) |
| 碎片问题 | 无碎片 | 容易碎片 |
| 灵活性 | 固定大小 | 任意大小 |
| 适用场景 | 小包、高频收发 | 大块数据、变长数据 |
| 典型用途 | 接收 pbuf、ARP 包 | TCP 窗口、应用缓存 |
说白了,小包走池,大包走堆。MQTT 的 PUBLISH 消息如果小于 256 字节,完全可以用池;如果消息体有几十 KB,那就得用堆。
4.4 调优实战:一个 MQTT 场景
我记得有一次帮客户调一个智能网关。设备跑 MQTT,上报传感器数据,每条消息 200 字节左右,每秒上报 10 条。客户反映设备运行 3 天后必死机。
我一看配置:
PBUF_POOL_SIZE = 8
PBUF_POOL_BUFSIZE = 128
MEM_SIZE = 2048
问题很明显:
- 池子只有 8 个,每个才 128 字节。200 字节的消息需要 2 个 pbuf 链起来,一次上报就要占 2 个池子。10 条/秒就是 20 个池子,池子早被抢光了。
- 堆只有 2KB,TCP 窗口稍微大点就爆了。
我改成:
PBUF_POOL_SIZE = 32
PBUF_POOL_BUFSIZE = 256
MEM_SIZE = 8192
同时把 MQTT 的发送缓冲区从堆里分配,改成用池子(因为消息大小固定)。改完后设备连续跑了 30 天,再没死过机。
lwip_stats 里的内存使用情况。我习惯用 stats_init() 初始化统计,然后定期打印 lwip_stats.mem.used 和 lwip_stats.pbuf.used。
4.5 内存管理架构图
下面这张图,是我自己画的内存管理决策流程。每次调优时,我就照着这个思路走:
4.6 总结几个要点
- 内存池优先:能走池子就别走堆。池子快、无碎片、好调试。
- 堆要留余量:MEM_SIZE 至少是理论最大需求的 1.3 倍。
- 统计是眼睛:lwip_stats 里的 mem.used、pbuf.used、pbuf.lost 是调优的导航仪。
- 别贪多:池子不是越多越好。配多了浪费 RAM,配少了丢包。找到平衡点。