很早之前就有看nginx的激动,然而始终被一些事耽误着,最近在忙碌之中,抽出点工夫,看了下Nginx代码,发现整体上并不是很难看懂,而且刚好想学习nginx+lua开发。

nginx 在互联网公司应用很广,最重要的性能当属反向代理和负载平衡了吧,当然还有缓存。所以有必要对 nginx 相熟应用和深刻理解。

记得我之前在很多文章有提到,后盾组件框架次要有三种:redis单过程单线程,memcache单过程多线程,nginx多过程;等看了nginx之后,我也算集齐了。

nginx以模块化形式开发,比方外围模块,event模块,http模块,而后为了反对多平台,event模块下又有对各大平台的封装反对,例如linux平台epoll,mac平台kqueue等等;而后http模块也被拆分成了很多子模块。

这篇文章算是我本人做的笔记吧,把之前钻研的货色记录下。

兴许是之前看过redis 和 golang 以及 python的 http 框架,nginx整体框架比拟容易就看懂了,当然很多细节还需前面缓缓看。

这篇文章次要介绍 nginx 是如何开启,以及申请是怎么执行的,所以这篇文章次要就是以下两点:

  1. nginx开启流程;
  2. 重要回调函数设置;
  3. nginx解决http申请;
  4. 总结

1. nginx开启流程

nginx体量很大,想要在较短时间内看完所有代码很难,而且我看得工夫也不是很多,所以,这里次要站在宏观角度,对nginx做个整体分析。

其实如果间接从main函数间接开始看,其实也是能够看懂大部分,然而 nginx 回调函数太多了,看着看着,忽然跑出一个回调函数,常常就懵逼了。

因而,就须要用gdb来定点调试;

要应用gdb,首先须要在gcc编译时,退出-g选项,能够如下操作:

  1. 关上nginx目录/auto/cc/conf文件,而后更改ngx_compile_opt=”-c”选项,增加-g,即为ngx_compile_opt=”-c -g”;
  2. 而后运行./configure和make即可编译生成可执行文件,在文件objs目录下;

生成可执行文件nginx之后,间接在终端运行即可,nginx会加载默认配置文件,以daemon模式运行;

nginx运行之后,即可通过gdb来调试; 

按如下命令开启gdb

而后,通过pidof命令获取nginx过程号,即可attach,如下:

nginx默认开启一个master过程和一个worker过程,因而上述命令会返回两个过程号,在我主机上8125和8126,较小是master过程,较大的是worker过程;接下来,先看下master过程,

这样就能够间接调试nginx的worker过程,用命令bt能够查看master过程的函数栈

nginx开启之后,首先启动的就是master过程,从main函数开始,

  1. main函数次要是做一些初始化操作,初始化启动参数,开启daemon,新建pid文件等等,而后调用ngx_master_process_cycle函数;
  2. 在ngx_master_process_cycle函数中最重要就是开启子过程,而后调用sigsuspend函数,master过程则阻塞在在信号中;

因而,master过程工作就是开启子过程,而后治理子过程;怎么治理了?

信号,对,就是信号;当master过程收到一个信号之后,就把这个信号传递给worker过程,worker过程进而依据不同信号别离解决。

那么问题又来了,master过程是如何把信号传递给worker过程的?

管道,对,就是管道。原理和memcache的master线程和worker线程通信机制一样,即每个worker过程有两个文件描述符fd[0]和fd[1],一个读端,一个写端;

worker过程将读端退出epoll事件监听,当master过程收到一个信号后,在每个worker过程写端写入一个flag,而后worker过程触发读事件,读取flag,并依据flag做相应操作。

因而nginx接管客户端申请以及解决客户端申请,次要是在worker过程,咱们来看下,worker过程函数栈

因为 worker 过程是由 master 过程 fork 进去,因而 worker 过程蕴含 master 过程的函数栈;咱们间接从#5函数开始看,

  1. ngx_start_worker_processes 函数调用ngx_spawn_process开启子过程,并且设置master过程和worker过程通信的管道;
  2. ngx_spawn_process函数次要是设置master过程和worker过程间通信管道,例如非阻塞等等,而后通过fork函数正式开启子过程;

    子过程调用通过参数传递进来的回调函数ngx_worker_process_cycle正式切入子过程局部,父过程则接着设置worker过程相干属性;

  3. ngx_worker_process_cycle 一开始调用 ngx_worker_process_init 函数对worker 过程做些初始化设置,包含设置过程优先级,worker过程容许关上的最大文件描述符,对阻塞信号的设置,初始化所有模块,将master过程和worker过程间通信管道增加到监听可读事件等等;

    而后在一个有限循环中,函数ngx_worker_process_cycle接着调用ngx_process_events_and_timers,开启事件监听循环;

  4. 在ngx_process_events_and_timers 函数中,先是获取锁,如果获取到锁,listenfd 即可接管客户端,否则 listenfd 不可接管客户端事件;

    而后调用ngx_process_events函数,这个函数也就是ngx_epoll_process_events函数,开启开启事件监听;

ok,worker 过程此时已就绪,期待客户端连贯以及申请数据。

为了防止惊群景象以及实现worker过程负载平衡,每次有客户端连贯时,所有worker过程会先争抢锁,如果某个worker过程获取到锁,即可执行接管客户端和客户端申请事件;

如果worker过程没有争抢到锁,只执行客户端申请事件。

2. 重要回调函数设置

当nginx的master过程和worker过程开启之后,客户端即可发送申请;接下来,就看看nginx是如何解决申请的;

当客户端发送申请之后,首先是通过tcp三次握手建设连贯;当连贯建设胜利之后,即执行listenfd的回调函数,然而listenfd的回调函数是哪个了?这对于老手来说,其实是很难发现listenfd回调函数。

上面剖析下:

像listenfd的回调函数以及模块间是如何拼凑在一起,这些简直都是在模块初始化时实现的。

对于listenfd的回调函数即是在event模块初始化时或者调用event模块一些设置函数时设置;

客户端连贯上服务器之后,服务器收到申请之后的回调函数也是在http模块初始化时或者调用模http模块一些设置函数时设置的。

在event模块初始化时,调用的是ngx_event_process_init函数,上面列出这个函数最重要的代码:

在for循环中,迭代每个监听套接字,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,因而ngx_event_actions构造体就是在epoll模块加载时设置,在上述代码前半部分。咱们来看下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。

如果应用的poll监听器,那么调用将是ngx_poll_add_event等等。

接下来,再剖析一个很重要的回调函数,即客户端连上客户端之后,发送申请时的回调函数,先来看下,listenfd回调函数

当客户端连贯服务器时,首先listenfd回调函数先是调用accept函数接管客户端申请,而后从对象池中获取一个封装客户端socket连贯对象。

如果目前应用的是epoll事件监听器,则调用ngx_add_conn(c)放入事件监听,最初调用ngx_listening_t的回调函数,对客户端连贯进一步操作;

ok,这个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最重要的职能,也是最简单的一部分。能够大略说下执行流程:

  1. 读取解析申请行;
  2. 读取解析申请头;
  3. 开始最重要的局部,即多阶段解决;

    nginx把申请解决划分成了11个阶段,也就是说当nginx读取了申请行和申请头之后,将申请封装了构造体ngx_http_request_t,而后每个阶段的handler都会依据这个ngx_http_request_t,对申请进行解决,例如重写uri,权限管制,门路查找,生成内容以及记录日志等等;

  4. 将后果返回给客户端;

多阶段解决是nginx模块最重要的局部,因为第三方模块也是注册在这;例如有人写了一个利用nginx和memcache做页面缓存的第三方模块,也能够把memcache换成redis集群等等;

而且nginx多阶段解决有点相似python和golang web框架的中间件,后者次要是用装璜器模式,对handler一层一层封装,而nginx是用数组(链表)模式组合多阶段handler,而后按handler链表执行即可;

因为多阶段这块内容还没齐全看懂,所以跟着网上教程,写了个最简略的第三方模块,用于设置定点调试,察看http阶段函数执行过程,步骤如下:

  1. 在nginx目录下新建一个目录thm(third mudole),在新建一个foo目录(foo模块),而后在foo目录下新建ngx_http_foo_module.c

而后同样是在foo目录下新建一个配置文件config

这样,一个最简略的第三方模块就编写实现。

上述两个函数很好了解,一个是初始化函数,将这个模块的 handler 注册到某个阶段中。

这个例子是在阶段NGX_HTTP_CONTENT_PHASE,而后当程序执行到上述阶段时,即可执行foo模块;最初从新编译生成可执行文件即可。

接下来,利用gdb来看下http执行过程,把定点设置在

简要阐明下上述函数,我浏览的版本和运行版本不一样,因而上述仅供参考:

  1. 当有客户端发送tcp连贯申请时,ngx_epoll_process_events返回listenfd可读事件,调用ngx_event_accept函数接管客户端申请,而后将申请封装成ngx_connection_t构造体,最初调用ngx_http_init_connection函数进入http解决;
  2. 在新版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函数;
  3. ngx_http_process_request_line函数外部先是调用ngx_http_read_request_header函数将申请行读取到缓存中,而后调用ngx_http_parse_request_line函数解析出申请行信息,最初调用ngx_http_process_request_header解决申请头;
  4. 在函数 ngx_http_process_request_header 外部先是调用函数ngx_http_read_request_header 读取申请头,而后调用 ngx_http_parse_header_line 函数解析出申请头,接着调用 ngx_http_process_request_header 函数对申请头进行必要的验证,最初调用ngx_http_process_request 函数解决申请;
  5. 在ngx_http_process_request 函数外部调用 ngx_http_handler(ngx_http_request_t _r) 函数,而在ngx_http_handler(ngx_http_request_t_ r) 函数外部调用 函数ngx_http_core_run_phases进行多阶段解决;
  6. 咱们来看下多阶段处理函数ngx_http_core_run_phases  

  7. http 多阶段解决,每个阶段可能对应一个 handler,也可能对应多个 handler,而每个阶段对应同一个checker。

因而上述while循环中,迭代所有http模块handler,而后在handler函数中依据申请构造体ngx_http_request_t做出相应的解决;

上述gdb调试后果,能够看出NGX_HTTP_CONTENT_PHASE 阶段的 checker函数为 ngx_http_core_content_phase,而后再在这个 checker 函数外部执行foo 模块的 handler(ngx_http_foo_handler)。

等到多阶段解决完结之后,最初再将 response 返回给客户端。

4. 总结

这篇文章次要就是宏观剖析下nginx整体运行流程,因为第一次看nginx时,有很多看不懂的中央,所以这篇文章也算是做笔记吧。后续还需认真看多阶段解决,因为第三方开发模块也是注册在多阶段过程,以及相熟ngx+lua模块开发。

本文链接:http://luodw.cc/2017/03/17/ng...