您的位置:首页 > 运维架构 > Nginx

#内存管理的艺术# 之 Nginx slab的实现 --- 第四篇“基于块的内存释放”

2016-05-19 00:00 639 查看
摘要: 前面我们已经讲了Nginx slab的基本内存分配机制, 但如果内存用完了不释放,下次再用时又去申请一块新的内存,这是什么?这是内存泄露啊,是要坚决避免的,所以本篇开始我们就来看看与内存分配相对应的释放机制吧!

访问这里,获取更多原创内容。

说明:本系列的文章基于Nginx-1.5.0版本代码。

本篇开始将涉及到Nginx slab内存管理中与内存释放相关的内容,紧跟上一篇的步伐,趁热打铁,就从“基于块的内存释放”开始吧。

开门见源码:

[code=plain]void
ngx_slab_free(ngx_slab_pool_t *pool, void *p)
{
ngx_shmtx_lock(&pool->mutex);

ngx_slab_free_locked(pool, p);

ngx_shmtx_unlock(&pool->mutex);
}

void
ngx_slab_free_locked(ngx_slab_pool_t *pool, void *p)
{
size_t            size;
uintptr_t         slab, m, *bitmap;
ngx_uint_t        n, type, slot, shift, map;
ngx_slab_page_t  *slots, *page;

ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0, "slab free: %p", p);

/*判断待释放的地址是否在合法的内存空间中*/
if ((u_char *) p < pool->start || (u_char *) p > pool->end) {
ngx_slab_error(pool, NGX_LOG_ALERT, "ngx_slab_free(): outside of pool");
goto fail;
}

/*由于页内存管理单元与真正的内存页是一一对应的,所以可以通过地址偏移量推算出数组下标,从而找到对应的页内存管理单元*/
n = ((u_char *) p - pool->start) >> ngx_pagesize_shift;
page = &pool->pages
;
/*根据前几篇的内容可以知道slab字段在不同情况下存储的内容有所不同*/
slab = page->slab;
type = page->prev & NGX_SLAB_PAGE_MASK;

/*针对不同的内存类型进行不同的操作*/
switch (type) {

/*对应chunk_size < exact_size的情况*/
case NGX_SLAB_SMALL:

/*在这种情况下,shift值实际可能是3、4、5、6,用低4bits就足够存放了*/
shift = slab & NGX_SLAB_SHIFT_MASK;
/*计算块大小*/
size = 1 << shift;

/*内存块的起始地址p应该与块大小对齐,这一点在内存分配的时候就已经保证了*/
if ((uintptr_t) p & (size - 1)) {
goto wrong_chunk;
}

/*计算地址p对应的内存块在页中的偏移量(是页中的第几个块,n从0开始)*/
n = ((uintptr_t) p & (ngx_pagesize - 1)) >> shift;
/*(n & (sizeof(uintptr_t) * 8 - 1)): 计算除了若干个uintptr_t类型的值外,还需要多少个bit才能完全表示n个bit位*/
/*m表示在一个uintptr_t类型中的偏移量*/
m = (uintptr_t) 1 << (n & (sizeof(uintptr_t) * 8 - 1));
/*计算要表示n个块所需要的uintptr_t值的个数*/
n /= (sizeof(uintptr_t) * 8);
bitmap = (uintptr_t *) ((uintptr_t) p & ~(ngx_pagesize - 1));

/*如果bitmap中对应的位没有置位,表示该块内存已经释放过了,不能重复释放,于是直接跳到chunk_already_free,否则就进行内存释放的相关操作*/
if (bitmap
& m) {
/*page->next==NULL: 表明对应页中的内存块在此前已经全部分配出去了,那么对其中的一块内存进行释放时还需要将对应的页内存管理单元重新挂接到slot分级管理数组的首部,以便在下次收到内存申请时能够继续从该页分配*/
if (page->next == NULL) {
slots = (ngx_slab_page_t *)
((u_char *) pool + sizeof(ngx_slab_pool_t));
slot = shift - pool->min_shift;

page->next = slots[slot].next;
slots[slot].next = page;

page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;
page->next->prev = (uintptr_t) page | NGX_SLAB_SMALL;
}

/*将对应的置位标记清除*/
bitmap
&= ~m;

/*计算需要用作bitmap的块数*/
n = (1 << (ngx_pagesize_shift - shift)) / 8 / (1 << shift);

if (n == 0) {
n = 1;
}

/*如果bitmap[0]中除了用作bitmap本身的块以外还有未释放的内存块,则直接返回,否则需要继续判断后续内存块是否都已经完全释放*/
if (bitmap[0] & ~(((uintptr_t) 1 << n) - 1)) {
goto done;
}

/*计算bitmap数组中的元素个数*/
map = (1 << (ngx_pagesize_shift - shift)) / (sizeof(uintptr_t) * 8);

/*如果还有未释放的内存块,则不做更多操作*/
for (n = 1; n < map; n++) {
if (bitmap
) {
goto done;
}
}

/*否则,将对应的整个内存页释放掉*/
ngx_slab_free_pages(pool, page, 1);

goto done;
}

goto chunk_already_free;

/*对应chunk_size == exact_size的情况*/
case NGX_SLAB_EXACT:

m = (uintptr_t) 1 <<
(((uintptr_t) p & (ngx_pagesize - 1)) >> ngx_slab_exact_shift);
size = ngx_slab_exact_size;

/*同样地,内存块的起始地址需要与块大小对齐*/
if ((uintptr_t) p & (size - 1)) {
goto wrong_chunk;
}

/*这种情况下,slab字段存放的就是bitmap*/
if (slab & m) {
/*同样,如果在此之前该页中的内存已经全部分配完毕了,那么需要将该页对应的页内存管理单元重新挂接到slot分级数组下,这样在下一次内存申请时就可以从该页中分配了*/
if (slab == NGX_SLAB_BUSY) {
slots = (ngx_slab_page_t *)
((u_char *) pool + sizeof(ngx_slab_pool_t));
slot = ngx_slab_exact_shift - pool->min_shift;

page->next = slots[slot].next;
slots[slot].next = page;

page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_EXACT;
page->next->prev = (uintptr_t) page | NGX_SLAB_EXACT;
}

/*将对应的内存块标记为空闲*/
page->slab &= ~m;

/*如果该页中还有未释放的内存块,则不做其它操作*/
/*注意,如果前面slab==NGX_SLAB_BUSY成立的话,那么这个判断也一定成立*/
if (page->slab) {
goto done;
}

/*否则释放整页*/
ngx_slab_free_pages(pool, page, 1);

goto done;
}

goto chunk_already_free;

/*对应chunk_size > exact_size的情况*/
case NGX_SLAB_BIG:

shift = slab & NGX_SLAB_SHIFT_MASK;
size = 1 << shift;

if ((uintptr_t) p & (size - 1)) {
goto wrong_chunk;
}

/*计算与内存块对应的bit位*/
m = (uintptr_t) 1 << ((((uintptr_t) p & (ngx_pagesize - 1)) >> shift)
+ NGX_SLAB_MAP_SHIFT);

if (slab & m) {

if (page->next == NULL) {
slots = (ngx_slab_page_t *)
((u_char *) pool + sizeof(ngx_slab_pool_t));
slot = shift - pool->min_shift;

page->next = slots[slot].next;
slots[slot].next = page;

page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_BIG;
page->next->prev = (uintptr_t) page | NGX_SLAB_BIG;
}

page->slab &= ~m;

if (page->slab & NGX_SLAB_MAP_MASK) {
goto done;
}

ngx_slab_free_pages(pool, page, 1);

goto done;
}

goto chunk_already_free;

/*如果地址p对应的内存是按页分配的,就进入下面的流程*/
case NGX_SLAB_PAGE:

/*这时地址p应该与页大小对齐*/
if ((uintptr_t) p & (ngx_pagesize - 1)) {
goto wrong_chunk;
}

/*NGX_SLAB_PAGE_FREE定义为0,表示该页是空闲的*/
if (slab == NGX_SLAB_PAGE_FREE) {
ngx_slab_error(pool, NGX_LOG_ALERT,
"ngx_slab_free(): page is already free");
goto fail;
}

/*NGX_SLAB_PAGE_BUSY表示这不是一段连续内存空间中的第一页*/
if (slab == NGX_SLAB_PAGE_BUSY) {
ngx_slab_error(pool, NGX_LOG_ALERT,
"ngx_slab_free(): pointer to wrong page");
goto fail;
}

/*根据内存地址推算页管理单元的下标*/
n = ((u_char *) p - pool->start) >> ngx_pagesize_shift;
/*这段连续内存空间中的页数目*/
size = slab & ~NGX_SLAB_PAGE_START;

/*将具体实现交给“基于页的内存释放”函数来完成*/
ngx_slab_free_pages(pool, &pool->pages
, size);

ngx_slab_junk(p, size << ngx_pagesize_shift);

return;
}

/* not reached */

return;

done:

ngx_slab_junk(p, size);

return;

wrong_chunk:

ngx_slab_error(pool, NGX_LOG_ALERT,
"ngx_slab_free(): pointer to wrong chunk");

goto fail;

/*内存已经释放过了,则什么也不做*/
chunk_already_free:

ngx_slab_error(pool, NGX_LOG_ALERT,
"ngx_slab_free(): chunk is already free");

fail:

return;
}


下面我们基于如下的一种场景来讨论“基于块的内存释放”:

------ 假设程序中会大量申请chunk_size = 16bytes的内存块,在将一整页的内存块都分配出去后仍然不能满足需求,所以需要继续从新的内存页中进行划分,直到后来的某一个时刻释放了一块内存,那么这种场景对应的内存布局如下:



上面这幅图对应了在将整个的m1内存页都分配出去后,继续从m0内存页分配内存块时的内存布局。需要注意的是,这时与m1内存页对应的内存管理单元并没有挂接在相应的slot分级管理链表下。

然后在某一时刻,释放了m1内存页中的某个内存块,这时的内存情况布局如下图:



可以看到,当向一个已经完全分配的内存页中释放一个内存块时,这个页对应的页内存管理单元会被重新挂接到相应的slot分级管理链表首部,为的是在下次内存申请时能够继续从该页中进行分配。

从代码中我们还可以看到,当一个页中的内存块已经完全释放的时候,会调用“基于页的内存释放”函数来完成整个内存页的释放,这一部分内容将会在下一篇中继续讲解。

这一篇的内容就结束了,怎么样,有没有再次感受到Nginx slab分配器设计的巧妙之处,比如建立在”内存对齐”机制上“自我管理“能力,使得在进行内存释放时只需给出内存(块/页)的起始地址,就可以推算出内存类型、待释放的连续内存页数、页内存管理单元地址和分级内存管理单元地址等内容,而无需再使用额外的数据结构。

P.S. 最后再抛出一个问题,Nginx slab在管理小于exact_size的内存块时,其实是需要将页起始地址处的一部分内存拿来用作bitmap的,并同时将对应的bit置位,以保证在分配时不会将这些块分配出去,但在释放的流程中并没有对待释放的地址进行判断,也就是说一个用作bitmap的块地址有可能被“有意”或“无意”的传递进来,从而被不正确地“释放”了,这算不算是“百密难免一疏”呢?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Nginx slab 内存管理