Unix 網絡 IO 模型及 Linux 的 IO 多路複用模型

本文主要探討的問題有如下兩個:linux

  1. Unix 中的五種網絡 IO 模型;
  2. Linux 中 IO 多路複用的實現。

基本概念

在介紹網絡模型以前,先簡單介紹一些基本概念。編程

文件描述符 fd

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

在 Linux 中,內核將全部的外部設備都當作一個文件來進行操做,而對一個文件的讀寫操做會調用內核提供的系統命令,返回一個 fd,對一個 socket 的讀寫也會有相應的描述符,稱爲 socketfd(socket 描述符),實際上描述符就是一個數字,它指向內核中的一個結構體(文件路徑、數據區等一些屬性)。
 安全

用戶空間與內核空間、內核態與用戶態

如今操做系統都是採用虛擬存儲器,那麼對32位操做系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。操心繫統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也有訪問底層硬件設備的全部權限。爲了保證用戶進程不能直接操做內核,保證內核的安全,操心繫統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。針對 linux 操做系統而言(以32位操做系統爲例)網絡

  • 將最高的 1G 字節(從虛擬地址 0xC0000000 到 0xFFFFFFFF),供內核使用,稱爲內核空間;
  • 將較低的 3G 字節(從虛擬地址 0x00000000 到 0xBFFFFFFF),供各個進程使用,稱爲用戶空間。

每一個進程能夠經過系統調用進入內核,所以,Linux 內核由系統內的全部進程共享。因而,從具體進程的角度來看,每一個進程能夠擁有 4G 字節的虛擬空間。數據結構

  • 當一個任務(進程)執行系統調用而陷入內核代碼中執行時,稱進程處於內核運行態(內核態)。此時處理器處於特權級最高的(0級)內核代碼中執行。當進程處於內核態時,執行的內核代碼會使用當前進程的內核棧,每一個進程都有本身的內核棧;
  • 當進程在執行用戶本身的代碼時,則稱其處於用戶運行態(用戶態)。此時處理器在特權級最低的(3級)用戶代碼中運行。當正在執行用戶程序而忽然被中斷程序中斷時,此時用戶程序也能夠象徵性地稱爲處於進程的內核態。由於中斷處理程序將使用當前進程的內核棧。

上下文切換

當一個進程在執行時,CPU 的全部寄存器中的值、進程的狀態以及堆棧中的內容被稱爲該進程的上下文。多線程

當內核須要切換到另外一個進程時,它須要保存當前進程的全部狀態,即保存當前進程的上下文,以便在再次執行該進程時,可以必獲得切換時的狀態執行下去。在 Linux 中,當前進程上下文均保存在進程的任務數據結構中。在發生中斷時,內核就在被中斷進程的上下文中,在內核態下執行中斷服務例程。但同時會保留全部須要用到的資源,以便中繼服務結束時能恢復被中斷進程的執行。併發

 

UNIX 的網絡 IO 模型

根據 UNIX 網絡編程對 IO 模型的分類,UNIX 提供瞭如下 5 種 IO 模型。異步

阻塞 IO 模型

最經常使用的 IO 模型就是阻塞 IO 模型,在缺省條件下,全部文件操做都是阻塞的,以 socket 讀爲例來介紹一下此模型,以下圖所示。socket

在用戶空間調用 recvfrom,系統調用直到數據包達到且被複制到應用進程的緩衝區中或中間發生異常返回,在這個期間進程會一直等待。進程從調用 recvfrom 開始到它返回的整段時間內都是被阻塞的,所以,被稱爲阻塞 IO 模型。

 

非阻塞 IO 模型

recvfrom 從應用到內核的時,若是該緩衝區沒有數據,就會直接返回 EWOULDBLOCK 錯誤,通常都對非阻塞 IO 模型進行輪詢檢查這個狀態,看看內核是否是有數據到來,流程以下圖所示。

也就是說非阻塞的 recvform 系統調用調用以後,進程並無被阻塞,內核立刻返回給進程。

  • 若是數據還沒準備好,此時會返回一個 error。進程在返回以後,能夠乾點別的事情,而後再發起 recvform 系統調用。重複上面的過程,循環往復的進行 recvform 系統調用,這個過程一般被稱之爲輪詢

輪詢檢查內核數據,直到數據準備好,再拷貝數據到進程,進行數據處理。須要注意,拷貝數據整個過程,進程仍然是屬於阻塞的狀態。

在 Linux 下,能夠經過設置 socket 使其變爲 non-blocking。

 

IO 多路複用模型

Linux 提供 select、poll、epoll,進程經過講一個或者多個 fd 傳遞給 select、poll、epoll 系統調用,阻塞在 select 操做(這個是內核級別的調用)上,這樣的話,能夠同時監聽多個 fd 是否處於就緒狀態。其中,

  • select/poll 是順序掃描 fd 是否就緒,並且支持的 fd 數量有限;
  • epoll 是基於事件驅動方式代替順序掃描性能更高。

這個後面詳細講述,具體流程以下圖所示。

多路複用的特色是經過一種機制一個進程能同時等待 IO 文件描述符,內核監視這些文件描述符(套接字描述符),其中的任意一個進入讀就緒狀態,select, poll,epoll 函數就能夠返回,它最大的優點就是能夠同時處理多個鏈接。

 

信號驅動 IO 模型

首先須要開啓 socket 信號驅動 IO 功能,並經過系統調用 sigaction 執行一個信號處理函數(非阻塞,當即返回)。當數據就緒時,會爲該進程生成一個 SIGIO 信號,經過信號回調通知應用程序調用 recvfrom 來讀取數據,並通知主循環喊出處理數據,流程以下圖所示。

 

異步 IO 模型

告知內核啓動某個事件,並讓內核在整個操做完成後(包括將數據從內核複製到用戶本身的緩衝區)經過咱們,流程以下圖所示。

與信號驅動模式的主要區別是:

  • 信號驅動 IO 由內核通知咱們什麼時候能夠開始一個 IO 操做;
  • 異步 IO 操做由內核通知咱們 IO 什麼時候完成。

內核是經過嚮應用程序發送 signal 或執行一個基於線程的回調函數來完成此次 IO 處理過程,告訴用戶 read 操做已經完成,在 Linux 中,通知的方式是信號:

  1. 當進程正處於用戶態時,應用須要立馬進行處理,通常狀況下,是先將事件登記一下,放進一個隊列中;
  2. 當進程正處於內核態時,好比正在以同步阻塞模式讀磁盤,那麼只能先把這個通知掛起來,等內核態的事情完成以後,再觸發信號通知;
  3. 若是這個進程如今被掛起來了,好比 sleep,那就把這個進程喚醒,等 CPU 空閒時,就會調度這個進程,觸發信號通知。

 

幾種 IO 模型比較


 

Linux 的 IO 多路複用模型

IO 多路複用經過把多個 IO 阻塞複用到同一個 select 的阻塞上,從而使得系統在單線程的狀況下,能夠同時處理多個 client 請求,與傳統的多線程/多進程模型相比,IO 多路複用的最大優點是系統開銷小,系統不須要建立新的額外的進程或線程,也不須要維護這些進程和線程的運行,節省了系統資源,IO 多路複用的主要場景以下:

  1. Server 須要同時處理多個處於監聽狀態或者鏈接狀態的 socket;
  2. Server 須要同時處理多種網絡協議的 socket。

IO 多路複用實際上就是經過一種機制,一個進程能夠監視多個描 fd,一旦某個 fd 就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做,目前支持 IO 多路複用的系統有 select、pselect、poll、epoll,但它們本質上都是同步 IO。

在 Linux 網絡編程中,最初是選用 select 作輪詢和網絡事件通知,然而 select 的一些固有缺陷致使了它的應用受到了很大的限制,最終 Linux 選擇 epoll。

select

select 函數監視的 fd 分3類,分別是 writefdsreadfds、和 exceptfds。調用後select 函數會阻塞,直到有 fd 就緒(有數據 可讀、可寫、或者有 except),或者超時(timeout 指定等待時間,若是當即返回設爲 null 便可),函數返回。當select函數返回後,能夠經過遍歷 fdset,來找到就緒的 fd。

select 目前幾乎在全部的平臺上支持,其良好跨平臺支持也是它的一個優勢。select 的一個最大的缺陷就是單個進程對打開的 fd 是有必定限制的,它由 FD_SETSIZE 限制,默認值是1024,若是修改的話,就須要從新編譯內核,不過這會帶來網絡效率的降低。

select 和 poll 另外一個缺陷就是隨着 fd 數目的增長,可能只有不多一部分 socket 是活躍的,可是 select/poll 每次調用時都會線性掃描所有的集合,致使效率呈現線性的降低。

poll

poll 本質上和 select 沒有區別,它將用戶傳入的數組拷貝到內核空間,而後查詢每一個 fd 對應的設備狀態,若是設備就緒則在設備等待隊列中加入一項並繼續遍歷,若是遍歷完全部 fd 後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷 fd。這個過程經歷了屢次無謂的遍歷。

它沒有最大鏈接數的限制,緣由是它是基於鏈表來存儲的,可是一樣如下兩個缺點:

  1. 大量的 fd 的數組被總體複製於用戶態和內核地址空間之間;
  2. poll 還有一個特色是【水平觸發】,若是報告了 fd 後,沒有被處理,那麼下次 poll 時會再次報告該 fd;
  3. fd 增長時,線性掃描致使性能降低。

epoll

epoll 支持水平觸發和邊緣觸發,最大的特色在於邊緣觸發,它只告訴進程哪些 fd 變爲就緒態,而且只會通知一次。還有一個特色是,epoll 使用【事件】的就緒通知方式,經過 epoll_ctl 註冊 fd,一旦該 fd 就緒,內核就會採用相似 callback 的回調機制來激活該 fd,epoll_wait 即可以收到通知。

epoll的優勢:

  1. 沒有最大併發鏈接的限制,它支持的 fd 上限受操做系統最大文件句柄數;
  2. 效率提高,不是輪詢的方式,不會隨着 fd 數目的增長效率降低。epoll 只會對【活躍】的 socket 進行操做,這是由於在內核實現中 epoll 是根據每一個 fd 上面的 callback 函數實現的,只有【活躍】的 socket 纔會主動的去調用 callback 函數,其餘 idle 狀態的 socket 則不會。epoll 的性能不會受 fd 總數的限制。
  3. select/poll 都須要內核把 fd 消息通知給用戶空間,而 epoll 是經過內核和用戶空間 mmap 同一塊內存實現。

epoll 對 fd 的操做有兩種模式:LT(level trigger)和ET(edge trigger)。LT 模式是默認模式,LT 模式與 ET 模式的區別以下:

  • LT 模式:當 epoll_wait 檢測到描述符事件發生並將此事件通知應用程序,應用程序能夠不當即處理該事件,下次調用 epoll_wait 時,會再次響應應用程序並通知此事件;
  • ET 模式:當 epoll_wait 檢測到描述符事件發生並將此事件通知應用程序,應用程序必須當即處理該事件,若是不處理,下次調用 epoll_wait 時,不會再次響應應用程序並通知此事件。

三種模型的區別

類別 select poll epoll
支持的最大鏈接數 由 FD_SETSIZE 限制 基於鏈表存儲,沒有限制 受系統最大句柄數限制
fd 劇增的影響 線性掃描 fd 致使性能很低 同 select 基於 fd 上 callback 實現,沒有性能降低的問題
消息傳遞機制 內核須要將消息傳遞到用戶空間,須要內核拷貝 同 select epoll 經過內核與用戶空間共享內存來實現

介紹完 IO 多路複用以後,後續咱們看一下 Java 網絡編程中的 NIO 模型及其背後的實現機制。

==========END==========

相關文章
相關標籤/搜索