成人a片产无码免费视频在线观看_三级毛片黄色_粉嫩av免费一区二区三区_久久高清免费视频_国产无遮挡a片又黄又爽漫画_亚 洲 成 人 网站在线观看_久久久久成人精品无码中文字幕_国产精品免费精品自在线观看_天天做天天欢摸夜夜摸狠狠摸_精品国产亚洲福利一区二区_日日干国产_人人干干人人_日本人妻中文字幕乱码系列_国产白嫩受无套呻吟_中文字幕欧美日韩va免费视频_午夜三级在线观看_91久久超碰_激情内射日本一区二区三区_欧美日韩精品在线免费观看_日韩精品一区二区在线播放

您的位置:首頁 > 國內 >

深度解析 slab 內存池回收內存以及銷毀全流程

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

在上篇文章 《深入理解 slab cache 內存分配全鏈路實現》 中,筆者詳細地為大家介紹了 slab cache 進行內存分配的整個鏈路實現,本文我們就來到了 slab cache 最后的一部分內容了,當申請的內存使用完畢之后,下面就該釋放內存了。

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


【資料圖】

內核提供了 kmem_cache_free 函數,用于將對象釋放回其所屬的 slab cache 中,參數 x 表示我們要釋放的內存塊(對象)的虛擬內存地址,參數 s 指向內存塊所屬的 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. 內存釋放之前的校驗工作

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

隨后在 virt_to_head_page 函數中通過內存塊的虛擬內存地址 x 找到其所在的物理內存頁 page。然后調用 slab_free 將內存塊釋放回 slab cache 中。

通過虛擬內存地址尋找物理內存頁 page 的過程涉及到的背景知識比較復雜,這個筆者后面會單獨拎出來介紹,這里大家只需要簡單了解 virt_to_head_page 函數的作用即可。

static inline struct kmem_cache *cache_from_obj(struct kmem_cache *s, void *x){    struct kmem_cache *cachep;    // 通過對象的虛擬內存地址 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 函數首先會通過釋放對象的虛擬內存地址找到其所在的物理內存頁 page,然后通過 struct page 結構中的 slab_cache 指針找到 page 所屬的 slab cache。

static inline struct kmem_cache *virt_to_cache(const void *obj){    struct page *page;    // 根據對象的虛擬內存地址 *obj 找到其所在的內存頁 page    // 如果 slub 背后是多個內存頁(復合頁),則返回復合頁的首頁 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 結構中的 slab_cache 屬性找到其所屬的 slub    return page->slab_cache;}
2. slab cache 在快速路徑下回收內存
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 回收內存相關的邏輯封裝在 do_slab_free 函數中:

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

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

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

如果釋放對象所在的 slab 剛好是 slab cache 在本地 cpu 緩存 kmem_cache_cpu->page 緩存的 slab,那么內核就會直接將對象釋放回緩存 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 中對象分配與釋放流程的全局事務 id    // 既可以用來標識同一個分配或者釋放的事務流程,也可以用來標識區分所屬 cpu 本地緩存    unsigned long tid;redo:    // 接下來我們需要獲取 slab cache 的 cpu 本地緩存    // 這里的 do..while 循環是要保證獲取到的 cpu 本地緩存 c 是屬于執行進程的當前 cpu    // 因為進程可能由于搶占或者中斷的原因被調度到其他 cpu 上執行,所需需要確保兩者的 tid 是否一致    do {        // 獲取執行當前進程的 cpu 中的 tid 字段        tid = this_cpu_read(s->cpu_slab->tid);        // 獲取 cpu 本地緩存 cpu_slab        c = raw_cpu_ptr(s->cpu_slab);        // 如果兩者的 tid 字段不一致,說明進程已經被調度到其他 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 中的頭結點處        // 釋放對象中的 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 函數的開始首先就獲取 slab cache 的本地 cpu 緩存結構 kmem_cache_cpu,為了保證我們獲取到的 cpu 本地緩存結構與運行當前進程所在的 cpu 是相符的,所以這里還是需要在 do .... while循環內判斷兩者的 tid。這一點,筆者已經在本文之前的內容里多次強調過了,這里不在贅述。

內核在確保已經獲取了正確的 kmem_cache_cpu 結構之后,就會馬上判斷該釋放對象所在的 slab 是否正是 slab cache 本地 cpu 緩存了的 slab —— page == c->page

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

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

3. slab cache 在慢速路徑下回收內存

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

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

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

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

在將對象所在內存恢復到初始狀態之后,內核首先會將對象直接釋放回其所屬的 slab 中,并調整 slab 結構 page 的相關屬性。

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

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

如果 slab 不在 kmem_cache_cpu->partial 鏈表中,并且該 slab 由于對象的釋放剛好由一個 full slab 變為了一個 partial slab,為了利用局部性的優勢,內核需要將該 slab 插入到 kmem_cache_cpu->partial 鏈表中。

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

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

static void __slab_free(struct kmem_cache *s, struct page *page,            void *head, void *tail, int cnt,            unsigned long addr){    // 用于指向對象釋放回 slub 之前,slub 的 freelist    void *prior;    // 對象所屬的 slub 之前是否在本地 cpu 緩存 partial 鏈表中    int was_frozen;    // 后續會對 slub 對應的 page 結構相關屬性進行修改    // 修改后的屬性會臨時保存在 new 中,后面通過 cas 替換    struct page new;    unsigned long counters;    struct kmem_cache_node *n = NULL;    stat(s, FREE_SLOWPATH);    // free_debug_processing 中會調用 init_object,清理對象內存無用信息,重新恢復對象內存布局到初始狀態    if (kmem_cache_debug(s) &&     !free_debug_processing(s, page, head, tail, cnt, addr))        return;    do {        // 獲取 slub 中的空閑對象列表,prior = null 表示此時 slub 是一個 full slub,意思就是該 slub 中的對象已經全部被分配出去了        prior = page->freelist;        counters = page->counters;        // 將釋放的對象插入到 freelist 的頭部,將對象釋放回 slub        // 將 tail 對象的 freepointer 設置為 prior        set_freepointer(s, tail, prior);        // 將原有 slab 的相應屬性賦值給 new page        new.counters = counters;        // 獲取原來 slub 中的 frozen 狀態,是否在 cpu 緩存 partial 鏈表中        was_frozen = new.frozen;        // inuse 表示 slub 已經分配出去的對象個數,這里是釋放 cnt 個對象,所以 inuse 要減去 cnt        new.inuse -= cnt;        // !new.inuse 表示此時 slub 變為了一個 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 結構中包含 partial 列表,用于 cpu 緩存部分分配的 slub            if (kmem_cache_has_cpu_partial(s) && !prior) {                // 如果 kmem_cache_cpu 包含 partial 列表并且該 slub 剛剛由 full slub 變為 partial slub                // 凍結該 slub,后續會將該 slub 插入到 kmem_cache_cpu 的 partial 列表中                new.frozen = 1;            } else {                 // 如果 kmem_cache_cpu 中沒有配置 partial 列表,那么直接釋放至 kmem_cache_node 中                // 或者該 slub 由一個 partial slub 變為了 empty slub,調整 slub 的位置到 kmem_cache_node->partial 鏈表中                n = get_node(s, page_to_nid(page));                // 后續會操作 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 變為了 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,因為之前已經通過 set_freepointer 將對象釋放回 slub 了,這里只需要記錄 slub 狀態即可        if (was_frozen)            stat(s, FREE_FROZEN);        return;    }        // 后續的邏輯就是處理需要將 slub 放入 kmem_cache_node 中的 partial 列表的情形    // 在將 slub 放入 node 緩存之前,需要判斷 node 緩存的 nr_partial 是否超過了指定閾值 min_partial(位于 kmem_cache 結構)    // nr_partial 表示 kmem_cache_node 中 partial 列表中緩存的 slub 個數    // min_partial 表示 slab cache 規定 kmem_cache_node 中 partial 列表可以容納的 slub 最大個數    // 如果 nr_partial 超過了最大閾值 min_partial,則不能放入 kmem_cache_node 里    if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))        // 如果 slub 變為了一個 empty slub 并且 nr_partial 超過了最大閾值 min_partial        // 跳轉到 slab_empty 分支,將 slub 釋放回伙伴系統中        goto slab_empty;    // 如果 cpu 本地緩存中沒有配置 partial 列表并且 slub 剛剛從 full slub 變為 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 釋放會伙伴系統    // 首先將 slub 從對應的管理鏈表上刪除    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 回伙伴系統,底層調用 __free_pages 將 slub 所管理的所有 page 釋放回伙伴系統    discard_slab(s, page);}
3.1 直接釋放對象回 slab,調整 slab 相關屬性
static void __slab_free(struct kmem_cache *s, struct page *page,            void *head, void *tail, int cnt,            unsigned long addr){    // 后續會對 slub 對應的 page 結構相關屬性進行修改    // 修改后的屬性會臨時保存在 new 中,后面通過 cas 替換    struct page new;              ....... 省略 ..........    do {        prior = page->freelist;        counters = page->counters;        // 將對象直接釋放回 slab 中,調整 slab 的 freelist 指針,以及對象的 freepointer 指針        set_freepointer(s, tail, prior);        new.counters = counters;        // 獲取原來 slub 中的 frozen 狀態,是否在 cpu 緩存 partial 中        was_frozen = new.frozen;        // inuse 表示 slub 已經分配出去的對象個數,這里是釋放 cnt 個對象,所以 inuse 要減去 cnt        new.inuse -= cnt;              ....... 省略 ..........        // cas 更新 slub 中的 freelist     } while (!cmpxchg_double_slab(s, page,        prior, counters,        head, new.counters,        "__slab_free")); .            ...... 省略 ..........}

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

當對象被釋放回 slab 中之后,slab 結構中的相應屬于就需要做出相應的調整,比如:

調整 page 結構中的 freelist,它需要指向剛剛被釋放的對象。調整 page 結構中的 inuse,inuse 表示 slab 中已經被分配出去的對象個數,此時對象已經釋放回 slab 中,需要調整 inuse 字段。后續內核會根據不同情況,調整 page 結構的 frozen 屬性。

內核會定義一個新的 page 結構 new,將原有 slab 的 page 結構需要更新的上述屬性的新值,先一一復制給 new 的對應屬性,最后通過 cmpxchg_double_slab 原子更新 slab 對應的屬性。

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

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

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

此時 new page 結構中的 frozen,inuse 屬性依然是上述 struct page 結構中展示的初始值。

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

而 new.frozen, new.inuse 是 page 結構初始狀態的值,并不是原有 slab 結構中的值,這樣做肯定不對啊,難道是內核的一個 bug ?

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

union 結構體中定義的字段全部共享一片內存,union 結構體的內存占用由其中最大的屬性決定。而 struct 結構體中的每個字段都是獨占一片內存的。

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

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

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

而 inuse,frozen 屬性的值也在這個內存塊中,所以原有 slab 中的 inuse,frozen 屬性也就跟著一起賦值到 new page 的對應屬性中了。這樣一來,后續的邏輯處理也就通順了。

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

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

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

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

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

由于 page 的 freelist 指針在 union 結構體之外,所以需要在cmpxchg_double_slab 中單獨更新。

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

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

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

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

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

was_frozen = new.frozen;

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

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

3.3 釋放對象所屬 slab 從 full slab 變為了 partial slab

如果釋放對象所屬 slab 原來是一個 full slab,恰恰說明該 slab 擁有比較好的局部性,進程經常從該 slab 中分配對象,slab 十分活躍,才導致它變為了一個 full slab

prior = page->freelist = null

隨著對象的釋放,該 slab 從一個 full slab 變為了 partial slab,內核為了更好的利用該 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 函數最重要的一個考量邏輯是需要確保 kmem_cache_cpu->partial 鏈表中所有 slab 中包含的空閑對象總數不能超過 kmem_cache->cpu_partial 的限制。

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

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

如果超過了,則需要先將當前 kmem_cache_cpu->partial 鏈表中所有的 slab 轉移到其對應的 NUMA 節點緩存 kmem_cache_node->partial 鏈表中。轉移完成之后,在將釋放對象所屬的 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 個數,這里的列表是指 partial 列表    int pages;    // slub 所在管理列表中的包含的空閑對象總數,這里的列表是指 partial 列表    // 內核會將列表總體的信息存放在列表首頁 page 的相關字段中    int pobjects;    // 禁止搶占    preempt_disable();    do {        pages = 0;        pobjects = 0;        // 獲取 slab cache 中原有的 cpu 本地緩存 partial 列表首頁        oldpage = this_cpu_read(s->cpu_slab->partial);        // 如果 partial 列表不為空,則需要判斷 partial 列表中所有 slub 包含的空閑對象總數是否超過了 s->cpu_partial 規定的閾值        // 超過 s->cpu_partial 則需要將 kmem_cache_cpu->partial 列表中原有的所有 slub 轉移到 kmem_cache_node-> partial 列表中        // 轉移之后,再把當前 slub 插入到 kmem_cache_cpu->partial 列表中        // 如果沒有超過 s->cpu_partial ,則無需轉移直接插入        if (oldpage) {            // 從 partial 列表首頁中獲取列表中包含的空閑對象總數            pobjects = oldpage->pobjects;            // 從 partial 列表首頁中獲取列表中包含的 slub 總數            pages = oldpage->pages;            if (drain && pobjects > s->cpu_partial) {                unsigned long flags;                // 關閉中斷,防止并發訪問                local_irq_save(flags);                // partial 列表中所包含的空閑對象總數 pobjects 超過了 s->cpu_partial 規定的閾值                // 則需要將現有 partial 列表中的所有 slub 轉移到相應的 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 是否需要轉移        // 釋放對象所在的 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 轉移到  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 */}

那么我們如何知道 kmem_cache_cpu->partial 鏈表所包含的空閑對象總數到底是多少呢?

這就用到了 struct page 結構中的兩個重要屬性:

struct page {      // slab 所在鏈表中的包含的 slab 總數      int pages;        // slab 所在鏈表中包含的對象總數      int pobjects; }

我們都知道 slab 在內核中的數據結構用 struct page 中的相關結構體表示,slab 在 slab cache 架構中一般是由 kmem_cache_cpu->partial 鏈表和 kmem_cache_node->partial 鏈表來組織管理。

那么我們如何知道 partial 鏈表中包含多少個 slab ?包含多少個空閑對象呢?

答案是內核會將 parital 鏈表中的這些總體統計信息存儲在鏈表首個 slab 結構中。也就是說存儲在首個 page 結構中的 pages 屬性和 pobjects 屬性中。

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

如果超過了,則通過 unfreeze_partials 轉移 kmem_cache_cpu->partial 鏈表中的所有 slab 到對應的 kmem_cache_node->partial 鏈表中。

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

kmem_cache_node->partial 鏈表中所包含 slab 個數的上限由 kmem_cache 結構中的 min_partial 屬性決定。

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

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

// 將 kmem_cache_cpu->partial 列表中包含的 slub unfreeze// 并轉移到對應的 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 轉移到對應 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 節點對應的 kmem_cache_node 緩存        n2 = get_node(s, page_to_nid(page));        // 如果和上一個轉移的 slub 所在的 numa 節點不一樣        // 則需要釋放上一個 numa 節點的 list_lock,并對當前 numa 節點的 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,因為即將被轉移到對應的 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 個數是受 s->min_partial 閾值限制的        // 所以這里還需要檢查 nr_partial 是否超過了 min_partial        // 如果當前被轉移的 slub 是一個 empty slub 并且 nr_partial 超過了 min_partial 的限制,則需要將 slub 釋放回伙伴系統中        if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {            // discard_page 用于將需要釋放回伙伴系統的 slub 串聯起來            // 后續統一將 discard_page 鏈表中的 slub 釋放回伙伴系統            page->next = discard_page;            discard_page = page;        } else {            // 其他情況,只要 slub 不為 empty ,不管 nr_partial 是否超過了 min_partial            // 都需要將 slub 轉移到對應 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 統一釋放回伙伴系統    while (discard_page) {        page = discard_page;        discard_page = discard_page->next;        stat(s, DEACTIVATE_EMPTY);        // 底層調用 __free_pages 將 slub 所管理的所有 page 釋放回伙伴系統        discard_slab(s, page);        stat(s, FREE_SLAB);    }#endif  /* CONFIG_SLUB_CPU_PARTIAL */}
3.4 釋放對象所屬 slab 從 partial slab 變為了 empty slab

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

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

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

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

4. slab cache 的銷毀

終于到了本文最后一個小節了, slab cache 最為復雜的內容我們已經踏過去了,本小節的內容將會非常的輕松愉悅,這一次筆者來為大家介紹一下 slab cache 的銷毀過程。

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

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

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

釋放 slab cache 在所有 NUMA 節點中的緩存 kmem_cache_node 占用的資源,也就是將 kmem_cache_node->partial 鏈表中緩存的所有 slab ,統統釋放回伙伴系統中。

在 sys 文件系統中移除 /sys/kernel/slab/節點相關信息。

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

釋放 kmem_cache_cpu 結構,kmem_cache_node 結構,kmem_cache 結構。釋放對象的過程就是 《1. slab cache 如何回收內存》小節中介紹的內容。

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

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,防止訪問內存的時候進行內存熱插拔    get_online_mems();    // 獲取 slab cache 鏈表的全局互斥鎖    mutex_lock(&slab_mutex);    // 將 slab cache 的引用技術減 1    s->refcount--;    // 判斷 slab cache 是否還存在其他地方的引用    if (s->refcount)        // 如果該 slab cache 還存在引用,則不能銷毀,跳轉到 out_unlock 分支        goto out_unlock;    // 銷毀 memory cgroup 相關的 cache ,這里不是本文重點    err = shutdown_memcg_caches(s);    if (!err)        // slab cache 銷毀的核心函數,銷毀邏輯就封裝在這里        err = shutdown_cache(s);    if (err) {        pr_err("kmem_cache_destroy %s: Slab cache still has objects\n",               s->name);        dump_stack();    }out_unlock:    // 釋放相關的自旋鎖和信號量    mutex_unlock(&slab_mutex);    put_online_mems();    put_online_cpus();}

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

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

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

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

指定的 slab_flags_t 相同。

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

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

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

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

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

如果存在其他地方的引用,則需要停止銷毀流程,如果沒有其他地方的引用,則調用 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 文件系統中移除 /sys/kernel/slab/name 節點的相關資源    sysfs_slab_unlink(s);    sysfs_slab_release(s);    // 釋放 kmem_cache_cpu 結構    // 釋放 kmem_cache_node 結構    // 釋放 kmem_cache 結構    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,將它們統統歸還到伙伴系統中。

釋放 slab cache 在所有 NUMA 節點中的緩存 kmem_cache_node 占用的資源,也就是將 kmem_cache_node->partial 鏈表中緩存的所有 slab ,統統釋放回伙伴系統中。

在 sys 文件系統中移除 /sys/kernel/slab/節點相關信息。

/* * 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,統統歸還給伙伴系統    flush_all(s);    // 釋放 slab cache 中 numa 節點緩存 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 文件系統中移除 /sys/kernel/slab/name 節點相關信息    sysfs_slab_remove(s);    return 0;}
4.2 釋放 slab cache 在各個 cpu 中的緩存資源

內核通過 on_each_cpu_cond 挨個遍歷所有 cpu,在遍歷的過程中通過 has_cpu_slab 判斷 slab cache 是否在該 cpu 中還占有緩存資源,如果是則調用 flush_cpu_slab 將緩存資源釋放回伙伴系統中。

// 釋放 kmem_cache_cpu 中占用的所有內存資源static void flush_all(struct kmem_cache *s){    // 遍歷每個 cpu,通過 has_cpu_slab 函數檢查 cpu 上是否還有 slab cache 的相關緩存資源    // 如果有,則調用 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 到伙伴系統        flush_slab(s, c);    // 將 cpu 本地緩存中的 partial 列表里的 slub 全部釋放回伙伴系統    unfreeze_partials(s, c);}
4.3 釋放 slab cache 的核心數據結構

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

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

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

《細節拉滿,80 張圖帶你一步一步推演 slab 內存池的設計與實現》

《從內核源碼看 slab 內存池的創建初始化流程》

《深入理解 slab cache 內存分配全鏈路實現》

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

在此基礎之上,我們繼續采用一步一圖的方式,一步一步推演出 slab 內存池的整體架構,如下圖所示:

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

在內核源碼章節的開始,筆者首先為大家介紹了 slab 內存池的創建流程,流程圖如下:

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

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

最后筆者介紹了 slab 內存池的銷毀過程:

好了,整個 slab cache 相關的內容到此就結束了,感謝大家的收看,我們下篇文章見~~~

關鍵詞:

參與評論

主站蜘蛛池模板: 广州学而贸易有限公司| 约翰芬雷华能设计工程有限公司| 鑫鸿机电设备有限公司| 恒利装饰工程有限公司| 闽临贵金属有限公司| 达丰(上海)有限公司招聘| 江苏华星医疗器械有限公司| 深圳崇德物业有限公司| 米兰设计装饰工程有限公司| 昆明宝明工贸有限公司| 建设有限公司的经营范围| 江南制药机械有限公司| 中金融资租赁有限公司| 汉高化学技术有限公司| 维他奶广州有限公司| 安徽宁国密封件有限公司| 上海统一超商有限公司| 兰州矿场机械有限公司| 宏达国际贸易有限公司| 上海压件制造有限公司| 上海可口可乐饮料有限公司| 中信隆进出口有限公司| 东莞捷永塑胶制品有限公司| 合肥 萍 有限公司| 东莞东尼仪器有限公司| 食品批发贸易有限公司| 德津实业(深圳)有限公司| 嘉吉食品(漯河)有限公司| 富创光电(深圳)有限公司| 北控工程技术有限公司| 华威建筑工程有限公司| 中山市 家具有限公司招聘| 网御星云信息技术有限公司| 大连特殊制品有限公司| 江苏新纪元有限公司| 长城汽车有限公司电话| 芜湖麦凯瑞有限公司| 无锡三星电子有限公司| 上海大众有限公司怎么样| 上海迪爱斯通信设备有限公司| 光宝常州电子有限公司| 移远通信技术有限公司| 无锡华通气动有限公司| 浙江箱包皮件有限公司| 阳江厨房用品有限公司| 新疆中基实业有限公司| 鑫通交通器材有限公司| 上海金桥环保有限公司| 森松(江苏)重工有限公司| 广州淮杰实业有限公司| 颖通(上海)贸易有限公司| 旭川化学 昆山 有限公司| 无锡伏尔康有限公司| 深圳中兴新通讯有限公司| 泰州联成化学有限公司| 鼎鑫国际贸易有限公司| 达柯思北京贸易有限公司| 山东管业有限公司地址| 国电山西电力有限公司| 昌宏装饰工程有限公司| 菲妮迪深圳有限公司| 自动装备与工程有限公司| 厦门乐装饰有限公司| 盐城数控机床有限公司| 天津艺虹印刷有限公司| 山东贝朗制药有限公司| 上海科瑞物业有限公司| 博禄塑料 上海 有限公司| 青岛空间装饰有限公司| 安徽徽源医药有限公司| 德士达建材有限公司| 山东拓博化工有限公司| 苏州世硕电子有限公司| 辽宁生物医药有限公司| 江苏和兴汽车有限公司| 云南营养品有限公司| 山东养殖设备有限公司| 深圳海洋电子有限公司| 吉凯恩工业有限公司| 爱拓(上海)有限公司| 珠海华发实业有限公司| 宝福珠宝首饰有限公司| 郑州科华科贸有限公司| 曼吉医疗器械有限公司| 达丰电脑有限公司招聘| 通信技术有限公司简介| 廊坊埃意电子有限公司| 利通国际货运有限公司| 内蒙古集成有限公司| 吉豹电子(深圳)有限公司| 深圳迈诺电子有限公司| 安固建筑技术有限公司| 新城新能源有限公司| 欧亚特机械有限公司| 以色列化工有限公司| 深圳市远洋渔业有限公司| 深圳市冠科电子有限公司| 友邦香港保险有限公司| 丘钛微电子有限公司| 东风实业车辆有限公司| 捷美丰友化工有限公司| 恒通小额贷款有限公司| 天津品之翼有限公司| 创世纪信息有限公司| 北京凌志技术有限公司| 厦门宝龙工业有限公司| 博美(北京)玻璃有限公司| 汽车销售有限公司英文| 深圳雨伞业有限公司| 大连水产贸易有限公司| 河北北华建筑设计有限公司| 十堰正堰工贸有限公司| 民生健康药业有限公司| 许继继电器有限公司| 广州方邦电子有限公司| 中联建工程有限公司| 嘉吉食品(漯河)有限公司| 汉圣化工(宁波)有限公司| 翔宇医疗设备有限公司| 武汉南源置业有限公司| 锋宏(昆山)有限公司| 辽宁园林绿化工程有限公司| 新兴的信息技术有限公司| 衣念时装贸易有限公司| 广州 净水器有限公司| 芜湖机床制造有限公司| 中德塑钢型材有限公司| 伟业自动化设备有限公司| 华正技术有限公司招聘信息| 安森国际贸易有限公司| 重庆京宏源实业有限公司| 鸿星电子深圳有限公司| 合肥 胶粘剂有限公司| 长沙 责任有限公司| 诺斯贝尔无纺日化有限公司| 索菲测量技术有限公司| 江西电动汽车有限公司| 伟本机电上海有限公司| 杭州建材家居有限公司| 巴布科克威尔科克斯有限公司| 扬州光电仪器有限公司| 苏州菲斯达有限公司| 广西东糖业有限公司| 格威精密机械有限公司| 山西丰喜肥业有限公司| 银盛小额贷款有限公司| 北京振冲工程有限公司| 揭阳市泰不锈钢有限公司| 广州市保升有限公司| 上海长华贸易有限公司| 临朐烘干设备有限公司| 喜乐多食品有限公司| 深圳市航达电子有限公司| 上海海鼎信息工程有限公司| tcl通讯宁波有限公司| 普路通供应链有限公司| 北京曙光信息产业有限公司| 西藏信息技术有限公司| 任丘华北化工有限公司| 山西丰喜肥业有限公司| 南京华伯仪器有限公司| 成都包装机械设备有限公司| 宏大金属制品有限公司| 瓯堡纺织染整有限公司| 北京中铁物业有限公司| 汉成广州电子有限公司| 郑州花花牛有限公司| 佛山市豪电子有限公司| 大唐高鸿信息技术有限公司| 河北第四建筑工程有限公司| 海鸥照相机有限公司| 宝派 上海有限公司| 广州慕歌服饰有限公司| 金外滩发展有限公司| 深圳海川实业有限公司| 苏州中华保险有限公司| 废品回收 有限公司| 南京五金工具有限公司| 傲立信息产业有限公司| 山西中瑞贸易有限公司| 诺马(常州)有限公司| 深圳市优盛有限公司| 洛瓦空气工程有限公司| 伟业自动化设备有限公司| 默克化工技术有限公司| 苏州亿加亿有限公司| 亿企通信息技术有限公司| 久久信息技术有限公司| 常州园林建筑工程有限公司| 卜内门漆油有限公司| 宁波 停车设备有限公司| 烟台 苹果 有限公司| 北京诚济制药有限公司| 惠州市赛德电子有限公司| 建泰橡胶(深圳)有限公司| 北京天下商机有限公司| 上海娱乐有限公司地址| 江苏安吉汽车有限公司| 泰宝聚合物有限公司| 苏州信达重工有限公司| 石家庄安瑞科机械有限公司| 胜迈有限公司怎么样| 桂林营销策划有限公司| 黄骅市塑料制品有限公司| 上海佳礼礼品有限公司| 江苏盘龙建筑装饰有限公司| 老山神酒业有限公司| 南通兴电子有限公司| 建达机械制造有限公司| 康硕电子(苏州)有限公司| 筑原建筑设计有限公司| 广州中汉用品有限公司| 株洲旗滨玻璃有限公司| 青海建筑设计有限公司| 漳州发展汽车有限公司| 万成建筑工程有限公司| 广州本和食品有限公司| 太仓市包装有限公司| 深圳市远洋渔业有限公司| 晋江恒江机械有限公司| 海南省农业有限公司| 甘肃仕通汽车销售有限公司| 万峰装饰工程有限公司| 深圳恒昌模具有限公司| 协顺灯饰(惠州)有限公司| 昆山欣兴同泰有限公司| 四川金网通电子有限公司| 河南予华仪器有限公司| 富士胶片印版有限公司| 深圳东部交通有限公司| 陕汽淮南专用汽车有限公司| 苏州高中压阀门有限公司| 东莞五金厂有限公司| 深圳空调维修有限公司| 宁波勋辉电器有限公司 辉| 恒利装饰工程有限公司| 北京中盛担保有限公司| 安特优发动机工程有限公司| 南昌金属制品有限公司| 深圳市塑胶包装有限公司| 麦威迪实业有限公司| 连云港 医疗用品有限公司| 义居装饰工程有限公司| 苏州丰工业设备有限公司| 欧拓 重庆 防音配件有限公司| 贵阳包装材料有限公司| 华科泰生物技术有限公司| 日轻(上海)有限公司| 贺氏苏州特殊材料有限公司| 欧林 上海 有限公司| 路港建设工程有限公司| 盛发纺织印染有限公司| 伊诺特烟台有限公司| 万华化学有限公司招聘| 深圳市乐兴电子有限公司| 江阴苏龙发电有限公司| 北京千喜鹤餐饮有限公司| 大亚木业黑龙江有限公司| 北京长城华冠有限公司| 圳兴利家具有限公司| 三菱电机上海有限公司| 新疆新业能源有限公司| 裕佳昌国际货运有限公司| 中海集装箱运输有限公司招聘 | 上海思特电子有限公司| 贝儿孕婴用品有限公司| 广通汽车制造有限公司| 镇泰(广州)实业有限公司| 九江 服装有限公司| 浙江领航电子有限公司| 好上谷食品有限公司| 时珍堂药业有限公司| 鼎展国际贸易有限公司| 济南万达广场置业有限公司| 乐途汽车租赁有限公司| 柳州信息技术有限公司| 上海的高泵业有限公司| 振威安全技术发展有限公司| 中粮 成都 粮油工业有限公司| 洛阳众品食品有限公司| 东莞水泥制品有限公司| 慧瑞信息技术有限公司| 齐鲁制药有限公司 电话| 深圳易创电子有限公司| 烟台爱烟电子有限公司| 上海东松国际贸易有限公司| 深圳市康迈电子有限公司| 上海大宛食品有限公司| 北京电控设备有限公司| 试玩信息技术有限公司| 浙江进出口食品有限公司| 普迅电力信息技术有限公司| 江西宏声电子有限公司| 上海佳礼礼品有限公司| 山东安泰工程有限公司| 威斯卡特工业有限公司| 天河城百货有限公司| 博易通 深圳 有限公司| 优博防雨服饰有限公司| 多氟多新能源有限公司| 广州园林建设有限公司| 深圳茂业电子有限公司| 上海品奕金属有限公司| 美恒自动化有限公司| 奥力来康体设备有限公司| 彩虹庄印染有限公司| 卡斯柯信号有限公司 上海| 时代超声设备有限公司| 沱牌舍得酒业有限公司| 南昌辉门密封件有限公司| 南昌华兴针织有限公司| 锦州春光机械有限公司| 雅昌艺术印刷有限公司| 凯天装饰工程有限公司| 深圳旅游发展有限公司| 瑞得信息技术有限公司| 四川蓝光实业有限公司| 广西铝加工有限公司| 亚马逊 香港 有限公司| 深圳通科电子有限公司| 湘潭冶金设备制造有限公司| 淄博体育用品有限公司| 恒美化妆品有限公司| 湖南华网电力有限公司| 东方电器(大连)有限公司| 政通新型铝材有限公司| 云南腾药制药有限公司| 徽香源食品有限公司| 广州安仪电子有限公司| 上实环境水务有限公司| 香港港营香港有限公司| 啄木鸟北京有限公司| 瑞达塑料制品有限公司| 无锡新电动车有限公司| 苏州斯达克听力有限公司| 苏州日立汽车部件有限公司| 深圳宝创汽车贸易有限公司| 北京天明门业有限公司| 新世纪标志有限公司| 深圳诺金实业有限公司| 新疆神州医药有限公司| 上海 打印设备有限公司| 园创装饰工程有限公司| 海虹医药电子交易有限公司| 沃特斯阀门有限公司| 第一实业上海有限公司| 武汉和昌化工有限公司| 浙江亿江机电有限公司| 中博健康产业有限公司| 远东高中压阀门有限公司| 成都仁孚汽车有限公司| 上海高时石材有限公司| 东芝变压器有限公司| 汉邦化妆品有限公司| 长城建设监理有限公司| 可诺奈食品有限公司| 佳铭装饰工程有限公司| 奥豪斯仪器()有限公司| 威世(惠州)电子有限公司| 山东绿爱食品有限公司| 达飞轮船船务有限公司| 江苏爱特福有限公司| 重庆金华机械有限公司| 广州港新沙港务有限公司| 优胜辉煌教育有限公司| 上海尚地技术有限公司| 九毛九餐饮连锁有限公司| 首汽汽车租赁有限公司| 深圳市普森电子有限公司| 京特(深圳)有限公司| 成都市天然气有限公司| 利惠五金塑料有限公司| 迈龙汽车用品有限公司| 铭丰有限公司怎么样| 大明金属制品无锡有限公司| 上海璐璐实业有限公司| 深圳福尔电子有限公司| 兴盛塑料制品有限公司| 北京拓北技术有限公司| 佛山塑料材料有限公司| 修水神茶实业有限公司| 泰山工程机械有限公司| 深圳市新创电子有限公司| 赛思达机械设备有限公司| 中山市品仕有限公司| 上海喷码设备有限公司| 威斯泰电器有限公司| 泉州双环贸易有限公司| 南方摩托车有限公司| 山东济宁纺织有限公司| 爱安德(深圳)有限公司| 贵州黎阳发动机有限公司| 西安中科能源有限公司| 泰州中海油有限公司招聘| 浙能嘉兴发电有限公司| 莱州塑料机械有限公司| 珠海宇光电有限公司| 武汉尚品食品有限公司| 泰科电子有限公司待遇| 上海仪器销售有限公司| 李先生餐饮有限公司| 广州化工设备有限公司| 东航国际旅行社有限公司| 西安教育发展有限公司| 金宇生物药品有限公司| 南宁三峰能源有限公司| 网御星云信息技术有限公司| 振威安全技术发展有限公司| 漳州市裕食品有限公司| 上海好时贸易有限公司| 食安山东食品有限公司| 河南米河净化设备有限公司| 东莞市巨塑塑胶有限公司| 漳州经济发展有限公司| 上海欣和 食品有限公司| 无锡市 轮胎有限公司| 罗博泰尔机器人技术有限公司| 武汉大华纺织有限公司| 广东顺的建设工程有限公司| 华润置业有限公司地址| 天津起重机有限公司| 个人工作能力有限公司| 广州市浩洋有限公司| 广州康美药业有限公司| 东莞伟能五金电子有限公司| 梅里亚动物保健有限公司| 上海英宇塑料有限公司| 三元乳业上海有限公司| 东莞鑫联电子有限公司| 郑州丹尼斯有限公司| 杭州南方机电有限公司| 同捷汽车设计有限公司| 北京威汉机电有限公司| 成都全能保健有限公司| 海丰塑料包装有限公司| 杭州胜胜机电有限公司| 海南省农业有限公司| 欧普国际贸易有限公司| 亿凯信息技术有限公司| 广州天丰化工有限公司| 一航一工程有限公司地址| 北京石油在线技术有限公司| 陕西中勘工程有限公司| 烟台 传动技术有限公司| 上海 镱 有限公司| 沈阳日野汽车有限公司| 克鲁勃上海有限公司| 哈尔滨食品经销有限公司| 深圳和记黄埔地产有限公司| 广州旭众有限公司招聘| 上海宝原物业有限公司| 河北景观设计有限公司| 瑞康医疗用品有限公司| 唯爱思技术有限公司| 广东锻压机床有限公司| 北京正信担保有限公司| 北奔汽车重庆有限公司| 元进耐火材料有限公司| 利丰贸易(深圳)有限公司| 海德门电子有限公司| 捷信融资担保有限公司| 北京科学教育有限公司| 广州市金麦食品有限公司| 东莞智能装备有限公司| 青岛益和电气有限公司| 中冀联合通讯技术有限公司| 深圳顺章电器有限公司| 禹华通信技术有限公司| 上海中邦斯瑞有限公司| 上海于顺门窗有限公司| 深圳市信瑞达有限公司| 爱德华测量有限公司| 宁波 停车设备有限公司| 米乐星娱乐有限公司| 浙江省天然气有限公司| 紫薇星实业有限公司| 深圳中海能源有限公司| 佛山市金日有限公司| 青岛荏原环境有限公司| 黄石捷德万达金卡有限公司 | 宏威新材料有限公司| 广德南方水泥有限公司| 中铁融资租赁有限公司| 业聚医疗器械有限公司| 品辰装饰工程有限公司| 主力实业 深圳 有限公司| 深圳市黑豹建材有限公司| 广州越位服饰有限公司| 万裕三信电子有限公司| 深圳明泰电讯有限公司| 盛隆电气(北京)有限公司| 东曹广州化工有限公司| 上海以弈信息技术有限公司 | 中银(宁波)电池有限公司| 江苏江汇食品有限公司| 杭州 led 有限公司| 顺德区悦电器有限公司| 铭鸿建设工程有限公司| 苏州礼安医药连锁有限公司| 嵊州市汽车有限公司| 邢台精细化工有限公司| 佳源机电昆山有限公司| 特变电工硅业有限公司| 体育设施有限公司福建| 厦门太古宇航有限公司| 福马(福建)食品有限公司| 广州莉都化妆品有限公司| 云数贸贸易有限公司| 方向电子(深圳)有限公司| 汕头美饰实业有限公司| 天津神技夹具设备有限公司| 诸暨医药药材有限公司| 盛智营销策划有限公司| 唐山 电子工程有限公司| 山东莱芜钢铁有限公司| 辉瑞动物保健品有限公司| 华晨鑫源汽车有限公司| 北京sm娱乐有限公司| 烟台爱烟电子有限公司| 深圳市易能电气有限公司| 云南天叶茶叶有限公司| 深圳市通达光电有限公司| 重庆机械工程有限公司| 苏州化工机械有限公司| 上菱空调机电有限公司| 黑美人茶业有限公司| 首汽汽车修理有限公司| 硕士迪电子有限公司| 科美 杭州 机械有限公司| 山东潍坊烟草有限公司| 新疆新业能源有限公司| 伟康橡塑制品有限公司| 青岛马士基有限公司| 广东建筑材料有限公司| 深圳市融易投有限公司| 无锡恒发幕墙有限公司| 捷翔有限公司怎么样| 爱普科斯电阻电容有限公司| 佛山菲比家居有限公司| 深圳百盛百货有限公司| 庆安压缩机有限公司| 富宝(沙发)制造有限公司| 中睿合创广州有限公司| 深圳市科迈技术有限公司| 化妆品有限公司介绍| 商洛 农业 有限公司| 北京诺瑞医药有限公司| 兴河建设工程有限公司| 北京赛多利斯仪器有限公司| 上海英雄金笔有限公司| 勘测技术与工程有限公司| 广州三菱电机压缩机有限公司| 当代北辰置业有限公司| 振鹏达食品有限公司| 兴荣纺织品有限公司| 鲜活果汁昆山有限公司| 英菲克电子有限公司| 丰城瑞林稀贵有限公司| 宏创实业发展有限公司| 衡阳 制造有限公司招聘| 爱立华卫浴有限公司| 圣锦园林工程有限公司| 深圳市品尚服饰有限公司| 泰德兰电子有限公司| 北京康尔福有限公司| 中油恒燃石油燃气有限公司| 索悦电子产品有限公司| 广州汽车弹簧有限公司| 潍坊华电发电有限公司| 中通国际贸易有限公司| 北京中瑞能技术有限公司| 遵义钢结构有限公司| 南京璨宇光学有限公司| 德州宝雅汽车有限公司| 锐珂上海医疗器材有限公司| 深圳市捷信电子有限公司| 永盛进出口贸易有限公司| 马鞍山制药有限公司| 深圳创讯电子有限公司| 曼斯顿电梯浙江有限公司| 深圳市科聚材料有限公司| 健良(上海) 有限公司| 威海市针织有限公司| 上鱼舫餐饮有限公司| 上海天诚通信有限公司| 江特绝缘材料有限公司| 临清市临凯轴承有限公司| 生活客贸易有限公司| 上海集优机械有限公司| 贵阳融资担保有限公司| 深圳华峰实业有限公司| 山东江泉实业有限公司| 南京运输贸易有限公司| 辉瑞动物保健有限公司| 重庆旺庆物资有限公司| 福建永福工程顾问有限公司| 紫丁香日用品有限公司| 四川华晶玻璃有限公司| 浙江新东方紧固件有限公司| 蓝帜 南京 有限公司| 许继新能源有限公司| 泉州明辉轻工有限公司| 华院数据技术有限公司| 信音电子(苏州)有限公司| 甘肃变压器有限公司| 上海的高泵业有限公司| 金坛正信光伏有限公司| 温州瑞州建设有限公司| 广州西克传感器有限公司| 青岛马士基有限公司| 武汉桥梁监理有限公司| 深圳宝源贸易有限公司| 廊坊新奥燃气有限公司| 外电国际贸易有限公司| 广州珀比服饰有限公司| 华尔派建筑装饰工程有限公司| 东方汇装饰工程有限公司怎么样| 金桥焊接材料有限公司| 河南石油工程有限公司| 白马湖食品有限公司| 河北省玻璃钢有限公司| 苏净仪器自控设备有限公司| 铭铝铝业有限公司招聘信息| 厦门市政工程设计院有限公司 | 湖州过滤机械有限公司| 贵阳贸易有限公司招聘信息| 粤安消防工程有限公司| 顺嘉塑胶制品有限公司| 思迈医疗器械有限公司| 伟旺达电子有限公司| 味丰食品北京有限公司| 耐特汽车电子有限公司| 西藏信息技术有限公司| 湖南安化茶业有限公司| 广州康美药业有限公司| 苏州高中压阀门有限公司| 上海亚华湖有限公司| 海纳机械制造有限公司| 中机岩土工程有限公司| 佛山市致兴有限公司| 上海东聚电线有限公司| 凯源电力工程有限公司| 洁定医疗器械有限公司| 大明金属制品无锡有限公司 | 华正技术有限公司招聘信息| 东冠信息技术有限公司| 深圳市华良有限公司| 江西与德的发展有限公司| 青岛大地化工有限公司| 无锡零部件有限公司招聘| 江门市卫生用品有限公司| 联合赛尔生物工程有限公司| 华庄机电设备有限公司| 东莞市君业有限公司| 汇美(国际)有限公司| 深圳中深建装饰设计工程有限公司 | 民生健康药业有限公司| 大连 森 有限公司电话| 太原向明机械有限公司| 东风实业车辆有限公司| 金润 香港 有限公司| 成都鑫电缆有限公司| 海华建筑设计有限公司| 电科电气设备有限公司| 创全的发展有限公司|