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