專屬客服18376668806 在線咨詢 留言/需求提交

磁盤及網絡 IO 工作方式解析

PIO 與 DMA

有必要簡單地說說慢速 I/O 設備和內存之間的數據傳輸方式。

  • PIO
    我們拿磁盤來說,很早以前,磁盤和內存之間的數據傳輸是需要 CPU 控制的,也就是說如果我們讀取磁盤文件到內存中,數據要經過 CPU存儲轉發,這種方式稱為 PIO。顯然這種方式非常不合理,需要占用大量的 CPU 時間來讀取文件,造成文件訪問時系統幾乎停止響應。

  • DMA
    后來,DMA(直接內存訪問,Direct Memory Access)取代了 PIO,它可以不經過 CPU 而直接進行磁盤和內存的數據交換。在 DMA 模式下,CPU 只需要向 DMA 控制器下達指令,讓 DMA 控制器來處理數據的傳送即可,DMA 控制器通過系統總線來傳輸數據,傳送完畢再通知 CPU,這樣就在很大程度上降低了CPU占有率,大大節省了系統資源,而它的傳輸速度與PIO的差異其實并不十分明顯,因為這主要取決于慢速設備的速度。

可以肯定的是,PIO 模式的計算機我們現在已經很少見到了。

標準文件訪問方式


具體步驟:

當應用程序調用 read 接口時,操作系統檢查在內核的高速緩存有沒有需要的數據,如果已經緩存了,那么就直接從緩存中返回,如果沒有,則從磁盤中讀取,然后緩存在操作系統的緩存中。

應用程序調用 write 接口時,將數據從用戶地址空間復制到內核地址空間的緩存中,這時對用戶程序來說,寫操作已經完成,至于什么時候再寫到磁盤中,由操作系統決定,除非顯示調用了 sync 同步命令。


內存映射(減少數據在用戶空間和內核空間之間的拷貝操作,適合大量數據傳輸)

Linux 內核提供一種訪問磁盤文件的特殊方式,它可以將內存中某塊地址空間和我們要指定的磁盤文件相關聯,從而把我們對這塊內存的訪問轉換為對磁盤文件的訪問,這種技術稱為內存映射(Memory Mapping)。

操作系統將內存中的某一塊區域與磁盤中的文件關聯起來,當要訪問內存中的一段數據時,轉換為訪問文件的某一段數據。這種方式的目的同樣是減少數據從內核空間緩存到用戶空間緩存的數據復制操作,因為這兩個空間的數據是共享的

內存映射是指將硬盤上文件的位置與進程邏輯地址空間中一塊大小相同的區域一一對應,當要訪問內存中一段數據時,轉換為訪問文件的某一段數據。這種方式的目的同樣是減少數據在用戶空間和內核空間之間的拷貝操作。當大量數據需要傳輸的時候,采用內存映射方式去訪問文件會獲得比較好的效率。

使用內存映射文件處理存儲于磁盤上的文件時,將不必再對文件執行 I/O 操作,這意味著在對文件進行處理時將不必再為文件申請并分配緩存,所有的文件緩存操作均由系統直接管理,由于取消了將文件數據加載到內存、數據從內存到文件的回寫以及釋放內存塊等步驟,使得內存映射文件在處理大數據量的文件時能起到相當重要的作用

訪問步驟

在大多數情況下,使用內存映射可以提高磁盤 I/O 的性能,它無須使用 read() 或 write() 等系統調用來訪問文件,而是通過 mmap() 系統調用來建立內存和磁盤文件的關聯,然后像訪問內存一樣自由地訪問文件。

有兩種類型的內存映射,共享型和私有型,前者可以將任何對內存的寫操作都同步到磁盤文件,而且所有映射同一個文件的進程都共享任意一個進程對映射內存的修改;后者映射的文件只能是只讀文件,所以不可以將對內存的寫同步到文件,而且多個進程不共享修改。顯然,共享型內存映射的效率偏低,因為如果一個文件被很多進程映射,那么每次的修改同步將花費一定的開銷。

直接 I/O(繞過內核緩沖區,自己管理 I/O 緩存區)

在 Linux 2.6 中,內存映射和直接訪問文件沒有本質上差異,因為數據從進程用戶態內存空間到磁盤都要經過兩次復制,即在磁盤與內核緩沖區之間以及在內核緩沖區與用戶態內存空間。

引入內核緩沖區的目的在于提高磁盤文件的訪問性能,因為當進程需要讀取磁盤文件時,如果文件內容已經在內核緩沖區中,那么就不需要再次訪問磁盤;而當進程需要向文件中寫入數據時,實際上只是寫到了內核緩沖區便告訴進程已經寫成功,而真正寫入磁盤是通過一定的策略進行延遲的。

然而,對于一些較復雜的應用,比如數據庫服務器,它們為了充分提高性能,希望繞過內核緩沖區,由自己在用戶態空間實現并管理 I/O 緩沖區,包括緩存機制和寫延遲機制等,以支持獨特的查詢機制,比如數據庫可以根據更加合理的策略來提高查詢緩存命中率。另一方面,繞過內核緩沖區也可以減少系統內存的開銷,因為內核緩沖區本身就在使用系統內存。

應用程序直接訪問磁盤數據,不經過操作系統內核數據緩沖區,這樣做的目的是減少一次從內核緩沖區到用戶程序緩存的數據復制。這種方式通常是在對數據的緩存管理由應用程序實現的數據庫管理系統中。

直接 I/O 的缺點就是如果訪問的數據不在應用程序緩存中,那么每次數據都會直接從磁盤進行加載,這種直接加載會非常緩慢。通常直接 I/O 跟異步 I/O 結合使用會得到較好的性能。

訪問步驟

Linux 提供了對這種需求的支持,即在open()系統調用中增加參數選項 O_DIRECT,用它打開的文件便可以繞過內核緩沖區的直接訪問,這樣便有效避免了 CPU 和內存的多余時間開銷

順便提一下,與 O_DIRECT 類似的一個選項是 O_SYNC,后者只對寫數據有效,它將寫入內核緩沖區的數據立即寫入磁盤,將機器故障時數據的丟失減少到最小,但是它仍然要經過內核緩沖區。

sendfile/零拷貝(網絡 I/O,kafka 用到此特性)

普通的網絡傳輸步驟如下:

1)操作系統將數據從磁盤復制到操作系統內核的頁緩存中
2)應用將數據從內核緩存復制到應用的緩存中
3)應用將數據寫回內核的 Socket 緩存中
4)操作系統將數據從 Socket 緩存區復制到網卡緩存,然后將其通過網絡發出


1、當調用 read 系統調用時,通過 DMA(Direct Memory Access)將數據 copy 到內核模式
2、然后由 CPU 控制將內核模式數據 copy 到用戶模式下的 buffer 中
3、read 調用完成后,write 調用首先將用戶模式下 buffer 中的數據 copy 到內核模式下的 socket buffer 中
4、最后通過 DMA copy 將內核模式下的 socket buffer 中的數據 copy 到網卡設備中傳送。

從上面的過程可以看出,數據白白從內核模式到用戶模式走了一圈,浪費了兩次 copy,而這兩次 copy 都是 CPU copy,即占用 CPU 資源。

sendfile


通過 sendfile 傳送文件只需要一次系統調用,當調用 sendfile 時:
1、首先通過 DMA copy 將數據從磁盤讀取到 kernel buffer 中
2、然后通過 CPU copy 將數據從 kernel buffer copy 到 sokcet buffer 中
3、最終通過 DMA copy 將 socket buffer 中數據 copy 到網卡 buffer 中發送
sendfile 與 read/write 方式相比,少了 一次模式切換一次 CPU copy。但是從上述過程中也可以發現從 kernel buffer 中將數據 copy 到socket buffer 是沒必要的。

為此,Linux2.4 內核對 sendfile 做了改進,下圖所示

改進后的處理過程如下:
1、DMA copy 將磁盤數據 copy 到 kernel buffer 中
2、向 socket buffer 中追加當前要發送的數據在 kernel buffer 中的位置和偏移量
3、DMA gather copy 根據 socket buffer 中的位置和偏移量直接將 kernel buffer 中的數據 copy 到網卡上。

經過上述過程,數據只經過了 2 次 copy 就從磁盤傳送出去了。(事實上這個 Zero copy 是針對內核來講的,數據在內核模式下是 Zero-copy 的)。
當前許多高性能 http server 都引入了 sendfile 機制,如 nginx,lighttpd 等。

FileChannel.transferTo(Java 中的零拷貝)

Java NIO 中 FileChannel.transferTo(long position, long count, WriteableByteChannel target)方法將當前通道中的數據傳送到目標通道 target 中,在支持 Zero-Copy 的 linux 系統中,transferTo() 的實現依賴于 sendfile() 調用。

傳統方式對比零拷貝方式:

整個數據通路涉及 4 次數據復制和 2 個系統調用,如果使用 sendfile 則可以避免多次數據復制,操作系統可以直接將數據從內核頁緩存中復制到網卡緩存,這樣可以大大加快整個過程的速度。

大多數時候,我們都在向 Web 服務器請求靜態文件,比如圖片、樣式表等,根據前面的介紹,我們知道在處理這些請求的過程中,磁盤文件的數據先要經過內核緩沖區,然后到達用戶內存空間,因為是不需要任何處理的靜態數據,所以它們又被送到網卡對應的內核緩沖區,接著再被送入網卡進行發送。

數據從內核出去,繞了一圈,又回到內核,沒有任何變化,看起來真是浪費時間。在 Linux 2.4 的內核中,嘗試性地引入了一個稱為 khttpd 的內核級 Web 服務器程序,它只處理靜態文件的請求。引入它的目的便在于內核希望請求的處理盡量在內核完成,減少內核態的切換以及用戶態數據復制的開銷。

同時,Linux 通過系統調用將這種機制提供給了開發者,那就是 sendfile() 系統調用。它可以將磁盤文件的特定部分直接傳送到代表客戶端的 socket 描述符,加快了靜態文件的請求速度,同時也減少了 CPU 和內存的開銷。

在 OpenBSD 和 NetBSD 中沒有提供對 sendfile 的支持。通過strace的跟蹤看到了 Apache 在處理 151 字節的小文件時,使用了 mmap() 系統調用來實現內存映射,但是在 Apache 處理較大文件的時候,內存映射會導致較大的內存開銷,得不償失,所以 Apache 使用了 sendfile64() 來傳送文件,sendfile64() 是 sendfile() 的擴展實現,它在 Linux 2.4 之后的版本中提供。

這并不意味著 sendfile 在任何場景下都能發揮顯著的作用。對于請求較小的靜態文件,sendfile 發揮的作用便顯得不那么重要,通過壓力測試,我們模擬100 個并發用戶請求151字節的靜態文件,是否使用 sendfile 的吞吐率幾乎是相同的,可見在處理小文件請求時,發送數據的環節在整個過程中所占時間的比例相比于大文件請求時要小很多,所以對于這部分的優化效果自然不十分明顯

docs

99久久香蕉国产线看观看