内存池的概念
(Memory Pool)是一种内存分配方式,又被称为固定大小区块规划(fixed-size-blocks allocation)。
通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。
内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。
当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。
这样做的一个显著优点是,使得内存分配效率得到提升。
作用
减少内存碎片
提升内存分配的效率
设计策略
nginx内存大体思路有以下几点:
1:大小内存块(把内存根据一个大小临界值划分为:大内存块、小内存块两种)
2:nginx所有申请的内存都是16字节对齐的,这样做主要是为了解决内存不齐多次寻址导致的性能降低,按照2的n次方大小字节对齐的内存访问速度更快
3:同一个业务的资源申请和释放都统一执行,有效避免因忘记释放某些资源导致的泄露,主要体现在外部资源清理器的设计 — ngx_pool_cleanup_t * cleanup
4:内存分配器用一个分配失败计数器,把小内存块节点划分成热点块和不良块,避免资源多次申请失败的情况(每个节点分配失败次数累计达到6次就把热点指针移动到下一个节点,避免下次申请的时候访问同一个不良节点)
5:大块内存前三节点释放重用策略
结构分析
//外部附加资源链索引信息
typedef struct {
ngx_fd_t fd;//外部文件资源句柄(文件描述符...)
u_char *name;//外部资源名称(文件名...)
ngx_log_t *log;//外部资源日志器
} ngx_pool_cleanup_file_t;
//外部资源链节点
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler;//外部资源清理器回调函数指针
void *data;//外部清理器回调函数指针参数
ngx_pool_cleanup_t *next;//外部资源链表
};
//大内存块链表节点
struct ngx_pool_large_s {
ngx_pool_large_t *next;//下一块大内存链表指针
void *alloc;//大内存块地址指针
};
//小内存块链表节点
typedef struct {
u_char *last;//是一个unsigned char 类型的指针,保存的是/当前内存池分配到末位地址,即下一次分配从此处开始
u_char *end;//内存块结束位置
ngx_pool_t *next;//内存池里面有很多块内存,这些内存块就是通过该指针连成链表的,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;
ngx_chain_t *chain;//缓冲区链表,下一篇博客介绍
ngx_pool_large_t *large;//大内存块(nginx内存池分配策略有大内存块和小内存块之分,能提升内存分配效率)
/*内存池附加的外部资源清理器回调器:
nginx业务在很多时候,除了申请内存外,还伴随着大量的外部资源申请
比如打开了文件或者其他资源句柄,理论上属于同一业务场景的资源集合
应该统一申请统一释放,这里在内存池中集成了清理器
只要在申请资源的时候,把释放资源的回调函数注册进来
等内存池放的时候统一回调清理,就能有效的避免资源泄露)*/
ngx_pool_cleanup_t *cleanup;
ngx_log_t *log;
};
内存池模型
主要函数:
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);//内存池重置
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_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);//注册外部资源释放回调callback,用于释放和内存池相关业务的外部资源(比如http请求中可能会打开、创建一些文件,这些文件需要伴随内存池释放而释放、删除)
void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd);//执行外部资源释放操作(调用事先注册的callback)
void ngx_pool_cleanup_file(void *data);//关闭一个外部资源文件,赋值给回调函数
void ngx_pool_delete_file(void *data);//关闭一个外部资源文件(文件引用计数减一,并不一定真正删除),赋值给回调函数
内存池创建函数
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;//失败次数初始值0
size = size - sizeof(ngx_pool_t);//除去头部节点,就是真实可供外申请的大小
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;//设置大块内存和小块内存临界值,内存池最大不超过4095,x86中页的大小为4K,define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
p->current = p;//热点指针初始位置是第一个节点(分配的时候从这个节点开始搜索合适的内存块)
p->chain = NULL;
p->large = NULL;//初始状态没有大块内存
p->cleanup = NULL;//初始状态没有外部清理器
p->log = log;
return p;
}
线程池销毁函数
void ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
//调用外部资源清理回调callback,清理外部资源(文件...)
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;
}
}
}
内存池重置
重置内存池只是放大块内存,小块内存只移动指针,不做真正释放
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;
}
大块内存释放函数
/这里需要说明一下,大块内存采用头插法,每次申请新节点都会在头部开始搜索可插入的位置;
并且大块内存跟小块内存不一样(小块内存只有销毁内存池会释放,内存池重置的话,只做指针的偏移,这个策略和nginx的业务场景有关),
大块内存节点是可以释放的,但是节点不会直接删除,这样在链表中会形成一些空节点,
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_align_ptr 这是一个用来内存地址取整的宏,取整可以降低CPU读取内存的次数,提高性能。因为内存池并不是调用malloc等系统api申请内存,而是通过移动指针标记,所以内存对齐的话,可以提高内存读取次数
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);
}
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_pal/loc_large(pool, size);
}
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);
}
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;
/* 遍历 pool->current 指向的内存池链表,根据 p->d.failed 值更新 pool->current */
for (p = pool->current; p->d.next; p = p->d.next) {
/* current 字段的变动是根据统计来做的,如果从当前内存池节点分配内存总失败次数(记录在
* 字段 p->d.failed 内) 大于等于 6 次(这是一个经验值,具体判断是 "if (p->d.failed++ > 4)"
* 由于 p->d.failed 初始值为 0,所以当这个判断为真时,至少已经分配失败 6 次了),就将
* current 字段移到下一个内存池节点,如果下一个内存池节点的 failed 统计数也大于等于 6 次,
* 再下一个,依次类推,如果直到最后仍然是 failed 统计数大于等于 6 次,那么 current 字段
* 指向刚新分配的内存池节点 */
if (p->d.failed++ > 4) {
pool->current = p->d.next;
}
}
p->d.next = new;
return m;
}
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;
/*这里需要说明一下,大块内存采用头插法,每次申请新节点都会在头部开始搜索可插入的位置;
并且大块内存跟小块内存不一样(小块内存只有销毁内存池或者重置内存池的时候才会释放,这个策略和nginx的业务场景有关),
大块内存节点是可以释放的,但是节点不会直接删除,这样在链表中会形成一些空节点,
nginx有一个小技巧,前三个节点中的空节点可以重用(第四个节点开始,就算是空节点,
也不会被重用和删除),直接申请一块内存,然后直接挂上去就行了;
所以前三个节点必须尽快释放,否则就会导致第三个节点往后的节点形成一些已经释放的空壳节点,又得不到重用,链表就可能会出现膨胀的问题,性能降低。
*/
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;
arge->next = pool->large;
pool->large = large;
return p;
}
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;
}
外部资源释放器相关函数
//注册cleanup
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;
}
//执行清理器回调函数,执行外部资源清理释放工作
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;
}
}
}
}
//外部资源清理回调最终调用到的接口
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);
}
}
//外部资源清理回调最终调用到的接口
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);
}
}
关于外部资源清理器的使用例子
nginx_file.c的ngx_create_temp_file函数中,注册了一个临时文件清理回调,具体代码如下:
ngx_int_t ngx_create_temp_file(ngx_file_t *file, ngx_path_t *path, ngx_pool_t *pool,gx_uint_t persistent, ngx_uint_t clean, ngx_uint_t access)
{
/*...省略*/
ngx_pool_cleanup_t *cln = ngx_pool_cleanup_add(pool, sizeof(ngx_pool_cleanup_file_t));
if (cln == NULL) {
return NGX_ERROR;
}
for ( ;; ) {
(void) ngx_sprintf(p, "%010uD%Z", n);
if (!prefix) {
ngx_create_hashed_filename(path, file->name.data, file->name.len);
}
ngx_log_debug1(NGX_LOG_DEBUG_CORE, file->log, 0, "hashed path: %s", file->name.data);
file->fd = ngx_open_tempfile(file->name.data, persistent, access);
ngx_log_debug1(NGX_LOG_DEBUG_CORE, file->log, 0,"temp fd:%d", file->fd);
if (file->fd != NGX_INVALID_FILE) {
cln->handler = clean ? ngx_pool_delete_file : ngx_pool_cleanup_file;
ngx_pool_cleanup_file_t *clnf = cln->data;
clnf->fd = file->fd;
clnf->name = file->name.data;
clnf->log = pool->log;
return NGX_OK;
}
/*...省略*/
}
通过这个创建临时文件的函数中,申请了内存和打开了文件,所以在申请内存的同时也注册了一个伴随着内存释放而执行的外部资源清理回调函数