Nginx源码阅读:ngx_palloc 内存池

  • 一、内存池
  • 二、大块
  • 三、chunck(小块)
  • 四、nginx内存池的结构图
  • 五、源码阅读
  • 1、`ngx_create_pool`
  • 2、`ngx_destroy_pool`
  • 3、`ngx_reset_pool`
  • 4、`ngx_palloc`
  • 5、`ngx_pnalloc`
  • 6、`ngx_palloc_small`
  • 7、`ngx_palloc_block`
  • 8、`ngx_palloc_large`
  • 9、`ngx_pmemalign`
  • 10、`ngx_pfree`
  • 11、`ngx_pcalloc`


一、内存池

内存池主要是为了解决内存碎片的问题,如果有大量客户端连接,并且每次只占用一点内存,会出现很多的内存碎片。

nginx中为了更好地管理内存,分为大块和小块(chunk),大块用于存储大的内存,小块用于存储比较小的内存。

应用:
nginx中一个tcp连接来了之后,处理该连接的所有数据,内存都由内存池来管理。

二、大块

nginx中大块的结构

typedef struct ngx_pool_large_s  ngx_pool_large_t;
//大块
struct ngx_pool_large_s {
    ngx_pool_large_t     *next;
    void                 *alloc;
};

大块的存储结构图

通过链表将节点串起来

nginx Swap nginx swap内存_nginx Swap

三、chunck(小块)

//ngx_pool_data_t可以理解为是嵌入到ngx_pool_s里面的
typedef struct {
    u_char               *last;//内存块中last指向还没有用过的内存的头部(比如要分配5个字节,那么从last开始分配)
    u_char               *end;//当前内存块中末尾
    ngx_pool_t           *next;//下一块chunck
    ngx_uint_t            failed;
} ngx_pool_data_t;

//内存池
struct ngx_pool_s {
    ngx_pool_data_t       d;//存储一些chunck块的指针信息
    size_t                max;//chunk块的大小
    ngx_pool_t           *current;//当前pool(chunck)查找空余chunck的起始遍历的地址
    ngx_chain_t          *chain;//用于存储buffer的链结构(在内存池中并没有使用)
    ngx_pool_large_t     *large;//大块
    ngx_pool_cleanup_t   *cleanup;//用于清理小块内存
    ngx_log_t            *log;//日志相关
};

只有第一个chunck块(x小块),才会指向大块。后面几个结构中的large都是没有用的。

四、nginx内存池的结构图

nginx Swap nginx swap内存_nginx_02

五、源码阅读

1、ngx_create_pool

创建内存池

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);//分配一块根据size大小,并以16字节对齐(是要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;//不超过(uintptr_t)-1的大小,也就是uintptr_t类型的最大值

    p->current = p;//指向当前的内存池
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p;
}

2、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) {//遍历chunck,并调用当前chunck的清理chunck内存的回调函数
        if (c->handler) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "run cleanup: %p", c);
            c->handler(c->data);
        }
    }

    //清理大块
    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }
    //释放每个chunck的头部
    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
        ngx_free(p);

        if (n == NULL) {
            break;
        }
    }
}

3、ngx_reset_pool

重置内存池
1.释放所有大块
2.将所有chunk中的last指针重置,也就是放在头部的后面。不需要清空chunck里面的数据,使用的时候直接覆盖就行了

void
ngx_reset_pool(ngx_pool_t *pool)
{
    ngx_pool_t        *p;
    ngx_pool_large_t  *l;

    //释放所有的大块内存(但是,大块的链表节点依然还在,因为它存储在chunck里面)
    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }
    //将chunck中last指针初始化,chunck内存中的数据不需要清理,因为,可以直接覆盖
    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;
    pool->chain = NULL;
    pool->large = NULL;
}

4、ngx_palloc

从内存池中取一块来进行分配。
如果小于chunck可以分配的最大大小,那么就分配到chunck中(小块)
如果大于chunck可以分配的最大大小,那么就分配到大块中

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)//如果不是调试版本
    if (size <= pool->max) {//如果分配的内存小于 小块能存放的最大大小,那么就分配小块,否则就分配大块
        return ngx_palloc_small(pool, size, 1);
    }
#endif

    return ngx_palloc_large(pool, size);
}

5、ngx_pnalloc

和ngx_palloc类似,只是,不采用内存对齐

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);
}

6、ngx_palloc_small

在chunck(小块)中分配一块内存
遍历chunck链,如果找到一块可以存放的空间,就返回可分配的内存空间的起始地址
如果找不到,就创建一个新的chunck

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) {//如果使用内存对齐,那么下面就执行对齐。比如按照4个字节对齐,内存已经使用了14字节,那么都当作已经用完了16个字节,从这后面再开始使用
            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;//如果当前chunck没有内存可以供当前大小内存分配,就查找下一个chunck

    } while (p);

    return ngx_palloc_block(pool, size);//遍历完所有chunck都没有可以分配的,那么就新创建一个chunck
}

7、ngx_palloc_block

创建一块新的chunk内存空间,也就是在ngx_palloc_small的chunck链中没找到可以分配的空间,那么就创建一块。

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);//分配一块psize大小的内存
    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;
    //通过遍历,来获得链表尾部
    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {//如果failed > 4, 就漏过这个chunck(也就是说每个块,在查找块的时候只允许失败4次,失败4次后,让current指向该节点的下一个)
            pool->current = p->d.next;
        }
    }
    //链表尾部用来存新的chunck
    p->d.next = new;

    return m;
}

8、ngx_palloc_large

分配一个大块节点(节点存到(chunck)小块里面),通过链表的方式,串起来。
然后节点上的指针,指向大块,通过这种方式,将大块组织起来

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) {//如果查找3次还没有找到空的节点,那么就直接break,然后采用头插法
            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;
}

9、ngx_pmemalign

可以自定义对齐字节大小,创建一块大块内存

void *
ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)
{
    void              *p;
    ngx_pool_large_t  *large;

    p = ngx_memalign(alignment, size, pool->log);
    if (p == NULL) {
        return NULL;
    }

    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;
}

10、ngx_pfree

释放内存池,小块不会被释放,只有大块释放了

ngx_int_t
ngx_pfree(ngx_pool_t *pool, void *p)
{
    ngx_pool_large_t  *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);
            ngx_free(l->alloc);
            l->alloc = NULL;

            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}

大块可以通过ngx_pfree来释放内存,那么小块呢?
一个tcp连接,就会创建内存池,小块在tcp断开连接时候,才会释放,就是ngx_destory_pool的时候

11、ngx_pcalloc

从内存池中分配一块内存,并初始化为0

//分配内存,并初始化为0
void *
ngx_pcalloc(ngx_pool_t *pool, size_t size)
{
    void *p;

    p = ngx_palloc(pool, size);
    if (p) {
        ngx_memzero(p, size);
    }

    return p;
}