When I am studying the initialize process of the nginx, I found that the array need reading. There are seldom comments on the sources of nginx. Many a function need reading and comprehending if you want to know what they can do. During reading the sources I took down some notes. The following is the part of my notes. It is wrriten in Chinese.
Nginx源码研究
Nginx的源码是0.8.16版本。不是最新版本,但是与网上其他人研究nginx的源码有所修改。阅读时注意参照对比。
一、内存池
1. 内存分配相关函数
ngx_alloc.c中包括所有nginx内存申请的相关函数。
ngx_alloc() 包装了malloc(),仅添加了内存分配失败时的,log输出和debug时的log输出。
ngx_calloc() 调用上面的函数,成功分配后,将内存清零。
ngx_memalign() 也是向操作系统申请内存,只不过采用内存对齐方式。估计是为了减少内存碎片。如果操作系统支持posix_memalign()就采用它,如果支持memalign()则用memalign()。在0.8.19版本中,作者不再使用ngx_alloc(),而全部改用ngx_memalign()。注:在nginx的main()函数中,通过将ngx_pagesize 设置为1024来指定内存分配按1024bytes对齐。这是不是意味着你虽指示分配10 bytes的内存,实际上nginx也向操作系统申请至少1024bytes的内存。
2. 内存池结构
Nginx的内存池类型是ngx_pool_t。这个类型定义在ngx_core.h中。
typedef struct ngx_pool_s ngx_pool_t;
由定义可知ngx_pool_t背后实际上是struct ngx_pool_s。这个结构体在ngx_palloc.h中有定义。
据说以前版本nginx中内存池的结构如下:
struct ngx_pool_s {
u_char *last;
u_char *end;
ngx_pool_t *current;
ngx_chain_t *chain;
ngx_pool_t *next;
ngx_pool_large_t *large;
ngx_pool_cleanup_t *cleanup;
ngx_log_t *log;
};
目前版本中结构则是这样:
内存池管理结点:
typedef struct {
u_char *last; /* 指向所使用内存的最后的地址 */
u_char *end; /* 指向所申请到内存块的最后的地址 */
ngx_pool_t *next; /* 指向下一个ngx_pool_t */
ngx_uint_t failed; /* 这个 */
} ngx_pool_data_t;
内存池管理队列的头结点:
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; /* 指向log的指针 */
};
清除函数的指针如下:
typedef void (*ngx_pool_cleanup_pt)(void *data);
清除函数的队列结构:
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; /* 清除函数 */
void *data; /* 清除函数所用的参数 */
ngx_pool_cleanup_t *next; /* 下一个结点的指针 */
};
指向大块内存的结构:
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
ngx_pool_large_t *next; /* 指向下一个大块内存。*/
/* 大块内存也是队列管理。*/
void *alloc; /* 指向申请的大块数据 */
};
当待分配空间已经超过了池子自身大小,nginx也没有别的好办法,只好按照你需要分配的大小,实际去调用malloc()函数去分配,例如池子的大小是1K,待分配的大小是1M。实际上池子里只存储了ngx_pool_large_t结构,这个结构中的alloc指针,指向被分配的内存,并把这个指针返回给系统使用。
ngx_create_pool() 函数用来创建内存池。
第一步,调用ngx_alloc()申请内存;
第二步,设置ngx_pool_t中的成员d中的各个变量;
…
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
p->d.end = (u_char *) p + size;
…
从代码看出,d.end指向内存块的结尾处,而d.last则指向所占用的内存的结尾处。刚申请的内存中占用ngx_pool_t结构作为管理单元。所以,此时d.last指向(u_char *) p + sizeof(ngx_pool_t)处。
第三步,设置其他成员。注意:在计算max时,max中存放的数指所申请内存块中空闲的大小。因此,在计算max之前先减去了管理结点本身的大小。
ngx_destroy_pool() 用来释放内存池,一共分三步:
第一步、在释放前先对业务逻辑进行释放前的处理
for (c = pool->cleanup; c; c = c->next) {
if (c->handler) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"run cleanup: %p", c);
c->handler(c->data);
}
}
第二步、释放large占用的内存
for (l = pool->large; l; l = l->next) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
if (l->alloc) {
ngx_free(l->alloc);
}
}
第三步、释放所有的池子
for (p = pool, n = pool->next; /* void */; p = n, n = n->next) {
ngx_free(p);
if (n == NULL) {
break;
}
}
ngx_palloc_large() 函数专用来申请大块内存。
第一步,申请的大块内存,在ngx_pool_t中大块他队列中寻找空闲的ngx_pool_larger结点。如果找到,将大块内存挂在该结点上。
ngx_pool_larger队列中查找空闲结点数不会超过三次。超过三个结点没找到空闲结点就放弃。
创建一个新的结点,将申请到地大块内存挂在这个新结点上。将这个结点插入队列头部。
ngx_palloc() 函数用来申请内存块。首先要说明的是内存池中可能是由多块内存块组成的队列。其中每块内存都有一个ngx_pool_t管理结点用来连成管理队列。
1. 如果申请的内存大小超过了当前的内存池结点中空闲空间,nginx直接采用ngx_palloc_large()函数进行大块内存申请;
2. 在内存池管理队列中寻找能够满足申请大小的管理结点。如果找到了,先按32位对齐方式计算申请内存的指针,再将last指向申请内存块的尾部。
3. 找不到合适的内存池,用ngx_palloc_block()函数。
ngx_pnalloc() 函数与ngx_palloc()函数唯一不同之处,就是在计算申请内存的指针的方式未按32位对齐方式计算。
ngx_palloc_block() 函数用来分配新的内存池块,形成一个队列。
这个函数中申请到新的内存池块后,在该块中分配完ngx_pool_data_t结点后,将这个结点挂在内存池队列的结尾处。
这个函数中有两个要注意的地方:
1. 在内存池块中保留ngx_pool_data_t时,不仅是按ngx_pool_data_t大小计算而且是按32位对齐。
2. ngx_pool_data_t结构中的failed的妙用。单从字面上不是太好理角这个成员变量的作用。实际上是用来计数用的。
for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
current = p->d.next;
}
}
从上面这段代码是寻找内存池队列的尾部。当队列较长,由于内存池管理队列是单向队列所以每次从头到尾搜索是很费时的。每次搜寻失败的结点(非尾部结点)的failed加1。failed指出了该结点经历多少次查寻,目前版本中超过4次时,将内存池的current指针指向其后续的结点。这样,下次再做类似查询时,可以跳过若干不必要的结点加快查询速度。
ngx_pmemalign() 函数采用内存对齐的方式申请大内存块。
ngx_pfree() 函数用来释放大内存块。成功返回NGX_OK,失败返回NGX_DECLINED。
ngx_pcalloc() 函数使用ngx_palloc()函数申请内存后,将申请的内存清零。
ngx_pool_cleanup_add() 函数只是用来添加内存池的cleanup队。
由ngx_pool_cleanup_t类型的结点组成了内存池的清除函数处理队列。后加入队列的函数先调用。Nginx中预定义了两个cleanup函数。
void ngx_pool_cleanup_file(void *data) 用来关闭打开的文件。
void ngx_pool_delete_file(void *data) 用来删除文件并且试图关闭文件。
3. 小结
下面是nginx内存池的概貌图。
二、array
1. 结构
struct ngx_array_s {
void *elts; /* 指向数组元素的起始地址 */
ngx_uint_t nelts; /* 现使用的元素的数目 */
size_t size; /* 元素的大小 */
ngx_uint_t nalloc; /* 分配数组中元素的数目 */
ngx_pool_t *pool; /* 指向所在的内存池的指针 */
};
注:nelts应该小于等于nalloc。例:分配了5个元素,使用了3个元素;则nalloc = 5, nelts = 3。
2. 相关函数
ngx_array_init() 函数,在指定的array对象上分配n个元素,元素大小为size。
ngx_array_create()函数,函数参数中没有array对象的指针。这个函数在内存池中创建一个array对象,并且分配n个元素,元素大小为size。
ngx_array_push()函数,将array对象当作堆栈,作压栈处理。如果当前内存池没有空闲空间可用,就会申请新的内存池并且创建一个是原来array对象两倍大小的新array,原array对象中的元素复制到新array中。
ngx_array_push_n()函数,与ngx_array_push()函数功能类似。
ngx_array_push_n()是压n个元素,ngx_array_push()压入一个元素。