C++:06.Nginx內存池

 先了解一下Nginx:

Nginx是一個高性能的HTTP反向代理服務器,接收瀏覽器請求。其特點是佔有內存少,併發能力強,穩定性高。

nginx有什麼功能?

接收http服務,正向反向代理(負載均衡)。

正向代理代理客戶端,反向代理代理服務器,反向代理也稱作負載均衡器

http協議本身就是一個短鏈接,無狀態的協議

內存池的好處:

減少向系統申請和釋放內存的時間開銷,解決內存頻繁分配產生的碎片,提示程序性能。

nginx有小塊內存和大塊內存的概念,小塊內存nginx在分配的時候會嘗試在當前的內存池節點中分配,而大塊內存會調用系統函數malloc向操作系統申請

區分小塊內存和大塊內存的原因有2個:

1、針對大塊內存  如果它的生命週期遠遠短於所屬的內存池,那麼提供一個單獨的釋放函數是十分有意義的,但不區分大塊內存和小塊內存,針對大的內存塊便會無法提前釋放了

2、大塊內存與小塊內存的界限是一頁內存4K(下文有體到),大於一頁的內存在物理上不一定是連續的,所以如果分配的內存大於一頁的話,從內存池中使用,和向操作系統重新申請效率差不多是等價的。

參考博文:https://www.cnblogs.com/magicsoar/p/6040238.html


主要數據結構:

ngx_pool_data_t(內存池數據塊結構)

typedef struct   包含了所需要操作這個內存池數據的一些指針。
{
    u_char               *last;    unsigned char類型指針,保存當前數據區的已經使用的數據的結尾。
    u_char               *end;     表示當前的內存池的結尾。end-last就是內存池未使用的大小。
    ngx_pool_t           *next;    指向下一塊內存池。
    ngx_uint_t            failed;  標記請求小塊內存由於內存池空間不夠,而需要重新分配一個小內存池的次數。
} ngx_pool_data_t;

ngx_pool_t(內存池頭部結構)

struct ngx_pool_t
{
    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_large_s(大塊內存池類型)

struct ngx_pool_large_s 
{
    ngx_pool_large_t *next;  指向下一大塊內存
    void *alloc;             本塊內存
};

ngx_pool_cleanup_s(內存池中的數據清理)

struct ngx_pool_cleanup_s 
{
    ngx_pool_cleanup_pt  handler;   清理函數
    void                *data;      傳給清理函數的數據
    ngx_pool_cleanup_t  *next;      指向下一個清理函數  調用destroy時會遍歷清理函數鏈表,調用handler
}

 主要函數:

1、ngx_create_pool()  內存池創建

宏定義的函數,就是對malloc進行簡單封裝。裏面寫入了錯誤日誌
#define ngx_memalign(alignment,size,log) ngx_alloc(size,log) 
內存池的數據區的最大容量。在x86上ngx_pagesize=4096。
因爲尋址頁面大小爲4096,是一個相對合適的數字,保證size不會太大,導致尋址不到浪費空間
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)

ngx_pool_t *  函數返回值類型
ngx_create_pool(size_t size, ngx_log_t *log) 傳進來的size並不是真正能使用的大小,應減去sizeof(ngx_poll_t)。
{
    ngx_pool_t *p;
   
    p = ngx_memalign(NGX_POOL_ALIGNMENT,size,log)  用宏定義了一個函數,第一個參數宏定義的16,用來內存對齊
    if (p == NULL) 
    {
        return NULL;
    }

    內存已經申請了,對結構體指針初始化。
    由於一開始數據區爲空,因此last指向數據區的開始。
    p->d.last = (u_char *) p + sizeof(ngx_pool_t);  可以證明上述實際使用空間並沒有size
    end也就是數據區的結束位置
    p->d.end = (u_char *) p + size;
    p->d.next = NULL;
    p->d.failed = 0;
    
    這裏纔是我們真正能使用的大小。
    size = size - sizeof(ngx_pool_t);
    
    然後設置max。內存池的最大值也就是size和最大容量之間的最小值。
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
    
    current表示當前的內存池。
    p->current = p;
    
    其他的域置NULL。
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;
    返回指針。
    return p;
}

爲什麼要考慮內存對齊?

因爲內存對齊可以降低CPU讀取內存的次數,提高性能。主要爲減少內存的I/O操作。 


2、內存申請:

ngx_palloc()    分配的內存會對齊

ngx_calloc()    用來分配一塊清0的內存

ngx_pnalloc()  分配的內存不會對齊

就對齊的計算,不必深究
#define ngx_align_ptr(p,a) (u_char *)(((uintptr_t)(p) + ((uintptr_t)a - 1)) &~((uintptr_t)a - 1))
#define NGX_ALIGNMENT sizeof(unsigned long)

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
    u_char *m;
    ngx_pool_t *p;
    
    //判斷當前申請的大小是否超過max,如果超過則說明是申請大塊內存,此時進入large
    if (size <= pool->max)  如果申請小塊內存,進入
    {
        得到當前的內存池指針。
        p = pool->current;
        開始遍歷內存池
        do {
            首先對齊last指針。調用宏對內存地址進行對齊處理。
            如果是ngx_pnalloc(),就省去這一步。
            m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);  

            得到當前內存池中的可用大小。如果剩下的空間不夠,則直接返回當前的last,也就是數據的指針。
            if ((size_t) (p->d.end - m) >= size)  如果當前夠存,進入
            {
                偏移last,然後返回前面保存的last。
                p->d.last = m + size;
                return m;
            }

            否則繼續遍歷
            p = p->d.next;
        } while (p);

        到達這裏說明內存池已經滿掉,因此我們需要重新分配一個小塊內存然後鏈接到當前的data的next上。    
        return ngx_palloc_block(pool, size);
    }
    申請大塊。
    return ngx_palloc_large(pool, size);
}

ngx_palloc_block()  申請小塊內存

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

    計算當前的內存池的大小。
    psize = (size_t) (pool->d.end - (u_char *) pool);

    再申請一個與pool同樣大小的內存
    m = ngx_memalign(NGX_POOL_ALIGNMENT,psize,pool->log)
    if (m == NULL) 
    {
        return NULL;
    }

    new = (ngx_pool_t *) m;
    接下來和我們create一個內存池做的操作一樣。就是更新一些指針
    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;

    讓m指向該塊內存ngx_pool_data_t結構體之後數據區的起始位置
    m += sizeof(ngx_pool_data_t);
    m = ngx_align_ptr(m, NGX_ALIGNMENT);  對齊指針,前面說了
    new->d.last = m + size;  設置last指針位置
   
    設置current。
    current = pool->current;
    
    這裏遍歷所有的子內存池,這裏主要是通過failed來標記重新分配子內存池的次數,然後找出最後一個大於4的,標記它
    for (p = current; p->d.next; p = p->d.next) 
    {
        if (p->d.failed++ > 4) 
        {
            當failed大於4說明我們至少請求了4次內存分配,都不能滿足我們的請求,
            此時我們就假設老的內存都已經沒有空間了,因此我們就從比較新的內存塊開始。
            current = p->d.next;  
        }
    }

    鏈接到最後一個內存池後面
    p->d.next = new;

    如果current爲空,則current就爲new。
    pool->current = current ? current : new;
    return m;
}

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_memalign(NGX_POOL_ALIGNMENT,size,pool->log)  上面提了
    if (p == NULL) 
    {
        return NULL;
    }
    n = 0;

    開始遍歷large鏈表,如果有alloc(也就是內存區指針)爲空,則直接指針賦值然後返回。一般第一次請求大塊內存都會
    for (large = pool->large; large; large = large->next) 
    {
        if (large->alloc == NULL) 
        {
            large->alloc = p;
            return p;
        }
        鏈表不能超過4個,只對前3個內存塊進行檢查,否則就直接分配內存。
        if (n++ > 3) 
        {
            break;
        }
    }

    malloc一大內存塊。
    large = ngx_palloc(pool, sizeof(ngx_pool_large_t));  調用本身
    if (large == NULL) 
    {
        ngx_free(p);  從pool的large鏈表中找到p,然後free掉。
        return NULL;
    }

    然後鏈接數據區指針p到large。
    large->alloc = p;
    large->next = pool->large;
    pool->large = large;
    return p;
}

3、ngx_destroy_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);  說結構體的時候就提到了這個函數
        }
    }

    free大塊內存   ngx_palloc_large申請的內容
    for (l = pool->large; l; l = l->next) 
    {
        ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
        if (l->alloc) 
        {
            ngx_free(l->alloc);
        }
    }
    
    遍歷小塊內存池。  ngx_create_pool申請的內存
    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) 
    {
        直接free掉。
        ngx_free(p);
        if (n == NULL) 
        {
            break;
        }
    }
}

補充:

1、在nginx內存池中只有大塊內存提供了free接口【ngx_pfree(ngx_pool_t *pool, void *p),這個函數就是從pool的large鏈表中找到p,然後free掉它】,可以回收釋放。

小塊數據是不釋放的,只有當整個內存池被destroy掉纔會被釋放,這樣就簡化了內存池的操作。爲什麼不釋放?原因:http服務器是間歇性處理,是一個短連接,無狀態的協議。處理完請求,服務器斷開。在處理完http請求完成之後,就可以發起內存池的重置函數。

2、ngx_pool_cleanup_t *  ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
這個函數也就是添加一個ngx_pool_cleanup_t到當前的pool上,然後返回,我們此時就能通過返回的結構來給對應的handler賦值。當內存池destroy的時候我們就能添加這些清理函數到pool中,然後當內存池釋放的時候就會自動調用。


4、ngx_pfree()  內存池清理

ngx_int _t
ngx_pfree(ngx_pool_t *pool,void *p)
{
    ngx_pool_large_s *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);              
            free(l->alloc );
            l->alloc =NULL;
            return NGX_OK;
        }
    }
    return NGX_DECLINED;
}

注:因爲大塊內存只對前三個進行檢查,否則就直接分配,所以大塊內存必須及時釋放。


 5、ngx_reset_pool()  內存池重置

void
ngx_reset_pool(ngx_pool_t *pool)
{
    ngx_pool_large_s *l;
    ngx_pool_s *p;
    
    釋放所有大塊內存
    for(l=pool->large ;l;l=l->next )
    {
        if(l->alloc )
        {
            free(l->alloc );
            l->alloc =NULL;
        }
    }
    pool->large = NULL;

    重置所有小塊內存
    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;
}

在處理完一批http請求完成之後就可以發起內存池的重置函數

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