1. 程式人生 > >菜鳥學習Nginx之記憶體池

菜鳥學習Nginx之記憶體池

從今天開始深入介紹Nginx框架。

首先來談談我對《深入理解Nginx模組開發與架構解析》看法,這本書應該是到目前為止,市面寫的最詳細,最充實的書籍(沒有之一),值得擁有。然而此書對於一個小白來說,並不太適合,此書適合有相關使用經驗或者開發經驗,適合於進一步深造的同學。如果是小白,建議先瀏覽一下網上的部落格,對Nginx各個方面有一定了解,然後在深入閱讀此書。這是僅僅是我個人經驗,畢竟我是這樣走過來的。最後建議閱讀此書的朋友,最好多閱讀幾遍,每一次都有不少的收穫。

開始我們今天的主題,任何一款軟體都離不開資料結構,良好的資料結構對於軟體的發展會起到事半功倍的效果。

一、Nginx很奇葩

Nginx這款軟體很奇葩,但同時體現出它的優秀。說它奇葩之處體現在:為了節約記憶體,不會輕易主動申請記憶體,而經常複用,例如利用一個指標最低2bit始終為0這個特性,來儲存一個欄位。但是它為了提升效能,卻又申請大塊空間,例如在共享記憶體方面,它為每個變數申請128位元組(考慮CPU二級快取)。

二、資料結構

2.1資料結構

本篇將介紹始終貫穿整個軟體的物件--ngx_pool_t記憶體池。記憶體池存在意義,不用多說,直接上資料結構定義。

typedef struct ngx_pool_large_s  ngx_pool_large_t;

//申請大記憶體段

struct ngx_pool_large_s {

    ngx_pool_large_t     *next;

    void                 *alloc; /* 儲存通過malloc返回的指標 */

};

//記憶體池元資料 用於把記憶體池節點使用連結串列方式關聯起來

typedef struct {

    u_char               *last; /* 可分配起始位置 */

    u_char               *end;  /* 當前記憶體池塊 最後有效位置 */

    ngx_pool_t           *next; /* 指向下一個記憶體池塊 */

    ngx_uint_t            failed; /* 代表從池中申請記憶體失敗次數 */

} ngx_pool_data_t;



/* 記憶體池頭,跳過記憶體池頭是資料區 */

typedef struct ngx_pool_s            ngx_pool_t;

struct ngx_pool_s {

    ngx_pool_data_t       d; /* 當前記憶體池元資料 */

    size_t                max; /* 申請空間大於max則表示需要申請大記憶體,只在建立記憶體池時賦值 */

    ngx_pool_t           *current; /* current用於加速遍歷 */

    ngx_chain_t          *chain; /* 在http filter模組中使用 主要用於http response */

    ngx_pool_large_t     *large;  /* 大記憶體 */

    ngx_pool_cleanup_t   *cleanup;  /* 設定回撥 ngx_pool_cleanup_add */

    ngx_log_t            *log;

};

2.2、記憶體池組織形態

2.3 特點

Nginx實現的記憶體池有如下特點:

  1. 為了滿足大記憶體需求(一個記憶體池節點,實際可用記憶體大小是固定的max,當申請的記憶體大於max則認為是大記憶體),nginx設計一個獨立連結串列(large)用於儲存大記憶體塊。大記憶體塊採用malloc/free直接申請堆記憶體,可見大記憶體適用生命週期較短場景,否則會把記憶體耗盡。大記憶體頭部ngx_pool_large_t記憶體是從當前記憶體池中申請。為什麼這樣設計呢?為了複用。
  2. 對於大記憶體,始終存放到記憶體池首節點中,對大記憶體頭sizeof(ngx_pool_large_t)的記憶體空間一定是來自current所指向記憶體池節點。如上圖所示,可參考後續原始碼分析中。
  3. 往往的應用層業務邏輯很複雜,為了方便通常在業務邏輯最後環節進行資源的回收,Nginx也考慮到此需求,所以在記憶體池中增加ngx_pool_cleanup_t結構。注意:雖然這裡名稱叫做pool_cleanup,業務層只需要設定好自己的回撥函式即可。Nginx框架在釋放記憶體池之前就會呼叫回撥函式。至於回撥函式內容就非常靈活,完全取決於當前業務邏輯。例如:關閉各種檔案控制代碼,刪除各種臨時資料檔案等。
  4. 資料結構 ngx_pool_data_t,是將記憶體池節點以連結串列形式進行關聯即next,每次建立新記憶體節點掛在連結串列最後zuiho。其中end-last(減法)等於可用記憶體空間。

 三、相關介面

下面介紹相關程式碼,畢竟只有看懂程式碼才能深入理解其中的內涵

/**
 * 建立記憶體池
 */
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);//16位元組對齊
    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;
}

簡要說明:

  1. 為了保證訪問速度,採用16位元組方式對齊。
  2. 如果申請的記憶體大小大於一個記憶體頁大小(一般是4k),雖然能夠申請成功,但是有記憶體浪費。因此在使用記憶體時需要注意大小。
/**
 * 銷燬記憶體池
 */
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);//執行回撥函式
        }
    }


/*為了篇幅 刪除debug除錯資訊 */

//釋放大記憶體,由此可知所有的大記憶體均放在記憶體池首節點中
    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;
        }
    }
}


/**
 * 業務模組設定清理回撥
 */

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;//設定null,由呼叫者在外部設定
    c->next = p->cleanup;
    p->cleanup = c;
    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
    return c;
}

那麼如使用記憶體池呢?非常簡單,直接呼叫ngx_palloc/ngx_pnalloc,流程圖如下:

這裡需要闡明一個觀點,對於Nginx來說,所有申請的記憶體均來自記憶體池(除大記憶體),可以理解成萬物皆池化。

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC) //預設不開啟
    if (size <= pool->max) {//檢查待申請的記憶體是否大於max,如果大於則表明申請大記憶體
        return ngx_palloc_small(pool, size, 1);
    }
#endif
    return ngx_palloc_large(pool, size);

}

/**
 * 從記憶體塊中分配記憶體
 */
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);//分配新記憶體池節點
}

/**
 * 向作業系統申請分配記憶體塊
 * block 代表塊
 */
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;/* size表示業務從記憶體池中申請的空間大小 */

    /* 將每一個記憶體失敗次數加1 如果失敗次數大於4次 則修改current指標 提升遍歷速度 */
    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;//始終條current指標
        }
    }

    p->d.next = new;//掛連結串列 放到連結串列最後
    return m;
}

/**
 * 申請大記憶體 直接向作業系統申請
 */
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;
    /* 遍歷large連結串列 遍歷三次仍然沒有找到合適位置 則建立一個新節點 */
    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }

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


    /**
     * 建立新節點然後在連結串列頭插入 large頭部資訊 也是從池中分配
     * 此處比較巧妙 體現萬物皆池化的特點
     */
    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;

}

四、記憶體池生命週期

      在Nginx中有三種不同生命週期的記憶體池為:程序級、連線級、請求級。

級別

存活時長

說明

程序級

伴隨整個程序,時間最長

ngx_cycle_t中記憶體池

連線級

伴隨tcp會話,時間居中

ngx_connection_t中記憶體池

請求級

伴隨http一次請求,時間最短

ngx_http_request_t中記憶體池

為什麼會出現三種級別的記憶體池呢?仔細想想可知,對於Nginx萬物皆池化,所有記憶體的申請必須通過記憶體池。

一個程序啟動肯定需要一個(一些)用於儲存全域性資料。

Nginx是用於網路通訊,自然需要維持tcp相關資料,例如:對於長連線http請求。

請求級,自然對應http請求,雖然http採用長連線方式,但是每一次http請求可能都不一樣,自然需要為每個http請求分配一個記憶體池。

五、總結

       Nginx實現的記憶體池比較簡單易懂,我們在開發自己的應用程式,只要保證所有記憶體均來自記憶體池這唯一標準,那麼就不會出現記憶體問題。萬物皆池化!!下一篇,我們來看看ngx_buf_t