首頁  >  文章  >  運維  >  nginx記憶體池如何實現

nginx記憶體池如何實現

WBOY
WBOY轉載
2023-05-17 13:26:271313瀏覽

一、簡介

最新穩定版本nginx1.20.2。
為了能高效、快速的分配內存,以及減少內存碎片等,nginx實現了自己的內存池基礎組件。
主要實作檔案ngx_palloc.h, ngx_palloc.c

#二、資料結構

2.1 記憶體池主要結構

typedef struct {
    u_char               *last;
    u_char               *end;
    ngx_pool_t           *next;
    ngx_uint_t            failed;
} ngx_pool_data_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_pool_data_t結構體來表示目前記憶體池資訊。
last :下次開始分配的位址
end: 記憶體池的結束位址
next: 記憶體池鍊錶,將多個記憶體池連接起來

max
整個記憶體池的最大大小

current
指向從目前記憶體池開始尋找可用記憶體

chain
buffer所使用的,這裡不涉及

large
當需要的記憶體大於記憶體池最大大小時,需要透過malloc直接分配,然後形成鍊錶進行組織

cleanup
清理工作的回呼鍊錶

log
日誌句柄

2.2 大記憶體鏈

當需要分配的記憶體比記憶體池的最大大小都大時,記憶體池無法滿足分配,所以直接從系統中分配,然後構成一個鍊錶進行維護。

typedef struct ngx_pool_large_s  ngx_pool_large_t;

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

2.3 清理任務鏈

有一個回呼任務的鍊錶,當記憶體池銷毀時,將依序遍歷此鍊錶,逐一回調handler進行清理工作。

typedef void (*ngx_pool_cleanup_pt)(void *data);

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

三、記憶體結構圖

3.1 邏輯

nginx記憶體池如何實現

#3.2 實際

nginx記憶體池如何實現

nginx記憶體池如何實現

可以看出,很多節點都是從記憶體池中分配的,所以可以把精力都放在實際的資料上而不必在意其他細節上。

    四、實作
  • 4.1 建立記憶體池<pre class="brush:cpp;">/* * NGX_MAX_ALLOC_FROM_POOL should be (ngx_pagesize - 1), i.e. 4095 on x86. * On Windows NT it decreases a number of locked pages in a kernel. */ #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) #define NGX_DEFAULT_POOL_SIZE (16 * 1024)</pre><pre class="brush:cpp;">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-&gt;d.last = (u_char *) p + sizeof(ngx_pool_t); p-&gt;d.end = (u_char *) p + size; p-&gt;d.next = NULL; p-&gt;d.failed = 0; size = size - sizeof(ngx_pool_t); p-&gt;max = (size &lt; NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL; p-&gt;current = p; p-&gt;chain = NULL; p-&gt;large = NULL; p-&gt;cleanup = NULL; p-&gt;log = log; return p; }</pre>從程式碼可以看到,記憶體池最大不超過pagesize的大小

  • #4.2 從記憶體池中分配空間

    分配函數分了記憶體對齊和記憶體不對齊,但這只控制了記憶體池中分配空間,不控制大記憶體分配。
(1)分配小空間

記憶體對齊

ngx_palloc


記憶體不對齊

ngx_pnalloc

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

當需要分配的空間小於max時,將使用小記憶體分配方式(即從記憶體池中分配空間),而ngx_pnalloc和ngx_palloc相比只是呼叫ngx_palloc_small時的最後一個參數為0。

從pool->current指向的記憶體池開始遍歷,尋找滿足分配大小的空間,找到則返回首地址

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

當現有記憶體池中都無法滿足分配條件時,在建立新的記憶體池<pre class="brush:cpp;">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-&gt;d.end - (u_char *) pool); m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool-&gt;log); if (m == NULL) { return NULL; } new = (ngx_pool_t *) m; new-&gt;d.end = m + psize; new-&gt;d.next = NULL; new-&gt;d.failed = 0; m += sizeof(ngx_pool_data_t); m = ngx_align_ptr(m, NGX_ALIGNMENT); new-&gt;d.last = m + size; for (p = pool-&gt;current; p-&gt;d.next; p = p-&gt;d.next) { if (p-&gt;d.failed++ &gt; 4) { pool-&gt;current = p-&gt;d.next; } } p-&gt;d.next = new; return m; }</pre>其中,在建立好新的記憶體池後,又做了一次遍歷,將failed計數加一,當大於4時,將跳過此記憶體池,下次就不從它開始查找。

即認為超過4次你都無法滿足分配,以後都不能滿足分配,不再用你了,減少遍歷個數,加快成功分配效率

(2)分配大空間

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

可以看出,為了避免分配空間,遍歷large鏈查找可重用的節點,但是如果鍊錶過大又可能太慢,所以只查找前三個,如果三個都沒有找到,則直接分配(而且節點也是從記憶體池中分配的,所以後續清理時,不需要管節點,只需要釋放申請的大記憶體本身)
  • 記憶體對齊

    void *
    ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)
    {
        void              *p;
        ngx_pool_large_t  *large;
    
        p = ngx_memalign(alignment, size, pool->log);
        if (p == NULL) {
            return NULL;
        }
    
        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;
    }

    4.3 註冊清理任務
  • 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;
    }
  • 可以看出,這裡只是分配了一個節點,並沒有設置handler以及data數據,所以還得看具體的調用方進行設置,因為這裡返回了分配的節點。

    例如在函數
  • ngx_create_temp_file
  • ngx_int_t
    ngx_create_temp_file(ngx_file_t *file, ngx_path_t *path, ngx_pool_t *pool,
        ngx_uint_t persistent, ngx_uint_t clean, ngx_uint_t access)
    {
        ...
    
        cln = ngx_pool_cleanup_add(pool, sizeof(ngx_pool_cleanup_file_t));
        if (cln == NULL) {
            return NGX_ERROR;
        }
    
           ...
            file->fd = ngx_open_tempfile(file->name.data, persistent, access);
    				...
            if (file->fd != NGX_INVALID_FILE) {
    
                cln->handler = clean ? ngx_pool_delete_file : ngx_pool_cleanup_file;
                clnf = cln->data;
    
                clnf->fd = file->fd;
                clnf->name = file->name.data;
                clnf->log = pool->log;
    
                return NGX_OK;
            }
    			...
    }

    產生臨時文件,將fd以及文件名註冊到清理任務中,後續文件不使用了則不需要特殊處理,記憶體記憶體池釋放時將統一清理。
4.4 重置記憶體池


#釋放大記憶體重置記憶體中last

    #重置failed計數
  • 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;
    }
  • 這裡有個現象:
  • 在記憶體池中空間不足時,將呼叫

    ngx_palloc_block

    建立一個新的記憶體池,而last指向的是
  • m = sizeof(ngx_pool_data_t);
  • , 因此目前新分配的記憶體池將比第一個記憶體池可用大小多了(max,current,chain,large, cleanup,log)這幾個欄位大小(可能沒有那麼多,因為要對齊,可能對齊後就完全一樣了),而現在重置時,

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

    每個記憶體池可用大小又變成一樣的。 ######4.5 銷毀記憶體池############回呼清理任務############釋放大記憶體################### ##釋放記憶體池本身###
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;
        }
    }
}

4.6 大内存释放

通过遍历找到要释放的节点,将内存释放,并且将alloc设置成NULL,则有了节点重用的情况。

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

4.7 分配并清空数据

void *
ngx_pcalloc(ngx_pool_t *pool, size_t size)
{
    void *p;

    p = ngx_palloc(pool, size);
    if (p) {
        ngx_memzero(p, size);
    }

    return p;
}

正常分配的空间中都是垃圾数据,所以当前函数在分配空间后,将分配的空间清零。

4.8 回调文件清理

(1) 手动关闭指定fd

遍历清理任务,找到ngx_pool_cleanup_file的handler,如果是要关闭的fd,则回调

void
ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd)
{
    ngx_pool_cleanup_t       *c;
    ngx_pool_cleanup_file_t  *cf;

    for (c = p->cleanup; c; c = c->next) {
        if (c->handler == ngx_pool_cleanup_file) {

            cf = c->data;

            if (cf->fd == fd) {
                c->handler(cf);
                c->handler = NULL;
                return;
            }
        }
    }
}

(2) 关闭fd

void
ngx_pool_cleanup_file(void *data)
{
    ngx_pool_cleanup_file_t  *c = data;

    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d",
                   c->fd);

    if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
                      ngx_close_file_n " \"%s\" failed", c->name);
    }
}

(3) 删除文件并关闭fd

void
ngx_pool_delete_file(void *data)
{
    ngx_pool_cleanup_file_t  *c = data;

    ngx_err_t  err;

    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d %s",
                   c->fd, c->name);

    if (ngx_delete_file(c->name) == NGX_FILE_ERROR) {
        err = ngx_errno;

        if (err != NGX_ENOENT) {
            ngx_log_error(NGX_LOG_CRIT, c->log, err,
                          ngx_delete_file_n " \"%s\" failed", c->name);
        }
    }

    if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
                      ngx_close_file_n " \"%s\" failed", c->name);
    }
}

以上是nginx記憶體池如何實現的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除