蒼穹之邊,浩瀚之摯,眰恦之美; 悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說》 寫在開頭 作為一名Java Developer,我們都清楚地知道,主要從搭載Linux系統上的伺服器程式來說,使用Java編寫的是”單進程-多線程"程式,而用C++語言編寫的,可能是“單進程-多線程”程式,“多 ...
蒼穹之邊,浩瀚之摯,眰恦之美; 悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說》
寫在開頭
作為一名Java Developer,我們都清楚地知道,主要從搭載Linux系統上的伺服器程式來說,使用Java編寫的是”單進程-多線程"程式,而用C++語言編寫的,可能是“單進程-多線程”程式,“多進程-單線程”程式或者是“多進程-多線程”程式。
從一定程度上 來說,主要由於Java程式並不直接運行在Linux系統上,而是運行在JVM(Java 虛擬機)上,而一個JVM實例是一個Linux進程,每一個JVM都是一個獨立的“沙盒”,JVM之間相互獨立,互不通信。
所以,Java程式只能在這一個進程裡面,開發多個線程實現併發,而C++直接運行在Linux系統上,可以直接利用Linux系統提供的強大的進程間通信(Inter-Process Communication,IPC),很容易創建多個進程,並實現進程間通信。
當然,我們可以明確的是,“多進程-多線程”程式是”單進程-多線程"程式和“多進程-單線程”程式的組合體。無論是C++開發者在Linux系統中使用的pthread,還是Java開發者使用的java.util.concurrent(JUC)庫,這些線程機制的都需要一定的線程I/O模型來做理論支撐。
所以,接下來,我們就讓我們一起探討和揭開常見的線程I/O模型的神秘面紗,針對那些盤根錯落的枝末細節,才能讓我們更好地瞭解和正確認識ava領域中的線程機制。
基本概述
I/O模型是指電腦涉及I/O操作時使用到的模型。
一般分析Java領域中的線程I/O模型是何物時,需要先理解一下什麼是I/O模型 ?
I/O模型是為解決各種問題而提出的,與之相關的概念有線程(Thread),阻塞(Blocking),非阻塞(Non-Blocking) ,同步(Synchronous) 和非同步(Asynchronous) 等。
按照一定意義上說,I/O模型可以分為阻塞I/O(Blocking IO,BIO),非阻塞I/O(Non-Blocking IO,NIO)兩大類。
當然,需要註意的是,電腦的I/O還包括各種設備的I/O,比如網路I/O,磁碟I/O,鍵盤I/O和滑鼠I/O等。
一般來說,程式在執行I/O操作時,需要從內核空間複製數據,但是內核空間的數據需要較長時間的的準備,由此可能會導致用戶空間產生阻塞。
應用程式處於用戶空間,一個應用程式對應著一個進程,而進程中包含了緩衝區(Buffer),因此這裡又對應著一個緩衝I/O(Buffered I/O),其中:
- 當需要進行I/O操作時,需要通過內核空間來執行相應的操作,比如,內核空間負責於鍵盤,磁碟,網路等控制器進行通信。
- 當內核空間得到不同設備的控制器發送過來的數據後,會將數據複製到用戶空間提供給用戶程式使用。
由此可見,I/O模型 是人與電腦實現溝通和交流的主要通信模型。
特別註意的是,這裡的尤其指出網路I/O模型。由於網路I/O模型存在諸多概念性的東西,有操作系統層面的,也有應用層架構層面的,在不同的層面表示的意思也千差萬別,需要我們仔細甄別。
在網路I/O模型中,我們會經常聽到阻塞和非阻塞,同步和非同步等相關的概念,而且也會混淆這個概念,其中最常見的三個問題:
- 首先,認為非阻塞I/0(Non-Blocking IO) 和非同步I/O(Asynchronous IO) 是同一個概念
- 其次,認為Linux系統中的select,poll,epoll 等這類I/O多路復用是非同步I/O(Asynchronous IO) 模型
- 最後,存在一種I/O模型叫非同步阻塞I/O(Asynchronous Blocking IO))模型,實際上並沒有這種模型
由此可見,其實造成這三個問題的主要原因就是,我們在討論的時候,有的是站在Linux操作系統層面說的,有的是站在在Java的JDK層面來說的,甚至有的是站在上層框架(中間件 Netty,Tomcat,Nginx,C++中的asio)封裝的模型來說的。
綜上所述,針對於不同的層面,需要我們仔細辨析和甄別,這才能讓我們理解得更加透徹。
一. Linux操作系統中的I/O模型
現在操作系統都是採用虛擬存儲器,那麼對32位操作系統而言,它的定址空間(虛擬存儲空間)為4G(2的32次方)。
操心系統的核心是內核,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體設備的所有許可權。
針對linux操作系統而言,為了保證用戶進程不能直接操作內核,保證內核的安全,操心系統將虛擬空間劃分為兩部分,一部分為內核空間,一部分為用戶空間。其中:
- 內核空間(Kernel Space):將最高的1G位元組(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,是Linux 內核的運行空間。
- 用戶空間(User Space):將較低的3G位元組(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,是用戶程式的運行空間。
每個進程可以通過系統調用進入內核,因此,Linux內核由系統內的所有進程共用。
於是,從具體進程的角度來看,每個進程可以擁有4G位元組的虛擬空間,其中內核空間和用戶空間是隔離的,即使用戶的程式崩潰,內核也不受影響。
但是,在 CPU 的所有指令中,有些指令是非常危險的,如果錯用,將導致系統崩潰,比如清記憶體、設置時鐘等。如果允許所有的程式都可以使用這些指令,那麼系統崩潰的概率將大大增加。
由於CPU 將指令分為特權指令和非特權指令,對於那些危險的指令,只允許操作系統及其相關模塊使用,普通應用程式只能使用那些不會造成災難的指令。比如 Intel 的 CPU 將特權等級分為 4 個級別:Ring0~Ring3。
其實 Linux 系統只使用了 Ring0 和 Ring3 兩個運行級別(Windows 系統也是一樣的)。當進程運行在 Ring3 級別時被稱為運行在用戶態,而運行在 Ring0 級別時被稱為運行在內核態。
由此可見,由於有了用戶空間和內核空間概念,其linux內部結構可以分為三部分,從最底層到最上層依次是:硬體(Hardware Platfrom)–>內核空間(Kernel Space)–>用戶空間(User Space)。
(一). 基本定義
由於,應用程式處於用戶空間,一個應用程式對應著一個進程,當需要進行I/O操作時,需要通過內核空間來執行相應的操作,而當內核空間得到不同設備的控制器發送過來的數據後,會將數據複製到用戶空間提供給用戶程式使用。
其間表示著,會有一個進程切換的動作,主要概念就是:當進程運行在內核空間時就處於內核態,而進程運行在用戶空間時則處於用戶態,其中:
- 在內核態下,進程運行在內核地址空間中,此時 CPU 可以執行任何指令。運行的代碼也不受任何的限制,可以自由地訪問任何有效地址,也可以直接進行埠的訪問。
- 在用戶態下,進程運行在用戶地址空間中,被執行的代碼要受到 CPU 的諸多檢查,它們只能訪問映射其地址空間的頁表項中規定的在用戶態下可訪問頁面的虛擬地址,且只能對任務狀態段(TSS)中 I/O 許可點陣圖(I/O Permission Bitmap)中規定的可訪問埠進行直接訪問。
但是,對於以前的 DOS 操作系統來說,是沒有內核空間、用戶空間以及內核態、用戶態這些概念的。可以認為所有的代碼都是運行在內核態的,因而用戶編寫的應用程式代碼可以很容易的讓操作系統崩潰掉。
而對於 Linux 來說,通過區分內核空間和用戶空間的設計,隔離了操作系統代碼(操作系統的代碼要比應用程式的代碼健壯很多)與應用程式代碼。即便是單個應用程式出現錯誤也不會影響到操作系統的穩定性,這樣其它的程式還可以正常的運行。
所以,區分內核空間和用戶空間本質上是要提高操作系統的穩定性及可用性,而進程切換是為了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復以前掛起的某個進程的執行。
一般情況下,任何進程都是在操作系統內核的支持下運行的,是與內核緊密相關的。
從一個進程的運行轉到另一個進程上運行,這個過程中基本會做如下操作:
- 保存處理器上下文,包括程式計數器和其他寄存器。
- 更新PCB信息
- 把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列
- 選擇另一個進程執行,並更新其PCB
- 更新記憶體管理的數據結構
- 恢復處理器上下文
特別需要註意的是,進程切換勢必要考慮調用者等待被調用者返回調用結果時的狀態和消息通知機制、狀態等問題,這個其實就是對應阻塞與非阻塞,同步與非同步的關心的本質問題:
- 首先,對於阻塞與非阻塞的角度來說,是調用者等待被調用者返回調用結果時的狀態:
- 阻 塞:調用結果返回之前,調用者會被掛起(不可中斷睡眠態),調用者只有在得到返回結果之後才能繼續;
- 非阻塞:調用者在結果返回之前,不會被掛起;即調用不會阻塞調用者,調用者可以繼續處理其他的工作;
- 其次,對於同步與非同步的角度來說,關註的是消息通知機制、狀態:
- 同 步:調用發出之後不會立即返回,但一旦返回則是最終結果;
- 異 步:調用發出之後,被調用方立即返回消息,但返回的並非最終結果;被調用者通過狀態、通知機制等來通知調用者,會通過回調函數處理;
綜上所述,這便為我們理解和掌握Linux系統中I/O 模型奠定了基礎。接下來,我們主要來看看Linux系統中的網路I/O 模型和文件操作 I/O 模型。
(二). 網路I/O 模型
Linux 的內核將所有外部設備都看做一個文件來操作(一切皆文件),對一個文件的讀寫操作會調用內核提供的系統命令,返回一個file descriptor(fd,文件描述符)。而對一個socket的讀寫也會有響應的描述符,稱為socket fd(socket文件描述符),描述符就是一個數字,指向內核中的一個結構體(文件路徑,數據區等一些屬性)。
根據UNIX網路編程對I/O模型的分類來說,Linux系統中的網路I/O 模型主要分為同步阻塞IO(Blocking I/O,BIO),同步非阻塞IO(Non-Blocking I/O,NIO),IO多路復用(I/O Multiplexing),非同步IO(Asynchronous I/O,AIO)以及信號驅動式I/O(Signal-Driven I/O)等5種模型,其中:
1.同步阻塞IO(BIO)
同步阻塞式I/O(BIO)模型是最常用的一個模型,也是最簡單的模型。預設情況下,所有文件操作都是阻塞的。
在Linux中,同步阻塞式I/O(BIO)模型下,所有的套接字預設情況下都是阻塞的。
比如I/O模型下的套接字介面:在進程空間中調用recvfrom,其系統調用直到數據包到達且被覆制到應用進程的緩衝區中或者發生錯誤時才返回,在此期間一直等待。
進程在調用recvfrom開始到它返回的整段時間內都是被阻塞的,所以叫阻塞I/O模型。
進程在向內核調用執行recvfrom操作時阻塞,只有當內核將磁碟中的數據複製到內核緩衝區(內核記憶體空間),並實時複製到進程的緩存區完畢後返回;或者發生錯誤時(系統調用信號被中斷)返回。
在載入數據到數據複製完成,整個進程都是被阻塞的,不能處理的別的I/O,此時的進程不再消費CPU時間,而是等待響應的狀態,從處理的角度來看,這是非常有效的。
這種I/O模型下,執行的兩個階段進程都是阻塞的,其中:
-
第一階段(阻塞):
①:進程向內核發起系統調用(recvfrom);當進程發起調用後,進程開始掛起(進程進入不可中斷睡眠狀態),進程一直處於等待內核處理結果的狀態,此時的進程不能處理其他I/O,亦被阻塞。
②:內核收到進程的系統調用請求後,此時的數據包並未準備好,此時內核亦不會給進程發送任何消息,直到磁碟中的數據載入至內核緩衝區; -
第二階段(阻塞):
③:內核再將內核緩衝區中的數據複製到用戶空間中的進程緩衝區中(真正執行IO過程的階段),直到數據複製完成。
④:內核返回成功數據處理完成的指令給進程;進程在收到指令後再對數據包進程處理;處理完成後,此時的進程解除不可中斷睡眠態,執行下一個I/O操作。綜上所述,在Linux中,同步阻塞式I/O(BIO)模型最典型的代表就是阻塞方式下的read/write函數調用。
2.同步非阻塞IO(NIO)
同步非阻塞IO(NIO)模型是進程在調用recvfrom從應用層到內核的時候,就直接返回一個WAGAIN標識或EWOULDBLOCK錯誤,一般都對非阻塞I/O模型進行輪詢檢查這個狀態,看內核是不是有數據到來。
在Linux中,同步非阻塞IO(NIO)模型模型下,進程在向內核調用函數recvfrom執行I/O操作時,socket是以非阻塞的形式打開的。
也就是說,進程進行系統調用後,內核沒有準備好數據的情況下,會立即返回一個錯誤碼,說明進程的系統調用請求不會立即滿足。
在進程發起recvfrom系統調用時,進程並沒有被阻塞,內核馬上返回了一個error。
進程在收到error,可以處理其他的事物,過一段時間在次發起recvfrom系統調用;其不斷的重覆發起recvfrom系統調用,這個過程即為進程輪詢(polling)。
輪詢的方式向內核請求數據,直到數據準備好,再複製到用戶空間緩衝區,進行數據處理。
需要註意的是,複製過程中進程還是阻塞的。
一般情況下,進程採用輪詢(polling)的機制檢測I/O調用的操作結果是否已完成,會消耗大量的CPU時鐘周期,性能上並不一定比阻塞式I/O高。
這種I/O模型下,執行的第一階段進程都是非阻塞的,第二階段進程都是阻塞的,其中:
-
第一階段(非阻塞):
①:進程向內核發起IO調用請求,內核接收到進程的I/O調用後準備處理並返回“error”的信息給進程;此後每隔一段時間進程都會想內核發起詢問是否已處理完,即輪詢,此過程稱為為忙等待;
②:內核收到進程的系統調用請求後,此時的數據包並未準備好,此時內核會給進程發送error信息,直到磁碟中的數據載入至內核緩衝區; -
第二階段(阻塞):
③:內核再將內核緩衝區中的數據複製到用戶空間中的進程緩衝區中(真正執行IO過程的階段,進程阻塞),直到數據複製完成。
④:內核返回成功數據處理完成的指令給進程;進程在收到指令後再對數據包進程處理;
綜上所述,在Linux中,同步非阻塞IO(NIO)模型模型最典型的代表就是以O_NONBLOCK參數打開fd,然後執行read/write函數調用。
3.IO多路復用(I/O Multiplexing)
IO多路復用(I/O Multiplexing)模型也被稱為事件驅動式I/O模型(Event Driven I/O),Linux提供select/poll,進程通過將一個或多個fd傳遞給select或poll系統調用,阻塞在select操作上,這樣,select/poll可以幫我們偵測多個fd是否處於就緒狀態。select/poll是順序掃描fd是否就緒,而且支持的fd數量有限,因此它的使用受到了一些制約。Linux還提供一個epoll系統調用,epoll使用基於事件驅動方式代替順序掃描,因此性能更高。當有fd就緒時,立即回調函數rollback。
在Linux中,IO多路復用(I/O Multiplexing)模型模型下,每一個socket,一般都會設置成non-blocking。
進程通過調用內核中的select()、poll()、epoll()函數發起系統調用請求。
selec/poll/epoll相當於內核中的代理,進程所有的請求都會先請求這幾個函數中的某一個;此時,一個進程可以同時處理多個網路連接的I/O。
select/poll/epoll這個函數會不斷的輪詢(polling)所負責的socket,當某個socket有數據報準備好了(意味著socket可讀),就會返回可讀的通知信號給進程。
用戶進程調用select/poll/epoll後,進程實際上是被阻塞的,同時,內核會監視所有select/poll/epoll所負責的socket,當其中任意一個數據準備好了,就會通知進程。
只不過進程是阻塞在select/poll/epoll之上,而不是被內核准備數據過程中阻塞。
此時,進程再發起recvfrom系統調用,將數據中內核緩衝區拷貝到內核進程,這個過程是阻塞的。
雖然select/poll/epoll可以使得進程看起來是非阻塞的,因為進程可以處理多個連接,但是最多只有1024個網路連接的I/O;本質上進程還是阻塞的,只不過它可以處理更多的網路連接的I/O而已。
這種I/O模型下,執行的第一階段進程都是阻塞的,第二階段進程都是阻塞的,其中:
-
第一階段(阻塞在select/poll之上):
①:進程向內核發起select/poll的系統調用,select將該調用通知內核開始準備數據,而內核不會返回任何通知消息給進程,但進程可以繼續處理更多的網路連接I/O;
②:內核收到進程的系統調用請求後,此時的數據包並未準備好,此時內核亦不會給進程發送任何消息,直到磁碟中的數據載入至內核緩衝區;而後通過select()/poll()函數將socket的可讀條件返回給進程 -
第二階段(阻塞):
③:進程在收到SIGIO信號程式之後,進程向內核發起系統調用(recvfrom);
④:內核再將內核緩衝區中的數據複製到用戶空間中的進程緩衝區中(真正執行IO過程的階段),直到數據複製完成。
⑤:內核返回成功數據處理完成的指令給進程;進程在收到指令後再對數據包進程處理;處理完成後,此時的進程解除不可中斷睡眠態,執行下一個I/O操作。
4.非同步IO(AIO)
非同步IO(AIO)模型是告知內核啟動某個操作,並讓內核在整個操作完成後(包括數據的複製)通知進程。信號驅動I/O模型通知的是何時可以開始一個I/O操作,非同步I/O模型有內核通知I/O操作何時已經完成。
在Linux中,非同步IO(AIO)模型中,進程會向內核請求air_read(非同步讀)的系統調用操作,會把套接字描述符、緩衝區指針、緩衝區大小和文件偏移一起發給內核,當內核收到後會返回“已收到”的消息給進程,此時進程可以繼續處理其他I/O任務。
也就是說,在第一階段內核准備數據的過程中,進程並不會被阻塞,會繼續執行。
第二階段,當數據報準備好之後,內核會負責將數據報複制到用戶進程緩衝區,這個過程也是由內核完成,進程不會被阻塞。
複製完成後,內核向進程遞交aio_read的指定信號,進程在收到信號後進行處理並處理數據報向外發送。
在進程發起I/O調用到收到結果的過程,進程都是非阻塞的。
從一定程度上說,非同步IO(AIO)模型可以說是在信號驅動式I/O模型的一個特例。
這種I/O模型下,執行的第一階段進程都是非阻塞的,第二階段進程都是非阻塞的,其中:
-
第一階段(非阻塞):
①:進程向內核請求air_read(非同步讀)的系統調用操作,會把套接字描述符、緩衝區指針、緩衝區大小和文件偏移一起發給內核,當內核收到後會返回“已收到”的消息給進程
②:內核將磁碟中的數據載入至內核緩衝區,直到數據報準備好; -
第二階段(非阻塞):
③:內核開始複製數據,將準備好的數據報複制到進程記憶體空間,知道數據報複制完成
④:內核向進程遞交aio_read的返回指令信號,通知進程數據已複製到進程記憶體中
5.信號驅動式I/O(Signal-Driven I/O)
信號驅動式I/O(Signal-Driven I/O)模型是指首先開啟套介面信號驅動I/O功能,並通過系統調用sigaction執行一個信號處理函數(此系統調用立即返回,進程繼續工作,非阻塞)。當數據準備就緒時,就為改進程生成一個SIGIO信號,通過信號回調通知應用程式調用recvfrom來讀取數據,並通知主迴圈函數處理樹立。
在Linux中,信號驅動式I/O(Signal-Driven I/O)模型中,進程預先告知內核,使得某個文件描述符上發生了變化時,內核使用信號通知該進程。
在信號驅動式I/O模型,進程使用socket進行信號驅動I/O,並建立一個SIGIO信號處理函數。
當進程通過該信號處理函數向內核發起I/O調用時,內核並沒有準備好數據報,而是返回一個信號給進程,此時進程可以繼續發起其他I/O調用。
也就是說,在第一階段內核准備數據的過程中,進程並不會被阻塞,會繼續執行。
當數據報準備好之後,內核會遞交SIGIO信號,通知用戶空間的信號處理程式,數據已準備好;此時進程會發起recvfrom的系統調用,這一個階段與阻塞式I/O無異。
也就是說,在第二階段內核覆制數據到用戶空間的過程中,進程同樣是被阻塞的。
這種I/O模型下,執行的第一階段進程都是非阻塞的,第二階段進程都是阻塞的,其中:
-
第一階段(非阻塞):
①:進程使用socket進行信號驅動I/O,建立SIGIO信號處理函數,向內核發起系統調用,內核在未準備好數據報的情況下返回一個信號給進程,此時進程可以繼續做其他事情
②:內核將磁碟中的數據載入至內核緩衝區完成後,會遞交SIGIO信號給用戶空間的信號處理程式; -
第二階段(阻塞):
③:進程在收到SIGIO信號程式之後,進程向內核發起系統調用(recvfrom);
④:內核再將內核緩衝區中的數據複製到用戶空間中的進程緩衝區中(真正執行IO過程的階段),直到數據複製完成。
⑤:內核返回成功數據處理完成的指令給進程;進程在收到指令後再對數據包進程處理;處理完成後,此時的進程解除不可中斷睡眠態,執行下一個I/O操作。
(二). 文件操作 I/O 模型
在Linux系統中的網路I/O 模型,按照文件操作IO來說,主要分為緩衝IO(Buffered I/O),直接IO(Direct I/O),記憶體映射(Memory-Mapped,mmap),零拷貝(Zero Copy)等4種模型,其中:
1.緩衝IO(Buffered I/O)
緩衝IO(Buffered I/O) 是指在記憶體里開闢一塊區域里存放的數據,主要用來接收用戶輸入和用於電腦輸出的數據以減小系統開銷和提高外設效率的緩衝區機制。
緩存I/O又被稱作標準I/O,大多數文件系統的預設I/O操作都是緩存I/O。在Linux的緩存I/O機制中,數據先從磁碟複製到內核空間的緩衝區,然後從內核空間緩衝區複製到應用程式的地址空間。
總的來說,緩衝區是記憶體空間的一部分,在記憶體中預留了一定的存儲空間,用來暫時保存輸入和輸出等I/O操作的一些數據,這些預留的空間就叫做緩衝區。
而buffer緩衝區和Cache緩存區都屬於緩衝區的一種buffer緩衝區存儲速度不同步的設備或者優先順序不同的設備之間的傳輸數據,比如鍵盤、滑鼠等;
此外,buffer一般是用在寫入磁碟的;Cache緩存區是位於CPU和主記憶體之間的容量較小但速度很快的存儲器,Cache保存著CPU剛用過的數據或迴圈使用的數據;Cache緩存區的運用一般是在I/O的請求上
緩存區按性質分為兩種,一種是輸入緩衝區,另一種是輸出緩衝區。
對於C、C++程式來言,類似cin、getchar等輸入函數讀取數據時,並不會直接從鍵盤上讀取,而是遵循著一個過程:cingetchar --> 輸入緩衝區 --> 鍵盤,
我們從鍵盤上輸入的字元先存到緩衝區裡面,cingetchar等函數是從緩衝區裡面讀取輸入;
那麼相對於輸出來說,程式將要輸出的結果並不會直接輸出到屏幕當中區,而是先存放到輸出緩存區,然後利用coutputchar等函數將緩衝區中的內容輸出到屏幕上。
cin和cout本質上都是對緩衝區中的內容進行操作。
使用緩衝區機制的主要可以解決的問題,主要有:
- 減少CPU對磁碟的讀寫次數: CPU讀取磁碟中的數據並不是直接讀取磁碟,而是先將磁碟的內容讀入到記憶體,也就是緩衝區,然後CPU對緩衝區進行讀取,進而操作數據;電腦對緩衝區的操作時間遠遠小於對磁碟的操作時間,大大的加快了運行速度
- 提高CPU的執行效率: 比如說使用印表機列印文檔,列印的速度是相對比較慢的,我們操作CPU將要列印的內容輸出到緩衝區中,然後CPU轉手就可以做其他的操作,進而提高CPU的效率
- 合併讀寫: 比如說對於一個文件的數據,先讀取後寫入,迴圈執行10次,然後關閉文件,如果存在緩衝機制,那麼就可能只有第一次讀和最後一次寫是真實操作,其他的操作都是在操作緩存
但是,在緩存 I/O 機制中,DMA 方式可以將數據直接從磁碟讀到頁緩存中,或者將數據從頁緩存直接寫回到磁碟上,而不能直接在應用程式地址空間和磁碟之間進行數據傳輸。
這樣,數據在傳輸過程中需要在應用程式地址空間(用戶空間)和緩存(內核空間)之間進行多次數據拷貝操作,這些數據拷貝操作所帶來的CPU以及記憶體開銷是非常大的。
在Linux中,緩衝區分為三大類:全緩衝、行緩衝、無緩衝,其中:
- 全緩衝;只有在緩衝區被填滿之後才會進行I/O操作;最典型的全緩衝就是對磁碟文件的讀寫。
- 行緩衝;只有在輸入或者是輸出中遇到換行符的時候才會進行I/O操作;這忠允許我們一次寫一個字元,但是只有在寫完一行之後才做I/O操作。一般來說,標準輸入流(stdin)和標準輸出流(stdout)是行緩衝。
- 無緩衝;標準I/O不緩存字元;其中表現最明顯的就是標準錯誤輸出流(stderr),這使得出錯信息儘快的返回給用戶。
2.直接IO(Direct I/O)
直接IO(Direct I/O)是指應用程式直接訪問磁碟數據,而不經過內核緩衝區,也就是繞過內核緩衝區,自己管理IO緩存區,這樣做的目的是減少一次內核緩衝區到用戶程式緩存的數據複製。
直接IO就是在應用層Buffer和磁碟之間直接建立通道。這樣在讀寫數據的時候就能夠減少上下文切換次數,同時也能夠減少數據拷貝次數,從而提高效率。
引入內核緩衝區的目的在於提高磁碟文件的訪問性能,因為當進程需要讀取磁碟文件時,如果文件內容已經在內核緩衝區中,那麼就不需要再次訪問磁碟。而當進程需要向文件寫入數據是,實際上只是寫到了內核緩衝區便告訴進程已經寫成功,而真正寫入磁碟是通過一定的策略進行延時的。
然而,對於一些較複雜的應用,比如資料庫伺服器,他們為了充分提高性能。希望繞過內核緩衝區,由自己在用戶態空間時間並管理IO緩衝區,包括緩存機制和寫延遲機制等,以支持獨特的查詢機制,比如資料庫可以根據加合理的策略來提高查詢緩存命中率。另一方面,繞過內核緩衝區也可以減少系統記憶體的開銷,因為內核緩衝區本身就在使用系統記憶體。
3.記憶體映射(Memory-Mapped,mmap)
記憶體映射(Memory-Mapped I/O,mmap)是指把物理記憶體映射到進程的地址空間之內,這些應用程式就可以直接使用輸入輸出的地址空間,從而提高讀寫的效率。
記憶體映射(Memory-mapped I/O)是將磁碟文件的數據映射到記憶體,用戶通過修改記憶體就能修改磁碟文件。
Linux提供了mmap()函數,用來映射物理記憶體。在驅動程式中,應用程式以設備文件為對象,調用mmap()函數,內核進行記憶體映射的準備工作,生成vm_area_struct結構體,然後調用設備驅動程式中定義的mmap函數。
4.零拷貝(Zero Copy)
零拷貝(Zero Copy)技術是指電腦執行操作時,CPU不需要先將數據從某處記憶體複製到另一個特定區域,這種技術通常用於通過網路傳輸文件時節省CPU周期和記憶體帶寬。
在此之前,我們需要知道什麼是拷貝?拷貝主要是指把數據從一塊記憶體中複製到另外一塊記憶體中。
零拷貝(Zero Copy)是一種I/O操作優化技術,主要是指電腦執行操作時,CPU不需要先將數據從某處記憶體複製到另一個特定區域,通常用於通過網路傳輸文件時節省CPU周期和記憶體帶寬,還可以減少上下文切換以及CPU的拷貝時間。
但是需要註意的是,零拷貝技術實際實現並沒有具體的標準,主要取決於操作系統如何實現和完全依賴於操作系統是否支持?一般來說,操作系統支持,就可以零拷貝;否則就沒有辦法做到零拷貝。
一般來說,當我們需要把一些本地磁碟的文件(File)中的數據發送到網路的時候,對於預設的標準i/O來說,Read操作流程:磁碟->內核緩衝區->用戶緩衝區-->應用程式記憶體 和 Write操作流程:磁碟<-內核緩衝區<-用戶緩衝區<-應用程式記憶體,整個過程中數據拷貝會有6次拷貝,3次Read操作,3次Write操作。
如果不用零拷貝,一般來說,主要採用如下兩種方式實現:
- 第一種實現方式:利用直接I/O實現:磁碟->內核緩衝區->應用程式記憶體->Socket緩衝區->網路,整個過程中數據拷貝會有4次拷貝,2次Read操作,2次Write操作,記憶體拷貝是2次。
- 第二種實現方式:利用記憶體映射文件(mmnp)實現:磁碟->內核緩衝區->Socket緩衝區->網路,整個過程中數據拷貝會有3次拷貝,2次Read操作,1次Write操作,記憶體拷貝是1次。
如果使用零拷貝技術實現的話,磁碟->內核緩衝區->網路,整個過程中數據拷貝會有2次拷貝,1次Read操作,1次Write操作,記憶體拷貝是0次。
由此可見,零拷貝是從記憶體的角度來說,數據在記憶體中沒有發生過數據拷貝,只在記憶體和I/O之間傳輸。
在Linux中,系統提供了sendfile函數來實現零拷貝,主要形式:
sendfile(int out_fd,int in_fd,off_t * offset,size_t count)
參數描述:
- out_fd:待寫入內容的文件描述符,一般為accept的返回值
- in_fd:待讀出內容的文件描述符,一般為open的返回值
- offset:指定從讀入文件流的哪個位置開始讀,如果為空,則使用讀入文件流的預設位置,一般設置為NULL
- count:兩文件描述符之間的位元組數,一般給struct stat結構體的一個變數,在struct stat中可以設置文件描述符屬性
⚠️[特別註意]:
in_fd規定指向真實的文件,不能是socket等管道文件描述符,一般使open返回值,而out_fd則是socket描述符
在Java中,FileChannel提供transferTo(和transferFrom)方法來實現sendFile功能。
(三). 主動(Reacror)與被動(Proactor)I/O模型
主動與被動I/O模型是指網路I/O模型中的基於Reacror模式與Proactor模式等兩種設計模式設計的I/O模型,算是所有網路I/O模型的抽象模型。
除了上述提到的網路I/O模型,還有基於Reacror模式與Proactor模式等兩種設計模式設計的I/O模型,是網路框架的基本設計模型。
不論是操作系統的網路I/O模型的設計,還是上層框架中的網路I/O模型的設計,都是基於這兩種設計模式來設計的。其中:
1.Reacror模式:
Reacror模式是主動模式,主要是指應用程式不斷輪詢,訪問操作系統,或者網路框架,網路I/O模型是否就緒。
在Linux系統中,其select,poll和epoll等網路I/O模型都是 Reacror模式下的產生物。需要在應用程式裡面一隻有一個迴圈來輪詢。其中,Java中的NIO模型也是屬於這種模式。
在 Reacror模式下,實際的 網路I/O請求操作都是在應用程式下執行的。
2.Proactor模式:
Proactor模式是被動模式,主要是指應用程式網路I/O操作請求全部托管和交付給操作系統或者網路框架來實現。
在 Proactor模式下,實際的 網路I/O請求操作都是在應用程式下執行,之後再回調到應用程式。
(四). 伺服器編程I/O模型
伺服器編程I/O模型是指一個伺服器會有1+N+M個線程,主要有1個監聽線程,N個I/O線程,M個Worker線程,因此也稱為1+N+M伺服器編程模型。
在1+N+M伺服器編程模型中,監聽線程->對應每一個客戶端socket建立和連接,I/O線程->對應N的個數通常是以CPU核數作為參考,而Worker線程>M的個數根據實際業務場景的數據上層決定。其中:
- 監聽線程: 主要負責Accept事件的註冊和處理。和每一個新進來的客戶端建立socket連接,然後把socket連接轉接交給I/O線程,完成結束後繼續監聽新的客戶端請求。
- I/O線程:主要負責每個socket連接上面read/write事件的註冊和實際的socket的讀寫。負責把讀到的請求放入Requset隊列,最後托管交給Worker線程處理。
- Worker線程:主要是純粹的業務線程,沒有socket連接上的read(讀)/write(寫)操作。Worker線程處理完請求最後寫入響應Response隊列,最終交給I/O線程返回客戶端。
實際上,在linux系統中epoll和Java中的NIO模型,以及基於Netty的開發的網路框架,都是按照1+N+M伺服器編程模型來做的。
寫在最後
I/O模型是為解決各種問題而提出的,主要涉及有線程(Thread),阻塞(Blocking),非阻塞(Non-Blocking) ,同步(Synchronous) 和非同步(Asynchronous) 等相關的概念。
按照一定意義上說,I/O模型可以分為阻塞I/O(Blocking IO,BIO),非阻塞I/O(Non-Blocking IO,NIO)兩大類。
在Linux系統中,其中:
- 根據UNIX網路編程對I/O模型的分類來說,網路I/O 模型主要分為同步阻塞IO(Blocking I/O,BIO),同步非阻塞IO(Non-Blocking I/O,NIO),IO多路復用(I/O Multiplexing),非同步IO(Asynchronous I/O,AIO)以及信號驅動式I/O(Signal-Driven I/O)等5種模型。
- 按照文件操作IO來說,主要分為緩衝IO(Buffered I/O),直接IO(Direct I/O),記憶體映射(Memory-Mapped,mmap),零拷貝(Zero Copy)等4種模型。
其中,在文件操作I/O中,我們需要區別對待拷貝和映射:
拷貝主要是指把數據從一塊記憶體中複製到另外一塊記憶體中,而映射只是持有數據的一份引用(或者叫地址),數據本身只有一份。
除此之外,網路I/O模型,還有基於Reacror模式與Proactor模式等兩種設計模式設計的I/O模型,是網路框架的基本設計模型。
以及,一個伺服器會有1+N+M個線程,主要有1個監聽線程,N個I/O線程,M個Worker線程,因此也稱為1+N+M伺服器編程模型。
綜上所述,只有正確和清楚地知道這個基礎指導,才能加深我們對Java領域中的多線程模型的認識,才能更好地指導我們掌握併發編程。
版權聲明:本文為博主原創文章,遵循相關版權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源。