memcached源码分析三-libevent与命令解析
转载请注明出处https://www.cnblogs.com/yang-zd/p/11352833.html,谢谢合作!
前面已经分析了memcached中的slabs内存管理及缓存对象如何利用item表达并存储在slabs管理的空间中,并分析了如何实现LRU策略实现缓存对象的释放。但memcached是一个client-server结构的缓存系统,服务端需要接收客户端的指令,然后在服务端做相应的操作,比如新建缓存对象,读取缓存对象。memcached使用libevent实现这些网络通信操作,接下来将会分析memcached如何使用libevent处理网络事件。
1. threads in memcached
通常的网络编程模型中,服务端都会有一个处于listening状态的socket,当客户端尝试与服务端建立连接时,服务端会建立一个新的socket与该客户端通信,并将该通信任务交给其它线程完成。事实上,memcached也采用了这样的模型,但它通过libevent实现这个模型。
简单地说libevent的工作方式就是:一个event_base,一些加入event_base中的events,每一个event都有一个回调函数,event_bse不停地循环等待这些events发生,事件发生后调用对应event的回调函数。
memcached中有2类event_base:main_base,位于主线程中,处理listening socket;normal event_base,位于工作线程中,处理accepted socket。它们的关系可以简单地以图1-1表示,

图1-1 memcached中的工作线程模型
(ps: 工作线程的数量可以在启动memcached时设置)
memcached使用struct LIBEVENT_THREAD结构保存工作线程相关的信息,其定义如下,
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 *new_conn_queue; /* queue of new connections to handle */
cache_t *suffix_cache; /* suffix cache */
#ifdef EXTSTORE
cache_t *io_cache; /* IO objects */
void *storage; /* data object for storage system */
#endif
logger *l; /* logger buffer */
void *lru_bump_buf; /* async LRU bump buffer */
#ifdef TLS
char *ssl_wbuf;
#endif
} LIBEVENT_THREAD;
thread_id它保存了线程的线程id,base,保存了属于该线程的event_base结构。
notify_event, notify_received_fd, notify_send_fd三个成员一起用于完成唤醒线程的任务,notify_event被加入到它的event_base中,监听nofity_received_fd上的读事件,当有读事件发生时,唤醒线程。外部想要唤醒该线程时,向notify_send_fd发送数据,即可引发nofity_received_fd上的读事件。这两个fd的关联通过pipe通道完成。
另外有一个new_conn_queue成员,这个成员用来协助完成主线程的任务分发工作。当主线程接受新的连接后,建立一个struct conn_queue_item结构,并将该结构push到某个工作线程的new_conn_queue指向的队列上。worker thread被唤醒后从该队列上pop新的连接建立新的事件加入它的event_base,开始监听它的网络通信。以下是new_conn_queue的及其元素CQ_ITEM的定义
/* A connection queue. */
typedef struct conn_queue CQ;
struct conn_queue {
CQ_ITEM *head;
CQ_ITEM *tail;
pthread_mutex_t lock;
}; typedef struct conn_queue_item CQ_ITEM;
struct conn_queue_item {
int sfd;
enum conn_states init_state;
int event_flags;
int read_buffer_size;
enum network_transport transport;
enum conn_queue_item_modes mode;
conn *c;
void *ssl;
CQ_ITEM *next;
};
CQ_ITEM中sfd包含了相应的socket,transport表示传输层协议类型,read_buffer_size代表读数据的buffer初始大小,init_state表示这个conn的初始状态,用于struct conn结构,conn结构是memcached中用于读写数据的一个关键数据结构,后面将进行介绍。相关的队列操作函数是cq_pop与cq_push。
无论是listening socekt的事件回调函数还是accepted socket的事件回调函数,在memcached中都是event_handler,该函数会调用一个相当复杂的函数drive_mathine,它会根据状态进行不同的处理。如main thread中的listening socket有连接事件到来时,回调函数将调用accept函数接受连接请求,并将新建立的socket交给某个worker thread处理。worker thread中的socket的事件发生时,处理工作相对较复杂,包括接收数据,建立item,并进行存储,或者返回响应信息等。
2. drive_machine
首先,为了服务端与客户端的沟通,memcached定义了它的信息的格式协议。协议有两类,一类是text protocol,一类是binary protocol,具体可参考https://github.com/memcached/memcached/wiki/Protocols。事件回调函数中根据协议对消息进行解析,并进行响应。
前面提到,memcached中所有的libevent网络事件的回调函数都是event_handler,而这个函数又简单地调用了drive_machine完成工作,drive_mathine根据conn_states值做不同的操作。每一个连接在memcached中都使用一个conn结构进行描述,其中包含了它的socket, event及读写数据使用到的相关buffer描述,而conn_states正是描述struct conn目前所处的状态的成员,其取值范围如下,
/**
* Possible states of a connection.
*/
enum conn_states {
conn_listening, /**< the socket which listens for connections */
conn_new_cmd, /**< Prepare connection for next command */
conn_waiting, /**< waiting for a readable socket */
conn_read, /**< reading in a command line */
conn_parse_cmd, /**< try to parse a command from the input buffer */
conn_write, /**< writing out a simple response */
conn_nread, /**< reading in a fixed number of bytes */
conn_swallow, /**< swallowing unnecessary bytes w/o storing */
conn_closing, /**< closing this connection */
conn_mwrite, /**< writing out many items sequentially */
conn_closed, /**< connection is closed */
conn_watch, /**< held by the logger thread as a watcher */
conn_max_state /**< Max state value (used for assertion) */
};
conn_listening状态下drive_machine会从该listening socket上接受客户端连接,建立新的连接,分发给工作线程。
具体的状态转移及所做的相关操作见图2-1,由于其中使用了许多struct conn的成员,在此之前先前conn的定义列出:
/**
* The structure representing a connection into memcached.
*/
struct conn {
int sfd;
#ifdef TLS
SSL *ssl;
char *ssl_wbuf;
bool ssl_enabled;
#endif
sasl_conn_t *sasl_conn;
bool sasl_started;
bool authenticated;
enum conn_states state;
enum bin_substates substate;
rel_time_t last_cmd_time;
struct event event;
short ev_flags;
short which; /** which events were just triggered */ char *rbuf; /** buffer to read commands into */
char *rcurr; /** but if we parsed some already, this is where we stopped */
int rsize; /** total allocated size of rbuf */
int rbytes; /** how much data, starting from rcur, do we have unparsed */ char *wbuf;
char *wcurr;
int wsize;
int wbytes;
/** which state to go into after finishing current write */
enum conn_states write_and_go;
void *write_and_free; /** free this memory after finishing writing */ char *ritem; /** when we read in an item's value, it goes here */
int rlbytes; /* data for the nread state */ /**
* item is used to hold an item structure created after reading the command
* line of set/add/replace commands, but before we finished reading the actual
* data. The data is read into ITEM_data(item) to avoid extra copying.
*/ void *item; /* for commands set/add/replace */ /* data for the swallow state */
int sbytes; /* how many bytes to swallow */ /* data for the mwrite state */
struct iovec *iov;
int iovsize; /* number of elements allocated in iov[] */
int iovused; /* number of elements used in iov[] */ struct msghdr *msglist;
int msgsize; /* number of elements allocated in msglist[] */
int msgused; /* number of elements used in msglist[] */
int msgcurr; /* element in msglist[] being transmitted now */
int msgbytes; /* number of bytes in current msg */ item **ilist; /* list of items to write out */
int isize;
item **icurr;
int ileft; char **suffixlist;
int suffixsize;
char **suffixcurr;
int suffixleft;
#ifdef EXTSTORE
int io_wrapleft;
unsigned int recache_counter;
io_wrap *io_wraplist; /* linked list of io_wraps */
bool io_queued; /* FIXME: debugging flag */
#endif
enum protocol protocol; /* which protocol this connection speaks */
enum network_transport transport; /* what transport is used by this connection */ /* data for UDP clients */
int request_id; /* Incoming UDP request ID, if this is a UDP "connection" */
struct sockaddr_in6 request_addr; /* udp: Who sent the most recent request */
socklen_t request_addr_size;
unsigned char *hdrbuf; /* udp packet headers */
int hdrsize; /* number of headers' worth of space is allocated */ bool noreply; /* True if the reply should not be sent. */
/* current stats command */
struct {
char *buffer;
size_t size;
size_t offset;
} stats; /* Binary protocol stuff */
/* This is where the binary header goes */
protocol_binary_request_header binary_header;
uint64_t cas; /* the cas to return */
short cmd; /* current command being processed */
int opaque;
int keylen;
conn *next; /* Used for generating a list of conn structures */
LIBEVENT_THREAD *thread; /* Pointer to the thread object serving this connection */
int (*try_read_command)(conn *c); /* pointer for top level input parser */
ssize_t (*read)(conn *c, void *buf, size_t count);
ssize_t (*sendmsg)(conn *c, struct msghdr *msg, int flags);
ssize_t (*write)(conn *c, void *buf, size_t count);
};

图2-1 struct conn的状态转移图
现在结合struct conn定义与状态转移图,对一个简单的客户端命令开始到客户端收到返回消息的过程进行描述,
- 首先,main_base中的listening socket对应的连接的conn_states为conn_listening.
- 当新的连接到达后,drive_machine调用accept函数接受新的连接,它(conns[id])被分发给工作线程,并处于conn_new_cmd的状态
- 处于conn_new_cmd状态的conns[id]检查它的成员rbytes,查看缓存rbuf中是否有数据,无数据则进入conn_waiting状态.
- 处于conn_waiting状态的conns[id]的成员event在event_base中等待触发,令conns[id]的下一个状态为conn_read
- conns[id]的读事件触发,处于conn_read状态的它从网络上读数据存储conns[id].rbuf中,并进入conn_parse_cmd状态
- 处于conn_parse_cmd状态的conns[id]从conns[id].buf中解析命令头,以binary protocol为例,解析出来的头部信息放入conns[id]. binary_header中,包含了key的长度,extended信息的长度,body的长度,命令等关键信息,这些工作调用conns. try_read_command函数完成。
- try_read_command根据解析的命令做不同的处理:信息已经足够,不需要读取更多信息,直接处理然后返回结果,如查询版本的命令、noop命令等,处理后进入conn_mwrite状态返回结果,然后再次进入conn_new_cmd状态;命令还需要读取更多信息,那么首先读取extras部分的信息,长度由conns[id].binary_header. extlen与conns[id].binary_header. keylen决定,这个长度信息放入conns[id].rlbytes中,存放数据的buf起始点记录在conns[id].ritem中,进入conn_nread状态(调用bin_read_key函数完成)。
- 处于conn_nread状态的conns[id],或者它的rbuf中已经有需要的rlbytes数据,或者仍然需要从网络上读取数据放入ritem起始的位置(此时ritem指向rbuf可用空间),数据足够后调用complete_nread函数进行处理。
- 如果前一次conn_nread读取的是extras部分的信息,complete_nread会根据命令类型进行处理:不需要数据信息,如delete命令、touch命令,处理后进入conn_mwrite状态返回结果,然后进入conn_new_cmd状态;需要继续读取数据信息,如add、set命令,首先它会根据binary_head中的body长度计算出需要的存储空间,预分配item进行存储,即conns[id].item,令conns[id].ritem指向conns[id].item的数据区,conns[id].rlbytes记录需要读取的数据长度,再次进入conn_nread状态。
- 再次进入conn_nread状态的conns[id]读取完数据后,调用complete_nread进行处理,将item加入到hashtable与lru的链表上,完成存储。进入conn_new_cmd状态。
conn_mwrite状态下,drive_machine会向连接的客户端发送消息(调用transmit函数),如果顺利完成则进入下一次的conn_new_cmd状态。
conn_write状态表示要发送的数据在wbuf中,需要先加入到msglist中,再进入conn_mwrite状态。
如果处理过程中出现了错误,导致连接发生混乱或者连接中断了,那么进入conn_closing,关闭连接。
memcached中向客户端返回消息使用了库函数sendmsg,因此需要返回给客户端的消息都存储在结构struct conn中的msglist中,其存储结构可以大致表示如图2-2:

图2-2 struct conn结构中msglist的结构
msglist是一个动态扩展的msghdr数组, msgused是数组中已被使用的部分的计数, msgsize是数组长度,msgcurr是目前发送的消息。
iov也是一个动态扩展的iovec数组,iovsize表示数据长度,iovused表示数组中已使用的部分的计数。
buf是存储需要发送的数据的空间,它的起始地址与长度记录在一个iovec结构中,这些buf一般是con中的wbuf, write_and_free,ilist中的item的数据区。
struct conn结构中有一个substate成员对于complete_nread处理binary protocol很重要,complete根据该值决定后续需要做的处理。substate的取值范围定义如下:
enum bin_substates {
bin_no_state,
bin_reading_set_header,
bin_reading_cas_header,
bin_read_set_value,
bin_reading_get_key,
bin_reading_stat,
bin_reading_del_header,
bin_reading_incr_header,
bin_read_flush_exptime,
bin_reading_sasl_auth,
bin_reading_sasl_auth_data,
bin_reading_touch_key,
};
这些状态是与binary protocol的命令对应的,如
set命令,substate首先会设置为bin_reading_set_header状态,表示conns[id]在第一次conn_nread状态读取了它需要的额外信息,但它还有data需要读取,需要预分配conns[id].item,后续的conn_nread状态读数据直接读入item中。然后它会设置为bin_read_set_value状态,表示第二次conn_nread读取了data数据并已存入item中,需要将该item放入hashtable与lru链表中。
delete命令,substate会设置为bin_reading_del_header状态,它仅需要一次conn_nread状态读取所需要的额外信息,然后从hashtable与lru链表中删除item,返回删除结果。
struct conn中部分其它成员的作用:
ilist是一个指向item的指针的链表,存储的gets命令需要返回的item的指针,它也是一个动态扩展的数据,因为text协议的gets命令可能请求多个缓存对象。
protocol代表连接使用的协议类型,text或者binary
transport代表UDP或者TCP传输层协议
thread存储这个连接归属的工作线程的指针
suffixlist是一个char指针的动态扩展数组,存储着客户端的flags的字符串形式。
3.部分源码函数功能说明
void event_handler(const int fd, const short which, void *arg)
网络事件的回调函数,事实上drive_machine才是核心函数,event_handler简单地调用了drive_machine。
static void drive_machine(conn *c)
根据c->state的值进行不同的处理。
void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags,
int read_buffer_size, enum network_transport transport, void *ssl)
conn_listening状态的conn在接受到新的连接后,调用该函数构造一个conn结构,并将其分发给工作线程处理。
static enum try_read_result try_read_network(conn *c)
conn_read状态的连接c调用该函数从网络上读取数据。
static int try_read_command_binary(conn *c)
在将数据读到c.rbuf后,调用该函数对binary protocol的命令进行解析,解析结果存储c.binary_head中。
static void dispatch_bin_command(conn *c)
在try_read_command_binary中调用,根据命令执行不同的操作:命令不需要额外数据了,处理并返回结果;命令仍需要数据,调用bin_read_key设置好c.ritem、c.rlbytes及c.substate,函数返回,进入conn_nread状态。
static void complete_nread_binary(conn *c)
binaray命令需要的extras数据已经放到了缓冲区,根据c->substate决定完成命令并返回或者继续读data部分。
static int try_read_command_ascii(conn *c)
text协议使用的解析命令的函数,实际上调用process_command完成工作。
static void process_command(conn *c, char *command)
根据命令行中的命令关键字进行相应的处理,如get命令调用process_get_command;add/set命令调用process_update_command;incr/decr调用process_arithmetic_command…
static enum transmit_result transmit(conn *c)
将连接的c->msglist中的消息发送回客户端。核心操作为库函数sendmsg。
memcached源码分析三-libevent与命令解析的更多相关文章
- Memcached源码分析之从SET命令开始说起
作者:Calix 如果直接把memcached的源码从main函数开始说,恐怕会有点头大,所以这里以一句经典的“SET”命令简单地开个头,算是回忆一下memcached的作用,后面的结构篇中关于命令解 ...
- Memcached源码分析
作者:Calix,转载请注明出处:http://calixwu.com 最近研究了一下memcached的源码,在这里系统总结了一下笔记和理解,写了几 篇源码分析和大家分享,整个系列分为“结构篇”和“ ...
- Memcached源码分析之请求处理(状态机)
作者:Calix 一)上文 在上一篇线程模型的分析中,我们知道,worker线程和主线程都调用了同一个函数,conn_new进行事件监听,并返回conn结构体对象.最终有事件到达时,调用同一个函数ev ...
- Memcached源码分析之内存管理
先再说明一下,我本次分析的memcached版本是1.4.20,有些旧的版本关于内存管理的机制和数据结构与1.4.20有一定的差异(本文中会提到). 一)模型分析在开始解剖memcached关于内存管 ...
- Memcached源码分析之线程模型
作者:Calix 一)模型分析 memcached到底是如何处理我们的网络连接的? memcached通过epoll(使用libevent,下面具体再讲)实现异步的服务器,但仍然使用多线程,主要有两种 ...
- memcached源码分析-----item过期失效处理以及LRU爬虫
memcached源码分析-----item过期失效处理以及LRU爬虫,memcached-----item 转载请注明出处:http://blog.csdn.net/luotuo44/article ...
- tomcat源码分析(三)一次http请求的旅行-从Socket说起
p { margin-bottom: 0.25cm; line-height: 120% } tomcat源码分析(三)一次http请求的旅行 在http请求旅行之前,我们先来准备下我们所需要的工具. ...
- 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入
使用react全家桶制作博客后台管理系统 前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...
- spring IoC源码分析 (3)Resource解析
引自 spring IoC源码分析 (3)Resource解析 定义好了Resource之后,看到XmlFactoryBean的构造函数 public XmlBeanFactory(Resource ...
随机推荐
- Python 11 提取括号中间的内容
原文:https://blog.csdn.net/your_answer/article/details/80456550 import re string = 'abe(ac)ad)' p1 = r ...
- SQLSERVER获取数据库中的所有表的名称、表中所有字段的属性
1.查询数据库中的所有数据库名: SELECT Name FROM Master..SysDatabases ORDER BY Name 2.查询某个数据库中所有的表名: SELECT Name FR ...
- ffmpeg结合SDL编写播放器(三)
接下来是解析影片的帧 /*** project.c ***/ #include<stdio.h> #include<libavcodec/avcodec.h> #include ...
- MySQL数据库中文乱码问题
mysql> select * from books; +-----+---------------------------------+---------+-------------+---- ...
- js浮点数精度丢失问题及如何解决js中浮点数计算不精准
js中进行数字计算时候,会出现精度误差的问题.先来看一个实例: console.log(0.1+0.2===0.3);//false console.log(0.1+0.1===0.2);//true ...
- FCN内容通读
本文完全为个人心得体会,只做记录用,欢迎交流 替换全连接层为卷积层 以alexnet为例,替换了最后三层fc为卷积层,得到的是通道数很大(4096)而长宽很小的输出,其实我不太能理解这里的创新点,或许 ...
- PHP系列 | Thinkphp3.2 上传七牛 bad token 问题 [ layui.upload 图片/文件上传]
前端代码 <div class="logo_out" id="upload-logo"></div> JS代码 /** * 上传图片 * ...
- IDEA的SonarLint插件报错Unable to create symbol table for
执行sonarLint 报错: Unable to create symbol table for ***File won't be refreshed because there were erro ...
- 用openssl 生成证书的过程
1. 安装 openssl 后可以执行如下命令来生成私钥和对应的证书请求文件 ca openssl req -new -keyout private.key -out for_request.csr ...
- 015 vue的项目
一:搭建项目 1.框架 在原有的基础上 src: 2.index.html <!DOCTYPE html> <html lang="en"> <hea ...