Cannoli是一款面向qemu用戶的高性能跟踪引擎,是一個Rust 編寫的Python(Python 3.6.5) 編譯器,旨在評估對性能有負面影響的Python 語言特性。它可以記錄所執行的PC 以及內存操作的軌跡。
Cannoli 旨在以最小的QEMU 執行干擾記錄這些信息。這意味著QEMU 需要產生一個事件流,並將它們移交給另一個進程來處理更複雜的事件分析。在QEMU JIT執行期間進行分析會大大降低執行速度。
Cannoli可以每秒處理數十億條目標指令,可以處理多線程qemu-user應用程序,並允許多個線程使用來自單個QEMU線程的數據以並行處理跟踪。
性能要求QEMU中有一個trace模塊,可以對於一些函數進行跟踪,例如qemu_malloc, qemu_free等,對於QEMU本身的調試很用幫助。跟踪的一個大問題就是性能。許多簡單的解決方案(如使用Unicorn進行模擬)可能導致每秒只能得到100萬條左右的模擬指令。這聽起來可能很多,但是現代的x86處理器通常每個週期執行2條指令。如果你在4 GHz處理器上運行,運行或啟動需要1秒,通常會執行50 -100億條指令。簡而言之,以每秒1000 萬條指令的速度跟踪這樣的事情對於任何開發或研究週期來說都是非常緩慢的。
僅僅試圖獲取PC 的執行指令地址的完整日誌通常就很困難,更不用說記錄所有內存訪問。
這就不得不用到Cannoli了!
Cannoli 能夠執行QEMU 的完整跟踪,比基本QEMU 執行速度降低約20-80%。這意味著每秒可以獲得大約10 億條目標指令。不過,這會因你的CPU 時鐘頻率、系統噪聲等而有很大差異。我們稍後會詳細介紹性能,因為存在很多細微差別。 Cannoli 經過精心設計,可以將超過20 GiB/s的數據從單個QEMU 線程流式傳輸到多線程跟踪分析過程。
QEMU 補丁作為用戶,最好打個補丁,其中包含大約200 行新添加的內容。這些都是用#ifdef CANNOLI進行控制的,這樣,如果CANNOLI沒有定義,QEMU構建時就完全等同於沒有任何補丁。
這些補丁與用戶沒有太大的相關性,只是知道它們向QEMU添加了一個-cannoli標誌,該標誌期望獲得到共享庫的路徑。這個共享庫被加載到QEMU中,並在JIT的不同位置調用
要打補丁,運行執行以下命令:
git am qemu_patches.patch
Cannoli服務器加載到QEMU 中的共享庫稱為Cannoli 服務器。該庫在cannoli_server/src/lib.rs 中公開了兩個基本回調函數。
這些掛鉤為用戶提供了一個機會來決定是否應該掛鉤給定的指令或內存訪問。返回true(默認值)會導致檢測指令。返回false 意味著沒有向JIT 添加任何檢測,因此QEMU以高速模擬的方式運行。
當QEMU提取目標指令時,將調用此API。在這種情況下,提取是模擬器的核心操作,它在其中反彙編目標指令,並將其轉換為IL 或JIT 到另一個架構中執行。由於QEMU緩存它已經提取的指令,這些函數被稱為'rarely'(與指令本身執行的頻率有關),因此這是你應該放入智能邏輯以過濾掛鉤內容的位置。
如果掛載少量指令,該工具的性能消耗幾乎為零。 Cannoli旨在為完全跟踪提供非常低的消耗,但是如果你不需要完全跟踪,你應該在這個階段進行過濾。這從一開始就防止了JIT被檢測到,並為最終用戶提供了一種過濾機制。
Cannoli客戶端然後Cannoli 有一個客戶端組件,客戶端的目標是處理QEMU 產生的海量數據流。此外,Cannoli 的API 在設計時考慮了線程,因此單個線程可以在qemu-user 中運行,並且可以通過線程化分析來完成對該流的複雜分析,同時在QEMU 本身中獲得最大的單核性能。
Cannoli 公開了一個標準的Rust 特徵樣式接口,你可以在你的結構上實現Cannoli。作為這個特徵的實現者,你必須實現init。這是你為單線程可變上下文Self 以及多線程共享不可變上下文Self:Context 創建結構的位置。
然後,你可以選擇實現以下回調:
這些回調是相對不言自明的,除了線程方面。三個主要的執行回調exec、read 和write 可以從多個線程並行調用。因此,這些不是按順序調用的。這是應該進行無狀態處理的地方。它們也只有對Self:Context 的不可變訪問,因為它們是並行運行的。這是進行任何不需要知道指令或內存訪問的順序/順序的處理的正確位置。例如,應在此處完成從pc 轉換為符號+ 地址的符號應用,以便你可以進行符號化跟踪。
所有主要的回調,exec、read 和write,都返回一個Option
然後,該跟踪通過跟踪回調完全按順序向用戶公開。跟踪回調函數是從各種線程調用的,例如,你可能在不同的TID 中運行,但是,它是否確保始終按順序和相對於執行的順序調用。因此,你可以獲得對self 的可變訪問,以及對共享Self:Context 的引用。
我知道這是一個奇怪的API,但它有效地允許並行處理跟踪,直到你絕對需要它是連續的。我希望它不會讓最終用戶感到困惑,但是處理10億條指令/秒的數據需要在消費者端進行線程處理,否則QEMU就會成為瓶頸!
注意:除非另有說明,否則此處的性能數據是基於我的Intel(R) Xeon(R) Silver 4310 CPU @ 2.10GHz。啟用超線程,啟用turbo,128 GiB RAM @ 2667 MHz w/8內存通道。
在高層次上,整個設計圍繞著大量數據的運營程序(在JIT 中運行的QEMU 線程)。對於Cannoli,我們專門針對QEMU 的QEMU 單線程吞吐量進行優化,以便我們可以對長時間運行或大型進程(如Web 瀏覽器)進行內省。
這與縮放不同,在縮放中你並不真正關心單個線程的性能,而是整個系統。在我們的示例中,我們希望支持每秒從單個QEMU 線程流式傳輸數十億條指令,同時使用線程用戶對這些數據進行相對複雜的分析。
基本基准在examples/benchmark中,你可以找到一個運行小mipsel二進製文件的基準,該二進製文件只是在一個循環中執行一堆nop。這意味著對PC跟踪的性能進行基準測試。
要使用此基準,請啟動基準客戶端:
cdexamples/benchmarkcargorun--release然後使用cannoli'd QEMU 運行基準測試!
/home/pleb/qemu/build/qemu-mipsel-cannoli~/cannoli/target/release/libcannoli_server.so./benchmark在示例中,我得到以下信息(在我的例子中,我使用了benchmark_graph)。
在單個QEMU線程上跟踪大約每秒22億條指令。
Mempipe在低性能消耗的情況下獲得這種級別的跟踪需要一些非常獨特的設計。 Cannoli的核心是一個名為mempipe的庫。在高層次上,這是一種延遲極低的共享內存IPC機制。
最重要的是,JIT 掛鉤的設計是盡可能地減少消耗,特別注意分支預測、代碼大小(用於減少icache 污染)等細節,並註意目標架構的位數以減少運行32 位目標時產生的數據量。
事實上,你會發現幾乎所有的API,除了高級Cannoli 特徵,利用Rust 宏定義相同代碼的兩個副本,一個用於32 位目標,一個用於64 位目標。這略微增加了代碼複雜性,但當我們飽和內存帶寬時,我們希望特殊情況下,32位目標產生的有效數據只有1/2小指針大小。
核心mempipe IPC 機制允許在適合L1 緩存的進程之間傳輸緩衝區。由於這些緩衝區非常小,IPC 數據包的頻率非常高。這很難實現。我可以對1字節的有效負載每秒進行大約1000萬次傳輸。這有效地飽和了英特爾芯片對緩存一致性通信量所能做的事情。
緩存一致性如果你不熟悉緩存一致性,那麼你的處理器就是這樣做的,以確保內存在所有處理器上都是相同的值,即使它存儲在多個緩存中。
簡單的模型是MESI 模型。這定義了每個緩存行可以處於的狀態。
Modified ——存儲在高速緩存行中的數據是唯一準確的內存副本;
Exclusive ——存儲在緩存行中的數據是乾淨的(緩存行和內存中的數據相同),這是系統上緩存行中內存的唯一副本;
Shared ——存儲在高速緩存行中的數據是乾淨的,並且系統上有多個不同的高速緩存存儲相同的信息;
Invalid——在軟件層面對我們沒有任何意義,它只是意味著緩存行未使用;
現在,現代處理器使用稍微複雜一些的緩存一致性模型,但我們在這裡不做深入探討。本質上是一樣的。
需要注意的是,任何時候內核需要同步它們的緩存,它們需要訪問L2緩存,有時甚至需要訪問內存。
進入修改狀態需要大量的性能消耗。想想看,為了獲得對內存的獨占訪問,你必須有效地使用鎖來獲得控制。從硬件的角度來看,你必須告訴其他所有的核心使其對給定行的緩存無效,並且在其他核心這樣做之前你不能獲得所有權。這實際上類似於執行Mutex:lock()。
然而,從專用到修改是便宜的。這是專用MESI 狀態的全部意義所在。它允許處理器知道它在轉換到修改時不必通知其他內核,因為它已經知道是內存的唯一副本。
之所以談這個,是因為在進行IPC 時,緩存一致性很重要。至關重要的是,在我們的熱循環中,我們不會導致緩存一致性流量。從更高的層次上來說,這意味著我們需要能夠通過只讀方式對所有數據結構進行熱輪詢。這允許多個用戶線程輪詢郵箱(例如,所有用戶都在輪詢處於共享狀態的緩存行)。當產生一個完整的緩衝區時,才會消耗寫入內存的緩存一致性。運營程序刷新緩衝區的行為將導致所有用戶核心的緩存失效,並且他們必須在後續訪問時通過L2 獲取內存。此獲取也必須將運營程序的緩存狀態從modified更改為shared。
因此,我們設計了mempipe 庫來輪詢一組郵箱,該郵箱保證與正在傳輸的數據位於不同的緩存行上。如果我們將傳輸緩衝區與郵箱放在同一緩存行上,那麼將會出現嚴重的緩存不穩。