前言
一、内存池基本概念
二、nginx数据类型监测
三、nginx内存池相关结构体定义
1.内存池数据管理结构
2.内存池数据结构
3.大内存块数据结构
4.内存释放处理结构
四、内存池的操作
1.内存池创建
2.内存池的使用
3.小内存块的申请
4.大内存块的申请
5.内存池的释放
五、代码示例
1.开发环境
2.代码改造
3.测试demo
4.编译命令
5.运行效果
由于业务的需要经常用nginx作为反向代理服务器,提供http的web服务、rtmp和hls的流媒体服务,虽然对于nginx的配置和使用了然于胸,但是对于nginx的实现原理却知之甚少,知其然却不知其所以然,作为一个码龄超过20年的c++程序员显然是有些说不过去的,从本期开始将会不定期的更新对于nginx源代码的解读文档,一个是对自己学习的总结,另外一个也希望能够给大家一些不一样的解读角度,让大家更加易于理解nginx的源代码
一、内存池基本概念
内存池(Memory Pool)是一种内存分配方式,又被称为固定大小区块规划(fixed-size-blocks allocation)。通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能,内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存,然后通过链表的方式将内存块串联起来统一管理,这样做的一个显著优点是,使得内存分配效率得到提升。
ngix本身提供了诸多的自定义类型,其命名结构规则为ngx(前缀)_类型关键字_t(指的是typedef),主要包括基础类型、高级类型、通信类型、组件类型等
nginx有一套完整的内存池定义、应用和操作流程,我们先来看一下nginx内存池的定义,主要包括内存池数据管理结构(ngx_pool_data_t)、内存池数据结构(ngx_pool_t)、大内存块数据结构(ngx_pool_large_t)、内存释放处理结构(ngx_pool_cleanup_t)等内容
ngx_pool_data_t结构体主要负责小内存块(申请的内存大小size小于ngx_pool_s结构体内的max)的管理
typedef struct {
u_char *last; //当前内存分配结束后的位置,即下一个可使用的剩余内存起始地址
u_char *end; //内存池结束位置
ngx_pool_t *next; //整个内存池包含的内存块的链表,指向下一个内存块
ngx_uint_t failed; //不满足要求分配失败的数量
} ngx_pool_data_t;
主要是负责对于内存块的整体管理,不管这个内存块是所谓的大块还是小块,同时利用current指针优化了在多内存块的情况下数据查询的效率。大内存块指的是变量large指向的内容为大于max值的不规则大小的内存链表,小块则是current指向的规则的ngx_pool_s内存区域首地址
typedef struct ngx_pool_s ngx_pool_t;
struct ngx_pool_s{
ngx_pool_data_t d; //数据块
size_t max; //内存池块的最大可用值s
ngx_pool_t *current; //当前内存池块的地址
ngx_chain_t *chain; //该指针挂接一个ngx_chain_t结构
ngx_pool_large_t *large; //大块内地址,大块内存即超过max的内存请求
ngx_pool_cleanup_t *cleanup; //释放内存池的callback操作函数
ngx_log_t *log; //日志信息
};
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
ngx_pool_large_t *next;
void *alloc;
};
ngx_pool_large_s本身也比较简单,就是一个标准链表,指向下一个节点的指针和本节点的申请的内存区域起始地址指针.
typedef void (*ngx_pool_cleanup_pt)(void *data);
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
struct ngx_pool_cleanup_s {
//是一个函数指针,指向一个可以释放data所对应资源的函数。该函数的只有一个参数,就是data
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)
{
ngx_pool_t *p;
//采用16位对齐的方式申请size字节的内存空间
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
if (p == NULL) {
return NULL;
}
//从新申请内存的投标跳过sizeof(ngx_pool_t),p->d.last是能够可以使用的内存区域的起始地址
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
//此次申请内存区域的最大边界
p->d.end = (u_char *) p + size;
//下一个内存块的起始位置
p->d.next = NULL;
//不满足下次(本次为0、下次为1,至少两次才会开始计数)内存使用、必须要重新申请的次数
p->d.failed = 0;
size = size - sizeof(ngx_pool_t);
//设定有效内存区域大小,如果大小大于NGX_MAX_ALLOC_FROM_POOL,则用NGX_MAX_ALLOC_FROM_POOL,如果小于则用size
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_create_pool主要操作过程包括2个步骤
关于NGX_MAX_ALLOC_FROM_POOL再啰嗦两句,nginx源码对其的定义是#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1),其中ngx_pagesize此处指的是内存页面大小,在x86(32/64位)架构下,内存页面的大小为4096字节也就是4K,这么做的目的是申请的内存块大小不要超过4096字节,这样在进行申请内存集中在一个内存页面内的好处是内存访问的时候提升内存cache的命中率
如果需要使用的内存小于pool->max,则申请小内存块,否则申请大内存块
void * ngx_palloc(ngx_pool_t *pool, size_t size)
{
//如果需要使用的内存小于pool->max,则申请小内存块,否则申请大内存块
#if !(NGX_DEBUG_PALLOC)
{
if (size <= pool->max) {
return ngx_palloc_small(pool, size, 1);
}
#endif
return ngx_palloc_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;
//记录下内存池第一个内存块的current指针
p = pool->current;
do {
m = p->d.last;
//需要对齐的话保证对齐
if (align) {
m = ngx_align_ptr(m, NGX_ALIGNMENT);
}
//如果剩余的内存空间(p->d.end – m)大于需要使用的内存空间则返回起始地址并修改last的指向减小剩余内存空间的大小。
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;
//按照第一次申请的内存块大小进行新内存块的申请,大小可能是4095或其他size指定的值。
psize = (size_t) (pool->d.end - (u_char *) pool);
//采用16位对齐申请内存
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;
//循环开始设置d.failed的值,目的是修正pool->current的值
//第一次进来p->d.next的值是null,因此只有内存池具备了两个以上内存块才能开始for循环。也就是开始申请第3个内存块才能进入for循环,因为第一个是ngx_creat_poot函数创建的,第二个是第一次进入ngx_palloc_block函数创建的,并在本函数结束的时候初步构建p->d.next的链表,但是次数并没满足for循环进入条件,第二次进入ngx_palloc_block函数才能够开启for循环。条件语句p->d.failed++>4要求p->d.failed的值为5、p->d.failed++后为6才能进入pool->current = p->d.next,也就是说加上第一次的不满足循环要求、需要连续6次进入ngx_palloc_block函数才能够改变pool->current的值,此时内存池中包括第一次ngx_creat_poot创建的内存块,总共有7个经由ngx_memalign申请的内存块,p->d.failed的值变化从第一次到最近申请的内存块的变化为6、4、3、2、1、0、0,p->next的指向的是p->d.failed为4的,但是此时循环并没有终止,后继的p->d.failed还在不断的加1,直至ngx_palloc_block函数执行结束全部的内存块的p->d.failed值分别是6、5、4、3、2、1、0。
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;
}
按照程序的处理逻辑,8次扩容后、总共9个内存块的内存快照如下图所示:
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;
//在ngx_pool_t中大块内存节点large链表中寻找空闲的ngx_pool_larger结点。如果找到将大块内存挂在该结点上的ngx_pool_larger队列中
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
//查找空闲结点数不会超过五次。超过五个结点没找到空闲结点就放弃
if (n++ > 3) {
break;
}
}
//如果超过5次仍没找到空闲的large节点,则创建一个新的ngx_pool_large_t
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;
}
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);
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;
}
}
}
所有的nginx源代码的分析都是基于nginx 1.12.1版本,基于virtualbox6.1部署的64位的ubuntu16.1的系统,为了避免权限的影响,采用root账户进行全部操作,基本和以root的方式登录centos也没啥区别了。开发工具采用的是vscode,安装remote ssh插件,利用方式远程进行编辑、编译、调试方便多多
在文件src/core/ngx_palloc.c的函数ngx_palloc_block内进行修改用于测试内存分配变化情况
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;
//新增测试行
for (p = pool; p->d.next; p = p->d.next) {
printf("p->d.failed:%d\r\n",(int)p->d.failed);
}
printf("--------------------------\r\n");
需要主要的是,此处的修改需要重新编译nginx,之后的示例才起作用。另外示例里面并没有用到ngx_log_t,为了编译通过和更多的模块解耦,程序中涉及ngx_log_error函数的内容进行了注释不在起作用,同样也需要先重新编译nginx生成新的.o文件才行
#include
#include
#include
#include "ngx_config.h"
#include "ngx_core.h"
#include "ngx_list.h"
#include "ngx_palloc.h"
#include "ngx_string.h"
#define N 10
void print_list(ngx_list_t *l)
{
ngx_list_part_t *p = &(l->part);
while(p)
{
for(int i = 0 ; i < p->nelts ;i++)
{
char* data = ((ngx_str_t*)p->elts+i)->data;
printf("%s\n",data);
}
p = p->next;
printf("------------------\n");
}
}
int main()
{
ngx_pool_t *pool = NULL;
pool = ngx_create_pool(5000,0);
printf("ngx_pagesize等于0的情况下pool->max等于:%ld\r\n",pool->max);
ngx_destroy_pool(pool);
ngx_pagesize = 4096;
pool = ngx_create_pool(5000,0);
printf("ngx_pagesize等于4096的情况下pool->max等于:%ld\r\n",pool->max);
ngx_list_t* l = ngx_list_create(pool,N,sizeof(ngx_str_t));
for(int i = 0;i < 80 ;i++)
{
ngx_str_t *ptr = ngx_list_push(l);
char *buf = ngx_palloc(pool,512);
sprintf(buf,"demo:%d\n",i+1);
ptr->len = strlen(buf);
ptr->data = buf;
}
//print_list(l);
ngx_destroy_pool(pool);
return 1;
}
gcc -o pool ngx_pool_demo.c -I./core/ -I./os/unix/ -I../objs/ -I../objs/src/core ../objs/src/core/ngx_palloc.o ../objs/src/core/ngx_list.o ../objs/src/os/unix/ngx_alloc.o -g