話說,盤古開天的時候,設備訪問內存(DMA)就只接受物理地址,所以CPU要把一個地址告訴設備,就只能給物理地址。但設備的地址長度還比CPU的總線長度短,所以只能分配低地址來給設備用。所以CPU這邊的接口就只有dma=dma_alloc(dev, size),分配了物理地址,然后映射為內核的va,然后把pa作為dma地址,CPU提供給設備,設備訪問這個dma地址,就得到內存里面的那個數據了。
后來設備做強了,雖然地址總線不長,但可以帶一個頁表,把它能訪問的有限長度的dma地址轉換為和物理空間一樣長的物理地址。這樣就有了dma=dma_map(dev, va)。這樣,其實我們對同一個物理地址就有三個地址的概念了:CPU看到的地址va,設備看到的地址dma,和總線看到的pa。
設備帶個頁表,這不就是mmu嗎?於是,通用的iommu的概念(硬件上)就被發明了。所以dma_map(dev, va),在有iommu的設備上,就變成了對iommu的通用頁表操作。iova=iommu_alloc(), iommu_map(domain, iova, pa);
這里我們發現了兩個新概念,一個是iova,這個很好理解,就是原來的dma地址了(io的va嘛),另一個是domain,這本質是一個頁表,為什么要把這個頁表獨立封裝出來,這個我們很快會看到的。
我這個需要提醒一句,iommu用的頁表,和mmu用的頁表,不是同一個頁表,為了容易區分,我們把前者叫做iopt,后者叫pt。兩者都可以翻譯虛擬地址為物理地址,物理地址是一樣的,都是pa,而對於va,前者我們叫iova,后者我們叫va。
又到了后來,人們需要支持虛擬化,提出了VFIO的概念,需要在用戶進程中直接訪問設備,那我們就要支持在用戶態直接發起DMA操作了,用戶態發起DMA,它自己在分配iova,直接設置下來,要求iommu就用這個iova,那我內核對這個設備做dma_map,也要分配iova。這兩者沖突怎么解決呢?
dma_map還可以避開用戶態請求過的va空間,用戶態的請求沒法避開內核的dma_map的呀。
VFIO這樣解決:默認情況下,iommu上會綁定一個default_domain,它具有IOMMU_DOMAIN_DMA屬性,原來怎么弄就怎么弄,這時你可以調用dma_map()。但如果你要用VFIO,你就要先detach原來的驅動,改用VFIO的驅動,VFIO就給你換一個domain,這個domain的屬性是IOMMU_DOMAIN_UNMANAGED,之后你愛用哪個iova就用那個iova,你自己保證不會沖突就好,VFIO通過iommu_map(domain, iova, pa)來執行這種映射。
等你從VFIO上detach,把你的domain刪除了,這個iommu就會恢復原來的default_domain,這樣你就可以繼續用你的dma API了。
這種情況下,你必須給你的設備選一種應用模式,非此即彼。
很多設備,比如GPU,沒有用VFIO,也會自行創建unmanaged的domain,自己管理映射,這個就變成一個通用的接口了。
好了,這個是Linux內核的現狀(截止到4.20)。如果基於這個現狀,我們要同時讓用戶態和內核態都可以做mapping的話,唯一的手段是使用unmanaged模式,然后va都從用戶態分配(比如通過mmap),然后統一用iommu_map完成這個映射。
但實際上,Linux的這個框架,已經落后於硬件的發展了。因為現在大部分IOMMU,都支持多進程訪問。比如我有兩個進程同時從用戶態訪問設備,他們自己管理iova,這樣,他們給iommu提供的iova就可能是沖突的。所以,IOMMU硬件同時支持多張iopt,用進程的id作為下標(對於PCIE設備來說,就是pasid了)。
這樣,我們可以讓內核使用pasid=0的iopt,每個用戶進程用pasid=xxx的iopt,這樣就互相不會沖突了。
為了支持這樣的應用模式,ARM的Jean Philipse做了一套補丁,為domain增加pasid支持。他的方法是domain上可以bind多個pasid,bind的時候給你分配一個io_mm,然后你用iommu_sva_map()帶上這個io_mm來做mapping。
這種情況下,你不再需要和dma api隔離了,因為他會自動用pasid=0(實際硬件不一定是這樣的接口,這只是比喻)的iopt來做dma api,用其他pasid來做用戶態。這時你也不再需要unmanaged的domain了。你繼續用dma的domain,然后bind一個pasid上去即可。
但Jean這個補丁上傳的時候正好遇到Intel的Scalable Virtual IO的補丁在上傳,Intel要用這個特性來實現更輕量級的VFIO。原來的VFIO,是整個設備共享給用戶態的,有了pasid這個概念,我可以基於pasid分配資源,基於pasid共享給用戶態。但Jean的補丁要求使用的時候就要bind一個pasid上來。但VFIO是要分配完設備,等有進程用這個設備的時候才能提供pasid。
為了解決這個問題,Jean又加了一個aux domain的概念,你可以給一個iommu創建一個primary domain,和多個aux domain。那些aux domain可以晚點再綁定pasid上來。
后面這個變化,和前面的接口是兼容的,對我們來說都一樣,我們只要有pasid用就可以了。
一些關鍵詞:
- DMAR - DMA重映射
- DRHD - DMA重映射硬件單元定義
- RMRR - 預留內存區域報告結構
- ZLR - 從PCI設備讀取零長度
- IOVA - IO虛擬地址。
基本的東西
ACPI枚舉並列出平台中的不同DMA引擎,以及PCI設備與DMA引擎控制它們之間的設備范圍關系。
什么是RMRR?
BIOS控制一些設備,例如USB設備執行PS2仿真。用於這些設備的存儲區域在e820映射中標記為保留。當我們打開DMA轉換時,DMA到這些區域將失敗。因此,BIOS使用RMRR指定這些區域以及需要訪問這些區域的設備。 OS希望為這些區域設置單位映射,以便這些設備訪問這些區域。
IOVA是如何產生的?
表現良好的驅動程序在向需要執行DMA的設備發送命令之前調用pci_map _ *()調用。完成DMA並且不再需要映射后,設備將執行pci_unmap _ *()調用以取消映射該區域。
Intel IOMMU驅動程序為每個域分配一個虛擬地址。每個PCIE設備都有自己的域(因此保護)。 p2p網橋下的設備與p2p網橋下的所有設備共享虛擬地址(due to transaction id aliasing for p2p bridges)。
IOVA生成非常通用。我們使用與vmalloc()相同的技術,但這些不是全局地址空間,而是針對每個域分開。不同的DMA引擎可以支持不同數量的域。
我們還為每個映射分配了保護頁面,因此我們可以嘗試捕獲可能發生的任何溢出。
硬件結構
先看下一個典型的X86物理服務器視圖:
在多路服務器上我們可以有多個DMAR Unit(這里可以直接理解為多個IOMMU硬件), 每個DMAR會負責處理其下掛載設備的DMA請求進行地址翻譯。例如上圖中, PCIE Root Port (dev:fun) (14:0)下面掛載的所有設備的DMA請求由DMAR #1負責處理, PCIE Root Port (dev:fun) (14:1)下面掛載的所有設備的DMA請求由DMAR #2負責處理, 而DMAR #3下掛載的是一個Root-Complex集成設備[29:0],這個設備的DMA請求被DMAR #3承包, DMAR #4的情況比較復雜,它負責處理Root-Complex集成設備[30:0]以及I/OxAPIC設備的DMA請求。這些和IOMMU相關的硬件拓撲信息需要BIOS通過ACPI表呈現給OS,這樣OS才能正確驅動IOMMU硬件工作。
關於硬件拓撲信息呈現,這里有幾個概念需要了解一下:
-
DRHD: DMA Remapping Hardware Unit Definition 用來描述DMAR Unit(IOMMU)的基本信息
-
RMRR: Reserved Memory Region Reporting 用來描述那些保留的物理地址,這段地址空間不被重映射
-
ATSR: Root Port ATS Capability 僅限於有Device-TLB的情形,Root Port需要向OS報告支持ATS的能力
-
RHSA: Remapping Hardware Static Affinity Remapping親和性,在有NUMA的系統下可以提升DMA Remapping的性能
BIOS通過在ACPI表中提供一套DMA Remapping Reporting Structure 信息來表述物理服務器上的IOMMU拓撲信息, 這樣OS在加載IOMMU驅動的時候就知道如何建立映射關系了
VT-d-----DMAR表組織結構



2. RMRR(Reserved Memory Region Reporting)表

3. ATSR(Root Port ATS Capability Reporting)表

4. RHSA(Remapping Hardware Status Affinity)表