Page Cache 的同步
廣義上Cache的同步方式有兩種,即Write Through(寫穿)
和Write back(寫回)
. 從名字上就能看出這兩種方式都是從寫操作的不同處理方式引出的概念(純讀的話就不存在Cache一致性了,不是麼)。對應到Linux的Page Cache
上所謂Write Through
就是指write(2)
操作將資料複製到Page Cache
後立即和下層進行同步的寫操作,完成下層的更新後才傳回。而Write back
正好相反,指的是寫完Page Cache
就可以傳回了。Page Cache
到下層的更新操作是非同步進行的。
Linux下Buffered IO
預設使用的是Write back
機制,即檔案操作的寫只寫到Page Cache
就傳回,之後Page Cache
到磁碟的更新操作是非同步進行的。Page Cache
中被修改的記憶體頁稱之為臟頁(Dirty Page),臟頁在特定的時候被一個叫做pdflush(Page Dirty Flush)
的核心執行緒寫入磁碟,寫入的時機和條件如下:
-
當空閑記憶體低於一個特定的閾值時,核心必須將臟頁寫回磁碟,以便釋放記憶體。
-
當臟頁在記憶體中駐留時間超過一個特定的閾值時,核心必須將超時的臟頁寫回磁碟。
-
使用者行程呼叫
sync(2)
、fsync(2)
、fdatasync(2)
系統呼叫時,核心會執行相應的寫回操作。
掃清策略由以下幾個引數決定(數值單位均為1/100秒):
# flush每隔5秒執行一次
root@082caa3dfb1d / $ sysctl vm.dirty_writeback_centisecs
vm.dirty_writeback_centisecs = 500
# 記憶體中駐留30秒以上的臟資料將由flush在下一次執行時寫入磁碟
root@082caa3dfb1d / $ sysctl vm.dirty_expire_centisecs
vm.dirty_expire_centisecs = 3000
# 若臟頁佔總物理記憶體10%以上,則觸發flush把臟資料寫回磁碟
root@082caa3dfb1d / $ sysctl vm.dirty_background_ratio
vm.dirty_background_ratio = 10
預設是寫回方式,如果想指定某個檔案是寫穿方式呢?即寫操作的可靠性壓倒效率的時候,能否做到呢?當然能,除了之前提到的fsync(2)
之類的系統呼叫外,在open(2)
開啟檔案時,傳入O_SYNC
這個flag即可實現。這裡給篇參考文章[5],不再贅述(更好的選擇是去讀TLPI相關章節)。
檔案讀寫遭遇斷電時,資料還安全嗎?相信你有自己的答案了。使用O_SYNC
或者fsync(2)
掃清檔案就能保證安全嗎?現代磁碟一般都內建了快取,程式碼層面上也只能講資料掃清到磁碟的快取了。當資料已經進入到磁碟的高速快取時斷電了會怎麼樣?這個恐怕不能一概而論了。不過可以使用hdparm -W0
命令關掉這個快取,相應的,磁碟效能必然會降低。
檔案操作與鎖
當多個行程/執行緒對同一個檔案發生寫操作的時候會發生什麼?如果寫的是檔案的同一個位置呢?這個問題討論起來有點複雜了。首先write(2)
呼叫不是原子操作,不要被TLPI的中文版5.2章節的第一句話誤導了(英文版也是有歧義的,作者在這裡給出了勘誤資訊)。當多個write(2)
操作對一個檔案的同一部分發起寫操作的時候,情況實際上和多個執行緒訪問共享的變數沒有什麼區別。按照不同的邏輯執行流,會有很多種可能的結果。也許大多數情況下符合預期,但是本質上這樣的程式碼是不可靠的。
特別的,檔案操作中有兩個操作是內核保證原子的。分別是open(2)
呼叫的O_CREAT
和O_APPEND
這兩個flag屬性。前者是檔案不存在就建立,後者是每次寫檔案時把檔案遊標移動到檔案最後追加寫(NFS等檔案系統不保證這個flag)。有意思的問題來了,以O_APPEND
方式開啟的檔案write(2)
操作是不是原子的?檔案遊標的移動和呼叫寫操作是原子的,那寫操作本身會不會發生改變呢?有的開源軟體比如apache寫日誌就是這樣寫的,這是可靠安全的嗎?坦白講我也不清楚,有人說Then O_APPEND is atomic and write-in-full for all reasonably-sized> writes to regular files.
但是我也沒有找到很權威的說法。這裡給出一個郵件串列上的討論,可以參考下[6]。今天先放過去,後面有時間的話專門研究下這個問題。如果你能給出很明確的說法和證明,還望不吝賜教。
Linux下的檔案鎖有兩種,分別是flock(2)
的方式和fcntl(2)
的方式,前者源於BSD,後者源於System V,各有限制和應用場景。老規矩,TLPI上講的很清楚的這裡不贅述。我個人是沒有用過檔案鎖的,系統設計的時候一般會避免多個執行流寫一個檔案的情況,或者在程式碼邏輯上以mutex加鎖,而不是直接加鎖檔案本身。資料庫場景下這樣的操作可能會多一些(這個純屬臆測),這就不是我瞭解的範疇了。
磁碟的效能測試
在具體的機器上跑服務程式,如果涉及大量IO的話,首先要對機器本身的磁碟效能有明確的瞭解,包括不限於IOPS、IO Depth等等。這些資料不僅能指導系統設計,也能幫助資源規劃以及定位系統瓶頸。比如我們知道機械磁碟的連續讀寫效能一般不會超過120M/s,而普通的SSD磁碟隨意就能超過機械盤幾倍(商用SSD的連續讀寫速率達到2G+/s不是什麼新鮮事)。另外由於磁碟的工作原理不同,機械磁碟需要旋轉來尋找資料存放的磁軌,所以其隨機存取的效率受到了“尋道時間”的嚴重影響,遠遠小於連續存取的效率;而SSD磁碟讀寫任意扇區可以認為是相同的時間,隨機存取的效能遠遠超過機械盤。所以呢,在機械磁碟作為底層儲存時,如果一個執行緒寫檔案很慢的話,多個執行緒分別去寫這個檔案的各個部分能否加速呢?不見得吧?如果這個檔案很大,各個部分的尋道時間帶來極大的時間消耗的話,效率就很低了(先不考慮Page Cache
)。SSD呢?可以明確,設計合理的話,SSD多執行緒讀寫檔案的效率會高於單執行緒。當前的SSD盤很多都以高併發的讀取為賣點的,一個執行緒壓根就喂不飽一塊SSD盤。一般SSD的IO Depth都在32甚至更高,使用32或者64個執行緒才能跑滿一個SSD磁碟的頻寬(同步IO情況下)。
具體的SSD原理不在本文計劃內,這裡給出一篇詳細的參考文章[7]。有時候一些文章中所謂的STAT磁碟一般說的就是機械盤(雖然STAT本身只是一個匯流排介面)。介面會影響儲存裝置的最大速率,基本上是STAT -> PCI-E -> NVMe
的發展路徑,具體請自行Google瞭解。
具體的裝置一般使用fio
工具[8]來測試相關磁碟的讀寫效能。fio的介紹和使用教程有很多[9],不再贅述。這裡不想貼效能資料的原因是儲存介質的發展實在太快了,一方面不想貼某些很快就過時的資料以免讓初學者留下不恰當的第一印象,另一方面也希望讀寫自己實踐下fio命令。
前文提到儲存介質的原理會影響程式設計,我想稍微的解釋下。這裡說的“影響”不是說具體的讀寫能到某個速率,程式中就依賴這個數值,換個工作環境就效能大幅度降低(當然,為專門的機型做過最佳化的結果很可能有這個副作用)。而是說根據儲存介質的特性,程式的設計起碼要遵循某個設計套路。舉個簡單的例子,SATA機械盤的隨機存取很慢,那系統設計時,就要盡可能的避免隨機的IO出現,盡可能的轉換成連續的檔案存取來加速執行。比如Google的LevelDB就是轉換隨機的Key-Value寫入為Binlog(連續檔案寫入)+ 記憶體插入MemTable(記憶體隨機讀寫可以認為是O(1)的效能),之後批次dump到磁碟(連續檔案寫入)。這種LSM-Tree
的設計便是合理的利用了儲存介質的特性,做到了最大化的效能利用(磁碟換成SSD也依舊能有很好的執行效率)。
寫在最後
每天抽出不到半個小時,零零散散地寫了一週,這是說是入門都有些謬贊了,只算是對Linux下的IO機制稍微深入的介紹了一點。無論如何,希望學習完Linux系統程式設計的同學,能繼續的往下走一走,嘗試理解系統呼叫背後隱含的機制和原理。探索的結果無所謂,重要的是探索的過程以及相關的學習經驗和方法。前文提出的幾個問題我並沒有刻意去解答所有的,但是讀到現在,不知道你自己能回答上幾個了?
參考文獻
[1] 圖片引自《Computer Systems: A Programmer’s Perspective》Chapter 6 The Memory Hierarchy, 另可參考 https://zh.wikipedia.org/wiki/%E5%AD%98%E5%82%A8%E5%99%A8%E5%B1%B1
[2] Locality of reference,https://en.wikipedia.org/wiki/Locality_of_reference
[3] 圖片引自《The Linux Programming Interface》Chapter 13 FILE I/O BUFFERING
[4] Linux Storage Stack Diagram, https://www.thomas-krenn.com/en/wiki/Linux_Storage_Stack_Diagram
[5] O_DIRECT和O_SYNC詳解, http://www.cnblogs.com/suzhou/p/5381738.html
[6] http://librelist.com/browser/usp.ruby/2013/6/5/o-append-atomicity/
[7] Coding for SSD, https://dirtysalt.github.io/coding-for-ssd.html
[8] fio作者Jens Axboe是Linux核心IO部分的maintainer,工具主頁 http://freecode.com/projects/fio/
[9] How to benchmark disk, https://www.binarylane.com.au/support/solutions/articles/1000055889-how-to-benchmark-disk-i-o
[10] 深入Linux核心架構, (德)莫爾勒, 人民郵電出版社
(已完結)