之前曾經(jīng)使用 epoll 構建過一個輕量級的 tcp 服務框架: 一個工業(yè)級、跨平臺、輕量級的 tcp 網(wǎng)絡服務框架:gevent
在調(diào)試的過程中,發(fā)現(xiàn)一些 epoll 之前沒怎么注意到的特性。 a) iocp 是完全線程安全的,即同時可以有多個線程等待在 iocp 的完成隊列上; 而 epoll 不行,同時只能有一個線程執(zhí)行 epoll_wait 操作,因此這里需要做一點處理, 網(wǎng)上有人使用 condition_variable + mutex 實現(xiàn) leader-follower 線程模型,但我只用了一個 mutex 就實現(xiàn)了, 當有事件發(fā)生了,leader 線程在執(zhí)行事件處理器之前 unlock 這個 mutex, 就可以允許等待在這個 mutex 上的其它線程中的一個進入 epoll_wait 從而擔任新的 leader。 ?。ú恢蓝嗉右粋€ cv 有什么用,有明白原理的提示一下哈)
b) epoll 在加入、刪除句柄時是可以跨線程的,而且這一操作是線程安全的。 之前一直以為 epoll 會像 select 一像,添加或刪除一個句柄需要先通知 leader 從 epoll_wait 中醒來, 在重新 wait 之前通過 epoll_ctl 添加或刪除對應的句柄。但是現(xiàn)在看完全可以在另一個線程中執(zhí)行 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 有兩種事件觸發(fā)方式,一種是默認的水平觸發(fā)(LT)模式,即只要有可讀的數(shù)據(jù),就一直觸發(fā)讀事件; 還有一種是邊緣觸發(fā)(ET)模式,即只在沒有數(shù)據(jù)到有數(shù)據(jù)之間觸發(fā)一次,如果一次沒有讀完全部數(shù)據(jù), 則也不會再次觸發(fā),除非所有數(shù)據(jù)被讀完,且又有新的數(shù)據(jù)到來,才觸發(fā)。使用 ET 模式的好處是, 不用在每次執(zhí)行處理器前將句柄從 epoll 移除、在執(zhí)行完之后再加入 epoll 中, ?。ㄈ绻贿@樣做的話,下一個進來的 leader 線程還會認為這個句柄可讀,從而導致一個連接的數(shù)據(jù)被多個線程同時處理) 從而導致頻繁的移除、添加句柄。好多網(wǎng)上的 epoll 例子也推薦這種方式。但是我在親自驗證后,發(fā)現(xiàn)使用 ET 模式有兩個問題:
1)如果連接上來了大量數(shù)據(jù),而每次只能讀取部分(緩存區(qū)限制),則第 N 次讀取的數(shù)據(jù)與第 N+1 次讀取的數(shù)據(jù), 有可能是兩個線程中執(zhí)行的,在讀取時它們的順序是可以保證的,但是當它們通知給用戶時,第 N+1 次讀取的數(shù)據(jù) 有可能在第 N 次讀取的數(shù)據(jù)之前送達給應用層。這是因為線程的調(diào)度導致的,雖然第 N+1 次數(shù)據(jù)只有在第 N 次數(shù)據(jù) 讀取完之后才可能產(chǎn)生,但是當?shù)?N+1 次數(shù)據(jù)所在的線程可能先于第 N 次數(shù)據(jù)所在的線程被調(diào)度,上述場景就會產(chǎn)生。 這需要細心的設計讀數(shù)據(jù)到給用戶之間的流程,防止線程搶占(需要加一些保證順序的鎖); 2)當大量數(shù)據(jù)發(fā)送結(jié)束時,連接中斷的通知(on_error)可能早于某些數(shù)據(jù)(on_read)到達,其實這個原理與上面類似, 就是客戶端在所有數(shù)據(jù)發(fā)送完成后主動斷開連接,而獲取連接中斷的線程可能先于末尾幾個數(shù)據(jù)所在的線程被調(diào)度, 從而在應用層造成混亂(on_error 一般會刪除事件處理器,但是 on_read 又需要它去做回調(diào),好的情況會造成一些 數(shù)據(jù)丟失,不好的情況下直接崩潰)
鑒于以上兩點,最后我還是使用了默認的 LT 觸發(fā)模式,幸好有 b) 特性,我僅僅是增加了一些移除、添加的代碼, 而且我不用在應用層加鎖來保證數(shù)據(jù)的順序性了。
d) 一定要捕捉 SIGPIPE 事件,因為當某些連接已經(jīng)被客戶端斷開時,而服務端還在該連接上 send 應答包時: 第一次 send 會返回 ECONNRESET(104),再 send 會直接導致進程退出。如果捕捉該信號后,則第二次 send 會返回 EPIPE(32)。 這樣可以避免一些莫名其妙的退出問題(我也是通過 gdb 掛上進程才發(fā)現(xiàn)是這個信號導致的)。
e) 當管理多個連接時,通常使用一種 map 結(jié)構來管理 socket 與其對應的數(shù)據(jù)結(jié)構(特別是回調(diào)對象:handler)。 但是不要使用 socket 句柄作為這個映射的 key,因為當一個連接中斷而又有一個新的連接到來時,linux 上傾向于用最小的 fd 值為新的 socket 分配句柄,大部分情況下,它就是你剛剛 close 或客戶端中斷的句柄。這樣一來很容易導致一些混亂的情況。 例如新的句柄插入失?。ㄒ驗榕f的雖然已經(jīng)關閉但是還未來得及從 map 中移除)、舊句柄的清理工作無意間關閉了剛剛分配的 新連接(清理時 close 同樣的 fd 導致新分配的連接中斷)……而在 win32 上不存在這樣的情況,這并不是因為 winsock 比 bsdsock 做的更好, 相同的, winsock 也存在新分配的句柄與之前剛關閉的句柄一樣的場景(當大量客戶端不停中斷重連時);而是因為 iocp 基于提前 分配的內(nèi)存塊作為某個 IO 事件或連接的依據(jù),而 map 的 key 大多也依據(jù)這些內(nèi)存地址構建,所以一般不存在重復的情況(只要還在 map 中就不釋放對應內(nèi)存)。
經(jīng)過觀察,我發(fā)現(xiàn)在 linux 上,即使新的連接占據(jù)了舊的句柄值,它的端口往往也是不同的,所以這里使用了一個三元組作為 map 的 key: { fd, local_port, remote_port } 當 fd 相同時,local_port 與 remote_port 中至少有一個是不同的,從而可以區(qū)分新舊連接。
f) 如果連接中斷或被對端主動關閉連接時,本端的 epoll 是可以檢測到連接斷開的,但是如果是自己 close 掉了 socket 句柄,則 epoll 檢測不到連接已斷開。 這個會導致客戶端在不停斷開重連過程中積累大量的未釋放對象,時間長了有可能導致資源不足從而崩潰。 目前還沒有找到產(chǎn)生這種現(xiàn)象的原因,Windows 上沒有這種情況,有清楚這個現(xiàn)象原因的同學,不吝賜教啊
最后,再亂入一波 iocp 的特性: iocp 在異步事件完成后,會通過完成端口完成通知,但在某些情況下,異步操作可以“立即完成”, 就是說雖然只是提交異步事件,但是也有可能這個操作直接完成了。這種情況下,可以直接處理得到的數(shù)據(jù),相當于是同步調(diào)用。 但是我要說的是,千萬不要直接處理數(shù)據(jù),因為當你處理完之后,完成端口依舊會在之后進行通知,導致同一個數(shù)據(jù)被處理多次的情況。 所以最好的實踐就是,不論是否立即完成,都交給完成端口去處理,保證數(shù)據(jù)的一次性。
|
|
來自: python_lover > 《待分類》