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