1. 前言
Nginx是当前最流行的HTTP Server之一,根据W3Techs的统计,目前世界排名(根据Alexa)前100万的网站中,Nginx的占有率为6.8%。与Apache相比,Nginx在高并发情况下具有巨大的性能优势。
Nginx属于典型的微内核设计,其内核非常简洁和优雅,同时具有非常高的可扩展性。Nginx最初仅仅主要被用于做反向代理,后来随着HTTP核心的成熟和各种HTTP扩展模块的丰富,Nginx越来越多被用来取代Apache而单独承担HTTP Server的责任,例如目前淘宝内各个部门正越来越多使用Nginx取代Apache,据笔者了解,在腾讯和新浪等公司也存在类似情况。
同时,大量的第三方扩展模块也令Nginx越来越强大。例如,由淘宝的工程师清无(王晓哲)和春来(章亦春)所开发的nginx_lua_module可以将Lua语言嵌入到Nginx配置中,从而利用Lua极大增强了Nginx本身的编程能力,甚至可以不用配合其它脚本语言(如PHP或Python等),只靠Nginx本身就可以实现复杂业务的处理。而春来所开发的ngx_openresty更是通过集成LuaJIT等组件,将Nginx本身变成了一个完全的应用开发平台。目前淘宝数据平台与产品部量子统计的产品都是基于ngx_openresty所开发。
本文将会重点关注Nginx模块开发入门及基础。目前Nginx的学习资料非常少,而扩展模块开发相关的资料几乎只有《Emiller's Guide To Nginx Module Development》一文,此文十分经典,但是由于Nginx版本的演进,其中少许内容可能有点过时。本文是笔者在研读这篇文章和Nginx源代码的基础上,对自己学习Nginx模块开发的一个总结。本文将通过一个完整的模块开发实例讲解Nginx模块开发的入门内容。
2. 简介
nginx是一个开源的高性能web服务器系统,事件驱动的请求处理方式和极其苛刻的资源使用方式,使得nginx成为名副其实的高性能服务器。nginx的源码质量也相当高,作者“家酿”了许多代码,自造了不少轮子,诸如内存池、缓冲区、字符串、链表、红黑树等经典数据结构,事件驱动模型,http解析,各种子处理模块,甚至是自动编译脚本都是作者根据自己的理解写出来的,也正因为这样,才使得nginx比其他的web服务器更加高效。
nginx的代码相当精巧和紧凑,虽然全部代码仅有10万行,但功能毫不逊色于几十万行的apache。不过各个部分之间耦合的比较厉害,很难把其中某个部分的实现拆出来使用。对于这样一个中大型的复杂系统源码进行分析,是有一定的难度的,刚开始也很难找到下手的入口,所以做这样的事情就必须首先明确目标和计划。
首先这个系统中几乎涵盖了实现高性能服务器的各种必杀技,epoll、kqueue、master-workers、pool、buffer... ...,也涵盖了很多web服务开发方面的技术,ssi、ssl、proxy、gzip、regex、load balancing、reconfiguration、hot code swapping... ...,还有一些常用的精巧的数据结构实现,所有的东西很主流;其次是一流的代码组织结构和干净简洁的代码风格,尤其是整个系统的命名恰到好处,可读性相当高,这种风格值得学习和模仿;第三是通过阅读源码可以感受到作者严谨的作风和卓越的能力,可以给自己增加动力,树立榜样的力量。
另一方面,要达到这些目标难度很高,必须要制定详细的计划和采取一定有效的方法。
对于这么大的一个系统,想一口气知晓全部的细节是不可能的,并且nginx各个部分的实现之间关系紧密,不可能做到窥一斑而知全身,合适的做法似乎应该是从main开始,先了解nginx的启动过程的顺序,然后进行问题分解,再逐个重点分析每一个重要的部分。
对每个理解的关键部分进行详细的记录和整理也是很重要的,这也是这个源码分析日志系列所要完成的任务。
为了更深刻的理解代码实现的关键,修改代码和写一些测试用例是不可避免的,这就需要搭建一个方便调试的环境,这也比较容易,因为使用的linux系统本身就是一个天然的开发调试环境。
3. 概览
3.1. 代码阅读心得
源码分析是一个逐步取经的过程,最开始是一个大概了解的过程,各种认识不会太深刻,但是把这些真实的感受也记录下来,觉得挺有意思的,可能有些认识是片面或者是不正确的,但可以通过后面更深入细致的分析过程,不断的纠正错误和深化理解。源码分析是一个过程,经验是逐步累积起来的,希望文字可以把这种累积的感觉也准确记录下来。
现在就看看对nginx源码的第一印象吧。
大体上分析源代码都要经历三遍过程:
l 浏览,通过阅读源码的文档和注释,阅读接口,先弄清楚每个模块是干什么的而不关心它是怎么做的,画出架构草图。
l 精读,根据架构草图把系统分为小部分,每个部分从源码实现自底向上的阅读,更深入细致的理解每个模块的实现方式以及与模块外部的接口方式等,弄明白模块是怎么做的,为什么这样做,有没有更好的方式,自己会如何实现等等问题。
l 总结回顾,完善架构图,把架构图中那些模糊的或者空着的模块重新补充完善,把一些可复用的实现放入自己的代码库中。
现在是浏览阶段,并不适合过早涉及代码的实现细节,要借助nginx的文档理解其整体架构和模块划分。经过几年的发展,nginx现在的文档已经是很丰富了,nginx的英文wiki上包含了各个模块的详细文档,faq也涵盖了很丰富的论题,利用这些文档足以建立 nginx的架构草图。所以浏览阶段主要的工作就是阅读文档和画架构草图了。
对于源码分析,工具是相当关键的。这几天阅读源码的过程,熟悉了三个杀手级的工具:scrapbook离线文件管理小程序、graphviz图形生成工具、leo-editor文学编程理念的编辑器。
scrapbook是firefox下一款轻量高效的离线文件管理扩展程序,利用scrapbook把nginx的wiki站点镜像到本地只需要几分钟而已,管理也相当简单,和书签类似。
graphviz是通过编程画图的工具集合,用程序把图形的逻辑和表现表示出来,然后通过命令行执行适当的命令就可以解析生成图形。
leo- editor与其说是一个工具平台,不如说是一套理念。和其他编辑器ide不同的是,leo关注的是文章内容的内在逻辑和段落层次,文章的表现形式和格式是次要的。用leo的过程,其实就是在编程,虽然刚开始有些不适应,但习惯之后确实很爽,杀手级的体验感,很听话。
3.2. 原码结构
源码包解压之后,根目录下有几个子目录和几个文件,最重要的子目录是auto和src,最重要的文件是configure脚本。
不同于绝大多数的开源代码,nginx的configure脚本是作者手工编写的,没有使用autoconf之类的工具去自动生成,configure脚本会引用auto目录下面的脚本文件来干活。
根据不同的用途,auto目录下面的脚本各司其职,有检查编译器版本的,有检查操作系统版本的,有检查标准库版本的,有检查模块依赖情况的,有关于安装的,有关于初始化的,有关于多线程检查的等等。
configure作为一个总驱动,调用这些脚本去生成版本信息头文件、默认被包含的模块的声明代码和Makefile文件,版本信息头文件 (ngx_auto_config.h,ngx_auto_headers.h)和默认被包含的模块的声明代码(ngx_modules.c)被放置在新创建的objs目录下。
要注意的是,这几个生成的文件和src下面的源代码一样重要,对于理解源码是不可忽略的重要部分。
3.3. 原码路径
src是源码存放的目录,configure创建的objs/src目录是用来存放生成的.o文件的,注意区分一下。
src按照功能特性划分为几个部分,对应着是几个不同的子目录。
l src/core存放着主干部分、基础数据结构和基础设施的源码,main函数在src/core/nginx.c中,这是分析源码的一个很好的起点。
l src/event存放着事件驱动模型和相关模块的源码。
l src/http存放着http server和相关模块的源码。
l src/mail存放着邮件代理和相关模块的源码。
l src/misc存放着C++兼容性测试和google perftools模块的源码。
l src/os存放着依赖于操作系统实现的源码,nginx启动过程中最重要的master和workers创建代码就在这个目录下,多少让人觉得有点意外。
l nginx 的实现中有非常多的结构体,一般命名为ngx_XXX_t,这些结构体分散在许多头文件中,而在src/core/ngx_core.h中把几乎所有的头文件都集合起来,所有的实现文件都会包含这个ngx_core.h头文件,说nginx的各部分源码耦合厉害就是这个原因,但实际上nginx各个部分之间逻辑上是划分的很清晰的,整体上是一种松散的结构。nginx实现了一些精巧的基础数据结构,例如 ngx_string_t,ngx_list_t,ngx_array_t,ngx_pool_t,ngx_buf_t,ngx_queue_t,ngx_rbtree_t,ngx_radix_tree_t 等等,还有一些重要的基础设施,比如log,configure file,time等等,这些数据结构和基础设施频繁的被使用在许多地方,这会让人感觉nginx逻辑上的联系比较紧密,但熟悉了这些基础数据结构的实现代码就会感觉到这些数据结构都是清晰分明的,并没有真正的耦合在一起,只是有些多而已,不过nginx中“家酿”的代码也正是它的一个很明显的亮点。
nginx是高度模块化的,可以根据自己的需要定制模块,也可以自己根据一定的标准开发需要的模块,已经定制的模块会在objs/ngx_modules.c中声明,这个文件是由configure生成的。
nginx 启动过程中,很重要的一步就是加载和初始化模块,这是在ngx_init_cycle中完成的,ngx_init_cycle会调用模块的hook接口(init_module)对模块初始化,ngx_init_cycle还会调用ngx_open_listening_sockets初始化 socket,如果是多进程方式启动,就会调用ngx_master_process_cycle完成最后的启动动作,ngx_master_process_cycle调用ngx_start_worker_processes生成多个工作子进程,ngx_start_worker_processes调用ngx_worker_process_cycle创建工作内容,如果进程有多个子线程,这里也会初始化线程和创建线程工作内容,初始化完成之后,ngx_worker_process_cycle会进入处理循环,调用 ngx_process_events_and_timers,该函数调用ngx_process_events监听事件,并把事件投递到事件队列 ngx_posted_events中,最终会在ngx_event_thread_process_posted中处理事件。
事件机制是nginx中很关键的一个部分,linux下使用了epool,freebsd下使用了kqueue管理事件。
3.4. 资料
利用nginx wiki和互联网收集了不少nginx相关的文档资料,但是仔细阅读之后发觉对理解nginx架构有直接帮助的资料不多,一些有帮助的资料也要结合阅读部分源码细节才能搞清楚所述其是,可能nginx在非俄国之外的环境下流行不久,应用还很简单,相关的英文和中文文档也就不够丰富的原因吧。
如果要了解nginx的概况和使用方法,wiki足以满足需要,wiki上有各个模块的概要和详细指令说明,也有丰富的配置文件示例,不过对于了解nginx系统架构和开发没有相关的文档资料。
nginx的开发主要是指撰写自定义模块代码。这需要了解nginx的模块化设计思想,模块化也是nginx的一个重要思想。如果要整体上了解nginx,从模块化入手是一个不错的起点。emiller的nginx模块开发指引是目前最好的相关资料了(http://emiller.info/nginx-modules-guide.html),这份文档作为nginx的入门文档也是合适的,不过其中有些内容很晦涩,很难理解,要结合阅读源码,反复比对才能真正理解其内涵。
如果要从整体上了解nginx架构和源码结构,Joshua zhu的广州技术沙龙讲座的pdf和那张大图是不错的材料,这份pdf可以在wiki的资源页面中找到地址链接。
相信最好的文档就是源码本身了。随着阅读源码的量越来越大,也越来越深入,使我认识到最宝贵的文档就在源码本身。之前提到过,nginx的代码质量很高,命名比较讲究,虽然很少注释,但是很有条理的结构体命名和变量命名使得阅读源码就像是阅读文档。不过要想顺利的保持这种感觉也不是一件简单的事情,觉得要做好如下几点:
1)熟悉C语言,尤其是对函数指针和宏定义要有足够深入的理解,nginx是模块化的,它的模块化不同于apache,它不是动态加载模块,而是把需要的模块都编译到系统中,这些模块可以充分利用系统核心提供的诸多高效的组件,把数据拷贝降到最低的水平,所以这些模块的实现利用了大量的函数指针实现回掉操作,几乎是无函数指针不nginx的地步。
2)重点关注nginx的命名,包括函数命名,结构体命名和变量命名,这些命名把nginx看似耦合紧密的实现代码清晰的分开为不同层次不同部分的组件和模块,这等效于注释。尤其要关注变量的命名,后面关于模块的分析中会再次重申这一点。
3)写一个自定义的模块,利用nginx强大的内部组件,这是深入理解nginx的一个有效手段。
接下来的分析过程,着眼于两个重点,一个就是上面提到的模块化思想的剖析,力争结合自身理解把这个部分分析透彻;另一个重点是nginx的事件处理流程,这是高性能的核心,是nginx的core。
4. 自动脚本
nginx的自动脚本指的是configure脚本程序和auto子目录下面的脚本程序。自动脚本完成两件事情,其一是检查环境,其二是生成文件。生成的文件有两类,一类是编译代码需要的Makefile文件,一类是根据环境检查结果生成的c代码。生成的Makefile很干净,也很容易阅读。生成的c代码有三个文件,ngx_auto_config.h是根据环境检查的结果声明的一些宏定义,这个头文件被include进ngx_core.h中,所以会被所有的源码引用到,这确保了源码是可移植的;ngx_auto_headers.h中也是一些宏定义,不过是关于系统头文件存在性的声明;ngx_modules.c是默认被包含进系统中的模块的声明,如果想去掉一些模块,只要修改这个文件即可。
configure是自动脚本的总驱动,它通过组合auto目录下不同功能的脚本程序完成环境检查和生成文件的任务。环境检查主要是三个部分:编译器版本及支持特性、操作系统版本及支持特性、第三方库支持,检查的脚本程序分别存放在auto/cc、auto/os、auto/lib三个子目录中。检查的方法很有趣,通过自动编译用于检查某个特性的代码片段,根据编译器的输出情况判定是否支持该种特性。根据检查的情况,如果环境足以支持运行一个简单版本的nginx,就会生成 Makefile和c代码,这些文件会存放在新创建的objs目录下。当然,也可能会失败,假如系统不支持pcre和ssh,如果没有屏蔽掉相关的模块,自动脚本就会失败。
auto目录下的脚本职能划分非常清晰,有检查环境的,有检查模块的,有提供帮助信息的(./configure –help),有处理脚本参数的,也有一些脚本纯粹是为了模块化自动脚本而设计出来的,比如feature脚本是用于检查单一特性的,其他的环境检查脚本都会调用这个脚本去检查某个特性。还有一些脚本是用来输出信息到生成文件的,比如have、nohave、make、install等。
之所以要在源码分析中专门谈及自动脚本,是因为nginx的自动脚本不是用autoconf之类的工具生成的,而是作者手工编写的,并且包含一定的设计成分,对于需要编写自动脚本的人来说,有很高的参考价值。这里也仅仅是粗略的介绍一下,需要详细了解最好是读一下这些脚本,这些脚本并没有使用多少生僻的语法,可读性是不错的。
后面开始进入真正的源码分析阶段,nginx的源码中有非常多的结构体,这些结构体之间引用也很频繁,很难用文字表述清楚之间的关系,觉得用图表是最好的方式,因此需要掌握一种高效灵活的作图方法,我选择的是graphviz,这是at&t贡献的跨平台的图形生成工具,通过写一种称为“the dot language”的脚本语言,然后用dot命令就可以直接生成指定格式的图,很方便。
5. 在Linux下的安装与运行
使用Nginx的第一步是下载Nginx源码包,例如1.0.0的下载地址为http://nginx.org/download/nginx-1.0.0.tar.gz。下载完后用tar命令解压缩,进入目录后安装过程与Linux下通常步骤无异,例如我想将Nginx安装到/usr/local/nginx下,则执行如下命令:
./configure --prefix=/usr/local/nginx make make install |
安装完成后可以直接使用下面命令启动Nginx:/usr/local/nginx/sbin/nginx。
Nginx默认以Deamon进程启动,输入下列命令:curl -i http://localhost/,就可以检测Nginx是否已经成功运行;或者也可以在浏览器中输入http://localhost/,应该可以看到Nginx的欢迎页面了。
启动后如果想停止Nginx可以使用:/usr/local/nginx/sbin/nginx -s stop。
6. 配置文件基本结构
配置文件可以看做是Nginx的灵魂,Nginx服务在启动时会读入配置文件,而后续几乎一切动作行为都是按照配置文件中的指令进行的,因此如果将Nginx本身看做一个计算机,那么Nginx的配置文件可以看成是全部的程序指令。
下面是一个Nginx配置文件的实例:
#user nobody; worker_processes 8; error_log logs/error.log; pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream;
sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on;
server { listen 80; server_name localhost; location / { root /home/yefeng/www; index index.html index.htm; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } } |
Nginx配置文件是纯文本文件,你可以用任何文本编辑器如vim或emacs打开它,通常它会在nginx安装目录的conf下,如我的nginx安装在/usr/local/nginx,主配置文件默认放在/usr/local/nginx/conf/nginx.conf。
其中“#”表示此行是注释,由于笔者为了学习扩展开发安装了一个纯净的Nginx,因此配置文件没有经过太多改动。
Nginx的配置文件是以block的形式组织的,一个block通常使用大括号“{}”表示。block分为几个层级,整个配置文件为main层级,这是最大的层级;在main层级下可以有event、http等层级,而http中又会有server block,server block中可以包含location block。
每个层级可以有自己的指令(Directive),例如worker_processes是一个main层级指令,它指定Nginx服务的Worker进程数量。有的指令只能在一个层级中配置,如worker_processes只能存在于main中,而有的指令可以存在于多个层级,在这种情况下,子block会继承父block的配置,同时如果子block配置了与父block不同的指令,则会覆盖掉父block的配置。指令的格式是“指令名 参数1 参数2 … 参数N;”,注意参数间可用任意数量空格分隔,最后要加分号。
在开发Nginx HTTP扩展模块过程中,需要特别注意的是main、server和location三个层级,因为扩展模块通常允许指定新的配置指令在这三个层级中。
最后要提到的是配置文件是可以包含的,如上面配置文件中“include mime.types”就包含了mine.types这个配置文件,此文件指定了各种HTTP Content-type。
一般来说,一个server block表示一个Host,而里面的一个location则代表一个路由映射规则,这两个block可以说是HTTP配置的核心。
下图是Nginx配置文件通常结构图示。
Nginx配置文件主要分成四部分:main(全局设置)、server(主机设置)、upstream(上游服务器设置)和 location(URL匹配特定位置后的设置)。每部分包含若干个指令。main部分设置的指令将影响其它所有设置;server部分的指令主要用于指定主机和端口;upstream的指令用于设置一系列的后端服务器;location部分用于匹配网页位置(比如,根目录“/”,“/images”,等等)。他们之间的关系式:server继承main,location继承server;upstream既不会继承指令也不会被继承。它有自己的特殊指令,不需要在其他地方的应用。
7. 启动流程
7.1. main()分析
nginx启动过程如下。
l 调用ngx_strerror_init
初始化错误信息,将linux系统的错误编码信息,导入到内存中,为的是以后使用的时候,直接从内存中获取,这是从效率方面出发考虑的。
使用的是malloc直接去分配内存,因为当正式使用的情况下,此功能不一定会开放。
l 调用ngx_get_options()解析命令参数;
主要选项有如下:
选项 | 作用 | 相关变量 |
-?,-h | 显示帮助信息 | ngx_show_version ngx_show_help |
-v | 显示版本信息并退出 | ngx_show_version |
-V | 显示 nginx 的版本,编译器版本和配置参数。 | ngx_show_version ngx_show_configure |
-t | 不运行,而仅仅测试配置文件。nginx 将检查配置文件的语法的正确性,并尝试打开配置文件中所引用到的文件。 | ngx_test_config |
-q |
| ngx_quiet_mode |
-s signal | 发送一个信号到主进程stop, quit, reopen, reload | ngx_process |
-p prefix | 设置配置的路径 | ngx_conf_file |
-c filename | 设置配置文件的名称 | ngx_prefix |
-g directives | 指定全局配置指令 | ngx_conf_params |
l 调用ngx_show_version,显示辅助信息
显示辅助信息的主要流程如下:
IF (1 == ngx_show_version)
展示版本信息
IF (1 == ngx_show_help)
展示帮助信息
ENDIF
IF (1 == ngx_show_configure)
展示编译信息
ENDIF
ENDIF
l 调用ngx_time_init()初始化并更新时间,如全局变量ngx_cached_time;
l 调用ngx_log_init()初始化日志,如初始化全局变量ngx_prefix,打开日志文件ngx_log_file.fd;
l 清零全局变量ngx_cycle,并为ngx_cycle.pool创建大小为1024B的内存池;
l 调用ngx_save_argv(),保存命令行参数至全局变量ngx_os_argv、ngx_argc、ngx_argv中;
l 调用ngx_process_options(),初始化ngx_cycle的prefix, conf_prefix, conf_file, conf_param等字段;
l 调用ngx_os_init(),初始化系统相关变量,如内存页面大小ngx_pagesize,ngx_cacheline_size,最大连接数ngx_max_sockets等;
l 调用ngx_crc32_table_init(),初始化CRC表(后续的CRC校验通过查表进行,效率高);
l 调用ngx_add_inherited_sockets()继承sockets;
l 解析环境变量NGINX_VAR="NGINX"中的sockets,并保存至ngx_cycle.listening数组;
l 设置ngx_inherited=1;
l 调用ngx_set_inherited_sockets(),逐一对ngx_cycle.listening数组中的sockets进行设置;具体可参考<nginx源码分析—初始化过程中处理继承的sockets>
l 初始化每个module的index,并计算ngx_max_module;具体可参考<nginx源码分析—模块及其初始化>;
l 调用ngx_init_cycle()进行初始化;
l 该初始化主要对ngx_cycle结构进行;具体可参考<nginx源码分析—全局变量ngx_cycle的初始化>;
l 若有信号,则进入ngx_signal_process()处理;
l 调用ngx_init_signals()初始化信号;主要完成信号处理程序的注册;
l 若无继承sockets,且设置了守护进程标识,则调用ngx_daemon()创建守护进程;
l 调用ngx_create_pidfile()创建进程记录文件;(非NGX_PROCESS_MASTER=1进程,不创建该文件)
l 进入进程主循环;
l 若为NGX_PROCESS_SINGLE=1模式,则调用ngx_single_process_cycle()进入进程循环;
l 否则为master-worker模式,调用ngx_master_process_cycle()进入进程循环;具体可参考<nginx源码分析—master/worker进程启动>。
7.2. 注意问题
7.2.1. 几个初值
ngx_cycle = cycle; ccf = (ngx_core_conf_t*) ngx_get_conf(cycle->conf_ctx, ngx_core_module); if (ccf->master && ngx_process == NGX_PROCESS_SINGLE) { ngx_process = NGX_PROCESS_MASTER; } |
此处单独将该段代码拿出来,说明以下问题。
ngx_process在此处的值是什么?——NGX_PROCESS_SINGLE=0
ccf->master在此处的值之什么?——NGX_CONF_UNSET=-1
ngx_prefix何时初始化的?——ngx_log_init()中初始化
(1)ccf->master
ccf->master的值是在ngx_init_cycle()函数中调用NGX_CORE_MODULE模块的create_conf钩子(callback)完成初始化的。
具体可参考<nginx源码分析—core模块callback>。
(2)ngx_process
ngx_process是全局变量,定义如下。
//./src/os/unix/ngx_process_cycle.c ngx_uint_t ngx_process; ngx_pid_t ngx_pid; ngx_uint_t ngx_threaded; |
在ngx_log_init()中初始化。实际上,在main()函数开始调用的ngx_get_options()函数,即是处理nginx启动的命令,并从中获取参数并赋予相应的变量。
-p:ngx_prefix
-c:ngx_conf_file
-g:ngx_conf_params
-s:ngx_signal
7.2.2. nginx工作模式
因此,main()函数返回前调用ngx_master_process_cycle()函数进入多进程(master/worker)工作模式。如下。
if (ngx_process == NGX_PROCESS_SINGLE) { //单进程 ngx_single_process_cycle(cycle); } else { //多进程 ngx_master_process_cycle(cycle); } |
具体请参考<nginx源码分析—master/worker进程启动>。
7.2.3. 一些配置
看源代码时,会注意到有些宏(宏全部大写,中间用下划线隔开,这是nginx代码规范。实际上,绝大多数系统均采用此规范)找不到定义,例如NGX_PREFIX、NGX_CONF_PREFIX、NGX_CONF_PATH等。
实际上,这些宏定义由configure程序进行自动配置时生成。配置时会自动生成ngx_auto_config.h文件(如果你用source insight阅读源代码,需要将该文件加入工程),如下。
./objs/ngx_auto_config.h(此处列出其中一部分常用的宏,未按顺序)
#ifndef NGX_COMPILER #define NGX_COMPILER "gcc 4.6.1 20110908 (Red Hat 4.6.1-9) (GCC) " #endif
#ifndef NGX_PCRE #define NGX_PCRE 1 #endif
#ifndef NGX_PREFIX #define NGX_PREFIX "/usr/local/nginx/" #endif
#ifndef NGX_CONF_PREFIX #define NGX_CONF_PREFIX "conf/" #endif
#ifndef NGX_CONF_PATH #define NGX_CONF_PATH "conf/nginx.conf" #endif
#ifndef NGX_PID_PATH #define NGX_PID_PATH "logs/nginx.pid" #endif
#ifndef NGX_LOCK_PATH #define NGX_LOCK_PATH "logs/nginx.lock"
#ifndef NGX_ERROR_LOG_PATH #define NGX_ERROR_LOG_PATH "logs/error.log" #endif
#ifndef NGX_HTTP_LOG_PATH #define NGX_HTTP_LOG_PATH "logs/access.log" #endif
#ifndef NGX_HTTP_CLIENT_TEMP_PATH #define NGX_HTTP_CLIENT_TEMP_PATH "client_body_temp" #endif
#ifndef NGX_HTTP_PROXY_TEMP_PATH #define NGX_HTTP_PROXY_TEMP_PATH "proxy_temp" #endif |
7.2.4. 其他开关
还有一些开关,如NGX_FREEBSD, NGX_PCRE,NGX_OPENSSL等,这些宏也在configure过程中自动配置。nginx启动时会根据这些宏是否定义调用相应的函数。此处非本文重点,不再赘述。
以上3个宏分别调用相应函数进行debug的初始化、正则表达式初始化和SSL的初始化。如下。(代码未按顺序)
#if (NGX_FREEBSD) ngx_debug_init(); #endif
#if (NGX_PCRE) ngx_regex_init(); #endif
#if (NGX_OPENSSL) ngx_ssl_init(log); #endif |
7.3. 小结
本文简单分析nginx的启动过程。主要是main函数中的调用,在main中,只有顶层的调用,没有接触到细节。
8. 模块工作原理概述
8.1. 主体流程
Nginx本身支持多种模块,如HTTP模块、EVENT模块和MAIL模块,本文只讨论HTTP模块。
Nginx本身做的工作实际很少,当它接到一个HTTP请求时,它仅仅是通过查找配置文件将此次请求映射到一个location block,而此location中所配置的各个指令则会启动不同的模块去完成工作,因此模块可以看做Nginx真正的劳动工作者。通常一个location中的指令会涉及一个handler模块和多个filter模块(当然,多个location可以复用同一个模块)。handler模块负责处理请求,完成响应内容的生成,而filter模块对响应内容进行处理。因此Nginx模块开发分为handler开发和filter开发(本文不考虑load-balancer模块)。下图展示了一次常规请求和响应的过程。
Nginx的模块有三种角色:
- handlers 处理http请求并构造输出
- filters 处理handler产生的输出
- load-balancers 当有多于一个的后端服务器时,选择一台将http请求发送过去
许多可能你认为是web server的工作,实际上都是由模块来完成的:任何时候,Nginx提供文件或者转发请求到另一个server,都是通过handler来实现的;而当需要Nginx用gzip压缩输出或者在服务端加一些东东的话,filter就派上用场了;Nginx的core模块主要管理网络层和应用层协议,并启动针对特定请求的一系列后续模块。这种分散式的体系结构使得由你自己来实现强大的内部单元成为了可能。
注意:不像Apache的模块那样,Nginx的模块都不是动态链接的。(换句话说,Nginx的模块都是静态编译的) 模块是如何被调用的呢?典型地说,当server启动时,每一个handler都有机会去处理配置文件中的location定义,如果有多个 handler被配置成需要处理某一特定的location时,只有其中一个handler能够“获胜”。
一个handler有三种返回方式:正常、错误、放弃处理转由默认的handler来处理(典型地如处理静态文件的时候)。
如果handler的作用是把请求反向代理到后端服务器,那么就是刚才说的模块的第三种角色load-balancer了。load-balancer主要是负责决定将请求发送给哪个后端服务器。Nginx目前支持两种load-balancer模块:round-robin(轮询,处理请求就像打扑克时发牌那样)和IP hash(众多请求时,保证来自同一ip的请求被分发的同一个后端服务器)。
如果handler返回(就是http响应,即filter的输入)正确无误,那么fileter就被调用了。每个location配置里都可以添加多个filter,所以说(比如)响应可以被压缩和分块。多个filter的执行顺序是编译时就确定了的。filter采用了经典的“接力链表(CHAIN OF RESPONSIBILITY)”模式:一个filter被调用并处理,接下来调用下一个filter,直到最后一个filter被调用完成,Nginx 才真正完成响应流程。
最帅的部分是在 filter链中,每个filter不会等待之前的filter完全完工,它可以处理之前filter正在输出的内容,这有一点像Unix中的管道。 Filter的操作都基于buffers_,buffer通常情况下等于一个页的大小(4k),你也可以在nginx.conf里改变它的大小。这意味着,比如说,模块可以在从后端服务器收到全部的响应之前,就开始压缩这个响应并流化(stream to)给客户端了。
总结一下上面的内容,一个典型的周期应当是这样的:
² 客户端发送HTTP request
² Nginx基于location的配置选择一个合适的handler
² (如果有) load-balancer选择一个后端服务器
² Handler处理请求并顺序将每一个响应buffer发送给第一个filter
² 第一个filter讲输出交给第二个filter
² 第二个给第三个
² 第三个给第四个
² 以此类推
² 最终响应发送给客户端
我之所以说“典型地”是因为Ngingx的模块具有很强的定制性。模块开发者需要花很多精力精确定义模块在何时、如何产生作用。模块调用实际上是通过一系列的回调函数做到的,很多很多。名义上来说,你的函数可以在以下时候被执行:
l server读取配置文件之前
l 读取location和server的每一条配置指令
l 当Nginx初始化main配置段时
l 当Nginx初始化server配置段时(例如:host/port)
l 当Nginx合并server配置和main配置时
l 当Nginx初始化location配置时
l 当Nginx合并location配置和它的父server配置时
l 当Nginx的主进程启动时
l 当一个新的worker进程启动时
l 当一个worker进程退出时
l 当主进程退出时
l handle 一个请求
l Filter响应头
l Filter响应体
l 选择一个后端服务器
l 初始化一个将发往后端服务器的请求
l 重新-初始化一个将发往后端服务器的请求
l 处理来自后端服务器的响应
l 完成与后端服务器的交互
难以置信!有这么多的功能任你处置,而你只需仅仅通过多组有用的钩子(由函数指针组成的结构体)和相应的实现函数。
8.2. 模块类型及配置地方
要知道nginx有哪些模块,一个快速的方法就是编译nginx。编译之后,会在源代码根目录下生成objs目录,该目录中包含有objs/ngx_auto_config.h和objs/ngx_auto_headers.h,以及objs/ngx_modules.c文件,当然,还有Makefile文件等。
其中,生成的objs/ngx_modules.c文件中,重新集中申明(使用extern关键字)了nginx配置的所有模块,这些模块可通过编译前的configure命令进行配置,即设置哪些模块需要编译,哪些不被编译。如下:
extern ngx_module_t ngx_core_module; extern ngx_module_t ngx_errlog_module; extern ngx_module_t ngx_conf_module; extern ngx_module_t ngx_events_module; extern ngx_module_t ngx_event_core_module; extern ngx_module_t ngx_epoll_module; extern ngx_module_t ngx_regex_module; extern ngx_module_t ngx_http_module; extern ngx_module_t ngx_http_core_module; ……. |
很显然,这些模块均是在此处用extern进行申明,以表明其他模块可以访问,而对其本身的定义和初始化ngx_module_t结构在其对应的.c文件中进行。例如,ngx_core_module模块便是在src/core/nginx.c文件中定义并进行静态初始化。实际上,ngx_core_module是一个全局的结构体对象,其他模块类同。
8.3. 模块描述
8.3.1. 数据结构
8.3.1.1. ngx_module_t结构
nginx的模块化架构最基本的数据结构为ngx_module_t,因此,此处,我们先分析这个结构,在./src/core/ngx_conf_file.h文件中定义。如下:
#define NGX_MODULE_V1 0, 0, 0, 0, 0, 0, 1 //该宏用来初始化前7个字段 #define NGX_MODULE_V1_PADDING 0, 0, 0, 0, 0, 0, 0, 0 //该宏用来初始化最后8个字段
struct ngx_module_s{ ngx_uint_t ctx_index; //分类模块计数器 ngx_uint_t index; //模块计数器
ngx_uint_t spare0; ngx_uint_t spare1; ngx_uint_t spare2; ngx_uint_t spare3;
ngx_uint_t version; //版本 void *ctx; //该模块的上下文,每个种类的模块有不同的上下文 ngx_command_t *commands; //该模块的命令集,指向一个ngx_command_t结构数组 ngx_uint_t type; //该模块的种类,为core/event/http/mail中的一种
//以下是一些callback函数 ngx_uint_t (*init_master)(ngx_log_t *log); //初始化master ngx_uint_t (*init_module)(ngx_cycle_t *cycle); //初始化模块 ngx_uint_t (*init_process)(ngx_cycle_t *cycle); //初始化工作进程 ngx_uint_t (*init_thread)(ngx_cycle_t *cycle); //初始化线程 void (*exit_thread)(ngx_cycle_t *cycle); //退出线程 void (*exit_process)(ngx_cycle_t *cycle); //退出工作进程 void (*exit_master)(ngx_cycle_t *cycle); //退出master
uintptr_t spare_hook0; //这些字段貌似没用过 uintptr_t spare_hook1; uintptr_t spare_hook2; uintptr_t spare_hook3; uintptr_t spare_hook4; uintptr_t spare_hook5; uintptr_t spare_hook6; uintptr_t spare_hook7; }; |
其中,init_master, init_module, init_process, init_thread, exit_thread, exit_process, exit_master分别在初始化master、初始化模块、初始化工作进程、初始化线程、退出线程、退出工作进程、退出master时被调用。
8.3.1.2. ngx_command_t结构
模块的命令集commands指向一个ngx_command_t结构数组,在src/core/ngx_conf_file.h文件中定义。如下:
struct ngx_command_s { ngx_str_t name; //命令名 ngx_uint_t type; //命令类型 char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); ngx_uint_t conf; ngx_uint_t offset; void *post; };
#define ngx_null_command { ngx_null_string, 0, NULL, 0, 0, NULL } //空命令 |
8.3.2. 模块类图
nginx为C语言开发的开源高性能web server,其代码中大量使用了callback方式,例如模块结构ngx_module_t中的init_master等。实际上,我们可以将ngx_module_t看作C++的一个类,其中的数据字段便是其属性,而那些callback便是该类的操作。——这应该就是nginx的模块化思想。画出的ngx_module_t的类图如下:
8.4. 组织模块
8.4.1. 全局数组ngx_modules
nginx拥有几十个模块,那么,这些模块是如保存在一个全局指针数组ngx_modules[]中,数组的每一个元素均为一个全局ngx_module_t对象的指针。如下。请参考objs/ngx_modules.c文件中的定义:
ngx_module_t *ngx_modules[] = { &ngx_core_module, &ngx_errlog_module, &ngx_conf_module, &ngx_events_module, &ngx_event_core_module, …. } |
8.4.2. 组织结构图
这些模块的组织结构图如下所示,因模块较多,图中只画出一部分有代表性的重要模块:
8.5. 模块种类
在对全局数组ngx_modules进行初始化时,即对每一个模块进行了静态初始化。其中对模块的type字段的初始化是通过以下几个宏进行的。
(1) 文件./src/core/ngx_conf_file.h
#define NGX_CORE_MODULE 0x45524F43 /* "CORE" */ #define NGX_CONF_MODULE 0x464E4F43 /* "CONF" */ |
(2) 文件./src/event/ngx_event.h
#define NGX_EVENT_MODULE 0x544E5645 /* "EVNT" */ |
(3) 文件./src/http/ngx_http_config.h
#define NGX_HTTP_MODULE 0x50545448 /* "HTTP" */ |
即模块种类宏,定义为一个十六进制的数,这个十六进制的数就是其类型对应的ASCII码。因此,nginx共有4种类型的模块,分别为"CORE","CONF","EVNT","HTTP"。
实际上,如果在configure阶段,使用了"--with-mail"参数,mail模块将被编译进来,其对应的宏如下。
#define NGX_MAIL_MODULE 0x4C49414D /* "MAIL" */ |
因此,严格来讲,nginx有5中类型的模块,"CORE","CONF","EVNT","HTTP","MAIL"。
8.6. 初始化模块
8.6.1. 静态初始化
即编译期间完成的数据成员初始化。记mname为某个模块的名字,其静态初始化过程如下。
(1) 用宏NGX_MODULE_V1初始化前7个字段
(2) 用全局对象ngx_mname_module_ctx的地址初始化ctx指针
(3) 用全局数组ngx_mname_commands[]初始化commands指针
(4) 用宏NGX_CORE_MODULE等初始化type字段
(5) 初始化init_master等callback
(6) 用宏NGX_MODULE_V1_PADDING初始化最后8个字段
由此可见,在定义该模块(全局结构对象)时,将其ctx_index和index均初始化为0。因此,模块的静态初始化(数据成员初始化)实际上只是对模块上下文、模块命令集和模块类型进行初始化。
8.6.2. 动态初始化
即nginx运行(启动)初期,对模块本身的初始化。
8.6.2.1. index字段的初始化
对各个模块的index字段的初始化是在main函数中进行的,如下。
ngx_max_module = 0; for (i = 0; ngx_modules[i]; i++) { ngx_modules[i]->index = ngx_max_module++; } |
可见,该for-loop执行后,每个模块的index值便是其在ngx_modules[]数组中的下标值,且全局变量ngx_max_module为模块个数。
8.6.2.2. ctx_index字段的初始化
(1) "EVNT"类型的模块
static char * ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { /* …. */ ngx_event_max_module = 0; for (i = 0; ngx_modules[i]; i++) { if (ngx_modules[i]->type ! = NGX_EVENT_MODULE) { continue; }
ngx_modules[i]->ctx_index = ngx_event_max_module++; } /* ….. */ } |
(2) "HTTP"类型的模块
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { /* .... */ ngx_http_max_module = 0; for (m = 0; ngx_modules[m]; m++) { if (ngx_modules[m]->type ! = NGX_HTTP_MODULE) { continue; }
ngx_modules[m]->ctx_index = ngx_http_max_module++; } /* .... */ } |
(3) "MAIL"类型的模块
static char * ngx_mail_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { /* ... */ ngx_mail_max_module = 0; for (m = 0; ngx_modules[m]; m++) { if (ngx_modules[m]->type ! = NGX_MAIL_MODULE) { continue; }
ngx_modules[m]->ctx_index = ngx_mail_max_module++; } /* ... */ } |
8.6.2.3. 其他初始化
其他的初始化工作,将在nginx启动及其进程启动分析中介绍。
8.7. 小结
本文主要讲述了nginx的模块及其初始化,包括所有模块的组织,及模块的静态初始化和部分动态初始化。
9. 数据结构
9.1. 字符串类型
9.1.1. 简介
Nginx使用自己的、单独的方式去管理字符串类型。
使用的文件:src/core/string.h/c。
9.1.2. 字符串结构
typedef struct { size_t len; //字符串长度 u_char* data; //指向字符串的指针 } ngx_str_t; #define ngx_null_string { 0, NULL } |
字符串类型结构简单,在整个nginx中,到处使用。
9.1.3. 提供的操作
ngx_string | 初始化函数 |
ngx_null_string | 初始化空字符串函数 |
ngx_tolower | 字符转小写函数 |
ngx_toupper | 字符转大写函数 |
ngx_strncmp | 比较指定长度的字符串是否相同 |
ngx_strcmp | 比较字符串是否相同 |
ngx_strstr | 从字符串中找到需要的字符串 |
ngx_strlen | 字符串的长度 |
ngx_strchr | 在字符串中找到匹配的字符,返回 0为匹配 |
ngx_strlchr | 在字符串中找到匹配的字符,返回匹配的指针 |
ngx_memzero | 把一片内存区设置为0 |
ngx_memset | 把一片内存区设置为指定的数 |
ngx_memcpy | 复制内存,没有返回 |
ngx_cpymem | 复制内存,返回复制完了dst的最后一个字符的下一个字符的指针 |
ngx_copy | 同ngx_cpymem |
ngx_memcmp | 比较内存中的数据是否相同 |
ngx_strlow | 把字符串都转换成小写 |
ngx_cpystrn | 复制字符串,并且返回字符串的最后一个字符的下一个字符的指针 |
ngx_pstrdup | 复制字符串到pool,返回字符串的指针 |
ngx_sprintf | 把各种类型的数据格式化输出到buf,最大的长度为65536 |
ngx_snprintf | 把各种类型的数据格式化输出到指定长度的buf |
ngx_strcasecmp | 不分大小写比较两个字符串是否相同 |
ngx_strncasecmp | 指定长短不分大小写比较两个字符串是否相同 |
ngx_strnstr | 在指定大小一个字符串中是否有子字符串 |
ngx_strstrn | 在一个字符串中是否有子指定大小的字符串 |
ngx_strcasestrn | 在一个字符串中是否有子指定大小的字符串,不区分大小写 |
ngx_rstrncmp | 从后往前比较两个字符串是否相同,返回相同的位置 |
ngx_rstrncasecmp | 从后往前比较两个字符串是否相同,返回相同的位置,不区分大小写 |
ngx_memn2cmp | 比较两个指定长度的内存是否相同,也比较长的内存是否包含短的内存 |
ngx_atoi | 指定长度的字符串转换成数字 |
ngx_atosz | 指定长度的字符串转换成ssize_t类型数字 |
ngx_atoof | 指定长度的字符串转换成off_t类型数字 |
ngx_atotm | 指定长度的字符串转换成time_t类型数字 |
ngx_hextoi | 指定长度的字符串转换成十六进制数字 |
ngx_hex_dump | 把数字转换成16进制的字符串 |
ngx_encode_base64 | base64编码 |
ngx_decode_base64 | base64解码 |
ngx_utf8_decode | 把 utf8字符解码成双字节的 unicode或是单字节字符,但是该函数会移动*p的值 |
ngx_utf8_length | 得到utf8编码的字符占几个字节 |
ngx_utf8_cpystrn | 赋值utf8字符串,保证完整的复制 |
ngx_escape_uri | 对uri进行编码 |
ngx_unescape_uri | 对uri的进行解码 |
ngx_escape_html | 对html进行编码 |
ngx_sort | 排序,主要是用于数组排序 |
ngx_qsort | 快速排序 |
ngx_value | 把宏数字转换成字符串 |
9.2. 内存池ngx_pool_t
9.2.1. 简介
nginx对内存的管理由其自己实现的内存池结构ngx_pool_t来完成,本文重点叙述nginx的内存管理。
nginx内存管理相关文件:
(1) src/os/unix/ngx_alloc.h/.c
内存相关的操作,封装了最基本的内存分配函数。
如free/malloc/memalign/posix_memalign,分别被封装为ngx_free,ngx_alloc/ngx_calloc, ngx_memalign
l ngx_alloc:封装malloc分配内存
l ngx_calloc:封装malloc分配内存,并初始化空间内容为0
l ngx_memalign:返回基于一个指定alignment的大小为size的内存空间,且其地址为alignment的整数倍,alignment为2的幂。
(2) ./src/core/ngx_palloc.h/.c
l 封装创建/销毁内存池,从内存池分配空间等函数
9.2.2. 内存池结构
nginx对内存的管理均统一完成,例如,在特定的生命周期统一建立内存池(如main函数系统启动初期即分配1024B大小的内存池),需要内存时统一分配内存池中的内存,在适当的时候释放内存池的内存(如关闭http链接时调用ngx_destroy_pool进行销毁)。
因此,开发者只需在需要内存时进行申请即可,不用过多考虑内存的释放等问题,大大提高了开发的效率。先看一下内存池结构。
9.2.2.1. ngx_pool_t结构
此处统一一下概念,内存池的数据块:即分配内存在这些数据块中进行,一个内存池可以有多一个内存池数据块。nginx的内存池结构如下:
typedef struct { //内存池的数据块位置信息 u_char *last; //当前内存池分配到此处,即下一次分配从此处开始 u_char *end; //内存池结束位置 ngx_pool_t *next; //内存池里面有很多块内存,这些内存块就是通过该指针连成链表的 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_chain_t结构 ngx_pool_large_t *large; //大块内存链表,即分配空间超过max的内存 ngx_pool_cleanup_t *cleanup; //释放内存池的callback ngx_log_t *log; //日志信息 }; |
其中,sizeof(ngx_pool_data_t)=16B,sizeof(ngx_pool_t)=40B。
nginx将几乎所有的结构体放在ngx_core.h文件中重新进行了申明,如下:
typedef struct ngx_module_s ngx_module_t; typedef struct ngx_conf_s ngx_conf_t; typedef struct ngx_cycle_s ngx_cycle_t; typedef struct ngx_pool_s ngx_pool_t; typedef struct ngx_chain_s ngx_chain_t; typedef struct ngx_log_s ngx_log_t; typedef struct ngx_array_s ngx_array_t; typedef struct ngx_open_file_s ngx_open_file_t; typedef struct ngx_command_s ngx_command_t; typedef struct ngx_file_s ngx_file_t; typedef struct ngx_event_s ngx_event_t; typedef struct ngx_event_aio_s ngx_event_aio_t; typedef struct ngx_connection_s ngx_connection_t; |
9.2.2.2. 其他相关结构
其他与内存池相干的数据结构,如清除资源的cleanup链表,分配的大块内存链表等,如下。
/* * NGX_MAX_ALLOC_FROM_POOL should be (ngx_pagesize - 1), i.e. 4095 on x86. * On Windows NT it decreases a number of locked pages in a kernel. */ #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) //在x86体系结构下,该值一般为4096B,即4K #define NGX_DEFAULT_POOL_SIZE (16* 1024) #define NGX_POOL_ALIGNMENT 16 #define NGX_MIN_POOL_SIZE ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), NGX_POOL_ALIGNMENT)
typedef void (*ngx_pool_cleanup_pt)(void *data); //cleanup的callback类型
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; //下一个cleanup callback };
typedef struct ngx_pool_large_s ngx_pool_large_t; struct ngx_pool_large_s{ ngx_pool_large_t *next; //指向下一块大块内存 void *alloc; //指向分配的大块内存 }; ... ... typedef struct { ngx_fd_t fd; u_char *name; ngx_log_t *log; } ngx_pool_cleanup_file_t; |
(gdb) p getpagesize()
$18 = 4096
全局变量ngx_pagesize的初始化是在如下函数中完成的。src/os/unix/ngx_posix_init.c
ngx_int_t ngx_os_init(ngx_log_t *log) { ngx_uint_t n;
#if (NGX_HAVE_OS_SPECIFIC_INIT) if (ngx_os_specific_init(log) != NGX_OK) { return NGX_ERROR; } #endif ngx_init_setproctitle(log); /** 该函数为glibc的库函数,由系统调用实现, 返回内核中的PAGE_SIZE,该值依赖体系结构**/ ngx_pagesize = getpagesize(); ngx_cacheline_size = NGX_CPU_CACHE_LINE; ... } |
这些数据结构之间的关系,请参考后面的图。
9.2.2.3. ngx_pool_t的逻辑结构
这些数据结构逻辑结构图如下。注:本文采用UML的方式画出该图。
9.2.3. 创建内存池
创建内存池有ngx_create_pool()函数完成,代码如下:
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; } //last指向ngx_pool_t结构体之后数据取起始位置 p->d.last = (u_char *) p + sizeof(ngx_pool_t); //end指向分配的整个size大小的内存的末尾 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; //最大不超过4095B p->current = p; p->chain = NULL; p->large = NULL; p->cleanup = NULL; p->log = log; return p; } |
例如,调用ngx_create_pool(1024, 0x80d1c4c)后,创建的内存池物理结构如下图:
9.2.4. 销毁内存池
销毁内存池由如下函数完成:void ngx_destroy_pool(ngx_pool_t *pool)。
该函数将遍历内存池链表,所有释放内存,如果注册了clenup(也是一个链表结构),亦将遍历该cleanup链表结构依次调用clenup的handler清理。同时,还将遍历large链表,释放大块内存。
9.2.5. 重置内存池
重置内存池由下面的函数完成:void ngx_reset_pool(ngx_pool_t *pool)。
该函数将释放所有large内存,并且将d->last指针重新指向ngx_pool_t结构之后数据区的开始位置,同刚创建后的位置相同。
9.2.6. 分配内存
内存分配的函数如下。
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);
返回值为分配的内存起始地址。选择其中的两个函数进行分析,其他的也很好理解,省略。
9.2.6.1. ngx_palloc()函数分析
ngx_palloc()代码如下,分析请参考笔者所加的注释。
void * ngx_palloc(ngx_pool_t *pool, size_t size) { u_char *m; ngx_pool_t *p; //判断待分配内存与max值 if (size <= pool->max) { //小于max值,则从current节点开始遍历pool链表 p = pool->current; do { m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT); if ((size_t) (p->d.end - m) >= size) { //在该节点指向的内存块中分配size大小的内存 p->d.last = m + size; return m; } p = p->d.next; } while (p); //链表里没有能分配size大小内存的节点,则生成一个新的节点并在其中分配内存 return ngx_palloc_block(pool, size); } //大于max值,则在large链表里分配内存 return ngx_palloc_large(pool, size); } |
例如,在之前说明,创建的内存池中分配200B的内存,调用ngx_palloc(pool, 200)后,该内存池物理结构如下图。
9.2.6.2. ngx_palloc_block()函数分析
ngx_palloc_block函数代码如下,分析请参考笔者所加的注释。
static void * ngx_palloc_block(ngx_pool_t *pool, size_t size) { u_char *m; size_t psize; ngx_pool_t *p, *new, *current;
//计算pool的大小 psize = (size_t) (pool->d.end - (u_char *) pool); //分配一块与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; //设置end指针 new->d.next = NULL; new->d.failed = 0;
//让m指向该块内存ngx_pool_data_t结构体之后数据区起始位置 m += sizeof(ngx_pool_data_t); //按4字节对齐 m = ngx_align_ptr(m, NGX_ALIGNMENT); //在数据区分配size大小的内存并设置last指针 new->d.last = m + size;
current = pool->current; for (p = current; p->d.next; p = p->d.next) { if (p->d.failed++ > 4) { //failed的值只在此处被修改 current = p->d.next; //失败4次以上移动current指针 } }
p->d.next = new; //将这次分配的内存块new加入该内存池 pool->current = current ? current : new; return m; } |
注意:该函数分配一块内存后,last指针指向的是ngx_pool_data_t结构体(大小16B)之后数据区的起始位置。而创建内存池时时,last指针指向的是ngx_pool_t结构体(大小40B)之后数据区的起始位置。
9.2.7. 释放内存
请参考如下函数:ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)。
需要注意的是该函数只释放large链表中注册的内存,普通内存在ngx_destroy_pool中统一释放。
9.2.8. 注册cleanup
请参考如下函数,该函数实现也很简单,此处不再赘述。
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
9.2.9. 内存池的物理结构
针对本文上节的例子,画出的内存池的物理结构如下图。
从该图也能看出结论,即内存池第一块内存前40字节为ngx_pool_t结构,后续加入的内存块前16个字节为ngx_pool_data_t结构,这两个结构之后便是真正可以分配内存区域。
因此,本文Reference中的内存分配相关中的图是有一点点小问题的,并不是每一个节点的前面都是ngx_pool_t结构。
9.2.10. 一个例子
理解并掌握开源软件的最好方式莫过于自己写一些测试代码,或者改写软件本身,并进行调试来进一步理解开源软件的原理和设计方法。本节给出一个创建内存池并从中分配内存的简单例子。
9.2.10.1. 代码
/** * ngx_pool_t test, to test ngx_palloc, ngx_palloc_block, ngx_palloc_large */
#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, ...) { }
void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } }
int main() { ngx_pool_t *pool;
printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc block 1 from the pool:\n"); printf("--------------------------------\n"); ngx_palloc(pool, 512); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc block 2 from the pool:\n"); printf("--------------------------------\n"); ngx_palloc(pool, 512); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc block 3 from the pool :\n"); printf("--------------------------------\n"); ngx_palloc(pool, 512); dump_pool(pool);
ngx_destroy_pool(pool); return 0; } |
9.2.10.2. 7.4.2、如何编译
这个问题是编写测试代码或者改写软件本身最迫切需要解决的问题,否则,编写的代码无从编译或运行,那也无从进行调试并理解软件了。
如何对自己编写的测试代码进行编译,可参考Linux平台代码覆盖率测试-编译过程自动化及对链接的解释、Linux平台如何编译使用Google test写的单元测试?。我们要做的是学习这种编译工程的方法,针对该例子,笔者编写的makefile文件如下。——这便是本节的主要目的。
CXX = gcc CXXFLAGS += -g -Wall -Wextra
NGX_ROOT = /usr/src/nginx-1.0.4
TARGETS = ngx_pool_t_test TARGETS_C_FILE = $(TARGETS).c
CLEANUP = rm -f $(TARGETS) *.o
all: $(TARGETS)
clean: $(CLEANUP)
CORE_INCS = -I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \
NGX_PALLOC = $(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING = $(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC = $(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o
$(TARGETS): $(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $^ -o $@ |
9.2.10.3. 7.4.3、运行运行结果
# ./ngx_pool_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8922020 .d .last = 0x8922048 .end = 0x8922420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984
-------------------------------- alloc block 1 from the pool: -------------------------------- pool = 0x8922020 .d .last = 0x8922248 .end = 0x8922420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 472
-------------------------------- alloc block 2 from the pool: -------------------------------- pool = 0x8922020 .d .last = 0x8922248 .end = 0x8922420 .next = 0x8922450 .failed = 0 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 472
pool = 0x8922450 .d .last = 0x8922660 .end = 0x8922850 .next = 0x0 .failed = 0 .max = 0 .current = 0x0 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 496
-------------------------------- alloc block 3 from the pool : -------------------------------- pool = 0x8922020 .d .last = 0x8922248 .end = 0x8922420 .next = 0x8922450 .failed = 1 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 472
pool = 0x8922450 .d .last = 0x8922660 .end = 0x8922850 .next = 0x8922880 .failed = 0 .max = 0 .current = 0x0 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 496
pool = 0x8922880 .d .last = 0x8922a90 .end = 0x8922c80 .next = 0x0 .failed = 0 .max = 0 .current = 0x0 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 496 |
9.2.11. 小结
本文针对nginx-1.0.4的内存管理进行了较为全面的分析,包括相关内存池数据结构,内存池的创建、销毁,以及从内存池中分配内存等。最后通过一个简单例子向读者展示nginx内存池的创建和分配操作,同时借此向读者展示编译测试代码的方法。
分析完nginx的内存管理,你一定惊叹于nginx作者的聪明才智。这种内存管理的设计方法小巧、快捷,值得借鉴!
9.3. 数组结构ngx_array_t
9.3.1. 简介
本文开始介绍nginx的容器,先从最简单的数组开始。
数组实现文件:文件:./src/core/ngx_array.h/.c。
9.3.2. 数组结构
9.3.2.1. ngx_array_t结构
nginx的数组结构为ngx_array_t,定义如下:
struct ngx_array_s { void *elts; //数组数据区起始位置 ngx_uint_t nelts; //实际存放的元素个数 size_t size; //每个元素大小 ngx_uint_t nalloc; //数组所含空间个数,即实际分配的小空间的个数 ngx_pool_t *pool; //该数组在此内存池中分配 }; typedef struct ngx_array_s ngx_array_t; |
sizeof(ngx_array_t)=20。由其定义可见,nginx的数组也要从内存池中分配。将分配nalloc个大小为size的小空间,实际分配的大小为(nalloc * size)。
9.3.2.2. ngx_array_t的逻辑结构
ngx_array_t结构引用了ngx_pool_t结构,因此本文参考内存池结构ngx_pool_t及内存管理一文画出相关结构的逻辑图,如下:
9.3.3. 数组操作
数组操作共有5个,如下:
//创建数组 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);
//初始化数组 Static ngx_inline ngx_int_t ngx_array_init(ngx_array_t*array, ngx_pool_t *pool, ngx_uint_t n, size_t size) |
因实现都很简单,本文简单分析前3个函数。
9.3.3.1. 创建数组
创建数组的操作实现如下,首先分配数组头(20B),然后分配数组数据区,两次分配均在传入的内存池(pool指向的内存池)中进行。然后简单初始化数组头并返回数组头的起始位置。
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; } a->elts = ngx_palloc(p,n * size); //接着分配n*size大小的区域作为数组数据区 if (a->elts == NULL) { return NULL; } a->nelts = 0; //初始化 a->size = size; a->nalloc = n; a->pool = p; return a; //返回数组头的起始位置 } |
创建数组后内存池的物理结构图如下。
9.3.3.2. 销毁数组
销毁数组的操作实现如下,包括销毁数组数据区和数组头。这里的销毁动作实际上就是修改内存池的last指针,并没有调用free等释放内存的操作,显然,这种维护效率是很高的。
void ngx_array_destroy(ngx_array_t*a) { ngx_pool_t *p; p = a->pool; 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; //设置内存池的last指针 } } |
9.3.3.3. 添加元素
向数组添加元素的操作有两个,ngx_array_push和ngx_array_push_n,分别添加一个和多个元素。
但实际的添加操作并不在这两个函数中完成,例如ngx_array_push返回可以在该数组数据区中添加这个元素的位置,ngx_array_push_n则返回可以在该数组数据区中添加n个元素的起始位置,而添加操作即在获得添加位置之后进行,如后文的例子。
void * ngx_array_push(ngx_array_t*a) { void *elt, *new; size_t size; ngx_pool_t *p;
//数组数据区满 if (a->nelts ==a->nalloc) { /* the arrayis full */ //计算数组数据区的大小 size = a->size *a->nalloc; p = a->pool; //若1.内存池的last指针指向数组数据区的末尾 //且2.内存池未使用的区域可以再分配一个size大小的小空间 if ((u_char *)a->elts + size == p->d.last &&p->d.last + a->size <= p->d.end) { /* * the array allocation is the lastin the pool * and there is space for newallocation */ //分配一个size大小的小空间(a->size为数组一个元素的大小) p->d.last +=a->size; //实际分配小空间的个数加1 a->nalloc++; } else { /* allocate a new array */ //否则,扩展数组数据区为原来的2倍 new =ngx_palloc(p, 2 * size); if (new == NULL) { return NULL; } //将原来数据区的内容拷贝到新的数据区 ngx_memcpy(new,a->elts, size); a->elts = new; //注意:此处转移数据后,并未释放原来的数据区,内存池将统一释放 a->nalloc *= 2; } } //数据区中实际已经存放数据的子区的末尾 elt = (u_char *)a->elts + a->size * a->nelts; //即最后一个数据末尾,该指针就是下一个元素开始的位置 a->nelts++; //返回该末尾指针,即下一个元素应该存放的位置 return elt; } |
由此可见,向数组中添加元素实际上也是在修该内存池的last指针(若数组数据区满)及数组头信息,即使数组满了,需要扩展数据区内容,也只需要内存拷贝完成,并不需要数据的移动操作,这个效率也是相当高的。
下图是向数组中添加10个整型元素后的一个例子。代码可参考下文的例子。当然,数组元素也不仅限于例子的整型数据,也可以是其他类型的数据,如结构体等。
9.3.4. 一个例子
9.3.4.1. 代码
/** * ngx_array_t test, to test ngx_array_create, ngx_array_push */
#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, ...) { }
void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } }
void dump_array(ngx_array_t* a) { if (a) { printf("array = 0x%x\n", a); printf(" .elts = 0x%x\n", a->elts); printf(" .nelts = %d\n", a->nelts); printf(" .size = %d\n", a->size); printf(" .nalloc = %d\n", a->nalloc); printf(" .pool = 0x%x\n", a->pool);
printf("elements: "); int *ptr = (int*)(a->elts); for (; ptr < (int*)(a->elts + a->nalloc * a->size); ) { printf("0x%x ", *ptr++); } printf("\n"); } }
int main() { ngx_pool_t *pool; int i;
printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc an array from the pool:\n"); printf("--------------------------------\n"); ngx_array_t *a = ngx_array_create(pool, 10, sizeof(int)); dump_pool(pool);
for (i = 0; i < 10; i++) { int *ptr = ngx_array_push(a); *ptr = i + 1; }
dump_array(a);
ngx_array_destroy(a); ngx_destroy_pool(pool); return 0; } |
9.3.4.2. 如何编译
CXX = gcc CXXFLAGS +=-g -Wall -Wextra
NGX_ROOT =/usr/src/nginx-1.0.4
TARGETS =ngx_array_t_test TARGETS_C_FILE= $(TARGETS).c
CLEANUP = rm-f $(TARGETS) *.o
all:$(TARGETS)
clean: $(CLEANUP)
CORE_INCS =-I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \
NGX_PALLOC =$(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING =$(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC =$(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_ARRAY =$(NGX_ROOT)/objs/src/core/ngx_array.o
$(TARGETS):$(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING)$(NGX_ALLOC) $(NGX_ARRAY) $^ -o $@ |
9.3.4.3. 运行结果
# ./ngx_array_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x860b020 .d .last = 0x860b048 .end = 0x860b420 .next = 0x0 .failed = 0 .max = 984 .current = 0x860b020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984 -------------------------------- alloc an array from the pool: -------------------------------- pool = 0x860b020 .d .last = 0x860b084 .end = 0x860b420 .next = 0x0 .failed = 0 .max = 984 .current = 0x860b020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 924 array = 0x860b048 .elts = 0x860b05c .nelts = 10 .size = 4 .nalloc = 10 .pool = 0x860b020 elements: 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xa |
9.3.5. 小结
本文针对数组结构进行了较为全面的分析,包括数组相关数据结构,数组的创建、销毁,以及向数组中添加元素等。最后通过一个简单例子向读者展示nginx数组的创建、添加元素和销毁操作,同时借此向读者展示编译测试代码的方法。
9.4. 链表结构ngx_list_t
9.4.1. 简介
本文继续介绍nginx的容器——链表。
链表实现文件:文件:src/core/ngx_list.h/.c。
9.4.2. 链表结构
9.4.2.1. ngx_list_t结构
nginx的链表(头)结构为ngx_list_t,链表节点结构为ngx_list_part_t,定义如下:
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; |
其中,sizeof(ngx_list_t)=28,sizeof(ngx_list_part_t)=12。
由此可见,nginx的链表,也要从内存池中分配。对于每一个节点(list part)将分配nalloc个大小为size的小空间,实际分配的大小为(nalloc * size)。
9.4.2.2. ngx_list_t的逻辑结构
ngx_list_t结构引用了ngx_pool_t结构,因此本文参考内存池结构ngx_pool_t及内存管理一文画出相关结构的逻辑图,如下:
9.4.3. 链表操作
链表操作共3个,如下:
//创建链表 ngx_list_t*ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);
//初始化链表 static ngx_inline ngx_int_t ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_tn, size_t size);
//添加元素 void*ngx_list_push(ngx_list_t *l) |
9.4.3.1. 创建链表
创建链表的操作实现如下,首先分配链表头(28B),然后分配头节点(即链表头中包含的part)数据区,两次分配均在传入的内存池(pool指向的内存池)中进行。然后简单初始化链表头并返回链表头的起始位置。
ngx_list_t * ngx_list_create(ngx_pool_t*pool, ngx_uint_t n, size_t size) { ngx_list_t *list; list = ngx_palloc(pool,sizeof(ngx_list_t)); //从内存池中分配链表头 if (list == NULL) { return NULL; } list->part.elts =ngx_palloc(pool, n * size); //接着分配n*size大小的区域作为链表数据区 if (list->part.elts == NULL) { return NULL; } list->part.nelts = 0; //初始化 list->part.next = NULL; list->last = &list->part; list->size = size; list->nalloc = n; list->pool = pool; return list; //返回链表头的起始位置 } |
创建链表后内存池的物理结构图如下:
9.4.3.2. 添加元素
添加元素操作实现如下,同nginx数组实现类似,其实际的添加操作并不在该函数中完成。函数ngx_list_push返回可以在该链表数据区中放置元素(元素可以是1个或多个)的位置,而添加操作即在获得添加位置之后进行,如后文的例子。
void * ngx_list_push(ngx_list_t*l) { void *elt; ngx_list_part_t *last; last = l->last; if (last->nelts ==l->nalloc) { //链表数据区满 /* the last part is full, allocate anew list part */ last =ngx_palloc(l->pool, sizeof(ngx_list_part_t)); //分配节点(list part) if (last == NULL) { return NULL; } last->elts =ngx_palloc(l->pool, l->nalloc * l->size);//分配该节点(part)的数据区 if (last->elts == NULL) { return NULL; } last->nelts = 0; last->next = NULL; l->last->next =last; //将分配的list part插入链表 l->last = last; //并修改list头的last指针 }
elt = (char *)last->elts + l->size * last->nelts; //计算下一个数据在链表数据区中的位置 last->nelts++; //实际存放的数据个数加1
return elt; //返回该位置 } |
由此可见,向链表中添加元素实际上就是从内存池中分配链表节点(part)及其该节点的实际数据区,并修改链表节点(part)信息。
注1:与数组的区别,数组数据区满时要扩充数据区空间;而链表每次要分配节点及其数据区。
注2:链表的每个节点(part)的数据区中可以放置1个或多个元素,这里的元素可以是一个整数,也可以是一个结构。
下图是一个有3个节点的链表的逻辑结构图。
图中的线太多,容易眼晕,下面这个图可能好一些。
9.4.4. 一个例子
9.4.4.1. 代码
/** * ngx_list_t test, to test ngx_list_create, ngx_list_push */
#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_list.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, ...) { }
void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } }
void dump_list_part(ngx_list_t* list, ngx_list_part_t* part) { int *ptr = (int*)(part->elts); int loop = 0;
printf(" .part = 0x%x\n", &(list->part)); printf(" .elts = 0x%x ", part->elts); printf("("); for (; loop < list->nalloc - 1; loop++) { printf("0x%x, ", ptr[loop]); } printf("0x%x)\n", ptr[loop]); printf(" .nelts = %d\n", part->nelts); printf(" .next = 0x%x", part->next); if (part->next) printf(" -->\n"); printf(" \n"); }
void dump_list(ngx_list_t* list) { if (list == NULL) return;
printf("list = 0x%x\n", list); printf(" .last = 0x%x\n", list->last); printf(" .part = 0x%x\n", &(list->part)); printf(" .size = %d\n", list->size); printf(" .nalloc = %d\n", list->nalloc); printf(" .pool = 0x%x\n\n", list->pool);
printf("elements:\n");
ngx_list_part_t *part = &(list->part); while (part) { dump_list_part(list, part); part = part->next; } printf("\n"); }
int main() { ngx_pool_t *pool; int i;
printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc an list from the pool:\n"); printf("--------------------------------\n"); ngx_list_t *list = ngx_list_create(pool, 5, sizeof(int)); dump_pool(pool);
for (i = 0; i < 15; i++) { int *ptr = ngx_list_push(list); *ptr = i + 1; }
printf("--------------------------------\n"); printf("the list information:\n"); printf("--------------------------------\n"); dump_list(list);
printf("--------------------------------\n"); printf("the pool at the end:\n"); printf("--------------------------------\n"); dump_pool(pool);
ngx_destroy_pool(pool); return 0; } |
9.4.4.2. 如何编译
CXX = gcc CXXFLAGS +=-g -Wall -Wextra
NGX_ROOT =/usr/src/nginx-1.0.4
TARGETS =ngx_list_t_test TARGETS_C_FILE= $(TARGETS).c
CLEANUP = rm-f $(TARGETS) *.o
all:$(TARGETS)
clean: $(CLEANUP)
CORE_INCS =-I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \
NGX_PALLOC =$(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING =$(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC =$(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_LIST =$(NGX_ROOT)/objs/src/core/ngx_list.o
$(TARGETS):$(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING)$(NGX_ALLOC) $(NGX_LIST) $^ -o $@ |
9.4.4.3. 运行结果
# ./ngx_list_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x9208020 .d .last = 0x9208048 .end = 0x9208420 .next = 0x0 .failed = 0 .max = 984 .current = 0x9208020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984 -------------------------------- alloc an list from the pool: -------------------------------- pool = 0x9208020 .d .last = 0x9208078 .end = 0x9208420 .next = 0x0 .failed = 0 .max = 984 .current = 0x9208020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 936 -------------------------------- the list information: -------------------------------- list = 0x9208048 .last = 0x9208098 .part = 0x920804c .size = 4 .nalloc = 5 .pool = 0x9208020 elements: .part = 0x920804c .elts = 0x9208064 (0x1, 0x2, 0x3, 0x4, 0x5) .nelts = 5 .next = 0x9208078 --> .part = 0x920804c .elts = 0x9208084 (0x6, 0x7, 0x8, 0x9, 0xa) .nelts = 5 .next = 0x9208098 --> .part = 0x920804c .elts = 0x92080a4 (0xb, 0xc, 0xd, 0xe, 0xf) .nelts = 5 .next = 0x0 -------------------------------- the pool at the end: -------------------------------- pool = 0x9208020 .d .last = 0x92080b8 .end = 0x9208420 .next = 0x0 .failed = 0 .max = 984 .current = 0x9208020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 872 |
9.4.5. 小结
本文针对nginx-1.0.4的容器——链表结构进行了较为全面的分析,包括链表相关数据结构,链表创建和向链表中添加元素等。最后通过一个简单例子向读者展示nginx链表创建和添加元素操作,同时借此向读者展示编译测试代码的方法。
9.5. 队列结构ngx_queue_t
9.5.1. 简介
链表实现文件:文件:src/core/ngx_queue.h/.c。
9.5.2. 队列结构
nginx的队列是由具有头节点的双向循环链表实现的,每一个节点结构为ngx_queue_t,定义如下:
typedef struct ngx_queue_s ngx_queue_t; struct ngx_queue_s { //队列结构 ngx_queue_t *prev; ngx_queue_t *next; }; |
其中,sizeof(ngx_queue_t)=8。从队列结构定义可以看出,nginx的队列结构里并没有其节点的数据内容。
9.5.3. 队列操作
队列有如下操作:
//初始化队列 ngx_queue_init(q) //判断队列是否为空 ngx_queue_empty(h) //在头节点之后插入新节点 ngx_queue_insert_head(h, x) //在尾节点之后插入新节点 ngx_queue_insert_tail(h, x) //删除节点x ngx_queue_remove(x) //分割队列 ngx_queue_split(h, q, n) //链接队列 ngx_queue_add(h, n) //获取队列的中间节点 ngx_queue_t *ngx_queue_middle(ngx_queue_t *queue) //排序队列(稳定的插入排序) void ngx_queue_sort(ngx_queue_t *queue,ngx_int_t (*cmp)(const ngx_queue_t*, const ngx_queue_t*)) |
其中,插入节点、取队列头、取队列尾等操作由宏实现,获取中间节点、排序等操作由函数实现。
9.5.3.1. 初始化队列
#define ngx_queue_init(q) \ (q)->prev = q; \ (q)->next = q |
9.5.3.2. 在头节点之后插入
在头节点之后插入操作由宏ngx_queue_insert_head完成,如下:
#define ngx_queue_insert_head(h, x) \ (x)->next = (h)->next; \ (x)->next->prev = x; \ (x)->prev = h; \ (h)->next = x |
画出该操作的逻辑图,如下:
图中虚线表示被修改/删除的指针,蓝色表示新修改/增加的指针。
9.5.3.3. 在尾节点之后插入
在尾节点之后插入操作由宏ngx_queue_insert_tail完成,如下:
#define ngx_queue_insert_tail(h, x) \ (x)->prev = (h)->prev; \ (x)->prev->next = x; \ (x)->next = h; \ (h)->prev = x |
该操作的逻辑图如下:
9.5.3.4. 删除节点
在尾节点之后插入操作由宏ngx_queue_remove完成,如下:
#if (NGX_DEBUG) #define ngx_queue_remove(x) \ (x)->next->prev = (x)->prev; \ (x)->prev->next = (x)->next; \ (x)->prev = NULL; \ (x)->next = NULL #else #define ngx_queue_remove(x) \ (x)->next->prev = (x)->prev; \ (x)->prev->next = (x)->next #endif |
该操作的逻辑图如下:
9.5.3.5. 分割队列
分割队列操作由宏ngx_queue_split完成,如下:
#define ngx_queue_split(h, q, n) \ (n)->prev = (h)->prev; \ (n)->prev->next = n; \ (n)->next = q; \ (h)->prev = (q)->prev; \ (h)->prev->next = h; \ (q)->prev = n; |
该宏有3个参数,h为队列头(即链表头指针),将该队列从q节点将队列(链表)分割为两个队列(链表),q之后的节点组成的新队列的头节点为n,图形演示如下:
9.5.3.6. 增加链接队列
链接队列由宏ngx_queue_add完成,操作如下:
#define ngx_queue_add(h, n) \ (h)->prev->next = (n)->next; \ (n)->next->prev = (h)->prev; \ (h)->prev = (n)->prev; \ (h)->prev->next = h; |
其中,h、n分别为两个队列的指针,即头节点指针,该操作将n队列链接在h队列之后。演示图形如下:
宏ngx_queue_split和ngx_queue_add只在http模块locations相关操作中使用,在后续的讨论http模块locations相关操作时再详细叙述。
9.5.3.7. 获取中间节点
中间节点,若队列有奇数个(除头节点外)节点,则返回中间的节点;若队列有偶数个节点,则返回后半个队列的第一个节点。操作如下:
ngx_queue_t * ngx_queue_middle(ngx_queue_t *queue) { ngx_queue_t *middle, *next; middle = ngx_queue_head(queue); if (middle == ngx_queue_last(queue)) { return middle; } next = ngx_queue_head(queue); for ( ;; ) { middle = ngx_queue_next(middle); next = ngx_queue_next(next); if (next == ngx_queue_last(queue)) {//偶数个节点,在此返回后半个队列的第一个节点 return middle; } next = ngx_queue_next(next); if (next == ngx_queue_last(queue)) {//奇数个节点,在此返回中间节点 return middle; } } } |
注意:代码中的next指针,其每次均会后移两个位置(节点),而middle指针每次后移一个位置(节点)。演示图形如下:
9.5.3.8. 队列排序
队列排序采用的是稳定的简单插入排序方法,即从第一个节点开始遍历,依次将当前节点(q)插入前面已经排好序的队列(链表)中,下面程序中,前面已经排好序的队列的尾节点为prev。操作如下:
/* the stable insertion sort */ void ngx_queue_sort(ngx_queue_t *queue, ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *)) { ngx_queue_t *q, *prev, *next; q = ngx_queue_head(queue); if (q == ngx_queue_last(queue)) { return; } for (q = ngx_queue_next(q); q != ngx_queue_sentinel(queue); q = next) { prev = ngx_queue_prev(q); next = ngx_queue_next(q); ngx_queue_remove(q); do { if (cmp(prev, q) <= 0) { //比较 break; } prev = ngx_queue_prev(prev); //prev指针前移 } while (prev != ngx_queue_sentinel(queue)); ngx_queue_insert_after(prev, q); //将q插入prev节点之后(此处即为简单插入) } } |
该排序操作使用前面介绍的宏来完成其插入动作,只是一些简单的修改指针指向的操作,效率较高。
9.5.3.9. 如何获取队列节点数据
由队列基本结构和以上操作可知,nginx的队列操作只对链表指针进行简单的修改指向操作,并不负责节点数据空间的分配。因此,用户在使用nginx队列时,要自己定义数据结构并分配空间,且在其中包含一个ngx_queue_t的指针或者对象,当需要获取队列节点数据时,使用ngx_queue_data宏,其定义如下:
#define ngx_queue_data(q, type, link) \ (type *) ((u_char *) q – offsetof(type, link)) |
由该宏定义可以看出,一般定义队列节点结构(该结构类型为type)时,需要将真正的数据放在前面,而ngx_queue_t结构放在后面,故该宏使用减法计算整个节点结构的起始地址(需要进行类型转换)。
数据结构如下:
9.5.4. 一个例子
给出一个创建内存池并从中分配队列头节点和其他节点组成队列的简单例子。在该例中,队列的数据是一系列的二维点(x,y分别表示该点的横、纵坐标),将这些点插入队列后进行排序,以此向读者展示nginx队列的使用方法。
9.5.4.1. 代码
/** * ngx_queue_t test */
#include <stdio.h> #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_palloc.h" #include "ngx_queue.h"
//2-dimensional point (x, y) queue structure typedef struct{ int x; int y; } my_point_t;
typedef struct{ my_point_t point; ngx_queue_t queue; } my_point_queue_t;
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, ...) { }
void dump_pool(ngx_pool_t* pool) { while (pool){ printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } }
void dump_queue_from_head(ngx_queue_t *que) { ngx_queue_t *q = ngx_queue_head(que); printf("(0x%x: (0x%x, 0x%x)) <==> \n", que, que->prev, que->next); for (; q != ngx_queue_sentinel(que); q = ngx_queue_next(q)){ my_point_queue_t *point = ngx_queue_data(q, my_point_queue_t, queue); printf("(0x%x: (%-2d, %-2d), 0x%x: (0x%x, 0x%x)) <==> \n", point, point->point.x, point->point.y, &point->queue, point->queue.prev, point->queue.next); } }
void dump_queue_from_tail(ngx_queue_t *que) { ngx_queue_t *q = ngx_queue_last(que); printf("(0x%x: (0x%x, 0x%x)) <==> \n", que, que->prev, que->next); for (; q != ngx_queue_sentinel(que); q = ngx_queue_prev(q)) { my_point_queue_t *point = ngx_queue_data(q, my_point_queue_t, queue); printf("(0x%x: (%-2d, %-2d), 0x%x: (0x%x, 0x%x)) <==> \n", point, point->point.x, point->point.y, &point->queue, point->queue.prev, point->queue.next); } }
//sort from small to big ngx_int_t my_point_cmp(const ngx_queue_t* lhs, const ngx_queue_t* rhs) { my_point_queue_t *pt1 = ngx_queue_data(lhs, my_point_queue_t, queue); my_point_queue_t *pt2 = ngx_queue_data(rhs, my_point_queue_t, queue); if (pt1->point.x < pt2->point.x) return 0; else if (pt1->point.x > pt2->point.x) return 1; else if (pt1->point.y < pt2->point.y) return 0; else if (pt1->point.y > pt2->point.y) return 1; return 1; }
#define Max_Num 6
int main() { ngx_pool_t *pool; ngx_queue_t *myque; my_point_queue_t *point; my_point_t points[Max_Num] = { {10, 1}, {20, 9}, {9, 9}, {90, 80}, {5, 3}, {50, 20} }; int i;
printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc a queue head and nodes :\n"); printf("--------------------------------\n"); myque = ngx_palloc(pool, sizeof(ngx_queue_t)); //alloc a queue head ngx_queue_init(myque); //init the queue
//insert some points into the queue for (i = 0; i < Max_Num; i++) { point = (my_point_queue_t*)ngx_palloc(pool, sizeof(my_point_queue_t)); point->point.x = points[i].x; point->point.y = points[i].y; ngx_queue_init(&point->queue);
//insert this point into the points queue ngx_queue_insert_head(myque, &point->queue); }
dump_queue_from_tail(myque); printf("\n");
printf("--------------------------------\n"); printf("sort the queue:\n"); printf("--------------------------------\n"); ngx_queue_sort(myque, my_point_cmp); dump_queue_from_head(myque); printf("\n");
printf("--------------------------------\n"); printf("the pool at the end:\n"); printf("--------------------------------\n"); dump_pool(pool);
ngx_destroy_pool(pool); return 0; } |
9.5.4.2. 10.4.2、如何编译
CXX = gcc CXXFLAGS += -g -Wall -Wextra NGX_ROOT = /usr/src/nginx-1.0.4 TARGETS = ngx_queue_t_test TARGETS_C_FILE = $(TARGETS).c CLEANUP = rm -f $(TARGETS) *.o all: $(TARGETS) clean: $(CLEANUP) CORE_INCS = -I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \ NGX_PALLOC = $(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING = $(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC = $(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_QUEUE = $(NGX_ROOT)/objs/src/core/ngx_queue.o $(TARGETS): $(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $(NGX_QUEUE) $^ -o $@ |
9.5.4.3. 10.4.3、运行结果
# ./ngx_queue_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8bcf020 .d .last = 0x8bcf048 .end = 0x8bcf420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8bcf020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984
-------------------------------- alloc a queue head and nodes : -------------------------------- (0x8bcf048: (0x8bcf058, 0x8bcf0a8)) <==> (0x8bcf050: (10, 1 ), 0x8bcf058: (0x8bcf068, 0x8bcf048)) <==> (0x8bcf060: (20, 9 ), 0x8bcf068: (0x8bcf078, 0x8bcf058)) <==> (0x8bcf070: (9 , 9 ), 0x8bcf078: (0x8bcf088, 0x8bcf068)) <==> (0x8bcf080: (90, 80), 0x8bcf088: (0x8bcf098, 0x8bcf078)) <==> (0x8bcf090: (5 , 3 ), 0x8bcf098: (0x8bcf0a8, 0x8bcf088)) <==> (0x8bcf0a0: (50, 20), 0x8bcf0a8: (0x8bcf048, 0x8bcf098)) <==>
-------------------------------- sort the queue: -------------------------------- (0x8bcf048: (0x8bcf088, 0x8bcf098)) <==> (0x8bcf090: (5 , 3 ), 0x8bcf098: (0x8bcf048, 0x8bcf078)) <==> (0x8bcf070: (9 , 9 ), 0x8bcf078: (0x8bcf098, 0x8bcf058)) <==> (0x8bcf050: (10, 1 ), 0x8bcf058: (0x8bcf078, 0x8bcf068)) <==> (0x8bcf060: (20, 9 ), 0x8bcf068: (0x8bcf058, 0x8bcf0a8)) <==> (0x8bcf0a0: (50, 20), 0x8bcf0a8: (0x8bcf068, 0x8bcf088)) <==> (0x8bcf080: (90, 80), 0x8bcf088: (0x8bcf0a8, 0x8bcf048)) <==>
-------------------------------- the pool at the end: -------------------------------- pool = 0x8bcf020 .d .last = 0x8bcf0b0 .end = 0x8bcf420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8bcf020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 880 |
该队列的逻辑图如下:
9.5.5. 10.5、小结
本文针对nginx-1.0.4的队列进行了较为全面的分析,包括队列结构和队列操作,队列操作主要包括在头节点之后插入节点、在尾节点之后插入节点、获取中间节点、队列排序等。最后通过一个简单例子向读者展示nginx队列的使用方法,同时借此向读者展示编译测试nginx代码的方法。
9.6. hash结构ngx_hash_t
9.6.1. 简介
链表实现文件:文件:src/core/ngx_hash.h/.c。
9.6.2. hash结构
nginx的hash结构比其list、array、queue等结构稍微复杂一些。
9.6.2.1. ngx_hash_t结构
nginx的hash结构为ngx_hash_t,hash元素结构为ngx_hash_elt_t,定义如下:
typedef struct { //hash元素结构 void *value; //value,即某个key对应的值,即<key,value>中的value u_short len; //name长度 u_char name[1]; //要hash的数据(在nginx中表现为字符串),即<key,value>中的key } ngx_hash_elt_t;
typedef struct { //hash结构 ngx_hash_elt_t **buckets; //hash桶(有size个桶) ngx_uint_t size; //hash桶个数 } ngx_hash_t; |
其中,sizeof(ngx_hash_t) = 8,sizeof(ngx_hash_elt_t) = 8。实际上,ngx_hash_elt_t结构中的name字段就是ngx_hash_key_t结构中的key。这在ngx_hash_init()函数中可以看到,请参考后续的分析。该结构在模块配置解析时经常使用。
9.6.2.2. ngx_hash_init_t结构
nginx的hash初始化结构是ngx_hash_init_t,用来将其相关数据封装起来作为参数传递给ngx_hash_init()或ngx_hash_wildcard_init()函数。这两个函数主要是在http相关模块中使用,例如ngx_http_server_names()函数(优化http Server Names),ngx_http_merge_types()函数(合并httptype),ngx_http_fastcgi_merge_loc_conf()函数(合并FastCGI Location Configuration)等函数或过程用到的参数、局部对象/变量等。这些内容将在后续的文章中讲述。
ngx_hash_init_t结构如下。sizeof(ngx_hash_init_t)=28。
typedef struct { //hash初始化结构 ngx_hash_t *hash; //指向待初始化的hash结构 ngx_hash_key_pt key; //hash函数指针
ngx_uint_t max_size; //bucket的最大个数 ngx_uint_t bucket_size; //每个bucket的空间
char *name; //该hash结构的名字(仅在错误日志中使用) ngx_pool_t *pool; //该hash结构从pool指向的内存池中分配 ngx_pool_t *temp_pool; //分配临时数据空间的内存池 } ngx_hash_init_t; |
9.6.2.3. ngx_hash_key_t结构
该结构也主要用来保存要hash的数据,即键-值对<key,value>,在实际使用中,一般将多个键-值对保存在ngx_hash_key_t结构的数组中,作为参数传给ngx_hash_init()或ngx_hash_wildcard_init()函数。其定义如下:
typedef struct { //hash key结构 ngx_str_t key; //key,为nginx的字符串结构 ngx_uint_t key_hash; //由该key计算出的hash值(通过hash函数如ngx_hash_key_lc()) void *value; //该key对应的值,组成一个键-值对<key,value> } ngx_hash_key_t;
typedef struct { //字符串结构 size_t len; //字符串长度 u_char *data; //字符串内容 } ngx_str_t; |
其中,sizeof(ngx_hash_key_t) = 16。一般在使用中,value指针可能指向静态数据区(例如全局数组、常量字符串)、堆区(例如动态分配的数据区用来保存value值)等。可参考本文后面的例子。
关于ngx_table_elt_t结构和ngx_hash_keys_arrays_t结构,因其对于hash结构本身没有太大作用,主要是为模块配置、referer合法性验证等设计的数据结构,例如http的core模块、map模块、referer模块、SSI filter模块等,此处不再讲述,将在后续的文章中介绍。
9.6.2.4. hash的逻辑结构
ngx_hash_init_t结构引用了ngx_pool_t结构,因此本文参考内存池结构ngx_pool_t及内存管理一文画出相关结构的逻辑图,如下:
9.6.3. hash操作
9.6.3.1. NGX_HASH_ELT_SIZE宏
NGX_HASH_ELT_SIZE宏用来计算上述ngx_hash_elt_t结构大小,定义如下:
#define NGX_HASH_ELT_SIZE(name) //该参数name即为ngx_hash_elt_t结构指针 (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *))) //以4字节对齐 |
在32位平台上,sizeof(void*)=4,(name)->key.len即是ngx_hash_elt_t结构中name数组保存的内容的长度,其中的"+2"是要加上该结构中len字段(u_short类型)的大小。
因此,NGX_HASH_ELT_SIZE(name)=4+ngx_align((name)->key.len + 2, 4),该式后半部分即是(name)->key.len+2以4字节对齐的大小。
9.6.3.2. hash函数
nginx提供的hash函数有以下几种:
#define ngx_hash(key, c) ((ngx_uint_t) key * 31 + c) //hash宏 ngx_uint_t ngx_hash_key(u_char *data, size_t len); ngx_uint_t ngx_hash_key_lc(u_char *data, size_t len); //lc表示lower case,即字符串转换为小写后再计算hash值 ngx_uint_t ngx_hash_strlow(u_char *dst, u_char *src, size_t n); |
hash函数都很简单,以上3个函数都会调用ngx_hash宏,该宏返回一个(长)整数。此处介绍第一个函数,定义如下:
ngx_uint_t ngx_hash_key(u_char *data, size_t len) { ngx_uint_t i, key; key = 0; for (i = 0; i < len; i++) { key = ngx_hash(key, data[i]); } return key; } |
因此,ngx_hash_key函数的计算可表述为下列公式。
Key[0] = data[0] Key[1] = data[0]*31 + data[1] Key[2] = (data[0]*31 + data[1])*31 + data[2] ... Key[len-1] = ((((data[0]*31 + data[1])*31 + data[2])*31) ... data[len-2])*31 + data[len-1] |
key[len-1]即为传入的参数data对应的hash值。
9.6.3.3. hash初始化
hash初始化由ngx_hash_init()函数完成,其names参数是ngx_hash_key_t结构的数组,即键-值对<key,value>数组,nelts表示该数组元素的个数。因此,在调用该函数进行初始化之前,ngx_hash_key_t结构的数组是准备好的,如何使用,可以采用nginx的ngx_array_t结构,详见本文后面的例子。
该函数初始化的结果就是将names数组保存的键-值对<key,value>,通过hash的方式将其存入相应的一个或多个hash桶(即代码中的buckets)中,该hash过程用到的hash函数一般为ngx_hash_key_lc等。hash桶里面存放的是ngx_hash_elt_t结构的指针(hash元素指针),该指针指向一个基本连续的数据区。该数据区中存放的是经hash之后的键-值对<key',value'>,即ngx_hash_elt_t结构中的字段<name,value>。每一个这样的数据区存放的键-值对<key',value'>可以是一个或多个。
此处有几个问题需要说明。
问题1:为什么说是基本连续?
——用NGX_HASH_ELT_SIZE宏计算某个hash元素的总长度时,存在以sizeof(void*)对齐的填补(padding)。因此将names数组中的键-值对<key,value>中的key拷贝到ngx_hash_elt_t结构的name[1]数组中时,已经为该hash元素分配的空间不会完全被用完,故这个数据区是基本连续的。这一点也可以参考本节后面的结构图或本文后面的例子。
问题2:这些基本连续的数据区从哪里分配的?
——当然是从该函数的第一个参数ngx_hash_init_t的pool字段指向的内存池中分配的。
问题3:<key',value'>与<key,value>不同的是什么?
——key保存的仅仅是个指针,而key'却是key拷贝到name[1]的结果。而value和value'都是指针。如1.3节说明,value指针可能指向静态数据区(例如全局数组、常量字符串)、堆区(例如动态分配的数据区用来保存value值)等。可参考本文后面的例子。
问题4:如何知道某个键-值对<key,value>放在哪个hash桶中?
——key = names[n].key_hash % size; 代码中的这个计算是也。计算结果key即是该键要放在那个hash桶的编号(从0到size-1)。
该函数代码如下。一些疑点、难点的解释请参考//后笔者所加的注释,也可参考本节的hash结构图。
//nelts是names数组中(实际)元素的个数 ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts) { u_char *elts; size_t len; u_short *test; ngx_uint_t i, n, key, size, start, bucket_size; ngx_hash_elt_t *elt, **buckets;
for (n = 0; n < nelts; n++) { //检查names数组的每一个元素,判断桶的大小是否够分配 if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *)) { //有任何一个元素,桶的大小不够为该元素分配空间,则退出 ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0, "could not build the %s, you should " "increase %s_bucket_size: %i", hinit->name, hinit->name, hinit->bucket_size); return NGX_ERROR; } }
//分配2*max_size个字节的空间保存hash数据(该内存分配操作不在nginx的内存池中进行,因为test只是临时的) test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log); if (test == NULL) { return NGX_ERROR; }
bucket_size = hinit->bucket_size - sizeof(void *); //一般sizeof(void*)=4
start = nelts / (bucket_size / (2 * sizeof(void *))); // start = start ? start : 1;
if (hinit->max_size > 10000 && hinit->max_size / nelts < 100) { start = hinit->max_size - 1000; }
for (size = start; size < hinit->max_size; size++) {
ngx_memzero(test, size * sizeof(u_short));
//标记1:此块代码是检查bucket大小是否够分配hash数据 for (n = 0; n < nelts; n++) { if (names[n].key.data == NULL) { continue; }
//计算key和names中所有name长度,并保存在test[key]中 key = names[n].key_hash % size; //若size=1,则key一直为0 test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
if (test[key] > (u_short) bucket_size) {//若超过了桶的大小,则到下一个桶重新计算 goto next; } }
goto found;
next:
continue; }
//若没有找到合适的bucket,退出 ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0, "could not build the %s, you should increase " "either %s_max_size: %i or %s_bucket_size: %i", hinit->name, hinit->name, hinit->max_size, hinit->name, hinit->bucket_size);
ngx_free(test);
return NGX_ERROR;
found: //找到合适的bucket
for (i = 0; i < size; i++) { //将test数组前size个元素初始化为4 test[i] = sizeof(void *); }
/** 标记2:与标记1代码基本相同,但此块代码是再次计算所有hash数据的总长度(标记1的检查已通过) 但此处的test[i]已被初始化为4,即相当于后续的计算再加上一个void指针的大小。 */ for (n = 0; n < nelts; n++) { if (names[n].key.data == NULL) { continue; }
//计算key和names中所有name长度,并保存在test[key]中 key = names[n].key_hash % size; //若size=1,则key一直为0 test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n])); }
//计算hash数据的总长度 len = 0;
for (i = 0; i < size; i++) { if (test[i] == sizeof(void *)) {//若test[i]仍为初始化的值4,即没有变化,则继续 continue; }
//对test[i]按ngx_cacheline_size对齐(32位平台,ngx_cacheline_size=32) test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));
len += test[i]; }
if (hinit->hash == NULL) {//在内存池中分配hash头及buckets数组(size个ngx_hash_elt_t*结构) hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t) + size * sizeof(ngx_hash_elt_t *)); if (hinit->hash == NULL) { ngx_free(test); return NGX_ERROR; }
//计算buckets的启示位置(在ngx_hash_wildcard_t结构之后) buckets = (ngx_hash_elt_t **) ((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));
} else { //在内存池中分配buckets数组(size个ngx_hash_elt_t*结构) buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *)); if (buckets == NULL) { ngx_free(test); return NGX_ERROR; } }
//接着分配elts,大小为len+ngx_cacheline_size,此处为什么+32?——下面要按32字节对齐 elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size); if (elts == NULL) { ngx_free(test); return NGX_ERROR; }
//将elts地址按ngx_cacheline_size=32对齐 elts = ngx_align_ptr(elts, ngx_cacheline_size);
for (i = 0; i < size; i++) { //将buckets数组与相应elts对应起来 if (test[i] == sizeof(void *)) { continue; }
buckets[i] = (ngx_hash_elt_t *) elts; elts += test[i];
}
for (i = 0; i < size; i++) { //test数组置0 test[i] = 0; }
for (n = 0; n < nelts; n++) { //将传进来的每一个hash数据存入hash表 if (names[n].key.data == NULL) { continue; }
//计算key,即将被hash的数据在第几个bucket,并计算其对应的elts位置 key = names[n].key_hash % size; elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);
//对ngx_hash_elt_t结构赋值 elt->value = names[n].value; elt->len = (u_short) names[n].key.len;
ngx_strlow(elt->name, names[n].key.data, names[n].key.len);
//计算下一个要被hash的数据的长度偏移 test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n])); }
for (i = 0; i < size; i++) { if (buckets[i] == NULL) { continue; }
//test[i]相当于所有被hash的数据总长度 elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);
elt->value = NULL; }
ngx_free(test); //释放该临时空间
hinit->hash->buckets = buckets; hinit->hash->size = size;
return NGX_OK; } |
所谓的hash数据长度即指ngx_hash_elt_t结构被赋值后的长度。nelts个元素存放在names数组中,调用该函数对hash进行初始化之后,这nelts个元素被保存在size个hash桶指向的ngx_hash_elts_t数据区,这些数据区中共保存了nelts个hash元素。即hash桶(buckets)存放的是ngx_hash_elt_t数据区的起始地址,以该起始地址开始的数据区存放的是经hash之后的hash元素,每个hash元素的最后是以name[0]为开始的字符串,该字符串就是names数组中某个元素的key,即键值对<key,value>中的key,然后该字符串之后会有几个字节的因对齐产生的padding。
一个典型的经初始化后的hash物理结构如下。具体的可参考后文的例子。
9.6.3.4. hash查找
hash查找操作由ngx_hash_find()函数完成,代码如下:
//由key,name,len信息在hash指向的hash table中查找该key对应的value void * ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len) { ngx_uint_t i; ngx_hash_elt_t *elt;
elt = hash->buckets[key % hash->size];//由key找到所在的bucket(该bucket中保存其elts地址)
if (elt == NULL) { return NULL; }
while (elt->value) { if (len != (size_t) elt->len) { //先判断长度 goto next; }
for (i = 0; i < len; i++) { if (name[i] != elt->name[i]) { //接着比较name的内容(此处按字符匹配) goto next; } }
return elt->value; //匹配成功,直接返回该ngx_hash_elt_t结构的value字段
next: //注意此处从elt->name[0]地址处向后偏移,故偏移只需加该elt的len即可,然后在以4字节对齐 elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len, sizeof(void *)); continue; }
return NULL; } |
查找操作相当简单,由key直接计算所在的bucket,该bucket中保存其所在ngx_hash_elt_t数据区的起始地址;然后根据长度判断并用name内容匹配,匹配成功,其ngx_hash_elt_t结构的value字段即是所求。
9.6.4. 一个例子
本节给出一个创建内存池并从中分配hash结构、hash桶、hash元素并将键-值对<key,value>加入该hash结构的简单例子。
在该例中,将完成这样一个应用,将给定的多个url及其ip组成的二元组<url,ip>作为<key,value>,初始化时对这些<url,ip>进行hash,然后根据给定的url查找其对应的ip地址,若没有找到,则给出相关提示信息。以此向读者展示nginx的hash使用方法。
9.6.4.1. 代码
/** * ngx_hash_t test * in this example, it will first save URLs into the memory pool, and IPs saved in static memory. * then, give some examples to find IP according to a URL. */
#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" #include "ngx_hash.h"
#define Max_Num 7 #define Max_Size 1024 #define Bucket_Size 64 //256, 64
#define NGX_HASH_ELT_SIZE(name) \ (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
/* for hash test */ static ngx_str_t urls[Max_Num] = { ngx_string("www.baidu.com"), //220.181.111.147 ngx_string("www.sina.com.cn"), //58.63.236.35 ngx_string("www.google.com"), //74.125.71.105 ngx_string("www.qq.com"), //60.28.14.190 ngx_string("www.163.com"), //123.103.14.237 ngx_string("www.sohu.com"), //219.234.82.50 ngx_string("www.abo321.org") //117.40.196.26 };
static char* values[Max_Num] = { "220.181.111.147", "58.63.236.35", "74.125.71.105", "60.28.14.190", "123.103.14.237", "219.234.82.50", "117.40.196.26" };
#define Max_Url_Len 15 #define Max_Ip_Len 15
#define Max_Num2 2
/* for finding test */ static ngx_str_t urls2[Max_Num2] = { ngx_string("www.china.com"), //60.217.58.79 ngx_string("www.csdn.net") //117.79.157.242 };
ngx_hash_t* init_hash(ngx_pool_t *pool, ngx_array_t *array); void dump_pool(ngx_pool_t* pool); void dump_hash_array(ngx_array_t* a); void dump_hash(ngx_hash_t *hash, ngx_array_t *array); ngx_array_t* add_urls_to_array(ngx_pool_t *pool); void find_test(ngx_hash_t *hash, ngx_str_t addr[], int num);
/* for passing compiling */ 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(/* int argc, char **argv */) { ngx_pool_t *pool = NULL; ngx_array_t *array = NULL; ngx_hash_t *hash;
printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL);
dump_pool(pool);
printf("--------------------------------\n"); printf("create and add urls to it:\n"); printf("--------------------------------\n"); array = add_urls_to_array(pool); //in fact, here should validate array dump_hash_array(array);
printf("--------------------------------\n"); printf("the pool:\n"); printf("--------------------------------\n"); dump_pool(pool);
hash = init_hash(pool, array); if (hash == NULL) { printf("Failed to initialize hash!\n"); return -1; }
printf("--------------------------------\n"); printf("the hash:\n"); printf("--------------------------------\n"); dump_hash(hash, array); printf("\n");
printf("--------------------------------\n"); printf("the pool:\n"); printf("--------------------------------\n"); dump_pool(pool);
//find test printf("--------------------------------\n"); printf("find test:\n"); printf("--------------------------------\n"); find_test(hash, urls, Max_Num); printf("\n");
find_test(hash, urls2, Max_Num2);
//release ngx_array_destroy(array); ngx_destroy_pool(pool);
return 0; }
ngx_hash_t* init_hash(ngx_pool_t *pool, ngx_array_t *array) { ngx_int_t result; ngx_hash_init_t hinit;
ngx_cacheline_size = 32; //here this variable for nginx must be defined hinit.hash = NULL; //if hinit.hash is NULL, it will alloc memory for it in ngx_hash_init hinit.key = &ngx_hash_key_lc; //hash function hinit.max_size = Max_Size; hinit.bucket_size = Bucket_Size; hinit.name = "my_hash_sample"; hinit.pool = pool; //the hash table exists in the memory pool hinit.temp_pool = NULL;
result = ngx_hash_init(&hinit, (ngx_hash_key_t*)array->elts, array->nelts); if (result != NGX_OK) return NULL;
return hinit.hash; }
void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } }
void dump_hash_array(ngx_array_t* a) { char prefix[] = " ";
if (a == NULL) return;
printf("array = 0x%x\n", a); printf(" .elts = 0x%x\n", a->elts); printf(" .nelts = %d\n", a->nelts); printf(" .size = %d\n", a->size); printf(" .nalloc = %d\n", a->nalloc); printf(" .pool = 0x%x\n", a->pool);
printf(" elements:\n"); ngx_hash_key_t *ptr = (ngx_hash_key_t*)(a->elts); for (; ptr < (ngx_hash_key_t*)(a->elts + a->nalloc * a->size); ptr++) { printf(" 0x%x: {key = (\"%s\"%.*s, %d), key_hash = %-10ld, value = \"%s\"%.*s}\n", ptr, ptr->key.data, Max_Url_Len - ptr->key.len, prefix, ptr->key.len, ptr->key_hash, ptr->value, Max_Ip_Len - strlen(ptr->value), prefix); } printf("\n"); }
/** * pass array pointer to read elts[i].key_hash, then for getting the position - key */ void dump_hash(ngx_hash_t *hash, ngx_array_t *array) { int loop; char prefix[] = " "; u_short test[Max_Num] = {0}; ngx_uint_t key; ngx_hash_key_t* elts; int nelts;
if (hash == NULL) return;
printf("hash = 0x%x: **buckets = 0x%x, size = %d\n", hash, hash->buckets, hash->size);
for (loop = 0; loop < hash->size; loop++) { ngx_hash_elt_t *elt = hash->buckets[loop]; printf(" 0x%x: buckets[%d] = 0x%x\n", &(hash->buckets[loop]), loop, elt); } printf("\n");
elts = (ngx_hash_key_t*)array->elts; nelts = array->nelts; for (loop = 0; loop < nelts; loop++) { char url[Max_Url_Len + 1] = {0};
key = elts[loop].key_hash % hash->size; ngx_hash_elt_t *elt = (ngx_hash_elt_t *) ((u_char *) hash->buckets[key] + test[key]);
ngx_strlow(url, elt->name, elt->len); printf(" buckets %d: 0x%x: {value = \"%s\"%.*s, len = %d, name = \"%s\"%.*s}\n", key, elt, (char*)elt->value, Max_Ip_Len - strlen((char*)elt->value), prefix, elt->len, url, Max_Url_Len - elt->len, prefix); //replace elt->name with url
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&elts[loop])); } }
ngx_array_t* add_urls_to_array(ngx_pool_t *pool) { int loop; char prefix[] = " "; ngx_array_t *a = ngx_array_create(pool, Max_Num, sizeof(ngx_hash_key_t));
for (loop = 0; loop < Max_Num; loop++) { ngx_hash_key_t *hashkey = (ngx_hash_key_t*)ngx_array_push(a); hashkey->key = urls[loop]; hashkey->key_hash = ngx_hash_key_lc(urls[loop].data, urls[loop].len); hashkey->value = (void*)values[loop]; /** for debug printf("{key = (\"%s\"%.*s, %d), key_hash = %-10ld, value = \"%s\"%.*s}, added to array\n", hashkey->key.data, Max_Url_Len - hashkey->key.len, prefix, hashkey->key.len, hashkey->key_hash, hashkey->value, Max_Ip_Len - strlen(hashkey->value), prefix); */ }
return a; }
void find_test(ngx_hash_t *hash, ngx_str_t addr[], int num) { ngx_uint_t key; int loop; char prefix[] = " ";
for (loop = 0; loop < num; loop++) { key = ngx_hash_key_lc(addr[loop].data, addr[loop].len); void *value = ngx_hash_find(hash, key, addr[loop].data, addr[loop].len); if (value) { printf("(url = \"%s\"%.*s, key = %-10ld) found, (ip = \"%s\")\n", addr[loop].data, Max_Url_Len - addr[loop].len, prefix, key, (char*)value); } else { printf("(url = \"%s\"%.*s, key = %-10d) not found!\n", addr[loop].data, Max_Url_Len - addr[loop].len, prefix, key); } } } |
9.6.4.2. 如何编译
CXX = gcc CXXFLAGS += -g -Wall -Wextra
NGX_ROOT = /usr/src/nginx-1.0.4
TARGETS = ngx_hash_t_test TARGETS_C_FILE = $(TARGETS).c
CLEANUP = rm -f $(TARGETS) *.o
all: $(TARGETS)
clean: $(CLEANUP)
CORE_INCS = -I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \
NGX_PALLOC = $(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING = $(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC = $(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_ARRAY = $(NGX_ROOT)/objs/src/core/ngx_array.o NGX_HASH = $(NGX_ROOT)/objs/src/core/ngx_hash.o
$(TARGETS): $(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $(NGX_ARRAY) $(NGX_HASH) $^ -o $@ |
9.6.4.3. 运行结果
bucket_size=64字节:运行结果如下:
# ./ngx_hash_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8870020 .d .last = 0x8870048 .end = 0x8870420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8870020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984
-------------------------------- create and add urls to it: -------------------------------- array = 0x8870048 .elts = 0x887005c .nelts = 7 .size = 16 .nalloc = 7 .pool = 0x8870020 elements: 0x887005c: {key = ("www.baidu.com" , 13), key_hash = 270263191 , value = "220.181.111.147"} 0x887006c: {key = ("www.sina.com.cn", 15), key_hash = 1528635686, value = "58.63.236.35" } 0x887007c: {key = ("www.google.com" , 14), key_hash = -702889725, value = "74.125.71.105" } 0x887008c: {key = ("www.qq.com" , 10), key_hash = 203430122 , value = "60.28.14.190" } 0x887009c: {key = ("www.163.com" , 11), key_hash = -640386838, value = "123.103.14.237" } 0x88700ac: {key = ("www.sohu.com" , 12), key_hash = 1313636595, value = "219.234.82.50" } 0x88700bc: {key = ("www.abo321.org" , 14), key_hash = 1884209457, value = "117.40.196.26" }
-------------------------------- the pool: -------------------------------- pool = 0x8870020 .d .last = 0x88700cc .end = 0x8870420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8870020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 852
-------------------------------- the hash: -------------------------------- hash = 0x88700cc: **buckets = 0x88700d8, size = 3 0x88700d8: buckets[0] = 0x8870100 0x88700dc: buckets[1] = 0x8870140 0x88700e0: buckets[2] = 0x8870180
buckets 1: 0x8870140: {value = "220.181.111.147", len = 13, name = "www.baidu.com" } buckets 2: 0x8870180: {value = "58.63.236.35" , len = 15, name = "www.sina.com.cn"} buckets 1: 0x8870154: {value = "74.125.71.105" , len = 14, name = "www.google.com" } buckets 2: 0x8870198: {value = "60.28.14.190" , len = 10, name = "www.qq.com" } buckets 0: 0x8870100: {value = "123.103.14.237" , len = 11, name = "www.163.com" } buckets 0: 0x8870114: {value = "219.234.82.50" , len = 12, name = "www.sohu.com" } buckets 0: 0x8870128: {value = "117.40.196.26" , len = 14, name = "www.abo321.org" }
-------------------------------- the pool: -------------------------------- pool = 0x8870020 .d .last = 0x88701c4 .end = 0x8870420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8870020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 604
-------------------------------- find test: -------------------------------- (url = "www.baidu.com" , key = 270263191 ) found, (ip = "220.181.111.147") (url = "www.sina.com.cn", key = 1528635686) found, (ip = "58.63.236.35") (url = "www.google.com" , key = -702889725) found, (ip = "74.125.71.105") (url = "www.qq.com" , key = 203430122 ) found, (ip = "60.28.14.190") (url = "www.163.com" , key = -640386838) found, (ip = "123.103.14.237") (url = "www.sohu.com" , key = 1313636595) found, (ip = "219.234.82.50") (url = "www.abo321.org" , key = 1884209457) found, (ip = "117.40.196.26")
(url = "www.china.com" , key = -1954599725) not found! (url = "www.csdn.net" , key = -1667448544) not found! |
以上结果是bucket_size=64字节的输出。由该结果可以看出,对于给定的7个url,程序将其分到了3个bucket中,详见该结果。该例子的hash物理结构图如下:
bucket_size=256字节:运行结果如下:
# ./ngx_hash_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8b74020 .d .last = 0x8b74048 .end = 0x8b74420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8b74020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984
-------------------------------- create and add urls to it: -------------------------------- array = 0x8b74048 .elts = 0x8b7405c .nelts = 7 .size = 16 .nalloc = 7 .pool = 0x8b74020 elements: 0x8b7405c: {key = ("www.baidu.com" , 13), key_hash = 270263191 , value = "220.181.111.147"} 0x8b7406c: {key = ("www.sina.com.cn", 15), key_hash = 1528635686, value = "58.63.236.35" } 0x8b7407c: {key = ("www.google.com" , 14), key_hash = -702889725, value = "74.125.71.105" } 0x8b7408c: {key = ("www.qq.com" , 10), key_hash = 203430122 , value = "60.28.14.190" } 0x8b7409c: {key = ("www.163.com" , 11), key_hash = -640386838, value = "123.103.14.237" } 0x8b740ac: {key = ("www.sohu.com" , 12), key_hash = 1313636595, value = "219.234.82.50" } 0x8b740bc: {key = ("www.abo321.org" , 14), key_hash = 1884209457, value = "117.40.196.26" }
-------------------------------- the pool: -------------------------------- pool = 0x8b74020 .d .last = 0x8b740cc .end = 0x8b74420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8b74020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 852
-------------------------------- the hash: -------------------------------- hash = 0x8b740cc: **buckets = 0x8b740d8, size = 1 0x8b740d8: buckets[0] = 0x8b740e0
buckets 0: {value = "220.181.111.147", len = 13, name = "www.baidu.com" } buckets 0: {value = "58.63.236.35" , len = 15, name = "www.sina.com.cn"} buckets 0: {value = "74.125.71.105" , len = 14, name = "www.google.com" } buckets 0: {value = "60.28.14.190" , len = 10, name = "www.qq.com" } buckets 0: {value = "123.103.14.237" , len = 11, name = "www.163.com" } buckets 0: {value = "219.234.82.50" , len = 12, name = "www.sohu.com" } buckets 0: {value = "117.40.196.26" , len = 14, name = "www.abo321.org" }
-------------------------------- the pool: -------------------------------- pool = 0x8b74020 .d .last = 0x8b7419c .end = 0x8b74420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8b74020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 644
-------------------------------- find test: -------------------------------- (url = "www.baidu.com" , key = 270263191 ) found, (ip = "220.181.111.147") (url = "www.sina.com.cn", key = 1528635686) found, (ip = "58.63.236.35") (url = "www.google.com" , key = -702889725) found, (ip = "74.125.71.105") (url = "www.qq.com" , key = 203430122 ) found, (ip = "60.28.14.190") (url = "www.163.com" , key = -640386838) found, (ip = "123.103.14.237") (url = "www.sohu.com" , key = 1313636595) found, (ip = "219.234.82.50") (url = "www.abo321.org" , key = 1884209457) found, (ip = "117.40.196.26")
(url = "www.china.com" , key = -1954599725) not found! (url = "www.csdn.net" , key = -1667448544) not found! |
以上结果是bucket_size=256字节的输出。由给结果可以看出,对于给定的7个url,程序将其放到了1个bucket中,即ngx_hash_init()函数中的size=1,因这7个url的总长度只有140,因此,只需size=1个bucket,即buckets[0]。
下表是ngx_hash_init()函数在计算过程中的一些数据。物理结构图省略,可参考上图。
url | 计算长度 | test[0]的值 |
4+ngx_align(13+2,4)=20 | 20 | |
4+ngx_align(15+2,4)=24 | 44 | |
4+ngx_align(14+2,4)=20 | 64 | |
4+ngx_align(10+2,4)=16 | 80 | |
4+ngx_align(11+2,4)=20 | 100 | |
4+ngx_align(12+2,4)=20 | 120 | |
4+ngx_align(14+2,4)=20 | 140 |
9.6.5. 小结
本文针对nginx的hash结构进行了较为全面的分析,包括hash结构、hash元素结构、hash初始化结构等,hash操作主要包括hash初始化、hash查找等。最后通过一个简单例子向读者展示nginx的hash使用方法,并给出详细的运行结果,且画出hash的物理结构图,以此向图这展示hash的设计、原理;同时借此向读者展示编译测试nginx代码的方法。
10. Nginx模块开发
下面本文展示一个简单的Nginx模块开发全过程,我们开发一个叫echo的handler模块,这个模块功能非常简单,它接收“echo”指令,指令可指定一个字符串参数,模块会输出这个字符串作为HTTP响应。例如,做如下配置:
location /echo { echo "hello nginx"; } |
则访问http://hostname/echo时会输出hello nginx。
直观来看,要实现这个功能需要三步:1、读入配置文件中echo指令及其参数;2、进行HTTP包装(添加HTTP头等工作);3、将结果返回给客户端。下面本文将分部介绍整个模块的开发过程。
10.1. 模块配置结构
首先我们需要一个结构用于存储从配置文件中读进来的相关指令参数,即模块配置信息结构。根据Nginx模块开发规则,这个结构的命名规则为ngx_http_[module-name]_[main|srv|loc]_conf_t。其中main、srv和loc分别用于表示同一模块在三层block中的配置信息。
这里我们的echo模块,只需要运行在loc层级下,需要存储一个字符串参数,因此我们可以定义如下的模块配置:
typedef struct { ngx_str_t ed; } ngx_http_echo_loc_conf_t; |
其中字段ed用于存储echo指令指定的需要输出的字符串。注意这里ed的类型,在Nginx模块开发中使用ngx_str_t类型表示字符串,这个类型定义在core/ngx_string中:
typedef struct { size_t len; u_char *data; } ngx_str_t; |
其中两个字段分别表示字符串的长度和数据起始地址。注意在Nginx源代码中对数据类型进行了别称定义,如ngx_int_t为intptr_t的别称,为了保持一致,在开发Nginx模块时也应该使用这些Nginx源码定义的类型而不要使用C原生类型。除了ngx_str_t外,其它三个常用的nginx type分别为:
typedef intptr_t ngx_int_t; typedef uintptr_t ngx_uint_t; typedef intptr_t ngx_flag_t; |
具体定义请参看core/ngx_config.h。关于intptr_t和uintptr_t请参考C99中的stdint.h或http://linux.die.net/man/3/intptr_t。
10.2. 定义指令
一个Nginx模块往往接收一至多个指令,echo模块接收一个指令“echo”。Nginx模块使用一个ngx_command_t数组表示模块所能接收的所有模块,其中每一个元素表示一个条指令。ngx_command_t是ngx_command_s的一个别称(Nginx习惯于使用“_s”后缀命名结构体,然后typedef一个同名“_t”后缀名称作为此结构体的类型名),ngx_command_s定义在core/ngx_config_file.h中:
struct ngx_command_s { ngx_str_t name; ngx_uint_t type; char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); ngx_uint_t conf; ngx_uint_t offset; void *post; }; |
1.name是词条指令的名称,不能包含空格。
2.type使用掩码标志位的方式,配置指令参数。
相关可用type定义在core/ngx_config_file.h中:
* NGX_HTTP_MAIN_CONF: 指令出现在main配置部分是合法的 * NGX_HTTP_SRV_CONF: 指令在server配置部分出现是合法的 * NGX_HTTP_LOC_CONF: 指令在location配置部分出现是合法的 * NGX_HTTP_UPS_CONF: 指令在upstream配置部分出现是合法的
* NGX_CONF_NOARGS: 指令没有参数 * NGX_CONF_TAKE1: 指令读入1个参数 * NGX_CONF_TAKE2: 指令读入2个参数 * ... * NGX_CONF_TAKE7: 指令读入7个参数
* NGX_CONF_FLAG: 指令读入1个布尔型数据 ("on" or "off") * NGX_CONF_1MORE: 指令至少读入1个参数 * NGX_CONF_2MORE: 指令至少读入2个参数 |
其中NGX_CONF_NOARGS表示此指令不接受参数,NGX_CON F_TAKE1-7表示精确接收1-7个,NGX_CONF_TAKE12表示接受1或2个参数,NGX_CONF_1MORE表示至少一个参数,NGX_CONF_FLAG表示接受“on|off”……
3.set是一个函数指针,这个函数一般是将配置文件中相关指令的参数,转化成需要的格式并存入配置结构体。设定函数会在遇到指令时执行。
a.指向结构体 ngx_conf_t 的指针, 这个结构体里包含需要传递给指令的参数
b.指向结构体 ngx_command_t 的指针
c.指向模块自定义配置结构体的指针
Nginx预定义了一些转换函数,可以方便我们调用,这些函数定义在core/ngx_conf_file.h中,一般以“_slot”结尾,例如:
* ngx_conf_set_flag_slot: 将 "on" or "off" 转换成 1 or 0
* ngx_conf_set_str_slot: 将字符串保存为 ngx_str_t
* ngx_conf_set_num_slot: 解析一个数字并保存为int
* ngx_conf_set_size_slot: 解析一个数据大小(如:"8k", "1m") 并保存为size_t
4-5.conf用于指定Nginx相应配置文件,在内存起始地址。一般可以通过内置常量指定,如NGX_HTTP_MAIN_CONF_OFFSET,NGX_HTTP_SRV_CONF_OFFSET,或者 NGX_HTTP_LOC_CONF_OFFSET。offset指定此条指令的参数的偏移量。
下面是echo模块的指令定义:
static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_echo, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), NULL }, ngx_null_command }; |
6. post指向模块在读配置的时候需要的一些零碎变量。一般它是NULL。
指令数组的命名规则为ngx_http_[module-name]_commands,注意数组最后一个元素要是ngx_null_command结束。
10.3. 模块上下文
静态的ngx_http_module_t结构体,包含一大坨函数引用,用来创建和合并三段配置 (main,server,location),命名方式一般是:ngx_http_<module name>_module_ctx,这个结构主要用于定义各个Hook函数。这些函数引用依次是:
函数的入参各不相同,取决于他们具体要做的事情。这里http/ngx_http_config.h是结构体的具体定义:
typedef struct { ngx_int_t (*preconfiguration)(ngx_conf_t *cf); ngx_int_t (*postconfiguration)(ngx_conf_t *cf);
void *(*create_main_conf)(ngx_conf_t *cf); char *(*init_main_conf)(ngx_conf_t *cf, void *conf);
void *(*create_srv_conf)(ngx_conf_t *cf); char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
void *(*create_loc_conf)(ngx_conf_t *cf); char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf); } ngx_http_module_t; |
* preconfiguration 在读入配置前调用
* postconfiguration 在读入配置后调用
* create_main_conf 在创建main配置时调用(比如,用来分配空间和设置默认值)
* init_main_conf 在初始化main配置时调用(比如,把原来的默认值用nginx.conf读到的值来覆盖)
* init_main_conf 在创建server配置时调用
* merge_srv_conf 合并server和main配置时调用
* create_loc_conf 创建location配置时调用
* merge_loc_conf 合并location和server配置时调用
可以把你不需要的函数设置为NULL,Nginx会忽略掉他们。
绝大多数的 handler只使用最后两个:一个用来为特定location配置来分配内存,(叫做 ngx_http_<module name>_create_loc_conf);另一个用来设定默认值以及合并继承过来的配置值(叫做 ngx_http_<module name >_merge_loc_conf)。合并函数同时还会检查配置的有效性,如果有错误,则server的启动将被挂起。
下面是一个使用模块上下文结构体的例子:
static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_echo_create_loc_conf, /* create location configration */ ngx_http_echo_merge_loc_conf /* merge location configration */ }; |
现在开始讲得更深一点。这些配置回调函数看其来很像,所有模块都一样,而且Nginx的API都会用到这个部分。
可以看到一共有8个Hook注入点,分别会在不同时刻被Nginx调用,由于我们的模块仅仅用于location域,这里将不需要的注入点设为NULL即可。其中create_loc_conf用于初始化一个配置结构体,如:为配置结构体分配内存等工作;merge_loc_conf用于将其父block的配置信息合并到此结构体中,也就是实现配置的继承。这两个函数会被Nginx自动调用。注意这里的命名规则:ngx_http_[module-name]_[create|merge]_[main|srv|loc]_conf。
10.4. create_loc_conf
它的入参是(ngx_conf_t),返回值是更新了的模块配置结构体
static void * ngx_http_echo_create_loc_conf(ngx_conf_t *cf) { ngx_http_echo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; return conf; }
|
首先需要指出的是Nginx的内存分配;只要使用了 ngx_palloc(malloc的一个包装函数)或者 ngx_pcalloc (calloc的包装函数),就不用担心内存的释放了。
create_loc_conf新建一个ngx_http_echo_loc_conf_t,分配内存,并初始化其中的数据,然后返回这个结构的指针。
10.5. merge_loc_conf
下面的例子是我的模块echo中的合并函数:
static char * ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); return NGX_CONF_OK; } |
merge_loc_conf将父block域的配置信息合并到create_loc_conf新建的配置结构体中。
这里的需要注意的是Nginx提供了一些好用的合并函数用来合并不同类型的数据(ngx_conf_merge_<data type>_value),这类函数的入参是:
1. 当前location 的变量值
2. 如果第一个参数没有被设置而采用的值
3. 如果第一第二个参数都没有被设置而采用的值
结果会被保存在第一个参数中。能用的合并函数包括 ngx_conf_merge_size_value, ngx_conf_merge_msec_value 等等。可参见 core/ngx_conf_file.h.
以ngx_conf_merge_str_value为例:
#define ngx_conf_merge_str_value(conf, prev, default) \ if (conf.data == NULL) { \ if (prev.data) { \ conf.len = prev.len; \ conf.data = prev.data; \ } else { \ conf.len = sizeof(default) - 1; \ conf.data = (u_char *) default; \ } \ } |
同时可以看到,core/ngx_conf_file.h还定义了很多merge value的宏用于merge各种数据。它们的行为比较相似:使用prev填充conf,如果prev的数据为空则使用default填充。
同时还需要注意的是:错误的处理。函数需要往log文件写一些东西,同时返回NGX_CONF_ERROR。这个返回值会将server的启动挂起。(因为被标示为NGX_LOG_EMERG级别,所以错误同时还会输出到标准输出。作为参考,core/ngx_log.h列出了所有的日志级别。)
10.6. 模块定义
接下来我们间接地介绍更深一层:结构体ngx_module_t。该结构体变量命名方式为ngx_http_<module name>_module。它包含模块的内容和指令执行方式,同时也还包含一些回调函数(退出线程,退出进程,等等)。模块定义在有的时候会被用作查找的关键字,来查找与特定模块相关联的数据。模块定义通常像是这样:
ngx_module_t ngx_http_<mode name>_module = { NGX_MODULE_V1, &ngx_http_module_ctx, /* module context */ ngx_http_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; |
仅仅替换掉合适的<module name>就可以了。模块可以添加一些回调函数来处理线程/进程的创建和销毁,但是绝大多数模块都用NULL。(关于这些回调函数的入参,可以参考 core/ngx_conf_file.h.)
10.7. Handler
下面的工作是编写handler。handler可以说是模块中真正干活的代码。
Handler一般做4件事:
- 读入模块配置(获取location配置)。
- 处理功能业务(生成合适的响应)。
- 产生HTTP header(发送响应头)。
- 产生HTTP body(发送响应体)。
Handler有一个参数,即请求结构体。请求结构体包含很多关于客户请求的有用信息,比如说请求方法,URI,请求头等等。
下面先贴出echo模块的代码,然后通过分析代码的方式介绍如何实现这四步。这一块的代码比较复杂:
static ngx_int_t ngx_http_echo_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_echo_loc_conf_t *elcf; elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module); if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST))) { return NGX_HTTP_NOT_ALLOWED; } r->headers_out.content_type.len = sizeof("text/html") - 1; r->headers_out.content_type.data = (u_char *) "text/html"; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = elcf->ed.len; if(r->method == NGX_HTTP_HEAD) { rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } } b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if(b == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer."); return NGX_HTTP_INTERNAL_SERVER_ERROR; } out.buf = b; out.next = NULL; b->pos = elcf->ed.data; b->last = elcf->ed.data + (elcf->ed.len); b->memory = 1; b->last_buf = 1; rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } return ngx_http_output_filter(r, &out); } |
handler会接收一个ngx_http_request_t指针类型的参数,这个参数指向一个ngx_http_request_t结构体,此结构体存储了这次HTTP请求的一些信息,这个结构定义在http/ngx_http_request.h中:
struct ngx_http_request_s { uint32_t signature; /* "HTTP" */ ngx_connection_t *connection; void **ctx; void **main_conf; void **srv_conf; void **loc_conf; ngx_http_event_handler_pt read_event_handler; ngx_http_event_handler_pt write_event_handler; /* … */ ngx_http_handler_pt content_handler; ngx_uint_t access_code; ngx_http_variable_value_t *variables; /* ... */ } |
由于ngx_http_request_s定义比较长,这里我只截取了一部分。可以看到里面有诸如uri,args和request_body等HTTP常用信息。这里需要特别注意的几个字段是headers_in、headers_out和chain,它们分别表示request header、response header和输出数据缓冲区链表(缓冲区链表是Nginx I/O中的重要内容,后面会单独介绍)。
第一步是获取模块配置信息,这一块只要简单使用ngx_http_get_module_loc_conf就可以了。现在我们就可以访问之前在合并函数中设置的所有变量了。
第二步是功能逻辑。uri 是请求的路径, e.g. "/echo"。args 请求串参数中问号后面的参数 (e.g. "name=john"). headers_in 包含有很多有用的东西,比如说cookie啊,浏览器信息啊什么的,但是许多模块可能用不到这些东东。如果你感兴趣的话,可以参看http/ngx_http_request.h 。对于生成输出,这些信息应该是够了。完整的ngx_http_request_t结构体定义在http/ngx_http_request.h。
第三步是设置response header。Header内容可以通过填充headers_out实现,我们这里只设置了Content-type和Content-length等基本内容,ngx_http_headers_out_t定义了所有可以设置的HTTP Response Header信息:
typedef struct { ngx_list_t headers; ngx_uint_t status; ngx_str_t status_line; /* … */ ngx_str_t charset; u_char *content_type_lowcase; ngx_uint_t content_type_hash; ngx_array_t cache_control; off_t content_length_n; time_t date_time; time_t last_modified_time; } ngx_http_headers_out_t; |
这里并不包含所有HTTP头信息,如果需要可以使用agentzh(春来)开发的Nginx模块HttpHeadersMore在指令中指定更多的Header头信息。
设置好头信息后使用ngx_http_send_header就可以将头信息输出,ngx_http_send_header接受一个ngx_http_request_t类型的参数。
第四步也是最重要的一步是输出Response body。这里首先要了解Nginx的I/O机制,Nginx允许handler一次产生一组输出,可以产生多次,Nginx将输出组织成一个单链表结构,链表中的每个节点是一个chain_t,定义在core/ngx_buf.h:
struct ngx_chain_s { ngx_buf_t *buf; ngx_chain_t *next; }; |
其中ngx_chain_t是ngx_chain_s的别名,buf为某个数据缓冲区的指针,next指向下一个链表节点,可以看到这是一个非常简单的链表。ngx_buf_t的定义比较长而且很复杂,这里就不贴出来了,请自行参考core/ngx_buf.h。ngx_but_t中比较重要的是pos和last,分别表示要缓冲区数据在内存中的起始地址和结尾地址,这里我们将配置中字符串传进去,last_buf是一个位域,设为1表示此缓冲区是链表中最后一个元素,为0表示后面还有元素。因为我们只有一组数据,所以缓冲区链表中只有一个节点,如果需要输入多组数据可将各组数据放入不同缓冲区后插入到链表。下图展示了Nginx缓冲链表的结构:
缓冲数据准备好后,用ngx_http_output_filter就可以输出了(会送到filter进行各种过滤处理)。ngx_http_output_filter的第一个参数为ngx_http_request_t结构,第二个为输出链表的起始地址&out。ngx_http_out_put_filter会遍历链表,输出所有数据。
以上就是handler的所有工作,请对照描述理解上面贴出的handler代码。
10.8. Upstream
我已经帮你了解了如何让你的handler来产生响应。有些时候你可以用一小段C代码就可以得到响应,但是通常情况下你需要同另外一台server打交道(比如你正在写一个用来实现某种网络协议的模块)。你当然可以自己实现一套网络编程的东东,但是如果你只收到部分的响应,需要等待余下的响应数据,你会怎么办?你不会想阻塞整个事件处理循环吧?这样会毁掉Nginx的良好性能!幸运的是,Nginx允许你在它处理后端服务器(叫做"upstreams")的机制上加入你的回调函数,因此你的模块将可以和其他的server通信,同时还不会妨碍其他的请求。这一节将介绍模块如何和一个upstream(如 Memcached, FastCGI,或者另一个 HTTP server)通信。
10.8.1. Upstream 回调函数概要
与其他模块的回调处理函数不一样,upstream模块的处理函数几乎不做“实事”。它压根不调用ngx_http_output_filter。它仅仅是告诉回调函数什么时候可以向upstream server写数据了,以及什么时候能从upstream server读数据了。实际上它有6个可用的钩子:
- create_request 生成发送到
- upstream server的请求缓冲(或者一条缓冲链)
- reinit_request 在与后端服务器连接被重置的情况下(在create_request 被第二次调用之前)被调用
- process_header 处理upstream 响应的第一个bit,通常是保存一个指向upstream "payload"的指针
- abort_request 在客户端放弃请求时被调用 finalize_request 在Nginx完成从upstream读取数据后调用
- input_filter 这是一个消息体的filter,用来处理响应消息体(例如把尾部删除)
这些钩子是怎么勾上去的呢?下面是一个例子,简单版本的代理模块处理函数:
static ngx_int_t ngx_http_proxy_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_http_upstream_t *u; ngx_http_proxy_loc_conf_t *plcf;
plcf = ngx_http_get_module_loc_conf(r, ngx_http_proxy_module);
/* set up our upstream struct */ u = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_t)); if (u == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; }
u->peer.log = r->connection->log; u->peer.log_error = NGX_ERROR_ERR;
u->output.tag = (ngx_buf_tag_t) &ngx_http_proxy_module;
u->conf = &plcf->upstream;
/* attach the callback functions */ u->create_request = ngx_http_proxy_create_request; u->reinit_request = ngx_http_proxy_reinit_request; u->process_header = ngx_http_proxy_process_status_line; u->abort_request = ngx_http_proxy_abort_request; u->finalize_request = ngx_http_proxy_finalize_request; r->upstream = u;
rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init);
if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { return rc; }
return NGX_DONE; } |
看上去都是些例行事务,不过重要的是那些回调函数。同时还要注意的是ngx_http_read_client_request_body,它又设置了一个回调函数,在Nginx完成从客户端读数据后会被调用。
这些个回调函数都要做些什么工作呢?通常情况下,reinit_request, abort_request, 和 finalize_request用来设置或重置一些内部状态,但这些都是几行代码的事情。真正做苦力的是create_request 和 process_header。
10.8.2. create_request 回调函数
假设我有一个upstream server,它读入一个字符打印出两个字符。那么函数应该如何来写呢? create_request需要申请一个buffer来存放“一个字符”的请求,为buffer申请一个链表,并且把链表挂到upstream结构体上。看起来就像这样:
static ngx_int_t ngx_http_character_server_create_request(ngx_http_request_t *r) { /* make a buffer and chain */ ngx_buf_t *b; ngx_chain_t *cl;
b = ngx_create_temp_buf(r->pool, sizeof("a") - 1); if (b == NULL) return NGX_ERROR;
cl = ngx_alloc_chain_link(r->pool); if (cl == NULL) return NGX_ERROR;
/* hook the buffer to the chain */ cl->buf = b; /* chain to the upstream */ r->upstream->request_bufs = cl;
/* now write to the buffer */ b->pos = "a"; b->last = b->pos + sizeof("a") - 1;
return NGX_OK; } |
当然实际应用中你很可能还会用到请求里面的URI。r->uri作为一个 ngx_str_t类型也是有效的,GET的参数在r->args中,最后别忘了你还能访问请求头和 cookie信息。
10.8.3. process_header 回调函数
process_header把响应指针移到客户端可以接收到的部分。同时它还会从upstream 读入头信息,并且相应的设置发往客户端的响应头。
这里有个小例子,读进两个字符的响应。我们假设第一个字符代表“状态”字符。如果它是问号,我们将返回一个404错误并丢弃剩下的那个字符。如果它是空格,我们将以 200 OK的响应把另一个字符返回给客户端。那么我们如何来实现这个process_header 函数呢?
static ngx_int_t ngx_http_character_server_process_header(ngx_http_request_t *r) { ngx_http_upstream_t *u; u = r->upstream;
/* read the first character */ switch(u->buffer.pos[0]) { case '?': r->header_only; /* suppress this buffer from the client */ u->headers_in.status_n = 404; break; case ' ': u->buffer.pos++; /* move the buffer to point to the next character */ u->headers_in.status_n = 200; break; }
return NGX_OK; } |
就是这样。操作头部,改变指针,搞定!注意headers_in实际上就是我们之前提到过的头部结构体( http/ngx_http_request.h),但是它位于来自upstream的头中。一个真正的代理模块,会在头信息的处理上做很多文章,不光是错误处理,做什么完全取决于你的想法。
10.8.4. 状态保持
好了,还记得我说过abort_request, reinit_request和finalize_request 可以用来重置内部状态吗?这是因为许多upstream模块都有其内部状态。模块需要定义一个自定义上下文结构 ,来标记目前为止从upstream读到了什么。这跟之前说的“模块上下文”不是一个概念。“模块上下文”是预定义类型,而自定义上下文结构可以包含任何你需要的数据和字段(这可是你自己定义的结构体)。这个结构体在create_request函数中被实例化,大概像这样:
ngx_http_character_server_ctx_t *p; /* my custom context struct */ p = ngx_pcalloc(r->pool, sizeof(ngx_http_character_server_ctx_t)); if (p == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } ngx_http_set_ctx(r, p, ngx_http_character_server_module); |
最后一行实际上将自定义上下文结构体注册到了特定的请求和模块名上,以便在稍后取用。当你需要这个结构体时(可能所有的回调函数中都需要它),只需要:
ngx_http_proxy_ctx_t *p; p = ngx_http_get_module_ctx(r, ngx_http_proxy_module); |
指针 p 可以得到当前的状态. 设置、重置、增加、减少、往里填数据……你可以随心所欲的操作它。当upstream服务器返回一块一块的响应时,读取这些响应的过程中使用持久状态机是个很nx的办法,它不用阻塞主事件循环。
10.9. Handler的装载
Handler的装载通过往模块启用了的指令的回调函数中添加代码来完成。比如,我的echo中ngx_command_t是这样的:
{ ngx_string("echo "), NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, ngx_http_echo, 0, 0, NULL } |
回调函数是里面的第三个元素,在这个例子中就是那个ngx_http_echo。回调函数的参数是由指令结构体(ngx_conf_t, 包含用户配置的参数),相应的ngx_command_t结构体以及一个指向模块自定义配置结构体的指针组成的。我的echo模块中,这些函数是这样子的:
static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_echo_handler; ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK; } |
这个函数除了调用ngx_conf_set_str_slot转化echo指令的参数外,还将修改了核心模块配置(也就是这个location的配置),将其handler替换为我们编写的handler:ngx_http_echo_handler。这样就屏蔽了此location的默认handler,使用ngx_http_echo_handler产生HTTP响应。
这里可以分为两步:首先,得到当前location的“core”结构体,再分配给它一个 handler。很简单,不是吗? 我已经把我知道的关于hanler模块的东西全招了,现在可以来说说输出过滤链上的filter模块了。
11. Filters
Filter操作handler生成的响应。头部filter操作HTTP头,body filter操作响应的内容。
11.1. 剖析Header Filter
头部Filter由三个步骤组成:
1. 决定何时操作响应
2. 操作响应
3. 调用下一个filter
举个例子,比如有一个简化版本的“不改变”头部filter:如果客户请求头中的If- Modified-Since和响应头中的Last-Modified相符,它把响应状态设置成304。注意这个头部filter只读入一个参数:ngx_http_request_t结构体,而我们可以通过它操作到客户请求头和一会将被发送的响应消息头。
static ngx_int_t ngx_http_not_modified_header_filter(ngx_http_request_t *r) { time_t if_modified_since; if_modified_since = ngx_http_parse_time(r->headers_in.if_modified_since->value.data, r->headers_in.if_modified_since->value.len);
/* step 1: decide whether to operate */ if (if_modified_since != NGX_ERROR && if_modified_since == r->headers_out.last_modified_time) {
/* step 2: operate on the header */ r->headers_out.status = NGX_HTTP_NOT_MODIFIED; r->headers_out.content_type.len = 0; ngx_http_clear_content_length(r); ngx_http_clear_accept_ranges(r); }
/* step 3: call the next filter */ return ngx_http_next_header_filter(r); } |
结构headers_out和我们在hander那一节中看到的是一样的(参考http/ngx_http_request.h),也可以随意处置。
11.2. 剖析Body Filter
因为body filter一次只能操作一个buffer(链表),这使得编写body filter需要一定的技巧。模块需要知道什么时候可以覆盖输入buffer,用新申请的buffer_替换已有的,或者在现有的某个buffer前或后插入一个新buffer。有时候模块会收到许多buffer使得它不得不操作一个不完整的链表,这使得事情变得更加复杂了。而更加不幸的是,Nginx没有为我们提供上层的API来操作buffer链表,所以body filter是比较难懂(当然也比较难写)。但是,有些操作你还是可以看出来的。
一个body filter原型大概是这个样子(例子代码从Nginx源代码的“chunked” filter中取得): static ngx_int_t ngx_http_chunked_body_filter(ngx_http_request_t *r, ngx_chain_t *in);
第一个参数是请求结构体,第二个参数则是指向当前部分链表(可能包含0,1,或更多的buffer)头的指针。
再来举个例子好了。假设我们想要做的是在每个请求之后插入文本"<l!-- Served by Nginx -->"。首先,我们需要判断给我们的buffer链表中是否已经包含响应的最终buffer。就像之前我说的,这里没有简便好用的API,所以我们只能自己来写个循环:
ngx_chain_t *chain_link; int chain_contains_last_buffer = 0;
for ( chain_link = in; chain_link != NULL; chain_link = chain_link->next ) { if (chain_link->buf->last_buf) chain_contains_last_buffer = 1; } |
如果我们没有最后的缓冲区,就返回:
if (!chain_contains_last_buffer) return ngx_http_next_body_filter(r, in); |
很好,现在最后一个缓冲区已经存在链表中了。接下来我们分配一个新缓冲区:
ngx_buf_t *b; b = ngx_calloc_buf(r->pool); if (b == NULL) { return NGX_ERROR; } |
把数据放进去:
b->pos = (u_char *) ""; b->last = b->pos + sizeof("") - 1; |
把这个缓冲区挂在新的链表上:
ngx_chain_t added_link; added_link.buf = b; added_link.next = NULL; |
最后,把这个新链表挂在先前链表的末尾:
chain_link->next = added_link; |
并根据变化重置变量"last_buf"的值:
chain_link->buf->last_buf = 0; added_link->buf->last_buf = 1; |
再将修改过的链表传递给下一个输出过滤函数:
return ngx_http_next_body_filter(r, in); |
现有的函数做了比我们更多的工作,比如mod_perl($response->body =~ s/$/<!-- Served by mod_perl -->/),但是缓冲区链确实是一个强大的构想,它可以让程序员渐进地处理数据,这使得客户端可以尽可能早地得到响应。但是依我来看,缓冲区链表实在需要一个更为干净的接口,这样程序员也可以避免操作不一致状态的链表。但是目前为止,所有的操作风险都得自己控制。
11.3. Filter的装载
Filter在在回调函数post-configuration中被装载。header filter和body filter都是在这里被装载的。
我们以chunked filter模块为例来具体看看:
static ngx_http_module_t ngx_http_chunked_filter_module_ctx = { NULL, /* preconfiguration */ ngx_http_chunked_filter_init, /* postconfiguration */ ... }; |
ngx_http_chunked_filter_init中的具体实现如下:
static ngx_int_t ngx_http_chunked_filter_init(ngx_conf_t *cf) { ngx_http_next_header_filter = ngx_http_top_header_filter; ngx_http_top_header_filter = ngx_http_chunked_header_filter;
ngx_http_next_body_filter = ngx_http_top_body_filter; ngx_http_top_body_filter = ngx_http_chunked_body_filter;
return NGX_OK; } |
发生了什么呢?好吧,如果你还记得,过滤模块组成了一条”接力链表“。当handler生成一个响应后,调用2个函数:ngx_http_output_filter它调用全局函数ngx_http_top_body_filter;以及ngx_http_send_header 它调用全局函数ngx_top_header_filter。
ngx_http_top_body_filter 和 ngx_http_top_header_filter是body和header各自的头部filter链的“链表头”。链表上的每一个“连接”都保存着链表中下一个连接的函数引用(分别是 ngx_http_next_body_filter 和 ngx_http_next_header_filter)。当一个filter完成工作之后,它只需要调用下一个filter,直到一个特殊的被定义成“写”的filter被调用,这个“写”filter的作用是包装最终的HTTP响应。你在这个filter_init函数中看到的就是,模块把自己添加到filter链表中;它先把旧的“头部”filter当做是自己的“下一个”,然后再声明“它自己”是“头部”filter。(因此,最后一个被添加的filter会第一个被执行。)
每个filter要么返回一个错误码,要么用`return ngx_http_next_body_filter();`来作为返回语句
因此,如果filter顺利链执行到了链尾(那个特别定义的的”写“filter),将返回一个"OK"响应,但如果执行过程中遇到了错误,链将被砍断,同时Nginx将给出一个错误的信息。这是一个单向的,错误快速返回的,只使用函数引用实现的链表!
12. Load-balancers
Load-balancer用来决定哪一个后端将会收到请求;具体的实现是round-robin方式或者把请求进行hash。本节将介绍load-balancer模块的装载及其调用。我们将用upstream_hash_module(full source)作例子。upstream_hash将对nginx.conf里配置的变量进行 hash,来选择后端服务器。
一个load-balancer分为六个部分:
1.启用配置指令 (e.g, hash;) 将会调用注册函数
2.注册函数将定义一些合法的server 参数 (e.g., weight=) 并注册一个 upstream初始化函数
3. upstream初始化函数将在配置经过验证后被调用,并且:
* 解析 server 名称为特定的IP地址
* 为每个sokcet连接分配空间
* 设置对端初始化函数的回调入口
4.对端初始化函数将在每次请求时被调用一次,它主要负责设置一些负载均衡函数将会使用的数据结构。
5.负载均衡函数决定把请求分发到哪里;每个请求将至少调用一次这个函数(如果后端服务器失败了,那就是多次了)。
6.对端释放函数 可以在与对应的后端服务器结束通信之后更新统计信息 (成功或失败)
12.1. 启用指令
指令声明,既确定了他们在哪里生效又确定了一旦流程遇到指令将要调用什么函数。load-balancer的指令需要置NGX_HTTP_UPS_CONF标志位,一遍让Nginx知道这个指令只会在upstream块中有效。同时它需要提供一个指向注册函数的指针。下面列出的是upstream_hash模块的指令声明:
{ ngx_string("hash"), NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS, ngx_http_upstream_hash, 0, 0, NULL } |
12.2. 注册函数
上面的回调函数ngx_http_upstream_hash就是所谓的注册函数。之所以这样叫(我起得名字)是因为它注册了把upstream初始化函数和周边的upstream配置注册到了一块。另外,注册函数还定义了特定upstream块中的server指令的一些选项(如weight=, fail_timeout=),下面是upstream_hash模块的注册函数:
ngx_int_t ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_upstream_srv_conf_t *uscf; ngx_http_script_compile_t sc; ngx_str_t *value; ngx_array_t *vars_lengths, *vars_values;
value = cf->args->elts;
/* the following is necessary to evaluate the argument to "hash" as a $variable */ ngx_memzero(&sc, sizeof(ngx_http_script_compile_t)); vars_lengths = NULL; vars_values = NULL; sc.cf = cf; sc.source = &value[1]; sc.lengths = &vars_lengths; sc.values = &vars_values; sc.complete_lengths = 1; sc.complete_values = 1; if (ngx_http_script_compile(&sc) != NGX_OK) { return NGX_CONF_ERROR; } /* end of $variable stuff */ uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module); /* the upstream initialization function */ uscf->peer.init_upstream = ngx_http_upstream_init_hash; uscf->flags = NGX_HTTP_UPSTREAM_CREATE; /* OK, more $variable stuff */ uscf->values = vars_values->elts; uscf->lengths = vars_lengths->elts; /* set a default value for "hash_method" */ if (uscf->hash_function == NULL) { uscf->hash_function = ngx_hash_key; } return NGX_CONF_OK; } |
除了依葫芦画瓢的用来计算$variable的代码,剩下的都很简单,就是分配一个回调函数,设置一些标志位。哪些标志位是有效的呢?
* NGX_HTTP_UPSTREAM_CREATE: 让upstream块中有 server 指令。我实在想不出那种情形会用不到它。
* NGX_HTTP_UPSTREAM_WEIGHT: 让server指令获取选项 weight=
* NGX_HTTP_UPSTREAM_MAX_FAILS: 允许选项max_fails=
* NGX_HTTP_UPSTREAM_FAIL_TIMEOUT: 允许选项fail_timeout=
* NGX_HTTP_UPSTREAM_DOWN: 允许选项 down
* NGX_HTTP_UPSTREAM_BACKUP: 允许选项backup
每一个模块都可以访问这些配置值。
一切都取决于模块自己的决定。也就是说,max_fails不会被自动强制执行;所有的失败逻辑都是由模块作者决定的。过会我们再说这个。目前,我们还没有完成对回调函数的追踪呢。接下来,我们来看upstream初始化函数 (上面的函数中的回调函数init_upstream )。
12.3. upstream 初始化函数
upstream 初始化函数的目的是,解析主机名,为socket分配空间,分配(另一个)回调函数。下面是upstream_hash:
ngx_int_t ngx_http_upstream_init_hash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) { ngx_uint_t i, j, n; ngx_http_upstream_server_t *server; ngx_http_upstream_hash_peers_t *peers; /* set the callback */ us->peer.init = ngx_http_upstream_init_upstream_hash_peer; if (!us->servers) { return NGX_ERROR; } server = us->servers->elts; /* figure out how many IP addresses are in this upstream block. */ /* remember a domain name can resolve to multiple IP addresses. */ for (n = 0, i = 0; i < us->servers->nelts; i++) { n += server[i].naddrs; } /* allocate space for sockets, etc */ peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_hash_peers_t) + sizeof(ngx_peer_addr_t) * (n - 1)); if (peers == NULL) { return NGX_ERROR; } peers->number = n; /* one port/IP address per peer */ for (n = 0, i = 0; i < us->servers->nelts; i++) { for (j = 0; j < server[i].naddrs; j++, n++) { peers->peer[n].sockaddr = server[i].addrs[j].sockaddr; peers->peer[n].socklen = server[i].addrs[j].socklen; peers->peer[n].name = server[i].addrs[j].name; } } /* save a pointer to our peers for later */ us->peer.data = peers;
return NGX_OK; } |
这个函数包含的东西ms比我们期望的多些。大部分的工作ms都该被抽象出来,但事实却不是,我们只能忍受这一点。倒是有一种简化的策略:调用另一个模块的upstream初始化函数,把这些脏活累活(对端的分配等等)都让它干了,然后再覆盖其us->peer.init这个回调函数。例子可以参见http/modules/ngx_http_upstream_ip_hash_module.c。
在我们这个观点中的关键点是设置对端初始化函数的指向,在我们这个例子里是ngx_http_upstream_init_upstream_hash_peer。
12.4. 对端初始化函数
对端初始化函数每个请求调用一次。它会构造一个数据结构,模块会用这个数据结构来选择合适的后端服务器;这个数据结构保存着和后端交互的重试次数,通过它可以很容易的跟踪链接失败次数或者是计算好的哈希值。这个结构体习惯性地被命名为ngx_http_upstream_<module name>_peer_data_t。
另外,对端初始化函数还会构建两个回调函数:
* get: load-balancing 函数
* free: 对端释放函数 (通常只是在连接完成后更新一些统计信息)
似乎还不止这些,它同时还初始化了一个叫做tries的变量。只要tries是正数,Nginx将继续重试当前的load-banlancer。当tries变为0时,Nginx将放弃重试。一切都取决于get 和 free 如何设置合适的tries。
下面是upstream_hash中对端初始化函数的例子:
static ngx_int_t ngx_http_upstream_init_hash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) { ngx_http_upstream_hash_peer_data_t *uhpd; ngx_str_t val; /* evaluate the argument to "hash" */ if (ngx_http_script_run(r, &val, us->lengths, 0, us->values) == NULL) { return NGX_ERROR; } /* data persistent through the request */ uhpd = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t) + sizeof(uintptr_t) * ((ngx_http_upstream_hash_peers_t *)us->peer.data)->number / (8 * sizeof(uintptr_t))); if (uhpd == NULL) { return NGX_ERROR; } /* save our struct for later */ r->upstream->peer.data = uhpd; uhpd->peers = us->peer.data; /* set the callbacks and initialize "tries" to "hash_again" + 1*/ r->upstream->peer.free = ngx_http_upstream_free_hash_peer; r->upstream->peer.get = ngx_http_upstream_get_hash_peer; r->upstream->peer.tries = us->retries + 1; /* do the hash and save the result */ uhpd->hash = us->hash_function(val.data, val.len); return NGX_OK; } |
12.5. 负载均衡函数
主要部分现在才开始。模块就是在这里选择upstream服务器的。负载均衡函数的原型看上去是这样的: static ngx_int_t ngx_http_upstream_get__peer(ngx_peer_connection_t *pc, void *data);
data是我们存放所关注的客户端连接中有用信息的结构体。pc则是要存放我们将要去连接的server的相关信息。负载均衡函数做的事情就是填写pc->sockaddr, pc->socklen, 和 pc->name。如果你懂一点网络编程的话,这些东西应该都比较熟悉了;但实际上他们跟我们手头上的任务来比并不算很重要。我们不关心他们代表什么;我们只想知道从哪里找到合适的值来填写他们。
这个函数必须找到一个可用server的列表,挑一个分配给pc。我们来看看upstream_hash是怎么做的吧: 之前upstream_hash模块已经通过调用ngx_http_upstream_init_hash,把server列表存放在了ngx_http_upstream_hash_peer_data_t 这一结构中。这个结构就是现在的data:
ngx_http_upstream_hash_peer_data_t *uhpd = data;
对端列表现在在uhpd->peers->peer中了。我们通过对哈希值与 server总数取模来从这个数组中取得最终的对端服务器:
ngx_peer_addr_t *peer = &uhpd->peers->peer[uhpd->hash % uhpd->peers->number];
终于大功告成了:
pc->sockaddr = peers->sockaddr;
pc->socklen = peers->socklen;
pc->name = &peers->name;
return NGX_OK;
就是这样!如果load-balancer模块返回 NGX_OK,则意味着”来吧,上这个 server吧!“。如果返回的是NGX_BUSY,说明所有的后端服务器目前都不可用,此时Nginx应该重试。
12.6. 对端释放函数
对端释放函数在upstream连接就绪之后开始运行,它的目的是跟踪失败。函数原型如下:
Void ngx_http_upstream_free__peer(ngx_peer_connection_t *pc, void *data,
ngx_uint_t state);
头两个参数和我们在load-balancer函数中看到的一样。第三个参数是一个state变量,它表明了当前连接是成功还是失败。它可能是NGX_PEER_FAILED (连接失败) 和 NGX_PEER_NEXT (连接失败或者连接成功但程序返回了错误)按位或的结果。如果它是0则代表连接成功。
这些失败如何处理则由模块的开发者自己定。如果根本不再用,那结果则应存放到data中,这是一个指向每个请求自定义的结构体。
但是对端释放函数的关键作用是可以设置pc->tries为 0来阻止Nginx在load-balancer模块中重试。最简单的对端释放函数应该是这样的:
pc->tries = 0;
这样就保证了如果发往后端服务器的请求遇到了错误,客户端将得到一个502 Bad Proxy的错误。
这儿还有一个更为复杂的例子,是从upstream_hash模块中拿来的。如果后端连接失败,它会在位向量 (叫做 tried,一个 uintptr_t类型的数组)中标示失败,然后继续选择一个新的后端服务器直至成功。
#define ngx_bitvector_index(index) index / (8 * sizeof(uintptr_t)) #define ngx_bitvector_bit(index) (uintptr_t) 1 << index % (8 * sizeof(uintptr_t))
static void ngx_http_upstream_free_hash_peer(ngx_peer_connection_t *pc, void *data, ngx_uint_t state) { ngx_http_upstream_hash_peer_data_t *uhpd = data; ngx_uint_t current;
if (state & NGX_PEER_FAILED && --pc->tries) { /* the backend that failed */ current = uhpd->hash % uhpd->peers->number;
/* mark it in the bit-vector */ uhpd->tried[ngx_bitvector_index(current)] |= ngx_bitvector_bit(current);
do { /* rehash until we're out of retries or we find one that hasn't been tried */ uhpd->hash = ngx_hash_key((u_char *)&uhpd->hash, sizeof(ngx_uint_t)); current = uhpd->hash % uhpd->peers->number; } while ((uhpd->tried[ngx_bitvector_index(current)] & ngx_bitvector_bit(current)) && --pc->tries); } } |
因为load-balancer函数只会看新的uhpd->hash的值,所以这样是行之有效的。
许多应用程序不提供重试功能,或者在更高层的逻辑中进行了控制。但其实你也看到了,只需这么几行代码这个功能就可以实现了。
13. 组合Nginx Module
上面完成了Nginx模块各种组件的开发下面就是将这些组合起来了。一个Nginx模块被定义为一个ngx_module_t结构,这个结构的字段很多,不过开头和结尾若干字段一般可以通过Nginx内置的宏去填充,下面是我们echo模块的模块主体定义:
ngx_module_t ngx_http_echo_module = { NGX_MODULE_V1, &ngx_http_echo_module_ctx, /* module context */ ngx_http_echo_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; |
开头和结尾分别用NGX_MODULE_V1和NGX_MODULE_V1_PADDING 填充了若干字段,就不去深究了。这里主要需要填入的信息从上到下以依次为context、指令数组、模块类型以及若干特定事件的回调处理函数(不需要可以置为NULL),其中内容还是比较好理解的,注意我们的echo是一个HTTP模块,所以这里类型是NGX_HTTP_MODULE,其它可用类型还有NGX_EVENT_MODULE(事件处理模块)和NGX_MAIL_MODULE(邮件模块)。
这样,整个echo模块就写好了,下面给出echo模块的完整代码:
/* * Copyright (C) Eric Zhang */ #include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h>
/* Module config */ typedef struct { ngx_str_t ed; } ngx_http_echo_loc_conf_t;
static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf); static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);
/* Directives */ static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_echo, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), NULL }, ngx_null_command };
/* Http context of the module */ static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_echo_create_loc_conf, /* create location configration */ ngx_http_echo_merge_loc_conf /* merge location configration */ };
/* Module */ ngx_module_t ngx_http_echo_module = { NGX_MODULE_V1, &ngx_http_echo_module_ctx, /* module context */ ngx_http_echo_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; /* Handler function */ static ngx_int_t ngx_http_echo_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_echo_loc_conf_t *elcf; elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module); if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST))) { return NGX_HTTP_NOT_ALLOWED; } r->headers_out.content_type.len = sizeof("text/html") - 1; r->headers_out.content_type.data = (u_char *) "text/html"; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = elcf->ed.len; if(r->method == NGX_HTTP_HEAD) { rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } } b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if(b == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer."); return NGX_HTTP_INTERNAL_SERVER_ERROR; } out.buf = b; out.next = NULL; b->pos = elcf->ed.data; b->last = elcf->ed.data + (elcf->ed.len); b->memory = 1; b->last_buf = 1; rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } return ngx_http_output_filter(r, &out); }
static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_echo_handler; ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK; }
static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf) { ngx_http_echo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; return conf; }
static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); return NGX_CONF_OK; } |
14. Nginx模块的安装
Nginx不支持动态链接模块,所以安装模块需要将模块代码与Nginx源代码进行重新编译。安装模块的步骤如下:
1.编写模块config文件,这个文件需要放在和模块源代码文件放在同一目录下。文件内容如下:
ngx_addon_name=模块完整名称 HTTP_MODULES="$HTTP_MODULES 模块完整名称" NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/源代码文件名" |
2.进入Nginx源代码,使用下面命令编译安装
./configure --prefix=安装目录 --add-module=模块源代码文件目录 make make install |
这样就完成安装了,例如,我的源代码文件放在/home/yefeng/ngxdev/ngx_http_echo下,我的config文件为:
ngx_addon_name=ngx_http_echo_module HTTP_MODULES="$HTTP_MODULES ngx_http_echo_module" NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_echo_module.c" |
编译安装命令为:
./configure --prefix=/usr/local/nginx --add-module=/home/yefeng/ngxdev/ngx_http_echo make sudo make install |
这样echo模块就被安装在我的Nginx上了,下面测试一下,修改配置文件,增加以下一项配置:
location /echo { echo "This is my first nginx module!!!"; } |
然后用curl测试一下: curl -i http://localhost/echo
结果如下:
可以看到模块已经正常工作了,也可以在浏览器中打开网址,就可以看到结果:
15. 更深入的学习
本文只是简要介绍了Nginx模块的开发过程,由于篇幅的原因,不能面面俱到。因为目前Nginx的学习资料很少,如果读者希望更深入学习Nginx的原理及模块开发,那么阅读源代码是最好的办法。在Nginx源代码的core/下放有Nginx的核心代码,对理解Nginx的内部工作机制非常有帮助,http/目录下有Nginx HTTP相关的实现,http/module下放有大量内置http模块,可供读者学习模块的开发,另外在http://wiki.nginx.org/3rdPartyModules上有大量优秀的第三方模块,也是非常好的学习资料。