射洪哪里可以做網(wǎng)站北京seo推廣外包
參考面試官:簡(jiǎn)單說(shuō)一下阻塞IO、非阻塞IO、IO復(fù)用的區(qū)別 ?_unix環(huán)境編程 阻塞io和非阻塞io-CSDN博客
同步阻塞(BIO)
BIO 以流的方式處理數(shù)據(jù)
應(yīng)用程序發(fā)起一個(gè)系統(tǒng)調(diào)用(recvform),這個(gè)時(shí)候應(yīng)用程序會(huì)一直阻塞下去,直到內(nèi)核把數(shù)據(jù)準(zhǔn)備好,并將其從內(nèi)核復(fù)制到用戶空間,復(fù)制完成后返回成功提示,這個(gè)時(shí)候應(yīng)用程序才會(huì)繼續(xù)處理數(shù)據(jù)。
服務(wù)實(shí)現(xiàn)模式為一個(gè)連接對(duì)應(yīng)一個(gè)線程,即客戶端發(fā)送一個(gè)連接,服務(wù)端要有一個(gè)線程來(lái)處理。
- 缺點(diǎn)
? ? ? ? 一旦有高并發(fā)的大量請(qǐng)求,就會(huì)有如下問(wèn)題: 1)線程不夠用,就算使用了線程池復(fù)用線程也無(wú)濟(jì)于事(一臺(tái)機(jī)器需要維護(hù) 1 萬(wàn)個(gè)連接,相當(dāng)于要維護(hù) 1 萬(wàn)個(gè)進(jìn)程/線程,操作系統(tǒng)就算死扛也是扛不住的);2)阻塞I/O模式下,會(huì)有大量的線程被阻塞,一直在等待數(shù)據(jù),這個(gè)時(shí)候的線程被掛起,只能干等,CPU利用率很低,換句話說(shuō),系統(tǒng)的吞吐量差;3)如果網(wǎng)絡(luò)I/O堵塞或者有網(wǎng)絡(luò)抖動(dòng)或者網(wǎng)絡(luò)故障等,線程的阻塞時(shí)間可能很長(zhǎng),整個(gè)系統(tǒng)也變的不可靠;4)服務(wù)器線程太多,壓力太大,導(dǎo)致服務(wù)器宕機(jī)。
同步非阻塞(NIO)
NIO 以緩沖區(qū)的方式處理數(shù)據(jù),緩沖區(qū) I/O 的效率比流 I/O 高很多
應(yīng)用進(jìn)程需要不斷詢問(wèn)內(nèi)核數(shù)據(jù)是否就緒,在內(nèi)核數(shù)據(jù)還未就緒時(shí),應(yīng)用進(jìn)程還可以做其他事情。
服務(wù)實(shí)現(xiàn)模式是一個(gè)線程可以處理多個(gè)連接,即客戶端發(fā)送的連接都會(huì)注冊(cè)到多路復(fù)用器上,然后進(jìn)行輪詢連接,有I/O請(qǐng)求就處理。
- 優(yōu)點(diǎn):模型簡(jiǎn)單,實(shí)現(xiàn)難度低;與阻塞IO模型對(duì)比,它在等待數(shù)據(jù)報(bào)的過(guò)程中,進(jìn)程并沒(méi)有阻塞,它可以做其他的事情。
-
缺點(diǎn):輪詢發(fā)送 recvform,消耗CPU 資源。
I/O多路復(fù)用
在沒(méi)有使用IO多路復(fù)用機(jī)制時(shí),有BIO、NIO兩種實(shí)現(xiàn)方式,但是會(huì)出現(xiàn)阻塞或者開(kāi)銷大的問(wèn)題
參考這次答應(yīng)我,一舉拿下 I/O 多路復(fù)用! (qq.com)
非阻塞IO模型需要進(jìn)程不斷地輪詢發(fā)起recvform系統(tǒng)調(diào)用,就會(huì)有很多的線程不斷調(diào)用recvfrom 請(qǐng)求數(shù)據(jù),先不說(shuō)服務(wù)器能不能扛得住這么多線程,就算扛得住那么很明顯這種方式是不是太浪費(fèi)資源了,線程是我們操作系統(tǒng)的寶貴資源,大量的線程用來(lái)去讀取數(shù)據(jù)了,那么就意味著能做其它事情的線程就會(huì)少。
select/poll/epoll 這是三個(gè)多路復(fù)用接口,都能實(shí)現(xiàn) C10K 嗎?接下來(lái),我們分別說(shuō)說(shuō)它們。
select/poll
select 實(shí)現(xiàn)多路復(fù)用的方式是,將已連接的 Socket 都放到一個(gè)文件描述符集合,然后調(diào)用 select 函數(shù)將文件描述符集合拷貝到內(nèi)核里,讓內(nèi)核來(lái)檢查是否有網(wǎng)絡(luò)事件產(chǎn)生,檢查的方式很粗暴,就是通過(guò)遍歷文件描述符集合的方式,當(dāng)檢查到有事件產(chǎn)生后,將此 Socket 標(biāo)記為可讀或可寫(xiě), 接著再把整個(gè)文件描述符集合拷貝回用戶態(tài)里,然后用戶態(tài)還需要再通過(guò)遍歷的方法找到可讀或可寫(xiě)的 Socket,然后再對(duì)其處理。
所以,對(duì)于 select 這種方式,需要進(jìn)行?2 次「遍歷」文件描述符集合,一次是在內(nèi)核態(tài)里,一個(gè)次是在用戶態(tài)里 ,而且還會(huì)發(fā)生?2 次「拷貝」文件描述符集合,先從用戶空間傳入內(nèi)核空間,由內(nèi)核修改后,再傳出到用戶空間中。
select 使用固定長(zhǎng)度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的個(gè)數(shù)是有限制的,在 Linux 系統(tǒng)中,由內(nèi)核中的 FD_SETSIZE 限制, 默認(rèn)最大值為?1024
,只能監(jiān)聽(tīng) 0~1023 的文件描述符。
poll 不再用 BitsMap 來(lái)存儲(chǔ)所關(guān)注的文件描述符,取而代之用動(dòng)態(tài)數(shù)組,以鏈表形式來(lái)組織,突破了 select 的文件描述符個(gè)數(shù)限制,當(dāng)然還會(huì)受到系統(tǒng)文件描述符限制。
但是 poll 和 select 并沒(méi)有太大的本質(zhì)區(qū)別,都是使用「線性結(jié)構(gòu)」存儲(chǔ)進(jìn)程關(guān)注的 Socket 集合,因此都需要遍歷文件描述符集合來(lái)找到可讀或可寫(xiě)的 Socket,時(shí)間復(fù)雜度為 O(n),而且也需要在用戶態(tài)與內(nèi)核態(tài)之間拷貝文件描述符集合,這種方式隨著并發(fā)數(shù)上來(lái),性能的損耗會(huì)呈指數(shù)級(jí)增長(zhǎng)。
epoll
epoll 通過(guò)兩個(gè)方面,很好解決了 select/poll 的問(wèn)題。
第一點(diǎn),epoll 在內(nèi)核里使用紅黑樹(shù)來(lái)跟蹤進(jìn)程所有待檢測(cè)的文件描述字,把需要監(jiān)控的 socket 通過(guò)epoll_ctl()函數(shù)加入內(nèi)核中的紅黑樹(shù)里,紅黑樹(shù)是個(gè)高效的數(shù)據(jù)結(jié)構(gòu),增刪查一般時(shí)間復(fù)雜度是O(logn),通過(guò)對(duì)這棵黑紅樹(shù)進(jìn)行操作,這樣就不需要像 select/poll 每次操作時(shí)都傳入整個(gè) socket 集合,只需要傳入一個(gè)待檢測(cè)的 socket,減少了內(nèi)核和用戶空間大量的數(shù)據(jù)拷貝和內(nèi)存分配。
第二點(diǎn), epoll 使用事件驅(qū)動(dòng)的機(jī)制,內(nèi)核里維護(hù)了一個(gè)鏈表來(lái)記錄就緒事件,當(dāng)某個(gè) socket 有事件發(fā)生時(shí),通過(guò)回調(diào)函數(shù)內(nèi)核會(huì)將其加入到這個(gè)就緒事件列表中,當(dāng)用戶調(diào)用epoll_wait()函數(shù)時(shí),只會(huì)返回有事件發(fā)生的文件描述符集合,不需要像 select/poll 那樣輪詢掃描整個(gè) socket 集合,大大提高了檢測(cè)的效率。
從下圖你可以看到 epoll 相關(guān)的接口作用:
?
epoll 的方式即使監(jiān)聽(tīng)的 Socket 數(shù)量越多的時(shí)候,效率不會(huì)大幅度降低,能夠同時(shí)監(jiān)聽(tīng)的 Socket 的數(shù)目也非常的多了,上限就為系統(tǒng)定義的進(jìn)程打開(kāi)的最大文件描述符個(gè)數(shù)。因而,epoll 被稱為解決 C10K 問(wèn)題的利器。
epoll 支持兩種事件觸發(fā)模式,分別是邊緣觸發(fā)(edge-triggered,ET)和水平觸發(fā)(level-triggered,LT)。
這兩個(gè)術(shù)語(yǔ)還挺抽象的,其實(shí)它們的區(qū)別還是很好理解的。
-
使用邊緣觸發(fā)模式時(shí),當(dāng)被監(jiān)控的 Socket 描述符上有可讀事件發(fā)生時(shí),服務(wù)器端只會(huì)從 epoll_wait 中蘇醒一次,即使進(jìn)程沒(méi)有調(diào)用 read 函數(shù)從內(nèi)核讀取數(shù)據(jù),也依然只蘇醒一次,因此我們程序要保證一次性將內(nèi)核緩沖區(qū)的數(shù)據(jù)讀取完;
-
使用水平觸發(fā)模式時(shí),當(dāng)被監(jiān)控的 Socket 上有可讀事件發(fā)生時(shí),服務(wù)器端不斷地從 epoll_wait 中蘇醒,直到內(nèi)核緩沖區(qū)數(shù)據(jù)被 read 函數(shù)讀完才結(jié)束,目的是告訴我們有數(shù)據(jù)需要讀取;
舉個(gè)例子,你的快遞被放到了一個(gè)快遞箱里,如果快遞箱只會(huì)通過(guò)短信通知你一次,即使你一直沒(méi)有去取,它也不會(huì)再發(fā)送第二條短信提醒你,這個(gè)方式就是邊緣觸發(fā);如果快遞箱發(fā)現(xiàn)你的快遞沒(méi)有被取出,它就會(huì)不停地發(fā)短信通知你,直到你取出了快遞,它才消停,這個(gè)就是水平觸發(fā)的方式。
這就是兩者的區(qū)別,水平觸發(fā)的意思是只要滿足事件的條件,比如內(nèi)核中有數(shù)據(jù)需要讀,就一直不斷地把這個(gè)事件傳遞給用戶;而邊緣觸發(fā)的意思是只有第一次滿足條件的時(shí)候才觸發(fā),之后就不會(huì)再傳遞同樣的事件了。
如果使用水平觸發(fā)模式,當(dāng)內(nèi)核通知文件描述符可讀寫(xiě)時(shí),接下來(lái)還可以繼續(xù)去檢測(cè)它的狀態(tài),看它是否依然可讀或可寫(xiě)。所以在收到通知后,沒(méi)必要一次執(zhí)行盡可能多的讀寫(xiě)操作。
如果使用邊緣觸發(fā)模式,I/O 事件發(fā)生時(shí)只會(huì)通知一次,而且我們不知道到底能讀寫(xiě)多少數(shù)據(jù),所以在收到通知后應(yīng)盡可能地讀寫(xiě)數(shù)據(jù),以免錯(cuò)失讀寫(xiě)的機(jī)會(huì)。因此,我們會(huì)循環(huán)從文件描述符讀寫(xiě)數(shù)據(jù),那么如果文件描述符是阻塞的,沒(méi)有數(shù)據(jù)可讀寫(xiě)時(shí),進(jìn)程會(huì)阻塞在讀寫(xiě)函數(shù)那里,程序就沒(méi)辦法繼續(xù)往下執(zhí)行。所以,邊緣觸發(fā)模式一般和非阻塞 I/O 搭配使用,程序會(huì)一直執(zhí)行 I/O 操作,直到系統(tǒng)調(diào)用(如?read
?和?write
)返回錯(cuò)誤,錯(cuò)誤類型為?EAGAIN
?或?EWOULDBLOCK
。
一般來(lái)說(shuō),邊緣觸發(fā)的效率比水平觸發(fā)的效率要高,因?yàn)檫吘売|發(fā)可以減少 epoll_wait 的系統(tǒng)調(diào)用次數(shù),系統(tǒng)調(diào)用也是有一定的開(kāi)銷的的,畢竟也存在上下文的切換。
select/poll 只有水平觸發(fā)模式,epoll 默認(rèn)的觸發(fā)模式是水平觸發(fā),但是可以根據(jù)應(yīng)用場(chǎng)景設(shè)置為邊緣觸發(fā)模式。