Jump to content

內部機制9.png

MempipeMempipe是一種超高速的IPC機制,它使Cannoli能夠第一時間工作。它提供了一個低延遲的API,用於通過Linux上的shm*() API將緩衝區從一個進程傳輸到另一個進程。具體來說,它是一種基於輪詢的IPC機制,這意味著使用者在新數據到達之前對郵箱進行熱輪詢。

你可以在mempipe/src/lib.rs 中找到所有代碼。其核心有兩個結構,一個SendPipe 和一個RecvPipe。

Const泛型SendPipe和RecvPipe都使用兩個Const泛型,分別是CHUNK_SIZE和NUM_BUFFERS泛型。 CHUNK_SIZE以字節為單位定義每個緩衝區的大小。這個塊的大小越小,需要進行的傳輸就越多,緩存中的數據就越多。這實際上是緩衝區的大小,該緩衝區將被數據填滿,填滿時會自動刷新。

NUM_BUFFERS泛型指定內存管道中的緩衝區數量。實際上,這就是啟用來自QEMU 的非阻塞數據流的原因。當QEMU向另一個緩衝區生成數據時,用戶可以處理一個緩衝區。建議將此值設置為大於1,否則QEMU將在處理緩衝區時阻塞,但不要設置得太高,否則只會增加可能用於流媒體的內存數量,導致更多的緩存不穩定。

這兩種泛型都是可調的,會顯著影響性能。就我個人而言,我發現將CHUNK_SIZE設置為L1緩存的1/2(在大多數x86系統上是16kib),將NUM_BUFFERS設置為4似乎是一個不錯的基準。

管道創建創建一個SendPipe 很簡單。你調用SendPipe:create() 並返回一個SendPipe。在內部,它生成一個隨機的64 位數字,用作管道標識符。然後它以這個管道ID 作為文件名創建一個共享內存文件,設置共享內存的長度,並將其映射為可讀寫。我們還在共享內存中放置了一個小的標頭文件,這樣我們就可以確保當我們連接到管道時,它與我們期望的參數匹配。

打開管道創建RecvPipe 也很簡單,只需使用分配給SendPipe 的UID(可從SendPipe:uid() 獲得)調用RecvPipe:open()。然後,確保RecvPipe的Const泛型與SendPipe的Const泛型相匹配(包括在共享內存的元數據中),最後,它將映射內存並返回管道。

數據產生要從SendPipe生成數據,你需要調用SendPipe:alloc_buffer。這給了用戶一個只寫的ChunkWriter,它可以用ChunkWriter:send來寫。調用alloc_buffer會在熱循環中阻塞,直到緩衝區可用。重要的是,用戶要以盡可能快的速度使用數據,以防止發送方停頓太長時間。使用正確的可調參數,用戶應該總是領先於運營程序,因此alloc_buffer 應該立即有效地返回。

當通過alloc_buffer 獲得緩衝區時,應保證為發送進程所有,因此我們可以安全地可變地寫入它。內存是未初始化的,但沒關係,因為ChunkWriter 只提供寫入訪問,因此讀取未初始化的內存是不可能的。

數據處理在撰寫本文時,我對使用數據的最終設計並不滿意。首先,你從RecvPipe:request_ticket 請求一個票據。這有效地讓管道知道你對數據感興趣,並為你獲取將要處理的數據的唯一ID。然後,你調用RecvPipe:try_recv 來使用票據,並將返回新票據(如果數據已處理)或舊票據(如果recv 沒有任何數據)。 try_recv 是非阻塞的。如果不存在數據,則立即返回。

票據模型有點奇怪,但它允許我們循環分配用戶線程到緩衝區。這會在處理線程之間盡可能均勻地分配處理負載。它也很重要,因為它決定了正在處理的數據的順序,這對於我們有序的跟踪要求很重要。

我想找到對這個API的改進,但我還沒有這麼做,主要是因為它工作得很好,速度超級快。

QEMU補丁Cannoli 包含一些QEMU 的補丁。你可以在文件qemu_patches.patch 的repo 中找到這些內容。這些是目前最新的QEMU eec398119fc6911d99412c37af06a6bc27871f85 的補丁,但是它們被設計為在QEMU 版本之間可以移動。

這些補丁向QEMU引入了大約200行代碼。

QEMU 掛鉤當-cannoli 命令行參數傳入QEMU 時,它會觸發Cannoli 共享庫的dlopen()。然後它獲取Cannoli 條目點的地址(稱為query_version32 或query_version64)。 32 位或64 位後綴不是指共享庫本身的位數(目前所有東西都只支持x86_64 作為主機/JIT 目標),而是指被模擬的目標的位數。所有的掛鉤都設計為以不同的方式處理32 位和64 位目標,因為這會減小數據流的大小,從而在模擬32 位目標時最大限度地提高性能。

調用query_versionX 返回對Cannoli 結構的引用,該結構定義了QEMU 將在某些事件上調度的各種回調。

登記預訂因為我們將在幾乎每條目標指令上生成數據,所以我們實際上希望在寄存器中存儲少量關於跟踪緩衝區和長度的元數據。在內存中執行此操作將非常耗能,因為它將導致對每個目標指令進行多個內存訪問。

因此,我們對tcg_target_reg_alloc_order打補丁,以從QEMU寄存器調度器中刪除x86_64寄存器r12、r13和r14。這可以防止QEMU在其JIT中使用它們,從而使我們在JIT執行期間獨占地控制這些寄存器。這些寄存器是基於SYS-V ABI被調用保存的寄存器。這一點很重要,因為QEMU可以在JIT中調用C函數,我們希望確保在發生這些調用時保留寄存器。

JIT進入和退出由於我們保留了對一些寄存器的控制權,因此我們需要確保這些寄存器在QEMU JIT 進入和退出時被正確設置和保存。 JIT 條目和出口是QEMU 從運行QEMU C 代碼過渡到運行生成的JIT 代碼,再回到退出QEMU 的邊界。這些條目和出口是在tcg_target_qemu_prologue() 函數中為每個JIT-target-architecture 定義的。這有效地設置上下文、調用JIT 並恢復上下文。對於熟悉操作系統開發的人來說,這實際上是一種有效的上下文切換。

我們在這裡添加了一些掛鉤,允許我們調用Rust 共享庫中的代碼。具體來說,就是jit_entry() 和jit_exit() 函數。這些在JIT 的上下文中被調用,並提供對r12、r13 和r14 寄存器的訪問,以便可以在每次JIT 進入和退出時保存和恢復它們。

在我們的示例中,$entry 函數(cannoli/cannoli_server/src/cannoli_internals.rs) 從mempipe中分配一個緩衝區,在r12 中設置一個指向它的指針,在r13 中設置一個指向它末尾的指針,然後返回。這將通過執行JIT建立r12和r13的狀態。

$exit函數決定JIT產生的字節數(由r12中的當前指針表示,它已經是高級了),並通過IPC將數據發送給用戶。

加載和存儲對於加載和存儲,我們鉤住了tcg_out_qemu_ld()和tcg_out_qemu_st()。這些函數是特定於x86_64- jit目標的函數,它們為到來賓地址空間的內存操作提供了捕獲所有接收器(catch-all sink),用於各自的加載和存儲。

指令執行對於指令執行,我們掛鉤tcg_gen_code(),特別是INDEX_op_insnstart() QEMU TCG 指令,它表示指令開始執行的地址。

JIT shellcode 注入內存和指令掛鉤都做同樣的事情。它們在Rust代碼中調用一個回調函數,該回調函數被傳遞給qemu提供的緩衝區和長度。然後,此回調可以使用直接發送到JIT 流中的shellcode 填充QEMU 提供的緩衝區。這為我們的Rust 庫提供了將任意代碼注入JIT 流的能力。如果你是高級用戶,則可以通過為不同的指令提供不同的掛鉤來做一些非常酷的事情。

Cannoli服務器Cannoli 服務器(通過掛鉤加載到QEMU 中)已經預定義了一些掛鉤。這些是指令和內存操作掛鉤。

Cannoli 的整個流程(在其默認配置中)是在JIT 條目分配一個IPC 緩衝區,在JIT 期間填充它,如果它填滿了就刷新它,在JIT退出時也刷新它。

默認的指令和內存掛鉤執行最少的組裝,以確保跟踪緩衝區中有足夠的空間,刷新它(通過回調到Rust,如果它是滿的,它可以在這裡調用Rust,因為這些事件“很少”發生,例如。每幾千條目標指令),最後將內存或指令執行指令以相對簡單的格式存儲到跟踪中。

Cannoli 服務器共享對象包含所有掛鉤和代碼的兩個副本,這樣同一個共享對象就可以同時用於32位和64位目標,而無需重新編譯!

這些掛鉤直接寫入mempipe 提供的緩衝區,就這麼簡單!任何更複雜的內容都將嚴重損害性能!

Cannoli最後,Cannoli 本身就是一個用戶庫。由於我們每秒可能處理數十億條指令,所以我們將所有Cannoli設計成使用線程。這允許你在多個線程上執行相對複雜的跟踪使用和處理,同時不會影響QEMU單線程任務。

這很簡單。 Cannoli 創建請求的線程數,並在那些等待數據的線程上旋轉。使用mempipe 票據系統,每個線程都要排隊等待數據進入。當緩衝區出現他們的票號時,該線程處理來自QEMU 的數據。

由於並行處理意味著跟踪不再是有序的,我們允許用戶為每個事件返回他們自己的結構,然後在排序後將其返回給他們。這允許用戶進行線程處理,直到他們需要排序。

總結盡可能快地將數據從一個進程傳輸到另一個進程是一個非常困難的問題。關於處理器的詳細信息,如緩存一致性,對於獲得高吞吐量至關重要,特別是在希望盡可能防止生成線程阻塞時。

0 Comments

Recommended Comments

There are no comments to display.

Guest
Add a comment...