Nginx是當前最流行的HTTP Server之一,根據W3Techs的統計,目前世界排名(根據Alexa)前100萬的網站中,Nginx的佔有率爲6.8%。與Apache相比,Nginx在高併發狀況下具備巨大的性能優點。html
Nginx屬於典型的微內核設計,其內核很是簡潔和優雅,同時具備很是高的可擴展性。Nginx最初僅僅主要被用於作反向代理,後來隨着HTTP核心的成熟和各類HTTP擴展模塊的豐富,Nginx愈來愈多被用來取代Apache而單獨承擔HTTP Server的責任,例如目前淘寶內各個部門正愈來愈多使用Nginx取代Apache,據筆者瞭解,在騰訊和新浪等公司也存在相似狀況。node
同時,大量的第三方擴展模塊也令Nginx愈來愈強大。例如,由淘寶的工程師清無(王曉哲)和春來(章亦春)所開發的nginx_lua_module能夠將Lua語言嵌入到Nginx配置中,從而利用Lua極大加強了Nginx自己的編程能力,甚至能夠不用配合其它腳本語言(如PHP或Python等),只靠Nginx自己就能夠實現複雜業務的處理。而春來所開發的ngx_openresty更是經過集成LuaJIT等組件,將Nginx自己變成了一個徹底的應用開發平臺。目前淘寶數據平臺與產品部量子統計的產品都是基於ngx_openresty所開發。linux
本文將會重點關注Nginx模塊開發入門及基礎。目前Nginx的學習資料很是少,而擴展模塊開發相關的資料幾乎只有《Emiller's Guide To Nginx Module Development》一文,此文十分經典,可是因爲Nginx版本的演進,其中少量內容可能有點過期。本文是筆者在研讀這篇文章和Nginx源代碼的基礎上,對本身學習Nginx模塊開發的一個總結。本文將經過一個完整的模塊開發實例講解Nginx模塊開發的入門內容。nginx
nginx是一個開源的高性能web服務器系統,事件驅動的請求處理方式和極其苛刻的資源使用方式,使得nginx成爲名副其實的高性能服務器。nginx的源碼質量也至關高,做者「家釀」了許多代碼,自造了很多輪子,諸如內存池、緩衝區、字符串、鏈表、紅黑樹等經典數據結構,事件驅動模型,http解析,各類子處理模塊,甚至是自動編譯腳本都是做者根據本身的理解寫出來的,也正由於這樣,才使得nginx比其餘的web服務器更加高效。c++
nginx的代碼至關精巧和緊湊,雖然所有代碼僅有10萬行,但功能絕不遜色於幾十萬行的apache。不過各個部分之間耦合的比較厲害,很難把其中某個部分的實現拆出來使用。對於這樣一箇中大型的複雜系統源碼進行分析,是有必定的難度的,剛開始也很難找到下手的入口,因此作這樣的事情就必須首先明確目標和計劃。git
首先這個系統中幾乎涵蓋了實現高性能服務器的各類必殺技,epoll、kqueue、master-workers、pool、buffer... ...,也涵蓋了不少web服務開發方面的技術,ssi、ssl、proxy、gzip、regex、load balancing、reconfiguration、hot code swapping... ...,還有一些經常使用的精巧的數據結構實現,全部的東西很主流;其次是一流的代碼組織結構和乾淨簡潔的代碼風格,尤爲是整個系統的命名恰到好處,可讀性至關高,這種風格值得學習和模仿;第三是經過閱讀源碼能夠感覺到做者嚴謹的做風和卓越的能力,能夠給本身增長動力,樹立榜樣的力量。程序員
另外一方面,要達到這些目標難度很高,必需要制定詳細的計劃和採起必定有效的方法。github
對於這麼大的一個系統,想一口氣知曉所有的細節是不可能的,而且nginx各個部分的實現之間關係緊密,不可能作到窺一斑而知全身,合適的作法彷佛應該是從main開始,先了解nginx的啓動過程的順序,而後進行問題分解,再逐個重點分析每個重要的部分。web
對每一個理解的關鍵部分進行詳細的記錄和整理也是很重要的,這也是這個源碼分析日誌系列所要完成的任務。正則表達式
爲了更深入的理解代碼實現的關鍵,修改代碼和寫一些測試用例是不可避免的,這就須要搭建一個方便調試的環境,這也比較容易,由於使用的linux系統自己就是一個自然的開發調試環境。
源碼分析是一個逐步取經的過程,最開始是一個大概瞭解的過程,各類認識不會太深入,可是把這些真實的感覺也記錄下來,以爲挺有意思的,可能有些認識是片面或者是不正確的,但能夠經過後面更深刻細緻的分析過程,不斷的糾正錯誤和深化理解。源碼分析是一個過程,經驗是逐步累積起來的,但願文字能夠把這種累積的感受也準確記錄下來。
如今就看看對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的過程,其實就是在編程,雖然剛開始有些不適應,但習慣以後確實很爽,殺手級的體驗感,很聽話。
源碼包解壓以後,根目錄下有幾個子目錄和幾個文件,最重要的子目錄是auto和src,最重要的文件是configure腳本。
不一樣於絕大多數的開源代碼,nginx的configure腳本是做者手工編寫的,沒有使用autoconf之類的工具去自動生成,configure腳本會引用auto目錄下面的腳本文件來幹活。
根據不一樣的用途,auto目錄下面的腳本各司其職,有檢查編譯器版本的,有檢查操做系統版本的,有檢查標準庫版本的,有檢查模塊依賴狀況的,有關於安裝的,有關於初始化的,有關於多線程檢查的等等。
configure做爲一個總驅動,調用這些腳本去生成版本信息頭文件、默認被包含的模塊的聲明代碼和Makefile文件,版本信息頭文件 (ngx_auto_config.h,ngx_auto_headers.h)和默認被包含的模塊的聲明代碼(ngx_modules.c)被放置在新建立的objs目錄下。
要注意的是,這幾個生成的文件和src下面的源代碼同樣重要,對於理解源碼是不可忽略的重要部分。
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管理事件。
利用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。
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命令就能夠直接生成指定格式的圖,很方便。
使用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。
配置文件能夠看作是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既不會繼承指令也不會被繼承。它有本身的特殊指令,不須要在其餘地方的應用。
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進程啓動>。
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
所以,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進程啓動>。
看源代碼時,會注意到有些宏(宏所有大寫,中間用下劃線隔開,這是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 |
還有一些開關,如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 |
本文簡單分析nginx的啓動過程。主要是main函數中的調用,在main中,只有頂層的調用,沒有接觸到細節。
Nginx自己支持多種模塊,如HTTP模塊、EVENT模塊和MAIL模塊,本文只討論HTTP模塊。
Nginx自己作的工做實際不多,當它接到一個HTTP請求時,它僅僅是經過查找配置文件將這次請求映射到一個location block,而此location中所配置的各個指令則會啓動不一樣的模塊去完成工做,所以模塊能夠看作Nginx真正的勞動工做者。一般一個location中的指令會涉及一個handler模塊和多個filter模塊(固然,多個location能夠複用同一個模塊)。handler模塊負責處理請求,完成響應內容的生成,而filter模塊對響應內容進行處理。所以Nginx模塊開發分爲handler開發和filter開發(本文不考慮load-balancer模塊)。下圖展現了一次常規請求和響應的過程。
Nginx的模塊有三種角色:
許多可能你認爲是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 完成與後端服務器的交互
難以置信!有這麼多的功能任你處置,而你只需僅僅經過多組有用的鉤子(由函數指針組成的結構體)和相應的實現函數。
要知道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是一個全局的結構體對象,其餘模塊類同。
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時被調用。
模塊的命令集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 } //空命令 |
nginx爲C語言開發的開源高性能web server,其代碼中大量使用了callback方式,例如模塊結構ngx_module_t中的init_master等。實際上,咱們能夠將ngx_module_t看做C++的一個類,其中的數據字段即是其屬性,而那些callback即是該類的操做。——這應該就是nginx的模塊化思想。畫出的ngx_module_t的類圖以下:
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, …. } |
這些模塊的組織結構圖以下所示,因模塊較多,圖中只畫出一部分有表明性的重要模塊:
在對全局數組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"。
即編譯期間完成的數據成員初始化。記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。所以,模塊的靜態初始化(數據成員初始化)實際上只是對模塊上下文、模塊命令集和模塊類型進行初始化。
即nginx運行(啓動)初期,對模塊自己的初始化。
對各個模塊的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爲模塊個數。
(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++; } /* ... */ } |
其餘的初始化工做,將在nginx啓動及其進程啓動分析中介紹。
本文主要講述了nginx的模塊及其初始化,包括全部模塊的組織,及模塊的靜態初始化和部分動態初始化。
Nginx使用本身的、單獨的方式去管理字符串類型。
使用的文件:src/core/string.h/c。
typedef struct { size_t len; //字符串長度 u_char* data; //指向字符串的指針 } ngx_str_t; #define ngx_null_string { 0, NULL } |
字符串類型結構簡單,在整個nginx中,處處使用。
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 |
把宏數字轉換成字符串 |
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 封裝建立/銷燬內存池,從內存池分配空間等函數
nginx對內存的管理均統一完成,例如,在特定的生命週期統一創建內存池(如main函數系統啓動初期即分配1024B大小的內存池),須要內存時統一分配內存池中的內存,在適當的時候釋放內存池的內存(如關閉http連接時調用ngx_destroy_pool進行銷燬)。
所以,開發者只需在須要內存時進行申請便可,不用過多考慮內存的釋放等問題,大大提升了開發的效率。先看一下內存池結構。
此處統一一下概念,內存池的數據塊:即分配內存在這些數據塊中進行,一個內存池能夠有多一個內存池數據塊。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; |
其餘與內存池相干的數據結構,如清除資源的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; ... } |
這些數據結構之間的關係,請參考後面的圖。
這些數據結構邏輯結構圖以下。注:本文采用UML的方式畫出該圖。
建立內存池有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)後,建立的內存池物理結構以下圖:
銷燬內存池由以下函數完成:void ngx_destroy_pool(ngx_pool_t *pool)。
該函數將遍歷內存池鏈表,全部釋放內存,若是註冊了clenup(也是一個鏈表結構),亦將遍歷該cleanup鏈表結構依次調用clenup的handler清理。同時,還將遍歷large鏈表,釋放大塊內存。
重置內存池由下面的函數完成:void ngx_reset_pool(ngx_pool_t *pool)。
該函數將釋放全部large內存,而且將d->last指針從新指向ngx_pool_t結構以後數據區的開始位置,同剛建立後的位置相同。
內存分配的函數以下。
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);
返回值爲分配的內存起始地址。選擇其中的兩個函數進行分析,其餘的也很好理解,省略。
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)後,該內存池物理結構以下圖。
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)以後數據區的起始位置。
請參考以下函數:ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)。
須要注意的是該函數只釋放large鏈表中註冊的內存,普通內存在ngx_destroy_pool中統一釋放。
請參考以下函數,該函數實現也很簡單,此處再也不贅述。
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
針對本文上節的例子,畫出的內存池的物理結構以下圖。
從該圖也能看出結論,即內存池第一塊內存前40字節爲ngx_pool_t結構,後續加入的內存塊前16個字節爲ngx_pool_data_t結構,這兩個結構以後即是真正能夠分配內存區域。
所以,本文Reference中的內存分配相關中的圖是有一點點小問題的,並非每個節點的前面都是ngx_pool_t結構。
理解並掌握開源軟件的最好方式莫過於本身寫一些測試代碼,或者改寫軟件自己,並進行調試來進一步理解開源軟件的原理和設計方法。本節給出一個建立內存池並從中分配內存的簡單例子。
/** * 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; } |
這個問題是編寫測試代碼或者改寫軟件自己最迫切須要解決的問題,不然,編寫的代碼無從編譯或運行,那也無從進行調試並理解軟件了。
如何對本身編寫的測試代碼進行編譯,可參考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 $@ |
# ./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 |
本文針對nginx-1.0.4的內存管理進行了較爲全面的分析,包括相關內存池數據結構,內存池的建立、銷燬,以及從內存池中分配內存等。最後經過一個簡單例子向讀者展現nginx內存池的建立和分配操做,同時藉此向讀者展現編譯測試代碼的方法。
分析完nginx的內存管理,你必定驚歎於nginx做者的聰明才智。這種內存管理的設計方法小巧、快捷,值得借鑑!
本文開始介紹nginx的容器,先從最簡單的數組開始。
數組實現文件:文件:./src/core/ngx_array.h/.c。
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)。
ngx_array_t結構引用了ngx_pool_t結構,所以本文參考內存池結構ngx_pool_t及內存管理一文畫出相關結構的邏輯圖,以下:
數組操做共有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個函數。
建立數組的操做實現以下,首先分配數組頭(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; //返回數組頭的起始位置 } |
建立數組後內存池的物理結構圖以下。
銷燬數組的操做實現以下,包括銷燬數組數據區和數組頭。這裏的銷燬動做實際上就是修改內存池的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指針 } } |
向數組添加元素的操做有兩個,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個整型元素後的一個例子。代碼可參考下文的例子。固然,數組元素也不只限於例子的整型數據,也能夠是其餘類型的數據,如結構體等。
/** * 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; } |
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 $@ |
# ./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 |
本文針對數組結構進行了較爲全面的分析,包括數組相關數據結構,數組的建立、銷燬,以及向數組中添加元素等。最後經過一個簡單例子向讀者展現nginx數組的建立、添加元素和銷燬操做,同時藉此向讀者展現編譯測試代碼的方法。
本文繼續介紹nginx的容器——鏈表。
鏈表實現文件:文件:src/core/ngx_list.h/.c。
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)。
ngx_list_t結構引用了ngx_pool_t結構,所以本文參考內存池結構ngx_pool_t及內存管理一文畫出相關結構的邏輯圖,以下:
鏈表操做共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) |
建立鏈表的操做實現以下,首先分配鏈表頭(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; //返回鏈表頭的起始位置 } |
建立鏈表後內存池的物理結構圖以下:
添加元素操做實現以下,同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個節點的鏈表的邏輯結構圖。
圖中的線太多,容易眼暈,下面這個圖可能好一些。
/** * 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; } |
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 $@ |
# ./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 |
本文針對nginx-1.0.4的容器——鏈表結構進行了較爲全面的分析,包括鏈表相關數據結構,鏈表建立和向鏈表中添加元素等。最後經過一個簡單例子向讀者展現nginx鏈表建立和添加元素操做,同時藉此向讀者展現編譯測試代碼的方法。
鏈表實現文件:文件:src/core/ngx_queue.h/.c。
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的隊列結構裏並無其節點的數據內容。
隊列有以下操做:
//初始化隊列 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*)) |
其中,插入節點、取隊列頭、取隊列尾等操做由宏實現,獲取中間節點、排序等操做由函數實現。
#define ngx_queue_init(q) \ (q)->prev = q; \ (q)->next = q |
在頭節點以後插入操做由宏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 |
畫出該操做的邏輯圖,以下:
圖中虛線表示被修改/刪除的指針,藍色表示新修改/增長的指針。
在尾節點以後插入操做由宏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 |
該操做的邏輯圖以下:
在尾節點以後插入操做由宏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 |
該操做的邏輯圖以下:
分割隊列操做由宏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,圖形演示以下:
連接隊列由宏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相關操做時再詳細敘述。
中間節點,若隊列有奇數個(除頭節點外)節點,則返回中間的節點;若隊列有偶數個節點,則返回後半個隊列的第一個節點。操做以下:
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指針每次後移一個位置(節點)。演示圖形以下:
隊列排序採用的是穩定的簡單插入排序方法,即從第一個節點開始遍歷,依次將當前節點(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節點以後(此處即爲簡單插入) } } |
該排序操做使用前面介紹的宏來完成其插入動做,只是一些簡單的修改指針指向的操做,效率較高。
由隊列基本結構和以上操做可知,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結構放在後面,故該宏使用減法計算整個節點結構的起始地址(須要進行類型轉換)。
數據結構以下:
給出一個建立內存池並從中分配隊列頭節點和其餘節點組成隊列的簡單例子。在該例中,隊列的數據是一系列的二維點(x,y分別表示該點的橫、縱座標),將這些點插入隊列後進行排序,以此向讀者展現nginx隊列的使用方法。
/** * 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; } |
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 $@ |
# ./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 |
該隊列的邏輯圖以下:
本文針對nginx-1.0.4的隊列進行了較爲全面的分析,包括隊列結構和隊列操做,隊列操做主要包括在頭節點以後插入節點、在尾節點以後插入節點、獲取中間節點、隊列排序等。最後經過一個簡單例子向讀者展現nginx隊列的使用方法,同時藉此向讀者展現編譯測試nginx代碼的方法。
鏈表實現文件:文件:src/core/ngx_hash.h/.c。
nginx的hash結構比其list、array、queue等結構稍微複雜一些。
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()函數中能夠看到,請參考後續的分析。該結構在模塊配置解析時常用。
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; |
該結構也主要用來保存要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模塊等,此處再也不講述,將在後續的文章中介紹。
ngx_hash_init_t結構引用了ngx_pool_t結構,所以本文參考內存池結構ngx_pool_t及內存管理一文畫出相關結構的邏輯圖,以下:
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字節對齊的大小。
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值。
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物理結構以下。具體的可參考後文的例子。
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字段便是所求。
本節給出一個建立內存池並從中分配hash結構、hash桶、hash元素並將鍵-值對<key,value>加入該hash結構的簡單例子。
在該例中,將完成這樣一個應用,將給定的多個url及其ip組成的二元組<url,ip>做爲<key,value>,初始化時對這些<url,ip>進行hash,而後根據給定的url查找其對應的ip地址,若沒有找到,則給出相關提示信息。以此向讀者展現nginx的hash使用方法。
/** * 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); } } } |
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 $@ |
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 |
本文針對nginx的hash結構進行了較爲全面的分析,包括hash結構、hash元素結構、hash初始化結構等,hash操做主要包括hash初始化、hash查找等。最後經過一個簡單例子向讀者展現nginx的hash使用方法,並給出詳細的運行結果,且畫出hash的物理結構圖,以此向圖這展現hash的設計、原理;同時藉此向讀者展現編譯測試nginx代碼的方法。
下面本文展現一個簡單的Nginx模塊開發全過程,咱們開發一個叫echo的handler模塊,這個模塊功能很是簡單,它接收「echo」指令,指令可指定一個字符串參數,模塊會輸出這個字符串做爲HTTP響應。例如,作以下配置:
location /echo { echo "hello nginx"; } |
則訪問http://hostname/echo時會輸出hello nginx。
直觀來看,要實現這個功能須要三步:一、讀入配置文件中echo指令及其參數;二、進行HTTP包裝(添加HTTP頭等工做);三、將結果返回給客戶端。下面本文將分部介紹整個模塊的開發過程。
首先咱們須要一個結構用於存儲從配置文件中讀進來的相關指令參數,即模塊配置信息結構。根據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。
一個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結束。
靜態的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。
它的入參是(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,分配內存,並初始化其中的數據,而後返回這個結構的指針。
下面的例子是個人模塊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列出了全部的日誌級別。)
接下來咱們間接地介紹更深一層:結構體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.)
下面的工做是編寫handler。handler能夠說是模塊中真正幹活的代碼。
Handler通常作4件事:
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代碼。
我已經幫你瞭解瞭如何讓你的handler來產生響應。有些時候你能夠用一小段C代碼就能夠獲得響應,可是一般狀況下你須要同另一臺server打交道(好比你正在寫一個用來實現某種網絡協議的模塊)。你固然能夠本身實現一套網絡編程的東東,可是若是你只收到部分的響應,須要等待餘下的響應數據,你會怎麼辦?你不會想阻塞整個事件處理循環吧?這樣會毀掉Nginx的良好性能!幸運的是,Nginx容許你在它處理後端服務器(叫作"upstreams")的機制上加入你的回調函數,所以你的模塊將能夠和其餘的server通訊,同時還不會妨礙其餘的請求。這一節將介紹模塊如何和一個upstream(如 Memcached, FastCGI,或者另外一個 HTTP server)通訊。
與其餘模塊的回調處理函數不同,upstream模塊的處理函數幾乎不作「實事」。它壓根不調用ngx_http_output_filter。它僅僅是告訴回調函數何時能夠向upstream server寫數據了,以及何時能從upstream server讀數據了。實際上它有6個可用的鉤子:
這些鉤子是怎麼勾上去的呢?下面是一個例子,簡單版本的代理模塊處理函數:
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。
假設我有一個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信息。
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的頭中。一個真正的代理模塊,會在頭信息的處理上作不少文章,不光是錯誤處理,作什麼徹底取決於你的想法。
好了,還記得我說過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的辦法,它不用阻塞主事件循環。
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模塊了。
Filter操做handler生成的響應。頭部filter操做HTTP頭,body 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),也能夠隨意處置。
由於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 -->/),可是緩衝區鏈確實是一個強大的構想,它可讓程序員漸進地處理數據,這使得客戶端能夠儘量早地獲得響應。可是依我來看,緩衝區鏈表實在須要一個更爲乾淨的接口,這樣程序員也能夠避免操做不一致狀態的鏈表。可是目前爲止,全部的操做風險都得本身控制。
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將給出一個錯誤的信息。這是一個單向的,錯誤快速返回的,只使用函數引用實現的鏈表!
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.對端釋放函數 能夠在與對應的後端服務器結束通訊以後更新統計信息 (成功或失敗)
指令聲明,既肯定了他們在哪裏生效又肯定了一旦流程遇到指令將要調用什麼函數。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 } |
上面的回調函數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 )。
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。
對端初始化函數每一個請求調用一次。它會構造一個數據結構,模塊會用這個數據結構來選擇合適的後端服務器;這個數據結構保存着和後端交互的重試次數,經過它能夠很容易的跟蹤連接失敗次數或者是計算好的哈希值。這個結構體習慣性地被命名爲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; } |
主要部分如今纔開始。模塊就是在這裏選擇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應該重試。
對端釋放函數在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的值,因此這樣是行之有效的。
許多應用程序不提供重試功能,或者在更高層的邏輯中進行了控制。但其實你也看到了,只需這麼幾行代碼這個功能就能夠實現了。
上面完成了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; } |
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
結果以下:
能夠看到模塊已經正常工做了,也能夠在瀏覽器中打開網址,就能夠看到結果:
本文只是簡要介紹了Nginx模塊的開發過程,因爲篇幅的緣由,不能面面俱到。由於目前Nginx的學習資料不多,若是讀者但願更深刻學習Nginx的原理及模塊開發,那麼閱讀源代碼是最好的辦法。在Nginx源代碼的core/下放有Nginx的核心代碼,對理解Nginx的內部工做機制很是有幫助,http/目錄下有Nginx HTTP相關的實現,http/module下放有大量內置http模塊,可供讀者學習模塊的開發,另外在http://wiki.nginx.org/3rdPartyModules上有大量優秀的第三方模塊,也是很是好的學習資料。