1. 程式人生 > 其它 >磁碟IO

磁碟IO

磁碟IO的分類

快取IO(標準IO)

快取I/O又被稱作標準I/O,大多數檔案系統的預設I/O操作都是快取I/O。在Linux的快取I/O機制中,資料先從磁碟複製到核心空間的緩衝區,然後從核心空間緩衝區複製到應用程式的地址空間。

讀操作:作業系統檢查核心的緩衝區有沒有需要的資料,如果已經快取了,那麼就直接從快取中返回;否則從磁碟中讀取,然後快取在作業系統的快取中。

寫操作:將資料從使用者空間複製到核心空間的快取中。這時對使用者程式來說寫操作就已經完成,至於什麼時候再寫到磁碟中由作業系統決定,除非顯示地呼叫了sync同步命令(詳情參考《【珍藏】linux 同步IO: sync、fsync與fdatasync

》)。

快取IO的優缺點

優點
  1. 在一定程度上分離了核心空間和使用者空間,保護系統本身的執行安全
  2. 可以減少讀盤的次數,從而提高效能
缺點

快取 I/O 機制中,DMA 方式可以將資料直接從磁碟讀到頁快取中,或者將資料從頁快取直接寫回到磁碟上,而不能直接在應用程式地址空間和磁碟之間進行資料傳輸,這樣,資料在傳輸過程中需要在應用程式地址空間(使用者空間)和快取(核心空間)之間進行多次資料拷貝操作,這些資料拷貝操作所帶來的CPU以及記憶體開銷是非常大的。

直接IO

直接IO就是應用程式直接訪問磁碟資料,而不經過核心緩衝區,這樣做的目的是減少一次從核心緩衝區到使用者程式快取的資料複製。比如說資料庫管理系統這類應用,它們更傾向於選擇它們自己的快取機制,因為資料庫管理系統往往比作業系統更瞭解資料庫中存放的資料,資料庫管理系統可以提供一種更加有效的快取機制來提高資料庫中資料的存取效能。

​ 直接IO的缺點:如果訪問的資料不在應用程式快取中,那麼每次資料都會直接從磁碟載入,這種直接載入會非常快取。通常直接IO與非同步IO結合使用,會得到比較好的效能。(非同步IO:當訪問資料的執行緒發出請求之後,執行緒會接著去處理其他事,而不是阻塞等待)

PIO與DMA

慢速I/O裝置和記憶體之間的資料傳輸方式(還是磁碟IO範疇)

  • 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系統呼叫時,通過DMADirect 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資源。

Zero-Copy&sendfile模式

Linux 2.1版本核心引入了sendfile函式,用於將檔案通過socket傳送。
sendfile(socket, file, len);
該函式通過一次系統呼叫完成了檔案的傳送,減少了原來 read/write方式的模式切換。此外更是減少了資料的copy,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到網絡卡上。

Zero copy是針對核心來講的,資料在核心模式下是Zerocopy,當前許多高效能http server都引入了sendfile機制,如nginx,lighttpd等。

FileChannel.transferTo(Java中的零拷貝)

Java NIO中FileChannel.transferTo(long position, long count, WriteableByteChannel target)方法將當前通道中的資料傳送到目標通道target中,在支援Zero-Copy的linux系統中,transferTo()的實現依賴於 sendfile()呼叫。

使用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的吞吐率幾乎是相同的,可見在處理小檔案請求時,傳送資料的環節在整個過程中所佔時間的比例相比於大檔案請求時要小很多,所以對於這部分的優化效果自然不十分明顯

參考文章

Zero-Copy&sendfile淺析

網路IO和磁碟IO詳解