以前曾經使用 epoll 構建過一個輕量級的 tcp 服務框架:html
一個工業級、跨平臺、輕量級的 tcp 網絡服務框架:gevent linux
在調試的過程當中,發現一些 epoll 以前沒怎麼注意到的特性。緩存
a) iocp 是徹底線程安全的,即同時能夠有多個線程等待在 iocp 的完成隊列上;安全
而 epoll 不行,同時只能有一個線程執行 epoll_wait 操做,所以這裏須要作一點處理,bash
網上有人使用 condition_variable + mutex 實現 leader-follower 線程模型,但我只用了一個 mutex 就實現了,網絡
當有事件發生了,leader 線程在執行事件處理器以前 unlock 這個 mutex,數據結構
就能夠容許等待在這個 mutex 上的其它線程中的一個進入 epoll_wait 從而擔任新的 leader。多線程
(不知道多加一個 cv 有什麼用,有明白原理的提示一下哈)app
b) epoll 在加入、刪除句柄時是能夠跨線程的,並且這一操做是線程安全的。框架
以前一直覺得 epoll 會像 select 一像,添加或刪除一個句柄須要先通知 leader 從 epoll_wait 中醒來,
在從新 wait 以前經過 epoll_ctl 添加或刪除對應的句柄。可是如今看徹底能夠在另外一個線程中執行 epoll_ctl 操做
而不用擔憂多線程問題。這個在 man 手冊頁也有描述(man epoll_wait):
NOTES While one thread is blocked in a call to epoll_pwait(), it is possible for another thread to add a file descriptor to the waited-upon epoll instance. If the new file descriptor becomes ready, it will cause the epoll_wait() call to unblock. For a discussion of what may happen if a file descriptor in an epoll instance being monitored by epoll_wait() is closed in another thread, see select(2).
c) epoll 有兩種事件觸發方式,一種是默認的水平觸發(LT)模式,即只要有可讀的數據,就一直觸發讀事件;
還有一種是邊緣觸發(ET)模式,即只在沒有數據到有數據之間觸發一次,若是一次沒有讀徹底部數據,
則也不會再次觸發,除非全部數據被讀完,且又有新的數據到來,才觸發。使用 ET 模式的好處是,
不用在每次執行處理器前將句柄從 epoll 移除、在執行完以後再加入 epoll 中,
(若是不這樣作的話,下一個進來的 leader 線程還會認爲這個句柄可讀,從而致使一個鏈接的數據被多個線程同時處理)
從而致使頻繁的移除、添加句柄。好多網上的 epoll 例子也推薦這種方式。可是我在親自驗證後,發現使用 ET 模式有兩個問題:
1)若是鏈接上來了大量數據,而每次只能讀取部分(緩存區限制),則第 N 次讀取的數據與第 N+1 次讀取的數據,
有多是兩個線程中執行的,在讀取時它們的順序是能夠保證的,可是當它們通知給用戶時,第 N+1 次讀取的數據
有可能在第 N 次讀取的數據以前送達給應用層。這是由於線程的調度致使的,雖然第 N+1 次數據只有在第 N 次數據
讀取完以後纔可能產生,可是當第 N+1 次數據所在的線程可能先於第 N 次數據所在的線程被調度,上述場景就會產生。
這須要細心的設計讀數據到給用戶之間的流程,防止線程搶佔(須要加一些保證順序的鎖);
2)當大量數據發送結束時,鏈接中斷的通知(on_error)可能早於某些數據(on_read)到達,其實這個原理與上面相似,
就是客戶端在全部數據發送完成後主動斷開鏈接,而獲取鏈接中斷的線程可能先於末尾幾個數據所在的線程被調度,
從而在應用層形成混亂(on_error 通常會刪除事件處理器,可是 on_read 又須要它去作回調,好的狀況會形成一些
數據丟失,很差的狀況下直接崩潰)
鑑於以上兩點,最後我仍是使用了默認的 LT 觸發模式,幸虧有 b) 特性,我僅僅是增長了一些移除、添加的代碼,
並且我不用在應用層加鎖來保證數據的順序性了。
d) 必定要捕捉 SIGPIPE 事件,由於當某些鏈接已經被客戶端斷開時,而服務端還在該鏈接上 send 應答包時:
第一次 send 會返回 ECONNRESET(104),再 send 會直接致使進程退出。若是捕捉該信號後,則第二次 send 會返回 EPIPE(32)。
這樣能夠避免一些莫名其妙的退出問題(我也是經過 gdb 掛上進程才發現是這個信號致使的)。
e) 當管理多個鏈接時,一般使用一種 map 結構來管理 socket 與其對應的數據結構(特別是回調對象:handler)。
可是不要使用 socket 句柄做爲這個映射的 key,由於當一個鏈接中斷而又有一個新的鏈接到來時,linux 上傾向於用最小的
fd 值爲新的 socket 分配句柄,大部分狀況下,它就是你剛剛 close 或客戶端中斷的句柄。這樣一來很容易致使一些混亂的狀況。
例如新的句柄插入失敗(由於舊的雖然已經關閉可是還將來得及從 map 中移除)、舊句柄的清理工做無心間關閉了剛剛分配的
新鏈接(清理時 close 一樣的 fd 致使新分配的鏈接中斷)……而在 win32 上不存在這樣的狀況,這並非由於 winsock 比 bsdsock 作的更好,
相同的, winsock 也存在新分配的句柄與以前剛關閉的句柄同樣的場景(當大量客戶端不停中斷重連時);而是由於 iocp 基於提早
分配的內存塊做爲某個 IO 事件或鏈接的依據,而 map 的 key 大多也依據這些內存地址構建,因此通常不存在重複的狀況(只要還在 map 中就不釋放對應內存)。
通過觀察,我發如今 linux 上,即便新的鏈接佔據了舊的句柄值,它的端口每每也是不一樣的,因此這裏使用了一個三元組做爲 map 的 key:
{ fd, local_port, remote_port }
當 fd 相同時,local_port 與 remote_port 中至少有一個是不一樣的,從而能夠區分新舊鏈接。
f) 若是鏈接中斷或被對端主動關閉鏈接時,本端的 epoll 是能夠檢測到鏈接斷開的,可是若是是本身 close 掉了 socket 句柄,則 epoll 檢測不到鏈接已斷開。
這個會致使客戶端在不停斷開重連過程當中積累大量的未釋放對象,時間長了有可能致使資源不足從而崩潰。
目前尚未找到產生這種現象的緣由,Windows 上沒有這種狀況,有清楚這個現象緣由的同窗,不吝賜教啊
最後,再亂入一波 iocp 的特性:
iocp 在異步事件完成後,會經過完成端口完成通知,但在某些狀況下,異步操做能夠「當即完成」,
就是說雖然只是提交異步事件,可是也有可能這個操做直接完成了。這種狀況下,能夠直接處理獲得的數據,至關因而同步調用。
可是我要說的是,千萬不要直接處理數據,由於當你處理完以後,完成端口依舊會在以後進行通知,致使同一個數據被處理屢次的狀況。
因此最好的實踐就是,不管是否當即完成,都交給完成端口去處理,保證數據的一次性。