雲(儲存),鬆(耦合)
正如我們所瞭解的,核心不斷用包含塊裝置資料的頁填充頁快取記憶體。只要程序修改了資料,相應的頁就被標記為髒頁,即把它的PG_dirty標誌置位。
Unix系統允許把髒緩衝區寫入塊裝置的操作延遲執行,因為這種策略可以顯著地提高系統的效能。對快取記憶體中的頁的幾次寫操作可能只需對相應的磁碟塊進行一次緩慢的物理更新就可以滿足。此外,寫操作沒有讀操作那麼緊迫,因為程序通常是不會由於延遲寫而掛起,而大部分情況都因為延遲讀而掛起。正是由於延遲寫,使得任一物理塊裝置平均為讀請求提供的服務將多於寫請求。
一個髒頁可能直到最後一刻(即直到系統關閉時)都一直逗留在主存中。然而,從延遲寫策略的侷限性來看,它有兩個主要的缺點:
(1)如果發生了硬體錯誤或電源掉電的情況,那麼就無法再獲得RAM的內容,因此從系統啟動以來對檔案進行的很多修改就丟失了。
(2)頁快取記憶體的大小(由此存放它所需的RAM的大小)就可能要很大——至少要與所訪問塊裝置的大小相同。
因此,在下列條件下把髒頁重新整理(寫入)到磁碟:
(1)頁快取記憶體變得太滿,但還需要更多的頁,或者髒頁的數量已經太多。
(2)自從頁變成髒頁以來已過去太長時間。
(3)程序請求對塊裝置或者特定檔案任何待定的變化都進行重新整理。通過呼叫sync()、fsync()或fdatasync()系統呼叫來實現。
緩衝區頁的引入使問題更加複雜。與每個緩衝區頁相關的緩衝區首部使核心能夠了解每個獨立塊緩衝區的狀態。如果至少有一個緩衝區首部的BH_Dirty標誌被置位,就應該設定相應緩衝區頁的PG_dirty標誌。當核心選擇要重新整理的緩衝區頁時,它掃描相應的緩衝區首部,並只把髒塊的內容有效地寫到磁碟。一旦核心把緩衝區的所有髒頁重新整理到磁碟,就把頁的PG_dirty標記清0。
1 pdflush核心執行緒
早期版本的Linux使用bdflush核心執行緒系統地掃描頁快取記憶體以搜尋要重新整理的髒頁,並且使用另一個核心執行緒kupdate來保證所有的頁不會“髒”太長的時間。Linux 2.6用一組通用核心執行緒pdflush代替上述兩個執行緒。
這些核心執行緒結構靈活,它們作用於兩個引數:一個指向執行緒要執行的函式的指標和一個函式要用的引數。系統中pdflush核心執行緒的數量是要動態調整的:pdflush執行緒太少時就建立,太多時就殺死。因為這些核心執行緒所執行的函式可以阻塞,所以建立多個而不是一個pdflush核心執行緒可以改善系統性能。
根據下面的原則控制pdflush執行緒的產生和消亡:
- 必須有至少兩個,最多八個pdflush核心執行緒。
- 如果到最近的1s期間沒有空閒pdflush,就應該建立新的pdflush。
- 如果最近一次pdflush變為空閒的時間超過了1s,就應該刪除一個pdflush。
所有的pdflush核心執行緒都有pdflush_work描述符。空閒pdflush核心執行緒的描述符都集中在pdflush_list連結串列中;在多處理器系統中,pdflush_lock自旋鎖保護該連結串列不會被併發訪問。nr_pdflush_threads變數(可以從檔案/proc/sys/vm/nr_pdflush_threads中讀出這個變童的值)存放pdflush核心執行緒(空閒的或忙的)的總數。最後,last_empty_jifs變數存放pdflush執行緒的pdflush_list連結串列變為空的時間(以jiffies表示)。
型別 |
欄位 |
說明 |
struct task_struct * |
who |
指向核心執行緒描述符的指標 |
void(*)(unsigned long) |
fn |
核心執行緒所執行的回撥函式 |
unsigned long |
arg0 |
給回撥函式的引數 |
struct list head |
list |
pdflush_list連結串列的連結 |
unsigned long |
when_i_went_to_sleep |
當核心執行緒可用時的時間(以jiffies表示) |
所有pdflush核心執行緒都執行函式__pdflush,它本質上迴圈執行一直到核心執行緒死亡。我們不妨假設pdflush核心執行緒是空閒的,而程序正在TASK_INTERRUPTIBLE狀態睡眠。一但核心執行緒被喚醒,__pdflush()就訪問其pdflush_work描述符,並執行欄位fn中的回撥函式,把arg0欄位中的引數傳遞給該函式。當回撥函式結束時__pdflush()檢查last_empty_jifs變數的值:如果不存在空閒pdflush核心執行緒的時間已經超過1s,而且pdflush核心執行緒的數量不到8個,函式__pdflush()就建立另外一個核心執行緒。相反,如果pdflush_list連結串列中的最後一項對應的pdflush核心執行緒空閒時間超過了1s,而且系統中有兩個以上的pdflush核心執行緒,函式__pdflush()就終止:就像在“核心執行緒”博文中所描述的,相應的核心執行緒執行_exit()系統呼叫,並因此而被撤消。否則,如果系統中pdflush核心執行緒不多於兩個,__pdflush()就把核心執行緒的pdflush_work描述符重新插入到pdflush_list連結串列中,並使核心執行緒睡眠。
pdflush_operation()函式用來啟用空閒的pdflush核心執行緒。該函式作用於兩個參引數:一個指標fn,指向必須執行的函式;以及引數argO。函式執行下面的步驟:
1. 從pdflush_list連結串列中獲取pdf指標,它指向空閒pdflush核心執行緒的pdflush_work描述符。如果連結串列為空,就返回-1。如果連結串列中僅剩一個元素,就把jiffies的值賦給變數last_empty_jifs。
2. 把引數fn和arg0分別賦給pdf->fn和pdf->arg0。
3. 呼叫wake_up_process()喚醒空閒的pdflush核心執行緒,即pdf->who。
把哪些工作委託給Pdflush核心執行緒來完成呢?其中一些工作與髒資料的重新整理相關。尤其是,pdflush通常執行下面的回撥函式之一:
- background_writeout():系統地掃描頁快取記憶體以搜尋要重新整理的髒頁(參見下節“搜尋要重新整理的髒頁”)。
- wb_kupdate():檢查頁快取記憶體中是否有“髒”了很長時間的頁(參見稍後“回寫陳舊的髒頁”)。
2 搜尋要重新整理的髒頁
所有的基樹都可能有要重新整理的髒頁。為了得到所有這些頁,就要徹底搜尋與在磁碟上有映像的索引節點相應的所有address_space物件。由於頁快取記憶體可能有大量的頁,如果用一個單獨的執行流來掃描整個快取記憶體,會令CPU和磁碟長時間繁忙。因此,Linux使用一種複雜的機制把對頁快取記憶體的掃描劃分為幾個執行流。
wakeup_bdflush()函式接收頁快取記憶體中應該重新整理的髒頁數量作為引數;0值表示快取記憶體中的所有髒頁都應該寫回磁碟。該函式呼叫pdflush_operation()喚醒pdflush核心執行緒(參見上一節),並委託它執行回撥函式background_writeout(),後者有效地從頁快取記憶體獲得指定數量的髒頁,並把它們寫回磁碟。
當記憶體不足或使用者顯式地請求重新整理操作時執行wakeup_bdflush()函式。特別是在下述情況下會呼叫該函式:
- 使用者態程序發出sync()系統呼叫
- grow_buffers()函式分配一個新緩衝區頁時失敗
- 頁框回收演算法呼叫free_more_memory()或try_to_free_pages()
- menmpool_alloc()函式分配一個新的記憶體池元素時失敗
此外,執行background_writeout()回撥函式的pdflush核心執行緒是由滿足以下兩個條件的程序喚醒的:一是對頁快取記憶體中的頁內容進行了修改,二是引起髒頁部分增加到超過某個髒背景閾值 (background threshold)。背景閾值通常設定為系統中所有頁的10%,不過可以通過修改檔案/proc/sys/vm/dirty_background_ratio來調整這個值。
background_writeout()函式依賴於作為雙向通訊裝置的writeback_control結構:一方面,它告訴輔助函式writeback_modes()要做什麼;另一方面,它儲存寫回磁碟的頁的數量的統計值。下面是這個結構最重要的欄位:
sync_mode:表示同步模式:WB_SYNC_ALL表示如果遇到一個上鎖的索引節點,必須等待而不能略過它;WB_SYNC_HOLD表示把上鎖的索引節點放入稍後涉及的連結串列中;WB_SYNC_NONE表示簡單地略過上鎖的索引節點。
bdi:如果不為空,就指向backing_dev_info結構。此時,只有屬於基本塊裝置的髒頁將會被重新整理。
older_than_this:如果不為空,就表示應該略過比指定值還新的索引節點。
nr_to_write:當前執行流中仍然要寫的髒頁的數量。
nonblocking:如果這個標誌被置位,就不能阻塞程序。
background_writeout()函式只作用於一個引數nr_pages,表示應該重新整理到磁碟的最少頁數。它本質上執行下述步驟:
1. 從每CPU變數page_state中讀當前頁快取記憶體中頁和髒頁的數量。如果髒頁所佔的比例低於給定的閾值,而且已經至少有nr_pages頁被重新整理到磁碟,該函式就終止。這個閾值通常大約是系統中總頁數的40%,可以通過寫檔案/proc/sys/vm/dirty_ratio來調整這個值。
2. 呼叫writeback_inodes()嘗試寫1024個髒頁(見下面)。
3. 檢查有效寫過的頁的數量,並減少需要寫的頁的個數。
4. 如果已經寫過的頁少於1024頁,或略過了一些頁,則可能塊裝置的請求佇列處於擁塞狀態:此時,background_writeout()函式使當前程序在特定的等待佇列上睡眠100ms,或使當前程序睡眠到佇列變得不擁塞。
5. 返回到第1步。
writeback_inodes()函式只作用於一個引數,就是指標wbc,它指向writeback_control描述符。該描述符的nr_to_write欄位存有要重新整理到磁碟的頁數。函式返回時,該欄位存有要重新整理到磁碟的剩餘頁數,如果一切順利,則該欄位的值被賦為0。
我們假設writeback_inodes()函式被呼叫的條件為:指標wbc->bdi和wbc->older_than_this被置為NULL,WB_SYNC_NONE同步模式和wbc->nonblocking標誌置位(這些值都由background_writeout()函式設定)。函式writeback_inodes()掃描在super_blocks變數中建立的超級塊連結串列。當遍歷完整個連結串列或重新整理的頁數達到預期數量時,就停止掃描。對每個超級塊sb,函式執行下述步驟:
1. 檢查sb->s_dirty或sb->s_io連結串列是否為空:第一個連結串列集中了超級塊的髒索引節點,而第二個連結串列集中了等待被傳輸到磁碟的索引節點(見下面)。如果兩個連結串列都為空,說明相應檔案系統的索引節點沒有髒頁,因此函式處理連結串列中的下一個超級塊。
2. 此時,超級塊有髒索引節點。對超級塊sb呼叫sync_sb_inodes(),該函式執行下面的操作:
a) 把sb->s_dirty的所有索引節點插入sb->s_io指向的連結串列,並清空髒索引節點連結串列。
b) 從sb->s_io獲得下一個索引節點的指標。如果該連結串列為空,就返回。
c) 如果sync_sb_inodes()函式開始執行後,索引節點變為髒節點,就略過這個索引節點的髒頁並返回。注意,sb->s_io連結串列中可能殘留一些髒索引節點。
d) 如果當前程序是pdflush核心執行緒,sync_sb_inodes()就檢查執行在另一個CPU上的pdflush核心執行緒是否已經試圖重新整理這個塊裝置檔案的髒頁。這是通過一個原子測試和對索引節點的backing_dev_info的BDI_pdflush標誌的設定操作來完成的。本質上,它對同一個請求佇列上有多個pdflush核心執行緒是毫無意義的。
e) 把索引節點的引用計數器加1。
f) 呼叫__writeback_single_inode()回寫與所選擇的索引節點相關的髒緩衝區:
i. 如果索引節點被鎖定,就把它移到髒索引節點連結串列中(inode->i_sb->s_dirty)並返回0。(因為我們假定wbc->sync_mode欄位不等於WB_SYNC_ALL,所以函式不會因為等待索引結點解鎖而阻塞。)
ii. 使用索引節點地址空間的writepages方法,或者在沒有這個方法的情況下使用mpage_writepages()函式來寫wbc->nr_to_write個髒頁。該函式呼叫find_get_pages_tag()函式快速獲得索引節點地址空間的所有髒頁(參見本章前面“基樹的標記”一節),細節將在下一章描述。
iii. 如果索引節點是髒的,就用超級塊的write_inode方法把索引節點寫到磁碟。實現該方法的函式通常依靠submit_bh()來傳輸一個數據塊。
iv. 檢查索引節點的狀態。如果索引節點還有髒頁,就把索引節點移回sb->s_dirty連結串列,如果索引節點引用計數器為0,就把索引節點移到mode_unused連結串列中;否則就把索引節點移到inode_in_use連結串列中。
v. 返回在第2f(ii)步所呼叫的函式的錯誤程式碼。
g) 回到sync_sb_modes()函式中。如果當前程序是pdflush核心執行緒,就把在第2d步設定的BDI_pdflush標誌清0。
h) 如果略過了剛處理的索引節點中的一些頁,那麼該索引節點包括鎖定的緩衝區:把sb->s_io連結串列中的所有剩餘索引節點移回到sb->s_dirty連結串列中,以後講重新處理它們。
i) 把索引節點的引用計數器減1。
j) 如果wbc->nr_to_write大於0,則回到第2b步搜尋同一個超級塊的其他髒索引節點。否則,sync_sb_inodes()函式終止。
3. 回到writeback_inodes()函式中。如果wbc->nr_to_write大於0,就跳轉到1步,並繼續處理全域性連結串列中的下一個超級塊。否則,就返回。
3 回寫陳舊的髒頁
如前所述,核心試圖避免當一些頁很久沒有被重新整理時發生飢餓危險。因此,髒頁在保留一定時間後,核心就顯式地開始進行1/O資料的傳輸,把髒頁的內容寫到磁碟。
回寫陳舊髒頁的工作委託給了被定期喚醒的pdflush核心執行緒。在核心初始化期間page_writeback_init()函式建立wb_timer動態定時器,以便定時器的到期時間發生在dirty_writeback_centisecs檔案中所規定的幾百分之一秒之後(通常是500分之一秒,不過可以通過修改/proc/sys/vm/dirty_writeback_centisecs檔案調整這個值)。定時器函式wb_timer_fn()本質上呼叫pdflush_operation()函式,傳遞給它的引數是回撥函式wb_kupdate()的地址。
wb_kupdate()函式遍歷頁快取記憶體搜尋陳舊的髒索引節點,它執行下面的步驟:
1. 呼叫sync_supers()函式把髒的超級塊寫到磁碟中(參見下一節)。雖然這與頁快取記憶體中的頁重新整理沒有很密切的關係,但對sync_supers()的呼叫確保了任何超級塊髒的時間通常不會超過5s。
2. 把當前時間減30s所對應的值(用jiffies表示)的指標存放在writeback_control描述符的older_than_this欄位中。允許一個頁保持髒狀態的最長時間是30s。
3. 根據每CPU變數page_state確定當前在頁快取記憶體中髒頁的大概數量。
4. 反覆呼叫writeback_inodes(),直到寫入磁碟的頁數等於上一步所確定的值,或直到把所有保持髒狀態時間超過30s的頁都寫到磁碟。如果在迴圈的過程中一些請求佇列變得擁塞,函式就可能去睡眠。
5. 用mod_timer()重新啟動wb_timer動態定時器:一旦從呼叫該函式開始經歷過檔案dirty_writeback_centisecs中規定的幾百分之一秒時間後,定時器到期(或者如果本次執行的時間太長,就從現在開始1s後到期)。
4 sync()、fsync()和fdatasync()系統呼叫
我們下面來簡要介紹使用者應用程式把髒緩衝區重新整理到磁碟會用到的三個系統呼叫:
sync()
允許程序把所有的髒緩衝區重新整理到磁碟。
fsync()
允許程序把屬於特定開啟檔案的所有塊重新整理到磁碟。
fdatasync()
與fsync()非常相似,但不重新整理檔案的索引節點塊。
4.1 sync()系統呼叫
sync()系統呼叫的服務例程sys_sync()呼叫一系列輔助函式:
wakeup_bdflush(0);
sync_inodes(0);
sync_supers();
sync_filesystems(0);
sync_filesystems(1);
sync_inodes(1);
正如上一節所描述的,wakeup_bdflush()啟動pdflush核心執行緒,把頁快取記憶體中的所有髒頁重新整理到磁碟。
sync_inodes()函式掃描超級塊的連結串列以搜尋要重新整理的髒索引節點;它作用於引數wait,該引數表示在執行完重新整理之前函式是否必須等待。函式掃描當前已安裝的所有檔案系統的超級塊;對於每個包含髒索引節點的超級塊,sync_inodes()首先呼叫sync_sb_inodes()重新整理相應的髒頁,然後呼叫sync_blockdev()顯式重新整理該超級塊所在塊裝置的髒緩衝區頁。這一步之所以能完成是因為許多磁碟檔案系統的write_inode超級塊方法僅僅把磁碟索引節點對應的塊緩衝區標記為“髒”;函式sync_blockdev()確保把sync_sb_inodes()所完成的更新有效地寫到磁碟。
函式sync_supers()把髒超級塊寫到磁碟,如果需要,也可以使用適當的write_super超級塊操作。最後,sync_filesystems()為所有可寫的檔案系統執行sync_fs超級塊方法。該方法只不過是提供給檔案系統的一個“鉤子”,在需要對每個同步執行一些特殊操作時使用,只有像Ext3這樣的日誌檔案系統使用這個方法。
注意,sync_inodes()和sync_filesystems()都是被呼叫兩次,一次是引數wait等於0時,另一次是wait等於1時。這樣做的目的是:首先,它們把未上鎖的索引節點快速重新整理到磁碟;其次,它們等待所有上鎖的索引節點被解鎖,然後把它們逐個地寫到磁碟。
4.2 fsync()和fdatasync()系統呼叫
系統呼叫fsync()強制核心把檔案描述符引數fd所指定檔案的所有髒緩衝區寫到磁碟中(如果需要,還包括存有索引節點的緩衝區)。相應的服務例程獲得檔案物件的地址,並隨後呼叫fsync方法。通常這個方法以呼叫函式__writeback_single_inode()結束,該函式把與被選中的索引節點相關的髒頁和索引節點本身都寫回磁碟
系統呼叫fdatasync()與fsync()非常相似,但是它只把包含檔案資料而不是那些包含索引節點資訊的緩衝區寫到磁碟。由於Linux 2.6沒有提供專門的fdatasync()檔案方法,該系統呼叫使用fsync方法,因此與fsync()是相同的。