五種IO模型詳解

架構師-網路文章彙總

1 基礎

在引入IO模型前,先對io等待時某一段資料的“經歷”做一番解釋。如圖:

五種IO模型詳解

當某個程式或已存在的程序/執行緒(後文將不加區分的只認為是程序)需要某段資料時,它只能在使用者空間中屬於它自己的記憶體中訪問、修改,這段記憶體暫且稱之為app buffer。假設需要的資料在磁碟上,那麼程序首先得發起相關係統呼叫,通知核心去載入磁碟上的檔案。但正常情況下,資料只能載入到核心的緩衝區,暫且稱之為kernel buffer。資料載入到kernel buffer之後,還需將資料複製到app buffer。到了這裡,程序就可以對資料進行訪問、修改了。

現在有幾個需要說明的問題。

(1)。為什麼不能直接將資料載入到app buffer呢?

實際上是可以的,有些程式或者硬體為了提高效率和效能,可以實現核心旁路的功能,避過核心的參與,直接在儲存裝置和app buffer之間進行資料傳輸,例如RDMA技術就需要實現這樣的核心旁路功能。

但是,最普通也是絕大多數的情況下,為了安全和穩定性,資料必須先拷入核心空間的kernel buffer,再複製到app buffer,以防止程序串進核心空間進行破壞。

(2)。上面提到的資料幾次複製過程,複製方式是一樣的嗎?

不一樣。現在的儲存裝置(包括網絡卡)基本上都支援DMA操作。什麼是DMA(direct memory access,直接記憶體訪問)?簡單地說,就是記憶體和裝置之間的資料互動可以直接傳輸,不再需要計算機的CPU參與,而是透過硬體上的晶片(可以簡單地認為是一個小cpu)進行控制。

假設,儲存裝置不支援DMA,那麼資料在記憶體和儲存裝置之間的傳輸,必須由核心執行緒佔用CPU去完成資料複製(比如網絡卡不支援DMA時,核心負責將資料從網絡卡複製到kernel buffer)。而DMA就釋放了計算機的CPU,讓它可以去處理其他任務,DMA也釋放了從使用者程序切換到核心的過程,從而避免了使用者程序在這個複製階段被阻塞。

再說kernel buffer和app buffer之間的複製方式,這是兩段記憶體空間的資料傳輸,只能由核心佔用CPU來完成複製。

所以,在載入硬碟資料到kernel buffer的過程是DMA複製方式,而從kernel buffer到app buffer的過程是CPU參與的複製方式。

(3)。如果資料要透過TCP連線傳輸出去要怎麼辦?

例如,web服務對客戶端的響應資料,需要透過TCP連線傳輸給客戶端。

TCP/IP協議棧維護著兩個緩衝區:send buffer和recv buffer,它們合稱為socket buffer。需要透過TCP連線傳輸出去的資料,需要先複製到send buffer,再複製給網絡卡透過網路傳輸出去。如果透過TCP連線接收到資料,資料首先透過網絡卡進入recv buffer,再被複制到使用者空間的app buffer。

同樣,在資料複製到send buffer或從recv buffer複製到app buffer時,是核心佔用CPU來完成的資料複製。從send buffer複製到網絡卡或從網絡卡複製到recv buffer時,是DMA方式的複製,這個階段不需要切換到核心,也不需要計算機自身的CPU。

如下圖所示,是透過TCP連線傳輸資料時的過程。

五種IO模型詳解

(4)。網路資料一定要從kernel buffer複製到app buffer再複製到send buffer嗎?

不是。如果程序不需要修改資料,就直接傳送給TCP連線的另一端,可以不用從kernel buffer複製到app buffer,而是直接複製到send buffer。這就是零複製技術。

例如,如果httpd程序不需要訪問和修改任何資料,那麼將資料原原本本地複製到app buffer再原原本本地複製到send buffer然後傳輸出去的過程中,從kernel buffer複製到app buffer的過程是可以省略的。使用零複製技術,就可以減少一次複製過程,提升效率。

當然,實現零複製技術的方法有多種,見我的另一篇結束零複製的文章:零複製(zero copy)技術。

以下是以httpd程序處理檔案類請求時比較完整的資料操作流程。

五種IO模型詳解

大致解釋下:客戶端發起對某個檔案的請求,透過TCP連線,請求資料進入TCP的recv buffer,再透過recv()函式將資料讀入到app buffer,此時httpd工作程序對資料進行一番解析,知道請求的是某個檔案,於是發起read系統呼叫,於是核心載入該檔案,資料從磁碟複製到kernel buffer再複製到app buffer,此時httpd就要開始構建響應資料了,可能會對資料進行一番修改,例如在響應首部中加一個欄位,最後將修改或未修改的資料複製(例如send()函式)到send buffer中,再透過TCP連線傳輸給客戶端。

2 I/O模型

所謂的IO模型,描述的是出現I/O等待時程序的狀態以及處理資料的方式。圍繞著程序的狀態、資料準備到kernel buffer再到app buffer的兩個階段展開。其中資料複製到kernel buffer的過程稱為資料準備階段,資料從kernel buffer複製到app buffer的過程稱為資料複製階段。請記住這兩個概念,後面描述I/O模型時會一直用這兩個概念。

本文某些地方以httpd程序的TCP連線方式處理本地檔案為例,請無視httpd是否真的實現瞭如此、那般的功能,也請無視TCP連線處理資料的細節,這裡僅僅只是作為方便解釋的示例而已。

再次說明,從硬體裝置到記憶體的資料傳輸過程是不需要CPU參與的,而記憶體間傳輸資料是需要核心執行緒佔用CPU來參與的。

2。1 Blocking I/O模型

如圖:

五種IO模型詳解

假設客戶端發起index。html的檔案請求,httpd需要將index。html的資料從磁碟中載入到自己的httpd app buffer中,然後複製到send buffer中傳送出去。

但是在httpd想要載入index。html時,它首先檢查自己的app buffer中是否有index。html對應的資料,沒有就發起系統呼叫讓核心去載入資料,例如read(),核心會先檢查自己的kernel buffer中是否有index。html對應的資料,如果沒有,則從磁碟中載入,然後將資料準備到kernel buffer,再複製到app buffer中,最後被httpd程序處理。

如果使用Blocking I/O模型:

(1)。當設定為blocking i/o模型,httpd從到都是被阻塞的。

(2)。只有當資料複製到app buffer完成後,或者發生了錯誤,httpd才被喚醒處理它app buffer中的資料。

(3)。cpu會經過兩次上下文切換:使用者空間到核心空間再到使用者空間,第一次是發起系統呼叫的切換,第二次是核心將資料複製到app buffer完成後的切換。

(4)。由於階段的複製是不需要CPU參與的,所以在階段準備資料的過程中,cpu可以去處理其它程序的任務。

(5)。階段的資料複製需要CPU參與,將httpd阻塞。

(6)。這是最省事、最簡單的IO模式。

如下圖:

五種IO模型詳解

2。2 Non-Blocking I/O模型

(1)。當設定為non-blocking時,httpd第一次發起系統呼叫(如read())後,立即返回一個錯誤值EWOULDBLOCK,而不是讓httpd進入睡眠狀態。UNP中也正是這麼描述的。

When we set a socket to be nonblocking, we are telling the kernel “when an I/O operation that I request cannot be completed without putting the process to sleep, do not put the process to sleep, but return an error instead。

(2)。雖然read()立即返回了,但httpd還要不斷地去傳送read()檢查核心:資料是否已經成功複製到kernel buffer了?這稱為輪詢(polling)。每次輪詢時,只要核心沒有把資料準備好,read()就返回錯誤資訊EWOULDBLOCK。

(3)。直到kernel buffer中資料準備完成,再去輪詢時不再返回EWOULDBLOCK,而是將httpd阻塞,以等待資料複製到app buffer。

(4)。httpd在到階段不被阻塞,但是會不斷去傳送read()輪詢。在被阻塞,將cpu交給核心把資料copy到app buffer。

如下圖:

五種IO模型詳解

2。3 I/O Multiplexing模型

稱為多路IO模型或IO複用,意思是可以檢查多個IO等待的狀態。有三種IO複用模型:select、poll和epoll。其實它們都是一種函式,用於監控指定檔案描述符的資料是否就緒。

就緒指的是對某個系統呼叫不再阻塞了,可以直接執行IO。例如對於read()來說,資料準備好了就是就緒狀態,此時read()可以直接去讀取資料且能立即讀取到資料,對write()來說,就是有空間可以寫入資料了(比如緩衝區未滿),此時write()可以直接寫入。

就緒種類包括是否可讀、是否可寫以及是否異常,其中可讀條件中就包括了資料是否準備好,也即資料是否已經在kernel buffer中。當就緒之後,將通知程序,程序再發送對資料操作的系統呼叫,如read()。

所以,這三個函式僅僅只是處理了資料是否準備好以及如何通知程序的問題。可以將這幾個函式結合阻塞和非阻塞IO模式使用,但通常IO複用都會結合非阻塞IO模式。

select()和poll()差不多,它們的監控和通知手段是類似的,只不過poll()要更聰明一點,某些時候效率也更高些,此處僅以select()監控單個檔案請求為例簡單介紹IO複用,至於更具體的、監控多個檔案以及epoll的方式,在本文的最後專門解釋。

(1)。當想要載入某個檔案時,假如httpd要發起read()系統呼叫,如果是阻塞或者非阻塞情形,那麼read()會根據資料是否準備好而決定是否返回。是否可以主動去監控這個資料是否準備到了kernel buffer中呢,亦或者是否可以監控send buffer中是否有新資料進入呢?這就是select()/poll()/epoll的作用。

(2)。當使用select()時,程序被select()所『阻塞』,之所以阻塞要加上引號,是因為select()有時間間隔選項可用控制阻塞時長,如果該選項設定為0,則select不阻塞而是立即返回,還可以設定為永久阻塞。

(3)。當select()的監控物件就緒時,httpd程序透過輪詢判斷知道可以執行read()了,於是httpd再發起read()系統呼叫,此時資料會從kernel buffer複製到app buffer中並read()成功。

(4)。httpd發起read()系統呼叫後切換到核心,由核心佔用CPU來複制資料到app buffer,所以httpd程序被阻塞。

上面的描述可能還太過抽象,這裡用shell虛擬碼來簡單描述select()的工作方式(細節並非準確,但易於理解)。假設有一個select命令,作用和select()函式相同。虛擬碼如下:

# select監控指定的檔案描述符,並返回已就緒的描述符數量給x # 程序將阻塞在select命令上,直到select返回 x=$(select fd1 fd2 fd3)  # 如果x大於0,說明有檔案描述符資料就緒,於是遍歷所有fd, # 並分別使用read去讀取這些fd,但並不知道具體是哪個fd已 # 就緒,所以read時最好是非阻塞的讀取,否則read每一個未 # 就緒的fd時都會阻塞 if [ x -gt 0 ];then   for fd in fd1 fd2 fd3;do    read -t 0 -u $fd  # read操作最好是非阻塞的   done fi

所以,在使用IO複用模型時,真正的IO操作(比如read)最好是非阻塞方式的,但並非必須。比如只監控一個檔案描述符時,select能返回意味著這個檔案描述符一定是就緒的(select還有其它返回值,但這裡不考慮其它返回值。

IO多路複用時,模型如圖:

五種IO模型詳解

select/poll的效能問題

select()/poll()的效能會隨著監控的檔案描述符數量增多而快速下降。其原因是多方面的。

其中一個原因是它們所監控的檔案描述符會以某種資料結構全部傳送給核心,讓核心負責監控這些檔案描述符,當核心發現某個檔案描述符已經就緒時,會修改資料結構,然後將修改後的資料結構傳回給程序。所以涉及了兩次資料傳輸的過程。

對於select()來說,每次傳遞的資料結構的大小是固定的,都是1024個描述符的大小。對於poll()來說,只會傳遞被監控的檔案描述符,所以檔案描述符少的時候,poll()的效能是可以的,此外poll()可以超出1024個檔案描述符的監控數量限制,但隨著描述符數量的增多,來回傳遞的資料量也是非常大的。

基於這方面的效能考慮,更建議使用訊號驅動IO或epoll模型,它們都是直接告訴核心要監控哪些檔案描述符,核心會以合適的資料結構安排這些待監控的檔案描述符(如epoll,核心採用紅黑樹的方式),換句話說,它們不會傳遞一大片的檔案描述符資料結構,效率更高。

使用IO複用還有很多細節,本文在此僅只是對其作最基本的功能性描述,在本文末還會多做一些擴充套件,至於更多的內容需自行了解。

2。4 Signal-driven I/O模型

即訊號驅動IO模型。當檔案描述符上設定了O_ASYNC標記時,就表示該檔案描述符是訊號驅動的IO。

注:可能你覺得O_ASYNC應該表示的是非同步IO,但並非如此。

在歷史上,訊號驅動IO也稱為非同步IO,比如這個標記就暗含了這一點歷史。

如今常說的術語【非同步】,是由POSIX AIO規範所提供的功能,這個非同步表示某程序發起IO操作時,立即返回,當IO完成或有錯誤時,該程序會收到通知,於是該程序可以去處理這個通知,比如執行回撥函式。

當某個檔案描述符使用訊號驅動IO模型時,要求程序配置訊號SIGIO的訊號處理程式,然後程序就可以做其他任何事情。當該檔案描述符就緒時,核心會向該程序傳送SIGIO訊號。該程序收到SIGIO訊號後,就會去執行已經配置號的訊號處理程式。

通常來說,SIGIO的訊號處理程式中會編寫read()類的讀取程式碼,這表示在收到SIGIO時在訊號處理程式中執行read操作,另一種常見的作法是在SIGIO的訊號處理程式中設定某變數標記,然後在外部判斷該標記是否為true,如果標記為true,則執行read類的操作。

使用Shell虛擬碼如下:

# 第一種模式:在訊號處理程式中執行IO操作 trap ‘read。。。’ SIGIO  # 設定SIGIO的訊號處理程式 # 然後就可以執行其它任意任務 # 當在執行其它任務過程中,核心傳送了SIGIO,程序會 # 立即去執行SIGIO訊號處理程式 。。。other codes。。。 ​ # 第二種模式:在訊號處理程式中設定變數標記,在外部執行IO操作 trap ‘a=1’ SIGIO 。。。 other codes。。。 #  while [ $a -eq 1 ];do  read。。。 done

很明顯,使用訊號驅動IO模型時,程序對該描述符的讀取是被動的,程序不會主動在描述符就緒前執行讀取操作。

其實訊號驅動IO模型就像是小姐姐在閒逛,小姐姐本沒有想過要買什麼東西,但如果發現有合適的,也會去買下來,在逛了一段時間後,一件超短裙閃現到小姐姐的視線,小姐姐很喜歡這個款式,於是立即決定買下來。

這裡可以做出大膽的推測,並非所有檔案描述符型別都能使用訊號驅動的IO模型。如果某檔案描述符想要開啟訊號驅動IO,要求有某個另一端會主動向該描述符傳送資料,比如管道、套接字、終端等都符合這種要求。顯然,普通檔案系統上的檔案IO是無法使用訊號驅動IO的。

回到訊號驅動IO模型,由於程序沒有主動執行IO操作,所以不會阻塞,當資料就緒後,程序收到核心傳送的SIGIO訊號,程序會去執行SIGIO的訊號處理程式,當程序執行read()時,由於資料已經就緒,所以可以直接將資料從kernel buffer複製到app buffer,read()過程中,程序將被阻塞。

注意:sigio訊號只是通知了資料就緒,但並不知道有多少資料已經就緒。

如圖:

五種IO模型詳解

2。5 Asynchronous I/O模型

即非同步IO模型。

非同步IO來自於POSIX AIO規範,它專門提供了能非同步IO的讀寫類函式,如aio_read(),aio_write()等。

使用非同步IO函式時,要求指定IO完成時或IO出現錯誤時的通知方式,通知方式主要分兩類:

傳送指定的訊號來通知

在另一個執行緒中執行指定的回撥函式

為了幫助理解,這裡假設aio_read()的語法如下(真實的語法要複雜的多):

aio_read(x,y,z,notify_mode,notify_value)

其中nofity_mode允許的值有兩種:

當notify_mode引數的值為SIGEV_SIGNAL時,notify_value引數的值為一個訊號

當notify_mode引數的值為SIGEV_THREAD,notify_value引數的值為一個函式,這個函式稱為回撥函式

當使用非同步IO函式時,程序不會因為要執行IO操作而阻塞,而是立即返回。

例如,當程序執行非同步IO函式aio_read()時,它會請求核心執行具體的IO操作,當資料已經就緒且從kernel buffer複製到app buffer後,核心認為IO操作已經完成,於是核心會根據呼叫非同步IO函式時指定的通知方式來執行對應的操作:

如果通知模式是訊號通知方式(SIGEV_SIGNAL),則在IO完成時,核心會向程序傳送notify_value指定的訊號

如果通知模式是訊號回撥方式(SIGEV_THREAD),則在IO完成時,核心會在一個獨立的執行緒中執行notify_value指定的回撥函式

回顧一下訊號驅動IO,訊號驅動IO要求有另一端主動向檔案描述符寫入資料,所以它支援像socket、pipe、terminal這類檔案描述符,但不支援普通檔案IO的檔案描述符。

而非同步IO則沒有這個限制,非同步IO操作藉助的是那些具有神力的非同步函式,只要檔案描述符能讀寫,就能使用非同步IO函式來實現非同步IO。

所以,非同步IO在整個過程中都不會被阻塞。如圖:

五種IO模型詳解

看上去非同步很好,但是注意,在複製kernel buffer資料到app buffer中時是需要CPU參與的,這意味著不受阻的程序會和非同步呼叫函式爭用CPU。以httpd為例,如果併發量比較大,httpd接入的連線數可能就越多,CPU爭用情況就越嚴重,非同步函式返回成功訊號的速度就越慢。如果不能很好地處理這個問題,非同步IO模型也不一定就好。

2。6 同步IO和非同步IO、阻塞和非阻塞的區分

阻塞和非阻塞,體現在當前程序是否可執行,是否能獲取到CPU。

當阻塞和非阻塞的概念體現在IO模型上:

阻塞IO:從開始發起IO操作開始就阻塞,直到IO完成才返回,所以程序會立即進入睡眠態

非阻塞IO:發起IO操作時,如果當前資料已就緒,則切換到核心態由核心完成資料複製(從kernel buffer複製到app buffer),此時程序被阻塞,因為它的CPU已經被核心搶走了。如果發起IO操作時資料未就緒,則立即返回而不阻塞,即程序繼續享有CPU,可以繼續任務。但程序不知道資料何時就緒,所以通常會採用輪循程式碼(比如while迴圈)不斷判斷資料是否就緒,當資料最終就緒後,切換到核心態,程序仍然被阻塞

同步和非同步,考慮的是兩邊資料是否同步(比如kernel buffer和app buffer之間資料是否同步)。同步和非同步的區別體現在兩邊資料尚未完成同步時的行為:

同步:在保持兩邊資料同步的過程中,程序被阻塞,由核心搶佔其CPU去完成資料同步,直到兩邊資料同步,程序才被喚醒

非同步:在保持兩邊資料同步的過程中,由核心默默地在後臺完成資料同步(如果不理解,可認為是單獨開了一個核心執行緒負責資料同步),核心不會搶佔程序的CPU,所以程序自身不被阻塞,當核心完成兩端資料同步時,通知程序已同步完成

這裡阻塞和非阻塞、同步和非同步都是廣義的概念,上面所做的解釋適用於所有使用這些術語的情況,而不僅僅是本文所專注的IO模型。

回到阻塞、非阻塞、同步、非同步的IO模型,再對它們囉嗦囉嗦。

阻塞、非阻塞、IO複用、訊號驅動都是同步IO模型。需注意,雖然不同IO模型在載入資料到kernel buffer的資料準備過程中可能阻塞、可能不阻塞,但kernel buffer才是read()函式讀取資料時的物件,同步的意思是讓kernel buffer和app buffer資料同步。在保持kernel buffer和app buffer同步的過程中,CPU將從執行read()操作的程序切換到核心態,核心獲取CPU複製資料到app buffer,所以執行read()操作的程序在這個同步的階段中是被阻塞的。

只有非同步IO模型才是非同步的,因為它呼叫的是具有【神力】的非同步IO函式(如aio_read()),呼叫這些函式時會請求核心,當資料已經複製到app buffer後,通知程序並執行指定的操作。

需要注意的是,無論是哪種IO模型,在將資料從kernel buffer複製到app buffer的這個階段,都是需要CPU參與的。只不過,同步IO模型和非同步IO模型中,CPU參與的方式不一樣:

同步IO模型中,呼叫read()的程序會切換到核心,由核心佔用CPU來執行資料複製,所以原程序在此階段一直被阻塞

非同步IO模型中,由核心在後臺默默的執行資料複製,所以原程序在此階段不被阻塞

如圖:

五種IO模型詳解

2。7 訊號驅動IO和非同步IO的區別

很多人都不理解訊號驅動IO和非同步IO之間的區別,一方面是因為它們都立即返回,另一方面是因為它們看似都是被動的或後臺的。

但其實在前文已經分析清楚了它們的區別,這裡僅做總結性分析。在此之前,還是借用前文使用過的類比。

訊號驅動IO模型:小姐姐在逛街,小姐姐本沒有想過要買什麼東西,但如果發現有合適的,也會去買下來,在逛了一段時間後,一件超短裙閃現到小姐姐的視線,小姐姐很喜歡這個款式,於是立即決定買下來,買的時候小姐姐不能再幹其它事情。

非同步IO模型:小姐姐在逛街,她這次帶上了男朋友,只要想買東西,都可以讓男朋友去幫忙買,而小姐姐可以繼續自己逛自己的,男朋友買好後通知小姐姐即可。

1。非同步IO

非同步IO透過呼叫具有非同步IO能力的函式來實現。在呼叫非同步函式時,要求指定IO完成時的通知方式。

當IO完成後,核心(這裡的核心是廣義的,不再侷限於作業系統核心,它也可以是瀏覽器核心,或語言的直譯器,或語言的虛擬機器)要麼通知程序,要麼執行回撥函式。

這裡所謂的IO完成,表示的是已經保持了兩邊資料的同步(比如kernel buffer和app buffer之間)。而非同步之所以稱為非同步,就體現在完成兩邊資料同步的階段中,它表示由核心在後臺默默完成資料的同步任務。

對於非同步IO來說,它不在乎什麼型別的檔案描述符,socket、pipe、fifo、terminal以及普通檔案都可以執行非同步IO。

2。訊號驅動IO

訊號驅動IO是同步IO模型。

當某個檔案描述符設定了O_ASYNC標記時(前文說過,稱呼為O_ASYNC是歷史原因),表示該檔案描述符開啟訊號驅動IO的功能。

使用訊號驅動IO,要求程序註冊SIGIO的訊號處理程式,註冊之後,程序就可以做其他任務。

當有另一端向該描述符寫入資料時,就意味著該檔案描述符已經就緒,核心會發送SIGIO訊號給程序,於是程序會去執行已經註冊的SIGIO訊號處理程式。一般來說,訊號處理程式中,要麼是read()類的讀取函式,要麼是為後面是否讀取做判斷的變數標記。

但是,核心傳送SIGIO訊號只是通知程序資料已經就緒,但就緒了多少資料量,程序並不知道。

而且,程序因為收到通知而認為可以資料已就緒,於是執行read(),程序在執行read()的時候,CPU將從使用者態切換到核心態,由核心獲取CPU來執行資料同步操作,所以在這個階段中,程序的read()是被阻塞的。

因為訊號驅動要求有另一端主動寫入資料,所以socket、pipe、fifo、terminal等檔案描述符型別是可以訊號驅動IO 的,但是不支援對普通檔案使用訊號驅動IO。

3。select()、poll()和epoll

前面說了,這三個函式是檔案描述符狀態監控的函式,它們可以監控一系列檔案的一系列事件,當出現滿足條件的事件後,就認為是就緒或者錯誤。事件大致分為3類:可讀事件、可寫事件和異常事件。它們通常都放在迴圈結構中進行迴圈監控。

select()和poll()函式處理方式的本質類似,只不過poll()稍微先進一點,而epoll處理方式就比這兩個函式先進多了。當然,就算是先進分子,在某些情況下效能也不一定就比老傢伙們強。

select() & poll()

首先,透過FD_SET宏函式建立待監控的描述符集合,並將此描述符集合作為select()函式的引數,可以在指定select()函式阻塞時間間隔,於是select()就建立了一個監控物件。

除了普通檔案描述符,還可以監控套接字,因為套接字也是檔案,所以select()也可以監控套接字檔案描述符,例如recv buffer中是否收到了資料,也即監控套接字的可讀性,send buffer中是否滿了,也即監控套接字的可寫性。select()預設最大可監控1024個檔案描述符。而poll()則沒有此限制。

select()的時間間隔引數分3種:

(1)。設定為指定時間間隔內阻塞,除非之前有就緒事件發生。

(2)。設定為永久阻塞,除非有就緒事件發生。

(3)。設定為完全不阻塞,即立即返回。但因為select()通常在迴圈結構中,所以這是輪詢監控的方式。

當建立了監控物件後,由核心監控這些描述符集合,於此同時呼叫select()的程序被阻塞(或輪詢)。當監控到滿足就緒條件時(監控事件發生),select()將被喚醒(或暫停輪詢),於是select()返回滿足就緒條件的描述符數量,之所以是數量而不僅僅是一個,是因為多個檔案描述符可能在同一時間滿足就緒條件。由於只是返回數量,並沒有返回哪一個或哪幾個檔案描述符,所以通常在使用select()之後,還會在迴圈結構中的if語句中使用宏函式FD_ISSET進行遍歷,直到找出所有的滿足就緒條件的描述符。最後將描述符集合透過指定函式複製回用戶空間,以便被程序處理。

監聽描述符集合的大致過程如下圖所示,其中select()只是其中的一個環節:

五種IO模型詳解

大概描述下這個迴圈監控的過程:

(1)。首先透過FD_ZERO宏函式初始化描述符集合。圖中每個小方格表示一個檔案描述符。

(2)。透過FD_SET宏函式建立描述符集合,此時集合中的檔案描述符都被開啟,也就是稍後要被select()監控的物件。

(3)。使用select()函式監控描述符集合。當某個檔案描述符滿足就緒條件時,select()函式返回集合中滿足條件的數量。圖中標黃色的小方塊表示滿足就緒條件的描述符。

(4)。透過FD_ISSET宏函式遍歷整個描述符集合,並將滿足就緒條件的描述符傳送給程序。同時,使用FD_CLR宏函式將滿足就緒條件的描述符從集合中移除。

(5)。進入下一個迴圈,繼續使用FD_SET宏函式向描述符集合中新增新的待監控描述符。然後重複(3)、(4)兩個步驟。

如果使用簡單的虛擬碼來描述:

FD_ZEROfor() { FD_SET() select() if(){ FD_ISSET() FD_CLR() } writen()}

以上所說只是一種需要迴圈監控的示例,具體如何做卻是不一定的。不過從中也能看出這一系列的流程。

epoll

epoll比poll()、select()先進,考慮以下幾點,自然能看出它的優勢所在:

(1)。epoll_create()建立的epoll例項可以隨時透過epoll_ctl()來新增和刪除感興趣的檔案描述符,不用再和select()每個迴圈後都要使用FD_SET更新描述符集合的資料結構。

(2)。在epoll_create()建立epoll例項時,還建立了一個epoll就緒連結串列list。而epoll_ctl()每次向epoll例項新增描述符時,還會註冊該描述符的回撥函式。當epoll例項中的描述符滿足就緒條件時將觸發回撥函式,被移入到就緒連結串列list中。

(3)。當呼叫epoll_wait()進行監控時,它只需確定就緒連結串列中是否有資料即可,如果有,將複製到使用者空間以被程序處理,如果沒有,它將被阻塞。當然,如果監控的物件設定為非阻塞模式,它將不會被阻塞,而是不斷地去檢查。

也就是說,epoll的處理方式中,根本就無需遍歷描述符集合。

備註:

這篇文章摘抄來自網路。我打算總結一些列架構師需要的優秀文章,由於自己

寫會

花太多時間,我決定做一個搬運工,為大家篩選優秀的文章,最後我會做成索引方便大家查詢。