#內存管理的藝術# 之 Nginx slab的實現 --- 第三篇「基於塊的內存分配」

訪問這裏,獲取更多原創內容。    數組

    說明:本系列的文章基於Nginx-1.5.0版本代碼。佈局

    在「基本佈局」一篇中咱們曾經介紹過,ngx_slab.c的實現中將內存的分配分爲了兩個大類,除了上一篇講的「基於頁的內存分配」外,另外一類就是本篇中要介紹的「基於塊的內存分配」了。學習

    爲了可以知足對小塊內存的申請需求,Nginx slab分配器將頁劃分爲更小的塊(chunk),並引入了「slot分級內存管理數組」來與「page頁內存管理數組」一塊兒完成對小塊內存的分配和釋放流程的管理。ui

    根據「基本佈局」一篇中的內容能夠知道,Nginx slab分配器是按2的冪次大小來進行分級的:spa

Nginx slab分配器的分級結構圖.net

    當最小塊大小(min_size)爲8bytes,頁大小(pagesize)爲4096bytes時,分級數爲:debug

pagesize_shift - min_shift = 9

    即對應ngx_slab_init()中的n爲9,也就是說slot分級管理數組中有9個元素,咱們後面的討論都是基於這個模型來進行的。設計

    在上面的「Nginx slab分配器的分級結構圖」中咱們還看到了兩個比較陌生的變量:ngx_slab_exact_shift(7) 和 ngx_slab_exact_size(128),這兩個值分別表明什麼含義,又是怎麼獲得的呢?code

    前面說過,基於頁的內存管理單元和基於塊的內存管理單元均採用了ngx_slab_page_t結構來表示:blog

typedef struct ngx_slab_page_s  ngx_slab_page_t;

struct ngx_slab_page_s {
    uintptr_t         slab;
    ngx_slab_page_t  *next;
    uintptr_t         prev;
};
/*
typedef long intptr_t;
typedef unsigned long uintptr_t;
*/

    其中的slab字段在不一樣的場景下表示不一樣的含義:

  • 在基於頁的內存管理流程中,它能夠表示連續的空閒頁數目;
  • 在基於塊的內存管理流程中:

            1)當chunk_size == exact_size時,slab字段用做bitmap,表示一頁中各個內存塊的使用狀況;

            2)當chunk_size < exact_size時,slab字段用於存儲與塊大小相對應的移位數(chunk_shift);

            3)當chunk_size > exact_size時,slab字段的高16bits用做bitmap,而低16bits則用於存儲與塊大小對應的移位數;

    其中第一種含義在「基於頁的內存分配」中已經涉及到了,下面就來解釋一下後面的三種含義。

    在進行基於塊的內存管理時,slab分配器須要藉助bitmap來標記一頁中各個內存塊的使用狀況 ,若是想用一個uintptr_t類型的值來徹底表示一頁中的全部塊,那麼一頁能劃分的塊數爲:8 * sizeof(uintptr_t),每塊的大小就等於pagesize / (8 * sizeof(uintptr_t)),在32位環境下,就是 4096 / 32 = 128,這個值就是上面看到的ngx_slab_exact_size。

    當劃分的塊小於exact_size時,使用一個uintptr_t是沒法徹底表示一頁中的全部內存塊的,須要借用page頁中起始地址處的部份內存空間做爲bitmap,這時的slab字段只用於存儲與頁大小對應的shift移位數。

    當劃分的塊大於exact_size時,最多使用16bits就能夠徹底表示一個頁中全部塊的分配狀況了,這樣就能夠把一個uintptr_t類型值分爲2個部分,其中高16位用做bitmap,標記塊的分配狀況,而低16位則用於記錄對應的shift移位數。

    因此說,ngx_slab_exact_shift 和 ngx_slab_exact_size這兩個變量實際上在內存塊的分級管理中起到了很是重要的做用,slab分配器正是基於這兩個變量的值又將內存塊的管理細分爲上述3種不一樣的方式,若是你是初次接觸這部分的概念,不妨再仔細理解一下吧。

    下面就結合源碼來深刻學習吧:

void *
ngx_slab_alloc_locked(ngx_slab_pool_t *pool, size_t size)
{
    size_t            s;
    uintptr_t         p, n, m, mask, *bitmap;
    ngx_uint_t        i, slot, shift, map;
    ngx_slab_page_t  *page, *prev, *slots;

    ...
    /*基於頁的內存分配*/
    ...

    /*基於塊的內存分配*/
    /*根據申請的內存大小計算對應的分級數*/
    if (size > pool->min_size) {
        shift = 1;
        for (s = size - 1; s >>= 1; shift++) { /* void */ }
        slot = shift - pool->min_shift;

    } else {
        size = pool->min_size;
        shift = pool->min_shift;
        slot = 0;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0,
                   "slab alloc: %uz slot: %ui", size, slot);

    /*找到分級數組的首地址*/
    slots = (ngx_slab_page_t *) ((u_char *) pool + sizeof(ngx_slab_pool_t));
    page = slots[slot].next;

    /*根據「基本佈局」一篇所講的內容,在初始化完成以後slot分級數組下其實沒有掛接任何內容,因此在第一次分配時這裏的條件並不知足,會跳到後面去執行*/
    if (page->next != page) {
        ...
        ...
    }

    /*若是是第一次分配小塊內存,slot分級數組下尚未掛接任何內容,則會先跳轉到這裏分配一頁,因此說基於塊的內存分配實際上能夠看做是在內存頁的分配基礎上又作了進一步地細分,這也是咱們爲何把這一部分放在頁內存分配以後的緣由*/
    page = ngx_slab_alloc_pages(pool, 1);

    if (page) {
        /*對應於chunk_size < exact_size的狀況*/
        if (shift < ngx_slab_exact_shift) {
            /*這種狀況下須要將內存頁的起始部分劃分出來用做bitmap*/
            p = (page - pool->pages) << ngx_pagesize_shift;
            bitmap = (uintptr_t *) (pool->start + p);
            
            /*塊大小(取整到2的整數次冪)*/
            s = 1 << shift;
            /*
            (1 << (ngx_pagesize_shift - shift)) : 當塊大小爲s時,一個整頁能夠被劃分的塊數;
            (一頁中的塊數 / 8) : 當每塊用一個bit標識時,對應須要的字節(bytes)數;
            (字節數 / s) : 獲得bitmap須要佔用的塊數n
            */
            n = (1 << (ngx_pagesize_shift - shift)) / 8 / s;

            if (n == 0) {
                n = 1;
            }

            /*將bitmap佔用的塊數和即將分配出去的塊標記爲佔用*/
            bitmap[0] = (2 << n) - 1;

            /*與上面相似,這裏計算存放bitmap所須要的uintptr_t類型數據的個數,即bitmap數組長度*/
            map = (1 << (ngx_pagesize_shift - shift)) / (sizeof(uintptr_t) * 8);

            /*將後續的標誌位都清零*/
            for (i = 1; i < map; i++) {
                bitmap[i] = 0;
            }

            /*這時的slab字段存放與塊大小對應的shift移位值*/
            page->slab = shift;
            page->next = &slots[slot];
            page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;

            slots[slot].next = page;

            /*跳過用做bitmap的內存塊, 找到第一個空閒塊*/
            p = ((page - pool->pages) << ngx_pagesize_shift) + s * n;
            p += (uintptr_t) pool->start;

            goto done;

        } else if (shift == ngx_slab_exact_shift) {/*對應於chunk_size == exact_size的狀況*/

            /*這種狀況下slab字段被用做bitmap,將其賦值爲1表示第一塊即將被分配出去*/
            page->slab = 1;
            page->next = &slots[slot];
            page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_EXACT;

            slots[slot].next = page;

            p = (page - pool->pages) << ngx_pagesize_shift;
            p += (uintptr_t) pool->start;

            goto done;

        } else { /* shift > ngx_slab_exact_shift */ /*對應於chunk_size > exact_size的狀況*/

            /*這時最多隻須要16bits就能夠徹底表示一頁中全部塊的分配狀況了, 所以能夠將slab字段分爲兩個部分,其中的高16bits用做bitmap,而低16bits則記錄與塊大小相應的移位數*/
            page->slab = ((uintptr_t) 1 << NGX_SLAB_MAP_SHIFT) | shift;
            page->next = &slots[slot];
            page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_BIG;

            slots[slot].next = page;

            p = (page - pool->pages) << ngx_pagesize_shift;
            p += (uintptr_t) pool->start;

            goto done;
        }
    }

    p = 0;

done:

    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0, "slab alloc: %p", p);
    return (void *) p;
}

    下面的這幅佈局圖很是直觀地解釋了上述代碼,這裏給出的是 chunk_size(=16bytes) < exact_size 這種相對複雜場景下的內存分配狀況,觸類旁通,另外兩種場景就會很容易理解了。

    有了上面的內容做爲鋪墊,再來看看當不是第一次分配小塊內存的狀況下的處理流程:

void *
ngx_slab_alloc_locked(ngx_slab_pool_t *pool, size_t size)
{
    size_t            s;
    uintptr_t         p, n, m, mask, *bitmap;
    ngx_uint_t        i, slot, shift, map;
    ngx_slab_page_t  *page, *prev, *slots;

    ...
    /*基於頁的內存分配*/
    ...

    /*基於塊的內存分配*/
    /*根據請求的內存大小計算對應的分級數*/
    if (size > pool->min_size) {
        shift = 1;
        for (s = size - 1; s >>= 1; shift++) { /* void */ }
        slot = shift - pool->min_shift;

    } else {
        size = pool->min_size;
        shift = pool->min_shift;
        slot = 0;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0,
                   "slab alloc: %uz slot: %ui", size, slot);

    /*找到分級數組的首地址*/
    slots = (ngx_slab_page_t *) ((u_char *) pool + sizeof(ngx_slab_pool_t));
    page = slots[slot].next;

    /*若是slot分級數組下已經掛接了page頁管理結構,就從這裏開始執行*/
    if (page->next != page) {

        if (shift < ngx_slab_exact_shift) {

            do {
                p = (page - pool->pages) << ngx_pagesize_shift;
                bitmap = (uintptr_t *) (pool->start + p);

                /*計算bitmap數組長度*/
                map = (1 << (ngx_pagesize_shift - shift))
                          / (sizeof(uintptr_t) * 8);

                /*遍歷bitmap數組,找到一個空閒塊*/
                for (n = 0; n < map; n++) {

                    /*在32位環境下,NGX_SLAB_BUSY=0xffffffff,表示對應的bit所有置位,也就表明了沒有空閒塊*/
                    if (bitmap[n] != NGX_SLAB_BUSY) {

                        /*還有空閒塊*/
                        for (m = 1, i = 0; m; m <<= 1, i++) {
                            if ((bitmap[n] & m)) {
                                continue;
                            }

                            bitmap[n] |= m;

                            /*計算已經分配的塊對應的內存空間大小*/
                            i = ((n * sizeof(uintptr_t) * 8) << shift)
                                + (i << shift);

                            if (bitmap[n] == NGX_SLAB_BUSY) {
                                for (n = n + 1; n < map; n++) {
                                     /*若是後面還有空閒塊,則無需特殊處理,直接返回本次分配的地址*/
                                     if (bitmap[n] != NGX_SLAB_BUSY) {
                                         p = (uintptr_t) bitmap + i;

                                         goto done;
                                     }
                                }

                                /*該頁中已經沒有空閒塊,則將它從slot分級數組中摘掉,下次再有新的內存申請時可能須要從新申請一個新頁*/
                                prev = (ngx_slab_page_t *)
                                            (page->prev & ~NGX_SLAB_PAGE_MASK);
                                prev->next = page->next;
                                page->next->prev = page->prev;

                                page->next = NULL;
                                page->prev = NGX_SLAB_SMALL;
                           }

                            p = (uintptr_t) bitmap + i;

                            goto done;
                        }
                    }
                }

                page = page->next;

            } while (page);

        } else if (shift == ngx_slab_exact_shift) {

            do {
                /*這時slab字段中存儲的就是bitmap自己,slab!=NGX_SLAB_BUSY表示還有空閒塊*/
                if (page->slab != NGX_SLAB_BUSY) {

                    for (m = 1, i = 0; m; m <<= 1, i++) {
                        if ((page->slab & m)) {
                            continue;
                        }

                        page->slab |= m;

                        if (page->slab == NGX_SLAB_BUSY) {
                            prev = (ngx_slab_page_t *)
                                            (page->prev & ~NGX_SLAB_PAGE_MASK);
                            prev->next = page->next;
                            page->next->prev = page->prev;

                            page->next = NULL;
                            page->prev = NGX_SLAB_EXACT;
                        }

                        /*計算頁首地址偏移量*/
                        p = (page - pool->pages) << ngx_pagesize_shift;
                        /*計算塊地址偏移量*/
                        p += i << shift;
                        p += (uintptr_t) pool->start;

                        goto done;
                    }
                }

                page = page->next;

            } while (page);

        } else { /* shift > ngx_slab_exact_shift */

            /*在32位環境下,當exact_shift < chunk_shift( < page_shift)時,chunk_shift的取值可能爲八、九、十、11, 實際只需4個bits就能夠表示了,slab & NGX_SLAB_SHIFT_MASK(=0x0f),能夠取出chunk_shift*/
            n = ngx_pagesize_shift - (page->slab & NGX_SLAB_SHIFT_MASK);
            /*獲得一個頁面中所能劃分的塊數*/
            n = 1 << n;
            /*這些塊對應的bitmap掩碼*/
            n = ((uintptr_t) 1 << n) - 1;
            /*32位環境下,NGX_SLAB_MAP_SHIFT=16,即將掩碼移到高16位上,由於這時slab字段的高16位中存放的是bitmap的內容*/
            mask = n << NGX_SLAB_MAP_SHIFT;
            /*注意,上面這幾步的計算仍是很巧妙的*/

            do {
                /*NGX_SLAB_MAP_MASK=0xffff0000,根據掩碼判斷是否還有空閒塊*/
                if ((page->slab & NGX_SLAB_MAP_MASK) != mask) {

                    /*逐位查找空閒塊*/
                    for (m = (uintptr_t) 1 << NGX_SLAB_MAP_SHIFT, i = 0;
                         m & mask;
                         m <<= 1, i++)
                    {
                        if ((page->slab & m)) {
                            continue;
                        }

                        page->slab |= m;

                        if ((page->slab & NGX_SLAB_MAP_MASK) == mask) {
                            prev = (ngx_slab_page_t *)
                                            (page->prev & ~NGX_SLAB_PAGE_MASK);
                            prev->next = page->next;
                            page->next->prev = page->prev;

                            page->next = NULL;
                            page->prev = NGX_SLAB_BIG;
                        }

                        p = (page - pool->pages) << ngx_pagesize_shift;
                        p += i << shift;
                        p += (uintptr_t) pool->start;

                        goto done;
                    }
                }

                page = page->next;

            } while (page);
        }
    }

    /*若是是第一次分配小塊內存,slot分級數組下尚未掛接任何內容,則會先跳轉到這裏分配一頁*/
    page = ngx_slab_alloc_pages(pool, 1);

    if (page) {
        ...
        ...
    }

    p = 0;

done:

    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0, "slab alloc: %p", p);
    return (void *) p;
}

    下面一幅圖給出了當一頁中的內存塊都已經分配完畢,沒有空閒塊時的內存佈局狀況:

 

    到這裏Nginx slab中小塊內存的分配流程就基本介紹完了,能夠看到,因爲採用了內存對齊等機制,Nginx slab中的內存分配能夠很好地實現「自我管理」,也就是說在給出起始地址的狀況下可以方便快捷地正確推算出相關管理結構的位置和內容,而無需額外的存儲結構,這種設計值得好好學習和理解。

相關文章
相關標籤/搜索