RFC-0219:Zircon Page 壓縮

RFC-0219:Zircon 頁面壓縮
狀態已接受
區域
  • Kernel
說明

在匿名 VMO 頁面的核心壓縮中。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2022-05-31
審查日期 (年-月-日)2023-06-03

摘要

本文建議為匿名使用者記憶體新增核心內壓縮和解壓縮系統。

雖然系統位於核心內,但由於會影響記憶體帳戶,因此對使用者空間而言並非完全透明。但除了 API 異動外,所有功能異動都包含在核心中。

提振精神

Fuchsia 經常在資源受限的裝置上使用,增加記憶體集區可大幅提升使用者體驗。

記憶體核心壓縮是其他作業系統使用的標準技術,支援這項技術有助於 Fuchsia 達到同等水準。

利害關係人

協助人員:

cpu@google.com

審查者:

eieio@google.com、rashaeqbal@google.com、maniscalco@google.com

已諮詢:

mseaborn@google.com、mvanotti@google.com、wez@google.com、plabatut@google.com、 jamesr@google.com、davemoore@google.com

社交:

將核心設計和實作部分的 RFC 前提案文件,分享給 Zircon 核心團隊和更廣泛的利害關係人。

設計

本節將概要說明實作的設計選擇、壓縮策略,以及建議的 API 變更和擴充功能。

老化和追蹤

匿名頁面的老化處理方式應與有頁面支援的頁面類似,因此:

  • 在匿名頁面中加入反向連結,以便執行 vm_page_tVmObjectPaged + 偏移查閱。每當網頁在 VMO 中移動時,都需要更新反向連結,這會增加成本。
  • 變更頁面佇列,讓系統不會以不同方式處理分頁備份和匿名頁面。

網頁佇列中的老化作業已有現行的有效和無效集合概念。 現有的逐出作業絕不會從有效集逐出,以避免出現顛簸狀況。匿名頁面會加入相同的佇列,並受到相同的壓縮限制。

如果頁面經過壓縮,系統會從頁面佇列追蹤中移除這些頁面,與遭逐出的頁面類似。如果頁面無法壓縮,系統會將其移至另一個清單,避免再次嘗試壓縮。如果存取這類頁面,系統會將其移回有效集,並再次嘗試壓縮。

雖然系統只會壓縮閒置網頁,但我們支援三種觸發條件:

  • 處理 LRU 佇列時,即使沒有記憶體壓力,也會急切壓縮舊頁面。
  • 在記憶體壓力下壓縮,並執行逐出作業。
  • 壓縮,以免發生 OOM 錯誤。

這些觸發條件會透過核心指令列標記切換。

VmPageList 標記

VmPageList 商店需要有額外的標記類型,可執行下列操作:

  • 指出網頁存在,但已壓縮。
  • 儲存足夠的中繼資料,以便在壓縮儲存空間中找到網頁。

VmPageList 的項目已編碼,可同時儲存 vm_page_t 指標和零頁面標記,並可擴充為額外編碼。

目前網頁清單項目是 64 位元的值,會編碼為下列選項:

  • 0:空白項目
  • 最低有效位元 = 1:零頁面標記
  • 其他項目:指向 vm_page_t 的指標

由於指標對齊,這個配置會運作,因此 vm_page_t 指標的底部 3 位元一律為零。因此,我們可以將這個配置一般化,讓頁面清單項目的底部 3 位元代表類型,其餘位元則為相關聯的資料。初始類型如下:

  • 0:空白項目或 vm_page_t 指標 (視資料位元是否已設定而定)
  • 1:零頁面標記。剩餘資料位元必須為 0。
  • 2:壓縮網頁。其餘位元是壓縮系統傳回的不透明權杖。
  • 3-7:目前類型無效。

壓縮作業

為著重於簡單有效率的初始使用案例,初始儲存空間策略會執行單一頁面壓縮和解壓縮,不會依 VMO 明確分組頁面。

不依 VMO 分組網頁,可讓壓縮路徑以與現有逐出系統相同的方式饋送。雖然不同 VMO 的頁面會有效地混雜在單一儲存系統中,但嚴格來說,不同 VMO 之間不得建立鎖定依附元件。這是為了防止透過時間通道洩漏資訊,以及避免因延遲時間過長而懲處無關的程序。

為避免不必要的鎖定爭用,執行壓縮或解壓縮演算法時,不應保留 VMO 鎖定,且壓縮作業不應保留解壓縮所需的任何鎖定。

壓縮演算法

以下是壓縮路徑的概略說明,省略了錯誤處理案例。壓縮的輸入內容是頁面、該頁面所屬的 VMO,以及該頁面在 VMO 中的偏移。取得 VMO 鎖定後,系統會使用偏移量驗證頁面是否仍在 VMO 中,這屬於錯誤處理,否則會在這個虛擬程式碼中略過。

Take VMO lock
Update page list to use temporary compression reference
Release VMO lock
Compress page to buffer
Take compressed storage lock
Allocate storage slot
Release storage lock
Copy buffer to storage
Take VMO lock
Check page list still holds temporary compression reference
Update page list to final compression reference
Release VMO lock

一開始使用暫時參照而非立即分配參照,是為了在產生參照時提供資料大小,讓壓縮儲存系統在產生參照時更具彈性。這樣一來,資料表就能代表參照中的確切資料位置,不必透過額外資料表間接參照。

即使未使用暫時參照,仍需取得兩次 VMO 鎖定,才能解決壓縮期間使用網頁時可能發生的任何競爭條件。舉例來說,頁面 (在本例中為參照) 可能在壓縮期間取消認可,除非我們再次取得 VMO 鎖定並檢查,否則不會知道這點。

壓縮期間,由於 VMO 鎖定未保留,因此另一個執行緒可能會找到暫時參照並嘗試使用。由於臨時參照實際上沒有可解壓縮的資料,因此我們有兩種解決方法:

  • 等待壓縮完成,然後擷取原始網頁。
  • 複製原始頁面 (這項作業的記憶體會在壓縮作業開始時預先分配)。

這是為了確保網頁在整個壓縮過程中都處於已知狀態,且由您擁有。在壓縮作業進行期間使用原始頁面會造成問題,原因如下:

  • 在壓縮演算法讀取網頁時修改網頁,可能會增加壓縮演算法的潛在錯誤和攻擊面,因為壓縮演算法可能無法因應並行的資料變更。
  • VMO 的可快取性或其他屬性可能已變更,導致壓縮器並行快取使用量出現未定義的行為。

這裡的建議是複製原始頁面,因為這種情況不太可能發生,而複製頁面可更快解決要求,並避免需要其他同步機制。這裡的複製作業是在 VMO 鎖定下進行,但這與所有其他寫入時複製路徑一致,因為頁面副本可能是在 VMO 鎖定下建立。由於一次只會進行單一壓縮作業,因此任何 VMO 作業最多只會遇到一次這種情況。

解壓縮演算法

實作解壓縮功能是為了運用現有的 PageRequest 系統,該系統已用於延遲分配作業和滿足使用者分頁要求。使用者分頁 VMO 的缺席頁面會產生頁面要求,VMO 頁面清單中的壓縮參照也會觸發類似程序:

Take VMO lock
Observe page is compressed
Fill in the PageRequest
Drop VMO lock
Wait on the PageRequest
Retry operation

填寫頁面要求時,我們需要儲存頁面清單中的壓縮參照,以及 VMO 和偏移。這需要擴增現有的 PageRequest 結構。

這與使用者分頁要求和壓縮的差異在於,等待行為可以直接解決要求,在此情況下是指執行解壓縮。

解壓縮作業必須能容許多個執行緒嘗試存取同一頁面,並提供優先順序繼承的流動方式:

Declare stack allocated OwnedWaitQueue
Take compressed storage lock
Validate compressed reference
if compressed page has wait queue then
    take thread_lock
    release compressed storage lock
    wait on referenced wait queue
else
    store reference to our allocated wait queue in compressed block
    release compressed storage lock
    decompress into new page
    take VMO lock
    update VMO book keeping
    release VMO lock
    take compressed storage lock
    take thread lock
    remove wait queue reference
    release compressed storage lock
    wake all threads on wait queue

這裡使用 thread_lock 只是因為這是執行等待佇列作業時必須持有的鎖定,並未運用持有 thread_lock 的其他屬性。

目標壓縮大小

由於解壓縮會產生相關費用,因此只有在網頁大小大幅縮減時,才值得儲存壓縮版本。否則,儲存未壓縮版本可能更划算,因為這樣可以省下解壓縮費用。

這個目標大小應為可調整的選項,不過在其他系統中,70% 的目標很常見。

如果壓縮嘗試無法符合這項目標,就會視為失敗,並如老化和追蹤一節所述,將網頁放回網頁佇列。

零頁掃描

由於壓縮作業基本上需要檢查來源網頁中的所有位元組,因此可藉此偵測及重複資料刪除零網頁。這代表壓縮的替代成功結果,其中會回報輸入頁面只是零頁面,且不會將結果儲存在壓縮儲存系統中。VMO 呼叫端可以改用零頁面標記取代頁面。

現成的壓縮演算法實作通常無法回報輸入內容全為零,因此如要瞭解這點,必須採取下列任一做法:

  • 修改或建立實作項目,以執行這項追蹤作業。
  • 將壓縮結果與已知簽章進行比較。

第二個選項是利用壓縮演算法的確定性,通常在低數十位元組中具有零頁面表示法。因此,先執行大小檢查,再執行記憶體比較檢查,可能就已足夠便宜。

這項提案不需要初始壓縮實作項目來執行零頁面偵測,但必須建構 API 和 VMO 邏輯來支援這項功能。最終應以壓縮支援取代現有的零頁面掃描器。

壓縮演算法

建議使用 LZ4 做為初始壓縮演算法。詳情請參閱「實驗性評估」一節,但請注意,這項功能有以下重要規定:

  • 適用於小型檔案 (4KiB)。
  • 在穩定後初始化作業期間,不會執行 malloc 或同等作業。
  • 可平行解壓縮,且沒有共用的可變動狀態。壓縮作業必須能與解壓縮作業並行。
  • 雖然並非必要,但系統支援多個平行壓縮。

此外,LZ4 還有一個實用屬性,就是能根據受限的目標緩衝區大小,提早中止壓縮作業。

壓縮後的儲存空間

儲存壓縮網頁時,程式碼複雜度與執行階段之間會有所取捨,可能導致片段化和記憶體用量增加。一開始的策略是將固定數量的壓縮頁面 (本例為三個) 封裝到單一儲存頁面中。

這項儲存策略的核心概念是,每個儲存頁面都可視為有左、中、右三個儲存空間,因此最多可達到 3:1 的壓縮比。

導入程序相當簡單,包含下列步驟:

  • 搜尋任何現有部分填寫的頁面,找出可用的時段。
  • 如果沒有合適的現有版位,請放置在新網頁的左側版位。

這兩項複雜功能分別是:

  • 部分填滿的頁面應根據其可用空間大小,分組到搜尋清單中,以便使用 O(1) 搜尋進行近乎最佳的封裝。
  • 如果左側或右側的資料量夠大,中間位置可能會無法使用。

這類策略的優點包括:

  • 導入方式輕鬆簡單。
  • 可預測的作業,不會出現長尾效能。
  • 最差的情況是完全不使用額外記憶體。

可能需要處理的一種病態行為是,網頁解壓縮後可能存在可填補的洞,並釋放其他儲存空間網頁。如有需要,可以實作這類壓縮系統。

從長遠來看,這可能會演變成或使用更通用的堆積或 Slab 樣式分配器。

如需初步結果,請參閱實驗評估部分。

帳戶和指標

雖然壓縮作業不需要任何使用者輸入內容,但會影響記憶體用量的回報方式。我們也想將額外資訊外洩到使用者空間,瞭解壓縮作業的執行情況。

內容與已提交

現有的查詢介面會將已提交的位元組和頁面視為直接在 VMO 中分配的實際記憶體,用於保存資料。

這些現有查詢包括:

  • ZX_INFO_TASK_STATS,回報已提交的記憶體 (以 mem_private_bytesmem_shared_bytes 等為單位)。
  • ZX_INFO_PROCESS_MAPS,其中會回報對應 VMO 的 committed_pages
  • ZX_INFO_PROCESS_VMOS / ZX_INFO_VMO,回報 committed_bytes
  • ZX_INFO_KMEM_STATS / ZX_INFO_KMEM_STATS_EXTENDED,這些報表會計為已提交。vmo_bytes

除了已提交的內容位元組/網頁,我們還會新增內容位元組/網頁的概念。凡是提及已提交記憶體的查詢,仍會繼續套用至直接在 VMO 中保存內容的記憶體。內容是指核心為使用者保留的資料量,不具體說明保留方式。也就是說,內容可直接保留在頁面中,因此也會計入已提交的內容,或是壓縮後不計入已提交的內容。

以這種規模來說,壓縮內容的儲存空間大小不明。由於儲存空間大小取決於系統的整體壓縮行為和產生的片段,因此嘗試在儲存空間中歸因個別壓縮頁面會很複雜且不穩定。

雖然匿名 VMO 的起始值為名義上的零,但這不算是核心記憶的內容。如果使用者明確提交或修改任何頁面,該頁面就會成為內容。之後,即使使用者離開或將內容重設為零,核心也不會注意到這點,可能會繼續將其視為追蹤的內容。

就逐出和使用者分頁器而言,可從使用者分頁器擷取的內容不視為核心追蹤的內容。因此,逐出會從已承諾和內容金額中扣除。

指標

為了瞭解壓縮作業的執行情況,以利調整 / 開發及持續監控系統健康狀態,我們需要收集並提供指標。

這些指標應能回答壓縮作業的執行情況,以及對系統效能的影響。具體來說,我們想瞭解以下事項:

  • 壓縮儲存空間比率。
  • 網頁壓縮時間長度。
  • 哪些類型的網頁會壓縮,以及觸發壓縮的原因。
  • 解壓縮延遲時間。
  • 執行壓縮和解壓縮作業所花費的 CPU 時間。
  • 壓縮嘗試成功與失敗的比例。

API 變更

透過結構演變和查詢版本控管機制擴充:

  • zx_info_maps_mapping_t 具有 content_pages 欄位。
  • zx_info_vmo_t 具有 content_bytes 欄位。
  • ZX_INFO_TASK_RUNTIME,以回報 page_fault_decompress_time 為總 page_fault 時間的子集。

需要為這些項目進行版本管理的 zx_object_get_info 查詢如下:

  • ZX_INFO_PROCESS_MAPS
  • ZX_INFO_PROCESS_VMOS
  • ZX_INFO_VMO
  • ZX_INFO_TASK_RUNTIME

現有欄位 (例如 committed_bytes 中的 zx_info_vmo_t) 的註解和說明文件將會更新,以明確指出這些欄位不會計算處於壓縮狀態的頁面。

確切內容將在疊代階段中確定,但建議新增 ZX_INFO_KMEM_STATS_COMPRESSION 查詢來產生報表:

struct zx_info_kmem_stats_compression {
    // Size in bytes of the content that is current being compressed.
    uint64_t uncompressed_content_bytes;

    // Size in bytes of all memory, including metadata, fragmentation and other
    // overheads, of the compressed memory area. Note that due to base book
    // keeping overhead this could be non-zero, even when
    // |uncompressed_content_bytes| is zero.
    uint64_t compressed_storage_bytes;

    // Size in bytes of any fragmentation in the compressed memory area.
    uint64_t compressed_fragmentation_bytes;

    // Total amount of time compression has spent on a CPU across all threads.
    // Compression may happen in parallel and so this can increase faster than
    // wall clock time.
    zx_duration_t compression_time;

    // Total amount of time decompression has spent on a CPU across all threads.
    // Decompression may happen in parallel and so this can increase faster than
    // wall clock time.
    zx_duration_t decompression_time;

    // Total number of times compression has been done on a page, regardless of
    // whether the compressed result was ultimately retained.
    uint64_t total_page_compression_attempts;

    // How many of the total compression attempts were considered failed and
    // were not stored. An example reason for failure would be a page not being
    // compressed sufficiently to be considered worth storing.
    uint64_t failed_page_compression_attempts;

    // Number of times pages have been decompressed.
    uint64_t total_page_decompressions;

    // Number of times a page was removed from storage without needing to be
    // decompressed. An example that would cause this is a VMO being destroyed.
    uint64_t compressed_page_evictions;

    // How many pages compressed due to the page being inactive, but without
    // there being memory pressure.
    uint64_t eager_page_compressions;

    // How many pages compressed due to general memory pressure.
    uint64_t memory_pressure_page_compressions;

    // How many pages compressed due to attempting to avoid OOM or near OOM
    // scenarios.
    uint64_t critical_memory_page_compressions;

    // The nanoseconds in the base unit of time for
    // |pages_decompressed_within_log_time|.
    uint64_t pages_decompressed_unit_ns;

    // How long pages spent compressed before being decompressed, grouped in log
    // buckets. Pages that got evicted, and hence were not decompressed, are not
    // counted here. Buckets are in |pages_decompressed_unit_ns| and round up
    // such that:
    // 0: Pages decompressed in <1 unit
    // 1: Pages decompressed between 1 and 2 units
    // 2: Pages decompressed between 2 and 4 units
    // ...
    // 7: Pages decompressed between 64 and 128 units
    // How many pages are held compressed for longer than 128 units can be
    // inferred by subtracting from |total_page_decompressions|.
    uint64_t pages_decompressed_within_log_time[8];
};

如要回報網頁壓縮時間,就必須記錄每個壓縮網頁的時間戳記。這不應對潛在壓縮比造成重大影響,但可讓您瞭解網頁是否正在快速解壓縮。

ZX_INFO_KMEM_STATS_EXTENDED 分頁器位元組

ZX_INFO_KMEM_STATS_EXTENDED 查詢有專門與分頁支援 VMO 中的位元組數量相關的欄位。這些欄位包括:

    // The amount of memory committed to pager-backed VMOs.
    uint64_t vmo_pager_total_bytes;

    // The amount of memory committed to pager-backed VMOs, that has been most
    // recently accessed, and would not be eligible for eviction by the kernel
    // under memory pressure.
    uint64_t vmo_pager_newest_bytes;

    // The amount of memory committed to pager-backed VMOs, that has been least
    // recently accessed, and would be the first to be evicted by the kernel
    // under memory pressure.
    uint64_t vmo_pager_oldest_bytes;

提議的實作方式會統一頁面佇列中的分頁備份和匿名 VMO 頁面,因此提供這項資訊並不實際。因此,提案是將這些定義重新定義為所有可分頁 / 可回收的記憶體,而不僅是可明確逐出使用者分頁支援的 VMO。否則系統會保留總計、最新和最舊的定義。

雖然可壓縮記憶體的回收作業不會直接轉換為 PMM 可用記憶體增加量,但這些查詢並非要代表可回收的 PMM 記憶體確切數量。而是提供記憶體在各年齡層的相對分配情形,並可用於執行驗證,例如 oldest_bytes 下降且在裝置 OOM 時接近零等。

為使用者分頁備份可清除的記憶體和可壓縮的匿名記憶體提供多個欄位,價值也令人存疑。在某些情況下,能夠區分這兩者很有價值,但大多數時候,我認為您還是會想要這兩者的匯總報表。

停用壓縮 / 延遲時間敏感的 VMO

對於延遲敏感型應用程式,必須有停用 VMO 壓縮的機制,因為無論解壓縮延遲多麼短,都可能超出其容許範圍。

避免回收和其他可能增加記憶體存取延遲的 Kernel 活動,是目前正在解決的問題。因此,這裡不會提出任何設計,但這項工作是壓縮作業的必要條件。

擴充功能

除了這項初步的設計提案,我們還可進行一些探索和改良。雖然這些通常屬於實作詳細資料,但我們在此列出,是為了說明這項設計的長期適用性。建議在產品上使用前完成的改善項目包括:

  • 移除個別的零頁面重複資料刪除作業,改用壓縮功能。

此外,您也可以考慮以下做法,或許能帶來實質效益:

  • 在強制清除分頁備份記憶體前,先壓縮記憶體。
  • 支援執行壓縮作業,直到記憶體壓力過大時,才捨棄未壓縮的網頁。
  • 走訪其他舊網頁清單,並積極壓縮網頁,而不只是 LRU 佇列中的網頁。

此外,您也可以進一步壓縮儲存空間和調整壓縮演算法,直接演進或為不同產品提供選項。視侵入性或差異程度而定,這些可能需要個別的 RFC。

實作

導入程序會分成多個階段,詳情請參閱本文。

常見的鷹架

我們會先實作常見的 VM 系統變更,支援匿名網頁的淘汰和追蹤。這些變更不會影響行為或 API,但風險最高,因此建議優先進行,以便:

  • 盡可能長時間在樹狀結構中進行變更並測試。
  • 能夠評估候選壓縮集區 (即舊的匿名網頁)。

核心實作

完成常見變更後,主要核心實作項目即可透過功能標記登陸樹狀結構。這樣一來,您就可以先登陸並測試壓縮實作項目,確保其穩定性,再透過任何 API 公開其存在。

API 演進

針對指標和會計變更執行 API 演進和結構遷移。由於核心實作項目位於樹狀結構中,因此可以連結 API 變更,但壓縮功能仍會受到功能標記限制,因此除了額外欄位外,一般使用者不會看到任何功能變更。

整合、擴充功能和微調

測試導入作業並回報指標後,即可評估不同產品,找出任何問題,並進行擴充和調整。

這項測試的結果將決定:

  • 壓縮功能預設為開啟,部分產品會停用這項功能,或
  • 壓縮功能預設為關閉,部分產品會選擇啟用。

擴充功能和調整

現在可以推出擴展設計的部分內容,一般實作作業也會持續調整。

效能

您需要評估兩個方面的成效。

常見的額外負荷

即使停用壓縮功能,仍須對常見的 VM 程式碼進行基本變更,而這些變更會影響效能。

預期將反向連結新增至匿名頁面,會為所有 VMO 複製和毀損事件增加可評估的額外負荷。這些作業並非效能關鍵路徑,因為通常會在初始化和終止步驟期間發生,但仍應檢查對產品的影響。

其他 VMO 路徑會產生少量額外負荷,以維護反向連結和更新年齡,但這應該屬於雜訊,不會造成可衡量的影響。

現有的 VMO 微基準測試涵蓋這些路徑,且將做為主要驗證工具。

壓縮效能

壓縮的本質是 CPU 與記憶體的取捨,因此只能根據產品及其需求評估效能。

否則,建議的指標 API 可協助您瞭解壓縮作業的費用,並根據節省的記憶體量,使用壓縮可調參數控制這些費用。

安全性考量

時間碼通道

雖然壓縮頁面可能具有共置儲存空間,但存取這個儲存空間的程序定義為:VMO 上沒有可導致可測量管道的遞移依附元件。這是刻意為之,目的是為了避免類似記憶體重複資料刪除攻擊的攻擊。

即使缺少遞移依附元件,如果攻擊者可以控制與密碼位於同一網頁的資料,仍有可能建立時間管道。這類資料可能會經過設計,讓壓縮作業根據密鑰成功或失敗,進而讓攻擊者瞭解密鑰的相關資訊。目前尚不清楚這類攻擊是否可行,但如果擔心發生這種情況,可以採用與防止延遲敏感工作壓縮相同的機制。

系統行為管道

您可以透過 ZX_INFO_KMEM_STATS_COMPRESSION 直接查詢整體系統記憶體用量,也可以查詢 VMO 等物件,並觀察內容和已提交位元組之間是否有差異,藉此推斷整體系統記憶體用量。您也可以查詢 ZX_INFO_KMEM_STATS_COMPRESSION 中的其他統計資料。

這項資訊可間接瞭解整體系統行為。不過,我們認為光是知道您控管的 VMO 網頁是否經過壓縮,並不足以瞭解其他程序。

LZ4

雖然 LZ4 程式庫的檔案數量和 LoC 指標較少,但仍是以 C 的低階樣式編寫的非簡單程式碼,可能含有錯誤。圖書館需要混合使用下列項目:

  • 整體適用性安全審查。
  • 額外強化和測試。
  • 可能需要重寫有問題的內容。

強化和重寫的組合可能會導致部分或所有實作項目移植到更安全的表示法,例如 Rust 或 WUFFS。

測試

與成效類似,測試也有兩個維度。

VM 正確性

我們會透過單元測試和整合測試,評估一般 VM 變更和壓縮特定變更的技術正確性。

對於一般 VM 變更,無論核心單元測試或額外核心測試是否啟用壓縮功能,都會受到影響,因此會視情況新增。

使用 QEMU 執行器進行整合測試,以便在啟用壓縮功能的情況下執行額外測試。

系統行為

雖然壓縮並非免費,但不得對產品負責人認為重要的任何行為造成負面影響。這些行為會因產品而異,但舉例來說,在記憶體不足的情況下,可能會發生音訊跳轉、加劇的顛簸,以及不良的使用者體驗。

這包括手動測試產品、使用鎖定爭用追蹤等工具,以及與產品測試團隊和產品負責人合作進行評估。

說明文件

zx_object_get_info 查詢的異動和新增內容,以及其他指令列選項,都會記錄在文件中。

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

未知數和反覆運算

這項提案的確切優點和成本,必須等到在實際產品上妥善實作及評估後,才能完全瞭解。雖然本提案盡量為所有演算法和結構提供具體合理的初始提案,但這些提案幾乎一定會根據實際使用資料而有所變動。

缺點

這項提案的主要缺點是,核心中的 VM 系統會變得更加複雜。雖然只要停用壓縮功能,就能避免任何效能問題,但壓縮功能的存在需要變更常見的基礎架構。因此,即使未使用壓縮功能,仍會永久存在複雜性和風險。

使用者分頁壓縮替代方案

透過使用者分頁機制,壓縮和解壓縮作業可外包給使用者空間,不必在核心中執行。在許多方面,這與 Linux 上的 ZRAM 類似,都是由使用者空間檔案系統驅動程式庫程式實作。但很可惜,這項功能也有許多相同的缺點。

優點

在使用者空間中進行壓縮主要有兩個原因:

  1. 從核心中移除複雜的壓縮和解壓縮程式碼。
  2. 提供彈性,方便使用者導入。

很抱歉,(1) 並非完全正確,因為除非為每個安全網域建立個別的密封執行個體 (無論如何判斷),否則這個使用者空間程序可以查看系統中每個程序的匿名記憶體。由於任何安全漏洞都可能導致系統中任何使用者記憶體遭到查看及編輯,因此這項功能必須極度值得信賴。因此,與核心相比,信任此處的壓縮和解壓縮程式碼的門檻不會較低。

提供彈性的實作方式當然值得,但如果能將特定匿名 VMO 指派給特定壓縮器,會更具吸引力。這時甚至可能不需要壓縮,但可以實作交換或其他策略。

缺點

使用者空間壓縮的主要缺點是會對核心造成限制。能夠樂觀壓縮及解壓縮網頁 (甚至可能不必捨棄未壓縮的網頁),可讓核心獲得極大的實作彈性。所有這類架構都可以透過使用者分頁器實作,但這會變成更複雜的分散式系統問題,需要非常謹慎的 API 和並行推理。

雖然 Zircon 是元件系統,且旨在以低廉的成本切換使用者空間工作,但需要向下呼叫使用者空間程序來解壓縮 4KiB 頁面,會大幅增加延遲和成本。這會促使系統延後壓縮作業,直到網頁較舊時才執行,進而減少可節省的記憶體。在現有使用者分頁錯誤的案例中,系統會假設正在查詢緩慢的持續性儲存空間,因此清除作業會盡可能延後。

即使使用者空間處理壓縮和解壓縮作業,仍須完成所有核心變更,才能追蹤網頁存留時間並觸發壓縮作業。因此,所有可調整項目和指標仍須存在於核心中。 此外,VM 系統目前無法將使用者呼叫器與子 VMO 建立關聯,因此需要進一步變更及重新設計 VM 系統。

核心程式碼解譯器

我們可透過使用者提供的核心程式碼 (如 Linux eBPF),支援使用者提供的演算法,而非固定的壓縮演算法。這樣一來,使用者就能彈性導入,不必擔心執行使用者下呼叫會影響效能。

雖然不需要明確模式轉換,但核心仍需將此視為類似使用者下呼叫,因為效能特徵未知,因此在叫用提供的程式碼時,仍需謹慎處理鎖定和其他依附元件。

這種做法的主要缺點是,核心中的 VM 必須具備足夠的效能,才能產生可接受的壓縮效果,但這需要大量實作,肯定會很複雜,且大幅增加核心的信任運算基礎。

未來

未來我們可能希望以交換儲存空間為基礎,提供匿名 VMO。這絕對需要在使用者空間中,透過使用者分頁機制實作。此時,這類交換實作項目可以執行壓縮,而不只是交換。不過,由於壓縮的深層核心整合具有優勢,我認為這項技術會與核心壓縮並存,而非取代核心壓縮。

會計替代方案

提案目前建議為現有的已提交計數,提供內容位元組/網頁的新定義。您可以在不變更計數內容的情況下,選擇其他命名方式,例如:

  • 常駐且專責
  • 常駐和內容

這些做法都需要變更所有現有的已提交使用情形,屬於大規模變更,且會在兩個 API 版本之間造成更大的不連續性。

ZX_INFO_KMEM_STATS_EXTENDED pager bytes alternatives

雖然建議的實作方式會統一分頁器備份和匿名佇列,導致無法輕易區分兩者的計數,但替代實作方式可以分別保留計數。

這個替代實作方式會為每個可回收佇列提供兩組不同的計數器。儲存在 vm_page_t 中的高位元會用於區分在佇列之間移動頁面時,需要更新哪個計數。page_queue_priv

這種做法的缺點如下:

  • page_queue_priv 的使用方式日益複雜。現在會變成封裝欄位,而非計數器,所有讀取和寫入作業都必須保留或遮蓋額外位元。
  • 存取收割 (發生於持有 arch 空間鎖時) 的成本會增加,因為佇列資訊需要解碼,且正確的計數需要更新。
  • 多個計數欄位,且維持一致性更加複雜。

如提案所述,我認為就分頁位元組欄位的實際用途而言,考量到額外的實作複雜度和執行階段成本,分別計算的價值極小。

實驗評估

為了提議 LZ4 壓縮演算法和壓縮儲存空間策略,我們建構了實驗性壓縮版本,其中包含足夠的片段,可執行下列操作:

  • 收集一組會壓縮的非使用中匿名網頁樣本資料。
  • 對這個資料集執行不同的壓縮演算法。

目的是評估不同壓縮演算法的:

  • 實際產品的真實輸入資料。
  • 在與核心正確相符的受限執行環境中。

第二點尤其重要,因為核心程式碼的執行環境不支援 FPU/SSE/AVX 等。

評估時使用 64 MiB 的輸入資料集,並將每個 4 KiB 的網頁個別饋送至壓縮器,就像實際運作一樣。結果資料的解讀方式如下:

  • 成功壓縮是指壓縮至原始大小的 70% 以上。
  • 系統會計算所有網頁的壓縮時間,包括最終未壓縮儲存的網頁。這與實際使用情況相符,因為系統不會預先知道壓縮是否會成功。
  • 只有成功壓縮的網頁才會計入解壓縮時間。
  • 總大小會計算每個網頁所需的儲存空間,因此成功壓縮的網頁會將壓縮後的大小加入計算,而壓縮失敗的網頁則會以 4KiB 計算。

這種計數方式的理由是,最終目標是釋放記憶體並盡量減少 CPU 使用量。如果壓縮演算法有時能快速壓縮網頁,有時卻會耗用大量 CPU 資源,但最終無法壓縮,這樣不僅無法節省多少記憶體,還會耗用大量 CPU 資源。

NUC 7

演算法 壓縮 (MiB/秒) 解壓縮 (MiB/秒) 解壓縮最差延遲時間 (奈秒) 總大小 (MiB)
Zstd(-7) 752.1470396 1446.748532 9302.3125 18.61393929
minilzo 1212.400326 2135.49407 6817.839844 16.16410065
lz4(1) 1389.675715 2920.094092 6355.849609 17.21983242
lz4(11) 1801.55426 3136.318174 5255.708984 19.92521095
WKdm 1986.560443 3119.523406 3095.283203 21.98047638

ARM A53

演算法 壓縮 (MiB/秒) 解壓縮 (MiB/秒) 解壓縮最差延遲時間 (奈秒) 總大小 (MiB)
Zstd(-7) 189.3189527 486.6744277 29856.93359 18.61393929
minilzo 317.4546381 528.877112 15633.99219 16.16410065
lz4(1) 294.5410547 1127.360347 12683.55273 17.21983242
lz4(11) 378.2672263 1247.749072 12452.67676 19.92521095
WKdm 337.6087322 471.9796684 14254.39453 21.98047638

ARM A73

演算法 壓縮 (MiB/秒) 解壓縮 (MiB/秒) 解壓縮最差延遲時間 (奈秒) 總大小 (MiB)
Zstd(-7) 284.3621221 623.15235 20958.90332 18.61393929
minilzo 487.4546545 747.9218419 16784.83105 16.16410065
lz4(1) 441.5927551 1159.839867 10007.28418 17.21983242
lz4(11) 573.7741984 1240.964187 9987.875 19.92521095
WKdm 639.5418085 597.6976995 13867.47266 21.98047638

壓縮後的儲存空間

我們使用相同的 64 MiB 測試資料集,評估提議的壓縮儲存系統。在本例中,系統評估了兩種模式:只有左側和右側位置的兩頁式系統,以及建議的三個位置系統。目的是嘗試量化包含中間位置的複雜度優勢,相較於真正簡單的雙位置系統。

LZ4 是建議使用的壓縮演算法,因此我們使用 LZ4 產生要儲存的壓縮資料,並將加速因子設為 11。也就是說,在 16384 個輸入頁面中,有 12510 個產生需要儲存的資料,而 3874 個則未經壓縮。

吃角子老虎 儲存空間頁面 總 MiB 節省的 MiB 數 壓縮比
雙插槽 6255 39.5 24.4 1.62
三插槽 4276 31.8 32.1 2.01

請注意,6255 剛好是 12510 的一半,表示每個頁面都有可用的緩衝區,也就是說,就 LZ4 輸入而言,這個儲存策略達到了最佳壓縮比。

既有技術和參考資料

所有主流的現代作業系統 (Windows、macOS、Linux 和 Linux 衍生版本,例如 CastOS 等) 都支援記憶體壓縮,可做為磁碟交換的替代或輔助方案。

三頁式儲存空間策略的靈感來自 Linux 的 zbud 和 z3fold 記憶體管理系統。