文章目录
- Nginx内存池总览
- 内存池中变量类型定义
- 创建内存池:ngx_create_pool
- 内存池分配空间:ngx_palloc
- 小块内存空间分配:ngx_palloc_small
- 创建小块内存池:ngx_palloc_block
- 大块内存空间分配:ngx_palloc_large
- <br />
- 重置内存池:ngx_reset_pool
- 释放大块内存:ngx_pfree
- 销毁内存池:ngx_destroy_pool
- 注册内存回收函数:ngx_pool_cleanup_add
Nginx内存池总览
内存池中变量类型定义
/*
* 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.
*/
// 内存池中小内存和大内存分配的分界点:一个页的大小4k
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
// 默认的内存池大小
#define NGX_DEFAULT_POOL_SIZE (16 * 1024)
// 内存池的对齐字节数
#define NGX_POOL_ALIGNMENT 16
// 最小内存池大小
#define NGX_MIN_POOL_SIZE \
ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), \
NGX_POOL_ALIGNMENT)
typedef void (*ngx_pool_cleanup_pt)(void *data);
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;
};
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;
// 内存池类型
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;
ngx_pool_cleanup_t *cleanup;
ngx_log_t *log;
};
创建内存池:ngx_create_pool
// 创建内存池
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p; // 内存池的头部信息,记录内存池中可用内存的信息
// 对size进行16字节的内存对齐之后开辟对应大小的空间(不同平台调用函数不同)
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_palloc
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
// 如果需要分配的内存小于等于pool->max,那么就使用小内存分配
// 否则就使用大内存块分配。注意:pool->max最大也不能超过一个页面大小(4K)
if (size <= pool->max) {
return ngx_palloc_small(pool, size, 1);
}
#endif
return ngx_palloc_large(pool, size);
}
小块内存空间分配: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; // 从pool->current指向的大内存块进行分配内存
do {
m = p->d.last;
if (align) {
// 对可用空间的起始地址进行内存对齐
// 这样可以减少CPU进行IO的次数,提高CPU访问数据的效率
m = ngx_align_ptr(m, NGX_ALIGNMENT);
}
// 判断当前大内存块中的可用内存是否能够分配size大小的内存
// 如果可以,就直接分配出去。否则就寻找下一个大内存块中的可用内存
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);
}
创建小块内存池: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);
// 分配内存对齐之后的大内存块
// 注意:分配的大内存块只有内存块的头信息(ngx_pool_data_t),其余的部分都没有
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
if (m == NULL) {
return NULL;
}
// new成为新大内存的头信息
new = (ngx_pool_t *) m;
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
// m在跳过内存块的头部信息之后,进行内存对齐。最后就可以被分配出去使用了
m += sizeof(ngx_pool_data_t);
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;
// 遍历内存块链表,并设置pool->current
for (p = pool->current; p->d.next; p = p->d.next) {
// 如果内存块分配内存失败的次数超过4次,那么下一次分配内存的时候
// 就会默认该大内存块所剩的可用内存太小,不适合进行内存分配了
// 之后分配内存的时候就会直接跳过该大内存块,使用后面的大内存块
// 进行内存分配
if (p->d.failed++ > 4) {
pool->current = p->d.next;
}
}
// 将新创建的大内存块串联在链表中
p->d.next = new;
return m;
}
大块内存空间分配: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); // malloc大块内存
if (p == NULL) {
return NULL;
}
n = 0;
// 遍历大块内存块的链表,将其中节点中alloc变量指向NULL的节点指向刚创建的大块内存
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
// 为了减少遍历节点,所以只会从pool->large向后找三个节点
// 如果这三个节点的alloc都不为NULL的话,就不会在找了,而是创建新的large节点
if (n++ > 3) {
break;
}
}
// 使用小块内存分配器分配large节点的头部信息
large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
if (large == NULL) {
ngx_free(p);
return NULL;
}
// 将节点的alloc指针指向分配的大块内存
// 并将头部信息节点头插到large链表中
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
重置内存池: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) {
// fixbug: 除了第一个内存池,其余的内存池头部信息只有ngx_pool_data_t
// 不需要跳过ngx_pool_t这么多
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
p->d.failed = 0; // 重置内存池分配内存失败次数
}
/*
// 完美的重置小块内存池的方式
for (p = pool; p; p = p->d.next) {
if (p == pool) { // 处理第一个内存池
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
} else { // 处理第二个及其之后的内存池
p->d.last = (u_char *) p + sizeof(ngx_pool_data_t);
}
p->d.failed = 0;
}
*/
// 重置内存池元信息
pool->current = pool; // 分配内存的第一个内存池重置为pool
pool->chain = NULL;
pool->large = NULL; // 重置大块内存链表
}
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
// 如果需要分配的内存小于等于pool->max,那么就使用小内存分配
// 否则就使用大内存块分配。注意:pool->max最大也不能超过一个页面大小(4K)
if (size <= pool->max) {
return ngx_palloc_small(pool, size, 1);
}
#endif
return ngx_palloc_large(pool, size);
}
释放大块内存:ngx_pfree
// 释放内存池中起始地址为p的大块内存
ngx_int_t
ngx_pfree(ngx_pool_t *pool, void *p)
{
ngx_pool_large_t *l;
for (l = pool->large; l; l = l->next) {
// 在大块内存块的链表中找到以p为起始位置的节点
// 释放p指向大块内存块,并将其头部信息节点的alloc置为NULL
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;
}
为什么Nginx只提供了大块内存的释放,而不提供小块内存的释放?
- 从小块内存的分配方式来看,小块内存无法被直接回收。因为需要被释放的小块内存的前后可能都在被使用中,所以不能直接更新last指针(可用内存空间的起始地址)或者直接释放该内存。
- 从Nginx的应用场景来看,Nginx是一个HTTP服务器,并且是一个基于短连接的服务器。在客户端和服务端的一次request和response之后,连接就会自动断开。即使是HTTP1.1中提供的keep-alive也会有时间限制,并不是会一直占用这个连接。当连接断开的时候,Nginx就会调用ngx_reset_pool重置整个内存池,这个时候小块内存和大块内存就都会被释放。这个内存池也可以给下一个连接进行分配内存。正是因为Nginx是一个HTTP服务器,而不是一个一直需要为一个用户提供服务的服务器,因此可以不用释放小块内存的资源。
销毁内存池: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;
// 执行用户注册的cleanup回调,将内存块指向的资源释放掉
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节点的头信息都在小块内存池中
// 所以小块内存池一定要最后才能被销毁
}
注册内存回收函数:ngx_pool_cleanup_add
如果直接释放掉内存的话,那么内存块中指向的资源就资源泄漏了。因此用户需要将资源释放函数注册到内存池中,这样在销毁内存池的时候,就会依次将内存指向的资源,大块内存,小块内存全部释放,从而不会造成内存资源泄漏的情况。
// 注册cleanup回调函数,用于释放内存块指向的资源
// 相当于是内存块对象的析构函数了
ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
// 创建cleanup节点
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
if (c == NULL) {
return NULL;
}
// 给cleanup节点中的data开辟空间
if (size) {
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
c->handler = NULL; // 清理回调函数handler置空
// 将ngx_pool_cleanup_t节点头插到cleanup链表中
c->next = p->cleanup;
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
}