希望写完这一系列文章,小程们可以对照memcache的源码全部看明白。第一篇文章先将memcache的网络接入,写的很是精彩。
memcache的接入层是依赖与libevent框架的,由其帮助管理I/O多路复用。不过,这里不对libevent做过多的介绍,以后我会陆续写关于libevent的文章,本文重点关注memcache接入层的编程艺术。姑且当libevent是个黑盒子吧。
memcache的接入是由一个主线程和一组worker线程(至少一个线程)协同完成,是一个Master-worker模式。Master负责接入请求,然后指定一个Worker去处理。Worker线程由一个简单的hash算法管理,基本可以理解为循环挨着取线程处理请求,简单的图示:
Master通知Worker的方式有点好玩,是通过管道来通知的。这样对于编码来说方便了很多,因为很多细节都交给内核处理,逻辑不用关心太多。
好了,现在梳理一下memcache的各个接入对象。
1,线程对象,pthread类型的线程被memcache封装成一个结构,与libevent有机的结合起来。
typedef struct {
pthread_t thread_id; /* unique ID of this thread */
struct event_base *base; /* libevent handle this thread uses */
struct event notify_event; /* listen event for notify pipe */
int notify_receive_fd; /* receiving end of notify pipe */
int notify_send_fd; /* sending end of notify pipe */
struct thread_stats stats; /* Stats generated by this thread */
struct conn_queue *; /* queue of new connections to handle */
cache_t *suffix_cache; /* suffix cache */
} LIBEVENT_THREAD;
详细讲一个其中的各个元素,对后续的逻辑的理解很有帮助。
thread_id,这个很容易了,线程的唯一ID
base,这是libevent的event_base对象,因为libevent并不是线程安全的,所以每个线程需要维护自己的event,并不能多个线程共用一个。
notify_event,上面我们提到,Mater通知Worker是通过管道通知的,Worker接受时间就是这个了
nofify_receive_fd,管道的接收端,创建后由上面的base来进行管理
notify_send_fd,管道的发送端,会由Master往里写数据
stats,统计用的,主要功能以后再说
new_conn_queue,Master收到请求后,会把请求打包后挂到一个想通知的Worker负责的列表里面,这就是那个列表指针
Master和Worker的初始化在thread_init里面完成。首先是Master,着重说明的是它的event_base就是全局变量main_base。
Worker会执行以下几步:
1)创建一个pipe,两端赋给自己的那俩变量。
2)初始化自己的event_base。
3)将notify_event,receive_fd,和回调函数thread_libevent_process关联起来,当Master通知过来时,这个Worker能够收到数据
4)初始化列表指针
5)初始化cash指针
2,连接对象
memcache将fd封装成一个连接对象,conn
这个对象里面的数据比较多,挑几个重点的说一下。
int sfd;,连接的fd
conn_states state; 描述conn状态的,所有状态的定义都在conn_states 里面
struct event event;
short ev_flags;
short which; /** which events were just triggered */
LIBEVENT_THREAD *thread; 处理它的那个Worker线程指针
3,辅助连接对象
上面提到过,Master接收到请求后,会把这个请求打包挂到某一个Worker下面的new_conn_queue下面,这个下面挂的对象是conn_queue_item。
里面的变量不具体介绍了,只保留了conn对象的一部分与这个请求相关的一些数据。
现在,结合代码,说一下整个的接入流程(略去了很多写server的通用代码,比如接受参数,守护进程运行,更改limit参数等)。
1)首先,Master初始化自己的event_base
L:4588:
/* initialize main thread libevent instance */
main_base = event_init();
2,连接对象(conn)初始化 ,预先分配200个连接对象,即同时可以接受并处理200个客户端的请求
L:4594:
conn_init();
3,Master和Worker线程的初始化
L:4606
/* start up worker threads if MT mode */
thread_init(settings.num_threads, main_base);
这个函数可以好好看看,里面的具体功能在上面已经讲过了。
4,如果是TCP模式就需要穿件套接字,并监听了。
L4665:
if (settings.udpport && server_socket(settings.udpport, udp_transport, portnumber_file))
这个函数是主要的接入函数,我会详细介绍它。
创建完套接字后会进入conn_new ,这个函数是接入层相当重要的一个函数,任何一个新的连接都会由这个函数来处理。它会把刚刚创建的套接字由传进来的event_base来管理。以后Worker收到
套接字会与event_handler绑定,当有时间发生时(这里是可读事件),会进入drive_machine函数,也就是memcache的状态机,是接入层与逻辑层的纽带,以后我会专门有文章来讲它,这里只介绍接受请求的逻辑。
Master线程监听了套接字后,当有请求过来时,就能捕获到了,经event_handler->drive_machine->dispatch_conn_new的路径。在dispatch_conn_new这个函数里面会看到主线程会创建一个辅助连接对象conn_queue_item,并把它挂到某个Worker下面。然后往管道写入一个字符,通知管道的另一端Worker,有请求来了。
Worker的event_base会收到请求,进入回调函数thread_libevent_process,把链表中的对象取出来,组成一个新的连接对象conn,这是通过conn_new来处理的,上面Master处理监听套接字时用的就是它,既然这样,我就好好的介绍一下这个函数。
首先,上面我们提到过,连接对象conn初始化时预先分配了200个对象空间,不过有请求来了之后竟然不从这里取用,这是最让我糊涂的地方,那初始化用来干什么呢?所以第一个请求需要重新申请空间。应用到代码中就是先执行
conn *c = conn_from_freelist();
会返回NULL,然后申请空间:
c = (conn *)calloc(1, sizeof(conn)))。
另外,conn对象有一个event变量,上面我们提到过。用event_set初始化,与从链表中取出的fd,和回调函数event_handler绑定。
L:422, event_set(&c->event, sfd, event_flags, event_handler, (void *)c);
L:423, event_base_set(base, &c->event);与该线程的event对象绑定,此后,这个fd就由这个线程的libevent来管理了。
当有请求来时,进入event_handler,进而进入状态机。