磁盤及網絡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

Zero-Copy&sendfile淺析

原文地址:https://segmentfault.com/a/1190000007692223


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章