Nginx 內存池(pool)分析

Nginx 內存池(pool)分析

Nginx 內存池管理的源碼在src/core/ngx_palloc.hsrc/core/ngx_palloc.c 兩個文件中。

先將我整理的註釋等內容貼上,方便下面分析:

ngx_create_pool:創建pool

ngx_destory_pool:銷燬 pool

ngx_reset_pool:重置pool中的部分數據

ngx_palloc/ngx_pnalloc:從pool中分配一塊內存

ngx_pool_cleanup_add:爲pool添加cleanup數據

 

struct ngx_pool_cleanup_s {

    ngx_pool_cleanup_pt   handler;  // 當前 cleanup 數據的回調函數

    void                 *data;     // 內存的真正地址

    ngx_pool_cleanup_t   *next;     // 指向下一塊 cleanup 內存的指針

};

 

struct ngx_pool_large_s {

    ngx_pool_large_t     *next;     // 指向下一塊 large 內存的指針

    void                 *alloc;    // 內存的真正地址

};

 

typedef struct {

    u_char               *last;     // 當前 pool 中用完的數據的結尾指針,即可用數據的開始指針

    u_char               *end;      // 當前 pool 數據庫的結尾指針

    ngx_pool_t           *next;     // 指向下一個 pool 的指針

    ngx_uint_t            failed;   // 當前 pool 內存不足以分配的次數

} ngx_pool_data_t;

 

struct ngx_pool_s {

    ngx_pool_data_t       d;        // 包含 pool 的數據區指針的結構體

    size_t                max;      // 當前 pool 最大可分配的內存大小(Bytes

    ngx_pool_t           *current;  // pool 當前正在使用的pool的指針

    ngx_chain_t          *chain;    // pool 當前可用的 ngx_chain_t 數據,注意:由 ngx_free_chain 賦值

    ngx_pool_large_t     *large;    // pool 中指向大數據快的指針(大數據快是指 size > max 的數據塊)

    ngx_pool_cleanup_t   *cleanup;  // pool 中指向 ngx_pool_cleanup_t 數據塊的指針

    ngx_log_t            *log;      // pool 中指向 ngx_log_t 的指針,用於寫日誌的

};

 

使用 ngx_create_poolngx_destory_poolngx_reset_pool三個函數來創建、銷燬、重置 pool。使用ngx_pallocngx_pnallocngx_pool_cleanup_add來使用pool。使用結構體 ngx_pool_t 管理整個 pool。下面將詳細分析其工作方式。

 

我們以 nginx 接受並處理 http 請求的方式,來分析pool的工作流程。

ngx_http_request.c 中,ngx_http_init_request 函數便是 http 請求處理的開始,在其中調用了 ngx_create_pool 來創建對應於 http 請求的 pool。同一個c文件中,ngx_http_free_request 函數便是 http 請求處理的結束,在其中調用了 ngx_destory_pool

我們一步步來看具體工作流程。首先,調用ngx_create_pool來創建一個pool,源碼如下:

ngx_pool_t *

ngx_create_pool(size_t size, ngx_log_t *log)

{

    ngx_pool_t  *p;

 

    // 分配一塊 size 大小的內存

    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log); 

    if (p == NULL) {

        return NULL;

    }

 

    // pool中的數據項賦初始值

    p->d.last = (u_char *) p + sizeof(ngx_pool_t);

    p->d.end = (u_char *) p + size;

    p->d.next = NULL;

    p->d.failed = 0;

 

    size = size - sizeof(ngx_pool_t);

    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;  // pool 中最大可用大小

 

    // 繼續賦初始值

    p->current = p;

    p->chain = NULL;

    p->large = NULL;

    p->cleanup = NULL;

    p->log = log;

 

    return p;

}

 

創建完pool後,pool示例如爲:

 

最左邊的便是創建的pool內存池,其中首sizeof(ngx_pool_t)便是poolheader信息,header信息中的各個字段用於管理整個pool。由於此時剛創建,pool中除了header之外,沒有任何數據。

注意:current 永遠指向此pool的開始地址。current的意思是當前的pool地址,而非pool中的地址。

從代碼的角度來說,pool->d.last ~ pool->d.end 中的內存區便是可用數據區。

 

    接下來,我們使用ngx_palloc從內存池中獲取一塊內存,源碼如下:

void *

ngx_palloc(ngx_pool_t *pool, size_t size)

{

    u_char      *m;

    ngx_pool_t  *p;

 

    // 判斷 size 是否大於 pool 最大可使用內存大小

    if (size <= pool->max) {

 

        p = pool->current;

 

        do {

            // m 對其到內存對齊地址

            m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);

            // 判斷 pool 中剩餘內存是否夠用

            if ((size_t) (p->d.end - m) >= size) {

                p->d.last = m + size;

 

                return m;

            }

 

            p = p->d.next;

 

        } while (p);

 

        return ngx_palloc_block(pool, size);

    }

 

    return ngx_palloc_large(pool, size);

}

此處需要分3步進行討論。當需要的內存大於pool最大可分配內存大小時;否則,當需要的內存大於pool目前可用內存大小時;否則,當需要的內存可以在此pool中分配時。

我們先從最簡單的情況開始,即,當需要的內存可以在此pool中分配時。此時從代碼流程可以看到,判斷內存夠用後,直接移動 p->d.last 指針,令其向下偏移到指定的值即可,使用此種方式可以避免新分配內存的系統調用,效率大大提高。此時的 pool 示例圖爲:

 

我們繼續討論第二種情況,當需要的內存大於pool目前可用內存大小時。從代碼流程可以看到,此時首先尋找pool數據區中的下一個節點,看是否有夠用的內存,如不夠,則調用ngx_palloc_block 重新分配,我們將問題簡單化,由於剛創建poolpool->d.next指針爲NULL,所以肯定會重新分配一塊內存。源碼如下:

static void *

ngx_palloc_block(ngx_pool_t *pool, size_t size)

{

    u_char      *m;

    size_t       psize;

    ngx_pool_t  *p, *new, *current;

 

    // 先前的整個 pool 的大小

    psize = (size_t) (pool->d.end - (u_char *) pool);

 

    // 在內存對齊了的前提下,新分配一塊內存

    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);

    if (m == NULL) {

        return NULL;

    }

 

    new = (ngx_pool_t *) m;

 

    // 對新分配的內存賦初始值

    new->d.end = m + psize;

    new->d.next = NULL;

    new->d.failed = 0;

 

    m += sizeof(ngx_pool_data_t);

    m = ngx_align_ptr(m, NGX_ALIGNMENT);

    new->d.last = m + size;

 

    current = pool->current;

 

// 判斷在當前 pool 分配內存的失敗次數,即:不能複用當前 pool 的次數,

// 如果大於 4 次,這放棄在此 pool 上再次嘗試分配內存,以提高效率

    for (p = current; p->d.next; p = p->d.next) {

        if (p->d.failed++ > 4) {

            current = p->d.next;

        }

    }

 

    // 讓舊指針數據區的 next 指向新分配的 pool

    p->d.next = new;

 

    // 更新 current 指針

    pool->current = current ? current : new;

 

    return m;

}

    通過上面可以看到,nginx 重新分配了一個新pool,新pool大小跟之前的大小一樣,然後對 pool 賦初始值,最終將新pool串到老pool的後面。注意,此處新poolcurrent指針目前沒有起用,爲NULL。另外,在此處會判斷一個pool嘗試分配內存失敗的次數,如果失敗次數大於4(不等於4),則更新current指針,放棄對老pool的內存進行再使用。此時的pool示例圖爲:

       

 

我們討論最後一種情況,當需要的內存大於pool最大可分配內存大小時,此時首先判斷size已經大於pool->max的大小了,所以直接調用ngx_palloc_large進行大內存分配,我們將注意力轉向這個函數,源碼爲:

static void *

ngx_palloc_large(ngx_pool_t *pool, size_t size)

{

    void              *p;

    ngx_uint_t         n;

    ngx_pool_large_t  *large;

 

// 重新申請一塊大小爲 size 的新內存

// 注意:此處不使用 ngx_memalign 的原因是,新分配的內存較大,對其也沒太大必要

//       而且後面提供了 ngx_pmemalign 函數,專門用戶分配對齊了的內存

    p = ngx_alloc(size, pool->log);

    if (p == NULL) {

        return NULL;

    }

 

    n = 0;

 

    // 查找可複用的 large 指針

for (large = pool->large; large; large = large->next) {

    // 判斷當前 large 指針是否指向真正的內存,否則直接拿來用

    // ngx_free 可使此指針爲 NULL

        if (large->alloc == NULL) {

            large->alloc = p;

            return p;

        }

 

        // 如果當前 large 後串的 large 內存塊數目大於 3 (不等於3),

// 則直接去下一步分配新內存,不再查找了

        if (n++ > 3) {

            break;

        }

    }

 

    // ngx_pool_large_t 分配一塊內存

    large = ngx_palloc(pool, sizeof(ngx_pool_large_t));

    if (large == NULL) {

        ngx_free(p);

        return NULL;

    }

 

    // 將新分配的 large 串到鏈表後面

    large->alloc = p;

    large->next = pool->large;

    pool->large = large;

 

    return p;

}

由如上代碼可知,函數首先申請一塊大小爲size的內存,然後判斷當前 large 鏈表中是否有存在複用的可能性,有的話,當然直接賦值返回;如果沒有,則新分配一塊大小爲sizeof(ngx_pool_large_t)的內存,串到large鏈表的後面。我們繼續上面的例子,由於之前沒有分配過large內存,所以此時直接將新內存塊串起來。此時pool示例圖爲:

 

         至此,在pool中分配普通內存的情況我們就討論完了。如果有新內存需要分配,無非也就是在pool中直接移動last指針,nextlarge next指針後面串接新的內存塊而已。

 

我們接下來看看函數ngx_pool_cleanup_add,在pool中分配帶有handler的內存,先上源碼:

ngx_pool_cleanup_t *

ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)

{

    ngx_pool_cleanup_t  *c;

   

    // 首先申請 sizeof(ngx_pool_cleanup_t) 大小的內存作爲header信息

    c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));

    if (c == NULL) {

        return NULL;

    }

 

if (size) {

    // cleanup 中有內存大小的話,分配 size 大小的內存空間

        c->data = ngx_palloc(p, size);

        if (c->data == NULL) {

            return NULL;

        }

 

    } else {

        c->data = NULL;

    }

   

    // cleanup 數據結構其他項進行賦值

    c->handler = NULL;

    c->next = p->cleanup;

 

    // cleanup 數據串進去

    p->cleanup = c;

 

    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);

    return c;

}

我們看到源碼首先分配 header 大小的頭信息內存,然後判斷是否要真正分配內存,如果要的話,分配內存,最後將新的數據塊串起來。我們繼續上面的示例圖,將分配一個 cleanup 之後的示例圖畫出。此時 pool 示例圖爲:

 

    在此順帶提一點,pool 中的 chain 指向一個 ngx_chain_t 數據,其值是由宏 ngx_free_chain 進行賦予的,指向之前用完了的,可以釋放的ngx_chain_t數據。由函數ngx_alloc_chain_link進行使用。

 

接下來我們通過上面的圖討論一下ngx_reset_pool函數,源碼:

void

ngx_reset_pool(ngx_pool_t *pool)

{

    ngx_pool_t        *p;

    ngx_pool_large_t  *l;

 

    // 釋放 large 數據塊的內存

    for (l = pool->large; l; l = l->next) {

        if (l->alloc) {

            ngx_free(l->alloc);

        }

    }

 

    // pool 直接下屬 large 設爲 NULL 即可,無需再上面的 for 循環中每次都進行設置

    pool->large = NULL;

 

    // 重置指針位置,讓 pool 中的內存可用

    for (p = pool; p; p = p->d.next) {

        p->d.last = (u_char *) p + sizeof(ngx_pool_t);

    }

}

    可以看到,代碼相當簡單,將largepool 中原有內存還原到初始狀態而已。

 

最後我們討論一下ngx_destory_pool函數,銷燬創建的pool,源碼:

void

ngx_destroy_pool(ngx_pool_t *pool)

{

    ngx_pool_t          *p, *n;

    ngx_pool_large_t    *l;

    ngx_pool_cleanup_t  *c;

 

    // 調用 cleanup 中的 handler 函數,清理特定資源

    for (c = pool->cleanup; c; c = c->next) {

        if (c->handler) {

            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,

                           "run cleanup: %p", c);

            c->handler(c->data);

        }

    }

 

    // 釋放 large 數據塊的內存

    for (l = pool->large; l; l = l->next) {

        ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);

 

        if (l->alloc) {

            ngx_free(l->alloc);

        }

    }

 

    // 釋放整個 pool

    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {

        ngx_free(p);

 

        if (n == NULL) {

            break;

        }

    }

}

代碼也相當簡單,首先調用 cleanup 中的handler函數來清理特定資源,然後釋放large內存,最終釋放整個pool

最終整個pool就銷燬的無影無蹤了。細心的朋友可能會發現,銷燬時似乎忘了釋放 cleanup 內存塊分配的內存了,真的是這樣嗎?呃,這個還是留給大家自己想吧。

 

 



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章