天天看點

nginx源碼分析—記憶體池結構ngx_pool_t及記憶體管理

本部落格( http://blog.csdn.net/livelylittlefish)貼出作者(阿波)相關研究、學習内容所做的筆記,歡迎廣大朋友指正!

Content

0. 序

1. 記憶體池結構

1.1 ngx_pool_t結構

1.2 其他相關結構

1.3 ngx_pool_t的邏輯結構

2. 記憶體池操作

2.1 建立記憶體池

2.2 銷毀記憶體池

2.3 重置記憶體池

2.4 配置設定記憶體

2.4.1 ngx_palloc()函數分析

2.4.2 ngx_palloc_block()函數分析

2.5 釋放記憶體

2.6 注冊cleanup

2.7 記憶體池的實體結構

3. 一個例子

3.1 代碼

3.2 如何編譯

3.3 運作結果

4. 小結

5. 緻謝

0. 序

nginx對記憶體的管理由其自己實作的記憶體池結構ngx_pool_t來完成,本文重點叙述nginx的記憶體管理。

nginx記憶體管理相關檔案:

(1) ./src/os/unix/ngx_alloc.h/.c

  • 記憶體相關的操作,封裝了最基本的記憶體配置設定函數
  • 如free/malloc/memalign/posix_memalign,分别被封裝為ngx_free,ngx_alloc/ngx_calloc, ngx_memalign
    • ngx_alloc:封裝malloc配置設定記憶體
    • ngx_calloc:封裝malloc配置設定記憶體,并初始化空間内容為0
    • ngx_memalign:傳回基于一個指定alignment的大小為size的記憶體空間,且其位址為alignment的整數倍,alignment為2的幂。

(2) ./src/core/ngx_palloc.h/.c

  • 封裝建立/銷毀記憶體池,從記憶體池配置設定空間等函數

.表示nginx-1.0.4代碼目錄,本文為/usr/src/nginx-1.0.4。

1. 記憶體池結構

nginx對記憶體的管理均統一完成,例如,在特定的生命周期統一建立記憶體池(如main函數系統啟動初期即配置設定1024B大小的記憶體池),需要記憶體時統一配置設定記憶體池中的記憶體,在适當的時候釋放記憶體池的記憶體(如關閉http連結時調用ngx_destroy_pool進行銷毀)。

是以,開發者隻需在需要記憶體時進行申請即可,不用過多考慮記憶體的釋放等問題,大大提高了開發的效率。先看一下記憶體池結構。

1.1 ngx_pool_t結構

此處統一一下概念,記憶體池的資料塊:即配置設定記憶體在這些資料塊中進行,一個記憶體池可以有多一個記憶體池資料塊。nginx的記憶體池結構如下。 

00048: typedef struct {
00049:   u_char      *last;  //目前記憶體池配置設定到此處,即下一次配置設定從此處開始
00050:   u_char      *end;   //記憶體池結束位置
00051:   ngx_pool_t  *next;  //記憶體池裡面有很多塊記憶體,這些記憶體塊就是通過該指針連成連結清單的
00052:   ngx_uint_t  failed; //記憶體池配置設定失敗次數
00053: } ngx_pool_data_t;    //記憶體池的資料塊位置資訊
00054:
00055:
00056: struct ngx_pool_s{    //記憶體池頭部結構
00057:    ngx_pool_data_t     d;       //記憶體池的資料塊
00058:    size_t              max;     //記憶體池資料塊的最大值
00059:    ngx_pool_t         *current; //指向目前記憶體池
00060:    ngx_chain_t        *chain;   //該指針挂接一個ngx_chain_t結構
00061:    ngx_pool_large_t   *large;   //大塊記憶體連結清單,即配置設定空間超過max的記憶體
00062:    ngx_pool_cleanup_t *cleanup; //釋放記憶體池的callback
00063:    ngx_log_t          *log;     //日志資訊
00064: };
           

其中,sizeof(ngx_pool_data_t)=16B,sizeof(ngx_pool_t)=40B。

nginx将幾乎所有的結構體放在ngx_core.h檔案中重新進行了申明,如下。

typedef struct ngx_module_s      ngx_module_t;
typedef struct ngx_conf_s        ngx_conf_t;
typedef struct ngx_cycle_s       ngx_cycle_t;
typedef struct ngx_pool_s        ngx_pool_t;
typedef struct ngx_chain_s       ngx_chain_t;
typedef struct ngx_log_s         ngx_log_t;
typedef struct ngx_array_s       ngx_array_t;
typedef struct ngx_open_file_s   ngx_open_file_t;
typedef struct ngx_command_s     ngx_command_t;
typedef struct ngx_file_s        ngx_file_t;
typedef struct ngx_event_s       ngx_event_t;
typedef struct ngx_event_aio_s   ngx_event_aio_t;
typedef struct ngx_connection_s  ngx_connection_t;
           

1.2 其他相關結構

其他與記憶體池相幹的資料結構,如清除資源的cleanup連結清單,配置設定的大塊記憶體連結清單等,如下。

00015: /*
00016: * NGX_MAX_ALLOC_FROM_POOL should be (ngx_pagesize - 1), i.e. 4095 on x86.
00017: * On Windows NT it decreases a number of locked pages in a kernel.
00018: */
00019: #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)  //在x86體系結構下,該值一般為4096B,即4K
00020:
00021: #define NGX_DEFAULT_POOL_SIZE    (16* 1024)
00022:
00023: #define NGX_POOL_ALIGNMENT    16
00024: #define NGX_MIN_POOL_SIZE    \
00025:    ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)),    \
00026:    NGX_POOL_ALIGNMENT)
00027:
00028:
00029: typedef void (*ngx_pool_cleanup_pt)(void *data);    //cleanup的callback類型
00030:
00031: typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
00032:
00033: struct ngx_pool_cleanup_s{
00034:    ngx_pool_cleanup_pt handler;
00035:    void    *data;              //指向要清除的資料
00036:    ngx_pool_cleanup_t *next;   //下一個cleanup callback
00037: };
00038:
00039:
00040: typedef struct ngx_pool_large_s ngx_pool_large_t;
00041:
00042: struct ngx_pool_large_s{
00043:    ngx_pool_large_t  *next;    //指向下一塊大塊記憶體
00044:    void    *alloc;             //指向配置設定的大塊記憶體
00045: };
...
...
00067: typedef struct {
00068:    ngx_fd_t   fd;
00069:    u_char    *name;
00070:    ngx_log_t *log;
00071: } ngx_pool_cleanup_file_t;
00072:
           

(gdb) p getpagesize()

$18 = 4096

全局變量ngx_pagesize的初始化是在如下函數中完成的。./src/os/unix/ngx_posix_init.c

ngx_int_t
ngx_os_init(ngx_log_t *log)
{
    ngx_uint_t  n;

#if (NGX_HAVE_OS_SPECIFIC_INIT)
    if (ngx_os_specific_init(log) != NGX_OK) {
        return NGX_ERROR;
    }
#endif

    ngx_init_setproctitle(log);

    /** 該函數為glibc的庫函數,由系統調用實作,傳回核心中的PAGE_SIZE,該值依賴體系結構*/
    ngx_pagesize = getpagesize();      
    ngx_cacheline_size = NGX_CPU_CACHE_LINE;
    ...
}
           

這些資料結構之間的關系,請參考後面的圖。

1.3 ngx_pool_t的邏輯結構

這些資料結構邏輯結構圖如下。注:本文采用UML的方式畫出該圖。

nginx源碼分析—記憶體池結構ngx_pool_t及記憶體管理

2. 記憶體池操作

2.1 建立記憶體池

建立記憶體池有ngx_create_pool()函數完成,代碼如下。

00015: ngx_pool_t *
00016: ngx_create_pool(size_t size, ngx_log_t *log)
00017: {
00018:    ngx_pool_t *p;
00019:
00020:    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
00021:    if (p == NULL) {
00022:       return NULL;
00023:    }
00024:
00025:    p->d.last = (u_char *) p + sizeof(ngx_pool_t);  //last指向ngx_pool_t結構體之後資料取起始位置
00026:    p->d.end = (u_char *) p + size;  //end指向配置設定的整個size大小的記憶體的末尾
00027:    p->d.next = NULL;
00028:    p->d.failed = 0;
00029:
00030:    size = size - sizeof(ngx_pool_t);
00031:    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;  //最大不超過4095B
00032:
00033:    p->current = p;
00034:    p->chain = NULL;
00035:    p->large = NULL;
00036:    p->cleanup = NULL;
00037:    p->log = log;
00038:
00039:    return p;
00040: }
           

例如,調用ngx_create_pool(1024, 0x80d1c4c)後,建立的記憶體池實體結構如下圖。

nginx源碼分析—記憶體池結構ngx_pool_t及記憶體管理

2.2 銷毀記憶體池

銷毀記憶體池由如下函數完成。

void ngx_destroy_pool(ngx_pool_t *pool)

該函數将周遊記憶體池連結清單,所有釋放記憶體,如果注冊了clenup(也是一個連結清單結構),亦将周遊該cleanup連結清單結構依次調用clenup的handler清理。同時,還将周遊large連結清單,釋放大塊記憶體。

2.3 重置記憶體池

重置記憶體池由下面的函數完成。

void ngx_reset_pool(ngx_pool_t *pool);

該函數将釋放所有large記憶體,并且将d->last指針重新指向ngx_pool_t結構之後資料區的開始位置,同剛建立後的位置相同。

2.4 配置設定記憶體

記憶體配置設定的函數如下。

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

傳回值為配置設定的記憶體起始位址。選擇其中的兩個函數進行分析,其他的也很好了解,省略。

2.4.1 ngx_palloc()函數分析

ngx_palloc()代碼如下,分析請參考筆者所加的注釋。

00115: void *
00116: ngx_palloc(ngx_pool_t *pool, size_t size)
00117: {
00118:    u_char    *m;
00119:    ngx_pool_t *p;
00120:
00121:    if (size <= pool->max) {//判斷待配置設定記憶體與max值
00122:
00123:       p = pool->current;   //小于max值,則從current節點開始周遊pool連結清單
00124:
00125:       do {
00126:          m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
00127:
00128:          if ((size_t) (p->d.end - m) >= size) {
00129:             p->d.last = m + size;  //在該節點指向的記憶體塊中配置設定size大小的記憶體
00130:
00131:             return m;
00132:          }
00133:
00134:          p = p->d.next;
00135:
00136:       } while (p);
00137:
00138:       return ngx_palloc_block(pool, size); //連結清單裡沒有能配置設定size大小記憶體的節點,則生成一個新的節點并在其中配置設定記憶體
00139:    }
00140:
00141:    return ngx_palloc_large(pool, size);  //大于max值,則在large連結清單裡配置設定記憶體
00142: }
           

例如,在2.1節中建立的記憶體池中配置設定200B的記憶體,調用ngx_palloc(pool, 200)後,該記憶體池實體結構如下圖。

nginx源碼分析—記憶體池結構ngx_pool_t及記憶體管理

2.4.2 ngx_palloc_block()函數分析

ngx_palloc_block函數代碼如下,分析請參考筆者所加的注釋。

00175: static void *
00176: ngx_palloc_block(ngx_pool_t *pool, size_t size)
00177: {
00178:    u_char    *m;
00179:    size_t    psize;
00180:    ngx_pool_t *p, *new, *current;
00181:
00182:    psize = (size_t) (pool->d.end - (u_char *) pool);      //計算pool的大小
00183:
00184:    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);//配置設定一塊與pool大小相同的記憶體
00185:    if (m == NULL) {
00186:       return NULL;
00187:    }
00188:
00189:    new = (ngx_pool_t *) m;
00190:
00191:    new->d.end = m + psize; //設定end指針
00192:    new->d.next = NULL;
00193:    new->d.failed = 0;
00194:
00195:    m += sizeof(ngx_pool_data_t); //讓m指向該塊記憶體ngx_pool_data_t結構體之後資料區起始位置
00196:    m = ngx_align_ptr(m, NGX_ALIGNMENT); //按4位元組對齊
00197:    new->d.last = m + size;       //在資料區配置設定size大小的記憶體并設定last指針
00198:
00199:    current = pool->current;
00200:
00201:    for (p = current; p->d.next; p = p->d.next) {
00202:       if (p->d.failed++ > 4) {   //failed的值隻在此處被修改
00203:          current = p->d.next;    //失敗4次以上移動current指針
00204:       }
00205:    }
00206:
00207:    p->d.next = new;  //将這次配置設定的記憶體塊new加入該記憶體池
00208:
00209:    pool->current = current ? current : new;
00210:
00211:    return m;
00212: }
           

注意:該函數配置設定一塊記憶體後,last指針指向的是ngx_pool_data_t結構體(大小16B)之後資料區的起始位置。而建立記憶體池時時,last指針指向的是ngx_pool_t結構體(大小40B)之後資料區的起始位置。

結合2.7節的記憶體池的實體結構,更容易了解。

2.5 釋放記憶體

請參考如下函數,不再贅述。 

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)

需要注意的是該函數隻釋放large連結清單中注冊的記憶體,普通記憶體在ngx_destroy_pool中統一釋放。

2.6 注冊cleanup

請參考如下函數,該函數實作也很簡單,此處不再贅述。

ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)

2.7 記憶體池的實體結構

針對本文第3節的例子,畫出的記憶體池的實體結構如下圖。

nginx源碼分析—記憶體池結構ngx_pool_t及記憶體管理

從該圖也能看出2.4節的結論,即記憶體池第一塊記憶體前40位元組為ngx_pool_t結構,後續加入的記憶體塊前16個位元組為ngx_pool_data_t結構,這兩個結構之後便是真正可以配置設定記憶體區域。

是以,本文Reference中的記憶體配置設定相關中的圖是有一點點小問題的,并不是每一個節點的前面都是ngx_pool_t結構。

3. 一個例子

了解并掌握開源軟體的最好方式莫過于自己寫一些測試代碼,或者改寫軟體本身,并進行調試來進一步了解開源軟體的原理和設計方法。本節給出一個建立記憶體池并從中配置設定記憶體的簡單例子。

3.1 代碼

/**
 * ngx_pool_t test, to test ngx_palloc, ngx_palloc_block, ngx_palloc_large
 */

#include <stdio.h>
#include "ngx_config.h"
#include "ngx_conf_file.h"
#include "nginx.h"
#include "ngx_core.h"
#include "ngx_string.h"
#include "ngx_palloc.h"

volatile ngx_cycle_t  *ngx_cycle;

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

void dump_pool(ngx_pool_t* pool)
{
    while (pool)
    {
        printf("pool = 0x%x\n", pool);
        printf("  .d\n");
        printf("    .last = 0x%x\n", pool->d.last);
        printf("    .end = 0x%x\n", pool->d.end);
        printf("    .next = 0x%x\n", pool->d.next);
        printf("    .failed = %d\n", pool->d.failed);
        printf("  .max = %d\n", pool->max);
        printf("  .current = 0x%x\n", pool->current);
        printf("  .chain = 0x%x\n", pool->chain);
        printf("  .large = 0x%x\n", pool->large);
        printf("  .cleanup = 0x%x\n", pool->cleanup);
        printf("  .log = 0x%x\n", pool->log);
        printf("available pool memory = %d\n\n", pool->d.end - pool->d.last);
        pool = pool->d.next;
    }
}

int main()
{
    ngx_pool_t *pool;

    printf("--------------------------------\n");
    printf("create a new pool:\n");
    printf("--------------------------------\n");
    pool = ngx_create_pool(1024, NULL);
    dump_pool(pool);

    printf("--------------------------------\n");
    printf("alloc block 1 from the pool:\n");
    printf("--------------------------------\n");
    ngx_palloc(pool, 512);
    dump_pool(pool);

    printf("--------------------------------\n");
    printf("alloc block 2 from the pool:\n");
    printf("--------------------------------\n");
    ngx_palloc(pool, 512);
    dump_pool(pool);

    printf("--------------------------------\n");
    printf("alloc block 3 from the pool :\n");
    printf("--------------------------------\n");
    ngx_palloc(pool, 512);
    dump_pool(pool);

    ngx_destroy_pool(pool);
    return 0;
}
           

3.2 如何編譯

這個問題是編寫測試代碼或者改寫軟體本身最迫切需要解決的問題,否則,編寫的代碼無從編譯或運作,那也無從進行調試并了解軟體了。

如何對自己編寫的測試代碼進行編譯,可參考Linux平台代碼覆寫率測試-編譯過程自動化及對連結的解釋、Linux平台如何編譯使用Google test寫的單元測試?。我們要做的是學習這種編譯工程的方法,針對該例子,筆者編寫的makefile檔案如下。——這便是本節的主要目的。

CXX = gcc
CXXFLAGS += -g -Wall -Wextra

NGX_ROOT = /usr/src/nginx-1.0.4

TARGETS = ngx_pool_t_test
TARGETS_C_FILE = $(TARGETS).c

CLEANUP = rm -f $(TARGETS) *.o

all: $(TARGETS)

clean:
	$(CLEANUP)

CORE_INCS = -I. \
	-I$(NGX_ROOT)/src/core \
	-I$(NGX_ROOT)/src/event \
	-I$(NGX_ROOT)/src/event/modules \
	-I$(NGX_ROOT)/src/os/unix \
	-I$(NGX_ROOT)/objs \

NGX_PALLOC = $(NGX_ROOT)/objs/src/core/ngx_palloc.o
NGX_STRING = $(NGX_ROOT)/objs/src/core/ngx_string.o
NGX_ALLOC = $(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o

$(TARGETS): $(TARGETS_C_FILE)
	$(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $^ -o [email protected]
           

3.3 運作運作結果

# ./ngx_pool_t_test
--------------------------------
create a new pool:
--------------------------------
pool = 0x8922020
  .d
    .last = 0x8922048
    .end = 0x8922420
    .next = 0x0
    .failed = 0
  .max = 984
  .current = 0x8922020
  .chain = 0x0
  .large = 0x0
  .cleanup = 0x0
  .log = 0x0
available pool memory = 984

--------------------------------
alloc block 1 from the pool:
--------------------------------
pool = 0x8922020
  .d
    .last = 0x8922248
    .end = 0x8922420
    .next = 0x0
    .failed = 0
  .max = 984
  .current = 0x8922020
  .chain = 0x0
  .large = 0x0
  .cleanup = 0x0
  .log = 0x0
available pool memory = 472

--------------------------------
alloc block 2 from the pool:
--------------------------------
pool = 0x8922020
  .d
    .last = 0x8922248
    .end = 0x8922420
    .next = 0x8922450
    .failed = 0
  .max = 984
  .current = 0x8922020
  .chain = 0x0
  .large = 0x0
  .cleanup = 0x0
  .log = 0x0
available pool memory = 472

pool = 0x8922450
  .d
    .last = 0x8922660
    .end = 0x8922850
    .next = 0x0
    .failed = 0
  .max = 0
  .current = 0x0
  .chain = 0x0
  .large = 0x0
  .cleanup = 0x0
  .log = 0x0
available pool memory = 496

--------------------------------
alloc block 3 from the pool :
--------------------------------
pool = 0x8922020
  .d
    .last = 0x8922248
    .end = 0x8922420
    .next = 0x8922450
    .failed = 1
  .max = 984
  .current = 0x8922020
  .chain = 0x0
  .large = 0x0
  .cleanup = 0x0
  .log = 0x0
available pool memory = 472

pool = 0x8922450
  .d
    .last = 0x8922660
    .end = 0x8922850
    .next = 0x8922880
    .failed = 0
  .max = 0
  .current = 0x0
  .chain = 0x0
  .large = 0x0
  .cleanup = 0x0
  .log = 0x0
available pool memory = 496

pool = 0x8922880
  .d
    .last = 0x8922a90
    .end = 0x8922c80
    .next = 0x0
    .failed = 0
  .max = 0
  .current = 0x0
  .chain = 0x0
  .large = 0x0
  .cleanup = 0x0
  .log = 0x0
available pool memory = 496
           

4. 小結

本文針對nginx-1.0.4的記憶體管理進行了較為全面的分析,包括相關記憶體池資料結構,記憶體池的建立、銷毀,以及從記憶體池中配置設定記憶體等。最後通過一個簡單例子向讀者展示nginx記憶體池的建立和配置設定操作,同時借此向讀者展示編譯測試代碼的方法。

分析完nginx的記憶體管理,你一定驚歎于nginx作者的聰明才智。這種記憶體管理的設計方法小巧、快捷,值得借鑒!

5. 緻謝

寫作本文,筆者參考了Reference裡yixiao的Nginx源碼分析-記憶體池和RainX1982的Nginx代碼研究計劃。在此給予他們誠摯的感謝!

Reference

man posix_memalign (manual頁)(Allocate aligned memory)

man getpagesize (manual頁)(Get memory page size)

Nginx源碼分析-記憶體池 (yixiao)

Nginx代碼研究計劃 (RainX1982)

Appendix: posix_memalign

The  function  posix_memalign()  allocates  size  bytes and places theaddress of the allocated memory in*memptr.  The address of the allocated memory will be a multiple of alignment, which must be a  power  oftwo and a multiple of sizeof(void *).

the memory is not zeroed.

posix_memalign() returns zero on success, or one of the error values listed in the next section on  failure.  Note that errno is not set.

繼續閱讀