| RFC-0254:變更寫入時複製頁面的歸因 | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 變更 Zircon 公開的 API 和語意,以指派寫入時複製的頁面。 |
| 問題 | |
| Gerrit 變更 | |
| 作者 | |
| 審查人員 | |
| 提交日期 (年-月-日) | 2024-08-02 |
| 審查日期 (年-月-日) | 2024-07-29 |
問題陳述
Zircon 目前公開的 VMO 記憶體歸因行為,在兩方面都對 Starnix 核心造成問題:
首先,Zircon 目前的記憶體歸因行為只與 Zircon VMO 實作中廣泛共用的鎖定相容。執行大量 Starnix 程序時,這個共用鎖定會導致一些效能問題。
Starnix 會使用 VMO 備份每個 MAP_ANONYMOUS Linux 對應,並在 Linux 程序發出 fork() 呼叫時複製所有這類 VMO。如果 Linux 程序數量眾多,就會導致許多 VMO 爭用一小組共用鎖定。我們在原型中進行記憶體節省最佳化時,Starnix 會為程序中的所有對應使用一個大型 VMO,而共用鎖定會退化為「全域」鎖定,所有 Starnix VMO 都會爭用這個鎖定。
其次,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
社交:
這項 RFC 已在與 Zircon、Starnix 和記憶體團隊利害關係人的對話中公開。利害關係人也審查了設計文件,其中列出這份 RFC 中提議的變更。
需求條件
- 核心的記憶體歸因實作項目不得透過更精細的 VMO 階層鎖定,防止 Zircon 最佳化鎖定爭用。
- 當系統中的所有 VMO 採用
memory_monitor總和歸因等工具時,總和必須與系統的實際實體記憶體用量一致。這就是「總和為一」的屬性。 - Starnix 必須能夠在 Zircon 提供的 API 基礎上,模擬 Linux 記憶體歸因 API。包括在相關的 proc 檔案系統項目中提供 USS、RSS 和 PSS 測量結果,以及其他測量結果。
設計
我們提議的變更會影響 Zircon 屬性頁面在 VMO 複製項目之間透過寫入時複製的共用方式。這些是 COW 私有和 COW 共用頁面。
我們不會建議變更 Zircon 屬性頁面在程序間的共用方式,例如透過記憶體對應或重複的 VMO 控制代碼。這些是程序專用和程序共用的頁面。
使用者空間負責在適當情況下,將程序共用的頁面歸因於一或多個程序。Zircon 一般不會在決策時考慮程序共用,但有一個例外。詳情請參閱「回溯相容性」。
共用類型彼此獨立,特定頁面可以是 COW 私有、COW 共用、程序私有和程序共用的任意組合。
Zircon 的現有行為
Zircon 現有的歸因行為會提供單一 VMO 測量結果:來自 VMO COW 私有和 COW 共用頁面的歸因記憶體,每個頁面都有專屬歸因。
- 這項機制會將 COW 共用頁面獨一無二地歸因於最舊的現有 VMO,該 VMO 可以參照這些頁面。COW 共用頁面有時會在同層級 VMO 之間共用,因此參照頁面的最舊 VMO 不一定是其他 VMO 的父項。
- 將所有 VMO 的記憶體用量加總,即可得出系統的總記憶體用量。
- 這項指標不會評估 VMO 的 USS,因為可能包含部分 COW 共用頁面。
- 此外,由於 VMO 的 RSS 可能未包含所有 COW 共用頁面,因此系統也不會評估這項指標。
- 這項指標不會測量 VMO 的 PSS,因為這項指標會根據網頁的共用次數,調整每個網頁的貢獻度。
Zircon 的新行為
Zircon 的新歸因行為提供三種 VMO 評估方式:
- USS:來自 VMO COW 私人頁面的歸因記憶體。這項評估只會將網頁歸因於該 VMO。這項指標會測量 VMO 遭到毀損時釋放的記憶體。
- RSS:來自 VMO 的 COW 私人頁面和 COW 共用頁面的記憶體。這項指標會將 COW 共用頁面歸因於共用這些頁面的每個 VMO,因此會多次計算 COW 共用頁面。這項指標會測量 VMO 參照的記憶體總量。
- PSS:來自 VMO 的 COW 私有和 COW 共用頁面的已歸因記憶體。這項評估方式會將每個 COW 共用網頁的歸因,平均分配給共用該網頁的所有 VMO。這可能是特定 VMO 的位元組分數,因為每個 VMO 都會取得每個共用頁面位元組的相同分數。這項指標會測量 VMO 的「比例」記憶體用量。將所有 VMO 的記憶體用量加總,即可得出全系統的記憶體用量總計。
我們選擇公開所有 USS、RSS 和 PSS,藉此變更歸因 API,因為在我們探索的替代方案中,只有這個選項符合所有需求。
對使用者空間的影響
這項提案會繼續保留「總和為一」的屬性,現有使用者空間工具就是依據這項屬性運作。在新版行為模式下,歸因查詢可能會產生不同的結果,但將系統中所有 VMO 的查詢加總,仍可準確計算記憶體用量。這些變更不會導致 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「加總為一」,但可能會導致部分結果不正確。舉例來說,它可以將父項 VMO 的部分 COW 私人頁面指派給未參照該 VMO 的程序,因此這些程序無法參照這些頁面。此外,當系統與部分子項共用父項 COW 共用頁面時,也會錯誤地縮放這些頁面。我們的變更會讓這項行為變得多餘,因此我們將移除這項行為。如果多個程序直接參照 VMO,工具仍會調整 VMO 的記憶體。詳情請參閱「向後相容性」。
系統呼叫 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 副本的次數會減少,也能避免在多個 VMO 之間消歧義共用 COW 頁面時,進行耗費資源的檢查。
稍後,實作細部階層鎖定功能後,頁面錯誤和多個 VMO 系統呼叫 (包括 zx_vmo_read、zx_vmo_write、zx_vmo_op_range 和 zx_vmo_transfer_data) 的效能將有所提升。目前這些作業會爭用所有相關 VMO 共用的階層鎖定,導致相關 VMO 副本上的所有作業序列化。如果 VMO 經過多次複製,序列化效果會大幅降低。許多 Starnix 和啟動檔案系統 VMO 都屬於這個類別。
建立及銷毀子 VMO 時,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 值,但只會讓這些值更加準確,因此我們預期不會有任何負面影響。
memory_monitor 的父項 VMO 記憶體歸因方法有正確性問題,而新的歸因行為可以修正這些問題。為實作修正,我們必須以不具回溯相容性的方式變更這項工具的實作方式。為避免流失,我們會將新的 _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 變更。
不過,由於需要現有的共用階層鎖定才能運作,因此會封鎖使用精細階層鎖定。模擬作業會導致在更精細的鎖定策略下,歸因查詢發生死結。這也會導致歸因查詢的效能更差,而歸因查詢的效能目前已令人擔憂。最後,這項資訊無法提供 Starnix 計算 USS、RSS 或 PSS 測量結果所需的資訊。
僅公開 USS 和 RSS
在這個選項中,我們會將核心變更為將寫入時複製的共用頁面歸因於共用這些頁面的每個 VMO,並分別公開私人和共用頁面的歸因。
與細微的階層鎖定相容。
不過,這無法為使用者空間提供足夠資訊,以保留「總和為一」屬性,或供 Starnix 計算 PSS 測量值。核心會使用這個選項多次複製屬性 COW 共用頁面。每個 COW 共用頁面可共用的次數取決於寫入模式,因此使用者空間需要每個頁面的資訊,才能重複使用 COW 共用頁面,或在複製項目之間平均分配這些頁面。
公開每個 VMO 樹狀結構的詳細資訊
在這個選項中,我們會將核心變更為將屬性共用的寫入時複製頁面歸因於共用這些頁面的每個 VMO,並公開每個樹狀結構和每個 VMO 的資訊:
- 樹狀結構 ID,可將 VMO 連結至樹狀結構
- 用於打破僵局的穩定 VMO 專屬 ID,例如時間戳記
- 每個 VMO 的私人頁面數
- 每個樹狀結構的共用網頁總數
- 每個 VMO 可見的每個樹狀結構共用頁面數量
這個函式會提供使用者空間,以及滿足「總和為一」屬性所需的資訊。
不過,由於需要現有的共用階層鎖定才能運作,因此會封鎖我們想進行的鎖定爭用最佳化。如果沒有這類共用鎖定,就無法計算「每個樹狀結構的共用網頁總數」。這個選項也會將核心實作詳細資料公開給使用者空間,導致日後維護 VM 程式碼更加困難。也無法提供 Starnix 計算 PSS 測量結果所需的資訊。
既有技術和參考資料
其他作業系統也提供本 RFC 中使用的 RSS、USS 和 PSS 測量值,但有時會使用不同名稱。Windows、macOS 和 Linux 都會追蹤 RSS 和 USS。Linux 作業系統會追蹤 PSS。
Linux 會透過 proc 檔案系統公開這些測量結果。ps 和 top 等歸因工具會以簡單易懂的方式呈現這些資訊。