nginx 記憶體池分析
阿新 • • 發佈:2020-10-19
# nginx 記憶體池 ngx_pool_t
nginx 是自己實現了記憶體池的,所以在nginx ngx_pool_t 這個結構也隨處可見,這裡主要分析一下記憶體池的分配邏輯。
記憶體池實現了包括小塊記憶體、大塊記憶體和清理資源幾種資源的處理,應該來說覆蓋了絕大數的使用場景了。
## 相關結構定義
```c
// 大塊記憶體
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
ngx_pool_large_t *next; // 下一個大塊記憶體池
void *alloc; // 實際分配記憶體
};
// 小塊記憶體池
typedef struct {
u_char *last; // 可分配記憶體起始地址
u_char *end; // 可分配記憶體結束地址
ngx_pool_t *next; // 指向記憶體管理結構
ngx_uint_t failed; // 記憶體分配失敗次數
} ngx_pool_data_t;
// 記憶體池管理結構
typedef struct ngx_pool_s ngx_pool_t;
struct ngx_pool_s {
ngx_pool_data_t d; // 小塊記憶體池
size_t max; // 小塊記憶體最大的分配記憶體,評估大記憶體還是小塊記憶體
ngx_pool_t *current; // 當前開始分配的小塊記憶體池
ngx_chain_t *chain; // chain
ngx_pool_large_t *large; // 大塊記憶體
ngx_pool_cleanup_t *cleanup; // 待清理資源
ngx_log_t *log; // 日誌物件
};
```
ngx_pool_t 是整個記憶體池的管理結構,這種結構對於個記憶體池物件來說可能存在多個,但是對於使用者而言,第一下訪問的始終是建立時返回的那個。多個 ngx_pool_t 通過 `d.next` 來進行連線,`current` 指向 當前開始分配的小塊記憶體池,注意 ngx_pool_data_t 在記憶體池結構的起始處,可以進行型別轉換訪問到不同的成員。
## 實現
### 記憶體對齊
```c
#define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1))
#define ngx_align_ptr(p, a) \
(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
```
參考 [ngx_align 值對齊巨集](https://www.cnblogs.com/shuqin/p/13832722.html) 分析,`ngx_align_ptr` 同理
### 建立記憶體池
max 的最大值為 4095,當從記憶體池中申請的記憶體大小大於 max 時,不會從小塊記憶體中進行分配。
```c
ngx_uint_t ngx_pagesize = getpagesize(); // Linux 上是 4096
#define NGX_POOL_ALIGNMENT 16
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) // 4095
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); // 16 位元組對齊申請 size 大小的記憶體
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); // 設定小塊記憶體可分配的最大值(小於 4095)
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;
}
```
記憶體池建立後的結構邏輯如圖所示:
![](https://img2020.cnblogs.com/blog/1461087/202010/1461087-20201018233124363-407757885.png)
### 記憶體申請
申請的記憶體塊以 max 作為區分
```c
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);
}
```
#### 小塊記憶體申請
current 指向每次申請記憶體時開始檢索分配的小塊記憶體池,而 ngx_palloc_small 的引數 pool 在記憶體池沒有回收時,是固定不變的。
```c
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; // 從 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); // 沒有滿足分配的記憶體池,再申請一個小塊記憶體池
}
```
當在小塊記憶體池中找到了合適的記憶體後的結構如下:
![](https://img2020.cnblogs.com/blog/1461087/202010/1461087-20201018233157028-1243893997.png)
當沒有小塊記憶體池滿足申請時,會再申請一個小塊記憶體池來滿足分配,在設定完 last 和 end 兩個記憶體指示器後,對從 current 開始的記憶體池成員 failed 進行自增操作,並且當這個記憶體池的 failed 分配次數大於 4 時,表面這個記憶體分配失敗的次數太多,根據經驗應該下一次分配可能還是失敗,所以直接跳過這個記憶體池,移動 current。
新的記憶體塊插入至記憶體池連結串列的尾端。
```c
#define NGX_ALIGNMENT sizeof(unsigned long) // 8
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); // 16 位元組對齊申請
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;
}
```
分配一塊記憶體池後邏輯結構如下:
![](https://img2020.cnblogs.com/blog/1461087/202010/1461087-20201018233249049-153853293.png)
#### 大塊記憶體申請
大塊記憶體是通過 `large` 連線的,並且都屬於 ngx_create_pool 返回的 ngx_pool_t 結構。malloc 分配的記憶體由一個 ngx_pool_large_t 節點來掛載,而這個 ngx_pool_large_t 節點又是從小塊記憶體池中分配的。
- 為避免large連結串列長度過大導致在遍歷尋找空閒掛載節點耗時過長,限制了遍歷的節點為3,如果沒有滿足要求則直接分配
- 頭插法 插入至large連結串列中,新的節點後面也是最先被訪問
```c
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); // 呼叫 malloc
if (p == NULL) {
return NULL;
}
n = 0;
for (large = pool->large; large; large = large->next) { // 從large 中連結串列中找到 alloc 為 NULL 的節點,將分配的記憶體掛在該節點上
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
if (n++ > 3) { // 為了避免過多的遍歷,限制次數為 0
break;
}
}
// 當遍歷的 ngx_pool_large_t 節點中 alloc 都有指向的記憶體時,從小塊記憶體中分配一個 ngx_pool_large_t 節點用於掛載新分配的大記憶體
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;
}
```
第一次大塊記憶體分配後的結構如下:
![](https://img2020.cnblogs.com/blog/1461087/202010/1461087-20201018233232368-357289374.png)
### 完整記憶體池結構邏輯
- 所有的記憶體池結構都通過 d.next 連線
- 前兩個記憶體池結構的 current 都指向第三個記憶體池結構
- 所有的 ngx_pool_large_t 節點都是從小記憶體池中分配的
- 所有的 ngx_pool_large_t 節點都是連線在首個記憶體池結構上的
- ngx_pool_large_t 節點的 alloc 被釋放但 ngx_pool_large_t 節點不回收
![](https://img2020.cnblogs.com/blog/1461087/202010/1461087-20201018233212941-1409715236.png)
## 總結
ngx_pool_t 記憶體分配方面
- 通過 current 和 d.next 來訪問其他的記憶體池結構
- 插入方式
- 小塊記憶體池通過尾插法插入至記憶體池連結串列的尾端
- 大塊記憶體通過頭插法插入至large連結串列的首部
- 限制次數
- 小記憶體分配失敗(failed)次數大於4次後就不再作為分配記憶體的池子了
- 大記憶體只尋找 large 連結串列中前三節點是否可以掛載新分配的記憶體
- 記憶體對齊,多處記憶體對齊減少記憶體跨 cache 的數量
其實總體而言這是一個比較簡單的記憶體池了,還是有一些記憶體浪費的地方,`限制次數` 可以說明這個情況,不過這也是在簡單、高效和記憶體分配上的一個