任何网络协议栈,从数据层面看,本质都是发送端与接收端之间不间断的“打包”与“拆包”过程。发送端从应用层承接数据(payload),协议栈根据通信目的为其逐层添加协议头部(header),最终交付底层驱动发送至传输介质。接收端则执行逆操作:底层驱动从介质获取数据帧并上报协议栈,协议栈逐层剥离头部,直至将原始数据还原给应用层。因此,减少数据跨层传递带来的时间和空间开销,是提升协议栈性能的核心。

在LwIP中,使用 pbuf 这种数据结构来描述一个数据包。本文将重点研究这个数据结构。

一、pbuf的结构体

/* pbuf.h */
struct pbuf {
  //指向下一个pbuf,当网络数据包很大时,如果使用的是PBUF_POOL 会把这个网络数据包交给多个pbuf链在一起进行管理。
  struct pbuf *next;

  //指向数据区域
  void *payload;

  //当前pbuf及后续pbuf的数据区域长度的和
  u16_t tot_len;

  //当前pbuf的数据区域长度
  u16_t len;

  //pbuf的类型 可以是PBUF_RAM、PBUF_POOL 、PBUF_ROM、PBUF_REF中的一种
  u8_t type_internal;

  //flags
  u8_t flags;

  //被引用的次数,当有一个指针指向这个pbuf时,ref就会jia'yi
  LWIP_PBUF_REF_T ref;

  //对于接收的数据包,指向接收网卡(netif)的索引
  u8_t if_idx;

};

二、pbuf的类型

根据前面pbuf的结构体,我们知道pbuf有四种类型。在LwIP中,这四种类型由一个枚举类型的数据结构定义它们。

/* pbuf.h */
typedef enum {

  PBUF_RAM = (PBUF_ALLOC_FLAG_DATA_CONTIGUOUS | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP),
  
  PBUF_ROM = PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF,
  
  PBUF_REF = (PBUF_TYPE_FLAG_DATA_VOLATILE | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF),

  PBUF_POOL = (PBUF_ALLOC_FLAG_RX | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL)
} pbuf_type;

1、PBUF_RAM

PBUF_RAM类型的pbuf在LwIP中用的比较多,由内存堆分配而来。一般协议栈在发送数据时,就会申请这种类型的pbuf。这种类型的pbuf包含pbuf结构体信息和数据,是一段连续的内存。当然,PBUF_RAM在申请时,也会预留各个协议层级的头部信息区域(pbuf layer)。 PBUF_RAM 的示意图如下图所示

2、PBUF_POOL

PBUF_POOL类型和PBUF_RAM类型的pbuf都是差不多的,其pbuf的结构体信息和数据同样是存在连续的内存块中。不同的是,PBUF_POOL的内存来自于内存池,在网卡接收时,就会使用这种类型的pbuf来保存接收到的数据。

协议栈在系统内存池初始化时,会初始化以下两个内存池。

 /* memp_std.h */
 LWIP_MEMPOOL(PBUF, MEMP_NUM_PBUF, sizeof(struct pbuf),"PBUF_REF/ROM")
 LWIP_PBUF_MEMPOOL(PBUF_POOL,PBUF_POOL_SIZE,PBUF_POOL_BUFSIZE,"PBUF_POOL")

MEMP_PBUF内存池是专门用于存放pbuf数据结构的内存池,主要用于PBUF_ROM、PBUF_REF类型的pbuf,其大小为sizeof(struct pbuf),内存块的数量为MEMP_NUM_PBUF;

而MEMP_PBUF_POOL则包含pbuf结构与数据区域,也就是PBUF_POOL类型的pbuf。大小为PBUF_POOL_BUFSIZE,和TCP_MSS有关,一般设置为1460(TCP_MSS) + 40 + 14 ;内存块的数量为PBUF_POOL_SIZE。

如果使用PBUF_POOL类型的pbuf,则碰到大的数据包时,会使用多个pbuf链在一起进行管理。

3、PBUF_REF、PBUF_ROM

PBUF_ROM类型和PBUF_ROM的pbuf,它们的长度等于pbuf数据结构(sizeof(struct pbuf)),从MEMP_PBUF中分配得到。

其中PBUF_ROM类型的pbuf,它的数据区域(payload)指向了ROM中的一段区域;而PBUF_RAM类型的pbuf,它的数据区域指向的是RAM中的一段区域。

二、pbuf的使用

1、pbuf_alloc()

pbuf的申请有两个很重要的参数,一个就是前面提到的pbuf的类型;另外一个则是pbuf在协议栈哪一层被申请,这个参数的目的在于,只有知道了pbuf被申请的层级,才好在pbuf中预留协议的头部信息长度。例如在传输层申请的pbuf,我们需要预留传输层协议头部+网络层头部长度+链路层头部长度;而在网络层申请的pbuf,只需要预留网络层头部长度+链路层头部长度。

在LwIP中,这个参数使用枚举类型定义。

typedef enum {
  //传输层预留长度 = 链路头部信息长度 + 网络层头部信息长度 + 传输层头部信息长度
  PBUF_TRANSPORT = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN,
  //网络层预留长度 = 链路头部信息长度 + 网络层头部信息长度 
  PBUF_IP = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN,
  //链路层预留长度 = 链路头部信息长度 
  PBUF_LINK = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN,

  PBUF_RAW_TX = PBUF_LINK_ENCAPSULATION_HLEN,

  PBUF_RAW = 0
} pbuf_layer;

知道了这两个关键参数,我们一起来看看pbuf_alloc这个函数的实现方式。

/* pbuf.c */
struct pbuf *
pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{
  struct pbuf *p;
  //计算需要为协议头部信息预留的空间
  u16_t offset = (u16_t)layer;  

  
  switch (type) {

    //如果需要申请的pbuf类型是ref和rom 不需要为其分配数据区域的空间,只需要分配pbuf结构体大小的空间即可
    case PBUF_REF: /* fall through */
    case PBUF_ROM:
      p = pbuf_alloc_reference(NULL, length, type);
      break;

    //如果类型是PBUF_POOL
    case PBUF_POOL: {
      struct pbuf *q, *last;
      u16_t rem_len; /* remaining length */
      p = NULL;
      last = NULL;
      rem_len = length;
      do {   //循环 内存池中的每个内存块能够管理的数据长度有限
        u16_t qlen;
        //从pbuf_pool内存池中申请一个内存块
        q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
        if (q == NULL) {
          PBUF_POOL_IS_EMPTY();
          /* free chain so far allocated */
          if (p) {
            pbuf_free(p);
          }
          /* bail out unsuccessfully */
          return NULL;
        }
        //计算当前申请到的内存块能装多少数据
        qlen = LWIP_MIN(rem_len, (u16_t)(PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset)));
        //初始化pbuf结构体
        pbuf_init_alloced_pbuf(q, LWIP_MEM_ALIGN((void *)((u8_t *)q + SIZEOF_STRUCT_PBUF + offset)),
                               rem_len, qlen, type, 0);
        LWIP_ASSERT("pbuf_alloc: pbuf q->payload properly aligned",
                    ((mem_ptr_t)q->payload % MEM_ALIGNMENT) == 0);
        LWIP_ASSERT("PBUF_POOL_BUFSIZE must be bigger than MEM_ALIGNMENT",
                    (PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset)) > 0 );
        //如果当前节点是第一个节点,那么这个pbuf节点就是头节点
        if (p == NULL) {
          /* allocated head of pbuf chain (into p) */
          p = q;
        } else {
        //已经有头节点了,那么现在的节点都是后续节点,我们需要让last节点的next指向当前节点
          /* make previous pbuf point to this pbuf */
          last->next = q;
        }
        //记录一下last节点,便于与后续节点建立关系
        last = q;
        rem_len = (u16_t)(rem_len - qlen);
        offset = 0;   //关键点 offset直接清0了 意味着后面的节点不用再预留头部信息的空间了
      } while (rem_len > 0); //直到所有的数据都有地方可以放
      break;
    }
    case PBUF_RAM: {
      // 计算实际需要的 payload 大小 = 头部预留(offset) + 数据长度(length) 并做内存对齐
      mem_size_t payload_len = (mem_size_t)(LWIP_MEM_ALIGN_SIZE(offset) + LWIP_MEM_ALIGN_SIZE(length));
      // 计算总分配大小 = pbuf结构体大小 + payload_len
      mem_size_t alloc_len = (mem_size_t)(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF) + payload_len);

      /* bug #50040: Check for integer overflow when calculating alloc_len */
      if ((payload_len < LWIP_MEM_ALIGN_SIZE(length)) ||
          (alloc_len < LWIP_MEM_ALIGN_SIZE(length))) {
        return NULL;
      }

      // 直接从堆中分配内存 区别于内存池,内存堆可以一次性分配很大的空间
      p = (struct pbuf *)mem_malloc(alloc_len);
      if (p == NULL) {
        return NULL;
      }
      // 初始化pbuf
      pbuf_init_alloced_pbuf(p, LWIP_MEM_ALIGN((void *)((u8_t *)p + SIZEOF_STRUCT_PBUF + offset)),
                             length, length, type, 0);
      LWIP_ASSERT("pbuf_alloc: pbuf->payload properly aligned",
                  ((mem_ptr_t)p->payload % MEM_ALIGNMENT) == 0);
      break;
    }
    default:
      LWIP_ASSERT("pbuf_alloc: erroneous type", 0);
      return NULL;
  }
  LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_alloc(length=%"U16_F") == %p\n", length, (void *)p));
  return p;
}

2、pbuf_free()

有申请,对应就有释放。当某一pbuf中的数据已经被内核处理完时,需要对pbuf进行释放,如果pbuf一直得不到释放,将导致内存泄漏,后续将无法再申请到可用的pbuf。

pbuf_free函数将会递减pbuf()的引用次数(pbuf->ref),当pbuf的引用次数减至0时,pbuf所对应的内存空间将会被完全释放。pbuf_free函数可以处理某一pbuf或pbuf链。

下面来看看pbuf_free的代码

u8_t
pbuf_free(struct pbuf *p)
{
  u8_t alloc_src;
  struct pbuf *q;
  u8_t count;

  if (p == NULL) {
    LWIP_ASSERT("p != NULL", p != NULL);
    LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
                ("pbuf_free(p == NULL) was called.\n"));
    return 0;
  }
  LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free(%p)\n", (void *)p));

  PERF_START;

  count = 0;
  //当待释放的pbuf不为空时
  while (p != NULL) {
    LWIP_PBUF_REF_T ref;
    SYS_ARCH_DECL_PROTECT(old_level);
    /* Since decrementing ref cannot be guaranteed to be a single machine operation
     * we must protect it. We put the new ref into a local variable to prevent
     * further protection. */
    SYS_ARCH_PROTECT(old_level);
    /* all pbufs in a chain are referenced at least once */
    LWIP_ASSERT("pbuf_free: p->ref > 0", p->ref > 0);
    //递减pbuf的引用次数
    ref = --(p->ref);
    SYS_ARCH_UNPROTECT(old_level);
    /* this pbuf is no longer referenced to? */
    if (ref == 0) {
      //当引用次数减至0时,说明此时pbuf所对应的内存空间可以被释放了 记录一下当前pbuf的next
      q = p->next;
      LWIP_DEBUGF( PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free: deallocating %p\n", (void *)p));
      //获取pbuf的类型
      alloc_src = pbuf_get_allocsrc(p);
      //根据pbuf的不同,选择合适的释放方式
#if LWIP_SUPPORT_CUSTOM_PBUF
      /* is this a custom pbuf? */
      if ((p->flags & PBUF_FLAG_IS_CUSTOM) != 0) {
        struct pbuf_custom *pc = (struct pbuf_custom *)p;
        LWIP_ASSERT("pc->custom_free_function != NULL", pc->custom_free_function != NULL);
        pc->custom_free_function(p);
      } else
#endif /* LWIP_SUPPORT_CUSTOM_PBUF */
      {
        /* is this a pbuf from the pool? */
        if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL) {
          memp_free(MEMP_PBUF_POOL, p);
          /* is this a ROM or RAM referencing pbuf? */
        } else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF) {
          memp_free(MEMP_PBUF, p);
          /* type == PBUF_RAM */
        } else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP) {
          mem_free(p);
        } else {
          /* @todo: support freeing other types */
          LWIP_ASSERT("invalid pbuf type", 0);
        }
      }
      //被释放的pbuf数量加一
      count++;
      /* proceed to next pbuf */
      //开始处理下一个pbuf
      p = q;
      /* p->ref > 0, this pbuf is still referenced to */
      /* (and so the remaining pbufs in chain as well) */
    } else {
      LWIP_DEBUGF( PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free: %p has ref %"U16_F", ending here.\n", (void *)p, (u16_t)ref));
      /* stop walking through the chain */
      p = NULL;
    }
  }

  PERF_STOP("pbuf_free");
  //将释放的pbuf数量返回
  return count;
}

结合上述代码,我们假设现在对以下四条pbuf的链分别进行释放,数字代表pbuf链表中各个节点的引用次数:

1->2->3 becomes ...1->3    // 第一个释放后,指向第二个
3->3->3 becomes 2->3->3    // 只释放了第一个
1->1->2 becomes ......1    // 释放前两个
2->1->1 becomes 1->1->1    // 释放第一个
1->1->1 becomes .......    // 全部释放

3、pbuf中的常见函数接口

pbuf_realloc(struct pbuf *p, u16_t new_len)  用于将pbuf缩减为new_len的长度

pbuf_header(struct pbuf *p, s16_t header_size_increment)  之前说过,网络数据包的控制是双向的,在发包时,对网络数据包附加头部信息;在收包时,移除头部信息还原应用数据。pbuf_header函数可以用于调整pbuf的payload指针,实现在数据包前面添加或删除协议头。当header_size_increment小于0时,说明需要将payload指针向后移动,移除掉协议头部信息;当header_size_increment大于0时,说明需要将payload指针向前移动,添加协议头部信息

除此之外,pbuf的操作接口还有很多,如pbuf_copy,pbuf_take等等,后面在遇到的时候再进行介绍。