- 阻塞 io 模型 blocking IO
- 非阻塞 io 模型 nonblocking IO
- io多路復(fù)用模型 IO multiplexing
- 細(xì)談 io 多路復(fù)用技術(shù) select 和poll
- 細(xì)談事件驅(qū)動(dòng)--epoll
- 總結(jié)
操作系統(tǒng)在處理io的時(shí)候,主要有兩個(gè)階段:
- 等待數(shù)據(jù)傳到io設(shè)備
- io設(shè)備將數(shù)據(jù)復(fù)制到user space
我們一般將上述過程簡化理解為:
- 等到數(shù)據(jù)傳到kernel內(nèi)核space
- kernel內(nèi)核區(qū)域?qū)?shù)據(jù)復(fù)制到user space(理解為進(jìn)程或者線程的緩沖區(qū))
而根據(jù)這兩個(gè)階段而不同的操作方法,就會(huì)產(chǎn)生多種io模型,本文只討論select,poll,epoll,所以只引出三種io模型。
阻塞 io 模型 blocking IO
最常用的也就是阻塞io模型。默認(rèn)情況下,所有文件操作都是阻塞的。我們以套接字接口為例來講解此模型,在進(jìn)程空間調(diào)用recvfrom,其系統(tǒng)調(diào)用知道數(shù)據(jù)包到達(dá)并且被復(fù)制到進(jìn)程緩沖中或者發(fā)生錯(cuò)誤時(shí)才會(huì)返回,在此期間會(huì)一直阻塞,所以進(jìn)程在調(diào)用recvfrom開始到它返回的整段時(shí)間都是阻塞的,因此稱之為阻塞io模型。
注意:在阻塞狀態(tài)下,程序是不會(huì)浪費(fèi)CPU的,cpu只是不執(zhí)行io操作了,還會(huì)去做別的。

應(yīng)用層有數(shù)據(jù)過來,會(huì)調(diào)用recvfrom方法,但是這個(gè)時(shí)候應(yīng)用層的數(shù)據(jù)還沒復(fù)制到kernel中,將應(yīng)用層數(shù)據(jù)復(fù)制到kerne這個(gè)階段是需要時(shí)間的,所以recvfrom方法會(huì)阻塞,當(dāng)內(nèi)核中的數(shù)據(jù)準(zhǔn)備好之后,recvfrom方法還不會(huì)返回,而是會(huì)發(fā)起一個(gè)系統(tǒng)調(diào)用將kernel中的數(shù)據(jù)復(fù)制到進(jìn)程的緩沖區(qū)中,也就是user space,當(dāng)這個(gè)工作完成之后,recvfrom才會(huì)返回并解除程序的阻塞。
所以我們總結(jié)可以發(fā)現(xiàn),主要就是上面兩個(gè)階段
- 應(yīng)用層數(shù)據(jù)到kernel
- kernel復(fù)制到user space
阻塞io模型就是將這個(gè)兩個(gè)過程合并在一起,一起阻塞。
而非阻塞模型則是將第一個(gè)過程的阻塞變成非阻塞,第二個(gè)階段是系統(tǒng)調(diào)用,是必須阻塞的,所以非阻塞模型也是同步的,因?yàn)樗鼈冊(cè)趉ernel里的數(shù)據(jù)準(zhǔn)備好之后,進(jìn)行系統(tǒng)調(diào)用,將數(shù)據(jù)拷貝到進(jìn)程緩沖區(qū)中。
非阻塞 io 模型 nonblocking IO
就是對(duì)于第一個(gè)階段,也就是應(yīng)用層數(shù)據(jù)到kernel的過程中,recvfrom會(huì)輪詢檢查,如果kernel數(shù)據(jù)沒有準(zhǔn)備還,就返回一個(gè)EWOULDBLOCK錯(cuò)誤。不斷的輪詢檢查,直到發(fā)現(xiàn)kernel中的數(shù)據(jù)準(zhǔn)備好了,就返回,然后進(jìn)行系統(tǒng)調(diào)用,將數(shù)據(jù)從kernel拷貝到進(jìn)程緩沖區(qū)中。有點(diǎn)類似busy-waiting的方法。

io多路復(fù)用模型 IO multiplexing
目的:因?yàn)樽枞P驮跊]有收到數(shù)據(jù)的時(shí)候就會(huì)阻塞卡住,如果一次需要接受多個(gè)socket fd的時(shí)候,就會(huì)導(dǎo)致必須處理完前面的fd,才能處理后面的fd,即使可能后面的fd比前面的fd還要先準(zhǔn)備好,所以這樣就會(huì)造成客戶端的嚴(yán)重延遲。為了處理多個(gè)請(qǐng)求,我們自然先想到用多線程來處理多個(gè)socket fd,但是這樣又會(huì)啟動(dòng)大量的線程,造成資源的浪費(fèi),所以這個(gè)時(shí)候就出現(xiàn)了io多路復(fù)用技術(shù)。就是用一個(gè)進(jìn)程來處理多個(gè)fd的請(qǐng)求。
應(yīng)用:適用于針對(duì)大量的io請(qǐng)求的情況,對(duì)于服務(wù)器必須在同時(shí)處理來自客戶端的大量的io操作的時(shí)候,就非常適合

細(xì)談 io 多路復(fù)用技術(shù) select 和poll
select
select的工作流程:
單個(gè)進(jìn)程就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的io請(qǐng)求(同時(shí)阻塞多個(gè)io操作)。基本原理就是程序呼叫select,然后整個(gè)程序就阻塞了,這時(shí)候,kernel就會(huì)輪詢檢查所有select負(fù)責(zé)的fd,當(dāng)找到一個(gè)client中的數(shù)據(jù)準(zhǔn)備好了,select就會(huì)返回,這個(gè)時(shí)候程序就會(huì)系統(tǒng)調(diào)用,將數(shù)據(jù)從kernel復(fù)制到進(jìn)程緩沖區(qū)。

下圖為select同時(shí)從多個(gè)客戶端接受數(shù)據(jù)的過程
雖然服務(wù)器進(jìn)程會(huì)被select阻塞,但是select會(huì)利用內(nèi)核不斷輪詢監(jiān)聽其他客戶端的io操作是否完成。

Poll介紹
poll的原理與select非常相似,差別如下:
- 描述fd集合的方式不同,poll使用 pollfd 結(jié)構(gòu)而不是select結(jié)構(gòu)fd_set結(jié)構(gòu),所以poll是鏈?zhǔn)降?,沒有最大連接數(shù)的限制
- poll有一個(gè)特點(diǎn)是水平觸發(fā),也就是通知程序fd就緒后,這次沒有被處理,那么下次poll的時(shí)候會(huì)再次通知同個(gè)fd已經(jīng)就緒。
select缺點(diǎn)
根據(jù)fd_size的定義,它的大小為32個(gè)整數(shù)大?。?2位機(jī)器為32*32,所有共有1024bits可以記錄fd),每個(gè)fd一個(gè)bit,所以最大只能同時(shí)處理1024個(gè)fd
每次要判斷【有哪些event發(fā)生】這件事的成本很高,因?yàn)閟elect(polling也是)采取主動(dòng)輪詢機(jī)制
1.每一次呼叫 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 與 user之間的數(shù)據(jù)溝通
2.然后kernel還要輪詢每個(gè)fd,約線性時(shí)間
- 假設(shè)現(xiàn)實(shí)中,有1百萬個(gè)客戶端同時(shí)與一個(gè)服務(wù)器保持著tcp連接,而每一個(gè)時(shí)刻,通常只有幾百上千個(gè)tcp連接是活躍的,這時(shí)候我們?nèi)匀皇褂胹elect/poll機(jī)制,kernel必須在搜尋完100萬個(gè)fd之后,才能找到其中狀態(tài)是active的,這樣資源消耗大而且效率低下。
對(duì)于select和poll的上述缺點(diǎn),就引進(jìn)了一種新的技術(shù),epoll技術(shù)
細(xì)談事件驅(qū)動(dòng)--epoll
epoll 提供了三個(gè)函數(shù):
int epoll_create(int size);
建立一個(gè) epoll 對(duì)象,并傳回它的idint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
事件注冊(cè)函數(shù),將需要監(jiān)聽的事件和需要監(jiān)聽的fd交給epoll對(duì)象int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待注冊(cè)的事件被觸發(fā)或者timeout發(fā)生
epoll解決的問題:
epoll沒有fd數(shù)量限制
epoll沒有這個(gè)限制,我們知道每個(gè)epoll監(jiān)聽一個(gè)fd,所以最大數(shù)量與能打開的fd數(shù)量有關(guān),一個(gè)g的內(nèi)存的機(jī)器上,能打開10萬個(gè)左右epoll不需要每次都從user space 將fd set復(fù)制到內(nèi)核kernel
epoll在用epoll_ctl函數(shù)進(jìn)行事件注冊(cè)的時(shí)候,已經(jīng)將fd復(fù)制到內(nèi)核中,所以不需要每次都重新復(fù)制一次select 和 poll 都是主動(dòng)輪詢機(jī)制,需要拜訪每一個(gè) FD;
epoll是被動(dòng)觸發(fā)方式,給fd注冊(cè)了相應(yīng)事件的時(shí)候,我們?yōu)槊恳粋€(gè)fd指定了一個(gè)回調(diào)函數(shù),當(dāng)數(shù)據(jù)準(zhǔn)備好之后,就會(huì)把就緒的fd加入一個(gè)就緒的隊(duì)列中,epoll_wait的工作方式實(shí)際上就是在這個(gè)就緒隊(duì)列中查看有沒有就緒的fd,如果有,就喚醒就緒隊(duì)列上的等待者,然后調(diào)用回調(diào)函數(shù)。
- 雖然epoll。poll。epoll都需要查看是否有fd就緒,但是epoll之所以是被動(dòng)觸發(fā),就在于它只要去查找就緒隊(duì)列中有沒有fd,就緒的fd是主動(dòng)加到隊(duì)列中,epoll不需要一個(gè)個(gè)輪詢確認(rèn)。
換一句話講,就是select和poll只能通知有fd已經(jīng)就緒了,但不能知道究竟是哪個(gè)fd就緒,所以select和poll就要去主動(dòng)輪詢一遍找到就緒的fd。而epoll則是不但可以知道有fd可以就緒,而且還具體可以知道就緒fd的編號(hào),所以直接找到就可以,不用輪詢。
總結(jié)
- select, poll是為了解決同時(shí)大量IO的情況(尤其網(wǎng)絡(luò)服務(wù)器),但是隨著連接數(shù)越多,性能越差
- epoll是select和poll的改進(jìn)方案,在 linux 上可以取代 select 和 poll,可以處理大量連接的性能問題