碼農有道 在網絡數(shù)據(jù)傳輸時經歷了哪些buffer(請戳我)一文中主要總結了從客戶端發(fā)起一個http請求,網絡數(shù)據(jù)的流向,有了上文的基礎,本文再來講講五種I/O模型。 所謂的IO模型,描述的是出現(xiàn)I/O等待時進程的狀態(tài)以及處理數(shù)據(jù)的方式。圍繞著進程的狀態(tài)、數(shù)據(jù)準備到kernel buffer再到app buffer的兩個階段展開。其中數(shù)據(jù)復制到kernel buffer的過程稱為數(shù)據(jù)準備階段,數(shù)據(jù)從kernel buffer復制到app buffer的過程稱為數(shù)據(jù)復制階段。請記住這兩個概念,后面描述I/O模型時會一直用這兩個概念。 本文以httpd進程的TCP連接方式處理本地文件為例,請無視httpd是否真的實現(xiàn)了如此、那般的功能,也請無視TCP連接處理數(shù)據(jù)的細節(jié),這里僅僅只是作為方便解釋的示例而已。另外,本文用本地文件作為I/O模型的對象不是很適合,它的重頭戲是在套接字上, 再次說明:從硬件設備到內存的數(shù)據(jù)傳輸過程是不需要CPU參與的,而內存間傳輸數(shù)據(jù)是需要CPU參與的。 如圖: 假設客戶端發(fā)起index.html的文件請求,httpd需要將index.html的數(shù)據(jù)從磁盤中加載到自己的httpd app buffer中,然后復制到send buffer中發(fā)送出去。 但是在httpd想要加載index.html時,它首先檢查自己的app buffer中是否有index.html對應的數(shù)據(jù),沒有就發(fā)起系統(tǒng)調用讓內核去加載數(shù)據(jù),例如read(),內核會先檢查自己的kernel buffer中是否有index.html對應的數(shù)據(jù),如果沒有,則從磁盤中加載,然后將數(shù)據(jù)準備到kernel buffer,再復制到app buffer中,最后被httpd進程處理。 如果使用Blocking I/O模型: 1:當設置為blocking i/o模型,httpd從到都是被阻塞的。 2:只有當數(shù)據(jù)復制到app buffer完成后,或者發(fā)生了錯誤,httpd才被喚醒處理它app buffer中的數(shù)據(jù)。 3:cpu會經過兩次上下文切換:用戶空間到內核空間再到用戶空間。 4:由于階段的拷貝是不需要CPU參與的,所以在階段準備數(shù)據(jù)的過程中,cpu可以去處理其它進程的任務。 5:階段的數(shù)據(jù)復制需要CPU參與,將httpd阻塞,在某種程度上來說,有助于提升它的拷貝速度。 如下圖: 如果使用Non-Blocking I/O模型: 1:.當設置為non-blocking時,httpd第一次發(fā)起系統(tǒng)調用(如read())后,立即返回一個錯誤值EWOULDBLOCK(至于read()讀取一個普通文件時能否返回EWOULDBLOCK請無視,畢竟I/O模型主要是針對套接字文件的,就當read()是recv()好了),而不是讓httpd進入睡眠狀態(tài)。 2:雖然read()立即返回了,但httpd還要不斷地去發(fā)送read()檢查內核:數(shù)據(jù)是否已經成功拷貝到kernel buffer了?這稱為輪詢(polling)。每次輪詢時,只要內核沒有把數(shù)據(jù)準備好,read()就返回錯誤信息EWOULDBLOCK。 3:直到kernel buffer中數(shù)據(jù)準備完成,再去輪詢時不再返回EWOULDBLOCK,而是將httpd阻塞,以等待數(shù)據(jù)復制到app buffer。 4:httpd在到階段不被阻塞,但是會不斷去發(fā)送read()輪詢。在被阻塞,將cpu交給內核把數(shù)據(jù)copy到app buffer。 如下圖: 稱為多路IO模型或IO復用,意思是可以檢查多個IO等待的狀態(tài)。有三種IO復用模型:select、poll和epoll。其實它們都是一種函數(shù),用于監(jiān)控指定文件描述符的數(shù)據(jù)是否就緒,就緒指的是對某個系統(tǒng)調用不再阻塞了,例如對于read()來說,就是數(shù)據(jù)準備好了就是就緒狀態(tài)。 就緒種類包括是否可讀、是否可寫以及是否異常,其中可讀條件中就包括了數(shù)據(jù)是否準備好。當就緒之后,將通知進程,進程再發(fā)送對數(shù)據(jù)操作的系統(tǒng)調用,如read()。所以,這三個函數(shù)僅僅只是處理了數(shù)據(jù)是否準備好以及如何通知進程的問題??梢詫⑦@幾個函數(shù)結合阻塞和非阻塞IO模式使用,例如設置為非阻塞時,select()/poll()/epoll將不會阻塞在對應的描述符上,調用函數(shù)的進程/線程也就不會被阻塞。 如果使用I/O Multiplexing模型: 1:當想要加載某個文件時,假如httpd要發(fā)起read()系統(tǒng)調用,如果是阻塞或者非阻塞情形,那么read()會根據(jù)數(shù)據(jù)是否準備好而決定是否返回,是否可以主動去監(jiān)控這個數(shù)據(jù)是否準備到了kernel buffer中呢,亦或者是否可以監(jiān)控send buffer中是否有新數(shù)據(jù)進入呢?這就是select()/poll()/epoll的作用。 2:當使用select()時,httpd發(fā)起一個select調用,然后httpd進程被select()'阻塞'。由于此處假設只監(jiān)控了一個請求文件,所以select()會在數(shù)據(jù)準備到kernel buffer中時直接喚醒httpd進程。之所以阻塞要加上雙引號,是因為select()有時間間隔選項可用控制阻塞時長,如果該選項設置為0,則select不阻塞,此時表示立即返回但一直輪詢檢查是否就緒,還可以設置為永久阻塞。 3:當select()的監(jiān)控對象就緒時,將通知(輪詢情況)或喚醒(阻塞情況)httpd進程,httpd再發(fā)起read()系統(tǒng)調用,此時數(shù)據(jù)會從kernel buffer復制到app buffer中并read()成功。 4:httpd發(fā)起第二個系統(tǒng)調用(即read())后被阻塞,CPU全部交給內核用來復制數(shù)據(jù)到app buffer。 (5).對于httpd只處理一個連接的情況下,IO復用模型還不如blocking I/O模型,因為它前后發(fā)起了兩個系統(tǒng)調用(即select()和read()),甚至在輪詢的情況下會不斷消耗CPU。但是IO復用的優(yōu)勢就在于能同時監(jiān)控多個文件描述符。 如圖: 即信號驅動IO模型。當開啟了信號驅動功能時,首先發(fā)起一個信號處理的系統(tǒng)調用,如sigaction(),這個系統(tǒng)調用會立即返回。但數(shù)據(jù)在準備好時,會發(fā)送SIGIO信號,進程收到這個信號就知道數(shù)據(jù)準備好了,于是發(fā)起操作數(shù)據(jù)的系統(tǒng)調用,如read()。 在發(fā)起信號處理的系統(tǒng)調用后,進程不會被阻塞,但是在read()將數(shù)據(jù)從kernel buffer復制到app buffer時,進程是被阻塞的。如圖: 即異步IO模型。當設置為異步IO模型時,httpd首先發(fā)起異步系統(tǒng)調用(如aio_read(),aio_write()等),并立即返回。這個異步系統(tǒng)調用告訴內核,不僅要準備好數(shù)據(jù),還要把數(shù)據(jù)復制到app buffer中。 httpd從返回開始,直到數(shù)據(jù)復制到app buffer結束都不會被阻塞。當數(shù)據(jù)復制到app buffer結束,將發(fā)送一個信號通知httpd進程。 如圖: 看上去異步很好,但是注意,在復制kernel buffer數(shù)據(jù)到app buffer中時是需要CPU參與的,這意味著不受阻的httpd會和異步調用函數(shù)爭用CPU。如果并發(fā)量比較大,httpd接入的連接數(shù)可能就越多,CPU爭用情況就越嚴重,異步函數(shù)返回成功信號的速度就越慢。如果不能很好地處理這個問題,異步IO模型也不一定就好。 阻塞、非阻塞、IO復用、信號驅動都是同步IO模型。因為在發(fā)起操作數(shù)據(jù)的系統(tǒng)調用(如本文的read())過程中是被阻塞的。這里要注意,雖然在加載數(shù)據(jù)到kernel buffer的數(shù)據(jù)準備過程中可能阻塞、可能不阻塞,但kernel buffer才是read()函數(shù)的操作對象,同步的意思是讓kernel buffer和app buffer數(shù)據(jù)同步。顯然,在保持kernel buffer和app buffer同步的過程中,進程必須被阻塞,否則read()就變成異步的read()。 只有異步IO模型才是異步的,因為發(fā)起的異步類的系統(tǒng)調用(如aio_read())已經不管kernel buffer何時準備好數(shù)據(jù)了,就像后臺一樣read一樣,aio_read()可以一直等待kernel buffer中的數(shù)據(jù),在準備好了之后,aio_read()自然就可以將其復制到app buffer。 如圖: |
|