1. 前言
Nginx是當前最流行的HTTP Server之一,根據W3Techs的統計,目前世界排名(根據Alexa)前100萬的網站中,Nginx的占有率為6.8%。與Apache相比,Nginx在高並發情況下具有巨大的性能優勢。
Nginx屬於典型的微內核設計,其內核非常簡潔和優雅,同時具有非常高的可擴展性。Nginx最初僅僅主要被用於做反向代理,后來隨着HTTP核心的成熟和各種HTTP擴展模塊的豐富,Nginx越來越多被用來取代Apache而單獨承擔HTTP Server的責任,例如目前淘寶內各個部門正越來越多使用Nginx取代Apache,據筆者了解,在騰訊和新浪等公司也存在類似情況。
同時,大量的第三方擴展模塊也令Nginx越來越強大。例如,由淘寶的工程師清無(王曉哲)和春來(章亦春)所開發的nginx_lua_module可以將Lua語言嵌入到Nginx配置中,從而利用Lua極大增強了Nginx本身的編程能力,甚至可以不用配合其它腳本語言(如PHP或Python等),只靠Nginx本身就可以實現復雜業務的處理。而春來所開發的ngx_openresty更是通過集成LuaJIT等組件,將Nginx本身變成了一個完全的應用開發平台。目前淘寶數據平台與產品部量子統計的產品都是基於ngx_openresty所開發。
本文將會重點關注Nginx模塊開發入門及基礎。目前Nginx的學習資料非常少,而擴展模塊開發相關的資料幾乎只有《Emiller's Guide To Nginx Module Development》一文,此文十分經典,但是由於Nginx版本的演進,其中少許內容可能有點過時。本文是筆者在研讀這篇文章和Nginx源代碼的基礎上,對自己學習Nginx模塊開發的一個總結。本文將通過一個完整的模塊開發實例講解Nginx模塊開發的入門內容。
2. 簡介
nginx是一個開源的高性能web服務器系統,事件驅動的請求處理方式和極其苛刻的資源使用方式,使得nginx成為名副其實的高性能服務器。nginx的源碼質量也相當高,作者“家釀”了許多代碼,自造了不少輪子,諸如內存池、緩沖區、字符串、鏈表、紅黑樹等經典數據結構,事件驅動模型,http解析,各種子處理模塊,甚至是自動編譯腳本都是作者根據自己的理解寫出來的,也正因為這樣,才使得nginx比其他的web服務器更加高效。
nginx的代碼相當精巧和緊湊,雖然全部代碼僅有10萬行,但功能毫不遜色於幾十萬行的apache。不過各個部分之間耦合的比較厲害,很難把其中某個部分的實現拆出來使用。對於這樣一個中大型的復雜系統源碼進行分析,是有一定的難度的,剛開始也很難找到下手的入口,所以做這樣的事情就必須首先明確目標和計划。
首先這個系統中幾乎涵蓋了實現高性能服務器的各種必殺技,epoll、kqueue、master-workers、pool、buffer... ...,也涵蓋了很多web服務開發方面的技術,ssi、ssl、proxy、gzip、regex、load balancing、reconfiguration、hot code swapping... ...,還有一些常用的精巧的數據結構實現,所有的東西很主流;其次是一流的代碼組織結構和干凈簡潔的代碼風格,尤其是整個系統的命名恰到好處,可讀性相當高,這種風格值得學習和模仿;第三是通過閱讀源碼可以感受到作者嚴謹的作風和卓越的能力,可以給自己增加動力,樹立榜樣的力量。
另一方面,要達到這些目標難度很高,必須要制定詳細的計划和采取一定有效的方法。
對於這么大的一個系統,想一口氣知曉全部的細節是不可能的,並且nginx各個部分的實現之間關系緊密,不可能做到窺一斑而知全身,合適的做法似乎應該是從main開始,先了解nginx的啟動過程的順序,然后進行問題分解,再逐個重點分析每一個重要的部分。
對每個理解的關鍵部分進行詳細的記錄和整理也是很重要的,這也是這個源碼分析日志系列所要完成的任務。
為了更深刻的理解代碼實現的關鍵,修改代碼和寫一些測試用例是不可避免的,這就需要搭建一個方便調試的環境,這也比較容易,因為使用的linux系統本身就是一個天然的開發調試環境。
3. 概覽
3.1. 代碼閱讀心得
源碼分析是一個逐步取經的過程,最開始是一個大概了解的過程,各種認識不會太深刻,但是把這些真實的感受也記錄下來,覺得挺有意思的,可能有些認識是片面或者是不正確的,但可以通過后面更深入細致的分析過程,不斷的糾正錯誤和深化理解。源碼分析是一個過程,經驗是逐步累積起來的,希望文字可以把這種累積的感覺也准確記錄下來。
現在就看看對nginx源碼的第一印象吧。
大體上分析源代碼都要經歷三遍過程:
l 瀏覽,通過閱讀源碼的文檔和注釋,閱讀接口,先弄清楚每個模塊是干什么的而不關心它是怎么做的,畫出架構草圖。
l 精讀,根據架構草圖把系統分為小部分,每個部分從源碼實現自底向上的閱讀,更深入細致的理解每個模塊的實現方式以及與模塊外部的接口方式等,弄明白模塊是怎么做的,為什么這樣做,有沒有更好的方式,自己會如何實現等等問題。
l 總結回顧,完善架構圖,把架構圖中那些模糊的或者空着的模塊重新補充完善,把一些可復用的實現放入自己的代碼庫中。
現在是瀏覽階段,並不適合過早涉及代碼的實現細節,要借助nginx的文檔理解其整體架構和模塊划分。經過幾年的發展,nginx現在的文檔已經是很豐富了,nginx的英文wiki上包含了各個模塊的詳細文檔,faq也涵蓋了很豐富的論題,利用這些文檔足以建立 nginx的架構草圖。所以瀏覽階段主要的工作就是閱讀文檔和畫架構草圖了。
對於源碼分析,工具是相當關鍵的。這幾天閱讀源碼的過程,熟悉了三個殺手級的工具:scrapbook離線文件管理小程序、graphviz圖形生成工具、leo-editor文學編程理念的編輯器。
scrapbook是firefox下一款輕量高效的離線文件管理擴展程序,利用scrapbook把nginx的wiki站點鏡像到本地只需要幾分鍾而已,管理也相當簡單,和書簽類似。
graphviz是通過編程畫圖的工具集合,用程序把圖形的邏輯和表現表示出來,然后通過命令行執行適當的命令就可以解析生成圖形。
leo- editor與其說是一個工具平台,不如說是一套理念。和其他編輯器ide不同的是,leo關注的是文章內容的內在邏輯和段落層次,文章的表現形式和格式是次要的。用leo的過程,其實就是在編程,雖然剛開始有些不適應,但習慣之后確實很爽,殺手級的體驗感,很聽話。
3.2. 原碼結構
源碼包解壓之后,根目錄下有幾個子目錄和幾個文件,最重要的子目錄是auto和src,最重要的文件是configure腳本。
不同於絕大多數的開源代碼,nginx的configure腳本是作者手工編寫的,沒有使用autoconf之類的工具去自動生成,configure腳本會引用auto目錄下面的腳本文件來干活。
根據不同的用途,auto目錄下面的腳本各司其職,有檢查編譯器版本的,有檢查操作系統版本的,有檢查標准庫版本的,有檢查模塊依賴情況的,有關於安裝的,有關於初始化的,有關於多線程檢查的等等。
configure作為一個總驅動,調用這些腳本去生成版本信息頭文件、默認被包含的模塊的聲明代碼和Makefile文件,版本信息頭文件 (ngx_auto_config.h,ngx_auto_headers.h)和默認被包含的模塊的聲明代碼(ngx_modules.c)被放置在新創建的objs目錄下。
要注意的是,這幾個生成的文件和src下面的源代碼一樣重要,對於理解源碼是不可忽略的重要部分。
3.3. 原碼路徑
src是源碼存放的目錄,configure創建的objs/src目錄是用來存放生成的.o文件的,注意區分一下。
src按照功能特性划分為幾個部分,對應着是幾個不同的子目錄。
l src/core存放着主干部分、基礎數據結構和基礎設施的源碼,main函數在src/core/nginx.c中,這是分析源碼的一個很好的起點。
l src/event存放着事件驅動模型和相關模塊的源碼。
l src/http存放着http server和相關模塊的源碼。
l src/mail存放着郵件代理和相關模塊的源碼。
l src/misc存放着C++兼容性測試和google perftools模塊的源碼。
l src/os存放着依賴於操作系統實現的源碼,nginx啟動過程中最重要的master和workers創建代碼就在這個目錄下,多少讓人覺得有點意外。
l nginx 的實現中有非常多的結構體,一般命名為ngx_XXX_t,這些結構體分散在許多頭文件中,而在src/core/ngx_core.h中把幾乎所有的頭文件都集合起來,所有的實現文件都會包含這個ngx_core.h頭文件,說nginx的各部分源碼耦合厲害就是這個原因,但實際上nginx各個部分之間邏輯上是划分的很清晰的,整體上是一種松散的結構。nginx實現了一些精巧的基礎數據結構,例如 ngx_string_t,ngx_list_t,ngx_array_t,ngx_pool_t,ngx_buf_t,ngx_queue_t,ngx_rbtree_t,ngx_radix_tree_t 等等,還有一些重要的基礎設施,比如log,configure file,time等等,這些數據結構和基礎設施頻繁的被使用在許多地方,這會讓人感覺nginx邏輯上的聯系比較緊密,但熟悉了這些基礎數據結構的實現代碼就會感覺到這些數據結構都是清晰分明的,並沒有真正的耦合在一起,只是有些多而已,不過nginx中“家釀”的代碼也正是它的一個很明顯的亮點。
nginx是高度模塊化的,可以根據自己的需要定制模塊,也可以自己根據一定的標准開發需要的模塊,已經定制的模塊會在objs/ngx_modules.c中聲明,這個文件是由configure生成的。
nginx 啟動過程中,很重要的一步就是加載和初始化模塊,這是在ngx_init_cycle中完成的,ngx_init_cycle會調用模塊的hook接口(init_module)對模塊初始化,ngx_init_cycle還會調用ngx_open_listening_sockets初始化 socket,如果是多進程方式啟動,就會調用ngx_master_process_cycle完成最后的啟動動作,ngx_master_process_cycle調用ngx_start_worker_processes生成多個工作子進程,ngx_start_worker_processes調用ngx_worker_process_cycle創建工作內容,如果進程有多個子線程,這里也會初始化線程和創建線程工作內容,初始化完成之后,ngx_worker_process_cycle會進入處理循環,調用 ngx_process_events_and_timers,該函數調用ngx_process_events監聽事件,並把事件投遞到事件隊列 ngx_posted_events中,最終會在ngx_event_thread_process_posted中處理事件。
事件機制是nginx中很關鍵的一個部分,linux下使用了epool,freebsd下使用了kqueue管理事件。
3.4. 資料
利用nginx wiki和互聯網收集了不少nginx相關的文檔資料,但是仔細閱讀之后發覺對理解nginx架構有直接幫助的資料不多,一些有幫助的資料也要結合閱讀部分源碼細節才能搞清楚所述其是,可能nginx在非俄國之外的環境下流行不久,應用還很簡單,相關的英文和中文文檔也就不夠豐富的原因吧。
如果要了解nginx的概況和使用方法,wiki足以滿足需要,wiki上有各個模塊的概要和詳細指令說明,也有豐富的配置文件示例,不過對於了解nginx系統架構和開發沒有相關的文檔資料。
nginx的開發主要是指撰寫自定義模塊代碼。這需要了解nginx的模塊化設計思想,模塊化也是nginx的一個重要思想。如果要整體上了解nginx,從模塊化入手是一個不錯的起點。emiller的nginx模塊開發指引是目前最好的相關資料了(http://emiller.info/nginx-modules-guide.html),這份文檔作為nginx的入門文檔也是合適的,不過其中有些內容很晦澀,很難理解,要結合閱讀源碼,反復比對才能真正理解其內涵。
如果要從整體上了解nginx架構和源碼結構,Joshua zhu的廣州技術沙龍講座的pdf和那張大圖是不錯的材料,這份pdf可以在wiki的資源頁面中找到地址鏈接。
相信最好的文檔就是源碼本身了。隨着閱讀源碼的量越來越大,也越來越深入,使我認識到最寶貴的文檔就在源碼本身。之前提到過,nginx的代碼質量很高,命名比較講究,雖然很少注釋,但是很有條理的結構體命名和變量命名使得閱讀源碼就像是閱讀文檔。不過要想順利的保持這種感覺也不是一件簡單的事情,覺得要做好如下幾點:
1)熟悉C語言,尤其是對函數指針和宏定義要有足夠深入的理解,nginx是模塊化的,它的模塊化不同於apache,它不是動態加載模塊,而是把需要的模塊都編譯到系統中,這些模塊可以充分利用系統核心提供的諸多高效的組件,把數據拷貝降到最低的水平,所以這些模塊的實現利用了大量的函數指針實現回掉操作,幾乎是無函數指針不nginx的地步。
2)重點關注nginx的命名,包括函數命名,結構體命名和變量命名,這些命名把nginx看似耦合緊密的實現代碼清晰的分開為不同層次不同部分的組件和模塊,這等效於注釋。尤其要關注變量的命名,后面關於模塊的分析中會再次重申這一點。
3)寫一個自定義的模塊,利用nginx強大的內部組件,這是深入理解nginx的一個有效手段。
接下來的分析過程,着眼於兩個重點,一個就是上面提到的模塊化思想的剖析,力爭結合自身理解把這個部分分析透徹;另一個重點是nginx的事件處理流程,這是高性能的核心,是nginx的core。
4. 自動腳本
nginx的自動腳本指的是configure腳本程序和auto子目錄下面的腳本程序。自動腳本完成兩件事情,其一是檢查環境,其二是生成文件。生成的文件有兩類,一類是編譯代碼需要的Makefile文件,一類是根據環境檢查結果生成的c代碼。生成的Makefile很干凈,也很容易閱讀。生成的c代碼有三個文件,ngx_auto_config.h是根據環境檢查的結果聲明的一些宏定義,這個頭文件被include進ngx_core.h中,所以會被所有的源碼引用到,這確保了源碼是可移植的;ngx_auto_headers.h中也是一些宏定義,不過是關於系統頭文件存在性的聲明;ngx_modules.c是默認被包含進系統中的模塊的聲明,如果想去掉一些模塊,只要修改這個文件即可。
configure是自動腳本的總驅動,它通過組合auto目錄下不同功能的腳本程序完成環境檢查和生成文件的任務。環境檢查主要是三個部分:編譯器版本及支持特性、操作系統版本及支持特性、第三方庫支持,檢查的腳本程序分別存放在auto/cc、auto/os、auto/lib三個子目錄中。檢查的方法很有趣,通過自動編譯用於檢查某個特性的代碼片段,根據編譯器的輸出情況判定是否支持該種特性。根據檢查的情況,如果環境足以支持運行一個簡單版本的nginx,就會生成 Makefile和c代碼,這些文件會存放在新創建的objs目錄下。當然,也可能會失敗,假如系統不支持pcre和ssh,如果沒有屏蔽掉相關的模塊,自動腳本就會失敗。
auto目錄下的腳本職能划分非常清晰,有檢查環境的,有檢查模塊的,有提供幫助信息的(./configure –help),有處理腳本參數的,也有一些腳本純粹是為了模塊化自動腳本而設計出來的,比如feature腳本是用於檢查單一特性的,其他的環境檢查腳本都會調用這個腳本去檢查某個特性。還有一些腳本是用來輸出信息到生成文件的,比如have、nohave、make、install等。
之所以要在源碼分析中專門談及自動腳本,是因為nginx的自動腳本不是用autoconf之類的工具生成的,而是作者手工編寫的,並且包含一定的設計成分,對於需要編寫自動腳本的人來說,有很高的參考價值。這里也僅僅是粗略的介紹一下,需要詳細了解最好是讀一下這些腳本,這些腳本並沒有使用多少生僻的語法,可讀性是不錯的。
后面開始進入真正的源碼分析階段,nginx的源碼中有非常多的結構體,這些結構體之間引用也很頻繁,很難用文字表述清楚之間的關系,覺得用圖表是最好的方式,因此需要掌握一種高效靈活的作圖方法,我選擇的是graphviz,這是at&t貢獻的跨平台的圖形生成工具,通過寫一種稱為“the dot language”的腳本語言,然后用dot命令就可以直接生成指定格式的圖,很方便。
5. 在Linux下的安裝與運行
使用Nginx的第一步是下載Nginx源碼包,例如1.0.0的下載地址為http://nginx.org/download/nginx-1.0.0.tar.gz。下載完后用tar命令解壓縮,進入目錄后安裝過程與Linux下通常步驟無異,例如我想將Nginx安裝到/usr/local/nginx下,則執行如下命令:
./configure --prefix=/usr/local/nginx make make install |
安裝完成后可以直接使用下面命令啟動Nginx:/usr/local/nginx/sbin/nginx。
Nginx默認以Deamon進程啟動,輸入下列命令:curl -i http://localhost/,就可以檢測Nginx是否已經成功運行;或者也可以在瀏覽器中輸入http://localhost/,應該可以看到Nginx的歡迎頁面了。
啟動后如果想停止Nginx可以使用:/usr/local/nginx/sbin/nginx -s stop。
6. 配置文件基本結構
配置文件可以看做是Nginx的靈魂,Nginx服務在啟動時會讀入配置文件,而后續幾乎一切動作行為都是按照配置文件中的指令進行的,因此如果將Nginx本身看做一個計算機,那么Nginx的配置文件可以看成是全部的程序指令。
下面是一個Nginx配置文件的實例:
#user nobody; worker_processes 8; error_log logs/error.log; pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream;
sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on;
server { listen 80; server_name localhost; location / { root /home/yefeng/www; index index.html index.htm; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } } |
Nginx配置文件是純文本文件,你可以用任何文本編輯器如vim或emacs打開它,通常它會在nginx安裝目錄的conf下,如我的nginx安裝在/usr/local/nginx,主配置文件默認放在/usr/local/nginx/conf/nginx.conf。
其中“#”表示此行是注釋,由於筆者為了學習擴展開發安裝了一個純凈的Nginx,因此配置文件沒有經過太多改動。
Nginx的配置文件是以block的形式組織的,一個block通常使用大括號“{}”表示。block分為幾個層級,整個配置文件為main層級,這是最大的層級;在main層級下可以有event、http等層級,而http中又會有server block,server block中可以包含location block。
每個層級可以有自己的指令(Directive),例如worker_processes是一個main層級指令,它指定Nginx服務的Worker進程數量。有的指令只能在一個層級中配置,如worker_processes只能存在於main中,而有的指令可以存在於多個層級,在這種情況下,子block會繼承父block的配置,同時如果子block配置了與父block不同的指令,則會覆蓋掉父block的配置。指令的格式是“指令名 參數1 參數2 … 參數N;”,注意參數間可用任意數量空格分隔,最后要加分號。
在開發Nginx HTTP擴展模塊過程中,需要特別注意的是main、server和location三個層級,因為擴展模塊通常允許指定新的配置指令在這三個層級中。
最后要提到的是配置文件是可以包含的,如上面配置文件中“include mime.types”就包含了mine.types這個配置文件,此文件指定了各種HTTP Content-type。
一般來說,一個server block表示一個Host,而里面的一個location則代表一個路由映射規則,這兩個block可以說是HTTP配置的核心。
下圖是Nginx配置文件通常結構圖示。
Nginx配置文件主要分成四部分:main(全局設置)、server(主機設置)、upstream(上游服務器設置)和 location(URL匹配特定位置后的設置)。每部分包含若干個指令。main部分設置的指令將影響其它所有設置;server部分的指令主要用於指定主機和端口;upstream的指令用於設置一系列的后端服務器;location部分用於匹配網頁位置(比如,根目錄“/”,“/images”,等等)。他們之間的關系式:server繼承main,location繼承server;upstream既不會繼承指令也不會被繼承。它有自己的特殊指令,不需要在其他地方的應用。
7. 啟動流程
7.1. main()分析
nginx啟動過程如下。
l 調用ngx_strerror_init
初始化錯誤信息,將linux系統的錯誤編碼信息,導入到內存中,為的是以后使用的時候,直接從內存中獲取,這是從效率方面出發考慮的。
使用的是malloc直接去分配內存,因為當正式使用的情況下,此功能不一定會開放。
l 調用ngx_get_options()解析命令參數;
主要選項有如下:
選項 |
作用 |
相關變量 |
-?,-h |
顯示幫助信息 |
ngx_show_version ngx_show_help |
-v |
顯示版本信息並退出 |
ngx_show_version |
-V |
顯示 nginx 的版本,編譯器版本和配置參數。 |
ngx_show_version ngx_show_configure |
-t |
不運行,而僅僅測試配置文件。nginx 將檢查配置文件的語法的正確性,並嘗試打開配置文件中所引用到的文件。 |
ngx_test_config |
-q |
|
ngx_quiet_mode |
-s signal |
發送一個信號到主進程stop, quit, reopen, reload |
ngx_process |
-p prefix |
設置配置的路徑 |
ngx_conf_file |
-c filename |
設置配置文件的名稱 |
ngx_prefix |
-g directives |
指定全局配置指令 |
ngx_conf_params |
l 調用ngx_show_version,顯示輔助信息
顯示輔助信息的主要流程如下:
IF (1 == ngx_show_version)
展示版本信息
IF (1 == ngx_show_help)
展示幫助信息
ENDIF
IF (1 == ngx_show_configure)
展示編譯信息
ENDIF
ENDIF
l 調用ngx_time_init()初始化並更新時間,如全局變量ngx_cached_time;
l 調用ngx_log_init()初始化日志,如初始化全局變量ngx_prefix,打開日志文件ngx_log_file.fd;
l 清零全局變量ngx_cycle,並為ngx_cycle.pool創建大小為1024B的內存池;
l 調用ngx_save_argv(),保存命令行參數至全局變量ngx_os_argv、ngx_argc、ngx_argv中;
l 調用ngx_process_options(),初始化ngx_cycle的prefix, conf_prefix, conf_file, conf_param等字段;
l 調用ngx_os_init(),初始化系統相關變量,如內存頁面大小ngx_pagesize,ngx_cacheline_size,最大連接數ngx_max_sockets等;
l 調用ngx_crc32_table_init(),初始化CRC表(后續的CRC校驗通過查表進行,效率高);
l 調用ngx_add_inherited_sockets()繼承sockets;
l 解析環境變量NGINX_VAR="NGINX"中的sockets,並保存至ngx_cycle.listening數組;
l 設置ngx_inherited=1;
l 調用ngx_set_inherited_sockets(),逐一對ngx_cycle.listening數組中的sockets進行設置;具體可參考<nginx源碼分析—初始化過程中處理繼承的sockets>
l 初始化每個module的index,並計算ngx_max_module;具體可參考<nginx源碼分析—模塊及其初始化>;
l 調用ngx_init_cycle()進行初始化;
l 該初始化主要對ngx_cycle結構進行;具體可參考<nginx源碼分析—全局變量ngx_cycle的初始化>;
l 若有信號,則進入ngx_signal_process()處理;
l 調用ngx_init_signals()初始化信號;主要完成信號處理程序的注冊;
l 若無繼承sockets,且設置了守護進程標識,則調用ngx_daemon()創建守護進程;
l 調用ngx_create_pidfile()創建進程記錄文件;(非NGX_PROCESS_MASTER=1進程,不創建該文件)
l 進入進程主循環;
l 若為NGX_PROCESS_SINGLE=1模式,則調用ngx_single_process_cycle()進入進程循環;
l 否則為master-worker模式,調用ngx_master_process_cycle()進入進程循環;具體可參考<nginx源碼分析—master/worker進程啟動>。
7.2. 注意問題
7.2.1. 幾個初值
ngx_cycle = cycle; ccf = (ngx_core_conf_t*) ngx_get_conf(cycle->conf_ctx, ngx_core_module); if (ccf->master && ngx_process == NGX_PROCESS_SINGLE) { ngx_process = NGX_PROCESS_MASTER; } |
此處單獨將該段代碼拿出來,說明以下問題。
ngx_process在此處的值是什么?——NGX_PROCESS_SINGLE=0
ccf->master在此處的值之什么?——NGX_CONF_UNSET=-1
ngx_prefix何時初始化的?——ngx_log_init()中初始化
(1)ccf->master
ccf->master的值是在ngx_init_cycle()函數中調用NGX_CORE_MODULE模塊的create_conf鈎子(callback)完成初始化的。
具體可參考<nginx源碼分析—core模塊callback>。
(2)ngx_process
ngx_process是全局變量,定義如下。
//./src/os/unix/ngx_process_cycle.c ngx_uint_t ngx_process; ngx_pid_t ngx_pid; ngx_uint_t ngx_threaded; |
在ngx_log_init()中初始化。實際上,在main()函數開始調用的ngx_get_options()函數,即是處理nginx啟動的命令,並從中獲取參數並賦予相應的變量。
-p:ngx_prefix
-c:ngx_conf_file
-g:ngx_conf_params
-s:ngx_signal
7.2.2. nginx工作模式
因此,main()函數返回前調用ngx_master_process_cycle()函數進入多進程(master/worker)工作模式。如下。
if (ngx_process == NGX_PROCESS_SINGLE) { //單進程 ngx_single_process_cycle(cycle); } else { //多進程 ngx_master_process_cycle(cycle); } |
具體請參考<nginx源碼分析—master/worker進程啟動>。
7.2.3. 一些配置
看源代碼時,會注意到有些宏(宏全部大寫,中間用下划線隔開,這是nginx代碼規范。實際上,絕大多數系統均采用此規范)找不到定義,例如NGX_PREFIX、NGX_CONF_PREFIX、NGX_CONF_PATH等。
實際上,這些宏定義由configure程序進行自動配置時生成。配置時會自動生成ngx_auto_config.h文件(如果你用source insight閱讀源代碼,需要將該文件加入工程),如下。
./objs/ngx_auto_config.h(此處列出其中一部分常用的宏,未按順序)
#ifndef NGX_COMPILER #define NGX_COMPILER "gcc 4.6.1 20110908 (Red Hat 4.6.1-9) (GCC) " #endif
#ifndef NGX_PCRE #define NGX_PCRE 1 #endif
#ifndef NGX_PREFIX #define NGX_PREFIX "/usr/local/nginx/" #endif
#ifndef NGX_CONF_PREFIX #define NGX_CONF_PREFIX "conf/" #endif
#ifndef NGX_CONF_PATH #define NGX_CONF_PATH "conf/nginx.conf" #endif
#ifndef NGX_PID_PATH #define NGX_PID_PATH "logs/nginx.pid" #endif
#ifndef NGX_LOCK_PATH #define NGX_LOCK_PATH "logs/nginx.lock"
#ifndef NGX_ERROR_LOG_PATH #define NGX_ERROR_LOG_PATH "logs/error.log" #endif
#ifndef NGX_HTTP_LOG_PATH #define NGX_HTTP_LOG_PATH "logs/access.log" #endif
#ifndef NGX_HTTP_CLIENT_TEMP_PATH #define NGX_HTTP_CLIENT_TEMP_PATH "client_body_temp" #endif
#ifndef NGX_HTTP_PROXY_TEMP_PATH #define NGX_HTTP_PROXY_TEMP_PATH "proxy_temp" #endif |
7.2.4. 其他開關
還有一些開關,如NGX_FREEBSD, NGX_PCRE,NGX_OPENSSL等,這些宏也在configure過程中自動配置。nginx啟動時會根據這些宏是否定義調用相應的函數。此處非本文重點,不再贅述。
以上3個宏分別調用相應函數進行debug的初始化、正則表達式初始化和SSL的初始化。如下。(代碼未按順序)
#if (NGX_FREEBSD) ngx_debug_init(); #endif
#if (NGX_PCRE) ngx_regex_init(); #endif
#if (NGX_OPENSSL) ngx_ssl_init(log); #endif |
7.3. 小結
本文簡單分析nginx的啟動過程。主要是main函數中的調用,在main中,只有頂層的調用,沒有接觸到細節。
8. 模塊工作原理概述
8.1. 主體流程
Nginx本身支持多種模塊,如HTTP模塊、EVENT模塊和MAIL模塊,本文只討論HTTP模塊。
Nginx本身做的工作實際很少,當它接到一個HTTP請求時,它僅僅是通過查找配置文件將此次請求映射到一個location block,而此location中所配置的各個指令則會啟動不同的模塊去完成工作,因此模塊可以看做Nginx真正的勞動工作者。通常一個location中的指令會涉及一個handler模塊和多個filter模塊(當然,多個location可以復用同一個模塊)。handler模塊負責處理請求,完成響應內容的生成,而filter模塊對響應內容進行處理。因此Nginx模塊開發分為handler開發和filter開發(本文不考慮load-balancer模塊)。下圖展示了一次常規請求和響應的過程。
Nginx的模塊有三種角色:
- handlers 處理http請求並構造輸出
- filters 處理handler產生的輸出
- load-balancers 當有多於一個的后端服務器時,選擇一台將http請求發送過去
許多可能你認為是web server的工作,實際上都是由模塊來完成的:任何時候,Nginx提供文件或者轉發請求到另一個server,都是通過handler來實現的;而當需要Nginx用gzip壓縮輸出或者在服務端加一些東東的話,filter就派上用場了;Nginx的core模塊主要管理網絡層和應用層協議,並啟動針對特定請求的一系列后續模塊。這種分散式的體系結構使得由你自己來實現強大的內部單元成為了可能。
注意:不像Apache的模塊那樣,Nginx的模塊都不是動態鏈接的。(換句話說,Nginx的模塊都是靜態編譯的) 模塊是如何被調用的呢?典型地說,當server啟動時,每一個handler都有機會去處理配置文件中的location定義,如果有多個 handler被配置成需要處理某一特定的location時,只有其中一個handler能夠“獲勝”。
一個handler有三種返回方式:正常、錯誤、放棄處理轉由默認的handler來處理(典型地如處理靜態文件的時候)。
如果handler的作用是把請求反向代理到后端服務器,那么就是剛才說的模塊的第三種角色load-balancer了。load-balancer主要是負責決定將請求發送給哪個后端服務器。Nginx目前支持兩種load-balancer模塊:round-robin(輪詢,處理請求就像打撲克時發牌那樣)和IP hash(眾多請求時,保證來自同一ip的請求被分發的同一個后端服務器)。
如果handler返回(就是http響應,即filter的輸入)正確無誤,那么fileter就被調用了。每個location配置里都可以添加多個filter,所以說(比如)響應可以被壓縮和分塊。多個filter的執行順序是編譯時就確定了的。filter采用了經典的“接力鏈表(CHAIN OF RESPONSIBILITY)”模式:一個filter被調用並處理,接下來調用下一個filter,直到最后一個filter被調用完成,Nginx 才真正完成響應流程。
最帥的部分是在 filter鏈中,每個filter不會等待之前的filter完全完工,它可以處理之前filter正在輸出的內容,這有一點像Unix中的管道。 Filter的操作都基於buffers_,buffer通常情況下等於一個頁的大小(4k),你也可以在nginx.conf里改變它的大小。這意味着,比如說,模塊可以在從后端服務器收到全部的響應之前,就開始壓縮這個響應並流化(stream to)給客戶端了。
總結一下上面的內容,一個典型的周期應當是這樣的:
² 客戶端發送HTTP request
² Nginx基於location的配置選擇一個合適的handler
² (如果有) load-balancer選擇一個后端服務器
² Handler處理請求並順序將每一個響應buffer發送給第一個filter
² 第一個filter講輸出交給第二個filter
² 第二個給第三個
² 第三個給第四個
² 以此類推
² 最終響應發送給客戶端
我之所以說“典型地”是因為Ngingx的模塊具有很強的定制性。模塊開發者需要花很多精力精確定義模塊在何時、如何產生作用。模塊調用實際上是通過一系列的回調函數做到的,很多很多。名義上來說,你的函數可以在以下時候被執行:
l server讀取配置文件之前
l 讀取location和server的每一條配置指令
l 當Nginx初始化main配置段時
l 當Nginx初始化server配置段時(例如:host/port)
l 當Nginx合並server配置和main配置時
l 當Nginx初始化location配置時
l 當Nginx合並location配置和它的父server配置時
l 當Nginx的主進程啟動時
l 當一個新的worker進程啟動時
l 當一個worker進程退出時
l 當主進程退出時
l handle 一個請求
l Filter響應頭
l Filter響應體
l 選擇一個后端服務器
l 初始化一個將發往后端服務器的請求
l 重新-初始化一個將發往后端服務器的請求
l 處理來自后端服務器的響應
l 完成與后端服務器的交互
難以置信!有這么多的功能任你處置,而你只需僅僅通過多組有用的鈎子(由函數指針組成的結構體)和相應的實現函數。
8.2. 模塊類型及配置地方
要知道nginx有哪些模塊,一個快速的方法就是編譯nginx。編譯之后,會在源代碼根目錄下生成objs目錄,該目錄中包含有objs/ngx_auto_config.h和objs/ngx_auto_headers.h,以及objs/ngx_modules.c文件,當然,還有Makefile文件等。
其中,生成的objs/ngx_modules.c文件中,重新集中申明(使用extern關鍵字)了nginx配置的所有模塊,這些模塊可通過編譯前的configure命令進行配置,即設置哪些模塊需要編譯,哪些不被編譯。如下:
extern ngx_module_t ngx_core_module; extern ngx_module_t ngx_errlog_module; extern ngx_module_t ngx_conf_module; extern ngx_module_t ngx_events_module; extern ngx_module_t ngx_event_core_module; extern ngx_module_t ngx_epoll_module; extern ngx_module_t ngx_regex_module; extern ngx_module_t ngx_http_module; extern ngx_module_t ngx_http_core_module; ……. |
很顯然,這些模塊均是在此處用extern進行申明,以表明其他模塊可以訪問,而對其本身的定義和初始化ngx_module_t結構在其對應的.c文件中進行。例如,ngx_core_module模塊便是在src/core/nginx.c文件中定義並進行靜態初始化。實際上,ngx_core_module是一個全局的結構體對象,其他模塊類同。
8.3. 模塊描述
8.3.1. 數據結構
8.3.1.1. ngx_module_t結構
nginx的模塊化架構最基本的數據結構為ngx_module_t,因此,此處,我們先分析這個結構,在./src/core/ngx_conf_file.h文件中定義。如下:
#define NGX_MODULE_V1 0, 0, 0, 0, 0, 0, 1 //該宏用來初始化前7個字段 #define NGX_MODULE_V1_PADDING 0, 0, 0, 0, 0, 0, 0, 0 //該宏用來初始化最后8個字段
struct ngx_module_s{ ngx_uint_t ctx_index; //分類模塊計數器 ngx_uint_t index; //模塊計數器
ngx_uint_t spare0; ngx_uint_t spare1; ngx_uint_t spare2; ngx_uint_t spare3;
ngx_uint_t version; //版本 void *ctx; //該模塊的上下文,每個種類的模塊有不同的上下文 ngx_command_t *commands; //該模塊的命令集,指向一個ngx_command_t結構數組 ngx_uint_t type; //該模塊的種類,為core/event/http/mail中的一種
//以下是一些callback函數 ngx_uint_t (*init_master)(ngx_log_t *log); //初始化master ngx_uint_t (*init_module)(ngx_cycle_t *cycle); //初始化模塊 ngx_uint_t (*init_process)(ngx_cycle_t *cycle); //初始化工作進程 ngx_uint_t (*init_thread)(ngx_cycle_t *cycle); //初始化線程 void (*exit_thread)(ngx_cycle_t *cycle); //退出線程 void (*exit_process)(ngx_cycle_t *cycle); //退出工作進程 void (*exit_master)(ngx_cycle_t *cycle); //退出master
uintptr_t spare_hook0; //這些字段貌似沒用過 uintptr_t spare_hook1; uintptr_t spare_hook2; uintptr_t spare_hook3; uintptr_t spare_hook4; uintptr_t spare_hook5; uintptr_t spare_hook6; uintptr_t spare_hook7; }; |
其中,init_master, init_module, init_process, init_thread, exit_thread, exit_process, exit_master分別在初始化master、初始化模塊、初始化工作進程、初始化線程、退出線程、退出工作進程、退出master時被調用。
8.3.1.2. ngx_command_t結構
模塊的命令集commands指向一個ngx_command_t結構數組,在src/core/ngx_conf_file.h文件中定義。如下:
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; };
#define ngx_null_command { ngx_null_string, 0, NULL, 0, 0, NULL } //空命令 |
8.3.2. 模塊類圖
nginx為C語言開發的開源高性能web server,其代碼中大量使用了callback方式,例如模塊結構ngx_module_t中的init_master等。實際上,我們可以將ngx_module_t看作C++的一個類,其中的數據字段便是其屬性,而那些callback便是該類的操作。——這應該就是nginx的模塊化思想。畫出的ngx_module_t的類圖如下:
8.4. 組織模塊
8.4.1. 全局數組ngx_modules
nginx擁有幾十個模塊,那么,這些模塊是如保存在一個全局指針數組ngx_modules[]中,數組的每一個元素均為一個全局ngx_module_t對象的指針。如下。請參考objs/ngx_modules.c文件中的定義:
ngx_module_t *ngx_modules[] = { &ngx_core_module, &ngx_errlog_module, &ngx_conf_module, &ngx_events_module, &ngx_event_core_module, …. } |
8.4.2. 組織結構圖
這些模塊的組織結構圖如下所示,因模塊較多,圖中只畫出一部分有代表性的重要模塊:
8.5. 模塊種類
在對全局數組ngx_modules進行初始化時,即對每一個模塊進行了靜態初始化。其中對模塊的type字段的初始化是通過以下幾個宏進行的。
(1) 文件./src/core/ngx_conf_file.h
#define NGX_CORE_MODULE 0x45524F43 /* "CORE" */ #define NGX_CONF_MODULE 0x464E4F43 /* "CONF" */ |
(2) 文件./src/event/ngx_event.h
#define NGX_EVENT_MODULE 0x544E5645 /* "EVNT" */ |
(3) 文件./src/http/ngx_http_config.h
#define NGX_HTTP_MODULE 0x50545448 /* "HTTP" */ |
即模塊種類宏,定義為一個十六進制的數,這個十六進制的數就是其類型對應的ASCII碼。因此,nginx共有4種類型的模塊,分別為"CORE","CONF","EVNT","HTTP"。
實際上,如果在configure階段,使用了"--with-mail"參數,mail模塊將被編譯進來,其對應的宏如下。
#define NGX_MAIL_MODULE 0x4C49414D /* "MAIL" */ |
因此,嚴格來講,nginx有5中類型的模塊,"CORE","CONF","EVNT","HTTP","MAIL"。
8.6. 初始化模塊
8.6.1. 靜態初始化
即編譯期間完成的數據成員初始化。記mname為某個模塊的名字,其靜態初始化過程如下。
(1) 用宏NGX_MODULE_V1初始化前7個字段
(2) 用全局對象ngx_mname_module_ctx的地址初始化ctx指針
(3) 用全局數組ngx_mname_commands[]初始化commands指針
(4) 用宏NGX_CORE_MODULE等初始化type字段
(5) 初始化init_master等callback
(6) 用宏NGX_MODULE_V1_PADDING初始化最后8個字段
由此可見,在定義該模塊(全局結構對象)時,將其ctx_index和index均初始化為0。因此,模塊的靜態初始化(數據成員初始化)實際上只是對模塊上下文、模塊命令集和模塊類型進行初始化。
8.6.2. 動態初始化
即nginx運行(啟動)初期,對模塊本身的初始化。
8.6.2.1. index字段的初始化
對各個模塊的index字段的初始化是在main函數中進行的,如下。
ngx_max_module = 0; for (i = 0; ngx_modules[i]; i++) { ngx_modules[i]->index = ngx_max_module++; } |
可見,該for-loop執行后,每個模塊的index值便是其在ngx_modules[]數組中的下標值,且全局變量ngx_max_module為模塊個數。
8.6.2.2. ctx_index字段的初始化
(1) "EVNT"類型的模塊
static char * ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { /* …. */ ngx_event_max_module = 0; for (i = 0; ngx_modules[i]; i++) { if (ngx_modules[i]->type ! = NGX_EVENT_MODULE) { continue; }
ngx_modules[i]->ctx_index = ngx_event_max_module++; } /* ….. */ } |
(2) "HTTP"類型的模塊
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { /* .... */ ngx_http_max_module = 0; for (m = 0; ngx_modules[m]; m++) { if (ngx_modules[m]->type ! = NGX_HTTP_MODULE) { continue; }
ngx_modules[m]->ctx_index = ngx_http_max_module++; } /* .... */ } |
(3) "MAIL"類型的模塊
static char * ngx_mail_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { /* ... */ ngx_mail_max_module = 0; for (m = 0; ngx_modules[m]; m++) { if (ngx_modules[m]->type ! = NGX_MAIL_MODULE) { continue; }
ngx_modules[m]->ctx_index = ngx_mail_max_module++; } /* ... */ } |
8.6.2.3. 其他初始化
其他的初始化工作,將在nginx啟動及其進程啟動分析中介紹。
8.7. 小結
本文主要講述了nginx的模塊及其初始化,包括所有模塊的組織,及模塊的靜態初始化和部分動態初始化。
9. 數據結構
9.1. 字符串類型
9.1.1. 簡介
Nginx使用自己的、單獨的方式去管理字符串類型。
使用的文件:src/core/string.h/c。
9.1.2. 字符串結構
typedef struct { size_t len; //字符串長度 u_char* data; //指向字符串的指針 } ngx_str_t; #define ngx_null_string { 0, NULL } |
字符串類型結構簡單,在整個nginx中,到處使用。
9.1.3. 提供的操作
ngx_string |
初始化函數 |
ngx_null_string |
初始化空字符串函數 |
ngx_tolower |
字符轉小寫函數 |
ngx_toupper |
字符轉大寫函數 |
ngx_strncmp |
比較指定長度的字符串是否相同 |
ngx_strcmp |
比較字符串是否相同 |
ngx_strstr |
從字符串中找到需要的字符串 |
ngx_strlen |
字符串的長度 |
ngx_strchr |
在字符串中找到匹配的字符,返回 0為匹配 |
ngx_strlchr |
在字符串中找到匹配的字符,返回匹配的指針 |
ngx_memzero |
把一片內存區設置為0 |
ngx_memset |
把一片內存區設置為指定的數 |
ngx_memcpy |
復制內存,沒有返回 |
ngx_cpymem |
復制內存,返回復制完了dst的最后一個字符的下一個字符的指針 |
ngx_copy |
同ngx_cpymem |
ngx_memcmp |
比較內存中的數據是否相同 |
ngx_strlow |
把字符串都轉換成小寫 |
ngx_cpystrn |
復制字符串,並且返回字符串的最后一個字符的下一個字符的指針 |
ngx_pstrdup |
復制字符串到pool,返回字符串的指針 |
ngx_sprintf |
把各種類型的數據格式化輸出到buf,最大的長度為65536 |
ngx_snprintf |
把各種類型的數據格式化輸出到指定長度的buf |
ngx_strcasecmp |
不分大小寫比較兩個字符串是否相同 |
ngx_strncasecmp |
指定長短不分大小寫比較兩個字符串是否相同 |
ngx_strnstr |
在指定大小一個字符串中是否有子字符串 |
ngx_strstrn |
在一個字符串中是否有子指定大小的字符串 |
ngx_strcasestrn |
在一個字符串中是否有子指定大小的字符串,不區分大小寫 |
ngx_rstrncmp |
從后往前比較兩個字符串是否相同,返回相同的位置 |
ngx_rstrncasecmp |
從后往前比較兩個字符串是否相同,返回相同的位置,不區分大小寫 |
ngx_memn2cmp |
比較兩個指定長度的內存是否相同,也比較長的內存是否包含短的內存 |
ngx_atoi |
指定長度的字符串轉換成數字 |
ngx_atosz |
指定長度的字符串轉換成ssize_t類型數字 |
ngx_atoof |
指定長度的字符串轉換成off_t類型數字 |
ngx_atotm |
指定長度的字符串轉換成time_t類型數字 |
ngx_hextoi |
指定長度的字符串轉換成十六進制數字 |
ngx_hex_dump |
把數字轉換成16進制的字符串 |
ngx_encode_base64 |
base64編碼 |
ngx_decode_base64 |
base64解碼 |
ngx_utf8_decode |
把 utf8字符解碼成雙字節的 unicode或是單字節字符,但是該函數會移動*p的值 |
ngx_utf8_length |
得到utf8編碼的字符占幾個字節 |
ngx_utf8_cpystrn |
賦值utf8字符串,保證完整的復制 |
ngx_escape_uri |
對uri進行編碼 |
ngx_unescape_uri |
對uri的進行解碼 |
ngx_escape_html |
對html進行編碼 |
ngx_sort |
排序,主要是用於數組排序 |
ngx_qsort |
快速排序 |
ngx_value |
把宏數字轉換成字符串 |
9.2. 內存池ngx_pool_t
9.2.1. 簡介
nginx對內存的管理由其自己實現的內存池結構ngx_pool_t來完成,本文重點敘述nginx的內存管理。
nginx內存管理相關文件:
(1) src/os/unix/ngx_alloc.h/.c
內存相關的操作,封裝了最基本的內存分配函數。
如free/malloc/memalign/posix_memalign,分別被封裝為ngx_free,ngx_alloc/ngx_calloc, ngx_memalign
l ngx_alloc:封裝malloc分配內存
l ngx_calloc:封裝malloc分配內存,並初始化空間內容為0
l ngx_memalign:返回基於一個指定alignment的大小為size的內存空間,且其地址為alignment的整數倍,alignment為2的冪。
(2) ./src/core/ngx_palloc.h/.c
l 封裝創建/銷毀內存池,從內存池分配空間等函數
9.2.2. 內存池結構
nginx對內存的管理均統一完成,例如,在特定的生命周期統一建立內存池(如main函數系統啟動初期即分配1024B大小的內存池),需要內存時統一分配內存池中的內存,在適當的時候釋放內存池的內存(如關閉http鏈接時調用ngx_destroy_pool進行銷毀)。
因此,開發者只需在需要內存時進行申請即可,不用過多考慮內存的釋放等問題,大大提高了開發的效率。先看一下內存池結構。
9.2.2.1. ngx_pool_t結構
此處統一一下概念,內存池的數據塊:即分配內存在這些數據塊中進行,一個內存池可以有多一個內存池數據塊。nginx的內存池結構如下:
typedef struct { //內存池的數據塊位置信息 u_char *last; //當前內存池分配到此處,即下一次分配從此處開始 u_char *end; //內存池結束位置 ngx_pool_t *next; //內存池里面有很多塊內存,這些內存塊就是通過該指針連成鏈表的 ngx_uint_t failed; //內存池分配失敗次數 } ngx_pool_data_t;
struct ngx_pool_s{ //內存池頭部結構 ngx_pool_data_t d; //內存池的數據塊 size_t max; //內存池數據塊的最大值 ngx_pool_t *current; //指向當前內存池 ngx_chain_t *chain; //該指針掛接一個ngx_chain_t結構 ngx_pool_large_t *large; //大塊內存鏈表,即分配空間超過max的內存 ngx_pool_cleanup_t *cleanup; //釋放內存池的callback ngx_log_t *log; //日志信息 }; |
其中,sizeof(ngx_pool_data_t)=16B,sizeof(ngx_pool_t)=40B。
nginx將幾乎所有的結構體放在ngx_core.h文件中重新進行了申明,如下:
typedef struct ngx_module_s ngx_module_t; typedef struct ngx_conf_s ngx_conf_t; typedef struct ngx_cycle_s ngx_cycle_t; typedef struct ngx_pool_s ngx_pool_t; typedef struct ngx_chain_s ngx_chain_t; typedef struct ngx_log_s ngx_log_t; typedef struct ngx_array_s ngx_array_t; typedef struct ngx_open_file_s ngx_open_file_t; typedef struct ngx_command_s ngx_command_t; typedef struct ngx_file_s ngx_file_t; typedef struct ngx_event_s ngx_event_t; typedef struct ngx_event_aio_s ngx_event_aio_t; typedef struct ngx_connection_s ngx_connection_t; |
9.2.2.2. 其他相關結構
其他與內存池相干的數據結構,如清除資源的cleanup鏈表,分配的大塊內存鏈表等,如下。
/* * NGX_MAX_ALLOC_FROM_POOL should be (ngx_pagesize - 1), i.e. 4095 on x86. * On Windows NT it decreases a number of locked pages in a kernel. */ #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) //在x86體系結構下,該值一般為4096B,即4K #define NGX_DEFAULT_POOL_SIZE (16* 1024) #define NGX_POOL_ALIGNMENT 16 #define NGX_MIN_POOL_SIZE ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), NGX_POOL_ALIGNMENT)
typedef void (*ngx_pool_cleanup_pt)(void *data); //cleanup的callback類型
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t; struct ngx_pool_cleanup_s{ ngx_pool_cleanup_pt handler; void *data; //指向要清除的數據 ngx_pool_cleanup_t *next; //下一個cleanup callback };
typedef struct ngx_pool_large_s ngx_pool_large_t; struct ngx_pool_large_s{ ngx_pool_large_t *next; //指向下一塊大塊內存 void *alloc; //指向分配的大塊內存 }; ... ... typedef struct { ngx_fd_t fd; u_char *name; ngx_log_t *log; } ngx_pool_cleanup_file_t; |
(gdb) p getpagesize()
$18 = 4096
全局變量ngx_pagesize的初始化是在如下函數中完成的。src/os/unix/ngx_posix_init.c
ngx_int_t ngx_os_init(ngx_log_t *log) { ngx_uint_t n;
#if (NGX_HAVE_OS_SPECIFIC_INIT) if (ngx_os_specific_init(log) != NGX_OK) { return NGX_ERROR; } #endif ngx_init_setproctitle(log); /** 該函數為glibc的庫函數,由系統調用實現, 返回內核中的PAGE_SIZE,該值依賴體系結構**/ ngx_pagesize = getpagesize(); ngx_cacheline_size = NGX_CPU_CACHE_LINE; ... } |
這些數據結構之間的關系,請參考后面的圖。
9.2.2.3. ngx_pool_t的邏輯結構
這些數據結構邏輯結構圖如下。注:本文采用UML的方式畫出該圖。
9.2.3. 創建內存池
創建內存池有ngx_create_pool()函數完成,代碼如下:
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); if (p == NULL) { return NULL; } //last指向ngx_pool_t結構體之后數據取起始位置 p->d.last = (u_char *) p + sizeof(ngx_pool_t); //end指向分配的整個size大小的內存的末尾 p->d.end = (u_char *) p + size; p->d.next = NULL; p->d.failed = 0; size = size - sizeof(ngx_pool_t); p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL; //最大不超過4095B p->current = p; p->chain = NULL; p->large = NULL; p->cleanup = NULL; p->log = log; return p; } |
例如,調用ngx_create_pool(1024, 0x80d1c4c)后,創建的內存池物理結構如下圖:
9.2.4. 銷毀內存池
銷毀內存池由如下函數完成:void ngx_destroy_pool(ngx_pool_t *pool)。
該函數將遍歷內存池鏈表,所有釋放內存,如果注冊了clenup(也是一個鏈表結構),亦將遍歷該cleanup鏈表結構依次調用clenup的handler清理。同時,還將遍歷large鏈表,釋放大塊內存。
9.2.5. 重置內存池
重置內存池由下面的函數完成:void ngx_reset_pool(ngx_pool_t *pool)。
該函數將釋放所有large內存,並且將d->last指針重新指向ngx_pool_t結構之后數據區的開始位置,同剛創建后的位置相同。
9.2.6. 分配內存
內存分配的函數如下。
void *ngx_palloc(ngx_pool_t *pool, size_t size);
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);
返回值為分配的內存起始地址。選擇其中的兩個函數進行分析,其他的也很好理解,省略。
9.2.6.1. ngx_palloc()函數分析
ngx_palloc()代碼如下,分析請參考筆者所加的注釋。
void * ngx_palloc(ngx_pool_t *pool, size_t size) { u_char *m; ngx_pool_t *p; //判斷待分配內存與max值 if (size <= pool->max) { //小於max值,則從current節點開始遍歷pool鏈表 p = pool->current; do { m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT); if ((size_t) (p->d.end - m) >= size) { //在該節點指向的內存塊中分配size大小的內存 p->d.last = m + size; return m; } p = p->d.next; } while (p); //鏈表里沒有能分配size大小內存的節點,則生成一個新的節點並在其中分配內存 return ngx_palloc_block(pool, size); } //大於max值,則在large鏈表里分配內存 return ngx_palloc_large(pool, size); } |
例如,在之前說明,創建的內存池中分配200B的內存,調用ngx_palloc(pool, 200)后,該內存池物理結構如下圖。
9.2.6.2. ngx_palloc_block()函數分析
ngx_palloc_block函數代碼如下,分析請參考筆者所加的注釋。
static void * ngx_palloc_block(ngx_pool_t *pool, size_t size) { u_char *m; size_t psize; ngx_pool_t *p, *new, *current;
//計算pool的大小 psize = (size_t) (pool->d.end - (u_char *) pool); //分配一塊與pool大小相同的內存 m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log); if (m == NULL) { return NULL; }
new = (ngx_pool_t *) m; new->d.end = m + psize; //設置end指針 new->d.next = NULL; new->d.failed = 0;
//讓m指向該塊內存ngx_pool_data_t結構體之后數據區起始位置 m += sizeof(ngx_pool_data_t); //按4字節對齊 m = ngx_align_ptr(m, NGX_ALIGNMENT); //在數據區分配size大小的內存並設置last指針 new->d.last = m + size;
current = pool->current; for (p = current; p->d.next; p = p->d.next) { if (p->d.failed++ > 4) { //failed的值只在此處被修改 current = p->d.next; //失敗4次以上移動current指針 } }
p->d.next = new; //將這次分配的內存塊new加入該內存池 pool->current = current ? current : new; return m; } |
注意:該函數分配一塊內存后,last指針指向的是ngx_pool_data_t結構體(大小16B)之后數據區的起始位置。而創建內存池時時,last指針指向的是ngx_pool_t結構體(大小40B)之后數據區的起始位置。
9.2.7. 釋放內存
請參考如下函數:ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)。
需要注意的是該函數只釋放large鏈表中注冊的內存,普通內存在ngx_destroy_pool中統一釋放。
9.2.8. 注冊cleanup
請參考如下函數,該函數實現也很簡單,此處不再贅述。
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
9.2.9. 內存池的物理結構
針對本文上節的例子,畫出的內存池的物理結構如下圖。
從該圖也能看出結論,即內存池第一塊內存前40字節為ngx_pool_t結構,后續加入的內存塊前16個字節為ngx_pool_data_t結構,這兩個結構之后便是真正可以分配內存區域。
因此,本文Reference中的內存分配相關中的圖是有一點點小問題的,並不是每一個節點的前面都是ngx_pool_t結構。
9.2.10. 一個例子
理解並掌握開源軟件的最好方式莫過於自己寫一些測試代碼,或者改寫軟件本身,並進行調試來進一步理解開源軟件的原理和設計方法。本節給出一個創建內存池並從中分配內存的簡單例子。
9.2.10.1. 代碼
/** * ngx_pool_t test, to test ngx_palloc, ngx_palloc_block, ngx_palloc_large */
#include <stdio.h> #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_string.h" #include "ngx_palloc.h"
volatile ngx_cycle_t *ngx_cycle;
void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { }
void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } }
int main() { ngx_pool_t *pool;
printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc block 1 from the pool:\n"); printf("--------------------------------\n"); ngx_palloc(pool, 512); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc block 2 from the pool:\n"); printf("--------------------------------\n"); ngx_palloc(pool, 512); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc block 3 from the pool :\n"); printf("--------------------------------\n"); ngx_palloc(pool, 512); dump_pool(pool);
ngx_destroy_pool(pool); return 0; } |
9.2.10.2. 7.4.2、如何編譯
這個問題是編寫測試代碼或者改寫軟件本身最迫切需要解決的問題,否則,編寫的代碼無從編譯或運行,那也無從進行調試並理解軟件了。
如何對自己編寫的測試代碼進行編譯,可參考Linux平台代碼覆蓋率測試-編譯過程自動化及對鏈接的解釋、Linux平台如何編譯使用Google test寫的單元測試?。我們要做的是學習這種編譯工程的方法,針對該例子,筆者編寫的makefile文件如下。——這便是本節的主要目的。
CXX = gcc CXXFLAGS += -g -Wall -Wextra
NGX_ROOT = /usr/src/nginx-1.0.4
TARGETS = ngx_pool_t_test TARGETS_C_FILE = $(TARGETS).c
CLEANUP = rm -f $(TARGETS) *.o
all: $(TARGETS)
clean: $(CLEANUP)
CORE_INCS = -I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \
NGX_PALLOC = $(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING = $(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC = $(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o
$(TARGETS): $(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $^ -o $@ |
9.2.10.3. 7.4.3、運行運行結果
# ./ngx_pool_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8922020 .d .last = 0x8922048 .end = 0x8922420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984
-------------------------------- alloc block 1 from the pool: -------------------------------- pool = 0x8922020 .d .last = 0x8922248 .end = 0x8922420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 472
-------------------------------- alloc block 2 from the pool: -------------------------------- pool = 0x8922020 .d .last = 0x8922248 .end = 0x8922420 .next = 0x8922450 .failed = 0 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 472
pool = 0x8922450 .d .last = 0x8922660 .end = 0x8922850 .next = 0x0 .failed = 0 .max = 0 .current = 0x0 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 496
-------------------------------- alloc block 3 from the pool : -------------------------------- pool = 0x8922020 .d .last = 0x8922248 .end = 0x8922420 .next = 0x8922450 .failed = 1 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 472
pool = 0x8922450 .d .last = 0x8922660 .end = 0x8922850 .next = 0x8922880 .failed = 0 .max = 0 .current = 0x0 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 496
pool = 0x8922880 .d .last = 0x8922a90 .end = 0x8922c80 .next = 0x0 .failed = 0 .max = 0 .current = 0x0 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 496 |
9.2.11. 小結
本文針對nginx-1.0.4的內存管理進行了較為全面的分析,包括相關內存池數據結構,內存池的創建、銷毀,以及從內存池中分配內存等。最后通過一個簡單例子向讀者展示nginx內存池的創建和分配操作,同時借此向讀者展示編譯測試代碼的方法。
分析完nginx的內存管理,你一定驚嘆於nginx作者的聰明才智。這種內存管理的設計方法小巧、快捷,值得借鑒!
9.3. 數組結構ngx_array_t
9.3.1. 簡介
本文開始介紹nginx的容器,先從最簡單的數組開始。
數組實現文件:文件:./src/core/ngx_array.h/.c。
9.3.2. 數組結構
9.3.2.1. ngx_array_t結構
nginx的數組結構為ngx_array_t,定義如下:
struct ngx_array_s { void *elts; //數組數據區起始位置 ngx_uint_t nelts; //實際存放的元素個數 size_t size; //每個元素大小 ngx_uint_t nalloc; //數組所含空間個數,即實際分配的小空間的個數 ngx_pool_t *pool; //該數組在此內存池中分配 }; typedef struct ngx_array_s ngx_array_t; |
sizeof(ngx_array_t)=20。由其定義可見,nginx的數組也要從內存池中分配。將分配nalloc個大小為size的小空間,實際分配的大小為(nalloc * size)。
9.3.2.2. ngx_array_t的邏輯結構
ngx_array_t結構引用了ngx_pool_t結構,因此本文參考內存池結構ngx_pool_t及內存管理一文畫出相關結構的邏輯圖,如下:
9.3.3. 數組操作
數組操作共有5個,如下:
//創建數組 ngx_array_t*ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);
//銷毀數組 Void ngx_array_destroy(ngx_array_t *a);
//向數組中添加元素 void* ngx_array_push(ngx_array_t *a); void* ngx_array_push_n(ngx_array_t *a, ngx_uint_t n);
//初始化數組 Static ngx_inline ngx_int_t ngx_array_init(ngx_array_t*array, ngx_pool_t *pool, ngx_uint_t n, size_t size) |
因實現都很簡單,本文簡單分析前3個函數。
9.3.3.1. 創建數組
創建數組的操作實現如下,首先分配數組頭(20B),然后分配數組數據區,兩次分配均在傳入的內存池(pool指向的內存池)中進行。然后簡單初始化數組頭並返回數組頭的起始位置。
ngx_array_t* ngx_array_create(ngx_pool_t*p, ngx_uint_t n, size_t size) { ngx_array_t *a; a = ngx_palloc(p,sizeof(ngx_array_t)); //從內存池中分配數組頭 if (a == NULL) { return NULL; } a->elts = ngx_palloc(p,n * size); //接着分配n*size大小的區域作為數組數據區 if (a->elts == NULL) { return NULL; } a->nelts = 0; //初始化 a->size = size; a->nalloc = n; a->pool = p; return a; //返回數組頭的起始位置 } |
創建數組后內存池的物理結構圖如下。
9.3.3.2. 銷毀數組
銷毀數組的操作實現如下,包括銷毀數組數據區和數組頭。這里的銷毀動作實際上就是修改內存池的last指針,並沒有調用free等釋放內存的操作,顯然,這種維護效率是很高的。
void ngx_array_destroy(ngx_array_t*a) { ngx_pool_t *p; p = a->pool; if ((u_char *) a->elts+ a->size * a->nalloc == p->d.last) { //先銷毀數組數據區 p->d.last -=a->size * a->nalloc; //設置內存池的last指針 } if ((u_char *) a +sizeof(ngx_array_t) == p->d.last) { //接着銷毀數組頭 p->d.last = (u_char*) a; //設置內存池的last指針 } } |
9.3.3.3. 添加元素
向數組添加元素的操作有兩個,ngx_array_push和ngx_array_push_n,分別添加一個和多個元素。
但實際的添加操作並不在這兩個函數中完成,例如ngx_array_push返回可以在該數組數據區中添加這個元素的位置,ngx_array_push_n則返回可以在該數組數據區中添加n個元素的起始位置,而添加操作即在獲得添加位置之后進行,如后文的例子。
void * ngx_array_push(ngx_array_t*a) { void *elt, *new; size_t size; ngx_pool_t *p;
//數組數據區滿 if (a->nelts ==a->nalloc) { /* the arrayis full */ //計算數組數據區的大小 size = a->size *a->nalloc; p = a->pool; //若1.內存池的last指針指向數組數據區的末尾 //且2.內存池未使用的區域可以再分配一個size大小的小空間 if ((u_char *)a->elts + size == p->d.last &&p->d.last + a->size <= p->d.end) { /* * the array allocation is the lastin the pool * and there is space for newallocation */ //分配一個size大小的小空間(a->size為數組一個元素的大小) p->d.last +=a->size; //實際分配小空間的個數加1 a->nalloc++; } else { /* allocate a new array */ //否則,擴展數組數據區為原來的2倍 new =ngx_palloc(p, 2 * size); if (new == NULL) { return NULL; } //將原來數據區的內容拷貝到新的數據區 ngx_memcpy(new,a->elts, size); a->elts = new; //注意:此處轉移數據后,並未釋放原來的數據區,內存池將統一釋放 a->nalloc *= 2; } } //數據區中實際已經存放數據的子區的末尾 elt = (u_char *)a->elts + a->size * a->nelts; //即最后一個數據末尾,該指針就是下一個元素開始的位置 a->nelts++; //返回該末尾指針,即下一個元素應該存放的位置 return elt; } |
由此可見,向數組中添加元素實際上也是在修該內存池的last指針(若數組數據區滿)及數組頭信息,即使數組滿了,需要擴展數據區內容,也只需要內存拷貝完成,並不需要數據的移動操作,這個效率也是相當高的。
下圖是向數組中添加10個整型元素后的一個例子。代碼可參考下文的例子。當然,數組元素也不僅限於例子的整型數據,也可以是其他類型的數據,如結構體等。
9.3.4. 一個例子
9.3.4.1. 代碼
/** * ngx_array_t test, to test ngx_array_create, ngx_array_push */
#include <stdio.h> #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_string.h" #include "ngx_palloc.h" #include "ngx_array.h"
volatile ngx_cycle_t *ngx_cycle;
void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { }
void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } }
void dump_array(ngx_array_t* a) { if (a) { printf("array = 0x%x\n", a); printf(" .elts = 0x%x\n", a->elts); printf(" .nelts = %d\n", a->nelts); printf(" .size = %d\n", a->size); printf(" .nalloc = %d\n", a->nalloc); printf(" .pool = 0x%x\n", a->pool);
printf("elements: "); int *ptr = (int*)(a->elts); for (; ptr < (int*)(a->elts + a->nalloc * a->size); ) { printf("0x%x ", *ptr++); } printf("\n"); } }
int main() { ngx_pool_t *pool; int i;
printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc an array from the pool:\n"); printf("--------------------------------\n"); ngx_array_t *a = ngx_array_create(pool, 10, sizeof(int)); dump_pool(pool);
for (i = 0; i < 10; i++) { int *ptr = ngx_array_push(a); *ptr = i + 1; }
dump_array(a);
ngx_array_destroy(a); ngx_destroy_pool(pool); return 0; } |
9.3.4.2. 如何編譯
CXX = gcc CXXFLAGS +=-g -Wall -Wextra
NGX_ROOT =/usr/src/nginx-1.0.4
TARGETS =ngx_array_t_test TARGETS_C_FILE= $(TARGETS).c
CLEANUP = rm-f $(TARGETS) *.o
all:$(TARGETS)
clean: $(CLEANUP)
CORE_INCS =-I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \
NGX_PALLOC =$(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING =$(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC =$(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_ARRAY =$(NGX_ROOT)/objs/src/core/ngx_array.o
$(TARGETS):$(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING)$(NGX_ALLOC) $(NGX_ARRAY) $^ -o $@ |
9.3.4.3. 運行結果
# ./ngx_array_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x860b020 .d .last = 0x860b048 .end = 0x860b420 .next = 0x0 .failed = 0 .max = 984 .current = 0x860b020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984 -------------------------------- alloc an array from the pool: -------------------------------- pool = 0x860b020 .d .last = 0x860b084 .end = 0x860b420 .next = 0x0 .failed = 0 .max = 984 .current = 0x860b020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 924 array = 0x860b048 .elts = 0x860b05c .nelts = 10 .size = 4 .nalloc = 10 .pool = 0x860b020 elements: 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xa |
9.3.5. 小結
本文針對數組結構進行了較為全面的分析,包括數組相關數據結構,數組的創建、銷毀,以及向數組中添加元素等。最后通過一個簡單例子向讀者展示nginx數組的創建、添加元素和銷毀操作,同時借此向讀者展示編譯測試代碼的方法。
9.4. 鏈表結構ngx_list_t
9.4.1. 簡介
本文繼續介紹nginx的容器——鏈表。
鏈表實現文件:文件:src/core/ngx_list.h/.c。
9.4.2. 鏈表結構
9.4.2.1. ngx_list_t結構
nginx的鏈表(頭)結構為ngx_list_t,鏈表節點結構為ngx_list_part_t,定義如下:
typedef struct ngx_list_part_s ngx_list_part_t; struct ngx_list_part_s { //鏈表節點結構 void *elts; //指向該節點實際的數據區(該數據區中可以存放nalloc個大小為size的元素) ngx_uint_t nelts; //實際存放的元素個數 ngx_list_part_t *next; //指向下一個節點 };
typedef struct{ //鏈表頭結構 ngx_list_part_t *last; //指向鏈表最后一個節點(part) ngx_list_part_t part; //鏈表頭中包含的第一個節點(part) size_t size; //每個元素大小 ngx_uint_t nalloc; //鏈表所含空間個數,即實際分配的小空間的個數 ngx_pool_t *pool; //該鏈表節點空間在此內存池中分配 }ngx_list_t; |
其中,sizeof(ngx_list_t)=28,sizeof(ngx_list_part_t)=12。
由此可見,nginx的鏈表,也要從內存池中分配。對於每一個節點(list part)將分配nalloc個大小為size的小空間,實際分配的大小為(nalloc * size)。
9.4.2.2. ngx_list_t的邏輯結構
ngx_list_t結構引用了ngx_pool_t結構,因此本文參考內存池結構ngx_pool_t及內存管理一文畫出相關結構的邏輯圖,如下:
9.4.3. 鏈表操作
鏈表操作共3個,如下:
//創建鏈表 ngx_list_t*ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);
//初始化鏈表 static ngx_inline ngx_int_t ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_tn, size_t size);
//添加元素 void*ngx_list_push(ngx_list_t *l) |
9.4.3.1. 創建鏈表
創建鏈表的操作實現如下,首先分配鏈表頭(28B),然后分配頭節點(即鏈表頭中包含的part)數據區,兩次分配均在傳入的內存池(pool指向的內存池)中進行。然后簡單初始化鏈表頭並返回鏈表頭的起始位置。
ngx_list_t * ngx_list_create(ngx_pool_t*pool, ngx_uint_t n, size_t size) { ngx_list_t *list; list = ngx_palloc(pool,sizeof(ngx_list_t)); //從內存池中分配鏈表頭 if (list == NULL) { return NULL; } list->part.elts =ngx_palloc(pool, n * size); //接着分配n*size大小的區域作為鏈表數據區 if (list->part.elts == NULL) { return NULL; } list->part.nelts = 0; //初始化 list->part.next = NULL; list->last = &list->part; list->size = size; list->nalloc = n; list->pool = pool; return list; //返回鏈表頭的起始位置 } |
創建鏈表后內存池的物理結構圖如下:
9.4.3.2. 添加元素
添加元素操作實現如下,同nginx數組實現類似,其實際的添加操作並不在該函數中完成。函數ngx_list_push返回可以在該鏈表數據區中放置元素(元素可以是1個或多個)的位置,而添加操作即在獲得添加位置之后進行,如后文的例子。
void * ngx_list_push(ngx_list_t*l) { void *elt; ngx_list_part_t *last; last = l->last; if (last->nelts ==l->nalloc) { //鏈表數據區滿 /* the last part is full, allocate anew list part */ last =ngx_palloc(l->pool, sizeof(ngx_list_part_t)); //分配節點(list part) if (last == NULL) { return NULL; } last->elts =ngx_palloc(l->pool, l->nalloc * l->size);//分配該節點(part)的數據區 if (last->elts == NULL) { return NULL; } last->nelts = 0; last->next = NULL; l->last->next =last; //將分配的list part插入鏈表 l->last = last; //並修改list頭的last指針 }
elt = (char *)last->elts + l->size * last->nelts; //計算下一個數據在鏈表數據區中的位置 last->nelts++; //實際存放的數據個數加1
return elt; //返回該位置 } |
由此可見,向鏈表中添加元素實際上就是從內存池中分配鏈表節點(part)及其該節點的實際數據區,並修改鏈表節點(part)信息。
注1:與數組的區別,數組數據區滿時要擴充數據區空間;而鏈表每次要分配節點及其數據區。
注2:鏈表的每個節點(part)的數據區中可以放置1個或多個元素,這里的元素可以是一個整數,也可以是一個結構。
下圖是一個有3個節點的鏈表的邏輯結構圖。
圖中的線太多,容易眼暈,下面這個圖可能好一些。
9.4.4. 一個例子
9.4.4.1. 代碼
/** * ngx_list_t test, to test ngx_list_create, ngx_list_push */
#include <stdio.h> #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_string.h" #include "ngx_palloc.h" #include "ngx_list.h"
volatile ngx_cycle_t *ngx_cycle;
void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { }
void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } }
void dump_list_part(ngx_list_t* list, ngx_list_part_t* part) { int *ptr = (int*)(part->elts); int loop = 0;
printf(" .part = 0x%x\n", &(list->part)); printf(" .elts = 0x%x ", part->elts); printf("("); for (; loop < list->nalloc - 1; loop++) { printf("0x%x, ", ptr[loop]); } printf("0x%x)\n", ptr[loop]); printf(" .nelts = %d\n", part->nelts); printf(" .next = 0x%x", part->next); if (part->next) printf(" -->\n"); printf(" \n"); }
void dump_list(ngx_list_t* list) { if (list == NULL) return;
printf("list = 0x%x\n", list); printf(" .last = 0x%x\n", list->last); printf(" .part = 0x%x\n", &(list->part)); printf(" .size = %d\n", list->size); printf(" .nalloc = %d\n", list->nalloc); printf(" .pool = 0x%x\n\n", list->pool);
printf("elements:\n");
ngx_list_part_t *part = &(list->part); while (part) { dump_list_part(list, part); part = part->next; } printf("\n"); }
int main() { ngx_pool_t *pool; int i;
printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc an list from the pool:\n"); printf("--------------------------------\n"); ngx_list_t *list = ngx_list_create(pool, 5, sizeof(int)); dump_pool(pool);
for (i = 0; i < 15; i++) { int *ptr = ngx_list_push(list); *ptr = i + 1; }
printf("--------------------------------\n"); printf("the list information:\n"); printf("--------------------------------\n"); dump_list(list);
printf("--------------------------------\n"); printf("the pool at the end:\n"); printf("--------------------------------\n"); dump_pool(pool);
ngx_destroy_pool(pool); return 0; } |
9.4.4.2. 如何編譯
CXX = gcc CXXFLAGS +=-g -Wall -Wextra
NGX_ROOT =/usr/src/nginx-1.0.4
TARGETS =ngx_list_t_test TARGETS_C_FILE= $(TARGETS).c
CLEANUP = rm-f $(TARGETS) *.o
all:$(TARGETS)
clean: $(CLEANUP)
CORE_INCS =-I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \
NGX_PALLOC =$(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING =$(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC =$(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_LIST =$(NGX_ROOT)/objs/src/core/ngx_list.o
$(TARGETS):$(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING)$(NGX_ALLOC) $(NGX_LIST) $^ -o $@ |
9.4.4.3. 運行結果
# ./ngx_list_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x9208020 .d .last = 0x9208048 .end = 0x9208420 .next = 0x0 .failed = 0 .max = 984 .current = 0x9208020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984 -------------------------------- alloc an list from the pool: -------------------------------- pool = 0x9208020 .d .last = 0x9208078 .end = 0x9208420 .next = 0x0 .failed = 0 .max = 984 .current = 0x9208020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 936 -------------------------------- the list information: -------------------------------- list = 0x9208048 .last = 0x9208098 .part = 0x920804c .size = 4 .nalloc = 5 .pool = 0x9208020 elements: .part = 0x920804c .elts = 0x9208064 (0x1, 0x2, 0x3, 0x4, 0x5) .nelts = 5 .next = 0x9208078 --> .part = 0x920804c .elts = 0x9208084 (0x6, 0x7, 0x8, 0x9, 0xa) .nelts = 5 .next = 0x9208098 --> .part = 0x920804c .elts = 0x92080a4 (0xb, 0xc, 0xd, 0xe, 0xf) .nelts = 5 .next = 0x0 -------------------------------- the pool at the end: -------------------------------- pool = 0x9208020 .d .last = 0x92080b8 .end = 0x9208420 .next = 0x0 .failed = 0 .max = 984 .current = 0x9208020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 872 |
9.4.5. 小結
本文針對nginx-1.0.4的容器——鏈表結構進行了較為全面的分析,包括鏈表相關數據結構,鏈表創建和向鏈表中添加元素等。最后通過一個簡單例子向讀者展示nginx鏈表創建和添加元素操作,同時借此向讀者展示編譯測試代碼的方法。
9.5. 隊列結構ngx_queue_t
9.5.1. 簡介
鏈表實現文件:文件:src/core/ngx_queue.h/.c。
9.5.2. 隊列結構
nginx的隊列是由具有頭節點的雙向循環鏈表實現的,每一個節點結構為ngx_queue_t,定義如下:
typedef struct ngx_queue_s ngx_queue_t; struct ngx_queue_s { //隊列結構 ngx_queue_t *prev; ngx_queue_t *next; }; |
其中,sizeof(ngx_queue_t)=8。從隊列結構定義可以看出,nginx的隊列結構里並沒有其節點的數據內容。
9.5.3. 隊列操作
隊列有如下操作:
//初始化隊列 ngx_queue_init(q) //判斷隊列是否為空 ngx_queue_empty(h) //在頭節點之后插入新節點 ngx_queue_insert_head(h, x) //在尾節點之后插入新節點 ngx_queue_insert_tail(h, x) //刪除節點x ngx_queue_remove(x) //分割隊列 ngx_queue_split(h, q, n) //鏈接隊列 ngx_queue_add(h, n) //獲取隊列的中間節點 ngx_queue_t *ngx_queue_middle(ngx_queue_t *queue) //排序隊列(穩定的插入排序) void ngx_queue_sort(ngx_queue_t *queue,ngx_int_t (*cmp)(const ngx_queue_t*, const ngx_queue_t*)) |
其中,插入節點、取隊列頭、取隊列尾等操作由宏實現,獲取中間節點、排序等操作由函數實現。
9.5.3.1. 初始化隊列
#define ngx_queue_init(q) \ (q)->prev = q; \ (q)->next = q |
9.5.3.2. 在頭節點之后插入
在頭節點之后插入操作由宏ngx_queue_insert_head完成,如下:
#define ngx_queue_insert_head(h, x) \ (x)->next = (h)->next; \ (x)->next->prev = x; \ (x)->prev = h; \ (h)->next = x |
畫出該操作的邏輯圖,如下:
圖中虛線表示被修改/刪除的指針,藍色表示新修改/增加的指針。
9.5.3.3. 在尾節點之后插入
在尾節點之后插入操作由宏ngx_queue_insert_tail完成,如下:
#define ngx_queue_insert_tail(h, x) \ (x)->prev = (h)->prev; \ (x)->prev->next = x; \ (x)->next = h; \ (h)->prev = x |
該操作的邏輯圖如下:
9.5.3.4. 刪除節點
在尾節點之后插入操作由宏ngx_queue_remove完成,如下:
#if (NGX_DEBUG) #define ngx_queue_remove(x) \ (x)->next->prev = (x)->prev; \ (x)->prev->next = (x)->next; \ (x)->prev = NULL; \ (x)->next = NULL #else #define ngx_queue_remove(x) \ (x)->next->prev = (x)->prev; \ (x)->prev->next = (x)->next #endif |
該操作的邏輯圖如下:
9.5.3.5. 分割隊列
分割隊列操作由宏ngx_queue_split完成,如下:
#define ngx_queue_split(h, q, n) \ (n)->prev = (h)->prev; \ (n)->prev->next = n; \ (n)->next = q; \ (h)->prev = (q)->prev; \ (h)->prev->next = h; \ (q)->prev = n; |
該宏有3個參數,h為隊列頭(即鏈表頭指針),將該隊列從q節點將隊列(鏈表)分割為兩個隊列(鏈表),q之后的節點組成的新隊列的頭節點為n,圖形演示如下:
9.5.3.6. 增加鏈接隊列
鏈接隊列由宏ngx_queue_add完成,操作如下:
#define ngx_queue_add(h, n) \ (h)->prev->next = (n)->next; \ (n)->next->prev = (h)->prev; \ (h)->prev = (n)->prev; \ (h)->prev->next = h; |
其中,h、n分別為兩個隊列的指針,即頭節點指針,該操作將n隊列鏈接在h隊列之后。演示圖形如下:
宏ngx_queue_split和ngx_queue_add只在http模塊locations相關操作中使用,在后續的討論http模塊locations相關操作時再詳細敘述。
9.5.3.7. 獲取中間節點
中間節點,若隊列有奇數個(除頭節點外)節點,則返回中間的節點;若隊列有偶數個節點,則返回后半個隊列的第一個節點。操作如下:
ngx_queue_t * ngx_queue_middle(ngx_queue_t *queue) { ngx_queue_t *middle, *next; middle = ngx_queue_head(queue); if (middle == ngx_queue_last(queue)) { return middle; } next = ngx_queue_head(queue); for ( ;; ) { middle = ngx_queue_next(middle); next = ngx_queue_next(next); if (next == ngx_queue_last(queue)) {//偶數個節點,在此返回后半個隊列的第一個節點 return middle; } next = ngx_queue_next(next); if (next == ngx_queue_last(queue)) {//奇數個節點,在此返回中間節點 return middle; } } } |
注意:代碼中的next指針,其每次均會后移兩個位置(節點),而middle指針每次后移一個位置(節點)。演示圖形如下:
9.5.3.8. 隊列排序
隊列排序采用的是穩定的簡單插入排序方法,即從第一個節點開始遍歷,依次將當前節點(q)插入前面已經排好序的隊列(鏈表)中,下面程序中,前面已經排好序的隊列的尾節點為prev。操作如下:
/* the stable insertion sort */ void ngx_queue_sort(ngx_queue_t *queue, ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *)) { ngx_queue_t *q, *prev, *next; q = ngx_queue_head(queue); if (q == ngx_queue_last(queue)) { return; } for (q = ngx_queue_next(q); q != ngx_queue_sentinel(queue); q = next) { prev = ngx_queue_prev(q); next = ngx_queue_next(q); ngx_queue_remove(q); do { if (cmp(prev, q) <= 0) { //比較 break; } prev = ngx_queue_prev(prev); //prev指針前移 } while (prev != ngx_queue_sentinel(queue)); ngx_queue_insert_after(prev, q); //將q插入prev節點之后(此處即為簡單插入) } } |
該排序操作使用前面介紹的宏來完成其插入動作,只是一些簡單的修改指針指向的操作,效率較高。
9.5.3.9. 如何獲取隊列節點數據
由隊列基本結構和以上操作可知,nginx的隊列操作只對鏈表指針進行簡單的修改指向操作,並不負責節點數據空間的分配。因此,用戶在使用nginx隊列時,要自己定義數據結構並分配空間,且在其中包含一個ngx_queue_t的指針或者對象,當需要獲取隊列節點數據時,使用ngx_queue_data宏,其定義如下:
#define ngx_queue_data(q, type, link) \ (type *) ((u_char *) q – offsetof(type, link)) |
由該宏定義可以看出,一般定義隊列節點結構(該結構類型為type)時,需要將真正的數據放在前面,而ngx_queue_t結構放在后面,故該宏使用減法計算整個節點結構的起始地址(需要進行類型轉換)。
數據結構如下:
9.5.4. 一個例子
給出一個創建內存池並從中分配隊列頭節點和其他節點組成隊列的簡單例子。在該例中,隊列的數據是一系列的二維點(x,y分別表示該點的橫、縱坐標),將這些點插入隊列后進行排序,以此向讀者展示nginx隊列的使用方法。
9.5.4.1. 代碼
/** * ngx_queue_t test */
#include <stdio.h> #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_palloc.h" #include "ngx_queue.h"
//2-dimensional point (x, y) queue structure typedef struct{ int x; int y; } my_point_t;
typedef struct{ my_point_t point; ngx_queue_t queue; } my_point_queue_t;
volatile ngx_cycle_t *ngx_cycle;
void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { }
void dump_pool(ngx_pool_t* pool) { while (pool){ printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } }
void dump_queue_from_head(ngx_queue_t *que) { ngx_queue_t *q = ngx_queue_head(que); printf("(0x%x: (0x%x, 0x%x)) <==> \n", que, que->prev, que->next); for (; q != ngx_queue_sentinel(que); q = ngx_queue_next(q)){ my_point_queue_t *point = ngx_queue_data(q, my_point_queue_t, queue); printf("(0x%x: (%-2d, %-2d), 0x%x: (0x%x, 0x%x)) <==> \n", point, point->point.x, point->point.y, &point->queue, point->queue.prev, point->queue.next); } }
void dump_queue_from_tail(ngx_queue_t *que) { ngx_queue_t *q = ngx_queue_last(que); printf("(0x%x: (0x%x, 0x%x)) <==> \n", que, que->prev, que->next); for (; q != ngx_queue_sentinel(que); q = ngx_queue_prev(q)) { my_point_queue_t *point = ngx_queue_data(q, my_point_queue_t, queue); printf("(0x%x: (%-2d, %-2d), 0x%x: (0x%x, 0x%x)) <==> \n", point, point->point.x, point->point.y, &point->queue, point->queue.prev, point->queue.next); } }
//sort from small to big ngx_int_t my_point_cmp(const ngx_queue_t* lhs, const ngx_queue_t* rhs) { my_point_queue_t *pt1 = ngx_queue_data(lhs, my_point_queue_t, queue); my_point_queue_t *pt2 = ngx_queue_data(rhs, my_point_queue_t, queue); if (pt1->point.x < pt2->point.x) return 0; else if (pt1->point.x > pt2->point.x) return 1; else if (pt1->point.y < pt2->point.y) return 0; else if (pt1->point.y > pt2->point.y) return 1; return 1; }
#define Max_Num 6
int main() { ngx_pool_t *pool; ngx_queue_t *myque; my_point_queue_t *point; my_point_t points[Max_Num] = { {10, 1}, {20, 9}, {9, 9}, {90, 80}, {5, 3}, {50, 20} }; int i;
printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool);
printf("--------------------------------\n"); printf("alloc a queue head and nodes :\n"); printf("--------------------------------\n"); myque = ngx_palloc(pool, sizeof(ngx_queue_t)); //alloc a queue head ngx_queue_init(myque); //init the queue
//insert some points into the queue for (i = 0; i < Max_Num; i++) { point = (my_point_queue_t*)ngx_palloc(pool, sizeof(my_point_queue_t)); point->point.x = points[i].x; point->point.y = points[i].y; ngx_queue_init(&point->queue);
//insert this point into the points queue ngx_queue_insert_head(myque, &point->queue); }
dump_queue_from_tail(myque); printf("\n");
printf("--------------------------------\n"); printf("sort the queue:\n"); printf("--------------------------------\n"); ngx_queue_sort(myque, my_point_cmp); dump_queue_from_head(myque); printf("\n");
printf("--------------------------------\n"); printf("the pool at the end:\n"); printf("--------------------------------\n"); dump_pool(pool);
ngx_destroy_pool(pool); return 0; } |
9.5.4.2. 10.4.2、如何編譯
CXX = gcc CXXFLAGS += -g -Wall -Wextra NGX_ROOT = /usr/src/nginx-1.0.4 TARGETS = ngx_queue_t_test TARGETS_C_FILE = $(TARGETS).c CLEANUP = rm -f $(TARGETS) *.o all: $(TARGETS) clean: $(CLEANUP) CORE_INCS = -I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \ NGX_PALLOC = $(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING = $(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC = $(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_QUEUE = $(NGX_ROOT)/objs/src/core/ngx_queue.o $(TARGETS): $(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $(NGX_QUEUE) $^ -o $@ |
9.5.4.3. 10.4.3、運行結果
# ./ngx_queue_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8bcf020 .d .last = 0x8bcf048 .end = 0x8bcf420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8bcf020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984
-------------------------------- alloc a queue head and nodes : -------------------------------- (0x8bcf048: (0x8bcf058, 0x8bcf0a8)) <==> (0x8bcf050: (10, 1 ), 0x8bcf058: (0x8bcf068, 0x8bcf048)) <==> (0x8bcf060: (20, 9 ), 0x8bcf068: (0x8bcf078, 0x8bcf058)) <==> (0x8bcf070: (9 , 9 ), 0x8bcf078: (0x8bcf088, 0x8bcf068)) <==> (0x8bcf080: (90, 80), 0x8bcf088: (0x8bcf098, 0x8bcf078)) <==> (0x8bcf090: (5 , 3 ), 0x8bcf098: (0x8bcf0a8, 0x8bcf088)) <==> (0x8bcf0a0: (50, 20), 0x8bcf0a8: (0x8bcf048, 0x8bcf098)) <==>
-------------------------------- sort the queue: -------------------------------- (0x8bcf048: (0x8bcf088, 0x8bcf098)) <==> (0x8bcf090: (5 , 3 ), 0x8bcf098: (0x8bcf048, 0x8bcf078)) <==> (0x8bcf070: (9 , 9 ), 0x8bcf078: (0x8bcf098, 0x8bcf058)) <==> (0x8bcf050: (10, 1 ), 0x8bcf058: (0x8bcf078, 0x8bcf068)) <==> (0x8bcf060: (20, 9 ), 0x8bcf068: (0x8bcf058, 0x8bcf0a8)) <==> (0x8bcf0a0: (50, 20), 0x8bcf0a8: (0x8bcf068, 0x8bcf088)) <==> (0x8bcf080: (90, 80), 0x8bcf088: (0x8bcf0a8, 0x8bcf048)) <==>
-------------------------------- the pool at the end: -------------------------------- pool = 0x8bcf020 .d .last = 0x8bcf0b0 .end = 0x8bcf420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8bcf020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 880 |
該隊列的邏輯圖如下:
9.5.5. 10.5、小結
本文針對nginx-1.0.4的隊列進行了較為全面的分析,包括隊列結構和隊列操作,隊列操作主要包括在頭節點之后插入節點、在尾節點之后插入節點、獲取中間節點、隊列排序等。最后通過一個簡單例子向讀者展示nginx隊列的使用方法,同時借此向讀者展示編譯測試nginx代碼的方法。
9.6. hash結構ngx_hash_t
9.6.1. 簡介
鏈表實現文件:文件:src/core/ngx_hash.h/.c。
9.6.2. hash結構
nginx的hash結構比其list、array、queue等結構稍微復雜一些。
9.6.2.1. ngx_hash_t結構
nginx的hash結構為ngx_hash_t,hash元素結構為ngx_hash_elt_t,定義如下:
typedef struct { //hash元素結構 void *value; //value,即某個key對應的值,即<key,value>中的value u_short len; //name長度 u_char name[1]; //要hash的數據(在nginx中表現為字符串),即<key,value>中的key } ngx_hash_elt_t;
typedef struct { //hash結構 ngx_hash_elt_t **buckets; //hash桶(有size個桶) ngx_uint_t size; //hash桶個數 } ngx_hash_t; |
其中,sizeof(ngx_hash_t) = 8,sizeof(ngx_hash_elt_t) = 8。實際上,ngx_hash_elt_t結構中的name字段就是ngx_hash_key_t結構中的key。這在ngx_hash_init()函數中可以看到,請參考后續的分析。該結構在模塊配置解析時經常使用。
9.6.2.2. ngx_hash_init_t結構
nginx的hash初始化結構是ngx_hash_init_t,用來將其相關數據封裝起來作為參數傳遞給ngx_hash_init()或ngx_hash_wildcard_init()函數。這兩個函數主要是在http相關模塊中使用,例如ngx_http_server_names()函數(優化http Server Names),ngx_http_merge_types()函數(合並httptype),ngx_http_fastcgi_merge_loc_conf()函數(合並FastCGI Location Configuration)等函數或過程用到的參數、局部對象/變量等。這些內容將在后續的文章中講述。
ngx_hash_init_t結構如下。sizeof(ngx_hash_init_t)=28。
typedef struct { //hash初始化結構 ngx_hash_t *hash; //指向待初始化的hash結構 ngx_hash_key_pt key; //hash函數指針
ngx_uint_t max_size; //bucket的最大個數 ngx_uint_t bucket_size; //每個bucket的空間
char *name; //該hash結構的名字(僅在錯誤日志中使用) ngx_pool_t *pool; //該hash結構從pool指向的內存池中分配 ngx_pool_t *temp_pool; //分配臨時數據空間的內存池 } ngx_hash_init_t; |
9.6.2.3. ngx_hash_key_t結構
該結構也主要用來保存要hash的數據,即鍵-值對<key,value>,在實際使用中,一般將多個鍵-值對保存在ngx_hash_key_t結構的數組中,作為參數傳給ngx_hash_init()或ngx_hash_wildcard_init()函數。其定義如下:
typedef struct { //hash key結構 ngx_str_t key; //key,為nginx的字符串結構 ngx_uint_t key_hash; //由該key計算出的hash值(通過hash函數如ngx_hash_key_lc()) void *value; //該key對應的值,組成一個鍵-值對<key,value> } ngx_hash_key_t;
typedef struct { //字符串結構 size_t len; //字符串長度 u_char *data; //字符串內容 } ngx_str_t; |
其中,sizeof(ngx_hash_key_t) = 16。一般在使用中,value指針可能指向靜態數據區(例如全局數組、常量字符串)、堆區(例如動態分配的數據區用來保存value值)等。可參考本文后面的例子。
關於ngx_table_elt_t結構和ngx_hash_keys_arrays_t結構,因其對於hash結構本身沒有太大作用,主要是為模塊配置、referer合法性驗證等設計的數據結構,例如http的core模塊、map模塊、referer模塊、SSI filter模塊等,此處不再講述,將在后續的文章中介紹。
9.6.2.4. hash的邏輯結構
ngx_hash_init_t結構引用了ngx_pool_t結構,因此本文參考內存池結構ngx_pool_t及內存管理一文畫出相關結構的邏輯圖,如下:
9.6.3. hash操作
9.6.3.1. NGX_HASH_ELT_SIZE宏
NGX_HASH_ELT_SIZE宏用來計算上述ngx_hash_elt_t結構大小,定義如下:
#define NGX_HASH_ELT_SIZE(name) //該參數name即為ngx_hash_elt_t結構指針 (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *))) //以4字節對齊 |
在32位平台上,sizeof(void*)=4,(name)->key.len即是ngx_hash_elt_t結構中name數組保存的內容的長度,其中的"+2"是要加上該結構中len字段(u_short類型)的大小。
因此,NGX_HASH_ELT_SIZE(name)=4+ngx_align((name)->key.len + 2, 4),該式后半部分即是(name)->key.len+2以4字節對齊的大小。
9.6.3.2. hash函數
nginx提供的hash函數有以下幾種:
#define ngx_hash(key, c) ((ngx_uint_t) key * 31 + c) //hash宏 ngx_uint_t ngx_hash_key(u_char *data, size_t len); ngx_uint_t ngx_hash_key_lc(u_char *data, size_t len); //lc表示lower case,即字符串轉換為小寫后再計算hash值 ngx_uint_t ngx_hash_strlow(u_char *dst, u_char *src, size_t n); |
hash函數都很簡單,以上3個函數都會調用ngx_hash宏,該宏返回一個(長)整數。此處介紹第一個函數,定義如下:
ngx_uint_t ngx_hash_key(u_char *data, size_t len) { ngx_uint_t i, key; key = 0; for (i = 0; i < len; i++) { key = ngx_hash(key, data[i]); } return key; } |
因此,ngx_hash_key函數的計算可表述為下列公式。
Key[0] = data[0] Key[1] = data[0]*31 + data[1] Key[2] = (data[0]*31 + data[1])*31 + data[2] ... Key[len-1] = ((((data[0]*31 + data[1])*31 + data[2])*31) ... data[len-2])*31 + data[len-1] |
key[len-1]即為傳入的參數data對應的hash值。
9.6.3.3. hash初始化
hash初始化由ngx_hash_init()函數完成,其names參數是ngx_hash_key_t結構的數組,即鍵-值對<key,value>數組,nelts表示該數組元素的個數。因此,在調用該函數進行初始化之前,ngx_hash_key_t結構的數組是准備好的,如何使用,可以采用nginx的ngx_array_t結構,詳見本文后面的例子。
該函數初始化的結果就是將names數組保存的鍵-值對<key,value>,通過hash的方式將其存入相應的一個或多個hash桶(即代碼中的buckets)中,該hash過程用到的hash函數一般為ngx_hash_key_lc等。hash桶里面存放的是ngx_hash_elt_t結構的指針(hash元素指針),該指針指向一個基本連續的數據區。該數據區中存放的是經hash之后的鍵-值對<key',value'>,即ngx_hash_elt_t結構中的字段<name,value>。每一個這樣的數據區存放的鍵-值對<key',value'>可以是一個或多個。
此處有幾個問題需要說明。
問題1:為什么說是基本連續?
——用NGX_HASH_ELT_SIZE宏計算某個hash元素的總長度時,存在以sizeof(void*)對齊的填補(padding)。因此將names數組中的鍵-值對<key,value>中的key拷貝到ngx_hash_elt_t結構的name[1]數組中時,已經為該hash元素分配的空間不會完全被用完,故這個數據區是基本連續的。這一點也可以參考本節后面的結構圖或本文后面的例子。
問題2:這些基本連續的數據區從哪里分配的?
——當然是從該函數的第一個參數ngx_hash_init_t的pool字段指向的內存池中分配的。
問題3:<key',value'>與<key,value>不同的是什么?
——key保存的僅僅是個指針,而key'卻是key拷貝到name[1]的結果。而value和value'都是指針。如1.3節說明,value指針可能指向靜態數據區(例如全局數組、常量字符串)、堆區(例如動態分配的數據區用來保存value值)等。可參考本文后面的例子。
問題4:如何知道某個鍵-值對<key,value>放在哪個hash桶中?
——key = names[n].key_hash % size; 代碼中的這個計算是也。計算結果key即是該鍵要放在那個hash桶的編號(從0到size-1)。
該函數代碼如下。一些疑點、難點的解釋請參考//后筆者所加的注釋,也可參考本節的hash結構圖。
//nelts是names數組中(實際)元素的個數 ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts) { u_char *elts; size_t len; u_short *test; ngx_uint_t i, n, key, size, start, bucket_size; ngx_hash_elt_t *elt, **buckets;
for (n = 0; n < nelts; n++) { //檢查names數組的每一個元素,判斷桶的大小是否夠分配 if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *)) { //有任何一個元素,桶的大小不夠為該元素分配空間,則退出 ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0, "could not build the %s, you should " "increase %s_bucket_size: %i", hinit->name, hinit->name, hinit->bucket_size); return NGX_ERROR; } }
//分配2*max_size個字節的空間保存hash數據(該內存分配操作不在nginx的內存池中進行,因為test只是臨時的) test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log); if (test == NULL) { return NGX_ERROR; }
bucket_size = hinit->bucket_size - sizeof(void *); //一般sizeof(void*)=4
start = nelts / (bucket_size / (2 * sizeof(void *))); // start = start ? start : 1;
if (hinit->max_size > 10000 && hinit->max_size / nelts < 100) { start = hinit->max_size - 1000; }
for (size = start; size < hinit->max_size; size++) {
ngx_memzero(test, size * sizeof(u_short));
//標記1:此塊代碼是檢查bucket大小是否夠分配hash數據 for (n = 0; n < nelts; n++) { if (names[n].key.data == NULL) { continue; }
//計算key和names中所有name長度,並保存在test[key]中 key = names[n].key_hash % size; //若size=1,則key一直為0 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; }
//若沒有找到合適的bucket,退出 ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0, "could not build the %s, you should increase " "either %s_max_size: %i or %s_bucket_size: %i", hinit->name, hinit->name, hinit->max_size, hinit->name, hinit->bucket_size);
ngx_free(test);
return NGX_ERROR;
found: //找到合適的bucket
for (i = 0; i < size; i++) { //將test數組前size個元素初始化為4 test[i] = sizeof(void *); }
/** 標記2:與標記1代碼基本相同,但此塊代碼是再次計算所有hash數據的總長度(標記1的檢查已通過) 但此處的test[i]已被初始化為4,即相當於后續的計算再加上一個void指針的大小。 */ for (n = 0; n < nelts; n++) { if (names[n].key.data == NULL) { continue; }
//計算key和names中所有name長度,並保存在test[key]中 key = names[n].key_hash % size; //若size=1,則key一直為0 test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n])); }
//計算hash數據的總長度 len = 0;
for (i = 0; i < size; i++) { if (test[i] == sizeof(void *)) {//若test[i]仍為初始化的值4,即沒有變化,則繼續 continue; }
//對test[i]按ngx_cacheline_size對齊(32位平台,ngx_cacheline_size=32) test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));
len += test[i]; }
if (hinit->hash == NULL) {//在內存池中分配hash頭及buckets數組(size個ngx_hash_elt_t*結構) hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t) + size * sizeof(ngx_hash_elt_t *)); if (hinit->hash == NULL) { ngx_free(test); return NGX_ERROR; }
//計算buckets的啟示位置(在ngx_hash_wildcard_t結構之后) buckets = (ngx_hash_elt_t **) ((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));
} else { //在內存池中分配buckets數組(size個ngx_hash_elt_t*結構) buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *)); if (buckets == NULL) { ngx_free(test); return NGX_ERROR; } }
//接着分配elts,大小為len+ngx_cacheline_size,此處為什么+32?——下面要按32字節對齊 elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size); if (elts == NULL) { ngx_free(test); return NGX_ERROR; }
//將elts地址按ngx_cacheline_size=32對齊 elts = ngx_align_ptr(elts, ngx_cacheline_size);
for (i = 0; i < size; i++) { //將buckets數組與相應elts對應起來 if (test[i] == sizeof(void *)) { continue; }
buckets[i] = (ngx_hash_elt_t *) elts; elts += test[i];
}
for (i = 0; i < size; i++) { //test數組置0 test[i] = 0; }
for (n = 0; n < nelts; n++) { //將傳進來的每一個hash數據存入hash表 if (names[n].key.data == NULL) { continue; }
//計算key,即將被hash的數據在第幾個bucket,並計算其對應的elts位置 key = names[n].key_hash % size; elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);
//對ngx_hash_elt_t結構賦值 elt->value = names[n].value; elt->len = (u_short) names[n].key.len;
ngx_strlow(elt->name, names[n].key.data, names[n].key.len);
//計算下一個要被hash的數據的長度偏移 test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n])); }
for (i = 0; i < size; i++) { if (buckets[i] == NULL) { continue; }
//test[i]相當於所有被hash的數據總長度 elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);
elt->value = NULL; }
ngx_free(test); //釋放該臨時空間
hinit->hash->buckets = buckets; hinit->hash->size = size;
return NGX_OK; } |
所謂的hash數據長度即指ngx_hash_elt_t結構被賦值后的長度。nelts個元素存放在names數組中,調用該函數對hash進行初始化之后,這nelts個元素被保存在size個hash桶指向的ngx_hash_elts_t數據區,這些數據區中共保存了nelts個hash元素。即hash桶(buckets)存放的是ngx_hash_elt_t數據區的起始地址,以該起始地址開始的數據區存放的是經hash之后的hash元素,每個hash元素的最后是以name[0]為開始的字符串,該字符串就是names數組中某個元素的key,即鍵值對<key,value>中的key,然后該字符串之后會有幾個字節的因對齊產生的padding。
一個典型的經初始化后的hash物理結構如下。具體的可參考后文的例子。
9.6.3.4. hash查找
hash查找操作由ngx_hash_find()函數完成,代碼如下:
//由key,name,len信息在hash指向的hash table中查找該key對應的value void * ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len) { ngx_uint_t i; ngx_hash_elt_t *elt;
elt = hash->buckets[key % hash->size];//由key找到所在的bucket(該bucket中保存其elts地址)
if (elt == NULL) { return NULL; }
while (elt->value) { if (len != (size_t) elt->len) { //先判斷長度 goto next; }
for (i = 0; i < len; i++) { if (name[i] != elt->name[i]) { //接着比較name的內容(此處按字符匹配) goto next; } }
return elt->value; //匹配成功,直接返回該ngx_hash_elt_t結構的value字段
next: //注意此處從elt->name[0]地址處向后偏移,故偏移只需加該elt的len即可,然后在以4字節對齊 elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len, sizeof(void *)); continue; }
return NULL; } |
查找操作相當簡單,由key直接計算所在的bucket,該bucket中保存其所在ngx_hash_elt_t數據區的起始地址;然后根據長度判斷並用name內容匹配,匹配成功,其ngx_hash_elt_t結構的value字段即是所求。
9.6.4. 一個例子
本節給出一個創建內存池並從中分配hash結構、hash桶、hash元素並將鍵-值對<key,value>加入該hash結構的簡單例子。
在該例中,將完成這樣一個應用,將給定的多個url及其ip組成的二元組<url,ip>作為<key,value>,初始化時對這些<url,ip>進行hash,然后根據給定的url查找其對應的ip地址,若沒有找到,則給出相關提示信息。以此向讀者展示nginx的hash使用方法。
9.6.4.1. 代碼
/** * ngx_hash_t test * in this example, it will first save URLs into the memory pool, and IPs saved in static memory. * then, give some examples to find IP according to a URL. */
#include <stdio.h> #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_string.h" #include "ngx_palloc.h" #include "ngx_array.h" #include "ngx_hash.h"
#define Max_Num 7 #define Max_Size 1024 #define Bucket_Size 64 //256, 64
#define NGX_HASH_ELT_SIZE(name) \ (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
/* for hash test */ static ngx_str_t urls[Max_Num] = { ngx_string("www.baidu.com"), //220.181.111.147 ngx_string("www.sina.com.cn"), //58.63.236.35 ngx_string("www.google.com"), //74.125.71.105 ngx_string("www.qq.com"), //60.28.14.190 ngx_string("www.163.com"), //123.103.14.237 ngx_string("www.sohu.com"), //219.234.82.50 ngx_string("www.abo321.org") //117.40.196.26 };
static char* values[Max_Num] = { "220.181.111.147", "58.63.236.35", "74.125.71.105", "60.28.14.190", "123.103.14.237", "219.234.82.50", "117.40.196.26" };
#define Max_Url_Len 15 #define Max_Ip_Len 15
#define Max_Num2 2
/* for finding test */ static ngx_str_t urls2[Max_Num2] = { ngx_string("www.china.com"), //60.217.58.79 ngx_string("www.csdn.net") //117.79.157.242 };
ngx_hash_t* init_hash(ngx_pool_t *pool, ngx_array_t *array); void dump_pool(ngx_pool_t* pool); void dump_hash_array(ngx_array_t* a); void dump_hash(ngx_hash_t *hash, ngx_array_t *array); ngx_array_t* add_urls_to_array(ngx_pool_t *pool); void find_test(ngx_hash_t *hash, ngx_str_t addr[], int num);
/* for passing compiling */ volatile ngx_cycle_t *ngx_cycle; void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { }
int main(/* int argc, char **argv */) { ngx_pool_t *pool = NULL; ngx_array_t *array = NULL; ngx_hash_t *hash;
printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL);
dump_pool(pool);
printf("--------------------------------\n"); printf("create and add urls to it:\n"); printf("--------------------------------\n"); array = add_urls_to_array(pool); //in fact, here should validate array dump_hash_array(array);
printf("--------------------------------\n"); printf("the pool:\n"); printf("--------------------------------\n"); dump_pool(pool);
hash = init_hash(pool, array); if (hash == NULL) { printf("Failed to initialize hash!\n"); return -1; }
printf("--------------------------------\n"); printf("the hash:\n"); printf("--------------------------------\n"); dump_hash(hash, array); printf("\n");
printf("--------------------------------\n"); printf("the pool:\n"); printf("--------------------------------\n"); dump_pool(pool);
//find test printf("--------------------------------\n"); printf("find test:\n"); printf("--------------------------------\n"); find_test(hash, urls, Max_Num); printf("\n");
find_test(hash, urls2, Max_Num2);
//release ngx_array_destroy(array); ngx_destroy_pool(pool);
return 0; }
ngx_hash_t* init_hash(ngx_pool_t *pool, ngx_array_t *array) { ngx_int_t result; ngx_hash_init_t hinit;
ngx_cacheline_size = 32; //here this variable for nginx must be defined hinit.hash = NULL; //if hinit.hash is NULL, it will alloc memory for it in ngx_hash_init hinit.key = &ngx_hash_key_lc; //hash function hinit.max_size = Max_Size; hinit.bucket_size = Bucket_Size; hinit.name = "my_hash_sample"; hinit.pool = pool; //the hash table exists in the memory pool hinit.temp_pool = NULL;
result = ngx_hash_init(&hinit, (ngx_hash_key_t*)array->elts, array->nelts); if (result != NGX_OK) return NULL;
return hinit.hash; }
void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } }
void dump_hash_array(ngx_array_t* a) { char prefix[] = " ";
if (a == NULL) return;
printf("array = 0x%x\n", a); printf(" .elts = 0x%x\n", a->elts); printf(" .nelts = %d\n", a->nelts); printf(" .size = %d\n", a->size); printf(" .nalloc = %d\n", a->nalloc); printf(" .pool = 0x%x\n", a->pool);
printf(" elements:\n"); ngx_hash_key_t *ptr = (ngx_hash_key_t*)(a->elts); for (; ptr < (ngx_hash_key_t*)(a->elts + a->nalloc * a->size); ptr++) { printf(" 0x%x: {key = (\"%s\"%.*s, %d), key_hash = %-10ld, value = \"%s\"%.*s}\n", ptr, ptr->key.data, Max_Url_Len - ptr->key.len, prefix, ptr->key.len, ptr->key_hash, ptr->value, Max_Ip_Len - strlen(ptr->value), prefix); } printf("\n"); }
/** * pass array pointer to read elts[i].key_hash, then for getting the position - key */ void dump_hash(ngx_hash_t *hash, ngx_array_t *array) { int loop; char prefix[] = " "; u_short test[Max_Num] = {0}; ngx_uint_t key; ngx_hash_key_t* elts; int nelts;
if (hash == NULL) return;
printf("hash = 0x%x: **buckets = 0x%x, size = %d\n", hash, hash->buckets, hash->size);
for (loop = 0; loop < hash->size; loop++) { ngx_hash_elt_t *elt = hash->buckets[loop]; printf(" 0x%x: buckets[%d] = 0x%x\n", &(hash->buckets[loop]), loop, elt); } printf("\n");
elts = (ngx_hash_key_t*)array->elts; nelts = array->nelts; for (loop = 0; loop < nelts; loop++) { char url[Max_Url_Len + 1] = {0};
key = elts[loop].key_hash % hash->size; ngx_hash_elt_t *elt = (ngx_hash_elt_t *) ((u_char *) hash->buckets[key] + test[key]);
ngx_strlow(url, elt->name, elt->len); printf(" buckets %d: 0x%x: {value = \"%s\"%.*s, len = %d, name = \"%s\"%.*s}\n", key, elt, (char*)elt->value, Max_Ip_Len - strlen((char*)elt->value), prefix, elt->len, url, Max_Url_Len - elt->len, prefix); //replace elt->name with url
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&elts[loop])); } }
ngx_array_t* add_urls_to_array(ngx_pool_t *pool) { int loop; char prefix[] = " "; ngx_array_t *a = ngx_array_create(pool, Max_Num, sizeof(ngx_hash_key_t));
for (loop = 0; loop < Max_Num; loop++) { ngx_hash_key_t *hashkey = (ngx_hash_key_t*)ngx_array_push(a); hashkey->key = urls[loop]; hashkey->key_hash = ngx_hash_key_lc(urls[loop].data, urls[loop].len); hashkey->value = (void*)values[loop]; /** for debug printf("{key = (\"%s\"%.*s, %d), key_hash = %-10ld, value = \"%s\"%.*s}, added to array\n", hashkey->key.data, Max_Url_Len - hashkey->key.len, prefix, hashkey->key.len, hashkey->key_hash, hashkey->value, Max_Ip_Len - strlen(hashkey->value), prefix); */ }
return a; }
void find_test(ngx_hash_t *hash, ngx_str_t addr[], int num) { ngx_uint_t key; int loop; char prefix[] = " ";
for (loop = 0; loop < num; loop++) { key = ngx_hash_key_lc(addr[loop].data, addr[loop].len); void *value = ngx_hash_find(hash, key, addr[loop].data, addr[loop].len); if (value) { printf("(url = \"%s\"%.*s, key = %-10ld) found, (ip = \"%s\")\n", addr[loop].data, Max_Url_Len - addr[loop].len, prefix, key, (char*)value); } else { printf("(url = \"%s\"%.*s, key = %-10d) not found!\n", addr[loop].data, Max_Url_Len - addr[loop].len, prefix, key); } } } |
9.6.4.2. 如何編譯
CXX = gcc CXXFLAGS += -g -Wall -Wextra
NGX_ROOT = /usr/src/nginx-1.0.4
TARGETS = ngx_hash_t_test TARGETS_C_FILE = $(TARGETS).c
CLEANUP = rm -f $(TARGETS) *.o
all: $(TARGETS)
clean: $(CLEANUP)
CORE_INCS = -I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \
NGX_PALLOC = $(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING = $(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC = $(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_ARRAY = $(NGX_ROOT)/objs/src/core/ngx_array.o NGX_HASH = $(NGX_ROOT)/objs/src/core/ngx_hash.o
$(TARGETS): $(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $(NGX_ARRAY) $(NGX_HASH) $^ -o $@ |
9.6.4.3. 運行結果
bucket_size=64字節:運行結果如下:
# ./ngx_hash_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8870020 .d .last = 0x8870048 .end = 0x8870420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8870020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984
-------------------------------- create and add urls to it: -------------------------------- array = 0x8870048 .elts = 0x887005c .nelts = 7 .size = 16 .nalloc = 7 .pool = 0x8870020 elements: 0x887005c: {key = ("www.baidu.com" , 13), key_hash = 270263191 , value = "220.181.111.147"} 0x887006c: {key = ("www.sina.com.cn", 15), key_hash = 1528635686, value = "58.63.236.35" } 0x887007c: {key = ("www.google.com" , 14), key_hash = -702889725, value = "74.125.71.105" } 0x887008c: {key = ("www.qq.com" , 10), key_hash = 203430122 , value = "60.28.14.190" } 0x887009c: {key = ("www.163.com" , 11), key_hash = -640386838, value = "123.103.14.237" } 0x88700ac: {key = ("www.sohu.com" , 12), key_hash = 1313636595, value = "219.234.82.50" } 0x88700bc: {key = ("www.abo321.org" , 14), key_hash = 1884209457, value = "117.40.196.26" }
-------------------------------- the pool: -------------------------------- pool = 0x8870020 .d .last = 0x88700cc .end = 0x8870420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8870020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 852
-------------------------------- the hash: -------------------------------- hash = 0x88700cc: **buckets = 0x88700d8, size = 3 0x88700d8: buckets[0] = 0x8870100 0x88700dc: buckets[1] = 0x8870140 0x88700e0: buckets[2] = 0x8870180
buckets 1: 0x8870140: {value = "220.181.111.147", len = 13, name = "www.baidu.com" } buckets 2: 0x8870180: {value = "58.63.236.35" , len = 15, name = "www.sina.com.cn"} buckets 1: 0x8870154: {value = "74.125.71.105" , len = 14, name = "www.google.com" } buckets 2: 0x8870198: {value = "60.28.14.190" , len = 10, name = "www.qq.com" } buckets 0: 0x8870100: {value = "123.103.14.237" , len = 11, name = "www.163.com" } buckets 0: 0x8870114: {value = "219.234.82.50" , len = 12, name = "www.sohu.com" } buckets 0: 0x8870128: {value = "117.40.196.26" , len = 14, name = "www.abo321.org" }
-------------------------------- the pool: -------------------------------- pool = 0x8870020 .d .last = 0x88701c4 .end = 0x8870420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8870020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 604
-------------------------------- find test: -------------------------------- (url = "www.baidu.com" , key = 270263191 ) found, (ip = "220.181.111.147") (url = "www.sina.com.cn", key = 1528635686) found, (ip = "58.63.236.35") (url = "www.google.com" , key = -702889725) found, (ip = "74.125.71.105") (url = "www.qq.com" , key = 203430122 ) found, (ip = "60.28.14.190") (url = "www.163.com" , key = -640386838) found, (ip = "123.103.14.237") (url = "www.sohu.com" , key = 1313636595) found, (ip = "219.234.82.50") (url = "www.abo321.org" , key = 1884209457) found, (ip = "117.40.196.26")
(url = "www.china.com" , key = -1954599725) not found! (url = "www.csdn.net" , key = -1667448544) not found! |
以上結果是bucket_size=64字節的輸出。由該結果可以看出,對於給定的7個url,程序將其分到了3個bucket中,詳見該結果。該例子的hash物理結構圖如下:
bucket_size=256字節:運行結果如下:
# ./ngx_hash_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8b74020 .d .last = 0x8b74048 .end = 0x8b74420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8b74020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984
-------------------------------- create and add urls to it: -------------------------------- array = 0x8b74048 .elts = 0x8b7405c .nelts = 7 .size = 16 .nalloc = 7 .pool = 0x8b74020 elements: 0x8b7405c: {key = ("www.baidu.com" , 13), key_hash = 270263191 , value = "220.181.111.147"} 0x8b7406c: {key = ("www.sina.com.cn", 15), key_hash = 1528635686, value = "58.63.236.35" } 0x8b7407c: {key = ("www.google.com" , 14), key_hash = -702889725, value = "74.125.71.105" } 0x8b7408c: {key = ("www.qq.com" , 10), key_hash = 203430122 , value = "60.28.14.190" } 0x8b7409c: {key = ("www.163.com" , 11), key_hash = -640386838, value = "123.103.14.237" } 0x8b740ac: {key = ("www.sohu.com" , 12), key_hash = 1313636595, value = "219.234.82.50" } 0x8b740bc: {key = ("www.abo321.org" , 14), key_hash = 1884209457, value = "117.40.196.26" }
-------------------------------- the pool: -------------------------------- pool = 0x8b74020 .d .last = 0x8b740cc .end = 0x8b74420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8b74020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 852
-------------------------------- the hash: -------------------------------- hash = 0x8b740cc: **buckets = 0x8b740d8, size = 1 0x8b740d8: buckets[0] = 0x8b740e0
buckets 0: {value = "220.181.111.147", len = 13, name = "www.baidu.com" } buckets 0: {value = "58.63.236.35" , len = 15, name = "www.sina.com.cn"} buckets 0: {value = "74.125.71.105" , len = 14, name = "www.google.com" } buckets 0: {value = "60.28.14.190" , len = 10, name = "www.qq.com" } buckets 0: {value = "123.103.14.237" , len = 11, name = "www.163.com" } buckets 0: {value = "219.234.82.50" , len = 12, name = "www.sohu.com" } buckets 0: {value = "117.40.196.26" , len = 14, name = "www.abo321.org" }
-------------------------------- the pool: -------------------------------- pool = 0x8b74020 .d .last = 0x8b7419c .end = 0x8b74420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8b74020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 644
-------------------------------- find test: -------------------------------- (url = "www.baidu.com" , key = 270263191 ) found, (ip = "220.181.111.147") (url = "www.sina.com.cn", key = 1528635686) found, (ip = "58.63.236.35") (url = "www.google.com" , key = -702889725) found, (ip = "74.125.71.105") (url = "www.qq.com" , key = 203430122 ) found, (ip = "60.28.14.190") (url = "www.163.com" , key = -640386838) found, (ip = "123.103.14.237") (url = "www.sohu.com" , key = 1313636595) found, (ip = "219.234.82.50") (url = "www.abo321.org" , key = 1884209457) found, (ip = "117.40.196.26")
(url = "www.china.com" , key = -1954599725) not found! (url = "www.csdn.net" , key = -1667448544) not found! |
以上結果是bucket_size=256字節的輸出。由給結果可以看出,對於給定的7個url,程序將其放到了1個bucket中,即ngx_hash_init()函數中的size=1,因這7個url的總長度只有140,因此,只需size=1個bucket,即buckets[0]。
下表是ngx_hash_init()函數在計算過程中的一些數據。物理結構圖省略,可參考上圖。
url |
計算長度 |
test[0]的值 |
4+ngx_align(13+2,4)=20 |
20 |
|
4+ngx_align(15+2,4)=24 |
44 |
|
4+ngx_align(14+2,4)=20 |
64 |
|
4+ngx_align(10+2,4)=16 |
80 |
|
4+ngx_align(11+2,4)=20 |
100 |
|
4+ngx_align(12+2,4)=20 |
120 |
|
4+ngx_align(14+2,4)=20 |
140 |
9.6.5. 小結
本文針對nginx的hash結構進行了較為全面的分析,包括hash結構、hash元素結構、hash初始化結構等,hash操作主要包括hash初始化、hash查找等。最后通過一個簡單例子向讀者展示nginx的hash使用方法,並給出詳細的運行結果,且畫出hash的物理結構圖,以此向圖這展示hash的設計、原理;同時借此向讀者展示編譯測試nginx代碼的方法。
10. Nginx模塊開發
下面本文展示一個簡單的Nginx模塊開發全過程,我們開發一個叫echo的handler模塊,這個模塊功能非常簡單,它接收“echo”指令,指令可指定一個字符串參數,模塊會輸出這個字符串作為HTTP響應。例如,做如下配置:
location /echo { echo "hello nginx"; } |
則訪問http://hostname/echo時會輸出hello nginx。
直觀來看,要實現這個功能需要三步:1、讀入配置文件中echo指令及其參數;2、進行HTTP包裝(添加HTTP頭等工作);3、將結果返回給客戶端。下面本文將分部介紹整個模塊的開發過程。
10.1. 模塊配置結構
首先我們需要一個結構用於存儲從配置文件中讀進來的相關指令參數,即模塊配置信息結構。根據Nginx模塊開發規則,這個結構的命名規則為ngx_http_[module-name]_[main|srv|loc]_conf_t。其中main、srv和loc分別用於表示同一模塊在三層block中的配置信息。
這里我們的echo模塊,只需要運行在loc層級下,需要存儲一個字符串參數,因此我們可以定義如下的模塊配置:
typedef struct { ngx_str_t ed; } ngx_http_echo_loc_conf_t; |
其中字段ed用於存儲echo指令指定的需要輸出的字符串。注意這里ed的類型,在Nginx模塊開發中使用ngx_str_t類型表示字符串,這個類型定義在core/ngx_string中:
typedef struct { size_t len; u_char *data; } ngx_str_t; |
其中兩個字段分別表示字符串的長度和數據起始地址。注意在Nginx源代碼中對數據類型進行了別稱定義,如ngx_int_t為intptr_t的別稱,為了保持一致,在開發Nginx模塊時也應該使用這些Nginx源碼定義的類型而不要使用C原生類型。除了ngx_str_t外,其它三個常用的nginx type分別為:
typedef intptr_t ngx_int_t; typedef uintptr_t ngx_uint_t; typedef intptr_t ngx_flag_t; |
具體定義請參看core/ngx_config.h。關於intptr_t和uintptr_t請參考C99中的stdint.h或http://linux.die.net/man/3/intptr_t。
10.2. 定義指令
一個Nginx模塊往往接收一至多個指令,echo模塊接收一個指令“echo”。Nginx模塊使用一個ngx_command_t數組表示模塊所能接收的所有模塊,其中每一個元素表示一個條指令。ngx_command_t是ngx_command_s的一個別稱(Nginx習慣於使用“_s”后綴命名結構體,然后typedef一個同名“_t”后綴名稱作為此結構體的類型名),ngx_command_s定義在core/ngx_config_file.h中:
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; }; |
1.name是詞條指令的名稱,不能包含空格。
2.type使用掩碼標志位的方式,配置指令參數。
相關可用type定義在core/ngx_config_file.h中:
* NGX_HTTP_MAIN_CONF: 指令出現在main配置部分是合法的 * NGX_HTTP_SRV_CONF: 指令在server配置部分出現是合法的 * NGX_HTTP_LOC_CONF: 指令在location配置部分出現是合法的 * NGX_HTTP_UPS_CONF: 指令在upstream配置部分出現是合法的
* NGX_CONF_NOARGS: 指令沒有參數 * NGX_CONF_TAKE1: 指令讀入1個參數 * NGX_CONF_TAKE2: 指令讀入2個參數 * ... * NGX_CONF_TAKE7: 指令讀入7個參數
* NGX_CONF_FLAG: 指令讀入1個布爾型數據 ("on" or "off") * NGX_CONF_1MORE: 指令至少讀入1個參數 * NGX_CONF_2MORE: 指令至少讀入2個參數 |
其中NGX_CONF_NOARGS表示此指令不接受參數,NGX_CON F_TAKE1-7表示精確接收1-7個,NGX_CONF_TAKE12表示接受1或2個參數,NGX_CONF_1MORE表示至少一個參數,NGX_CONF_FLAG表示接受“on|off”……
3.set是一個函數指針,這個函數一般是將配置文件中相關指令的參數,轉化成需要的格式並存入配置結構體。設定函數會在遇到指令時執行。
a.指向結構體 ngx_conf_t 的指針, 這個結構體里包含需要傳遞給指令的參數
b.指向結構體 ngx_command_t 的指針
c.指向模塊自定義配置結構體的指針
Nginx預定義了一些轉換函數,可以方便我們調用,這些函數定義在core/ngx_conf_file.h中,一般以“_slot”結尾,例如:
* ngx_conf_set_flag_slot: 將 "on" or "off" 轉換成 1 or 0
* ngx_conf_set_str_slot: 將字符串保存為 ngx_str_t
* ngx_conf_set_num_slot: 解析一個數字並保存為int
* ngx_conf_set_size_slot: 解析一個數據大小(如:"8k", "1m") 並保存為size_t
4-5.conf用於指定Nginx相應配置文件,在內存起始地址。一般可以通過內置常量指定,如NGX_HTTP_MAIN_CONF_OFFSET,NGX_HTTP_SRV_CONF_OFFSET,或者 NGX_HTTP_LOC_CONF_OFFSET。offset指定此條指令的參數的偏移量。
下面是echo模塊的指令定義:
static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_echo, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), NULL }, ngx_null_command }; |
6. post指向模塊在讀配置的時候需要的一些零碎變量。一般它是NULL。
指令數組的命名規則為ngx_http_[module-name]_commands,注意數組最后一個元素要是ngx_null_command結束。
10.3. 模塊上下文
靜態的ngx_http_module_t結構體,包含一大坨函數引用,用來創建和合並三段配置 (main,server,location),命名方式一般是:ngx_http_<module name>_module_ctx,這個結構主要用於定義各個Hook函數。這些函數引用依次是:
函數的入參各不相同,取決於他們具體要做的事情。這里http/ngx_http_config.h是結構體的具體定義:
typedef struct { ngx_int_t (*preconfiguration)(ngx_conf_t *cf); ngx_int_t (*postconfiguration)(ngx_conf_t *cf);
void *(*create_main_conf)(ngx_conf_t *cf); char *(*init_main_conf)(ngx_conf_t *cf, void *conf);
void *(*create_srv_conf)(ngx_conf_t *cf); char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
void *(*create_loc_conf)(ngx_conf_t *cf); char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf); } ngx_http_module_t; |
* preconfiguration 在讀入配置前調用
* postconfiguration 在讀入配置后調用
* create_main_conf 在創建main配置時調用(比如,用來分配空間和設置默認值)
* init_main_conf 在初始化main配置時調用(比如,把原來的默認值用nginx.conf讀到的值來覆蓋)
* init_main_conf 在創建server配置時調用
* merge_srv_conf 合並server和main配置時調用
* create_loc_conf 創建location配置時調用
* merge_loc_conf 合並location和server配置時調用
可以把你不需要的函數設置為NULL,Nginx會忽略掉他們。
絕大多數的 handler只使用最后兩個:一個用來為特定location配置來分配內存,(叫做 ngx_http_<module name>_create_loc_conf);另一個用來設定默認值以及合並繼承過來的配置值(叫做 ngx_http_<module name >_merge_loc_conf)。合並函數同時還會檢查配置的有效性,如果有錯誤,則server的啟動將被掛起。
下面是一個使用模塊上下文結構體的例子:
static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_echo_create_loc_conf, /* create location configration */ ngx_http_echo_merge_loc_conf /* merge location configration */ }; |
現在開始講得更深一點。這些配置回調函數看其來很像,所有模塊都一樣,而且Nginx的API都會用到這個部分。
可以看到一共有8個Hook注入點,分別會在不同時刻被Nginx調用,由於我們的模塊僅僅用於location域,這里將不需要的注入點設為NULL即可。其中create_loc_conf用於初始化一個配置結構體,如:為配置結構體分配內存等工作;merge_loc_conf用於將其父block的配置信息合並到此結構體中,也就是實現配置的繼承。這兩個函數會被Nginx自動調用。注意這里的命名規則:ngx_http_[module-name]_[create|merge]_[main|srv|loc]_conf。
10.4. create_loc_conf
它的入參是(ngx_conf_t),返回值是更新了的模塊配置結構體
static void * ngx_http_echo_create_loc_conf(ngx_conf_t *cf) { ngx_http_echo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; return conf; }
|
首先需要指出的是Nginx的內存分配;只要使用了 ngx_palloc(malloc的一個包裝函數)或者 ngx_pcalloc (calloc的包裝函數),就不用擔心內存的釋放了。
create_loc_conf新建一個ngx_http_echo_loc_conf_t,分配內存,並初始化其中的數據,然后返回這個結構的指針。
10.5. merge_loc_conf
下面的例子是我的模塊echo中的合並函數:
static char * ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); return NGX_CONF_OK; } |
merge_loc_conf將父block域的配置信息合並到create_loc_conf新建的配置結構體中。
這里的需要注意的是Nginx提供了一些好用的合並函數用來合並不同類型的數據(ngx_conf_merge_<data type>_value),這類函數的入參是:
1. 當前location 的變量值
2. 如果第一個參數沒有被設置而采用的值
3. 如果第一第二個參數都沒有被設置而采用的值
結果會被保存在第一個參數中。能用的合並函數包括 ngx_conf_merge_size_value, ngx_conf_merge_msec_value 等等。可參見 core/ngx_conf_file.h.
以ngx_conf_merge_str_value為例:
#define ngx_conf_merge_str_value(conf, prev, default) \ if (conf.data == NULL) { \ if (prev.data) { \ conf.len = prev.len; \ conf.data = prev.data; \ } else { \ conf.len = sizeof(default) - 1; \ conf.data = (u_char *) default; \ } \ } |
同時可以看到,core/ngx_conf_file.h還定義了很多merge value的宏用於merge各種數據。它們的行為比較相似:使用prev填充conf,如果prev的數據為空則使用default填充。
同時還需要注意的是:錯誤的處理。函數需要往log文件寫一些東西,同時返回NGX_CONF_ERROR。這個返回值會將server的啟動掛起。(因為被標示為NGX_LOG_EMERG級別,所以錯誤同時還會輸出到標准輸出。作為參考,core/ngx_log.h列出了所有的日志級別。)
10.6. 模塊定義
接下來我們間接地介紹更深一層:結構體ngx_module_t。該結構體變量命名方式為ngx_http_<module name>_module。它包含模塊的內容和指令執行方式,同時也還包含一些回調函數(退出線程,退出進程,等等)。模塊定義在有的時候會被用作查找的關鍵字,來查找與特定模塊相關聯的數據。模塊定義通常像是這樣:
ngx_module_t ngx_http_<mode name>_module = { NGX_MODULE_V1, &ngx_http_module_ctx, /* module context */ ngx_http_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; |
僅僅替換掉合適的<module name>就可以了。模塊可以添加一些回調函數來處理線程/進程的創建和銷毀,但是絕大多數模塊都用NULL。(關於這些回調函數的入參,可以參考 core/ngx_conf_file.h.)
10.7. Handler
下面的工作是編寫handler。handler可以說是模塊中真正干活的代碼。
Handler一般做4件事:
- 讀入模塊配置(獲取location配置)。
- 處理功能業務(生成合適的響應)。
- 產生HTTP header(發送響應頭)。
- 產生HTTP body(發送響應體)。
Handler有一個參數,即請求結構體。請求結構體包含很多關於客戶請求的有用信息,比如說請求方法,URI,請求頭等等。
下面先貼出echo模塊的代碼,然后通過分析代碼的方式介紹如何實現這四步。這一塊的代碼比較復雜:
static ngx_int_t ngx_http_echo_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_echo_loc_conf_t *elcf; elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module); if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST))) { return NGX_HTTP_NOT_ALLOWED; } r->headers_out.content_type.len = sizeof("text/html") - 1; r->headers_out.content_type.data = (u_char *) "text/html"; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = elcf->ed.len; if(r->method == NGX_HTTP_HEAD) { rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } } b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if(b == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer."); return NGX_HTTP_INTERNAL_SERVER_ERROR; } out.buf = b; out.next = NULL; b->pos = elcf->ed.data; b->last = elcf->ed.data + (elcf->ed.len); b->memory = 1; b->last_buf = 1; rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } return ngx_http_output_filter(r, &out); } |
handler會接收一個ngx_http_request_t指針類型的參數,這個參數指向一個ngx_http_request_t結構體,此結構體存儲了這次HTTP請求的一些信息,這個結構定義在http/ngx_http_request.h中:
struct ngx_http_request_s { uint32_t signature; /* "HTTP" */ ngx_connection_t *connection; void **ctx; void **main_conf; void **srv_conf; void **loc_conf; ngx_http_event_handler_pt read_event_handler; ngx_http_event_handler_pt write_event_handler; /* … */ ngx_http_handler_pt content_handler; ngx_uint_t access_code; ngx_http_variable_value_t *variables; /* ... */ } |
由於ngx_http_request_s定義比較長,這里我只截取了一部分。可以看到里面有諸如uri,args和request_body等HTTP常用信息。這里需要特別注意的幾個字段是headers_in、headers_out和chain,它們分別表示request header、response header和輸出數據緩沖區鏈表(緩沖區鏈表是Nginx I/O中的重要內容,后面會單獨介紹)。
第一步是獲取模塊配置信息,這一塊只要簡單使用ngx_http_get_module_loc_conf就可以了。現在我們就可以訪問之前在合並函數中設置的所有變量了。
第二步是功能邏輯。uri 是請求的路徑, e.g. "/echo"。args 請求串參數中問號后面的參數 (e.g. "name=john"). headers_in 包含有很多有用的東西,比如說cookie啊,瀏覽器信息啊什么的,但是許多模塊可能用不到這些東東。如果你感興趣的話,可以參看http/ngx_http_request.h 。對於生成輸出,這些信息應該是夠了。完整的ngx_http_request_t結構體定義在http/ngx_http_request.h。
第三步是設置response header。Header內容可以通過填充headers_out實現,我們這里只設置了Content-type和Content-length等基本內容,ngx_http_headers_out_t定義了所有可以設置的HTTP Response Header信息:
typedef struct { ngx_list_t headers; ngx_uint_t status; ngx_str_t status_line; /* … */ ngx_str_t charset; u_char *content_type_lowcase; ngx_uint_t content_type_hash; ngx_array_t cache_control; off_t content_length_n; time_t date_time; time_t last_modified_time; } ngx_http_headers_out_t; |
這里並不包含所有HTTP頭信息,如果需要可以使用agentzh(春來)開發的Nginx模塊HttpHeadersMore在指令中指定更多的Header頭信息。
設置好頭信息后使用ngx_http_send_header就可以將頭信息輸出,ngx_http_send_header接受一個ngx_http_request_t類型的參數。
第四步也是最重要的一步是輸出Response body。這里首先要了解Nginx的I/O機制,Nginx允許handler一次產生一組輸出,可以產生多次,Nginx將輸出組織成一個單鏈表結構,鏈表中的每個節點是一個chain_t,定義在core/ngx_buf.h:
struct ngx_chain_s { ngx_buf_t *buf; ngx_chain_t *next; }; |
其中ngx_chain_t是ngx_chain_s的別名,buf為某個數據緩沖區的指針,next指向下一個鏈表節點,可以看到這是一個非常簡單的鏈表。ngx_buf_t的定義比較長而且很復雜,這里就不貼出來了,請自行參考core/ngx_buf.h。ngx_but_t中比較重要的是pos和last,分別表示要緩沖區數據在內存中的起始地址和結尾地址,這里我們將配置中字符串傳進去,last_buf是一個位域,設為1表示此緩沖區是鏈表中最后一個元素,為0表示后面還有元素。因為我們只有一組數據,所以緩沖區鏈表中只有一個節點,如果需要輸入多組數據可將各組數據放入不同緩沖區后插入到鏈表。下圖展示了Nginx緩沖鏈表的結構:
緩沖數據准備好后,用ngx_http_output_filter就可以輸出了(會送到filter進行各種過濾處理)。ngx_http_output_filter的第一個參數為ngx_http_request_t結構,第二個為輸出鏈表的起始地址&out。ngx_http_out_put_filter會遍歷鏈表,輸出所有數據。
以上就是handler的所有工作,請對照描述理解上面貼出的handler代碼。
10.8. Upstream
我已經幫你了解了如何讓你的handler來產生響應。有些時候你可以用一小段C代碼就可以得到響應,但是通常情況下你需要同另外一台server打交道(比如你正在寫一個用來實現某種網絡協議的模塊)。你當然可以自己實現一套網絡編程的東東,但是如果你只收到部分的響應,需要等待余下的響應數據,你會怎么辦?你不會想阻塞整個事件處理循環吧?這樣會毀掉Nginx的良好性能!幸運的是,Nginx允許你在它處理后端服務器(叫做"upstreams")的機制上加入你的回調函數,因此你的模塊將可以和其他的server通信,同時還不會妨礙其他的請求。這一節將介紹模塊如何和一個upstream(如 Memcached, FastCGI,或者另一個 HTTP server)通信。
10.8.1. Upstream 回調函數概要
與其他模塊的回調處理函數不一樣,upstream模塊的處理函數幾乎不做“實事”。它壓根不調用ngx_http_output_filter。它僅僅是告訴回調函數什么時候可以向upstream server寫數據了,以及什么時候能從upstream server讀數據了。實際上它有6個可用的鈎子:
- create_request 生成發送到
- upstream server的請求緩沖(或者一條緩沖鏈)
- reinit_request 在與后端服務器連接被重置的情況下(在create_request 被第二次調用之前)被調用
- process_header 處理upstream 響應的第一個bit,通常是保存一個指向upstream "payload"的指針
- abort_request 在客戶端放棄請求時被調用 finalize_request 在Nginx完成從upstream讀取數據后調用
- input_filter 這是一個消息體的filter,用來處理響應消息體(例如把尾部刪除)
這些鈎子是怎么勾上去的呢?下面是一個例子,簡單版本的代理模塊處理函數:
static ngx_int_t ngx_http_proxy_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_http_upstream_t *u; ngx_http_proxy_loc_conf_t *plcf;
plcf = ngx_http_get_module_loc_conf(r, ngx_http_proxy_module);
/* set up our upstream struct */ u = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_t)); if (u == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; }
u->peer.log = r->connection->log; u->peer.log_error = NGX_ERROR_ERR;
u->output.tag = (ngx_buf_tag_t) &ngx_http_proxy_module;
u->conf = &plcf->upstream;
/* attach the callback functions */ u->create_request = ngx_http_proxy_create_request; u->reinit_request = ngx_http_proxy_reinit_request; u->process_header = ngx_http_proxy_process_status_line; u->abort_request = ngx_http_proxy_abort_request; u->finalize_request = ngx_http_proxy_finalize_request; r->upstream = u;
rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init);
if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { return rc; }
return NGX_DONE; } |
看上去都是些例行事務,不過重要的是那些回調函數。同時還要注意的是ngx_http_read_client_request_body,它又設置了一個回調函數,在Nginx完成從客戶端讀數據后會被調用。
這些個回調函數都要做些什么工作呢?通常情況下,reinit_request, abort_request, 和 finalize_request用來設置或重置一些內部狀態,但這些都是幾行代碼的事情。真正做苦力的是create_request 和 process_header。
10.8.2. create_request 回調函數
假設我有一個upstream server,它讀入一個字符打印出兩個字符。那么函數應該如何來寫呢? create_request需要申請一個buffer來存放“一個字符”的請求,為buffer申請一個鏈表,並且把鏈表掛到upstream結構體上。看起來就像這樣:
static ngx_int_t ngx_http_character_server_create_request(ngx_http_request_t *r) { /* make a buffer and chain */ ngx_buf_t *b; ngx_chain_t *cl;
b = ngx_create_temp_buf(r->pool, sizeof("a") - 1); if (b == NULL) return NGX_ERROR;
cl = ngx_alloc_chain_link(r->pool); if (cl == NULL) return NGX_ERROR;
/* hook the buffer to the chain */ cl->buf = b; /* chain to the upstream */ r->upstream->request_bufs = cl;
/* now write to the buffer */ b->pos = "a"; b->last = b->pos + sizeof("a") - 1;
return NGX_OK; } |
當然實際應用中你很可能還會用到請求里面的URI。r->uri作為一個 ngx_str_t類型也是有效的,GET的參數在r->args中,最后別忘了你還能訪問請求頭和 cookie信息。
10.8.3. process_header 回調函數
process_header把響應指針移到客戶端可以接收到的部分。同時它還會從upstream 讀入頭信息,並且相應的設置發往客戶端的響應頭。
這里有個小例子,讀進兩個字符的響應。我們假設第一個字符代表“狀態”字符。如果它是問號,我們將返回一個404錯誤並丟棄剩下的那個字符。如果它是空格,我們將以 200 OK的響應把另一個字符返回給客戶端。那么我們如何來實現這個process_header 函數呢?
static ngx_int_t ngx_http_character_server_process_header(ngx_http_request_t *r) { ngx_http_upstream_t *u; u = r->upstream;
/* read the first character */ switch(u->buffer.pos[0]) { case '?': r->header_only; /* suppress this buffer from the client */ u->headers_in.status_n = 404; break; case ' ': u->buffer.pos++; /* move the buffer to point to the next character */ u->headers_in.status_n = 200; break; }
return NGX_OK; } |
就是這樣。操作頭部,改變指針,搞定!注意headers_in實際上就是我們之前提到過的頭部結構體( http/ngx_http_request.h),但是它位於來自upstream的頭中。一個真正的代理模塊,會在頭信息的處理上做很多文章,不光是錯誤處理,做什么完全取決於你的想法。
10.8.4. 狀態保持
好了,還記得我說過abort_request, reinit_request和finalize_request 可以用來重置內部狀態嗎?這是因為許多upstream模塊都有其內部狀態。模塊需要定義一個自定義上下文結構 ,來標記目前為止從upstream讀到了什么。這跟之前說的“模塊上下文”不是一個概念。“模塊上下文”是預定義類型,而自定義上下文結構可以包含任何你需要的數據和字段(這可是你自己定義的結構體)。這個結構體在create_request函數中被實例化,大概像這樣:
ngx_http_character_server_ctx_t *p; /* my custom context struct */ p = ngx_pcalloc(r->pool, sizeof(ngx_http_character_server_ctx_t)); if (p == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } ngx_http_set_ctx(r, p, ngx_http_character_server_module); |
最后一行實際上將自定義上下文結構體注冊到了特定的請求和模塊名上,以便在稍后取用。當你需要這個結構體時(可能所有的回調函數中都需要它),只需要:
ngx_http_proxy_ctx_t *p; p = ngx_http_get_module_ctx(r, ngx_http_proxy_module); |
指針 p 可以得到當前的狀態. 設置、重置、增加、減少、往里填數據……你可以隨心所欲的操作它。當upstream服務器返回一塊一塊的響應時,讀取這些響應的過程中使用持久狀態機是個很nx的辦法,它不用阻塞主事件循環。
10.9. Handler的裝載
Handler的裝載通過往模塊啟用了的指令的回調函數中添加代碼來完成。比如,我的echo中ngx_command_t是這樣的:
{ ngx_string("echo "), NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, ngx_http_echo, 0, 0, NULL } |
回調函數是里面的第三個元素,在這個例子中就是那個ngx_http_echo。回調函數的參數是由指令結構體(ngx_conf_t, 包含用戶配置的參數),相應的ngx_command_t結構體以及一個指向模塊自定義配置結構體的指針組成的。我的echo模塊中,這些函數是這樣子的:
static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_echo_handler; ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK; } |
這個函數除了調用ngx_conf_set_str_slot轉化echo指令的參數外,還將修改了核心模塊配置(也就是這個location的配置),將其handler替換為我們編寫的handler:ngx_http_echo_handler。這樣就屏蔽了此location的默認handler,使用ngx_http_echo_handler產生HTTP響應。
這里可以分為兩步:首先,得到當前location的“core”結構體,再分配給它一個 handler。很簡單,不是嗎? 我已經把我知道的關於hanler模塊的東西全招了,現在可以來說說輸出過濾鏈上的filter模塊了。
11. Filters
Filter操作handler生成的響應。頭部filter操作HTTP頭,body filter操作響應的內容。
11.1. 剖析Header Filter
頭部Filter由三個步驟組成:
1. 決定何時操作響應
2. 操作響應
3. 調用下一個filter
舉個例子,比如有一個簡化版本的“不改變”頭部filter:如果客戶請求頭中的If- Modified-Since和響應頭中的Last-Modified相符,它把響應狀態設置成304。注意這個頭部filter只讀入一個參數:ngx_http_request_t結構體,而我們可以通過它操作到客戶請求頭和一會將被發送的響應消息頭。
static ngx_int_t ngx_http_not_modified_header_filter(ngx_http_request_t *r) { time_t if_modified_since; if_modified_since = ngx_http_parse_time(r->headers_in.if_modified_since->value.data, r->headers_in.if_modified_since->value.len);
/* step 1: decide whether to operate */ if (if_modified_since != NGX_ERROR && if_modified_since == r->headers_out.last_modified_time) {
/* step 2: operate on the header */ r->headers_out.status = NGX_HTTP_NOT_MODIFIED; r->headers_out.content_type.len = 0; ngx_http_clear_content_length(r); ngx_http_clear_accept_ranges(r); }
/* step 3: call the next filter */ return ngx_http_next_header_filter(r); } |
結構headers_out和我們在hander那一節中看到的是一樣的(參考http/ngx_http_request.h),也可以隨意處置。
11.2. 剖析Body Filter
因為body filter一次只能操作一個buffer(鏈表),這使得編寫body filter需要一定的技巧。模塊需要知道什么時候可以覆蓋輸入buffer,用新申請的buffer_替換已有的,或者在現有的某個buffer前或后插入一個新buffer。有時候模塊會收到許多buffer使得它不得不操作一個不完整的鏈表,這使得事情變得更加復雜了。而更加不幸的是,Nginx沒有為我們提供上層的API來操作buffer鏈表,所以body filter是比較難懂(當然也比較難寫)。但是,有些操作你還是可以看出來的。
一個body filter原型大概是這個樣子(例子代碼從Nginx源代碼的“chunked” filter中取得): static ngx_int_t ngx_http_chunked_body_filter(ngx_http_request_t *r, ngx_chain_t *in);
第一個參數是請求結構體,第二個參數則是指向當前部分鏈表(可能包含0,1,或更多的buffer)頭的指針。
再來舉個例子好了。假設我們想要做的是在每個請求之后插入文本"<l!-- Served by Nginx -->"。首先,我們需要判斷給我們的buffer鏈表中是否已經包含響應的最終buffer。就像之前我說的,這里沒有簡便好用的API,所以我們只能自己來寫個循環:
ngx_chain_t *chain_link; int chain_contains_last_buffer = 0;
for ( chain_link = in; chain_link != NULL; chain_link = chain_link->next ) { if (chain_link->buf->last_buf) chain_contains_last_buffer = 1; } |
如果我們沒有最后的緩沖區,就返回:
if (!chain_contains_last_buffer) return ngx_http_next_body_filter(r, in); |
很好,現在最后一個緩沖區已經存在鏈表中了。接下來我們分配一個新緩沖區:
ngx_buf_t *b; b = ngx_calloc_buf(r->pool); if (b == NULL) { return NGX_ERROR; } |
把數據放進去:
b->pos = (u_char *) ""; b->last = b->pos + sizeof("") - 1; |
把這個緩沖區掛在新的鏈表上:
ngx_chain_t added_link; added_link.buf = b; added_link.next = NULL; |
最后,把這個新鏈表掛在先前鏈表的末尾:
chain_link->next = added_link; |
並根據變化重置變量"last_buf"的值:
chain_link->buf->last_buf = 0; added_link->buf->last_buf = 1; |
再將修改過的鏈表傳遞給下一個輸出過濾函數:
return ngx_http_next_body_filter(r, in); |
現有的函數做了比我們更多的工作,比如mod_perl($response->body =~ s/$/<!-- Served by mod_perl -->/),但是緩沖區鏈確實是一個強大的構想,它可以讓程序員漸進地處理數據,這使得客戶端可以盡可能早地得到響應。但是依我來看,緩沖區鏈表實在需要一個更為干凈的接口,這樣程序員也可以避免操作不一致狀態的鏈表。但是目前為止,所有的操作風險都得自己控制。
11.3. Filter的裝載
Filter在在回調函數post-configuration中被裝載。header filter和body filter都是在這里被裝載的。
我們以chunked filter模塊為例來具體看看:
static ngx_http_module_t ngx_http_chunked_filter_module_ctx = { NULL, /* preconfiguration */ ngx_http_chunked_filter_init, /* postconfiguration */ ... }; |
ngx_http_chunked_filter_init中的具體實現如下:
static ngx_int_t ngx_http_chunked_filter_init(ngx_conf_t *cf) { ngx_http_next_header_filter = ngx_http_top_header_filter; ngx_http_top_header_filter = ngx_http_chunked_header_filter;
ngx_http_next_body_filter = ngx_http_top_body_filter; ngx_http_top_body_filter = ngx_http_chunked_body_filter;
return NGX_OK; } |
發生了什么呢?好吧,如果你還記得,過濾模塊組成了一條”接力鏈表“。當handler生成一個響應后,調用2個函數:ngx_http_output_filter它調用全局函數ngx_http_top_body_filter;以及ngx_http_send_header 它調用全局函數ngx_top_header_filter。
ngx_http_top_body_filter 和 ngx_http_top_header_filter是body和header各自的頭部filter鏈的“鏈表頭”。鏈表上的每一個“連接”都保存着鏈表中下一個連接的函數引用(分別是 ngx_http_next_body_filter 和 ngx_http_next_header_filter)。當一個filter完成工作之后,它只需要調用下一個filter,直到一個特殊的被定義成“寫”的filter被調用,這個“寫”filter的作用是包裝最終的HTTP響應。你在這個filter_init函數中看到的就是,模塊把自己添加到filter鏈表中;它先把舊的“頭部”filter當做是自己的“下一個”,然后再聲明“它自己”是“頭部”filter。(因此,最后一個被添加的filter會第一個被執行。)
每個filter要么返回一個錯誤碼,要么用`return ngx_http_next_body_filter();`來作為返回語句
因此,如果filter順利鏈執行到了鏈尾(那個特別定義的的”寫“filter),將返回一個"OK"響應,但如果執行過程中遇到了錯誤,鏈將被砍斷,同時Nginx將給出一個錯誤的信息。這是一個單向的,錯誤快速返回的,只使用函數引用實現的鏈表!
12. Load-balancers
Load-balancer用來決定哪一個后端將會收到請求;具體的實現是round-robin方式或者把請求進行hash。本節將介紹load-balancer模塊的裝載及其調用。我們將用upstream_hash_module(full source)作例子。upstream_hash將對nginx.conf里配置的變量進行 hash,來選擇后端服務器。
一個load-balancer分為六個部分:
1.啟用配置指令 (e.g, hash;) 將會調用注冊函數
2.注冊函數將定義一些合法的server 參數 (e.g., weight=) 並注冊一個 upstream初始化函數
3. upstream初始化函數將在配置經過驗證后被調用,並且:
* 解析 server 名稱為特定的IP地址
* 為每個sokcet連接分配空間
* 設置對端初始化函數的回調入口
4.對端初始化函數將在每次請求時被調用一次,它主要負責設置一些負載均衡函數將會使用的數據結構。
5.負載均衡函數決定把請求分發到哪里;每個請求將至少調用一次這個函數(如果后端服務器失敗了,那就是多次了)。
6.對端釋放函數 可以在與對應的后端服務器結束通信之后更新統計信息 (成功或失敗)
12.1. 啟用指令
指令聲明,既確定了他們在哪里生效又確定了一旦流程遇到指令將要調用什么函數。load-balancer的指令需要置NGX_HTTP_UPS_CONF標志位,一遍讓Nginx知道這個指令只會在upstream塊中有效。同時它需要提供一個指向注冊函數的指針。下面列出的是upstream_hash模塊的指令聲明:
{ ngx_string("hash"), NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS, ngx_http_upstream_hash, 0, 0, NULL } |
12.2. 注冊函數
上面的回調函數ngx_http_upstream_hash就是所謂的注冊函數。之所以這樣叫(我起得名字)是因為它注冊了把upstream初始化函數和周邊的upstream配置注冊到了一塊。另外,注冊函數還定義了特定upstream塊中的server指令的一些選項(如weight=, fail_timeout=),下面是upstream_hash模塊的注冊函數:
ngx_int_t ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_upstream_srv_conf_t *uscf; ngx_http_script_compile_t sc; ngx_str_t *value; ngx_array_t *vars_lengths, *vars_values;
value = cf->args->elts;
/* the following is necessary to evaluate the argument to "hash" as a $variable */ ngx_memzero(&sc, sizeof(ngx_http_script_compile_t)); vars_lengths = NULL; vars_values = NULL; sc.cf = cf; sc.source = &value[1]; sc.lengths = &vars_lengths; sc.values = &vars_values; sc.complete_lengths = 1; sc.complete_values = 1; if (ngx_http_script_compile(&sc) != NGX_OK) { return NGX_CONF_ERROR; } /* end of $variable stuff */ uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module); /* the upstream initialization function */ uscf->peer.init_upstream = ngx_http_upstream_init_hash; uscf->flags = NGX_HTTP_UPSTREAM_CREATE; /* OK, more $variable stuff */ uscf->values = vars_values->elts; uscf->lengths = vars_lengths->elts; /* set a default value for "hash_method" */ if (uscf->hash_function == NULL) { uscf->hash_function = ngx_hash_key; } return NGX_CONF_OK; } |
除了依葫蘆畫瓢的用來計算$variable的代碼,剩下的都很簡單,就是分配一個回調函數,設置一些標志位。哪些標志位是有效的呢?
* NGX_HTTP_UPSTREAM_CREATE: 讓upstream塊中有 server 指令。我實在想不出那種情形會用不到它。
* NGX_HTTP_UPSTREAM_WEIGHT: 讓server指令獲取選項 weight=
* NGX_HTTP_UPSTREAM_MAX_FAILS: 允許選項max_fails=
* NGX_HTTP_UPSTREAM_FAIL_TIMEOUT: 允許選項fail_timeout=
* NGX_HTTP_UPSTREAM_DOWN: 允許選項 down
* NGX_HTTP_UPSTREAM_BACKUP: 允許選項backup
每一個模塊都可以訪問這些配置值。
一切都取決於模塊自己的決定。也就是說,max_fails不會被自動強制執行;所有的失敗邏輯都是由模塊作者決定的。過會我們再說這個。目前,我們還沒有完成對回調函數的追蹤呢。接下來,我們來看upstream初始化函數 (上面的函數中的回調函數init_upstream )。
12.3. upstream 初始化函數
upstream 初始化函數的目的是,解析主機名,為socket分配空間,分配(另一個)回調函數。下面是upstream_hash:
ngx_int_t ngx_http_upstream_init_hash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) { ngx_uint_t i, j, n; ngx_http_upstream_server_t *server; ngx_http_upstream_hash_peers_t *peers; /* set the callback */ us->peer.init = ngx_http_upstream_init_upstream_hash_peer; if (!us->servers) { return NGX_ERROR; } server = us->servers->elts; /* figure out how many IP addresses are in this upstream block. */ /* remember a domain name can resolve to multiple IP addresses. */ for (n = 0, i = 0; i < us->servers->nelts; i++) { n += server[i].naddrs; } /* allocate space for sockets, etc */ peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_hash_peers_t) + sizeof(ngx_peer_addr_t) * (n - 1)); if (peers == NULL) { return NGX_ERROR; } peers->number = n; /* one port/IP address per peer */ for (n = 0, i = 0; i < us->servers->nelts; i++) { for (j = 0; j < server[i].naddrs; j++, n++) { peers->peer[n].sockaddr = server[i].addrs[j].sockaddr; peers->peer[n].socklen = server[i].addrs[j].socklen; peers->peer[n].name = server[i].addrs[j].name; } } /* save a pointer to our peers for later */ us->peer.data = peers;
return NGX_OK; } |
這個函數包含的東西ms比我們期望的多些。大部分的工作ms都該被抽象出來,但事實卻不是,我們只能忍受這一點。倒是有一種簡化的策略:調用另一個模塊的upstream初始化函數,把這些臟活累活(對端的分配等等)都讓它干了,然后再覆蓋其us->peer.init這個回調函數。例子可以參見http/modules/ngx_http_upstream_ip_hash_module.c。
在我們這個觀點中的關鍵點是設置對端初始化函數的指向,在我們這個例子里是ngx_http_upstream_init_upstream_hash_peer。
12.4. 對端初始化函數
對端初始化函數每個請求調用一次。它會構造一個數據結構,模塊會用這個數據結構來選擇合適的后端服務器;這個數據結構保存着和后端交互的重試次數,通過它可以很容易的跟蹤鏈接失敗次數或者是計算好的哈希值。這個結構體習慣性地被命名為ngx_http_upstream_<module name>_peer_data_t。
另外,對端初始化函數還會構建兩個回調函數:
* get: load-balancing 函數
* free: 對端釋放函數 (通常只是在連接完成后更新一些統計信息)
似乎還不止這些,它同時還初始化了一個叫做tries的變量。只要tries是正數,Nginx將繼續重試當前的load-banlancer。當tries變為0時,Nginx將放棄重試。一切都取決於get 和 free 如何設置合適的tries。
下面是upstream_hash中對端初始化函數的例子:
static ngx_int_t ngx_http_upstream_init_hash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) { ngx_http_upstream_hash_peer_data_t *uhpd; ngx_str_t val; /* evaluate the argument to "hash" */ if (ngx_http_script_run(r, &val, us->lengths, 0, us->values) == NULL) { return NGX_ERROR; } /* data persistent through the request */ uhpd = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t) + sizeof(uintptr_t) * ((ngx_http_upstream_hash_peers_t *)us->peer.data)->number / (8 * sizeof(uintptr_t))); if (uhpd == NULL) { return NGX_ERROR; } /* save our struct for later */ r->upstream->peer.data = uhpd; uhpd->peers = us->peer.data; /* set the callbacks and initialize "tries" to "hash_again" + 1*/ r->upstream->peer.free = ngx_http_upstream_free_hash_peer; r->upstream->peer.get = ngx_http_upstream_get_hash_peer; r->upstream->peer.tries = us->retries + 1; /* do the hash and save the result */ uhpd->hash = us->hash_function(val.data, val.len); return NGX_OK; } |
12.5. 負載均衡函數
主要部分現在才開始。模塊就是在這里選擇upstream服務器的。負載均衡函數的原型看上去是這樣的: static ngx_int_t ngx_http_upstream_get__peer(ngx_peer_connection_t *pc, void *data);
data是我們存放所關注的客戶端連接中有用信息的結構體。pc則是要存放我們將要去連接的server的相關信息。負載均衡函數做的事情就是填寫pc->sockaddr, pc->socklen, 和 pc->name。如果你懂一點網絡編程的話,這些東西應該都比較熟悉了;但實際上他們跟我們手頭上的任務來比並不算很重要。我們不關心他們代表什么;我們只想知道從哪里找到合適的值來填寫他們。
這個函數必須找到一個可用server的列表,挑一個分配給pc。我們來看看upstream_hash是怎么做的吧: 之前upstream_hash模塊已經通過調用ngx_http_upstream_init_hash,把server列表存放在了ngx_http_upstream_hash_peer_data_t 這一結構中。這個結構就是現在的data:
ngx_http_upstream_hash_peer_data_t *uhpd = data;
對端列表現在在uhpd->peers->peer中了。我們通過對哈希值與 server總數取模來從這個數組中取得最終的對端服務器:
ngx_peer_addr_t *peer = &uhpd->peers->peer[uhpd->hash % uhpd->peers->number];
終於大功告成了:
pc->sockaddr = peers->sockaddr;
pc->socklen = peers->socklen;
pc->name = &peers->name;
return NGX_OK;
就是這樣!如果load-balancer模塊返回 NGX_OK,則意味着”來吧,上這個 server吧!“。如果返回的是NGX_BUSY,說明所有的后端服務器目前都不可用,此時Nginx應該重試。
12.6. 對端釋放函數
對端釋放函數在upstream連接就緒之后開始運行,它的目的是跟蹤失敗。函數原型如下:
Void ngx_http_upstream_free__peer(ngx_peer_connection_t *pc, void *data,
ngx_uint_t state);
頭兩個參數和我們在load-balancer函數中看到的一樣。第三個參數是一個state變量,它表明了當前連接是成功還是失敗。它可能是NGX_PEER_FAILED (連接失敗) 和 NGX_PEER_NEXT (連接失敗或者連接成功但程序返回了錯誤)按位或的結果。如果它是0則代表連接成功。
這些失敗如何處理則由模塊的開發者自己定。如果根本不再用,那結果則應存放到data中,這是一個指向每個請求自定義的結構體。
但是對端釋放函數的關鍵作用是可以設置pc->tries為 0來阻止Nginx在load-balancer模塊中重試。最簡單的對端釋放函數應該是這樣的:
pc->tries = 0;
這樣就保證了如果發往后端服務器的請求遇到了錯誤,客戶端將得到一個502 Bad Proxy的錯誤。
這兒還有一個更為復雜的例子,是從upstream_hash模塊中拿來的。如果后端連接失敗,它會在位向量 (叫做 tried,一個 uintptr_t類型的數組)中標示失敗,然后繼續選擇一個新的后端服務器直至成功。
#define ngx_bitvector_index(index) index / (8 * sizeof(uintptr_t)) #define ngx_bitvector_bit(index) (uintptr_t) 1 << index % (8 * sizeof(uintptr_t))
static void ngx_http_upstream_free_hash_peer(ngx_peer_connection_t *pc, void *data, ngx_uint_t state) { ngx_http_upstream_hash_peer_data_t *uhpd = data; ngx_uint_t current;
if (state & NGX_PEER_FAILED && --pc->tries) { /* the backend that failed */ current = uhpd->hash % uhpd->peers->number;
/* mark it in the bit-vector */ uhpd->tried[ngx_bitvector_index(current)] |= ngx_bitvector_bit(current);
do { /* rehash until we're out of retries or we find one that hasn't been tried */ uhpd->hash = ngx_hash_key((u_char *)&uhpd->hash, sizeof(ngx_uint_t)); current = uhpd->hash % uhpd->peers->number; } while ((uhpd->tried[ngx_bitvector_index(current)] & ngx_bitvector_bit(current)) && --pc->tries); } } |
因為load-balancer函數只會看新的uhpd->hash的值,所以這樣是行之有效的。
許多應用程序不提供重試功能,或者在更高層的邏輯中進行了控制。但其實你也看到了,只需這么幾行代碼這個功能就可以實現了。
13. 組合Nginx Module
上面完成了Nginx模塊各種組件的開發下面就是將這些組合起來了。一個Nginx模塊被定義為一個ngx_module_t結構,這個結構的字段很多,不過開頭和結尾若干字段一般可以通過Nginx內置的宏去填充,下面是我們echo模塊的模塊主體定義:
ngx_module_t ngx_http_echo_module = { NGX_MODULE_V1, &ngx_http_echo_module_ctx, /* module context */ ngx_http_echo_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; |
開頭和結尾分別用NGX_MODULE_V1和NGX_MODULE_V1_PADDING 填充了若干字段,就不去深究了。這里主要需要填入的信息從上到下以依次為context、指令數組、模塊類型以及若干特定事件的回調處理函數(不需要可以置為NULL),其中內容還是比較好理解的,注意我們的echo是一個HTTP模塊,所以這里類型是NGX_HTTP_MODULE,其它可用類型還有NGX_EVENT_MODULE(事件處理模塊)和NGX_MAIL_MODULE(郵件模塊)。
這樣,整個echo模塊就寫好了,下面給出echo模塊的完整代碼:
/* * Copyright (C) Eric Zhang */ #include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h>
/* Module config */ typedef struct { ngx_str_t ed; } ngx_http_echo_loc_conf_t;
static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf); static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);
/* Directives */ static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_echo, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), NULL }, ngx_null_command };
/* Http context of the module */ static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_echo_create_loc_conf, /* create location configration */ ngx_http_echo_merge_loc_conf /* merge location configration */ };
/* Module */ ngx_module_t ngx_http_echo_module = { NGX_MODULE_V1, &ngx_http_echo_module_ctx, /* module context */ ngx_http_echo_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; /* Handler function */ static ngx_int_t ngx_http_echo_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_echo_loc_conf_t *elcf; elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module); if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST))) { return NGX_HTTP_NOT_ALLOWED; } r->headers_out.content_type.len = sizeof("text/html") - 1; r->headers_out.content_type.data = (u_char *) "text/html"; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = elcf->ed.len; if(r->method == NGX_HTTP_HEAD) { rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } } b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if(b == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer."); return NGX_HTTP_INTERNAL_SERVER_ERROR; } out.buf = b; out.next = NULL; b->pos = elcf->ed.data; b->last = elcf->ed.data + (elcf->ed.len); b->memory = 1; b->last_buf = 1; rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } return ngx_http_output_filter(r, &out); }
static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_echo_handler; ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK; }
static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf) { ngx_http_echo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; return conf; }
static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); return NGX_CONF_OK; } |
14. Nginx模塊的安裝
Nginx不支持動態鏈接模塊,所以安裝模塊需要將模塊代碼與Nginx源代碼進行重新編譯。安裝模塊的步驟如下:
1.編寫模塊config文件,這個文件需要放在和模塊源代碼文件放在同一目錄下。文件內容如下:
ngx_addon_name=模塊完整名稱 HTTP_MODULES="$HTTP_MODULES 模塊完整名稱" NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/源代碼文件名" |
2.進入Nginx源代碼,使用下面命令編譯安裝
./configure --prefix=安裝目錄 --add-module=模塊源代碼文件目錄 make make install |
這樣就完成安裝了,例如,我的源代碼文件放在/home/yefeng/ngxdev/ngx_http_echo下,我的config文件為:
ngx_addon_name=ngx_http_echo_module HTTP_MODULES="$HTTP_MODULES ngx_http_echo_module" NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_echo_module.c" |
編譯安裝命令為:
./configure --prefix=/usr/local/nginx --add-module=/home/yefeng/ngxdev/ngx_http_echo make sudo make install |
這樣echo模塊就被安裝在我的Nginx上了,下面測試一下,修改配置文件,增加以下一項配置:
location /echo { echo "This is my first nginx module!!!"; } |
然后用curl測試一下: curl -i http://localhost/echo
結果如下:
可以看到模塊已經正常工作了,也可以在瀏覽器中打開網址,就可以看到結果:
15. 更深入的學習
本文只是簡要介紹了Nginx模塊的開發過程,由於篇幅的原因,不能面面俱到。因為目前Nginx的學習資料很少,如果讀者希望更深入學習Nginx的原理及模塊開發,那么閱讀源代碼是最好的辦法。在Nginx源代碼的core/下放有Nginx的核心代碼,對理解Nginx的內部工作機制非常有幫助,http/目錄下有Nginx HTTP相關的實現,http/module下放有大量內置http模塊,可供讀者學習模塊的開發,另外在http://wiki.nginx.org/3rdPartyModules上有大量優秀的第三方模塊,也是非常好的學習資料。