IO多路複用的 select、poll、epoll詳解

前幾篇文章講述了IO的幾種模式及netty的基本概念,netty基於多路複用模型下的reactor模式,對 大量鏈接、單個處理短且快 的場景很適用 。html

那在往底層思考,linux對於IO又是如何處理的呢react

C10K 問題

http://www.52im.net/thread-566-1-1.htmllinux

最初的服務器都是基於進程/線程模型的,新到來一個TCP鏈接,就須要分配1個進程(或者線程)。而進程又是操做系統最昂貴的資源,一臺機器沒法建立不少進程。若是是C10K就要建立1萬個進程,那麼單機而言操做系統是沒法承受的(每每出現效率低下甚至徹底癱瘓)。若是是採用分佈式系統,維持1億用戶在線須要10萬臺服務器,成本巨大。基於上述考慮,如何突破單機性能侷限,是高性能網絡編程所必需要直面的問題。這些侷限和問題最先被Dan Kegel 進行了概括和總結,並首次成系統地分析和提出解決方案,後來這種廣泛的網絡現象和技術侷限都被你們稱爲 C10K 問題。redis

C10K 問題的最大特色是:設計不夠良好的程序,其性能和鏈接數及機器性能的關係每每是非線性的

舉個例子:若是沒有考慮過 C10K 問題,一個經典的基於 select 的程序能在舊服務器上很好處理 1000 併發的吞吐量,它在 2 倍性能新服務器上每每處理不了併發 2000 的吞吐量。這是由於在策略不當時,大量操做的消耗和當前鏈接數 n 成線性相關。會致使單個任務的資源消耗和當前鏈接數的關係會是 O(n)。而服務程序須要同時對數以萬計的socket 進行 I/O 處理,積累下來的資源消耗會至關可觀,這顯然會致使系統吞吐量不能和機器性能匹配。編程

以上這就是典型的C10K問題在技術層面的表現。C10K問題本質上是操做系統的問題。對於Web1.0/2.0時代的操做系統而言, 傳統的同步阻塞I/O模型都是同樣的,處理的方式都是requests per second,併發10K和100的區別關鍵在於CPU。建立的進程線程多了,數據拷貝頻繁(緩存I/O、內核將數據拷貝到用戶進程空間、阻塞), 進程/線程上下文切換消耗大, 致使操做系統崩潰,這就是C10K問題的本質!
可見,解決C10K問題的關鍵就是儘量減小這些CPU等核心計算資源消耗,從而榨乾單臺服務器的性能,突破C10K問題所描述的瓶頸。segmentfault

概念說明

用戶空間與內核空間

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

處在內核空間稱爲內核態,用戶空間稱爲用戶態! 內核的權限遠大於用戶空間權限,硬件、IO等等系統操做只能經過內核調用!緩存

進程切換

爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復之前掛起的某個進程的執行。這種行爲被稱爲進程切換。安全

進程的阻塞

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

文件描述符fd

文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。

文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者建立一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫每每會圍繞着文件描述符展開。可是文件描述符這一律念每每只適用於UNIX、Linux這樣的操做系統

I/O的socket操做也是一種文件描述符fd

緩存 I/O

緩存 I/O 又被稱做標準 I/O,大多數文件系統的默認 I/O 操做都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操做系統會將 I/O 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。

緩存 I/O 的缺點:數據在傳輸過程當中須要在應用程序地址空間和內核進行屢次數據拷貝操做,這些數據拷貝操做所帶來的 CPU 以及內存開銷是很是大的

I/O 多路複用之select、poll、epoll詳解

詳細部分能夠參閱:
http://www.javashuo.com/article/p-sqagmtjn-x.html; 
http://www.javashuo.com/article/p-ocfrejfu-ng.html

select,poll,epoll都是IO多路複用的機制。一個進程能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做

select,poll,epoll本質上都是同步I/O,由於他們都須要在讀寫事件就緒後本身負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需本身負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。

Netty與redis(單線程的下的I/O多路複用) 使用epoll模式

select/poll的幾大缺點

  1. select的本質是採用32個整數的32位,即32*32= 1024來標識,fd值爲1-1024。(句柄上限+重複初始化+逐個排查全部文件句柄狀態效率不高)
    1. 當fd的值超過1024限制時,就必須修改FD_SETSIZE(管理的句柄上限)的大小,這個時候就能夠標識32*max值範圍的fd。
    2. 在使用上,由於只有一個字段記錄關注和發生事件,每次調用以前要從新初始化 fd_set 結構體。
    3. select的觸發方式是水平觸發,應用程序若是沒有完成對一個已經就緒的文件描述符進行IO操做,那麼以後每次select調用仍是會將這些文件描述符通知進程。
  2. poll主要解決 select 的前兩個問題,但仍是得逐個排查全部文件句柄狀態效率不高:
    1. 經過一個pollfd數組向內核傳遞須要關注的事件,故沒有描述符個數的限制。
    2. pollfd中的events字段和revents分別用於標示關注的事件和發生的事件,故pollfd數組只須要被初始化一次。
  3. select/poll 將這個fd列表維持在用戶態, 每次調用時都須要把fd集合從用戶態拷貝到內核態, 並在內核中遍歷傳遞進來的全部fd; 返回的的是含有整個句柄的數組,應用程序須要遍歷整個數組才能發現哪些句柄發生了事件

epoll

epoll是poll的一種優化,在內核中維持了fd的列表,只遍歷發生事件的fd集合。

與poll/select不一樣,epoll再也不是一個單獨的系統調用,而是由epoll_create/epoll_ctl/epoll_wait三個系統調用組成,epoll在2.6之後的內核才支持。

綜合的來講:

epoll在內核中申請一個簡易的文件系統,把原先的select/poll調用分紅了3個部分。鏈接的套接字(socket句柄)是採用紅黑樹的結構存儲在內核cache中的,並給內核中斷處理程序註冊一個回調函數,告訴內核:若是這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。

當有事件準備就緒時,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裏了(epoll的基礎是回調)。當epoll_wait調用時,僅僅觀察這個list鏈表裏有沒有數據便可;有數據就返回,沒有數據就sleep,等到timeout時間到後即便鏈表沒數據也返回。

1)調用epoll_create創建一個epoll對象(在epoll文件系統中爲這個句柄對象分配資源, 建立了紅黑樹和就緒鏈表)
2)調用epoll_ctl向epoll對象中添加這100萬個鏈接的套接字 (若是增長socket句柄,則檢查在紅黑樹中是否存在,存在當即返回,不存在則添加到樹幹上,而後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據)
3)調用epoll_wait收集發生的事件的鏈接 (馬上返回準備就緒鏈表裏的數據)

兩種模式LT和ET

ET是邊緣觸發,LT是水平觸發,一個表示只有在變化的邊際觸發,一個表示在某個階段都會觸發。

當一個socket句柄上有事件時,內核會把該句柄插入上面所說的準備就緒list鏈表,這時咱們調用epoll_wait,會把準備就緒的socket拷貝到用戶態內存,而後清空準備就緒list鏈表,最後,epoll_wait幹了件事,就是檢查這些socket,若是不是ET模式(就是LT模式的句柄了),而且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了。因此,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回這個句柄。(從上面這段,能夠看出,LT還有個回放的過程,低效了)

場景假設

eg.有100萬個客戶端同時與一個服務器進程保持着TCP鏈接。而每一時刻,一般只有幾百上千個TCP鏈接是活躍的(事實上大部分場景都是這種狀況)。如何實現這樣的高併發?

在select/poll時代,服務器進程每次都把這100萬個鏈接告訴操做系統(從用戶態複製句柄數據結構到內核態),讓操做系統內核去查詢這些套接字上是否有事件發生,輪詢完後,再將句柄數據複製到用戶態,讓服務器應用程序輪詢處理已發生的網絡事件,這一過程資源消耗較大,所以,select/poll通常只能處理幾千的併發鏈接。若是沒有I/O事件產生,咱們的程序就會阻塞在select處。可是依然有個問題,咱們從select那裏僅僅知道了,有I/O事件發生了,但卻並不知道是那幾個流(可能有一個,多個,甚至所有),咱們只能無差異輪詢全部流,找出能讀出數據,或者寫入數據的流,對他們進行操做。處理的流越多,每一次無差異輪詢時間就越長!

相關文章
相關標籤/搜索