1 基礎
在引入IO模型前,先對io等待時某一段數據的"經歷"做一番解釋。如圖:
當某個程序或已存在的進程/線程(后文將不加區分的只認為是進程)需要某段數據時,它只能在用戶空間中屬于它自己的內存中訪問、修改,這段內存暫且稱之為App buffer。假設需要的數據在磁盤上,那么進程首先得發起相關系統調用,通知內核去加載磁盤上的文件。但正常情況下,數據只能加載到內核的緩沖區,暫且稱之為kernel buffer。數據加載到kernel buffer之后,還需將數據復制到app buffer。到了這里,進程就可以對數據進行訪問、修改了。
現在有幾個需要說明的問題。
(1).為什么不能直接將數據加載到app buffer呢?
實際上是可以的,有些程序或者硬件為了提高效率和性能,可以實現內核旁路的功能,避過內核的參與,直接在存儲設備和app buffer之間進行數據傳輸,例如RDMA技術就需要實現這樣的內核旁路功能。
但是,最普通也是絕大多數的情況下,為了安全和穩定性,數據必須先拷入內核空間的kernel buffer,再復制到app buffer,以防止進程串進內核空間進行破壞。
(2).上面提到的數據幾次拷貝過程,拷貝方式是一樣的嗎?
不一樣?,F在的存儲設備(包括網卡)基本上都支持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連接傳輸數據時的過程。
(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進程處理文件類請求時比較完整的數據操作流程。
大致解釋下:客戶端發起對某個文件的請求,通過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模型
如圖:
假設客戶端發起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模式。
如下圖:
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。
如下圖:
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多路復用時,模型如圖:
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信號只是通知了數據就緒,但并不知道有多少數據已經就緒。
如圖:
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在整個過程中都不會被阻塞。如圖:
看上去異步很好,但是注意,在復制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模型中,由內核在后臺默默的執行數據拷貝,所以原進程在此階段不被阻塞
如圖:
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()只是其中的一個環節:
大概描述下這個循環監控的過程:
(1).首先通過FD_ZERO宏函數初始化描述符集合。圖中每個小方格表示一個文件描述符。
(2).通過FD_SET宏函數創建描述符集合,此時集合中的文件描述符都被打開,也就是稍后要被select()監控的對象。
(3).使用select()函數監控描述符集合。當某個文件描述符滿足就緒條件時,select()函數返回集合中滿足條件的數量。圖中標黃色的小方塊表示滿足就緒條件的描述符。
(4).通過FD_ISSET宏函數遍歷整個描述符集合,并將滿足就緒條件的描述符發送給進程。同時,使用FD_CLR宏函數將滿足就緒條件的描述符從集合中移除。
(5).進入下一個循環,繼續使用FD_SET宏函數向描述符集合中添加新的待監控描述符。然后重復(3)、(4)兩個步驟。
如果使用簡單的偽代碼來描述:
FD_ZERO
for() {
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的處理方式中,根本就無需遍歷描述符集合。
備注:
這篇文章摘抄來自網絡。我打算總結一些列架構師需要的優秀文章,由于自己寫會花太多時間,我決定做一個搬運工,為大家篩選優秀的文章,最后我會做成索引方便大家查找。