访问这里,获取更多原创内容。
说明:本系列的文章基于Nginx-1.5.0版本代码。
在“基本布局”一篇中我们曾经介绍过,ngx_slab.c的实现中将内存的分配分为了两个大类,除了上一篇讲的“基于页的内存分配”外,另一类就是本篇中要介绍的“基于块的内存分配”了。
为了能够满足对小块内存的申请需求,Nginx slab分配器将页划分为更小的块(chunk),并引入了“slot分级内存管理数组”来与“page页内存管理数组”一起完成对小块内存的分配和释放流程的管理。
根据“基本布局”一篇中的内容可以知道,Nginx slab分配器是按2的幂次大小来进行分级的:
Nginx slab分配器的分级结构图
当最小块大小(min_size)为8bytes,页大小(pagesize)为4096bytes时,分级数为:
pagesize_shift - min_shift = 9
即对应ngx_slab_init()中的n为9,也就是说slot分级管理数组中有9个元素,我们后面的讨论都是基于这个模型来进行的。
在上面的“Nginx slab分配器的分级结构图”中我们还看到了两个比较陌生的变量:ngx_slab_exact_shift(7) 和 ngx_slab_exact_size(128),这两个值分别代表什么含义,又是怎么得到的呢?
前面说过,基于页的内存管理单元和基于块的内存管理单元均采用了ngx_slab_page_t结构来表示:
typedef struct ngx_slab_page_s ngx_slab_page_t;
struct ngx_slab_page_s {
uintptr_t slab;
ngx_slab_page_t *next;
uintptr_t prev;
};
/*
typedef long intptr_t;
typedef unsigned long uintptr_t;
*/
其中的slab字段在不同的场景下表示不同的含义:
- 在基于页的内存管理流程中,它可以表示连续的空闲页数目;
- 在基于块的内存管理流程中:
1)当chunk_size == exact_size时,slab字段用作bitmap,表示一页中各个内存块的使用情况;
2)当chunk_size < exact_size时,slab字段用于存储与块大小相对应的移位数(chunk_shift);
3)当chunk_size > exact_size时,slab字段的高16bits用作bitmap,而低16bits则用于存储与块大小对应的移位数;
其中第一种含义在“基于页的内存分配”中已经涉及到了,下面就来解释一下后面的三种含义。
在进行基于块的内存管理时,slab分配器需要借助bitmap来标记一页中各个内存块的使用情况 ,如果想用一个uintptr_t类型的值来完全表示一页中的所有块,那么一页能划分的块数为:8 * sizeof(uintptr_t),每块的大小就等于pagesize / (8 * sizeof(uintptr_t)),在32位环境下,就是 4096 / 32 = 128,这个值就是上面看到的ngx_slab_exact_size。
当划分的块小于exact_size时,使用一个uintptr_t是无法完全表示一页中的所有内存块的,需要借用page页中起始地址处的部分内存空间作为bitmap,这时的slab字段只用于存储与页大小对应的shift移位数。
当划分的块大于exact_size时,最多使用16bits就可以完全表示一个页中所有块的分配情况了,这样就可以把一个uintptr_t类型值分为2个部分,其中高16位用作bitmap,标记块的分配情况,而低16位则用于记录对应的shift移位数。
所以说,ngx_slab_exact_shift 和 ngx_slab_exact_size这两个变量实际上在内存块的分级管理中起到了非常重要的作用,slab分配器正是基于这两个变量的值又将内存块的管理细分为上述3种不同的方式,如果你是初次接触这部分的概念,不妨再仔细理解一下吧。
下面就结合源码来深入学习吧:
void *
ngx_slab_alloc_locked(ngx_slab_pool_t *pool, size_t size)
{
size_t s;
uintptr_t p, n, m, mask, *bitmap;
ngx_uint_t i, slot, shift, map;
ngx_slab_page_t *page, *prev, *slots;
...
/*基于页的内存分配*/
...
/*基于块的内存分配*/
/*根据申请的内存大小计算对应的分级数*/
if (size > pool->min_size) {
shift = 1;
for (s = size - 1; s >>= 1; shift++) { /* void */ }
slot = shift - pool->min_shift;
} else {
size = pool->min_size;
shift = pool->min_shift;
slot = 0;
}
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0,
"slab alloc: %uz slot: %ui", size, slot);
/*找到分级数组的首地址*/
slots = (ngx_slab_page_t *) ((u_char *) pool + sizeof(ngx_slab_pool_t));
page = slots[slot].next;
/*根据“基本布局”一篇所讲的内容,在初始化完成之后slot分级数组下其实没有挂接任何内容,所以在第一次分配时这里的条件并不满足,会跳到后面去执行*/
if (page->next != page) {
...
...
}
/*如果是第一次分配小块内存,slot分级数组下还没有挂接任何内容,则会先跳转到这里分配一页,所以说基于块的内存分配实际上可以看作是在内存页的分配基础上又做了进一步地细分,这也是我们为什么把这一部分放在页内存分配之后的原因*/
page = ngx_slab_alloc_pages(pool, 1);
if (page) {
/*对应于chunk_size < exact_size的情况*/
if (shift < ngx_slab_exact_shift) {
/*这种情况下需要将内存页的起始部分划分出来用作bitmap*/
p = (page - pool->pages) << ngx_pagesize_shift;
bitmap = (uintptr_t *) (pool->start + p);
/*块大小(取整到2的整数次幂)*/
s = 1 << shift;
/*
(1 << (ngx_pagesize_shift - shift)) : 当块大小为s时,一个整页可以被划分的块数;
(一页中的块数 / 8) : 当每块用一个bit标识时,对应需要的字节(bytes)数;
(字节数 / s) : 得到bitmap需要占用的块数n
*/
n = (1 << (ngx_pagesize_shift - shift)) / 8 / s;
if (n == 0) {
n = 1;
}
/*将bitmap占用的块数和即将分配出去的块标记为占用*/
bitmap[0] = (2 << n) - 1;
/*与上面类似,这里计算存放bitmap所需要的uintptr_t类型数据的个数,即bitmap数组长度*/
map = (1 << (ngx_pagesize_shift - shift)) / (sizeof(uintptr_t) * 8);
/*将后续的标志位都清零*/
for (i = 1; i < map; i++) {
bitmap[i] = 0;
}
/*这时的slab字段存放与块大小对应的shift移位值*/
page->slab = shift;
page->next = &slots[slot];
page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;
slots[slot].next = page;
/*跳过用作bitmap的内存块, 找到第一个空闲块*/
p = ((page - pool->pages) << ngx_pagesize_shift) + s * n;
p += (uintptr_t) pool->start;
goto done;
} else if (shift == ngx_slab_exact_shift) {/*对应于chunk_size == exact_size的情况*/
/*这种情况下slab字段被用作bitmap,将其赋值为1表示第一块即将被分配出去*/
page->slab = 1;
page->next = &slots[slot];
page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_EXACT;
slots[slot].next = page;
p = (page - pool->pages) << ngx_pagesize_shift;
p += (uintptr_t) pool->start;
goto done;
} else { /* shift > ngx_slab_exact_shift */ /*对应于chunk_size > exact_size的情况*/
/*这时最多只需要16bits就可以完全表示一页中所有块的分配情况了, 因此可以将slab字段分为两个部分,其中的高16bits用作bitmap,而低16bits则记录与块大小相应的移位数*/
page->slab = ((uintptr_t) 1 << NGX_SLAB_MAP_SHIFT) | shift;
page->next = &slots[slot];
page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_BIG;
slots[slot].next = page;
p = (page - pool->pages) << ngx_pagesize_shift;
p += (uintptr_t) pool->start;
goto done;
}
}
p = 0;
done:
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0, "slab alloc: %p", p);
return (void *) p;
}
下面的这幅布局图非常直观地解释了上述代码,这里给出的是 chunk_size(=16bytes) < exact_size 这种相对复杂场景下的内存分配情况,举一反三,另外两种场景就会很容易理解了。
有了上面的内容作为铺垫,再来看看当不是第一次分配小块内存的情况下的处理流程:
void *
ngx_slab_alloc_locked(ngx_slab_pool_t *pool, size_t size)
{
size_t s;
uintptr_t p, n, m, mask, *bitmap;
ngx_uint_t i, slot, shift, map;
ngx_slab_page_t *page, *prev, *slots;
...
/*基于页的内存分配*/
...
/*基于块的内存分配*/
/*根据请求的内存大小计算对应的分级数*/
if (size > pool->min_size) {
shift = 1;
for (s = size - 1; s >>= 1; shift++) { /* void */ }
slot = shift - pool->min_shift;
} else {
size = pool->min_size;
shift = pool->min_shift;
slot = 0;
}
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0,
"slab alloc: %uz slot: %ui", size, slot);
/*找到分级数组的首地址*/
slots = (ngx_slab_page_t *) ((u_char *) pool + sizeof(ngx_slab_pool_t));
page = slots[slot].next;
/*如果slot分级数组下已经挂接了page页管理结构,就从这里开始执行*/
if (page->next != page) {
if (shift < ngx_slab_exact_shift) {
do {
p = (page - pool->pages) << ngx_pagesize_shift;
bitmap = (uintptr_t *) (pool->start + p);
/*计算bitmap数组长度*/
map = (1 << (ngx_pagesize_shift - shift))
/ (sizeof(uintptr_t) * 8);
/*遍历bitmap数组,找到一个空闲块*/
for (n = 0; n < map; n++) {
/*在32位环境下,NGX_SLAB_BUSY=0xffffffff,表示对应的bit全部置位,也就代表了没有空闲块*/
if (bitmap[n] != NGX_SLAB_BUSY) {
/*还有空闲块*/
for (m = 1, i = 0; m; m <<= 1, i++) {
if ((bitmap[n] & m)) {
continue;
}
bitmap[n] |= m;
/*计算已经分配的块对应的内存空间大小*/
i = ((n * sizeof(uintptr_t) * 8) << shift)
+ (i << shift);
if (bitmap[n] == NGX_SLAB_BUSY) {
for (n = n + 1; n < map; n++) {
/*如果后面还有空闲块,则无需特殊处理,直接返回本次分配的地址*/
if (bitmap[n] != NGX_SLAB_BUSY) {
p = (uintptr_t) bitmap + i;
goto done;
}
}
/*该页中已经没有空闲块,则将它从slot分级数组中摘掉,下次再有新的内存申请时可能需要重新申请一个新页*/
prev = (ngx_slab_page_t *)
(page->prev & ~NGX_SLAB_PAGE_MASK);
prev->next = page->next;
page->next->prev = page->prev;
page->next = NULL;
page->prev = NGX_SLAB_SMALL;
}
p = (uintptr_t) bitmap + i;
goto done;
}
}
}
page = page->next;
} while (page);
} else if (shift == ngx_slab_exact_shift) {
do {
/*这时slab字段中存储的就是bitmap本身,slab!=NGX_SLAB_BUSY表示还有空闲块*/
if (page->slab != NGX_SLAB_BUSY) {
for (m = 1, i = 0; m; m <<= 1, i++) {
if ((page->slab & m)) {
continue;
}
page->slab |= m;
if (page->slab == NGX_SLAB_BUSY) {
prev = (ngx_slab_page_t *)
(page->prev & ~NGX_SLAB_PAGE_MASK);
prev->next = page->next;
page->next->prev = page->prev;
page->next = NULL;
page->prev = NGX_SLAB_EXACT;
}
/*计算页首地址偏移量*/
p = (page - pool->pages) << ngx_pagesize_shift;
/*计算块地址偏移量*/
p += i << shift;
p += (uintptr_t) pool->start;
goto done;
}
}
page = page->next;
} while (page);
} else { /* shift > ngx_slab_exact_shift */
/*在32位环境下,当exact_shift < chunk_shift( < page_shift)时,chunk_shift的取值可能为8、9、10、11, 实际只需4个bits就可以表示了,slab & NGX_SLAB_SHIFT_MASK(=0x0f),可以取出chunk_shift*/
n = ngx_pagesize_shift - (page->slab & NGX_SLAB_SHIFT_MASK);
/*得到一个页面中所能划分的块数*/
n = 1 << n;
/*这些块对应的bitmap掩码*/
n = ((uintptr_t) 1 << n) - 1;
/*32位环境下,NGX_SLAB_MAP_SHIFT=16,即将掩码移到高16位上,因为这时slab字段的高16位中存放的是bitmap的内容*/
mask = n << NGX_SLAB_MAP_SHIFT;
/*注意,上面这几步的计算还是很巧妙的*/
do {
/*NGX_SLAB_MAP_MASK=0xffff0000,根据掩码判断是否还有空闲块*/
if ((page->slab & NGX_SLAB_MAP_MASK) != mask) {
/*逐位查找空闲块*/
for (m = (uintptr_t) 1 << NGX_SLAB_MAP_SHIFT, i = 0;
m & mask;
m <<= 1, i++)
{
if ((page->slab & m)) {
continue;
}
page->slab |= m;
if ((page->slab & NGX_SLAB_MAP_MASK) == mask) {
prev = (ngx_slab_page_t *)
(page->prev & ~NGX_SLAB_PAGE_MASK);
prev->next = page->next;
page->next->prev = page->prev;
page->next = NULL;
page->prev = NGX_SLAB_BIG;
}
p = (page - pool->pages) << ngx_pagesize_shift;
p += i << shift;
p += (uintptr_t) pool->start;
goto done;
}
}
page = page->next;
} while (page);
}
}
/*如果是第一次分配小块内存,slot分级数组下还没有挂接任何内容,则会先跳转到这里分配一页*/
page = ngx_slab_alloc_pages(pool, 1);
if (page) {
...
...
}
p = 0;
done:
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0, "slab alloc: %p", p);
return (void *) p;
}
下面一幅图给出了当一页中的内存块都已经分配完毕,没有空闲块时的内存布局情况:
到这里Nginx slab中小块内存的分配流程就基本介绍完了,可以看到,由于采用了内存对齐等机制,Nginx slab中的内存分配可以很好地实现“自我管理”,也就是说在给出起始地址的情况下能够方便快捷地正确推算出相关管理结构的位置和内容,而无需额外的存储结构,这种设计值得好好学习和理解。