从今天开始深入介绍Nginx框架。
首先来谈谈我对《深入理解Nginx模块开发与架构解析》看法,这本书应该是到目前为止,市面写的最详细,最充实的书籍(没有之一),值得拥有。然而此书对于一个小白来说,并不太适合,此书适合有相关使用经验或者开发经验,适合于进一步深造的同学。如果是小白,建议先浏览一下网上的博客,对Nginx各个方面有一定了解,然后在深入阅读此书。这是仅仅是我个人经验,毕竟我是这样走过来的。最后建议阅读此书的朋友,最好多阅读几遍,每一次都有不少的收获。
开始我们今天的主题,任何一款软件都离不开数据结构,良好的数据结构对于软件的发展会起到事半功倍的效果。
一、Nginx很奇葩
Nginx这款软件很奇葩,但同时体现出它的优秀。说它奇葩之处体现在:为了节约内存,不会轻易主动申请内存,而经常复用,例如利用一个指针最低2bit始终为0这个特性,来存储一个字段。但是它为了提升性能,却又申请大块空间,例如在共享内存方面,它为每个变量申请128字节(考虑CPU二级缓存)。
二、数据结构
2.1数据结构
本篇将介绍始终贯穿整个软件的对象--ngx_pool_t内存池。内存池存在意义,不用多说,直接上数据结构定义。
typedef struct ngx_pool_large_s ngx_pool_large_t;
//申请大内存段
struct ngx_pool_large_s {
ngx_pool_large_t *next;
void *alloc; /* 保存通过malloc返回的指针 */
};
//内存池元数据 用于把内存池节点使用链表方式关联起来
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; /* 申请空间大于max则表示需要申请大内存,只在创建内存池时赋值 */
ngx_pool_t *current; /* current用于加速遍历 */
ngx_chain_t *chain; /* 在http filter模块中使用 主要用于http response */
ngx_pool_large_t *large; /* 大内存 */
ngx_pool_cleanup_t *cleanup; /* 设置回调 ngx_pool_cleanup_add */
ngx_log_t *log;
};
2.2、内存池组织形态
2.3 特点
Nginx实现的内存池有如下特点:
- 为了满足大内存需求(一个内存池节点,实际可用内存大小是固定的max,当申请的内存大于max则认为是大内存),nginx设计一个独立链表(large)用于保存大内存块。大内存块采用malloc/free直接申请堆内存,可见大内存适用生命周期较短场景,否则会把内存耗尽。大内存头部ngx_pool_large_t内存是从当前内存池中申请。为什么这样设计呢?为了复用。
- 对于大内存,始终存放到内存池首节点中,对大内存头sizeof(ngx_pool_large_t)的内存空间一定是来自current所指向内存池节点。如上图所示,可参考后续源码分析中。
- 往往的应用层业务逻辑很复杂,为了方便通常在业务逻辑最后环节进行资源的回收,Nginx也考虑到此需求,所以在内存池中增加ngx_pool_cleanup_t结构。注意:虽然这里名称叫做pool_cleanup,业务层只需要设置好自己的回调函数即可。Nginx框架在释放内存池之前就会调用回调函数。至于回调函数内容就非常灵活,完全取决于当前业务逻辑。例如:关闭各种文件句柄,删除各种临时数据文件等。
- 数据结构 ngx_pool_data_t,是将内存池节点以链表形式进行关联即next,每次创建新内存节点挂在链表最后zuiho。其中end-last(减法)等于可用内存空间。
三、相关接口
下面介绍相关代码,毕竟只有看懂代码才能深入理解其中的内涵
/**
* 创建内存池
*/
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);//16字节对齐
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;
}
简要说明:
- 为了保证访问速度,采用16字节方式对齐。
- 如果申请的内存大小大于一个内存页大小(一般是4k),虽然能够申请成功,但是有内存浪费。因此在使用内存时需要注意大小。
/**
* 销毁内存池
*/
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);//执行回调函数
}
}
/*为了篇幅 删除debug调试信息 */
//释放大内存,由此可知所有的大内存均放在内存池首节点中
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;
}
}
}
/**
* 业务模块设置清理回调
*/
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;//设置null,由调用者在外部设置
c->next = p->cleanup;
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
}
那么如使用内存池呢?非常简单,直接调用ngx_palloc/ngx_pnalloc,流程图如下:
这里需要阐明一个观点,对于Nginx来说,所有申请的内存均来自内存池(除大内存),可以理解成万物皆池化。
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC) //默认不开启
if (size <= pool->max) {//检查待申请的内存是否大于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;
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);//分配新内存池节点
}
/**
* 向操作系统申请分配内存块
* 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);
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;/* size表示业务从内存池中申请的空间大小 */
/* 将每一个内存失败次数加1 如果失败次数大于4次 则修改current指针 提升遍历速度 */
for (p = pool->current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
pool->current = p->d.next;//始终条current指针
}
}
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;
/* 遍历large链表 遍历三次仍然没有找到合适位置 则创建一个新节点 */
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
if (n++ > 3) {
break;
}
}
/**
* 创建新节点然后在链表头插入 large头部信息 也是从池中分配
* 此处比较巧妙 体现万物皆池化的特点
*/
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;
}
四、内存池生命周期
在Nginx中有三种不同生命周期的内存池为:进程级、连接级、请求级。
级别 |
存活时长 |
说明 |
进程级 |
伴随整个进程,时间最长 |
ngx_cycle_t中内存池 |
连接级 |
伴随tcp会话,时间居中 |
ngx_connection_t中内存池 |
请求级 |
伴随http一次请求,时间最短 |
ngx_http_request_t中内存池 |
为什么会出现三种级别的内存池呢?仔细想想可知,对于Nginx万物皆池化,所有内存的申请必须通过内存池。
一个进程启动肯定需要一个(一些)用于保存全局数据。
Nginx是用于网络通信,自然需要维持tcp相关数据,例如:对于长连接http请求。
请求级,自然对应http请求,虽然http采用长连接方式,但是每一次http请求可能都不一样,自然需要为每个http请求分配一个内存池。
五、总结
Nginx实现的内存池比较简单易懂,我们在开发自己的应用程序,只要保证所有内存均来自内存池这唯一标准,那么就不会出现内存问题。万物皆池化!!下一篇,我们来看看ngx_buf_t。