LwIP协议栈学习--内存管理

 源码算法

case PBUF_POOL:
p = memp_malloc(MEMP_PBUF_POOL);缓存

case PBUF_RAM:
p = (struct pbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) + LWIP_MEM_ALIGN_SIZE(length));网络

 case PBUF_ROM:
 case PBUF_REF:
 /* only allocate memory for the pbuf structure */
 p = memp_malloc(MEMP_PBUF);数据结构

需求多线程

在内存需求分析的基础上,阐述了LwIP TCP/IP协议栈中pbuf结构的基本原理和内存管理机制的实现。函数

TCP/IP是一种基于OSI参考模型的分层网络体系结构,它由应用层、运输层、网络层、数据链路层、物理层组成。各层之间消息的传递经过数据报的形式进行。因为各层之间报头长度不同。当数据在不一样协议层之间传递时.对数据进行封装和去封装、增长和删除操做将十分频繁。性能

在嵌入式系统开发中也常常遇到相似问题。用户数据从本地嵌入式设备传输到远程主机的过程当中,要通过各层协议,对消息的封装、去封装和拷贝操做几乎是不可避免的。而一般所采用的用一段连续的内存区来存储、传递数据的作法会有如下的缺陷:
   (1)当从上层向下层传递数据时,下层协议须要对数据进行封装,而上层在申请内存时没有(也不该该)考虑下层的须要。这样会致使下层协议处理时须要从新申请内存并进行内存拷贝,从而影响程序的效率。
  (2)当从下层向上层传递数据时,下层协议专有的数据结构应当对上层协议不可见。所以也须要从新申请内存并进行内存拷贝。
  (3)随着数据的逐层处理,其内容可能有所增减,而连续内存很难处理这种动态的数据增删。
this

必须有一种能适应数据动态增删、但在逻辑上又呈现连续性的数据结构,以知足在各协议层之间传递数据而不须要进行内存拷贝。嵌入式TCP/IP协议栈要求简单高效,并减小对内存的需求。这些都须要相应的内存管理机制实现。spa

LwIP协议栈中pbuf介绍线程

LwIP利用pbuf结构实现数据传递,它与BSD中的Mbuf很类似。pbuf的主要用途是保存在应用程序和同络接口间互相传递的用户数据。

Pbuf的结构体定义以下:

struct pbuf {

 struct pbuf *next; /*指向下一个buf*/

 void *payload; /*指向pbuf数据中的起始位置*/

  u16_t tot_len; /*该pbuf和后续pbuf中数据长度的总和*/

  u16_t len;  /*该pbuf中数据的长度*/

u8_t /*pbuf_type*/ type; /*pbuf的类型*/

  u8_t flags; /*misc flag*/

  u16_t ref; /*该pbuf引用计数*/

};

 

Pbuf_type类型枚举以下:

typedef enum {

  PBUF_RAM, /* pbuf data is stored in RAM */

  PBUF_ROM, /* pbuf data is stored in ROM */

  PBUF_REF, /* pbuf comes from the pbuf pool */

  PBUF_POOL /* pbuf payload refers to RAM */

} pbuf_type;

 

LwIP内存管理的实现

在运行TCT/IP协议栈的嵌入式系统中。能够把整个系统的存储区域分为协议栈管理的存储器和应用程序管理的存储器两部分。

协议栈管理的存储器

协议栈管理的存储器是指TCP/IP内核可以操做的内存区域,主要用于装载待接收和发送的网络数据分组。当接收到分组或者有分组要发送时,TCP/IP协议栈为这些分组分配缓存;接收到的分组交付给应用程序或者分组已经发送完毕后,对分配的缓存回收重用。协议栈分配的缓存必须能容纳各类大小的报文,例如从仅仅几个字节的ICMP回答报文到几百个字节的TCP分段报文。

PBUF_POOL

PBUF_POOL是具备固定容量的pbuf,主要供网络设备驱动使用,为收到的数据分组分配缓存。在协议栈管理的内存中初始化了一个pbuf池(PBUF_P00L),具备相同尺寸的pbuf都是从这个pbuf池中分配获得。通常使用多个PBUF_POOL连接成一个链表,用于存储数据分组。PBUF_POOL类型pbuf一般用于网卡驱动程序中,由于从缓冲池中分配一个PBUF_POOL类型pbuf耗时不多,因此适合在中断处理程序中完成。 一般用PBUF_POOL型pbuf存放收到的数据包。如图1所示。

PBUF_POOL型pbuf的数据区不能设置的太大,太大对于短的数据包来讲,形成了内存空间的浪费,也不能过小,缘由有两点:1)若是数据区设置过小,pbuf结构相对于数据区来讲就占用了太多的内存空间,致使内存利用率降低;2)处理报文时是经过移动payload指针来指向特定的协议首部,为了方便操做,数据分组首部信息的存放最好不要跨越pbuf,所以,数据区的大小就不能小于各层协议首部信息大小的总和。


 

看源码可知

low_level_input()---->调用  p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);     

  frame = ETH_Get_Received_Frame();
 
  /* Obtain the size of the packet and put it into the "len" variable. */
  len = frame.length;
  buffer = (u8 *)frame.buffer;
 
  /* We allocate a pbuf chain of pbufs from the Lwip buffer pool */
  p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);
 
  /* copy received frame to pbuf chain */
  if (p != NULL)
  {
    for (q = p; q != NULL; q = q->next)
    {
      memcpy((u8_t*)q->payload, (u8_t*)&buffer[l], q->len);
      l = l + q->len;
    }   
  }

 

PBUF_RAM

应用程序发送动态产生的数据时.能够用PBUF_RAM类型的pbuf。PBUF_RAM在事先划分好的内存堆中分配。从系统中申请一段内存做为变长分配内存的区域,对这段内存定义分配、回收连续的内存区域等操做。变长分配内存区域是由一系列未分配和已分配的内存块组成,采用链表对这段内存进行管理——在每一个已分配和未分配的内存块的首部放上简单的管理信息,变长内存分配区域最初是一个连续的未分配内存块,prev指向0,next指向最后,used为0,表示是一个只有一个未分配的内存堆,对该内存堆的操做相似于C语言中的malloc/free。内存堆分配的结构如图2所示。图2中每一个被分配的存储块附带了一个小结构,该结构的两个指针指向相邻的内存块。used标识位用来指示该内存块的分配状况,阴影部分表示已经被分配了,此时used为1。当须要一块N字节的存储块时,就对整个存储堆进行搜索。若是找到一块未用的(used=O)而且容量不小于N字节的区域就表示分配成功,而且置used为1。而分配的内存块使用完后须要释放,为了避免产生碎片,相邻且未用的内存块须要进行合并。


 

应用程序管理的存储器

PBUF_ROM

应用程序管理的存储器是指应用程序管理、操做的存储区域.通常从该区域为应用程序发送数据分配缓存。虽然该存储区域不禁TCP/IP协议栈管理,但在不严格分层的协议栈中,该存储区域必须与TCP/IP管理的存储器协同工做。为节省内存,LwIP不采起分级访问模式,而是经过指针访问数据。这样就不须要为数据的传递分配存储空间。应用程序发送的数据在交付LwIP后,LwIP就认为这些数据是不能被改动的,所以应用程序的数据被认为是永远存在而且不能被改变的。这一点与ROM很类似.类型名PBUF_ROM也由此而来。

如图3所示,PBUF_ROM的数据指针payload指向External memory(外部存储区)。Extemal memory指不禁TCP/IP协议栈管理的存储区,它能够是应用程序管理的存储器为用户数据分配的缓存,也能够是ROM区域,如静态网页中的字符串常量等。因为由应用程序交付的数据不能被改动,所以就须要动态地分配一个PBUF_RAM来装载协议的首部,而后将PBUF_RAM(首部)添加到PBUF_ROM(数据)的前面。这样就构成了一个完整的数据分组。


PBUF_REF

PBUF_REF和PBUF_ROM的特性很是类似,均可以实现数据的零拷贝。可是当发送的数据须要排队时就表现出PBUF_REF的特性了。例如在发送分组时,待发送的分组须要在ARP队列中排队,假如这些分组中有PBUF_ROM类型的pbuf,则说明该类型pbuf中的数据位于应用程序的存储区域,是经过指针被PBUF_ROM引用的。这样直到分组被处理以前,被引用的应用程序的这块存储区域都不能另做它用。在此状况下要用到PBUF_REF类型的pbuf。在排队时,LwIP会为PBUF_REF类型的pbuf分配缓存(PBUF_POOL或PBUF_RAM),并将引用的应用程序的数据拷贝到分配的缓存中。这样应用程序中被引用数据的存储区域就能被释放。

PBUF_POOL类型pbuf缓冲池在协议栈初始化时设置,在内存中申请一块静态存储区,初始化成pbuf链表。分配该类型pbuf时,从缓冲池首部根据申请的大小分配一个或多个pbuf。与PBUF_RAM类型须要采用最早适应算法搜索空闲内存块,花费少了不少,所以速度更快。

Pbuf的主要操做:

1) struct pbuf *pbuf_alloc(pbuf_layer, u16_t length, pbuf_type type);
   分配pbuf。type的值能够为PBUF_RAM、PBUF_ROM、PBUF_REF或PBUF_POOL之一。size是要申请的空间大小。layer字段定义了头部字节数。

2) pbuf_realloc(struct pbuf *p, u16_t new_len)

收缩pbuf队列,将pbuf队列收缩一个新的长度。new_len必须小于pbuf的总长度。
    3) INT8 pbuf_header(struct pbuf *p, s16_t header_size);
   调整payload指针的指向,指向或隐藏各层协议首部信息。
    4)void pbuf_ref(struct pbuf *p);
   增长pbuf引用次数计数。该函数对pbuf中的ref执行加1操做。只有当ref字段为0时,才能够回收该pbuf。
    5)INT8 pbuf_free(struct pbuf *p);
    减小一个pbuf或pbuf链表的引用计数,当ref为0时,回收该pbuf,根据其flags字段的值回收到相应的内存区域。

6) u8_t pbuf_clen(struct pbuf *p)

计算pbuf链中pbuf节点的个数
    7) void pbuf_chain(struct pbuf *h, struct pbuf *t);
    把t所指向的pbuf连接到h所指向的pbuf以后(h和t均可以是pbuf链表)。
    8) void pbuf_queue(struct pbuf *p, struct pbuf *n);
   用于把暂时不发送的分组放到缓冲队列中。p是指向当前缓冲队列第一个分组的指针,是要放到缓冲队列的分组。
    9) struct pbuf *pbuf_dechain(struct pbuf *p);
   在缓冲队列的首部取出一个分组,返回指向剩下分组第一个pbuf的指针。

10) err_t pbuf_copy(struct pbuf *p_to, struct pbuf *p_from)

  将p_from链的内容拷贝到p_to链中

  11) u16_t pbuf_copy_partial(struct pbuf *buf, void *dataptr, u16_t len, u16_t offset)

  将pbuf中偏移offset开始的len长度内容拷贝到dataptr指向的存储区中

 

代码解读:

PBUF_RAM的内存堆管理:

  堆的控制结构定义

  struct mem {

  /** index (-> ram[next]) of the next struct */

  mem_size_t next;

  /** index (-> ram[next]) of the next struct */

  mem_size_t prev;

  /** 1: this area is used; 0: this area is unused */

  u8_t used;

};

堆的静态定义:

/** the heap. we need one struct mem at the end and some room for alignment */

static u8_t ram_heap[MEM_SIZE_ALIGNED + (2*SIZEOF_STRUCT_MEM) + MEM_ALIGNMENT];

堆的实际大小为:定义的MEM的大小 + 两个控制结构的大小 + 1个字节的对齐变量。

加上对齐量的做用在于:堆初始化的时候会将堆首指针ram_heap按照对齐量进行对齐。

三个全局堆指针:

static u8_t *ram;

/** the last entry, always unused! */

static struct mem *ram_end;

/** pointer to the lowest free block, this is used for faster search */

static struct mem *lfree;

 

堆的操做函数:

void mem_init(void):

/*初始化堆头、堆尾控制结构,建立互斥信号量*/

void *mem_malloc(mem_size_t size)

/*根据size从堆中分配内存*/

void mem_free(void *rmem)

/*释放堆内存*/

static void plug_holes(struct mem *mem)

/*对相邻且未用的内存块进行合并。在mem_free中调用*/

 

PBUF_POOL的内存管理:

全局定义:

static u8_t memp_memory[…]

const u16_t memp_sizes[MEMP_MAX];

static const u16_t memp_num[MEMP_MAX];

memp_sizes和memp_num记录固定块大小和个数

/*memp_tab将各固定块队列用链表串接*/

static struct memp *memp_tab[MEMP_MAX];

 

操做函数:

void memp_init(void)

/*将全局memp_memory按照固定块大小进行分割,并用memp_tab 进行串接。同时若是定义了溢出检查,则在内容存储区的先后写入溢出检查字符0xcd*/

void *memp_malloc(memp_t type);

/*根据type类型的值在memp_tab中查找,返回数据区指针*/

void memp_free(memp_t type, void *mem)

/*将内存指针归还到type类型的链中去*/

 

对宏定义的说明:

在memp.h中出现了相似的宏定义:

const u16_t memp_sizes[MEMP_MAX] = {

#define LWIP_MEMPOOL(name,num,size,desc)  MEMP_ALIGN_SIZE(size),

#include "lwip/memp_std.h"

};

 

/** This array holds the number of elements in each pool. */

static const u16_t memp_num[MEMP_MAX] = {

#define LWIP_MEMPOOL(name,num,size,desc)  (num),

#include "lwip/memp_std.h"

};

 

/** This array holds a textual description of each pool. */

#ifdef LWIP_DEBUG

static const char *memp_desc[MEMP_MAX] = {

#define LWIP_MEMPOOL(name,num,size,desc)  (desc),

#include "lwip/memp_std.h"

};

 

typedef enum {

#define LWIP_MEMPOOL(name,num,size,desc)  MEMP_##name,

#include "lwip/memp_std.h"

  MEMP_MAX

} memp_t;

 

上述定义中,对宏LWIP_MEMPOOL都进行了重定义,并且结构体和枚举成员都没有具体值。关键就是在memp_std.h中。

在memp_std.h中定义了可能出现的类型、数量以及描述,对于不一样的结构体,宏LWIP_MEMPOOL取不一样的字段值。根据头文件的含义,就是将头文件替换到#include之处。

注意到:此头文件没有一般的#ifndef ..#define ..#endif防止重复包含的字段。同时在头文件结束处进行了#undef,便于从新定义宏的含义。

 

pbuf结构实现了层与层之间的数据传递,但其很是消耗内存,而且须要TCP/IP协议栈为之分配存储空间,例如协议控制udp_pcb、tep_pcb等。一般,嵌入式TCP/IP协议栈都不是严格分层的,尽可能减小对内存的需求是实现嵌入式TCP/IP的重点,内核的内存管理机制直接关系到嵌入式TCP/IP协议栈的性能。在实际实现的过程当中,还须要考虑内存边界对齐的问题。在多线程环境下,还须要加入对共享资源的访问控制等。