面試題:BIO,NIO,AIO 的區(qū)別是什么?說說 select 和 epoll 工作機(jī)制與差異?為何 epoll 如此高效
面試官:說說看你知道的IO模型有哪些?
先說說看什么是IO吧。IO表示輸入輸出,訪問外部設(shè)備讀取數(shù)據(jù)或者向外部設(shè)備寫數(shù)據(jù)。常見的文件讀寫操作、網(wǎng)絡(luò)通信操作,其實(shí)都是IO的過程。
這里以阻塞式讀取為例,分析IO操作所進(jìn)行的工作:
- 當(dāng)read/recv時,如果緩沖區(qū)中沒有數(shù)據(jù),那就阻塞式等待數(shù)據(jù)就緒 -- 等待。
- 當(dāng)read/recv時,如果緩沖區(qū)中有數(shù)據(jù),那么就將數(shù)據(jù)從緩沖區(qū)拷貝到應(yīng)用層 -- 拷貝。
因此,我們可以認(rèn)為,IO = 等待 + 拷貝。只要執(zhí)行流(進(jìn)程/線程)參與了等待和拷貝其中之一,那么就認(rèn)為這個執(zhí)行流進(jìn)行了IO操作。
下圖展示了在進(jìn)行寫文件操作時系統(tǒng)所進(jìn)行的工作,可分為三步:
再來說IO模型,經(jīng)典的IO模型主要包括以下幾種:
一、阻塞IO(BIO)
定義:在BIO模型中,當(dāng)程序進(jìn)行IO操作時,它會持續(xù)等待直到操作完成。在此期間,線程被阻塞,不能執(zhí)行其他任務(wù)。
特點(diǎn):
- 傳統(tǒng)的IO模型,適用于連接請求較少且請求處理簡單的場景。
- 在這種模型中,服務(wù)器為每個客戶端連接創(chuàng)建一個線程,導(dǎo)致線程數(shù)量隨著客戶端的增加而急劇增加,消耗大量系統(tǒng)資源。
二、非阻塞IO(NIO)
定義:為了解決BIO模型的線程阻塞問題,NIO模型引入了非阻塞的概念。在NIO中,當(dāng)一個線程進(jìn)行IO操作時,它不會等待操作完成,而是繼續(xù)執(zhí)行其他任務(wù)。當(dāng)IO操作完成時,線程會收到通知。非阻塞式IO一般采用輪詢檢查的方法進(jìn)行IO操作,即:通過循環(huán),不斷檢查IO資源是否已經(jīng)就緒,就緒就讀取,不就緒就執(zhí)行其他的工作。
特點(diǎn):
- 提高了線程的利用率,適用于連接請求多且單個請求處理耗時較長的場景。
- 需要不斷輪詢或者使用事件通知來檢查操作是否完成,可能會占用一定的CPU時間。
- Java的NIO庫提供了基于Reactor設(shè)計模式的非阻塞IO操作,也稱為IO多路復(fù)用。
三、異步IO(AIO)
定義:異步IO就是發(fā)起IO的執(zhí)行流本身不參與IO工作,而是有另外一個執(zhí)行流來進(jìn)行IO操作,發(fā)起IO的線程只需要等待進(jìn)行IO的執(zhí)行流反饋回結(jié)果。這樣發(fā)起IO的執(zhí)行流就不需要等到,可以處理其他的工作。當(dāng)操作完成時,系統(tǒng)會通知該線程。
特點(diǎn):
- 能夠顯著提高并發(fā)性能,因?yàn)槎鄠€操作可以同時進(jìn)行。
- 適用于高并發(fā)、高吞吐量的場景,如網(wǎng)絡(luò)服務(wù)器、大規(guī)模并行計算等。
- Java的NIO2(即AIO)基于Proactor設(shè)計模式的異步非阻塞模型。
四、IO多路復(fù)用
定義:使用操作系統(tǒng)提供的select、poll或epoll等多路復(fù)用機(jī)制,允許應(yīng)用程序同時監(jiān)視多個IO事件。
特點(diǎn):
- 應(yīng)用程序可以將多個IO請求注冊到一個多路復(fù)用器上,然后通過輪詢或者阻塞等待多路復(fù)用器通知事件的發(fā)生。
- 適用于需要同時處理多個連接的場景,提高了系統(tǒng)的并發(fā)性能。
- 有效地管理多個IO操作,減少系統(tǒng)資源的消耗。
五、信號驅(qū)動IO
定義:通過自定義對SIGIO信號的處理函數(shù)來實(shí)現(xiàn)信號驅(qū)動式IO,當(dāng)進(jìn)程收到SIGIO信號的時候,就調(diào)用對應(yīng)的處理函數(shù)來進(jìn)行IO操作,這樣保證在調(diào)用IO接口的時候數(shù)據(jù)一定是就緒的,在沒有收到信號時不影響進(jìn)程進(jìn)行其他的工作,信號驅(qū)動式IO避免了阻塞等待資源就緒,提高了IO效率。
特點(diǎn):
- 相比阻塞IO和非阻塞IO更為靈活。
- 適用于需要處理多個IO事件的場景。
面試官:阻塞IO(BIO)和非阻塞IO(NIO)的區(qū)別是什么?NIO和異步IO(AIO)的區(qū)別是什么?
一、以下是 BIO 和 NIO 的主要差異:
(1) 阻塞IO(Blocking IO)
- 等待操作完成:在阻塞IO模型中,當(dāng)一個線程發(fā)起一個I/O請求(如讀取文件、網(wǎng)絡(luò)通信等)時,該線程會被阻塞,直到I/O操作完成并且數(shù)據(jù)已經(jīng)準(zhǔn)備好。
- 線程占用:在I/O操作進(jìn)行期間,線程無法執(zhí)行其他任務(wù),導(dǎo)致線程的利用率降低。如果I/O操作耗時較長,線程可能會長時間處于空閑狀態(tài)。
- 簡單性:阻塞IO模型相對簡單,因?yàn)殚_發(fā)者不需要處理I/O操作的異步通知和狀態(tài)檢查。
- 資源消耗:在高并發(fā)場景下,阻塞IO模型可能導(dǎo)致大量的線程被創(chuàng)建和阻塞,從而消耗大量的系統(tǒng)資源(如內(nèi)存和CPU)。
(2) 非阻塞IO(Non-blocking IO)
- 立即返回:在非阻塞IO模型中,當(dāng)一個線程發(fā)起一個I/O請求時,該請求會立即返回,不會阻塞線程。線程可以繼續(xù)執(zhí)行其他任務(wù),而無需等待I/O操作完成。
- 輪詢或回調(diào):非阻塞IO模型通常需要通過輪詢(polling)或回調(diào)(callback)機(jī)制來檢查I/O操作的狀態(tài)。輪詢是主動檢查I/O操作是否完成,而回調(diào)是當(dāng)I/O操作完成時由系統(tǒng)通知應(yīng)用程序。
- 復(fù)雜性:非阻塞IO模型相對復(fù)雜,因?yàn)殚_發(fā)者需要處理I/O操作的異步通知和狀態(tài)檢查,以及管理多個I/O請求的狀態(tài)。
- 資源效率:在高并發(fā)場景下,非阻塞IO模型可以更有效地利用系統(tǒng)資源,因?yàn)榫€程不會被阻塞,可以處理更多的并發(fā)請求。然而,由于需要頻繁地檢查I/O操作的狀態(tài),可能會增加CPU的使用率。
(3) 選擇
- 阻塞IO適用于連接請求較少且請求處理簡單的場景,或者當(dāng)I/O操作不會顯著影響程序性能時。
- 非阻塞IO適用于連接請求多且單個請求處理耗時較長的場景,或者當(dāng)需要處理大量并發(fā)請求時。
二、以下是NIO和AIO的差異:
(1) 工作原理
NIO:
- 同步非阻塞I/O模型。
- 使用一個單獨(dú)的線程或線程池來處理所有的I/O操作。
- 當(dāng)一個操作不能立即完成時,線程不會被阻塞,而是繼續(xù)執(zhí)行其他任務(wù)。
- 使用緩沖區(qū)(Buffer)來存儲數(shù)據(jù),并通過選擇器(Selector)來監(jiān)聽多個通道(Channel)上的事件,從而實(shí)現(xiàn)并發(fā)處理多個I/O操作。
AIO:
- 異步非阻塞I/O模型。
- 不需要通過選擇器來監(jiān)聽通道的事件,而是由操作系統(tǒng)在數(shù)據(jù)準(zhǔn)備就緒時通知應(yīng)用程序。
- 應(yīng)用程序在數(shù)據(jù)讀寫操作時不會被阻塞,而是在操作完成時收到通知。
- 引入了異步通道的概念,簡化了程序編寫,并提高了并發(fā)處理能力。
(2) 性能與資源消耗
NIO:
- 通過并發(fā)處理多個I/O操作來提高性能。
- 需要合理地管理線程池和緩沖區(qū)資源,以避免資源耗盡或性能下降。
AIO:
- 通過異步操作來避免線程阻塞,從而提高了系統(tǒng)的并發(fā)處理能力。
- 在高并發(fā)場景下,AIO通常能夠表現(xiàn)出更好的性能,因?yàn)樗軌蚋行У乩孟到y(tǒng)資源。
(3) 編程復(fù)雜度
NIO:
- 編程相對復(fù)雜,需要處理緩沖區(qū)、選擇器、通道等組件的交互。
AIO:
- 編程復(fù)雜度也較高,但相對于NIO來說,由于引入了異步操作的概念,可以更加靈活地處理I/O操作。
- 開發(fā)者需要熟悉AIO的編程模型,并能夠正確地處理異步通知和回調(diào)機(jī)制。
面試官:能詳細(xì)說一下多路復(fù)用中 select 和 epoll 的工作機(jī)制和性能差異嗎?
先說說多路復(fù)用。多路復(fù)用(Multiplexing)是一種能夠同時監(jiān)控多個文件描述符(如網(wǎng)絡(luò)連接、文件、管道等),一旦某個描述符就緒(通常指有數(shù)據(jù)可讀或可寫),便能夠通知程序進(jìn)行相應(yīng)的讀寫操作的技術(shù)。
select和epoll是兩種常見的I/O多路復(fù)用機(jī)制,它們的工作機(jī)制和性能差異如下:
一、select的工作機(jī)制
基本原理:
- select函數(shù)允許一個進(jìn)程同時監(jiān)視多個文件描述符,以查看它們是否有I/O事件發(fā)生(如可讀、可寫或有異常)。
- 它通過監(jiān)視一個文件描述符集合,在其中的文件描述符中有I/O事件發(fā)生時返回。
使用方法:
- 調(diào)用select時,需要指定三個文件描述符集合:讀集合、寫集合和異常集合,以及一個時間限制。
- select函數(shù)會阻塞,直到至少有一個文件描述符準(zhǔn)備好進(jìn)行I/O操作,或者超時發(fā)生。
- 當(dāng)select返回時,它會通過修改這三個集合來指示哪些文件描述符已經(jīng)準(zhǔn)備好進(jìn)行I/O操作。
性能瓶頸:
- 當(dāng)監(jiān)視的文件描述符數(shù)量很大時,select的性能會顯著下降,因?yàn)樗枰闅v整個文件描述符集合來查找哪些文件描述符已經(jīng)準(zhǔn)備好進(jìn)行I/O操作。
- select有一個內(nèi)置的文件描述符數(shù)量限制(通常是1024個),這在某些高并發(fā)場景下可能不夠用。
- select的超時參數(shù)是一個時間間隔,而不是一個精確的時間點(diǎn),這可能導(dǎo)致在某些情況下時間控制不準(zhǔn)確。
二、epoll的工作機(jī)制
基本原理:
- epoll是Linux內(nèi)核提供的一種I/O多路復(fù)用機(jī)制,是select和poll機(jī)制的改進(jìn)版。
- 它使用事件驅(qū)動的方式,只通知那些真正發(fā)生了I/O事件的文件描述符。
核心API:
- epoll_create():創(chuàng)建一個epoll實(shí)例,并返回一個文件描述符。
- epoll_ctl():將需要監(jiān)控的文件描述符添加到epoll實(shí)例中,或從epoll實(shí)例中刪除,或?qū)ΡO(jiān)聽事件進(jìn)行修改。
- epoll_wait():等待注冊的事件發(fā)生,返回事件的數(shù)目,并將觸發(fā)的事件寫入事件數(shù)組中。
工作流程:
- 應(yīng)用程序通過epoll_create()系統(tǒng)調(diào)用創(chuàng)建一個epoll實(shí)例。
- 應(yīng)用程序通過epoll_ctl()系統(tǒng)調(diào)用將需要監(jiān)控的文件描述符添加到epoll實(shí)例中。
- 內(nèi)核會將這些文件描述符及其感興趣的事件類型(可讀、可寫、異常等)記錄在內(nèi)核的事件表中。
- 應(yīng)用程序通過epoll_wait()系統(tǒng)調(diào)用進(jìn)入休眠狀態(tài),等待內(nèi)核通知有事件發(fā)生。
- 內(nèi)核會不斷監(jiān)控這些文件描述符的狀態(tài)變化,一旦有事件發(fā)生,就會將就緒的文件描述符添加到就緒隊(duì)列中。
- 當(dāng)epoll_wait()被喚醒時,內(nèi)核會返回就緒隊(duì)列中的文件描述符列表以及就緒的事件類型。
- 應(yīng)用程序根據(jù)返回的信息來處理對應(yīng)的I/O操作。
性能優(yōu)勢:
- epoll的時間復(fù)雜度接近O(1),而select是O(n),因此在處理大量文件描述符時,epoll具有明顯的性能優(yōu)勢。
- epoll沒有文件描述符數(shù)量的限制,可以支持海量的文件描述符。
- epoll支持邊緣觸發(fā)(Edge Triggered)和水平觸發(fā)(Level Triggered)兩種工作模式,提供了更靈活的事件通知機(jī)制。
三、select與epoll的性能比較
時間復(fù)雜度:
- select的時間復(fù)雜度為O(n),需要遍歷整個文件描述符集合來查找就緒的文件描述符。
- epoll的時間復(fù)雜度接近O(1),因?yàn)樗褂脙?nèi)核事件表來存儲需要監(jiān)控的文件描述符,并只返回就緒的文件描述符。
文件描述符數(shù)量限制:
- select有文件描述符數(shù)量的限制(通常是1024個),這在某些高并發(fā)場景下可能不夠用。
- epoll沒有文件描述符數(shù)量的限制,可以支持海量的文件描述符。
系統(tǒng)資源消耗:
- 當(dāng)監(jiān)視的文件描述符數(shù)量很大時,select會消耗大量的系統(tǒng)資源,因?yàn)樗枰獜?fù)制整個文件描述符集合到用戶空間。
- epoll則不會消耗大量的系統(tǒng)資源,因?yàn)樗恍枰谖募枋龇麪顟B(tài)發(fā)生變化時才更新內(nèi)核事件表。
可擴(kuò)展性:
- 由于select的性能瓶頸和文件描述符數(shù)量限制,它在高并發(fā)場景下可能無法提供足夠的可擴(kuò)展性。
- epoll則具有良好的可擴(kuò)展性,可以支持大量的并發(fā)連接和文件描述符。
面試官:epoll底層的數(shù)據(jù)結(jié)構(gòu)什么,請結(jié)合epoll的底層結(jié)構(gòu)說說epoll的工作機(jī)制。以及為什么epoll如此高效?
epoll底層主要使用了紅黑樹和就緒隊(duì)列(雙鏈表)這兩種數(shù)據(jù)結(jié)構(gòu),其高效性主要源于以下幾個方面:
一、epoll底層數(shù)據(jù)結(jié)構(gòu)
紅黑樹:
- 用于存儲所有添加到epoll中的需要監(jiān)控的事件。
- 紅黑樹的插入、刪除和查找操作的時間復(fù)雜度都是O(log n),其中n是樹中節(jié)點(diǎn)的數(shù)量。這使得epoll能夠高效地管理大量的文件描述符。
就緒隊(duì)列(雙鏈表):
- 存放著將要通過epoll_wait返回給用戶的滿足條件的事件。
- 當(dāng)有I/O事件發(fā)生時,內(nèi)核會將對應(yīng)的事件添加到就緒隊(duì)列中。
- epoll_wait在調(diào)用時只需要檢查就緒隊(duì)列中是否有事件即可,無需遍歷整個紅黑樹。
再聊聊epoll的具體實(shí)現(xiàn)機(jī)制。
如下圖所示:
當(dāng)某一進(jìn)程調(diào)用epoll_create方法時,Linux內(nèi)核會創(chuàng)建一個eventpoll結(jié)構(gòu)體,這個結(jié)構(gòu)體中有兩個成員與epoll的使用方式密切相關(guān)。
struct eventpoll{
....
/*紅黑樹的根節(jié)點(diǎn),這顆樹中存儲著所有添加到epoll中的需要監(jiān)控的事件*/
struct rb_root rbr;
/*雙鏈表中則存放著將要通過epoll_wait返回給用戶的滿足條件的事件*/
struct list_head rdlist;
....
};
每一個epoll對象都有一個獨(dú)立的eventpoll結(jié)構(gòu)體,用于存放通過epoll_ctl方法向epoll對象中添加進(jìn)來的事件。這些事件都會掛載在紅黑樹中,重復(fù)添加的事件就可以通過紅黑樹log(n)的查找復(fù)雜度高效的識別出來(其中n為樹的高度)。
所有添加到epoll中的事件都會與設(shè)備(如網(wǎng)卡)驅(qū)動程序建立回調(diào)關(guān)系。這個回調(diào)方法會將就緒的事件添加到rdlist雙鏈表(就緒鏈表)中。
每一個事件對應(yīng)一個epitem結(jié)構(gòu)體,如下所示。
struct epitem{
struct rb_node rbn;//紅黑樹節(jié)點(diǎn)
struct list_head rdllink;//雙向鏈表節(jié)點(diǎn)
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所屬的eventpoll對象
struct epoll_event event; //期待發(fā)生的事件類型
}
當(dāng)調(diào)用epoll_wait檢查是否有事件發(fā)生時,只需要檢查eventpoll對象中的就緒隊(duì)列rdlist中是否有epitem元素即可。如果rdlist不為空,則把發(fā)生的事件復(fù)制到用戶態(tài),同時將事件數(shù)量返回給用戶進(jìn)程,最后用戶進(jìn)程根據(jù)事件執(zhí)行相應(yīng)的讀寫處理。
二、epoll高效性原因
事件通知機(jī)制:
- epoll使用事件通知的方式,當(dāng)監(jiān)控的文件描述符有事件發(fā)生時才會返回。
- 這減少了不必要的系統(tǒng)調(diào)用和開銷,因?yàn)橹挥性谡嬲惺录l(fā)生時才會進(jìn)行后續(xù)的處理。
避免數(shù)據(jù)拷貝:
- 在使用epoll時,可以將要監(jiān)聽的文件描述符注冊到內(nèi)核中。
- 之后,用戶程序只需等待內(nèi)核通知就可以,不必在每次調(diào)用時傳遞文件描述符列表。
- 這降低了用戶空間和內(nèi)核空間之間的數(shù)據(jù)拷貝,提高了效率。
支持兩種工作模式:
- epoll支持水平觸發(fā)(Level Triggered)和邊緣觸發(fā)(Edge Triggered)兩種工作模式。
- 邊緣觸發(fā)模式僅在狀態(tài)變化時通知應(yīng)用程序,這允許開發(fā)者實(shí)現(xiàn)更高效的處理邏輯,因?yàn)榭梢员苊庵貜?fù)檢測已處理過的事件。
動態(tài)管理文件描述符:
- epoll沒有文件描述符數(shù)量的限制,能夠動態(tài)管理大量的文件描述符。
- 這使得epoll在需要高并發(fā)、高效能I/O操作的網(wǎng)絡(luò)編程場景中表現(xiàn)出色。
面試官:你剛剛有提到epoll的兩種觸發(fā)模式,請解釋一下邊緣觸發(fā)(ET)和水平觸發(fā)(LT)的區(qū)別?
一、邊緣觸發(fā)(ET)與水平觸發(fā)(LT)的區(qū)別
觸發(fā)機(jī)制:
- 邊緣觸發(fā)(ET):只在文件描述符的狀態(tài)從不可讀/不可寫變?yōu)榭勺x/可寫時觸發(fā)一次。也就是說,它在狀態(tài)發(fā)生變化的時候通知應(yīng)用程序,且通知僅發(fā)送一次。
- 水平觸發(fā)(LT):當(dāng)文件描述符處于可讀或可寫狀態(tài)時,會持續(xù)通知應(yīng)用程序,直到文件描述符不再處于該狀態(tài)。即,只要文件描述符保持在可讀/可寫狀態(tài),epoll就會不斷通知應(yīng)用程序。
應(yīng)用程序響應(yīng):
- 邊緣觸發(fā)(ET):要求應(yīng)用程序在接收到通知后立即且徹底地處理所有可讀/可寫的數(shù)據(jù),否則可能會錯過后續(xù)數(shù)據(jù),導(dǎo)致數(shù)據(jù)丟失。因此,使用ET模式時,應(yīng)用程序需要具備更高的復(fù)雜性和更精細(xì)的控制。
- 水平觸發(fā)(LT):應(yīng)用程序可以按需處理數(shù)據(jù),不必立即處理完所有數(shù)據(jù)。即使只處理部分?jǐn)?shù)據(jù),下次調(diào)用epoll_wait時,epoll仍會繼續(xù)通知應(yīng)用程序該文件描述符仍然可讀/可寫。
適用場景:
- 邊緣觸發(fā)(ET):適用于高性能場景,因?yàn)樗鼫p少了不必要的系統(tǒng)調(diào)用。同時,它也要求應(yīng)用程序能夠高效、徹底地處理數(shù)據(jù)。
- 水平觸發(fā)(LT):適用于大多數(shù)情況,特別是當(dāng)應(yīng)用程序需要持續(xù)處理就緒事件或處理多個相關(guān)事件時。它提供了更簡單、直觀的通知機(jī)制。
以下是一個使用Python的epoll模塊來演示ET和LT觸發(fā)模式的示例:
import os
import socket
import select
import epoll
# 創(chuàng)建一個TCP/IP套接字
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.bind(('localhost', 12345))
server_sock.listen(5)
server_sock.setblocking(0) # 設(shè)置為非阻塞模式
# 創(chuàng)建一個epoll實(shí)例
epoller = epoll.epoll()
# 為服務(wù)器套接字注冊事件,LT模式
epoller.register(server_sock.fileno(), epoll.EPOLLIN | epoll.EPOLLET if False else epoll.EPOLLIN) # 將if False改為True以使用ET模式
# 客戶端連接處理函數(shù)
def handle_connection(client_sock):
data = client_sock.recv(1024)
if data:
print(f"Received: {data.decode()}")
client_sock.sendall(data) # Echo the data back to the client
else:
# 沒有數(shù)據(jù)表示客戶端已經(jīng)關(guān)閉連接
client_sock.close()
print("Client disconnected.")
# 主循環(huán)
try:
while True:
events = epoller.poll(1000) # 超時時間為1000毫秒
for fileno, event in events:
if fileno == server_sock.fileno():
# 服務(wù)器套接字上有新的連接請求
client_sock, client_addr = server_sock.accept()
client_sock.setblocking(0) # 設(shè)置為非阻塞模式
print(f"Accepted connection from {client_addr}")
# 為客戶端套接字注冊事件,這里我們根據(jù)之前的條件選擇ET或LT模式
epoller.register(client_sock.fileno(), epoll.EPOLLIN | epoll.EPOLLET if False else epoll.EPOLLIN)
else:
# 客戶端套接字上有數(shù)據(jù)可讀或連接關(guān)閉等事件
client_sock = socket.socket(fileno=fileno) # 從文件描述符恢復(fù)套接字對象
# 根據(jù)ET或LT模式的不同,處理數(shù)據(jù)的方式也會有所不同
if False: # 如果為True,則表示使用ET模式
# ET模式下,需要確保一次讀取所有可用數(shù)據(jù)
while True:
try:
data = client_sock.recv(1024)
if not data:
break # 沒有數(shù)據(jù)表示連接已關(guān)閉
print(f"Received (ET): {data.decode()}")
client_sock.sendall(data) # Echo the data back to the client
except BlockingIOError:
# 在ET模式下,如果沒有更多數(shù)據(jù)可讀,會拋出BlockingIOError異常
break
else:
# LT模式下,每次有數(shù)據(jù)可讀時都會觸發(fā)事件
handle_connection(client_sock)
# 如果客戶端連接已關(guān)閉,則注銷該套接字
if not client_sock.fileno() in [fd for fd, _ in epoller.poll(0)]: # 使用非阻塞poll檢查連接是否仍然活躍
epoller.unregister(client_sock.fileno())
client_sock.close()
except KeyboardInterrupt:
print("Server stopped by user.")
finally:
epoller.close()
server_sock.close()
代碼中通過epoll.EPOLLIN | epoll.EPOLLET來選擇ET模式,而僅通過epoll.EPOLLIN來選擇LT模式。你可以通過修改條件(即if False改為if True)來切換模式。
- 在ET模式下,由于只有在狀態(tài)變化時才會觸發(fā)事件,因此我們需要使用循環(huán)來讀取所有可用的數(shù)據(jù),直到recv拋出BlockingIOError異常,表示當(dāng)前沒有更多數(shù)據(jù)可讀。
- 在LT模式下,每次有數(shù)據(jù)可讀時都會觸發(fā)事件,因此我們可以直接調(diào)用handle_connection函數(shù)來處理連接。
由于epoll模塊是Linux特有的,因此這段代碼只能在Linux環(huán)境下運(yùn)行。
為了簡化示例,錯誤處理和資源清理可能不是最完善的。在實(shí)際應(yīng)用中,你需要更仔細(xì)地處理這些情況。
使用epoll.poll(0)來檢查連接是否仍然活躍是一種非阻塞的檢查方式,但它可能不是最優(yōu)雅或最高效的方法。在實(shí)際應(yīng)用中,你可能需要設(shè)計更復(fù)雜的邏輯來處理連接的狀態(tài)。面試官:說說看你知道的IO模型有哪些?