蒼穹之邊,浩瀚之摯,眰恦之美; 悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說》

寫在開頭

作為一名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領域中的多執行緒模型的認識,才能更好地指導我們掌握并發編程,
著作權宣告:本文為博主原創文章,遵循相關著作權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/501406.html
標籤:其他
