直接記憶體存取 (DMA) 這項功能可讓硬體在不需要 CPU 介入的情況下存取記憶體。最高等級,系統會提供要移轉的記憶體區域來源和目的地 (及其大小),並告知要複製資料。有些硬體週邊裝置甚至支援執行多項「散佈 / 收集」樣式作業,因此可以依序執行多項複製作業,不必進行額外的 CPU 介入措施。
指定行銷區域注意事項
為了確保能徹底感謝問題,請務必留意下列事項:
- 每項程序都會在虛擬位址空間中運作
- MMU 可以將連續的虛擬位址範圍對應到多個不連續的實體位址範圍 (反之亦然),
- 每項程序都有一個有限的實際位址
- 部分週邊裝置會透過輸入 / 輸出記憶體管理單元 (IOMMU) 支援他們擁有的虛擬位址。
接著就來逐一討論每個環節。
虛擬、實體和裝置實際位址
程序可存取的位址為虛擬,也就是由 CPU 記憶體管理單元 (MMU) 建立的插圖。MMU 會將虛擬地址對應至實際地址。對應精細程度是以名為「頁面大小」的參數為基礎,該參數至少為 4,000 個位元組,但新型處理器可提供較大的尺寸。
在上圖中,我們顯示了具有多個虛擬位址的特定程序 (程序 12)。MMU 負責將藍色虛擬位址對應至 CPU 實體公車位址 (紅色)。每個程序都有各自的對應,因此即使程序 12 具有虛擬位址 300
,其他程序可能也有虛擬位址 300
。該程序的虛擬位址 300
(如有) 會對應至程序 12 中的實際地址。
請注意,我們使用小小數數字做為「地址」,以求簡潔扼要。 實際上,上方顯示的每個正方形都代表一個記憶體 (4K 以上) 的頁面,並且根據 32 或 64 位元值 (視平台而定) 識別。
圖中的重點如下:
- 虛擬位址可以分配給多個群組 (其中三個是
300
-303
、420
-421
,以及770
-771
); - 虛擬運算 (例如
300
-303
) 不一定是連續的。 - 部分虛擬地址無法對應 (例如沒有虛擬地址
304
) - 部分程序可能無法取得所有實際地址 (例如,程序
12
無法存取實際地址120
)。
視平台的可用硬體而定,裝置的位址空間不一定會採用類似的翻譯。如果沒有 IOMMU,週邊裝置使用的位址就會與 CPU 使用的實體位址相同:
在上圖中,裝置的位址空間 (例如影格緩衝區或控制暫存器) 會直接顯示在 CPU 的實體位址範圍內。也就是說,裝置會透過 122
到 125
(含首尾) 佔用實際地址。
為了存取裝置的記憶體,系統需要建立從某些虛擬位址與 122
到 125
的實際地址的 MMU 對應。詳細步驟請見下方說明。
然而,使用 IOMMU 時,週邊裝置看到的位址可能與 CPU 的實體位址不同:
在本例中,裝置有自身已知的「裝置實體」地址,也就是 0
至 3
的位址 (含首尾)。IOMMU 可分別將裝置實體位址 0
到 3
對應至 CPU 實體位址 109
、110
、101
和 119
。
在這種情況下,為使用裝置記憶體的程序,必須安排兩項對應:
- 一組標記,也就是從虛擬位址空間設定的
300
至303
) 至 CPU 實體位址空間 (分別為109
、110
、101
和119
),透過 MMU 和 - 一組透過 IOMMU 範圍,從 CPU 實體位址空間 (位址
109
、110
、101
和119
) 到裝置實體位址 (0
到3
)。
雖然這看似複雜,但 Zircon 提供了抽象層,可以消除複雜度。
此外,如下圖所示,取得 IOMMU 數量的原因和提供的好處,與使用 MMU 取得的好處類似。
記憶體連續性
當您分配大量記憶體 (例如使用 calloc()) 時,程序當然會遇到大型連續的虛擬位址範圍。MMU 可能會在虛擬定址層級建立連續記憶體的錯覺,即使 MMU 可能會選擇回到該記憶體區域,並在實體位址層級提供實體不連續的記憶體,也會造成這種情況。
此外,當程序分配及釋放記憶體時,實體記憶體與虛擬位址空間的對應作業往往更加複雜,從而鼓勵出現更多「Swiss 起司」孔 (也就是對應中出現更多不連續情形)。
因此,請務必留意,連續虛擬地址不一定是連續的實體位址,而且會隨著時間的推移而變得更加寶貴。
存取權控管
MMU 的另一個好處是,程序受限於實體記憶體可視 (基於安全性和可靠性)。不過,這會影響驅動程式,而程序必須明確要求從虛擬位址空間對應至實際位址空間,並擁有執行此作業的必要權限。
伊曼
一般建議使用連續的實體記憶體。執行一次傳輸 (使用一個來源位址和一個目的地地址) 會比設定及管理多個個別傳輸作業更有效率 (每次傳輸作業之間可能需要 CPU 介入才能設定下一個傳輸作業)。
IOMMU (如果有的話) 可對週邊裝置執行相同操作 (即 CPU 的 MMU 進行程序) 來緩解此問題,透過將多個不連續的區塊對應至虛擬連續空間,對週邊裝置就知道它處理了連續位址空間的錯覺。限制對應區域後,IOMMU 也會透過與 MMU 相同的方式,防止週邊裝置存取不在目前作業「範圍內」的記憶體,藉此提供安全性。
融會貫通並靈活運用
因此,當您寫驅動程式庫時,可能會需要擔心虛擬、實體和裝置實體的地址空間。但事實並非如此。
指定行銷區域和你的驅動程式庫
Zircon 提供一組函式,可讓您明確處理上述所有項目。以下運作方式如下:
BTI 核心物件提供模型的抽象化機制,以及用於處理與 VMO 相關聯的實體 (或裝置實體) 位址的 API。
在驅動程式庫的初始化作業中,呼叫 Pci::GetBti() 以取得 BTI 控制代碼:
#include <lib/device-protocol/pci.h>
zx_status_t Pci::GetBti(uint32_t index,
zx::bti* out_bti);
GetBti() 函式位於 ddk::Pci
類別 (與上述所有其他 PCI 函式相同) 中,且接受 index
參數 (保留供日後使用,請使用 0
)。它會透過 out_bti
指標引數傳回 BTI 物件。
接下來,您需要 VMO。簡單來說,您可以將 VMO 視為指向記憶體區塊的指標,但這不只是,這是代表一組虛擬頁面的核心物件,代表一組虛擬頁面 (不一定有實體頁面),且可對應到驅動程式庫程式程序的虛擬位址空間。(還不只如此,但我們會針對其他章節進行討論)。
最終,這些網頁是 DMA 轉移的來源或目的地。
zx_vmo_create() 和 zx_vmo_create_continueiguous() 這兩個函式會分配記憶體,並將其繫結至 VMO:
zx_status_t zx_vmo_create(uint64_t size,
uint32_t options,
zx_handle_t* out);
zx_status_t zx_vmo_create_contiguous(zx_handle_t bti,
size_t size,
uint32_t alignment_log2,
zx_handle_t* out);
如您所見,兩者都會使用指出所需位元組數的 size
參數,而且兩者都會傳回 VMO (透過 out
)。兩者都會針對特定大小分配虛擬連續頁面。
請注意,這與標準 C 程式庫記憶體配置函式 (例如 malloc()) 不同,後者會分配虛擬連續記憶體,但不會受到頁面邊界的限制。舉例來說,資料列中的兩個小型 malloc() 呼叫可能會從「同一個」頁面分配兩個記憶體區域,而 VMO 建立函式一律會分配記憶體,以從新頁面開始。
zx_vmo_create_continueiguous() 函式會執行 zx_vmo_create(),「並」確保頁面適合整理,以搭配指定的 BTI 使用 (這也是為什麼需要 BTI 控制代碼)。此元素還包含 alignment_log2
參數,可用來指定最小對齊需求。顧名思義,此值必須是 2 的整數功率 (其中 0
值表示頁面對齊)。
此時,分配的記憶體有兩種「檢視畫面」:
- 一個連續的虛擬位址空間 代表從驅動程式庫的視角代表記憶體
- 一組 (可能是連續、可能承諾) 的實體網頁,以供週邊裝置使用。
使用這些網頁之前,您必須確保這些網頁出現在記憶體中 (亦即「已修訂」:您的程序可存取實體頁面),且週邊裝置可透過 IOMMU (如有) 存取這些頁面。此外,您也需要網頁位址 (從裝置的角度),以便在裝置上編寫 DMA 控制器來存取這些網頁。
zx_bti_pin() 函式可用來執行以下所有操作:
#include <zircon/syscalls.h>
zx_status_t zx_bti_pin(zx_handle_t bti, uint32_t options,
zx_handle_t vmo, uint64_t offset, uint64_t size,
zx_paddr_t* addrs, size_t addrs_count,
zx_handle_t* pmt);
這個函式有 8 個參數:
參數 | 目的 |
---|---|
bti |
這個週邊裝置的 BTI |
options |
選項 (請見下方) |
vmo |
此記憶體區域的 VMO |
offset |
與 VMO 開頭的偏移量 |
size |
VMO 中的位元組總數 |
addrs |
退貨地址清單 |
addrs_count |
addrs 中的元素數量 |
pmt |
傳回 PMT (請見下方說明) |
addrs
參數是指向您提供的 zx_paddr_t
陣列的指標。系統會在這個位置傳回各網頁的周邊裝置地址。陣列是 addrs_count
元素,且必須符合 zx_bti_pin() 預期的元素數量。
寫入
addrs
的值適合用來編寫週邊裝置的 DMA 控制器,也就是說,這些值會將 IOMMU 執行的任何轉譯 (如果有的話) 納入考量。
就技術層面而言,zx_bti_pin() 的另一項影響是,核心會確保在固定時不會停用這些網頁 (也就是移動或重複使用)。
options
引數實際上是選項的點陣圖:
選項 | 目的 |
---|---|
ZX_BTI_PERM_READ |
週邊裝置 (由驅動程式庫寫入) 皆可讀取 |
ZX_BTI_PERM_WRITE |
網頁都可以由周邊裝置寫入 (由驅動程式庫讀取) |
ZX_BTI_COMPRESS |
(請見下方的「最小連續屬性」) |
例如,請參閱上方顯示「裝置 3」的圖表。如果有 IOMMU,則 addrs
會包含 0
、1
、2
和 3
(即裝置實體地址)。如果沒有 IOMMU,則 addrs
會包含 109
、110
、101
和 119
(亦即實際地址)。
權限
請記住,權限是從週邊裝置的角度來看,而非驅動程式庫。舉例來說,在區塊裝置的 write 作業中,裝置會從記憶體「讀取」,因此驅動程式庫會指定 ZX_BTI_PERM_READ
,反之亦然。
最小連續屬性
根據預設,透過 addrs
傳回的每個地址都是一頁的內容。
如要要求更大的區塊,您可以在 options
引數中設定 ZX_BTI_COMPRESS
選項。在這種情況下,每個傳回的項目的長度都會對應至「最小連續性」屬性。雖然您無法設定這個屬性,但可以使用 zx_object_get_info() 加以讀取。實際上,最低連續屬性可確保 zx_bti_pin() 一律可傳回至少這麼多位元組的連續位址。
舉例來說,如果屬性的值值為 1 MB,則呼叫 zx_bti_pin() 時,要求的大小為 2MB,最多會傳回兩個連續執行作業。如果要求的大小為 2.5 MB,則最多會傳回三個實體連續執行作業,以此類推。
釘選的記憶體權杖 (PMT)
zx_bti_pin()
會在 pmt 引數成功時,傳回固定的記憶體權杖 (PMT)。裝置完成記憶體交易後,驅動程式必須呼叫 zx_pmt_unpin()
,才能取消固定並撤銷裝置對記憶體頁面的存取權。
進階主題
快取一致性
在完全 DMA 一致性架構中,硬體可確保 CPU 快取中的資料與主要記憶體中的資料相同,且無需任何軟體介入。並非所有架構都具有 DMA 一致。在這些系統中執行 DMA 作業之前,驅動程式庫必須針對記憶體範圍叫用適當的快取作業,確保 CPU 已一致,這樣就不會存取過時的資料。
如要在 VMO 所代表的記憶體上叫用快取作業,請使用 zx_vmo_op_range()
syscall。在周邊裝置讀取 (驅動程式庫寫入) 作業之前,請使用 ZX_VMO_OP_CACHE_CLEAN
清理快取,以便將骯髒資料寫入主要記憶體。在周邊裝置寫入 (驅動程式庫-read) 之前,請使用 ZX_VMO_OP_CACHE_CLEAN_INVALIDATE
清理快取行並將其標示為無效,確保系統會在下次存取時從主記憶體擷取資料。