概述
Nginx ("engine x") 是一个高性能的 HTTP 和 反向代理 服务器,也是一个 IMAP/POP3/SMTP 代理服务器 。 Nginx 是由 Igor Sysoev 为俄罗斯访问量第二的Rambler.ru 站点开发的,它已经在该站点运行超过四年多了。 Igor 将源代码以类BSD许可证的形式发布。自Nginx 发布四年来,Nginx 已经因为它的稳定性、丰富的功能集 、示例配置文件和低系统资源的消耗而闻名了。目前国内各大门户网站已经部署了Nginx, 如新浪、网易、腾讯等;国内几个重要的视频分享网站也部署了Nginx,如六房间、酷6等。 新近发现Nginx 技术在国内日趋火热,越来越多的网站开始部署Nginx。
- from http://wiki.nginx.org/NginxChs
我们研究nginx的源代码的动机是为了完成分段反向代理项目的开发,由于分段反向代理的需求要求对web server的并发性很强,并且是给予http协议的基础上进行的, 所以我们选择了使用Nginx的模块的形式进行开发。
我们发现目前学习nginx的例子很少,主要是emiller的模块开发介绍这篇文章, 但是单独研究这篇文章发现很多晦涩难懂的地方,而目前还没有其他更好的文章来对这些地方做解释, 有些东西必须要通过源代码的研读才可以了解的更加清楚,所以我们决定开始进行代码研究计划,以便于更好的完成开发任务
根据目前的状况,我们决定使用最新的稳定版本进行研究,故而选择 0.7.61 版作为调研对象。
http://sysoev.ru/nginx/nginx-0.7.61.tar.gz
研究计划
下面是王晓哲帮助制定的调研计划
- 学习emiller的文章
- 熟悉nginx的基本数据结构 1w
- 了解nginx的core module 的结构和运行机制, 同时参考 event 和 os module 的实现 1w
- 了解nginx的http core module 的结构和运行机制 1w
- 学习handler 类型module的编写 2d
- 学习filter 类型module的编写 2d
- upstream类型module的编写 1w
参与人员
徐景(rainx), 王晓哲(chaoslawful)
研究文档
学习emiller的文章
http://www.evanmiller.org/nginx-modules-guide.html
熟悉nginx的基本数据结构
nginx 代码的目录结构
解开nginx的代码后,在src目录下发现有如下的几个目录
core event http mail misc os
其中 :
- core : 该目录存放core module的代码,也是nginx服务的入口
- http : http core module 的代码,nginx作为web/http proxy server运行时的核心模块
- mail : mail core module 的代码,nginx作为pop3/imap/smtp proxy server运行时的核心模块 ( 不在我们本次研究范围内 )
- event : nginx 自身对事件处理逻辑的封装
- os : nginx对各个平台抽象逻辑的封装
- misc : nginx 的一些utils,定义了test和profiler的一些外围模块的逻辑
为了方便了解整个结构, 我们在src目录下建立了一个子目录,叫做demo,然后根据每个章节内容的不同,分别在demo下建立子目录,存放一些学习时使用的代码,如我们目前的章节是学习基本的数据类型,所以建立basic_types子目录
rainx@rainx-laptop:~/land/nginx-0.7.61/src$ mkdir -p demo/basic_types/
nginx简单的数据类型的表示
在 core/ngx_config.h 目录里面定义了基本的数据类型的映射,大部分都映射到c语言自身的数据类型
typedef intptr_t ngx_int_t; typedef uintptr_t ngx_uint_t; typedef intptr_t ngx_flag_t;
其中 ngx_int_t, nginx_flag_t, 都映射为 intptr_t; ngx_uint_t映射为 uintptr_t
这两个类型在/usr/include/stdint.h的定义为:
/* Types for `void *' pointers. */ #if __WORDSIZE == 64 # ifndef __intptr_t_defined typedef long int intptr_t; # define __intptr_t_defined # endif typedef unsigned long int uintptr_t; #else # ifndef __intptr_t_defined typedef int intptr_t; # define __intptr_t_defined # endif typedef unsigned int uintptr_t; #endif
所以基本的操作和整形/指针类型的操作类似
建立文件
#include <stdio.h> #include "../../core/ngx_config.h" int main() { ngx_uint_t a; ngx_int_t b; a = 1000; b = -1000; printf ("%d + %d = %d\n", a, b, a+b); return 0; }
编译测试
gcc -I ../../../objs/ -I ../../os/unix/ basic_types_int.c -o basic_types_int ./basic_types_int 1000 + -1000 = 0
nginx字符串的数据类型的表示
nginx对c语言的字符串类型进行了简单的封装, core/ngx_string.h/c 里面包含这些封装的内容
其中定义了 ngx_str_t ,ngx_keyval_t, ngx_variable_value_t
这几个基础类型的定义如下
typedef struct { size_t len; u_char *data; } ngx_str_t; typedef struct { ngx_str_t key; ngx_str_t value; } ngx_keyval_t; typedef struct { unsigned len:28; unsigned valid:1; unsigned no_cacheable:1; unsigned not_found:1; unsigned escape:1; u_char *data; } ngx_variable_value_t;
可以看出 ngx_str_t 在原有的uchar* 的基础上加入的字符串长度的附加信息, 初始化使用ngx_string宏进行,他的定义为:
#define ngx_string(str) { sizeof(str) - 1, (u_char *) str }
测试字符串的代码 demo/basic_types/basic_types_str.c
#include <stdio.h> #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_string.h" #include "ngx_string.h" volatile ngx_cycle_t *ngx_cycle; void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { } int main() { u_char* p = NULL; ngx_uint_t size; ngx_str_t dst; ngx_str_t mystr = ngx_string("hello, world !"); ngx_keyval_t pair = {ngx_string("url"), ngx_string("http://rainx.cn/index.php?test=1")}; int dst_len =ngx_base64_encoded_length(mystr.len); printf("source length is %d, destination length is %d\n", mystr.len, dst_len ); p = malloc( ngx_base64_encoded_length(mystr.len) + 1); dst.data = p; ngx_encode_base64(&dst, &mystr); printf("source str is %s\ndestination str is %s\n", mystr.data, dst.data); free(p); size = pair.value.len + 2 * ngx_escape_uri(NULL, pair.value.data, pair.value.len, NGX_ESCAPE_URI); p = malloc (size * sizeof(u_char)); ngx_escape_uri(p, pair.value.data, pair.value.len, NGX_ESCAPE_URI); printf("escaped %s is : %s (%d)\noriginal url size is %d\n", pair.key.data, p, size, pair.value.len); free(p); return 0; }
编译运行
gcc -c -O -pipe -O -W -Wall -Wpointer-arith -Wno-unused-parameter -Wunused-function -Wunused-variable -Wunused-value -Werror -g -I ../../../objs/ -I ../../os/unix/ basic_types_str.c -I../../core/ -I../../event/ -I../../os/ -o basic_types_str.o gcc -o basic_types_str basic_types_str.o ../../../objs/src/core/ngx_{string,palloc}.o ../../../objs/src/os/unix/ngx_alloc.o -lcrypt -lpcre -lcrypto -lz rainx@rainx-laptop:~/land/nginx-0.7.61/src/demo/basic_types$ ./basic_types_str source length is 14, destination length is 20 source str is hello, world ! destination str is aGVsbG8sIHdvcmxkICE= escaped url is : http://rainx.cn/index.php%3ftest=1 (34) original url size is 32
core/ngx_string.h/c 中同时也封装了一批字符/字符串处理的函数和宏,他们的使用大多数情况下和c标准库中的类似,只是在内存分配相关的函数中有一定的区别。
比如 u_char *ngx_pstrdup(ngx_pool_t *pool, ngx_str_t *src); 除了源字符串驻外,还要传入ngx_pool_t的指针作为参数,使用nginx自己的内存分配方式进行内存的分配。
除了标准的字符串操作外, nginx还实现了例如:
// base64 编码/解码函数和宏 #define ngx_base64_encoded_length(len) (((len + 2) / 3) * 4) #define ngx_base64_decoded_length(len) (((len + 3) / 4) * 3) void ngx_encode_base64(ngx_str_t *dst, ngx_str_t *src); ngx_int_t ngx_decode_base64(ngx_str_t *dst, ngx_str_t *src); //utf-8 编码/解码相关函数 uint32_t ngx_utf8_decode(u_char **p, size_t n); size_t ngx_utf8_length(u_char *p, size_t n); u_char *ngx_utf8_cpystrn(u_char *dst, u_char *src, size_t n, size_t len); // urlencode和html实体的编码解码 uintptr_t ngx_escape_uri(u_char *dst, u_char *src, size_t size, ngx_uint_t type); void ngx_unescape_uri(u_char **dst, u_char **src, size_t size, ngx_uint_t type); uintptr_t ngx_escape_html(u_char *dst, u_char *src, size_t size);
等对于http服务有帮助的宏和函数
内存分配相关
系统功能封装
内存相关的操作主要在 os/unix/ngx_alloc.{h,c} 和 core/ngx_palloc.{h,c} 下
其中 os/unix/ngx_alloc.{h,c} 封装了最基本的内存分配函数,是对c原有的malloc/free/memalign 等原有的函数的封装,对应的函数为:
- ngx_alloc 使用malloc分配内存空间
- ngx_calloc 使用malloc分配内存空间,并且将空间内容初始化为0
- ngx_memalign 返回基于一个指定的alignment大小的数值为对齐基数的空间
- ngx_free 对内存的释放操作
ngx的内存池
为了方便系统模块对内存的使用,方便内存的管理,nginx自己实现了进程池的机制来进行内存的分配和释放, 首先nginx会在特定的生命周期帮你统一建立内存池,当需要进行内存分配的时候统一通过内存池中的内存进行分配,最后nginx会在适当的时候释放内存池的资源,开发者只要在需要的时候对内存进行申请即可,不用过多考虑内存的释放等问题,大大提高了开发的效率。
内存池的主要结构为:
//ngx_palloc.h 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; }; //ngx_core.h typedef struct ngx_pool_s ngx_pool_t; typedef struct ngx_chain_s ngx_chain_t;
下面是我简单画的一个图来描述这个结构:
link :
http://www.flickr.com/photos/rainx/3765612584/sizes/o/
下面解释一下主要的几个操作:
// 创建内存池 ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);
大致的过程是创建使用 ngx_alloc 分配一个size大小的空间, 然后将 ngx_pool_t* 指向这个空间, 并且初始化里面的成员, 其中
p->d.last = (u_char *) p + sizeof(ngx_pool_t); // 初始指向 ngx_pool_t 结构体后面 p->d.end = (u_char *) p + size; // 整个结构的结尾后面 p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL; // 最大不超过 NGX_MAX_ALLOC_FROM_POOL,也就是getpagesize()-1 大小
其他大都设置为null或者0
// 销毁内存池 void ngx_destroy_pool(ngx_pool_t *pool);
遍历链表,所有释放内存,其中如果注册了clenup(也是一个链表结构), 会一次调用clenup 的 handler 进行清理。
// 重置内存池 void ngx_reset_pool(ngx_pool_t *pool);
释放所有large段内存, 并且将d->last指针重新指向 ngx_pool_t 结构之后(和创建时一样)
// 从内存池里分配内存 void *ngx_palloc(ngx_pool_t *pool, size_t size); void *ngx_pnalloc(ngx_pool_t *pool, size_t size); void *ngx_pcalloc(ngx_pool_t *pool, size_t size); void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);
ngx_palloc的过程一般为,首先判断待分配的内存是否大于 pool->max的大小,如果大于则使用 ngx_palloc_large 在 large 链表里分配一段内存并返回, 如果小于测尝试从链表的 pool->current 开始遍历链表,尝试找出一个可以分配的内存,当链表里的任何一个节点都无法分配内存的时候,就调用 ngx_palloc_block 生成链表里一个新的节点, 并在新的节点里分配内存并返回, 同时, 还会将pool->current 指针指向新的位置(从链表里面pool->d.failed小于等于4的节点里找出)
,其他几个函数也基本上为 ngx_palloc 的变种,实现方式大同小异
// 释放指定的内存 ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);
这个操作只有在内存在large链表里注册的内存在会被真正释放,如果分配的是普通的内存,则会在destory_pool的时候统一释放.
// 注册cleanup回叫函数(结构体) ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);
这个过程和我们之前经常使用的有些区别, 他首先在传入的内存池中分配这个结构的空间(包括data段), 然后将为结构体分配的空间返回, 通过操作返回的ngx_pool_cleanup_t结构来添加回叫的实现。 (这个过程在nginx里面出现的比较多,也就是 xxxx_add 操作通常不是实际的添加操作,而是分配空间并返回一个指针,后续我们还要通过操作指针指向的空间来实现所谓的add)
下面是内存操作的一些例子 demo/basic_types/mem_op.c
#include <stdio.h> #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_string.h" #include "ngx_palloc.h" volatile ngx_cycle_t *ngx_cycle; void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { } typedef struct example_s { int a; char* b; } example_t; int main() { ngx_pool_t *pool; example_t* exp; char* s; pool = ngx_create_pool(5000, NULL); printf("available pool regular pool free size is %d now\n", (ngx_uint_t) (pool->d.end - pool->d.last)); exp = ngx_palloc(pool, sizeof(example_t)) ; s = ngx_palloc(pool, sizeof("hello,world")); printf("available pool regular pool free size is %d now\n", (ngx_uint_t) (pool->d.end - pool->d.last)); exp->a = 1; exp->b = s; strcpy(s, "hello,world"); printf("pool max is %d\n", pool->max); printf("exp->a is %d, exp->b is %s\n", exp->a, exp->b); ngx_destroy_pool(pool); return 0; }
编译运行结果
gcc -c -O -pipe -O -W -Wall -Wpointer-arith -Wno-unused-parameter -Wunused-function -Wunused-variable -Wunused-value -Werror -g -I ../../../objs/ -I ../../os/unix/ mem_op.c -I../../core/ -I../../event/ -I../../os/ -o mem_op.o gcc -o mem_op mem_op.o ../../../objs/src/core/ngx_{string,palloc}.o ../../../objs/src/os/unix/ngx_alloc.o -lcrypt -lpcre -lcrypto -lz rainx@rainx-laptop:~/land/nginx-0.7.61/src/demo/basic_types$ ./mem_op available pool regular pool free size is 4960 now available pool regular pool free size is 4940 now pool max is 4960 exp->a is 1, exp->b is hello,world
ngx的基本容器
ngx_array
对应的文件为 core/ngx_array.{c|h}
ngx_array是nginx内部封装的使用 ngx_pool_t对内存池进行分配的数组容器,其中的数据是在一整片内存区中连续存放的。更新数组时只能在尾部压入1个或多个元素。
数组的实现结构为
struct ngx_array_s { void *elts; ngx_uint_t nelts; size_t size; ngx_uint_t nalloc; ngx_pool_t *pool; };
其中 elts 为具体的数据区域的指针, nelts 为数组实际包含的元素数量, size为数组单个元素的大小, nalloc为数组容器预先(或者重新)分配的内存大小, pool 为分配基于的内存池
常用的操作有
// 创建一个新的数组容器 ngx_array_t *ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);
// 销毁数组容器 void ngx_array_destroy(ngx_array_t *a);
// 将新的元素加入数组容器 void *ngx_array_push(ngx_array_t *a); void *ngx_array_push_n(ngx_array_t *a, ngx_uint_t n); //返回n个元素的指针
这里需要注意的是,和之前的ngx_pool_cleanup_add一样, ngx_array_push只是进行内存分配的操作,我们需要对返回的指针指向的地址进行赋值等操作来实现实际数组值的添加。
具体一点的push操作的实现为,
- 首先判断 nalloc是否和nelts相等,即数组预先分配的空间已经满了,如果没满则计算地址直接返回指针
- 如果已经满了则先判断是否我们的pool中的当前链表节点还有剩余的空间,如果有则直接在当前的pool链表节点中分配内存,并返回
- 如果当前链表节点没有足够的空间则使用ngx_palloc重新分配一个2倍于之前数组空间大小的数组,然后将数据转移过来,并返回新地址的指针
下面是一个array的例子:
demo/basic_types/array_and_hash.c
#include <stdio.h> #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_string.h" #include "ngx_palloc.h" #include "ngx_array.h" volatile ngx_cycle_t *ngx_cycle; void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { } int main() { ngx_pool_t* pool; ngx_array_t* arr; int n; int* ele; pool = ngx_create_pool(4000, NULL); arr = ngx_array_create(pool, 10, sizeof(ngx_uint_t)); for (n=0; n < 5; n++) { ele = (int*) ngx_array_push(arr); *ele = n; printf("new element %d added\n", n); } printf("arr->nelts is %d, arr->nalloc = %d\n", arr->nelts, arr->nalloc); for (n=5; n < 15; n++) { ele = (int*) ngx_array_push(arr); *ele = n; printf("new element %d added\n", n); } printf("arr->nelts is %d, arr->nalloc = %d\n", arr->nelts, arr->nalloc); ngx_array_destroy(arr); ngx_destroy_pool(pool); return 0; }
编译运行
gcc -c -O -pipe -O -