LWIP之Memp原理

前言

    在这个色彩缤纷的时代,网络扮演着重要的角色,作为网络通讯的主要协议,TCP/IP协议就变得非常的重要,对于嵌入式系统来说,处理能力以及内存上的瓶颈导致嵌入式TCP/IP协议栈不可能像PC上的协议栈那么强大,而LWIP作为一个小型的开源免费的TCP/IP协议栈,以简洁,高效,占用内存少被广泛的使用在各类嵌入式网络应用中,本文就是基于LWIP的内存角度开展的。

正文

         本文将从三个角度来分析LWIP的memp内存池机制。

【1】memp相关宏以及变量的解释

【2】Memp的内存机制原理

【3】Memp的源码分析

 

为了让大家更好的理解lwip的源码,我觉得从开头就将一些关键的变量或者结构根大家做一个介绍,以方便更好的配合源码阅读。


1、memp相关宏以及变量的解释

【1】宏定义解释

1)	MEMP_MEM_MALLOC    定义是否使用内存堆机制来为内存池分配内存
2)	MEM_USE_POOLS      定义使用内存池来给内存堆分配内存
3)	MEMP_OVERFLOW_CHECK  溢出检测
4)	MEMP_SANITY_REGION_BEFORE   内存池下溢检测区域大小
5)	MEMP_SANITY_REGION_AFTER    内存池溢出检测区域大小
6)	MEMP_SIZE            对齐后的mem结构的大小(用于管理memp)       
7)	MEMP_ALIGN_SIZE(x)   计算x进行内存对齐后的大小
8)	LWIP_DEBUG           lwip的调试输出
9)	MEMP_SEPARATE_POOLS  定义memp是否使用分离的内存池
10)	MEMP_SANITY_CHECK   定义memp是否开启环路检测

2】数据结构

struct memp {
  struct memp *next;            //下一个可用内存池的首地址
#if MEMP_OVERFLOW_CHECK
  const char *file;
  int line;
#endif /* MEMP_OVERFLOW_CHECK */
};  
memp的内存管理结构

这个数据结构是memp最重要的数据结构,其主要实现对memp的管理,包括内存的分配和回收等。

3】变量

const u16_t memp_sizes[MEMP_MAX];  //记录每一类型的内存池大小
static const u16_t memp_num[MEMP_MAX]; //记录每一类型的内存池的数量
static struct memp *memp_tab[MEMP_MAX]; //记录各类型空闲内存池的首地址

如果使用单一内存池,则

static u8_t memp_memory[MEM_ALIGNMENT - 1 
#define LWIP_MEMPOOL(name,num,size,desc)+((num)*(MEMP_SIZE+ EMP_ALIGN_SIZE(size) ) )
#include "lwip/memp_std.h"
];  //内存池

否则还需要为各个内存池提供定义

#define LWIP_MEMPOOL(name,num,size,desc) u8_t memp_memory_ ## name ## _base \
  [((num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size)))];   
#include "lwip/memp_std.h"

/** This array holds the base of each memory pool. */
static u8_t *const memp_bases[] = { 
#define LWIP_MEMPOOL(name,num,size,desc) memp_memory_ ## name ## _base,   
#include "lwip/memp_std.h"
};

当然如果使用了debug,那么还有

static const char *memp_desc[MEMP_MAX]; //各类型内存池的描述信息

 

2、Memp的内存机制原理

LWIP的内存池的分配方式存在几种情况 

1、用户直接定义物理内存,并从中分配内存

2、使用内存堆来为内存池分配内存

这里,内存堆相关的分析将在下一章进行,这里主要探讨第一种方式。lwip对于内存池的定义也存在两个情况,及

           1、使用统一形式的memp_memory来定义整个系统的内存池大小

            2、分别定义各个类型的内存池

其定义已经在上面描述,这里不再赘述,至于为什么定义两种,笔者个人观点认为有这么几个原因

          1> 使用独立的内存池定义可以更加灵活满足不同内存池对速度的要求,如可能对于一些需要快速访问的内存池使用片内的RAM定义,而对于一些大容量而且速度要求不太高的内存池可以使用外部扩展内存。这样就可以很好处理成本和效率。

         2> 在内存池相关的处理上更加的灵活, 可扩展性更强,如后期也可以实现在不同内存池上的安全区域大小差异化,这样往往可以压缩系统的内存需求,当然目前来说这个优点感觉并不明显。相反,存在优点的同时,其缺点也很明显,在内存对齐的同时需要更多的空间,这将增大系统的内存开支。

         在实现基本的内存池,其也实现了一些安全机制,这些机制可以保证系统更稳定安全的运行,避免由于诸如内存溢出、链表环路等一些问题所带来的问题,具体怎么实现将在源码里面进行分析。那么memp初始化完成,其是怎么样的一个数据结构呢? 如下图

LWIP之Memp原理

可以看出,初始化过程中,其实memp完成了以下几种操作

       1、同一类型内存池的空闲池连接为一个单向链表。

       2、初始化了一些管理单元,比如memp_tab,memp_size等,便于后期内存的分配管理。

这里需要注意的是,不同类型的内存池并没有通过链表连接起来,各类型链表相互独立。当然,以上为统一memp_memory分配的内存池,上面讲过还存在另外一个,其初始如下。

LWIP之Memp原理

其实与memp_mempry完全相同,只是各类型内存池的memory来自不同的地方。

说完了内存的初始化,那么在讲讲内存的安全机制,安全机制在lwip为预编译,由客户自己选择编译,上面提供的插头默认没有加入溢出保护机制,如下为加入溢出保护机制的内存池结构。

LWIP之Memp原理

其内存检测原理极其简单,就是在用户数据区域的 首部和尾部填充固定的数据,当发生了溢出,那么这些固定数据必定发生破坏,那么将很容易的被协议栈检测出来。至于填充的固定数据到底有多少,这个可以由由于自定义配置,配置通过MEMP_SANITY_REGION_BEFORE和MEMP_SANITY_REGION_AFTER来完成,默认为16Byte

至于内存池的分配和回收过程也比较简单,仅仅就是用户申请特定类型内存块,分配成功将返回该内存块的地址,并将该内存块从空闲的链表移除即可,这里就不在提供插图 (ps:原谅作者是一个比较懒的人),具体的过程将在源码分析给出。

最后来一张memp管理的全家福

LWIP之Memp原理

 

3、Memp的源码分析

memp的操作无非两种

1、memp的分配

2、memp的内存回收

接下来我们来看看其是如何做到内存的高效分配和回收的。(这里插一段,你们知道为什么为什么memp相关的操作全部实现了内存对齐,并且对齐的字节可分配吗? 读者可以思考一下? 好了,答案揭晓: 主要是为了上面提到的搞笑内存分配和回收,保持协议栈的高效率运行,因为如果内存对齐刚好和处理器的总线取址宽度一直,那么所有相关的取址操作仅仅只需要单指令就可以运行,否则处理器可能还需要对取回来的数进行分解操作等,那么将白白浪费较多的指令周期,内存分配延迟加大。而对齐字节可配置是由于客户的开发平台不一致,有的可能总线宽度为2Byte,有的甚至1Byte,这就需要灵活的修改了,关于内存对齐相关知识,读者可自己搜索一下) 插曲好长啊,哈哈哈,一说到其他的就来劲,回到正题,

memp如何实现的内存分配呢?那还要从内存怎么初始化说起。

 

【1】内存池的初始化

void memp_init(void)
{
  struct memp *memp;
  u16_t i, j;

  for (i = 0; i < MEMP_MAX; ++i) {  //这里是一些相关的统计信息,并没有任何实际作用
    MEMP_STATS_AVAIL(used, i, 0);
    MEMP_STATS_AVAIL(max, i, 0);
    MEMP_STATS_AVAIL(err, i, 0);
    MEMP_STATS_AVAIL(avail, i, memp_num[i]);
  }

#if !MEMP_SEPARATE_POOLS
  memp = (struct memp *)LWIP_MEM_ALIGN(memp_memory);  //这里是使用统一的内存
#endif /* !MEMP_SEPARATE_POOLS */
  /* for every pool: */
  for (i = 0; i < MEMP_MAX; ++i) {
    memp_tab[i] = NULL;  //初始化各内存的末端 全部指向NULL
#if MEMP_SEPARATE_POOLS
    memp = (struct memp*)memp_bases[i]; //使用独立的内存
#endif /* MEMP_SEPARATE_POOLS */
    /* create a linked list of memp elements */
    for (j = 0; j < memp_num[i]; ++j) { //连接同类型内存池为一个空闲链表,并将表头存放在//memp_tab中
      memp->next = memp_tab[i];  
      memp_tab[i] = memp;
      memp = (struct memp *)(void *)((u8_t *)memp + MEMP_SIZE + memp_sizes[i]
#if MEMP_OVERFLOW_CHECK
        + MEMP_SANITY_REGION_AFTER_ALIGNED
#endif
      );
    }
  }
#if MEMP_OVERFLOW_CHECK  //加入安全机制
  memp_overflow_init();   //安全机制的初始化
  /* check everything a first time to see if it worked */
  memp_overflow_check_all(); //检测是否存在溢出(包括上溢和下溢)
#endif /* MEMP_OVERFLOW_CHECK */
}

由上面可以看出,其实lwip只做了两件事

1、链接同类型内存池,并形成链表

2、初始化相关的管理参数,如memp_tab等

当然,如果考虑加入安全机制,那么还存在

3、安全机制的初始化和溢出检查

 

【2】内存池的分配

void *
#if !MEMP_OVERFLOW_CHECK
memp_malloc(memp_t type)
#else
memp_malloc_fn(memp_t type, const char* file, const int line)
#endif
{
  struct memp *memp;
  SYS_ARCH_DECL_PROTECT(old_level);
 
  LWIP_ERROR("memp_malloc: type < MEMP_MAX", (type < MEMP_MAX), return NULL;);

  SYS_ARCH_PROTECT(old_level); //临界区保护
#if MEMP_OVERFLOW_CHECK >= 2 //这里默认MEMP_OVERFLOW_CHECK>=2将对所有内存池//进行溢出检查
  memp_overflow_check_all();
#endif /* MEMP_OVERFLOW_CHECK >= 2 */

  memp = memp_tab[type];  //memp_tab为指向各类型空闲POOL的指针
  
  if (memp != NULL) {  //如果当前内存池存在空闲内存,那么分配内存
    memp_tab[type] = memp->next;
#if MEMP_OVERFLOW_CHECK
    memp->next = NULL;
    memp->file = file;
    memp->line = line;
#endif /* MEMP_OVERFLOW_CHECK */
    MEMP_STATS_INC_USED(used, type);
    LWIP_ASSERT("memp_malloc: memp properly aligned",
                ((mem_ptr_t)memp % MEM_ALIGNMENT) == 0);
    memp = (struct memp*)(void *)((u8_t*)memp + MEMP_SIZE); //返回应用区地址
  } else {  //内存耗尽
    LWIP_DEBUGF(MEMP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("memp_malloc: out of memory in pool %s\n", memp_desc[type]));
    MEMP_STATS_INC(err, type);
  }

  SYS_ARCH_UNPROTECT(old_level);

  return memp;
}

从上面malloc函数可以看出,memp的分配之所以高效主要取决于其良好的管理结构,以至于在申请内存的时候只需要进行一个简单的判断,而不需要进入迭代或者其他的附加的查询操作,导致其效率的损失,就目前来看,其分配的时间复杂度为O(1),可以说非常高效。

 

【3】内存的释放

void memp_free(memp_t type, void *mem)
{
  struct memp *memp;
  SYS_ARCH_DECL_PROTECT(old_level);

  if (mem == NULL) { //回收地址有效性检测
    return;
  }
  LWIP_ASSERT("memp_free: mem properly aligned",
                ((mem_ptr_t)mem % MEM_ALIGNMENT) == 0);

  memp = (struct memp *)(void *)((u8_t*)mem - MEMP_SIZE);

  SYS_ARCH_PROTECT(old_level);
#if MEMP_OVERFLOW_CHECK
#if MEMP_OVERFLOW_CHECK >= 2 //溢出检测
  memp_overflow_check_all();
#else
  memp_overflow_check_element_overflow(memp, type);
  memp_overflow_check_element_underflow(memp, type);
#endif /* MEMP_OVERFLOW_CHECK >= 2 */
#endif /* MEMP_OVERFLOW_CHECK */

  MEMP_STATS_DEC(used, type);  
  
  memp->next = memp_tab[type]; //地址回收
  memp_tab[type] = memp;

#if MEMP_SANITY_CHECK
  LWIP_ASSERT("memp sanity", memp_sanity());
#endif /* MEMP_SANITY_CHECK */

  SYS_ARCH_UNPROTECT(old_level);
}

同样,mmep的回收也是非常高效的,其时间复杂也同样为O(1), 仅仅设计简单的链表插入,这里不做赘述。

到此为止,lwip的memp内存池的原理及操作基本讲完了,当然,memp还提供了一些其他的API函数,比如memp_get_memorysize(void),memp_sanity(void)等,其都比较简单,读者可自行阅读源码。

以上就是lwip提供的由用户定义的内存池的分配和回收的全部,但是,我要强调的时,memp并不一定就是有memp_memory来提供,它还有可能是有MEM内存堆来分配的,因此,就存在另外一套接口,其定义在memp.h中,如下

#if MEMP_MEM_MALLOC

#include "mem.h"

#define memp_init()
#define memp_malloc(type)     mem_malloc(memp_sizes[type])
#define memp_free(type, mem)  mem_free(mem)

#else /* MEMP_MEM_MALLOC */

因此,如果使用内存堆来分配的话,对内存池的操作就非常的简单了,读者就可以并且内存池的相关操作,只需要内存堆mem的细节就可以了。

那么关于mem内存堆的原理将在下一章分析,本章到此结束。

上一篇:LwIP应用开发笔记之五:LwIP无操作系统TCP服务器


下一篇:LwIP应用开发笔记之二:LwIP无操作系统UDP服务器