accept/dispatch:
memcached使用"主线程统一accept/dispatch子线程"网络模型处理客户端的连接和通信,也就是《UNIX网络编程 卷1 第三版》第30章里面的第8个模型。
"主线程统一accept/dispatch子线程"的基础设施:主线程创建多个子线程(这些子线程也称为worker线程),每一个线程都维持自己的事件循环,即每个线程都有自己的epoll,并且都会调用epoll_wait函数进入事件监听状态。每一个worker线程(子线程)和主线程之间都用一条管道相互通信。每一个子线程都监听自己对应那条管道的读端。当主线程想和某一个worker线程进行通信,直接往对应的那条管道写入数据即可。
"主线程统一accept/dispatch子线程"模型的工作流程:主线程负责监听进程对外的TCP监听端口。当客户端申请连接connect到进程的时候,主线程负责接收accept客户端的连接请求。然后主线程选择其中一个worker线程,把客户端fd通过对应的管道传给worker线程。worker线程得到客户端的fd后负责和这个客户端进行一切的通信。
"主线程统一accept/dispatch子线程"模型的工作示意图如下图所示:
memcached里面的"主线程统一accept/dispatch子线程"和上面所说的差不多,区别在于:1. memcached使用libevent作为进行事件监听;2.memcached往管道里面写的内容不是fd,而是一个简单的字符。每一个worker线程都维护一个CQ队列,主线程把fd和一些信息写入一个CQ_ITEM里面,然后主线程往worker线程的CQ队列里面push这个CQ_ITEM。接着主线程使用管道通知worker线程:“我已经发了一个新客户给你,你去处理吧”。
memcached的"主线程统一accept/dispatch子线程"如下面这幅经典的图所示:
memcached的具体实现:
上图看到每一个worker线程都有一个CQ队列,主线程accept到新客户端后,就把新客户端的信息封装成一个CQ_ITEM,然后push到选定线程的CQ队列中。
CQ队列:
现在我们来看一下CQ队列长什么样的。
- 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;
- CQ_ITEM *next;
- };
-
-
- typedef struct conn_queue CQ;
- struct conn_queue {
- CQ_ITEM *head;
- CQ_ITEM *tail;
- pthread_mutex_t lock;
- };
可以看到结构体conn_queue(即CQ队列结构体)有一个pthread_mutex_t类型变量lock,这说明主线程往某个worker线程的CQ队列里面push一个CQ_ITEM的时候必然要加锁的。下面是初始化CQ队列,以及push、pop一个CQ_ITEM的代码。
- static void cq_init(CQ *cq) {
- pthread_mutex_init(&cq->lock, NULL);
- cq->head = NULL;
- cq->tail = NULL;
- }
-
- static CQ_ITEM *cq_pop(CQ *cq) {
- CQ_ITEM *item;
-
- pthread_mutex_lock(&cq->lock);
- item = cq->head;
- if (NULL != item) {
- cq->head = item->next;
- if (NULL == cq->head)
- cq->tail = NULL;
- }
- pthread_mutex_unlock(&cq->lock);
-
- return item;
- }
-
-
-
-
- static void cq_push(CQ *cq, CQ_ITEM *item) {
- item->next = NULL;
-
- pthread_mutex_lock(&cq->lock);
- if (NULL == cq->tail)
- cq->head = item;
- else
- cq->tail->next = item;
- cq->tail = item;
- pthread_mutex_unlock(&cq->lock);
- }
注意
,cq_pop函数不同于STL里面的pop。cq_pop函数会返回一个CQ_ITEM。
由上面代码得到的CQ队列如下图所示:
为worker线程构建CQ队列:
主线程又是怎么访问各个worker线程的CQ队列呢?在C语言里面的答案当然是使用全局变量啦。memcached专门定义了结构体,如下:
- typedef struct {
- pthread_t thread_id;
- struct event_base *base;
- struct event notify_event;
- int notify_receive_fd;
- int notify_send_fd;
- struct conn_queue *new_conn_queue;
- ...
- } 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结构体指针。
- static LIBEVENT_THREAD *threads;
-
-
- void thread_init(int nthreads, struct event_base *main_base) {
- int i;
-
-
- pthread_mutex_init(&cqi_freelist_lock, NULL);
- cqi_freelist = NULL;
-
-
-
- threads = calloc(nthreads, sizeof(LIBEVENT_THREAD));
-
- for (i = 0; i < nthreads; i++) {
- int fds[2];
- if (pipe(fds)) {
- perror("Can't create notify pipe");
- exit(1);
- }
-
- threads[i].notify_receive_fd = fds[0];
- threads[i].notify_send_fd = fds[1];
-
-
-
- setup_thread(&threads[i]);
- }
-
-
- for (i = 0; i < nthreads; i++) {
-
- create_worker(worker_libevent, &threads[i]);
- }
-
- ...
- }
-
-
-
- static void setup_thread(LIBEVENT_THREAD *me) {
- me->base = event_init();
-
-
-
- event_set(&me->notify_event, me->notify_receive_fd,
- EV_READ | EV_PERSIST, thread_libevent_process, me);
- event_base_set(me->base, &me->notify_event);
-
- if (event_add(&me->notify_event, 0) == -1) {
- fprintf(stderr, "Can't monitor libevent notify pipe\n");
- exit(1);
- }
-
-
- me->new_conn_queue = malloc(sizeof(struct conn_queue));
-
- cq_init(me->new_conn_queue);
-
- ...
- }
-
-
- static void create_worker(void *(*func)(void *), void *arg) {
- pthread_t thread;
- pthread_attr_t attr;
- int ret;
-
- pthread_attr_init(&attr);
-
- if ((ret = pthread_create(&thread, &attr, func, arg)) != 0) {
- fprintf(stderr, "Can't create thread: %s\n",
- strerror(ret));
- exit(1);
- }
- }
CQ_ITEM内存池:
memcached在申请一个CQ_ITEM结构体时,并不是直接使用malloc申请的。因为这样做的话可能会导致大量的内存碎片(作为长期运行的服务器进程memcached需要考虑这个问题)。为此,memcached也为CQ_ITEM使用类似内存池的技术:预分配一块比较大的内存,将这块大内存切分成多个CQ_ITEM。下面是实现代码:
-
-
-
-
-
- static CQ_ITEM *cqi_new(void) {
-
- CQ_ITEM *item = NULL;
- pthread_mutex_lock(&cqi_freelist_lock);
- if (cqi_freelist) {
- item = cqi_freelist;
- cqi_freelist = item->next;
- }
- pthread_mutex_unlock(&cqi_freelist_lock);
-
- if (NULL == item) {
- int i;
-
- item = malloc(sizeof(CQ_ITEM) * ITEMS_PER_ALLOC);
-
-
-
- for (i = 2; i < ITEMS_PER_ALLOC; i++)
- item[i - 1].next = &item[i];
-
- pthread_mutex_lock(&cqi_freelist_lock);
-
-
-
- item[ITEMS_PER_ALLOC - 1].next = cqi_freelist;
- cqi_freelist = &item[1];
- pthread_mutex_unlock(&cqi_freelist_lock);
- }
-
- return item;
- }
-
-
-
- static void cqi_free(CQ_ITEM *item) {
- pthread_mutex_lock(&cqi_freelist_lock);
- item->next = cqi_freelist;
- cqi_freelist = item;
- pthread_mutex_unlock(&cqi_freelist_lock);
- }
主线程的工作:
前面展示了在"主线程统一accept/dispatch子线程"中worker线程是怎么构建基础设施的。接下来看看主线程为了构建基础需要完成哪些工作。首先我们来看一下main函数,该main函数已经被我删除得很精简了。
- int main (int argc, char **argv) {
-
-
- if (!sanitycheck()) {
- return EX_OSERR;
- }
-
-
- settings_init();
-
- ...
-
-
- main_base = event_init();
-
- conn_init();
-
-
-
- thread_init(settings.num_threads, main_base);
-
-
-
- clock_handler(0, 0, 0);
-
-
-
- if (settings.socketpath == NULL) {
- FILE *portnumber_file = NULL;
-
- if (settings.port && server_sockets(settings.port, tcp_transport,
- portnumber_file)) {
- vperror("failed to listen on TCP port %d", settings.port);
- exit(EX_OSERR);
- }
-
- ...
- }
-
-
-
- if (event_base_loop(main_base, 0) != 0) {
- retval = EXIT_FAILURE;
- }
-
- return retval;
- }
在main函数中,主线程创建了属于自己的event_base,存放在全局变量main_base中。在main函数的最后,主线程调用event_base_loop进入事件循环中。中间的server_sockets函数是创建一个监听客户端的socket,并将创建一个event监听该socket的可读事件。下面就看一下这个函数。为了简单起见下面的代码都忽略错误处理。
-
-
- static int server_sockets(int port, enum network_transport transport,
- FILE *portnumber_file) {
-
-
- char *b;
- int ret = 0;
-
- char *list = strdup(settings.inter);
-
-
- for (char *p = strtok_r(list, ";,", &b);
- p != NULL;
- p = strtok_r(NULL, ";,", &b)) {
- int the_port = port;
- char *s = strchr(p, ':');
-
-
- if (s != NULL) {
- *s = '\0';
- ++s;
- if (!safe_strtol(s, &the_port)) {
- return 1;
- }
- }
- if (strcmp(p, "*") == 0) {
- p = NULL;
- }
-
- ret |= server_socket(p, the_port, transport, portnumber_file);
- }
- free(list);
- return ret;
- }
-
-
- static conn *listen_conn = NULL;
-
-
-
- static int server_socket(const char *interface,
- int port,
- enum network_transport transport,
- FILE *portnumber_file) {
- int sfd;
- struct linger ling = {0, 0};
- struct addrinfo *ai;
- struct addrinfo *next;
- struct addrinfo hints = { .ai_flags = AI_PASSIVE,
- .ai_family = AF_UNSPEC };
- char port_buf[NI_MAXSERV];
- int success = 0;
- int flags =1;
-
- hints.ai_socktype = IS_UDP(transport) ? SOCK_DGRAM : SOCK_STREAM;
-
-
- snprintf(port_buf, sizeof(port_buf), "%d", port);
- getaddrinfo(interface, port_buf, &hints, &ai);
-
-
- for (next= ai; next; next= next->ai_next) {
- conn *listen_conn_add;
-
-
- sfd = new_socket(next);
- bind(sfd, next->ai_addr, next->ai_addrlen);
-
- success++;
- listen(sfd, settings.backlog);
-
-
- if (!(listen_conn_add = conn_new(sfd, conn_listening,
- EV_READ | EV_PERSIST, 1,
- transport, main_base))) {
- fprintf(stderr, "failed to create listening connection\n");
- exit(EXIT_FAILURE);
- }
-
-
- listen_conn_add->next = listen_conn;
- listen_conn = listen_conn_add;
-
- }
-
- freeaddrinfo(ai);
-
-
- return success == 0;
- }
-
-
- static int new_socket(struct addrinfo *ai) {
- int sfd;
- int flags;
- sfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
- flags = fcntl(sfd, F_GETFL, 0);
- fcntl(sfd, F_SETFL, flags | O_NONBLOCK);
-
- return sfd;
- }
上面代码的流程还是蛮清晰的。就是根据用户的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函数),就申请动态申请一个数组。有新连接就从这个数组中分配一个元素即可。
- conn **conns;
- static void conn_init(void) {
-
-
- int next_fd = dup(1);
-
-
- int headroom = 10;
- struct rlimit rl;
-
-
- max_fds = settings.maxconns + headroom + next_fd;
-
-
- if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {
- max_fds = rl.rlim_max;
- } else {
- fprintf(stderr, "Failed to query maximum file descriptor; "
- "falling back to maxconns\n");
- }
-
- close(next_fd);
-
-
-
-
- if ((conns = calloc(max_fds, sizeof(conn *))) == NULL) {
- fprintf(stderr, "Failed to allocate connection structures\n");
-
- exit(1);
- }
- }
上面代码中,calloc申请的是conn*指针数组而不是conn结构体数组。主要是因为conn结构体是比较大的一个结构体(成员变量很多)。不一定会存在settings.maxconns个同时在线的客户端。所以可以等到需要conn结构体的时候再去动态申请。需要时去动态申请,这样会有内存碎片啊!非也!!因为可以循环使用的。如果没有这个conn*指针数组,那么当这个连接断开后就要free这个conn结构体所占的内存(不然就内存泄漏了)。有了这个数组那么就可以不free,由数组管理这个内存。下面的conn_new函数展示了这一点。
-
- conn *conn_new(const int sfd, enum conn_states init_state,
- const int event_flags,
- const int read_buffer_size, enum network_transport transport,
- struct event_base *base) {
- conn *c;
-
- assert(sfd >= 0 && sfd < max_fds);
- c = conns[sfd];
-
- if (NULL == c) {
- if (!(c = (conn *)calloc(1, sizeof(conn)))) {
- fprintf(stderr, "Failed to allocate connection object\n");
- return NULL;
- }
-
- ...
-
- c->sfd = sfd;
- conns[sfd] = c;
- }
-
- ...
- c->state = init_state;
-
-
- event_set(&c->event, sfd, event_flags, event_handler, (void *)c);
- event_base_set(base, &c->event);
- c->ev_flags = event_flags;
-
- if (event_add(&c->event, 0) == -1) {
- perror("event_add");
- return NULL;
- }
-
- return c;
- }
综合上面的代码可以看到,主线程的基础实施也已经搭好了。注意,主线程对于socket fd 可读事件的回调函数是event_handler,回调参数是conn这个结构体指针。
牛刀小试:
主线程和worker线程的基础设施都已经搭建好了,现在来尝试一下accept一个客户端。在跑一遍整个流程之前,先回忆一下回调函数。worker线程对于管道可读事件的回调函数是ethread_libevent_process函数。主线程对于socket fd可读事件的回调函数是event_handler函数。conn结构体成员state的值为conn_listening。现在走起!!直奔event_handler函数。
- void event_handler(const int fd, const short which, void *arg) {
- conn *c;
-
- c = (conn *)arg;
- assert(c != NULL);
-
- c->which = which;
- if (fd != c->sfd) {
- conn_close(c);
- return;
- }
-
- drive_machine(c);
- return;
- }
太简单了吧,有没有搞错。event_handler函数确实简单,但其调用的drive_machine函数就确实很复杂。drive_machine函数内部是一个有限状态机。本文已经很长了,所以不会详解讲解有限状态机。下面只挑出处理新连接的那部分讲解。
- static void drive_machine(conn *c) {
- bool stop = false;
- int sfd;
- socklen_t addrlen;
- struct sockaddr_storage addr;
- int res;
- const char *str;
-
- assert(c != NULL);
-
-
-
- while (!stop) {
-
- switch(c->state) {
- case conn_listening:
- addrlen = sizeof(addr);
-
- sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen);
-
- ...
-
-
- dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST,
- DATA_BUFFER_SIZE, tcp_transport);
-
- stop = true;
- break;
-
- ...
- }
- }
-
- return;
- }
-
-
-
- static int last_thread = -1;
-
-
- void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags,
- int read_buffer_size, enum network_transport transport) {
- CQ_ITEM *item = cqi_new();
-
- char buf[1];
-
- int tid = (last_thread + 1) % settings.num_threads;
-
- LIBEVENT_THREAD *thread = threads + tid;
-
- last_thread = tid;
-
- item->sfd = sfd;
- item->init_state = init_state;
- item->event_flags = event_flags;
- item->read_buffer_size = read_buffer_size;
- item->transport = transport;
-
- cq_push(thread->new_conn_queue, item);
-
- buf[0] = 'c';
- if (write(thread->notify_send_fd, buf, 1) != 1) {
- perror("Writing to thread notify pipe");
- }
- }
现在主线程已经通知了选定的worker线程。接下来就是worker线程怎么处理这个通知了。下面看一下worker线程的管道可读事件回调函数thread_libevent_process。
- static void thread_libevent_process(int fd, short which, void *arg) {
- LIBEVENT_THREAD *me = arg;
- CQ_ITEM *item;
- char buf[1];
-
- read(fd, buf, 1);
-
- switch (buf[0]) {
- case 'c':
-
- item = cq_pop(me->new_conn_queue);
-
- if (NULL != item) {
-
-
- conn *c = conn_new(item->sfd, item->init_state, item->event_flags,
- item->read_buffer_size, item->transport, me->base);
-
- c->thread = me;
-
- cqi_free(item);
- }
- break;
-
- }
- }
正如前面所说的,memcached为每一个连接申请一个conn结构体进行维护。conn_new函数内部会为这个socket fd申请一个event并添加到该worker线程的event_base里面。当客户端发送命令时,worker线程就能监听到。这个conn_new函数前面已经说过了,这里也就不给出代码了。
在以后,都是worker线程负责这里这个客户端的一切通信,也是worker线程负责完成客户端的命令,包括申请内存存储数据、查询数据、删掉数据。这些苦工都是worker线程完成的,而没有其它线程帮忙。不过大可放心,memcached对于这命令一般都能在常数时间时间复杂度内完成。所以,即使一个worker线程有多个客户端连接,也完全应付得过来。
转自:http://blog.csdn.net/luotuo44/article/details/42705475