memcached源码分析-----get命令处理流程

 

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

 

 

        本文以get命令为例子,探讨memcached是如何处理命令的。本文只是探讨memcached处理命令的工作流程,具体的代码细节在不影响阅读的前提下能省略的就省略、能取默认值就取默认值、内存是足够的(不需要动态申请空间就够用了)。涉及到数组、缓存区的就假设已经分配好了。

 

 

        现在假定memcached里面有了一个键值为”tk”的item,此时我们使用命令”get tk”获取对应item的内容。

 

       《半同步半异步网络模型》展示了当memcached进程accept一个新客户端连接时,会把该连接的一些信息封装成一个conn结构体,并且把新连接的初始状态设置成conn_new_cmd。此时,worker线程等待客户端命令的到来。conn结构体有很多成员变量,后文只会列出使用到的成员。

 

 

读取命令:

等待有数据可读:

 

 

        当客户端发送get命令后,memcached的event_base就会监听到客户端对应的socket fd变成可读了,接着就会调用回调函数event_handler处理这个可读事件。实际上回调函数event_handler只是一个傀儡函数,它会调用drive_machine函数进行处理。drive_machine是一个有限状态机,在真正读数据之前它会在几个状态中跳转。

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. void event_handler(const int fd, const short which, void *arg) {  
  2.     conn *c;  
  3.   
  4.     c = (conn *)arg;  
  5.   
  6.     c->which = which;  
  7.   
  8.     /* sanity */  
  9.     if (fd != c->sfd) {  
  10.         conn_close(c);  
  11.         return;  
  12.     }  
  13.   
  14.     drive_machine(c);  
  15.   
  16.     /* wait for next event */  
  17.     return;  
  18. }  
  19.   
  20.   
  21. struct conn {  
  22.     int    sfd;//该conn对应的socket fd  
  23.     enum conn_states  state;//当前状态  
  24.     struct event event;//该conn对应的event  
  25.     short  ev_flags;//event当前监听的事件类型  
  26.     short  which;   /** which events were just triggered */ //触发event回调函数的原因  
  27.   
  28.     //读缓冲区  
  29.     char   *rbuf;   /** buffer to read commands into */   
  30.     //有效数据的开始位置。从rbuf到rcurr之间的数据是已经处理的了,变成无效数据了  
  31.     char   *rcurr;  /** but if we parsed some already, this is where we stopped */  
  32.     //读缓冲区的总长度  
  33.     int    rsize;   /** total allocated size of rbuf */  
  34.     //有效数据的长度。初始值为0  
  35.     int    rbytes;  /** how much data, starting from rcur, do we have unparsed */  
  36.   
  37.   
  38.     ...  
  39.      
  40.     LIBEVENT_THREAD *thread;//这个conn属于哪个worker线程  
  41. };  
  42.   
  43.   
  44. static void drive_machine(conn *c) {  
  45.     bool stop = false;  
  46.     int sfd;  
  47.     int nreqs = settings.reqs_per_event;//20  
  48.     int res;  
  49.     const char *str;  
  50.   
  51.     //drive_machine被调用会进行状态判断,并进行一些处理。但也可能发生状态的转换  
  52.     //此时就需要一个循环,当进行状态转换时,也能处理  
  53.     while (!stop) {  
  54.   
  55.         switch(c->state) {  
  56.             ...  
  57.   
  58.         case conn_waiting://等待socket变成可读的  
  59.             if (!update_event(c, EV_READ | EV_PERSIST)) {//更新监听事件失败  
  60.                 conn_set_state(c, conn_closing);  
  61.                 break;  
  62.             }  
  63.   
  64.             conn_set_state(c, conn_read);  
  65.             //居然stop循环,不过没关系,因为event的可读事件是水平触发的。  
  66.             //马上又会再次进入有限状态机,并且进入下面的conn_read case中。  
  67.             stop = true;  
  68.             break;  
  69.   
  70.         case conn_new_cmd:  
  71.   
  72.             --nreqs;  
  73.             if (nreqs >= 0) {//简单起见,不考虑nreqs小于0的情况  
  74.                 //如果该conn的读缓冲区没有数据,那么将状态改成conn_waiting  
  75.                 //如果该conn的读缓冲区有数据,  那么将状态改成conn_pase_cmd  
  76.                 reset_cmd_handler(c);  
  77.             }  
  78.             break;  
  79.   
  80.             ...  
  81.         }  
  82.     }  
  83.   
  84.     return;  
  85. }  
  86.   
  87.   
  88. static void reset_cmd_handler(conn *c) {  
  89.     c->cmd = -1;  
  90.       
  91.     ...  
  92.     //为了简单,这里假设没有数据  
  93.     if (c->rbytes > 0) {//读缓冲区里面有数据   
  94.         conn_set_state(c, conn_parse_cmd);//解析读到的数据  
  95.     } else {   
  96.         conn_set_state(c, conn_waiting);//否则等待数据的到来  
  97.     }  
  98. }  
  99.   
  100. //设置conn的状态  
  101. static void conn_set_state(conn *c, enum conn_states state) {  
  102.     ...  
  103.   
  104.     if (state != c->state) {  
  105.         c->state = state;  
  106.     }  
  107. }  



读取数据:

 

        在前面,conn的状态跳转到了conn_read。在case conn_read中,worker线程会调用try_read_network函数读取客户端发送的数据。try_read_network函数会尽可能地把所有的数据都读进conn的读缓存区中(当然也是有一个最大限度的)。

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. static void drive_machine(conn *c) {  
  2.     bool stop = false;  
  3.     int sfd;  
  4.     int nreqs = settings.reqs_per_event;//20  
  5.     int res;  
  6.     const char *str;  
  7.   
  8.     //drive_machine被调用会进行状态判断,并进行一些处理。但也可能发生状态的转换  
  9.     //此时就需要一个循环,当进行状态转换时,也能处理  
  10.     while (!stop) {  
  11.   
  12.         switch(c->state) {  
  13.             ...  
  14.   
  15.         case conn_read:  
  16.             //这里假定为TCP  
  17.             res = IS_UDP(c->transport) ? try_read_udp(c) : try_read_network(c);  
  18.   
  19.             switch (res) {  
  20.             …  
  21.             case READ_DATA_RECEIVED://读取到了数据,接着就去解析数据  
  22.                 conn_set_state(c, conn_parse_cmd);  
  23.                 break;  
  24.             …  
  25.             }  
  26.             break;  
  27.   
  28.             ...  
  29.         }  
  30.     }  
  31.   
  32.     return;  
  33. }  
  34.   
  35.   
  36.  //尽可能把socket的所有数据都读进c指向的一个缓冲区里面  
  37. static enum try_read_result try_read_network(conn *c) {  
  38.     enum try_read_result gotdata = READ_NO_DATA_RECEIVED;  
  39.     int res;  
  40.     ...  
  41.   
  42.     while (1) {  
  43.         ...  
  44.         int avail = c->rsize - c->rbytes;  
  45.         res = read(c->sfd, c->rbuf + c->rbytes, avail);  
  46.         if (res > 0) {  
  47.             ...  
  48.             gotdata = READ_DATA_RECEIVED;  
  49.             c->rbytes += res;  
  50.             if (res == avail) {//可能还有数据没有读出来  
  51.                 continue;  
  52.             } else {  
  53.                 break;//socket暂时还没数据了(即已经读取完)  
  54.             }  
  55.         }  
  56.         ...  
  57.     }  
  58.     return gotdata;  
  59. }  



解析命令:

 

        前面已经展示了,worker线程怎么读取数据(命令),并且在读取完毕后会把conn的状态设置为conn_parse_cmd。为了简单起见,我们假设经过一次读取就已经成功读取了一条完整的get命令。

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. static void drive_machine(conn *c) {  
  2.     int res;  
  3.   
  4.     while (!stop) {  
  5.   
  6.         switch(c->state) {  
  7.         case conn_parse_cmd :  
  8.             //返回1表示正在处理读取的一条命令  
  9.             //返回0表示需要继续读取socket的数据才能解析命令  
  10.             //如果读取到了一条完整的命令,那么函数内部会去解析,  
  11.             //并进行调用process_command函数进行一些处理.  
  12.             //像set、add、replace、get这些命令,会在处理的时候调用  
  13.             //conn_set_state(c, conn_nread)  
  14.             if (try_read_command(c) == 0) {  
  15.                 /* wee need more data! */  
  16.                 conn_set_state(c, conn_waiting);  
  17.             }  
  18.   
  19.             break;  
  20.         }  
  21.     }  
  22.   
  23.     return;  
  24. }  
  25.   
  26.   
  27.   
  28. /* 
  29.  * if we have a complete line in the buffer, process it. 
  30.  */  
  31. static int try_read_command(conn *c) {  
  32.   
  33.     ...  
  34.    
  35.     char *el, *cont;  
  36.   
  37.     el = memchr(c->rcurr, '\n', c->rbytes);  
  38.     if (!el) {//没有读取到一条完整的命令  
  39.         ...//为了简单,不考虑这种情况。  
  40.         return 0;//返回0表示需要继续读取socket的数据才能解析命令  
  41.     }  
  42.   
  43.   
  44.     //来到这里,说明已经读取到至少一条完整的命令  
  45.   
  46.       
  47.     cont = el + 1;//用cont指向下一行的开始,无论行尾是\n还是\r\n  
  48.   
  49.     //不同的平台对于行尾有不同的处理,有的为\r\n有的则是\n。所以memcached  
  50.     //还要判断一下\n前面的一个字符是否为\r  
  51.     if ((el - c->rcurr) > 1 && *(el - 1) == '\r') {  
  52.         el--;//指向行尾的开始字符  
  53.     }  
  54.   
  55.     //'\0',C语言字符串结尾符号。结合c->rcurr这个开始位置,就可以确定  
  56.     //这个命令(现在被看作一个字符串)的开始和结束位置。rcurr指向了一个字符串  
  57.     //注意,下一条命令的开始位置由前面的cont指明了  
  58.     *el = '\0';  
  59.       
  60.   
  61.     c->last_cmd_time = current_time;  
  62.     //处理这个命令  
  63.     process_command(c, c->rcurr);//命令字符串由c->rcurr指向  
  64.   
  65.     ...  
  66.   
  67.     return 1;//返回1表示正在处理读取的一条命令  
  68. }  


        上面的try_read_command函数,以\n或者\n\r为作为一条数据的结尾。并且会把数据的结尾赋值为’\0’,这样conn的rcurr指针就相当于指向一个以’\0’结尾的字符串。接着就会调用process_command函数处理这个字符串,在处理之前还要解析出这个字符串具体是什么命令。

 

符号化命令内容:

 

        在执行命令之前,必须要知道接收到的字符串是什么命令以及参数是什么。为此,memcached会调用tokenize_command函数处理命令字符串,将字符串符号化。比如命令字符串"set tt 3 0 10",将符号化为”set”、”tt”、”3”、”0”和”10”(后面会将这些称为token)。此外tokenize_command还会清除命令字符串里面的多余空白符。Memcached定义了一个token_t结构体(如下面代码所示)。memcached还为每一条字符串命令定义一个token_t数组,数组每一个元素的value成员指向对应token的开始位置,length成员则记录该token的长度。

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. #define COMMAND_TOKEN 0  
  2. #define SUBCOMMAND_TOKEN 1  
  3. #define KEY_TOKEN 1  
  4.   
  5. #define MAX_TOKENS 8  
  6.   
  7. typedef struct token_s {  
  8.     char *value;  
  9.     size_t length;  
  10. } token_t;  
  11.   
  12.   
  13. //command指向这条命令(该命令以字符串的形式表示)  
  14. static void process_command(conn *c, char *command) {  
  15.   
  16.     token_t tokens[MAX_TOKENS];  
  17.     size_t ntokens;  
  18.     int comm;  
  19.   
  20.     ...  
  21.   
  22.     //将一条命令分割成一个个的token,并用tokens数组一一对应的指向  
  23.     //比如命令"set tt 3 0 10",将被分割成"set"、"tt"、"3"、"0"、"10"  
  24.     //并用tokens数组的5个元素对应指向。token_t类型的value成员指向对应token  
  25.     //在command字符串中的位置,length则指明该token的长度。  
  26.     //该函数返回token的数量,数量是用户敲入的命令token数 + 1.  
  27.     //上面的set命令例子,tokenize_command会返回6。  最后一个token是无意义的  
  28.     ntokens = tokenize_command(command, tokens, MAX_TOKENS);//将命令记号化  
  29.   
  30.     //对于命令"get tk",那么tokens[0].value 等于指向"get"的开始位置  
  31.     //tokens[1].value 则指向"tk"的开始位置  
  32.     if (ntokens >= 3 &&  
  33.         ((strcmp(tokens[COMMAND_TOKEN].value, "get") == 0) ||  
  34.          (strcmp(tokens[COMMAND_TOKEN].value, "bget") == 0))) {  
  35.   
  36.         process_get_command(c, tokens, ntokens, false);  
  37.   
  38.     }  
  39.     else  
  40.     {  
  41.         ...//根据tokens判断是否为其他命令,并进行对应的处理  
  42.     }  
  43.       
  44. }  



执行命令:

回应信息的存储:

 

 

        process_get_command函数在处理get命令时,并不是直接拷贝一份item的数据(考虑一下效率和内存),所以memcached是直接使用item本身的数据,用iovec结构体的成员变量指向item里面的数据。这样能省去拷贝数据内存,也能提高效率。但memcached里面的item可能随时被删除(归还给slab内存分配器),可以通过占用这个item,防止item被删除。在《item引用计数》中说到,只要增加item的引用计数就能防止这个item被删除。于是在process_get_command函数中会占有item,并用一个item指针数组记录其占用了哪些item(这个数组在conn结构体中)。当memcached将item的数据返回给客户端后,就会释放对item的占用。

        前面说到memcached使用iovec结构体的成员变量指向item的数据,但memcached并不是使用writev函数向客户端写数据的,而是使用sendmsg函数。sendmsg函数使用msghdr结构体指针作为参数。因为sendmsg函数中msghdr结构体中的iovec数组长度是有限制的,所以conn结构体中有一个msghdr数组。数组中每一个msghdr结构体带有IOV_MAX个iovec结构体。通过动态申请msghdr数组,可以使得有很多个iovec结构体,不再受IOV_MAX的限制。当然前面说到的iovec结构体个数也是要有足够多,所以conn结构体里面还是有一个iovec指针用来动态申请iovec结构体。现在来看一下conn结构体对应的成员。

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. struct conn {  
  2.   
  3.     struct iovec *iov;//iovec数组指针  
  4.     //数组大小  
  5.     int    iovsize;   /* number of elements allocated in iov[] */  
  6.     //已经使用的数组元素个数  
  7.     int    iovused;   /* number of elements used in iov[] */  
  8.   
  9.   
  10.     //因为msghdr结构体里面的iovec结构体数组长度是有限制的。所以为了能  
  11.     //传输更多的数据,只能增加msghdr结构体的个数.add_msghdr函数负责增加  
  12.     struct msghdr *msglist;//指向msghdr数组  
  13.     //数组大小  
  14.     int    msgsize;   /* number of elements allocated in msglist[] */  
  15.     //已经使用了的msghdr元素个数  
  16.     int    msgused;   /* number of elements used in msglist[] */  
  17.     //正在用sendmsg函数传输msghdr数组中的哪一个元素  
  18.     int    msgcurr;   /* element in msglist[] being transmitted now */  
  19.     //msgcurr指向的msghdr总共有多少个字节  
  20.     int    msgbytes;  /* number of bytes in current msg */  
  21.   
  22.   
  23.     //worker线程需要占有这个item,直至把item的数据都写回给客户端了  
  24.     //故需要一个item指针数组记录本conn占有的item  
  25.     item   **ilist;   /* list of items to write out */  
  26.     int    isize;//数组的大小  
  27.     item   **icurr;//当前使用到的item(在释放占用item时会用到)  
  28.     int    ileft;//ilist数组中有多少个item需要释放  
  29. };  



        在process_command函数中,memcached会增加msglist数组的大小。

 

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. static void process_command(conn *c, char *command) {  
  2.   
  3.     c->msgcurr = 0;  
  4.     c->msgused = 0;  
  5.     c->iovused = 0;  
  6.     if (add_msghdr(c) != 0) {  
  7.         out_of_memory(c, "SERVER_ERROR out of memory preparing response");  
  8.         return;  
  9.     }  
  10.   
  11.     ...  
  12. }  
  13.   
  14.   
  15. /* 
  16.  * Adds a message header to a connection. 
  17.  * 
  18.  * Returns 0 on success, -1 on out-of-memory. 
  19.  */  
  20. static int add_msghdr(conn *c)  
  21. {  
  22.     struct msghdr *msg;  
  23.   
  24.     assert(c != NULL);  
  25.   
  26.     if (c->msgsize == c->msgused) {//已经用完了  
  27.         msg = realloc(c->msglist, c->msgsize * 2 * sizeof(struct msghdr));  
  28.         if (! msg) {  
  29.             return -1;  
  30.         }  
  31.         c->msglist = msg;  
  32.         c->msgsize *= 2;  
  33.     }  
  34.   
  35.     msg = c->msglist + c->msgused;//msg指向空闲的节点  
  36.   
  37.     /* this wipes msg_iovlen, msg_control, msg_controllen, and 
  38.        msg_flags, the last 3 of which aren't defined on solaris: */  
  39.     memset(msg, 0, sizeof(struct msghdr));  
  40.   
  41.     msg->msg_iov = &c->iov[c->iovused];//指向空闲的iovec  
  42.   
  43.   
  44.     c->msgbytes = 0;  
  45.     c->msgused++;  
  46.   
  47.     return 0;  
  48. }  



 

 

        前面说到memcached使用iovec结构体的成员变量指向item的数据,实际上除了item数据,所有回应客户端的数据(包括错误信息)都是通过iovec结构体指向的。memcached通过add_iov函数把要回应的字符串加入到iovec中。

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. static int add_iov(conn *c, const void *buf, int len) {  
  2.     struct msghdr *m;  
  3.     int leftover;  
  4.     bool limit_to_mtu;  
  5.   
  6.     assert(c != NULL);  
  7.   
  8.     //在process_command函数中,一开始会调用add_msghdr函数,而add_msghdr会把  
  9.     //msgused++,所以msgused会等于1,即使在conn_new函数中它被赋值为0  
  10.     do {  
  11.         m = &c->msglist[c->msgused - 1];  
  12.   
  13.         /* 
  14.          * Limit UDP packets, and the first payloads of TCP replies, to 
  15.          * UDP_MAX_PAYLOAD_SIZE bytes. 
  16.          */  
  17.         limit_to_mtu = IS_UDP(c->transport) || (1 == c->msgused);  
  18.   
  19.         /* We may need to start a new msghdr if this one is full. */  
  20.         if (m->msg_iovlen == IOV_MAX ||//一个msghdr最多只能有IOV_MAX个iovec结构体  
  21.             (limit_to_mtu && c->msgbytes >= UDP_MAX_PAYLOAD_SIZE)) {  
  22.             add_msghdr(c);  
  23.             m = &c->msglist[c->msgused - 1];  
  24.         }  
  25.   
  26.         //保证iovec数组是足够用的。调用add_iov函数一次会消耗一个iovec结构体  
  27.         //所以可以在插入数据之前保证iovec数组是足够用的  
  28.         if (ensure_iov_space(c) != 0)  
  29.             return -1;  
  30.   
  31.         /* If the fragment is too big to fit in the datagram, split it up */  
  32.         if (limit_to_mtu && len + c->msgbytes > UDP_MAX_PAYLOAD_SIZE) {  
  33.             leftover = len + c->msgbytes - UDP_MAX_PAYLOAD_SIZE;  
  34.             len -= leftover;  
  35.         } else {  
  36.             leftover = 0;  
  37.         }  
  38.   
  39.         m = &c->msglist[c->msgused - 1];  
  40.   
  41.         //用一个iovec结构体指向要回应的数据  
  42.         m->msg_iov[m->msg_iovlen].iov_base = (void *)buf;  
  43.         m->msg_iov[m->msg_iovlen].iov_len = len;  
  44.   
  45.         c->msgbytes += len;  
  46.         c->iovused++;  
  47.         m->msg_iovlen++;  
  48.   
  49.         buf = ((char *)buf) + len;  
  50.         len = leftover;  
  51.     } while (leftover > 0);  
  52.   
  53.     return 0;  
  54. }  
  55.   
  56.   
  57.   
  58. /* 
  59.  * Ensures that there is room for another struct iovec in a connection's 
  60.  * iov list. 
  61.  * 
  62.  * Returns 0 on success, -1 on out-of-memory. 
  63.  */  
  64. static int ensure_iov_space(conn *c) {  
  65.     assert(c != NULL);  
  66.   
  67.     //已经使用完了之前申请的  
  68.     if (c->iovused >= c->iovsize) {  
  69.         int i, iovnum;  
  70.         struct iovec *new_iov = (struct iovec *)realloc(c->iov,  
  71.                                 (c->iovsize * 2) * sizeof(struct iovec));  
  72.         if (! new_iov) {  
  73.             return -1;  
  74.         }  
  75.         c->iov = new_iov;  
  76.         c->iovsize *= 2;  
  77.   
  78.         /* Point all the msghdr structures at the new list. */  
  79.         //因为iovec数组已经重新分配在别的空间了,而msglist数组元素指向这个iovec  
  80.         //数组,所以需要修改msglist数组元素的值  
  81.         for (i = 0, iovnum = 0; i < c->msgused; i++) {  
  82.             c->msglist[i].msg_iov = &c->iov[iovnum];  
  83.             iovnum += c->msglist[i].msg_iovlen;  
  84.         }  
  85.     }  
  86.   
  87.     return 0;  
  88. }  

 

        看了上面的代码,可能读者还不是很明白前面列出的conn结构体成员的关联。不懂的,可以参考下图:

        

 

 

处理get命令:

 

        有了上面的说明和代码,现在来看一下process_get_command函数。当然我们这里也是假设上面三个数组都是分配了内存。在process_get_command函数中会涉及到item的哈希表查找和删除(超时懒惰删除),关于这两点可以分别参考《哈希表查找item》和《删除item》。

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. item *item_get(const char *key, const size_t nkey) {  
  2.     item *it;  
  3.     uint32_t hv;  
  4.     hv = hash(key, nkey);  
  5.     item_lock(hv);  
  6.     it = do_item_get(key, nkey, hv);  
  7.     item_unlock(hv);  
  8.     return it;  
  9. }  
  10.   
  11.   
  12. /** wrapper around assoc_find which does the lazy expiration logic */  
  13. //调用do_item_get的函数都已经加上了item_lock(hv)段级别锁或者全局锁  
  14. item *do_item_get(const char *key, const size_t nkey, const uint32_t hv) {  
  15.     //mutex_lock(&cache_lock);  
  16.     item *it = assoc_find(key, nkey, hv);//assoc_find函数内部没有加锁  
  17.       
  18.     ...  
  19.     //mutex_unlock(&cache_lock);  
  20.   
  21.     if (it != NULL) {  
  22.         if (...) {  
  23.             ...  
  24.         } else if (it->exptime != 0 && it->exptime <= current_time) {//该item已经过期失效了  
  25.             do_item_unlink(it, hv);//引用计数会减一  
  26.             do_item_remove(it);//引用计数减一,如果引用计数等于0,就删除  
  27.             it = NULL;  
  28.         } else {  
  29.             //把这个item标志为被访问过的  
  30.             it->it_flags |= ITEM_FETCHED;  
  31.         }  
  32.     }  
  33.   
  34.     return it;  
  35. }  
  36.   
  37.   
  38.   
  39.   
  40. /* ntokens is overwritten here... shrug.. */  
  41. static inline void process_get_command(conn *c, token_t *tokens, size_t ntokens, bool return_cas) {  
  42.     char *key;  
  43.     size_t nkey;  
  44.     int i = 0;  
  45.     item *it;  
  46.     token_t *key_token = &tokens[KEY_TOKEN];  
  47.     char *suffix;  
  48.     assert(c != NULL);  
  49.   
  50.     do {  
  51.         //因为一个get命令可以同时获取多条记录的内容  
  52.         //比如get key1 key2 key3  
  53.         while(key_token->length != 0) {  
  54.   
  55.             key = key_token->value;  
  56.             nkey = key_token->length;  
  57.   
  58.             it = item_get(key, nkey);  
  59.     
  60.             if (it) {  
  61.   
  62.                 /* 
  63.                  * Construct the response. Each hit adds three elements to the 
  64.                  * outgoing data list: 
  65.                  *   "VALUE " 
  66.                  *   key 
  67.                  *   " " + flags + " " + data length + "\r\n" + data (with \r\n) 
  68.                  */  
  69.   
  70.                 if (return_cas)  
  71.                 {  
  72.                     ...//不是cas  
  73.                 }  
  74.                 else  
  75.                 {  
  76.                   //填充要返回的信息  
  77.                   if (add_iov(c, "VALUE ", 6) != 0 ||//如果add_iov成功,则返回0  
  78.                       add_iov(c, ITEM_key(it), it->nkey) != 0 ||  
  79.                       add_iov(c, ITEM_suffix(it), it->nsuffix + it->nbytes) != 0)  
  80.                       {  
  81.                           item_remove(it);//引用计数减一  
  82.                           break;  
  83.                       }  
  84.                 }  
  85.   
  86.                 //刷新这个item的访问时间以及在LRU队列中的位置  
  87.                 item_update(it);  
  88.   
  89.                 //并不会马上放弃对这个item的占用。因为在add_iov函数中,memcached并不为  
  90.                 //复制一份item,而是直接使用item结构体本身的数据。故不能马上解除对  
  91.                 //item的引用,不然其他worker线程就有机会把这个item释放,导致野指针  
  92.                 *(c->ilist + i) = it;//把这个item放到ilist数组中,日后会进行释放的  
  93.                 i++;  
  94.   
  95.             }   
  96.   
  97.             key_token++;  
  98.         }  
  99.   
  100.   
  101.         //因为调用一次tokenize_command最多只可以解析MAX_TOKENS-1个token,但  
  102.         //get命令的键值key个数可以有很多个,所以此时就会出现后面的键值  
  103.         //不在第一次tokenize的tokens数组中,此时需要多次调用tokenize_command  
  104.         //函数,把所有的键值都tokenize出来。注意,此时还是在get命令中。  
  105.         //当然在看这里的代码时直接忽略这种情况,我们只考虑"get tk"命令  
  106.         if(key_token->value != NULL) {  
  107.             ntokens = tokenize_command(key_token->value, tokens, MAX_TOKENS);  
  108.             key_token = tokens;  
  109.         }  
  110.   
  111.     } while(key_token->value != NULL);  
  112.   
  113.     c->icurr = c->ilist;  
  114.     c->ileft = i;  
  115.   
  116.     /* 
  117.         If the loop was terminated because of out-of-memory, it is not 
  118.         reliable to add END\r\n to the buffer, because it might not end 
  119.         in \r\n. So we send SERVER_ERROR instead. 
  120.     */  
  121.     if (key_token->value != NULL || add_iov(c, "END\r\n", 5) != 0  
  122.         || (IS_UDP(c->transport) && build_udp_headers(c) != 0)) {  
  123.         out_of_memory(c, "SERVER_ERROR out of memory writing get response");  
  124.     }  
  125.     else {  
  126.         conn_set_state(c, conn_mwrite);//更改conn的状态  
  127.         c->msgcurr = 0;  
  128.     }  
  129. }  



回应命令:

        前面的process_get_command函数已经把要写的数据都通过iovec结构体指明了,并且把conn的状态设置为conn_mwrite。现在来看一下memcached具体是怎么写数据的。

 

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. static void drive_machine(conn *c) {  
  2.   
  3.     bool stop = false;  
  4.   
  5.     while (!stop) {  
  6.   
  7.         switch(c->state) {  
  8.           
  9.         ...  
  10.         case conn_mwrite:  
  11.             ...  
  12.   
  13.             switch (transmit(c)) {//发送数据给c->sfd指明的客户端  
  14.             case TRANSMIT_COMPLETE://发送数据完毕  
  15.                 if (c->state == conn_mwrite) {  
  16.                     conn_release_items(c);//释放对item的占用  
  17.                     /* XXX:  I don't know why this wasn't the general case */  
  18.                     if(c->protocol == binary_prot) {  
  19.                         conn_set_state(c, c->write_and_go);  
  20.                     } else {//我们只考虑文本协议  
  21.                         conn_set_state(c, conn_new_cmd);//又回到了一开始的conn_new_cmd状态  
  22.                     }  
  23.                 }   
  24.                 break;  
  25.   
  26.             case TRANSMIT_INCOMPLETE://还没发送完毕  
  27.                 break;  
  28.   
  29.             }  
  30.             break;  
  31.         }  
  32.     }  
  33.   
  34.     return;  
  35. }  
  36.   
  37.   
  38.   
  39.   
  40.  //通过s->sfd把数据写到对端  
  41. static enum transmit_result transmit(conn *c) {  
  42.   
  43.     if (c->msgcurr < c->msgused &&   
  44.             c->msglist[c->msgcurr].msg_iovlen == 0) {//msgcurr指向的msghdr已经发送完毕  
  45.         /* Finished writing the current msg; advance to the next. */  
  46.         c->msgcurr++;  
  47.     }  
  48.   
  49.   
  50.     if (c->msgcurr < c->msgused) {//所有的数据都已经发送完毕  
  51.         ssize_t res;  
  52.         struct msghdr *m = &c->msglist[c->msgcurr];  
  53.   
  54.         res = sendmsg(c->sfd, m, 0);  
  55.         if (res > 0) {  
  56.   
  57.             //通过sendmsg返回值确定已经写了多少个iovec数组。循环减去每一个iovec数组的每一个  
  58.             //元素的数据长度即可  
  59.             while (m->msg_iovlen > 0 && res >= m->msg_iov->iov_len) {  
  60.                 res -= m->msg_iov->iov_len;  
  61.                 m->msg_iovlen--;  
  62.                 m->msg_iov++;  
  63.             }  
  64.   
  65.             //只写了iovec结构体的部分数据  
  66.             if (res > 0) {  
  67.                 m->msg_iov->iov_base = (caddr_t)m->msg_iov->iov_base + res;  
  68.                 m->msg_iov->iov_len -= res;  
  69.             }  
  70.             return TRANSMIT_INCOMPLETE;  
  71.         }  
  72.   
  73.     } else {  
  74.         return TRANSMIT_COMPLETE;  
  75.     }  
  76. }  



 

 

        可以看到,即使transmit函数一次把所有的数据都写到了客户端,还是会调用transmit函数两次才能返回TRANSMIT_COMPLETE。当memcached把所有的数据都写回客户端后,就会调用conn_release_items函数释放对item的占用。

[cpp] view plain copy
 
 在CODE上查看代码片派生到我的代码片
    1. static void conn_release_items(conn *c) {  
    2.   
    3.     ...  
    4.     while (c->ileft > 0) {  
    5.         item *it = *(c->icurr);  
    6.         assert((it->it_flags & ITEM_SLABBED) == 0);  
    7.         item_remove(it);  
    8.         c->icurr++;  
    9.         c->ileft--;  
    10.     }  
    11.   
    12.     ...  
    13.     c->icurr = c->ilist;  
    14. }  

posted on 2016-06-05 10:46  c++kuzhon  阅读(179)  评论(0)    收藏  举报

导航