很久以前就有看nginx的冲动,但是一直被一些事情耽误了。最近百忙之中抽出时间阅读了Nginx代码,发现整体上并不难理解,正好想学习一下nginx+lua开发。Nginx广泛应用于互联网公司。最重要的功能是反向代理和负载均衡,当然还有缓存。所以有必要熟悉nginx,深入了解。记得之前在很多文章中提到,后台组件框架主要有3种:redis单进程单线程、memcache单进程多线程、nginx多进程;看完nginx,我都能算出来了。nginx采用模块化的方式开发,如核心模块、事件模块、http模块,同时为了支持多平台,事件模块也对各大平台进行了封装支持,如linux平台上的epoll、linux平台上的kqueuemac平台等;那么http模块也被拆分成了很多子模块。这篇文章可以看作是自己的笔记,记录自己之前学习的东西。可能是因为之前看过redis、golang和python的http框架。nginx的整体框架还是比较容易理解的。当然,很多细节需要后面再看。本文主要介绍nginx是如何开启的,以及request是如何执行的,所以本文主要关注以下两点:nginx的开启流程;重要的回调函数设置;nginx处理http请求;总结1.nginx开启过程nginx体积很大,很难在短时间内看完所有代码,我也没有太多时间看,所以这里主要分析nginx从宏观上看整体。其实如果直接从main函数开始,其实大部分都能看懂,但是nginx的回调函数太多了。如果你看着它,突然跑出一个回调函数,你常常会一头雾水。因此,需要使用gdb进行定点调试;使用gdb,首先需要在编译gcc的时候加上-g选项,可以这样做:打开nginx目录/auto/cc/conf文件,然后更改ngx_compile_opt="-c"选项,加上-g,即ngx_compile_opt=”-c-g”;然后运行./configure和make在文件objs目录下编译生成可执行文件;生成可执行文件nginx后,直接在终端运行,nginx会加载默认的配置文件并作为守护进程运行;nginx运行后,可以通过gdb进行调试;按照下面命令打开gdb然后通过pidof命令获取nginx进程号,然后attach,如下:nginx默认开启一个master进程和一个worker进程,所以上面的命令会返回两个进程号,8125而我主机上的8126,小的是master进程,大的是worker进程;接下来,先看master进程,这样可以直接Debugnginx的worker进程,使用命令bt查看master进程的函数栈。nginx启动后,先启动master进程。从main函数开始,main函数主要进行一些初始化操作,初始化启动参数,启动daemon,新建pid文件等,然后调用ngx_master_process_cycle函数;ngx_master_process_cycle函数中最重要的是启动子进程,然后调用sigsuspend函数,master进程阻塞在信号中;因此,master进程的任务就是启动子进程,然后管理子进程;如何管理?信号,是的,是信号;当master进程收到信号后,将信号传递给worker进程,worker进程再根据不同的信号进行处理。那么问题又来了,master进程如何将信号传递给worker进程呢?管道,是的,管道。原理和memcache的master线程和worker线程的通信机制是一样的,即每个worker进程有两个文件描述符fd[0]和fd[1],一个读端,一个写端;worker进程将read端添加到epoll事件监听,master进程收到信号后,在每个worker进程的write端写入一个flag,然后worker进程触发read事件,读取flag,执行根据flag进行相应的操作。所以nginx接收客户端请求和处理客户端请求,主要是在worker进程中进行。再来看worker进程函数栈,因为worker进程被master进程fork出来,所以worker进程包含了master进程的函数栈;我们直接从#5函数开始看,ngx_start_worker_processes函数调用ngx_spawn_process启动子进程,并设置master进程和worker进程之间的通信管道;ngx_spawn_process函数主要是设置master进程和worker进程之间的通信管道,比如非阻塞等,然后通过fork函数正式启动子进程;子进程调用参数传入的回调函数ngx_worker_process_cycle正式切入子进程部分,父进程再设置worker进程的相关属性;ngx_worker_process_cycle首先调用ngx_worker_process_init函数初始化worker进程,包括设置进程优先级、worker进程允许打开的最大文件描述符、阻塞信号的设置、所有模块的初始化、master进程的添加以及用于侦听可读事件等的工作进程间通信管道;然后死循环,函数ngx_worker_process_cycle再调用ngx_process_events_and_timers,开启事件监听循环;在ngx_process_events_and_timers函数中,首先获取锁,如果获取到锁,listenfd可以接收到client,否则listenfd无法接收到client事件;然后调用ngx_process_events函数,也就是ngx_epoll_process_events函数,开启事件监听;ok,worker进程已经准备就绪,等待客户端连接请求数据。为了避免拥挤现象,实现worker进程负载均衡,每有一个client连接,所有worker进程都会先竞争锁。如果一个worker进程获得了锁,就可以执行接收客户端和客户端请求事件;如果工作进程不竞争锁,则只执行客户端请求事件。2.重要的回调函数设置当nginx的master进程和worker进程启动后,client可以发送请求;接下来我们看看nginx是如何处理请求的;客户端发送请求时,首先通过tcp握手3次建立连接;当连接建立成功后,会执行listenfd的回调函数,但是listenfd的回调函数是哪个呢?对于新手来说,其实很难找到listenfd的回调函数。下面分析一下:像listenfd这样的回调函数,模块是怎么拼凑起来的,这些几乎都是在模块初始化的时候完成的。listenfd的回调函数是在初始化事件模块或者调用事件模块的一些设置函数时设置的;客户端连接到服务端后,在http模块初始化或者http模块的一些设置调用函数时,也会设置服务端收到请求后的回调函数。当初始化事件模块时,会调用ngx_event_process_init函数。该函数最重要的代码如下:在for循环中,遍历每一个监听socket,recv是listenfd连接对象的读取事件。这里,设置listenfd读取事件的回调函数为ngx_event_accept函数,然后将每个listenfd加入事件监听器,设置为可读事件。ok,我们再看ngx_add_conn和ngx_add_event的定义,如下所示:可见ngx_add_conn和ngx_add_event都是ngx_event_actions结构体中设置的函数指针;其实这个ngx_event_actions是nginx跨平台的关键,因为不同平台使用的事件监听器不一样,导致ngx_event_actions不一样。比如linux使用epoll,所以在加载epoll模块的时候设置了ngx_event_actions结构体,在上面代码的前半部分。我们看epoll模块actions.init函数:从代码中可以看出,ngx_event_actions设置为ngx_epoll_module_ctx.actions,再看这个结构体:因此,在调用ngx_add_conn和ngx_add_event时,分别调用了ngx_epoll_add_connection和ngx_epoll_add_event;首先,如果此时使用mac平台,使用的事件监听器是kqueue,那么在调用ngx_add_event时,会调用ngx_kqueue_add_event。如果使用轮询侦听器,那么调用将是ngx_poll_add_event等等。接下来分析一个很重要的回调函数,即客户端连接到客户端,发送请求时的回调函数。首先我们看一下listenfd的回调函数。当客户端连接到服务器时,listenfd回调函数首先调用accept函数接收客户端请求,然后从对象池中获取一个封装好的客户端socket连接对象。如果当前使用了epoll事件监听器,则调用ngx_add_conn(c)放入事件监听器,最后调用ngx_listening_t的回调函数进一步操作客户端连接;好的,这个ls->handler(c)是什么?第一次看到代码的时候,惊呆了!!!还记得我之前说过的话吗?模块之间的连接几乎都是在模块初始化的时候或者调用模块的一些设置函数的时候设置的,那么接下来我们就来看看http模块在初始化的时候做了些什么。http模块并没有在模块初始化函数中设置ls->handler(c),而是在读取“http”命令时在执行命令函数ngx_http_block中设置;真的是隐藏的够深,经历了四个函数,最后看到了ls-handler的设置函数,也就是ngx_http_init_connection函数,这个函数在http模块里面,是客户端http请求处理的入口函数;至此,我们可以知道服务端收到客户端后,首先将客户端发送到ngx_connection_t结构中,然后交给http模块执行http请求。3、nginx处理http请求nginx处理http请求是nginx最重要的功能,也是最复杂的部分。大致可以说执行过程:读取分析请求行;读取分析请求头;开始最重要的部分,即多阶段处理;nginx将请求处理分为11个阶段,也就是说,当nginx读取到请求行和请求头后,将请求封装在结构体ngx_http_request_t中,然后每个阶段的handler会根据这个ngx_http_request_t来处理请求,比如重写uri、权限控制、路径搜索、生成内容和记录日志等;将结果返回给客户端;多阶段处理是nginx模块中最重要的部分,因为第三方模块也注册在这里;比如有人写了一个第三方模块,使用nginx和memcache做页面缓存,也可以用redisClusters代替memcache等;而nginx多阶段处理有点类似于python和golangweb框架的中间件。后者主要是使用装饰器模式将handlers层层封装起来,而nginx则将多阶段的handlers以数组(链表)的形式组合起来,然后按handler链表执行;因为对多阶段的内容还没有完全了解,所以跟着网上的教程,自己写了一个最简单的设置定点调试的第三方模块,观察http阶段函数的执行过程。步骤如下:在nginx目录下新建目录thm(第三个mudole),新建foo目录(foo模块),然后在foo目录下新建ngx_http_foo_module.c,在foo下新建配置文件config目录。编写了简单的第三方模块。以上两个函数很容易理解。一个是初始化函数,将这个模块的handler注册到某个阶段。这个例子是在NGX_HTTP_CONTENT_PHASE阶段,然后当程序执行到上面这个阶段的时候,foo模块就可以执行了;最后,可以重新编译生成可执行文件。接下来用gdb查看http执行过程,在上面功能的简要说明中设置固定点。我看的版本和运行版本不一样,所以以上仅供参考:当客户端发送tcp连接请求时,ngx_epoll_process_events返回listenfd可读事件,调用ngx_event_accept函数接收客户端请求,然后将请求封装成ngx_connection_t结构体,最后调用ngx_http_init_connection函数进入http处理;在新版本的nginx中,没有看到ngx_http_wait_request_handler,而是改为ngx_http_init_connection(ngx_connection_t*c)函数,然后在这个函数里面调用ngx_http_init_request函数初始化请求结构体ngx_http_request_t,调用ngx_http_process_request_line函数;insidethengx_http_process_request_linefunction,firstcallthengx_http_read_request_headerfunctiontoreadtherequestlineintothecache,thencallthengx_http_parse_request_linefunctiontoparseouttherequestlineinformation,andfinallycallngx_http_process_request_header处理请求头;在函数ngx_http_process_request_header内部先是调用函数ngx_http_read_request_header读取请求头,然后调用ngx_http_parse_header_line函数解析出请求头,接着调用ngx_http_process_request_header函数对请求头进行必要的验证,最后调用ngx_http_process_request函数处理请求;在ngx_http_process_request函数内部Callthengx_http_handler(ngx_http_request_t_r)function,andcallthefunctionngx_http_core_run_phasesinside用于多阶段处理的ngx_http_handler(ngx_http_request_t_r)函数;再来看多阶段处理函数ngx_http_core_run_phaseshttp多阶段处理,每个阶段可能对应一个handler,也可能对应多个handler,每个stage对应同一个checker。因此,在上面的while循环中,遍历所有http模块的handler,然后在handler函数中根据request结构ngx_http_request_t做相应的处理;以上gdb调试结果显示NGX_HTTP_CONTENT_PHASE阶段的检查器函数为ngx_http_core_content_phase,然后在这个检查器函数内部执行foo模块的handler(ngx_http_foo_handler)。等到多阶段处理结束,最后将响应返回给客户端。4.综上所述,这篇文章主要是宏观分析下nginx的整体运行过程,因为第一次看nginx的时候,有很多地方没看懂,所以这篇文章可以算是一张纸条。后续需要仔细看一下多阶段处理,因为多阶段过程中也注册了第三方开发模块,熟悉ngx+lua模块开发。本文链接:http://luodw.cc/2017/03/17/ng...
