深度好文: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

相關文章
相關標籤/搜索