Linux系統(tǒng)I/O模型及select、poll、epoll原理和應(yīng)用

基本概念說明

理解Linux的IO模型之前,首先要了解一些基本概念,才能理解這些IO模型設(shè)計的依據(jù)

用戶空間和內(nèi)核空間

操作系統(tǒng)使用虛擬內(nèi)存來映射物理內(nèi)存,對于32位的操作系統(tǒng)來說,虛擬地址空間為4G(2^32)。操作系統(tǒng)的核心是內(nèi)核,為了保護用戶進程不能直接操作內(nèi)核,保證內(nèi)核安全,操作系統(tǒng)將虛擬地址空間劃分為內(nèi)核空間和用戶空間。內(nèi)核可以訪問全部的地址空間,擁有訪問底層硬件設(shè)備的權(quán)限,普通的應(yīng)用程序需要訪問硬件設(shè)備必須通過系統(tǒng)調(diào)用來實現(xiàn)。

對于Linux系統(tǒng)來說,將虛擬內(nèi)存的最高1G字節(jié)的空間作為內(nèi)核空間僅供內(nèi)核使用,低3G字節(jié)的空間供用戶進程使用,稱為用戶空間。

進程的狀態(tài)

  • 就緒
  • 阻塞
  • 運行

進程切換

文件描述符fd

緩存I/O

又被稱為標準I/O,大多數(shù)文件系統(tǒng)的默認I/O都是緩存I/O。在Linux系統(tǒng)的緩存I/O機制中,操作系統(tǒng)會將I/O的數(shù)據(jù)緩存在頁緩存(內(nèi)存)中,也就是數(shù)據(jù)先被拷貝到內(nèi)核的緩沖區(qū)(內(nèi)核地址空間),然后才會從內(nèi)核緩沖區(qū)拷貝到應(yīng)用程序的緩沖區(qū)(用戶地址空間)。

這種方式很明顯的缺點就是數(shù)據(jù)傳輸過程中需要再應(yīng)用程序地址空間和內(nèi)核空間進行多次數(shù)據(jù)拷貝操作,這些操作帶來的CPU以及內(nèi)存的開銷是非常大的。

二I/O模式

由于Linux系統(tǒng)采用的緩存I/O模式,對于一次I/O訪問,以讀操作舉例,數(shù)據(jù)先會被拷貝到內(nèi)核緩沖區(qū),然后才會從內(nèi)核緩沖區(qū)拷貝到應(yīng)用程序的緩存區(qū),當一個read系統(tǒng)調(diào)用發(fā)生的時候,會經(jīng)歷兩個階段:

  • 等待數(shù)據(jù)到來,進程處于阻塞狀態(tài)
  • 當數(shù)據(jù)準備就緒后,將數(shù)據(jù)從內(nèi)核拷貝到用戶進程,進程處于運行狀態(tài)

正是因為這兩個狀態(tài),Linux系統(tǒng)才產(chǎn)生了多種不同的網(wǎng)絡(luò)I/O模式的方案

Linux系統(tǒng)I/O模型

阻塞IO(blocking IO)

Linux系統(tǒng)默認情況下所有socke都是blocking的,一個讀操作流程如下:


以UDP socket為例,當用戶進程調(diào)用了recvfrom系統(tǒng)調(diào)用,如果數(shù)據(jù)還沒準備好,應(yīng)用進程被阻塞,內(nèi)核直到數(shù)據(jù)到來且將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到了應(yīng)用進程緩沖區(qū),然后向用戶進程返回結(jié)果,用戶進程才解除block狀態(tài),重新運行起來。

阻塞模行下只是阻塞了當前的應(yīng)用進程,其他進程還可以執(zhí)行,不消耗CPU時間,CPU的利用率較高。

非阻塞IO(nonblocking IO)

Linux可以設(shè)置socket為非阻塞的,非阻塞模式下執(zhí)行一個讀操作流程如下:


當用戶進程發(fā)出recvfrom系統(tǒng)調(diào)用時,如果kernel中的數(shù)據(jù)還沒準備好,recvfrom會立即返回一個error結(jié)果,不會阻塞用戶進程,用戶進程收到error時知道數(shù)據(jù)還沒準備好,過一會再調(diào)用recvfrom,直到kernel中的數(shù)據(jù)準備好了,內(nèi)核就立即將數(shù)據(jù)拷貝到用戶內(nèi)存然后返回ok,這個過程需要用戶進程去輪詢內(nèi)核數(shù)據(jù)是否準備好。

非阻塞模型下由于要處理更多的系統(tǒng)調(diào)用,因此CPU利用率比較低。

信號驅(qū)動IO

應(yīng)用進程使用sigaction系統(tǒng)調(diào)用,內(nèi)核立即返回,等到kernel數(shù)據(jù)準備好時會給用戶進程發(fā)送一個信號,告訴用戶進程可以進行IO操作了,然后用戶進程再調(diào)用IO系統(tǒng)調(diào)用如recvfrom,將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到應(yīng)用進程。流程如下:


相比于輪詢的方式,不需要多次系統(tǒng)調(diào)用輪詢,信號驅(qū)動IO的CPU利用率更高。

異步IO

異步IO模型與其他模型最大的區(qū)別是,異步IO在系統(tǒng)調(diào)用返回的時候所有操作都已經(jīng)完成,應(yīng)用進程既不需要等待數(shù)據(jù)準備,也不需要在數(shù)據(jù)到來后等待數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū),流程如下:



在數(shù)據(jù)拷貝完成后,kernel會給用戶進程發(fā)送一個信號告訴其read操作完成了。

IO多路復(fù)用

是用select、poll等待數(shù)據(jù),可以等待多個socket中的任一個變?yōu)榭勺x,這一過程會被阻塞,當某個套接字數(shù)據(jù)到來時返回,之后再用recvfrom系統(tǒng)調(diào)用把數(shù)據(jù)從內(nèi)核緩存區(qū)復(fù)制到用戶進程,流程如下:


流程類似阻塞IO,甚至比阻塞IO更差,多使用了一個系統(tǒng)調(diào)用,但是IO多路復(fù)用最大的特點是讓單個進程能同時處理多個IO事件的能力,又被稱為事件驅(qū)動IO,相比于多線程模型,IO復(fù)用模型不需要線程的創(chuàng)建、切換、銷毀,系統(tǒng)開銷更小,適合高并發(fā)的場景。

select

select是IO多路復(fù)用模型的一種實現(xiàn),當select函數(shù)返回后可以通過輪詢fdset來找到就緒的socket。

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

優(yōu)點是幾乎所有平臺都支持,缺點在于能夠監(jiān)聽的fd數(shù)量有限,Linux系統(tǒng)上一般為1024,是寫死在宏定義中的,要修改需要重新編譯內(nèi)核。而且每次都要把所有的fd在用戶空間和內(nèi)核空間拷貝,這個操作是比較耗時的。

poll

poll和select基本相同,不同的是poll沒有最大fd數(shù)量限制(實際也會受到物理資源的限制,因為系統(tǒng)的fd數(shù)量是有限的),而且提供了更多的時間類型。

int poll(struct pollfd *fds, unsigned int nfds, int timeout)

總結(jié):select和poll都需要在返回后通過輪詢的方式檢查就緒的socket,事實上同時連的大量socket在一個時刻只有很少的處于就緒狀態(tài),因此隨著監(jiān)視的描述符數(shù)量的變多,其性能也會逐漸下降。

epoll

epoll是select和poll的改進版本,更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個事件表中,這樣在用戶空間和內(nèi)核空間的copy只需一次。

int epoll_create(int size)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

epoll_create()用來創(chuàng)建一個epoll句柄。
epoll_ctl() 用于向內(nèi)核注冊新的描述符或者是改變某個文件描述符的狀態(tài)。已注冊的描述符在內(nèi)核中會被維護在一棵紅黑樹上,通過回調(diào)函數(shù)內(nèi)核會將 I/O 準備好的描述符加入到一個就緒鏈表中管理。
epoll_wait() 可以從就緒鏈表中得到事件完成的描述符,因此進程不需要通過輪詢來獲得事件完成的描述符。

LT模式(水平觸發(fā),默認)

當epoll_wait檢測到描述符IO事件發(fā)生并且通知給應(yīng)用程序時,應(yīng)用程序可以不立即處理該事件,下次調(diào)用epoll_wait還會再次通知該事件,支持block和nonblocking socket。

ET模式(邊緣觸發(fā))

當epoll_wait檢測到描述符IO事件發(fā)生并且通知給應(yīng)用程序時,應(yīng)用程序需要立即處理該事件,如果不立即處理,下次調(diào)用epoll_wait不會再次通知該事件。

ET模式在很大程度上減少了epoll事件被重復(fù)觸發(fā)的次數(shù),因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用nonblocking socket,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務(wù)餓死。

應(yīng)用場景

  • select的timeout參數(shù)精度為微妙,而poll和epoll都是毫秒,會因此select更加適合對實時性要求比較高的場景

  • poll 沒有最大描述符數(shù)量的限制,如果平臺支持并且對實時性要求不高,應(yīng)該使用 poll 而不是 select。

  • epoll適合高并發(fā)的場景,有大量描述符需要同時監(jiān)聽,并且最好是長連接。
    不適合監(jiān)控的描述符狀態(tài)變化頻繁且短暫,因為epoll的描述符都存在內(nèi)核中,每次對其狀態(tài)修改都要通過epoll_ctl系統(tǒng)調(diào)用來實現(xiàn),頻繁的系統(tǒng)調(diào)用導(dǎo)致頻繁在內(nèi)核態(tài)和用戶態(tài)切換,會大大降低性能。

參考

【segmentfault】Linux IO模式及 select、poll、epoll詳解
【GitHub】CyC2018/CS-Notes

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容