RFC-0274:核心輔助 CPU 剖析 | |
---|---|
狀態 | 已接受 |
區域 |
|
說明 | 以時間為準的單一工作階段 CPU 分析器,會利用影格指標從使用者執行緒取得回溯追蹤記錄 |
問題 | |
Gerrit 變更 | |
作者 | |
審查人員 | |
提交日期 (年-月-日) | 2025-05-29 |
審查日期 (年-月-日) | 2025-09-15 |
問題陳述
Fuchsia 開發人員需要有效的效能分析工具,包括 CPU 分析。目前 Fuchsia 缺少 CPU 分析的穩定 API,這對分析原生 Fuchsia 元件和在 Starnix 容器中執行的二進位檔效能至關重要。我們需要這個 API 來瞭解程式碼中的熱點。
摘要
本文提出 Fuchsia 的 CPU 剖析設計。具體來說,這項工具會根據時間,使用單一工作階段 CPU 分析器,並利用影格指標從使用者執行緒取得回溯。熟悉 Fuchsia 剖析的讀者可能會發現,建議的設計與目前已簽入的實驗性剖析器非常相似,但仍存在一些主要差異。
利害關係人
協助人員:
- davemoore@google.com
審查者:
- eieio@google.com
- mcgrathr@google.com
- wilkinsonclay@google.com
已諮詢:
- adamperry@google.com
- tq-performance@google.com
- maniscalco@google.com
社交:
這項 RFC 已在 zircon-discuss 郵寄清單上發布。
需求條件
為支援快速開發,本文建議的剖析器 API 刻意保持簡潔。這個 API 必須:
- 能夠使用框架指標,對使用者空間執行緒的呼叫堆疊進行取樣。
- 適用於 eng 和 userdebug 建構版本。
- 能夠收集 Fuchsia 程式 (包括 Starnix) 的架構指標式回溯追蹤記錄。
- 能夠收集在 Starnix 容器中執行的 Linux 程式的框架指標回溯。
- 支援以最高 4000 Hz 的頻率進行時間取樣。
而且在執行所有這些作業時,對系統效能造成的負擔應盡量減少。
明確的非目標和未來工作
我們明確希望避免以下情況,以免偏離讓 Zircon 執行階段與語言無關的目標:
- 在核心中加入對專屬語言執行階段 (例如 Go 執行階段) 的認知。
此外,在制定這項提案時,我們也考慮了許多剖析功能,但為了加快進展,已將這些功能排除在範圍之外。這些項目與設計相容,但會盡可能分開,做為日後加購項目。
- 硬體輔助剖析 (例如 Intel LBR 或 ARM 效能計數器)。
- 在使用者空間初始化之前,以及可進行系統呼叫之前,分析早期系統啟動程序。
- 剖析核心呼叫堆疊。
- 處理動態連結程式庫在我們從中載入符號之前,重複使用位址 (透過 dlopen/dlclose) 的情況。
- 在支援的架構上支援陰影呼叫堆疊。
- 支援多個並行剖析工作階段。
- 可設定要剖析哪些工作 (執行緒、程序或工作)。
如需更詳細的說明,請參閱「未來工作」。
設計
核心設計
圖 1 - 核心結構
上圖概述了我們計畫實作的系統。這項功能會使用單一全域取樣工作階段,提供以時間為準的取樣存取權。
一般流程如下:
- 使用者發出系統呼叫,根據設定建立取樣器。在此系統呼叫期間:
- 核心會分配、對應及釘選用於儲存取樣資料的每個 CPU 緩衝區。
- 系統會為每個 CPU 建立單調計時器,以下稱為「取樣計時器」。
- 系統會傳回控制代碼,使用者可透過該代碼要求控制平面作業,以及讀取資料。
- 接著,使用者會發出系統呼叫來啟動取樣,這會從設定每個 CPU 的取樣計時器開始。
- 每次觸發取樣計時器時,我們會檢查目前的執行緒。如果應取樣執行緒,我們會取樣並寫入目前 CPU 的緩衝區。
- 使用者隨時可以讀取取樣資料。
- 使用者可以繼續取樣,直到想停止為止。此時,使用者會叫用系統呼叫來停止取樣、從緩衝區讀取剩餘資料,然後關閉取樣器控制代碼。
主要邏輯會在 ThreadSampler 中實作,這是包含工作階段所需狀態的全域單例項。使用者可以透過下一節所述的 zx_sampler_*
系統呼叫與其互動。
Syscall API
下列系統呼叫 API 會用於與取樣器互動。請注意,我們考慮了幾種不同的替代方案;這些替代方案及其優缺點會在「考慮的替代方案」一節中討論。
zx_sampler_create
下列系統呼叫會建立取樣器,並傳回取樣器的控制代碼:
// The sampler_config_t sets the properties of a sampler.
//
// Note that there is no version number on this struct; the options argument in
// zx_sampler_create can be used as one to evolve this struct.
struct zx_sampler_config_t {
// How often a sample should be taken.
zx_duration_mono_t sample_period;
// The maximum number of stack frames to collect in each sample.
uint16_t max_stack_depth;
};
// zx_sampler_create will create the sampler if it has not already been created
// and then return a handle to it.
//
// Arguments:
// * sampler_resource: A handle to the sampling resource.
// * options: Must be zero.
// * config: The configuration to sample with.
// * sampler_out: An out parameter that will contain a handle to a
// sampler on success.
zx_status_t zx_sampler_create(
zx_handle_t sampler_resource,
uint64_t options,
const zx_sampler_config_t* config,
zx_handle_t* sampler_out);
sampler_resource
是新建立的系統資源,用於控管取樣器建立作業。
如果使用者空間透過 zx_handle_close
關閉這個系統呼叫傳回的控制代碼,工作階段就會遭到刪除,且所有目標都會停止取樣,如同呼叫 zx_sampler_stop
一樣。
在本 RFC 中,我們計畫實作具有單一全域單例項取樣器的取樣器,且一次只允許建立一個取樣器。如果全域工作階段正在使用中,zx_sampler_create
會傳回 ZX_ERR_ALREADY_EXISTS
,直到現有控制代碼關閉為止。請參閱「未來工作」中的「多個工作階段」。
zx_sampler_[start|stop]
系統會使用下列系統呼叫啟動及停止取樣:
// zx_sampler_start will start sampling, and zx_sampler_stop will stop sampling.
//
// Arguments (for both syscalls):
// * sampler: A handle to the global sampler.
// * options: Must be zero for now.
zx_status_t zx_sampler_start(zx_handle_t sampler, uint64_t options);
zx_status_t zx_sampler_stop(zx_handle_t sampler, uint64_t options);
請注意,在取樣器已在執行時叫用 zx_sampler_start
無效,且會傳回 ZX_ERR_BAD_STATE
。同樣地,如果取樣器未執行,呼叫 zx_sampler_stop
也是無效的,且也會傳回 ZX_ERR_BAD_STATE
。
zx_sampler_read
以下系統呼叫會用於讀取取樣器資料:
// zx_sampler_read reads sampler data.
//
// Arguments:
// * sampler: A handle to the global sampler.
// * options: Must be zero.
// * buf: The user buffer to read data into.
// * buf_len: The size of buf.
// * actual: An out parameter that will contain the amount of data that was
// actually read on success.
zx_status_t zx_sampler_read(
zx_handle_t sampler,
uint64_t options,
void* buf,
size_t buf_len,
size_t* actual);
這項函式會將所有可用的取樣資料讀取到提供的緩衝區。如果緩衝區太小,無法容納所有資料,系統會傳回 ZX_ERR_INVALID_ARGS
。
當取樣未進行時 (即呼叫 zx_sampler_stop
後),即可有效叫用此系統呼叫。在這種情況下,系統會讀取並傳回取樣期間產生的任何資料。
取得樣本
計時器觸發時,我們會檢查目前的 CPU。如果我們已遷移,我們會立即返回。接著,我們會檢查目前 CPU 上執行的執行緒。如果執行緒不是我們想取樣的執行緒 (例如核心執行緒),我們只會記錄時間戳記並傳回,因為核心堆疊取樣不在本提案的範圍內。
即使目前的執行緒是使用者執行緒,我們也無法立即從計時器回呼取樣呼叫堆疊。這是因為我們無法從中斷環境安全地讀取使用者記憶體,因為這可能需要發生頁面錯誤。而是設定 THREAD_SIGNAL_SAMPLE_STACK
執行緒信號並傳回。
稍後,當執行緒即將結束核心時,系統會叫用 ProcessPendingSignals,檢查是否有 THREAD_SIGNAL_SAMPLE_STACK
。如果存在信號,我們會對堆疊進行取樣。我們可以在這裡安全地嘗試讀取使用者記憶體,因為:
- 我們不再處於中斷環境,因此可以視需要發生頁面錯誤
- 我們已解除大部分的保留款項
- 核心堆疊相對較淺,因此我們可以在核心堆疊上讀取可能很深層的呼叫堆疊。
實驗性 API 遇到的問題是,如果特定行程觸發計時器超過一次,這種方法可能會導致樣本遺失,因為每個行程只會擷取一個堆疊樣本。我們改良了實驗性取樣器,會立即發出含有時間戳記和其他輔助資料的部分記錄,然後再發出含有堆疊追蹤記錄的後續記錄。一或多個部分記錄可能參照相同的堆疊追蹤延續記錄 (請參閱「資料格式」)。格式)。由於部分記錄不會讀取使用者記憶體,且寫入的緩衝區已對應並固定,因此在插斷環境中發出是安全的。
從使用者空間取樣呼叫堆疊
取樣作業完全可以在使用者空間中進行,但效率不彰,目前是剖析的選項。不過,這樣做需要多次呼叫 zx_process_read_memory
,且每個樣本大約需要 2 到 3 毫秒。即使取樣率為 100 Hz,也會佔用 20% 至 30% 的 CPU,造成難以承受的負擔。核心中的類似作業需要 2 到 3 微秒,因此我們能以超過 4000 Hz 的取樣率取樣,且負擔較低。
使用頁框指標還原呼叫堆疊
堆疊會由步行框架指標讀取。取樣需要啟用影格指標的建構作業。Fuchsia 目前預設會在使用者產品和核心中啟用框架指標。
由於使用者程式可控管自己的堆疊,核心在讀取使用者記憶體時必須採取一些防範措施。必須謹慎地只追蹤指向取樣目標有效記憶體的指標,並可能需要限制取樣堆疊的深度。使用者堆疊可能相當深,超過 30 個堆疊影格,因此核心會提供可設定的堆疊最大深度,以取樣 Syscall API 一節中提及的內容。
由於影格指標可提供良好的跨平台用途,並限制複製的記憶體量,因此會使用影格指標,而非其他方法。我們確實打算在未來探索及實作其他選項。
符號化
使用者程式如要符號化收到的指令指標呼叫堆疊,必須取得目標程式的額外資訊。它需要知道哪些程式庫和可執行檔對應到哪些位置。使用者空間負責取得相關資訊,以符號化收到的呼叫堆疊。
符號化作業需要先將呼叫堆疊上的指令指標,轉換為適當 ELF 二進位檔中的位移。如今,使用者空間會透過下列方式執行這項操作:
- 使用
zx_object_get_info(ZX_INFO_PROCESS_MAPS).
擷取目前程序中的記憶體對應 - 反覆查看傳回的
zx_info_maps_t[]
,找出 ELF 二進位檔的對應關係,以及載入這些二進位檔的基底位址。 - 在每個基底位址上使用
zx_process_read_memory
讀取 ELF 標頭,取得對應至對應二進位的建構 ID。 - 工作階段完成後,系統會使用 ELF 建構 ID 和基底位址,以及偵錯資訊資料庫,將指令指標符號化。
由於重複查詢的成本很高,使用者空間 CPU 分析器目前只會掃描對應兩次。第一種情況是攔截執行緒的偵錯啟動通知,這類通知是由 ld.so 在連結執行緒的程式庫後直接觸發。第二個是剖析工作階段結束後立即產生。 第二次掃描的目的是擷取可能在第一次動態連結後載入的任何動態載入程式庫 (例如透過 dlopen)。
雖然在實務上,這會擷取現有 Fuchsia 程式使用的大部分對應,但就動態載入的程式庫而言,這並不完全正確:
- 它不會處理在程序結束前取消對應/卸載/dlcose 的對應,針對不同程式庫重複使用相同位址可能會導致符號損毀
- 在工作階段結束前結束的程序不會再次掃描動態載入的程式庫,且只會包含動態連結程式庫的符號。
我們將 dlopen/dlclose 的處理作業留給這個設計的後續疊代,因為支援這些作業需要在核心、動態連結器和載入器、元件管理員及其載入器服務實作之間進行額外協調,可能還需要檔案系統,才能協調符號化動態載入檔案所需的資料。
從核心串流資料
取樣器剖析器會產生大量資料,因此需要有效率地從核心串流輸出。幸好,Fuchsia 最近新增了核心追蹤的串流支援功能,我們打算採用類似的做法。
簡而言之,我們打算使用每個 CPU 的單一讀取器/寫入器環形緩衝區,儲存取樣器資料。每個緩衝區的寫入器都可以繼續作業,不必取得任何鎖定,而讀取器和其他控制作業則會使用鎖定。緩衝區已滿時,任何嘗試寫入緩衝區的新記錄都會遭到捨棄,這與現有核心 FXT 寫入程式庫支援的行為相符。
資料格式
資料會以 FXT Blob 記錄的形式寫入每個 CPU 對應的固定緩衝區,實作方式與核心追蹤相同。由於緩衝區已對應並固定,因此在中斷環境中寫入緩衝區時,不會有頁面錯誤的風險。
如果使用者空間元件無法快速讀取資料,且我們嘗試寫入會導致緩衝區溢位的記錄,系統會捨棄該記錄,並記錄捨棄的記錄數量。當使用者空間釋出緩衝區空間時,我們會發出記錄,讓使用者瞭解有多少記錄遭到捨棄。
發出的記錄
我們會將下列 FXT Blob 記錄寫入緩衝區,其中包含以下資訊:
範例記錄
FXT Blob Record {
u64 fxt_header, // Blob_type SAMPLE
u64 fields_bitflags,
// Each of the following optional fields are only included if the
// corresponding `fields_bitflags` bit is set
Optional<u64> continuation,
Optional<u64> continuation_completion,
Optional<u64> pid,
Optional<u64> tid,
Optional<u64> boot_ts,
Optional<u64, [u64]> user_fp_call_stack,
}
樣本會寫入為 FXT Blob 記錄。Blob 中的第一個欄位是 u64,其中包含每個欄位的位元欄位。只有在 bitflags 中設定對應位元時,系統才會依所述順序顯示這些欄位。位元旗標的位元分配方式如下:
- 1:續傳 (u64),如果後續還有樣本,請設定此值
- 1:continuation_completion (u64),如果這會將樣本新增至先前的記錄,請設定這個值
- 1:pid (koid/u64)
- 1:tid (koid/u64)
- 1: boot_ts (u64)
- 1:user_fp_call_stack (以長度為前置字串的 u64 陣列)
- 48:保留
接續
記錄可以透過設定「continuation」位元並提供 u64 ID,指出後續還有其他資訊。後續記錄應設定「continuation_completion」位元,並使用相符的 ID,內容應附加至先前的記錄。多筆記錄可能會使用相同的續傳 ID,而與這類 ID 相符的續傳完成記錄,其樣本會重複並附加至每筆記錄。
每個 CPU 都會保留單調遞增的計數器 (在 CPU 數量之間分配 u64 空間),並在發出每個完成記錄後遞增。
https://fxrev.dev/1252454 提供了範例,說明如何從中斷內容發出這些接續記錄。
CPU 事件
核心可以修改 CPU 的電源狀態,這可能會影響取樣行為。如要瞭解這些與電源相關的 CPU 作業和取樣互動,請參閱這篇文章。
CPU 熱插拔
緩衝區由個別 CPU 外部的資料結構擁有。CPU 離線時,緩衝區會保留,但不會寫入資料。讀取器可以隨時讀取資料,而緩衝區會在工作階段控制代碼關閉時遭到毀損。
停權
這項設計的初始實作會使用單調計時器,觸發呼叫堆疊的收集作業。因此,系統暫停期間 (特別是暫停至閒置狀態) 不會收集任何樣本。我們認為這是可接受的解決方案,因為在暫停至閒置期間不會排定任何執行緒,這表示即使我們使用啟動計時器,也不會對任何呼叫堆疊進行取樣。
使用者空間設計
使用者空間主要透過「cpu_profiler.cm」元件與剖析互動。使用者空間元件負責:
- 從
zx_sampler_create
篩選資料,轉換較高層級的使用者空間設定 (例如依 tid/pid/測試/元件路徑名稱/網址附加)。 - 服務緩衝區和竊取資料
- 讀取目標的 ELF 標頭,取得符號化資料
- 與 ffx、starnix 或其他呼叫端通訊
實作
實作作業會從現有的實驗性取樣器 API 開始,將其與本文件所述內容保持一致,並在文件內容變更時更新 API。
與安全、隱私權和 API 委員會諮詢,並解決提出的問題後,API 會放入下一個 vdso,持續開發和疊代,直到獲得批准。
提案 API 與實驗 API 的差異
與上述提案相比,現有的核心取樣支援功能缺少以下項目:
- 支援每個行程的多個樣本進入核心。
- 核心串流支援
成效
取樣 API 應允許以 4000 Hz 的頻率取樣,且額外負荷低於 10%,或每 250 微秒取樣週期執行時間少於 25 微秒。您可以檢查啟用和未啟用剖析功能時,對基準的影響,以及測量啟用和未啟用剖析功能時的 CPU、記憶體和其他資源用量,藉此評估影響。
人體工學
建議的 API 可讓呼叫端在取樣工作階段期間避免呼叫 zx_process_read_memory
,大幅簡化呼叫堆疊取樣的實作程序。
回溯相容性
這個 API 是 Zircon 公開的現有 API 以外的新增項目。我們打算在日後疊代這個 API,因此納入未來工作相關章節,以及如何擴充提案的 API 來支援這項工作。我們保留了預留欄位和其他機制,以便以回溯相容的方式演進這個 API。
安全性考量
我們推出新的「取樣」資源,與現有的核心追蹤資源類似。這會由元件管理員轉送至單一使用者空間剖析元件,該元件負責將較高層級的使用者空間設定 (例如依據 tid/pid/測試/路徑名稱/網址附加) 轉換為 zx_sampler_create
資料的篩選條件。
安全性審查摘要
- 透過特定取樣資源、cpu_profiler 元件用戶端的有限允許清單、對 eng/userdebug 建構版本的限制,以及偵錯系統呼叫指令列標記 (最後一個標記可能需要在某個時間點放寬 userdebug 的限制;請參閱下文),可有效控管用戶端存取權。
- 惡意追蹤堆疊的風險有限,因為系統會控管堆疊深度上限和基本指標驗證。有人可能會想像使用側管道讀取偽造影格指標所指向的具備權限記憶體,但這不太可能造成任何實際問題。核心中的程式碼會盡量減少,特別是避免在該處進行 ELF 剖析。
- 如要在使用者偵錯版本上允許取樣器系統呼叫存取權,但不要啟用所有其他偵錯系統呼叫,可能需要進一步討論 (超出這個初始設計的範圍)。
隱私權注意事項
在三種主要情況下,系統會擷取設定檔,並從 Fuchsia 裝置中移除資料。
1) 本機開發人員在工程裝置上進行效能診斷/疑難排解。 2) 基礎架構端對端 CUJ 效能測試。基礎架構會使用測試帳戶,在預先決定的重要使用者歷程中自動執行測試。我們會啟用追蹤和剖析功能,監控效能特徵。3) 快照和欄位追蹤。這項工作流程會定期記錄測試裝置上的設定檔,因此需要額外的隱私權控管措施。這項方法收集的資料是由 Perfetto 收集,該工具已具備現有的隱私權控制項,例如追蹤記錄篩選和編輯。如要新增其他欄位設定檔以供收集,還須通過額外的隱私權審查。
存取私密資料和資料保存
雖然呼叫堆疊和執行追蹤記錄有助於分析效能,但可能會公開機密資料。呼叫堆疊中的函式名稱、參數,甚至是資料本身的部分內容,都可能揭露處理中的資料類型或其他私人資訊。
這類私密資料預計會顯示在開發人員裝置和基礎架構測試裝置上,且不會由剖析堆疊的較高層級進行修訂。因此,在用途 (1) 和 (2) 中,系統只會在收到要求時擷取設定檔,並立即以檔案形式將資料傳回給要求者,不會儲存或上傳至其他位置。
存取限制
我們使用以下組合: - 路由至元件建構時間允許清單的系統資源 - 僅限元件建構時間允許清單的 FIDL API - 使用者產品的建構時間停用功能 防止私密資訊遭到意外揭露。如果想儲存資料,較高層級的允許清單元件會負責修訂和篩選資料。如果沒有上述隱私權控制項,系統絕不會自動在使用者裝置上擷取設定檔。
測試
目前的 CPU 分析器實作項目存在於核心和使用者空間層級的單元、整合和端對端測試中,可測試目前的 zx_process_read_memory 實作項目 (以及啟用時的目前實驗性 API)。
這些測試會繼續測試相關流程,並擴充測試範圍,涵蓋此處建議新增的功能,例如串流和新的記錄類型。
說明文件
我們需要更新 //docs/development/profiling/profiling-cpu-usage.md 的 CPU 剖析器使用說明文件,反映不需要建構標記。
我們也需要更新及修正 zx_sampler_* 系統呼叫的系統呼叫說明文件。
缺點、替代方案和未知事項
在擬定這份設計文件時,我們考慮了許多替代方案。 我們已排除部分替代方案,改用更優質的選項,但有些替代方案只是因為目前產品不需要而遭排除。因此,日後這個設計的疊代版本可能會納入本節中的部分項目。
替代系統呼叫 API
以工作為準
或者,工作是可從中探查資訊的事物。使用者會設定工作,以開始對輸出緩衝區進行取樣。您可以透過設定屬性處理控制平面。
zx_status_t zx_task_probe(zx_handle_t profiling_resource,
zx_handle_t task,
zx_sample_config_t config,
zx_handle_t* ep0_out // Buffer to write to)
zx_set_property(ep0, ZX_PROBE_STATE, ZX_PROBE_STATE_START);
優點
- 不需要新的調度器,資料會儲存在工作調度器和 iob 中
缺點
- 沒有明顯的方法可要求核心執行緒的取樣,我們無法取得這些執行緒的控制代碼
- 無法明確指定全系統取樣。也許設定檔中指定了 root-工作 + kernel_threads。
- 取樣兩個不相關的執行緒/程序需要多個工作階段 (或額外邏輯,將輸出內容合併至單一緩衝區)
以工作設定檔為準
與以工作為準的設定檔類似,但我們也會運用執行緒設定檔基礎架構。在這個方法中,我們會建立含有設定和輸出緩衝區的 probe_profile,並將其套用至工作。
zx_profile_create(zx_resource_t, zx_sample_config_t config, &ep0,
&profile);
zx_task_set_profile(zx_handle_t task, zx_handle_t profile, ...);
zx_set_property(zx_handle_t task, ZX_PROBE_STATE, ZX_PROBE_STATE_START);
優點
- 重複使用現有的核心基礎架構
- 明確區分關注事項:建立設定檔需要權限,執行緒可以決定是否應套用設定檔,控管設定檔需要與建立設定檔不同的權限,讀取和控管設定檔都可以個別處理。
缺點
- 系統呼叫分散在三個不同的
zx_
前置字元中,難以追蹤且無法探索 API。
未合併的系統呼叫
我們可以改為提供一對系統呼叫給使用者空間:
- 等待所要求事件發生的能力
- 這是一種核心輔助資料收集方式,可有效收集目標的相關資料
優點:
- 緩衝區配置和寫入作業完全可以在使用者空間中完成
- 設定複雜度可分散到多個呼叫中
缺點:
- 喚醒使用者空間剖析器會增加額外負荷,但剖析器會立即呼叫核心,然後再次進入休眠狀態,速率為 16000 Hz (4 個 CPU 的取樣率為 4000 Hz)。
- 4 個 CPU 會爭相喚醒/通知單一使用者程序
- 額外副本:使用者空間會將取樣資料複製到自己的緩衝區
- 目標程序必須暫停,直到喚醒剖析器並完成取樣為止。
這個問題的剖析很精彩,但我們 (坦白說,有點毫無根據) 認為這會造成不必要的負擔,而且很難有效率地完成。標準最佳化方法:
- 提供事件的範例資料,不必額外進行系統呼叫來取得資料
- 允許指定要將事件寫入何處,避免額外副本
- 允許批次處理讀取事件,避免喚醒
導致我們重新推導上述變體。
核心輔助符號化
Linux 用於追蹤對應項目的策略,是在可執行檔記憶體對應至附加程序時發出記錄:
struct {
struct perf_event_header header;
u32 pid, tid;
u64 addr; // Mapping address in target u64
usize len; // Length of mapping
u64 pgoff; // Page offset of mapping
char filename[]; // Location of backing memory
};
由於我們沒有類似的全域檔案系統概念可讀取載入的程式庫,因此在 Fuchsia 中較難實現這項功能。共用物件會隨每個元件套件一併發布,外部元件不容易存取這些物件 (也不一定需要存取)。
雖然我們可使用 elf-search (如「符號化」一節所述) 取得所需資訊,但仍會遇到相同限制,也就是只有在附加程序存留期間才能使用。我們最終仍會遇到競爭條件,也就是在收到 mmap 通知後,如果我們沒有盡快對通知採取行動,程序可能會結束。
除了使用檔案名稱,其他做法還包括:
- ELF 建構 ID,這會涉及教導核心從使用者記憶體剖析 ELF,或
- 相關對應的 koid,這會涉及使用者空間維護某種 koid -> 建構 ID 服務。
如果我們發現使用 ZX_INFO_PROCESS_MAPS
取得基底位址,以及在使用者空間中取得建構 ID 的方法效能不足,可能就需要更認真考慮這些選項。zx_process_read_memory
後續作業
使用者可能需要許多功能,本節彙整了本文提及或未提及的各種功能,這些功能會影響未來的理想狀態。
多個工作階段
多個工作階段是所需功能,例如,可同時剖析 FIDL 用戶端和伺服器,而不需進行全系統剖析。不過,由於這會帶來許多額外的複雜性,因此提案將其排除在範圍之外。我們建議使用 API,這些 API 表面上可用於多個工作階段樣式 API,但如果建立多個工作階段,就會發生錯誤。我們會在後續版本中支援多個工作階段。
提早啟動剖析
如目前的設計,需要使用者空間元件,並依賴領域查詢 API 的元件管理工具,會對我們何時開始設定剖析設下一些限制。具體來說,在元件管理員準備就緒並可啟動使用者剖析元件之前,無法進行剖析。
後續可能需要透過啟動引數 (例如 ktrace) 啟用早期啟動剖析功能。開機引數會說明緩衝區大小和設定。這項功能會在核心準備就緒時自動開啟剖析功能,並允許使用者在緩衝區中讀取資料,而不必覆寫資料。這項作業可以在核心或使用者啟動程序中完成。接著,我們可以在啟動時將取樣器控制代碼傳送至 component_manager。
取樣核心堆疊
在處理要求核心堆疊的事件時,我們能夠立即走訪核心的框架指標鏈,因為這不需要使用者副本,並寫出記錄,以供稍後可能的使用者空間呼叫堆疊延續。
Zircon 和停用中斷
由於我們依賴計時器中斷,且部分核心工作是在中斷停用時完成,因此我們無法掌握這些工作。舉例來說,zx_futex_wake
會啟用中斷功能來處理基本控制碼驗證,但走訪 futex 清單和喚醒等待者時,都會停用中斷功能。這會限制核心堆疊的精細程度,因為取樣中斷只會在重新啟用中斷後觸發。在這種情況下,我們只會看到 CPU 時間歸因於重新啟用中斷的位置,通常是 Guard::~Guard
解構函式。
CPU 效能計數器
主要的 CPU 架構提供一組 CPU 暫存器/計數器,可設定用來計算指令執行、快取未中斷或分支未中斷等事件。這些計數器可能會在溢位時產生中斷,讓我們有機會重設計數器並記錄資料。通常只有權限模式程式碼可以存取及設定這些功能,並可選擇向無權限程式碼公開部分功能。
軟體事件
OS 作業中的軟體事件 (例如頁面錯誤、系統呼叫或內容切換) 可讓使用者程式診斷程式碼中的熱點,這些熱點會導致發生這些事件。雖然部分事件可使用使用者空間包裝函式進行插樁,但核心可有效記錄這些事件的發生時間,是可靠的資訊來源。
以工作為準或可感知情境切換的事件
由於硬體計數器是每個 CPU 的資源,因此必須與取樣的執行緒一併儲存及還原。否則,每次環境切換後的第一個樣本會是隨機的,取決於之前排程的程序。同樣地,瞭解情境切換可讓我們只從感興趣的任務取樣,而非在後續篩選,藉此更有效運用緩衝區空間。
堆疊取樣的替代方法
目前的設計會使用框架指標來逐步執行呼叫堆疊。這是個不錯的起點,但我們可能想支援其他更架構專屬的方法,例如在 ARM 上使用 shadow-call-stack,或在 Intel 上使用 LBR。我們最終可能也會想支援使用者空間程式,指定核心可用來產生呼叫堆疊的二進位檔 (類似於 DTrace 或 ebpf)。
既有技術和參考資料
什麼是 CPU 剖析?
本文將說明以樣本為基礎的 CPU 剖析。如果您不熟悉這項技術,這是一種可觀測的方法,有助於回答「程式碼的熱點在哪裡?」這個問題。與追蹤相比,取樣不需要新增編譯時間追蹤點,但資料是隨機取樣。以樣本為基礎的剖析主要前提是,要求每次目標發生「x」時,記錄一些「y」資料。「x」可以是「每 250 微秒」或「每 1000 次 L1 快取未命中」,而「y」通常是呼叫堆疊和時間戳記。結果通常會以火焰圖的形式呈現,而非時間序列。
雖然以 Linux 為主,但 Brenan Gregg 精彩地介紹了這種方法可進行的分析類型。
其他作業系統採用的方法
Linux
Linux 會將「效能事件」(例如硬體計數器、計時器或軟體事件) 視為觸發時會將資料寫入檔案描述元的項目。如要取得其中一個 fd,Linux 提供「perf_event_open」系統呼叫。並建立檔案描述元,在發生要求的事件時,將可設定的資料量寫入其中。對於低頻率事件,檔案描述元可以直接讀取;對於高頻率事件,檔案描述元則會對應並讀取,就像環形緩衝區一樣。
Windows
Windows 提供一般事件追蹤架構。在收集的事件中,呼叫者可以要求呼叫堆疊,並使用影格指標擷取這些堆疊。
macOS 和 Dtrace
MacOS 提供 DTrace。DTrace 並非提供特定的「取得某些設定檔」系統呼叫,而是公開一般用途的指令碼語言介面,可讓您在發生某些事件時定義可自訂的行為。例如:
dtrace -x ustackframes=100 -n 'profile-99 /execname == "starnix_runner.cm" && \
arg1/ { @[ustack()] = count(); } tick-60s { exit(0); }' -o out.stacks
以 99 Hz 的頻率,對名為「starnix_runner.cm」的工作取樣堆疊框架。
部分作業系統會將 DTrace 做為主要的剖析方法 (例如透過 Instruments.app 和其他 BSD 的 macOS),或將其做為選用的外掛程式 (例如 DTrace 會以可載入的核心模組形式實作 Linux,也會透過 SystemTap 顯示類似功能)。