久久久人人人婷婷色东京热,久久精品aⅴ无码中文字字幕重口

您的位置:首頁 > 國(guó)內(nèi) >

深度解析 slab 內(nèi)存池回收內(nèi)存以及銷毀全流程

2023-05-26 11:38:46 來源:博客園

在上篇文章 《深入理解 slab cache 內(nèi)存分配全鏈路實(shí)現(xiàn)》 中,筆者詳細(xì)地為大家介紹了 slab cache 進(jìn)行內(nèi)存分配的整個(gè)鏈路實(shí)現(xiàn),本文我們就來到了 slab cache 最后的一部分內(nèi)容了,當(dāng)申請(qǐng)的內(nèi)存使用完畢之后,下面就該釋放內(nèi)存了。

在接下來的內(nèi)容中,筆者為大家介紹一下內(nèi)核是如何將內(nèi)存塊釋放回 slab cache 的。我們還是先從 slab cache 釋放內(nèi)存的內(nèi)核 API 開始聊起~~~


【資料圖】

內(nèi)核提供了 kmem_cache_free 函數(shù),用于將對(duì)象釋放回其所屬的 slab cache 中,參數(shù) x 表示我們要釋放的內(nèi)存塊(對(duì)象)的虛擬內(nèi)存地址,參數(shù) s 指向內(nèi)存塊所屬的 slab cache。

void kmem_cache_free(struct kmem_cache *s, void *x){    // 確保指定的是 slab cache : s 為對(duì)象真正所屬的 slab cache    s = cache_from_obj(s, x);    if (!s)        return;    // 將對(duì)象釋放會(huì) slab cache 中    slab_free(s, virt_to_head_page(x), x, NULL, 1, _RET_IP_);}
1. 內(nèi)存釋放之前的校驗(yàn)工作

在開始釋放內(nèi)存塊 x 之前,內(nèi)核需要首先通過 cache_from_obj 函數(shù)確認(rèn)內(nèi)存塊 x 是否真正屬于我們指定的 slab cache。不能將內(nèi)存塊釋放到其他的 slab cache 中。

隨后在 virt_to_head_page 函數(shù)中通過內(nèi)存塊的虛擬內(nèi)存地址 x 找到其所在的物理內(nèi)存頁 page。然后調(diào)用 slab_free 將內(nèi)存塊釋放回 slab cache 中。

通過虛擬內(nèi)存地址尋找物理內(nèi)存頁 page 的過程涉及到的背景知識(shí)比較復(fù)雜,這個(gè)筆者后面會(huì)單獨(dú)拎出來介紹,這里大家只需要簡(jiǎn)單了解 virt_to_head_page 函數(shù)的作用即可。

static inline struct kmem_cache *cache_from_obj(struct kmem_cache *s, void *x){    struct kmem_cache *cachep;    // 通過對(duì)象的虛擬內(nèi)存地址 x 找到對(duì)象所屬的 slab cache    cachep = virt_to_cache(x);    // 校驗(yàn)指定的 slab cache : s 是否是對(duì)象真正所屬的 slab cache : cachep    WARN_ONCE(cachep && !slab_equal_or_root(cachep, s),          "%s: Wrong slab cache. %s but object is from %s\n",          __func__, s->name, cachep->name);    return cachep;}

virt_to_cache 函數(shù)首先會(huì)通過釋放對(duì)象的虛擬內(nèi)存地址找到其所在的物理內(nèi)存頁 page,然后通過 struct page 結(jié)構(gòu)中的 slab_cache 指針找到 page 所屬的 slab cache。

static inline struct kmem_cache *virt_to_cache(const void *obj){    struct page *page;    // 根據(jù)對(duì)象的虛擬內(nèi)存地址 *obj 找到其所在的內(nèi)存頁 page    // 如果 slub 背后是多個(gè)內(nèi)存頁(復(fù)合頁),則返回復(fù)合頁的首頁 head page    page = virt_to_head_page(obj);    if (WARN_ONCE(!PageSlab(page), "%s: Object is not a Slab page!\n",                    __func__))        return NULL;    // 通過 page 結(jié)構(gòu)中的 slab_cache 屬性找到其所屬的 slub    return page->slab_cache;}
2. slab cache 在快速路徑下回收內(nèi)存
static __always_inline void slab_free(struct kmem_cache *s, struct page *page,                      void *head, void *tail, int cnt,                      unsigned long addr){    if (slab_free_freelist_hook(s, &head, &tail))        do_slab_free(s, page, head, tail, cnt, addr);}

slab cache 回收內(nèi)存相關(guān)的邏輯封裝在 do_slab_free 函數(shù)中:

static __always_inline void do_slab_free(struct kmem_cache *s,                struct page *page, void *head, void *tail,                int cnt, unsigned long addr)
參數(shù) kmem_cache *s 表示釋放對(duì)象所在的 slab cache,指定我們要將對(duì)象釋放到哪里。參數(shù) page 表示釋放對(duì)象所在的 slab,slab 在內(nèi)核中使用 struct page 結(jié)構(gòu)來表示。參數(shù) head 指向釋放對(duì)象的虛擬內(nèi)存地址(起始內(nèi)存地址)。該函數(shù)支持向 slab cache 批量的釋放多個(gè)對(duì)象,參數(shù) tail 指向批量釋放對(duì)象中最后一個(gè)對(duì)象的虛擬內(nèi)存地址。參數(shù) cnt 表示釋放對(duì)象的個(gè)數(shù),也是用于批量釋放對(duì)象參數(shù) addr 用于 slab 調(diào)試,這里我們不需要關(guān)心。

slab cache 針對(duì)內(nèi)存的回收流程其實(shí)和我們?cè)谏掀恼?《深入理解 slab cache 內(nèi)存分配全鏈路實(shí)現(xiàn)》 中介紹的 slab cache 內(nèi)存分配流程是相似的。

內(nèi)存回收總體也是分為快速路徑 fastpath 和慢速路徑 slow path,在 do_slab_free 函數(shù)中內(nèi)核會(huì)首先嘗試 fastpath 的回收流程。

如果釋放對(duì)象所在的 slab 剛好是 slab cache 在本地 cpu 緩存 kmem_cache_cpu->page 緩存的 slab,那么內(nèi)核就會(huì)直接將對(duì)象釋放回緩存 slab 中。

static __always_inline void do_slab_free(struct kmem_cache *s,                struct page *page, void *head, void *tail,                int cnt, unsigned long addr){    void *tail_obj = tail ? : head;    struct kmem_cache_cpu *c;    // slub 中對(duì)象分配與釋放流程的全局事務(wù) id    // 既可以用來標(biāo)識(shí)同一個(gè)分配或者釋放的事務(wù)流程,也可以用來標(biāo)識(shí)區(qū)分所屬 cpu 本地緩存    unsigned long tid;redo:    // 接下來我們需要獲取 slab cache 的 cpu 本地緩存    // 這里的 do..while 循環(huán)是要保證獲取到的 cpu 本地緩存 c 是屬于執(zhí)行進(jìn)程的當(dāng)前 cpu    // 因?yàn)檫M(jìn)程可能由于搶占或者中斷的原因被調(diào)度到其他 cpu 上執(zhí)行,所需需要確保兩者的 tid 是否一致    do {        // 獲取執(zhí)行當(dāng)前進(jìn)程的 cpu 中的 tid 字段        tid = this_cpu_read(s->cpu_slab->tid);        // 獲取 cpu 本地緩存 cpu_slab        c = raw_cpu_ptr(s->cpu_slab);        // 如果兩者的 tid 字段不一致,說明進(jìn)程已經(jīng)被調(diào)度到其他 cpu 上了        // 需要再次獲取正確的 cpu 本地緩存    } while (IS_ENABLED(CONFIG_PREEMPT) &&         unlikely(tid != READ_ONCE(c->tid)));    // 如果釋放對(duì)象所屬的 slub (page 表示)正好是 cpu 本地緩存的 slub    // 那么直接將對(duì)象釋放到 cpu 緩存的 slub 中即可,這里就是快速釋放路徑 fastpath    if (likely(page == c->page)) {        // 將對(duì)象釋放至 cpu 本地緩存 freelist 中的頭結(jié)點(diǎn)處        // 釋放對(duì)象中的 freepointer 指向原來的 c->freelist        set_freepointer(s, tail_obj, c->freelist);        // cas 更新 cpu 本地緩存 s->cpu_slab 中的 freelist,以及 tid        if (unlikely(!this_cpu_cmpxchg_double(                s->cpu_slab->freelist, s->cpu_slab->tid,                c->freelist, tid,                head, next_tid(tid)))) {            note_cmpxchg_failure("slab_free", s, tid);            goto redo;        }        stat(s, FREE_FASTPATH);    } else        // 如果當(dāng)前釋放對(duì)象并不在 cpu 本地緩存中,那么就進(jìn)入慢速釋放路徑 slowpath        __slab_free(s, page, head, tail_obj, cnt, addr);}

既然是快速路徑釋放,那么在 do_slab_free 函數(shù)的開始首先就獲取 slab cache 的本地 cpu 緩存結(jié)構(gòu) kmem_cache_cpu,為了保證我們獲取到的 cpu 本地緩存結(jié)構(gòu)與運(yùn)行當(dāng)前進(jìn)程所在的 cpu 是相符的,所以這里還是需要在 do .... while循環(huán)內(nèi)判斷兩者的 tid。這一點(diǎn),筆者已經(jīng)在本文之前的內(nèi)容里多次強(qiáng)調(diào)過了,這里不在贅述。

內(nèi)核在確保已經(jīng)獲取了正確的 kmem_cache_cpu 結(jié)構(gòu)之后,就會(huì)馬上判斷該釋放對(duì)象所在的 slab 是否正是 slab cache 本地 cpu 緩存了的 slab —— page == c->page。

如果是的話,直接將對(duì)象釋放回緩存 slab 中,調(diào)整 kmem_cache_cpu->freelist 指向剛剛釋放的對(duì)象,調(diào)整釋放對(duì)象的 freepointer 指針指向原來的 kmem_cache_cpu->freelist 。

如果當(dāng)前釋放對(duì)象并不在 slab cache 的本地 cpu 緩存中,那么就會(huì)進(jìn)入慢速路徑 slowpath 釋放內(nèi)存。

3. slab cache 在慢速路徑下回收內(nèi)存

slab cache 在慢速路徑下回收內(nèi)存的邏輯比較復(fù)雜,因?yàn)檫@里涉及到很多的場(chǎng)景,需要改變釋放對(duì)象所屬 slab 在 slab cache 架構(gòu)中的位置。

下面筆者會(huì)帶大家一一梳理這些場(chǎng)景,我們一起來看一下內(nèi)核在這些不同場(chǎng)景中到底是如何處理的?

在開始閱讀本小節(jié)的內(nèi)容之前,建議大家先回顧下 《細(xì)節(jié)拉滿,80 張圖帶你一步一步推演 slab 內(nèi)存池的設(shè)計(jì)與實(shí)現(xiàn)》 一文中的 ”8. slab 內(nèi)存釋放原理“ 小節(jié)。

在將對(duì)象釋放回對(duì)應(yīng)的 slab 中之前,內(nèi)核需要首先清理一下對(duì)象所占的內(nèi)存,重新填充對(duì)象的內(nèi)存布局恢復(fù)到初始未使用狀態(tài)。因?yàn)閷?duì)象所占的內(nèi)存此時(shí)包含了很多已經(jīng)被使用過的無用信息。這項(xiàng)工作內(nèi)核在 free_debug_processing 函數(shù)中完成。

在將對(duì)象所在內(nèi)存恢復(fù)到初始狀態(tài)之后,內(nèi)核首先會(huì)將對(duì)象直接釋放回其所屬的 slab 中,并調(diào)整 slab 結(jié)構(gòu) page 的相關(guān)屬性。

接下來就到復(fù)雜的處理部分了,內(nèi)核會(huì)在這里處理多種場(chǎng)景,并改變 slab 在 slab cache 架構(gòu)中的位置。

如果 slab 本來就在 slab cache 本地 cpu 緩存 kmem_cache_cpu->partial 鏈表中,那么對(duì)象在釋放之后,slab 的位置不做任何改變。

如果 slab 不在 kmem_cache_cpu->partial 鏈表中,并且該 slab 由于對(duì)象的釋放剛好由一個(gè) full slab 變?yōu)榱艘粋€(gè) partial slab,為了利用局部性的優(yōu)勢(shì),內(nèi)核需要將該 slab 插入到 kmem_cache_cpu->partial 鏈表中。

如果 slab 不在 kmem_cache_cpu->partial 鏈表中,并且該 slab 由于對(duì)象的釋放剛好由一個(gè) partial slab 變?yōu)榱艘粋€(gè) empty slab,說明該 slab 并不是很活躍,內(nèi)核會(huì)將該 slab 放入對(duì)應(yīng) NUMA 節(jié)點(diǎn)緩存 kmem_cache_node->partial 鏈表中,刀槍入庫,馬放南山。如果不符合第 2, 3 種場(chǎng)景,但是 slab 本來就在對(duì)應(yīng)的 NUMA 節(jié)點(diǎn)緩存 kmem_cache_node->partial 鏈表中,那么對(duì)象在釋放之后,slab 的位置不做任何改變。

下面我們就到內(nèi)核的源碼實(shí)現(xiàn)中,來一一驗(yàn)證這四種慢速釋放場(chǎng)景。

static void __slab_free(struct kmem_cache *s, struct page *page,            void *head, void *tail, int cnt,            unsigned long addr){    // 用于指向?qū)ο筢尫呕?slub 之前,slub 的 freelist    void *prior;    // 對(duì)象所屬的 slub 之前是否在本地 cpu 緩存 partial 鏈表中    int was_frozen;    // 后續(xù)會(huì)對(duì) slub 對(duì)應(yīng)的 page 結(jié)構(gòu)相關(guān)屬性進(jìn)行修改    // 修改后的屬性會(huì)臨時(shí)保存在 new 中,后面通過 cas 替換    struct page new;    unsigned long counters;    struct kmem_cache_node *n = NULL;    stat(s, FREE_SLOWPATH);    // free_debug_processing 中會(huì)調(diào)用 init_object,清理對(duì)象內(nèi)存無用信息,重新恢復(fù)對(duì)象內(nèi)存布局到初始狀態(tài)    if (kmem_cache_debug(s) &&     !free_debug_processing(s, page, head, tail, cnt, addr))        return;    do {        // 獲取 slub 中的空閑對(duì)象列表,prior = null 表示此時(shí) slub 是一個(gè) full slub,意思就是該 slub 中的對(duì)象已經(jīng)全部被分配出去了        prior = page->freelist;        counters = page->counters;        // 將釋放的對(duì)象插入到 freelist 的頭部,將對(duì)象釋放回 slub        // 將 tail 對(duì)象的 freepointer 設(shè)置為 prior        set_freepointer(s, tail, prior);        // 將原有 slab 的相應(yīng)屬性賦值給 new page        new.counters = counters;        // 獲取原來 slub 中的 frozen 狀態(tài),是否在 cpu 緩存 partial 鏈表中        was_frozen = new.frozen;        // inuse 表示 slub 已經(jīng)分配出去的對(duì)象個(gè)數(shù),這里是釋放 cnt 個(gè)對(duì)象,所以 inuse 要減去 cnt        new.inuse -= cnt;        // !new.inuse 表示此時(shí) slub 變?yōu)榱艘粋€(gè) empty slub,意思就是該 slub 中的對(duì)象還沒有分配出去,全部在 slub 中        // !prior 表示由于本次對(duì)象的釋放,slub 剛剛從一個(gè) full slub 變成了一個(gè) partial slub (意思就是該 slub 中的對(duì)象部分分配出去了,部分沒有分配出去)        // !was_frozen 表示該 slub 不在 cpu 本地緩存中        if ((!new.inuse || !prior) && !was_frozen) {            // 注意:進(jìn)入該分支的 slub 之前都不在 cpu 本地緩存中            // 如果配置了 CONFIG_SLUB_CPU_PARTIAL 選項(xiàng),那么表示 cpu 本地緩存 kmem_cache_cpu 結(jié)構(gòu)中包含 partial 列表,用于 cpu 緩存部分分配的 slub            if (kmem_cache_has_cpu_partial(s) && !prior) {                // 如果 kmem_cache_cpu 包含 partial 列表并且該 slub 剛剛由 full slub 變?yōu)?partial slub                // 凍結(jié)該 slub,后續(xù)會(huì)將該 slub 插入到 kmem_cache_cpu 的 partial 列表中                new.frozen = 1;            } else {                 // 如果 kmem_cache_cpu 中沒有配置 partial 列表,那么直接釋放至 kmem_cache_node 中                // 或者該 slub 由一個(gè) partial slub 變?yōu)榱?empty slub,調(diào)整 slub 的位置到 kmem_cache_node->partial 鏈表中                n = get_node(s, page_to_nid(page));                // 后續(xù)會(huì)操作 kmem_cache_node 中的 partial 列表,所以這里需要獲取 list_lock                spin_lock_irqsave(&n->list_lock, flags);            }        }        // cas 更新 slub 中的 freelist 以及 counters    } while (!cmpxchg_double_slab(s, page,        prior, counters,        head, new.counters,        "__slab_free"));    // 該分支要處理的場(chǎng)景是:    // 1: 該 slub 原來不在 cpu 本地緩存的 partial 列表中(!was_frozen),但是該 slub 剛剛從 full slub 變?yōu)榱?partial slub,需要放入 cpu-> partial 列表中    // 2: 該 slub 原來就在 cpu 本地緩存的 partial 列表中,直接將對(duì)象釋放回 slub 即可    if (likely(!n)) {        // 處理場(chǎng)景 1        if (new.frozen && !was_frozen) {            // 將 slub 插入到 kmem_cache_cpu 中的 partial 列表中            put_cpu_partial(s, page, 1);            stat(s, CPU_PARTIAL_FREE);        }                // 處理場(chǎng)景2,因?yàn)橹耙呀?jīng)通過 set_freepointer 將對(duì)象釋放回 slub 了,這里只需要記錄 slub 狀態(tài)即可        if (was_frozen)            stat(s, FREE_FROZEN);        return;    }        // 后續(xù)的邏輯就是處理需要將 slub 放入 kmem_cache_node 中的 partial 列表的情形    // 在將 slub 放入 node 緩存之前,需要判斷 node 緩存的 nr_partial 是否超過了指定閾值 min_partial(位于 kmem_cache 結(jié)構(gòu))    // nr_partial 表示 kmem_cache_node 中 partial 列表中緩存的 slub 個(gè)數(shù)    // min_partial 表示 slab cache 規(guī)定 kmem_cache_node 中 partial 列表可以容納的 slub 最大個(gè)數(shù)    // 如果 nr_partial 超過了最大閾值 min_partial,則不能放入 kmem_cache_node 里    if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))        // 如果 slub 變?yōu)榱艘粋€(gè) empty slub 并且 nr_partial 超過了最大閾值 min_partial        // 跳轉(zhuǎn)到 slab_empty 分支,將 slub 釋放回伙伴系統(tǒng)中        goto slab_empty;    // 如果 cpu 本地緩存中沒有配置 partial 列表并且 slub 剛剛從 full slub 變?yōu)?partial slub    // 則將 slub 插入到 kmem_cache_node 中    if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {        remove_full(s, n, page);        add_partial(n, page, DEACTIVATE_TO_TAIL);        stat(s, FREE_ADD_PARTIAL);    }    spin_unlock_irqrestore(&n->list_lock, flags);    // 剩下的情況均屬于 slub 原來就在 kmem_cache_node 中的 partial 列表中    // 直接將對(duì)象釋放回 slub 即可,無需改變 slub 的位置,直接返回    return;slab_empty:    // 該分支處理的場(chǎng)景是: slub 太多了,將 empty slub 釋放會(huì)伙伴系統(tǒng)    // 首先將 slub 從對(duì)應(yīng)的管理鏈表上刪除    if (prior) {        /*         * Slab on the partial list.         */        remove_partial(n, page);        stat(s, FREE_REMOVE_PARTIAL);    } else {        /* Slab must be on the full list */        remove_full(s, n, page);    }    spin_unlock_irqrestore(&n->list_lock, flags);    stat(s, FREE_SLAB);    // 釋放 slub 回伙伴系統(tǒng),底層調(diào)用 __free_pages 將 slub 所管理的所有 page 釋放回伙伴系統(tǒng)    discard_slab(s, page);}
3.1 直接釋放對(duì)象回 slab,調(diào)整 slab 相關(guān)屬性
static void __slab_free(struct kmem_cache *s, struct page *page,            void *head, void *tail, int cnt,            unsigned long addr){    // 后續(xù)會(huì)對(duì) slub 對(duì)應(yīng)的 page 結(jié)構(gòu)相關(guān)屬性進(jìn)行修改    // 修改后的屬性會(huì)臨時(shí)保存在 new 中,后面通過 cas 替換    struct page new;              ....... 省略 ..........    do {        prior = page->freelist;        counters = page->counters;        // 將對(duì)象直接釋放回 slab 中,調(diào)整 slab 的 freelist 指針,以及對(duì)象的 freepointer 指針        set_freepointer(s, tail, prior);        new.counters = counters;        // 獲取原來 slub 中的 frozen 狀態(tài),是否在 cpu 緩存 partial 中        was_frozen = new.frozen;        // inuse 表示 slub 已經(jīng)分配出去的對(duì)象個(gè)數(shù),這里是釋放 cnt 個(gè)對(duì)象,所以 inuse 要減去 cnt        new.inuse -= cnt;              ....... 省略 ..........        // cas 更新 slub 中的 freelist     } while (!cmpxchg_double_slab(s, page,        prior, counters,        head, new.counters,        "__slab_free")); .            ...... 省略 ..........}

這一部分的邏輯比較簡(jiǎn)單,在 __slab_free 內(nèi)存釋放流程的開始,內(nèi)核不管三七二十一,首先會(huì)將對(duì)象直接釋放回其所在的 slab 中。

當(dāng)對(duì)象被釋放回 slab 中之后,slab 結(jié)構(gòu)中的相應(yīng)屬于就需要做出相應(yīng)的調(diào)整,比如:

調(diào)整 page 結(jié)構(gòu)中的 freelist,它需要指向剛剛被釋放的對(duì)象。調(diào)整 page 結(jié)構(gòu)中的 inuse,inuse 表示 slab 中已經(jīng)被分配出去的對(duì)象個(gè)數(shù),此時(shí)對(duì)象已經(jīng)釋放回 slab 中,需要調(diào)整 inuse 字段。后續(xù)內(nèi)核會(huì)根據(jù)不同情況,調(diào)整 page 結(jié)構(gòu)的 frozen 屬性。

內(nèi)核會(huì)定義一個(gè)新的 page 結(jié)構(gòu) new,將原有 slab 的 page 結(jié)構(gòu)需要更新的上述屬性的新值,先一一復(fù)制給 new 的對(duì)應(yīng)屬性,最后通過 cmpxchg_double_slab 原子更新 slab 對(duì)應(yīng)的屬性。

struct page {        struct {    /*  slub 相關(guān)字段 */             ........ 省略 .........            // 指向 page 所屬的 slab cache            struct kmem_cache *slab_cache;            // 指向 slab 中第一個(gè)空閑對(duì)象            void *freelist;     /* first free object */            union {                unsigned long counters;                struct {            /* SLUB */                                 // slab 中已經(jīng)分配出去的對(duì)象                    unsigned inuse:16;                    // slab 中包含的對(duì)象總數(shù)                    unsigned objects:15;                    // 該 slab 是否在對(duì)應(yīng) slab cache 的本地 CPU 緩存中                    // frozen = 1 表示緩存再本地 cpu 緩存中                    unsigned frozen:1;                };            };        };}

按照正常的更新套路來說,我們?cè)诟略?slab 結(jié)構(gòu)中的 freelist,inuse,frozen 這三個(gè)屬性之前,首先需要將原有 slab 的這三個(gè)舊的屬性值一一賦值到臨時(shí)結(jié)構(gòu) new page 中,然后在 slab 結(jié)構(gòu)舊值的基礎(chǔ)上調(diào)整著三個(gè)屬性的新值,最后通過 cmpxchg_double_slab 將這三個(gè)屬性的新值原子地更新回 slab 中。

但是我們查看 __slab_free 的代碼發(fā)現(xiàn),內(nèi)核并不是這樣操作的,內(nèi)核只是將原有 slab 的 counter 屬性賦值給 new page,而原有 slab 中的 frozen,inuse 屬性并沒有賦值過去。

此時(shí) new page 結(jié)構(gòu)中的 frozen,inuse 屬性依然是上述 struct page 結(jié)構(gòu)中展示的初始值。

而內(nèi)核后續(xù)的操作就更加奇怪了,直接使用 new.frozen 來判斷原有 slab 是否在 slab cache 本地 cpu 的 partial 鏈表中,直接把 new.inuse 屬性當(dāng)做原有 slab 中已經(jīng)分配出去對(duì)象的個(gè)數(shù)。

而 new.frozen, new.inuse 是 page 結(jié)構(gòu)初始狀態(tài)的值,并不是原有 slab 結(jié)構(gòu)中的值,這樣做肯定不對(duì)啊,難道是內(nèi)核的一個(gè) bug ?

其實(shí)并不是,這是內(nèi)核非常騷的一個(gè)操作,這一點(diǎn)對(duì)于 Java 程序員來說很難理解。我們?cè)谧屑?xì)看一下 struct page 結(jié)構(gòu),就會(huì)發(fā)現(xiàn) counter 屬性和 inuse,frozen 屬性被定義在一個(gè) union 結(jié)構(gòu)體中。

union 結(jié)構(gòu)體中定義的字段全部共享一片內(nèi)存,union 結(jié)構(gòu)體的內(nèi)存占用由其中最大的屬性決定。而 struct 結(jié)構(gòu)體中的每個(gè)字段都是獨(dú)占一片內(nèi)存的。

由于 union 結(jié)構(gòu)體中各個(gè)字段都是共享一塊內(nèi)存,所以一個(gè)字段的改變就會(huì)影響其他字段的值,從另一方面來看,通過一個(gè)字段就可以將整個(gè) union 結(jié)構(gòu)占用的內(nèi)存塊拿出來。明白這些,我們?cè)诨仡^來看內(nèi)核的操作。

struct page {            union {                unsigned long counters;                struct {            /* SLUB */                                 // slab 中已經(jīng)分配出去的對(duì)象                    unsigned inuse:16;                    // slab 中包含的對(duì)象總數(shù)                    unsigned objects:15;                    // 該 slab 是否在對(duì)應(yīng) slab cache 的本地 CPU 緩存中                    // frozen = 1 表示緩存再本地 cpu 緩存中                    unsigned frozen:1;                };            };}

page 結(jié)構(gòu)中的 counters 是和 inuse,frozen 共用同一塊內(nèi)存的,內(nèi)核在 __slab_free 中將原有 slab 的 counters 屬性賦值給 new.counters 的一瞬間,counters 所在的內(nèi)存塊也就賦值到 new page 的 union 結(jié)構(gòu)中了。

而 inuse,frozen 屬性的值也在這個(gè)內(nèi)存塊中,所以原有 slab 中的 inuse,frozen 屬性也就跟著一起賦值到 new page 的對(duì)應(yīng)屬性中了。這樣一來,后續(xù)的邏輯處理也就通順了。

counters = page->counters;        new.counters = counters;        // 獲取原來 slub 中的 frozen 狀態(tài),是否在 cpu 緩存 partial 中        was_frozen = new.frozen;        // inuse 表示 slub 已經(jīng)分配出去的對(duì)象個(gè)數(shù),這里是釋放 cnt 個(gè)對(duì)象,所以 inuse 要減去 cnt        new.inuse -= cnt;

同樣的道理,我們?cè)賮砜磧?nèi)核 cmpxchg_double_slab 中的更新操作:

內(nèi)核明明在 do .... while循環(huán)中更新了 freelist,inuse,frozen 這三個(gè)屬性,而 counters 屬性只是讀取并沒有更新操作,那么為什么在 cmpxchg_double_slab 只是更新 page 結(jié)構(gòu)的 freelist 和 counters 呢?inuse,frozen 這兩個(gè)屬性又在哪里更新的呢?

do {             ....... 省略 ..........        // cas 更新 slub 中的 freelist     } while (!cmpxchg_double_slab(s, page,        prior, counters,        head, new.counters,        "__slab_free"));

我想大家現(xiàn)在一定能夠解釋這個(gè)問題了,由于 counters,inuse,frozen 共用一塊內(nèi)存,當(dāng) inuse,frozen 的值發(fā)生變化之后,雖然 counters 的值沒有發(fā)生變化,但是我們可以通過更新 counters 來將原有 slab 中的這塊內(nèi)存一起更新掉,這樣 inuse,frozen 的值也跟著被更新了。

由于 page 的 freelist 指針在 union 結(jié)構(gòu)體之外,所以需要在cmpxchg_double_slab 中單獨(dú)更新。

筆者曾經(jīng)為了想給大家解釋清楚 page->counters 這個(gè)屬性的作用,而翻遍了 slab 的所有源碼,發(fā)現(xiàn)內(nèi)核源碼中對(duì)于 page->counters 的使用都是只做簡(jiǎn)單的讀取,并不做改變,然后直接在更新,這個(gè)問題也困擾了筆者很久。

直到為大家寫這篇文章的時(shí)候,才頓悟。原來 page->counters 的作用只是為了指向 inuse,frozen 所在的內(nèi)存,方便在 cmpxchg_double_slab 中同時(shí)原子地更新這兩個(gè)屬性。

接下來的內(nèi)容就到了 slab cache 回收內(nèi)存最為復(fù)雜的環(huán)節(jié)了,大家需要多一些耐心,繼續(xù)跟著筆者的思路走下去,我們一起來看下內(nèi)核如何處理三種內(nèi)存慢速釋放的場(chǎng)景。

3.2 釋放對(duì)象所屬 slab 本來就在 cpu 緩存 partial 鏈表中

was_frozen 指向釋放對(duì)象所屬 slab 結(jié)構(gòu)中的 frozen 屬性,用來表示 slab 是否在 slab cache 的本地 cpu 緩存 partial 鏈表中。

was_frozen = new.frozen;

如果 was_frozen == true表示釋放對(duì)象所屬 slab 本來就在 kmem_cache_cpu->partial 鏈表中,內(nèi)核將對(duì)象直接釋放回 slab 中,slab 的原有位置不做改變。

下面我們看下 was_frozen == fasle也就是 slab 不在 kmem_cache_cpu->partial 鏈表中 的時(shí)候,內(nèi)核又是如何處理的 ?

3.3 釋放對(duì)象所屬 slab 從 full slab 變?yōu)榱?partial slab

如果釋放對(duì)象所屬 slab 原來是一個(gè) full slab,恰恰說明該 slab 擁有比較好的局部性,進(jìn)程經(jīng)常從該 slab 中分配對(duì)象,slab 十分活躍,才導(dǎo)致它變?yōu)榱艘粋€(gè) full slab

prior = page->freelist = null

隨著對(duì)象的釋放,該 slab 從一個(gè) full slab 變?yōu)榱?partial slab,內(nèi)核為了更好的利用該 slab 的局部性,所以需要將該 slab 插入到 slab cache 的本地 cpu 緩存 kmem_cache_cpu->partial 鏈表中。

if (kmem_cache_has_cpu_partial(s) && !prior) {                new.frozen = 1;        }         if (new.frozen && !was_frozen) {            // 將 slub 插入到 kmem_cache_cpu 中的 partial 列表中            put_cpu_partial(s, page, 1);            stat(s, CPU_PARTIAL_FREE);        }        

將 slab 插入到 kmem_cache_cpu->partial 鏈表的邏輯封裝在 put_cpu_partial 中,put_cpu_partial 函數(shù)最重要的一個(gè)考量邏輯是需要確保 kmem_cache_cpu->partial 鏈表中所有 slab 中包含的空閑對(duì)象總數(shù)不能超過 kmem_cache->cpu_partial 的限制。

struct kmem_cache {    // 限定 slab cache 在每個(gè) cpu 本地緩存 partial 鏈表中所有 slab 中空閑對(duì)象的總數(shù)    unsigned int cpu_partial;};

在釋放對(duì)象所在的 slab 插入到 kmem_cache_cpu->partial 鏈表之前,put_cpu_partial 函數(shù)需要判斷當(dāng)前 kmem_cache_cpu->partial 鏈表中包含的空閑對(duì)象總數(shù) pobjects 是否超過了 kmem_cache->cpu_partial 的限制。

如果超過了,則需要先將當(dāng)前 kmem_cache_cpu->partial 鏈表中所有的 slab 轉(zhuǎn)移到其對(duì)應(yīng)的 NUMA 節(jié)點(diǎn)緩存 kmem_cache_node->partial 鏈表中。轉(zhuǎn)移完成之后,在將釋放對(duì)象所屬的 slab 插入到 kmem_cache_cpu->partial 鏈表中。

static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain){// 只有配置了 CONFIG_SLUB_CPU_PARTIAL 選項(xiàng),kmem_cache_cpu 中才有會(huì) partial 列表#ifdef CONFIG_SLUB_CPU_PARTIAL    // 指向原有 kmem_cache_cpu 中的 partial 列表    struct page *oldpage;    // slub 所在管理列表中的 slub 個(gè)數(shù),這里的列表是指 partial 列表    int pages;    // slub 所在管理列表中的包含的空閑對(duì)象總數(shù),這里的列表是指 partial 列表    // 內(nèi)核會(huì)將列表總體的信息存放在列表首頁 page 的相關(guān)字段中    int pobjects;    // 禁止搶占    preempt_disable();    do {        pages = 0;        pobjects = 0;        // 獲取 slab cache 中原有的 cpu 本地緩存 partial 列表首頁        oldpage = this_cpu_read(s->cpu_slab->partial);        // 如果 partial 列表不為空,則需要判斷 partial 列表中所有 slub 包含的空閑對(duì)象總數(shù)是否超過了 s->cpu_partial 規(guī)定的閾值        // 超過 s->cpu_partial 則需要將 kmem_cache_cpu->partial 列表中原有的所有 slub 轉(zhuǎn)移到 kmem_cache_node-> partial 列表中        // 轉(zhuǎn)移之后,再把當(dāng)前 slub 插入到 kmem_cache_cpu->partial 列表中        // 如果沒有超過 s->cpu_partial ,則無需轉(zhuǎn)移直接插入        if (oldpage) {            // 從 partial 列表首頁中獲取列表中包含的空閑對(duì)象總數(shù)            pobjects = oldpage->pobjects;            // 從 partial 列表首頁中獲取列表中包含的 slub 總數(shù)            pages = oldpage->pages;            if (drain && pobjects > s->cpu_partial) {                unsigned long flags;                // 關(guān)閉中斷,防止并發(fā)訪問                local_irq_save(flags);                // partial 列表中所包含的空閑對(duì)象總數(shù) pobjects 超過了 s->cpu_partial 規(guī)定的閾值                // 則需要將現(xiàn)有 partial 列表中的所有 slub 轉(zhuǎn)移到相應(yīng)的 kmem_cache_node->partial 列表中                unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));                // 恢復(fù)中斷                local_irq_restore(flags);                // 重置 partial 列表                oldpage = NULL;                pobjects = 0;                pages = 0;                stat(s, CPU_PARTIAL_DRAIN);            }        }        // 無論 kmem_cache_cpu-> partial 列表中的 slub 是否需要轉(zhuǎn)移        // 釋放對(duì)象所在的 slub 都需要填加到  kmem_cache_cpu-> partial 列表中        pages++;        pobjects += page->objects - page->inuse;        page->pages = pages;        page->pobjects = pobjects;        page->next = oldpage;        // 通過 cas 將 slub 插入到 partial 列表的頭部    } while (this_cpu_cmpxchg(s->cpu_slab->partial, oldpage, page)                                != oldpage);    // s->cpu_partial = 0 表示 kmem_cache_cpu->partial 列表不能存放 slub    // 將釋放對(duì)象所在的 slub 轉(zhuǎn)移到  kmem_cache_node-> partial 列表中    if (unlikely(!s->cpu_partial)) {        unsigned long flags;        local_irq_save(flags);        unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));        local_irq_restore(flags);    }    preempt_enable();#endif  /* CONFIG_SLUB_CPU_PARTIAL */}

那么我們?nèi)绾沃? kmem_cache_cpu->partial 鏈表所包含的空閑對(duì)象總數(shù)到底是多少呢?

這就用到了 struct page 結(jié)構(gòu)中的兩個(gè)重要屬性:

struct page {      // slab 所在鏈表中的包含的 slab 總數(shù)      int pages;        // slab 所在鏈表中包含的對(duì)象總數(shù)      int pobjects; }

我們都知道 slab 在內(nèi)核中的數(shù)據(jù)結(jié)構(gòu)用 struct page 中的相關(guān)結(jié)構(gòu)體表示,slab 在 slab cache 架構(gòu)中一般是由 kmem_cache_cpu->partial 鏈表和 kmem_cache_node->partial 鏈表來組織管理。

那么我們?nèi)绾沃?partial 鏈表中包含多少個(gè) slab ?包含多少個(gè)空閑對(duì)象呢?

答案是內(nèi)核會(huì)將 parital 鏈表中的這些總體統(tǒng)計(jì)信息存儲(chǔ)在鏈表首個(gè) slab 結(jié)構(gòu)中。也就是說存儲(chǔ)在首個(gè) page 結(jié)構(gòu)中的 pages 屬性和 pobjects 屬性中。

在 put_cpu_partial 函數(shù)的開始,內(nèi)核直接獲取 parital 鏈表的首個(gè) slab —— oldpage,并通過 oldpage->pobjectss->cpu_partial比較,來判斷當(dāng)前 kmem_cache_cpu->partial 鏈表中包含的空閑對(duì)象總數(shù)是否超過了 kmem_cache 結(jié)構(gòu)中規(guī)定的 cpu_partial 閾值。

如果超過了,則通過 unfreeze_partials 轉(zhuǎn)移 kmem_cache_cpu->partial 鏈表中的所有 slab 到對(duì)應(yīng)的 kmem_cache_node->partial 鏈表中。

既然 kmem_cache_cpu->partial 鏈表有容量的限制,那么同樣 kmem_cache_node->partial 鏈表中的容量也會(huì)有限制。

kmem_cache_node->partial 鏈表中所包含 slab 個(gè)數(shù)的上限由 kmem_cache 結(jié)構(gòu)中的 min_partial 屬性決定。

struct kmem_cache {    // slab cache 在 numa node 中緩存的 slab 個(gè)數(shù)上限,slab 個(gè)數(shù)超過該值,空閑的 empty slab 則會(huì)被回收至伙伴系統(tǒng)    unsigned long min_partial;}

如果當(dāng)前要轉(zhuǎn)移的 slab 是一個(gè) empty slab,并且此時(shí) kmem_cache_node->partial 鏈表所包含的 slab 個(gè)數(shù) kmem_cache_node->nr_partial已經(jīng)超過了 kmem_cache-> min_partial的限制,那么內(nèi)核就會(huì)直接將這個(gè) empty slab 釋放回伙伴系統(tǒng)中。

// 將 kmem_cache_cpu->partial 列表中包含的 slub unfreeze// 并轉(zhuǎn)移到對(duì)應(yīng)的 kmem_cache_node->partial 列表中static void unfreeze_partials(struct kmem_cache *s,        struct kmem_cache_cpu *c){#ifdef CONFIG_SLUB_CPU_PARTIAL    struct kmem_cache_node *n = NULL, *n2 = NULL;    struct page *page, *discard_page = NULL;    // 挨個(gè)遍歷 kmem_cache_cpu->partial 列表,將列表中的 slub 轉(zhuǎn)移到對(duì)應(yīng) kmem_cache_node->partial 列表中    while ((page = c->partial)) {        struct page new;        struct page old;        // 將當(dāng)前遍歷到的 slub 從 kmem_cache_cpu->partial 列表摘下        c->partial = page->next;        // 獲取當(dāng)前 slub 所在的 numa 節(jié)點(diǎn)對(duì)應(yīng)的 kmem_cache_node 緩存        n2 = get_node(s, page_to_nid(page));        // 如果和上一個(gè)轉(zhuǎn)移的 slub 所在的 numa 節(jié)點(diǎn)不一樣        // 則需要釋放上一個(gè) numa 節(jié)點(diǎn)的 list_lock,并對(duì)當(dāng)前 numa 節(jié)點(diǎn)的 list_lock 加鎖        if (n != n2) {            if (n)                spin_unlock(&n->list_lock);            n = n2;            spin_lock(&n->list_lock);        }        do {            old.freelist = page->freelist;            old.counters = page->counters;            VM_BUG_ON(!old.frozen);            new.counters = old.counters;            new.freelist = old.freelist;            // unfrozen 當(dāng)前 slub,因?yàn)榧磳⒈晦D(zhuǎn)移到對(duì)應(yīng)的 kmem_cache_node->partial 列表            new.frozen = 0;            // cas 更新當(dāng)前 slub 的 freelist,frozen 屬性        } while (!__cmpxchg_double_slab(s, page,                old.freelist, old.counters,                new.freelist, new.counters,                "unfreezing slab"));        // 因?yàn)?kmem_cache_node->partial 列表中所包含的 slub 個(gè)數(shù)是受 s->min_partial 閾值限制的        // 所以這里還需要檢查 nr_partial 是否超過了 min_partial        // 如果當(dāng)前被轉(zhuǎn)移的 slub 是一個(gè) empty slub 并且 nr_partial 超過了 min_partial 的限制,則需要將 slub 釋放回伙伴系統(tǒng)中        if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {            // discard_page 用于將需要釋放回伙伴系統(tǒng)的 slub 串聯(lián)起來            // 后續(xù)統(tǒng)一將 discard_page 鏈表中的 slub 釋放回伙伴系統(tǒng)            page->next = discard_page;            discard_page = page;        } else {            // 其他情況,只要 slub 不為 empty ,不管 nr_partial 是否超過了 min_partial            // 都需要將 slub 轉(zhuǎn)移到對(duì)應(yīng) kmem_cache_node->partial 列表的末尾            add_partial(n, page, DEACTIVATE_TO_TAIL);            stat(s, FREE_ADD_PARTIAL);        }    }    if (n)        spin_unlock(&n->list_lock);    // 將 discard_page 鏈表中的 slub 統(tǒng)一釋放回伙伴系統(tǒng)    while (discard_page) {        page = discard_page;        discard_page = discard_page->next;        stat(s, DEACTIVATE_EMPTY);        // 底層調(diào)用 __free_pages 將 slub 所管理的所有 page 釋放回伙伴系統(tǒng)        discard_slab(s, page);        stat(s, FREE_SLAB);    }#endif  /* CONFIG_SLUB_CPU_PARTIAL */}
3.4 釋放對(duì)象所屬 slab 從 partial slab 變?yōu)榱?empty slab

如果釋放對(duì)象所在的 slab 原來是一個(gè) partial slab ,由于對(duì)象的釋放剛好變成了一個(gè) empty slab,恰恰說明該 slab 并不是一個(gè)活躍的 slab,它的局部性不好,內(nèi)核已經(jīng)好久沒有從該 slab 中分配對(duì)象了,所以內(nèi)核選擇刀槍入庫,馬放南山。將它釋放回 kmem_cache_node->partial 鏈表中作為本地 cpu 緩存的后備選項(xiàng)。

在將這個(gè) empty slab 插入到 kmem_cache_node->partial 鏈表之前,同樣需要檢查當(dāng)前 partial 鏈表中的容量 kmem_cache_node->nr_partial不能超過 kmem_cache-> min_partial 的限制。如果超過限制了,直接將這個(gè) empty slab 釋放回伙伴系統(tǒng)中。

if ((!new.inuse || !prior) && !was_frozen) {            if (kmem_cache_has_cpu_partial(s) && !prior) {                new.frozen = 1;            } else {                 // !new.inuse 表示當(dāng)前 slab 剛剛從一個(gè) partial slab 變?yōu)榱?empty slab                n = get_node(s, page_to_nid(page));                spin_lock_irqsave(&n->list_lock, flags);            }        }      if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))        // 如果 slub 變?yōu)榱艘粋€(gè) empty slub 并且 nr_partial 超過了最大閾值 min_partial        // 跳轉(zhuǎn)到 slab_empty 分支,將 slub 釋放回伙伴系統(tǒng)中        goto slab_empty;

釋放對(duì)象所屬的 slab 本來就在 kmem_cache_node->partial 鏈表中,這種情況下就是直接釋放對(duì)象回 slab 中,無需改變 slab 的位置。

4. slab cache 的銷毀

終于到了本文最后一個(gè)小節(jié)了, slab cache 最為復(fù)雜的內(nèi)容我們已經(jīng)踏過去了,本小節(jié)的內(nèi)容將會(huì)非常的輕松愉悅,這一次筆者來為大家介紹一下 slab cache 的銷毀過程。

slab cache 的銷毀過程剛剛好和 slab cache 的創(chuàng)建過程相反,筆者在 《從內(nèi)核源碼看 slab 內(nèi)存池的創(chuàng)建初始化流程》的內(nèi)容中,通過一步一步的源碼演示,最終勾勒出 slab cache 的完整架構(gòu):

slab cache 銷毀的核心步驟如下:

首先需要釋放 slab cache 在所有 cpu 中的緩存 kmem_cache_cpu 中占用的資源,包括被 cpu 緩存的 slab (kmem_cache_cpu->page),以及 kmem_cache_cpu->partial 鏈表中緩存的所有 slab,將它們統(tǒng)統(tǒng)歸還到伙伴系統(tǒng)中。

釋放 slab cache 在所有 NUMA 節(jié)點(diǎn)中的緩存 kmem_cache_node 占用的資源,也就是將 kmem_cache_node->partial 鏈表中緩存的所有 slab ,統(tǒng)統(tǒng)釋放回伙伴系統(tǒng)中。

在 sys 文件系統(tǒng)中移除 /sys/kernel/slab/節(jié)點(diǎn)相關(guān)信息。

從 slab cache 的全局列表中刪除該 slab cache。

釋放 kmem_cache_cpu 結(jié)構(gòu),kmem_cache_node 結(jié)構(gòu),kmem_cache 結(jié)構(gòu)。釋放對(duì)象的過程就是 《1. slab cache 如何回收內(nèi)存》小節(jié)中介紹的內(nèi)容。

下面我們一起到內(nèi)核源碼中看一下具體的銷毀過程:

void kmem_cache_destroy(struct kmem_cache *s){    int err;    if (unlikely(!s))        return;    // 獲取 cpu_hotplug_lock,防止 cpu 熱插拔改變 online cpu map    get_online_cpus();    // 獲取 mem_hotplug_lock,防止訪問內(nèi)存的時(shí)候進(jìn)行內(nèi)存熱插拔    get_online_mems();    // 獲取 slab cache 鏈表的全局互斥鎖    mutex_lock(&slab_mutex);    // 將 slab cache 的引用技術(shù)減 1    s->refcount--;    // 判斷 slab cache 是否還存在其他地方的引用    if (s->refcount)        // 如果該 slab cache 還存在引用,則不能銷毀,跳轉(zhuǎn)到 out_unlock 分支        goto out_unlock;    // 銷毀 memory cgroup 相關(guān)的 cache ,這里不是本文重點(diǎn)    err = shutdown_memcg_caches(s);    if (!err)        // slab cache 銷毀的核心函數(shù),銷毀邏輯就封裝在這里        err = shutdown_cache(s);    if (err) {        pr_err("kmem_cache_destroy %s: Slab cache still has objects\n",               s->name);        dump_stack();    }out_unlock:    // 釋放相關(guān)的自旋鎖和信號(hào)量    mutex_unlock(&slab_mutex);    put_online_mems();    put_online_cpus();}

在開始正式銷毀 slab cache 之前,首先需要將 slab cache 的引用計(jì)數(shù) refcount 減 1。并需要判斷 slab cache 是否還存在其他地方的引用。

slab cache 這里在其他地方存在引用的可能性,相關(guān)細(xì)節(jié)筆者在《從內(nèi)核源碼看 slab 內(nèi)存池的創(chuàng)建初始化流程》 一文中的 ”1. __kmem_cache_alias“ 小節(jié)的內(nèi)容中已經(jīng)詳細(xì)介紹過了。

當(dāng)我們利用 kmem_cache_create 創(chuàng)建 slab cache 的時(shí)候,內(nèi)核會(huì)檢查當(dāng)前系統(tǒng)中是否存在一個(gè)各項(xiàng)參數(shù)和我們要?jiǎng)?chuàng)建 slab cache 參數(shù)差不多的一個(gè) slab cache,如果存在,那么內(nèi)核就不會(huì)再繼續(xù)創(chuàng)建新的 slab cache,而是復(fù)用已有的 slab cache。

一個(gè)可以被復(fù)用的 slab cache 需要滿足以下四個(gè)條件:

指定的 slab_flags_t 相同。

指定對(duì)象的 object size 要小于等于已有 slab cache 中的對(duì)象 size (kmem_cache->size)。

如果指定對(duì)象的 object size 與已有 kmem_cache->size 不相同,那么它們之間的差值需要再一個(gè) word size 之內(nèi)。

已有 slab cache 中的 slab 對(duì)象對(duì)齊 align (kmem_cache->align)要大于等于指定的 align 并且可以整除 align 。 。

隨后會(huì)在 sys 文件系統(tǒng)中為復(fù)用 slab cache 起一個(gè)別名 alias 并創(chuàng)建一個(gè) /sys/kernel/slab/aliasname 目錄,但是該目錄下的文件需要軟鏈接到原有 slab cache 在 sys 文件系統(tǒng)對(duì)應(yīng)目錄下的文件。這里的 aliasname 就是我們通過 kmem_cache_create 指定的 slab cache 名稱。

在這種情況,系統(tǒng)中的 slab cache 就可能在多個(gè)地方產(chǎn)生引用,所以在銷毀的時(shí)候需要判斷這一點(diǎn)。

如果存在其他地方的引用,則需要停止銷毀流程,如果沒有其他地方的引用,則調(diào)用 shutdown_cache 開始正式的銷毀流程。

static int shutdown_cache(struct kmem_cache *s){    // 這里會(huì)釋放 slab cache 占用的所有資源    if (__kmem_cache_shutdown(s) != 0)        return -EBUSY;    // 從 slab cache 的全局列表中刪除該 slab cache    list_del(&s->list);    // 釋放 sys 文件系統(tǒng)中移除 /sys/kernel/slab/name 節(jié)點(diǎn)的相關(guān)資源    sysfs_slab_unlink(s);    sysfs_slab_release(s);    // 釋放 kmem_cache_cpu 結(jié)構(gòu)    // 釋放 kmem_cache_node 結(jié)構(gòu)    // 釋放 kmem_cache 結(jié)構(gòu)    slab_kmem_cache_release(s);    }    return 0;}
4.1 釋放 slab cache 占用的所有資源

首先需要釋放 slab cache 在所有 cpu 中的緩存 kmem_cache_cpu 中占用的資源,包括被 cpu 緩存的 slab (kmem_cache_cpu->page),以及 kmem_cache_cpu->partial 鏈表中緩存的所有 slab,將它們統(tǒng)統(tǒng)歸還到伙伴系統(tǒng)中。

釋放 slab cache 在所有 NUMA 節(jié)點(diǎn)中的緩存 kmem_cache_node 占用的資源,也就是將 kmem_cache_node->partial 鏈表中緩存的所有 slab ,統(tǒng)統(tǒng)釋放回伙伴系統(tǒng)中。

在 sys 文件系統(tǒng)中移除 /sys/kernel/slab/節(jié)點(diǎn)相關(guān)信息。

/* * Release all resources used by a slab cache. */int __kmem_cache_shutdown(struct kmem_cache *s){    int node;    struct kmem_cache_node *n;    // 釋放 slab cache 本地 cpu 緩存 kmem_cache_cpu 中緩存的 slub 以及 partial 列表中的 slub,統(tǒng)統(tǒng)歸還給伙伴系統(tǒng)    flush_all(s);    // 釋放 slab cache 中 numa 節(jié)點(diǎn)緩存 kmem_cache_node 中 partial 列表上的所有 slub    for_each_kmem_cache_node(s, node, n) {        free_partial(s, n);        if (n->nr_partial || slabs_node(s, node))            return 1;    }    // 在 sys 文件系統(tǒng)中移除 /sys/kernel/slab/name 節(jié)點(diǎn)相關(guān)信息    sysfs_slab_remove(s);    return 0;}
4.2 釋放 slab cache 在各個(gè) cpu 中的緩存資源

內(nèi)核通過 on_each_cpu_cond 挨個(gè)遍歷所有 cpu,在遍歷的過程中通過 has_cpu_slab 判斷 slab cache 是否在該 cpu 中還占有緩存資源,如果是則調(diào)用 flush_cpu_slab 將緩存資源釋放回伙伴系統(tǒng)中。

// 釋放 kmem_cache_cpu 中占用的所有內(nèi)存資源static void flush_all(struct kmem_cache *s){    // 遍歷每個(gè) cpu,通過 has_cpu_slab 函數(shù)檢查 cpu 上是否還有 slab cache 的相關(guān)緩存資源    // 如果有,則調(diào)用 flush_cpu_slab 進(jìn)行資源的釋放    on_each_cpu_cond(has_cpu_slab, flush_cpu_slab, s, 1, GFP_ATOMIC);}static bool has_cpu_slab(int cpu, void *info){    struct kmem_cache *s = info;    // 獲取 cpu 在 slab cache 上的本地緩存    struct kmem_cache_cpu *c = per_cpu_ptr(s->cpu_slab, cpu);    // 判斷 cpu 本地緩存中是否還有緩存的 slub    return c->page || slub_percpu_partial(c);}static void flush_cpu_slab(void *d){    struct kmem_cache *s = d;    // 釋放 slab cache 在 cpu 上的本地緩存資源    __flush_cpu_slab(s, smp_processor_id());}static inline void __flush_cpu_slab(struct kmem_cache *s, int cpu){    struct kmem_cache_cpu *c = per_cpu_ptr(s->cpu_slab, cpu);    if (c->page)        // 釋放 cpu 本地緩存的 slub 到伙伴系統(tǒng)        flush_slab(s, c);    // 將 cpu 本地緩存中的 partial 列表里的 slub 全部釋放回伙伴系統(tǒng)    unfreeze_partials(s, c);}
4.3 釋放 slab cache 的核心數(shù)據(jù)結(jié)構(gòu)

這里的釋放流程正是筆者在本文 《1. slab cache 如何回收內(nèi)存》小節(jié)中介紹的內(nèi)容。

void slab_kmem_cache_release(struct kmem_cache *s){    // 釋放 slab cache 中的 kmem_cache_cpu 結(jié)構(gòu)以及 kmem_cache_node 結(jié)構(gòu)    __kmem_cache_release(s);    // 最后釋放 slab cache 的核心數(shù)據(jù)結(jié)構(gòu) kmem_cache    kmem_cache_free(kmem_cache, s);}
總結(jié)

整個(gè) slab cache 系列篇幅非常龐大,涉及到的細(xì)節(jié)非常豐富,為了方便大家回顧,筆者這里將 slab cache 系列涉及到的重點(diǎn)內(nèi)容再次梳理總結(jié)一下。

《細(xì)節(jié)拉滿,80 張圖帶你一步一步推演 slab 內(nèi)存池的設(shè)計(jì)與實(shí)現(xiàn)》

《從內(nèi)核源碼看 slab 內(nèi)存池的創(chuàng)建初始化流程》

《深入理解 slab cache 內(nèi)存分配全鏈路實(shí)現(xiàn)》

在本文正式進(jìn)入 slab 相關(guān)內(nèi)容之后,筆者首先為大家詳細(xì)介紹了 slab 內(nèi)存池中對(duì)象的內(nèi)存布局情況,如下圖所示:

在此基礎(chǔ)之上,我們繼續(xù)采用一步一圖的方式,一步一步推演出 slab 內(nèi)存池的整體架構(gòu),如下圖所示:

隨后基于此架構(gòu),筆者介紹了在不同場(chǎng)景下 slab 內(nèi)存池分配內(nèi)存以及回收內(nèi)存的核心原理。在交代完核心原理之后,我們進(jìn)一步深入到內(nèi)核源碼實(shí)現(xiàn)中來一一驗(yàn)證。

在內(nèi)核源碼章節(jié)的開始,筆者首先為大家介紹了 slab 內(nèi)存池的創(chuàng)建流程,流程圖如下:

在 slab 內(nèi)存池創(chuàng)建出來之后,隨后筆者又深入介紹了 slab 內(nèi)存池如何分配內(nèi)存塊的相關(guān)源碼實(shí)現(xiàn),其中詳細(xì)介紹了在多種不同場(chǎng)景下,內(nèi)核如何處理內(nèi)存塊的分配。

在我們清除了 slab 內(nèi)存池如何分配內(nèi)存塊的源碼實(shí)現(xiàn)之后,緊接著筆者又介紹了 slab 內(nèi)存池如何進(jìn)行內(nèi)存塊的回收,回收過程要比分配過程復(fù)雜很多,同樣也涉及到多種復(fù)雜場(chǎng)景的處理:

最后筆者介紹了 slab 內(nèi)存池的銷毀過程:

好了,整個(gè) slab cache 相關(guān)的內(nèi)容到此就結(jié)束了,感謝大家的收看,我們下篇文章見~~~

關(guān)鍵詞:

參與評(píng)論