nginx部分数据结构

阅读nginx源码,先从基础组件着手,这里从最基本的,数据结构入手。

ngx_int_t

使用的是intptr_t,在32位系统中是4B,在64位系统中是8B,做到了支持跨平台。

ngx_str_t

这是nginx中字符串的数据结构。

typedef struct {
    size_t      len;
    u_char     *data;
} ngx_str_t;

使用char *,主要有两个缺点,一是不带len字段,容易溢出;二是内存分配的问题,操作之前需要先malloc。改进的方法,是加入长度字段,同时引入内存池,这样的话有两种做法,一是使用柔性数组。

struct {
    size_t      len;
    u_char     data[0];
};

这样做的缺点是灵活性较差。字符串数组的长度不可改变,尤其是不能加长,因为数组后面的空间可能已经被使用了。
nginx使用的是第二种做法。这样做有几个好处,一是字符串与结构体可以在空间上不连续;二是方便扩展;三是在重复引用一段字符串内存时,可以只用指针进行引用,减少了很多不必要的内存分配与拷贝。正是基于此特性,在nginx中,必须谨慎的去修改一个字符串。
注意宏函数

#define ngx_string(str)     { sizeof(str) - 1, (u_char *) str }

参数str是指针,因此要么传一个常量,要么传一个内存池(堆)中的string。

ngx_list_t

这是nginx中链表的数据结构。链表本身是会产生内存碎片的,但是因为nginx的链表是分配在内存池中(nginx中万物皆可池化),是一整块内存。

typedef struct ngx_list_part_s ngx_list_part_t;  
   
struct ngx_list_part_s {  //链表每个节点的结构
    void             *elts;   //指向该节点的数据区(该数据区中可存放nalloc个大小为size的元素)  
    ngx_uint_t        nelts;  //已存放的元素个数  
    ngx_list_part_t  *next;   //指向下一个链表节点  
};  
   
typedef struct{              //链表头结构  
    ngx_list_part_t  *last;   //指向链表最后一个节点(part)  
    ngx_list_part_t   part;   //链表头中包含的第一个节点(part)  
    size_t            size;   //每个元素大小  
    ngx_uint_t        nalloc; //链表所含空间个数,即实际分配的小空间的个数  
    ngx_pool_t       *pool;   //内存池  
}ngx_list_t;

正常情况下,对于一个数据结构,应该提供增删改查等操作方法,但是nginx的链表只有增。
增:注意nginx链表增的做法,和以往使用链表方式不太一样,一句话总结,就是push当new用。

/* 注意使用方法
 * 入参:传入一个链表结构,然后计算该链表节点数目以及可写入地址,返回可用起始地址。
 * 具体赋值操作,在函数返回后,在调用的地方进行复制。
*/
void *
ngx_list_push(ngx_list_t *l)
{
    void             *elt;
    ngx_list_part_t  *last;
 
    last = l->last;
    //最后一个节点没有可用空间则进行重新分配
    if (last->nelts == l->nalloc) {//表示当前数组中可用空间为0,需要重新分配
 
        /* the last part is full, allocate a new list part */
 
        last = ngx_palloc(l->pool, sizeof(ngx_list_part_t));
        if (last == NULL) {
            return NULL;
        }
 
        last->elts = ngx_palloc(l->pool, l->nalloc * l->size);
        if (last->elts == NULL) {
            return NULL;
        }
 
        last->nelts = 0;
        last->next = NULL;
 
        l->last->next = last;
        l->last = last;
    }
    //偏移指针 返回可用空间
    elt = (char *) last->elts + l->size * last->nelts;
    last->nelts++;
 
    return elt;
}

删:删除节点没有必要,到时候直接删除整个内存池就可以。nginx会在连接建立时创建内存池,连接释放时销毁内存池。
改:nginx不会对接收到的数据进行修改。另外,即便要修改,查找的效率也极低。
查:查找没有意义,因为只能遍历,效率极低。
当然,还可以在测试时自己写个函数遍历打印链表数据进行检查,但是在实际生产过程中要去掉。

ngx_table_elt_t

主要用于存储http头的数据。

typedef struct {
    ngx_uint_t hash;
    ngx_str_t key;
    ngx_str_t value;
    u_char *lowcase_key;
} ngx_table_elt_t;

key存储头部名称,value存储对应的值,lowcase_key用于忽略http头部名称的大小写,hash用于快速检索到头部。

ngx_buf_t

主要用于数据文件同步时。

struct ngx_buf_s {
//只截取部分源码
    u_char          *pos;  //待处理数据的起始位置
    u_char          *last;  //待处理数据的结束位置

    off_t            file_pos;  //文件中待处理数据的起始位置,用偏移量表示
    off_t            file_last;  //文件中待处理数据的结束位置,用偏移量表示

//整个buf的起始位置
    u_char          *start;         /* start of buffer */
//整个buf的结束位置
    u_char          *end;           /* end of buffer */
    ngx_buf_tag_t    tag;
    ngx_file_t      *file;
    ngx_buf_t       *shadow;
/*
略
*/

一个buf对应一个文件,但有时候只定义一个buf是无法满足业务需求,这时候需要就需要把多个buf关联起来。nginx中通过ngx_chain_t通过单链表方式将其组织起来,定义如下

struct ngx_chain_s {
    ngx_buf_t    *buf;
    ngx_chain_t  *next;
};

ngx_shm_t

nginx中共享内存的数据结构。
共享内存一般有两种做法,mmap和shmget。对于mmap,nginx中有两种实现方法,一是使用匿名映射,二是映射到/dev/zero中。

ngx_array_t

存储多个数据的时候,很容易想到数组,但是使用数组有个前提,需要固定好数组的长度,那么超出了长度怎么办?nginx中用到了动态数组。nginx中的动态数组与链表做法较为相似。
nginx动态数组提供的方法主要有create、push、push_n。这里介绍一下,没有pop方法,这和链表没有delete是一样的,因为数组在内存池中。

扩容

nginx动态扩容有两种情况。
1、数组所在内存块还能再分配一个元素。就直接在数组后面再分配一个元素的空间,这就要求这个内存块在分配数组之后,不要再分配其他空间。
2、该内存块不能再分配一个元素。重新分配一个内存块,并直接分配原数组两倍元素数量大小的空间。

void *
ngx_array_push(ngx_array_t *a)
{
    void        *elt, *new;
    size_t       size;
    ngx_pool_t  *p;
 
    if (a->nelts == a->nalloc) {//数组已满
        size = a->size * a->nalloc;
        p = a->pool;
 
        if ((u_char *) a->elts + size == p->d.last
            && p->d.last + a->size <= p->d.end)//如果p的剩余空间>=一个数组元素的空间,就分配一个空间给数组
        {
            p->d.last += a->size;//调整pool的last,即修改下一次可分配空间的其实地址
            a->nalloc++;
 
        } else {
            new = ngx_palloc(p, 2 * size);//申请新的空间,大小是原来的2倍,假如pool的内存不足够分配一个新的数组元素
            if (new == NULL) {
                return NULL;
            }
 
            ngx_memcpy(new, a->elts, size);//把原有的元素拷贝到新分配的内存区
            a->elts = new;//修改数组数据区的地址,使其指向新分配的内存区
            a->nalloc *= 2;//修改数组可容纳的元素个数,是原来容纳元素的2倍
        }
    }
 
    elt = (u_char *) a->elts + a->size * a->nelts;//新增加元素的地址
    a->nelts++;//数组中元素的个数加1
 
    return elt;//返回新增加元素的地址
}

同理,push_n方法的扩容原则类似。注意空间不够的情况中,分配新数组的空间,在原数组元素数较少时,是分配2*n个元素的空间。

void *
ngx_array_push_n(ngx_array_t *a, ngx_uint_t n)
{
    void        *elt, *new;
    size_t       size;
    ngx_uint_t   nalloc;
    ngx_pool_t  *p;
 
    size = n * a->size;
 
    if (a->nelts + n > a->nalloc) {//数组已满
 
        p = a->pool;
 
        if ((u_char *) a->elts + a->size * a->nalloc == p->d.last
            && p->d.last + size <= p->d.end)//如果pool剩余的内存能够容纳这n个元素,就不用重新分配内存
        {
            p->d.last += size;//修改last使其指向可分配内存的起始地址
            a->nalloc += n;//数组容纳元素个数+n
 
        } else {//如果pool剩余的内存不能够容纳这n个元素,就重新分配内存
 
            nalloc = 2 * ((n >= a->nalloc) ? n : a->nalloc);//申请2倍的内存
 
            new = ngx_palloc(p, nalloc * a->size);
            if (new == NULL) {
                return NULL;
            }
 
            ngx_memcpy(new, a->elts, a->nelts * a->size);//把原有的元素拷贝到新申请的内存中
            a->elts = new;//修改数组元素区的地址
            a->nalloc = nalloc;//修改数组能够容纳的元素个数
        }
    }
 
    elt = (u_char *) a->elts + a->size * a->nelts;//新增元素的首地址
    a->nelts += n;//已存储元素个数+n
 
    return elt;
}

应用

动态数组,主要用在不确定个数的数据存储上。
比如,http状态中对于模块的注册,conf中http块中的server、location,以及conf文件中每个字段所带的参数。

ngx_mempool_t

nginx内存池的实现较为复杂,分为大块和小块,各种内存块各自是以链表的形式组织的。

typedef struct ngx_pool_large_s  ngx_pool_large_t;

/**
 * @brief Nginx 大块
 * 
 */
struct ngx_pool_large_s {
    ngx_pool_large_t     *next;
    void                 *alloc;
};

/**
 * @brief ngx 小块
 */
typedef struct {
    u_char               *last;      /**< 可以使用的内存的开始位置*/
    u_char               *end;        /**小块内存的最大位置*/
    ngx_pool_t           *next;       /**< 指向下一块内存小块*/
    ngx_uint_t            failed;
} ngx_pool_data_t;

/**
 * @brief Nginx 内存池是管理一个大块的集合和小块的集合
 * 
 */
struct ngx_pool_s {
    ngx_pool_data_t       d;         /**ngix 内存池的数据小块内存*/
    size_t                max;       /**< nginx pool 的大小 */
    ngx_pool_t           *current;   /**< nginx pool 当前位置*/
    ngx_chain_t          *chain;
    ngx_pool_large_t     *large;      /**< nginx pool 大块内存*/
    ngx_pool_cleanup_t   *cleanup;
    ngx_log_t            *log;
};

可以看到,这里内存池使用了日志ngx_log_t。在debug时打开。在遇到问题时,可以通过热更新开启log。遇到内存泄漏时,打开log,看到相应的内存信息。在生产环境中不要打开log,否则会大大降低内存分配的性能。
那么,内存池分配大块小块的界限是什么呢?这个可以从内存池创建函数得出。

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);
    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;  //4095与size较小值

    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p;
}

小内存块分配

注意内存对齐的设置。

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);  // 当现有内存块中都无法满足分配条件时,创建新的内存块
}

分配新的小内存块

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;

    /**
	 * 缓存池的pool数据结构会挂载子节点的ngx_pool_t数据结构
	 * 子节点的ngx_pool_t数据结构中只用到pool->d的结构,只保存数据
	 * 每添加一个子节点,p->d.failed就会+1,当添加超过4个子节点的时候,
	 * pool->current会指向到最新的子节点地址
	 *
	 * 这个逻辑主要是为了防止pool上的子节点过多,导致每次ngx_palloc循环pool->d.next链表
	 * 将pool->current设置成最新的子节点之后,每次最大循环4次,不会去遍历整个缓存池链表
	*/
    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;
}

大内存块分配

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

ngx_rbtree_t

nginx实现了红黑树。

struct ngx_rbtree_s {
    // 指向树的根节点。
    ngx_rbtree_node_t     *root;
    // 指向NIL哨兵节点
    ngx_rbtree_node_t     *sentinel;
    // 表示红黑树添加元素的函数指针,它决定在添加新节点时的行为
    ngx_rbtree_insert_pt   insert;//红黑树内部插入函数用于将待插入的节点放在合适的NIL叶子节点处
};


struct ngx_rbtree_node_s {
    // 无符号整形的关键字
    ngx_rbtree_key_t       key;
    // 左子节点
    ngx_rbtree_node_t     *left;
    // 右子节点
    ngx_rbtree_node_t     *right;
    // 父节点
    ngx_rbtree_node_t     *parent;
    // 节点的颜色,0表示黑色,1表示红色
    u_char                 color;
    // 仅1个字节的节点数据。由于表示的空间太小,所
以一般很少使用。
    u_char                 data;
};

这里介绍一下ngx_rbtree_insert_pt回调函数,所进行的操作是确定新节点插入的位置,也可以只是替换而不新增节点,具体看初始化时传入的函数指针到底是什么。

// 是个宏函数
#define ngx_rbtree_init(tree, s, i)         \                                
    ngx_rbtree_sentinel_init(s);            \                                  
    (tree)->root = s;                      \                                   
    (tree)->sentinel = s;                   \                                   
    // 注意没有分号                           \
    (tree)->insert = i

具体插入删除节点等操作就不再列举了,与之前介绍的操作一致。这里只把部分插入操作的代码贴出来,看下ngx_rbtree_insert_pt回调函数是如何调用的。

void 
ngx_rbtree_insert(ngx_thread_volatile ngx_rbtree_t *tree,
    ngx_rbtree_node_t *node)//红黑树容器对外提供的插入API
{
    ngx_rbtree_node_t  **root, *temp, *sentinel;
 
    /* a binary tree insert */
 
    root = (ngx_rbtree_node_t **) &tree->root;
    sentinel = tree->sentinel;
 
    if (*root == sentinel) 
    {
        // ...
    }
 
    tree->insert(*root, node, sentinel);//将node插入到树中合适的NIL节点处,此时红黑树性质可能被破坏,需要后续重新平衡树
 
    /* re-balance tree */
 
    while (node != *root && ngx_rbt_is_red(node->parent))
    {
        // ...
    }

    ngx_rbt_black(*root);//对根节点重绘为黑色,当case1插入节点node为新根时有用,其他case该步多余但无害处
}

你可能感兴趣的:(笔记,nginx)