一篇學(xué)會(huì)阿里面試問(wèn)的 Select、Poll、Epoll 模型
這一篇要說(shuō)的select、poll、epoll這三個(gè)的區(qū)別,大家對(duì)于IO多路復(fù)用都了解吧,這個(gè)問(wèn)題也是面試官最最愛(ài)問(wèn)的問(wèn)題之一了。
操作系統(tǒng)在處理IO的時(shí)候,主要客源分為兩個(gè)階段:
- 等待數(shù)據(jù)傳遞到IO設(shè)備。
- IO設(shè)備將數(shù)據(jù)復(fù)制到用戶空間user space。
也就可以將上述過(guò)程簡(jiǎn)化理解為:
等待數(shù)據(jù)到kernel內(nèi)核區(qū)域。
kernel內(nèi)核區(qū)域?qū)?shù)據(jù)復(fù)制到用戶區(qū)域user space,用戶區(qū)域可以理解為JVM區(qū)域,即進(jìn)程或者線程的緩沖區(qū)。
BIO
這是屬于最簡(jiǎn)單的同步阻塞IO模型
當(dāng)應(yīng)用層有數(shù)據(jù)過(guò)來(lái)的時(shí)候,會(huì)調(diào)用recvfrom方法,但是這個(gè)時(shí)候應(yīng)用層的數(shù)據(jù)還沒(méi)有復(fù)制到kernel內(nèi)核區(qū),也就是上面說(shuō)的第一個(gè)過(guò)程,這個(gè)過(guò)程需要時(shí)間,所以recvfrom會(huì)阻塞。
當(dāng)內(nèi)核kernel中的數(shù)據(jù)準(zhǔn)備好了之后,recvfrom方法并不會(huì)返回,而是會(huì)發(fā)起一個(gè)系統(tǒng)調(diào)用將kernel中的數(shù)據(jù)復(fù)制到JVM的進(jìn)程中的緩沖區(qū)中,也就是用戶空間user space。
當(dāng)這個(gè)復(fù)制也完成之后,也就是上面說(shuō)的兩個(gè)階段全部完成之后,recvfrom才會(huì)返回并且解除程序的阻塞。
默認(rèn)情況下,所有的文件操作都是阻塞的,在進(jìn)程調(diào)用期間的recvfrom,直到數(shù)據(jù)包達(dá)到并且被復(fù)制到JVM進(jìn)程緩沖區(qū)或者發(fā)生錯(cuò)誤的時(shí)候才會(huì)返回,在此期間一直阻塞。
在阻塞期間,CPU不能執(zhí)行IO操作,但是CPU還是可以去做別的事情的,阻塞了,但沒(méi)有完全阻塞。
我們?cè)倏床僮飨到y(tǒng)處理IO的兩個(gè)步驟
- 等待數(shù)據(jù)到kernel內(nèi)核區(qū)域
- kernel內(nèi)核區(qū)域?qū)?shù)據(jù)復(fù)制到用戶區(qū)域user space
阻塞IO模型,其實(shí)就是把這兩個(gè)階段合并在一起,一起阻塞。
NIO
非阻塞其實(shí)就是把第一個(gè)過(guò)程的阻塞變成非阻塞,也就是recvfrom函數(shù)會(huì)不斷的去詢問(wèn)kernel內(nèi)核區(qū)域中的數(shù)據(jù)是否準(zhǔn)備好。
如果數(shù)據(jù)沒(méi)有準(zhǔn)備好,就返回EWOULDBLOCK錯(cuò)誤,不斷的去進(jìn)行輪詢檢查,知道發(fā)現(xiàn)kernel內(nèi)核中的數(shù)據(jù)準(zhǔn)備好了,就會(huì)返回。
第二個(gè)階段屬于系統(tǒng)調(diào)用,是必須阻塞的,就是將數(shù)據(jù)從kernel內(nèi)核區(qū)域拷貝到用戶區(qū)域,也就是進(jìn)程緩沖區(qū)中。
Linux系統(tǒng)將所有設(shè)備都當(dāng)作文件來(lái)處理,而Linux用文件描述符fd來(lái)標(biāo)識(shí)每個(gè)文件對(duì)象。
問(wèn)題
阻塞模型在沒(méi)有收到數(shù)據(jù)的時(shí)候就會(huì)阻塞卡主,如果一個(gè)線程需要接受到多個(gè)客戶端的socket的fd連接,這樣會(huì)導(dǎo)致在處理這種情況效率比較低。
必須處理完前面的所有fd,才可以處理后面的fd,即使后面的fd可能比前面的fd提前準(zhǔn)備好了,但是也得等,這樣會(huì)導(dǎo)致客戶端的嚴(yán)重延遲。
于是為了處理多個(gè)請(qǐng)求,有的小伙伴可能會(huì)想到用多個(gè)線程來(lái)改善,可以引入線程池改善這個(gè)情況,并且通過(guò)線程池的最大數(shù)量來(lái)控制這個(gè)限度,但是這并沒(méi)有從根本上解決問(wèn)題。
應(yīng)用:適用于針對(duì)大量的io請(qǐng)求的情況,對(duì)于服務(wù)器必須在同時(shí)處理來(lái)自客戶端的大量的io操作的時(shí)候,就非常適合。
Select工作流程
單個(gè)進(jìn)程可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的IO請(qǐng)求,就是調(diào)用select之后,整個(gè)程序就阻塞了。
此時(shí)需要把所有的fd集合都從JVM用戶空間拷貝到kernel內(nèi)核空間,這個(gè)開(kāi)銷很大。
然后去輪詢檢查所有的select負(fù)責(zé)的fd,當(dāng)找到一個(gè)client中的數(shù)據(jù)準(zhǔn)備好了之后,select就會(huì)返回,此時(shí)將數(shù)據(jù)從kernel復(fù)制到進(jìn)程的緩沖區(qū)中。
缺點(diǎn):
- 每次都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),消耗資源。
- 每次調(diào)用需要進(jìn)行輪詢所有傳遞進(jìn)來(lái)的fd,也比較消耗資源。
你想啊,要是有十萬(wàn)個(gè)連接,而活躍的只有幾個(gè),那每次都要遍歷這十萬(wàn)個(gè),豈不是很糟糕。
- 支持的fd數(shù)量有限,根據(jù)fd_size的定義,它的大小為32個(gè)整數(shù)大?。?2位機(jī)器為32*32,所有共有1024bits可以記錄fd),每個(gè)fd一個(gè)bit,所以最大只能同時(shí)處理1024個(gè)fd。
每一次呼叫select都需要先從 user space把 FD_SET復(fù)制到 kernel(約線性時(shí)間成本)。
為什么 select 不能像epoll一樣,只做一次復(fù)制就好呢?
每一次呼叫 select()前,F(xiàn)D_SET都可能更動(dòng),而 epoll 提供了共享記憶存儲(chǔ)結(jié)構(gòu),所以不需要這里的kernel內(nèi)核區(qū)域和用戶區(qū)域之間的全量數(shù)據(jù)復(fù)制。
Poll
poll的原理與select非常相似,但是呢,有兩點(diǎn)的不同之處。
一個(gè)是存儲(chǔ)fd的集合不同,select是有限的,而poll的方式存儲(chǔ)是鏈?zhǔn)降?,沒(méi)有最大連接數(shù)的限制。
另一點(diǎn)就是水平觸發(fā),也就是通知程序fd就緒后,這次沒(méi)有被處理,那么下次poll的時(shí)候會(huì)再次通知同個(gè)fd已經(jīng)就緒。
Epoll
epoll既然是對(duì)select和poll的改進(jìn),就應(yīng)該能避免上述的三個(gè)缺點(diǎn)。那epoll都是怎么解決的呢?
在此之前,我們先看一下epoll和select和poll的調(diào)用接口上的不同,select和poll都只提供了一個(gè)函數(shù)——select或者poll函數(shù)。而epoll提供了三個(gè)函數(shù),epoll_create,epoll_ctl和epoll_wait。
epoll_create是創(chuàng)建一個(gè)epoll句柄;epoll_ctl是注冊(cè)要監(jiān)聽(tīng)的事件類型;epoll_wait則是等待事件的產(chǎn)生。
對(duì)于每次都需要把數(shù)據(jù)從用戶區(qū)全量復(fù)制到kernel內(nèi)核區(qū)這個(gè)缺點(diǎn),epoll的解決方案在epoll_ctl函數(shù)中。
每次注冊(cè)新的事件到epoll句柄中時(shí)(在epoll_ctl中指定EPOLL_CTL_ADD),會(huì)把所有的fd拷貝進(jìn)內(nèi)核,而不是在epoll_wait的時(shí)候重復(fù)拷貝。epoll保證了每個(gè)fd在整個(gè)過(guò)程中只會(huì)拷貝一次。
對(duì)于第二個(gè)每次都需要輪詢所有fd這個(gè)缺點(diǎn)
epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對(duì)應(yīng)的設(shè)備等待隊(duì)列中,而只在epoll_ctl時(shí)把current掛一遍(這一遍必不可少)并為每個(gè)fd指定一個(gè)回調(diào)函數(shù),當(dāng)設(shè)備就緒,喚醒等待隊(duì)列上的等待者時(shí),就會(huì)調(diào)用這個(gè)回調(diào)函數(shù),而這個(gè)回調(diào)函數(shù)會(huì)把就緒的fd加入一個(gè)就緒鏈表)。
epoll_wait的工作實(shí)際上就是在這個(gè)就緒鏈表中查看有沒(méi)有就緒的fd(利用schedule_timeout()實(shí)現(xiàn)睡一會(huì),判斷一會(huì)的效果,和select實(shí)現(xiàn)中的第7步是類似的)。
把主動(dòng)權(quán)交給了每一個(gè)連接,當(dāng)設(shè)備就緒的時(shí)候,調(diào)用回調(diào)函數(shù)才會(huì)加入到這個(gè)就緒的集合。
對(duì)于第三個(gè)數(shù)量的缺點(diǎn)
epoll沒(méi)有這個(gè)限制,它所支持的FD上限是最大可以打開(kāi)文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于1024,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬(wàn)左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。