阅读nginx源码,先从基础组件着手,这里从最基本的,数据结构入手。
使用的是intptr_t,在32位系统中是4B,在64位系统中是8B,做到了支持跨平台。
这是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。
这是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不会对接收到的数据进行修改。另外,即便要修改,查找的效率也极低。
查:查找没有意义,因为只能遍历,效率极低。
当然,还可以在测试时自己写个函数遍历打印链表数据进行检查,但是在实际生产过程中要去掉。
主要用于存储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用于快速检索到头部。
主要用于数据文件同步时。
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;
};
nginx中共享内存的数据结构。
共享内存一般有两种做法,mmap和shmget。对于mmap,nginx中有两种实现方法,一是使用匿名映射,二是映射到/dev/zero
中。
存储多个数据的时候,很容易想到数组,但是使用数组有个前提,需要固定好数组的长度,那么超出了长度怎么办?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文件中每个字段所带的参数。
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;
}
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该步多余但无害处
}