现在的位置: 首页 > 综合 > 正文

分析memcached源代码

2014年01月25日 ⁄ 综合 ⁄ 共 16912字 ⁄ 字号 评论关闭

本文针对memcached 1.21 for windows(主要原因是Linux实在缺乏一种简单易用的C++ IDE,使用gdb调试我会发疯的,code::block挺不错的,但是感觉还是不如VC,我非常喜欢VC/VS的调试功能,其中几个特性包括随便拖变量到watch窗口,在watch窗口可以编辑被观察的变量,鼠标悬停时显示变量的值。这几个特性是我希望一个调试器所应该必备的),其源代码可在http://jehiah.cz/projects/memcached-win32/memcached下载到,记得同时下载”libevent
1.1a for Win32 “,然后将这个libevent.lib这个静态库放到Win32-Prj目录下,将libevent.h头文件放在项目目录下,接下来在“Win32- Code/win32.h”文件中加入这一句:#include “../event.h”,这样就可以正常编译这个工程了 ^_^

memcached代码是用C编写的,呵呵,好久没有看过这么大的C程序了,时常要犯点小错误,其中最常见的是在代码中随便声明一个变量,然后使用之。在 C++中使用惯了,提示编译错误时都有点难于置信,C语言只允许在一个block的开始处声明变量。另外一个常犯的错误是”struct A {….};”,接着便使用”A a”来声明结构体变量,C++允许这样做,但是C却不允许,必须使用在前面加上”struct”关键字。 memcached代码结构是非常清晰的,不似C++程序那般绕来绕去,并且我发现调试运行C程序有一个特别方便之处,大量的全局变量,可以一直观察它的变动。这里先提示一个小技巧,settings这个结构体保存着程序运行时需要的参数,有些在程序启动时指定好了就不会改变,有些会不断地被改变(应该只有oldest_live会被改变)。memcached.c文件中这句代码”struct
settings settings;”很让人郁闷,结构体类型和结构体变量同名了,这样就无法在调试器的watch窗口看到settings的值了,所以建议将结构体类型修改为”_settings”,这样就可以看到settings的值了。

可以通过telnet轻松连接到memcached服务器(当然也可以自己写一个简单的客户端来使用服务器,用C/C++写都不会太复杂,因为 telnet不适合产生大量的数据,所以当需要使用大量数据来测试memcached,一个可编程的客户端是少不了的),然后使用相关的命令来操作数据项,memcached主要有”set/get/replace/delete/incr/decr/”这几个数据操作命令。在分析这些命令之前补充点背景知识吧。memcached使用libevent(这个库我也不是很了解,不过没关系,只要知道它的一些基本使用方法就不会影响到对memcached
源代码的理解)来支持多客户端连接,有一个主套接字监听客户端的请求,然后针对每个新的连接生成一个新的conn结构体变量,这个结构体的sfd属性则保存了针对该客户端的连接套接字,再将这个连接套接字加入到libevent体系中(通过event_set来完成),这样套接字有改变时,将触发 event_handler函数被执行。针对每个客户端都有一个唯一的conn变量为之服务,只有当客户端断开连接时该conn结构体才会消亡。 memcached使用了一个有趣的状态机,程序初始化完成之后,drive_machine就成了整个程序的核心了,所有的操作和跳转都是在这个函数中完成的。另外一点需要注意的是,Windows
telnet和Linux telnet程序的不同,Windows telnet使用的是单字符输入模式,而Linux telnet程序则使用行输入模式,Windows的telnet客户端每敲入一个字符都会被送到服务器断,而Linux telnet则在敲入回车之后才会将一个整行送给服务器,这一点在调试时可以清晰地看到,不知道可不可以切换这两种模式。

memcached经常有一些位操作,标志位的置位和清除的方法分别如:
it->it_flags |= ITEM_DELETED
it->it_flags &= ~ITEM_DELETED

ok,有了上面的基础就开始分析下set命令吧。
set foo 0 0 3
bar
STORED
set ecy 0 0 3
wxy
STORED
get foo ecy
VALUE foo 0 3
bar
VALUE ecy 0 3
wxy
END
上面这段代码显示了set命令的使用方法。

从登陆到输入set命令,状态机的状态转换如下:
conn_listening->conn_read->conn_nread->conn_write->conn_mwrite->conn_read

注意服务器只有第一个conn对象才可以处于conn_listening状态,它一直负责监听,当监听到客户端有新的连接时,它会new出一个新的conn对象来为之服务,所有后面的状态是客户端对应的conn对象的状态,不要弄混淆了。针对每个客户端的conn对象在处理完一条命令之后就会处于 conn_read状态,等待用户继续输入命令。drive_machine的接受的形参可不是同一个conn对象,每个套接字触发的事件都会携带有参数,这个参数arg即是和这个套接字关联的conn对象,所以强制转换一下类型就好了。
void event_handler(int fd, short which, void *arg) {
conn *c;
c = (conn *)arg;
c->which = which;
可以在每种状态的case 下加上一条打印语句,显示当前的状态,这样就可以方便地看到状态的转变。

conn_read有几个非常重要的函数,其中try_read_network函数负责从网络上读取字节,try_read_command则判断接到到的数据中是否含有一个换行,客户端回车输入”set foo 0 0 3″这一行后便回车,此时服务器收到的字节是:”set foo 0 0 3\r\n”,所以如果有换行的话就意味着一条命令已经输入完全了,否则循环调用try_read_network等待用户继续输入。如果一条命令已经输入完全,那么就调用process_command处理之。对于set命令,完整的输入形如”set
foo 0 0 3″,服务器收到这行命令之后,就会调用process_update_command处理它。在process_update_command函数中会调用item_alloc分配并初始化一个item。因为是一个set命令,输入”foo 0 0 3″之后,后面肯定接着要输入数据项的值,所以process_update_command函数的最后有这样一句:conn_set_state(c, conn_nread);,状态机就转换为”conn_nread”状态。

在”conn_nread”状态下,服务器通过下面这句来读取网络上的数据,一直要读到什么时候结束呢?”set foo x y z”,第一个x指定了item的标志位(注意这个标志位同item的it_flags属性一点关系都没有的),第二个y指定了item的过期时间,第三个 z则指定了item的value的长度。用户必须严格输入z长度的字符,否则就出错。
/* now try reading from the socket */
res = read(c->sfd, c->ritem, c->rlbytes); //可以跟踪下c->ritem的变化,它使得value放在key的后面
if (res > 0) {
stats.bytes_read += res;
c->ritem += res;
c->rlbytes -= res; //res为本次读取的字符数,rlbytes指明还有多少个字符等待输入
//rlbytes的初值就是上面说的z了,这里从网络读到多少数据就减掉相应的长度
break;
}

下面这句判断rlbytes是否为零,如果为0就需要结束读取了。
/* we are reading rlbytes into ritem; */
if (c->rlbytes == 0) {
complete_nread(c);
break;
}

在执行complete_nread函数之前,item的key和value都已经保存到正确的位置上了,它们在内存中保存的形式如下:
0×01030020 00 00 00 00 00 00 00 00 00 00 00 00 5a 00 00 00 00 00 …………Z…..
0×01030032 00 00 05 00 00 00 00 00 06 01 01 03 00 00 66 6f 6f
00 …………..foo.
0×01030044 20 30 20 33 0d 0a 62 61 72 0d 0a 00 00 00 00 00 00 00 0 3..bar………
(数字的ASCII码从30开始,所以0就是30了;20是空格的ASCII码;0D 0A就是回车换行了;这里可以看到key和suffix中间有一个’00′,这是因为#define ITEM_suffix(item) ((char*) &((item)->end[0]) + (item)->nkey + 1)后面加了一个1的缘故,而当初初始化slab时,对所有申请的内存空间都用memset清零了,所以这里有一个零)

接下来分析下一个完整的item是如何被保存起来的。
/*
* Generates the variable-sized part of the header for an object.
*
* key – The key
* nkey – The length of the key
* flags – key flags
* nbytes – Number of bytes to hold value and addition CRLF terminator
* suffix – Buffer for the “VALUE” line suffix (flags, size).
* nsuffix – The length of the suffix is stored here.
*
* Returns the total size of the header.
*/
int item_make_header(char *key, uint8_t nkey, int flags, int nbytes,
char *suffix, int *nsuffix) {
*nsuffix = sprintf(suffix, ” %u %u\r\n”, flags, nbytes – 2);
return sizeof(item) + nkey + *nsuffix + nbytes;
}

这里的nbytes将value的长度保存起来了。因为item的key和value都不是定长的,所有必须保存它们的长度,key的长度直接保存在 item的nkey中,value的长度则没有专门的变量来记录,这个suffix便是用来保存这些数据的,一是item的flag,另一个是value 的长度。

item_alloc函数在初始化item的后面有一句代码:memcpy(ITEM_suffix(it), suffix, nsuffix);

这样便将suffix复制到了#define ITEM_suffix(item) ((char*) &((item)->end[0]) + (item)->nkey + 1)处,这样就可以看出一个item是怎样保存的,首先放的是item,item中最后存放的是key,然后是suffix,接着才是value。这样就可以理解宏ITEM_data的定义了。
#define ITEM_data(item) ((char*) &((item)->end[0]) + (item)->nkey + 1 + (item)->nsuffix)

complete_nread主要是要生成应答客户端的信息,并且调用item_link函数将item加入hash表中,然后调用 item_link_q将本item放到LRU链表的第一个位置。
分析到这里就不得不讲讲memcached怎么管理这些item了。memcached启动时默认申请64MB的内存,memcached使用slab来管理内存。
static slabclass_t slabclass[POWER_LARGEST+1];

最多可以有两百个slab,不知道为什么不使用第零个。
unsigned int size = sizeof(item) + settings.chunk_size;

默认情况下第一个slab管理的内存项大小为80字节,然后一次倍乘factor(默认值为1.25)。既然内存按固定大小分配,那么碎片也就再所难免了。每个slab首先分配1MB大小的内存,将1MB除于本slab管理固定内存块大小便是内存块的总数目了。第一个slab管理的内存块大小为:1024 * 1024 / 80 = 13107。默认情况下的启动信息如下:
slab class 1: chunk size 80 perslab 13107
slab class 2: chunk size 100 perslab 10485
slab class 3: chunk size 128 perslab 8192
slab class 4: chunk size 160 perslab 6553
slab class 5: chunk size 200 perslab 5242
slab class 6: chunk size 252 perslab 4161
slab class 7: chunk size 316 perslab 3318
slab class 8: chunk size 396 perslab 2647
slab class 9: chunk size 496 perslab 2114
slab class 10: chunk size 620 perslab 1691
slab class 11: chunk size 776 perslab 1351
slab class 12: chunk size 972 perslab 1078
slab class 13: chunk size 1216 perslab 862
slab class 14: chunk size 1520 perslab 689
slab class 15: chunk size 1900 perslab 551
slab class 16: chunk size 2376 perslab 441
slab class 17: chunk size 2972 perslab 352
slab class 18: chunk size 3716 perslab 282
slab class 19: chunk size 4648 perslab 225
slab class 20: chunk size 5812 perslab 180
slab class 21: chunk size 7268 perslab 144
slab class 22: chunk size 9088 perslab 115
slab class 23: chunk size 11360 perslab 92
slab class 24: chunk size 14200 perslab 73
slab class 25: chunk size 17752 perslab 59
slab class 26: chunk size 22192 perslab 47
slab class 27: chunk size 27740 perslab 37
slab class 28: chunk size 34676 perslab 30
slab class 29: chunk size 43348 perslab 24
slab class 30: chunk size 54188 perslab 19
slab class 31: chunk size 67736 perslab 15
slab class 32: chunk size 84672 perslab 12
slab class 33: chunk size 105840 perslab 9
slab class 34: chunk size 132300 perslab 7
slab class 35: chunk size 165376 perslab 6
slab class 36: chunk size 206720 perslab 5
slab class 37: chunk size 258400 perslab 4
slab class 38: chunk size 323000 perslab 3
slab class 39: chunk size 403752 perslab 2
slab class 40: chunk size 504692 perslab 2
<68 server listening

item都存放在其相应的slab内,然后会有两个重要的链表将它们链接起来。其中一个是单向链表,一个是双向链表。
typedef struct _stritem {
struct _stritem *next;
struct _stritem *prev;
struct _stritem *h_next; /* hash chain next */
………………………..}

item结构体有上面这三个指针。其中h_next将item链接到hash表中,hash表的默认大小为65536项,如果数据项多于 65536,就会产生冲突了。memcached采用拉链法来解决冲突,所有hash值相同的key将会被链接成一个单向的链表。prev,next这两个指针则用于将item加入LRU链表中。处于相同slab id的item会被链接成一个双向的链表,heads和tails这两个数组指针分别指向这些slab LRU链表的首项和尾项。当iteam_alloc分配item时,它首先会调用slabs_alloc来申请内存,slabs_alloc有这样一句有趣的一句:
if (! (p->end_page_ptr || p->sl_curr || slabs_newslab(id)))
return 0;

这里有两个或运算符,非(A或B或C)其实等价于(非A)与(非B)与(非C),这样就好理解多了。先从本slab中申请,如果没有内存的话就去 slot里面找,如果还没有找到的话就要new新的了。如果这些办法都失败了,iteam_alloc就需用动动LRU的脑筋了,它会从尾部循环50次,看看没有可以释放的item,代码如下:
for (search = tails[id]; tries>0 && search; tries–, search=search->prev) {
if (search->refcount==0) {
item_unlink(search);
break;
}

item_link_q和item_unlink_q函数专门处理item LRU,一个用于将item放到LRU链表的首位置,一个用于从LRU链表中移除本item。

这里提一下refcount这个属性,当初我一直以为这个引用计数同LRU有关系,后来经杨成一提醒,我才豁然大悟。这个值其实同LRU一点关系都没有,LRU只是通过get来更新链表。refcount用户多客户端的护持操作,当一个客户端get很多个item时,memcached在 process_get_command函数中有这样一句”it->refcount++;”,它使得每个item的refcount都被置成了 1,这样它们就不能被删除掉了。我又想既然这个版本的memcached是单线程的,没有并发问题,应该也无所谓的。memcached使用
sendmsg这个函数发送数据,这个函数的实现如下:
inline int sendmsg(int s, const struct msghdr *msg, int flags)
{
DWORD dwBufferCount;
if(WSASendTo((SOCKET) s,
(LPWSABUF)msg->msg_iov,
(DWORD)msg->msg_iovlen,
&dwBufferCount,
flags,
msg->msg_name,
msg->msg_namelen,
NULL,
NULL
) == 0) {
return dwBufferCount;
}

if(WSAGetLastError() == WSAECONNRESET) return 0;
return -1;
}

它其实封装了WSASendTo函数,这是一个异步发送函数,这就意味着它可能不会一次性将所有的数据都发送出去,如果不将其要发送的数据上锁的话,该 item很有可能就被别的客户端给删除掉了,待会儿想法都没得发了。refcount的值不为0就为1,当操作结束之后,它就会被重置为0。我想 refcount取个lock什么的名字会更适合一些吧。1.2.8是多线程版的,大家可能会想有了多线程的互斥应该就不需要refcount这个变量吧,如果memcached运行于多线程下是可以不用它的,可是”./configure”时如果不加上”–enable-threads”选项,它仍然是单线程版的,所以refcount仍然有它的使用之处。

在item_link_q函数中加入一下代码就可以清晰看到LRU链表的变动情况了(记得将”item* iter”放在函数的前面哦)
printf(“===========================\n”);
while(iter)
{
printf(“key: %s\n”, ITEM_key(iter));
iter = iter->next;
}
printf(“===========================\n”);

并不是每次get一个item,它都会被放到LRU的链首,process_get_command会调用item_update来更新item的一些数据,只有两次访问间隔ITEM_UPDATE_INTERVAL多秒,该item才会被更新,ITEM_UPDATE_INTERVAL等于60。
void item_update(item *it) {
if (it->time < current_time – ITEM_UPDATE_INTERVAL) { assert((it->it_flags & ITEM_SLABBED) == 0);
item_unlink_q(it);
it->time = current_time;
item_link_q(it);
}
}

接下来分析下get命令,这个命令看似简单,其实也是蛮复杂的。这里涉及到两个非常重要的结构体:
struct iovec {
u_long iov_len;
char FAR* iov_base;
};
struct msghdr
{
void *msg_name; /* Socket name */
int msg_namelen; /* Length of name */
struct iovec *msg_iov; /* Data blocks */
int msg_iovlen; /* Number of blocks */
void *msg_accrights; /* Per protocol magic (eg BSD file descriptor passing) */
int msg_accrightslen; /* Length of rights list */
};

msghdr中的msg_iov可以指向一个iovec数组,conn_new为每个客户端创建conn对象时,就初始好了这几个结构体链表:
c->isize = ITEM_LIST_INITIAL;
c->iovsize = IOV_LIST_INITIAL;
c->msgsize = MSG_LIST_INITIAL;
c->hdrsize = 0;
c->rbuf = (char *) malloc(c->rsize);
c->wbuf = (char *) malloc(c->wsize);
//申请200个指针所用的内存空间
c->ilist = (item **) malloc(sizeof(item *) * c->isize);
//申请400个iovec结构体所用的内存空间
c->iov = (struct iovec *) malloc(sizeof(struct iovec) * c->iovsize);
//申请10个msghdr所用的内存空间
c->msglist = (struct msghdr *) malloc(sizeof(struct msghdr) * c->msgsize);

客户端的每个操作,服务器都会应答,如果是合法的命令,服务器会将处理后的结果返回给客户端;如果是错误的命令,服务器要告知错误相关的信息,所以在process_command函数中有这样一段代码:
if (add_msghdr(c)) {
out_string(c, “SERVER_ERROR out of memory”);
return;
}

add_msghdr函数初始化每次应答所需的msghdr结构体,msg->msg_iov指向c->iov所指向的数组的某个位置,待会儿add_iov便会使用紧挨着这个位置之后的iov结构体,一个conn对象可能会用到多个msghdr,每个msghdr所用的iov会放在数组的连续的位置上。msg->msg_name在sendmsg函数中有用,WSASendTo用到了它,不过因为通过套接字已经可以知道客户端的IP地址和端口号了,所以传个空指针也无所谓。
msg = c->msglist + c->msgused;
/* this wipes msg_iovlen, msg_control, msg_controllen, and
msg_flags, the last 3 of which aren’t defined on solaris: */
memset(msg, 0, sizeof(struct msghdr));
msg->msg_iov = &c->iov[c->iovused];
msg->msg_name = &c->request_addr;
msg->msg_namelen = c->request_addr_size;

add_iov(原型为:int add_iov(conn *c, const void *buf, int len)函数的关键操作如下:
m = &c->msglist[c->msgused - 1];
m->msg_iov[m->msg_iovlen].iov_base = (void*) buf;
m->msg_iov[m->msg_iovlen].iov_len = len;

process_get_command函数会连续三次调用add_iov函数:
if (add_iov(c, “VALUE “, 6) ||
add_iov(c, ITEM_key(it), it->nkey) ||
add_iov(c, ITEM_suffix(it), it->nsuffix + it->nbytes))
{
break;
}

最后conn对象就要转变状态了,在conn_mwrite状态下继续处理。
conn_set_state(c, conn_mwrite);

千万不要因为它只占据了小小一块位置就小看了这一句:
switch (transmit(c))

往客户端发送的数据都是由这个函数完成的。
struct msghdr *m = &c->msglist[c->msgcurr];
res = sendmsg(c->sfd, m, 0);

再看看sendmsg的实现吧。
inline int sendmsg(int s, const struct msghdr *msg, int flags)
{
DWORD dwBufferCount;
if(WSASendTo((SOCKET) s,
(LPWSABUF)msg->msg_iov,
(DWORD)msg->msg_iovlen,
&dwBufferCount,
flags,
msg->msg_name,
msg->msg_namelen,
NULL,
NULL
) == 0) {
……………………………

WSASendTo可以将一组iov发送出去,msg->msg_iov指向iov数组的首地址,msg->msg_iovlen为 iov数组的长度。现在可以分析下”get foo1 foo2″这条语句的情况。

get foo1和get foo2每个都要调用三次add_iov函数如下:
if (add_iov(c, “VALUE “, 6) ||
add_iov(c, ITEM_key(it), it->nkey) ||
add_iov(c, ITEM_suffix(it), it->nsuffix + it->nbytes))

所以这就用了6个iov结构体,调用完了之后执行下面的语句:
c->msgbytes += len;
c->iovused++;
m->msg_iovlen++;
导致m->msg_iovlen又被加1了,所以就是7个了,第七个iov被用来存放”END\r\n”。

在process_get_command函数”add_iov(c, “END\r\n”, 5);”这句之后加入一下代码,就可以清楚看到添加iov结构体的一些信息了。
{
struct msghdr* m = &c->msglist[c->msgcurr];
printf(“=============================\n”);
for(j = 0; j < c->msglist->msg_iovlen; ++j)
{
char* buf = m->msg_iov[j].iov_base;
printf(“c->iovused=%d, m->msg_iovlen=%d, buf=%s\n”, c->iovused, m->msg_iovlen, buf);
}
printf(“=============================\n”);
}

ok,接下来就要看怎么删除一个item了。

“delete item n”,参数n可以指定n秒后才删除该item,如果不带这个参数,item将会被立即删除。删除item的函数为 process_delete_command。
当n等于0时:
if (exptime == 0) {
item_unlink(it);
out_string(c, “DELETED”);
return;
}
================================
void item_unlink(item *it) {
if (it->it_flags & ITEM_LINKED) {
it->it_flags &= ~ITEM_LINKED;
stats.curr_bytes -= ITEM_ntotal(it);
stats.curr_items -= 1;
assoc_delete(ITEM_key(it), it->nkey); //解除hash表索引
item_unlink_q(it);
}
if (it->refcount == 0) item_free(it);
}
================================
void item_free(item *it) {
unsigned int ntotal = ITEM_ntotal(it);
assert((it->it_flags & ITEM_LINKED) == 0);
assert(it != heads[it->slabs_clsid]);
assert(it != tails[it->slabs_clsid]);
assert(it->refcount == 0);
/* so slab size changer can tell later if item is already free or not */
it->slabs_clsid = 0;
it->it_flags |= ITEM_SLABBED;
slabs_free(it, ntotal);
}
==================================

slabs_free函数原型为:void slabs_free(void *ptr, size_t size),它的代码有这样一段如下:
#ifdef USE_SYSTEM_MALLOC
mem_malloced -= size;
free(ptr);
return;
#endif
………………….
p->slots[p->sl_curr++] = ptr;

可以看出,如果不定义USE_SYSTEM_MALLOC宏,memcached是不会释放内存的,它会把已经被删除的节点所占用的内存用 slots数组保存起来,以后slabs_alloc首先就从这个slots里面取内存。因为item被删除掉了,如果使用get命令来获取该item的值,那么 assoc_find(key, nkey)将返回一个NULL指针。

上面item_unlink函数还有一个问题,如果当前item的refcount不为0,这个item占用的内存将不会被标记释放,这会导致内存泄漏吗?item的refcount不为0肯定是被别的客户端锁定了,所以我们要考察下drive_machine的处理流程。

“case conn_mwrite:->case TRANSMIT_COMPLETE:”这个语句块中会调用item_remove,该函数实现如下:
void item_remove(item *it) {
assert((it->it_flags & ITEM_SLABBED) == 0);
if (it->refcount) it->refcount–;
assert((it->it_flags & ITEM_DELETED) == 0 || it->refcount);
if (it->refcount == 0 && (it->it_flags & ITEM_LINKED) == 0) {
item_free(it);
}
}

这里判断如果item的refcount为1,就将其减为0,然后再判断it->it_flags不为ITEM_LINKED,如果这两项都被满足了,就调用item_free函数,这个函数内部将调用slabs_free函数,所以也就不存在内存泄漏了。

接下来分析第二种情况,当n不等于0时:
it->refcount++;
/* use its expiration time as its deletion time now */
it->exptime = realtime(exptime);
it->it_flags |= ITEM_DELETED;
todelete[delcurr++] = it;
out_string(c, “DELETED”);

首先将refcount加1,这样该item就不会被别的客户端删除掉。这里的exptime其实就是n 了,”realtime(exptime);”取服务器启动的时间加上这个n值表示当前时间n秒之后本item会过期,因此这个删除是强制性的,不管之前这个key的过期时间是多久的,它都将在n秒之后被删除掉。接着就将item放入todelete数组中。删除该数组的item项的工作就是 delete_handler的事情了,这个函数每隔5s钟被触发执行一次。其主要处理逻辑如下:
{
int i, j=0;
for (i=0; i<delcurr; i++) {
item *it = todelete[i];
if (item_delete_lock_over(it)) {
assert(it->refcount > 0);
it->it_flags &= ~ITEM_DELETED;
item_unlink(it);
item_remove(it);
} else {
todelete[j++] = it;
}
}
delcurr = j;
}

这里使用了i,j两个变量调整数组。item_delete_lock_over函数判断item是否过期,如果过期就删除之,否则被存放到数组的另外一个位置,这个位置用j索引,这样遍历完了之后,数组存放的自然都是未被删除的item了。

最后分析下transmit这个函数吧,其全部实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/*
* Transmit the next chunk of data from our list of msgbuf structures.
*
* Returns:
* TRANSMIT_COMPLETE All done writing.
* TRANSMIT_INCOMPLETE More data remaining to write.
* TRANSMIT_SOFT_ERROR Can’t write any more right now.
* TRANSMIT_HARD_ERROR Can’t write (c-&gt;state is set to conn_closing)
*/
int transmit(conn *c) {
 int res;
 
 if (c->msgcurr < c->msgused &&
  c->msglist[c->msgcurr].msg_iovlen == 0) {
  /* Finished writing the current msg; advance to the next. */
  c->msgcurr++;
 }
 
 //判断是不是到了最后一个mshdr
 if (c->msgcurr < c->msgused) {
  struct msghdr *m = &c->msglist[c->msgcurr];
 
  res = sendmsg(c->sfd, m, 0);
 
  if (res > 0) {
   stats.bytes_written += res;
 
   /* We’ve written some of the data. Remove the completed
   iovec entries from the list of pending writes. */
   //循环发送iov结构体,没发送一个出去,就将 msg_iovlen减1,然后将msg_iov加1
   //指向下一个iov结构体
   while (m->msg_iovlen > 0 && res >= m->msg_iov->iov_len) {
    res -= m->msg_iov->iov_len;
    m->msg_iovlen–;
    m->msg_iov++;
   }
 
   /* Might have written just part of the last iovec entry;
   adjust it so the next write will do the rest. */
   if (res > 0) {
    m->msg_iov->iov_base += res;
    m->msg_iov->iov_len -= res;
   }
 
   return TRANSMIT_INCOMPLETE;
  }
 
  if (res == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
   if (!update_event(c, EV_WRITE | EV_PERSIST)) {
    if (settings.verbose > 0)
     fprintf(stderr, “Couldn’t update event\n”);
 
    conn_set_state(c, conn_closing);
 
    return TRANSMIT_HARD_ERROR;
   }
 
   return TRANSMIT_SOFT_ERROR;
  }
 
  /* if res==0 or res==-1 and error is not EAGAIN or EWOULDBLOCK,
  we have a real error, on which we close the connection */
  if (settings.verbose > 0)
   perror(“Failed to write, and not due to blocking”);
 
  if (c->udp)
   conn_set_state(c, conn_read);
  else
   conn_set_state(c, conn_closing);
 
  return TRANSMIT_HARD_ERROR;
 
 } else {
  return TRANSMIT_COMPLETE;
 }
}

什么情况下一个客户端对象conn对象会有msglist会大于1呢?这又要从drive_machine说起了,因为”case conn_mwrite”靠得比较后面,所以当一个客户端使用循环get许多个item时,就会导致这个客户端对应的conn对象的mshlist长度大于1,transmit函数一次将一个msghdr及其附随的iov发送出去。

好了,如果熟悉了set/get/delete这三个基本的命令,分析单线程版的memcached就不在话下了。这里还有几个需要仔细分析的地方:

1、比较多线程版的memcached同单线程版memcached的异同,多线程版的memcached结构发生诸多变化。
编译使用单线程版,然后使用”memcached -d”启动服务器,当有两个客户端连接上服务器时,”pstree -a”显示内容如下:
├─hald
│ └─hald-runner
│ ├─hald-addon-acpi
│ ├─hald-addon-cpuf
│ ├─hald-addon-gene
│ ├─hald-addon-inpu
│ ├─hald-addon-rfki
│ └─hald-addon-stor
├─indicator-apple –oaf-activate-iid=OAFIID:GNOME_IndicatorApplet_Factory –oaf-ior-fd=18
├─klogd -P /var/run/klogd/kmsg
├─memcached -vv -d
├─mixer_applet2 –oaf-activate-iid=OAFIID:GNOME_MixerApplet_Factory –oaf-ior-fd=26
│ └─{mixer_applet2}
当编译使用多线程版时,当有五个个客户端连接上服务器时,pstree显示内容如下:
├─indicator-apple
├─klogd
├─memcached───4*[{memcached}]
├─mixer_applet2───{mixer_applet2}
├─multiload-apple
如果不指定线程数,服务器默认将开启4个线程(由settings.num_threads指定),每个线程都可以为多个客户端服务。

2、当使用 UDP协议时的情况

3、客户端使用的分布式协议

4、还有很多细节有待进一步考究

哎,路漫漫其修远兮,大家都要上下而求索啊 ^_^

抱歉!评论已关闭.