RFC-0226:Zircon Pager 寫回

RFC-0226:Zircon Pager Writeback
狀態已接受
區域
  • 核心
說明

核心支援功能,可追蹤及回寫對分頁器支援的 VMOs 的修改

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2023-04-13
審查日期 (年-月-日)2023-09-19

摘要

本文說明 Zircon 核心支援的 pager 後援記憶體,可修改後再寫回 (同步) 至 pager 來源,例如儲存磁碟。

提振精神

Zircon 支援建立由使用者空間分頁服務支援的 VMOs (虛擬記憶體物件),通常由檔案系統代管。個別檔案會在記憶體中以 VMO 的形式呈現。當使用者存取 VMO 的頁面時,系統會根據需求將頁面錯誤修正,並由使用者分頁器從磁碟讀取頁面內容。

Zircon 瀏覽器 API 原本只支援唯讀檔案,因此沒有任何機制可將記憶體中已髒污的 VMO 頁面寫回磁碟。這項設計足以主控 blobfs 這類不可變動的檔案系統,可在 Fuchsia 上提供所有可執行檔和其他唯讀檔案。不過,如果是用於一般用途的檔案系統,則需要寫回支援功能,因為在這種情況下,檔案內容可由用戶端修改,並需要同步回磁碟。

如果沒有回寫支援,minfsfxfs 等可變更的檔案系統就無法充分利用需求分頁。因此,可變動的檔案系統必須使用匿名 (非分頁器支援) VMOs,在記憶體中快取檔案,並自行管理這些 VMOs 的內容。即使很少或從未存取其中的某些頁面,這些 VMOs 可能仍需要完整保留在記憶體中。將這些匿名 VMOs 切換為 pager 支援的 VMOs,讓頁面可視需要發生錯誤並遭到淘汰,進而讓可變動的檔案系統更有效地運用記憶體。回寫支援功能還可讓可變動的檔案系統用戶端直接在 VMOs 上執行讀取和寫入作業,而非依賴管道進行資料傳輸,因為受管道限制的傳輸速度可能非常緩慢。

在本文件的其餘部分中,使用者分頁器和檔案系統這兩個詞彙會視情況交替使用。

相關人員

協助人員:

  • cpu@google.com

審查者:

  • adanis@google.com、csuter@google.com

諮詢:

  • brettw@google.com、cdrllrd@google.com、godtamit@google.com、maniscalco@google.com、travisg@google.com

社會化:

這份 RFC 已通過 Local Storage 團隊的設計審查。

需求條件

建議的設計可達成下列目標:

  1. 強化 Zircon,以支援以分頁器為後端的 VMOs 的回寫功能,進而建構高效能可變動的檔案系統 (使用 Zircon 串流)。
  2. 支援透過 VM 對應 (例如 mmap'd 檔案) 讀取及寫入檔案。
  3. 提供使用者分頁器啟動的最佳努力清除髒頁,以降低因意外關機而導致資料遺失的風險。
  4. 在日後的迭代中,允許核心透過使用者分頁機制,針對系統記憶體壓力移除髒頁。

以及一些非目標:

  1. 除了使用者分頁器啟動的清除作業之外,核心不會保證髒頁的清除速度。不過,未來的進化版本可能會新增限制未完成的髒資料量,並讓核心根據該數量啟動回寫要求的功能。
  2. 防止違反核心/分頁器通訊協定而導致資料遺失並非目標。如果使用者分頁器無法在關閉 VMO 的句柄 (或終止) 前查詢髒頁並將其寫回,可能會發生資料遺失的情形。

設計

總覽

這項設計提案旨在支援兩種即時用途:

  • 檔案系統用戶端可透過串流存取檔案,串流是包裝檔案 VMOs 的核心物件。這大致可視為類似 zx_vmo_read()zx_vmo_write(),因為串流系統呼叫會在內部包裝 VMO 讀取/寫入核心例程式。
  • 檔案系統用戶端也可以 mmap 檔案,這大致等同於使用 zx_vmar_map 將檔案 VMO 對應至用戶端程序的位址空間。

為簡化說明,本文件的其餘部分將討論與檔案 VMO 的直接互動,可透過系統呼叫 (zx_vmo_read/write) 或 VM 對應 (zx_vmar_map) 進行。

以下列舉幾個範例,說明涉及回寫的互動可能會是什麼樣子。

示例 1

  1. 檔案系統用戶端會針對指定範圍的檔案 VMO 執行 zx_vmo_read()
  2. 由於 VMO 是透過分頁器支援,因此核心會為相關聯的使用者分頁器產生讀取要求。
  3. 由檔案系統代管的使用者分頁器會滿足這項要求。它會提供從磁碟讀取的內容頁面。
  4. 檔案系統用戶端會針對相同範圍的 VMO 執行 zx_vmo_write()。我們先前已在步驟 3 中填入 VMO 的網頁,因此可以直接寫入。目前只有記憶體中做出的修改,但需要在某個時間點將這些修改反映回磁碟。
  5. 使用者分頁器會查詢核心,找出 VMO 中已髒汙 / 修改的範圍。這可在檔案系統執行的定期背景刷新作業中執行,也可以回應檔案系統用戶端要求的刷新作業。
  6. 使用者分頁器會將查詢的髒位址範圍寫回磁碟。此時,已成功將修改過的檔案內容儲存至磁碟。

示例 2

  1. 檔案系統用戶端會使用 zx_vmar_map() 對應檔案 VMO。對應會從地址 addr 開始。用戶端會讀取從 addr 開始的範圍。
  2. 與範例 1 相同。
  3. 與範例 1 相同。
  4. 檔案系統用戶端現在會寫入從 addr 開始的相同範圍。基礎頁面已填入資料,因此內容會在記憶體中修改。這些修改必須在某個時間點反映在磁碟上。
  5. 與範例 1 相同。
  6. 與範例 1 相同。

以下範例會先讀取 VMO,再執行寫入作業。請注意,這只是為了清楚起見,將使用者分頁器的頁面填入作業分成不同的步驟。用戶端可以直接寫入尚未在記憶體中的檔案偏移量;寫入作業會在使用者分頁器先提供該頁面時才會解除封鎖。

上述兩個範例都假設檔案系統會採用覆寫模式進行寫入作業,也就是說,系統可以直接寫入已填入 (已提交) 的頁面,而不需要先要求額外的空間。經過修改的內容會寫回磁碟上的相同位置,因此不需要為修改內容分配額外的空間。不過,fxfsminfs 等檔案系統採用的是 Copy-on-Write (CoW) 模式,每個修改都需要在磁碟上分配新的空間。因此,我們也需要一種機制,為已提交的頁面保留寫入空間;步驟 4 已修改為在寫入作業繼續執行前,等待該保留作業。

為了執行回寫,Zircon 分頁器 API 已擴充支援下列項目:

  • 核心會封鎖寫入 VMOs,這些 VMOs 是使用者分頁器已指出應遵循 Copy-on-Write 配置的 VMOs,並在使用者分頁器已確認寫入作業後繼續執行。
  • 核心會追蹤 VMO 中的髒頁,並提供機制,將這些資訊顯示給使用者分頁器。
  • 使用者分頁器會向核心指出何時在 VMO 中同步髒位範圍,以及何時完成,以便核心能據此更新髒追蹤狀態。
  • 核心會將 VMO 大小調整資訊提供給使用者分頁器。
  • 使用者分頁器有一種方法,可查詢核心代為追蹤的相關資訊,例如 VMO 上次修改的時間。

建立 Pager 和 VMO

分頁器建立系統呼叫保持不變,也就是說,zx_pager_create() 會用於建立分頁器,並將 options 設為 0。

使用 zx_pager_create_vmo() 建立的 Pager 支援型 VMOs 會與 Pager 連接埠和金鑰相關聯,而該金鑰會用於該 VMO 的頁面要求封包。zx_pager_create_vmo() 系統呼叫也支援新的 options 旗標 ZX_VMO_TRAP_DIRTY。這表示核心應擷取任何對 VMO 的寫入作業,並先向使用者分頁器要求寫入作業的確認訊息。這個標記適用於以 Copy-on-Write 模式運作的檔案。稍後會進一步說明此標記。

// Create a VMO (returned via |out|) backed by |pager|. Pager requests will be
// queued on |port| and will contain the provided |key| as an identifier.
// |size| will be rounded up to the page boundary.
//
// |options| must be 0 or a combination of the following flags:
// ZX_VMO_RESIZABLE - if the VMO can be resized.
// ZX_VMO_TRAP_DIRTY - if writes to clean pages in the VMO should be trapped by the
// kernel and forwarded to the pager service for acknowledgement before proceeding
// with the write.
zx_status_t zx_pager_create_vmo(zx_handle_t pager,
                                uint32_t options,
                                zx_handle_t port,
                                uint64_t key,
                                uint64_t size,
                                zx_handle_t* out);

根據預設,所有 pager 支援的 VMOs 都會視為可變動;這也適用於實作唯讀檔案系統,且不需額外付費。修改網頁的程式碼路徑,可能不會用於唯讀的 VMOs。不過,如果 VMOs 遭到修改 (可能是意外濫用),但使用者分頁器從未查詢其中的髒頁,並嘗試將其寫回,則修改後的內容只會保留在記憶體中。日後當核心產生回寫要求時,使用者分頁器可以將此類 VMOs 的回寫要求視為錯誤,或直接忽略。

提供 VMO 網頁

在接收到 pager 讀取要求後,使用者 pager 會使用 zx_pager_supply_pages() 依需求填入 pager 支援的 VMO 中的頁面。這個系統呼叫已存在,可與唯讀 VMOs 搭配使用。

回寫的網頁狀態

由分頁器支援的 VMOs 可有三種狀態的頁面:DirtyCleanAwaitingClean。這些狀態會在 vm_page_t 中編碼。

這三種狀態之間的轉換步驟如下:

  1. 新提供 zx_pager_supply_pages() 的網頁會以 Clean 開始。
  2. 寫入網頁後,網頁會轉換為 Dirty。如果 VMO 是使用 ZX_VMO_TRAP_DIRTY 建立,核心會先在收到使用者 DIRTY 分頁器請求的確認訊息時阻斷。稍後會提供此互動行為的詳細資訊。
  3. 使用者分頁器會在稍後透過系統呼叫,向核心查詢 VMO 中的髒頁清單。
  4. 對於核心傳回的每個髒頁,使用者分頁器都會叫用系統呼叫,向核心發出信號,表示它開始寫回頁面,並將狀態變更為 AwaitingClean
  5. 如果網頁寫入的時間超過此點,其狀態會切換回 Dirty
  6. 當使用者分頁器完成寫回頁面時,就會發出另一個系統呼叫。如果網頁狀態目前為 AwaitingClean,則會轉換為 Clean
  7. 如果使用者分頁器在寫回時發生錯誤,頁面會保留在 AwaitingClean 中。日後針對髒頁執行的查詢會傳回 AwaitingCleanDirty 頁面,以便使用者分頁器嘗試再次寫回頁面。

下方的狀態圖顯示這些狀態之間的轉換。

髒狀態轉換流程圖

AwaitingClean 需要以獨立狀態追蹤,原因如下:

  • 雖然 CleanAwaitingClean 狀態的頁面在寫入時會轉換為 Dirty,但使用者分頁器正在寫回的頁面必須與 Clean 頁面區別對待。Clean 頁面在記憶體壓力下可進行回收,但正在寫回的頁面必須受到保護,以免遭到回收。

  • 核心需要知道哪個版本的網頁已寫回,以便在使用者分頁器完成時,正確將網頁轉換為 Clean。這點很重要,因為這樣才能區分在清除前 (會安全地寫入磁碟) 和之後 (需要稍後寫回) 的頁面寫入作業。

  • 我們可以在回寫作業開始時避免使用者分頁器的系統呼叫,而核心在將頁面傳回給使用者分頁器的髒頁查詢時,可以直接將頁面標示為 AwaitingClean。不過,在查詢後,使用者分頁器可能還需要一段時間才會開始清除網頁,因此網頁會持續一段較長的時間處於髒污狀態。使用較窄的回寫窗格,可提高使用者分頁器將頁面成功移至 Clean 狀態的可能性。

為了更新髒狀態,核心會追蹤頁面何時透過 VMO 和串流寫入系統呼叫,以及透過 VM 對應寫入。請注意,這項功能適用於透過 VM 對應發生的任何寫入作業,無論是使用者或核心執行,也就是說,這項功能也適用於核心透過 user_copy 執行的寫入作業,而 user_copyzx_channel_read() 等系統呼叫的一部分。

zx_vmo_write() 等系統呼叫期間推斷髒頁很簡單,因為範圍已指定。另一種存取 VMO 的方式,是透過程序位址空間中的 VM 對應。為了在輔助程式支援的 VMOs 中追蹤髒狀態,可寫入的對應項目會在對應的頁面表項目中移除寫入權限。因此,寫入作業會產生保護錯誤,您可以透過還原寫入權限,並將網頁狀態標示為 Dirty 來解決這項錯誤。

此處所指的 Dirty 狀態是 vm_page_t 追蹤的狀態,也就是軟體追蹤的髒狀態。x86 的硬體頁面表支援髒位元追蹤,但我們選擇不使用這項功能,以便在初始導入時衍生頁面的髒位元狀態。對於不支援位址表中的髒位元位址的舊版 arm64 平台,我們還是需要在軟體中追蹤髒位元。因此,為了實現一致性和簡化實作方式,我們選擇一開始不使用硬體髒位元來推斷頁面的髒 / 淨狀態。依賴硬體頁面表位元也會導致頁面表回收作業出現複雜問題,因此日後如果要依賴硬體位元,就必須考量這項問題。

值得一提的是,只有直接由分頁器支援的 VMOs 才符合髒追蹤資格。換句話說,由分頁器支援的 VMOs 的 CoW 複本不會選擇加入髒追蹤,也不會看到任何回寫要求。以複本編寫的網頁會從父項的網頁副本分支,而複本會直接擁有這些網頁,做為獨立的網頁。

為待寫入的資料預留空間

寫入標記為使用 ZX_VMO_TRAP_DIRTY 建立選項標記的 VMOs,以擷取髒資料轉換,需要檔案系統的確認。這項解決方案分為兩個部分,v1 會從簡單開始,著重於正確性,而 v2 則會在 v1 的基礎上改善效能。v1 提案大致遵循同步模型,檔案系統會為新的寫入作業保留空間。在 v2 中,我們會新增另一個層級,用於在核心中表示髒汙保留配額,以及如何套用至 VMOs,以便核心可以自行追蹤保留項目。這可減少核心和檔案系統之間的來回通訊,進而提升效能。

ZX_VMO_TRAP_DIRTY v1

ZX_VMO_TRAP_DIRTY VMO 建立旗標表示核心應在 VMO 中擷取任何 Clean->Dirty 頁面轉換 (或 AwaitingClean->Dirty 轉換)。當寫入作業發生在未髒污的頁面時,核心會產生 ZX_PAGER_VMO_DIRTY pager 要求。透過 VM 對應寫入時,要求會跨越包含錯誤位址的單一頁面。針對串流/VMO 寫入作業,核心會針對需要寫入的範圍內,每個相鄰的非髒頁傳送要求。

範圍 [start, end) 的髒資料要求如下所示。

zx_packet_page_request_t request {
    .command = ZX_PAGER_VMO_DIRTY,
    .flags = 0,
    // |offset| and |length| will be page-aligned.
    .offset = start,
    .length = end - start,
};

ZX_VMO_TRAP_DIRTY 建立標記適用於以 CoW 模式寫入的檔案,以及以覆寫模式寫入的稀疏檔案。如果未指定標記,系統會將頁面標記為 Dirty,並在未使用者分頁器的情況下繼續寫入作業;這項功能適用於以覆寫模式寫入的非稀疏檔案。

使用者分頁器會使用 zx_pager_op_range() 確認 ZX_PAGER_VMO_DIRTY 要求:

  • ZX_PAGER_OP_DIRTY 會將未設為 Dirty 的頁面狀態設為 Dirty,並讓核心繼續執行已封鎖的寫入作業。
  • ZX_PAGER_OP_FAIL 不會變更頁面的目前狀態,並會導致 zx_vmo_write() 呼叫寫入來源失敗,為 VM 對應產生致命的頁面錯誤例外狀況,以及在 zx_stream_write() 的情況下,透過部分寫入成功傳回。
// |pager| is the pager handle.
// |pager_vmo| is the VMO handle.
// |offset| and |length| specify the range, i.e. [|offset|, |offset| + |length|).
//
// |op| can be:
//
// ZX_PAGER_OP_DIRTY - The userspace pager wants to transition pages in the range
// [offset, offset + length) from clean to dirty. This will unblock any writes that
// were waiting on ZX_PAGER_VMO_DIRTY page requests for the specified range.
// |data| must be 0.
//
// ZX_PAGER_OP_FAIL - The userspace pager failed to fulfill page requests for
// |pager_vmo| in the range [offset, offset + length) with command
// ZX_PAGER_VMO_READ or ZX_PAGER_VMO_DIRTY.
//
// |data| contains the error encountered, a zx_status_t error code sign-extended
// to a |uint64_t| value - permitted values are ZX_ERR_IO, ZX_ERR_IO_DATA_INTEGRITY,
// ZX_ERR_BAD_STATE and ZX_ERR_NO_SPACE.
zx_status_t zx_pager_op_range(zx_handle_t pager,
                              uint32_t op,
                              zx_handle_t pager_vmo,
                              uint64_t offset,
                              uint64_t length,
                              uint64_t data);

視檔案系統清除髒資料和標記網頁 Clean 的頻率而定,當用戶端寫入網頁時,這種做法可能會造成重大效能成本。為避免這類成本,檔案系統可能會盡可能延遲清除髒資料,但這並非提供的最佳動機,因為髒頁無法淘汰,會導致記憶體壓力,而清除間隔時間越長,資料遺失的可能性也越高。v2 提案會嘗試減少來自用戶端寫入作業的部分效能成本。

ZX_VMO_TRAP_DIRTY v2

系統會新增新的系統呼叫 zx_pager_set_dirty_pages_limit(),指定核心可累積的髒頁數量。預期的情況是,檔案系統會預先為這些髒頁保留空間。這是每個分頁的限制,預設為零。您可以使用 zx_pager_set_dirty_pages_limit() 將限制設為非零值 (如有需要,可多次設定)。v1 設計基本上會以此限制設為零的方式運作。

zx_status_t zx_pager_set_dirty_pages_limit(zx_handle_t pager_handle,
                                           uint64_t num_dirty_pages);

核心會追蹤每個頁面瀏覽器的髒頁數量 (詳情請參閱後續的「可追蹤的頁面類型」),並在轉換為 Dirty 時增加計數,在轉換為 Clean 時則減少計數。核心會像 v1 一樣攔截每個髒頁轉換,但如果可以不超過分配的髒頁限制,則會簡單地增加未完成的髒頁數量。如果新計數不超過限制,核心會繼續寫入,而不會涉及使用者分頁器。這應是正常的運作模式,因此每次網頁髒污時,都會節省與使用者分頁器的來回通訊成本。

使用這種方法時,使用者分頁器需要向核心傳達兩件事:

  1. 整個分頁器的髒位元限制
  2. 在髒污時會計入該限制的網頁

針對 2),我們再次依賴 ZX_VMO_TRAP_DIRTY VMO 建立標記。這個標記現在會觸發新類型的分頁器要求:ZX_VMO_DIRTY_MODE。當核心現在陷阱寫入時,會諮詢檔案系統,判斷是否應將這些頁面納入髒限制的計算範圍。使用者分頁器會以 zx_pager_op_range 回應,其中包含兩種新操作類型之一。

  • ZX_PAGER_OP_DIRTY_MODE_POOLED 會告知核心,範圍內的網頁會計入每個頁面髒位元限制。這項功能適用於以 CoW 模式運作的檔案,以及以覆寫模式運作的檔案稀疏區域。

  • ZX_PAGER_OP_DIRTY_MODE_UNPOOLED 會告知核心,範圍內的頁面不會計入髒位元限制。這項功能適用於以覆寫模式運作的非稀疏檔案區域。

標示為 ZX_PAGER_OP_DIRTY_MODE_POOLED 的網頁會轉換為 Dirty,且未超過 pager dirty 限制的未完成 pager 髒頁數會遞增。不過,如果頁面髒值超過分頁器髒值限制,核心會開始產生 ZX_PAGER_VMO_DIRTY 封包,也就是 v1 中所述的預設模式。您可以提供選用旗標,在頁面寫回 (轉換為 Clean) 時設定髒模式,這樣就能節省擷取未來寫入作業的成本,以產生 ZX_VMO_DIRTY_MODE 分頁器要求。

這項設計可讓檔案系統在其 VMOs 上混合不同類型的寫入模式,進而提供靈活的模型。以 CoW 模式編寫的檔案會使用 ZX_VMO_TRAP_DIRTY 建立 VMOs,且其頁面可使用「共用」模式。同樣地,您可以使用 ZX_VMO_TRAP_DIRTY 標記建立覆寫模式中的稀疏檔案,並分別為稀疏和非稀疏區域使用「已彙整」和「未彙整」模式。一律使用覆寫模式的檔案可以完全省略 ZX_VMO_TRAP_DIRTY 標記,且永遠不會需要支付寫入作業的 pager 要求費用。

當使用者分頁器開始在超出髒額度時接收 ZX_PAGER_VMO_DIRTY 要求時,應會開始清理網頁,以便為新的髒網頁騰出空間。系統會在使用 zx_pager_set_dirty_pages_limit() 完成時發出信號,並使用與先前相同或新的限制。在這個呼叫之後,核心會恢復檢查累積的髒位計數是否超過日後寫入作業的髒位限制,並只在再次觸及髒位限制時產生 ZX_PAGER_VMO_DIRTY 要求。

ZX_VMO_TRAP_DIRTY v1 與 v2 之間的差異

v1 和 v2 之間的主要差異在於負責追蹤預訂數量的實體。在 v1 中,檔案系統負責追蹤保留項目,而核心會告知檔案系統何時以及要增加多少保留數量。由於負責攔截保留項目潛在變更的實體 (核心) 與負責實際記帳的實體 (檔案系統) 不同,因此兩者之間必須緊密連結。在 v2 中,我們嘗試讓核心追蹤保留數,以便稍微放寬這項限制。因此,只有在下列情況下,才需要與檔案系統進行通訊:1) 需要設定 VMO 範圍,才能選擇啟用 (或停用) 核心保留追蹤功能,以及 2) 核心的保留配額用盡,且檔案系統需要介入。

我們預期 2) 是邊緣情況,因為檔案系統會定期將髒頁刷新至磁碟。大部分的通訊內容應是由於 1) 核心可以多次要求相同範圍的資訊 (例如跨越重疊範圍的寫入作業),而檔案系統也可以多次向核心提供相同範圍的多餘資訊。在網頁上設定髒值模式實際上不會影響髒值限制,因為髒值計數只會在實際寫入網頁時增加。因此,檔案系統也可以在網頁上設定髒值模式,以便減少日後分頁器要求的效能成本 (但要注意,因為網頁可能會遭到淘汰)。

找出髒亂範圍

使用者分頁器需要一種機制,才能找出 VMO 中的髒頁,以便將這些髒頁寫回。這裡有兩種不同的模型可供考量:當使用者分頁器查詢核心的髒頁資訊時,可使用提取模型;當核心透過傳送使用者分頁器回寫要求來指出髒頁時,則可使用推送模型。初始設計會從較簡單的拉取模型開始,並引入髒汙範圍查詢系統呼叫,可能如下所示:

// |pager| is the pager handle.
// |pager_vmo| is the vmo handle.
// |offset| and |length| specify the VMO range to query dirty pages within.
// Must be page-aligned.
//
// |buffer| points to an array of type |zx_vmo_dirty_range_t| defined as follows.
// typedef struct zx_vmo_dirty_range {
//   // Represents the range [offset, offset + length).
//   uint64_t offset;
//   uint64_t length;
//   // Any options applicable to the range.
//   // ZX_VMO_DIRTY_RANGE_IS_ZERO indicates that the range contains all zeros.
//   uint64_t options;
// } zx_vmo_dirty_range_t;
//
// |buffer_size| is the size of |buffer|.
//
// |actual| is an optional pointer to return the number of dirty ranges that were
// written to |buffer|.
//
// |avail| is an optional pointer to return the number of dirty ranges that are
// available to read. If |buffer| is insufficiently large, |avail| will be larger
// than |actual|.
//
// Upon success, |actual| will contain the number of dirty ranges that were copied
// out to |buffer|. The number of dirty ranges that are copied out to |buffer| is
// constrained by |buffer_size|, i.e. it is possible for there to exist more dirty
// ranges in [offset, offset + length) that could not be accommodated in |buffer|.
// The caller can assume than any range that had been made dirty prior to
// making the call will either be contained in |buffer|, or will have a start
// offset strictly greater than the last range in |buffer|. Therefore, the caller
// can advance |offset| and make another query to discover further dirty ranges,
// until |avail| is zero.
//
zx_status_t zx_pager_query_dirty_ranges(zx_handle_t pager,
                                        zx_handle_t pager_vmo,
                                        uint64_t offset,
                                        uint64_t length,
                                        void* buffer,
                                        size_t buffer_size,
                                        size_t* actual,
                                        size_t* avail);

使用者分頁器應多次叫用這項查詢,並提前查詢的偏移量,直到處理完所有髒頁面為止。

在提取模式中,頁面清除的速度完全取決於檔案系統選擇查詢髒位範圍並嘗試回寫的速度。不過,在某些情況下,核心本身可能需要發起寫回頁面的請求,例如在記憶體壓力下,以便清理髒頁並隨後釋放。在這種情況下,核心可能會依 LRU 順序傳送髒頁的回寫要求。這項資訊可做為使用者分頁器的提示,讓分頁器提高刷新網頁的速度,例如在處理延遲要求時。

在 VMO 中,針對髒位元範圍 [start, end) 的回寫要求可能如下所示。

zx_packet_page_request_t request {
    .command = ZX_PAGER_VMO_WRITEBACK,
    .flags = ZX_PAGER_MEMORY_PRESSURE,
    // |offset| and |length| will be page-aligned.
    .offset = start,
    .length = end - start,
};

將髒資料範圍寫回

zx_pager_op_range() 系統呼叫已擴充支援兩個額外的作業,ZX_PAGER_OP_WRITEBACK_BEGINZX_PAGER_OP_WRITEBACK_END,用於分別指出使用者分頁器何時開始刷新頁面,以及何時完成。

  • ZX_PAGER_OP_WRITEBACK_BEGIN 會將指定範圍內的任何 Dirty 頁面狀態變更為 AwaitingClean。對於已處於 AwaitingCleanClean 狀態的網頁,系統會忽略此屬性,並維持這些狀態不變。
  • ZX_PAGER_OP_WRITEBACK_END 會將指定範圍內的任何 AwaitingClean 頁面狀態變更為 Clean。對於已是 Clean 的網頁或 Dirty 的網頁,系統會略過這項屬性,並維持其狀態不變。

如果在執行刷新作業時遇到任何錯誤 (例如在 ZX_PAGER_OP_WRITEBACK_BEGIN 之後但在 ZX_PAGER_OP_WRITEBACK_END 之前),使用者分頁器就必須停止執行任何動作。假設沒有其他寫入作業,這些頁面會維持在核心的 AwaitingClean 狀態。當核心再次查詢髒頁時,會一併納入 AwaitingClean 頁面和 Dirty 頁面,然後使用者分頁器可以再次嘗試將這些失敗的頁面寫回。

// Supported |op| values are:
// ZX_PAGER_OP_WRITEBACK_BEGIN indicates that the user pager is about to
// begin writing back the specified range and the pages are marked |AwaitingClean|.
// ZX_PAGER_OP_WRITEBACK_END indicates that that user pager is done writing
// back the specified range and the pages are marked |Clean|.
//
// |pager| is the pager handle.
// |pager_vmo| is the VMO handle.
// |offset| and |length| specify the range to apply the |op| to, i.e. [|offset|,
// |offset| + |length|).
// For ZX_PAGER_OP_WRITEBACK_*, |data| is unused and should be 0.
zx_status_t zx_pager_op_range(zx_handle_t pager,
                              uint32_t op,
                              zx_handle_t pager_vmo,
                              uint64_t offset,
                              uint64_t length,
                              uint64_t data);

針對 ZX_PAGER_OP_WRITEBACK_BEGINdata 可選擇設為 ZX_VMO_DIRTY_RANGE_IS_ZERO,表示呼叫端希望將指定範圍寫回為零。這個方法的用途是當呼叫端處理由 zx_pager_query_dirty_ranges() 傳回的範圍時,其 options 設為 ZX_VMO_DIRTY_RANGE_IS_ZERO。這可確保在查詢後但在回寫開始前,在範圍內建立的任何非零內容不會遺失,因為系統會錯誤地假設該內容仍為零,並將其標示為乾淨 (因此可淘汰)。

調整 VMO 大小

在處理 VMO 中缺少內容的方式上,由 Pager 支援的 VMOs 與匿名 (非由 Pager 支援) VMOs 不同。匿名 VMOs 的隱含初始內容為零,因此未提交的頁面會隱含零。但在使用 pager 支援的 VMOs 時,未提交的頁面不代表零,只是代表 pager 尚未為這些頁面提供內容。不過,如果要將大小調整為較大的尺寸,分頁器無法在新延伸的範圍內提供頁面,因為該內容尚未存在於備份來源 (例如儲存磁碟) 中,因此沒有任何內容可分頁。核心可以提供這個新延伸範圍內的頁面,並將其設為零,而無須諮詢使用者分頁器。

系統會透過追蹤跨越新大小範圍的零間隔來處理大小調整作業,其中核心會隱含提供零頁面。使用者分頁器尚未知曉這個零間隔,因此當使用者分頁器查詢髒位元範圍時,這個範圍會被回報為髒位元。此外,zx_vmo_dirty_range_t 中的選項欄位會設為 ZX_VMO_DIRTY_RANGE_IS_ZERO,表示這個範圍內的值全為零。

如果 VMO 是使用 ZX_VMO_TRAP_DIRTY 標記建立,且頁面會寫入這個新擴充的範圍,則核心會為這些頁面產生 ZX_PAGER_VMO_DIRTY 分頁器要求,然後再提交。這是因為檔案系統可能需要為實際 (非零) 頁面保留空間。這個模型假設檔案系統可在磁碟上以稀疏區域有效呈現零,因此只有在頁面在新擴充範圍內提交時,才會諮詢檔案系統。

將 VMO 從分頁器中卸離

zx_pager_detach_vmo() 會將 ZX_PAGER_COMPLETE 封包排入佇列,表示使用者尋呼機日後不會再收到該 VMO 的尋呼機要求。這也表示使用者分頁器應查詢並回寫任何未完成的髒頁。請注意,只有在髒頁已寫回時,分離作業才會阻斷;它只是通知使用者分頁器可能需要刷新。

分離後,需要產生 pager 要求的 zx_vmo_read() / zx_vmo_write() 會因 ZX_ERR_BAD_STATE 而失敗。透過對應項目讀取及寫入資料時,同樣會產生需要分頁要求的致命頁面錯誤例外狀況。核心可以隨意從 VMO 捨棄已清除的頁面。不過,核心會保留髒頁,直到使用者分頁器將其清除為止。也就是說,即使 VMO 已解除連結,ZX_PAGER_OP_WRITEBACK_BEGINZX_PAGER_OP_WRITEBACK_END 仍會繼續支援。zx_pager_op_range() 在所有其他作業和 zx_pager_supply_pages() 上失敗,且 ZX_ERR_BAD_STATE 在已解除連結的 vmo 上失敗。

如果分頁器在相關聯的 VMOs 中毀損髒頁,則無論是否有任何未完成的回寫要求,核心都可以在該時刻移除這些頁面。換句話說,只要有分頁器可清除髒頁,髒頁就會保證保留在記憶體中。

查詢 pager VMO 統計資料

核心會追蹤 VMO 是否已修改,使用者分頁器可查詢這項資訊。這項屬性可供使用者分頁器用來追蹤 mtime

// |pager| is the pager handle.
// |pager_vmo| is the VMO handle.
// |options| can be ZX_PAGER_RESET_VMO_STATS to reset the queried stats.
// |buffer| points to a struct of type |zx_pager_vmo_stats_t|.
// |buffer_size| is the size of the buffer and should be large enough to
// accommodate |zx_pager_vmo_stats_t|.
//
// typedef struct zx_pager_vmo_stats {
//   uint32_t modified;
// } zx_pager_vmo_stats_t;
zx_status_t zx_pager_query_vmo_stats(zx_handle_t pager,
                                     zx_handle_t pager_vmo,
                                     uint32_t options,
                                     void* buffer,
                                     size_t buffer_size);

如果 VMO 已修改,則傳回的 zx_pager_vmo_stats_tmodified 欄位會設為 ZX_PAGER_VMO_STATS_MODIFIED,否則會設為 0。zx_pager_vmo_stats_t 結構體日後可擴充更多欄位,方便使用者分頁器進行查詢。

modified 狀態會在修改 VMO 的系統呼叫 (例如 zx_vmo_write()zx_vmo_set_size()) 上更新,也會在第一次透過對應傳送寫入頁面錯誤時更新。系統已追蹤網頁上的第一個寫入錯誤,以便正確管理 CleanDirty 的轉換,因此會更新 modified 狀態。不過,透過髒頁對應項目傳入的未來寫入作業不會受到追蹤,因為這會大幅降低寫入對應 VMOs 的速度。因此,對於對應的 VMOs,modified 狀態可能不完全正確。

如果使用者分頁器也想重設查詢的統計資料,options 可以是 ZX_PAGER_RESET_VMO_STATSoptions 值為 0 時不會重設任何狀態,且會執行純查詢。請注意,如果指定 ZX_PAGER_RESET_VMO_STATS 選項,此呼叫可能會消耗可查詢的狀態,進而影響日後的 zx_pager_query_vmo_stats() 呼叫。舉例來說,如果 zx_vmo_write() 後面接連兩個連續的 zx_pager_query_vmo_stats() 呼叫,且使用 ZX_PAGER_RESET_VMO_STATS 選項,則只有第一個呼叫會看到 modified 設定。由於在第一個 zx_pager_query_vmo_stats() 之後沒有進一步修改,第二個 zx_pager_query_vmo_stats() 會將 modified 傳回為 0。

實作

Pager 回寫功能已開發一段時間,且 @next vDSO 提供所有新的 API 部分。fxfs 已採用回寫 API 來支援串流讀取/寫入作業和 mmap

成效

有了分頁器回寫功能,fxfs 就能從透過管道執行 I/O 改為使用串流,進而提升各項基準的效能,約為 40 至 60 倍。

安全性考量

無。

隱私權注意事項

無。

測試

我們已編寫核心核心測試和壓力測試,以便測試分頁器系統呼叫。另外還有儲存空間測試和效能基準。

說明文件

核心系統呼叫說明文件已更新。

缺點、替代方案和未知事項

寫回要求的頻率限制

在日後的迭代中,當核心產生回寫要求 (在記憶體壓力下或以穩定的背景速率) 時,我們需要某種政策來控制在分頁器埠上排入的回寫要求數量。一個選項是讓核心追蹤飛行中未完成的要求數量,並嘗試將其控制在特定限制內。

另一個選項是使用者分頁器可設定可調整的項目,直接或間接決定回寫要求產生的頻率。舉例來說,使用者分頁器可以指定在將回寫作業排入佇列前,網頁可保留髒資料的建議時間長度,或是使用者分頁器可支援的寫入作業的一般資料傳輸速率。有些檔案系統可能需要的背景回寫率遠高於全域系統預設值。使用者分頁器也可能會指定處理要求時的細緻度 (系統頁面大小的倍數),這麼做可能會很有幫助。核心會在計算範圍時將此納入考量,整體而言可能產生較少的請求。

追蹤及查詢網頁年齡資訊

在初始實作中,核心會追蹤頁面佇列中的髒頁,並依頁面首次髒污的時間排序。這個佇列可用於日後產生回寫要求,且在清除頁面後,即可將頁面從髒佇列移至 (乾淨) 分頁器支援的佇列,目前該佇列會追蹤及過濾唯讀頁面。我們可能也想更精確地追蹤髒頁的生命週期;將髒頁和乾淨頁整合至共用集區可能會比較合理,以便針對全域工作集利用老化和存取位元追蹤功能。我們也可能想透過新的 API 將這項年齡資訊公開給使用者分頁器,以便在處理回寫要求時納入考量。

在回寫期間封鎖後續寫入作業

這裡提出的設計不會在回寫進行期間 (即 ZX_PAGER_OP_WRITEBACK_BEGINZX_PAGER_OP_WRITEBACK_END 之間) 阻斷新寫入作業。相反地,寫入的網頁會再次標示為髒頁。某些檔案系統可能會在頁面處於 AwaitingClean 狀態時,封鎖頁面上的寫入作業。我們可以考慮在日後新增 ZX_PAGER_OP_WRITEBACK_BEING_SYNC,在回寫期間封鎖寫入作業。請注意,ZX_VMO_TRAP_DIRTY v1 確實提供一種解決方法,可透過 ZX_PAGER_VMO_DIRTY 分頁器要求解決這個問題,檔案系統可以在清除作業進行時暫緩處理。

既有技術與參考資料