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

时间:2022-06-30 16:20:59

 源码

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)随着数据的逐层处理,其内容可能有所增减,而连续内存很难处理这种动态的数据增删。

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

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,因此,数据区的大小就不能小于各层协议首部信息大小的总和。

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


 

看源码可知

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。而分配的内存块使用完后需要释放,为了不产生碎片,相邻且未用的内存块需要进行合并。

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


 

应用程序管理的存储器

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(数据)的前面。这样就构成了一个完整的数据分组。

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


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协议栈的性能。在实际实现的过程中,还需要考虑内存边界对齐的问题。在多线程环境下,还需要加入对共享资源的访问控制等。