Memcached是一個分佈式的內存緩存庫,正好自己想寫個cache的模塊,那麼就偷偷師吧。
功能庫看的是實現原理和思路,性能庫看的是實現細節,memcahed是屬於一個看性能的庫(實現cache功能的模塊很多,但是性能就有高低了)
1、memcached的數據交互協議
memcached是分佈式的內存緩存服務器,它是通過socket(tcp/udp/unixsock)與其他程序交換數據的,這樣就需要一套協議來保證正常通信。
現在先看看memcached的通信協議,memcached的通信協議是以" "標誌符來表示一個完整的解析單元的,這裏說的解析單元是指服務器可以"理解"的最小數據分塊,一個完整的命令是由一個或多個可解析單元組成的,現在列舉一下通信命令:
對於所有的命令都有可能產生錯誤,這時服務器返回:
"ERROR ":服務器收到一個無效的命令
"CLIENT_ERROR error_msg ":命令格式錯誤
"SERVER_ERROR error_msg ":服務器處理錯誤,這時連接會被關閉
2、memcached的內存管理
內存管理都需要解決:分配、回收、碎片這幾個一般性問題,memcached的處理方式是(通過宏USE_SYSTEM_MALLOC 可以控制memcached使用系統的malloc/free管理內存,這裏不討論);
分配-->預分配 + 動態2倍分配 --> 減少realloc的調用
回收-->從來不釋放內存 --> memcached的目的就是通過內存緩存數據,沒有必要釋放
碎片-->固定大小分配 + 通過額外的動態指針數組保存各個分塊的地址 + (增加HEAD/TAIL指針數組)LRU算法回收
--> 加快獲得空閒"內存塊"的獲得
memcached將內存劃分爲不同大小的集合(通過結構體slabclass_t來維護一個集合的信息),現在看看slabclass_t結構。這裏有幾個概念:
內存單元:集合中維護的"邏輯內存塊"的大小,它是以sizeof(void*)字節對齊的
內存頁: slabclass_t分配內存的時候是以perslab個內存單元分配的,這perslab個連續的內存單元就是內存頁
瞭解了memcached的內存管理的數據結構後,下面看看內存管理相關的幾個主要函數:
do_slabs_newslab(class_id) 這個函數分配一個新的內存頁
|-->檢查內存分配是否已經超過了限制
|
檢查是否需要進行內存頁指針的2倍擴展
|
從操作系統中申請內存malloc(len)-->對於需要在不同的slabclass間轉移數據的應用len使用的固定大小1M
| 而其他應用這裏分配的是size*perslab,這裏size是sizeof(void *)對齊的
|
確認malloc成功後初時化它的值爲0(memset), (我想這裏可以使用calloc代替)
將end_page_ptr指向新的內存頁,並把它加入到內存頁數組中,同時修改對應的計算變量 (這個函數是memcached中唯一分配"用戶可用內存"的地方, "用戶可用"是指set/update/replace指令可以控制的內存)
do_slabs_alloc(need_size) 這個函數功能是根據需要的大小申請內存塊
|
根據需要的大小查找對應的slabclass_t結構
|
檢查是否超出了設置的內存限額
檢查內存單元指針數組slots是否爲空,如果非空-->返回一個空的內存單元
檢查是否分配了新的內存頁, 如果是-->返回一個"新的"內存單元(沒有加入到slots中)
如果新的內存頁爲空,那麼調用do_slabs_newslab從系統分配內存
(當然do_slabs_alloc還會修改對應的計數變量)
do_slabs_free(pointer, mem_unit_size)
|
根據mem_unit_size查找對應的slabclass_t結構
|
檢查是否需要進行內存單元數組slots的2倍擴展,(2倍擴展都是使用realloc完成數據轉移的)
|
將釋放的內存加入到內存單元數組中
(可以看到memcached是不真正釋放內存的,而且它的分配與釋放操作都是很簡單的指針賦值操作,
這就是memcached內存的管理,也是它快的原因之一,另外一個原因在於它的hash算法)
do_slabs_reassign(src_id, des_id) 這個函數將個集合中的某一個內存頁的內容複製到另一個集合的一個內存頁中
|
根據id獲得源集合和目標集合的slabcalss_t結構
|
檢查源集合和目標集合的狀態:只有在源集合沒有新分配的內存頁,而且存在有效的內存單元; 目標集合沒有新分
| 配的內存頁,並且內存頁指針有空閒的空間(這個可能通過2倍擴展得到);源集合和目標集合的內存頁
| 大小都是1M
|
循環源集合中每個內存單元(事實上它保存的是struct item 結構體),對於已經被"申請使用"的內存單元進行"清理":
從關聯列表中(hashtable)刪除該鍵值assoc_delete(ITEM_key(it), it->nkey)
這時還會檢查item的引用計數是否爲0,如果不是就會設置該忙的狀態was_buse = true
從訪問歷史雙向鏈表中刪除對應的item, item_unlink_q(it)
檢查item的引用計數,當它是0時,釋放該item,item_free(it)
|
循環檢查源集合的內存單元數組,並從中修改指向需要"複製"的內存頁中的內存塊
|
如果的忙狀態爲true,那麼就會返回-1
(從處理流程來看,這時該內存頁的內存單元已經清理出了hashtable和訪問歷史鏈表。如果返回-1, 客戶端就可以在稍後重新提交ressign的請求)
|
將內存分頁掛到目標集合的新的分頁上,並修改對應的計數變量和將item的slabs_clsid設置爲0,這個主要是要保此代碼對於slabs_clsid檢查的一致性
3. 名值對hashtable的管理
memcached使用的是hashtable來維護名值對(通過hash值和掩碼計算最後的hashtable的下標),爲了降低hash值的碰撞,memcached 使用自動擴展的策略,當hashtable中保存的item數目大於它的大小的1.5倍時,memcached就會實行hashtable的擴展,並把原hanshtable中的元素會重新hash並放到新的hashtable中,而且爲了降低查找的延時,這種數據遷移會分散在多次的訪問中(後面我們再詳細分析)。
現在我們看看相關的幾個函數的實現。
assoc_init()分配hashtable的所需的內存
|-->unsigned int hash_size = hashsize(hashpower) * sizeof(void*);
primary_hashtable = malloc(hash_size);
memset(primary_hashtable, 0, hash_size);
assoc_find(key, key_len) 根據鍵尋找對應的值
|
根據key和key_len計算hash值(hash算法的細節可以參考http://burtleburtle.net/bob/hash/doobs.html)
|
根據hash值和掩碼計算hashtable的下標
如果當前處於hashtable的擴展過程,並且下標值小於數據遷移的記錄值,那麼就從新的hashtable中獲得該下標對應的item鏈表,否則就從原來的hashtable中獲得item鏈表
|
循環對比鏈表中的item的key尋找對應的item
assoc_insert(item) 將item加入到hashtable中
|
驗證item的key不在hashtable中
|
計算hash值和需要更新的hashtable的下標和assoc_find的算法一樣,根據下標和當前是否處於hashtable的擴展過程中來更新舊的hashtable或新的hashtable
|
如果當前不是處於擴展狀態,那麼就檢查hashtable中保存的item數是否超過其大小的1.5倍,如果是就進行2倍的容量擴展assoc_expand()
assoc_expand()
|-->hashtable的2倍容量擴展
|
將hashtable中的第一個下標的item列表重新計算hash值並移到新的hashtable中
(這裏只移動了一個下標的item鏈表do_assoc_move_next_bucket)
|
對於其他的元素的遷移會在用戶用戶請求的時候進行移動,這是把時間消耗分散的延遲處理方式
當元素遷移完成後,就會釋放舊的hashtable佔用的資源free
assoc_delete(key, key_len) 從hashtable中刪除對應key的item
|-->尋找key對應的元素的指針變量的地址
_hashitem_before
|
修改item的h_next指針,從鏈表中刪除該元素
4. 數據保存對象struct item結構
memcached在內部使用雙向鏈表維護item的數據,現在我們看看item結構體和幾個主要的函數
do_item_alloc(key, key_len, client_flag, expiretime, data_len) 申請足夠大的內存單元
|
計算所需的內存單元大小item_make_header(key_len+1(加1表示字符串末尾的"0"), client_flag, data_len, buf, &extral_len))
|
根據所需大小查找內存單元
slabs_clsid(need_size) --> 檢查是否存在內存分組
slabs_alloc(need_size) --> slabs_alloc是一個宏,對於多線程模式和單線程模式,它會映射到不同的函數
|
如果slabs_alloc失敗,就從保存訪問歷史的全局變量tails中查找最多50次,得到一個最近最久沒有使用的並且引用計數爲0的item,將它從hashtable和訪問歷史鏈表中"刪除"do_item_unlink(do_item_unlink調用本身並不保證item佔用的內存返回到可用隊列中,只有當item的引用計數變成0時纔會進行真正的資源返還,由於在調用do_item_unlink前已經檢查了引用計數的值,所以item佔用的內存將會變成可用)
|
重新調用slabs_alloc請求內存,如果失敗就直接返回NULL
|
初始化新的item的成員-->需要注意的是next/prev/h_next都會初始化爲0,而且引用計算refcount會設置爲1,也就是說使用方需要保證最後會將refcount減少1,這裏item的狀態變量it_flags也會初始化爲0
item_free(item*) 刪除item的資源,包括hashtable、訪問歷史鏈表 heads/tails/sizes, 把item佔用的資源返回緩存中
|
將item的狀態變量設爲ITEM_SLABBED,並將對應的資源返回到緩衝中
slabs_free(item*, item_total_size);
item_unlink_q(item *) 從歷史鏈表中刪除對應的item
item_link_q(item *) 將item加入到歷史鏈表中
這兩個函數的細節要結合一起看, item_link_q中是將item依次加入到heads的第一個元素,也就是說它是時間反向的,而tails是時間一致的,最先訪問的item在tails的前面,而且tails的指針不是每次都修改的,只有在tails爲空的時候纔會更新,而鏈表的構造也是在插入到heads的時候完成了。可以看到item_unlink_q中處理(heads[class_id] == item) 和 (tails[class_id] == item)的情況,它們修改的指針分別是"head = it->next"和"*tail = it->prev;"
這兩個函數是不修改item的任何表示狀態的變量的
do_item_link(item*)
do_item_unlink(item *)
這兩個函數分別將item對象加入到歷史鏈表和將item從歷史鏈表中刪除,它們首先將item加入hashtabl(或從hashtable刪除),然後分別進行item_link_q/item_unlink_q
需要注意的是,對於do_item_unlink函數,還會檢查refcount,當爲零時,它會將釋放item的資源item_free
這兩個函數還會修改item的狀態變量"it->it_flags |= ITEM_LINKED/it->it_flags &= ~ITEM_LINKED;"
do_item_update(item*)/do_item_replace(item *it, item *new_it)
這兩個函數的很相似,do_item_update調用的是item_unlink_q和item_link_q,do_item_replace調用的是do_item_unlink和do_item_link,它們的代碼都比較簡單。
do_item_get_notedeleted(key, key_len, &delete_lock) 這個函數根據key查找沒有被刪除的item元素
|
從hashtable中查找對應的item
檢查item是否已經超時,這裏檢查主要是memcached是5秒一次檢查超時的,如果在這段時間內獲取item就有可能得到實際上已經超時的item, if(!item_delete_lock_over(it)){*delete_lock = false; it = NULL;}
|
檢查全局設置,如果設置了統一個有效時間,並且當前時間已經超過了這個有效時間,同時item是在有效時間前設置的,那麼就刪除該item
檢查item的超時時間,如果已經超時,就把item刪除
do_item_unlink(it);
|
增加item的引用計數,並返回item指針
do_item_flush_expired() 這個函數是把settting.oldest_live時間後的item全部刪除,這個函數的調用主要是處理memcached客戶端的"flush_all"指令使用
需要注意的是當client發送"flush_all"時,memcached會修改settting.oldest_live的值,這是會對do_item_get_notedeleted造成影響,因爲在下一次調用do_item_get_notedeleted的時候會對settting.oldest_live進行對比,在這個時間之前的item也會被刪除,這個也是"flush_all"的分散處理時間的策略,這裏也是出於性能因數作的設計。
do_store_item(item* , comm) 根據comm的值(add/replace/set), 處理item的值
這個函數主要是調用其他相關的函數完成功能,需要注意的是它的處理邏輯:
"add":如果存在該key對應的非刪除的item(do_item_get_notedeleted得到),那麼更新這個item的訪問歷史,對於新接收到的數據是忽略的,如果由於delete_lock不存在,那麼不作任何處理,如果完全沒有這個key對應的item, 那麼就增加這個key的item(do_item_link(it);)
"replace":如果存在該key對應的非刪除的item(do_item_get_notedeleted得到),更新這個key對應的值(do_item_replace), 如果由於delete_lock不存在,那麼不作任何處理,如果完全沒有這個key對應的item, 那麼就增加這個key的item(do_item_link(it);)
"set":查詢key對應的item(do_item_get_nocheck,其實前面已經調用了do_item_get_notedeleted,如果發現是delete_lock,那麼調用do_item_get_nocheck),如果存在那麼更新key對應的ite(do_item_replace,對應的item的it_flag會被完全更新), 否則調用 do_item_link將item加入到hashtable和訪問歷史鏈表中
item相關的操作函數中還有幾個是關於狀態查詢的,這裏就不展開了^_^。
5、memcached中表示網絡連接的數據抽象
memcached的網絡通信使用的是libevent(關於libevent的討論請參考我的另一篇blog),同時定義了相關的數據結構,現在我們來看一下:
conn結構體是memcached中成員最多的結構體,下面我們在看看這些成員是怎麼使用的。
6、memcached的處理流程
現在我們來看看memcached的整個運作流程:
註冊SIGINT信號-->簡單的退出
signal(SIGINT, sig_handler)
|
初始化全局設置settings_init
|
取消標準錯誤輸出的緩衝setbuf(stderr, NULL)
|
非常經典的解析命令行參數,覆蓋默認設置(注意全局變量optarg的使用)
while( c = getopt(argc, argv, ...)){
switch(c){....} }
|
將系統資源設置到最大的xiandu
getrlimit(RLIMIT_CORE, &rlim) <==> setrlimit(RLIMIT_CORE, &rlim_new)
|
創建監聽socket(這裏以TCP socket爲例說明)
server_socket(settings.port, 0)-->第二個參數0表示這個是tcp socket
| |--->創建socket,並將它設置爲非阻塞模式
| socket(...)
| if ((flags = fcntl(sfd, F_GETFL, 0)) < 0 || fcntl(sfd, F_SETFL, flags | O_NONBLOCK) < 0){...}
| 很經典的if語句^_^
| (如果使用ioctrl來設置socket爲非阻塞的話{ u_long flag = 1; ioctl(sfd, FIONBIO , &flag);},即使ioctl返回成
| 功,也不表示該socket已經設置爲非阻塞模式了)
| |
| 將socket設置爲地址重用,這個主要是可以在TIME_WAIT狀態下馬上就能綁定該端口
| setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, (void *)&flags, sizeof(flags));
| 這裏總結一下SO_REUSEADDR的作用:
| 1、單進程在TIME_WAIT狀態下重新綁定一個端口,IP和PORT完全相同
| 2、多網卡,多IP狀態下,多進程可以用不同的IP綁定同一個端口
| 3、單進程多IP可以綁定同一個端口
| 4、在UDP多播的應用中,多進程可以用相同的IP和PORT綁定相同的地址
| |
| 設置socket的選項:
| setsockopt(sfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&flags, sizeof(flags));
| |
| 設置SO_KEEPALIVE選項後,如果2小時(具體時間與TCP協議棧的實現有關)內socket完全空閒,TCP將
| 發送一個主機存活探測包,這是TCP協議中必須響應的包,接受方若正常就以正確ACK包響應,如果接收
| 方崩潰或重啓,則以RST包響應,RST接收方設置錯誤號爲ECONNREST,如果探測包接收方無任何響應,
| 源自Berkelay的TCP協議棧等待75秒再次發送一個探測包,當一共發送9個探測包仍然沒有任何響應時,
| 那麼發送方放棄並將發送socket的錯誤號設爲ETIMEOUT
| |
| setsockopt(sfd, SOL_SOCKET, SO_LINGER, (void *)&ling, sizeof(ling));
| 設置socket的關閉方式,以struct linger結構調用setsocketopt
| 當參數爲SO_DONTLINGER時(相當於SO_LINGER,且struct linger 爲{0,0}),表示調用closesocket時強制
| 關閉socket,所有懸掛的數據將丟棄,對方recv調用將返回ECONNRESET。如果struct linger結構的
| l_onoff =1 同時l_linger != 0,當阻塞的socket調用closesocket時將一直阻塞直到懸掛的數據發送完或者超
| 時(l_linger表示超時的秒數)。當非阻塞的socket調用closesocket時將返回EWOULDBLOCK/EAGAIN錯誤
| |
| setsockopt(sfd, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags));
| 這是設置數據包的Nagle化,Nagle化是將小的數據組裝爲一個比較大的幀一次發送的算法,它的處理方式
| 很簡單:就是在接收到前一個包的確認到來前一直緩存發送的數據(write調用),Nagle算法可以緩解網絡
| 擁塞。對於一些網絡應用,需要把網絡數據包組裝爲一個最大的網絡傳輸單元一次發送可以節省流量,這種
| 情況下可以使用TCP_CORK選項來強制socket使用MSS發送數據,不過這個選項只可以在linux平臺上使用
| |
| 綁定端口並將socket轉換爲被動socket
| bind(sfd, (struct sockaddr *)&addr, sizeof(addr))
| listen(sfd, 1024)
|
把權限降低到普通的用戶(當然需要啓動的用戶爲root)
(getuid/geteuid-->getpwnam(username)-->setgid(pw->pw_gid); setuid(pw->pw_uid))
|
當前進程進入daemon模式
daemon(maxcore, settings.verbose);關於daemon進程相關情況請參考我的另一篇blog,這裏不展開了^_^
|
初始化libevent
event_init();
|
初始化memcached的運行環境
item_init();-->將歷史訪問鏈表初始化爲NULL
stats_init();-->全局狀態初始化,主要是統計信息的初始化,如:命令請求、數據流量等
assoc_init();-->hashtable的初始化,計算hashtable的大小-->分配空間-->初始化空間爲NULL
conn_init();-->抽象tcp連接的初始化, 分配了一個指向struct conn* 的指針數組, 這個數組有200個元素
slabs_init(mem_limit, unit_factor);-->內存管理初始化
| |-->循環計算每個內存集合的內存單元大小和每個內存頁所包含的內存單元數目
| 對於每個內存頁的大小是與sizeof(void*)對齊的,而且內存集合的內存單元大小是以factor因子增加的
| 如果當前的內存單元的大小超過0.5M,就好停止擴展,並在最後增加一個內存單元爲1M的集合
| (在memcached中內存頁大小的上限是1M)
| |
| 如果編譯的時候沒有定義DONT_PREALLOC_SLABS而且環境變量中也沒有定義T_MEMD_SLABS_ALLOC
| 那麼memcached就會進行內存的預分配slabs_preallocate(power_largest)-->調用do_slabs_newslab()真正分
| 配內存
|
禁止memcached分配的內存被交換到swap分區上
mlockall(MCL_CURRENT | MCL_FUTURE);-->只對支持的平臺調用
|
忽略SIGPIPE信號
sa.sa_handler = SIG_IGN;
sa.sa_flags = 0;
if (sigemptyset(&sa.sa_mask) == -1 ||sigaction(SIGPIPE, &sa, 0) == -1){...}
(對於SIGPIPE信號的默認處理是進程退出,爲了memcached的穩定性,這裏忽略了這個信號)
說明:對於突然中斷的socket,系統會向對方發送RST包,如果這個包在對方進行讀操作前到達,那麼讀取方得到的錯誤是ECONNRESET,如果RST包在讀操作後到達,那麼得到的錯誤是"an unexpected EOF";對於寫操作,如果寫發生在收到RST前,那麼在收到RST時會返回已經發送的字節數 如果再次調用就會得到ECONNRESET錯誤號, 並觸發SIGPIPE信號; 如果再write前就收到了RST包,那麼就會直接得到ECONNRESET錯誤號,而且觸發 SIGPIPE信號。
|
將監聽socket加入到封裝到struct conn中,並加入到libevent的監聽隊列中
conn_new(l_socket, conn_listening, EV_READ | EV_PERSIST, 1, false, main_base)
|-->從freeconns中提取一個空閒的conn,conn *c = conn_from_freelist();
| |
| 如果沒有空閒的conn對象(c == NULL), 構建新的conn
| c = (conn *)malloc(sizeof(conn))
| |
| 初始化conn 的成員,這裏需要分配的內存包括:
| rbuf:讀緩存,主要用於解析命令,當命令解析後,數據就會放到item的data中
| wbuf:寫緩存
| ilist:item列表數組,用於"get"指令中
| iov:iovec結構體數組指針
| msglist:msghdr結構體數組,用於劃分過大的數據塊
| |
| 設置事件的結構
| event_set(&c->event, sfd, event_flags, event_handler, (void *)c);
| 設置事件的base成員
| event_base_set(base, &c->event);
| 將事件安裝到libevent監聽的隊列中
| event_add(&c->event, 0)
|
保存daemon的pid文件
save_pid(getpid(), pid_file)
|
初始化多線程的工作環境(建立共享互斥量)
thread_init(thread_num, event_base)
| |-->初始化mutex和條件變量
| cache的操作互斥量,例如操作item和hashtable等
| pthread_mutex_init(&cache_lock, NULL);
| connection list的共享變量互斥量
| pthread_mutex_init(&conn_lock, NULL);
| slabs內存單元操作互斥量
| pthread_mutex_init(&slabs_lock, NULL);
| 查詢memcached狀態互斥量
| pthread_mutex_init(&stats_lock, NULL);
| 工作線程初始化互斥量
| pthread_mutex_init(&init_lock, NULL);
| 工作線程條件變量
| pthread_cond_init(&init_cond, NULL);
| 連接隊列互斥量
| pthread_mutex_init(&cqi_freelist_lock, NULL);
| |
| 分配線程相關的LIBEVENT_THREAD數據結構
| typedef struct {
| pthread_t thread_id; 線程id
| struct event_base *base; libevent的base結構
| struct event notify_event; 監聽通知事件
| int notify_receive_fd; 管道的接收方
| int notify_send_fd; 管道的發送方
| CQ new_conn_queue; 連接隊列
| } LIBEVENT_THREAD;
| threads = malloc(sizeof(LIBEVENT_THREAD) * nthreads);
| |
| 循環初始化工作線程相關的LIBEVENT_THREAD結構
| threads[0].base = main_base;
| threads[0].thread_id = pthread_self();
| (這裏設置0是爲了下面代碼處理的一致性,它是不另外創建線程運行的)
| threads[i].notify_receive_fd = pipe(fds)[0]
| threads[i].notify_send_fd = pipe(fds)[1]
| setup_thread((LIBEVENT_THREAD * )&threads[i]);
| | |-->(i!=0)
| | 對於每個線程數據LIBEVENT_THREAD初始化libevent
| | pthread_libevnet->base = event_init()
| | |
| | 設置libevent的事件結構
| | event_set(&pthread_libevnet->notify_event, pthread_libevnet->notify_receive_fd, EV_READ |
| | EV_PERSIST, thread_libevent_process, pthread_libevnet);
| | event_base_set(pthread_libevnet->base, &pthread_libevnet->notify_event);
| | |
| | 將事件加入到監聽隊列中
| | event_add(&pthread_libevnet->notify_event, 0)
| | |
| | 初始化連接對列
| | cq_init(&pthread_libevnet->new_conn_queue);
| | |-->初始化連接對列互斥量
| | pthread_mutex_init(&conn_queue->lock, NULL);
| | pthread_cond_init(&conn_queue->cond, NULL);
| |
| 循環創建工作線程
| create_worker(worker_libevent, &threads[i]), 需要注意的是,這裏是從下標1開始循環的,因爲0表示主線程
| | |-->初始化線程的屬性變量
| | pthread_attr_init(&attr)
| | |
| | 創建線程pthread_create(&thread, &attr, worker_libevent, &threads[i])
| | |
| | 設置條件變量,表示線程已經開始運行
| | pthread_mutex_lock(&init_lock);
| | init_count++;
| | pthread_cond_signal(&init_cond);
| | pthread_mutex_unlock(&init_lock);
| | |
| | 啓動事件的監聽event_base_loop(me->base, 0);
| |
| 等待工作線程初始化結束
| pthread_mutex_lock(&init_lock);
| init_count++; // 主線程(接收外部請求的線程)已經初始化完成
| while (init_count < nthreads) {
| pthread_cond_wait(&init_cond, &init_lock);
| }
| pthread_mutex_unlock(&init_lock);
| 這裏使用了條件變量來表示各個工作線程的初始化, init_count表示已經初始化完成的線程
|
註冊數據定時器,更新memcached的內部"當前時間"
clock_handler(0, 0, 0);
|--> 由於定期器是一次性的,所以當定時事件已經在libevent的中時,這裏會重新註冊
| evtimer_del(&clockevent)-->clockevent是全局變量
| |
| 將定時器加入到監聽隊列中
| evtimer_set(&clockevent, clock_handler, 0)-->可以看到,這裏是把定時器的回調函數重新設置爲clock_handler
| event_base_set(main_base, &clockevent)
| evtimer_add(&clock_base, &struct timeval{.tv_set = 1, .tv_usec=0})
| |
| 更新memcached的當前時間set_current_time()
|
註冊刪除數據的定時器
|-->delete_handler(0, 0, 0);
| |
| 這裏的流程和clock_handler基本相同,定時時間爲5秒
| |
| 刪除超時的數據
| run_deferred_deletes()
| |
| (在多線程模式下,這裏會先加鎖互斥量cache_lock)
| do_run_deferred_deletes()
| |-->這裏會先檢查刪除對列中item的超時時間,只有大於當前時間的item纔會真正刪除
|
啓動主線程監聽外部請求
event_base_loop(main_base, 0);
memcached的啓動細節我們已經清楚了,下面我們看一下"set-get"的數據處理流程(這裏略去了關於udp的設置)。
7、memcached的命令處理流程
(這裏以"set"-->"get"-->"delete" 命令流程爲例)
客戶端調用socket的connect連接到memcached服務器
|
memcached監測到listen socket可讀(libevent的event_base_loop)
調用回調函數event_handler()
|
調用狀態機處理請求
drive_machine(conn*)
|
由於監聽socket的狀態是conn_listening,所以這裏調用
sfd = accept(c->sfd, &addr, &addrlen)
|
如果errno == EMFILE(打開了過多的描述符),那麼就關閉監聽socket
否則設置sfd爲非阻塞狀態並創建新的連接(conn)與它對應,新的連接的狀態爲conn_read
對於單線程模式,dispatch_conn_new是一個指向conn_new函數
對於多線程模式,
| dispatch_conn_new的功能是將請求的conn掛到工作線程的conn queue上(這裏我們跟蹤一下多線程的工作
| 方式)
| |
| 創建新的connection item
| CQ_ITME * item = cqi_new()
| | |-->首先檢查全局變量cqi_freelist,它指向的是最後一個空閒的CQ_ITEM,如果沒有分配或者
| | 沒有空閒的,就指向NULL
| | |
| | 如果cqi_freelist指向NULL,那麼一次分配64個CQ_ITEM加入到cqi_freelist中,並將它們
| | "串接"起來。
| | item = malloc(sizeof(CQ_ITEM) * ITEMS_PER_ALLOC);
| | pthread_mutex_lock(&cqi_freelist_lock);
| | item[ITEMS_PER_ALLOC - 1].next = cqi_freelist;
| | cqi_freelist = &item[1];
| | pthread_mutex_unlock(&cqi_freelist_lock);
| |
| 將請求連接加入到工作線程的處理連接隊列中
| cq_push(&threads[thread].new_conn_queue, item);
| | |-->藉助CQ->lock把item加入到請求對列中
| | pthread_mutex_lock(&cq->lock);
| | if (NULL == cq->tail)
| | cq->head = item;
| | else
| | cq->tail->next = item;
| | cq->tail = item;
| | 這裏設計一個條件變量是因爲在cq_pop函數中會等待這個條件
| | 但是memcached中並沒有調用cq_pop函數,所以我想這可能是歷史原因留下來的
| | (也許以前的多線程的工作方式是其他線程一直在等待條件變量來獲得新的請求,例如:
| | while(conn_item = cq_pop(CQ)){
| | process_the_request;
| | }
| | 現在的方式是工作線程等待pipe可讀,這個表示主線程已經將connection item加入到了
| | conn queue中了,工作線程監測到可讀信號後,直接從conn queue取cq_peek就可以了。而
| | 等待時的掛起現在轉移到了libevnet中,而不是pthread_cond_wait上了。
| | )
| | pthread_cond_signal(&cq->cond);
| | pthread_mutex_unlock(&cq->lock);
| |
| 通過管道通知工作線程新的請求到來
| write(threads[thread].notify_send_fd, "", 1)
| 由於管道是通過系統內部的緩衝來交互數據的,所以寫入數據後,讀端(libevent)就會收到可讀的信號
|
主線程返回----------------------->工作線程喚醒調用回調函數
|
thread_libevent_process
|
從管道中讀取一個字符(內容是什麼其實不重要,它只是一個通知信號)從而清空可讀信號
read(fd, buf, 1)
|
提取一個請求連接元素item = cq_peek(&LIBEVENT_THREAD->new_conn_queue)-->通過cq->lock互斥量
得到等待的CQ_ITME,這裏是從head指針提取的,和cq_push中對tail指針的操作實對應的,保證了請求
按到來的請求得到處理
|
將連接加入到工作線程的監聽隊列中
conn_new(item->sfd, item->init_state, item->event_flags,
item->read_buffer_size, item->is_udp, me->base);
新的conn的處理函數會被設置爲event_handler
需要注意的是:這裏的struct event_base是工作線程的base,也就是它是加入到工作線程的監聽隊列中的
|
"釋放"CQ_ITEM-->將CQ_ITEM 對象連接到cgi_freelist上
(現在客戶端socket調用返回成功)
|
客戶端發送set指令:"set key1 0 10 10 helloworld "
|
memcached的工作線程發現socket可讀,回調event_handler
|
drive_machine(c);
|
當conn狀態是conn_read時
嘗試解析指令try_read_command()-->tcp面向字節,可能收到不完整的命令
| |-->如果當前接收緩存非空,而且在緩存中找到可識別的命令單元(" "),
| | 那麼解析命令process_command(c, c->rcurr),並更新已經處理的緩存內容
| | |
| | 增加消息對列add_msghdr(conn * )
| | |-->對比msgsize和msgused的大小,當msgsize==msgused時,
| | | 進行2倍擴展c->msglist=realloc(c->msglist, c->msgsize*2*sizeof(struct msghdr))
| | | c->msgsize *= 2
| | | |
| | | 初始化struct msghdr結構
| | | memset(msg, 0, sizeof(struct msghdr));
| | | msg->msg_iov = &c->iov[c->iovused];
| | | msg->msg_name = &c->request_addr;
| | | msg->msg_namelen = c->request_addr_size;
| | |
| | 解析指令ntokens = tokenize_command(command, tokens, MAX_TOKENS);
| | tokenize_ocmmand只是簡單地劃分單詞
| | |
| | 現在的指令是"set key1 0 10 10 helloworld "
| | ntokens = 6
| | tokens[COMMAND_TOKEN].value-->"set"
| | |
| | 處理更新指令
| | process_update_command(conn, tokens, ntokens, NREAD_SET)
| | |-->申請內存單元
| | it = item_alloc(key, nkey, flags, realtime(exptime), vlen+2);-->2表示" "
| | it的引用計數會變成1
| | |
| | 設置conn的成員狀態和緩存指針
| | c->item_comm = comm;
| | c->item = it;
| | c->ritem = ITEM_data(it);
| | c->rlbytes = it->nbytes;
| | 修改conn的狀態conn_set_state(c, conn_nread);
| | (現在conn的狀態轉到接收key對應的數據)
| |
| 如果但前緩存沒有合法的命令那麼接收網絡數據
| try_read_network(c)
| |-->清除已經處理的數據-->memmove
| |
| 檢查已讀取的字節和緩衝的長度,當讀取的字節大於緩存長度時對緩衝區進行
| 2倍擴展realloc(c->rbuf, c->rsize * 2);
| |
| 從socket中讀取數據並放到緩衝中
| res = read(c->sfd, c->rbuf + c->rbytes, c->rsize - c->rbytes);
| c->rbytes += res;
| |
| 如果read的返回值爲0,表示對方socket已經關閉,這時改變conn的狀態爲conn_closing
| 如果返回-1, 而且errno == EAGAIN 或者 errno == EWOULDBLOCK, 表示這時socket的緩存
| 已經清空,這時返回gotdata-->表示是否接收到數據
|
當conn的狀態是conn_nread時(通常由存儲指令:add/set/replace會轉移到這個狀態)
檢查是否已經讀取了命令中指定的字節數(包括數據的" ")
如果讀取完,調用complete_nread(c)進行處理
| |-->檢查item中接收到的數據的合法性(最後兩個字節是" ")
| |
| 保存item的值store_item(it, c->item_comm)
| 如果成功,調用out_string(c, "STORED")輸出結果
| out_string會將str寫到conn的輸出緩存中,並將狀態設置爲conn_write, 並且c->write_and_go = conn_read
| |
| 刪除item(這裏主要是減少引用)
| 需要注意的是,雖然這裏已經將引用計數變成了0,但是它並不會在釋放item所佔的內存單元,因爲
| 面已經調用了do_item_link(加入到訪問歷史中),所以在後面的get指令中會找到這個item
| (現在,我們已經將
|
好了,現在度於memcached的工作原理、實現細節和技巧我們都比較清楚了^_^。
memcached中對於錯誤的處理經常都需要把conn的狀態設置爲conn_closing,最後我們來看看它做了些什麼清除工作:
現在我們對於memcahced的整個構思和實現都有比較清楚的理解了^_^。memcached中還有很多指令我們沒有詳細跟蹤,不過幾乎全部的函數的功能我們已經分析了^_^。
8、整體感覺
1、memcached中有很多關於內存使用的細節(如:引用計數,訪問歷史等),而且它沒有使用*nix系統的鏈表來組織數據,而是單獨實現了功能非常簡單的鏈表這是專門針對cache的功能設計的。
2、在多線程模式中,memcached使用觸發器的思想(通過管道觸發libevent),而沒有在工作線程中直接等待條件變量,這簡化了多線程的協作,也簡化了管理邏輯
3、對於pthread condiction variable的使用memcached沒有註冊退出清除代碼,如果在pthread_cont_wait是觸發了cannel信號,那麼該muxter將會永久被鎖定直到進程退出
4、可以說memcached沒有資源重用機制(內存頁被申請後是不釋放的),如果一個應用開始的時候使用了大量的小片內存,但是一段時間後又使用另一種尺寸的 內存頁,那麼前面分配的內存頁將不會被釋放,而且這種情況會持續到memcahed重啓。
原載地址:http://happyiww.popo.blog.163.com/blog/static/922448320078682017946/