亚洲手机中文字幕_少妇久久久久久久久人妻无码_国产成 人 综合 亚洲专区_国产欧美日产高清欧美一区二区_综合中文字幕无码亚洲

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

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

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

在上篇文章 《深入理解 slab cache 內(nèi)存分配全鏈路實現(xiàn)》 中,筆者詳細地為大家介紹了 slab cache 進行內(nèi)存分配的整個鏈路實現(xiàn),本文我們就來到了 slab cache 最后的一部分內(nèi)容了,當申請的內(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ù),用于將對象釋放回其所屬的 slab cache 中,參數(shù) x 表示我們要釋放的內(nèi)存塊(對象)的虛擬內(nèi)存地址,參數(shù) s 指向內(nèi)存塊所屬的 slab cache。

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

在開始釋放內(nèi)存塊 x 之前,內(nèi)核需要首先通過 cache_from_obj 函數(shù)確認內(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 的過程涉及到的背景知識比較復雜,這個筆者后面會單獨拎出來介紹,這里大家只需要簡單了解 virt_to_head_page 函數(shù)的作用即可。

static inline struct kmem_cache *cache_from_obj(struct kmem_cache *s, void *x){    struct kmem_cache *cachep;    // 通過對象的虛擬內(nèi)存地址 x 找到對象所屬的 slab cache    cachep = virt_to_cache(x);    // 校驗指定的 slab cache : s 是否是對象真正所屬的 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ù)首先會通過釋放對象的虛擬內(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ù)對象的虛擬內(nèi)存地址 *obj 找到其所在的內(nèi)存頁 page    // 如果 slub 背后是多個內(nèi)存頁(復合頁),則返回復合頁的首頁 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 表示釋放對象所在的 slab cache,指定我們要將對象釋放到哪里。參數(shù) page 表示釋放對象所在的 slab,slab 在內(nèi)核中使用 struct page 結(jié)構(gòu)來表示。參數(shù) head 指向釋放對象的虛擬內(nèi)存地址(起始內(nèi)存地址)。該函數(shù)支持向 slab cache 批量的釋放多個對象,參數(shù) tail 指向批量釋放對象中最后一個對象的虛擬內(nèi)存地址。參數(shù) cnt 表示釋放對象的個數(shù),也是用于批量釋放對象參數(shù) addr 用于 slab 調(diào)試,這里我們不需要關(guān)心。

slab cache 針對內(nèi)存的回收流程其實和我們在上篇文章 《深入理解 slab cache 內(nèi)存分配全鏈路實現(xiàn)》 中介紹的 slab cache 內(nèi)存分配流程是相似的。

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

如果釋放對象所在的 slab 剛好是 slab cache 在本地 cpu 緩存 kmem_cache_cpu->page 緩存的 slab,那么內(nèi)核就會直接將對象釋放回緩存 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 中對象分配與釋放流程的全局事務(wù) id    // 既可以用來標識同一個分配或者釋放的事務(wù)流程,也可以用來標識區(qū)分所屬 cpu 本地緩存    unsigned long tid;redo:    // 接下來我們需要獲取 slab cache 的 cpu 本地緩存    // 這里的 do..while 循環(huán)是要保證獲取到的 cpu 本地緩存 c 是屬于執(zhí)行進程的當前 cpu    // 因為進程可能由于搶占或者中斷的原因被調(diào)度到其他 cpu 上執(zhí)行,所需需要確保兩者的 tid 是否一致    do {        // 獲取執(zhí)行當前進程的 cpu 中的 tid 字段        tid = this_cpu_read(s->cpu_slab->tid);        // 獲取 cpu 本地緩存 cpu_slab        c = raw_cpu_ptr(s->cpu_slab);        // 如果兩者的 tid 字段不一致,說明進程已經(jīng)被調(diào)度到其他 cpu 上了        // 需要再次獲取正確的 cpu 本地緩存    } while (IS_ENABLED(CONFIG_PREEMPT) &&         unlikely(tid != READ_ONCE(c->tid)));    // 如果釋放對象所屬的 slub (page 表示)正好是 cpu 本地緩存的 slub    // 那么直接將對象釋放到 cpu 緩存的 slub 中即可,這里就是快速釋放路徑 fastpath    if (likely(page == c->page)) {        // 將對象釋放至 cpu 本地緩存 freelist 中的頭結(jié)點處        // 釋放對象中的 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        // 如果當前釋放對象并不在 cpu 本地緩存中,那么就進入慢速釋放路徑 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)與運行當前進程所在的 cpu 是相符的,所以這里還是需要在 do .... while循環(huán)內(nèi)判斷兩者的 tid。這一點,筆者已經(jīng)在本文之前的內(nèi)容里多次強調(diào)過了,這里不在贅述。

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

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

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

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

slab cache 在慢速路徑下回收內(nèi)存的邏輯比較復雜,因為這里涉及到很多的場景,需要改變釋放對象所屬 slab 在 slab cache 架構(gòu)中的位置。

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

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

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

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

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

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

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

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

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

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;    // 對象所屬的 slub 之前是否在本地 cpu 緩存 partial 鏈表中    int was_frozen;    // 后續(xù)會對 slub 對應(yīng)的 page 結(jié)構(gòu)相關(guān)屬性進行修改    // 修改后的屬性會臨時保存在 new 中,后面通過 cas 替換    struct page new;    unsigned long counters;    struct kmem_cache_node *n = NULL;    stat(s, FREE_SLOWPATH);    // free_debug_processing 中會調(diào)用 init_object,清理對象內(nèi)存無用信息,重新恢復對象內(nèi)存布局到初始狀態(tài)    if (kmem_cache_debug(s) &&     !free_debug_processing(s, page, head, tail, cnt, addr))        return;    do {        // 獲取 slub 中的空閑對象列表,prior = null 表示此時 slub 是一個 full slub,意思就是該 slub 中的對象已經(jīng)全部被分配出去了        prior = page->freelist;        counters = page->counters;        // 將釋放的對象插入到 freelist 的頭部,將對象釋放回 slub        // 將 tail 對象的 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)分配出去的對象個數(shù),這里是釋放 cnt 個對象,所以 inuse 要減去 cnt        new.inuse -= cnt;        // !new.inuse 表示此時 slub 變?yōu)榱艘粋€ empty slub,意思就是該 slub 中的對象還沒有分配出去,全部在 slub 中        // !prior 表示由于本次對象的釋放,slub 剛剛從一個 full slub 變成了一個 partial slub (意思就是該 slub 中的對象部分分配出去了,部分沒有分配出去)        // !was_frozen 表示該 slub 不在 cpu 本地緩存中        if ((!new.inuse || !prior) && !was_frozen) {            // 注意:進入該分支的 slub 之前都不在 cpu 本地緩存中            // 如果配置了 CONFIG_SLUB_CPU_PARTIAL 選項,那么表示 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ù)會將該 slub 插入到 kmem_cache_cpu 的 partial 列表中                new.frozen = 1;            } else {                 // 如果 kmem_cache_cpu 中沒有配置 partial 列表,那么直接釋放至 kmem_cache_node 中                // 或者該 slub 由一個 partial slub 變?yōu)榱?empty slub,調(diào)整 slub 的位置到 kmem_cache_node->partial 鏈表中                n = get_node(s, page_to_nid(page));                // 后續(xù)會操作 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"));    // 該分支要處理的場景是:    // 1: 該 slub 原來不在 cpu 本地緩存的 partial 列表中(!was_frozen),但是該 slub 剛剛從 full slub 變?yōu)榱?partial slub,需要放入 cpu-> partial 列表中    // 2: 該 slub 原來就在 cpu 本地緩存的 partial 列表中,直接將對象釋放回 slub 即可    if (likely(!n)) {        // 處理場景 1        if (new.frozen && !was_frozen) {            // 將 slub 插入到 kmem_cache_cpu 中的 partial 列表中            put_cpu_partial(s, page, 1);            stat(s, CPU_PARTIAL_FREE);        }                // 處理場景2,因為之前已經(jīng)通過 set_freepointer 將對象釋放回 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 個數(shù)    // min_partial 表示 slab cache 規(guī)定 kmem_cache_node 中 partial 列表可以容納的 slub 最大個數(shù)    // 如果 nr_partial 超過了最大閾值 min_partial,則不能放入 kmem_cache_node 里    if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))        // 如果 slub 變?yōu)榱艘粋€ 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 列表中    // 直接將對象釋放回 slub 即可,無需改變 slub 的位置,直接返回    return;slab_empty:    // 該分支處理的場景是: slub 太多了,將 empty slub 釋放會伙伴系統(tǒng)    // 首先將 slub 從對應(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 直接釋放對象回 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ù)會對 slub 對應(yīng)的 page 結(jié)構(gòu)相關(guān)屬性進行修改    // 修改后的屬性會臨時保存在 new 中,后面通過 cas 替換    struct page new;              ....... 省略 ..........    do {        prior = page->freelist;        counters = page->counters;        // 將對象直接釋放回 slab 中,調(diào)整 slab 的 freelist 指針,以及對象的 freepointer 指針        set_freepointer(s, tail, prior);        new.counters = counters;        // 獲取原來 slub 中的 frozen 狀態(tài),是否在 cpu 緩存 partial 中        was_frozen = new.frozen;        // inuse 表示 slub 已經(jīng)分配出去的對象個數(shù),這里是釋放 cnt 個對象,所以 inuse 要減去 cnt        new.inuse -= cnt;              ....... 省略 ..........        // cas 更新 slub 中的 freelist     } while (!cmpxchg_double_slab(s, page,        prior, counters,        head, new.counters,        "__slab_free")); .            ...... 省略 ..........}

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

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

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

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

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

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

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

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

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

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

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

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

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

struct page {            union {                unsigned long counters;                struct {            /* SLUB */                                 // slab 中已經(jīng)分配出去的對象                    unsigned inuse:16;                    // slab 中包含的對象總數(shù)                    unsigned objects:15;                    // 該 slab 是否在對應(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 屬性的值也在這個內(nèi)存塊中,所以原有 slab 中的 inuse,frozen 屬性也就跟著一起賦值到 new page 的對應(yīng)屬性中了。這樣一來,后續(xù)的邏輯處理也就通順了。

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

同樣的道理,我們再來看內(nèi)核 cmpxchg_double_slab 中的更新操作:

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

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

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

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

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

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

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

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

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

was_frozen = new.frozen;

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

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

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

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

prior = page->freelist = null

隨著對象的釋放,該 slab 從一個 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ù)最重要的一個考量邏輯是需要確保 kmem_cache_cpu->partial 鏈表中所有 slab 中包含的空閑對象總數(shù)不能超過 kmem_cache->cpu_partial 的限制。

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

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

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

static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain){// 只有配置了 CONFIG_SLUB_CPU_PARTIAL 選項,kmem_cache_cpu 中才有會 partial 列表#ifdef CONFIG_SLUB_CPU_PARTIAL    // 指向原有 kmem_cache_cpu 中的 partial 列表    struct page *oldpage;    // slub 所在管理列表中的 slub 個數(shù),這里的列表是指 partial 列表    int pages;    // slub 所在管理列表中的包含的空閑對象總數(shù),這里的列表是指 partial 列表    // 內(nèi)核會將列表總體的信息存放在列表首頁 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 包含的空閑對象總數(shù)是否超過了 s->cpu_partial 規(guī)定的閾值        // 超過 s->cpu_partial 則需要將 kmem_cache_cpu->partial 列表中原有的所有 slub 轉(zhuǎn)移到 kmem_cache_node-> partial 列表中        // 轉(zhuǎn)移之后,再把當前 slub 插入到 kmem_cache_cpu->partial 列表中        // 如果沒有超過 s->cpu_partial ,則無需轉(zhuǎn)移直接插入        if (oldpage) {            // 從 partial 列表首頁中獲取列表中包含的空閑對象總數(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 列表中所包含的空閑對象總數(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));                // 恢復中斷                local_irq_restore(flags);                // 重置 partial 列表                oldpage = NULL;                pobjects = 0;                pages = 0;                stat(s, CPU_PARTIAL_DRAIN);            }        }        // 無論 kmem_cache_cpu-> partial 列表中的 slub 是否需要轉(zhuǎn)移        // 釋放對象所在的 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    // 將釋放對象所在的 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 鏈表所包含的空閑對象總數(shù)到底是多少呢?

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

struct page {      // slab 所在鏈表中的包含的 slab 總數(shù)      int pages;        // slab 所在鏈表中包含的對象總數(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 鏈表中包含多少個 slab ?包含多少個空閑對象呢?

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

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

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

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

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

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

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

// 將 kmem_cache_cpu->partial 列表中包含的 slub unfreeze// 并轉(zhuǎn)移到對應(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;    // 挨個遍歷 kmem_cache_cpu->partial 列表,將列表中的 slub 轉(zhuǎn)移到對應(yīng) kmem_cache_node->partial 列表中    while ((page = c->partial)) {        struct page new;        struct page old;        // 將當前遍歷到的 slub 從 kmem_cache_cpu->partial 列表摘下        c->partial = page->next;        // 獲取當前 slub 所在的 numa 節(jié)點對應(yīng)的 kmem_cache_node 緩存        n2 = get_node(s, page_to_nid(page));        // 如果和上一個轉(zhuǎn)移的 slub 所在的 numa 節(jié)點不一樣        // 則需要釋放上一個 numa 節(jié)點的 list_lock,并對當前 numa 節(jié)點的 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 當前 slub,因為即將被轉(zhuǎn)移到對應(yīng)的 kmem_cache_node->partial 列表            new.frozen = 0;            // cas 更新當前 slub 的 freelist,frozen 屬性        } while (!__cmpxchg_double_slab(s, page,                old.freelist, old.counters,                new.freelist, new.counters,                "unfreezing slab"));        // 因為 kmem_cache_node->partial 列表中所包含的 slub 個數(shù)是受 s->min_partial 閾值限制的        // 所以這里還需要檢查 nr_partial 是否超過了 min_partial        // 如果當前被轉(zhuǎn)移的 slub 是一個 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)移到對應(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 釋放對象所屬 slab 從 partial slab 變?yōu)榱?empty slab

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

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

if ((!new.inuse || !prior) && !was_frozen) {            if (kmem_cache_has_cpu_partial(s) && !prior) {                new.frozen = 1;            } else {                 // !new.inuse 表示當前 slab 剛剛從一個 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)榱艘粋€ empty slub 并且 nr_partial 超過了最大閾值 min_partial        // 跳轉(zhuǎn)到 slab_empty 分支,將 slub 釋放回伙伴系統(tǒng)中        goto slab_empty;

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

4. slab cache 的銷毀

終于到了本文最后一個小節(jié)了, slab cache 最為復雜的內(nèi)容我們已經(jīng)踏過去了,本小節(jié)的內(nèi)容將會非常的輕松愉悅,這一次筆者來為大家介紹一下 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é)點中的緩存 kmem_cache_node 占用的資源,也就是將 kmem_cache_node->partial 鏈表中緩存的所有 slab ,統(tǒng)統(tǒng)釋放回伙伴系統(tǒng)中。

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

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

釋放 kmem_cache_cpu 結(jié)構(gòu),kmem_cache_node 結(jié)構(gòu),kmem_cache 結(jié)構(gòu)。釋放對象的過程就是 《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)存的時候進行內(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 ,這里不是本文重點    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)的自旋鎖和信號量    mutex_unlock(&slab_mutex);    put_online_mems();    put_online_cpus();}

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

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

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

一個可以被復用的 slab cache 需要滿足以下四個條件:

指定的 slab_flags_t 相同。

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

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

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

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

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

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

static int shutdown_cache(struct kmem_cache *s){    // 這里會釋放 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é)點的相關(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é)點中的緩存 kmem_cache_node 占用的資源,也就是將 kmem_cache_node->partial 鏈表中緩存的所有 slab ,統(tǒng)統(tǒng)釋放回伙伴系統(tǒng)中。

在 sys 文件系統(tǒng)中移除 /sys/kernel/slab/節(jié)點相關(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é)點緩存 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é)點相關(guān)信息    sysfs_slab_remove(s);    return 0;}
4.2 釋放 slab cache 在各個 cpu 中的緩存資源

內(nèi)核通過 on_each_cpu_cond 挨個遍歷所有 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){    // 遍歷每個 cpu,通過 has_cpu_slab 函數(shù)檢查 cpu 上是否還有 slab cache 的相關(guān)緩存資源    // 如果有,則調(diào)用 flush_cpu_slab 進行資源的釋放    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é)

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

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

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

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

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

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

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

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

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

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

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

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

關(guān)鍵詞:

參與評論