NGINX的IO模型詳解


普及:

用戶空間與內核空間:

    現在操作系統都是采用虛擬存儲器,那么對32位操作系統而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。操作系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。為了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操作系統將虛擬空間划分為兩部分,一部分為內核空間,一部分為用戶空間。針對linux操作系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱為內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。

進程切換:

    為了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復以前掛起的某個進程的執行。這種行為被稱為進程切換。因此可以說,任何進程都是在操作系統內核的支持下運行的,是與內核緊密相關的。
從一個進程的運行轉到另一個進程上運行,這個過程中經過下面這些變化:
保存處理機上下文,包括程序計數器和其他寄存器。
更新PCB信息。
把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。
選擇另一個進程執行,並更新其PCB。
更新內存管理的數據結構。
恢復處理機上下文。

注:總而言之就是很耗資源,具體的可以參考這篇文章:

http://guojing.me/linux-kernel-architecture/posts/process-switch/

進程阻塞:

    正在執行的進程,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新數據尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由運行狀態變為阻塞狀態。可見,進程的阻塞是進程自身的一種主動行為,也因此只有處於運行態的進程(獲得CPU),才可能將其轉為阻塞狀態。當進程進入阻塞狀態,是不占用CPU資源的。

文件描述符:

    文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞着文件描述符展開。但是文件描述符這一概念往往只適用於UNIX、Linux這樣的操作系統。

緩存IO:

    緩存 IO 又被稱作標准 IO,大多數文件系統的默認 IO 操作都是緩存 IO。在 Linux 的緩存 IO 機制中,操作系統會將 IO 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操作系統內核的緩沖區中,然后才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。

緩存IO的缺點:

    數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。

Linux IO模型:

    網絡IO的本質是socket的讀取,socket在linux系統中被抽象為流,IO可以理解為對流的操作.對於一次IO訪問,數據會先被拷到操作系統內核的緩沖區中,然后才會從操作系統內核的緩沖區拷貝到應用程序的地址空間,所以說當一個read操作發生時,它會經理兩個階段:

第一階段:等待數據准備 (Waiting for the data to be ready)。
第二階段:將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)。

    對socket流而言:

第一步:通常涉及等待網絡上的數據分組到達,然后被復制到內核的某個緩沖區。
第二步:把數據從內核緩沖區復制到應用進程緩沖區。

網絡應用需要處理的無非就是兩大類問題,網絡IO,數據計算。相對於后者,網絡IO的延遲,給應用帶來的性能瓶頸大於后者。網絡IO的模型大致有如下幾種:

同步模型(synchronous IO) 
   阻塞IO(bloking IO)
   非阻塞IO(non-blocking IO)
   多路復用IO(multiplexing IO)
   信號驅動式IO(signal-driven IO)
異步IO(asynchronous IO)

同步4個:阻塞IO、非阻塞IO、多路復用IO、信號驅使式IO

異步1個:異步IO

注:由於信號驅動式(signal driven IO)在實際中並不常用,所以我這只提及剩下的四種IO Model。

在深入介紹Linux IO各種模型之前,讓我們先來探索一下基本 Linux IO 模型的簡單矩陣。如下圖所示:

 

每個 IO 模型都有自己的使用模式,它們對於特定的應用程序都有自己的優點。

同步和異步主要針對C(client)端
同步:
所謂的同步,就是在c端發出一個功能調用時,在沒有得到結果之前,該調用步返回,也就是說必須一件一件事做,等前一件事完了之后才做后一件事。
如:普通的B/S模式(同步):提交請求->等待服務器處理->處理完畢返回,這期間客戶端瀏覽器不能干任何事

異步:
與同步相對。當C端一個異步過程調用發出之后,調用者不能立即得到結果,實際處理這個調用的部件在完成后,通過狀態,通知和回調來通知調用者。
如:請求通過事件觸發->服務器處理(瀏覽器仍然可以做其他事情)->處理完畢

阻塞和非阻塞主要針對S端(server)

阻塞:
阻塞調用是指調用結果返回之前,當前線程會被掛起(線程進入非可執行狀態,在這個狀態,cpu不會分配時間片,線程暫停運行)函數只有得到結果返回。
阻塞調用和同步調用的區別:對同步來說,很多時候當前線程還是激活的,只是邏輯上沒有返回,如,在socket編程中調用recv函數,如果緩沖區沒有數據,這個函數就會一直等待,直到有數據返回。而此前當前線程還有可能繼續處理各種各樣的消息。

阻塞的例子:比如去取A樓一層(假設是內核緩沖區)取快遞,但是比不知道什么時候來,你有不能干別的事情,只能死等着但是可以睡覺(進程處於休眠狀態),因為你知道快遞把貨送來時一定會給比大電話

非阻塞:
非阻塞與阻塞概念想對應,指在不能立即得到結果之前,該函數不會阻塞當前線程,而會立即返回。

非阻塞的例子:還是等快遞,如果用輪詢的方式,每隔5分鍾去A樓一層(內核緩沖區)去看快遞來了沒,沒來,立即返回,如果快遞來了,就放到A樓一層,等你去取。

對象是否處於阻塞模式和函數是不是阻塞調用有很強的相關性,但不是一一對應的。阻塞對象上可以有非阻塞的調用方式,我們可以通過輪詢狀態,在適當的時候調用阻塞函數,就可以避免阻塞,而對於非阻塞對象,調用函數可以進入阻塞調用,對於select:
1:同步 

我客戶端(C端調用者)一個功能,該功能沒有結束前,我死等結果。

2:異步

我(c端調用者)調用一個功能,不知道該功能結果,該功能有結果后通知我,即回調通知
同步和異步主要針對c端,但是跟s端不是完全沒關系,同步和異步必須s端配合才能實現,同步和異步由c端控制,但是s端是否為阻塞還是非阻塞,c端不關心。

3:阻塞

就是調用我(s端被調用者,函數),我(s端被調用者,函數)沒有完全接受完數據或者沒有得到結果之前,我不會返回。

4:非阻塞

就是調用我(s端被調用者,函數),我(s端被調用者,函數)立即返回,通過select通知調用者

同步I/O與異步I/O的區別在與數據訪問的時候進程是否阻塞
阻塞I/O與非阻塞I/O的區別在與:應該程序的調用是否立即返回。

阻塞和非阻塞是指server端的進程訪問的數據如果尚未就緒,進程是否需要等待,簡單說這相當於函數內部的實現區別,也就是未就緒時時直接返回還是等待就緒。

就同步和異步是指client端訪問數據的機制,同步一般指主動請求並等待I/O操作完畢的方式,當數據就緒后再讀寫額時候必須阻塞,異步則指主動請求數據后便可以繼續處理其他任務,隨后等待I/O,操作完畢的通知。

一、阻塞I/O模型:
簡介:進程會一直阻塞,直到數據拷貝完成
應用程序調用一個I/O函數,導致應用程序阻塞,等待數據准備好,如果數據沒有准備好,一直等待。。數據准備好,從內核拷貝到用戶空間,I/O函數返回成功

阻塞I/O模型圖:在調用recv()/recvfrom(),發生在內核中等待數據和復制數據過程。

 

當調用recv()函數時,系統首先檢查是否有准備好的數據,如果數據沒有准備好,那么系統就處於等待狀態,當數據准備好后,將數據從系統緩沖區復制到用戶空間,然后函數返回。在套接應用程序中,當調用recv()函數時,未必用戶空間就已經存在數據,那么此時recv()函數處於等待狀態

二、非阻塞I/O模型:
簡介:我們把一個套接口設置為非阻塞就是告訴內存,當所請求的I/O操作無法完成時,不要驚進程睡眠,而是返回一個錯誤,河陽I/O函數會不斷的測試數據是否准備好,沒有准備好,繼續測試,直到數據准備好為止。在測試的過程中會占用大量的CPU時間。

 

三、I/O復用模型:
簡介:主要是select和epoll;對於一個I/O端口,兩次調用,兩次返回,比阻塞I/O並沒有什么優勢,只是能實現同時對多個I/O端口進行監聽。

I/O復用模型會調用select,poll函數,這幾個函數也會使進程阻塞,但是和阻塞I/O不同的,這個函數可以同時阻塞多個I/O操作,而且可以同時對多個讀操作,多個寫操作的I/O函數進行檢測,直到有數據可讀或可寫時,才真正調用I/O操作函數。

四、信號驅動I/O:
簡介:兩次調用,兩次返回
首先允許套接口進行信號驅動I/O,並安裝一個信號處理函數,進程繼續運行並不阻塞。昂數據准備好時,進程會收到一個SIGIO信號,可以在信號處理函數中調用I/O操作函數處理數據。

五、異步I/O模型:
簡介:數據拷貝的時候進程無需阻塞
當一個異步過程調用發出后,調用者不能立刻得到結果。實際處理這個調用的部件在完成后,通過狀態,通知和回調通知調用者輸入輸出操作。

同步I/O引起進程阻塞,直到I/O操作完成 
異步I/O不會引起進程阻塞 
I/O復用先通過select調用阻塞

 

NGINX:

nginx 支持多種並發模型,並發模型的具體實現根據系統平台而有所不同。
在支持多種並發模型的平台上,nginx 自動選擇最高效的模型。但我們也可以使用 use 指令在配置文件中顯式地定義某個並發模型。

NGINX中支持的並發模型:

select:

IO多路復用、標准並發模型。在編譯 nginx 時,如果所使用的系統平台沒有更高效的並發模型,select 模塊將被自動編譯。configure 腳本的選項:--with-select_module 和 --without-select_module 可被用來強制性地開啟或禁止 select 模塊的編譯

poll:

IO多路復用、標准並發模型。與 select 類似,在編譯 nginx 時,如果所使用的系統平台沒有更高效的並發模型,poll 模塊將被自動編譯。configure 腳本的選項:--with-poll_module 和 --without-poll_module 可用於強制性地開啟或禁止 poll 模塊的編譯

epoll:

IO多路復用、高效並發模型,可在 Linux 2.6+ 及以上內核可以使用

kqueue:

IO多路復用、高效並發模型,可在 FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0, and Mac OS X 平台中使用

/dev/poll:

高效並發模型,可在 Solaris 7 11/99+, HP/UX 11.22+ (eventport), IRIX 6.5.15+, and Tru64 UNIX 5.1A+ 平台使用

eventport:

高效並發模型,可用於 Solaris 10 平台,PS:由於一些已知的問題,建議 使用/dev/poll替代。

 

為什么epoll快?

比較一下Apache常用的select,和Nginx常用的epoll

select:

1、最大並發數限制,因為一個進程所打開的 FD (文件描述符)是有限制的,由 FD_SETSIZE 設置,默認值是 1024/2048 ,因此 Select 模型的最大並發數就被相應限制了。自己改改這個 FD_SETSIZE ?想法雖好,可是先看看下面吧 …
2、效率問題, select 每次調用都會線性掃描全部的 FD 集合,這樣效率就會呈現線性下降,把 FD_SETSIZE 改大的后果就是,大家都慢慢來,什么?都超時了。
3、內核 / 用戶空間 內存拷貝問題,如何讓內核把 FD 消息通知給用戶空間呢?在這個問題上 select 采取了內存拷貝方法,在FD非常多的時候,非常的耗費時間。

總結為:1、連接數受限 2、查找配對速度慢 3、數據由內核拷貝到用戶態消耗時間

epoll:

1、Epoll 沒有最大並發連接的限制,上限是最大可以打開文件的數目,這個數字一般遠大於 2048, 一般來說這個數目和系統內存關系很大 ,具體數目可以 cat /proc/sys/fs/file-max 查看。
2、效率提升, Epoll 最大的優點就在於它只管你“活躍”的連接 ,而跟連接總數無關,因此在實際的網絡環境中, Epoll 的效率就會遠遠高於 select 和 poll 。
3、內存共享, Epoll 在這點上使用了“共享內存 ”,這個內存拷貝也省略了。

 

還不懂?

舉個栗子

假設你在大學中讀書,要等待一個朋友來訪,而這個朋友只知道你在A號樓,但是不知道你具體住在哪里,於是你們約好了在A號樓門口見面.

如果你使用的阻塞IO模型來處理這個問題,那么你就只能一直守候在A號樓門口等待朋友的到來,在這段時間里你不能做別的事情,不難知道,這種方式的效率是低下的.

現在時代變化了,開始使用多路復用IO模型來處理這個問題.你告訴你的朋友來了A號樓找樓管大媽,讓她告訴你該怎么走.這里的樓管大媽扮演的就是多路復用IO的角色.

select版大媽做的是如下的事情:比如同學甲的朋友來了,select版大媽比較笨,她帶着朋友挨個房間進行查詢誰是同學甲,你等的朋友來了,於是在實際的代碼中,select版大媽做的是以下的事情:

int n = select(&readset,NULL,NULL,100);

for (int i = 0; n > 0; ++i)
{
    if (FD_ISSET(fdarray[i], &readset))
    {
        do_something(fdarray[i]);
        --n;
    }
}

epoll版大媽就比較先進了,她記下了同學甲的信息,比如說他的房間號,那么等同學甲的朋友到來時,只需要告訴該朋友同學甲在哪個房間即可,不用自己親自帶着人滿大樓的找人了.於是epoll版大媽做的事情可以用如下的代碼表示:

n=epoll_wait(epfd,events,20,500);

for(i=0;i<n;++i)
{
    do_something(events[n]);
}

在epoll中,關鍵的數據結構epoll_event定義如下:

typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;
struct epoll_event {
    __uint32_t events;     /* Epoll events */
    epoll_data_t data;     /* User data variable */
};

可以看到,epoll_data是一個union結構體,它就是epoll版大媽用於保存同學信息的結構體,它可以保存很多類型的信息:fd,指針,等等.有了這個結構體,epoll大媽可以不用吹灰之力就可以定位到同學甲.別小看了這些效率的提高,在一個大規模並發的服務器中,輪詢IO是最耗時間的操作之一.再回到那個例子中,如果每到來一個朋友樓管大媽都要全樓的查詢同學,那么處理的效率必然就低下了,過不久樓底就有不少的人了.
對比最早給出的阻塞IO的處理模型, 可以看到采用了多路復用IO之后, 程序可以自由的進行自己除了IO操作之外的工作, 只有到IO狀態發生變化的時候由多路復用IO進行通知, 然后再采取相應的操作, 而不用一直阻塞等待IO狀態發生變化了.

從上面的分析也可以看出,epoll比select的提高實際上是一個用空間換時間思想的具體應用.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM