3. LwIP 核心数据结构:pbuf、netif、tcp_pcb 深度解析
做嵌入式网络开发,说白了就是在跟这三个结构体打交道。pbuf、netif、tcp_pcb,它们是 LwIP 的骨架。我见过不少新手,代码调不通就急着改参数,其实根子往往是对这三个结构体理解不到位。
今天咱们就把它们拆开揉碎了讲。你想想看,搞懂了数据怎么存、网卡怎么管、连接怎么控,协议栈在你眼里就没秘密了。
3.1 pbuf:数据包的搬运工
pbuf 是 LwIP 里最基础的数据结构。它不负责“存”数据,而是负责“管”数据。为什么这么说?因为嵌入式系统内存金贵,pbuf 的设计目标就是零拷贝和灵活拼接。
3.1.1 pbuf 的类型与结构
pbuf 有四种类型,我习惯按使用场景来记:
| 类型 | 宏定义 | 数据存储位置 | 典型用途 |
|---|---|---|---|
| PBUF_RAM | 0x01 | 堆内存(连续) | 应用层发送数据 |
| PBUF_ROM | 0x02 | ROM/常量区 | 静态协议头、固件字符串 |
| PBUF_REF | 0x03 | 外部内存(用户提供) | 零拷贝场景,比如 DMA 缓冲区 |
| PBUF_POOL | 0x04 | 固定大小内存池 | 网卡接收数据、中断上下文 |
核心区别:PBUF_RAM 和 PBUF_POOL 由 LwIP 内部管理内存;PBUF_ROM 和 PBUF_REF 只维护指针,不负责释放。选错了类型,轻则内存泄漏,重则野指针崩溃。
每个 pbuf 结构体本身很小,大概 20 字节左右。它通过 next 指针连成链表,这就是所谓的“pbuf 链”。
struct pbuf {
struct pbuf *next; // 指向下一个 pbuf,形成链表
void *payload; // 指向实际数据区域的指针
u16_t tot_len; // 整个 pbuf 链的总长度
u16_t len; // 当前 pbuf 节点的数据长度
u8_t type; // 上面表格里的四种类型
u8_t flags; // 标志位,比如 PBUF_FLAG_PUSH
u16_t ref; // 引用计数,用于共享数据
};
我的习惯:调试时先打印 tot_len 和 len。如果 tot_len 不等于各节点 len 之和,那链表一定断了。我曾经花了一整天查一个 MQTT 丢包问题,最后发现是 pbuf 链在某个中间节点被意外释放了。
3.1.2 pbuf 操作避坑指南
操作 pbuf 有几个铁律,违反了就是坑:
- 申请时预留头部空间:用
pbuf_alloc(PBUF_TRANSPORT, size, PBUF_RAM),LwIP 会自动在 payload 前面留出协议头空间。别自己算偏移,容易算错。 - 引用计数要配对:
pbuf_ref()和pbuf_free()必须成对出现。我曾经在回调函数里多调了一次pbuf_free(),结果同一个 pbuf 被释放了两次,系统直接 hard fault。 - 不要直接修改 PBUF_ROM 的数据:它指向只读区域,写进去就是非法访问。
我曾经踩过的坑:在中断服务函数里用了 pbuf_alloc(PBUF_RAM, ...)。RAM 类型的分配可能触发内存碎片整理,耗时不可控。中断里只能用 PBUF_POOL,它是 O(1) 分配,不会阻塞。
3.2 netif:网卡的抽象层
netif 就是 LwIP 眼里的“网卡”。不管你的硬件是 SPI 接口的 ENC28J60,还是 RMII 接口的 STM32 内置 MAC,在 LwIP 里都统一成一个 struct netif。
3.2.1 netif 的核心字段
struct netif {
struct netif *next; // 链表,支持多网卡
ip_addr_t ip_addr; // IP 地址
ip_addr_t netmask; // 子网掩码
ip_addr_t gw; // 默认网关
netif_input_fn input; // 数据接收回调(底层→协议栈)
netif_output_fn output; // 数据发送回调(协议栈→底层)
netif_linkoutput_fn linkoutput; // 链路层发送(ARP 解析后)
void *state; // 用户自定义上下文,比如 SPI 句柄
u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; // MAC 地址
u8_t hwaddr_len; // MAC 地址长度
u16_t mtu; // 最大传输单元
u8_t flags; // 标志位,如 NETIF_FLAG_UP、NETIF_FLAG_LINK_UP
};
这里我重点说三个回调函数:
- input:底层驱动收到数据后调用它,把 pbuf 喂给协议栈。注意,这个函数可能在中断里执行。
- output:IP 层要发包时调用。它负责把 pbuf 交给底层驱动。如果目标 IP 需要 ARP 解析,LwIP 会先走
linkoutput发 ARP 请求。 - linkoutput:直接操作 MAC 层发送,不经过 IP 层。ARP 包就走这个路径。
性能关键:如果你的网卡支持零拷贝,可以在 output 回调里直接操作 pbuf 的 payload 指针,避免数据搬移。我优化过一个项目,把吞吐量从 2Mbps 提到了 7Mbps,靠的就是这个。
3.2.2 多网卡场景下的 netif 管理
LwIP 用链表管理所有 netif。默认第一个添加的网卡是默认路由。如果你有两个网卡——比如一个以太网、一个 4G 模块——需要手动设置路由优先级。
// 添加网卡
struct netif eth0, wwan0;
netif_add(ð0, &ipaddr, &netmask, &gw, NULL, ethernet_init, ethernet_input);
netif_set_default(ð0); // 以太网作为默认路由
// 4G 模块只处理特定网段
netif_add(&wwan0, &ipaddr_wwan, &netmask_wwan, &gw_wwan, NULL, wwan_init, wwan_input);
// 不设置默认路由,只通过路由表匹配
我的建议:多网卡场景下,一定要实现 netif_find_by_ip() 的替代逻辑。LwIP 默认的路由选择很朴素,就是遍历链表找第一个匹配的。如果你的业务需要策略路由,得自己写路由钩子。
3.3 tcp_pcb:TCP 连接的灵魂
tcp_pcb 是 LwIP 里最复杂的数据结构。一个 TCP 连接的所有状态、缓冲区、回调函数都挂在它上面。我刚开始看源码时,被它几十个字段吓到了。后来发现,核心就三块:连接状态、收发缓冲区、事件回调。
3.3.1 tcp_pcb 的核心字段分组
| 分组 | 关键字段 | 作用 |
|---|---|---|
| 连接标识 | local_ip, remote_ip, local_port, remote_port | 五元组,唯一标识一个连接 |
| 状态机 | state (CLOSED, SYN_SENT, ESTABLISHED 等) | TCP 状态,决定协议栈行为 |
| 发送窗口 | snd_nxt, snd_wnd, snd_wl1, snd_wl2 | 控制发送速率和流量 |
| 接收窗口 | rcv_nxt, rcv_wnd, rcv_ann_wnd | 通告对端我能收多少数据 |
| 回调函数 | recv, sent, poll, err | 应用层事件通知 |
| 缓冲区 | unsent, unacked, ooseq | 待发送、已发送未确认、乱序数据 |
这里我特别想说说缓冲区链表。unsent 是等待发送的数据,unacked 是已经发出但还没收到 ACK 的。这两个链表的管理直接决定了 TCP 的吞吐量。
我曾经踩过的坑:在 MQTT 应用中,我设置了很小的 TCP_SND_BUF(2KB),结果发布 10KB 的消息时,LwIP 要分 5 次发送,每次都要等 ACK。后来我把发送缓冲区调到 8KB,吞吐量直接翻倍。记住,MQTT 的 QoS 1/2 依赖 TCP 的确认机制,缓冲区太小会严重拖慢速度。
3.3.2 tcp_pcb 的生命周期管理
一个 TCP 连接从创建到销毁,tcp_pcb 经历了这些阶段:
- 创建:
tcp_new()分配 pcb,状态为 CLOSED。 - 绑定:
tcp_bind()绑定本地端口。 - 连接/监听:客户端调
tcp_connect(),服务端调tcp_listen()。注意,tcp_listen()会创建一个新的struct tcp_pcb_listen,它比普通 pcb 更轻量。 - 数据传输:通过回调函数驱动。
tcp_write()只是把数据拷贝到 unsent 链表,真正的发送由 LwIP 的定时器触发。 - 关闭:
tcp_close()或tcp_abort()。前者走四次挥手,后者直接发 RST。
关键点:千万不要在回调函数里直接 tcp_close()。正确的做法是设置一个标志位,在主循环里关闭。因为回调函数可能在 LwIP 的上下文里执行,直接关闭会导致 pcb 被意外释放,其他回调还在用这个指针。
3.4 三者的协作关系
这三个结构体不是孤立的。我画了一张图,帮你理清它们怎么配合:
数据流动的路径很清晰:应用层调用 tcp_write(),数据被封装成 pbuf 挂在 tcp_pcb 的发送链上。LwIP 内核定时器触发发送,pbuf 经过 IP 层、ARP 层,最终通过 netif 的 output 回调交给驱动。接收路径反过来,驱动通过 netif 的 input 回调把 pbuf 喂给协议栈,协议栈解析后挂在 tcp_pcb 的接收链上,最后通过 recv 回调通知应用层。
性能调优的切入点:如果你发现 MQTT 消息延迟高,先查 pbuf 链是不是太长(每个节点开销大),再查 tcp_pcb 的发送窗口是不是被填满了,最后看 netif 的 output 回调里有没有不必要的内存拷贝。这三步走完,90% 的性能问题都能定位。
3.5 小结:三个结构体,一个世界观
pbuf 管数据怎么存和传,netif 管数据从哪个口进出,tcp_pcb 管连接怎么维护。搞懂这三者,LwIP 在你眼里就是透明的。下次遇到网络问题,别急着上网搜答案,先问问自己:是 pbuf 分配失败了?还是 netif 没起来?还是 tcp_pcb 的状态机卡住了?