天天看點

Linux 檔案系統之高速緩沖區

作者:Linux碼農

高速緩沖區是檔案系統通路塊裝置中資料的必經要道。

高速緩沖區在塊裝置與核心其他程式之間起着一個橋梁作用。除了塊裝置驅動程式 以外,核心程式如果需要通路塊裝置中的資料,就都需要經過高速緩沖區來間接地操作。

為了通路塊裝置上檔案系統中的資料,核心 可以每次都通路塊裝置,進行讀或寫操作。但是每次 I/O 操作的時間與記憶體和 CPU 的處理速度相比是非 常慢的。為了提高系統的性能,核心就在記憶體中開辟了一個高速資料緩沖區(池)(buffer cache),并将 其劃分成一個個與磁盤資料塊大小相等的緩沖塊來使用和管理,以期減少通路塊裝置的次數。

在 linux 核心中,高速緩沖區位于核心代碼和主記憶體區之間,如圖所示

Linux 檔案系統之高速緩沖區

高速緩沖中存放着最近被使用過 的各個塊裝置中的資料塊。

當需要從塊裝置中讀取資料時,緩沖區管理程式首先會在高速緩沖中尋找。如果相應資料已經在緩沖中,就無需再從塊裝置上讀。如果資料不在高速緩沖中,就發出讀塊裝置的命 令,将資料讀到高速緩沖中。

當需要把資料寫到塊裝置中時,系統就會在高速緩沖區中申請一塊空閑的 緩沖塊來臨時存放這些資料。至于什麼時候把資料真正地寫到裝置中去,則是通過裝置資料同步實作的。

Linux 核心實作高速緩沖區的程式是 buffer.c。檔案系統中其他程式通過指定需要通路的裝置号和數 據邏輯塊号來調用它的塊讀寫函數。這些接口函數有:塊讀取函數 bread()、塊提前預讀函數 breada()和 頁塊讀取函數 bread_page()。頁塊讀取函數一次讀取一頁記憶體所能容納的緩沖塊數(4 塊)。

高速緩沖區初始化

整個高速緩沖區被劃分成 1024 位元組大小的緩沖塊,這正好與塊裝置上的磁盤邏輯塊大小相同。

高速 緩沖采用 hash 表和包含所有緩沖塊的連結清單進行操作管理。在緩沖區初始化過程中,初始化程式從整個緩 沖區的兩端開始,分别同時設定緩沖塊頭結構和劃分出對應的緩沖塊,見圖所示。

Linux 檔案系統之高速緩沖區

緩沖區的高端 被劃分成一個個 1024 位元組的緩沖塊,低端則分别建立起對應各緩沖塊的緩沖頭結構 buffer_head 。該頭結構用于描述對應緩沖塊的屬性,并且用于把所有緩沖頭連接配接成連結清單。劃分操作一直持續到緩沖區中沒有足夠的記憶體再劃分出緩沖塊為止。

高速緩沖區結構和連結清單

所有緩沖塊的 buffer_head 被連結成一個雙向連結清單結構,稱為空閑連結清單,如圖所示。

Linux 檔案系統之高速緩沖區

圖中 free_list 指針是該連結清單的頭指針,指向空閑塊連結清單中第一個“最為空閑的”緩沖塊,即近期最少使用的緩沖塊。而該緩沖塊的反向指針 b_prev_free 則指向緩沖塊連結清單中最後一個緩沖塊,即最近剛使用的緩沖塊。

圖中緩沖頭結構中“其他字段”包括塊裝置号、緩沖資料的邏輯塊号,這兩個字段唯一确定了緩沖 塊中資料對應的塊裝置和資料塊。另外還有幾個狀态标志:資料有效(更新)标志、修改标志、資料被 使用的程序數和本緩沖塊是否上鎖标志。

核心程式在使用高速緩沖區中的緩沖塊時,是指定裝置号(dev)和所要通路裝置資料的邏輯塊号 (block),通過調用緩沖塊讀取函數 bread()、bread_page()或 breada()進行操作。這幾個函數都使用緩沖 區搜尋管理函數 getblk(),用于在所有緩沖塊中尋找比對或最為空閑的緩沖塊。該函數将在下面重點說明。在系統釋放緩沖塊時,需要調用 brelse()函數。所有這些緩沖塊資料存取和管理函數的調用層次關系可用下圖來描述。

Linux 檔案系統之高速緩沖區

高速緩沖區的 Hash 表

為了能夠快速而有效地在緩沖區中尋找判斷出請求的資料塊是否已經被讀入到緩沖區中,buffer.c 程式使用了具有 307 個 buffer_head 指針項的 hash(散列、雜湊)數組表結構。Hash 表所使用的散列函數 由裝置号和邏輯塊号組合而成。程式中使用的具體 hash 函數是:(裝置号^邏輯塊号) Mod 307。圖中指針 b_prev、b_next 就是用于 hash 表中散列在同一項上多個緩沖塊之間的雙向連結,即把 hash 函數 計算出的具有相同散列值的緩沖塊連結在散列數組同一項連結清單上。對于動态變化的 hash 表結構某一時刻的狀态 可參見下圖所示。

Linux 檔案系統之高速緩沖區

其中,雙箭頭橫線表示散列在同一 hash 表項中緩沖塊頭結構之間的雙向連結指針。虛線表示緩沖區 中所有緩沖塊組成的一個雙向循環連結清單(即所謂空閑連結清單),而 free_list 是該連結清單最為空閑緩沖塊處的頭 指針。實際上這個雙向連結清單是一個最近最少使用 LRU(Least Recently Used)連結清單。

緩沖塊搜尋函數

上面提及的三個函數在執行時都調用了 getblk(),以擷取适合的空閑緩沖塊。該函數會首先調用 get_hash_table()函數,在 hash 表隊列中搜尋指定裝置号和邏輯塊号的緩沖塊是否已經存在。如果指定的 緩沖塊存在就立刻傳回對應緩沖頭結構的指針;如果不存在,則從空閑連結清單頭開始,對空閑連結清單進行掃 描,尋找一個空閑緩沖塊。在尋找過程中還要對找到的空閑緩沖塊作比較,根據賦予修改标志和鎖定标 志組合而成的權值,比較哪個空閑塊最适合。若找到的空閑塊既沒有被修改也沒有被鎖定,就不用繼續 尋找了。若此時沒有找到空閑塊,則讓目前程序進入睡眠狀态,待繼續執行時再次尋找。若該空閑塊被 鎖定,則程序也需進入睡眠,等待驅動程式對其解鎖。若在睡眠等待的過程中,該緩沖塊又被其他程序 占用,那麼隻要再重頭開始搜尋緩沖塊。否則判斷該緩沖塊是否已被修改過,若是,則将該塊寫盤,并 等待該塊解鎖。此時如果該緩沖塊又被别的程序占用,那麼又一次全功盡棄,隻好再重頭開始執行 getblk()。

在經曆了以上折騰後,此時有可能出現另外一個意外情況,也就是在我們睡眠時,可能其他程序已 經将我們所需要的緩沖塊加進了 hash 隊列中,是以這裡需要最後一次搜尋一下 hash 隊列。如果真的在 hash 隊列中找到了我們所需要的緩沖塊,那麼我們又得對找到的緩沖塊進行以上判斷處理,是以,又一 次地我們需要重頭開始執行 getblk()。

最後,我們才算找到了一塊沒有被程序使用、沒有被上鎖,而且是幹淨(修改标志未置位)的空閑緩沖塊。于是我們就将該塊的引用次數置 1,并複位其他幾個标志,然後從空閑表中移出該塊的緩沖頭 結構。在設定了該緩沖塊所屬的裝置号和相應的邏輯号後,再将其插入 hash 表對應表項首部并連結到空 閑隊列的末尾處。由于搜尋空閑塊是從空閑隊列頭開始的,是以這種先從空閑隊列中移出并使用最近不 常用的緩沖塊,然後再重新插入到空閑隊列尾部的操作也就實作了最近最少使用 LRU 算法。最終,傳回 該緩沖塊頭的指針。

從上述分析可以可知,函數在每次擷取新的空閑緩沖塊時,就會把它移到 free_list 頭指針所指連結清單 的最後面,即越靠近連結清單末端的緩沖塊被使用的時間就越近。是以如果 hash 表中沒有找到對應緩沖塊, 就會在搜尋新空閑緩沖塊時從 free_list 連結清單頭處開始搜尋。可以看出,核心取得緩沖塊的算法使用了以 下政策:

  • 如果指定的緩沖塊存在于 hash 表中,則說明已經得到可用緩沖塊,于是直接傳回;
  • 否則就需要在連結清單中從 free_list 頭指針處開始搜尋,即從最近最少使用的緩沖塊處開始。

是以最理想的情況是找到一個完全空閑的緩沖塊,即 b_dirt 和 b_lock 标志均為 0 的緩沖塊;但是如 果不能滿足這兩個條件,那麼就需要根據 b_dirt 和 b_lock 标志計算出一個值。因為裝置操作通常很耗時, 是以在計算時需加大 b_dirt 的權重。然後我們在計算結果值最小的緩沖塊上等待(如果緩沖塊已經上鎖)。最後當标志 b_lock 為 0 時,表示所等待的緩沖塊原内容已經寫到塊裝置上。于是 getblk()就獲得了一塊 空閑緩沖塊。

緩沖塊讀取函數

由以上處理我們可以看到,getblk()傳回的緩沖塊可能是一個新的空閑塊,也可能正好是含有我們需 要資料的緩沖塊,它已經存在于高速緩沖區中。是以對于讀取資料塊操作(bread()),此時就要判斷該緩 沖塊的更新标志,看看所含資料是否有效,如果有效就可以直接将該資料塊傳回給申請的程式。否則就 需要調用裝置的低層塊讀寫函數(ll_rw_block()),并同時讓自己進入睡眠狀态,等待資料被讀入緩沖塊。在醒來後再判斷資料是否有效了。如果有效,就可将此資料返給申請的程式,否則說明對裝置的讀操作 失敗了,沒有取到資料。于是,釋放該緩沖塊,并傳回 NULL 值。

當程式不再需要使用一個緩沖塊中的資料時,就可調用 brelse()函數,以釋放該緩沖塊并喚醒因等待 該緩沖塊而進入睡眠狀态的程序。注意,空閑緩沖塊連結清單中的緩沖塊,并不是都是空閑的。是以隻有當 被寫盤重新整理、解鎖且沒有其他程序引用時(引用計數=0),才能挪作它用。

高速緩沖區通路過程和同步操作

綜上所述,高速緩沖區在提高對塊裝置的通路效率和增加資料共享方面起着重要的作用。除驅動程 序以外,核心其他上層程式對塊裝置的讀寫操作都需要經過高速緩沖區管理程式來間接地實作。它們之 間的主要聯系是通過高速緩沖區管理程式中的 bread()函數和塊裝置底層接口函數 ll_rw_block()來實作。

上層程式若要通路塊裝置資料就通過 bread()向緩沖區管理程式申請。如果所需的資料已經在高速緩沖區 中,管理程式就會将資料直接傳回給程式。如果所需的資料暫時還不在緩沖區中,則管理程式會通過 ll_rw_block()向塊裝置驅動程式申請,同時讓程式對應的程序睡眠等待。等到塊裝置驅動程式把指定的數 據放入高速緩沖區後,管理程式才會傳回給上層程式。見下圖所示。

Linux 檔案系統之高速緩沖區

對于更新和同步(Synchronization)操作,其主要作用是讓記憶體中的一些緩沖塊内容與磁盤等塊設 備上的資訊一緻。sync_inodes()的主要作用是把 i 節點表 inode_table 中的 i 節點資訊與磁盤上的一緻 起來。但需要經過系統高速緩沖區這一中間環節。

實際上,任何同步操作都被分成了兩個階段:

  1. 資料結構資訊與高速緩沖區中的緩沖塊同步問題,由驅動程式獨立負責;
  2. 高速緩沖區中資料塊與磁盤對應塊的同步問題,由這裡的緩沖管理程式負責。

sync_inodes()函數不會直接與磁盤打交道,它隻能前進到緩沖區這一步,即隻負責與緩沖區中的信 息同步。剩下的操作需要緩沖管理程式負責。為了讓 sync_inodes()知道哪些 i 節點與磁盤上的不同,就 必須首先讓緩沖區中内容與磁盤上的内容一緻。這樣 sync_inodes()通過與目前磁盤在緩沖區中的最新數 據比較才能知道哪些磁盤inode需要修改和更新。最後再進行高速緩沖區與磁盤裝置的第二次同步操作, 做到記憶體中的資料與塊裝置中的資料真正的同步。