Will Will's Blog

lwIP学习笔记-源码

2017-04-20
Will

lwIP的源码可以到官网的Source Code页面下载,下载v2.0.1 release版本,关于lwIP的源码设计文档,可以参考《Design and Implementation of the lwIP tcpIP stack.pdf》,另外官网的wiki的提供给开发者的文档Architectural Rx flow,也非常值得参考。

概述

lwIP的设计与常见的一些TCP/IP协议栈有些不同,如Linux的TCP/IP协议栈,在设计上严格区分应用层和底层协议栈之间的交互,底层的协议栈与操作系统融合,是操作系统的一部分,应用层需要调用操作系统提供的接口来实现与协议栈之间的交互,而且应用层也会抽象出一层供应用层调用的TCP/IP接口,而且在内存使用上应用层和底层协议栈也是分开的,因此存在数据的copy。lwIP一般用于实时系统,没有严格的应用层与操作系统层的区分,TCP/IP协议栈和应用层可以共享内存,因此不存在数据的copy,效率更高,消耗资源更少。 lwIP在实时系统中将TCP/IP协议栈的处理放入到一个单独的任务中,应用层的处理也可以放入这一个任务,也可以单独另外创建一个任务。lwIP中所有与操作系统相关的部分全部封装成了单独的函数或宏定义,开发者在移植的时候根据具体的操作系统来实现或重定义。因此,lwIP非常容易移植到各种版本的实时操作系统上。 lwIP是针对于嵌入式实时系统设计的TCP/IP协议栈,值得去研究一下它的设计理念和源码。

Buffer and Memory Management

pbufs

src/core/pbuf.c中的pbuf_alloc()和pbuf_free()

typedef enum {
  //传输层header,如tcp/udp,如调用udp_send()
  PBUF_TRANSPORT,
  //IP层header,如调用raw_send()
  PBUF_IP,
  //数据链路层header,如调用ethernet_output()
  PBUF_LINK,
  //额外的ethernet header
  PBUF_RAW_TX,
  //网卡驱动层,如调用netif->input()
  PBUF_RAW
} pbuf_layer;

typedef enum {
  //pbuf的data存储在RAM,常用于Tx,pbuf数据结构和data分配在连续的RAM内存
  PBUF_RAM,
  //pbuf的data存储在ROM,pbuf数据结构与data分离在不同区域,pbuf->payload指向data,而且payload不会被copy
  PBUF_ROM,
  //类似于PBUF_ROM,data存储在RAM,但payload可能被复写
  PBUF_REF,
  //payload指向RAM,从内存pool中分配,一般用于Rx,类似于PBUF_RAM,pbuf数据结构和data分配在连续的RAM内存。不能用于Tx!
  PBUF_POOL
} pbuf_type;

struct pbuf *pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{
  ...

  //根据pbuf_layer的类型计算包头长度offset
  switch (layer) {
    ...
  }

  //根据pbuf_type类型具体进行内存分配
  switch (type) {
    case PBUF_POOL:
      //从内存pool中分配,调用memp_malloc()
      p = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
      ...
      //payload直接指向data,内存对齐,跳过pbuf数据结构和offset
      p->payload = LWIP_MEM_ALIGN((void *)((u8_t *)p + (SIZEOF_STRUCT_PBUF + offset)));
      ...
      //一个包可能会被分割成多个包,tot_len是整个pbuf chain长度
      p->tot_len = length;
      //此pbuf包长
      p->len = LWIP_MIN(length, PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset));
      ...
      p->ref = 1;
      r = p;
      //剩余长度
      rem_len = length - p->len;
      //循环链接成pbuf chain链表
      while (rem_len > 0) {
        //再分配一个pbuf
        q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
        ...
        //链表链接成pbuf chain
        r->next = q;
        q->tot_len = (u16_t)rem_len;
        q->len = LWIP_MIN((u16_t)rem_len, PBUF_POOL_BUFSIZE_ALIGNED);
        //payload指向data,已经没有header offset
        q->payload = (void *)((u8_t *)q + SIZEOF_STRUCT_PBUF);
        q->ref = 1;
        rem_len -= q->len;
        r = q;
      }
      break;

    case PBUF_RAM:
      //从堆内存中直接分配,pbuf数据结构和data在连续的RAM内存,没有形成pbuf chain
      p = (struct pbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) + LWIP_MEM_ALIGN_SIZE(length));
      ...
      p->payload = LWIP_MEM_ALIGN((void *)((u8_t *)p + SIZEOF_STRUCT_PBUF + offset));
      p->len = p->tot_len = length;
      p->next = NULL;
      p->type = type;
      ...
      break;

    case PBUF_ROM:
    case PBUF_REF:
      //从内存pool中分配,只分配pbuf数据结构大小的内存,payload后期再赋值,指向分离的data
      p = (struct pbuf *)memp_malloc(MEMP_PBUF);
      ...
      p->payload = NULL;
      p->len = p->tot_len = length;
      p->next = NULL;
      p->type = type;
      break;

    ...
  }

  p->ref = 1;
  p->flags = 0;

  return p;
}

通过pbuf_alloc()可见pbuf分四种类型:PBUF_POOL,PBUF_RAM,PBUF_REF,PBUF_ROM。

  • PBUF_POOL

lwIP_pbuf_pool_model

PBUF_POOL从内存pool中分配,固定长度分配,分配的每一个pbuf包都是连续内存,如果包长超过一个pbuf固定分配长度,则拆分成多个pbuf包,然后形成pbuf chain,一般用于Rx,即device drivers接收网络数据包

  • PBUF_RAM

lwIP_pbuf_ram_model

PBUF_RAM从内存堆中分配,4种类型中唯一分配方式不同的一个,非固定长度,分配在连续内存上,一般用于Tx

  • PBUF_REF & PBUF_ROM

lwIP_pbuf_ref_rom_model

PBUF_REF和PBUF_ROM分配相同,在内存pool中分配,只分配pbuf数据结构大小,payload为NULL,将来指向data内存,pbuf数据结构和data分离,常用于应用

接着再看一下pbuf_free()

/* 官方示例:根据引用次数free()前后的变化
 * 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 .......
 */
u8_t pbuf_free(struct pbuf *p)
{
  ...

  count = 0;
  while (p != NULL) {
    ...
    ref = --(p->ref);
    if (ref == 0) { //此pbuf不再被其他pbuf引用
      q = p->next;
      type = p->type;
      ...
      if (type == PBUF_POOL) {
        memp_free(MEMP_PBUF_POOL, p);
      } else if (type == PBUF_ROM || type == PBUF_REF) {
        memp_free(MEMP_PBUF, p);
      } else {
        mem_free(p);
      }
      ...
      count++;
      p = q;
    } else {
      p = NULL; //此pbuf还在被其他pbuf引用,不能free,退出
    }
  }

  return count;
}

从pbuf_free()可以看出pbuf->ref用于记录此pbuf被引用的次数,如果不再被引用了则free掉,如果还有被引用,则减少引用记录次数,不free然后退出;如果是pbuf chain,则在遇到第一个不能free掉的pbuf后直接退出,不再继续遍历。

memeory management

通过上面的pbuf源码,可以看出lwIP有2种内存分配方式,即memp_malloc()/memp_free()和mem_malloc()/mem_free()。mem_malloc()/mem_free()与我们常用的堆分配释放类似,lwIP里面主要应对不定长的Tx时的pbuf,可以使用标准c库里的堆管理,也可以使用静态内存池,也可以使用自定义的堆管理,与前面FreeRTOS的heap_4类似的堆管理,简洁高效,尽量避免内存碎片。主要看一下memp_malloc()/memp_free(),用来应对定长的pbuf,定长的内存管理更加的简洁,效率更高,不会产生内存碎片,但是可能存在浪费空间的问题(PBUF_POOL),总要有所取舍,特别在接收时使用这种内存分配策略。

#define LWIP_MEMPOOL_DECLARE(name,num,size,desc) \
  LWIP_DECLARE_MEMORY_ALIGNED(memp_memory_ ## name ## _base, ((num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size)))); \
  \ LWIP_MEMPOOL_DECLARE_STATS_INSTANCE(memp_stats_ ## name) \
  \ static struct memp *memp_tab_ ## name; \
  \ const struct memp_desc memp_ ## name = { \
    DECLARE_LWIP_MEMPOOL_DESC(desc) \
    LWIP_MEMPOOL_DECLARE_STATS_REFERENCE(memp_stats_ ## name) \
    LWIP_MEM_ALIGN_SIZE(size), \
    (num), \
    memp_memory_ ## name ## _base, \
    &memp_tab_ ## name \
  };

#define LWIP_MEMPOOL(name,num,size,desc) LWIP_MEMPOOL_DECLARE(name,num,size,desc)
#include "lwip/priv/memp_std.h"

const struct memp_desc* const memp_pools[MEMP_MAX] = {
#define LWIP_MEMPOOL(name,num,size,desc) &memp_ ## name,
#include "lwip/priv/memp_std.h"
};

上面的源码是memp相关的定义,摘录自memp.c/memp.h,另外再结合memp_std.h文件就可以明白作者的代码原理,编译阶段预处理将宏定义展开后,还原出c语言源码,而增删改查则通过头文件memp_std.h就可以完成,非常简洁的源码设计,值得学习!

...

#if LWIP_UDP //通过宏LWIP_UDP控制是否定义TCP相关的静态内存pool
LWIP_MEMPOOL(UDP_PCB, MEMP_NUM_UDP_PCB, sizeof(struct udp_pcb), "UDP_PCB")
#endif

#if LWIP_TCP //通过宏LWIP_TCP控制是否定义TCP相关的静态内存pool
LWIP_MEMPOOL(TCP_PCB, MEMP_NUM_TCP_PCB, sizeof(struct tcp_pcb), "TCP_PCB")
LWIP_MEMPOOL(TCP_PCB_LISTEN, MEMP_NUM_TCP_PCB_LISTEN, sizeof(struct tcp_pcb_listen), "TCP_PCB_LISTEN")
LWIP_MEMPOOL(TCP_SEG, MEMP_NUM_TCP_SEG, sizeof(struct tcp_seg), "TCP_SEG")
#endif

...

//PBUF_REF/ROM/POOL相关的静态内存pool定义
LWIP_PBUF_MEMPOOL(PBUF, MEMP_NUM_PBUF, 0, "PBUF_REF/ROM")
LWIP_PBUF_MEMPOOL(PBUF_POOL, PBUF_POOL_SIZE, PBUF_POOL_BUFSIZE, "PBUF_POOL")

...

#if MEMP_USE_CUSTOM_POOLS //如果启用自定义静态内存pool来替换mem.c里面的内存管理,还需定义头文件lwippools.h
#include "lwippools.h"
#endif

//每次#include memp_std.h,根据源码的#define,将本次的#uddef
#undef LWIP_MEMPOOL
#undef LWIP_MALLOC_MEMPOOL
#undef LWIP_MALLOC_MEMPOOL_START
#undef LWIP_MALLOC_MEMPOOL_END
#undef LWIP_PBUF_MEMPOOL

通过memp_std.h里的宏定义,在编译时确定静态内存pool大小,整片静态内存,分割成定长的内存块,再看pbuf.c中的memp_malloc(MEMP_PBUF_POOL)和memp_malloc(MEMP_PBUF),其中MEMP_PBUF_POOL/MEMP_PBUF是enum memp_t定义的枚举类型(源码如下所示),编译阶段展开,通过这个枚举类型在相应的连续整片的静态内存pool中分配固定长度的内存块,这些内存块形成链表的结构,定长的内存块链表在分配释放上是非常快的,而且不产生内存碎片,只是有时候有可能有些浪费内存,但是在此更倾向于效率。

#define LWIP_MEMPOOL(name,num,size,desc)
#include "lwip/priv/memp_std.h"

typedef enum {
#define LWIP_MEMPOOL(name,num,size,desc)  MEMP_##name,
#include "lwip/priv/memp_std.h"
  MEMP_MAX
} memp_t;

再看几个宏定义:

#define MEMP_MEM_MALLOC 0 //采用memp_malloc/memp_free,如果此宏定义定义为1,则采用mem_malloc/mem_free

#define MEM_USE_POOLS 0 //采用mem_malloc/mem_free自己的内存管理方式,如果定义为1,则采用静态内存pool这种类型的方式,此时还需要定义另一个宏和头文件

#define MEMP_USE_CUSTOM_POOLS 0 //如上所示,如果mem_malloc/mem_free采用静态内存pool的方式则需要将此宏定义为1,而且通过memp_std.h头文件可知,还需定义头文件lwippools.h,定义静态内存pool

netif

lwIP使用数据结构netif来描述一个网络接口

struct netif {
  struct netif *next; //指向下一个netif

#if LWIP_IPV4
  ip_addr_t ip_addr; //ipv4的IP地址,子网掩码,网关设置
  ip_addr_t netmask;
  ip_addr_t gw;
#endif
  ...
  netif_input_fn input; //接收函数,网卡驱动调用,送给tcp/ip stack
#if LWIP_IPV4
  netif_output_fn output; //发送函数,IP层要发送数据调用
#endif
  netif_linkoutput_fn linkoutput; //ethernet_output()发送调用,arp调用
  ...
  void *state; //指向网卡状态,可以由网卡驱动修改,可由开发者自定义的ethernetif
#ifdef netif_get_client_data
  void* client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX + LWIP_NUM_NETIF_CLIENT_DATA];
#endif
  ...
  u16_t mtu; //maximum transfer unit (in bytes)
  u8_t hwaddr_len; //mac地址长度
  u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; //mac地址
  u8_t flags;
  char name[2]; //网络接口类型描述
  u8_t num; //网络接口个数
  ...
#if LWIP_IPV4 && LWIP_IGMP
  netif_igmp_mac_filter_fn igmp_mac_filter;
#endif
  ...
#if ENABLE_LOOPBACK
  struct pbuf *loop_first;
  struct pbuf *loop_last;
#if LWIP_LOOPBACK_MAX_PBUFS
  u16_t loop_cnt_current;
#endif
#endif
};

网卡驱动初始化时用到此数据结构,参看一下测试程序的初始化就更直观了test\fuzz\fuzz.c,但是还不是具体的设备,只是一个测试程序,参考一个具体的例子

  ...
  struct netif net_test; //网络接口定义
  ip4_addr_t addr;
  ip4_addr_t netmask;
  ip4_addr_t gw;
  u8_t pktbuf[2000];
  size_t len;

  lwip_init(); //lwip初始化

  IP4_ADDR(&addr, 172, 30, 115, 84);
  IP4_ADDR(&netmask, 255, 255, 255, 0);
  IP4_ADDR(&gw, 172, 30, 115, 1);

  netif_add(&net_test, &addr, &netmask, &gw, &net_test, testif_init, ethernet_input); //关注初始化函数,input函数
  netif_set_up(&net_test);
  ...

网卡的初始化的驱动文件ethernetif.c是lwIP提供的一个网卡驱动模板,err_t ethernetif_init(struct netif *netif),更底层的初始化函数static void low_level_init(struct netif *netif),以及相应的底层input/output


Similar Posts

Comments