Memcached學習(六)--內存管理

模型分析

 

在開始解剖memcached關於內存管理的源代碼以前,先宏觀上分析一下memcached內存管理的模型是怎樣子的:php

提個建議,我以爲memcached內存管理的模型與咱們平時作做業的做業本「畫格子給咱們往格子裏面寫字」的邏輯很像,一本本做業本就是咱們的內存空間,而咱們往裏寫的字就是咱們要存下來的數據,因此分析的時候能夠想像一下用方格做業本寫字的情景。算法

一、首先,先介紹memcached中關於內存管理的幾個重要的概念:數組

a)slab、chunkapp

  slab是一塊內存空間,默認大小爲1M,而memcached會把一個slab分割成一個個chunk,好比說1M的slab分紅兩個0.5M的chunk,因此說slab和chunk其實都是表明實質的內存空間,chunk只是把slab分割後的更小的單元而已。slab就至關於做業本中的「頁」,而chunk則是把一頁畫成一個個格子中的「格」。ide

b)itemmemcached

  item是咱們要保存的數據,例如php代碼:$memcached->set(「name」,」abc」,30);表明咱們把一個key爲name,value爲abc的鍵值對保存在內存中30秒,那麼上述中的」name」, 「abc」, 30這些數據實質都是咱們要memcached保存下來的數據, memcached會把這些數據打包成一個item,這個item實際上是memcached中的一個結構體(固然結構遠不止上面提到的三個字段這麼簡單),把打包好的item保存起來,完成工做。而item保存在哪裏?其實就是上面提到的」chunk」,一個item保存在一個chunk中。函數

  chunk是實質的內存空間,item是要保存的東西,因此關係是:item是往chunk中塞的。仍是拿做業原本比喻,item就是至關於咱們要寫的「字」,把它寫到做業本某一「頁(slab)」中的「格子(chunk)」裏。fetch

c)slabclassui

經過上面a)b)咱們知道,slab(都假設爲1M)會割成一個個chunk,而item往chunk中塞。this

那麼問題來了:咱們要把這個1M的slab割成多少個chunk?就是一頁紙,要畫多少個格子?

  咱們往chunk中塞item的時候,item總不可能會與chunk的大小徹底匹配吧,chunk過小塞不下或者chunk太大浪費了怎麼辦?就是咱們寫字的時候,格子過小,字出界了,或者咱們的字很小寫在一個大格子裏面好浪費。因此memcached的設計是,咱們會準備「幾種slab」,而不一樣一種的slab分割的chunk的大小不同,也就是說根據「slab分割的chunk的大小不同」來分紅「不一樣的種類的slab」,而 slabclass就是「slab的種類」的意思了。繼續拿做業原本比喻:假設咱們如今有不少張A4紙,有些咱們畫成100個格子,有些咱們畫成200個格子,有些300…。咱們把畫了相同個格子(也相同大小)的紙釘在一塊兒,成爲一本本「做業本」,每本「做業本」的格子大小都是同樣的,不一樣的「做業本」也表明着「畫了不一樣的大小格子的A4紙的集合」,而這個做業本就是slabclass啦!

 

二、對上面的概念有了個感性的認識了,咱們來解剖memcached中比較重要的結構體和變量:

a)slabclass_t(即上面說到的slabclass類型)

 1 typedef struct {
 2     unsigned int size;  //chunk的大小 或者說item的大小
 3     unsigned int perslab;//每一個slab有多少個item,slab又稱「頁」
 4     /**
 5     下面這個slots的解析:
 6     當前slabclass的空閒item鏈表,也是可用item鏈表,當前slabclass一切能夠用的內存空間都在此,
 7     這裏是內存分配的入口,分配內存的時候都是在這個鏈表上擠一個出去。
 8     ps:memcached的新版本纔開始把slots做爲「全部空閒的item連接」的用途,之前的版本slots鏈表保存的是「回收的item」的意思,
 9     而舊版本新分配的slab,是用end_page_ptr指針及end_page_free來控制,此版本已不用。
10     */
11     void *slots;
12     unsigned int sl_curr; //當前slabclass還剩多少空閒的item,即上面的slots數
13     unsigned int slabs;  //這個slabclass分配了多少個slab了
14     /**
15     下面slab_list和lisa_size的解析:
16     slab_list是這個slabclass下的slabs列表,邏輯上是一個數組,每一個元素是一個slab指針。
17     list_size是slab_list的元素個數。
18     注意這個list_size和上面的slabs的不一樣:
19         因爲slab_list是一個空間大小固定的數組,是數組!而list_size是這個數組元素的個數,表明slab_list的空間大小。
20         slabs表明已經分配出去的slabs數,list_size則表明能夠有多少個slabs數
21         因此當slabs等於list_size的時候表明這個slab_list已經滿了,得增大空間。
22     */
23     void **slab_list;
24     unsigned int list_size;
25     unsigned int killing; /* index+1 of dying slab, or zero if none */
26     size_t requested; /* The number of requested bytes */
27 } slabclass_t;

  代碼註釋中已經把大部分字段解析了,但再重點解析一個字段:slot,很重要。

  回想咱們的做業本,寫字的時候只須要知道做業本中的下一個空白格子在哪裏而後寫上去便可,由於用做業本寫字是有規律的,老是從第一頁第一行左邊開始往右寫,因此已經用的格子老是連續的。

  舊版本的memcached也是用這種思路,每一個slabclass保存着一個指向下一個空白chunk的指針的變量(end_page_ptr),但memcached內存管理和寫做業很不同的地方在於,memcached裏面保存的item是會過時的,並且每一個item的過時時間都極可能不同,也就是說做業本里面有些字會過時,過時以後相應的空格能夠回收並再次利用,因爲這些回收的item是不連續的,因此舊版本的memcached把每一個slabclass中過時的item串成一個鏈表,而每一個slabclass中的slot就是它相應的被回收的item鏈表。因此舊版本的memcached在分配內存空間的時候,先去slot找有沒有回收的item,沒有的話再去end_page_ptr找到下一個新的可用的空白的chunk。

  新版本的memcached包括如今分析的1.4.20版本,memcached把舊版本end_page_ptr去掉,把新的可用的chunk也整合到slot中,也就是說slot的定義由「回收的item」變爲「空閒可用的item」,每當咱們新開闢一個slab並把它分割成一個個chunk的時候,同時立刻把這些chunk先初始化成有結構的item(item是一個結構體),只是這個item的用戶數據字段爲空,待填充狀態,稱這些item爲」free的item」,並把這些free的item串成鏈表保存在slot中。而舊的item過時了,回收了,也變成」free的item」,也一樣插入到這個slot鏈表中。因此在新版本memcached中slabclass的slot的概念是指「空閒的item鏈表」!雖然這時內存分配的邏輯沒有舊版本那樣像做業本的思路那麼形象,但代碼和邏輯都變得更純粹了,每次要分配內存,只須要直接從slot鏈表中拿一個出去便可。

  memcached在啓動的時候會實例化幾本「做業本」:

static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];

slabclass[i]就是一本」做業本」,i理解爲做業本的id。

 

b)item結構體

 1 typedef struct _stritem {
 2     struct _stritem *next; //鏈表中下一個,這個鏈表有多是slots鏈表,也有多是LRU鏈表,但一個item不可能同時這兩個鏈表中,因此複用一個指針。
 3     struct _stritem *prev; //鏈表中上一個。
 4     struct _stritem *h_next;  //相同hash值中鏈表的下一個。
 5     rel_time_t time;   //最近訪問時間
 6     rel_time_t exptime;  //過時時間
 7     int nbytes;  //value的字節數
 8     unsigned short refcount; //引用計數
 9     uint8_t nsuffix;  //後綴長度
10     uint8_t it_flags;  //標記
11     uint8_t slabs_clsid;  //item所在的slabclass的id值
12     uint8_t nkey; //鍵長
13     /* this odd type prevents type-punning issues when we do
14      * the little shuffle to save space when not using CAS. */
15     union {
16         uint64_t cas;
17         char end;
18     } data[]; //數據,這個數據不只僅包括key對應的value,還有key、CAS、後綴等等數據也存在此,因此它有4部分「拼」成:CAS(可選),KEY,後綴,VALUE。
19     /* if it_flags & ITEM_CAS we have 8 bytes CAS */
20     /* then null-terminated key */
21     /* then " flags length\r\n" (no terminating null) */
22     /* then data with terminating \r\n (no terminating null; it's binary!) */
23 } item;

 

c)head變量和tail變量

 使用內存保存數據總會有滿的狀況,滿就得淘汰,而memcached中的淘汰機制是LRU(最近最少使用算法 ),因此每一個slabclass都保存着一個LRU隊列,而head[i]和tail[i]則就是id爲i的slabclass LRU隊列的頭部和尾部,尾部的item是最應該淘汰的項,也就是最近最少使用的項。

3)下面結合下面的結構圖對memcached內存分配的模型進行解說:

a)初始化slabclass數組,每一個元素slabclass[i]都是不一樣size的slabclass。

b)每開闢一個新的slab,都會根據所在的slabclass的size來分割chunk,分割完chunk以後,把chunk空間初始化成一個個free item,並插入到slot鏈表中。

c)咱們每使用一個free item都會從slot鏈表中刪除掉並插入到LRU鏈表相應的位置。

d)每當一個used item被訪問的時候都會更新它在LRU鏈表中的位置,以保證LRU鏈表從尾到頭淘汰的權重是由高到低的。

e)會有另外一個叫「item爬蟲」的線程(之後會講到)慢慢地從LRU鏈表中去爬,把過時的item淘汰掉而後從新插入到slot鏈表中(但這種方式並不實時,並不會一過時就回收)。

f)當咱們要進行內存分配時,例如一個SET命令,它的通常步驟是:

  1. 計算出要保存的數據的大小,而後選擇相應的slabclass進入下面處理:
  2. 首先,從相應的slabclass LRU鏈表的尾部開始,嘗試找幾回(默認是5次),看看有沒有過時的item(雖然有item爬蟲線程在幫忙查找,但這裏分配的時候,程序仍是會嘗試一下本身找,本身臨時充當牛爬蟲的角色),若是有就利用這個過時的item空間。
  3. 若是沒找到過時的,則嘗試去slot鏈表中拿空閒的free item。
  4. 若是slot鏈表中沒有空閒的free item了,嘗試申請內存,開闢一塊新的slab,開闢成功後,slot鏈表就又有可用的free item了。
  5. 若是開不了新的slab那說明內存都已經滿了,用完了,只能淘汰,因此用LRU鏈表尾部找出一個item淘汰之,並做爲free item返回。

 

 代碼實現

若是是SET命令最終會來到item_alloc函數執行內存分配的工做,咱們看下它的代碼:

 

  1 item *item_alloc(char *key, size_t nkey, int flags, rel_time_t exptime, int nbytes) {
  2     item *it;
  3     it = do_item_alloc(key, nkey, flags, exptime, nbytes, 0); //調用do_item_alloc
  4     return it;
  5 }
  6 /**
  7 item分配
  8 把這個函數弄清楚,基本就把memcached內存管理機制大致弄清楚了。
  9 */
 10 item *do_item_alloc(char *key, const size_t nkey, const int flags,
 11                     const rel_time_t exptime, const int nbytes,
 12                     const uint32_t cur_hv) {
 13     uint8_t nsuffix;
 14     item *it = NULL;
 15     char suffix[40];
 16     size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix); //item總大小
 17     if (settings.use_cas) {
 18         ntotal += sizeof(uint64_t); //若是有用到cas 那麼item大小還要加上unit64_t的size
 19     }
 20     unsigned int id = slabs_clsid(ntotal); //根據item大小,找到適合的slabclass
 21     if (id == 0)
 22         return 0;
 23     mutex_lock(&cache_lock); //cache鎖
 24     /* do a quick check if we have any expired items in the tail.. */
 25     /* 準備分配新的item了,隨便快速瞄一下lru鏈表末尾有沒有過時item,有的話就用過時的空間 */
 26     int tries = 5;
 27     int tried_alloc = 0;
 28     item *search;
 29     void *hold_lock = NULL;
 30     rel_time_t oldest_live = settings.oldest_live;
 31     search = tails[id]; //這個tails是一個全局變量,tails[xx]是id爲xx的slabclass lru鏈表的尾部
 32     //從LRU鏈表尾部(就是最久沒使用過的item)開始往前找
 33     for (; tries > 0 && search != NULL; tries--, search=search->prev) {
 34         if (search->nbytes == 0 && search->nkey == 0 && search->it_flags == 1) {
 35             /* We are a crawler, ignore it. */
 36             /*
 37                 這裏註釋意思是說咱們如今是以爬蟲的身份來爬出過時的空間,
 38                 像爬到這種異常的item,就別管了,不是爬蟲要作的事,不要就好了。
 39              */
 40             tries++;
 41             continue;
 42         }
 43         /**
 44         你會看到不少地方有下面這個hv,在這先簡單說下,也可先略過,其實它是對item的一個hash,獲得hv值,這個hv主要有兩個
 45         做用:
 46         1)用於hash表保存item,經過hv計算出哈希表中的桶號
 47         2)用於item lock表中鎖住item,經過hv計算出應該用item lock表中哪一個鎖對當前item進行加鎖
 48         這二者都涉及到一個粒度問題,不可能保證每一個不同的key的hv不會相同,全部hash方法均可能
 49         出現衝突。
 50         因此hash表中用鏈表的方式處理衝突的item,而item lock表中會多個item共享一個鎖,或者說
 51         多個桶共享一個鎖。
 52         */
 53         uint32_t hv = hash(ITEM_key(search), search->nkey);
 54         /* Attempt to hash item lock the "search" item. If locked, no
 55          * other callers can incr the refcount
 56          */
 57         /* Don't accidentally grab ourselves, or bail if we can't quicklock */
 58          /**
 59          嘗試去鎖住當前item。
 60          */
 61         if (hv == cur_hv || (hold_lock = item_trylock(hv)) == NULL)
 62             continue;
 63         if (refcount_incr(&search->refcount) != 2) {
 64             refcount_decr(&search->refcount);
 65             /* Old rare bug could cause a refcount leak. We haven't seen
 66              * it in years, but we leave this code in to prevent failures
 67              * just in case
 68             沒看懂這裏的意思.....
 69              */
 70             if (settings.tail_repair_time &&
 71                     search->time + settings.tail_repair_time < current_time) {
 72                 itemstats[id].tailrepairs++;
 73                 search->refcount = 1;
 74                 do_item_unlink_nolock(search, hv);
 75             }
 76             if (hold_lock)
 77                 item_trylock_unlock(hold_lock);
 78             continue;
 79         }
 80         /* Expired or flushed */
 81         //超時了...
 82         if ((search->exptime != 0 && search->exptime < current_time)
 83             || (search->time <= oldest_live && oldest_live <= current_time)) {
 84             itemstats[id].reclaimed++;
 85             if ((search->it_flags & ITEM_FETCHED) == 0) {
 86                 itemstats[id].expired_unfetched++;
 87             }
 88             it = search; //拿下空間
 89             slabs_adjust_mem_requested(it->slabs_clsid, ITEM_ntotal(it), ntotal); //更新統計數據
 90             /**
 91             什麼是link,在這簡單說下,就是把item加到哈希表和LRU鏈表的過程。詳見items::do_item_link函數 這裏把item舊的link取消掉,當前函數do_item_alloc的工做只是拿空間,而日後可知道拿到item空間後會對這塊item進行「link」工做,而這裏這塊item空間是舊的item超時而後拿來用的,因此先把它unlink掉
 92             */
 93             do_item_unlink_nolock(it, hv);
 94             /* Initialize the item block: */
 95             it->slabs_clsid = 0;
 96         } else if ((it = slabs_alloc(ntotal, id)) == NULL) {/*若是沒有找到超時的item,則
 97                 調用slabs_alloc分配空間,詳見slabs_alloc
 98                 若是slabs_alloc分配空間失敗,即返回NULL,則往下走,下面的代碼是
 99                 把LRU列表最後一個給淘汰,即便item沒有過時。
100                 這裏通常是可用內存已經滿了,須要按LRU進行淘汰的時候。
101             */
102             tried_alloc = 1; //標記一下,表示有進入此分支,表示有嘗試過調用slabs_alloc去分配新的空間。
103             //記下被淘汰item的信息,像咱們使用memcached常常會查看的evicted_time就是在這裏賦值啦!
104             if (settings.evict_to_free == 0) {
105                 itemstats[id].outofmemory++;
106             } else {
107                 itemstats[id].evicted++;
108                 itemstats[id].evicted_time = current_time - search->time; //被淘汰的item距離上次使用多長時間了
109                 if (search->exptime != 0)
110                     itemstats[id].evicted_nonzero++;
111                 if ((search->it_flags & ITEM_FETCHED) == 0) {
112                     itemstats[id].evicted_unfetched++;
113                 }
114                 it = search;
115                 slabs_adjust_mem_requested(it->slabs_clsid, ITEM_ntotal(it), ntotal);//更新統計數據
116                 do_item_unlink_nolock(it, hv); //從哈希表和LRU鏈表中刪掉
117                 /* Initialize the item block: */
118                 it->slabs_clsid = 0;
119                 /*
120                  在這也能夠先略過下面的邏輯
121                  若是當前slabclass有item被淘汰掉了,說明可用內存都滿了,再也沒有
122                  slab可分配了,
123                  而若是 slab_automove=2 (默認是1),這樣會致使angry模式,
124                  就是隻要分配失敗了,就立刻進行slab重分配:把別的slabclass空間犧牲
125                  掉一些,立刻給如今的slabclass分配空間,而不會合理地根據淘汰統計
126                  數據來分析要怎麼重分配(slab_automove = 1則會)。
127                  */
128                 if (settings.slab_automove == 2)
129                     slabs_reassign(-1, id);
130             }
131         }
132         refcount_decr(&search->refcount);
133         /* If hash values were equal, we don't grab a second lock */
134         if (hold_lock)
135             item_trylock_unlock(hold_lock);
136         break;
137     }
138     /**
139     若是上面的for循環裏面沒有找到空間,而且沒有進入過else if ((it = slabs_alloc(ntotal, id)) == NULL)這個分支沒有 嘗試調slabs_alloc分配空間(有這種可能性),那麼,下面這行代碼就是再嘗試分配。
140     你會以爲上面那個循環寫得特糾結,邏輯不清,估計你也看醉了。其實整個分配原則是這樣子:
141     1)先從LRU鏈表找下看看有沒有剛好過時的空間,有的話就用這個空間。
142     2)若是沒有過時的空間,就分配新的空間。
143     3)若是分配新的空間失敗,那麼每每是內存都用光了,則從LRU鏈表中把最舊的即便沒過時的item淘汰掉,空間分給新的item用。
144     問題是:這個從「LRU鏈表找到的item」是一個不肯定的東西,有可能這個item數據異常,有可能這個item因爲與別的item共用鎖的桶號
145     這個桶被鎖住了,因此總之各類緣由這個item此刻不必定可用,所以用了一個循環嘗試找幾回(上面是5)。
146     因此邏輯是:
147     1)我先找5次LRU看看有沒有可用的過時的item,有就用它。(for循環5次)
148     2)5次沒有找到可用的過時的item,那我分配新的。
149     3)分配新的不成功,那我再找5次看看有沒有可用的雖然沒過時的item,淘汰它,把空間給新的item用。(for循環5次)
150     那麼這裏有個問題,若是代碼要寫得邏輯清晰一點,我得寫兩個for循環,一個是爲了第2)步前「找可用的過時的」item,
151     一個是第2)步不成功後「找可用的用來淘汰的」空間。並且有重複的邏輯「找到可用的」,因此memcached做者就合在一塊兒了,
152     而後只能把第2)步也塞到for循環裏面,確實挺尷尬的。。。估計memcached做者也寫得很糾結。。。
153     因此就頗有可能出現5次都沒找到可用的空間,都沒進入過elseif那個分支就被continue掉了,爲了記下有沒有進過elseif
154     分支就挫挫地用一個tried_alloc變量來作記號。。
155     */
156     if (!tried_alloc && (tries == 0 || search == NULL))
157         it = slabs_alloc(ntotal, id);
158     if (it == NULL) {
159         itemstats[id].outofmemory++;
160         mutex_unlock(&cache_lock);
161         return NULL; //沒錯!會有分配新空間不成功,並且嘗試5次淘汰舊的item也沒成功的時候,只能返回NULL。。
162     }
163     assert(it->slabs_clsid == 0);
164     assert(it != heads[id]);
165     //來到這裏,說明item分配成功,下面主要是一些初始化工做。
166     /* Item initialization can happen outside of the lock; the item's already
167      * been removed from the slab LRU.
168      */
169     it->refcount = 1; /* the caller will have a reference */
170     mutex_unlock(&cache_lock);
171     it->next = it->prev = it->h_next = 0;
172     it->slabs_clsid = id;
173     DEBUG_REFCNT(it, '*');
174     it->it_flags = settings.use_cas ? ITEM_CAS : 0;
175     it->nkey = nkey;
176     it->nbytes = nbytes;
177     memcpy(ITEM_key(it), key, nkey);
178     it->exptime = exptime;
179     memcpy(ITEM_suffix(it), suffix, (size_t)nsuffix);
180     it->nsuffix = nsuffix;
181     return it;
182 }

上面的代碼基本的中心邏輯就是:

  1. 先從LRU鏈表找下看看有沒有剛好過時的空間,有的話就用這個空間。
  2. 若是沒有過時的空間,就分配新的空間。
  3. 若是分配新的空間失敗,那麼每每是內存都用光了,則從LRU鏈表中把最舊的即便沒過時的item淘汰掉,空間分給新的item用。

而第2步分配的新空間,經過slabs_alloc(ntotal, id)函數來完成,咱們再來解剖一下這個函數的實現:

 

 1 void *slabs_alloc(size_t size, unsigned int id) {
 2     void *ret;
 3     pthread_mutex_lock(&slabs_lock);
 4     ret = do_slabs_alloc(size, id);
 5     pthread_mutex_unlock(&slabs_lock);
 6     return ret;
 7 }
 8 /**
 9 根據item大小和slabsclass分配空間
10 */
11 static void *do_slabs_alloc(const size_t size, unsigned int id) {
12     slabclass_t *p;
13     void *ret = NULL;
14     item *it = NULL;
15     if (id < POWER_SMALLEST || id > power_largest) { //默認最大是200,最小是1
16         MEMCACHED_SLABS_ALLOCATE_FAILED(size, 0);
17         return NULL;
18     }
19     p = &slabclass[id]; //slabclass是一個全局變量,是各個slabclass對象數組,在這取得當前id對應的slabclass
20     assert(p->sl_curr == 0 || ((item *)p->slots)->slabs_clsid == 0);
21     /*
22     下面這個if的邏輯至關於:
23     若是p->sl_curr==0,即slots鏈表中沒有空閒的空間,則do_slabs_newslab分配新slab
24     若是p->sl_curr==0,且do_slabs_newslab分配新slab失敗,則進入if,ret = NULL,不然進入下面的elseif
25     */
26     if (! (p->sl_curr != 0 || do_slabs_newslab(id) != 0)) {
27         /* We don't have more memory available */
28         ret = NULL;
29     } else if (p->sl_curr != 0) { //若是進入此分支是由於slots鏈表中還有空閒的空間
30         /* return off our freelist */
31         //把空閒的item分配出去
32         it = (item *)p->slots;
33         p->slots = it->next;
34         if (it->next) it->next->prev = 0;
35         p->sl_curr--;
36         ret = (void *)it;
37     }
38     if (ret) {
39         p->requested += size; //分配成功,記下已分配的字節數
40         MEMCACHED_SLABS_ALLOCATE(size, id, p->size, ret);
41     } else {
42         MEMCACHED_SLABS_ALLOCATE_FAILED(size, id);
43     }
44     return ret;
45 }

do_slabs_alloc函數作的事情就是先從slot中找,沒有可用的item的話就調用do_slabs_newslab函數開闢新的slab,固然若是開闢失敗就返回NULL告訴上層說,已經滿了,沒新slab能夠開了。咱們進去看看do_slabs_newslab是怎麼實現的:

 

 1 /**
 2 爲slabclass[id]分配新的slab,僅噹噹前的slabclass中slots沒有空閒的空間才調用
 3 此函數分配新的slab
 4 */
 5 static int do_slabs_newslab(const unsigned int id) {
 6     slabclass_t *p = &slabclass[id];
 7     int len = settings.slab_reassign ? settings.item_size_max
 8         : p->size * p->perslab; //先判斷是否開啓了自定義slab大小,若是沒有就按默認的,即p->size*p->perslab(<=1M)
 9     char *ptr;
10     /**
11     下面if的邏輯是:
12         若是內存超出了限制,分配失敗進入if,返回0
13         不然調用grow_slab_list檢查是否要增大slab_list的大小
14             若是在grow_slab_list返回失敗,則不繼續分配空間,進入if,返回0
15             不然分配空間memory_allocate,若是分配失敗,一樣進入if,返回0;
16     */
17     if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) ||
18         (grow_slab_list(id) == 0) ||
19         ((ptr = memory_allocate((size_t)len)) == 0)) {
20         MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
21         return 0;
22     }
23     memset(ptr, 0, (size_t)len); //清乾淨內存空間
24     split_slab_page_into_freelist(ptr, id); //把新申請的slab放到slots中去
25     p->slab_list[p->slabs++] = ptr; //把新的slab加到slab_list數組中
26     mem_malloced += len; //記下已分配的空間大小
27     MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);
28     return 1;
29 }
30 //附上grow_slab_list函數的說明
31 static int grow_slab_list (const unsigned int id) {
32     slabclass_t *p = &slabclass[id];
33     /**
34         p->slab_list是一個空間大小固定的數組,是數組!而list_size是這個數組分配的空間。
35         p->slabs表明已經分配出去的slabs數
36         而p->list_size表明能夠用多少個slabs數
37         因此當slabs等於list_size的時候表明這個slab_list已經滿了,得增大空間。
38     */
39     if (p->slabs == p->list_size) {
40         size_t new_size = (p->list_size != 0) ? p->list_size * 2 : 16;
41         void *new_list = realloc(p->slab_list, new_size * sizeof(void *)); //
42         if (new_list == 0) return 0;
43         p->list_size = new_size;
44         p->slab_list = new_list;
45     }
46     return 1;
47 }
48 /**
49 分配內存空間
50 */
51 static void *memory_allocate(size_t size) {
52     void *ret;
53     /**
54     有兩種分配策略
55     1)若是是開啓了內存預分配策略,則只須要從預分配好的內存塊那裏割一塊出來。即進入下面的else分支
56     2)若是沒有開啓預分配,則malloc分配內存
57     關於預分配詳見 slabs_init
58     */
59     if (mem_base == NULL) {
60         ret = malloc(size); //在這裏終於見到咱們熟悉的malloc函數!!
61     } else {
62     //省略預分配邏輯
63     }
64     return ret;
65 }

do_slabs_newslab最終會調用咱們熟悉的malloc函數申請slab內存空間,申請成功後執行split_slab_page_into_freelist函數把slab割成一塊塊chunk,並把chunk初始化成free item並加到slot中:

 1 /**
 2 把整個slab打散成一個個(也叫chunk)放到相應的slots鏈表中
 3 */
 4 static void split_slab_page_into_freelist(char *ptr, const unsigned int id) {
 5     slabclass_t *p = &slabclass[id];
 6     int x;
 7     for (x = 0; x < p->perslab; x++) {
 8         do_slabs_free(ptr, 0, id); //這個函數主要做用是讓當前item空間可用,即加到slots鏈表中。
 9         ptr += p->size;
10     }
11 }
12 /**
13 這個函數的命名雖然叫do_slabs_free,聽上去好像是釋放空間,其實質是把空間變成可用。
14 怎樣的空間纔算可用?就是加到當前slabclass的slots鏈表中而已。
15 因此新申請的slab也會調用這個函數,讓整個slab變爲可用。
16 ps: 這個命名應該有歷史緣由,由於之前的memcached版本slots鏈表保存的是回收的item空間,而
17 如今保存的是全部可用的item空間。
18 */
19 static void do_slabs_free(void *ptr, const size_t size, unsigned int id) {
20     slabclass_t *p;
21     item *it;
22     assert(((item *)ptr)->slabs_clsid == 0);
23     assert(id >= POWER_SMALLEST && id <= power_largest);
24     if (id < POWER_SMALLEST || id > power_largest)
25         return;
26     MEMCACHED_SLABS_FREE(size, id, ptr);
27     p = &slabclass[id];
28     it = (item *)ptr;
29     it->it_flags |= ITEM_SLABBED; //把item標記爲slabbed狀態
30     it->prev = 0;
31     it->next = p->slots; //插入到slots鏈表中
32     if (it->next) it->next->prev = it;
33     p->slots = it;
34     p->sl_curr++; //空閒item數加1
35     p->requested -= size;
36     return;
37 }

  至此,咱們已經把「分配」item空間的工做說完了。

相關文章
相關標籤/搜索