之前曾經使用 epoll 構建過一個輕量級的 tcp 服務框架:
一個工業級、跨平臺、輕量級的 tcp 網路服務框架:gevent
在除錯的程序中,發現一些 epoll 之前沒怎么注意到的特性,
a) iocp 是完全執行緒安全的,即同時可以有多個執行緒等待在 iocp 的完成佇列上;
而 epoll 不行,同時只能有一個執行緒執行 epoll_wait 操作,因此這里需要做一點處理,
網上有人使用 condition_variable + mutex 實作 leader-follower 執行緒模型,但我只用了一個 mutex 就實作了,
當有事件發生了,leader 執行緒在執行事件處理器之前 unlock 這個 mutex,
就可以允許等待在這個 mutex 上的其它執行緒中的一個進入 epoll_wait 從而擔任新的 leader,
(不知道多加一個 cv 有什么用,有明白原理的提示一下哈)
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 在異步事件完成后,會通過完成埠完成通知,但在某些情況下,異步操作可以“立即完成”,
就是說雖然只是提交異步事件,但是也有可能這個操作直接完成了,這種情況下,可以直接處理得到的資料,相當于是同步呼叫,
但是我要說的是,千萬不要直接處理資料,因為當你處理完之后,完成埠依舊會在之后進行通知,導致同一個資料被處理多次的情況,
所以最好的實踐就是,不論是否立即完成,都交給完成埠去處理,保證資料的一次性,
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/61693.html
標籤:Linux
上一篇:linux修改環境變數分析
