相关介绍
nginx提供了大量的基础数据结构,对一些数据结构进行了封装和实现,我们在模块开发中进程会用到,当然nginx源码里面也是随处可见,熟悉基础数据结构有助于我们更快的去了解nginx源码与内部机制。
基础数据结构
ngx_string
ngx_string结构
ngx_string的实现与封装主要在ngx_string.h和ngx_string.c中,结构如下
typedef struct {
size_t len;
u_char *data;
} ngx_str_t;
1,在结构体当中,data指向字符串数据的第一个字符,字符串的结束用长度来表示,而不是由’\0’来表示结束。所以,在写nginx代码时,处理字符串的方法跟我们平时使用有很大的不一样,但要时刻记住,字符串不以’\0’结束,所以不可以用string库的函数来操作字符串,当然也没必要,nginx自己封装好了库。
2,在系统调用中,就不可以直接用nginx的字符串了,需要处理了才能直接用linux的系统调用。
3,uri、args等等,这些字符串的data部分,都是指向在接收数据时创建buffer所指向的内存中,uri,args就没有必要copy一份出来。这样的话,减少了很多不必要的内存分配与拷贝。
相关源码解析
下面我们来看一下部分源码吧
这是一段简单的把字符转化为数字的代码
/*从该函数可以看出ngx字符串处理函数并不是直接对上述结构体直接操
作,而是对其中的u_char的字符指针和size_t长度进行操作*/
ngx_int_t
ngx_atoi(u_char *line, size_t n)
{
ngx_int_t value, cutoff, cutlim;
if (n == 0) {
return NGX_ERROR;
}
//取出最大值得尾部与前(len - 1)位
cutoff = NGX_MAX_INT_T_VALUE / 10;
cutlim = NGX_MAX_INT_T_VALUE % 10;
for (value = 0; n--; line++) {
if (*line < '0' || *line > '9') {
return NGX_ERROR;
}
//每读取一个数字就进行比较,保证下次读出不会越界。
if (value >= cutoff && (value > cutoff || *line - '0' > cutlim)) {
return NGX_ERROR;
}
value = value * 10 + (*line - '0');
}
return value;
}
相关宏解析
其实大多数函数都不是对ngx_str_t进行操作,而是取出来对指针和长度进行操作。而且字符串的处理宏特别多,函数居少。
//新建一个字符串,长度设为传统字符串长度(不包含0字符结尾)
#define ngx_string(str) { sizeof(str) - 1, (u_char *) str }
//空字符串
#define ngx_null_string { 0, NULL }
//这是一个将字符串变为null的宏
#define ngx_str_null(str) (str)->len = 0; (str)->data = NULL
以上是字符串的相关介绍,字符串比较简单,遇到具体的功能我们再讨论。
ngx_array_t
ngx_array_t结构
数组结构如下,ngx_pool_t是内存池。
typedef struct {
void *elts;
ngx_uint_t nelts;
size_t size;
ngx_uint_t nalloc;
ngx_pool_t *pool;
} ngx_array_t;
elts 指向实际的数据存储的区域的指针。
nelts 数组内部元素的个数。
size 一个数组元素的大小。
nalloc 数组的容量。
pool 该数组用来分配内存的内存池。(ngx_pool_t我们将在后几章介绍)
ngx_array_t初始化
我们先从初始化来了解吧,该函数是供创建调用。
//初始化容量为n的,单个元素大小为size的数组,该函数为内联函数
static ngx_inline ngx_int_t
ngx_array_init(ngx_array_t *array, ngx_pool_t *pool, ngx_uint_t n, size_t size)
{
//目前数组内元素的个数
array->nelts = 0;
//单个元素大小为size
array->size = size;
//数组的容量
array->nalloc = n;
//指定存储数组内容的内存的池。
array->pool = pool;
//为数组在内存池分配内存(大小为 容纳元素个数 * 单个元素大小)
array->elts = ngx_palloc(pool, n * size);
if (array->elts == NULL) {
return NGX_ERROR;
}
return NGX_OK;
}
ngx_array_t创建
数组的创建,数组在创建的时候,需要内存池,然后数组的容量,以及数组单个元素的大小。
ngx_array_t *
ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size)
{
ngx_array_t *a;
//现在内存池中分配数组的结构体,没有分配存储数组元素的内存
a = ngx_palloc(p, sizeof(ngx_array_t));
if (a == NULL) {
return NULL;
}
//对结构体进行赋值,并在内存池分配存储数组元素的内存
if (ngx_array_init(a, p, n, size) != NGX_OK) {
return NULL;
}
return a;
}
可以看出数组信息的结构体和数组存储元素的内存在同一个内存池(ngx_pool_t)。
ngx_array_t压入元素
下面我们再看看在数组里面压入元素
void *
ngx_array_push(ngx_array_t *a)
{
void *elt, *new;
size_t size;
ngx_pool_t *p;
//如果数组已经满了
if (a->nelts == a->nalloc) {
/* the array is full */
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)
{
/**这里面last是内存池当前分配到的地址(即已经分配的最大内存地址),end是内存池的最后(包含已经分配和未分配)**/
/*
* the array allocation is the last in the pool
* and there is space for new allocation
*/
//直接将内存池的已分配的游标后移就可以了
p->d.last += a->size;
//然后给容量数量+1
a->nalloc++;
//如果数组不是在已分配内存块的末尾
} else {
/* allocate a new array */
//再在内存池里面分配两倍容量的数组
new = ngx_palloc(p, 2 * size);
if (new == NULL) {
return NULL;
}
//然后将数组拷贝到新的内存上面
ngx_memcpy(new, a->elts, size);
//改变指针指向
a->elts = new;
//扩容1倍
a->nalloc *= 2;
}
}
//获取新加元素的地址
elt = (u_char *) a->elts + a->size * a->nelts;
//已经使用数量+1
a->nelts++;
//返回元素地址
return elt;
}
ngx_array_t的销毁
void
ngx_array_destroy(ngx_array_t *a)
{
ngx_pool_t *p;
p = a->pool;
//如果数组位于已经分配内存的最后,那么直接将last游标前移。
if ((u_char *) a->elts + a->size * a->nalloc == p->d.last) {
p->d.last -= a->size * a->nalloc;
}
//在释放数组元素存储内存以后,如果数组结构也位于内存池已经分配内存的最后,那么仍然将内存池last游标前移
if ((u_char *) a + sizeof(ngx_array_t) == p->d.last) {
p->d.last = (u_char *) a;
}
}
可以看出,如果数组不在内存池已分配内存的最后,那么销毁内存池将不做任何处理。
心得
可以看出,nginx稍微复杂一点的数据结构,就会采用内存池来实现,带来了很大的优势。由于使用ngx_palloc分配内存,数组在扩容时,旧的内存不会被释放,会造成内存的浪费。所以数组大小最好一次性搞定。对于字符串,设计比较巧妙,比较适合处理url,字符串的设计千变万化,只有适合自己业务(http)的才是最好的。