歡迎光臨
每天分享高質量文章

聊聊Linux IO(下)

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_CREATO_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內核架構, (德)莫爾勒, 人民郵電出版社

(已完結)

赞(0)

分享創造快樂