[nginx 源碼走讀] 內存池

[nginx 源碼走讀] 內存池

nginx 內存池(源碼)通過大小內存塊的鏈式管理邏輯大致如下圖(部分內存對齊的細節沒有添加進去):

內存池


內存池數據結構

小內存塊

小內存塊是通過鏈表進行管理,內存分配過程,涉及到結點上空閒內存匹配是鏈表的遍歷,複雜度是 O(n)O(n),爲了提高效率,增加了failed 分配內存失敗次數統計(具體邏輯在分配函數裏)

typedef struct {
    u_char               *last;
    u_char               *end;
    ngx_pool_t           *next;
    ngx_uint_t            failed;
} ngx_pool_data_t;

大內存塊

大內存塊沒有複雜的空閒空間管理邏輯,都是直接分配單獨的結點,需要銷燬時直接釋放。

typedef struct ngx_pool_large_s  ngx_pool_large_t;
struct ngx_pool_large_s {
    ngx_pool_large_t     *next;
    void                 *alloc;
};

內存文件

struct ngx_chain_s {
    ngx_buf_t    *buf;
    ngx_chain_t  *next;
};

內存池

nginx 內存池,主要通過大小空閒內存塊兩個鏈表進行維護, 內存池主要是對小塊內存(max)進行邏輯管理達到重複利用。

  1. 小內存分配,在小內存塊ngx_pool_data_t鏈表進行分配。
  2. 大內存分配,在大內存塊ngx_pool_large_t鏈表分配。

可能因爲大塊內存長度比較大,重複利用率比較低,而且佔用空間比較大,不宜長期留存在物理內存空間上,所以作者不對大塊內存進行復雜大內存空間管理。

typedef struct ngx_pool_s ngx_pool_t;
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;    // 日誌
};

接口

創建內存池

ngx_int_t
ngx_os_init(ngx_log_t *log) {
    ngx_pagesize = getpagesize();
}

#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1)
#define NGX_POOL_ALIGNMENT       16

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

    // 分配 16 字節對齊的內存。
    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;

    // 小塊內存大小,空閒內存最大小於 page size。
    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;
}

內存對齊申請空間

內存對齊,涉及到 cpu 工作效率,是高性能系統不可缺少的一環,有空可以深入研究。

#if (NGX_HAVE_POSIX_MEMALIGN)

void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log) {
    void  *p;
    int    err;

    err = posix_memalign(&p, alignment, size);

    if (err) {
        ngx_log_error(NGX_LOG_EMERG, log, err,
                      "posix_memalign(%uz, %uz) failed", alignment, size);
        p = NULL;
    }

    ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,
                   "posix_memalign: %p:%uz @%uz", p, size, alignment);

    return p;
}

#elif (NGX_HAVE_MEMALIGN)

void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log) {
    void  *p;

    p = memalign(alignment, size);
    if (p == NULL) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
                      "memalign(%uz, %uz) failed", alignment, size);
    }

    ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,
                   "memalign: %p:%uz @%uz", p, size, alignment);

    return p;
}


#else

#define ngx_memalign(alignment, size, log)  ngx_alloc(size, log)

#endif

#ifndef NGX_ALIGNMENT
#define NGX_ALIGNMENT   sizeof(unsigned long)    /* platform word */
#endif

釋放內存池

除了對大小內存塊數據進行釋放,還增加了回調操作的設計,方便開發者進行部分具體的業務處理。

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

    // 釋放大內存塊
    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;
        }
    }
}

分配內存

如果分配的內存在小內存塊空間範圍內,就通過小內存塊空閒鏈表中分配,否則直接分配到大內存塊鏈表中。

void *
ngx_palloc(ngx_pool_t *pool, size_t size) {
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) {
        return ngx_palloc_small(pool, size, 1);
    }
#endif
    return ngx_palloc_large(pool, size);
}

pool->max 查看 ngx_create_pool 的實現:

size = size - sizeof(ngx_pool_t);
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

分配小內存

滿足條件 size <= pool->max 的小內存的空間分配,遍歷小內存塊鏈表,從已分配的空間中查找合適的空閒空間進行分配,否則再創建新的小內存塊進行匹配。

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_ALIGNMENT對齊的偏移位置指針。
            m = ngx_align_ptr(m, NGX_ALIGNMENT);
        }

        // 如果有足夠空間,就返回分配的空間,空閒內存減少 size 大小
        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);

    // 分配 16字節對齊的空間。
    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_ALIGNMENT對齊的偏移位置指針
    m = ngx_align_ptr(m, NGX_ALIGNMENT);

    // 分配 size 大小的空閒空間出去
    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;
}

申請大塊內存

大塊內存已分配的大塊數據,除了內存塊頭部信息是可以重複利用的,數據不會重複利用,不用將被 ngx_pfree 釋放掉。

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

    // 新結點插入到表頭,有點像 lru,將活躍數據放到前面去。
    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;
}

釋放大內存塊

只是釋放數據,沒有釋放塊的數據結構頭。爲了重複利用數據結構頭信息,所以釋放數據並沒有刪除鏈表結點,這裏通過鏈表遍歷進行刪除,效率會不會很低。

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

重置內存池

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


問題

nginx 的內存池實現足夠精簡高效,但是依然有些問題不能兼顧到:

  • 鏈表管理:
    鏈表的查找遍歷時間複雜度是 O(n)O(n)ngx_pfree 效率不高。
  • 小內存塊鏈表,current 問題:
    當遇到密集地分配比較大的小內存場景時,導致已分配結點,分配失敗,failed 次數增加。current 指向新的結點,由於是單向鏈表,前面的結點其實還有足夠的空閒空間分配給其它小內存的,導致空閒空間利用率不高。
  • 大內存塊鏈表,重複利用已分配的信息頭問題:
    遍歷粒度很小,是否會產生大量內存碎片。
  • 小內存回收問題:
    內存池只對大內存塊進行內存回收,並沒有小內存塊的內存回收管理。只有 ngx_reset_poolngx_destroy_pool 是對其進行銷燬處理的。

所以綜合以上問題,這個內存池只適合於輕量級的內存管理。


測試

nginx 代碼耦合不是很大,可以扣出來調試跟蹤一下工作流程。(源碼

int main() {
    ngx_pool_t *pool = ngx_create_pool(2 * 1024);
    void *p = ngx_palloc(pool, 256);
    void *p2 = ngx_palloc(pool, 1024);
    void *p3 = ngx_palloc(pool, 1024);
    void *p4 = ngx_palloc(pool, 256);
    void *p5 = ngx_palloc(pool, 1024);
    void *p6 = ngx_palloc(pool, 1024);
    void *p7 = ngx_palloc(pool, 4 * 1024);

    ngx_pool_cleanup_t *c = (ngx_pool_cleanup_t *)ngx_pool_cleanup_add(pool, 0);
    memcpy(p, "hello world!", strlen("hello world!") + 1);
    c->handler = test_cleanup;
    c->data = p;

    ngx_destroy_pool(pool);
    return 0;
}

參考

Nginx 源碼分析-- 內存池(pool)的分析 三
nginx源碼分析–內存對齊處理
利用cpu緩存實現高性能程序
ngx_align_ptr

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