RFC-0254:變更寫入頁面內文的出處

RFC-0254:變更 Copy-on-Write 頁面的歸因
狀態已接受
區域
  • 核心
說明

變更 Zircon 公開的 API 和語義,以便歸因寫入時複製的網頁。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2024-08-02
審查日期 (年-月-日)2024-07-29

問題陳述

Zircon 目前公開的行為,是將記憶體歸給 VMOs,這對 Starnix 核心造成了兩種問題:

首先,Zircon 目前的記憶體歸屬行為僅相容於在 Zircon 的 VMO 實作中使用廣泛共用的鎖定。在執行許多 Starnix 程序時,這項共用鎖定會導致一些效能問題。

Starnix 會為每個 MAP_ANONYMOUS Linux 對應項目提供 VMO,並在 Linux 程序發出 fork() 呼叫時複製所有此類 VMO。在許多 Linux 程序中,結果是許多 VMOs 爭奪一小組共用鎖定。我們製作了省記憶體最佳化原型,其中 Starnix 會為程序中的所有對應使用一個大型 VMO,共用鎖定機制會退化為所有 Starnix VMOs 爭用的「全域」鎖定機制。

其次,Starnix 無法使用 Zircon 提供的 API 模擬 Linux 記憶體歸屬的確切行為。這些指標不會區分 VMO 中的私人和共用寫入時複製網頁,因此也不會公開任何這類共用網頁的分享次數。Starnix 需要這項資訊,才能準確計算各種 /proc 檔案系統項目中公開的 USSPSS 值。

我們只會針對 Zircon 的記憶體歸屬 API 和行為提出變更,以便解決這些問題,同時保留依賴這些 API 的工具現有功能組合。我們在此不打算解決在使用者空間實體 (例如元件和執行者) 之間歸屬記憶體的一般問題。我們也不打算讓這些變更成為歸因的「最後定論」,而是希望藉此解決 Starnix 目前面臨的特定效能瓶頸和功能缺口。

摘要

改善 Zircon 的記憶體歸因 API 和行為,以考量共用寫入時複製的頁面。

相關人員

協助人員:

neelsa@google.com

審查者:

  • adanis@google.com
  • jamesr@google.com
  • etiennej@google.com
  • wez@google.com

諮詢:

  • maniscalco@google.com
  • davemoore@google.com

社會化:

我們與 Zircon、Starnix 和 Memory 團隊的利益相關者進行對話,並將這項 RFC 納入討論。利害關係人也審查了設計文件,其中概述了本 RFC 中提議的變更。

需求條件

  1. 核心的記憶體歸屬實作不得透過讓 VMO 階層鎖定更精細,而導致無法在 Zircon 中最佳化鎖定爭用情形。
  2. memory_monitor 這類工具為系統中的所有 VMOs 加總歸因時,總量必須與系統的實際實體記憶體用量一致。這就是「相加為 1」的屬性。
  3. Starnix 必須能夠在 Zircon 提供的 API 之上模擬 Linux 記憶體歸屬 API。這包括在相關的 proc 檔案系統項目中提供 USSRSSPSS 等測量值。

設計

我們提出的變更會影響 Zircon 如何透過copy-on-write 在 VMO 副本之間共用網頁屬性。這些是 COW 私有和 COW 共用頁面。

我們不建議變更 Zircon 透過記憶體對應或重複的 VMO 句柄,在程序之間共用屬性頁面的方式。這些是程序專屬和程序共用頁面。

使用者空間負責在適當情況下,將程序共用頁面歸給一或多個程序。Zircon 通常不會在決策過程中考慮共用程序,但有一個例外。詳情請參閱「向下相容性」。

共用類型是獨立的,也就是說,某個網頁可以是 COW 私有、COW 共用、程序私有和程序共用的任意組合。

Zircon 的現有行為

Zircon 現有的歸因行為會提供單一 VMO 測量值:來自 VMO 的 COW 私有和 COW 共用頁面的歸因記憶體,每個頁面都有獨特的歸因。

  • 它會將 COW 共用網頁的唯一屬性指派給最早的現行 VMO,以便參照這些網頁。同層級的 VMOs 有時會共用 COW 共用網頁,因此參照網頁的現存最舊 VMO 不必是其他 VMO 的父項。
  • 將系統中所有 VMOs 的記憶體用量加總,即可測量總系統記憶體用量。
  • 這項指標不會評估 VMO 的 USS,因為這可能包含部分 COW 共用頁面。
  • 這項指標也不會評估 VMO 的 RSS,因為這可能不包含 所有 COW 共用網頁。
  • 這項指標不會評估 VMO 的 PSS,因為這項指標會根據分享次數來衡量每個網頁的貢獻度。

Zircon 的新行為

Zircon 的新歸因行為提供三種 VMO 評估方式:

  • USS:來自 VMO 的 COW 私人頁面的歸屬記憶體。這項評估只會將網頁歸因給該 VMO。它會評估在 VMO 遭到銷毀時,系統會釋放的記憶體。
  • RSS:來自 VMO 的 COW-private 和 COW-shared 頁面的歸屬記憶體。這項評估會將 COW 共用網頁歸給共用這些網頁的每個 VMO,因此會多次計算 COW 共用網頁。這個指標會評估 VMO 參照的記憶體總量。
  • PSS:來自 VMO 的 COW-private 和 COW-shared 頁面的歸屬記憶體。這項評估方式會將每個 COW 共用網頁的歸因平均分配給共用該網頁的所有 VMOs。對於特定 VMO,這可能會是位元組的分數,它會評估 VMOs 記憶體的「比例」用量。在整個系統中為所有 VMOs 加總,即可測量總系統記憶體用量。

我們選擇透過公開所有 USSRSSPSS 來變更歸因 API,因為這是我們嘗試的替代方案中唯一符合所有需求的選項。

對使用者空間的影響

此提案會繼續保留現有使用者空間工具所依賴的「相加為一」屬性。在新的行為下,歸因查詢可能會產生不同的結果,但系統會繼續對系統中所有 VMOs 的查詢求和,以便提供記憶體使用量的正確計數。這些變更不會導致 memory_monitorps 等程式開始過度或低估記憶體,反而會讓每個 VMO 和每個工作單元的計數更準確。

新的 API 會公開 PSS 的位元組小數值,且使用者空間必須選擇加入,才能觀察這些位元組小數。_fractional_scaled 欄位包含部分位元組,除非使用者空間使用這些位元組,否則會略微低估 PSS

Starnix 目前會低估 Linux 對應的 RSS 值,因為目前的歸因行為只會將 COW 共用網頁計為一次。RSS 應將共用網頁計入多次。在沒有 Zircon 的協助下,Starnix 無法模擬這種行為,因此我們會變更 Zircon 歸因 API,以提供正確的 RSS 值。我們可以透過另一輪 API 變更來執行這項操作,但為了避免流失,我們選擇將變更一起批次處理。

memory_monitor 會將 VMO 視為與參照其任何子項的程序共用,即使是未直接參照 VMO 的程序也是如此。它會在這些程序之間平均分配父項 VMO 的歸屬記憶體。這會嘗試計算 COW 共用頁面,並讓 memory_monitor 能夠「加總為 1」,但可能會導致部分不正確的結果。舉例來說,它可以將父項 VMO 的 COW 私有頁面部分指派給不參照該 VMO 的程序,因此無法參照該頁面。當父級 COW 共用頁面與部分子項共用,但與其他子項不共用時,也會錯誤縮放父級頁面。這項行為已因我們的變更而變得多餘,因此我們會移除這項行為。當多個程序直接參照 VMOs 時,這項工具仍會調整 VMOs 的記憶體。詳情請參閱「向下相容性」。

Syscall API 異動

Zircon 的歸因 API 包含 zx_object_get_info 主題,我們會進行以下變更:

  • ZX_INFO_VMO
    • 我們會透過以下方式維持 ABI 和 API 的回溯相容性:
      • 重新命名 ZX_INFO_VMO ->ZX_INFO_VMO_V3,同時保留其值。
      • 重新命名 zx_info_vmo_t -> zx_info_vmo_v3_t
    • 我們將新增 ZX_INFO_VMOzx_info_vmo_t,並變更 zx_info_vmo_t 所有版本中兩個現有欄位的含義:
typedef struct zx_info_vmo {
  // Existing and unchanged `zx_info_vmo_t` fields omitted for brevity.

  // These fields already exist but change meaning.
  //
  // These fields include both private and shared pages accessible by a VMO.
  // This is the RSS for the VMO.
  //
  // Prior versions of these fields assigned any copy-on-write pages shared by
  // multiple VMO clones to only one of the VMOs, making the fields inadequate
  // for computing RSS. If 2 VMO clones shared a single page then queries on
  // those VMOs would count either `zx_system_get_page_size()` or 0 bytes in
  // these fields depending on the VMO queried.
  //
  // These fields now include all copy-on-write pages accessible by a VMO. In
  // the above example queries on both VMO's count `zx_system_get_page_size()`
  // bytes in these fields. Queries on related VMO clones will count any shared
  // copy-on-write pages multiple times.
  //
  // In all other respects these fields are the same as before.
  uint64_t committed_bytes;
  uint64_t populated_bytes;

  // These are new fields.
  //
  // These fields include only private pages accessible by a VMO and not by any
  // related clones. This is the USS for the VMO.
  //
  // These fields are defined iff `committed_bytes` and `populated_bytes` are,
  // and they are the same re: the definition of committed vs. populated.
  uint64_t committed_private_bytes;
  uint64_t populated_private_bytes;

  // These are new fields.
  //
  // These fields include both private and shared copy-on-write page that a VMO
  // can access, with each shared page's contribution scaled by how many VMOs
  // can access that page. This is the PSS for the VMO.
  //
  // The PSS values may contain fractional bytes, which are included in the
  // "fractional_" fields. These fields are fixed-point counters with 63-bits
  // of precision, where 0x800... represents a full byte. Users may accumulate
  // these fractional bytes and count a full byte when the sum is 0x800... or
  // greater.
  //
  // These fields are defined iff `committed_bytes` and `populated_bytes` are,
  // and they are the same re: the definition of committed vs. populated.
  uint64_t committed_scaled_bytes;
  uint64_t populated_scaled_bytes;
  uint64_t committed_fractional_scaled_bytes;
  uint64_t populated_fractional_scaled_bytes;
} zx_info_vmo_t;
  • ZX_INFO_PROCESS_VMOS
    • 我們會透過以下方式維持 ABI 和 API 的回溯相容性:
      • 重新命名 ZX_INFO_PROCESS_VMOS ->ZX_INFO_PROCESS_VMOS_V3 並保留其值。
    • 我們會新增 ZX_INFO_PROCESS_VMOS
      • 本主題會重複使用 zx_info_vmo_t 結構體家族,因此所有對這些結構體的變更都會套用至此。
  • ZX_INFO_PROCESS_MAPS
    • 我們會透過以下方式維持 ABI 回溯相容性:
      • 重新命名 ZX_INFO_PROCESS_MAPS ->ZX_INFO_PROCESS_MAPS_V2 並保留其值。
      • 重新命名 zx_info_maps_t -> zx_info_maps_v2_t
      • 重新命名 zx_info_maps_mapping_t -> zx_info_maps_mapping_v2_t
    • 我們會在以下情況中中斷 API 相容性:
      • 重新命名 committed_pagespopulated_pages 是一項破壞性變更。請參閱下方的「向下相容性」。
    • 我們將新增 ZX_INFO_PROCESS_MAPSzx_info_maps_tzx_info_maps_mapping_t,並變更現有版本 zx_info_maps_mapping_t 中兩個欄位的含義。這些欄位不會出現在新版的結構體中 (請見下方):
typedef struct zx_info_maps {
  // No changes, but is required to reference the new `zx_info_maps_mapping_t`.
} zx_info_maps_t;

typedef struct zx_info_maps_mapping {
  // Existing and unchanged `zx_info_maps_mapping_t` fields omitted for brevity.

  // These fields are present in older versions of `zx_info_maps_mapping_t` but
  // not this new version. In the older versions they change meaning.
  //
  // See `committed_bytes` and `populated_bytes` in the `zx_info_vmo_t` struct.
  // These fields change meaning in the same way.
  uint64_t committed_pages;
  uint64_t populated_pages;

  // These are new fields which replace `committed_pages` and `populated_pages`
  // in this new version of `zx_info_maps_mapping_t`.
  //
  // These fields are defined in the same way as the ones in `zx_info_vmo_t`.
  uint64_t committed_bytes;
  uint64_t populated_bytes;

  // These are new fields.
  //
  // These fields are defined in the same way as the ones in `zx_info_vmo_t`.
  uint64_t committed_private_bytes;
  uint64_t populated_private_bytes;
  uint64_t committed_scaled_bytes;
  uint64_t populated_scaled_bytes;
  uint64_t committed_fractional_scaled_bytes;
  uint64_t populated_fractional_scaled_bytes;
} zx_info_maps_mapping_t;
  • ZX_INFO_TASK_STATS
    • 我們會透過以下方式維持 ABI 和 API 的回溯相容性:
      • 重新命名 ZX_INFO_TASK_STATS ->ZX_INFO_TASK_STATS_V1,同時保留其值。
      • 重新命名 zx_info_task_stats_t -> zx_info_task_stats_v1_t
    • 我們將新增 ZX_INFO_TASK_STATSzx_info_task_stats_t,並變更 zx_info_task_stats_t 結構體所有版本中三個現有欄位的含義:
typedef struct zx_info_task_stats {
  // These fields already exist but change meaning.
  //
  // These fields include either private or shared pages accessible by mappings
  // in a task.
  // `mem_private_bytes` is the USS for the task.
  // `mem_private_bytes + mem_shared_bytes` is the RSS for the task.
  // `mem_private_bytes + mem_scaled_shared_bytes` is the PSS for the task.
  //
  // Prior versions of these fields only considered pages to be shared when they
  // were mapped into multiple address spaces. They could incorrectly attribute
  // shared copy-on-write pages as "private".
  //
  // They now consider pages to be shared if they are shared via either multiple
  // address space mappings or copy-on-write.
  //
  // `mem_private_bytes` contains only pages which are truly private - only one
  // VMO can access the pages and that VMO is mapped into one address space.
  //
  // `mem_shared_bytes` and `mem_scaled_shared_bytes` contain all shared pages
  // regardless of how they are shared.
  //
  // `mem_scaled_shared_bytes` scales the shared pages it encounters in two
  // steps: first each page is scaled by how many VMOs share that page via
  // copy-on-write, then each page is scaled by how many address spaces map the
  // VMO in the mapping currently being considered.
  //
  // For example, consider a single page shared between 2 VMOs P and C.
  //
  // If P is mapped into task p1 and C is mapped into tasks p2 and p3:
  // `mem_private_bytes` will be 0 for all 3 tasks.
  // `mem_shared_bytes` will be `zx_system_get_page_size()` for all 3 tasks.
  // `mem_scaled_shared_bytes` will be `zx_system_get_page_size() / 2` for p1
  // and `zx_system_get_page_size() / 4` for both p2 and p3.
  //
  // If P is mapped into task p1 and C is mapped into tasks p1 and p2:
  // `mem_private_bytes` will be 0 for both tasks.
  // `mem_shared_bytes` will be `2 * zx_system_get_page_size()` for p1 and
  // `zx_system_get_page_size()` for p2.
  // `mem_scaled_shared_bytes` will be `3 * zx_system_get_page_size() / 4` for
  // p1 and `zx_system_get_page_size() / 4` for p2.
  uint64_t mem_private_bytes;
  uint64_t mem_shared_bytes;
  uint64_t mem_scaled_shared_bytes;

  // This is a new field.
  //
  // This field is defined in the same way as the "_fractional" fields in
  // `zx_info_vmo_t`.
  uint64_t mem_fractional_scaled_shared_bytes;
} zx_info_task_stats_t;

實作

我們會將結構性核心 API 變更 (例如新增和重新命名欄位) 實作為單一初始 CL,以減少流失率。我們會將所有新欄位的值設為 0,但 _fractional_scaled 欄位會設為 UINT64_MAX 的哨兵值。

接下來,我們將變更 memory_monitor 行為,將父項 VMO 記憶體歸屬給程序,並使用 PSS _scaled_bytes 欄位,而非 RSS _bytes 欄位。_fractional_scaled 欄位中的哨兵值目前會限制這兩種向後不相容的行為。這項限制和其他新欄位中的 0 值,表示不會變更任何使用者空間行為。

接著,我們會從核心公開新的歸因行為。這項變更會改變 committed_bytes 等現有欄位的意義,而新欄位則可採用非零值。_fractional_scaled 欄位無法再採用哨兵值,因此使用者空間會自動開始使用新的行為和新欄位,以便充分利用變更。

最後,我們會從 memory_monitor 中移除對 _fractional_scaled 欄位的檢查。

我們變更的舊版 API 已淘汰。我們會在日後確認沒有任何二進位檔再參照這些檔案時,移除這些檔案。

成效

新的歸因導入作業可改善歸因查詢的成效。這項功能會減少處理 VMO 複本的次數,並避免執行耗時的檢查作業,這些檢查作業會用於在多個 VMOs 之間區分共用 COW 頁面。

之後,實作精細階層鎖定功能,可改善頁面錯誤和多個 VMO 系統呼叫 (包括 zx_vmo_readzx_vmo_writezx_vmo_op_rangezx_vmo_transfer_data) 的效能。目前,這些作業會爭用所有相關 VMOs 共用的階層鎖定,這會在相關 VMO 的複本上序列化所有這些作業。對於經過多次複製的 VMOs,序列化會變得更糟。許多 Starnix 和啟動檔案系統虛擬機器均屬於此類。

在建立及銷毀子 VMOs 時,Zircon 會執行更多工作。建立 SNAPSHOTSNAPSHOT_MODIFIED 子項時,會變成 O(#parent_pages),而非 O(1)。在某些情況下,摧毀任何類型的子項都會變成 O(#parent_pages),但在其他情況下,可能會變成 O(#child_pages)。Zircon 目前會將這項工作延後至更頻繁的作業,例如頁面錯誤。將其移至不常使用的 zx_vmo_create_child()zx_vmo_destroy() 作業,可加快其他頻繁作業的交換速度。因此,我們不預期會出現任何使用者可見的效能倒退情形。

我們會使用現有的微基準和宏基準,驗證預期的效能差異。

回溯相容性

ABI 和 API 相容性

我們會為所有核心 API 變更建立額外的版本主題,並保留對所有現有主題的支援。這麼做可在所有情況下保留 ABI 相容性,並在所有情況下保留 API 相容性 (除了一個情況)。

zx_info_maps_mapping_t 中重新命名 committed_pagespopulated_pages 是破壞 API 的變更。這些欄位只會在少數位置用於 Fuchsia 內部工具,因此重新命名這些欄位的 CL 也會變更所有呼叫位置的名稱。

其他變更則不需要更新現有的呼叫網址,除非要利用新加入的欄位。

行為相容性

Starnix、ps 和 Fuchsia 外部的幾個程式庫會計算每個 VMO 或每個工作項目的 RSS 值。新的歸因行為會多次計算 COW 共用網頁,因此這些運算會更準確。這會以不相容的方式變更 RSS 值,但只會讓 RSS 值更準確,因此我們不預期會產生任何負面影響。

memory_monitor 為父項 VMOs 歸屬記憶體的方法有正確性問題,新的歸屬行為可以修正這些問題。我們必須以不具回溯相容性的方式變更此工具的實作方式,才能實作修正項目。為避免流失,我們會將新的 _fractional_scaled 欄位設為 UINT64_MAX 的哨兵值,直到我們實作新的歸因行為為止。我們會針對此值限制 memory_monitor 的實作方式。歸因值通常不會產生這個哨兵值,因此非常適合做為「功能旗標」。

我們在 ZX_INFO_TASK_STATS 中保留「共用」的意思為「程序共用」,儘管其運作方式與其他歸因 API 不同。這可讓依賴 ZX_INFO_TASK_STATS 行為的現有程式繼續運作,而不會造成侵入性變更。未來的作業可能會將這些 ZX_INFO_TASK_STATS 用途替換為其他查詢,或許是 memory_monitor,屆時我們就能淘汰並移除 ZX_INFO_TASK_STATS

測試

核心測試套件中已有許多可驗證記憶體歸屬性的測試。

我們會新增幾項測試,驗證新歸因 API 和行為的極端情況。

說明文件

我們會更新 zx_object_get_info 的系統呼叫說明文件,反映這些變更。

我們會在 docs/concepts/memory 下方新增頁面,說明 Zircon 的記憶體歸因 API 並提供範例。

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

在擬定這項提案的過程中,我們探索了一些替代解決方案模型。大多數的做法都很容易實作,但效能或可維護性不佳。

模擬目前歸因行為

在這個選項中,我們會變更核心,在新的實作項目上模擬現有的歸因行為。

這個 API 不存在回溯相容性問題,也不需要任何 API 變更。

不過,由於需要現有的共用階層鎖定才能運作,因此會阻止使用精細階層鎖定。模擬會導致歸因查詢在更精細的鎖定策略下發生死結。這也會導致歸因查詢的效能更差,而這項功能的效能目前已令人擔憂。最後,它不會提供 Starnix 用來計算 USSRSSPSS 的資訊。

僅公開 USS 和 RSS

在這個選項中,我們會變更核心,將共用的寫入時複製頁面歸因給共用這些頁面的每個 VMO,並分別公開私人和共用頁面的歸因。

它與精細階層鎖定相容。

不過,它不會為使用者空間提供足夠的資訊,無法保留「相加為 1」屬性,也無法讓 Starnix 計算 PSS 測量。核心會使用這個選項多次歸屬 COW 共用頁面。每個 COW 共用頁面的共用次數可能因寫入模式而異,因此使用者空間需要每頁資訊,才能刪除 COW 共用頁面的重複內容,或將這些頁面平均分配給各個複本。

公開每個 VMO 樹狀結構的詳細資訊

在這個選項中,我們會變更核心,將共用的寫入時複製頁面歸因給共用這些頁面的每個 VMO,並公開個別樹狀結構和個別 VMO 的資訊:

  • 將 VMO 連結至其樹狀結構的樹狀結構 ID
  • 用於決定勝負的穩定 VMO ID,例如時間戳記
  • 每個 VMO 的私人網頁
  • 每個樹狀結構的共用網頁總數
  • 每個 VMO 可查看的每個樹狀結構共用網頁數量

它會為使用者空間提供滿足「sums to one」屬性所需的資訊。

不過,由於需要現有的共用階層鎖定才能運作,因此會阻斷我們想要進行的鎖定競爭最佳化。如未使用這類共用鎖定,就無法計算「每個樹狀結構體的共用網頁總數」。這個選項也會將核心實作細節公開給使用者空間,這會使日後維護 VM 程式碼變得更加困難。也無法提供 Starnix 用來計算 PSS 測量的資訊。

既有技術與參考資料

其他作業系統提供本 RFC 中使用的 RSSUSSPSS 測量值,但有時會使用不同的名稱。Windows、macOS 和 Linux 都會追蹤 RSSUSS。以 Linux 為基礎的作業系統會追蹤 PSS

Linux 會透過 proc 檔案系統公開這些測量值。因此,我們推出了 ps 和 top 等歸因工具,以便以友善的方式呈現這些資訊。