Ngix内存管理
宏定义和结构体
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) //分配最大的内存 4096 (一页内存)
#define NGX_DEFAULT_POOL_SIZE (16 * 1024) //默认内存池大小 16k
#define NGX_POOL_ALIGNMENT 16 //内存对齐的基准 16
#define NGX_MIN_POOL_SIZE //内存池最小的大小
ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), \
NGX_POOL_ALIGNMENT)
//内存对齐函数
#define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1))
https://blog.csdn.net/weixin_45768137/article/details/127266001?spm=1001.2014.3001.5501
//上述链接是这个函数的讲解,和SGI STL 中的round_up是一样的
//内存块的头部信息
//U_char* == char *
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_MAX_ALLOC_FROM_POOL 4095
ngx_pool_t *current; //指向内存块的首地址
ngx_chain_t *chain;
ngx_pool_large_t *large; //大块内存的入口指针
ngx_pool_cleanup_t *cleanup; //内存块链接指针
ngx_log_t *log; //日志指针
};
//current --- log 只在首个内存块中存在
//下述为大块内存头部信息
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
ngx_pool_large_t *next; //链表节点指针
void *alloc; //指向大块内存的指针
};
//下述为外部内存头部信息
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; //链表节点指针
};
相关函数
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_t *ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
//如果未指定平台,则调用malloc()进行分配内存块
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
if (p == NULL) {
return NULL;
}
//last指向申请内存除头部信息外的首地址,也就是能使用的内存的首地址
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
//end指向申请的内存块的末尾地址
p->d.end = (u_char *) p + size;
p->d.next = NULL;
p->d.failed = 0;
//内存池总体大小最大为 NGX_MAX_ALLOC_FROM_POOL (4095)
size = size - sizeof(ngx_pool_t);
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
//current指向刚刚申请的内存块的地址,后续会有变动
p->current = p;
p->chain = NULL;
//大块内存头部信息指针先置为null
p->large = NULL;
//外部资源处理链表指针先置为null
p->cleanup = NULL;
p->log = log;
return p;
}
创建内存块函数,并且初始化内存卡中的一些头部信息和指针指向.
posix_memalign(&p, alignment, size);
针对不同平台的定义
{
#if (NGX_HAVE_POSIX_MEMALIGN || NGX_HAVE_MEMALIGN)
void *ngx_memalign(size_t alignment, size_t size, ngx_log_t *log);
#else
#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)
#endif
}
#if (NGX_HAVE_POSIX_MEMALIGN)
void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)
{
void *p;
int err;
//返回 size 字节的动态内存,并且该内存的地址是 alignment 的倍数,由p指向这块内存
err = posix_memalign(&p, alignment, size);
if (err) {
ngx_log_error(NGX_LOG_EMERG, log, err,
"posix_memalign(%uz, %uz) failed", alignment, size);
p = NULL;
}
ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,
"posix_memalign: %p:%uz @%uz", p, size, alignment);
return p;
}
#elif (NGX_HAVE_MEMALIGN)
void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)
{
void *p;
//返回一个是size大小并且地址是 alignment 的倍数的内存块
p = memalign(alignment, size);
if (p == NULL) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
"memalign(%uz, %uz) failed", alignment, size);
}
ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,
"memalign: %p:%uz @%uz", p, size, alignment);
return p;
}
#endif
分配内存函数
//不做内存对齐
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_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; //char * 类型
ngx_pool_t *p;
p = pool->current; //p指向当前内存块的首地址,分配内存都从current指向的内存块进行分配
do {
m = p->d.last; //m指向当前内存块的可用首地址
//如果 align > 0 需要内存对齐
if (align) {
/*
将 m 对齐到 NGX_ALIGNMENT 整数倍的地址
NGX_ALIGNMENT:
#define NGX_ALIGNMENT sizeof(unsigned long) // platform word
#define NGX_ALIGNMENT _MAX_ALIGNMENT
*/
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); //当p指向nullptr时跳出循环
//当前所有内存块都不满足分配 size 个字节,进入下述函数,开辟新内存块
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;
//获取传入的 pool 内存块的整体大小(创建内存池时传入的size大小),pool就是我们一开始创建的第一个内存块
psize = (size_t) (pool->d.end - (u_char *) pool);
//分配一个和 pool 大小相同的内存块
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
if (m == NULL) {
return NULL;
}
//new指向新分配的内存块首地址
new = (ngx_pool_t *) m;
//end指向新内存块的末尾地址
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
/*
m指向可用内存块的首地址,并且后续返回给客端
注意!!
ngx_pool_data_t 这是头部信息的结构体
ngx_pool_t 这是包括 current --- log 这些变量的结构体
说明除了第一个内存块有 current --- log 这些变量,其他的内存块都没有这些变量
*/
m += sizeof(ngx_pool_data_t);
//让m内存地址对齐 16 的整数倍
m = ngx_align_ptr(m, NGX_ALIGNMENT);
//新内存块的last指向后续可用内存的首地址,m是要返回给客端的
new->d.last = m + size;
//每次分配失败后对所有的内存块(除了新开辟的)的failed进行++,当failed为4,表示这个内存块
//已经无法进行分配内存了,则调整current指针的位置到下一个内存块,下次就直接从current的指向开始分配
//在ngx_palloc_small()函数中我们可以知道每次分配内存都是从current指针指向的内存块进行分配内存
//所以到这里我们可以知道ngix内存池是由一个个小块内存块进行串联起来的
//问题: 当 current移动之后,记录在第一个内存块中的信息 如: log large chain 指针这些变量会跟着一起移动吗? 如果不移动后续怎么操作
//答:后续所有传入的pool 都是第一个开辟的 pool 只是 pool 里面的current指向改变了
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;
}
大块内存分配 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;
/*
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
ngx_pool_large_t *next;
void *alloc;
};
在Ngix中的小块内存是不会被释放掉的,所以被分配的 大块内存的头部是一直都存在的.
下述代码就是重复利用大块内存的头部信息,
判断前三个large指针的指向是否为空,如果为空,则让它指向新分配的大块内存,
如果前三个large都不为空,则直接创建一个新的大块内存头部指向分配的大块内存
*/
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) {
//如果小块内存分配失败,则free()掉申请的大块内存并且返回null
ngx_free(p);
return NULL;
}
//大块内存头部信息的 alloc 指针指向大块内存的首地址
large->alloc = p;
//使用头插法把大块内存的头部进串起来
large->next = pool->large;
pool->large = large;
return p;
}
通过large指针维护了一个大块内存头部信息的链表(这个头部信息中保存了大块内存的地址).每次重新开辟大块内存时,会判断前三个头部信息的alloc指针是否为空,如果为空则把新开辟的大块内存直接用alloc指向.否则重新分配一个头部信息来管理新开辟的大块内存
内存重置函数 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);
}
}
//此处的内存重置,会导致除了第一块内存块的后续内存块中一部分的内存无法使用
//因为后续内存块的last指针多偏移了 sizeof(ngx_poot_t) - sizeof(ngx_pool_data_t) 字节
/*
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
p->d.failed = 0;
}
*/
//针对第一块内存块进行重置
p = pool;
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
p->d.failed = 0;
//针对后续内存块进行重置
for (p = p->d.next; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_data_t);
p->d.failed = 0;
}
//调整当前current指针
pool->current = pool;
pool->chain = NULL;
pool->large = NULL;
/*
小块内存不释放的问题:
ngix中没有提供任何小块内存的释放函数,并且从小块内存的分配方式来看(偏移指针),它也无法进行
小块内存的释放.
为什么不设置一个小块内存的分配:
nginx本质是一个http服务器,当客户发起一个request请求,nginx处理完成之后,就会释放掉这个客端所占用的
资源(断开链接),此时就可以调用 ngx_reset_pool()来重置内存池了
即使是 keep-alive 链接在时间内没有再次发送请求到服务器,也会执行上述流程
*/
}
通过large指针维护了一个大块内存头部信息的链表(这个头部信息中保存了大块内存的地址),释放的时候遍历这个链表并且依次free(large->alloc)即可
小块内存只需要重置last指针的位置和对第一块内存做特殊处理.
内存池销毁函数 ngx_destroy_pool()
//外部内存:指分配的内存中有指针指向了其他地方的内存
/*
struct data{
int *p //外部内存指针
FILE *fd //外部文件指针
}
*/
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;
}
//如果回调函数有参数传入,那么分配给data内存(作为函数的传入参数)
if (size) {
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
//先把外部内存的析构函数置为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_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
//遍历外部内存的头部信息链表,并且调用所有外部内存的释放函数
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);
//传入需要的参数,如果没有则传输null handler(void *data)
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指针来维护了一个外部内存释放的链表,在销毁内存池的时候都需要遍历这个链表,并且把每个节点的handler调用一遍,然后再依次释放大块内存和小块内存