APR内存池的学习与实现
在很早之前,本人在学习Apache中的内存池的实现的时候,尝试过仿照其重新简单实现了下。主要目的是更快得理解它。这篇文章也是基于当初我自己重新写了一遍的代码,其原理是一致的。同时也参考了《APR内存池概述》这篇文章,并修正了其中一些错误之处。
那么关于什么是内存池,内存池有哪些好处,本文就不赘述了。直接进入正题吧
内存节点
在了解池之前,首先得介绍节点。为了方便地对所有已经分配的内存进行管理,用内存节点的概念来描述每次分配的内存块,其定义如下:
struct _MEMNODE
{
struct _MEMNODE *next;
struct _MEMNODE **ref;
unsigned int index;
unsigned int free_index;
char *first_avail;
char *endp;
};
_MEMNODE
是内存管理的基石,在后文中,我们称之为“内存节点”或“节点”。
从上面的结构中可以看出在_MEMNODE
内部没有任何的"空闲空间"来容纳实际分配的内存。事实上也确实如此,它从不单独存在,一旦分配了实际的空间之后,该结构将置于最顶部,如下图所示:
first_avail
和end_p
在上图中,假如我们分配了一定大小的空间,为了能够将该空间用节点进行记录,我们将_MEMNODE
置于整个空间的头部,则实际可用空间为剩下的空白部分,同时结构中还提供了first_avail
和end_p
指针分别指向这块可用空间的首部和尾部。当这块可用空间被不断利用时,first_avail
指针也不断随之移动,而end_p
指针是永远指向尾部不动的,不过(end_p-first_avail)之间则永远是当前的空闲空间。上图的右边部分演示了这种布局。
index
和free_index
Apache中对内存的分配大小并不是随意的,随意的分配可能会造成更多的内存碎片。为此Apache采取的则是“规则块”分配原则。Apache所支持的分配的最小空间是8K,如果分配的空间达不到8K的大小,则按照8K去分配;如果需要的空间超过8K,则将分配的空间往上调整为4K的倍数。
即:如果需要分配的空间(包括节点头的空间)总和小于8K,则以8K进行分配,否则调整为4K的整数倍。
举个例子:如果我们要求分配的size大小为1024,其按照最小单元分配,实际分配大小为8192;如果我们要求分配的空间为8192,由于其加上内存节点头,大于8192,此时将按照最小单元分配4k,此时实际分配的空间大小为8192+4096=12K。
那么这样一来,每个节点的空间大小都不完全一样,为此分配节点本身必须了解本节点的大小,于是使用index
进行记录。需要注意的是,index
只是记录当前节点大小相对于4K的倍数,计算方法为:
index=(size>>BOUNDARY_INDEX)-1; //BOUNDARY_INDEX=12,即4K。
这样如果index=5,我们就可以知道该节点大小为24K;反过来也是如此。
这样将大小转化为index
是为了方便程序处理(后面会详细提到),我们将该值称之为“索引大小”,与此相同,free_index
则是定义了当前节点中可用空间的索引大小。
next
和ref
额,关于这两个成员,大家就顾名思义吧。就不再赘述了。不同的节点之间通过next
指针形成节点链表;另外当在节点内部的时候ref
为了方便引用节点本身。
相关代码
相关宏定义:
#define MEM_ALIGN(size,boundary) (((size) + ((boundary)-1)) & ~((boundary)-1))
#define MEM_ALIGN_DEFAULT(size) MEM_ALIGN(size,8)
#define BOUNDARY_INDEX 12
#define MAX_INDEX 20
相关常量:
const unsigned int BOUNDARY_SIZE = 1 << BOUNDARY_INDEX; //4096
const unsigned int MIN_ALLOC = 2 * BOUNDARY_SIZE; //8192
const unsigned int SIZEOF_MEMNODE = MEM_ALIGN_DEFAULT(sizeof(MEMNODE));//MEMNODE大小
节点分配函数部分代码:
MEMNODE *Allocator_alloc(ALLOCATOR *allocator, size_t in_size)
{
MEMNODE *node, **ref;
unsigned int max_index;
size_t size, i, index;
//4096的倍数,不满8192,则分配8192
size = MEM_ALIGN(in_size + SIZEOF_MEMNODE, BOUNDARY_SIZE);
if (size < in_size) return NULL;
if (size < MIN_ALLOC) size = MIN_ALLOC;
//将大小转为下标
index = (size >> BOUNDARY_INDEX) - 1;
//中间代码暂时省略,文章后面慢慢补齐的同时再做介绍
//.....
//
if ((node = (MEMNODE*)malloc(size)) == NULL)
return NULL;
node->next = NULL;
node->index = index;
node->first_avail = (char *)node + SIZEOF_MEMNODE;
node->endp = (char *)node + size;
return node;
}
内存分配子
那么同样的道理,为了方便分配和管理内存节点_MEMNODE
,Apache引入了一个叫做allocator的东西,姑且就叫它内存分配子吧,细心的你可能已经在上文的相关代码里见到了它,那么这个结构的定义如下:
struct _ALLOCATOR
{
unsigned int max_index;
unsigned int max_free_index;
unsigned int current_free_index;
CRITICAL_SECTION *pcs;
struct _MEMPOOL *owner;
struct _MEMNODE *pfree[MAX_INDEX];
};
节点链表数组pfree
该结构中最最重要的那肯定就是pfree
数组了,数组的每个元素都是_MEMNODE
类型的地址,指向一个节点链表。
pfree
中的所有链表中的节点的大小并不完全相同,这取决于当前链表在pfree
数组中的索引,此处pfree
数组的索引具有两层含义:1、该节点链表在数组中的实际索引;2、它同时还标记了当前链表中节点的大小(这就是上文提到的“索引大小”)。
所以,同一个链表中的所有节点大小都完全相等(索引0例外,后文会提)。而,索引越大,节点也就越大。
节点的大小与节点所在链表的索引存在如下关系:
节点大小=4K(index&#