1. 程式人生 > >[nginx] nginx記憶體池ngx_pool_t的介紹

[nginx] nginx記憶體池ngx_pool_t的介紹

本文詳細介紹nginx中記憶體池的設計和實現。

 

nginx pool 由 小記憶體拉鍊、大記憶體拉鍊、回撥函式拉鍊組成。建立的第一個ngx_pool_s頭部,會作為整個pool的head,儲存一些拉鍊資訊。

1 建立一個ngx_pool:

struct ngx_pool_s {
    ngx_pool_data_t       d;
    size_t                max;
    ngx_pool_t           *current;
    ngx_chain_t          *chain;
    ngx_pool_large_t     *large;
    ngx_pool_cleanup_t   *cleanup;
    ngx_log_t            *log;
};
typedef struct {
    u_char               *last;
    u_char               *end;
    ngx_pool_t           *next;
    ngx_uint_t            failed;
} ngx_pool_data_t;

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

如上程式碼,ngx_create_pool建立了第一個ngx_pool_t(ngx_pool_s),pool的起始地址會做16位元組對齊。第一個pool結構如下:

第一個pool結構的頭部,儲存了整個池子的資訊。其中,last和end分別指向當前poll小記憶體的起止位置;next指向下一個pool結構,是小記憶體拉鍊;failed儲存當前pool結構申請小記憶體失敗的次數,用於判定當前pool是否可用;max是小記憶體的閾值,申請記憶體大於max認為是大記憶體;current指向當前可用的起始pool;chain目前沒用到;large儲存當前pool結構中的大記憶體拉鍊;cleanup是個回撥函式拉鍊;log用來記錄日誌。

2 ngx_pool記憶體操作

第一個ngx_pool_t創建出來之後,就可以從pool中申請記憶體。小記憶體直接從last和end之間的區域獲取;大記憶體用malloc申請,還要從小記憶體區域申請一個結構體儲存大記憶體地址,然後把這個結構體儲存在large拉鍊下。小記憶體不夠的時候,會建立一個新的ngx_pool_t。

從pool中申請記憶體,提供了以下四個函式:

void *ngx_palloc(ngx_pool_t *pool, size_t size);
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);

ngx_palloc申請小記憶體時會做位元組對齊,而ngx_pnalloc不做,兩者都根據pool->max判斷申請小記憶體或大記憶體;ngx_pcalloc呼叫ngx_palloc,然後清空記憶體;ngx_pmemalign直接申請大記憶體,並且會做位元組對齊。

2.1 申請小記憶體

小記憶體的申請由函式ngx_palloc_small完成:

static ngx_inline void *
ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align)
{
    u_char      *m;
    ngx_pool_t  *p;

    p = pool->current;

    do {
        m = p->d.last;

        if (align) {
            m = ngx_align_ptr(m, 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);
}


static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new;

    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;

    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;
        }
    }

    p->d.next = new;

    return m;
}

小記憶體申請,從current指向的當前可用pool結構開始遍歷,對於每個pool結構,嘗試從小記憶體空間申請記憶體。當所有pool中小記憶體空間都不足的時候,建立一個新的pool,也就是ngx_palloc_block

ngx_palloc_block申請一個新的pool結構,掛在pool->d.next的尾部。同時,從新申請的pool中分配需要的記憶體,此時固定做位元組對齊;更新current指向的可用pool,由以上程式碼可以知道,current拉鍊所表示的當前可用的pool最多隻有5個。不能說current拉鍊長度最長只有5,因為後續有reset操作,可以將current恢復到第一個pool,但是整個池子的所有pool拉鍊都還在。

新申請的pool結構如下:

可以看到,和第一個pool結構不同,只有ngx_pool_data_t。ngx_pool_t是ngx pool的結構,ngx_pool_data_t是pool的小記憶體拉鍊,將ngx_pool_data_t放在ngx_pool_t的開始位置,可以方便進行型別轉換,當前的做法,可以看做是將ngx pool的結構放在了小記憶體拉鍊的第一塊中。

2.2 申請大記憶體

大記憶體申請由函式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;

    p = ngx_alloc(size, pool->log);
    if (p == NULL) {
        return NULL;
    }

    n = 0;

    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }

        if (n++ > 3) {
            break;
        }
    }

    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;
}

malloc申請大記憶體,從large拉鍊中找到空著的large塊,只找前四個,找到就填進去,找不到就用小記憶體申請函式申請一個large塊,把這個large塊放到large拉鍊頭部。large塊的申請,會做位元組對齊,但是大記憶體本身不做。

重用空閒的large塊,節約記憶體空間;只從前四個large塊中查詢,減少large拉鍊過長時查詢的時間。頭插法的large拉鍊,基於最近申請的大記憶體,更有可能被釋放的假設。

2.3 對大記憶體進行位元組對齊

ngx_pmemalign 和 ngx_palloc_large的區別在於兩點:對大記憶體進行位元組對齊;不找空著的large塊。依然是頭插法,放到large拉鍊的頭部。程式碼不放了。

2.4 記憶體釋放

ngx_int_t
ngx_pfree(ngx_pool_t *pool, void *p)
{
    ngx_pool_large_t  *l;

    for (l = pool->large; l; l = l->next) {
        if (p == l->alloc) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "free: %p", l->alloc);
            ngx_free(l->alloc);
            l->alloc = NULL;

            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}

遍歷拉鍊,釋放large拉鍊的記憶體,這樣申請large記憶體的時候就有必要檢查是否有空餘的large了。small記憶體不能單獨釋放。

2.5 pool重置

void
ngx_reset_pool(ngx_pool_t *pool)
{
    ngx_pool_t        *p;
    ngx_pool_large_t  *l;

    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }

    for (p = pool; p; p = p->d.next) {
        p->d.last = (u_char *) p + sizeof(ngx_pool_t);
        p->d.failed = 0;
    }

    pool->current = pool;
    pool->chain = NULL;
    pool->large = NULL;
}

reset操作釋放所有large記憶體,large塊本身是在small區域申請的,所以不需要釋放;重置last指標的位置,重用small記憶體區域;current恢復到第一個pool。

這裡要注意的是,經過reset之後,所有pool結構的small區域起始位置都和第一個pool保持一致了,這和新申請pool結構的時候不同,reset後有部分small區域被浪費了。如下圖:

2.6 pool銷燬

void
ngx_destroy_pool(ngx_pool_t *pool)
{
    ngx_pool_t          *p, *n;
    ngx_pool_large_t    *l;
    ngx_pool_cleanup_t  *c;

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

#if (NGX_DEBUG)

    /*
     * we could allocate the pool->log from this pool
     * so we cannot use this log while free()ing the pool
     */

    for (l = pool->large; l; l = l->next) {
        ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
    }

    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
        ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                       "free: %p, unused: %uz", p, p->d.end - p->d.last);

        if (n == NULL) {
            break;
        }
    }

#endif

    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }

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

        if (n == NULL) {
            break;
        }
    }
}

銷燬時,首先依次遍歷cleanup拉鍊,呼叫回撥函式釋放對應的記憶體;之後釋放large記憶體;之後依次釋放所有的pool。

2.7 cleanup拉鍊

typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;

struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt   handler;
    void                 *data;
    ngx_pool_cleanup_t   *next;
};

ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
    ngx_pool_cleanup_t  *c;

    c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
    if (c == NULL) {
        return NULL;
    }

    if (size) {
        c->data = ngx_palloc(p, size);
        if (c->data == NULL) {
            return NULL;
        }

    } else {
        c->data = NULL;
    }

    c->handler = NULL;
    c->next = p->cleanup;

    p->cleanup = c;

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

    return c;
}

cleanup塊記憶體從pool池子中申請。data記憶體可以從pool中獲得,也可以是外部申請,外部申請的data需要由回撥函式銷燬。cleanup拉鍊是頭插法,這意味著,最後進入池子的回撥函式,在銷燬時會被優先執行。

有了ngx_pool_cleanup_t,可以提供類似cpp中delete的行為,此時的回撥函式類似於解構函式。

2.8 fd相關的cleanup

記憶體池還提供了一些fd相關的cleanup拉鍊操作。其實這個和記憶體池關係不大,個人理解,因為cleanup拉鍊儲存的各種資料,預設只在池子銷燬的時候,才由回撥函式銷燬,提供fd相關函式,是為了在池子銷燬之前能夠對 fd進行一些操作,目前提供了清理指定fd的操作。相關函式如下,詳見原始碼。

typedef struct {
    ngx_fd_t              fd;
    u_char               *name;
    ngx_log_t            *log;
} ngx_pool_cleanup_file_t;

void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd);
void ngx_pool_cleanup_file(void *data);
void ngx_pool_delete_file(void *data);