memcached线程模型---main thread线程

本文详细介绍了memcached的线程模型,特别是main thread如何初始化worker thread,以及它们如何通过管道进行通信。在启动过程中,main thread创建与worker thread通信的管道,并初始化worker thread对象。每个worker thread监听notify_event_fd,等待main thread通过管道发送的数据。main thread创建监听套接字,当有客户端连接请求时,通过accept()建立连接,然后将新连接分派给worker thread。worker thread接收到数据后,创建conn对象并开始处理客户端通信。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本节介绍各个线程以及相关数据的创建以及初始化工作。描述具体代码前,先介绍主要数据结构。memcached将原始线程id(pthread_t)封装成LIBEVENT_THREAD对象,该对象与线程一一对应,此对象定义如下:

/*
 * File: memcached.h
 */
typedef struct {
    pthread_t thread_id; /* 线程id */
    struct event_base *base;
/* 该event_base对象管理该线程所有的IO事件 */
    struct event notify_event;
/* 此事件对象与下面的notify_receive_fd描述符关联 */
    int notify_receive_fd;
/* 与main thread通信的管道(pipe)的接收端描述符 */
    int notify_send_fd;
/* 与main thread通信的管道的发送端描述符 */
    struct thread_stats stats; /* Stats generated by this thread */
    struct conn_queue *new_conn_queue; /* 此队列是被锁保护的同步对象,主要用来在main thread线程与该worker thread线程之间传递初始化conn对象所需数据 */
    cache_t *suffix_cache; /* suffix cache */} LIBEVENT_THREAD;

/*
 * File: thread.c
 * 与所有worker thread线程对应的线程对象数组
 */
static LIBEVENT_THREAD *threads;

 

 

    main thread线程创建以及初始化worker thread的操作主要通过thread_init()和setup_thread()函数来完成。thread_init()主要代码如下:

  1. /* 
  2.  * File: thread.c 
  3.  * thread_init() 
  4.  */  
  5. // 1) 此for循环初始化worker thread线程对象数组。  
  6. for (i = 0; i < nthreads; i++) {  
  7. // 1.1) 创建与main thread线程通信的管道,并初始化notify_*_fd描述符。  
  8.     int fds[2];  
  9.     if (pipe(fds)) {  
  10.         perror("Can't create notify pipe");  
  11.         exit(1);  
  12.     }  
  13.     threads[i].notify_receive_fd = fds[0];  
  14.     threads[i].notify_send_fd = fds[1];  
  15.   
  16. // 1.2) 主要用来注册与threads[i]线程的notify_event_fd描述符相关的IO事件。  
  17.     setup_thread(&threads[i]);  
  18. }  
  19.   
  20. // 2) 此for循环启动worker thread线程。worker_libevent()函数内部主要调用event_base_loop()函数,即循环监听该线程注册的IO事件。  
  21. /* Create threads after we've done all the libevent setup. */  
  22. for (i = 0; i < nthreads; i++) {  
  23.     create_worker(worker_libevent, &threads[i]);  
  24. }  
  25.   
  26. // 3) 等待所有子线程,即worker thread线程启动后,此函数才返回。  
  27. /* Wait for all the threads to set themselves up before returning. */  
  28. pthread_mutex_lock(&init_lock);  
  29. while (init_count < nthreads) {  
  30.     pthread_cond_wait(&init_cond, &init_lock);  
  31. }  
  32. pthread_mutex_unlock(&init_lock);  
<span style="font-size:24px;">/*
 * File: thread.c
 * thread_init()
 */
// 1) 此for循环初始化worker thread线程对象数组。
for (i = 0; i < nthreads; i++) {
// 1.1) 创建与main thread线程通信的管道,并初始化notify_*_fd描述符。
    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];

// 1.2) 主要用来注册与threads[i]线程的notify_event_fd描述符相关的IO事件。
    setup_thread(&threads[i]);
}

// 2) 此for循环启动worker thread线程。worker_libevent()函数内部主要调用event_base_loop()函数,即循环监听该线程注册的IO事件。
/* Create threads after we've done all the libevent setup. */
for (i = 0; i < nthreads; i++) {
    create_worker(worker_libevent, &threads[i]);
}

// 3) 等待所有子线程,即worker thread线程启动后,此函数才返回。
/* Wait for all the threads to set themselves up before returning. */
pthread_mutex_lock(&init_lock);
while (init_count < nthreads) {
    pthread_cond_wait(&init_cond, &init_lock);
}
pthread_mutex_unlock(&init_lock);</span>

       thread_init()函数的重点是通过setup_thread()函数为每个worker thread线程注册与notify_event_fd描述符有关的IO事件,这里的notify_event_fd描述符是该worker thread线程与main thread线程通信的管道的接收端描述符。通过注册与该描述符有关的IO事件,worker thread线程就能监听main thread线程发给自己的数据(事件)。setup_thread()函数主要代码如下:

 

/*
 * File: thread.c
 * setup_thread()
 */
// 1.2.1) 初始化线程对象中notify_event事件对象,并将其注册到event_base对象
/* Listen for notifications from other threads */
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);
}

// 1.2.2) 创建与初始化new_conn_queue队列。
me->new_conn_queue = malloc(sizeof(struct conn_queue));
if (me->new_conn_queue == NULL) {
    perror("Failed to allocate memory for connection queue");
    exit(EXIT_FAILURE);
}
cq_init(me->new_conn_queue);

由1.2.1)处代码段知,该worker thread线程将监听notify_event_fd描述符上的可读事件,即监听与main thread线程t通信的管道上的可读事件,并指定用thread_libevent_process()函数处理该事件。
        在3)处的代码段执行完毕后,各个worker thread线程就已经完成初始化并启动,而且各个worker thread线程开始监听并等待处理与notify_receive_fd描述符有关的IO事件。


        在worker thread线程启动后,main thread线程就要创建监听套接字(listening socket)来等待客户端连接请求。这里的监听(listen)客户端连接请求与libevent中的监听(monitor)IO事件有一定区别。在memcached中,套接字跟线程id一样,都被进一步封装。套接字被封装成conn对象,表示与客户端的连接(connection),该结构体定义很大,现选择与主题相关的几个字段,定义如下:

  1. /*  
  2.  * File: memcache.h  
  3.  */    
  4. typedef struct conn conn;    
  5. struct conn {    
  6.     int    sfd;    // 原始套接字    
  7.     sasl_conn_t *sasl_conn;    
  8.     enum conn_states  state;    // 此连接的态变变量,用于标记此连接在运行过程中的各个状态。此字段很重要。取值范围由conn_states枚举定义。        
  9.     enum bin_substates substate;  // 与state字段类似  
  10.     struct event event;    // 此事件对象与该套接字,即sfd字段关联。    
  11.     short  ev_flags; // 与上一字段有关,指定监听的事件类型,如EV_READ。    
  12.     short  which;   /** which events were just triggered */    
  13. // 以下字段略    
  14. }  
<span style="font-size:24px;">/* 
 * File: memcache.h 
 */  
typedef struct conn conn;  
struct conn {  
    int    sfd;    // 原始套接字  
    sasl_conn_t *sasl_conn;  
    enum conn_states  state;    // 此连接的态变变量,用于标记此连接在运行过程中的各个状态。此字段很重要。取值范围由conn_states枚举定义。      
    enum bin_substates substate;  // 与state字段类似
    struct event event;    // 此事件对象与该套接字,即sfd字段关联。  
    short  ev_flags; // 与上一字段有关,指定监听的事件类型,如EV_READ。  
    short  which;   /** which events were just triggered */  
// 以下字段略  
}</span>

        下面是main thread线程创建listening socket的地方:

  1. /* 
  2.  * File: memcached.c 
  3.  * server_socket() 
  4.  */  
  5. // 4) main thread线程在这里创建并初始化listening socket,包括注册与该conn对象相关的IO事件。注意conn_listening参数,它指定了该conn对象的初始化状态。  
  6. if (!(listen_conn_add = conn_new(sfd, conn_listening,  
  7.                                              EV_READ | EV_PERSIST, 1,  
  8.                                              transport, main_base))) {  
  9.     fprintf(stderr, "failed to create listening connection\n");  
  10.     exit(EXIT_FAILURE);  
  11. }  
  12. listen_conn_add->next = listen_conn;  
  13. listen_conn = listen_conn_add;  
<span style="font-size:24px;">/*
 * File: memcached.c
 * server_socket()
 */
// 4) main thread线程在这里创建并初始化listening socket,包括注册与该conn对象相关的IO事件。注意conn_listening参数,它指定了该conn对象的初始化状态。
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;</span>

 

        conn_new()是memcached中一个重要的函数,此函数负责将原始套接字封装成为一个conn对象,同时会注册与该conn对象相关的IO事件,并指定该连接(conn)的初始状态。这里要注意的是listening socket的conn对象被初始化为conn_listening状态,这个细节会在后面用到。conn_new()函数的部分代码如下:

/*
 * File: memcached.c
 * conn_new()
 */
// 4.1) 初始化conn对象的相关字段。注意state字段。
c->sfd = sfd;
c->state = init_state;

// 中间初始化步骤略

// 4.2) 注册与该连接有关的IO事件
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) {
    if (conn_add_to_freelist(c)) {
       conn_free(c);
    }
    perror("event_add");
    return NULL;
}

再次提醒,连接对象的state字段是一个很重要的变量,它标志了该conn对象在运行过程中的各个状态,该字段的取值范围由conn_states枚举定义。由4处代码段,传递给conn_new()函数的conn_listening常量知,main thread线程创建了一个初始状态为conn_listening的连接。这里可以提前透露下,worker thread线程在接受main thread线程的分派后(下一节会介绍),会创建初始状态为conn_new_cmd的conn对象。         大家应该熟悉了如何注册IO事件,就不赘述了。这里要提醒的是,你会发现memcached中所有conn对象相关的处理函数都是event_handler()函数,它在内部将主要的事件处理部分交给drive_machine()函数。这个函数就全权负责处理与客户连接相关的事件。        主线程在完成初始化后,会通过event_base_loop()进入监听循环,此时主线程开始等待listening socket上的连接请求。

         2,客户端连接的建立与分派        

        上一节介绍的启动步骤完成之后,memcached的主线程开始监听listening socket上的可读事件,即等待客户端连接请求,而worker thread监听各自notify_receive_fd描述符上的可读事件,即等待来自main thread线程的数据。现在,我们来看当客户端向memcached服务器发来连接请求,memcached会如何处理。        参考上一节关于创建listening socket的部分内容,我们知道,当客户端发来连接请求,main thread线程会因listening socket发生可读事件而返回(wake up),并调用event_handler()函数来处理该请求,此函数会调用drive_machie()函数,其中处理客户端连接请求的部分如下:

/*
 * File: memcached.c
 * drive_machine()
 */
switch(c->state) {
        case conn_listening:

// 5) 以下数行建立与客户端的连接,得到sfd套接字。
            addrlen = sizeof(addr);
            if ((sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen)) == -1) {
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    /* these are transient, so don't log anything */
                    stop = true;
                } else if (errno == EMFILE) {
                    if (settings.verbose > 0)
                        fprintf(stderr, "Too many open connections\n");
                    accept_new_conns(false);
                    stop = true;
                } else {
                    perror("accept()");
                    stop = true;
                }
                break;
            }
            if ((flags = fcntl(sfd, F_GETFL, 0)) < 0 ||
                fcntl(sfd, F_SETFL, flags | O_NONBLOCK) < 0) {
                perror("setting O_NONBLOCK");
                close(sfd);
                break;
            }
// 6) 此函数将main thread线程创建的原始套接字以及一些初始化数据,传递给某个指定的worker thread线程。

            dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST,
                                     DATA_BUFFER_SIZE, tcp_transport);
            stop = true;
            break;

需要铭记于心的是drive_machine是多线程环境执行的,主线程和workers都会执行drive_machine 

  1. static void drive_machine(conn *c) {   
  2.     bool stop = false;   
  3.     int sfd, flags = 1;   
  4.     socklen_t addrlen;   
  5.     struct sockaddr_storage addr;   
  6.     int res;   
  7.    
  8.     assert(c != NULL);   
  9.    
  10.     while (!stop) {   
  11.    
  12.         switch(c->state) {   
  13.         case conn_listening:   
  14.             addrlen = sizeof(addr);   
  15.             if ((sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen)) == -1) {   
  16.                 //省去n多错误情况处理   
  17.                 break;   
  18.             }   
  19.             if ((flags = fcntl(sfd, F_GETFL, 0)) < 0 ||   
  20.                 fcntl(sfd, F_SETFL, flags | O_NONBLOCK) < 0) {   
  21.                 perror("setting O_NONBLOCK");   
  22.                 close(sfd);   
  23.                 break;   
  24.             }   
  25.             dispatch_conn_new(sfd, conn_read, EV_READ | EV_PERSIST,   
  26.                                      DATA_BUFFER_SIZE, false);   
  27.             break;   
  28.    
  29.         case conn_read:   
  30.             if (try_read_command(c) != 0) {   
  31.                 continue;   
  32.             }   
  33.         ....//省略   
  34.      }        
  35.  }   

首先大家不到被while循环误导(大部分做java的同学都会马上联想到是个周而复始的loop)其实while通常满足一个case后就会break了,这里用while是考虑到垂直触发方式下,必须读到EWOULDBLOCK错误才可以 

言归正传,drive_machine主要是通过当前连接的state来判断该进行何种处理,因为通过libevent注册了读写时间后回调的都是这个核心函数,所以实际上我们在注册libevent相应事件时,会同时把事件状态写到该conn结构体里,libevent进行回调时会把该conn结构作为参数传递过来,就是该方法的形参 

memcached里连接的状态通过一个enum声明  

  1. enum conn_states {   
  2.     conn_listening,  /** the socket which listens for connections */   
  3.     conn_read,       /** reading in a command line */   
  4.     conn_write,      /** writing out a simple response */   
  5.     conn_nread,      /** reading in a fixed number of bytes */   
  6.     conn_swallow,    /** swallowing unnecessary bytes w/o storing */   
  7.     conn_closing,    /** closing this connection */   
  8.     conn_mwrite,     /** writing out many items sequentially */   
  9. };  

实际对于case conn_listening:这种情况是主线程自己处理的,workers线程永远不会执行此分支 
我们看到主线程进行了accept后调用了 
  dispatch_conn_new(sfd, conn_read, EV_READ | EV_PERSIST,DATA_BUFFER_SIZE, false); 

  这个函数就是通知workers线程的地方,看看 

  1. void dispatch_conn_new(int sfd, int init_state, int event_flags,   
  2.                        int read_buffer_size, int is_udp) {   
  3.     CQ_ITEM *item = cqi_new();   
  4.     int thread = (last_thread + 1) % settings.num_threads;   
  5.    
  6.     last_thread = thread;   
  7.    
  8.     item->sfd = sfd;   
  9.     item->init_state = init_state;   
  10.     item->event_flags = event_flags;   
  11.     item->read_buffer_size = read_buffer_size;   
  12.     item->is_udp = is_udp;   
  13.    
  14.     cq_push(&threads[thread].new_conn_queue, item);   
  15.    
  16.     MEMCACHED_CONN_DISPATCH(sfd, threads[thread].thread_id);   
  17.     if (write(threads[thread].notify_send_fd, "", 1) != 1) {   
  18.         perror("Writing to thread notify pipe");   
  19.     }   
  20. }   

 可以清楚的看到,主线程首先创建了一个新的CQ_ITEM,然后通过round robin策略选择了一个thread并通过cq_push将这个CQ_ITEM放入了该线程的CQ队列里,那么对应的workers线程是怎么知道的呢 

就是通过这个write(threads[thread].notify_send_fd, "", 1)向该线程管道写了1字节数据,则该线程的libevent立即回调了thread_libevent_process方法(上面已经描述过) 

然后那个线程取出item,注册读时间,当该条连接上有数据时,最终也会回调drive_machine方法,也就是drive_machine方法的 case conn_read:等全部是workers处理的,主线程只处理conn_listening 建立连接这个 

此函数主要新建并初始化了一个CQ_ITEM对象,该对象包含许多创建conn对象所需用的初始化数据,如原始套接字(sfd),初始化状态(init_state)等,然后该函数将该CQ_ITEM对象传递给某个被选定的worker thread线程。在上一节介绍LIBEVENT_THREAD线程对象时说过,new_conn_queue队列用来在两个线程之间传递数据,这里就被用来向worker thread线程传递一个CQ_ITEM对象。除此之外,还要注意main thread线程向与worker thread线程连接的管道写入了一个字节的数据。此举意在触发管道另一端,即notify_receive_fd描述符的可读事件。现在我们看管道另一端的worker thread线程会发生什么。
        我们知道memcached启动后,worker thread线程会监听notify_receive_fd描述符上的可读事件。因为main thread线程向管道写入了一个字节的数据,worker thread线程会因notify_receive_fd描述符上发生可读事件而返回,并调用事先注册时指定的thread_libevent_process()函数来处理该事件,该函数主要代码如下:

  1. /* 
  2.  * File: thread.c 
  3.  * thread_libevent_process() 
  4.  */  
  5. // 7) 从管道中读出一个字节数据,此字节即main thread线程先前向notify_send_fd描述符写入的字节。  
  6. if (read(fd, buf, 1) != 1)  
  7.         if (settings.verbose > 0)  
  8.             fprintf(stderr, "Can't read from libevent pipe\n");  
  9.   
  10. // 8) 从new_conn_queue队列中弹出一个CQ_ITEM对象,此对象即先前main thread线程推入new_conn_queue队列的对象。  
  11.     item = cq_pop(me->new_conn_queue);  
  12.   
  13. // 9) 根据这个CQ_ITEM对象,创建并初始化conn对象,该对象负责客户端与该worker thread线程之间的通信。  
  14.     if (NULL != item) {  
  15.         conn *c = conn_new(item->sfd, item->init_state, item->event_flags,  
  16.                            item->read_buffer_size, item->transport, me->base);  
  17.   
  18. // 以下略  
/*
 * File: thread.c
 * thread_libevent_process()
 */
// 7) 从管道中读出一个字节数据,此字节即main thread线程先前向notify_send_fd描述符写入的字节。
if (read(fd, buf, 1) != 1)
        if (settings.verbose > 0)
            fprintf(stderr, "Can't read from libevent pipe\n");

// 8) 从new_conn_queue队列中弹出一个CQ_ITEM对象,此对象即先前main thread线程推入new_conn_queue队列的对象。
    item = cq_pop(me->new_conn_queue);

// 9) 根据这个CQ_ITEM对象,创建并初始化conn对象,该对象负责客户端与该worker thread线程之间的通信。
    if (NULL != item) {
        conn *c = conn_new(item->sfd, item->init_state, item->event_flags,
                           item->read_buffer_size, item->transport, me->base);

// 以下略

        注意,在7)处代码段,从管道读出的一个字节数据就是main thread线程在2.4处写入的数据。显然,该数据本身没有意义,它的目的只是触发worker thread线程这边notify_receive_fd描述符的可读事件。然后根据取得的CQ_ITEM对象创建并初始化conn对象。这里要注意的是,在6)处代码段,main thread线程将该CQ_ITEM对象的init_state字段初始化为conn_new_cmd,那么worker thread线程创建的conn对象的state字段将被初始化为conn_new_cmd。

        到这里,就完成了从客户端发送连接请求,到main thread线程创建原始套接字,再到将原始套接字等初始化数据分派到各个worker thread线程,到最后worker thread线程创建conn对象,开始负责与客户端之间通信的整个流程。worker thread就从这里开始监听该客户端连接的可读事件,并准备用event_handler()函数处理从客户端发来的数据。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值