一:基础知识
内存对齐
内存对齐的概念在此不多描述,这里解释nginx中关于调整内存对齐的实现,在nginx使用了ngx_align_ptr 函数来调整指针指向位置,以实现字节对齐,ngx_align_ptr 在ngx_config.h中宏定义如下
#define ngx_align_ptr(p, a) \
(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
先理解 m &~ n 的意思,举个例子说明
int m = 50;
int n = 4;
printf("m &~ n= %d\n",m &~ (n-1));
printf("m+1 &~ n= %d\n",m+1 &~ (n-1));
printf("m+2 &~ n= %d\n",m+2 &~ (n-1));
printf("m+3 &~ n= %d\n",m+3 &~ (n-1));
printf("m+4 &~ n= %d\n",m+4 &~ (n-1));
打印如下,很明显 从 0~m 中找出最大整除的数字.
在回过头来理解ngx_align_ptr (p,n),p是指针,n是字节对齐的大小,意思就是从0 ~(指针p+8所指向的地址值)中找到最大整除的值(也是地址值),为了直观的得出结论,假设p的地址是8,n也是8,展开宏就是:
(u_char *) (8) + (8 - 1)) & ~(8 - 1))
结合m &~ n 的结论,返回的是一个指向地址8的u_char指针,假设p的地址不能被8整除,而是7 或者 9呢?
显然,当p地址是7时 返回一个指向8的指针,是9时返回的是一个16的地址,这种情况地址9至地址15的内存浪费了,但是这样可以提升性能。所以从结果来看确实是按8字节对齐了。
所以最终ngx_align_ptr(p,n) 目的是将指针p调整到按n字节对齐的第一个较大地址p1
关键的宏定义
字节对齐大小
#define NGX_POOL_ALIGNMENT 16
大小块内存的阈值 NGX_MAX_ALLOC_FROM_POOL ,以此值来区分需要分配的是大块的内存还是小块的内存
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
ngx_pagesize 在ngx_posix_init.c 赋值的,就是内存分页的大小,源码如下
ngx_pagesize = getpagesize();
函数定义在ngx_palloc.h,实现在ngx_palloc.c中
二:内存池的创建
ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log) //创建一个 szie大小的内存
创建完成时各数据的内存分布及指针指向:
三:从内存池的分配内存
在此区分了大小块内存的分配过程,小块使用ngx_palloc_small 接口,大块使用ngx_palloc_large,区分的条件是pool->max,依据前面所得,就是内存页大小
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);
}
3.1小块内存分配
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);
}
3.1.1、遍历查找可以分配出去的内存
遍历一个如下图的单向链表,这个链表什么时候增加的节点先不论,后续讨论,
每次遍历的目的都是为了从当前节点中分配块内存出去,当然前提是有足够的大小,下面分析遍历的逻辑(结合源码):
为了方便说明,引入了2个指针,pstart 和pend,
pstart指向 剩余内存的起始位置,也就是p->d->last,
pend指向剩余内存的结束位置,也就是p->d->end,
在分配时会调用ngx_align_ptr,结合一节“基础知识”,可以知道,是找到pstart与pend之间的第一个按16字节对齐的位置,作为分配的起始位置,也就是源码中的m。(这里其实会浪费一些内存,就是pstart 与 m之间的空间,但是字节对齐后可以提高性能)
if ((size_t) (p->d.end - m) >= size) 就是判断剩余空间的大小是否足够,就是下图中的m 与pend 之间的大小是否大于size
如果大于,说明有足够的空间用来分配,则调整剩余内存的起始位置,也就是last指针的位置
3.1.2、未找到剩余空间比待分配的内存要大的节点
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找到内存池的尾部,并链接新分配的内存
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_pool_t *指向这个内存地址,并把该节点链接到内存池的尾部,(可以对着上面的图与代码查看,这里就不在给图示了),每次遍历时都会从pool->current 开始遍历,这个指针会根据p->d.failed 的次数来调整位置,每次在内存池中分配不了内存,都会从系统新分配一次内存,这样表示内存池分配失败,p->d.failed+1,当大于4时,则会将pool->current 指向下一个指针,提高遍历性能
3.2 大块内存分配
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;
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;
large->next = pool->large;
pool->large = large;
return p;
}