3

源代码路径

版本:1.8.0

src\core\Ngx_palloc.h
src\core\Ngx_palloc.c

主要作用分析

提供了一种机制,帮助进行资源管理(内存、文件)。可以类比C++中的RAII机制。

以内存管理为例,通常是手工进行malloc/free,这种做法的优点是灵活、高效,缺点是容易出错,造成内存泄漏以及各种莫名其妙的BUG。

因此,在C/C++中正确的管理内存一直是最棘手的问题。

C++中提供了RAII机制和智能指针来解决这个问题。Nginx采用C语言开发,实现了一套用来管理资源的机制。这就是nginx pool

数据结构

ngx_pool_data_t

typedef struct {
    u_char               *last;
    u_char               *end;
    ngx_pool_t           *next;
    ngx_uint_t            failed;
} ngx_pool_data_t;

首先看一个示意图:
图片描述

last指针表示ngx_pool_data_t所管理的内存block中已使用的内存的末尾地址,这也是下次分配的起始地址。

end指针表示ngx_pool_data_t所管理的内存block的尾地址,该指针在分配此block时确定,用来标记last的边界值。

next用来指向下一个ngx_pool_data_t的内存block地址,用于形成block链表。
failed用来标记该block使用时分配失败次数。

ngx_pool_data_t的管理和使用

围绕核心数据结构ngx_pool_data_t, Nginx是如何管理和使用的呢?

本篇主要从以下几个方面说明:
1) 如何初始化ngx_pool_data_t链表;
2) 如何向链表增加ngx_pool_data_t节点。
3) 如何回收/销毁ngx_pool_data_t节点及链表

初始化ngx_pool_data_t链表

基本思路:

  1. 所谓初始化ngx_pool_data_t链表就是申请一段内存block,然后将该block指针void* p转成ngx_pool_data_t*类型的指针,这样,就可以利用ngx_pool_data_t*指针来管理这段block,而这个block就是ngx_pool_data_t链表的节点
  2. 当然要能够正确管理block还需要初始化ngx_pool_data_t的成员变量lastendnextfailed。这样,通过ngx_pool_data_t指针,可以获取管理ngx_pool_data_t链表节点的能力
  3. 但是现在我们还没有管理整个ngx_pool_data_t链表的能力,那么如何做呢?通过在内存block的起始部分添加lastendnextfailed等信息可以管理一段内存。相应地,通过在ngx_pool_data_t链表第一个节点添加管理链表的信息,就可以管理整个链表。

同时,由于链表第一个节点只是一个特殊的节点所以,负责管理节点的结构体ngx_pool_data_t应该是的负责管理链表结构体节点的子集

Nginx源码中这个管理链表的结构体就是:ngx_pool_s,其结构体定义为:

struct ngx_pool_s {
    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;
};

其中,dngx_pool_data_t用来管理节点本身,而其他变量则用来管理链表。

  • current用来指示链表中当前正在使用的节点;
  • chain用来在节点上挂接filter链表;
  • cleanup用来注册资源回收handler等;
  • large是与节点大内存小内存有关,这里暂不分析。

通过以上分析,可知,ngx_pool_s是管理整个ngx_pool_data_t链表,同时也能够管理所在的链表节点。

Nginx源码里,申请的block指针为void*统一转成ngx_pool_s*,这样,在链表第一个节点,通过ngx_pool_s*管理整个链表及节点本身,在其他节点,通过ngx_pool_s*管理节点本身。

根据以上思路,可以很容易明白Nginx源码里关于创建ngx_pool_data_t链表的代码

函数声明: Ngx_palloc.h

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);

说明:

输入要分配的ngx_pool_t节点block大小size,返回一个ngx_pool_t*的指针。该指针既用来管理链表,也用来管理节点block本身。
因此,后续链表的插入,删除都是通过操作该指针来完成,而使用链表第一个节点的block也是通过该指针来完成。

函数定义: Ngx_palloc.c

ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
    ngx_pool_t  *p;

    // 申请内存block,为提高效率,进行了对齐
    // 转成ngx_pool_t*指针
    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);  
    if (p == NULL) {
        return NULL;
    }

    // 初始化ngx_pool_data_t用来管理节点block本身
    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;

    // 为提高效率能使用的block最大值为NGX_MAX_ALLOC_FROM_POOL
    // 输入size最小值为sizeof(ngx_pool_t),输入小于该值会造成nginx崩溃
    size = size - sizeof(ngx_pool_t);
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    // 初始时,链表current指向自身
    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p;
}

向链表增加ngx_pool_data_t节点

基本思路:

上一小节中关于如何初始化ngx_pool_data_t链表的思路,可以基本套用到增加ngx_pool_data_t节点中来,只是在结构体ngx_pool_t成员变量初始化上有所区别。

直接上源码:

static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    // 输入变量pool用来表示整个链表
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new;

    // 计算链表第一个节点的block大小
    psize = (size_t) (pool->d.end - (u_char *) pool);
    // 申请与链表第一个节点大小相同的内存block
    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    if (m == NULL) {
        return NULL;
    }
    // 转为ngx_pool_t*类型
    new = (ngx_pool_t *) m;
    // 初始化节点管理结构体ngx_pool_data_t
    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;
    // 为了效率,对ngx_pool_data_t的last变量进行对齐操作
    m += sizeof(ngx_pool_data_t);
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    new->d.last = m + size;
    // 将第一个链表节点用于操作链表的pool指针的current变量指向当前节点
    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;
}

当然,在使用时,Nginx并不是直接使用ngx_palloc_block来操作链表,而是使用ngx_palloc
原因在于,使用ngx_create_poolngx_palloc_block构造链表之后,我们得到的是多个连续的内存块组成的列表,这些内存块大小相同,其大小为调用ngx_create_pool传入的size值或NGX_MAX_ALLOC_FROM_POOL

而在实际使用时malloc的大小不一定等于这些内存块的大小,所以,需要提供一个接口,能够从ngx_pool_t链表中获取需要大小的内存。

这个函数就是ngx_palloc

基本思路:

如果要获取的内存大小大于ngx_pool_t节点的大小,那么属于创建大内存,调用ngx_palloc_large(pool, size)。这里我们暂不分析大内存的问题。

如果要获取的内存小于ngx_pool_t节点的大小,那么尝试从当前节点分配。

这里分两种情况:

  1. 当前节点剩余空间足够的情况,直接分配;
  2. 当前节点剩余空间不足的情况,创建一个新节点,并将新节点分配

P.S 这里的分配就是返回指针

函数声明:

void *ngx_palloc(ngx_pool_t *pool, size_t size);

函数定义:

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    ngx_pool_t  *p;
    // 获取内存小于链表节点内存的情况
    if (size <= pool->max) {

        p = pool->current;
        // 尝试从当前节点分配内存
        do {
            m = ngx_align_ptr(p->d.last, 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);
    }
    // 获取内存小于链表节点内存的情况
    return ngx_palloc_large(pool, size);
}

今天先到这里,后续再补充或另起一篇写。


_Zhao
1.2k 声望82 粉丝