造輪子內存池原因引入
作為C/C++程序員, 相較JAVA程序員的一個重大特征是我們可以直接訪問內存, 自己管理內存, 這個可以說是我們的特色, 也是我們的苦楚了.
java可以有虛擬機幫助管理內存, 但是我們只能自己管理內存, 一不小心產生了內存泄漏問題, 又特別是服務器的內存泄漏問題, 進程不死去, 泄漏的內存就一直無法回收.
所以對于內存的管理一直是我們C系列程序員深挖的事情.
所以對于C++有智能指針這個東西. 還有內存池組件. 內存池組件也不能完全避免內存泄漏, 但是它可以很好的幫助我們定位內存泄漏的點, 以及可以減少內存申請和釋放的次數, 提高效率
大量的malloc/free小內存所帶來的弊端
弊端
- malloc/free的底層是調用系統調用, 這兩者庫函數是對于系統調用的封裝, 頻繁的系統調用所帶來的用戶內核態切換花費大量時間, 大大降低系統執行效率
- 頻繁的申請小內存, 帶來的大量內存碎片, 內存使用率低下且導致無法申請大塊的內存
- 沒有內存回收機制, 很容易造成內存泄漏
內存碎片出現原因解釋
- 內部內存碎片定義: 已經被分配出去了(明確分配到一個進程), 但是無法被利用的空間
- 內存分配的起始地址 一定要是 4, 8, 16整除地址
- 內存是按照頁進行分配的, 中間會產生外部內存碎片, 無法分配給進程
- 內部內存碎片:頻繁的申請小塊內存導致了內存不連續性,中間的小內存間隙又不足以滿足我們的內存申請要求, 無法申請出去利用起來, 這個就是內部內存碎片.
出現場景
最為典型的場景就是高并發是的頻繁內存申請, 釋放. (http請求) (tcp連接)
大牛解決措施(nginx內存池)
nginx內存池, 公認的設計方式非常巧妙的一款內存池設計組件, 專門針對高并發下面的大量的內存申請釋放而產生的.
在系統層,我們可以使用高性能內存管理組件 Tcmalloc Jemalloc(優化效率和碎片問題)
在應用層: 我們可以根據需求設計內存池進行管理 (高并發可以借助nginx內存池設計)
內存池技術
啥叫作內存池技術
就是說在真正使用內存之前, 先提前申請分配一定數量的、大小相等(一般情況下)的內存塊留作備用, 當需要分配內存的時候, 直接從內存塊中獲取. 如果內存塊不夠了, 再申請新的內存塊.
內存池: 就是將這些提前申請的內存塊組織管理起來的數據結構
優勢何在:統一對程序所使用的內存進行統一的分配和回收, 提前申請的塊, 然后將塊中的內存合理的分配出去, 極大的減少了系統調用的次數. 提高了內存利用率. 統一的內存分配回收使得內存泄漏出現的概率大大降低
內存池技術為啥可以解決上文弊端
高并發時系統調用頻繁(malloc free頻繁),降低了系統的執行效率
- 內存池提前預先分配大塊內存,統一釋放,極大的減少了malloc 和 free 等函數的調用。
頻繁使用時增加了系統內存的碎片,降低內存使用效率
- 內存池每次請求分配大小適度的內存塊,最大避免了碎片的產生
沒有內存回收機制,容易造成內存泄漏
- 在生命周期結束后統一釋放內存,極大的避免了內存泄露的發生
高并發內存池nginx內存池源碼刨析
啥是高并發
系統能夠同時并行處理很多請求就是高并發
高并發具備的特征
- 響應時間短
- 支持并發用戶數高
- 支持用戶接入量高
- 連接建立時間短
nginx_memory_pool為啥就適合高并發
內存池生存時間應該盡可能短,與請求或者連接具有相同的周期
減少碎片堆積和內存泄漏
避免不同請求連接之間互相影響
一個連接或者一個請求就創建一個內存池專門為其服務, 內存池的生命周期和連接的生命周期保持一致.
仿寫nginx內存池
實現思路
- 對于每個請求或者連接都會建立相應的內存池,建立好內存池之后,我們可以直接從內存池中申請所需要的內存,不用去管內存的釋放,當內存池使用完成之后一次性銷毀內存池。
- 區分大小內存塊的申請和釋放,大于內存池塊最大尺寸的定義為大內存塊,使用單獨的大內存塊鏈表保存,即時分配和釋放
- 小于等于池尺寸的定義為小內存塊,直接從預先分配的內存塊中提取,不夠就擴充池中的內存,在生命周期內對小塊內存不做釋放,直到最后統一銷毀。
內存池大小, 以及內存對齊的宏定義
#define MP_ALIGNMENT 32
#define MP_PAGE_SIZE 4096
#define MP_MAX_ALLOC_FROM_POOL (MP_PAGE_SIZE-1)
#define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1))
//分配內存起點對齊
結構定義以及圖解分析
typedef struct mp_large_s {
struct mp_large_s* next;
void* alloc;//data區
} mp_large_s;
typedef struct mp_node_s {
unsigned char* last;//下一次內存分配的起點
unsigned char* end;//當前內存塊末尾
size_t failed;//當前內存塊分配失敗的次數
struct mp_node_s* next;
} mp_node_s;
typedef struct mp_pool_s {
mp_large_s* large;//指向大塊內存起點
mp_node_s* current;//指向當前可分配的小內存塊起點
int max;//小塊最大內存
mp_node_s head[0];//存儲地址, 不占據內存,變長結構體技巧
//存儲首塊小內存塊head地址
} mp_pool_s;
mp_pool_s 內存池結構
- large 指向第一個大塊
- current 指向當前可分配的小塊
- head 始終指向第一塊小塊
mp_node_s 小塊內存結構
- last 下一次內存分配的起點, 本次內存分配的終點
- end 塊內存末尾
- failed 當前內存塊申請內存的失敗次數, nginx采取的方式是失敗次數達到一定程度就更換current,current是開始嘗試分配的內存塊, 也就是說失敗達到一定次數, 就不再申請這個內存塊了.
mp_large_s 大塊內存塊
- 正常的申請, 然后使用鏈表連接管理起來.
- alloc 內存塊, 分配內存塊
函數原型以及功能敘述
//函數申明
mp_pool_s *mp_create_pool(size_t size);//創建內存池
void mp_destory_pool( mp_pool_s *pool);//銷毀內存池
void *mp_alloc(mp_pool_s *pool, size_t size);
//從內存池中申請并且進行字節對齊
void *mp_nalloc(mp_pool_s *pool, size_t size);
//從內存池中申請不進行字節對齊
void *mp_calloc(mp_pool_s *pool, size_t size);
//模擬calloc
void mp_free(mp_pool_s *pool, void *p);
void mp_reset_pool(struct mp_pool_s *pool);
//重置內存池
static void *mp_alloc_block(struct mp_pool_s *pool, size_t size);
//申請小塊內存
static void *mp_alloc_large(struct mp_pool_s *pool, size_t size);
//申請大塊內存
對應nginx函數原型
重點函數分塊細節刨析
mp_create_pool: 創建線程池
第一塊內存: 大小設置為 size + sizeof(node) + sizeof(pool) ?
mp_node_s head[0] 啥意思?
mp_pool_s* mp_create_pool(size_t size) {
struct mp_pool_s *p = NULL;
int ret = posix_memalign((void **)&p, MP_ALIGNMENT, size + sizeof(mp_pool_s) + sizeof(mp_node_s));
if (ret) {
return NULL;
}
//內存池小塊的大小限制
p- >max = (size < MP_MAX_ALLOC_FROM_POOL) ? size : MP_MAX_ALLOC_FROM_POOL;
p- >current = p- >head;//第一塊為當前塊
p- >large = NULL;
p- >head- >last = (unsigned char *)p + sizeof( mp_pool_s) + sizeof(mp_node_s);
p- >head- >end = p- >head- >last + size;
p- >head- >failed = 0;
return p;
}
看完了代碼來回答一下問題
- 為了盡可能地避免內存碎片地產生, 小內存地申請, 于是我采取地方式是將 memory pool內存池也放入到首塊內存中地方式. 同時所有地node結點信息也都統一存儲在每一個內存塊中.
- head[0] : 是一種常用于變長結構體地技巧, 不占用內存, 僅僅只是表示一個地址信息, 存儲head node 的地址.
mp_alloc 帶字節對齊的內存申請
首先按照size大小選擇內存分配方式, 小于等于線程池小塊最大大小限制就從已有小塊中申請, 小塊不足就調用mp_alloc_block創建新的小塊 否則就調用 mp_alloc_large 申請創建一個大塊內存
mp_align_ptr 用于字節對齊
void *mp_alloc(mp_pool_s *pool, size_t size) {
mp_node_s* p = NULL;
unsigned char* m = NULL;
if (size <= MP_MAX_ALLOC_FROM_POOL) {//從小塊中分配
p = pool- >current;
do {//循環嘗試從現有小塊中申請
m = mp_align_ptr(p- >last, MP_ALIGNMENT);
if ((size_t)(p- >end - m) >= size) {
p- >last = m + size;
return m;
}
p = p- >next;
} while (p);
//說明小塊中都分配失敗了, 于是從新申請一個小塊
return mp_alloc_block(pool, size);
}
//從大塊中分配
return mp_alloc_large(pool, size);
}
mp_alloc_block 申請創建新的小塊內存
psize 大小等于mp_node_s結點內存大小 + 實際可用內存塊大小
搞清楚內存塊組成:結點信息 + 實際可用內存塊
返回的內存是實際可用內存的起始地址
//申請小塊內存
void *mp_alloc_block(struct mp_pool_s *pool, size_t size) {
unsigned char* m = NULL;
size_t psize = 0;//內存池每一塊的大小
psize = (size_t)((unsigned char*)pool- >head- >end - (unsigned char*)pool- >head);
int ret = posix_memalign((void**)&m, MP_ALIGNMENT, psize);
if (ret) return NULL;
//此時已經分配出來一個新的塊了
mp_node_s* new_node, *p, *current;
new_node = (mp_node_s*)m;
new_node- >end = m + psize;
new_node- >failed = 0;
new_node- >next = NULL;
m += sizeof(mp_node_s);//跳過node
//對于m進行地址起點內存對齊
m = mp_align_ptr(m, MP_ALIGNMENT);
new_node- >last = m + size;
current = pool- >current;
//循環尋找新的可分配內存塊起點current
for (p = current; p- >next; p = p- >next) {
if (p- >failed++ > 4) {
current = p- >next;
}
}
//將new_node連接到最后一塊內存上, 并且嘗試跟新pool- >current
pool- >current = current ? current : new_node;
p- >next = new_node;
return m;
}
mp_alloc_large 申請創建新的大塊內存
大塊內存參考nginx_pool 采取采取的是malloc分配
先分配出來所需大塊內存. 在pool的large鏈表中尋找是否存在空閑的alloc. 存在則將內存掛在上面返回. 尋找5次還沒有找到就另外申請一個新的large結點掛載內存, 鏈接到large list中管理
mp_large_s* node 是從內存池中分配的, 也就是從小塊中分配的 why? 減少內存碎片, 將大塊的node信息放入小塊內存中,避免小內存的申請, 減少內存碎片
留疑? 空閑的alloc從何而來?
void *mp_alloc_large(struct mp_pool_s *pool, size_t size) {
void* p = malloc(size);
if (p == NULL) return NULL;
mp_large_s* l = NULL;
size_t cnt = 0;
for (l = pool- >large; l; l = l- >next) {
if (l- >alloc) {
l- >alloc = p;
return p;
}
if (cnt++ > 3) {
break;//為了提高效率, 檢查前5個塊, 沒有空閑alloc就從新申請large
}
}
l = mp_alloc(pool, sizeof(struct mp_large_s));
if (l == NULL) {
free(p);
return NULL;
}
l- >alloc = p;
l- >next = pool- >large;
pool- >large = l;
return p;
}
空閑的alloc是被free掉了空閑出來的. 雖然nginx采取的是小塊不單獨回收, 最后統一回收, 因為小塊的回收非常難以控制, 不清楚何時可以回收. 但是對于大塊nginx提供了free回收接口.
mp_free_large 回收大塊內存資源
void mp_free_large(mp_pool_s *pool, void *p) {
mp_large_s* l = NULL;
for (l = pool- >large; l; l = l- >next) {
if (p == l- >alloc) {
free(l- >alloc);
l- >alloc = NULL;
return ;
}
}
}
整體代碼附下
#ifndef _MPOOL_H_
#define _MPOOL_H_
#include < stdlib.h >
#include < stdio.h >
#include < string.h >
#include < unistd.h >
#include < fcntl.h >
#define MP_ALIGNMENT 32
#define MP_PAGE_SIZE 4096
#define MP_MAX_ALLOC_FROM_POOL (MP_PAGE_SIZE-1)
#define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1))
//內存起點對齊
typedef struct mp_large_s {
struct mp_large_s* next;
void* alloc;//data區
} mp_large_s;
typedef struct mp_node_s {
unsigned char* last;//下一次內存分配的起點
unsigned char* end;//當前內存塊末尾
size_t failed;//當前內存塊分配失敗的次數
struct mp_node_s* next;
} mp_node_s;
typedef struct mp_pool_s {
mp_large_s* large;//指向大塊內存起點
mp_node_s* current;//指向當前可分配的小內存塊起點
int max;//小塊最大內存
mp_node_s head[0];//存儲地址, 不占據內存,變長結構體技巧
//存儲首塊小內存塊head地址
} mp_pool_s;
//函數申明
mp_pool_s *mp_create_pool(size_t size);//創建內存池
void mp_destory_pool( mp_pool_s *pool);//銷毀內存池
void *mp_alloc(mp_pool_s *pool, size_t size);
//從內存池中申請并且進行字節對齊
void *mp_nalloc(mp_pool_s *pool, size_t size);
//從內存池中申請不進行字節對齊
void *mp_calloc(mp_pool_s *pool, size_t size);
//模擬calloc
void mp_free(mp_pool_s *pool, void *p);
void mp_reset_pool(struct mp_pool_s *pool);
//重置內存池
static void *mp_alloc_block(struct mp_pool_s *pool, size_t size);
//申請小塊內存
static void *mp_alloc_large(struct mp_pool_s *pool, size_t size);
//申請大塊內存
mp_pool_s* mp_create_pool(size_t size) {
struct mp_pool_s *p = NULL;
int ret = posix_memalign((void **)&p, MP_ALIGNMENT, size + sizeof(mp_pool_s) + sizeof(mp_node_s));
if (ret) {
return NULL;
}
//內存池小塊的大小限制
p- >max = (size < MP_MAX_ALLOC_FROM_POOL) ? size : MP_MAX_ALLOC_FROM_POOL;
p- >current = p- >head;//第一塊為當前塊
p- >large = NULL;
p- >head- >last = (unsigned char *)p + sizeof( mp_pool_s) + sizeof(mp_node_s);
p- >head- >end = p- >head- >last + size;
p- >head- >failed = 0;
return p;
}
void mp_destory_pool( mp_pool_s *pool) {
//先銷毀大塊
mp_large_s* l = NULL;
mp_node_s* p = pool- >head- >next, *q = NULL;
for (l = pool- >large; l; l = l- >next) {
if (l- >alloc) {
free(l- >alloc);
l- >alloc = NULL;
}
}
//然后銷毀小塊內存
while (p) {
q = p- >next;
free(p);
p = q;
}
free(pool);
}
//申請小塊內存
void *mp_alloc_block(struct mp_pool_s *pool, size_t size) {
unsigned char* m = NULL;
size_t psize = 0;//內存池每一塊的大小
psize = (size_t)((unsigned char*)pool- >head- >end - (unsigned char*)pool- >head);
int ret = posix_memalign((void**)&m, MP_ALIGNMENT, psize);
if (ret) return NULL;
//此時已經分配出來一個新的塊了
mp_node_s* new_node, *p, *current;
new_node = (mp_node_s*)m;
new_node- >end = m + psize;
new_node- >failed = 0;
new_node- >next = NULL;
m += sizeof(mp_node_s);//跳過node
//對于m進行地址起點內存對齊
m = mp_align_ptr(m, MP_ALIGNMENT);
new_node- >last = m + size;
current = pool- >current;
for (p = current; p- >next; p = p- >next) {
if (p- >failed++ > 4) {
current = p- >next;
}
}
//將new_node連接到最后一塊內存上, 并且嘗試跟新pool- >current
pool- >current = current ? current : new_node;
p- >next = new_node;
return m;
}
//申請大塊內存
void *mp_alloc_large(struct mp_pool_s *pool, size_t size) {
void* p = malloc(size);
if (p == NULL) return NULL;
mp_large_s* l = NULL;
size_t cnt = 0;
for (l = pool- >large; l; l = l- >next) {
if (l- >alloc) {
l- >alloc = p;
return p;
}
if (cnt++ > 3) {
break;//為了提高效率, 檢查前5個塊, 沒有空閑alloc就從新申請large
}
}
l = mp_alloc(pool, sizeof(struct mp_large_s));
if (l == NULL) {
free(p);
return NULL;
}
l- >alloc = p;
l- >next = pool- >large;
pool- >large = l;
return p;
}
//帶有字節對齊的申請
void *mp_alloc(mp_pool_s *pool, size_t size) {
mp_node_s* p = NULL;
unsigned char* m = NULL;
if (size < MP_MAX_ALLOC_FROM_POOL) {//從小塊中分配
p = pool- >current;
do {
m = mp_align_ptr(p- >last, MP_ALIGNMENT);
if ((size_t)(p- >end - m) >= size) {
p- >last = m + size;
return m;
}
p = p- >next;
} while (p);
//說明小塊中都分配失敗了, 于是從新申請一個小塊
return mp_alloc_block(pool, size);
}
//從大塊中分配
return mp_alloc_large(pool, size);
}
//不帶字節對齊的從內存池中申請內存
void *mp_nalloc(mp_pool_s *pool, size_t size) {
mp_node_s* p = NULL;
unsigned char* m = NULL;
if (size < MP_MAX_ALLOC_FROM_POOL) {//從小塊中分配
p = pool- >current;
do {
m = p- >last;
if ((size_t)(p- >end - m) >= size) {
p- >last = m + size;
return m;
}
p = p- >next;
} while (p);
//說明小塊中都分配失敗了, 于是從新申請一個小塊
return mp_alloc_block(pool, size);
}
//從大塊中分配
return mp_alloc_large(pool, size);
}
void *mp_calloc(struct mp_pool_s *pool, size_t size) {
void *p = mp_alloc(pool, size);
if (p) {
memset(p, 0, size);
}
return p;
}
void mp_free(mp_pool_s *pool, void *p) {
mp_large_s* l = NULL;
for (l = pool- >large; l; l = l- >next) {
if (p == l- >alloc) {
free(l- >alloc);
l- >alloc = NULL;
return ;
}
}
}
#endif
-
內存
+關注
關注
8文章
3103瀏覽量
74919 -
JAVA
+關注
關注
20文章
2983瀏覽量
106645 -
程序
+關注
關注
117文章
3817瀏覽量
82255 -
函數
+關注
關注
3文章
4365瀏覽量
63965 -
nginx
+關注
關注
0文章
163瀏覽量
12461
發布評論請先 登錄
C++內存池的設計與實現
內存池可以調節內存的大小嗎
RT-Thread內存管理之內存池實現分析
Linux 內存池源碼淺析
Nginx目錄結構有哪些

內存池主要解決的問題

評論