五種IO模型分析

分類:IT技術 時間:2017-10-05

本文目錄:
1. 基礎
2. I/O模型
2.1 Blocking I/O模型
2.2 Non-Blocking I/O模型
2.3 I/O Multiplexing模型
2.4 Signal-driven I/O模型
2.5 Asynchronous I/O模型
2.6 同步IO和異步IO、阻塞和非阻塞的區分
3. select()、poll()和epoll
3.1 select() & poll()
3.2 epoll

1. 基礎

在引入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計算從哪個地址中獲取數據、拷入到對方的哪些地址、拷入多少數據(多少個數據塊、數據塊在哪裏)等等,僅僅完成一次數據傳輸,CPU都要做很多事情。而DMA就釋放了計算機的CPU,讓它可以去處理其他任務。

再說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操作方式的拷貝。

如下圖所示,是通過TCP連接傳輸數據時的過程。

(4).網絡數據一定要從kernel buffer復制到app buffer再復制到send buffer嗎

不是。如果進程不需要修改數據,就直接發送給TCP連接的另一端,可以不用從kernel buffer復制到app buffer,而是直接復制到send buffer。這就是零復制技術。

例如httpd不需要訪問和修改任何信息時,將數據原原本本地復制到app buffer再原原本本地復制到send 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連接處理數據的細節,這裏僅僅只是作為方便解釋的示例而已。另外,本文用本地文件作為I/O模型的對象不是很適合,它的重頭戲是在套接字上,如果想要看處理TCP/UDP過程中套接字的I/O模型,請看完此文後,再結合我的另一篇文章"不可不知的socket和TCP連接過程"以重新認識I/O模型。

再次說明,從硬件設備到內存的數據傳輸過程是不需要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會經過兩次上下文切換:用戶空間到內核空間再到用戶空間。
(4).由於階段的拷貝是不需要CPU參與的,所以在階段準備數據的過程中,cpu可以去處理其它進程的任務。
(5).階段的數據復制需要CPU參與,將httpd阻塞,在某種程度上來說,有助於提升它的拷貝速度。
(6).這是最省事、最簡單的IO模式。

如下圖:

2.1 Non-Blocking I/O模型

(1).當設置為non-blocking時,httpd第一次發起系統調用(如read())後,立即返回一個錯誤值EWOULDBLOCK(至於read()讀取一個普通文件時能否返回EWOULDBLOCK請無視,畢竟I/O模型主要是針對套接字文件的,就當read()是recv()好了),而不是讓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。其實它們都是一種函數,用於監控指定文件描述符的數據是否就緒,就緒指的是對某個系統調用不再阻塞了,例如對於read()來說,就是數據準備好了就是就緒狀態。就緒種類包括是否可讀、是否可寫以及是否異常,其中可讀條件中就包括了數據是否準備好。當就緒之後,將通知進程,進程再發送對數據操作的系統調用,如read()。所以,這三個函數僅僅只是處理了數據是否準備好以及如何通知進程的問題。可以將這幾個函數結合阻塞和非阻塞IO模式使用,例如設置為非阻塞時,select()/poll()/epoll將不會阻塞在對應的描述符上,調用函數的進程/線程也就不會被阻塞。

select()和poll()差不多,它們的監控和通知手段是一樣的,只不過poll()要更聰明一點,所以此處僅以select()監控單個文件請求為例簡單介紹IO復用,至於更具體的、監控多個文件以及epoll的方式,在本文的最後專門解釋。

(1).當想要加載某個文件時,假如httpd要發起read()系統調用,如果是阻塞或者非阻塞情形,那麽read()會根據數據是否準備好而決定是否返回,是否可以主動去監控這個數據是否準備到了kernel buffer中呢,亦或者是否可以監控send buffer中是否有新數據進入呢?這就是select()/poll()/epoll的作用。
(2).當使用select()時,httpd發起一個select調用,然後httpd進程被select()"阻塞"。由於此處假設只監控了一個請求文件,所以select()會在數據準備到kernel buffer中時直接喚醒httpd進程。之所以阻塞要加上雙引號,是因為select()有時間間隔選項可用控制阻塞時長,如果該選項設置為0,則select不阻塞,此時表示立即返回但一直輪詢檢查是否就緒,還可以設置為永久阻塞。
(3).當select()的監控對象就緒時,將通知(輪詢情況)或喚醒(阻塞情況)httpd進程,httpd再發起read()系統調用,此時數據會從kernel buffer復制到app buffer中並read()成功。
(4).httpd發起第二個系統調用(即read())後被阻塞,CPU全部交給內核用來復制數據到app buffer。 (5).對於httpd只處理一個連接的情況下,IO復用模型還不如blocking I/O模型,因為它前後發起了兩個系統調用(即select()和read()),甚至在輪詢的情況下會不斷消耗CPU。但是IO復用的優勢就在於能同時監控多個文件描述符。

如圖:

更詳細的說明,見本文末。

2.4 Signal-driven I/O模型

即信號驅動IO模型。當開啟了信號驅動功能時,首先發起一個信號處理的系統調用,如sigaction(),這個系統調用會立即返回。但數據在準備好時,會發送SIGIO信號,進程收到這個信號就知道數據準備好了,於是發起操作數據的系統調用,如read()。

在發起信號處理的系統調用後,進程不會被阻塞,但是在read()將數據從kernel buffer復制到app buffer時,進程是被阻塞的。如圖:

2.5 Asynchronous I/O模型

即異步IO模型。當設置為異步IO模型時,httpd首先發起異步系統調用(如aio_read(),aio_write()等),並立即返回。這個異步系統調用告訴內核,不僅要準備好數據,還要把數據復制到app buffer中。

httpd從返回開始,直到數據復制到app buffer結束都不會被阻塞。當數據復制到app buffer結束,將發送一個信號通知httpd進程。

如圖:

看上去異步很好,但是註意,在復制kernel buffer數據到app buffer中時是需要CPU參與的,這意味著不受阻的httpd會和異步調用函數爭用CPU。如果並發量比較大,httpd接入的連接數可能就越多,CPU爭用情況就越嚴重,異步函數返回成功信號的速度就越慢。如果不能很好地處理這個問題,異步IO模型也不一定就好。

2.6 同步IO和異步IO、阻塞和非阻塞的區分

阻塞、非阻塞、IO復用、信號驅動都是同步IO模型。因為在發起操作數據的系統調用(如本文的read())過程中是被阻塞的。這裏要註意,雖然在加載數據到kernel buffer的數據準備過程中可能阻塞、可能不阻塞,但kernel buffer才是read()函數的操作對象,同步的意思是讓kernel buffer和app buffer數據同步。顯然,在保持kernel buffer和app buffer同步的過程中,進程必須被阻塞,否則read()就變成異步的read()。

只有異步IO模型才是異步的,因為發起的異步類的系統調用(如aio_read())已經不管kernel buffer何時準備好數據了,就像後臺一樣read一樣,aio_read()可以一直等待kernel buffer中的數據,在準備好了之後,aio_read()自然就可以將其復制到app buffer。

如圖:

3 select()、poll()和epoll

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

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

3.1 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()
}

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

3.2 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的處理方式中,根本就無需遍歷描述符集合。

回到linux系列文章大綱:http://www.cnblogs.com/f-ck-need-u/p/7048359.html

回到網站架構系列文章大綱:http://www.cnblogs.com/f-ck-need-u/p/7576137.html

回到數據庫系列文章大綱:http://www.cnblogs.com/f-ck-need-u/p/7586194.html

轉載請註明出處:http://www.cnblogs.com/f-ck-need-u/p/7624733.html

註:若您覺得這篇文章還不錯請點擊右下角推薦,您的支持能激發作者更大的寫作熱情,非常感謝!


Tags: button font-size display margin input overflow

文章來源:


ads
ads

相關文章
ads

相關文章

ad