iocp/epool這類東西是用於與裝置通訊時擷取通知。
這裡的裝置在訪問硬碟上的檔案就是和檔案系統驅動打交道,訪問socket時則是和網路驅動(軟體->硬體)打交道。
os驅動運行模式一般是發送命令,等待命令完成,擷取結果。
層次類似這樣
user
-----------------------
kernel <=> driver
driver下面可以掛很多個,其具體實現跟使用者沒什麼關係, kernel會知道什麼時候可以認為操作實際完成。
曆史悠久的2種訪問方式
A. 阻塞訪問時(read/send),每次api call後,會等待操作完成再返回,此時使用者線程處於掛起狀態。白白浪費了處理器時間。
其工作模式(實際的通知模式等可能跟描述有差異,但流程上是這樣)
user->api -> 發送請求至kernel 掛起使用者線程
請求完成時
driver發送請求結果通知kernel,kernel 返回結果至api,恢複使用者線程。
B.非同步訪問:
user->api 根據使用者操作 查詢裝置讀寫狀態,根據狀態返回結果(成功返回資料,或失敗需要block)
這裡比較特殊的是讀。
例如訪問磁碟檔案,第一次讀請求肯定是沒資料的,因為kernel不知道你請求的是讀哪塊資料,不可能說開啟檔案,系統就馬上把檔案讀取到緩衝裡面。
所以讀檔案的第一次訪問,實際操作是發送讀取命令到driver,然後返回would block。
kernel收到返回資料後下次讀操作可以立刻完成。
對於可以主動產生資料的裝置(socket/串口之類),第一次讀就有可能立即完成。
-----------------------------------------------------------------------------------------------------------------------------------------
在裝置密集情況下:
同步模式顯然是很低效的,大部分cpu時間在等待裝置完成操作。
非同步模式要稍微好點,但是需要大量時間輪詢,裝置很多,io少時cpu消耗在查詢上面很大。
於是出現了selecl/pool模式用於查詢一堆裝置的讀寫狀態。然後標記可讀,或可寫的裝備供使用者操作。
在select/pool時每次需提交大量的裝置控制代碼(r/w/e控制代碼數組),而可能只返回其中一個,操作效率太低。
於是epool出現。
epool和select/pool不同在於,其關注的裝置列表是由系統核心維護 用epool_ctrl來增減修改關注裝置或需要查詢的狀態。
因此每次查詢時無需提交裝置列表。效率上面大大增加了。
不過epool
iocp使用了另外一種完全不同的通知方式。
通過iocp訪問裝置時,直接提交命令+使用者緩衝區至核心驅動,然後立即返回。此時使用者線程可以繼續運行做其他的事情。
使用者想知道命令完成狀態時,通過GetQueuedCompletionStatus來擷取完成的請求和資料。
所以使用者可以先提交一大堆io請求,然後慢慢處理返回。
iocp比epoll高效的原因在於,epool每次查詢後,返回的還是一個狀態數組,很可能大部分資料是沒用的,但是應用程式還是的迴圈來找有用的狀態。
而iocp每次返回的東西必定是有用的,使用者可以直接使用結果,減少了尋找這一步,所以對於大量裝置時效率比epool高
其他io通知方式:
kqueue 因為沒有用過,不敢妄加評論。
----------------------------------------------------------------------------------------------------------------------------------------------
關於iocp在socket伺服器上應用
首先,iocp這東西跟線程沒什麼關係,他只和裝置打交道。
所謂的iocp work thread說法本來就是有問題的,應該是thread worked with iocp, 也就是io+邏輯線程。
NumberOfConcurrentThreads 是指最多有多少個線程可以同時查詢這個iocp的狀態,多餘此數位線程查詢時全部掛起在GetQueuedCompletionStatus。
其次,io速度和處理速度的平衡性。
假設一個伺服器程式,每秒能接受 1m位元組的資料請求,但是其cpu上運算過程只能處理512k的資料運算,
那麼單純的提高io效率最終結果就是buffer溢出,伺服器崩潰。
現在來看看iocp socket在伺服器上的的2種使用方法。
A. 1個iocp對象,NumberOfConcurrentThreads =nCpu*2, threads workd with iocp=nCpu*2。
msdn裡面iocp推薦的做法,code project上的實現也是這種。
這種模式本身是沒有什麼問題的,但是因為誤解和錯誤的用法,會引起效率損失。
msdn裡面例子上面,iocp是對檔案操作,這和socket操作有細微的不同。
比如做檔案密碼編譯演算法。檔案可以劃分為等大小的小塊,然後提交讀取請求,擷取資料後計算,提交寫請求。這些都在 threads workd with iocp裡面完成。
因為其資料沒有前後相關性,thread可以完全的並發處理,此時最為高效。而且自動的平衡了io和處理速度。
同時在io速度低時,被喚醒的thread 可能永遠是第一個(iocp內部跟蹤了線程標誌,知道哪個是最近被喚醒的線程)。線程切換開銷最小。
此為理想狀態下的伺服器情況。
而現實是殘酷的,對於socket操作來說,因為收到資料有很大程度上的前後相關性,可能2個線程分別擷取了同一個串連的2段讀取資料,這樣的話,後一個線程只能等待前面一個線程完成操作,然後鎖定串連再做處理。
同時,每次到達的資料也可能不會是一個完整的可以處理資料區塊,此時還的另外發起讀請求。直到擷取到完整資料區段拼包處理。
此類模型代碼複雜,如果應用方式正確的話是個不錯的選擇。
錯誤的使用方法:
某些應用方法是把這整個模型作為一個io線程來用,然後單獨的一個或幾個邏輯線程處理輸入緩衝,那麼造成的問題是 邏輯處理時間為0,io效率最大化。 假設邏輯線程跟不上io速度的話,server 最終就是crash。(緩衝區一直增加)
B. n個iocp對象,n個線程,socket平均綁定至各iocp對象
n=1時,就是select/pool/epool的工作方式了。單線程,不會有包的前後順序問題,效率受邏輯處理速度影響。
n>1時,密集io情況下,少做了包順序檢查看起來比A方式效率要高些,但是如果鎖定操作比較多的話,反倒降低效率。只有一個線程,等待的話其他io也沒法處理了。
同時在請求較少時,線程切換開銷很大。
此模型最好的用法是作為單獨的io線程,只使用1個線程做專門的io, 其他線程做邏輯處理資料。
此時io線程邏輯處理時間基本沒有,io效率得以最大化。
另外對於簡單的如echo/socks這類協議的做server也很實用。
現在總結下2種方式:
A.n個(iocp+邏輯線程)
B.iocp io線程1個 + n個邏輯線程
測試:
測試環境
server和client一樣: win7 64 +8g ,100m網路。cpu i5 2500k
server端是一個簡單的登陸+echo協議。登陸請求成功後echo用戶端的請求內容。
client端登陸成功後,每100ms發送20byte請求內容,其中4byte是自訂協議的包頭。
2邊都使用了tcp_nodely 1,用來類比大量的ip包。
5k個串連。
初步估計單向流量為(20+20)*10*串連數。=2MB 16mbits
實際流量為56mbit
server端:
A方式實現,4線程 cpu消耗在20%左右,其中約3/4為kernel時間
B方式單線程實現(io和邏輯一起一個線程) 與上面差不多,看起來cpu消耗似乎低點,應該是跟線程切換有關
也就是說,如果用單獨的io線程來使用iocp,1個線程就是最優解,其他方法只會降低效率。
對於io線程的讀請求,應該由邏輯線程式控制制,io線程只控制需要組包時的讀請求。 避免緩衝溢出。
寫請求可以由io線程維護隊列(或者只計數)避免緩衝區溢位。
補充些iocp容易犯的錯誤:
1.acceptex
acceptEx不要傳遞額外的讀取buffer(recvdatalength=0),避免拒絕服務的攻擊
(惡意用戶端串連上不發送資料,此時io未完成,無法投遞新accpetEx,耗完後其他用戶端無法串連)
2.send/recv/read/write直接返回成功的時候,不要回收緩衝區contex,成功的調用iocp會有通知的。
只有在io調用失敗時才需要自己回收,其他的都在iocp result裡面處理。
3.overlapped和wsbuf有些說法是不要放在一起,會出現nobuf的error,經測試,這種說法沒有道理。
wsae_nobuf是因為太多掛起的io調用,把系統記憶體耗完了,跟記憶體鎖定沒關係。
唯一可能最佳化就是記憶體頁對齊的地方。