文章目录
前言
由于业务的需要经常用nginx作为反向代理服务器,提供http的web服务、rtmp和hls的流媒体服务,虽然对于nginx的配置和使用了然于胸,但是对于nginx的实现原理却知之甚少,知其然却不知其所以然,作为一个码龄超过20年的c++程序员显然是有些说不过去的,从本期开始将会不定期的更新对于nginx源代码的解读文档,一个是对自己学习的总结,另外一个也希望能够给大家一些不一样的解读角度,让大家更加易于理解nginx的源代码
一、内存池基本概念
内存池(Memory Pool)是一种内存分配方式,又被称为固定大小区块规划(fixed-size-blocks allocation)。通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能,内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存,然后通过链表的方式将内存块串联起来统一管理,这样做的一个显著优点是,使得内存分配效率得到提升。
二、nginx数据类型监测
ngix本身提供了诸多的自定义类型,其命名结构规则为ngx(前缀)_类型关键字_t(指的是typedef),主要包括基础类型、高级类型、通信类型、组件类型等

- 基础类型的包括ngx_int_t、ngx_string_t、ngx_list_t、ngx_buf_t、ngx_table_elt_t、ngx_chain_t等等,
- 在基础类型的基础上形成了一些更加复杂的高级类型,包括ngx_queue_t、ngx_array_t、ngx_rbtree_t、ngx_radix_tree_t、ngx_hash_t等等.
- 由于nginx本身是多进程的协同运行的一种架构,因此还有一些进程通信之类的类型,包括ngx_shm_t、ngx_channel_t、ngx_single_t、ngx_shmtx_t、自旋锁、原子操作等等.
- 另外就是更加复杂的组件类型,包括ngx_pool_t、ngx_thread_pool_t、ngx_slab_t、ngx_connection_t。
三、nginx内存池相关结构体定义
nginx有一套完整的内存池定义、应用和操作流程,我们先来看一下nginx内存池的定义,主要包括内存池数据管理结构(ngx_pool_data_t)、内存池数据结构(ngx_pool_t)、大内存块数据结构(ngx_pool_large_t)、内存释放处理结构(ngx_pool_cleanup_t)等内容
1.内存池数据管理结构
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;
- 每次申请一次小内存调整一次last指针的值,调整后的值为last+最后一次申请的大小,且不成大于end.
- End为内存块的结束位置,如果新申请的内存大小加上last大于end,则需要申请新的内存块,并将next指向新内存块的起始位置。
- failed这个值有点迷惑性,开始理解成内存申请失败,实际上应该是由于剩余内存块的空间不够的情况下用于新申请内存的计数,下节会详细讲到。
- sizeof(ngx_pool_data_t)的大小在不同系统架构的表现是不一样的,32位系统下为16字节,64位系统下是32字节。
2.内存池数据结构
主要是负责对于内存块的整体管理,不管这个内存块是所谓的大块还是小块,同时利用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; //日志信息
};
- 首先明确ngx_pool_s和ngx_pool_t就是一个东西
- 结构体变量d为类型是内存池数据管理结构(ngx_pool_data_t)的成员变量,上面已经解释过不在赘述
- 无符号整形max为可用与使用的有效内存区域大小,加入申请的内存大小为1024,则max为1024-sizeof(ngx_pool_t),在32系统下,sizeof(ngx_pool_t)的大小为40字节,其中16字节为sizeof(ngx_pool_data_t)的大小,ngx_pool_data_t包含4个变量,加上ngx_pool_t自身的6个,正好10个变量,每个4字节,10个正好40个字节,则32位情况下max=1024-40=884,而64位情况下sizeof(ngx_pool_t)则占用80和字节,则64位情况下max=1024-40=844。
- 指针current在内存块小于6的情况下指向的都是自己的起始位置,如果超过6个内存块,比如已有n个内存块,则current指向第n-6个内存块的起始位置,这么做的目的是为了在查找内存变量的时候不需要从头进行遍历,提升查找效率。至于为什么是6下面会详细解释。
- 指针chain可以理解为临时的缓存去,实际操作的时候正常情况下其应该是指向一个buff缓冲区。
- 指针large指向的内容为大于max值的不规则大小的内存链表,有别于大内存块存在的标准内存块即max标记的内存大小,有效区域为结构体变量d的last和end指针指向的内存地址区域之间即d.end-d.last。
- 指针cleanup指向释放内存池的callback操作函数。
3.大内存块数据结构
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本身也比较简单,就是一个标准链表,指向下一个节点的指针和本节点的申请的内存区域起始地址指针.
4.内存释放处理结构
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;
};
四、内存池的操作
内存池的操作主要包括创建、使用、重置、释放几个方面,主要的接口函数如下
1.内存池创建
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_memalign实际申请内存区域,申请的内存区域采用NGX_POOL_ALIGNMENT的方式进行对齐,NGX_POOL_ALIGNMENT必须为2的幂次方,在32位系统中是以8字节为边界对齐,在64位系统是以16字节为边界对齐的,nginx默认的NGX_POOL_ALIGNMENT为16,ngx_memalign还会进一步调用posix_memalign函数进行内存申请,posix_memalign的返回的异常结果如果是EINVAL则表示NGX_POOL_ALIGNMENT不是2的幂次方,如果是ENOMEM表示内存不足。如果系统不支持posix_memalign,系统会调用memalign申请内存。
- 申请成功之后将新申请的内存与ngx_pool_t指针进行关联并初始化ngx_pool_t对象,ngx_pool_t会占用新申请内存区域的最前部40字节(32位)或80字节(64位)存储空间,之后便是实际可用的内存区域,以64位系统为例,以1024字节大小为内存块创建内存池对象并初始化后的内存快照如下图:

关于NGX_MAX_ALLOC_FROM_POOL再啰嗦两句,nginx源码对其的定义是#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1),其中ngx_pagesize此处指的是内存页面大小,在x86(32/64位)架构下,内存页面的大小为4096字节也就是4K,这么做的目的是申请的内存块大小不要超过4096字节,这样在进行申请内存集中在一个内存页面内的好处是内存访问的时候提升内存cache的命中率
2.内存池的使用
如果需要使用的内存小于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);
}
3.小内存块的申请
//小内存块申请
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个内存块的内存快照如下图所示:

4.大内存块的申请
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;
}
5.内存池的释放
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;
}
}
}
五、代码示例
1.开发环境
所有的nginx源代码的分析都是基于nginx 1.12.1版本,基于virtualbox6.1部署的64位的ubuntu16.1的系统,为了避免权限的影响,采用root账户进行全部操作,基本和以root的方式登录centos也没啥区别了。开发工具采用的是vscode,安装remote ssh插件,利用方式远程进行编辑、编译、调试方便多多
2.代码改造
在文件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文件才行
3.测试demo
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}
4.编译命令
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
5.运行效果


580

被折叠的 条评论
为什么被折叠?



