使用PHP构建内存池
内存池的作用
- 提高内存分配效率,通过预先向系统申请一块大的内存用来分配,避免重复向系统申请和释放内存所带来的消耗,提高执行效率
- 实现动态内存分配和释放(malloc只支持小于128K的内存)
- 提高内存利用率,避免产生大量的内存碎片
实现
首选我们需要申请一块大的内存用来分配,我们将这块内存称为chunk内存。假设这块内存的大小为size = 100Byte,它返回的内存起始地址为*ptr = 0x0。
我们使用一个内存池管理类HeapObject进行管理。在只申请不释放的情况下,我们只需使用两个成员变量*ptr和size就可以管理内存池了。它的结构为:
HeapObject{
public ptr;//可分配内存起始地址
public size;//剩余可分配内存
public function __contruct($ptr, $size){
$this->ptr = $ptr;
$this->size = $size;
}
//分配内存
public function malloc($size){
if($this->size < $size){
return NULL;
}
$ptr = $this->ptr;
$this->size -= $size;
$this->ptr += $size;
return $ptr;
}
}
//初始化内存池
$HeapObject = new HeapObject(0x0,100);
当我们申请内存时,只需要偏移ptr指针和减少可分配内存就可以了。
$ptr = $HeapObject->malloc(20);//申请20Byte的内存
/**
* $HeapObject {
* $ptr => 0x14;
* $size => 80;
* }
* /
当$HeapObject->size只剩下20Byte时,如果我们向内存池申请30Byte大小的内存,会发现内存池中可分配的内存不够了,这时候我们需要再申请一块chunk内存用来分配。将ptr偏移到新chunk的起始位置。分配之后的结构为
$ptr = $HeapObject->malloc(30);//申请20Byte的内存
/**
* $HeapObject {
* $ptr => 0x82;
* $size => 70;
* }
* /
但是这样旧内存的20Byte就无法被分配。
所以这时候只通过ptr和size这两个成员变量来管理内存池已经无法满足需求了。
我们需要再增加一个ChunkObject类来管理chunk内存里的ptr和size。
HeapObject{
public $Chunks = [];//chunk数组
public $size;//内存池已分配内存
public function Chunk_init(){
$this->Chunks[] = new ChunkObject($this->Chunk_ini());
}
private function Chunk_ini($chunk = 1){
//向系统申请内存$chunk个CHUNK_SIZE的内存
...
return ptr;
}
public function malloc($size){
foreach($this->chunks as $chunk){
if($chunk->size < $size)continue;
$this->size += $size;
$ptr = $chunk->malloc($size);
if($ptr === NULL)continue;
return $ptr;
}
return NULL;
}
}
ChunkObject{
public ptr;//可分配内存起始地址
public size;//剩余可分配内存
public function __contruct($ptr, $size){
$this->ptr = $ptr;
$this->size = $size;
}
public function malloc($size){
if($this->size < $size){
return NULL;
}
$ptr = $this->ptr;
$this->size -= $size;
$this->ptr += $size;
return $ptr;
}
}
申请内存时首先遍历$HeapObject->chunks数组,查找数组中的元素$ChunkOject的可分配内存size是否满足需求,满足则在该$ChunkObject上进行内存分配,如果所有chunk都不满足,则增加一个新的chunk进行分配。
如果分配大于chunk大小的内存时该怎么办呢?这时候我们需要申请n个chunk用来分配内存,但是申请出来的chunk无法再分配,再用HeapObject->Chunks来管理显然不太合适,所以我们定义一个新的成员变量(HeapObject->huge_list)来管理这些大于chunk大小的内存。我们将大于等于chunk大小的内存称为huge内存,分配小于chunk大小的内存称为large内存分配。HeapObject修改为:
HeapObject{
public $Chunks = [];//Chunk数组
public $size;//内存池已分配内存
public $huge_list = [];
public function Chunk_init(){
ptr = $this->Chunk_ini();
h $this->Chunks[ptr] = new ChunkObject(ptr);
}
private function Chunk_ini($chunk = 1){
//向系统申请内存$chunk个CHUNK_SIZE的内存
...
return ptr;
}
public function malloc($size){
if($size >= ChunkObject::CHUNK_SIZE){
$this->Huge_malloc($size);
}else{
$this->large_malloc($size);
}
}
public function large_malloc($size){
foreach($this->chunks as $chunk){
if($chunk->size < $size)continue;
$this->size += $size;
$ptr = $chunk->malloc($size);
if($ptr === NULL)continue;
return $ptr;
}
return NULL;
}
public function huge_malloc($size){
$chunk_size = ceil($size / ChunkObject::CHUNK_SIZE);
$ptr = $this->chunk_ini($chunk_size);
$this->huge_list[] = [$ptr, $chunk_size*ChunkObject::CHUNK_SIZE];
}
}
在考虑内存回收的情况下,用这样的结构来管理内存池就不太合适了。只用ptr和size来管理内存池就无法使用已回收的内存。
在这种情况下我们需要保存所有可以分配的内存间隙。我们使用一个数组$ChunkObject->free_map[]来管理可分配的内存间隙。
ChunkObject{
public ptr;//内存起始地址
public size;//剩余可分配内存
const CHUNK_SIZE = 100;//chunk的大小
public $free_map = [];//array(array(ptr,size),array(ptr,size)...array(ptr,size));
public function __contruct($ptr){
$this->ptr = $ptr;
$this->size = self::CHUNK_SIZE;
$this->free_map[] = [$ptr,self::CHUNK_SIZE];
}
public function malloc($size){
if($this->size < $size){
return NULL;
}
foreach($this->free_map as &$map){
if($map[1] < $size)continue;
$ptr = $map[0];
$map[0] += $size;
if($map[1] -= $size == 0)unset($map);
$this->size -= $size;
return $ptr
}
return NULL;
}
public function free($ptr,$size){
$this->free_map[] = [$ptr,$size];
$this->size += $size;
}
}
在内存不断的申请和释放之后,很容易产生大量的内存间隙,这种内存间隙就是所谓的内存碎片。如连续申请两块5Byte大小的内存,将第一块内存释放,再在该位置申请一个4Byte大小的内存,这样就会产生1Byte大小的内存间隙。这样大小的内存很少被申请,不仅增加了内存申请时需要遍历的节点,还浪费了内存,降低了内存分配的效率。
我们可以将内存切割成一个个page,分配的时候只要分配n个page。这样虽然还是会造成内存的浪费,但是增加了内存间隙的大小,减少了内存分配时需要遍历的节点数量。提高了内存分配效率,这也是典型的空间换时间。
在上面的例子中,我们可以将内存分成4Byte大小的内存块。需要分配5Byte大小的内存时,我们分配2块内存块给程序,也就是8Byte,虽然这样会有3Byte大小的内存被浪费了,但是这样使内存间隙的最小为4Byte,需要遍历的节点变少了(100 / 4 = 25个节点,最坏需要遍历25/2 = 12.5个节点)。
将100Byte大小的内存以4Byte进行切割,可以切割成25块。我们将4Byte大小的内存块称为page,chunk只需要管理好这25个page就好了,page只有两种状态,分配和未分配。以0来标识未分配,1来标识已分配。ChunkObject可以修改成:
ChunkObject{
public ptr;//内存起始地址
public size;//剩余可分配内存
const CHUNK_SIZE = 100;//chunk的大小
public $free_map = [];//array(0,0,0...0,0);
public function __contruct($ptr){
$this->ptr = $ptr;
$this->size = self::CHUNK_SIZE;
for($i = 0;$i < 25;$i++){
$this->free_map[] = 0;
}
}
public function malloc($size){
if($this->size < $size){
return NULL;
}
$page_num = ceil($size / 4);
$page_index = NULL;
$free_page_num = 0;
foreach($this->free_map as $index => $page){
if($page == 0){
if($page_index === NULL)$page_index = $index;
$free_page_num += 1;
continue;
}
if($page == 1){
if($page_index !== NULL && $free_page_num >= $page_num){
$this->size -= $page_num*4;
for($i = $page_index;$i < $page_num;$i++){
$this->free_map[$i] = 1;
}
return $this->ptr + $page_index*4;
}
$page_index = NULL;
$free_page_num = 0;
}
}
return NULL;
}
public function free($ptr,$size){
$page_num = ceil($size / 4);
$page_index = floor(($ptr - $this->ptr)/4);
for($i = $page_index;$i < $page_num;$i++){
$this->free_map[$i] = 0;
}
$this->size += $page_num * 4;
}
}
内存分配就是根据需要分配的page_num查找连续0的个数是否满足条件,满足则返回连续0的起始位置ptr.
为了使内存分配更加紧凑,产生的间隙更少,需要查找可以分配间隙的最优解。也就是使free_page_num - page_num的值最小
当我们需要申请1Byte大小的内存时,我们分配一个page,但是这样就会造成3Byte的浪费。如果像chunk那样来管理page的话,显然没有必要。简单的办法是,我们可以用这个page专门用来分配1Byte大小的内存,这样一个page就可以分配4个1Byte大小的内存。我们将这样的内存块称为slot。
小于page的内存大小有1Byte、2Byte、3Byte。1Byte和2Byte都可以在一个page上分配,3Byte则无法占满一个page,我们可以用3个page来分配4个3Byte大小的内存(3*4Byte = 4*3Byte)。
这些小于page的内存称为small内存。
在HeapObject上增加一个新的成员变量用来管理slot。
HeapObject{
···
public free_slot = [];//array(size => array(ptr,ptr))
const SLOT_SIZE = [//[solt大小=>[需要的pape数量,可分配的slot数量]]
1=>[1,4],2=>[1,2],3=>[3,4]
],
public function small_malloc($size){
$ptr = array_shift($this->free_slot[$size]);
if($ptr == NULL){
$ptr = $this->large_malloc(self::SLOT_SIZE[$size][0]*4);
for($i = 1;$i< self::SLOT_SIZE[$size][1];$i++){
$this->free_slot[] = $slot_ptr + $i*$size;
}
}
return $ptr;
}
public function small_free($ptr,$size){
array_unshift($this->free_slot[$size],$ptr);
}
···
}
分配slot大小的内存时,先在HeapObject->free_slot上查找是否有可用的节点,有则直接返回,没有则分配对应的page并初始化slot内存。HeapObject->free_slot上只需要保存ptr的值就够了,size都是固定的。
现在的结构已可以满足所有大小的内存的分配,但是在内存释放的时候还是要告诉内存池需要释放的内存大小,无法满足动态内存释放的要求。
如果释放的内存大小与申请的内存大小不一致就会产生内存泄漏的问题。我们需要增加一定的规则才能使我们知道ptr所对应的内存大小。
常用的方法是使用内存对齐:使ptr向一定的规则对齐,如使chunk的内存起始地址都为100的整数倍,这样我们就能直接知道需要释放的ptr所对应的chunk指针(chunk_ptr = (ptr / 100)*100),而ptr所在的page_index = (ptr % 100)/4,但这样还无法知道我们需要释放的内存大小。所以我们需要额外的结构来管理。
ChunkObject{
···
public $map = [];//[page_info,page_info]
/**
* page_info:type_num
* type = 1 为large内存分配,num为所分配的page数量
* type = 2 为small内存分配,num为对应的size
*
* /
···
}
如上图,需要释放的ptr是205。首先获得chunk的起始地址为200,查到对应的ChunkObject。在计算得到page_index = 1,则在ChunkObject->map中对应的page_info为2_1。可以得到该ptr分配的是slot内存,大小为1字节,释放的时候将ptr加到HeapObject->free_slot对应数组的头部就可以了。
如果是page内存,则将ChunkObject->free_map中对应的数组元素置为0。
以上就是使用PHP来实现的内存池。但是用hashTable来管理节点,显然效率不会太高。接下来我们将一起来学习在C语言中,Zend内核是如何实现内存池。
Zend 内存池
首先我们先来回顾下上面所涉及到的概念
- Chunk:内存池中向系统申请和释放的最小单位,
chunk的大小为2M。 - Page:在
chunk中将内存切割成512个page,每个page的大小为4KB。 - slot:
Zend将小于page的内存按大小分成了30种规格,最小为8Byte,最大为3KB。
内存分配策略
- Huge内存:大于
2M的内存使用Huge内存分配策略,分配n个chunk大小的内存,使用huge_list链表管理。 - Large内存:大于
3KB,小于4092KB的内存使用large内存分配策略,分配n个连续的page内存,使用zend_mm_chunk结构体管理 - Small内存:小于等于3KB的内存使用
small内存分配策略,分配对应大小的slot内存。使用free_slot链表管理。
struct _zend_mm_heap {
zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
zend_mm_huge_list *huge_list; /* list of huge allocated blocks */
zend_mm_chunk *main_chunk;
zend_mm_chunk *cached_chunks; /* list of unused chunks */
int chunks_count; /* number of allocated chunks */
int peak_chunks_count; /* peak number of allocated chunks for current request */
int cached_chunks_count; /* number of cached chunks */
double avg_chunks_count; /* average number of chunks allocated per request */
int last_chunks_delete_boundary; /* number of chunks after last deletion */
int last_chunks_delete_count; /* number of deletion over the last boundary */
};
zend_mm_heap对应上文的HeapObject。
*free_slot为slot指针数组,ZEND_MM_BINS常量为30。数组保存的是各个规格slot链表的头部,链表的每个节点只保存next指针。
- 分配
small内存的时候,先查找heap->free_slot[bin_num]是否为null,是则重新分配slot内存,否则删除头部节点并返回ptr。 - 释放
small内存的时候,根据ptr获取page_info,获得slot的下标,将节点添加到链表头部就可以。
/* num, size, count, pages */
#define ZEND_MM_BINS_INFO(_, x, y) \
_( 0, 8, 512, 1, x, y) \
_( 1, 16, 256, 1, x, y) \
_( 2, 24, 170, 1, x, y) \
_( 3, 32, 128, 1, x, y) \
_( 4, 40, 102, 1, x, y) \
_( 5, 48, 85, 1, x, y) \
_( 6, 56, 73, 1, x, y) \
_( 7, 64, 64, 1, x, y) \
_( 8, 80, 51, 1, x, y) \
_( 9, 96, 42, 1, x, y) \
_(10, 112, 36, 1, x, y) \
_(11, 128, 32, 1, x, y) \
_(12, 160, 25, 1, x, y) \
_(13, 192, 21, 1, x, y) \
_(14, 224, 18, 1, x, y) \
_(15, 256, 16, 1, x, y) \
_(16, 320, 64, 5, x, y) \
_(17, 384, 32, 3, x, y) \
_(18, 448, 9, 1, x, y) \
_(19, 512, 8, 1, x, y) \
_(20, 640, 32, 5, x, y) \
_(21, 768, 16, 3, x, y) \
_(22, 896, 9, 2, x, y) \
_(23, 1024, 8, 2, x, y) \
_(24, 1280, 16, 5, x, y) \
_(25, 1536, 8, 3, x, y) \
_(26, 1792, 16, 7, x, y) \
_(27, 2048, 8, 4, x, y) \
_(28, 2560, 8, 5, x, y) \
_(29, 3072, 4, 3, x, y)
*huge_list为huge内存链表的头部,节点保存huge内存的*ptr、size和*next。
- 分配
huge内存的时候,将节点插入头部。 - 释放
huge内存的时候,根据*ptr遍历链表,删除对应的节点并释放内存。
struct _zend_mm_huge_list {
void *ptr;
size_t size;
zend_mm_huge_list *next;
};
*main_chunk为chunk链表的头部,节点为zend_mm_chunk。zend启动时会申请一块chunk作为main_chunk,main_chunk保存了zend_mm_heap结构,在进程结束之后才会被释放。chunk链表为环状双向链表,新申请的chunk都会添加到链表的尾部。需要申请page时遍历chunk链表查找是否有足够可分配的page。
*cached_chunks为chunk缓存链表的头部,回收chunk时并不会立即向系统释放内存,而是会根据一定规则设置缓存chunk的上限,当缓存chunk的数量达到上限时才会向系统释放。
zend_mm_chunk对应上文的ChunkObject。
struct _zend_mm_chunk {
zend_mm_heap *heap;
zend_mm_chunk *next;
zend_mm_chunk *prev;
uint32_t free_pages; /* number of free pages */
uint32_t free_tail; /* number of free pages at the end of chunk */
uint32_t num;
char reserve[64 - (sizeof(void*) * 3 + sizeof(uint32_t) * 3)];
zend_mm_heap heap_slot; /* used only in main chunk */
zend_mm_page_map free_map; /* 512 bits or 64 bytes */
zend_mm_page_info map[ZEND_MM_PAGES]; /* 2 KB = 512 * 4 */
};
zend_mm_heap只会在main_chunk中分配。
free_map为8个64位无符号整数数组或者16个32位无符号整数数组(根据操作系统区分)
zend使用bit位来保存page的分配情况,0为未分配,1为已分配。所以为了保存512个page的分配情况,需要512bit的空间。数组元素的值为-1时表示对应的page都已被分配。
分配page时就是在二进制数中查找连续0的个数。zend使用GCC内置函数__builtlin_czl(二进制数从右起连续0的个数),具体操作如下:
假设需要操作的数为一个字节
bit = 00100101
page_index = __builtlin_ctrl(~bit)//取反码 11011010 ,连续0的个数为1
tmp &= tmp + 1 //将右起连续1置为0 00100100
page_index2 = __builtlin_ctrl(tmp)//连续0的个数为2
page_num = page_index2 - page_index//可以得到page的可分配间隙 page下标为1,可分配page数量为1。
//将已查找过的位数置为1
tmp |= tmp - 1//00100111
//重复以上的步骤,查找出最佳的page_index和page_num
map为512个32位无符号整数数组
zend使用32位无符号整数的头两位来保存page的类型。用16进制来标识为:
large内存的page_info,首两位为01,16进制数表示为0x40000000。尾部用来保存使用的page数量,如0x40000003表示连续使用了3个page。slot内存第一个page的page_info,首两位为10,16进制数表示为0x80000000。尾部用来保存使用的slot规格,如0x80000001表示该page用来分配8Byte的slot内存。slot内存剩下page的page_info,首两位为11,16进制数表示为0xb0010000。尾部用来保存使用的slot规格,如0xb0010001表示该page用来分配8Byte的slot内存,中间部分(第16位开始)用来标识索引。
在申请chunk时,它的首个4KB的位置都会用来存放zend_mm_chunk结构,所以chunk只有511个page可供使用。
申请n*chunk内存时,会将内存地址ptr向2M位置对齐。第一次申请,如果(off = ptr % 2M )== 0则返回ptr,
否则将内存释放,重新申请(n+1)*chunk内存,将ptr向2M位置偏移,new_ptr = ptr + 2M - off,掐头去尾释放(ptr,2M - off)和(new_ptr+2M,off)的内存。
释放内存时就可以根据ptr来判断需要释放的是何种内存。
计算off = ptr % 2M:
off == 0,因为chunk的一个page被占用了,所以off为0的只能是Huge内存,直接在huge_list中查找对应的节点。off > 0,large或者small内存,需要查找对应的page_info才能确定,page_index = off / 4KB,在map中查找对应的page_info。type为large时,将对应的page置为未分配状态;type为small时,将ptr添加到对应的slot链表。
源码分析
1615

被折叠的 条评论
为什么被折叠?



