程序员人生 网站导航

memcached源码分析-----slab内存分配器

栏目:php教程时间:2015-03-01 00:51:48
  温馨提示:本文用到了1些可以在启动memcached设置的全局变量。关于这些全局变量的含义可以参考《memcached启动参数详解》。对这些全局变量,处理方式就像《如何浏览memcached源代码》所说的那样直接取其默许值。

slab内存池分配器:

 

slab简介:

 

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

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

        

 

 

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

        

 

 

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

        

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

 

 

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

 

        看完了图,现在来看1下memcached是怎样肯定slab分配器的分配规格的。由于memcached使用了全局变量,先来看1下全局变量。

//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数组,数组的每个元素就是1个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数组中,已使用了的元素个数.

        可以看到,上面的代码定义了1个全局slabclass数组。这个数组就是前面那些图的slabclass_t数组。虽然slabclass数组有201个元素,但可能其实不会所有元素都使用的。由全局变量power_largest指明使用了多少个元素.下面看1下slabs_init函数,该函数对这个数组进行1些初始化操作。该函数会在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;//用户设置或默许的内存最大限制 //用户要求预分配1大块的内存,以后需要内存,就向这块内存申请。 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. Will allocate in smaller chunks "); } } //初始化数组,这个操作很重要,数组中所有元素的成员变量值都为0了 memset(slabclass, 0, sizeof(slabclass)); //slabclass数组中的第1个元素其实不使用 //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可以存储更大的1条数据信息。固然是有限制的,最大也只能为128MB。巧的是,slab分配器能分配的最大内存也是受这个settings.item_size_max所限制。由于每个slab分配器能分配的最大内存有上限,所以slabclass数组中的每个slabclass_t都有多个slab分配器,其用1个数组管理这些slab分配器。而这个数组大小是不受限制的,所以对某个特定的尺寸的item是可以有很多很多的。固然全部memcached能分配的总内存大小也是有限制的,可以在启动memcached的时候通过-m选项设置,默许值为64MB。slabs_init函数中的limit参数就是memcached能分配的总内存。


预分配内存:

 

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

 

        现在就假定用户需要预先分配内存,看1下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分配1个内存页 //如果分配失败,将退出程序.由于这个预分配的内存是后面程序运行的基础 //如果这里分配失败了,后面的代码无从履行。所以就直接退出程序。 exit(1); } } } //slabclass_t中slab的数目是渐渐增多的。该函数的作用就是为slabclass_t申请多1个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)。1般都会成功,除非没法分配内存 ((ptr = memory_allocate((size_t)len)) == 0)) {//分配len字节内存(也就是1个页) return 0; } memset(ptr, 0, (size_t)len);//清零内存块是必须的 //将这块内存切成1个个的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函数内部调用了3个函数。函数grow_slab_list的作用是增大slab数组的大小(以下图所示的slab数组)。memory_allocate函数则是负责申请大小为len字节的内存。而函数split_slab_page_into_freelist则负责把申请到的内存切分成多个item,并且把这些item用指向连起来,构成双向链表。以下图所示:前面已见过这图了,看完代码再来看1下吧。

        

 

 

 

        下面看1下那3个函数的具体实现。

//增加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指向的内存页划分成1个个的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指向的内存划分成1个个的item.1共划成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变量指向第1个空闲可使用的item p->sl_curr++;//空闲可使用的item数量 p->requested -= size;//减少这个slabclass_t分配出去的字节数 return; }


 

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

        

        虽然混乱,但肯定还是会有前面那张逻辑图那样的清晰链表图,其中slots变量指向第1个空闲的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数组中,第1个元素是没有使用的 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得到1些空闲的item然后分配出去。代码以下面所示:

//向slabclass申请1个item。在调用该函数之前,已调用slabs_clsid函数肯定 //本次申请是向哪一个slabclass_t申请item了,参数id就是指明是向哪一个slabclass_t //申请item。如果该slabclass_t是有空闲item,那末就从空闲的item队列中分配1个 //如果没有空闲item,那末就申请1个内存页。再重新申请的页中分配1个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申请1个内存页 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调用失败,否则都会来到这里.不管1开始sl_curr是不是为0。 //p->slots指向第1个空闲的item,此时要把第1个空闲的item分配出去 /* return off our freelist */ it = (item *)p->slots; p->slots = it->next;//slots指向下1个空闲的item if (it->next) it->next->prev = 0; p->sl_curr--;//空闲数目减1 ret = (void *)it; } if (ret) { p->requested += size;//增加本slabclass分配出去的字节数 } return ret; }

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

 

        在本文前面的代码中,都没有看到锁的。作为memcached这个用锁大户,有点不正常。其实前面的代码中,有1些是要加锁才能访问的,比如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); }

 

 

 

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐