IO訪問方式
磁盤IO

具體步驟:
當應用程序調用read接口時,操作系統檢查內核緩沖區中是否存在需要的數據,如果存在,就直接從內核緩存中直接返回,否則從磁盤中讀取,然后緩存至操作系統的緩存中。
當應用程序調用write接口時,將數據直接從用戶地址空間復制到內核地址空間的緩存中,這時對用戶程序來說,寫操作已經完成了,至於什么時候寫入磁盤中,由操作系統決定,除非顯示調用sync同步命令。
網絡IO

1、當調用系統read接口時,通過DMA(Direct Memory Access)將數據拷貝到內核緩沖區;
2、然后由CPU控制,將內核緩沖區的數據拷貝到用戶模式的buffer中;
3、當調用系統write接口時,會把用戶模式下buffer數據拷貝到內核緩沖區的Socket Buffer中;
4、最后通過DMA copy將內核模式下的socket buffer中數據拷貝到網卡設備中傳輸。
從上面整個read、write過程來看,數據白白從內核模式到用戶模式走了一圈,浪費了兩次copy,而這兩次有需要CPU copy,即占用CPU資源。
DMA(直接存儲器訪問):
直接存儲器存取(DMA)用來提供在外設和存儲器之間或者存儲器和存儲器之間的高速數據傳輸。 你只要使能並配置好了DMA,DMA就可以將一批數據從源地址搬運到目的地址去而不經過CPU的干預
要進行數據傳輸就必須有兩個條件:數據從哪傳(源地址),數據傳到哪里去(目的地址)。是的DMA的確有這兩項設置,通過軟件設置,設置好源地址和目的地址。
磁盤IO與網絡IO對比
磁盤IO主要延遲是由(以15000rpm硬盤為例):機械轉動延時(機械硬盤為主要性能瓶頸,平均2ms)+尋址延時*(2-3ms)+塊傳輸延時(一般4k每塊,40m/s的傳輸速度,延時一般為0.1ms)決定。(平均為5ms)
網絡IO主要延時是由:服務器響應延時+帶寬限制+網絡延時+跳轉路由延時+本地接收延時 決定。(一般為幾十到幾千毫秒,受環境影響較大)
所以,一般來說,網絡IO延時要大於磁盤IO延時。
用戶緩沖區:
IO,其實意味着:數據不停地搬入搬出緩沖區而已(使用了緩沖區)。比如,用戶程序發起讀操作,導致“ syscall read ”系統調用,就會把數據搬入到 一個buffer中;用戶發起寫操作,導致 “syscall write ”系統調用,將會把一個 buffer 中的數據 搬出去(發送到網絡中 or 寫入到磁盤文件)。
DMA(Direct Memory Access,直接內存存取,不需要CPU參與,下面有解釋) 是所有現代電腦的重要特色,它允許不同速度的硬件裝置來溝通,而不需要依賴於 CPU 的大量中斷負載。
整個IO過程的流程如下:
1)程序員寫代碼創建一個緩沖區(這個緩沖區是用戶緩沖區):哈哈。然后在一個while循環里面調用read()方法讀數據(觸發"syscall read"系統調用)
byte[] b = new byte[4096]; while((read = inputStream.read(b))>=0) { total = total + read; // other code.... }
2)當執行到read()方法時,其實底層是發生了很多操作的: ①內核給磁盤控制器發命令說:我要讀磁盤上的某某塊磁盤塊上的數據。②在DMA的控制下,把磁盤上的數據讀入到內核緩沖區。③內核把數據從內核緩沖區復制到用戶緩沖區。這里的用戶緩沖區應該就是我們寫的代碼中 new 的 byte[] 數組。
內核緩沖區(kernel space):
操心系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。為了保證用戶進程不能直接操作內核,保證內核的安全,操心系統將虛擬空間划分為兩部分,一部分為內核空間,一部分為用戶空間。針對linux操作系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱為內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。每個進程可以通過系統調用進入內核,因此,Linux內核由系統內的所有進程共享。
對於操作系統而言,JVM只是一個用戶進程,處於用戶態空間中。而處於用戶態空間的進程是不能直接操作底層的硬件的。而IO操作就需要操作底層的硬件,比如磁盤。因此,IO操作必須得借助內核的幫助才能完成(中斷,trap),即:會有用戶態到內核態的切換。
我們寫代碼 new byte[] 數組時,一般是都是“隨意” 創建一個“任意大小”的數組。比如,new byte[128]、new byte[1024]、new byte[4096]....,即用戶緩沖區,但是,對於磁盤塊的讀取而言,每次訪問磁盤讀數據時,並不是讀任意大小的數據的,而是:每次讀一個磁盤塊或者若干個磁盤塊(這是因為訪問磁盤操作代價是很大的,而且我們也相信局部性原理) 因此,就需要有一個“中間緩沖區”--即內核緩沖區。先把數據從磁盤讀到內核緩沖區中,然后再把數據從內核緩沖區搬到用戶緩沖區。這也是為什么我們總感覺到第一次read操作很慢,而后續的read操作卻很快的原因吧。因為,對於后續的read操作而言,它所需要讀的數據很可能已經在內核緩沖區了,此時只需將內核緩沖區中的數據拷貝到用戶緩沖區即可,並未涉及到底層的讀取磁盤操作,當然就快了。

NIO中的內存映射:

內核空間的 buffer 與 用戶空間的 buffer 都映射到同一塊 物理內存區域。當用戶進程訪問“內存映射文件”(即用戶緩存)地址時,自動產生缺頁錯誤,然后由底層的OS負責將磁盤上的數據送到物理內存區域,用戶訪問用戶空間的 buffer時,直接轉到物理內存區域,這就是直接內存映射IO,也即JAVA NIO中提到的內存映射文件,或者說 直接內存....總之,它們表達的意思都差不多。
傳統的IO讀寫有兩種方式:IO中斷和DMA。他們各自的原理如下。
1、IO中斷原理
整個流程如下:
- 1.用戶進程調用read等系統調用向操作系統發出IO請求,請求讀取數據到自己的內存緩沖區中。自己進入阻塞狀態。
- 2.操作系統收到請求后,進一步將IO請求發送磁盤。
- 3.磁盤驅動器收到內核的IO請求,把數據從磁盤讀取到驅動器的緩沖中。此時不占用CPU。當驅動器的緩沖區被讀滿后,向內核發起中斷信號告知自己緩沖區已滿。
- 4.內核收到中斷,使用CPU時間將磁盤驅動器的緩存中的數據拷貝到內核緩沖區中。
- 5.如果內核緩沖區的數據少於用戶申請的讀的數據,重復步驟3跟步驟4,直到內核緩沖區的數據足夠多為止。
- 6.CPU將數據從內核緩沖區拷貝到用戶緩沖區,同時從系統調用中返回。完成任務。
缺點:用戶的每次IO請求,都需要CPU多次參與。
2、DMA原理
- 1.用戶進程調用read等系統調用向操作系統發出IO請求,請求讀取數據到自己的內存緩沖區中。自己進入阻塞狀態。
- 2.操作系統收到請求后,進一步將IO請求發送DMA。然后讓CPU干別的活去。
- 3.DMA進一步將IO請求發送給磁盤。
- 4.磁盤驅動器收到DMA的IO請求,把數據從磁盤讀取到驅動器的緩沖中。當驅動器的緩沖區被讀滿后,向DMA發起中斷信號告知自己緩沖區已滿。
- 4.DMA收到磁盤驅動器的信號,將磁盤驅動器的緩存中的數據拷貝到內核緩沖區中。此時不占用CPU(IO中斷這里是占用CPU的)。這個時候只要內核緩沖區的數據少於用戶申請的讀的數據,內核就會一直重復步驟3跟步驟4,直到內核緩沖區的數據足夠多為止。
- 5.當DMA讀取了足夠多的數據,就會發送中斷信號給CPU。
- 6.CPU收到DMA的信號,知道數據已經准備好,於是將數據從內核拷貝到用戶空間,系統調用返回。
跟IO中斷模式相比,DMA模式下,DMA就是CPU的一個代理,它負責了一部分的拷貝工作,從而減輕了CPU的負擔。
DMA的優點就是:中斷少,CPU負擔低。
文件到網絡場景的zero copy技術
1、傳統IO讀寫方式的問題
在讀取文件數據然后發送到網絡這個場景中,傳統IO讀寫方式的過程如下。
由圖可知,整個過程總共發生了四次拷貝和四次的用戶態和內核態的切換。
用戶態和內核態的切換如下。借個網上的圖。
2、zero copy技術
zero copy技術就是減少不必要的內核緩沖區跟用戶緩沖區間的拷貝,從而減少CPU的開銷和內核態切換開銷,達到性能的提升。
zero copy下,同樣的讀取文件然后通過網絡發送出去,只需要拷貝三次,只發生兩次內核態和用戶態的切換。
再次盜用一下別人的圖。
linux下的zero copy技術
linux下的用來實現zero copy的常見接口由如下幾個:
- ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)
- long splice(int fdin, int fdout, size_t len, unsigned int flags);
這兩個接口都可以用來在兩個文件描述符之間傳輸數據,實現所謂的zero copy。
splice接口則要求兩個文件描述符中至少要有一個是pipe。
sendfile跟splice的局限性
上面提到的用來實現零拷貝的sendfile和splice接口,僅限於文件跟文件,文件跟sock之間傳輸數據,但是沒法直接在兩個socket之間傳輸數據的。這就是sendfile和splice接口的局限性。
如果要實現socket跟socket之間的數據直接拷貝,需要開辟一個pipe,然后調用兩次splice。這樣還是帶來跟傳統IO讀寫一樣的問題。系能其實並沒有什么大的提升。
