Linux 网络 IO 模型
寫在前面
本文主要介紹 Unix/Linux 下五種網絡 IO 模型,但是。為了更好的理解下面提到的五種網絡 IO 的概念,我們有必要先理清下面這幾個概念。
用戶空間與內核空間
一個計算機通常有一定大小的內存空間,如一臺計算機有 4GB 的地址空間,但是程序并不能完全使用這些地址空間,因為這些地址空間是被劃分為 用戶空間和內核空間 的。用戶應用程序只能使用用戶空間的內存,這里所說的使用是指應用程序能夠申請的內存空間,并不是真正訪問的地址空間。下面看下什么是用戶空間和內核空間:
用戶空間
用戶空間是常規進程所在的區域,什么是常規進程,打開任務管理器看到的就是常規進程:
JVM 就是常規進程,駐守于用戶空間,用戶空間是非特權區域,比如在該區域執行的代碼不能直接訪問硬件設備。
內核空間
內核空間主要是指操作系統運行時所使用的用于程序調度、虛擬內存的使用或者連接硬件資源等的程序邏輯。內核代碼有特別的權利,比如它能與設備控制器通訊,控制著整個用于區域進程的運行狀態。和 I/O 相關的一點是:所有 I/O 都直接或間接通過內核空間。
那么,為什么要劃分用戶空間和內核空間呢?這也是為了保證操作系統的穩定性和安全性。用戶程序不可以直接訪問硬件資源,如果用戶程序需要訪問硬件資源,必須調用操作系統提供的接口,這個調用接口的過程也就是系統調用。每一次系統調用都會存在兩個內存空間之間的數據交互,通常的網絡傳輸也是一次系統調用,通過網絡傳輸的數據先是從內核空間接收到遠程機器的數據,然后再從內核空間復制到用戶空間,供用戶程序使用。
下面通過一張圖更形象的描述這一過程:
小貼士:這種內核空間與用戶空間的數據的復制很費時,雖然保住了程序運行的安全性和穩定性,但是犧牲了一部分的效率。但是,目前的操作系統已經針對這一塊進行了不錯的優化,這里不是我們討論的重點。
小貼士:如何分配用戶空間和內核空間的比例也是一個問題,是更多地分配給用戶空間供用戶程序使用,還是首先保住內核有足夠的空間來運行,還是要平衡一下。在當前的 Windows 32 位操作系統中,默認用戶空間:內核空間的比例是 1:1,而在 32 位 Linux 系統中的默認比例是 3:1(3GB 用戶空間、1GB 內核空間)。
同步和異步
同步和異步是一種思想,涉獵到的領域也比較多,在 I/O 領域(同步 IO,異步 IO),請求調用領域(同步請求,異步請求,同步調用,異步調用)。雖然,涉及多種領域,但是思想是一樣的。同步和異步,真正的關注點是 消息通信機制 。
同步
以 “調用” 為例,所謂同步,就是 在發出一個 “調用請求” 時,在沒有得到結果之前,該 “調用請求” 就不返回,但是一旦調用返回就得到返回值了。換句話說,就是由 "調用者" 主動等待 “被調用者” 的結果。像我們平時寫的,方法 A 調用 Math.random() 方法、方法 B 調用 String.substring() 方法都是同步調用,因為調用者主動在等待這些方法的返回。
異步
所謂異步,則正好相反,當一個異步調用請求發出之后,調用者不會立刻得到這個請求真正的執行完后得出的結果,立即返回的可能只是一個偽結果 。因此異步調用適用于那些對數據一致性要求不是很高的場景,或者是執行過程很耗時的場景。如果這種場景下,我們希望獲取異步調用的結果,"被調用者"可以通過狀態、通知來通知調用者,或通過回調函數處理這個調用,對應 Java 中的有 Future/FutureTask、wait/notify 體現了這一思想。
阻塞和非阻塞
阻塞和非阻塞其實是針對進程或者是線程的狀態來判定的。比如下面的,用戶進程從操作系統的內核緩沖區讀取數據的時候,如果此時內核緩沖區中的數據還沒準備好的話,操作系統可采用的一種方式就是將用戶進程阻塞在那兒,那么此時該用戶進程的狀態就會從運行狀態變為阻塞狀態,也就是阻塞了。
了解了上面的基礎知識之后,接下來我們就正式進入 Linux 的網絡 IO 模型。
Linux 網絡 IO 模型
理解這五種網絡 I/O 模型之前,我們還得得先清楚一個網絡 IO 事件發生,會涉及到哪些對象,會經歷哪些步驟:
網絡 IO 涉及到的對象
對于一個網絡 IO (這里我們以 read 舉例),它會涉及到兩個系統對象,一個是調用這個 IO 的進程或者是線程,另一個就是 Linux 系統內核空間和用戶空間。
進程執行 I/O 操作的步驟
進程執行 I/O 操作,歸結起來,就是向操作系統發出請求,讓它要么把緩沖區里的數據排干凈(寫),要么用數據把緩沖區填滿(讀)。進程利用這一機制處理所有數據進出操作,操作系統內部處理這一任務的機制,其復雜程度可能超乎想像,但就概念而言,卻非常直白易懂,對于一個網絡 IO,這里我們以 read 為例,當一個 read 操作發生時,會經歷兩個階段:
內核緩沖區準備數據
內核緩沖區數據拷貝到用戶緩沖區
幾種 IO 模型的區別就體現在這兩階段,下面對這幾種 IO 模型進行詳細介紹。
阻塞 IO
當用戶進程開始調用了 recvfrom 這個函數后,就開始了 IO 的 第一階段:內核緩沖區準備數據。對于網絡 IO 來說,數據只有在積累到一定的量的時候才會發送,這個時候內核緩沖區就要等待足夠的數據到來。而在用戶緩沖區這邊,用戶進程會一直被操作系統阻塞,當內核緩沖區數據準備好了,此時就會將內核緩沖區中的數據拷貝到用戶緩沖區,然后 由操作系統喚醒被阻塞的用戶進程 并將結果返回給用戶進程,此時用戶進程才重新運行起來。所以,阻塞 IO 的特點就是在 IO 執行的兩個階段都被阻塞了。
非阻塞 IO
從圖中可以看出,當用戶進程發出 read 操作時,如果內核緩沖區中的數據還沒有準備好,那么它并不會阻塞用戶進程,而是立刻返回一個 error。從用戶進程角度講 ,它發起一個 read 操作后,并沒有被阻塞,而是馬上就得到了一個結果。用戶進程判斷結果是一個 error 時,它就知道數據還沒有準備好,于是它可以再次發送 read 操作,就這樣一直進行下去,到這里第一階段都是一直在輪訓。一旦內核緩沖區中的數據準備好了,并且又再次收到了用戶進程的 read 請求,那么它馬上就將數據從內核緩沖區拷貝到用戶緩沖區,然后返回給用戶線程,這是第二階段。所以,用戶進程在第一階段其實并沒有被操作系統一直阻塞,而是需要不斷的主動詢問內核緩沖區數據好了沒有。只有在第二階段數據拷貝到時候會被阻塞 。
IO 多路復用
IO 多路復用實際上就是通過一種機制,一個進程可以監視多個描 fd,一旦某個 fd 就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作,這種機制目前有 select、pselect、poll、epoll,但它們本質上都是同步 IO。
注意,上面的阻塞 IO 和非阻塞 IO 用戶進程都是只是調用 recvfrom 一個函數,而這里用戶進程還會再調用一個 select 函數,當用戶進程調用了 select,那么整個進程會被阻塞,而同時,操作系統會 “監視” 所有 select 負責的 socket 所對應的的內核緩沖區的數據,當任何一個 socket 所對應的內核緩沖區中的數據準備好了,就會返回可讀條件的通知。此時用戶進程再調用 read 操作,將數據從內核緩沖區拷貝到用戶緩沖區。
這個圖和阻塞 IO 的圖其實并沒有太大的區別,事實上,還更差一些。因為這里需要使用兩個 system call (select 和 recvfrom),而阻塞 IO 只調用了一個system call (recvfrom)。但是,調用 select 的優勢在于它可以同時處理多個 socket。(所以,如果處理的連接數不是很高的話,使用 select 的 web server 不一定比使用 多線程 + 阻塞 IO 的 web server 性能更好,可能延遲還更大。
小貼士:強調一下,select 的優勢并不是對于單個連接能處理得更快,而是在于能處理更多的連接。
在 IO 多路復用模型中,實際上,對于每一個 socket,一般都設置成為非阻塞的,但是,如上圖所示,整個用戶進程實際上是一直被阻塞的。只不過用戶進程是被 select 這個函數阻塞的 ,而不是被 socket IO 給阻塞的(或者也可以理解為是操作系統阻塞的)。
這里肯定有人要問那 select 的作用不就是阻塞多個用戶進程,然后將這些用戶進程與服務器建立的 socket 監視起來,看看哪個 socket 對應的內核緩沖區中的數據準備好了,然后再通知用戶進程,讓用戶進程再發一次 recvfrom 請求來進行數據拷貝。那 epoll 的作用也是這個呀,為啥人家就說 epoll 的效率更高呢?下面,我就來詳細的介紹為啥 epoll 效率更高。要想知道這個我們就要先了解一下 Linux 的 select,poll,epoll 函數。
我們先來看一下 Linux 的 select,poll,epoll 具體作用是什么,有什么區別?
select
select 函數監視的 fd(磁盤描述符,注意:Linux 下系統各組件都是以磁盤描述符的形式存在的,例如 socket) 分 3 類,分別是 writefds,readfds,和 exceptfds。調用 select 函數后會阻塞,直到有 fd 就緒(有數據可讀、可寫、或者有 except),或者超時(timeout 指定等待時間,如果立即返回則設為 null 即可),它就會將它剛剛監控的所有的 fd 對應的標識符的集合 fd_set (注意,這里將內核緩沖區中數據已經就緒的 fd 的標識會打上一個標記)返回給用戶進程,然后用戶進程再去遍歷 fd_set 找出其中內核緩沖區中數據已經就緒的 fd 的標識符,然后再去發送 recvfrom 請求,開始第二階段。
select 優點:
select 缺點:
select 的很大的缺陷就是單個進程能監控的 fd 的數量是有一定限制的,它由 FD_SETSIZE 限制,默認值是 1024,如果修改的話,就需要重新編譯內核,不過這會帶來網絡效率的下降 。
select 模型下內核地址空間和用戶地址空間每次數據復制都是復制所有的 fd; 隨著 fd 數目的增加,可能只有很少一部分 fd 是活躍的,但是 select 每次調用時都會遍歷整個 fd_set,檢查每個 fd 的數據就緒的狀態,這就導致效率很低。
poll
poll 本質上和 select 沒有區別,它也是將整個 fd_set 告訴給用戶進程。和 select 不同的是它沒有最大連接數的限制,原因是它是基于鏈表來存儲的。
poll 缺點:
模型下內核地址空間和用戶地址空間每次數據復制都是復制所有的 fd。
poll 還有一個特點是:水平觸發,如果報告了 fd 處于就緒狀態后,沒有被處理,那么下次 poll 時會再次報告該 fd;fd 增加時,線性掃描導致性能下降。
epoll
epoll 支持水平觸發和邊緣觸發,最大的特點在于邊緣觸發,它只告訴進程哪些 fd 變為就緒態,并且只會通知一次。還有一個特點是,epoll 使用 事件 的就緒通知方式,通過 epoll_ctl 注冊 fd,一旦該 fd 就緒,內核就會采用類似 callback 的回調機制來激活該 fd,epoll_wait 就可以收到通知。
epoll的優點:
沒有最大并發連接的限制,它支持的 fd 上限受操作系統最大文件句柄數;
效率提升,不同于 select 和 poll,epoll 只會對 活躍(數據處于就緒狀態) 的 fd 進行操作,這是因為在內核實現中 epoll 是根據每個 fd 上面的 callback 函數實現的,只有活躍的 fd 才會主動的去調用 callback 函數,其他 idle 狀態的 fd 則不會。epoll 的性能不會受 fd 總數的限制。
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 時,不會再次響應應用程序并通知此事件。
這里用一張表格展示一下幾個函數的區別:
| 支持的最大連接數 | 由 FD_SETSIZE 限制 | 基于鏈表存儲,沒有限制 | 受系統最大句柄數限制 |
| fd 劇增的影響 | 線性掃描 fd 導致性能很低 | 同 select | 基于 fd 上 callback 實現,沒有性能下降的問題 |
| 消息傳遞機制 | 內核需要將消息傳遞到用戶空間,需要內核拷貝 | 同 select | epoll 通過內核與用戶空間共享內存來實現 |
到這里,我們大概就知道了為什么 epoll 比 select 和 poll 效率更高了。
信號驅動 IO
進程和內核的 fd 建立一個 sigio 的處理程序,然后自己做其他事情,并不會阻塞,當內核數據準備好的時候會觸發 Sigaction 系統調用告訴用戶進程數據準備好了,此時,用戶進程發出 recvfrom 進行第二階段。
異步 IO
用戶進程發出異步 IO后,IO 操作立即返回,用戶進程這時就可以去做別的事情了,之后的一切工作都又內核來完成。當內核數據準備好的時候,內核自動將數據拷貝到用戶空間 (不阻塞用戶進程,這里是和上面幾個都不同的),拷貝完成后向用戶進程發送信號。
小貼士:Linux 下 的 asynchronous IO 其實用得很少。
最后看一下 Linux 下五種網絡 IO 模型的比較:
寫這篇網絡 IO 模型是為了后面深入研究 NIO 和 Netty 做準備,也希望能夠為大家解決一些疑問。
參考:
《碼農翻身》
https://blog.csdn.net/baidu_39511645/article/details/78283680
https://www.cnblogs.com/wlwl/p/10291397.html
https://juejin.im/entry/585ba7038d6d810065d3d54a
https://www.cnblogs.com/xrq730/p/5074199.html
https://blog.51cto.com/xingej/1971598
https://www.cnblogs.com/javalyy/p/8882066.html
https://www.jianshu.com/p/6f132d27aeaf?utm_campaign
https://blog.csdn.net/u013374645/article/details/82808301
http://baijiahao.baidu.com/s?id=1604983471279587214&wfr=spider&for=pc
轉載于:https://www.cnblogs.com/tkzL/p/11494134.html
總結
以上是生活随笔為你收集整理的Linux 网络 IO 模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如果你的Spring水平就这?求求就不要
- 下一篇: 平均工资达 1.6 万元!2020 年一