memcached源码分析-----LRU队列与item结构体

转载请注明出处:http://blog.csdn.net/luotuo44/article/details/42869325

LRU队列:

之前的《slab内存分配》博文已经说到一个slab class里面的所有slab分配器都只分配相同大小的item,不同的slab class分配不同大小的item。item结构体里面有一个slabs_clsid成员,用来指明自己是属于哪个slab class的。这里把slabs_clsid值相同的item称为是同一类item。

slab分配器负责分配一个item,但这个item并非直接给哈希表进行管理。从《哈希表的删除操作》可以看到,对哈希表中的某一个item进行删除只是简单地将这个item从哈希表的冲突链中删除掉,并没有把item内存归还给slab。实际上,slab分配器分配出去的item是由一个LRU队列进行管理的。当一个item从LRU队列中删除就会归还给slab分配器

LRU队列其实是一个双向链表。memcached里面有多条LRU队列,条数等于item的种类数。所以呢,同一类item(slabs_clsid成员变量值相等)将放到同一条LRU队列中。之所以是双向链表,是因为要方便从前后两个方向插入和遍历链表。下面看一下item结构体的部分成员。

typedef struct _stritem {
    struct _stritem *next; //next指针,用于LRU链表
    struct _stritem *prev; //prev指针,用于LRU链表
    struct _stritem *h_next;//h_next指针,用于哈希表的冲突链
    rel_time_t      time;   //最后一次访问时间。绝对时间
	  ...
    uint8_t         slabs_clsid;/* which slab class we're in */
} item;

接下来看一下管理LRU队列的数据结构。其实是超级简单的,就三个全局数组而已,而且是static数组(开心吧)。

//memcached.h文件
#define POWER_LARGEST  200

//items.c文件
#define LARGEST_ID POWER_LARGEST
static item *heads[LARGEST_ID];//指向每一个LRU队列头
static item *tails[LARGEST_ID];//指向每一个LRU队列尾
static unsigned int sizes[LARGEST_ID];//每一个LRU队列有多少个item

可以看到这三个数组的大小是和slabclass提供的最大slab class个数是一样的。这样也印证了一个LRU队列就对应一类item。heads[i]指向第i类LRU链表的第一个item,tails[i]则指向了第i类LRU链表的最后一个item,sizes[i]则指明第i类LRU链表有多少个item。由LRU队列和heads和tails构成的结构如下图所示:

LRU队列的基本操作:

插入操作:

假设我们有一个item要插入到LRU链表中,那么可以通过调用item_link_q函数把item插入到LRU队列中。下面是具体的实现代码。

//将item插入到LRU队列的头部
static void item_link_q(item *it) { /* item is the new head */
    item **head, **tail;
    assert(it->slabs_clsid < LARGEST_ID);
    assert((it->it_flags & ITEM_SLABBED) == 0);

    head = &heads[it->slabs_clsid];
    tail = &tails[it->slabs_clsid];
    assert(it != *head);
    assert((*head && *tail) || (*head == 0 && *tail == 0));

	//头插法插入该item
    it->prev = 0;
    it->next = *head;
    if (it->next) it->next->prev = it;
    *head = it;//该item作为对应链表的第一个节点

	//如果该item是对应id上的第一个item,那么还会被认为是该id链上的最后一个item
	//因为在head那里使用头插法,所以第一个插入的item,到了后面确实成了最后一个item
    if (*tail == 0) *tail = it;
    sizes[it->slabs_clsid]++;//个数加一
    return;
}

删除操作:

有了插入函数,肯定有对应的删除函数。删除函数是蛮简单,主要是处理删除这个节点后,该节点的前后驱节点怎么拼接在一起。

//将it从对应的LRU队列中删除
static void item_unlink_q(item *it) {
    item **head, **tail;
    assert(it->slabs_clsid < LARGEST_ID);
    head = &heads[it->slabs_clsid];
    tail = &tails[it->slabs_clsid];

    if (*head == it) {//链表上的第一个节点
        assert(it->prev == 0);
        *head = it->next;
    }
    if (*tail == it) {//链表上的最后一个节点
        assert(it->next == 0);
        *tail = it->prev;
    }
    assert(it->next != it);
    assert(it->prev != it);

	//把item的前驱节点和后驱节点连接起来
    if (it->next) it->next->prev = it->prev;
    if (it->prev) it->prev->next = it->next;
    sizes[it->slabs_clsid]--;//个数减一
    return;
}

可以看到无论是插入还是删除一个item,其耗时都是常数。其实对于memcached来说几乎所有的操作时间复杂度都是常数级的。

更新操作:

为什么要把item插入到LRU队列头部呢?当然实现简单是其中一个原因。但更重要的是这是一个LRU队列!!还记得操作系统里面的LRU吧。这是一种淘汰机制。在LRU队列中,排得越靠后就认为是越少使用的item,此时被淘汰的几率就越大。所以新鲜item(访问时间新),要排在不那么新鲜item的前面,所以插入LRU队列的头部是不二选择。下面的do_item_update函数佐证了这一点。do_item_update函数是先把旧的item从LRU队列中删除,然后再插入到LRU队列中(此时它在LRU队列中排得最前)。除了更新item在队列中的位置外,还会更新item的time成员,该成员指明上一次访问的时间(绝对时间)。如果不是为了LRU,那么do_item_update函数最简单的实现就是直接更新time成员即可。

#define ITEM_UPDATE_INTERVAL 60 //更新频率为60秒

void do_item_update(item *it) {
	//下面的代码可以看到update操作是耗时的。如果这个item频繁被访问,
	//那么会导致过多的update,过多的一系列费时操作。此时更新间隔就应运而生
	//了。如果上一次的访问时间(也可以说是update时间)距离现在(current_time)
	//还在更新间隔内的,就不更新。超出了才更新。
    if (it->time < current_time - ITEM_UPDATE_INTERVAL) {

        mutex_lock(&cache_lock);
        if ((it->it_flags & ITEM_LINKED) != 0) {
            item_unlink_q(it);//从LRU队列中删除
            it->time = current_time;//更新访问时间
            item_link_q(it);//插入到LRU队列的头部
        }
        mutex_unlock(&cache_lock);
    }
}

memcached处理get命令时会调用do_item_update函数更新item的访问时间,更新其在LRU队列的位置。在memcached中get命令是很频繁的命令,排在LRU队列第一或者前几的item更是频繁被get。对于排在前几名的item来说,调用do_item_update是意义不大的,因为调用do_item_update后其位置还是前几名,并且LRU淘汰再多item也难于淘汰不到它们(一个LRU队列的item数量是很多的)。另一方面,do_item_update函数耗时还是会有一定的耗时,因为要抢占cache_lock锁。如果频繁调用do_item_update函数性能将下降很多。于是memcached就是使用了更新间隔。

前面讲了怎么在LRU队列中插入和删除一个item,现在讲一下怎么从slab分配器中申请一个item和怎么归还item给slab分配器。

item结构体:

虽然前面多次提到了item,但item长成什么样子估计读者还是迷迷糊糊的。下面看一下item结构体的完整定义吧,留意英文注释。

#define ITEM_LINKED 1 //该item插入到LRU队列了
#define ITEM_CAS 2 //该item使用CAS
#define ITEM_SLABBED 4 //该item还在slab的空闲队列里面,没有分配出去
#define ITEM_FETCHED 8 //该item插入到LRU队列后,被worker线程访问过

typedef struct _stritem {
    struct _stritem *next;//next指针,用于LRU链表
    struct _stritem *prev;//prev指针,用于LRU链表
    struct _stritem *h_next;//h_next指针,用于哈希表的冲突链
    rel_time_t      time;//最后一次访问时间。绝对时间
    rel_time_t      exptime;//过期失效时间,绝对时间
    int             nbytes;//本item存放的数据的长度
    unsigned short  refcount;//本item的引用数
    uint8_t         nsuffix;//后缀长度    /* length of flags-and-length string */
    uint8_t         it_flags;//item的属性   /* ITEM_* above */
    uint8_t         slabs_clsid;/* which slab class we're in */
    uint8_t         nkey;//键值的长度       /* key length, w/terminating null and padding */
    /* this odd type prevents type-punning issues when we do
     * the little shuffle to save space when not using CAS. */
    union {
        uint64_t cas;
        char end;
    } data[];
    /* if it_flags & ITEM_CAS we have 8 bytes CAS */
    /* then null-terminated key */
    /* then " flags length\r\n" (no terminating null) */
    /* then data with terminating \r\n (no terminating null; it's binary!) */
} item;

上面代码里面的英文注释说明了item布局。item结构体的最后一个成员是data[],这样的定义称为柔性数组。柔性数组的一个使用特点是,数据域就存放在数组的后面。memcached的item也是这样使用柔性数组的。上面的item只是定义了item结构体本身的成员,但之前的博文一直用item表示item存储的数据。这样写是合理的。因为item结构体本身和item对应的数据都是存放slab分配器分配的同一块内存里面。在《slab内存分配》初始化slabclass数组的时候,其分配的内存块大小是sizeof(item)
+settings.chunk_size。

item结构体后面紧接着的是一堆数据,并非仅仅是用户要存储的data,具体如下图所示:

看上面的图可能还不完全清楚明了item以及对应数据的存储形式。我懂的,码农是需要代码才能完全清楚明了的。

#define ITEM_key(item) (((char*)&((item)->data))          + (((item)->it_flags & ITEM_CAS) ? sizeof(uint64_t) : 0))

#define ITEM_suffix(item) ((char*) &((item)->data) + (item)->nkey + 1          + (((item)->it_flags & ITEM_CAS) ? sizeof(uint64_t) : 0))

#define ITEM_data(item) ((char*) &((item)->data) + (item)->nkey + 1          + (item)->nsuffix          + (((item)->it_flags & ITEM_CAS) ? sizeof(uint64_t) : 0))

#define ITEM_ntotal(item) (sizeof(struct _stritem) + (item)->nkey + 1          + (item)->nsuffix + (item)->nbytes          + (((item)->it_flags & ITEM_CAS) ? sizeof(uint64_t) : 0))

static size_t item_make_header(const uint8_t nkey, const int flags, const int nbytes,
                     char *suffix, uint8_t *nsuffix) {
    /* suffix is defined at 40 chars elsewhere.. */
    *nsuffix = (uint8_t) snprintf(suffix, 40, " %d %d\r\n", flags, nbytes - 2);
    return sizeof(item) + nkey + *nsuffix + nbytes;//计算总大小
}

//key、flags、exptime三个参数是用户在使用set、add命令存储一条数据时输入的参数。
//nkey是key字符串的长度。nbytes则是用户要存储的data长度+2,因为在data的结尾处还要加上"\r\n"
//cur_hv则是根据键值key计算得到的哈希值。
item *do_item_alloc(char *key, const size_t nkey, const int flags,
                    const rel_time_t exptime, const int nbytes,
                    const uint32_t cur_hv) {
    uint8_t nsuffix;
    item *it = NULL;
    char suffix[40];
	//要存储这个item需要的总空间。要注意第一个参数是nkey+1,所以上面的那些宏计算时
	//使用了(item)->nkey + 1
    size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);
    if (settings.use_cas) {//开启了CAS功能
        ntotal += sizeof(uint64_t);
    }

	//根据大小判断从属于哪个slab
    unsigned int id = slabs_clsid(ntotal);
    if (id == 0)//0表示不属于任何一个slab
        return 0;

	...

	it = slabs_alloc(ntotal, id);//从slab分配器中申请内存
	it->refcount = 1;
	it->it_flags = settings.use_cas ? ITEM_CAS : 0;
    it->nkey = nkey;
    it->nbytes = nbytes;
    memcpy(ITEM_key(it), key, nkey);//这里只拷贝nkey个字节,最后一个字节空着
    it->exptime = exptime;
    memcpy(ITEM_suffix(it), suffix, (size_t)nsuffix);
    it->nsuffix = nsuffix;

	return it;
}

码农们好好体会上面代码中的那个几个宏定义以及item_make_header函数。上面的代码还是超级简单地介绍了do_item_alloc函数,之所以说超级简单是因为这个函数实际上是相当复杂的。

上面简单给出了向slab申请一个item,现在贴出归还item的代码。

//items.c文件
void item_free(item *it) {
    size_t ntotal = ITEM_ntotal(it);
    unsigned int clsid;

    clsid = it->slabs_clsid;
    it->slabs_clsid = 0;
    slabs_free(it, ntotal, clsid);
}

//slabs.c文件
void slabs_free(void *ptr, size_t size, unsigned int id) {
    pthread_mutex_lock(&slabs_lock);
    do_slabs_free(ptr, size, id);//归还给slab分配器
    pthread_mutex_unlock(&slabs_lock);
}

item和哈希表的联系:

前面的do_item_alloc函数是根据所需的大小申请一个item。从do_item_alloc实现代码来看,它没有把这个item插入到哈希表和LRU队列中。实际上这个任务是由另外的函数实现的。

接下来看一下,怎么把item传给哈希表、LRU队列以及怎么从哈希表、LRU队列收回item(有时还需要归还给slab的)。

//将item插入到哈希表和LRU队列中,插入到哈希表需要哈希值hv
int do_item_link(item *it, const uint32_t hv) {

	//确保这个item已经从slab分配出去并且还没插入到LRU队列中
    assert((it->it_flags & (ITEM_LINKED|ITEM_SLABBED)) == 0);

	//当哈希表不在为扩展而迁移数据时,就往哈希表插入item
	//当哈希表在迁移数据时,会占有这个锁。
    mutex_lock(&cache_lock);
    it->it_flags |= ITEM_LINKED;//加入 已link标志
    it->time = current_time;

    /* Allocate a new CAS ID on link. */
    ITEM_set_cas(it, (settings.use_cas) ? get_cas_id() : 0);
    assoc_insert(it, hv);//将这个item插入到哈希表中
    item_link_q(it);//将这个item插入到链表中
    refcount_incr(&it->refcount);//引用计数加一
    mutex_unlock(&cache_lock);

    return 1;
}

//从哈希表删除,所以需要哈希值hv
void do_item_unlink(item *it, const uint32_t hv) {

    mutex_lock(&cache_lock);
    if ((it->it_flags & ITEM_LINKED) != 0) {
        it->it_flags &= ~ITEM_LINKED;//减去已link标志

        assoc_delete(ITEM_key(it), it->nkey, hv);//将这个item从哈希表中删除
        item_unlink_q(it);//从链表中删除该item
        do_item_remove(it);//向slab归还这个item
    }
    mutex_unlock(&cache_lock);
}

void do_item_remove(item *it) {
    assert((it->it_flags & ITEM_SLABBED) == 0);
    assert(it->refcount > 0);

    if (refcount_decr(&it->refcount) == 0) {//引用计数等于0的时候归还
        item_free(it);//归还该item给slab
    }
}

在使用do_item_remove函数向slab归还item时,会先测试这个item的引用数是否等于0。引用数可以简单理解为有没有worker线程在使用这个item。引用数的将会在后面的博文中详细讲解。

前面的代码中很少看到锁。但实际上前面的item操作都是需要加锁的,因为可能多个worker线程同时操作哈希表和LRU队列。之所以很少看到锁是因为他们都使用了包裹函数(如果看过《UNIX网络编程》,这个概念应该不陌生)。在包裹函数中加锁和解锁。前面的函数中,函数名一般都是以do_作为前缀。其对应的包裹函数名就是去掉前缀do_。锁的介绍不是本博文的任务,后面会有专门的博文介绍锁的使用。

时间: 2024-10-07 22:58:39

memcached源码分析-----LRU队列与item结构体的相关文章

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

memcached源码分析-----item过期失效处理以及LRU爬虫,memcached-----item 转载请注明出处:http://blog.csdn.net/luotuo44/article/details/42963793 温馨提示:本文用到了一些可以在启动memcached设置的全局变量.关于这些全局变量的含义可以参考<memcached启动参数详解>.对于这些全局变量,处理方式就像<如何阅读memcached源代码>所说的那样直接取其默认值. 过期失效处理: 一个i

Memcached源码分析之内存管理

先再说明一下,我本次分析的memcached版本是1.4.20,有些旧的版本关于内存管理的机制和数据结构与1.4.20有一定的差异(本文中会提到). 一)模型分析在开始解剖memcached关于内存管理的源代码之前,先宏观上分析一下memcached内存管理的模型是怎样子的: 提个建议,我觉得memcached内存管理的模型与我们平时做作业的作业本“画格子给我们往格子里面写字”的逻辑很像,一本本作业本就是我们的内存空间,而我们往里写的字就是我们要存下来的数据,所以分析的时候可以想像一下用方格作业

Memcached源码分析之线程模型

作者:Calix 一)模型分析 memcached到底是如何处理我们的网络连接的? memcached通过epoll(使用libevent,下面具体再讲)实现异步的服务器,但仍然使用多线程,主要有两种线程,分别是“主线程”和“worker线程”,一个主线程,多个worker线程. 主线程负责监听网络连接,并且accept连接.当监听到连接时,accept后,连接成功,把相应的client fd丢给其中一个worker线程.worker线程接收主线程丢过来的client fd,加入到自己的epol

Linux c 开发 - Memcached源码分析之命令解析(2)

前言 从我们上一章<Linux c 开发 - Memcached源码分析之基于Libevent的网络模型>我们基本了解了Memcached的网络模型.这一章节,我们需要详细解读Memcached的命令解析. 我们回顾上一章发现Memcached会分成主线程和N个工作线程.主线程主要用于监听accpet客户端的Socket连接,而工作线程主要用于接管具体的客户端连接. 主线程和工作线程之间主要通过基于Libevent的pipe的读写事件来监听,当有连接练上来的时候,主线程会将连接交个某一个工作线

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

作者:Calix 如果直接把memcached的源码从main函数开始说,恐怕会有点头大,所以这里以一句经典的“SET”命令简单地开个头,算是回忆一下memcached的作用,后面的结构篇中关于命令解析部分主要也是围绕着SET命令展开分析,相信把一句SET命令背后做的事情都搞清楚,那么memcached大部分源码都了解得七七八八了. 那么,回忆一下,set命令做了个什么事情? 无非就是把一个value set到某个key上面,保存在内存当中. 再细化一下: 1)memcached是一个缓存服务器

Memcached源码分析

作者:Calix,转载请注明出处:http://calixwu.com 最近研究了一下memcached的源码,在这里系统总结了一下笔记和理解,写了几 篇源码分析和大家分享,整个系列分为“结构篇”和“源码篇”,建议先从结构篇开始看起,要特别说明的是我本次分析的是memcached1.4.20的版 本,不同版本会有所差异,另外,文章均为本人的个人理解,如果解析得不好或者错误的地方敬请指正. 好了,不啰嗦了,下面是导航: [结构篇] Memcached源码分析之从SET命令开始说起 Memcache

memcached源码分析item过期失效处理

原文地址:http://www.faceye.net/search/142629.html 过期失效处理: 一个item在两种情况下会过期失效:1.item的exptime时间戳到了.2.用户使用flush_all命令将全部item变成过期失效的.读者可能会说touch命令也可以使得一个item过期失效,其实这也属于前面说的第一种情况. 超时失效: 对于第一种过期失效,memcached的使用懒惰处理:不主动检测一个item是否过期失效.当worker线程访问这个item时,才检测这个item的

memcached源码分析-----item锁级别与item引用计数

        转载请注明出处:http://blog.csdn.net/luotuo44/article/details/42913549 锁级别: 从前面的<扩展哈希表>知道:哈希表进行扩展时,有一个专门的线程负责将item数据从旧哈希表迁移到新哈希表(由此,也称这个线程为迁移线程).此外,还有一些worker线程会时不时访问item(包括插入.删除和获取).这些线程的对item所做的操作基本上都是互斥的,必须加锁控制. 如果只使用一个锁,抢到该锁才能使用哈希表,没有抢到则不能使用.这样的

memcached源码分析-----memcached启动参数详解以及关键配置的默认值

转载请注明出处: http://blog.csdn.net/luotuo44/article/details/42672913 本文开启本系列博文的代码分析.本系列博文研究是memcached版本是1.4.21. 本文将给出memcached启动时各个参数的详细解释以及一些关键配置的默认值.以便在分析memcached源码的时候好随时查看.当然也方便使用memcached时可以随时查看各个参数的含义.<如何阅读memcached源码>说到memcached有很多全局变量(也就是关键配置),这些