nginx內存池源碼解析

內存池概述

    內存池是在真正使用內存之前,預先申請分配一定數量的、大小相等(一般情況下)的內存塊留作備用。當有新的內存需求時,就從內存池中分出一部分內存塊,若內存塊不夠用時,再繼續申請新的內存。

   內存池的好處有減少向系統申請和釋放內存的時間開銷,解決內存頻繁分配產生的碎片,提示程序性能,減少程序員在編寫代碼中對內存的關註等

   目前一些常見的內存池實現方案有STL中的內存分配區,boost中的object_pool,nginx中的ngx_pool_t,google的開源項目TCMalloc等。

為瞭自身使用的方便,Nginx封裝瞭很多有用的數據結構,比如ngx_str_t ,ngx_array_t, ngx_pool_t 等等,對於內存池,nginx設計的十分精煉,值得我們學習,本文重點給大傢介紹nginx內存池源碼,並用一個實際的代碼例子作瞭進一步的講解。

ererdr

一、nginx數據結構

// SGI STL小塊和大塊內存的分界點:128B
// nginx(給HTTP服務器所有的模塊分配內存)小塊和大塊內存的分界點:4096B
#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1) 

// 內存池默認大小
#define NGX_DEFAULT_POOL_SIZE    (16 * 1024)

// 內存池字節對齊,SGI STL對其是8B
#define NGX_POOL_ALIGNMENT       16
#define NGX_MIN_POOL_SIZE        ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), \
		                         NGX_POOL_ALIGNMENT)

// 將開辟的內存調整到16的整數倍
#define ngx_align(d, a)          (((d) + (a - 1)) & ~(a - 1))
typedef struct ngx_pool_s ngx_pool_t;

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;    // 指向可用於分配空間的內存塊(failed < 4)的起始地址
    ngx_chain_t          *chain;      // 連接所有的內存池塊
    ngx_pool_large_t     *large;	  // 大塊內存的入口指針
    ngx_pool_cleanup_t   *cleanup;    // 內存池塊的清理操作,用戶可設置回調函數,在內存池塊釋放之前執行清理操作
    ngx_log_t            *log;        // 日志
};

在這裡插入圖片描述

二、nginx向OS申請空間ngx_create_pool

// 根據size進行內存開辟
ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){
    ngx_pool_t  *p;
	// 根據系統平臺定義的宏以及用戶執行的size,調用不同平臺的API開辟內存池
    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);              // 能使用的空間 = 總空間 - 頭信息
    // 指定的大小若大於一個頁面就用一個頁面,否則用指定的大小
    // max = min(size, 4096),max指的是除開頭信息以外的內存塊的大小
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    p->current = p;         // 指向可用於分配空間的內存塊的起始地址
    p->chain = NULL;
    p->large = NULL;        // 小塊內存直接在內存塊開辟,大塊內存在large指向的內存開辟
    p->cleanup = NULL;
    p->log = log;

    return p;
}

在這裡插入圖片描述

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

void *
ngx_pnalloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) {
        return ngx_palloc_small(pool, size, 0);  // 不考慮內存對齊
    }
#endif

    return ngx_palloc_large(pool, size);
}

void* ngx_pcalloc(ngx_pool_t *pool, size_t size){
    void *p;
    p = ngx_palloc(pool, size); // 考慮內存對齊
    if (p) {
        ngx_memzero(p, size);   // 可以初始化內存為0
    }

    return p;
}

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;
	// 從第一個內存塊的current指針指向的內存池進行分配
    p = pool->current;

    do {
        m = p->d.last;  // m指向可分配內存的起始地址

        if (align) {
        	// 把m調整為NGX_ALIGNMENT整數倍
            m = ngx_align_ptr(m, NGX_ALIGNMENT);
        }
		// 內存池分配內存的核心代碼
        if ((size_t) (p->d.end - m) >= size) {
        	// 若可分配空間 >= 申請的空間
        	// 偏移d.last指針,記錄空閑空間的首地址
            p->d.last = m + size;
            return m;
        }
        // 當前內存塊的空閑空間不夠分配,若有下一個內存塊則轉向下一個內存塊
        // 若沒有,p會被置空,退出while
        p = p->d.next;
    } while (p);
	
    return ngx_palloc_block(pool, size);
}

當前內存池的塊足夠分配:

在這裡插入圖片描述

當前內存池的塊不夠分配:

  1. 開辟新的內存塊,修改新內存塊頭信息的last、end、next、failed
  2. 前面所有內存塊的failed++
  3. 連接新的內存塊以及前面的內存塊
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);
	
	// 將psize對齊為NGX_POOL_ALIGNMENT的整數倍後,向OS申請空間
    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;		   // 下一塊內存的地址為NULL 
    new->d.failed = 0;		   // 當前內存塊分配空間失敗的次數
    
	// 指向頭信息的尾部,而max,current、chain等隻在第一個內存塊有
    m += sizeof(ngx_pool_data_t);  
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    new->d.last = m + size;                // last指向當前塊空閑空間的起始地址
	
	// 由於每次都是從pool->current開始分配空間
	// 若執行到這裡,除瞭new這個內存塊分配成功,其他的內存塊全部分配失敗
    for (p = pool->current; p->d.next != NULL; p = p->d.next) {
    	// 對所有的內存塊的failed都++,直到該內存塊分配失敗的次數大於4瞭
    	// 就表示該內存塊的剩餘空間很小瞭,不能再分配空間瞭
    	// 就修改current指針,下次從current開始分配空間,再次分配的時候可以不用遍歷前面的內存塊
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;
        }
    }
	
    p->d.next = new;   // 連接可分配空間的首個內存塊 和 新開辟的內存塊

    return m;
}

在這裡插入圖片描述

四、大塊內存的分配與釋放

typedef struct ngx_pool_large_s  ngx_pool_large_t;

struct ngx_pool_large_s {
    ngx_pool_large_t     *next;   // 下一個大塊內存的起始地址
    void                 *alloc;  // 大塊內存的起始地址
};

static void * ngx_palloc_large(ngx_pool_t *pool, size_t size){
    void              *p;
    ngx_uint_t         n;
    ngx_pool_large_t  *large;
	
	// 調用的就是malloc
    p = ngx_alloc(size, pool->log);
    if (p == NULL) {
        return NULL;
    }

    n = 0;
	// for循環遍歷存儲大塊內存信息的鏈表
    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
        	// 當大塊內存被ngx_pfree時,alloc為NULL
        	// 遍歷鏈表,若大塊內存的首地址為空,則把當前malloc的內存地址寫入alloc
            large->alloc = p;
            return p;
        }
		// 遍歷4次後,若還沒有找到被釋放過的大塊內存對應的信息
		// 為瞭提高效率,直接在小塊內存中申請空間保存大塊內存的信息
        if (n++ > 3) {
            break;
        }
    }
	// 通過指針偏移在小塊內存池上分配存放大塊內存*next和*alloc的空間
    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
    	// 如果在小塊內存上分配存儲*next和*alloc空間失敗,則無法記錄大塊內存
    	// 釋放大塊內存p
        ngx_free(p);
        return NULL;
    }
	
    large->alloc = p;			   // alloc指向大塊內存的首地址
    large->next = pool->large;	   // 這兩句采用頭插法,將新內存塊的記錄信息存放於以large為頭結點的鏈表中
    pool->large = large;

    return p;
}

在這裡插入圖片描述

大塊內存的釋放

// 釋放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) {
    	// 遍歷存儲大塊內存信息的鏈表,找到p對應的大塊內存
        if (p == l->alloc) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "free: %p", l->alloc);
            // 釋放大塊內存,但不釋放存儲信息的內存空間
            ngx_free(l->alloc);  // free
            l->alloc = NULL;     // alloc置空

            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}

五、關於小塊內存不釋放

就用瞭last和end兩個指著標識空閑的空間,是無法將已經使用的空間合理歸還到內存池的,隻是會重置內存池。同時還存儲瞭指向大內存塊large和清理函數cleanup的頭信息

考慮到nginx的效率,小塊內存分配高效,同時也不回收內存

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);
        }
    }
	
	// 遍歷小塊內存的鏈表,重置last、failed、current、chain、large等管理信息
    for (p = pool; p; p = p->d.next) {
    	// 由於隻有第一個內存塊有除瞭ngx_pool_data_t以外的管理信息,別的內存塊隻有ngx_pool_data_t的信息
    	// 不會出錯,但是會浪費空間
        p->d.last = (u_char *) p + sizeof(ngx_pool_t);
        p->d.failed = 0;
    }
	
	// current指向可用於分配內存的內存塊
    pool->current = pool;
    pool->chain = NULL;
    pool->large = NULL;
}

nginx本質是http服務器,通常處理的是短鏈接,間接性提供服務,需要的內存不大,所以不回收內存,重置即可。

客戶端發起一個requests請求後,nginx服務器收到請求會返回response響應,若在keep-alive時間內沒有收到客戶的再次請求,nginx服務器會主動斷開連接,此時會reset內存池。下一次客戶端請求再到來時,可以復用內存池。

如果是處理長鏈接,隻要客戶端還在線,服務器的資源就無法釋放,直到系統資源耗盡。長鏈接一般使用SGI STL內存池的方式進行內存的開辟和釋放,而這種方式分配和回收空間的效率就比nginx低

六、銷毀和清空內存池

假設如下情況:

// 假設內存對齊為4B
typedef struct{
	char* p;
	char data[508];
}stData;

ngx_pool_t *pool = ngx_create_pool(512, log);  // 創建一個總空間為512B的nginx內存塊
stData* data_ptr = ngx_alloc(512);            // 因為可用的實際內存大小為:512-sizeof(ngx_pool_t),所以屬於大內存開辟
data_ptr->p = malloc(10);                   // p指向外界堆內存,類似於C++對象中對用占用瞭外部資源

當回收大塊內存的時候,調用ngx_free,就會導致內存泄漏

在這裡插入圖片描述

以上內存泄漏的問題,可以通過回調函數進行內存釋放(通過函數指針實現)

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

typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;

// 以下結構體由ngx_pool_s.cleanup指向,也是存放在內存池的小塊內存
struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt   handler;
    void                 *data;     // 指向需要釋放的資源
    ngx_pool_cleanup_t   *next;     // 釋放資源的函數都放在一個鏈表,用next指向這個鏈表
};

nginx提供的函數接口:

// p表示內存池的入口地址,size表示p->cleanup->data指針的大小
// p->cleanup指向含有清理函數信息的結構體
// ngx_pool_cleanup_add返回 含有清理函數信息的結構體 的指針
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申請size的空間
        c->data = ngx_palloc(p, size);
        if (c->data == NULL) {
            return NULL;
        }
    } else {
        c->data = NULL;
    }

    c->handler = NULL;
    // 采用頭插法,將當前結構體串在pool->cleanup後
    c->next = p->cleanup;
    p->cleanup = c;

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

    return c;
}

使用方式:

void release(void* p){
	free(p);
}

ngx_pool_cleanup_t* clean_ptr = ngx_clean_cleanup_add(pool, sizeof(char*));
clean_ptr->handler = &release;   // 用戶設置銷毀內存池前需要調用的函數
clean_ptr->data = data_ptr->p;   // 用戶設置銷毀內存池前需要釋放的內存的地址

ngx_destroy_pool(pool);          // 用戶銷毀內存池

七、編譯測試內存池接口功能

void ngx_destroy_pool(ngx_pool_t *pool)
{
    ngx_pool_t          *p, *n;
    ngx_pool_large_t    *l;
    ngx_pool_cleanup_t  *c;
	
	// 遍歷cleanup鏈表(存放的時釋放前需要調用的函數),可釋放外部占用的資源
    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;
        }
    }
}

在這裡插入圖片描述

執行configure生成Makefile文件(若報錯則表示需要apt安裝軟件)

在這裡插入圖片描述

Makefile如下:

在這裡插入圖片描述

執行make命令使用Makefile編譯源碼,在相應目錄下生成 .o文件

在這裡插入圖片描述

#include <ngx_config.h>
#include <nginx.h>
#include <ngx_core.h>
#include <ngx_palloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err,
            const char *fmt, ...){

}

typedef struct Data stData;
struct Data{
    char *ptr;
    FILE *pfile;
};

void func1(char *p){
    printf("free ptr mem!\n");
    free(p);
}

void func2(FILE *pf){
    printf("close file!\n");
    fclose(pf);
}

void main(){
	// max = 512 - sizeof(ngx_pool_t)
	// 創建總空間為512字節的nginx內存塊
    ngx_pool_t *pool = ngx_create_pool(512, NULL);
    if(pool == NULL){
        printf("ngx_create_pool fail...");
        return;
    }
    
	// 從小塊內存池分配的
    void *p1 = ngx_palloc(pool, 128); 
    if(p1 == NULL){
        printf("ngx_palloc 128 bytes fail...");
        return;
    }
	
	// 從大塊內存池分配的
    stData *p2 = ngx_palloc(pool, 512); 
    if(p2 == NULL){
        printf("ngx_palloc 512 bytes fail...");
        return;
    }
    
    // 占用外部堆內存
    p2->ptr = malloc(12);
    strcpy(p2->ptr, "hello world");
    // 文件描述符
    p2->pfile = fopen("data.txt", "w");
    
    ngx_pool_cleanup_t *c1 = ngx_pool_cleanup_add(pool, sizeof(char*));
    c1->handler = func1;   // 設置回調函數
    c1->data = p2->ptr;    // 設置資源地址

    ngx_pool_cleanup_t *c2 = ngx_pool_cleanup_add(pool, sizeof(FILE*));
    c2->handler = func2;
    c2->data = p2->pfile;
	
	// 1.調用所有的預置的清理函數 2.釋放大塊內存 3.釋放小塊內存池所有內存
    ngx_destroy_pool(pool); 

    return;
}

在這裡插入圖片描述

由於ngx_pool_cleanup_add中用頭插法將創建的清理塊鏈入pool->cleanup,所以ngx_destroy_pool的時候先清理文件後清理堆內存。

相關測試代碼推送到:https://github.com/BugMaker-shen/nginx_sgistl_pool

到此這篇關於nginx內存池源碼解析的文章就介紹到這瞭,更多相關nginx內存池內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: