第3章 内存管理移植:pbuf结构解析、内存池与内存堆配置、零拷贝技术实现
内存管理,说白了就是协议栈的"后勤部"。数据包在协议栈里层层传递,从网卡驱动一路跑到应用层,中间要经过无数次的内存分配、拷贝、释放。搞不好,系统就卡死在内存碎片上了。
我个人习惯,移植协议栈的第一步不是调网络接口,而是先把内存管理摸透。为什么?因为后面所有的问题,十有八九都能追溯到内存配置不合理。
3.1 pbuf结构:协议栈的数据快递箱
先看pbuf。它是lwIP里最基础的数据结构,所有网络数据都装在pbuf里传递。你想想看,一个TCP数据包从网卡收进来,经过IP层、TCP层,最后到应用层,每一层都可能要修改数据头。如果每层都重新分配内存、拷贝数据,那效率就太低了。
pbuf的设计思路是:用链表把多个内存块串起来,每个内存块可以指向不同的位置。这样上层只需要调整指针,不用搬数据。
struct pbuf {
struct pbuf *next; // 指向下一个pbuf节点
void *payload; // 数据起始地址
u16_t tot_len; // 整个pbuf链的总长度
u16_t len; // 当前节点的数据长度
u8_t type; // 类型:PBUF_RAM, PBUF_POOL, PBUF_ROM
u8_t flags; // 标志位
u16_t ref; // 引用计数
};
这里有三个关键类型,我列个表方便对比:
| 类型 | 内存来源 | 特点 | 典型场景 |
|---|---|---|---|
| PBUF_RAM | 堆(heap) | 大小灵活,可调整 | 应用层发送数据 |
| PBUF_POOL | 内存池(pool) | 固定大小,分配快 | 网卡接收数据 |
| PBUF_ROM | 外部静态数据 | 不参与内存管理 | 常量数据、零拷贝 |
我在项目中遇到过一个问题:某个设备在高速收发数据时,系统频繁进入HardFault。查了两天才发现,是PBUF_POOL的节点大小配置太小,导致一个数据包被拆成几十个pbuf节点,链表遍历开销巨大,还把栈给撑爆了。
3.2 内存池与内存堆:两种策略,两种脾气
lWIP提供了两套内存分配机制:内存池(memp)和内存堆(mem)。它们各有各的脾气,你得摸清楚。
3.2.1 内存池(memp)
内存池是预先划分好的一块区域,切成固定大小的块。分配和释放都是O(1)复杂度,没有碎片问题。适合用在中断上下文或者对实时性要求高的地方。
配置在lwipopts.h里:
// 使能内存池
#define MEMP_USE_CUSTOM_POOLS 1
// 定义各个池的大小和数量
#define PBUF_POOL_SIZE 16 // pbuf池数量
#define PBUF_POOL_BUFSIZE 256 // 每个pbuf的数据区大小
#define TCP_PCB_NUM 5 // TCP控制块数量
#define UDP_PCB_NUM 5 // UDP控制块数量
我建议你根据实际业务场景来算这些数值。比如你的设备最多支持4个TCP连接,那TCP_PCB_NUM设成5就够了,留一个余量。设多了浪费RAM,设少了连接建立失败。
3.2.2 内存堆(mem)
内存堆就是传统的malloc/free机制。lwIP自己实现了一套,支持合并相邻空闲块,减少碎片。但分配时间不确定,不适合中断里用。
// 堆配置
#define MEM_SIZE (16 * 1024) // 堆总大小 16KB
#define MEM_ALIGNMENT 4 // 对齐字节
#define MEM_LIBC_MALLOC 0 // 使用lwIP自带堆
3.3 零拷贝技术:让数据飞一会儿
零拷贝,说白了就是尽量减少数据搬移。数据从网卡DMA进来,直接让协议栈各层"看"同一块内存,而不是每层都复制一份。
lwIP里实现零拷贝主要有三种方式:
- PBUF_REF/ROM类型:数据本身不动,pbuf只记录指针。应用层发送时,直接把用户缓冲区的地址给pbuf,协议栈只操作头。
- pbuf链式结构:一个大包拆成多个pbuf节点,每个节点指向不同的内存区域。TCP分段时,不需要重新分配内存,直接调整链表就行。
- 网卡驱动直接使用pbuf:接收数据时,DMA直接把数据写到预先分配好的PBUF_POOL里,驱动层零拷贝。
下面这张图展示了零拷贝的核心流程:
实现零拷贝的关键在于驱动层和应用层的配合。驱动接收数据时,不能自己malloc一块内存再把数据拷进去,而是直接用协议栈提供的pbuf。应用层读取数据时,通过回调函数拿到pbuf指针,用完再释放。
3.4 移植实战:配置一个稳定的内存系统
好了,理论说完了,咱们来点实际的。下面是我在一个STM32F407项目上的配置,RAM总共192KB,给协议栈分了32KB。
// lwipopts.h 关键配置
// 内存池配置
#define MEMP_NUM_PBUF 32 // pbuf池数量
#define PBUF_POOL_SIZE 20 // 数据pbuf池数量
#define PBUF_POOL_BUFSIZE 512 // 每个pbuf数据区大小
// 内存堆配置
#define MEM_SIZE (20 * 1024) // 堆大小20KB
#define MEM_ALIGNMENT 4
// TCP/UDP控制块
#define MEMP_NUM_TCP_PCB 6 // TCP控制块
#define MEMP_NUM_TCP_PCB_LISTEN 3 // 监听TCP控制块
#define MEMP_NUM_UDP_PCB 4 // UDP控制块
// 零拷贝相关
#define LWIP_NETIF_TX_SINGLE_PBUF 1 // 发送时尽量用单个pbuf
配置完以后,我习惯做两件事:
- 压力测试:用PC连续ping设备24小时,同时用上位机发大量TCP数据,观察内存是否泄漏。
- 统计监控:在代码里加几个计数器,定期打印memp的可用块数和mem的剩余空间。如果数值持续下降,说明有泄漏。
最后说一句,内存管理移植没有标准答案。不同的硬件平台、不同的业务场景,配置参数天差地别。我的建议是:先按保守值配,跑起来以后用统计工具看实际使用量,再逐步调整。别一上来就想追求极致,稳定第一。