半同步/半异步:

 

        memcached使用半同步/半异步网络模型处理客户端的连接和通信。

        半同步/半异步模型的基础设施:主线程创建多个子线程(这些子线程也称为worker线程),每一个线程都维持自己的事件循环,即每个线程都有自己的epoll,并且都会调用epoll_wait函数进入事件监听状态。每一个worker线程(子线程)和主线程之间都用一条管道相互通信。每一个子线程都监听自己对应那条管道的读端。当主线程想和某一个worker线程进行通信,直接往对应的那条管道写入数据即可。

        半同步/半异步模型的工作流程:主线程负责监听进程对外的TCP监听端口。当客户端申请连接connect到进程的时候,主线程负责接收accept客户端的连接请求。然后主线程选择其中一个worker线程,把客户端fd通过对应的管道传给worker线程。worker线程得到客户端的fd后负责和这个客户端进行一切的通信。

 

        半同步/半异步模型的工作示意图如下图所示:

         memcached源码分析-----半同步半异步网络模型_memcached

 

 

        memcached里面的半同步/半异步和上面所说的差不多,区别在于:1. memcached使用libevent作为进行事件监听;2.memcached往管道里面写的内容不是fd,而是一个简单的字符。每一个worker线程都维护一个CQ队列,主线程把fd和一些信息写入一个CQ_ITEM里面,然后主线程往worker线程的CQ队列里面push这个CQ_ITEM。接着主线程使用管道通知worker线程:“我已经发了一个新客户给你,你去处理吧”。

        memcached的半同步/半异步如下面这幅经典的图所示:

         memcached源码分析-----半同步半异步网络模型_主线程_02

 

 

memcached的具体实现:

 

        上图看到每一个worker线程都有一个CQ队列,主线程accept到新客户端后,就把新客户端的信息封装成一个CQ_ITEM,然后push到选定线程的CQ队列中。


CQ队列:

 

        现在我们来看一下CQ队列长什么样的。

[cpp]  view plain copy  
 
  1. typedef struct conn_queue_item  CQ_ITEM;  
  2.   
  3. struct conn_queue_item {  
  4.     int               sfd;  
  5.     enum conn_states  init_state;  
  6.     int               event_flags;  
  7.     int               read_buffer_size;  
  8.     enum network_transport     transport;  
  9.     CQ_ITEM          *next;  
  10. };  
  11.   
  12. /* A connection queue. */  
  13. typedef struct conn_queue  CQ;  
  14. struct conn_queue {  
  15.     CQ_ITEM *head;//指向队列的第一个节点  
  16.     CQ_ITEM *tail;//指向队列的最后一个节点  
  17.     pthread_mutex_t lock; //一个队列就对应一个锁  
  18. };  

        可以看到结构体conn_queue(即CQ队列结构体)有一个pthread_mutex_t类型变量lock,这说明主线程往某个worker线程的CQ队列里面push一个CQ_ITEM的时候必然要加锁的。下面是初始化CQ队列,以及push、pop一个CQ_ITEM的代码。

 

[cpp]  view plain copy  
 
  1. static void cq_init(CQ *cq) {  
  2.     pthread_mutex_init(&cq->lock, NULL);  
  3.     cq->head = NULL;  
  4.     cq->tail = NULL;  
  5. }  
  6.   
  7. static CQ_ITEM *cq_pop(CQ *cq) {  
  8.     CQ_ITEM *item;  
  9.   
  10.     pthread_mutex_lock(&cq->lock);  
  11.     item = cq->head;  
  12.     if (NULL != item) {  
  13.         cq->head = item->next;  
  14.         if (NULL == cq->head)  
  15.             cq->tail = NULL;  
  16.     }  
  17.     pthread_mutex_unlock(&cq->lock);  
  18.   
  19.     return item;  
  20. }  
  21.   
  22. /* 
  23.  * Adds an item to a connection queue. 
  24.  */  
  25. static void cq_push(CQ *cq, CQ_ITEM *item) {  
  26.     item->next = NULL;  
  27.   
  28.     pthread_mutex_lock(&cq->lock);  
  29.     if (NULL == cq->tail)  
  30.         cq->head = item;  
  31.     else  
  32.         cq->tail->next = item;  
  33.     cq->tail = item;  
  34.     pthread_mutex_unlock(&cq->lock);  
  35. }  
        注意,cq_pop函数不同于STL里面的pop。cq_pop函数会返回一个CQ_ITEM。

 

 

 

        由上面代码得到的CQ队列如下图所示:

         memcached源码分析-----半同步半异步网络模型_数组_03

 

 

为worker线程构建CQ队列:

        主线程又是怎么访问各个worker线程的CQ队列呢?在C语言里面的答案当然是使用全局变量啦。memcached专门定义了结构体,如下:

 

[cpp]  view plain copy  
 
  1. typedef struct {  
  2.     pthread_t thread_id; //线程id        
  3.     struct event_base *base; //线程所使用的event_base   
  4.     struct event notify_event;//用于监听管道读事件的event  
  5.     int notify_receive_fd; //管道的读端fd  
  6.     int notify_send_fd;   //管道的写端fd  
  7.     struct conn_queue *new_conn_queue; /* queue of new connections to handle */  
  8.     ...  
  9. } LIBEVENT_THREAD;  

 

        看到LIBEVENT_THREAD结构体的这些成员,完全可以顾名思义。memcached定义了LIBEVENT_THREAD类型的一个全局变量指针threads。当确定了memcached有多少个worker线程后,就会动态申请一个LIBEVENT_THREAD数组,并让threads指向其。于是每一个worker线程都对应有一个LIBEVENT_THREAD结构体。主线程通过全局变量threads就可以很方便地访问每一个worker线程的CQ队列和通信管道。

 

        上面介绍了每一个线程都有一个LIBEVENT_THREAD结构体,现在来看一下具体的代码实现。注意代码里面监听管道可读的event的回调函数是thread_libevent_process,回调参数是线程自己的LIBEVENT_THREAD结构体指针。

[cpp]  view plain copy  
 
  1. static LIBEVENT_THREAD *threads;  
  2. //参数nthreads是worker线程的数量。main_base则是主线程的event_base  
  3. //主线程在main函数 调用本函数,创建nthreads个worker线程  
  4. void thread_init(int nthreads, struct event_base *main_base) {  
  5.     int         i;  
  6.        
  7. //申请一个CQ_ITEM时需要加锁,后面会介绍  
  8.     pthread_mutex_init(&cqi_freelist_lock, NULL);  
  9.     cqi_freelist = NULL;  
  10.   
  11.   
  12.     //申请具有nthreads个元素的LIBEVENT_THREAD数组  
  13.     threads = calloc(nthreads, sizeof(LIBEVENT_THREAD));  
  14.   
  15.     for (i = 0; i < nthreads; i++) {  
  16.         int fds[2];  
  17.         if (pipe(fds)) {//为每个worker线程分配一个管道,用于通知worker线程  
  18.             perror("Can't create notify pipe");  
  19.             exit(1);  
  20.         }  
  21.   
  22.         threads[i].notify_receive_fd = fds[0];  
  23.         threads[i].notify_send_fd = fds[1];  
  24.   
  25.         //每一个线程配一个event_base,并设置event监听notify_receive_fd的读事件  
  26.         //同时还为这个线程分配一个conn_queue队列  
  27.         setup_thread(&threads[i]);  
  28.     }  
  29.   
  30.     /* Create threads after we've done all the libevent setup. */  
  31.     for (i = 0; i < nthreads; i++) {  
  32.         //创建线程,线程函数为worker_libevent, 线程参数为&threads[i]  
  33.         create_worker(worker_libevent, &threads[i]);  
  34.     }  
  35.   
  36.     ...  
  37. }  
  38.   
  39.   
  40.   
  41. static void setup_thread(LIBEVENT_THREAD *me) {  
  42.     me->base = event_init();//新建一个event_base  
  43.   
  44.     /* Listen for notifications from other threads */  
  45.     //监听管道的读端  
  46.     event_set(&me->notify_event, me->notify_receive_fd,//监听管道的读端  
  47.               EV_READ | EV_PERSIST, thread_libevent_process, me); //等同于event_new  
  48.     event_base_set(me->base, &me->notify_event);//将event_base和event相关联  
  49.   
  50.     if (event_add(&me->notify_event, 0) == -1) {  
  51.         fprintf(stderr, "Can't monitor libevent notify pipe\n");  
  52.         exit(1);  
  53.     }  
  54.   
  55.     //创建一个CQ队列  
  56.     me->new_conn_queue = malloc(sizeof(struct conn_queue));  
  57.   
  58.     cq_init(me->new_conn_queue);  
  59.   
  60.     ...  
  61. }  
  62.   
  63.   
  64. static void create_worker(void *(*func)(void *), void *arg) {  
  65.     pthread_t       thread;  
  66.     pthread_attr_t  attr;  
  67.     int             ret;  
  68.   
  69.     pthread_attr_init(&attr);  
  70.   
  71.     if ((ret = pthread_create(&thread, &attr, func, arg)) != 0) {  
  72.         fprintf(stderr, "Can't create thread: %s\n",  
  73.                 strerror(ret));  
  74.         exit(1);  
  75.     }  
  76. }  

 

 

CQ_ITEM内存池:

 

        memcached在申请一个CQ_ITEM结构体时,并不是直接使用malloc申请的。因为这样做的话可能会导致大量的内存碎片(作为长期运行的服务器进程memcached需要考虑这个问题)。为此,memcached也为CQ_ITEM使用类似内存池的技术:预分配一块比较大的内存,将这块大内存切分成多个CQ_ITEM。下面是实现代码:

[cpp]  view plain copy  
 
  1. //本函数采用了一些优化手段.并非每调用一次本函数就申请一块内存。这会导致  
  2.  //内存碎片。这里采取的优化方法是,一次性分配64个CQ_ITEM大小的内存(即预分配).  
  3.  //下次调用本函数的时候,直接从之前分配64个中要一个即可。  
  4.  //由于是为了防止内存碎片,所以不是以链表的形式放置这64个CQ_ITEM。而是数组的形式。  
  5.  //于是,cqi_free函数就有点特别了。它并不会真正释放.而是像内存池那样归还  
  6. static CQ_ITEM *cqi_new(void) {  
  7.     //所有线程都会访问cqi_freelist的。所以需要加锁  
  8.     CQ_ITEM *item = NULL;  
  9.     pthread_mutex_lock(&cqi_freelist_lock);  
  10.     if (cqi_freelist) {  
  11.         item = cqi_freelist;  
  12.         cqi_freelist = item->next;  
  13.     }  
  14.     pthread_mutex_unlock(&cqi_freelist_lock);  
  15.   
  16.     if (NULL == item) {//没有多余的CQ_ITEM了  
  17.         int i;  
  18.   
  19.         item = malloc(sizeof(CQ_ITEM) * ITEMS_PER_ALLOC);//该宏等于64  
  20.   
  21.         //item[0]直接返回为调用者,不用next指针连在一起。调用者负责将  
  22.         //item[0].next赋值为NULL  
  23.         for (i = 2; i < ITEMS_PER_ALLOC; i++)//将这块内存分成一个个的item并且用next指针像链表一样连起来  
  24.             item[i - 1].next = &item[i];  
  25.   
  26.         pthread_mutex_lock(&cqi_freelist_lock);  
  27.         //因为主线程负责申请CQ_ITEM,子线程负责释放CQ_ITEM。所以cqi_freelist此刻  
  28.         //可能并不等于NULL。由于使用头插法,所以无论cqi_freeelist是否为NULL,都能  
  29.         //把链表连起来的。  
  30.         item[ITEMS_PER_ALLOC - 1].next = cqi_freelist;  
  31.         cqi_freelist = &item[1];  
  32.         pthread_mutex_unlock(&cqi_freelist_lock);  
  33.     }  
  34.   
  35.     return item;  
  36. }  
  37.   
  38.   
  39. //并非释放,而是像内存池那样归还  
  40. static void cqi_free(CQ_ITEM *item) {  
  41.     pthread_mutex_lock(&cqi_freelist_lock);  
  42.     item->next = cqi_freelist;  
  43.     cqi_freelist = item;  //头插法归还  
  44.     pthread_mutex_unlock(&cqi_freelist_lock);  
  45. }  


主线程的工作:

 

        前面展示了在半同步/半异步中worker线程是怎么构建基础设施的。接下来看看主线程为了构建基础需要完成哪些工作。首先我们来看一下main函数,该main函数已经被我删除得很精简了。

[cpp]  view plain copy  
 
  1. int main (int argc, char **argv) {  
  2.       
  3.     //检查libevent的版本是否足够新.1.3即可  
  4.     if (!sanitycheck()) {  
  5.         return EX_OSERR;  
  6.     }  
  7.   
  8.     //对memcached的关键设置取默认值  
  9.     settings_init();  
  10.   
  11.     ...//解析memcached启动参数  
  12.   
  13.     //main_base是一个struct event_base类型的全局变量  
  14.     main_base = event_init();//为主线程创建一个event_base  
  15.   
  16.     conn_init();//先不管,后面会说到  
  17.   
  18.     //创建settings.num_threads个worker线程,并且为每个worker线程创建一个CQ队列  
  19.     //并为这些worker申请各自的event_base,worker线程然后进入事件循环中     
  20.     thread_init(settings.num_threads, main_base);  
  21.   
  22.     //设置一个定时event(也叫超时event),定时(频率为一秒)更新current_time变量  
  23.     //这个超时event是add到全局变量main_base里面的,所以主线程负责更新current_time(这是一个很重要的全局变量)  
  24.     clock_handler(0, 0, 0);  
  25.   
  26.   
  27.     /* create the listening socket, bind it, and init */  
  28.     if (settings.socketpath == NULL) {  
  29.         FILE *portnumber_file = NULL;  
  30.         //创建监听客户端的socket  
  31.         if (settings.port && server_sockets(settings.port, tcp_transport,//tcp_transport是枚举类型  
  32.                                            portnumber_file)) {  
  33.             vperror("failed to listen on TCP port %d", settings.port);  
  34.             exit(EX_OSERR);  
  35.         }  
  36.   
  37.         ...   
  38.     }  
  39.   
  40.   
  41.      
  42.     if (event_base_loop(main_base, 0) != 0) {//主线程进入事件循环  
  43.         retval = EXIT_FAILURE;  
  44.     }  
  45.   
  46.     return retval;  
  47. }  

        在main函数中,主线程创建了属于自己的event_base,存放在全局变量main_base中。在main函数的最后,主线程调用event_base_loop进入事件循环中。中间的server_sockets函数是创建一个监听客户端的socket,并将创建一个event监听该socket的可读事件。下面就看一下这个函数。为了简单起见下面的代码都忽略错误处理。

[cpp]  view plain copy  
 
  1. //port是默认的11211或者用户使用-p选项设置的端口号  
  2. //主线程在main函数会调用本函数  
  3. static int server_sockets(int port, enum network_transport transport,  
  4.                           FILE *portnumber_file) {  
  5.   
  6.     //settings.inter里面可能有多个IP地址.如果有多个那么将用逗号分隔  
  7.     char *b;  
  8.     int ret = 0;  
  9.     //复制一个字符串,避免下面的strtok_r函数修改(污染)全局变量settings.inter  
  10.     char *list = strdup(settings.inter);  
  11.   
  12.     //这个循环主要是处理多个IP的情况  
  13.     for (char *p = strtok_r(list, ";,", &b);  
  14.          p != NULL; //分割出一个个的ip,使用分号;作为分隔符  
  15.          p = strtok_r(NULL, ";,", &b)) {  
  16.         int the_port = port;  
  17.         char *s = strchr(p, ':');//启动的可能使用-l ip:port 参数形式  
  18.         //ip后面接着端口号,即指定ip的同时也指定了该ip的端口号  
  19.         //此时采用ip后面的端口号,而不是采用-p指定的端口号  
  20.         if (s != NULL) {  
  21.             *s = '\0';//截断后面的端口号,使得p指向的字符串只是一个ip  
  22.             ++s;  
  23.             if (!safe_strtol(s, &the_port)) {//非法端口号参数值  
  24.                 return 1;  
  25.             }  
  26.         }  
  27.         if (strcmp(p, "*") == 0) {  
  28.             p = NULL;  
  29.         }  
  30.         //处理其中一个IP。有p指定ip(或者hostname)  
  31.         ret |= server_socket(p, the_port, transport, portnumber_file);  
  32.     }  
  33.     free(list);  
  34.     return ret;  
  35. }  
  36.   
  37.   
  38. static conn *listen_conn = NULL;//监听队列(可能要同时监听多个IP)  
  39.   
  40.   
  41.  //interface是一个ip、hostname或者NULL。这个ip字符串后面没有端口号。端口号由参数port指出  
  42. static int server_socket(const char *interface,  
  43.                          int port,  
  44.                          enum network_transport transport,  
  45.                          FILE *portnumber_file) {  
  46.     int sfd;  
  47.     struct linger ling = {0, 0};  
  48.     struct addrinfo *ai;  
  49.     struct addrinfo *next;  
  50.     struct addrinfo hints = { .ai_flags = AI_PASSIVE,  
  51.                               .ai_family = AF_UNSPEC };  
  52.     char port_buf[NI_MAXSERV];  
  53.     int success = 0;  
  54.     int flags =1;  
  55.   
  56.     hints.ai_socktype = IS_UDP(transport) ? SOCK_DGRAM : SOCK_STREAM;  
  57.   
  58.   
  59.     snprintf(port_buf, sizeof(port_buf), "%d", port);  
  60.     getaddrinfo(interface, port_buf, &hints, &ai);  
  61.   
  62.     //如果interface是一个hostname的话,那么可能就有多个ip  
  63.     for (next= ai; next; next= next->ai_next) {  
  64.         conn *listen_conn_add;  
  65.   
  66.         //创建一个套接字,然后设置为非阻塞的  
  67.         sfd = new_socket(next);//调用socket函数  
  68.         bind(sfd, next->ai_addr, next->ai_addrlen);  
  69.   
  70.         success++;  
  71.         listen(sfd, settings.backlog);  
  72.   
  73.   
  74.         if (!(listen_conn_add = conn_new(sfd, conn_listening,  
  75.                                          EV_READ | EV_PERSIST, 1,  
  76.                                          transport, main_base))) {  
  77.             fprintf(stderr, "failed to create listening connection\n");  
  78.             exit(EXIT_FAILURE);  
  79.         }  
  80.   
  81.         //将要监听的多个conn放到一个监听队列里面  
  82.         listen_conn_add->next = listen_conn;  
  83.         listen_conn = listen_conn_add;  
  84.   
  85.     }  
  86.   
  87.     freeaddrinfo(ai);  
  88.   
  89.     /* Return zero iff we detected no errors in starting up connections */  
  90.     return success == 0;  
  91. }  
  92.   
  93.   
  94. static int new_socket(struct addrinfo *ai) {  
  95.     int sfd;  
  96.     int flags;  
  97.     sfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);  
  98.     flags = fcntl(sfd, F_GETFL, 0);  
  99.     fcntl(sfd, F_SETFL, flags | O_NONBLOCK);  
  100.   
  101.     return sfd;  
  102. }  

        上面代码的流程还是蛮清晰的。就是根据用户的IP和端口号建立一个socket,bind、listen监听客户端的到来。因为主线程申请的socketfd已经设置为非阻塞的,所以listen函数会立刻返回。在main函数中,主线程最终将调用event_base_loop函数进入事件监听循环,处理客户端的连接请求。

 

连接管理者conn:

 

        现在我们来关注一下conn_new函数。因为在这里函数里面会创建一个用于监听socket fd的event,并调用event_add加入到主线程的event_base中。从conn_new的函数名来看,是new一个conn。确实如何。事实上memcached为每一个socket fd(也就是一个连接)都创建一个conn结构体,用于管理这个socket fd(连接)。因为一个连接会有很多数据和状态信息,所以需要一个结构体来负责管理。所以阅读conn_new函数之前,还需要先阅读一下conn_init函数,了解conn结构体的一些初试化。

        在《命令行参数详解》中有提到,可以在启动memcached的时候通过命令行参数-c num指定memcached允许的最大同时在线客户端数量。即使没有使用该参数,memcached也会采用默认值的,具体的默认值可以参数《关键配置的默认值》。也就是说在启动memcached之后就可以确定最多允许多少个客户端同时在线。有了这个数值就不用一有新连接就malloc一个conn结构体(这样会很容易造成内存碎片)。有了这个数值那么可以在一开始(conn_init函数),就申请动态申请一个数组。有新连接就从这个数组中分配一个元素即可。

[cpp]  view plain copy  
 
  1. conn **conns;  
  2. static void conn_init(void) {  
  3.     /* We're unlikely to see an FD much higher than maxconns. */  
  4.     //已经dup返回当前未使用的最小正整数,所以next_fd等于此刻已经消耗了的fd个数  
  5.     int next_fd = dup(1);//获取当前已经使用的fd的个数  
  6.     //预留一些文件描述符。也就是多申请一些conn结构体。以免有别的需要把文件描述符  
  7.     //给占了。导致socket fd的值大于这个数组长度  
  8.     int headroom = 10;//预留一些文件描述符  /* account for extra unexpected open FDs */  
  9.     struct rlimit rl;  
  10.   
  11.     //settings.maxconns的默认值是1024.  
  12.     max_fds = settings.maxconns + headroom + next_fd;  
  13.   
  14.     /* But if possible, get the actual highest FD we can possibly ever see. */  
  15.     if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {  
  16.         max_fds = rl.rlim_max;  
  17.     } else {  
  18.         fprintf(stderr, "Failed to query maximum file descriptor; "  
  19.                         "falling back to maxconns\n");  
  20.     }  
  21.   
  22.     close(next_fd);//next_fd只是用来计数的,并没有其他用途  
  23.   
  24.     //注意,申请的conn结构体数量是比settings.maxconns这个客户端同时在线数  
  25.     //还要大的。因为memcached是直接用socket fd的值作为数组下标的。也正是  
  26.     //这个原因,前面需要使用headroom预留一些空间给突发情况  
  27.     if ((conns = calloc(max_fds, sizeof(conn *))) == NULL) {//注意是conn指针不是conn结构体  
  28.         fprintf(stderr, "Failed to allocate connection structures\n");  
  29.         /* This is unrecoverable so bail out early. */  
  30.         exit(1);  
  31.     }  
  32. }  

        上面代码中,calloc申请的是conn*指针数组而不是conn结构体数组。主要是因为conn结构体是比较大的一个结构体(成员变量很多)。不一定会存在settings.maxconns个同时在线的客户端。所以可以等到需要conn结构体的时候再去动态申请。需要时去动态申请,这样会有内存碎片啊!非也!!因为可以循环使用的。如果没有这个conn*指针数组,那么当这个连接断开后就要free这个conn结构体所占的内存(不然就内存泄漏了)。有了这个数组那么就可以不free,由数组管理这个内存。下面的conn_new函数展示了这一点。

[cpp]  view plain copy  
 
  1. //为sfd分配一个conn结构体,并且为这个sfd建立一个event,然后让base监听这个event  
  2. conn *conn_new(const int sfd, enum conn_states init_state,//init_state值为conn_listening  
  3.                 const int event_flags,  
  4.                 const int read_buffer_size, enum network_transport transport,  
  5.                 struct event_base *base) {  
  6.     conn *c;  
  7.   
  8.     assert(sfd >= 0 && sfd < max_fds);  
  9.     c = conns[sfd];//直接使用下标  
  10.   
  11.     if (NULL == c) {//之前没有哪个连接用过这个sfd值,需要申请一个conn结构体  
  12.         if (!(c = (conn *)calloc(1, sizeof(conn)))) {  
  13.             fprintf(stderr, "Failed to allocate connection object\n");  
  14.             return NULL;  
  15.         }  
  16.       
  17.         ...//初始化一些成员变量  
  18.   
  19.         c->sfd = sfd;  
  20.         conns[sfd] = c; //将这个结构体交由conns数组管理  
  21.     }  
  22.   
  23.     ...//初始化另外一些成员变量  
  24.     c->state = init_state;//值为conn_listening  
  25.   
  26.     //等同于event_assign,会自动关联current_base。event的回调函数是event_handler  
  27.     event_set(&c->event, sfd, event_flags, event_handler, (void *)c);  
  28.     event_base_set(base, &c->event);  
  29.     c->ev_flags = event_flags;  
  30.   
  31.     if (event_add(&c->event, 0) == -1) {  
  32.         perror("event_add");  
  33.         return NULL;  
  34.     }  
  35.   
  36.     return c;  
  37. }  


 

        综合上面的代码可以看到,主线程的基础实施也已经搭好了。注意,主线程对于socket fd 可读事件的回调函数是event_handler,回调参数是conn这个结构体指针。


 

牛刀小试:

 

        主线程和worker线程的基础设施都已经搭建好了,现在来尝试一下accept一个客户端。在跑一遍整个流程之前,先回忆一下回调函数。worker线程对于管道可读事件的回调函数是ethread_libevent_process函数。主线程对于socket fd可读事件的回调函数是event_handler函数。conn结构体成员state的值为conn_listening。现在走起!!直奔event_handler函数。

[cpp]  view plain copy  
 
  1. void event_handler(const int fd, const short which, void *arg) {  
  2.     conn *c;  
  3.   
  4.     c = (conn *)arg;  
  5.     assert(c != NULL);  
  6.   
  7.     c->which = which;  
  8.     if (fd != c->sfd) {  
  9.         conn_close(c);  
  10.         return;  
  11.     }  
  12.   
  13.     drive_machine(c);  
  14.     return;  
  15. }  

        太简单了吧,有没有搞错。event_handler函数确实简单,但其调用的drive_machine函数就确实很复杂。drive_machine函数内部是一个有限状态机。本文已经很长了,所以不会详解讲解有限状态机。下面只挑出处理新连接的那部分讲解。

[cpp]  view plain copy  
 
  1. static void drive_machine(conn *c) {  
  2.     bool stop = false;  
  3.     int sfd;  
  4.     socklen_t addrlen;  
  5.     struct sockaddr_storage addr;  
  6.     int res;  
  7.     const char *str;  
  8.   
  9.     assert(c != NULL);  
  10.   
  11.     //drive_machine被调用会进行状态判断,并进行一些处理。但也可能发生状态的转换  
  12.     //此时就需要一个循环,当进行状态转换时,也能处理  
  13.     while (!stop) {  
  14.   
  15.         switch(c->state) {  
  16.         case conn_listening:  
  17.             addrlen = sizeof(addr);  
  18.   
  19.             sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen);  
  20.   
  21.             ...  
  22.   
  23.             //选定一个worker线程,new一个CQ_ITEM,把这个CQ_ITEM仍给这个线程.  
  24.             dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST,  
  25.                                  DATA_BUFFER_SIZE, tcp_transport);  
  26.   
  27.             stop = true;  
  28.             break;  
  29.   
  30.             ...  
  31.         }  
  32.     }  
  33.   
  34.     return;  
  35. }  
  36.   
  37.   
  38.   
  39. static int last_thread = -1;  
  40.   
  41. //参数 sfd, conn_new_cmd, EV_READ | EV_PERSIST, DATA_BUFFER_SIZE, tcp_transport  
  42. void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags,  
  43.                        int read_buffer_size, enum network_transport transport) {  
  44.     CQ_ITEM *item = cqi_new();//申请一个CQ_ITEM  
  45.   
  46.     char buf[1];  
  47.   
  48.     int tid = (last_thread + 1) % settings.num_threads;//轮询的方式选定一个worker线程  
  49.   
  50.     LIBEVENT_THREAD *thread = threads + tid;  
  51.   
  52.     last_thread = tid;  
  53.   
  54.     item->sfd = sfd;  
  55.     item->init_state = init_state;//conn_new_cmd  
  56.     item->event_flags = event_flags;//EV_READ | EV_PERSIST  
  57.     item->read_buffer_size = read_buffer_size;//DATA_BUFFER_SIZE(2048)  
  58.     item->transport = transport;  
  59.   
  60.     cq_push(thread->new_conn_queue, item);//把这个item放到选定的worker线程的CQ队列中  
  61.   
  62.     buf[0] = 'c';  
  63.     if (write(thread->notify_send_fd, buf, 1) != 1) {//通知worker线程,有新客户端连接到来  
  64.         perror("Writing to thread notify pipe");  
  65.     }  
  66. }  

 


        现在主线程已经通知了选定的worker线程。接下来就是worker线程怎么处理这个通知了。下面看一下worker线程的管道可读事件回调函数thread_libevent_process。

[cpp]  view plain copy  
  1. static void thread_libevent_process(int fd, short which, void *arg) {  
  2.     LIBEVENT_THREAD *me = arg;  
  3.     CQ_ITEM *item;  
  4.     char buf[1];  
  5.   
  6.     read(fd, buf, 1);  
  7.   
  8.     switch (buf[0]) {  
  9.     case 'c':  
  10.         //从CQ队列中读取一个item,因为是pop所以读取后,CQ队列会把这个item从队列中删除  
  11.         item = cq_pop(me->new_conn_queue);  
  12.   
  13.         if (NULL != item) {  
  14.             //为sfd分配一个conn结构体,并且为这个sfd建立一个event,然后让base监听这个event  
  15.             //这个sfd的事件回调函数是event_handler  
  16.             conn *c = conn_new(item->sfd, item->init_state, item->event_flags,  
  17.                                item->read_buffer_size, item->transport, me->base);  
  18.   
  19.             c->thread = me;  
  20.   
  21.             cqi_free(item);  
  22.         }  
  23.         break;  
  24.   
  25.     }  
  26. }  

        正如前面所说的,memcached为每一个连接申请一个conn结构体进行维护。conn_new函数内部会为这个socket fd申请一个event并添加到该worker线程的event_base里面。当客户端发送命令时,worker线程就能监听到。这个conn_new函数前面已经说过了,这里也就不给出代码了。

 

 

        在以后,都是worker线程负责这里这个客户端的一切通信,也是worker线程负责完成客户端的命令,包括申请内存存储数据、查询数据、删掉数据。这些苦工都是worker线程完成的,而没有其它线程帮忙。不过大可放心,memcached对于这命令一般都能在常数时间时间复杂度内完成。所以,即使一个worker线程有多个客户端连接,也完全应付得过来。

 

 

半同步/半异步:

 

        memcached使用半同步/半异步网络模型处理客户端的连接和通信。

        半同步/半异步模型的基础设施:主线程创建多个子线程(这些子线程也称为worker线程),每一个线程都维持自己的事件循环,即每个线程都有自己的epoll,并且都会调用epoll_wait函数进入事件监听状态。每一个worker线程(子线程)和主线程之间都用一条管道相互通信。每一个子线程都监听自己对应那条管道的读端。当主线程想和某一个worker线程进行通信,直接往对应的那条管道写入数据即可。

        半同步/半异步模型的工作流程:主线程负责监听进程对外的TCP监听端口。当客户端申请连接connect到进程的时候,主线程负责接收accept客户端的连接请求。然后主线程选择其中一个worker线程,把客户端fd通过对应的管道传给worker线程。worker线程得到客户端的fd后负责和这个客户端进行一切的通信。

 

        半同步/半异步模型的工作示意图如下图所示:

         memcached源码分析-----半同步半异步网络模型_memcached

 

 

        memcached里面的半同步/半异步和上面所说的差不多,区别在于:1. memcached使用libevent作为进行事件监听;2.memcached往管道里面写的内容不是fd,而是一个简单的字符。每一个worker线程都维护一个CQ队列,主线程把fd和一些信息写入一个CQ_ITEM里面,然后主线程往worker线程的CQ队列里面push这个CQ_ITEM。接着主线程使用管道通知worker线程:“我已经发了一个新客户给你,你去处理吧”。

        memcached的半同步/半异步如下面这幅经典的图所示:

         memcached源码分析-----半同步半异步网络模型_主线程_02

 

 

memcached的具体实现:

 

        上图看到每一个worker线程都有一个CQ队列,主线程accept到新客户端后,就把新客户端的信息封装成一个CQ_ITEM,然后push到选定线程的CQ队列中。

 

CQ队列:

 

        现在我们来看一下CQ队列长什么样的。

[cpp]  view plain copy  
 
  1. typedef struct conn_queue_item  CQ_ITEM;  
  2.   
  3. struct conn_queue_item {  
  4.     int               sfd;  
  5.     enum conn_states  init_state;  
  6.     int               event_flags;  
  7.     int               read_buffer_size;  
  8.     enum network_transport     transport;  
  9.     CQ_ITEM          *next;  
  10. };  
  11.   
  12. /* A connection queue. */  
  13. typedef struct conn_queue  CQ;  
  14. struct conn_queue {  
  15.     CQ_ITEM *head;//指向队列的第一个节点  
  16.     CQ_ITEM *tail;//指向队列的最后一个节点  
  17.     pthread_mutex_t lock; //一个队列就对应一个锁  
  18. };  


        可以看到结构体conn_queue(即CQ队列结构体)有一个pthread_mutex_t类型变量lock,这说明主线程往某个worker线程的CQ队列里面push一个CQ_ITEM的时候必然要加锁的。下面是初始化CQ队列,以及push、pop一个CQ_ITEM的代码。

 

[cpp]  view plain copy  
 
  1. static void cq_init(CQ *cq) {  
  2.     pthread_mutex_init(&cq->lock, NULL);  
  3.     cq->head = NULL;  
  4.     cq->tail = NULL;  
  5. }  
  6.   
  7. static CQ_ITEM *cq_pop(CQ *cq) {  
  8.     CQ_ITEM *item;  
  9.   
  10.     pthread_mutex_lock(&cq->lock);  
  11.     item = cq->head;  
  12.     if (NULL != item) {  
  13.         cq->head = item->next;  
  14.         if (NULL == cq->head)  
  15.             cq->tail = NULL;  
  16.     }  
  17.     pthread_mutex_unlock(&cq->lock);  
  18.   
  19.     return item;  
  20. }  
  21.   
  22. /* 
  23.  * Adds an item to a connection queue. 
  24.  */  
  25. static void cq_push(CQ *cq, CQ_ITEM *item) {  
  26.     item->next = NULL;  
  27.   
  28.     pthread_mutex_lock(&cq->lock);  
  29.     if (NULL == cq->tail)  
  30.         cq->head = item;  
  31.     else  
  32.         cq->tail->next = item;  
  33.     cq->tail = item;  
  34.     pthread_mutex_unlock(&cq->lock);  
  35. }  

        注意,cq_pop函数不同于STL里面的pop。cq_pop函数会返回一个CQ_ITEM。

 

 

 

        由上面代码得到的CQ队列如下图所示:

         memcached源码分析-----半同步半异步网络模型_数组_03

 

 

为worker线程构建CQ队列:

        主线程又是怎么访问各个worker线程的CQ队列呢?在C语言里面的答案当然是使用全局变量啦。memcached专门定义了结构体,如下:

 

[cpp]  view plain copy  
 
  1. typedef struct {  
  2.     pthread_t thread_id; //线程id        
  3.     struct event_base *base; //线程所使用的event_base   
  4.     struct event notify_event;//用于监听管道读事件的event  
  5.     int notify_receive_fd; //管道的读端fd  
  6.     int notify_send_fd;   //管道的写端fd  
  7.     struct conn_queue *new_conn_queue; /* queue of new connections to handle */  
  8.     ...  
  9. } LIBEVENT_THREAD;  

 

 

        看到LIBEVENT_THREAD结构体的这些成员,完全可以顾名思义。memcached定义了LIBEVENT_THREAD类型的一个全局变量指针threads。当确定了memcached有多少个worker线程后,就会动态申请一个LIBEVENT_THREAD数组,并让threads指向其。于是每一个worker线程都对应有一个LIBEVENT_THREAD结构体。主线程通过全局变量threads就可以很方便地访问每一个worker线程的CQ队列和通信管道。

 

        上面介绍了每一个线程都有一个LIBEVENT_THREAD结构体,现在来看一下具体的代码实现。注意代码里面监听管道可读的event的回调函数是thread_libevent_process,回调参数是线程自己的LIBEVENT_THREAD结构体指针。

[cpp]  view plain copy  
 
  1. static LIBEVENT_THREAD *threads;  
  2. //参数nthreads是worker线程的数量。main_base则是主线程的event_base  
  3. //主线程在main函数 调用本函数,创建nthreads个worker线程  
  4. void thread_init(int nthreads, struct event_base *main_base) {  
  5.     int         i;  
  6.        
  7. //申请一个CQ_ITEM时需要加锁,后面会介绍  
  8.     pthread_mutex_init(&cqi_freelist_lock, NULL);  
  9.     cqi_freelist = NULL;  
  10.   
  11.   
  12.     //申请具有nthreads个元素的LIBEVENT_THREAD数组  
  13.     threads = calloc(nthreads, sizeof(LIBEVENT_THREAD));  
  14.   
  15.     for (i = 0; i < nthreads; i++) {  
  16.         int fds[2];  
  17.         if (pipe(fds)) {//为每个worker线程分配一个管道,用于通知worker线程  
  18.             perror("Can't create notify pipe");  
  19.             exit(1);  
  20.         }  
  21.   
  22.         threads[i].notify_receive_fd = fds[0];  
  23.         threads[i].notify_send_fd = fds[1];  
  24.   
  25.         //每一个线程配一个event_base,并设置event监听notify_receive_fd的读事件  
  26.         //同时还为这个线程分配一个conn_queue队列  
  27.         setup_thread(&threads[i]);  
  28.     }  
  29.   
  30.     /* Create threads after we've done all the libevent setup. */  
  31.     for (i = 0; i < nthreads; i++) {  
  32.         //创建线程,线程函数为worker_libevent, 线程参数为&threads[i]  
  33.         create_worker(worker_libevent, &threads[i]);  
  34.     }  
  35.   
  36.     ...  
  37. }  
  38.   
  39.   
  40.   
  41. static void setup_thread(LIBEVENT_THREAD *me) {  
  42.     me->base = event_init();//新建一个event_base  
  43.   
  44.     /* Listen for notifications from other threads */  
  45.     //监听管道的读端  
  46.     event_set(&me->notify_event, me->notify_receive_fd,//监听管道的读端  
  47.               EV_READ | EV_PERSIST, thread_libevent_process, me); //等同于event_new  
  48.     event_base_set(me->base, &me->notify_event);//将event_base和event相关联  
  49.   
  50.     if (event_add(&me->notify_event, 0) == -1) {  
  51.         fprintf(stderr, "Can't monitor libevent notify pipe\n");  
  52.         exit(1);  
  53.     }  
  54.   
  55.     //创建一个CQ队列  
  56.     me->new_conn_queue = malloc(sizeof(struct conn_queue));  
  57.   
  58.     cq_init(me->new_conn_queue);  
  59.   
  60.     ...  
  61. }  
  62.   
  63.   
  64. static void create_worker(void *(*func)(void *), void *arg) {  
  65.     pthread_t       thread;  
  66.     pthread_attr_t  attr;  
  67.     int             ret;  
  68.   
  69.     pthread_attr_init(&attr);  
  70.   
  71.     if ((ret = pthread_create(&thread, &attr, func, arg)) != 0) {  
  72.         fprintf(stderr, "Can't create thread: %s\n",  
  73.                 strerror(ret));  
  74.         exit(1);  
  75.     }  
  76. }  

 

 

CQ_ITEM内存池:

 

        memcached在申请一个CQ_ITEM结构体时,并不是直接使用malloc申请的。因为这样做的话可能会导致大量的内存碎片(作为长期运行的服务器进程memcached需要考虑这个问题)。为此,memcached也为CQ_ITEM使用类似内存池的技术:预分配一块比较大的内存,将这块大内存切分成多个CQ_ITEM。下面是实现代码:

[cpp]  view plain copy  
 
  1. //本函数采用了一些优化手段.并非每调用一次本函数就申请一块内存。这会导致  
  2.  //内存碎片。这里采取的优化方法是,一次性分配64个CQ_ITEM大小的内存(即预分配).  
  3.  //下次调用本函数的时候,直接从之前分配64个中要一个即可。  
  4.  //由于是为了防止内存碎片,所以不是以链表的形式放置这64个CQ_ITEM。而是数组的形式。  
  5.  //于是,cqi_free函数就有点特别了。它并不会真正释放.而是像内存池那样归还  
  6. static CQ_ITEM *cqi_new(void) {  
  7.     //所有线程都会访问cqi_freelist的。所以需要加锁  
  8.     CQ_ITEM *item = NULL;  
  9.     pthread_mutex_lock(&cqi_freelist_lock);  
  10.     if (cqi_freelist) {  
  11.         item = cqi_freelist;  
  12.         cqi_freelist = item->next;  
  13.     }  
  14.     pthread_mutex_unlock(&cqi_freelist_lock);  
  15.   
  16.     if (NULL == item) {//没有多余的CQ_ITEM了  
  17.         int i;  
  18.   
  19.         item = malloc(sizeof(CQ_ITEM) * ITEMS_PER_ALLOC);//该宏等于64  
  20.   
  21.         //item[0]直接返回为调用者,不用next指针连在一起。调用者负责将  
  22.         //item[0].next赋值为NULL  
  23.         for (i = 2; i < ITEMS_PER_ALLOC; i++)//将这块内存分成一个个的item并且用next指针像链表一样连起来  
  24.             item[i - 1].next = &item[i];  
  25.   
  26.         pthread_mutex_lock(&cqi_freelist_lock);  
  27.         //因为主线程负责申请CQ_ITEM,子线程负责释放CQ_ITEM。所以cqi_freelist此刻  
  28.         //可能并不等于NULL。由于使用头插法,所以无论cqi_freeelist是否为NULL,都能  
  29.         //把链表连起来的。  
  30.         item[ITEMS_PER_ALLOC - 1].next = cqi_freelist;  
  31.         cqi_freelist = &item[1];  
  32.         pthread_mutex_unlock(&cqi_freelist_lock);  
  33.     }  
  34.   
  35.     return item;  
  36. }  
  37.   
  38.   
  39. //并非释放,而是像内存池那样归还  
  40. static void cqi_free(CQ_ITEM *item) {  
  41.     pthread_mutex_lock(&cqi_freelist_lock);  
  42.     item->next = cqi_freelist;  
  43.     cqi_freelist = item;  //头插法归还  
  44.     pthread_mutex_unlock(&cqi_freelist_lock);  
  45. }  



主线程的工作:

 

        前面展示了在半同步/半异步中worker线程是怎么构建基础设施的。接下来看看主线程为了构建基础需要完成哪些工作。首先我们来看一下main函数,该main函数已经被我删除得很精简了。

[cpp]  view plain copy  
 
  1. int main (int argc, char **argv) {  
  2.       
  3.     //检查libevent的版本是否足够新.1.3即可  
  4.     if (!sanitycheck()) {  
  5.         return EX_OSERR;  
  6.     }  
  7.   
  8.     //对memcached的关键设置取默认值  
  9.     settings_init();  
  10.   
  11.     ...//解析memcached启动参数  
  12.   
  13.     //main_base是一个struct event_base类型的全局变量  
  14.     main_base = event_init();//为主线程创建一个event_base  
  15.   
  16.     conn_init();//先不管,后面会说到  
  17.   
  18.     //创建settings.num_threads个worker线程,并且为每个worker线程创建一个CQ队列  
  19.     //并为这些worker申请各自的event_base,worker线程然后进入事件循环中     
  20.     thread_init(settings.num_threads, main_base);  
  21.   
  22.     //设置一个定时event(也叫超时event),定时(频率为一秒)更新current_time变量  
  23.     //这个超时event是add到全局变量main_base里面的,所以主线程负责更新current_time(这是一个很重要的全局变量)  
  24.     clock_handler(0, 0, 0);  
  25.   
  26.   
  27.     /* create the listening socket, bind it, and init */  
  28.     if (settings.socketpath == NULL) {  
  29.         FILE *portnumber_file = NULL;  
  30.         //创建监听客户端的socket  
  31.         if (settings.port && server_sockets(settings.port, tcp_transport,//tcp_transport是枚举类型  
  32.                                            portnumber_file)) {  
  33.             vperror("failed to listen on TCP port %d", settings.port);  
  34.             exit(EX_OSERR);  
  35.         }  
  36.   
  37.         ...   
  38.     }  
  39.   
  40.   
  41.      
  42.     if (event_base_loop(main_base, 0) != 0) {//主线程进入事件循环  
  43.         retval = EXIT_FAILURE;  
  44.     }  
  45.   
  46.     return retval;  
  47. }  

 

        在main函数中,主线程创建了属于自己的event_base,存放在全局变量main_base中。在main函数的最后,主线程调用event_base_loop进入事件循环中。中间的server_sockets函数是创建一个监听客户端的socket,并将创建一个event监听该socket的可读事件。下面就看一下这个函数。为了简单起见下面的代码都忽略错误处理。

[cpp]  view plain copy  
 
  1. //port是默认的11211或者用户使用-p选项设置的端口号  
  2. //主线程在main函数会调用本函数  
  3. static int server_sockets(int port, enum network_transport transport,  
  4.                           FILE *portnumber_file) {  
  5.   
  6.     //settings.inter里面可能有多个IP地址.如果有多个那么将用逗号分隔  
  7.     char *b;  
  8.     int ret = 0;  
  9.     //复制一个字符串,避免下面的strtok_r函数修改(污染)全局变量settings.inter  
  10.     char *list = strdup(settings.inter);  
  11.   
  12.     //这个循环主要是处理多个IP的情况  
  13.     for (char *p = strtok_r(list, ";,", &b);  
  14.          p != NULL; //分割出一个个的ip,使用分号;作为分隔符  
  15.          p = strtok_r(NULL, ";,", &b)) {  
  16.         int the_port = port;  
  17.         char *s = strchr(p, ':');//启动的可能使用-l ip:port 参数形式  
  18.         //ip后面接着端口号,即指定ip的同时也指定了该ip的端口号  
  19.         //此时采用ip后面的端口号,而不是采用-p指定的端口号  
  20.         if (s != NULL) {  
  21.             *s = '\0';//截断后面的端口号,使得p指向的字符串只是一个ip  
  22.             ++s;  
  23.             if (!safe_strtol(s, &the_port)) {//非法端口号参数值  
  24.                 return 1;  
  25.             }  
  26.         }  
  27.         if (strcmp(p, "*") == 0) {  
  28.             p = NULL;  
  29.         }  
  30.         //处理其中一个IP。有p指定ip(或者hostname)  
  31.         ret |= server_socket(p, the_port, transport, portnumber_file);  
  32.     }  
  33.     free(list);  
  34.     return ret;  
  35. }  
  36.   
  37.   
  38. static conn *listen_conn = NULL;//监听队列(可能要同时监听多个IP)  
  39.   
  40.   
  41.  //interface是一个ip、hostname或者NULL。这个ip字符串后面没有端口号。端口号由参数port指出  
  42. static int server_socket(const char *interface,  
  43.                          int port,  
  44.                          enum network_transport transport,  
  45.                          FILE *portnumber_file) {  
  46.     int sfd;  
  47.     struct linger ling = {0, 0};  
  48.     struct addrinfo *ai;  
  49.     struct addrinfo *next;  
  50.     struct addrinfo hints = { .ai_flags = AI_PASSIVE,  
  51.                               .ai_family = AF_UNSPEC };  
  52.     char port_buf[NI_MAXSERV];  
  53.     int success = 0;  
  54.     int flags =1;  
  55.   
  56.     hints.ai_socktype = IS_UDP(transport) ? SOCK_DGRAM : SOCK_STREAM;  
  57.   
  58.   
  59.     snprintf(port_buf, sizeof(port_buf), "%d", port);  
  60.     getaddrinfo(interface, port_buf, &hints, &ai);  
  61.   
  62.     //如果interface是一个hostname的话,那么可能就有多个ip  
  63.     for (next= ai; next; next= next->ai_next) {  
  64.         conn *listen_conn_add;  
  65.   
  66.         //创建一个套接字,然后设置为非阻塞的  
  67.         sfd = new_socket(next);//调用socket函数  
  68.         bind(sfd, next->ai_addr, next->ai_addrlen);  
  69.   
  70.         success++;  
  71.         listen(sfd, settings.backlog);  
  72.   
  73.   
  74.         if (!(listen_conn_add = conn_new(sfd, conn_listening,  
  75.                                          EV_READ | EV_PERSIST, 1,  
  76.                                          transport, main_base))) {  
  77.             fprintf(stderr, "failed to create listening connection\n");  
  78.             exit(EXIT_FAILURE);  
  79.         }  
  80.   
  81.         //将要监听的多个conn放到一个监听队列里面  
  82.         listen_conn_add->next = listen_conn;  
  83.         listen_conn = listen_conn_add;  
  84.   
  85.     }  
  86.   
  87.     freeaddrinfo(ai);  
  88.   
  89.     /* Return zero iff we detected no errors in starting up connections */  
  90.     return success == 0;  
  91. }  
  92.   
  93.   
  94. static int new_socket(struct addrinfo *ai) {  
  95.     int sfd;  
  96.     int flags;  
  97.     sfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);  
  98.     flags = fcntl(sfd, F_GETFL, 0);  
  99.     fcntl(sfd, F_SETFL, flags | O_NONBLOCK);  
  100.   
  101.     return sfd;  
  102. }  


        上面代码的流程还是蛮清晰的。就是根据用户的IP和端口号建立一个socket,bind、listen监听客户端的到来。因为主线程申请的socketfd已经设置为非阻塞的,所以listen函数会立刻返回。在main函数中,主线程最终将调用event_base_loop函数进入事件监听循环,处理客户端的连接请求。

 

连接管理者conn:

 

        现在我们来关注一下conn_new函数。因为在这里函数里面会创建一个用于监听socket fd的event,并调用event_add加入到主线程的event_base中。从conn_new的函数名来看,是new一个conn。确实如何。事实上memcached为每一个socket fd(也就是一个连接)都创建一个conn结构体,用于管理这个socket fd(连接)。因为一个连接会有很多数据和状态信息,所以需要一个结构体来负责管理。所以阅读conn_new函数之前,还需要先阅读一下conn_init函数,了解conn结构体的一些初试化。

        在《命令行参数详解》中有提到,可以在启动memcached的时候通过命令行参数-c num指定memcached允许的最大同时在线客户端数量。即使没有使用该参数,memcached也会采用默认值的,具体的默认值可以参数《关键配置的默认值》。也就是说在启动memcached之后就可以确定最多允许多少个客户端同时在线。有了这个数值就不用一有新连接就malloc一个conn结构体(这样会很容易造成内存碎片)。有了这个数值那么可以在一开始(conn_init函数),就申请动态申请一个数组。有新连接就从这个数组中分配一个元素即可。

[cpp]  view plain copy  
 
  1. conn **conns;  
  2. static void conn_init(void) {  
  3.     /* We're unlikely to see an FD much higher than maxconns. */  
  4.     //已经dup返回当前未使用的最小正整数,所以next_fd等于此刻已经消耗了的fd个数  
  5.     int next_fd = dup(1);//获取当前已经使用的fd的个数  
  6.     //预留一些文件描述符。也就是多申请一些conn结构体。以免有别的需要把文件描述符  
  7.     //给占了。导致socket fd的值大于这个数组长度  
  8.     int headroom = 10;//预留一些文件描述符  /* account for extra unexpected open FDs */  
  9.     struct rlimit rl;  
  10.   
  11.     //settings.maxconns的默认值是1024.  
  12.     max_fds = settings.maxconns + headroom + next_fd;  
  13.   
  14.     /* But if possible, get the actual highest FD we can possibly ever see. */  
  15.     if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {  
  16.         max_fds = rl.rlim_max;  
  17.     } else {  
  18.         fprintf(stderr, "Failed to query maximum file descriptor; "  
  19.                         "falling back to maxconns\n");  
  20.     }  
  21.   
  22.     close(next_fd);//next_fd只是用来计数的,并没有其他用途  
  23.   
  24.     //注意,申请的conn结构体数量是比settings.maxconns这个客户端同时在线数  
  25.     //还要大的。因为memcached是直接用socket fd的值作为数组下标的。也正是  
  26.     //这个原因,前面需要使用headroom预留一些空间给突发情况  
  27.     if ((conns = calloc(max_fds, sizeof(conn *))) == NULL) {//注意是conn指针不是conn结构体  
  28.         fprintf(stderr, "Failed to allocate connection structures\n");  
  29.         /* This is unrecoverable so bail out early. */  
  30.         exit(1);  
  31.     }  
  32. }  

 

        上面代码中,calloc申请的是conn*指针数组而不是conn结构体数组。主要是因为conn结构体是比较大的一个结构体(成员变量很多)。不一定会存在settings.maxconns个同时在线的客户端。所以可以等到需要conn结构体的时候再去动态申请。需要时去动态申请,这样会有内存碎片啊!非也!!因为可以循环使用的。如果没有这个conn*指针数组,那么当这个连接断开后就要free这个conn结构体所占的内存(不然就内存泄漏了)。有了这个数组那么就可以不free,由数组管理这个内存。下面的conn_new函数展示了这一点。

[cpp]  view plain copy  
 
  1. //为sfd分配一个conn结构体,并且为这个sfd建立一个event,然后让base监听这个event  
  2. conn *conn_new(const int sfd, enum conn_states init_state,//init_state值为conn_listening  
  3.                 const int event_flags,  
  4.                 const int read_buffer_size, enum network_transport transport,  
  5.                 struct event_base *base) {  
  6.     conn *c;  
  7.   
  8.     assert(sfd >= 0 && sfd < max_fds);  
  9.     c = conns[sfd];//直接使用下标  
  10.   
  11.     if (NULL == c) {//之前没有哪个连接用过这个sfd值,需要申请一个conn结构体  
  12.         if (!(c = (conn *)calloc(1, sizeof(conn)))) {  
  13.             fprintf(stderr, "Failed to allocate connection object\n");  
  14.             return NULL;  
  15.         }  
  16.       
  17.         ...//初始化一些成员变量  
  18.   
  19.         c->sfd = sfd;  
  20.         conns[sfd] = c; //将这个结构体交由conns数组管理  
  21.     }  
  22.   
  23.     ...//初始化另外一些成员变量  
  24.     c->state = init_state;//值为conn_listening  
  25.   
  26.     //等同于event_assign,会自动关联current_base。event的回调函数是event_handler  
  27.     event_set(&c->event, sfd, event_flags, event_handler, (void *)c);  
  28.     event_base_set(base, &c->event);  
  29.     c->ev_flags = event_flags;  
  30.   
  31.     if (event_add(&c->event, 0) == -1) {  
  32.         perror("event_add");  
  33.         return NULL;  
  34.     }  
  35.   
  36.     return c;  
  37. }  



 

        综合上面的代码可以看到,主线程的基础实施也已经搭好了。注意,主线程对于socket fd 可读事件的回调函数是event_handler,回调参数是conn这个结构体指针。

 

 

牛刀小试:

 

        主线程和worker线程的基础设施都已经搭建好了,现在来尝试一下accept一个客户端。在跑一遍整个流程之前,先回忆一下回调函数。worker线程对于管道可读事件的回调函数是ethread_libevent_process函数。主线程对于socket fd可读事件的回调函数是event_handler函数。conn结构体成员state的值为conn_listening。现在走起!!直奔event_handler函数。

[cpp]  view plain copy  
 
  1. void event_handler(const int fd, const short which, void *arg) {  
  2.     conn *c;  
  3.   
  4.     c = (conn *)arg;  
  5.     assert(c != NULL);  
  6.   
  7.     c->which = which;  
  8.     if (fd != c->sfd) {  
  9.         conn_close(c);  
  10.         return;  
  11.     }  
  12.   
  13.     drive_machine(c);  
  14.     return;  
  15. }  

 

        太简单了吧,有没有搞错。event_handler函数确实简单,但其调用的drive_machine函数就确实很复杂。drive_machine函数内部是一个有限状态机。本文已经很长了,所以不会详解讲解有限状态机。下面只挑出处理新连接的那部分讲解。

[cpp]  view plain copy  
 
  1. static void drive_machine(conn *c) {  
  2.     bool stop = false;  
  3.     int sfd;  
  4.     socklen_t addrlen;  
  5.     struct sockaddr_storage addr;  
  6.     int res;  
  7.     const char *str;  
  8.   
  9.     assert(c != NULL);  
  10.   
  11.     //drive_machine被调用会进行状态判断,并进行一些处理。但也可能发生状态的转换  
  12.     //此时就需要一个循环,当进行状态转换时,也能处理  
  13.     while (!stop) {  
  14.   
  15.         switch(c->state) {  
  16.         case conn_listening:  
  17.             addrlen = sizeof(addr);  
  18.   
  19.             sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen);  
  20.   
  21.             ...  
  22.   
  23.             //选定一个worker线程,new一个CQ_ITEM,把这个CQ_ITEM仍给这个线程.  
  24.             dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST,  
  25.                                  DATA_BUFFER_SIZE, tcp_transport);  
  26.   
  27.             stop = true;  
  28.             break;  
  29.   
  30.             ...  
  31.         }  
  32.     }  
  33.   
  34.     return;  
  35. }  
  36.   
  37.   
  38.   
  39. static int last_thread = -1;  
  40.   
  41. //参数 sfd, conn_new_cmd, EV_READ | EV_PERSIST, DATA_BUFFER_SIZE, tcp_transport  
  42. void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags,  
  43.                        int read_buffer_size, enum network_transport transport) {  
  44.     CQ_ITEM *item = cqi_new();//申请一个CQ_ITEM  
  45.   
  46.     char buf[1];  
  47.   
  48.     int tid = (last_thread + 1) % settings.num_threads;//轮询的方式选定一个worker线程  
  49.   
  50.     LIBEVENT_THREAD *thread = threads + tid;  
  51.   
  52.     last_thread = tid;  
  53.   
  54.     item->sfd = sfd;  
  55.     item->init_state = init_state;//conn_new_cmd  
  56.     item->event_flags = event_flags;//EV_READ | EV_PERSIST  
  57.     item->read_buffer_size = read_buffer_size;//DATA_BUFFER_SIZE(2048)  
  58.     item->transport = transport;  
  59.   
  60.     cq_push(thread->new_conn_queue, item);//把这个item放到选定的worker线程的CQ队列中  
  61.   
  62.     buf[0] = 'c';  
  63.     if (write(thread->notify_send_fd, buf, 1) != 1) {//通知worker线程,有新客户端连接到来  
  64.         perror("Writing to thread notify pipe");  
  65.     }  
  66. }  

 

 

        现在主线程已经通知了选定的worker线程。接下来就是worker线程怎么处理这个通知了。下面看一下worker线程的管道可读事件回调函数thread_libevent_process。

[cpp]  view plain copy  
 
  1. static void thread_libevent_process(int fd, short which, void *arg) {  
  2.     LIBEVENT_THREAD *me = arg;  
  3.     CQ_ITEM *item;  
  4.     char buf[1];  
  5.   
  6.     read(fd, buf, 1);  
  7.   
  8.     switch (buf[0]) {  
  9.     case 'c':  
  10.         //从CQ队列中读取一个item,因为是pop所以读取后,CQ队列会把这个item从队列中删除  
  11.         item = cq_pop(me->new_conn_queue);  
  12.   
  13.         if (NULL != item) {  
  14.             //为sfd分配一个conn结构体,并且为这个sfd建立一个event,然后让base监听这个event  
  15.             //这个sfd的事件回调函数是event_handler  
  16.             conn *c = conn_new(item->sfd, item->init_state, item->event_flags,  
  17.                                item->read_buffer_size, item->transport, me->base);  
  18.   
  19.             c->thread = me;  
  20.   
  21.             cqi_free(item);  
  22.         }  
  23.         break;  
  24.   
  25.     }  
  26. }  

 

        正如前面所说的,memcached为每一个连接申请一个conn结构体进行维护。conn_new函数内部会为这个socket fd申请一个event并添加到该worker线程的event_base里面。当客户端发送命令时,worker线程就能监听到。这个conn_new函数前面已经说过了,这里也就不给出代码了。

 

 

        在以后,都是worker线程负责这里这个客户端的一切通信,也是worker线程负责完成客户端的命令,包括申请内存存储数据、查询数据、删掉数据。这些苦工都是worker线程完成的,而没有其它线程帮忙。不过大可放心,memcached对于这命令一般都能在常数时间时间复杂度内完成。所以,即使一个worker线程有多个客户端连接,也完全应付得过来。