memcached源码学习-Main线程

原创 2014年09月05日 15:59:08

http://chenzhenianqing.cn/articles/1223.html

看了看memcached, memcached 主要的线程框架是master-slave的主线程-工作线程模式,单进程,多线程,之间通过管道和链表通信,基本就是这样。

下面具体看下代码。

稍微回顾一下:

  • memcached的线程框架为:单进程,多线程,master-worker,二者用管道通信,链表传递数据。
  • 使用libevent 开发框架, master-worker分别拥有不同的event_base,因此他们能各自进行事件监听, 相对还是挺清晰的;
  • master负责建立客户端连接,worker负责跟实际客户端进行数据交互,处理请求。

所以这里master只是一个分发器而已,具体请求由工作线程去处理,解析等。这部分后续记录吧。

 

 

 

worker工作线程

memcached服务其使用libevent库进行网络事件的监听等,在main函数的开头,解析完所有的配置参数后,主线程会先创建一个struct event_base *main_base 全局结构,代表这是主线程的event结构,用来进行循环等的。接下来做了一些简单的hash等初始化,然后调用了一个比较关键的函数: thread_init:

1     /* initialize main thread libevent instance */
2     main_base = event_init();
3   
4     /* initialize other stuff */
5     stats_init();
6     assoc_init(settings.hashpower_init);
7     conn_init();//默认分配200个连接结构
8     slabs_init(settings.maxbytes, settings.factor, preallocate);
9   
10     /*
11      * ignore SIGPIPE signals; we can use errno == EPIPE if we
12      * need that information
13      */
14     //当服务器close一个连接时,若client端接着发数据。根据TCP协议的规定,会收到一个RST响应,
15     //client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。
16     if (sigignore(SIGPIPE) == -1) {
17         perror("failed to ignore SIGPIPE; sigaction");
18         exit(EX_OSERR);
19     }
20     /* start up worker threads if MT mode */
21     thread_init(settings.num_threads, main_base);//默认4个线程,启动其libevet结构。设置监听管道,管道用来让主线程告诉自己,有新事>
22 件了
23     //上面工作线程已经准备好了,下面主线程终于可以设置自己的东西了,比如打开LISTEN socket,进行accepted监听。
24     //memcached的模式为:主线程负责accept,完了管道告诉工作线程去处理实际的请求。2着不同的event_base
25   
26     if (start_assoc_maintenance_thread() == -1) {//启动维护线程
27         exit(EXIT_FAILURE);
28     }

上面thread_init 就是创建了配置的工作线程,来看看其内容。memcached线程之间对各个数据的访问都是用互斥锁,条件变量等。 然后为每个线程申请一个LIBEVENT_THREAD 结构, 来看看这个结构的内容:

1 typedef struct {
2     pthread_t thread_id;        /* unique ID of this thread */
3     struct event_base *base;    /* libevent handle this thread uses */
4     struct event notify_event;  /* listen event for notify pipe */
5     int notify_receive_fd;      /* receiving end of notify pipe */
6     int notify_send_fd;         /* sending end of notify pipe */
7     struct thread_stats stats;  /* Stats generated by this thread */
8     struct conn_queue *new_conn_queue; /* queue of new connections to handle */
9     cache_t *suffix_cache;      /* suffix cache */
10     uint8_t item_lock_type;     /* use fine-grained or global item lock */
11 } LIBEVENT_THREAD;

很简单,线程ID, 属于这个线程的event_base, new_conn_queue, 这些后面再介绍。上面变量中,最重要的就是notify_receive_fd 和notify_send_fd, 他们是用来进行管道通信的。一个专门读,一个专门写。然后调用setup_thread 将notify_receive_fd 加入到event里面,设置相关的回调。

1 void thread_init(int nthreads, struct event_base *main_base) {
2     //初始化线程之间的锁,初始化工作线程,为每个线程分配里边event结构, 并且启动event_base_loop()循环
3     //工作线程监听notify_receive_fd,  可读回调为thread_libevent_process
4 //`````
5   
6     //为每个线程申请一个LIBEVENT_THREAD线程结构,存放event等
7     threads = calloc(nthreads, sizeof(LIBEVENT_THREAD));
8     if (! threads) {
9         perror("Can't allocate thread descriptors");
10         exit(1);
11     }
12     //
13     dispatcher_thread.base = main_base;
14     dispatcher_thread.thread_id = pthread_self();
15   
16     for (i = 0; i < nthreads; i++) {
17         int fds[2];
18         if (pipe(fds)) {//新建一个双向管道
19             perror("Can't create notify pipe");
20             exit(1);
21         }
22   
23         threads[i].notify_receive_fd = fds[0];//这是接收的,工作线程监听这个fd的可读事件即可
24         threads[i].notify_send_fd = fds[1];//这是发送的
25   
26         //工作线程初始化,初始化里边event, 将notify_receive_fd加入event里面回调为thread_libevent_process
27         setup_thread(&threads[i]);
28         /* Reserve three fds for the libevent base, and two for the pipe */
29         stats.reserved_fds += 5;
30     }
31   
32     /* Create threads after we've done all the libevent setup. */
33     for (i = 0; i < nthreads; i++) {//真正创建工作线程,执行函数为worker_libevent
34         create_worker(worker_libevent, &threads[i]);
35     }

上面setup_thread 用来初始化事件的。他负责将notify_receive_fd 放到线程自有的event_base, 里面。并初始化me->new_conn_queue, 前者用来通知对方,有新连接了,后者用来记录新连接的具体内容是什么。具体怎么通知后面写下。

1 static void setup_thread(LIBEVENT_THREAD *me) {
2     //工作线程初始化,初始化里边event, 将notify_receive_fd加入event里面回调为thread_libevent_process
3     me->base = event_init();//创建一个属于线程自己的event结构,这里也是一个循环哈
4     if (! me->base) {
5         fprintf(stderr, "Can't allocate event base\n");
6         exit(1);
7     }
8   
9     //下面将我自己的me->notify_receive_fd管道加入到事件循环里面,监听可读事件,回调为thread_libevent_process
10     /* Listen for notifications from other threads */
11     event_set(&me->notify_event, me->notify_receive_fd,
12               EV_READ | EV_PERSIST, thread_libevent_process, me);
13     event_base_set(me->base, &me->notify_event);///设置关系
14   
15     if (event_add(&me->notify_event, 0) == -1) {//加入
16         fprintf(stderr, "Can't monitor libevent notify pipe\n");
17         exit(1);
18     }
19   
20     me->new_conn_queue = malloc(sizeof(struct conn_queue));//上面me->notify_receive_fd用来告诉对方,有新东西了,实际上东西放在new
21 _conn_queue里面的

上面me->base = event_init() 指令为当线程分配一个属于线程自己的I/O位置了。具体里边event怎么使用旧不多说了。

注意后面的event_set 函数的参数,设置可读写的执行函数为thread_libevent_process,这样等有事件的时候,线程独立的event_base就会触发返回,比如epoll_wait返回了。

thread_init 最后调用create_worker 函数,真正的创建线程,设置线程函数等。这个时候由于event_base已经设置好了各项参数,将事件加入epoll等事件监听函数,所以可以真正创建线程了,这个由create_worker 完成,其实就是个普通的pthread_create 创建线程。

master线程

master线程也就是主线程, 在创建好工作线程后,会调用server_sockets等函数,打开监听句柄等操作。 调用关系为: main-> server_sockets->server_socket; 后者负责创建一TCP连接,进行监听事件。 这里注意memcache支持TCP 和UDP ,这里只讲TCP的情况;

1 static int server_socket(const char *interface,
2                          int port,
3                          enum network_transport transport,
4                          FILE *portnumber_file) {
5     //创建socket,然后根据其是TCP还是UDP,设置相关的结构,到目前为止就可以监听了</pre>
6 //·····
7   
8         if (IS_UDP(transport)) {//如果是UDP协议,那么直接将这个监听端口分破给num_threads_per_udp个线程,让他们一起处理
9             //因为是数据报文,接收的时候是一个报文一个报文接收,所以随便某个线程接收了都行,不需要accept。
10         } else {//tcp模式
11             //将这个TCP连接放到main_base事件循环中,设置为EV_PERSIST模式,回调为:event_handler
12             if (!(listen_conn_add = conn_new(sfd, conn_listening,
13                                              EV_READ | EV_PERSIST, 1,
14                                              transport, main_base))) {
15                 fprintf(stderr, "failed to create listening connection\n");
16                 exit(EXIT_FAILURE);
17             }
18             listen_conn_add->next = listen_conn;
19             listen_conn = listen_conn_add;//挂载到链表头部
20         }

conn_new函数负责初始化一个连接的conn *c; 结构,这里memcache对于连接的查找方法为直接用fd大小去索引数组,这样速度快很多倍,而且这个数目不会太大的。conn_new里面大部分是数据字段初始化操作。主要就是讲参数里面的fd句柄加入到主线程的 event_base中:

1 conn *conn_new(const int sfd, enum conn_states init_state,
2                 const int event_flags,
3                 const int read_buffer_size, enum network_transport transport,
4                 struct event_base *base) {
5     //thread_libevent_process等调用这里,设置一个连接的客户端结构,然后将其加入到event里面,执行函数统一为event_handler
6     c = conns[sfd];//直接用fd进行数组索引
7   
8     //加入到事件循环里面。,不管是LISTEN SOCK,还是客户端的SOCK,都是这个执行函数。只有工作线程的管道不是这个
9     //工作线程的管道执行函数是thread_libevent_process,也就是本函数的调用者之一
10     event_set(&c->event, sfd, event_flags, event_handler, (void *)c);//用参数,填充&c->event
11     event_base_set(base, &c->event);//ev->ev_base = base, 就让event记住了所属的base
12     c->ev_flags = event_flags;//其实 c->event->ev_events也记录了这个事件集合的。
13   
14     if (event_add(&c->event, 0) == -1) {//加到epoll里面去
15         perror("event_add");
16         return NULL;
17     }
18 }

这样,一个监听SOCKET就建立起来了,并且设置了可读事件的毁掉函数为event_handler, 这个函数及其重要,是主线程的线程循环。

主线程回到main函数后,就进入了事件循环了:

1     /* Drop privileges no longer needed */
2     drop_privileges();
3   
4     //进入事件循环,监听sock的回调为 event_handler,实际上等于drive_machine, 工作线程有自己的event_base_loop,各自循环自己的,由一>
5 个管道沟通。
6     //主线程accept一个连接后,调用dispatch_conn_new分配给一个工作线程,轮训
7     /* enter the event loop */
8     if (event_base_loop(main_base, 0) != 0) {
9         retval = EXIT_FAILURE;
10     }

此时这个事件循环里面只有监听SOCK的事件的,于是如果有新连接到来的话,event_handler 就会被调用,实际上基本等于drive_machine 函数被调用。

主线程事件循环

drive_machine  函数相当于一个自动机,由连接的不同状态进行循环处理,直到完毕。

其大体结构为:

1 static void drive_machine(conn *c) {
2 //····
3     while (!stop) {
4   
5         switch(c->state) {//根据这个SOCK的状态进行处理,
6         case conn_listening://对于监听SOCK, 在server_socket里面设置为conn_listening状态了的
7         case conn_waiting:
8         case conn_read:
9         case conn_write:
10 }
11 //````

这里只关注conn_listening 新连接的事件,也就是如果这个连接的状态是conn_listening,那么说明它是LISTEN socket了。这个是在conn_new的参数里面指定的。 来看看董自动机的新连接处理方法:

1     while (!stop) {
2   
3         switch(c->state) {//根据这个SOCK的状态进行处理,
4         case conn_listening://对于监听SOCK, 在server_socket里面设置为conn_listening状态了的
5             addrlen = sizeof(addr);
6 #ifdef HAVE_ACCEPT4
7             if (use_accept4) {
8                 sfd = accept4(c->sfd, (struct sockaddr *)&addr, &addrlen, SOCK_NONBLOCK);//快速设置SOCK_NONBLOCK状态
9             } else {
10                 sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen);
11             }
12 #else
13             sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen);
14 #endif
15             if (settings.maxconns_fast &&
16 //·····
17             } else {//OK,将这个连接分配给线程去处理吧
18                 dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST,
19                                      DATA_BUFFER_SIZE, tcp_transport);
20             }
21   
22             stop = true;
23             break;

可以看出上面的新连接也还算挺简单的,就accept一个新连接,然后调用dispatch_conn_new 函数,进行分发,分发给其他地方的线程去处理真正的请求,这里只是简单的accept了连接。调用dispatch_conn_new 的时候,设置连接状态为
啊conn_new_cmd, 也就是等待命令状态.

主线程接收这个连接后,怎么分配给其他工作线程处理呢?是通过dispatch_conn_new 函数完成的。

方法为轮训选一个线程,然后将当前连接的状态, 缓存区等放到一个CQ_ITEM 结构里面,然后将其加入到线程的thread->new_conn_queue 链表里面的,注意cq_push 函数是得加锁的。 然后调用write(thread->notify_send_fd, buf, 1) 简单的将一个”c”  字符写入到该线程的管道中,这样该线程一定会被换起来,然后就能检测到这个新连接,然后就可以为其提供服务了。

1 void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags,
2                        int read_buffer_size, enum network_transport transport) {
3     //分配给不同的线程去处理,通过管道
4     CQ_ITEM *item = cqi_new();
5     char buf[1];
6     if (item == NULL) {
7         close(sfd);
8         /* given that malloc failed this may also fail, but let's try */
9         fprintf(stderr, "Failed to allocate memory for connection object\n");
10         return ;
11     }
12   
13     int tid = (last_thread + 1) % settings.num_threads;//轮训分配
14   
15     LIBEVENT_THREAD *thread = threads + tid;//找到那个线程,然后将这个fd加入到这个线程的连接队列里面
16   
17     last_thread = tid;
18   
19     item->sfd = sfd;//填充结构
20     item->init_state = init_state;
21     item->event_flags = event_flags;
22     item->read_buffer_size = read_buffer_size;
23     item->transport = transport;
24   
25     cq_push(thread->new_conn_queue, item);//加锁,放到队列尾部
26   
27     MEMCACHED_CONN_DISPATCH(sfd, thread->thread_id);
28     buf[0] = 'c';
29     if (write(thread->notify_send_fd, buf, 1) != 1) {//写一个字节,告诉他有新东西了
30         perror("Writing to thread notify pipe");
31     }
32 }

稍微回顾一下:

  • memcached的线程框架为:单进程,多线程,master-worker,二者用管道通信,链表传递数据。
  • 使用libevent 开发框架, master-worker分别拥有不同的event_base,因此他们能各自进行事件监听, 相对还是挺清晰的;
  • master负责建立客户端连接,worker负责跟实际客户端进行数据交互,处理请求。

所以这里master只是一个分发器而已,具体请求由工作线程去处理,解析等。这部分后续记录吧。

MEMCACHED源码之main初始化

一.启动memcached 1.memcached启动选项: -p TCP监听端口 (default: 11211) -U UDP 监听端口 (default: 11211, 0 is off)...
  • hao508506
  • hao508506
  • 2016年09月01日 14:46
  • 517

memcached源码阅读----使用libevent和多线程模型

本篇文章主要是我今天阅读memcached源码关于进程启动,在网络这块做了哪些事情。 一、iblievent的使用     首先我们知道,memcached是使用了iblievet作为网络框架的,...
  • wallwind
  • wallwind
  • 2014年09月14日 23:55
  • 8767

Memcached线程模型分析

介绍Memcached中多线程如何实现,线程之间如何通信等问题
  • KangRoger
  • KangRoger
  • 2015年08月30日 22:30
  • 1146

深入理解memcached,高并发、懒惰与LRU(一)

1. Memcached如何支持高并发         Memcached使用多路复用I/O模型。传统阻塞I/O中,系统可能随时因为某个用户连接还没做好I/O准备而一直等待,知道这个连接做好准备。如果...
  • u012675743
  • u012675743
  • 2015年07月07日 15:03
  • 2863

memcached源码分析-----网络模型

转载请注明出处: 半同步/半异步:         memcached使用半同步/半异步网络模型处理客户端的连接和通信。         半同步/半异步模型的基础设施:主线程创建多个子线程(这...
  • luotuo44
  • luotuo44
  • 2015年01月14日 09:59
  • 5038

深入分析Memcached的线程 main()函数

三,在main()函数中,初始化main_thread的event_base实例,见memcached.c //定义main_thread的event_base实例 static struct e...
  • tycoon1988
  • tycoon1988
  • 2014年08月26日 10:18
  • 227

CAFFE源码学习笔记之一

单纯的将自己的笔记上的内容一点点搬运过来。 在复习卷积神经网络的同时还能学习一下系统级c++程序的规范和技巧。 ××××××××××××××××× 一、前言 本系列就是要把caffe这样一个系...
  • sinat_22336563
  • sinat_22336563
  • 2017年03月30日 11:18
  • 291

Java中的main线程是不是最后一个退出的线程

Java中的main线程是不是最后一个退出的线程 个人blog原文地址:http://www.gemoji.me/when_main_thread_end/  之所以写这篇文章,是因为...
  • anhuidelinger
  • anhuidelinger
  • 2013年08月27日 19:31
  • 10573

Memcached源码分析 - Memcached源码分析之总结篇(8)

Memcached源码分析共8篇文章,前7篇文章主要分析每个模块的c源代码。这一篇文章主要是将之前的流程串起来,总结和回顾。同时通过这篇文章可以全局去看Memcached的结构。 之前7篇文章: 《L...
  • initphp
  • initphp
  • 2015年04月06日 00:07
  • 2580

memcached主线程工作线程通信机制

accept/dispatch:         memcached使用"主线程统一accept/dispatch子线程"网络模型处理客户端的连接和通信,也就是《UNIX网络编程 卷1 ...
  • qq_15457239
  • qq_15457239
  • 2015年09月16日 10:28
  • 835
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:memcached源码学习-Main线程
举报原因:
原因补充:

(最多只允许输入30个字)