nginx內存池基本原理及問題

nginx的內存池相關文章已經很多了,這裏寫一下簡單原理和最近碰到的問題。

用到的幾個結構,相應說明請看註釋:

//每次能從pool分配的最大內存塊大小,ngx_pagesize在X86下一般是4096,即4k,也就是說每次能從pool分配的最大內存塊大小爲4095字節,將近4k
#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1)

//默認pool大小:16k
#define NGX_DEFAULT_POOL_SIZE    (16 * 1024)

struct ngx_pool_large_s {  /*大塊內存數據結構*/
    ngx_pool_large_t     *next;  /*其實是一個頭插法的單鏈表,每次分配一個大塊內存都將列表節點插入到這個鏈表的表頭*/
    void                 *alloc; /*大塊內存是直接用malloc來分配的,alloc就是用來保存分配到的內存地址*/
};


typedef struct {  /*一個內存池是由多個pool節點組成的鏈,這個結構用來鏈接各個pool節點和保存pool節點可用的內存區域起止地址*/
    u_char               *last;  /*當前內存分配結束位置,即下一段可分配內存的起始位置*/
    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;     /*一次能分配的最大內存大小*/
    ngx_pool_t           *current; /*用來保存當前從哪個pool上分配內存的pool指針,每次分配內存都會從current指向的pool上分配*/
    ngx_chain_t          *chain;
    ngx_pool_large_t     *large;   /*大塊內存列表,*/
    ngx_pool_cleanup_t   *cleanup;
    ngx_log_t            *log;
};


先看一下創建內存池的實現代碼,不到20行的代碼,很簡單的:

ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
    ngx_pool_t  *p;

    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
    if (p == NULL) {
        return NULL;
    }

    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;

    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p;
}

從上面的代碼可以知道max最大爲4095,也就是說每次申請的內存最大大小爲4095字節,超出則使用大塊內存(參考ngx_palloc和ngx_palloc_large的實現,這裏不講了)。

在X64下(下同),調用pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, log)後,得到的pool內存大小及佈局如下:


last和end指針指向的內存區域就是可用空間,pool頭已經佔80個字節了,所以可用空間比創建時指定的pool大小少80個字節,這裏是我覺得設計得不合理的地方,這個pool的大小應該等於用戶在創建pool時指定的大小加上pool頭大小,這樣用戶創建了多少就能用多少。

得到pool之後就可以從裏面分配內存了,先來看一下分配內存的函數實現,也是幾行代碼:

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    ngx_pool_t  *p;

    if (size <= pool->max) {

        p = pool->current;

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

            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);
}

比如調用p = ngx_palloc(pool, 32);  last會向後移動32個字節,內存佈局如下:


假設這個pool可用空間不夠了,那麼會調用ngx_palloc_block分配一個與當前pool大小一樣的pool,並將該pool掛在內存池鏈上和將last指針調整好之後直接返回給用戶可用的內存地址,如下圖是調用q = ngx_palloc(pool, 128);後的內存佈局,左邊是可用空間不夠的pool,右邊是ngx_palloc_block分配的pool:


以上就是nginx內存池的基本原理,首先分配一個大塊內存,然後每次分配小塊內存時直接修改last指針後直接返回內存地址,非常之高效。

基本原理講完了,再來看看ngx_palloc的bug,這個bug隱藏得比較深,耗費了好幾天才搞定。這個bug是我同事KawaruNagisa發現的,他也給官網提bug並accept了,相信下個版本會得到修復的。我接觸nginx時間不是很長,這個bug正好有機會來研究研究nginx的內存池實現。先來看看ngx_palloc的幾行代碼:

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
……
            m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);           //內存對齊,在64位系統下是8字節對齊

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

                return m;
            }
……
}

上面的代碼中本意是先得到對齊之後的m,並和end指針對比,如果end和m之間的可用空間大於等於要分配的size,那麼就分配成功,並將last向後移動。但是還有另一種情況沒有考慮到:假設 last=28, end=30, size = 32, end-last=2,即end和last之間只有2個字節的可用空間,那麼將last 8字節對齊之後爲32,即m=32,那麼end-m=-2,-2再轉換成size_t則變成了18446744073709551614,再跟size相比,肯定爲true,接着調整last指針並返回m;而我們要分配32個字節,應該再分配一個pool並在這個pool上分配內存,顯然是bug啊。用gdb模擬一下:


所以上面代碼中if條件裏應該加上p->d.end > m ,修復後的ngx_palloc應該如下:

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    ngx_pool_t  *p;

    if (size <= pool->max) {

        p = pool->current;

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

            if (p->d.end > m && (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);
}

另一種避免這個Bug的方法是創建pool時(ngx_create_pool)指定的size要按NGX_ALIGNMENT字節對齊,否則比較容易出Bug。


更詳細的nginx內存池講解請參考:

http://blog.csdn.net/v_july_v/article/details/7040425

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