深度好文:Nginx 是如何启动并处理 http 请求的?

很早以前就有看nginx的冲动,可是一直被一些事耽搁着,最近在繁忙之中,抽出点时间,看了下Nginx代码,发现总体上并非很难看懂,并且恰好想学习nginx+lua开发。python

nginx 在互联网公司使用很广,最重要的功能当属反向代理和负载均衡了吧,固然还有缓存。因此有必要对 nginx 熟悉使用和深刻了解。linux

记得我以前在不少文章有提到,后台组件框架主要有三种:redis单进程单线程,memcache单进程多线程,nginx多进程;等看了nginx以后,我也算集齐了。nginx

nginx以模块化方式开发,好比核心模块,event模块,http模块,而后为了支持多平台,event模块下又有对各大平台的封装支持,例如linux平台epoll,mac平台kqueue等等;而后http模块也被拆分红了不少子模块。golang

这篇文章算是我本身作的笔记吧,把以前研究的东西记录下。web

也许是以前看过redis 和 golang 以及 python的 http 框架,nginx总体框架比较容易就看懂了,固然不少细节还需后面慢慢看。redis

这篇文章主要介绍 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...

image