Nginx內存管理詳解


Nginx內存管理詳解

目錄:

1.Nginx內存管理介紹

2.Nginx內存池的邏輯結構

3.Nginx內存池的基本數據結構

4.內存池基本操作介紹

5.內存池管理源碼詳解

6.內存池使用源碼詳解

7.小結

 

 

 

1.Nginx內存管理介紹

  在C/C++語言程序設計中,通常由程序員自己管理內存的分配和釋放,其方式通常是malloc(free)和new(delete)等API。這樣做的缺點在於:由於所申請內存塊的大小不定,當頻繁使用時會造成大量的內存碎片從降低性能。通常我們所使用的解決辦法就是內存池

  什么是內存池呢?內存池就是在真正使用內存之前,先申請分配一定數量的、大小相等(一般情況下)的內存塊留作備用。當有新的內存需求時,就從內存池中分出一部分內存塊,若內存塊不夠再繼續申請新的內存。而不是每次需要了就調用分配內存的系統API(如malloc)進行申請,每次不需要了就調用系統釋放內存的API(如free)進行釋放。這樣做的一個顯著優點是,使得內存分配效率得到提升。因此使用內存池的方式對程序所使用的內存進行統一的分配和回收,是當前最流行且高效的內存管理方法,能夠在很大程度上降低內存管理的難度,減少程序的缺陷,提高整個程序的穩定性。

  通過減少頻繁的內存申請和釋放可以提升效率很容易理解,那么內存池究竟是怎么提高程序的穩定性的呢?我們知道在C/C++語言中,並沒有提供直接可用的垃圾回收機制,因此在程序編寫中, 一個特別容易發生的錯誤就是內存泄露,對於運行時間短,內存需求小的程序來說,泄露一點內存除了影響程序運行效率之外可能並不會造成多大的問題。但是類似於Ngnix這樣需要長期運行的web服務器程序來說,內存泄露是一件非常嚴重的災難,這會使得程序由於內存耗盡而崩潰,重啟之前不再能夠提供相應的web服務。還有一種情況就是當內存分配與釋放的邏輯在程序中相隔較遠時,很容易發生內存被釋放兩次乃至多次的情況。使用內存池使得我們在開發程序時,只用關心內存的分配,而釋放就交給內存池來完成。

  那么內存池在Nginx中究竟是怎么使用的呢?通常我們對於每個請求或者連接都會建立相應的內存池,建立好內存池之后,我們可以直接從內存池中申請所需要的內存,而不用去管內存的釋放,唯一需要注意的就是當內存池使用完成之后需要記得銷毀內存池。此時,內存池會調用相應的數據清理函數(如果有的話),之后會釋放在內存池中管理的內存。

  大家可能會問,既然申請的內存在內存池銷毀的時候才會被釋放,這不會存在內存的浪費么?畢竟使用完了不再需要的內存為什么不立即釋放而非要等到銷毀內存池時才釋放呢?確實存在這個問題,不過大家不用擔心。在Nginx中,對於大塊內存可以使用ngx_pfree()函數提前釋放。並且由於Nginx是一個純粹的web服務器,而web服務器通常使用的協議是Http協議,並且在傳輸層使用的是Tcp協議,我們知道每一個tcp連接都是由生命周期的,因此基於tcp的http請求都會有一個很短暫的生命周期。對於這種擁有很短暫生命周期的請求,我們所建立的內存池的生命周期也相應會很短暫,因此其所占用的內存資源很快就可以得到釋放,不會出現太多的資源浪費的問題。畢竟工程就是一種折中嘛,我們需要在內存資源浪費和減低程序內存管理難度、提升效率之間選擇一個合適的權衡。

  說了這么多,現在就讓我們開始研究和學習Nginx內存管理的機制和源碼吧。注:本文的講解都是基於nginx-1.10.3版本。

 

 

 

2.Nginx內存池的邏輯結構

  前面提到Nginx內存管理機制其實就是內存池,其底層實現就是一個鏈表結構。我們需要對內存池進行管理和分配,依賴的就是ngx_pool_t結構體,可以認為該結構就是內存池的分配管理模塊。那么內存池的邏輯結構究竟是什么樣呢?其實就是一個ngx_pool_t結構體,在這個結構體中包含了三個部分:小塊內存形成的單鏈表,大塊內存形成的單鏈表和數據清理函數形成的單鏈表。先給出一張整個內存池內部實現的結構圖,方便大家理解。具體如圖2.1所示:

 

 

圖2.1 Nginx內存池示意圖

 

   圖2.1完整的展示了ngx_pool_t內存池中小塊內存、大塊內存和資源清理函數鏈表間的關系。圖中,內存池預先分配的剩余空閑內存不足以滿足用戶申請的內存需求,導致又分配了兩個小內存池。其中原內存池的failed成員已經大於4,所以current指向了第2塊小塊內存池,這樣當用戶再次從小塊內存池中請求分配內存空間時,將會直接忽略第1塊小內存池,從第2塊小塊內存池開始遍歷。從這里可以看到,我們使用的內存池確實存在當failed成員大於4之后不能利用其空閑內存的資源浪費現象(由於current指針后移)。值得注意的是:我們的第2、3塊小塊內存池中只包含了ngx_pool_t結構體和數據區,並不包含max、current、...、log。這是由於后續第1塊小內存池已經包含了這些信息,后續的小塊內存池不必在浪費空間存儲這些信息。我們在第6小節:內存池的使用中將會有所介紹。圖中共分配了3個大塊內存,其中第二塊的alloc為NULL(提前調用了ngx_pfree())。圖中還掛在了兩個資源清理方法。提醒一下的是:如果在這里沒有弄清楚,沒有關系,看完了后面的部分再回過頭來理解這個示意圖就能夠很好的理解了。這里只是先給出一個概括性的Nginx內存池邏輯結構的介紹,先給大家留下一個大概的印象。

 

 

 

3.Nginx內存池的基本數據結構

本部分主要介紹內存池中重要的數據結構,主要是ngx_pool_t,然后介紹ngx_pool_t中三個重要數據結構:ngx_pool_data_t,ngx_pool_large_t和ngx_pool_cleanup_t。

 

(1)ngx_pool_t

  我們可以在Nginx的源碼的src/core/目錄下的nax_palloc.h頭文件中看到:

 

1 struct ngx_pool_s { 2  ngx_pool_data_t d; 3  size_t max; 4     ngx_pool_t           *current; 5     ngx_chain_t          *chain; 6     ngx_pool_large_t     *large; 7     ngx_pool_cleanup_t   *cleanup; 8     ngx_log_t            *log; 9 };

 

   並且在src/core/ngx_core.h中:

typedef struct ngx_pool_s        ngx_pool_t;

 

下面將具體講解ngx_pool_t結構體中每個成員的含義和用途:

 d:ngx_pool_data_t結構體,描述內存池中的小塊內存。當小塊內存不足時,會再分配一個ngx_pool_t(里面含有一個新分配且未使用的小塊內存空間和用於管理這塊內存空間的ngx_pool_data_t結構體)。這些小塊內存塊之間通過d中的next成員鏈接形成的單鏈表。掛在d成員上。

 

max:評估申請內存屬於小塊還是大塊的標准,在x86上默認是4095字節。

 

current:多個小塊內存構成單鏈表時,指向分配內存時遍歷的第一個小塊內存。

 

chain:與內存池關系不大,略過。

 

large:ngx_pool_large_t結構體當用戶申請的內存空間大於max時,就會分配大塊內存。而多個大塊內存之間是通過ngx_pool_large_t中的next成員鏈接形成的單鏈表。掛在large成員上。

 

cleanup:ngx_pool_cleanup_t結構體,所有待清理的資源(例如需要關閉或者刪除的文件)以ngx_pool_cleanup_t對象中的next成員鏈接形成單鏈表。掛在cleanup成員上。

 

log:內存池中執行時輸出日志的地方。

 

 

(a).ngx_pool_data_t

  我們可以在Nginx的源碼的src/core/目錄下的nax_palloc.h頭文件中看到:

 

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

 

下面將具體講解ngx_pool_data_t結構體中每個成員的含義和用途: 

last:指向小塊內存中未分配的空閑內存的首地址。

 

end:指向當前小塊內存的尾部。

 

next:同屬於一個內存池的多個小塊內存之間,通過next成員鏈接形成單鏈表。

 

failed: 每當當前的小塊內存由於空閑部分較少而不能滿足用戶提出的內存申請請求時,failed成員就會加1。當failed成員大於4后,ngx_pool_t的current成員就會移向下一個小塊內存,在以后分配內存時,將從下一個小塊內存開始遍歷。

 

 

(b).ngx_pool_large_t

  我們可以在Nginx的源碼的src/core/nax_palloc.h頭文件中看到:

 

typedef struct ngx_pool_large_s  ngx_pool_large_t;

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

 

下面將具體講解ngx_pool_large_t結構體中每個成員的含義和用途:

next:所有大塊內存通過next指針鏈接在一起形成單鏈表。

 

alloc:指向分配的大塊內存,后面我們將會看到大塊內存底層是通過ngx_alloc分配,ngx_free釋放。釋放完了之后賦值為NULL。

 

 

(c).ngx_pool_cleanup_t

   我們可以在Nginx的源碼的src/core/nax_palloc.h頭文件中看到:

 

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

 

 下面將具體講解ngx_pool_cleanup_t結構體中每個成員的含義和用途:

handler:初始化為NULL,需要設置的清理函數。

 

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

 

根據上面的聲明,可以看出,ngx_pool_clean_pt是一個函數指針,有一個通用型的參數data,返回類型為void。后面我們會看到當銷毀內存池的時候,底層會遍歷掛在cleanup成員上的單鏈表上的各個節點,調用各節點的數據清理函數完成相應的清理操作。這是通過回調函數實現的。

 

data:用於向數據清理函數傳遞的參數,指向待清理的數據的地址,若沒有則為NULL。我們可以通過ngx_pool_cleanup_add函數添加數據清理函數,當其中的參數size>0時,data不為NULL。

 

next:用於鏈接所有的數據清理函數形成單鏈表。由ngx_pool_cleanup_add函數設置next成員,用於將當前ngx_pool_cleanup_t(由ngx_pool_cleanup_add函數返回)添加到cleanup鏈表中。

 

 

 

4.內存池基本操作介紹

  這一部分主要簡單講解與內存池管理有關的基本操作(共15個)。主要包括四個部分:(a).內存池操作 (b).基於內存池的分配、釋放操作 (3).隨着內存池釋放同步釋放資源的操作 (4).與內存池無關的分配、釋放操作。在第5和第6節中,我們會對部分常用內存池的操作進行代碼上的詳細介紹。

 

(a).內存池操作:

 

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);
void ngx_destroy_pool(ngx_pool_t *pool);
void ngx_reset_pool(ngx_pool_t *pool);

 

  ngx_create_pool

  創建內存池,其參數size為整個內存的大小,包括結構管理(ngx_pool_t)和后續可分配的空閑內存。這意味着,size必須大於等於sizeof(ngx_pool_t),通常在32位的系統是是40字節,后面我們介紹源碼時會詳細的介紹。通常size的默認大小為NGX_DEFAULT_POOL_SIZE(#define NGX_DEFAULT_POOL_SIZE    (16 * 1024)),可以看到為16k。不用擔心其不夠用,因為當不夠用時,Nginx會對內存池進行內存空間的擴展,也就是申請一個新的內存池(鏈表)節點(程序中成為一個block),然后掛在內存池的最后面。

 

  ngx_destory_pool

  銷毀內存池,它會執行通過ngx_pool_cleanup_add函數添加的各種資源清理方法,然后釋放大塊內存,最后把整個pool分配的內存釋放掉。

 

  ngx_reset_pool

  重置內存池,即將在內存池中原有的內存釋放后繼續使用。后面我們會看到,這個方法是把大塊的內存釋放給操作系統,而小塊的內存則在不釋放的情況下復用。

 

 

(b).基於內存池的分配、釋放操作

 

void *ngx_palloc(ngx_pool_t *pool, size_t size);
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

  

      ngx_palloc

  分配地址對齊的內存。內存對齊可以減少cpu讀取內存的次數,代價是存在一些內存浪費。

 

  ngx_pnalloc

  同ngx_palloc,區別是分配內存時不考慮對齊。

 

  ngx_pcalloc

  同ngx_palloc,區別是分配完對齊的內存后,再調用memset全部初始化為0。

 

  ngx_pmemalign

  按參數alignment進行地址對齊來分配內存。注意,這樣分配的內存不管申請的size有多小,都不會使用小塊內存,它們直接從進程的堆中分配,並掛在大塊內存組成的large單鏈表中。

 

  ngx_pfree

  提前釋放大塊內存。由於其實現是遍歷large單鏈表,尋找ngx_pool_large_t對應的alloc成員后調用ngx_free(alloc),實際上是直接調用free(alloc),釋放內存給操作系統,將ngx_pool_large_t移出鏈表並刪除。效率不高。

 

 

(c).隨着內存池釋放同步釋放資源的操作

 

ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);
void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd);
void ngx_pool_cleanup_file(void *data);
void ngx_pool_delete_file(void *data);

    

    ngx_pool_cleanup_add

  添加一個需要在內存釋放時同步釋放的資源。該方法會返回一個ngx_pool_cleanup_t結構體,而我們得到該結構體后需要設置ngx_pool_cleanup_t的handler成員為釋放資源時執行的方法。ngx_pool_clean_add的參數size,當它不為0時,會分配size大小的內存,並將ngx_pool_cleanup_t的data成員指向該內存,這樣可以利用這段內存傳遞參數,供資源清理函數使用。當size為0時,data將為NULL。

 

  ngx_pool_run_cleanup_file

  在內存釋放前,如果需要提前關閉文件(調用ngx_pool_cleanup_add添加的文件,同時ngx_pool_cleanup_t的handler成員被設置為ngx_pool_cleanup_file),則調用該方法。

 

  ngx_pool_cleanup_file

  以關閉文件來釋放資源的方法,可以設置到ngx_pool_cleanup_t的handler成員。

 

  ngx_pool_delete_file 

  以刪除文件來釋放資源的方法,可以設置到ngx_pool_cleanup_t的handler成員。

 

 

(d).與內存池無關的分配、釋放操作

 

void *ngx_alloc(size_t size, ngx_log_t *log);
void *ngx_calloc(size_t size, ngx_log_t *log);

#define ngx_free          free

  

  這部分的聲明和定義實際上並不在src/core/ngx_palloc.h中,而是在/src/os/unix/ngx_alloc.h中。

 

  ngx_alloc

  從操作系統中分配內存,通過調用malloc實現。

 

  ngx_calloc

  從操作系統中分配內存並全部初始化為0,通過調用malloc和memset實現。

 

  ngx_free

  從上面的宏定義可以看到,其就是free函數,釋放內存到操作系統。

 

 

 

5.內存池管理源碼詳解

   本部分的源碼可以在src/core/ngx_palloc.h、src/core/ngx_palloc.c、src/os/unix/ngx_alloc.h和src/os/unix/ngx_alloc.c中找到。內存池的管理主要包括內存池的創建、銷毀以及重置操作。我們通過對源碼的分析來研究和學習Nginx的內存管理技術。

 

(a).內存池的創建

  創建內存池的操作主要由ngx_create_pool()函數完成,代碼如下:

 

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

 

   

  在這段代碼中,首先通過ngx_memalign()函數申請對齊的內存,其大小為size個字節。如果內存申請失敗,則返回NULL,否則對ngx_pool_t結構體中的成員進行初始化。在進行初始化之前,讓我們先討論以下什么是小塊內存?

 

 

/*
 * 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)

 

 

   這是ngx_palloc.h中的一個注釋及宏定義,從中我們可以看到在x86系統4095字節是一個標准。因為ngx_pagesize中存放的是當前Nginx服務器運行的系統中一頁內存頁的大小,而在x86的系統上就是4KB。由於存在減1的關系,這意味着在x86系統上,小於等於4095字節的內存被稱為小塊內存,而大於4095字節的內存被稱為大塊內存。當然這並不是絕對的,在上述源碼中,我們看到如果傳遞的參數size滿足:size - sizeof(ngx_pool_t) < NGX_MAX_ALLOC_FROM_POOL時,其max的值為size(小於NGX_MAX_ALLOC_FROM_POOL),而當size不滿足上述不等式時,其值為NGX_MAX_ALLOC_FROM_POOL。也就是說NGX_MAX_ALLOC_FROM_POOL是一個最大的門限,申請的小塊內存的大小應該不超過其大小。在初始化max之后,我們將last指向分配好的空閑內存空間的首地址,end指向內存池的尾部。並將next初始化為NULL,failed的值初始化為0。然后再將current指向這塊內存池的首地址,large和cleanup也被初始化為NULL,最后返回指向分配好的內存空間的首地址。為了更加清晰地展示內存池的創建過程,下面將會舉一個例子來說明。但是在這之前,我們先來分析以下ngx_memalign()函數的實現源碼。

 

  關於ngx_memalign()的細節我們可以在src/os/unix/ngx_alloc.c中看到其源碼,前面部分是聲明,后面是定義。如下所示:

 

/*
 * Linux has memalign() or posix_memalign()
 * Solaris has memalign()
 * FreeBSD 7.0 has posix_memalign(), besides, early version's malloc()
 * aligns allocations bigger than page size at the page boundary
 */

#if (NGX_HAVE_POSIX_MEMALIGN || NGX_HAVE_MEMALIGN)

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

#else

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

#endif

 

 

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

#endif

 

  我們還需要知道的就是在linux系統下,分配內存有三個系統調用,如果不考慮內存對齊,則有malloc();如果考慮內存對齊,則有:memalign()和posix_memalign();從ngx_memalign()的具體聲明和實現中,我們可以看出這其實一個條件編譯。如果系統定義了NGX_HAVE_POSIX_MEMALIGN,則調用posix_memalign()申請對齊的內存;如果系統定義了NGX_HAVE_MEMALIGN,則調用memalign()申請對齊的內存;並且這兩種內存對齊默認都是基於16字節的。否則直接調用ngx_alloc(),而ngx_alloc()直接調用malloc()申請不對齊的內存。講完了內存池中三種申請內存的方式之后,我們可以開始講解創建內存池的實例了。

  比如說我們需要創建一個大小為1024字節的內存池作為一個分配模塊:

 

ngx_pool_t *pool = ngx_create_pool (1024,  log);

 

  為了方便,我們不妨假設申請的這塊內存的起始地址為10。執行完創建內存池的操作后,內存中的分布情況如圖5.1所示:

 

圖5.1 創建內存池內存片段圖

 

  從執行結果可以看出:創建的內存池總共占用了1024個字節,起始地址為10,結束地址為1034。指向內存池的指針為pool。last指針為50(10+40),因為起始地址是10,而ngx_pool_t結構體所占用的內存空間為40字節,怎么計算得到的呢?其實很簡單,只需要考慮結構體在內存中的對齊問題即可。在x86中(x64中指針在內存中占用8字節而不是4字節)如下所示:

 

 

typedef struct {
    u_char               *last;//4字節
    u_char               *end;//4字節
    ngx_pool_t           *next;//4字節
    ngx_uint_t            failed;//4字節
} ngx_pool_data_t;


struct ngx_pool_s {
    ngx_pool_data_t       d;//16字節
    size_t                max;//4字節
    ngx_pool_t           *current;//4字節
    ngx_chain_t          *chain;//4字節
    ngx_pool_large_t     *large;//4字節
    ngx_pool_cleanup_t   *cleanup;//4字節
    ngx_log_t            *log;//4字節
};

 

   

  我們可以計算得到,在x86的系統中ngx_pool_t結構體各個成員變量占用的空間為40字節。因此last的值為50。end的值為10+1024=1034。max的值為1024-40=984。current=10。可以看到:

在物理內存中,申請到的內存空間被分為了兩部分,前面一部分是ngx_pool_t內存管理結構各個成員變量所占用的空間,此處為40字節。后面部分的984字節的空閑空間才是我們可以在后續的程序中真正可以利用的,用來存放數據的。以上就是Nging內存池創建的主要原理和具體實現。

 

 

(b).內存池的銷毀

  銷毀內存池的工作主要由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);
        }
    }

#if (NGX_DEBUG)

    /*
     * we could allocate the pool->log from this pool
     * so we cannot use this log while free()ing the pool
     */

    for (l = pool->large; l; l = l->next) {
        ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
    }

    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
        ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                       "free: %p, unused: %uz", p, p->d.end - p->d.last);

        if (n == NULL) {
            break;
        }
    }

#endif

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

 

  我們可以看到,銷毀內存池的主要步驟為:先通過遍歷掛在cleanup上數據清理函數鏈表,通過回調函數handler做相應的數據清理;中間輸出部分只與調試程序相關,可忽略。然后遍歷掛在large上的大塊內存鏈表,調用ngx_free()函數釋放節點所占的大塊內存空間;最后,遍歷掛在d->next上的小塊內存池鏈表,釋放小塊內存池(包括管理結構和數據區)占用的空間,在這一步中,我們首先清理了第一塊ngx_pool_t(包括了large、cleanup等成員)代表的小塊內存池,然后再清理剩下的其他小塊內存池。經過以上三個過程,就可以完成數據清理、釋放整個內存池占用的內存空間,並銷毀內存池。需要注意的是:由於內存池的結構,我們必須最后清理管理結構ngx_pool_t(第一塊小塊內存池),因為如果先清理第一塊ngx_pool_t代表的內存池的話,我們就找不到掛在large和cleanup上的單鏈表了,因為我們清理了其單鏈表的第一個節點。

 

 

(c).內存池的重置

  重置內存池,就是將內存池分配到初始分配的狀態。這是由ngx_reset_pool()函數完成的。代碼如下:

 

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

   

  我們可以看到,重置內存池十分簡單。首先將掛在large上的大塊內存鏈表上的各個節點釋放掉,並將pool->large賦值為NULL。之后,將所有小塊內存池構成的單鏈表中的所有節點結尾的last指針重置到剛分配時的位置。小塊內存中存儲的數據並沒有被釋放,其在以后的內存池使用的過程中將會被覆蓋更新。這可以減少內存分配的次數,提升內存重用率。但會浪費一些內存空間。

 

 

 

6.內存池使用源碼詳解

   內存池創建好之后,如何進行使用呢?這些內存使用完了之后是如何進行回收利用的呢?下面的部分將會詳細的介紹內存池的使用。

 

(a).從內存池中申請內存

  在Nginx中,基於內存池的申請方法主要有ngx_palloc、ngx_pnalloc、ngx_pcalloc和ngx_pmemalign共4種方法。而不基於內存池,直接從操作系統中申請內存的主要有ngx_alloc和ngx_calloc共兩種方法。在這一小節中,我們只講述從內存池中申請內存相關的4中方法。而其他的部分將會在后面的小節進行講解。

  基於內存池的4中內存申請方法的區別在第4章:內存池API介紹中已經詳細闡述了。此處不再贅述。

 

(1).ngx_palloc

  下面給出源碼:

 

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

 

  從其實現中,我們可以看出,ngx_palloc()總共有兩個參數,第一個是在那個內存池上申請內存(之前我們曾經提到過通常為每個Http請求或者連接創建一個內存池,此處需要傳遞的參數就是這些內存池對應的指針),另一個參數是size,表示申請內存的大小。進入函數后,首先是判斷申請的內存大小和max(小塊內存標准)的關系,如果size<max,就調用ngx_palloc_small()函數申請內存。否則調用ngx_palloc_large()函數申請內存。下面讓我們先來看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;

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

 

  從上述源碼中,我們可以看到,該函數從current指向的內存池(小塊內存池鏈表)中開始循環遍歷。在每一次遍歷中,我們首先獲得目前內存池中未分配的空閑內存的首地址last,並賦值給m,然后由於從ngx_palloc()函數中傳遞過來的align=1,因此調用ngx_align_ptr(),這是個什么呢?僅從此我們不能判斷其是函數還是宏,下面我們給出其源碼,在src/core/ngx_config.h中,如下所示:

 

#define ngx_align_ptr(p, a)                                                   \
    (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

 

  可以看出,這是一個宏定義,該操作比較巧妙,用於計算以參數a對齊后的偏移指針p。實際上,我們最后分配的內存空間就是從對齊后的偏移指針開始的,這可能會浪費少數幾個字節,但卻能提高讀取效率。接着分析ngx_palloc-small()函數中的源碼,在調用完宏ngx_align_ptr(m, NGX_ALIGNMENT)后我們得到了以默認參數16對齊的偏移指針m。此時,我們已經擁有了對齊后的空閑內存地址空間的首地址m和尾部地址end,我們就可以計算出該塊內存池(一個block)剩余的空閑內存空間大小:p->d.end - m。那么這個剩余的空閑內存空間是否一定能滿足用戶的內存申請請求(size個字節)呢?答案是否定的。因此我們需要將從current開始的每一個小塊內存池的剩余空閑內存空間和size進行比較,遍歷鏈表直到找到滿足申請大小(size個字節)的小塊內存池。如果小塊內存池鏈表上的某塊小塊內存能夠滿足需求,那么我們就將從Nginx的內存池中划分出內存空間,並更新last的值(將last的值后移size個字節),然后返回m。

  如果遍歷完整個小塊內存池都沒有找到滿足申請大小的內存,則程序調用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;

    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;

    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_palloc_block()函數就是應對這種情況而出現的。該函數實現了對內存池的擴容。

  需要注意的是,由於我們遍歷完了整個鏈表,因此此時的pool指針指向的是內存池鏈表的最后一個節點。所以說在ngx_palloc_block()中計算的是當前內存池最后一個節點的大小psize。該大小為需要擴展的空間大小。然后,我們調用前面提到過的ngx_memalgin()函數申請新的內存空間,大小為psize,作為新的小塊內存池節點。之后,我們將這個節點掛在內存池的最后面。具體怎么實現的呢?我們來詳細的看一看。

  首先將這個新節點進行初始化,包括d->end、d->next、d->failed。然后將指向這塊內存的首地址m后移sizeof(ngx_pool_data_t),大家可能還記得我們在創建內存池ngx_pool_create()時,內存池中空閑地址的首地址是在整個內存池的首地址的基礎上后移了sizeof(ngx_pool_t),那么為什么此處創建新的內存池節點只需要后移sizeof(ngx_pool_data_t)呢?在x86系統上,sizeof(ngx_pool_data_t)對應16個字節,而sizeof(ngx_pool_t)對應40個字節。其實大家仔細想一想,我們創建的內存池是小塊內存池鏈表的第一個節點,這個節點中除了包含ngx_pool_data_t結構體之外,還需要包含large指針、cleanup指針等。而小塊內存池后面的節點均沒有必要包含這些成員,因為我們的large鏈表和cleanup鏈表是直接且僅僅掛在小塊內存池鏈表的第一個節點上的。不需要再掛到后續的其他小塊內存池鏈表的結構上。這么想是不是覺得比較合理呢?答案就是這樣的。但是我們之前的重置內存池操作中,並沒有把后續的從第二個節點開始的小塊內存池鏈表上的空閑內存的起始地址初始化為(u_char *)p + sizeof (ngx_pool_data_t),而是將所有節點(包括第一個)的空閑內存地址初始化為(u_char *)p + sizeof (ngx_pool_t)。這樣做會浪費一些內存空間,但是整個重置內存池操作會簡單一點點。因為不用區分第一個節點和其他節點。如果區分的話,我們需要讓第一個節點的空閑內存的起始地址初始化為(u_char *)p + sizeof (ngx_pool_t),將其他節點的空閑內存的起始地址初始化為(u_char *)p + sizeof (ngx_pool_data_t)。我們的Nginx源碼就是這么實現的。大家知道就行了。因為這並不會影響內存池的使用。

  在完成對新的內存池節點的初始化之后。我們需要將這個節點加入到小塊內存池鏈表的尾部。具體怎么實現的呢?

  首先我們找到current指針,並根據這個指針遍歷小塊內存池鏈表,在每一個遍歷中,我們將每個節點的failed成員加1(這是因為你們這些節點不能給我分配內存啊,不然也不會調用我,因此對你們的failed成員統統加1)。並且加1之后,進行判斷,如果某個節點的failed成員的值大於4,那么就將current指向下一個節點(下次再分配內存時將會自動忽略這個節點)。

  在遍歷完小塊內存池的鏈表后,我們的pool指針已經指向了鏈表的最后一個節點,因此在鏈表的尾部插入一個節點非常簡單,p->d.next = new這個語句就能完成。之后返回這個指向這個新節點的空閑內存空間的首地址。

  上述就是ngx_palloc_small()函數完成的功能,內容比較多大家可能都忘了,我們還沒有講解ngx_palloc()函數的另外一個部分:ngx_palloc_large(),這個函數是用於當用戶申請的內存大小大於我們的小塊內存標准max的情況。下面我們將會看到,這種情況下,申請的內存將被當作是大數據塊,將會被掛在large鏈表上。先給出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_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;
}

 

  從上面的代碼中我們可以看出我們首先調用ngx_alloc()函數申請一塊大小為size的內存空間,ngx_alloc()函數實際上就是簡單的封裝了以下malloc()函數,后面我們會詳細的講解。這里知道它是由malloc實現的就好了。申請完內存之后,開始遍歷large鏈表,找到鏈表中alloc為NULL的節點,用alloc指向剛申請到的內存空間並返回。注意這段循環代碼至多執行3次,如果在3次后都沒有找到alloc為NULL的節點,就會退出循環,繼續執行后面的代碼。限制代碼執行的次數是為了提升內存分配的效率,因為large鏈表可能會很大。

  之后,我們調用ngx_palloc_small()重新申請一塊大小為sizeof(ngx_pool_large_t)結構體大小的內存,建立一個新節點。最后我們把新建立的節點插入到large鏈表的頭部,返回申請的內存空間的起始地址。為什么是插入頭部而不是插入尾部呢?這里面其實是有依據的,因為我們之前為了防止large過大將遍歷large鏈表的次數設置為3,如果插在尾部,那么遍歷鏈表前面的三個節點就沒有意義了,因為每次都可能會遍歷不到后面的空閑節點,而導致每次都需要重新建立新節點。並且插入頭部,從頭部開始遍歷也會使得效率比較高。因為這樣遍歷到空閑的大塊內存節點的概率會高很多。

 

 

(2).ngx_pnalloc

  先給出其源碼:

 

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

 

  我們可以看到,ngx_pnalloc()和ngx_palloc()非常相似,唯一的區別就是ngx_pnalloc()中調用的是ngx_palloc_small(pool, size, 0),而ngx_palloc()中調用的是ngx_palloc_small(pool, size, 1)。那么實際上的含義有什么區別呢?ngx_pnalloc()分配內存時不考慮內存數據對齊,而ngx_palloc()分配內存時考慮內存數據對齊。

 

 

(3).ngx_pcalloc

  我們先給出其源碼,如下所示:

 

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

 

  從其實現可以看出,ngx_pcalloc()和ngx_palloc()非常的相似,唯一的區別就是ngx_pcalloc()函數將剛申請到的內存空間全部初始化為0。

 

 

(4).ngx_pmemalign

  我們給出其源碼,如下所示:

 

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

 

  從其源碼實現中,我們可以看出ngx_pmemalign()函數首先調用ngx_memalign()函數來申請對齊的內存地址空間。然后ngx_palloc_small()函數來建立一個新的大數據塊節點。並將ngx_pmemalign()函數申請的內存空間直接掛在新建的大塊數據節點的alloc成員上。最后再將新建的大數據塊節點掛在大塊內存組成的單鏈表中。

  上面就是整個基於內存池申請內存的4種方法的源碼實現及其分析。下面我們會繼續講解釋放內存和回收內存。

  ngx_pfree()函數用於提前釋放大塊內存。

 

 

(b).釋放內存

  此處我們將介紹基於內存池的內存釋放操作函數ngx_pfree(),與內存池無關的內存釋放操作ngx_free()將在后面被講解。

  在Nginx中,小塊內存並不存在提前釋放這么一說,因為其占用的內存較少,不太需要被提前釋放。但是對於非常大的內存,如果它的生命周期遠遠短於所屬的內存池,那么在內存池銷毀之前提前釋放它就變得有意義了。下面先給出其源碼:

 

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

 

  從其實現中可以看出,ngx_pfree()函數的實現十分簡單。通過遍歷large單鏈表,找到待釋放的內存空間(alloc所指向的內存空間),然后調用ngx_free()函數釋放內存。后面我們會看到ngx_free()函數是free()函數的一個簡單封裝。釋放alloc所占用的空間后,將alloc設置為NULL。我們需要注意的是:ngx_pfree()函數僅僅釋放了large鏈表上每個節點的alloc成員所占用的空間,並沒有釋放ngx_pool_large_t結構所占用的內存空間。如此實現的意義在於:下次分配大塊內存時,會期望復用這個ngx_pool_large_t結構體。從這里可以想到,如果large鏈表中的元素很多,那么ngx_pfree()的遍歷耗損的性能是不小的,如果不能確定內存確實非常大,最好不要調用ngx_pfree。

 

 

(c).隨着內存池釋放同步釋放資源的操作

  在Nginx服務器程序中,有些數據類型在回收其所占的資源時不能直接通過釋放內存空間的方式進行,而需要在釋放之前對數據進行指定的數據清理操作。ngx_pool_cleanup_t結構體的函數指針handler就是這么一個數據清理函數,其data成員就指向要清理的數據的內存地址。我們將要清理的方法和數據存放到ngx_pool_cleanup_t結構體中,通過next成員組成內存回收鏈表,就可以實現在釋放內存前對數據進行指定的數據清理操作。而與這些操作相關的方法有:ngx_pool_cleanup_add()、ngx_pool_run_cleanup_file()、ngx_pool_cleanup_file()和ngx_pool_delete_file()共4種。下面我們將分別講解這些操作。

 

(1).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 = 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;
}

 

  從其實現中我們可以看出,我們首先調用ngx_palloc()函數申請cleanup單鏈表中的一個新節點(指向ngx_pool_cleanup_t結構體的指針),然后根據參數size是否為0決定是否需要申請存放目標數據的內存空間。當size>0時,調用ngx_palloc()函數申請大小為size個字節的用於存放待清理的數據的內存空間。這些要清理的數據存儲在ngx_pool_cleanup_t結構體的data成員指向的內存空間中。這樣可以利用這段內存傳遞參數,供清理資源的方法使用。當size=0時,data為NULL。最后將新生成的ngx_pool_cleanup_t結構體掛在cleanup單鏈表的頭部。返回一個指向ngx_pool_cleanup_t結構體的指針。而我們得到后需要設置ngx_pool_cleanup_t的handler成員為釋放資源時執行的方法。

返回的指向ngx_pool_cleanup_t結構體的指針具體怎么使用呢?我們對ngx_pool_cleanup_t結構體的data成員指向的內存空間填充目標數據時,將會為handler成員指定相應的函數。

 

 

(2).ngx_pool_run_cleanup_file()

  在內存池釋放前,如果需要提前關閉文件,則調用該方法。下面給出其源碼,如下所示:

 

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

 

  再給出ngx_pool_cleanup_file結構體的聲明和定義(在src/core/ngx_palloc.h頭文件中),如下所示:

 

typedef struct {
    ngx_fd_t              fd;
    u_char               *name;
    ngx_log_t            *log;
} ngx_pool_cleanup_file_t;

 

  從上述源碼中,我們可以看出,ngx_pool_run_cleanup_file()通過遍歷cleanup單鏈表,尋找單鏈表上的一個節點,這個節點滿足handler(函數指針)等於ngx_pool_cleanup_file(在與函數名相關的表達式中,函數名會被編譯器隱式轉換成函數指針)。由於ngx_pool_cleanup_t結構體的data成員經常會指向ngx_pool_cleanup_file_t(在后面的ngx_pool_cleanup_file()函數中我們可以看到),我們將這個節點data指針賦值給cf(ngx_pool_cleanup_t結構指針)。之后如果傳遞過來的參數fd與cf->fd相同的話(代表我們找到了需要提前關閉的文件描述符fd),就提前執行ngx_pool_cleanup_file(fd),進行文件的關閉操作。

 

 

(3).ngx_pool_cleanup_file()

  該方法以關閉文件的方式來釋放資源,可以被設置為ngx_pool_cleanup_t的handler成員(函數指針)。我們給出其源碼實現,如下所示:

 

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

 

  可以看出,ngx_pool_cleanup_t結構的data成員指向ngx_pool_cleanup_file_t結構體(前面講解ngx_pool_run_cleanup_file()提到過)。之后直接調用ngx_close_file()函數關閉對應的文件。而ngx_close_file()底層是是通過close()函數實現的。

 

 

(4).ngx_pool_delete_file()

  以刪除文件來釋放資源的方法,可以設置到ngx_pool_cleanup_t的handler成員。我們先給出其源碼,如下所示:

 

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

 

  可以看出,ngx_pool_cleanup_t結構的data成員指向ngx_pool_cleanup_file_t結構體,在程序中我們先將傳遞過來的參數data(待清理的目標數據)賦值給c,然后對c的成員name(文件名稱)調用ngx_delete_file()函數,完成對文件的刪除操作,之后調用ngx_close_file()函數關閉相應的文件流(關閉這個文件流可以阻止刪除的文件再次被訪問,並且釋放FILE結構使得它可以被做用於其他的文件),這就是我們為什么在刪除對應的文件后還需要關閉打開的文件流的原因。

  補充一下:ngx_close_file和ngx_delete_file其實是一個宏定義,我們可以在src/os/unix/ngx_files.h中看到其具體實現,如下所示:

 

#define ngx_close_file           close
#define ngx_close_file_n         "close()"


#define ngx_delete_file(name)    unlink((const char *) name)
#define ngx_delete_file_n        "unlink()"

 

  可以看到,ngx_close_file其實就是close,在Nginx服務器程序編譯階段僅僅做一個簡單的替換。ngx_delete_file(name)也是一個宏定義,本質上為unlink((const char *) name),該函數會刪除參數name指定的文件。

  

 

(d).與內存池無關的資源分配、釋放操作

   與內存池無關的內存分配和釋放操作主要有ngx_alloc()、ngx_calloc()和ngx_free()共3中操作方法。下面我們將繼續講解它們的具體實現。

 

(1).ngx_alloc()

  ngx_alloc()函數直接從操作系統中申請內存,其實現是對malloc()函數的一個簡單封裝。我們可以在src/os/unix/ngx_alloc.c中找到其源碼。如下所示:

 

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

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

    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);

    return p;
}

 

  可以看到,其實現非常簡單。僅僅是封裝了malloc()函數,並做了一些日志和調試方面的處理。

 

 

(2).ngx_calloc()

  ngx_calloc()和ngx_alloc()非常相似,唯一的區別是在調用malloc()函數申請完內存之后,會調用ngx_memzero()函數將內存全部初始化為0。ngx_memzero()就是memset()函數。

 

 

(3).ngx_free()

  我們可以在src/os/unix/ngx_alloc.h中看到其源碼,如下所示:

 

#define ngx_free          free

 

  可以看到Nginx程序釋放內存的函數非常簡單,和銷毀內存池中用的是同一個(free)。這里需要再次說明的是:對於在不同場合下從內存池中申請的內存空間的釋放時機是不一樣的。一般只有大數據塊才直接調用ngx_free()函數進行釋放,其他數據空間的釋放都是在內存池銷毀的時機完成的,不需要提前完成。

  至此,Nginx與內存相關的操作的源碼實現已基本講完了。大家如果想進一步研究和學習Nginx內存管理機制,可以從官方下載Nginx源碼,從源碼中去發現Nginx降低系統內存開銷的方法。

 

 

 

7.小結

  所有的講解都講述完了,我們來進行總結一下。在第1節中,我們介紹了Nginx的內存管理機制-內存池的基本原理和使用內存池管理Nginx服務器程序帶來的好處。為了方便大家對內存池結構的理解,我們在第2節中特意給出了ngx_pool_t內存池的示意圖2.1,並簡單的闡述了這個圖的具體含義。在此基礎上,我們繼續在第3節中講述了與內存池相關的重要的數據結構,主要包括ngx_pool_t、ngx_pool_data_t、ngx_pool_large_t和ngx_pool_cleanup_t。然后為了給大家一個內存池操作方法的宏觀介紹,我們在第4節講述了內存的主要操作方法(共15個分成4類)。之后在第5節中我們詳細介紹了內存池的管理,主要包括內存池的創建、銷毀和重置。在第6節中我們詳細介紹了內存池的使用,主要包括從內存池中如何申請內存、釋放內存和回收內存。這兩個小結是整個Nginx內存管理的精華部分,我們在這部分中詳細的分析Nginx的源碼實現,從源碼的角度去講解Nginx內存管理用到的技術,方便我們在以后的程序設計中可以借鑒和學習。最后,希望這篇文章能真正幫助到大家學習Nginx。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM