注:本文是對網上資料和書本的總結,有錯誤的地方請指出,謝謝。
假如有一天,你想訪問你讀你/tmp目錄下的hello文件。會經歷什麼?
首先你向操作系統發起你想讀的請求,然後操作系統就會將數據返回給你,你就會可以看見hello文件的內容。真的這麼簡單嗎?其實不是的。
我們先需要了解用戶空間和內核空間:
現在操作系統都是採用虛擬存儲器,那麼對32位操作系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。操作系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。爲了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操心繫統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。針對linux操作系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲用戶空間。
(by the way:一個進程的堆的大小也是虛擬內存的大小-1G的內核空間大小,基本就是約等於3G(4G-1G),而通常棧是由編譯器 決定的,基本是1M)
緩存 I/O 又被稱作標準 I/O,大多數文件系統的默認 I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操作系統會將 I/O 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操作系統內核的緩衝區中,然後纔會從操作系統內核的緩衝區拷貝到應用程序的地址空間。
緩存 I/O 的缺點:
數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。
1. 等待數據準備 (Waiting for the data to be ready)
2. 將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)
過程如下:
(這個時候你可能會說爲什麼要這麼麻煩,不直接從讓應用程序訪問磁盤。當然這個是可以的,這種稱爲直接I/O。但是大多數文件的默認操作都是緩存I/O。這個不是本文的重點,大家可以查資料瞭解)
Linux產生了5種I/O模型:
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路複用( IO multiplexing)
- 信號驅動 I/O( signal driven IO)
- 異步 I/O(asynchronous IO)
阻塞/O模型:
常用的I/O操作比如read和write,以及套接字上的accept、recvfrom都是阻塞I/O。在linux中,默認情況下所有的socket都是blocking,以數據報套接字的典型例子:
也就是當用recvfrom時,
kernel就開始了IO的第一個階段:準備數據(對於網絡IO來說,很多時候數據在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的數據到來)。這個過程需要等待,也就是說數據被拷貝到操作系統內核的緩衝區中是需要一個過程的。而在用戶進程這邊,整個進程會被阻塞(當然,是進程自己選擇的阻塞)。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,然後kernel返回結果,用戶進程才解除block的狀態,重新運行起來。
所以,blocking IO的特點就是在IO執行的兩個階段都被block了。
這樣,當服務器需要處理1000個連接的的時候,而且只有很少連接忙碌的,那麼會需要1000個線程或進程來處理1000個連接,而1000個線程大部分是被阻塞起來的。由於CPU的核數或超線程數一般都不大,比如4,8,16,32,64,128,比如4個核要跑1000個線程,那麼每個線程的時間槽非常短,而線程切換非常頻繁。這樣是有問題的:線程是有內存開銷的,1個線程可能需要512K(或2M)存放棧,那麼1000個線程就要512M(或2G)內存。線程的切換,或者說上下文切換是有CPU開銷的,當大量時間花在上下文切換的時候,分配給真正的操作的CPU就要少很多。
非阻塞/O模型:
那麼,我們就要引入非阻塞I/O的概念,非阻塞IO很簡單,通過fcntl(POSIX)或ioctl(Unix)設爲非阻塞模式,這時,當你調用read時,如果有數據收到,就返回數據,如果沒有數據收到,就立刻返回一個錯誤,如EWOULDBLOCK。這樣是不會阻塞線程了,但是你還是要不斷的輪詢來讀取或寫入。
(阻塞時是不佔用CPU的,運行態的線程才佔用CPU,這樣進程或者線程的切換開銷比較大。非阻塞就是爲了減少大量的上下文切換)
於是,我們需要引入IO多路複用的概念。多路複用是指使用一個線程來檢查多個文件描述符(Socket)的就緒狀態,比如調用select和poll函數,傳入多個文件描述符,如果有一個文件描述符就緒,則返回,否則阻塞直到超時。得到就緒狀態後進行真正的操作可以在同一個線程裏執行,也可以啓動線程執行(比如使用線程池)。
I/O複用模型:
這樣在處理1000個連接時,只需要1個線程監控就緒狀態,對就緒的每個連接開一個線程處理就可以了,這樣需要的線程數大大減少,減少了內存開銷和上下文切換的CPU開銷。
使用select函數的方式如下圖所示:select比fork高效的地方:
select是內核會用更高效的方式去做,而用戶空間的代碼每一次系統調用都要包含一次用戶空間到內核空間的轉換,以及內核再轉換回來,這樣就很浪費機器週期。而且內核中的poll接口實現會根據操作文件類型的不同有不一樣的選擇,竭盡全力去節省時間。
select/epoll的作用是,(相比傳統的fork/thread模式)讓你的系統資源更專注地用在I/O和數據處理上,而不是用於 thread context switch上。
epoll爲什麼這麼快
以一個生活中的例子來解釋.
假設你在大學中讀書,要等待一個朋友來訪,而這個朋友只知道你在A號樓,但是不知道你具體住在哪裏,於是你們約好了在A號樓門口見面.
如果你使用的阻塞IO模型來處理這個問題,那麼你就只能一直守候在A號樓門口等待朋友的到來,在這段時間裏你不能做別的事情,不難知道,這種方式的效率是低下的.
進一步解釋select和epoll模型的差異.
select版大媽做的是如下的事情:比如同學甲的朋友來了,select版大媽比較笨,她帶着朋友挨個房間進行查詢誰是同學甲,你等的朋友來了。
epoll版大媽就比較先進了,她記下了同學甲的信息,比如說他的房間號,那麼等同學甲的朋友到來時,只需要告訴該朋友同學甲在哪個房間即可,不用自己親自帶着人滿大樓的找人了.
別小看了這些效率的提高,在一個大規模併發的服務器中,輪詢IO是最耗時間的操作之一.再回到那個例子中,如果每到來一個朋友樓管大媽都要全樓的查詢同學,那麼處理的效率必然就低下了,過不久樓底就有不少的人了.
對比最早給出的阻塞IO的處理模型, 可以看到採用了多路複用IO之後, 程序可以自由的進行自己除了IO操作之外的工作, 只有到IO狀態發生變化的時候由多路複用IO進行通知, 然後再採取相應的操作, 而不用一直阻塞等待IO狀態發生變化了.
從上面的分析也可以看出,epoll比select的提高實際上是一個用空間換時間思想的具體應用.
參考文章: http://www.cppblog.com/converse/archive/2008/10/12/63836.aspx
https://www.zhihu.com/question/19732473
http://blog.csdn.net/tennysonsky/article/details/45745887
https://segmentfault.com/a/1190000003063859
《unix網絡編程一》