RFC-0012:Zircon 可捨棄記憶體

RFC-0012:Zircon 可捨棄記憶體
狀態已接受
區域
  • Kernel
說明

說明使用者空間應用程式向核心指出特定記憶體緩衝區符合回收資格的機制。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2020-10-27
審查日期 (年-月-日)2020-12-02

摘要

本 RFC 說明使用者空間應用程式向核心指出特定記憶體緩衝區符合回收資格的機制。當系統可用記憶體不足時,核心就能捨棄這些緩衝區。

提振精神

在 Zircon 這類過度配置系統中,管理可用記憶體是個複雜的問題,因為系統允許使用者應用程式配置的記憶體量,可能超出系統目前可用的記憶體量。這是透過使用虛擬記憶體物件 (VMO) 達成,這些物件會延遲由實體頁面備份,因為其中的部分已提交。

如果高估任何時間點使用的實體記憶體量,並根據該量拒絕後續的記憶體配置要求,可能會導致記憶體閒置。這可能會影響效能,因為應用程式會將大量記憶體用於快取。另一方面,如果低估使用的可用記憶體量,可能會導致系統上所有可用記憶體快速用盡,進而發生記憶體不足 (OOM) 的情況。此外,「可用」記憶體的定義本身就很複雜。

Zircon 核心會監控可用實體記憶體量,並在不同層級產生記憶體壓力信號。這些信號的目的是讓使用者空間應用程式根據系統的可用記憶體量,縮減 (或擴大) 記憶體用量。雖然這有助於避免系統記憶體不足,但將這些信號的發起者 (核心) 與回應者 (使用者應用程式) 解除連結並非理想做法。回應記憶體壓力時,程序沒有足夠的背景資訊,無法判斷應釋放多少記憶體;核心能更清楚掌握系統的整體記憶體用量,也能考量其他可回收的記憶體形式,例如可逐出的使用者分頁備份記憶體。

這項 RFC 提議的機制可讓核心在記憶體壓力下,直接回收使用者空間記憶體緩衝區。這種做法有幾個優點:

  • 可進一步控管要清除多少記憶體;核心可以查看可用記憶體層級,並只清除所需記憶體。
  • 核心可以使用 LRU 配置來捨棄記憶體,這可能更適合在記憶體中容納目前的工作集。
  • 有時,使用者空間可能無法及時因應記憶體壓力訊號而釋放記憶體。在某些情況下,系統可能無法復原。
  • 使用者空間用戶端喚醒以回應記憶體壓力時,有時可能需要更多記憶體。

設計

總覽

可捨棄記憶體通訊協定大致運作方式如下:

  1. 使用者空間程序會建立 VMO,並標示為可捨棄
  2. 直接存取 VMO (zx_vmo_read/zx_vmo_write) 或透過位址空間中的對應 (zx_vmar_map) 存取 VMO 之前,程序會鎖定 VMO,表示該 VMO 正在使用中。
  3. 完成後,該程序會解除鎖定 VMO,表示不再需要 VMO。核心會將所有解除鎖定的可捨棄 VMO 視為符合回收資格,並可在記憶體壓力下捨棄這些 VMO。
  4. 當程序需要再次存取 VMO 時,會嘗試鎖定該 VMO。現在可以透過下列兩種方式之一成功取得這個鎖定。
    • 如果 VMO 的頁面仍完好無損 (也就是核心尚未捨棄),鎖定作業可能會成功。
    • 如果核心已捨棄 VMO,鎖定作業會成功,同時向用戶端指出其頁面已遭捨棄,以便重新初始化或採取其他必要動作。
  5. 完成後,系統會再次解鎖 VMO。視需要重複上述鎖定和解鎖程序。

請注意,可捨棄的記憶體並非直接取代記憶體壓力信號。觀察記憶體壓力變化,對於其他元件層級的決策仍有價值,例如選擇何時啟動記憶體密集型活動或執行緒。日後我們也可能使用這些信號,終止元件內的閒置程序。記憶體壓力信號也能讓元件更精確地控制要釋放的記憶體和時間。

Discardable Memory API

我們可以擴充現有的 zx_vmo_create()zx_vmo_op_range() 系統呼叫,以支援這項功能。

  • zx_vmo_create() 將擴充支援新的 options 旗標 - ZX_VMO_DISCARDABLE。這個旗標可與 ZX_VMO_RESIZABLE 搭配使用。不過,有關可調整大小 VMO 的一般建議也適用於可捨棄 VMO,也就是說,在程序之間共用可調整大小 VMO 可能很危險,應盡量避免。

  • zx_vmo_op_range() 將擴充支援新作業,提供鎖定和解鎖功能,包括 ZX_VMO_OP_LOCKZX_VMO_OP_TRY_LOCKZX_VMO_OP_UNLOCK

  • 鎖定和解除鎖定會套用至整個 VMO,因此 offsetsize 應涵蓋整個 VMO 範圍。在 VMO 中鎖定和解除鎖定較小範圍是錯誤的行為。雖然目前的實作方式並未嚴格要求 offsetsize,但確保只有整個 VMO 範圍視為有效,才能在日後新增子範圍支援,而不會變更用戶端的行為。

  • ZX_VMO_OP_TRY_LOCK 作業會嘗試鎖定 VMO,但可能會失敗。 如果核心尚未捨棄 VMO,就會成功;如果核心已捨棄 VMO,就會失敗並顯示 ZX_ERR_NOT_AVAILABLE。如果失敗,用戶端應使用 ZX_VMO_OP_LOCK 重試,只要傳入的引數有效,保證會成功。這項 ZX_VMO_OP_TRY_LOCK 作業是輕量型選項,可嘗試鎖定 VMO,不必設定緩衝區引數。如果無法鎖定 VMO,用戶端也可以選擇不採取任何行動。

  • ZX_VMO_OP_LOCK 作業也需要 buffer 引數,也就是 zx_vmo_lock_state 結構體的 out 指標。這個結構體供核心傳回用戶端可能實用的資訊,包含:

    • offsetsize 追蹤鎖定範圍:這是用戶端傳入的 sizeoffset 引數。這些值純粹是為了方便而傳回,因此用戶端不需要另外追蹤範圍,可以直接使用傳回的結構體。如果呼叫成功,這些值一律會與傳遞至 zx_vmo_op_range() 呼叫的 sizeoffset 值相同。
    • discarded_offsetdiscarded_size 追蹤已捨棄的範圍:這是鎖定範圍內包含已捨棄頁面的最大範圍。這個範圍內並非所有頁面都會遭到捨棄,這只是這個範圍內所有捨棄子範圍的聯集,可能也包含未捨棄的頁面。使用目前的 API 時,如果核心已捨棄 VMO,捨棄的範圍會涵蓋整個 VMO。如果未捨棄,discarded_offsetdiscarded_size 都會設為零。
  • 鎖定本身不會在 VMO 中提交任何頁面。這只會將 VMO 的狀態標示為「不可捨棄」。用戶端可以使用適用於一般 VMO 的任何現有方法,在 VMO 中提交頁面,例如 zx_vmo_write()ZX_VMO_OP_COMMIT、對應 VMO,以及直接寫入對應的位址。

// |options| supports a new flag - ZX_VMO_DISCARDABLE.
zx_status_t zx_vmo_create(uint64_t size, uint32_t options, zx_handle_t* out);

// |op| is ZX_VMO_OP_LOCK, ZX_VMO_OP_TRY_LOCK, and ZX_VMO_OP_UNLOCK to
// respectively lock, try lock and unlock a discardable VMO.
// |offset| must be 0 and |size| must the size of the VMO.
//
// ZX_VMO_OP_LOCK requires |buffer| to point to a |zx_vmo_lock_state| struct,
// and |buffer_size| to be the size of the struct.
//
// Returns ZX_ERR_NOT_SUPPORTED if the vmo has not been created with the
// ZX_VMO_DISCARDABLE flag.
zx_status_t zx_vmo_op_range(zx_handle_t handle,
                            uint32_t op,
                            uint64_t offset,
                            uint64_t size,
                            void* buffer,
                            size_t buffer_size);

// |buffer| for ZX_VMO_OP_LOCK is a pointer to struct |zx_vmo_lock_state|.
typedef struct zx_vmo_lock_state {
  // The |offset| that was passed in.
  uint64_t offset;
  // The |size| that was passed in.
  uint64_t size;
  // Start of the discarded range. Will be 0 if undiscarded.
  uint64_t discarded_offset;
  // The size of discarded range. Will be 0 if undiscarded.
  uint64_t discarded_size;
} zx_vmo_lock_state_t;

zx::vmo 介面將擴充為支援 ZX_VMO_OP_LOCKZX_VMO_OP_TRY_LOCKZX_VMO_OP_UNLOCK 作業,並搭配 op_range()。Rust、Go 和 Dart 繫結也會更新。

這個 API 可讓用戶端彈性地在多個程序之間共用可捨棄的 VMO。每個需要存取 VMO 的程序都可以獨立執行這項操作,並視需要鎖定及解鎖 VMO。根據鎖定狀態的假設,程序之間不需要仔細協調。只有在沒有人鎖定 VMO 時,核心才會將 VMO 視為可回收的項目。

VMO 限制

  • 可捨棄記憶體 API 僅支援 VmObjectPaged 類型,因為根據定義,VmObjectPhysical 無法捨棄。

  • 由於捨棄複製階層中的 VMO 可能會導致非預期的行為,因此 API 不相容於 VMO 副本 (快照和 COW 副本) 和切片。zx_vmo_create_child() 系統呼叫會在可捨棄的 VMO 上失敗。

  • ZX_VMO_DISCARDABLE 旗標無法用於 zx_pager_create_vmo()options 引數。主要原因是頁面支援的 VMO 可以複製,但可捨棄的 VMO 無法複製。此外,以分頁備份的 VMO 隱含可捨棄性,因此不需要額外標記。

與現有 VMO 作業互動

現有 VMO 作業的語意將維持不變。舉例來說,zx_vmo_read()不會先驗證可捨棄的 VMO 是否已鎖定,再允許作業。客戶有責任確保存取 VMO 時已鎖定該物件,以免核心從中捨棄該物件。這項做法可限制這項變更的範圍。核心提供的唯一保證是,在 VMO 鎖定期間,核心不會捨棄 VMO 的頁面。

即使 VMO 遭到捨棄,只要用戶端在存取對應項之前鎖定 VMO,VMO 的任何對應項都會繼續有效。如果 VMO 已捨棄,客戶不需要重新建立對應。

核心捨棄 VMO 後,如果未先鎖定 VMO,對其執行的任何後續作業都會失敗,就像 VMO 沒有已提交的頁面一樣,而且沒有機制可視需要提交頁面。舉例來說,zx_vmo_read() 會因 ZX_ERR_OUT_OF_RANGE 而失敗。如果 VMO 已對應至程序的位址空間,對應位址的未鎖定存取權會導致嚴重頁面錯誤例外狀況。

核心實作

追蹤中繼資料

  • VmObjectPaged 中的 options_ 位元遮罩將擴充為支援 kDiscardable 標記;我們目前只使用 32 位元中的 4 位元。
  • VmObjectPaged 中會新增 lock_count 欄位,追蹤 VMO 上未完成的鎖定作業數量。
  • 核心會維護可回收 VMO 的全域清單,也就是系統上所有未鎖定且可捨棄的 VMO。清單更新方式如下:
    • ZX_VMO_OP_LOCK 會遞增 VMO 的 lock_count。如果 lock_count 從 0 變成 1,VMO 會從全域可回收清單中移除。
    • ZX_VMO_OP_UNLOCK 會遞減 VMO 的 lock_count。如果 lock_count 降至 0,VMO 就會加入全域可回收清單。

回收邏輯

當可捨棄的 VMO 的 lock_count 降至零時,就會加入全域可回收清單,並在再次鎖定時移除。這會維護系統上所有未鎖定且可捨棄 VMO 的 LRU 順序。當記憶體不足時,核心可以依序從這個清單中取消 VMO 的佇列,並捨棄這些 VMO,然後檢查每個 VMO 後的可用記憶體層級。這是在實務上,回收邏輯可能呈現的簡化版本。稍後會說明其他注意事項。

捨棄作業

核心端會實作「捨棄」,方法是取消 VMO 的所有頁面,並將 VMO 的內部狀態設為 discarded。如果 VMO 的狀態為 discardedVmObjectPaged::GetPageLocked() 會失敗並顯示 ZX_ERR_NOT_FOUND。後續的 ZX_VMO_OP_LOCK 作業會清除 discarded 狀態。GetPageLocked() 是所有 VMO 網頁存取權的函式,包括透過 zx_vmo_read/write 系統呼叫和透過 VM 對應的網頁存取權。這讓我們能夠在捨棄的未解鎖 VMO 上失敗系統呼叫,也能在透過對應存取捨棄的未解鎖 VMO 時產生例外狀況。

實作

這是新的 API,因此目前沒有任何依附元件。核心端實作可以獨立完成。API 實作完成後,使用者空間用戶端即可開始採用。

效能

效能影響會因用戶端使用案例而異。使用 API 時,客戶可以注意以下幾點。

  • 在存取可捨棄的 VMO 前,zx_vmo_op_range() 系統呼叫會鎖定及解除鎖定,這可能會在效能關鍵路徑上增加明顯的延遲。因此,系統呼叫應在可容許或隱藏延遲時間增加的程式碼路徑中使用。
  • 由於快取會在記憶體中保留較長的時間,因此用戶的效能也可能提升。現在,在記憶體壓力下遭用戶端捨棄的緩衝區可以保留更久,因為核心只會捨棄必要記憶體。用戶端可以透過快取命中率、緩衝區需要重新初始化的次數等,追蹤這項變更。

安全性考量

無。

隱私權注意事項

無。

測試

  • 核心測試 / 單元測試,可從多個執行緒執行新的 API。
  • 單元測試會驗證核心端的回收行為,也就是只能捨棄未鎖定的 VMO。

說明文件

Zircon 系統呼叫說明文件需要更新,才能納入新的 API。

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

鎖定 VMO 內的範圍

回收的精細程度是選擇整個 VMO,而不是支援 VMO 內範圍的更精細捨棄作業。造成這種情況的原因如下:

  • 重建捨棄部分頁面的 VMO 可能會很棘手。以一般用途來說,VMO 用於代表匿名記憶體緩衝區,重新填入捨棄的頁面可能都是零,這對於未捨棄的其餘頁面來說,可能不一定有意義。此外,只保留 VMO 的部分頁面可能沒有價值,也就是說,VMO 必須完整填入資料才有意義。
  • VMO 的精細程度可簡化VmObjectPaged導入作業,只需要最少的追蹤中繼資料。我們不需要追蹤鎖定的範圍,以便稍後與解鎖的範圍相符。也不會涉及複雜的範圍合併作業。
  • 此外,這項機制也相當輕量,可一次釋出大量記憶體。如果改為支援網頁細微程度,可能需要維護網頁佇列,並淘汰可捨棄的網頁,類似於我們用來清除使用者分頁備份網頁的機制。

如果需要,建議的 API 會在 zx_vmo_op_range() 中保留 offsetsize 引數,目前未使用,但未來可指出可回收的範圍。為鎖定 API (頁面細微程度鎖定) 新增範圍支援,似乎是目前提案的自然延伸。如果客戶要以個別 VMO 備份可捨棄的小型區域,成本可能會過高,這項功能可解決這個問題。

核心實作捨棄作業

當核心回收可捨棄的 VMO 時,會取消認可其頁面,並將其狀態追蹤為 discarded。日後對網頁提出的解鎖要求都會在 discarded 狀態下失敗;VMO 再次鎖定後,discarded 狀態就會清除。另一種做法是直接取消提交頁面,而不明確追蹤狀態。不過,追蹤 discarded 狀態可實現更嚴格的失敗模型。舉例來說,假設用戶端在位址空間中對應了可捨棄的 VMO,而核心在某個時間點捨棄了該 VMO。如果用戶端現在嘗試透過對應存取 VMO,但未先鎖定 VMO,就會發生嚴重頁面錯誤。反之,如果核心只會取消認可頁面,後續的解鎖存取權只會導致系統將零頁面默默交給用戶端。這可能不會遭到偵測,或因出現非預期的零頁面而導致更細微的錯誤。

另一種做法是將 VMO 內部大小調整為零。這樣一來,我們就能預設取得所需的失敗模型,不必進行任何明確的狀態追蹤。不過,除了使用者看到的外部大小,這還需要追蹤 VMO 的內部實作定義大小。雖然擁有內部實作定義的大小是實用的技巧,未來可能也有助於其他用途,但兩種不同的概念會造成混淆,且容易發生錯誤。因此,在我們找到其他具體的使用情境,可明確看出除了外部大小之外,還需要內部大小的情況前,我們選擇避免採取這種做法。

使用原子作業加快鎖定 API 的速度

這項鎖定最佳化功能提供替代的低延遲選項,可鎖定及解除鎖定可捨棄的 VMO,適用於預期會頻繁鎖定及解除鎖定的用戶端。這純粹是效能最佳化,因此日後如有需要,我們可能會新增這項功能。

這項 API 使用名為 Metex 的鎖定基本類型,類似於 Zircon futex,可透過使用者空間原子快速鎖定,進而節省系統呼叫成本。

可捨棄的 VMO 可以與 metex 建立關聯,用於鎖定和解除鎖定,而非 zx_vmo_op_range() 系統呼叫。metex 可能有三種狀態:已鎖定 (由使用者空間用戶端使用中)、可捨棄 (核心可回收),以及「需要系統呼叫」(可能已由核心回收,需要系統呼叫才能檢查狀態)。鎖定及解鎖 VMO 時,不需要輸入核心,只要以原子方式翻轉鎖定和可捨棄之間的 metex 狀態即可。當核心捨棄 VMO 時,會以原子方式將其狀態翻轉為「需要系統呼叫」,表示用戶端需要與核心同步,以檢查捨棄狀態。這項提案的更多詳細資料超出本 RFC 的範圍,將在另一份文件中提供。

以分頁為基礎的建立 API

任何由分頁器支援的 VMO 本質上都是可捨棄的 VMO,因為分頁器提供機制,可視需要重新填充分頁。本 RFC 建議的捨棄式記憶體類型為匿名捨棄式記憶體;另一種是檔案支援的捨棄式記憶體,例如由 blobfs 使用者分頁程式填入的 Blob 記憶體內表示法。考量到這一點,我們可以考慮使用替代的建立 API,將可捨棄的 VMO 與分頁器建立關聯。VMO 建立呼叫可能如下所示:

zx_pager_create(0, &pager_handle);

zx_pager_create_vmo(pager_handle, 0, pager_port_handle, vmo_key, vmo_size,
                    &vmo_handle);

鎖定和解鎖功能會按照先前的提議,透過 zx_vmo_op_range() 運作。 只有在解鎖時,核心才能從 VMO 捨棄頁面。

這麼做的好處是,無論是檔案支援或匿名,都能為所有可捨棄的記憶體提供適用的統一建立 API。

不過,這個分頁在這種情況下並無特殊用途。由於處理的是一般匿名記憶體,因此可能只會視需求提供零頁面。如果需要以特定內容填入網頁,則更適合使用分頁器。為了視需要建立零頁面,而引入額外的間接層 (無論是技術複雜度或效能負擔),似乎沒有必要;核心中已存在這項功能,適用於一般 (非分頁支援) VMO。

使用保留物件鎖定

這裡建議的鎖定 API 容易發生錯誤,導致可捨棄的 VMO 遭到意外 (或惡意) 解鎖。在某些情況下,某個程序可能會認為 VMO 已鎖定,但另一個程序已將其解除鎖定,也就是說,第二個程序發出額外的解除鎖定要求。即使第一個程序在存取 VMO 前已正確鎖定,仍會因此發生錯誤或當機。

我們可以實作具有保留物件的鎖定功能,在建立時鎖定 VMO,並在銷毀時解鎖,不必執行鎖定和解鎖作業。

zx_vmo_create_retainer(vmo_handle, &retainer_handle);

只要固定夾把手處於開啟狀態,VMO 就會保持鎖定。在上述範例中,這兩個程序都會使用自己的保留項目鎖定 VMO,避免發生錯誤的額外解除鎖定。這種鎖定模型可降低這類錯誤的發生機率,並在發生時輕鬆診斷。

但缺點是核心需要儲存更多中繼資料,才能追蹤 VMO 的鎖定狀態。現在我們有與可捨棄 VMO 相關聯的保留物件清單,而非單一 lock_count 欄位。如果想避免惡意使用者導致核心無限制成長,我們可能也會想限制這份清單的長度。

回收訂單的優先順序

為簡化起步流程,核心會依 LRU 順序回收未鎖定的可捨棄 VMO。如果需要,我們可以在日後探索讓用戶端明確指定回收的優先順序 (每個優先順序帶中的 VMO 仍可依 LRU 順序回收)。建議的 API 預留了空間,日後可透過 zx_vmo_op_range() 中目前未使用的 buffer 參數支援 ZX_VMO_OP_UNLOCK

不過,我們可能不需要這種程度的控制,全域 LRU 順序可能就足夠了。如果用戶想進一步控制特定緩衝區的回收時間,可以改為選擇記憶體壓力信號,並自行捨棄這些緩衝區。

與其他回收策略的互動

目前我們還可透過另外兩種機制回收記憶體:

  • 使用者分頁支援記憶體 (記憶體內 Blob) 的頁面逐出作業,由核心在 CRITICAL 記憶體壓力層級 (接近記憶體不足) 執行。
  • 記憶體壓力信號,使用者空間元件本身會在 CRITICAL 和 WARNING 記憶體壓力層級釋放記憶體。

我們需要找出可捨棄的記憶體在這個架構中的位置,確保沒有單一回收策略承擔大部分的負擔。舉例來說,我們可能會想維持某種檔案支援記憶體與可捨棄記憶體的逐出比率。

鎖定分頁支援的 VMO

我們可能會在日後將 ZX_VMO_OP_LOCKZX_VMO_OP_UNLOCK 作業擴展至以呼叫器為基礎的 VMO。過去曾有支援鎖定使用者分頁備份 VMO 的需求,如果出現具體用途,我們可能會提供這項功能。舉例來說,blobfs 可能會將記憶體中的 VMO 鎖定為其認為重要的 Blob,或是不太符合核心 LRU 逐出配置的 Blob,藉此避免重新分頁的效能成本。

鎖定以分頁備份的 VMO,可與可捨棄記憶體 API 完美結合,因為以分頁備份的 VMO 本質上可視為一種可捨棄記憶體,使用者分頁提供專門機制來重新填充分頁。鎖定和解除鎖定會同時套用至這兩種可捨棄的記憶體,兩者之間的主要差異在於建立和填入的方式。

決定何時重新填入已捨棄的 VMO

用戶端可能需要判斷何時可以安全地重新填入已捨棄的 VMO。如果 VMO 在記憶體壓力下重新填入,額外提交的頁面可能會加劇系統的記憶體壓力,使其更接近 OOM。此外,如果記憶體壓力持續存在,VMO 後續解鎖後可能會遭到捨棄。這可能會導致系統顛簸,也就是用戶端重複重新填入 VMO,但核心很快就會捨棄該 VMO。

目前觀察系統記憶體壓力層級的唯一機制,是訂閱 fuchsia.memorypressure 服務,但對這個用途來說,這項服務相當昂貴。我們可能會考慮擴展這項服務,提供執行一次性查詢的方法。我們也可以考慮透過 zx_vmo_lock_state 結構體傳回壓力程度指標,可以是目前的記憶體壓力程度本身,也可以是粗略擷取系統是否承受記憶體壓力的布林值。

偵錯輔助工具,可追蹤未鎖定的 VMO 存取權

您或許可以啟用建構旗標後方的額外檢查,在未鎖定的可捨棄 VMO 上導致系統呼叫失敗。這有助於開發人員輕鬆找出 VMO 存取作業未先上鎖的錯誤,不必依賴記憶體壓力下捨棄 VMO,然後才導致失敗。日後新增範圍支援時,這類 VMO 鎖定狀態檢查可能會迅速變得昂貴,因此不適合在正式環境中啟用,但或許能做為偵錯工具。

透過對應項目找出未鎖定的 VMO 存取權可能較難實作。我們可以探索幾種方法來達成這個目標:

  • 解除對應已對應的可捨棄 VMO (解鎖時)。採用這種做法時,我們需要確保現有的 VMO / VMAR 語意維持不變。
  • 教導鎖定 / 解除鎖定呼叫周圍的包裝函式,告知 ASAN 解除鎖定的 VMO 對應項應視為已中毒,直到再次鎖定為止,使用 ASAN_POISON_MEMORY_REGION 介面。

既有技術和參考資料