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
檔案系統項目中公開的 USS 和 PSS 值。
我們只會針對 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 中提議的變更。
需求條件
- 核心的記憶體歸屬實作不得透過讓 VMO 階層鎖定更精細,而導致無法在 Zircon 中最佳化鎖定爭用情形。
- 當
memory_monitor
這類工具為系統中的所有 VMOs 加總歸因時,總量必須與系統的實際實體記憶體用量一致。這就是「相加為 1」的屬性。 - Starnix 必須能夠在 Zircon 提供的 API 之上模擬 Linux 記憶體歸屬 API。這包括在相關的 proc 檔案系統項目中提供 USS、RSS 和 PSS 等測量值。
設計
我們提出的變更會影響 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 加總,即可測量總系統記憶體用量。
我們選擇透過公開所有 USS、RSS 和 PSS 來變更歸因 API,因為這是我們嘗試的替代方案中唯一符合所有需求的選項。
對使用者空間的影響
此提案會繼續保留現有使用者空間工具所依賴的「相加為一」屬性。在新的行為下,歸因查詢可能會產生不同的結果,但系統會繼續對系統中所有 VMOs 的查詢求和,以便提供記憶體使用量的正確計數。這些變更不會導致 memory_monitor
和 ps
等程式開始過度或低估記憶體,反而會讓每個 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_VMO
和zx_info_vmo_t
,並變更zx_info_vmo_t
所有版本中兩個現有欄位的含義:
- 我們會透過以下方式維持 ABI 和 API 的回溯相容性:
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
結構體家族,因此所有對這些結構體的變更都會套用至此。
- 本主題會重複使用
- 我們會透過以下方式維持 ABI 和 API 的回溯相容性:
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_pages
和populated_pages
是一項破壞性變更。請參閱下方的「向下相容性」。
- 重新命名
- 我們將新增
ZX_INFO_PROCESS_MAPS
、zx_info_maps_t
和zx_info_maps_mapping_t
,並變更現有版本zx_info_maps_mapping_t
中兩個欄位的含義。這些欄位不會出現在新版的結構體中 (請見下方):
- 我們會透過以下方式維持 ABI 回溯相容性:
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_STATS
和zx_info_task_stats_t
,並變更zx_info_task_stats_t
結構體所有版本中三個現有欄位的含義:
- 我們會透過以下方式維持 ABI 和 API 的回溯相容性:
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_read
、zx_vmo_write
、zx_vmo_op_range
和 zx_vmo_transfer_data
) 的效能。目前,這些作業會爭用所有相關 VMOs 共用的階層鎖定,這會在相關 VMO 的複本上序列化所有這些作業。對於經過多次複製的 VMOs,序列化會變得更糟。許多 Starnix 和啟動檔案系統虛擬機器均屬於此類。
在建立及銷毀子 VMOs 時,Zircon 會執行更多工作。建立 SNAPSHOT
和 SNAPSHOT_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_pages
和 populated_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 用來計算 USS、RSS 或 PSS 的資訊。
僅公開 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 中使用的 RSS、USS 和 PSS 測量值,但有時會使用不同的名稱。Windows、macOS 和 Linux 都會追蹤 RSS 和 USS。以 Linux 為基礎的作業系統會追蹤 PSS。
Linux 會透過 proc 檔案系統公開這些測量值。因此,我們推出了 ps 和 top 等歸因工具,以便以友善的方式呈現這些資訊。