轉自 http://blog.csdn.net/shagoo/article/details/6531950php
上一篇《Socket深度探究4PHP(一)》中,你們應該對 poll/select/epoll/kqueue 這幾個 IO 模型有了必定的瞭解,爲了讓你們更深刻的理解 Socket 的技術內幕,在這個篇幅,我會對這幾種模式作一個比較詳細的分析和對比;另外,你們可能也同說過 AIO 的概念,這裏也會作一個簡單的介紹;最後咱們會對兩種主流異步模式 Reactor 和 Proactor 模式進行對比和討論。
首先,然咱們逐個介紹一下 2.6 內核(2.6.21.1)中的 poll/select/epoll/kqueue 這幾個 IO 模型。
> POLL
先說說 poll,poll 和 select 爲大部分 Unix/Linux 程序員所熟悉,這倆個東西原理相似,性能上也不存在明顯差別,但 select 對所監控的文件描述符數量有限制,因此這裏選用 poll 作說明。poll 是一個系統調用,其內核入口函數爲 sys_poll,sys_poll 幾乎不作任何處理直接調用 do_sys_poll,do_sys_poll 的執行過程能夠分爲三個部分:
一、將用戶傳入的 pollfd 數組拷貝到內核空間,由於拷貝操做和數組長度相關,時間上這是一個 O(n) 操做,這一步的代碼在 do_sys_poll 中包括從函數開始到調用 do_poll 前的部分。
二、查詢每一個文件描述符對應設備的狀態,若是該設備還沒有就緒,則在該設備的等待隊列中加入一項並繼續查詢下一設備的狀態。查詢完全部設備後若是沒有一個設備就緒,這時則須要掛起當前進程等待,直到設備就緒或者超時,掛起操做是經過調用 schedule_timeout 執行的。設備就緒後進程被通知繼續運行,這時再次遍歷全部設備,以查找就緒設備。這一步由於兩次遍歷全部設備,時間複雜度也是 O(n),這裏面不包括等待時間。相關代碼在 do_poll 函數中。
三、將得到的數據傳送到用戶空間並執行釋放內存和剝離等待隊列等善後工做,向用戶空間拷貝數據與剝離等待隊列等操做的的時間複雜度一樣是 O(n),具體代碼包括 do_sys_poll 函數中調用 do_poll 後到結束的部分。
可是,即使經過 select() 或者 poll() 函數複用事件通知具備突出的優勢,不過其餘具備相似功能的函數實現也能夠達到一樣的性能。然而,這些實如今跨平臺方面沒有實現標準化。你必須在使用這些特定函數實現同喪失可移植性之間進行權衡。咱們如今就討論一下兩個替代方法:Solaris 系統下的 /dev/poll 和 FreeBSD 系統下的 kqueue:
一、Solaris 系統下的 /dev/poll:在Solaris 7系統上,Sun引入了/dev/poll設備。在使用 /dev/poll的時候,你首先要打開/dev/poll做爲一個普通文件。而後構造pollfd結構,方式同普通的poll()函數調用同樣。這些 pollfd結構隨後寫入到打開的 /dev/poll 文件描述符。在打開句柄的生存週期內, /dev/poll會根據pollfd結構返回事件(注意,pollfd結構內的事件字段中的特定POLLREMOVE將從/dev/poll的列表中刪除對應的fd)。經過調用特定的ioctl (DP_POLL) 和dvpoll,程序就能夠從/dev/poll得到須要的信息。在使用dvpoll結構的狀況下,發生的事件就能夠被檢測到了。
二、FreeBSD 系統下的 kqueue:在FreeBSD 4.1中推出。FreeBSD的kqueue API設計爲比其餘對應函數提供更爲普遍的事件通知能力。kqueue API提供了一套通用過濾器,能夠模仿poll()語法(EVFILT_READ和EVFILT_WRITE)。不過,它還實現了文件系統變化(EVFILT_VNODE)、進程狀態變動(EVFILT_PROC)和信號交付(EVFILT_SIGNAL)的有關通知。
> EPOLL
接下來分析 epoll,與 poll/select 不一樣,epoll 再也不是一個單獨的系統調用,而是由 epoll_create/epoll_ctl/epoll_wait 三個系統調用組成,後面將會看到這樣作的好處。先來看 sys_epoll_create(epoll_create對應的內核函數),這個函數主要是作一些準備工做,好比建立數據結構,初始化數據並最終返回一個文件描述符(表示新建立的虛擬 epoll 文件),這個操做能夠認爲是一個固定時間的操做。epoll 是作爲一個虛擬文件系統來實現的,這樣作至少有如下兩個好處:
一、能夠在內核裏維護一些信息,這些信息在屢次 epoll_wait 間是保持的,好比全部受監控的文件描述符。
二、epoll 自己也能夠被 poll/epoll。
具體 epoll 的虛擬文件系統的實現和性能分析無關,再也不贅述。
在 sys_epoll_create 中還能看到一個細節,就是 epoll_create 的參數 size 在現階段是沒有意義的,只要大於零就行。
接着是 sys_epoll_ctl(epoll_ctl對應的內核函數),須要明確的是每次調用 sys_epoll_ctl 只處理一個文件描述符,這裏主要描述當 op 爲 EPOLL_CTL_ADD 時的執行過程,sys_epoll_ctl 作一些安全性檢查後進入 ep_insert,ep_insert 裏將 ep_poll_callback 作爲回掉函數加入設備的等待隊列(假定這時設備還沒有就緒),因爲每次 poll_ctl 只操做一個文件描述符,所以也能夠認爲這是一個 O(1) 操做。ep_poll_callback 函數很關鍵,它在所等待的設備就緒後被系統回掉,執行兩個操做:
一、將就緒設備加入就緒隊列,這一步避免了像 poll 那樣在設備就緒後再次輪詢全部設備找就緒者,下降了時間複雜度,由 O(n) 到 O(1)。
二、喚醒虛擬的 epoll 文件。
最後是 sys_epoll_wait,這裏實際執行操做的是 ep_poll 函數。該函數等待將進程自身插入虛擬 epoll 文件的等待隊列,直到被喚醒(見上面 ep_poll_callback 函數描述),最後執行 ep_events_transfer 將結果拷貝到用戶空間。因爲只拷貝就緒設備信息,因此這裏的拷貝是一個 O(1) 操做。
還有一個讓人關心的問題就是 epoll 對 EPOLLET 的處理,即邊沿觸發的處理,粗略看代碼就是把一部分水平觸發模式下內核作的工做交給用戶來處理,直覺上不會對性能有太大影響,感興趣的朋友歡迎討論。
> POLL/EPOLL 對比:
表面上 poll 的過程能夠看做是由一次 epoll_create,若干次 epoll_ctl,一次 epoll_wait,一次 close 等系統調用構成,實際上 epoll 將 poll 分紅若干部分實現的緣由正是由於服務器軟件中使用 poll 的特色(好比Web服務器):
一、須要同時 poll 大量文件描述符;
二、每次 poll 完成後就緒的文件描述符只佔全部被 poll 的描述符的不多一部分。
三、先後屢次 poll 調用對文件描述符數組(ufds)的修改只是很小;
傳統的 poll 函數至關於每次調用都重起爐竈,從用戶空間完整讀入 ufds,完成後再次徹底拷貝到用戶空間,另外每次 poll 都須要對全部設備作至少作一次加入和刪除等待隊列操做,這些都是低效的緣由。
epoll 將以上狀況都細化考慮,不須要每次都完整讀入輸出 ufds,只需使用 epoll_ctl 調整其中一小部分,不須要每次 epoll_wait 都執行一次加入刪除等待隊列操做,另外改進後的機制使的沒必要在某個設備就緒後搜索整個設備數組進行查找,這些都能提升效率。另外最明顯的一點,從用戶的使用來講,使用 epoll 沒必要每次都輪詢全部返回結果已找出其中的就緒部分,O(n) 變 O(1),對性能也提升很多。
此外這裏還發現一點,是否是將 epoll_ctl 改爲一次能夠處理多個 fd(像 semctl 那樣)會提升些許性能呢?特別是在假設系統調用比較耗時的基礎上。不過關於系統調用的耗時問題還會在之後分析。
> POLL/EPOLL 測試數據對比:
測試的環境:我寫了三段代碼來分別模擬服務器,活動的客戶端,僵死的客戶端,服務器運行於一個自編譯的標準 2.6.11 內核系統上,硬件爲 PIII933,兩個客戶端各自運行在另外的 PC 上,這兩臺PC比服務器的硬件性能要好,主要是保證能輕易讓服務器滿載,三臺機器間使用一個100M交換機鏈接。
服務器接受並poll全部鏈接,若是有request到達則回覆一個response,而後繼續poll。
活動的客戶端(Active Client)模擬若干併發的活動鏈接,這些鏈接不間斷的發送請求接受回覆。
僵死的客戶端(zombie)模擬一些只鏈接但不發送請求的客戶端,其目的只是佔用服務器的poll描述符資源。
測試過程:保持10個併發活動鏈接,不斷的調整僵併發鏈接數,記錄在不一樣比例下使用 poll 與 epoll 的性能差異。僵死併發鏈接數根據比例分別是:0,10,20,40,80,160,320,640,1280,2560,5120,10240。
下圖中橫軸表示僵死併發鏈接與活動併發鏈接之比,縱軸表示完成 40000 次請求回覆所花費的時間,以秒爲單位。紅色線條表示 poll 數據,綠色表示 epoll 數據。能夠看出,poll 在所監控的文件描述符數量增長時,其耗時呈線性增加,而 epoll 則維持了一個平穩的狀態,幾乎不受描述符個數影響。
可是要注意的是在監控的全部客戶端都是活動時,poll 的效率會略高於 epoll(主要在原點附近,即僵死併發鏈接爲0時,圖上不易看出來),究竟 epoll 實現比 poll 複雜,監控少許描述符並不是它的長處。
> epoll 的優勢綜述
一、支持一個進程打開大數目的socket描述符(FD):select 最不能忍受的是一個進程所打開的 FD 是有必定限制的,由 FD_SETSIZE 設置,在 Linux 中,這個值是 1024。對於那些須要支持的上萬鏈接數目的網絡服務器來講顯然太少了。這時候你一是能夠選擇修改這個宏而後從新編譯內核,不過資料也同時指出這樣會帶來網絡效率的降低,二是能夠選擇多進程的解決方案(傳統的 Apache 方案),不過雖然 linux 上面建立進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,因此也不是一種完美的方案。不過 epoll 則沒有這個限制,它所支持的 FD 上限是最大能夠打開文件的數目,這個數字通常遠大於 1024,舉個例子,在 1GB 內存的機器上大約是 10 萬左右,具體數目能夠 cat /proc/sys/fs/file-max 察看,通常來講這個數目和系統內存關係很大。
二、IO 效率不隨 FD 數目增長而線性降低:傳統的 select/poll 另外一個致命弱點就是當你擁有一個很大的 socket 集合,不過因爲網絡延時,任一時間只有部分的 socket 是"活躍"的,可是 select/poll 每次調用都會線性掃描所有的集合,致使效率呈現線性降低。可是 epoll 不存在這個問題,它只會對"活躍"的 socket 進行操做---這是由於在內核實現中 epoll 是根據每一個 fd 上面的 callback 函數實現的。那麼,只有"活躍"的 socket 纔會主動的去調用 callback 函數,其餘 idle 狀態 socket 則不會,在這點上,epoll 實現了一個"僞"AIO,由於這時候推進力在 os 內核。在一些 benchmark 中,若是全部的 socket 基本上都是活躍的 --- 好比一個高速LAN環境,epoll 並不比 select/poll 有什麼效率,相反,若是過多使用 epoll_ctl,效率相比還有稍微的降低。可是一旦使用 idle connections 模擬 WAN 環境,epoll 的效率就遠在 select/poll 之上了。
三、使用 mmap 加速內核與用戶空間的消息傳遞:這點實際上涉及到 epoll 的具體實現了。不管是 select,poll 仍是 epoll 都須要內核把 FD 消息通知給用戶空間,如何避免沒必要要的內存拷貝就很重要,在這點上,epoll 是經過內核於用戶空間 mmap 同一塊內存實現的。而若是你想我同樣從 2.5 內核就關注 epoll 的話,必定不會忘記手工 mmap 這一步的。
四、內核微調:這一點其實不算 epoll 的優勢了,而是整個 linux 平臺的優勢。也許你能夠懷疑 linux 平臺,可是你沒法迴避 linux 平臺賦予你微調內核的能力。好比,內核 TCP/IP 協議棧使用內存池管理 sk_buff 結構,那麼能夠在運行時期動態調整這個內存 pool(skb_head_pool) 的大小 --- 經過 echo XXXX > /proc/sys/net/core/hot_list_length 完成。再好比 listen 函數的第 2 個參數(TCP 完成 3 次握手的數據包隊列長度),也能夠根據你平臺內存大小動態調整。更甚至在一個數據包面數目巨大但同時每一個數據包自己大小卻很小的特殊系統上嘗試最新的 NAPI 網卡驅動架構。
> AIO 和 Epoll
epoll 和 aio(這裏的aio是指linux 2.6內核後提供的aio api)的區別:
一、aio 是異步非阻塞的。實際上是aio是用線程池實現了異步IO。
二、epoll 在這方面的定義上有點複雜,首先 epoll 的 fd 集裏面每個 fd 都是非阻塞的,可是 epoll(包括 select/poll)在調用時阻塞等待 fd 可用,而後 epoll 只是一個異步通知機制,只是在 fd 可用時通知你,並無作任何 IO 操做,因此不是傳統的異步。
在這方面,Windows 無疑是前行者,固然 Boost C++ 庫已經實現了 linux 下 aio 的機制,有興趣的朋友能夠參考:http://stlchina.huhoo.NET/twiki/bin/view.pl/Main/WebHome
> Reactor 和 Proactor
通常地,I/O多路複用機制都依賴於一個事件多路分離器(Event Demultiplexer)。分離器對象可未來自事件源的I/O事件分離出來,並分發到對應的read/write事件處理器(Event Handler)。開發人員預先註冊須要處理的事件及其事件處理器(或回調函數);事件分離器負責將請求事件傳遞給事件處理器。兩個與事件分離器有關的模式是Reactor和Proactor。Reactor模式採用同步IO,而Proactor採用異步IO。
在Reactor中,事件分離器負責等待文件描述符或socket爲讀寫操做準備就緒,而後將就緒事件傳遞給對應的處理器,最後由處理器負責完成實際的讀寫工做。而在Proactor模式中,處理器--或者兼任處理器的事件分離器,只負責發起異步讀寫操做。IO操做自己由操做系統來完成。傳遞給操做系統的參數須要包括用戶定義的數據緩衝區地址和數據大小,操做系統才能從中獲得寫出操做所需數據,或寫入從socket讀到的數據。事件分離器捕獲IO操做完成事件,而後將事件傳遞給對應處理器。好比,在windows上,處理器發起一個異步IO操做,再由事件分離器等待IOCompletion事件。典型的異步模式實現,都創建在操做系統支持異步API的基礎之上,咱們將這種實現稱爲「系統級」異步或「真」異步,由於應用程序徹底依賴操做系統執行真正的IO工做。
舉個例子,將有助於理解Reactor與Proactor兩者的差別,以讀操做爲例(類操做相似)。
在Reactor中實現讀:
- 註冊讀就緒事件和相應的事件處理器
- 事件分離器等待事件
- 事件到來,激活分離器,分離器調用事件對應的處理器。
- 事件處理器完成實際的讀操做,處理讀到的數據,註冊新的事件,而後返還控制權。
與以下Proactor(真異步)中的讀過程比較:
- 處理器發起異步讀操做(注意:操做系統必須支持異步IO)。在這種狀況下,處理器無視IO就緒事件,它關注的是完成事件。
- 事件分離器等待操做完成事件
- 在分離器等待過程當中,操做系統利用並行的內核線程執行實際的讀操做,並將結果數據存入用戶自定義緩衝區,最後通知事件分離器讀操做完成。
- 事件分離器呼喚處理器。
- 事件處理器處理用戶自定義緩衝區中的數據,而後啓動一個新的異步操做,並將控制權返回事件分離器。
對於不提供異步 IO API 的操做系統來講,這種辦法能夠隱藏 Socket API 的交互細節,從而對外暴露一個完整的異步接口。藉此,咱們就能夠進一步構建徹底可移植的,平臺無關的,有通用對外接口的解決方案。上述方案已經由Terabit P/L公司實現爲 TProactor (ACE compatible Proactor) :http://www.terabit.com.au/solutions.PHP。正是由於 linux 對 aio 支持的不完整,因此 ACE_Proactor 框架在 linux 上的表現不好,大部分在 windows 上執行正常的代碼,在 linux 則運行異常,甚至不能編譯經過。這個問題一直困擾着很大多數 ACE 的用戶,如今好了,有一個 TProactor 幫助解決了在 Linux 不完整支持 AIO 的條件下,正常使用(至少是看起來正常)ACE_Proactor。TProactor 有兩個版本:C++ 和 Java 的。C++ 版本採用 ACE 跨平臺底層類開發,爲全部平臺提供了通用統一的主動式異步接口。Boost.Asio 庫,也是採起了相似的這種方案來實現統一的 IO 異步接口。
如下是一張 TProactor 架構設計圖,有興趣的朋友能夠看看:
到這裏,第二部分的內容結束了,相信你們對 Socket 的底層技術原理有了一個更深層次的理解,在下一篇《Socket深度探究4PHP(三)》我將會深刻 PHP 源代碼,探究一下 PHP 在 Socket 這部分的一些技術內幕,而後介紹一下目前在這個領域比較活躍的項目(Node.js)。java