深度解析 slab 記憶體池回收記憶體以及銷毀全流程

来源:https://www.cnblogs.com/binlovetech/archive/2023/05/26/17434311.html
-Advertisement-
Play Games

在上篇文章 [《深入理解 slab cache 記憶體分配全鏈路實現》](https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&mid=2247488152&idx=1&sn=7c65f8ee28e9cc14a86e9df92b6d2b93&chksm=c ...


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

image

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

image

內核提供了 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。

image

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 中。

image

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 。

image

如果當前釋放對象並不在 slab cache 的本地 cpu 緩存中,那麼就會進入慢速路徑 slowpath 釋放記憶體。

3. slab cache 在慢速路徑下回收記憶體

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

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

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

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

image

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

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

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

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

image

  1. 如果 slab 不在 kmem_cache_cpu->partial 鏈表中,並且該 slab 由於對象的釋放剛好由一個 partial slab 變為了一個 empty slab,說明該 slab 並不是很活躍,內核會將該 slab 放入對應 NUMA 節點緩存 kmem_cache_node->partial 鏈表中,刀槍入庫,馬放南山。

image

  1. 如果不符合第 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 結構體中的每個欄位都是獨占一片記憶體的。

image

由於 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 鏈表中

image

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

 was_frozen = new.frozen;

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

image

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

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

image

如果釋放對象所屬 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);
        }
        

image

將 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 的限制。

image

如果超過了,則需要先將當前 kmem_cache_cpu->partial 鏈表中所有的 slab 轉移到其對應的 NUMA 節點緩存 kmem_cache_node->partial 鏈表中。轉移完成之後,在將釋放對象所屬的 slab 插入到 kmem_cache_cpu->partial 鏈表中。

image

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

image

如果釋放對象所在的 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 釋放回伙伴系統中。

image

        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 的位置。

image

4. slab cache 的銷毀

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

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

image

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

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

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

  3. 在 sys 文件系統中移除 /sys/kernel/slab/<cacchename> 節點相關信息。

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

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

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

image

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 需要滿足以下四個條件:

  1. 指定的 slab_flags_t 相同。

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

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

  4. 已有 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 占用的所有資源

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

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

  3. 在 sys 文件系統中移除 /sys/kernel/slab/<cacchename> 節點相關信息。

/*
 * 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 系列涉及到的重點內容再次梳理總結一下。

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

image

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

image

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

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

image

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

image

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

image

最後筆者介紹了 slab 記憶體池的銷毀過程:

image

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ​ # Maven的核心解壓與配置 @[toc] ## 1. Maven 官網地址 首頁:[Maven – Welcome to Apache Maven(opens new window)](https://maven.apache.org/) ![在這裡插入圖片描述](https://img20 ...
  • 36 package cn.com.pep; 37 import java.util.concurrent.TimeUnit; 38 import java.util.concurrent.locks.AbstractOwnableSynchronizer; 39 import java.util. ...
  • Python全景系列的第七篇,本文將深入探討Python模塊與包的基本概念,使用方法以及其在實際項目中的應用。我們也會揭示一些鮮為人知,卻又實用的技術細節。 ...
  • > ML.Net - 開源的跨平臺機器學習框架 > - 支持CPU/GPU訓練 > - 輕鬆簡潔的預測代碼 > - 可擴展其他的機器學習平臺 > - 跨平臺 ![img](https://img2023.cnblogs.com/blog/1339560/202305/1339560-20230524 ...
  • 要使用 `systemctl` 啟動單個服務,其中包含多個進程,你可以使用 Systemd 的 `template` 機制。以下是使用 Systemd 'template' 以創建一個可同時啟動多個進程的服務單元文件的過程: 1. 為你的服務創建一個 template 服務單元文件。服務單元文件通常 ...
  • 目錄 一、條件判斷 二、邏輯判斷 三、if和case 四、七個實驗 一、條件判斷 1.test測試 test [ 條件表達式 ] -e:測試目錄是否存在 -d:測試是否為目錄 -f:是否為文件 -r:當前用戶是否有讀寫許可權 -w:當前用戶是否有寫許可權 -x:當前用戶是否有執行許可權 2.整數值判斷 格 ...
  • INFINI Labs 產品更新啦~,本次產品版本更新包括 Gateway v1.14.0、Console v1.2.0、Easysearch v1.1.1 等,其中 Console 在上一版基礎上做了很多優化改進以及新增了一些特性,如新增數據比對校驗功能、數據看板模塊新增了表格組件、圖表組件支持下 ...
  • 我們都知道,預設情況下,nginx的項目log是一直被累計寫入的,隨著時間越久,那麼這個文件就會越大,這個時候如果我們要去做一些查找和排查就會比較困難,因為日誌文件太大,操作起來比較費勁。 因此我們為了規避這個問題,提出日誌切割的方案。 那日誌切割的原理是怎麼樣的,我們來分析一下,我們先統計下連續1 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...