RFC-0266:可對應記憶體的核心時脈

RFC-0266:可對應記憶體的核心時鐘
狀態已接受
區域
  • 核心
說明

提供一種方法,讓使用者模式程序讀取核心時鐘狀態,而不必進入 Zircon 核心。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2024-11-21
評論日期 (年-月-日)2025-02-14

問題描述

Starnix 需要一種方法,為用戶端程式提供行為符合 Linux CLOCK_MONOTONIC 定義的時鐘。這個時鐘必須代表單調時間軸,其速率調整與全系統 UTC 時間軸相同,但當不同執行緒能夠透過外部方式建立觀測順序時,這個時鐘也必須始終保持單調和連續。換句話說,這些屬性必須在多位觀察者依序觀察的過程中保持一致。

請注意,Zircon/Fuchsia 對 CLOCK_MONOTONIC 的定義目前不符合所有這些要求,也沒有任何計畫要這麼做。雖然 Zircon 的版本始終是單調且連續的,但它一律以系統基礎參考時鐘硬體的速率執行,且不會嘗試與任何外部參考進行速率比對。

預期 Starnix 用戶端會頻繁使用 Linux CLOCK_MONOTONIC 時鐘,因此必須盡可能降低任何解決方案的查詢成本。更複雜的是,Starnix 的用戶端必須在受限模式下運作,也就是說,他們不得直接發出 Zircon 系統呼叫。Starnix 用戶端最多只能退出受限模式並進入 Starnix 核心,在該核心中可執行 Zircon 系統呼叫。請注意,這是 Starnix 目前用來提供全系統 UTC 時間軸存取權的策略,但這種做法的效能成本已經相當高。理想的解決方案是為 Starnix 用戶端提供 Linux CLOCK_MONOTONIC 存取權,而不必讓用戶端退出受限模式「或」進入 Zircon 核心,因為這項負擔已變得過於沉重。

摘要

Zircon 核心時鐘物件已能滿足 Linux 版 CLOCK_MONOTONIC 的大部分需求。這類時鐘可建構使用者模式定義的時間軸,核心可保證多個觀察者觀察到的時間軸一律單調且連續,且可委派給授權單位 (在本例中為 Timekeeper) 進行維護。

這份 RFC 提案提出一種方法,允許在不進入 Zircon 核心的情況下讀取 Zircon 核心時鐘,使讀取作業的負擔非常低,前提是時鐘物件的參考時鐘也可以在不進入 Zircon 核心的情況下讀取。此外,這種讀取時鐘物件的方法可讓 Starnix 用戶端讀取時鐘,甚至不需要退出受限模式,讓 Starnix 能夠將 Linux CLOCK_MONOTONIC和世界標準時間兩者發布至用戶端,而不需要退出受限模式。

相關人員

  • Starnix 實作者
  • Timekeeper 維護人員
  • 核心維護人員
  • 媒體維護者

講師:

  • jamesr@google.com

審查者:

  • rashaeqbal@google.com
  • rudymathu@google.com
  • jamesr@google.com
  • adanis@google.com
  • mpuryear@google.com

諮詢對象:

  • maniscalco@google.com
  • mcgrathr@google.com
  • fmil@google.com

社交:

這項 RFC 是在內部設計文件中提出,並與利害關係人討論,直到認為可以開始 RPC 程序為止。

需求條件

本提案的主要目的之一,是提供 Starnix 一個路徑,為其受限模式程序提供代表 Linux 版本 CLOCK_MONOTONIC 的時鐘。雖然能夠對應記憶體時鐘還有其他好處,但這裡所述的需求主要來自於必須滿足 Starnix 和 Linux 定義的需求。

記憶體對應時鐘必須能夠:

  • 符合 CLOCK_MONOTONIC 的 Linux 需求
  • 與系統範圍的 UTC 時鐘一樣,會進行相同的費率調整。
  • 在多位觀察者依序觀察的序列中,維持單調性和連續性。
  • 在「可負擔的執行階段成本」下,從 Starnix 嚴格篩選模式程序查詢。

CLOCK_MONOTONIC的 Linux 需求

根據 Linux clock_gettime man 頁面,CLOCK_MONOTONIC 的需求如下。

A nonsettable system-wide clock that represents monotonic time since—as
described by POSIX—"some unspecified point in the past".  On Linux, that point
corresponds to the number of seconds that the system has been running since it
was booted.

The CLOCK_MONOTONIC clock is not affected by discontinuous jumps in the system
time (e.g., if the system administrator manually changes the clock), but is
affected by the incremental adjustments performed by adjtime(3) and NTP.  This
clock does not count time that the system is suspended.  All CLOCK_MONOTONIC
variants guarantee  that  the  time returned by consecutive calls will not go
backwards, but successive calls may—depending on the architecture—return
identical (not-increased) time values.

容易維護

Linux 對 CLOCK_MONOTONIC 的定義要求,必須對 CLOCK_MONOTONIC 時間軸進行相同的速率調整,以維持系統範圍的 UTC 定義。目前,代表系統世界標準時間時間軸的時鐘是由元件管理服務建立,並透過程序啟動通訊協定,以重複控制代碼 (具有適當權限) 的形式發布至程序。名為 Timekeeper 的程序會收到這個時鐘的控制代碼,並具備寫入權限。負責使用可用的外部參考資料維護 UTC 時鐘,持續修正 UTC 的本地表示法。

根據上述 Linux 需求,系統中必須有某個項目,將 Timekeeper 套用至系統範圍世界標準時間時鐘的相同速率調整,套用至 Starnix 將提供給用戶端的 Linux CLOCK_MONOTONIC 版本。

一致性

如 Linux 規定所述,CLOCK_MONOTONIC 必須是單調且連續的。因此,觀察時鐘的執行緒絕不能看到時鐘倒退或向前跳。換句話說,時鐘的觀測結果必須與這些單調/連續屬性一致。

雖然 Linux 手冊頁面並未明確說明,但我們也認為這項規定代表時鐘不只要從單一觀察者的角度來看保持一致,也必須「多觀察者」一致。換句話說,假設多個執行緒產生排序觀察序列 (O1、O2、O3...On),則觀察序列必須與單調和連續屬性一致。

只有在多個觀察者之間有互動,且互動建立順序時,多個觀察者所做的觀察序列才會有定義的順序。舉例來說,如果兩個執行緒 (T1 和 T2) 進行兩次觀測 (O1 和 O2),並將觀測結果傳送至第三個執行緒,則沒有定義的順序。T1 和 T2 沒有互動,因此無法判斷哪個觀測值「先發生」,且序列沒有定義的順序。

舉例來說,假設有一個由互斥鎖保護的全域觀測清單 (可能是內部偵錯記錄檔),許多不同的執行緒會定期鎖定互斥鎖、進行觀測、將觀測結果附加至全域清單,最後解鎖互斥鎖。這份清單現在包含多位觀察者依序觀察到的結果,順序由互斥鎖的排他性決定。這個觀測值序列必須與時鐘的單調和連續屬性一致。

可從 Starnix 用戶端讀取,且「執行階段成本合理」。

雖然這不是 Linux 的要求,但仍是必要條件。Starnix 受限模式程序必須能以「非常便宜」的方式觀察 CLOCK_MONOTONIC 時鐘。請注意,Starnix 用戶端目前使用 Zircon 核心時鐘物件,為用戶端提供世界標準時間。原本會退出受限模式並進入 Starnix 核心,然後發出進入 Zircon 核心的 Zircon 系統呼叫。

由於必須退出受限模式並進入 Zircon 核心,導致負擔過重,因此系統經過最佳化,讓 Starnix 嘗試保留時鐘物件轉換的快取副本,這樣一來,就不必在每次查詢時進入 Zircon 核心,即可查詢時鐘。雖然這項做法有所改善,但仍需要 Starnix 用戶端退出嚴格篩選模式,而且在維護時鐘轉換的快取副本,以及產生與非 Starnix 程式觀察結果一致的結果時,會產生其他複雜問題。

設計

概括來說,這裡的設計是使用 Zircon 核心時鐘物件管理 Starnix 範圍內的 Linux CLOCK_MONOTONIC 時間軸概念,並擴充物件 API,以便以極低的成本觀察時鐘,讓 Starnix 用戶端甚至不必退出嚴格篩選模式。

除了絕不離開嚴格篩選模式的成效目標外,Zircon 核心時鐘已滿足上述要求。我們快速回顧一下每項最佳做法。

容易維護

核心時鐘是核心物件,由具有權限的控制代碼管理,因此本質上可委派。這為 Starnix 提供多個選項,可用於維護代表 Linux CLOCK_MONOTONIC 定義的時鐘。

1) 時鐘可由元件管理服務建立,並發布至 Timekeeper 進行維護,以及發布至 Starnix 供使用。2) 時鐘可由 Timekeeper 建立及維護,並由 Starnix 透過 FIDL 介面從 Timekeeper 擷取使用。3) 時鐘可由 Starnix 建立,並透過 FIDL 介面傳送至 Timekeeper 進行維護。4) 完全不同的內容。

這裡的主要優點是,由於它是核心物件,因此時鐘的維護可以委派給 Timekeeper,後者目前也維護 UTC 時鐘,因此可以相對簡單地將相同的速率調整套用至 Linux CLOCK_MONOTONIC 時鐘,就像對 Fuchsia UTC 時鐘所做的調整一樣。

一致性

Zircon 核心時鐘已保證多觀察者一致性,如上所述。核心不允許任何可能產生不一致觀測結果 (依時鐘建立時的屬性定義) 的更新作業。因此,無論維護者有多少錯誤或惡意,都無法讓觀察者看到不一致的序列。

同樣地,核心中用於觀察時鐘的通訊協定可確保觀察結果一致。只要 API 的任何新增內容都遵循相同通訊協定,就能確保一致性。

可接受的觀測成本

這就來到設計中最重要的部分:以可接受的效能代價觀察核心時鐘物件的能力。這裡的主要目標是設計一種讀取時鐘的方法,不需要進入 Zircon 核心 (消除該負擔),而且可以採用甚至不需要受限模式程序退出受限模式的方式實作 (無論該系統呼叫是否進入 Zircon 核心,都需要這樣做才能進行 Zircon 系統呼叫)。

可對應的州/省

瞭解核心時鐘物件的運作方式可能會有幫助,詳情請參閱這裡的公開文件。

不過,最重要的是要瞭解,核心時鐘的狀態實際上只是仿射函式,可將所選參考時鐘 (Zircon 單調或啟動) 轉換為時鐘代表的合成時間軸目前值。如要持續觀察時鐘物件,必須成功執行「讀取交易」,記錄轉換的目前狀態和參考時鐘的目前值,並確保在交易期間,合成時鐘的轉換狀態未發生任何變更。

目前,時鐘的狀態會儲存在核心記憶體中,無法 (直接) 從使用者模式存取。使用者模式可能會使用 zx_clock_get_details 系統呼叫擷取時鐘狀態的最新版本,但這不僅需要系統呼叫,而且在系統呼叫傳回時,資訊可能已經過時,可能會導致觀察序列不一致。

不過,這個狀態並沒有什麼特別之處。具備時鐘讀取權限的使用者,已可擷取時鐘有效轉換的詳細資料。我們應該可以消除程式進入 Zircon 核心來觀察時鐘詳細資料的需求,只需擴充時鐘物件實作,允許時鐘狀態儲存在 RAM 頁面中,該頁面可以對應 (唯讀) 到使用者模式程序。透過這種方法,使用者模式可以將時鐘狀態對應至其程序,只要遵循觀測通訊協定,且參考時鐘可直接存取,而不必進入 Zircon 核心,即可進行一致的觀測,而不會產生進入核心的額外負擔。

在使用者模式下觀察時鐘

如要在使用者模式中觀察記憶體對應時鐘,必須遵循非常特定的通訊協定,且該通訊協定必須隨時與核心的行為相符。此外,遵循通訊協定需要瞭解對應頁面中時鐘狀態資料的特定版面配置。Zircon 必須保有變更觀測通訊協定或共用記憶體版面配置的能力,而不必與用戶端程式協調,並經過完整的正式淘汰週期。

因此,使用者必須將指向對應時鐘狀態的指標視為指向不透明 blob 的指標,並將其傳遞至 Zircon vDSO 中實作的 Zircon syscall。請注意,這些系統呼叫幾乎不需要實際進入 Zircon 核心,即可讀取時鐘。只有在時鐘物件的參考時鐘只能透過進入核心讀取時,才需要進入核心 (目前極為罕見)。刻意不公開共用資料格式,並將讀取實作放入 Zircon vDSO 實作的 Zircon 系統呼叫中,使用者就能享有不必進入 Zircon 核心的效能優勢,同時保留變更結構和讀取通訊協定詳細資料的能力,而不必變更 ABI 合約。

話雖如此 (如前所述),Starnix 用戶端會在受限模式下執行,因此無法發出 Zircon 系統呼叫,即使是不會進入 Zircon 核心的呼叫也一樣。請注意,對於想存取現有 Zircon 系統呼叫 (通常不會進入 Zircon 核心) 的嚴格篩選模式用戶端來說,這已經是問題,最明顯的例子是 zx_clock_get_monotonic()。

目前,Starnix 會使用特殊解決方法來處理這些情況。系統會建立通用程式庫 (libfasttime),並將用於實作 get_monotonic 的邏輯移至該程式庫。如果擷取單調參考資料時不需要進入核心,Zircon vDSO 會使用這個程式庫實作 zx_clock_get_monotonic(),否則會改用完整核心項目。

Starnix 對其受限模式用戶端執行類似的操作,以便讓他們存取 Zircon 單調參考,並使用 libfasttime 中的程式碼在受限模式下實作實際作業,而不需要退出受限模式來實作 zx_clock_get_monotonic() 的功能。這裡的計畫是將記憶體對應時鐘讀取作業放入 libfasttime,然後在所有地方使用這個實作,藉此擴充這項技術。

這表示 Starnix 和 Zircon 核心之間存在依附關係。具體來說,每當 libfasttime 有所變更,Starnix 和 Zircon 都必須重新建構並一併部署。我們正在努力消除這項依附關係,新的時鐘讀取作業最終實現時,將會利用這些成果。擴充現有技術不會讓情況比現在更糟,因此被視為可接受的做法。

時鐘讀取通訊協定詳細資料。

以下說明目前如何以保證一致性的方式觀測核心時鐘,並將在 libfasttime 中初步實作。這項資訊旨在協助您瞭解「幕後」運作方式,但不應視為核心 ABI 合約的一部分。

在內部,核心時鐘物件會使用一種稱為 SeqLock 的特殊鎖定類型,以保護其狀態。在實作不使用系統呼叫的時鐘觀測方法時,這些鎖定機制有一些實用的屬性。注意事項:

1) SeqLock 允許並行讀取,但讀取交易絕不會阻止寫入交易。因此,使用者模式程式無法阻止其他使用者模式程式讀取時鐘,也無法暫停核心中更新時鐘的程式碼。2) SeqLock 讀取交易不需要變動狀態。因此,如果鎖定狀態是已對應時鐘狀態的一部分,即使狀態對應為唯讀,鎖定功能仍可在使用者模式下使用。3) SeqLock 讀取交易不會封鎖讀取器執行緒,也就是說,在使用者模式中不需要 futex syscall。此外,這類函式無法防止讀取或寫入交易同時發生,因此不需要停用中斷 (在使用者模式中無法執行這項操作)。

概略來說,正確觀察時鐘很簡單。順序如下:

1) 使用保護對應時鐘狀態的 SeqLock 開始讀取交易。2) 將時鐘狀態複製到本機堆疊分配的緩衝區。 3) 讀取參考時鐘值。4) 結束讀取交易,如果交易中斷,則重試直到成功為止。5) 使用儲存的參考時鐘值和時鐘狀態,計算合成時間值。

不過,深入探究後,就會發現這並非表面上那麼簡單。SeqLock 交易必須針對鎖定狀態和受保護的酬載狀態使用不可部分完成的載入,且可能需要明確的柵欄指令,具體取決於酬載的處理方式 (AcqRel 不可部分完成與 Relaxed 不可部分完成)。更複雜的是,做為參考時鐘基礎的硬體計數器暫存器並非記憶體,且在排序和管線執行方面不遵循 C++ 記憶體模型。必須使用特殊架構和計時器專用指令,才能防止讀取這個暫存器時「滑出」交易。最後,將參考時鐘值轉換為適當合成時鐘值的方法,必須在所有使用者之間保持一致,否則捨入誤差可能會導致使用不同轉換技術的觀察者,產生一系列不一致的觀察結果。

為了將所有內容保留在同一頁,這裡的設計需要重構程式碼,如下所示:

1) zx_clock_read 和 zx_clock_get_details 作業的核心會從核心程式碼移至 libfasttime,而核心程式碼會重構為使用 libfasttime 實作。2) zx_clock_read 和 zx_clock_get_details 的版本會以 Zircon 系統呼叫的形式,新增至 Zircon vDSO。這些版本會對應至本機位址空間的時鐘狀態。這些 vDSO 呼叫的實作將以 libfasttime 實作為基礎,確保每個人都使用適當相符的實作。

API

支援可對應時鐘的使用者模式 API 包含三個新的系統呼叫,其中兩個通常不會進入 Zircon 核心。此外,我們也會推出新選項,在建立時鐘時必須傳遞,並推出新的 zx_object_get_info() 主題,讓使用者瞭解時鐘例項的對應大小。

建立時鐘

如要建立可對應的時鐘,使用者必須將新的 ZX_CLOCK_OPT_MAPPABLE 旗標傳遞至 zx_clock_create 的選項欄位。成功呼叫建立可對應時鐘後,時鐘物件會在內部配置 VMO,並使用這個 VMO 儲存其狀態,而非內部核心記憶體。此外,除了預設時鐘權限外,這個成功呼叫傳回的控制代碼也會包含 ZX_RIGHT_MAP。如果建立時鐘時未啟用這個選項,初始控制代碼就不會包含這項權限。

決定對應時鐘所需的空間

在本 RFC 草案發布時,核心時鐘狀態的目前大小 (包括對齊用的填充) 為 160 位元組。這可以輕鬆放入單一 4 KB 頁面,且有很大的空間可供日後擴充。

不過,基於多種原因,如果只是假設對應時鐘的大小現在和未來都剛好是一個 4 KB 頁面,並不是明智的做法。

1) 系統的基礎頁面大小可能在未來變更,例如從 4 KB 變更為 16 KB,甚至是 64 KB。2) 時鐘物件的狀態實際上可能需要大幅成長,甚至可能超過一頁(理論範例可應要求提供)。 3) 開發人員可能需要暫時擴充對應時鐘表示法的大小,以便進行偵錯,這需要維護更多狀態。即使較大的時鐘表示法從未提供給任何使用者,在實驗中增加時鐘大小,理想情況下不應破壞現有程式碼。

因此,對應時鐘的大小不一定固定,如果使用者想將時鐘狀態對應至位址空間,就必須知道時鐘狀態的大小,才能妥善管理位址空間。

為解決這個問題,我們將新增資訊主題 (ZX_INFO_CLOCK_MAPPED_SIZE)。在已成功使用 ZX_CLOCK_OPT_MAPPABLE 旗標建立的時鐘上,與 zx_clock_get_info() 搭配使用時,系統會以單一 uint64_t 傳回對應時鐘狀態的大小 (以位元組為單位)。

如果嘗試擷取不可對應時鐘物件的時鐘對應大小,系統會視為錯誤並傳回 ZX_ERR_INVALID_ARGS;如果嘗試擷取非時鐘類型物件的對應時鐘大小,則會傳回 ZX_ERR_WRONG_TYPE。

對應及取消對應時鐘

系統會新增系統呼叫 (zx_vmar_map_clock),讓使用者能將內部 VMO 對應至其位址空間,以便用於 read 和 get_details 作業。

zx_status_t zx_vmar_map_clock(zx_handle_t handle,
                              zx_vm_option_t options,
                              uint64_t vmar_offset,
                              zx_handle_t clock_handle,
                              uint64_t len,
                              user_out_ptr<zx_vaddr_t> mapped_addr);

這項作業與 zx_vmar_map() 或 zx_vmar_map_iob()「幾乎」相同,但有幾項重要差異。

1) zx_vmar_map() 支援的大多數選項都可使用,但並非全部。使用者尤其不得指定 ZX_VM_PERM_WRITE、ZX_VM_PERM_EXECUTE 或 ZX_VM_PERM_READ_IF_XOM_UNSUPPORTED。時鐘狀態不得由使用者模式用戶端變更 (因此沒有 WRITE),且絕對不是程式碼 (因此沒有 EXECUTE)。如果嘗試使用上述任一選項,會導致 ZX_ERR_INVALID_ARGS 錯誤。系統支援所有其他選項,尤其是與 VMAR 中對應位置相關的選項。2) len 是必要項目 (與其他 VMAR 對應作業相同),但對應量並非任意或由使用者模式控制。特定時鐘執行個體的對應長度必須與使用新推出的 ZX_INFO_CLOCK_MAPPED_SIZE 主題擷取資訊時回報的大小相符。為 len 傳遞任何其他值都會視為錯誤。3) 未提供 vmo_offset 參數。使用者必須一律對應所有時鐘狀態。不允許從 VMO 偏移量開始的部分時鐘狀態對應,因此 vmo_offset 無法使用。4) 未提供 region_index 參數,因為該參數與 zx_vmar_map_iob() 相同。 IO 環形緩衝區物件可能有多個區域,每個區域都可以獨立對應,但時鐘物件沒有,這表示索引參數沒有任何作用。

將時鐘的內部 VMO 保持在核心內部,表示我們不必指定或測試時鐘 VMO 的行為,當傳遞至任何其他 VMO 系統呼叫時。

取消對應時鐘狀態的方式,與取消對應使用者模式建立的其他對應相同。使用者只需呼叫 zx_vmar_unmap(),傳遞對應作業傳回的虛擬位址,以及原始對應中使用的 len。

使用者在對應時鐘控制代碼後,可以自由選擇是否關閉控制代碼。對應關係會保留,仍可用於讀取時鐘狀態。如果對應時鐘的所有控制代碼都已關閉,且時鐘物件已刪除,則內部 VMO 和任何剩餘的對應都會保留。不過,此時任何程式都無法繼續「維護」時鐘的轉換,因為物件已刪除,現在任何程式都無法更新轉換。概念上,時鐘會繼續存在,並永遠漂移。請注意,這與時鐘維護者關閉其負責維護的時鐘的最後一個控制代碼,而時鐘的消費者保留其控制代碼沒有任何不同。

觀察時鐘。

zx_status_t zx_clock_read_mapped(const void* clock_addr, zx_time_t* now);

zx_status_t zx_clock_get_details_mapped(const void* clock_addr,
                                        uint64_t options,
                                        void* details);

如要觀察時鐘,使用者可以呼叫適當的新系統呼叫,例如 zx_clock_read_mapped 或 zx_clock_get_details_mapped。

這些系統呼叫會在 Zircon vDSO 中實作,且只有在存取基礎參考時鐘需要時,才會進入 Zircon 核心。

函式本身的運作方式與未對應的對應函式 (zx_clock_read 和 zx_clock_get_details) 相同,但有下列例外狀況。

1) 使用者必須傳遞時鐘狀態在目前位址空間中的對應位址,而不是將時鐘控制代碼做為第一個引數傳遞,同時該狀態仍會對應。傳遞任何其他值,或將位址傳遞至已取消對應 (完全或部分) 的時鐘狀態,會導致未定義的行為。2) 呼叫這兩項常式時,系統不會檢查權限。由於時鐘狀態已成功對應至使用者的位址空間,因此使用者自然有權讀取對應的時鐘。

如前所述,Starnix 用戶端會在受限模式下執行,無法直接使用這些系統呼叫。為避免需要退出嚴格篩選模式,Starnix 可能會以類似目前的使用方式使用 libfasttime,以實作與 zx_clock_get_monotonic() 相同的功能,直到開發出更好的技術,避免 Starnix 依賴 libfasttime 為止。

zx_info_maps_t 結構中的時鐘對應表示法

Zircon 目前提供幾種不同的方式,讓開發人員擷取目前有效對應的相關資訊,以便進行診斷。兩者皆使用 zx_object_get_info() 主題存取:

++ ZX_INFO_VMAR_MAPS ++ ZX_INFO_PROCESS_MAPS

第一個主題會列舉指定 VMAR 中的一組有效對應,第二個主題則會列舉程序物件中的一組有效對應。無論是哪一種情況,結果都會以 zx_info_maps_t 結構陣列的形式傳回。現在時鐘物件本身可以建立對應,我們需要能夠回答一些問題,瞭解對應時鐘的列舉記錄中會有哪些資訊。

1)「名稱」欄位應填入什麼內容?核心時鐘目前沒有可指派的名稱,因此在列舉時鐘對應時,名稱會設為常數字串「kernel-clock」,方便使用者查看診斷資料。如果 (未來) 核心時鐘擴充為允許使用者設定名稱,則顯而易見的下一步是回報使用者指派的名稱,而不是常數名稱。2) 記錄中聯集的「類型」為何?按照對應 IOB 區域的範例,類型會是 ZX_INFO_MAPS_TYPE_MAPPING,並填入 u.mapping 的成員。3) u.mapping.vmo_koid 欄位回報的值為何?雖然核心時鐘在內部使用 VMO,透過對應與程序共用時鐘狀態所需的頁面,但這個 VMO 並未包裝在 VmObjectDispatcher 中,因此沒有指派給它的 KOID。因此,系統會改為回報時鐘物件本身的 KOID。

實作

這項功能的實作會以一系列 Gerrit CL 的形式進行,主要是為了讓程式碼審查規模保持在合理且可管理的範圍內。要提交的 CL 如下。

1) API 虛設常式會新增至 Zircon vDSO,用於上述 3 項呼叫。這些虛設常式的初始實作只會傳回 ZX_ERR_NOT_SUPPORTED。一開始會新增至 @next API。2) read 和 get_details 實作項目會移至 libfasttime,核心程式碼也會變更為使用這些實作項目,而非使用自己的實作項目。3) zx_clock_create 的更新行為、zx_clock_get_vmo 的實際實作,以及對 ZX_INFO_CLOCK_MAPPED_SIZE 主題的支援,都會新增至核心。4) zx_clock_read_mapped 和 zx_clock_get_details_mapped 的實際實作項目會新增至 Zircon vDSO。5) C++ 專屬包裝函式會新增至 libzx。6) 整個功能的測試會新增至現有的 Zircon 核心時鐘測試。這項功能和新系統呼叫的說明文件,將新增至現有的公開說明文件。

到這個步驟,實作程序就完成了。現在可以將其他語言專屬繫結新增至 Rust,Starnix 也可以使用 libfasttime 中的功能和程式碼,為嚴格篩選模式用戶端提供有效率的 Zircon 時鐘存取權。

經過一段時間的使用和穩定化 (期間可能會視情況變更 API) 後,API 會從 @next 移除,並升級為目前 Zircon Kernel API 的成員。

效能

從 Zircon 程序存取記憶體對應核心時鐘的成本,預計會比透過系統呼叫存取時鐘略低,因為進入 Zircon 核心和執行控制代碼驗證的所有成本都已消除。這也會稍微減輕程序控制代碼資料表的壓力,因為在控制代碼驗證期間,資料表不再需要鎖定以供讀取。

這樣做的代價是記憶體用量會略微增加,因為我們現在需要一頁記憶體來儲存時鐘狀態,而不是像現在一樣,使用約 160 位元組的核心堆積記憶體來追蹤時鐘狀態。此外,VMO 物件本身的核心表示法需要少量的核心記錄,而選擇對應時鐘的程序也需要分頁表負擔。

Starnix 嚴格篩選模式程序受到的影響預計會更顯著。用戶端不必再退出受限模式,並進入 Starnix 核心,即可存取時鐘物件。此外,Starnix 核心也不再需要監控全系統的 UTC 變更,並更新其維護的內部快取版本,以免發出系統呼叫來讀取時鐘。相反地,受限模式用戶端可以直接讀取時鐘,不必退出目前的結構定義,切換至 Starnix 或 Zircon 核心。

人體工學

整體 API 人體工學應與現有 API 幾乎相同。只要進行少量設定 (擷取及對應 VMO),使用者就能以與現有方式完全相同的方式讀取 Zircon 核心時鐘,只需將時鐘狀態指標替換為原本要傳遞至原始讀取常式的時鐘控制代碼即可。

以下舉個小例子,說明 API 差異。

// An object which creates a non-mappable kernel clock and allows users to read it.
class BasicClock {
 public:
  BasicClock() {
    const zx_status_t status = zx::clock::create(ZX_CLOCK_OPT_AUTO_START,
                                                 nullptr,
                                                 &clock_);
    ZX_ASSERT(status == ZX_OK);
  }

  zx_time_t Read() const {
    zx_time_t val{0};
    const zx_status_t status = clock_.read(&val);
    ZX_ASSERT(status == ZX_OK);
    return val;
  }

 private:
  zx::clock clock_;
};

// And now the same thing, but with a mappable clock.  There are a few extra
// setup and teardown steps, but the read operation is virtually identical.
// An object which creates a non-mappable kernel clock and allows users to read it.
class MappedClock {
 public:
  MappedClock() {
    // Make the clock
    zx_status_t status = zx::clock::create(ZX_CLOCK_OPT_AUTO_START | ZX_CLOCK_OPT_MAPPABLE,
                                           nullptr,
                                           &clock_);
    ZX_ASSERT(status == ZX_OK);

    // Get the mapped size.
    status = clock_.get_info(ZX_INFO_CLOCK_MAPPED_SIZE, &mapped_clock_size_,
                             sizeof(mapped_clock_size_), nullptr, nullptr);
    ZX_ASSERT(status == ZX_OK);

    // Map it.  We can close the clock now if we want to, just need to remember to un-map our
    // region when we destruct.
    constexpr zx_vm_option_t opts = ZX_VM_PERM_READ | ZX_VM_MAP_RANGE;
    status = zx::vmar::root_self()->map(opts, 0, clock_vmo, 0, mapped_clock_size_, &clock_addr_);
    ZX_ASSERT(status == ZX_OK);
  }

  ~MappedClock() {
    // Un-map our clock.
    zx::vmar::root_self()->unmap(clock_addr_, mapped_clock_size_);
  }

  // The read is basically the same, just pass the mapped clock address instead of using the handle.
  zx_time_t Read() const {
    zx_time_t val{0};
    const zx_status_t status = zx::clock::read_mapped(reinterpret_cast<const void*>(clock_addr_),
                                                      &val);
    ZX_ASSERT(status == ZX_OK);
    return val;
  }

 private:
  zx::clock clock_;
  zx_vaddr_t clock_addr_{0};
  size_t mapped_clock_size_{0};
};

回溯相容性

如果程式可以直接發出 Zircon 系統呼叫,就不需要考慮回溯相容性問題。現有的時鐘功能不會有任何變更,且 Zircon vDSO 的性質可避免我們必須對狀態結構的版面配置或必要的時鐘觀測通訊協定做出任何長期承諾。

不過,Starnix 會依附於核心 (更精確地說,是 libfasttime)。對結構版面配置或觀測通訊協定所做的任何變更,都會在 libfasttime 程式庫中更新,且 Starnix 必須重新編譯。目前我們認為這不是重大問題,原因有兩個。

1) 由於 Zircon 單調和啟動時間軸的無系統呼叫存取權,是以受限模式提供給 Starnix 用戶端,因此目前就是這種情況。換句話說,這並非新的依附元件,只是延續既有做法。2) 長期而言,我們正在考慮如何消除預先存在的依附元件。如果這些構想成真,這個新 API 將與 zx_clock_get_monotonic 一樣,受益於這些改進。

安全性考量

允許使用者將時鐘狀態 (唯讀) 對應至其位址空間,不會造成已知的安全性影響。用於觀察時鐘的通訊協定不允許使用者模式獨占鎖定時鐘 (這會使我們公開於各種 DoS 攻擊),也不允許修改時鐘狀態 (可能導致核心保證的時鐘屬性失效)。時鐘狀態包含的所有資訊,都已可透過呼叫 zx_clock_get_details() 從使用者模式存取,這項新功能只是最佳化存取方式。

直接存取核心時鐘狀態的功能,並不會額外提供存取高解析度計時器的功能。時鐘只是現有參考資料的轉換,因此程序可用的基礎參考時鐘解析度,未來可能受到的任何限制,也必然會影響從這些參考資料衍生的合成時鐘。

隱私權注意事項

這項提案目前沒有已知的隱私權影響。這裡不涉及任何使用者資料。

測試

測試核心功能時,會以兩種方式擴充 Zircon 時鐘單元測試。

1) 新增測試,確保涉及建立、對應和取消對應可對應時鐘的規則 (如上所述) 確實執行。2) zx_clock_read 和 zx_clock_get_details 的現有測試也會擴充,以便測試這些呼叫的 mapped 版本。兩者的行為應該幾乎相同,因此測試也應該如此。

說明文件

1) 更新頂層公開文件,加入功能的一般說明,並提供新加入 API 方法的連結。此外,至少應包含一個建立、對應、讀取和取消對應時鐘的範例。2) zx_clock_create 將擴充,以描述新的 ZX_CLOCK_OPT_MAPPABLE 旗標。3) 時鐘參考文件將新增頁面,說明 zx_clock_get_vmo、zx_clock_read_mapped 和 zx_clock_get_details_mapped。

缺點、替代方案和未知因素

建立可對應時鐘的替代方案

除了明確禁止上述所有不允許的行為 (並為該程式碼編寫測試) 之外,實作成本相對較低。進行小幅重構,將觀測常式移至通用位置,並稍微重構核心中的 ClockDispatcher 程式碼,以便除了內部儲存空間外,還能使用 VMO 儲存空間。接著只要進行功能測試和編寫說明文件即可。

雖然有其他替代方案,但需要投入更多心力,且最終會產生非常具體的解決方案,而非一般工具。

舉例來說,Starnix 可以建立及管理自己的 Linux CLOCK_MONOTONIC 概念,然後透過類似的記憶體對應技術,或強制嚴格篩選模式用戶端退出嚴格篩選模式並進入 Starnix 核心,將其公開給嚴格篩選模式程序。不過,仍有許多問題需要解決。

1) 鎖定高讀取並行。SeqLock 非常實用,但在使用者模式下,雖然可以輕鬆用於讀取交易,但鎖定為使用者模式下的專屬寫入作業卻非常危險,因為無法停用中斷,或以其他方式防止非自願搶占,這可能會導致更新作業在搶占時,阻止讀取作業繼續進行。2) 取得時鐘校正資訊。Starnix 要建立的時鐘應與系統層級的 UTC 參考時鐘一樣,進行相同的頻率調整,但這些調整是由 Timekeeper 執行,而非 Starnix。複製 Timerkeeper 的工作會耗費大量精力 (而且是多餘的工作),因此 Timerkeeper 可能需要將校正資訊轉送至 Starnix 維護的時鐘,而 Starnix 可能需要向 Timekeeper 提供意見回饋資訊,才能關閉時鐘復原迴圈。3) 確保記憶體順序語意正確。視 #1 採用的解決方案而定,您必須謹慎確保時鐘狀態酬載可觀測,且不會發生任何正式資料競爭,同時將時鐘暫存器觀測值保留在讀取交易中。這可能有點棘手,而且要正確設定和維護也有些困難。

因此,Starnix 可以視需要執行許多這類作業,但這會導致重複執行核心時鐘已執行的作業,造成相對較大的工作量。這也意味著未來需要進行額外測試,並有額外的長期維護需求。

libfasttime 抽象化的替代方案

為了向 Starnix 受限模式用戶端提供承諾的效能優勢,受限模式用戶端程式中必須存在程式碼 (以某種方式),既能瞭解時鐘狀態的版面配置,又能遵循對時鐘狀態進行一致觀察所需的通訊協定。

本提案提供 libfasttime 形式的程式碼,用戶端可將其嵌入程式中,以觀察記憶體對應時鐘,進而滿足這項需求。這種做法的缺點顯而易見 (且相當嚴重),那就是在無法進行 Zircon 系統呼叫,但希望使用記憶體對應時鐘功能的用戶端之間,建立依附元件。每當核心變更導致 libfasttime 變更時,用戶端「必須」根據新程式庫重新建構,否則可能會有不相容的風險。即使這是預先存在的問題 (已存在於 zx_clock_get_monotonic() 的對等實作中),也不會改變這個事實。

另一種做法可能是將 Zircon 公開的 ABI 正式化,嚴格記錄時鐘狀態的記憶體內版面配置,以及對時鐘進行一致觀察所需的通訊協定,讓用戶端實作及維護自己的「可對應時鐘讀取」常式。

不過,這會導致以下結果:

1) 變更時鐘狀態的版面配置或用於觀察時鐘狀態的通訊協定,難度大幅增加。基本上,要變更這項功能的 Zircon 端會變得更加困難。2) 客戶的責任負擔大幅增加。他們現在不只要使用 (可能經過測試和維護) 程式庫程式碼,還要負責閱讀並充分瞭解假設的新 API 規格。此外,他們還必須實作、測試及維護自己的觀測常式版本。

因此,雖然這是一種替代方法,但由於上述兩項重大缺點,目前並未認真考慮。預計未來會推出其他方法,為類似 Starnix 嚴格篩選模式用戶端的用戶端解決這個問題,在不增加用戶端過多實作和維護負擔的情況下,提供核心彈性。

ZX_CLOCK_OPT_MAPPABLE 的替代方案

與其要求時鐘建立者決定特定時鐘是否可對應,不如在需要時,直接建立時鐘狀態 VMO。舉例來說,如果使用者呼叫 zx_vmar_map_clock(),但尚未分配任何 VMO,只要即時分配 VMO,並將其換成核心堆積狀態即可。

請注意,這項轉換無法復原。時鐘一旦設為可對應,我們就無法合理地將時鐘改回不可對應,因為我們無法控制對應的用戶端目前是否存在,也無法以任何合理的方式強制用戶端改回。

當然,您可以編寫程式碼來執行這項操作,但必須解決競爭狀況。zx_vmar_map_clock 附近需要導入鎖定 (或其他序列化機制),防止需要 VMO 分配的並行要求彼此競爭。在 VMO 儲存空間中交換,取代核心內狀態,實際上會是時鐘更新,需要序列鎖定寫入交易,同時將狀態從一個位置移至另一個位置。

雖然這可行,但需要更多程式碼、更複雜,也需要更多測試。此外,嚴格測試也意味著要測試並妥善處理並行要求導致的競爭狀況,而單元測試等方法無法確實做到這一點。

整體而言,要求使用者在建立時鐘時選擇加入「可對應性」,目前被視為合理且不會造成過度負擔的要求,可避免需要處理延遲繫結要求才能對應的複雜情況。

zx_vmar_map_clock() 的替代方案

在這項提案中,核心時鐘物件將可成為「可對應物件」,與 VMO 和 IOB 區域並列。與 IOB 區域一樣,系統會為其指派專屬的特殊 syscall,用於對應其基礎 VMO,但 VMO 本身絕不會直接提供給使用者模式。

另一種做法是略過專門的對應呼叫,改為讓使用者直接存取基礎 VMO。因此,請移除 zx_vmar_map_clock() 系統呼叫,並改為新增 zx_clock_get_vmo() 呼叫。

優點

假設相同的方法可以套用至 IOB 區域 (核心中其他可對應、非嚴格 VMO 物件),這表示系統不再需要考慮 VMO 以外的任何項目,做為可對應物件。這種一致性有助於使用者瞭解及推論系統。

++「Zircon 中有哪些物件『可對應』?方法很簡單!VMOs, and nothing else」。 ++「我可以用『clock vmo』做什麼?方法很簡單!授予控制代碼的權限允許您執行的任何操作。

「在診斷對應查詢中,我應該回報哪個 KOID?」這類問題將不再是問題。一個是直接回報 VMO 本身的 KOID。現在,它已正式存在 (「在幕後」) 做為核心調度器物件,我們可以直接使用指派給調度器的 KOID。同樣地,VMO 是可命名的物件,因此要回報的名稱現在也很明顯。

另一個好處是,開發人員可以更輕鬆地遵守先前所述的「必須完全按照我們指示的大小進行對應」規定。這項要求不會消失,但 zx_clock_get_vmo() syscall 的指定方式可以傳回必要的對應大小,以及 VMO 的控制代碼。這樣一來,使用者就必須知道大小才能取得 VMO (一開始對應時鐘狀態的硬性規定),因此很容易遵循規定。

最後,直接公開 VMO 可讓系統上層以其他方式共用時鐘。他們可以複製並共用時鐘本身的控制代碼 (具有適當權限),但現在也可以直接複製基礎 VMO 的控制代碼 (同樣具有適當權限),並共用該控制代碼。

缺點

不過,直接公開 VMO 並非完全沒有缺點。允許對應時鐘狀態的目的非常明確。從使用者的角度來看,甚至未定義內容對應的 VMO。使用者唯一要做的事,就是將對應的位址當做權杖,傳遞給專門的系統呼叫,其負擔比傳統系統呼叫低。

不過,如果公開 VMO 本身,就會公開大量額外的核心 API 介面區域。如果使用者能直接存取基礎 VMO,他們可能會嘗試執行許多功能上毫無意義的操作,但現在這些操作都可能發生。

++ 使用者可以建立時鐘狀態 VMO 的 CoW 複製,或任何子項嗎?這代表什麼? ++ 使用者可以控管時鐘狀態 VMO 的快取政策嗎? ++ 如果使用者嘗試設定時鐘狀態 VMO 的大小,會發生什麼事?「串流」大小呢?++ 使用者可以要求哪些 op_range 作業?這些內容有任何價值嗎?是否會造成任何安全威脅?++ 誰能控制 VMO 物件的屬性?任何人都可以設定 VMO 名稱嗎?還是應該加以限制?如果應該限制,該如何限制?

使用者現在可以執行的部分作業既無意義,也無害,例如 zx_vmo_read()。使用者絕對不需要這麼做,因為系統並未為使用者定義要讀取的內容 (依設計)。不過,允許他們這麼做會有任何危險嗎?技術上來說,是的,因為允許他們在不遵循序列鎖定通訊協定的情況下讀取資料,可能會導致正式的資料競爭,因為時鐘會同時更新,但實際上並非如此。他們可能會從讀取中得到垃圾資料,但系統不會停止並起火。

不過,對於許多其他作業,答案就不那麼明確。CoW 複製幾乎肯定不允許,但物件名稱呢?

您必須逐一評估這些潛在作業,確保允許作業不會影響安全性和隱私權。(至少基於謹慎考量) 預期大多數這類作業都會遭到禁止。如果沒有任何實質用途,預設立場應為不允許這些作業。

這表示現在必須強制執行這些限制。其中有些可以透過適當的控制代碼權限完成,但其他可能需要自訂強制執行機制。無論採用何種機制,核心中都會有需要強制執行這些限制的程式碼 (部分為新程式碼)。您必須編寫測試,確保限制已就緒且正確執行。測試和強制執行程式碼也需要永久維護,不僅是現有 API,未來新增至系統的任何 VMO API 也是如此。

大多數使用對應時鐘的使用者對這些都不感興趣。他們只想以最佳方式存取時鐘物件,並不想設定對應的快取政策,或建立時鐘狀態的子項複製版本 (無論這代表什麼意思)。使用者可能想設定對應名稱,但只要為時鐘物件本身命名,就能輕鬆完成這項操作,就像為 VMO 命名一樣。

我們很難不認為,讓使用者存取這個 API 介面,對對應時鐘的合法使用者沒有任何好處,但可能對試圖在公開 API 介面中尋找漏洞的惡意行為人有利。分析這些潛在攻擊、緩解攻擊,以及隨著系統未來發展而強制執行這些緩解措施的成本,目前尚未完全瞭解,但可以肯定的是,這些成本幾乎肯定不容小覷。

結論

目前,我們決定遵循 IOB 區域的前例,讓時鐘物件本身「可對應」,大幅縮減公開的 API 介面,並希望降低功能和維護的整體複雜度。

日後,我們可能會重新評估這項決定,或許會決定將時鐘和 IOB 區域與模型整合,其中基礎 VMO 會直接公開,而不是「除了 VMO 之外,還可以對應的特殊事物」,但目前系統只會提供極有限的能力,控管共用狀態的使用方式。

既有技術和參考資料

Current API

序列鎖