温馨提示:本文用到了一些可以在启动memcached设置的全局变量。关于这些全局变量的含义可以参考《memcached启动参数详解》。对于这些全局变量,处理方式就像《如何阅读memcached源代码》所说的那样直接取其默认值

slab内存池分配器:

slab简介:

memcached使用了一个叫slab的内存分配方法,有关slab的介绍可以参考链接1链接2。可以简单地把它看作内存池。memcached内存池分配的内存块大小是固定的。虽然是固定大小,但memcached的能分配的内存大小(尺寸)也是有很多种规格的。一般来说,是满足需求的。

memcached声明了一个slabclass_t结构体类型,并且定义了一个slabclass_t类型数组slabclass(是一个全局变量)。可以把数组的每一个元素称为一个slab分配器。一个slab分配器能分配的内存大小是固定的,不同的slab分配的内存大小是不同的。下面借一幅经典的图来说明:

从每个slab class(slab分配器)分配出去的内存块都会用指针连接起来的(连起来才不会丢失啊)。如下图所示:

上图是一个逻辑图。每一个item都不大,从几B到1M。如果每一个item都是地动态调用malloc申请的,势必会造成很多内存碎片。所以memcached的做法是,先申请一个比较大的一块内存,然后把这块内存划分成一个个的item,并用两个指针(prev和next)把这些item连接起来。所以实际的物理图如下所示:

上图中,每一个slabclass_t都有一个slab数组。同一个slabclass_t的多个slab分配的内存大小是相同的,不同的slabclass_t分配的内存大小是不同的。因为每一个slab分配器能分配出去的总内存都是有一个上限的,所以对于一个slabclass_t来说,要想分配很多内存就必须有多个slab分配器。

确定slab分配器的分配规格:

看完了图,现在来看一下memcached是怎么确定slab分配器的分配规格的。因为memcached使用了全局变量,先来看一下全局变量。

//slabs.c文件
typedef struct {
unsigned int size;//slab分配器分配的item的大小
unsigned int perslab; //每一个slab分配器能分配多少个item void *slots; //指向空闲item链表
unsigned int sl_curr; //空闲item的个数 //这个是已经分配了内存的slabs个数。list_size是这个slabs数组(slab_list)的大小
unsigned int slabs; //本slabclass_t可用的slab分配器个数
//slab数组,数组的每一个元素就是一个slab分配器,这些分配器都分配相同尺寸的内存
void **slab_list;
unsigned int list_size; //slab数组的大小, list_size >= slabs //用于reassign,指明slabclass_t中的哪个块内存要被其他slabclass_t使用
unsigned int killing; size_t requested; //本slabclass_t分配出去的字节数
} slabclass_t; #define POWER_SMALLEST 1
#define POWER_LARGEST 200
#define CHUNK_ALIGN_BYTES 8
#define MAX_NUMBER_OF_SLAB_CLASSES (POWER_LARGEST + 1) //数组元素虽然有MAX_NUMBER_OF_SLAB_CLASSES个,但实际上并不是全部都使用的。
//实际使用的元素个数由power_largest指明
static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];//201
static int power_largest;//slabclass数组中,已经使用了的元素个数.

可以看到,上面的代码定义了一个全局slabclass数组。这个数组就是前面那些图的slabclass_t数组。虽然slabclass数组有201个元素,但可能并不会所有元素都使用的。由全局变量power_largest指明使用了多少个元素.下面看一下slabs_init函数,该函数对这个数组进行一些初始化操作。该函数会在main函数中被调用。

//slabs.c文件
static size_t mem_limit = 0;//用户设置的内存最大限制
static size_t mem_malloced = 0; //如果程序要求预先分配内存,而不是到了需要的时候才分配内存,那么
//mem_base就指向那块预先分配的内存.
//mem_current指向还可以使用的内存的开始位置
//mem_avail指明还有多少内存是可以使用的
static void *mem_base = NULL;
static void *mem_current = NULL;
static size_t mem_avail = 0; //参数factor是扩容因子,默认值是1.25
void slabs_init(const size_t limit, const double factor, const bool prealloc) {
int i = POWER_SMALLEST - 1; //settings.chunk_size默认值为48,可以在启动memcached的时候通过-n选项设置
//size由两部分组成: item结构体本身 和 这个item对应的数据
//这里的数据也就是set、add命令中的那个数据.后面的循环可以看到这个size变量会
//根据扩容因子factor慢慢扩大,所以能存储的数据长度也会变大的
unsigned int size = sizeof(item) + settings.chunk_size; mem_limit = limit;//用户设置或者默认的内存最大限制 //用户要求预分配一大块的内存,以后需要内存,就向这块内存申请。
if (prealloc) {//默认值为false
mem_base = malloc(mem_limit);
if (mem_base != NULL) {
mem_current = mem_base;
mem_avail = mem_limit;
} else {
fprintf(stderr, "Warning: Failed to allocate requested memory in"
" one large chunk.\nWill allocate in smaller chunks\n");
}
} //初始化数组,这个操作很重要,数组中所有元素的成员变量值都为0了
memset(slabclass, 0, sizeof(slabclass)); //slabclass数组中的第一个元素并不使用
//settings.item_size_max是memcached支持的最大item尺寸,默认为1M(也就是网上
//所说的memcached存储的数据最大为1MB)。
while (++i < POWER_LARGEST && size <= settings.item_size_max / factor) {
/* Make sure items are always n-byte aligned */
if (size % CHUNK_ALIGN_BYTES)//8字节对齐
size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES); //这个slabclass的slab分配器能分配的item大小
slabclass[i].size = size;
//这个slabclass的slab分配器最多能分配多少个item(也决定了最多分配多少内存)
slabclass[i].perslab = settings.item_size_max / slabclass[i].size;
size *= factor;//扩容
} //最大的item
power_largest = i;
slabclass[power_largest].size = settings.item_size_max;
slabclass[power_largest].perslab = 1; ... if (prealloc) {//预分配内存
slabs_preallocate(power_largest);
}
}

上面代码中出现的item是用来存储我们放在memcached的数据。代码中的循环决定了slabclass数组中的每一个slabclass_t能分配的item大小,也就是slab分配器能分配的item大小,同时也确定了slab分配器能分配的item个数。

上面的代码还可以看到,可以通过增大settings.item_size_max而使得memcached可以存储更大的一条数据信息。当然是有限制的,最大也只能为128MB。巧的是,slab分配器能分配的最大内存也是受这个settings.item_size_max所限制。因为每一个slab分配器能分配的最大内存有上限,所以slabclass数组中的每一个slabclass_t都有多个slab分配器,其用一个数组管理这些slab分配器。而这个数组大小是不受限制的,所以对于某个特定的尺寸的item是可以有很多很多的。当然整个memcached能分配的总内存大小也是有限制的,可以在启动memcached的时候通过-m选项设置,默认值为64MB。slabs_init函数中的limit参数就是memcached能分配的总内存。

预分配内存:

现在就假设用户需要预先分配一些内存,而不是等到客户端发送存储数据命令的时候才分配内存。slabs_preallocate函数是为slabclass数组中每一个slabclass_t元素预先分配一些空闲的item。由于item可能比较小(上面的代码也可以看到这一点),所以不能以item为单位申请内存(这样很容易造成内存碎片)。于是在申请的使用就申请一个比较大的一块内存,然后把这块内存划分成一个个的item,这样就等于申请了多个item。本文将申请得到的这块内存称为内存页,也就是申请了一个页。如果全局变量settings.slab_reassign为真,那么页的大小为settings.item_size_max,否则等于slabclass_t.size
* slabclass_t.perslab。settings.slab_reassign主要用于平衡各个slabclass_t的。后文将统一使用内存页、页大小称呼这块分配内存,不区分其大小。

现在就假设用户需要预先分配内存,看一下slabs_preallocate函数。该函数的参数值为使用到的slabclass数组元素个数。slabs_preallocate函数的调用是分配slab内存块和和设置item的。

//参数值为使用到的slabclass数组元素个数
//为slabclass数组的每一个元素(使用到的元素)分配内存
static void slabs_preallocate (const unsigned int maxslabs) {
int i;
unsigned int prealloc = 0; //遍历slabclass数组
for (i = POWER_SMALLEST; i <= POWER_LARGEST; i++) {
if (++prealloc > maxslabs)//当然是只遍历使用了的数组元素
return;
if (do_slabs_newslab(i) == 0) {//为每一个slabclass_t分配一个内存页
//如果分配失败,将退出程序.因为这个预分配的内存是后面程序运行的基础
//如果这里分配失败了,后面的代码无从执行。所以就直接退出程序。
exit(1);
}
} } //slabclass_t中slab的数目是慢慢增多的。该函数的作用就是为slabclass_t申请多一个slab
//参数id指明是slabclass数组中的那个slabclass_t
static int do_slabs_newslab(const unsigned int id) {
slabclass_t *p = &slabclass[id];
//settings.slab_reassign的默认值为false,这里就采用false。
int len = settings.slab_reassign ? settings.item_size_max
: p->size * p->perslab;//其积 <= settings.item_size_max
char *ptr; //mem_malloced的值通过环境变量设置,默认为0
if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) ||
(grow_slab_list(id) == 0) ||//增长slab_list(失败返回0)。一般都会成功,除非无法分配内存
((ptr = memory_allocate((size_t)len)) == 0)) {//分配len字节内存(也就是一个页) return 0;
} memset(ptr, 0, (size_t)len);//清零内存块是必须的
//将这块内存切成一个个的item,当然item的大小有id所控制
split_slab_page_into_freelist(ptr, id); //将分配得到的内存页交由slab_list掌管
p->slab_list[p->slabs++] = ptr;
mem_malloced += len; return 1;
}

上面的do_slabs_newslab函数内部调用了三个函数。函数grow_slab_list的作用是增大slab数组的大小(如下图所示的slab数组)。memory_allocate函数则是负责申请大小为len字节的内存。而函数split_slab_page_into_freelist则负责把申请到的内存切分成多个item,并且把这些item用指向连起来,形成双向链表。如下图所示:前面已经见过这图了,看完代码再来看一下吧。

下面看一下那三个函数的具体实现。

//增加slab_list成员指向的内存,也就是增大slab_list数组。使得可以有更多的slab分配器
//除非内存分配失败,否则都是返回1,无论是否真正增大了
static int grow_slab_list (const unsigned int id) {
slabclass_t *p = &slabclass[id];
if (p->slabs == p->list_size) {//用完了之前申请到的slab_list数组的所有元素
size_t new_size = (p->list_size != 0) ? p->list_size * 2 : 16;
void *new_list = realloc(p->slab_list, new_size * sizeof(void *));
if (new_list == 0) return 0;
p->list_size = new_size;
p->slab_list = new_list;
}
return 1;
} //申请分配内存,如果程序是有预分配内存块的,就向预分配内存块申请内存
//否则调用malloc分配内存
static void *memory_allocate(size_t size) {
void *ret; //如果程序要求预先分配内存,而不是到了需要的时候才分配内存,那么
//mem_base就指向那块预先分配的内存.
//mem_current指向还可以使用的内存的开始位置
//mem_avail指明还有多少内存是可以使用的
if (mem_base == NULL) {//不是预分配内存
/* We are not using a preallocated large memory chunk */
ret = malloc(size);
} else {
ret = mem_current; //在字节对齐中,最后几个用于对齐的字节本身就是没有意义的(没有被使用起来)
//所以这里是先计算size是否比可用的内存大,然后才计算对齐 if (size > mem_avail) {//没有足够的可用内存
return NULL;
} //现在考虑对齐问题,如果对齐后size 比mem_avail大也是无所谓的
//因为最后几个用于对齐的字节不会真正使用
/* mem_current pointer _must_ be aligned!!! */
if (size % CHUNK_ALIGN_BYTES) {//字节对齐.保证size是CHUNK_ALIGN_BYTES (8)的倍数
size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);
} mem_current = ((char*)mem_current) + size;
if (size < mem_avail) {
mem_avail -= size;
} else {//此时,size比mem_avail大也无所谓
mem_avail = 0;
}
} return ret;
} //将ptr指向的内存页划分成一个个的item
static void split_slab_page_into_freelist(char *ptr, const unsigned int id) {
slabclass_t *p = &slabclass[id];
int x;
for (x = 0; x < p->perslab; x++) {
//将ptr指向的内存划分成一个个的item.一共划成perslab个
//并将这些item前后连起来。
//do_slabs_free函数本来是worker线程向内存池归还内存时调用的。但在这里
//新申请的内存也可以当作是向内存池归还内存。把内存注入内存池中
do_slabs_free(ptr, 0, id);
ptr += p->size;//size是item的大小
}
} static void do_slabs_free(void *ptr, const size_t size, unsigned int id) {
slabclass_t *p;
item *it; assert(((item *)ptr)->slabs_clsid == 0);
assert(id >= POWER_SMALLEST && id <= power_largest);
if (id < POWER_SMALLEST || id > power_largest)
return; p = &slabclass[id]; it = (item *)ptr;
//为item的it_flags添加ITEM_SLABBED属性,标明这个item是在slab中没有被分配出去
it->it_flags |= ITEM_SLABBED; //由split_slab_page_into_freelist调用时,下面4行的作用是
//让这些item的prev和next相互指向,把这些item连起来.
//当本函数是在worker线程向内存池归还内存时调用,那么下面4行的作用是,
//使用链表头插法把该item插入到空闲item链表中。
it->prev = 0;
it->next = p->slots;
if (it->next) it->next->prev = it;
p->slots = it;//slot变量指向第一个空闲可以使用的item p->sl_curr++;//空闲可以使用的item数量
p->requested -= size;//减少这个slabclass_t分配出去的字节数
return;
}

在do_slabs_free函数的注释说到,在worker线程向内存池归还内存时,该函数也是会被调用的。因为同一slab内存块中的各个item归还时间不同,所以memcached运行一段时间后,item链表就会变得很混乱,不会像上面那个图那样。有可能如下图那样:

虽然混乱,但肯定还是会有前面那张逻辑图那样的清晰链表图,其中slots变量指向第一个空闲的item。

向内存池申请内存:

与do_slabs_free函数对应的是do_slabs_alloc函数。当worker线程向内存池申请内存时就会调用该函数。在调用之前就要根据所申请的内存大小,确定好要向slabclass数组的哪个元素申请内存了。函数slabs_clsid就是完成这个任务。

unsigned int slabs_clsid(const size_t size) {//返回slabclass索引下标值
int res = POWER_SMALLEST;//res的初始值为1 //返回0表示查找失败,因为slabclass数组中,第一个元素是没有使用的
if (size == 0)
return 0; //因为slabclass数组中各个元素能分配的item大小是升序的
//所以从小到大直接判断即可在数组找到最小但又能满足的元素
while (size > slabclass[res].size)
if (res++ == power_largest) /* won't fit in the biggest slab */
return 0;
return res;
}

在do_slabs_alloc函数中如果对应的slabclass_t有空闲的item,那么就直接将之分配出去。否则就需要扩充slab得到一些空闲的item然后分配出去。代码如下面所示:

//向slabclass申请一个item。在调用该函数之前,已经调用slabs_clsid函数确定
//本次申请是向哪个slabclass_t申请item了,参数id就是指明是向哪个slabclass_t
//申请item。如果该slabclass_t是有空闲item,那么就从空闲的item队列中分配一个
//如果没有空闲item,那么就申请一个内存页。再从新申请的页中分配一个item
//返回值为得到的item,如果没有内存了,返回NULL
static void *do_slabs_alloc(const size_t size, unsigned int id) {
slabclass_t *p;
void *ret = NULL;
item *it = NULL; if (id < POWER_SMALLEST || id > power_largest) {//下标越界
MEMCACHED_SLABS_ALLOCATE_FAILED(size, 0);
return NULL;
} p = &slabclass[id];
assert(p->sl_curr == 0 || ((item *)p->slots)->slabs_clsid == 0); //如果p->sl_curr等于0,就说明该slabclass_t没有空闲的item了。
//此时需要调用do_slabs_newslab申请一个内存页
if (! (p->sl_curr != 0 || do_slabs_newslab(id) != 0)) {
//当p->sl_curr等于0并且do_slabs_newslab的返回值等于0时,进入这里
/* We don't have more memory available */
ret = NULL;
} else if (p->sl_curr != 0) {
//除非do_slabs_newslab调用失败,否则都会来到这里.无论一开始sl_curr是否为0。
//p->slots指向第一个空闲的item,此时要把第一个空闲的item分配出去 /* return off our freelist */
it = (item *)p->slots;
p->slots = it->next;//slots指向下一个空闲的item
if (it->next) it->next->prev = 0;
p->sl_curr--;//空闲数目减一
ret = (void *)it;
} if (ret) {
p->requested += size;//增加本slabclass分配出去的字节数
} return ret;
}

可以看到在do_slabs_alloc函数的内部也是通过调用do_slabs_newslab增加item的。

在本文前面的代码中,都没有看到锁的。作为memcached这个用锁大户,有点不正常。其实前面的代码中,有一些是要加锁才能访问的,比如do_slabs_alloc函数。之所以上面的代码中没有看到,是因为memcached使用了包裹函数(这个概念对应看过《UNIX网络编程》的读者来说很熟悉吧)。memcached在包裹函数中加锁后,才访问上面的那些函数的。下面就是两个包裹函数。

static pthread_mutex_t slabs_lock = PTHREAD_MUTEX_INITIALIZER;
void *slabs_alloc(size_t size, unsigned int id) {
void *ret; pthread_mutex_lock(&slabs_lock);
ret = do_slabs_alloc(size, id);
pthread_mutex_unlock(&slabs_lock);
return ret;
} void slabs_free(void *ptr, size_t size, unsigned int id) {
pthread_mutex_lock(&slabs_lock);
do_slabs_free(ptr, size, id);
pthread_mutex_unlock(&slabs_lock);
}
 

memcached源码分析-----slab内存分配器的更多相关文章

  1. Memcached源码分析之内存管理

    先再说明一下,我本次分析的memcached版本是1.4.20,有些旧的版本关于内存管理的机制和数据结构与1.4.20有一定的差异(本文中会提到). 一)模型分析在开始解剖memcached关于内存管 ...

  2. Memcached源码分析——slab的初始化

    以下内容仅为本人的笔记. /** * Determines the chunk sizes and initializes the slab class descriptors * according ...

  3. Memcached源码分析之请求处理(状态机)

    作者:Calix 一)上文 在上一篇线程模型的分析中,我们知道,worker线程和主线程都调用了同一个函数,conn_new进行事件监听,并返回conn结构体对象.最终有事件到达时,调用同一个函数ev ...

  4. Memcached源码分析之从SET命令开始说起

    作者:Calix 如果直接把memcached的源码从main函数开始说,恐怕会有点头大,所以这里以一句经典的“SET”命令简单地开个头,算是回忆一下memcached的作用,后面的结构篇中关于命令解 ...

  5. Memcached源码分析

    作者:Calix,转载请注明出处:http://calixwu.com 最近研究了一下memcached的源码,在这里系统总结了一下笔记和理解,写了几 篇源码分析和大家分享,整个系列分为“结构篇”和“ ...

  6. linux内存源码分析 - SLAB分配器概述【转】

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 之前说了管理区页框分配器,这里我们简称为页框分配器,在页框分配器中主要是管理物理内存,将物理内存的页框分配给申请 ...

  7. linux内存源码分析 - SLAB分配器概述

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 之前说了管理区页框分配器,这里我们简称为页框分配器,在页框分配器中主要是管理物理内存,将物理内存的页框分配给申请 ...

  8. memcached源码分析-----item过期失效处理以及LRU爬虫

    memcached源码分析-----item过期失效处理以及LRU爬虫,memcached-----item 转载请注明出处:http://blog.csdn.net/luotuo44/article ...

  9. Memcached源码分析之线程模型

    作者:Calix 一)模型分析 memcached到底是如何处理我们的网络连接的? memcached通过epoll(使用libevent,下面具体再讲)实现异步的服务器,但仍然使用多线程,主要有两种 ...

随机推荐

  1. H3C交换机引发的奇葩故障

    设备:H3C S5120-28P-SI 故障:某个交换机的接口速率只有100Mbps. 描述:这个故障还是很特别的,因为按普通的测试办法很难第一时间判断是交换机的固件问题,我也是做了几乎所有外围设备和 ...

  2. js中把ajax获取的数据转化成树状结构(并做成多级联动效果)

    1.首先通过ajax获取数据,此处省略,直接贴出获取到的数据格式 var arr = [{ id: 1, name: "一级标题", pid: 0 }, { id: 2, name ...

  3. angular4脚手架搭建

    Angular4.X安装,创建 1.安装最新的nodejs(node -v ,npm -v) 2.新建文件夹(右键git bash here)npm install -g @angular/cli 3 ...

  4. No input file specified. phpStudy nginx报错解决方案

    1.首先观察路径是否存在, 2.在vhsos.conf文件中 先科普下: 在Windows系统中,正斜杠 / 表示除法,用来进行整除运算:反斜杠 \ 用来表示目录. 在Unix系统中,/ 表示目录:\ ...

  5. linux----------VMware如何链接局域网其它电脑上的虚拟机,选择桥接模式即可。仔细阅读虚拟机的三个连接方式

    VMWare提供了三种工作模式,它们是bridged(桥接模式).NAT(网络地址转换模式)和host-only(主机模式).要想在网络管理和维护中合理应用它们,你就应该先了解一下这三种工作模式.   ...

  6. PAT (Basic Level) Practice (中文)1001 害死人不偿命的(3n+1)猜想

    1001 害死人不偿命的(3n+1)猜想 卡拉兹(Callatz)猜想: 对任何一个正整数 n,如果它是偶数,那么把它砍掉一半:如果它是奇数,那么把 (3n+1) 砍掉一半.这样一直反复砍下去,最后一 ...

  7. 贝叶斯公式由浅入深大讲解—AI基础算法入门

    1 贝叶斯方法 长久以来,人们对一件事情发生或不发生的概率,只有固定的0和1,即要么发生,要么不发生,从来不会去考虑某件事情发生的概率有多大,不发生的概率又是多大.而且概率虽然未知,但最起码是一个确定 ...

  8. php 图片添加文字,水印

    因为工作需求,用到这个,网上找了很多,也没有找到好的方式,最后找到这种感觉比较简单的方式,记录下来,以备后用.   $im = imagecreatefrompng("img/yyk_bg. ...

  9. Task3

    姓名:蔡典 学号:1425052044 班级:信管142 兴趣爱好:电影,美剧,游戏 个人编程能力:较弱,没自己写过代码 码云账号:18809188@qq.com 直接搜索码云然后进入官网输入基本信息 ...

  10. Linq基础+Lambda表达式对数据库的增删改及简单查询

    一.Linq to sql 类 高集成化的数据库访问技术 使用Linq可以代替之前的Ado.Net,省去了自己敲代码的实体类和数据访问类的大量工作 实体类: 添加一个Linq to sql 类 --- ...