C++:06.Nginx内存池

 先了解一下Nginx:

Nginx是一个高性能的HTTP反向代理服务器,接收浏览器请求。其特点是占有内存少,并发能力强,稳定性高。

nginx有什么功能?

接收http服务,正向反向代理(负载均衡)。

正向代理代理客户端,反向代理代理服务器,反向代理也称作负载均衡器

http协议本身就是一个短链接,无状态的协议

内存池的好处:

减少向系统申请和释放内存的时间开销,解决内存频繁分配产生的碎片,提示程序性能。

nginx有小块内存和大块内存的概念,小块内存nginx在分配的时候会尝试在当前的内存池节点中分配,而大块内存会调用系统函数malloc向操作系统申请

区分小块内存和大块内存的原因有2个:

1、针对大块内存  如果它的生命周期远远短于所属的内存池,那么提供一个单独的释放函数是十分有意义的,但不区分大块内存和小块内存,针对大的内存块便会无法提前释放了

2、大块内存与小块内存的界限是一页内存4K(下文有体到),大于一页的内存在物理上不一定是连续的,所以如果分配的内存大于一页的话,从内存池中使用,和向操作系统重新申请效率差不多是等价的。

参考博文:https://www.cnblogs.com/magicsoar/p/6040238.html


主要数据结构:

ngx_pool_data_t(内存池数据块结构)

typedef struct   包含了所需要操作这个内存池数据的一些指针。
{
    u_char               *last;    unsigned char类型指针,保存当前数据区的已经使用的数据的结尾。
    u_char               *end;     表示当前的内存池的结尾。end-last就是内存池未使用的大小。
    ngx_pool_t           *next;    指向下一块内存池。
    ngx_uint_t            failed;  标记请求小块内存由于内存池空间不够,而需要重新分配一个小内存池的次数。
} ngx_pool_data_t;

ngx_pool_t(内存池头部结构)

struct ngx_pool_t
{
    ngx_pool_data_t       d;        这就是上面内玩意
    size_t                max;      内存池所能容纳的最大值
    ngx_pool_t           *current;  指向当前的内存池的头
    ngx_chain_t          *chain;    将所有内存池都链接起来
    ngx_pool_large_t     *large;    链接大块内存池
    ngx_pool_cleanup_t   *cleanup;  清理函数列表
    ngx_log_t            *log;      日志
};

 ngx_pool_large_s(大块内存池类型)

struct ngx_pool_large_s 
{
    ngx_pool_large_t *next;  指向下一大块内存
    void *alloc;             本块内存
};

ngx_pool_cleanup_s(内存池中的数据清理)

struct ngx_pool_cleanup_s 
{
    ngx_pool_cleanup_pt  handler;   清理函数
    void                *data;      传给清理函数的数据
    ngx_pool_cleanup_t  *next;      指向下一个清理函数  调用destroy时会遍历清理函数链表,调用handler
}

 主要函数:

1、ngx_create_pool()  内存池创建

宏定义的函数,就是对malloc进行简单封装。里面写入了错误日志
#define ngx_memalign(alignment,size,log) ngx_alloc(size,log) 
内存池的数据区的最大容量。在x86上ngx_pagesize=4096。
因为寻址页面大小为4096,是一个相对合适的数字,保证size不会太大,导致寻址不到浪费空间
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)

ngx_pool_t *  函数返回值类型
ngx_create_pool(size_t size, ngx_log_t *log) 传进来的size并不是真正能使用的大小,应减去sizeof(ngx_poll_t)。
{
    ngx_pool_t *p;
   
    p = ngx_memalign(NGX_POOL_ALIGNMENT,size,log)  用宏定义了一个函数,第一个参数宏定义的16,用来内存对齐
    if (p == NULL) 
    {
        return NULL;
    }

    内存已经申请了,对结构体指针初始化。
    由于一开始数据区为空,因此last指向数据区的开始。
    p->d.last = (u_char *) p + sizeof(ngx_pool_t);  可以证明上述实际使用空间并没有size
    end也就是数据区的结束位置
    p->d.end = (u_char *) p + size;
    p->d.next = NULL;
    p->d.failed = 0;
    
    这里才是我们真正能使用的大小。
    size = size - sizeof(ngx_pool_t);
    
    然后设置max。内存池的最大值也就是size和最大容量之间的最小值。
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
    
    current表示当前的内存池。
    p->current = p;
    
    其他的域置NULL。
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;
    返回指针。
    return p;
}

为什么要考虑内存对齐?

因为内存对齐可以降低CPU读取内存的次数,提高性能。主要为减少内存的I/O操作。 


2、内存申请:

ngx_palloc()    分配的内存会对齐

ngx_calloc()    用来分配一块清0的内存

ngx_pnalloc()  分配的内存不会对齐

就对齐的计算,不必深究
#define ngx_align_ptr(p,a) (u_char *)(((uintptr_t)(p) + ((uintptr_t)a - 1)) &~((uintptr_t)a - 1))
#define NGX_ALIGNMENT sizeof(unsigned long)

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
    u_char *m;
    ngx_pool_t *p;
    
    //判断当前申请的大小是否超过max,如果超过则说明是申请大块内存,此时进入large
    if (size <= pool->max)  如果申请小块内存,进入
    {
        得到当前的内存池指针。
        p = pool->current;
        开始遍历内存池
        do {
            首先对齐last指针。调用宏对内存地址进行对齐处理。
            如果是ngx_pnalloc(),就省去这一步。
            m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);  

            得到当前内存池中的可用大小。如果剩下的空间不够,则直接返回当前的last,也就是数据的指针。
            if ((size_t) (p->d.end - m) >= size)  如果当前够存,进入
            {
                偏移last,然后返回前面保存的last。
                p->d.last = m + size;
                return m;
            }

            否则继续遍历
            p = p->d.next;
        } while (p);

        到达这里说明内存池已经满掉,因此我们需要重新分配一个小块内存然后链接到当前的data的next上。    
        return ngx_palloc_block(pool, size);
    }
    申请大块。
    return ngx_palloc_large(pool, size);
}

ngx_palloc_block()  申请小块内存

static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char *m;
    size_t psize;
    ngx_pool_t *p, *new, *current;

    计算当前的内存池的大小。
    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 = (ngx_pool_t *) m;
    接下来和我们create一个内存池做的操作一样。就是更新一些指针
    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;

    让m指向该块内存ngx_pool_data_t结构体之后数据区的起始位置
    m += sizeof(ngx_pool_data_t);
    m = ngx_align_ptr(m, NGX_ALIGNMENT);  对齐指针,前面说了
    new->d.last = m + size;  设置last指针位置
   
    设置current。
    current = pool->current;
    
    这里遍历所有的子内存池,这里主要是通过failed来标记重新分配子内存池的次数,然后找出最后一个大于4的,标记它
    for (p = current; p->d.next; p = p->d.next) 
    {
        if (p->d.failed++ > 4) 
        {
            当failed大于4说明我们至少请求了4次内存分配,都不能满足我们的请求,
            此时我们就假设老的内存都已经没有空间了,因此我们就从比较新的内存块开始。
            current = p->d.next;  
        }
    }

    链接到最后一个内存池后面
    p->d.next = new;

    如果current为空,则current就为new。
    pool->current = current ? current : 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_memalign(NGX_POOL_ALIGNMENT,size,pool->log)  上面提了
    if (p == NULL) 
    {
        return NULL;
    }
    n = 0;

    开始遍历large链表,如果有alloc(也就是内存区指针)为空,则直接指针赋值然后返回。一般第一次请求大块内存都会
    for (large = pool->large; large; large = large->next) 
    {
        if (large->alloc == NULL) 
        {
            large->alloc = p;
            return p;
        }
        链表不能超过4个,只对前3个内存块进行检查,否则就直接分配内存。
        if (n++ > 3) 
        {
            break;
        }
    }

    malloc一大内存块。
    large = ngx_palloc(pool, sizeof(ngx_pool_large_t));  调用本身
    if (large == NULL) 
    {
        ngx_free(p);  从pool的large链表中找到p,然后free掉。
        return NULL;
    }

    然后链接数据区指针p到large。
    large->alloc = p;
    large->next = pool->large;
    pool->large = large;
    return p;
}

3、ngx_destroy_pool()  内存池的销毁

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);  说结构体的时候就提到了这个函数
        }
    }

    free大块内存   ngx_palloc_large申请的内容
    for (l = pool->large; l; l = l->next) 
    {
        ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
        if (l->alloc) 
        {
            ngx_free(l->alloc);
        }
    }
    
    遍历小块内存池。  ngx_create_pool申请的内存
    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) 
    {
        直接free掉。
        ngx_free(p);
        if (n == NULL) 
        {
            break;
        }
    }
}

补充:

1、在nginx内存池中只有大块内存提供了free接口【ngx_pfree(ngx_pool_t *pool, void *p),这个函数就是从pool的large链表中找到p,然后free掉它】,可以回收释放。

小块数据是不释放的,只有当整个内存池被destroy掉才会被释放,这样就简化了内存池的操作。为什么不释放?原因:http服务器是间歇性处理,是一个短连接,无状态的协议。处理完请求,服务器断开。在处理完http请求完成之后,就可以发起内存池的重置函数。

2、ngx_pool_cleanup_t *  ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
这个函数也就是添加一个ngx_pool_cleanup_t到当前的pool上,然后返回,我们此时就能通过返回的结构来给对应的handler赋值。当内存池destroy的时候我们就能添加这些清理函数到pool中,然后当内存池释放的时候就会自动调用。


4、ngx_pfree()  内存池清理

ngx_int _t
ngx_pfree(ngx_pool_t *pool,void *p)
{
    ngx_pool_large_s *l;
    释放大块内存
    for(l=pool->large ;l;l=l->next )
    {
        if(p==l->alloc)
        {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC,pool->log,0,"free:%p",l->alloc);              
            free(l->alloc );
            l->alloc =NULL;
            return NGX_OK;
        }
    }
    return NGX_DECLINED;
}

注:因为大块内存只对前三个进行检查,否则就直接分配,所以大块内存必须及时释放。


 5、ngx_reset_pool()  内存池重置

void
ngx_reset_pool(ngx_pool_t *pool)
{
    ngx_pool_large_s *l;
    ngx_pool_s *p;
    
    释放所有大块内存
    for(l=pool->large ;l;l=l->next )
    {
        if(l->alloc )
        {
            free(l->alloc );
            l->alloc =NULL;
        }
    }
    pool->large = NULL;

    重置所有小块内存
    for(p=pool;p;p=p->d.next )
    {
        p->d .last =(u_char *)p+sizeof(ngx_pool_t);
        p->d .failed =0;
    }
    pool->current =pool;
}

在处理完一批http请求完成之后就可以发起内存池的重置函数

猜你喜欢

转载自blog.csdn.net/qq_41214278/article/details/83747002