提交 68567051 编写于 作者: ToTensor's avatar ToTensor

update code

上级 0b311705
<p class="left"><b>图书在版编目(CIP)数据</b></p>
<p class="left">深入剖析Nginx/高群凯著.--北京:人民邮电出版社,2013.5</p>
<p class="left">ISBN 978-7-115-30762-0</p>
<p class="left">Ⅰ.①深… Ⅱ.①高… Ⅲ.①Web服务器 Ⅳ.①TP393.09</p>
<p class="left">中国版本图书馆CIP数据核字(2013)第008788号</p>
<p class="center"><b>内容提要</b></p>
<p class="left">Nginx是一款功能强大的高性能Web 和反向代理服务器,最初由俄罗斯程序员Igor Sysoev 开发,而当前由IgorSysoev领导的专业公司Nginx, Inc.进行持续的维护与更新。Nginx可以在大多数UNIX或类UNIX系统上编译运行,比如FreeBSD、Solaris、Linux等,并且官方还提供有Windows下的可执行版本。目前,Nginx在Netflix、Wordpress.com、新浪、网易、腾讯、豆瓣等国内外众多知名网站中应用。</p>
<p class="left">本书不是一本关于Nginx配置指令如何使用的介绍手册。本书重点在于通过剖析Nginx的源代码,探究其功能结构及其内部实现原理。全书共14章和3个附录。首先介绍了开始剖析Nginx源代码前的准备工作,以及跟踪和调试的方法;然后,分别深入分析了Nginx的进程模型、数据结构、配置指令、主要功能模块、I/O事件处理、变量机制、客户端请求过程、Filter模块实例、负载均衡策略以及Handler模块等。附录部分提供了Nginx的编译模块、运行配置等有用信息。</p>
<p class="left">从源码剖析的角度出发,是程序员常用的学习和提高方法。本书是作者多年研读Nginx代码、深入思考和不断实践的结晶。本书适合系统程序员、软件开发工程师、Nginx高级运维工程师阅读参考,对于有志从事相关工作的IT专业学生,更是不可多得的学习资料。</p>
<p class="center"><b>深入剖析Nginx</b></p>
<p class="left">◆著 高群凯</p>
<p class="left">责任编辑 陈冀康</p>
<p class="left">◆人民邮电出版社出版发行  北京市崇文区夕照寺街14号</p>
<p class="left">邮编 100061  电子邮件 315@ptpress.com.cn</p>
<p class="left">网址 http://www.ptpress.com.cn</p>
<p class="left">北京艺辉印刷有限公司印刷</p>
<p class="left">◆开本:800×1000 1/16</p>
<p class="left">印张:22</p>
<p class="left">字数:423千字  2013年5月第1版</p>
<p class="left">印数:1-3500册  2013年5月北京第1次印刷</p>
<p class="center">ISBN 978-7-115-30762-0</p>
<p class="center">定价:59.00元(附光盘)</p>
<p class="center"><b>读者服务热线:(010)67132692 印装质量热线:(010)67129223</b></p>
<p class="center"><b>反盗版热线:(010)67171154</b></p>
\ No newline at end of file
<p class="left">作为轻量级HTTP服务的典型代表,Nginx除了具备体积小、配置灵活、并发能力强、稳定等众所周知的特点以外,在官方网站还详细列出了Nginx的一些主要特性,我们来详细了解一下<a id="ac11"><sup>[11]</sup></a></p>
<p class="left">1.HTTP服务基本特性</p>
<p class="left">• 处理静态页面请求;</p>
<p class="left">• 处理index 首页请求;</p>
<p class="left">• 对请求目录进行列表显示;</p>
<p class="left">• 支持多进程间的负载均衡;</p>
<p class="left">• 对打开文件描述符进行缓存(提高性能);</p>
<p class="left">• 对反向代理进行缓存(加速);</p>
<p class="left">• 支持FastCGI、uwsgi、SCGI 和memcached 多种后端服务器;</p>
<p class="left">• 支持gzip、ranges、chunked、XSLT、SSI 以及图像缩放;</p>
<p class="left">• 支持SSL、TLS SNI。</p>
<p class="left">2.HTTP服务高级特性</p>
<p class="left">• 基于名称的虚拟主机;</p>
<p class="left">• 基于IP 的虚拟主机;</p>
<p class="left">• 支持Keep-alive 和pipelined 连接;</p>
<p class="left">• 灵活和方便的配置;</p>
<p class="left">• 在更新配置和升级执行程序时提供不间断服务;</p>
<p class="left">• 可自定义客户端访问的日志格式;</p>
<p class="left">• 带缓存的日志写操作(提高性能);</p>
<p class="left">• 支持快速的日志文件切换;</p>
<p class="left">• 支持对3xx-5xx 错误代码进行重定向;</p>
<p class="left">• URI 重写支持正则表达式;</p>
<p class="left">• 根据客户端地址执行不同的功能;</p>
<p class="left">• 支持基于客户端IP 地址的访问控制;</p>
<p class="left">• 支持基于HTTP 基本认证机制的访问控制;</p>
<p class="left">• 支持HTTPreferer 验证;</p>
<p class="left">• 支持HTTP 协议的PUT、DELETE、MKCOL、COPY 以及MOVE 方法;</p>
<p class="left">• 支持FLV流和MP4 流;</p>
<p class="left">• 支持限速机制;</p>
<p class="left">• 支持单客户端的并发控制;</p>
<p class="left">• 支持Perl脚本嵌入。</p>
<p class="left">3.邮件代理服务特性</p>
<p class="left">• 使用外部HTTP 认证服务器将用户重定向到IMAP/POP3 服务器;</p>
<p class="left">• 使用外部HTTP 认证服务器将用户重定向到内部SMTP 服务器;</p>
<p class="left">• 支持的认证方式。</p>
<p class="left">♦ POP3:USER/PASS、APOP、AUTH LOGIN/PLAIN/CRAM-MD5。</p>
<p class="left">♦ IMAP:sLOGIN、AUTH LOGIN/PLAIN/CRAM-MD5。</p>
<p class="left">♦ SMTP:AUTH LOGIN/PLAIN/CRAM-MD5。</p>
<p class="left">• 支持SSL;</p>
<p class="left">• 支持STARTTLS 和STLS。</p>
<p class="left">4.架构和扩展性</p>
<p class="left">• 一个主进程和多个工作进程配合服务的工作模型;</p>
<p class="left">• 工作进程以非特权用户运行(安全性考虑);</p>
<p class="left">• 支持的事件机制有:kqueue(FreeBSD 4.1+)、epoll(Linux 2.6+)、rt signals(Linux2.2.19+)、/dev/poll(Solaris 711/99+)、eventports(Solaris10)、select 和poll;</p>
<p class="left">• 支持 kqueue 的众多特性,包括 EV_CLEAR、EV_DISABLE(临时禁止事件)、NOTE_LOWAT、EV_EOF等;</p>
<p class="left">• 支持sendfile(FreeBSD3.1+、Linux2.2+、Mac OSX10.5+)、sendfile64(Linux2.4.21+)和sendfilev(Solaris8 7/01+);</p>
<p class="left">• 支持异步文件IO(FreeBSD 4.3+、Linux2.6.22+);</p>
<p class="left">• 支持DIRECTIO(FreeBSD 4.4+、Linux2.4+、Solaris2.6+、Mac OS X);</p>
<p class="left">• 支持Accept-filters(FreeBSD4.1+、NetBSD5.0+)和TCP_DEFER_ACCEPT(Linux2.4+);</p>
<p class="left">• 10000 个非活跃HTTPkeep-alive 连接仅占用约2.5MB 内存;</p>
<p class="left">• 最少程度的数据拷贝操作。</p>
<p class="left">5.已测试过的操作系统和平台</p>
<p class="left">• FreeBSD 3~10/i386、FreeBSD5~10/amd64;</p>
<p class="left">• Linux 2.2~3/i386、Linux2.6~3/amd64;</p>
<p class="left">• Solaris9/i386、sun4u、Solaris10/i386、amd64、sun4v;</p>
<p class="left">• AIX7.1/powerpc;</p>
<p class="left">• HP-UX11.31/ia64;</p>
<p class="left">• Mac OS X/ppc、i386;</p>
<p class="left">• WindowsXP、Windows Server 2003。</p>
<p class="left">从上面列表可以看到Nginx功能的丰富与强悍。当然,这里给出的还只是Nginx功能的简单描述,而对于每项功能的具体使用以及是如何实现的,我们还不得而知,而这也正是本书将要展开叙述的全部内容。</p>
<p class="left" id="bw2"></p>
<p class="left">Nginx的源码可通过官网提供的下载地址<a id="ac12"><sup>[12]</sup></a>找到,截止当前的最新版本是Nginx1.2.0,也就是本书所针对的版本。虽然官网下载页没有提供Nginx旧版源码的下载链接,但Nginx的所有版本源码包都是放在目录http://nginx.org/download/下的,所以包括Nginx 0.1.0 版本在内的Nginx源码都能下载到。</p>
<p class="left">由于 Nginx 背后有公司运作,所以其更新速度比较快,相关资料也比较齐全,下面是一些有用的网址。</p>
<p class="left">• 官方主页:http://nginx.org/。</p>
<p class="left">• 使用手册:http://nginx.org/en/docs/。</p>
<p class="left">• 配置指令:http://wiki.nginx.org/DirectiveIndex。</p>
<p class="left">• 版本任务:http://trac.nginx.org/nginx/report/2。</p>
<p class="left">• 开发路线图:http://trac.nginx.org/nginx/roadmap。</p>
<p class="left">• 邮件讨论组:http://mailman.nginx.org/mailman/listinfo。</p>
<p class="left">所有相关信息基本都能从官方主页链入,如果想找什么资料,建议先去官网看看。</p>
<p class="left" id="bw3"></p>
<p class="left">将Nginx源码包解压后,目录文件如下所示。</p>
<p class="left">[root@localhost nginx-1.2.0]# ls -F</p>
<p class="left">auto/ CHANGES CHANGES.ru conf/ configure* contrib/ html/ LICENSE man/ README src/</p>
<p class="left">其中</p>
<p class="left">• auto/:包含了很多会在执行configure 进行编译配置时调用的检测代码。</p>
<p class="left">• CHANGES:Nginx 的版本更新细节记录。英文版。</p>
<p class="left">• CHANGES.ru:Nginx 的版本更新细节记录。俄文版。</p>
<p class="left">• conf/:Nginx 提供的一些默认配置文件。</p>
<p class="left">• configure*:根据系统环境设定Nginx 编译选项的执行脚本。</p>
<p class="left">• contrib/:网友贡献的一些有用脚本。</p>
<p class="left">• html/:提供了两个默认html 页面,比如index.html 的Welcome tonginx!。</p>
<p class="left">• LICENSE:声明的Nginx 源码许可协议。</p>
<p class="left">• man/:Nginx 的Man手册,本文文件,可直接用vi 或记事本打开。</p>
<p class="left">• README:读我文件,内容很简单,通告一下官网地址。</p>
<p class="left">• src/:Nginx 源码,分门别类,比如实现事件的event 等,很清晰。</p>
<p class="left">执行configure脚本后将生成Makefile文件和objs目录,这是根据当前系统环境生成的相关编译配置。Nginx并没有使用Autoconf<a id="ac13"><sup>[13]</sup></a>和Automake<a id="ac14"><sup>[14]</sup></a>等这样的自动化工具来做这个工作,而都是手动编码实现的。比如当Nginx判断当前Linux系统是否支持epoll时,它采用的方法就是编写一款小应用程序,并在其中调用epoll_create()函数,然后再根据它是否可被正常编译执行来做这个判断。具体可参考文件nginx-1.2.0/auto/os/linux和nginx-1.2.0/auto/feature内相关代码。</p>
<p class="left" id="bw4"></p>
<p class="left">对于Windows平台,首选Source Insight<a id="ac15"><sup>[15]</sup></a>源码阅读工具。该工具功能强大,根据其官方网站的介绍,Source Insight是一款面向项目开发的程序编辑器和代码浏览器,它拥有内置的对C/C++、C#和Java等程序的分析功能。SourceInsight能自动分析和动态维护源码工程的符号数据库,并在用户查看代码时显示有用的对应上下文信息。</p>
<p class="left">如果是在Linux平台下,则可以利用Vi<a id="ac16"><sup>[16]</sup></a>、Taglist<a id="ac17"><sup>[17]</sup></a>、Cscope<a id="ac18"><sup>[18]</sup></a>以及Ctag<a id="ac19"><sup>[19]</sup></a>这几个工具来组合成阅读Nginx源码的环境。它们的组合也许要费一段功夫,但磨刀不误砍柴工,为了更方便快捷地阅读Nginx源码,花这点时间还是比较值得的。</p>
<p class="left">当然,我们还有另外一个更方便简单的选择:Source Navigator<a id="ac20"><sup>[20]</sup></a>。Source Navigator(Sourcenav)是由Red Hat推出的一款查看和分析源代码的强大图形界面工具,可以与前面介绍的Source Insight相媲美,而且Sourcenav是开源的。除了提供源代码的编辑、查看功能, Sourcenav同时还支持编译器和调试器的集成,因此可以构建成一套完整的IDE开发环境。Sourcenav针对Windows和UNIX/Linux,提供两种版本,在Windows下的版本,解压即可以使用,但是要注意解压路径不能包含空格以及中文字符。图1-1所示是Sourcenav在Ubuntu 8.10平台下的运行界面。</p>
<p class="left">不管是在Windows平台下还是在Linux平台下,搭建一个得心应手的源码阅读环境,是我们阅读源码达到事半功倍效果的有力保证。</p>
<div class="pic">
<img alt="figure_0023_0002" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0023_0002.jpg">
</div>
<div class="grap">
图1-1 Sourcenav在Ubuntu 8.10平台下的运行界面
</div>
<p class="left" id="bw5"></p>
<p class="left">我们将在第2章里介绍如何对Nginx进行跟踪与调试,除了对Nginx进程进行直接的跟踪与调试的工具以外,还会用到另外两个HTTP测试工具:wget<a id="ac21"><sup>[21]</sup></a>与curl<a id="ac22"><sup>[22]</sup></a>。关于这两个工具的差别,可以看这里<a id="ac23"><sup>[23]</sup></a>,要注意的主要是wget 1.12 及以前的版本仅支持HTTP 1.0 协议(虽然也包括部分HTTP1.1 的特性),所以在测试HTTP1.1的相关特性时,最好使用wget-1.13 以后版本或curl。另外,通过给wget加上--debug选项或给curl加上-v选项能看到它们请求的详细信息<a id="ac24"><sup>[24]</sup></a>,这对我们的测试提供的帮助也非常大。</p>
<p class="left">我用到的其他测试辅助工具主要还有如下几个。</p>
<p class="left">• Wireshark<a id="ac25"><sup>[25]</sup></a>:抓包使用。</p>
<p class="left">• Nc<a id="ac26"><sup>[26]</sup></a>:网络工具中的瑞士军刀,短小精悍,功能强大<a id="ac27"><sup>[27]</sup></a>。</p>
<p class="left">• Firefox<a id="ac28"><sup>[28]</sup></a>:结合firebug<a id="ac29"><sup>[29]</sup></a>看HTTP请求响应内容。</p>
<p class="left">• Opera<a id="ac30"><sup>[30]</sup></a>:浏览器,测试HTTP。</p>
<p class="left">• Hexdump<a id="ac31"><sup>[31]</sup></a>:看十六进制数据。</p>
<p class="left" id="bw6"></p>
<p class="left">Nginx的编译安装很简单,使用Linux下通用的三板斧即可:./configure、make、make install。当然,这样做的话,那么一切都是使用的默认配置,如果要做修改,则必须在执行 configure时指定,比如对Nginx加上调试功能。</p>
<p class="left">[root@localhost nginx-1.2.0]# ./configure --with-debug</p>
<p class="left">修改默认安装路径。</p>
<p class="left">[root@localhost nginx-1.2.0]# ./configure --prefix=/usr/gqk/</p>
<p class="left">所有这些配置选项可以通过命令查看。</p>
<p class="left">[root@localhost nginx-1.2.0]# ./configure –help</p>
<p class="left">在默认情况下,Nginx被安装在/usr/local/nginx/目录下,而其他目录也大都以此为父目录,比如 Web 根目录为/usr/local/nginx/html/,日志记录在文件/usr/local/nginx/logs/access.log 和/usr/local/nginx/logs/error.log内。</p>
<p class="left">编译好后的Nginx,执行它很简单,一般我们只需指定配置文件即可。</p>
<p class="left">[root@localhost~]#/home/gqk/nginx-1.2.0/objs/nginx-c/usr/local/nginx/conf/nginx.conf.test</p>
<p class="left">如果不指定配置文件,那么默认就是安装目录下的 nginx.conf 文件,比如:/usr/local/nginx/conf/nginx.conf。通过ps命令可以看到Nginx是否已正常执行。</p>
<p class="left">[root@localhost ~]# ps auxf | grep nginx | grep -v grep</p>
<p class="left">root 3949 0.0 0.1 5216 572 ? Ss Oct05 0:00 nginx: master process /home/gqk/nginx-1.2.0/objs/nginx -c /usr/local/nginx/conf/nginx.conf.test</p>
<p class="left">nobody 3950 0.0 0.3 5404 1236 ? T Oct05 0:00 \_ nginx: worker process</p>
<p class="left">查看Nginx对应的监听套接口。</p>
<p class="left">[root@localhost ~]# netstat -natp | grep nginx</p>
<p class="left">tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 3949/nginx</p>
<p class="left" id="bw7"></p>
<p class="left">本书主要针对的是Nginx的Web服务器功能,这其中牵扯到很多的国际标准协议,比如说HTTP协议、URL标准、HTML标准等。因此,把与之相关的RFC文档准备好是必不可少的。这里列出几个站点,方便查阅。</p>
<p class="left">http://www.rfc.net RFC的官方站点<a id="ac32"><sup>[32]</sup></a>,可以检查RFC最及时的更新情况。</p>
<p class="left">http://www.ietf.org 最重要的 Internet组织之一。</p>
<p class="left">http://sunsite.dk  RFC查询非常强大(可以以FTP登录下载全部RFC文档)。</p>
<p class="left">http://www.iso.ch  ISO-国际标准化组织。</p>
<p class="left">http://standards.ieee.org IEEE-电气与电子工程师协会。</p>
<p class="left">http://web.ansi.org ANSI-美国国家标准化组织。</p>
<p class="left">http://www.itu.int  ITU-国际电信同盟。</p>
<p class="left">http://www.rfc-editor.org/ RFC归档搜索网。</p>
<p class="left">http://www.faqs.org/rfcs/ RFC归档搜索网。</p>
<p class="left">http://www.cnpaf.net/ 中国协议分析网。</p>
<p class="left"><b>注 释</b></p>
<p class="footnote"><a id="anchor1">[1].http://www.nginx.org/en/</a></p>
<p class="footnote"><a id="anchor2">[2].http://sysoev.ru/en/</a></p>
<p class="footnote"><a id="anchor3">[3].http://www.yandex.ru/</a></p>
<p class="footnote"><a id="anchor4">[4].http://www.mail.ru/</a></p>
<p class="footnote"><a id="anchor5">[5].http://vkontakte.ru/</a></p>
<p class="footnote"><a id="anchor6">[6].http://www.rambler.ru/</a></p>
<p class="footnote"><a id="anchor7">[7].http://news.netcraft.com/archives/2012/08/02/august-2012-web-server-survey.html</a></p>
<p class="footnote"><a id="anchor8">[8].https://signup.netflix.com/openconnect/software</a></p>
<p class="footnote"><a id="anchor9">[9].http://www.nginx.com/cs/nginx-automattic.html</a></p>
<p class="footnote"><a id="anchor10">[10].http://blog.fastmail.fm/2007/01/04/webimappop-frontend-proxies-changed-to-nginx/</a></p>
<p class="footnote"><a id="anchor11">[11].http://www.nginx.org/en/</a></p>
<p class="footnote"><a id="anchor12">[12].http://nginx.org/en/download.html</a></p>
<p class="footnote"><a id="anchor13">[13].http://www.gnu.org/software/autoconf/</a></p>
<p class="footnote"><a id="anchor14">[14].http://www.gnu.org/software/automake/</a></p>
<p class="footnote"><a id="anchor15">[15].http://www.sourceinsight.com/</a></p>
<p class="footnote"><a id="anchor16">[16].http://vim.sourceforge.net/</a></p>
<p class="footnote"><a id="anchor17">[17].http://vim-taglist.sourceforge.net/</a></p>
<p class="footnote"><a id="anchor18">[18].http://cscope.sourceforge.net/</a></p>
<p class="footnote"><a id="anchor19">[19].http://ctags.sourceforge.net/</a></p>
<p class="footnote"><a id="anchor20">[20].http://sourcenav.sourceforge.net/</a></p>
<p class="footnote"><a id="anchor21">[21].http://www.gnu.org/software/wget/</a></p>
<p class="footnote"><a id="anchor22">[22].2 http://curl.haxx.se/</a></p>
<p class="footnote"><a id="anchor23">[23].http://daniel.haxx.se/docs/curl-vs-wget.html</a></p>
<p class="footnote"><a id="anchor24">[24].http://lenky.info/?p=1841</a></p>
<p class="footnote"><a id="anchor25">[25].http://www.wireshark.org/</a></p>
<p class="footnote"><a id="anchor26">[26].http://netcat.sourceforge.net/</a></p>
<p class="footnote"><a id="anchor27">[27].http://linux.die.net/max/I/nc</a></p>
<p class="footnote"><a id="anchor28">[28].http://www.mozilla.org/en-US/firefox/fx/#desktop</a></p>
<p class="footnote"><a id="anchor29">[29].https://getfirebug.com/</a></p>
<p class="footnote"><a id="anchor30">[30].http://www.opera.com/</a></p>
<p class="footnote"><a id="anchor31">[31].ftp://ftp.kernel.org/pub/linux/utils/util-linux/</a></p>
<p class="footnote"><a id="anchor32">[32].要查阅rfc 文档直接访问http://www.ietf.org/rfc/rfc----.txt或http://tools.ietf.org/html/rfc----,将横线换成rfc 文档的对应序号,比如:http://www.ietf.org/rfc/rfc2616.txt或http://tools.ietf.org/html/rfc2616。</a></p>
\ No newline at end of file
<h1 class="center"><a>第1章 源码分析的准备工作</a></h1>
<p class="left">从Nginx(读作engine x)的官方网站<a id="ac1"><sup>[1]</sup></a>,我们可以看到如下介绍:Nginx是Igor Sysoev<a id="ac2"><sup>[2]</sup></a>编写的一款HTTP和反向代理服务器,另外它也可以当作邮件代理服务器。它一直被众多流量巨大的俄罗斯网站所使用,例如Yandex<a id="ac3"><sup>[3]</sup></a>、Mail.Ru<a id="ac4"><sup>[4]</sup></a>、VKontakte<a id="ac5"><sup>[5]</sup></a>以及Rambler<a id="ac6"><sup>[6]</sup></a>等。据Netcraft统计,截止到2012年8月份,世界上最繁忙的网站中有11.48%<a id="ac7"><sup>[7]</sup></a>在使用Nginx作为其服务器或者代理服务器。部分典型成功案例有:Netflix<a id="ac8"><sup>[8]</sup></a>、Wordpress.com<a id="ac9"><sup>[9]</sup></a>和FastMail.FM<a id="ac10"><sup>[10]</sup></a>。鉴于Nginx的强大性能与稳定性,在国内也有大量的高压力网站在使用Nginx,如新浪、网易、腾讯、CSDN、酷六、水木社区、豆瓣等。</p>
<p class="left" id="bw1"></p>
ngx_shm_zone_t *
ngx_shared_memory_add(ngx_conf_t *cf, ngx_str_t *name, size_t size, void *tag)
{
 shm_zone->init = NULL;
\ No newline at end of file
 ngx_cycle_t *
 ngx_init_cycle(ngx_cycle_t *old_cycle)
 {
 …
  if (shm_zone[i].init(&shm_zone[i], NULL) != NGX_OK) {
   goto failed;
  }
\ No newline at end of file
 cache->shm_zone->init = ngx_http_file_cache_init;
 cache->shm_zone->data = cache;
\ No newline at end of file
<p class="left">优秀的程序都会带有自己的日志输出接口,并且一般还会给出不同等级的输出级别,以便于重次信息的过滤,比如Linux内核的日志输出标准接口为printk,并且给出了KERN_EMERG、KERN_ALERT、KERN_DEBUG等这样的输出等级。Nginx与此类似,下面具体来看。</p>
<p class="left">为了获取最丰富的日志信息,我们在进行configure配置时,需要把--with-debug选项加上,这样能生成一个名为NGX_DEBUG的宏,而在Nginx源码内,该宏被用作控制开关,如果没有它,那么很多日志逻辑代码将在 make 编译时直接跳过。比如对单连接的 debug_connection调试指令、分模块日志调试debug_http功能等。</p>
<p class="left">00: 代码片段2.2-1,文件名: ngx_auto_config.h</p>
<p class="left">01: #define NGX_CONFIGURE " --with-debug"</p>
<p class="left">02:</p>
<p class="left">03: #ifndef NGX_DEBUG</p>
<p class="left">04: #define NGX_DEBUG 1</p>
<p class="left">05: #endif</p>
<p class="left">620: 代码片段2.2-2,文件名: nginx.c</p>
<p class="left">621: #if (NGX_DEBUG)</p>
<p class="left">622:  {</p>
<p class="left">623:  char **e;</p>
<p class="left">624:  for (e = env; *e; e++) {</p>
<p class="left">625:   ngx_log_debug1(NGX_LOG_DEBUG_CORE, cycle->log, 0, "env: %s", *e);</p>
<p class="left">626:  }</p>
<p class="left">627:  }</p>
<p class="left">628: #endif</p>
<p class="left">有了上面这个编译前提条件之后,我们还需在配置文件里做恰当的设置。关于这点,Nginx提供的主要配置指令为error_log。该配置项的默认情况(默认值定义在objs/ngx_auto_config.h文件内)为</p>
<p class="left">error_log logs/error.log error;</p>
<p class="left">表示日志信息记录在logs/error.log(如果没改变Nginx的默认工作路径的话,那么其父目录为/usr/local/nginx/)文件内,而日志记录级别为error。</p>
<p class="left">在实际进行配置时,可以修改日志信息记录文件路径(比如修改为/dev/null,此时所有日志信息将被输出到所谓的 Linux 黑洞设备,导致日志信息全部丢弃)或直接输出到标准终端(此时指定为stderr)。Nginx提供的日志记录级别一共有八级,等级从低到高分别为debug、info、notice、warn、error、crit、alert、emerg。如果设置为error,则表示Nginx内等级为error、crit、alert和emerg的四种级别的日志将被输出到日志文件或标准终端。另外的debug、info、notice和warn这四种日志将被直接过滤掉而不会输出。因此如果我们只关注特别严重的信息,只需将日志等级设置为error即可大大减少Nginx的日志输出量,这样就避免了在大量的日志信息里寻找重要信息的麻烦。</p>
<p class="left">当我们利用日志跟踪 Nginx 时,需要获取最大量的日志信息,所以此时可以把日志等级设置为最低的debug级。在这种情况下,如果觉得调试日志太多,Nginx提供按模块控制的更细粒等级:debug_core、debug_alloc、debug_mutex、debug_event、debug_http、debug_imap。比如如果只想看http的调试日志,则需做如下设置。</p>
<p class="left">error_log logs/error.log debug_http;</p>
<p class="left">此时Nginx将输出从info到emerg所有等级的日志信息,而debug日志则将只输出与http模块相关的内容。</p>
<p class="left">error_log配置指令可以放在配置文件的多个上下文内,比如main、http、server、location,但同一个上下文中只能设置一个error_log,否则Nginx将提示类似如下这样的错误。</p>
<p class="left">nginx: [emerg] "error_log" directive is duplicate in /usr/local/nginx/conf/ nginx.conf:9</p>
<p class="left">但在不同的配置文件上下文里可以设置各自的error_log配置指令,通过设置不同的日志文件,这是Nginx提供的又一种信息切割过滤手段。</p>
<p class="left">00: 代码片段2.2-3,文件名: example.conf</p>
<p class="left">01: ...</p>
<p class="left">02: error_log logs/error.log error;</p>
<p class="left">03: ...</p>
<p class="left">04: http {</p>
<p class="left">05:  error_log logs/http.log debug;</p>
<p class="left">06:  ...</p>
<p class="left">07:  server {</p>
<p class="left">08:   ...</p>
<p class="left">09:   error_log logs/server.log debug;</p>
<p class="left">10: ...</p>
<p class="left">Nginx 提供的另一种更有针对性的日志调试信息记录是针对特定连接的,这通过debug_connection配置指令来设置,比如如下设置调试日志仅针对IP地址192.168.1.1和IP段192.168.10.0/24:</p>
<p class="left">11: 代码片段2.2-4,文件名: example.conf</p>
<p class="left">12: events {</p>
<p class="left">13:  debug_connection 192.168.1.1;</p>
<p class="left">14:  debug_connection 192.168.10.0/24;</p>
<p class="left">15: }</p>
<p class="left">Nginx 的日志功能仍在不断改进中,如能利用得好,对于我们跟踪 Nginx 还是非常有帮助的,至少我知道有不少朋友十分习惯于使用C库的printf()函数打印调试,相比之下,Nginx提供的ngx_log_xxx()系列函数要强大得多。</p>
<p class="left" id="bw15"></p>
 #define NGX_CONFIGURE " --with-debug"
 #ifndef NGX_DEBUG
 #define NGX_DEBUG 1
 #endif
#if (NGX_DEBUG)
  {
  char **e;
  for (e = env; *e; e++) {
   ngx_log_debug1(NGX_LOG_DEBUG_CORE, cycle->log, 0, "env: %s", *e);
  }
  }
#endif
\ No newline at end of file
 ...
 error_log logs/error.log error;
 ...
 http {
  error_log logs/http.log debug;
  ...
  server {
   ...
   error_log logs/server.log debug;
 ...
\ No newline at end of file
 events {
  debug_connection 192.168.1.1;
  debug_connection 192.168.10.0/24;
 }
\ No newline at end of file
<p class="left">Linux下有两个命令strace<a id="ac9"><sup>[9]</sup></a>和ltrace<a id="ac10"><sup>[10]</sup></a>可以分别用来查看一个应用程序在运行过程中所发起的系统函数调用和动态库函数调用,这对作为标准应用程序的Nginx自然同样可用。由于这两个命令大同小异,下面就仅以strace为例做简单介绍,大致了解一些它能帮助我们获取哪些有用的调试信息。关于strace/ltrace以及后面介绍的pstack更多的用法请参考对应的Man手册。</p>
<p class="left">从strace的Man手册可以看到几个有用的选项。</p>
<p class="left">• -ppid:通过进程号来指定被跟踪的进程。</p>
<p class="left">• -ofilename:将跟踪信息输出到指定文件。</p>
<p class="left">• -f:跟踪其通过frok调用产生的子进程。</p>
<p class="left">• -t:输出每一个系统调用的发起时间。</p>
<p class="left">• -T:输出每一个系统调用消耗的时间。</p>
<p class="left">首先利用 ps 命令查看到系统当前存在的 Nginx 进程,然后用 strace 命令的-p 选项跟踪Nginx工作进程,如图2-2所示。</p>
<div class="pic">
<img alt="figure_0037_0004" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0037_0004.jpg">
</div>
<div class="grap">
图2-2 查看Nginx进程
</div>
<p class="left">为了简化操作,我这里只设定了一个工作进程,该工作进程会停顿在epoll_wait系统调用上,这是合理的,因为在没有客户端请求时,Nginx就阻塞于此(除非是在争用accept_mutex锁),在另一终端执行wget命令向Nginx发出http请求后,再来看strace的输出,如图2-3所示。</p>
<p class="left">[root@localhost ~]# wget 127.0.0.1</p>
<div class="pic">
<img alt="figure_0038_0005" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0038_0005.jpg">
</div>
<div class="grap">
图2-3 strace的输出
</div>
<p class="left">通过strace的输出可以看到Nginx工作进程在处理一次客户端请求过程中发起的所有系统调用。我这里测试请求的HTML非常简单,没有附带css、js、jpg等文件,所以看到的输出也比较简单。strace输出的每一行记录一次系统调用,等号左边是系统调用名以及调用参数,等号右边是该系统调用的返回值。逐一说明如下所述。</p>
<p class="left">1.epoll_wait 返回值为1,表示有1 个描述符存在可读/写事件,这里当然是可读事件。</p>
<p class="left">2.accept4 接受该请求,返回的数字3表示socket的文件描述符。</p>
<p class="left">3.epoll_ctl 把accept4 建立的socket 套接字(注意参数3)加入到事件监听机制里。</p>
<p class="left">4.recv 从发生可读事件的 socket 文件描述符内读取数据,读取的数据存在第二个参数内,读取了107个字节。</p>
<p class="left">5.stat64 判断客户端请求的html 文件是否存在,返回值为0 表示存在。</p>
<p class="left">6.open/fstat64 打开并获取文件状态信息。open 文件返回的文件描述符为 9,后面几个系统调用都用到这个值。</p>
<p class="left">7.writev 把响应头通过文件描述符3 代表的socket套接字发给客户端。</p>
<p class="left">8.sendfile64 把文件描述符9 代表的响应体通过文件描述符3 代表的socket 套接字发给客户端。</p>
<p class="left">9.再往文件描述符4 代表的日志文件内write 一条日志信息。</p>
<p class="left">10.recv 看客户端是否还发了其他待处理的请求/信息。</p>
<p class="left">11.最后关闭文件描述符3代表的socket 套接字。</p>
<p class="left">由于strace能够提供Nginx执行过程中的这些内部信息,所以在出现一些奇怪现象时,比如Nginx 启动失败、响应的文件数据和预期不一致、莫名其妙的Segment ation Fault 段错误、存在性能瓶颈(利用-T选项跟踪各个函数的消耗时间),利用strace也许能提供一些相关帮助。最后,要退出strace跟踪,按Ctrl+C即可。</p>
<p class="left">命令strace跟踪的是系统调用,对于Nginx本身的函数调用关系无法给出更为明朗的信息,如果我们发现Nginx当前运行不正常,想知道Nginx当前内部到底在执行什么函数,那么命令pstack就是一个非常方便实用的工具。</p>
<p class="left">pstack的使用也非常简单,后面跟进程ID即可。比如在无客户端请求的情况下,Nginx阻塞在epoll_wait系统调用处,此时利用pstack查看到的Nginx函数调用堆栈关系,如图2-4所示。</p>
<div class="pic">
<img alt="figure_0039_0006" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0039_0006.jpg">
</div>
<div class="grap">
图2-4 Nginx 函数调用堆栈关系
</div>
<p class="left">从main()函数到epoll_wait()函数的调用关系一目了然,和在gdb内看到的堆栈信息一模一样,其实命令pstack本身也就是一个利用gdb实现的Shell脚本,关于这点,感兴趣的读者可以自己看下pstack对应的脚本程序。</p>
<p class="left" id="bw16"></p>
<p class="left">利用strace命令能帮助我们获取到Nginx在运行过程中所发起的所有系统调用,但是不能看到Nginx内部各个函数的调用情况。利用gdb调试Nginx能让我们很清晰地获得Nginx每一步的执行流程,但是单步调试毕竟是非常麻烦的,有没有更为方便的方法一次性获得Nginx程序执行的整个流程呢?答案是肯定的,而且方法还非常多<a id="ac11"><sup>[11]</sup></a>。虽然相比直接使用某些强大工具(如System Tap<a id="ac12"><sup>[12]</sup></a>)而言,下面要介绍的方法比较笨,但它的确可行,而且从这个过程中也许能学到一些额外的知识。我们只需利用gcc的一个名为-finstrument-functions<a id="ac13"><sup>[13]</sup></a>的编译选项,再加上一些我们自己的处理,就可以达到既定目的。关于gcc提供的这个-finstrument-functions选项,这里不做过多介绍,我们只需明白它提供的是一种函数调用记录追踪功能。关于这些,感兴趣的读者请直接参考gcc官网手册,下面来看获得Nginx程序完整执行流程的具体操作。</p>
<p class="left">首先,我们准备两个文件,文件名和文件内容分别如下。</p>
<p class="left">00: 代码片段2.4-1,文件名: my_debug.h</p>
<p class="left">01: #ifndef MY_DEBUG_LENKY_H</p>
<p class="left">02: #define MY_DEBUG_LENKY_H</p>
<p class="left">03: #include <stdio.h></p>
<p class="left">04:</p>
<p class="left">05: void enable_my_debug( void ) __attribute__((no_instrument_function));</p>
<p class="left">06: void disable_my_debug( void ) __attribute__((no_instrument_function));</p>
<p class="left">07: int get_my_debug_flag( void ) __attribute__((no_instrument_function));</p>
<p class="left">08: void set_my_debug_flag( int ) __attribute__((no_instrument_function));</p>
<p class="left">09: void main_constructor( void ) __attribute__((no_instrument_function, constructor));</p>
<p class="left">10: voidmain_destructor(void) __attribute__((no_instrument_function,destructor));</p>
<p class="left">11: void __cyg_profile_func_enter(void *,void *) __attribute__((no_instrument_function));</p>
<p class="left">12: void __cyg_profile_func_exit( void *, void *) __attribute__((no_instrument_ function));</p>
<p class="left">13:</p>
<p class="left">14: #ifndef MY_DEBUG_MAIN</p>
<p class="left">15: extern FILE *my_debug_fd;</p>
<p class="left">16: #else</p>
<p class="left">17: FILE *my_debug_fd;</p>
<p class="left">18: #endif</p>
<p class="left">19: #endif</p>
<p class="left">00: 代码片段2.4-2,文件名: my_debug.c</p>
<p class="left">01: #include "my_debug.h"</p>
<p class="left">02: #define MY_DEBUG_FILE_PATH "/usr/local/nginx/sbin/mydebug.log"</p>
<p class="left">03: int _flag = 0;</p>
<p class="left">04:</p>
<p class="left">05: #define open_my_debug_file() \</p>
<p class="left">06:  (my_debug_fd = fopen(MY_DEBUG_FILE_PATH, "a"))</p>
<p class="left">07:</p>
<p class="left">08: #define close_my_debug_file() \</p>
<p class="left">09:  do { \</p>
<p class="left">10:   if (NULL != my_debug_fd) { \</p>
<p class="left">11:    fclose(my_debug_fd); \</p>
<p class="left">12:   } \</p>
<p class="left">13:  }while(0)</p>
<p class="left">14:</p>
<p class="left">15: #define my_debug_print(args, fmt...) \</p>
<p class="left">16:  do{ \</p>
<p class="left">17:   if (0 == _flag) { \</p>
<p class="left">18:    break; \</p>
<p class="left">19:   } \</p>
<p class="left">20:   if (NULL == my_debug_fd && NULL == open_my_debug_file()) { \</p>
<p class="left">21:    printf("Err: Can not open output file.\n"); \</p>
<p class="left">22:    break; \</p>
<p class="left">23:   } \</p>
<p class="left">24:   fprintf(my_debug_fd, args, ##fmt); \</p>
<p class="left">25:   fflush(my_debug_fd); \</p>
<p class="left">26:  }while(0)</p>
<p class="left">27:</p>
<p class="left">28: void enable_my_debug( void )</p>
<p class="left">29: {</p>
<p class="left">30:  _flag = 1;</p>
<p class="left">31: }</p>
<p class="left">32: void disable_my_debug( void )</p>
<p class="left">33: {</p>
<p class="left">34:  _flag = 0;</p>
<p class="left">35: }</p>
<p class="left">36: int get_my_debug_flag( void )</p>
<p class="left">37: {</p>
<p class="left">38:  return _flag;</p>
<p class="left">39: }</p>
<p class="left">40: void set_my_debug_flag( int flag )</p>
<p class="left">41: {</p>
<p class="left">42:  _flag = flag;</p>
<p class="left">43: }</p>
<p class="left">44: void main_constructor( void )</p>
<p class="left">45: {</p>
<p class="left">46:  //Do Nothing</p>
<p class="left">47: }</p>
<p class="left">48: void main_destructor( void )</p>
<p class="left">49: {</p>
<p class="left">50:  close_my_debug_file();</p>
<p class="left">51: }</p>
<p class="left">52: void __cyg_profile_func_enter( void *this, void *call )</p>
<p class="left">53: {</p>
<p class="left">54:  my_debug_print("Enter\n%p\n%p\n", call, this);</p>
<p class="left">55: }</p>
<p class="left">56: void __cyg_profile_func_exit( void *this, void *call )</p>
<p class="left">57: {</p>
<p class="left">58:  my_debug_print("Exit\n%p\n%p\n", call, this);</p>
<p class="left">59: }</p>
<p class="left">这两个文件是我2009年写的,比较乱,不过够用且测试无误,所以我这里也就直接先用它。将这两个文件放到/nginx-1.2.0/src/core/目录下,然后编辑/nginx-1.2.0/objs/Makefile文件,给CFLAGS选项增加-finstrument-functions选项。</p>
<p class="left">02: 代码片段2.4-3,文件名: Makefile</p>
<p class="left">03: CFLAGS = -pipe -O0 -W -Wall -Wpointer-arith -Wno-unused-parameter -Wunused-function -Wunused-va  riable -Wunused-value -Werror -g -finstrument-functions</p>
<p class="left">接着,需要将my_debug.h和my_debug.c引入到Nginx源码里一起编译,所以继续修改/nginx-1.2.0/objs/Makefile文件,根据Nginx的Makefile文件特点,修改的地方主要有如下几处。</p>
<p class="left">00: 代码片段2.4-4,文件名: Makefile</p>
<p class="left">01: …</p>
<p class="left">18: CORE_DEPS = src/core/nginx.h \</p>
<p class="left">19:  src/core/my_debug.h \</p>
<p class="left">20: …</p>
<p class="left">84: HTTP_DEPS = src/http/ngx_http.h \</p>
<p class="left">85:  src/core/my_debug.h \</p>
<p class="left">86: …</p>
<p class="left">102: objs/nginx: objs/src/core/nginx.o \</p>
<p class="left">103:   objs/src/core/my_debug.o \</p>
<p class="left">104: …</p>
<p class="left">211:   $(LINK) -o objs/nginx \</p>
<p class="left">212:   objs/src/core/my_debug.o \</p>
<p class="left">213: …</p>
<p class="left">322: objs/src/core/my_debug.o: $(CORE_DEPS) \</p>
<p class="left">323:   src/core/my_debug.c</p>
<p class="left">324:   $(CC) -c $(CFLAGS) $(CORE_INCS) \</p>
<p class="left">325:     -o objs/src/core/my_debug.o \</p>
<p class="left">326:     src/core/my_debug.c</p>
<p class="left">327: …</p>
<p class="left">为了在 Nginx 源码里引入 my_debug,这需要在 Nginx 所有源文件都包含有头文件 my_debug.h,当然没必要每个源文件都去添加对这个头文件的引入,我们只需要在头文件ngx_core.h内加入对my_debug.h文件的引入即可,这样其他Nginx的源文件就间接地引入了这个文件。</p>
<p class="left">37: 代码片段2.4-5,文件名: ngx_core.h</p>
<p class="left">38: #include "my_debug.h"</p>
<p class="left">在源文件nginx.c的最前面加上对宏MY_DEBUG_MAIN的定义,以使得Nginx程序有且仅有一个my_debug_fd变量的定义。</p>
<p class="left">06: 代码片段2.4-6,文件名: nginx.c</p>
<p class="left">07: #define MY_DEBUG_MAIN 1</p>
<p class="left">08:</p>
<p class="left">09: #include <ngx_config.h></p>
<p class="left">10: #include <ngx_core.h></p>
<p class="left">11: #include <nginx.h></p>
<p class="left">最后就是根据我们想要截取的执行流程,在适当的位置调用函数enable_my_debug();和函数 disable_my_debug();,这里仅作测试,直接在 main 函数入口处调用 enable_my_debug();,而disable_my_debug();函数就不调用了。</p>
<p class="left">200: 代码片段2.4-7,文件名: nginx.c</p>
<p class="left">201: main(int argc, char *const *argv)</p>
<p class="left">202: {</p>
<p class="left">203: …</p>
<p class="left">208: enable_my_debug();</p>
<p class="left">至此,代码增补工作已经完成,重新编译Nginx,如果之前已编译过Nginx,那么需记得先把Nginx源文件的时间戳进行刷新。</p>
<p class="left">以单进程模式运行 Nginx,并且在配置文件里将日志功能的记录级别设置低一点,否则将有大量的日志函数调用堆栈信息,经过这样的设置后,我们才能获得更清晰的 Nginx 执行流程,即配置文件里做如下设置。</p>
<p class="left">00: 代码片段2.4-8,文件名: nginx.c</p>
<p class="left">01: master_process off;</p>
<p class="left">02: error_log logs/error.log emerg;</p>
<p class="left">正常运行后的Nginx将产生一个记录程序执行流程的文件,这个文件会随着Nginx的持续运行迅速增大,所以在恰当的地方调用 disable_my_debug();函数是非常有必要的,不过我这里在获取到一定量的信息后就直接kill掉Nginx进程了。mydebug.log的内容如下所示。</p>
<p class="left">[root@localhost sbin]# head -n 20 mydebug.log</p>
<p class="left">Enter</p>
<p class="left">0x804a5fc</p>
<p class="left">0x806e2b3</p>
<p class="left">Exit</p>
<p class="left">0x804a5fc</p>
<p class="left">0x806e2b3</p>
<p class="left"></p>
<p class="left">这记录的是Nginx执行函数调用关系,不过这里的函数还只是以对应的地址显示而已,利用另外一个工具 addr2line 可以将这些地址转换回可读的函数名。addr2line 工具在大多数Linux发行版上默认有安装,如果没有那么在官网<a id="ac14"><sup>[14]</sup></a>下载即可,其具体用法也可以参考官网手册<a id="ac15"><sup>[15]</sup></a>。这里我们直接使用,写个addr2line.sh脚本。</p>
<p class="left">00: 代码片段2.4-9,文件名: addr2line.sh</p>
<p class="left">01: #!/bin/sh</p>
<p class="left">02:</p>
<p class="left">03: if [ $# != 3 ]; then</p>
<p class="left">04:  echo 'Usage: addr2line.sh executefile addressfile functionfile'</p>
<p class="left">05:  exit</p>
<p class="left">06: fi;</p>
<p class="left">07:</p>
<p class="left">08: cat $2 | while read line</p>
<p class="left">09: do</p>
<p class="left">10:  if [ "$line" = 'Enter' ]; then</p>
<p class="left">11:   read line1</p>
<p class="left">12:   read line2</p>
<p class="left">13: #  echo $line >> $3</p>
<p class="left">14:   addr2line -e $1 -f $line1 -s >> $3</p>
<p class="left">15:   echo "--->" >> $3</p>
<p class="left">16:   addr2line -e $1 -f $line2 -s | sed 's/^/ /' >> $3</p>
<p class="left">17:   echo >> $3</p>
<p class="left">18:  elif [ "$line" = 'Exit' ]; then</p>
<p class="left">19:   read line1</p>
<p class="left">20:   read line2</p>
<p class="left">21:   addr2line -e $1 -f $line2 -s | sed 's/^/ /' >> $3</p>
<p class="left">22:   echo "<---" >> $3</p>
<p class="left">23:   addr2line -e $1 -f $line1 -s >> $3</p>
<p class="left">24: #  echo $line >> $3</p>
<p class="left">25:   echo >> $3</p>
<p class="left">26:  fi;</p>
<p class="left">27: done</p>
<p class="left">执行addr2line.sh进行地址与函数名的转换,这个过程挺慢的,因为从上面的Shell脚本可以看到对于每一个函数地址都调用addr2line进行转换,执行效率完全没有考虑,不过够用就好,如果非要追求高效率,直接写个C程序来做这个转换工作当然也是可以的。</p>
<p class="left">[root@localhost sbin]# vi addr2line.sh</p>
<p class="left">[root@localhost sbin]# chmod a+x addr2line.sh</p>
<p class="left">[root@localhost sbin]# ./addr2line.sh nginx mydebug.log myfun.log</p>
<p class="left">[root@localhost sbin]# head -n 12 myfun.log</p>
<p class="left">main</p>
<p class="left">nginx.c:212</p>
<p class="left">---></p>
<p class="left marg-left2">ngx_strerror_init</p>
<p class="left marg-left2">ngx_errno.c:47</p>
<p class="left marg-left2">ngx_strerror_init</p>
<p class="left marg-left2">ngx_errno.c:47</p>
<p class="left"><---</p>
<p class="left">main</p>
<p class="left">nginx.c:212</p>
<p class="left"></p>
<p class="left">关于如何获得 Nginx 程序执行流程的方法大体就是上面描述的这样,不过这里介绍得很粗略,写的代码也仅只是作为示范使用,关于 gcc 以及相关工具的更深入研究已不在本书的讨论范围之内,如感兴趣可查看上文中提供的相关链接。</p>
<p class="left" id="bw17"></p>
 #ifndef MY_DEBUG_LENKY_H
 #define MY_DEBUG_LENKY_H
 #include
 void enable_my_debug( void ) __attribute__((no_instrument_function));
 void disable_my_debug( void ) __attribute__((no_instrument_function));
 int get_my_debug_flag( void ) __attribute__((no_instrument_function));
 void set_my_debug_flag( int ) __attribute__((no_instrument_function));
 void main_constructor( void ) __attribute__((no_instrument_function, constructor));
 voidmain_destructor(void) __attribute__((no_instrument_function,destructor));
 void __cyg_profile_func_enter(void *,void *) __attribute__((no_instrument_function));
 void __cyg_profile_func_exit( void *, void *) __attribute__((no_instrument_ function));
 #ifndef MY_DEBUG_MAIN
 extern FILE *my_debug_fd;
 #else
 FILE *my_debug_fd;
 #endif
 #endif
 #include "my_debug.h"
 #define MY_DEBUG_FILE_PATH "/usr/local/nginx/sbin/mydebug.log"
 int _flag = 0;
 #define open_my_debug_file() \
  (my_debug_fd = fopen(MY_DEBUG_FILE_PATH, "a"))
 #define close_my_debug_file() \
  do { \
   if (NULL != my_debug_fd) { \
    fclose(my_debug_fd); \
   } \
  }while(0)
 #define my_debug_print(args, fmt...) \
  do{ \
   if (0 == _flag) { \
    break; \
   } \
   if (NULL == my_debug_fd && NULL == open_my_debug_file()) { \
    printf("Err: Can not open output file.\n"); \
    break; \
   } \
   fprintf(my_debug_fd, args, ##fmt); \
   fflush(my_debug_fd); \
  }while(0)
 void enable_my_debug( void )
 {
  _flag = 1;
 }
 void disable_my_debug( void )
 {
  _flag = 0;
 }
 int get_my_debug_flag( void )
 {
  return _flag;
 }
 void set_my_debug_flag( int flag )
 {
  _flag = flag;
 }
 void main_constructor( void )
 {
  //Do Nothing
 }
 void main_destructor( void )
 {
  close_my_debug_file();
 }
 void __cyg_profile_func_enter( void *this, void *call )
 {
  my_debug_print("Enter\n%p\n%p\n", call, this);
 }
 void __cyg_profile_func_exit( void *this, void *call )
 {
  my_debug_print("Exit\n%p\n%p\n", call, this);
 }
\ No newline at end of file
 CFLAGS = -pipe -O0 -W -Wall -Wpointer-arith -Wno-unused-parameter -Wunused-function -Wunused-va  riable -Wunused-value -Werror -g -finstrument-functions
\ No newline at end of file
 …
 CORE_DEPS = src/core/nginx.h \
  src/core/my_debug.h \
 …
 HTTP_DEPS = src/http/ngx_http.h \
  src/core/my_debug.h \
 …
objs/nginx: objs/src/core/nginx.o \
   objs/src/core/my_debug.o \
   $(LINK) -o objs/nginx \
   objs/src/core/my_debug.o \
objs/src/core/my_debug.o: $(CORE_DEPS) \
   src/core/my_debug.c
   $(CC) -c $(CFLAGS) $(CORE_INCS) \
     -o objs/src/core/my_debug.o \
     src/core/my_debug.c
\ No newline at end of file
 #define MY_DEBUG_MAIN 1
 #include
 #include
 #include
\ No newline at end of file
main(int argc, char *const *argv)
{
enable_my_debug();
\ No newline at end of file
 master_process off;
 error_log logs/error.log emerg;
\ No newline at end of file
 #!/bin/sh
 if [ $# != 3 ]; then
  echo 'Usage: addr2line.sh executefile addressfile functionfile'
  exit
 fi;
 cat $2 | while read line
 do
  if [ "$line" = 'Enter' ]; then
   read line1
   read line2
 #  echo $line >> $3
   addr2line -e $1 -f $line1 -s >> $3
   echo "--->" >> $3
   addr2line -e $1 -f $line2 -s | sed 's/^/ /' >> $3
   echo >> $3
  elif [ "$line" = 'Exit' ]; then
   read line1
   read line2
   addr2line -e $1 -f $line2 -s | sed 's/^/ /' >> $3
   echo "> $3
   addr2line -e $1 -f $line1 -s >> $3
 #  echo $line >> $3
   echo >> $3
  fi;
 done
\ No newline at end of file
<p class="left">如果我们对代码做过单元测试,那么肯定知道加桩的概念,简单点说就是为了让一个模块执行起来,额外添加的一些支撑代码。比如,我要简单测试一个实现某种排序算法的子函数的功能是否正常,那么我也许需要写一个 main()函数,设置一个数组,提供一些乱序的数据,然后利用这些数据调用排序子函数(假设它提供的接口就是对数组的排序),然后 printf打印排序后的结果,看是否排序正常,所有写的这些额外代码(main()函数、数组、printf打印)就是桩代码。</p>
<p class="left">上面提到的这种用于单元测试的方法,同样也可以用来深度调试 Nginx 内部逻辑,而且Nginx很多的基础实现(比如slab机制、红黑树、chain链、array数组等)都比较独立,要调试它们只需提供少量的桩代码即可。</p>
<p class="left">以Nginx的slab机制为例,我们通过下面所提供的这些桩代码即可调试该功能的具体实现。Nginx 的 slab 机制用于对多进程共享内存的管理,不过单进程也是一样的执行逻辑,除了加/解锁直通以外(即加锁时必定成功),所以我们采取最简单的办法,直接在 Nginx 本身的main()函数内插入我们的桩代码。当然,必须根据具体情况把桩代码放在合适的调用位置,比如这里的slab机制就依赖一些全局变量(像ngx_pagesize等),所以需要把桩代码的调用位置放在这些全局变量的初始化之后。</p>
<p class="left">197: 代码片段2.5-1,文件名: nginx.c</p>
<p class="left">198: void ngx_slab_test()</p>
<p class="left">199: {</p>
<p class="left">200:  ngx_shm_t shm;</p>
<p class="left">201:  ngx_slab_pool_t *sp;</p>
<p class="left">202:  u_char *file;</p>
<p class="left">203:  void *one_page;</p>
<p class="left">204:  void *two_page;</p>
<p class="left">205:</p>
<p class="left">206:  ngx_memzero(&shm, sizeof(shm));</p>
<p class="left">207:  shm.size = 4 * 1024 * 1024;</p>
<p class="left">208:  if (ngx_shm_alloc(&shm) != NGX_OK) {</p>
<p class="left">209:   goto failed;</p>
<p class="left">210:  }</p>
<p class="left">211:</p>
<p class="left">212:  sp = (ngx_slab_pool_t *) shm.addr;</p>
<p class="left">213:  sp->end = shm.addr + shm.size;</p>
<p class="left">214:  sp->min_shift = 3;</p>
<p class="left">215:  sp->addr = shm.addr;</p>
<p class="left">216:</p>
<p class="left">217: #if (NGX_HAVE_ATOMIC_OPS)</p>
<p class="left">218:  file = NULL;</p>
<p class="left">219: #else</p>
<p class="left">220:  #error must support NGX_HAVE_ATOMIC_OPS.</p>
<p class="left">221: #endif</p>
<p class="left">222:  if (ngx_shmtx_create(&sp->mutex, &sp->lock, file) != NGX_OK) {</p>
<p class="left">223:   goto failed;</p>
<p class="left">224:  }</p>
<p class="left">225:</p>
<p class="left">226:  ngx_slab_init(sp);</p>
<p class="left">227:</p>
<p class="left">228:  one_page = ngx_slab_alloc(sp, ngx_pagesize);</p>
<p class="left">229:  two_page = ngx_slab_alloc(sp, 2 * ngx_pagesize);</p>
<p class="left">230:</p>
<p class="left">231:  ngx_slab_free(sp, one_page);</p>
<p class="left">232:  ngx_slab_free(sp, two_page);</p>
<p class="left">233:</p>
<p class="left">234:  ngx_shm_free(&shm);</p>
<p class="left">235:</p>
<p class="left">236:  exit(0);</p>
<p class="left">237: failed:</p>
<p class="left">238:  printf("failed.\n");</p>
<p class="left">239:  exit(-1);</p>
<p class="left">240: }</p>
<p class="left">241: …</p>
<p class="left">353:  if (ngx_os_init(log) != NGX_OK) {</p>
<p class="left">354:   return 1;</p>
<p class="left">355:  }</p>
<p class="left">356:</p>
<p class="left">357:  ngx_slab_test();</p>
<p class="left">358: …</p>
<p class="left">上面是修改之后的nginx.c源文件,直接make后生成新的Nginx,不过这个可执行文件不再是一个Web服务器,而是一个简单的调试slab机制的辅助程序。可以看到,程序在进入main()函数后先做一些初始化工作,然后通过 ngx_slab_test()函数调入到桩代码内执行调试逻辑,完成既定目标后便直接exit()退出整个程序。</p>
<p class="left">正常运行时,Nginx 本身对内存的申请与释放是不可控的,所以直接去调试 Nginx 内存管理的 slab 机制的相关代码逻辑非常困难,利用这种加桩的办法,ngx_slab_alloc()申请内存和ngx_slab_free()释放内存都能精确控制,对每一次内存的申请或释放后,slab机制的内部结构发生了怎样的变化都能准确地掌握,对其相关逻辑的理解也就没有那么困难了。</p>
<p class="left" id="bw18"></p>
void ngx_slab_test()
{
  ngx_shm_t shm;
  ngx_slab_pool_t *sp;
  u_char *file;
  void *one_page;
  void *two_page;
  ngx_memzero(&shm, sizeof(shm));
  shm.size = 4 * 1024 * 1024;
  if (ngx_shm_alloc(&shm) != NGX_OK) {
   goto failed;
  }
  sp = (ngx_slab_pool_t *) shm.addr;
  sp->end = shm.addr + shm.size;
  sp->min_shift = 3;
  sp->addr = shm.addr;
#if (NGX_HAVE_ATOMIC_OPS)
  file = NULL;
#else
  #error must support NGX_HAVE_ATOMIC_OPS.
#endif
  if (ngx_shmtx_create(&sp->mutex, &sp->lock, file) != NGX_OK) {
   goto failed;
  }
  ngx_slab_init(sp);
  one_page = ngx_slab_alloc(sp, ngx_pagesize);
  two_page = ngx_slab_alloc(sp, 2 * ngx_pagesize);
  ngx_slab_free(sp, one_page);
  ngx_slab_free(sp, two_page);
  ngx_shm_free(&shm);
  exit(0);
failed:
  printf("failed.\n");
  exit(-1);
}
  if (ngx_os_init(log) != NGX_OK) {
   return 1;
  }
  ngx_slab_test();
\ No newline at end of file
<p class="left">前面所讲的调试方法都是针对 Nginx 本身很容易跑到的逻辑,而对于某些只有在特定情况下才会被执行到的代码,又该怎样去调试呢?举个例子,我们知道 Nginx 里有大量的超时处理,比如,如果读取客户端请求头部数据超时,Nginx 就将执行对应的超时处理函数,假设我想通过单步执行的方式来了解这部分相关逻辑,无疑就得让 Nginx 的执行逻辑走到这条路径上来。由于此时影响 Nginx 行为的决定因素是客户端所发送的请求头部数据,我们就必须在客户端做动作来构造出这种场景。一般的浏览器,如IE、Firefox等发出请求的行为基本已经固定,而常用的命令行工具,比如 curl、wget 的源代码又略显复杂,定制它们的请求动作和改变环境来构造所需的场景相对较为麻烦,所以一种更便利的方法就是我们自己写个socket通信的客户端即可,而这并不需要多少代码。</p>
<p class="left">下面给出一个测试示例用代码,为了简单,所以服务器IP和端口都是固定在代码里的,用于发送数据的函数write()调用也未做返回值判断等(后续还有其他类似测试代码也是如此,这点请注意)。</p>
<p class="left">00: 代码片段2.6-1,文件名: request_timeout.c</p>
<p class="left">01: /**</p>
<p class="left">02: * gcc -Wall -g -o request_timeout request_timeout.c</p>
<p class="left">03: */</p>
<p class="left">04: #include <sys/types.h></p>
<p class="left">05: #include <stdio.h></p>
<p class="left">06: #include <stdlib.h></p>
<p class="left">07: #include <string.h></p>
<p class="left">08: #include <errno.h></p>
<p class="left">09: #include <sys/socket.h></p>
<p class="left">10: #include <netinet/in.h></p>
<p class="left">11: #include <arpa/inet.h></p>
<p class="left">12: #include <unistd.h></p>
<p class="left">13:</p>
<p class="left">14: //charreq_header[]="GET/HTTP/1.1\r\nUser-Agent:curl/7.19.7\r\nHost:127.0.0.1\r\nAccept: */*\r\n\r\n";</p>
<p class="left">15: char req_header[] = "GET / HTTP/1.1\r\nUser-Agent: curl/7.19.7\r\n";</p>
<p class="left">16:</p>
<p class="left">17: int main(int argc, char *const *argv)</p>
<p class="left">18: {</p>
<p class="left">19:  int sockfd;</p>
<p class="left">20:  struct sockaddr_in server_addr;</p>
<p class="left">21:</p>
<p class="left">22:  if ((sockfd = socket (AF_INET, SOCK_STREAM, 0)) == -1) {</p>
<p class="left">23:   fprintf (stderr, "Socket error,%s\r\n", strerror (errno));</p>
<p class="left">24:   return -1;</p>
<p class="left">25:  }</p>
<p class="left">26:</p>
<p class="left">27:  bzero (&server_addr, sizeof (server_addr));</p>
<p class="left">28:  server_addr.sin_family = AF_INET;</p>
<p class="left">29:  server_addr.sin_port = htons (80);</p>
<p class="left">30:</p>
<p class="left">31:  if(!inet_aton("192.168.1.1", &server_addr.sin_addr)) {</p>
<p class="left">32:   fprintf (stderr, "Bad address:%s\r\n", strerror (errno));</p>
<p class="left">33:   close (sockfd);</p>
<p class="left">34:   return -1;</p>
<p class="left">35:  }</p>
<p class="left">36:</p>
<p class="left">37:  if (connect (sockfd, (struct sockaddr *) (&server_addr),</p>
<p class="left">38:   sizeof (struct sockaddr)) == -1) {</p>
<p class="left">39:   fprintf (stderr, "Connect Error:%s\r\n", strerror (errno));</p>
<p class="left">40:   close (sockfd);</p>
<p class="left">41:   return -1;</p>
<p class="left">42:  }</p>
<p class="left">43:</p>
<p class="left">44:  write (sockfd, req_header, strlen(req_header));</p>
<p class="left">45:</p>
<p class="left">46:  close (sockfd);</p>
<p class="left">47:  return 0;</p>
<p class="left">48: }</p>
<p class="left">该程序的代码比较简单,变量req_header存储的是http请求头部数据,被注释掉的是正常的请求头,而我这里使用的请求头是不完整的(正常请求头可以用wget、curl或wireshark<a id="ac16"><sup>[16]</sup></a>等工具获得,异常请求头必须根据自己所预期场景来进行构造,比如在这里,其他异常情况的请求头可能导致Nginx以其他错误方式返回而不是进行超时监控),所以这会使得Nginx在接收到该请求后,持续等待进一步的头部数据,直到超时。编译这个源代码得到应用程序request_timeout。</p>
<p class="left">将接受http请求的Nginx工作进程绑定到gdb,然后在超时函数ngx_event_expire_timers()内的第149行下断点并按c继续。</p>
<p class="left">75: 代码片段2.6-2,文件名: ngx_event_timer.c</p>
<p class="left">76: void</p>
<p class="left">77: ngx_event_expire_timers(void)</p>
<p class="left">78: {</p>
<p class="left">79: …</p>
<p class="left">147:    ev->timedout = 1;</p>
<p class="left">148:</p>
<p class="left">149:    ev->handler(ev);</p>
<p class="left">这个断点是Nginx已经捕获到超时事件,设置其超时旗标并调用对应的回调函数进行处理。在另一个gdb内执行request_timeout,当然,我们需要让它停止在第47行<a id="ac17"><sup>[17]</sup></a>,避免程序退出,导致它与Nginx工作进程之间的连接断开。等待约 60 秒(Nginx读取请求头部数据的默认超时时间为60秒,可通过配置指令client_header_timeout修改)后,attach到Nginx工作进程的gdb就会断下来,按s跟进函数,再顺着执行路径而下就会发现此时Nginx将执行到这个逻辑里。</p>
<p class="left">955: 代码片段2.6-3,文件名: ngx_event_timer.c</p>
<p class="left">956: static void</p>
<p class="left">957: ngx_http_process_request_headers(ngx_event_t *rev)</p>
<p class="left">958: {</p>
<p class="left">959: …</p>
<p class="left">976:  if (rev->timedout) {</p>
<p class="left">977:   ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");</p>
<p class="left">978:   c->timedout = 1;</p>
<p class="left">979:   ngx_http_close_request(r, NGX_HTTP_REQUEST_TIME_OUT);</p>
<p class="left">980:   return;</p>
<p class="left">981:  }</p>
<p class="left">将执行到第976行的if判断内部,即连接超时,我们看到对于在读取请求头部数据超时的情况下,Nginx 工作进程最后所做的几步主要工作,即日志记录、关闭请求并返回。通过这样一个实例,我们也就了解了如何去调试这样的特殊应用逻辑,不仅仅只是针对客户端,对于后端应用服务器也能如此进行模拟构造。</p>
<p class="left">上面演示的环境构造步骤,虽然比较简单且能真实模拟,但毕竟需要我们了解它的细节,也就是需知道触发这种情况的前提条件,如果前提条件比较多,那么模拟起来可能还是比较麻烦,其实,如果我们只是了解一下 Nginx 如果这样执行会怎么样,那么完全可以通过利用gdb的p命令或set命令修改对应条件变量的值来达到目的。比如在前面的例子里,在一般情况下,rev->timedout为0,即不超时而无法执行第977-980行代码,但我又想看一下执行这几条语句的情况会怎么样,那么就可以像下面这样做。</p>
<p class="left">Breakpoint 1, ngx_http_process_request_headers (rev=0x94a6bfc) at src/http/ ngx_http_request.c:976</p>
<p class="left">976  if (rev->timedout) {</p>
<p class="left">(gdb) p rev->timedout</p>
<p class="left">$1 = 0</p>
<p class="left">(gdb) p rev->timedout=1</p>
<p class="left">$2 = 1</p>
<p class="left">(gdb) n</p>
<p class="left">977   ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");</p>
<p class="left">(gdb) set rev->timedout=0</p>
<p class="left">(gdb) p rev->timedout</p>
<p class="left">$3 = 0</p>
<p class="left">(gdb)</p>
<p class="left">通过执行“prev->timedout=1”把变量rev->timedout的值改为1,这样就执行到第977 行了,当然,如上所示,set命令也可以改变Nginx执行变量的值。值得特别注意的是,这样做仅仅只是因为改变了条件判断的变量值而使得 Nginx 程序执行路径发生变化,但是其在新的路径上,可能由于使用的某些变量值不是原本所期望的情况而导致执行异常。</p>
<p class="left"><b>注 释</b></p>
<p class="footnote"><a id="anchor1">[1].http://www.gnu.org/software/gdb/documentation/。</a></p>
<p class="footnote"><a id="anchor2">[2].默认情况下,应该是已启用-g选项的。</a></p>
<p class="footnote"><a id="anchor3">[3].执行命令".lconfigure--help"可以看到所有参数选项。</a></p>
<p class="footnote"><a id="anchor4">[4].http://sourceware.org/gdb/current/onlinedocs/gdb/Set-Watchpoints.html#Set-Watchpoints</a></p>
<p class="footnote"><a id="anchor5">[5].http://lenky.info/?p=1910</a></p>
<p class="footnote"><a id="anchor6">[6].http://cgdb.github.com/</a></p>
<p class="footnote"><a id="anchor7">[7].http://cgdb.github.com/docs/index.html</a></p>
<p class="footnote"><a id="anchor8">[8].http://lenky.info/?p=1409</a></p>
<p class="footnote"><a id="anchor9">[9].http://sourceforge.net/projects/strace/</a></p>
<p class="footnote"><a id="anchor10">[10].http://www.ltrace.org/</a></p>
<p class="footnote"><a id="anchor11">[11].http://lenky.info/?p=2202</a></p>
<p class="footnote"><a id="anchor12">[12].http://sourceware.org/sysemtop/</a></p>
<p class="footnote"><a id="anchor13">[13].http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html#Code-Gen-Options</a></p>
<p class="footnote"><a id="anchor14">[14].http://sourceware.org/binutils/</a></p>
<p class="footnote"><a id="anchor15">[15].http://sourceware.org/binutils/docs/binutils/addr2line.html。</a></p>
<p class="footnote"><a id="anchor16">[16].http://www.wireshark.org/。</a></p>
<p class="footnote"><a id="anchor17">[17].在此行加个gdb断点进行暂停或在该行后利用sleep()函数进行暂停都可以。</a></p>
\ No newline at end of file
 /**
 * gcc -Wall -g -o request_timeout request_timeout.c
 */
 #include
 #include
 #include
 #include
 #include
 #include
 #include
 #include
 #include
 //charreq_header[]="GET/HTTP/1.1\r\nUser-Agent:curl/7.19.7\r\nHost:127.0.0.1\r\nAccept: */*\r\n\r\n";
 char req_header[] = "GET / HTTP/1.1\r\nUser-Agent: curl/7.19.7\r\n";
 int main(int argc, char *const *argv)
 {
  int sockfd;
  struct sockaddr_in server_addr;
  if ((sockfd = socket (AF_INET, SOCK_STREAM, 0)) == -1) {
   fprintf (stderr, "Socket error,%s\r\n", strerror (errno));
   return -1;
  }
  bzero (&server_addr, sizeof (server_addr));
  server_addr.sin_family = AF_INET;
  server_addr.sin_port = htons (80);
  if(!inet_aton("192.168.1.1", &server_addr.sin_addr)) {
   fprintf (stderr, "Bad address:%s\r\n", strerror (errno));
   close (sockfd);
   return -1;
  }
  if (connect (sockfd, (struct sockaddr *) (&server_addr),
   sizeof (struct sockaddr)) == -1) {
   fprintf (stderr, "Connect Error:%s\r\n", strerror (errno));
   close (sockfd);
   return -1;
  }
  write (sockfd, req_header, strlen(req_header));
  close (sockfd);
  return 0;
 }
\ No newline at end of file
 void
 ngx_event_expire_timers(void)
 {
 …
    ev->timedout = 1;
    ev->handler(ev);
\ No newline at end of file
static void
ngx_http_process_request_headers(ngx_event_t *rev)
{
  if (rev->timedout) {
   ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
   c->timedout = 1;
   ngx_http_close_request(r, NGX_HTTP_REQUEST_TIME_OUT);
   return;
  }
\ No newline at end of file
<h1 class="center"><a>第2章 跟踪与调试</a></h1>
<p class="left">跟踪与调试,不仅是我们解决程序Bug的有力途径,也是帮助我们理解现有代码的有效方法。通过跟踪程序执行的过程,我们可以清楚地了解程序的内部逻辑,对于不明就里的实现细节,调试查看程序内部变量也能更好地帮助我们做出正确的理解。本章将介绍一些跟踪与调试程序的方法,除了最基本的gdb调试,我还将结合个人经验,介绍一些相对高级的应用技巧。</p>
<p class="left" id="bw8"></p>
<p class="left">如前面介绍的那样,正常执行起来后的Nginx会有多个进程,最基本的有master_process(即监控进程,也叫主进程)和worker_process(即工作进程),还可能会有Cache相关进程。这些进程之间会相互进行通信,以传递一些信息(主要是监控进程往工作进程传递)。除了自身进程之间的相互通信,Nginx还凭借强悍的功能模块与外界四通八达,比如通过upstream与后端Web服务器通信、依靠fastcgi与后端应用服务器通信等。一个较为完整的整体框架结构如图3-1所示。</p>
<div class="pic">
<img alt="figure_0051_0007" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0051_0007.jpg">
</div>
<div class="grap">
图3-1 Nginx 整体框架结构图
</div>
<p class="left" id="bw20"></p>
<p class="left">Nginx的进程模型和现在大多数后台服务程序一样,按职责将进程分成监控进程和工作进程两类,启动Nginx的主进程将充当监控进程,而由主进程fork()出来的子进程则充当工作进程。工作进程的任务自然是完成具体的业务逻辑,而监控进程充当整个进程组与用户的交互接口,同时对工作进程进行监护,比如如果某工作进程意外退出,监控进程将重新fork()生成一个新的工作进程。Nginx也可以单进程模型执行,在这种进程模型下,主进程就是工作进程,此时没有监控进程,单进程模型比较简单且官方建议<a id="ac1"><sup>[1]</sup></a>仅供开发与测试使用,所以下面主要分析多进程模型。</p>
<p class="left">分析Nginx多进程模型的入口为主进程的ngx_master_process_cycle()函数,在该函数做完信号处理设置等之后就会调用一个名为 ngx_start_worker_processes()的函数用于 fork()产生出子进程(子进程数目通过函数调用的第二个实参指定),子进程作为一个新的实体开始充当工作进程的角色执行ngx_worker_process_cycle()函数,该函数主体为一个无限for ( ;; )循环,持续不断地处理客户端的服务请求,而主进程继续执行 ngx_master_process_cycle()函数,也就是作为监控进程执行主体for ( ;; )循环,这自然也是一个无限循环,直到进程终止才退出。服务进程基本都是这种写法,所以不用详述,下面先看看图3-2所示的这个模型。</p>
<p class="left">图3-2 表现得很清晰,监控进程和每个工作进程各有一个无限for ( ;; )循环,以便进程持续的等待和处理自己负责的事务,直到进程退出。</p>
<p class="left" id="bw21"></p>
<h3 class="center"><a>3.2.1 监控进程</a></h3>
<p class="left">监控进程的无限for ( ;; )循环内有一个关键的sigsuspend()函数调用,该函数的调用使得监控进程的大部分时间都处于挂起等待状态,直到监控进程接收到信号为止。当监控进程接收到信号时,信号处理函数 ngx_signal_handler()就会被执行。我们知道信号处理函数一般都要求足够简单,所以在该函数内执行的动作主要也就是根据当前信号值对相应的旗标变量做设置,而实际的处理逻辑必须放在主体代码里来进行,所以该for ( ;; )循环接下来的代码就是判断有哪些旗标变量被设置而需要处理的,比如 ngx_reap(有子进程退出?)、ngx_quit 或ngx_terminate(进行要退出或终止?值得注意的是,虽然两个旗标都是表示结束Nginx,不过ngx_quit 的结束更优雅,它会让 Nginx 监控进程做一些清理工作且等待子进程也完全清理并退出之后才终止,而ngx_terminate更为粗暴,不过它通过使用SIGKILL信号能保证在一段时间后必定被结束掉)、ngx_reconfigure(重新加载配置)等。当所有信号都处理完时又挂起在函数sigsuspend()调用处继续等待新的信号,如此反复,构成监控进程的主要执行体。</p>
<div class="pic">
<img alt="figure_0053_0008" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0053_0008.jpg">
</div>
<div class="grap">
图3-2 Nginx 的核心进程模型框图
</div>
<p class="left">82: 代码片段3.2.1-1,文件名: ngx_process_cycle.c</p>
<p class="left">83: void</p>
<p class="left">84: ngx_master_process_cycle(ngx_cycle_t *cycle)</p>
<p class="left">85: {</p>
<p class="left">86: …</p>
<p class="left">146:  for ( ;; ) {</p>
<p class="left">147:   …</p>
<p class="left">170:   sigsuspend(&set);</p>
<p class="left">171:   …</p>
<p class="left">177:   if (ngx_reap) {</p>
<p class="left">178:   …</p>
<p class="left">184:   if (!live && (ngx_terminate || ngx_quit)) {</p>
<p class="left">185:   …</p>
<p class="left">188:   if (ngx_terminate) {</p>
<p class="left">189:   …</p>
<p class="left">210:   if (ngx_quit) {</p>
<p class="left">211:   …</p>
<p class="left">212:  }</p>
<p class="left">213: …</p>
<p class="left" id="bw22"></p>
<h3 class="center"><a>3.2.2 工作进程</a></h3>
<p class="left">工作进程的执行主体与监控进程类似,不过工作进程既然名为工作进程,那么它的主要关注点就是与客户端或后端真实服务器(此时Nginx作为中间代理)之间的数据可读/可写等I/O交互事件,而不是进程信号,所以工作进程的阻塞点是在像select()、epoll_wait()等这样的I/O 多路复用函数调用处,以等待发生数据可读/可写事件,当然,也可能被新收到的进程信号中断。关于I/O多路复用的更多细节,后续章节会详细讲解。</p>
<p class="left">721: 代码片段3.2.2-1,文件名: ngx_process_cycle.c</p>
<p class="left">722: static void</p>
<p class="left">723: ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)</p>
<p class="left">724: {</p>
<p class="left">725: …</p>
<p class="left">780:  for ( ;; ) {</p>
<p class="left">781:</p>
<p class="left">782:   if (ngx_exiting) {</p>
<p class="left">783:   …</p>
<p class="left">806:   ngx_process_events_and_timers(cycle);</p>
<p class="left">807:</p>
<p class="left">808:   if (ngx_terminate) {</p>
<p class="left">809:   …</p>
<p class="left">810:  }</p>
<p class="left">811: …</p>
<p class="left">在代码片段3.2.2-1中,通过函数ngx_process_events_and_timers()调到对应的事件监控阻塞点,即(以epoll_wait()为例)</p>
<p class="left">ngx_process_events_and_timers() -> ngx_process_events()/ngx_epoll_process_ events() ->epoll_wait()</p>
<p class="left">函数epoll_wait()会阻塞等待,一旦有事件发生或收到信号就会立即返回,工作进程也就开始对发生的事件进行逐个处理,关于这部分的具体逻辑,我们暂且不说,等到第7章再看。</p>
<p class="left" id="bw23"></p>
 void
 ngx_master_process_cycle(ngx_cycle_t *cycle)
 {
 …
  for ( ;; ) {
   …
   sigsuspend(&set);
   …
   if (ngx_reap) {
   …
   if (!live && (ngx_terminate || ngx_quit)) {
   …
   if (ngx_terminate) {
   …
   if (ngx_quit) {
   …
  }
\ No newline at end of file
static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
  for ( ;; ) {
   if (ngx_exiting) {
   …
   ngx_process_events_and_timers(cycle);
   if (ngx_terminate) {
   …
  }
\ No newline at end of file
<p class="left">如果Nginx 开启了缓存功能,比如ProxyCache,那么Nginx 还将创建另外两个Cache 相关进程。编写类似如下的Nginx配置文件。</p>
<p class="left">17: 代码片段3.3-1,文件名: nginx.conf</p>
<p class="left">18: worker_processes 1;</p>
<p class="left">19:</p>
<p class="left">20: http {</p>
<p class="left">21: …</p>
<p class="left">22:  proxy_cache_path /data/nginx/cache/one levels=1:2 keys_zone=one:10m;</p>
<p class="left">23:</p>
<p class="left">24:  server {</p>
<p class="left">25:   listen 80;</p>
<p class="left">26:   location / {</p>
<p class="left">27:    proxy_cache one;</p>
<p class="left">28:    proxy_cache_valid 200 302 10m;</p>
<p class="left">29:    proxy_pass http://load_balance;</p>
<p class="left">30:   }</p>
<p class="left">31:  }</p>
<p class="left">以该配置文件启动Nginx后,我们就能看到如下4个进程。</p>
<p class="left">[root@localhost nginx-1.2.0]# ps auxf | grep nginx | grep -v grep</p>
<p class="left">root 16126 0.0 0.1 15460 576 ? Ss 18:42 0:00 nginx: master process objs/nginx -c /usr/local/nginx/conf/nginx.conf</p>
<p class="left">nobody 16127 0.0 0.2 15636 928 ? S 18:42 0:00 \_ nginx: worker process</p>
<p class="left">nobody 16128 0.0 0.2 15612 912 ? S 18:42 0:00 \_ nginx: cache manager process</p>
<p class="left">nobody 16129 0.0 0.2 15612 804 ? S 18:42 0:00 \_ nginx: cache loader process</p>
<p class="left">从前面的介绍中,我们已经知道master process和worker process各自的功能和内部逻辑,而cache manager process(Cache 管理进程)与cache loader process(Cache加载进程)则是与Cache缓存机制相关的进程。它们也是由主进程创建,对应的模型框图如图3-3所示。</p>
<div class="pic">
<img alt="figure_0055_0009" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0055_0009.jpg">
</div>
<div class="grap">
图3-3 Nginx 的Cache进程模型框图
</div>
<p class="left">Cache 进程不处理客户端请求,也就没有监控的 I/O 事件,而其处理的是超时事件,在ngx_process_events_and_timers()函数内执行的事件处理函数只有ngx_event_expire_timers()函数。</p>
<p class="left" id="bw24"></p>
<h3 class="center"><a>3.3.1 Cache管理进程</a></h3>
<p class="left">Cache管理进程与Cache加载进程的主流程都是ngx_cache_manager_process_cycle()函数,但是它们附带的参数不同。管理进程执行到函数ngx_cache_manager_process_cycle()内时,传递的data为ngx_cache_manager_ctx。</p>
<p class="left">68: 代码片段3.3.1-1,文件名: ngx_process_cycle.c</p>
<p class="left">69: static ngx_cache_manager_ctx_t ngx_cache_manager_ctx = {</p>
<p class="left">70:  ngx_cache_manager_process_handler, "cache manager process", 0</p>
<p class="left">71: };</p>
<p class="left">结构体 ngx_cache_manager_ctx_t的定义如下。</p>
<p class="left">29: 代码片段3.3.1-2,文件名: ngx_process_cycle.h</p>
<p class="left">30: typedef struct {</p>
<p class="left">31:  ngx_event_handler_pt  handler;</p>
<p class="left">32:  char      *name;</p>
<p class="left">33:  ngx_msec_t    delay;</p>
<p class="left">34: } ngx_cache_manager_ctx_t;</p>
<p class="left">再看函数 ngx_cache_manager_process_cycle()的具体代码。</p>
<p class="left">1282:代码片段3.3.1-3,文件名: ngx_process_cycle.c</p>
<p class="left">1283:static void</p>
<p class="left">1284:ngx_cache_manager_process_cycle(ngx_cycle_t *cycle, void *data)</p>
<p class="left">1285:{</p>
<p class="left">1286: ngx_cache_manager_ctx_t *ctx = data;</p>
<p class="left">1287:</p>
<p class="left">1288: void  *ident[4];</p>
<p class="left">1289: ngx_event_t ev;</p>
<p class="left">1290:…</p>
<p class="left">1297: ngx_close_listening_sockets(cycle);</p>
<p class="left">1298:</p>
<p class="left">1299: ngx_memzero(&ev, sizeof(ngx_event_t));</p>
<p class="left">1300: ev.handler = ctx->handler;</p>
<p class="left">1301: ev.data = ident;</p>
<p class="left">1302: ev.log = cycle->log;</p>
<p class="left">1303: ident[3] = (void *) -1;</p>
<p class="left">1304:…</p>
<p class="left">1309: ngx_add_timer(&ev, ctx->delay);</p>
<p class="left">Cache管理进程不接收客户端请求,所以在代码第1297行关闭了监听套接口。其他代码创建了一个事件对象并设置了对应的超时事件。注意两点。第一,代码第1303行并没有特别的设定功能,仅只是因为事件对象的data字段一般挂载的是connect对象,此处设置为−1刚好是把 connect 对象的 fd 字段设置为−1,以避免在其他代码里走到异常逻辑。第二,此处ctx->delay为0,因此立即超时,执行对应的函数。</p>
<p class="left">ngx_process_events_and_timers() -> ngx_event_expire_timers() -> ngx_cache_ manager_process_handler()</p>
<p class="left">函数ngx_cache_manager_process_handler()的处理很简单,它会调用每一个磁盘缓存管理对象的manager()函数,然后重新设置事件对象的下一次超时时刻后返回。</p>
<p class="left">1328:代码片段3.3.1-4,文件名: ngx_process_cycle.c</p>
<p class="left">1329:static void</p>
<p class="left">1330:ngx_cache_manager_process_handler(ngx_event_t *ev)</p>
<p class="left">1331:{</p>
<p class="left">1332:…</p>
<p class="left">1338: path = ngx_cycle->pathes.elts;</p>
<p class="left">1339: for (i = 0; i < ngx_cycle->pathes.nelts; i++) {</p>
<p class="left">1340:</p>
<p class="left">1341:  if (path[i]->manager) {</p>
<p class="left">1342:    n = path[i]->manager(path[i]->data);</p>
<p class="left">1343:…</p>
<p class="left">1347:  }</p>
<p class="left">1348: }</p>
<p class="left">1349:…</p>
<p class="left">1354: ngx_add_timer(ev, next * 1000);</p>
<p class="left">1355:}</p>
<p class="left">对于我们这里的示例,对应的manager()函数为ngx_http_file_cache_manager()函数,这是Nginx在调用函数ngx_http_file_cache_set_slot()解析配置指令proxy_cache_path时设置的回调值。函数 ngx_http_file_cache_manager()做了两件事情,首先删除已过期的缓存文件,然后检查缓存文件总大小是否超限,如果超限则进行强制删除。代码如下。</p>
<p class="left">1312:代码片段3.3.1-5,文件名: ngx_http_file_cache.c</p>
<p class="left">1313:static time_t</p>
<p class="left">1314:ngx_http_file_cache_manager(void *data)</p>
<p class="left">1315:{</p>
<p class="left">1316:…</p>
<p class="left">1321: next = ngx_http_file_cache_expire(cache);</p>
<p class="left">1322:…</p>
<p class="left">1326: for ( ;; ) {</p>
<p class="left">1327:…</p>
<p class="left">1336:   size = cache->sh->size;</p>
<p class="left">1337:…</p>
<p class="left">1336:   if (size < cache->max_size) {</p>
<p class="left">1337:    return next;</p>
<p class="left">1338:   }</p>
<p class="left">1339:</p>
<p class="left">1340:   wait = ngx_http_file_cache_forced_expire(cache);</p>
<p class="left">1341:</p>
<p class="left">1342:   if (wait > 0) {</p>
<p class="left">1343:    return wait;</p>
<p class="left">1344:   }</p>
<p class="left">1345:…</p>
<p class="left">1349: }</p>
<p class="left">1350:}</p>
<p class="left">代码逻辑容易理解,代码第1342行的判断为真则表示当前缓存文件(如果存在)都在使用中,所以需直接返回等待,避免CPU 空旋for ( ;; )循序导致CPU 计算能力的浪费。</p>
<p class="left">总结来说,Cache 管理进程的任务就是清理超时缓存文件,限制缓存文件总大小,这个过程反反复复,直到Nginx整个进程退出为止。</p>
<p class="left" id="bw25"></p>
<h3 class="center"><a>3.3.2 Cache加载进程</a></h3>
<p class="left">以3.3-1配置代码执行的Nginx在一开始会有4个进程,但在一段时间后,Cache加载进程将消失,这是因为Cache加载进程的功能是在Nginx正常启动后(具体是60秒)将磁盘中上次缓存的对象加载到内存中。可以看到,这个过程是一次性的,所以当 Cache 加载进程完成它的加载任务后也就自动退出了。</p>
<p class="left">Cache加载进程执行的到ngx_cache_manager_process_cycle()为止的上层函数调用与Cache管理进程一致,但在该函数内设置的事件对象回调函数为ngx_cache_loader_process_handler()。</p>
<p class="left">72: 代码片段3.3.2-1,文件名: ngx_process_cycle.c</p>
<p class="left">73: static ngx_cache_manager_ctx_t ngx_cache_loader_ctx = {</p>
<p class="left">74:  ngx_cache_loader_process_handler, "cache loader process", 60000</p>
<p class="left">75: };</p>
<p class="left">事件对象的超时时间为 60000毫秒。函数 ngx_cache_loader_process_handler()执行的是每一个磁盘缓存管理对象的 loader()回调函数。</p>
<p class="left">1357:代码片段3.3.2-2,文件名: ngx_process_cycle.c</p>
<p class="left">1358:static void</p>
<p class="left">1359:ngx_cache_loader_process_handler(ngx_event_t *ev)</p>
<p class="left">1360:{</p>
<p class="left">1361:…</p>
<p class="left">1367: path = cycle->pathes.elts;</p>
<p class="left">1368: for (i = 0; i < cycle->pathes.nelts; i++) {</p>
<p class="left">1369:…</p>
<p class="left">1374:  if (path[i]->loader) {</p>
<p class="left">1375:    path[i]->loader(path[i]->data);</p>
<p class="left">1376:    ngx_time_update();</p>
<p class="left">1377:  }</p>
<p class="left">1378: }</p>
<p class="left">1379:</p>
<p class="left">1380: exit(0);</p>
<p class="left">1381:}</p>
<p class="left">注意代码第1380行的exit(0)函数调用,可见Cache加载进程的执行逻辑是一次性的。同样的设置流程,对于我们这里的示例,对应的 loader()函数被设置为 ngx_http_file_cache_loader()函数,该函数给磁盘缓存管理对象对应路径下已有的缓存文件建立对应的红黑树,从而让Nginx可以继续使用上次缓存的文件。</p>
<p class="left" id="bw26"></p>
 worker_processes 1;
 http {
 …
  proxy_cache_path /data/nginx/cache/one levels=1:2 keys_zone=one:10m;
  server {
   listen 80;
   location / {
    proxy_cache one;
    proxy_cache_valid 200 302 10m;
    proxy_pass http://load_balance;
   }
  }
\ No newline at end of file
 static ngx_cache_manager_ctx_t ngx_cache_manager_ctx = {
  ngx_cache_manager_process_handler, "cache manager process", 0
 };
\ No newline at end of file
 typedef struct {
  ngx_event_handler_pt  handler;
  char      *name;
  ngx_msec_t    delay;
 } ngx_cache_manager_ctx_t;
\ No newline at end of file
static void
ngx_cache_manager_process_cycle(ngx_cycle_t *cycle, void *data)
{
 ngx_cache_manager_ctx_t *ctx = data;
 void  *ident[4];
 ngx_event_t ev;
 ngx_close_listening_sockets(cycle);
 ngx_memzero(&ev, sizeof(ngx_event_t));
 ev.handler = ctx->handler;
 ev.data = ident;
 ev.log = cycle->log;
 ident[3] = (void *) -1;
 ngx_add_timer(&ev, ctx->delay);
\ No newline at end of file
static void
ngx_cache_manager_process_handler(ngx_event_t *ev)
{
 path = ngx_cycle->pathes.elts;
 for (i = 0; i < ngx_cycle->pathes.nelts; i++) {
  if (path[i]->manager) {
    n = path[i]->manager(path[i]->data);
  }
 }
 ngx_add_timer(ev, next * 1000);
}
\ No newline at end of file
static time_t
ngx_http_file_cache_manager(void *data)
{
 next = ngx_http_file_cache_expire(cache);
 for ( ;; ) {
   size = cache->sh->size;
   if (size < cache->max_size) {
    return next;
   }
   wait = ngx_http_file_cache_forced_expire(cache);
   if (wait > 0) {
    return wait;
   }
 }
}
\ No newline at end of file
 static ngx_cache_manager_ctx_t ngx_cache_loader_ctx = {
  ngx_cache_loader_process_handler, "cache loader process", 60000
 };
\ No newline at end of file
static void
ngx_cache_loader_process_handler(ngx_event_t *ev)
{
 path = cycle->pathes.elts;
 for (i = 0; i < cycle->pathes.nelts; i++) {
  if (path[i]->loader) {
    path[i]->loader(path[i]->data);
    ngx_time_update();
  }
 }
 exit(0);
}
\ No newline at end of file
<p class="left">运行在多进程模型的Nginx在正常工作时,自然就会有多个进程实例,例如,图3-4是在配置worker_processes 4;情况下的显示,Nginx 设置的进程title 能很好地帮助我们区分监控进程与工作进程,不过带上选项f的ps命令以树目录的形式打印各个进程信息也能帮助我们做这个区分。多进程联合工作必定要牵扯到进程之间的通信问题,下面就来看看 Nginx 是如何做的(仅关注监控进程与工作进程)。</p>
<div class="pic">
<img alt="figure_0059_0010" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0059_0010.jpg">
</div>
<div class="grap">
图3-4 Nginx 进程树
</div>
<p class="left">采用socketpair()函数创造一对未命名的UNIX域套接字来进行Linux下具有亲缘关系的进程之间的双向通信是一个非常不错的解决方案。Nginx就是这么做的,先看fork()生成新工作进程的ngx_spawn_process()函数以及相关代码。</p>
<p class="left">21: 代码片段3.4-1,文件名: ngx_process.h</p>
<p class="left">22: typedef struct {</p>
<p class="left">23:  ngx_pid_t  pid;</p>
<p class="left">24:  int    status;</p>
<p class="left">25:  ngx_socket_t  channel[2];</p>
<p class="left">26: …</p>
<p class="left">27: } ngx_process_t;</p>
<p class="left">28: …</p>
<p class="left">47: #define NGX_MAX_PROCESSES  1024</p>
<p class="left">35: 代码片段3.4-2,文件名: ngx_process.c</p>
<p class="left">36: ngx_process_t ngx_processes[NGX_MAX_PROCESSES];</p>
<p class="left">37:</p>
<p class="left">86: ngx_pid_t</p>
<p class="left">87: ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,</p>
<p class="left">88:  char *name, ngx_int_t respawn)</p>
<p class="left">89: {</p>
<p class="left">90: …</p>
<p class="left">117:  if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1)</p>
<p class="left">118: …</p>
<p class="left">186:  pid = fork();</p>
<p class="left">187: …</p>
<p class="left">在该函数进行 fork()之前,先调用了 socketpair()创建一对 socket 描述符存放在变量 ngx_processes[s].channel内(其中s标志在ngx_processes数组内第一个可用元素的下标,比如最开始产生第一个工作进程时,可用元素的下标s为0),而在fork()之后,由于子进程继承了父进程的资源,那么父子进程就都有了这一对socket描述符,而Nginx将channel[0]给父进程使用,channel[1]给子进程使用,这样分别错开地使用不同socket描述符,即可实现父子进程之间的双向通信。</p>
<div class="pic">
<img alt="figure_0060_0011" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0060_0011.jpg">
</div>
<div class="grap">
图3-5 利用socketpair()创建socket描述符对
</div>
<p class="left">除此之外,对于各个子进程之间,也可以进行双向通信。如前面所述,父子进程的通信channel设定是自然而然的事情,而子进程之间的通信 channel 设定就涉及进程之间文件描述符(socket描述符也属于文件描述符)的传递,因为虽然在后生成的子进程通过继承的 channel[0]能够往在前生成的子进程发送信息,但在前生成的子进程无法获知在后生成子进程的channel[0]而不能发送信息,所以在后生成的子进程必须利用已知的在前生成子进程的channel[0]进行主动告知。</p>
<p class="left">在子进程的启动初始化函数 ngx_worker_process_init()里,会把 ngx_channel (也就是channel[1])加入到读事件监听集里,对应的回调处理函数为ngx_channel_handler()。</p>
<p class="left">834: 代码片段3.4-3,文件名: ngx_process_cycle.c</p>
<p class="left">835: static void</p>
<p class="left">836: ngx_worker_process_init(ngx_cycle_t *cycle, ngx_uint_t priority)</p>
<p class="left">837: {</p>
<p class="left">838: …</p>
<p class="left">994:  if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,</p>
<p class="left">995:         ngx_channel_handler)</p>
<p class="left">996:   == NGX_ERROR)</p>
<p class="left">997:  {</p>
<p class="left">998: …</p>
<p class="left">而在父进程fork()生成一个新子进程后,就会立即通过ngx_pass_open_channel()函数把这个子进程的相关信息告知给其前面已生成的子进程。</p>
<p class="left">430: 代码片段3.4-4,文件名: ngx_process_cycle.c</p>
<p class="left">431: static void</p>
<p class="left">432: ngx_pass_open_channel(ngx_cycle_t *cycle, ngx_channel_t *ch)</p>
<p class="left">433: {</p>
<p class="left">434:</p>
<p class="left">436:  for (i = 0; i < ngx_last_process; i++) {</p>
<p class="left">437: …</p>
<p class="left">453:   ngx_write_channel(ngx_processes[i].channel[0],</p>
<p class="left">454:        ch, sizeof(ngx_channel_t), cycle->log);</p>
<p class="left">455:  }</p>
<p class="left">456: }</p>
<p class="left">其中参数ch里包含了刚创建的新子进程(假定为A)的pid、进程信息在全局数组里存储下标、socket描述符channel[0]等信息,这里通过for循环遍历所有存活的其他子进程,然后调用函数ngx_write_channel()通过继承的channel[0]描述符进行信息主动告知,而收到这些消息的子进程将执行设置好的回调函数 ngx_channel_handler(),把接收到的新子进程 A 的相关信息存储在全局变量ngx_processes内。</p>
<p class="left">1066:代码片段3.4-5,文件名: ngx_process_cycle.c</p>
<p class="left">1067:static void</p>
<p class="left">1068:ngx_channel_handler(ngx_event_t *ev)</p>
<p class="left">1069:{</p>
<p class="left">1070:…</p>
<p class="left">1126:  case NGX_CMD_OPEN_CHANNEL:</p>
<p class="left">1127:…</p>
<p class="left">1132:    ngx_processes[ch.slot].pid = ch.pid;</p>
<p class="left">1133:    ngx_processes[ch.slot].channel[0] = ch.fd;</p>
<p class="left">1134:    break;</p>
<p class="left">1135:…</p>
<p class="left">这样,前后子进程都有了对方的相关信息,相互通信也就没有问题了。这其中有一些具体实现细节没有提到,这里也不打算详说<a id="ac2"><sup>[2]</sup></a>,直接看一下表3-1中的实例,就以上面显示的各个父子进程为例。</p>
<div class="grap">
表3-1 Nginx父子进程通信Channel实例
</div>
<div class="pic">
<img alt="figure_0061_0012" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0061_0012.jpg">
</div>
<p class="left">表3-1 中,{a, b}分别表示channel[0]和channel[1]的值,−1 表示这之前是描述符,但在其后被主动close()掉了,0表示这一直都无对应的描述符,其他数字表示对应的描述符值。每一列数据都表示该列所对应进程与其它进程进行通信的描述符,如果当前列所对应进程为父进程,那么它与其它进程进行通信的描述符都为channel[0];如果当前列所对应进程为子进程,那么它与父进程进行通信的描述符为channel[1],与其它子进程进行通信的描述符都为channel[0]。比如,带*的{3, 7}表示如果父进程8706 向子进程8707 发送消息,需使用channel[0],即描述符3,它的channel[1]为7,没有被close()关闭掉,但一直也都没有被使用,所以没有影响,不过按道理应该关闭才是。而带**的{−1, 7}表示如果子进程8707 向父进程8706发送消息(注意该数据所处的行位置,如果是子进程8709与父进程8706进行通信,那么使用的描述符将是带#3 的{−1, 11}所对应的 channel[1],即描述符 11),需使用channel[1],即描述符7,它的channel[0]为−1表示已经close()关闭掉了(Nginx某些地方调用 close()时并没有设置对应变量为−1,我这里为了更好说明,将已经 close()掉的描述符全部标记为−1了)。</p>
<p class="left">越是后生成的子进程,其channel[0]与父进程的对应channel[0]值相同的越多,因为基本都是继承而来,但前面生成的子进程的 channel[0]是通过传递获得的,所以与父进程的对应channel[0]不一定相等。比如如果子进程8707向子进程8710发送消息,需使用channel[0],即描述符10,而对应的父进程channel[0]却是12。虽然它们在各自进程里表现为不同的整型数字,但在内核里表示同一个描述符结构,即不管是子进程8707往描述符10写数据还是父进程8706往描述符12写数据,子进程8710都能通过描述符13正确读取到这些数据,至于子进程8710怎么识别它读到的数据是来自子进程8707还是父进程8706,就得靠其收到的数据特征(比如pid字段)来做标记区分。</p>
<p class="left">最后,就目前 Nginx 代码来看,子进程并没有往父进程发送任何消息,子进程之间也没有相互通信的逻辑。也许是因为 Nginx 有其他一些更好的进程通信方式,比如共享内存等,所以这种 channel 通信目前仅做为父进程往子进程发送消息使用。但由于有这个基础在这,如果未来要使用channel做这样的事情,的确是可以的。</p>
<p class="left" id="bw27"></p>
 typedef struct {
  ngx_pid_t  pid;
  int    status;
  ngx_socket_t  channel[2];
 …
 } ngx_process_t;
 …
 #define NGX_MAX_PROCESSES  1024
 ngx_process_t ngx_processes[NGX_MAX_PROCESSES];
 ngx_pid_t
 ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
  char *name, ngx_int_t respawn)
 {
 …
  if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1)
  pid = fork();
\ No newline at end of file
static void
ngx_worker_process_init(ngx_cycle_t *cycle, ngx_uint_t priority)
{
  if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,
         ngx_channel_handler)
   == NGX_ERROR)
  {
\ No newline at end of file
static void
ngx_pass_open_channel(ngx_cycle_t *cycle, ngx_channel_t *ch)
{
  for (i = 0; i < ngx_last_process; i++) {
   ngx_write_channel(ngx_processes[i].channel[0],
        ch, sizeof(ngx_channel_t), cycle->log);
  }
}
\ No newline at end of file
static void
ngx_channel_handler(ngx_event_t *ev)
{
  case NGX_CMD_OPEN_CHANNEL:
    ngx_processes[ch.slot].pid = ch.pid;
    ngx_processes[ch.slot].channel[0] = ch.fd;
    break;
\ No newline at end of file
<p class="left">共享内存是Linux下进程之间进行数据通信的最有效方式之一,而Nginx就为我们提供了统一的操作接口来使用共享内存。</p>
<p class="left">在Nginx里,一块完整的共享内存以结构体ngx_shm_zone_t来封装表示,其中包括的字段有共享内存的名称( shm_zone[i].shm.name )、大小( shm_zone[i].shm.size )、标签( shm_zone[i].tag )、分配内存的起始地址( shm_zone[i].shm.addr )以及初始回调函数(shm_zone[i].init)等。</p>
<p class="left">24: 代码片段3.5-1,文件名: ngx_cycle.h</p>
<p class="left">25: typedef struct ngx_shm_zone_s ngx_shm_zone_t;</p>
<p class="left">26: …</p>
<p class="left">27: struct ngx_shm_zone_s {</p>
<p class="left">28:  void     *data;</p>
<p class="left">29:  ngx_shm_t   shm;</p>
<p class="left">30:  ngx_shm_zone_init_pt init;</p>
<p class="left">31:  void      *tag;</p>
<p class="left">32: };</p>
<p class="left">这些字段大都容易理解,只有tag字段需要解释一下,因为看上去它和name字段有点重复,而事实上,name字段主要用作共享内存的唯一标识,它能让Nginx知道我想使用哪个共享内存,但它没法让 Nginx 区分我到底是想新创建一个共享内存,还是使用那个已存在的旧的共享内存。举个例子,模块A创建了共享内存sa,模块A或另外一个模块B再以同样的名称sa去获取共享内存,那么此时Nginx是返回模块A已创建的那个共享内存sa给模块A/模块B,还是直接以共享内存名重复提示模块A/模块B出错呢?不管Nginx采用哪种做法都有另外一种情况出错,所以新增一个 tag 字段做冲突标识,该字段一般也就指向当前模块的ngx_module_t变量即可。这样在上面的例子中,通过tag字段的帮助,如果模块A/模块B再以同样的名称sa去获取模块A已创建的共享内存sa,模块A将获得它之前创建的共享内存的引用(因为模块A前后两次请求的tag相同),而模块B则将获得共享内存已做他用的错误提示(因为模块B请求的tag与之前模块A请求时的tag不同)。</p>
<p class="left">当我们要使用一个共享内存时,总会在配置文件里加上该共享内存的相关配置信息,而Nginx 在进行配置解析的过程中,根据这些配置信息就会创建对应的共享内存,不过此时的创建仅仅只是代表共享内存的结构体 ngx_shm_zone_t 变量的创建,这具体实现在函数shared_memory_add()内。另外从这个函数中,我们也可以看到Nginx使用的所有共享内存都以list链表的形式组织在全局变量cf->cycle->shared_memory下,在创建新的共享内存之前会先对该链表进行遍历查找以及冲突检测,对于已经存在且不存在冲突的共享内存可直接返回引用。以ngx_http_limit_req_module模块为例,它需要的共享内存在配置文件里以limit_req_zone配置项出现。</p>
<p class="left">limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;</p>
<p class="left">Nginx 在进行配置解析时,遇到 limit_req_zone 配置项则调用其对应的处理函数 ngx_http_limit_req_zone() ,而在该函数内又将继续调用函数 shared_memory_add()创建对应的ngx_shm_zone_t结构体变量并加入到全局链表内。</p>
<p class="left">ngx_http_limit_req_zone() -> ngx_shared_memory_add() -> ngx_list_push()</p>
<p class="left">共享内存的真正创建是在配置文件全部解析完后,所有代表共享内存的结构体ngx_shm_zone_t变量以链表的形式挂接在全局变量cf->cycle->shared_memory下,Nginx此时遍历该链表并逐个进行实际创建,即分配内存、管理机制(比如锁、slab)初始化等。</p>
<p class="left">398: 代码片段3.5-2,文件名: ngx_cycle.c</p>
<p class="left">399: /* create shared memory */</p>
<p class="left">400:</p>
<p class="left">401: part = &cycle->shared_memory.part;</p>
<p class="left">402: shm_zone = part->elts;</p>
<p class="left">403:</p>
<p class="left">404: for (i = 0; /* void */ ; i++) {</p>
<p class="left">405: …</p>
<p class="left">467:   if (ngx_shm_alloc(&shm_zone[i].shm) != NGX_OK) {</p>
<p class="left">468: …</p>
<p class="left">471:   if (ngx_init_zone_pool(cycle, &shm_zone[i]) != NGX_OK) {</p>
<p class="left">472: …</p>
<p class="left">475:   if (shm_zone[i].init(&shm_zone[i], NULL) != NGX_OK) {</p>
<p class="left">476: ...</p>
<p class="left">477: }</p>
<p class="left">其中函数 ngx_shm_alloc()是共享内存的实际分配,针对当前系统可提供接口,可以是mmap或shmget等。而ngx_init_zone_pool()函数是共享内存管理机制的初始化,因为共享内存的使用涉及到另外两个主题:第一,既然是共享内存,那么必然是多进程共同使用,所以必须考虑互斥问题;第二,Nginx 既以性能著称,那么对于共享内存自然也有其独特的使用方式,虽然我们可以不用(在马上要介绍到的init回调函数里做覆盖处理即可),但在这里也默认都会以这种slab的高效访问机制进行初始化。关于这两点,这里暂且略过,待后续再做讨论。</p>
<p class="left">回调函数 shm_zone[i].init()是各个共享内存所特定的,根据使用方的自身需求不同而不同,这也是我们在使用共享内存时需特别注意的函数。继续看实例ngx_http_limit_req_module模块的init函数ngx_http_limit_req_init_zone()。</p>
<p class="left">398: 代码片段3.5-3,文件名: ngx_http_limit_req_module.c</p>
<p class="left">399: static ngx_int_t</p>
<p class="left">400: ngx_http_limit_req_init_zone(ngx_shm_zone_t *shm_zone, void *data)</p>
<p class="left">401: {</p>
<p class="left">402:  ngx_http_limit_req_ctx_t *octx = data;</p>
<p class="left">403: …</p>
<p class="left">398: if (octx) {</p>
<p class="left">399: …</p>
<p class="left">608:   ctx->shpool = octx->shpool;</p>
<p class="left">609: …</p>
<p class="left">608:  return NGX_OK;</p>
<p class="left">609:  }</p>
<p class="left">610:</p>
<p class="left">611:  ctx->shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;</p>
<p class="left">612: …</p>
<p class="left">608:  ctx->sh = ngx_slab_alloc(ctx->shpool, sizeof(ngx_http_limit_req_ shctx_t));</p>
<p class="left">609: …</p>
<p class="left">函数ngx_http_limit_req_init_zone()的第二个参数data表示“旧”数据,在进行重新加载配置时(即Nginx收到SIGHUP信号)该值将不为空。如果旧数据可继续使用,那么可直接返回 NGX_OK;否则,需根据自身模块逻辑对共享内存的使用做相关初始化,比如ngx_http_limit_req_module模块,在第634、642行直接使用默认已初始化好的slab机制,进行内存的分配等。当函数 ngx_http_limit_req_init_zone()正确执行结束,一个完整的共享内存就已创建并初始完成,接着要做的就是共享内存的使用,这即回到前面提到的两个主题:互斥与slab。</p>
<p class="left">要解决互斥问题,无非就是利用锁机制,强制同一时刻只能有一个进程在访问共享内存,其基本原理就是利用共享的简单资源(比如最简单的原子变量)来代表复杂资源,一个进程在需要操作复杂资源之前先获得对简单资源的使用权限。因为简单资源足够简单,对它的使用权限的获取往往只有一步或几步,所以更容易避免冲突。这个应该是容易理解的,比如一个需要100步的操作肯定比一个只需要3步的操作更容易发生冲突(每一步需要的复杂度相同),因为前一种情况可能会发生一个进程在进行了 99 步后却因另外一个进程发出动作而失败的情况,而后一种情况的进程执行完3步后就已经获得完全使用权限了。</p>
<p class="left">要讲清楚 Nginx 互斥锁的实现,如果不结合具体的代码恐怕是不行的,因为都是一些细节上的考量,比如根据各种不同的CPU架构选择不同的汇编指令、使用不同的共享简单资源(原子变量或文件描述符),并没有什么特别难以理解的地方,查 CPU 手册和系统 Man 手册很容易懂,所以具体实现这里暂且不讲。Nginx 互斥锁的使用非常简单,提供的接口函数以及含义如表3-2。</p>
<div class="grap">
表3-2 Nginx互斥锁接口函数
</div>
<div class="pic">
<img alt="figure_0066_0013" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0066_0013.jpg">
</div>
<p class="left" id="bw28"></p>
 typedef struct ngx_shm_zone_s ngx_shm_zone_t;
 …
 struct ngx_shm_zone_s {
  void     *data;
  ngx_shm_t   shm;
  ngx_shm_zone_init_pt init;
  void      *tag;
 };
\ No newline at end of file
 /* create shared memory */
 part = &cycle->shared_memory.part;
 shm_zone = part->elts;
 for (i = 0; /* void */ ; i++) {
   if (ngx_shm_alloc(&shm_zone[i].shm) != NGX_OK) {
   if (ngx_init_zone_pool(cycle, &shm_zone[i]) != NGX_OK) {
   if (shm_zone[i].init(&shm_zone[i], NULL) != NGX_OK) {
...
 }
\ No newline at end of file
static ngx_int_t
ngx_http_limit_req_init_zone(ngx_shm_zone_t *shm_zone, void *data)
{
  ngx_http_limit_req_ctx_t *octx = data;
 if (octx) {
   ctx->shpool = octx->shpool;
  return NGX_OK;
  }
  ctx->shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
  ctx->sh = ngx_slab_alloc(ctx->shpool, sizeof(ngx_http_limit_req_ shctx_t));
\ No newline at end of file
   page->slab = ((uintptr_t) 1 << NGX_SLAB_MAP_SHIFT) | shift;
\ No newline at end of file
<p class="left">Nginx的slab机制与Linux的slab机制在基本原理上并没有什么特别大的不同(当然,相比而言,Linux的slab机制要复杂得多),简单来说也就是基于两点:缓存与对齐。缓存意味着预分配,即提前申请好内存并对内存做好划分形成内存池,当我们需要使用一块内存空间时, Nginx就直接从已经申请并划分好的内存池里取出一块合适大小的内存即可,而内存的释放也是把内存返还给Nginx的内存池,而不是操作系统;对齐则意味着内存的申请与分配总是按2的幂次方进行,即内存大小总是为8、16、32、64等,比如,虽然只申请33个字节的内存,但也将获得实际64字节可用大小的内存,这的确存在一些内存浪费,但对于内存性能的提升是显著的<a id="ac3"><sup>[3]</sup></a>,更重要的是把内部碎片也掌握在可控的范围内。</p>
<p class="left">Nginx的slab机制主要是和共享内存一起使用,前面提到对于共享内存,Nginx在解析完配置文件,把即将使用的共享内存全部以 list 链表的形式组织在全局变量 cf->cycle->shared_memory下之后,就会统一进行实际的内存分配,而Nginx的slab机制要做的就是对这些共享内存进行进一步的内部划分与管理。关于这点,从函数ngx_slab_init()的逻辑即可初见端倪。不过在此之前,先看看ngx_init_zone_pool()函数对它的调用。</p>
<p class="left">916: 代码片段3.6-1,文件名: ngx_slab.c</p>
<p class="left">917: static ngx_int_t</p>
<p class="left">918: ngx_init_zone_pool(ngx_cycle_t *cycle, ngx_shm_zone_t *zn)</p>
<p class="left">919: {</p>
<p class="left">920:  u_char   *file;</p>
<p class="left">921:  ngx_slab_pool_t *sp;</p>
<p class="left">922:</p>
<p class="left">923:  sp = (ngx_slab_pool_t *) zn->shm.addr;</p>
<p class="left">924: …</p>
<p class="left">937:  sp->end = zn->shm.addr + zn->shm.size;</p>
<p class="left">938:  sp->min_shift = 3;</p>
<p class="left">939: sp->addr = zn->shm.addr;</p>
<p class="left">940: …</p>
<p class="left">960:  ngx_slab_init(sp);</p>
<p class="left">961: …</p>
<p class="left">函数 ngx_init_zone_pool()是在共享内存分配好后进行的初始化调用,而该函数内又调用了本节介绍的重点对象slab的初始化函数ngx_slab_init();,此时的情况如图3-6所示。</p>
<div class="pic">
<img alt="figure_0067_0014" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0067_0014.jpg">
</div>
<div class="grap">
图3-6 共享内存初始布局图
</div>
<p class="left">可以看到此时共享内存的开始部分内存已经被用作结构体ngx_slab_pool_t的存储空间,这相当于是slab机制的额外开销(overhead),后面还会看到其他额外开销,任何一种管理机制都有自己的一些控制信息需要存储,所以这些内存使用是无法避免的。共享内存剩下的部分才是被管理的主体,slab 机制对这部分内存进行两级管理,首先是 page 页,然后是 page页内的slab块(通过slot对相等大小的slab块进行管理,为了区分slab机制,下面以slot块来指代这些slab块),也就是说slot块是在page页内存的再一次管理。</p>
<p class="left">在继续对slab机制分析之前,先看看下面这个表格里记录的一些变量以及其对应的值,因为它们可以帮助我们对后面内容的理解。这些变量会根据系统环境的不同而不同,但一旦系统环境确定,那么这些值也就将都是一些常量值,表3-3 基于的系统环境在本书最开始有统一介绍,这里不再赘述。</p>
<div class="grap">
表3-3 常变量的值与描述
</div>
<div class="pic">
<img alt="figure_0068_0015" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0068_0015.jpg">
</div>
<p class="left">再来看slab机制对page页的管理,初始结构示意图如图3-7所示。</p>
<div class="pic">
<img alt="figure_0068_0016" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0068_0016.jpg">
</div>
<div class="grap">
图3-7 slab机制的page 页管理
</div>
<p class="left">slab 机制对page 页的静态管理主要体现在ngx_slab_page_t[K]和page[N] 这两个数组上,需要解释几点。</p>
<p class="left">第一,虽然是一个页管理结构(即ngx_slab_page_t元素)与一个page内存页相对应,但因为有对齐消耗以及slot块管理结构体的占用(图中的ngx_slab_page_t[n]数组),所以实际上页管理结构体数目比page页内存数目要多,即图中的ngx_slab_page_t[N]到ngx_slab_page_t[K-1],这些结构体完全被忽视,我们也不用去管它们,只是需要知道有这些东西的存在。</p>
<p class="left">第二,如何根据页管理结构page获得对应内存页的起始地址p?计算方法如下。</p>
<p class="left">384: 代码片段3.6-2,文件名: ngx_slab.c</p>
<p class="left">385:   p = (page - pool->pages) << ngx_pagesize_shift;</p>
<p class="left">386:   p += (uintptr_t) pool->start;</p>
<p class="left">对照前面图示来看这很明显,无需过多解释;相反,根据内存页的起始地址 p 也能计算出其对应的页管理结构page。</p>
<p class="left">第三,对齐是指实际 page 内存页按 ngx_pagesize 大小对齐,从图中看就是原本的 start是那个虚线箭头所指的位置,对齐后就是实线箭头所指的位置,对齐能提高对内存页的访问速度,但这有一些内存浪费,并且末尾可能因为不够一个 page 内存页而被浪费掉,所以在ngx_slab_init()函数的最末尾有一次最终可用内存页的准确调整。</p>
<p class="left">75: 代码片段3.6-3,文件名: ngx_cycle.c</p>
<p class="left">76: void</p>
<p class="left">77: ngx_slab_init(ngx_slab_pool_t *pool)</p>
<p class="left">78: {</p>
<p class="left">79: …</p>
<p class="left">130:  m = pages - (pool->end - pool->start) / ngx_pagesize;</p>
<p class="left">131:  if (m > 0) {</p>
<p class="left">132:   pages -= m;</p>
<p class="left">133:   pool->pages->slab = pages;</p>
<p class="left">134:  }</p>
<p class="left">135: …</p>
<p class="left">代码第130行计算的m值如果大于0,说明对齐等操作导致实际可用内存页数减少,所以后面的if语句进行判断调整。</p>
<p class="left">page页的静态管理结构基本就是如此了,再来看page页的动态管理,即page页的申请与释放,这就稍微麻烦一点,因为一旦page页被申请或释放,那么就有了相应的状态:使用或空闲。先看空闲页的管理,Nginx对空闲page页进行链式管理,链表的头节点pool->free,初始状态下的链表情况如图3-8所示。</p>
<div class="pic">
<img alt="figure_0070_0017" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0070_0017.jpg">
</div>
<div class="grap">
图3-8 slab机制的空闲页管理链表
</div>
<p class="left">这是一个有点特别的链表,它的节点可以是一个数组,比如图3-8中的ngx_slab_page_t[N]数组就是一个链表节点,这个数组通过第0号数组元素,即ngx_slab_page_t[0],接入到这个空闲page页链表内,并且整个数组的元素个数也记录在这个第0号数组元素的slab字段内。</p>
<p class="left">如果经历如下几步内存操作:子进程1从共享内存中申请1页,子进程2接着申请了2页,然后子进程1又释放掉刚申请的1页,那么空闲链表各是一个什么状态呢?逐步来看。</p>
<p class="left">子进程1从共享内存中申请1页,如图3-9所示。</p>
<div class="pic">
<img alt="figure_0070_0018" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0070_0018.jpg">
</div>
<div class="grap">
图3-9 子进程1从共享内存中申请1页
</div>
<p class="left">子进程2接着申请了2页,如图3-10所示。</p>
<p class="left">然后子进程1又释放掉刚申请的1页,如图3-11所示。</p>
<p class="left">释放的page页被插入到链表头部,如果子进程2接着释放其拥有的那2页内存,那么空闲链表结构将如图3-12所示。</p>
<div class="pic">
<img alt="figure_0071_0019" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0071_0019.jpg">
</div>
<div class="grap">
图3-10 子进程2接着申请了2页
</div>
<div class="pic">
<img alt="figure_0071_0020" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0071_0020.jpg">
</div>
<div class="grap">
图3-11 子进程1又释放掉刚申请的1页
</div>
<div class="pic">
<img alt="figure_0071_0021" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0071_0021.jpg">
</div>
<div class="grap">
图3-12 空闲链表结构示意图
</div>
<p class="left">可以看到,Nginx对空闲page页的链式管理不会进行节点合并,不过关系不大,毕竟page页既不是slab机制的最小管理单元,也不是其主要分配单元。对处于使用状态中的page页,也是采用的链式管理,在介绍其详细之前,需先来看看 slab 机制的第二级管理机制,即 slot块,这样便于前后的连贯理解。</p>
<p class="left">slot块是对每一页page内存的内部管理,它将page页划分成很多小块,各个page页的slot块大小可以不相等,但同一个page页的slot块大小一定相等。page页的状态通过其所在的链表即可辨明,而page页内各个slot块的状态却需要一个额外的标记,在Nginx的具体实现里采用的是位图方式,即一个bit位标记一个对应slot块的状态,1为使用,0为空闲。</p>
<p class="left">根据slot块的大小不同,一个page页可划分的slot块数也不同,从而需要的位图大小也不一样。前面提到过,每一个page页对应一个名为ngx_slab_page_t的管理结构,该结构体有一个uintptr_t类型的slab字段。在32位平台上(也就是本书讨论的设定平台),uintptr_t类型占4个字节,即slab字段有32个bit位。如果page页划分的slot块数小于等于32,那么Nginx直接利用该字段充当位图,这在Nginx内叫exact划分,每个slot块的大小保存在全局变量ngx_slab_exact_size以及ngx_slab_exact_shift内。比如,1个4KB的page页,如果每个slot块大小为128字节,那么恰好可划分成32块。图3-13是这种划分下的一种可能的中间情况。</p>
<div class="pic">
<img alt="figure_0072_0022" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0072_0022.jpg">
</div>
<div class="grap">
图3-13 page 页可能存在的slot 块数划分情况
</div>
<p class="left">如果划分的每个slot块比ngx_slab_exact_size还大,那意味着一个page页划分的slot块数更少,此时当然也是使用 ngx_slab_page_t 结构体的 slab 字段作为位图。由于比ngx_slab_exact_size大的划分可以有很多种,所以需要把其具体的大小也记录下来,这个值同样也记录在 slab 字段里。这样做是可行的,由于划分总是按 2 次幂增长,所以比ngx_slab_exact_size还大的划分至少要减少一半的slot块数,因此利用slab字段的一半bit位即可完整表示所有slot块的状态。具体点说就是:slab字段的高端bit用作位图,低端bit用于存储slot块大小(仅存其对应的移位数)。代码如下。</p>
<p class="left">378: 代码片段3.6-4: ngx_slab.c</p>
<p class="left">379:   page->slab = ((uintptr_t) 1 << NGX_SLAB_MAP_SHIFT) | shift;</p>
<p class="left">如果申请的内存大于等于ngx_slab_max_size,Nginx直接返回一个page整页,此时已经不在slot块管理里,所有无需讨论。下面来看小于ngx_slab_exact_size的情况,此时slot块数目已经超出了slab字段可表示的容量。比如假设按8字节划分,那么1个4KB的page页将被划分为512块,表示各个slot块状态的位图也就需要512个bit位,一个slab字段明显是不足够的,所以需要为位图另找存储空间,而slab字段仅用于存储slot块大小(仅存其对应的移位数)。</p>
<p class="left">另找的位图存储空间就落在page页内,具体点说是其划分的前面几个slot块内。接着刚才说的例子,512个bit位的位图,即64个字节,而一个slot块有8个字节,所以就需要占用page页的前8个slot块用作位图。一个按8字节划分slot块的page页初始情况如图3-14所示。</p>
<div class="pic">
<img alt="figure_0073_0023" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0073_0023.jpg">
</div>
<div class="grap">
图3-14 按8 字节划分slot 块的page 页初始情况
</div>
<p class="left">由于前几个slot块一开始就被用作位图空间,所以必须把它们对应的bit位设置为1,表示其状态为使用。</p>
<p class="left">不论哪种情况,都有了slot块的大小以及状态,那对slot块的分配与释放就水到渠成了。下面回到slab机制的最后一个话题,即对处于使用状态中的page页的链式管理。其实很简单,首先,根据每页划分的slot块大小,将各个page页加入到不同的链表内。在我们这里设定的平台上,也就是按8、16、32、64、128、256、512、1024、2048一共9条链表,在ngx_slab_init()函数里有其初始化。</p>
<p class="left">102: 代码片段3.6-5,文件名: ngx_slab.c</p>
<p class="left">103: n = ngx_pagesize_shift - pool->min_shift;</p>
<p class="left">104:</p>
<p class="left">105: for (i = 0; i < n; i++) {</p>
<p class="left">106:   slots[i].slab = 0;</p>
<p class="left">107:   slots[i].next = &slots[i];</p>
<p class="left">108:   slots[i].prev = 0;</p>
<p class="left">109: }</p>
<p class="left">假设申请一块 8字节的内存,那么 slab机制将一共分配 page那么多页,将它按 8字节做slot划分,并且接入到链表slots[0]内,相关示例(表示这只是其中一处实现)代码如下。</p>
<p class="left">352: 代码片段3.6-6,文件名: ngx_slab.c</p>
<p class="left">353:   page->slab = shift;</p>
<p class="left">354:   page->next = &slots[slot];</p>
<p class="left">355:   page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;</p>
<p class="left">356:</p>
<p class="left">357:   slots[slot].next = page;</p>
<p class="left">page->prev按4字节对齐,所以末尾两位可以用做他用,这里用于标记当前slot划分类型为NGX_SLAB_SMALL,如图3-15所示。</p>
<div class="pic">
<img alt="figure_0074_0024" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0074_0024.jpg">
</div>
<div class="grap">
图3-15 NGX_SLAB_SMALL类型的slot划分
</div>
<p class="left">继续申请8字节的内存不会分配新的page页,除非刚才那页page(暂且称之为页A)被全是使用完,一旦页A被使用完,它会被拆除出链表,相关示例代码如下。</p>
<p class="left">232: 代码片段3.6-7,文件名: ngx_slab.c</p>
<p class="left">233:  prev = (ngx_slab_page_t *)</p>
<p class="left">234:      (page->prev & ~NGX_SLAB_PAGE_MASK);</p>
<p class="left">235:  prev->next = page->next;</p>
<p class="left">236:  page->next->prev = page->prev;</p>
<p class="left">237:</p>
<p class="left">238:  page->next = NULL;</p>
<p class="left">239:  page->prev = NGX_SLAB_SMALL;</p>
<p class="left">第234行是过滤掉末尾的标记位,以获得正确的前一节点的地址,如图3-16所示。</p>
<div class="pic">
<img alt="figure_0075_0025" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0075_0025.jpg">
</div>
<div class="grap">
图3-16 页A 使用完后拆除出链表
</div>
<p class="left">如果仍然继续申请8字节的内存,那么Nginx的slab机制必须分配新的page页(暂且称之为页B),类似于前面介绍的那样,页B会被加入到链表内,此时链表中只有一个节点,但如果此时页A释放了某个slot块,它又会被加入到链表中,终于形成了具有两个节点的链表,相关示例代码(变量page指向页A)如下,如图3-17所示。</p>
<p class="left">455: 代码片段3.6-8,文件名: ngx_slab.c</p>
<p class="left">456:  page->next = slots[slot].next;</p>
<p class="left">457:  slots[slot].next = page;</p>
<p class="left">458:</p>
<p class="left">459:  page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;</p>
<p class="left">460:  page->next->prev = (uintptr_t) page | NGX_SLAB_SMALL;</p>
<div class="pic">
<img alt="figure_0075_0026" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0075_0026.jpg">
</div>
<div class="grap">
图3-17 页A 重新加入到链表
</div>
<p class="left" id="bw29"></p>
static ngx_int_t
ngx_init_zone_pool(ngx_cycle_t *cycle, ngx_shm_zone_t *zn)
{
  u_char   *file;
  ngx_slab_pool_t *sp;
  sp = (ngx_slab_pool_t *) zn->shm.addr;
  sp->end = zn->shm.addr + zn->shm.size;
  sp->min_shift = 3;
 sp->addr = zn->shm.addr;
  ngx_slab_init(sp);
\ No newline at end of file
   p = (page - pool->pages) << ngx_pagesize_shift;
   p += (uintptr_t) pool->start;
\ No newline at end of file
 void
 ngx_slab_init(ngx_slab_pool_t *pool)
 {
 …
  m = pages - (pool->end - pool->start) / ngx_pagesize;
  if (m > 0) {
   pages -= m;
   pool->pages->slab = pages;
  }
\ No newline at end of file
 n = ngx_pagesize_shift - pool->min_shift;
 for (i = 0; i < n; i++) {
   slots[i].slab = 0;
   slots[i].next = &slots[i];
   slots[i].prev = 0;
 }
\ No newline at end of file
   page->slab = shift;
   page->next = &slots[slot];
   page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;
   slots[slot].next = page;
\ No newline at end of file
  prev = (ngx_slab_page_t *)
      (page->prev & NGX_SLAB_PAGE_MASK);
  prev->next = page->next;
  page->next->prev = page->prev;
  page->next = NULL;
  page->prev = NGX_SLAB_SMALL;
\ No newline at end of file
  page->next = slots[slot].next;
  slots[slot].next = page;
  page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;
  page->next->prev = (uintptr_t) page | NGX_SLAB_SMALL;
\ No newline at end of file
<p class="left">通过对signal信号的处理,使得Nginx支持与用户进行信息交互,从而实现某些特定功能,比如在不中止Nginx服务的情况下更新配置。在前面曾简单提到过Nginx各类进程对信号的处理,下面详细来看。</p>
<p class="left" id="bw30"></p>
<h3 class="center"><a>3.7.1 准备工作</a></h3>
<p class="left">Nginx对所有发往其自身的信号进行了统一管理,其封装了一个对应的ngx_signal_t结构体来描述一个信号。</p>
<p class="left">13: 代码片段3.7.1-1,文件名: ngx_process.c</p>
<p class="left">14: typedef struct {</p>
<p class="left">15:  int signo;</p>
<p class="left">16:  char *signame;</p>
<p class="left">17:  char *name;</p>
<p class="left">18:  void (*handler)(int signo);</p>
<p class="left">19: } ngx_signal_t;</p>
<p class="left">其中字段signo也就是对应的信号值,比如SIGHUP、SIGINT等,当然这是宏,其具体在库头文件signal.h内有定义,比如宏SIGHUP就是数值1<a id="ac4"><sup>[4]</sup></a></p>
<p class="left">[root@localhost ~]# grep SIGHUP /usr/include/*/signal.h</p>
<p class="left">/usr/include/asm-generic/signal.h:#define SIGHUP 1</p>
<p class="left">/usr/include/asm/signal.h:#define SIGHUP  1</p>
<p class="left">字段signame为信号名,信号值所对应宏的字符串,比如"SIGHUP"。字段name和信号名不一样,名称表明该信号的自定义作用,即 Nginx 根据自身对该信号的使用功能而设定的一个字符串,比如SIGHUP用于实现“在不中止Nginx服务的情况下更新配置”的功能,所以对应的该字段为"reload"。字段handler,处理信号的回调函数指针,未直接忽略的信号,其处理函数全部为函数ngx_signal_handler()。</p>
<p class="left">有了描述单个信号的结构体后,Nginx 定义了一个 ngx_signal_t 数组类型的全局变量signals,把它将要处理的信号全部罗列在其中,看其中的几个元素示例。</p>
<p class="left">38: 代码片段3.7.1-2,文件名: ngx_process.c</p>
<p class="left">39: ngx_signal_t signals[] = {</p>
<p class="left">40:  { ngx_signal_value(NGX_RECONFIGURE_SIGNAL),</p>
<p class="left">41:  "SIG" ngx_value(NGX_RECONFIGURE_SIGNAL),</p>
<p class="left">42:  "reload",</p>
<p class="left">43:  ngx_signal_handler },</p>
<p class="left">44: …</p>
<p class="left">80:  { SIGPIPE, "SIGPIPE, SIG_IGN", "", SIG_IGN },</p>
<p class="left">81:</p>
<p class="left">82:  { 0, NULL, "", NULL }</p>
<p class="left">83: };</p>
<p class="left">ngx_signal_value()、ngx_value()等几个都是宏,虽然展开后也很简单,但是它用到了一点额外的知识,如表3-4所示。另外,在C语言代码中,以空格隔开的连续的多个字符串会自动连接,比如两个字符串"SIG""HUP"将自动组合为"SIGHUP"。</p>
<div class="grap">
表3-4 字符串宏操作
</div>
<div class="pic">
<img alt="figure_0077_0027" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0077_0027.jpg">
</div>
<p class="left">有了上面这些介绍,那么对于如下所示的signals[0]的各个字段值就很容易理解了。</p>
<p class="left">{1, "SIGHUP", "reload", ngx_signal_handler}</p>
<p class="left">对于signals数组的后面几个元素,其回调函数为SIG_IGN,表示忽略该信号,这和不做设置是不一样的。如果不做设置,那么将按系统默认的处理进行,而这里主动设置为忽略,也是对其的一种处理方式。数组最后一个元素的各个字段为0或NULL,这是把它当末尾哨兵使用,这是不定数组的惯用手法,以便后续能方便地对它做遍历(因为结束条件的判断就由哨兵把持即可)。</p>
<p class="left" id="bw31"></p>
<h3 class="center"><a>3.7.2 设置生效</a></h3>
<p class="left">做好了准备工作,接下来就要对它们进行设置以便生效,进而在 Nginx 收到信号时能调用对应的回调函数进行处理。</p>
<p class="left">在Nginx的启动流程里,有一个下面这样的函数调用。</p>
<p class="left">main() -> ngx_init_signals()</p>
<p class="left">即由函数ngx_init_signals()完成信号的设置工作。</p>
<p class="left">282: 代码片段3.7.2-1,文件名: ngx_process.c</p>
<p class="left">283: ngx_int_t</p>
<p class="left">284: ngx_init_signals(ngx_log_t *log)</p>
<p class="left">285: {</p>
<p class="left">286:  ngx_signal_t  *sig;</p>
<p class="left">287:  struct sigaction sa;</p>
<p class="left">288:</p>
<p class="left">289:  for (sig = signals; sig->signo != 0; sig++) {</p>
<p class="left">290:   ngx_memzero(&sa, sizeof(struct sigaction));</p>
<p class="left">291:   sa.sa_handler = sig->handler;</p>
<p class="left">292:   sigemptyset(&sa.sa_mask);</p>
<p class="left">293:   if (sigaction(sig->signo, &sa, NULL) == -1) {</p>
<p class="left">294:     ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,</p>
<p class="left">295:        "sigaction(%s) failed", sig->signame);</p>
<p class="left">296:     return NGX_ERROR;</p>
<p class="left">297:   }</p>
<p class="left">298:  }</p>
<p class="left">299:</p>
<p class="left">300:  return NGX_OK;</p>
<p class="left">301: }</p>
<p class="left">了解API函数sigaction()的话,上面代码很容易看懂,在这里我们也看到了signals数组末尾哨兵的功能。</p>
<p class="left">对信号进行设置并生效是在 fork()函数调用之前进行的,所以工作进程等都能受此作用。当然,一般情况下,我们不会向工作进程等子进程发送控制信息,而主要是向监控进程父进程发送,父进程收到信号做相应处理后,再根据情况看是否要把信号再通知到其他所有子进程。</p>
<p class="left" id="bw32"></p>
<h3 class="center"><a>3.7.3 处理实例</a></h3>
<p class="left">本小节以惯用的“在不间断Nginx服务的情况下更新Nginx使用配置”为例,来看下Nginx的整个处理过程。</p>
<p class="left">我们知道,在一般情况下,Nginx主进程总是阻塞在sigsuspend()函数调用点,以等待接收信号,通过pstack命令可以验证这一情况(注意:命令后半句的前后并不是单引号字符,而是反引号)。</p>
<p class="left">[root@localhost ~]# pstack `cat /usr/local/nginx/logs/nginx.pid`</p>
<p class="left">#0 0x00fe6424 in __kernel_vsyscall ()</p>
<p class="left">#1 0x00af0ec7 in sigsuspend () from /lib/libc.so.6</p>
<p class="left">#2 0x08070b29 in ngx_master_process_cycle ()</p>
<p class="left">#3 0x0804b81f in main ()</p>
<p class="left">如果Nginx主进程被直接kill-9掉了,那么函数sigsuspend()将得不到返回<a id="ac5"><sup>[5]</sup></a>,此时Nginx相关子进程也无法获得主进程发送的信号(因为主进程根本就没机会做这个动作),我们这里不考虑这种情况。如果Nginx主进程收到并捕获了信号,比如用户通过kill命令进行信号发送。</p>
<p class="left">[root@localhost ~]# kill -s SIGHUP 'cat /usr/local/nginx/logs/nginx.pid'</p>
<p class="left">那么函数sigsuspend()将在对应的信号处理函数执行完之后才返回继续处理,这点很重要,看Nginx的信号处理回调函数ngx_signal_handler()。</p>
<p class="left">349: 代码片段3.7.3-1,文件名: ngx_process.c</p>
<p class="left">350:  case ngx_signal_value(NGX_RECONFIGURE_SIGNAL):</p>
<p class="left">351:   ngx_reconfigure = 1;</p>
<p class="left">352:   action = ", reconfiguring";</p>
<p class="left">353:   break;</p>
<p class="left">对信号的处理非常的简单,仅根据其收到的信号对相应的全局变量进行置位操作,这符合信号处理函数要求简单快速的一般特点。</p>
<p class="left">函数ngx_signal_handler()处理完后返回,进而函数sigsuspend()返回,从而主进程可以执行后面的代码。</p>
<p class="left">145: 代码片段3.7.3-2,文件名: ngx_process_cycle.c</p>
<p class="left">146: for ( ;; ) {</p>
<p class="left">147: …</p>
<p class="left">170:   sigsuspend(&set);</p>
<p class="left">171: …</p>
<p class="left">227:   if (ngx_reconfigure) {</p>
<p class="left">228:    ngx_reconfigure = 0;</p>
<p class="left">229: …</p>
<p class="left">260:   }</p>
<p class="left">261: …</p>
<p class="left">284:   if (ngx_noaccept) {</p>
<p class="left">285:    ngx_noaccept = 0;</p>
<p class="left">286:    ngx_noaccepting = 1;</p>
<p class="left">287:    ngx_signal_worker_processes(cycle,</p>
<p class="left">288:         ngx_signal_value(NGX_SHUTDOWN_SIGNAL));</p>
<p class="left">289:   }</p>
<p class="left">290:  }</p>
<p class="left">第170行的函数sigsuspend()返回后,后面对全局变量进行判断,发现置位了,那么就开始处理,当然,在处理前先把该全局变量复位,以免下次重复进入。如代码第287 行所示,如果有必要,会利用函数 ngx_signal_worker_processes()再把信号值发送给子进程,而子进程收到信号的处理也大致类似,这无需多说。</p>
<p class="left">这里要注意一下信号发送函数 ngx_signal_worker_processes(),它首先通过父子进之间的channel调用函数ngx_write_channel()进行信号传递,当这种方法失败时才利用kill()函数,相关代码逻辑也比较简单,略过不提。</p>
<p class="left">另外,除了通过kill命令向Nginx发送信号外,Nginx本身封装了几个对信号的发送工作,这主要是通过Nginx的命令行参数选项提供的,比如stop,quit,reopen,reload,而其内部实现也是通过读取当前正在执行的Nginx进程所对应的nginx.pid文件,获得它对应的pid,然后调用kill()函数进行信号发送,对应的函数调用流程为</p>
<p class="left">main() -> ngx_signal_process() -> ngx_os_signal_process()</p>
<p class="left">由于nginx.pid文件路径可以通过配置指令pid指定。如果当前执行的Nginx进程与准备发送信号的Nginx程序使用的是不同的配置,并且配置文件中指定的nginx.pid文件路径不同,那么将可能出现发送失败的情况。比如,当前正在执行的 Nginx 进程使用的配置是nginx.conf.old,指定的pid路径为pid /usr/local/nginx/conf/nginx.pid.old;,而此时用默认配置执行nginx -s reload 将获得如下错误。</p>
<p class="left">[root@localhost nginx-1.2.0]# objs/nginx -s reload</p>
<p class="left">nginx: [error] open() "/usr/local/nginx/logs/nginx.pid" failed (2: No such file ordirectory)</p>
<p class="left"><b>注 释</b></p>
<p class="footnote"><a id="anchor1">[1].http://nginx.org/en/docs/ngx_core_module.html#master_process。</a></p>
<p class="footnote"><a id="anchor2">[2].感兴趣的读者请以关键字“进程之间文件描述符传递”搜索互联网相应资源。</a></p>
<p class="footnote"><a id="anchor3">[3].关于内存对齐对性能的影响,可以参考:http://lenky.info/?p=310。</a></p>
<p class="footnote"><a id="anchor4">[4].Man手册的第7节也可以看到:man 7 signal或http://unixhelp.ed.ac.uk/CGI/man-cgi?signal+7。</a></p>
<p class="footnote"><a id="anchor5">[5].Man手册:man sigsuspend。</a></p>
\ No newline at end of file
 typedef struct {
  int signo;
  char *signame;
  char *name;
  void (*handler)(int signo);
 } ngx_signal_t;
\ No newline at end of file
 ngx_signal_t signals[] = {
  { ngx_signal_value(NGX_RECONFIGURE_SIGNAL),
  "SIG" ngx_value(NGX_RECONFIGURE_SIGNAL),
  "reload",
  ngx_signal_handler },
 …
  { SIGPIPE, "SIGPIPE, SIG_IGN", "", SIG_IGN },
  { 0, NULL, "", NULL }
 };
\ No newline at end of file
ngx_int_t
ngx_init_signals(ngx_log_t *log)
{
  ngx_signal_t  *sig;
  struct sigaction sa;
  for (sig = signals; sig->signo != 0; sig++) {
   ngx_memzero(&sa, sizeof(struct sigaction));
   sa.sa_handler = sig->handler;
   sigemptyset(&sa.sa_mask);
   if (sigaction(sig->signo, &sa, NULL) == -1) {
     ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
        "sigaction(%s) failed", sig->signame);
     return NGX_ERROR;
   }
  }
  return NGX_OK;
}
\ No newline at end of file
  case ngx_signal_value(NGX_RECONFIGURE_SIGNAL):
   ngx_reconfigure = 1;
   action = ", reconfiguring";
   break;
\ No newline at end of file
 for ( ;; ) {
   sigsuspend(&set);
   if (ngx_reconfigure) {
    ngx_reconfigure = 0;
   }
   if (ngx_noaccept) {
    ngx_noaccept = 0;
    ngx_noaccepting = 1;
    ngx_signal_worker_processes(cycle,
         ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
   }
  }
\ No newline at end of file
<h1 class="center"><a>第3章 进程模型</a></h1>
<p class="left">一般情况下,在启动Nginx后系统将出现多个Nginx进程,每个进程各司其责共同完成对客户端请求处理响应的任务。这些进程各自负责哪些业务逻辑、它们之间是否有交互以及如何交互等是本章将要介绍的内容。</p>
<p class="left" id="bw19"></p>
<p class="left">对于任何一个应用程序,在执行具体逻辑的时候必定要使用到内存资源,Nginx 也不例外,而针对自身业务的特点,Nginx 封装了一个名为 ngx_pool_t 类型的内存池,下面就先来看看这个内存池的相关信息。不过在此之前,额外多说几句个人经验:对于C代码里的一个复杂数据结构,我们应该怎样去开始着手分析和理解?其实,和看其他代码一样,先要找到入口,比如应用程序从 main()函数入手,数据结构当然没有 main()函数,不过它可能有类似于C++类一样的初始函数(在C++类中,也就是构造函数),一般被命名为init()、create()等,所以对于Nginx内存池,我们也先来看它的初始函数ngx_create_pool()。</p>
<p class="left">15: 代码片段4.1-1,文件名: ngx_palloc.c</p>
<p class="left">16: ngx_pool_t *</p>
<p class="left">17: ngx_create_pool(size_t size, ngx_log_t *log)</p>
<p class="left">18: {</p>
<p class="left">19:  ngx_pool_t *p;</p>
<p class="left">20:</p>
<p class="left">21:  p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);</p>
<p class="left">22: …</p>
<p class="left">26:  p->d.last = (u_char *) p + sizeof(ngx_pool_t);</p>
<p class="left">27:  p->d.end = (u_char *) p + size;</p>
<p class="left">28: …</p>
<p class="left">31:  size = size - sizeof(ngx_pool_t);</p>
<p class="left">32:  p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;</p>
<p class="left">33:</p>
<p class="left">34:  p->current = p;</p>
<p class="left">35: …</p>
<p class="left">40:  return p;</p>
<p class="left">41: }</p>
<p class="left">19: 代码片段4.1-2,文件名: ngx_palloc.h</p>
<p class="left">20: #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)</p>
<p class="left">21: …</p>
<p class="left">24: #define NGX_POOL_ALIGNMENT  16</p>
<p class="left">一些给ngx_pool_t结构体字段赋值为NULL或0的语句被我去掉了,代码片段4.1-1的第21行是进行 16 字节的内存对齐分配,对齐处理一般是为了从性能上做考虑。而另一个值得关注的赋值语句是代码第32行,可以看到max最大值为4095(假设一页大小为4KB),在后面会看到这个字段的应用。一个完整的代表当前状况的图如图4-1所示(max的值可能为size,当然,从代码片段4.1-1的第32行可以看出,max的值还可能是NGX_MAX_ALLOL_FROM_POOL)。</p>
<div class="pic">
<img alt="figure_0082_0028" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0082_0028.jpg">
</div>
<div class="grap">
图4-1 内存池初始结构图
</div>
<p class="left">创建的内存池被结构体ngx_pool_t占去开头一部分(即额外开销overhead),Nginx实际从该内存池里分配空间的起始位置从p->d.last开始,随着内存池空间的对外分配,这个字段的指向会向后移动。其他字段为空,不过既然设置有这些字段,那肯定是有作用的,但可以暂且不管,&nbsp;分析到后面自然会遇到,所以直接来看如何从这个内存池里分配内存空间。接口函数挺多,有如下这些。</p>
<p class="left">void *ngx_palloc(ngx_pool_t *pool, size_t size)</p>
<p class="left">void *ngx_pnalloc(ngx_pool_t *pool, size_t size)</p>
<p class="left">void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)</p>
<p class="left">void *ngx_pcalloc(ngx_pool_t *pool, size_t size)</p>
<p class="left">static void *ngx_palloc_block(ngx_pool_t *pool, size_t size)</p>
<p class="left">static void *ngx_palloc_large(ngx_pool_t *pool, size_t size)</p>
<p class="left">后面两个接口被static修饰,初步猜想这是内调函数,不会被外部使用,而事实上也的确如此,所以真正对外的内存分配接口只有前面四个,逐一来看。</p>
<p class="left">函数ngx_palloc()尝试从pool内存池里分配size大小的内存空间。这有两种情况。第一种,如果size小于等于pool->max(我们称之为小块内存分配),即小于等于内存池总大小或1页内存(4K-1),那么就可以从内存池里分配,这个分配的内存不一定是来之当前内存池节点(为什么说是节点?马上会解释),因为有可能当前内存池节点里可用的内存空间大小已经小于size,如果是这样的话,就需调用函数ngx_palloc_block()申请一个新的等同大小的内存池节点,然后从这个新内存池节点里分配出size大小的内存空间。除此之外,函数ngx_palloc_block()还会做另外两件事情。首先,把新内存池节点连接到上一个内存池节点的p->d.next字段下形成单链表,如图4-2所示。</p>
<div class="pic">
<img alt="figure_0083_0029" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0083_0029.jpg">
</div>
<div class="grap">
图4-2 申请并连接新的内存池节点
</div>
<p class="left">既然有链表,那么就有链表节点,所以我前面说是内存池节点也就是基于这个原因,新链表节点的加入是在链表尾进行的。另外可以看到新建立的内存池节点的 overhead 都只有结构体ngx_pool_data_t了,这是当然的,一个内存池没有必要有多个ngx_pool_t描述结构,内存能省就省。</p>
<p class="left">函数 ngx_palloc_block()做的另一件事情是根据需要移动内存池描述结构 ngx_pool_t 的current 字段,这个字段记录了后续从该内存池分配内存的起始内存池节点,即从这个字段指向的内存池节点开始搜索可分配的内存。current 字段的变动是根据统计来做的,如果从当前内存池节点分配内存总失败次数(记录在字段p->d.failed内)大于等于6次(这是一个经验值,具体判断是“if (p->d.failed++> 4) {”,由于p->d.failed 初始值为0,所以当这个判断为真时,至少已经分配失败6 次了),就将current字段移到下一个内存池节点,如果下一个内存池节点的failed统计数也大于等于6次,再下一个,依次如此,如果直到最后仍然是failed统计数大于等于6次,那么current字段则指向刚新分配的内存池节点。</p>
<p class="left">在函数ngx_palloc()内,实现小块内存分配逻辑的相关代码如下所示。</p>
<p class="left">123: 代码片段4.1-3,文件名: ngx_palloc.c</p>
<p class="left">124:  p = pool->current;</p>
<p class="left">125:</p>
<p class="left">126:  do {</p>
<p class="left">127:   m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);</p>
<p class="left">128:</p>
<p class="left">129:   if ((size_t) (p->d.end - m) >= size) {</p>
<p class="left">130:     p->d.last = m + size;</p>
<p class="left">131:</p>
<p class="left">132:     return m;</p>
<p class="left">133:   }</p>
<p class="left">134:</p>
<p class="left">135:   p = p->d.next;</p>
<p class="left">136:</p>
<p class="left">137:  } while (p);</p>
<p class="left">138:</p>
<p class="left">139:  return ngx_palloc_block(pool, size);</p>
<p class="left">pool->current 字段的变动是基于性能的考虑,如果从前面的内存池节点里分配内存总是失败,那在下次再进行内存分配时,当然就没有必要再去搜索这些内存池节点,把pool->current指向后移,也就是直接跳过它们。</p>
<p class="left">再来看size大于pool->max的情况(即分配大块内存),此时函数ngx_palloc()直接“return ngx_palloc_large(pool, size);”,函数 ngx_palloc_large()只能调用系统 API 接口 malloc()向操作系统申请内存,申请的内存块被挂接在内存池字段p->large->alloc下(会有对应的管理头结构ngx_pool_large_t),如图4-3所示。</p>
<p class="left">如果继续分配大块内存,那么从系统新分配的内存块就以单链表的形式继续挂载在内存池字段p->large->alloc下,不过这是一种链头插入(和之前的另一个链表有点不同),如图4-4所示。</p>
<p class="left">在内存池的使用过程中,由于大块内存可能会被释放(通过函数ngx_pfree()),此时将空出其对应的头结构体变量ngx_pool_large_t,所以在进行实际的链头插入操作前,会去搜索当前是否有这种情况存在(如图4-5所示)。</p>
<div class="pic">
<img alt="figure_0085_0030" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0085_0030.jpg">
</div>
<div class="grap">
图4-3 申请大块内存
</div>
<div class="pic">
<img alt="figure_0085_0031" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0085_0031.jpg">
</div>
<div class="grap">
图4-4 继续申请大块内存
</div>
<p class="left">如果有,则直接把新分配的内存块设置在其alloc指针字段下,综合平衡考虑,这种搜索也只是对前面几个链表节点进行。</p>
<p class="left">两种情况都分析完后,ngx_palloc()函数的介绍就算到此结束了。不过,有个细节问题,为什么要将 pool->max 字段的最大值限制在一页内存?从前面的分析可知,这个字段是区分小块内存与大块内存的临界,所以这里的原因也就在于只有当分配的内存空间小于一页时才有缓存的必要(即向Nginx内存池申请),否则的话,还不如直接利用系统接口malloc()向操作系统申请。这可以认为是一种经验,比如很多有内核编程经验的人都知道,在 32 位 Linux系统环境里,一般情况下,如果申请的内存空间大于一页,此时就没有必要使用kmalloc()函数,而使用vmalloc()函数就好,道理与此类似。</p>
<div class="pic">
<img alt="figure_0086_0032" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0086_0032.jpg">
</div>
<div class="grap">
图4-5 释放大块内存
</div>
<p class="left">回过头来看另外几个内存分配函数,ngx_pnalloc()函数与刚才介绍的 ngx_palloc()函数实现基本一致,但是它取得的内存起始地址没有做对齐处理(典型代码对比情况)。</p>
<p class="left">116: 代码片段4.1-4,文件名: ngx_palloc.c</p>
<p class="left">117: ngx_palloc(ngx_pool_t *pool, size_t size)</p>
<p class="left">118: …</p>
<p class="left">127:    m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);</p>
<p class="left">128:</p>
<p class="left">147: ngx_pnalloc(ngx_pool_t *pool, size_t size)</p>
<p class="left">148: …</p>
<p class="left">157:    m = p->d.last;</p>
<div class="pic">
<img alt="figure_0086_0033" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0086_0033.jpg">
</div>
<div class="grap">
图4-6 ngx_pnalloc()函数与ngx_palloc()函数申请内存对比情况
</div>
<p class="left">函数ngx_pcalloc()是在ngx_palloc()上的封装,但它在返回分配的内存之前会对这些内存做清零操作。</p>
<p class="left">函数 ngx_pmemalign()不管 size 大小如何,都直接向操作系统申请内存,然后挂接在p->large->alloc字段下。当然,根据名称就可以看出,申请的内存额外的有对齐处理。</p>
<p class="left">Nginx另外还提供的一套函数接口ngx_pool_cleanup_add()、ngx_pool_run_cleanup_file()、ngx_pool_cleanup_file()、ngx_pool_delete_file()用于对内存与其他资源的关联管理,也就是说从内存池里申请一块内存时,可能外部会附带一些其他资源(比如说打开的文件),这些资源的使用和申请的内存是绑定在一起的,那么在进行资源释放的时候,当然也就希望这些资源的释放能放置在和内存池释放时一起进行(通过handler()回调函数),既能避免无意的资源泄露,又省得单独执行资源释放的麻烦。具体代码逻辑不多赘述,如图4-7所示。</p>
<div class="pic">
<img alt="figure_0087_0034" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0087_0034.jpg">
</div>
<div class="grap">
图4-7 资源释放
</div>
<p class="left">最后,来看内存池的释放问题,从代码中不难看出 Nginx 仅提供对大块内存的释放(通过接口ngx_pfree()),而没有提供对小块内存的释放,这意味着从内存池里分配出去的内存不会再回收到内存池里来,而只有在销毁整个内存池时,所有这些内存才会回收到系统内存里,这是Nginx内存池一个很重要的特点,前面介绍的很多内存池设计与处理也都是基于这个特点。Nginx内存池这样设计的原因在于Web Server应用的特殊性,即阶段与时效,对于其处理的业务逻辑分有明确的阶段,而对每一个阶段又有明确的时效,因此Nginx可针对阶段来分配内存池,针对时效来销毁内存池。比如,当一个阶段(比如request处理)开始(或其过程中)就创建对应所需的内存池,而当这个阶段结束时就销毁其对应的内存池,由于这个阶段有严格的时效性,即在一段时间后,其必定会因正常处理、异常错误或超时等而结束,所以不会出现Nginx长时间占据大量无用内存池的情况,既然如此,在其阶段过程中回收不用的小块内存自然也就是不必要的,等待一会再一起回收岂不更简单方便?关于内存池的释放,具体实现在函数ngx_destroy_pool()与ngx_reset_pool()内,这两者逻辑都比较简单明朗,无需多说。</p>
<p class="left">毫无疑问,内存池的使用给Nginx带来了诸多好处,比如内存使用的便利、逻辑代码的简化以及程序性能的提升,但另一方面,它对我们在使用相关内存诊断工具(比如Valgrind<a id="ac1"><sup>[1]</sup></a>)进行问题排查时产生了一定程度上的负面干扰,导致我们可能无法正确的捕获到相关内存异常问题。因此,在进行Nginx二次开发过程中,如遇到诡异的内存问题无法顺利排查时,不妨试试一些第三方补丁<a id="ac2"><sup>[2]</sup></a>,其通过禁用Nginx的内存池,即采用malloc/free接口直接使用系统内存的方式来避免内存池的负面干扰,方便Valgrind工具的使用,以提高解决问题的正确率和效率。</p>
<p class="left" id="bw34"></p>
 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);
 …
  p->d.last = (u_char *) p + sizeof(ngx_pool_t);
  p->d.end = (u_char *) p + size;
 …
  size = size - sizeof(ngx_pool_t);
  p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
  p->current = p;
 …
  return p;
 }
 #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
 …
 #define NGX_POOL_ALIGNMENT  16
\ No newline at end of file
  p = pool->current;
  do {
   m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
   if ((size_t) (p->d.end - m) >= size) {
     p->d.last = m + size;
     return m;
   }
   p = p->d.next;
  } while (p);
  return ngx_palloc_block(pool, size);
\ No newline at end of file
ngx_palloc(ngx_pool_t *pool, size_t size)
    m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
ngx_pnalloc(ngx_pool_t *pool, size_t size)
    m = p->d.last;
\ No newline at end of file
<p class="left">Nginx Hash数据结构的创建过程有点复杂,这从其初始函数ngx_hash_init()就占去200 多行可知一二,但这种复杂是源于Nginx对高效率的极致追求。我们知道影响Hash数据结构查找效率的最大因素是哈希冲突<a id="ac3"><sup>[3]</sup></a>的平均次数,如果没有冲突,那么每一次查找都是一次完成,效率自然很高。如何减少平均冲突次数,一方面是选择好的映射函数,另一方面是扩大Hash内存空间。Nginx的哈希映射函数可以认为是两重,先是计算哈希键值key(这是一个字符串)的哈希码hashcode,然后对hashcode做一个按实际Hash内存空间大小取模运算就得到其在这个内存空间里的具体位置,这部分比较简单无需多说。另一个重点就是Hash内存空间大小的选择,虽然说越大的内存空间冲突自然也就越少,但也不能无限制的大,因此以时间换取空间也是必要的,这是一个选择中间平衡的过程,也是函数ngx_hash_init()的主要逻辑。下面就开始依附实例来逐步解析Nginx Hash数据结构的内部实现。</p>
<p class="left">Nginx对虚拟主机的管理使用到了Hash数据结构,比如假设配置文件里有如下这样的配置。</p>
<p class="left">23: 代码片段4.2-1,文件名: nginx.conf</p>
<p class="left">24:  server {</p>
<p class="left">25:   listen 192.168.1.1:80;</p>
<p class="left">26:   server_name www.web_test2.com blog.web_test2.com;</p>
<p class="left">27: …</p>
<p class="left">41:  server {</p>
<p class="left">42:   listen  192.168.1.1:80;</p>
<p class="left">43:   server_name www.web_test1.com bbs.web_test1.com;</p>
<p class="left">44: …</p>
<p class="left">当Nginx以此配置文件正常启动后,如果来了一个客户端请求到192.168.1.1的80端口,那么Nginx肯定就要做一个查找,看当前请求该使用哪个Server配置。为了提高查找效率,所以在启动开始后,Nginx就将根据这些server_name建立起一个Hash数据结构。</p>
<p class="left">1512:代码片段4.2-2,文件名: ngx_http.c</p>
<p class="left">1513: hash.key = ngx_hash_key_lc;</p>
<p class="left">1514: hash.max_size = cmcf->server_names_hash_max_size;</p>
<p class="left">1515: hash.bucket_size = cmcf->server_names_hash_bucket_size;</p>
<p class="left">1516: hash.name = "server_names_hash";</p>
<p class="left">1517: hash.pool = cf->pool;</p>
<p class="left">1518:</p>
<p class="left">1519: if (ha.keys.nelts) {</p>
<p class="left">1520:  hash.hash = &addr->hash;</p>
<p class="left">1521:  hash.temp_pool = NULL;</p>
<p class="left">1522:</p>
<p class="left">1523:  if (ngx_hash_init(&hash, ha.keys.elts, ha.keys.nelts) != NGX_OK) {</p>
<p class="left">代码第1523行调用初始函数ngx_hash_init()开始创建这个对应的Hash数据结构,看看此时的已准备数据(对于ngx_hash_keys_arrays_t结构体,这里只关注与此相关的keys字段以及已构建成的数据内容,之前它的其他字段以及它的数据是如何构建的,比较简单,所以在这里不做讲解)。</p>
<div class="pic">
<img alt="figure_0089_0035" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0089_0035.jpg">
</div>
<div class="grap">
图4-8 Hash 数据结构初始状态
</div>
<p class="left">ngx_hash_init()函数的第二和第三个参数指示了创建这个 Hash 的原始来源数据(后续称之为实际元素),这无需多说,但是另外一个值得注意的是通过第一个参数传入的 hash.hash其实是一个输出参数,如果我们还注意到给该字段赋的值,表明最后生成的Hash数据结构会存放在addr->hash内,从而后面才可以引用并使用到这个Hash,而它的结构非常简单:</p>
<p class="left">22: 代码片段4.2-3,文件名: ngx_hash.h</p>
<p class="left">23: typedef struct {</p>
<p class="left">24:  ngx_hash_elt_t **buckets;</p>
<p class="left">25:  ngx_uint_t  size;</p>
<p class="left">26: } ngx_hash_t;</p>
<p class="left">这是由Nginx提供的Hash数据结构的特点所决定的,即创建之后就不可再修改,只供高效查找,所以相关字段自然是相当简洁。我们不妨先来看看生成之后的Hash是怎样一个结构。</p>
<div class="pic">
<img alt="figure_0090_0036" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0090_0036.jpg">
</div>
<div class="grap">
图4-9 Hash 数据结构实例
</div>
<p class="left">上图中,字段buckets指向的就是Hash节点所对应的存储空间,不过这里具体实现时用的是二级指针,那么*buckets本身是一个数组,每一个数组元素用来存储映射到此的Hash节点。由于可能有多个实际元素映射到同一个Hash节点(即发生冲突),所以对实际元素再次进行数组形式的组织存储在一个bucket内,这个数组的结束以哨兵元素NULL作为标记,而前面的每一个ngx_hash_elt_t结构对应一个实际元素的存储。这里的实例,整体上也就形成上面所示那样的结构图。对于图中name字段的长度为 1,却保存了那么多数据,是不是会有问题?这也是具体实现上的技巧,类似于gcc的0长度数组<a id="ac4"><sup>[4]</sup></a></p>
<p class="left">对于包含4个实际元素的Hash数据结构却只有5个Hash节点(为什么是5呢?在后面马上会讲到),并且没有冲突(即同一个Hash节点下只有一个实际元素),是不是很惊讶Nginx是怎么做到的?它为什么知道只要5个Hash节点的内存空间就足够了呢?这就是实现在函数ngx_hash_init()前面部分的相关测试代码所做的功劳。</p>
<p class="left">247: 代码片段4.2-4,文件名: ngx_hash.c</p>
<p class="left">248: #define NGX_HASH_ELT_SIZE(name) \</p>
<p class="left">249:  (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))</p>
<p class="left">250:</p>
<p class="left">251: ngx_int_t</p>
<p class="left">252: ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)253: {</p>
<p class="left">254: …</p>
<p class="left">260:  for (n = 0; n < nelts; n++) {</p>
<p class="left">261:   if (hinit->bucket_size<NGX_HASH_ELT_SIZE(&names[n])+sizeof(void *))</p>
<p class="left">262: …</p>
<p class="left">267:    return NGX_ERROR;</p>
<p class="left">代码第261行的这个判断是确保一个bucket至少能存放一个实际元素以及结束哨兵,如果有任意一个实际元素(比如其name 字段特别的长)无法存放到bucket 内则报错返回。具体实现来看,NGX_HASH_ELT_SIZE(&names[n])是该实际元素names[n]所需的内存空间(有对齐处理),而sizeof(void *)自然就是结束哨兵的所需内存空间,hinit->bucket_size 记录了一个bucket的内存空间大小,所以拿它们做比较即可。</p>
<p class="left">接下来开始测试针对当前传入的所有实际元素,测试分配多少个Hash节点(也就是多少bucket)会比较好,即能省内存又能少冲突,否则的话,直接把Hash节点数目设置为最大值hinit->max_size即可。看看这个测试的具体过程(也就是前面那个数值5是怎么得来的)。</p>
<p class="left">270: 代码片段4.2-5,文件名: ngx_hash.c</p>
<p class="left">271: test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);</p>
<p class="left">272: …</p>
<p class="left">276: bucket_size = hinit->bucket_size - sizeof(void *);</p>
<p class="left">277:</p>
<p class="left">278: start = nelts / (bucket_size / (2 * sizeof(void *)));</p>
<p class="left">279: start = start ? start : 1;</p>
<p class="left">280:</p>
<p class="left">281: if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {</p>
<p class="left">282:   start = hinit->max_size - 1000;</p>
<p class="left">283: }</p>
<p class="left">代码第276行代码是计算一个bucket除去结束哨兵所占空间后的实际可用空间大小。代码第278~279行代码计算所需bucket的最小个数,注意到存储一个实际元素所需的内存空间的最小值也就是(2 * sizeof(void *))(即宏 NGX_HASH_ELT_SIZE 的对齐处理),所以一个bucket 可以存储的最大实际元素个数就为bucket_size /(2* sizeof(void *)),然后总实际元素个数nelts除以这个值也就是最少所需要的bucket个数。第281及以后的代码处理另外一种特殊情况,这是一种经验值,如果这个if条件成立,意味着实际元素个数非常多,那么有必要直接把start起始值调高,否则在后面的循环里要执行过多的无用测试。</p>
<p class="left">284: 代码片段4.2-6,文件名: ngx_hash.c</p>
<p class="left">285: for (size = start; size < hinit->max_size; size++) {</p>
<p class="left">286:</p>
<p class="left">287:   ngx_memzero(test, size * sizeof(u_short));</p>
<p class="left">288:</p>
<p class="left">289:   for (n = 0; n < nelts; n++) {</p>
<p class="left">290:    if (names[n].key.data == NULL) {</p>
<p class="left">291:     continue;</p>
<p class="left">292:    }</p>
<p class="left">293:</p>
<p class="left">294:    key = names[n].key_hash % size;</p>
<p class="left">295:    test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));</p>
<p class="left">296: …</p>
<p class="left">303:    if (test[key] > (u_short) bucket_size) {</p>
<p class="left">304:     goto next;</p>
<p class="left">305:    }</p>
<p class="left">306:   }</p>
<p class="left">307:</p>
<p class="left">308:   goto found;</p>
<p class="left">309:</p>
<p class="left">310: next:</p>
<p class="left">311:</p>
<p class="left">312:   continue;</p>
<p class="left">313: }</p>
<p class="left">上面的代码就是获取Hash结构最终节点数目的逻辑,其实也非常的简单,就是逐步增加Hash节点数目(那么对应的bucket数目同步增加),然后把所有的实际元素往这些bucket里添放,这有可能发生冲突,但只要冲突的次数可以容忍,即任意一个bucket都还没满,那么就继续填,如果发生有任何一个bucket满溢了(即第303行代码为真,test[key]记录了key这个hash节点所对应的bucket内存储实际元素后的总大小,如果它大于一个bucket可用的最大空间bucket_size,自然也就是满溢了),那么就必须增加Hash节点、增加bucket。如果所有实际元素都填完后没有发生满溢,那么当前的size值就是最终的节点数目值。</p>
<p class="left">找到需创建的Hash节点数目值,接下来就是实际的Hash结构创建工作,这部分逻辑比较简单,但是其对内存的使用有点技巧,直接点说就是所有buckets所占的内存空间是连接在一起的,并且是按需分配(即某个bucket需多少内存存储实际元素就分配多少内存,不多也不少,当然,除了额外的对齐处理),如图4-10所示。</p>
<div class="pic">
<img alt="figure_0092_0037" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0092_0037.jpg">
</div>
<div class="grap">
图4-10 hash 数据结构的使用
</div>
<p class="left">对创建好的Nginxhash数据结构,唯一的可执行操作就是查找,实现在函数ngx_hash_find()内,逻辑非常的简单,先对key值取模得到对应的Hash节点,然后在该Hash节点所对应的bucket里逐个(实现类似数组,结束有哨兵保证)对比元素名称(比如"www.web_test1.com")来找到唯一的那个实际元素,最后返回其value值(比如,如果在addr->hash结构里找到对应的实际元素,返回value就是其ngx_http_core_srv_conf_t配置)。当然,这是一切OK的情况,否则的话就是没找到,返回NULL。</p>
<p class="left" id="bw35"></p>
  server {
   listen 192.168.1.1:80;
   server_name www.web_test2.com blog.web_test2.com;
 …
  server {
   listen  192.168.1.1:80;
   server_name www.web_test1.com bbs.web_test1.com;
 …
\ No newline at end of file
 hash.key = ngx_hash_key_lc;
 hash.max_size = cmcf->server_names_hash_max_size;
 hash.bucket_size = cmcf->server_names_hash_bucket_size;
 hash.name = "server_names_hash";
 hash.pool = cf->pool;
 if (ha.keys.nelts) {
  hash.hash = &addr->hash;
  hash.temp_pool = NULL;
  if (ngx_hash_init(&hash, ha.keys.elts, ha.keys.nelts) != NGX_OK) {
\ No newline at end of file
 typedef struct {
  ngx_hash_elt_t **buckets;
  ngx_uint_t  size;
 } ngx_hash_t;
\ No newline at end of file
#define NGX_HASH_ELT_SIZE(name) \
  (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
ngx_int_t
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)253: {
  for (n = 0; n < nelts; n++) {
   if (hinit->bucket_size
    return NGX_ERROR;
\ No newline at end of file
 test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
 bucket_size = hinit->bucket_size - sizeof(void *);
 start = nelts / (bucket_size / (2 * sizeof(void *)));
 start = start ? start : 1;
 if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {
   start = hinit->max_size - 1000;
 }
\ No newline at end of file
 for (size = start; size < hinit->max_size; size++) {
   ngx_memzero(test, size * sizeof(u_short));
   for (n = 0; n < nelts; n++) {
    if (names[n].key.data == NULL) {
     continue;
    }
    key = names[n].key_hash % size;
    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;
 }
\ No newline at end of file
<p class="left">基树(Radixtree),是一种基于二进制表示键值的二叉查找树,正是由于其键值的这个特点,所以只有在特定的情况下才会使用,典型的应用场景有文件系统、路由表等。关于基树的理论知识,在一些数据结构或算法书籍上有详细描述,所以这里直接来看 Nginx 基树的具体实现。</p>
<p class="left">按惯例,从其初始函数 ngx_radix_tree_create()开始分析,代码首先分配了基树描述结构ngx_radix_tree_t的内存,然后创建了一个只有根节点的基树,如图4-11所示。</p>
<div class="pic">
<img alt="figure_0093_0038" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0093_0038.jpg">
</div>
<div class="grap">
图4-11 只包含有根节点的基树
</div>
<p class="left">在ngx_radix_tree_t结构体中,除root以外的几个字段都是为了对该基树所使用的内存进行管理所做的设计,free 字段下挂载的是当前空闲的树节点(即从树里删除出来而没被使用的废弃节点,这个节点所占内存空间既没有返还给内存池,也没有返还给系统)。这些节点以单链表的形式组织起来(把节点描述结构ngx_radix_node_t的right字段当链表的next字段使用),所以在节点申请函数 ngx_radix_alloc()里,会先去这个空闲链表查找是否有废弃节点可用。如果有的话,就直接取链头节点返回;否则就要申请(如果之前没申请过页内存或者上次剩余内存不足一个基树节点)一页内存(ngx_pagesize大小,有对齐处理),然后先从中分出一个待分配的基树节点,剩下内存的起始地址和大小分布记录在tree->start和tree->size字段里,以便下次分配基树节点时,可从该剩余内存里直接获取。读者会有个疑问,对于申请的页内存,基树相关代码里怎么没有释放,并且如果申请第二块内存页则第一块内存页的起始地址都搞丢了,这会不会导致内存泄露?当然不会,这些基树内存的最终回收会在 Nginx内存池里处理,所以不用担心。</p>
<p class="left">关于基树对内存的管理与使用,上面就介绍完了,下面再来专心看基树本身的相关逻辑。在函数 ngx_radix_tree_create()创建完只有根节点的基树后,还会根据参数 preallocate 进行树节点的创建。如果该值指定为0,表示不需预创建而直接返回;如果preallocate为正数n,则表示要预创建的基树(预创建的是一颗满二叉树,即,除了叶子节点,其他节点的子节点都有左右两个子节点)深度(假定根节点的层次为 0,树深度定义为最大的叶结点层次,也就是说如果preallocate值为1,那么树深度为1,接下来将创建2个树节点;如果preallocate指定为2,那么树深度为2,那么接下来将一共创建6个树节点);如果preallocate为−1,则表示要选择一个默认深度,这根据平台的不同而不同。如果为其他负数,那这就是一个未被函数 ngx_radix_tree_create()所处理的异常输入,比如如果为−2,那么将几乎创建无数多个树节点而必定由于内存不足而失败,所以调用函数ngx_radix_tree_create()时需特别小心。</p>
<p class="left">根据平台的不同选择默认深度的代码也有bug<a id="ac5"><sup>[5]</sup></a>,不过这是一个“笔误”,在计算一页内存可以存放多少树节点时用错了结构体。</p>
<p class="left">61: 代码片段4.3-1,文件名: ngx_radix_tree.c</p>
<p class="left">62:  if (preallocate == -1) {</p>
<p class="left">63:   switch (ngx_pagesize / sizeof(ngx_radix_tree_t)) {</p>
<p class="left">代码第63行的代码sizeof(ngx_radix_tree_t)应该为sizeof(ngx_radix_node_t),这个bug不算严重,仅影响默认预创建的节点个数,所以会稍微影响一下性能(或者说没有其原本的期望目标性能那么好)。如何选择默认深度是从性能上来考虑的,认为默认预分配的节点所占内存总大小为一页即为最佳(这只是Nginx代码作者的一种经验看法),这样认识是否恰当我们不做评论,按此计算,在x86 32 位平台上,一个节点大小为16 字节,所以一页4KB 内存可以创建256个节点,那么代表树的深度的preallocate值也就是7(即总节点数=2^(树深度+1)–1,因为这里预创建的是一颗满二叉树)。其他的平台情况类似计算即可。</p>
<p class="left">接下来正式进行树节点创建,相关逻辑如下。</p>
<p class="left">14: 代码片段4.3-2,文件名: ngx_radix_tree.c</p>
<p class="left">15: ngx_radix_tree_t *</p>
<p class="left">16: ngx_radix_tree_create(ngx_pool_t *pool, ngx_int_t preallocate)</p>
<p class="left">17: {</p>
<p class="left">18: …</p>
<p class="left">81:  mask = 0;</p>
<p class="left">82:  inc = 0x80000000;</p>
<p class="left">83:</p>
<p class="left">84:  while (preallocate--) {</p>
<p class="left">85:</p>
<p class="left">86:   key = 0;</p>
<p class="left">87:   mask >>= 1;</p>
<p class="left">88:   mask |= 0x80000000;</p>
<p class="left">89:</p>
<p class="left">90:   do {</p>
<p class="left">91:    if (ngx_radix32tree_insert(tree, key, mask, NGX_RADIX_NO_VALUE)</p>
<p class="left">92:     != NGX_OK)</p>
<p class="left">93:    {</p>
<p class="left">94:     return NULL;</p>
<p class="left">95:    }</p>
<p class="left">96:</p>
<p class="left">97:    key += inc;</p>
<p class="left">98:</p>
<p class="left">99:   } while (key);</p>
<p class="left">100:</p>
<p class="left">101:   inc >>= 1;</p>
<p class="left">102:  }</p>
<p class="left">在讲解这代码之前需要先补充几点背景知识。首先,Nginx提供的这个基树仅被geo模块使用,这个模块使用基树来处理IP地址的匹配查找;其次,在nginx-1.2.0版本内,geo模块仅支持IPv4,这意味着这颗基树支持的最大深度为32就足够用了,所以这里的几个变量key、mask、inc 都为 uint32_t 类型;再次,key 与节点的对应是从高位向低位逐步匹配的,比如图4-12中节点d所对应的key是0x40000000,节点f所对应的key是0xC0000000,等。为什么要这样对应呢?这是因为 geo 模块里真正使用的 IP 网络地址,比如 192.168.0.0/16、10.10.0.0/16等,它们前面bit 位才是有效区分位,如果从后往前位匹配,那么会有大量的bit 0,导致基本任何一个IP网络地址插入到该基树都会达到32层。</p>
<div class="pic">
<img alt="figure_0095_0039" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0095_0039.jpg">
</div>
<div class="grap">
图4-12 Nginx geo模块对基树的使用
</div>
<p class="left">最后,在基树的两个节点操作函数ngx_radix32tree_insert()和ngx_radix32tree_delete()中,有了参数key,为什么还有一个参数mask。</p>
<p class="left">38: 代码片段4.3-3,文件名: ngx_radix_tree.h</p>
<p class="left">39: ngx_int_t ngx_radix32tree_insert(ngx_radix_tree_t *tree,</p>
<p class="left">40:  uint32_t key, uint32_t mask, uintptr_t value);</p>
<p class="left">41: ngx_int_t ngx_radix32tree_delete(ngx_radix_tree_t *tree,</p>
<p class="left">42:  uint32_t key, uint32_t mask);</p>
<p class="left">43: uintptr_t ngx_radix32tree_find(ngx_radix_tree_t *tree, uint32_t key);</p>
<p class="left">它是做什么用的?其实,这还是跟 Nginx 内基树的应用有关。刚才已经说过,基树只被geo模块使用,而geo模块存储的IP网络地址大多只有前面bit位有效,比如192.168.0.0/16只有前 16 位有效,那么参数 mask 就是用于告诉函数 ngx_radix32tree_insert() ,插入“192.168.0.0/16”所对应的树节点,只要到16位(也就是前16层)就可以了,否则的话,就要到32位而白白浪费内存,更糟糕的是还无法区分“192.168.0.0/16”与“192.168.0.0/24”这两种不同情况,而加了参数mask就能解决这个问题。函数ngx_radix32tree_delete()的道理与此类似。而另一个重点函数ngx_radix32tree_find()不需要参数mask的原因在于它是最长匹配,而且利用key值一直往下匹配时,遇到空节点会自然停止。</p>
<p class="left">另外,值得注意的是,这里预创建的是一颗满二叉(基)树,这在前面提到过。有了这些背景知识,我们再来看代码:第82行的inc赋值0x80000000以及第88行的初始mask值(第一次循环时),即最高位为1,对应1层节点(根节点对应第0层);第90行到第99的代码用于创建这一层的所有节点。比如,在第1 层时,key首先为0,创建的左节点a,当key+=inc 后,即key等于0x80000000,此时创建的是右节点b。再执行key += inc后,由于溢出导致key为0,从而do(…)while (key)循环退出。</p>
<p class="left">当第二次进入while (preallocate--){}循环(假设此时为真,即预创建的树深度超过1)后, mask 值等于0x C0000000,对应2 层节点。在内部do(…)while (key)循环内,key值依次是:0x0、0x40000000、0x80000000、0xC0000000、0x0(因为溢出而得到该值),前面4次分别创建c、d、e、f4 个节点,第5 次时循环退出。</p>
<p class="left">其他层次节点的创建情况与上类似而无需再讲,整个基树的解析到此结束。因为理解了函数 ngx_radix_tree_create(),另外的几个未提及具体实现的函数接口也就很容易懂了,值得注意的是基树并不要求是满二叉树,仅仅只是在函数 ngx_radix_tree_create()里设定预创建为满二叉树,但是在其他地方调用函数ngx_radix32tree_insert()进行节点插入时,是哪个位置的树节点就从根开始创建到哪个位置,并不会让这颗基树时时刻刻都保持为满二叉树。</p>
<p class="left"><b>注 释</b></p>
<p class="footnote"><a id="anchor1">[1].http://valgrind.org/</a></p>
<p class="footnote"><a id="anchor2">[2].https://github.com/shrimp/no-pool-nginx</a></p>
<p class="footnote"><a id="anchor3">[3].http://lenky.info/?p=2150</a></p>
<p class="footnote"><a id="anchor4">[4].http://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html。</a></p>
<p class="footnote"><a id="anchor5">[5].http://forum.nginx.org/read.php?2,229884。</a></p>
\ No newline at end of file
  if (preallocate == -1) {
   switch (ngx_pagesize / sizeof(ngx_radix_tree_t)) {
\ No newline at end of file
 ngx_radix_tree_t *
 ngx_radix_tree_create(ngx_pool_t *pool, ngx_int_t preallocate)
 {
 …
  mask = 0;
  inc = 0x80000000;
  while (preallocate--) {
   key = 0;
   mask >>= 1;
   mask |= 0x80000000;
   do {
    if (ngx_radix32tree_insert(tree, key, mask, NGX_RADIX_NO_VALUE)
     != NGX_OK)
    {
     return NULL;
    }
    key += inc;
   } while (key);
   inc >>= 1;
  }
\ No newline at end of file
 ngx_int_t ngx_radix32tree_insert(ngx_radix_tree_t *tree,
  uint32_t key, uint32_t mask, uintptr_t value);
 ngx_int_t ngx_radix32tree_delete(ngx_radix_tree_t *tree,
  uint32_t key, uint32_t mask);
 uintptr_t ngx_radix32tree_find(ngx_radix_tree_t *tree, uint32_t key);
\ No newline at end of file
<h1 class="center"><a>第4章 数据结构</a></h1>
<p class="left">为了自身使用的方便,Nginx 封装了很多非常有用的数据结构,其中包括一些很简单的封装,比如字符串ngx_str_t结构体。</p>
<p class="left">15: 代码片段4-1,文件名: ngx_string.h</p>
<p class="left">16: typedef struct {</p>
<p class="left">17:  size_t len;</p>
<p class="left">18:  u_char *data;</p>
<p class="left">19: } ngx_str_t;</p>
<p class="left">我们可以很容易读懂每个字段的含义,还有一些比如ngx_list_t、ngx_array_t、ngx_queue_t等也都较为简单,所以本章不打算介绍这些基础数据结构,而把重点放在那些稍微复杂一点的数据结构,比如内存池、哈希等的封装与实现上。</p>
<p class="left" id="bw33"></p>
 typedef struct {
  size_t len;
  u_char *data;
 } ngx_str_t;
\ No newline at end of file
<p class="left">Nginx的配置文件格式是其作者Igor Sysoev自己定义的,并没有采用像语法分析生成器LEMON<a id="ac2"><sup>[2]</sup></a>那种经典复杂的LALR(1)语法来描述配置信息,而是采用一种近似于key-value对的形式,当然,这只是从配置文件内容静态格式上的直观简单描述。事实上,Nginx配置文件可以认为是一种上下文相关的,高度可扩展的,有作用域以及可自定义变量等诸多高级语言特性的脚本语言。本小节暂只介绍配置文件的静态格式,对于更进一步的内容留待后面章节详细阐述。</p>
<p class="left">对于这种自定义格式的配置文件,好处就是自由、灵活,而坏处就是对于 Nginx 的每一项配置信息都必须做针对性的解析和设置,因此我们很容易看到 Nginx 源码里有大量篇幅的配置信息解析与赋值代码。</p>
<p class="left">Nginx配置文件是由多个配置项组成的,每一个配置项都有一个项目名和对应的项目值,项目名又称为指令(Directive),而项目值可能是简单的字符串(以分号结尾),也可能是由简单字符串和多个配置项组合而成配置块的复合结构(以大括号}结尾),我们可以将配置项归纳为两种:简单配置项和复杂配置项<a id="ac3"><sup>[3]</sup></a></p>
<div class="pic">
<img alt="figure_0098_0040" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0098_0040.jpg">
</div>
<div class="grap">
图5-1 配置文件格式结构图
</div>
<p class="left">上图只是一个示例,而实际的简单配置项与复杂配置项会更多样化。要区分简单配置项与复杂配置项很简单,不带大括号的就是简单配置项,反之则反,比如</p>
<p class="left">error_log /var/log/nginx.error_log info;</p>
<p class="left">因为它不带大括号,所以是一个简单配置项。而</p>
<p class="left">location ~ \.php$ {</p>
<p class="left marg-left2">fastcgi_pass 127.0.0.1:1025;</p>
<p class="left">}</p>
<p class="left">带大括号,所以这是一个复杂配置项。为什么要做这种看似毫无意义的区分?因为后面会看到对于复杂配置项而言,Nginx并不做具体的解析与赋值操作,一般只是申请对应的内容空间、切换解析状态,然后递归调用(因为复杂配置项本身含有递归的思想)解析函数,而真正将用户配置信息转换为Nginx内控制变量的值,还是依靠那些简单配置项所对应的处理函数来做。</p>
<p class="left">不管是简单配置项还是复杂配置项,它们的项目名和项目值都是由标记(token:这里是指一个配置文件字符串内容中被空格、引号、分号、tab号、括号,比如‘{’、换行符等分割开来的字符子串)组成的,配置项目名就是一个token,而配置项目值可以是一个、两个和多个token组成。</p>
<p class="left">比如简单配置项</p>
<p class="left">daemon off;</p>
<p class="left">其项目名daemon为一个token,项目值off也是一个token。简单配置项:</p>
<p class="left">error_page 404 /404.html;</p>
<p class="left">其项目值就包含有两个token,分别为404和/404.html。</p>
<p class="left">对于复杂配置项</p>
<p class="left">location /gqk {</p>
<p class="left marg-left2">index index.html index.htm index.php;</p>
<p class="left marg-left2">try_files $uri $uri/ @gqk;</p>
<p class="left">}</p>
<p class="left">其项目名location为一个token,项目值是一个token(/gqk)和多条简单配置项(通过大括号)组成的复合结构(后续称之为配置块)。上面几个例子中的taken都是被空格分割出来的,事实上下面这样的配置也是正确的。</p>
<p class="left">"daemon" "off";</p>
<p class="left">'daemon' 'off';</p>
<p class="left">daemon 'off';</p>
<p class="left">"daemon" off;</p>
<p class="left">当然,一般情况下没必要画蛇添足似地去加些引号,除非我们需要在 token 内包含空格而又不想使用转义字符(\)的话就可以利用引号,比如</p>
<p class="left">log_format main '$remote_addr - $remote_user [$time_local] $status '</p>
<p class="left marg-left2">'"$request" $body_bytes_sent "$http_referer" '</p>
<p class="left marg-left2">'"$http_user_agent" "$http_x_forwarded_for"';</p>
<p class="left">但是像下面这种格式就会有问题,这对于我们来说很容易理解,不多赘述。</p>
<p class="left">"daemon "off";</p>
<p class="left">最后值得提一下的是,Nginx配置文件里的注释信息以井号(#)作为开头标记。</p>
<p class="left">直观上看到的配置文件格式大概就是上面介绍的这些,但根据 Nginx 应用本身的特定,我们可以对配置文件做上下文识别和区分,或者说是配置项的作用域。因为虽然某项配置项在同一个上下文里只能设置一次,但却可以在不同的上下文里设置多次,以便达到更细粒的控制。比如配置项error_log就是如此,在不同的server上下文里可以设置不同的日志输出级别和输出文件路径。就http应用而言,目前Nginx预定义的配置上下文主要包括main、http、server、location4 种(还有其他几种,比如event、upstream、if、mail 等)。图5-2 是一个http服务器示例配置的上下文情况。</p>
<div class="pic">
<img alt="figure_0100_0041" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0100_0041.jpg">
</div>
<div class="grap">
图5-2 http服务器示例配置的上下文情况
</div>
<p class="left" id="bw37"></p>
<p class="left">前面提到对于配置文件里的每一项配置,程序都必须针对性地解析并转化为内部控制变量的值,因此对于所有可能出现的配置项,Nginx 都会提供对应的代码做它的解析转换工作,如果配置文件内出现了Nginx无法解析的配置项,那么Nginx将报错并直接退出程序。</p>
<p class="left">举例来说,对于配置项daemon,在模块ngx_core_module的配置项目解析数组内的第一元素就是保存的对该配置项进行解析所需要的信息,比如daemon配置项的类型,执行实际解析操作的回调函数,解析出来的配置项值所存放的地址等。</p>
<p class="left">32: 代码片段5.2-1,文件名: nginx.c</p>
<p class="left">33: static ngx_command_t ngx_core_commands[] = {</p>
<p class="left">34:</p>
<p class="left">35:  { ngx_string("daemon"),</p>
<p class="left">36:   NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG,</p>
<p class="left">37:   ngx_conf_set_flag_slot,</p>
<p class="left">38:   0,</p>
<p class="left">39:   offsetof(ngx_core_conf_t, daemon),</p>
<p class="left">40:   NULL },</p>
<p class="left">而如果我在配置文件中加入如下配置内容。</p>
<p class="left">lenky on;</p>
<p class="left">Nginx 启动后将直接返回如下提示错误,这是因为对于“lenky on”这个配置项,Nginx根本就没有对应的代码去解析它。</p>
<p class="left">[emerg]: unknown directive "lenky" in /usr/local/nginx/conf/nginx.conf:2</p>
<p class="left">如果读者在使用 Nginx 的过程中也遇到类似的错误提示,那么应立即检查配置文件是否不小心敲错了字符或配置指令在当前执行版本的Nginx里尚不支持等。</p>
<p class="left">为了统一配置项目的解析,Nginx利用ngx_command_s数据类型对所有的Nginx配置项进行了统一的描述。</p>
<p class="left">77: 代码片段5.2-2,文件名: ngx_conf_file.h</p>
<p class="left">78: struct ngx_command_s {</p>
<p class="left">79:  ngx_str_t name;</p>
<p class="left">80:  ngx_uint_t type;</p>
<p class="left">81:  char   *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);</p>
<p class="left">82:  ngx_uint_t conf;</p>
<p class="left">83:  ngx_uint_t offset;</p>
<p class="left">84:  void   *post;</p>
<p class="left">85: };</p>
<p class="left">这是一个结构体数据类型,它包含多个字段,其中几个主要字段的含义为:字段 name指定与其对应的配置项目的名称,字段set指向配置指令处理回调函数,而字段offset指定转换后控制值的存放位置。</p>
<p class="left">以上面的daemon配置项目为例,当遇到配置文件里的daemon项目名时,Nginx就调用ngx_conf_set_flag_slot()回调函数对其项目值进行解析,并根据其是 on 还是 off 把 ngx_core_conf_t的daemon字段置为1或者0,这样就完成了从配置项目信息到Nginx内部实际值的转换过程。当然,这还有其他一些细节未说,下面再具体来看看。</p>
<p class="left">ngx_command_s结构体的type字段指定该配置项的多种相关信息。</p>
<p class="left">1.该配置的类型:NGX_CONF_FLAG 表示该配置项目有一个布尔类型的值,例如daemon就是一个布尔类型的配置项目,其值为on或者off;NGX_CONF_BLOCK表示该配置项目为复杂配置项,因此其有一个由大括号组织起来的多值块,比如配置项http、events等。</p>
<p class="left">2.该配置项目的配置值的token 个数:NGX_CONF_NOARGS、NGX_CONF_TAKE1、NGX_CONF_TAKE2……NGX_CONF_TAKE7 ,分别表示该配置项的配置值没有token、一个、两个……七个token;NGX_CONF_TAKE12、NGX_CONF_TAKE123、NGX_CONF_1MORE等这些表示该配置项的配置值的token个数不定,分别为1个或2个、1个或2个或3个、1个以上。</p>
<p class="left">3.该配置项目可处在的上下文:NGX_MAIN_CONF(配置文件最外层,不包含其内的类似于 http 这样的配置块内部,即不向内延伸,其他上下文都有这个特性)、NGX_EVENT_CONF(event 配置块)、NGX_HTTP_MAIN_CONF(http 配置块)、NGX_HTTP_SRV_CONF(http的server指令配置块)、NGX_HTTP_LOC_CONF(http的location指令配置块)、NGX_HTTP_SIF_CONF(http的在server配置块内的if指令配置块)、NGX_HTTP_LIF_CONF(http的在location配置块内的if指令配置块)、NGX_HTTP_LMT_CONF(http的limit_except指令配置块)、NGX_HTTP_UPS_CONF ( http 的 upstream 指令配置块)、NGX_MAIL_MAIN_CONF ( mail 配置块)、NGX_MAIL_SRV_CONF(mail的server指令配置块)、等等。</p>
<p class="left">字段 conf 主要由 NGX_HTTP_MODULE 类型模块所使用,其指定当前配置项所在的大致位置,取值为 NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET、NGX_HTTP_LOC_CONF_OFFSET三者之一、其他模块基本不用该字段,直接指定为0。</p>
<p class="left">字段offset指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移(利用offsetof宏)。对于复杂配置项目,例如server,它不用保存配置项值,或者说它本身无法保存,也可以说是因为它的值被分得更细小而被单个保存起来,此时字段offset指定为0即可。</p>
<p class="left">字段post在大多数情况下都为NULL,但在某些特殊配置项中也会指定其值,而且多为回调函数指针,例如auth_basic、connection_pool_size、request_pool_size、optimize_host_names、client_body_in_file_only等配置项。</p>
<p class="left">每个模块都把自己所需要的配置项目的对应ngx_command_s结构体变量组成一个数组,并以ngx_xxx_xxx_commands的形式命名,该数组以元素ngx_null_command作为结束哨兵。</p>
<p class="left" id="bw38"></p>
 static ngx_command_t ngx_core_commands[] = {
  { ngx_string("daemon"),
   NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG,
   ngx_conf_set_flag_slot,
   0,
   offsetof(ngx_core_conf_t, daemon),
   NULL },
\ No newline at end of file
 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;
 };
\ No newline at end of file
<p class="left">下面开始对Nginx配置信息的整个解析流程进行描述。假设我们以命令</p>
<p class="left">nginx -c /usr/local/nginx/conf/nginx.conf</p>
<p class="left">启动Nginx,而配置文件nginx.conf也比较简单,如下所示。</p>
<p class="left">00: 代码片段5.3-1,文件名: nginx.conf</p>
<p class="left">01: worker_processes 2;</p>
<p class="left">02: error_log logs/error.log debug;</p>
<p class="left">03: events {</p>
<p class="left">04:  use epoll;</p>
<p class="left">05:  worker_connections 1024;</p>
<p class="left">06: }</p>
<p class="left">07: http {</p>
<p class="left">08:  include mime.types;</p>
<p class="left">09:  default_type application/octet-stream;</p>
<p class="left">10:  server {</p>
<p class="left">11:   listen 8888;</p>
<p class="left">12:   server_name localhost;</p>
<p class="left">13:   location / {</p>
<p class="left">14:    root html;</p>
<p class="left">15:    index index.html index.htm;</p>
<p class="left">16:   }</p>
<p class="left">17:   error_page 404 /404.html;</p>
<p class="left">18:   error_page 500 502 503 504 /50x.html;</p>
<p class="left">19:   location = /50x.html {</p>
<p class="left">20:    root html;</p>
<p class="left">21:   }</p>
<p class="left">22:  }</p>
<p class="left">23: }</p>
<p class="left">00: 代码片段5.3-2,文件名: mime.types</p>
<p class="left">01: types {</p>
<p class="left">02:  text/html html htm shtml;</p>
<p class="left">03:  text/css css;</p>
<p class="left">04:  text/xml xml;</p>
<p class="left">05:  image/gif gif;</p>
<p class="left">06:  image/jpeg jpeg jpg;</p>
<p class="left">07:  application/x-javascript js;</p>
<p class="left">08: …</p>
<p class="left">09: }</p>
<p class="left">首先,抹掉一些前枝末节,我们直接跟着 Nginx 的启动流程进入到与配置信息相关的函数调用处。</p>
<p class="left">main() -> ngx_init_cycle() -> ngx_conf_parse():</p>
<p class="left">267: 代码片段5.3-3,文件名: ngx_cycle.c</p>
<p class="left">268: if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) {</p>
<p class="left">269:  environ = senv;</p>
<p class="left">270:  ngx_destroy_cycle_pools(&conf);</p>
<p class="left">271:  return NULL;</p>
<p class="left">272: }</p>
<p class="left">此处调用ngx_conf_parse()函数传入了两个参数,第一个参数为ngx_conf_s变量,关于这个变量我们在他处再讲,而第二个参数就是保存的配置文件路径的字符串/usr/local/nginx/conf/nginx.conf。ngx_conf_parse()函数是执行配置文件解析的关键函数,其原型声明如下。</p>
<p class="left">char *ngx_conf_parse(ngx_conf_t *cf, ngx_str_t *filename);</p>
<p class="left">它是一个间接递归函数,也就是说虽然我们在该函数体内看不到直接的对其本身的调用,但是它执行的一些其他函数(比如ngx_conf_handler())内又会调用到ngx_conf_parse()函数,从而形成递归。这一般在处理复杂配置项和一些特殊配置指令时发生,比如指令 include、events、http、server、location等。</p>
<p class="left">ngx_conf_parse()函数体代码量不算太多,但是它照样也将配置内容的解析过程分得很清楚,总体来看分成以下三个步骤。</p>
<p class="left">1.判断当前解析状态。</p>
<p class="left">2.读取配置标记token。</p>
<p class="left">3.当读取了合适数量的标记 token 后对其进行实际的处理,也就是将配置值转换为Nginx内对应控制变量的值。</p>
<p class="left">当进入到ngx_conf_parse()函数时,首先做的第一步是判断当前解析过程处在一个什么样的状态,这有三种可能。</p>
<p class="left">1.正要开始解析一个配置文件:此时的参数filename指向一个配置文件路径字符串,需要函数ngx_conf_parse()打开该文件并获取相关的文件信息(比如文件描述符等)以便下面代码读取文件内容并进行解析。除了在上面介绍的Nginx启动时开始配置文件解析属于这种情况以外,还有当遇到include指令时也将以这种状态调用ngx_conf_parse()函数,因为include指令表示一个新的配置文件要开始解析。状态标记为type=parse_file;。</p>
<p class="left">2.正要开始解析一个复杂配置项值:此时配置文件已经打开并且也已经对文件进行了部分解析,当遇到复杂配置项比如events、http等时,这些复杂配置项的处理函数又会递归调用 ngx_conf_parse()函数,此时解析的内容还是来自当前的配置文件,因此无需再次打开它,状态标记为type= parse_block;。</p>
<p class="left">3.正要开始解析命令行参数配置项值,在对用户通过命令行-g参数输入的配置信息进行解析时处于这种状态,如nginx-g'daemonon;',Nginx在调用ngx_conf_parse()函数对命令行参数配置信息'daemonon;'进行解析时就是这种状态,状态标记为type=parse_param;。</p>
<p class="left">在判断好当前解析状态之后就开始读取配置文件内容,前面已经提到配置文件都是由一个个token组成的,因此接下来就是循环从配置文件里读取token,而ngx_conf_read_token()函数就是用来做这个事情的。</p>
<p class="left">rc = ngx_conf_read_token(cf);</p>
<p class="left">函数ngx_conf_read_token()对配置文件进行逐个字符扫描并解析出单个的token。当然,该函数并不会频繁的去读取配置文件,它每次将从文件内读取足够多的内容以填满一个大小为 NGX_CONF_BUFFER(4096)的缓存区(除了最后一次,即配置文件剩余内容本来就不够了),这个缓存区在函数ngx_conf_parse()内申请并保存引用到变量cf->conf_file->buffer内,函数ngx_conf_read_token()反复使用该缓存区,该缓存区可能有如下一些状态。</p>
<p class="left">初始状态,即函数ngx_conf_parse()内申请缓存区后的初始状态,如图5-3所示。</p>
<div class="pic">
<img alt="figure_0105_0042" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0105_0042.jpg">
</div>
<div class="grap">
图5-3 函数ngx_conf_parse()内申请后的初始状态
</div>
<p class="left">处理过程中的中间状态,有一部分配置内容已经被解析为一个个 token 并保存起来,而有一部分内容正要被组合成token,还有一部分内容等待处理,如图5-4所示</p>
<div class="pic">
<img alt="figure_0105_0043" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0105_0043.jpg">
</div>
<div class="grap">
图5-4 函数ngx_conf_parse()内使用缓存区后的中间状态
</div>
<p class="left">已解析字符和已扫描字符都属于已处理字符,但它们又是不同的:已解析字符表示这些字符已经被作为token额外保存起来了,所以这些字符已经完全没用了;而已扫描字符表示这些字符还未组成一个完整的token,所以它们还不能被丢弃。</p>
<p class="left">当缓存区里的字符都处理完时,需要继续从打开的配置文件中读取新的内容到缓存区,此时的临界状态为,如图5-5所示。</p>
<div class="pic">
<img alt="figure_0106_0044" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0106_0044.jpg">
</div>
<div class="grap">
图5-5 函数ngx_conf_parse()内使用缓存区后的临界状态
</div>
<p class="left">前面图示说过,已解析字符已经没用了,因此我们可以将已扫描但还未组成token的字符移动到缓存区的前面,然后从配置文件内读取内容填满缓存区剩余的空间,情况如图5-6所示。</p>
<div class="pic">
<img alt="figure_0106_0045" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0106_0045.jpg">
</div>
<div class="grap">
图5-6 从配置文件内读取内容填满缓存区剩余空间的状态
</div>
<p class="left">如果最后一次读取配置文件内容不够,那么情况如图5-7所示。</p>
<div class="pic">
<img alt="figure_0107_0046" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0107_0046.jpg">
</div>
<div class="grap">
图5-7 最后一次读取配置文件内容不够的状态
</div>
<p class="left">函数ngx_conf_read_token()在读取了合适数量的标记token之后就开始下一步骤,即对这些标记进行实际的处理,那多少才算是读取了合适数量的标记呢?区别对待,对于简单配置项,读取其全部的标记,也就是遇到配置项结束标记分号;为止,此时一条简单配置项的所有标记都已经被读取并存放在cf->args数组内,因此可以开始下一步骤,即执行回调函数进行实际的解析处理;对于复杂配置项则是读完其配置块前的所有标记,即遇到大括号{为止,此时复杂配置项处理函数所需要的标记都已读取到,而对于配置块{}内的标记将在接下来的函数ngx_conf_parse()递归调用中继续处理,这可能是一个反复的过程。当然,ngx_conf_read_token()函数也可能在其他情况下提前返回,比如配置文件格式出错、文件处理完(遇到文件结束)、块配置处理完(遇到大括号}),这几种返回情况的处理都很简单,不再赘述。</p>
<p class="left">ngx_conf_read_token()函数如何识别并将token缓存在cf->args数组中的逻辑还是比较简单的。首先是对配置文件临时缓存区内容的调整(如有必要),这对应前面几个图示的缓存区状态。接着通过缓存区从前往后的扫描整个配置文件的内容,对每一个字符与前面已扫描字符的组合进行有效性检查并进行一些状态旗标切换,比如d_quoted旗标置1则表示当前处于双引号字符串后,last_space 旗标置 1 则表示前一个字符为空白字符(包括空格、回车、tab等)……这些旗标能大大方便接下来的字符有效性组合检查,比如前面的 nginx.conf 配置文件的第5行末尾多加了个分号(即有2个分号),那么启动Nginx将报错。</p>
<p class="left">nginx: [emerg] unexpected ";" in /usr/local/nginx/conf/nginx.conf:5</p>
<p class="left">再接下来就是判断当前已扫描字符是否能够组成一个 token 标记,两个双引号、两个单引号、两个空白字符之间的字符就能够组成一个token标记,此时在cf->args数组内申请对应的存储空间并进行token标记字符串拷贝,从而完成一个token标记的解析与读取工作。此时根据情况要么继续进行下一个token标记的解析与读取,要么返回到ngx_conf_parse()函数内进行实际的处理。</p>
<p class="left">表5-1列出了ngx_conf_parse()函数在解析nginx.conf配置文件时每次调用ngx_conf_read_token()函数后的cf->args里存储的内容是什么(这通过gdb调试Nginx时在ngx_conf_file.c:185处加断点就很容易看到这些信息),这会大大帮助对后续内容的理解。</p>
<div class="grap">
表5-1 cf->args里存储内容实例
</div>
<div class="pic">
<img alt="figure_0108_0047" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0108_0047.jpg">
</div>
<p class="left">ngx_conf_read_token()函数的返回值决定了ngx_conf_parse()函数接下来的进一步处理,参见表5-2。</p>
<div class="grap">
表5-2 ngx_conf_read_token()函数返回值
</div>
<div class="pic">
<img alt="figure_0109_0048" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0109_0048.jpg">
</div>
<p class="left">讨论情况3,我们知道此时解析转换所需要token都已经保存到cf->args内,那么接下来就是要将这些token转换为Nginx内控制变量的值,执行此逻辑的主要是ngx_conf_handler()函数,不过在此之前会首先判断cf->handler回调函数是否存在,该回调函数存在的目的是针对类似于“text/html html htm shtml;”和“text/css css;”这样的types 配置项或geo 模块里“192.168.0.0/16 local”这样的不定配置项。这些配置项的主要特点是众多且变化不定(一般可被用户自由配置),但格式又基本统一,往往以key/values的形式存在,更重要的是对于这些配置项, Nginx的处理也很简单,只是拷贝到对应的变量内,所以这时一般会提供一个统一的cf->handler回调函数来做这个工作。比如types指令的处理函数ngx_http_core_types()就将cf->handler赋值为ngx_http_core_type(),从而使得mime.types的转换与设置全部由该函数统一处理。</p>
<p class="left">配置转换核心函数ngx_conf_handler()的调用被传入了两个参数,ngx_conf_t类型的cf包含有不少重要的信息,比如转换所需要token就保存在cf->args内,而第二个参数无需多说,记录的是最近一次token解析函数ngx_conf_read_token()的返回值。</p>
<p class="left">前面说过Nginx的每一个配置指令都对应一个ngx_command_s数据类型变量,记录着该配置指令的解析回调函数、转换值存储位置等,而每一个模块又都把自身所相关的所有指令以数组的形式组织起来,所以函数 ngx_conf_handler()首先做的就是查找当前指令所对应的ngx_command_s变量,这通过循环遍历各个模块的指令数组即可。由于Nginx的所有模块也是以数组的形式组织起来的,所有在 ngx_conf_handler()函数体内我们可以看到有两个 for 循环的遍历查找。</p>
<p class="left">279: 代码片段5.3-4,文件名: ngx_conf_file.c</p>
<p class="left">280: static ngx_int_t</p>
<p class="left">281: ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last)</p>
<p class="left">282: {</p>
<p class="left">283: …</p>
<p class="left">293:  for (i = 0; ngx_modules[i]; i++) {</p>
<p class="left">294: …</p>
<p class="left">303:   cmd = ngx_modules[i]->commands;</p>
<p class="left">304: …</p>
<p class="left">308:   for ( /* void */ ; cmd->name.len; cmd++) {</p>
<p class="left">两个for循环的结束判断之所以可以这样写,是因为这些数组都带有对应的末尾哨兵。具体代码里面还有一些有效性判断(比如当前模块类型、指令名称、项目值个数、指令位置)等操作,虽然繁琐但并没有难点所以忽略不讲,直接看里面的函数调用。</p>
<p class="left">393: 代码片段5.3-5,文件名: ngx_conf_file.c</p>
<p class="left">394: rv = cmd->set(cf, cmd, conf);</p>
<p class="left">当代码执行到这里,则Nginx已经查找到配置指令所对应的ngx_command_s变量cmd,所以这里就开始调用回调函数进行处理,以配置项目“worker_processes 2;”为例,对应的ngx_command_s变量为</p>
<p class="left">69: 代码片段5.3-6,文件名: nginx.c</p>
<p class="left">70: { ngx_string("worker_processes"),</p>
<p class="left">71:  NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1,</p>
<p class="left">72:  ngx_conf_set_num_slot,</p>
<p class="left">73:  0,</p>
<p class="left">74:  offsetof(ngx_core_conf_t, worker_processes),</p>
<p class="left">75:  NULL },</p>
<p class="left">那么其回调函数为ngx_conf_set_num_slot()。这是一个比较公共的配置项处理函数,也就是那种数字的配置项目都可以使用该函数进行转换。该函数的内部逻辑非常简单,首先找到转换后值的存储位置,然后利用ngx_atoi()函数把字符串的数字转换为整型的数字,存储到对应位置。这就完成了从配置文件里的“worker_processes 2;”到Nginx 里 ngx_core_conf_t 结构体类型变量conf的worker_processes字段控制值的转换。</p>
<p class="left">worker_processes指令的回调处理函数比较简单,对于复杂配置项,比如server指令的回调处理函数ngx_http_core_server()就要复杂得多,比如它会申请内存空间(以便存储其包含的简单配置项的控制值)、会再次调用ngx_conf_parse()等,这些留在后续需要的时候再做详解。</p>
<p class="left">对于Nginx配置文件的解析流程基本就是如此,上面的介绍忽略了很多细节,前面也说过,事实上,对于每个具体配置信息解析的代码(即各种各样的回调函数cmd->set的具体实现)占去了Nginx大量的源代码,而我们这里却没有做过多的分析,仅例举了worker_processes配置指令的简单解析过程。虽然对于不同的配置项,解析代码会根据自身应用不同而不同,但基本框架大致就是这样了。最后,看一个Nginx配置文件解析的流程图,如图5-8所示。</p>
<div class="pic">
<img alt="figure_0111_0049" src="http://csdn-ebook-resources.oss-cn-beijing.aliyuncs.com/images/608fd0c7025a4a34a97a29897b067d24/figure_0111_0049.jpg">
</div>
<div class="grap">
图5-8 Nginx 配置文件解析的流程图
</div>
<p class="left" id="bw39"></p>
 worker_processes 2;
 error_log logs/error.log debug;
 events {
  use epoll;
  worker_connections 1024;
 }
 http {
  include mime.types;
  default_type application/octet-stream;
  server {
   listen 8888;
   server_name localhost;
   location / {
    root html;
    index index.html index.htm;
   }
   error_page 404 /404.html;
   error_page 500 502 503 504 /50x.html;
   location = /50x.html {
    root html;
   }
  }
 }
 types {
  text/html html htm shtml;
  text/css css;
  text/xml xml;
  image/gif gif;
  image/jpeg jpeg jpg;
  application/x-javascript js;
 …
 }
\ No newline at end of file
if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) {
  environ = senv;
  ngx_destroy_cycle_pools(&conf);
  return NULL;
}
\ No newline at end of file
static ngx_int_t
ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last)
{
  for (i = 0; ngx_modules[i]; i++) {
   cmd = ngx_modules[i]->commands;
   for ( /* void */ ; cmd->name.len; cmd++) {
\ No newline at end of file
 { ngx_string("worker_processes"),
  NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1,
  ngx_conf_set_num_slot,
  0,
  offsetof(ngx_core_conf_t, worker_processes),
  NULL },
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册