上一篇文章咱們講了一下硬盤(磁盤 & SSD)在硬件上的一些限制,總結了兩個優化硬盤 I/O 的方向。本篇咱們就從 Linux 軟件開發的角度,講一下在軟件設計中咱們應該如何提升硬盤 I/O。html
本文地址:http://www.javashuo.com/article/p-xyjmvbnr-hp.htmlnode
這裏,咱們會涉及一個新的 「緩存」 概念。注意,這裏的 「緩存」 和前文所說起的存儲架構中的 「cache」 雖然中英文用詞都同樣,但二者是不一樣的。
本文所說的緩存,指的是在 Linux 操做系統層面,在應用程序對硬盤進行讀寫(read
/ write
系統調用)時,對硬盤資源所作的一個預加載 / 延寫入的機制。linux
多年之前有一次面試,我被問了一個問題:
—— 「你說一說,咱們調用write()
以後,Linux 是怎麼調用到底層的?」面試
我一臉懵逼,第一反應是這個問題太泛了,若是把我所知道的全部東西說出來的話,從頂層軟件過程到底層硬件驅動編寫,我能夠講一下午(參見個人工做經歷)。編程
我只好再問了一句:「這個範疇有點大,請問您能不能具體地問一下呢?」
估計面試官也沒想到我會反問,他只是重複了一下:「你就……把這個過程說一下吧。」
因而乎,我就從系統調用的實現原理講起,然而很快就被面試官打斷:「好吧能夠了,你回去等消息吧。」segmentfault
消息確定是沒等到,而我至今也沒把握面試官但願聽到的答案是什麼。此次說到硬盤 I/O 的時候我突然想到:或許面試官要的是這個吧? ————緩存
在現代操做系統中,一個 「真正的」 文件,當調用 read
/ write
的時候,數據固然不會簡單地就直達硬盤。對於 Linux 而言,這個過程的一部分是這樣的:服務器
在操做系統內核空間內,read / write 到硬件設備之間,按順序有這麼幾層:架構
read
/ write
/ ioctl
之類的系統調用就在這一層。當調用 open
以後,內核會爲每個 file descriptor 建立一個 file_operations
結構體實例。這個結構體裏包含了 open、write、seek 等的實例(回調函數)。這一層實際上是 Linux 文件和設備體系的精華之一,不少東西都隱藏或暴露在這一層。不過本文不研究這一塊/proc
文件等等這裏我以爲 IBM 的資料講的特別清楚。下面是重點摘抄:異步
當應用程序須要讀取文件中的數據時,操做系統先分配一些內存,將數據從存儲設備讀入到這些內存中,而後再將數據分發給應用程序;當須要往文件中寫數據時,操做系統先分配內存接收用戶數據,而後再將數據從內存寫到磁盤上。
對於每一個文件的第一個讀請求,系統讀入所請求的頁面並讀入緊隨其後的少數幾個頁面(很多於一個頁面,一般是三個頁面),這時的預讀稱爲同步預讀。
若是應用程序接下來是順序讀取的話,那麼文件 cache 命中,OS 會加大同步預讀的範圍,加強緩存效率,此時的預讀被稱爲異步預讀
若是接下來 cache 沒命中,那麼 OS 會繼續使用同步預讀。
知道了原理以後,接下來就是怎麼作的問題了——
從緩存的工做機制來看,很簡單,若是要充分利用 Linux 的文件緩存機制,那麼最好的方法就是:每個文件都儘量地採用順序讀寫,避免大量的 seek
調用。
其實這一條和我前一篇文章中從硬盤工做原理的角度出發提出的兩個思路是一致的。好了,如今咱們能夠搬出具體的 coding 思路了。這總結起來比講原理短多了。如下就把幾個設計思路講一下吧:
從文件緩存角度,若是頻繁地隨機讀取一個文件不一樣的位置,極可能致使緩存命中率降低。那麼 OS 就不得不頻繁地往硬盤上預讀,進一步致使硬盤利用率低下。因此在讀寫文件的時候,儘量的只是簡單寫入或者簡單讀取文件,而不要使用 seek
。
這條原則很是適用於 log 文件的寫入:當寫入 log 的時候,寫就行了,不要常常翻回去查看之前的內容。
整個系統,最好只有一個進程進行磁盤的讀寫。而不是多個進程進行文件存取。這個思路,一方面和上一條 「順序寫」 原則的理由實際上是一致的。當多個進程進行磁盤讀寫的時候,隨機度瞬間飆升。特別是多個進程操做多個文件的時候,磁盤的磁頭極可能須要頻繁大範圍地移動。
若是確實有必要多個進程分別讀取多個不一樣文件的話,能夠考慮下面的替代方案:
若是是多個進程同時寫入一個文件(好比 log),那就更好辦了。這種狀況下,能夠在這幾個進程和文件中間加入一個內部文件服務器,將全部進程的存取文件需求彙總到該文件服務器中進行統一處理。
ProcessA ProcessB ProcessC | | | | V | *----> The File <---*
改成
ProcessA ProcessB ProcessC | | | | V | *----> ProcessD <---* | V The File
順便還能夠在這個服務進程中實現一些本身的緩存機制,配合 Linux 自身的文件緩存進一步優化磁盤 I/O 效率。
這裏能夠看看下面這個僞代碼:
const int WRITE_BLOCK_SIZE = 4096 for (int i = 0 to 999) { write(fd, buff, WRITE_BLOCK_SIZE) }
其實這個問題,就是我在上一篇文章的 "硬盤文件存取速度的考量" 小節中所說的內容了。
這裏有一個常量 WRITE_BLOCK_SIZE
, 這並非能夠隨意取的值,比較合適的是 4096 或者其倍數,理由是文件系統每每以 4kB 爲頁,若是沒有寫夠 4kB 的話,將致使文件須要多餘的讀出動做。雖然文件緩存在必定程度上可以幫你緩解,但總會有一部分操做會最落地到底層 I/O 的。因此實際操做中,要儘可能以 4kB 爲邊界操做大文件。
有一個問題被提了出來:咱們都知道,當咱們面對一個大目錄(目錄中有不少不少文件)的時候,這個目錄刷出來須要很長的時間。那麼咱們在開發的時候是否是要避免常常在這個大目錄中讀寫文件呢?
實際上,當你第一次操做這個大目錄的時候,可能延時確實會比較大。可是實測只要進入了這個目錄以後,再後續操做的時候,卻一點都不慢,和其餘的普通目錄至關。
這個問題的緣由,我我的猜想(求權威人士指正)是這樣的:
目錄在文件系統中,是以一個 inode 的方式存在的,那麼載入目錄,實際上就是載入這個 inode。從存儲的角度,inode 也只是一個普通的文件,那麼載入 inode 的動做和載入其餘文件同樣,也會通過文件緩存策略。載入了一次以後,只要你持續地訪問它,那麼操做系統就會將這個 inode 保持在緩存中。所以後續的操做,就是直接讀寫 RAM 了,並不會受到硬盤 I/O 瓶頸的影響。
Linux 內核的文件 Cache 管理機制介紹
磁盤I/O那些事
Linux系統結構 詳解
【Linux編程基礎】文件與目錄--知識總結