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:
2 | main_base = event_init(); |
6 | assoc_init(settings.hashpower_init); |
8 | slabs_init(settings.maxbytes, settings.factor, preallocate); |
16 | if (sigignore(SIGPIPE) == -1) { |
17 | perror ( "failed to ignore SIGPIPE; sigaction" ); |
21 | thread_init(settings.num_threads, main_base); |
26 | if (start_assoc_maintenance_thread() == -1) { |
上面thread_init 就是创建了配置的工作线程,来看看其内容。memcached线程之间对各个数据的访问都是用互斥锁,条件变量等。 然后为每个线程申请一个LIBEVENT_THREAD 结构, 来看看这个结构的内容:
3 | struct event_base *base; |
4 | struct event notify_event; |
7 | struct thread_stats stats; |
8 | struct conn_queue *new_conn_queue; |
10 | uint8_t item_lock_type; |
很简单,线程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) { |
7 | threads = calloc (nthreads, sizeof (LIBEVENT_THREAD)); |
9 | perror ( "Can't allocate thread descriptors" ); |
13 | dispatcher_thread.base = main_base; |
14 | dispatcher_thread.thread_id = pthread_self(); |
16 | for (i = 0; i < nthreads; i++) { |
19 | perror ( "Can't create notify pipe" ); |
23 | threads[i].notify_receive_fd = fds[0]; |
24 | threads[i].notify_send_fd = fds[1]; |
27 | setup_thread(&threads[i]); |
29 | stats.reserved_fds += 5; |
33 | for (i = 0; i < nthreads; i++) { |
34 | create_worker(worker_libevent, &threads[i]); |
上面setup_thread 用来初始化事件的。他负责将notify_receive_fd 放到线程自有的event_base, 里面。并初始化me->new_conn_queue, 前者用来通知对方,有新连接了,后者用来记录新连接的具体内容是什么。具体怎么通知后面写下。
1 | static void setup_thread(LIBEVENT_THREAD *me) { |
3 | me->base = event_init(); |
5 | fprintf (stderr, "Can't allocate event base\n" ); |
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); |
15 | if (event_add(&me->notify_event, 0) == -1) { |
16 | fprintf (stderr, "Can't monitor libevent notify pipe\n" ); |
20 | me->new_conn_queue = malloc ( sizeof ( struct 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, |
3 | enum network_transport transport, |
4 | FILE *portnumber_file) { |
8 | if (IS_UDP(transport)) { |
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" ); |
18 | listen_conn_add->next = listen_conn; |
19 | listen_conn = listen_conn_add;//挂载到链表头部 |
conn_new函数负责初始化一个连接的conn *c; 结构,这里memcache对于连接的查找方法为直接用fd大小去索引数组,这样速度快很多倍,而且这个数目不会太大的。conn_new里面大部分是数据字段初始化操作。主要就是讲参数里面的fd句柄加入到主线程的 event_base中:
1 | conn *conn_new( const int sfd, enum conn_states init_state, |
3 | const int read_buffer_size, enum network_transport transport, |
4 | struct event_base *base) { |
10 | event_set(&c->event, sfd, event_flags, event_handler, ( void *)c); |
11 | event_base_set(base, &c->event); |
12 | c->ev_flags = event_flags; |
14 | if (event_add(&c->event, 0) == -1) { |
这样,一个监听SOCKET就建立起来了,并且设置了可读事件的毁掉函数为event_handler, 这个函数及其重要,是主线程的线程循环。
主线程回到main函数后,就进入了事件循环了:
8 | if (event_base_loop(main_base, 0) != 0) { |
此时这个事件循环里面只有监听SOCK的事件的,于是如果有新连接到来的话,event_handler 就会被调用,实际上基本等于drive_machine 函数被调用。
主线程事件循环
drive_machine 函数相当于一个自动机,由连接的不同状态进行循环处理,直到完毕。
其大体结构为:
1 | static void drive_machine(conn *c) { |
这里只关注conn_listening 新连接的事件,也就是如果这个连接的状态是conn_listening,那么说明它是LISTEN socket了。这个是在conn_new的参数里面指定的。 来看看董自动机的新连接处理方法:
5 | addrlen = sizeof (addr); |
8 | sfd = accept4(c->sfd, ( struct sockaddr *)&addr, &addrlen, SOCK_NONBLOCK); |
10 | sfd = accept(c->sfd, ( struct sockaddr *)&addr, &addrlen); |
13 | sfd = accept(c->sfd, ( struct sockaddr *)&addr, &addrlen); |
15 | if (settings.maxconns_fast && |
18 | dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST, |
19 | DATA_BUFFER_SIZE, tcp_transport); |
可以看出上面的新连接也还算挺简单的,就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) { |
4 | CQ_ITEM *item = cqi_new(); |
9 | fprintf (stderr, "Failed to allocate memory for connection object\n" ); |
13 | int tid = (last_thread + 1) % settings.num_threads; |
15 | LIBEVENT_THREAD * thread = threads + tid; |
20 | item->init_state = init_state; |
21 | item->event_flags = event_flags; |
22 | item->read_buffer_size = read_buffer_size; |
23 | item->transport = transport; |
25 | cq_push( thread ->new_conn_queue, item); |
27 | MEMCACHED_CONN_DISPATCH(sfd, thread ->thread_id); |
29 | if (write( thread ->notify_send_fd, buf, 1) != 1) { |
30 | perror ( "Writing to thread notify pipe" ); |
稍微回顾一下:
- memcached的线程框架为:单进程,多线程,master-worker,二者用管道通信,链表传递数据。
- 使用libevent 开发框架, master-worker分别拥有不同的event_base,因此他们能各自进行事件监听, 相对还是挺清晰的;
- master负责建立客户端连接,worker负责跟实际客户端进行数据交互,处理请求。
所以这里master只是一个分发器而已,具体请求由工作线程去处理,解析等。这部分后续记录吧。