Memcached源碼分析(線程模型)


http://www.iteye.com/topic/344172

目前網上關於memcached的分析主要是內存管理部分,下面對memcached的線程模型做下簡單分析 

有不對的地方還請大家指正,對memcahced和libevent不熟悉的請先google之 

先看下memcahced啓動時線程處理的流程 
 

memcached的多線程主要是通過實例化多個libevent實現的,分別是一個主線程和n個workers線程 
無論是主線程還是workers線程全部通過libevent管理網絡事件,實際上每個線程都是一個單獨的libevent實例 

主線程負責監聽客戶端的建立連接請求,以及accept 連接 
workers線程負責處理已經建立好的連接的讀寫等事件 

先看一下大致的圖示: 
 

首先看下主要的數據結構(thread.c): 

C代碼  收藏代碼
  1. /* An item in the connection queue. */  
  2. typedef struct conn_queue_item CQ_ITEM;  
  3. struct conn_queue_item {  
  4.     int     sfd;  
  5.     int     init_state;  
  6.     int     event_flags;  
  7.     int     read_buffer_size;  
  8.     int     is_udp;  
  9.     CQ_ITEM *next;  
  10. };  


CQ_ITEM 實際上是主線程accept後返回的已建立連接的fd的封裝 

C代碼  收藏代碼
  1. /* A connection queue. */  
  2. typedef struct conn_queue CQ;  
  3. struct conn_queue {  
  4.     CQ_ITEM *head;  
  5.     CQ_ITEM *tail;  
  6.     pthread_mutex_t lock;  
  7.     pthread_cond_t  cond;  
  8. };  


CQ是一個管理CQ_ITEM的單向鏈表 

C代碼  收藏代碼
  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.     CQ  new_conn_queue;         /* queue of new connections to handle */  
  8. } LIBEVENT_THREAD;  


這是memcached裏的線程結構的封裝,可以看到每個線程都包含一個CQ隊列,一條通知管道pipe 
和一個libevent的實例event_base 

另外一個重要的最重要的結構是對每個網絡連接的封裝conn 

C代碼  收藏代碼
  1. typedef struct{  
  2.   int sfd;  
  3.   int state;  
  4.   struct event event;  
  5.   short which;  
  6.   char *rbuf;  
  7.   ... //這裏省去了很多狀態標誌和讀寫buf信息等  
  8. }conn;  

memcached主要通過設置/轉換連接的不同狀態,來處理事件(核心函數是drive_machine) 

下面看下線程的初始化流程: 

在memcached.c的main函數中,首先對主線程的libevent做了初始化 

C代碼  收藏代碼
  1. /* initialize main thread libevent instance */  
  2.  main_base = event_init();  


然後初始化所有的workers線程,並啓動,啓動過程細節在後面會有描述 
C代碼  收藏代碼
  1. /* start up worker threads if MT mode */  
  2. thread_init(settings.num_threads, main_base);  

接着主線程調用(這裏只分析tcp的情況,目前memcached支持udp方式) 
C代碼  收藏代碼
  1. server_socket(settings.port, 0)  

這個方法主要是封裝了創建監聽socket,綁定地址,設置非阻塞模式並註冊監聽socket的 
libevent 讀事件等一系列操作 

然後主線程調用 
C代碼  收藏代碼
  1. /* enter the event loop */  
  2. event_base_loop(main_base, 0);  


這時主線程啓動開始通過libevent來接受外部連接請求,整個啓動過程完畢 

下面看看thread_init是怎樣啓動所有workers線程的,看一下thread_init裏的核心代碼 

C代碼  收藏代碼
  1. void thread_init(int nthreads, struct event_base *main_base) {  
  2.  //。。。省略  
  3.    threads = malloc(sizeof(LIBEVENT_THREAD) * nthreads);  
  4.     if (! threads) {  
  5.         perror("Can't allocate thread descriptors");  
  6.         exit(1);  
  7.     }  
  8.   
  9.     threads[0].base = main_base;  
  10.     threads[0].thread_id = pthread_self();  
  11.   
  12.     for (i = 0; i < nthreads; i++) {  
  13.         int fds[2];  
  14.         if (pipe(fds)) {  
  15.             perror("Can't create notify pipe");  
  16.             exit(1);  
  17.         }  
  18.   
  19.         threads[i].notify_receive_fd = fds[0];  
  20.         threads[i].notify_send_fd = fds[1];  
  21.   
  22.     setup_thread(&threads[i]);  
  23.     }  
  24.   
  25.     /* Create threads after we've done all the libevent setup. */  
  26.     for (i = 1; i < nthreads; i++) {  
  27.         create_worker(worker_libevent, &threads[i]);  
  28.     }  
  29. }  

threads的聲明是這樣的 
static LIBEVENT_THREAD *threads; 

thread_init首先malloc線程的空間,然後第一個threads作爲主線程,其餘都是workers線程 
然後爲每個線程創建一個pipe,這個pipe被用來作爲主線程通知workers線程有新的連接到達 

看下setup_thread 

C代碼  收藏代碼
  1. static void setup_thread(LIBEVENT_THREAD *me) {  
  2.     if (! me->base) {  
  3.         me->base = event_init();  
  4.         if (! me->base) {  
  5.             fprintf(stderr, "Can't allocate event base\n");  
  6.             exit(1);  
  7.         }  
  8.     }  
  9.   
  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.     cq_init(&me->new_conn_queue);  
  21. }  

setup_thread主要是創建所有workers線程的libevent實例(主線程的libevent實例在main函數中已經建立) 

由於之前 threads[0].base = main_base;所以第一個線程(主線程)在這裏不會執行event_init() 
然後就是註冊所有workers線程的管道讀端的libevent的讀事件,等待主線程的通知 
最後在該方法裏將所有的workers的CQ初始化了 

create_worker實際上就是真正啓動了線程,pthread_create調用worker_libevent方法,該方法執行 
event_base_loop啓動該線程的libevent 

這裏我們需要記住每個workers線程目前只在自己線程的管道的讀端有數據時可讀時觸發,並調用 
thread_libevent_process方法 

看一下這個函數 
C代碼  收藏代碼
  1. static void thread_libevent_process(int fd, short which, void *arg){  
  2.     LIBEVENT_THREAD *me = arg;  
  3.     CQ_ITEM *item;  
  4.     char buf[1];  
  5.   
  6.     if (read(fd, buf, 1) != 1)  
  7.         if (settings.verbose > 0)  
  8.             fprintf(stderr, "Can't read from libevent pipe\n");  
  9.   
  10.     item = cq_peek(&me->new_conn_queue);  
  11.   
  12.     if (NULL != item) {  
  13.         conn *c = conn_new(item->sfd, item->init_state, item->event_flags,  
  14.                            item->read_buffer_size, item->is_udp, me->base);  
  15.         。。。//省略  
  16.     }  
  17. }  


函數參數的fd是這個線程的管道讀端的描述符 
首先將管道的1個字節通知信號讀出(這是必須的,在水平觸發模式下如果不處理該事件,則會被循環通知,知道事件被處理) 

cq_peek是從該線程的CQ隊列中取隊列頭的一個CQ_ITEM,這個CQ_ITEM是被主線程丟到這個隊列裏的,item->sfd是已經建立的連接 
的描述符,通過conn_new函數爲該描述符註冊libevent的讀事件,me->base是代表自己的一個線程結構體,就是說對該描述符的事件 
處理交給當前這個workers線程處理,conn_new方法的最重要的內容是: 

C代碼  收藏代碼
  1. conn *conn_new(const int sfd, const int init_state, const int event_flags,  
  2.                 const int read_buffer_size, const bool is_udp, struct event_base *base) {  
  3.     。。。  
  4.             event_set(&c->event, sfd, event_flags, event_handler, (void *)c);  
  5.         event_base_set(base, &c->event);  
  6.         c->ev_flags = event_flags;  
  7.         if (event_add(&c->event, 0) == -1) {  
  8.         if (conn_add_to_freelist(c)) {  
  9.             conn_free(c);  
  10.         }  
  11.         perror("event_add");  
  12.         return NULL;  
  13.         }  
  14.     。。。  
  15. }  

可以看到新的連接被註冊了一個事件(實際是EV_READ|EV_PERSIST),由當前線程處理(因爲這裏的event_base是該workers線程自己的) 
當該連接有可讀數據時會回調event_handler函數,實際上event_handler裏主要是調用memcached的核心方法drive_machine 

最後看看主線程是如何通知workers線程處理新連接的,主線程的libevent註冊的是監聽socket描述字的可讀事件,就是說 
當有建立連接請求時,主線程會處理,回調的函數是也是event_handler(因爲實際上主線程也是通過conn_new初始化的監聽socket 的libevent可讀事件) 

最後看看memcached網絡事件處理的最核心部分- drive_machine 
需要銘記於心的是drive_machine是多線程環境執行的,主線程和workers都會執行drive_machine 

C代碼  收藏代碼
  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聲明 
C代碼  收藏代碼
  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線程的地方,看看 

 
C代碼  收藏代碼
  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 建立連接這個 

這部分代碼確實比較多,沒法全部貼出來,請大家參考源碼,最新版本1.2.6,我省去了很多優化的地方 
比如,每個CQ_ITEM被malloc時會一次malloc很多個,以減小碎片的產生等等細節。 

時間倉促,有紕漏的地方,歡迎大家拍磚。 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章