Linux 磁盤I/O的三種方式對比:標準I/O、直接 I/O、mmap

1.引入

讓我們先看一下Linux系統下IO結構模型

從圖中可以看到標準 I/O、mmap、直接 I/O 這三種 I/O 方式在流程上的差異

1.1 標準I/O

大多數文件系統的默認I/O操作都是標準I/O。在Linux的緩存I/O機制中,數據先從磁盤複製到內核空間的緩衝區,然後從內核空間緩衝區複製到應用程序的地址空間。
讀操作:操作系統檢查內核的緩衝區有沒有需要的數據,如果已經緩存了,那麼就直接從緩存中返回;否則從磁盤中讀取,然後緩存在操作系統的緩存中。
寫操作:將數據從用戶空間複製到內核空間的緩存中。這時對用戶程序來說寫操作就已經完成,至於什麼時候再寫到磁盤中由操作系統決定,除非顯示地調用了sync等同步命令。
緩存I/O的優點:1)在一定程度上分離了內核空間和用戶空間,保護系統本身的運行安全;2)可以減少讀盤的次數,從而提高性能。
緩存I/O的缺點:數據在傳輸過程中需要在應用程序地址空間和緩存之間進行多次數據拷貝操作,這些數據拷貝操作所帶來的CPU以及內存開銷是非常大的。

下面我們read()操作爲例,下圖是read操作時流程

在這裏插入圖片描述

它會導致數據先從磁盤拷貝到 Page Cache 中,然後再從 Page Cache 拷貝到應用程序的用戶空間,這樣就會多一次內存拷貝。系統這樣設計主要是因爲內存相對磁盤是高速設備,即使多拷貝 100 次,內存也比真正讀一次硬盤要快。

寫操作

前面提到寫操作講數據從用戶控件複製到內核空間的緩存中,數據什麼時候寫到磁盤由應用程序採用的寫操作機制決定,默認是採用延遲寫機制,應用程序只需要將數據寫到頁緩存就可以了,完全不需要等待數據全部被寫入磁盤,系統會負責定期將頁緩存數據寫入磁盤。

從中可以看出來,緩存 I/O 可以很大程度減少真正讀寫磁盤的次數,從而提升性能。但是延遲寫機制可能會導致數據丟失,那系統究竟會在什麼時機真正把頁緩存的數據寫入磁盤呢?

Page Cache 中被修改的內存稱爲“髒頁”,內核通過 flush 線程定期將數據寫入磁盤。具體寫入的條件我們可以通過 /proc/sys/vm 文件或者sysctl -a | grep vm 命令得到。

// 
flush 每隔 5 秒執行一次
vm.dirty_writeback_centisecs = 500
// 內存中駐留 30 秒以上的髒數據將由flush 在下一次執行時寫入磁盤
vm.dirty_expire_centisecs = 3000
// 指示若髒頁佔總物理內存 10%以上,則觸發 flush 把髒數據寫回磁盤
vm.dirty_background_ratio = 10
// 系統所能擁有的最大髒頁緩存的總大小
vm.dirty_ratio = 20

在實際應用中,如果某些數據我們覺得非常重要,是完全不允許有丟失風險的,這個時候我們應該採用同步寫機制。在應用程序中使用 sync、fsync、msync 等系統調用時,內核都會立刻將相應的數據寫回到磁盤。

1.2直接I/O

直接IO就是應用程序直接訪問磁盤數據,而不經過內核緩衝區,這樣做的目的是減少一次從內核緩衝區到用戶程序緩存的數據複製。比如說數據庫管理系統這類應用,它們更傾向於選擇它們自己的緩存機制,因爲數據庫管理系統往往比操作系統更瞭解數據庫中存放的數據,數據庫管理系統可以提供一種更加有效的緩存機制來提高數據庫中數據的存取性能。
直接IO的缺點:如果訪問的數據不在應用程序緩存中,那麼每次數據都會直接從磁盤加載,這種直接加載會非常耗時。通常直接IO與異步IO結合使用,會得到比較好的性能。(異步IO:當訪問數據的線程發出請求之後,線程會接着去處理其他事,而不是阻塞等待)

直接I/O流程
在這裏插入圖片描述

從圖中你可以看到,直接 I/O 訪問文件方式減少了一次數據拷貝和一些系統調用的耗時,很大程度降低了 CPU 的使用率以及內存的佔用。
但是直接與磁盤交互非常耗時,所以只有確定標準I/O開銷非常巨大才考慮使用直接I/O。

1.3 mmap

mmap是指將硬盤上文件的位置與進程邏輯地址空間中一塊大小相同的區域一一對應,當要訪問內存中一段數據時,轉換爲訪問文件的某一段數據。這種方式的目的同樣是減少數據在用戶空間和內核空間之間的拷貝操作。當大量數據需要傳輸的時候,採用內存映射方式去訪問文件會獲得比較好的效率。
使用內存映射文件處理存儲於磁盤上的文件時,將不必再對文件執行I/O操作,這意味着在對文件進行處理時將不必再爲文件申請並分配緩存,所有的文件緩存操作均由系統直接管理,由於取消了將文件數據加載到內存、數據從內存到文件的回寫以及釋放內存塊等步驟,使得內存映射文件在處理大數據量的文件時能起到相當重要的作用。

mmap的優點:

  1. 減少系統調用。我們只需要一次 mmap() 系統調用,後續所有的調用像操作內存一樣,而不會出現大量的 read/write 系統調用。

  2. 減少數據拷貝。普通的 read() 調用,數據需要經過兩次拷貝;而 mmap 只需要從磁盤拷貝一次就可以了,並且由於做過內存映射,也不需要再拷貝回用戶空間。

  3. 可靠性高。mmap 把數據寫入頁緩存後,跟緩存 I/O 的延遲寫機制一樣,可以依靠內核線程定期寫回磁盤。但是需要提的是,mmap 在內核崩潰、突然斷電的情況下也一樣有可能引起內容丟失,當然我們也可以使用 msync來強制同步寫。

在這裏插入圖片描述

從上面的圖看來,我們使用 mmap 僅僅只需要一次數據拷貝。看起來 mmap 的確可以秒殺普通的文件讀寫,那我們爲什麼不全都使用 mmap 呢?事實上,它也存在一些缺點:

  1. 虛擬內存增大。mmap 會導致虛擬內存增大,我們的 APK、Dex、so 都是通過 mmap 讀取。而目前大部分的應用還沒支持 64 位,除去內核使用的地址空間,一般我們可以使用的虛擬內存空間只有 3GB 左右。如果 mmap 一個 1GB 的文件,應用很容易會出現虛擬內存不足所導致的 OOM。
  2. 磁盤延遲。mmap 通過缺頁中斷向磁盤發起真正的磁盤 I/O,所以如果我們當前的問題是在於磁盤 I/O 的高延遲,那麼用 mmap() 消除小小的系統調用開銷是杯水車薪的。

在 Android 中可以將文件通過MemoryFile或者MappedByteBuffer映射到內存,然後進行讀寫,使用這種方式對於小文件和頻繁讀寫操作的文件還是有一定優勢的。我通過簡單代碼測試,測試結果如下。

在這裏插入圖片描述

從上面的數據看起來 mmap 好像的確跟寫內存的性能差不多,但是這並不正確,因爲我們並沒有計算文件系統異步落盤的耗時。在低端機或者系統資源嚴重不足的時候,mmap 也一樣會出現頻繁寫入磁盤,這個時候性能就會出現快速下降。

mmap 比較適合於對同一塊區域頻繁讀寫的情況,推薦也使用線程來操作。用戶日誌、數據上報都滿足這種場景,另外需要跨進程同步的時候,mmap 也是一個不錯的選擇。Android 跨進程通信有自己獨有的 Binder 機制,它內部也是使用 mmap 實現。

在這裏插入圖片描述

利用 mmap,Binder 在跨進程通信只需要一次數據拷貝,比傳統的 Socket、管道等跨進程通信方式會少一次數據拷貝。

在這裏插入圖片描述

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