對linux核心來說,讀寫要經過層層路徑,才能真正讀寫硬碟。從io路徑來說,io要經過page cache,io排程隊列,dispatch隊列,ncq隊列和硬碟cache,才能真正到達硬碟。
Page cache:page cache是linux核心提供的緩存接口,page cache的名字就說明核心是通過page單元(通常4K大小)來管理cache。讀操作首先在page cache查找,如果找到,就複制page cache的内容傳回,找不到,才真正調用下層處理。寫操作,buffer io 寫到page cache就傳回,真正的磁盤寫,是由核心的pdflush核心線程負責
IO排程隊列:
Linux核心提供了四種io排程算法,as,deadline,cfq,noop。每種排程算法都實作了一個排程隊列,io首先在隊列中排序(noop最簡單,不排序),然後根據條件,決定是否到dispatch隊列。從排程隊列下發,涉及一個unplug的概念。也就是說,排程隊列通常處于阻塞(plug)狀态,當執行unplug操作時,io離開排程隊列,開始下發。unplug是個循環動作,将排程隊列的所有io都嘗試下發,直到不能下發為止。
總結一下,執行unplug有下列條件:
第一個io啟動了三毫秒的定時器,定時器到了,會unplug,開始下發
io請求超過設定的限制(預設是4),執行unplug,開始下發
Sync标志的io,立即unplug,開始下發。
Barrier标志的io,清空排程隊列後,執行unplug,開始下發
一個io執行完畢,也要unplug隊列。
dispatch隊列:dispatch隊列對應用關系不大。但是核心層對日志檔案系統的joural資料,提供了一種barrier io,這個主要在dispatch隊列實作。
Ncq隊列:
NCQ是sata硬碟自身的隊列。(sas硬碟的隊列叫TCQ)。NCQ隊列是由作業系統建立的,但是加入到NCQ隊列的io,是由硬碟來決定執行順序。為了實作這個,NCQ隊列建立在核心的DMA記憶體中,然後通知硬碟,至于硬碟選擇那個io執行,是硬碟自身選擇的結果。
硬碟cache:
硬碟cache是硬碟内部的cache。如果打開硬碟cache的話,寫硬碟的io,首先是到硬碟cache,而非直接落到硬碟。
Pdflush提供了四個參數來控制回寫。在核心實作中,pdflush的回寫政策控制還比較複雜。
但是簡單一點說,核心預設情況下,每5秒鐘掃描髒頁,如果髒頁生存時間超過30秒(預設數值),就刷髒頁到磁盤。
詳細的可參考本人寫的《 linux核心回寫機制和調整 》一文。
從上文的分析,通常的io寫,到page cache層就結束傳回了,并沒真正寫到硬碟。這樣機器掉電或者故障的時候,就有丢失資料的風險。為了盡快下io,系統又提供了一些措施解決這個問題。
O_SYNC:打開檔案的時候,可以設定O_SYNC标志,在page cache的寫完成後,如果檔案有O_SYNC标志,立即開始将io下發,進入排程隊列。随後将檔案系統的meta data資料也下發,然後開始循環執行unplug操作,直到所有的寫io完成。和回寫機制比較,O_SYNC沒有等髒頁生存30秒,就嘗試立即下發到硬碟。
O_SYNC本質就是io下發,然後執行unplug操作。O_SYNC的幾個問題是:
寫page cache時候要把io拆成4k的單元。回寫也是每次寫4K的頁面,如果是大io,就需要核心的排程層把4k的io重新再合并起來。這是備援的過程
每個io都要立即unplug,這樣就不能實作io的排序和合并。O_SYNC的性能相當低。
如果多個程序并發寫,不能保證寫操作的順序。Ncq隊列根據硬碟磁頭的位置和磁盤旋轉位置确定執行的順序。一般是meta data資料一起寫,這樣存在不同步的風險。
如果硬碟cache打開了,那麼寫隻到硬碟cache就傳回了。存在丢資料的風險。通常存儲廠商都要求硬碟cache關閉。不過騰訊的伺服器都是打開硬碟cache的。
O_DIRECT:打開檔案的時候,可設定O_DIRECT标志。O_DIRECT不使用核心提供的page cache。這樣讀操作,就不會到page cache中檢查是否有需要資料存在。而寫操作,也不會将資料寫入page cache,而是送入排程隊列。
O_DIRECT執行寫io的時候,會置WRITE_SYNC标志。這個标志在io進入排程隊列後,會執行一次unplug操作。而不是像O_SYNC那樣,循環執行unplug操作。
為了避免O_SYNC每個寫io都要阻塞等待的問題,系統提供了fsync和fdatasync系統調用,可以讓應用層自己控制同步的時機。
Fsync:fsync将檔案範圍内,所有的髒頁面都下發到硬碟。然後也要将髒的中繼資料寫到 硬碟。如果檔案的inode本身有變化,同樣需要寫到硬碟。
Fdatasync:fdatasync和fsync的差別其實很輕微。比如ext2檔案系統,如果檔案的inode隻有輕微的變化,fdatasync此時不更新inode。典型的輕微變化是檔案atime的變化。而在ext3檔案系統,fsync和fdatasync是完全一樣的。不管是否輕微變化,都要回寫inode。
Fsync和fdatasync都是對整個檔案的操作,如果應用隻想重新整理檔案的指定位置,這兩個系統調用就失效了。是以新的核心還提供了sync_file_range來指定範圍寫。不過要注意,sync_file_range是不回寫檔案的meta data。必須應用層保證meta data沒有更新。
詳細的可參考本人寫的《 Linux核心回寫機制和調整 》一文。
從上文的分析看出,核心沒有為使用者态提供保證順序的,确定寫到硬碟的系統調用。但是對于核心檔案系統來說,必須提供這樣的接口。比如日志檔案系統,必須要資料落到硬碟後,才能修改中繼資料的日志。否則,出錯情況下就可能造成檔案系統崩潰。為此,核心專門提供了一個barrier方式實作日志的準确寫到硬碟。
檔案系統的barrier io,意味着,這個barrier io之前的寫io必須完成。同時,在barrier io完成之前(是真正寫到硬碟,不是寫到cache就傳回),也不能有别的寫io再執行。為此,上文分析的dispatch 隊列完成了這個功能。
當寫io從排程隊列進入dispatch隊列的時候,要檢查是否是一個barrier io。如果是barrier io,dispatch首先在隊列中插入一個SCSI指令SYNCHRONIZE_CACHE,這個指令訓示硬碟cache刷所有的寫io到硬碟。然後再下發barrier io,之後再插入一個SYNCHRONIZE_CACHE指令,訓示硬碟将剛才的barrier io真正寫到硬碟(還有一種方式,是通過指令攜帶FUA标志實作不經過cache直接下盤)。
對于單一的存儲系統來說,資料一緻性,性能和可靠性是幾個沖突的名額。标準的linux核心在這方面也有些左右為難。比如核心在io失敗的情況下,一般會重試(核心設定了5次的重試)。這樣重試時間可能超過1分鐘,這對網際網路系統的業務來說是不可承受的。如何找到合适點,平衡幾個名額的關系,從作業系統最底層提升産品的可靠性和性能,是一項長期的任務。